From 159ea3d7ddb4cef93b0e5f08d79965204a2f19c3 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Mon, 18 May 2026 22:54:13 +0200 Subject: [PATCH] Add AI documentation source files (.ai.md + .ai.json) --- include/xrpl/basics/Archive.h.ai.json | 32 + include/xrpl/basics/Archive.h.ai.md | 31 + include/xrpl/basics/BasicConfig.h.ai.json | 324 ++++ include/xrpl/basics/BasicConfig.h.ai.md | 44 + include/xrpl/basics/Blob.h.ai.json | 14 + include/xrpl/basics/Blob.h.ai.md | 17 + include/xrpl/basics/Buffer.h.ai.json | 42 + include/xrpl/basics/Buffer.h.ai.md | 35 + include/xrpl/basics/ByteUtilities.h.ai.json | 38 + include/xrpl/basics/ByteUtilities.h.ai.md | 26 + .../basics/CompressionAlgorithms.h.ai.json | 93 ++ .../xrpl/basics/CompressionAlgorithms.h.ai.md | 53 + include/xrpl/basics/CountedObject.h.ai.json | 88 ++ include/xrpl/basics/CountedObject.h.ai.md | 55 + include/xrpl/basics/DecayingSample.h.ai.json | 115 ++ include/xrpl/basics/DecayingSample.h.ai.md | 39 + include/xrpl/basics/Expected.h.ai.json | 109 ++ include/xrpl/basics/Expected.h.ai.md | 41 + include/xrpl/basics/FileUtilities.h.ai.json | 58 + include/xrpl/basics/FileUtilities.h.ai.md | 33 + .../xrpl/basics/IntrusivePointer.h.ai.json | 115 ++ include/xrpl/basics/IntrusivePointer.h.ai.md | 52 + .../xrpl/basics/IntrusivePointer.ipp.ai.json | 698 +++++++++ .../xrpl/basics/IntrusivePointer.ipp.ai.md | 64 + .../xrpl/basics/IntrusiveRefCounts.h.ai.json | 118 ++ .../xrpl/basics/IntrusiveRefCounts.h.ai.md | 56 + include/xrpl/basics/KeyCache.h.ai.json | 14 + include/xrpl/basics/KeyCache.h.ai.md | 13 + include/xrpl/basics/LocalValue.h.ai.json | 76 + include/xrpl/basics/LocalValue.h.ai.md | 64 + include/xrpl/basics/Log.h.ai.json | 62 + include/xrpl/basics/Log.h.ai.md | 57 + include/xrpl/basics/MallocTrim.h.ai.json | 32 + include/xrpl/basics/MallocTrim.h.ai.md | 45 + include/xrpl/basics/MathUtilities.h.ai.json | 32 + include/xrpl/basics/MathUtilities.h.ai.md | 23 + include/xrpl/basics/Mutex.hpp.ai.json | 60 + include/xrpl/basics/Mutex.hpp.ai.md | 51 + include/xrpl/basics/Number.h.ai.json | 363 +++++ include/xrpl/basics/Number.h.ai.md | 54 + include/xrpl/basics/RangeSet.h.ai.json | 91 ++ include/xrpl/basics/RangeSet.h.ai.md | 39 + include/xrpl/basics/Resolver.h.ai.json | 69 + include/xrpl/basics/Resolver.h.ai.md | 33 + include/xrpl/basics/ResolverAsio.h.ai.json | 34 + include/xrpl/basics/ResolverAsio.h.ai.md | 25 + include/xrpl/basics/SHAMapHash.h.ai.json | 144 ++ include/xrpl/basics/SHAMapHash.h.ai.md | 40 + .../basics/SharedWeakCachePointer.h.ai.json | 136 ++ .../basics/SharedWeakCachePointer.h.ai.md | 43 + .../basics/SharedWeakCachePointer.ipp.ai.json | 395 +++++ .../basics/SharedWeakCachePointer.ipp.ai.md | 39 + include/xrpl/basics/SlabAllocator.h.ai.json | 110 ++ include/xrpl/basics/SlabAllocator.h.ai.md | 84 + include/xrpl/basics/Slice.h.ai.json | 86 ++ include/xrpl/basics/Slice.h.ai.md | 33 + include/xrpl/basics/StringUtilities.h.ai.json | 125 ++ include/xrpl/basics/StringUtilities.h.ai.md | 54 + include/xrpl/basics/TaggedCache.h.ai.json | 323 ++++ include/xrpl/basics/TaggedCache.h.ai.md | 69 + include/xrpl/basics/TaggedCache.ipp.ai.json | 415 +++++ include/xrpl/basics/TaggedCache.ipp.ai.md | 60 + include/xrpl/basics/ToString.h.ai.json | 71 + include/xrpl/basics/ToString.h.ai.md | 38 + .../xrpl/basics/UnorderedContainers.h.ai.json | 14 + .../xrpl/basics/UnorderedContainers.h.ai.md | 33 + include/xrpl/basics/UptimeClock.h.ai.json | 36 + include/xrpl/basics/UptimeClock.h.ai.md | 40 + include/xrpl/basics/algorithm.h.ai.json | 39 + include/xrpl/basics/algorithm.h.ai.md | 25 + include/xrpl/basics/base64.h.ai.json | 54 + include/xrpl/basics/base64.h.ai.md | 39 + include/xrpl/basics/base_uint.h.ai.json | 152 ++ include/xrpl/basics/base_uint.h.ai.md | 41 + include/xrpl/basics/chrono.h.ai.json | 71 + include/xrpl/basics/chrono.h.ai.md | 37 + include/xrpl/basics/comparators.h.ai.json | 42 + include/xrpl/basics/comparators.h.ai.md | 21 + include/xrpl/basics/contract.h.ai.json | 50 + include/xrpl/basics/contract.h.ai.md | 56 + include/xrpl/basics/hardened_hash.h.ai.json | 42 + include/xrpl/basics/hardened_hash.h.ai.md | 33 + include/xrpl/basics/join.h.ai.json | 91 ++ include/xrpl/basics/join.h.ai.md | 37 + include/xrpl/basics/make_SSLContext.h.ai.json | 53 + include/xrpl/basics/make_SSLContext.h.ai.md | 37 + include/xrpl/basics/mulDiv.h.ai.json | 37 + include/xrpl/basics/mulDiv.h.ai.md | 26 + .../partitioned_unordered_map.h.ai.json | 60 + .../basics/partitioned_unordered_map.h.ai.md | 56 + include/xrpl/basics/random.h.ai.json | 92 ++ include/xrpl/basics/random.h.ai.md | 34 + include/xrpl/basics/rocksdb.h.ai.json | 9 + include/xrpl/basics/rocksdb.h.ai.md | 34 + include/xrpl/basics/safe_cast.h.ai.json | 84 + include/xrpl/basics/safe_cast.h.ai.md | 44 + include/xrpl/basics/sanitizers.h.ai.json | 9 + include/xrpl/basics/sanitizers.h.ai.md | 15 + include/xrpl/basics/scope.h.ai.json | 46 + include/xrpl/basics/scope.h.ai.md | 33 + include/xrpl/basics/spinlock.h.ai.json | 48 + include/xrpl/basics/spinlock.h.ai.md | 43 + include/xrpl/basics/strHex.h.ai.json | 30 + include/xrpl/basics/strHex.h.ai.md | 47 + include/xrpl/basics/tagged_integer.h.ai.json | 186 +++ include/xrpl/basics/tagged_integer.h.ai.md | 77 + .../beast/asio/io_latency_probe.h.ai.json | 169 +++ .../xrpl/beast/asio/io_latency_probe.h.ai.md | 41 + .../xrpl/beast/clock/abstract_clock.h.ai.json | 71 + .../xrpl/beast/clock/abstract_clock.h.ai.md | 48 + .../beast/clock/basic_seconds_clock.h.ai.json | 28 + .../beast/clock/basic_seconds_clock.h.ai.md | 52 + .../xrpl/beast/clock/manual_clock.h.ai.json | 61 + include/xrpl/beast/clock/manual_clock.h.ai.md | 41 + .../beast/container/aged_container.h.ai.json | 25 + .../beast/container/aged_container.h.ai.md | 34 + .../aged_container_utility.h.ai.json | 32 + .../container/aged_container_utility.h.ai.md | 23 + .../xrpl/beast/container/aged_map.h.ai.json | 35 + include/xrpl/beast/container/aged_map.h.ai.md | 32 + .../beast/container/aged_multimap.h.ai.json | 35 + .../beast/container/aged_multimap.h.ai.md | 41 + .../beast/container/aged_multiset.h.ai.json | 31 + .../beast/container/aged_multiset.h.ai.md | 18 + .../xrpl/beast/container/aged_set.h.ai.json | 31 + include/xrpl/beast/container/aged_set.h.ai.md | 61 + .../container/aged_unordered_map.h.ai.json | 39 + .../container/aged_unordered_map.h.ai.md | 71 + .../aged_unordered_multimap.h.ai.json | 39 + .../container/aged_unordered_multimap.h.ai.md | 9 + .../aged_unordered_multiset.h.ai.json | 35 + .../container/aged_unordered_multiset.h.ai.md | 46 + .../container/aged_unordered_set.h.ai.json | 35 + .../container/aged_unordered_set.h.ai.md | 38 + .../aged_associative_container.h.ai.json | 61 + .../detail/aged_associative_container.h.ai.md | 19 + .../detail/aged_container_iterator.h.ai.json | 138 ++ .../detail/aged_container_iterator.h.ai.md | 56 + .../detail/aged_ordered_container.h.ai.json | 117 ++ .../detail/aged_ordered_container.h.ai.md | 79 + .../detail/aged_unordered_container.h.ai.json | 126 ++ .../detail/aged_unordered_container.h.ai.md | 86 ++ .../detail/empty_base_optimization.h.ai.json | 102 ++ .../detail/empty_base_optimization.h.ai.md | 38 + .../beast/core/CurrentThreadName.h.ai.json | 43 + .../xrpl/beast/core/CurrentThreadName.h.ai.md | 41 + include/xrpl/beast/core/LexicalCast.h.ai.json | 155 ++ include/xrpl/beast/core/LexicalCast.h.ai.md | 48 + include/xrpl/beast/core/List.h.ai.json | 245 +++ include/xrpl/beast/core/List.h.ai.md | 31 + .../xrpl/beast/core/LockFreeStack.h.ai.json | 51 + include/xrpl/beast/core/LockFreeStack.h.ai.md | 56 + .../xrpl/beast/core/SemanticVersion.h.ai.json | 80 + .../xrpl/beast/core/SemanticVersion.h.ai.md | 36 + include/xrpl/beast/hash/hash_append.h.ai.json | 702 +++++++++ include/xrpl/beast/hash/hash_append.h.ai.md | 54 + include/xrpl/beast/hash/uhash.h.ai.json | 28 + include/xrpl/beast/hash/uhash.h.ai.md | 49 + include/xrpl/beast/hash/xxhasher.h.ai.json | 60 + include/xrpl/beast/hash/xxhasher.h.ai.md | 33 + .../xrpl/beast/insight/Collector.h.ai.json | 161 ++ include/xrpl/beast/insight/Collector.h.ai.md | 29 + include/xrpl/beast/insight/Counter.h.ai.json | 98 ++ include/xrpl/beast/insight/Counter.h.ai.md | 43 + .../xrpl/beast/insight/CounterImpl.h.ai.json | 42 + .../xrpl/beast/insight/CounterImpl.h.ai.md | 31 + include/xrpl/beast/insight/Event.h.ai.json | 52 + include/xrpl/beast/insight/Event.h.ai.md | 52 + .../xrpl/beast/insight/EventImpl.h.ai.json | 42 + include/xrpl/beast/insight/EventImpl.h.ai.md | 31 + include/xrpl/beast/insight/Gauge.h.ai.json | 124 ++ include/xrpl/beast/insight/Gauge.h.ai.md | 34 + .../xrpl/beast/insight/GaugeImpl.h.ai.json | 44 + include/xrpl/beast/insight/GaugeImpl.h.ai.md | 32 + include/xrpl/beast/insight/Group.h.ai.json | 30 + include/xrpl/beast/insight/Group.h.ai.md | 34 + include/xrpl/beast/insight/Groups.h.ai.json | 59 + include/xrpl/beast/insight/Groups.h.ai.md | 27 + include/xrpl/beast/insight/Hook.h.ai.json | 50 + include/xrpl/beast/insight/Hook.h.ai.md | 27 + include/xrpl/beast/insight/HookImpl.h.ai.json | 24 + include/xrpl/beast/insight/HookImpl.h.ai.md | 40 + include/xrpl/beast/insight/Insight.h.ai.json | 9 + include/xrpl/beast/insight/Insight.h.ai.md | 26 + include/xrpl/beast/insight/Meter.h.ai.json | 88 ++ include/xrpl/beast/insight/Meter.h.ai.md | 44 + .../xrpl/beast/insight/MeterImpl.h.ai.json | 42 + include/xrpl/beast/insight/MeterImpl.h.ai.md | 41 + .../beast/insight/NullCollector.h.ai.json | 37 + .../xrpl/beast/insight/NullCollector.h.ai.md | 33 + .../beast/insight/StatsDCollector.h.ai.json | 47 + .../beast/insight/StatsDCollector.h.ai.md | 41 + include/xrpl/beast/net/IPAddress.h.ai.json | 112 ++ include/xrpl/beast/net/IPAddress.h.ai.md | 43 + .../beast/net/IPAddressConversion.h.ai.json | 70 + .../beast/net/IPAddressConversion.h.ai.md | 32 + include/xrpl/beast/net/IPAddressV4.h.ai.json | 53 + include/xrpl/beast/net/IPAddressV4.h.ai.md | 29 + include/xrpl/beast/net/IPAddressV6.h.ai.json | 42 + include/xrpl/beast/net/IPAddressV6.h.ai.md | 44 + include/xrpl/beast/net/IPEndpoint.h.ai.json | 125 ++ include/xrpl/beast/net/IPEndpoint.h.ai.md | 41 + include/xrpl/beast/rfc2616.h.ai.json | 210 +++ include/xrpl/beast/rfc2616.h.ai.md | 41 + include/xrpl/beast/test/yield_to.h.ai.json | 86 ++ include/xrpl/beast/test/yield_to.h.ai.md | 35 + include/xrpl/beast/type_name.h.ai.json | 25 + include/xrpl/beast/type_name.h.ai.md | 33 + include/xrpl/beast/unit_test.h.ai.json | 9 + include/xrpl/beast/unit_test.h.ai.md | 50 + include/xrpl/beast/unit_test/amount.h.ai.json | 84 + include/xrpl/beast/unit_test/amount.h.ai.md | 33 + .../detail/const_container.h.ai.json | 76 + .../unit_test/detail/const_container.h.ai.md | 25 + .../beast/unit_test/global_suites.h.ai.json | 66 + .../beast/unit_test/global_suites.h.ai.md | 40 + include/xrpl/beast/unit_test/match.h.ai.json | 83 + include/xrpl/beast/unit_test/match.h.ai.md | 53 + .../xrpl/beast/unit_test/recorder.h.ai.json | 95 ++ include/xrpl/beast/unit_test/recorder.h.ai.md | 48 + .../xrpl/beast/unit_test/reporter.h.ai.json | 126 ++ include/xrpl/beast/unit_test/reporter.h.ai.md | 39 + .../xrpl/beast/unit_test/results.h.ai.json | 232 +++ include/xrpl/beast/unit_test/results.h.ai.md | 43 + include/xrpl/beast/unit_test/runner.h.ai.json | 227 +++ include/xrpl/beast/unit_test/runner.h.ai.md | 43 + include/xrpl/beast/unit_test/suite.h.ai.json | 311 ++++ include/xrpl/beast/unit_test/suite.h.ai.md | 37 + .../xrpl/beast/unit_test/suite_info.h.ai.json | 68 + .../xrpl/beast/unit_test/suite_info.h.ai.md | 61 + .../xrpl/beast/unit_test/suite_list.h.ai.json | 57 + .../xrpl/beast/unit_test/suite_list.h.ai.md | 48 + include/xrpl/beast/unit_test/thread.h.ai.json | 99 ++ include/xrpl/beast/unit_test/thread.h.ai.md | 35 + include/xrpl/beast/utility/Journal.h.ai.json | 326 ++++ include/xrpl/beast/utility/Journal.h.ai.md | 53 + .../beast/utility/PropertyStream.h.ai.json | 709 +++++++++ .../xrpl/beast/utility/PropertyStream.h.ai.md | 33 + .../xrpl/beast/utility/WrappedSink.h.ai.json | 135 ++ .../xrpl/beast/utility/WrappedSink.h.ai.md | 41 + include/xrpl/beast/utility/Zero.h.ai.json | 200 +++ include/xrpl/beast/utility/Zero.h.ai.md | 45 + .../beast/utility/instrumentation.h.ai.json | 9 + .../beast/utility/instrumentation.h.ai.md | 60 + .../xrpl/beast/utility/maybe_const.h.ai.json | 20 + .../xrpl/beast/utility/maybe_const.h.ai.md | 46 + include/xrpl/beast/utility/rngfill.h.ai.json | 53 + include/xrpl/beast/utility/rngfill.h.ai.md | 48 + include/xrpl/beast/utility/temp_dir.h.ai.json | 48 + include/xrpl/beast/utility/temp_dir.h.ai.md | 23 + include/xrpl/beast/xor_shift_engine.h.ai.json | 77 + include/xrpl/beast/xor_shift_engine.h.ai.md | 57 + include/xrpl/conditions/Condition.h.ai.json | 101 ++ include/xrpl/conditions/Condition.h.ai.md | 38 + include/xrpl/conditions/Fulfillment.h.ai.json | 139 ++ include/xrpl/conditions/Fulfillment.h.ai.md | 43 + .../detail/PreimageSha256.h.ai.json | 90 ++ .../conditions/detail/PreimageSha256.h.ai.md | 39 + .../xrpl/conditions/detail/error.h.ai.json | 41 + include/xrpl/conditions/detail/error.h.ai.md | 19 + .../xrpl/conditions/detail/utils.h.ai.json | 154 ++ include/xrpl/conditions/detail/utils.h.ai.md | 33 + include/xrpl/core/ClosureCounter.h.ai.json | 138 ++ include/xrpl/core/ClosureCounter.h.ai.md | 31 + include/xrpl/core/Coro.ipp.ai.json | 369 +++++ include/xrpl/core/Coro.ipp.ai.md | 60 + include/xrpl/core/HashRouter.h.ai.json | 314 ++++ include/xrpl/core/HashRouter.h.ai.md | 45 + include/xrpl/core/Job.h.ai.json | 113 ++ include/xrpl/core/Job.h.ai.md | 42 + include/xrpl/core/JobQueue.h.ai.json | 297 ++++ include/xrpl/core/JobQueue.h.ai.md | 60 + include/xrpl/core/JobTypeData.h.ai.json | 85 ++ include/xrpl/core/JobTypeData.h.ai.md | 29 + include/xrpl/core/JobTypeInfo.h.ai.json | 57 + include/xrpl/core/JobTypeInfo.h.ai.md | 36 + include/xrpl/core/JobTypes.h.ai.json | 70 + include/xrpl/core/JobTypes.h.ai.md | 37 + include/xrpl/core/LoadEvent.h.ai.json | 84 + include/xrpl/core/LoadEvent.h.ai.md | 40 + include/xrpl/core/LoadMonitor.h.ai.json | 81 + include/xrpl/core/LoadMonitor.h.ai.md | 50 + include/xrpl/core/NetworkIDService.h.ai.json | 26 + include/xrpl/core/NetworkIDService.h.ai.md | 41 + .../xrpl/core/PeerReservationTable.h.ai.json | 127 ++ .../xrpl/core/PeerReservationTable.h.ai.md | 49 + include/xrpl/core/PerfLog.h.ai.json | 91 ++ include/xrpl/core/PerfLog.h.ai.md | 79 + include/xrpl/core/ServiceRegistry.h.ai.json | 32 + include/xrpl/core/ServiceRegistry.h.ai.md | 49 + include/xrpl/core/StartUpType.h.ai.json | 32 + include/xrpl/core/StartUpType.h.ai.md | 30 + include/xrpl/core/detail/Workers.h.ai.json | 141 ++ include/xrpl/core/detail/Workers.h.ai.md | 47 + include/xrpl/core/detail/semaphore.h.ai.json | 43 + include/xrpl/core/detail/semaphore.h.ai.md | 27 + include/xrpl/crypto/RFC1751.h.ai.json | 181 +++ include/xrpl/crypto/RFC1751.h.ai.md | 39 + include/xrpl/crypto/csprng.h.ai.json | 70 + include/xrpl/crypto/csprng.h.ai.md | 51 + include/xrpl/crypto/secure_erase.h.ai.json | 32 + include/xrpl/crypto/secure_erase.h.ai.md | 23 + include/xrpl/git/Git.h.ai.json | 25 + include/xrpl/git/Git.h.ai.md | 34 + .../xrpl/json/JsonPropertyStream.h.ai.json | 293 ++++ include/xrpl/json/JsonPropertyStream.h.ai.md | 27 + include/xrpl/json/Output.h.ai.json | 52 + include/xrpl/json/Output.h.ai.md | 39 + include/xrpl/json/Writer.h.ai.json | 191 +++ include/xrpl/json/Writer.h.ai.md | 39 + .../xrpl/json/detail/json_assert.h.ai.json | 9 + include/xrpl/json/detail/json_assert.h.ai.md | 27 + include/xrpl/json/json_errors.h.ai.json | 22 + include/xrpl/json/json_errors.h.ai.md | 41 + include/xrpl/json/json_forwards.h.ai.json | 40 + include/xrpl/json/json_forwards.h.ai.md | 9 + include/xrpl/json/json_reader.h.ai.json | 114 ++ include/xrpl/json/json_reader.h.ai.md | 36 + include/xrpl/json/json_value.h.ai.json | 160 ++ include/xrpl/json/json_value.h.ai.md | 53 + include/xrpl/json/json_writer.h.ai.json | 169 +++ include/xrpl/json/json_writer.h.ai.md | 37 + include/xrpl/json/to_string.h.ai.json | 37 + include/xrpl/json/to_string.h.ai.md | 23 + .../xrpl/ledger/AcceptedLedgerTx.h.ai.json | 92 ++ include/xrpl/ledger/AcceptedLedgerTx.h.ai.md | 39 + include/xrpl/ledger/AmendmentTable.h.ai.json | 309 ++++ include/xrpl/ledger/AmendmentTable.h.ai.md | 49 + include/xrpl/ledger/ApplyView.h.ai.json | 171 +++ include/xrpl/ledger/ApplyView.h.ai.md | 97 ++ include/xrpl/ledger/ApplyViewImpl.h.ai.json | 93 ++ include/xrpl/ledger/ApplyViewImpl.h.ai.md | 35 + include/xrpl/ledger/BookDirs.h.ai.json | 100 ++ include/xrpl/ledger/BookDirs.h.ai.md | 37 + include/xrpl/ledger/BookListeners.h.ai.json | 48 + include/xrpl/ledger/BookListeners.h.ai.md | 41 + include/xrpl/ledger/CachedSLEs.h.ai.json | 14 + include/xrpl/ledger/CachedSLEs.h.ai.md | 17 + include/xrpl/ledger/CachedView.h.ai.json | 188 +++ include/xrpl/ledger/CachedView.h.ai.md | 51 + include/xrpl/ledger/CanonicalTXSet.h.ai.json | 191 +++ include/xrpl/ledger/CanonicalTXSet.h.ai.md | 50 + include/xrpl/ledger/Dir.h.ai.json | 121 ++ include/xrpl/ledger/Dir.h.ai.md | 51 + include/xrpl/ledger/Ledger.h.ai.json | 418 +++++ include/xrpl/ledger/Ledger.h.ai.md | 73 + include/xrpl/ledger/LedgerTiming.h.ai.json | 41 + include/xrpl/ledger/LedgerTiming.h.ai.md | 57 + include/xrpl/ledger/OpenView.h.ai.json | 162 ++ include/xrpl/ledger/OpenView.h.ai.md | 52 + include/xrpl/ledger/OrderBookDB.h.ai.json | 107 ++ include/xrpl/ledger/OrderBookDB.h.ai.md | 37 + include/xrpl/ledger/PaymentSandbox.h.ai.json | 304 ++++ include/xrpl/ledger/PaymentSandbox.h.ai.md | 41 + include/xrpl/ledger/PendingSaves.h.ai.json | 55 + include/xrpl/ledger/PendingSaves.h.ai.md | 35 + include/xrpl/ledger/RawView.h.ai.json | 92 ++ include/xrpl/ledger/RawView.h.ai.md | 45 + include/xrpl/ledger/ReadView.h.ai.json | 42 + include/xrpl/ledger/ReadView.h.ai.md | 77 + include/xrpl/ledger/Sandbox.h.ai.json | 53 + include/xrpl/ledger/Sandbox.h.ai.md | 36 + include/xrpl/ledger/View.h.ai.json | 397 +++++ include/xrpl/ledger/View.h.ai.md | 61 + .../ledger/detail/ApplyStateTable.h.ai.json | 218 +++ .../ledger/detail/ApplyStateTable.h.ai.md | 54 + .../ledger/detail/ApplyViewBase.h.ai.json | 180 +++ .../xrpl/ledger/detail/ApplyViewBase.h.ai.md | 47 + .../ledger/detail/RawStateTable.h.ai.json | 161 ++ .../xrpl/ledger/detail/RawStateTable.h.ai.md | 49 + .../ledger/detail/ReadViewFwdRange.h.ai.json | 184 +++ .../ledger/detail/ReadViewFwdRange.h.ai.md | 33 + .../detail/ReadViewFwdRange.ipp.ai.json | 406 +++++ .../ledger/detail/ReadViewFwdRange.ipp.ai.md | 35 + .../xrpl/ledger/helpers/AMMHelpers.h.ai.json | 625 ++++++++ .../xrpl/ledger/helpers/AMMHelpers.h.ai.md | 61 + .../helpers/AccountRootHelpers.h.ai.json | 155 ++ .../ledger/helpers/AccountRootHelpers.h.ai.md | 35 + .../helpers/CredentialHelpers.h.ai.json | 119 ++ .../ledger/helpers/CredentialHelpers.h.ai.md | 54 + .../ledger/helpers/DelegateHelpers.h.ai.json | 32 + .../ledger/helpers/DelegateHelpers.h.ai.md | 35 + .../ledger/helpers/DirectoryHelpers.h.ai.json | 142 ++ .../ledger/helpers/DirectoryHelpers.h.ai.md | 37 + .../ledger/helpers/EscrowHelpers.h.ai.json | 47 + .../xrpl/ledger/helpers/EscrowHelpers.h.ai.md | 37 + .../ledger/helpers/MPTokenHelpers.h.ai.json | 243 +++ .../ledger/helpers/MPTokenHelpers.h.ai.md | 47 + .../ledger/helpers/NFTokenHelpers.h.ai.json | 300 ++++ .../ledger/helpers/NFTokenHelpers.h.ai.md | 43 + .../ledger/helpers/OfferHelpers.h.ai.json | 37 + .../xrpl/ledger/helpers/OfferHelpers.h.ai.md | 38 + .../helpers/PaymentChannelHelpers.h.ai.json | 42 + .../helpers/PaymentChannelHelpers.h.ai.md | 51 + .../helpers/PermissionedDEXHelpers.h.ai.json | 59 + .../helpers/PermissionedDEXHelpers.h.ai.md | 43 + .../helpers/RippleStateHelpers.h.ai.json | 382 +++++ .../ledger/helpers/RippleStateHelpers.h.ai.md | 63 + .../ledger/helpers/TokenHelpers.h.ai.json | 434 ++++++ .../xrpl/ledger/helpers/TokenHelpers.h.ai.md | 60 + .../ledger/helpers/VaultHelpers.h.ai.json | 111 ++ .../xrpl/ledger/helpers/VaultHelpers.h.ai.md | 45 + include/xrpl/net/AutoSocket.h.ai.json | 221 +++ include/xrpl/net/AutoSocket.h.ai.md | 51 + include/xrpl/net/HTTPClient.h.ai.json | 86 ++ include/xrpl/net/HTTPClient.h.ai.md | 49 + .../xrpl/net/HTTPClientSSLContext.h.ai.json | 116 ++ include/xrpl/net/HTTPClientSSLContext.h.ai.md | 50 + include/xrpl/net/RegisterSSLCerts.h.ai.json | 24 + include/xrpl/net/RegisterSSLCerts.h.ai.md | 29 + include/xrpl/nodestore/Backend.h.ai.json | 173 +++ include/xrpl/nodestore/Backend.h.ai.md | 47 + include/xrpl/nodestore/Database.h.ai.json | 300 ++++ include/xrpl/nodestore/Database.h.ai.md | 70 + .../xrpl/nodestore/DatabaseRotating.h.ai.json | 63 + .../xrpl/nodestore/DatabaseRotating.h.ai.md | 42 + .../xrpl/nodestore/DummyScheduler.h.ai.json | 56 + include/xrpl/nodestore/DummyScheduler.h.ai.md | 41 + include/xrpl/nodestore/Factory.h.ai.json | 98 ++ include/xrpl/nodestore/Factory.h.ai.md | 38 + include/xrpl/nodestore/Manager.h.ai.json | 72 + include/xrpl/nodestore/Manager.h.ai.md | 29 + include/xrpl/nodestore/NodeObject.h.ai.json | 65 + include/xrpl/nodestore/NodeObject.h.ai.md | 38 + include/xrpl/nodestore/Scheduler.h.ai.json | 41 + include/xrpl/nodestore/Scheduler.h.ai.md | 43 + include/xrpl/nodestore/Task.h.ai.json | 30 + include/xrpl/nodestore/Task.h.ai.md | 11 + include/xrpl/nodestore/Types.h.ai.json | 18 + include/xrpl/nodestore/Types.h.ai.md | 9 + .../nodestore/detail/BatchWriter.h.ai.json | 97 ++ .../xrpl/nodestore/detail/BatchWriter.h.ai.md | 41 + .../detail/DatabaseNodeImp.h.ai.json | 181 +++ .../nodestore/detail/DatabaseNodeImp.h.ai.md | 31 + .../detail/DatabaseRotatingImp.h.ai.json | 171 +++ .../detail/DatabaseRotatingImp.h.ai.md | 75 + .../nodestore/detail/DecodedBlob.h.ai.json | 61 + .../xrpl/nodestore/detail/DecodedBlob.h.ai.md | 51 + .../nodestore/detail/EncodedBlob.h.ai.json | 59 + .../xrpl/nodestore/detail/EncodedBlob.h.ai.md | 70 + .../nodestore/detail/ManagerImp.h.ai.json | 87 ++ .../xrpl/nodestore/detail/ManagerImp.h.ai.md | 35 + include/xrpl/nodestore/detail/codec.h.ai.json | 81 + include/xrpl/nodestore/detail/codec.h.ai.md | 64 + .../xrpl/nodestore/detail/varint.h.ai.json | 112 ++ include/xrpl/nodestore/detail/varint.h.ai.md | 59 + include/xrpl/protocol/AMMCore.h.ai.json | 187 +++ include/xrpl/protocol/AMMCore.h.ai.md | 54 + include/xrpl/protocol/AccountID.h.ai.json | 135 ++ include/xrpl/protocol/AccountID.h.ai.md | 68 + .../xrpl/protocol/AmountConversions.h.ai.json | 293 ++++ .../xrpl/protocol/AmountConversions.h.ai.md | 80 + include/xrpl/protocol/ApiVersion.h.ai.json | 81 + include/xrpl/protocol/ApiVersion.h.ai.md | 45 + include/xrpl/protocol/Asset.h.ai.json | 303 ++++ include/xrpl/protocol/Asset.h.ai.md | 71 + include/xrpl/protocol/Batch.h.ai.json | 37 + include/xrpl/protocol/Batch.h.ai.md | 43 + include/xrpl/protocol/Book.h.ai.json | 82 + include/xrpl/protocol/Book.h.ai.md | 52 + include/xrpl/protocol/BuildInfo.h.ai.json | 68 + include/xrpl/protocol/BuildInfo.h.ai.md | 41 + include/xrpl/protocol/Concepts.h.ai.json | 42 + include/xrpl/protocol/Concepts.h.ai.md | 79 + include/xrpl/protocol/ErrorCodes.h.ai.json | 199 +++ include/xrpl/protocol/ErrorCodes.h.ai.md | 48 + include/xrpl/protocol/Feature.h.ai.json | 128 ++ include/xrpl/protocol/Feature.h.ai.md | 51 + include/xrpl/protocol/Fees.h.ai.json | 49 + include/xrpl/protocol/Fees.h.ai.md | 57 + include/xrpl/protocol/HashPrefix.h.ai.json | 57 + include/xrpl/protocol/HashPrefix.h.ai.md | 69 + include/xrpl/protocol/IOUAmount.h.ai.json | 102 ++ include/xrpl/protocol/IOUAmount.h.ai.md | 45 + include/xrpl/protocol/Indexes.h.ai.json | 714 +++++++++ include/xrpl/protocol/Indexes.h.ai.md | 41 + .../protocol/InnerObjectFormats.h.ai.json | 33 + .../xrpl/protocol/InnerObjectFormats.h.ai.md | 39 + include/xrpl/protocol/Issue.h.ai.json | 111 ++ include/xrpl/protocol/Issue.h.ai.md | 63 + include/xrpl/protocol/KeyType.h.ai.json | 54 + include/xrpl/protocol/KeyType.h.ai.md | 45 + include/xrpl/protocol/Keylet.h.ai.json | 39 + include/xrpl/protocol/Keylet.h.ai.md | 45 + include/xrpl/protocol/KnownFormats.h.ai.json | 104 ++ include/xrpl/protocol/KnownFormats.h.ai.md | 42 + include/xrpl/protocol/LedgerFormats.h.ai.json | 86 ++ include/xrpl/protocol/LedgerFormats.h.ai.md | 50 + include/xrpl/protocol/LedgerHeader.h.ai.json | 60 + include/xrpl/protocol/LedgerHeader.h.ai.md | 58 + .../xrpl/protocol/LedgerShortcut.h.ai.json | 14 + include/xrpl/protocol/LedgerShortcut.h.ai.md | 25 + include/xrpl/protocol/MPTAmount.h.ai.json | 56 + include/xrpl/protocol/MPTAmount.h.ai.md | 51 + include/xrpl/protocol/MPTIssue.h.ai.json | 177 +++ include/xrpl/protocol/MPTIssue.h.ai.md | 67 + include/xrpl/protocol/MultiApiJson.h.ai.json | 80 + include/xrpl/protocol/MultiApiJson.h.ai.md | 80 + .../protocol/NFTSyntheticSerializer.h.ai.json | 28 + .../protocol/NFTSyntheticSerializer.h.ai.md | 26 + include/xrpl/protocol/NFTokenID.h.ai.json | 46 + include/xrpl/protocol/NFTokenID.h.ai.md | 23 + .../xrpl/protocol/NFTokenOfferID.h.ai.json | 39 + include/xrpl/protocol/NFTokenOfferID.h.ai.md | 32 + include/xrpl/protocol/PathAsset.h.ai.json | 157 ++ include/xrpl/protocol/PathAsset.h.ai.md | 33 + include/xrpl/protocol/PayChan.h.ai.json | 37 + include/xrpl/protocol/PayChan.h.ai.md | 48 + include/xrpl/protocol/Permissions.h.ai.json | 83 + include/xrpl/protocol/Permissions.h.ai.md | 74 + include/xrpl/protocol/Protocol.h.ai.json | 96 ++ include/xrpl/protocol/Protocol.h.ai.md | 71 + include/xrpl/protocol/PublicKey.h.ai.json | 217 +++ include/xrpl/protocol/PublicKey.h.ai.md | 50 + include/xrpl/protocol/Quality.h.ai.json | 103 ++ include/xrpl/protocol/Quality.h.ai.md | 59 + .../xrpl/protocol/QualityFunction.h.ai.json | 88 ++ include/xrpl/protocol/QualityFunction.h.ai.md | 81 + include/xrpl/protocol/RPCErr.h.ai.json | 29 + include/xrpl/protocol/RPCErr.h.ai.md | 9 + include/xrpl/protocol/Rate.h.ai.json | 145 ++ include/xrpl/protocol/Rate.h.ai.md | 39 + .../xrpl/protocol/RippleLedgerHash.h.ai.json | 14 + .../xrpl/protocol/RippleLedgerHash.h.ai.md | 27 + include/xrpl/protocol/Rules.h.ai.json | 78 + include/xrpl/protocol/Rules.h.ai.md | 53 + include/xrpl/protocol/SField.h.ai.json | 185 +++ include/xrpl/protocol/SField.h.ai.md | 67 + include/xrpl/protocol/SOTemplate.h.ai.json | 115 ++ include/xrpl/protocol/SOTemplate.h.ai.md | 76 + include/xrpl/protocol/STAccount.h.ai.json | 180 +++ include/xrpl/protocol/STAccount.h.ai.md | 49 + include/xrpl/protocol/STAmount.h.ai.json | 334 ++++ include/xrpl/protocol/STAmount.h.ai.md | 50 + include/xrpl/protocol/STArray.h.ai.json | 304 ++++ include/xrpl/protocol/STArray.h.ai.md | 39 + include/xrpl/protocol/STBase.h.ai.json | 124 ++ include/xrpl/protocol/STBase.h.ai.md | 50 + include/xrpl/protocol/STBitString.h.ai.json | 179 +++ include/xrpl/protocol/STBitString.h.ai.md | 42 + include/xrpl/protocol/STBlob.h.ai.json | 141 ++ include/xrpl/protocol/STBlob.h.ai.md | 46 + include/xrpl/protocol/STCurrency.h.ai.json | 86 ++ include/xrpl/protocol/STCurrency.h.ai.md | 44 + include/xrpl/protocol/STExchange.h.ai.json | 98 ++ include/xrpl/protocol/STExchange.h.ai.md | 48 + include/xrpl/protocol/STInteger.h.ai.json | 181 +++ include/xrpl/protocol/STInteger.h.ai.md | 50 + include/xrpl/protocol/STIssue.h.ai.json | 177 +++ include/xrpl/protocol/STIssue.h.ai.md | 42 + include/xrpl/protocol/STLedgerEntry.h.ai.json | 217 +++ include/xrpl/protocol/STLedgerEntry.h.ai.md | 41 + include/xrpl/protocol/STNumber.h.ai.json | 49 + include/xrpl/protocol/STNumber.h.ai.md | 37 + include/xrpl/protocol/STObject.h.ai.json | 243 +++ include/xrpl/protocol/STObject.h.ai.md | 66 + include/xrpl/protocol/STParsedJSON.h.ai.json | 31 + include/xrpl/protocol/STParsedJSON.h.ai.md | 44 + include/xrpl/protocol/STPathSet.h.ai.json | 395 +++++ include/xrpl/protocol/STPathSet.h.ai.md | 52 + include/xrpl/protocol/STTakesAsset.h.ai.json | 49 + include/xrpl/protocol/STTakesAsset.h.ai.md | 40 + include/xrpl/protocol/STTx.h.ai.json | 58 + include/xrpl/protocol/STTx.h.ai.md | 61 + include/xrpl/protocol/STValidation.h.ai.json | 102 ++ include/xrpl/protocol/STValidation.h.ai.md | 35 + include/xrpl/protocol/STVector256.h.ai.json | 217 +++ include/xrpl/protocol/STVector256.h.ai.md | 39 + .../xrpl/protocol/STXChainBridge.h.ai.json | 342 +++++ include/xrpl/protocol/STXChainBridge.h.ai.md | 53 + include/xrpl/protocol/SecretKey.h.ai.json | 179 +++ include/xrpl/protocol/SecretKey.h.ai.md | 41 + include/xrpl/protocol/Seed.h.ai.json | 143 ++ include/xrpl/protocol/Seed.h.ai.md | 47 + include/xrpl/protocol/SeqProxy.h.ai.json | 131 ++ include/xrpl/protocol/SeqProxy.h.ai.md | 38 + include/xrpl/protocol/Serializer.h.ai.json | 521 +++++++ include/xrpl/protocol/Serializer.h.ai.md | 51 + include/xrpl/protocol/Sign.h.ai.json | 96 ++ include/xrpl/protocol/Sign.h.ai.md | 50 + .../xrpl/protocol/SystemParameters.h.ai.json | 39 + .../xrpl/protocol/SystemParameters.h.ai.md | 37 + include/xrpl/protocol/TER.h.ai.json | 271 ++++ include/xrpl/protocol/TER.h.ai.md | 57 + include/xrpl/protocol/TxFlags.h.ai.json | 130 ++ include/xrpl/protocol/TxFlags.h.ai.md | 50 + include/xrpl/protocol/TxFormats.h.ai.json | 31 + include/xrpl/protocol/TxFormats.h.ai.md | 43 + include/xrpl/protocol/TxMeta.h.ai.json | 136 ++ include/xrpl/protocol/TxMeta.h.ai.md | 50 + include/xrpl/protocol/TxSearched.h.ai.json | 14 + include/xrpl/protocol/TxSearched.h.ai.md | 53 + include/xrpl/protocol/UintTypes.h.ai.json | 120 ++ include/xrpl/protocol/UintTypes.h.ai.md | 45 + include/xrpl/protocol/Units.h.ai.json | 143 ++ include/xrpl/protocol/Units.h.ai.md | 66 + .../protocol/XChainAttestations.h.ai.json | 220 +++ .../xrpl/protocol/XChainAttestations.h.ai.md | 45 + include/xrpl/protocol/XRPAmount.h.ai.json | 51 + include/xrpl/protocol/XRPAmount.h.ai.md | 75 + include/xrpl/protocol/detail/STVar.h.ai.json | 280 ++++ include/xrpl/protocol/detail/STVar.h.ai.md | 47 + .../xrpl/protocol/detail/b58_utils.h.ai.json | 84 + .../xrpl/protocol/detail/b58_utils.h.ai.md | 46 + .../xrpl/protocol/detail/secp256k1.h.ai.json | 26 + .../xrpl/protocol/detail/secp256k1.h.ai.md | 23 + .../protocol/detail/token_errors.h.ai.json | 50 + .../xrpl/protocol/detail/token_errors.h.ai.md | 33 + include/xrpl/protocol/digest.h.ai.json | 100 ++ include/xrpl/protocol/digest.h.ai.md | 35 + .../xrpl/protocol/json_get_or_throw.h.ai.json | 100 ++ .../xrpl/protocol/json_get_or_throw.h.ai.md | 66 + include/xrpl/protocol/jss.h.ai.json | 18 + include/xrpl/protocol/jss.h.ai.md | 65 + include/xrpl/protocol/messages.h.ai.json | 9 + include/xrpl/protocol/messages.h.ai.md | 25 + include/xrpl/protocol/nft.h.ai.json | 119 ++ include/xrpl/protocol/nft.h.ai.md | 68 + include/xrpl/protocol/nftPageMask.h.ai.json | 18 + include/xrpl/protocol/nftPageMask.h.ai.md | 54 + include/xrpl/protocol/serialize.h.ai.json | 29 + include/xrpl/protocol/serialize.h.ai.md | 25 + include/xrpl/protocol/st.h.ai.json | 9 + include/xrpl/protocol/st.h.ai.md | 47 + include/xrpl/protocol/tokens.h.ai.json | 161 ++ include/xrpl/protocol/tokens.h.ai.md | 57 + .../LedgerEntryBase.h.ai.json | 75 + .../protocol_autogen/LedgerEntryBase.h.ai.md | 38 + .../LedgerEntryBuilderBase.h.ai.json | 70 + .../LedgerEntryBuilderBase.h.ai.md | 42 + .../STObjectValidation.h.ai.json | 32 + .../STObjectValidation.h.ai.md | 33 + .../TransactionBase.h.ai.json | 196 +++ .../protocol_autogen/TransactionBase.h.ai.md | 43 + .../TransactionBuilderBase.h.ai.json | 216 +++ .../TransactionBuilderBase.h.ai.md | 73 + include/xrpl/protocol_autogen/Utils.h.ai.json | 14 + include/xrpl/protocol_autogen/Utils.h.ai.md | 50 + .../ledger_entries/AMM.h.ai.json | 284 ++++ .../ledger_entries/AMM.h.ai.md | 49 + .../ledger_entries/AccountRoot.h.ai.json | 528 +++++++ .../ledger_entries/AccountRoot.h.ai.md | 39 + .../ledger_entries/Amendments.h.ai.json | 122 ++ .../ledger_entries/Amendments.h.ai.md | 31 + .../ledger_entries/Bridge.h.ai.json | 192 +++ .../ledger_entries/Bridge.h.ai.md | 47 + .../ledger_entries/Check.h.ai.json | 30 + .../ledger_entries/Check.h.ai.md | 40 + .../ledger_entries/Credential.h.ai.json | 257 ++++ .../ledger_entries/Credential.h.ai.md | 39 + .../ledger_entries/DID.h.ai.json | 194 +++ .../ledger_entries/DID.h.ai.md | 46 + .../ledger_entries/Delegate.h.ai.json | 197 +++ .../ledger_entries/Delegate.h.ai.md | 33 + .../ledger_entries/DepositPreauth.h.ai.json | 199 +++ .../ledger_entries/DepositPreauth.h.ai.md | 43 + .../ledger_entries/DirectoryNode.h.ai.json | 407 +++++ .../ledger_entries/DirectoryNode.h.ai.md | 36 + .../ledger_entries/Escrow.h.ai.json | 364 +++++ .../ledger_entries/Escrow.h.ai.md | 43 + .../ledger_entries/FeeSettings.h.ai.json | 195 +++ .../ledger_entries/FeeSettings.h.ai.md | 44 + .../ledger_entries/LedgerHashes.h.ai.json | 113 ++ .../ledger_entries/LedgerHashes.h.ai.md | 43 + .../ledger_entries/Loan.h.ai.json | 485 ++++++ .../ledger_entries/Loan.h.ai.md | 51 + .../ledger_entries/LoanBroker.h.ai.json | 435 ++++++ .../ledger_entries/LoanBroker.h.ai.md | 52 + .../ledger_entries/MPToken.h.ai.json | 196 +++ .../ledger_entries/MPToken.h.ai.md | 39 + .../ledger_entries/MPTokenIssuance.h.ai.json | 322 ++++ .../ledger_entries/MPTokenIssuance.h.ai.md | 53 + .../ledger_entries/NFTokenOffer.h.ai.json | 238 +++ .../ledger_entries/NFTokenOffer.h.ai.md | 45 + .../ledger_entries/NFTokenPage.h.ai.json | 154 ++ .../ledger_entries/NFTokenPage.h.ai.md | 52 + .../ledger_entries/NegativeUNL.h.ai.json | 139 ++ .../ledger_entries/NegativeUNL.h.ai.md | 37 + .../ledger_entries/Offer.h.ai.json | 204 +++ .../ledger_entries/Offer.h.ai.md | 49 + .../ledger_entries/Oracle.h.ai.json | 259 ++++ .../ledger_entries/Oracle.h.ai.md | 44 + .../ledger_entries/PayChannel.h.ai.json | 364 +++++ .../ledger_entries/PayChannel.h.ai.md | 35 + .../PermissionedDomain.h.ai.json | 171 +++ .../ledger_entries/PermissionedDomain.h.ai.md | 42 + .../ledger_entries/RippleState.h.ai.json | 203 +++ .../ledger_entries/RippleState.h.ai.md | 37 + .../ledger_entries/SignerList.h.ai.json | 196 +++ .../ledger_entries/SignerList.h.ai.md | 41 + .../ledger_entries/Ticket.h.ai.json | 151 ++ .../ledger_entries/Ticket.h.ai.md | 40 + .../ledger_entries/Vault.h.ai.json | 364 +++++ .../ledger_entries/Vault.h.ai.md | 49 + .../XChainOwnedClaimID.h.ai.json | 231 +++ .../ledger_entries/XChainOwnedClaimID.h.ai.md | 44 + .../XChainOwnedCreateAccountClaimID.h.ai.json | 196 +++ .../XChainOwnedCreateAccountClaimID.h.ai.md | 40 + .../transactions/AMMBid.h.ai.json | 196 +++ .../transactions/AMMBid.h.ai.md | 51 + .../transactions/AMMClawback.h.ai.json | 163 ++ .../transactions/AMMClawback.h.ai.md | 42 + .../transactions/AMMCreate.h.ai.json | 128 ++ .../transactions/AMMCreate.h.ai.md | 39 + .../transactions/AMMDelete.h.ai.json | 114 ++ .../transactions/AMMDelete.h.ai.md | 31 + .../transactions/AMMDeposit.h.ai.json | 213 +++ .../transactions/AMMDeposit.h.ai.md | 45 + .../transactions/AMMVote.h.ai.json | 142 ++ .../transactions/AMMVote.h.ai.md | 41 + .../transactions/AMMWithdraw.h.ai.json | 217 +++ .../transactions/AMMWithdraw.h.ai.md | 56 + .../transactions/AccountDelete.h.ai.json | 154 ++ .../transactions/AccountDelete.h.ai.md | 68 + .../transactions/AccountSet.h.ai.json | 301 ++++ .../transactions/AccountSet.h.ai.md | 35 + .../transactions/Batch.h.ai.json | 109 ++ .../transactions/Batch.h.ai.md | 35 + .../transactions/CheckCancel.h.ai.json | 88 ++ .../transactions/CheckCancel.h.ai.md | 46 + .../transactions/CheckCash.h.ai.json | 130 ++ .../transactions/CheckCash.h.ai.md | 63 + .../transactions/CheckCreate.h.ai.json | 151 ++ .../transactions/CheckCreate.h.ai.md | 37 + .../transactions/Clawback.h.ai.json | 105 ++ .../transactions/Clawback.h.ai.md | 53 + .../transactions/CredentialAccept.h.ai.json | 104 ++ .../transactions/CredentialAccept.h.ai.md | 48 + .../transactions/CredentialCreate.h.ai.json | 159 ++ .../transactions/CredentialCreate.h.ai.md | 56 + .../transactions/CredentialDelete.h.ai.json | 130 ++ .../transactions/CredentialDelete.h.ai.md | 39 + .../transactions/DIDDelete.h.ai.json | 92 ++ .../transactions/DIDDelete.h.ai.md | 37 + .../transactions/DIDSet.h.ai.json | 131 ++ .../transactions/DIDSet.h.ai.md | 38 + .../transactions/DelegateSet.h.ai.json | 104 ++ .../transactions/DelegateSet.h.ai.md | 30 + .../transactions/DepositPreauth.h.ai.json | 175 +++ .../transactions/DepositPreauth.h.ai.md | 42 + .../transactions/EnableAmendment.h.ai.json | 100 ++ .../transactions/EnableAmendment.h.ai.md | 36 + .../transactions/EscrowCancel.h.ai.json | 125 ++ .../transactions/EscrowCancel.h.ai.md | 50 + .../transactions/EscrowCreate.h.ai.json | 192 +++ .../transactions/EscrowCreate.h.ai.md | 44 + .../transactions/EscrowFinish.h.ai.json | 195 +++ .../transactions/EscrowFinish.h.ai.md | 48 + .../transactions/LedgerStateFix.h.ai.json | 109 ++ .../transactions/LedgerStateFix.h.ai.md | 52 + .../LoanBrokerCoverClawback.h.ai.json | 133 ++ .../LoanBrokerCoverClawback.h.ai.md | 35 + .../LoanBrokerCoverDeposit.h.ai.json | 100 ++ .../LoanBrokerCoverDeposit.h.ai.md | 38 + .../LoanBrokerCoverWithdraw.h.ai.json | 122 ++ .../LoanBrokerCoverWithdraw.h.ai.md | 53 + .../transactions/LoanBrokerDelete.h.ai.json | 112 ++ .../transactions/LoanBrokerDelete.h.ai.md | 41 + .../transactions/LoanBrokerSet.h.ai.json | 210 +++ .../transactions/LoanBrokerSet.h.ai.md | 47 + .../transactions/LoanDelete.h.ai.json | 112 ++ .../transactions/LoanDelete.h.ai.md | 31 + .../transactions/LoanManage.h.ai.json | 114 ++ .../transactions/LoanManage.h.ai.md | 41 + .../transactions/LoanPay.h.ai.json | 125 ++ .../transactions/LoanPay.h.ai.md | 42 + .../transactions/LoanSet.h.ai.json | 448 ++++++ .../transactions/LoanSet.h.ai.md | 72 + .../transactions/MPTokenAuthorize.h.ai.json | 101 ++ .../transactions/MPTokenAuthorize.h.ai.md | 45 + .../MPTokenIssuanceCreate.h.ai.json | 194 +++ .../MPTokenIssuanceCreate.h.ai.md | 36 + .../MPTokenIssuanceDestroy.h.ai.json | 112 ++ .../MPTokenIssuanceDestroy.h.ai.md | 35 + .../transactions/MPTokenIssuanceSet.h.ai.json | 217 +++ .../transactions/MPTokenIssuanceSet.h.ai.md | 37 + .../transactions/NFTokenAcceptOffer.h.ai.json | 119 ++ .../transactions/NFTokenAcceptOffer.h.ai.md | 54 + .../transactions/NFTokenBurn.h.ai.json | 101 ++ .../transactions/NFTokenBurn.h.ai.md | 36 + .../transactions/NFTokenCancelOffer.h.ai.json | 84 + .../transactions/NFTokenCancelOffer.h.ai.md | 35 + .../transactions/NFTokenCreateOffer.h.ai.json | 139 ++ .../transactions/NFTokenCreateOffer.h.ai.md | 47 + .../transactions/NFTokenMint.h.ai.json | 186 +++ .../transactions/NFTokenMint.h.ai.md | 55 + .../transactions/NFTokenModify.h.ai.json | 130 ++ .../transactions/NFTokenModify.h.ai.md | 47 + .../transactions/OfferCancel.h.ai.json | 88 ++ .../transactions/OfferCancel.h.ai.md | 73 + .../transactions/OfferCreate.h.ai.json | 151 ++ .../transactions/OfferCreate.h.ai.md | 41 + .../transactions/OracleDelete.h.ai.json | 112 ++ .../transactions/OracleDelete.h.ai.md | 40 + .../transactions/OracleSet.h.ai.json | 210 +++ .../transactions/OracleSet.h.ai.md | 72 + .../transactions/Payment.h.ai.json | 255 ++++ .../transactions/Payment.h.ai.md | 49 + .../PaymentChannelClaim.h.ai.json | 213 +++ .../transactions/PaymentChannelClaim.h.ai.md | 38 + .../PaymentChannelCreate.h.ai.json | 193 +++ .../transactions/PaymentChannelCreate.h.ai.md | 55 + .../transactions/PaymentChannelFund.h.ai.json | 129 ++ .../transactions/PaymentChannelFund.h.ai.md | 47 + .../PermissionedDomainDelete.h.ai.json | 112 ++ .../PermissionedDomainDelete.h.ai.md | 31 + .../PermissionedDomainSet.h.ai.json | 109 ++ .../PermissionedDomainSet.h.ai.md | 39 + .../transactions/SetFee.h.ai.json | 259 ++++ .../transactions/SetFee.h.ai.md | 39 + .../transactions/SetRegularKey.h.ai.json | 89 ++ .../transactions/SetRegularKey.h.ai.md | 34 + .../transactions/SignerListSet.h.ai.json | 101 ++ .../transactions/SignerListSet.h.ai.md | 37 + .../transactions/TicketCreate.h.ai.json | 88 ++ .../transactions/TicketCreate.h.ai.md | 35 + .../transactions/TrustSet.h.ai.json | 154 ++ .../transactions/TrustSet.h.ai.md | 33 + .../transactions/UNLModify.h.ai.json | 154 ++ .../transactions/UNLModify.h.ai.md | 35 + .../transactions/VaultClawback.h.ai.json | 154 ++ .../transactions/VaultClawback.h.ai.md | 64 + .../transactions/VaultCreate.h.ai.json | 153 ++ .../transactions/VaultCreate.h.ai.md | 44 + .../transactions/VaultDelete.h.ai.json | 88 ++ .../transactions/VaultDelete.h.ai.md | 33 + .../transactions/VaultDeposit.h.ai.json | 104 ++ .../transactions/VaultDeposit.h.ai.md | 41 + .../transactions/VaultSet.h.ai.json | 168 ++ .../transactions/VaultSet.h.ai.md | 48 + .../transactions/VaultWithdraw.h.ai.json | 134 ++ .../transactions/VaultWithdraw.h.ai.md | 27 + .../XChainAccountCreateCommit.h.ai.json | 132 ++ .../XChainAccountCreateCommit.h.ai.md | 38 + ...ChainAddAccountCreateAttestation.h.ai.json | 240 +++ .../XChainAddAccountCreateAttestation.h.ai.md | 37 + .../XChainAddClaimAttestation.h.ai.json | 239 +++ .../XChainAddClaimAttestation.h.ai.md | 67 + .../transactions/XChainClaim.h.ai.json | 149 ++ .../transactions/XChainClaim.h.ai.md | 43 + .../transactions/XChainCommit.h.ai.json | 145 ++ .../transactions/XChainCommit.h.ai.md | 42 + .../transactions/XChainCreateBridge.h.ai.json | 117 ++ .../transactions/XChainCreateBridge.h.ai.md | 53 + .../XChainCreateClaimID.h.ai.json | 116 ++ .../transactions/XChainCreateClaimID.h.ai.md | 33 + .../transactions/XChainModifyBridge.h.ai.json | 118 ++ .../transactions/XChainModifyBridge.h.ai.md | 33 + include/xrpl/rdb/DBInit.h.ai.json | 14 + include/xrpl/rdb/DBInit.h.ai.md | 41 + include/xrpl/rdb/DatabaseCon.h.ai.json | 101 ++ include/xrpl/rdb/DatabaseCon.h.ai.md | 52 + include/xrpl/rdb/RelationalDatabase.h.ai.json | 38 + include/xrpl/rdb/RelationalDatabase.h.ai.md | 45 + include/xrpl/rdb/SociDB.h.ai.json | 199 +++ include/xrpl/rdb/SociDB.h.ai.md | 45 + include/xrpl/resource/Charge.h.ai.json | 101 ++ include/xrpl/resource/Charge.h.ai.md | 36 + include/xrpl/resource/Consumer.h.ai.json | 133 ++ include/xrpl/resource/Consumer.h.ai.md | 47 + include/xrpl/resource/Disposition.h.ai.json | 18 + include/xrpl/resource/Disposition.h.ai.md | 27 + include/xrpl/resource/Fees.h.ai.json | 18 + include/xrpl/resource/Fees.h.ai.md | 46 + include/xrpl/resource/Gossip.h.ai.json | 29 + include/xrpl/resource/Gossip.h.ai.md | 31 + .../xrpl/resource/ResourceManager.h.ai.json | 78 + include/xrpl/resource/ResourceManager.h.ai.md | 44 + include/xrpl/resource/Types.h.ai.json | 29 + include/xrpl/resource/Types.h.ai.md | 21 + include/xrpl/resource/detail/Entry.h.ai.json | 84 + include/xrpl/resource/detail/Entry.h.ai.md | 37 + include/xrpl/resource/detail/Import.h.ai.json | 36 + include/xrpl/resource/detail/Import.h.ai.md | 31 + include/xrpl/resource/detail/Key.h.ai.json | 62 + include/xrpl/resource/detail/Key.h.ai.md | 39 + include/xrpl/resource/detail/Kind.h.ai.json | 18 + include/xrpl/resource/detail/Kind.h.ai.md | 23 + include/xrpl/resource/detail/Logic.h.ai.json | 261 ++++ include/xrpl/resource/detail/Logic.h.ai.md | 55 + include/xrpl/resource/detail/Tuning.h.ai.json | 18 + include/xrpl/resource/detail/Tuning.h.ai.md | 31 + include/xrpl/server/Handoff.h.ai.json | 26 + include/xrpl/server/Handoff.h.ai.md | 55 + include/xrpl/server/InfoSub.h.ai.json | 386 +++++ include/xrpl/server/InfoSub.h.ai.md | 42 + include/xrpl/server/LoadFeeTrack.h.ai.json | 46 + include/xrpl/server/LoadFeeTrack.h.ai.md | 41 + include/xrpl/server/Manifest.h.ai.json | 116 ++ include/xrpl/server/Manifest.h.ai.md | 43 + include/xrpl/server/NetworkOPs.h.ai.json | 306 ++++ include/xrpl/server/NetworkOPs.h.ai.md | 53 + include/xrpl/server/Port.h.ai.json | 91 ++ include/xrpl/server/Port.h.ai.md | 43 + include/xrpl/server/Server.h.ai.json | 24 + include/xrpl/server/Server.h.ai.md | 61 + include/xrpl/server/Session.h.ai.json | 64 + include/xrpl/server/Session.h.ai.md | 39 + include/xrpl/server/SimpleWriter.h.ai.json | 69 + include/xrpl/server/SimpleWriter.h.ai.md | 33 + include/xrpl/server/State.h.ai.json | 113 ++ include/xrpl/server/State.h.ai.md | 41 + include/xrpl/server/Vacuum.h.ai.json | 32 + include/xrpl/server/Vacuum.h.ai.md | 35 + include/xrpl/server/WSSession.h.ai.json | 49 + include/xrpl/server/WSSession.h.ai.md | 59 + include/xrpl/server/Wallet.h.ai.json | 125 ++ include/xrpl/server/Wallet.h.ai.md | 29 + include/xrpl/server/Writer.h.ai.json | 59 + include/xrpl/server/Writer.h.ai.md | 48 + .../xrpl/server/detail/BaseHTTPPeer.h.ai.json | 183 +++ .../xrpl/server/detail/BaseHTTPPeer.h.ai.md | 53 + include/xrpl/server/detail/BasePeer.h.ai.json | 69 + include/xrpl/server/detail/BasePeer.h.ai.md | 62 + .../xrpl/server/detail/BaseWSPeer.h.ai.json | 197 +++ include/xrpl/server/detail/BaseWSPeer.h.ai.md | 95 ++ include/xrpl/server/detail/Door.h.ai.json | 124 ++ include/xrpl/server/detail/Door.h.ai.md | 58 + .../xrpl/server/detail/JSONRPCUtil.h.ai.json | 25 + .../xrpl/server/detail/JSONRPCUtil.h.ai.md | 45 + .../xrpl/server/detail/LowestLayer.h.ai.json | 27 + .../xrpl/server/detail/LowestLayer.h.ai.md | 40 + .../server/detail/PlainHTTPPeer.h.ai.json | 71 + .../xrpl/server/detail/PlainHTTPPeer.h.ai.md | 61 + .../xrpl/server/detail/PlainWSPeer.h.ai.json | 53 + .../xrpl/server/detail/PlainWSPeer.h.ai.md | 25 + .../xrpl/server/detail/SSLHTTPPeer.h.ai.json | 85 ++ .../xrpl/server/detail/SSLHTTPPeer.h.ai.md | 49 + .../xrpl/server/detail/SSLWSPeer.h.ai.json | 53 + include/xrpl/server/detail/SSLWSPeer.h.ai.md | 59 + .../xrpl/server/detail/ServerImpl.h.ai.json | 159 ++ include/xrpl/server/detail/ServerImpl.h.ai.md | 66 + include/xrpl/server/detail/Spawn.h.ai.json | 40 + include/xrpl/server/detail/Spawn.h.ai.md | 36 + include/xrpl/server/detail/io_list.h.ai.json | 111 ++ include/xrpl/server/detail/io_list.h.ai.md | 37 + include/xrpl/shamap/Family.h.ai.json | 99 ++ include/xrpl/shamap/Family.h.ai.md | 38 + include/xrpl/shamap/FullBelowCache.h.ai.json | 109 ++ include/xrpl/shamap/FullBelowCache.h.ai.md | 43 + include/xrpl/shamap/SHAMap.h.ai.json | 590 +++++++ include/xrpl/shamap/SHAMap.h.ai.md | 58 + .../SHAMapAccountStateLeafNode.h.ai.json | 72 + .../shamap/SHAMapAccountStateLeafNode.h.ai.md | 57 + include/xrpl/shamap/SHAMapAddNode.h.ai.json | 119 ++ include/xrpl/shamap/SHAMapAddNode.h.ai.md | 38 + include/xrpl/shamap/SHAMapInnerNode.h.ai.json | 337 ++++ include/xrpl/shamap/SHAMapInnerNode.h.ai.md | 43 + include/xrpl/shamap/SHAMapItem.h.ai.json | 70 + include/xrpl/shamap/SHAMapItem.h.ai.md | 39 + include/xrpl/shamap/SHAMapLeafNode.h.ai.json | 105 ++ include/xrpl/shamap/SHAMapLeafNode.h.ai.md | 47 + .../xrpl/shamap/SHAMapMissingNode.h.ai.json | 36 + include/xrpl/shamap/SHAMapMissingNode.h.ai.md | 43 + include/xrpl/shamap/SHAMapNodeID.h.ai.json | 163 ++ include/xrpl/shamap/SHAMapNodeID.h.ai.md | 46 + .../xrpl/shamap/SHAMapSyncFilter.h.ai.json | 39 + include/xrpl/shamap/SHAMapSyncFilter.h.ai.md | 35 + include/xrpl/shamap/SHAMapTreeNode.h.ai.json | 162 ++ include/xrpl/shamap/SHAMapTreeNode.h.ai.md | 42 + .../xrpl/shamap/SHAMapTxLeafNode.h.ai.json | 88 ++ include/xrpl/shamap/SHAMapTxLeafNode.h.ai.md | 38 + .../shamap/SHAMapTxPlusMetaLeafNode.h.ai.json | 55 + .../shamap/SHAMapTxPlusMetaLeafNode.h.ai.md | 51 + include/xrpl/shamap/TreeNodeCache.h.ai.json | 14 + include/xrpl/shamap/TreeNodeCache.h.ai.md | 34 + .../shamap/detail/TaggedPointer.h.ai.json | 165 ++ .../xrpl/shamap/detail/TaggedPointer.h.ai.md | 68 + .../shamap/detail/TaggedPointer.ipp.ai.json | 495 ++++++ .../shamap/detail/TaggedPointer.ipp.ai.md | 43 + include/xrpl/tx/ApplyContext.h.ai.json | 196 +++ include/xrpl/tx/ApplyContext.h.ai.md | 61 + include/xrpl/tx/SignerEntries.h.ai.json | 64 + include/xrpl/tx/SignerEntries.h.ai.md | 38 + include/xrpl/tx/Transactor.h.ai.json | 432 ++++++ include/xrpl/tx/Transactor.h.ai.md | 73 + include/xrpl/tx/apply.h.ai.json | 56 + include/xrpl/tx/apply.h.ai.md | 45 + include/xrpl/tx/applySteps.h.ai.json | 119 ++ include/xrpl/tx/applySteps.h.ai.md | 43 + .../xrpl/tx/invariants/AMMInvariant.h.ai.json | 119 ++ .../xrpl/tx/invariants/AMMInvariant.h.ai.md | 55 + .../tx/invariants/FreezeInvariant.h.ai.json | 115 ++ .../tx/invariants/FreezeInvariant.h.ai.md | 61 + .../tx/invariants/InvariantCheck.h.ai.json | 96 ++ .../xrpl/tx/invariants/InvariantCheck.h.ai.md | 66 + .../InvariantCheckPrivilege.h.ai.json | 48 + .../InvariantCheckPrivilege.h.ai.md | 81 + .../invariants/LoanBrokerInvariant.h.ai.json | 55 + .../tx/invariants/LoanBrokerInvariant.h.ai.md | 39 + .../tx/invariants/LoanInvariant.h.ai.json | 41 + .../xrpl/tx/invariants/LoanInvariant.h.ai.md | 35 + .../xrpl/tx/invariants/MPTInvariant.h.ai.json | 66 + .../xrpl/tx/invariants/MPTInvariant.h.ai.md | 50 + .../xrpl/tx/invariants/NFTInvariant.h.ai.json | 66 + .../xrpl/tx/invariants/NFTInvariant.h.ai.md | 48 + .../PermissionedDEXInvariant.h.ai.json | 41 + .../PermissionedDEXInvariant.h.ai.md | 46 + .../PermissionedDomainInvariant.h.ai.json | 46 + .../PermissionedDomainInvariant.h.ai.md | 47 + .../tx/invariants/VaultInvariant.h.ai.json | 51 + .../xrpl/tx/invariants/VaultInvariant.h.ai.md | 41 + include/xrpl/tx/paths/AMMLiquidity.h.ai.json | 128 ++ include/xrpl/tx/paths/AMMLiquidity.h.ai.md | 49 + include/xrpl/tx/paths/AMMOffer.h.ai.json | 136 ++ include/xrpl/tx/paths/AMMOffer.h.ai.md | 44 + include/xrpl/tx/paths/BookTip.h.ai.json | 64 + include/xrpl/tx/paths/BookTip.h.ai.md | 33 + include/xrpl/tx/paths/Flow.h.ai.json | 100 ++ include/xrpl/tx/paths/Flow.h.ai.md | 64 + include/xrpl/tx/paths/Offer.h.ai.json | 226 +++ include/xrpl/tx/paths/Offer.h.ai.md | 41 + include/xrpl/tx/paths/OfferStream.h.ai.json | 97 ++ include/xrpl/tx/paths/OfferStream.h.ai.md | 59 + include/xrpl/tx/paths/RippleCalc.h.ai.json | 66 + include/xrpl/tx/paths/RippleCalc.h.ai.md | 43 + .../xrpl/tx/paths/detail/AmountSpec.h.ai.json | 9 + .../xrpl/tx/paths/detail/AmountSpec.h.ai.md | 25 + .../tx/paths/detail/EitherAmount.h.ai.json | 83 + .../xrpl/tx/paths/detail/EitherAmount.h.ai.md | 54 + .../xrpl/tx/paths/detail/FlatSets.h.ai.json | 32 + include/xrpl/tx/paths/detail/FlatSets.h.ai.md | 31 + .../tx/paths/detail/FlowDebugInfo.h.ai.json | 169 +++ .../tx/paths/detail/FlowDebugInfo.h.ai.md | 55 + .../xrpl/tx/paths/detail/StepChecks.h.ai.json | 37 + .../xrpl/tx/paths/detail/StepChecks.h.ai.md | 52 + include/xrpl/tx/paths/detail/Steps.h.ai.json | 652 ++++++++ include/xrpl/tx/paths/detail/Steps.h.ai.md | 59 + .../xrpl/tx/paths/detail/StrandFlow.h.ai.json | 78 + .../xrpl/tx/paths/detail/StrandFlow.h.ai.md | 59 + .../account/AccountDelete.h.ai.json | 89 ++ .../transactors/account/AccountDelete.h.ai.md | 64 + .../transactors/account/AccountSet.h.ai.json | 76 + .../tx/transactors/account/AccountSet.h.ai.md | 62 + .../account/SetRegularKey.h.ai.json | 50 + .../transactors/account/SetRegularKey.h.ai.md | 43 + .../account/SignerListSet.h.ai.json | 95 ++ .../transactors/account/SignerListSet.h.ai.md | 55 + .../transactors/bridge/XChainBridge.h.ai.json | 343 +++++ .../transactors/bridge/XChainBridge.h.ai.md | 47 + .../transactors/check/CheckCancel.h.ai.json | 62 + .../tx/transactors/check/CheckCancel.h.ai.md | 28 + .../tx/transactors/check/CheckCash.h.ai.json | 73 + .../tx/transactors/check/CheckCash.h.ai.md | 85 ++ .../transactors/check/CheckCreate.h.ai.json | 61 + .../tx/transactors/check/CheckCreate.h.ai.md | 37 + .../credentials/CredentialAccept.h.ai.json | 73 + .../credentials/CredentialAccept.h.ai.md | 39 + .../credentials/CredentialCreate.h.ai.json | 61 + .../credentials/CredentialCreate.h.ai.md | 43 + .../credentials/CredentialDelete.h.ai.json | 54 + .../credentials/CredentialDelete.h.ai.md | 35 + .../delegate/DelegateSet.h.ai.json | 88 ++ .../transactors/delegate/DelegateSet.h.ai.md | 35 + .../xrpl/tx/transactors/dex/AMMBid.h.ai.json | 73 + .../xrpl/tx/transactors/dex/AMMBid.h.ai.md | 52 + .../tx/transactors/dex/AMMClawback.h.ai.json | 85 ++ .../tx/transactors/dex/AMMClawback.h.ai.md | 58 + .../tx/transactors/dex/AMMContext.h.ai.json | 78 + .../tx/transactors/dex/AMMContext.h.ai.md | 55 + .../tx/transactors/dex/AMMCreate.h.ai.json | 57 + .../xrpl/tx/transactors/dex/AMMCreate.h.ai.md | 45 + .../tx/transactors/dex/AMMDelete.h.ai.json | 73 + .../xrpl/tx/transactors/dex/AMMDelete.h.ai.md | 40 + .../tx/transactors/dex/AMMDeposit.h.ai.json | 168 ++ .../tx/transactors/dex/AMMDeposit.h.ai.md | 46 + .../xrpl/tx/transactors/dex/AMMVote.h.ai.json | 61 + .../xrpl/tx/transactors/dex/AMMVote.h.ai.md | 33 + .../tx/transactors/dex/AMMWithdraw.h.ai.json | 210 +++ .../tx/transactors/dex/AMMWithdraw.h.ai.md | 51 + .../tx/transactors/dex/OfferCancel.h.ai.json | 62 + .../tx/transactors/dex/OfferCancel.h.ai.md | 29 + .../tx/transactors/dex/OfferCreate.h.ai.json | 118 ++ .../tx/transactors/dex/OfferCreate.h.ai.md | 54 + .../tx/transactors/did/DIDDelete.h.ai.json | 98 ++ .../xrpl/tx/transactors/did/DIDDelete.h.ai.md | 37 + .../xrpl/tx/transactors/did/DIDSet.h.ai.json | 40 + .../xrpl/tx/transactors/did/DIDSet.h.ai.md | 27 + .../transactors/escrow/EscrowCancel.h.ai.json | 62 + .../transactors/escrow/EscrowCancel.h.ai.md | 31 + .../transactors/escrow/EscrowCreate.h.ai.json | 73 + .../transactors/escrow/EscrowCreate.h.ai.md | 54 + .../transactors/escrow/EscrowFinish.h.ai.json | 71 + .../transactors/escrow/EscrowFinish.h.ai.md | 64 + .../lending/LendingHelpers.h.ai.json | 650 ++++++++ .../lending/LendingHelpers.h.ai.md | 45 + .../lending/LoanBrokerCoverClawback.h.ai.json | 61 + .../lending/LoanBrokerCoverClawback.h.ai.md | 37 + .../lending/LoanBrokerCoverDeposit.h.ai.json | 56 + .../lending/LoanBrokerCoverDeposit.h.ai.md | 40 + .../lending/LoanBrokerCoverWithdraw.h.ai.json | 56 + .../lending/LoanBrokerCoverWithdraw.h.ai.md | 47 + .../lending/LoanBrokerDelete.h.ai.json | 73 + .../lending/LoanBrokerDelete.h.ai.md | 48 + .../lending/LoanBrokerSet.h.ai.json | 66 + .../transactors/lending/LoanBrokerSet.h.ai.md | 62 + .../transactors/lending/LoanDelete.h.ai.json | 73 + .../tx/transactors/lending/LoanDelete.h.ai.md | 36 + .../transactors/lending/LoanManage.h.ai.json | 90 ++ .../tx/transactors/lending/LoanManage.h.ai.md | 64 + .../tx/transactors/lending/LoanPay.h.ai.json | 71 + .../tx/transactors/lending/LoanPay.h.ai.md | 75 + .../tx/transactors/lending/LoanSet.h.ai.json | 116 ++ .../tx/transactors/lending/LoanSet.h.ai.md | 39 + .../nft/NFTokenAcceptOffer.h.ai.json | 123 ++ .../nft/NFTokenAcceptOffer.h.ai.md | 37 + .../tx/transactors/nft/NFTokenBurn.h.ai.json | 54 + .../tx/transactors/nft/NFTokenBurn.h.ai.md | 39 + .../nft/NFTokenCancelOffer.h.ai.json | 62 + .../nft/NFTokenCancelOffer.h.ai.md | 42 + .../nft/NFTokenCreateOffer.h.ai.json | 73 + .../nft/NFTokenCreateOffer.h.ai.md | 58 + .../tx/transactors/nft/NFTokenMint.h.ai.json | 115 ++ .../tx/transactors/nft/NFTokenMint.h.ai.md | 59 + .../transactors/nft/NFTokenModify.h.ai.json | 49 + .../tx/transactors/nft/NFTokenModify.h.ai.md | 45 + .../transactors/oracle/OracleDelete.h.ai.json | 88 ++ .../transactors/oracle/OracleDelete.h.ai.md | 27 + .../tx/transactors/oracle/OracleSet.h.ai.json | 62 + .../tx/transactors/oracle/OracleSet.h.ai.md | 50 + .../payment/DepositPreauth.h.ai.json | 94 ++ .../payment/DepositPreauth.h.ai.md | 72 + .../tx/transactors/payment/Payment.h.ai.json | 111 ++ .../tx/transactors/payment/Payment.h.ai.md | 56 + .../PaymentChannelClaim.h.ai.json | 84 + .../PaymentChannelClaim.h.ai.md | 54 + .../PaymentChannelCreate.h.ai.json | 61 + .../PaymentChannelCreate.h.ai.md | 52 + .../PaymentChannelFund.h.ai.json | 54 + .../PaymentChannelFund.h.ai.md | 41 + .../PermissionedDomainDelete.h.ai.json | 62 + .../PermissionedDomainDelete.h.ai.md | 29 + .../PermissionedDomainSet.h.ai.json | 73 + .../PermissionedDomainSet.h.ai.md | 37 + .../tx/transactors/system/Batch.h.ai.json | 100 ++ .../xrpl/tx/transactors/system/Batch.h.ai.md | 79 + .../tx/transactors/system/Change.h.ai.json | 87 ++ .../xrpl/tx/transactors/system/Change.h.ai.md | 53 + .../system/LedgerStateFix.h.ai.json | 78 + .../transactors/system/LedgerStateFix.h.ai.md | 29 + .../transactors/system/TicketCreate.h.ai.json | 56 + .../transactors/system/TicketCreate.h.ai.md | 47 + .../tx/transactors/token/Clawback.h.ai.json | 62 + .../tx/transactors/token/Clawback.h.ai.md | 53 + .../token/MPTokenAuthorize.h.ai.json | 85 ++ .../token/MPTokenAuthorize.h.ai.md | 43 + .../token/MPTokenIssuanceCreate.h.ai.json | 111 ++ .../token/MPTokenIssuanceCreate.h.ai.md | 53 + .../token/MPTokenIssuanceDestroy.h.ai.json | 62 + .../token/MPTokenIssuanceDestroy.h.ai.md | 38 + .../token/MPTokenIssuanceSet.h.ai.json | 93 ++ .../token/MPTokenIssuanceSet.h.ai.md | 37 + .../tx/transactors/token/TrustSet.h.ai.json | 82 + .../tx/transactors/token/TrustSet.h.ai.md | 59 + .../transactors/vault/VaultClawback.h.ai.json | 88 ++ .../transactors/vault/VaultClawback.h.ai.md | 71 + .../transactors/vault/VaultCreate.h.ai.json | 84 + .../tx/transactors/vault/VaultCreate.h.ai.md | 74 + .../transactors/vault/VaultDelete.h.ai.json | 62 + .../tx/transactors/vault/VaultDelete.h.ai.md | 41 + .../transactors/vault/VaultDeposit.h.ai.json | 62 + .../tx/transactors/vault/VaultDeposit.h.ai.md | 35 + .../tx/transactors/vault/VaultSet.h.ai.json | 73 + .../tx/transactors/vault/VaultSet.h.ai.md | 41 + .../transactors/vault/VaultWithdraw.h.ai.json | 62 + .../transactors/vault/VaultWithdraw.h.ai.md | 47 + src/libxrpl/basics/Archive.cpp.ai.json | 653 ++++++++ src/libxrpl/basics/Archive.cpp.ai.md | 37 + src/libxrpl/basics/BasicConfig.cpp.ai.json | 349 +++++ src/libxrpl/basics/BasicConfig.cpp.ai.md | 67 + src/libxrpl/basics/CountedObject.cpp.ai.json | 125 ++ src/libxrpl/basics/CountedObject.cpp.ai.md | 51 + src/libxrpl/basics/FileUtilities.cpp.ai.json | 562 +++++++ src/libxrpl/basics/FileUtilities.cpp.ai.md | 37 + src/libxrpl/basics/Log.cpp.ai.json | 598 ++++++++ src/libxrpl/basics/Log.cpp.ai.md | 49 + src/libxrpl/basics/MallocTrim.cpp.ai.json | 301 ++++ src/libxrpl/basics/MallocTrim.cpp.ai.md | 39 + src/libxrpl/basics/Number.cpp.ai.json | 477 ++++++ src/libxrpl/basics/Number.cpp.ai.md | 110 ++ src/libxrpl/basics/ResolverAsio.cpp.ai.json | 425 ++++++ src/libxrpl/basics/ResolverAsio.cpp.ai.md | 33 + .../basics/StringUtilities.cpp.ai.json | 382 +++++ src/libxrpl/basics/StringUtilities.cpp.ai.md | 35 + src/libxrpl/basics/UptimeClock.cpp.ai.json | 101 ++ src/libxrpl/basics/UptimeClock.cpp.ai.md | 48 + src/libxrpl/basics/base64.cpp.ai.json | 147 ++ src/libxrpl/basics/base64.cpp.ai.md | 33 + src/libxrpl/basics/contract.cpp.ai.json | 110 ++ src/libxrpl/basics/contract.cpp.ai.md | 58 + .../basics/make_SSLContext.cpp.ai.json | 342 +++++ src/libxrpl/basics/make_SSLContext.cpp.ai.md | 42 + src/libxrpl/basics/mulDiv.cpp.ai.json | 181 +++ src/libxrpl/basics/mulDiv.cpp.ai.md | 42 + .../clock/basic_seconds_clock.cpp.ai.json | 230 +++ .../beast/clock/basic_seconds_clock.cpp.ai.md | 62 + .../beast/core/CurrentThreadName.cpp.ai.json | 173 +++ .../beast/core/CurrentThreadName.cpp.ai.md | 39 + .../beast/core/SemanticVersion.cpp.ai.json | 428 ++++++ .../beast/core/SemanticVersion.cpp.ai.md | 36 + .../beast/insight/Collector.cpp.ai.json | 58 + src/libxrpl/beast/insight/Collector.cpp.ai.md | 7 + src/libxrpl/beast/insight/Groups.cpp.ai.json | 358 +++++ src/libxrpl/beast/insight/Groups.cpp.ai.md | 72 + src/libxrpl/beast/insight/Hook.cpp.ai.json | 50 + src/libxrpl/beast/insight/Hook.cpp.ai.md | 9 + src/libxrpl/beast/insight/Metric.cpp.ai.json | 142 ++ src/libxrpl/beast/insight/Metric.cpp.ai.md | 26 + .../beast/insight/NullCollector.cpp.ai.json | 303 ++++ .../beast/insight/NullCollector.cpp.ai.md | 37 + .../beast/insight/StatsDCollector.cpp.ai.json | 669 ++++++++ .../beast/insight/StatsDCollector.cpp.ai.md | 45 + .../beast/net/IPAddressConversion.cpp.ai.json | 161 ++ .../beast/net/IPAddressConversion.cpp.ai.md | 33 + src/libxrpl/beast/net/IPAddressV4.cpp.ai.json | 228 +++ src/libxrpl/beast/net/IPAddressV4.cpp.ai.md | 63 + src/libxrpl/beast/net/IPAddressV6.cpp.ai.json | 175 +++ src/libxrpl/beast/net/IPAddressV6.cpp.ai.md | 25 + src/libxrpl/beast/net/IPEndpoint.cpp.ai.json | 440 ++++++ src/libxrpl/beast/net/IPEndpoint.cpp.ai.md | 42 + .../beast/utility/beast_Journal.cpp.ai.json | 377 +++++ .../beast/utility/beast_Journal.cpp.ai.md | 51 + .../utility/beast_PropertyStream.cpp.ai.json | 979 ++++++++++++ .../utility/beast_PropertyStream.cpp.ai.md | 41 + src/libxrpl/conditions/Condition.cpp.ai.json | 583 +++++++ src/libxrpl/conditions/Condition.cpp.ai.md | 44 + .../conditions/Fulfillment.cpp.ai.json | 718 +++++++++ src/libxrpl/conditions/Fulfillment.cpp.ai.md | 43 + src/libxrpl/conditions/error.cpp.ai.json | 232 +++ src/libxrpl/conditions/error.cpp.ai.md | 34 + src/libxrpl/core/HashRouter.cpp.ai.json | 297 ++++ src/libxrpl/core/HashRouter.cpp.ai.md | 39 + src/libxrpl/core/detail/Job.cpp.ai.json | 257 ++++ src/libxrpl/core/detail/Job.cpp.ai.md | 43 + src/libxrpl/core/detail/JobQueue.cpp.ai.json | 720 +++++++++ src/libxrpl/core/detail/JobQueue.cpp.ai.md | 59 + src/libxrpl/core/detail/LoadEvent.cpp.ai.json | 229 +++ src/libxrpl/core/detail/LoadEvent.cpp.ai.md | 29 + .../core/detail/LoadMonitor.cpp.ai.json | 352 +++++ src/libxrpl/core/detail/LoadMonitor.cpp.ai.md | 61 + src/libxrpl/core/detail/Workers.cpp.ai.json | 309 ++++ src/libxrpl/core/detail/Workers.cpp.ai.md | 49 + src/libxrpl/crypto/RFC1751.cpp.ai.json | 224 +++ src/libxrpl/crypto/RFC1751.cpp.ai.md | 50 + src/libxrpl/crypto/csprng.cpp.ai.json | 314 ++++ src/libxrpl/crypto/csprng.cpp.ai.md | 45 + src/libxrpl/crypto/secure_erase.cpp.ai.json | 86 ++ src/libxrpl/crypto/secure_erase.cpp.ai.md | 35 + src/libxrpl/git/Git.cpp.ai.json | 194 +++ src/libxrpl/git/Git.cpp.ai.md | 21 + .../json/JsonPropertyStream.cpp.ai.json | 403 +++++ src/libxrpl/json/JsonPropertyStream.cpp.ai.md | 45 + src/libxrpl/json/Output.cpp.ai.json | 160 ++ src/libxrpl/json/Output.cpp.ai.md | 33 + src/libxrpl/json/Writer.cpp.ai.json | 488 ++++++ src/libxrpl/json/Writer.cpp.ai.md | 47 + src/libxrpl/json/json_reader.cpp.ai.json | 471 ++++++ src/libxrpl/json/json_reader.cpp.ai.md | 51 + src/libxrpl/json/json_value.cpp.ai.json | 410 +++++ src/libxrpl/json/json_value.cpp.ai.md | 45 + .../json/json_valueiterator.cpp.ai.json | 445 ++++++ src/libxrpl/json/json_valueiterator.cpp.ai.md | 38 + src/libxrpl/json/json_writer.cpp.ai.json | 647 ++++++++ src/libxrpl/json/json_writer.cpp.ai.md | 51 + src/libxrpl/json/to_string.cpp.ai.json | 84 + src/libxrpl/json/to_string.cpp.ai.md | 14 + .../ledger/AcceptedLedgerTx.cpp.ai.json | 546 +++++++ src/libxrpl/ledger/AcceptedLedgerTx.cpp.ai.md | 35 + .../ledger/ApplyStateTable.cpp.ai.json | 597 ++++++++ src/libxrpl/ledger/ApplyStateTable.cpp.ai.md | 71 + src/libxrpl/ledger/ApplyView.cpp.ai.json | 274 ++++ src/libxrpl/ledger/ApplyView.cpp.ai.md | 45 + src/libxrpl/ledger/ApplyViewBase.cpp.ai.json | 454 ++++++ src/libxrpl/ledger/ApplyViewBase.cpp.ai.md | 39 + src/libxrpl/ledger/ApplyViewImpl.cpp.ai.json | 196 +++ src/libxrpl/ledger/ApplyViewImpl.cpp.ai.md | 45 + src/libxrpl/ledger/BookDirs.cpp.ai.json | 451 ++++++ src/libxrpl/ledger/BookDirs.cpp.ai.md | 37 + src/libxrpl/ledger/BookListeners.cpp.ai.json | 449 ++++++ src/libxrpl/ledger/BookListeners.cpp.ai.md | 33 + src/libxrpl/ledger/CachedView.cpp.ai.json | 330 ++++ src/libxrpl/ledger/CachedView.cpp.ai.md | 47 + src/libxrpl/ledger/CanonicalTXSet.cpp.ai.json | 204 +++ src/libxrpl/ledger/CanonicalTXSet.cpp.ai.md | 59 + src/libxrpl/ledger/Dir.cpp.ai.json | 367 +++++ src/libxrpl/ledger/Dir.cpp.ai.md | 48 + src/libxrpl/ledger/Ledger.cpp.ai.json | 803 ++++++++++ src/libxrpl/ledger/Ledger.cpp.ai.md | 57 + src/libxrpl/ledger/OpenView.cpp.ai.json | 517 +++++++ src/libxrpl/ledger/OpenView.cpp.ai.md | 56 + src/libxrpl/ledger/PaymentSandbox.cpp.ai.json | 651 ++++++++ src/libxrpl/ledger/PaymentSandbox.cpp.ai.md | 68 + src/libxrpl/ledger/RawStateTable.cpp.ai.json | 453 ++++++ src/libxrpl/ledger/RawStateTable.cpp.ai.md | 48 + src/libxrpl/ledger/ReadView.cpp.ai.json | 221 +++ src/libxrpl/ledger/ReadView.cpp.ai.md | 48 + src/libxrpl/ledger/View.cpp.ai.json | 606 ++++++++ src/libxrpl/ledger/View.cpp.ai.md | 72 + .../ledger/helpers/AMMHelpers.cpp.ai.json | 1058 +++++++++++++ .../ledger/helpers/AMMHelpers.cpp.ai.md | 75 + .../helpers/AccountRootHelpers.cpp.ai.json | 580 +++++++ .../helpers/AccountRootHelpers.cpp.ai.md | 42 + .../helpers/CredentialHelpers.cpp.ai.json | 413 +++++ .../helpers/CredentialHelpers.cpp.ai.md | 50 + .../helpers/DirectoryHelpers.cpp.ai.json | 404 +++++ .../ledger/helpers/DirectoryHelpers.cpp.ai.md | 35 + .../ledger/helpers/MPTokenHelpers.cpp.ai.json | 654 ++++++++ .../ledger/helpers/MPTokenHelpers.cpp.ai.md | 49 + .../ledger/helpers/NFTokenHelpers.cpp.ai.json | 589 +++++++ .../ledger/helpers/NFTokenHelpers.cpp.ai.md | 59 + .../ledger/helpers/OfferHelpers.cpp.ai.json | 470 ++++++ .../ledger/helpers/OfferHelpers.cpp.ai.md | 29 + .../helpers/PaymentChannelHelpers.cpp.ai.json | 315 ++++ .../helpers/PaymentChannelHelpers.cpp.ai.md | 31 + .../PermissionedDEXHelpers.cpp.ai.json | 442 ++++++ .../helpers/PermissionedDEXHelpers.cpp.ai.md | 33 + .../helpers/RippleStateHelpers.cpp.ai.json | 683 +++++++++ .../helpers/RippleStateHelpers.cpp.ai.md | 53 + .../ledger/helpers/TokenHelpers.cpp.ai.json | 685 +++++++++ .../ledger/helpers/TokenHelpers.cpp.ai.md | 45 + .../ledger/helpers/VaultHelpers.cpp.ai.json | 519 +++++++ .../ledger/helpers/VaultHelpers.cpp.ai.md | 57 + src/libxrpl/net/HTTPClient.cpp.ai.json | 959 ++++++++++++ src/libxrpl/net/HTTPClient.cpp.ai.md | 45 + src/libxrpl/net/RegisterSSLCerts.cpp.ai.json | 459 ++++++ src/libxrpl/net/RegisterSSLCerts.cpp.ai.md | 50 + src/libxrpl/nodestore/BatchWriter.cpp.ai.json | 341 +++++ src/libxrpl/nodestore/BatchWriter.cpp.ai.md | 33 + src/libxrpl/nodestore/Database.cpp.ai.json | 683 +++++++++ src/libxrpl/nodestore/Database.cpp.ai.md | 47 + .../nodestore/DatabaseNodeImp.cpp.ai.json | 376 +++++ .../nodestore/DatabaseNodeImp.cpp.ai.md | 35 + .../nodestore/DatabaseRotatingImp.cpp.ai.json | 752 +++++++++ .../nodestore/DatabaseRotatingImp.cpp.ai.md | 56 + src/libxrpl/nodestore/DecodedBlob.cpp.ai.json | 396 +++++ src/libxrpl/nodestore/DecodedBlob.cpp.ai.md | 33 + .../nodestore/DummyScheduler.cpp.ai.json | 134 ++ .../nodestore/DummyScheduler.cpp.ai.md | 25 + src/libxrpl/nodestore/ManagerImp.cpp.ai.json | 435 ++++++ src/libxrpl/nodestore/ManagerImp.cpp.ai.md | 38 + src/libxrpl/nodestore/NodeObject.cpp.ai.json | 172 +++ src/libxrpl/nodestore/NodeObject.cpp.ai.md | 17 + .../backend/MemoryFactory.cpp.ai.json | 568 +++++++ .../nodestore/backend/MemoryFactory.cpp.ai.md | 41 + .../nodestore/backend/NuDBFactory.cpp.ai.json | 529 +++++++ .../nodestore/backend/NuDBFactory.cpp.ai.md | 43 + .../nodestore/backend/NullFactory.cpp.ai.json | 411 +++++ .../nodestore/backend/NullFactory.cpp.ai.md | 23 + .../backend/RocksDBFactory.cpp.ai.json | 778 ++++++++++ .../backend/RocksDBFactory.cpp.ai.md | 47 + src/libxrpl/protocol/AMMCore.cpp.ai.json | 494 ++++++ src/libxrpl/protocol/AMMCore.cpp.ai.md | 46 + src/libxrpl/protocol/AccountID.cpp.ai.json | 410 +++++ src/libxrpl/protocol/AccountID.cpp.ai.md | 52 + src/libxrpl/protocol/Asset.cpp.ai.json | 343 +++++ src/libxrpl/protocol/Asset.cpp.ai.md | 46 + src/libxrpl/protocol/Book.cpp.ai.json | 238 +++ src/libxrpl/protocol/Book.cpp.ai.md | 62 + src/libxrpl/protocol/BuildInfo.cpp.ai.json | 422 ++++++ src/libxrpl/protocol/BuildInfo.cpp.ai.md | 35 + src/libxrpl/protocol/ErrorCodes.cpp.ai.json | 281 ++++ src/libxrpl/protocol/ErrorCodes.cpp.ai.md | 45 + src/libxrpl/protocol/Feature.cpp.ai.json | 530 +++++++ src/libxrpl/protocol/Feature.cpp.ai.md | 70 + src/libxrpl/protocol/IOUAmount.cpp.ai.json | 577 +++++++ src/libxrpl/protocol/IOUAmount.cpp.ai.md | 56 + src/libxrpl/protocol/Indexes.cpp.ai.json | 883 +++++++++++ src/libxrpl/protocol/Indexes.cpp.ai.md | 106 ++ .../protocol/InnerObjectFormats.cpp.ai.json | 886 +++++++++++ .../protocol/InnerObjectFormats.cpp.ai.md | 50 + src/libxrpl/protocol/Issue.cpp.ai.json | 273 ++++ src/libxrpl/protocol/Issue.cpp.ai.md | 41 + src/libxrpl/protocol/Keylet.cpp.ai.json | 156 ++ src/libxrpl/protocol/Keylet.cpp.ai.md | 23 + .../protocol/LedgerFormats.cpp.ai.json | 370 +++++ src/libxrpl/protocol/LedgerFormats.cpp.ai.md | 37 + src/libxrpl/protocol/LedgerHeader.cpp.ai.json | 222 +++ src/libxrpl/protocol/LedgerHeader.cpp.ai.md | 54 + src/libxrpl/protocol/MPTAmount.cpp.ai.json | 143 ++ src/libxrpl/protocol/MPTAmount.cpp.ai.md | 35 + src/libxrpl/protocol/MPTIssue.cpp.ai.json | 492 ++++++ src/libxrpl/protocol/MPTIssue.cpp.ai.md | 31 + .../NFTSyntheticSerializer.cpp.ai.json | 474 ++++++ .../protocol/NFTSyntheticSerializer.cpp.ai.md | 38 + src/libxrpl/protocol/NFTokenID.cpp.ai.json | 669 ++++++++ src/libxrpl/protocol/NFTokenID.cpp.ai.md | 31 + .../protocol/NFTokenOfferID.cpp.ai.json | 490 ++++++ src/libxrpl/protocol/NFTokenOfferID.cpp.ai.md | 42 + src/libxrpl/protocol/PathAsset.cpp.ai.json | 90 ++ src/libxrpl/protocol/PathAsset.cpp.ai.md | 33 + src/libxrpl/protocol/Permissions.cpp.ai.json | 347 +++++ src/libxrpl/protocol/Permissions.cpp.ai.md | 47 + src/libxrpl/protocol/Protocol.cpp.ai.json | 147 ++ src/libxrpl/protocol/Protocol.cpp.ai.md | 48 + src/libxrpl/protocol/PublicKey.cpp.ai.json | 267 ++++ src/libxrpl/protocol/PublicKey.cpp.ai.md | 44 + src/libxrpl/protocol/Quality.cpp.ai.json | 538 +++++++ src/libxrpl/protocol/Quality.cpp.ai.md | 64 + .../protocol/QualityFunction.cpp.ai.json | 280 ++++ .../protocol/QualityFunction.cpp.ai.md | 73 + src/libxrpl/protocol/RPCErr.cpp.ai.json | 236 +++ src/libxrpl/protocol/RPCErr.cpp.ai.md | 21 + src/libxrpl/protocol/Rate2.cpp.ai.json | 500 ++++++ src/libxrpl/protocol/Rate2.cpp.ai.md | 54 + src/libxrpl/protocol/Rules.cpp.ai.json | 289 ++++ src/libxrpl/protocol/Rules.cpp.ai.md | 37 + src/libxrpl/protocol/SField.cpp.ai.json | 327 ++++ src/libxrpl/protocol/SField.cpp.ai.md | 72 + src/libxrpl/protocol/SOTemplate.cpp.ai.json | 263 ++++ src/libxrpl/protocol/SOTemplate.cpp.ai.md | 45 + src/libxrpl/protocol/STAccount.cpp.ai.json | 333 ++++ src/libxrpl/protocol/STAccount.cpp.ai.md | 52 + src/libxrpl/protocol/STAmount.cpp.ai.json | 978 ++++++++++++ src/libxrpl/protocol/STAmount.cpp.ai.md | 126 ++ src/libxrpl/protocol/STArray.cpp.ai.json | 497 ++++++ src/libxrpl/protocol/STArray.cpp.ai.md | 77 + src/libxrpl/protocol/STBase.cpp.ai.json | 523 +++++++ src/libxrpl/protocol/STBase.cpp.ai.md | 54 + src/libxrpl/protocol/STBlob.cpp.ai.json | 317 ++++ src/libxrpl/protocol/STBlob.cpp.ai.md | 36 + src/libxrpl/protocol/STCurrency.cpp.ai.json | 316 ++++ src/libxrpl/protocol/STCurrency.cpp.ai.md | 50 + src/libxrpl/protocol/STInteger.cpp.ai.json | 400 +++++ src/libxrpl/protocol/STInteger.cpp.ai.md | 33 + src/libxrpl/protocol/STIssue.cpp.ai.json | 275 ++++ src/libxrpl/protocol/STIssue.cpp.ai.md | 43 + .../protocol/STLedgerEntry.cpp.ai.json | 554 +++++++ src/libxrpl/protocol/STLedgerEntry.cpp.ai.md | 45 + src/libxrpl/protocol/STNumber.cpp.ai.json | 434 ++++++ src/libxrpl/protocol/STNumber.cpp.ai.md | 50 + src/libxrpl/protocol/STObject.cpp.ai.json | 975 ++++++++++++ src/libxrpl/protocol/STObject.cpp.ai.md | 69 + src/libxrpl/protocol/STParsedJSON.cpp.ai.json | 389 +++++ src/libxrpl/protocol/STParsedJSON.cpp.ai.md | 51 + src/libxrpl/protocol/STPathSet.cpp.ai.json | 367 +++++ src/libxrpl/protocol/STPathSet.cpp.ai.md | 43 + src/libxrpl/protocol/STTakesAsset.cpp.ai.json | 260 ++++ src/libxrpl/protocol/STTakesAsset.cpp.ai.md | 37 + src/libxrpl/protocol/STTx.cpp.ai.json | 802 ++++++++++ src/libxrpl/protocol/STTx.cpp.ai.md | 55 + src/libxrpl/protocol/STValidation.cpp.ai.json | 618 ++++++++ src/libxrpl/protocol/STValidation.cpp.ai.md | 47 + src/libxrpl/protocol/STVar.cpp.ai.json | 409 +++++ src/libxrpl/protocol/STVar.cpp.ai.md | 60 + src/libxrpl/protocol/STVector256.cpp.ai.json | 296 ++++ src/libxrpl/protocol/STVector256.cpp.ai.md | 61 + .../protocol/STXChainBridge.cpp.ai.json | 669 ++++++++ src/libxrpl/protocol/STXChainBridge.cpp.ai.md | 54 + src/libxrpl/protocol/SecretKey.cpp.ai.json | 362 +++++ src/libxrpl/protocol/SecretKey.cpp.ai.md | 43 + src/libxrpl/protocol/Seed.cpp.ai.json | 650 ++++++++ src/libxrpl/protocol/Seed.cpp.ai.md | 41 + src/libxrpl/protocol/Serializer.cpp.ai.json | 654 ++++++++ src/libxrpl/protocol/Serializer.cpp.ai.md | 61 + src/libxrpl/protocol/Sign.cpp.ai.json | 229 +++ src/libxrpl/protocol/Sign.cpp.ai.md | 33 + src/libxrpl/protocol/TER.cpp.ai.json | 153 ++ src/libxrpl/protocol/TER.cpp.ai.md | 69 + src/libxrpl/protocol/TxFormats.cpp.ai.json | 1011 ++++++++++++ src/libxrpl/protocol/TxFormats.cpp.ai.md | 51 + src/libxrpl/protocol/TxMeta.cpp.ai.json | 597 ++++++++ src/libxrpl/protocol/TxMeta.cpp.ai.md | 45 + src/libxrpl/protocol/UintTypes.cpp.ai.json | 374 +++++ src/libxrpl/protocol/UintTypes.cpp.ai.md | 53 + .../protocol/XChainAttestations.cpp.ai.json | 1137 ++++++++++++++ .../protocol/XChainAttestations.cpp.ai.md | 38 + src/libxrpl/protocol/digest.cpp.ai.json | 413 +++++ src/libxrpl/protocol/digest.cpp.ai.md | 47 + src/libxrpl/protocol/tokens.cpp.ai.json | 609 ++++++++ src/libxrpl/protocol/tokens.cpp.ai.md | 59 + .../protocol_autogen/placeholder.cpp.ai.json | 41 + .../protocol_autogen/placeholder.cpp.ai.md | 14 + src/libxrpl/rdb/DatabaseCon.cpp.ai.json | 380 +++++ src/libxrpl/rdb/DatabaseCon.cpp.ai.md | 53 + src/libxrpl/rdb/SociDB.cpp.ai.json | 673 ++++++++ src/libxrpl/rdb/SociDB.cpp.ai.md | 43 + src/libxrpl/resource/Charge.cpp.ai.json | 205 +++ src/libxrpl/resource/Charge.cpp.ai.md | 45 + src/libxrpl/resource/Consumer.cpp.ai.json | 479 ++++++ src/libxrpl/resource/Consumer.cpp.ai.md | 37 + src/libxrpl/resource/Fees.cpp.ai.json | 94 ++ src/libxrpl/resource/Fees.cpp.ai.md | 46 + .../resource/ResourceManager.cpp.ai.json | 419 +++++ .../resource/ResourceManager.cpp.ai.md | 39 + src/libxrpl/server/InfoSub.cpp.ai.json | 482 ++++++ src/libxrpl/server/InfoSub.cpp.ai.md | 41 + src/libxrpl/server/JSONRPCUtil.cpp.ai.json | 185 +++ src/libxrpl/server/JSONRPCUtil.cpp.ai.md | 38 + src/libxrpl/server/LoadFeeTrack.cpp.ai.json | 421 +++++ src/libxrpl/server/LoadFeeTrack.cpp.ai.md | 39 + src/libxrpl/server/Manifest.cpp.ai.json | 744 +++++++++ src/libxrpl/server/Manifest.cpp.ai.md | 41 + src/libxrpl/server/Port.cpp.ai.json | 332 ++++ src/libxrpl/server/Port.cpp.ai.md | 42 + src/libxrpl/server/State.cpp.ai.json | 246 +++ src/libxrpl/server/State.cpp.ai.md | 31 + src/libxrpl/server/Vacuum.cpp.ai.json | 188 +++ src/libxrpl/server/Vacuum.cpp.ai.md | 29 + src/libxrpl/server/Wallet.cpp.ai.json | 344 +++++ src/libxrpl/server/Wallet.cpp.ai.md | 43 + src/libxrpl/shamap/SHAMap.cpp.ai.json | 689 +++++++++ src/libxrpl/shamap/SHAMap.cpp.ai.md | 101 ++ src/libxrpl/shamap/SHAMapDelta.cpp.ai.json | 434 ++++++ src/libxrpl/shamap/SHAMapDelta.cpp.ai.md | 61 + .../shamap/SHAMapInnerNode.cpp.ai.json | 594 ++++++++ src/libxrpl/shamap/SHAMapInnerNode.cpp.ai.md | 51 + src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.json | 356 +++++ src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.md | 57 + src/libxrpl/shamap/SHAMapNodeID.cpp.ai.json | 608 ++++++++ src/libxrpl/shamap/SHAMapNodeID.cpp.ai.md | 57 + src/libxrpl/shamap/SHAMapSync.cpp.ai.json | 570 +++++++ src/libxrpl/shamap/SHAMapSync.cpp.ai.md | 45 + src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.json | 427 ++++++ src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.md | 45 + src/libxrpl/tx/ApplyContext.cpp.ai.json | 455 ++++++ src/libxrpl/tx/ApplyContext.cpp.ai.md | 67 + src/libxrpl/tx/SignerEntries.cpp.ai.json | 187 +++ src/libxrpl/tx/SignerEntries.cpp.ai.md | 40 + src/libxrpl/tx/Transactor.cpp.ai.json | 621 ++++++++ src/libxrpl/tx/Transactor.cpp.ai.md | 121 ++ src/libxrpl/tx/apply.cpp.ai.json | 362 +++++ src/libxrpl/tx/apply.cpp.ai.md | 56 + src/libxrpl/tx/applySteps.cpp.ai.json | 351 +++++ src/libxrpl/tx/applySteps.cpp.ai.md | 50 + .../tx/invariants/AMMInvariant.cpp.ai.json | 546 +++++++ .../tx/invariants/AMMInvariant.cpp.ai.md | 57 + .../tx/invariants/FreezeInvariant.cpp.ai.json | 418 +++++ .../tx/invariants/FreezeInvariant.cpp.ai.md | 62 + .../tx/invariants/InvariantCheck.cpp.ai.json | 586 +++++++ .../tx/invariants/InvariantCheck.cpp.ai.md | 60 + .../LoanBrokerInvariant.cpp.ai.json | 429 ++++++ .../invariants/LoanBrokerInvariant.cpp.ai.md | 54 + .../tx/invariants/LoanInvariant.cpp.ai.json | 430 ++++++ .../tx/invariants/LoanInvariant.cpp.ai.md | 31 + .../tx/invariants/MPTInvariant.cpp.ai.json | 569 +++++++ .../tx/invariants/MPTInvariant.cpp.ai.md | 49 + .../tx/invariants/NFTInvariant.cpp.ai.json | 529 +++++++ .../tx/invariants/NFTInvariant.cpp.ai.md | 56 + .../PermissionedDEXInvariant.cpp.ai.json | 499 ++++++ .../PermissionedDEXInvariant.cpp.ai.md | 39 + .../PermissionedDomainInvariant.cpp.ai.json | 231 +++ .../PermissionedDomainInvariant.cpp.ai.md | 41 + .../tx/invariants/VaultInvariant.cpp.ai.json | 352 +++++ .../tx/invariants/VaultInvariant.cpp.ai.md | 61 + src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.json | 361 +++++ src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.md | 40 + src/libxrpl/tx/paths/AMMOffer.cpp.ai.json | 246 +++ src/libxrpl/tx/paths/AMMOffer.cpp.ai.md | 50 + src/libxrpl/tx/paths/BookStep.cpp.ai.json | 573 +++++++ src/libxrpl/tx/paths/BookStep.cpp.ai.md | 49 + src/libxrpl/tx/paths/BookTip.cpp.ai.json | 358 +++++ src/libxrpl/tx/paths/BookTip.cpp.ai.md | 46 + src/libxrpl/tx/paths/DirectStep.cpp.ai.json | 750 +++++++++ src/libxrpl/tx/paths/DirectStep.cpp.ai.md | 59 + src/libxrpl/tx/paths/Flow.cpp.ai.json | 301 ++++ src/libxrpl/tx/paths/Flow.cpp.ai.md | 38 + .../tx/paths/MPTEndpointStep.cpp.ai.json | 378 +++++ .../tx/paths/MPTEndpointStep.cpp.ai.md | 39 + src/libxrpl/tx/paths/OfferStream.cpp.ai.json | 427 ++++++ src/libxrpl/tx/paths/OfferStream.cpp.ai.md | 52 + src/libxrpl/tx/paths/PaySteps.cpp.ai.json | 344 +++++ src/libxrpl/tx/paths/PaySteps.cpp.ai.md | 61 + src/libxrpl/tx/paths/RippleCalc.cpp.ai.json | 355 +++++ src/libxrpl/tx/paths/RippleCalc.cpp.ai.md | 47 + .../tx/paths/XRPEndpointStep.cpp.ai.json | 471 ++++++ .../tx/paths/XRPEndpointStep.cpp.ai.md | 73 + .../account/AccountDelete.cpp.ai.json | 635 ++++++++ .../account/AccountDelete.cpp.ai.md | 51 + .../account/AccountSet.cpp.ai.json | 315 ++++ .../transactors/account/AccountSet.cpp.ai.md | 41 + .../account/SetRegularKey.cpp.ai.json | 406 +++++ .../account/SetRegularKey.cpp.ai.md | 43 + .../account/SignerListSet.cpp.ai.json | 415 +++++ .../account/SignerListSet.cpp.ai.md | 55 + .../bridge/XChainBridge.cpp.ai.json | 1057 +++++++++++++ .../transactors/bridge/XChainBridge.cpp.ai.md | 57 + .../transactors/check/CheckCancel.cpp.ai.json | 320 ++++ .../transactors/check/CheckCancel.cpp.ai.md | 45 + .../transactors/check/CheckCash.cpp.ai.json | 450 ++++++ .../tx/transactors/check/CheckCash.cpp.ai.md | 62 + .../transactors/check/CheckCreate.cpp.ai.json | 531 +++++++ .../transactors/check/CheckCreate.cpp.ai.md | 51 + .../credentials/CredentialAccept.cpp.ai.json | 402 +++++ .../credentials/CredentialAccept.cpp.ai.md | 40 + .../credentials/CredentialCreate.cpp.ai.json | 329 ++++ .../credentials/CredentialCreate.cpp.ai.md | 57 + .../credentials/CredentialDelete.cpp.ai.json | 328 ++++ .../credentials/CredentialDelete.cpp.ai.md | 56 + .../delegate/DelegateSet.cpp.ai.json | 562 +++++++ .../delegate/DelegateSet.cpp.ai.md | 57 + .../delegate/DelegateUtils.cpp.ai.json | 420 +++++ .../delegate/DelegateUtils.cpp.ai.md | 29 + .../tx/transactors/dex/AMMBid.cpp.ai.json | 571 +++++++ .../tx/transactors/dex/AMMBid.cpp.ai.md | 55 + .../transactors/dex/AMMClawback.cpp.ai.json | 673 ++++++++ .../tx/transactors/dex/AMMClawback.cpp.ai.md | 41 + .../tx/transactors/dex/AMMCreate.cpp.ai.json | 490 ++++++ .../tx/transactors/dex/AMMCreate.cpp.ai.md | 57 + .../tx/transactors/dex/AMMDelete.cpp.ai.json | 251 +++ .../tx/transactors/dex/AMMDelete.cpp.ai.md | 44 + .../tx/transactors/dex/AMMDeposit.cpp.ai.json | 613 ++++++++ .../tx/transactors/dex/AMMDeposit.cpp.ai.md | 58 + .../tx/transactors/dex/AMMVote.cpp.ai.json | 509 +++++++ .../tx/transactors/dex/AMMVote.cpp.ai.md | 49 + .../transactors/dex/AMMWithdraw.cpp.ai.json | 769 ++++++++++ .../tx/transactors/dex/AMMWithdraw.cpp.ai.md | 73 + .../transactors/dex/OfferCancel.cpp.ai.json | 237 +++ .../tx/transactors/dex/OfferCancel.cpp.ai.md | 39 + .../transactors/dex/OfferCreate.cpp.ai.json | 576 +++++++ .../tx/transactors/dex/OfferCreate.cpp.ai.md | 59 + .../tx/transactors/did/DIDDelete.cpp.ai.json | 300 ++++ .../tx/transactors/did/DIDDelete.cpp.ai.md | 41 + .../tx/transactors/did/DIDSet.cpp.ai.json | 428 ++++++ .../tx/transactors/did/DIDSet.cpp.ai.md | 39 + .../tx/transactors/escrow/Escrow.cpp.ai.json | 601 ++++++++ .../tx/transactors/escrow/Escrow.cpp.ai.md | 31 + .../escrow/EscrowCancel.cpp.ai.json | 365 +++++ .../transactors/escrow/EscrowCancel.cpp.ai.md | 41 + .../escrow/EscrowCreate.cpp.ai.json | 454 ++++++ .../transactors/escrow/EscrowCreate.cpp.ai.md | 60 + .../escrow/EscrowFinish.cpp.ai.json | 383 +++++ .../transactors/escrow/EscrowFinish.cpp.ai.md | 41 + .../lending/LendingHelpers.cpp.ai.json | 1143 ++++++++++++++ .../lending/LendingHelpers.cpp.ai.md | 66 + .../LoanBrokerCoverClawback.cpp.ai.json | 536 +++++++ .../lending/LoanBrokerCoverClawback.cpp.ai.md | 54 + .../LoanBrokerCoverDeposit.cpp.ai.json | 545 +++++++ .../lending/LoanBrokerCoverDeposit.cpp.ai.md | 38 + .../LoanBrokerCoverWithdraw.cpp.ai.json | 496 ++++++ .../lending/LoanBrokerCoverWithdraw.cpp.ai.md | 60 + .../lending/LoanBrokerDelete.cpp.ai.json | 406 +++++ .../lending/LoanBrokerDelete.cpp.ai.md | 57 + .../lending/LoanBrokerSet.cpp.ai.json | 606 ++++++++ .../lending/LoanBrokerSet.cpp.ai.md | 57 + .../lending/LoanDelete.cpp.ai.json | 481 ++++++ .../transactors/lending/LoanDelete.cpp.ai.md | 61 + .../lending/LoanManage.cpp.ai.json | 466 ++++++ .../transactors/lending/LoanManage.cpp.ai.md | 68 + .../transactors/lending/LoanPay.cpp.ai.json | 425 ++++++ .../tx/transactors/lending/LoanPay.cpp.ai.md | 41 + .../transactors/lending/LoanSet.cpp.ai.json | 227 +++ .../tx/transactors/lending/LoanSet.cpp.ai.md | 64 + .../nft/NFTokenAcceptOffer.cpp.ai.json | 604 ++++++++ .../nft/NFTokenAcceptOffer.cpp.ai.md | 47 + .../transactors/nft/NFTokenBurn.cpp.ai.json | 361 +++++ .../tx/transactors/nft/NFTokenBurn.cpp.ai.md | 41 + .../nft/NFTokenCancelOffer.cpp.ai.json | 338 +++++ .../nft/NFTokenCancelOffer.cpp.ai.md | 44 + .../nft/NFTokenCreateOffer.cpp.ai.json | 590 +++++++ .../nft/NFTokenCreateOffer.cpp.ai.md | 40 + .../transactors/nft/NFTokenMint.cpp.ai.json | 538 +++++++ .../tx/transactors/nft/NFTokenMint.cpp.ai.md | 63 + .../transactors/nft/NFTokenModify.cpp.ai.json | 300 ++++ .../transactors/nft/NFTokenModify.cpp.ai.md | 47 + .../oracle/OracleDelete.cpp.ai.json | 462 ++++++ .../transactors/oracle/OracleDelete.cpp.ai.md | 42 + .../transactors/oracle/OracleSet.cpp.ai.json | 543 +++++++ .../tx/transactors/oracle/OracleSet.cpp.ai.md | 44 + .../payment/DepositPreauth.cpp.ai.json | 332 ++++ .../payment/DepositPreauth.cpp.ai.md | 48 + .../transactors/payment/Payment.cpp.ai.json | 676 +++++++++ .../tx/transactors/payment/Payment.cpp.ai.md | 75 + .../PaymentChannelClaim.cpp.ai.json | 619 ++++++++ .../PaymentChannelClaim.cpp.ai.md | 55 + .../PaymentChannelCreate.cpp.ai.json | 450 ++++++ .../PaymentChannelCreate.cpp.ai.md | 46 + .../PaymentChannelFund.cpp.ai.json | 407 +++++ .../PaymentChannelFund.cpp.ai.md | 33 + .../PermissionedDomainDelete.cpp.ai.json | 383 +++++ .../PermissionedDomainDelete.cpp.ai.md | 29 + .../PermissionedDomainSet.cpp.ai.json | 321 ++++ .../PermissionedDomainSet.cpp.ai.md | 37 + .../tx/transactors/system/Batch.cpp.ai.json | 218 +++ .../tx/transactors/system/Batch.cpp.ai.md | 52 + .../tx/transactors/system/Change.cpp.ai.json | 420 +++++ .../tx/transactors/system/Change.cpp.ai.md | 51 + .../system/LedgerStateFix.cpp.ai.json | 231 +++ .../system/LedgerStateFix.cpp.ai.md | 53 + .../system/TicketCreate.cpp.ai.json | 392 +++++ .../transactors/system/TicketCreate.cpp.ai.md | 67 + .../tx/transactors/token/Clawback.cpp.ai.json | 596 ++++++++ .../tx/transactors/token/Clawback.cpp.ai.md | 35 + .../token/MPTokenAuthorize.cpp.ai.json | 449 ++++++ .../token/MPTokenAuthorize.cpp.ai.md | 41 + .../token/MPTokenIssuanceCreate.cpp.ai.json | 505 ++++++ .../token/MPTokenIssuanceCreate.cpp.ai.md | 62 + .../token/MPTokenIssuanceDestroy.cpp.ai.json | 320 ++++ .../token/MPTokenIssuanceDestroy.cpp.ai.md | 45 + .../token/MPTokenIssuanceSet.cpp.ai.json | 530 +++++++ .../token/MPTokenIssuanceSet.cpp.ai.md | 58 + .../tx/transactors/token/TrustSet.cpp.ai.json | 428 ++++++ .../tx/transactors/token/TrustSet.cpp.ai.md | 44 + .../vault/VaultClawback.cpp.ai.json | 471 ++++++ .../transactors/vault/VaultClawback.cpp.ai.md | 37 + .../transactors/vault/VaultCreate.cpp.ai.json | 524 +++++++ .../transactors/vault/VaultCreate.cpp.ai.md | 60 + .../transactors/vault/VaultDelete.cpp.ai.json | 484 ++++++ .../transactors/vault/VaultDelete.cpp.ai.md | 45 + .../vault/VaultDeposit.cpp.ai.json | 513 +++++++ .../transactors/vault/VaultDeposit.cpp.ai.md | 53 + .../tx/transactors/vault/VaultSet.cpp.ai.json | 507 +++++++ .../tx/transactors/vault/VaultSet.cpp.ai.md | 35 + .../vault/VaultWithdraw.cpp.ai.json | 193 +++ .../transactors/vault/VaultWithdraw.cpp.ai.md | 53 + .../consensus/RCLCensorshipDetector.h.ai.json | 110 ++ .../consensus/RCLCensorshipDetector.h.ai.md | 49 + .../app/consensus/RCLConsensus.cpp.ai.json | 766 ++++++++++ .../app/consensus/RCLConsensus.cpp.ai.md | 59 + .../app/consensus/RCLConsensus.h.ai.json | 130 ++ src/xrpld/app/consensus/RCLConsensus.h.ai.md | 56 + src/xrpld/app/consensus/RCLCxLedger.h.ai.json | 80 + src/xrpld/app/consensus/RCLCxLedger.h.ai.md | 55 + .../app/consensus/RCLCxPeerPos.cpp.ai.json | 416 +++++ .../app/consensus/RCLCxPeerPos.cpp.ai.md | 55 + .../app/consensus/RCLCxPeerPos.h.ai.json | 121 ++ src/xrpld/app/consensus/RCLCxPeerPos.h.ai.md | 49 + src/xrpld/app/consensus/RCLCxTx.h.ai.json | 147 ++ src/xrpld/app/consensus/RCLCxTx.h.ai.md | 33 + .../app/consensus/RCLValidations.cpp.ai.json | 327 ++++ .../app/consensus/RCLValidations.cpp.ai.md | 35 + .../app/consensus/RCLValidations.h.ai.json | 96 ++ .../app/consensus/RCLValidations.h.ai.md | 53 + .../AbstractFetchPackContainer.h.ai.json | 33 + .../ledger/AbstractFetchPackContainer.h.ai.md | 27 + .../app/ledger/AcceptedLedger.cpp.ai.json | 149 ++ src/xrpld/app/ledger/AcceptedLedger.cpp.ai.md | 36 + src/xrpld/app/ledger/AcceptedLedger.h.ai.json | 55 + src/xrpld/app/ledger/AcceptedLedger.h.ai.md | 31 + .../app/ledger/AccountStateSF.cpp.ai.json | 114 ++ src/xrpld/app/ledger/AccountStateSF.cpp.ai.md | 49 + src/xrpld/app/ledger/AccountStateSF.h.ai.json | 71 + src/xrpld/app/ledger/AccountStateSF.h.ai.md | 23 + src/xrpld/app/ledger/BuildLedger.h.ai.json | 39 + src/xrpld/app/ledger/BuildLedger.h.ai.md | 70 + .../ledger/ConsensusTransSetSF.cpp.ai.json | 265 ++++ .../app/ledger/ConsensusTransSetSF.cpp.ai.md | 31 + .../app/ledger/ConsensusTransSetSF.h.ai.json | 50 + .../app/ledger/ConsensusTransSetSF.h.ai.md | 25 + src/xrpld/app/ledger/InboundLedger.h.ai.json | 233 +++ src/xrpld/app/ledger/InboundLedger.h.ai.md | 69 + src/xrpld/app/ledger/InboundLedgers.h.ai.json | 43 + src/xrpld/app/ledger/InboundLedgers.h.ai.md | 41 + .../app/ledger/InboundTransactions.h.ai.json | 67 + .../app/ledger/InboundTransactions.h.ai.md | 41 + src/xrpld/app/ledger/LedgerCleaner.h.ai.json | 42 + src/xrpld/app/ledger/LedgerCleaner.h.ai.md | 41 + .../app/ledger/LedgerHistory.cpp.ai.json | 411 +++++ src/xrpld/app/ledger/LedgerHistory.cpp.ai.md | 47 + src/xrpld/app/ledger/LedgerHistory.h.ai.json | 155 ++ src/xrpld/app/ledger/LedgerHistory.h.ai.md | 42 + src/xrpld/app/ledger/LedgerHolder.h.ai.json | 38 + src/xrpld/app/ledger/LedgerHolder.h.ai.md | 37 + src/xrpld/app/ledger/LedgerMaster.h.ai.json | 507 +++++++ src/xrpld/app/ledger/LedgerMaster.h.ai.md | 62 + .../app/ledger/LedgerPersistence.h.ai.json | 67 + .../app/ledger/LedgerPersistence.h.ai.md | 37 + src/xrpld/app/ledger/LedgerReplay.h.ai.json | 39 + src/xrpld/app/ledger/LedgerReplay.h.ai.md | 33 + .../app/ledger/LedgerReplayTask.h.ai.json | 211 +++ src/xrpld/app/ledger/LedgerReplayTask.h.ai.md | 43 + src/xrpld/app/ledger/LedgerReplayer.h.ai.json | 103 ++ src/xrpld/app/ledger/LedgerReplayer.h.ai.md | 38 + src/xrpld/app/ledger/LedgerToJson.h.ai.json | 55 + src/xrpld/app/ledger/LedgerToJson.h.ai.md | 60 + src/xrpld/app/ledger/LocalTxs.h.ai.json | 51 + src/xrpld/app/ledger/LocalTxs.h.ai.md | 46 + src/xrpld/app/ledger/OpenLedger.h.ai.json | 118 ++ src/xrpld/app/ledger/OpenLedger.h.ai.md | 60 + src/xrpld/app/ledger/OrderBookDB.h.ai.json | 37 + src/xrpld/app/ledger/OrderBookDB.h.ai.md | 39 + .../app/ledger/OrderBookDBImpl.cpp.ai.json | 579 +++++++ .../app/ledger/OrderBookDBImpl.cpp.ai.md | 50 + .../app/ledger/OrderBookDBImpl.h.ai.json | 46 + src/xrpld/app/ledger/OrderBookDBImpl.h.ai.md | 53 + .../app/ledger/TransactionMaster.h.ai.json | 90 ++ .../app/ledger/TransactionMaster.h.ai.md | 29 + .../app/ledger/TransactionStateSF.cpp.ai.json | 151 ++ .../app/ledger/TransactionStateSF.cpp.ai.md | 53 + .../app/ledger/TransactionStateSF.h.ai.json | 79 + .../app/ledger/TransactionStateSF.h.ai.md | 57 + .../app/ledger/detail/BuildLedger.cpp.ai.json | 414 +++++ .../app/ledger/detail/BuildLedger.cpp.ai.md | 45 + .../ledger/detail/InboundLedger.cpp.ai.json | 648 ++++++++ .../app/ledger/detail/InboundLedger.cpp.ai.md | 56 + .../ledger/detail/InboundLedgers.cpp.ai.json | 748 +++++++++ .../ledger/detail/InboundLedgers.cpp.ai.md | 41 + .../detail/InboundTransactions.cpp.ai.json | 613 ++++++++ .../detail/InboundTransactions.cpp.ai.md | 35 + .../ledger/detail/LedgerCleaner.cpp.ai.json | 509 +++++++ .../app/ledger/detail/LedgerCleaner.cpp.ai.md | 43 + .../detail/LedgerDeltaAcquire.cpp.ai.json | 635 ++++++++ .../detail/LedgerDeltaAcquire.cpp.ai.md | 37 + .../detail/LedgerDeltaAcquire.h.ai.json | 162 ++ .../ledger/detail/LedgerDeltaAcquire.h.ai.md | 41 + .../ledger/detail/LedgerMaster.cpp.ai.json | 858 +++++++++++ .../app/ledger/detail/LedgerMaster.cpp.ai.md | 106 ++ .../detail/LedgerPersistence.cpp.ai.json | 425 ++++++ .../ledger/detail/LedgerPersistence.cpp.ai.md | 36 + .../ledger/detail/LedgerReplay.cpp.ai.json | 141 ++ .../app/ledger/detail/LedgerReplay.cpp.ai.md | 25 + .../detail/LedgerReplayMsgHandler.cpp.ai.json | 463 ++++++ .../detail/LedgerReplayMsgHandler.cpp.ai.md | 31 + .../detail/LedgerReplayMsgHandler.h.ai.json | 65 + .../detail/LedgerReplayMsgHandler.h.ai.md | 29 + .../detail/LedgerReplayTask.cpp.ai.json | 441 ++++++ .../ledger/detail/LedgerReplayTask.cpp.ai.md | 48 + .../ledger/detail/LedgerReplayer.cpp.ai.json | 435 ++++++ .../ledger/detail/LedgerReplayer.cpp.ai.md | 57 + .../ledger/detail/LedgerToJson.cpp.ai.json | 597 ++++++++ .../app/ledger/detail/LedgerToJson.cpp.ai.md | 59 + .../app/ledger/detail/LocalTxs.cpp.ai.json | 486 ++++++ .../app/ledger/detail/LocalTxs.cpp.ai.md | 72 + .../app/ledger/detail/OpenLedger.cpp.ai.json | 872 +++++++++++ .../app/ledger/detail/OpenLedger.cpp.ai.md | 58 + .../ledger/detail/SkipListAcquire.cpp.ai.json | 531 +++++++ .../ledger/detail/SkipListAcquire.cpp.ai.md | 43 + .../ledger/detail/SkipListAcquire.h.ai.json | 179 +++ .../app/ledger/detail/SkipListAcquire.h.ai.md | 45 + .../ledger/detail/TimeoutCounter.cpp.ai.json | 258 ++++ .../ledger/detail/TimeoutCounter.cpp.ai.md | 41 + .../ledger/detail/TimeoutCounter.h.ai.json | 63 + .../app/ledger/detail/TimeoutCounter.h.ai.md | 39 + .../detail/TransactionAcquire.cpp.ai.json | 614 ++++++++ .../detail/TransactionAcquire.cpp.ai.md | 53 + .../detail/TransactionAcquire.h.ai.json | 91 ++ .../ledger/detail/TransactionAcquire.h.ai.md | 54 + .../detail/TransactionMaster.cpp.ai.json | 465 ++++++ .../ledger/detail/TransactionMaster.cpp.ai.md | 37 + src/xrpld/app/main/Application.cpp.ai.json | 831 ++++++++++ src/xrpld/app/main/Application.cpp.ai.md | 72 + src/xrpld/app/main/Application.h.ai.json | 88 ++ src/xrpld/app/main/Application.h.ai.md | 60 + src/xrpld/app/main/BasicApp.cpp.ai.json | 97 ++ src/xrpld/app/main/BasicApp.cpp.ai.md | 40 + src/xrpld/app/main/BasicApp.h.ai.json | 33 + src/xrpld/app/main/BasicApp.h.ai.md | 38 + .../app/main/CollectorManager.cpp.ai.json | 236 +++ src/xrpld/app/main/CollectorManager.cpp.ai.md | 32 + src/xrpld/app/main/CollectorManager.h.ai.json | 38 + src/xrpld/app/main/CollectorManager.h.ai.md | 27 + src/xrpld/app/main/GRPCServer.cpp.ai.json | 377 +++++ src/xrpld/app/main/GRPCServer.cpp.ai.md | 61 + src/xrpld/app/main/GRPCServer.h.ai.json | 248 +++ src/xrpld/app/main/GRPCServer.h.ai.md | 51 + src/xrpld/app/main/LoadManager.cpp.ai.json | 325 ++++ src/xrpld/app/main/LoadManager.cpp.ai.md | 45 + src/xrpld/app/main/LoadManager.h.ai.json | 56 + src/xrpld/app/main/LoadManager.h.ai.md | 38 + src/xrpld/app/main/Main.cpp.ai.json | 291 ++++ src/xrpld/app/main/Main.cpp.ai.md | 53 + src/xrpld/app/main/NodeIdentity.cpp.ai.json | 214 +++ src/xrpld/app/main/NodeIdentity.cpp.ai.md | 40 + src/xrpld/app/main/NodeIdentity.h.ai.json | 32 + src/xrpld/app/main/NodeIdentity.h.ai.md | 24 + .../app/main/NodeStoreScheduler.cpp.ai.json | 260 ++++ .../app/main/NodeStoreScheduler.cpp.ai.md | 32 + .../app/main/NodeStoreScheduler.h.ai.json | 68 + src/xrpld/app/main/NodeStoreScheduler.h.ai.md | 31 + src/xrpld/app/main/Tuning.h.ai.json | 14 + src/xrpld/app/main/Tuning.h.ai.md | 23 + .../app/misc/AmendmentTableImpl.h.ai.json | 52 + src/xrpld/app/misc/AmendmentTableImpl.h.ai.md | 29 + src/xrpld/app/misc/DeliverMax.h.ai.json | 51 + src/xrpld/app/misc/DeliverMax.h.ai.md | 25 + src/xrpld/app/misc/FeeVote.h.ai.json | 80 + src/xrpld/app/misc/FeeVote.h.ai.md | 27 + src/xrpld/app/misc/FeeVoteImpl.cpp.ai.json | 509 +++++++ src/xrpld/app/misc/FeeVoteImpl.cpp.ai.md | 49 + .../app/misc/NegativeUNLVote.cpp.ai.json | 299 ++++ src/xrpld/app/misc/NegativeUNLVote.cpp.ai.md | 47 + src/xrpld/app/misc/NegativeUNLVote.h.ai.json | 168 ++ src/xrpld/app/misc/NegativeUNLVote.h.ai.md | 48 + src/xrpld/app/misc/NetworkOPs.cpp.ai.json | 1349 +++++++++++++++++ src/xrpld/app/misc/NetworkOPs.cpp.ai.md | 81 + src/xrpld/app/misc/SHAMapStore.h.ai.json | 47 + src/xrpld/app/misc/SHAMapStore.h.ai.md | 37 + src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.json | 691 +++++++++ src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.md | 60 + src/xrpld/app/misc/SHAMapStoreImp.h.ai.json | 216 +++ src/xrpld/app/misc/SHAMapStoreImp.h.ai.md | 49 + src/xrpld/app/misc/Transaction.h.ai.json | 380 +++++ src/xrpld/app/misc/Transaction.h.ai.md | 39 + src/xrpld/app/misc/TxQ.h.ai.json | 358 +++++ src/xrpld/app/misc/TxQ.h.ai.md | 55 + src/xrpld/app/misc/ValidatorKeys.h.ai.json | 67 + src/xrpld/app/misc/ValidatorKeys.h.ai.md | 39 + src/xrpld/app/misc/ValidatorList.h.ai.json | 109 ++ src/xrpld/app/misc/ValidatorList.h.ai.md | 65 + src/xrpld/app/misc/ValidatorSite.h.ai.json | 170 +++ src/xrpld/app/misc/ValidatorSite.h.ai.md | 57 + .../misc/detail/AccountTxPaging.cpp.ai.json | 410 +++++ .../app/misc/detail/AccountTxPaging.cpp.ai.md | 32 + .../app/misc/detail/AccountTxPaging.h.ai.json | 35 + .../app/misc/detail/AccountTxPaging.h.ai.md | 37 + .../misc/detail/AmendmentTable.cpp.ai.json | 742 +++++++++ .../app/misc/detail/AmendmentTable.cpp.ai.md | 67 + .../app/misc/detail/DeliverMax.cpp.ai.json | 240 +++ .../app/misc/detail/DeliverMax.cpp.ai.md | 35 + .../app/misc/detail/Transaction.cpp.ai.json | 465 ++++++ .../app/misc/detail/Transaction.cpp.ai.md | 41 + src/xrpld/app/misc/detail/TxQ.cpp.ai.json | 878 +++++++++++ src/xrpld/app/misc/detail/TxQ.cpp.ai.md | 104 ++ .../app/misc/detail/ValidatorKeys.cpp.ai.json | 303 ++++ .../app/misc/detail/ValidatorKeys.cpp.ai.md | 30 + .../app/misc/detail/ValidatorList.cpp.ai.json | 863 +++++++++++ .../app/misc/detail/ValidatorList.cpp.ai.md | 95 ++ .../app/misc/detail/ValidatorSite.cpp.ai.json | 679 +++++++++ .../app/misc/detail/ValidatorSite.cpp.ai.md | 54 + src/xrpld/app/misc/detail/Work.h.ai.json | 35 + src/xrpld/app/misc/detail/Work.h.ai.md | 44 + src/xrpld/app/misc/detail/WorkBase.h.ai.json | 142 ++ src/xrpld/app/misc/detail/WorkBase.h.ai.md | 52 + src/xrpld/app/misc/detail/WorkFile.h.ai.json | 64 + src/xrpld/app/misc/detail/WorkFile.h.ai.md | 56 + src/xrpld/app/misc/detail/WorkPlain.h.ai.json | 96 ++ src/xrpld/app/misc/detail/WorkPlain.h.ai.md | 36 + src/xrpld/app/misc/detail/WorkSSL.cpp.ai.json | 349 +++++ src/xrpld/app/misc/detail/WorkSSL.cpp.ai.md | 40 + src/xrpld/app/misc/detail/WorkSSL.h.ai.json | 115 ++ src/xrpld/app/misc/detail/WorkSSL.h.ai.md | 44 + .../misc/detail/setup_HashRouter.cpp.ai.json | 234 +++ .../misc/detail/setup_HashRouter.cpp.ai.md | 36 + src/xrpld/app/misc/make_NetworkOPs.h.ai.json | 43 + src/xrpld/app/misc/make_NetworkOPs.h.ai.md | 25 + src/xrpld/app/misc/setup_HashRouter.h.ai.json | 27 + src/xrpld/app/misc/setup_HashRouter.h.ai.md | 21 + src/xrpld/app/rdb/PeerFinder.h.ai.json | 90 ++ src/xrpld/app/rdb/PeerFinder.h.ai.md | 25 + .../app/rdb/backend/SQLiteDatabase.h.ai.json | 347 +++++ .../app/rdb/backend/SQLiteDatabase.h.ai.md | 50 + .../app/rdb/backend/detail/Node.cpp.ai.json | 685 +++++++++ .../app/rdb/backend/detail/Node.cpp.ai.md | 57 + .../app/rdb/backend/detail/Node.h.ai.json | 261 ++++ src/xrpld/app/rdb/backend/detail/Node.h.ai.md | 60 + .../backend/detail/SQLiteDatabase.cpp.ai.json | 726 +++++++++ .../backend/detail/SQLiteDatabase.cpp.ai.md | 53 + .../app/rdb/detail/PeerFinder.cpp.ai.json | 460 ++++++ src/xrpld/app/rdb/detail/PeerFinder.cpp.ai.md | 38 + src/xrpld/app/tx/detail/Taker.h.ai.json | 9 + src/xrpld/app/tx/detail/Taker.h.ai.md | 27 + src/xrpld/consensus/Consensus.cpp.ai.json | 569 +++++++ src/xrpld/consensus/Consensus.cpp.ai.md | 45 + src/xrpld/consensus/Consensus.h.ai.json | 155 ++ src/xrpld/consensus/Consensus.h.ai.md | 55 + src/xrpld/consensus/ConsensusParms.h.ai.json | 58 + src/xrpld/consensus/ConsensusParms.h.ai.md | 50 + .../consensus/ConsensusProposal.h.ai.json | 167 ++ src/xrpld/consensus/ConsensusProposal.h.ai.md | 42 + src/xrpld/consensus/ConsensusTypes.h.ai.json | 35 + src/xrpld/consensus/ConsensusTypes.h.ai.md | 71 + src/xrpld/consensus/DisputedTx.h.ai.json | 133 ++ src/xrpld/consensus/DisputedTx.h.ai.md | 60 + src/xrpld/consensus/LedgerTrie.h.ai.json | 226 +++ src/xrpld/consensus/LedgerTrie.h.ai.md | 71 + src/xrpld/consensus/Validations.h.ai.json | 54 + src/xrpld/consensus/Validations.h.ai.md | 65 + src/xrpld/core/Config.h.ai.json | 161 ++ src/xrpld/core/Config.h.ai.md | 51 + src/xrpld/core/ConfigSections.h.ai.json | 31 + src/xrpld/core/ConfigSections.h.ai.md | 26 + src/xrpld/core/NetworkIDServiceImpl.h.ai.json | 45 + src/xrpld/core/NetworkIDServiceImpl.h.ai.md | 21 + src/xrpld/core/TimeKeeper.h.ai.json | 50 + src/xrpld/core/TimeKeeper.h.ai.md | 35 + src/xrpld/core/detail/Config.cpp.ai.json | 512 +++++++ src/xrpld/core/detail/Config.cpp.ai.md | 65 + .../detail/NetworkIDServiceImpl.cpp.ai.json | 88 ++ .../detail/NetworkIDServiceImpl.cpp.ai.md | 19 + src/xrpld/overlay/Cluster.h.ai.json | 141 ++ src/xrpld/overlay/Cluster.h.ai.md | 27 + src/xrpld/overlay/ClusterNode.h.ai.json | 73 + src/xrpld/overlay/ClusterNode.h.ai.md | 27 + src/xrpld/overlay/Compression.h.ai.json | 77 + src/xrpld/overlay/Compression.h.ai.md | 31 + src/xrpld/overlay/Message.h.ai.json | 93 ++ src/xrpld/overlay/Message.h.ai.md | 42 + src/xrpld/overlay/Overlay.h.ai.json | 203 +++ src/xrpld/overlay/Overlay.h.ai.md | 45 + src/xrpld/overlay/Peer.h.ai.json | 24 + src/xrpld/overlay/Peer.h.ai.md | 46 + src/xrpld/overlay/PeerSet.h.ai.json | 117 ++ src/xrpld/overlay/PeerSet.h.ai.md | 34 + src/xrpld/overlay/ReduceRelayCommon.h.ai.json | 18 + src/xrpld/overlay/ReduceRelayCommon.h.ai.md | 40 + src/xrpld/overlay/Slot.h.ai.json | 238 +++ src/xrpld/overlay/Slot.h.ai.md | 73 + src/xrpld/overlay/Squelch.h.ai.json | 62 + src/xrpld/overlay/Squelch.h.ai.md | 69 + src/xrpld/overlay/detail/Cluster.cpp.ai.json | 400 +++++ src/xrpld/overlay/detail/Cluster.cpp.ai.md | 49 + .../overlay/detail/ConnectAttempt.cpp.ai.json | 464 ++++++ .../overlay/detail/ConnectAttempt.cpp.ai.md | 55 + .../overlay/detail/ConnectAttempt.h.ai.json | 250 +++ .../overlay/detail/ConnectAttempt.h.ai.md | 43 + .../overlay/detail/Handshake.cpp.ai.json | 464 ++++++ src/xrpld/overlay/detail/Handshake.cpp.ai.md | 49 + src/xrpld/overlay/detail/Handshake.h.ai.json | 137 ++ src/xrpld/overlay/detail/Handshake.h.ai.md | 74 + src/xrpld/overlay/detail/Message.cpp.ai.json | 269 ++++ src/xrpld/overlay/detail/Message.cpp.ai.md | 29 + .../overlay/detail/OverlayImpl.cpp.ai.json | 843 ++++++++++ .../overlay/detail/OverlayImpl.cpp.ai.md | 99 ++ .../overlay/detail/OverlayImpl.h.ai.json | 325 ++++ src/xrpld/overlay/detail/OverlayImpl.h.ai.md | 47 + src/xrpld/overlay/detail/PeerImp.cpp.ai.json | 1292 ++++++++++++++++ src/xrpld/overlay/detail/PeerImp.cpp.ai.md | 40 + src/xrpld/overlay/detail/PeerImp.h.ai.json | 685 +++++++++ src/xrpld/overlay/detail/PeerImp.h.ai.md | 91 ++ .../detail/PeerReservationTable.cpp.ai.json | 456 ++++++ .../detail/PeerReservationTable.cpp.ai.md | 33 + src/xrpld/overlay/detail/PeerSet.cpp.ai.json | 430 ++++++ src/xrpld/overlay/detail/PeerSet.cpp.ai.md | 35 + .../overlay/detail/ProtocolMessage.h.ai.json | 102 ++ .../overlay/detail/ProtocolMessage.h.ai.md | 59 + .../detail/ProtocolVersion.cpp.ai.json | 356 +++++ .../overlay/detail/ProtocolVersion.cpp.ai.md | 37 + .../overlay/detail/ProtocolVersion.h.ai.json | 92 ++ .../overlay/detail/ProtocolVersion.h.ai.md | 35 + .../overlay/detail/TrafficCount.cpp.ai.json | 392 +++++ .../overlay/detail/TrafficCount.cpp.ai.md | 43 + .../overlay/detail/TrafficCount.h.ai.json | 49 + src/xrpld/overlay/detail/TrafficCount.h.ai.md | 51 + src/xrpld/overlay/detail/Tuning.h.ai.json | 18 + src/xrpld/overlay/detail/Tuning.h.ai.md | 38 + .../overlay/detail/TxMetrics.cpp.ai.json | 565 +++++++ src/xrpld/overlay/detail/TxMetrics.cpp.ai.md | 35 + src/xrpld/overlay/detail/TxMetrics.h.ai.json | 128 ++ src/xrpld/overlay/detail/TxMetrics.h.ai.md | 57 + .../overlay/detail/ZeroCopyStream.h.ai.json | 164 ++ .../overlay/detail/ZeroCopyStream.h.ai.md | 27 + src/xrpld/overlay/make_Overlay.h.ai.json | 36 + src/xrpld/overlay/make_Overlay.h.ai.md | 50 + src/xrpld/overlay/predicates.h.ai.json | 149 ++ src/xrpld/overlay/predicates.h.ai.md | 35 + .../peerfinder/PeerfinderManager.h.ai.json | 320 ++++ .../peerfinder/PeerfinderManager.h.ai.md | 41 + src/xrpld/peerfinder/Slot.h.ai.json | 70 + src/xrpld/peerfinder/Slot.h.ai.md | 41 + .../peerfinder/detail/Bootcache.cpp.ai.json | 399 +++++ .../peerfinder/detail/Bootcache.cpp.ai.md | 51 + .../peerfinder/detail/Bootcache.h.ai.json | 155 ++ src/xrpld/peerfinder/detail/Bootcache.h.ai.md | 50 + src/xrpld/peerfinder/detail/Checker.h.ai.json | 129 ++ src/xrpld/peerfinder/detail/Checker.h.ai.md | 59 + src/xrpld/peerfinder/detail/Counts.h.ai.json | 148 ++ src/xrpld/peerfinder/detail/Counts.h.ai.md | 47 + .../peerfinder/detail/Endpoint.cpp.ai.json | 140 ++ .../peerfinder/detail/Endpoint.cpp.ai.md | 34 + src/xrpld/peerfinder/detail/Fixed.h.ai.json | 59 + src/xrpld/peerfinder/detail/Fixed.h.ai.md | 45 + .../peerfinder/detail/Handouts.h.ai.json | 117 ++ src/xrpld/peerfinder/detail/Handouts.h.ai.md | 27 + .../peerfinder/detail/Livecache.h.ai.json | 136 ++ src/xrpld/peerfinder/detail/Livecache.h.ai.md | 51 + src/xrpld/peerfinder/detail/Logic.h.ai.json | 281 ++++ src/xrpld/peerfinder/detail/Logic.h.ai.md | 78 + .../detail/PeerfinderConfig.cpp.ai.json | 346 +++++ .../detail/PeerfinderConfig.cpp.ai.md | 55 + .../detail/PeerfinderManager.cpp.ai.json | 701 +++++++++ .../detail/PeerfinderManager.cpp.ai.md | 39 + .../peerfinder/detail/SlotImp.cpp.ai.json | 461 ++++++ src/xrpld/peerfinder/detail/SlotImp.cpp.ai.md | 47 + src/xrpld/peerfinder/detail/SlotImp.h.ai.json | 211 +++ src/xrpld/peerfinder/detail/SlotImp.h.ai.md | 45 + src/xrpld/peerfinder/detail/Source.h.ai.json | 53 + src/xrpld/peerfinder/detail/Source.h.ai.md | 27 + .../detail/SourceStrings.cpp.ai.json | 262 ++++ .../peerfinder/detail/SourceStrings.cpp.ai.md | 41 + .../peerfinder/detail/SourceStrings.h.ai.json | 42 + .../peerfinder/detail/SourceStrings.h.ai.md | 27 + src/xrpld/peerfinder/detail/Store.h.ai.json | 44 + src/xrpld/peerfinder/detail/Store.h.ai.md | 39 + .../peerfinder/detail/StoreSqdb.h.ai.json | 77 + src/xrpld/peerfinder/detail/StoreSqdb.h.ai.md | 31 + src/xrpld/peerfinder/detail/Tuning.h.ai.json | 22 + src/xrpld/peerfinder/detail/Tuning.h.ai.md | 48 + .../peerfinder/detail/iosformat.h.ai.json | 200 +++ src/xrpld/peerfinder/detail/iosformat.h.ai.md | 25 + src/xrpld/peerfinder/make_Manager.h.ai.json | 51 + src/xrpld/peerfinder/make_Manager.h.ai.md | 51 + .../perflog/detail/PerfLogImp.cpp.ai.json | 704 +++++++++ src/xrpld/perflog/detail/PerfLogImp.cpp.ai.md | 54 + src/xrpld/perflog/detail/PerfLogImp.h.ai.json | 105 ++ src/xrpld/perflog/detail/PerfLogImp.h.ai.md | 49 + src/xrpld/rpc/BookChanges.h.ai.json | 61 + src/xrpld/rpc/BookChanges.h.ai.md | 50 + src/xrpld/rpc/CTID.h.ai.json | 52 + src/xrpld/rpc/CTID.h.ai.md | 39 + src/xrpld/rpc/Context.h.ai.json | 41 + src/xrpld/rpc/Context.h.ai.md | 45 + src/xrpld/rpc/DeliveredAmount.h.ai.json | 63 + src/xrpld/rpc/DeliveredAmount.h.ai.md | 37 + src/xrpld/rpc/GRPCHandlers.h.ai.json | 43 + src/xrpld/rpc/GRPCHandlers.h.ai.md | 44 + src/xrpld/rpc/MPTokenIssuanceID.h.ai.json | 43 + src/xrpld/rpc/MPTokenIssuanceID.h.ai.md | 33 + src/xrpld/rpc/Output.h.ai.json | 35 + src/xrpld/rpc/Output.h.ai.md | 9 + src/xrpld/rpc/RPCCall.h.ai.json | 68 + src/xrpld/rpc/RPCCall.h.ai.md | 34 + src/xrpld/rpc/RPCHandler.h.ai.json | 36 + src/xrpld/rpc/RPCHandler.h.ai.md | 44 + src/xrpld/rpc/RPCSub.h.ai.json | 73 + src/xrpld/rpc/RPCSub.h.ai.md | 35 + src/xrpld/rpc/Request.h.ai.json | 43 + src/xrpld/rpc/Request.h.ai.md | 29 + src/xrpld/rpc/Role.h.ai.json | 60 + src/xrpld/rpc/Role.h.ai.md | 58 + src/xrpld/rpc/ServerHandler.h.ai.json | 194 +++ src/xrpld/rpc/ServerHandler.h.ai.md | 55 + src/xrpld/rpc/Status.h.ai.json | 121 ++ src/xrpld/rpc/Status.h.ai.md | 35 + .../rpc/detail/AccountAssets.cpp.ai.json | 500 ++++++ src/xrpld/rpc/detail/AccountAssets.cpp.ai.md | 37 + src/xrpld/rpc/detail/AccountAssets.h.ai.json | 58 + src/xrpld/rpc/detail/AccountAssets.h.ai.md | 26 + src/xrpld/rpc/detail/AssetCache.cpp.ai.json | 307 ++++ src/xrpld/rpc/detail/AssetCache.cpp.ai.md | 41 + src/xrpld/rpc/detail/AssetCache.h.ai.json | 112 ++ src/xrpld/rpc/detail/AssetCache.h.ai.md | 40 + .../rpc/detail/DeliveredAmount.cpp.ai.json | 538 +++++++ .../rpc/detail/DeliveredAmount.cpp.ai.md | 42 + src/xrpld/rpc/detail/Handler.cpp.ai.json | 286 ++++ src/xrpld/rpc/detail/Handler.cpp.ai.md | 44 + src/xrpld/rpc/detail/Handler.h.ai.json | 84 + src/xrpld/rpc/detail/Handler.h.ai.md | 68 + .../rpc/detail/LegacyPathFind.cpp.ai.json | 245 +++ src/xrpld/rpc/detail/LegacyPathFind.cpp.ai.md | 49 + src/xrpld/rpc/detail/LegacyPathFind.h.ai.json | 45 + src/xrpld/rpc/detail/LegacyPathFind.h.ai.md | 40 + src/xrpld/rpc/detail/MPT.h.ai.json | 60 + src/xrpld/rpc/detail/MPT.h.ai.md | 37 + .../rpc/detail/MPTokenIssuanceID.cpp.ai.json | 521 +++++++ .../rpc/detail/MPTokenIssuanceID.cpp.ai.md | 25 + src/xrpld/rpc/detail/PathRequest.cpp.ai.json | 815 ++++++++++ src/xrpld/rpc/detail/PathRequest.cpp.ai.md | 50 + src/xrpld/rpc/detail/PathRequest.h.ai.json | 122 ++ src/xrpld/rpc/detail/PathRequest.h.ai.md | 53 + .../rpc/detail/PathRequestManager.cpp.ai.json | 579 +++++++ .../rpc/detail/PathRequestManager.cpp.ai.md | 50 + .../rpc/detail/PathRequestManager.h.ai.json | 117 ++ .../rpc/detail/PathRequestManager.h.ai.md | 49 + src/xrpld/rpc/detail/Pathfinder.cpp.ai.json | 381 +++++ src/xrpld/rpc/detail/Pathfinder.cpp.ai.md | 70 + src/xrpld/rpc/detail/Pathfinder.h.ai.json | 198 +++ src/xrpld/rpc/detail/Pathfinder.h.ai.md | 75 + .../rpc/detail/PathfinderUtils.h.ai.json | 54 + src/xrpld/rpc/detail/PathfinderUtils.h.ai.md | 33 + src/xrpld/rpc/detail/RPCCall.cpp.ai.json | 588 +++++++ src/xrpld/rpc/detail/RPCCall.cpp.ai.md | 65 + src/xrpld/rpc/detail/RPCHandler.cpp.ai.json | 494 ++++++ src/xrpld/rpc/detail/RPCHandler.cpp.ai.md | 47 + src/xrpld/rpc/detail/RPCHelpers.cpp.ai.json | 519 +++++++ src/xrpld/rpc/detail/RPCHelpers.cpp.ai.md | 49 + src/xrpld/rpc/detail/RPCHelpers.h.ai.json | 100 ++ src/xrpld/rpc/detail/RPCHelpers.h.ai.md | 31 + .../rpc/detail/RPCLedgerHelpers.cpp.ai.json | 505 ++++++ .../rpc/detail/RPCLedgerHelpers.cpp.ai.md | 43 + .../rpc/detail/RPCLedgerHelpers.h.ai.json | 132 ++ src/xrpld/rpc/detail/RPCLedgerHelpers.h.ai.md | 68 + src/xrpld/rpc/detail/RPCSub.cpp.ai.json | 431 ++++++ src/xrpld/rpc/detail/RPCSub.cpp.ai.md | 35 + .../rpc/detail/RippleLineCache.cpp.ai.json | 170 +++ .../rpc/detail/RippleLineCache.cpp.ai.md | 15 + .../rpc/detail/RippleLineCache.h.ai.json | 9 + src/xrpld/rpc/detail/RippleLineCache.h.ai.md | 15 + src/xrpld/rpc/detail/Role.cpp.ai.json | 385 +++++ src/xrpld/rpc/detail/Role.cpp.ai.md | 51 + .../rpc/detail/ServerHandler.cpp.ai.json | 805 ++++++++++ src/xrpld/rpc/detail/ServerHandler.cpp.ai.md | 70 + src/xrpld/rpc/detail/Status.cpp.ai.json | 522 +++++++ src/xrpld/rpc/detail/Status.cpp.ai.md | 35 + .../rpc/detail/TransactionSign.cpp.ai.json | 823 ++++++++++ .../rpc/detail/TransactionSign.cpp.ai.md | 59 + .../rpc/detail/TransactionSign.h.ai.json | 124 ++ src/xrpld/rpc/detail/TransactionSign.h.ai.md | 55 + src/xrpld/rpc/detail/TrustLine.cpp.ai.json | 415 +++++ src/xrpld/rpc/detail/TrustLine.cpp.ai.md | 42 + src/xrpld/rpc/detail/TrustLine.h.ai.json | 176 +++ src/xrpld/rpc/detail/TrustLine.h.ai.md | 49 + src/xrpld/rpc/detail/Tuning.h.ai.json | 41 + src/xrpld/rpc/detail/Tuning.h.ai.md | 43 + src/xrpld/rpc/detail/WSInfoSub.h.ai.json | 50 + src/xrpld/rpc/detail/WSInfoSub.h.ai.md | 52 + .../rpc/handlers/ChannelVerify.cpp.ai.json | 471 ++++++ .../rpc/handlers/ChannelVerify.cpp.ai.md | 63 + src/xrpld/rpc/handlers/Handlers.h.ai.json | 519 +++++++ src/xrpld/rpc/handlers/Handlers.h.ai.md | 45 + src/xrpld/rpc/handlers/VaultInfo.cpp.ai.json | 539 +++++++ src/xrpld/rpc/handlers/VaultInfo.cpp.ai.md | 33 + .../account/AccountChannels.cpp.ai.json | 587 +++++++ .../account/AccountChannels.cpp.ai.md | 39 + .../account/AccountCurrencies.cpp.ai.json | 430 ++++++ .../account/AccountCurrencies.cpp.ai.md | 53 + .../handlers/account/AccountInfo.cpp.ai.json | 257 ++++ .../handlers/account/AccountInfo.cpp.ai.md | 64 + .../handlers/account/AccountLines.cpp.ai.json | 251 +++ .../handlers/account/AccountLines.cpp.ai.md | 49 + .../handlers/account/AccountNFTs.cpp.ai.json | 602 ++++++++ .../handlers/account/AccountNFTs.cpp.ai.md | 52 + .../account/AccountObjects.cpp.ai.json | 392 +++++ .../handlers/account/AccountObjects.cpp.ai.md | 35 + .../account/AccountOffers.cpp.ai.json | 683 +++++++++ .../handlers/account/AccountOffers.cpp.ai.md | 61 + .../handlers/account/AccountTx.cpp.ai.json | 490 ++++++ .../rpc/handlers/account/AccountTx.cpp.ai.md | 71 + .../account/GatewayBalances.cpp.ai.json | 467 ++++++ .../account/GatewayBalances.cpp.ai.md | 50 + .../account/NoRippleCheck.cpp.ai.json | 558 +++++++ .../handlers/account/NoRippleCheck.cpp.ai.md | 57 + .../handlers/account/OwnerInfo.cpp.ai.json | 204 +++ .../rpc/handlers/account/OwnerInfo.cpp.ai.md | 27 + .../rpc/handlers/admin/BlackList.cpp.ai.json | 189 +++ .../rpc/handlers/admin/BlackList.cpp.ai.md | 35 + .../rpc/handlers/admin/UnlList.cpp.ai.json | 146 ++ .../rpc/handlers/admin/UnlList.cpp.ai.md | 25 + .../handlers/admin/data/CanDelete.cpp.ai.json | 523 +++++++ .../handlers/admin/data/CanDelete.cpp.ai.md | 43 + .../admin/data/LedgerCleaner.cpp.ai.json | 71 + .../admin/data/LedgerCleaner.cpp.ai.md | 34 + .../admin/data/LedgerRequest.cpp.ai.json | 232 +++ .../admin/data/LedgerRequest.cpp.ai.md | 44 + .../admin/keygen/ValidationCreate.cpp.ai.json | 330 ++++ .../admin/keygen/ValidationCreate.cpp.ai.md | 42 + .../admin/keygen/WalletPropose.cpp.ai.json | 417 +++++ .../admin/keygen/WalletPropose.cpp.ai.md | 45 + .../admin/keygen/WalletPropose.h.ai.json | 27 + .../admin/keygen/WalletPropose.h.ai.md | 25 + .../handlers/admin/log/LogLevel.cpp.ai.json | 259 ++++ .../rpc/handlers/admin/log/LogLevel.cpp.ai.md | 33 + .../handlers/admin/log/LogRotate.cpp.ai.json | 101 ++ .../handlers/admin/log/LogRotate.cpp.ai.md | 22 + .../handlers/admin/peer/Connect.cpp.ai.json | 250 +++ .../rpc/handlers/admin/peer/Connect.cpp.ai.md | 44 + .../peer/PeerReservationsAdd.cpp.ai.json | 306 ++++ .../admin/peer/PeerReservationsAdd.cpp.ai.md | 31 + .../peer/PeerReservationsDel.cpp.ai.json | 241 +++ .../admin/peer/PeerReservationsDel.cpp.ai.md | 34 + .../peer/PeerReservationsList.cpp.ai.json | 128 ++ .../admin/peer/PeerReservationsList.cpp.ai.md | 34 + .../rpc/handlers/admin/peer/Peers.cpp.ai.json | 396 +++++ .../rpc/handlers/admin/peer/Peers.cpp.ai.md | 28 + .../server_control/LedgerAccept.cpp.ai.json | 197 +++ .../server_control/LedgerAccept.cpp.ai.md | 43 + .../admin/server_control/Stop.cpp.ai.json | 77 + .../admin/server_control/Stop.cpp.ai.md | 40 + .../signing/ChannelAuthorize.cpp.ai.json | 403 +++++ .../admin/signing/ChannelAuthorize.cpp.ai.md | 50 + .../handlers/admin/signing/Sign.cpp.ai.json | 312 ++++ .../rpc/handlers/admin/signing/Sign.cpp.ai.md | 50 + .../admin/signing/SignFor.cpp.ai.json | 329 ++++ .../handlers/admin/signing/SignFor.cpp.ai.md | 50 + .../admin/status/ConsensusInfo.cpp.ai.json | 130 ++ .../admin/status/ConsensusInfo.cpp.ai.md | 51 + .../admin/status/FetchInfo.cpp.ai.json | 176 +++ .../handlers/admin/status/FetchInfo.cpp.ai.md | 23 + .../admin/status/GetCounts.cpp.ai.json | 201 +++ .../handlers/admin/status/GetCounts.cpp.ai.md | 33 + .../handlers/admin/status/GetCounts.h.ai.json | 23 + .../handlers/admin/status/GetCounts.h.ai.md | 7 + .../handlers/admin/status/Print.cpp.ai.json | 281 ++++ .../rpc/handlers/admin/status/Print.cpp.ai.md | 27 + .../admin/status/ValidatorInfo.cpp.ai.json | 229 +++ .../admin/status/ValidatorInfo.cpp.ai.md | 27 + .../status/ValidatorListSites.cpp.ai.json | 83 + .../admin/status/ValidatorListSites.cpp.ai.md | 29 + .../admin/status/Validators.cpp.ai.json | 81 + .../admin/status/Validators.cpp.ai.md | 34 + .../rpc/handlers/ledger/Ledger.cpp.ai.json | 671 ++++++++ .../rpc/handlers/ledger/Ledger.cpp.ai.md | 43 + .../rpc/handlers/ledger/Ledger.h.ai.json | 71 + src/xrpld/rpc/handlers/ledger/Ledger.h.ai.md | 47 + .../handlers/ledger/LedgerClosed.cpp.ai.json | 182 +++ .../handlers/ledger/LedgerClosed.cpp.ai.md | 50 + .../handlers/ledger/LedgerCurrent.cpp.ai.json | 131 ++ .../handlers/ledger/LedgerCurrent.cpp.ai.md | 39 + .../handlers/ledger/LedgerData.cpp.ai.json | 440 ++++++ .../rpc/handlers/ledger/LedgerData.cpp.ai.md | 49 + .../handlers/ledger/LedgerDiff.cpp.ai.json | 379 +++++ .../rpc/handlers/ledger/LedgerDiff.cpp.ai.md | 36 + .../handlers/ledger/LedgerEntry.cpp.ai.json | 1199 +++++++++++++++ .../rpc/handlers/ledger/LedgerEntry.cpp.ai.md | 47 + .../ledger/LedgerEntryHelpers.h.ai.json | 344 +++++ .../ledger/LedgerEntryHelpers.h.ai.md | 43 + .../handlers/ledger/LedgerHeader.cpp.ai.json | 301 ++++ .../handlers/ledger/LedgerHeader.cpp.ai.md | 27 + .../handlers/orderbook/AMMInfo.cpp.ai.json | 498 ++++++ .../rpc/handlers/orderbook/AMMInfo.cpp.ai.md | 54 + .../orderbook/BookChanges.cpp.ai.json | 181 +++ .../handlers/orderbook/BookChanges.cpp.ai.md | 38 + .../handlers/orderbook/BookOffers.cpp.ai.json | 508 +++++++ .../handlers/orderbook/BookOffers.cpp.ai.md | 51 + .../orderbook/DepositAuthorized.cpp.ai.json | 644 ++++++++ .../orderbook/DepositAuthorized.cpp.ai.md | 61 + .../orderbook/GetAggregatePrice.cpp.ai.json | 578 +++++++ .../orderbook/GetAggregatePrice.cpp.ai.md | 45 + .../orderbook/NFTBuyOffers.cpp.ai.json | 192 +++ .../handlers/orderbook/NFTBuyOffers.cpp.ai.md | 25 + .../orderbook/NFTOffersHelpers.h.ai.json | 33 + .../orderbook/NFTOffersHelpers.h.ai.md | 29 + .../orderbook/NFTSellOffers.cpp.ai.json | 192 +++ .../orderbook/NFTSellOffers.cpp.ai.md | 17 + .../handlers/orderbook/PathFind.cpp.ai.json | 369 +++++ .../rpc/handlers/orderbook/PathFind.cpp.ai.md | 44 + .../orderbook/RipplePathFind.cpp.ai.json | 344 +++++ .../orderbook/RipplePathFind.cpp.ai.md | 46 + .../handlers/server_info/Feature.cpp.ai.json | 326 ++++ .../handlers/server_info/Feature.cpp.ai.md | 29 + .../rpc/handlers/server_info/Fee.cpp.ai.json | 130 ++ .../rpc/handlers/server_info/Fee.cpp.ai.md | 35 + .../handlers/server_info/Manifest.cpp.ai.json | 205 +++ .../handlers/server_info/Manifest.cpp.ai.md | 36 + .../server_info/ServerDefinitions.cpp.ai.json | 281 ++++ .../server_info/ServerDefinitions.cpp.ai.md | 52 + .../server_info/ServerInfo.cpp.ai.json | 222 +++ .../handlers/server_info/ServerInfo.cpp.ai.md | 27 + .../server_info/ServerState.cpp.ai.json | 249 +++ .../server_info/ServerState.cpp.ai.md | 49 + .../handlers/server_info/Version.h.ai.json | 55 + .../rpc/handlers/server_info/Version.h.ai.md | 35 + .../handlers/subscribe/Subscribe.cpp.ai.json | 641 ++++++++ .../handlers/subscribe/Subscribe.cpp.ai.md | 39 + .../subscribe/Unsubscribe.cpp.ai.json | 440 ++++++ .../handlers/subscribe/Unsubscribe.cpp.ai.md | 50 + .../handlers/transaction/Simulate.cpp.ai.json | 538 +++++++ .../handlers/transaction/Simulate.cpp.ai.md | 45 + .../handlers/transaction/Submit.cpp.ai.json | 558 +++++++ .../rpc/handlers/transaction/Submit.cpp.ai.md | 43 + .../transaction/SubmitMultiSigned.cpp.ai.json | 343 +++++ .../transaction/SubmitMultiSigned.cpp.ai.md | 48 + .../transaction/TransactionEntry.cpp.ai.json | 437 ++++++ .../transaction/TransactionEntry.cpp.ai.md | 41 + .../rpc/handlers/transaction/Tx.cpp.ai.json | 414 +++++ .../rpc/handlers/transaction/Tx.cpp.ai.md | 39 + .../transaction/TxHistory.cpp.ai.json | 316 ++++ .../handlers/transaction/TxHistory.cpp.ai.md | 29 + .../transaction/TxReduceRelay.cpp.ai.json | 83 + .../transaction/TxReduceRelay.cpp.ai.md | 11 + .../rpc/handlers/utility/Ping.cpp.ai.json | 222 +++ src/xrpld/rpc/handlers/utility/Ping.cpp.ai.md | 28 + .../rpc/handlers/utility/Random.cpp.ai.json | 136 ++ .../rpc/handlers/utility/Random.cpp.ai.md | 38 + src/xrpld/rpc/json_body.h.ai.json | 86 ++ src/xrpld/rpc/json_body.h.ai.md | 39 + src/xrpld/shamap/NodeFamily.cpp.ai.json | 239 +++ src/xrpld/shamap/NodeFamily.cpp.ai.md | 37 + src/xrpld/shamap/NodeFamily.h.ai.json | 115 ++ src/xrpld/shamap/NodeFamily.h.ai.md | 46 + 2348 files changed, 331790 insertions(+) create mode 100644 include/xrpl/basics/Archive.h.ai.json create mode 100644 include/xrpl/basics/Archive.h.ai.md create mode 100644 include/xrpl/basics/BasicConfig.h.ai.json create mode 100644 include/xrpl/basics/BasicConfig.h.ai.md create mode 100644 include/xrpl/basics/Blob.h.ai.json create mode 100644 include/xrpl/basics/Blob.h.ai.md create mode 100644 include/xrpl/basics/Buffer.h.ai.json create mode 100644 include/xrpl/basics/Buffer.h.ai.md create mode 100644 include/xrpl/basics/ByteUtilities.h.ai.json create mode 100644 include/xrpl/basics/ByteUtilities.h.ai.md create mode 100644 include/xrpl/basics/CompressionAlgorithms.h.ai.json create mode 100644 include/xrpl/basics/CompressionAlgorithms.h.ai.md create mode 100644 include/xrpl/basics/CountedObject.h.ai.json create mode 100644 include/xrpl/basics/CountedObject.h.ai.md create mode 100644 include/xrpl/basics/DecayingSample.h.ai.json create mode 100644 include/xrpl/basics/DecayingSample.h.ai.md create mode 100644 include/xrpl/basics/Expected.h.ai.json create mode 100644 include/xrpl/basics/Expected.h.ai.md create mode 100644 include/xrpl/basics/FileUtilities.h.ai.json create mode 100644 include/xrpl/basics/FileUtilities.h.ai.md create mode 100644 include/xrpl/basics/IntrusivePointer.h.ai.json create mode 100644 include/xrpl/basics/IntrusivePointer.h.ai.md create mode 100644 include/xrpl/basics/IntrusivePointer.ipp.ai.json create mode 100644 include/xrpl/basics/IntrusivePointer.ipp.ai.md create mode 100644 include/xrpl/basics/IntrusiveRefCounts.h.ai.json create mode 100644 include/xrpl/basics/IntrusiveRefCounts.h.ai.md create mode 100644 include/xrpl/basics/KeyCache.h.ai.json create mode 100644 include/xrpl/basics/KeyCache.h.ai.md create mode 100644 include/xrpl/basics/LocalValue.h.ai.json create mode 100644 include/xrpl/basics/LocalValue.h.ai.md create mode 100644 include/xrpl/basics/Log.h.ai.json create mode 100644 include/xrpl/basics/Log.h.ai.md create mode 100644 include/xrpl/basics/MallocTrim.h.ai.json create mode 100644 include/xrpl/basics/MallocTrim.h.ai.md create mode 100644 include/xrpl/basics/MathUtilities.h.ai.json create mode 100644 include/xrpl/basics/MathUtilities.h.ai.md create mode 100644 include/xrpl/basics/Mutex.hpp.ai.json create mode 100644 include/xrpl/basics/Mutex.hpp.ai.md create mode 100644 include/xrpl/basics/Number.h.ai.json create mode 100644 include/xrpl/basics/Number.h.ai.md create mode 100644 include/xrpl/basics/RangeSet.h.ai.json create mode 100644 include/xrpl/basics/RangeSet.h.ai.md create mode 100644 include/xrpl/basics/Resolver.h.ai.json create mode 100644 include/xrpl/basics/Resolver.h.ai.md create mode 100644 include/xrpl/basics/ResolverAsio.h.ai.json create mode 100644 include/xrpl/basics/ResolverAsio.h.ai.md create mode 100644 include/xrpl/basics/SHAMapHash.h.ai.json create mode 100644 include/xrpl/basics/SHAMapHash.h.ai.md create mode 100644 include/xrpl/basics/SharedWeakCachePointer.h.ai.json create mode 100644 include/xrpl/basics/SharedWeakCachePointer.h.ai.md create mode 100644 include/xrpl/basics/SharedWeakCachePointer.ipp.ai.json create mode 100644 include/xrpl/basics/SharedWeakCachePointer.ipp.ai.md create mode 100644 include/xrpl/basics/SlabAllocator.h.ai.json create mode 100644 include/xrpl/basics/SlabAllocator.h.ai.md create mode 100644 include/xrpl/basics/Slice.h.ai.json create mode 100644 include/xrpl/basics/Slice.h.ai.md create mode 100644 include/xrpl/basics/StringUtilities.h.ai.json create mode 100644 include/xrpl/basics/StringUtilities.h.ai.md create mode 100644 include/xrpl/basics/TaggedCache.h.ai.json create mode 100644 include/xrpl/basics/TaggedCache.h.ai.md create mode 100644 include/xrpl/basics/TaggedCache.ipp.ai.json create mode 100644 include/xrpl/basics/TaggedCache.ipp.ai.md create mode 100644 include/xrpl/basics/ToString.h.ai.json create mode 100644 include/xrpl/basics/ToString.h.ai.md create mode 100644 include/xrpl/basics/UnorderedContainers.h.ai.json create mode 100644 include/xrpl/basics/UnorderedContainers.h.ai.md create mode 100644 include/xrpl/basics/UptimeClock.h.ai.json create mode 100644 include/xrpl/basics/UptimeClock.h.ai.md create mode 100644 include/xrpl/basics/algorithm.h.ai.json create mode 100644 include/xrpl/basics/algorithm.h.ai.md create mode 100644 include/xrpl/basics/base64.h.ai.json create mode 100644 include/xrpl/basics/base64.h.ai.md create mode 100644 include/xrpl/basics/base_uint.h.ai.json create mode 100644 include/xrpl/basics/base_uint.h.ai.md create mode 100644 include/xrpl/basics/chrono.h.ai.json create mode 100644 include/xrpl/basics/chrono.h.ai.md create mode 100644 include/xrpl/basics/comparators.h.ai.json create mode 100644 include/xrpl/basics/comparators.h.ai.md create mode 100644 include/xrpl/basics/contract.h.ai.json create mode 100644 include/xrpl/basics/contract.h.ai.md create mode 100644 include/xrpl/basics/hardened_hash.h.ai.json create mode 100644 include/xrpl/basics/hardened_hash.h.ai.md create mode 100644 include/xrpl/basics/join.h.ai.json create mode 100644 include/xrpl/basics/join.h.ai.md create mode 100644 include/xrpl/basics/make_SSLContext.h.ai.json create mode 100644 include/xrpl/basics/make_SSLContext.h.ai.md create mode 100644 include/xrpl/basics/mulDiv.h.ai.json create mode 100644 include/xrpl/basics/mulDiv.h.ai.md create mode 100644 include/xrpl/basics/partitioned_unordered_map.h.ai.json create mode 100644 include/xrpl/basics/partitioned_unordered_map.h.ai.md create mode 100644 include/xrpl/basics/random.h.ai.json create mode 100644 include/xrpl/basics/random.h.ai.md create mode 100644 include/xrpl/basics/rocksdb.h.ai.json create mode 100644 include/xrpl/basics/rocksdb.h.ai.md create mode 100644 include/xrpl/basics/safe_cast.h.ai.json create mode 100644 include/xrpl/basics/safe_cast.h.ai.md create mode 100644 include/xrpl/basics/sanitizers.h.ai.json create mode 100644 include/xrpl/basics/sanitizers.h.ai.md create mode 100644 include/xrpl/basics/scope.h.ai.json create mode 100644 include/xrpl/basics/scope.h.ai.md create mode 100644 include/xrpl/basics/spinlock.h.ai.json create mode 100644 include/xrpl/basics/spinlock.h.ai.md create mode 100644 include/xrpl/basics/strHex.h.ai.json create mode 100644 include/xrpl/basics/strHex.h.ai.md create mode 100644 include/xrpl/basics/tagged_integer.h.ai.json create mode 100644 include/xrpl/basics/tagged_integer.h.ai.md create mode 100644 include/xrpl/beast/asio/io_latency_probe.h.ai.json create mode 100644 include/xrpl/beast/asio/io_latency_probe.h.ai.md create mode 100644 include/xrpl/beast/clock/abstract_clock.h.ai.json create mode 100644 include/xrpl/beast/clock/abstract_clock.h.ai.md create mode 100644 include/xrpl/beast/clock/basic_seconds_clock.h.ai.json create mode 100644 include/xrpl/beast/clock/basic_seconds_clock.h.ai.md create mode 100644 include/xrpl/beast/clock/manual_clock.h.ai.json create mode 100644 include/xrpl/beast/clock/manual_clock.h.ai.md create mode 100644 include/xrpl/beast/container/aged_container.h.ai.json create mode 100644 include/xrpl/beast/container/aged_container.h.ai.md create mode 100644 include/xrpl/beast/container/aged_container_utility.h.ai.json create mode 100644 include/xrpl/beast/container/aged_container_utility.h.ai.md create mode 100644 include/xrpl/beast/container/aged_map.h.ai.json create mode 100644 include/xrpl/beast/container/aged_map.h.ai.md create mode 100644 include/xrpl/beast/container/aged_multimap.h.ai.json create mode 100644 include/xrpl/beast/container/aged_multimap.h.ai.md create mode 100644 include/xrpl/beast/container/aged_multiset.h.ai.json create mode 100644 include/xrpl/beast/container/aged_multiset.h.ai.md create mode 100644 include/xrpl/beast/container/aged_set.h.ai.json create mode 100644 include/xrpl/beast/container/aged_set.h.ai.md create mode 100644 include/xrpl/beast/container/aged_unordered_map.h.ai.json create mode 100644 include/xrpl/beast/container/aged_unordered_map.h.ai.md create mode 100644 include/xrpl/beast/container/aged_unordered_multimap.h.ai.json create mode 100644 include/xrpl/beast/container/aged_unordered_multimap.h.ai.md create mode 100644 include/xrpl/beast/container/aged_unordered_multiset.h.ai.json create mode 100644 include/xrpl/beast/container/aged_unordered_multiset.h.ai.md create mode 100644 include/xrpl/beast/container/aged_unordered_set.h.ai.json create mode 100644 include/xrpl/beast/container/aged_unordered_set.h.ai.md create mode 100644 include/xrpl/beast/container/detail/aged_associative_container.h.ai.json create mode 100644 include/xrpl/beast/container/detail/aged_associative_container.h.ai.md create mode 100644 include/xrpl/beast/container/detail/aged_container_iterator.h.ai.json create mode 100644 include/xrpl/beast/container/detail/aged_container_iterator.h.ai.md create mode 100644 include/xrpl/beast/container/detail/aged_ordered_container.h.ai.json create mode 100644 include/xrpl/beast/container/detail/aged_ordered_container.h.ai.md create mode 100644 include/xrpl/beast/container/detail/aged_unordered_container.h.ai.json create mode 100644 include/xrpl/beast/container/detail/aged_unordered_container.h.ai.md create mode 100644 include/xrpl/beast/container/detail/empty_base_optimization.h.ai.json create mode 100644 include/xrpl/beast/container/detail/empty_base_optimization.h.ai.md create mode 100644 include/xrpl/beast/core/CurrentThreadName.h.ai.json create mode 100644 include/xrpl/beast/core/CurrentThreadName.h.ai.md create mode 100644 include/xrpl/beast/core/LexicalCast.h.ai.json create mode 100644 include/xrpl/beast/core/LexicalCast.h.ai.md create mode 100644 include/xrpl/beast/core/List.h.ai.json create mode 100644 include/xrpl/beast/core/List.h.ai.md create mode 100644 include/xrpl/beast/core/LockFreeStack.h.ai.json create mode 100644 include/xrpl/beast/core/LockFreeStack.h.ai.md create mode 100644 include/xrpl/beast/core/SemanticVersion.h.ai.json create mode 100644 include/xrpl/beast/core/SemanticVersion.h.ai.md create mode 100644 include/xrpl/beast/hash/hash_append.h.ai.json create mode 100644 include/xrpl/beast/hash/hash_append.h.ai.md create mode 100644 include/xrpl/beast/hash/uhash.h.ai.json create mode 100644 include/xrpl/beast/hash/uhash.h.ai.md create mode 100644 include/xrpl/beast/hash/xxhasher.h.ai.json create mode 100644 include/xrpl/beast/hash/xxhasher.h.ai.md create mode 100644 include/xrpl/beast/insight/Collector.h.ai.json create mode 100644 include/xrpl/beast/insight/Collector.h.ai.md create mode 100644 include/xrpl/beast/insight/Counter.h.ai.json create mode 100644 include/xrpl/beast/insight/Counter.h.ai.md create mode 100644 include/xrpl/beast/insight/CounterImpl.h.ai.json create mode 100644 include/xrpl/beast/insight/CounterImpl.h.ai.md create mode 100644 include/xrpl/beast/insight/Event.h.ai.json create mode 100644 include/xrpl/beast/insight/Event.h.ai.md create mode 100644 include/xrpl/beast/insight/EventImpl.h.ai.json create mode 100644 include/xrpl/beast/insight/EventImpl.h.ai.md create mode 100644 include/xrpl/beast/insight/Gauge.h.ai.json create mode 100644 include/xrpl/beast/insight/Gauge.h.ai.md create mode 100644 include/xrpl/beast/insight/GaugeImpl.h.ai.json create mode 100644 include/xrpl/beast/insight/GaugeImpl.h.ai.md create mode 100644 include/xrpl/beast/insight/Group.h.ai.json create mode 100644 include/xrpl/beast/insight/Group.h.ai.md create mode 100644 include/xrpl/beast/insight/Groups.h.ai.json create mode 100644 include/xrpl/beast/insight/Groups.h.ai.md create mode 100644 include/xrpl/beast/insight/Hook.h.ai.json create mode 100644 include/xrpl/beast/insight/Hook.h.ai.md create mode 100644 include/xrpl/beast/insight/HookImpl.h.ai.json create mode 100644 include/xrpl/beast/insight/HookImpl.h.ai.md create mode 100644 include/xrpl/beast/insight/Insight.h.ai.json create mode 100644 include/xrpl/beast/insight/Insight.h.ai.md create mode 100644 include/xrpl/beast/insight/Meter.h.ai.json create mode 100644 include/xrpl/beast/insight/Meter.h.ai.md create mode 100644 include/xrpl/beast/insight/MeterImpl.h.ai.json create mode 100644 include/xrpl/beast/insight/MeterImpl.h.ai.md create mode 100644 include/xrpl/beast/insight/NullCollector.h.ai.json create mode 100644 include/xrpl/beast/insight/NullCollector.h.ai.md create mode 100644 include/xrpl/beast/insight/StatsDCollector.h.ai.json create mode 100644 include/xrpl/beast/insight/StatsDCollector.h.ai.md create mode 100644 include/xrpl/beast/net/IPAddress.h.ai.json create mode 100644 include/xrpl/beast/net/IPAddress.h.ai.md create mode 100644 include/xrpl/beast/net/IPAddressConversion.h.ai.json create mode 100644 include/xrpl/beast/net/IPAddressConversion.h.ai.md create mode 100644 include/xrpl/beast/net/IPAddressV4.h.ai.json create mode 100644 include/xrpl/beast/net/IPAddressV4.h.ai.md create mode 100644 include/xrpl/beast/net/IPAddressV6.h.ai.json create mode 100644 include/xrpl/beast/net/IPAddressV6.h.ai.md create mode 100644 include/xrpl/beast/net/IPEndpoint.h.ai.json create mode 100644 include/xrpl/beast/net/IPEndpoint.h.ai.md create mode 100644 include/xrpl/beast/rfc2616.h.ai.json create mode 100644 include/xrpl/beast/rfc2616.h.ai.md create mode 100644 include/xrpl/beast/test/yield_to.h.ai.json create mode 100644 include/xrpl/beast/test/yield_to.h.ai.md create mode 100644 include/xrpl/beast/type_name.h.ai.json create mode 100644 include/xrpl/beast/type_name.h.ai.md create mode 100644 include/xrpl/beast/unit_test.h.ai.json create mode 100644 include/xrpl/beast/unit_test.h.ai.md create mode 100644 include/xrpl/beast/unit_test/amount.h.ai.json create mode 100644 include/xrpl/beast/unit_test/amount.h.ai.md create mode 100644 include/xrpl/beast/unit_test/detail/const_container.h.ai.json create mode 100644 include/xrpl/beast/unit_test/detail/const_container.h.ai.md create mode 100644 include/xrpl/beast/unit_test/global_suites.h.ai.json create mode 100644 include/xrpl/beast/unit_test/global_suites.h.ai.md create mode 100644 include/xrpl/beast/unit_test/match.h.ai.json create mode 100644 include/xrpl/beast/unit_test/match.h.ai.md create mode 100644 include/xrpl/beast/unit_test/recorder.h.ai.json create mode 100644 include/xrpl/beast/unit_test/recorder.h.ai.md create mode 100644 include/xrpl/beast/unit_test/reporter.h.ai.json create mode 100644 include/xrpl/beast/unit_test/reporter.h.ai.md create mode 100644 include/xrpl/beast/unit_test/results.h.ai.json create mode 100644 include/xrpl/beast/unit_test/results.h.ai.md create mode 100644 include/xrpl/beast/unit_test/runner.h.ai.json create mode 100644 include/xrpl/beast/unit_test/runner.h.ai.md create mode 100644 include/xrpl/beast/unit_test/suite.h.ai.json create mode 100644 include/xrpl/beast/unit_test/suite.h.ai.md create mode 100644 include/xrpl/beast/unit_test/suite_info.h.ai.json create mode 100644 include/xrpl/beast/unit_test/suite_info.h.ai.md create mode 100644 include/xrpl/beast/unit_test/suite_list.h.ai.json create mode 100644 include/xrpl/beast/unit_test/suite_list.h.ai.md create mode 100644 include/xrpl/beast/unit_test/thread.h.ai.json create mode 100644 include/xrpl/beast/unit_test/thread.h.ai.md create mode 100644 include/xrpl/beast/utility/Journal.h.ai.json create mode 100644 include/xrpl/beast/utility/Journal.h.ai.md create mode 100644 include/xrpl/beast/utility/PropertyStream.h.ai.json create mode 100644 include/xrpl/beast/utility/PropertyStream.h.ai.md create mode 100644 include/xrpl/beast/utility/WrappedSink.h.ai.json create mode 100644 include/xrpl/beast/utility/WrappedSink.h.ai.md create mode 100644 include/xrpl/beast/utility/Zero.h.ai.json create mode 100644 include/xrpl/beast/utility/Zero.h.ai.md create mode 100644 include/xrpl/beast/utility/instrumentation.h.ai.json create mode 100644 include/xrpl/beast/utility/instrumentation.h.ai.md create mode 100644 include/xrpl/beast/utility/maybe_const.h.ai.json create mode 100644 include/xrpl/beast/utility/maybe_const.h.ai.md create mode 100644 include/xrpl/beast/utility/rngfill.h.ai.json create mode 100644 include/xrpl/beast/utility/rngfill.h.ai.md create mode 100644 include/xrpl/beast/utility/temp_dir.h.ai.json create mode 100644 include/xrpl/beast/utility/temp_dir.h.ai.md create mode 100644 include/xrpl/beast/xor_shift_engine.h.ai.json create mode 100644 include/xrpl/beast/xor_shift_engine.h.ai.md create mode 100644 include/xrpl/conditions/Condition.h.ai.json create mode 100644 include/xrpl/conditions/Condition.h.ai.md create mode 100644 include/xrpl/conditions/Fulfillment.h.ai.json create mode 100644 include/xrpl/conditions/Fulfillment.h.ai.md create mode 100644 include/xrpl/conditions/detail/PreimageSha256.h.ai.json create mode 100644 include/xrpl/conditions/detail/PreimageSha256.h.ai.md create mode 100644 include/xrpl/conditions/detail/error.h.ai.json create mode 100644 include/xrpl/conditions/detail/error.h.ai.md create mode 100644 include/xrpl/conditions/detail/utils.h.ai.json create mode 100644 include/xrpl/conditions/detail/utils.h.ai.md create mode 100644 include/xrpl/core/ClosureCounter.h.ai.json create mode 100644 include/xrpl/core/ClosureCounter.h.ai.md create mode 100644 include/xrpl/core/Coro.ipp.ai.json create mode 100644 include/xrpl/core/Coro.ipp.ai.md create mode 100644 include/xrpl/core/HashRouter.h.ai.json create mode 100644 include/xrpl/core/HashRouter.h.ai.md create mode 100644 include/xrpl/core/Job.h.ai.json create mode 100644 include/xrpl/core/Job.h.ai.md create mode 100644 include/xrpl/core/JobQueue.h.ai.json create mode 100644 include/xrpl/core/JobQueue.h.ai.md create mode 100644 include/xrpl/core/JobTypeData.h.ai.json create mode 100644 include/xrpl/core/JobTypeData.h.ai.md create mode 100644 include/xrpl/core/JobTypeInfo.h.ai.json create mode 100644 include/xrpl/core/JobTypeInfo.h.ai.md create mode 100644 include/xrpl/core/JobTypes.h.ai.json create mode 100644 include/xrpl/core/JobTypes.h.ai.md create mode 100644 include/xrpl/core/LoadEvent.h.ai.json create mode 100644 include/xrpl/core/LoadEvent.h.ai.md create mode 100644 include/xrpl/core/LoadMonitor.h.ai.json create mode 100644 include/xrpl/core/LoadMonitor.h.ai.md create mode 100644 include/xrpl/core/NetworkIDService.h.ai.json create mode 100644 include/xrpl/core/NetworkIDService.h.ai.md create mode 100644 include/xrpl/core/PeerReservationTable.h.ai.json create mode 100644 include/xrpl/core/PeerReservationTable.h.ai.md create mode 100644 include/xrpl/core/PerfLog.h.ai.json create mode 100644 include/xrpl/core/PerfLog.h.ai.md create mode 100644 include/xrpl/core/ServiceRegistry.h.ai.json create mode 100644 include/xrpl/core/ServiceRegistry.h.ai.md create mode 100644 include/xrpl/core/StartUpType.h.ai.json create mode 100644 include/xrpl/core/StartUpType.h.ai.md create mode 100644 include/xrpl/core/detail/Workers.h.ai.json create mode 100644 include/xrpl/core/detail/Workers.h.ai.md create mode 100644 include/xrpl/core/detail/semaphore.h.ai.json create mode 100644 include/xrpl/core/detail/semaphore.h.ai.md create mode 100644 include/xrpl/crypto/RFC1751.h.ai.json create mode 100644 include/xrpl/crypto/RFC1751.h.ai.md create mode 100644 include/xrpl/crypto/csprng.h.ai.json create mode 100644 include/xrpl/crypto/csprng.h.ai.md create mode 100644 include/xrpl/crypto/secure_erase.h.ai.json create mode 100644 include/xrpl/crypto/secure_erase.h.ai.md create mode 100644 include/xrpl/git/Git.h.ai.json create mode 100644 include/xrpl/git/Git.h.ai.md create mode 100644 include/xrpl/json/JsonPropertyStream.h.ai.json create mode 100644 include/xrpl/json/JsonPropertyStream.h.ai.md create mode 100644 include/xrpl/json/Output.h.ai.json create mode 100644 include/xrpl/json/Output.h.ai.md create mode 100644 include/xrpl/json/Writer.h.ai.json create mode 100644 include/xrpl/json/Writer.h.ai.md create mode 100644 include/xrpl/json/detail/json_assert.h.ai.json create mode 100644 include/xrpl/json/detail/json_assert.h.ai.md create mode 100644 include/xrpl/json/json_errors.h.ai.json create mode 100644 include/xrpl/json/json_errors.h.ai.md create mode 100644 include/xrpl/json/json_forwards.h.ai.json create mode 100644 include/xrpl/json/json_forwards.h.ai.md create mode 100644 include/xrpl/json/json_reader.h.ai.json create mode 100644 include/xrpl/json/json_reader.h.ai.md create mode 100644 include/xrpl/json/json_value.h.ai.json create mode 100644 include/xrpl/json/json_value.h.ai.md create mode 100644 include/xrpl/json/json_writer.h.ai.json create mode 100644 include/xrpl/json/json_writer.h.ai.md create mode 100644 include/xrpl/json/to_string.h.ai.json create mode 100644 include/xrpl/json/to_string.h.ai.md create mode 100644 include/xrpl/ledger/AcceptedLedgerTx.h.ai.json create mode 100644 include/xrpl/ledger/AcceptedLedgerTx.h.ai.md create mode 100644 include/xrpl/ledger/AmendmentTable.h.ai.json create mode 100644 include/xrpl/ledger/AmendmentTable.h.ai.md create mode 100644 include/xrpl/ledger/ApplyView.h.ai.json create mode 100644 include/xrpl/ledger/ApplyView.h.ai.md create mode 100644 include/xrpl/ledger/ApplyViewImpl.h.ai.json create mode 100644 include/xrpl/ledger/ApplyViewImpl.h.ai.md create mode 100644 include/xrpl/ledger/BookDirs.h.ai.json create mode 100644 include/xrpl/ledger/BookDirs.h.ai.md create mode 100644 include/xrpl/ledger/BookListeners.h.ai.json create mode 100644 include/xrpl/ledger/BookListeners.h.ai.md create mode 100644 include/xrpl/ledger/CachedSLEs.h.ai.json create mode 100644 include/xrpl/ledger/CachedSLEs.h.ai.md create mode 100644 include/xrpl/ledger/CachedView.h.ai.json create mode 100644 include/xrpl/ledger/CachedView.h.ai.md create mode 100644 include/xrpl/ledger/CanonicalTXSet.h.ai.json create mode 100644 include/xrpl/ledger/CanonicalTXSet.h.ai.md create mode 100644 include/xrpl/ledger/Dir.h.ai.json create mode 100644 include/xrpl/ledger/Dir.h.ai.md create mode 100644 include/xrpl/ledger/Ledger.h.ai.json create mode 100644 include/xrpl/ledger/Ledger.h.ai.md create mode 100644 include/xrpl/ledger/LedgerTiming.h.ai.json create mode 100644 include/xrpl/ledger/LedgerTiming.h.ai.md create mode 100644 include/xrpl/ledger/OpenView.h.ai.json create mode 100644 include/xrpl/ledger/OpenView.h.ai.md create mode 100644 include/xrpl/ledger/OrderBookDB.h.ai.json create mode 100644 include/xrpl/ledger/OrderBookDB.h.ai.md create mode 100644 include/xrpl/ledger/PaymentSandbox.h.ai.json create mode 100644 include/xrpl/ledger/PaymentSandbox.h.ai.md create mode 100644 include/xrpl/ledger/PendingSaves.h.ai.json create mode 100644 include/xrpl/ledger/PendingSaves.h.ai.md create mode 100644 include/xrpl/ledger/RawView.h.ai.json create mode 100644 include/xrpl/ledger/RawView.h.ai.md create mode 100644 include/xrpl/ledger/ReadView.h.ai.json create mode 100644 include/xrpl/ledger/ReadView.h.ai.md create mode 100644 include/xrpl/ledger/Sandbox.h.ai.json create mode 100644 include/xrpl/ledger/Sandbox.h.ai.md create mode 100644 include/xrpl/ledger/View.h.ai.json create mode 100644 include/xrpl/ledger/View.h.ai.md create mode 100644 include/xrpl/ledger/detail/ApplyStateTable.h.ai.json create mode 100644 include/xrpl/ledger/detail/ApplyStateTable.h.ai.md create mode 100644 include/xrpl/ledger/detail/ApplyViewBase.h.ai.json create mode 100644 include/xrpl/ledger/detail/ApplyViewBase.h.ai.md create mode 100644 include/xrpl/ledger/detail/RawStateTable.h.ai.json create mode 100644 include/xrpl/ledger/detail/RawStateTable.h.ai.md create mode 100644 include/xrpl/ledger/detail/ReadViewFwdRange.h.ai.json create mode 100644 include/xrpl/ledger/detail/ReadViewFwdRange.h.ai.md create mode 100644 include/xrpl/ledger/detail/ReadViewFwdRange.ipp.ai.json create mode 100644 include/xrpl/ledger/detail/ReadViewFwdRange.ipp.ai.md create mode 100644 include/xrpl/ledger/helpers/AMMHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/AMMHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/AccountRootHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/AccountRootHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/CredentialHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/CredentialHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/DelegateHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/DelegateHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/DirectoryHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/DirectoryHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/EscrowHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/EscrowHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/MPTokenHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/MPTokenHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/NFTokenHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/NFTokenHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/OfferHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/OfferHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/PaymentChannelHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/PaymentChannelHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/PermissionedDEXHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/PermissionedDEXHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/RippleStateHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/RippleStateHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/TokenHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/TokenHelpers.h.ai.md create mode 100644 include/xrpl/ledger/helpers/VaultHelpers.h.ai.json create mode 100644 include/xrpl/ledger/helpers/VaultHelpers.h.ai.md create mode 100644 include/xrpl/net/AutoSocket.h.ai.json create mode 100644 include/xrpl/net/AutoSocket.h.ai.md create mode 100644 include/xrpl/net/HTTPClient.h.ai.json create mode 100644 include/xrpl/net/HTTPClient.h.ai.md create mode 100644 include/xrpl/net/HTTPClientSSLContext.h.ai.json create mode 100644 include/xrpl/net/HTTPClientSSLContext.h.ai.md create mode 100644 include/xrpl/net/RegisterSSLCerts.h.ai.json create mode 100644 include/xrpl/net/RegisterSSLCerts.h.ai.md create mode 100644 include/xrpl/nodestore/Backend.h.ai.json create mode 100644 include/xrpl/nodestore/Backend.h.ai.md create mode 100644 include/xrpl/nodestore/Database.h.ai.json create mode 100644 include/xrpl/nodestore/Database.h.ai.md create mode 100644 include/xrpl/nodestore/DatabaseRotating.h.ai.json create mode 100644 include/xrpl/nodestore/DatabaseRotating.h.ai.md create mode 100644 include/xrpl/nodestore/DummyScheduler.h.ai.json create mode 100644 include/xrpl/nodestore/DummyScheduler.h.ai.md create mode 100644 include/xrpl/nodestore/Factory.h.ai.json create mode 100644 include/xrpl/nodestore/Factory.h.ai.md create mode 100644 include/xrpl/nodestore/Manager.h.ai.json create mode 100644 include/xrpl/nodestore/Manager.h.ai.md create mode 100644 include/xrpl/nodestore/NodeObject.h.ai.json create mode 100644 include/xrpl/nodestore/NodeObject.h.ai.md create mode 100644 include/xrpl/nodestore/Scheduler.h.ai.json create mode 100644 include/xrpl/nodestore/Scheduler.h.ai.md create mode 100644 include/xrpl/nodestore/Task.h.ai.json create mode 100644 include/xrpl/nodestore/Task.h.ai.md create mode 100644 include/xrpl/nodestore/Types.h.ai.json create mode 100644 include/xrpl/nodestore/Types.h.ai.md create mode 100644 include/xrpl/nodestore/detail/BatchWriter.h.ai.json create mode 100644 include/xrpl/nodestore/detail/BatchWriter.h.ai.md create mode 100644 include/xrpl/nodestore/detail/DatabaseNodeImp.h.ai.json create mode 100644 include/xrpl/nodestore/detail/DatabaseNodeImp.h.ai.md create mode 100644 include/xrpl/nodestore/detail/DatabaseRotatingImp.h.ai.json create mode 100644 include/xrpl/nodestore/detail/DatabaseRotatingImp.h.ai.md create mode 100644 include/xrpl/nodestore/detail/DecodedBlob.h.ai.json create mode 100644 include/xrpl/nodestore/detail/DecodedBlob.h.ai.md create mode 100644 include/xrpl/nodestore/detail/EncodedBlob.h.ai.json create mode 100644 include/xrpl/nodestore/detail/EncodedBlob.h.ai.md create mode 100644 include/xrpl/nodestore/detail/ManagerImp.h.ai.json create mode 100644 include/xrpl/nodestore/detail/ManagerImp.h.ai.md create mode 100644 include/xrpl/nodestore/detail/codec.h.ai.json create mode 100644 include/xrpl/nodestore/detail/codec.h.ai.md create mode 100644 include/xrpl/nodestore/detail/varint.h.ai.json create mode 100644 include/xrpl/nodestore/detail/varint.h.ai.md create mode 100644 include/xrpl/protocol/AMMCore.h.ai.json create mode 100644 include/xrpl/protocol/AMMCore.h.ai.md create mode 100644 include/xrpl/protocol/AccountID.h.ai.json create mode 100644 include/xrpl/protocol/AccountID.h.ai.md create mode 100644 include/xrpl/protocol/AmountConversions.h.ai.json create mode 100644 include/xrpl/protocol/AmountConversions.h.ai.md create mode 100644 include/xrpl/protocol/ApiVersion.h.ai.json create mode 100644 include/xrpl/protocol/ApiVersion.h.ai.md create mode 100644 include/xrpl/protocol/Asset.h.ai.json create mode 100644 include/xrpl/protocol/Asset.h.ai.md create mode 100644 include/xrpl/protocol/Batch.h.ai.json create mode 100644 include/xrpl/protocol/Batch.h.ai.md create mode 100644 include/xrpl/protocol/Book.h.ai.json create mode 100644 include/xrpl/protocol/Book.h.ai.md create mode 100644 include/xrpl/protocol/BuildInfo.h.ai.json create mode 100644 include/xrpl/protocol/BuildInfo.h.ai.md create mode 100644 include/xrpl/protocol/Concepts.h.ai.json create mode 100644 include/xrpl/protocol/Concepts.h.ai.md create mode 100644 include/xrpl/protocol/ErrorCodes.h.ai.json create mode 100644 include/xrpl/protocol/ErrorCodes.h.ai.md create mode 100644 include/xrpl/protocol/Feature.h.ai.json create mode 100644 include/xrpl/protocol/Feature.h.ai.md create mode 100644 include/xrpl/protocol/Fees.h.ai.json create mode 100644 include/xrpl/protocol/Fees.h.ai.md create mode 100644 include/xrpl/protocol/HashPrefix.h.ai.json create mode 100644 include/xrpl/protocol/HashPrefix.h.ai.md create mode 100644 include/xrpl/protocol/IOUAmount.h.ai.json create mode 100644 include/xrpl/protocol/IOUAmount.h.ai.md create mode 100644 include/xrpl/protocol/Indexes.h.ai.json create mode 100644 include/xrpl/protocol/Indexes.h.ai.md create mode 100644 include/xrpl/protocol/InnerObjectFormats.h.ai.json create mode 100644 include/xrpl/protocol/InnerObjectFormats.h.ai.md create mode 100644 include/xrpl/protocol/Issue.h.ai.json create mode 100644 include/xrpl/protocol/Issue.h.ai.md create mode 100644 include/xrpl/protocol/KeyType.h.ai.json create mode 100644 include/xrpl/protocol/KeyType.h.ai.md create mode 100644 include/xrpl/protocol/Keylet.h.ai.json create mode 100644 include/xrpl/protocol/Keylet.h.ai.md create mode 100644 include/xrpl/protocol/KnownFormats.h.ai.json create mode 100644 include/xrpl/protocol/KnownFormats.h.ai.md create mode 100644 include/xrpl/protocol/LedgerFormats.h.ai.json create mode 100644 include/xrpl/protocol/LedgerFormats.h.ai.md create mode 100644 include/xrpl/protocol/LedgerHeader.h.ai.json create mode 100644 include/xrpl/protocol/LedgerHeader.h.ai.md create mode 100644 include/xrpl/protocol/LedgerShortcut.h.ai.json create mode 100644 include/xrpl/protocol/LedgerShortcut.h.ai.md create mode 100644 include/xrpl/protocol/MPTAmount.h.ai.json create mode 100644 include/xrpl/protocol/MPTAmount.h.ai.md create mode 100644 include/xrpl/protocol/MPTIssue.h.ai.json create mode 100644 include/xrpl/protocol/MPTIssue.h.ai.md create mode 100644 include/xrpl/protocol/MultiApiJson.h.ai.json create mode 100644 include/xrpl/protocol/MultiApiJson.h.ai.md create mode 100644 include/xrpl/protocol/NFTSyntheticSerializer.h.ai.json create mode 100644 include/xrpl/protocol/NFTSyntheticSerializer.h.ai.md create mode 100644 include/xrpl/protocol/NFTokenID.h.ai.json create mode 100644 include/xrpl/protocol/NFTokenID.h.ai.md create mode 100644 include/xrpl/protocol/NFTokenOfferID.h.ai.json create mode 100644 include/xrpl/protocol/NFTokenOfferID.h.ai.md create mode 100644 include/xrpl/protocol/PathAsset.h.ai.json create mode 100644 include/xrpl/protocol/PathAsset.h.ai.md create mode 100644 include/xrpl/protocol/PayChan.h.ai.json create mode 100644 include/xrpl/protocol/PayChan.h.ai.md create mode 100644 include/xrpl/protocol/Permissions.h.ai.json create mode 100644 include/xrpl/protocol/Permissions.h.ai.md create mode 100644 include/xrpl/protocol/Protocol.h.ai.json create mode 100644 include/xrpl/protocol/Protocol.h.ai.md create mode 100644 include/xrpl/protocol/PublicKey.h.ai.json create mode 100644 include/xrpl/protocol/PublicKey.h.ai.md create mode 100644 include/xrpl/protocol/Quality.h.ai.json create mode 100644 include/xrpl/protocol/Quality.h.ai.md create mode 100644 include/xrpl/protocol/QualityFunction.h.ai.json create mode 100644 include/xrpl/protocol/QualityFunction.h.ai.md create mode 100644 include/xrpl/protocol/RPCErr.h.ai.json create mode 100644 include/xrpl/protocol/RPCErr.h.ai.md create mode 100644 include/xrpl/protocol/Rate.h.ai.json create mode 100644 include/xrpl/protocol/Rate.h.ai.md create mode 100644 include/xrpl/protocol/RippleLedgerHash.h.ai.json create mode 100644 include/xrpl/protocol/RippleLedgerHash.h.ai.md create mode 100644 include/xrpl/protocol/Rules.h.ai.json create mode 100644 include/xrpl/protocol/Rules.h.ai.md create mode 100644 include/xrpl/protocol/SField.h.ai.json create mode 100644 include/xrpl/protocol/SField.h.ai.md create mode 100644 include/xrpl/protocol/SOTemplate.h.ai.json create mode 100644 include/xrpl/protocol/SOTemplate.h.ai.md create mode 100644 include/xrpl/protocol/STAccount.h.ai.json create mode 100644 include/xrpl/protocol/STAccount.h.ai.md create mode 100644 include/xrpl/protocol/STAmount.h.ai.json create mode 100644 include/xrpl/protocol/STAmount.h.ai.md create mode 100644 include/xrpl/protocol/STArray.h.ai.json create mode 100644 include/xrpl/protocol/STArray.h.ai.md create mode 100644 include/xrpl/protocol/STBase.h.ai.json create mode 100644 include/xrpl/protocol/STBase.h.ai.md create mode 100644 include/xrpl/protocol/STBitString.h.ai.json create mode 100644 include/xrpl/protocol/STBitString.h.ai.md create mode 100644 include/xrpl/protocol/STBlob.h.ai.json create mode 100644 include/xrpl/protocol/STBlob.h.ai.md create mode 100644 include/xrpl/protocol/STCurrency.h.ai.json create mode 100644 include/xrpl/protocol/STCurrency.h.ai.md create mode 100644 include/xrpl/protocol/STExchange.h.ai.json create mode 100644 include/xrpl/protocol/STExchange.h.ai.md create mode 100644 include/xrpl/protocol/STInteger.h.ai.json create mode 100644 include/xrpl/protocol/STInteger.h.ai.md create mode 100644 include/xrpl/protocol/STIssue.h.ai.json create mode 100644 include/xrpl/protocol/STIssue.h.ai.md create mode 100644 include/xrpl/protocol/STLedgerEntry.h.ai.json create mode 100644 include/xrpl/protocol/STLedgerEntry.h.ai.md create mode 100644 include/xrpl/protocol/STNumber.h.ai.json create mode 100644 include/xrpl/protocol/STNumber.h.ai.md create mode 100644 include/xrpl/protocol/STObject.h.ai.json create mode 100644 include/xrpl/protocol/STObject.h.ai.md create mode 100644 include/xrpl/protocol/STParsedJSON.h.ai.json create mode 100644 include/xrpl/protocol/STParsedJSON.h.ai.md create mode 100644 include/xrpl/protocol/STPathSet.h.ai.json create mode 100644 include/xrpl/protocol/STPathSet.h.ai.md create mode 100644 include/xrpl/protocol/STTakesAsset.h.ai.json create mode 100644 include/xrpl/protocol/STTakesAsset.h.ai.md create mode 100644 include/xrpl/protocol/STTx.h.ai.json create mode 100644 include/xrpl/protocol/STTx.h.ai.md create mode 100644 include/xrpl/protocol/STValidation.h.ai.json create mode 100644 include/xrpl/protocol/STValidation.h.ai.md create mode 100644 include/xrpl/protocol/STVector256.h.ai.json create mode 100644 include/xrpl/protocol/STVector256.h.ai.md create mode 100644 include/xrpl/protocol/STXChainBridge.h.ai.json create mode 100644 include/xrpl/protocol/STXChainBridge.h.ai.md create mode 100644 include/xrpl/protocol/SecretKey.h.ai.json create mode 100644 include/xrpl/protocol/SecretKey.h.ai.md create mode 100644 include/xrpl/protocol/Seed.h.ai.json create mode 100644 include/xrpl/protocol/Seed.h.ai.md create mode 100644 include/xrpl/protocol/SeqProxy.h.ai.json create mode 100644 include/xrpl/protocol/SeqProxy.h.ai.md create mode 100644 include/xrpl/protocol/Serializer.h.ai.json create mode 100644 include/xrpl/protocol/Serializer.h.ai.md create mode 100644 include/xrpl/protocol/Sign.h.ai.json create mode 100644 include/xrpl/protocol/Sign.h.ai.md create mode 100644 include/xrpl/protocol/SystemParameters.h.ai.json create mode 100644 include/xrpl/protocol/SystemParameters.h.ai.md create mode 100644 include/xrpl/protocol/TER.h.ai.json create mode 100644 include/xrpl/protocol/TER.h.ai.md create mode 100644 include/xrpl/protocol/TxFlags.h.ai.json create mode 100644 include/xrpl/protocol/TxFlags.h.ai.md create mode 100644 include/xrpl/protocol/TxFormats.h.ai.json create mode 100644 include/xrpl/protocol/TxFormats.h.ai.md create mode 100644 include/xrpl/protocol/TxMeta.h.ai.json create mode 100644 include/xrpl/protocol/TxMeta.h.ai.md create mode 100644 include/xrpl/protocol/TxSearched.h.ai.json create mode 100644 include/xrpl/protocol/TxSearched.h.ai.md create mode 100644 include/xrpl/protocol/UintTypes.h.ai.json create mode 100644 include/xrpl/protocol/UintTypes.h.ai.md create mode 100644 include/xrpl/protocol/Units.h.ai.json create mode 100644 include/xrpl/protocol/Units.h.ai.md create mode 100644 include/xrpl/protocol/XChainAttestations.h.ai.json create mode 100644 include/xrpl/protocol/XChainAttestations.h.ai.md create mode 100644 include/xrpl/protocol/XRPAmount.h.ai.json create mode 100644 include/xrpl/protocol/XRPAmount.h.ai.md create mode 100644 include/xrpl/protocol/detail/STVar.h.ai.json create mode 100644 include/xrpl/protocol/detail/STVar.h.ai.md create mode 100644 include/xrpl/protocol/detail/b58_utils.h.ai.json create mode 100644 include/xrpl/protocol/detail/b58_utils.h.ai.md create mode 100644 include/xrpl/protocol/detail/secp256k1.h.ai.json create mode 100644 include/xrpl/protocol/detail/secp256k1.h.ai.md create mode 100644 include/xrpl/protocol/detail/token_errors.h.ai.json create mode 100644 include/xrpl/protocol/detail/token_errors.h.ai.md create mode 100644 include/xrpl/protocol/digest.h.ai.json create mode 100644 include/xrpl/protocol/digest.h.ai.md create mode 100644 include/xrpl/protocol/json_get_or_throw.h.ai.json create mode 100644 include/xrpl/protocol/json_get_or_throw.h.ai.md create mode 100644 include/xrpl/protocol/jss.h.ai.json create mode 100644 include/xrpl/protocol/jss.h.ai.md create mode 100644 include/xrpl/protocol/messages.h.ai.json create mode 100644 include/xrpl/protocol/messages.h.ai.md create mode 100644 include/xrpl/protocol/nft.h.ai.json create mode 100644 include/xrpl/protocol/nft.h.ai.md create mode 100644 include/xrpl/protocol/nftPageMask.h.ai.json create mode 100644 include/xrpl/protocol/nftPageMask.h.ai.md create mode 100644 include/xrpl/protocol/serialize.h.ai.json create mode 100644 include/xrpl/protocol/serialize.h.ai.md create mode 100644 include/xrpl/protocol/st.h.ai.json create mode 100644 include/xrpl/protocol/st.h.ai.md create mode 100644 include/xrpl/protocol/tokens.h.ai.json create mode 100644 include/xrpl/protocol/tokens.h.ai.md create mode 100644 include/xrpl/protocol_autogen/LedgerEntryBase.h.ai.json create mode 100644 include/xrpl/protocol_autogen/LedgerEntryBase.h.ai.md create mode 100644 include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h.ai.json create mode 100644 include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h.ai.md create mode 100644 include/xrpl/protocol_autogen/STObjectValidation.h.ai.json create mode 100644 include/xrpl/protocol_autogen/STObjectValidation.h.ai.md create mode 100644 include/xrpl/protocol_autogen/TransactionBase.h.ai.json create mode 100644 include/xrpl/protocol_autogen/TransactionBase.h.ai.md create mode 100644 include/xrpl/protocol_autogen/TransactionBuilderBase.h.ai.json create mode 100644 include/xrpl/protocol_autogen/TransactionBuilderBase.h.ai.md create mode 100644 include/xrpl/protocol_autogen/Utils.h.ai.json create mode 100644 include/xrpl/protocol_autogen/Utils.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/AMM.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/AMM.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Amendments.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Amendments.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Bridge.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Bridge.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Check.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Check.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Credential.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Credential.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/DID.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/DID.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Delegate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Delegate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Escrow.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Escrow.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Loan.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Loan.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/MPToken.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/MPToken.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Offer.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Offer.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Oracle.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Oracle.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/PayChannel.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/PayChannel.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/RippleState.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/RippleState.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/SignerList.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/SignerList.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Ticket.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Ticket.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Vault.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Vault.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h.ai.md create mode 100644 include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h.ai.json create mode 100644 include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/AMMBid.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/AMMBid.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/AMMClawback.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/AMMClawback.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/AMMCreate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/AMMCreate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/AMMDelete.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/AMMDelete.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/AMMDeposit.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/AMMDeposit.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/AMMVote.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/AMMVote.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/AMMWithdraw.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/AMMWithdraw.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/AccountDelete.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/AccountDelete.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/AccountSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/AccountSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/Batch.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/Batch.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/CheckCancel.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/CheckCancel.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/CheckCash.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/CheckCash.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/CheckCreate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/CheckCreate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/Clawback.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/Clawback.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/CredentialAccept.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/CredentialAccept.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/CredentialCreate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/CredentialCreate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/CredentialDelete.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/CredentialDelete.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/DIDDelete.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/DIDDelete.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/DIDSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/DIDSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/DelegateSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/DelegateSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/DepositPreauth.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/DepositPreauth.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/EnableAmendment.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/EnableAmendment.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/EscrowCancel.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/EscrowCancel.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/EscrowCreate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/EscrowCreate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/EscrowFinish.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/EscrowFinish.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LedgerStateFix.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LedgerStateFix.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LoanDelete.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LoanDelete.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LoanManage.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LoanManage.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LoanPay.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LoanPay.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/LoanSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/LoanSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenBurn.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenBurn.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenMint.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenMint.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenModify.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/NFTokenModify.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/OfferCancel.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/OfferCancel.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/OfferCreate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/OfferCreate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/OracleDelete.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/OracleDelete.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/OracleSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/OracleSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/Payment.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/Payment.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/SetFee.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/SetFee.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/SetRegularKey.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/SetRegularKey.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/SignerListSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/SignerListSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/TicketCreate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/TicketCreate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/TrustSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/TrustSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/UNLModify.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/UNLModify.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/VaultClawback.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/VaultClawback.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/VaultCreate.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/VaultCreate.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/VaultDelete.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/VaultDelete.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/VaultDeposit.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/VaultDeposit.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/VaultSet.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/VaultSet.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/VaultWithdraw.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/VaultWithdraw.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/XChainClaim.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/XChainClaim.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/XChainCommit.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/XChainCommit.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h.ai.md create mode 100644 include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h.ai.json create mode 100644 include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h.ai.md create mode 100644 include/xrpl/rdb/DBInit.h.ai.json create mode 100644 include/xrpl/rdb/DBInit.h.ai.md create mode 100644 include/xrpl/rdb/DatabaseCon.h.ai.json create mode 100644 include/xrpl/rdb/DatabaseCon.h.ai.md create mode 100644 include/xrpl/rdb/RelationalDatabase.h.ai.json create mode 100644 include/xrpl/rdb/RelationalDatabase.h.ai.md create mode 100644 include/xrpl/rdb/SociDB.h.ai.json create mode 100644 include/xrpl/rdb/SociDB.h.ai.md create mode 100644 include/xrpl/resource/Charge.h.ai.json create mode 100644 include/xrpl/resource/Charge.h.ai.md create mode 100644 include/xrpl/resource/Consumer.h.ai.json create mode 100644 include/xrpl/resource/Consumer.h.ai.md create mode 100644 include/xrpl/resource/Disposition.h.ai.json create mode 100644 include/xrpl/resource/Disposition.h.ai.md create mode 100644 include/xrpl/resource/Fees.h.ai.json create mode 100644 include/xrpl/resource/Fees.h.ai.md create mode 100644 include/xrpl/resource/Gossip.h.ai.json create mode 100644 include/xrpl/resource/Gossip.h.ai.md create mode 100644 include/xrpl/resource/ResourceManager.h.ai.json create mode 100644 include/xrpl/resource/ResourceManager.h.ai.md create mode 100644 include/xrpl/resource/Types.h.ai.json create mode 100644 include/xrpl/resource/Types.h.ai.md create mode 100644 include/xrpl/resource/detail/Entry.h.ai.json create mode 100644 include/xrpl/resource/detail/Entry.h.ai.md create mode 100644 include/xrpl/resource/detail/Import.h.ai.json create mode 100644 include/xrpl/resource/detail/Import.h.ai.md create mode 100644 include/xrpl/resource/detail/Key.h.ai.json create mode 100644 include/xrpl/resource/detail/Key.h.ai.md create mode 100644 include/xrpl/resource/detail/Kind.h.ai.json create mode 100644 include/xrpl/resource/detail/Kind.h.ai.md create mode 100644 include/xrpl/resource/detail/Logic.h.ai.json create mode 100644 include/xrpl/resource/detail/Logic.h.ai.md create mode 100644 include/xrpl/resource/detail/Tuning.h.ai.json create mode 100644 include/xrpl/resource/detail/Tuning.h.ai.md create mode 100644 include/xrpl/server/Handoff.h.ai.json create mode 100644 include/xrpl/server/Handoff.h.ai.md create mode 100644 include/xrpl/server/InfoSub.h.ai.json create mode 100644 include/xrpl/server/InfoSub.h.ai.md create mode 100644 include/xrpl/server/LoadFeeTrack.h.ai.json create mode 100644 include/xrpl/server/LoadFeeTrack.h.ai.md create mode 100644 include/xrpl/server/Manifest.h.ai.json create mode 100644 include/xrpl/server/Manifest.h.ai.md create mode 100644 include/xrpl/server/NetworkOPs.h.ai.json create mode 100644 include/xrpl/server/NetworkOPs.h.ai.md create mode 100644 include/xrpl/server/Port.h.ai.json create mode 100644 include/xrpl/server/Port.h.ai.md create mode 100644 include/xrpl/server/Server.h.ai.json create mode 100644 include/xrpl/server/Server.h.ai.md create mode 100644 include/xrpl/server/Session.h.ai.json create mode 100644 include/xrpl/server/Session.h.ai.md create mode 100644 include/xrpl/server/SimpleWriter.h.ai.json create mode 100644 include/xrpl/server/SimpleWriter.h.ai.md create mode 100644 include/xrpl/server/State.h.ai.json create mode 100644 include/xrpl/server/State.h.ai.md create mode 100644 include/xrpl/server/Vacuum.h.ai.json create mode 100644 include/xrpl/server/Vacuum.h.ai.md create mode 100644 include/xrpl/server/WSSession.h.ai.json create mode 100644 include/xrpl/server/WSSession.h.ai.md create mode 100644 include/xrpl/server/Wallet.h.ai.json create mode 100644 include/xrpl/server/Wallet.h.ai.md create mode 100644 include/xrpl/server/Writer.h.ai.json create mode 100644 include/xrpl/server/Writer.h.ai.md create mode 100644 include/xrpl/server/detail/BaseHTTPPeer.h.ai.json create mode 100644 include/xrpl/server/detail/BaseHTTPPeer.h.ai.md create mode 100644 include/xrpl/server/detail/BasePeer.h.ai.json create mode 100644 include/xrpl/server/detail/BasePeer.h.ai.md create mode 100644 include/xrpl/server/detail/BaseWSPeer.h.ai.json create mode 100644 include/xrpl/server/detail/BaseWSPeer.h.ai.md create mode 100644 include/xrpl/server/detail/Door.h.ai.json create mode 100644 include/xrpl/server/detail/Door.h.ai.md create mode 100644 include/xrpl/server/detail/JSONRPCUtil.h.ai.json create mode 100644 include/xrpl/server/detail/JSONRPCUtil.h.ai.md create mode 100644 include/xrpl/server/detail/LowestLayer.h.ai.json create mode 100644 include/xrpl/server/detail/LowestLayer.h.ai.md create mode 100644 include/xrpl/server/detail/PlainHTTPPeer.h.ai.json create mode 100644 include/xrpl/server/detail/PlainHTTPPeer.h.ai.md create mode 100644 include/xrpl/server/detail/PlainWSPeer.h.ai.json create mode 100644 include/xrpl/server/detail/PlainWSPeer.h.ai.md create mode 100644 include/xrpl/server/detail/SSLHTTPPeer.h.ai.json create mode 100644 include/xrpl/server/detail/SSLHTTPPeer.h.ai.md create mode 100644 include/xrpl/server/detail/SSLWSPeer.h.ai.json create mode 100644 include/xrpl/server/detail/SSLWSPeer.h.ai.md create mode 100644 include/xrpl/server/detail/ServerImpl.h.ai.json create mode 100644 include/xrpl/server/detail/ServerImpl.h.ai.md create mode 100644 include/xrpl/server/detail/Spawn.h.ai.json create mode 100644 include/xrpl/server/detail/Spawn.h.ai.md create mode 100644 include/xrpl/server/detail/io_list.h.ai.json create mode 100644 include/xrpl/server/detail/io_list.h.ai.md create mode 100644 include/xrpl/shamap/Family.h.ai.json create mode 100644 include/xrpl/shamap/Family.h.ai.md create mode 100644 include/xrpl/shamap/FullBelowCache.h.ai.json create mode 100644 include/xrpl/shamap/FullBelowCache.h.ai.md create mode 100644 include/xrpl/shamap/SHAMap.h.ai.json create mode 100644 include/xrpl/shamap/SHAMap.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapAccountStateLeafNode.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapAccountStateLeafNode.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapAddNode.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapAddNode.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapInnerNode.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapInnerNode.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapItem.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapItem.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapLeafNode.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapLeafNode.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapMissingNode.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapMissingNode.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapNodeID.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapNodeID.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapSyncFilter.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapSyncFilter.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapTreeNode.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapTreeNode.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapTxLeafNode.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapTxLeafNode.h.ai.md create mode 100644 include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h.ai.json create mode 100644 include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h.ai.md create mode 100644 include/xrpl/shamap/TreeNodeCache.h.ai.json create mode 100644 include/xrpl/shamap/TreeNodeCache.h.ai.md create mode 100644 include/xrpl/shamap/detail/TaggedPointer.h.ai.json create mode 100644 include/xrpl/shamap/detail/TaggedPointer.h.ai.md create mode 100644 include/xrpl/shamap/detail/TaggedPointer.ipp.ai.json create mode 100644 include/xrpl/shamap/detail/TaggedPointer.ipp.ai.md create mode 100644 include/xrpl/tx/ApplyContext.h.ai.json create mode 100644 include/xrpl/tx/ApplyContext.h.ai.md create mode 100644 include/xrpl/tx/SignerEntries.h.ai.json create mode 100644 include/xrpl/tx/SignerEntries.h.ai.md create mode 100644 include/xrpl/tx/Transactor.h.ai.json create mode 100644 include/xrpl/tx/Transactor.h.ai.md create mode 100644 include/xrpl/tx/apply.h.ai.json create mode 100644 include/xrpl/tx/apply.h.ai.md create mode 100644 include/xrpl/tx/applySteps.h.ai.json create mode 100644 include/xrpl/tx/applySteps.h.ai.md create mode 100644 include/xrpl/tx/invariants/AMMInvariant.h.ai.json create mode 100644 include/xrpl/tx/invariants/AMMInvariant.h.ai.md create mode 100644 include/xrpl/tx/invariants/FreezeInvariant.h.ai.json create mode 100644 include/xrpl/tx/invariants/FreezeInvariant.h.ai.md create mode 100644 include/xrpl/tx/invariants/InvariantCheck.h.ai.json create mode 100644 include/xrpl/tx/invariants/InvariantCheck.h.ai.md create mode 100644 include/xrpl/tx/invariants/InvariantCheckPrivilege.h.ai.json create mode 100644 include/xrpl/tx/invariants/InvariantCheckPrivilege.h.ai.md create mode 100644 include/xrpl/tx/invariants/LoanBrokerInvariant.h.ai.json create mode 100644 include/xrpl/tx/invariants/LoanBrokerInvariant.h.ai.md create mode 100644 include/xrpl/tx/invariants/LoanInvariant.h.ai.json create mode 100644 include/xrpl/tx/invariants/LoanInvariant.h.ai.md create mode 100644 include/xrpl/tx/invariants/MPTInvariant.h.ai.json create mode 100644 include/xrpl/tx/invariants/MPTInvariant.h.ai.md create mode 100644 include/xrpl/tx/invariants/NFTInvariant.h.ai.json create mode 100644 include/xrpl/tx/invariants/NFTInvariant.h.ai.md create mode 100644 include/xrpl/tx/invariants/PermissionedDEXInvariant.h.ai.json create mode 100644 include/xrpl/tx/invariants/PermissionedDEXInvariant.h.ai.md create mode 100644 include/xrpl/tx/invariants/PermissionedDomainInvariant.h.ai.json create mode 100644 include/xrpl/tx/invariants/PermissionedDomainInvariant.h.ai.md create mode 100644 include/xrpl/tx/invariants/VaultInvariant.h.ai.json create mode 100644 include/xrpl/tx/invariants/VaultInvariant.h.ai.md create mode 100644 include/xrpl/tx/paths/AMMLiquidity.h.ai.json create mode 100644 include/xrpl/tx/paths/AMMLiquidity.h.ai.md create mode 100644 include/xrpl/tx/paths/AMMOffer.h.ai.json create mode 100644 include/xrpl/tx/paths/AMMOffer.h.ai.md create mode 100644 include/xrpl/tx/paths/BookTip.h.ai.json create mode 100644 include/xrpl/tx/paths/BookTip.h.ai.md create mode 100644 include/xrpl/tx/paths/Flow.h.ai.json create mode 100644 include/xrpl/tx/paths/Flow.h.ai.md create mode 100644 include/xrpl/tx/paths/Offer.h.ai.json create mode 100644 include/xrpl/tx/paths/Offer.h.ai.md create mode 100644 include/xrpl/tx/paths/OfferStream.h.ai.json create mode 100644 include/xrpl/tx/paths/OfferStream.h.ai.md create mode 100644 include/xrpl/tx/paths/RippleCalc.h.ai.json create mode 100644 include/xrpl/tx/paths/RippleCalc.h.ai.md create mode 100644 include/xrpl/tx/paths/detail/AmountSpec.h.ai.json create mode 100644 include/xrpl/tx/paths/detail/AmountSpec.h.ai.md create mode 100644 include/xrpl/tx/paths/detail/EitherAmount.h.ai.json create mode 100644 include/xrpl/tx/paths/detail/EitherAmount.h.ai.md create mode 100644 include/xrpl/tx/paths/detail/FlatSets.h.ai.json create mode 100644 include/xrpl/tx/paths/detail/FlatSets.h.ai.md create mode 100644 include/xrpl/tx/paths/detail/FlowDebugInfo.h.ai.json create mode 100644 include/xrpl/tx/paths/detail/FlowDebugInfo.h.ai.md create mode 100644 include/xrpl/tx/paths/detail/StepChecks.h.ai.json create mode 100644 include/xrpl/tx/paths/detail/StepChecks.h.ai.md create mode 100644 include/xrpl/tx/paths/detail/Steps.h.ai.json create mode 100644 include/xrpl/tx/paths/detail/Steps.h.ai.md create mode 100644 include/xrpl/tx/paths/detail/StrandFlow.h.ai.json create mode 100644 include/xrpl/tx/paths/detail/StrandFlow.h.ai.md create mode 100644 include/xrpl/tx/transactors/account/AccountDelete.h.ai.json create mode 100644 include/xrpl/tx/transactors/account/AccountDelete.h.ai.md create mode 100644 include/xrpl/tx/transactors/account/AccountSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/account/AccountSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/account/SetRegularKey.h.ai.json create mode 100644 include/xrpl/tx/transactors/account/SetRegularKey.h.ai.md create mode 100644 include/xrpl/tx/transactors/account/SignerListSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/account/SignerListSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/bridge/XChainBridge.h.ai.json create mode 100644 include/xrpl/tx/transactors/bridge/XChainBridge.h.ai.md create mode 100644 include/xrpl/tx/transactors/check/CheckCancel.h.ai.json create mode 100644 include/xrpl/tx/transactors/check/CheckCancel.h.ai.md create mode 100644 include/xrpl/tx/transactors/check/CheckCash.h.ai.json create mode 100644 include/xrpl/tx/transactors/check/CheckCash.h.ai.md create mode 100644 include/xrpl/tx/transactors/check/CheckCreate.h.ai.json create mode 100644 include/xrpl/tx/transactors/check/CheckCreate.h.ai.md create mode 100644 include/xrpl/tx/transactors/credentials/CredentialAccept.h.ai.json create mode 100644 include/xrpl/tx/transactors/credentials/CredentialAccept.h.ai.md create mode 100644 include/xrpl/tx/transactors/credentials/CredentialCreate.h.ai.json create mode 100644 include/xrpl/tx/transactors/credentials/CredentialCreate.h.ai.md create mode 100644 include/xrpl/tx/transactors/credentials/CredentialDelete.h.ai.json create mode 100644 include/xrpl/tx/transactors/credentials/CredentialDelete.h.ai.md create mode 100644 include/xrpl/tx/transactors/delegate/DelegateSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/delegate/DelegateSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/AMMBid.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/AMMBid.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/AMMClawback.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/AMMClawback.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/AMMContext.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/AMMContext.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/AMMCreate.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/AMMCreate.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/AMMDelete.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/AMMDelete.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/AMMDeposit.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/AMMDeposit.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/AMMVote.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/AMMVote.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/AMMWithdraw.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/AMMWithdraw.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/OfferCancel.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/OfferCancel.h.ai.md create mode 100644 include/xrpl/tx/transactors/dex/OfferCreate.h.ai.json create mode 100644 include/xrpl/tx/transactors/dex/OfferCreate.h.ai.md create mode 100644 include/xrpl/tx/transactors/did/DIDDelete.h.ai.json create mode 100644 include/xrpl/tx/transactors/did/DIDDelete.h.ai.md create mode 100644 include/xrpl/tx/transactors/did/DIDSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/did/DIDSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/escrow/EscrowCancel.h.ai.json create mode 100644 include/xrpl/tx/transactors/escrow/EscrowCancel.h.ai.md create mode 100644 include/xrpl/tx/transactors/escrow/EscrowCreate.h.ai.json create mode 100644 include/xrpl/tx/transactors/escrow/EscrowCreate.h.ai.md create mode 100644 include/xrpl/tx/transactors/escrow/EscrowFinish.h.ai.json create mode 100644 include/xrpl/tx/transactors/escrow/EscrowFinish.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LendingHelpers.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LendingHelpers.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerDelete.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerDelete.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LoanBrokerSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LoanDelete.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LoanDelete.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LoanManage.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LoanManage.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LoanPay.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LoanPay.h.ai.md create mode 100644 include/xrpl/tx/transactors/lending/LoanSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/lending/LoanSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h.ai.json create mode 100644 include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h.ai.md create mode 100644 include/xrpl/tx/transactors/nft/NFTokenBurn.h.ai.json create mode 100644 include/xrpl/tx/transactors/nft/NFTokenBurn.h.ai.md create mode 100644 include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h.ai.json create mode 100644 include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h.ai.md create mode 100644 include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h.ai.json create mode 100644 include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h.ai.md create mode 100644 include/xrpl/tx/transactors/nft/NFTokenMint.h.ai.json create mode 100644 include/xrpl/tx/transactors/nft/NFTokenMint.h.ai.md create mode 100644 include/xrpl/tx/transactors/nft/NFTokenModify.h.ai.json create mode 100644 include/xrpl/tx/transactors/nft/NFTokenModify.h.ai.md create mode 100644 include/xrpl/tx/transactors/oracle/OracleDelete.h.ai.json create mode 100644 include/xrpl/tx/transactors/oracle/OracleDelete.h.ai.md create mode 100644 include/xrpl/tx/transactors/oracle/OracleSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/oracle/OracleSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/payment/DepositPreauth.h.ai.json create mode 100644 include/xrpl/tx/transactors/payment/DepositPreauth.h.ai.md create mode 100644 include/xrpl/tx/transactors/payment/Payment.h.ai.json create mode 100644 include/xrpl/tx/transactors/payment/Payment.h.ai.md create mode 100644 include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h.ai.json create mode 100644 include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h.ai.md create mode 100644 include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h.ai.json create mode 100644 include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h.ai.md create mode 100644 include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h.ai.json create mode 100644 include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h.ai.md create mode 100644 include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h.ai.json create mode 100644 include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h.ai.md create mode 100644 include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/system/Batch.h.ai.json create mode 100644 include/xrpl/tx/transactors/system/Batch.h.ai.md create mode 100644 include/xrpl/tx/transactors/system/Change.h.ai.json create mode 100644 include/xrpl/tx/transactors/system/Change.h.ai.md create mode 100644 include/xrpl/tx/transactors/system/LedgerStateFix.h.ai.json create mode 100644 include/xrpl/tx/transactors/system/LedgerStateFix.h.ai.md create mode 100644 include/xrpl/tx/transactors/system/TicketCreate.h.ai.json create mode 100644 include/xrpl/tx/transactors/system/TicketCreate.h.ai.md create mode 100644 include/xrpl/tx/transactors/token/Clawback.h.ai.json create mode 100644 include/xrpl/tx/transactors/token/Clawback.h.ai.md create mode 100644 include/xrpl/tx/transactors/token/MPTokenAuthorize.h.ai.json create mode 100644 include/xrpl/tx/transactors/token/MPTokenAuthorize.h.ai.md create mode 100644 include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h.ai.json create mode 100644 include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h.ai.md create mode 100644 include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h.ai.json create mode 100644 include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h.ai.md create mode 100644 include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/token/TrustSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/token/TrustSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/vault/VaultClawback.h.ai.json create mode 100644 include/xrpl/tx/transactors/vault/VaultClawback.h.ai.md create mode 100644 include/xrpl/tx/transactors/vault/VaultCreate.h.ai.json create mode 100644 include/xrpl/tx/transactors/vault/VaultCreate.h.ai.md create mode 100644 include/xrpl/tx/transactors/vault/VaultDelete.h.ai.json create mode 100644 include/xrpl/tx/transactors/vault/VaultDelete.h.ai.md create mode 100644 include/xrpl/tx/transactors/vault/VaultDeposit.h.ai.json create mode 100644 include/xrpl/tx/transactors/vault/VaultDeposit.h.ai.md create mode 100644 include/xrpl/tx/transactors/vault/VaultSet.h.ai.json create mode 100644 include/xrpl/tx/transactors/vault/VaultSet.h.ai.md create mode 100644 include/xrpl/tx/transactors/vault/VaultWithdraw.h.ai.json create mode 100644 include/xrpl/tx/transactors/vault/VaultWithdraw.h.ai.md create mode 100644 src/libxrpl/basics/Archive.cpp.ai.json create mode 100644 src/libxrpl/basics/Archive.cpp.ai.md create mode 100644 src/libxrpl/basics/BasicConfig.cpp.ai.json create mode 100644 src/libxrpl/basics/BasicConfig.cpp.ai.md create mode 100644 src/libxrpl/basics/CountedObject.cpp.ai.json create mode 100644 src/libxrpl/basics/CountedObject.cpp.ai.md create mode 100644 src/libxrpl/basics/FileUtilities.cpp.ai.json create mode 100644 src/libxrpl/basics/FileUtilities.cpp.ai.md create mode 100644 src/libxrpl/basics/Log.cpp.ai.json create mode 100644 src/libxrpl/basics/Log.cpp.ai.md create mode 100644 src/libxrpl/basics/MallocTrim.cpp.ai.json create mode 100644 src/libxrpl/basics/MallocTrim.cpp.ai.md create mode 100644 src/libxrpl/basics/Number.cpp.ai.json create mode 100644 src/libxrpl/basics/Number.cpp.ai.md create mode 100644 src/libxrpl/basics/ResolverAsio.cpp.ai.json create mode 100644 src/libxrpl/basics/ResolverAsio.cpp.ai.md create mode 100644 src/libxrpl/basics/StringUtilities.cpp.ai.json create mode 100644 src/libxrpl/basics/StringUtilities.cpp.ai.md create mode 100644 src/libxrpl/basics/UptimeClock.cpp.ai.json create mode 100644 src/libxrpl/basics/UptimeClock.cpp.ai.md create mode 100644 src/libxrpl/basics/base64.cpp.ai.json create mode 100644 src/libxrpl/basics/base64.cpp.ai.md create mode 100644 src/libxrpl/basics/contract.cpp.ai.json create mode 100644 src/libxrpl/basics/contract.cpp.ai.md create mode 100644 src/libxrpl/basics/make_SSLContext.cpp.ai.json create mode 100644 src/libxrpl/basics/make_SSLContext.cpp.ai.md create mode 100644 src/libxrpl/basics/mulDiv.cpp.ai.json create mode 100644 src/libxrpl/basics/mulDiv.cpp.ai.md create mode 100644 src/libxrpl/beast/clock/basic_seconds_clock.cpp.ai.json create mode 100644 src/libxrpl/beast/clock/basic_seconds_clock.cpp.ai.md create mode 100644 src/libxrpl/beast/core/CurrentThreadName.cpp.ai.json create mode 100644 src/libxrpl/beast/core/CurrentThreadName.cpp.ai.md create mode 100644 src/libxrpl/beast/core/SemanticVersion.cpp.ai.json create mode 100644 src/libxrpl/beast/core/SemanticVersion.cpp.ai.md create mode 100644 src/libxrpl/beast/insight/Collector.cpp.ai.json create mode 100644 src/libxrpl/beast/insight/Collector.cpp.ai.md create mode 100644 src/libxrpl/beast/insight/Groups.cpp.ai.json create mode 100644 src/libxrpl/beast/insight/Groups.cpp.ai.md create mode 100644 src/libxrpl/beast/insight/Hook.cpp.ai.json create mode 100644 src/libxrpl/beast/insight/Hook.cpp.ai.md create mode 100644 src/libxrpl/beast/insight/Metric.cpp.ai.json create mode 100644 src/libxrpl/beast/insight/Metric.cpp.ai.md create mode 100644 src/libxrpl/beast/insight/NullCollector.cpp.ai.json create mode 100644 src/libxrpl/beast/insight/NullCollector.cpp.ai.md create mode 100644 src/libxrpl/beast/insight/StatsDCollector.cpp.ai.json create mode 100644 src/libxrpl/beast/insight/StatsDCollector.cpp.ai.md create mode 100644 src/libxrpl/beast/net/IPAddressConversion.cpp.ai.json create mode 100644 src/libxrpl/beast/net/IPAddressConversion.cpp.ai.md create mode 100644 src/libxrpl/beast/net/IPAddressV4.cpp.ai.json create mode 100644 src/libxrpl/beast/net/IPAddressV4.cpp.ai.md create mode 100644 src/libxrpl/beast/net/IPAddressV6.cpp.ai.json create mode 100644 src/libxrpl/beast/net/IPAddressV6.cpp.ai.md create mode 100644 src/libxrpl/beast/net/IPEndpoint.cpp.ai.json create mode 100644 src/libxrpl/beast/net/IPEndpoint.cpp.ai.md create mode 100644 src/libxrpl/beast/utility/beast_Journal.cpp.ai.json create mode 100644 src/libxrpl/beast/utility/beast_Journal.cpp.ai.md create mode 100644 src/libxrpl/beast/utility/beast_PropertyStream.cpp.ai.json create mode 100644 src/libxrpl/beast/utility/beast_PropertyStream.cpp.ai.md create mode 100644 src/libxrpl/conditions/Condition.cpp.ai.json create mode 100644 src/libxrpl/conditions/Condition.cpp.ai.md create mode 100644 src/libxrpl/conditions/Fulfillment.cpp.ai.json create mode 100644 src/libxrpl/conditions/Fulfillment.cpp.ai.md create mode 100644 src/libxrpl/conditions/error.cpp.ai.json create mode 100644 src/libxrpl/conditions/error.cpp.ai.md create mode 100644 src/libxrpl/core/HashRouter.cpp.ai.json create mode 100644 src/libxrpl/core/HashRouter.cpp.ai.md create mode 100644 src/libxrpl/core/detail/Job.cpp.ai.json create mode 100644 src/libxrpl/core/detail/Job.cpp.ai.md create mode 100644 src/libxrpl/core/detail/JobQueue.cpp.ai.json create mode 100644 src/libxrpl/core/detail/JobQueue.cpp.ai.md create mode 100644 src/libxrpl/core/detail/LoadEvent.cpp.ai.json create mode 100644 src/libxrpl/core/detail/LoadEvent.cpp.ai.md create mode 100644 src/libxrpl/core/detail/LoadMonitor.cpp.ai.json create mode 100644 src/libxrpl/core/detail/LoadMonitor.cpp.ai.md create mode 100644 src/libxrpl/core/detail/Workers.cpp.ai.json create mode 100644 src/libxrpl/core/detail/Workers.cpp.ai.md create mode 100644 src/libxrpl/crypto/RFC1751.cpp.ai.json create mode 100644 src/libxrpl/crypto/RFC1751.cpp.ai.md create mode 100644 src/libxrpl/crypto/csprng.cpp.ai.json create mode 100644 src/libxrpl/crypto/csprng.cpp.ai.md create mode 100644 src/libxrpl/crypto/secure_erase.cpp.ai.json create mode 100644 src/libxrpl/crypto/secure_erase.cpp.ai.md create mode 100644 src/libxrpl/git/Git.cpp.ai.json create mode 100644 src/libxrpl/git/Git.cpp.ai.md create mode 100644 src/libxrpl/json/JsonPropertyStream.cpp.ai.json create mode 100644 src/libxrpl/json/JsonPropertyStream.cpp.ai.md create mode 100644 src/libxrpl/json/Output.cpp.ai.json create mode 100644 src/libxrpl/json/Output.cpp.ai.md create mode 100644 src/libxrpl/json/Writer.cpp.ai.json create mode 100644 src/libxrpl/json/Writer.cpp.ai.md create mode 100644 src/libxrpl/json/json_reader.cpp.ai.json create mode 100644 src/libxrpl/json/json_reader.cpp.ai.md create mode 100644 src/libxrpl/json/json_value.cpp.ai.json create mode 100644 src/libxrpl/json/json_value.cpp.ai.md create mode 100644 src/libxrpl/json/json_valueiterator.cpp.ai.json create mode 100644 src/libxrpl/json/json_valueiterator.cpp.ai.md create mode 100644 src/libxrpl/json/json_writer.cpp.ai.json create mode 100644 src/libxrpl/json/json_writer.cpp.ai.md create mode 100644 src/libxrpl/json/to_string.cpp.ai.json create mode 100644 src/libxrpl/json/to_string.cpp.ai.md create mode 100644 src/libxrpl/ledger/AcceptedLedgerTx.cpp.ai.json create mode 100644 src/libxrpl/ledger/AcceptedLedgerTx.cpp.ai.md create mode 100644 src/libxrpl/ledger/ApplyStateTable.cpp.ai.json create mode 100644 src/libxrpl/ledger/ApplyStateTable.cpp.ai.md create mode 100644 src/libxrpl/ledger/ApplyView.cpp.ai.json create mode 100644 src/libxrpl/ledger/ApplyView.cpp.ai.md create mode 100644 src/libxrpl/ledger/ApplyViewBase.cpp.ai.json create mode 100644 src/libxrpl/ledger/ApplyViewBase.cpp.ai.md create mode 100644 src/libxrpl/ledger/ApplyViewImpl.cpp.ai.json create mode 100644 src/libxrpl/ledger/ApplyViewImpl.cpp.ai.md create mode 100644 src/libxrpl/ledger/BookDirs.cpp.ai.json create mode 100644 src/libxrpl/ledger/BookDirs.cpp.ai.md create mode 100644 src/libxrpl/ledger/BookListeners.cpp.ai.json create mode 100644 src/libxrpl/ledger/BookListeners.cpp.ai.md create mode 100644 src/libxrpl/ledger/CachedView.cpp.ai.json create mode 100644 src/libxrpl/ledger/CachedView.cpp.ai.md create mode 100644 src/libxrpl/ledger/CanonicalTXSet.cpp.ai.json create mode 100644 src/libxrpl/ledger/CanonicalTXSet.cpp.ai.md create mode 100644 src/libxrpl/ledger/Dir.cpp.ai.json create mode 100644 src/libxrpl/ledger/Dir.cpp.ai.md create mode 100644 src/libxrpl/ledger/Ledger.cpp.ai.json create mode 100644 src/libxrpl/ledger/Ledger.cpp.ai.md create mode 100644 src/libxrpl/ledger/OpenView.cpp.ai.json create mode 100644 src/libxrpl/ledger/OpenView.cpp.ai.md create mode 100644 src/libxrpl/ledger/PaymentSandbox.cpp.ai.json create mode 100644 src/libxrpl/ledger/PaymentSandbox.cpp.ai.md create mode 100644 src/libxrpl/ledger/RawStateTable.cpp.ai.json create mode 100644 src/libxrpl/ledger/RawStateTable.cpp.ai.md create mode 100644 src/libxrpl/ledger/ReadView.cpp.ai.json create mode 100644 src/libxrpl/ledger/ReadView.cpp.ai.md create mode 100644 src/libxrpl/ledger/View.cpp.ai.json create mode 100644 src/libxrpl/ledger/View.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/AMMHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/AMMHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/AccountRootHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/AccountRootHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/CredentialHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/CredentialHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/DirectoryHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/DirectoryHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/MPTokenHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/MPTokenHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/NFTokenHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/NFTokenHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/OfferHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/OfferHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/RippleStateHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/RippleStateHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/TokenHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/TokenHelpers.cpp.ai.md create mode 100644 src/libxrpl/ledger/helpers/VaultHelpers.cpp.ai.json create mode 100644 src/libxrpl/ledger/helpers/VaultHelpers.cpp.ai.md create mode 100644 src/libxrpl/net/HTTPClient.cpp.ai.json create mode 100644 src/libxrpl/net/HTTPClient.cpp.ai.md create mode 100644 src/libxrpl/net/RegisterSSLCerts.cpp.ai.json create mode 100644 src/libxrpl/net/RegisterSSLCerts.cpp.ai.md create mode 100644 src/libxrpl/nodestore/BatchWriter.cpp.ai.json create mode 100644 src/libxrpl/nodestore/BatchWriter.cpp.ai.md create mode 100644 src/libxrpl/nodestore/Database.cpp.ai.json create mode 100644 src/libxrpl/nodestore/Database.cpp.ai.md create mode 100644 src/libxrpl/nodestore/DatabaseNodeImp.cpp.ai.json create mode 100644 src/libxrpl/nodestore/DatabaseNodeImp.cpp.ai.md create mode 100644 src/libxrpl/nodestore/DatabaseRotatingImp.cpp.ai.json create mode 100644 src/libxrpl/nodestore/DatabaseRotatingImp.cpp.ai.md create mode 100644 src/libxrpl/nodestore/DecodedBlob.cpp.ai.json create mode 100644 src/libxrpl/nodestore/DecodedBlob.cpp.ai.md create mode 100644 src/libxrpl/nodestore/DummyScheduler.cpp.ai.json create mode 100644 src/libxrpl/nodestore/DummyScheduler.cpp.ai.md create mode 100644 src/libxrpl/nodestore/ManagerImp.cpp.ai.json create mode 100644 src/libxrpl/nodestore/ManagerImp.cpp.ai.md create mode 100644 src/libxrpl/nodestore/NodeObject.cpp.ai.json create mode 100644 src/libxrpl/nodestore/NodeObject.cpp.ai.md create mode 100644 src/libxrpl/nodestore/backend/MemoryFactory.cpp.ai.json create mode 100644 src/libxrpl/nodestore/backend/MemoryFactory.cpp.ai.md create mode 100644 src/libxrpl/nodestore/backend/NuDBFactory.cpp.ai.json create mode 100644 src/libxrpl/nodestore/backend/NuDBFactory.cpp.ai.md create mode 100644 src/libxrpl/nodestore/backend/NullFactory.cpp.ai.json create mode 100644 src/libxrpl/nodestore/backend/NullFactory.cpp.ai.md create mode 100644 src/libxrpl/nodestore/backend/RocksDBFactory.cpp.ai.json create mode 100644 src/libxrpl/nodestore/backend/RocksDBFactory.cpp.ai.md create mode 100644 src/libxrpl/protocol/AMMCore.cpp.ai.json create mode 100644 src/libxrpl/protocol/AMMCore.cpp.ai.md create mode 100644 src/libxrpl/protocol/AccountID.cpp.ai.json create mode 100644 src/libxrpl/protocol/AccountID.cpp.ai.md create mode 100644 src/libxrpl/protocol/Asset.cpp.ai.json create mode 100644 src/libxrpl/protocol/Asset.cpp.ai.md create mode 100644 src/libxrpl/protocol/Book.cpp.ai.json create mode 100644 src/libxrpl/protocol/Book.cpp.ai.md create mode 100644 src/libxrpl/protocol/BuildInfo.cpp.ai.json create mode 100644 src/libxrpl/protocol/BuildInfo.cpp.ai.md create mode 100644 src/libxrpl/protocol/ErrorCodes.cpp.ai.json create mode 100644 src/libxrpl/protocol/ErrorCodes.cpp.ai.md create mode 100644 src/libxrpl/protocol/Feature.cpp.ai.json create mode 100644 src/libxrpl/protocol/Feature.cpp.ai.md create mode 100644 src/libxrpl/protocol/IOUAmount.cpp.ai.json create mode 100644 src/libxrpl/protocol/IOUAmount.cpp.ai.md create mode 100644 src/libxrpl/protocol/Indexes.cpp.ai.json create mode 100644 src/libxrpl/protocol/Indexes.cpp.ai.md create mode 100644 src/libxrpl/protocol/InnerObjectFormats.cpp.ai.json create mode 100644 src/libxrpl/protocol/InnerObjectFormats.cpp.ai.md create mode 100644 src/libxrpl/protocol/Issue.cpp.ai.json create mode 100644 src/libxrpl/protocol/Issue.cpp.ai.md create mode 100644 src/libxrpl/protocol/Keylet.cpp.ai.json create mode 100644 src/libxrpl/protocol/Keylet.cpp.ai.md create mode 100644 src/libxrpl/protocol/LedgerFormats.cpp.ai.json create mode 100644 src/libxrpl/protocol/LedgerFormats.cpp.ai.md create mode 100644 src/libxrpl/protocol/LedgerHeader.cpp.ai.json create mode 100644 src/libxrpl/protocol/LedgerHeader.cpp.ai.md create mode 100644 src/libxrpl/protocol/MPTAmount.cpp.ai.json create mode 100644 src/libxrpl/protocol/MPTAmount.cpp.ai.md create mode 100644 src/libxrpl/protocol/MPTIssue.cpp.ai.json create mode 100644 src/libxrpl/protocol/MPTIssue.cpp.ai.md create mode 100644 src/libxrpl/protocol/NFTSyntheticSerializer.cpp.ai.json create mode 100644 src/libxrpl/protocol/NFTSyntheticSerializer.cpp.ai.md create mode 100644 src/libxrpl/protocol/NFTokenID.cpp.ai.json create mode 100644 src/libxrpl/protocol/NFTokenID.cpp.ai.md create mode 100644 src/libxrpl/protocol/NFTokenOfferID.cpp.ai.json create mode 100644 src/libxrpl/protocol/NFTokenOfferID.cpp.ai.md create mode 100644 src/libxrpl/protocol/PathAsset.cpp.ai.json create mode 100644 src/libxrpl/protocol/PathAsset.cpp.ai.md create mode 100644 src/libxrpl/protocol/Permissions.cpp.ai.json create mode 100644 src/libxrpl/protocol/Permissions.cpp.ai.md create mode 100644 src/libxrpl/protocol/Protocol.cpp.ai.json create mode 100644 src/libxrpl/protocol/Protocol.cpp.ai.md create mode 100644 src/libxrpl/protocol/PublicKey.cpp.ai.json create mode 100644 src/libxrpl/protocol/PublicKey.cpp.ai.md create mode 100644 src/libxrpl/protocol/Quality.cpp.ai.json create mode 100644 src/libxrpl/protocol/Quality.cpp.ai.md create mode 100644 src/libxrpl/protocol/QualityFunction.cpp.ai.json create mode 100644 src/libxrpl/protocol/QualityFunction.cpp.ai.md create mode 100644 src/libxrpl/protocol/RPCErr.cpp.ai.json create mode 100644 src/libxrpl/protocol/RPCErr.cpp.ai.md create mode 100644 src/libxrpl/protocol/Rate2.cpp.ai.json create mode 100644 src/libxrpl/protocol/Rate2.cpp.ai.md create mode 100644 src/libxrpl/protocol/Rules.cpp.ai.json create mode 100644 src/libxrpl/protocol/Rules.cpp.ai.md create mode 100644 src/libxrpl/protocol/SField.cpp.ai.json create mode 100644 src/libxrpl/protocol/SField.cpp.ai.md create mode 100644 src/libxrpl/protocol/SOTemplate.cpp.ai.json create mode 100644 src/libxrpl/protocol/SOTemplate.cpp.ai.md create mode 100644 src/libxrpl/protocol/STAccount.cpp.ai.json create mode 100644 src/libxrpl/protocol/STAccount.cpp.ai.md create mode 100644 src/libxrpl/protocol/STAmount.cpp.ai.json create mode 100644 src/libxrpl/protocol/STAmount.cpp.ai.md create mode 100644 src/libxrpl/protocol/STArray.cpp.ai.json create mode 100644 src/libxrpl/protocol/STArray.cpp.ai.md create mode 100644 src/libxrpl/protocol/STBase.cpp.ai.json create mode 100644 src/libxrpl/protocol/STBase.cpp.ai.md create mode 100644 src/libxrpl/protocol/STBlob.cpp.ai.json create mode 100644 src/libxrpl/protocol/STBlob.cpp.ai.md create mode 100644 src/libxrpl/protocol/STCurrency.cpp.ai.json create mode 100644 src/libxrpl/protocol/STCurrency.cpp.ai.md create mode 100644 src/libxrpl/protocol/STInteger.cpp.ai.json create mode 100644 src/libxrpl/protocol/STInteger.cpp.ai.md create mode 100644 src/libxrpl/protocol/STIssue.cpp.ai.json create mode 100644 src/libxrpl/protocol/STIssue.cpp.ai.md create mode 100644 src/libxrpl/protocol/STLedgerEntry.cpp.ai.json create mode 100644 src/libxrpl/protocol/STLedgerEntry.cpp.ai.md create mode 100644 src/libxrpl/protocol/STNumber.cpp.ai.json create mode 100644 src/libxrpl/protocol/STNumber.cpp.ai.md create mode 100644 src/libxrpl/protocol/STObject.cpp.ai.json create mode 100644 src/libxrpl/protocol/STObject.cpp.ai.md create mode 100644 src/libxrpl/protocol/STParsedJSON.cpp.ai.json create mode 100644 src/libxrpl/protocol/STParsedJSON.cpp.ai.md create mode 100644 src/libxrpl/protocol/STPathSet.cpp.ai.json create mode 100644 src/libxrpl/protocol/STPathSet.cpp.ai.md create mode 100644 src/libxrpl/protocol/STTakesAsset.cpp.ai.json create mode 100644 src/libxrpl/protocol/STTakesAsset.cpp.ai.md create mode 100644 src/libxrpl/protocol/STTx.cpp.ai.json create mode 100644 src/libxrpl/protocol/STTx.cpp.ai.md create mode 100644 src/libxrpl/protocol/STValidation.cpp.ai.json create mode 100644 src/libxrpl/protocol/STValidation.cpp.ai.md create mode 100644 src/libxrpl/protocol/STVar.cpp.ai.json create mode 100644 src/libxrpl/protocol/STVar.cpp.ai.md create mode 100644 src/libxrpl/protocol/STVector256.cpp.ai.json create mode 100644 src/libxrpl/protocol/STVector256.cpp.ai.md create mode 100644 src/libxrpl/protocol/STXChainBridge.cpp.ai.json create mode 100644 src/libxrpl/protocol/STXChainBridge.cpp.ai.md create mode 100644 src/libxrpl/protocol/SecretKey.cpp.ai.json create mode 100644 src/libxrpl/protocol/SecretKey.cpp.ai.md create mode 100644 src/libxrpl/protocol/Seed.cpp.ai.json create mode 100644 src/libxrpl/protocol/Seed.cpp.ai.md create mode 100644 src/libxrpl/protocol/Serializer.cpp.ai.json create mode 100644 src/libxrpl/protocol/Serializer.cpp.ai.md create mode 100644 src/libxrpl/protocol/Sign.cpp.ai.json create mode 100644 src/libxrpl/protocol/Sign.cpp.ai.md create mode 100644 src/libxrpl/protocol/TER.cpp.ai.json create mode 100644 src/libxrpl/protocol/TER.cpp.ai.md create mode 100644 src/libxrpl/protocol/TxFormats.cpp.ai.json create mode 100644 src/libxrpl/protocol/TxFormats.cpp.ai.md create mode 100644 src/libxrpl/protocol/TxMeta.cpp.ai.json create mode 100644 src/libxrpl/protocol/TxMeta.cpp.ai.md create mode 100644 src/libxrpl/protocol/UintTypes.cpp.ai.json create mode 100644 src/libxrpl/protocol/UintTypes.cpp.ai.md create mode 100644 src/libxrpl/protocol/XChainAttestations.cpp.ai.json create mode 100644 src/libxrpl/protocol/XChainAttestations.cpp.ai.md create mode 100644 src/libxrpl/protocol/digest.cpp.ai.json create mode 100644 src/libxrpl/protocol/digest.cpp.ai.md create mode 100644 src/libxrpl/protocol/tokens.cpp.ai.json create mode 100644 src/libxrpl/protocol/tokens.cpp.ai.md create mode 100644 src/libxrpl/protocol_autogen/placeholder.cpp.ai.json create mode 100644 src/libxrpl/protocol_autogen/placeholder.cpp.ai.md create mode 100644 src/libxrpl/rdb/DatabaseCon.cpp.ai.json create mode 100644 src/libxrpl/rdb/DatabaseCon.cpp.ai.md create mode 100644 src/libxrpl/rdb/SociDB.cpp.ai.json create mode 100644 src/libxrpl/rdb/SociDB.cpp.ai.md create mode 100644 src/libxrpl/resource/Charge.cpp.ai.json create mode 100644 src/libxrpl/resource/Charge.cpp.ai.md create mode 100644 src/libxrpl/resource/Consumer.cpp.ai.json create mode 100644 src/libxrpl/resource/Consumer.cpp.ai.md create mode 100644 src/libxrpl/resource/Fees.cpp.ai.json create mode 100644 src/libxrpl/resource/Fees.cpp.ai.md create mode 100644 src/libxrpl/resource/ResourceManager.cpp.ai.json create mode 100644 src/libxrpl/resource/ResourceManager.cpp.ai.md create mode 100644 src/libxrpl/server/InfoSub.cpp.ai.json create mode 100644 src/libxrpl/server/InfoSub.cpp.ai.md create mode 100644 src/libxrpl/server/JSONRPCUtil.cpp.ai.json create mode 100644 src/libxrpl/server/JSONRPCUtil.cpp.ai.md create mode 100644 src/libxrpl/server/LoadFeeTrack.cpp.ai.json create mode 100644 src/libxrpl/server/LoadFeeTrack.cpp.ai.md create mode 100644 src/libxrpl/server/Manifest.cpp.ai.json create mode 100644 src/libxrpl/server/Manifest.cpp.ai.md create mode 100644 src/libxrpl/server/Port.cpp.ai.json create mode 100644 src/libxrpl/server/Port.cpp.ai.md create mode 100644 src/libxrpl/server/State.cpp.ai.json create mode 100644 src/libxrpl/server/State.cpp.ai.md create mode 100644 src/libxrpl/server/Vacuum.cpp.ai.json create mode 100644 src/libxrpl/server/Vacuum.cpp.ai.md create mode 100644 src/libxrpl/server/Wallet.cpp.ai.json create mode 100644 src/libxrpl/server/Wallet.cpp.ai.md create mode 100644 src/libxrpl/shamap/SHAMap.cpp.ai.json create mode 100644 src/libxrpl/shamap/SHAMap.cpp.ai.md create mode 100644 src/libxrpl/shamap/SHAMapDelta.cpp.ai.json create mode 100644 src/libxrpl/shamap/SHAMapDelta.cpp.ai.md create mode 100644 src/libxrpl/shamap/SHAMapInnerNode.cpp.ai.json create mode 100644 src/libxrpl/shamap/SHAMapInnerNode.cpp.ai.md create mode 100644 src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.json create mode 100644 src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.md create mode 100644 src/libxrpl/shamap/SHAMapNodeID.cpp.ai.json create mode 100644 src/libxrpl/shamap/SHAMapNodeID.cpp.ai.md create mode 100644 src/libxrpl/shamap/SHAMapSync.cpp.ai.json create mode 100644 src/libxrpl/shamap/SHAMapSync.cpp.ai.md create mode 100644 src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.json create mode 100644 src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.md create mode 100644 src/libxrpl/tx/ApplyContext.cpp.ai.json create mode 100644 src/libxrpl/tx/ApplyContext.cpp.ai.md create mode 100644 src/libxrpl/tx/SignerEntries.cpp.ai.json create mode 100644 src/libxrpl/tx/SignerEntries.cpp.ai.md create mode 100644 src/libxrpl/tx/Transactor.cpp.ai.json create mode 100644 src/libxrpl/tx/Transactor.cpp.ai.md create mode 100644 src/libxrpl/tx/apply.cpp.ai.json create mode 100644 src/libxrpl/tx/apply.cpp.ai.md create mode 100644 src/libxrpl/tx/applySteps.cpp.ai.json create mode 100644 src/libxrpl/tx/applySteps.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/AMMInvariant.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/AMMInvariant.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/FreezeInvariant.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/FreezeInvariant.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/InvariantCheck.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/InvariantCheck.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/LoanInvariant.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/LoanInvariant.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/MPTInvariant.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/MPTInvariant.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/NFTInvariant.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/NFTInvariant.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp.ai.md create mode 100644 src/libxrpl/tx/invariants/VaultInvariant.cpp.ai.json create mode 100644 src/libxrpl/tx/invariants/VaultInvariant.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/AMMOffer.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/AMMOffer.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/BookStep.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/BookStep.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/BookTip.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/BookTip.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/DirectStep.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/DirectStep.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/Flow.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/Flow.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/MPTEndpointStep.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/MPTEndpointStep.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/OfferStream.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/OfferStream.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/PaySteps.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/PaySteps.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/RippleCalc.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/RippleCalc.cpp.ai.md create mode 100644 src/libxrpl/tx/paths/XRPEndpointStep.cpp.ai.json create mode 100644 src/libxrpl/tx/paths/XRPEndpointStep.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/account/AccountDelete.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/account/AccountDelete.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/account/AccountSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/account/AccountSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/account/SetRegularKey.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/account/SetRegularKey.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/account/SignerListSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/account/SignerListSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/bridge/XChainBridge.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/bridge/XChainBridge.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/check/CheckCancel.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/check/CheckCancel.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/check/CheckCash.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/check/CheckCash.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/check/CheckCreate.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/check/CheckCreate.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/delegate/DelegateSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/delegate/DelegateSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/dex/AMMBid.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/dex/AMMBid.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/dex/AMMClawback.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/dex/AMMClawback.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/dex/AMMCreate.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/dex/AMMCreate.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/dex/AMMDelete.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/dex/AMMDelete.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/dex/AMMDeposit.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/dex/AMMDeposit.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/dex/AMMVote.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/dex/AMMVote.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/dex/OfferCancel.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/dex/OfferCancel.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/dex/OfferCreate.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/dex/OfferCreate.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/did/DIDDelete.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/did/DIDDelete.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/did/DIDSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/did/DIDSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/escrow/Escrow.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/escrow/Escrow.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LendingHelpers.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LendingHelpers.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LoanDelete.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LoanDelete.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LoanManage.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LoanManage.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LoanPay.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LoanPay.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/lending/LoanSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/lending/LoanSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenMint.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenMint.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenModify.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/nft/NFTokenModify.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/oracle/OracleDelete.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/oracle/OracleDelete.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/oracle/OracleSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/oracle/OracleSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/payment/DepositPreauth.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/payment/DepositPreauth.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/payment/Payment.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/payment/Payment.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/system/Batch.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/system/Batch.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/system/Change.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/system/Change.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/system/LedgerStateFix.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/system/LedgerStateFix.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/system/TicketCreate.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/system/TicketCreate.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/token/Clawback.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/token/Clawback.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/token/TrustSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/token/TrustSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/vault/VaultClawback.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/vault/VaultClawback.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/vault/VaultCreate.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/vault/VaultCreate.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/vault/VaultDelete.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/vault/VaultDelete.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/vault/VaultDeposit.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/vault/VaultDeposit.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/vault/VaultSet.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/vault/VaultSet.cpp.ai.md create mode 100644 src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp.ai.json create mode 100644 src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp.ai.md create mode 100644 src/xrpld/app/consensus/RCLCensorshipDetector.h.ai.json create mode 100644 src/xrpld/app/consensus/RCLCensorshipDetector.h.ai.md create mode 100644 src/xrpld/app/consensus/RCLConsensus.cpp.ai.json create mode 100644 src/xrpld/app/consensus/RCLConsensus.cpp.ai.md create mode 100644 src/xrpld/app/consensus/RCLConsensus.h.ai.json create mode 100644 src/xrpld/app/consensus/RCLConsensus.h.ai.md create mode 100644 src/xrpld/app/consensus/RCLCxLedger.h.ai.json create mode 100644 src/xrpld/app/consensus/RCLCxLedger.h.ai.md create mode 100644 src/xrpld/app/consensus/RCLCxPeerPos.cpp.ai.json create mode 100644 src/xrpld/app/consensus/RCLCxPeerPos.cpp.ai.md create mode 100644 src/xrpld/app/consensus/RCLCxPeerPos.h.ai.json create mode 100644 src/xrpld/app/consensus/RCLCxPeerPos.h.ai.md create mode 100644 src/xrpld/app/consensus/RCLCxTx.h.ai.json create mode 100644 src/xrpld/app/consensus/RCLCxTx.h.ai.md create mode 100644 src/xrpld/app/consensus/RCLValidations.cpp.ai.json create mode 100644 src/xrpld/app/consensus/RCLValidations.cpp.ai.md create mode 100644 src/xrpld/app/consensus/RCLValidations.h.ai.json create mode 100644 src/xrpld/app/consensus/RCLValidations.h.ai.md create mode 100644 src/xrpld/app/ledger/AbstractFetchPackContainer.h.ai.json create mode 100644 src/xrpld/app/ledger/AbstractFetchPackContainer.h.ai.md create mode 100644 src/xrpld/app/ledger/AcceptedLedger.cpp.ai.json create mode 100644 src/xrpld/app/ledger/AcceptedLedger.cpp.ai.md create mode 100644 src/xrpld/app/ledger/AcceptedLedger.h.ai.json create mode 100644 src/xrpld/app/ledger/AcceptedLedger.h.ai.md create mode 100644 src/xrpld/app/ledger/AccountStateSF.cpp.ai.json create mode 100644 src/xrpld/app/ledger/AccountStateSF.cpp.ai.md create mode 100644 src/xrpld/app/ledger/AccountStateSF.h.ai.json create mode 100644 src/xrpld/app/ledger/AccountStateSF.h.ai.md create mode 100644 src/xrpld/app/ledger/BuildLedger.h.ai.json create mode 100644 src/xrpld/app/ledger/BuildLedger.h.ai.md create mode 100644 src/xrpld/app/ledger/ConsensusTransSetSF.cpp.ai.json create mode 100644 src/xrpld/app/ledger/ConsensusTransSetSF.cpp.ai.md create mode 100644 src/xrpld/app/ledger/ConsensusTransSetSF.h.ai.json create mode 100644 src/xrpld/app/ledger/ConsensusTransSetSF.h.ai.md create mode 100644 src/xrpld/app/ledger/InboundLedger.h.ai.json create mode 100644 src/xrpld/app/ledger/InboundLedger.h.ai.md create mode 100644 src/xrpld/app/ledger/InboundLedgers.h.ai.json create mode 100644 src/xrpld/app/ledger/InboundLedgers.h.ai.md create mode 100644 src/xrpld/app/ledger/InboundTransactions.h.ai.json create mode 100644 src/xrpld/app/ledger/InboundTransactions.h.ai.md create mode 100644 src/xrpld/app/ledger/LedgerCleaner.h.ai.json create mode 100644 src/xrpld/app/ledger/LedgerCleaner.h.ai.md create mode 100644 src/xrpld/app/ledger/LedgerHistory.cpp.ai.json create mode 100644 src/xrpld/app/ledger/LedgerHistory.cpp.ai.md create mode 100644 src/xrpld/app/ledger/LedgerHistory.h.ai.json create mode 100644 src/xrpld/app/ledger/LedgerHistory.h.ai.md create mode 100644 src/xrpld/app/ledger/LedgerHolder.h.ai.json create mode 100644 src/xrpld/app/ledger/LedgerHolder.h.ai.md create mode 100644 src/xrpld/app/ledger/LedgerMaster.h.ai.json create mode 100644 src/xrpld/app/ledger/LedgerMaster.h.ai.md create mode 100644 src/xrpld/app/ledger/LedgerPersistence.h.ai.json create mode 100644 src/xrpld/app/ledger/LedgerPersistence.h.ai.md create mode 100644 src/xrpld/app/ledger/LedgerReplay.h.ai.json create mode 100644 src/xrpld/app/ledger/LedgerReplay.h.ai.md create mode 100644 src/xrpld/app/ledger/LedgerReplayTask.h.ai.json create mode 100644 src/xrpld/app/ledger/LedgerReplayTask.h.ai.md create mode 100644 src/xrpld/app/ledger/LedgerReplayer.h.ai.json create mode 100644 src/xrpld/app/ledger/LedgerReplayer.h.ai.md create mode 100644 src/xrpld/app/ledger/LedgerToJson.h.ai.json create mode 100644 src/xrpld/app/ledger/LedgerToJson.h.ai.md create mode 100644 src/xrpld/app/ledger/LocalTxs.h.ai.json create mode 100644 src/xrpld/app/ledger/LocalTxs.h.ai.md create mode 100644 src/xrpld/app/ledger/OpenLedger.h.ai.json create mode 100644 src/xrpld/app/ledger/OpenLedger.h.ai.md create mode 100644 src/xrpld/app/ledger/OrderBookDB.h.ai.json create mode 100644 src/xrpld/app/ledger/OrderBookDB.h.ai.md create mode 100644 src/xrpld/app/ledger/OrderBookDBImpl.cpp.ai.json create mode 100644 src/xrpld/app/ledger/OrderBookDBImpl.cpp.ai.md create mode 100644 src/xrpld/app/ledger/OrderBookDBImpl.h.ai.json create mode 100644 src/xrpld/app/ledger/OrderBookDBImpl.h.ai.md create mode 100644 src/xrpld/app/ledger/TransactionMaster.h.ai.json create mode 100644 src/xrpld/app/ledger/TransactionMaster.h.ai.md create mode 100644 src/xrpld/app/ledger/TransactionStateSF.cpp.ai.json create mode 100644 src/xrpld/app/ledger/TransactionStateSF.cpp.ai.md create mode 100644 src/xrpld/app/ledger/TransactionStateSF.h.ai.json create mode 100644 src/xrpld/app/ledger/TransactionStateSF.h.ai.md create mode 100644 src/xrpld/app/ledger/detail/BuildLedger.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/BuildLedger.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/InboundLedger.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/InboundLedger.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/InboundLedgers.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/InboundLedgers.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/InboundTransactions.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/InboundTransactions.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerCleaner.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerCleaner.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerMaster.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerMaster.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerPersistence.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerPersistence.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerReplay.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerReplay.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerReplayTask.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerReplayTask.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerReplayer.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerReplayer.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LedgerToJson.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LedgerToJson.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/LocalTxs.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/LocalTxs.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/OpenLedger.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/OpenLedger.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/SkipListAcquire.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/SkipListAcquire.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/SkipListAcquire.h.ai.json create mode 100644 src/xrpld/app/ledger/detail/SkipListAcquire.h.ai.md create mode 100644 src/xrpld/app/ledger/detail/TimeoutCounter.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/TimeoutCounter.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/TimeoutCounter.h.ai.json create mode 100644 src/xrpld/app/ledger/detail/TimeoutCounter.h.ai.md create mode 100644 src/xrpld/app/ledger/detail/TransactionAcquire.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/TransactionAcquire.cpp.ai.md create mode 100644 src/xrpld/app/ledger/detail/TransactionAcquire.h.ai.json create mode 100644 src/xrpld/app/ledger/detail/TransactionAcquire.h.ai.md create mode 100644 src/xrpld/app/ledger/detail/TransactionMaster.cpp.ai.json create mode 100644 src/xrpld/app/ledger/detail/TransactionMaster.cpp.ai.md create mode 100644 src/xrpld/app/main/Application.cpp.ai.json create mode 100644 src/xrpld/app/main/Application.cpp.ai.md create mode 100644 src/xrpld/app/main/Application.h.ai.json create mode 100644 src/xrpld/app/main/Application.h.ai.md create mode 100644 src/xrpld/app/main/BasicApp.cpp.ai.json create mode 100644 src/xrpld/app/main/BasicApp.cpp.ai.md create mode 100644 src/xrpld/app/main/BasicApp.h.ai.json create mode 100644 src/xrpld/app/main/BasicApp.h.ai.md create mode 100644 src/xrpld/app/main/CollectorManager.cpp.ai.json create mode 100644 src/xrpld/app/main/CollectorManager.cpp.ai.md create mode 100644 src/xrpld/app/main/CollectorManager.h.ai.json create mode 100644 src/xrpld/app/main/CollectorManager.h.ai.md create mode 100644 src/xrpld/app/main/GRPCServer.cpp.ai.json create mode 100644 src/xrpld/app/main/GRPCServer.cpp.ai.md create mode 100644 src/xrpld/app/main/GRPCServer.h.ai.json create mode 100644 src/xrpld/app/main/GRPCServer.h.ai.md create mode 100644 src/xrpld/app/main/LoadManager.cpp.ai.json create mode 100644 src/xrpld/app/main/LoadManager.cpp.ai.md create mode 100644 src/xrpld/app/main/LoadManager.h.ai.json create mode 100644 src/xrpld/app/main/LoadManager.h.ai.md create mode 100644 src/xrpld/app/main/Main.cpp.ai.json create mode 100644 src/xrpld/app/main/Main.cpp.ai.md create mode 100644 src/xrpld/app/main/NodeIdentity.cpp.ai.json create mode 100644 src/xrpld/app/main/NodeIdentity.cpp.ai.md create mode 100644 src/xrpld/app/main/NodeIdentity.h.ai.json create mode 100644 src/xrpld/app/main/NodeIdentity.h.ai.md create mode 100644 src/xrpld/app/main/NodeStoreScheduler.cpp.ai.json create mode 100644 src/xrpld/app/main/NodeStoreScheduler.cpp.ai.md create mode 100644 src/xrpld/app/main/NodeStoreScheduler.h.ai.json create mode 100644 src/xrpld/app/main/NodeStoreScheduler.h.ai.md create mode 100644 src/xrpld/app/main/Tuning.h.ai.json create mode 100644 src/xrpld/app/main/Tuning.h.ai.md create mode 100644 src/xrpld/app/misc/AmendmentTableImpl.h.ai.json create mode 100644 src/xrpld/app/misc/AmendmentTableImpl.h.ai.md create mode 100644 src/xrpld/app/misc/DeliverMax.h.ai.json create mode 100644 src/xrpld/app/misc/DeliverMax.h.ai.md create mode 100644 src/xrpld/app/misc/FeeVote.h.ai.json create mode 100644 src/xrpld/app/misc/FeeVote.h.ai.md create mode 100644 src/xrpld/app/misc/FeeVoteImpl.cpp.ai.json create mode 100644 src/xrpld/app/misc/FeeVoteImpl.cpp.ai.md create mode 100644 src/xrpld/app/misc/NegativeUNLVote.cpp.ai.json create mode 100644 src/xrpld/app/misc/NegativeUNLVote.cpp.ai.md create mode 100644 src/xrpld/app/misc/NegativeUNLVote.h.ai.json create mode 100644 src/xrpld/app/misc/NegativeUNLVote.h.ai.md create mode 100644 src/xrpld/app/misc/NetworkOPs.cpp.ai.json create mode 100644 src/xrpld/app/misc/NetworkOPs.cpp.ai.md create mode 100644 src/xrpld/app/misc/SHAMapStore.h.ai.json create mode 100644 src/xrpld/app/misc/SHAMapStore.h.ai.md create mode 100644 src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.json create mode 100644 src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.md create mode 100644 src/xrpld/app/misc/SHAMapStoreImp.h.ai.json create mode 100644 src/xrpld/app/misc/SHAMapStoreImp.h.ai.md create mode 100644 src/xrpld/app/misc/Transaction.h.ai.json create mode 100644 src/xrpld/app/misc/Transaction.h.ai.md create mode 100644 src/xrpld/app/misc/TxQ.h.ai.json create mode 100644 src/xrpld/app/misc/TxQ.h.ai.md create mode 100644 src/xrpld/app/misc/ValidatorKeys.h.ai.json create mode 100644 src/xrpld/app/misc/ValidatorKeys.h.ai.md create mode 100644 src/xrpld/app/misc/ValidatorList.h.ai.json create mode 100644 src/xrpld/app/misc/ValidatorList.h.ai.md create mode 100644 src/xrpld/app/misc/ValidatorSite.h.ai.json create mode 100644 src/xrpld/app/misc/ValidatorSite.h.ai.md create mode 100644 src/xrpld/app/misc/detail/AccountTxPaging.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/AccountTxPaging.cpp.ai.md create mode 100644 src/xrpld/app/misc/detail/AccountTxPaging.h.ai.json create mode 100644 src/xrpld/app/misc/detail/AccountTxPaging.h.ai.md create mode 100644 src/xrpld/app/misc/detail/AmendmentTable.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/AmendmentTable.cpp.ai.md create mode 100644 src/xrpld/app/misc/detail/DeliverMax.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/DeliverMax.cpp.ai.md create mode 100644 src/xrpld/app/misc/detail/Transaction.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/Transaction.cpp.ai.md create mode 100644 src/xrpld/app/misc/detail/TxQ.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/TxQ.cpp.ai.md create mode 100644 src/xrpld/app/misc/detail/ValidatorKeys.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/ValidatorKeys.cpp.ai.md create mode 100644 src/xrpld/app/misc/detail/ValidatorList.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/ValidatorList.cpp.ai.md create mode 100644 src/xrpld/app/misc/detail/ValidatorSite.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/ValidatorSite.cpp.ai.md create mode 100644 src/xrpld/app/misc/detail/Work.h.ai.json create mode 100644 src/xrpld/app/misc/detail/Work.h.ai.md create mode 100644 src/xrpld/app/misc/detail/WorkBase.h.ai.json create mode 100644 src/xrpld/app/misc/detail/WorkBase.h.ai.md create mode 100644 src/xrpld/app/misc/detail/WorkFile.h.ai.json create mode 100644 src/xrpld/app/misc/detail/WorkFile.h.ai.md create mode 100644 src/xrpld/app/misc/detail/WorkPlain.h.ai.json create mode 100644 src/xrpld/app/misc/detail/WorkPlain.h.ai.md create mode 100644 src/xrpld/app/misc/detail/WorkSSL.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/WorkSSL.cpp.ai.md create mode 100644 src/xrpld/app/misc/detail/WorkSSL.h.ai.json create mode 100644 src/xrpld/app/misc/detail/WorkSSL.h.ai.md create mode 100644 src/xrpld/app/misc/detail/setup_HashRouter.cpp.ai.json create mode 100644 src/xrpld/app/misc/detail/setup_HashRouter.cpp.ai.md create mode 100644 src/xrpld/app/misc/make_NetworkOPs.h.ai.json create mode 100644 src/xrpld/app/misc/make_NetworkOPs.h.ai.md create mode 100644 src/xrpld/app/misc/setup_HashRouter.h.ai.json create mode 100644 src/xrpld/app/misc/setup_HashRouter.h.ai.md create mode 100644 src/xrpld/app/rdb/PeerFinder.h.ai.json create mode 100644 src/xrpld/app/rdb/PeerFinder.h.ai.md create mode 100644 src/xrpld/app/rdb/backend/SQLiteDatabase.h.ai.json create mode 100644 src/xrpld/app/rdb/backend/SQLiteDatabase.h.ai.md create mode 100644 src/xrpld/app/rdb/backend/detail/Node.cpp.ai.json create mode 100644 src/xrpld/app/rdb/backend/detail/Node.cpp.ai.md create mode 100644 src/xrpld/app/rdb/backend/detail/Node.h.ai.json create mode 100644 src/xrpld/app/rdb/backend/detail/Node.h.ai.md create mode 100644 src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp.ai.json create mode 100644 src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp.ai.md create mode 100644 src/xrpld/app/rdb/detail/PeerFinder.cpp.ai.json create mode 100644 src/xrpld/app/rdb/detail/PeerFinder.cpp.ai.md create mode 100644 src/xrpld/app/tx/detail/Taker.h.ai.json create mode 100644 src/xrpld/app/tx/detail/Taker.h.ai.md create mode 100644 src/xrpld/consensus/Consensus.cpp.ai.json create mode 100644 src/xrpld/consensus/Consensus.cpp.ai.md create mode 100644 src/xrpld/consensus/Consensus.h.ai.json create mode 100644 src/xrpld/consensus/Consensus.h.ai.md create mode 100644 src/xrpld/consensus/ConsensusParms.h.ai.json create mode 100644 src/xrpld/consensus/ConsensusParms.h.ai.md create mode 100644 src/xrpld/consensus/ConsensusProposal.h.ai.json create mode 100644 src/xrpld/consensus/ConsensusProposal.h.ai.md create mode 100644 src/xrpld/consensus/ConsensusTypes.h.ai.json create mode 100644 src/xrpld/consensus/ConsensusTypes.h.ai.md create mode 100644 src/xrpld/consensus/DisputedTx.h.ai.json create mode 100644 src/xrpld/consensus/DisputedTx.h.ai.md create mode 100644 src/xrpld/consensus/LedgerTrie.h.ai.json create mode 100644 src/xrpld/consensus/LedgerTrie.h.ai.md create mode 100644 src/xrpld/consensus/Validations.h.ai.json create mode 100644 src/xrpld/consensus/Validations.h.ai.md create mode 100644 src/xrpld/core/Config.h.ai.json create mode 100644 src/xrpld/core/Config.h.ai.md create mode 100644 src/xrpld/core/ConfigSections.h.ai.json create mode 100644 src/xrpld/core/ConfigSections.h.ai.md create mode 100644 src/xrpld/core/NetworkIDServiceImpl.h.ai.json create mode 100644 src/xrpld/core/NetworkIDServiceImpl.h.ai.md create mode 100644 src/xrpld/core/TimeKeeper.h.ai.json create mode 100644 src/xrpld/core/TimeKeeper.h.ai.md create mode 100644 src/xrpld/core/detail/Config.cpp.ai.json create mode 100644 src/xrpld/core/detail/Config.cpp.ai.md create mode 100644 src/xrpld/core/detail/NetworkIDServiceImpl.cpp.ai.json create mode 100644 src/xrpld/core/detail/NetworkIDServiceImpl.cpp.ai.md create mode 100644 src/xrpld/overlay/Cluster.h.ai.json create mode 100644 src/xrpld/overlay/Cluster.h.ai.md create mode 100644 src/xrpld/overlay/ClusterNode.h.ai.json create mode 100644 src/xrpld/overlay/ClusterNode.h.ai.md create mode 100644 src/xrpld/overlay/Compression.h.ai.json create mode 100644 src/xrpld/overlay/Compression.h.ai.md create mode 100644 src/xrpld/overlay/Message.h.ai.json create mode 100644 src/xrpld/overlay/Message.h.ai.md create mode 100644 src/xrpld/overlay/Overlay.h.ai.json create mode 100644 src/xrpld/overlay/Overlay.h.ai.md create mode 100644 src/xrpld/overlay/Peer.h.ai.json create mode 100644 src/xrpld/overlay/Peer.h.ai.md create mode 100644 src/xrpld/overlay/PeerSet.h.ai.json create mode 100644 src/xrpld/overlay/PeerSet.h.ai.md create mode 100644 src/xrpld/overlay/ReduceRelayCommon.h.ai.json create mode 100644 src/xrpld/overlay/ReduceRelayCommon.h.ai.md create mode 100644 src/xrpld/overlay/Slot.h.ai.json create mode 100644 src/xrpld/overlay/Slot.h.ai.md create mode 100644 src/xrpld/overlay/Squelch.h.ai.json create mode 100644 src/xrpld/overlay/Squelch.h.ai.md create mode 100644 src/xrpld/overlay/detail/Cluster.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/Cluster.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/ConnectAttempt.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/ConnectAttempt.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/ConnectAttempt.h.ai.json create mode 100644 src/xrpld/overlay/detail/ConnectAttempt.h.ai.md create mode 100644 src/xrpld/overlay/detail/Handshake.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/Handshake.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/Handshake.h.ai.json create mode 100644 src/xrpld/overlay/detail/Handshake.h.ai.md create mode 100644 src/xrpld/overlay/detail/Message.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/Message.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/OverlayImpl.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/OverlayImpl.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/OverlayImpl.h.ai.json create mode 100644 src/xrpld/overlay/detail/OverlayImpl.h.ai.md create mode 100644 src/xrpld/overlay/detail/PeerImp.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/PeerImp.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/PeerImp.h.ai.json create mode 100644 src/xrpld/overlay/detail/PeerImp.h.ai.md create mode 100644 src/xrpld/overlay/detail/PeerReservationTable.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/PeerReservationTable.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/PeerSet.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/PeerSet.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/ProtocolMessage.h.ai.json create mode 100644 src/xrpld/overlay/detail/ProtocolMessage.h.ai.md create mode 100644 src/xrpld/overlay/detail/ProtocolVersion.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/ProtocolVersion.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/ProtocolVersion.h.ai.json create mode 100644 src/xrpld/overlay/detail/ProtocolVersion.h.ai.md create mode 100644 src/xrpld/overlay/detail/TrafficCount.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/TrafficCount.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/TrafficCount.h.ai.json create mode 100644 src/xrpld/overlay/detail/TrafficCount.h.ai.md create mode 100644 src/xrpld/overlay/detail/Tuning.h.ai.json create mode 100644 src/xrpld/overlay/detail/Tuning.h.ai.md create mode 100644 src/xrpld/overlay/detail/TxMetrics.cpp.ai.json create mode 100644 src/xrpld/overlay/detail/TxMetrics.cpp.ai.md create mode 100644 src/xrpld/overlay/detail/TxMetrics.h.ai.json create mode 100644 src/xrpld/overlay/detail/TxMetrics.h.ai.md create mode 100644 src/xrpld/overlay/detail/ZeroCopyStream.h.ai.json create mode 100644 src/xrpld/overlay/detail/ZeroCopyStream.h.ai.md create mode 100644 src/xrpld/overlay/make_Overlay.h.ai.json create mode 100644 src/xrpld/overlay/make_Overlay.h.ai.md create mode 100644 src/xrpld/overlay/predicates.h.ai.json create mode 100644 src/xrpld/overlay/predicates.h.ai.md create mode 100644 src/xrpld/peerfinder/PeerfinderManager.h.ai.json create mode 100644 src/xrpld/peerfinder/PeerfinderManager.h.ai.md create mode 100644 src/xrpld/peerfinder/Slot.h.ai.json create mode 100644 src/xrpld/peerfinder/Slot.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Bootcache.cpp.ai.json create mode 100644 src/xrpld/peerfinder/detail/Bootcache.cpp.ai.md create mode 100644 src/xrpld/peerfinder/detail/Bootcache.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Bootcache.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Checker.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Checker.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Counts.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Counts.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Endpoint.cpp.ai.json create mode 100644 src/xrpld/peerfinder/detail/Endpoint.cpp.ai.md create mode 100644 src/xrpld/peerfinder/detail/Fixed.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Fixed.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Handouts.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Handouts.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Livecache.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Livecache.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Logic.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Logic.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/PeerfinderConfig.cpp.ai.json create mode 100644 src/xrpld/peerfinder/detail/PeerfinderConfig.cpp.ai.md create mode 100644 src/xrpld/peerfinder/detail/PeerfinderManager.cpp.ai.json create mode 100644 src/xrpld/peerfinder/detail/PeerfinderManager.cpp.ai.md create mode 100644 src/xrpld/peerfinder/detail/SlotImp.cpp.ai.json create mode 100644 src/xrpld/peerfinder/detail/SlotImp.cpp.ai.md create mode 100644 src/xrpld/peerfinder/detail/SlotImp.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/SlotImp.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Source.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Source.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/SourceStrings.cpp.ai.json create mode 100644 src/xrpld/peerfinder/detail/SourceStrings.cpp.ai.md create mode 100644 src/xrpld/peerfinder/detail/SourceStrings.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/SourceStrings.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Store.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Store.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/StoreSqdb.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/StoreSqdb.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/Tuning.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/Tuning.h.ai.md create mode 100644 src/xrpld/peerfinder/detail/iosformat.h.ai.json create mode 100644 src/xrpld/peerfinder/detail/iosformat.h.ai.md create mode 100644 src/xrpld/peerfinder/make_Manager.h.ai.json create mode 100644 src/xrpld/peerfinder/make_Manager.h.ai.md create mode 100644 src/xrpld/perflog/detail/PerfLogImp.cpp.ai.json create mode 100644 src/xrpld/perflog/detail/PerfLogImp.cpp.ai.md create mode 100644 src/xrpld/perflog/detail/PerfLogImp.h.ai.json create mode 100644 src/xrpld/perflog/detail/PerfLogImp.h.ai.md create mode 100644 src/xrpld/rpc/BookChanges.h.ai.json create mode 100644 src/xrpld/rpc/BookChanges.h.ai.md create mode 100644 src/xrpld/rpc/CTID.h.ai.json create mode 100644 src/xrpld/rpc/CTID.h.ai.md create mode 100644 src/xrpld/rpc/Context.h.ai.json create mode 100644 src/xrpld/rpc/Context.h.ai.md create mode 100644 src/xrpld/rpc/DeliveredAmount.h.ai.json create mode 100644 src/xrpld/rpc/DeliveredAmount.h.ai.md create mode 100644 src/xrpld/rpc/GRPCHandlers.h.ai.json create mode 100644 src/xrpld/rpc/GRPCHandlers.h.ai.md create mode 100644 src/xrpld/rpc/MPTokenIssuanceID.h.ai.json create mode 100644 src/xrpld/rpc/MPTokenIssuanceID.h.ai.md create mode 100644 src/xrpld/rpc/Output.h.ai.json create mode 100644 src/xrpld/rpc/Output.h.ai.md create mode 100644 src/xrpld/rpc/RPCCall.h.ai.json create mode 100644 src/xrpld/rpc/RPCCall.h.ai.md create mode 100644 src/xrpld/rpc/RPCHandler.h.ai.json create mode 100644 src/xrpld/rpc/RPCHandler.h.ai.md create mode 100644 src/xrpld/rpc/RPCSub.h.ai.json create mode 100644 src/xrpld/rpc/RPCSub.h.ai.md create mode 100644 src/xrpld/rpc/Request.h.ai.json create mode 100644 src/xrpld/rpc/Request.h.ai.md create mode 100644 src/xrpld/rpc/Role.h.ai.json create mode 100644 src/xrpld/rpc/Role.h.ai.md create mode 100644 src/xrpld/rpc/ServerHandler.h.ai.json create mode 100644 src/xrpld/rpc/ServerHandler.h.ai.md create mode 100644 src/xrpld/rpc/Status.h.ai.json create mode 100644 src/xrpld/rpc/Status.h.ai.md create mode 100644 src/xrpld/rpc/detail/AccountAssets.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/AccountAssets.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/AccountAssets.h.ai.json create mode 100644 src/xrpld/rpc/detail/AccountAssets.h.ai.md create mode 100644 src/xrpld/rpc/detail/AssetCache.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/AssetCache.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/AssetCache.h.ai.json create mode 100644 src/xrpld/rpc/detail/AssetCache.h.ai.md create mode 100644 src/xrpld/rpc/detail/DeliveredAmount.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/DeliveredAmount.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/Handler.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/Handler.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/Handler.h.ai.json create mode 100644 src/xrpld/rpc/detail/Handler.h.ai.md create mode 100644 src/xrpld/rpc/detail/LegacyPathFind.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/LegacyPathFind.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/LegacyPathFind.h.ai.json create mode 100644 src/xrpld/rpc/detail/LegacyPathFind.h.ai.md create mode 100644 src/xrpld/rpc/detail/MPT.h.ai.json create mode 100644 src/xrpld/rpc/detail/MPT.h.ai.md create mode 100644 src/xrpld/rpc/detail/MPTokenIssuanceID.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/MPTokenIssuanceID.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/PathRequest.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/PathRequest.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/PathRequest.h.ai.json create mode 100644 src/xrpld/rpc/detail/PathRequest.h.ai.md create mode 100644 src/xrpld/rpc/detail/PathRequestManager.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/PathRequestManager.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/PathRequestManager.h.ai.json create mode 100644 src/xrpld/rpc/detail/PathRequestManager.h.ai.md create mode 100644 src/xrpld/rpc/detail/Pathfinder.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/Pathfinder.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/Pathfinder.h.ai.json create mode 100644 src/xrpld/rpc/detail/Pathfinder.h.ai.md create mode 100644 src/xrpld/rpc/detail/PathfinderUtils.h.ai.json create mode 100644 src/xrpld/rpc/detail/PathfinderUtils.h.ai.md create mode 100644 src/xrpld/rpc/detail/RPCCall.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/RPCCall.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/RPCHandler.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/RPCHandler.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/RPCHelpers.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/RPCHelpers.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/RPCHelpers.h.ai.json create mode 100644 src/xrpld/rpc/detail/RPCHelpers.h.ai.md create mode 100644 src/xrpld/rpc/detail/RPCLedgerHelpers.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/RPCLedgerHelpers.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/RPCLedgerHelpers.h.ai.json create mode 100644 src/xrpld/rpc/detail/RPCLedgerHelpers.h.ai.md create mode 100644 src/xrpld/rpc/detail/RPCSub.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/RPCSub.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/RippleLineCache.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/RippleLineCache.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/RippleLineCache.h.ai.json create mode 100644 src/xrpld/rpc/detail/RippleLineCache.h.ai.md create mode 100644 src/xrpld/rpc/detail/Role.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/Role.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/ServerHandler.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/ServerHandler.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/Status.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/Status.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/TransactionSign.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/TransactionSign.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/TransactionSign.h.ai.json create mode 100644 src/xrpld/rpc/detail/TransactionSign.h.ai.md create mode 100644 src/xrpld/rpc/detail/TrustLine.cpp.ai.json create mode 100644 src/xrpld/rpc/detail/TrustLine.cpp.ai.md create mode 100644 src/xrpld/rpc/detail/TrustLine.h.ai.json create mode 100644 src/xrpld/rpc/detail/TrustLine.h.ai.md create mode 100644 src/xrpld/rpc/detail/Tuning.h.ai.json create mode 100644 src/xrpld/rpc/detail/Tuning.h.ai.md create mode 100644 src/xrpld/rpc/detail/WSInfoSub.h.ai.json create mode 100644 src/xrpld/rpc/detail/WSInfoSub.h.ai.md create mode 100644 src/xrpld/rpc/handlers/ChannelVerify.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/ChannelVerify.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/Handlers.h.ai.json create mode 100644 src/xrpld/rpc/handlers/Handlers.h.ai.md create mode 100644 src/xrpld/rpc/handlers/VaultInfo.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/VaultInfo.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/AccountChannels.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/AccountChannels.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/AccountCurrencies.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/AccountCurrencies.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/AccountInfo.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/AccountInfo.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/AccountLines.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/AccountLines.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/AccountNFTs.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/AccountNFTs.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/AccountObjects.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/AccountObjects.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/AccountOffers.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/AccountOffers.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/AccountTx.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/AccountTx.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/GatewayBalances.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/GatewayBalances.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/NoRippleCheck.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/NoRippleCheck.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/account/OwnerInfo.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/account/OwnerInfo.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/BlackList.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/BlackList.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/UnlList.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/UnlList.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/data/CanDelete.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/data/CanDelete.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/keygen/WalletPropose.h.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/keygen/WalletPropose.h.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/log/LogLevel.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/log/LogLevel.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/log/LogRotate.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/log/LogRotate.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/peer/Connect.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/peer/Connect.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/peer/Peers.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/peer/Peers.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/server_control/Stop.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/server_control/Stop.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/signing/Sign.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/signing/Sign.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/signing/SignFor.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/signing/SignFor.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/status/GetCounts.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/status/GetCounts.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/status/GetCounts.h.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/status/GetCounts.h.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/status/Print.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/status/Print.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/admin/status/Validators.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/admin/status/Validators.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/ledger/Ledger.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/ledger/Ledger.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/ledger/Ledger.h.ai.json create mode 100644 src/xrpld/rpc/handlers/ledger/Ledger.h.ai.md create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerData.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerData.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h.ai.json create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h.ai.md create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/BookChanges.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/BookChanges.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/BookOffers.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/BookOffers.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/PathFind.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/PathFind.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/server_info/Feature.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/server_info/Feature.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/server_info/Fee.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/server_info/Fee.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/server_info/Manifest.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/server_info/Manifest.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/server_info/ServerInfo.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/server_info/ServerInfo.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/server_info/ServerState.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/server_info/ServerState.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/server_info/Version.h.ai.json create mode 100644 src/xrpld/rpc/handlers/server_info/Version.h.ai.md create mode 100644 src/xrpld/rpc/handlers/subscribe/Subscribe.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/subscribe/Subscribe.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/transaction/Simulate.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/transaction/Simulate.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/transaction/Submit.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/transaction/Submit.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/transaction/Tx.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/transaction/Tx.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/transaction/TxHistory.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/transaction/TxHistory.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/utility/Ping.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/utility/Ping.cpp.ai.md create mode 100644 src/xrpld/rpc/handlers/utility/Random.cpp.ai.json create mode 100644 src/xrpld/rpc/handlers/utility/Random.cpp.ai.md create mode 100644 src/xrpld/rpc/json_body.h.ai.json create mode 100644 src/xrpld/rpc/json_body.h.ai.md create mode 100644 src/xrpld/shamap/NodeFamily.cpp.ai.json create mode 100644 src/xrpld/shamap/NodeFamily.cpp.ai.md create mode 100644 src/xrpld/shamap/NodeFamily.h.ai.json create mode 100644 src/xrpld/shamap/NodeFamily.h.ai.md diff --git a/include/xrpl/basics/Archive.h.ai.json b/include/xrpl/basics/Archive.h.ai.json new file mode 100644 index 0000000000..35867e3148 --- /dev/null +++ b/include/xrpl/basics/Archive.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 13, + "name": "src" + }, + { + "lineno": 13, + "name": "dst" + } + ], + "classes": [], + "description": "Header file declaring a function to extract a tar archive compressed with lz4 using Boost Filesystem, within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Archive.h", + "functions": [ + { + "args": [ + "src", + "dst" + ], + "lineno": 13, + "name": "extractTarLz4" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/Archive.h.ai.md b/include/xrpl/basics/Archive.h.ai.md new file mode 100644 index 0000000000..d0ba2854fb --- /dev/null +++ b/include/xrpl/basics/Archive.h.ai.md @@ -0,0 +1,31 @@ +# `Archive.h` — Tar/LZ4 Archive Extraction + +This header declares a single utility function within the `xrpl` namespace: `extractTarLz4`. Its purpose is narrowly scoped — providing the XRPL node software with the ability to unpack `.tar.lz4` archives to a target directory at runtime. The most natural use case is ledger database bootstrapping, where a node downloads a pre-built snapshot of the ledger state rather than replaying the entire transaction history from genesis. + +## The Interface + +```cpp +void extractTarLz4( + boost::filesystem::path const& src, + boost::filesystem::path const& dst); +``` + +Both parameters are `boost::filesystem::path` rather than `std::string` or `std::filesystem::path`. This is consistent with the broader `xrpl/basics` module (see `FileUtilities.h`), which predates C++17's standard filesystem library and relies on Boost.Filesystem throughout. The function throws `std::runtime_error` on any failure — there is no return value to check or error code to inspect. + +## Implementation Design + +The implementation in `Archive.cpp` delegates all archive I/O to **libarchive**, a portable C library (``, ``). This is a deliberate choice over rolling a custom tar/lz4 parser: libarchive handles format detection, streaming decompression, and sparse file support in a well-tested, security-audited way. + +Resource management for the two libarchive handles — a reader (`ar`) and a disk writer (`aw`) — is handled via `std::unique_ptr` with custom deleters that call `archive_read_free` and `archive_write_free` respectively. This is the only safe pattern here: libarchive resources must be released even when intermediate steps throw, and wrapping them in `unique_ptr` ensures cleanup happens automatically as the stack unwinds. + +The reader is configured explicitly for the tar format and the lz4 filter (rather than using libarchive's auto-detection). This prevents the function from silently accepting other archive formats, keeping the interface contract tight. The file is opened with a 10240-byte block size, which matches the canonical recommendation in libarchive documentation. + +The disk writer is configured with `ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS`, meaning extracted files faithfully preserve timestamps, permissions, access control lists, and BSD file flags from the archive. For a snapshot intended to be a drop-in replacement for a live ledger database directory, this fidelity matters: the consuming software may rely on mtime or permission bits being intact. + +A non-obvious detail is the pathname rewriting on line 65: before writing each entry to disk, the function prepends `dst` to the entry's stored path using Boost.Filesystem's `/` operator. This is what places all extracted content under `dst` rather than at absolute paths embedded in the archive, and it prevents path traversal issues where a maliciously constructed archive might attempt to write files outside the intended directory tree. + +## Error Handling + +All errors are surfaced through `xrpl::Throw`, defined in `contract.h`. Unlike a raw `throw`, `Throw` first calls `LogThrow` to capture a stack trace before the exception propagates. This means extraction failures produce actionable diagnostics in the node's log — important for diagnosing corrupted snapshots or filesystem problems during a bootstrap operation that might otherwise appear as a silent crash. + +The function validates `src` is a regular file (not a directory or symlink) before opening it, providing a clear early error rather than letting libarchive fail with a less informative message. \ No newline at end of file diff --git a/include/xrpl/basics/BasicConfig.h.ai.json b/include/xrpl/basics/BasicConfig.h.ai.json new file mode 100644 index 0000000000..8f4e3092ec --- /dev/null +++ b/include/xrpl/basics/BasicConfig.h.ai.json @@ -0,0 +1,324 @@ +{ + "args": [ + { + "lineno": 15, + "name": "name" + }, + { + "lineno": 0, + "name": "src" + }, + { + "lineno": 0, + "name": "dst" + }, + { + "lineno": 45, + "name": "value" + }, + { + "lineno": 66, + "name": "key" + }, + { + "lineno": 72, + "name": "lines" + }, + { + "lineno": 78, + "name": "line" + }, + { + "lineno": 94, + "name": "other" + }, + { + "lineno": 156, + "name": "section" + }, + { + "lineno": 193, + "name": "sectionName" + }, + { + "lineno": 211, + "name": "ifs" + }, + { + "lineno": 220, + "name": "target" + }, + { + "lineno": 235, + "name": "defaultValue" + }, + { + "lineno": 272, + "name": "v" + } + ], + "classes": [ + { + "args": [ + "name" + ], + "lineno": 13, + "name": "Section" + }, + { + "args": [], + "lineno": 140, + "name": "BasicConfig" + } + ], + "description": "Defines classes and utility functions for handling configuration sections and key/value pairs, including parsing, storing, and retrieving configuration data for the xrpl project.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/BasicConfig.h", + "functions": [ + { + "args": [], + "lineno": 23, + "name": "Section::name" + }, + { + "args": [], + "lineno": 30, + "name": "Section::lines" + }, + { + "args": [], + "lineno": 37, + "name": "Section::values" + }, + { + "args": [ + "value" + ], + "lineno": 44, + "name": "Section::legacy" + }, + { + "args": [], + "lineno": 53, + "name": "Section::legacy" + }, + { + "args": [ + "key", + "value" + ], + "lineno": 65, + "name": "Section::set" + }, + { + "args": [ + "lines" + ], + "lineno": 71, + "name": "Section::append" + }, + { + "args": [ + "line" + ], + "lineno": 77, + "name": "Section::append" + }, + { + "args": [ + "name" + ], + "lineno": 82, + "name": "Section::exists" + }, + { + "args": [ + "name" + ], + "lineno": 85, + "name": "Section::get" + }, + { + "args": [ + "name", + "other" + ], + "lineno": 93, + "name": "Section::value_or" + }, + { + "args": [], + "lineno": 101, + "name": "Section::had_trailing_comments" + }, + { + "args": [], + "lineno": 110, + "name": "Section::empty" + }, + { + "args": [], + "lineno": 115, + "name": "Section::size" + }, + { + "args": [], + "lineno": 120, + "name": "Section::begin" + }, + { + "args": [], + "lineno": 125, + "name": "Section::cbegin" + }, + { + "args": [], + "lineno": 130, + "name": "Section::end" + }, + { + "args": [], + "lineno": 135, + "name": "Section::cend" + }, + { + "args": [ + "name" + ], + "lineno": 151, + "name": "BasicConfig::exists" + }, + { + "args": [ + "name" + ], + "lineno": 155, + "name": "BasicConfig::section" + }, + { + "args": [ + "name" + ], + "lineno": 158, + "name": "BasicConfig::section" + }, + { + "args": [ + "name" + ], + "lineno": 161, + "name": "BasicConfig::operator[]" + }, + { + "args": [ + "name" + ], + "lineno": 165, + "name": "BasicConfig::operator[]" + }, + { + "args": [ + "section", + "key", + "value" + ], + "lineno": 171, + "name": "BasicConfig::overwrite" + }, + { + "args": [ + "section" + ], + "lineno": 176, + "name": "BasicConfig::deprecatedClearSection" + }, + { + "args": [ + "section", + "value" + ], + "lineno": 183, + "name": "BasicConfig::legacy" + }, + { + "args": [ + "sectionName" + ], + "lineno": 192, + "name": "BasicConfig::legacy" + }, + { + "args": [], + "lineno": 201, + "name": "BasicConfig::had_trailing_comments" + }, + { + "args": [ + "ifs" + ], + "lineno": 210, + "name": "BasicConfig::build" + }, + { + "args": [ + "target", + "name", + "section" + ], + "lineno": 219, + "name": "set" + }, + { + "args": [ + "target", + "defaultValue", + "name", + "section" + ], + "lineno": 234, + "name": "set" + }, + { + "args": [ + "section", + "name", + "defaultValue" + ], + "lineno": 247, + "name": "get" + }, + { + "args": [ + "section", + "name", + "defaultValue" + ], + "lineno": 260, + "name": "get" + }, + { + "args": [ + "section", + "name", + "v" + ], + "lineno": 271, + "name": "get_if_exists" + }, + { + "args": [ + "section", + "name", + "v" + ], + "lineno": 277, + "name": "get_if_exists" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/BasicConfig.h.ai.md b/include/xrpl/basics/BasicConfig.h.ai.md new file mode 100644 index 0000000000..740d01fdc6 --- /dev/null +++ b/include/xrpl/basics/BasicConfig.h.ai.md @@ -0,0 +1,44 @@ +# `BasicConfig.h` — INI-Style Configuration Substrate + +`BasicConfig.h` defines the foundational data model for the XRPL node's configuration system. It sits at the bottom of a two-layer design: this file provides the in-memory representation and query interface for section-based configuration data, while the concrete `Config` class (in `src/xrpld/core/Config.h`) inherits from `BasicConfig` and adds filesystem loading, application-specific typed fields, and validator management. The header comment on `Config` explicitly labels that derived class as deprecated, signaling that `BasicConfig`'s style — decentralized, per-module parsing — is the intended long-term direction. + +## Data Model: Two Representations in One `Section` + +The `Section` class maintains three parallel containers for the same underlying config content: + +- `lookup_` — an `unordered_map` for `key = value` pairs, used for named lookups +- `values_` — a `vector` of non-key-value lines (bare tokens like IP addresses or file paths) +- `lines_` — a `vector` containing every non-empty, non-comment line in canonical form + +This triple storage isn't redundancy — it reflects the two distinct ways config sections are used in practice. Sections like `[server]` contain key=value pairs consumed by name; sections like `[validators]` contain bare values (one per line) iterated as a list. The `lines()` accessor preserves insertion order, which matters for list-type sections where positional meaning exists. + +The `append()` method is where parsing happens. It applies a Boost regex matching `^key=value` to each incoming line. Lines that match go into `lookup_` via `set()`; non-matching lines go into `values_`. Both go into `lines_`. The same method also handles inline comment stripping: `#` characters are treated as comment delimiters unless escaped with `\`. The escape character is consumed when found (`val.erase(comment - 1, 1)`), allowing literal `#` characters in values. This detail is tracked via `had_trailing_comments_`, which bubbles up through `BasicConfig::had_trailing_comments()` via `std::any_of` — presumably to emit a deprecation warning to operators about ambiguous config syntax. + +## The "Legacy" Pattern + +Some older config sections hold a single freeform value rather than key-value pairs — for example `[node_db]` in its pre-structured form. The `legacy()` getter/setter pair accommodates this by treating the first entry of `lines_` as the canonical value. Reading a `Section` as legacy on a multi-line section intentionally throws `std::runtime_error` via `Throw<>()`, enforcing that this access path is only valid for single-line sections. This prevents silent misreads where code expecting one value silently gets only the first of many. + +`BasicConfig` also exposes `legacy()` at the aggregate level, forwarding to the named section's `legacy()`. This provides `config.legacy("section_name")` as a convenience for the many legacy callsites in `Config.cpp`. + +## `BasicConfig`: Container and Access Protocol + +`BasicConfig` holds an `unordered_map`, keyed by section name. The critical behavioral difference between the const and non-const `section()` overloads reflects a deliberate design choice: + +- Non-const `section()` calls `map_.emplace(name, name)` — it auto-creates an empty section on first access. This allows callers to unconditionally call `config["new_section"].set(...)` without precondition checks. +- Const `section()` returns a reference to a `static Section const none("")` sentinel when the section doesn't exist. This avoids exceptions during read-only configuration queries and makes `operator[]` safe to call on a const `BasicConfig` even for absent sections. + +The `overwrite()` method is specifically for command-line argument injection, layering CLI-provided values over whatever the config file contains. `deprecatedClearSection()` (name signals intent) wipes a section's content by replacing its `Section` object wholesale — used historically to clear sections before reloading. + +The `build()` method is `protected`, not `public`. It consumes an `IniFileSections` (a `unordered_map>`), which is the raw pre-parsed form produced by `parseIniFile()` in `Config.cpp`. Subclasses call `build()` after obtaining this intermediate representation, keeping the file I/O and INI parsing out of `BasicConfig` itself. + +## Free Function Query Layer + +The file exports three sets of free functions designed for module-level configuration consumption: + +`set(target, name, section)` reads a named key, casts it via `boost::lexical_cast`, and assigns to `target` only on success — leaving `target` unchanged on missing key or bad cast. The two-argument variant adds an explicit default value applied on failure. Both return `bool` indicating whether the config file actually specified the value, which is important for distinguishing "user set this to the default" from "user didn't set this." + +`get(section, name, defaultValue)` is a value-returning variant; it catches `bad_lexical_cast` and falls back to the default silently. An overload handles `char const*` defaults to avoid awkward template deduction with string literals. + +`get_if_exists` is explicitly specialized to read boolean config values as integers (`0` or `1`) rather than as the string tokens `"true"` or `"false"`. This matches the XRPL config file convention where booleans are expressed numerically, and avoids `lexical_cast` which in Boost accepts `"true"` but not `"1"` depending on locale. + +Together these three free functions provide a consistent, exception-safe pattern that modules throughout the codebase use to pull typed values from their respective config sections without having to handle parse failures individually. \ No newline at end of file diff --git a/include/xrpl/basics/Blob.h.ai.json b/include/xrpl/basics/Blob.h.ai.json new file mode 100644 index 0000000000..1bbc644f5a --- /dev/null +++ b/include/xrpl/basics/Blob.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines a type alias 'Blob' for storing linear binary data as a vector of unsigned char within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Blob.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/Blob.h.ai.md b/include/xrpl/basics/Blob.h.ai.md new file mode 100644 index 0000000000..befbbf2478 --- /dev/null +++ b/include/xrpl/basics/Blob.h.ai.md @@ -0,0 +1,17 @@ +# `include/xrpl/basics/Blob.h` + +`Blob.h` introduces a single named type alias used throughout the XRPL codebase for owning, mutable binary data: + +```cpp +using Blob = std::vector; +``` + +Its role is to give raw byte sequences a meaningful, searchable name rather than leaving `std::vector` scattered as an anonymous type across the protocol and serialization layers. `Blob` appears as the internal storage buffer inside `Serializer` (`mData`), as the return type of serialization helpers, and in `StringUtilities` for hex encoding and SQL blob literals. + +`Blob` sits at one corner of the three-type binary data model in `xrpl::basics`: + +- **`Blob`** (`std::vector`) — mutable, dynamically resizable, owns its memory. The right choice when data is built up incrementally, as in `Serializer`. +- **`Buffer`** — fixed-size block allocated with `unique_ptr`, no capacity overhead, suitable when size is known upfront and resizing is not required. +- **`Slice`** — a non-owning, read-only `(pointer, length)` view. Cheap to copy and pass; `makeSlice()` factory overloads accept both `Blob` and `Buffer` seamlessly. + +The choice of `unsigned char` rather than `char` is deliberate: it avoids signed/unsigned arithmetic warnings when working with raw binary values and aligns with the `uint8_t` element type used by `Slice` and `Buffer`. Because `Blob` is simply a `std::vector`, callers get the full standard iterator interface, `push_back`, `resize`, and range-insert without any additional wrapper API. \ No newline at end of file diff --git a/include/xrpl/basics/Buffer.h.ai.json b/include/xrpl/basics/Buffer.h.ai.json new file mode 100644 index 0000000000..e3ecf36047 --- /dev/null +++ b/include/xrpl/basics/Buffer.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "size", + "data", + "other", + "s" + ], + "lineno": 10, + "name": "Buffer" + } + ], + "description": "Defines a Buffer class for managing dynamic byte arrays, similar to std::vector but optimized for use as a BufferFactory, including copy/move semantics, assignment from slices, and comparison operators.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Buffer.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 120, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 130, + "name": "operator!=" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/Buffer.h.ai.md b/include/xrpl/basics/Buffer.h.ai.md new file mode 100644 index 0000000000..d3f289d14e --- /dev/null +++ b/include/xrpl/basics/Buffer.h.ai.md @@ -0,0 +1,35 @@ +# `include/xrpl/basics/Buffer.h` + +## Role in the System + +`Buffer` is the XRPL codebase's canonical owning byte container. It occupies a distinct position alongside two other byte-handling types: `Slice`, which is a non-owning, immutable view into existing memory, and `Blob` (a typedef for `std::vector`), which is a general-purpose growable sequence. `Buffer` fills the gap between these: it owns its memory exclusively, is mutable, but makes no provision for incremental growth. When you need to allocate a block of bytes, write into it once, and pass it around by move, `Buffer` is the right tool. + +The class also satisfies an informal `BufferFactory` concept used by compression utilities — a callable that accepts a size and returns a `void*` to writable memory. This dual role as both a container and an allocator-callback is the most distinctive design choice in the file. + +## Ownership and Internal Layout + +The backing store is a `std::unique_ptr`, giving the class clear exclusive ownership with automatic deallocation. The invariant enforced throughout is that an empty buffer (`size_ == 0`) always holds a null pointer — never a zero-byte allocation. This is visible in the size constructor: `new std::uint8_t[size]` is called only when `size` is non-zero, and `alloc()` resets to `nullptr` if `n == 0`. The test suite verifies this invariant explicitly via its `sane()` helper, which asserts `data() == nullptr` iff `empty()`. Treating null as the canonical empty state avoids any ambiguity at the call site and makes zero-initialization checks safe without checking both pointer and size. + +## The `alloc()` Pattern — Discard, Don't Resize + +The central API difference from `std::vector` is `alloc(std::size_t n)`, which reallocates the buffer to exactly `n` bytes and discards any existing content. Unlike `vector::resize()`, there is no attempt to preserve data. This is intentional: the primary workload for `Buffer` is receiving output from operations like decompression, where the caller pre-computes the required size and wants a fresh block to write into. Reallocation is skipped entirely if the requested size equals the current size, avoiding a pointless free/alloc cycle when the same `Buffer` is reused across calls of equal output length. + +The `operator()(std::size_t n)` overload simply delegates to `alloc()` and returns a `void*`, satisfying the `BufferFactory` concept expected by `lz4Compress` in `CompressionAlgorithms.h`. That template function calls `bf(outCapacity)` to obtain the destination buffer — passing a `Buffer` object directly fills both roles (allocation and storage) in a single object. + +## Slice Integration + +`Buffer` is tightly coupled to `Slice`. It provides an implicit conversion `operator Slice() const noexcept`, so any `Buffer` can be passed wherever a `Slice` is expected without an explicit cast. The reverse — constructing a `Buffer` from a `Slice` — is marked `explicit`, preventing accidental copies of view-only data. + +The `operator=(Slice)` assignment requires particular attention: before copying, it checks via `XRPL_ASSERT` that the source slice does not overlap with the `Buffer`'s own storage. The danger is that `alloc()` frees the old memory first, and if the incoming `Slice` pointed into that memory, the subsequent `memcpy` would be a use-after-free. The assertion guards against this specific self-overlapping scenario. Note that `operator=(Buffer const&)` uses a different path through `alloc()` + `memcpy`, which naturally handles self-assignment because `alloc()` is a no-op when sizes match — the existing pointer is reused and then `memcpy`-d over itself (which is defined behavior for `memcpy` with identical source and destination). + +## Move Semantics + +Both move constructor and move assignment are `noexcept`, a static guarantee the test suite verifies with `static_assert`. This ensures `Buffer` can be held in standard containers like `std::vector` without triggering copies on reallocation. After a move, the source is left in a valid empty state: `p_` is null (via `unique_ptr` move semantics) and `size_` is explicitly reset to zero. + +## Comparison and Iteration + +Equality comparison is implemented as a free function using `std::memcmp` after a size check. The class exposes only `const_iterator` (raw `uint8_t const*` pointers), meaning range-for loops and standard algorithms can consume the buffer's contents read-only. Mutable iteration is available only through `data()`, keeping the interface honest about the distinction between reading and writing into the buffer. + +## Contrast with `Blob` + +`Blob` (`std::vector`) is still used extensively in the codebase for cases where the byte sequence grows incrementally, such as serialization output. `Buffer` is preferred when the size is known upfront, ownership transfer by move is the primary operation, or the `BufferFactory` pattern is required — for example, storing the output of an LZ4 decompression call without needing the capacity/size distinction that `vector` maintains internally. \ No newline at end of file diff --git a/include/xrpl/basics/ByteUtilities.h.ai.json b/include/xrpl/basics/ByteUtilities.h.ai.json new file mode 100644 index 0000000000..671adbb897 --- /dev/null +++ b/include/xrpl/basics/ByteUtilities.h.ai.json @@ -0,0 +1,38 @@ +{ + "args": [ + { + "lineno": 6, + "name": "value" + }, + { + "lineno": 13, + "name": "value" + } + ], + "classes": [], + "description": "Provides constexpr utility functions to convert values to kilobytes and megabytes within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/ByteUtilities.h", + "functions": [ + { + "args": [ + "value" + ], + "lineno": 6, + "name": "kilobytes" + }, + { + "args": [ + "value" + ], + "lineno": 13, + "name": "megabytes" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/ByteUtilities.h.ai.md b/include/xrpl/basics/ByteUtilities.h.ai.md new file mode 100644 index 0000000000..f52febc35b --- /dev/null +++ b/include/xrpl/basics/ByteUtilities.h.ai.md @@ -0,0 +1,26 @@ +# `ByteUtilities.h` — Compile-Time Byte-Size Helpers + +`ByteUtilities.h` is a minimal, header-only utility in `xrpl/basics` that provides two `constexpr` template functions — `kilobytes()` and `megabytes()` — for expressing byte-count constants in human-readable units at compile time. The file exists purely to eliminate magic numbers from sites that configure buffer sizes, memory limits, and slab allocator parameters throughout the XRPL codebase. + +## The Functions + +`kilobytes(value)` multiplies its argument by 1024. `megabytes(value)` composes that twice — it calls `kilobytes(kilobytes(value))` — which gives the correct factor of 1,048,576 (2²⁰) without any separate literal. Both functions are templated on `T`, so they work with any integral or arithmetic type and return the same type that the arithmetic produces, letting the caller's type context drive the result without an explicit cast. Both are `constexpr` and `noexcept`, meaning the computation happens entirely at compile time and has no runtime overhead whatsoever. + +The `static_assert` lines immediately below the definitions act as inline tests: they verify `kilobytes(2) == 2048` and `megabytes(3) == 3145728` during every compilation, preventing any silent regression if the implementation were ever accidentally changed. + +## Design Rationale + +The template design over a fixed `size_t` signature is deliberate. Call sites like `megabytes(std::size_t(60))` in `SHAMapItem.h` need to produce `std::size_t` results for slab allocator configuration, while other uses such as `megabytes(256)` in `RPCCall.cpp` are happy with `int`-width results for comparison. By letting `auto` return the natural result of the arithmetic, the functions avoid both unwanted narrowing conversions and unwanted widening that could paper over a type mismatch. + +The composition `kilobytes(kilobytes(value))` for megabytes is a small but telling choice: it reuses the already-tested primitive rather than independently writing `value * 1024 * 1024`, keeping the chain of trust short and making the relationship between units self-documenting. + +## Usage Across the Codebase + +The functions appear at exactly the kinds of boundaries where misreading a magnitude would have serious consequences: + +- **Overlay message cap**: `src/xrpld/overlay/Message.h` defines `constexpr std::size_t maximumMessageSize = megabytes(64)`, bounding the maximum peer-to-peer message size to 64 MiB. +- **RPC reply limit**: `src/xrpld/rpc/detail/RPCCall.cpp` defines `constexpr auto RPC_REPLY_MAX_BYTES = megabytes(256)` to guard against unbounded JSON responses. +- **Ledger and open-view buffers**: `include/xrpl/ledger/OpenView.h` and `include/xrpl/ledger/detail/RawStateTable.h` both set `initialBufferSize = kilobytes(256)` for their serialisation scratch buffers. +- **ShaMap slab allocator**: `SHAMapItem.h` uses `megabytes()` to express the per-size-class allocation limits for the slab allocator pools (60 MB, 46 MB, etc.), and `TaggedPointer.ipp` uses `kilobytes(512)` for the slab block granularity. + +The consistent use of these helpers rather than raw literals means that anyone reading any of those files immediately understands the intended scale without mental arithmetic, and the compiler catches any integer overflow that a bare literal might hide at the point of definition. \ No newline at end of file diff --git a/include/xrpl/basics/CompressionAlgorithms.h.ai.json b/include/xrpl/basics/CompressionAlgorithms.h.ai.json new file mode 100644 index 0000000000..6fb26dfece --- /dev/null +++ b/include/xrpl/basics/CompressionAlgorithms.h.ai.json @@ -0,0 +1,93 @@ +{ + "args": [ + { + "lineno": 18, + "name": "in" + }, + { + "lineno": 18, + "name": "inSize" + }, + { + "lineno": 18, + "name": "bf" + }, + { + "lineno": 41, + "name": "in" + }, + { + "lineno": 41, + "name": "inSizeUnchecked" + }, + { + "lineno": 41, + "name": "decompressed" + }, + { + "lineno": 41, + "name": "decompressedSizeUnchecked" + }, + { + "lineno": 62, + "name": "in" + }, + { + "lineno": 62, + "name": "inSize" + }, + { + "lineno": 62, + "name": "decompressed" + }, + { + "lineno": 62, + "name": "decompressedSize" + } + ], + "classes": [], + "description": "Provides LZ4 block compression and decompression utilities, including template and inline functions for compressing and decompressing data buffers and streams within the xrpl::compression_algorithms namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/CompressionAlgorithms.h", + "functions": [ + { + "args": [ + "in", + "inSize", + "bf" + ], + "lineno": 18, + "name": "lz4Compress" + }, + { + "args": [ + "in", + "inSizeUnchecked", + "decompressed", + "decompressedSizeUnchecked" + ], + "lineno": 41, + "name": "lz4Decompress" + }, + { + "args": [ + "in", + "inSize", + "decompressed", + "decompressedSize" + ], + "lineno": 62, + "name": "lz4Decompress" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "compression_algorithms" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/CompressionAlgorithms.h.ai.md b/include/xrpl/basics/CompressionAlgorithms.h.ai.md new file mode 100644 index 0000000000..b09e74dfea --- /dev/null +++ b/include/xrpl/basics/CompressionAlgorithms.h.ai.md @@ -0,0 +1,53 @@ +# `CompressionAlgorithms.h` — LZ4 Block Compression Primitives + +This header lives in `include/xrpl/basics/` and provides the low-level LZ4 compression and decompression routines used by the XRPL peer overlay network. It sits one abstraction layer below `src/xrpld/overlay/Compression.h`, which adds algorithm-selection logic and error suppression on top of what this file exposes. + +## Architectural Role + +When XRPL nodes exchange P2P messages they can optionally compress the payload before transmission. The overlay layer negotiates compression during the connection handshake and then routes compressed messages through the functions defined here. `CompressionAlgorithms.h` isolates the raw LZ4 calls — the `int`-based C API hazards, buffer management, and stream chunking — from the policy-level decisions that live in `Compression.h`. + +The functions are entirely in the `xrpl::compression_algorithms` namespace. There are no classes, no state, no singletons — just three free functions. + +## `lz4Compress` — Template with BufferFactory + +```cpp +template +std::size_t lz4Compress(void const* in, std::size_t inSize, BufferFactory&& bf) +``` + +The design choice to accept a `BufferFactory` callable rather than returning a `std::vector` is deliberate and important. The caller knows its allocation context: in the overlay code it may be writing into a Protobuf `CodedOutputStream` region or a pooled buffer. The factory receives the worst-case compressed size from `LZ4_compressBound` and returns a raw pointer; the template accepts any callable that satisfies this contract without virtual dispatch overhead. + +The sole pre-condition check guards against input larger than `UINT32_MAX`. LZ4's block API uses `int` internally, so exceeding that limit would silently truncate the size argument. The function throws via `Throw`, which logs a call stack through `contract.h` before throwing — consistent with XRPL's "crash loudly with context" philosophy for invariant violations. + +## `lz4Decompress` — Raw Buffer Overload + +```cpp +inline std::size_t lz4Decompress( + std::uint8_t const* in, std::size_t inSizeUnchecked, + std::uint8_t* decompressed, std::size_t decompressedSizeUnchecked) +``` + +The `Unchecked` naming in the parameters is the code's way of signalling that the `size_t` → `int` narrowing has not yet been validated. The function immediately casts both sizes to `int` and checks for `<= 0`. This catches two distinct failure modes: a genuinely zero-length buffer, and a `size_t` value large enough that the narrowing wrap produces a non-positive `int`. Separating these checks with distinct error messages makes debugging easier. + +`LZ4_decompress_safe` is used rather than the faster `LZ4_decompress_fast`. The safe variant takes the output buffer capacity as a bound and will not write past it even if the compressed data is malformed — essential when the input arrives from an untrusted peer on the network. + +The function enforces an exact-size postcondition: if `LZ4_decompress_safe` returns anything other than the expected `decompressedSize` it throws. This reflects the fact that, in the overlay protocol, the original message size is transmitted in the message header; any mismatch means either corruption or a peer bug. + +## `lz4Decompress` — Streaming ZeroCopyInputStream Overload + +```cpp +template +std::size_t lz4Decompress( + InputStream& in, std::size_t inSize, + std::uint8_t* decompressed, std::size_t decompressedSize) +``` + +This overload works with Protobuf-style `ZeroCopyInputStream` objects that expose data as a series of chunks rather than a single contiguous buffer. The key optimization is the fast path: if the very first chunk returned by `in.Next()` is at least `inSize` bytes long, the function uses that chunk's pointer directly and avoids any allocation. In practice, compressed P2P messages typically arrive in a single TCP read buffer, so this path is taken most of the time. + +When the data spans multiple chunks, the function lazily allocates a `std::vector` of exactly `inSize` bytes (note the `compressed.resize(inSize)` is only reached on the second iteration) and copies chunks into it until the full compressed message is assembled. After reading, any bytes that were consumed from the stream beyond `inSize` are returned via `in.BackUp()`, preserving the stream cursor for the next message in the framing protocol. + +The final validation before delegating to the raw overload checks that the amount actually read matches what was requested. This guards against a stream that ends early — e.g., a truncated TCP connection or a framing bug where the declared size doesn't match the available data. + +## Relationship to `Compression.h` + +The overlay's `Compression.h` wraps these two functions inside `compress()` and `decompress()` functions that add an `Algorithm` enum parameter (currently `Algorithm::LZ4 = 0x90` or `Algorithm::None`). Those wrappers catch all exceptions from the functions here and return `0` on failure, converting the throw-on-error contract into a return-zero-on-error contract. The distinction is intentional: the raw primitives throw so that callers who want structured error handling can use them; the overlay wrapper normalises failures to a `0` return value to simplify the state machine in the peer message processing loop. \ No newline at end of file diff --git a/include/xrpl/basics/CountedObject.h.ai.json b/include/xrpl/basics/CountedObject.h.ai.json new file mode 100644 index 0000000000..d248bbff2a --- /dev/null +++ b/include/xrpl/basics/CountedObject.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 23, + "name": "name" + }, + { + "lineno": 16, + "name": "minimumThreshold" + }, + { + "lineno": 65, + "name": "Object" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "CountedObjects" + }, + { + "args": [ + "name" + ], + "lineno": 22, + "name": "Counter" + }, + { + "args": [], + "lineno": 65, + "name": "CountedObject" + } + ], + "description": "Provides a mechanism to count and report the number of instances of various object types at runtime, using a lock-free linked list and atomic counters. Includes a base class for automatic instance counting.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/CountedObject.h", + "functions": [ + { + "args": [], + "lineno": 10, + "name": "getInstance" + }, + { + "args": [ + "minimumThreshold" + ], + "lineno": 16, + "name": "getCounts" + }, + { + "args": [], + "lineno": 36, + "name": "increment" + }, + { + "args": [], + "lineno": 41, + "name": "decrement" + }, + { + "args": [], + "lineno": 46, + "name": "getCount" + }, + { + "args": [], + "lineno": 51, + "name": "getNext" + }, + { + "args": [], + "lineno": 56, + "name": "getName" + }, + { + "args": [], + "lineno": 71, + "name": "getCounter" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/CountedObject.h.ai.md b/include/xrpl/basics/CountedObject.h.ai.md new file mode 100644 index 0000000000..085a20a3fd --- /dev/null +++ b/include/xrpl/basics/CountedObject.h.ai.md @@ -0,0 +1,55 @@ +# `include/xrpl/basics/CountedObject.h` + +## Purpose + +This header provides a zero-per-instance-overhead mechanism for counting live objects of any given type throughout the rippled process lifetime. It exists for operational diagnostics: the `get_counts` admin RPC command interrogates `CountedObjects` to report how many instances of each tracked type are currently alive, helping operators identify memory growth, cache saturation, or unexpected object accumulation. + +## Design Pattern — CRTP Instance Counting + +The design uses the Curiously Recurring Template Pattern (CRTP). A class opts into counting by inheriting `CountedObject`: + +```cpp +class SHAMapItem : public CountedObject { ... }; +class NodeObject : public CountedObject { ... }; +class Job : public CountedObject { ... }; +``` + +Across the codebase, roughly two dozen types follow this pattern — `STPathElement`, `STPath`, `InfoSub`, `HashRouter::Entry`, `Book`, `CanonicalTXSet`, and many more. Adding the base class is the entire integration cost; no other instrumentation is required. + +The key insight that makes this zero-per-instance overhead is that `CountedObject::getCounter()` returns a **function-local static** `Counter` object — one per template instantiation, not one per live instance. The only per-instance cost is two atomic increments (constructor and destructor) touching a shared counter. + +## Three-Layer Architecture + +**`CountedObject`** (template base class) — the public-facing layer. Its default constructor, copy constructor, and destructor call `getCounter().increment()` / `decrement()` respectively. The copy constructor is explicitly defined to increment because a copy produces a new live object; the assignment operator is `= default` because assigning between two existing objects doesn't change the total number of live instances. There is no explicit move constructor, so moves fall back to the copy constructor, which correctly increments for the new object while the source's destructor later decrements for the old one. + +**`CountedObjects::Counter`** (inner class) — the per-type bookkeeping node. Each `Counter` holds its type name (obtained via `beast::type_name()`, which uses `typeid` plus GCC/Clang ABI demangling for a human-readable string), an `std::atomic` live count, and a raw `Counter*` pointer to the next node in an intrusive singly-linked list. + +**`CountedObjects`** (singleton) — the global registry. It owns the head of the lock-free linked list and a count of registered counter types. + +## Lock-Free Registration + +`Counter` objects self-register when they are first constructed — which happens at first use of any given type, during static initialization of `getCounter()`'s local static. Registration must be thread-safe without a mutex, because many types can be instantiated concurrently at startup: + +```cpp +Counter* head = nullptr; +do { + head = instance.m_head.load(); + next_ = head; +} while (instance.m_head.exchange(this) != head); +``` + +This is a classic CAS (compare-and-swap) insertion loop: load the current head, set `next_` to it, then atomically exchange the head with `this`. If the head changed between the load and the exchange, retry. Because `Counter` objects are permanent (static lifetime), they are never removed from the list, so traversal during `getCounts()` never encounters a dangling pointer regardless of whether other registrations are happening concurrently. + +## `getCounts()` and the Reporting Path + +`CountedObjects::getCounts(int minimumThreshold)` traverses the linked list and collects `(name, count)` pairs for any type whose live count is at or above the threshold. It pre-reserves the result vector using `m_count.load()` as a hint (the comment in the implementation acknowledges this can be temporarily under-counted under concurrency — it is only an optimization). The results are sorted alphabetically before return. + +The `get_counts` admin RPC handler (`GetCounts.cpp`) calls this with a configurable `min_count` (defaulting to 10) and serializes the results into a JSON object, mixing them with cache statistics, database sizes, write load, and uptime. Object counts appear as top-level keys named by the demangled C++ type. + +## Concurrency Properties + +All per-type counts use `std::atomic` with default sequential consistency, so `increment()` and `decrement()` are safe from any thread. The linked-list head pointer `m_head` is also `std::atomic`. There are no mutexes anywhere in this file. The only non-atomic operation is reading `Counter::next_` during traversal in `getCounts()`, which is safe because `next_` is written exactly once at construction time and never modified thereafter. + +## Why Not Alternatives + +A virtual-function approach (e.g., a pure virtual `typeName()` method) would require each instance to carry a vtable pointer and would not trivially aggregate counts across all instances of the same type without additional infrastructure. A manual registry with `std::map` would need a mutex. The CRTP-plus-static-counter approach achieves type safety, automatic demangled names, lock-free operation, and zero per-instance storage — at the cost of slightly surprising copy/move semantics that operators must understand when subclassing. \ No newline at end of file diff --git a/include/xrpl/basics/DecayingSample.h.ai.json b/include/xrpl/basics/DecayingSample.h.ai.json new file mode 100644 index 0000000000..f6def9dfc1 --- /dev/null +++ b/include/xrpl/basics/DecayingSample.h.ai.json @@ -0,0 +1,115 @@ +{ + "args": [ + { + "lineno": 18, + "name": "now" + }, + { + "lineno": 26, + "name": "value" + }, + { + "lineno": 26, + "name": "now" + }, + { + "lineno": 34, + "name": "now" + }, + { + "lineno": 41, + "name": "now" + }, + { + "lineno": 61, + "name": "now" + }, + { + "lineno": 74, + "name": "value" + }, + { + "lineno": 74, + "name": "now" + }, + { + "lineno": 79, + "name": "now" + }, + { + "lineno": 86, + "name": "now" + } + ], + "classes": [ + { + "args": [ + "time_point now" + ], + "lineno": 10, + "name": "DecayingSample" + }, + { + "args": [ + "time_point now" + ], + "lineno": 61, + "name": "DecayWindow" + } + ], + "description": "Provides two template classes for sampling functions using exponential decay: DecayingSample (with a fixed window) and DecayWindow (with a half-life), useful for tracking decaying averages or statistics over time.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/DecayingSample.h", + "functions": [ + { + "args": [ + "value", + "now" + ], + "lineno": 26, + "name": "DecayingSample::add" + }, + { + "args": [ + "now" + ], + "lineno": 34, + "name": "DecayingSample::value" + }, + { + "args": [ + "now" + ], + "lineno": 41, + "name": "DecayingSample::decay" + }, + { + "args": [ + "value", + "now" + ], + "lineno": 74, + "name": "DecayWindow::add" + }, + { + "args": [ + "now" + ], + "lineno": 79, + "name": "DecayWindow::value" + }, + { + "args": [ + "now" + ], + "lineno": 86, + "name": "DecayWindow::decay" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/DecayingSample.h.ai.md b/include/xrpl/basics/DecayingSample.h.ai.md new file mode 100644 index 0000000000..5e3f1ac1f8 --- /dev/null +++ b/include/xrpl/basics/DecayingSample.h.ai.md @@ -0,0 +1,39 @@ +# `DecayingSample.h` — Exponential Decay Accumulators + +This header provides two small template classes that maintain a running accumulation of values that automatically decay over time. Both are used throughout the XRPL node to answer the question "how much activity has happened recently?" without needing to store timestamped histories — the decay does the windowing implicitly. + +## `DecayingSample` + +`DecayingSample` maintains an integer accumulator that decays by approximately `1/Window` of its current value each second, producing a rate estimate normalized over the window length. It drives the resource manager's per-peer charge tracking: `Entry.h` declares `local_balance` as `DecayingSample` where `decayWindowSeconds = 32` (a power of two, per a comment in `Tuning.h`, so the division can be optimized to a bit-shift by the compiler). + +The core decay step is deliberately integer arithmetic with ceiling division: + +```cpp +m_value -= (m_value + Window - 1) / Window; +``` + +This subtracts at least 1 when `m_value` is positive, so the value cannot stall at a non-zero integer indefinitely — a safety property important for rate limiting. Adding `Window - 1` before dividing implements ceiling division, meaning the decay rounds up rather than down. The practical effect is the balance decays slightly faster than the mathematically ideal `m_value *= (1 - 1/Window)^elapsed`, which is a conservative choice for load balancing: erring toward under-charging rather than over-charging. + +The `decay()` fast-path cuts off long idle periods: if more than `4 * Window` seconds have elapsed since the last update (which would leave the value at less than ~2% of its original magnitude), `m_value` is simply zeroed. This prevents the per-second loop from iterating hundreds of times on a reconnecting peer. + +`add()` ages the accumulator first, then adds the new sample, and returns `m_value / Window` — the normalized balance representing average load per second across the window. `value()` does the same without adding anything. Both methods demand a `time_point now` from the caller rather than reading a clock themselves; this makes the class testable and clock-agnostic. + +## `DecayWindow` + +`DecayWindow` takes a different approach: it stores a `double` and applies the mathematically exact exponential half-life formula: + +```cpp +value_ *= std::pow(2.0, -elapsed / HalfLife); +``` + +After exactly `HalfLife` seconds of inactivity, the accumulated value halves. After two half-lives it quarters, and so on. Unlike `DecayingSample`, which loops through whole seconds, `DecayWindow` casts the elapsed duration to `duration`, giving it sub-second precision — appropriate when the caller's clock has higher resolution or when calls are frequent. + +`InboundLedgers.cpp` uses this class as `DecayWindow<30, clock_type> fetchRate_` to measure the rate at which ledgers are being fetched from peers. Each fetch fires `fetchRate_.add(1, now)`. The `fetchRate()` accessor returns `60 * fetchRate_.value(now)`, converting the per-second average to a per-minute rate for reporting. + +The `static_assert(HalfLife > 0)` guards against a zero divisor in `std::pow`, which would produce undefined floating-point behavior. + +## Design Rationale: Two Classes Rather Than One + +The two classes reflect different use cases that have incompatible requirements. `DecayingSample` works with integer `value_type` (derived from the clock's duration representation), which matters for the resource manager where charges are counted in discrete units and the result feeds integer comparison thresholds. Integer arithmetic also avoids floating-point instability in tight loops. `DecayWindow` accepts `double` inputs and uses `std::pow`, accepting the floating-point cost in exchange for smooth decay curves and sub-second accuracy — the right tradeoff when measuring continuous rates rather than discrete charges. + +Neither class is thread-safe on its own; callers are responsible for synchronization. `InboundLedgersImp` wraps `fetchRate_` with `fetchRateMutex_`, and the resource `Entry` is similarly protected by the table's lock. \ No newline at end of file diff --git a/include/xrpl/basics/Expected.h.ai.json b/include/xrpl/basics/Expected.h.ai.json new file mode 100644 index 0000000000..9850dee463 --- /dev/null +++ b/include/xrpl/basics/Expected.h.ai.json @@ -0,0 +1,109 @@ +{ + "args": [ + { + "lineno": 29, + "name": "Impl" + }, + { + "lineno": 36, + "name": "Impl" + }, + { + "lineno": 43, + "name": "Impl" + }, + { + "lineno": 53, + "name": "E" + }, + { + "lineno": 80, + "name": "U" + }, + { + "lineno": 87, + "name": "U" + }, + { + "lineno": 128, + "name": "U" + } + ], + "classes": [ + { + "args": [], + "lineno": 16, + "name": "bad_expected_access" + }, + { + "args": [], + "lineno": 27, + "name": "throw_policy" + }, + { + "args": [ + "E const& e", + "E&& e" + ], + "lineno": 53, + "name": "Unexpected" + }, + { + "args": [ + "U&& r", + "Unexpected e" + ], + "lineno": 77, + "name": "Expected" + }, + { + "args": [ + "Expected()", + "Unexpected e" + ], + "lineno": 120, + "name": "Expected" + } + ], + "description": "This file provides an approximation of std::expected (proposed for C++23) using boost::outcome_v2::result, including custom error handling and policies for expected/unexpected result types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Expected.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "bad_expected_access" + }, + { + "args": [ + "Impl&& self" + ], + "lineno": 29, + "name": "wide_value_check" + }, + { + "args": [ + "Impl&& self" + ], + "lineno": 36, + "name": "wide_error_check" + }, + { + "args": [ + "Impl&& self" + ], + "lineno": 43, + "name": "wide_exception_check" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 25, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/Expected.h.ai.md b/include/xrpl/basics/Expected.h.ai.md new file mode 100644 index 0000000000..b010d7fa9f --- /dev/null +++ b/include/xrpl/basics/Expected.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/basics/Expected.h` + +## Role and Motivation + +This header provides `xrpl::Expected`, a polyfill for the `std::expected` type proposed for C++23 (P0323R10). At the time this code was written, `std::expected` was not yet available, so the implementation delegates all storage and state management to `boost::outcome_v2::result` while exposing an API that closely mirrors the eventual standard — making a future migration straightforward. + +`Expected` represents a value that is *either* a success of type `T` or an error of type `E`. Unlike `std::optional`, which only signals absence, `Expected` carries diagnostic information about *why* a result is missing. Unlike exceptions, it forces callers to explicitly inspect the outcome. The `[[nodiscard]]` attribute on both the primary template and the `void` specialization guarantees at compile time that callers cannot silently drop a return value — a critical safety property in a financial ledger where ignored error returns could mean silent transaction corruption. + +## Components + +### `bad_expected_access` + +A thin `std::runtime_error` subclass thrown whenever code tries to read the wrong half of an `Expected` — e.g., calling `value()` on an error-holding instance. It carries no additional data because the error value itself is available via `error()` and the point of failure is immediately clear from the stack trace. By inheriting from `std::runtime_error`, it integrates naturally with XRPL's existing exception hierarchy. + +### `detail::throw_policy` + +Boost.Outcome's policy mechanism controls what happens when the invariants of a `result` are violated. The default boost policy may assert or exhibit undefined behavior depending on build configuration; `throw_policy` replaces that with deterministic exception throwing. All three "wide" check entry points — `wide_value_check`, `wide_error_check`, and `wide_exception_check` — delegate to `Throw()` (from `contract.h`) rather than a bare `throw`, so the violation is also logged before the exception propagates, consistent with XRPL's programming-by-contract philosophy. + +### `Unexpected` + +A wrapper type that acts as an explicit tag for the error path. A function returning `Expected` constructs the error branch by returning `Unexpected(err)`, not a bare `E`. This prevents the implicit construction ambiguity that would arise if both `T` and `E` were, for example, `std::string`. The class provides all four value-category overloads of `value()` (lvalue/rvalue × const/non-const) for perfect forwarding into `Expected`'s constructor. + +The deduction guide `Unexpected(E (&)[N]) -> Unexpected` makes it ergonomic to pass string literals: `Unexpected("bad input")` deduces to `Unexpected` rather than to a fixed-length array type, avoiding obscure template errors. + +### `Expected` (primary template) + +Privately inherits from `boost::outcome_v2::result`. Private inheritance is intentional — it exposes only the `std::expected`-shaped API and hides the broader Outcome API (which includes channel-specific accessors and other facilities that would pollute the interface). The two constructors use `requires std::convertible_to` constraints so that implicit narrowing is rejected at compile time. + +`operator bool`, `operator*`, and `operator->` map onto `has_value()` and `value()`, matching the pointer-like ergonomics of the standard proposal. Accessing `operator*` or `operator->` on an error-holding `Expected` triggers `throw_policy::wide_value_check`, which throws `bad_expected_access`. Similarly, calling `error()` on a value-holding instance triggers `wide_error_check`. + +### `Expected` (partial specialization) + +Functions that either succeed (producing no value) or fail with a diagnostic use this specialization. Its default constructor calls `boost::outcome_v2::success()` to produce a successful instance — matching the proposed `std::expected{}` default construction semantics. This is the pattern used in `STTx::checkSign()` and related signature-verification methods, which return `Expected`: on success the caller simply checks `operator bool`; on failure the error string explains what went wrong. + +## Usage Patterns in the Codebase + +`tokens.h` defines a convenience alias `B58Result = Expected` for Base58Check encoding/decoding operations, where the error is a standard system error code. `base_uint.h` uses `Expected` for a `noexcept` hex-parsing path, capturing a per-character parse failure without throwing. `STTx.h` uses `Expected` for all signature-check entry points — a natural fit because signature validation either passes silently or produces a human-readable error message. + +## Design Trade-offs + +Choosing `boost::outcome` over a hand-rolled type means the storage layout, move semantics, and triviality propagation are handled by a well-tested library, reducing the risk of subtle UB in low-level storage operations. The cost is a dependency on Boost and some mismatch between Outcome's three-state model (value / error / exception pointer) and `std::expected`'s two-state model; the `wide_exception_check` override in `throw_policy` handles the third state consistently by also throwing `bad_expected_access`, even though `Expected` itself never stores an exception pointer in practice. When C++23 `std::expected` becomes universally available, the migration path is clear: the public API is already a subset of the standard interface. \ No newline at end of file diff --git a/include/xrpl/basics/FileUtilities.h.ai.json b/include/xrpl/basics/FileUtilities.h.ai.json new file mode 100644 index 0000000000..27a1839d22 --- /dev/null +++ b/include/xrpl/basics/FileUtilities.h.ai.json @@ -0,0 +1,58 @@ +{ + "args": [ + { + "lineno": 9, + "name": "ec" + }, + { + "lineno": 10, + "name": "sourcePath" + }, + { + "lineno": 11, + "name": "maxSize" + }, + { + "lineno": 15, + "name": "ec" + }, + { + "lineno": 16, + "name": "destPath" + }, + { + "lineno": 17, + "name": "contents" + } + ], + "classes": [], + "description": "Provides utility functions for reading from and writing to files using Boost filesystem, with error handling and optional size limit.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/FileUtilities.h", + "functions": [ + { + "args": [ + "ec", + "sourcePath", + "maxSize" + ], + "lineno": 8, + "name": "getFileContents" + }, + { + "args": [ + "ec", + "destPath", + "contents" + ], + "lineno": 14, + "name": "writeFileContents" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/FileUtilities.h.ai.md b/include/xrpl/basics/FileUtilities.h.ai.md new file mode 100644 index 0000000000..5c0beab862 --- /dev/null +++ b/include/xrpl/basics/FileUtilities.h.ai.md @@ -0,0 +1,33 @@ +# `include/xrpl/basics/FileUtilities.h` + +This header declares two thin file I/O utilities — `getFileContents` and `writeFileContents` — that form the XRPL codebase's standard interface for synchronous file access. The design problem they solve is not the I/O itself, but the error-handling contract: the broader rippled codebase avoids exceptions in many subsystems and instead relies on `boost::system::error_code` for structured, non-throwing error propagation. These two functions provide a consistent, exception-free surface for the handful of places in the node that must read or write files. + +## Interface Design + +Both functions follow the same convention: an `error_code&` output parameter is the first argument, populated on failure while the function returns an empty result (or returns nothing for the write case). This is the classic Boost.Asio-style error-out-parameter pattern, chosen over exception-throwing I/O because the callers — configuration loading, validator list file reads, test scaffolding — operate in contexts where an error is a recoverable condition requiring a structured diagnostic path rather than a stack unwind. + +`getFileContents` takes a `boost::filesystem::path` and an optional `std::size_t` upper bound. The `std::optional maxSize` parameter is the key safety valve: it allows callers to cap memory usage before any bytes are read into a `std::string`. When absent, the file is read in full. When present, the function checks the on-disk file size before opening the stream and returns `file_too_large` immediately if the limit is exceeded. This pre-check is cheap and prevents unbounded allocation when reading untrusted or potentially large files. + +`writeFileContents` takes a `boost::filesystem::path` and the string to write. It opens with `std::ios::out | std::ios::trunc`, guaranteeing the destination file is replaced atomically from the content's perspective — no partial appends. The function does not check or create intermediate directories; the caller is responsible for ensuring the destination path exists. + +## Implementation Notes (from the `.cpp`) + +`getFileContents` calls `boost::filesystem::canonical()` before doing anything else. This resolves symlinks and relative components into an absolute, normalized path, ensuring the subsequent `file_size()` check and stream open operate on the same physical file. Calling `canonical()` with the `ec` overload also intercepts path resolution errors (non-existent file, permission denied) through the same error-code channel rather than a filesystem exception. + +After path resolution, the implementation reads the file via a `std::istreambuf_iterator` range construction directly into a `std::string`. This is idiomatic C++ for slurping a whole file but has a subtle implication: for text-mode streams on some platforms, newline translation may occur. The stream is opened in `std::ios::in` (text mode), consistent with the intended use cases — TOML/JSON configuration and validator list JSON — where the content is human-readable text rather than binary data. + +Error checking is done at three points: path resolution failure, pre-open size check, and post-read `fileStream.bad()`. The `bad()` check (not `fail()`) specifically catches I/O errors during reading, not logical stream state issues, which is the correct guard for a hardware or OS-level read failure mid-stream. + +## Callers in Context + +The three primary call sites reveal the intended use scope: + +- `src/xrpld/core/detail/Config.cpp` uses `getFileContents` twice: once to load the main configuration file and once to load the validators file specified within that config. These are startup-time reads on the main thread, where a missing file is a fatal misconfiguration. +- `src/xrpld/app/misc/detail/WorkFile.h` uses `getFileContents` with a hard cap of `megabytes(1)` to read validator list files fetched from the network. The 1 MB cap is a deliberate denial-of-service defense against a maliciously large or corrupted file consuming unbounded memory. +- `src/xrpld/app/misc/detail/ValidatorList.cpp` uses `writeFileContents` to persist the current validator list as styled JSON after an update. + +The `maxSize` parameter's real motivation is visible in the `WorkFile` usage: without it, a 4 GB file at a validator list URL would allocate 4 GB of heap before the caller could inspect the error. The pre-check using `file_size()` is a TOCTOU (time-of-check/time-of-use) race in theory, but in practice the files involved are either local config files or freshly downloaded files in a controlled temp location, making the race window negligible. + +## Relationship to the `basics` Module + +Within `include/xrpl/basics/`, this header occupies the narrowest role: it is a leaf utility with no dependencies on other XRPL types. It depends only on Boost.Filesystem and ``, making it safe to include anywhere in the stack without pulling in heavier XRPL headers. The `ByteUtilities.h` header (which provides `kilobytes()` and `megabytes()`) is the natural companion when callers need to express size limits in readable units, as the test suite and `WorkFile` both demonstrate. \ No newline at end of file diff --git a/include/xrpl/basics/IntrusivePointer.h.ai.json b/include/xrpl/basics/IntrusivePointer.h.ai.json new file mode 100644 index 0000000000..ba9bede458 --- /dev/null +++ b/include/xrpl/basics/IntrusivePointer.h.ai.json @@ -0,0 +1,115 @@ +{ + "args": [ + { + "lineno": 61, + "name": "T* p" + }, + { + "lineno": 61, + "name": "TAdoptTag" + }, + { + "lineno": 63, + "name": "SharedIntrusive const& rhs" + }, + { + "lineno": 67, + "name": "SharedIntrusive const& rhs" + }, + { + "lineno": 70, + "name": "SharedIntrusive&& rhs" + }, + { + "lineno": 74, + "name": "SharedIntrusive&& rhs" + }, + { + "lineno": 196, + "name": "TT" + }, + { + "lineno": 196, + "name": "Args&&... args" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "StaticCastTagSharedIntrusive" + }, + { + "args": [], + "lineno": 19, + "name": "DynamicCastTagSharedIntrusive" + }, + { + "args": [], + "lineno": 27, + "name": "SharedIntrusiveAdoptIncrementStrongTag" + }, + { + "args": [], + "lineno": 34, + "name": "SharedIntrusiveAdoptNoIncrementTag" + }, + { + "args": [ + "T* p, TAdoptTag", + "SharedIntrusive const& rhs", + "SharedIntrusive const& rhs", + "SharedIntrusive&& rhs", + "SharedIntrusive&& rhs", + "StaticCastTagSharedIntrusive, SharedIntrusive const& rhs", + "StaticCastTagSharedIntrusive, SharedIntrusive&& rhs", + "DynamicCastTagSharedIntrusive, SharedIntrusive const& rhs", + "DynamicCastTagSharedIntrusive, SharedIntrusive&& rhs" + ], + "lineno": 54, + "name": "SharedIntrusive" + }, + { + "args": [ + "WeakIntrusive const& rhs", + "WeakIntrusive&& rhs", + "SharedIntrusive const& rhs", + "SharedIntrusive const&& rhs" + ], + "lineno": 151, + "name": "WeakIntrusive" + }, + { + "args": [ + "SharedWeakUnion const& rhs", + "SharedIntrusive const& rhs", + "SharedWeakUnion&& rhs", + "SharedIntrusive&& rhs" + ], + "lineno": 210, + "name": "SharedWeakUnion" + } + ], + "description": "This file implements shared and weak intrusive pointer classes (SharedIntrusive, WeakIntrusive, SharedWeakUnion) for reference-counted memory management, including utilities for casting and pointer creation, primarily for use in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/IntrusivePointer.h", + "functions": [ + { + "args": [ + "Args&&... args" + ], + "lineno": 196, + "name": "make_SharedIntrusive" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 222, + "name": "intr_ptr" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/IntrusivePointer.h.ai.md b/include/xrpl/basics/IntrusivePointer.h.ai.md new file mode 100644 index 0000000000..41f1aff705 --- /dev/null +++ b/include/xrpl/basics/IntrusivePointer.h.ai.md @@ -0,0 +1,52 @@ +# `include/xrpl/basics/IntrusivePointer.h` + +This header defines XRPL's custom intrusive smart pointer system: `SharedIntrusive`, `WeakIntrusive`, and `SharedWeakUnion`. The system was designed specifically for `SHAMapInnerNode` — the inner nodes of the radix-16 Merkle trie at the heart of ledger state — but is general enough to serve other reference-counted types. The driver for building this rather than using `std::shared_ptr` is a lifecycle feature called the *partial destructor*, combined with a memory-efficient combined strong/weak pointer variant. + +## Why Not `std::shared_ptr`? + +The file's own comment names the key difference clearly. With `std::shared_ptr` created via `make_shared`, the control block (which contains both the strong and weak counts) lives alongside the object in a single allocation. That allocation is not reclaimed until both the strong *and* weak counts hit zero. So if something holds a `std::weak_ptr` to an inner node, the node's full allocation — including its 16 child pointers — stays live even after the last `shared_ptr` drops. For the SHAMap this is expensive: each inner node can hold up to 16 child `SharedIntrusive` pointers. The partial destructor mechanism exists specifically to release those children as soon as the strong count falls to zero, leaving only a shell waiting for the weak count to drain. + +## Reference Count Layout + +The actual counters live in `IntrusiveRefCounts` (`IntrusiveRefCounts.h`), which must be a base class of any type `T` used with these pointers. A single `std::atomic` field encodes four things: + +- **Bits 0–15**: strong count (up to 65535 owners) +- **Bits 16–29**: weak count (14 bits, up to 16383 weak holders) +- **Bit 30**: `partialDestroyStarted` flag +- **Bit 31**: `partialDestroyFinished` flag + +Packing counts and flags into one atomic integer means `releaseStrongRef()` can atomically decrement the count *and* set the `partialDestroyStarted` flag in a single CAS loop, avoiding a TOCTOU window where two threads could both decide to trigger partial destruction. The two flags are required to safely sequence concurrent partial- and full-destruction: the last weak pointer release spins on `atomic::wait()` if the partial destructor has started but not yet finished, preventing `delete` from racing with `partialDestructor()`. + +## `SharedIntrusive` — The Strong Pointer + +`SharedIntrusive` holds a raw `T* ptr_` whose lifetime is controlled by an intrusive strong count on `*ptr_`. Copy construction calls `ptr_->addStrongRef()`; move construction steals via `unsafeExchange(nullptr)` without touching the count. When the last strong holder releases (`unsafeReleaseAndStore(nullptr)` called from destructor or `reset()`), `releaseStrongRef()` returns one of three `ReleaseStrongRefAction` values: + +- `noop` — other strong holders remain +- `destroy` — both counts are zero; `delete prev` +- `partialDestroy` — weak holders remain; call `prev->partialDestructor()` then `partialDestructorFinished(&prev)` + +The call to `partialDestructorFinished` is the responsibility of the smart pointer class, not the pointee's `partialDestructor()`. This deliberate separation — noted in comments — forces every new `partialDestructor` implementation to explicitly arrange that call, making it harder to accidentally omit the step that wakes waiting threads. + +The `unsafe*` private methods (`unsafeGetRawPtr`, `unsafeSetRawPtr`, `unsafeExchange`, `unsafeReleaseAndStore`) are named with the "unsafe" prefix not because they are dangerous in isolation, but as an architectural seam: the comment explicitly anticipates a future patch where `ptr_` might become `std::atomic`, and isolating all direct pointer access through these methods makes such a change localized. + +## Adopt Tags and `make_SharedIntrusive` + +Two tag types — `SharedIntrusiveAdoptIncrementStrongTag` and `SharedIntrusiveAdoptNoIncrementTag` — control whether adopting a raw pointer bumps the strong count. `make_SharedIntrusive()` allocates a new object with `new TT(...)` and wraps it with `NoIncrement`. This is correct because `IntrusiveRefCounts` initializes its atomic field to `strongDelta` (= 1), meaning the object is born with a strong count of one; incrementing again would be a double-count. The `static_assert` in `make_SharedIntrusive` verifies that the adopting constructor is `noexcept`, since a throw after the raw `new` but before the pointer is wrapped would leak the allocation. + +## Cast Tags + +`StaticCastTagSharedIntrusive` and `DynamicCastTagSharedIntrusive` are dispatch tags for cast-constructors, enabling the `intr_ptr::static_pointer_cast()` and `intr_ptr::dynamic_pointer_cast()` free functions. The move variant of the dynamic-cast constructor handles failure carefully: it uses `unsafeExchange` to steal the pointer from `rhs`, attempts `dynamic_cast`, and if it fails, exchanges the pointer back into `rhs` so ownership is not lost. + +## `WeakIntrusive` — The Weak Pointer + +`WeakIntrusive` mirrors the weak semantics of `std::weak_ptr`. Copy construction calls `ptr_->addWeakRef()`; the destructor calls `unsafeReleaseNoStore()` which invokes `releaseWeakRef()`. The interesting method is `lock()`: it calls `checkoutStrongRefFromWeak()`, a CAS loop that increments the strong count only if it is already non-zero. If the strong count has already hit zero the lock fails and an empty `SharedIntrusive` is returned. Note that copy assignment from a `WeakIntrusive` is deleted — the comment explains this was omitted to simplify the implementation since no current use case required it. + +## `SharedWeakUnion` — The Tagged Pointer + +`SharedWeakUnion` is the most architecturally unusual piece. It stores both the pointer value and a strong/weak discriminator inside a single `uintptr_t` field `tp_` by using pointer tagging: if the low bit is `1`, the pointer represents a weak reference; if it is `0`, a strong reference. This works because `alignof(T) >= 2` is statically asserted, guaranteeing the low bit of any valid `T*` is always zero. + +The practical value is for tagged caches, where a cache slot should hold a strong pointer when the object is actively needed but can downgrade to a weak pointer to allow eviction without cache churn. `convertToStrong()` and `convertToWeak()` perform in-place promotion and demotion: `convertToStrong()` atomically promotes a weak checkout to a strong reference using `checkoutStrongRefFromWeak()` then releases the weak count; `convertToWeak()` uses the atomic `addWeakReleaseStrongRef()` operation to swap one strong count for one weak count in a single CAS loop, handling the `partialDestroy` case that arises if this was the very last strong pointer. The `lock()` method unifies weak and strong paths: if already strong, increment and return; if weak, attempt a checkout. + +## `intr_ptr` Namespace + +The nested `intr_ptr` namespace provides `std::shared_ptr`-style vocabulary aliases — `SharedPtr`, `WeakPtr`, `SharedWeakUnionPtr`, `make_shared()`, `static_pointer_cast()`, `dynamic_pointer_cast()` — used throughout the SHAMap subsystem. `SHAMapInnerNode` stores its 16 children as `intr_ptr::SharedPtr` and exposes `partialDestructor()` to reset them when the last strong holder drops. \ No newline at end of file diff --git a/include/xrpl/basics/IntrusivePointer.ipp.ai.json b/include/xrpl/basics/IntrusivePointer.ipp.ai.json new file mode 100644 index 0000000000..5aa958ec3b --- /dev/null +++ b/include/xrpl/basics/IntrusivePointer.ipp.ai.json @@ -0,0 +1,698 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SharedIntrusive::SharedIntrusive(T* p, TAdoptTag)", + "if constexpr (std::is_same_v)", + "if (p)", + "p->addStrongRef()" + ], + "entry_point": "SharedIntrusive::SharedIntrusive(T* p, TAdoptTag)", + "purpose": "Constructs a SharedIntrusive from a raw pointer, optionally incrementing the strong ref count if adopting.", + "validation_points": [ + "if (p) // Validates pointer before addStrongRef" + ] + }, + { + "call_chain": [ + "SharedIntrusive::SharedIntrusive(SharedIntrusive const& rhs)", + "rhs.unsafeGetRawPtr()", + "if (p)", + "p->addStrongRef()" + ], + "entry_point": "SharedIntrusive::SharedIntrusive(SharedIntrusive const& rhs)", + "purpose": "Copy constructor, increments strong ref if pointer is not null.", + "validation_points": [ + "if (p) // Validates pointer before addStrongRef" + ] + }, + { + "call_chain": [ + "SharedIntrusive::operator=(SharedIntrusive const& rhs)", + "if (this == &rhs)", + "rhs.unsafeGetRawPtr()", + "if (p)", + "p->addStrongRef()", + "unsafeReleaseAndStore(p)" + ], + "entry_point": "SharedIntrusive::operator=(SharedIntrusive const& rhs)", + "purpose": "Assignment operator, handles self-assignment, increments ref if needed, releases old pointer.", + "validation_points": [ + "if (this == &rhs) // Self-assignment check", + "if (p) // Validates pointer before addStrongRef" + ] + }, + { + "call_chain": [ + "SharedIntrusive::operator=(SharedIntrusive const& rhs)", + "if constexpr (std::is_same_v)", + "if (this == &rhs)", + "rhs.unsafeGetRawPtr()", + "if (p)", + "p->addStrongRef()", + "unsafeReleaseAndStore(p)" + ], + "entry_point": "SharedIntrusive::operator=(SharedIntrusive const& rhs)", + "purpose": "Assignment from convertible type, handles self-assignment, increments ref if needed, releases old pointer.", + "validation_points": [ + "if (this == &rhs) // Self-assignment check (if T == TT)", + "if (p) // Validates pointer before addStrongRef" + ] + }, + { + "call_chain": [ + "SharedIntrusive::adopt(T* p)", + "if constexpr (std::is_same_v)", + "if (p)", + "p->addStrongRef()", + "unsafeReleaseAndStore(p)" + ], + "entry_point": "SharedIntrusive::adopt(T* p)", + "purpose": "Adopts a raw pointer, optionally increments ref count, releases old pointer.", + "validation_points": [ + "if (p) // Validates pointer before addStrongRef" + ] + } + ], + "data_flows": [ + { + "field": "ptr_", + "flow": [ + "T* p (input or from rhs)", + "if (p) validation", + "p->addStrongRef() (if validated)", + "ptr_ = p" + ], + "origin": "Constructor argument (T* p) or rhs.unsafeGetRawPtr()", + "transformations": [ + "Pointer is checked for null", + "Reference count incremented if not null", + "Stored in ptr_" + ], + "validated_at": "if (p) before addStrongRef" + }, + { + "field": "rhs (SharedIntrusive const& rhs or SharedIntrusive const& rhs)", + "flow": [ + "rhs (input)", + "if (this == &rhs) validation (self-assignment)", + "rhs.unsafeGetRawPtr()", + "if (p) validation", + "p->addStrongRef()", + "unsafeReleaseAndStore(p)" + ], + "origin": "Function argument", + "transformations": [ + "Self-assignment check", + "Pointer extracted", + "Reference count incremented if not null", + "Old pointer released and replaced" + ], + "validated_at": "if (this == &rhs), if (p)" + }, + { + "field": "T* p (adopt)", + "flow": [ + "T* p (input)", + "if (p) validation", + "p->addStrongRef() (if validated)", + "unsafeReleaseAndStore(p)" + ], + "origin": "adopt(T* p) argument", + "transformations": [ + "Pointer is checked for null", + "Reference count incremented if not null", + "Old pointer released and replaced" + ], + "validated_at": "if (p)" + } + ], + "description": "Implements the definitions for intrusive smart pointer types (SharedIntrusive, WeakIntrusive, SharedWeakUnion) used for reference-counted memory management in the xrpl namespace.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "template type parameters (via static_assert and if constexpr)", + "validation", + "missing", + "check" + ], + "evidence": "Field template type parameters (via static_assert and if constexpr) validated by C++ type system, manual null checks, static_assert", + "issue_pattern": "Missing validation for template type parameters (via static_assert and if constexpr)", + "why_false_positive": "C++ type system, manual null checks, static_assert validates template type parameters (via static_assert and if constexpr) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "pointer nullness (via if (p))", + "validation", + "missing", + "check" + ], + "evidence": "Field pointer nullness (via if (p)) validated by C++ type system, manual null checks, static_assert", + "issue_pattern": "Missing validation for pointer nullness (via if (p))", + "why_false_positive": "C++ type system, manual null checks, static_assert validates pointer nullness (via if (p)) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "p (pointer to T)", + "empty", + "string", + "validation" + ], + "evidence": "if (p) at SharedIntrusive::SharedIntrusive(T* p, TAdoptTag) noexcept", + "issue_pattern": "Missing empty string validation for p (pointer to T)", + "why_false_positive": "if (p) validates p (pointer to T) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rhs (SharedIntrusive const& rhs)", + "empty", + "string", + "validation" + ], + "evidence": "if (p) at SharedIntrusive::SharedIntrusive(SharedIntrusive const& rhs)", + "issue_pattern": "Missing empty string validation for rhs (SharedIntrusive const& rhs)", + "why_false_positive": "if (p) validates rhs (SharedIntrusive const& rhs) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rhs (SharedIntrusive const& rhs)", + "empty", + "string", + "validation" + ], + "evidence": "if (p) at SharedIntrusive::SharedIntrusive(SharedIntrusive const& rhs)", + "issue_pattern": "Missing empty string validation for rhs (SharedIntrusive const& rhs)", + "why_false_positive": "if (p) validates rhs (SharedIntrusive const& rhs) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "this and rhs (self-assignment)", + "empty", + "string", + "validation" + ], + "evidence": "if (this == &rhs) at SharedIntrusive::operator=(SharedIntrusive const& rhs)", + "issue_pattern": "Missing empty string validation for this and rhs (self-assignment)", + "why_false_positive": "if (this == &rhs) validates this and rhs (self-assignment) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "this and rhs (self-assignment)", + "empty", + "string", + "validation" + ], + "evidence": "if (this == &rhs) at SharedIntrusive::operator=(SharedIntrusive const& rhs)", + "issue_pattern": "Missing empty string validation for this and rhs (self-assignment)", + "why_false_positive": "if (this == &rhs) validates this and rhs (self-assignment) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "this and rhs (self-assignment)", + "empty", + "string", + "validation" + ], + "evidence": "if (this == &rhs) at SharedIntrusive::operator=(SharedIntrusive&& rhs)", + "issue_pattern": "Missing empty string validation for this and rhs (self-assignment)", + "why_false_positive": "if (this == &rhs) validates this and rhs (self-assignment) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "TT (template type parameter)", + "empty", + "string", + "validation" + ], + "evidence": "static_assert(!std::is_same_v, ...) at SharedIntrusive::operator=(SharedIntrusive&& rhs)", + "issue_pattern": "Missing empty string validation for TT (template type parameter)", + "why_false_positive": "static_assert(!std::is_same_v, ...) validates TT (template type parameter) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "TT (template type parameter)", + "type", + "validation", + "check" + ], + "evidence": "static_assert(!std::is_same_v, ...) at SharedIntrusive::operator=(SharedIntrusive&& rhs)", + "issue_pattern": "Missing type validation for TT (template type parameter)", + "why_false_positive": "static_assert(!std::is_same_v, ...) validates TT (template type parameter) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "TAdoptTag (template type parameter)", + "empty", + "string", + "validation" + ], + "evidence": "if constexpr (std::is_same_v) at SharedIntrusive::SharedIntrusive(T* p, TAdoptTag) noexcept and adopt(T* p)", + "issue_pattern": "Missing empty string validation for TAdoptTag (template type parameter)", + "why_false_positive": "if constexpr (std::is_same_v) validates TAdoptTag (template type parameter) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "TAdoptTag (template type parameter)", + "type", + "validation", + "check" + ], + "evidence": "if constexpr (std::is_same_v) at SharedIntrusive::SharedIntrusive(T* p, TAdoptTag) noexcept and adopt(T* p)", + "issue_pattern": "Missing type validation for TAdoptTag (template type parameter)", + "why_false_positive": "if constexpr (std::is_same_v) validates TAdoptTag (template type parameter) type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/IntrusivePointer.ipp", + "functions": [ + { + "args": [ + "SharedIntrusive const& rhs" + ], + "lineno": 61, + "name": "SharedIntrusive::operator=" + }, + { + "args": [ + "SharedIntrusive const& rhs" + ], + "lineno": 80, + "name": "SharedIntrusive::operator=" + }, + { + "args": [ + "SharedIntrusive&& rhs" + ], + "lineno": 99, + "name": "SharedIntrusive::operator=" + }, + { + "args": [ + "SharedIntrusive&& rhs" + ], + "lineno": 110, + "name": "SharedIntrusive::operator=" + }, + { + "args": [ + "std::nullptr_t" + ], + "lineno": 120, + "name": "SharedIntrusive::operator!=" + }, + { + "args": [ + "std::nullptr_t" + ], + "lineno": 126, + "name": "SharedIntrusive::operator==" + }, + { + "args": [ + "T* p" + ], + "lineno": 132, + "name": "SharedIntrusive::adopt" + }, + { + "args": [], + "lineno": 142, + "name": "SharedIntrusive::~SharedIntrusive" + }, + { + "args": [], + "lineno": 170, + "name": "SharedIntrusive::operator*" + }, + { + "args": [], + "lineno": 176, + "name": "SharedIntrusive::operator->" + }, + { + "args": [], + "lineno": 182, + "name": "SharedIntrusive::operator bool" + }, + { + "args": [], + "lineno": 188, + "name": "SharedIntrusive::reset" + }, + { + "args": [], + "lineno": 193, + "name": "SharedIntrusive::get" + }, + { + "args": [], + "lineno": 198, + "name": "SharedIntrusive::use_count" + }, + { + "args": [], + "lineno": 206, + "name": "SharedIntrusive::unsafeGetRawPtr" + }, + { + "args": [ + "T* p" + ], + "lineno": 211, + "name": "SharedIntrusive::unsafeSetRawPtr" + }, + { + "args": [ + "T* p" + ], + "lineno": 216, + "name": "SharedIntrusive::unsafeExchange" + }, + { + "args": [ + "T* next" + ], + "lineno": 221, + "name": "SharedIntrusive::unsafeReleaseAndStore" + }, + { + "args": [ + "SharedIntrusive const& rhs" + ], + "lineno": 265, + "name": "WeakIntrusive::operator=" + }, + { + "args": [ + "T* ptr" + ], + "lineno": 274, + "name": "WeakIntrusive::adopt" + }, + { + "args": [], + "lineno": 280, + "name": "WeakIntrusive::~WeakIntrusive" + }, + { + "args": [], + "lineno": 285, + "name": "WeakIntrusive::lock" + }, + { + "args": [], + "lineno": 294, + "name": "WeakIntrusive::expired" + }, + { + "args": [], + "lineno": 299, + "name": "WeakIntrusive::reset" + }, + { + "args": [], + "lineno": 304, + "name": "WeakIntrusive::unsafeReleaseNoStore" + }, + { + "args": [ + "SharedWeakUnion const& rhs" + ], + "lineno": 353, + "name": "SharedWeakUnion::operator=" + }, + { + "args": [ + "SharedIntrusive const& rhs" + ], + "lineno": 380, + "name": "SharedWeakUnion::operator=" + }, + { + "args": [ + "SharedIntrusive&& rhs" + ], + "lineno": 391, + "name": "SharedWeakUnion::operator=" + }, + { + "args": [], + "lineno": 399, + "name": "SharedWeakUnion::~SharedWeakUnion" + }, + { + "args": [], + "lineno": 404, + "name": "SharedWeakUnion::getStrong" + }, + { + "args": [], + "lineno": 415, + "name": "SharedWeakUnion::operator bool" + }, + { + "args": [], + "lineno": 420, + "name": "SharedWeakUnion::reset" + }, + { + "args": [], + "lineno": 425, + "name": "SharedWeakUnion::get" + }, + { + "args": [], + "lineno": 430, + "name": "SharedWeakUnion::use_count" + }, + { + "args": [], + "lineno": 437, + "name": "SharedWeakUnion::expired" + }, + { + "args": [], + "lineno": 442, + "name": "SharedWeakUnion::lock" + }, + { + "args": [], + "lineno": 463, + "name": "SharedWeakUnion::isStrong" + }, + { + "args": [], + "lineno": 468, + "name": "SharedWeakUnion::isWeak" + }, + { + "args": [], + "lineno": 473, + "name": "SharedWeakUnion::convertToStrong" + }, + { + "args": [], + "lineno": 491, + "name": "SharedWeakUnion::convertToWeak" + }, + { + "args": [], + "lineno": 517, + "name": "SharedWeakUnion::unsafeGetRawPtr" + }, + { + "args": [ + "T* p", + "RefStrength rs" + ], + "lineno": 522, + "name": "SharedWeakUnion::unsafeSetRawPtr" + }, + { + "args": [ + "std::nullptr_t" + ], + "lineno": 528, + "name": "SharedWeakUnion::unsafeSetRawPtr" + }, + { + "args": [], + "lineno": 532, + "name": "SharedWeakUnion::unsafeReleaseNoStore" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is a low-level utility for intrusive reference counting. Typical tests would be in files like IntrusivePointer_test.cpp or SharedIntrusive_test.cpp, likely under the 'test' or 'unittest' directories. Tests should cover: construction from raw pointer (null and non-null), copy/move construction, assignment (including self-assignment), adopt(), and destruction. Gaps may exist in testing edge cases such as self-assignment, null pointer handling, and cross-type assignment. No direct evidence of test files is present in this snippet, so coverage should be verified in the codebase.", + "validation_architecture": { + "auto_validated_fields": [ + "template type parameters (via static_assert and if constexpr)", + "pointer nullness (via if (p))" + ], + "framework": "C++ type system, manual null checks, static_assert", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (conditional logic only)", + "field": "p (pointer to T)", + "location": "SharedIntrusive::SharedIntrusive(T* p, TAdoptTag) noexcept", + "validated_by": "if (p)", + "validates": [ + "Checks if pointer p is not null before calling p->addStrongRef()" + ], + "validation_type": "null check" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logic only)", + "field": "rhs (SharedIntrusive const& rhs)", + "location": "SharedIntrusive::SharedIntrusive(SharedIntrusive const& rhs)", + "validated_by": "if (p)", + "validates": [ + "Checks if pointer p (from rhs.unsafeGetRawPtr()) is not null before calling p->addStrongRef()" + ], + "validation_type": "null check" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logic only)", + "field": "rhs (SharedIntrusive const& rhs)", + "location": "SharedIntrusive::SharedIntrusive(SharedIntrusive const& rhs)", + "validated_by": "if (p)", + "validates": [ + "Checks if pointer p (from rhs.unsafeGetRawPtr()) is not null before calling p->addStrongRef()" + ], + "validation_type": "null check" + }, + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "this and rhs (self-assignment)", + "location": "SharedIntrusive::operator=(SharedIntrusive const& rhs)", + "validated_by": "if (this == &rhs)", + "validates": [ + "Checks for self-assignment to avoid unnecessary operations" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "this and rhs (self-assignment)", + "location": "SharedIntrusive::operator=(SharedIntrusive const& rhs)", + "validated_by": "if (this == &rhs)", + "validates": [ + "Checks for self-assignment to avoid unnecessary operations (only if T == TT)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "this and rhs (self-assignment)", + "location": "SharedIntrusive::operator=(SharedIntrusive&& rhs)", + "validated_by": "if (this == &rhs)", + "validates": [ + "Checks for self-assignment to avoid unnecessary operations" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "static_assert failure (compile-time error)", + "field": "TT (template type parameter)", + "location": "SharedIntrusive::operator=(SharedIntrusive&& rhs)", + "validated_by": "static_assert(!std::is_same_v, ...)", + "validates": [ + "Ensures that this overload is not instantiated for T == TT" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (compile-time conditional)", + "field": "TAdoptTag (template type parameter)", + "location": "SharedIntrusive::SharedIntrusive(T* p, TAdoptTag) noexcept and adopt(T* p)", + "validated_by": "if constexpr (std::is_same_v)", + "validates": [ + "Ensures that addStrongRef() is only called if TAdoptTag is SharedIntrusiveAdoptIncrementStrongTag" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/IntrusivePointer.ipp.ai.md b/include/xrpl/basics/IntrusivePointer.ipp.ai.md new file mode 100644 index 0000000000..71ef2b89c9 --- /dev/null +++ b/include/xrpl/basics/IntrusivePointer.ipp.ai.md @@ -0,0 +1,64 @@ +# `IntrusivePointer.ipp` — Intrusive Smart Pointer Method Definitions + +This file provides the out-of-line template method bodies for three intrusive smart pointer classes declared in `IntrusivePointer.h`: `SharedIntrusive`, `WeakIntrusive`, and `SharedWeakUnion`. As is conventional for C++ template implementations that must be visible to all translation units, the definitions live in a `.ipp` file that `IntrusivePointer.h` includes, rather than in a `.cpp` file. + +## Why Not `std::shared_ptr`? + +The comment on `SharedIntrusive` in the header explains the core motivation: the XRPL codebase needs a smart pointer that can release an object's *data* when the last strong reference drops, while deferring the *memory* release until the last weak reference also drops. `std::shared_ptr` guarantees that the destructor runs at strong-count zero, but an implementation using `make_shared` keeps the entire memory block alive until weak-count zero. More importantly, `std::shared_ptr` provides no hook between "last strong gone" and "last weak gone." + +The intrusive design solves this by embedding reference counts directly in the pointee via `IntrusiveRefCounts` (a base struct the controlled type must inherit). When the strong count reaches zero, the pointer machinery calls a user-defined `partialDestructor()` — which can, for instance, reset `SHAMapInnerNode`'s child-pointer array to free the expensive working data — while the object shell continues to exist as long as any weak pointer holds on. Only when the weak count also reaches zero does `delete` run. + +## `SharedIntrusive` — The Strong Pointer + +Construction and assignment follow a consistent pattern: acquire the new ref before releasing the old. The copy constructor uses a lambda initializer to atomically call `addStrongRef()` before storing the pointer in `ptr_`, ensuring the count is correct even if the lambda result is used to initialize a field directly: + +```cpp +ptr_{[&] { + auto p = rhs.unsafeGetRawPtr(); + if (p) p->addStrongRef(); + return p; +}()} +``` + +Move construction is cheaper: it calls `unsafeExchange(nullptr)` on the source to steal the pointer without touching ref counts. The `static_assert` in the heterogeneous move-assignment operator enforces at compile time that the same-type case is handled by the homogeneous overload, preventing this overload from being instantiated for `T == TT`. + +### The `TAdoptTag` Pattern + +The `adopt(T* p)` method and the raw-pointer constructor both take a `TAdoptTag` template parameter constrained by the `CAdoptTag` concept. Two tags exist: `SharedIntrusiveAdoptIncrementStrongTag` increments the strong count (for absorbing a raw pointer that hasn't had its count bumped yet), and `SharedIntrusiveAdoptNoIncrementTag` adopts the pointer without incrementing. `make_SharedIntrusive` allocates via `new T` — `IntrusiveRefCounts` initializes the strong count to 1 — and then adopts with `SharedIntrusiveAdoptNoIncrementTag` to avoid a double-count. The `noexcept` guarantee on that constructor path is enforced with a `static_assert` inside `make_SharedIntrusive` to prevent memory leaks if construction were to throw after allocation. + +### `unsafeReleaseAndStore` — The Core Destruction Path + +Every operation that replaces the stored pointer funnels through `unsafeReleaseAndStore(T* next)`. It atomically swaps the new pointer in via `std::exchange`, then calls `releaseStrongRef()` on the evicted pointer. The return value is a `ReleaseStrongRefAction` enum with three values: + +- `noop` — other strong pointers remain; do nothing. +- `destroy` — no strong or weak pointers remain; call `delete`. +- `partialDestroy` — the weak count is non-zero; call `partialDestructor()` then `partialDestructorFinished()`. + +The `partialDestructorFinished` template friend function sets the `partialDestroyFinishedMask` bit atomically on `IntrusiveRefCounts::refCounts`, and if the weak count has already reached zero it calls `notify_one()` to wake any thread that is waiting in `releaseWeakRef()` for the partial destructor to complete before running the full destructor. + +### Cast Constructors + +`SharedIntrusive` supports both `static_cast` and `dynamic_cast` construction from a `SharedIntrusive`. For the move variant of `dynamic_cast`, there is a subtle correctness invariant: if `dynamic_cast` returns null (the cast fails), the source pointer is restored via `rhs.unsafeExchange(toSet)` to prevent the controlled object from leaking. A code comment notes that the `unsafeExchange` structure is also kept in anticipation of a future atomic pointer mode. + +## `WeakIntrusive` — The Weak Pointer + +`WeakIntrusive` manages a non-owning reference via `addWeakRef()` and `releaseWeakRef()`. Two deliberate omissions in the interface are worth noting: + +- Copy assignment from another `WeakIntrusive` is `delete`d. The header comment explains this is because there are currently no use cases, and omitting it simplifies implementation. It can be reintroduced if needed. +- There is no move constructor from `SharedIntrusive&&`. Moving a strong pointer into a weak pointer would require decrementing the strong count and adding a weak count, making it *more* expensive than copying the raw pointer and adding a weak ref. The deleted overload prevents this surprising hidden cost. + +`lock()` calls `checkoutStrongRefFromWeak()` on the raw pointer, which uses a CAS loop to atomically increment the strong count only if it is currently non-zero. On success, the new `SharedIntrusive` is constructed with `SharedIntrusiveAdoptNoIncrementTag` — the checkout already performed the increment, so a second increment must not occur. + +## `SharedWeakUnion` — The Tagged-Pointer Union + +`SharedWeakUnion` packs both a strong and weak reference into the space of a single pointer word. It stores the pointer as a `std::uintptr_t` called `tp_`, uses the low bit as a tag (1 = weak, 0 = strong), and recovers the raw pointer by masking with `ptrMask = ~1`. A `static_assert` on `alignof(T) >= 2` enforces that the actual pointer will never set the low bit, keeping the encoding sound. + +`unsafeGetRawPtr()` applies the mask; `unsafeSetRawPtr(T*, RefStrength)` stores the pointer and conditionally ORs in the tag bit. `isStrong()` / `isWeak()` read the tag bit directly. + +`convertToStrong()` and `convertToWeak()` allow in-place reference strength switching. `convertToWeak()` uses `addWeakReleaseStrongRef()` — an atomic operation on `IntrusiveRefCounts` that adds a weak delta and subtracts a strong delta in one CAS loop — to avoid a window where the strong count is zero but the weak count hasn't been incremented yet. If the result is `partialDestroy`, `convertToWeak` handles the two-phase partial destruction, including the `partialDestructorFinished` call that clears the pointer variable. + +`get()` returns the raw pointer only if the union holds a strong reference; calling `get()` on a weak-tagged union returns null. `lock()` unifies both cases: if already strong, it bumps the strong count and returns; if weak, it attempts `checkoutStrongRefFromWeak()` and adopts without increment on success. + +## Naming Convention for Primitives + +All methods prefixed with `unsafe` are private and skip reference counting entirely. They manipulate the raw pointer field directly. This naming convention serves two purposes: it makes the separation between raw pointer mechanics and safe counted semantics immediately visible during code review, and the header comments note that these wrappers exist in anticipation of a future patch to support atomic pointer storage (which would require replacing `std::exchange` with `std::atomic::exchange` inside these one-line helpers). \ No newline at end of file diff --git a/include/xrpl/basics/IntrusiveRefCounts.h.ai.json b/include/xrpl/basics/IntrusiveRefCounts.h.ai.json new file mode 100644 index 0000000000..c15d9a8e92 --- /dev/null +++ b/include/xrpl/basics/IntrusiveRefCounts.h.ai.json @@ -0,0 +1,118 @@ +{ + "args": [ + { + "lineno": 272, + "name": "v" + }, + { + "lineno": 282, + "name": "s" + }, + { + "lineno": 282, + "name": "w" + }, + { + "lineno": 299, + "name": "o" + } + ], + "classes": [ + { + "args": [], + "lineno": 38, + "name": "IntrusiveRefCounts" + }, + { + "args": [ + "FieldType v", + "CountType s, CountType w" + ], + "lineno": 101, + "name": "IntrusiveRefCounts::RefCountPair" + } + ], + "description": "Implements atomic reference counting for intrusive smart pointers, including strong and weak reference management, partial destruction, and thread-safe operations for use in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/IntrusiveRefCounts.h", + "functions": [ + { + "args": [], + "lineno": 120, + "name": "IntrusiveRefCounts::addStrongRef" + }, + { + "args": [], + "lineno": 125, + "name": "IntrusiveRefCounts::addWeakRef" + }, + { + "args": [], + "lineno": 130, + "name": "IntrusiveRefCounts::releaseStrongRef" + }, + { + "args": [], + "lineno": 170, + "name": "IntrusiveRefCounts::addWeakReleaseStrongRef" + }, + { + "args": [], + "lineno": 210, + "name": "IntrusiveRefCounts::releaseWeakRef" + }, + { + "args": [], + "lineno": 235, + "name": "IntrusiveRefCounts::checkoutStrongRefFromWeak" + }, + { + "args": [], + "lineno": 248, + "name": "IntrusiveRefCounts::expired" + }, + { + "args": [], + "lineno": 253, + "name": "IntrusiveRefCounts::use_count" + }, + { + "args": [], + "lineno": 258, + "name": "IntrusiveRefCounts::~IntrusiveRefCounts" + }, + { + "args": [ + "IntrusiveRefCounts::FieldType v" + ], + "lineno": 271, + "name": "IntrusiveRefCounts::RefCountPair::RefCountPair" + }, + { + "args": [ + "IntrusiveRefCounts::CountType s", + "IntrusiveRefCounts::CountType w" + ], + "lineno": 281, + "name": "IntrusiveRefCounts::RefCountPair::RefCountPair" + }, + { + "args": [], + "lineno": 289, + "name": "IntrusiveRefCounts::RefCountPair::combinedValue" + }, + { + "args": [ + "T** o" + ], + "lineno": 299, + "name": "partialDestructorFinished" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/IntrusiveRefCounts.h.ai.md b/include/xrpl/basics/IntrusiveRefCounts.h.ai.md new file mode 100644 index 0000000000..e37173a30f --- /dev/null +++ b/include/xrpl/basics/IntrusiveRefCounts.h.ai.md @@ -0,0 +1,56 @@ +# `IntrusiveRefCounts.h` — Atomic Reference Counting for Intrusive Smart Pointers + +## Role in the System + +`IntrusiveRefCounts` is the reference-counting backbone for XRPL's custom intrusive smart pointer family: `SharedIntrusive`, `WeakIntrusive`, and `SharedWeakUnion`, all defined in `IntrusivePointer.h`. A class participates in this system simply by inheriting from `IntrusiveRefCounts`; the notable consumer is `SHAMapInnerNode`, whose billions-of-ops-per-second traversal patterns make every byte and every allocation matter. + +The design answers a specific criticism of `std::shared_ptr`: with `make_shared`, the control block and the object are co-allocated, which is efficient, but the memory cannot be reclaimed until the weak count also hits zero. XRPL's intrusive design embeds the counts directly in the object, saves the separate control-block allocation, and — crucially — introduces a *partial destruction* protocol that lets the object shed its expensive payload (e.g., child node pointers) the moment the strong count reaches zero, even while weak pointers keep the shell alive. + +## The Packed Atomic Field + +All state lives in a single `mutable std::atomic refCounts`. The 32 bits are divided as follows: + +| Bits | Field | +|------|-------| +| 0–15 | Strong count (16 bits) | +| 16–29 | Weak count (14 bits) | +| 30 | `partialDestroyStartedBit` | +| 31 | `partialDestroyFinishedBit` | + +Packing everything into one atomic word means that decrementing a count *and* setting a flag can be done as a single compare-and-swap, eliminating any window between the two operations that a second atomic would expose. The helper struct `RefCountPair` wraps the unpack/repack logic: its constructor extracts each field by masking and shifting, and `combinedValue()` reassembles them. + +## Destruction Lifecycle and the Partial Destructor + +When `releaseStrongRef()` finds that the strong count is dropping to exactly one (i.e., to zero), it branches on whether any weak references exist: + +- **No weak refs**: returns `ReleaseStrongRefAction::destroy`. The caller (in `IntrusivePointer.ipp`) runs `delete ptr`, calling the full destructor. +- **Weak refs present**: atomically sets `partialDestroyStartedBit` *in the same CAS that decrements the count*, then returns `partialDestroy`. The caller invokes `ptr->partialDestructor()`, then calls the free function `partialDestructorFinished(&ptr)`. + +This CAS loop in `releaseStrongRef()` almost always executes once; looping is necessary only if another thread modifies `refCounts` between the `load` and `compare_exchange_weak`. + +`partialDestructorFinished()` is a `friend` function template declared in the class. It atomically sets `partialDestroyFinishedBit` via `fetch_or`, then — if the weak count is already zero at that point — calls `refCounts.notify_one()` to wake any thread blocked in `releaseWeakRef()`. The intentional `T**` (double-pointer) signature is a deliberate API ergonomic signal: after the call, `*o` is set to `nullptr` to discourage use-after-free, because another thread may immediately delete the object upon seeing the finished bit. + +## Why Two Bits for Partial Destroy + +There is a genuine race that a single bit cannot handle. Consider: + +1. Thread A: last strong pointer releases → strong count goes to zero, weak count is 1, `partialDestroyStarted` is about to be set (but has not been set yet). +2. Thread B: last weak pointer releases → sees `strong == 0`, `weak == 1 → 0` → would naively call the full destructor, racing with Thread A's partial destructor. + +The `partialDestroyStarted` bit, set atomically in the same CAS that decrements the strong count, prevents Thread B from proceeding until the partial destructor's state is stable. The `partialDestroyFinished` bit then lets `releaseWeakRef()` know it is safe to destroy. When neither bit is set, `releaseWeakRef()` calls `refCounts.wait(…)` — a futex-style block on the atomic value — and rechecks upon wake-up. + +## Key Operations + +**`addStrongRef()` / `addWeakRef()`** use `fetch_add` with `acq_rel` ordering — the common fast path that requires no CAS loop. + +**`addWeakReleaseStrongRef()`** is an atomic composite operation needed when `SharedWeakUnion` converts itself from a strong to a weak reference. Doing these as two separate operations would create a transient moment where the object has neither kind of reference holding it, which could trigger premature destruction. The implementation computes `weakDelta - strongDelta` and applies it as one delta in a CAS loop, while still setting `partialDestroyStartedBit` if appropriate. + +**`checkoutStrongRefFromWeak()`** implements `lock()` semantics: it atomically increments the strong count, but only if the strong count is currently nonzero. If it reaches zero between load and CAS, the loop exits with `false`, signalling that the object is already being destroyed. + +## Design Notes and Tradeoffs + +The `addStrongRef()` is `noexcept` by requirement: `make_SharedIntrusive` calls it immediately after `new T(...)`, and if it could throw, the freshly allocated object would leak. The `static_assert` in `make_SharedIntrusive` enforces this. + +The `uint16_t` strong count cap of 65 535 and 14-bit weak count cap of 16 383 are annotated with a `TODO`: if audit reveals these are insufficient, both types would need to widen to `uint32_t`, moving the entire atomic to `uint64_t`. The `checkStrongMaxValue` and `checkWeakMaxValue` constants leave a 32-unit margin below the hard cap, and debug-mode assertions in `RefCountPair`'s constructors fire before the actual overflow, providing early warning. + +The `partialDestructorFinished()` free function is deliberately *not* called at the end of the `partialDestructor()` virtual method itself. This means any class that inherits `IntrusiveRefCounts` and implements its own `partialDestructor()` must explicitly call `partialDestructorFinished()` when done. The comment explains this was chosen to make the protocol visible at the call site rather than hiding it inside the smart-pointer machinery, reducing the chance that new subclasses silently skip the required notification. \ No newline at end of file diff --git a/include/xrpl/basics/KeyCache.h.ai.json b/include/xrpl/basics/KeyCache.h.ai.json new file mode 100644 index 0000000000..67bfd75bce --- /dev/null +++ b/include/xrpl/basics/KeyCache.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines a type alias KeyCache as a TaggedCache specialized for uint256 keys and int values within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/KeyCache.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/KeyCache.h.ai.md b/include/xrpl/basics/KeyCache.h.ai.md new file mode 100644 index 0000000000..a50ac0baa1 --- /dev/null +++ b/include/xrpl/basics/KeyCache.h.ai.md @@ -0,0 +1,13 @@ +# `KeyCache.h` — Key-Only Cache Type Alias + +`KeyCache.h` provides a single-line type alias that expresses a common, narrow caching pattern throughout the XRPL codebase: track the *presence* of `uint256` keys over time, without attaching any value to them. + +```cpp +using KeyCache = TaggedCache; +``` + +The three template arguments to `TaggedCache` are: the key type (`uint256`, the 256-bit hash used pervasively in XRPL), a value type placeholder (`int`), and the `IsKeyCache` boolean flag set to `true`. That third argument is the critical one. Inside `TaggedCache`, `IsKeyCache = true` activates a compile-time branch via `std::conditional` that substitutes `KeyOnlyEntry` — a struct holding nothing but a `last_access` timestamp — in place of the full `ValueEntry` that carries a `shared_ptr` to an actual object. The `int` value type is therefore never stored or accessed; it exists only to satisfy the template parameter list. + +This design lets callers answer a single yes/no question efficiently: *"Have I seen this hash recently enough that I don't need to re-check it?"* The primary consumer is `FullBelowCache` in `include/xrpl/shamap/FullBelowCache.h`, which uses a `KeyCache` to remember which SHAMap tree nodes have all their descendants resident in the database. When a node is marked "full below," the ledger acquisition machinery can skip redundant subtree traversals, a meaningful performance win during sync. + +Because `KeyCache` is built directly on `TaggedCache`, it inherits the full sweep-based expiry mechanism, thread-safe access under `std::recursive_mutex`, and the `touch_if_exists` / `insert` interface — with `insert` enabled only in the key-only overload path when `IsKeyCache` is `true`. \ No newline at end of file diff --git a/include/xrpl/basics/LocalValue.h.ai.json b/include/xrpl/basics/LocalValue.h.ai.json new file mode 100644 index 0000000000..1e4fd79c70 --- /dev/null +++ b/include/xrpl/basics/LocalValue.h.ai.json @@ -0,0 +1,76 @@ +{ + "args": [ + { + "lineno": 34, + "name": "lvs" + }, + { + "lineno": 53, + "name": "Args... args" + } + ], + "classes": [ + { + "args": [], + "lineno": 9, + "name": "LocalValues" + }, + { + "args": [], + "lineno": 15, + "name": "BasicValue" + }, + { + "args": [ + "T", + "t" + ], + "lineno": 21, + "name": "Value" + }, + { + "args": [ + "Args... args" + ], + "lineno": 52, + "name": "LocalValue" + } + ], + "description": "Implements a thread- and coroutine-local storage utility for storing values specific to the calling thread or coroutine, using boost::thread_specific_ptr and custom value wrappers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/LocalValue.h", + "functions": [ + { + "args": [ + "lvs" + ], + "lineno": 34, + "name": "cleanup" + }, + { + "args": [], + "lineno": 41, + "name": "getLocalValues" + }, + { + "args": [], + "lineno": 67, + "name": "operator*" + }, + { + "args": [], + "lineno": 61, + "name": "operator->" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/LocalValue.h.ai.md b/include/xrpl/basics/LocalValue.h.ai.md new file mode 100644 index 0000000000..c3ab2dbde2 --- /dev/null +++ b/include/xrpl/basics/LocalValue.h.ai.md @@ -0,0 +1,64 @@ +# `LocalValue.h` — Coroutine-Aware Local Storage + +## Role and Motivation + +Standard `thread_local` storage is insufficient in a system that runs cooperative coroutines on a thread pool. XRPL's `JobQueue::Coro` coroutines (backed by `boost::coroutines2`) are created on one thread, suspended with `yield()`, and resumed — potentially on a different thread. If two coroutines share a worker thread, a naive `thread_local` variable would expose the state left behind by a previously suspended coroutine, producing subtle, hard-to-reproduce bugs. + +`LocalValue` solves this by providing **coroutine-aware local storage**: each coroutine (or plain thread, as a fallback) gets its own independent copy of a `T`, keyed to its execution context rather than its OS thread. + +## Architecture + +The design has two interlocking layers: a per-coroutine dictionary (`LocalValues`) managed via a thread-local pointer, and a typed wrapper (`LocalValue`) that uses its own address as a dictionary key. + +### `detail::LocalValues` + +`LocalValues` is a runtime dictionary that maps `void const*` keys to heap-allocated `BasicValue` instances. The keys are the addresses of `LocalValue` objects; the values hold a type-erased `Value` (a concrete subclass of `BasicValue`) accessed through a virtual `get()` that returns `void*`. This type-erasure pattern lets a single heterogeneous map hold values of arbitrary types without any registry or type-ID scheme — the `LocalValue` template handles all casts. + +The `onCoro` flag distinguishes two ownership modes. When `onCoro == true`, the `LocalValues` is embedded directly in a `Coro` object (`detail::LocalValues lvs_`) and its lifetime is tied to the coroutine's. When `onCoro == false`, the `LocalValues` was heap-allocated for a plain thread worker, and the `cleanup()` deleter passed to `boost::thread_specific_ptr` will `delete` it on thread exit. + +### Thread-Local Pointer Swap in `Coro::resume()` + +The critical mechanism lives in `Coro.ipp`. Every time a coroutine is resumed, `Coro::resume()` performs a pointer swap: + +```cpp +auto saved = detail::getLocalValues().release(); +detail::getLocalValues().reset(&lvs_); +// ... run coroutine body ... +detail::getLocalValues().release(); +detail::getLocalValues().reset(saved); +``` + +This installs the coroutine's private `lvs_` as the active `LocalValues` for the duration of the coroutine's time slice, then restores the previous state. Any `LocalValue` dereference during that time slice will therefore see the coroutine's private dictionary. Because `lvs_` is owned by the `Coro` object and the `thread_specific_ptr` merely borrows it (it will not delete it thanks to `cleanup()`'s `onCoro` guard), there is no double-free risk. + +### `LocalValue` — The Public Interface + +`LocalValue` is a global or static object that holds a single "prototype" value `t_` set at construction time. The prototype is never mutated; it only serves as the initializer for per-context copies. + +`operator*()` implements the lookup: + +1. Call `detail::getLocalValues().get()` to retrieve (or lazily create) the `LocalValues` for the current context. +2. If no `LocalValues` exists yet, allocate one with `onCoro = false` (plain thread path) and register it with the `thread_specific_ptr`. +3. Search for `this` in `lvs->values`. On a hit, cast the `void*` back to `T&` and return it. +4. On a miss, emplace a new `Value` copy-initialized from `t_`, then return a reference to that new copy. + +`operator->()` is a trivial forwarding wrapper to `operator*()`. + +## Concrete Usage + +In `IOUAmount.cpp`, `LocalValue` governs whether IOU arithmetic uses the newer `STNumber` code path or the legacy one. Wrapping this flag in a `LocalValue` rather than `thread_local` ensures that two coroutines executing concurrently on the same thread pool can independently select their arithmetic mode without one overwriting the other's flag: + +```cpp +static LocalValue r{true}; +``` + +The coroutine test in `Coroutine_test.cpp` verifies the isolation property directly: four coroutines each set `*lv = id` (their own integer ID), interleave via yields, and confirm that no coroutine ever sees another's value. A plain-thread job running on the same pool also sees an independent copy. + +## Key Design Decisions + +**Address-keyed map instead of a registry.** Using `this` (the `LocalValue*`) as the map key avoids any global registry or static ID counter. Each `LocalValue` is naturally unique by address, and the approach requires no synchronization beyond what the `thread_specific_ptr` already provides. + +**Lazy allocation.** The per-context copy is created on first dereference rather than at construction. This keeps coroutine startup cheap when only some `LocalValue` instances are actually accessed. + +**`void*` type erasure with `unique_ptr`.** A single `unordered_map` can hold values of unrelated types (`bool`, `int`, custom structs) because ownership and destruction are managed through the virtual destructor of `BasicValue`. The `LocalValue` template retains type knowledge and performs the cast safely — the key equality guarantees that the `void*` behind a given key will always be a `T*`. + +**`onCoro` ownership flag.** The asymmetry between coroutine-owned and thread-owned `LocalValues` is handled by a single boolean rather than two separate code paths or a custom deleter per instance. The `boost::thread_specific_ptr` deleter is fixed at static-initialization time, so the flag is the only extensible hook available. \ No newline at end of file diff --git a/include/xrpl/basics/Log.h.ai.json b/include/xrpl/basics/Log.h.ai.json new file mode 100644 index 0000000000..114d4f90ea --- /dev/null +++ b/include/xrpl/basics/Log.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 36, + "name": "partition" + }, + { + "lineno": 36, + "name": "thresh" + }, + { + "lineno": 36, + "name": "logs" + } + ], + "classes": [ + { + "args": [ + "level" + ], + "lineno": 27, + "name": "Logs" + }, + { + "args": [ + "partition", + "thresh", + "logs" + ], + "lineno": 33, + "name": "Logs::Sink" + }, + { + "args": [], + "lineno": 61, + "name": "Logs::File" + } + ], + "description": "This file defines logging utilities for the XRPL project, including log severity levels, a Logs manager class for handling log partitions and files, and macros/utilities for debug logging.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Log.h", + "functions": [ + { + "args": [ + "sink" + ], + "lineno": 168, + "name": "setDebugLogSink" + }, + { + "args": [], + "lineno": 175, + "name": "debugLog" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 18, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/Log.h.ai.md b/include/xrpl/basics/Log.h.ai.md new file mode 100644 index 0000000000..cbc3fdfae1 --- /dev/null +++ b/include/xrpl/basics/Log.h.ai.md @@ -0,0 +1,57 @@ +# `include/xrpl/basics/Log.h` — Logging Infrastructure + +## Role in the System + +`Log.h` is the entry point for all structured logging in the XRPL C++ server. It defines the central `Logs` class, which acts as a factory and registry for named logging partitions, manages the output file, and controls severity thresholds. Every component in the server — consensus, ledger management, networking, transaction processing — acquires a `beast::Journal` handle from `Logs`, then uses that handle to write messages at the appropriate severity level without coupling to the output destination. + +The header sits at the boundary between xrpl's own types and the `beast` logging primitives it builds on. `beast::Journal` (from `xrpl/beast/utility/Journal.h`) is the lightweight, copyable handle that individual subsystems carry; `Logs` is the stateful root that backs all those handles. + +## The `beast::Journal` Layer + +Before understanding `Logs`, it helps to understand what it produces. A `beast::Journal` is a non-owning, copyable pointer to a `beast::Journal::Sink`. The `Sink` is an abstract base providing two key operations: `write()` (filtered by threshold) and `writeAlways()` (bypasses the threshold check). The `Journal` itself offers named severity accessors — `trace()`, `debug()`, `info()`, `warn()`, `error()`, `fatal()` — each returning a `Journal::Stream` that implements `operator bool()` returning whether the level is currently active. + +This architecture makes cheap caller-side checks possible: every `Stream` can be tested for activity before any string formatting happens. + +## The `JLOG` Macro and Lazy Evaluation + +The `JLOG` macro is the primary idiom for logging throughout the codebase: + +```cpp +JLOG(j.warn()) << "Computed value: " << expensiveFunction(); +``` + +Expanding to an `if (!x) {} else x` pattern, this ensures that if the `warn()` stream is below threshold, the `<<` chain is never evaluated. This is architecturally significant: without it, even a disabled `trace()` call would still serialize all arguments into a string, burning CPU in a hot path. The macro rather than an inline function avoids any overhead whatsoever at the disabled level — the compiler simply skips the branch. `CLOG` is a similar guard for optional pointer-style stream objects (used when the stream may be null). + +## The `Logs` Class + +`Logs` is constructed once per process with an initial severity threshold and acts as the single routing point for all log output. It maintains a `std::map>` keyed by partition name (subsystem label), with a `boost::beast::iless` comparator for case-insensitive lookup. This means code can request `"Ledger"` or `"ledger"` and get the same sink. + +### Partition Sinks + +The inner `Logs::Sink` class extends `beast::Journal::Sink`, carrying a partition name and a back-reference to its parent `Logs`. When `write()` is called on a sink, it delegates immediately to `Logs::write()`, which holds the shared `mutex_`, formats the message, writes to the log file, and — unless `silent_` is set — writes to `std::cerr`. The indirection through `Logs::write()` is what makes it possible to serialize all partition output through one lock and one file handle. + +Callers acquire sinks via `Logs::get()` or `Logs::journal()`. The `get()` implementation uses `emplace()` on the map: if the partition already exists the existing sink is returned, otherwise a new one is created via the virtual `makeSink()` factory method. Because `makeSink()` is virtual, tests can subclass `Logs` to inject mock sinks without touching the rest of the system. + +Changing the global threshold via `Logs::threshold(Severity)` updates `thresh_` and then iterates all existing sinks to propagate the change. This means a run-time config reload can silence verbose partitions or turn on trace output without restarting the server. + +### File Output and Log Rotation + +The nested `Logs::File` class wraps a `std::ofstream` opened in append mode. The design is intentionally minimal: `open()`, `close()`, `write()`, `writeln()`. The critical method is `closeAndReopen()`, which is the POSIX log rotation idiom: external tools like `logrotate(8)` rename the live log file and then send a signal to the server; the server responds by calling `Logs::rotate()`, which closes its file handle and reopens the path, creating a fresh file at the original location. The `File` class stores `m_path` specifically to enable this reopen without any external coordination. + +### Message Formatting and Security Scrubbing + +`Logs::format()` — a private static method called unconditionally before any write — prepends a timestamp and renders the severity as a three-letter tag (`TRC`, `DBG`, `NFO`, `WRN`, `ERR`, `FTL`), followed by the partition name. It then truncates messages exceeding 12 KB to prevent runaway log lines. + +Critically, `format()` also performs **sensitive data redaction**. After composing the full output string, it scans for JSON keys `"seed"`, `"seed_hex"`, `"secret"`, `"master_key"`, `"master_seed"`, `"master_seed_hex"`, and `"passphrase"`, and replaces the associated quoted values with asterisks. This is a defensive measure to prevent operator errors from leaking wallet secrets into log files — even if a developer mistakenly logs a raw JSON RPC request containing signing keys. + +## Deprecated `LogSeverity` Enum + +The `LogSeverity` enum (`lsTRACE`, `lsDEBUG`, etc.) is a legacy mapping that predates the `beast::severities::Severity` enum. It is marked deprecated but kept for backwards compatibility, with `fromSeverity()` and `toSeverity()` providing the translation. The `fromString()` method accepts several aliases (`"warn"`, `"warning"`, `"warnings"`) using case-insensitive comparison, which serves the config file parser. + +## Debug Logging + +`setDebugLogSink()` and `debugLog()` provide a secondary, process-global logging channel backed by a thread-safe `DebugSink` Meyers singleton (defined in `Log.cpp`). This channel defaults to a null sink and is primarily for test code that wants to capture log output without constructing a full `Logs` instance. The documentation explicitly warns that output from `debugLog()` may never be seen — it is unsuitable for any information that matters operationally. + +## Thread Safety + +All mutable state in `Logs` — the sink map, the file handle, and the threshold — is protected by a single `mutable mutex_`. `beast::Journal` and `Stream` objects are copyable and safe to use across threads because they only dereference a `Sink*` for reads. The `DebugSink` in `Log.cpp` has its own separate mutex for swapping the debug sink atomically while concurrent `debugLog()` calls may be in flight. \ No newline at end of file diff --git a/include/xrpl/basics/MallocTrim.h.ai.json b/include/xrpl/basics/MallocTrim.h.ai.json new file mode 100644 index 0000000000..1c74950389 --- /dev/null +++ b/include/xrpl/basics/MallocTrim.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 54, + "name": "tag" + }, + { + "lineno": 54, + "name": "journal" + } + ], + "classes": [], + "description": "Provides a facility to attempt to return freed memory to the operating system by invoking malloc_trim on Linux/glibc, along with a report structure for before/after memory metrics.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/MallocTrim.h", + "functions": [ + { + "args": [ + "tag", + "journal" + ], + "lineno": 54, + "name": "mallocTrim" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/MallocTrim.h.ai.md b/include/xrpl/basics/MallocTrim.h.ai.md new file mode 100644 index 0000000000..a47354c399 --- /dev/null +++ b/include/xrpl/basics/MallocTrim.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/basics/MallocTrim.h` + +## Role in the System + +This header is a targeted memory-pressure relief valve for the XRPL node process. On long-running servers, glibc's ptmalloc allocator can accumulate free pages in its internal arenas without returning them to the OS, causing the process's Resident Set Size (RSS) to drift upward over time even when application-level memory usage has genuinely declined. `MallocTrim.h` exposes a controlled wrapper around `::malloc_trim(0)` that reclaims that slack, paired with a diagnostic report so callers can observe what the operation actually accomplished. + +## `MallocTrimReport` + +The `MallocTrimReport` struct is the observability surface for a single trim invocation. All fields default to sentinel values (`-1` or `false`) that signal "not populated" rather than zero, which is important because a real RSS reading or page-fault count of zero is valid and meaningful. The fields capture: + +- **`supported`** — whether the current platform supports trimming at all. This lets callers distinguish "trim ran and did nothing" from "trim was never attempted." +- **`trimResult`** — the raw return value from `::malloc_trim`: `1` if the allocator actually released pages, `0` if there was nothing to release. +- **`rssBeforeKB` / `rssAfterKB`** — process RSS read from `/proc/self/statm` bracketing the trim call, in kilobytes. +- **`durationUs`** — wall-clock duration of the `::malloc_trim` call in microseconds, useful for detecting trim latency spikes. +- **`minfltDelta` / `majfltDelta`** — thread-level minor and major page fault deltas (via `RUSAGE_THREAD`) across the trim, making it possible to detect cases where trimming caused unexpected kernel re-faulting of pages. + +The inline `deltaKB()` method returns the signed RSS change (negative means memory was freed, positive means RSS grew). It guards against uninitialized sentinel values by returning `0` if either RSS reading is negative. + +## `mallocTrim()` Function + +The function signature is: + +```cpp +MallocTrimReport mallocTrim(std::string_view tag, beast::Journal journal); +``` + +The `tag` is a caller-supplied identifier that appears in log output, allowing multiple call sites to be distinguished in diagnostics. The actual implementation in `MallocTrim.cpp` is compiled conditionally: the entire trim path is gated on `#if defined(__GLIBC__) && BOOST_OS_LINUX`. On any other platform, the function is a no-op that returns a default-constructed report with `supported = false`. + +When active, the implementation makes a deliberate performance tradeoff gated on the journal's debug severity. If debug logging is disabled, only `::malloc_trim(0)` is called and the report stays mostly at sentinel values — no `/proc` reads, no rusage calls, minimal overhead. If debug logging is enabled, the function reads `/proc/self/statm` before and after, calls `getrusage(RUSAGE_THREAD, ...)` before and after, and measures wall time. This layered instrumentation is only paid when someone is actively debugging memory behavior, which makes the function cheap enough for production call sites. + +The trim padding constant is hardcoded to `TRIM_PAD = 0`. A comment in the implementation documents that this choice came from 12-hour empirical testing on Mainnet across four padding values (0, 256 KB, 1 MB, 16 MB): zero delivered the best balance of RSS reduction and trim-latency stability without adding a tuning surface. This is a non-obvious design choice that prevents the parameter from becoming a configuration footgun. + +## Allocator Compatibility and Scope Limits + +The header's comment block is worth reading carefully: `malloc_trim` only affects glibc's own `sbrk`-based arenas. Large allocations backed by `mmap` are returned to the OS when freed, regardless of trimming. More critically, if jemalloc or tcmalloc is linked or `LD_PRELOAD`ed, calling `::malloc_trim` from glibc's symbol is harmless but does not touch those allocators' arenas. The function has no way to detect this at runtime, so the `supported` flag is set based on compiler/OS detection only, not on whether the active allocator will actually respond. + +## Call Site + +The function is invoked in `Application.cpp`'s `doSweep()` method, immediately after cache sweeps and ledger cleanup: + +```cpp +mallocTrim("doSweep", m_journal); +``` + +This is the canonical usage pattern: call at a known point after significant bulk-free operations, rather than on a timer. The header comment recommends rate-limiting calls to avoid churn, which `doSweep`'s existing sweep timer naturally provides. \ No newline at end of file diff --git a/include/xrpl/basics/MathUtilities.h.ai.json b/include/xrpl/basics/MathUtilities.h.ai.json new file mode 100644 index 0000000000..5bd9772632 --- /dev/null +++ b/include/xrpl/basics/MathUtilities.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 16, + "name": "count" + }, + { + "lineno": 16, + "name": "total" + } + ], + "classes": [], + "description": "Provides a constexpr function to calculate the percentage of one number divided by another, rounded up and capped between 0 and 100, with static_assert unit tests.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/MathUtilities.h", + "functions": [ + { + "args": [ + "count", + "total" + ], + "lineno": 16, + "name": "calculatePercent" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/MathUtilities.h.ai.md b/include/xrpl/basics/MathUtilities.h.ai.md new file mode 100644 index 0000000000..b6042bae64 --- /dev/null +++ b/include/xrpl/basics/MathUtilities.h.ai.md @@ -0,0 +1,23 @@ +# `MathUtilities.h` — Integer Percentage Utility + +This header lives in the `xrpl::basics` utility layer and provides a single function, `calculatePercent`, for computing integer percentages with ceiling rounding and range clamping. It exists because this specific combination of behaviors — ceiling division, overflow-safe clamping, and compile-time verifiability — recurs in the XRPL codebase wherever a count-over-total ratio needs to be expressed as a human-readable integer percentage without floating point. + +## `calculatePercent` + +```cpp +constexpr std::size_t calculatePercent(std::size_t count, std::size_t total) +``` + +The function answers: "what percent of `total` is `count`, rounded up to the nearest whole percent, and never exceeding 100?" The ceiling rounding is intentional: a fraction like 1/99 (≈1.01%) rounds up to 2, signalling that *some* nonzero fraction is present. This is the conservative, safety-oriented direction for thresholding use cases — if you're checking whether at least N% of validators have seen something, you want to err on the side of reporting a higher fraction rather than losing signal in truncation. + +The arithmetic `((std::min(count, total) * 100) + total - 1) / total` encodes three distinct behaviors in one expression. The `std::min(count, total)` clamp prevents the numerator from exceeding `total * 100`, which both enforces the 100% cap and guards against the case where `count > total` (e.g., a validator set that temporarily grows). The `+ total - 1` addend is the standard ceiling-division trick: it causes any non-zero remainder in the integer division to round up rather than truncate. The whole expression stays in unsigned integer arithmetic with no floating point, making it safe across all platforms and constexpr-eligible. + +The `assert(total != 0)` guard catches division-by-zero in debug builds. The comment explains why `XRPL_ASSERT` is not used here: the function is `constexpr`, and `XRPL_ASSERT` is not constexpr-compatible, so the plain `assert` is the appropriate defence at runtime while still permitting compile-time evaluation. + +## Real-World Use + +The primary call site is in `LedgerMaster.cpp`, which uses `calculatePercent` to decide whether to warn operators about a potential software upgrade need. Two separate thresholds are evaluated: whether at least 90% of the UNL (Unique Node List) validators have sent validation messages, and whether at least 60% of xrpld-running validators are on a higher version. Both checks use `calculatePercent` with named constants (`reportingPercent`, `cutoffPercent`) rather than raw literals, and the thresholding via `>=` naturally benefits from ceiling rounding — a value just above a threshold boundary will not be falsely excluded by truncation. + +## Compile-Time Tests + +The `static_assert` block at namespace scope doubles as both documentation and zero-cost test coverage. Fourteen cases are checked at compile time, covering the zero numerator, full 100% match, over-100% input, boundary rounding cases (1/99, 1/64, near-100% fractions), and large-scale inputs up to 100 million. If the implementation is ever changed and any of these properties breaks, the build fails immediately — there is no need for a separate test binary or runtime fixture. This pattern is well-suited to a pure arithmetic utility that has no external dependencies. \ No newline at end of file diff --git a/include/xrpl/basics/Mutex.hpp.ai.json b/include/xrpl/basics/Mutex.hpp.ai.json new file mode 100644 index 0000000000..d5188d75cb --- /dev/null +++ b/include/xrpl/basics/Mutex.hpp.ai.json @@ -0,0 +1,60 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ProtectedDataType" + }, + { + "lineno": 11, + "name": "MutexType" + }, + { + "lineno": 15, + "name": "LockType" + } + ], + "classes": [ + { + "args": [ + "MutexType& mutex, ProtectedDataType& data" + ], + "lineno": 18, + "name": "Lock" + }, + { + "args": [ + "ProtectedDataType data" + ], + "lineno": 56, + "name": "Mutex" + } + ], + "description": "Provides a thread-safe container (Mutex) for protecting data with a mutex, inspired by Rust's Mutex, and a Lock class for accessing the protected data safely.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Mutex.hpp", + "functions": [ + { + "args": [ + "Args&&... args" + ], + "lineno": 66, + "name": "Mutex::make" + }, + { + "args": [], + "lineno": 81, + "name": "Mutex::lock (const)" + }, + { + "args": [], + "lineno": 92, + "name": "Mutex::lock" + } + ], + "language": "cpp header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/Mutex.hpp.ai.md b/include/xrpl/basics/Mutex.hpp.ai.md new file mode 100644 index 0000000000..2bf97d6687 --- /dev/null +++ b/include/xrpl/basics/Mutex.hpp.ai.md @@ -0,0 +1,51 @@ +# `include/xrpl/basics/Mutex.hpp` + +## Purpose and Motivation + +This header introduces a Rust-inspired `Mutex` idiom to the XRPL C++ codebase. The core problem it solves is a well-known C++ anti-pattern: separating a mutex from the data it protects, which lets callers accidentally read or write the data without holding the lock. The standard library provides `std::mutex` and RAII guards, but the data itself remains a separate, freely-accessible member. Nothing in the type system stops code from doing `data_ = x;` without ever calling `lock()`. + +The Rust model solves this by making the lock guard the only path to the data. `Mutex.hpp` reproduces that guarantee in C++: the protected value lives inside `Mutex`, is entirely private, and the only way to reach it is through a `Lock` object returned by `Mutex::lock()`. A `Lock` holds both the RAII lock and a reference to the data, so when the `Lock` goes out of scope the mutex is released and the reference becomes unreachable simultaneously. + +The file is credited to the Clio project (`https://github.com/XRPLF/clio`) and lives under the `xrpl` namespace in `include/xrpl/basics/`. + +## `Lock` + +`Lock` is a thin RAII wrapper that bundles a lock instance (`LockType lock_`) with a reference to the protected data (`ProtectedDataType& data_`). It exposes `operator*`, `get()`, and `operator->` — all in both const and non-const variants — so users interact with the guarded value naturally. + +The constructor `Lock(MutexType& mutex, ProtectedDataType& data)` is `private`, with `friend class Mutex, MutexType>` as the sole access point. The `std::remove_const_t` strip is intentional: when the `Mutex` is const it instantiates `Lock`, whose friend declaration must still point back to `Mutex` (non-const), not the non-existent `Mutex`. This single line of `friend` is what makes the safety guarantee airtight — no code path outside `Mutex::lock()` can construct a `Lock`. + +The conversion operators `operator LockType&()` and `operator LockType const&()` allow a `Lock` to decay to a reference to the underlying lock object. This matters in practice when `std::unique_lock` is chosen as the `LockType` and the lock needs to be passed to `std::condition_variable::wait()`, which accepts a `std::unique_lock&`. The conversion keeps the idiom composable with the standard library's synchronisation utilities. + +## `Mutex` + +`Mutex` stores a `mutable MutexType mutex_` and a `ProtectedDataType data_{}`. The `mutable` qualifier is deliberate: acquiring a lock is logically a non-mutating operation on the container, so it must be callable on a `const Mutex` instance. Without `mutable` the const `lock()` overload could not take `mutex_` by non-const reference as required by any standard lock type. + +Two `lock()` overloads handle const-correctness propagation: + +```cpp +// const Mutex → Lock (read-only access) +Lock lock() const; + +// non-const Mutex → Lock (read-write access) +Lock lock(); +``` + +Calling `lock()` on a `const Mutex` yields a `Lock` whose `operator*` and `operator->` return `const` references. The compiler enforces this — there is no cast or escape hatch. The test suite verifies it with `static_assert(std::is_const_v<...>)`. + +Both overloads are templated on `LockType`, which defaults to `std::lock_guard`. Callers can substitute `std::unique_lock` for deferred or timed locking, or combine a `std::shared_mutex` as the `MutexType` with `std::shared_lock` as the `LockType` to implement reader-writer semantics with the same ergonomic API. The test in `src/tests/libxrpl/basics/Mutex.cpp` demonstrates exactly this: multiple shared readers coexist while an exclusive writer uses `std::unique_lock`. + +The `make()` static factory provides perfect-forwarding in-place construction of the protected object: + +```cpp +auto m = Mutex::make(arg1, arg2); // forwards to MyStruct(arg1, arg2) +``` + +This avoids a move-construct-then-copy sequence when constructing from arguments and also supports move-only types like `std::unique_ptr`. + +## Design Trade-offs + +The main cost of this pattern is that the `Lock` object must remain alive for the entire scope of any access. Code that needs to unlock mid-scope, or pass just the protected value to a function, requires `std::unique_lock` as the `LockType` so the conversion operator can expose the lock for manual `unlock()`/`lock()` calls. The default `std::lock_guard` is intentionally non-flexible — it signals "lock for this scope, nothing else" and prevents premature unlocking. + +There is no `try_lock()` surface exposed through `Mutex` itself. A caller wanting try-lock semantics must drop to `std::unique_lock` (for `try_lock()`) or interact with the raw mutex type outside this wrapper, which is a deliberate friction point rather than an oversight. The design prioritises the common, safe path — RAII lock for the lifetime of a `Lock` object — over less common, riskier patterns. + +The file is the sole resident of the `basics/` header directory that was found, suggesting it was added as a focused utility pulled from Clio rather than part of a larger local module. Its test coverage in `Mutex.cpp` is comprehensive: default construction, value construction, `make()` with forwarding, const and non-const access, custom lock types, `std::shared_mutex` reader-writer patterns, and move-only protected types. \ No newline at end of file diff --git a/include/xrpl/basics/Number.h.ai.json b/include/xrpl/basics/Number.h.ai.json new file mode 100644 index 0000000000..baaa21be47 --- /dev/null +++ b/include/xrpl/basics/Number.h.ai.json @@ -0,0 +1,363 @@ +{ + "args": [ + { + "lineno": 10, + "name": "amount" + }, + { + "lineno": 13, + "name": "value" + }, + { + "lineno": 31, + "name": "scale_" + }, + { + "lineno": 119, + "name": "mantissa" + }, + { + "lineno": 119, + "name": "exponent" + }, + { + "lineno": 117, + "name": "negative" + }, + { + "lineno": 373, + "name": "mode" + }, + { + "lineno": 407, + "name": "scale" + } + ], + "classes": [ + { + "args": [ + "mantissa_scale scale_" + ], + "lineno": 28, + "name": "MantissaRange" + }, + { + "args": [], + "lineno": 81, + "name": "Number" + }, + { + "args": [], + "lineno": 120, + "name": "Number::unchecked" + }, + { + "args": [], + "lineno": 127, + "name": "Number::normalized" + }, + { + "args": [ + "Number::rounding_mode mode" + ], + "lineno": 370, + "name": "saveNumberRoundMode" + }, + { + "args": [ + "Number::rounding_mode mode" + ], + "lineno": 386, + "name": "NumberRoundModeGuard" + }, + { + "args": [ + "MantissaRange::mantissa_scale scale" + ], + "lineno": 404, + "name": "NumberMantissaScaleGuard" + } + ], + "description": "Defines the xrpl::Number class, a floating point type for representing a wide range of asset values with precise mantissa and exponent control, including normalization, rounding, and mantissa range switching. Also provides related utilities, guards, and helper functions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Number.h", + "functions": [ + { + "args": [ + "Number const& amount" + ], + "lineno": 10, + "name": "to_string" + }, + { + "args": [ + "T value" + ], + "lineno": 13, + "name": "logTen" + }, + { + "args": [ + "T value" + ], + "lineno": 24, + "name": "isPowerOfTen" + }, + { + "args": [ + "Number const& x", + "Number const& y" + ], + "lineno": 154, + "name": "operator==" + }, + { + "args": [ + "Number const& x", + "Number const& y" + ], + "lineno": 160, + "name": "operator!=" + }, + { + "args": [ + "Number const& x", + "Number const& y" + ], + "lineno": 165, + "name": "operator<" + }, + { + "args": [], + "lineno": 186, + "name": "signum" + }, + { + "args": [], + "lineno": 191, + "name": "truncate" + }, + { + "args": [ + "Number const& x", + "Number const& y" + ], + "lineno": 194, + "name": "operator>" + }, + { + "args": [ + "Number const& x", + "Number const& y" + ], + "lineno": 198, + "name": "operator<=" + }, + { + "args": [ + "Number const& x", + "Number const& y" + ], + "lineno": 202, + "name": "operator>=" + }, + { + "args": [ + "std::ostream& os", + "Number const& x" + ], + "lineno": 206, + "name": "operator<<" + }, + { + "args": [ + "Number const& amount" + ], + "lineno": 211, + "name": "to_string" + }, + { + "args": [ + "Number f", + "unsigned d" + ], + "lineno": 213, + "name": "root" + }, + { + "args": [ + "Number f" + ], + "lineno": 215, + "name": "root2" + }, + { + "args": [], + "lineno": 220, + "name": "getround" + }, + { + "args": [ + "rounding_mode mode" + ], + "lineno": 222, + "name": "setround" + }, + { + "args": [], + "lineno": 229, + "name": "getMantissaScale" + }, + { + "args": [ + "MantissaRange::mantissa_scale scale" + ], + "lineno": 234, + "name": "setMantissaScale" + }, + { + "args": [], + "lineno": 238, + "name": "minMantissa" + }, + { + "args": [], + "lineno": 243, + "name": "maxMantissa" + }, + { + "args": [], + "lineno": 248, + "name": "mantissaLog" + }, + { + "args": [], + "lineno": 254, + "name": "oneSmall" + }, + { + "args": [], + "lineno": 256, + "name": "oneLarge" + }, + { + "args": [], + "lineno": 260, + "name": "one" + }, + { + "args": [ + "T minMantissa", + "T maxMantissa" + ], + "lineno": 264, + "name": "normalizeToRange" + }, + { + "args": [], + "lineno": 282, + "name": "normalize" + }, + { + "args": [ + "bool& negative", + "T& mantissa", + "int& exponent", + "internalrep const& minMantissa", + "internalrep const& maxMantissa" + ], + "lineno": 287, + "name": "normalize" + }, + { + "args": [ + "bool& negative", + "T& mantissa_", + "int& exponent_", + "MantissaRange::rep const& minMantissa", + "MantissaRange::rep const& maxMantissa" + ], + "lineno": 295, + "name": "doNormalize" + }, + { + "args": [], + "lineno": 299, + "name": "isnormal" + }, + { + "args": [ + "int exponentDelta" + ], + "lineno": 304, + "name": "shiftExponent" + }, + { + "args": [ + "rep mantissa" + ], + "lineno": 309, + "name": "externalToInternal" + }, + { + "args": [ + "Number x" + ], + "lineno": 324, + "name": "abs" + }, + { + "args": [ + "Number const& f", + "unsigned n" + ], + "lineno": 334, + "name": "power" + }, + { + "args": [ + "Number f", + "unsigned d" + ], + "lineno": 338, + "name": "root" + }, + { + "args": [ + "Number f" + ], + "lineno": 341, + "name": "root2" + }, + { + "args": [ + "Number const& f", + "unsigned n", + "unsigned d" + ], + "lineno": 344, + "name": "power" + }, + { + "args": [ + "Number const& x", + "Number const& limit" + ], + "lineno": 348, + "name": "squelch" + }, + { + "args": [ + "MantissaRange::mantissa_scale const& scale" + ], + "lineno": 355, + "name": "to_string" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/Number.h.ai.md b/include/xrpl/basics/Number.h.ai.md new file mode 100644 index 0000000000..28e4a85293 --- /dev/null +++ b/include/xrpl/basics/Number.h.ai.md @@ -0,0 +1,54 @@ +# `include/xrpl/basics/Number.h` — Decimal Floating-Point Arithmetic for XRPL + +`Number` is the XRPL ledger's custom decimal floating-point type. It exists because IEEE 754 `double` is unsuitable for consensus-critical financial arithmetic: binary floating point introduces rounding artifacts that can cause two nodes to compute slightly different results for the same transaction, breaking the ledger's agreement requirement. All arithmetic needed during transaction processing — AMM liquidity calculations, lending protocol interest, IOU amounts, XRP drops, MPT balances — routes through `Number`. + +## Internal Representation + +A `Number` stores three fields: a `bool negative_` sign flag, a `uint64_t mantissa_` (called `internalrep` in the code), and an `int exponent_`. The mantissa is kept *normalized*, meaning it sits in a range `[min, max]` where `min` is a power of ten and `max = min * 10 - 1`. The exponent is bounded to `[minExponent, maxExponent]` = `[-32768, 32768]`. Zero is the special case where mantissa equals zero and `exponent_` is `std::numeric_limits::lowest()`. + +This representation was inherited from `STAmount`, which encodes IOU values the same way. The key insight is that financial values in the ledger are already expressed in decimal — storing them in a decimal floating-point type avoids any binary-to-decimal conversion loss. + +## The Two Mantissa Scales — and Why They Both Exist + +The file introduces `MantissaRange`, a small value-type holding the `min`, `max`, and `log` for one of two permitted scales: + +- **Small scale** (`10^15` to `10^16 - 1`): the original `STAmount` normalization range. It gives 15–16 significant decimal digits, which is sufficient for IOU values but cannot exactly represent large integers like XRP drops or MPT balances up to `2^63 - 1 ≈ 9.2 × 10^18`. +- **Large scale** (`10^18` to `10^19 - 1`): introduced for `SingleAssetVault` and `LendingProtocol`. It gives 18–19 significant decimal digits, covering the full positive `int64_t` range and the integer values needed for XRP and MPT precisely. + +The active scale is controlled by the thread-local `range_` member, a `std::reference_wrapper` that points to one of two `constexpr static` instances (`smallRange` or `largeRange`). Using a reference wrapper rather than copying the range is deliberate: it prevents accidentally mutating the authoritative constants and makes the scale switch cheap — just a pointer swap. + +Scale switching is amendment-gated. In `applySteps.cpp`, the `with_txn_type` function checks `featureSingleAssetVault` and `featureLendingProtocol` and, if either is enabled, installs a `NumberMantissaScaleGuard` that sets the scale to `large` for the duration of the transaction and restores the previous value on exit. This means pre-amendment transactions continue to use the small scale, preserving backward-compatible arithmetic results even in mixed-amendment ledgers. + +## External vs. Internal Interface + +The internal mantissa is an unsigned `uint64_t` and can hold the large-scale maximum of `9,999,999,999,999,999,999` — which exceeds `INT64_MAX = 9,223,372,036,854,775,807`. However, the external interface (`mantissa()` and `exponent()`) must return a signed `int64_t`, so `mantissa()` silently divides by 10 and `exponent()` increments by 1 whenever the internal mantissa is in this "overflow zone." The pair is guaranteed consistent. This is the mechanism that allows the internal representation to precisely track the full large-scale range while still presenting a canonical 63-bit external view. + +`Number` cannot represent `-2^63` exactly, but that value is not a valid ledger amount: XRP drops are non-negative, and MPT maximum is `2^63 - 1`. + +The conversion from signed external `rep` to internal unsigned `internalrep` is handled by `externalToInternal()`. It cannot simply negate `INT64_MIN` because that is undefined behavior in C++; the method falls through to a 128-bit intermediate cast for that edge case. + +## Guard Digits and Rounding + +The `Guard` inner class is the arithmetic workhorse. It maintains a 64-bit BCD-like register of up to 16 guard decimal digits (each stored in 4 bits using a shift-and-push register) plus a sticky `xbit_` that records whether any non-zero digit was ever pushed beyond the 16-digit window. This is classic guard-digit arithmetic, ensuring that intermediate precision lost during alignment or multiplication is available for correct final rounding. + +Rounding mode is also thread-local (`mode_`). The four modes — `to_nearest` (banker's rounding / round-half-to-even), `towards_zero`, `downward`, `upward` — map directly to IEEE 754 semantics. The default is `to_nearest`. `NumberRoundModeGuard` sets a new mode on construction and restores the old one on destruction, enabling scoped rounding control without global mutation. + +## Arithmetic Implementation + +Addition aligns the two operands to the same exponent by dividing the smaller one by successive powers of 10, pushing each lost digit into a `Guard`. Subtraction is implemented as `*this += -y`. Multiplication and division use `uint128_t` intermediates (GCC/Clang `__uint128_t`; Boost multiprecision on MSVC) to avoid 64-bit overflow before normalization. The `divu10()` function is a bespoke 128-bit divide-by-10 using the Hacker's Delight bit-trick, avoiding the cost of a full 128/64 hardware division. + +`power(f, n)` uses repeated squaring (O(log n) multiplications). `root(f, d)` applies Newton–Raphson iteration until convergence, and `root2` is a specialized square-root shortcut. `power(f, n, d)` combines both for rational exponents. + +## Constructor Design + +The `unchecked{}` tag constructor bypasses normalization entirely. It exists for two legitimate purposes: constructing the compile-time constants `numZero`, `oneSml`, `oneLrg`, and the range sentinels; and at type-conversion boundaries where the caller guarantees the values are already normalized. The `normalized{}` tag constructor forces normalization from an unsigned internal representation and is reserved for unit tests. Regular constructors (taking a signed `int64_t` mantissa) always normalize via `externalToInternal()` + `normalize()`. + +The `implicit` conversion direction is intentional: any integral type or `STAmount` converts *to* `Number` implicitly, while extraction back to `int64_t` is `explicit`. This makes `Number` the natural accumulator type for mixed-mode expressions like `MPTAmount + Number` without risk of silent truncation on the way out. + +## Utility Helpers + +`squelch(x, limit)` returns zero when `abs(x) < limit` and `x` otherwise. This handles the common financial pattern of zeroing out sub-precision residuals that arise from rounding chains. + +`normalizeToRange(minMantissa, maxMantissa)` is a public template that reprojects the `Number` into a caller-supplied integer range, returning a `(mantissa, exponent)` pair. This is used by `STAmount` conversion code to map back from `Number` to specific asset representations without needing `Number` to know about each asset type. + +The `logTen()` and `isPowerOfTen()` constexpr templates at namespace scope verify the `MantissaRange` boundary invariants at compile time via `static_assert`, ensuring that the chosen scale boundaries are always exact powers of ten — a precondition for correct normalization arithmetic. \ No newline at end of file diff --git a/include/xrpl/basics/RangeSet.h.ai.json b/include/xrpl/basics/RangeSet.h.ai.json new file mode 100644 index 0000000000..da7ae691da --- /dev/null +++ b/include/xrpl/basics/RangeSet.h.ai.json @@ -0,0 +1,91 @@ +{ + "args": [ + { + "lineno": 22, + "name": "low" + }, + { + "lineno": 22, + "name": "high" + }, + { + "lineno": 39, + "name": "ci" + }, + { + "lineno": 54, + "name": "rs" + }, + { + "lineno": 73, + "name": "rs" + }, + { + "lineno": 73, + "name": "s" + }, + { + "lineno": 120, + "name": "rs" + }, + { + "lineno": 120, + "name": "t" + }, + { + "lineno": 120, + "name": "minVal" + } + ], + "classes": [], + "description": "Provides utilities for working with closed intervals and sets of intervals (ranges) over a domain T, including conversion to/from styled strings and finding missing values in a range set. Uses Boost ICL for interval management.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/RangeSet.h", + "functions": [ + { + "args": [ + "low", + "high" + ], + "lineno": 22, + "name": "range" + }, + { + "args": [ + "ci" + ], + "lineno": 39, + "name": "to_string" + }, + { + "args": [ + "rs" + ], + "lineno": 54, + "name": "to_string" + }, + { + "args": [ + "rs", + "s" + ], + "lineno": 73, + "name": "from_string" + }, + { + "args": [ + "rs", + "t", + "minVal" + ], + "lineno": 120, + "name": "prevMissing" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/RangeSet.h.ai.md b/include/xrpl/basics/RangeSet.h.ai.md new file mode 100644 index 0000000000..92644ddb9b --- /dev/null +++ b/include/xrpl/basics/RangeSet.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/basics/RangeSet.h` + +## Purpose and Context + +This header provides the XRPL ledger's primary abstraction for representing sparse sets of sequence numbers: specifically, which ledger indexes a node has fully acquired and validated. The core data structure, `RangeSet`, is used by `LedgerMaster` to maintain `mCompleteLedgers` — a live record of which historical ledgers are locally available — and supports serialization for network peer advertising and database persistence. + +The design is deliberately thin: both `ClosedInterval` and `RangeSet` are pure type aliases over Boost ICL (`boost::icl::closed_interval` and `boost::icl::interval_set`). All the algebraic machinery — automatic coalescing of adjacent intervals, set difference, containment queries — is delegated directly to Boost ICL, and the header simply layers XRPL-specific string serialization and one domain-relevant query (`prevMissing`) on top. + +## Types and Construction + +`ClosedInterval` represents a single contiguous range `[low, high]` where both endpoints are included. The `range(low, high)` helper exists purely to avoid repeating the template argument when constructing intervals inline — compare `range(10u, 15u)` vs. `ClosedInterval(10u, 15u)`. + +`RangeSet` is an ordered, normalized collection of disjoint `ClosedInterval` objects. The key property Boost ICL provides automatically is coalescing: inserting ledger 6 into `{1-5, 7-10}` yields `{1-10}` with no extra code. This invariant — the set always contains the minimum number of disjoint intervals — is what makes the format both compact in memory and straightforward to serialize. + +## Serialization + +The `to_string`/`from_string` pair implements a human-readable canonical format: `"1-2,4-6,9"` where a single-element interval is written as a bare number and a range uses a dash. `to_string` for an empty set returns `"empty"` rather than an empty string, providing a safe diagnostic representation. + +`from_string` is more carefully designed. It bears the `[[nodiscard]]` attribute, forcing callers to check success. On any parse failure — an unrecognized token, a lexical cast that fails, a dash-split producing more than two parts — the function immediately clears the output set and returns `false`. This all-or-nothing contract means the output `RangeSet` is never left in a partial or corrupt state, which matters because a partial ledger set could cause `LedgerMaster` to believe it has acquired ledgers it hasn't. + +Parsing uses `beast::lexicalCastChecked` for safe numeric conversion rather than `std::stoi` or `atoi`, which would silently truncate or throw on bad input. The loop eagerly clears `intervals` between tokens, avoiding stale data from a previous successful parse contaminating the next one. + +## `prevMissing`: Gap-Driven Acquisition + +The most algorithmically interesting function is `prevMissing`. Given a `RangeSet`, a target value `t`, and a lower bound `minVal`, it returns the largest value strictly less than `t` that is **not** in the set — that is, the largest gap below the query point. + +This is the engine behind `LedgerMaster`'s historical ledger acquisition loop. When the node is filling in its ledger history, it repeatedly calls `prevMissing(mCompleteLedgers, maxVal)` to find the next sequence it still needs to fetch. Scanning backward from the most recent known ledger and prioritizing the largest missing sequence number is an efficient greedy strategy: it minimizes the number of passes needed to converge on a contiguous range. + +The implementation is elegant: rather than iterating candidate values, it constructs the interval `[minVal, t-1]`, subtracts the existing set from it (Boost ICL set-difference), and returns the last element of the complement. This computes the answer in terms of interval arithmetic rather than element-by-element search, making it efficient even when the set contains thousands of intervals. + +The two early-exit conditions — empty set (everything is missing, so answer is `t-1`) and `t == minVal` (no valid predecessor exists) — are handled by returning `std::nullopt`, which the caller must check before dereferencing. + +## Concurrency Notes + +`RangeSet` itself has no internal synchronization. In `LedgerMaster`, every access to `mCompleteLedgers` is guarded by a separate `mCompleteLock` mutex — a deliberate externalized-locking design that keeps the data structure lightweight and composable while still protecting concurrent reads and writes during ledger validation, gap fill, and peer advertisement. + +## Relationship to Adjacent Code + +`RelationalDatabase.h` includes this header, indicating that ledger range information flows through the database layer as well — sequences of complete ledger ranges are stored and queried as part of persistent state. The string format serves double duty as both a wire representation for peer protocol messages and a storage format for the database, making `to_string`/`from_string` round-trip fidelity essential. \ No newline at end of file diff --git a/include/xrpl/basics/Resolver.h.ai.json b/include/xrpl/basics/Resolver.h.ai.json new file mode 100644 index 0000000000..5e6280e0f9 --- /dev/null +++ b/include/xrpl/basics/Resolver.h.ai.json @@ -0,0 +1,69 @@ +{ + "args": [ + { + "lineno": 27, + "name": "names" + }, + { + "lineno": 27, + "name": "handler" + }, + { + "lineno": 32, + "name": "names" + }, + { + "lineno": 32, + "name": "handler" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "Resolver" + } + ], + "description": "Defines the xrpl::Resolver abstract class for asynchronous and synchronous DNS resolution, including start/stop and resolve methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Resolver.h", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "stop_async" + }, + { + "args": [], + "lineno": 18, + "name": "stop" + }, + { + "args": [], + "lineno": 21, + "name": "start" + }, + { + "args": [ + "names", + "handler" + ], + "lineno": 27, + "name": "resolve" + }, + { + "args": [ + "names", + "handler" + ], + "lineno": 32, + "name": "resolve" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/Resolver.h.ai.md b/include/xrpl/basics/Resolver.h.ai.md new file mode 100644 index 0000000000..c0a45a0479 --- /dev/null +++ b/include/xrpl/basics/Resolver.h.ai.md @@ -0,0 +1,33 @@ +# `include/xrpl/basics/Resolver.h` + +## Role and Purpose + +`Resolver.h` defines the `xrpl::Resolver` abstract interface — the single extension point through which the XRPL node resolves DNS hostnames at runtime. Its job is narrow: given a batch of `host:port` strings, asynchronously resolve each one into a list of `beast::IP::Endpoint` values and deliver the results to a caller-supplied callback. The interface abstracts away the underlying I/O mechanism so that production code uses a real Boost.Asio resolver while tests can substitute a mock without touching callers. + +## Interface Design + +The class is intentionally minimal. It declares four pure-virtual methods: `start()`, `stop()`, `stop_async()`, and `resolve()`. These map directly to the lifecycle and operation of a background I/O service. The destructor is pure virtual with a non-inline out-of-line definition (provided in `ResolverAsio.cpp`), which is the standard C++ idiom for giving an abstract base class a vtable anchor without a concrete body in the header. + +The `HandlerType` alias, `std::function)>`, captures the callback contract. Each resolved name triggers one invocation, receiving both the original hostname string and the resulting address list. This pairing matters: callers often need to correlate results back to the name they submitted (e.g., to log which peer seed produced which IP), and re-passing the name through the completion avoids callers having to maintain their own lookup tables. + +## The Template/Virtual Overload Pair + +The `resolve()` method appears twice: once as a `virtual` function taking `HandlerType const&`, and once as a non-virtual `template` that wraps any callable into `HandlerType` before forwarding to the virtual overload. This is the non-virtual interface (NVI) pattern applied to templates. The motivation is that if the template version were virtual, every instantiation would need a vtable entry — impractical for a polymorphic class. Instead, the template normalises the input type, and the virtual function carries the actual polymorphic dispatch. Callers get the convenience of passing lambdas directly without boilerplate `std::function` construction. + +## Lifecycle Contract + +The three lifecycle methods reflect a service that runs on a shared Boost.Asio `io_context`. `start()` registers the resolver's reference in the pending I/O counter; `stop_async()` posts a cancellation request to the resolver's strand and returns immediately; `stop()` combines `stop_async()` with a blocking wait on a condition variable until all in-flight handlers drain. This two-phase shutdown pattern lets callers choose between fire-and-forget teardown (during orderly application shutdown where the io_context will be drained anyway) and synchronous teardown (when you need a hard guarantee that the resolver is idle before proceeding). + +## Concrete Implementation: `ResolverAsioImpl` + +The only production implementation is `ResolverAsioImpl`, which lives entirely inside `ResolverAsio.cpp` and is exposed only through the `ResolverAsio::New()` factory. This internal linkage is deliberate: the implementation is never constructed directly; ownership flows through `std::unique_ptr`. + +`ResolverAsioImpl` inherits from both `ResolverAsio` (which extends `Resolver`) and `AsyncObject`, a CRTP mixin that reference-counts outstanding completion handlers via an atomic integer. When the count drops to zero, `asyncHandlersComplete()` fires and notifies the condition variable that `stop()` is waiting on. The `CompletionCounter` RAII type is bound into every async handler so the count is maintained correctly even under cancellation paths. + +Work items are queued as `Work` structs in a `std::deque`. A subtle optimisation: names within a `Work` item are stored in reverse order (via `std::reverse_copy`), so `do_work()` pops from the back of the vector in O(1) rather than the front. The strand ensures that the queue is only ever accessed from the `io_context` thread, making no additional locking necessary on the deque itself. + +The `parseName()` helper handles two cases: if the string parses as a fully-qualified `beast::IP::Endpoint` (a raw IP address with port), it extracts the components directly without a DNS lookup. Otherwise it falls back to splitting on whitespace and `:` delimiters. This means callers can freely mix hostnames, plain IP addresses, and `host port` strings in the same batch. + +## Usage in Context + +`Application` holds a `std::unique_ptr` created at startup and passed to `OverlayImpl`. The overlay calls `resolve()` twice during startup: once for the hardcoded bootstrap IPs (including well-known XRPL Commons hub addresses) and once for the `[ips_fixed]` entries from the node's config file. Both calls use lambdas that convert the resulting `beast::IP::Endpoint` addresses into the PeerFinder's known-peers list. No caller ever interacts with the concrete `ResolverAsioImpl` type — all access flows through the `Resolver` interface, keeping the overlay's dependency on DNS mechanics entirely behind the abstraction. \ No newline at end of file diff --git a/include/xrpl/basics/ResolverAsio.h.ai.json b/include/xrpl/basics/ResolverAsio.h.ai.json new file mode 100644 index 0000000000..fbb42a4bd0 --- /dev/null +++ b/include/xrpl/basics/ResolverAsio.h.ai.json @@ -0,0 +1,34 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "ResolverAsio" + } + ], + "description": "Defines the ResolverAsio class, an implementation of the Resolver interface using Boost.Asio for asynchronous network resolution in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/ResolverAsio.h", + "functions": [ + { + "args": [], + "lineno": 9, + "name": "ResolverAsio" + }, + { + "args": [ + "boost::asio::io_context&", + "beast::Journal" + ], + "lineno": 12, + "name": "New" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/ResolverAsio.h.ai.md b/include/xrpl/basics/ResolverAsio.h.ai.md new file mode 100644 index 0000000000..09f41bd2a8 --- /dev/null +++ b/include/xrpl/basics/ResolverAsio.h.ai.md @@ -0,0 +1,25 @@ +# `include/xrpl/basics/ResolverAsio.h` + +## Role in the System + +`ResolverAsio.h` is the public header for the Boost.Asio-backed implementation of the abstract `Resolver` interface. Its purpose is narrow: it introduces one concrete subclass, `ResolverAsio`, and exposes a single static factory method. The header's brevity is intentional — it acts as an opaque handle into a non-trivial implementation that the caller never sees directly. + +## The Abstraction Layer + +`ResolverAsio` extends `Resolver`, the abstract base class defined in `Resolver.h`. That base declares the entire observable contract: `start()`, `stop()`, `stop_async()`, and a templated `resolve()` that accepts a list of hostname strings and a completion handler of type `std::function)>`. By defining `ResolverAsio` as a pure intermediate class (its constructor is `= default` and defaulted) with only a static `New()` factory, the header ensures that client code depends solely on the `Resolver` interface. The concrete work happens exclusively inside `ResolverAsioImpl`, which is defined entirely within `ResolverAsio.cpp` and is therefore invisible to any translation unit that includes this header. + +This two-level separation — abstract `Resolver` base, thin `ResolverAsio` header, hidden `ResolverAsioImpl` body — is a classic pImpl-adjacent pattern. The difference from a true pImpl is that the indirection goes through virtual dispatch rather than a pointer-to-impl member. The effect is the same: implementation details, including the Asio strand, the internal work queue, and the `AsyncObject` mixin, are completely insulated from the public API surface. + +## The Factory and Ownership Model + +`ResolverAsio::New(boost::asio::io_context&, beast::Journal)` returns a `std::unique_ptr`. The calling site in `Application.cpp` stores the result as a member and later accesses it through the `Resolver*` interface. Ownership is unambiguous: whoever holds the `unique_ptr` owns the object and is responsible for calling `stop()` before destruction. The destructor assertions in `ResolverAsioImpl` enforce this — destroying the object with pending I/O or without stopping first triggers `XRPL_ASSERT` failures. + +The `io_context` reference is non-owning and must outlive the resolver. This is a well-understood contract in Asio programming: the context drives all I/O and must not be destroyed before the objects that post work to it. + +## Why a Static Factory Over a Public Constructor? + +Making the constructor `explicit ... = default` while routing all real construction through `New()` ensures that the concrete `ResolverAsioImpl` type — with all its Asio internals — never needs to be named by consumers. This also gives the factory freedom to perform any pre-construction initialization and return the object as the abstract base pointer, guaranteeing that the caller can only interact through the `Resolver` interface without a cast. + +## Relationship to `beast::Journal` + +The `beast::Journal` parameter to `New()` is threaded directly into the implementation for structured logging of resolution queuing, stop events, and parse failures. It is stored by value rather than by reference, which is the standard practice for `Journal` — it is a lightweight handle that is cheap to copy. \ No newline at end of file diff --git a/include/xrpl/basics/SHAMapHash.h.ai.json b/include/xrpl/basics/SHAMapHash.h.ai.json new file mode 100644 index 0000000000..7165c82e7d --- /dev/null +++ b/include/xrpl/basics/SHAMapHash.h.ai.json @@ -0,0 +1,144 @@ +{ + "args": [ + { + "lineno": 14, + "name": "hash" + }, + { + "lineno": 47, + "name": "os" + }, + { + "lineno": 37, + "name": "x" + }, + { + "lineno": 37, + "name": "y" + }, + { + "lineno": 57, + "name": "h" + }, + { + "lineno": 68, + "name": "key" + } + ], + "classes": [ + { + "args": [ + "uint256 const& hash" + ], + "lineno": 10, + "name": "SHAMapHash" + } + ], + "description": "Defines the SHAMapHash class, which represents the hash of a node or the entire SHAMap in XRPL, providing utility methods and operators for hash manipulation and comparison.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/SHAMapHash.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "SHAMapHash" + }, + { + "args": [ + "uint256 const& hash" + ], + "lineno": 14, + "name": "SHAMapHash" + }, + { + "args": [], + "lineno": 18, + "name": "as_uint256" + }, + { + "args": [], + "lineno": 21, + "name": "as_uint256" + }, + { + "args": [], + "lineno": 24, + "name": "isZero" + }, + { + "args": [], + "lineno": 27, + "name": "isNonZero" + }, + { + "args": [], + "lineno": 30, + "name": "signum" + }, + { + "args": [], + "lineno": 33, + "name": "zero" + }, + { + "args": [ + "SHAMapHash const& x", + "SHAMapHash const& y" + ], + "lineno": 37, + "name": "operator==" + }, + { + "args": [ + "SHAMapHash const& x", + "SHAMapHash const& y" + ], + "lineno": 42, + "name": "operator<" + }, + { + "args": [ + "std::ostream& os", + "SHAMapHash const& x" + ], + "lineno": 47, + "name": "operator<<" + }, + { + "args": [ + "SHAMapHash const& x" + ], + "lineno": 52, + "name": "to_string" + }, + { + "args": [ + "H& h", + "SHAMapHash const& x" + ], + "lineno": 57, + "name": "hash_append" + }, + { + "args": [ + "SHAMapHash const& x", + "SHAMapHash const& y" + ], + "lineno": 63, + "name": "operator!=" + }, + { + "args": [ + "SHAMapHash const& key" + ], + "lineno": 68, + "name": "extract" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/SHAMapHash.h.ai.md b/include/xrpl/basics/SHAMapHash.h.ai.md new file mode 100644 index 0000000000..48655967f4 --- /dev/null +++ b/include/xrpl/basics/SHAMapHash.h.ai.md @@ -0,0 +1,40 @@ +# `SHAMapHash.h` — Strongly-Typed Node Hash for SHAMap + +## Role in the System + +`SHAMapHash` is a thin but intentional strong-typedef wrapper around `uint256` that represents the cryptographic hash of either a single node in the SHAMap radix tree or the hash of the entire map (which is, by definition, the hash of its root node). The XRP Ledger uses `uint256` for many semantically distinct purposes — transaction IDs, account state keys, ledger hashes, and node hashes — so wrapping the SHAMap-specific case in a distinct type lets the compiler enforce that a raw object identifier is never accidentally passed to an API that expects a node hash, or vice versa. + +## Class Design + +The class holds a single private member `hash_` of type `uint256` and provides precisely two construction paths: the default constructor (producing a zero hash), and an `explicit` single-argument constructor from `uint256 const&`. The `explicit` keyword is load-bearing here: it prevents implicit conversions from arbitrary `uint256` values into a `SHAMapHash`, which is the primary value of the wrapper type. Downstream code that wants to create a `SHAMapHash` must state that intention clearly. + +Access to the raw `uint256` value is provided through `as_uint256()`, offered in both const and mutable overloads. The mutable overload exists because certain serialization paths need to compute the hash in place. State-query helpers — `isZero()`, `isNonZero()`, `signum()`, and `zero()` — delegate directly to the corresponding `uint256` methods rather than reimplementing them, keeping the wrapper thin. + +The comparison operators `operator==` and `operator<`, along with `operator<<`, `to_string()`, and the `hash_append()` template, round out the set of operations needed to use `SHAMapHash` in containers, ordered structures, and diagnostic output. These are defined as hidden-friend functions inside the class body, which means they participate in argument-dependent lookup only when one of their arguments is a `SHAMapHash`, preventing accidental overload resolution with other types. `operator!=` is defined as a non-member inline outside the class, delegating to `operator==`. + +## The `extract()` Specialization + +The most architecturally significant piece of this header is the explicit specialization of the `extract()` function template at the bottom of the file: + +```cpp +template <> +inline std::size_t +extract(SHAMapHash const& key) +{ + return *reinterpret_cast(key.as_uint256().data()); +} +``` + +This specialization integrates `SHAMapHash` with `partitioned_unordered_map` — a concurrent-access-friendly container that splits its buckets across multiple independent maps, one per hardware thread. The `partitioned_unordered_map::partitioner()` method calls `extract(key) % partitions_` to decide which sub-map owns a given entry. For `SHAMapHash`, the partition selector is simply the first `sizeof(std::size_t)` bytes of the underlying 256-bit hash, reinterpreted as a native integer. Because SHA-512 half-digests (which generate SHAMap node hashes) have uniform bit distribution, this naive prefix extraction yields an even spread across partitions without any additional hashing. This is why the header includes `partitioned_unordered_map.h` at all — not for the map itself, but to place the `extract` specialization in the same translation unit that sees the primary template declaration. + +It is worth noting that the analogous `extract` specialization in `base_uint.h` uses `std::memcpy` to avoid potential undefined behavior from unaligned pointer access, whereas this specialization uses a direct `reinterpret_cast`. Both produce the same result on common architectures, but the `memcpy` form is more strictly correct under the C++ aliasing rules. + +## Relationship to the SHAMap Subsystem + +`SHAMapTreeNode` declares a protected `SHAMapHash hash_` member that holds the cached content hash for each tree node. Derived types — `SHAMapInnerNode`, `SHAMapLeafNode`, and their concrete variants — inherit this field and set it during construction or when their content is modified. `SHAMapInnerNode` additionally stores an array of 16 child `SHAMapHash` values inside its `TaggedPointer hashesAndChildren_` structure, enabling hash-based validity checks on child branches without requiring the child node to be loaded into memory. + +At the `SHAMap` level, `SHAMapHash` appears as the key type for cache lookups (`cacheLookup`, `canonicalize`), for fetching missing nodes from the database or peer-provided data (`fetchNodeNT`, `fetchNodeFromDB`), and as the return type of `getHash()` which exposes the map's current root hash. The `Delta` structure tracks nodes that were modified between two map versions, and the missing-node set inside `SHAMap::DeltaFinder` is typed as `std::set`, again relying on `operator<` for ordering. `SHAMapMissingNode` stores a `SHAMapHash` to report which hash was absent when a synchronization fetch failed, making it useful for targeted peer requests during ledger sync. + +## Summary + +`SHAMapHash` is a compact but effective type-safety boundary: it costs nothing at runtime — no vtable, no additional storage, no indirection — while preventing the kind of silent `uint256` category confusion that would otherwise be possible in a codebase that uses the same underlying 32-byte type for many distinct protocol-level concepts. Its `extract()` specialization is a deliberate hook into the partitioned hash map infrastructure, enabling lock-striped concurrent node caching without requiring any changes to the container itself. \ No newline at end of file diff --git a/include/xrpl/basics/SharedWeakCachePointer.h.ai.json b/include/xrpl/basics/SharedWeakCachePointer.h.ai.json new file mode 100644 index 0000000000..e7d37f7baf --- /dev/null +++ b/include/xrpl/basics/SharedWeakCachePointer.h.ai.json @@ -0,0 +1,136 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "SharedWeakCachePointer()", + "SharedWeakCachePointer(SharedWeakCachePointer const& rhs)", + "SharedWeakCachePointer(std::shared_ptr const& rhs)", + "SharedWeakCachePointer(SharedWeakCachePointer&& rhs)", + "SharedWeakCachePointer(std::shared_ptr&& rhs)" + ], + "lineno": 14, + "name": "SharedWeakCachePointer" + } + ], + "description": "Defines the SharedWeakCachePointer template class, a wrapper around std::variant for efficient storage and management of intrusive pointers in caches.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/SharedWeakCachePointer.h", + "functions": [ + { + "args": [ + "SharedWeakCachePointer const& rhs" + ], + "lineno": 18, + "name": "SharedWeakCachePointer" + }, + { + "args": [ + "std::shared_ptr const& rhs" + ], + "lineno": 22, + "name": "SharedWeakCachePointer" + }, + { + "args": [ + "SharedWeakCachePointer&& rhs" + ], + "lineno": 25, + "name": "SharedWeakCachePointer" + }, + { + "args": [ + "std::shared_ptr&& rhs" + ], + "lineno": 28, + "name": "SharedWeakCachePointer" + }, + { + "args": [ + "SharedWeakCachePointer const& rhs" + ], + "lineno": 31, + "name": "operator=" + }, + { + "args": [ + "std::shared_ptr const& rhs" + ], + "lineno": 35, + "name": "operator=" + }, + { + "args": [ + "std::shared_ptr&& rhs" + ], + "lineno": 39, + "name": "operator=" + }, + { + "args": [], + "lineno": 42, + "name": "~SharedWeakCachePointer" + }, + { + "args": [], + "lineno": 48, + "name": "getStrong" + }, + { + "args": [], + "lineno": 54, + "name": "operator bool" + }, + { + "args": [], + "lineno": 61, + "name": "reset" + }, + { + "args": [], + "lineno": 68, + "name": "get" + }, + { + "args": [], + "lineno": 74, + "name": "use_count" + }, + { + "args": [], + "lineno": 79, + "name": "expired" + }, + { + "args": [], + "lineno": 85, + "name": "lock" + }, + { + "args": [], + "lineno": 90, + "name": "isStrong" + }, + { + "args": [], + "lineno": 94, + "name": "isWeak" + }, + { + "args": [], + "lineno": 99, + "name": "convertToStrong" + }, + { + "args": [], + "lineno": 107, + "name": "convertToWeak" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/SharedWeakCachePointer.h.ai.md b/include/xrpl/basics/SharedWeakCachePointer.h.ai.md new file mode 100644 index 0000000000..74c286a643 --- /dev/null +++ b/include/xrpl/basics/SharedWeakCachePointer.h.ai.md @@ -0,0 +1,43 @@ +# `SharedWeakCachePointer` — Single-Slot Strong/Weak Pointer for Tagged Caches + +## Purpose and Motivation + +`SharedWeakCachePointer` solves a memory layout problem specific to the XRPL ledger's `TaggedCache` subsystem. A tagged cache must track every live object it has ever handed out, even after evicting that object from its hot tier. The naïve representation would store both a `std::shared_ptr` (the strong reference that keeps the object alive while it is cached) and a `std::weak_ptr` (the tracking reference after eviction) side-by-side in each cache entry. That wastes an entire control-block pointer per entry: both smart-pointer types are the same size internally, and only one is ever meaningful at a time. + +This class resolves that by wrapping `std::variant, std::weak_ptr>` in a single object. At any moment the cache entry holds *either* a strong pointer *or* a weak pointer — never both, never neither (after construction). The variant's discriminator bit is all that's needed to distinguish the two states. + +## State Model + +The class has two logical states and explicit transitions between them: + +**Strong state** — the variant holds a `std::shared_ptr`. The cache entry is "hot": it keeps the referenced object alive independently of any caller. `isStrong()` returns `true`, and `getStrong()` returns the `shared_ptr` by const reference without any atomic increment. + +**Weak state** — the variant holds a `std::weak_ptr`. The cache entry is "tracking": the object stays alive only as long as some caller holds its own `shared_ptr`. `isWeak()` returns `true`. Whether the tracked object still exists must be determined by calling `lock()` or `expired()`. + +`convertToStrong()` atomically attempts to promote a weak entry to strong by calling `weak_ptr::lock()`. If the referent has already been destroyed it returns `false` and the entry remains weak/expired. `convertToWeak()` demotes a strong entry by constructing a `weak_ptr` from the held `shared_ptr` and replacing the variant alternative. Both transitions are in-place — no allocation or deallocation, just variant reassignment. + +## Relationship to `TaggedCache` + +`TaggedCache` is the direct consumer of this class. Its inner `ValueEntry` stores a `shared_weak_combo_pointer_type` (defaulted to `SharedWeakCachePointer`) alongside a `last_access` timestamp. `ValueEntry` delegates the strong/weak distinction entirely to the wrapper: + +```cpp +bool isCached() const { return ptr && ptr.isStrong(); } +bool isWeak() const { if (!ptr) return true; return ptr.isWeak(); } +bool isExpired() const { return ptr.expired(); } +``` + +During `sweep()`, the cache iterates its entries and calls `convertToWeak()` on entries whose `last_access` has aged out. Entries that are already weak and whose `expired()` is `true` are removed from the map entirely. This two-phase lifecycle — strong while hot, weak while tracked — is the reason the pointer needs to change state in place rather than being replaced by a different type. + +## Accessor Semantics + +`lock()` is the general-purpose accessor. It works regardless of current state: it returns the held `shared_ptr` directly if the variant is strong, or calls `weak_ptr::lock()` if it is weak. `getStrong()` is a cheaper alternative when the caller already knows or expects the strong state; it returns the `shared_ptr` by const reference (avoiding a reference-count increment) and returns a static empty `shared_ptr` if the variant is currently weak. + +`operator bool()` deserves a note: it returns `true` when the variant's active alternative is `std::shared_ptr`, even if that `shared_ptr` is null (e.g., after `reset()`). It does *not* dereference the pointer. This is why `TaggedCache::ValueEntry::isCached()` combines both `ptr &&` (variant is in shared-state) and `ptr.isStrong()` (the shared pointer is non-null). `isStrong()` explicitly checks `p->get() != nullptr` and is therefore the correct predicate when null-ness matters. + +## Template Constraints and Covariance + +All constructors and assignment operators that accept a `std::shared_ptr` carry the constraint `requires std::convertible_to`. This permits initialising a `SharedWeakCachePointer` from a `shared_ptr` while preventing accidental narrowing conversions. Move overloads are provided alongside copy overloads to avoid unnecessary reference-count increments when a temporary `shared_ptr` is being transferred into the cache entry. + +## Implementation Split + +The class declaration lives in `SharedWeakCachePointer.h`, with all method bodies in `SharedWeakCachePointer.ipp`. `TaggedCache.h` includes the `.ipp` directly, which is the standard XRPL pattern for template implementations that are only needed by a single well-known consumer. This keeps the header lean and makes the dependency relationship explicit: if you include `TaggedCache.h` you get the implementation; including only the header gives you the interface for forward-declaration purposes. \ No newline at end of file diff --git a/include/xrpl/basics/SharedWeakCachePointer.ipp.ai.json b/include/xrpl/basics/SharedWeakCachePointer.ipp.ai.json new file mode 100644 index 0000000000..18626c5a50 --- /dev/null +++ b/include/xrpl/basics/SharedWeakCachePointer.ipp.ai.json @@ -0,0 +1,395 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SharedWeakCachePointer::SharedWeakCachePointer(std::shared_ptr const& rhs)" + ], + "entry_point": "SharedWeakCachePointer::SharedWeakCachePointer(std::shared_ptr const& rhs)", + "purpose": "Constructs a SharedWeakCachePointer from a std::shared_ptr if TT* is convertible to T*.", + "validation_points": [ + "C++20 concept: requires std::convertible_to (compile-time validation of pointer type compatibility)" + ] + }, + { + "call_chain": [ + "SharedWeakCachePointer::operator=(std::shared_ptr const& rhs)" + ], + "entry_point": "SharedWeakCachePointer::operator=(std::shared_ptr const& rhs)", + "purpose": "Assigns a std::shared_ptr to the SharedWeakCachePointer if TT* is convertible to T*.", + "validation_points": [ + "C++20 concept: requires std::convertible_to (compile-time validation of pointer type compatibility)" + ] + }, + { + "call_chain": [ + "SharedWeakCachePointer::SharedWeakCachePointer(std::shared_ptr&& rhs)" + ], + "entry_point": "SharedWeakCachePointer::SharedWeakCachePointer(std::shared_ptr&& rhs)", + "purpose": "Move-constructs a SharedWeakCachePointer from a std::shared_ptr if TT* is convertible to T*.", + "validation_points": [ + "C++20 concept: requires std::convertible_to (compile-time validation of pointer type compatibility)" + ] + }, + { + "call_chain": [ + "SharedWeakCachePointer::operator=(std::shared_ptr&& rhs)" + ], + "entry_point": "SharedWeakCachePointer::operator=(std::shared_ptr&& rhs)", + "purpose": "Move-assigns a std::shared_ptr to the SharedWeakCachePointer if TT* is convertible to T*.", + "validation_points": [ + "C++20 concept: requires std::convertible_to (compile-time validation of pointer type compatibility)" + ] + }, + { + "call_chain": [ + "SharedWeakCachePointer::convertToStrong()", + "SharedWeakCachePointer::isStrong()", + "std::get_if>(&combo_)" + ], + "entry_point": "SharedWeakCachePointer::convertToStrong()", + "purpose": "Converts the internal state to a strong pointer if possible.", + "validation_points": [ + "Checks if combo_ holds a std::weak_ptr and if it can be locked to a std::shared_ptr." + ] + }, + { + "call_chain": [ + "SharedWeakCachePointer::convertToWeak()", + "SharedWeakCachePointer::isWeak()", + "SharedWeakCachePointer::isStrong()", + "std::get_if>(&combo_)" + ], + "entry_point": "SharedWeakCachePointer::convertToWeak()", + "purpose": "Converts the internal state to a weak pointer if possible.", + "validation_points": [ + "Checks if combo_ holds a std::shared_ptr and converts it to std::weak_ptr." + ] + } + ], + "data_flows": [ + { + "field": "combo_", + "flow": [ + "Constructor (combo_ = rhs or combo_ = std::move(rhs))", + "Assignment operator (combo_ = rhs or combo_ = std::move(rhs))", + "Accessed by getStrong(), lock(), isStrong(), isWeak(), expired(), use_count(), get(), convertToStrong(), convertToWeak(), reset()" + ], + "origin": "Initialized in constructors (from std::shared_ptr or std::weak_ptr)", + "transformations": [ + "Set to std::shared_ptr or std::weak_ptr depending on input", + "Moved or copied as needed", + "Converted between strong/weak via convertToStrong/convertToWeak" + ], + "validated_at": "At construction/assignment: requires std::convertible_to (compile-time); at runtime: type checked via std::get_if" + }, + { + "field": "std::shared_ptr rhs (constructor/assignment input)", + "flow": [ + "Passed to constructor or assignment operator", + "combo_ = rhs or combo_ = std::move(rhs)" + ], + "origin": "User code or upstream function passes in a shared_ptr", + "transformations": [ + "Type-checked at compile time for pointer compatibility", + "Stored in combo_ as std::shared_ptr" + ], + "validated_at": "Compile-time: requires std::convertible_to" + }, + { + "field": "std::weak_ptr (internal state)", + "flow": [ + "convertToWeak() checks if combo_ holds std::shared_ptr", + "If so, combo_ = std::weak_ptr(*p)" + ], + "origin": "Created in convertToWeak() from std::shared_ptr", + "transformations": [ + "Conversion from strong to weak pointer" + ], + "validated_at": "Runtime: only if combo_ holds std::shared_ptr" + } + ], + "description": "Implements the methods for the SharedWeakCachePointer template class, which wraps a std::variant of std::shared_ptr and std::weak_ptr to efficiently manage strong and weak references for caching purposes.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "TT* convertible to T* (template parameter type)", + "validation", + "missing", + "check" + ], + "evidence": "Field TT* convertible to T* (template parameter type) validated by C++20 concepts (std::convertible_to)", + "issue_pattern": "Missing validation for TT* convertible to T* (template parameter type)", + "why_false_positive": "C++20 concepts (std::convertible_to) validates TT* convertible to T* (template parameter type) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "TT* convertible to T* (template parameter type)", + "empty", + "string", + "validation" + ], + "evidence": "C++20 concepts (std::convertible_to) at template constructors and assignment operators (e.g., SharedWeakCachePointer(std::shared_ptr const& rhs))", + "issue_pattern": "Missing empty string validation for TT* convertible to T* (template parameter type)", + "why_false_positive": "C++20 concepts (std::convertible_to) validates TT* convertible to T* (template parameter type) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "TT* convertible to T* (template parameter type)", + "type", + "validation", + "check" + ], + "evidence": "C++20 concepts (std::convertible_to) at template constructors and assignment operators (e.g., SharedWeakCachePointer(std::shared_ptr const& rhs))", + "issue_pattern": "Missing type validation for TT* convertible to T* (template parameter type)", + "why_false_positive": "C++20 concepts (std::convertible_to) validates TT* convertible to T* (template parameter type) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/SharedWeakCachePointer.ipp", + "functions": [ + { + "args": [ + "SharedWeakCachePointer const& rhs" + ], + "lineno": 5, + "name": "SharedWeakCachePointer" + }, + { + "args": [ + "std::shared_ptr const& rhs" + ], + "lineno": 9, + "name": "SharedWeakCachePointer" + }, + { + "args": [ + "SharedWeakCachePointer&& rhs" + ], + "lineno": 14, + "name": "SharedWeakCachePointer" + }, + { + "args": [ + "std::shared_ptr&& rhs" + ], + "lineno": 18, + "name": "SharedWeakCachePointer" + }, + { + "args": [ + "SharedWeakCachePointer const& rhs" + ], + "lineno": 24, + "name": "operator=" + }, + { + "args": [ + "std::shared_ptr const& rhs" + ], + "lineno": 29, + "name": "operator=" + }, + { + "args": [ + "std::shared_ptr&& rhs" + ], + "lineno": 36, + "name": "operator=" + }, + { + "args": [], + "lineno": 43, + "name": "~SharedWeakCachePointer" + }, + { + "args": [], + "lineno": 48, + "name": "getStrong" + }, + { + "args": [], + "lineno": 58, + "name": "operator bool" + }, + { + "args": [], + "lineno": 63, + "name": "reset" + }, + { + "args": [], + "lineno": 68, + "name": "get" + }, + { + "args": [], + "lineno": 73, + "name": "use_count" + }, + { + "args": [], + "lineno": 81, + "name": "expired" + }, + { + "args": [], + "lineno": 89, + "name": "lock" + }, + { + "args": [], + "lineno": 99, + "name": "isStrong" + }, + { + "args": [], + "lineno": 106, + "name": "isWeak" + }, + { + "args": [], + "lineno": 111, + "name": "convertToStrong" + }, + { + "args": [], + "lineno": 126, + "name": "convertToWeak" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, tests for SharedWeakCachePointer would be found in files like 'SharedWeakCachePointer_test.cpp' or similar in the test/unit directory. The main validation (std::convertible_to) is a compile-time check and would be tested by attempting to instantiate SharedWeakCachePointer with various pointer types. Runtime behaviors (convertToStrong, convertToWeak, lock, expired, etc.) should be covered by unit tests that check state transitions and pointer validity. Gaps: If tests do not attempt invalid type conversions, the compile-time validation may not be explicitly tested. Also, edge cases (e.g., null pointers, expired weak pointers) should be tested to ensure correct runtime behavior.", + "validation_architecture": { + "auto_validated_fields": [ + "TT* convertible to T* (template parameter type)" + ], + "framework": "C++20 concepts (std::convertible_to)", + "validation_layer": "compile-time (template instantiation/type checking)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Compilation error (static_assertion failure, concept not satisfied)", + "field": "TT* convertible to T* (template parameter type)", + "location": "template constructors and assignment operators (e.g., SharedWeakCachePointer(std::shared_ptr const& rhs))", + "validated_by": "C++20 concepts (std::convertible_to)", + "validates": [ + "Ensures TT* can be converted to T* before allowing construction or assignment" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/SharedWeakCachePointer.ipp.ai.md b/include/xrpl/basics/SharedWeakCachePointer.ipp.ai.md new file mode 100644 index 0000000000..805d6263ea --- /dev/null +++ b/include/xrpl/basics/SharedWeakCachePointer.ipp.ai.md @@ -0,0 +1,39 @@ +# `SharedWeakCachePointer.ipp` — Template Method Implementations + +## Role in the System + +This file provides the out-of-line method implementations for `SharedWeakCachePointer`, declared in `SharedWeakCachePointer.h`. Because the class is a template, the definitions live in a `.ipp` file rather than a `.cpp` file; `TaggedCache.h` `#include`s this `.ipp` directly to ensure the definitions are available at the point of instantiation. + +`SharedWeakCachePointer` is the core pointer abstraction that makes `TaggedCache`'s two-tier lifecycle work. Each `ValueEntry` in the cache map holds one of these, and the variant state — strong or weak — encodes which tier an object occupies: strongly-referenced objects are "hot" and kept alive by the cache itself; weakly-referenced objects are "warm" and tracked but not kept alive, surviving only as long as external code holds a `shared_ptr` to them. + +## Internal Representation + +The private `combo_` member is a `std::variant, std::weak_ptr>`. The design comment in the header is precise: this uses less memory than holding both pointers simultaneously. Because the variant can only hold one alternative at a time, storage is the size of the larger alternative plus discriminant overhead — a meaningful saving in a map that may contain millions of ledger-object entries. + +The variant discriminant is interrogated throughout via `std::get_if`, which returns a pointer to the held alternative (or `nullptr` if the wrong alternative is active), avoiding exceptions and enabling the no-throw invariants. + +## Lifecycle Management: `convertToStrong` and `convertToWeak` + +These two methods are the reason the class exists. `convertToWeak()` upgrades a hot cache entry to a warm one by replacing the `shared_ptr` alternative with a `std::weak_ptr` constructed from it, releasing the cache's own ownership stake. If the object still has external owners, `weak_ptr::lock()` will continue to succeed; once all external owners drop their handles the object is collected and subsequent `lock()` calls return null. + +`convertToStrong()` does the reverse during a cache-hit path: it calls `weak_ptr::lock()` and, if successful, replaces the variant with the resulting `shared_ptr`, reasserting the cache's ownership. Both methods are idempotent — calling them when already in the target state is a harmless no-op returning `true`. + +`TaggedCache::sweepHelper()` depends on this pair to implement the sweep lifecycle without erasing entries from the map: entries that haven't been accessed recently are demoted to weak; entries whose weak pointer has also expired are removed entirely. + +## Accessor Semantics + +**`getStrong()` vs `lock()`** serve different call sites. `getStrong()` returns a `const&` to the held `shared_ptr` — if the variant is in the weak state it returns a reference to a static empty `shared_ptr`. This avoids incrementing the reference count and is appropriate when the caller already knows the pointer is strong (e.g., during a cache insertion). `lock()` unconditionally produces a new owning `shared_ptr` by either copying the strong alternative or calling `weak_ptr::lock()`, and is the safe choice when the strength is uncertain. + +**`operator bool()`** returns `true` only if the variant holds a `shared_ptr` alternative and that pointer is non-null. A slot in weak state always returns `false`, which is deliberately asymmetric: code that checks `if (entry.ptr)` treats weak entries the same as empty entries, simplifying `TaggedCache::ValueEntry::isCached()` and `isWeak()`. + +**`expired()`** has a slightly subtle implementation: for the `weak_ptr` alternative it delegates to `weak_ptr::expired()`; for the `shared_ptr` alternative it returns `false` (a live strong pointer is never expired). A null strong pointer — i.e., a `shared_ptr{}` stored after `reset()` — falls through to the final `return !std::get_if>(&combo_)`, which evaluates to `true` because `get_if` returns a non-null pointer to the held (but null-content) `shared_ptr`. This edge case correctly marks a reset slot as expired. + +**`reset()`** explicitly stores a default-constructed `shared_ptr{}` into the variant rather than relying on any implicit state. This ensures the variant is in the `shared_ptr` alternative (the "null strong" state), preventing future calls from hitting the `weak_ptr` branch unexpectedly. + +## Type Safety via C++20 Concepts + +Every constructor and assignment operator that accepts a `shared_ptr` is gated by `requires std::convertible_to`. This enforces covariance at compile time: you can construct a `SharedWeakCachePointer` from a `shared_ptr` only if `Derived*` is implicitly convertible to `Base*`. No runtime check or `dynamic_cast` is involved. This mirrors the implicit conversions already present in `std::shared_ptr`'s own converting constructors, maintaining a familiar interface contract. + +## Concurrency Considerations + +`SharedWeakCachePointer` itself carries no internal lock. Concurrent mutation is the responsibility of the surrounding `TaggedCache`, which guards all access to `ValueEntry::ptr` through its `m_mutex`. The lack of internal synchronization is intentional: a per-pointer lock would add overhead to every cache access for a case where the surrounding container already serializes operations. \ No newline at end of file diff --git a/include/xrpl/basics/SlabAllocator.h.ai.json b/include/xrpl/basics/SlabAllocator.h.ai.json new file mode 100644 index 0000000000..6d3ea72860 --- /dev/null +++ b/include/xrpl/basics/SlabAllocator.h.ai.json @@ -0,0 +1,110 @@ +{ + "args": [ + { + "lineno": 17, + "name": "Type" + }, + { + "lineno": 209, + "name": "Type" + } + ], + "classes": [ + { + "args": [ + "extra", + "alloc", + "align" + ], + "lineno": 18, + "name": "SlabAllocator" + }, + { + "args": [ + "next", + "data", + "size", + "item" + ], + "lineno": 24, + "name": "SlabBlock" + }, + { + "args": [ + "cfg" + ], + "lineno": 210, + "name": "SlabAllocatorSet" + }, + { + "args": [ + "extra_", + "alloc_", + "align_" + ], + "lineno": 222, + "name": "SlabConfig" + } + ], + "description": "Implements a slab allocator and a set of slab allocators for efficient memory management of fixed-size objects, with support for alignment and extra bytes, intended for use in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/SlabAllocator.h", + "functions": [ + { + "args": [ + "p" + ], + "lineno": 54, + "name": "own" + }, + { + "args": [], + "lineno": 59, + "name": "allocate" + }, + { + "args": [ + "ptr" + ], + "lineno": 77, + "name": "deallocate" + }, + { + "args": [], + "lineno": 120, + "name": "size" + }, + { + "args": [], + "lineno": 129, + "name": "allocate" + }, + { + "args": [ + "ptr" + ], + "lineno": 186, + "name": "deallocate" + }, + { + "args": [ + "extra" + ], + "lineno": 246, + "name": "allocate" + }, + { + "args": [ + "ptr" + ], + "lineno": 266, + "name": "deallocate" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/SlabAllocator.h.ai.md b/include/xrpl/basics/SlabAllocator.h.ai.md new file mode 100644 index 0000000000..5429683f8f --- /dev/null +++ b/include/xrpl/basics/SlabAllocator.h.ai.md @@ -0,0 +1,84 @@ +# `SlabAllocator.h` — Fixed-Size Slab Memory Allocator + +## Why This File Exists + +Standard `malloc`/`new` carries costs that matter at XRPL's throughput: lock contention inside the system allocator, per-object bookkeeping overhead, and heap fragmentation that degrades TLB efficiency over time. `SlabAllocator.h` replaces this with a classic slab strategy: carve a large pre-aligned region into uniform slots, maintain a per-block free list, and serve allocations in O(1) without touching the system heap. The primary consumer is `SHAMapItem` — a high-churn node type that pairs a fixed header with a variable-length data payload living immediately after it in memory. + +## `SlabAllocator`: Structure and Internals + +### `SlabBlock` — Self-Hosting Memory Region + +The central internal structure is `SlabBlock`. Each slab is a single `boost::alignment::aligned_alloc` call; the `SlabBlock` header is placement-new'd at the very start of that buffer, and the item pool occupies the rest. This self-hosting layout means only one system allocation is needed per slab regardless of how many objects it holds. + +The constructor walks the entire pool and links every item-sized slot into a singly-linked free list. The link pointer is written with `std::memcpy` rather than a direct pointer cast: + +```cpp +std::memcpy(data, &l_, sizeof(std::uint8_t*)); +``` + +This idiom appears three times in the file. It is not premature caution — writing through a `uint8_t*` to store a `uint8_t**` value violates strict aliasing rules, which is undefined behavior that optimizers can miscompile in subtle ways. `memcpy` is the standard-blessed type-pun that compilers reliably lower to a plain store. + +Each `SlabBlock` carries its own `std::mutex` protecting only its own free list (`l_`). Per-block rather than per-allocator locking is deliberate: under concurrent load, multiple threads can allocate from different slabs simultaneously without contending. The `own()` method is a pointer range check against `[p_, p_ + size_)` — O(1) and branch-predictor-friendly, relying on the pool's contiguity. + +### Lock-Free Slab Growth + +The set of active `SlabBlock` instances forms a lock-free singly-linked list through `std::atomic slabs_`. When every existing slab is exhausted, `allocate()` allocates a fresh buffer at a **2 MiB boundary** — not an aesthetic choice, but a deliberate alignment to enable Linux transparent huge pages. For allocations ≥ 4 MiB, a `madvise(buf, size, MADV_HUGEPAGE)` hint is issued on Linux, potentially reducing TLB pressure significantly under memory-intensive workloads. + +The new slab is linked with a `compare_exchange_weak` CAS loop: + +```cpp +while (!slabs_.compare_exchange_weak( + slab->next_, slab, std::memory_order_release, std::memory_order_relaxed)) + ; +``` + +This is the standard lock-free list prepend. A subtle consequence: two threads could concurrently decide that all existing slabs are exhausted, both allocate new slabs, and both successfully link them. The result is a wasted slab's worth of memory in that rare race. The design explicitly accepts this for the sake of eliminating a global growth lock — correct, since slab growth events are infrequent and slab memory is not small. + +### The Intentional Destructor Leak + +The destructor body is empty with a `FIXME` comment explaining that releasing slab memory at shutdown is unsafe: there is no mechanism to guarantee that objects constructed inside the slab have been destroyed first. XRPL's shutdown model does not provide this guarantee, so the destructor deliberately leaks all slab memory. A controlled leak is far safer than a use-after-free. + +### Constructor Parameters and Disabled Allocators + +`SlabAllocator(extra, alloc, align)` accepts: +- `extra`: additional bytes beyond `sizeof(Type)` per slot (for trailing payload) +- `alloc`: slab size in bytes; **0 means the allocator is permanently disabled** and will always return `nullptr` — an explicit design affordance for environments needing minimal memory usage +- `align`: override for per-slot alignment; defaults to `alignof(Type)` + +`itemSize_` is computed as `align_up(sizeof(Type) + extra, itemAlignment_)`, ensuring every slot satisfies the type's alignment requirements including any trailing data. + +Two `static_assert`s enforce hard constraints: `sizeof(Type) >= sizeof(uint8_t*)` (the free-list pointer must fit inside the slot it inhabits), and `alignof(Type)` must be 4 or 8. + +## `SlabAllocatorSet`: Tiered Dispatch + +`SlabAllocatorSet` groups up to 64 `SlabAllocator` instances in a `boost::container::static_vector` — a fixed-capacity, stack-allocated container that avoids a heap allocation for the allocator array itself. Allocators are sorted by item size at construction and validated for uniqueness; duplicate sizes throw `std::runtime_error` at startup, appropriate since this is a static configuration error. + +`allocate(extra)` performs a linear scan for the smallest slot that can fit `sizeof(Type) + extra`, short-circuiting through the `maxSize_` fast path: + +```cpp +if (auto const size = sizeof(Type) + extra; size <= maxSize_) +``` + +If no configured tier can satisfy the request, `nullptr` is returned immediately without scanning. This lets callers implement a transparent fallback to `operator new[]` without coupling the allocator to any specific failure policy. + +`deallocate(ptr)` iterates across allocators and relies on each `SlabAllocator::deallocate()` returning `bool` to indicate ownership. The return value propagates to the caller so it can distinguish "pointer owned by the slab" from "pointer was allocated some other way." + +`SlabConfig` is a nested public value type exposing the `(extra, alloc, align)` triple, with `SlabAllocatorSet` declared as a `friend` to access its private fields. This keeps the construction API declarative without polluting the public interface with setters. + +## How This Is Used: `SHAMapItem` + +The concrete deployment is in `SHAMapItem.h`, where an `inline` global `SlabAllocatorSet` named `slabber` is configured with seven size tiers: + +```cpp +inline SlabAllocatorSet slabber({ + { 128, megabytes(60) }, + { 192, megabytes(46) }, + { 272, megabytes(60) }, + { 384, megabytes(56) }, + { 564, megabytes(40) }, + { 772, megabytes(46) }, + { 1052, megabytes(60) }, +}); +``` + +The sizes and slab capacities are manually tuned to match the expected distribution of ledger object sizes and to minimize intra-slab slack. `make_shamapitem()` calls `slabber.allocate(data.size())`, falls back to `new std::uint8_t[sizeof(SHAMapItem) + data.size()]` if the slab can't satisfy the request, then placement-new's the `SHAMapItem` into the raw memory. The matching `intrusive_ptr_release()` calls `slabber.deallocate()`, falling back to `delete[]` if the pointer wasn't slab-owned. This opt-in, fallback-capable design means `SlabAllocator` never needs to handle arbitrarily sized allocations and the caller never hard-depends on the slab being able to serve the request. \ No newline at end of file diff --git a/include/xrpl/basics/Slice.h.ai.json b/include/xrpl/basics/Slice.h.ai.json new file mode 100644 index 0000000000..f79de8ace6 --- /dev/null +++ b/include/xrpl/basics/Slice.h.ai.json @@ -0,0 +1,86 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "void", + "Slice const&", + "void const*, std::size_t" + ], + "lineno": 16, + "name": "Slice" + } + ], + "description": "Defines the xrpl::Slice class, an immutable, non-owning, lightweight view over a linear range of bytes, along with related utility functions and operators for handling byte slices.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Slice.h", + "functions": [ + { + "args": [ + "Hasher& h", + "Slice const& v" + ], + "lineno": 98, + "name": "hash_append" + }, + { + "args": [ + "Slice const& lhs", + "Slice const& rhs" + ], + "lineno": 104, + "name": "operator==" + }, + { + "args": [ + "Slice const& lhs", + "Slice const& rhs" + ], + "lineno": 112, + "name": "operator!=" + }, + { + "args": [ + "Slice const& lhs", + "Slice const& rhs" + ], + "lineno": 117, + "name": "operator<" + }, + { + "args": [ + "Stream& s", + "Slice const& v" + ], + "lineno": 124, + "name": "operator<<" + }, + { + "args": [ + "std::array const& a" + ], + "lineno": 131, + "name": "makeSlice" + }, + { + "args": [ + "std::vector const& v" + ], + "lineno": 137, + "name": "makeSlice" + }, + { + "args": [ + "std::basic_string const& s" + ], + "lineno": 143, + "name": "makeSlice" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/Slice.h.ai.md b/include/xrpl/basics/Slice.h.ai.md new file mode 100644 index 0000000000..5452eb9191 --- /dev/null +++ b/include/xrpl/basics/Slice.h.ai.md @@ -0,0 +1,33 @@ +# `xrpl/basics/Slice.h` — Immutable Byte-Range View + +`Slice` is the foundational non-owning byte-view type for the XRPL codebase. It fills the same conceptual role as `std::string_view` or `std::span` but predates both, and is tightly integrated with XRPL's binary protocol handling infrastructure. + +## Core Design + +The class holds exactly two fields: a `const uint8_t*` pointer and a `size_t` length. This makes it trivially copyable — two words on any 64-bit platform — and safe to pass by value everywhere, which is the intended usage. A default-constructed `Slice` is always valid, representing an empty range. There is no null-vs-empty distinction: an empty `Slice` is simply one with `size_ == 0`, and a null pointer is only possible when size is also zero. + +The class is explicitly `const`-only. There is no non-const `data()` accessor and no mutable iterator. This is deliberate: when a subsystem needs to mutate bytes, it uses `Buffer` (the companion owning type in `Buffer.h`). `Slice` is the read-only transport token. + +## Advancing and Trimming + +Two families of mutators allow consuming a byte stream in-place. `operator+=` / `operator+` advance the start of the slice and throw `std::domain_error` (via the `Throw<>` contract helper from `contract.h`) if `n > size_`. This makes them appropriate for protocol parsing loops where overrun is an error condition. In contrast, `remove_prefix` and `remove_suffix` perform no bounds checking, mirroring the intentionally unchecked semantics of `std::string_view::remove_prefix`. Callers are responsible for validating bounds before calling these. The distinction is important: the `+=` operator is for defensive parsing code, while `remove_prefix`/`remove_suffix` are for tight paths where invariants are already established. + +`substr()` follows `std::string_view::substr` semantics exactly: it throws `std::out_of_range` if `pos > size()`, but clamps `count` to avoid running past the end. This means `substr(pos)` reliably returns the tail of a slice without requiring the caller to compute the remaining length. + +## Indexing and Hashing + +`operator[]` is guarded only by `XRPL_ASSERT`, which is a debug-mode check via the Beast instrumentation layer. This trades safety in release builds for performance in hot paths, consistent with how the rest of the protocol code handles per-byte access. Callers are expected to validate bounds externally. + +The `hash_append` template function integrates `Slice` with XRPL's open hashing protocol. Any hasher that satisfies the `Hasher` concept (takes a pointer and byte-count) can hash a `Slice` directly. This is how `base_uint` instances, serialized ledger objects, and other byte sequences are hashed for use in unordered containers and the SHAMap. + +## Stream Output and Comparisons + +`operator<<` renders a `Slice` as its uppercase hex representation via `strHex`, which in turn uses `boost::algorithm::hex`. This hex rendering appears throughout XRPL logging and diagnostic output. The equality operator uses `std::memcmp` (fast, no locale concerns) and the less-than operator uses `std::lexicographical_compare`, giving `Slice` a total order suitable for use in sorted containers. + +## The `makeSlice` Factory Functions + +The three `makeSlice` overloads provide safe, implicit-free construction from common standard containers. The array and vector overloads are constrained via `std::enable_if` to only accept `T = char` or `T = unsigned char`, preventing the accidental construction of a `Slice` from a `std::vector` or similar. The `std::basic_string` overload has no such constraint since `char` is always the element type. These factories centralize the `reinterpret_cast` that is otherwise unavoidable when going from `char*` to `uint8_t*`, keeping that unsafe operation in one place. + +## Relationship to `Buffer` + +`Buffer` is the owning counterpart: it manages a heap-allocated `unique_ptr` and provides an implicit conversion `operator Slice()`. The explicit constructor `Buffer(Slice)` deep-copies from a slice. The assignment `Buffer& operator=(Slice)` includes an XRPL_ASSERT guard to detect the case where the source slice is a subset of the buffer being overwritten — a subtle aliasing bug that would otherwise silently corrupt data. Together, `Slice` (non-owning, cheap, immutable) and `Buffer` (owning, allocated, mutable) form the complete binary data vocabulary used throughout the XRPL protocol layer, including `Serializer`, `STBlob`, `SHAMapItem`, cryptographic key types, and the conditions/fulfillments subsystem. \ No newline at end of file diff --git a/include/xrpl/basics/StringUtilities.h.ai.json b/include/xrpl/basics/StringUtilities.h.ai.json new file mode 100644 index 0000000000..62aa028866 --- /dev/null +++ b/include/xrpl/basics/StringUtilities.h.ai.json @@ -0,0 +1,125 @@ +{ + "args": [ + { + "lineno": 18, + "name": "blob" + }, + { + "lineno": 22, + "name": "strSize" + }, + { + "lineno": 22, + "name": "begin" + }, + { + "lineno": 22, + "name": "end" + }, + { + "lineno": 61, + "name": "strSrc" + }, + { + "lineno": 66, + "name": "strSrc" + }, + { + "lineno": 81, + "name": "pUrl" + }, + { + "lineno": 81, + "name": "strUrl" + }, + { + "lineno": 83, + "name": "str" + }, + { + "lineno": 85, + "name": "s" + }, + { + "lineno": 93, + "name": "domain" + } + ], + "classes": [ + { + "args": [], + "lineno": 70, + "name": "parsedURL" + } + ], + "description": "Provides utility functions for handling binary data, hexadecimal encoding/decoding, URL parsing, string trimming, and TOML domain validation in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/StringUtilities.h", + "functions": [ + { + "args": [ + "blob" + ], + "lineno": 18, + "name": "sqlBlobLiteral" + }, + { + "args": [ + "strSize", + "begin", + "end" + ], + "lineno": 22, + "name": "strUnHex" + }, + { + "args": [ + "strSrc" + ], + "lineno": 61, + "name": "strUnHex" + }, + { + "args": [ + "strSrc" + ], + "lineno": 66, + "name": "strViewUnHex" + }, + { + "args": [ + "pUrl", + "strUrl" + ], + "lineno": 81, + "name": "parseUrl" + }, + { + "args": [ + "str" + ], + "lineno": 83, + "name": "trim_whitespace" + }, + { + "args": [ + "s" + ], + "lineno": 85, + "name": "to_uint64" + }, + { + "args": [ + "domain" + ], + "lineno": 93, + "name": "isProperlyFormedTomlDomain" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/StringUtilities.h.ai.md b/include/xrpl/basics/StringUtilities.h.ai.md new file mode 100644 index 0000000000..b46febea29 --- /dev/null +++ b/include/xrpl/basics/StringUtilities.h.ai.md @@ -0,0 +1,54 @@ +# `include/xrpl/basics/StringUtilities.h` + +## Role and Purpose + +This header is the central string manipulation toolkit for the `xrpl` namespace, gathering five loosely-related but frequently needed operations: SQLite blob escaping, hex decoding, URL parsing, whitespace trimming, integer parsing, and TOML domain validation. These utilities are used throughout the node's database layer, RPC subsystem, configuration parser, and peer-handshake code, making this one of the more broadly depended-upon headers in the `basics` module. + +The header deliberately keeps its own template logic (the `strUnHex` family) inline while deferring everything regex-heavy or Boost-heavy to the `.cpp` implementation, keeping compile times manageable for translation units that only need hex conversion. + +--- + +## Key Components + +### Hex Decoding — `strUnHex` + +The primary workhorse is the templated `strUnHex(strSize, begin, end)`. Rather than calling a library function, it builds a `static constexpr` 256-entry lookup table at compile time. The table maps every `unsigned char` value to its nibble value (0–15) or -1 for invalid characters, supporting both upper- and lower-case hex. Returning -1 for invalid bytes (rather than throwing) allows a cheap validity check without exception overhead. + +The design handles **odd-length hex strings** explicitly: if `strSize` is odd, the first character is decoded alone as a high nibble, making `"A"` decode to `\x0A` rather than failing. This matters in practice; the test suite verifies `"D0A"` decodes to the two-byte sequence `"\r\n"`. + +Two thin wrappers, `strUnHex(std::string const&)` and `strViewUnHex(std::string_view)`, exist solely to spare callers from passing the string size and iterators by hand. Both return `std::optional`, using `std::nullopt` to signal a malformed or invalid input — consistent with the xrpl codebase's preference for value-returning error signaling over exceptions in hot paths. + +The companion `strHex.h` (included here) provides the inverse direction via `boost::algorithm::hex`, giving callers both encode and decode in a single include. `Blob` — a `std::vector` — is the shared currency between them. + +### SQLite Blob Literals — `sqlBlobLiteral` + +`sqlBlobLiteral(Blob const&)` produces SQLite's `X'HEXDATA'` literal syntax, used when constructing raw SQL queries that embed binary ledger objects. It is called in `AcceptedLedgerTx::getEscMeta()` (which encodes raw transaction metadata for the ledger SQLite store) and in `STTx` serialization. The function pre-reserves `size * 2 + 3` characters to avoid reallocations, then uses `boost::algorithm::hex` for the hex encoding, sandwiched between the `X'` prefix and `'` suffix. The existence of this function keeps SQL-escaping concerns out of the objects that own the binary data. + +### URL Parsing — `parsedURL` and `parseUrl` + +`parsedURL` is a plain-data aggregate holding scheme, username, password, domain, an optional `uint16_t` port, and path. The equality operator omits `username` and `password`, which matters for connection deduplication: two endpoints with the same scheme, domain, port, and path are considered the same regardless of credentials. + +`parseUrl(parsedURL&, std::string const&)` drives a static `boost::regex` against the RFC 3986 authority-form URI pattern. Several non-obvious decisions are worth noting: + +- **IPv6 bracket stripping**: After the regex extracts the host segment, the result is passed through `beast::IP::Endpoint::from_string_checked` to strip the surrounding brackets from IPv6 addresses (e.g., `[::1]` becomes `::1`). Doing this via the IP endpoint parser means the bracket removal is validated rather than naïve substring manipulation. +- **Port overflow rejection**: Ports larger than 65535 cause `beast::lexicalCast` to return 0, and the function treats port 0 as a parse failure and returns `false`. This prevents silent misrouting to port 0. +- **Scheme normalization**: The scheme is converted to lowercase unconditionally, so callers can do case-insensitive scheme comparison without extra work. +- **Exception safety**: The regex match is wrapped in a bare `catch(...)` that returns `false`. This guards against `boost::regex` throwing on pathological input, which can happen with certain degenerate strings. + +`parseUrl` is called in `RPCSub` (WebSocket subscription URLs) and `ValidatorSite` (validator list fetch URLs), making robustness to malformed user input essential. + +### TOML Domain Validation — `isProperlyFormedTomlDomain` + +`isProperlyFormedTomlDomain(std::string_view)` validates that a string looks like a plausible internet domain for the purpose of fetching TOML-based validator metadata. The header comment explicitly warns this function is **not** a strict domain validity check — it rejects obviously bad inputs but may also reject some valid internationalized domain names (IDNs). The regex in the `.cpp` enforces label-level rules (no leading/trailing hyphens, alphanumeric plus hyphen, 1–63 characters per label) and requires at least one dot with a 2–63 character alphabetic TLD. Length is gated first (4–128 characters) before regex evaluation to avoid unnecessary overhead. This function is used in `Config.cpp` and `Handshake.cpp` to validate `[validator_token]` domain fields before attempting TOML file fetches. + +### Miscellaneous Helpers + +`trim_whitespace(std::string)` takes its argument by value and delegates to `boost::trim` in place, returning the result. The by-value parameter communicates intent: the caller's string is not modified, but no extra copy is needed when passing a temporary. + +`to_uint64(std::string const&)` wraps `beast::lexicalCastChecked` to return an `std::optional`, converting the library's boolean-plus-out-parameter convention into a modern value-returning form. + +--- + +## Design Notes + +The mix of inline template functions and opaque declarations is deliberate: `strUnHex` is generic over iterator types and cannot live in a `.cpp`, while `parseUrl` and `isProperlyFormedTomlDomain` carry static `boost::regex` objects that are expensive to initialize and must be compiled once. The header's `#include` of `strHex.h` and `Blob.h` closes the encode/decode loop for callers who need both directions of hex conversion, and the `boost/format.hpp` include (present in the header but not actively used by any declared function) suggests this header accumulated dependencies over time rather than being designed from scratch. \ No newline at end of file diff --git a/include/xrpl/basics/TaggedCache.h.ai.json b/include/xrpl/basics/TaggedCache.h.ai.json new file mode 100644 index 0000000000..0e45808ce7 --- /dev/null +++ b/include/xrpl/basics/TaggedCache.h.ai.json @@ -0,0 +1,323 @@ +{ + "args": [ + { + "lineno": 32, + "name": "name" + }, + { + "lineno": 32, + "name": "size" + }, + { + "lineno": 32, + "name": "expiration" + }, + { + "lineno": 32, + "name": "clock" + }, + { + "lineno": 32, + "name": "journal" + }, + { + "lineno": 32, + "name": "collector" + }, + { + "lineno": 64, + "name": "key" + }, + { + "lineno": 86, + "name": "data" + }, + { + "lineno": 86, + "name": "replaceCallback" + }, + { + "lineno": 106, + "name": "value" + }, + { + "lineno": 132, + "name": "digest" + }, + { + "lineno": 132, + "name": "h" + }, + { + "lineno": 139, + "name": "l" + }, + { + "lineno": 144, + "name": "prefix" + }, + { + "lineno": 144, + "name": "handler" + }, + { + "lineno": 163, + "name": "last_access_" + }, + { + "lineno": 175, + "name": "ptr_" + }, + { + "lineno": 181, + "name": "when_expire" + }, + { + "lineno": 181, + "name": "now" + }, + { + "lineno": 181, + "name": "partition" + }, + { + "lineno": 181, + "name": "stuffToSweep" + }, + { + "lineno": 181, + "name": "allRemovals" + } + ], + "classes": [ + { + "args": [ + "name", + "size", + "expiration", + "clock", + "journal", + "collector" + ], + "lineno": 18, + "name": "TaggedCache" + }, + { + "args": [ + "prefix", + "handler", + "collector" + ], + "lineno": 144, + "name": "Stats" + }, + { + "args": [ + "last_access_" + ], + "lineno": 163, + "name": "KeyOnlyEntry" + }, + { + "args": [ + "last_access_", + "ptr_" + ], + "lineno": 175, + "name": "ValueEntry" + } + ], + "description": "Implements a thread-safe tagged cache/map combination for storing and managing objects with strong and weak references, supporting cache expiration, concurrency, and metrics collection.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/TaggedCache.h", + "functions": [ + { + "args": [ + "name", + "size", + "expiration", + "clock", + "journal", + "collector" + ], + "lineno": 32, + "name": "TaggedCache" + }, + { + "args": [], + "lineno": 41, + "name": "clock" + }, + { + "args": [], + "lineno": 44, + "name": "size" + }, + { + "args": [], + "lineno": 47, + "name": "getCacheSize" + }, + { + "args": [], + "lineno": 50, + "name": "getTrackSize" + }, + { + "args": [], + "lineno": 53, + "name": "getHitRate" + }, + { + "args": [], + "lineno": 56, + "name": "clear" + }, + { + "args": [], + "lineno": 59, + "name": "reset" + }, + { + "args": [ + "key" + ], + "lineno": 64, + "name": "touch_if_exists" + }, + { + "args": [], + "lineno": 71, + "name": "sweep" + }, + { + "args": [ + "key", + "valid" + ], + "lineno": 73, + "name": "del" + }, + { + "args": [ + "key", + "data", + "replaceCallback" + ], + "lineno": 86, + "name": "canonicalize" + }, + { + "args": [ + "key", + "data" + ], + "lineno": 95, + "name": "canonicalize_replace_cache" + }, + { + "args": [ + "key", + "data" + ], + "lineno": 97, + "name": "canonicalize_replace_client" + }, + { + "args": [ + "key" + ], + "lineno": 99, + "name": "fetch" + }, + { + "args": [ + "key", + "value" + ], + "lineno": 106, + "name": "insert" + }, + { + "args": [ + "key" + ], + "lineno": 110, + "name": "insert" + }, + { + "args": [ + "key", + "data" + ], + "lineno": 117, + "name": "retrieve" + }, + { + "args": [], + "lineno": 120, + "name": "peekMutex" + }, + { + "args": [], + "lineno": 123, + "name": "getKeys" + }, + { + "args": [], + "lineno": 127, + "name": "rate" + }, + { + "args": [ + "digest", + "h" + ], + "lineno": 132, + "name": "fetch" + }, + { + "args": [ + "key", + "l" + ], + "lineno": 139, + "name": "initialFetch" + }, + { + "args": [], + "lineno": 141, + "name": "collect_metrics" + }, + { + "args": [ + "when_expire", + "now", + "partition", + "stuffToSweep", + "allRemovals", + "" + ], + "lineno": 181, + "name": "sweepHelper" + }, + { + "args": [ + "when_expire", + "now", + "partition", + "stuffToSweep", + "allRemovals", + "" + ], + "lineno": 190, + "name": "sweepHelper" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/TaggedCache.h.ai.md b/include/xrpl/basics/TaggedCache.h.ai.md new file mode 100644 index 0000000000..b76ad00172 --- /dev/null +++ b/include/xrpl/basics/TaggedCache.h.ai.md @@ -0,0 +1,69 @@ +# `include/xrpl/basics/TaggedCache.h` + +## Role and Purpose + +`TaggedCache` is the central in-memory caching primitive for the XRPL node. It appears in every subsystem that needs to keep parsed or computed data close to the CPU: ledger entries (`CachedSLEs`), SHAMap tree nodes (`TreeNodeCache`), accepted ledger objects, transaction history, and more. The design solves a problem that a plain LRU cache cannot: in a highly concurrent system where multiple threads may independently load the same keyed object from storage, you want all of them to converge on *one* canonical in-memory copy. An ordinary cache gives you a place to look things up; `TaggedCache` additionally enforces object identity. + +The file defines only the class template declaration and its nested types. All method bodies live in the companion `TaggedCache.ipp`, which is included by consumers that need the full implementation. + +## The Dual-Region Model + +Every entry lives in one of two logical regions within the same `m_cache` hash map: + +- **Strong region** ("cached"): The `ValueEntry` holds the `SharedWeakUnionPointerType` as a strong reference. This is what `m_cache_count` tracks. So long as an entry is here, the object stays alive regardless of whether any external code holds a reference to it. +- **Weak region** ("tracked"): After the entry is swept or explicitly demoted via `del()`, the pointer is converted to a weak reference. The entry remains in `m_cache` and allows later callers who fetch the key to re-promote it back to the strong region if the object is still alive — i.e., if external `shared_ptr` holders still exist. + +This two-region model means the total map size (`getTrackSize()`) is always ≥ the cache count (`getCacheSize()`). The gap is objects that are evicted from the hot cache but still alive elsewhere in the system. + +## Template Parameters and Pointer Abstraction + +The class is parameterized over two distinct pointer type arguments: + +- `SharedWeakUnionPointerType` (defaults to `SharedWeakCachePointer`): stored inside each `ValueEntry`. Must support `isStrong()`, `isWeak()`, `isExpired()`, `lock()`, `getStrong()`, `convertToStrong()`, and `convertToWeak()`. +- `SharedPointerType` (defaults to `std::shared_ptr`): returned to callers. + +This abstraction allows two distinct implementations to plug in: + +1. **`SharedWeakCachePointer`** (the default): a `std::variant, std::weak_ptr>`, saving the cost of storing both at once. Used with ordinary heap objects. +2. **`SharedWeakUnion`** (from `IntrusivePointer.h`): a single tagged raw pointer whose low bit encodes whether it is a strong or weak intrusive reference. Used by `TreeNodeCache` for `SHAMapTreeNode`, where the intrusive reference counting avoids the separate control-block allocation of `std::make_shared`. + +The `IsKeyCache` boolean parameter enables a third mode: a pure key-existence cache that stores no value at all — only a `KeyOnlyEntry` carrying a `last_access` timestamp. `KeyCache` (`TaggedCache`) uses this to track, for example, which full-below ranges the SHAMap has validated, without storing any associated data. + +## Canonicalization + +The `canonicalize(key, data, replaceCallback)` method is the most architecturally important operation. It is called when a piece of code has already loaded or constructed an object and wants to register it with the cache — but in a world where another thread may have beaten it there. + +The logic: + +1. If the key is absent, insert the new object as a strong entry and return `false` (the caller was first). +2. If the key is present and cached (strong), invoke `replaceCallback` to decide which copy wins. If the callback returns `true`, the cache entry is replaced with the caller's data; otherwise the caller's `data` parameter is updated to point at the existing canonical copy. +3. If the key is present but only weakly tracked, attempt to promote. If the object is still alive, again apply `replaceCallback`; if dead, adopt the caller's data. + +The two convenience wrappers bake in the replacement policy: `canonicalize_replace_cache` always prefers the caller's new data (useful when the cache may hold a stale version), while `canonicalize_replace_client` always prefers the existing cached copy (the usual object-identity guarantee). The callback receives `entry.ptr.getStrong()` only when `R` is not a no-argument callable, avoiding the cost of materializing a strong pointer for intrusive types when it isn't needed. + +## Sweep and Eviction + +`sweep()` is called periodically (typically from a timer thread). It computes a `when_expire` cutoff based on the configured `m_target_age` and the current cache pressure relative to `m_target_size`. When the cache is over capacity, the effective age window shrinks proportionally, clamped to a minimum of one second, so that a rapidly growing cache doesn't evict everything instantly. + +The `m_cache` is a `hardened_partitioned_hash_map`, which shards the data across multiple independent `std::unordered_map` partitions. `sweep()` spawns one worker thread per partition (`sweepHelper`) so that the per-partition linear scan proceeds in parallel. All threads are joined before the main lock is released. Swept entries whose strong pointers are about to be released are moved into a `SweptPointersVector` per partition; these vectors outlive the lock scope and are destroyed after the lock is dropped, so potentially expensive object destructors don't run under the lock. + +The sweep has two outcomes for a strong entry whose age exceeds `when_expire`: + +- If `use_count() == 1` (cache is the sole owner): move the strong pointer out to be destroyed and erase the map entry entirely. +- If `use_count() > 1` (someone else holds a reference): demote to weak. The entry survives in the map as a tracker. + +Expired weak entries (where the external owner has also released) are erased unconditionally. + +## Concurrency and the Recursive Mutex + +All public operations acquire `m_mutex`, which is a `std::recursive_mutex` by default. The recursive mutex is necessary because `del()` and `canonicalize()` may both be called from code paths that are already holding the lock via `peekMutex()`. The `peekMutex()` accessor is a deliberate escape hatch: callers like `ConsensusTransSetSF` need to hold the cache lock while issuing a batch of lookups to make the multi-step operation atomic. The class comment warns that callers must not modify cached objects unless they hold a lock over all cache operations, enforcing an implicit immutability contract on stored values. + +`sweep()` passes a `std::lock_guard const&` token to each `sweepHelper` overload as a proof-of-lock parameter — not to capture it, but to statically enforce at the call site that the lock is held when per-partition threads are spawned. Because `m_mutex` is recursive, the sweeper threads themselves don't attempt to re-acquire it; they work only on the partition they are handed. + +## Metric Integration + +The inner `Stats` struct integrates with the `beast::insight` metrics framework. Construction registers a hook callback (`collect_metrics`) that fires when the collector polls for data. The hook publishes two gauges: the current cache size and the hit-rate percentage. Hits and misses are accumulated as `uint64_t` counters (`m_hits`, `m_misses`) under the cache lock and converted to a rate on demand, so there is no atomic contention on the hot path. + +## Relationship to Consumers + +`CachedSLEs` is the simplest instantiation — `TaggedCache` with all defaults — used by `CachedView` to memoize ledger state entries looked up during transaction processing. `TreeNodeCache` substitutes the intrusive pointer pair for both the union pointer and the shared pointer type, avoiding control-block allocations for the high-frequency SHAMap node working set. `KeyCache` flips `IsKeyCache=true` to track membership of `uint256` keys with no associated value, used by `FullBelowCache` to short-circuit redundant SHAMap full-below validation. \ No newline at end of file diff --git a/include/xrpl/basics/TaggedCache.ipp.ai.json b/include/xrpl/basics/TaggedCache.ipp.ai.json new file mode 100644 index 0000000000..49c498ffc1 --- /dev/null +++ b/include/xrpl/basics/TaggedCache.ipp.ai.json @@ -0,0 +1,415 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TaggedCache::TaggedCache" + ], + "entry_point": "TaggedCache::TaggedCache", + "purpose": "Constructs a TaggedCache instance, initializing internal fields and metrics.", + "validation_points": [ + "No explicit validation in constructor; assumes parameters are valid." + ] + }, + { + "call_chain": [ + "TaggedCache::size" + ], + "entry_point": "TaggedCache::size", + "purpose": "Returns the number of elements in the cache.", + "validation_points": [ + "Locks mutex to ensure thread safety, but no data validation." + ] + }, + { + "call_chain": [ + "TaggedCache::getCacheSize" + ], + "entry_point": "TaggedCache::getCacheSize", + "purpose": "Returns the cache count (number of cached items).", + "validation_points": [ + "Locks mutex for thread safety, no data validation." + ] + }, + { + "call_chain": [ + "TaggedCache::getTrackSize" + ], + "entry_point": "TaggedCache::getTrackSize", + "purpose": "Returns the size of the cache container.", + "validation_points": [ + "Locks mutex for thread safety, no data validation." + ] + }, + { + "call_chain": [ + "TaggedCache::getHitRate" + ], + "entry_point": "TaggedCache::getHitRate", + "purpose": "Calculates and returns the cache hit rate as a percentage.", + "validation_points": [ + "Locks mutex for thread safety; uses std::max to avoid division by zero." + ] + }, + { + "call_chain": [ + "TaggedCache::clear" + ], + "entry_point": "TaggedCache::clear", + "purpose": "Clears the cache and resets the cache count.", + "validation_points": [ + "Locks mutex for thread safety, no data validation." + ] + }, + { + "call_chain": [ + "TaggedCache::reset" + ], + "entry_point": "TaggedCache::reset", + "purpose": "Clears the cache and resets cache count, hits, and misses.", + "validation_points": [ + "Locks mutex for thread safety, no data validation." + ] + }, + { + "call_chain": [ + "TaggedCache::touch_if_exists" + ], + "entry_point": "TaggedCache::touch_if_exists", + "purpose": "Checks if a key exists in the cache and updates its usage if so.", + "validation_points": [ + "Locks mutex for thread safety; key is checked for existence but not validated for format or value." + ] + } + ], + "data_flows": [ + { + "field": "m_cache", + "flow": [ + "constructor", + "used in size(), getTrackSize(), clear(), reset(), touch_if_exists()" + ], + "origin": "Initialized in TaggedCache constructor.", + "transformations": [ + "Cleared in clear() and reset(), queried in size() and getTrackSize(), checked for key existence in touch_if_exists()" + ], + "validated_at": "No explicit validation; only thread safety via mutex." + }, + { + "field": "m_cache_count", + "flow": [ + "constructor", + "used in getCacheSize(), clear(), reset()" + ], + "origin": "Initialized in constructor, updated in clear(), reset(), possibly elsewhere in full implementation.", + "transformations": [ + "Reset to 0 in clear() and reset(), returned in getCacheSize()" + ], + "validated_at": "No explicit validation." + }, + { + "field": "m_hits, m_misses", + "flow": [ + "constructor", + "used in getHitRate(), reset()" + ], + "origin": "Initialized in constructor, updated in cache accessors (not shown in this snippet).", + "transformations": [ + "Reset to 0 in reset(), used in hit rate calculation in getHitRate()" + ], + "validated_at": "Division by zero avoided in getHitRate() via std::max." + }, + { + "field": "key (template parameter)", + "flow": [ + "input to touch_if_exists()", + "checked for existence in m_cache" + ], + "origin": "Passed as argument to touch_if_exists() and likely other cache accessors (not shown).", + "transformations": [ + "Used as lookup key; no transformation." + ], + "validated_at": "No explicit validation of key format or value; only existence in cache is checked." + } + ], + "description": "This file implements the methods for the xrpl::TaggedCache template class, which provides a thread-safe, partitioned cache with strong/weak reference semantics, expiration, and metrics collection. It supports both key-value and key-only caching, with customizable pointer types and concurrency primitives.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/TaggedCache.ipp", + "functions": [ + { + "args": [ + "name", + "size", + "expiration", + "clock", + "journal", + "collector" + ], + "lineno": 15, + "name": "TaggedCache::TaggedCache" + }, + { + "args": [], + "lineno": 38, + "name": "TaggedCache::clock" + }, + { + "args": [], + "lineno": 51, + "name": "TaggedCache::size" + }, + { + "args": [], + "lineno": 64, + "name": "TaggedCache::getCacheSize" + }, + { + "args": [], + "lineno": 77, + "name": "TaggedCache::getTrackSize" + }, + { + "args": [], + "lineno": 90, + "name": "TaggedCache::getHitRate" + }, + { + "args": [], + "lineno": 104, + "name": "TaggedCache::clear" + }, + { + "args": [], + "lineno": 115, + "name": "TaggedCache::reset" + }, + { + "args": [ + "key" + ], + "lineno": 127, + "name": "TaggedCache::touch_if_exists" + }, + { + "args": [], + "lineno": 146, + "name": "TaggedCache::sweep" + }, + { + "args": [ + "key", + "valid" + ], + "lineno": 210, + "name": "TaggedCache::del" + }, + { + "args": [ + "key", + "data", + "replaceCallback" + ], + "lineno": 241, + "name": "TaggedCache::canonicalize" + }, + { + "args": [ + "key", + "data" + ], + "lineno": 292, + "name": "TaggedCache::canonicalize_replace_cache" + }, + { + "args": [ + "key", + "data" + ], + "lineno": 299, + "name": "TaggedCache::canonicalize_replace_client" + }, + { + "args": [ + "key" + ], + "lineno": 306, + "name": "TaggedCache::fetch" + }, + { + "args": [ + "key", + "value" + ], + "lineno": 319, + "name": "TaggedCache::insert" + }, + { + "args": [ + "key" + ], + "lineno": 338, + "name": "TaggedCache::insert" + }, + { + "args": [ + "key", + "data" + ], + "lineno": 353, + "name": "TaggedCache::retrieve" + }, + { + "args": [], + "lineno": 366, + "name": "TaggedCache::peekMutex" + }, + { + "args": [], + "lineno": 374, + "name": "TaggedCache::getKeys" + }, + { + "args": [], + "lineno": 389, + "name": "TaggedCache::rate" + }, + { + "args": [ + "digest", + "h" + ], + "lineno": 402, + "name": "TaggedCache::fetch" + }, + { + "args": [ + "key", + "l" + ], + "lineno": 423, + "name": "TaggedCache::initialFetch" + }, + { + "args": [], + "lineno": 445, + "name": "TaggedCache::collect_metrics" + }, + { + "args": [ + "when_expire", + "now", + "partition", + "stuffToSweep", + "allRemovals", + "lock" + ], + "lineno": 463, + "name": "TaggedCache::sweepHelper" + }, + { + "args": [ + "when_expire", + "now", + "partition", + "stuffToSweep", + "allRemovals", + "lock" + ], + "lineno": 507, + "name": "TaggedCache::sweepHelper" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file is a template implementation and does not contain direct validation logic or input sanitization; it relies on thread safety via mutexes and basic checks (e.g., avoiding division by zero in getHitRate). Validation of input parameters (such as cache size, expiration, or key validity) is not present in this code. Test coverage would likely be found in unit tests for TaggedCache in the rippled codebase, possibly in files like 'TaggedCache_test.cpp' or similar. However, based on this snippet, there is no evidence of explicit validation being tested, and edge cases such as invalid constructor parameters or malformed keys are not handled or tested here.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/include/xrpl/basics/TaggedCache.ipp.ai.md b/include/xrpl/basics/TaggedCache.ipp.ai.md new file mode 100644 index 0000000000..8e849c5496 --- /dev/null +++ b/include/xrpl/basics/TaggedCache.ipp.ai.md @@ -0,0 +1,60 @@ +# `TaggedCache.ipp` — Template Implementation of the XRPL Cache/Tracker + +## Role in the System + +`TaggedCache.ipp` contains the out-of-line template method bodies for `xrpl::TaggedCache`, a general-purpose concurrent cache that underpins most of rippled's in-memory object reuse: ledger state entries (`SLE`s), transactions, account roots, trust lines, and other ledger objects that are expensive to decode or fetch from disk. The `.ipp` pattern separates implementation from declaration while keeping both in header-includable form — the `.h` defines the class interface and the `.ipp` is included at the bottom (or by consuming translation units) to instantiate methods. + +The class solves a problem specific to ledger node processing: many concurrent paths may independently fetch or create an object identified by the same hash key. Rather than permitting duplicates, the cache enforces a single *canonical* instance per key, replacing or redirecting callers as needed. It also acts as a weak-reference *tracker* — once an object is evicted from the active cache, the map entry survives as a weak pointer, so any code that retained a reference before eviction can still benefit from deduplication. + +## Template Parameters and the Dual Mode + +`TaggedCache` is parameterized over eight template arguments. The most architecturally significant is `bool IsKeyCache`. When `false` (the default), each entry stores a `ValueEntry` — a timestamp plus a `SharedWeakUnionPointerType` that can hold either a strong or a weak reference to `T`. When `true`, each entry stores a `KeyOnlyEntry` — just a timestamp, with no associated value. Key-only mode is used for negative-existence caches (e.g., "have we seen this transaction hash before?") where the value is irrelevant and memory footprint matters. + +This duality is resolved at compile time via `std::conditional::type Entry`, allowing a single class template to cover both use cases without virtual dispatch or runtime branching in fast paths. + +## The Strong/Weak Pointer Abstraction + +`ValueEntry` wraps a `SharedWeakUnionPointerType`, which defaults to `SharedWeakCachePointer`. This type holds either a `std::shared_ptr` (strong reference) or a `std::weak_ptr` inside a `std::variant`, providing `isStrong()`, `isWeak()`, `convertToWeak()`, `convertToStrong()`, `getStrong()`, and `lock()`. An alternative intrusive implementation (`SharedWeakUnion`) stores the strong/weak tag in the low bit of the pointer itself, avoiding the variant overhead when objects participate in intrusive reference counting. + +The key insight is that `m_cache_count` tracks only strong-reference entries. The total `m_cache.size()` (returned by `getTrackSize()`) counts both strong and weak entries. This is why the class distinguishes `getCacheSize()` from `size()` / `getTrackSize()` — the former answers "how many objects is the cache keeping alive?" while the latter answers "how many objects is the cache aware of?" + +## `sweep()` — Adaptive Expiry and Parallel Eviction + +`sweep()` is the periodic eviction function. Its first decision is the expiry cutoff. If the cache is at or below `m_target_size`, entries older than `m_target_age` are expired. If the cache is oversize, the cutoff age is *proportionally shortened*: `target_age * target_size / current_size`, clamped to a minimum of one second. This creates a feedback loop: the more overloaded the cache is, the more aggressively it evicts. + +`sweep()` then spawns one `std::thread` per partition of the underlying `hardened_partitioned_hash_map`, calling `sweepHelper`. Each thread iterates its partition independently. Because each partition is a distinct data structure (not a subset of a shared one), there is no intra-sweep contention. All threads are joined before the outer `std::lock_guard` exits — the sweep is fully synchronous from the caller's perspective, but parallelised internally. + +The two `sweepHelper` overloads handle the value and key-only cases: + +- **Value cache sweep**: Three cases per entry — (1) weak and expired: move pointer into `stuffToSweep` and erase the map entry; (2) strong and expired but with external holders (`use_count() > 1`): demote to weak, leave in map; (3) strong and expired with no external holders: move into `stuffToSweep` and erase. +- **Key-only cache sweep**: Simpler — entries past the cutoff are erased. An extra guard clamps `last_access` to `now` if it is somehow in the future, preventing entries from appearing permanently recent. + +The `stuffToSweep` vector pattern is deliberate: moved-out smart pointers are destroyed *after* the mutex is released, so potentially expensive object destructors never run under the lock. The vector is sized per-partition to avoid reallocations. + +## `canonicalize()` — Deduplication Under Concurrency + +`canonicalize()` is the cache's most subtle operation. Its contract: given a key and a caller's shared pointer, if the cache already has a live entry for that key, one of them must win and the other must be redirected to point at the canonical instance. The `replaceCallback` parameter decides who wins. + +The callback has two supported signatures via `if constexpr`: +- `bool()` — a zero-argument predicate. The common variants `canonicalize_replace_cache` (always returns `true`, cache wins) and `canonicalize_replace_client` (always returns `false`, client wins) use this form. +- `bool(SharedPointerType)` — a unary predicate receiving the existing strong pointer, for policies that inspect content. + +The zero-argument form exists as a performance optimisation: obtaining a strong pointer from a `SharedWeakUnion` requires an atomic operation (`checkoutStrongRefFromWeak`), which is unnecessary for the simple "always replace" and "never replace" cases. + +The entry may be in one of three states: not present, present with strong reference, or present with weak reference. In the weak case, `canonicalize()` attempts to lock the weak pointer. If it succeeds, the object is still alive in memory (held by some other caller), and it is promoted back to a strong reference in the cache. If it fails (the object was destroyed), the new data is inserted as a fresh strong entry. + +## `fetch()` and `initialFetch()` + +`initialFetch()` is the shared internal lookup path: it handles the three states of a value entry (absent, strong, weak) and updates `m_cache_count` on weak→strong promotion. It does *not* increment `m_misses` — that is left to the callers so they can control miss accounting differently. + +The two-argument `fetch(key, handler)` overload implements a double-checked locking pattern. It checks the cache under lock, releases the lock, calls the handler to load from an external source (e.g., the database), then re-acquires the lock to insert the result. This avoids holding the cache mutex during I/O while still protecting the map from concurrent modifications. A second check on re-entry (via `emplace`) ensures that if another thread beat this one to insert while the lock was dropped, both insertions are handled gracefully. + +## `del()` — Conditional Erasure + +`del(key, valid)` has a nuanced `valid` flag. When `valid=true`, the strong reference is released (decrementing `m_cache_count`) and the entry is converted to a weak pointer, but the key stays in the map so that any existing external holders still benefit from tracking. When `valid=false`, or when the existing entry has already expired, the key is erased entirely. This models the difference between "this object is no longer cached" and "this key is invalid and must not be returned." + +## Metrics and Observability + +`m_stats` wires the cache into the `beast::insight` telemetry system via a hook that calls `collect_metrics()` periodically. This exports `size` (strong-reference count) and `hit_rate` as gauges. There is a subtle inconsistency: `touch_if_exists()` increments `m_stats.hits` / `m_stats.misses` (the collector-facing counters), while `fetch()` increments `m_hits` / `m_misses` (the raw counters returned by `getHitRate()` and `rate()`). The two accounting streams serve different audiences — the collector feeds external monitoring, while `getHitRate()` / `rate()` are queried programmatically within the process. + +`peekMutex()` exposes the internal `recursive_mutex` directly. Its use is intentional: callers that need to perform a composite operation atomically (e.g., fetch followed by conditional re-insert) must hold the same lock the cache uses. The `recursive_mutex` permits this without deadlocking when the same thread re-enters via a cache method while already holding the lock. \ No newline at end of file diff --git a/include/xrpl/basics/ToString.h.ai.json b/include/xrpl/basics/ToString.h.ai.json new file mode 100644 index 0000000000..d0cea167d9 --- /dev/null +++ b/include/xrpl/basics/ToString.h.ai.json @@ -0,0 +1,71 @@ +{ + "args": [ + { + "lineno": 15, + "name": "t" + }, + { + "lineno": 21, + "name": "b" + }, + { + "lineno": 26, + "name": "c" + }, + { + "lineno": 31, + "name": "s" + }, + { + "lineno": 36, + "name": "s" + } + ], + "classes": [], + "description": "Provides generalized to_string functions in the xrpl namespace to handle various types (arithmetic, bool, char, std::string, const char*), extending std::to_string.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/ToString.h", + "functions": [ + { + "args": [ + "t" + ], + "lineno": 15, + "name": "to_string" + }, + { + "args": [ + "b" + ], + "lineno": 21, + "name": "to_string" + }, + { + "args": [ + "c" + ], + "lineno": 26, + "name": "to_string" + }, + { + "args": [ + "s" + ], + "lineno": 31, + "name": "to_string" + }, + { + "args": [ + "s" + ], + "lineno": 36, + "name": "to_string" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/ToString.h.ai.md b/include/xrpl/basics/ToString.h.ai.md new file mode 100644 index 0000000000..a69ddb0e5c --- /dev/null +++ b/include/xrpl/basics/ToString.h.ai.md @@ -0,0 +1,38 @@ +# `include/xrpl/basics/ToString.h` + +## Purpose + +`ToString.h` provides a small family of `xrpl::to_string()` overloads that patch over two well-known deficiencies in the standard `std::to_string()` and make string conversion uniform across all primitive types in XRPL code. + +## Why This File Exists + +`std::to_string` has two inconvenient behaviors that appear repeatedly in ledger code: + +- `std::to_string(true)` returns `"1"`, not `"true"`. Log messages, JSON serialization, and error strings throughout the rippled codebase need human-readable booleans. +- `std::to_string('A')` returns `"65"` — the integer value of the character — because `char` is an arithmetic type and the standard function treats it numerically. + +Beyond these fixes, there is no single standard function that accepts `std::string` or `const char*` as identity cases. Generic template code that wants to uniformly convert any value to a string would have to branch on type. `xrpl::to_string` eliminates that branching. + +## Design + +The header defines five overloads under the `xrpl` namespace: + +**Arithmetic template** (line 14–19): A SFINAE-constrained function template gated on `std::is_arithmetic` that forwards to `std::to_string(t)`. This handles `int`, `long`, `float`, `double`, and the rest of the arithmetic family without any additional boilerplate. The `enable_if` guard prevents this template from competing with the non-template overloads below. + +**`bool` overload** (line 21–25): Returns `"true"` or `"false"`. Even though `bool` satisfies `std::is_arithmetic`, C++ overload resolution prefers a non-template exact-match over a template instantiation. This overload therefore silently wins whenever a `bool` is passed, suppressing the `"0"`/`"1"` behavior. + +**`char` overload** (line 27–31): Returns a one-character `std::string`. The same overload-resolution rule applies: `char` is arithmetic, but the non-template overload is preferred, preventing the integer-value conversion. + +**`std::string` overload** (line 33–37): Identity conversion — returns the string by value. This is what makes generic code like `template std::string format(T v) { return xrpl::to_string(v); }` work for string arguments without special-casing them. + +**`const char*` overload** (line 39–43): Constructs a `std::string` from a C-string literal, completing the string-identity family. + +## Usage in the Codebase + +The most visible consumer is `src/libxrpl/json/Writer.cpp`, which calls `xrpl::to_string(f)` when serializing `float` and `double` values to JSON output. The header is also `#include`d by `include/xrpl/json/Writer.h` and the XRPL transaction path debug-info header, and it appears indirectly wherever logging or JSON serialization needs to convert primitives to strings in a uniform way. + +The comment in the header (`"It's also possible to provide implementation of to_string for a class which needs a string implementation"`) signals the intended extension point: XRPL domain types such as `Number` define their own `xrpl::to_string` overload in their own translation unit, and ADL (argument-dependent lookup) finds it automatically. `Number.cpp` demonstrates this with a full `xrpl::to_string(Number const&)` implementation that handles XRP and IOU formatting. + +## Key Tradeoff + +The header deliberately keeps the arithmetic template *enabled* for `bool` and `char` types — it is the non-template overloads that win in practice, not any exclusion in the template constraint. An alternative design would exclude `bool` and `char` from the `enable_if` (`!std::is_same` etc.), which would make the intent more explicit at the cost of a more complex constraint expression. The current approach is shorter and relies on well-defined overload-resolution rules, which is idiomatic modern C++. \ No newline at end of file diff --git a/include/xrpl/basics/UnorderedContainers.h.ai.json b/include/xrpl/basics/UnorderedContainers.h.ai.json new file mode 100644 index 0000000000..3c422ea924 --- /dev/null +++ b/include/xrpl/basics/UnorderedContainers.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines type aliases for hash-based containers (maps and sets) with both standard and hardened (cryptographically secure) hash functions, within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/UnorderedContainers.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/UnorderedContainers.h.ai.md b/include/xrpl/basics/UnorderedContainers.h.ai.md new file mode 100644 index 0000000000..3849dcb239 --- /dev/null +++ b/include/xrpl/basics/UnorderedContainers.h.ai.md @@ -0,0 +1,33 @@ +# `include/xrpl/basics/UnorderedContainers.h` + +This header is the canonical entry point for hash-based container types throughout the XRPL codebase. It defines two distinct families of type aliases — `hash_*` and `hardened_hash_*` — that differ in a single, security-critical dimension: whether the underlying hash function is seeded at runtime to resist adversarial key collisions. + +## The Two-Family Design + +The file's own comment states the rule plainly: use `hash_*` containers for keys that do not need a cryptographically secure hashing algorithm; use `hardened_hash_*` when they do. This is not just a style preference — it is a security boundary. In a network node like rippled, many data structures are keyed by values that arrive from untrusted peers or from the open ledger, making them candidates for **HashDoS attacks** (where an adversary crafts many keys with identical hashes to degrade a hash map to O(n) lookup). The two families exist precisely to make that threat explicit at the type level. + +### `hash_*` containers + +`hash_map`, `hash_multimap`, `hash_set`, and `hash_multiset` all default to `beast::uhash<>` as their hash function. `beast::uhash` is a thin wrapper: it constructs a fresh `beast::xxhasher` (seeded-free, no runtime state) and calls `hash_append` on the key. `beast::xxhasher` itself wraps the XXH3 64-bit algorithm from the `xxhash` library — an extremely fast, non-cryptographic hash that works through a 64-byte internal buffer to avoid the streaming API overhead for small keys. There is no per-container randomization, so these containers are **fast but unsuitable for keys derived from external input**. + +### `hardened_hash_*` containers + +`hardened_hash_map`, `hardened_hash_multimap`, `hardened_hash_set`, `hardened_hash_multiset`, and `hardened_partitioned_hash_map` all default to `hardened_hash`, where `strong_hash` is an alias for `beast::xxhasher`. + +The protection comes from `hardened_hash`'s constructor. Each instance generates a random `seed_pair` (two independent `uint64_t` values) using a process-wide singleton: a `std::mt19937_64` generator seeded from `std::random_device`, protected by a `std::mutex`. This happens once per `hardened_hash` instance, not once per hash call. The seed is then passed into `beast::xxhasher` as its seed parameter on every invocation. Because each container instance has its own random seed that an attacker cannot know in advance, collisions crafted against one process are useless against another — and collisions crafted against one container cannot be reused against another container of the same type in the same process. + +The comment in `hardened_hash.h` explicitly warns against Murmur or CityHash as the `HashAlgorithm` template parameter, citing the SipHash vulnerability research (https://131002.net/siphash/#at). XXH3 with a secret seed is the chosen alternative — fast enough that it doesn't compromise performance for lookup-heavy paths, while offering the unpredictability the hardened family requires. + +## The Partitioned Variant + +`hardened_partitioned_hash_map` adds a third property beyond security: **sharded concurrency**. It wraps `partitioned_unordered_map, ...>`, which internally holds a `std::vector` of independent `std::unordered_map` sub-maps. On construction (defaulting to `std::thread::hardware_concurrency()` partitions), each key is routed to a specific sub-map by `extract(key) % partitions_`. The `extract` function is specialized for `std::string` (hashing via `beast::uhash`) and for integral keys (using them directly as shard indices). + +The sharding design assumes callers take per-partition locks externally — the class itself has no synchronization. The pattern is used where the key space is large and concurrent access from multiple threads is expected, allowing thread A to read partition 3 while thread B writes to partition 7 without contention. The default of hardware concurrency as partition count reflects a deliberate choice to match OS-level parallelism without over-provisioning. + +## Relationships + +- **`hardened_hash.h`** defines `hardened_hash` and the `make_seed_pair()` RNG machinery. The mutex-protected singleton ensures that two `hardened_hash` objects constructed concurrently from different threads each get independent seeds without races. +- **`partitioned_unordered_map.h`** defines the sharded map template. Its forward-iterator walks all partitions sequentially, so range-for over a `hardened_partitioned_hash_map` works but is not order-stable across calls. +- **`beast/hash/uhash.h`** and **`beast/hash/xxhasher.h`** provide the underlying hash plumbing. The `hash_append` protocol means any type that implements the `hash_append` free function in its own namespace is automatically supported by all containers in this file. + +In practice, `hash_map` and `hash_set` appear in performance-sensitive internal structures keyed by types the node controls (e.g., pathfinding caches, RPC result sets), while the `hardened_*` variants appear where ledger-derived or peer-supplied data forms the key. \ No newline at end of file diff --git a/include/xrpl/basics/UptimeClock.h.ai.json b/include/xrpl/basics/UptimeClock.h.ai.json new file mode 100644 index 0000000000..16d90ceeb7 --- /dev/null +++ b/include/xrpl/basics/UptimeClock.h.ai.json @@ -0,0 +1,36 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "UptimeClock" + }, + { + "args": [], + "lineno": 31, + "name": "update_thread" + } + ], + "description": "Defines the UptimeClock class, which tracks program uptime with seconds precision and provides a cached, performant way to query elapsed time since program start.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/UptimeClock.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "now" + }, + { + "args": [], + "lineno": 38, + "name": "start_clock" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/UptimeClock.h.ai.md b/include/xrpl/basics/UptimeClock.h.ai.md new file mode 100644 index 0000000000..69a0fd7892 --- /dev/null +++ b/include/xrpl/basics/UptimeClock.h.ai.md @@ -0,0 +1,40 @@ +# `UptimeClock` — Seconds-Precision Process Uptime Clock + +## Role and Motivation + +`UptimeClock` is a C++ named clock type (satisfying the `` `TrivialClock` concept) that tracks how many seconds have elapsed since the `xrpld` process first called `now()`. Its defining characteristic is that it never queries the OS for the current time on each call — instead, a single background thread wakes up once per second to increment an `std::atomic` counter, and every call to `now()` simply reads that cached integer. This makes it extremely cheap to call from hot paths anywhere in the server, at the cost of one-second granularity and a possible ~1 s error at the start of the process. + +The motivation is straightforward: many subsystems — load monitoring, overlay slot management, peer metrics, the uptime display in `get_counts` — need to compare elapsed time but do not need sub-second precision. Using `std::chrono::system_clock::now()` for each comparison would involve a syscall; using `UptimeClock::now()` costs a single atomic load. + +## Type Design + +`UptimeClock` is designed as a drop-in clock type for the `` framework: + +- `rep` is plain `int`, meaning the uptime counter fits in a 32-bit signed integer. This gives a practical maximum of ~68 years — more than sufficient for a network daemon. +- `period` is `std::ratio<1>`, meaning the tick unit is one second. +- `time_point` is `std::chrono::time_point`, carrying the seconds-since-start value. +- `is_steady` mirrors `std::chrono::system_clock::is_steady`. This is a minor implementation detail — the clock is not truly steady in the C++ sense (it is not guaranteed to never go backwards), but the value follows from its backing clock. + +Because `UptimeClock` satisfies the named clock requirements, it can be used as a template argument wherever the standard library or library code expects a clock, such as `reduce_relay::Slots` in `OverlayImpl.h` and `UptimeClock::time_point mLastUpdate` in `LoadMonitor`. + +## The Background Thread Mechanism + +The clock uses a lazy-initialization pattern: the first call to `now()` constructs a `static update_thread` via `start_clock()`. The function-local `static` ensures this happens exactly once, and C++11 guarantees that static-local initialization is thread-safe, so no explicit lock is needed. + +The `update_thread` inner class is a thin RAII wrapper around `std::thread`. It inherits from `std::thread` privately, exposes `std::thread::thread` constructors via a `using` declaration, and overrides the destructor to perform a clean shutdown: it sets the shared `stop_` atomic to `true` and calls `join()`, waiting up to 1 second for the thread to notice the flag and exit. The comment in the implementation is honest about this: the join may take up to 1 s but happens only once at shutdown, so the latency is acceptable. + +Inside the thread itself, the loop uses `std::this_thread::sleep_until` rather than `sleep_for`. This avoids drift: the next wake time is computed as `next += 1s` before sleeping, so accumulated scheduler jitter does not cause the counter to fall progressively behind wall time. + +Both `now_` and `stop_` are `std::atomic` — `now_` because it is read by multiple calling threads while being written by the update thread, and `stop_` because it must be visible across thread boundaries without a data race. + +## Epoch and Precision Caveats + +The `now()` function initializes the update thread on first call, not at process startup. The implementation comment acknowledges this: the epoch is strictly "time since first use" rather than "time since xrpld start". However, the first call to `now()` happens very early in initialization (e.g., when `LoadMonitor` is constructed), so the discrepancy is a small fraction of a second and does not matter for any current consumer. + +Separately, because the counter increments after each 1-second sleep, the value returned by `now()` starts at 0 and reaches 1 only after the first full second has elapsed. Consumers should treat the value as a lower bound on elapsed seconds, accurate to ±1 s. + +## Usage in the Codebase + +The primary consumers fall into two categories. First, display/reporting: `GetCounts.cpp` calls `UptimeClock::now()` to compute a human-readable uptime string ("3 days 4 hours 20 minutes"), decomposing the `time_point` via repeated `time_since_epoch() / unitVal` operations. Second, time-keyed data structures: `LoadMonitor` stores a `UptimeClock::time_point mLastUpdate` to rate-limit logging, and `OverlayImpl` instantiates `reduce_relay::Slots` to manage per-peer transmission windows that expire after a fixed number of seconds. + +In all these cases the one-second resolution is exactly what is needed, and the atomic-load cost of `now()` is negligible compared to the work being gated on it. \ No newline at end of file diff --git a/include/xrpl/basics/algorithm.h.ai.json b/include/xrpl/basics/algorithm.h.ai.json new file mode 100644 index 0000000000..276753fa50 --- /dev/null +++ b/include/xrpl/basics/algorithm.h.ai.json @@ -0,0 +1,39 @@ +{ + "args": [], + "classes": [], + "description": "This file provides generic algorithms for set intersection and conditional removal on ordered ranges, including a generalized set intersection function and a function to remove elements based on intersection or predicate matching.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/algorithm.h", + "functions": [ + { + "args": [ + "first1", + "last1", + "first2", + "last2", + "action", + "comp" + ], + "lineno": 13, + "name": "generalized_set_intersection" + }, + { + "args": [ + "first1", + "last1", + "first2", + "last2", + "pred", + "comp" + ], + "lineno": 49, + "name": "remove_if_intersect_or_match" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/algorithm.h.ai.md b/include/xrpl/basics/algorithm.h.ai.md new file mode 100644 index 0000000000..1117311b9b --- /dev/null +++ b/include/xrpl/basics/algorithm.h.ai.md @@ -0,0 +1,25 @@ +# `include/xrpl/basics/algorithm.h` + +This header provides two generic, header-only algorithms for working with sorted ranges. Both are evolutionary descendants of standard library algorithms — `std::set_intersection` and `std::remove_if` — extended to handle cases that the standard versions cannot express cleanly. The file has no dependencies beyond `` and serves as a low-level utility for the broader `xrpl::basics` module. + +## `generalized_set_intersection` + +The standard `std::set_intersection` copies matched elements into an output iterator. That is sufficient when you only need one element from each matched pair and want no side effects beyond writing to output. `generalized_set_intersection` replaces the output iterator with an `Action` functor that receives *both* matched elements — `action(*first1, *first2)` — giving the caller full read/write access to the matched pair. + +The traversal is the same two-pointer walk used by `std::set_intersection`: advance the iterator pointing to the smaller element, and when both are equal (equivalence under `comp`), fire the action and advance both. The implementation uses the strict-weak-ordering identity `a == b ⟺ !comp(a,b) && !comp(b,a)` to detect equality without requiring a separate equality operator. + +The sole user in the codebase is `RCLCensorshipDetector::propose()`, which calls this function to carry sequence numbers forward. The detector tracks how many consecutive consensus rounds each transaction has failed to be included, and the sequence counter must survive across rounds. When the node re-proposes a transaction it already tracked, `generalized_set_intersection` finds the match and copies the old `seq` field into the new `TxIDSeq` entry via the action lambda — `[](auto& x, auto const& y) { x.seq = y.seq; }`. A standard `set_intersection` can't do this because the output range would only hold one element type, not a reference to both. + +## `remove_if_intersect_or_match` + +This algorithm fuses `std::remove_if` with set intersection into a single O(n + m) pass. It removes elements from the first range `[first1, last1)` if either condition holds: the element appears in the sorted second range `[first2, last2)`, or a predicate `pred` returns true for it. The caller must subsequently call `erase` on the returned end iterator to actually shrink the container, following the standard erase-remove idiom. + +The internal state is maintained as three contiguous, non-overlapping regions in the original first range: `[original-first1, first1)` holds preserved elements compacted to the front; `[first1, i)` holds removed elements awaiting overwrite; `[i, last1)` holds untested elements. Each iteration either compacts `*i` into the preserved prefix (if it should be kept) or leaves a gap. Movement uses `std::move` rather than copy, which is important for types with expensive or move-only semantics. + +The fusion with intersection logic works because both ranges are sorted: when `*i >= *first2`, the only way `*i` could equal some element in the second range is if that element is at or near `first2`. The algorithm checks `!comp(*first2, *i)` to detect equality, then advances `i` (marking it removed) before stepping `first2`. Crucially, once `first2` has moved past `*i`, no earlier element in the second range can match future elements of the first range — a guarantee that depends entirely on sorted order. + +`RCLCensorshipDetector::check()` uses this to clean up its tracker in one pass after a consensus round. The second range is the set of accepted transaction IDs; the predicate fires `pred(x.txid, x.seq)` to let the caller detect potential censorship (e.g., if a transaction has been blocked for too many rounds). The comparator passed is `std::less` — C++14's heterogeneous comparator — which dispatches to whichever `operator<` overload best matches the operand types. Because `RCLCensorshipDetector` defines heterogeneous `operator<` between `TxIDSeq` and raw `TxID`, the intersection check compares across the two different element types of the two ranges without constructing temporary objects. + +## Design context + +Both algorithms enforce sorted-range preconditions but do not assert or check them. Violations silently produce incorrect results, consistent with the standard library's approach for range algorithms. The decision to keep these in a separate `algorithm.h` rather than inline inside `RCLCensorshipDetector` reflects the conventional separation of algorithmic mechanics from domain logic — the censorship detector is free to express its `propose` and `check` operations at the level of intent, delegating the traversal bookkeeping here. \ No newline at end of file diff --git a/include/xrpl/basics/base64.h.ai.json b/include/xrpl/basics/base64.h.ai.json new file mode 100644 index 0000000000..adad0bb8db --- /dev/null +++ b/include/xrpl/basics/base64.h.ai.json @@ -0,0 +1,54 @@ +{ + "args": [ + { + "lineno": 44, + "name": "data" + }, + { + "lineno": 44, + "name": "len" + }, + { + "lineno": 46, + "name": "s" + }, + { + "lineno": 52, + "name": "data" + } + ], + "classes": [], + "description": "Provides base64 encoding and decoding utilities for binary and string data, including functions to encode raw bytes or strings to base64 and decode base64 strings.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/base64.h", + "functions": [ + { + "args": [ + "std::uint8_t const* data", + "std::size_t len" + ], + "lineno": 44, + "name": "base64_encode" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 46, + "name": "base64_encode" + }, + { + "args": [ + "std::string_view data" + ], + "lineno": 52, + "name": "base64_decode" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 42, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/base64.h.ai.md b/include/xrpl/basics/base64.h.ai.md new file mode 100644 index 0000000000..87a432d75b --- /dev/null +++ b/include/xrpl/basics/base64.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/basics/base64.h` + +This header exposes the XRPL ledger's standard-library-free Base64 codec. It is part of the `xrpl/basics` utility layer — a collection of low-level primitives that the rest of the stack depends on but that carry no ledger-specific semantics themselves. + +## API Surface + +The header declares three free functions in the `xrpl` namespace: + +```cpp +std::string base64_encode(std::uint8_t const* data, std::size_t len); +std::string base64_encode(std::string const& s); // inline convenience overload +std::string base64_decode(std::string_view data); +``` + +The primary `base64_encode` overload takes a raw byte pointer and a length, reflecting the reality that the callers — cryptographic subsystems, peer handshaking, manifest deserialisation — deal in raw binary buffers rather than `std::string`. The second overload is a thin inline forwarder that `reinterpret_cast`s a `std::string`'s data pointer, colocating the type-pun at the definition site so it doesn't leak into every call site. + +`base64_decode` takes `std::string_view`, the right choice for a read-only text consumer: callers can pass a `std::string`, a string literal, or a substring view without forcing a copy. + +## Implementation Details (from `src/libxrpl/basics/base64.cpp`) + +The implementation is adapted from René Nyffenegger's public-domain codec (2004–2008) and lives inside an anonymous `base64` sub-namespace within `xrpl`, keeping its helper tables and low-level routines invisible to the public API. + +The encode path pre-allocates the output string to `encoded_size(n) = 4 * ((n + 2) / 3)` bytes, writes directly into the string buffer, then calls `resize` a second time with the actual byte count returned by the inner `encode()` function. This double-resize idiom avoids an extra heap allocation while ensuring the string's `size()` is accurate after the fact. + +The decode path does the same trick against `decoded_size(n) = ((n / 4) * 3) + 2`. The slight over-allocation (`+2`) is intentional: it accommodates the worst-case padding without needing a branch, and `decoded_size` is only used for reservation. The inner `decode()` function returns a `std::pair` — bytes written and input characters consumed — so the public `base64_decode` trims the string to the first element of the pair. + +The inverse-table approach in `decode()` is a classic O(1) lookup: a 256-element `signed char` table maps every possible byte value to its 6-bit value or `-1` for invalid characters. When a `-1` is encountered, decoding stops immediately and returns whatever has been written so far. The test suite intentionally exercises this: `base64_decode("not_base64!!")` and `base64_decode("not")` produce identical output because both stop at the first character (`n`) that maps to a valid value and then halt on subsequent invalid ones. + +This silent-truncation behavior is a deliberate pragmatic choice rather than an error-throwing design. The callers — manifest deserialisation, session-signature verification, validator token loading — handle malformed input at a higher level by checking the output length or passing the result through a cryptographic verifier that will reject garbage data. + +## Usage in the Codebase + +The two primary consumer categories are: + +**Peer handshaking** (`src/xrpld/overlay/detail/Handshake.cpp`): When two rippled nodes establish an encrypted overlay connection, the initiating side base64-encodes the raw ECDSA session signature bytes into the `Session-Signature` HTTP header, and the receiving side decodes it back to bytes before passing them to `verifyDigest`. Base64 here is transport hygiene — HTTP headers are text, signatures are binary. + +**Validator infrastructure** (`src/libxrpl/server/Manifest.cpp`, `ValidatorKeys.cpp`, `ValidatorList.cpp`): Validator manifests and revocations are serialised as binary blobs and then base64-encoded for embedding in configuration files and JSON RPC responses. The config parser strips whitespace from multi-line base64 blobs, concatenates them, and passes the result directly to `base64_decode`. The `RpcCall.cpp` and `ServerHandler.cpp` paths do the same for over-the-wire RPC payloads. + +The codec is intentionally RFC 4648 standard (alphabet `A–Z a–z 0–9 + /`, `=` padding) rather than the URL-safe variant, matching the expectations of existing tooling and the external validator configuration format. \ No newline at end of file diff --git a/include/xrpl/basics/base_uint.h.ai.json b/include/xrpl/basics/base_uint.h.ai.json new file mode 100644 index 0000000000..0650fe5818 --- /dev/null +++ b/include/xrpl/basics/base_uint.h.ai.json @@ -0,0 +1,152 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 15, + "name": "is_contiguous_container" + }, + { + "args": [], + "lineno": 21, + "name": "is_contiguous_container>" + }, + { + "args": [], + "lineno": 28, + "name": "is_contiguous_container" + }, + { + "args": [ + "void const* data, VoidHelper", + "std::uint64_t b", + "std::string_view sv", + "Container const& c" + ], + "lineno": 34, + "name": "base_uint" + }, + { + "args": [], + "lineno": 97, + "name": "base_uint::VoidHelper" + }, + { + "args": [], + "lineno": 464, + "name": "is_uniquely_represented>" + } + ], + "description": "Defines the xrpl::base_uint template class for fixed-width big-endian unsigned integers (128, 160, 192, 256 bits) used in XRP Ledger, along with related operators, utilities, and type aliases.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/base_uint.h", + "functions": [ + { + "args": [ + "Hasher& h", + "base_uint const& a" + ], + "lineno": 232, + "name": "hash_append" + }, + { + "args": [ + "base_uint const& lhs", + "base_uint const& rhs" + ], + "lineno": 370, + "name": "operator<=>" + }, + { + "args": [ + "base_uint const& lhs", + "base_uint const& rhs" + ], + "lineno": 384, + "name": "operator==" + }, + { + "args": [ + "base_uint const& a", + "std::uint64_t b" + ], + "lineno": 392, + "name": "operator==" + }, + { + "args": [ + "base_uint const& a", + "base_uint const& b" + ], + "lineno": 399, + "name": "operator^" + }, + { + "args": [ + "base_uint const& a", + "base_uint const& b" + ], + "lineno": 405, + "name": "operator&" + }, + { + "args": [ + "base_uint const& a", + "base_uint const& b" + ], + "lineno": 411, + "name": "operator|" + }, + { + "args": [ + "base_uint const& a", + "base_uint const& b" + ], + "lineno": 417, + "name": "operator+" + }, + { + "args": [ + "base_uint const& a" + ], + "lineno": 423, + "name": "to_string" + }, + { + "args": [ + "base_uint const& a" + ], + "lineno": 429, + "name": "to_short_string" + }, + { + "args": [ + "std::ostream& out", + "base_uint const& u" + ], + "lineno": 435, + "name": "operator<<" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 441, + "name": "extract" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + }, + { + "lineno": 14, + "name": "detail" + }, + { + "lineno": 461, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/base_uint.h.ai.md b/include/xrpl/basics/base_uint.h.ai.md new file mode 100644 index 0000000000..717090ca51 --- /dev/null +++ b/include/xrpl/basics/base_uint.h.ai.md @@ -0,0 +1,41 @@ +# `base_uint.h` — Fixed-Width Big-Endian Integers for the XRP Ledger Protocol + +## Role in the System + +`base_uint` is the foundational integer type for every fixed-width hash or identifier used in the XRP Ledger. Transaction hashes, ledger hashes, account IDs, currency codes, directory indices, node IDs, and MPT issuance IDs all live as instantiations of this template. The four concrete aliases `uint128`, `uint160`, `uint192`, and `uint256` are defined here; protocol-layer aliases like `AccountID`, `Currency`, `NodeID`, and `Directory` are defined in `UintTypes.h` and `AccountID.h` using the tagged form. + +The file originated in the Bitcoin codebase (copyright notice from 2009–2011), was adapted for Ripple/XRP, and has accumulated XRPL-specific concerns such as hardened hashing, `Slice` interoperability, and tag-based type safety. + +## Big-Endian Storage Is a Protocol Invariant + +The most important design constraint is stated in the class comment: **the internal representation is big-endian and is part of the binary wire protocol**. This isn't just a performance choice; changing it would corrupt serialized ledger data. + +Internally, the value is stored as `std::array`, and the comment acknowledges that the array is "really big-endian in byte order" while using 32-bit words for speed. Every arithmetic operation that traverses the array must respect this: `operator++`, `operator--`, and `operator+=` all call `boost::endian::big_to_native` before doing native arithmetic and `boost::endian::native_to_big` when writing back. This per-operation conversion is the price paid for using `uint32_t` words rather than raw `uint8_t` bytes. + +## Tag Parameter for Protocol-Level Type Safety + +The second template parameter `Tag` exists solely to make `base_uint<160, AccountIDTag>` and `base_uint<160, CurrencyTag>` incompatible types even though they have identical representations. Without `Tag`, an `AccountID` and a `Currency` would be the same C++ type, and the compiler couldn't catch accidental mixing. Tags are empty structs with nothing in them — their sole purpose is to name otherwise-identical instantiations as distinct. This is the phantom-type pattern, and it's used extensively across the protocol layer. + +## Hex Parsing: A Baked-In Byteswap + +The private `parseFromStringView()` method contains a non-obvious bit-manipulation trick. For each 32-bit word it consumes eight hex characters and places them using the shift sequence `{4, 0, 12, 8, 20, 16, 28, 24}`. This interleaving directly constructs the `uint32_t` value so that when its bytes are read from memory, they appear in the same big-endian order as the hex string — without a separate `native_to_big` call at the end. The first hex character (most significant nibble of the big-endian representation) is placed at bits 4–7, which on a little-endian platform lands in the lowest-address byte. The effect is that the in-memory byte sequence equals the hex string's byte sequence, satisfying the big-endian invariant efficiently. + +The parser accepts the special input `"0"` as a shorthand for the all-zero value, regardless of the expected width. Any other string must be exactly `2 * bytes` characters long; otherwise `ParseResult::badLength` is returned. This two-outcome design (via `Expected`) feeds both the `[[nodiscard]] bool parseHex()` path (which returns false on error) and the throwing `explicit constexpr base_uint(std::string_view)` constructor. The comment notes this constructor is intended for compile-time use and suggests it become `consteval` in C++23. + +## Comparison: Why Byte-by-Byte Works + +The spaceship operator `<=>` compares the two values using `std::mismatch` across the raw byte iterators. A comment explicitly explains why this is correct: because the data is stored in big-endian byte order, a byte-by-byte lexicographic comparison of the raw bytes produces the same result as a numeric comparison of the integers. A FIXME note records that `std::lexicographical_compare_three_way` would be preferable but was unavailable on macOS at the time of writing. + +## Hashing: Seeded and Raw + +`base_uint` exposes two hashing interfaces. The `using hasher = hardened_hash<>` member makes the hash seeded per-container construction using a random 128-bit seed (via `hardened_hash.h`). This resists hash-flooding attacks on `unordered_map`s keyed by protocol values. The `hash_append` friend function feeds raw memory directly to the hash algorithm without any endian conversion, which is correct because the bytes are already in a canonical form. The `beast::is_uniquely_represented` specialization at the bottom of the file asserts that there are no padding bytes, permitting hash algorithms to hash the whole object as a contiguous byte sequence. + +The `extract()` specialization for `uint256` is wired into `partitioned_unordered_map`, which uses the extracted value to choose a shard. It reads the first `sizeof(std::size_t)` bytes via `memcpy` to avoid undefined behavior from potentially unaligned access, and the comment notes this will compile to an equivalent direct load on most platforms. + +## Container Interface and `fromVoid` + +`base_uint` exposes a byte-level STL container interface (`begin()`, `end()`, `data()`, `size()`) with `value_type = unsigned char`. This lets it be treated as a byte range by serialization code, stream operators, and `Slice`-based APIs. The templated constructor and assignment operator accept any `is_contiguous_container` (including `Slice`) and `memcpy` the bytes in, with an assertion that sizes match exactly. The `fromVoid(void const*)` factory and its checked variant `fromVoidChecked` provide a controlled path from raw pointers, with the internal `VoidHelper` tag struct ensuring the ambiguity-prone `base_uint(0)` call routes to the `uint64_t` constructor rather than the raw-pointer one. + +## Deprecated Convenience Members + +`isZero()`, `isNonZero()`, and `zero()` are marked deprecated; the preferred idiom is comparison against `beast::zero` and assignment from `beast::zero`. The zero-value constructor `base_uint(beast::Zero)` and the assignment `operator=(beast::Zero)` both zero-fill the internal array, integrating with the `beast::Zero` sentinel type used across the codebase. \ No newline at end of file diff --git a/include/xrpl/basics/chrono.h.ai.json b/include/xrpl/basics/chrono.h.ai.json new file mode 100644 index 0000000000..89a9b9036c --- /dev/null +++ b/include/xrpl/basics/chrono.h.ai.json @@ -0,0 +1,71 @@ +{ + "args": [ + { + "lineno": 38, + "name": "tp" + }, + { + "lineno": 44, + "name": "tp" + }, + { + "lineno": 54, + "name": "tp" + }, + { + "lineno": 60, + "name": "tp" + } + ], + "classes": [ + { + "args": [], + "lineno": 23, + "name": "NetClock" + } + ], + "description": "Defines network and stopwatch clocks for the XRPL project, including time formatting utilities and epoch offset calculations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/chrono.h", + "functions": [ + { + "args": [ + "tp" + ], + "lineno": 38, + "name": "to_string" + }, + { + "args": [ + "tp" + ], + "lineno": 44, + "name": "to_string" + }, + { + "args": [ + "tp" + ], + "lineno": 54, + "name": "to_string_iso" + }, + { + "args": [ + "tp" + ], + "lineno": 60, + "name": "to_string_iso" + }, + { + "args": [], + "lineno": 74, + "name": "stopwatch" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/chrono.h.ai.md b/include/xrpl/basics/chrono.h.ai.md new file mode 100644 index 0000000000..7d28786759 --- /dev/null +++ b/include/xrpl/basics/chrono.h.ai.md @@ -0,0 +1,37 @@ +# `include/xrpl/basics/chrono.h` + +This header is the central time-abstraction layer for the XRPL codebase. It defines three conceptually distinct things: the network's own clock type (`NetClock`) with its unusual epoch, a pair of stopwatch types for measuring elapsed time in production and test contexts, and formatting utilities that bridge between `NetClock` timestamps and human-readable strings. Nearly every subsystem that cares about ledger close times, validation timestamps, or consensus durations imports this file. + +## The `NetClock` and Its Epoch + +`NetClock` is a C++ *Clock* named type — a class with the required nested `rep`, `period`, `duration`, and `time_point` typedefs — that serves as the tag for all network-relative timestamps on the XRP Ledger. Its `rep` is `std::uint32_t` and its `period` is `std::ratio<1>`, meaning each tick is one second and values are unsigned 32-bit integers. This gives roughly 136 years of range from the epoch, expiring around the year 2136. + +The epoch itself is January 1, 2000 00:00:00 UTC, not the Unix epoch. The offset `epoch_offset` (946684800 seconds) is computed at compile time using the `date::sys_days` facility and verified with a `static_assert`. According to a comment in `TimeKeeper.h`, this epoch was chosen arbitrarily by Arthur Britto and David Schwartz during early development and "no rationale has been provided for this curious and annoying, but otherwise unimportant, choice." The compile-time assertion exists precisely because this magic constant appears throughout serialized protocol data and on-ledger structures — any accidental change would be a silent protocol-breaking bug. + +`is_steady = false` is the correct declaration because `NetClock` tracks wall time, which can be stepped or skewed by NTP. Downstream code cannot assume monotonicity, and this flag ensures standard library machinery treats it accordingly. + +## Epoch Conversion and String Formatting + +Because `NetClock::time_point` values are seconds since 2000, converting them to display strings requires shifting by `epoch_offset` back to the Unix epoch so that the `date` library can format them correctly. Both overloads of `to_string` and `to_string_iso` handle this conversion: + +```cpp +return to_string(system_clock::time_point{tp.time_since_epoch() + epoch_offset}); +``` + +The `to_string` variant produces a human-friendly `"YYYY-Mon-DD HH:MM:SS UTC"` format; `to_string_iso` produces ISO 8601 `"YYYY-MM-DDTHH:MM:SSZ"`. Template overloads accepting `date::sys_time` allow the same functions to be called with standard UTC time points, while the `NetClock::time_point` overloads handle the epoch shift before delegating. A `static_assert` in `to_string_iso` guards that `NetClock::duration::period` is still `std::ratio<1>`, preventing silent precision loss if the clock's resolution were ever changed. + +## Stopwatch Types for Elapsed-Time Measurement + +`Stopwatch` (`beast::abstract_clock`) and `TestStopwatch` (`beast::manual_clock`) form a dependency-injection pair. Production components accept a `Stopwatch&` reference; unit tests supply a `TestStopwatch`, which exposes `set()`, `advance()`, and `operator++()` to move time forward deterministically without sleeping. + +The design follows the pattern documented in `abstract_clock.h`: making `now()` a virtual instance method rather than a static function so the clock can be injected as a dependency. This is why production code never calls `std::chrono::steady_clock::now()` directly — it always goes through the abstraction, enabling full time-control in tests. + +The `stopwatch()` free function returns a global singleton backed by `beast::basic_seconds_clock`. That class uses a background thread to sample `std::chrono::steady_clock` at most once per second and caches the result. Callers that need the current time repeatedly in a tight loop therefore pay only a single atomic load rather than a syscall per iteration, at the cost of up to one second of staleness — acceptable for the consensus and network-overlay subsystems that are the primary consumers. + +## Convenience Duration Aliases + +`days` and `weeks` are `std::chrono::duration` specialisations that fill a gap in C++14/17's `` (these became standard in C++20 as `std::chrono::days` and `std::chrono::weeks`). They use `std::ratio_multiply` to derive their periods from `std::chrono::hours::period`, keeping them interoperable with the rest of `` arithmetic. They appear in ledger aging, amendment timeouts, and fee-escalation calculations across the codebase. + +## Relationship to `TimeKeeper` + +`TimeKeeper` in `src/xrpld/core/TimeKeeper.h` is the sole concrete implementation of `beast::abstract_clock`. It uses `epoch_offset` from this header to convert `std::chrono::system_clock::now()` into a `NetClock::time_point`, and maintains an atomic `closeOffset_` that nudges the reported close time toward the network-wide consensus view. The split between the clock *type* definition here and the running clock *implementation* in `TimeKeeper` is intentional: it lets protocol-level code reference `NetClock::time_point` without depending on the application-layer time-synchronization logic. \ No newline at end of file diff --git a/include/xrpl/basics/comparators.h.ai.json b/include/xrpl/basics/comparators.h.ai.json new file mode 100644 index 0000000000..6679049bfd --- /dev/null +++ b/include/xrpl/basics/comparators.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "less" + }, + { + "args": [], + "lineno": 30, + "name": "equal_to" + } + ], + "description": "Provides MSVC-specific wrappers for std::less and std::equal_to to strip [[nodiscard]] from operator() for compatibility with boost::bimap, and otherwise aliases them directly.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/comparators.h", + "functions": [ + { + "args": [ + "T const& left", + "T const& right" + ], + "lineno": 22, + "name": "xrpl::less::operator()" + }, + { + "args": [ + "T const& left", + "T const& right" + ], + "lineno": 34, + "name": "xrpl::equal_to::operator()" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/comparators.h.ai.md b/include/xrpl/basics/comparators.h.ai.md new file mode 100644 index 0000000000..6d07a3667d --- /dev/null +++ b/include/xrpl/basics/comparators.h.ai.md @@ -0,0 +1,21 @@ +# `comparators.h` — MSVC Compatibility Shims for `boost::bimap` + +This file exists to paper over a specific compiler/library incompatibility: MSVC 2019 (16.9.0+) added `[[nodiscard]]` to the `operator()` of `std::less` and `std::equal_to`, which collides with how `boost::bimap` validates comparators. + +### The Problem + +`boost::bimap` checks that a provided comparator satisfies the `BinaryFunction` concept by invoking `operator()` and discarding the return value. When MSVC decorated those operators with `[[nodiscard]]`, that validation step began emitting warnings-treated-as-errors (or outright compilation failures), since the entire point of the check is to call the function and throw the result away. The two behaviors are fundamentally at odds: Boost deliberately ignores the return value, and MSVC now insists you don't. + +### The Solution + +Under `_MSC_VER`, `xrpl::less` and `xrpl::equal_to` are thin wrapper structs whose `operator()` delegates to the standard counterparts but is itself *not* marked `[[nodiscard]]`. The `result_type = bool` member alias satisfies the older Boost `BinaryFunction` concept checks, which sometimes inspect this typedef directly. On any non-MSVC compiler, the types are simple `using` aliases to `std::less` and `std::equal_to`, meaning there is zero overhead or behavioral difference on GCC, Clang, or other targets. + +### Usage in the Codebase + +Both known consumers — `Bootcache.h` in the PeerFinder subsystem and `ledgers.h` in the consensus simulation framework — include this header specifically because they use `boost::bimap` with ordered collection types. By substituting `xrpl::less` where `std::less` would ordinarily appear, those files compile cleanly across MSVC and non-MSVC toolchains without any conditional compilation at the call site. The fix is fully transparent to the caller. + +### Design Notes + +The `#else` branch uses `using` aliases rather than repeating the struct definitions — the right call, since it keeps non-MSVC builds on the real standard types with all their optimizations and specializations (including the transparent `void` specializations). The MSVC wrapper structs handle the default `T = void` template argument but do not attempt to re-implement the transparent comparator `void` specialization; this is acceptable because `boost::bimap` collection types always require explicitly typed comparators anyway. + +This is a narrow, surgical fix. The file makes no attempt to be a general comparator utility — it solves exactly one problem (stripped `[[nodiscard]]`) in exactly the context where it appears (ordered `boost::bimap` collections), and nothing more. \ No newline at end of file diff --git a/include/xrpl/basics/contract.h.ai.json b/include/xrpl/basics/contract.h.ai.json new file mode 100644 index 0000000000..50c6eb81ce --- /dev/null +++ b/include/xrpl/basics/contract.h.ai.json @@ -0,0 +1,50 @@ +{ + "args": [ + { + "lineno": 17, + "name": "title" + }, + { + "lineno": 58, + "name": "how" + } + ], + "classes": [], + "description": "Provides programming by contract utilities for exception handling, logging, and invariant checking in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/contract.h", + "functions": [ + { + "args": [ + "title" + ], + "lineno": 17, + "name": "LogThrow" + }, + { + "args": [], + "lineno": 27, + "name": "Rethrow" + }, + { + "args": [ + "args" + ], + "lineno": 41, + "name": "Throw" + }, + { + "args": [ + "how" + ], + "lineno": 58, + "name": "LogicError" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/contract.h.ai.md b/include/xrpl/basics/contract.h.ai.md new file mode 100644 index 0000000000..9a88cbc600 --- /dev/null +++ b/include/xrpl/basics/contract.h.ai.md @@ -0,0 +1,56 @@ +# `include/xrpl/basics/contract.h` — Programming by Contract Utilities + +This header implements the XRPL ledger's Programming by Contract (DbC) discipline — a small but critical set of primitives for handling precondition failures, invariant violations, and structured exception throwing throughout the codebase. Rather than leaving each subsystem to throw exceptions ad-hoc, `contract.h` centralises the pattern so that every exception path produces a log entry, and every logic error terminates predictably. + +## The Two Failure Modes + +The file cleanly distinguishes two fundamentally different failure categories: + +**Recoverable runtime errors** are handled by `Throw` and `Rethrow`. These represent conditions the caller is expected to handle — malformed data, failed I/O, invalid configurations. The exception propagates up the stack normally, and callers can `catch` and recover. + +**Unrecoverable logic errors** are handled by `LogicError`. These represent violated invariants — bugs in the program itself, not unexpected input. `LogicError` is declared `noexcept` and ends with `std::abort()`, signalling that there is no safe recovery path. + +## `Throw` — Logged Exception Throwing + +```cpp +template +[[noreturn]] XRPL_NO_SANITIZE_ADDRESS inline void +Throw(Args&&... args) +``` + +`Throw` is the standard mechanism for raising exceptions across the XRPL codebase. It constructs the exception object, logs a warning that includes the exception's demangled type name (via `beast::type_name()`) and its `what()` message, and then throws by move. The `static_assert` enforces that `E` derives from `std::exception`, preventing careless use of non-standard exception types that would bypass catch-all handlers expecting `std::exception&`. + +The logging before the throw is the key design choice here. Because exceptions can be silently swallowed by broad `catch(...)` handlers or can propagate across subsystem boundaries, having an unconditional warning log at the throw site creates a reliable audit trail even if no handler ever logs the caught exception. In practice, callers across `nodestore`, `json`, and `net` subsystems use this pattern: + +```cpp +Throw("lz4_decompress: integer overflow (input)"); +xrpl::Throw(message); +``` + +## `Rethrow` — Logged Rethrow + +`Rethrow` wraps the bare `throw;` statement, prepending a warning log entry. The comment in the source is honest about why this exists: `throw;` inside a catch block re-raises the active exception, but this is transparent to logging infrastructure. `Rethrow` makes the re-throw visible in log output, which is valuable when tracking exception propagation chains in production. + +## `LogThrow` — The Logging Primitive + +`LogThrow(title)` is the shared sink called by both `Throw` and `Rethrow`. The implementation (in `contract.cpp`) routes to `JLOG(debugLog().warn())`, feeding into the structured ledger logging framework. It takes a human-readable title string — in `Throw`, this is assembled as `"Throwing exception of type : "`. + +## `LogicError` — Unrecoverable Invariant Violations + +```cpp +[[noreturn]] void LogicError(std::string const& how) noexcept; +``` + +`LogicError` is a terminate path. Its implementation logs at `fatal` severity, writes directly to `std::cerr` as a belt-and-suspenders safeguard, fires the `UNREACHABLE` instrumentation macro (which is `assert(false)` in debug builds and a no-op in release/fuzzing mode via the Antithesis SDK integration), and then calls `std::abort()`. The entire body is wrapped in `// LCOV_EXCL_START/STOP` because correctly-operating code should never reach it — coverage tools would flag it as untested dead code without the exclusion. + +The `noexcept` marker is meaningful: a function signalling a logic error must not throw, because it's called from sites that are already in an undefined or corrupted state. `noexcept` also helps the compiler understand this is a hard termination point. + +The comment in `contract.cpp` explains a deliberate naming convention: `UNREACHABLE("LogicError", {{"message", s}})` is the only callsite that passes a dynamic message parameter to the instrumentation macro, because `LogicError` is a convergence point for many different unrelated execution paths — unlike the named per-feature `XRPL_ASSERT` calls elsewhere in the codebase. + +## ASAN Suppression + +Both `Throw` and `Rethrow` are annotated with `XRPL_NO_SANITIZE_ADDRESS`, defined in `sanitizers.h` as `__attribute__((no_sanitize("address", "hwaddress")))` on GCC/Clang. Address Sanitizer tracks memory state through normal control flow but poorly handles the non-local jumps caused by C++ exceptions — the unwinding path can trigger false positive stack-use-after-scope or heap-use-after-free reports. The annotation suppresses instrumentation for these specific functions without disabling ASAN globally, keeping sanitizer coverage intact everywhere exceptions are not thrown. + +## Design Rationale + +The primitive set is deliberately minimal. There is no `Precondition()` or `Postcondition()` macro — callers simply call `Throw` at the point of detected violation, with a descriptive message. This keeps the abstraction thin while providing the essential guarantees: every exception is logged before it leaves its origin, type safety is statically enforced, and logic errors terminate loudly rather than silently corrupting state. \ No newline at end of file diff --git a/include/xrpl/basics/hardened_hash.h.ai.json b/include/xrpl/basics/hardened_hash.h.ai.json new file mode 100644 index 0000000000..12ba0f9657 --- /dev/null +++ b/include/xrpl/basics/hardened_hash.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [ + { + "lineno": 56, + "name": "HashAlgorithm" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "state_t" + }, + { + "args": [ + "HashAlgorithm = beast::xxhasher" + ], + "lineno": 56, + "name": "hardened_hash" + } + ], + "description": "Provides a hardened hash functor for use in hash-based containers, using a seeded hash algorithm to resist adversarial inputs. Includes utilities for generating random seeds and a template class for hashing with customizable algorithms.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/hardened_hash.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "make_seed_pair" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/hardened_hash.h.ai.md b/include/xrpl/basics/hardened_hash.h.ai.md new file mode 100644 index 0000000000..fa3ff0a367 --- /dev/null +++ b/include/xrpl/basics/hardened_hash.h.ai.md @@ -0,0 +1,33 @@ +# `hardened_hash.h` — Per-Instance Seeded Hash Functor + +This header addresses a concrete security problem for any server that accepts external input and stores it in hash-based containers: **hash-flooding attacks**. An adversary who knows the hash algorithm can craft keys that all map to the same bucket, degrading `O(1)` container operations to `O(n)`. For XRPL — which receives transactions, peer messages, and ledger objects from untrusted sources — storing such data in standard `std::unordered_*` containers without mitigation would expose the node to denial-of-service. + +`hardened_hash` solves this by generating a fresh pair of 64-bit random seeds at construction time and injecting them into the hash algorithm. An attacker who cannot predict the seeds cannot craft collisions. + +## Seed Generation: `make_seed_pair()` + +The private `detail::make_seed_pair()` function holds a function-local static `state_t` that owns a `std::random_device` (OS entropy), a `std::mt19937_64` PRNG seeded from it at startup, and a uniform distribution over `uint64_t`. A `std::mutex` guards the shared mutable state so that concurrent construction of `hardened_hash` instances from multiple threads is safe. The C++11 guarantee of thread-safe static initialization handles the one-time construction of `state_t` itself; the mutex handles every subsequent call to draw two random seeds. + +The `bool = true` non-type template parameter on `make_seed_pair` is a deliberate extensibility hook — it allows test code to explicitly specialize the function template and inject deterministic seeds, without changing the default behavior in production. + +## `hardened_hash` Class + +The class stores a single `detail::seed_pair` (two `uint64_t` values) generated at construction. Its `operator()` builds a fresh `HashAlgorithm` seeded with those values, then dispatches through `beast::hash_append` to feed the object being hashed into it. The result is extracted via the hasher's `explicit operator result_type()` conversion. + +Because seeds are stored per-instance rather than globally, each container gets its own independent randomization. A hash-set and a hash-map holding the same key type will produce different bucket distributions for the same input — this reduces the blast radius if any single seed is ever somehow leaked or guessed. + +## The `hash_append` Protocol + +Types must be made hashable by providing a free function `hash_append(Hasher&, T const&)` discoverable via ADL. The function is expected to forward-append each constituent field of `T` into the hasher, recursively. This is the composable, algorithm-agnostic hashing design from the N3333/P0029 proposal family. It cleanly decouples the hash algorithm from the type being hashed: the same `T` can be hashed with `xxhasher`, a BLAKE variant, or any future algorithm without modifying `T`. + +## Default Algorithm: `beast::xxhasher` + +The default `HashAlgorithm` is `beast::xxhasher`, a wrapper around XXH3-64 with seed support. The implementation maintains a 64-byte internal buffer to avoid the streaming API for small inputs, only spilling to an `XXH3_state_t` when data exceeds that buffer. The two-seed constructor `xxhasher(Seed seed, Seed)` accepts both values but only uses the first as the XXH3 seed — the second parameter is effectively ignored, leaving room for a future extension without breaking the two-seed interface that `hardened_hash` provides. + +The header comment explicitly prohibits using Murmur or CityHash as the `HashAlgorithm`, citing the SipHash paper (https://131002.net/siphash/#at). Both of those algorithms are known to be trivially attackable via differential cryptanalysis once an attacker can observe output. XXH3 with a secret seed provides practical resistance to this class of attack for non-cryptographic use cases. + +## Integration via `UnorderedContainers.h` + +`UnorderedContainers.h` consumes `hardened_hash` to define the `hardened_hash_map`, `hardened_hash_set`, `hardened_hash_multimap`, `hardened_hash_multiset`, and `hardened_partitioned_hash_map` type aliases — all defaulting to `hardened_hash` (`strong_hash`). That file explicitly documents the split: use plain `hash_*` aliases (backed by `beast::uhash`) for internal data unreachable by adversaries; use `hardened_hash_*` aliases anywhere external data lands. This makes the security intent visible at the call site rather than buried in template arguments. + +Callers like `AccountID.cpp`, `HashRouter`, `CachedView`, and `AssetCache` all use the hardened variants for data structures keyed on ledger objects or peer-supplied identifiers — the exact scenarios where hash-flooding is a realistic threat. \ No newline at end of file diff --git a/include/xrpl/basics/join.h.ai.json b/include/xrpl/basics/join.h.ai.json new file mode 100644 index 0000000000..aa7e4de8e6 --- /dev/null +++ b/include/xrpl/basics/join.h.ai.json @@ -0,0 +1,91 @@ +{ + "args": [ + { + "lineno": 7, + "name": "s" + }, + { + "lineno": 7, + "name": "iter" + }, + { + "lineno": 7, + "name": "end" + }, + { + "lineno": 7, + "name": "delimiter" + }, + { + "lineno": 23, + "name": "c" + }, + { + "lineno": 23, + "name": "delim" + }, + { + "lineno": 43, + "name": "c" + }, + { + "lineno": 43, + "name": "delim" + }, + { + "lineno": 62, + "name": "c" + }, + { + "lineno": 62, + "name": "delim" + } + ], + "classes": [ + { + "args": [ + "c", + "delim" + ], + "lineno": 18, + "name": "CollectionAndDelimiter" + }, + { + "args": [ + "c", + "delim" + ], + "lineno": 38, + "name": "CollectionAndDelimiter" + }, + { + "args": [ + "c", + "delim" + ], + "lineno": 57, + "name": "CollectionAndDelimiter" + } + ], + "description": "Provides utilities for joining collections into a stream with a delimiter, including template classes for handling various collection types and specializations for arrays and C-strings.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/join.h", + "functions": [ + { + "args": [ + "s", + "iter", + "end", + "delimiter" + ], + "lineno": 7, + "name": "join" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/join.h.ai.md b/include/xrpl/basics/join.h.ai.md new file mode 100644 index 0000000000..b3a1a36fe4 --- /dev/null +++ b/include/xrpl/basics/join.h.ai.md @@ -0,0 +1,37 @@ +# `join.h` — Stream-Based Collection Joining with Delimiter + +This header provides a small but general-purpose utility for rendering the elements of any iterable collection into a stream with a separator between each element — the XRPL equivalent of Python's `str.join()` or Boost's `algorithm::join`, but targeted at streams rather than string construction. + +## Core Algorithm + +The free function `join(Stream& s, Iter iter, Iter end, std::string const& delimiter)` is the foundation. It handles the classic "join without trailing delimiter" pattern: the first element is written directly, and every subsequent element is prefixed by the delimiter. An empty range short-circuits immediately. Returning the stream reference allows the call to compose naturally within chained `<<` expressions. + +The design choice to accept iterators rather than a collection directly keeps this function maximally generic — it works with any input-iterator pair, whether from a standard container, a raw pointer range, or a custom sequence. + +## The `CollectionAndDelimiter` Wrapper + +The raw iterator-based `join()` is not convenient at a call site like a logging statement. The `CollectionAndDelimiter` class solves this by bundling a collection reference and a delimiter string together into a single value that can be inserted into any stream with `operator<<`. This is the primary API that callers actually use. + +The real-world use in `Pathfinder.cpp` illustrates the motivation perfectly: + +```cpp +JLOG(j_.debug()) << "addPathsForType " << CollectionAndDelimiter(pathType, ", "); +``` + +Without this wrapper, the developer would need to manually loop or build an intermediate string just to log a path type list. The wrapper defers the work until the stream actually needs the data, which fits naturally into XRPL's conditional-logging idiom — if the debug level is disabled, the stream never evaluates the insertion at all. + +The delimiter is stored by value inside the wrapper (`std::string const delimiter`), while the collection is stored by `const&`. This means the wrapper is a lightweight view: it borrows the collection but owns a copy of the delimiter string. Callers should ensure the collection outlives the wrapper, which is naturally satisfied when both are in the same expression or scope. + +## Specializations for Arrays and C-Strings + +Two partial specializations of `CollectionAndDelimiter` handle cases where template argument deduction would otherwise produce unworkable types. + +The `CollectionAndDelimiter` specialization handles C-style arrays of non-character types (e.g., `char letters[4]` or `std::string words[5]`). Since raw arrays decay to pointers in most template contexts, this specialization captures the element type and size as separate template parameters and constructs the iterator range as `collection` to `collection + N`. The collection is stored as a plain pointer rather than a reference-to-array to avoid array reference decay issues. + +The `CollectionAndDelimiter` specialization is the most defensive of the three. A `char` array might be a C-style string with a null terminator occupying the last position of the array — iterating through it character-by-character and including `'\0'` in the output would be wrong. The `operator<<` implementation therefore checks whether the last element of the array is the null terminator and, if so, backs the end iterator up by one before delegating to `join()`. This correctly handles the common case of a string literal like `"string"` (which has `N=7` but only 6 printable characters) as well as the degenerate case of `""` (which produces no output at all). + +Note that when a `std::string` is passed, it matches the primary template since `std::string` is a proper range with `begin()`/`end()` iterators — it iterates character by character, so `CollectionAndDelimiter(std::string{"hello"}, "-")` produces `"h-e-l-l-o"`. This is intentional and consistent behavior across all sequence types. + +## Relationship to the Broader Codebase + +Within the `xrpl/basics/` module, this file sits alongside other small utility headers (`strHex.h`, `toString.h`, etc.) that fill gaps in the standard library for common formatting tasks. The pattern of composing stream-insertable value objects is consistent with how other XRPL logging helpers are structured. The only current production caller is `Pathfinder::addPathsForType()`, but the utility is generic enough to serve any component that needs to log or serialize a collection in a readable form. \ No newline at end of file diff --git a/include/xrpl/basics/make_SSLContext.h.ai.json b/include/xrpl/basics/make_SSLContext.h.ai.json new file mode 100644 index 0000000000..9837a04eb9 --- /dev/null +++ b/include/xrpl/basics/make_SSLContext.h.ai.json @@ -0,0 +1,53 @@ +{ + "args": [ + { + "lineno": 10, + "name": "cipherList" + }, + { + "lineno": 15, + "name": "keyFile" + }, + { + "lineno": 16, + "name": "certFile" + }, + { + "lineno": 17, + "name": "chainFile" + }, + { + "lineno": 18, + "name": "cipherList" + } + ], + "classes": [], + "description": "This file declares functions for creating SSL contexts (both self-signed and authenticated) for use with Boost.Asio in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/make_SSLContext.h", + "functions": [ + { + "args": [ + "cipherList" + ], + "lineno": 10, + "name": "make_SSLContext" + }, + { + "args": [ + "keyFile", + "certFile", + "chainFile", + "cipherList" + ], + "lineno": 14, + "name": "make_SSLContextAuthed" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/make_SSLContext.h.ai.md b/include/xrpl/basics/make_SSLContext.h.ai.md new file mode 100644 index 0000000000..5fcf4cd85d --- /dev/null +++ b/include/xrpl/basics/make_SSLContext.h.ai.md @@ -0,0 +1,37 @@ +# `include/xrpl/basics/make_SSLContext.h` + +This header is the public interface for creating TLS/SSL contexts used across the XRPL node's two distinct network-facing subsystems: the peer-to-peer overlay network and the HTTP/WebSocket RPC server. It declares exactly two factory functions, keeping all OpenSSL implementation details confined to the corresponding `.cpp` translation unit. + +## The Two Security Modes + +The header reflects a deliberate architectural split in how XRPL secures its connections: + +`make_SSLContext(cipherList)` creates a context for **anonymous TLS**, used between validator/relay nodes in the overlay network. Peer identity in the overlay is not established by TLS certificates — it is established by XRPL's own cryptographic node identity scheme. TLS here serves purely as a transport encryption layer, so a self-signed ephemeral certificate is sufficient. The returned context is configured with `verify_none`, meaning neither side validates the other's certificate. + +`make_SSLContextAuthed(keyFile, certFile, chainFile, cipherList)` creates a context for **certificate-authenticated TLS**, used by the RPC/HTTP server when an operator supplies their own key and certificate files. This is appropriate for external-facing endpoints where clients (wallets, applications, monitoring tools) need server identity assurance. The `chainFile` parameter supports intermediate certificate chains, allowing operators to use CA-issued certificates without embedding the full chain in the certificate file itself. + +## Shared Security Baseline + +Both functions delegate to an internal `get_context()` helper that enforces a shared hardened baseline regardless of authentication mode: + +- SSLv2, SSLv3, TLSv1.0, and TLSv1.1 are all disabled — only TLS 1.2 and above are accepted. +- TLS compression is disabled (mitigates CRIME-class attacks). +- TLS renegotiation is disabled via `SSL_OP_NO_RENEGOTIATION`, which guards against CVE-2021-3499 on older OpenSSL versions. +- The default cipher list `"TLSv1.2:!CBC:!DSS:!PSK:!eNULL:!aNULL"` excludes block-cipher modes (CBC), DSS-based suites, pre-shared key suites, and any suites lacking encryption or authentication. +- Pre-generated 2048-bit Diffie-Hellman parameters are embedded directly in the binary (generated via `openssl dhparam 2048`). Hardcoding these avoids the startup latency of runtime generation and is safe because DH parameters are not secret — only the ephemeral DH keypairs need to remain secret, and those are handled by `single_dh_use`. + +## Self-Signed Certificate Design + +The anonymous context's certificate generation reveals several non-obvious defensive choices. The RSA key and X.509 certificate are created once as function-local statics, meaning they are shared across every call to `make_SSLContext` within a process lifetime. This is intentional: the certificate is ephemeral by design (it is regenerated on each server restart), so there is no value in creating multiple distinct certificates per connection. + +The certificate validity start time is backdated by 25 hours. This prevents a network observer from inferring the server's precise startup time from the certificate's `notBefore` field — a subtle privacy consideration that reduces side-channel information leakage. The certificate is set valid for two years from creation, and carries a 128-bit randomly generated serial number to avoid collisions in logs or caches. + +X.509v3 extensions are set to mark the certificate as a non-CA leaf (`CA:FALSE`), restrict key usage to `digitalSignature`, and allow the certificate for both `serverAuth` and `clientAuth` extended key usage — the latter because overlay connections are mutually encrypted (not strictly client/server asymmetric). + +## Error Handling Philosophy + +All failures in context construction call `LogicError()`, which terminates the process. This is appropriate because SSL context creation is a startup-time prerequisite: if the TLS layer cannot be initialized (e.g., invalid cipher list, missing key file, mismatched key and certificate), the node cannot operate safely and there is no meaningful recovery path. The authenticated path additionally calls `SSL_CTX_check_private_key` to verify that the loaded private key actually corresponds to the loaded certificate before returning, catching misconfigured deployments at startup rather than at connection time. + +## Callers + +In `OverlayImpl.cpp`, `make_SSLContext("")` (empty cipher list, falling back to the default) is called unconditionally during overlay setup — peer connections always use anonymous TLS. In `ServerHandler.cpp`, the choice between the two functions is made at port configuration time: if any of `ssl_key`, `ssl_cert`, or `ssl_chain` are populated, `make_SSLContextAuthed` is called; otherwise, `make_SSLContext` is used. Operators can override the cipher list on a per-port basis via the `ssl_ciphers` config directive, which is passed through as the `cipherList` argument. \ No newline at end of file diff --git a/include/xrpl/basics/mulDiv.h.ai.json b/include/xrpl/basics/mulDiv.h.ai.json new file mode 100644 index 0000000000..051301c2be --- /dev/null +++ b/include/xrpl/basics/mulDiv.h.ai.json @@ -0,0 +1,37 @@ +{ + "args": [ + { + "lineno": 17, + "name": "value" + }, + { + "lineno": 17, + "name": "mul" + }, + { + "lineno": 17, + "name": "div" + } + ], + "classes": [], + "description": "Provides a utility function to perform multiplication and division in a single step with overflow checking, returning an optional result.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/mulDiv.h", + "functions": [ + { + "args": [ + "value", + "mul", + "div" + ], + "lineno": 17, + "name": "mulDiv" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/mulDiv.h.ai.md b/include/xrpl/basics/mulDiv.h.ai.md new file mode 100644 index 0000000000..9835a09668 --- /dev/null +++ b/include/xrpl/basics/mulDiv.h.ai.md @@ -0,0 +1,26 @@ +# `include/xrpl/basics/mulDiv.h` + +This header declares a single arithmetic utility, `mulDiv()`, that computes `value * mul / div` on unsigned 64-bit integers without intermediate overflow and without sacrificing precision. It also exposes `muldiv_max`, a `constexpr` alias for `std::numeric_limits::max()`, which callers use as a natural overflow sentinel. + +## The problem it solves + +A direct evaluation of `value * mul / div` on `uint64_t` silently overflows whenever the product `value * mul` exceeds 2⁶⁴−1, which is common in ledger fee calculations where both operands can be near the full 64-bit range. Splitting the operation into two steps makes this worse, not better — intermediate truncation in integer division also discards precision. `mulDiv` fixes both issues at once. + +## Implementation strategy + +The implementation (`src/libxrpl/basics/mulDiv.cpp`) uses `boost::multiprecision::uint128_t` as the scratch type. The 64-bit inputs are multiplied into the 128-bit accumulator via Boost's `multiply()`, then divided in place. Only at the very end does the code check whether the 128-bit result fits back into a `uint64_t`; if it exceeds `muldiv_max` the function returns `std::nullopt`. This approach is exact — no floating-point rounding, no two-step integer approximation. + +## Return convention and caller responsibility + +The function is declared noexcept-by-convention (the header comment explicitly says "Throws: None") and returns `std::optional`. This shifts the overflow decision to the caller, which is important because different callers have different policies: + +- `LoadFeeTrack.cpp` treats overflow as a logic error and propagates an `std::overflow_error` via `Throw<>`. +- `TxQ.cpp` uses `value_or(xrpl::muldiv_max)` throughout — clamping to the maximum integer is acceptable for fee-level arithmetic where "astronomical fee" is a safe ceiling. + +The `muldiv_max` constant is exported from this header precisely to support the `value_or` pattern consistently across call sites. + +## Usage in practice + +Every call site in the ledger involves proportional fee scaling: converting raw fees to fee levels, applying percentage adjustments, escalating fees based on queue depth, or scaling a base fee by a load factor. In all cases the multiplication would overflow `uint64_t` for large inputs, but the mathematical result still fits once divided — exactly the scenario `mulDiv` is built for. + +The test suite (`src/tests/libxrpl/basics/mulDiv.cpp`) confirms correct results for values near `UINT64_MAX`, verifies commutativity of `value` and `mul`, checks zero-operand edge cases, and asserts that `std::nullopt` is returned when the division cannot bring the 128-bit product back under the 64-bit ceiling. \ No newline at end of file diff --git a/include/xrpl/basics/partitioned_unordered_map.h.ai.json b/include/xrpl/basics/partitioned_unordered_map.h.ai.json new file mode 100644 index 0000000000..b4b19604d9 --- /dev/null +++ b/include/xrpl/basics/partitioned_unordered_map.h.ai.json @@ -0,0 +1,60 @@ +{ + "args": [ + { + "lineno": 11, + "name": "key" + }, + { + "lineno": 16, + "name": "key" + }, + { + "lineno": 137, + "name": "partitions" + } + ], + "classes": [ + { + "args": [ + "std::optional partitions = std::nullopt" + ], + "lineno": 23, + "name": "partitioned_unordered_map" + }, + { + "args": [], + "lineno": 41, + "name": "iterator" + }, + { + "args": [], + "lineno": 87, + "name": "const_iterator" + } + ], + "description": "This file defines a thread-partitioned unordered map container template (partitioned_unordered_map) for concurrent or partitioned access, along with supporting iterator types and utility functions for key extraction and partitioning.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/partitioned_unordered_map.h", + "functions": [ + { + "args": [ + "key" + ], + "lineno": 11, + "name": "extract" + }, + { + "args": [ + "key" + ], + "lineno": 16, + "name": "extract" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/partitioned_unordered_map.h.ai.md b/include/xrpl/basics/partitioned_unordered_map.h.ai.md new file mode 100644 index 0000000000..4d6405759e --- /dev/null +++ b/include/xrpl/basics/partitioned_unordered_map.h.ai.md @@ -0,0 +1,56 @@ +# `partitioned_unordered_map.h` + +## Role and Purpose + +This header introduces `partitioned_unordered_map`, a sharded hash map whose primary purpose is to reduce lock contention in multi-threaded workloads. Rather than wrapping a single `std::unordered_map` with one coarse-grained lock, the container holds a `std::vector` of independent `std::unordered_map` instances ("partitions"). Each key deterministically belongs to exactly one partition, so callers who know the key can lock only that partition — leaving every other partition free for concurrent access. + +The container itself provides **no synchronization primitives**. That is an intentional design choice: the container exposes its raw `partition_map_type` (the vector of maps) through `map()`, and callers take responsibility for locking individual entries in that vector. The split between "partition selection" and "mutual exclusion" allows each consumer to use whatever locking mechanism suits it — `std::recursive_mutex`, `packed_spinlock`, or anything else. + +## Partition Selection and the `extract()` Hook + +Keys are mapped to partitions by `extract(key) % partitions_`. The free-function template `extract()` is a customization point separate from the map's `Hash` template parameter. The general template simply casts the key to `std::size_t`, suiting integer-like types. Two specializations in this header and two in sibling headers override it for richer types: + +- `std::string` — hashes via `beast::uhash<>{}`. +- `uint256` — reads the first `sizeof(std::size_t)` bytes of the 256-bit value via `memcpy` (avoiding UB from unaligned access). +- `SHAMapHash` — same byte-extraction trick applied to its inner `uint256`. + +Separating the partition key from the hash function matters because the hash function may be cryptographically randomized (as in `hardened_hash`) while the partition index must be **stable across calls** to ensure a key always lands in the same shard. The `extract()` convention achieves that stability without coupling partitioning logic to the `Hash` template. + +## Constructor and Partition Count + +```cpp +partitioned_unordered_map(std::optional partitions = std::nullopt) +``` + +When `partitions` is omitted or zero, the container defaults to `std::thread::hardware_concurrency()`, aligning the shard count with the number of logical CPU cores. This is a pragmatic default: one thread per core can own one shard, eliminating contention under ideally scheduled workloads. An `XRPL_ASSERT` guards against the edge case where `hardware_concurrency()` returns zero. + +## Iterator Design + +The nested `iterator` and `const_iterator` structs navigate a two-level structure using two sub-iterators: + +- `ait_` — a `partition_map_type::iterator` that points to the current inner `unordered_map`. +- `mit_` — a `map_type::iterator` that points to the current element within that inner map. + +The `inc()` helper advances `mit_`; when it exhausts a partition, it steps `ait_` forward until it finds a non-empty partition or reaches the end of the vector. `begin()` applies the same logic to skip leading empty partitions. `end()` points `ait_` past the vector's end and `mit_` to the last partition's `end()`. + +Two points are worth noting. First, equality comparison checks all three of `map_`, `ait_`, and `mit_`, so iterators from different `partitioned_unordered_map` instances correctly compare unequal. Second, `const_iterator` holds a non-const `map_type::iterator` internally (matching the non-const `partition_map_type*`); `const`-correctness at the element level is enforced by returning `const_reference` from `operator*()`. + +## Operations + +`find()` computes the partition index from the key, then delegates to the underlying `unordered_map::find()` within that shard. A miss returns `end()`. Both `emplace()` overloads — piecewise-construction and key-value forwarding — follow the same pattern: select the partition from the key, emplace into it, and wrap the returned iterator. + +`erase()` removes an element and then advances the iterator through any trailing empty partitions, so the returned iterator is valid for continued forward traversal. + +`size()` is O(N) — it accumulates counts across all partitions. This is a deliberate tradeoff; maintaining an atomic counter would add write contention on every insert and erase, undermining the sharding benefit. + +`operator[]` provides straightforward subscript access, routing through `map_[partitioner(key)]`. + +## Usage in `TaggedCache` + +The only consumer in the codebase is `TaggedCache`, which instantiates the container via the `hardened_partitioned_hash_map` alias defined in `UnorderedContainers.h`: + +```cpp +using hardened_partitioned_hash_map = partitioned_unordered_map, ...>; +``` + +`TaggedCache` exposes a `sweepHelper()` that receives an individual `partition_map_type` entry (one `unordered_map`) along with a held `std::lock_guard`, and processes that shard independently. The sweep can therefore spawn one thread per partition, working in parallel while each thread holds only its own per-partition lock — exactly the concurrency pattern the container's design enables. \ No newline at end of file diff --git a/include/xrpl/basics/random.h.ai.json b/include/xrpl/basics/random.h.ai.json new file mode 100644 index 0000000000..08343c91ab --- /dev/null +++ b/include/xrpl/basics/random.h.ai.json @@ -0,0 +1,92 @@ +{ + "args": [], + "classes": [], + "description": "Provides deterministic, thread-local, non-cryptographically secure pseudo-random number generation utilities, including functions for generating random integers, bytes, and booleans, using a default PRNG engine.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/random.h", + "functions": [ + { + "args": [], + "lineno": 32, + "name": "default_prng" + }, + { + "args": [ + "engine", + "min", + "max" + ], + "lineno": 65, + "name": "rand_int" + }, + { + "args": [ + "min", + "max" + ], + "lineno": 74, + "name": "rand_int" + }, + { + "args": [ + "engine", + "max" + ], + "lineno": 79, + "name": "rand_int" + }, + { + "args": [ + "max" + ], + "lineno": 84, + "name": "rand_int" + }, + { + "args": [ + "engine" + ], + "lineno": 89, + "name": "rand_int" + }, + { + "args": [], + "lineno": 94, + "name": "rand_int" + }, + { + "args": [ + "engine" + ], + "lineno": 104, + "name": "rand_byte" + }, + { + "args": [], + "lineno": 113, + "name": "rand_byte" + }, + { + "args": [ + "engine" + ], + "lineno": 121, + "name": "rand_bool" + }, + { + "args": [], + "lineno": 126, + "name": "rand_bool" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 19, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/random.h.ai.md b/include/xrpl/basics/random.h.ai.md new file mode 100644 index 0000000000..cf3151f718 --- /dev/null +++ b/include/xrpl/basics/random.h.ai.md @@ -0,0 +1,34 @@ +# `include/xrpl/basics/random.h` + +This header provides the XRPL ledger's general-purpose pseudo-random number generation layer. It deliberately occupies a narrow scope: fast, deterministic, thread-safe non-cryptographic randomness for simulation, jitter, test data, and protocol logic that does not require unpredictability guarantees. It is explicitly excluded from any use in key generation, IVs, or security-sensitive contexts. + +## The Engine: `beast::xor_shift_engine` + +The underlying generator is `beast::xor_shift_engine`, an xorshift128+ implementation with a `uint64_t` result type. The algorithm maintains two 64-bit state words, advances them with XOR-shift operations per the Vigna reference implementation, and mixes the seed through MurmurHash3 finalizer constants (`0xff51afd7ed558ccd` and `0xc4ceb9fe1a85ec53`) to ensure the state is well-distributed even from low-entropy seeds. The engine satisfies C++11's `UniformRandomBitGenerator` concept — it provides `min()`, `max()`, and `operator()` — making it compatible with `std::uniform_int_distribution` and all standard distribution types. + +Two `static_assert`s at namespace scope guard the engine contract: the result type must be unsigned integral, and its maximum must be at least as large as `uint64_t::max`. These are guarded with `#ifndef __INTELLISENSE__` to suppress false-positive diagnostic noise in IDEs, which sometimes fail to evaluate constant expressions across template instantiation boundaries. + +## `default_prng()`: A Two-Level Seeding Hierarchy + +The design challenge for thread-local PRNGs is giving each thread a distinct, high-quality seed without paying the cost of `std::random_device` on every thread startup (which can be slow or even blocking on some systems). `default_prng()` solves this with a two-level hierarchy: + +1. A single `static beast::xor_shift_engine seeder` is initialized once from `std::random_device`, which provides true entropy at program startup. A `static std::mutex` serializes all accesses to this seeder. +2. Each thread gets a `thread_local beast::xor_shift_engine engine` that is seeded lazily on first access by drawing one value from `seeder` under the mutex lock. After initialization, the thread-local engine runs entirely independently with zero contention. + +This approach ensures that threads never share RNG state, avoiding the need for per-call locking while still guaranteeing statistically independent sequences across threads. The `std::uniform_int_distribution` with lower bound `1` is used when seeding to respect the engine's constraint that seed zero is invalid (which would throw `std::domain_error` in `xor_shift_engine::seed()`). + +## `rand_int`: Overload Family + +The `rand_int` family provides six overloads covering every combination of: with/without explicit engine, with/without min, with/without max. All are constrained with `std::enable_if_t::value>` to prevent misuse with floating-point or enum types. The engine-taking variants additionally require `detail::is_engine::value`, which is a type alias for `std::is_invocable_r` — a minimal duck-typing check that the type can be called with no arguments and returns its `result_type`. All overloads delegate to `std::uniform_int_distribution`, and the comment in the implementation acknowledges that constructing the distribution object should be negligible cost, with a note to optimize if profiling reveals otherwise. + +The `XRPL_ASSERT` on the two-argument form checks `max > min` (strict), which is slightly tighter than `uniform_int_distribution`'s requirement of `min <= max`. Equal bounds are not supported through this API, which is a defensible choice since calling `rand_int(5, 5)` is almost certainly a bug. + +## `rand_byte` and `rand_bool` + +`rand_byte` is constrained to exactly `unsigned char` or `uint8_t` (not any integral type) via a conjunction in `std::enable_if_t`. The internal implementation routes through `rand_int` rather than `rand_int` — this sidesteps potential implementation-defined behavior in `std::uniform_int_distribution` on platforms where the standard library is not required to handle byte-wide integer distributions. + +`rand_bool` is the simplest primitive: `rand_int(engine, 1) == 1`, producing a fair coin flip. The symmetric phrasing (rather than `rand_int(engine, 1) != 0`) is intentional for clarity. + +## Usage Context + +The header is included across networking subsystems (`peerfinder`, `overlay`), consensus code, HTTP/WebSocket work queues, and extensively throughout the test suite. In production code the role is typically non-security jitter — for example, randomizing peer selection order or staggering reconnect timers — while tests use the engine-taking overloads with deterministic seeds to produce reproducible random data. The engine-taking overloads exist precisely to support this dual use: callers that need reproducibility pass their own seeded engine; callers that just want "some randomness" omit it and get the thread-local default. \ No newline at end of file diff --git a/include/xrpl/basics/rocksdb.h.ai.json b/include/xrpl/basics/rocksdb.h.ai.json new file mode 100644 index 0000000000..9163855e49 --- /dev/null +++ b/include/xrpl/basics/rocksdb.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "This header file conditionally includes various RocksDB headers if XRPL_ROCKSDB_AVAILABLE is defined, providing access to RocksDB database functionality.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/rocksdb.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/basics/rocksdb.h.ai.md b/include/xrpl/basics/rocksdb.h.ai.md new file mode 100644 index 0000000000..f4110506a0 --- /dev/null +++ b/include/xrpl/basics/rocksdb.h.ai.md @@ -0,0 +1,34 @@ +# `include/xrpl/basics/rocksdb.h` + +## Role and Purpose + +This header is a thin aggregation shim that centralizes all RocksDB public API includes behind a single compile-time feature flag. Rather than scattering `#if XRPL_ROCKSDB_AVAILABLE` guards and individual `` includes across every translation unit that touches the database layer, the codebase funnels all such inclusions through this one file. Any source file that needs RocksDB types simply includes `` and never needs to mention the feature flag itself. + +## The `XRPL_ROCKSDB_AVAILABLE` Guard + +The macro `XRPL_ROCKSDB_AVAILABLE` is set to `1` via a CMake target property when the RocksDB library is found during the build. The relevant line in `CMakeLists.txt`: + +``` +PROPERTIES INTERFACE_COMPILE_DEFINITIONS XRPL_ROCKSDB_AVAILABLE=1 +``` + +This means the macro is propagated as an interface definition on the CMake target, so any consumer that links against it automatically sees the flag without manual `-D` flags. When RocksDB is absent — for example, in minimal builds or unsupported platforms — the entire block compiles away to nothing, and the rest of the `#if XRPL_ROCKSDB_AVAILABLE` guards in implementation files like `RocksDBFactory.cpp` suppress the RocksDB-specific code paths as well. + +## Headers Aggregated + +When the flag is set, the file pulls in the full surface of RocksDB headers needed by the XRPL node-store backend: + +- Core database API (`db.h`, `options.h`, `status.h`, `slice.h`, `iterator.h`) +- Write pipeline (`write_batch.h`, `transaction_log.h`) +- Tuning and customization points (`cache.h`, `filter_policy.h`, `memtablerep.h`, `merge_operator.h`, `compaction_filter.h`, `slice_transform.h`, `comparator.h`) +- Diagnostics and introspection (`statistics.h`, `perf_context.h`, `table_properties.h`) +- Block and table format control (`table.h`, `flush_block_policy.h`, `universal_compaction.h`) +- Environment abstraction (`env.h`, `convenience.h`, `types.h`) + +The breadth of this list matches what `RocksDBFactory.cpp` actually uses: the factory constructs `rocksdb::Options`, employs the `rocksdb::EnvWrapper` subclass `RocksDBEnv` for custom thread naming, and uses `rocksdb::WriteBatch` for batched writes through the `BatchWriter` layer. + +## Design Rationale + +Keeping all RocksDB includes behind a single header prevents the `#if XRPL_ROCKSDB_AVAILABLE` boilerplate from leaking into every file that indirectly depends on RocksDB types. It also makes it straightforward to swap the exact set of required headers in one place should the RocksDB API evolve. The commented-out line `// #include ` is a remnant of an earlier investigation into an alternative RocksDB namespace or fork (`rocksdb2`) and signals that portability considerations were weighed at some point. + +This pattern is consistent with how XRPL handles other optional system dependencies: the feature availability check is resolved once at the CMake level and then expressed as a simple macro, keeping the C++ headers themselves free of build-system details. \ No newline at end of file diff --git a/include/xrpl/basics/safe_cast.h.ai.json b/include/xrpl/basics/safe_cast.h.ai.json new file mode 100644 index 0000000000..1aeb61e666 --- /dev/null +++ b/include/xrpl/basics/safe_cast.h.ai.json @@ -0,0 +1,84 @@ +{ + "args": [ + { + "lineno": 11, + "name": "Src" + }, + { + "lineno": 11, + "name": "Dest" + }, + { + "lineno": 17, + "name": "s" + } + ], + "classes": [], + "description": "Provides safe and unsafe casting utilities (safe_cast, unsafe_cast, safe_downcast) with compile-time checks for type safety, especially for integral and enum types, as well as pointer/reference downcasting with runtime assertions in debug mode.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/safe_cast.h", + "functions": [ + { + "args": [ + "Src s" + ], + "lineno": 17, + "name": "safe_cast" + }, + { + "args": [ + "Src s" + ], + "lineno": 32, + "name": "safe_cast" + }, + { + "args": [ + "Src s" + ], + "lineno": 38, + "name": "safe_cast" + }, + { + "args": [ + "Src s" + ], + "lineno": 46, + "name": "unsafe_cast" + }, + { + "args": [ + "Src s" + ], + "lineno": 56, + "name": "unsafe_cast" + }, + { + "args": [ + "Src s" + ], + "lineno": 62, + "name": "unsafe_cast" + }, + { + "args": [ + "Src* s" + ], + "lineno": 69, + "name": "safe_downcast" + }, + { + "args": [ + "Src& s" + ], + "lineno": 81, + "name": "safe_downcast" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/safe_cast.h.ai.md b/include/xrpl/basics/safe_cast.h.ai.md new file mode 100644 index 0000000000..dc23cb756c --- /dev/null +++ b/include/xrpl/basics/safe_cast.h.ai.md @@ -0,0 +1,44 @@ +# `include/xrpl/basics/safe_cast.h` + +This header provides a small family of casting utilities that replace raw `static_cast` at the call site with one that encodes intent and enforces correctness at compile time. Its motivating use case is enum-to-integer and integer-to-enum conversions, where `static_cast` is technically required but silently accepts lossy or sign-mismatching casts. + +## `SafeToCast` Concept + +The `SafeToCast` concept is the foundational correctness predicate. It evaluates to true only when both types are integral, neither cast direction loses sign range (a signed source cannot be cast to unsigned of equal size), and the destination is at least as wide as the source — with an extra byte of headroom required when signedness differs, since a signed destination must accommodate values that were representable in the unsigned source beyond the signed destination's positive range. This encoded arithmetic captures exactly what an implicit promotion would guarantee, made explicit and inspectable by the compiler. + +## `safe_cast`: Verified Lossless Conversion + +`safe_cast(src)` comes in three overloads covering integral→integral, integral→enum, and enum→integral conversions. + +The integral→integral overload enforces the `SafeToCast` rules via two `static_assert` statements and then delegates to `static_cast`. Because both assertions fire at compile time, zero-overhead is guaranteed — the optimizer sees a plain `static_cast` after template instantiation. The function is `constexpr noexcept`, so it composes freely in constant expressions and `noexcept` propagation chains. + +The enum overloads decompose the cast into a two-step path through the enum's underlying integer type: `enum→underlying_type→Dest` or `Src→underlying_type→enum`. This pattern forces the sign and width checks to operate on actual integer types rather than the nominal enum type, which has undefined underlying width without an explicit base, and keeps the enum-related casts from bypassing the size checks. + +## `unsafe_cast`: Explicit Acknowledgement of Lossy Casts + +`unsafe_cast(src)` inverts the `SafeToCast` predicate. Its `static_assert(!SafeToCast, ...)` will reject any call where the cast has actually become safe — meaning if underlying types are later changed to be compatible, the call site will break at compile time with a message suggesting promotion to `safe_cast`. This turns `unsafe_cast` into self-enforcing documentation: it records both the current necessity and the future obligation to revisit the decision. + +A real example from `STAmount.cpp` illustrates the intent: + +```cpp +mValue = unsafe_cast(-amount.drops()); +``` + +Here a signed drop count is negated and stored in an unsigned field; the caller knows the invariant holds, but the types make no such promise, so `unsafe_cast` marks the acknowledgement in code rather than a comment that can drift out of sync. + +## `safe_downcast`: Polymorphic Hierarchy Navigation + +`safe_downcast(src)` handles pointer and lvalue-reference downcasts within a polymorphic class hierarchy. Its two-mode design is characteristic of performance-sensitive C++: + +- **Release builds** (`NDEBUG` defined): compiles to a `static_cast` with a `// NOLINT` suppressing the Clang-Tidy warning about unchecked pointer downcasts. +- **Debug builds**: uses `dynamic_cast` for the pointer overload and checks the result against `nullptr` via `XRPL_ASSERT`; uses a `dynamic_cast` to a pointer in the reference overload just to validate the downcast, then falls through to `static_cast` for the actual conversion (since `dynamic_cast` to a reference throws on failure, and the assertion messaging is preferred here). + +This avoids the runtime overhead of `dynamic_cast` in production while catching incorrect casts during development and CI. The `XRPL_ASSERT` macro (aliased to `ALWAYS_OR_UNREACHABLE` from `instrumentation.h`) marks the check as an invariant that must hold, and during fuzzing the execution continues past a failure rather than aborting. + +## Integration with `Units.h` + +`Units.h` extends the same `safe_cast`/`unsafe_cast` names into the `xrpl` namespace for strong-typed unit wrappers (`XRPAmount`, `IOUAmount`, etc.). Those overloads accept `IntegralValue` wrapper types and unwrap them to their underlying integer, delegate to the integral overloads here, and rewrap the result. This pattern lets code cast between unit types using the same idiom as raw integer casts, maintaining compile-time correctness guarantees across the abstraction boundary. + +## Design Rationale + +Raw `static_cast` between integers and enums is legal C++ but invisible to reviewers — a narrowing cast looks identical to a widening one. By separating casts into `safe_cast` (always correct) and `unsafe_cast` (annotated exception), this header makes every numeric conversion a policy decision visible in the source. The compile-time inversion in `unsafe_cast` additionally prevents the codebase from accumulating dead safety exceptions as the code evolves. \ No newline at end of file diff --git a/include/xrpl/basics/sanitizers.h.ai.json b/include/xrpl/basics/sanitizers.h.ai.json new file mode 100644 index 0000000000..c03807c9db --- /dev/null +++ b/include/xrpl/basics/sanitizers.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "Defines a macro to disable AddressSanitizer (ASan) and Hardware AddressSanitizer (HwASan) instrumentation for specific functions, mainly to avoid false positives in certain control flow scenarios.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/sanitizers.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/basics/sanitizers.h.ai.md b/include/xrpl/basics/sanitizers.h.ai.md new file mode 100644 index 0000000000..48e61862d8 --- /dev/null +++ b/include/xrpl/basics/sanitizers.h.ai.md @@ -0,0 +1,15 @@ +# `include/xrpl/basics/sanitizers.h` + +## Purpose + +This header provides a single compiler-attribute macro, `XRPL_NO_SANITIZE_ADDRESS`, that suppresses AddressSanitizer (ASan) and Hardware AddressSanitizer (HwASan) instrumentation on individual functions. It exists because both sanitizers instrument every memory access via shadow memory, and certain legitimate C++ control-flow patterns — notably `throw`, `catch`, and coroutine stack switches — can cause ASan to report false positives. + +## The Macro + +On GCC and Clang, `XRPL_NO_SANITIZE_ADDRESS` expands to `__attribute__((no_sanitize("address", "hwaddress")))`, placed on a function declaration to tell the compiler to skip shadow-memory instrumentation for that function only. On MSVC and other non-GCC/Clang compilers the macro expands to nothing, so annotated code compiles cleanly everywhere without `#ifdef` noise at each call site. + +## Design Rationale + +The suppression is deliberately **surgical rather than global**. Running a sanitizer build and then disabling ASan process-wide would defeat the entire point of the instrumentation. By confining the annotation to the specific functions that trigger false positives, the rest of the codebase remains fully checked. + +The primary consumer is `contract.h`, which applies the macro to `Throw()` and `Rethrow()` — the two functions that perform the actual `throw` statement in XRPL's Programming-by-Contract layer. Both functions are `[[noreturn]]` and transfer control non-linearly, which is exactly the pattern ASan struggles with. Keeping the macro in its own header rather than inside `contract.h` preserves the option to annotate other throw-sites or coroutine switch-points across the codebase without creating a circular dependency on the broader contract machinery. \ No newline at end of file diff --git a/include/xrpl/basics/scope.h.ai.json b/include/xrpl/basics/scope.h.ai.json new file mode 100644 index 0000000000..51aeace3a3 --- /dev/null +++ b/include/xrpl/basics/scope.h.ai.json @@ -0,0 +1,46 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "EFP&& f", + "std::enable_if_t, scope_exit> && std::is_constructible_v>* = 0" + ], + "lineno": 22, + "name": "scope_exit" + }, + { + "args": [ + "EFP&& f", + "std::enable_if_t, scope_fail> && std::is_constructible_v>* = 0" + ], + "lineno": 61, + "name": "scope_fail" + }, + { + "args": [ + "EFP&& f", + "std::enable_if_t, scope_success> && std::is_constructible_v>* = 0" + ], + "lineno": 100, + "name": "scope_success" + }, + { + "args": [ + "std::unique_lock& lock" + ], + "lineno": 139, + "name": "scope_unlock" + } + ], + "description": "This file provides RAII (Resource Acquisition Is Initialization) scope guard helpers for C++ such as scope_exit, scope_fail, scope_success, and scope_unlock, which execute user-provided functions on scope exit, failure, or success, and manage mutex unlocking/locking automatically.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/scope.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/scope.h.ai.md b/include/xrpl/basics/scope.h.ai.md new file mode 100644 index 0000000000..3a2e5861d0 --- /dev/null +++ b/include/xrpl/basics/scope.h.ai.md @@ -0,0 +1,33 @@ +# `include/xrpl/basics/scope.h` + +This header provides four RAII scope-guard utilities for the `xrpl` namespace: `scope_exit`, `scope_fail`, `scope_success`, and `scope_unlock`. The first three follow the design specified in the C++ Library Fundamentals TS v3 (N4873, §[scopeguard]), giving XRPL the same ergonomics that the standard committee intended for a future `` header but without waiting for compiler support. `scope_unlock` is an independent addition that solves a recurring pattern in the ledger's concurrency model. + +## The Three Scope Guard Templates + +All three share a common structure: they wrap a callable `EF`, an `execute_on_destruction_` flag, and (for `scope_fail` and `scope_success`) a snapshot of `std::uncaught_exceptions()` taken at construction time. The difference lies entirely in the condition evaluated in each destructor. + +**`scope_exit`** is the unconditional guard. Its destructor calls the stored functor whenever `execute_on_destruction_` is `true`, regardless of whether the scope is leaving normally or due to an exception. This is the go-to tool for "always clean up this resource" situations — a direct replacement for wrapping teardown code in a dedicated RAII type. The `CheckCash` transactor uses it precisely this way: after temporarily tweaking a trust-line limit during a payment, a `scope_exit` guarantees the original value is restored before the function returns, even if an error path is taken mid-function. + +**`scope_fail`** fires only when the scope is unwinding due to an exception. It compares `std::uncaught_exceptions()` at destruction against the value snapshotted at construction; if the count has grown, an exception is in flight. This is the "rollback on failure" idiom: register compensating actions at the top of an operation, and they will run automatically if anything throws before completion. + +**`scope_success`** is the mirror of `scope_fail`. It fires only when `std::uncaught_exceptions()` has *not* grown since construction — the scope exited cleanly. Because its exit function only runs on the happy path, the destructor is conditionally `noexcept(noexcept(exit_function_()))`, propagating the callable's exception specification correctly. Unlike `scope_exit` and `scope_fail`, the constructor of `scope_success` does not force `noexcept` construction through `static_assert`, since the implementation already handles non-noexcept construction via its `noexcept(...)` specifier on the constructor itself. + +### The Deviation from the TS Specification + +The spec's constructors for `scope_exit` and `scope_fail` contain a `try/catch` block to handle the case where the functor's construction (not invocation) throws. In practice, almost all callers pass a lambda literal, making those constructors trivially noexcept. Several compilers flagged the try/catch as superfluous in these cases. The implementation resolves this by marking the constructors `noexcept` unconditionally and substituting a `static_assert` on `std::is_nothrow_constructible_v`. The effect is identical — throwing constructors are rejected — but via a compile-time diagnostic rather than a runtime branch. + +### Move Semantics and `release()` + +All three scope guards are move-constructible but not move-assignable. The move constructor forwards the functor and copies the `execute_on_destruction_` flag, then calls `rhs.release()` to disarm the source. This makes ownership transfer unambiguous: exactly one live instance holds responsibility for running the functor. Calling `release()` directly sets `execute_on_destruction_ = false`, cancelling the deferred action — useful when a resource has been successfully handed off and cleanup is no longer needed. + +CTAD guides (`scope_exit(EF) -> scope_exit`, etc.) allow clean brace-initialization without spelling out the template parameter. + +## `scope_unlock` + +This template solves a specific problem in XRPL's mutex-heavy ledger management code: a function holds a `std::unique_lock` for most of its work but must temporarily release it for a blocking call or an outward-facing callback, then re-acquire it before continuing. Without a RAII wrapper, every early-return path must remember to re-lock before leaving. + +`scope_unlock` inverts the conventional lock-guard contract. Construction immediately calls `plock->unlock()` (after asserting via `XRPL_ASSERT` that the lock is owned), and the destructor calls `plock->lock()`. The mutex is held for exactly the surrounding scope minus the inner block where `scope_unlock` lives. `LedgerMaster` uses this repeatedly — temporarily releasing its recursive mutex while publishing ledgers or fetching history — and `InboundLedgers` uses it to release a lock before calling `acquire()`. + +Unlike the three scope guards above, `scope_unlock` is intentionally immovable: both the copy constructor and copy-assignment operator are deleted, and there is no move constructor. The semantics of transferring "who will re-lock this mutex" are too error-prone to support safely, so the type is tied to the scope in which it is created. + +The `XRPL_ASSERT` at construction time (`plock->owns_lock()`) acts as a defensive invariant check: calling `scope_unlock` on an already-unlocked `unique_lock` would double-unlock the mutex, a precondition violation that the assertion catches in debug builds. \ No newline at end of file diff --git a/include/xrpl/basics/spinlock.h.ai.json b/include/xrpl/basics/spinlock.h.ai.json new file mode 100644 index 0000000000..7c20bac50a --- /dev/null +++ b/include/xrpl/basics/spinlock.h.ai.json @@ -0,0 +1,48 @@ +{ + "args": [ + { + "lineno": 67, + "name": "lock" + }, + { + "lineno": 67, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::atomic& lock, int index" + ], + "lineno": 49, + "name": "packed_spinlock" + }, + { + "args": [ + "std::atomic& lock" + ], + "lineno": 108, + "name": "spinlock" + } + ], + "description": "This file implements space-efficient spinlock and packed spinlock classes using atomic integers for use in the XRPL codebase, providing low-level locking primitives with architecture-specific optimizations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/spinlock.h", + "functions": [ + { + "args": [], + "lineno": 20, + "name": "spin_pause" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 14, + "name": "xrpl" + }, + { + "lineno": 16, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/spinlock.h.ai.md b/include/xrpl/basics/spinlock.h.ai.md new file mode 100644 index 0000000000..484de882e8 --- /dev/null +++ b/include/xrpl/basics/spinlock.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/basics/spinlock.h` + +## Purpose + +This header provides two template spinlock classes — `packed_spinlock` and `spinlock` — along with an architecture-specific CPU hint helper. Both classes meet the C++ `Lockable` named requirement, making them directly usable with `std::lock_guard`. They are deliberately low-level, narrowly scoped primitives intended for specific high-performance use cases rather than general synchronization; the file's own documentation warns that `std::mutex` frequently outperforms spinlocks on modern platforms and should be preferred unless profiling data justifies otherwise. + +## `detail::spin_pause()` + +The private helper `spin_pause()` abstracts away the CPU instruction used to reduce pipeline pressure inside spin loops. On x86/x86-64 it calls `_mm_pause()` (the `PAUSE` instruction), and on AArch64 it emits `yield` via inline assembly. Without this hint, a tight compare-loop saturates the processor's out-of-order execution pipeline with speculative loads, causing an expensive pipeline flush the moment the lock is finally released. Including the hint allows the processor to throttle speculative work and reduces the misprediction penalty on acquisition. + +## `packed_spinlock` + +This class packs multiple independent spinlocks into a single `std::atomic`, where each lock occupies one bit of the integer. The constructor takes a reference to the shared atomic and an index (0 to `sizeof(T)*8 - 1`); it precomputes the bitmask `1 << index` for use in every subsequent operation. + +Three static assertions enforce correctness at compile time: `T` must be unsigned, `std::atomic` must be always-lock-free (no fallback mutex), and the atomic must expose `fetch_or`/`fetch_and` — the primitive operations the lock depends on. + +**Lock acquisition** (`try_lock`) uses `fetch_or(mask_, memory_order_acquire)`. This atomically ORs the bit in and returns the *previous* value; if the returned value had that bit clear, the lock was uncontested and is now held. If the bit was already set, the lock was already taken and `try_lock` returns `false`. + +**The spin loop** in `lock()` implements the classic test-and-test-and-set (TATAS) pattern. After a failed `try_lock`, the thread spins on a `load(memory_order_relaxed)` rather than repeatedly issuing `fetch_or`. The relaxed load is intentional and critical: an exclusive read-modify-write operation like `fetch_or` always triggers a cache-line ownership transfer, so spinning with it under contention would flood the interconnect. The relaxed load allows the CPU to read from its local cache copy without broadcasting invalidation messages, dramatically reducing coherency traffic. + +**Unlocking** uses `fetch_and(~mask_, memory_order_release)`, atomically clearing only this lock's bit while leaving all sibling locks in the same word undisturbed. + +## `spinlock` + +This is a whole-word spinlock built on the same external `std::atomic`. It treats `0` as unlocked and `std::numeric_limits::max()` (all bits set) as locked. `try_lock()` uses `compare_exchange_weak(expected=0, desired=max, acquire, relaxed)` — acquiring on success, relaxing on failure. `unlock()` is a simple `store(0, memory_order_release)`. + +The same TATAS pattern governs the spin loop, and the same relaxed-load reasoning applies. + +**Critical compatibility caveat:** the file explicitly warns against mixing `spinlock` and `packed_spinlock` against the same atomic. A `spinlock` CAS checks for the full `0` state before acquiring; if any `packed_spinlock` elsewhere holds even a single bit, the `spinlock` will spin indefinitely. This is not merely a theoretical concern — in `SHAMapInnerNode.cpp`, both lock types share the same `lock_` member, and the code carefully partitions their usage (`spinlock` for whole-node operations, `packed_spinlock` for per-child-index operations) to avoid this conflict. + +## Memory Ordering Rationale + +The acquire-on-lock / release-on-unlock pairing is the minimum required for correctness: it creates a happens-before edge so that writes inside the critical section are visible to the next locker. The deliberate use of `memory_order_relaxed` on the polling loads is a performance optimization, not a correctness shortcut — it only governs how the spin loop observes the lock bit itself, not the protected data. + +## Real Usage in the Codebase + +In `AccountID.cpp`, `packed_spinlock` enables fine-grained sharding of a base58-encoded AccountID cache. A single `std::atomic` holds 64 independent spinlocks. When looking up an account, the hash of the `AccountID` is reduced modulo 64 to select a slot, and only that one bit is contested — no serialization occurs across unrelated slots. This is the paradigm case for packed spinlocks: many logically independent entries each need minimal mutual exclusion, and allocating a full mutex per entry would cost orders of magnitude more memory. + +In `SHAMapInnerNode.cpp`, individual child-node slots in the Merkle-Patricia trie are protected by per-slot `packed_spinlock` instances, enabling concurrent child pointer access. The whole-node `spinlock` protects coarser operations like cloning where all children need consistent visibility at once. + +## Design Trade-offs + +The inline documentation is candid about costs. Packing multiple locks into one word creates false-sharing at the *word level*: acquiring any one lock invalidates the entire cache line for every CPU that holds a copy, even if their target bits are uncontested. When contention is high across many bits simultaneously, this can make packed spinlocks *worse* than independent mutexes. The recommendation to use them only under profiling pressure is genuine — these are specialized tools whose value is space efficiency and NUMA-friendly hot-path latency, not broad-purpose locking. \ No newline at end of file diff --git a/include/xrpl/basics/strHex.h.ai.json b/include/xrpl/basics/strHex.h.ai.json new file mode 100644 index 0000000000..76f0639870 --- /dev/null +++ b/include/xrpl/basics/strHex.h.ai.json @@ -0,0 +1,30 @@ +{ + "args": [], + "classes": [], + "description": "Provides utility functions to convert binary data or containers into hexadecimal string representations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/strHex.h", + "functions": [ + { + "args": [ + "begin", + "end" + ], + "lineno": 8, + "name": "strHex" + }, + { + "args": [ + "from" + ], + "lineno": 22, + "name": "strHex" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/strHex.h.ai.md b/include/xrpl/basics/strHex.h.ai.md new file mode 100644 index 0000000000..4f4db55dda --- /dev/null +++ b/include/xrpl/basics/strHex.h.ai.md @@ -0,0 +1,47 @@ +# `strHex.h` — Binary-to-Hex String Conversion + +This header lives in `xrpl/basics` and provides the single utility `strHex`, a thin but carefully constructed wrapper around `boost::algorithm::hex`. Its role is to produce uppercase hexadecimal `std::string` representations of arbitrary binary data, and it is used pervasively across the XRPL codebase wherever raw bytes must be serialized for logging, JSON output, or human-readable display. + +## The Two Overloads + +The iterator-range form is the core implementation: + +```cpp +template +std::string strHex(FwdIt begin, FwdIt end) +``` + +It enforces at compile time — via `static_assert` — that `FwdIt` satisfies at least the `std::forward_iterator_tag` requirement. This is not a bureaucratic constraint: the implementation calls `std::distance(begin, end)` to pre-reserve the output string before delegating to `boost::algorithm::hex`. Forward iterators are guaranteed to be multi-pass, so the range can be measured and then iterated again. An input iterator is single-pass and would silently produce wrong results or undefined behavior if `distance` consumed it. The assertion catches this class of misuse at compile time rather than at runtime. + +The pre-reservation (`result.reserve(2 * std::distance(begin, end))`) is a deliberate performance optimization. Each input byte encodes to exactly two hex characters, so the output capacity is always known up front. Without it, `std::back_inserter` would trigger repeated reallocations as `boost::algorithm::hex` appends characters — potentially O(n log n) copies instead of O(n). + +The container overload is a convenience SFINAE shim: + +```cpp +template ().begin())> +std::string strHex(T const& from) +``` + +The second template parameter `class = decltype(std::declval().begin())` is a lightweight concept check that participates in overload resolution only for types that have a `begin()` member. This makes `strHex` uniformly callable on `std::string`, `std::vector`, `Blob`, `Slice`, and `base_uint` without requiring explicit specializations. The check is intentionally loose — it does not verify that `end()` exists or that the iterator satisfies the forward requirement — relying instead on the inner overload's `static_assert` to catch any mismatch at instantiation time. + +## Integration With the Basics Module + +`Slice.h` includes `strHex.h` and uses it directly in its stream insertion operator: + +```cpp +template +Stream& operator<<(Stream& s, Slice const& v) { + s << strHex(v); + return s; +} +``` + +This makes any `Slice` — the ledger's canonical immutable byte-range view — directly loggable and JSON-serializable as hex. `base_uint.h` similarly pulls in `strHex.h` and uses both overloads: `to_string(base_uint)` calls `strHex(a.cbegin(), a.cend())` to render full 256-bit or 160-bit hashes, while a truncation helper uses `strHex(a.cbegin(), a.cbegin() + 4) + "..."` for compact diagnostic output. + +RPC handlers use `strHex` directly when building JSON responses — for example, `LedgerHeader.cpp` serializes raw ledger header bytes into the `ledger_data` JSON field via `strHex(s.peekData())`, where `peekData()` returns a `std::vector`. + +## Design Notes + +The header has no corresponding `.cpp` file — everything is header-only template code. There is no integer overload (e.g. for `uint32_t` or `uint64_t`), which is a deliberate omission: integer-to-hex formatting carries questions about byte order and padding width that are better handled explicitly at call sites. `boost::endian` is included but not directly used within this header; it appears to be an indirect dependency carried by callers that deal with endian-correct serialization before passing bytes to `strHex`. + +The result is always uppercase hex (Boost.Algorithm.Hex produces uppercase by default), which aligns with the XRPL convention of presenting all hashes and raw binary in uppercase hexadecimal throughout its JSON API and log output. \ No newline at end of file diff --git a/include/xrpl/basics/tagged_integer.h.ai.json b/include/xrpl/basics/tagged_integer.h.ai.json new file mode 100644 index 0000000000..e3e6c9c7d3 --- /dev/null +++ b/include/xrpl/basics/tagged_integer.h.ai.json @@ -0,0 +1,186 @@ +{ + "args": [ + { + "lineno": 17, + "name": "Int" + }, + { + "lineno": 17, + "name": "Tag" + }, + { + "lineno": 27, + "name": "OtherInt" + }, + { + "lineno": 145, + "name": "HashAlgorithm" + } + ], + "classes": [ + { + "args": [ + "OtherInt value" + ], + "lineno": 18, + "name": "tagged_integer" + }, + { + "args": [], + "lineno": 146, + "name": "is_contiguously_hashable" + } + ], + "description": "Defines a type-safe wrapper around standard integral types called tagged_integer, providing type safety and supporting arithmetic, comparison, and bitwise operations. Also provides stream and string conversion operators, and a trait for hashability.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/tagged_integer.h", + "functions": [ + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 36, + "name": "operator<" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 41, + "name": "operator==" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 46, + "name": "operator+=" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 51, + "name": "operator-=" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 56, + "name": "operator*=" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 61, + "name": "operator/=" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 66, + "name": "operator%=" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 71, + "name": "operator|=" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 76, + "name": "operator&=" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 81, + "name": "operator^=" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 86, + "name": "operator<<=" + }, + { + "args": [ + "tagged_integer const& rhs" + ], + "lineno": 91, + "name": "operator>>=" + }, + { + "args": [], + "lineno": 96, + "name": "operator~" + }, + { + "args": [], + "lineno": 101, + "name": "operator+" + }, + { + "args": [], + "lineno": 106, + "name": "operator-" + }, + { + "args": [], + "lineno": 111, + "name": "operator++" + }, + { + "args": [], + "lineno": 116, + "name": "operator--" + }, + { + "args": [], + "lineno": 121, + "name": "operator Int" + }, + { + "args": [ + "std::ostream& s", + "tagged_integer const& t" + ], + "lineno": 126, + "name": "operator<<" + }, + { + "args": [ + "std::istream& s", + "tagged_integer& t" + ], + "lineno": 132, + "name": "operator>>" + }, + { + "args": [ + "tagged_integer const& t" + ], + "lineno": 138, + "name": "to_string" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 144, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/basics/tagged_integer.h.ai.md b/include/xrpl/basics/tagged_integer.h.ai.md new file mode 100644 index 0000000000..bffa61b351 --- /dev/null +++ b/include/xrpl/basics/tagged_integer.h.ai.md @@ -0,0 +1,77 @@ +# `tagged_integer.h` — Type-Safe Integer Wrapper + +## Purpose + +`tagged_integer.h` defines a template class that wraps any standard integral type (`Int`) and makes it a distinct C++ type by binding it to a phantom `Tag`. Two `tagged_integer` instantiations that share the same underlying integer type but carry different tags are completely separate types: they cannot be compared, assigned to each other, or passed interchangeably. This is the phantom-type (or "strong typedef") idiom applied to integers, and its entire value proposition is catching conceptual misuse at compile time rather than at runtime. + +The motivation is clear in a ledger protocol codebase where many distinct concepts are represented as bare integers — ledger sequence numbers, NFT taxon codes, transaction indices, ledger IDs. Without distinct types, a function that expects a sequence number will silently accept a taxon value. `tagged_integer` eliminates this class of bug with zero runtime cost. + +## Design of the Template Parameters + +`tagged_integer` takes two parameters: `Int` is the underlying integral storage type, and `Tag` is a phantom type whose sole purpose is to make each instantiation unique. `Tag` never needs to be a complete or meaningful type; in practice it is defined as an empty struct nested inside the owning class: + +```cpp +struct SeqTag; +using Seq = tagged_integer; + +struct IdTag; +using ID = tagged_integer; +``` + +`Seq` and `ID` both wrap `uint32_t`, but the compiler treats them as wholly unrelated types. You cannot pass a `Seq` where an `ID` is expected without an explicit cast. + +## Constructor Narrowing Guard + +The constructor is explicitly constrained to reject widening conversions: + +```cpp +template ::value && sizeof(OtherInt) <= sizeof(Int)>::type> +explicit constexpr tagged_integer(OtherInt value) noexcept : m_value(value) +``` + +The `sizeof(OtherInt) <= sizeof(Int)` guard prevents constructing a 32-bit `tagged_integer` from a 64-bit literal, which would silently truncate the value. A `uint32_t`-backed instance can be constructed from any narrower-or-equal integral type, but not from a wider one. The test suite exercises this through `static_assert` checks: `TagUInt1` (backed by `uint32_t`) is constructible from `uint32_t` but not from `uint64_t`; `TagUInt3` (backed by `uint64_t`) accepts both. + +The constructor is `explicit`, so raw integers cannot implicitly convert to a `tagged_integer` — nor can one `tagged_integer` implicitly convert to another, since there is no converting constructor between tagged types. Assignment is similarly restricted: the test verifies that `TagUInt1 = TagUInt2` does not compile even when both wrap `uint32_t`. + +A `static_assert` inside the constructor body confirms that no hidden padding was introduced (`sizeof(tagged_integer) == sizeof(Int)`), preserving the layout guarantee needed for contiguous hashing. + +## Operator Coverage via Boost.Operators + +Rather than spelling out every binary operator, the class inherits from a chain of Boost.Operators mixins: + +``` +boost::totally_ordered<..., + boost::integer_arithmetic<..., + boost::bitwise<..., + boost::unit_steppable<..., + boost::shiftable<...>>>>> +``` + +Each mixin synthesizes additional operators from the handful explicitly defined: + +- `totally_ordered` derives `!=`, `>`, `<=`, `>=` from `operator<` and `operator==`. +- `integer_arithmetic` derives binary `+`, `-`, `*`, `/`, `%` (and postfix `++`/`--`) from the compound-assignment operators and `unit_steppable`. +- `bitwise` derives binary `&`, `|`, `^` from `&=`, `|=`, `^=`. +- `shiftable` derives binary `<<`, `>>` from `<<=`, `>>=`. + +The compound-assignment operators are implemented by delegating to the same operation on `m_value`, so there is no performance overhead — inlining collapses them to single instructions. Unary `~`, `+`, and `-` are defined manually since they have no Boost mixin equivalent. + +The explicit cast `operator Int()` provides deliberate, opt-in access to the underlying integer, used when calling APIs or doing cross-domain conversions. Its `explicit` qualifier ensures that no accidental implicit decay to the raw type occurs. + +## Hashing Integration + +A specialization of `beast::is_contiguously_hashable` is provided in the `beast` namespace: + +```cpp +template +struct is_contiguously_hashable, HashAlgorithm> + : public is_contiguously_hashable +``` + +This inherits hashability directly from the underlying `Int` type. If `Int` can be hashed by copying its raw bytes (i.e., its bit pattern is canonical and platform-consistent), then a `tagged_integer` wrapping it can be too. The layout guarantee from the constructor's `static_assert` makes this safe: the wrapper adds no bytes and no padding, so hashing a `tagged_integer` is identical to hashing its `Int`. This enables `tagged_integer` instances to participate in Beast's hash-based containers and message-digest pipelines without any extra boilerplate per instantiation. + +## Relationship to the Broader Codebase + +The type is used in at least two protocol-level headers. In `include/xrpl/protocol/nft.h`, `Taxon` is defined as `tagged_integer`, distinguishing NFT taxon codes from raw integers. In `src/test/csf/ledgers.h`, the simulated ledger harness defines both `Ledger::Seq` and `Ledger::ID` as separate `tagged_integer` specializations wrapping the same `uint32_t` storage, making it impossible to accidentally pass a ledger sequence where a ledger identity is expected. `LedgerTiming.h` documents that its template function works with both built-in integers and `tagged_integer`s for the `Seq` parameter, confirming the type is designed to be a drop-in replacement wherever integers already work. \ No newline at end of file diff --git a/include/xrpl/beast/asio/io_latency_probe.h.ai.json b/include/xrpl/beast/asio/io_latency_probe.h.ai.json new file mode 100644 index 0000000000..512dcec1f3 --- /dev/null +++ b/include/xrpl/beast/asio/io_latency_probe.h.ai.json @@ -0,0 +1,169 @@ +{ + "args": [ + { + "lineno": 23, + "name": "period" + }, + { + "lineno": 23, + "name": "ios" + }, + { + "lineno": 67, + "name": "handler" + }, + { + "lineno": 80, + "name": "handler" + }, + { + "lineno": 92, + "name": "lock" + }, + { + "lineno": 92, + "name": "wait" + }, + { + "lineno": 117, + "name": "handler" + }, + { + "lineno": 117, + "name": "start" + }, + { + "lineno": 117, + "name": "repeat" + }, + { + "lineno": 117, + "name": "probe" + }, + { + "lineno": 127, + "name": "from" + }, + { + "lineno": 170, + "name": "ec" + } + ], + "classes": [ + { + "args": [ + "duration const& period", + "boost::asio::io_context& ios" + ], + "lineno": 11, + "name": "io_latency_probe" + }, + { + "args": [ + "Handler const& handler", + "time_point const& start", + "bool repeat", + "io_latency_probe* probe" + ], + "lineno": 116, + "name": "sample_op" + } + ], + "description": "Defines a template class beast::io_latency_probe that measures handler latency on a boost::asio::io_context queue, providing synchronous and asynchronous sampling and cancellation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/asio/io_latency_probe.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "get_io_context" + }, + { + "args": [], + "lineno": 43, + "name": "get_io_context" + }, + { + "args": [], + "lineno": 51, + "name": "cancel" + }, + { + "args": [], + "lineno": 57, + "name": "cancel_async" + }, + { + "args": [ + "Handler&& handler" + ], + "lineno": 67, + "name": "sample_one" + }, + { + "args": [ + "Handler&& handler" + ], + "lineno": 80, + "name": "sample" + }, + { + "args": [ + "std::unique_lock& lock", + "bool wait" + ], + "lineno": 92, + "name": "cancel" + }, + { + "args": [], + "lineno": 104, + "name": "addref" + }, + { + "args": [], + "lineno": 109, + "name": "release" + }, + { + "args": [ + "Handler const& handler", + "time_point const& start", + "bool repeat", + "io_latency_probe* probe" + ], + "lineno": 117, + "name": "sample_op" + }, + { + "args": [ + "sample_op&& from" + ], + "lineno": 127, + "name": "sample_op" + }, + { + "args": [], + "lineno": 139, + "name": "~sample_op" + }, + { + "args": [], + "lineno": 143, + "name": "operator()" + }, + { + "args": [ + "boost::system::error_code const& ec" + ], + "lineno": 170, + "name": "operator()" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/asio/io_latency_probe.h.ai.md b/include/xrpl/beast/asio/io_latency_probe.h.ai.md new file mode 100644 index 0000000000..0881cdf80d --- /dev/null +++ b/include/xrpl/beast/asio/io_latency_probe.h.ai.md @@ -0,0 +1,41 @@ +# `io_latency_probe.h` — IO Context Latency Measurement + +## Role in the System + +`io_latency_probe` exists to answer a critical operational question: how backed-up is the ASIO `io_context` dispatch queue? In the XRPL node, nearly all networked and timer-driven work flows through a single `io_context`. If that context becomes saturated — because handlers are slow, threads are starved, or work is piling up — handlers experience real but invisible delay between being posted and actually running. This class makes that invisible delay visible by injecting sentinel handlers whose sole purpose is to be timed. + +The class lives in `include/xrpl/beast/asio/io_latency_probe.h` inside the `beast` namespace and is instantiated in `Application.cpp` as `io_latency_sampler`, sampling at 100ms intervals and emitting a metrics event for any latency above 10ms, plus a journal warning above 500ms. + +## Measurement Mechanism + +The core technique is straightforward: `sample_one()` records `Clock::now()` then calls `boost::asio::post()` to queue a `sample_op` handler. When the `io_context` eventually dispatches that handler, it captures `Clock::now()` again and computes `elapsed = now - start`. The difference is not the handler's own execution time — it is purely the queue depth, expressed as time. A healthy, idle context shows near-zero latency; a loaded one can show hundreds of milliseconds. + +`sample()` works identically for the first measurement but then reschedules itself using `m_timer` to continue sampling at the configured `m_period`. + +## The Timer Compensation Formula + +The repeated-sampling path contains a subtle but important design choice. After measuring `elapsed` latency, the code computes the next wake time as: + +```cpp +typename Clock::time_point const when(now + m_probe->m_period - 2 * elapsed); +``` + +The factor of `2 * elapsed` is intentional. After observing latency `elapsed`, the timer is set to expire in `period - elapsed`. But the timer's async-wait completion handler itself must pass through the same `io_context` queue before executing, incurring another `elapsed` of delay. By subtracting the full `2 * elapsed`, the two delays cancel out, keeping the inter-sample interval close to `m_period` even under moderate load. When latency is so severe that `when <= now`, the code bypasses the timer entirely and calls `boost::asio::post()` directly — there is no point asking a timer to wait a negative duration. + +The timer completion path (the `error_code` overload of `operator()`) does not call the user handler at all — it just posts a fresh `sample_op` using `now` as the new start time. The handler is only invoked from the no-argument overload, which runs the real elapsed-time measurement. + +## Reference Counting and Safe Teardown + +The class uses an intrusive reference count (`m_count`) to guarantee the destructor blocks until all in-flight `sample_op` objects have finished. The count starts at 1 representing the probe itself. Every `sample_op` constructor calls `addref()` and every destructor calls `release()`. When `release()` drops the count to zero it notifies `m_cond`, waking the destructor's `m_cond.wait()`. + +Cancellation consumes that initial "1": calling `cancel()` sets `m_cancel = true` and does `--m_count`. With `wait = true` (the synchronous path used by the destructor), it then waits on `m_cond` until all outstanding ops drain. `cancel_async()` sets the flag and returns immediately, useful when calling from within the `io_context` thread itself to avoid a deadlock. + +Once `m_cancel` is true, `sample_one()` and `sample()` throw `std::logic_error` rather than silently accept new work. `sample_op::operator()()` checks `m_cancel` before scheduling the next repetition, so a running probe stops cleanly after its current dispatch. + +## Recursive Mutex Rationale + +`m_mutex` is a `std::recursive_mutex` rather than a plain `std::mutex`. Inside `sample()` and `sample_one()`, the public lock guard is held when a temporary `sample_op` is constructed — which immediately calls `addref()`, which tries to acquire the same mutex. Without reentrancy the constructor would deadlock against the caller. The temporary `sample_op` created inline as an argument to `post()` also gets destroyed (after being moved into the queue), triggering `release()` under the same lock. The recursive mutex handles both. + +## Production Usage + +In `ApplicationImp::io_latency_sampler`, the probe is wrapped with a `beast::insight::Event` to push latency readings into the application's collector metrics system. Any sample ≥ 10ms fires a stats event; any sample ≥ 500ms writes a journal warning. The `getIOLatency()` method on the `Application` interface returns the most recent sample atomically via `std::atomic`, allowing other subsystems to inspect current io_context health without blocking. \ No newline at end of file diff --git a/include/xrpl/beast/clock/abstract_clock.h.ai.json b/include/xrpl/beast/clock/abstract_clock.h.ai.json new file mode 100644 index 0000000000..1da6a9b9cf --- /dev/null +++ b/include/xrpl/beast/clock/abstract_clock.h.ai.json @@ -0,0 +1,71 @@ +{ + "args": [ + { + "lineno": 18, + "name": "Clock" + }, + { + "lineno": 43, + "name": "Facade" + }, + { + "lineno": 43, + "name": "Clock" + }, + { + "lineno": 61, + "name": "Facade" + }, + { + "lineno": 61, + "name": "Clock" + } + ], + "classes": [ + { + "args": [ + "abstract_clock()", + "abstract_clock(abstract_clock const&)" + ], + "lineno": 19, + "name": "abstract_clock" + }, + { + "args": [ + "abstract_clock_wrapper()" + ], + "lineno": 44, + "name": "abstract_clock_wrapper" + } + ], + "description": "Defines an abstract interface for a clock, allowing dependency injection of time sources for testing and flexibility, with facilities to wrap standard clocks and provide global instances.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/clock/abstract_clock.h", + "functions": [ + { + "args": [], + "lineno": 32, + "name": "now" + }, + { + "args": [], + "lineno": 51, + "name": "now" + }, + { + "args": [], + "lineno": 62, + "name": "get_abstract_clock" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "beast" + }, + { + "lineno": 41, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/clock/abstract_clock.h.ai.md b/include/xrpl/beast/clock/abstract_clock.h.ai.md new file mode 100644 index 0000000000..a63ecf2491 --- /dev/null +++ b/include/xrpl/beast/clock/abstract_clock.h.ai.md @@ -0,0 +1,48 @@ +# `abstract_clock.h` — Dependency-Injectable Clock Interface + +## Role and Motivation + +`abstract_clock` exists to solve a pervasive testability problem in systems that rely on time. The C++ standard library clock types (`std::chrono::steady_clock`, `std::chrono::system_clock`, etc.) expose `now()` as a **static** member function — there is no instance to swap out. Any component that calls `steady_clock::now()` directly is hardwired to wall time and cannot be controlled from a test harness. + +This header, part of the `beast` utility layer embedded inside the XRPL codebase, breaks that dependency by wrapping the clock concept behind a virtual interface whose `now()` is a **virtual instance method**. The clock can then be passed by reference as a constructor argument, and a test can supply a `manual_clock` instead of the real clock. + +## Design Walkthrough + +`abstract_clock` inherits the full suite of nested types from the template parameter — `rep`, `period`, `duration`, `time_point`, and `clock_type` — so downstream code can continue to use standard `` vocabulary (`clock_type::time_point`, etc.) without caring whether the underlying clock is real or simulated. The single `is_steady` constant is also mirrored as a `static constexpr` member so callers can inspect it at compile time. + +The interface itself is minimal by design: only one pure virtual method, `now()`, marked `[[nodiscard]]`. That's all a clock needs to provide. The destructor is virtual to ensure correct cleanup through a base pointer, and the copy constructor is defaulted so subclasses can still be copied if they choose. + +## The `abstract_clock_wrapper` Adapter + +`detail::abstract_clock_wrapper` is a thin concrete subclass that delegates `now()` to a separate `Clock` type's static `now()`. This introduces the **Facade vs. Clock** split: the public interface presents itself as `abstract_clock`, but the actual time sampling goes through `Clock`. In practice this allows, for example, wrapping `beast::basic_seconds_clock` (a coarse, cached clock) behind a `std::chrono::steady_clock`-typed interface — so callers deal only in standard `time_point` types while the implementation trades syscall frequency for throughput. + +## Global Instance Factory + +`get_abstract_clock()` returns a reference to a `static` instance of `abstract_clock_wrapper`. The `Clock` template parameter defaults to `Facade`, so the common case — wrapping a real standard clock — requires only one type argument. The static-local variable gives the singleton lifetime without requiring explicit initialization order management. + +The broader codebase consumes this factory through `xrpl::stopwatch()` in `include/xrpl/basics/chrono.h`: + +```cpp +using Stopwatch = beast::abstract_clock; +using TestStopwatch = beast::manual_clock; + +inline Stopwatch& stopwatch() { + return beast::get_abstract_clock< + beast::basic_seconds_clock::Clock, // Facade = steady_clock + beast::basic_seconds_clock>(); // Clock = coarse cached clock +} +``` + +This means production code throughout the XRPL node holds a `Stopwatch&` (i.e., `abstract_clock&`) and calls `clock_.now()` on it, while unit tests replace it with a `TestStopwatch` (i.e., `manual_clock`) whose time can be advanced deterministically. + +## Concrete Implementations + +- **`manual_clock`** (in `manual_clock.h`) — the test double. Stores time as a `time_point` member and exposes `set()`, `advance()`, and `operator++()` to move it forward. An `XRPL_ASSERT` enforces monotonicity when `Clock::is_steady` is true, preventing tests from accidentally reversing a steady clock. + +- **`basic_seconds_clock`** (in `basic_seconds_clock.h`) — the production implementation. Its `now()` is backed by a background thread that samples `std::chrono::steady_clock` at least once per second and caches the result, reducing the per-call cost for high-frequency callers throughout the networking and consensus layers. + +## Usage in XRPL Components + +`abstract_clock` shows up widely across the XRPL node's core layers. `TimeKeeper` extends `beast::abstract_clock` directly, making the network-adjusted clock injectable. Consensus (`Validations.h`, `Consensus.h`), peer management (`PeerfinderManager.h`, `Slot.h`), and ledger acquisition (`InboundLedgers.h`, `InboundTransactions.h`) all accept an `abstract_clock` reference, enabling their time-dependent logic to be driven by a `manual_clock` in unit tests without any stubbing framework. + +The pattern is a textbook application of the Dependency Injection principle applied specifically to time — a dependency that is easy to ignore until you need deterministic test coverage of timeout, expiry, and rate-limiting logic. \ No newline at end of file diff --git a/include/xrpl/beast/clock/basic_seconds_clock.h.ai.json b/include/xrpl/beast/clock/basic_seconds_clock.h.ai.json new file mode 100644 index 0000000000..702a893903 --- /dev/null +++ b/include/xrpl/beast/clock/basic_seconds_clock.h.ai.json @@ -0,0 +1,28 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "void" + ], + "lineno": 11, + "name": "basic_seconds_clock" + } + ], + "description": "Defines a basic_seconds_clock class in the beast namespace, providing a clock with at least one-second resolution, optimized for performance by sampling time in a dedicated thread.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/clock/basic_seconds_clock.h", + "functions": [ + { + "args": [], + "lineno": 25, + "name": "now" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/clock/basic_seconds_clock.h.ai.md b/include/xrpl/beast/clock/basic_seconds_clock.h.ai.md new file mode 100644 index 0000000000..fec53a908b --- /dev/null +++ b/include/xrpl/beast/clock/basic_seconds_clock.h.ai.md @@ -0,0 +1,52 @@ +# `basic_seconds_clock.h` — Low-Cost Cached Steady Clock + +## Role and Motivation + +`basic_seconds_clock` exists to make `now()` calls cheap. In a ledger server like rippled, dozens of subsystems (rate limiters, cache eviction, connection timeouts, fee escalation windows) query the current time on nearly every incoming message or transaction. Each call to `std::chrono::steady_clock::now()` crosses into the OS kernel. That cost is individually small but collectively significant under high load. + +The solution is a cached clock: a background thread samples the real clock at most once per second and stores the result atomically. Every call to `basic_seconds_clock::now()` from the application is then reduced to a single lock-free atomic load — no syscall, no lock acquisition, no kernel transition. The tradeoff is that the returned time may be up to one second stale, which is acceptable given that the ledger's natural time granularity (close intervals, expiry timers, network time) is measured in seconds. + +## Interface Design + +The class mirrors the `std::chrono::Clock` concept exactly — exposing `rep`, `period`, `duration`, `time_point`, and `is_steady` all aliased from `std::chrono::steady_clock`. This makes it a drop-in replacement anywhere a clock type is expected, and it satisfies the `Clock` named requirement so it can be used as a template argument. + +The `Clock` typedef (`std::chrono::steady_clock`) is publicly accessible, which matters for the `xrpl::stopwatch()` adapter (see `chrono.h`): it uses `Clock::Clock` as the facade type and `Clock` itself as the concrete implementation when calling `beast::get_abstract_clock()`, bridging the concrete cached clock into the `abstract_clock` dependency-injection interface that most XRPL subsystems depend on. + +## Implementation: `seconds_clock_thread` + +The real work lives in the unnamed namespace of `basic_seconds_clock.cpp` inside `seconds_clock_thread`. The design has three components: + +**Shared state**: A single `std::atomic` named `tp_` holds the raw integer representation of the last sampled `time_point`. A compile-time `static_assert` confirms `std::atomic::is_always_lock_free`, ensuring the atomic load in `now()` is truly lock-free on every supported platform — not just sometimes. + +**Background thread**: `run()` holds a `std::unique_lock` on `mut_` for its entire lifetime (the lock guards `stop_`, not `tp_`). On each iteration it calls `Clock::now()`, stores the result into `tp_` with a relaxed-ish atomic write, then calls `cv_.wait_until()` targeting the next second boundary (`floor(now) + 1s`). The predicate `[this] { return stop_; }` means the thread wakes up either at the next second tick or immediately when `stop_` is set. Targeting the floor-of-next-second rather than sleeping for a fixed one second ensures the cached time is refreshed at consistent wall-clock boundaries regardless of sampling jitter. + +**Shutdown**: The destructor sets `stop_ = true` under the mutex, releases the lock immediately so the background thread can observe it at the earliest moment (the comment calls this out explicitly), then calls `cv_.notify_one()` to unblock the `wait_until`, and finally `thread_.join()`. The `XRPL_ASSERT` in the destructor guards against double-destruction or misuse scenarios where the thread was never started. + +## Singleton Lifetime + +`basic_seconds_clock::now()` uses a function-local static: + +```cpp +static seconds_clock_thread clk; +return clk.now(); +``` + +This is the Meyer's singleton pattern. The `seconds_clock_thread` is constructed on the first call to `now()` and destroyed at program exit. The `clk.now()` call itself is just `Clock::time_point{Clock::duration{tp_.load()}}` — reconstructing a `time_point` from the atomic integer. This is the entire hot path: one atomic load and a trivial cast. + +## Integration with `xrpl::stopwatch()` + +`include/xrpl/basics/chrono.h` wires `basic_seconds_clock` into the broader system: + +```cpp +inline Stopwatch& stopwatch() { + using Clock = beast::basic_seconds_clock; + using Facade = Clock::Clock; // steady_clock + return beast::get_abstract_clock(); // cached impl +} +``` + +`Stopwatch` is defined as `beast::abstract_clock`, so callers type-check against the standard steady clock interface while the runtime delegates to the background-sampled implementation. Code under test substitutes `TestStopwatch` (a `manual_clock`) via the same `abstract_clock` polymorphism, completely bypassing `basic_seconds_clock`. + +## Concurrency and Safety + +`tp_` is the only state shared between `seconds_clock_thread` and any number of callers. Because it is a lock-free atomic, concurrent `now()` calls from multiple threads never contend with each other or with the background updater. The mutex and condition variable are exclusively internal to the background thread's sleep/wake cycle and shutdown handshake — they are never held during a `now()` call from application code. \ No newline at end of file diff --git a/include/xrpl/beast/clock/manual_clock.h.ai.json b/include/xrpl/beast/clock/manual_clock.h.ai.json new file mode 100644 index 0000000000..ad661bc1ff --- /dev/null +++ b/include/xrpl/beast/clock/manual_clock.h.ai.json @@ -0,0 +1,61 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "time_point const& now = time_point(duration(0))" + ], + "lineno": 15, + "name": "manual_clock" + } + ], + "description": "Implements a manual clock for unit testing, allowing manual advancement and setting of time, based on an abstract clock interface.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/clock/manual_clock.h", + "functions": [ + { + "args": [ + "time_point const& now = time_point(duration(0))" + ], + "lineno": 20, + "name": "manual_clock" + }, + { + "args": [], + "lineno": 24, + "name": "now" + }, + { + "args": [ + "time_point const& when" + ], + "lineno": 29, + "name": "set" + }, + { + "args": [ + "Integer seconds_from_epoch" + ], + "lineno": 36, + "name": "set" + }, + { + "args": [ + "std::chrono::duration const& elapsed" + ], + "lineno": 42, + "name": "advance" + }, + { + "args": [], + "lineno": 50, + "name": "operator++" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/clock/manual_clock.h.ai.md b/include/xrpl/beast/clock/manual_clock.h.ai.md new file mode 100644 index 0000000000..51bdf9f4a5 --- /dev/null +++ b/include/xrpl/beast/clock/manual_clock.h.ai.md @@ -0,0 +1,41 @@ +# `manual_clock.h` — Controllable Test Clock for the XRPL Beast Framework + +## Role and Purpose + +`manual_clock.h` provides `beast::manual_clock`, a concrete implementation of `abstract_clock` whose internal time never advances on its own. Instead, the caller drives all time progression explicitly through mutation methods. This makes it the standard tool for unit tests across the XRPL codebase wherever time-sensitive logic needs deterministic, reproducible behavior without waiting for wall-clock time to pass. + +## Design Relationship with `abstract_clock` + +The `abstract_clock` interface exists specifically to enable dependency injection of a clock — it promotes `now()` from a static member function (as in standard C++ clock types) to a virtual instance method, so production code can accept an `abstract_clock&` and tests can substitute a `manual_clock` without changing any other code. `manual_clock` is the canonical other half of that contract: the only concrete subclass that doesn't wrap a real system clock. + +The template parameter `Clock` must satisfy the C++ `Clock` concept (e.g., `std::chrono::steady_clock` or `std::chrono::system_clock`). This allows `manual_clock` to inherit the correct `rep`, `period`, `duration`, and `time_point` types from the underlying clock family, keeping type compatibility with code that depends on those associated types without ever calling the real clock. + +## State and Construction + +The class holds a single private member, `now_`, of type `time_point`. The constructor defaults `now_` to the epoch (`time_point(duration(0))`), but an explicit starting point can be supplied. This zero-epoch default is intentional: tests that want absolute timestamps set them explicitly with `set()`, while tests that only care about elapsed duration can work from zero without any setup. + +## Mutation API + +Three methods advance or position the clock: + +- **`set(time_point const& when)`** — Assigns an absolute time. For steady clocks (`Clock::is_steady == true`), an `XRPL_ASSERT` fires if `when` is earlier than the current time, enforcing the monotonicity guarantee that steady clocks provide in the real world. This check is compile-time bypassed for non-steady clocks (e.g., `system_clock`), which are allowed to go backwards. + +- **`set(Integer seconds_from_epoch)`** — A convenience overload that converts an integer second count into a `time_point` and delegates to the primary `set`. This allows test code to write terse statements like `c.set(1000)` instead of constructing a `time_point` explicitly. + +- **`advance(std::chrono::duration const& elapsed)`** — Adds a duration to the current time. The same monotonicity assertion applies for steady clocks. The template parameters `Rep` and `Period` make this compatible with any `std::chrono` duration literal (milliseconds, seconds, etc.), so tests can write `c.advance(std::chrono::milliseconds(500))` without casts. + +- **`operator++()`** — A prefix-increment shorthand that calls `advance(std::chrono::seconds(1))`, returning `*this`. This supports chaining and enables expressive test loops where time is ticked forward one second per iteration. + +## Assertion Strategy + +Both `set()` and `advance()` use `XRPL_ASSERT`, which resolves to `ALWAYS_OR_UNREACHABLE` from the Antithesis instrumentation framework — in non-Antithesis debug builds this is equivalent to a standard `assert`. The guard condition (`!Clock::is_steady || ...`) is a compile-time short-circuit: if the clock family is non-steady, the check is never evaluated, so there is zero overhead and no false positives when deliberately moving a system clock backwards in tests. + +## Usage Patterns in Tests + +In `beast_abstract_clock_test.cpp`, the manual clock is instantiated as `manual_clock`, time is positioned with `set()`, and `now()` is called to observe the current value — all without sleeping, making the test instant and deterministic. + +In the Consensus Simulation Framework (`src/test/csf/SimTime.h`), the entire simulation time axis is defined as a type alias: `using SimClock = beast::manual_clock`. This makes `manual_clock` the backbone of a multi-node ledger simulation, where hundreds of virtual nodes operate against a shared `SimClock` whose time is advanced by the scheduler to model network delays, timeouts, and proposal rounds — demonstrating that the class's lightweight design scales comfortably to complex simulation workloads. + +## Thread Safety + +`manual_clock` provides no internal synchronization. `now_` is a plain value type; concurrent reads are safe in practice because `time_point` is typically an integer alias, but concurrent writes from `set()` or `advance()` while another thread calls `now()` are a data race. For single-threaded test harnesses — the intended context — this is not a concern. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_container.h.ai.json b/include/xrpl/beast/container/aged_container.h.ai.json new file mode 100644 index 0000000000..fb0e413505 --- /dev/null +++ b/include/xrpl/beast/container/aged_container.h.ai.json @@ -0,0 +1,25 @@ +{ + "args": [ + { + "lineno": 5, + "name": "T" + } + ], + "classes": [ + { + "args": [], + "lineno": 6, + "name": "is_aged_container" + } + ], + "description": "Defines a type trait is_aged_container in the beast namespace to determine if a type is an aged container, defaulting to false.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_container.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_container.h.ai.md b/include/xrpl/beast/container/aged_container.h.ai.md new file mode 100644 index 0000000000..340817179c --- /dev/null +++ b/include/xrpl/beast/container/aged_container.h.ai.md @@ -0,0 +1,34 @@ +# `aged_container.h` — Type Trait Base for Aged Containers + +This small header establishes the foundation of the aged-container type system in the `beast` namespace. It defines `is_aged_container`, a type trait that answers the compile-time question: *is this type an aged container?* By default the answer is `std::false_type` — no type qualifies unless it explicitly opts in through a template specialization elsewhere. + +## Why This Exists + +The aged-container family (`aged_set`, `aged_map`, `aged_unordered_set`, etc.) all track the insertion time of each element using a `Clock` and expose a `chronological` range view. This enables time-based expiry — erasing elements older than some duration. Utility functions like `expire()` in `aged_container_utility.h` need to constrain their template parameters to types that actually provide this interface. Without a mechanism to distinguish aged containers from arbitrary types, such a function would compile against any container and fail at the point of use when `c.clock()` or `c.chronological` doesn't exist. + +The trait solves this cleanly via SFINAE: + +```cpp +template +typename std::enable_if::value, std::size_t>::type +expire(AgedContainer& c, std::chrono::duration const& age); +``` + +If `is_aged_container::value` is `false`, the `enable_if` substitution fails and the overload is dropped from consideration, producing a clear compile error rather than a cryptic missing-member error. + +## Opt-In Specialization Pattern + +The base template in this file is deliberately minimal — a single `std::false_type` default. The `std::true_type` specializations live in the concrete container implementation headers: + +- `detail/aged_ordered_container.h` specializes `is_aged_container` for `beast::detail::aged_ordered_container<...>`, covering `aged_set`, `aged_map`, `aged_multiset`, and `aged_multimap`. +- `detail/aged_unordered_container.h` does the same for `beast::detail::aged_unordered_container<...>`, covering the unordered variants. + +This separation is intentional. Code that only needs to write constrained templates over aged containers can include just this lightweight header without pulling in the full container machinery. The implementations, which are substantially heavier (hundreds of lines of intrusive-list bookkeeping and iterator logic), are kept entirely separate. + +## The Explicit Default Constructor + +Both the base template and each `std::true_type` specialization carry `explicit is_aged_container() = default;`. This suppresses aggregate-initialization warnings on older compilers that would otherwise treat a struct inheriting from `std::false_type` or `std::true_type` as an aggregate. It's a minor defensive pattern consistent across the entire trait hierarchy. + +## Relationship to the Broader Container Family + +All public aged container names (`aged_set`, `aged_map`, etc.) are type aliases for the private `detail::aged_ordered_container` or `detail::aged_unordered_container` templates, parameterized by `IsMulti` and `IsMap` booleans. The `is_aged_container` specializations match on those concrete implementation types, meaning the trait correctly identifies all eight public aliases as aged containers without requiring any additional specializations per alias. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_container_utility.h.ai.json b/include/xrpl/beast/container/aged_container_utility.h.ai.json new file mode 100644 index 0000000000..29466b80bf --- /dev/null +++ b/include/xrpl/beast/container/aged_container_utility.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 11, + "name": "AgedContainer& c" + }, + { + "lineno": 11, + "name": "std::chrono::duration const& age" + } + ], + "classes": [], + "description": "Provides a function to expire items from an aged container that are older than a specified duration.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_container_utility.h", + "functions": [ + { + "args": [ + "AgedContainer& c", + "std::chrono::duration const& age" + ], + "lineno": 10, + "name": "expire" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_container_utility.h.ai.md b/include/xrpl/beast/container/aged_container_utility.h.ai.md new file mode 100644 index 0000000000..f61c5699c7 --- /dev/null +++ b/include/xrpl/beast/container/aged_container_utility.h.ai.md @@ -0,0 +1,23 @@ +# `aged_container_utility.h` — Temporal Expiry for Aged Containers + +This header provides a single free function, `expire()`, that removes stale entries from any aged container in the `beast` namespace. It is the primary mechanism by which XRPL subsystems flush time-expired data from their aged maps and sets without scanning every element. + +## What It Does + +`expire(c, age)` sweeps the chronological front of an aged container and erases every element inserted more than `age` time units ago. The return value is the number of items removed. The caller provides the duration as any `std::chrono::duration` specialization, keeping the interface flexible across different time scales. + +The iteration exploits a key structural invariant of the aged containers: the `chronological` memberspace maintains elements in insertion order (oldest first). Because new elements are always appended to the back of the underlying `boost::intrusive::list` and `touch()` moves elements to the back, the front of the chronological sequence is always the oldest cohort. The loop therefore needs only to walk forward until it finds the first element not yet expired, after which the rest of the container is guaranteed to be younger. This makes the sweep `O(k)` in the number of expired elements — it stops as soon as it reaches live entries. + +The expiry threshold is computed once: `c.clock().now() - age`. Using the container's own `clock()` rather than a separately injected clock object ensures that the comparison is consistent with the timestamps that were recorded at insertion time. All aged containers are templated on a `Clock` type, defaulting to `std::chrono::steady_clock`, so this subtraction is always a well-typed `time_point` comparison. + +## Why a Free Function + +Eviction logic lives here as a free function rather than as a member of each container variant. This is deliberate: `aged_map`, `aged_set`, `aged_unordered_map`, `aged_unordered_set`, and their multi-key analogues are all distinct types (type aliases for `detail::aged_ordered_container` and `detail::aged_unordered_container`). Putting `expire` in each one would require either a virtual interface, a CRTP mixin, or eight near-identical member implementations. The free function avoids all of that — one algorithm, zero duplication, no runtime polymorphism. + +## SFINAE Guard + +The function signature uses `std::enable_if::value, std::size_t>::type` as the return type. `is_aged_container` is defined in `aged_container.h` as a traits struct defaulting to `std::false_type`; the detail implementations (`aged_ordered_container.h` and `aged_unordered_container.h`) each provide a `std::true_type` specialization for their concrete type. Passing a non-aged type to `expire()` therefore fails at template substitution rather than producing a confusing missing-member error deep inside the function body. + +## Usage in the Codebase + +The function's primary consumer is `src/xrpld/consensus/Validations.h`, which calls `beast::expire` on its `byLedger_` and `bySequence_` aged containers using a `validationSET_EXPIRES` duration parameter. This is how the validator set garbage-collects stale ledger validations — rather than tracking individual entries or running a scheduled scan of all entries, the consensus engine calls `expire()` at appropriate checkpoints and relies on the O(k) sweep to do the right amount of work proportional to how much has actually aged out. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_map.h.ai.json b/include/xrpl/beast/container/aged_map.h.ai.json new file mode 100644 index 0000000000..2ea1a98f34 --- /dev/null +++ b/include/xrpl/beast/container/aged_map.h.ai.json @@ -0,0 +1,35 @@ +{ + "args": [ + { + "lineno": 10, + "name": "Key" + }, + { + "lineno": 11, + "name": "T" + }, + { + "lineno": 12, + "name": "Clock" + }, + { + "lineno": 13, + "name": "Compare" + }, + { + "lineno": 14, + "name": "Allocator" + } + ], + "classes": [], + "description": "Defines a type alias 'aged_map' for an ordered associative container that tracks the age of its elements, using a specified clock and allocator, within the 'beast' namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_map.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_map.h.ai.md b/include/xrpl/beast/container/aged_map.h.ai.md new file mode 100644 index 0000000000..173fc6517d --- /dev/null +++ b/include/xrpl/beast/container/aged_map.h.ai.md @@ -0,0 +1,32 @@ +# `aged_map.h` — Time-Aware Ordered Associative Container + +`aged_map.h` is a thin type alias header that exposes `beast::aged_map` as a named type in the `beast` namespace. The file itself contains only the alias declaration; all implementation lives in `detail/aged_ordered_container.h`. + +```cpp +template , + class Allocator = std::allocator>> +using aged_map = detail::aged_ordered_container; +``` + +The two leading boolean template arguments to `aged_ordered_container` are the compile-time policy switches: `IsMulti = false` (unique keys, mirroring `std::map`) and `IsMap = true` (key-value pairs rather than bare keys). Companion aliases in the same directory follow the same pattern — `aged_multimap` passes `IsMulti = true`, while `aged_set` and `aged_multiset` pass `IsMap = false`. + +## What the underlying container provides + +`aged_ordered_container` maintains **two simultaneous index structures** over the same set of heap-allocated `element` nodes, using Boost.Intrusive to avoid any extra allocation: + +- A **Boost.Intrusive set** (or multiset) keyed on `Key`, providing the standard `O(log n)` ordered-map interface — `find`, `insert`, `erase`, `lower_bound`, `upper_bound`, etc. +- A **Boost.Intrusive doubly-linked list** ordered by insertion or last-touch time, exposed as the `chronological` memberspace. + +Each node stores both the `value_type` (`std::pair`) and a `time_point when` field. When an element is inserted, `when` is set to `clock().now()`. The `touch()` method updates `when` and moves the node to the tail of the chronological list, enabling LRU-style access tracking without any separate bookkeeping. + +## Clock abstraction and expiration + +The clock template parameter is wrapped through `abstract_clock`, which separates the clock's type from the `now()` call. This indirection lets test code inject a manual clock, advancing time deterministically to drive expiration logic — a critical capability in a ledger implementation where time-dependent behavior must be reproducible. + +The free function `expire()` in `aged_container_utility.h` demonstrates the intended usage pattern: it walks `chronological.cbegin()` forward, erasing every element whose `when` timestamp predates `clock().now() - age`. Because all insertions append to the back of the chronological list, the front always holds the oldest entry, so this is a linear-time sweep through a naturally ordered sequence — no sorting required. + +## Relationship to the container family + +`aged_map` is the direct drop-in for `std::map` in contexts where entries must carry an expiration age. The four ordered variants (`aged_map`, `aged_multimap`, `aged_set`, `aged_multiset`) and four unordered variants (`aged_unordered_map`, etc.) form a complete family, all delegating to `aged_ordered_container` or its unordered sibling. Choosing `aged_map` specifically means: unique keys, key-comparator ordering for lookup, and chronological ordering available as a secondary axis for cache management. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_multimap.h.ai.json b/include/xrpl/beast/container/aged_multimap.h.ai.json new file mode 100644 index 0000000000..d66898836c --- /dev/null +++ b/include/xrpl/beast/container/aged_multimap.h.ai.json @@ -0,0 +1,35 @@ +{ + "args": [ + { + "lineno": 11, + "name": "Key" + }, + { + "lineno": 12, + "name": "T" + }, + { + "lineno": 13, + "name": "Clock" + }, + { + "lineno": 14, + "name": "Compare" + }, + { + "lineno": 15, + "name": "Allocator" + } + ], + "classes": [], + "description": "Defines the aged_multimap template alias, a time-aware associative container allowing multiple values per key, based on aged_ordered_container.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_multimap.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_multimap.h.ai.md b/include/xrpl/beast/container/aged_multimap.h.ai.md new file mode 100644 index 0000000000..5b670b769f --- /dev/null +++ b/include/xrpl/beast/container/aged_multimap.h.ai.md @@ -0,0 +1,41 @@ +# `aged_multimap.h` — Time-Aware Multi-Value Associative Container + +## Purpose and Role + +`aged_multimap.h` defines `beast::aged_multimap`, a thin template alias that composes the full `detail::aged_ordered_container` implementation into a familiar, `std::multimap`-like interface extended with per-element timestamps. It lives alongside `aged_map`, `aged_set`, `aged_multiset`, and their unordered counterparts in the `beast` container family, all of which expose the same chronological tracking layer on top of standard ordered or hash-based associative semantics. + +## What the Alias Encodes + +The entire file reduces to one line: + +```cpp +using aged_multimap = detail::aged_ordered_container; +``` + +The two leading `bool` template arguments are the expressive core of `aged_ordered_container`'s design. `IsMulti = true` selects a `boost::intrusive::multiset` as the underlying sorted container, permitting duplicate keys — exactly the semantic difference between `std::map` and `std::multimap`. `IsMap = true` sets the `value_type` to `std::pair` rather than a bare key, enabling key-to-value mapping. Compare `aged_map`, which passes ``, with `aged_multiset`, which passes ``: the combinatorial product of those two flags yields all four ordered containers from a single implementation. + +## Template Parameters + +| Parameter | Default | Role | +|-----------|---------|------| +| `Key` | — | Sorted key type | +| `T` | — | Mapped value type | +| `Clock` | `std::chrono::steady_clock` | Clock used to timestamp insertions and `touch()` calls | +| `Compare` | `std::less` | Key ordering predicate | +| `Allocator` | `std::allocator>` | Element allocator | + +The `Clock` parameter is not used directly; `aged_ordered_container` wraps it in `abstract_clock`, an injectable abstraction that allows deterministic testing by substituting a manual clock without recompiling the container. + +## What `aged_ordered_container` Provides + +Because `aged_multimap` is a pure alias with no added members, all behavior comes from `aged_ordered_container`. Each element internally carries a `time_point` recording when it was inserted or last `touch()`-ed. The container maintains a parallel `boost::intrusive::list` of elements in insertion/touch order; this list is the backbone of the `chronological` memberspace, which exposes `begin()`/`end()` iterators that walk elements from oldest to newest. The intended use case is time-bounded caches: callers iterate the chronological range to expire all entries older than a given threshold, a pattern common in the XRPL fee queue, transaction cache, and node store layers. + +The `touch()` operation moves an element to the back of the chronological list without disturbing its position in the sorted key index — an O(1) intrusive list splice. This makes LRU-style eviction efficient without a secondary data structure. + +## Design Decision: Alias vs. Subclass + +Using a template alias rather than a derived class avoids vtable overhead, prevents accidental slicing, and keeps the public API identical across all four ordered aged-container variants without repeating any forwarding boilerplate. The cost is that `aged_multimap` cannot add member functions; any extension must be done in `aged_ordered_container` itself, guarded where necessary by the `IsMulti` / `IsMap` traits already present there (for example, `pair_value_compare` is compiled into every instantiation but only meaningful when `IsMap = true`). + +## Relationship to Sibling Files + +`aged_map.h` is structurally identical except for `IsMulti = false`. The two files exist separately — rather than as a single header with a flag parameter — to mirror the `` / `` naming convention from the standard library, making the intent of client code immediately readable at the point of declaration. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_multiset.h.ai.json b/include/xrpl/beast/container/aged_multiset.h.ai.json new file mode 100644 index 0000000000..a2aa8310d5 --- /dev/null +++ b/include/xrpl/beast/container/aged_multiset.h.ai.json @@ -0,0 +1,31 @@ +{ + "args": [ + { + "lineno": 10, + "name": "Key" + }, + { + "lineno": 11, + "name": "Clock" + }, + { + "lineno": 12, + "name": "Compare" + }, + { + "lineno": 13, + "name": "Allocator" + } + ], + "classes": [], + "description": "Defines the aged_multiset template alias in the beast namespace, which is a time-aware multiset container based on aged_ordered_container.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_multiset.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_multiset.h.ai.md b/include/xrpl/beast/container/aged_multiset.h.ai.md new file mode 100644 index 0000000000..cf52c7e615 --- /dev/null +++ b/include/xrpl/beast/container/aged_multiset.h.ai.md @@ -0,0 +1,18 @@ +# `aged_multiset.h` — Time-Aware Ordered Multiset Alias + +This file defines `beast::aged_multiset`, a thin template alias that exposes the multi-key, non-map variant of the underlying `detail::aged_ordered_container`. + +The alias maps directly to `aged_ordered_container`. The first boolean (`IsMulti = true`) switches the internal Boost.Intrusive storage from `boost::intrusive::set` to `boost::intrusive::multiset`, permitting duplicate keys. The second boolean (`IsMap = false`) means there is no mapped value type — the container stores keys only, with `void` passed as the `T` parameter. + +This fits into a four-way family of ordered aged containers, all backed by the same implementation template: + +| Alias | `IsMulti` | `IsMap` | +|---|---|---| +| `aged_set` | `false` | `false` | +| `aged_multiset` | `true` | `false` | +| `aged_map` | `false` | `true` | +| `aged_multimap` | `true` | `true` | + +The `aged_ordered_container` backing class augments every stored element with a `time_point` (`when`) drawn from the supplied `Clock`. A `chronological` member-space exposes begin/end iterators that traverse elements in insertion-time order, enabling efficient LRU- or TTL-style eviction without a secondary data structure. The `Clock` parameter defaults to `std::chrono::steady_clock` but is accessed through the `abstract_clock` wrapper, allowing test code to inject a mock clock. + +`aged_multiset` itself adds nothing beyond the alias — all interface, iterator, and eviction logic live in `detail/aged_ordered_container.h`. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_set.h.ai.json b/include/xrpl/beast/container/aged_set.h.ai.json new file mode 100644 index 0000000000..cdab1f3d2a --- /dev/null +++ b/include/xrpl/beast/container/aged_set.h.ai.json @@ -0,0 +1,31 @@ +{ + "args": [ + { + "lineno": 10, + "name": "Key" + }, + { + "lineno": 11, + "name": "Clock" + }, + { + "lineno": 12, + "name": "Compare" + }, + { + "lineno": 13, + "name": "Allocator" + } + ], + "classes": [], + "description": "Defines the aged_set type alias in the beast namespace, which is a set-like container that tracks the age of its elements using a specified clock.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_set.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_set.h.ai.md b/include/xrpl/beast/container/aged_set.h.ai.md new file mode 100644 index 0000000000..1e7514de96 --- /dev/null +++ b/include/xrpl/beast/container/aged_set.h.ai.md @@ -0,0 +1,61 @@ +# `aged_set.h` — Time-Indexed Ordered Set Alias + +## Role and Purpose + +`aged_set.h` is a single-line type-alias header that surfaces the `beast::aged_set` container into user code. Its entire body is: + +```cpp +template +using aged_set = detail::aged_ordered_container; +``` + +The two leading `bool` arguments to the underlying template encode the container's personality: `IsMulti=false` (unique keys, like `std::set`) and `IsMap=false` (no mapped value, keys carry the full payload). The entire implementation lives in `detail/aged_ordered_container.h`; this file's only job is to present a clean, std-library-style name with sensible defaults. + +## The Underlying `aged_ordered_container` + +The real complexity is in `aged_ordered_container`, which maintains **two parallel intrusive data structures** over the same heap-allocated `element` objects: + +- A **Boost.Intrusive set** (`cont_type`) — providing O(log n) key lookup, sorted by `Compare`. For `IsMulti=false` this is `boost::intrusive::set`. +- A **Boost.Intrusive list** (`chronological.list`) — an insertion-ordered sequence where the head holds the oldest element and the tail the newest. + +Each `element` node inherits from both `boost::intrusive::set_base_hook` and `boost::intrusive::list_base_hook`, so a single allocation simultaneously participates in both structures at zero extra overhead per node. + +The `element` struct stores two fields: the `value_type` (just `Key` for a set) and a `time_point when` recording when the element was last inserted or touched. The timestamp is stamped from `clock().now()` at allocation time inside `new_element()`, eliminating any separate timestamp-injection ceremony at the call site. + +## Clock Abstraction and Testability + +The `Clock` template parameter is not used raw — it is wrapped via `abstract_clock`, a reference-semantic wrapper that the container stores as a `std::reference_wrapper`. This indirection is the key to testability: production code passes a real `std::chrono::steady_clock`-based clock, while unit tests can substitute a manually advanced mock clock. All constructors require a clock reference; `aged_ordered_container() = delete` enforces this — there is no default-constructed, clock-free state. + +## The `chronological` Memberspace + +The `chronological` member object (an instance of the private `chronological_t` class) exposes a separate iterator range over the intrusive list in insertion/touch order. This "memberspace" pattern (from an ACCU article cited in the source) lets user code write: + +```cpp +for (auto it = s.chronological.begin(); it != s.chronological.end(); ++it) { ... } +``` + +while the key-ordered range is accessed through the container's primary `begin()`/`end()`. The chronological iterators support reverse traversal as well, so both oldest-first and newest-first sweeps are O(1) to begin. + +## `touch()` — LRU Semantics + +The `touch()` member function (available by key or by iterator) updates an element's `when` timestamp and moves it to the **tail** of the chronological list. The iterator-based overload uses `enable_if` to reject reverse iterators at compile time — mutating through a reverse iterator would silently corrupt the list order, so the trait `is_boost_reverse_iterator` is used to catch that at substitution time rather than at runtime. + +This design makes `aged_set` a direct building block for LRU caches and time-expiry tables: iterate `chronological.begin()` to find elements that have been untouched longest, erase them until only recently-touched entries remain. + +## Iterator Constness for Set Semantics + +Because keys in a set must be immutable (mutation would silently corrupt the ordering invariant), `iterator` and `reverse_iterator` are defined as `aged_container_iterator`. With `IsMap=false`, `!IsMap=true`, so the boolean template argument that controls constness is `true`, making the non-const iterator aliases identical to the const ones. This mirrors `std::set`'s guarantee that even non-const iterators yield `const Key&`. + +## Concrete Usage in XRPL + +The PeerFinder subsystem uses `aged_set` directly as its `Squelches` type: + +```cpp +using Squelches = beast::aged_set; +``` + +`ConnectHandouts` inserts an `IP::Address` into the squelch set the moment an outgoing connection attempt is made. The container's dual-index structure lets the caller both do O(log n) duplicate checks (`insert` returns a bool) and sweep expired squelch entries cheaply by walking `chronological.begin()` forward until ages fall within the acceptable window — no separate timestamp map is needed. + +## Sibling Variants + +The aged container family follows a consistent aliasing pattern. `aged_multiset` sets `IsMulti=true`. `aged_map` and `aged_multimap` set `IsMap=true` (and supply a non-`void` mapped type). The unordered variants (`aged_unordered_set`, etc.) alias `detail::aged_unordered_container` instead, which replaces the intrusive set with a Boost hash table while keeping the same chronological list structure and `touch()` interface. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_unordered_map.h.ai.json b/include/xrpl/beast/container/aged_unordered_map.h.ai.json new file mode 100644 index 0000000000..628f074a4f --- /dev/null +++ b/include/xrpl/beast/container/aged_unordered_map.h.ai.json @@ -0,0 +1,39 @@ +{ + "args": [ + { + "lineno": 11, + "name": "Key" + }, + { + "lineno": 12, + "name": "T" + }, + { + "lineno": 13, + "name": "Clock" + }, + { + "lineno": 14, + "name": "Hash" + }, + { + "lineno": 15, + "name": "KeyEqual" + }, + { + "lineno": 16, + "name": "Allocator" + } + ], + "classes": [], + "description": "Defines the aged_unordered_map template alias in the beast namespace, which is a time-aware unordered map container using customizable key, value, clock, hash, key equality, and allocator types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_unordered_map.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_unordered_map.h.ai.md b/include/xrpl/beast/container/aged_unordered_map.h.ai.md new file mode 100644 index 0000000000..98bc9da866 --- /dev/null +++ b/include/xrpl/beast/container/aged_unordered_map.h.ai.md @@ -0,0 +1,71 @@ +# `aged_unordered_map.h` + +## Role in the System + +`aged_unordered_map.h` is a thin alias header that exposes `beast::aged_unordered_map` — a hash-based associative container that enriches every stored element with a `time_point` timestamp. It is the unordered-map variant in a family of "aged container" types (`aged_map`, `aged_set`, `aged_unordered_set`, etc.) and serves as the primary building block for time-aware caches throughout the XRPL codebase. + +The file itself is minimal: it configures the underlying `detail::aged_unordered_container` policy template with `IsMulti=false, IsMap=true` and re-exports it as a friendly public alias with the familiar `std::unordered_map` template parameter order. + +## What It Actually Defines + +```cpp +template < + class Key, + class T, + class Clock = std::chrono::steady_clock, + class Hash = std::hash, + class KeyEqual = std::equal_to, + class Allocator = std::allocator>> +using aged_unordered_map = + detail::aged_unordered_container; +``` + +The two boolean policy flags select the four possible specializations of `aged_unordered_container`: + +| `IsMulti` | `IsMap` | Result | +|-----------|---------|--------| +| `false` | `true` | `aged_unordered_map` — unique keys, key+value pairs | +| `true` | `true` | `aged_unordered_multimap` — duplicate keys, key+value pairs | +| `false` | `false` | `aged_unordered_set` — unique keys only | +| `true` | `false` | `aged_unordered_multiset` — duplicate keys only | + +`aged_unordered_multimap` differs only in passing `IsMulti=true`, routing to `boost::intrusive::make_unordered_multiset` internally. + +## The Underlying Data Structure + +`aged_unordered_container` maintains **two simultaneous Boost.Intrusive data structures** over the same set of heap-allocated `element` nodes: + +1. A **`boost::intrusive::unordered_set`** (or `unordered_multiset`) for O(1) key lookup. Buckets are owned by an internal `Buckets` helper that performs safe rehashing — it swaps to a temporary vector before resizing downward to avoid destroying non-empty buckets. + +2. A **`boost::intrusive::list`** for temporal ordering. Every insertion appends the new element to the back of this list; the front always holds the oldest entry. + +Each `element` inherits from both an `unordered_set_base_hook` and a `list_base_hook`, so it participates in both intrusive structures with zero additional memory. Timestamps (`time_point when`) are stored directly in the element alongside its `value_type`. + +This dual-structure design is why the container can efficiently serve both hash-based lookup and age-based eviction — no secondary index or sorted tree is needed. + +## Temporal Operations + +The `chronological` nested object (implementing the "memberspace" idiom from ACCU Journal #1527) provides a separate iterator range over all elements in insertion order, oldest-first. Iterators expose a `.when()` accessor returning the element's `time_point`. + +`touch(iterator)` or `touch(key)` updates an element's timestamp to `clock().now()` and moves it to the back of the chronological list — making it the newest element again. This is the key primitive for LRU-style cache semantics. + +The free function `expire(container, age)` in `aged_container_utility.h` walks the chronological list from the front and erases all entries whose timestamp is older than `clock().now() - age`. Because the list is maintained in insertion/touch order, expired items are always at the front; `expire` stops as soon as it hits an element that is young enough, making eviction O(expired) rather than O(n). + +## Clock Abstraction + +The `Clock` template parameter is wrapped in `abstract_clock`, a virtual interface. The container holds a `std::reference_wrapper` rather than owning the clock. This means: + +- Callers inject the clock at construction time. +- In production, `std::chrono::steady_clock` is the default. +- In tests, a mock clock can be passed in to control time explicitly without sleep. + +## Configuration Compression + +All stateless policy objects — `ValueHash` (wrapping `Hash`), `KeyValueEqual` (wrapping `KeyEqual`), and `ElementAllocator` — are composed into a single private `config_t` object using empty-base optimization (`beast::detail::empty_base_optimization`). For the common case where `Hash`, `KeyEqual`, and `Allocator` are all default-constructed stateless types, `config_t` contributes zero bytes of overhead beyond the clock reference. + +## Relationship to Other Files + +- **`detail/aged_unordered_container.h`** — the full implementation; this header is only safe to include through the public aliases. +- **`aged_unordered_multimap.h`** — the `IsMulti=true` sibling, identical layout. +- **`aged_container_utility.h`** — provides the `expire()` free function, the standard consumer of `chronological` iterators. +- **`aged_container.h`** — declares the `is_aged_container` trait used by `expire()` to constrain its template parameter. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_unordered_multimap.h.ai.json b/include/xrpl/beast/container/aged_unordered_multimap.h.ai.json new file mode 100644 index 0000000000..4c88a6be58 --- /dev/null +++ b/include/xrpl/beast/container/aged_unordered_multimap.h.ai.json @@ -0,0 +1,39 @@ +{ + "args": [ + { + "lineno": 9, + "name": "Key" + }, + { + "lineno": 10, + "name": "T" + }, + { + "lineno": 11, + "name": "Clock" + }, + { + "lineno": 12, + "name": "Hash" + }, + { + "lineno": 13, + "name": "KeyEqual" + }, + { + "lineno": 14, + "name": "Allocator" + } + ], + "classes": [], + "description": "Defines the aged_unordered_multimap template alias in the beast namespace, which is a time-aware unordered multimap container using a customizable clock, hash, key equality, and allocator.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_unordered_multimap.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_unordered_multimap.h.ai.md b/include/xrpl/beast/container/aged_unordered_multimap.h.ai.md new file mode 100644 index 0000000000..c2c7067398 --- /dev/null +++ b/include/xrpl/beast/container/aged_unordered_multimap.h.ai.md @@ -0,0 +1,9 @@ +# `aged_unordered_multimap.h` + +This header is a single-line template alias that exposes `beast::aged_unordered_multimap` as the public face of `detail::aged_unordered_container`. The two boolean flags encode the container's behavior: `IsMulti=true` permits duplicate keys (the underlying Boost.Intrusive layer uses `unordered_multiset` rather than `unordered_set`), and `IsMap=true` sets `value_type` to `std::pair` rather than a bare key. + +The full implementation lives in `detail/aged_unordered_container.h`. Each stored element carries an intrusive hook for both hash-bucket membership and a chronological doubly-linked list, giving the container simultaneous O(1) average-case lookup by key and O(1) iteration in insertion/touch order via its `chronological` memberspace. A `touch()` call refreshes an element's timestamp to `clock.now()` and moves it to the back of the chronological list. The `Clock` parameter is abstracted through `abstract_clock`, which makes the clock injectable for testing. + +Within the family of aged containers in this directory, `aged_unordered_multimap` is the union of both "multi" and "map" axes. Its sibling `aged_unordered_map` differs only in `IsMulti=false`, and `aged_unordered_multiset` uses `IsMap=false` to drop the mapped value. All four unordered variants share the same implementation template, differentiated solely by those two compile-time booleans. A `static_assert` in `aged_associative_container_test.cpp` verifies that the alias resolves to exactly `detail::aged_unordered_container`. + +The canonical use case is a time-aware cache where multiple values may share the same key — the chronological iterator allows a single sweep to evict all entries whose `when` timestamp predates a threshold, without any full-container scan. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_unordered_multiset.h.ai.json b/include/xrpl/beast/container/aged_unordered_multiset.h.ai.json new file mode 100644 index 0000000000..d4852ebdc5 --- /dev/null +++ b/include/xrpl/beast/container/aged_unordered_multiset.h.ai.json @@ -0,0 +1,35 @@ +{ + "args": [ + { + "lineno": 9, + "name": "Key" + }, + { + "lineno": 10, + "name": "Clock" + }, + { + "lineno": 11, + "name": "Hash" + }, + { + "lineno": 12, + "name": "KeyEqual" + }, + { + "lineno": 13, + "name": "Allocator" + } + ], + "classes": [], + "description": "Defines the aged_unordered_multiset template alias in the beast namespace, providing an unordered multiset container with aging capabilities using a customizable clock, hash, key equality, and allocator.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_unordered_multiset.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_unordered_multiset.h.ai.md b/include/xrpl/beast/container/aged_unordered_multiset.h.ai.md new file mode 100644 index 0000000000..0043a0460d --- /dev/null +++ b/include/xrpl/beast/container/aged_unordered_multiset.h.ai.md @@ -0,0 +1,46 @@ +# `aged_unordered_multiset.h` + +This file defines `beast::aged_unordered_multiset` as a one-line template alias within the `beast` namespace. The entire substance lives in a single `using` declaration that wires the five public template parameters into the shared `detail::aged_unordered_container` engine. + +## Role in the Container Family + +The `beast` container directory provides eight "aged" associative containers, each a thin alias over one of two internal engines: + +| Alias | `IsMulti` | `IsMap` | Engine | +|---|---|---|---| +| `aged_unordered_set` | `false` | `false` | `aged_unordered_container` | +| **`aged_unordered_multiset`** | **`true`** | **`false`** | **`aged_unordered_container`** | +| `aged_unordered_map` | `false` | `true` | `aged_unordered_container` | +| `aged_unordered_multimap` | `true` | `true` | `aged_unordered_container` | + +`aged_unordered_multiset` differs from `aged_unordered_set` solely by passing `IsMulti = true`, which routes the internal `boost::intrusive::unordered_set` to `boost::intrusive::unordered_multiset`, permitting duplicate keys. The `IsMap = false` flag collapses the `value_type` to `Key` alone (rather than `std::pair`), and the `T` slot is left as `void`. + +## Template Parameters + +```cpp +template < + class Key, + class Clock = std::chrono::steady_clock, + class Hash = std::hash, + class KeyEqual = std::equal_to, + class Allocator = std::allocator> +``` + +The `Clock` parameter is not used raw; the engine wraps it in `abstract_clock`, an XRPL-internal abstraction that allows test code to substitute a controllable mock clock. This is the primary reason the clock is a template parameter rather than a constructor argument — the clock policy must be baked into the type so that the `time_point` typedef is consistent throughout the container. + +## What the Underlying Engine Provides + +`detail::aged_unordered_container` is built on two `boost::intrusive` structures that share a single pool of heap-allocated `element` nodes: + +1. A `boost::intrusive::unordered_multiset` (because `IsMulti = true`) that handles O(1) average lookup, insertion, and erasure by key. +2. A `boost::intrusive::list` that maintains all elements in insertion/touch order. + +Each `element` node embeds both the intrusive hooks for the unordered set and the list, plus a `value` (the `Key` itself for a set) and a `time_point when`. The dual-structure design means the chronological list costs no extra allocation — the list links are stored inside the same node used by the hash table. + +The container surfaces a `chronological` memberspace exposing begin/end iterators over the list, enabling callers to walk elements from oldest to newest (or in reverse). This makes `aged_unordered_multiset` a natural primitive for time-bounded caches and expiry queues: insertion order is preserved for free, and `touch()` moves an element to the tail of the chronological list, providing LRU semantics without a separate data structure. + +## Design Rationale + +The alias-over-engine pattern keeps the public API surface minimal. Because the only difference between the four unordered containers is a pair of `bool` template flags, all correctness fixes, performance tuning, and feature additions are automatically shared. The alternative — four separate class templates — would require maintaining four near-identical implementations. The cost is that the engine's template signature is slightly opaque (`IsMulti`, `IsMap`, `Key`, `T`, `Clock`, ...), which is hidden from users behind the clean four-parameter alias. + +The `T = void` slot for set variants is structurally unused; the engine uses `std::conditional, Key>` to compute `value_type`, so `void` never materialises in a real type. \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_unordered_set.h.ai.json b/include/xrpl/beast/container/aged_unordered_set.h.ai.json new file mode 100644 index 0000000000..0212c42851 --- /dev/null +++ b/include/xrpl/beast/container/aged_unordered_set.h.ai.json @@ -0,0 +1,35 @@ +{ + "args": [ + { + "lineno": 10, + "name": "Key" + }, + { + "lineno": 11, + "name": "Clock" + }, + { + "lineno": 12, + "name": "Hash" + }, + { + "lineno": 13, + "name": "KeyEqual" + }, + { + "lineno": 14, + "name": "Allocator" + } + ], + "classes": [], + "description": "Defines a type alias 'aged_unordered_set' in the 'beast' namespace, which is a specialization of a generic aged_unordered_container for storing unique keys with associated age information, using customizable hash, equality, clock, and allocator types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/aged_unordered_set.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/aged_unordered_set.h.ai.md b/include/xrpl/beast/container/aged_unordered_set.h.ai.md new file mode 100644 index 0000000000..7a9134b05a --- /dev/null +++ b/include/xrpl/beast/container/aged_unordered_set.h.ai.md @@ -0,0 +1,38 @@ +# `aged_unordered_set.h` + +## Role and Purpose + +This header provides `beast::aged_unordered_set`, a thin type alias that composes the generic `detail::aged_unordered_container` into a familiar `std::unordered_set`-like interface enriched with timestamp tracking. It exists to give XRPL components a standard-looking container for building time-aware caches — structures where entries expire after a configurable age rather than being evicted by capacity alone. + +The file itself is only 19 lines; all logic lives in `detail/aged_unordered_container.h`. + +## The Two-Index Architecture + +Every element in the container participates simultaneously in two Boost.Intrusive data structures: a `boost::intrusive::unordered_set` (or `unordered_multiset`) for O(1) hash-based key lookup, and a `boost::intrusive::list` that maintains insertion/touch order. Because Boost.Intrusive stores the hook pointers directly inside the `element` struct, there is no per-element allocation overhead beyond the element itself — both indexes share the same heap nodes. + +This dual-membership is the central design decision. An ordinary `std::unordered_set` plus a side `std::list` would require coordinated allocations and pointer synchronization; the intrusive approach makes insertion, erasure, and `touch()` all atomic with respect to both indexes at once. + +## Template Parameters + +```cpp +template < + class Key, + class Clock = std::chrono::steady_clock, + class Hash = std::hash, + class KeyEqual = std::equal_to, + class Allocator = std::allocator> +using aged_unordered_set = detail::aged_unordered_container< + false, false, Key, void, Clock, Hash, KeyEqual, Allocator>; +``` + +The two leading booleans hard-wire `IsMulti=false` (unique keys, no duplicates) and `IsMap=false` (set semantics — `value_type` is `Key` alone, not a `pair`). The `void` passed for `T` is vestigial; `aged_unordered_map` passes a real mapped type in that slot. The `Clock` parameter is not used to construct a default clock — the container holds an `abstract_clock&` reference that callers must supply, keeping clock behaviour injectable for tests. + +## Timestamp Semantics and Expiry + +On insertion the container stamps the new element with `clock().now()`. The `touch(key)` method — absent from standard containers — looks up the element, records the current time, and moves it to the back of the chronological list in O(1) time. This implements LRU-style refresh semantics without a separate data structure. + +The companion free function `expire(container, age)` in `aged_container_utility.h` iterates `chronological.cbegin()` forward, erasing elements whose `when` timestamp is older than `(now - age)`. Because the list is ordered oldest-first, the loop can terminate as soon as it finds a live entry, making bulk expiry proportional to the number of expired items rather than the total container size. + +## Relationship to Sibling Types + +`aged_unordered_multiset` is the identical alias with `IsMulti=true`, allowing duplicate keys. `aged_unordered_map` and `aged_unordered_multimap` swap `IsMap` to `true` and supply a real `T`. The ordered variants (`aged_set`, `aged_map`) delegate to `detail::aged_ordered_container` instead, which uses `boost::intrusive::set` for key lookup while sharing the same chronological-list pattern. \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/aged_associative_container.h.ai.json b/include/xrpl/beast/container/detail/aged_associative_container.h.ai.json new file mode 100644 index 0000000000..a84cf9fcc7 --- /dev/null +++ b/include/xrpl/beast/container/detail/aged_associative_container.h.ai.json @@ -0,0 +1,61 @@ +{ + "args": [ + { + "lineno": 6, + "name": "maybe_map" + }, + { + "lineno": 10, + "name": "Value" + }, + { + "lineno": 23, + "name": "Value" + } + ], + "classes": [ + { + "args": [ + "aged_associative_container_extract_t()" + ], + "lineno": 6, + "name": "aged_associative_container_extract_t" + }, + { + "args": [ + "aged_associative_container_extract_t()" + ], + "lineno": 19, + "name": "aged_associative_container_extract_t" + } + ], + "description": "Provides templates for extracting the key or value from elements in an associative container, with specialization for map-like and non-map-like containers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/detail/aged_associative_container.h", + "functions": [ + { + "args": [ + "Value const& value" + ], + "lineno": 10, + "name": "operator()" + }, + { + "args": [ + "Value const& value" + ], + "lineno": 23, + "name": "operator()" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "beast" + }, + { + "lineno": 4, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/aged_associative_container.h.ai.md b/include/xrpl/beast/container/detail/aged_associative_container.h.ai.md new file mode 100644 index 0000000000..48c46b2720 --- /dev/null +++ b/include/xrpl/beast/container/detail/aged_associative_container.h.ai.md @@ -0,0 +1,19 @@ +## `aged_associative_container_extract_t` + +This small header solves a single, focused problem: how to uniformly extract a key from a stored element when the container may be either map-like (where elements are `std::pair`) or set-like (where elements are the key itself). + +Both `aged_ordered_container` and `aged_unordered_container` are unified templates parameterised by a boolean `IsMap` flag. Internally they store a single `value_type` that is either a `std::pair` or bare `Key`, depending on that flag. In both containers, the private `extract()` static method needs to pull the key out of whatever is stored — for comparisons in the ordered case and for hashing/equality in the unordered case. Rather than duplicating that conditional logic or scattering `if constexpr` branches inside two large container bodies, the dispatch is centralised here into a tiny stateless functor. + +The primary template, `aged_associative_container_extract_t` (the `maybe_map` case), implements `operator()` by returning `value.first` — the key half of a `std::pair`. The full specialisation for `false` is the set case: `operator()` returns the whole value unchanged, since the element *is* the key. The call sites in both container headers are identical: + +```cpp +return aged_associative_container_extract_t()(value); +``` + +Because `IsMap` is already a compile-time constant in those templates, the compiler selects the correct specialisation at instantiation time with zero runtime cost. + +The template parameter is named `maybe_map` rather than `is_map` because it represents a conservative, "this might be a map" interpretation: the primary template handles the map path, and the specialisation handles the non-map (set) path. This naming also signals that the boolean is not a perfect semantic guarantee about the container type — it's a dispatch hint. + +The `explicit` default constructor on both specialisations is a deliberate defensiveness choice. It prevents implicit construction while still allowing straightforward instantiation, which is important for a stateless functor that may be stored inside larger containers using empty-base optimisation (the sibling `empty_base_optimization.h` in the same directory exists for exactly this purpose). + +The `operator()` in both variants is templated on `Value` rather than fixed to a concrete type. This keeps the extractor generic enough to work with any pair-like or key-identity element without requiring the container to bind the exact stored type at the extractor's own instantiation point — the container instantiates the functor first and passes the value later. \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/aged_container_iterator.h.ai.json b/include/xrpl/beast/container/detail/aged_container_iterator.h.ai.json new file mode 100644 index 0000000000..90d22a1c54 --- /dev/null +++ b/include/xrpl/beast/container/detail/aged_container_iterator.h.ai.json @@ -0,0 +1,138 @@ +{ + "args": [ + { + "lineno": 13, + "name": "is_const" + }, + { + "lineno": 13, + "name": "Iterator" + }, + { + "lineno": 32, + "name": "other_is_const" + }, + { + "lineno": 32, + "name": "OtherIterator" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "aged_ordered_container" + }, + { + "args": [], + "lineno": 13, + "name": "aged_container_iterator" + } + ], + "description": "This file defines the aged_container_iterator template class, which provides an iterator for aged containers in the Beast library, supporting both const and non-const access and tracking the time point associated with each element. It is used internally by aged_ordered_container and potentially aged_unordered_container.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/detail/aged_container_iterator.h", + "functions": [ + { + "args": [], + "lineno": 19, + "name": "aged_container_iterator" + }, + { + "args": [ + "aged_container_iterator const& other" + ], + "lineno": 32, + "name": "aged_container_iterator" + }, + { + "args": [ + "aged_container_iterator const& other" + ], + "lineno": 41, + "name": "aged_container_iterator" + }, + { + "args": [ + "aged_container_iterator const& other" + ], + "lineno": 49, + "name": "operator=" + }, + { + "args": [ + "aged_container_iterator const& other" + ], + "lineno": 57, + "name": "operator==" + }, + { + "args": [ + "aged_container_iterator const& other" + ], + "lineno": 63, + "name": "operator!=" + }, + { + "args": [], + "lineno": 69, + "name": "operator++" + }, + { + "args": [ + "int" + ], + "lineno": 75, + "name": "operator++" + }, + { + "args": [], + "lineno": 82, + "name": "operator--" + }, + { + "args": [ + "int" + ], + "lineno": 88, + "name": "operator--" + }, + { + "args": [], + "lineno": 95, + "name": "operator*" + }, + { + "args": [], + "lineno": 101, + "name": "operator->" + }, + { + "args": [], + "lineno": 107, + "name": "when" + }, + { + "args": [ + "OtherIterator const& iter" + ], + "lineno": 120, + "name": "aged_container_iterator" + }, + { + "args": [], + "lineno": 124, + "name": "iterator" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "beast" + }, + { + "lineno": 10, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/aged_container_iterator.h.ai.md b/include/xrpl/beast/container/detail/aged_container_iterator.h.ai.md new file mode 100644 index 0000000000..0e263d9a39 --- /dev/null +++ b/include/xrpl/beast/container/detail/aged_container_iterator.h.ai.md @@ -0,0 +1,56 @@ +# `aged_container_iterator.h` — Iterator Adapter for Time-Aware Containers + +## Role and Purpose + +This file defines the `aged_container_iterator` template in `beast::detail`, the single iterator type shared by all of beast's aged container family (`aged_ordered_container` and `aged_unordered_container`). Its job is to sit in front of a Boost.Intrusive iterator and translate what the underlying intrusive data structure exposes into what a user of an `aged_set`, `aged_map`, or their unordered equivalents expects to see. + +Each element in an aged container is stored as an internal `element` struct containing both a `value` (the user-visible key or key–value pair) and a `when` timestamp. `aged_container_iterator` strips away this implementation detail: dereferencing the iterator yields only `value`, while the timestamp remains accessible through the dedicated `when()` method. This separation keeps the iterator compliant with the standard container model while still supporting the core feature that distinguishes aged containers from their standard-library counterparts. + +## The `stashed` Indirection Trick + +A subtle design challenge arises because the iterator must resolve `value_type` and `time_point` without being able to see the full container class declaration — the container is a complex seven-parameter template, and the iterator lives in a separate `detail` header. The solution is the nested `stashed` struct inside `element`: + +```cpp +struct stashed { + using value_type = typename aged_ordered_container::value_type; + using time_point = typename aged_ordered_container::time_point; +}; +``` + +`aged_container_iterator` harvests both types through `Iterator::value_type::stashed::*` without needing to `#include` the container definition. This is not a runtime mechanism — `stashed` has no data members, only type aliases — but it cleanly threads the required type information through the Boost.Intrusive iterator's own `value_type` trait, solving the circular-include problem entirely in the type system. + +## Const-Correctness Enforcement + +The `is_const` boolean template parameter drives a `std::conditional` that selects `value_type const` or `value_type` as the reference and pointer targets. Callers cannot simply assign a `const_iterator` to an `iterator`; the constructors and assignment operator all use `std::enable_if` to reject `other_is_const == true && is_const == false` combinations at compile time. + +There are two distinct conversion constructors because two situations arise: + +1. **Same `Iterator`, different `is_const`** — handled implicitly (the non-explicit constructor at line 48). This is the usual non-const-to-const promotion that mirrors `std::vector::iterator` → `std::vector::const_iterator`. +2. **Different `Iterator` type** (e.g., forward iterator vs. reverse iterator) — handled by an `explicit` constructor (line 38) so that reverse-to-forward conversion cannot happen accidentally. `std::is_same::value == false` is required in the `enable_if`, preventing the explicit constructor from shadowing the implicit one when the iterator types are identical. + +This two-constructor scheme means you can write `const_iterator ci = iter;` naturally, but converting a `reverse_iterator` to a plain `iterator` requires an explicit cast — matching the semantics of `std::reverse_iterator`. + +## The `!IsMap` Convention + +The containers instantiate the iterator type with a slightly counter-intuitive convention: + +```cpp +// In aged_ordered_container: +using iterator = aged_container_iterator; +``` + +For a **set** (`IsMap = false`), `is_const = true`: the element type is the key itself, so it must be immutable. For a **map** (`IsMap = true`), `is_const = false`: `value_type` is `pair`, the key is already const inside the pair, and the mapped value `T` remains mutable through `iterator`. `const_iterator` is always `is_const = true` regardless. + +## Temporal Access via `when()` + +The `when()` method returns a `const` reference to `m_iter->when` — the `time_point` recorded when the element was inserted or last `touch()`-ed. This is the axis that makes aged containers useful for LRU caches, expiry queues, and rate-limited tables: code can traverse `chronological::begin()` → `chronological::end()` and evict elements whose `when()` is older than a threshold, without any external bookkeeping structure. + +## Friend Boundaries and the Private Constructor + +The primary constructor from a raw `OtherIterator` (line 136) and the `iterator()` accessor (line 142) are both `private`, friend-gated to `aged_ordered_container` and `aged_unordered_container`. This means only the container itself can wrap an intrusive iterator into a public-facing `aged_container_iterator`, and only the container can later unwrap one (needed for `erase()` and `touch()` implementations that must pass the raw intrusive iterator back into Boost.Intrusive's API). User code is never exposed to the underlying `element` layout. + +The `friend class aged_container_iterator` declaration lets any instantiation of the iterator access `m_iter` of any other instantiation, which is necessary for the cross-`is_const` conversion constructors and comparison operators to work without a public getter. + +## Iterator Category and Bidirectionality + +`iterator_category` is inherited directly from `std::iterator_traits`. Because `cont_type` is a Boost.Intrusive set or list (both of which provide bidirectional iterators), all four iterator flavors — `iterator`, `const_iterator`, `reverse_iterator`, and `const_reverse_iterator` — are bidirectional. The chronological list iterators also satisfy bidirectionality, enabling `rbegin()`/`rend()` traversal from newest to oldest element without any additional cost. \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h.ai.json b/include/xrpl/beast/container/detail/aged_ordered_container.h.ai.json new file mode 100644 index 0000000000..a575647034 --- /dev/null +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h.ai.json @@ -0,0 +1,117 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "()" + ], + "lineno": 15, + "name": "is_boost_reverse_iterator" + }, + { + "args": [ + "()" + ], + "lineno": 20, + "name": "is_boost_reverse_iterator>" + }, + { + "args": [ + "clock_type& clock", + "clock_type& clock, Compare const& comp", + "clock_type& clock, Allocator const& alloc", + "clock_type& clock, Compare const& comp, Allocator const& alloc", + "InputIt first, InputIt last, clock_type& clock", + "InputIt first, InputIt last, clock_type& clock, Compare const& comp", + "InputIt first, InputIt last, clock_type& clock, Allocator const& alloc", + "InputIt first, InputIt last, clock_type& clock, Compare const& comp, Allocator const& alloc", + "aged_ordered_container const& other", + "aged_ordered_container const& other, Allocator const& alloc", + "aged_ordered_container&& other", + "aged_ordered_container&& other, Allocator const& alloc", + "std::initializer_list init, clock_type& clock", + "std::initializer_list init, clock_type& clock, Compare const& comp", + "std::initializer_list init, clock_type& clock, Allocator const& alloc", + "std::initializer_list init, clock_type& clock, Compare const& comp, Allocator const& alloc" + ], + "lineno": 29, + "name": "aged_ordered_container" + }, + { + "args": [ + "time_point const& when_, value_type const& value_", + "time_point const& when_, value_type&& value_", + "time_point const& when_, Args&&... args" + ], + "lineno": 61, + "name": "element" + }, + { + "args": [ + "()", + "(pair_value_compare const& other)", + "(Compare const& compare)" + ], + "lineno": 87, + "name": "pair_value_compare" + }, + { + "args": [ + "()", + "(Compare const& compare)" + ], + "lineno": 108, + "name": "KeyValueCompare" + }, + { + "args": [ + "(clock_type& clock_)", + "(clock_type& clock_, Compare const& comp)", + "(clock_type& clock_, Allocator const& alloc_)", + "(clock_type& clock_, Compare const& comp, Allocator const& alloc_)", + "(config_t const& other)", + "(config_t const& other, Allocator const& alloc)", + "(config_t&& other)", + "(config_t&& other, Allocator const& alloc)" + ], + "lineno": 154, + "name": "config_t" + }, + { + "args": [], + "lineno": 246, + "name": "chronological_t" + } + ], + "description": "This file implements an associative container (ordered set/map and their multi variants) where each element is also indexed by time, allowing for time-based operations such as expiration and chronological traversal. It is designed for use cases like caches with expiring items and provides an interface similar to standard library containers, with additional time-aware features.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/detail/aged_ordered_container.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 1082, + "name": "swap" + }, + { + "args": [ + "c", + "age" + ], + "lineno": 1092, + "name": "expire" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "beast" + }, + { + "lineno": 13, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/aged_ordered_container.h.ai.md b/include/xrpl/beast/container/detail/aged_ordered_container.h.ai.md new file mode 100644 index 0000000000..1e3beb9c66 --- /dev/null +++ b/include/xrpl/beast/container/detail/aged_ordered_container.h.ai.md @@ -0,0 +1,79 @@ +# `aged_ordered_container.h` — Time-Indexed Associative Container + +## Role and Purpose + +`aged_ordered_container` is the single backing implementation for the four public type aliases in the beast container library: `aged_map`, `aged_set`, `aged_multimap`, and `aged_multiset`. It lives in `beast::detail` and is not used directly — callers use the thin alias headers (e.g., `aged_map.h`) which forward all template parameters to this class. + +The container solves a problem that appears repeatedly in the XRPL node implementation: you need an associative lookup structure (find by key in O(log n)) while simultaneously being able to enumerate elements in the order they were last accessed (oldest first), so that expiration and cache eviction can be implemented without scanning the entire container. A plain `std::map` gives you the first axis; a plain `std::list` gives you the second; `aged_ordered_container` fuses both at the cost of a single extra pointer pair per element and no additional heap allocation. + +## Dual-Index Architecture with Boost.Intrusive + +The core design insight is that every `element` node participates in **two** independent intrusive data structures simultaneously. `element` inherits both `boost::intrusive::set_base_hook` and `boost::intrusive::list_base_hook`, embedding the BST node links and the doubly-linked-list node links directly into the same heap allocation: + +```cpp +struct element : boost::intrusive::set_base_hook<...>, + boost::intrusive::list_base_hook<...> { + value_type value; + time_point when; +}; +``` + +`m_cont` is a `boost::intrusive::set` (or `multiset`) that provides key-ordered access and O(log n) lookup. `chronological.list` is a `boost::intrusive::list` that holds the same nodes in insertion/touch order. Because both containers hold pointers into the same `element` objects, there is no duplication of the user data — the two indexes are pure overhead (four pointers per element: BST left/right/parent and list prev/next, collapsed by the intrusive base hooks into the element itself). + +## The Chronological "Memberspace" + +The `chronological_t` class is an instance of the *memberspace* idiom (cited inline, referencing an ACCU article). Rather than a free namespace or a base class, it is a non-copyable member object whose only purpose is to expose a distinct set of `begin()`/`end()`/`rbegin()`/`rend()` iterators over `chronological.list`. Code iterating elements from oldest to newest writes `container.chronological.begin()`, keeping both traversal dimensions syntactically visible on a single container object. + +`chronological_t` is non-copyable and non-moveable, and is always a subobject of `aged_ordered_container` — the list it wraps (`list` member) is declared `mutable` so `const` iterators can still be obtained on a `const` container. + +## `touch()` — Updating the Time Index + +`touch(pos)` updates an element's `when` timestamp to `clock().now()`, then **unlinks the node from the chronological list and re-inserts it at the back**. The key-ordered index (`m_cont`) is unaffected. This O(1) operation moves any accessed element to the "newest" end of the list, making the front of the list permanently the oldest element. The public `touch(K const& k)` overload finds all matching elements via `equal_range` and calls the iterator form for each. + +Both `touch` and `erase` use `is_boost_reverse_iterator` to statically reject reverse iterators through SFINAE: calling `erase(reverse_iterator)` is a compile error, not a runtime error. Boost.Intrusive's reverse iterators carry different hook types, and this trait detects them. + +## `expire()` — Bulk Expiration + +The free function `expire(c, age)` exploits the chronological ordering directly: + +```cpp +auto const expired(c.clock().now() - age); +for (auto iter(c.chronological.cbegin()); + iter != c.chronological.cend() && iter.when() <= expired;) + iter = c.erase(iter); +``` + +Because elements are ordered oldest-first in the list, the function only ever touches elements it removes. Once it encounters an element newer than the expiry threshold, the loop terminates in O(k) where k is the number of expired items — not O(n) over the whole container. The `aged_container_iterator` wrapper exposes `when()` directly on the iterator, which reads `m_iter->when` without an extra lookup. + +## `element` Memory and `iterator_to` + +`new_element` allocates a single `element` via `ElementAllocatorTraits::allocate`, constructs it with the current `clock().now()` timestamp, and uses a `unique_ptr` with a custom `Deleter` to guarantee the allocation is freed on exception. `delete_element` destroys and deallocates in reverse order. + +`iterator_to(value_type&)` converts a reference to the user-visible `value` field back into a full `element*` via manual pointer arithmetic: + +```cpp +reinterpret_cast(reinterpret_cast(&value) - + ((std::size_t)std::addressof(((element*)0)->member))); +``` + +A `static_assert(std::is_standard_layout::value)` guards this operation. Because `element` inherits from two hook base classes before declaring `value`, the offset of `value` is not zero and the arithmetic is necessary. This same pattern appears in both the key-ordered and chronological `iterator_to` overloads. + +## `config_t` — Bundled Allocator, Comparator, Clock + +Rather than storing the comparator, allocator, and clock as three separate fields, the private `config_t` class packs them together using two optimizations: + +1. It inherits (privately) from `KeyValueCompare`, so a stateless comparator (the common case) occupies zero bytes via the empty base. +2. It inherits from `empty_base_optimization`, which is itself a conditional private base/member depending on whether the allocator is empty and non-final. This is the same pattern used throughout Boost containers. +3. The clock is held as a `std::reference_wrapper`, ensuring the container never copies the clock object and always goes through the abstract interface. + +## `emplace` Upfront Construction Cost + +For non-multi containers, `emplace` must construct the candidate `element` before checking uniqueness in `m_cont`, because the key is buried inside the constructed value and there is no way to extract it without construction. If `insert_check` reports that the key already exists, the freshly constructed element is immediately destroyed via `delete_element`. The comment marks this with `VFALCO NOTE Its unfortunate that we need to construct element here`. The multi-container variant avoids this waste since duplicate keys are always accepted. + +## Copy vs. Move Construction + +The copy constructor re-inserts all elements through the normal `insert` path, stamping each with the current clock time rather than preserving the original timestamps — there is no way to copy the `time_point` values. The move constructor transfers `m_cont` and `chronological.list` directly via intrusive container move semantics, preserving timestamps intact. The unusual case — move construction with a differing allocator — falls back to element-by-element insertion followed by `other.clear()`, since the underlying nodes cannot be transferred across allocators. + +## Public Interface Relationship + +The equality operator notes explicitly that comparison is done only on keys, ignoring mapped values. This differs from `std::map::operator==` which compares both. The specialization of `is_aged_container` at file scope tags `aged_ordered_container` for trait-based dispatch elsewhere in the XRPL codebase. \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h.ai.json b/include/xrpl/beast/container/detail/aged_unordered_container.h.ai.json new file mode 100644 index 0000000000..1a03e740a5 --- /dev/null +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h.ai.json @@ -0,0 +1,126 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "clock_type& clock", + "clock_type& clock, Hash const& hash", + "clock_type& clock, KeyEqual const& key_eq", + "clock_type& clock, Allocator const& alloc", + "clock_type& clock, Hash const& hash, KeyEqual const& key_eq", + "clock_type& clock, Hash const& hash, Allocator const& alloc", + "clock_type& clock, KeyEqual const& key_eq, Allocator const& alloc", + "clock_type& clock, Hash const& hash, KeyEqual const& key_eq, Allocator const& alloc", + "InputIt first, InputIt last, clock_type& clock", + "InputIt first, InputIt last, clock_type& clock, Hash const& hash", + "InputIt first, InputIt last, clock_type& clock, KeyEqual const& key_eq", + "InputIt first, InputIt last, clock_type& clock, Allocator const& alloc", + "InputIt first, InputIt last, clock_type& clock, Hash const& hash, KeyEqual const& key_eq", + "InputIt first, InputIt last, clock_type& clock, Hash const& hash, Allocator const& alloc", + "InputIt first, InputIt last, clock_type& clock, KeyEqual const& key_eq, Allocator const& alloc", + "InputIt first, InputIt last, clock_type& clock, Hash const& hash, KeyEqual const& key_eq, Allocator const& alloc", + "aged_unordered_container const& other", + "aged_unordered_container const& other, Allocator const& alloc", + "aged_unordered_container&& other", + "aged_unordered_container&& other, Allocator const& alloc", + "std::initializer_list init, clock_type& clock", + "std::initializer_list init, clock_type& clock, Hash const& hash", + "std::initializer_list init, clock_type& clock, KeyEqual const& key_eq", + "std::initializer_list init, clock_type& clock, Allocator const& alloc", + "std::initializer_list init, clock_type& clock, Hash const& hash, KeyEqual const& key_eq", + "std::initializer_list init, clock_type& clock, Hash const& hash, Allocator const& alloc", + "std::initializer_list init, clock_type& clock, KeyEqual const& key_eq, Allocator const& alloc", + "std::initializer_list init, clock_type& clock, Hash const& hash, KeyEqual const& key_eq, Allocator const& alloc" + ], + "lineno": 38, + "name": "aged_unordered_container" + }, + { + "args": [ + "time_point const& when_, value_type const& value_", + "time_point const& when_, value_type&& value_", + "time_point const& when_, Args&&... args" + ], + "lineno": 70, + "name": "element" + }, + { + "args": [ + "()", + "(Hash const& h)" + ], + "lineno": 97, + "name": "ValueHash" + }, + { + "args": [ + "()", + "(KeyEqual const& keyEqual)" + ], + "lineno": 120, + "name": "KeyValueEqual" + }, + { + "args": [ + "clock_type& clock_", + "clock_type& clock_, Hash const& hash", + "clock_type& clock_, KeyEqual const& keyEqual", + "clock_type& clock_, Allocator const& alloc_", + "clock_type& clock_, Hash const& hash, KeyEqual const& keyEqual", + "clock_type& clock_, Hash const& hash, Allocator const& alloc_", + "clock_type& clock_, KeyEqual const& keyEqual, Allocator const& alloc_", + "clock_type& clock_, Hash const& hash, KeyEqual const& keyEqual, Allocator const& alloc_", + "config_t const& other", + "config_t const& other, Allocator const& alloc", + "config_t&& other", + "config_t&& other, Allocator const& alloc" + ], + "lineno": 154, + "name": "config_t" + }, + { + "args": [ + "()", + "(Allocator const& alloc)" + ], + "lineno": 246, + "name": "Buckets" + }, + { + "args": [], + "lineno": 326, + "name": "chronological_t" + } + ], + "description": "This file implements an associative container (aged_unordered_container) where each element is indexed by both key and time, supporting expiration and cache-like behavior. It mirrors the interface of standard unordered containers, with additional chronological traversal and time-based operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/detail/aged_unordered_container.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 1207, + "name": "swap" + }, + { + "args": [ + "c", + "age" + ], + "lineno": 1222, + "name": "expire" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 27, + "name": "beast" + }, + { + "lineno": 28, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/aged_unordered_container.h.ai.md b/include/xrpl/beast/container/detail/aged_unordered_container.h.ai.md new file mode 100644 index 0000000000..00e5fab9c4 --- /dev/null +++ b/include/xrpl/beast/container/detail/aged_unordered_container.h.ai.md @@ -0,0 +1,86 @@ +# `aged_unordered_container.h` — Time-Indexed Hash Container + +## Role in the System + +This file provides `aged_unordered_container`, the implementation backbone for all four of XRPL's beast hash-based aged containers: `aged_unordered_set`, `aged_unordered_multiset`, `aged_unordered_map`, and `aged_unordered_multimap`. Its purpose is to give callers the O(1) lookup semantics of a standard `std::unordered_map` while simultaneously maintaining every element's insertion or last-touch timestamp in chronological order. This combination is the fundamental primitive for building time-bounded caches in the XRP Ledger — entries can be inserted, found by key in constant time, and expired in bulk by walking the chronological front of the container. + +The template signature encodes four orthogonal degrees of freedom: + +```cpp +template +class aged_unordered_container +``` + +`IsMulti` and `IsMap` toggle whether duplicate keys are permitted and whether the container is a map or set respectively, and `std::enable_if` guards on these flags select the correct overload family for every public mutating operation. The actual concrete-type aliases (`aged_unordered_map`, etc.) are thin wrappers that fix these two flags. + +## Dual-Index Architecture + +The central design decision is to use **two Boost.Intrusive containers simultaneously over the same pool of elements**. The private `element` struct multiply-inherits from both `boost::intrusive::unordered_set_base_hook` and `boost::intrusive::list_base_hook`: + +```cpp +struct element : boost::intrusive::unordered_set_base_hook<...>, + boost::intrusive::list_base_hook<...> +{ + value_type value; + time_point when; +}; +``` + +This single node participates in two separate intrusive data structures without any extra allocation. The `m_cont` member is the hash container (unordered_set or unordered_multiset depending on `IsMulti`) used for O(1) key-based lookup, while `chronological.list` is an intrusive doubly-linked list kept in insertion/touch order. Every insert pushes the new element to the back of `chronological.list`; every `touch()` unlinks the element and splices it back onto the tail. The invariant is that `chronological.list.begin()` always points to the oldest element — exactly what the `expire()` free function exploits. + +This architecture is far more efficient than maintaining a separate side-structure because there is zero extra heap allocation per element and zero pointer-indirection overhead; the hash container and the list share the exact same `element` objects. + +## The `element` Node and Key Extraction + +`element` stashes its container-level type aliases in a nested `stashed` struct. This is a deliberate forward-declaration trick: `aged_container_iterator` needs `value_type` and `time_point` without seeing the full container class, so it picks them up from `Iterator::value_type::stashed`. This breaks the mutual dependency between iterator and container template instantiations. + +The `aged_associative_container_extract_t` functor provides the `extract()` helper. For maps it returns `value.first`; for sets it returns the value itself. This keeps the hash and equality functors uniform regardless of whether the contained type is a bare key or a key-value pair. + +## Policy Consolidation: `config_t` and `empty_base_optimization` + +All per-container policies — `ValueHash`, `KeyValueEqual`, and the element allocator — are packed into a single `config_t` object using private inheritance and `empty_base_optimization`. When these policy types (the common `std::hash`, `std::equal_to`, and `std::allocator`) are stateless, the compiler can apply the empty base optimization and `config_t` carries zero bytes of overhead for them. + +`ValueHash` wraps the user-supplied `Hash` so that Boost.Intrusive, which operates on `element` nodes, can hash by extracting the key first. Similarly, `KeyValueEqual` wraps `KeyEqual` and provides three overloads — `(Key, element)`, `(element, Key)`, and `(element, element)` — so that Boost.Intrusive's heterogeneous lookup paths (`find`, `insert_check`, `count`) can compare a raw key against an element without constructing a temporary `element`. + +The clock is stored as `std::reference_wrapper` inside `config_t`, reflecting that the container never owns the clock and the clock outlives the container. Using `abstract_clock` rather than `Clock` directly means the container's clock can be replaced with a mock during unit tests — a dependency injection seam baked into the type system. + +## Bucket Management and Rehashing + +The `Buckets` inner class owns a `std::vector` and the `max_load_factor` float. Boost.Intrusive's unordered containers require an external array of bucket objects supplied at construction time via `bucket_traits`. When the vector needs to grow, `Buckets::rehash()` must take care with the ordering of operations: if the vector's capacity is insufficient, it first swaps in a fresh vector to avoid reusing still-live bucket storage, then calls `c.rehash()` on the intrusive container, then resizes. When shrinking, the sequence is reversed: intrusive rehash first, vector resize second, again to avoid destroying non-empty buckets. + +`maybe_rehash(n)` is called before every insert. It checks `would_exceed`, i.e., whether adding `n` items would push the load factor beyond the maximum, and if so resizes to accommodate `size() + n`. Bulk inserts from a pair of random-access iterators pre-batch the load check by measuring the distance in advance and calling `maybe_rehash` once, then delegating to `insert_unchecked` which skips the per-element check. + +## Chronological Memberspace + +The public `chronological` member of type `chronological_t` is a "memberspace" — an idiom for giving a class an inner namespace-like scope with its own iterator types and `begin`/`end`/`rbegin`/`rend`. The list backing `chronological` is declared `mutable` because `const` container operations still need to read it via const references. + +`chronological_t::iterator_to(value_type&)` is noteworthy: it recovers the enclosing `element*` from a raw `value_type&` via pointer arithmetic using `offsetof`-style calculation with `addressof`. This is sound only because `element` is guaranteed to be standard-layout, enforced at the call site by `static_assert(std::is_standard_layout::value, ...)`. The same `iterator_to` pattern appears on the main `m_cont` side. + +## `touch()` and the `expire()` Free Function + +`touch(pos)` resets an element's `when` to `clock().now()`, unlinks it from `chronological.list`, and splices it to the tail. This O(1) operation is the core of any LRU cache policy built on top of this container. `touch(key)` calls `equal_range` and touches all matching elements, returning the count; for a multimap use-case this lets all elements sharing a key be refreshed in one call. + +The file-scope free function `expire(c, age)` is the canonical expiry loop: + +```cpp +auto const expired(c.clock().now() - age); +for (auto iter(c.chronological.cbegin()); + iter != c.chronological.cend() && iter.when() <= expired;) +{ + iter = c.erase(iter); + ++n; +} +``` + +Because the list is maintained in chronological order and `touch` always moves elements to the tail, `cbegin()` always points at the oldest entry. The loop terminates as soon as it encounters an element younger than the cutoff, making bulk expiry O(k) where k is the number of expired elements — no full scan required. + +## Insert and Emplace Implementation + +For non-multi containers, insert uses Boost.Intrusive's two-phase `insert_check` / `insert_commit` protocol: first check whether the key already exists (and capture where to insert if not), then — only if absent — allocate and construct the element and commit it. This avoids an unnecessary allocation on duplicate keys. For multi containers the check is skipped and the element is always inserted. + +The `emplace` path for non-multi containers deviates from this in one place — the active `#if 1` block constructs the element before the uniqueness check and then deletes it if the key already exists. A commented-out `#else` block shows the `insert_check` / `insert_commit` alternative. The comment acknowledges this as unfortunate, and the dead code serves as a record of the considered-but-rejected alternative. + +## Comparison Operators + +`operator==` for non-multi containers iterates one container and checks membership in the other, deliberately comparing only keys (not `when` timestamps). The multi variant uses `equal_range` + `std::is_permutation` to handle repeated keys. The file guards against C++14's 4-iterator `is_permutation` via `BEAST_NO_CXX14_IS_PERMUTATION`, falling back to the 3-iterator version with a manual distance check as a workaround. \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/empty_base_optimization.h.ai.json b/include/xrpl/beast/container/detail/empty_base_optimization.h.ai.json new file mode 100644 index 0000000000..933c3d4f5f --- /dev/null +++ b/include/xrpl/beast/container/detail/empty_base_optimization.h.ai.json @@ -0,0 +1,102 @@ +{ + "args": [ + { + "lineno": 12, + "name": "T" + }, + { + "lineno": 16, + "name": "T" + }, + { + "lineno": 16, + "name": "UniqueID" + }, + { + "lineno": 16, + "name": "isDerived" + }, + { + "lineno": 25, + "name": "Arg1" + }, + { + "lineno": 25, + "name": "ArgN" + }, + { + "lineno": 43, + "name": "T" + }, + { + "lineno": 43, + "name": "UniqueID" + }, + { + "lineno": 56, + "name": "Arg1" + }, + { + "lineno": 56, + "name": "ArgN" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "is_empty_base_optimization_derived" + }, + { + "args": [ + "Arg1", + "ArgN" + ], + "lineno": 17, + "name": "empty_base_optimization" + }, + { + "args": [ + "Arg1", + "ArgN" + ], + "lineno": 44, + "name": "empty_base_optimization" + } + ], + "description": "Implements the empty base optimization utility for efficient storage of empty types, with specializations for when the type is or isn't suitable for EBO.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/container/detail/empty_base_optimization.h", + "functions": [ + { + "args": [], + "lineno": 27, + "name": "empty_base_optimization::member" + }, + { + "args": [], + "lineno": 32, + "name": "empty_base_optimization::member" + }, + { + "args": [], + "lineno": 62, + "name": "empty_base_optimization::member" + }, + { + "args": [], + "lineno": 67, + "name": "empty_base_optimization::member" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "beast" + }, + { + "lineno": 10, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/container/detail/empty_base_optimization.h.ai.md b/include/xrpl/beast/container/detail/empty_base_optimization.h.ai.md new file mode 100644 index 0000000000..4fd6d93355 --- /dev/null +++ b/include/xrpl/beast/container/detail/empty_base_optimization.h.ai.md @@ -0,0 +1,38 @@ +# `beast/container/detail/empty_base_optimization.h` + +## Purpose + +This header implements the *Empty Base Optimization* (EBO) wrapper pattern — a well-known C++ technique for eliminating the storage cost of stateless policy types. In standard C++, every object must have a unique address, so even a completely empty class consumes at least one byte when stored as a data member. But when an empty class is used as a *base*, the compiler is permitted to collapse its contribution to zero bytes. This file provides `empty_base_optimization` as a portable, uniform wrapper that exploits this rule when it is safe to do so and silently falls back to plain member storage when it is not. + +## The Eligibility Trait + +`is_empty_base_optimization_derived` encodes the two conditions that must hold for EBO to be applicable: + +1. `std::is_empty::value` — the type has no non-static data members and no virtual functions. +2. `!boost::is_final::value` — the type is not marked `final`, which would prevent inheritance outright. + +The `boost::is_final` check is the critical guard. A `final` class that is also empty cannot be used as a base, and the primary template would fail to compile without it. + +## Two Specializations via `isDerived` + +The template has three parameters: `T`, an integer `UniqueID` (default 0), and a computed boolean `isDerived`. Template partial specialization selects between two fundamentally different implementations: + +**EBO path (`isDerived = true`):** `empty_base_optimization` privately inherits from `T`. The `member()` accessor returns `*this` cast to `T&` — zero storage overhead when `T` is empty. All constructors forward their arguments directly to `T`'s constructor via perfect forwarding. + +**Fallback path (`isDerived = false`):** `empty_base_optimization` holds `T` as a plain data member `t_`. The `member()` accessor returns `t_`. This path is taken for non-empty types (which have state to store anyway), for `final` types (which cannot be inherited from), and for non-class types. + +The `member()` name provides a single uniform API so callers never need to know which path was taken. This is the core value of the abstraction. + +## The `UniqueID` Parameter + +The integer `UniqueID` exists to handle the case where a class must wrap the *same* empty type twice as a base. Without it, `class Foo : private empty_base_optimization, private empty_base_optimization` would produce a duplicate-base compilation error. Giving each instance a distinct `UniqueID` makes them different instantiations of the template and thus distinct base classes. In the current codebase only `UniqueID=0` (the default) is used, but the parameter leaves the door open for containers that might co-locate, say, a hash functor and an equal functor of the same stateless type. + +## Usage in the Aged Containers + +Both `aged_ordered_container` and `aged_unordered_container` inherit from `empty_base_optimization` inside their private `config_t` helper class, alongside direct private inheritance from policy types like `KeyValueCompare`, `ValueHash`, and `KeyValueEqual`. The `alloc()` method then delegates to `empty_base_optimization::member()`. + +In the overwhelmingly common case where users pass `std::allocator` — which is a stateless empty type — the allocator costs nothing in the `config_t` object. The `config_t` pattern deliberately aggregates the clock reference, comparator, hasher, equality predicate, and allocator into a single object so that the compiler can apply EBO across all of them simultaneously, a technique sometimes called the "compressed tuple" pattern. + +## Design Notes + +The file originates from Boost.Beast (Boost Software License), where the same EBO helper appears in many container implementations. The private inheritance (`class empty_base_optimization : private T`) intentionally hides `T`'s interface — callers use only `member()`, preventing accidental calls to methods of the wrapped policy object through the container's own public interface. This keeps the wrapper a pure storage mechanism with no behavioral leakage. \ No newline at end of file diff --git a/include/xrpl/beast/core/CurrentThreadName.h.ai.json b/include/xrpl/beast/core/CurrentThreadName.h.ai.json new file mode 100644 index 0000000000..87731fcdd9 --- /dev/null +++ b/include/xrpl/beast/core/CurrentThreadName.h.ai.json @@ -0,0 +1,43 @@ +{ + "args": [ + { + "lineno": 13, + "name": "newThreadName" + }, + { + "lineno": 29, + "name": "newThreadName" + } + ], + "classes": [], + "description": "Provides functions to set and get the name of the current thread, with special handling for Linux thread name length limits.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/core/CurrentThreadName.h", + "functions": [ + { + "args": [ + "newThreadName" + ], + "lineno": 13, + "name": "setCurrentThreadName" + }, + { + "args": [ + "newThreadName" + ], + "lineno": 29, + "name": "setCurrentThreadName" + }, + { + "args": [], + "lineno": 44, + "name": "getCurrentThreadName" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/core/CurrentThreadName.h.ai.md b/include/xrpl/beast/core/CurrentThreadName.h.ai.md new file mode 100644 index 0000000000..73b9f07b89 --- /dev/null +++ b/include/xrpl/beast/core/CurrentThreadName.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/beast/core/CurrentThreadName.h` + +This header, adapted from the JUCE framework, provides a small but operationally important facility for tagging OS threads with human-readable names. In a server process like `rippled` that runs dozens of concurrent threads — job queue workers, network I/O loops, database backends, ledger cleaners, gRPC handlers — meaningful thread names are essential for attaching debuggers, reading `top`/`htop`, and interpreting profiler output. + +## Design: Two-Layer Name Storage + +The implementation (in `CurrentThreadName.cpp`) maintains two parallel representations of the thread name: + +1. **An in-process `thread_local std::string`** (`detail::threadName`) that `getCurrentThreadName()` reads back. +2. **The OS-level thread name** set via platform-specific calls. + +The `thread_local` store is the reason `getCurrentThreadName()` returns an empty string for threads that have never called `setCurrentThreadName()` — the function reads its own thread's storage rather than interrogating the OS. This is intentional: the comment in the header notes that names set "by an external force" are intentionally invisible. The approach avoids any syscall on the read path and keeps reads fast. + +## The Linux 15-Character Constraint + +Linux imposes a hard 16-byte limit (including the null terminator) on thread names via `pthread_setname_np`. This header makes that constraint a compile-time concern rather than a silent runtime truncation. On Linux builds (`BOOST_OS_LINUX`), a template overload of `setCurrentThreadName` accepts only string literals: + +```cpp +template +void setCurrentThreadName(char const (&newThreadName)[N]) +{ + static_assert(N <= maxThreadNameLength + 1, "Thread name cannot exceed 15 characters"); + setCurrentThreadName(std::string_view(newThreadName, N - 1)); +} +``` + +The `static_assert` fires at compile time if the literal exceeds 15 characters, turning an easy-to-miss runtime defect into a build error. Callers passing a `std::string` or `std::string_view` at runtime fall through to the base overload, which truncates silently in the implementation (and optionally emits a `std::cerr` warning when `TRUNCATED_THREAD_NAME_LOGS` is defined). The `maxThreadNameLength` constant (15) is exposed publicly so tests and other code can query the limit without magic numbers. + +## Platform Implementations + +The `.cpp` file routes to three platform back-ends selected by `boost/predef.h` macros: + +- **macOS**: `pthread_setname_np(name.data())` — macOS's variant takes only the name (not a thread ID), so it always applies to the calling thread. +- **Linux**: `pthread_setname_np(pthread_self(), boundedName)` — the two-argument POSIX extension form, applied after truncating to `maxThreadNameLength` characters with `snprintf`. +- **Windows (MSVC debug builds only)**: Raises the documented Microsoft debugger exception `0x406d1388` with a `THREADNAME_INFO` struct. This only fires when `DEBUG` is defined and the MSVC compiler is in use, so it is a pure developer ergonomics path with no production overhead. + +## Usage in `rippled` + +The facility is used throughout the application layer wherever a dedicated thread is spawned: `BasicApp`, `LoadManager`, `LedgerCleaner`, `GRPCServer`, the RocksDB backend, `ResourceManager`, and others all call `setCurrentThreadName()` at thread startup. This makes it straightforward to identify which subsystem owns a thread when investigating hangs or CPU spikes. + +The header is intentionally minimal: two free functions and one compile-time constant, all in `namespace beast`. There are no classes, no virtual dispatch, and no dependencies beyond ``, ``, and ``. \ No newline at end of file diff --git a/include/xrpl/beast/core/LexicalCast.h.ai.json b/include/xrpl/beast/core/LexicalCast.h.ai.json new file mode 100644 index 0000000000..14ab4f12cc --- /dev/null +++ b/include/xrpl/beast/core/LexicalCast.h.ai.json @@ -0,0 +1,155 @@ +{ + "args": [ + { + "lineno": 22, + "name": "out" + }, + { + "lineno": 22, + "name": "in" + }, + { + "lineno": 28, + "name": "out" + }, + { + "lineno": 28, + "name": "in" + }, + { + "lineno": 43, + "name": "out" + }, + { + "lineno": 43, + "name": "in" + }, + { + "lineno": 52, + "name": "out" + }, + { + "lineno": 52, + "name": "in" + }, + { + "lineno": 78, + "name": "out" + }, + { + "lineno": 78, + "name": "in" + }, + { + "lineno": 89, + "name": "out" + }, + { + "lineno": 89, + "name": "in" + }, + { + "lineno": 99, + "name": "out" + }, + { + "lineno": 99, + "name": "in" + }, + { + "lineno": 109, + "name": "out" + }, + { + "lineno": 109, + "name": "in" + }, + { + "lineno": 120, + "name": "in" + }, + { + "lineno": 135, + "name": "in" + }, + { + "lineno": 135, + "name": "defaultValue" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "LexicalCast" + }, + { + "args": [], + "lineno": 34, + "name": "LexicalCast" + }, + { + "args": [], + "lineno": 73, + "name": "LexicalCast>" + }, + { + "args": [], + "lineno": 84, + "name": "LexicalCast" + }, + { + "args": [], + "lineno": 94, + "name": "LexicalCast" + }, + { + "args": [], + "lineno": 104, + "name": "LexicalCast" + }, + { + "args": [], + "lineno": 115, + "name": "BadLexicalCast" + } + ], + "description": "Provides type-safe lexical casting utilities for converting between strings and arithmetic/enum/integral types, with error checking and exception support.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/core/LexicalCast.h", + "functions": [ + { + "args": [ + "out", + "in" + ], + "lineno": 109, + "name": "lexicalCastChecked" + }, + { + "args": [ + "in" + ], + "lineno": 120, + "name": "lexicalCastThrow" + }, + { + "args": [ + "in", + "defaultValue" + ], + "lineno": 135, + "name": "lexicalCast" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "beast" + }, + { + "lineno": 13, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/core/LexicalCast.h.ai.md b/include/xrpl/beast/core/LexicalCast.h.ai.md new file mode 100644 index 0000000000..cd4fe51d50 --- /dev/null +++ b/include/xrpl/beast/core/LexicalCast.h.ai.md @@ -0,0 +1,48 @@ +# `beast/core/LexicalCast.h` — Lexical Type Conversion Utilities + +This header provides the `beast` namespace's lexical casting facility: safe, allocation-minimal conversion between string representations and numeric or boolean C++ types. It exists because `boost::lexical_cast` carries significant overhead (locale handling, exceptions as control flow, heap allocation), while `std::stoi`/`std::stoul` and similar functions have locale sensitivity and inconsistent error signaling. `LexicalCast.h` offers a uniform interface backed by `std::from_chars` — locale-independent, non-allocating, and precise about error conditions. + +## Template Specialization Architecture + +The implementation uses the classic C++ partial-specialization pattern: a `detail::LexicalCast` functor struct with `operator()` that does the actual conversion. Because partial specialization of function templates is not allowed in C++, the work is split into struct specializations and then thin public free-function wrappers call into them. This makes it straightforward to add new source or destination types without touching existing code. + +The specialization tree is: + +- **`LexicalCast`** — converts arithmetic types via `std::to_string`, and enums by first casting to their `underlying_type` before delegating. The enum path is important because XRPL uses typed enumerations in configuration contexts where human-readable integers are expected. + +- **`LexicalCast`** — the canonical parsing specialization. A `static_assert` enforces that `Out` must be integral; floating-point conversion is intentionally unsupported. The implementation uses `std::from_chars` for zero-allocation, locale-independent parsing. One notable detail: `std::from_chars` rejects a leading `+` sign, so the specialization manually advances the pointer past `+` before calling it. This accommodates formats like `"+1"` that can appear in config or protocol fields. + + The `bool` overload within this specialization is handled separately (via SFINAE with `!std::is_same_v`): it lowercases the input and accepts `"1"`, `"true"`, `"0"`, or `"false"`, returning false for anything else. This prevents the surprising default behavior where any non-zero string would be truthy. + +- **`LexicalCast>`** — a forwarding shim to the `std::string_view` specialization. The comment in the code explains its origin: as of early 2024, Boost uses `boost::core::basic_string_view` internally for HTTP header values (used in Boost.Beast's HTTP layer), and `Handshake.cpp` passes these header values directly to `lexicalCastChecked`. Without this specialization, there would be no implicit conversion and the call would fail to compile. + +- **`LexicalCast`** and the two `char*`/`char const*` variants — all forward to the `string_view` specialization. The pointer variants include `XRPL_ASSERT` checks for null, which in production builds map to `assert(...)` and in Antithesis fuzzing builds become structured assertions for the fuzzer's invariant engine. + +## Public API + +Three free functions expose the facility: + +`lexicalCastChecked(out, in)` returns `bool` — false on any parse or range error. This is the primary building block. It places the result in an output parameter rather than returning it, consistent with the checked conversion idiom where success and value are separate concerns. Callers like `ProtocolVersion.cpp` use it in guarded patterns: + +```cpp +if (!beast::lexicalCastChecked(major, std::string(m[1]))) + return std::nullopt; +``` + +`lexicalCastThrow(in)` returns `Out` by value and throws `BadLexicalCast` (a subclass of `std::bad_cast`) on failure. This is used when the caller considers parse failure a programming error or exceptional condition. + +`lexicalCast(in, defaultValue)` returns the parsed value or `defaultValue` on failure. The default for `defaultValue` is `Out()` (zero-initialized), so `lexicalCast(portStr)` returns `0` on failure — a property explicitly exploited in `StringUtilities.cpp`, where port `0` is then treated as an invalid URL: + +```cpp +pUrl.port = beast::lexicalCast(port); +if (pUrl.port == 0) + return false; +``` + +## Error Handling Tradeoffs + +The three-API design reflects the three legitimate calling styles: checked return for structured validation, exception for defensive contracts, and default-fallback for simple config parsing. Rather than unifying on exceptions (as `boost::lexical_cast` does) or solely on error codes, the header provides all three entry points atop a single shared functor. The `BadLexicalCast` exception class is minimal by design — it carries no message payload, because at the sites where it is thrown the context is already known to the caller. + +## Scope Limitation: Integrals Only + +The `static_assert` in `LexicalCast` deliberately restricts the output type to integrals. Floating-point parsing is excluded. XRPL's configuration and protocol layers do not round-trip floating-point values as strings in ways that require this facility, so supporting it would add complexity (locale handling, rounding modes) without a clear use case. Any attempt to instantiate `lexicalCast(...)` fails at compile time with a clear message. \ No newline at end of file diff --git a/include/xrpl/beast/core/List.h.ai.json b/include/xrpl/beast/core/List.h.ai.json new file mode 100644 index 0000000000..c4544efead --- /dev/null +++ b/include/xrpl/beast/core/List.h.ai.json @@ -0,0 +1,245 @@ +{ + "args": [ + { + "lineno": 10, + "name": "T" + }, + { + "lineno": 10, + "name": "U" + }, + { + "lineno": 17, + "name": "T const" + }, + { + "lineno": 17, + "name": "U" + }, + { + "lineno": 25, + "name": "T" + }, + { + "lineno": 25, + "name": "Tag" + }, + { + "lineno": 41, + "name": "N" + }, + { + "lineno": 98, + "name": "T" + }, + { + "lineno": 98, + "name": "Tag" + } + ], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "CopyConst" + }, + { + "args": [], + "lineno": 17, + "name": "CopyConst" + }, + { + "args": [], + "lineno": 25, + "name": "ListNode" + }, + { + "args": [ + "N* node = nullptr" + ], + "lineno": 41, + "name": "ListIterator" + }, + { + "args": [], + "lineno": 98, + "name": "List" + } + ], + "description": "This file implements an intrusive doubly linked list container (List) similar to std::list, allowing objects to be part of one or more lists simultaneously by deriving from List::Node. It provides STL-like interface and iterators, but requires manual memory management.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/core/List.h", + "functions": [ + { + "args": [], + "lineno": 109, + "name": "empty" + }, + { + "args": [], + "lineno": 115, + "name": "size" + }, + { + "args": [], + "lineno": 121, + "name": "front" + }, + { + "args": [], + "lineno": 128, + "name": "front" + }, + { + "args": [], + "lineno": 135, + "name": "back" + }, + { + "args": [], + "lineno": 142, + "name": "back" + }, + { + "args": [], + "lineno": 149, + "name": "begin" + }, + { + "args": [], + "lineno": 155, + "name": "begin" + }, + { + "args": [], + "lineno": 161, + "name": "cbegin" + }, + { + "args": [], + "lineno": 167, + "name": "end" + }, + { + "args": [], + "lineno": 173, + "name": "end" + }, + { + "args": [], + "lineno": 179, + "name": "cend" + }, + { + "args": [], + "lineno": 185, + "name": "clear" + }, + { + "args": [ + "pos", + "element" + ], + "lineno": 193, + "name": "insert" + }, + { + "args": [ + "pos", + "other" + ], + "lineno": 205, + "name": "insert" + }, + { + "args": [ + "pos" + ], + "lineno": 221, + "name": "erase" + }, + { + "args": [ + "element" + ], + "lineno": 233, + "name": "push_front" + }, + { + "args": [], + "lineno": 240, + "name": "pop_front" + }, + { + "args": [ + "element" + ], + "lineno": 249, + "name": "push_back" + }, + { + "args": [], + "lineno": 256, + "name": "pop_back" + }, + { + "args": [ + "other" + ], + "lineno": 263, + "name": "swap" + }, + { + "args": [ + "list" + ], + "lineno": 271, + "name": "prepend" + }, + { + "args": [ + "list" + ], + "lineno": 278, + "name": "append" + }, + { + "args": [ + "element" + ], + "lineno": 285, + "name": "iterator_to" + }, + { + "args": [ + "element" + ], + "lineno": 293, + "name": "const_iterator_to" + }, + { + "args": [ + "node" + ], + "lineno": 299, + "name": "element_from" + }, + { + "args": [ + "node" + ], + "lineno": 304, + "name": "element_from" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + }, + { + "lineno": 7, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/core/List.h.ai.md b/include/xrpl/beast/core/List.h.ai.md new file mode 100644 index 0000000000..229ce44e4e --- /dev/null +++ b/include/xrpl/beast/core/List.h.ai.md @@ -0,0 +1,31 @@ +# `beast/core/List.h` — Intrusive Doubly Linked List + +This file implements `beast::List`, an intrusive doubly-linked list container living in the `beast` namespace within XRPL's embedded utility library. It solves a specific systems-programming problem: maintaining objects in linked lists without any heap allocation for list bookkeeping, and without the copies that `std::list` would impose. + +## Why Intrusive? + +A standard `std::list` allocates a separate node on the heap for each element. In hot paths like XRPL's resource management system — which tracks thousands of peer connections across `inbound_`, `outbound_`, `admin_`, and `inactive_` lists simultaneously — those allocations and the pointer indirection they introduce matter. With an intrusive list, the linkage lives directly inside the object. Moving a `Resource::Entry` from the inbound list to the inactive list is a handful of pointer swaps with no allocator involvement. + +This design also means the list never owns its elements. When `erase()` removes a node, the object continues to exist unmodified; only its `m_next`/`m_prev` pointers are left dangling. The caller holds responsibility for the object's lifetime, which suits the XRPL pattern of storing entries in a hash map (`Table`) and maintaining intrusive lists as secondary views into that same population. + +## Structure + +Three components work together: + +**`detail::ListNode`** is the intrusive portion. An element type becomes list-eligible by publicly inheriting from `List::Node` (which resolves to this class). It holds only two raw pointers, `m_next` and `m_prev`, both defaulted to `nullptr`. Both `List` and `ListIterator` are declared `friend` to access these fields directly. The `Tag` template parameter is what allows an object to inhabit multiple simultaneous lists: each distinct tag produces a distinct `ListNode` base class, so the object carries independent `m_next`/`m_prev` pairs for each list type. + +**`detail::ListIterator`** is a bidirectional iterator parameterised on the node type `N`. The const/non-const duality is handled elegantly through the `CopyConst` trait: when `N` is a `const`-qualified node type, `value_type` inherits that constness, yielding a `const_iterator` without any code duplication. Dereferencing uses `static_cast(*m_node)`, which is safe because the node type is always a base class of the element type. + +**`List`** is the container. It holds two sentinel nodes — `m_head` and `m_tail` — rather than raw pointers. `m_head.m_prev` is set to `nullptr` on construction to mark it as the head sentinel, and `m_tail.m_next` is `nullptr` to mark the tail. This sentinel design means `begin()` returns an iterator to `m_head.m_next` and `end()` returns an iterator to `&m_tail`, so all insert and erase operations work uniformly at every position including the boundaries, with no special-case null checks. The list is non-copyable and non-assignable, preventing accidental shallow copies that would break the intrusive invariants. + +## The Tag Pattern in Practice + +`Resource::Logic` illustrates the expected usage. `Entry` derives from `beast::List::Node`, and `Logic` maintains four `EntryIntrusiveList` objects: `inbound_`, `outbound_`, `admin_`, and `inactive_`. The code comments explicitly note that because these are intrusive lists an `Entry` can be in at most one of them at any instant, and must be removed from the current list before being placed in another. This is the one-list-at-a-time constraint that the `Tag` mechanism exists to relax: if `Entry` needed to appear in two lists concurrently, it would inherit from two differently-tagged `List::Node` and `List::Node` bases. + +## Bulk Operations and `swap` + +The list-to-list `insert(iterator, List&)` overload splices an entire list into another in O(1) time, then clears the source. `prepend()`, `append()`, and `swap()` all build on this. The `swap()` implementation routes through a temporary list using `append()` calls — it does not use pointer tricks — which is correct but means `swap` does three list splices. This is acceptable since swap on intrusive containers is uncommon. + +## Absent Safeguards + +There is no mechanism to detect double-insertion (inserting a node that already belongs to a list), which would corrupt both lists. The invariants documented in each method — "the element must not already be in the list", "the element must exist in the list" — are stated in comments but are not enforced at runtime. Similarly, `clear()` deliberately does not free elements and does not zero the node pointers of removed elements; calling `clear()` leaves all previously-listed objects with stale `m_next`/`m_prev` pointers until they are re-inserted or destroyed. These are the standard tradeoffs of intrusive container design: maximum performance, minimum safety net. \ No newline at end of file diff --git a/include/xrpl/beast/core/LockFreeStack.h.ai.json b/include/xrpl/beast/core/LockFreeStack.h.ai.json new file mode 100644 index 0000000000..572c44503e --- /dev/null +++ b/include/xrpl/beast/core/LockFreeStack.h.ai.json @@ -0,0 +1,51 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "NodePtr node" + ], + "lineno": 9, + "name": "LockFreeStackIterator" + }, + { + "args": [], + "lineno": 89, + "name": "LockFreeStack" + }, + { + "args": [ + "Node* next" + ], + "lineno": 92, + "name": "Node" + } + ], + "description": "Implements a lock-free, multiple-producer multiple-consumer (MPMC) intrusive stack with forward iterators, suitable for concurrent use. Provides push, pop, and iteration operations, with thread safety and ABA problem warnings.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/core/LockFreeStack.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 61, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 70, + "name": "operator!=" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/core/LockFreeStack.h.ai.md b/include/xrpl/beast/core/LockFreeStack.h.ai.md new file mode 100644 index 0000000000..bd73cf03f6 --- /dev/null +++ b/include/xrpl/beast/core/LockFreeStack.h.ai.md @@ -0,0 +1,56 @@ +# `LockFreeStack.h` — Intrusive Lock-Free MPMC Stack + +## Role in the System + +`LockFreeStack` is a foundational concurrency primitive in the `beast` namespace, providing a Multiple-Producer Multiple-Consumer (MPMC) intrusive stack implemented with atomic compare-and-swap (CAS) operations. It lives alongside `List.h` — the intrusive doubly-linked list — and shares the same intrusive node-inheritance idiom. The primary consumer in the XRPL codebase is `Workers`, the thread pool implementation, which uses two simultaneous `LockFreeStack` instances to track all created workers and the subset that are currently paused. + +## Intrusive Design and the `Tag` Mechanism + +Like `beast::List`, `LockFreeStack` is intrusive: rather than allocating a wrapper node internally, it requires the element type to embed the `Node` type (typically by public inheritance). This eliminates heap allocations from `push_front`/`pop_front` entirely, a meaningful gain in a hot-path thread pool. + +The `Tag` template parameter is the key to using a single object in multiple stacks simultaneously. When `Workers` manages its thread pool: + +```cpp +class Worker : public beast::LockFreeStack::Node, + public beast::LockFreeStack::Node +``` + +The two `Node` base classes carry separate `m_next` pointers — one for the `m_everyone` stack (all workers ever created) and one for `m_paused` (only paused workers). Without tags, inheriting `Node` twice would be ambiguous and collapse the two link fields into one. The same idiom is documented extensively in `List.h` for doubly-linked lists. + +## Internal Representation: The Sentinel End Node + +The stack stores `m_end` as a `Node` member by value and uses it as the terminal sentinel — `m_head` points to `&m_end` when the stack is empty. This avoids a null pointer as the terminator, which matters for two reasons: it keeps the iterator's end comparison simple (`iterator(&m_end)` rather than `iterator(nullptr)`), and it gives `empty()` a single `m_head.load() == &m_end` check without a potential dereference. + +## Lock-Free Push and Pop + +Both `push_front` and `pop_front` implement the standard CAS retry loop: + +```cpp +while (!m_head.compare_exchange_strong( + old_head, node, + std::memory_order_release, std::memory_order_relaxed)) +``` + +The release ordering on success ensures that any writes to the node before it is pushed are visible to threads that subsequently load the head. The relaxed ordering on failure only updates the local `old_head` snapshot, so no ordering guarantee is wasted on failed attempts. + +`push_front` returns `true` if and only if the stack was empty before the push. This is a deliberate design: it allows the caller to detect the "first item added" event without a separate `empty()` check that would create a time-of-check/time-of-use (TOCTOU) race in a concurrent context. `Workers::addTask()` can use this signal to decide whether to wake a thread without an additional atomic read. + +`pop_front` returns `nullptr` when the stack is empty, giving callers an idiomatic ownership-transfer interface: the returned raw pointer represents the transferred ownership of the node back to the caller, and `nullptr` signals the empty case without an exception. + +There is a `VFALCO NOTE` comment on `push_front` observing that it takes a `Node*` (pointer) while the intrusive `List` interface takes a reference. This is a minor style inconsistency that was never resolved. + +## The ABA Problem — Explicit Caller Responsibility + +The header prominently documents that the implementation does not protect against the ABA problem. In a CAS-based stack, the ABA scenario arises when a thread reads the head pointer (value A), is preempted, another thread pops A, pushes a different node, then re-pushes A to the head — so the CAS in the original thread succeeds spuriously. The result is a corrupted stack. + +`LockFreeStack` documents this and explicitly assigns the responsibility for prevention to the caller. In the `Workers` use case, this is safe because workers are long-lived objects (allocated once in the constructor, destroyed only in the destructor) and are never destroyed while the stack is being mutated from other threads. + +## Iteration + +`LockFreeStackIterator` is a forward-only iterator typed on ``. The `IsConst` boolean drives all pointer and reference type selection through `std::conditional`, giving separate `iterator` and `const_iterator` types from a single implementation. The conversion constructor from `LockFreeStackIterator` is `explicit` to prevent accidental const-stripping, while allowing const-to-const or mutable-to-const conversions. + +Crucially, the stack documents that iteration is **not** safe when `push_front` or `pop_front` is called concurrently. The `begin()` call loads the current head atomically, but subsequent `operator++` calls load `m_next` without any protocol to prevent the node from being popped and re-used mid-traversal. The iterator facility is provided for single-threaded traversal scenarios, not as a concurrent view. + +## Summary + +`LockFreeStack` is a narrow, focused primitive: it does one thing (LIFO queue with atomic push/pop) and defers the harder problems (ABA, memory reclamation, concurrent iteration) to its callers. The intrusive, tag-parameterized design keeps it zero-overhead for the `Workers` use case where multiple simultaneous list memberships are required, and the sentinel node and push return value are small but deliberate choices that remove unnecessary atomic operations from the callers. \ No newline at end of file diff --git a/include/xrpl/beast/core/SemanticVersion.h.ai.json b/include/xrpl/beast/core/SemanticVersion.h.ai.json new file mode 100644 index 0000000000..30c3eae75d --- /dev/null +++ b/include/xrpl/beast/core/SemanticVersion.h.ai.json @@ -0,0 +1,80 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "void", + "std::string_view version" + ], + "lineno": 11, + "name": "SemanticVersion" + } + ], + "description": "Defines a SemanticVersion class for representing and comparing semantic version numbers according to the Semantic Versioning Specification.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/core/SemanticVersion.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 46, + "name": "compare" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 49, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 54, + "name": "operator!=" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 59, + "name": "operator>=" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 64, + "name": "operator<=" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 69, + "name": "operator>" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 74, + "name": "operator<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/core/SemanticVersion.h.ai.md b/include/xrpl/beast/core/SemanticVersion.h.ai.md new file mode 100644 index 0000000000..52bc18a5e9 --- /dev/null +++ b/include/xrpl/beast/core/SemanticVersion.h.ai.md @@ -0,0 +1,36 @@ +# `include/xrpl/beast/core/SemanticVersion.h` + +## Role and Purpose + +`SemanticVersion.h` provides a strict, spec-compliant implementation of [Semantic Versioning 2.0.0](http://semver.org/) within the `beast` namespace. Its primary job is to parse, store, serialize, and compare version strings in the form `MAJOR.MINOR.PATCH[-pre-release][+metadata]`. The class exists because the XRPL node (`rippled`) publishes its own software version both at startup and over the peer-to-peer network, requiring a reliable and unambiguous way to parse, validate, and compare those version strings. + +## The `SemanticVersion` Class + +The class is a straightforward data-holder: three `int` fields (`majorVersion`, `minorVersion`, `patchVersion`) and two `identifier_list` members (both `std::vector`) for `preReleaseIdentifiers` and `metaData`. This flat, public-member design reflects the fact that `SemanticVersion` is not encapsulating behavior on those fields — it is a structured representation that other code reads directly. The two constructors offer different contracts: the default constructor zero-initializes, while the `string_view` constructor throws `std::invalid_argument` if parsing fails. This split lets callers choose between exception-based control flow (when a valid version is expected) and error-code control flow via `parse()` (when handling untrusted input). + +## Parsing is Deliberately Strict + +`parse()` rejects anything the semver specification forbids: leading or trailing whitespace, leading zeroes on any numeric component (e.g., `01.2.3`), negative integers, missing components, and stray characters after the version string. The implementation in `SemanticVersion.cpp` is a character-consuming parser — `chop()` removes an exact prefix, `chopUInt()` reads and removes a decimal integer while enforcing no-leading-zero and range constraints, and `extract_identifier()`/`extract_identifiers()` handle the dot-delimited pre-release and metadata fields. Metadata identifiers are allowed to have leading zeroes (the semver spec permits this for the `+metadata` section, though not for pre-release identifiers), so `extract_identifiers` takes an `allowLeadingZeroes` flag that is set differently for each section. The function returns `false` rather than throwing so callers can treat invalid versions as a recoverable condition. + +## Comparison Rules and the `compare()` Function + +The free function `compare(lhs, rhs)` implements the semver precedence order and returns a three-way result (`-1`, `0`, `1`) in the style of `strcmp`, so all six comparison operators can be implemented as inline forwarding wrappers. This design avoids duplicating the comparison logic and makes the operators zero-overhead. + +The comparison logic follows the spec faithfully: + +1. **Major, minor, patch** are compared numerically in order. Any difference here is decisive. +2. **Pre-release vs. release**: a pre-release version (`1.0.0-alpha`) is always less than the corresponding release (`1.0.0`) even though their numeric components are identical. This is handled by checking `isRelease()` (which simply returns `preReleaseIdentifiers.empty()`) before entering field-by-field pre-release comparison. +3. **Pre-release identifier ordering**: identifiers are compared pairwise. Numeric identifiers are cast back to `int` and compared by value (so `beta.9 < beta.11`), while alphanumeric identifiers use lexicographic string comparison. Purely numeric identifiers sort below alphanumeric ones of the same position. When one version's pre-release list is exhausted before the other's, the longer list wins (e.g., `1.0.0-alpha < 1.0.0-alpha.1`). +4. **Build metadata is ignored entirely** — two versions that differ only in `+metadata` compare as equal. This is a deliberate semver spec rule: metadata is for informational context (e.g., build flags, git hashes) and must not affect version ordering. + +## Usage in the XRPL Network Stack + +`SemanticVersion` has two concrete consumers in the codebase: + +**`BuildInfo.cpp`** uses it in two ways. First, the version string constant (currently `"3.2.0-b0"`) is validated at startup inside a static initializer: `v.parse(s)` must succeed and `v.print() == s` must hold, guaranteeing that the hard-coded version string is well-formed and round-trips cleanly. A failure calls `LogicError`, crashing the process — this is intentional: a malformed version string is a build-time mistake, not a runtime recoverable condition. Second, `encodeSoftwareVersion()` parses the version string to pack `major`, `minor`, `patch`, and pre-release type (`rc` or `b`) into a compact 64-bit integer for peer-to-peer version advertisement, using `isPreRelease()` and iterating `preReleaseIdentifiers` to classify the build stage. + +**`ApiVersion.h`** uses it for legacy API version 1 responses. When the API version is unspecified (defaulting to version 1), the server's `version` response object includes `first`, `good`, and `last` fields represented as semver strings (`"1.0.0"`), instantiated as `static beast::SemanticVersion` objects. + +## Invariants and Defensive Patterns + +The `compare()` function uses `XRPL_ASSERT` to enforce that when one identifier is numeric, the other must be as well (and vice versa), since the identifier-type classification is done before branching. The test suite (`SemanticVersion_test.cpp`) exhaustively verifies the canonical semver ordering example from the specification — `1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0` — and also validates that metadata suffixes do not affect any comparison outcome, confirming correct spec compliance. \ No newline at end of file diff --git a/include/xrpl/beast/hash/hash_append.h.ai.json b/include/xrpl/beast/hash/hash_append.h.ai.json new file mode 100644 index 0000000000..64566f5c7f --- /dev/null +++ b/include/xrpl/beast/hash/hash_append.h.ai.json @@ -0,0 +1,702 @@ +{ + "args": [ + { + "lineno": 15, + "name": "T& t" + }, + { + "lineno": 23, + "name": "T& t" + }, + { + "lineno": 28, + "name": "T& t" + }, + { + "lineno": 33, + "name": "T& t" + }, + { + "lineno": 120, + "name": "Hasher& h" + }, + { + "lineno": 120, + "name": "T const& t" + }, + { + "lineno": 127, + "name": "Hasher& h" + }, + { + "lineno": 127, + "name": "T t" + }, + { + "lineno": 135, + "name": "Hasher& h" + }, + { + "lineno": 135, + "name": "T t" + }, + { + "lineno": 143, + "name": "Hasher& h" + }, + { + "lineno": 143, + "name": "std::nullptr_t" + }, + { + "lineno": 150, + "name": "Hasher& h" + }, + { + "lineno": 150, + "name": "T (&a)[N]" + }, + { + "lineno": 154, + "name": "Hasher& h" + }, + { + "lineno": 154, + "name": "std::basic_string const& s" + }, + { + "lineno": 158, + "name": "Hasher& h" + }, + { + "lineno": 158, + "name": "std::basic_string const& s" + }, + { + "lineno": 162, + "name": "Hasher& h" + }, + { + "lineno": 162, + "name": "std::pair const& p" + }, + { + "lineno": 166, + "name": "Hasher& h" + }, + { + "lineno": 166, + "name": "std::vector const& v" + }, + { + "lineno": 170, + "name": "Hasher& h" + }, + { + "lineno": 170, + "name": "std::vector const& v" + }, + { + "lineno": 174, + "name": "Hasher& h" + }, + { + "lineno": 174, + "name": "std::array const& a" + }, + { + "lineno": 178, + "name": "Hasher& h" + }, + { + "lineno": 178, + "name": "std::tuple const& t" + }, + { + "lineno": 182, + "name": "Hasher& h" + }, + { + "lineno": 182, + "name": "std::unordered_map const& m" + }, + { + "lineno": 187, + "name": "Hasher& h" + }, + { + "lineno": 187, + "name": "std::unordered_set const& s" + }, + { + "lineno": 192, + "name": "Hasher& h" + }, + { + "lineno": 192, + "name": "boost::container::flat_set const& v" + }, + { + "lineno": 195, + "name": "Hasher& h" + }, + { + "lineno": 195, + "name": "boost::container::flat_set const& v" + }, + { + "lineno": 198, + "name": "Hasher& h" + }, + { + "lineno": 198, + "name": "T0 const& t0" + }, + { + "lineno": 198, + "name": "T1 const& t1" + }, + { + "lineno": 198, + "name": "T const&... t" + }, + { + "lineno": 203, + "name": "Hasher& h" + }, + { + "lineno": 203, + "name": "T (&a)[N]" + }, + { + "lineno": 210, + "name": "Hasher& h" + }, + { + "lineno": 210, + "name": "std::basic_string const& s" + }, + { + "lineno": 217, + "name": "Hasher& h" + }, + { + "lineno": 217, + "name": "std::basic_string const& s" + }, + { + "lineno": 224, + "name": "Hasher& h" + }, + { + "lineno": 224, + "name": "std::pair const& p" + }, + { + "lineno": 230, + "name": "Hasher& h" + }, + { + "lineno": 230, + "name": "std::vector const& v" + }, + { + "lineno": 237, + "name": "Hasher& h" + }, + { + "lineno": 237, + "name": "std::vector const& v" + }, + { + "lineno": 243, + "name": "Hasher& h" + }, + { + "lineno": 243, + "name": "std::array const& a" + }, + { + "lineno": 249, + "name": "Hasher& h" + }, + { + "lineno": 249, + "name": "boost::container::flat_set const& v" + }, + { + "lineno": 254, + "name": "Hasher& h" + }, + { + "lineno": 254, + "name": "boost::container::flat_set const& v" + }, + { + "lineno": 260, + "name": "..." + }, + { + "lineno": 264, + "name": "Hasher& h" + }, + { + "lineno": 264, + "name": "T const& t" + }, + { + "lineno": 269, + "name": "Hasher& h" + }, + { + "lineno": 269, + "name": "std::tuple const& t" + }, + { + "lineno": 269, + "name": "std::index_sequence" + }, + { + "lineno": 276, + "name": "Hasher& h" + }, + { + "lineno": 276, + "name": "std::tuple const& t" + }, + { + "lineno": 282, + "name": "Hasher& h" + }, + { + "lineno": 282, + "name": "std::shared_ptr const& p" + }, + { + "lineno": 287, + "name": "Hasher& h" + }, + { + "lineno": 287, + "name": "std::chrono::duration const& d" + }, + { + "lineno": 292, + "name": "Hasher& h" + }, + { + "lineno": 292, + "name": "std::chrono::time_point const& tp" + }, + { + "lineno": 297, + "name": "Hasher& h" + }, + { + "lineno": 297, + "name": "T0 const& t0" + }, + { + "lineno": 297, + "name": "T1 const& t1" + }, + { + "lineno": 297, + "name": "T const&... t" + }, + { + "lineno": 303, + "name": "HashAlgorithm& h" + }, + { + "lineno": 303, + "name": "std::error_code const& ec" + } + ], + "classes": [ + { + "args": [], + "lineno": 44, + "name": "is_uniquely_represented" + }, + { + "args": [], + "lineno": 51, + "name": "is_uniquely_represented" + }, + { + "args": [], + "lineno": 56, + "name": "is_uniquely_represented" + }, + { + "args": [], + "lineno": 61, + "name": "is_uniquely_represented" + }, + { + "args": [], + "lineno": 66, + "name": "is_uniquely_represented>" + }, + { + "args": [], + "lineno": 75, + "name": "is_uniquely_represented>" + }, + { + "args": [], + "lineno": 84, + "name": "is_uniquely_represented" + }, + { + "args": [], + "lineno": 89, + "name": "is_uniquely_represented>" + }, + { + "args": [], + "lineno": 104, + "name": "is_contiguously_hashable" + }, + { + "args": [], + "lineno": 113, + "name": "is_contiguously_hashable" + } + ], + "description": "This file provides a set of C++ template utilities and metafunctions for determining if types are uniquely represented and contiguously hashable, and for appending various types to a hash algorithm in a consistent, endian-aware way. It includes specializations for standard containers, tuples, pairs, arrays, and chrono types, and supports Boost and standard library types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/hash/hash_append.h", + "functions": [ + { + "args": [ + "T& t" + ], + "lineno": 15, + "name": "reverse_bytes" + }, + { + "args": [ + "T& t", + "std::false_type" + ], + "lineno": 23, + "name": "maybe_reverse_bytes" + }, + { + "args": [ + "T& t", + "std::true_type" + ], + "lineno": 28, + "name": "maybe_reverse_bytes" + }, + { + "args": [ + "T& t", + "Hasher&" + ], + "lineno": 33, + "name": "maybe_reverse_bytes" + }, + { + "args": [ + "Hasher& h", + "T const& t" + ], + "lineno": 120, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "T t" + ], + "lineno": 127, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "T t" + ], + "lineno": 135, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::nullptr_t" + ], + "lineno": 143, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "T (&a)[N]" + ], + "lineno": 150, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::basic_string const& s" + ], + "lineno": 154, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::basic_string const& s" + ], + "lineno": 158, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::pair const& p" + ], + "lineno": 162, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::vector const& v" + ], + "lineno": 166, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::vector const& v" + ], + "lineno": 170, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::array const& a" + ], + "lineno": 174, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::tuple const& t" + ], + "lineno": 178, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::unordered_map const& m" + ], + "lineno": 182, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::unordered_set const& s" + ], + "lineno": 187, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "boost::container::flat_set const& v" + ], + "lineno": 192, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "boost::container::flat_set const& v" + ], + "lineno": 195, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "T0 const& t0", + "T1 const& t1", + "T const&... t" + ], + "lineno": 198, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "T (&a)[N]" + ], + "lineno": 203, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::basic_string const& s" + ], + "lineno": 210, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::basic_string const& s" + ], + "lineno": 217, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::pair const& p" + ], + "lineno": 224, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::vector const& v" + ], + "lineno": 230, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::vector const& v" + ], + "lineno": 237, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::array const& a" + ], + "lineno": 243, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "boost::container::flat_set const& v" + ], + "lineno": 249, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "boost::container::flat_set const& v" + ], + "lineno": 254, + "name": "hash_append" + }, + { + "args": [ + "..." + ], + "lineno": 260, + "name": "for_each_item" + }, + { + "args": [ + "Hasher& h", + "T const& t" + ], + "lineno": 264, + "name": "hash_one" + }, + { + "args": [ + "Hasher& h", + "std::tuple const& t", + "std::index_sequence" + ], + "lineno": 269, + "name": "tuple_hash" + }, + { + "args": [ + "Hasher& h", + "std::tuple const& t" + ], + "lineno": 276, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::shared_ptr const& p" + ], + "lineno": 282, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::chrono::duration const& d" + ], + "lineno": 287, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::chrono::time_point const& tp" + ], + "lineno": 292, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "T0 const& t0", + "T1 const& t1", + "T const&... t" + ], + "lineno": 297, + "name": "hash_append" + }, + { + "args": [ + "HashAlgorithm& h", + "std::error_code const& ec" + ], + "lineno": 303, + "name": "hash_append" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "beast" + }, + { + "lineno": 14, + "name": "detail" + }, + { + "lineno": 258, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/hash/hash_append.h.ai.md b/include/xrpl/beast/hash/hash_append.h.ai.md new file mode 100644 index 0000000000..63f6761d73 --- /dev/null +++ b/include/xrpl/beast/hash/hash_append.h.ai.md @@ -0,0 +1,54 @@ +# `include/xrpl/beast/hash/hash_append.h` + +## Role in the System + +This header is the foundation of the XRPL `beast` hashing framework. It implements the "hash append" idiom popularized by Howard Hinnant's N3980 proposal — a composable, algorithm-agnostic mechanism for feeding structured C++ data into any hash algorithm. Rather than tying types to a specific hash function or to `std::hash`, the pattern separates *what gets hashed* (the type) from *how it is hashed* (the algorithm), allowing both to evolve independently. Everything in the `beast::uhash` and `xrpl::hardened_hash` pipeline flows through the overloads defined here. + +## The Hasher Concept + +The framework is parameterized on a `Hasher` type that must satisfy three requirements: a callable `operator()(void const*, std::size_t)` that feeds raw bytes into the hasher state, a `static constexpr boost::endian::order endian` member declaring the byte order the hasher expects, and an `explicit operator result_type()` conversion to extract the final digest. The concrete implementation, `beast::xxhasher`, sets `endian == boost::endian::order::native`, meaning it accepts data in whatever byte order the host CPU uses and performs no byte swapping. A hypothetical network-canonical hasher could instead declare `endian == boost::endian::order::big`, triggering automatic byte reversal on little-endian platforms. + +## Two-Tier Type Classification + +The file establishes two layered metafunctions that control how data is fed to the hasher. + +`is_uniquely_represented` asks: *can equal values always be compared by raw `memcmp`?* Integers, enums, and pointers qualify because their in-memory bit patterns are in bijection with their abstract values. Floating-point types deliberately do not qualify — IEEE 754 defines `+0.0 == -0.0` yet the two have distinct bit patterns. Composites (`std::pair`, `std::tuple`, `std::array`, C arrays) qualify only when all members are uniquely represented *and* the compiler has added no padding (verified via `sizeof` comparisons). + +`is_contiguously_hashable` refines this: a type is contiguously hashable if it is uniquely represented *and* either the element size is one byte (byte order is irrelevant for single bytes) or the hasher's declared endian matches the native byte order. When this predicate is true, the entire object or range can be passed to the hasher as a single `h(ptr, size)` call. When false, each element must be individually dispatched through `hash_append`, where the endian machinery can intervene. + +## Endian-Safe Scalar Hashing + +The `detail` namespace provides `reverse_bytes(t)`, which in-place swaps all bytes of a scalar, and `maybe_reverse_bytes(t, hasher)`, which dispatches through a `std::integral_constant` tag to conditionally invoke the reversal. The condition evaluates at compile time: `Hasher::endian != boost::endian::order::native`. Because `xxhasher` always uses native order, these functions are no-ops in practice, but the machinery is there for hasher implementations that demand a fixed byte order across platforms. + +The three scalar `hash_append` overloads reflect the classification logic precisely: +- Contiguously hashable types (integers/enums/pointers on native-endian hashers): passed directly via `h(addressof(t), sizeof(t))`. +- Non-contiguously-hashable integrals, enums, and pointers: copied to a local, byte-reversed, then passed. +- Floating-point values: normalized by the idiom `if (t == 0) t = 0` — this collapses `-0.0` to `+0.0` before hashing, enforcing the invariant that equal values hash identically. + +## Container Overloads and Length Disambiguation + +Every container overload appends the container's *element count* after the element data. This is not redundant — without the size, a `vector` containing `{1, 2}` and another containing `{1}` followed immediately by `{2}` would produce identical byte sequences when their data is concatenated. Appending the size creates a domain separator that makes length-extension ambiguity impossible. + +For containers whose elements are contiguously hashable, the data is flushed to the hasher in one bulk call (`h(v.data(), v.size() * sizeof(T))`), trading the element-by-element dispatch overhead for a single operation. For other containers the loop approach is used. + +A subtle issue exists in the `boost::container::flat_set` contiguous-hashable path, which reads `h(&(v.begin()), v.size() * sizeof(Key))`. This takes the address of the *iterator object* rather than the address of the first element. The correct expression would be `h(&(*v.begin()), ...)` or `h(v.data(), ...)`. The non-contiguous path (element-by-element loop) is unaffected. + +## Tuple Hashing Trick + +Tuples that are not contiguously hashable use a pre-`constexpr for` idiom. `detail::tuple_hash` expands `hash_one(h, std::get(t))...` inside the argument list of `for_each_item(...)`, which accepts `...` and does nothing. Each `hash_one` call returns `int` to give the pack expansion something to hold, and `for_each_item` swallows the pack. This ensures all tuple elements are hashed in index order without requiring fold expressions or recursive templates. + +## Notable Special Cases + +`hash_append` for `std::shared_ptr` hashes the raw pointer address (`p.get()`), not the pointed-to value. This treats shared ownership groups as distinct hash keys by identity, which is appropriate when the pointer itself is the key. + +`hash_append` for `std::error_code` hashes both `ec.value()` (the numeric code) and `&ec.category()` (the address of the singleton category object). This correctly distinguishes identical numeric codes from different error domains, which `ec.value()` alone would conflate. + +## ADL Extension Mechanism + +The real power of the pattern is extensibility. User-defined types extend it by defining a free `hash_append(Hasher&, MyType const&)` function in the same namespace as `MyType`. Argument-dependent lookup then finds the right overload without any registration or inheritance. The XRPL codebase uses this pervasively: `base_uint` bypasses endian logic entirely (`h(a.data_.data(), sizeof(a.data_))` — its big-endian binary representation is part of the wire protocol), and `Slice` reduces to a single raw-byte flush since it already represents a plain byte range. Both declare their own `hash_append` overloads that live next to the type definition and are pulled in automatically by ADL wherever `hash_append` is called in a template context. + +## Relationship to `uhash` and `hardened_hash` + +`beast::uhash` is a one-liner adapter that constructs a `Hasher`, calls `hash_append(h, t)`, and returns `static_cast(h)`. It is the `std::hash`-compatible entry point for unordered containers. + +`xrpl::hardened_hash` extends this with per-instance random seeds generated at construction time (using `std::random_device` → `std::mt19937_64`). The seeds are passed to the hasher constructor, mixing them into every digest. This prevents hash-flooding denial-of-service attacks where an adversary submits keys crafted to maximise collisions in an unordered container — because the seed is unpredictable, the adversary cannot predict which inputs will collide. \ No newline at end of file diff --git a/include/xrpl/beast/hash/uhash.h.ai.json b/include/xrpl/beast/hash/uhash.h.ai.json new file mode 100644 index 0000000000..b5017be924 --- /dev/null +++ b/include/xrpl/beast/hash/uhash.h.ai.json @@ -0,0 +1,28 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "uhash" + } + ], + "description": "Defines a universal hash function template (uhash) in the beast namespace, using a specified Hasher (default xxhasher) and hash_append to compute hash values for arbitrary types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/hash/uhash.h", + "functions": [ + { + "args": [ + "t" + ], + "lineno": 13, + "name": "operator()" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/hash/uhash.h.ai.md b/include/xrpl/beast/hash/uhash.h.ai.md new file mode 100644 index 0000000000..0561592298 --- /dev/null +++ b/include/xrpl/beast/hash/uhash.h.ai.md @@ -0,0 +1,49 @@ +# `uhash.h` — Universal Hash Functor + +`uhash.h` defines the `beast::uhash` template struct, a thin adapter that bridges two complementary systems — the `hash_append` extensibility protocol and the `xxhasher` algorithm — into a single callable compatible with the `Hash` concept required by `std::unordered_map`, `std::unordered_set`, and their variants. + +## Role in the System + +The file is deliberately minimal. At 27 lines, it exists purely as connective tissue. Its job is to expose a single `operator()(T const&)` that any standard hash container can call, while delegating the two hard problems elsewhere: *how to serialize a type into bytes* (`hash_append`) and *how to turn bytes into a hash value* (`xxhasher`). This separation is the design's central insight, and `uhash` is the glue that makes it usable from the container side. + +In `UnorderedContainers.h`, `beast::uhash<>` appears as the default `Hash` template argument for the `xrpl::hash_map`, `hash_set`, `hash_multimap`, and `hash_multiset` type aliases. These are the non-cryptographic, performance-oriented containers used throughout the XRPL codebase wherever hash-based lookup is needed and DoS resistance is not required. The hardened variants (`hardened_hash_map`, etc.) use `hardened_hash` instead, which wraps the same `xxhasher` but seeds it with a random value per-process to defeat hash-flooding attacks. + +## The Template Design + +```cpp +template +struct uhash +{ + using result_type = typename Hasher::result_type; + + template + result_type operator()(T const& t) const noexcept + { + Hasher h; + hash_append(h, t); + return static_cast(h); + } +}; +``` + +Three things happen inside `operator()`: a fresh `Hasher` is default-constructed, `hash_append` is called to serialize `t` into the hasher's internal buffer, and then the hasher is cast to `result_type` to extract the final digest. + +The `Hasher` is constructed fresh for each call rather than being stored as member state. This is correct because `xxhasher` tracks accumulated input; reusing a partially-consumed hasher across calls would corrupt subsequent hashes. The stateless design of `uhash` itself makes it trivially copyable and safe for concurrent read use with separate `operator()` invocations. + +The `noexcept` specification on `operator()` propagates the contract established by all `hash_append` overloads and `xxhasher::operator()`. Since these are explicitly `noexcept`, the entire hash computation is guaranteed not to throw, which matters for use in container internals. + +## The `hash_append` Protocol + +`hash_append` is an ADL-based extension point defined in `hash_append.h`. It provides overloads for all common standard types — scalars, `std::string`, `std::vector`, `std::array`, `std::pair`, `std::tuple`, `std::shared_ptr`, chrono types, `boost::container::flat_set`, and more. Types that are "uniquely represented" (no padding, no alternative bit patterns for equal values) can be hashed by a single `memcpy`-style call to the hasher. Types that aren't (e.g., IEEE floats, types with padding) are walked element by element or byte-normalized first. + +Critically, the protocol handles endianness. `xxhasher` declares `endian = boost::endian::order::native`, which means `hash_append` skips byte-swapping for multi-byte scalars on the current platform. This optimizes the common case while keeping the protocol correct on big-endian platforms or with hypothetical cross-endian hashers. + +Custom types integrate by providing a free function `hash_append(Hasher&, MyType const&)` in their own namespace; ADL ensures `uhash` finds it automatically without any modification to `uhash` or `hash_append.h` itself. + +## The Default Hasher: `xxhasher` + +`xxhasher` wraps the XXH3 64-bit algorithm from the xxHash library. It maintains a 64-byte aligned internal buffer to avoid the overhead of the streaming API for small inputs — most ledger object hashes will fit entirely in that buffer, making them single-call `XXH3_64bits()` invocations rather than streaming updates. The streaming `XXH3_state_t` path is only activated when accumulated data exceeds 64 bytes, at which point a heap allocation is made. Because `uhash` constructs a fresh `xxhasher` per call, the streaming path is exercised only for large objects that exceed the buffer in a single hash computation. + +## When to Use `uhash` vs. `hardened_hash` + +The comment in `UnorderedContainers.h` states the rule directly: use `hash_*` containers (and by extension `uhash`) for keys that don't require cryptographic security; use `hardened_hash_*` for keys exposed to externally-controlled input. `uhash` with `xxhasher` is deterministic across program runs, making it unsuitable as the hash function for containers keyed on data from network peers — an attacker who can predict bucket collisions can trigger O(n) lookup degradation. `hardened_hash` mitigates this by seeding the hasher randomly at startup. \ No newline at end of file diff --git a/include/xrpl/beast/hash/xxhasher.h.ai.json b/include/xrpl/beast/hash/xxhasher.h.ai.json new file mode 100644 index 0000000000..dfb90a562e --- /dev/null +++ b/include/xrpl/beast/hash/xxhasher.h.ai.json @@ -0,0 +1,60 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "xxhasher(const&)", + "operator=(const&)", + "xxhasher()", + "xxhasher(Seed seed)", + "xxhasher(Seed seed, Seed)", + "operator()(void const* key, std::size_t len)", + "operator result_type()" + ], + "lineno": 11, + "name": "xxhasher" + } + ], + "description": "Implements the beast::xxhasher class, a 64-bit hash function wrapper around XXH3/XXH64, with internal buffering and optional seeding, for efficient hashing in the Beast library.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/hash/xxhasher.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "resetBuffers" + }, + { + "args": [ + "data", + "len" + ], + "lineno": 28, + "name": "updateHash" + }, + { + "args": [], + "lineno": 43, + "name": "allocState" + }, + { + "args": [ + "data", + "len" + ], + "lineno": 50, + "name": "flushToState" + }, + { + "args": [], + "lineno": 68, + "name": "retrieveHash" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/hash/xxhasher.h.ai.md b/include/xrpl/beast/hash/xxhasher.h.ai.md new file mode 100644 index 0000000000..5e4c824d32 --- /dev/null +++ b/include/xrpl/beast/hash/xxhasher.h.ai.md @@ -0,0 +1,33 @@ +# `beast/hash/xxhasher.h` — XXH3-backed Hasher for the Beast Framework + +## Role in the System + +`xxhasher` is the default 64-bit hash engine powering XRPL's unordered containers. It sits at the base of a small but carefully designed hashing stack: `uhash` is the `std`-compatible functor used for hash maps and sets throughout the codebase, and `hardened_hash` layers random seeding on top to resist hash-flooding attacks. The class wraps the [xxHash](https://github.com/Cyan4973/xxHash) library's XXH3 algorithm, chosen for its exceptional throughput and distribution quality. + +## The Two-Path Design: Buffer vs. Streaming State + +The most significant implementation decision is the split between a small fixed buffer and a lazily allocated streaming state. The class holds a 64-byte, cache-line-aligned internal buffer (`buffer_`, `alignas(64)`) and only allocates an `XXH3_state_t` on the heap when the total accumulated input exceeds that capacity. + +When `operator()(key, len)` is called and the incoming data fits within the remaining write window, `updateHash()` simply `memcpy`s into the buffer and advances the write pointer — zero heap allocation, zero xxHash API call. Only when an update would overflow the buffer does `flushToState()` trigger: it lazily creates the `XXH3_state_t` via `allocState()`, initializes it (with or without seed), pushes whatever was already sitting in the read buffer into the streaming state, then processes the new oversized chunk. + +This matters because the vast majority of types hashed by XRPL — integer keys, account IDs, small structs — produce only a handful of `hash_append` calls totalling well under 64 bytes. Those cases never touch the heap. The 64-byte alignment of `buffer_` is deliberate: it keeps the structure on a single cache line to avoid false sharing and to facilitate potential SIMD processing by XXH3 itself. + +## Finalization and Idempotency + +`operator result_type()` calls `retrieveHash()`, which branches on whether a streaming state was ever allocated. If not (the fast path for small inputs), it calls `XXH3_64bits` or `XXH3_64bits_withSeed` directly on the buffered bytes — a single-shot hash of the read buffer with no state overhead. If a streaming state exists, it flushes any remaining buffer content into the state and calls `XXH3_64bits_digest`. + +A subtlety: the tests explicitly verify that calling `operator result_type()` twice in succession returns the same value. Tracing through `flushToState(nullptr, 0)` reveals why: after flushing the read buffer into the streaming state, `resetBuffers()` resets both spans — so the read buffer becomes empty, and a second `flushToState(nullptr, 0)` call simply pushes zero bytes into the already-updated state before `XXH3_64bits_digest` is called again, producing the same digest. This idempotency is not accidental; the test `testOperatorResultTypeDoesNotChangeInternalState` guards it explicitly. + +## Seeding + +Two constructor overloads accept unsigned seed values. Both are SFINAE-constrained to `std::is_unsigned` to prevent accidental signed-integer conversions. The two-argument form (`xxhasher(Seed seed, Seed)`) accepts but discards the second seed, retaining only the first. This two-argument signature satisfies the interface expected by `hardened_hash`, which generates a random `seed_pair` (two `uint64_t` values) via `make_seed_pair()` — but since XXH3's seeded variant only accepts a single 64-bit seed, the second value is silently dropped. The seed is stored as `std::optional` so that zero-seed and no-seed cases remain semantically distinct. + +## Endian Contract + +The public `static constexpr auto const endian = boost::endian::order::native` member is not decorative — it is part of the `hash_append` protocol defined in `hash_append.h`. The `maybe_reverse_bytes` helper checks whether a hasher's `endian` matches `boost::endian::order::native`; if it doesn't, bytes are reversed before being fed to the hasher so that the hash value is independent of the host's byte order. By advertising native endian, `xxhasher` signals that no reversal is needed, keeping the feed path as cheap as possible on any platform. + +## Resource Management + +`xxhasher` is non-copyable (copy constructor and assignment deleted) because it owns a raw `XXH3_state_t*` pointer. The destructor frees the state via `XXH3_freeState` only when it was actually allocated, keeping the destructor effectively free for the common small-input case. Move semantics are not implemented, which is acceptable here since hashers are typically constructed, used, and destroyed in a single expression via `uhash::operator()`. + +The `static_assert(sizeof(std::size_t) == 8)` guards against 32-bit platforms where a 64-bit `result_type` would silently truncate when returned as `std::size_t`, catching misconfigured build environments at compile time rather than producing subtle hash collisions at runtime. \ No newline at end of file diff --git a/include/xrpl/beast/insight/Collector.h.ai.json b/include/xrpl/beast/insight/Collector.h.ai.json new file mode 100644 index 0000000000..c7173e3421 --- /dev/null +++ b/include/xrpl/beast/insight/Collector.h.ai.json @@ -0,0 +1,161 @@ +{ + "args": [ + { + "lineno": 36, + "name": "handler" + }, + { + "lineno": 41, + "name": "handler" + }, + { + "lineno": 48, + "name": "name" + }, + { + "lineno": 51, + "name": "prefix" + }, + { + "lineno": 51, + "name": "name" + }, + { + "lineno": 61, + "name": "name" + }, + { + "lineno": 64, + "name": "prefix" + }, + { + "lineno": 64, + "name": "name" + }, + { + "lineno": 74, + "name": "name" + }, + { + "lineno": 77, + "name": "prefix" + }, + { + "lineno": 77, + "name": "name" + }, + { + "lineno": 87, + "name": "name" + }, + { + "lineno": 90, + "name": "prefix" + }, + { + "lineno": 90, + "name": "name" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "Collector" + } + ], + "description": "Defines the Collector interface for collecting various types of metrics (counters, events, gauges, meters, hooks) in the beast::insight namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/Collector.h", + "functions": [ + { + "args": [], + "lineno": 25, + "name": "~Collector" + }, + { + "args": [ + "handler" + ], + "lineno": 36, + "name": "make_hook" + }, + { + "args": [ + "handler" + ], + "lineno": 41, + "name": "make_hook" + }, + { + "args": [ + "name" + ], + "lineno": 48, + "name": "make_counter" + }, + { + "args": [ + "prefix", + "name" + ], + "lineno": 51, + "name": "make_counter" + }, + { + "args": [ + "name" + ], + "lineno": 61, + "name": "make_event" + }, + { + "args": [ + "prefix", + "name" + ], + "lineno": 64, + "name": "make_event" + }, + { + "args": [ + "name" + ], + "lineno": 74, + "name": "make_gauge" + }, + { + "args": [ + "prefix", + "name" + ], + "lineno": 77, + "name": "make_gauge" + }, + { + "args": [ + "name" + ], + "lineno": 87, + "name": "make_meter" + }, + { + "args": [ + "prefix", + "name" + ], + "lineno": 90, + "name": "make_meter" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "beast" + }, + { + "lineno": 12, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/Collector.h.ai.md b/include/xrpl/beast/insight/Collector.h.ai.md new file mode 100644 index 0000000000..e75ef44a0e --- /dev/null +++ b/include/xrpl/beast/insight/Collector.h.ai.md @@ -0,0 +1,29 @@ +# `beast/insight/Collector.h` — Metrics Collection Interface + +## Role in the System + +`Collector` is the central factory interface for the `beast::insight` metrics subsystem within rippled. It defines the contract that any metrics backend must satisfy, allowing ledger components to instrument themselves with counters, gauges, meters, events, and polling hooks — all without coupling to a specific reporting destination. Components receive a `Collector::ptr` (a `std::shared_ptr`) at construction time and use it to create metric objects; the actual reporting destination is determined entirely by which concrete implementation was injected. + +The two known implementations are `NullCollector`, which silently discards all metrics (useful for tests or when monitoring is disabled), and `StatsDCollector`, which encodes metrics in the StatsD UDP wire format and sends them to a configured server address. + +## Design: Factory + Handle Separation + +`Collector` uses a two-level design. The interface itself acts as a factory: `make_counter()`, `make_gauge()`, `make_meter()`, `make_event()`, and `make_hook()` each return a small value-type handle (`Counter`, `Gauge`, `Meter`, `Event`, `Hook`). These handles are cheap to copy and assign — they are simply `shared_ptr` wrappers around an `Impl` base class (`CounterImpl`, `GaugeImpl`, etc.). The lifetime of the underlying metric is tied to the handle: when the last copy of a handle is destroyed, the metric stops being collected. + +This separation is deliberate. The `Collector` holds shared state (network connections, aggregation buffers, collection threads), while the returned handles are per-metric tokens that instrument code can store as member variables. A component that holds a `Counter` member needs no knowledge of the `Collector`'s lifecycle beyond the initial construction call. + +## The `make_hook` Overloads and Polling Style + +The most architecturally notable factory method is `make_hook`. Rather than requiring components to push every individual metric update immediately, `Hook` supports a *polling* style: the component registers a callback that the collector fires at each collection interval on its own internal thread. This allows a class to compute and update all its metrics in one burst, which can be more efficient than individual pushes for values that change frequently. The `Collector` class provides a template `make_hook(Handler)` overload that wraps any callable into `HookImpl::HandlerType` (a `std::function`), forwarding to the virtual `make_hook(HookImpl::HandlerType const&)`. This pattern avoids virtual template methods while still giving callers a convenient, type-erased interface. + +## Prefix Namespacing + +Each `make_*` factory comes in two overloads. The single-argument form takes only a `name`; the two-argument form takes a `prefix` and a `name`, concatenating them with a dot separator (`prefix + "." + name`) before forwarding to the virtual single-argument form. When `prefix` is empty, the concatenation is skipped and the bare name is used directly. This small convenience lets subsystems build hierarchically namespaced metric names (e.g., `"ledger.fetcher.hits"`) without requiring every call site to manually construct the full string, while keeping all the real dispatch logic in a single virtual method per metric type. + +## Null Handle Safety + +All metric handle types default-construct to a "null" state with no backing `Impl`. Every mutation operation on a handle (e.g., `Counter::increment`, `Gauge::set`) guards against the null case with an `if (m_impl)` check. This means that passing a default-constructed handle — or one returned from `NullCollector` — is always safe; instrumentation code never needs special-case logic to disable itself, and the cost in the null path is a single pointer comparison. + +## Relationship to Concrete Implementations + +`NullCollector::New()` and `StatsDCollector::New(...)` are static factory functions that return `shared_ptr`, reinforcing that callers should only ever interact with the base interface. `StatsDCollector` additionally accepts an `IP::Endpoint` and a `Journal` for logging, but that complexity is fully hidden behind the `Collector` abstraction. Any component that stores a `Collector::ptr` can be tested with a `NullCollector` and deployed with a `StatsDCollector` without any changes to the component itself. \ No newline at end of file diff --git a/include/xrpl/beast/insight/Counter.h.ai.json b/include/xrpl/beast/insight/Counter.h.ai.json new file mode 100644 index 0000000000..e8927015c2 --- /dev/null +++ b/include/xrpl/beast/insight/Counter.h.ai.json @@ -0,0 +1,98 @@ +{ + "args": [ + { + "lineno": 37, + "name": "amount" + }, + { + "lineno": 43, + "name": "amount" + }, + { + "lineno": 49, + "name": "amount" + } + ], + "classes": [ + { + "args": [ + "()", + "std::shared_ptr const& impl" + ], + "lineno": 13, + "name": "Counter" + } + ], + "description": "Defines the beast::insight::Counter class, a lightweight reference wrapper for a metric that measures an integral value, allowing increment and decrement operations for metrics collection.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/Counter.h", + "functions": [ + { + "args": [], + "lineno": 19, + "name": "Counter" + }, + { + "args": [ + "std::shared_ptr const& impl" + ], + "lineno": 27, + "name": "Counter" + }, + { + "args": [ + "value_type amount" + ], + "lineno": 36, + "name": "increment" + }, + { + "args": [ + "value_type amount" + ], + "lineno": 42, + "name": "operator+=" + }, + { + "args": [ + "value_type amount" + ], + "lineno": 48, + "name": "operator-=" + }, + { + "args": [], + "lineno": 54, + "name": "operator++" + }, + { + "args": [ + "int" + ], + "lineno": 60, + "name": "operator++" + }, + { + "args": [], + "lineno": 66, + "name": "operator--" + }, + { + "args": [ + "int" + ], + "lineno": 72, + "name": "operator--" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 7, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/Counter.h.ai.md b/include/xrpl/beast/insight/Counter.h.ai.md new file mode 100644 index 0000000000..0016a7bb36 --- /dev/null +++ b/include/xrpl/beast/insight/Counter.h.ai.md @@ -0,0 +1,43 @@ +# `beast/insight/Counter.h` — Lightweight Metric Handle for Integral Counting + +`Counter.h` defines the `beast::insight::Counter` class, a thin reference-counted handle used to track and report integer-valued metrics through the XRPL node's telemetry pipeline. It lives in the `beast::insight` subsystem alongside `Gauge`, `Meter`, `Event`, and `Hook` — a family of metric types that a `Collector` (most concretely `StatsDCollector`) exports to an external monitoring backend such as StatsD. + +## Role in the Insight System + +The `beast::insight` module follows a handle-body pattern throughout. Every user-facing metric type (`Counter`, `Gauge`, `Meter`, etc.) is a copyable, value-semantics wrapper around a `shared_ptr` to an abstract `*Impl` base class. `Counter` holds a `std::shared_ptr`, where `CounterImpl` is an abstract interface with a single pure-virtual method, `increment(value_type amount)`. The concrete implementation — wiring updates over UDP to a StatsD server — lives inside `StatsDCollector` and is invisible to callers. + +This design is intentional: application code interacts only with `Counter`, never with the underlying transport, so swapping the collector (e.g., to `NullCollector` for testing) requires no changes to the code emitting metrics. + +## Handle Semantics and Lifetime + +`Counter` is explicitly designed as a "lightweight reference wrapper which is cheap to copy and assign." Copying a `Counter` merely copies the `shared_ptr`, incrementing the reference count — both copies report to the same underlying metric. When the last `Counter` referencing a particular `CounterImpl` is destroyed, the `shared_ptr` reference count drops to zero and the metric is de-registered; the collector stops collecting it. This lifetime coupling means a subsystem can stop being monitored simply by letting its `Counter` member go out of scope, without any explicit unregistration call. + +The null-object idiom is also embedded directly. The default constructor leaves `m_impl` empty, and every mutating operation guards with `if (m_impl)` before dispatching. A default-constructed `Counter` silently accepts all arithmetic operations without crashing or emitting anything — useful in contexts where metric collection is optional or the collector is absent. + +## Operator Design — `const` on a Mutable Handle + +A subtle but important design choice: all arithmetic operators (`operator+=`, `operator-=`, `operator++`, `operator--`) and `increment()` are declared `const`. This might seem contradictory since these operations modify external state. However, `const` here applies to the handle itself (the `shared_ptr`), not the resource it points to. The pointer is not reseated; only the metric value behind it changes. This allows a `Counter` stored as a `const` member or accessed through a `const` reference to still emit updates — the right behavior for a metric that must be updated from within a logically `const` operation. + +## Bidirectional Counting vs. Gauges + +Despite the name "counter" suggesting a monotonically increasing value, `Counter` supports both increment and decrement via `operator-=` and `operator--`, which forward to `increment(-amount)`. The class comment describes it as "a gauge calculated at the server" — the distinction from `Gauge` (defined in `Gauge.h`) is conceptual: a `Gauge` is an absolute, instantaneous value set directly by the caller, while a `Counter` is adjusted relative to its current value and the server (StatsD backend) tracks the running total. In StatsD terminology, this maps to the "gauge with delta" or "counter" metric types. + +## `CounterImpl` Interface + +`CounterImpl` (in `CounterImpl.h`) inherits `std::enable_shared_from_this` and exposes only `virtual void increment(value_type amount) = 0` with a pure-virtual destructor. The `value_type` is `std::int64_t`, giving the full signed 64-bit range for both positive and negative adjustments. The `enable_shared_from_this` base is present so concrete implementations can safely pass `shared_ptr`s to themselves when registering with the collector's internal bookkeeping. + +## Usage Pattern + +Callers never construct `Counter` directly from a `CounterImpl`. Instead, they obtain one from a `Collector` via `make_counter(name)` or `make_counter(prefix, name)`, which builds the dotted metric path and returns a fully initialized `Counter`. The `Counter` is then stored as a member and incremented at the appropriate call sites: + +```cpp +// In some subsystem's constructor: +m_requestCounter = collector->make_counter("app", "requests"); + +// At a call site: +++m_requestCounter; // or +m_requestCounter += 5; // or +m_requestCounter.increment(n); +``` + +The entire metric emission path — from `Counter::increment()` through `CounterImpl::increment()` to the StatsD UDP packet — is thus hidden behind this two-file abstraction, keeping instrumented code clean and the telemetry backend fully substitutable. \ No newline at end of file diff --git a/include/xrpl/beast/insight/CounterImpl.h.ai.json b/include/xrpl/beast/insight/CounterImpl.h.ai.json new file mode 100644 index 0000000000..58936ff4a0 --- /dev/null +++ b/include/xrpl/beast/insight/CounterImpl.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [ + { + "lineno": 14, + "name": "amount" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "CounterImpl" + } + ], + "description": "Defines the CounterImpl abstract base class for implementing a counter metric in the beast::insight namespace, providing an interface for incrementing the counter and managing its lifetime via shared pointers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/CounterImpl.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "~CounterImpl" + }, + { + "args": [ + "amount" + ], + "lineno": 14, + "name": "increment" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + }, + { + "lineno": 5, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/CounterImpl.h.ai.md b/include/xrpl/beast/insight/CounterImpl.h.ai.md new file mode 100644 index 0000000000..da6dfe9406 --- /dev/null +++ b/include/xrpl/beast/insight/CounterImpl.h.ai.md @@ -0,0 +1,31 @@ +# `CounterImpl.h` — Abstract Backend Interface for Counter Metrics + +## Role in the System + +`CounterImpl.h` defines the pure-virtual base class that backs the `Counter` metric handle in the `beast::insight` telemetry framework. The `beast::insight` layer provides a thin abstraction over external metric backends (primarily StatsD), letting XRPL subsystems emit measurements without coupling to any specific transport. `CounterImpl` is the seam between the public-facing `Counter` handle and whichever concrete backend is in use. + +The file is intentionally minimal — 23 lines — because it exists purely to express the contract a backend must satisfy. All business logic lives either in `Counter.h` (the handle) or in the concrete implementations such as `StatsDCounterImpl` in `StatsDCollector.cpp`. + +## Design of `CounterImpl` + +The class inherits `std::enable_shared_from_this`, which is the central lifetime-management mechanism for the entire insight metric system. `Counter` (the public handle) holds a `std::shared_ptr` as its only data member. When the last `Counter` copy referencing a given impl is destroyed, the `shared_ptr` refcount drops to zero, triggering the impl's destructor and automatically unregistering the metric from the backend. In `StatsDCounterImpl`, the destructor calls `m_impl->remove(*this)`, cleanly deregistering itself from the collector's tracking list. The `shared_from_this()` pattern is exploited further inside `increment()` implementations to safely capture a strong reference when posting asynchronous work items (see below). + +The pure virtual destructor (`virtual ~CounterImpl() = 0`) makes the class abstract while still permitting destruction through a base pointer — the standard C++ idiom when no other method alone is sufficient to force abstract status. Here it is the right choice: `increment` could have been the sole pure virtual, but marking the destructor pure as well clearly communicates that `CounterImpl` must not be instantiated directly. + +## The Single Operation: `increment` + +`increment(value_type amount)` is the entire mutation surface. `value_type` is `std::int64_t`, which means a single method covers both incrementing (positive `amount`) and decrementing (negative `amount`). This is a deliberate simplification relative to `GaugeImpl`, which exposes separate `set()` and `increment()` methods because a gauge can be assigned an absolute value. A counter, by contrast, is always adjusted relatively — it accumulates a running delta. Exposing only `increment` prevents misuse and keeps implementations simpler. + +`Counter.h` translates the full operator interface (`+=`, `-=`, `++`, `--`) entirely in terms of `increment`, so backend implementations never need to handle those cases. The mapping is straightforward: `operator--` calls `increment(-1)`, `operator-=` calls `increment(-amount)`. + +## Null-Safety Pattern + +`Counter`'s `increment` method guards the call with `if (m_impl)` before dispatching. This means a default-constructed `Counter` (holding a null `shared_ptr`) silently drops all operations. This null-object pattern avoids the need for callers to check whether a metric was actually created, which matters in contexts where a `NullCollector` is installed and no real backend exists. + +## Asynchronous Dispatch in the Concrete Implementation + +Although not visible in this header, it is worth noting what `increment` must accommodate in real backends. `StatsDCounterImpl::increment` immediately dispatches onto the collector's Boost.Asio I/O context via `boost::asio::dispatch`, capturing a strong `shared_ptr` to itself via `shared_from_this()`. The actual mutation (`m_value += amount; m_dirty = true;`) runs on the I/O thread, making the operation thread-safe without explicit locking. This asynchronous design is why `enable_shared_from_this` is part of the base class rather than just the concrete class: the dispatch lambda must extend the object's lifetime until the posted handler executes, and only a `shared_ptr` can guarantee that. + +## Relationship to Sibling Interfaces + +`CounterImpl` is structurally parallel to `GaugeImpl`, `EventImpl`, `MeterImpl`, and `HookImpl`, all of which follow the same `enable_shared_from_this` + pure-virtual-interface pattern. The `Collector` interface produces concrete impls via factory methods (`make_counter`, `make_gauge`, etc.), wrapping them in the corresponding handle types. `CounterImpl` specifically sits at the narrowest point in this hierarchy: one method, one type alias, one abstract class. \ No newline at end of file diff --git a/include/xrpl/beast/insight/Event.h.ai.json b/include/xrpl/beast/insight/Event.h.ai.json new file mode 100644 index 0000000000..d6ad087ba8 --- /dev/null +++ b/include/xrpl/beast/insight/Event.h.ai.json @@ -0,0 +1,52 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "()", + "std::shared_ptr const& impl" + ], + "lineno": 18, + "name": "Event" + } + ], + "description": "Defines the Event class, a lightweight reference wrapper for reporting event timing metrics in the beast::insight namespace. Supports push-style event notifications with associated timing values.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/Event.h", + "functions": [ + { + "args": [], + "lineno": 23, + "name": "Event" + }, + { + "args": [ + "std::shared_ptr const& impl" + ], + "lineno": 30, + "name": "Event" + }, + { + "args": [ + "std::chrono::duration const& value" + ], + "lineno": 39, + "name": "notify" + }, + { + "args": [], + "lineno": 47, + "name": "impl" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + }, + { + "lineno": 8, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/Event.h.ai.md b/include/xrpl/beast/insight/Event.h.ai.md new file mode 100644 index 0000000000..8a604fd366 --- /dev/null +++ b/include/xrpl/beast/insight/Event.h.ai.md @@ -0,0 +1,52 @@ +# `beast/insight/Event.h` — Push-Style Timing Metric + +## Role in the System + +`Event` lives within the `beast::insight` metrics framework, which provides a uniform abstraction for exporting runtime telemetry from XRPL node subsystems. The framework defines five metric kinds — `Counter`, `Event`, `Gauge`, `Hook`, and `Meter` — all obtained through the `Collector` interface and forwarded to a backend such as `StatsDCollector`. `Event` specifically models a discrete occurrence that has an associated elapsed duration: for example, the time taken to process a transaction or validate a ledger. It complements `Counter` (a cumulative integer tally) and `Gauge` (an instantaneous snapshot) by capturing the *timing* of individual operations rather than their count or current magnitude. + +## Design: Reference Wrapper over a Polymorphic Implementation + +`Event` follows the same split-implementation pattern used throughout the insight module: a thin, value-semantic handle class pairs with an abstract `EventImpl` that backends override. `EventImpl` inherits `std::enable_shared_from_this` and exposes a single pure-virtual method: + +```cpp +virtual void notify(std::chrono::milliseconds const& value) = 0; +``` + +`Event` itself holds a `std::shared_ptr` as its only data member. This means `Event` is cheap to copy, safe to store by value anywhere in the codebase, and naturally ref-counted: when the last copy of an `Event` handle is destroyed, the `shared_ptr` refcount drops to zero and the implementation is torn down, stopping collection automatically without any explicit unregister call. + +## Null Metric Pattern + +A default-constructed `Event` has a null `m_impl`. Every method that touches the implementation guards with `if (m_impl)`, so a null `Event` silently no-ops. This is deliberate: call sites don't need to check whether a collector was actually configured. Code paths that run without a live collector — unit tests, minimal deployments — construct a null `Event` (or receive one from `NullCollector`) and incur no overhead beyond the branch. + +## `notify()` and Duration Coercion + +The only mutation method is `notify()`, a template accepting any `std::chrono::duration`: + +```cpp +template +void notify(std::chrono::duration const& value) const { + if (m_impl) + m_impl->notify(ceil(value)); +} +``` + +The internal `value_type` is `std::chrono::milliseconds` (defined in `EventImpl`). `notify()` uses `std::chrono::ceil` — not `duration_cast` — to convert the caller's duration to milliseconds. The choice of `ceil` over truncation is defensive: it ensures a sub-millisecond event is reported as 1 ms rather than silently disappearing as 0 ms, which would skew histograms in a StatsD backend. Callers can pass nanoseconds, microseconds, or any other duration unit; the coercion is automatic and lossless in the rounding-up direction. + +## Obtaining an `Event` + +`Event` objects are not constructed directly in application code. Instead, the `Collector` interface provides factory overloads: + +```cpp +virtual Event make_event(std::string const& name) = 0; +Event make_event(std::string const& prefix, std::string const& name); +``` + +The two-argument overload concatenates `prefix + "." + name` before delegating to the single-argument virtual, which is a convenience for subsystems that namespace their metrics hierarchically. The returned `Event` wraps whichever concrete `EventImpl` the backend supplies. + +## Push vs. Pull Semantics + +Unlike `Gauge` or `Hook`-based metrics — which support a polling model where the collector calls back into application state on each reporting interval — `Event` is strictly push-only. The owner calls `notify()` at the moment the timed operation completes, and the value is forwarded immediately to the backend. This makes sense architecturally: an event represents a point in time, so there is nothing to poll; waiting for a collection interval would conflate multiple independent events or lose the individual timing data entirely. + +## Relationship to Sibling Types + +All metric handle types in `beast/insight/` share the same structural idiom: a `final` wrapper class holding a `shared_ptr` to an abstract `*Impl`, with a null-safe default constructor. `Event`'s uniqueness is its push-only, time-valued interface. `Counter` supports increment/decrement operators for cumulative tracking; `Gauge` allows setting an arbitrary integer snapshot; `Meter` measures a rate. `Event` is the right choice when what matters is how long a specific operation took, reported exactly when it completes. \ No newline at end of file diff --git a/include/xrpl/beast/insight/EventImpl.h.ai.json b/include/xrpl/beast/insight/EventImpl.h.ai.json new file mode 100644 index 0000000000..5a1e8a5374 --- /dev/null +++ b/include/xrpl/beast/insight/EventImpl.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [ + { + "lineno": 14, + "name": "value" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "EventImpl" + } + ], + "description": "Defines the EventImpl abstract base class for event timing metrics in the beast::insight namespace, providing an interface for notifying event occurrences with millisecond precision.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/EventImpl.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "~EventImpl" + }, + { + "args": [ + "value" + ], + "lineno": 14, + "name": "notify" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "beast" + }, + { + "lineno": 6, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/EventImpl.h.ai.md b/include/xrpl/beast/insight/EventImpl.h.ai.md new file mode 100644 index 0000000000..2ca0d40228 --- /dev/null +++ b/include/xrpl/beast/insight/EventImpl.h.ai.md @@ -0,0 +1,31 @@ +# `EventImpl.h` — Abstract Interface for Event Timing Metrics + +## Role in the System + +`EventImpl` is the pure abstract base class at the heart of the `beast::insight` event metric type. Within the `insight` subsystem, "events" represent operations that have an associated duration — things like transaction processing time or validation latency. This file defines the minimal contract every concrete event backend must satisfy: accept a millisecond-precision timing value whenever such an event is fired. + +The file is intentionally minimal, containing only the abstract interface so that the front-facing handle (`Event`) and the concrete back-end implementations can be compiled and evolved independently. The same split-interface pattern is applied uniformly across the insight subsystem — compare `CounterImpl` (uses `int64_t` for incrementable counts) and `GaugeImpl` (tracks a current level), each following the same two-file structure. + +## Design of `EventImpl` + +`EventImpl` inherits `std::enable_shared_from_this`. This is not incidental: the only meaningful concrete implementation, `StatsDEventImpl` in `StatsDCollector.cpp`, dispatches `notify()` calls asynchronously onto a `boost::asio` I/O context. Inside the dispatch, it captures `shared_from_this()` to extend the object's lifetime across the async hop. Were `EventImpl` not `enable_shared_from_this`, the derived class could not safely call `std::static_pointer_cast(shared_from_this())` in `notify()` — the implementation would either be forced into an awkward workaround or risk a use-after-free if the last `Event` handle dropped just before the async callback ran. + +The pure-virtual destructor `virtual ~EventImpl() = 0` ensures the class is abstract while still permitting correct destruction of derived objects through a base pointer. The single pure-virtual method is: + +```cpp +virtual void notify(value_type const& value) = 0; +``` + +where `value_type` is `std::chrono::milliseconds`. Using a concrete `chrono` duration type rather than a raw integer makes the unit explicit and eliminates a class of silent scaling bugs. The user-facing `Event` wrapper reinforces this by accepting any `std::chrono::duration` and converting to milliseconds using `std::chrono::ceil` before forwarding to `notify()` — rounding up so that sub-millisecond events register as at least 1 ms rather than silently disappearing as zero. + +## Relationship to `Event` and `StatsDEventImpl` + +`Event` is a lightweight, copyable reference handle that owns a `shared_ptr`. It is the type callers hold and call `notify()` on. When the last `Event` copy is destroyed, the `shared_ptr` refcount drops to zero and the `EventImpl` is cleaned up. This design deliberately ties metric collection lifetime to the objects that own the metric handle, avoiding the need for explicit registration or deregistration. + +`StatsDCollector::make_event()` constructs a `StatsDEventImpl` — the only non-null concrete implementation — wraps it in a `shared_ptr`, and returns it via an `Event` handle. `StatsDEventImpl::notify()` posts work to the collector's I/O context via `boost::asio::dispatch`, then `do_notify()` formats the value in StatsD wire format (`prefix.name:count|ms`) and hands it to the collector's send buffer. The `|ms` type tag is the StatsD convention for timing samples. + +A `NullCollector` also exists and produces no-op `Event` objects — the `Event` default constructor sets its `m_impl` to `nullptr`, and `Event::notify()` silently skips the call when `m_impl` is null. This lets production code accept a collector reference without needing `#ifdef`-style guards. + +## Summary + +`EventImpl.h` is a deliberately small file — a focused contract between the public `Event` handle and whatever backend is collecting metrics. Its three design decisions that carry real weight are: inheriting `enable_shared_from_this` to support async lifetime extension, adopting `std::chrono::milliseconds` as `value_type` for type-safe duration reporting, and declaring a pure-virtual destructor to enforce abstractness while preserving correct polymorphic deletion. \ No newline at end of file diff --git a/include/xrpl/beast/insight/Gauge.h.ai.json b/include/xrpl/beast/insight/Gauge.h.ai.json new file mode 100644 index 0000000000..a66e643634 --- /dev/null +++ b/include/xrpl/beast/insight/Gauge.h.ai.json @@ -0,0 +1,124 @@ +{ + "args": [ + { + "lineno": 39, + "name": "value" + }, + { + "lineno": 45, + "name": "value" + }, + { + "lineno": 53, + "name": "amount" + }, + { + "lineno": 58, + "name": "amount" + }, + { + "lineno": 63, + "name": "amount" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const& impl" + ], + "lineno": 15, + "name": "Gauge" + } + ], + "description": "Defines the Gauge class, a lightweight reference wrapper for an integral metric that allows instantaneous measurement and adjustment of a value, typically used for metrics collection.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/Gauge.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "Gauge" + }, + { + "args": [ + "std::shared_ptr const& impl" + ], + "lineno": 29, + "name": "Gauge" + }, + { + "args": [ + "value_type value" + ], + "lineno": 39, + "name": "set" + }, + { + "args": [ + "value_type value" + ], + "lineno": 45, + "name": "operator=" + }, + { + "args": [ + "difference_type amount" + ], + "lineno": 53, + "name": "increment" + }, + { + "args": [ + "difference_type amount" + ], + "lineno": 58, + "name": "operator+=" + }, + { + "args": [ + "difference_type amount" + ], + "lineno": 63, + "name": "operator-=" + }, + { + "args": [], + "lineno": 68, + "name": "operator++" + }, + { + "args": [ + "int" + ], + "lineno": 73, + "name": "operator++" + }, + { + "args": [], + "lineno": 78, + "name": "operator--" + }, + { + "args": [ + "int" + ], + "lineno": 83, + "name": "operator--" + }, + { + "args": [], + "lineno": 88, + "name": "impl" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + }, + { + "lineno": 8, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/Gauge.h.ai.md b/include/xrpl/beast/insight/Gauge.h.ai.md new file mode 100644 index 0000000000..30ca4be323 --- /dev/null +++ b/include/xrpl/beast/insight/Gauge.h.ai.md @@ -0,0 +1,34 @@ +# `include/xrpl/beast/insight/Gauge.h` + +## Role in the System + +`Gauge` is one of the core metric types in the `beast::insight` metrics subsystem — the instrumentation layer that feeds runtime telemetry from the rippled node to external monitoring systems (primarily StatsD). A gauge models an instantaneous snapshot of a numeric value: queue depth, connected peer count, memory pressure, or any other measurement that can go up or down freely. Unlike a `Counter` (which only accumulates increments), a gauge supports both absolute assignment and signed relative adjustment. + +## Design Pattern: Handle-Body with Shared Ownership + +The class is a thin handle over a `std::shared_ptr`, following the same pimpl-via-shared_ptr pattern used by every metric type in this subsystem (`Counter`, `Event`, `Meter`). The handle is deliberately cheap: copying or moving a `Gauge` is just a reference-count bump on the shared impl. The metric remains registered with the collector only as long as at least one `Gauge` handle keeps the `shared_ptr` alive. When the last handle is destroyed, the impl's destructor fires and the metric is automatically unregistered — no explicit cleanup required. This lifetime semantics makes it safe to embed `Gauge` members directly in objects without lifecycle ceremony. + +## Null State + +Default construction yields a null gauge (`m_impl` is empty). Every mutating operation guards on `if (m_impl)` before delegating, so null gauges silently absorb all calls. This is the appropriate behavior for code paths where a collector may not be configured (e.g., unit tests using `NullCollector`). Callers never need to check whether a gauge is valid before using it. + +## `const` on Mutating Methods + +All mutation methods — `set()`, `increment()`, and every arithmetic operator — are marked `const`. This is intentional: `const` here applies to the *handle*, not to the underlying metric value stored inside the impl. A `const Gauge` cannot be reseated to a different impl, but it can still update the metric it points to. The design mirrors how `const std::shared_ptr` prevents pointer reassignment while leaving the pointed-to object mutable. + +## Value Types + +`GaugeImpl` defines `value_type` as `std::uint64_t` and `difference_type` as `std::int64_t`. The unsigned value type means the gauge cannot represent negative quantities, which suits typical server metrics. The signed difference type allows natural expressions like `gauge -= 5` when reducing a count. The `StatsDGaugeImpl` implementation in `StatsDCollector.cpp` handles edge cases explicitly: overflow is clamped to `UINT64_MAX` and underflow is clamped to `0`, preventing wrap-around bugs when increments arrive with bad signs. + +## `operator=` as Value Assignment + +`operator=(value_type)` overloads the assignment operator to mean "set the gauge to this absolute value", enabling the expressive `gauge = 42;` syntax. This is the only overload — no `operator=(Gauge const&)` is suppressed, so the compiler-generated copy-assignment still copies the handle (sharing the same impl). This design lets gauge handles be stored in standard containers while still supporting natural numeric-assignment syntax. + +## Relationship to `GaugeImpl` and Collector + +`GaugeImpl` (in `GaugeImpl.h`) is an abstract base with `enable_shared_from_this`, declaring only `set()` and `increment()` as pure virtual. Two concrete implementations exist: + +- **`NullGaugeImpl`** (in `NullCollector.cpp`): Both methods are empty no-ops. Used when the system runs without a metrics backend. +- **`StatsDGaugeImpl`** (in `StatsDCollector.cpp`): Delegates `set()` and `increment()` via `boost::asio::dispatch` to a dedicated I/O thread, where the actual value is maintained with dirty-flag tracking. Only changed values are flushed to the StatsD UDP stream at each collection interval, avoiding unnecessary wire traffic. + +`Gauge` handles are always created through `Collector::make_gauge(name)` — the public factory — not by constructing them directly. The explicit `impl()`-taking constructor is marked `explicit` precisely to prevent accidental construction outside of collector implementations. \ No newline at end of file diff --git a/include/xrpl/beast/insight/GaugeImpl.h.ai.json b/include/xrpl/beast/insight/GaugeImpl.h.ai.json new file mode 100644 index 0000000000..6542d62c24 --- /dev/null +++ b/include/xrpl/beast/insight/GaugeImpl.h.ai.json @@ -0,0 +1,44 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "GaugeImpl" + } + ], + "description": "Defines the GaugeImpl abstract base class for representing a gauge metric, which can be set or incremented, within the beast::insight metrics framework.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/GaugeImpl.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "~GaugeImpl" + }, + { + "args": [ + "value" + ], + "lineno": 14, + "name": "set" + }, + { + "args": [ + "amount" + ], + "lineno": 16, + "name": "increment" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + }, + { + "lineno": 5, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/GaugeImpl.h.ai.md b/include/xrpl/beast/insight/GaugeImpl.h.ai.md new file mode 100644 index 0000000000..7067ca845c --- /dev/null +++ b/include/xrpl/beast/insight/GaugeImpl.h.ai.md @@ -0,0 +1,32 @@ +# `GaugeImpl.h` — Abstract Interface for Gauge Metric Implementations + +`GaugeImpl` is the abstract backend interface for gauge-type metrics within the `beast::insight` telemetry framework. It sits at the boundary between the user-facing `Gauge` handle class and any concrete metric-reporting backend (such as `StatsDCollector` or `NullCollector`), following a consistent bridge pattern used throughout the `insight` module. + +## Role in the `beast::insight` Framework + +The `insight` namespace implements a lightweight, pluggable metrics system. Every metric type — counter, event, gauge, meter — is split into two layers: a thin, copyable handle (`Gauge`) that application code holds and uses, and a polymorphic implementation object (`GaugeImpl`) that the concrete `Collector` backend creates and owns. `GaugeImpl.h` defines the contract that any backend must satisfy to support gauge metrics. + +This separation means application code never depends directly on whether metrics go to StatsD, a log file, or nowhere at all. The `Gauge` handle just delegates to whatever `GaugeImpl` it was given — or silently does nothing if it holds a null `shared_ptr` (the "null metric" case, where `Gauge` was default-constructed). + +## Interface Design + +`GaugeImpl` declares exactly two operations: + +- `set(value_type value)` — assigns an absolute value to the gauge (e.g. "current queue depth is 47"). +- `increment(difference_type amount)` — adjusts the gauge by a signed delta (positive or negative). + +The type aliases express a deliberate design choice: `value_type` is `std::uint64_t` (unsigned, because a gauge represents a non-negative quantity like a count or size) while `difference_type` is `std::int64_t` (signed, because an adjustment can go either direction). This mirrors the `std::vector::size_type` / `std::ptrdiff_t` convention and prevents callers from accidentally passing a negative absolute value while still allowing bidirectional relative adjustment. + +The destructor is declared pure virtual (`= 0`) even though it must have a definition, which is C++'s standard idiom for forcing the class to be abstract while still permitting proper virtual destruction through a base pointer. Concrete subclasses must call `GaugeImpl::~GaugeImpl()` implicitly through their own destructors, which the linker provides as an out-of-line symbol. + +## Ownership and Lifetime via `enable_shared_from_this` + +`GaugeImpl` inherits from `std::enable_shared_from_this`. This allows a `GaugeImpl` subclass to produce a `shared_ptr` to itself — a necessity for implementations that need to register themselves with a background collection loop or scheduler inside the `Collector`. When the last `Gauge` handle goes out of scope, the `shared_ptr` reference count drops to zero and the metric is destroyed; the collector stops reporting it. This is the "last reference = metric disappears" lifetime semantics documented in `Gauge.h`. + +## Relationship to `Gauge` + +`Gauge` is the class that application code actually uses. It wraps a `shared_ptr` and exposes the same `set` / `increment` operations plus arithmetic operator overloads (`=`, `+=`, `-=`, `++`, `--`). Every operator first checks `if (m_impl)` before delegating, so a null-constructed `Gauge` is completely safe to use — all operations silently no-op. `Gauge` includes `GaugeImpl.h` directly and aliases its `value_type` and `difference_type`, so the numeric types are defined in exactly one place. + +## Parallel Structure Across the Module + +The same two-file pattern — a lightweight handle and a `*Impl` pure virtual base — is used for every metric kind in `insight`: `Counter`/`CounterImpl`, `Event`/`EventImpl`, `Gauge`/`GaugeImpl`, `Hook`/`HookImpl`, and `Meter`/`MeterImpl`. `CounterImpl`, for comparison, only exposes `increment` (no `set`), because counters are monotonically accumulating; gauges are the appropriate choice when a value can go up and down arbitrarily. The `Collector` interface (in `Collector.h`) is the factory that creates all these objects: `make_gauge(name)` returns a fully wired `Gauge` wrapping a backend-specific `GaugeImpl` subclass. \ No newline at end of file diff --git a/include/xrpl/beast/insight/Group.h.ai.json b/include/xrpl/beast/insight/Group.h.ai.json new file mode 100644 index 0000000000..e679595dcb --- /dev/null +++ b/include/xrpl/beast/insight/Group.h.ai.json @@ -0,0 +1,30 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "Group" + } + ], + "description": "Defines the Group class, a front-end collector that manages a group of metrics, inheriting from Collector and providing an interface to get the group's name.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/Group.h", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "name" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 7, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/Group.h.ai.md b/include/xrpl/beast/insight/Group.h.ai.md new file mode 100644 index 0000000000..1bc6526b6b --- /dev/null +++ b/include/xrpl/beast/insight/Group.h.ai.md @@ -0,0 +1,34 @@ +# `beast/insight/Group.h` — Namespaced Metric Collector Front-End + +`Group` is a minimal abstract interface within the `beast::insight` metrics subsystem that extends `Collector` with a single piece of identity: a name. Its purpose is to enable namespace isolation for metrics emitted by different XRPL subsystems without requiring each subsystem to manage its own metric prefixing. + +## Role in the Insight Subsystem + +`Collector` is the base factory interface through which code creates metric objects — `Counter`, `Gauge`, `Event`, `Meter`, and `Hook`. Any class that wants to export metrics accepts a `Collector::ptr` in its constructor and calls `make_counter()`, `make_gauge()`, and so on. + +`Group` IS-A `Collector` by inheritance, making it a transparent drop-in wherever a `Collector::ptr` is accepted. The additional `name()` method simply records which logical subsystem the group belongs to. The real work happens in the concrete implementation `detail::GroupImp` (defined in `Groups.cpp`), which intercepts every `make_*` call and prepends the group name as a dot-separated prefix: + +```cpp +std::string make_name(std::string const& name) { + return m_name + "." + name; +} +Counter make_counter(std::string const& name) override { + return m_collector->make_counter(make_name(name)); +} +``` + +So if a `Group` named `"consensus"` creates a counter called `"proposals"`, the metric arrives at the underlying `Collector` (e.g., `StatsDCollector`) as `"consensus.proposals"`. The subsystem calling `make_counter("proposals")` never needs to know its own prefix. + +## The `Groups` Factory and Lifecycle + +`Group` instances are not created directly; they are produced by `Groups`, a companion interface (`Groups.h`) that acts as a named registry. `Groups::get(name)` returns a `Group::ptr`, creating a new `GroupImp` if one does not already exist for that name, or returning the cached instance. The `operator[]` convenience overload delegates to `get`. The free function `make_Groups(Collector::ptr)` constructs a `GroupsImp` backed by a specific underlying collector. + +At the application layer, `CollectorManager` (in `app/main/CollectorManager.h`) surfaces both the raw `collector()` and a `group(name)` accessor, giving XRPL subsystems a uniform entry point to either a flat metric namespace or a prefixed group namespace. + +## Design Rationale + +The key design decision is that `Group` inherits from `Collector` rather than wrapping it with a separate interface. This means every existing subsystem API that accepts a `Collector::ptr` automatically supports grouped metrics: pass a `Group::ptr` and all metrics created through it will be namespaced without any code change at the call site. There is no need for a separate "grouped collector" concept or adapter pattern. + +The `name()` method is explicitly scoped for diagnostics rather than metric construction. The name is used in `GroupImp::make_name()` for prefix assembly, but the public API documents it as a diagnostic aid, keeping the interface contract focused. The `shared_ptr` alias `Group::ptr` follows the same convention as `Collector::ptr` throughout the subsystem, making ownership semantics uniform and cacheable inside `GroupsImp::m_items`. + +The combination of a short abstract header (`Group.h`) and a single implementation file (`Groups.cpp`) that hosts both `GroupImp` and `GroupsImp` keeps the concrete details hidden behind the ABI boundary. Callers only ever hold a `Group::ptr` and see the `Collector` interface; the prefix logic is an implementation detail invisible to metric-emitting subsystems. \ No newline at end of file diff --git a/include/xrpl/beast/insight/Groups.h.ai.json b/include/xrpl/beast/insight/Groups.h.ai.json new file mode 100644 index 0000000000..3c6acd1a63 --- /dev/null +++ b/include/xrpl/beast/insight/Groups.h.ai.json @@ -0,0 +1,59 @@ +{ + "args": [ + { + "lineno": 16, + "name": "name" + }, + { + "lineno": 19, + "name": "name" + }, + { + "lineno": 29, + "name": "collector" + } + ], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "Groups" + } + ], + "description": "Defines the Groups class, a container for managing a set of metric groups, and declares a factory function to create such a container using a specified collector.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/Groups.h", + "functions": [ + { + "args": [ + "name" + ], + "lineno": 16, + "name": "get" + }, + { + "args": [ + "name" + ], + "lineno": 19, + "name": "operator[]" + }, + { + "args": [ + "collector" + ], + "lineno": 29, + "name": "make_Groups" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + }, + { + "lineno": 8, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/Groups.h.ai.md b/include/xrpl/beast/insight/Groups.h.ai.md new file mode 100644 index 0000000000..99b2079c8e --- /dev/null +++ b/include/xrpl/beast/insight/Groups.h.ai.md @@ -0,0 +1,27 @@ +# `include/xrpl/beast/insight/Groups.h` + +## Role in the System + +`Groups.h` defines the `Groups` abstract interface, a registry that creates and caches named metric namespaces within the `beast::insight` telemetry subsystem. Its purpose is to let different XRPLD subsystems each claim a named prefix under a shared `Collector` without interfering with one another's metric names. Every module that wants to emit counters, gauges, events, or meters obtains a `Group` object through this registry, and all metrics it creates are automatically scoped under that group's name. + +## The `Groups` Interface + +`Groups` is a pure abstract class with a single virtual method, `get(std::string const& name)`, plus a convenience `operator[]` that forwards to it. Both return `Group::ptr const&` — a reference into the internal map — rather than a copy, intentionally avoiding shared-pointer reference-count overhead on every lookup. This is safe because the `Groups` instance in `CollectorManager` is held for the entire lifetime of the application, so the map entry (and the referenced `shared_ptr`) is guaranteed to outlive all callers. + +The factory function `make_Groups(Collector::ptr const& collector)` is the only way to obtain a concrete instance. It returns `std::unique_ptr`, signaling that ownership is exclusive and the resulting object is not meant to be shared. + +## Concrete Implementation (`Groups.cpp`) + +The implementation lives in `detail::GroupsImp`, which stores a `Collector::ptr` and an `unordered_map, uhash<>>`. The `get()` method uses `emplace` to perform a single-lookup upsert: `std::pair` captures whether the key was newly inserted, and only in that case is a new `GroupImp` constructed. This avoids the find-then-insert double-lookup that a naïve `count` + `operator[]` pattern would incur. + +`detail::GroupImp` implements the `Group` interface (which itself extends `Collector`). It stores the group name and the underlying `Collector`, and overrides every metric-creation method to prepend `name + "."` to each metric name via `make_name()`. For example, a call to `group->make_counter("tx_count")` on a group named `"ledger"` results in the metric `"ledger.tx_count"` being registered with the real collector. This dot-separated hierarchy maps directly to StatsD's naming convention, enabling hierarchical dashboards without callers needing to manage prefixes manually. Hooks are an exception: they are forwarded to the underlying collector unchanged, because hooks represent polling callbacks rather than named metrics. + +Copy assignment on `GroupImp` is explicitly deleted, preventing accidental shallow copies of a type that holds shared ownership over live metric state. + +## Relationship to `Group` and `Collector` + +`Group` inherits from `Collector`, meaning a `Group::ptr` is substitutable wherever a `Collector::ptr` is expected. This allows subsystems to receive their metric interface as a `Collector::ptr` while the `Groups` registry internally tracks them as `Group::ptr` values, preserving the named identity needed for diagnostics without leaking the registry abstraction to consumers. + +## Usage in `CollectorManager` + +The sole production consumer of `Groups` is `CollectorManager` (`src/xrpld/app/main/CollectorManager.cpp`). On startup, it constructs either a `StatsDCollector` or a `NullCollector` (based on config), then wraps it with `make_Groups`. All subsystems call `CollectorManager::group(name)` to obtain their scoped `Group`, which is a thin call to `m_groups->get(name)`. This pattern means subsystems never see the raw `StatsDCollector` or `NullCollector` directly — they only interact with the uniform `Collector` interface, and switching between real and null telemetry requires no change to any subsystem code. \ No newline at end of file diff --git a/include/xrpl/beast/insight/Hook.h.ai.json b/include/xrpl/beast/insight/Hook.h.ai.json new file mode 100644 index 0000000000..4a7fc1c3f9 --- /dev/null +++ b/include/xrpl/beast/insight/Hook.h.ai.json @@ -0,0 +1,50 @@ +{ + "args": [ + { + "lineno": 20, + "name": "impl" + } + ], + "classes": [ + { + "args": [ + "()", + "(std::shared_ptr const& impl)" + ], + "lineno": 11, + "name": "Hook" + } + ], + "description": "Defines the Hook class, which acts as a reference to a handler for performing polled metric collection in the beast::insight namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/Hook.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "Hook" + }, + { + "args": [ + "std::shared_ptr const& impl" + ], + "lineno": 20, + "name": "Hook" + }, + { + "args": [], + "lineno": 27, + "name": "impl" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 7, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/Hook.h.ai.md b/include/xrpl/beast/insight/Hook.h.ai.md new file mode 100644 index 0000000000..d52c776b51 --- /dev/null +++ b/include/xrpl/beast/insight/Hook.h.ai.md @@ -0,0 +1,27 @@ +# `Hook.h` — Polled Metric Collection Handle + +`Hook` is a lightweight reference-counted handle used in the `beast::insight` metrics framework to register a callback that the metrics backend calls at each collection interval. Where `Counter`, `Gauge`, `Event`, and `Meter` all use a push model (the application updates them as events occur), `Hook` inverts that control: the collector calls back into application code on its own schedule, letting the application snapshot internal state on demand. + +## Role in the Insight System + +The `beast::insight` subsystem provides an abstraction layer over metrics backends, with `StatsDCollector` as the primary live implementation and `NullCollector` as a no-op drop-in. All metric types in the system — `Counter`, `Gauge`, `Event`, `Meter`, and `Hook` — follow the same design: a thin public handle wraps a `std::shared_ptr` to an abstract `*Impl` base. This lets callers hold, copy, and discard metric handles freely without caring about the lifetime of the backend object. `Hook` is no different; it holds a `std::shared_ptr`. + +## Design of `HookImpl` + +`HookImpl` defines the interface the backend must implement, with a single `HandlerType = std::function` typedef. The base class is abstract (pure virtual destructor) and inherits from `std::enable_shared_from_this`, enabling the backend implementation to safely vend weak references to itself. The destructor is defaulted out-of-line in `Hook.cpp` to pin the vtable to a single translation unit — a standard technique for abstract bases in header-only-adjacent designs. + +The concrete backend, `StatsDHookImpl` in `StatsDCollector.cpp`, registers the `HandlerType` handler and calls it via `do_process()` on the collector's polling timer. The handler is responsible for reading application state and pushing values to any associated counters or gauges. + +## Null State and Lifecycle + +`Hook` provides a default constructor that leaves `m_impl` as a null `shared_ptr`. This null state is intentional: code can unconditionally call `collector->make_hook(handler)` on a `NullCollector` and receive a `Hook` that holds a null impl — the metric silently does nothing. This eliminates the need for conditional checks throughout the application when metrics are disabled. + +Lifetime management is entirely through reference counting. When the last `Hook` copy referring to a given `HookImpl` is destroyed, the impl is destroyed and the backend deregisters the handler. This gives callers precise control: storing the `Hook` as a member field keeps the handler active for the lifetime of the owning object; letting it go out of scope silently cancels it. + +## Factory Pattern + +The `Hook(std::shared_ptr const&)` constructor is `explicit` and marked for implementation use. Callers create hooks exclusively through `Collector::make_hook(handler)`, which is templated to accept any callable and forwards to the virtual `make_hook(HookImpl::HandlerType const&)`. This indirection keeps the handle type backend-agnostic: the same `Hook` type works regardless of whether the underlying collector is `StatsDCollector`, `NullCollector`, or a test double. + +## Usage Pattern in the Codebase + +Throughout the XRPL server, classes that own metrics (`OverlayImpl`, `LedgerMaster`, `NetworkOPs`, `PeerfinderManager`) declare a `beast::insight::Hook` as a struct member alongside their other metric handles. The hook is initialized in the stats struct constructor by calling `collector->make_hook(handler)` with a lambda or bound member function that reads the component's current runtime state — peer counts, queue depths, ledger sequence numbers — and assigns those values to the sibling `Gauge` or `Counter` members. This pattern cleanly separates the metrics snapshot logic from the event-driven update paths used by the push-model metric types. \ No newline at end of file diff --git a/include/xrpl/beast/insight/HookImpl.h.ai.json b/include/xrpl/beast/insight/HookImpl.h.ai.json new file mode 100644 index 0000000000..e99bf7307d --- /dev/null +++ b/include/xrpl/beast/insight/HookImpl.h.ai.json @@ -0,0 +1,24 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "HookImpl" + } + ], + "description": "Defines the HookImpl class, an abstract base class for hook implementations in the beast::insight namespace, supporting a handler type for polled collection.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/HookImpl.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + }, + { + "lineno": 5, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/HookImpl.h.ai.md b/include/xrpl/beast/insight/HookImpl.h.ai.md new file mode 100644 index 0000000000..fd5ced5885 --- /dev/null +++ b/include/xrpl/beast/insight/HookImpl.h.ai.md @@ -0,0 +1,40 @@ +# `HookImpl.h` — Abstract Base for Poll-Driven Metric Hooks + +## Role in the System + +`HookImpl` is the abstract base class at the bottom of the `beast::insight` metrics framework's hook mechanism. It exists to define the interface contract and carry the `HandlerType` alias that binds together the `Collector`, `Hook`, and their concrete backend implementations. On its own the class is intentionally minimal — it does nothing but establish the shared pointer ownership model and the function signature used when a collection interval fires. + +The `beast::insight` subsystem is the XRPL node's pluggable metrics layer. Client code obtains a `Collector` (either the live `StatsDCollector` or the no-op `NullCollector`) and creates metric objects from it: counters, gauges, events, meters, and hooks. A hook is special among these: rather than the client *pushing* a value to a metric object, the framework *calls back* into the client at each collection interval. This is the polling model, useful when computing a metric value is cheap but you only want to do it on demand (e.g., reading an atomic counter, or computing a queue depth). + +## Class Structure + +```cpp +class HookImpl : public std::enable_shared_from_this +{ +public: + using HandlerType = std::function; + virtual ~HookImpl() = 0; +}; +``` + +Inheriting `std::enable_shared_from_this` is necessary because the live implementation (`StatsDHookImpl`) registers itself into the collector's intrusive list at construction time and must produce a valid `shared_ptr` to itself from within its own constructor chain — a pattern seen across all `*Impl` types in this subsystem. + +The `HandlerType` alias is placed on `HookImpl` rather than on the public-facing `Hook` handle class because `Collector.h` must reference it in the virtual `make_hook(HookImpl::HandlerType const&)` signature. Since `Collector.h` already includes `Hook.h`, which in turn includes `HookImpl.h`, anchoring the type on `HookImpl` avoids any circular dependency. + +The pure virtual destructor (`= 0`) makes the class abstract while still requiring an out-of-line definition. That definition lives in `Hook.cpp` as a single `= default` line. This is a deliberate C++ idiom: without the out-of-line body, derived class destructors that implicitly call `~HookImpl()` would cause a linker error, yet the pure specifier prevents anyone from instantiating `HookImpl` directly. + +## Handle/Impl Split + +The rest of the design follows the same handle/impl pattern used for every metric type in this subsystem. `Hook` (in `Hook.h`) is a lightweight value type that wraps a `shared_ptr`. Users copy and store `Hook` objects freely; lifetime is managed automatically. The actual behavior lives in a heap-allocated `HookImpl` subclass. This split lets `Collector` return metric objects by value with no raw pointer exposure, while still allowing polymorphic backend implementations. + +## Concrete Implementations + +Two implementations exist: + +**`NullHookImpl`** (in `NullCollector.cpp`) is a trivially empty subclass that adds no state and overrides nothing beyond the destructor. It is returned by `NullCollector::make_hook()` when metrics collection is disabled, ensuring the rest of the code never needs to guard against null hooks. + +**`StatsDHookImpl`** (in `StatsDCollector.cpp`) stores the `HandlerType` callback and a `shared_ptr` back to the owning `StatsDCollectorImp`. On construction it registers itself into the collector's metric list; on destruction it deregisters. When the collector's background thread fires a collection interval it calls `do_process()` on every registered metric, and `StatsDHookImpl::do_process()` simply invokes `m_handler()`. The handler is whatever lambda or callable the client passed to `Collector::make_hook()`. + +## Design Tradeoffs + +The absence of any virtual `process()` or `invoke()` method on `HookImpl` itself is intentional. The only protocol between `HookImpl` and the backend collector is *lifetime* — the collector holds the hook alive via `enable_shared_from_this` and dispatches through the concrete type's own interface (`StatsDMetricBase::do_process()`). This means the abstract base stays clean and independent of any particular backend's scheduling mechanism. A future backend could store the handler differently or invoke it on a different thread without any change to `HookImpl.h`. \ No newline at end of file diff --git a/include/xrpl/beast/insight/Insight.h.ai.json b/include/xrpl/beast/insight/Insight.h.ai.json new file mode 100644 index 0000000000..7fc2237960 --- /dev/null +++ b/include/xrpl/beast/insight/Insight.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "This file is a header that includes various metric and insight-related classes and interfaces for the Beast insight metrics framework, such as collectors, counters, gauges, events, groups, hooks, and their implementations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/Insight.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/Insight.h.ai.md b/include/xrpl/beast/insight/Insight.h.ai.md new file mode 100644 index 0000000000..c1aae5c4b2 --- /dev/null +++ b/include/xrpl/beast/insight/Insight.h.ai.md @@ -0,0 +1,26 @@ +# `include/xrpl/beast/insight/Insight.h` + +This file is the single-include convenience header for the `beast::insight` metrics framework. It aggregates all fifteen component headers so that any translation unit wanting to instrument code for observability needs only `#include ` rather than managing a list of individual includes. + +## The beast::insight Framework + +The `beast::insight` subsystem provides a lightweight, backend-agnostic instrumentation layer for the XRPL node. The design centres on one abstract factory — `Collector` — and a set of handle types that represent individual metrics. + +`Collector` is a pure virtual class whose factory methods (`make_counter`, `make_gauge`, `make_event`, `make_meter`, `make_hook`) return value-semantic handle objects. Each handle wraps a `shared_ptr` to a corresponding `*Impl` abstract interface. The handle is cheap to copy and assign; crucially, **the metric stops being collected when the last handle is destroyed**. This gives metric lifetime the same shape as object lifetime — a subsystem that shuts down simply drops its handles and the collector automatically stops tracking those metric names. + +## Metric Kinds + +- **`Counter`** — a monotonically adjusted integral value, supporting `++`, `--`, `+=`, `-=`. Semantically a server-side accumulator; the producer owns the delta, not the absolute value. +- **`Gauge`** — an instantaneous snapshot of an arbitrary integral value. The caller both sets (`operator=`) and adjusts (`+=`/`-=`) it. Because a collector may aggregate multiple updates within one reporting interval, rapid fluctuations between polls are intentionally lossy. +- **`Event`** — a push-only timing metric. The `notify()` method accepts any `std::chrono::duration`, which is ceiling-converted to the implementation's `value_type`. Events model operations with an associated elapsed time rather than a running total. +- **`Hook`** — not a metric itself, but a polling callback registered with the collector. The hook is invoked once per collection interval on a collector-managed thread, making it the right choice for gathering values that are expensive to read or that must be sampled rather than pushed. + +## Concrete Backends + +Two `Collector` implementations are exposed through this header. `NullCollector` silently discards all operations — it is used when metrics are disabled or in unit tests that do not care about observability. `StatsDCollector` ships metrics over UDP to a StatsD aggregation server, accepting an `IP::Endpoint`, an optional dot-separated prefix string, and a `Journal` for logging. The prefix facility on `StatsDCollector`, combined with the two-argument overloads on `Collector` (e.g., `make_counter(prefix, name)`), lets callers namespace their metrics without embedding the prefix in every call site. + +`Group` extends `Collector` with a named scope, and `Groups` manages a registry of such scopes — together they allow different components of the node to own logically separate metric namespaces while sharing a single underlying transport. + +## Why a Single Aggregation Header + +The split between handle types (`Counter.h`, `Gauge.h`, …) and their abstract implementations (`CounterImpl.h`, `GaugeImpl.h`, …) exists so that concrete backend code can include only the `*Impl` interfaces without dragging in the full `Collector` hierarchy, and vice versa. `Insight.h` bridges that split for the common case: production code that both creates metrics and uses them through the handle API simply includes this file and holds a `Collector::ptr` received at construction time, remaining entirely decoupled from whether the underlying backend is StatsD, a null sink, or any future implementation. \ No newline at end of file diff --git a/include/xrpl/beast/insight/Meter.h.ai.json b/include/xrpl/beast/insight/Meter.h.ai.json new file mode 100644 index 0000000000..18cb72509f --- /dev/null +++ b/include/xrpl/beast/insight/Meter.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 35, + "name": "amount" + }, + { + "lineno": 41, + "name": "amount" + }, + { + "lineno": 47, + "name": "amount" + }, + { + "lineno": 26, + "name": "impl" + } + ], + "classes": [ + { + "args": [ + "()", + "std::shared_ptr const& impl" + ], + "lineno": 13, + "name": "Meter" + } + ], + "description": "Defines the Meter class, a lightweight reference wrapper for an increment-only metric used for measuring integral values, within the beast::insight namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/Meter.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "Meter" + }, + { + "args": [ + "std::shared_ptr const& impl" + ], + "lineno": 26, + "name": "Meter" + }, + { + "args": [ + "value_type amount" + ], + "lineno": 35, + "name": "increment" + }, + { + "args": [ + "value_type amount" + ], + "lineno": 41, + "name": "operator+=" + }, + { + "args": [], + "lineno": 47, + "name": "operator++" + }, + { + "args": [ + "int" + ], + "lineno": 53, + "name": "operator++" + }, + { + "args": [], + "lineno": 59, + "name": "impl" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + }, + { + "lineno": 8, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/Meter.h.ai.md b/include/xrpl/beast/insight/Meter.h.ai.md new file mode 100644 index 0000000000..510b335426 --- /dev/null +++ b/include/xrpl/beast/insight/Meter.h.ai.md @@ -0,0 +1,44 @@ +# `Meter.h` — Increment-Only Metric Handle + +## Role in the System + +`Meter.h` defines the `Meter` class within the `beast::insight` metrics collection framework. The `beast::insight` subsystem provides a thin abstraction layer over external telemetry backends (primarily StatsD), allowing XRPL node code to emit operational metrics without coupling to any specific reporting infrastructure. `Meter` represents one of five metric primitives in this framework alongside `Counter`, `Gauge`, `Event`, and `Hook`. + +A `Meter` is an **increment-only** integral counter. Where `Counter` models a bidirectional gauge that can both increase and decrease, `Meter` enforces the constraint that values only ever go up — appropriate for cumulative event counts such as transactions processed, packets received, or bytes written. + +## Handle/Body Design + +The class is a lightweight reference-counted handle over a `MeterImpl` backend. The actual metric state and any reporting logic live behind `MeterImpl`, which is a pure abstract base class with a single virtual method: + +```cpp +virtual void increment(value_type amount) = 0; +``` + +`MeterImpl` inherits `std::enable_shared_from_this`, and `Meter` holds the implementation via `std::shared_ptr`. This gives the handle value semantics — `Meter` objects are cheap to copy and assign, and the underlying metric remains live precisely as long as at least one `Meter` handle refers to it. When the last handle is destroyed, the `shared_ptr` refcount drops to zero, the `MeterImpl` is destroyed, and the metric silently stops reporting. This lifetime-as-collection-scope design means callers don't need an explicit deregister step. + +## Null Object Pattern + +The default constructor produces a **null meter** — one where `m_impl` is empty. Every mutation method guards with `if (m_impl)` before delegating, so a null `Meter` silently absorbs all increments with no effect: + +```cpp +void increment(value_type amount) const { + if (m_impl) + m_impl->increment(amount); +} +``` + +This is intentional. Code that receives a `Meter` from a `Collector` doesn't need to check whether telemetry is enabled. When the node is configured with `NullCollector`, `make_meter()` still returns a valid (but no-op) `Meter` object, and the calling code is identical in both cases. + +## `const`-Qualified Mutation Operators + +A notable design choice: `increment()`, `operator+=`, `operator++` (both prefix and postfix) are all declared `const`. This allows a component to store its metrics as `const Meter` member fields or within a `const`-qualified context and still mutate the underlying count. The `const` guarantee applies to the *handle* (the `shared_ptr` itself is not reseated), not to the referenced counter state, which is mutable through the pointer indirection. This is a deliberate ergonomic choice that mirrors how `const` member functions can still call methods on pointer or reference members when the pointed-to object's identity, not its value, is what the `const` is protecting. + +## Relationship to `Collector` and `MeterImpl` + +`Meter` objects are obtained exclusively through `Collector::make_meter()`, not constructed directly. The `Collector` interface serves as the factory; concrete implementations (`NullCollector`, `StatsDCollector`) produce the corresponding `MeterImpl` subclass and wrap it in a `Meter` handle. `NullCollector` yields a `NullMeterImpl` whose `increment()` is a no-op, while `StatsDCollector` produces an implementation that buffers and flushes increment events to the StatsD daemon. + +The `impl()` accessor exposes the `shared_ptr` to allow frameworks like `Groups` to introspect or aggregate metrics, without exposing the `Meter`'s internal mutation interface directly. + +## Comparison with `Counter` + +The only structural difference between `Meter` and `Counter` in this codebase is that `Counter` also exposes `operator-=`, `operator--` (prefix and postfix). The semantic difference is intentional: meters model monotonically increasing totals (cumulative counts), while counters model values that can go up or down (like queue depths). In StatsD terminology, these map to different metric types — a meter maps to a `c` (count) or `m` (meter) type, while a counter maps to a gauge. Enforcing this at the type level rather than by convention prevents callers from accidentally decrementing a metric that semantically should only grow. \ No newline at end of file diff --git a/include/xrpl/beast/insight/MeterImpl.h.ai.json b/include/xrpl/beast/insight/MeterImpl.h.ai.json new file mode 100644 index 0000000000..ae6971652a --- /dev/null +++ b/include/xrpl/beast/insight/MeterImpl.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [ + { + "lineno": 14, + "name": "amount" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "MeterImpl" + } + ], + "description": "Defines the MeterImpl abstract class for increment-only metric counters in the beast::insight namespace, providing an interface for incrementing a meter value.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/MeterImpl.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "~MeterImpl" + }, + { + "args": [ + "amount" + ], + "lineno": 14, + "name": "increment" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + }, + { + "lineno": 5, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/MeterImpl.h.ai.md b/include/xrpl/beast/insight/MeterImpl.h.ai.md new file mode 100644 index 0000000000..e0ce35007f --- /dev/null +++ b/include/xrpl/beast/insight/MeterImpl.h.ai.md @@ -0,0 +1,41 @@ +# `MeterImpl.h` — Abstract Interface for Increment-Only Metric Counters + +`MeterImpl` defines the abstract backend interface for the `Meter` metric type within the `beast::insight` telemetry subsystem. It occupies the same position in the insight hierarchy as `CounterImpl`, `GaugeImpl`, and `EventImpl` — it is the pure virtual contract that concrete collector backends (such as the StatsD implementation) must fulfill to participate in the metrics pipeline. + +## Role in the Insight Architecture + +The `beast::insight` subsystem follows a two-layer design. Each metric type has a lightweight, copyable **handle** class (`Meter`) and a separately-defined **implementation** base class (`MeterImpl`). The handle wraps a `shared_ptr` and delegates all operations through it. This separation allows the handle to be freely copied and passed around at near-zero cost while the implementation itself carries the real state — aggregated counts and I/O context references — inside the collector backend. + +`MeterImpl.h` is the implementation side of this split: it declares the abstract interface that any backend must provide, while `Meter.h` includes it to alias `value_type` and to accept `shared_ptr` in its constructor. + +## Semantics of a Meter vs. a Counter + +The only public method is `increment(value_type amount)`, and `value_type` is `std::uint64_t` — unsigned. This is a deliberate distinction from `CounterImpl`, which uses `std::int64_t` and therefore permits both positive and negative adjustments. A `Meter` is strictly monotonically increasing: it models a cumulative rate — events fired, bytes sent, transactions processed — where the value only ever grows. Callers who need to decrement a metric must use `Counter` instead. + +## `enable_shared_from_this` and Lifetime Management + +`MeterImpl` inherits `std::enable_shared_from_this`. This is not incidental. The concrete StatsD backend (`StatsDMeterImpl`) must dispatch `increment()` calls asynchronously onto the collector's `boost::asio` I/O thread to avoid locking. The dispatch looks like: + +```cpp +boost::asio::dispatch( + m_impl->get_io_context(), + std::bind(&StatsDMeterImpl::do_increment, + std::static_pointer_cast(shared_from_this()), + amount)); +``` + +Without `enable_shared_from_this`, obtaining a safe `shared_ptr` to `this` from inside a member function would be impossible. The base class provides the mechanism; `StatsDMeterImpl` uses `shared_from_this()` to extend the object's lifetime across the asynchronous dispatch gap, ensuring the object isn't destroyed between when `increment()` returns and when `do_increment()` actually runs on the I/O thread. + +The same mechanism drives metric lifetime: the `Meter` handle holds the only user-facing `shared_ptr`. When all handles referencing a metric go out of scope, the refcount falls to zero, the destructor runs, and the implementation unregisters itself from the collector — no explicit teardown is needed. + +## Concrete Implementation Shape + +`StatsDMeterImpl` inherits from both `MeterImpl` and `StatsDMetricBase`. Its `increment()` dispatches asynchronously; the actual accumulation happens in `do_increment()` on the I/O thread where it safely mutates `m_value` and sets a `m_dirty` flag. At each collection interval, `do_process()` calls `flush()`, which formats the StatsD wire message with the `|m` type suffix and posts the buffer to the UDP socket, then resets the accumulator to zero. This means the StatsD backend reports *delta* counts per interval rather than a lifetime total. + +## Relationship to `NullCollector` + +`NullCollector` provides a no-op implementation of the `Collector` interface used in testing or when metrics are disabled. Its `make_meter()` returns a default-constructed `Meter` (null handle containing no `shared_ptr`), which means `Meter::increment()` silently does nothing. The design at the `MeterImpl` level cleanly supports this: the null path never instantiates a `MeterImpl` at all. + +## Summary + +`MeterImpl.h` is intentionally minimal — 23 lines declaring one type alias and two virtual functions. Its significance lies not in its size but in the structural role it plays: it is the seam point between the user-facing `Meter` handle and whichever telemetry backend is active, enforces the unsigned-only increment contract that distinguishes meters from counters, and inherits `enable_shared_from_this` to underpin the safe asynchronous dispatch pattern used by the StatsD backend. \ No newline at end of file diff --git a/include/xrpl/beast/insight/NullCollector.h.ai.json b/include/xrpl/beast/insight/NullCollector.h.ai.json new file mode 100644 index 0000000000..23695c232a --- /dev/null +++ b/include/xrpl/beast/insight/NullCollector.h.ai.json @@ -0,0 +1,37 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "NullCollector()" + ], + "lineno": 8, + "name": "NullCollector" + } + ], + "description": "Defines the NullCollector class, a Collector implementation that does not collect metrics, within the beast::insight namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/NullCollector.h", + "functions": [ + { + "args": [], + "lineno": 10, + "name": "NullCollector" + }, + { + "args": [], + "lineno": 13, + "name": "New" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + }, + { + "lineno": 5, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/NullCollector.h.ai.md b/include/xrpl/beast/insight/NullCollector.h.ai.md new file mode 100644 index 0000000000..9c4b663787 --- /dev/null +++ b/include/xrpl/beast/insight/NullCollector.h.ai.md @@ -0,0 +1,33 @@ +# `NullCollector.h` — No-Op Metrics Collector + +`NullCollector` is the Null Object implementation of the `beast::insight::Collector` interface. Its purpose is to allow all production code that depends on metric collection to work unchanged when no external metrics backend is configured. Rather than sprinkling null checks throughout the codebase, callers always hold a valid `Collector::ptr` and call `make_counter()`, `make_gauge()`, etc. normally — with `NullCollector`, those calls simply do nothing. + +## Role in the Insight Subsystem + +The `beast::insight` module provides a thin abstraction over time-series metrics. The `Collector` base class declares virtual factory methods for five metric types: `Hook`, `Counter`, `Event`, `Gauge`, and `Meter`. There are exactly two concrete implementations: `StatsDCollector`, which ships metrics over UDP to a StatsD server, and `NullCollector`, which discards everything silently. + +`NullCollector` is selected at startup by `CollectorManager` in `src/xrpld/app/main/CollectorManager.cpp`. When the `[insight]` configuration section either omits `server` or sets it to anything other than `"statsd"`, `NullCollector::New()` is called and the resulting pointer flows through the entire application as the active collector. This makes metrics optional at the configuration level with zero code-path divergence in the components being monitored. + +## Header Design + +The header is intentionally minimal: a class declaration that publicly inherits `Collector` and exposes only a default constructor and the static `New()` factory. The constructor is `explicit` and defaulted, providing no user-accessible state. The factory returns `std::shared_ptr` — the base pointer type — rather than a pointer to `NullCollector` itself. This keeps callers decoupled from the concrete type, consistent with how `StatsDCollector::New()` also returns a typed pointer but consumers universally hold `Collector::ptr`. + +## Implementation (NullCollector.cpp) + +The actual work lives in the translation unit, where a private `detail::NullCollectorImp` class inherits `NullCollector` and overrides all five `Collector` factory methods. Each override creates and returns the corresponding null metric object: + +- `NullHookImpl` — stores the handler but never calls it (no polling thread is started). +- `NullCounterImpl` — `increment()` is an empty function body. +- `NullEventImpl` — `notify()` is an empty function body. +- `NullGaugeImpl` — both `set()` and `increment()` are empty function bodies. +- `NullMeterImpl` — `increment()` is an empty function body. + +All null `*Impl` classes explicitly delete the copy-assignment operator, preventing accidental sharing of impl state — a defensive pattern carried over from the live implementations where sharing would be a data race. + +The assignment of `operator=` as private (rather than using `= delete`) is a legacy C++03-style guard that predates widespread `= delete` usage in the codebase, but its intent is the same. + +`NullCollector::New()` constructs a `NullCollectorImp` wrapped in a `shared_ptr` and returns it cast to `shared_ptr`, keeping the implementation type entirely invisible to callers. + +## Usage Pattern + +Any XRPL subsystem that wants to report metrics accepts a `Collector::ptr` in its constructor. In tests and default configurations, `NullCollector::New()` is the standard provider — for example, `TaggedCache`, `FullBelowCache`, and the CSF simulation framework in `src/test/csf/collectors.h` all use it to silence metrics without special-casing their constructors. This makes `NullCollector` the go-to stub whenever a `Collector` dependency needs to be satisfied in an environment where metric reporting is irrelevant. \ No newline at end of file diff --git a/include/xrpl/beast/insight/StatsDCollector.h.ai.json b/include/xrpl/beast/insight/StatsDCollector.h.ai.json new file mode 100644 index 0000000000..807e3b5b2f --- /dev/null +++ b/include/xrpl/beast/insight/StatsDCollector.h.ai.json @@ -0,0 +1,47 @@ +{ + "args": [ + { + "lineno": 19, + "name": "address" + }, + { + "lineno": 19, + "name": "prefix" + }, + { + "lineno": 19, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "StatsDCollector" + } + ], + "description": "Defines the StatsDCollector class, a Collector implementation that reports metrics to a StatsD server, including a static factory method for instantiation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/insight/StatsDCollector.h", + "functions": [ + { + "args": [ + "address", + "prefix", + "journal" + ], + "lineno": 18, + "name": "New" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "beast" + }, + { + "lineno": 6, + "name": "insight" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/insight/StatsDCollector.h.ai.md b/include/xrpl/beast/insight/StatsDCollector.h.ai.md new file mode 100644 index 0000000000..a1b398effc --- /dev/null +++ b/include/xrpl/beast/insight/StatsDCollector.h.ai.md @@ -0,0 +1,41 @@ +# `StatsDCollector.h` — StatsD Metrics Export Interface + +This header is the public face of XRPL's live metrics subsystem. It declares `StatsDCollector`, the concrete `Collector` implementation that ships runtime metrics to an external StatsD aggregator over UDP. The header is deliberately minimal — a class declaration and a single factory function — with all real machinery hidden in `StatsDCollector.cpp`. + +## Role in the Insight Subsystem + +The `beast::insight` module provides a metrics abstraction that any XRPL subsystem can consume by accepting a `Collector::ptr`. Code that needs telemetry calls `make_counter()`, `make_gauge()`, etc., on this interface without knowing whether metrics go to a StatsD server or nowhere. `NullCollector` is the no-op alternative used when metrics are disabled; `StatsDCollector` is the live backend. This separation means instrumentation code has zero cost when metrics are disabled, and no StatsD-specific code leaks into application logic. + +## Factory Design + +`StatsDCollector` exposes only a static `New()` factory rather than a public constructor. This is the standard pimpl-plus-factory pattern: the real work is done by `detail::StatsDCollectorImp`, a class private to the `.cpp` that inherits `StatsDCollector` and `std::enable_shared_from_this`. Callers receive a `shared_ptr`, so the implementation type is fully encapsulated. The explicit `= default` constructor at line 17 ensures no inadvertent construction paths exist from outside the factory. + +`New()` takes three parameters: an `IP::Endpoint` identifying the StatsD server (address + port), a `prefix` string prepended to every metric name (e.g., `"rippled.mainnet"`), and a `Journal` for diagnostic logging. + +## Implementation Architecture (from `StatsDCollector.cpp`) + +The concrete `StatsDCollectorImp` owns a private `boost::asio::io_context` and launches a dedicated `std::thread` that calls `m_io_context.run()`. All metric I/O is serialized through a `boost::asio::strand`, so every mutation, timer callback, and UDP send runs on that single background thread. This design trades fine-grained per-metric locking for a simpler, strand-serialized event loop. + +### Metric Registration + +Each metric type (`StatsDCounterImpl`, `StatsDGaugeImpl`, etc.) inherits `StatsDMetricBase`, which is a `beast::List<>::Node` — an intrusive linked-list node. Upon construction, each metric calls `m_impl->add(*this)`, inserting itself into the collector's `metrics_` list under `metricsLock_` (a `std::recursive_mutex`). Destruction calls `remove()`. The recursive mutex accommodates the case where a `Hook` callback creates or destroys additional metrics during the timer sweep. + +### Periodic Flush Cycle + +A `boost::asio::basic_waitable_timer` fires every one second. `on_timer()` locks `metricsLock_`, iterates every registered `StatsDMetricBase` calling `do_process()`, then calls `send_buffers()` and re-arms the timer. The `do_process()` implementations check a `m_dirty` flag and, if set, format the metric as a StatsD line (e.g., `"prefix.name:42|c\n"` for counters, `"|g"` for gauges, `"|m"` for meters) and enqueue it into `m_data`. + +**Counters and meters** accumulate increments between flushes and reset to zero after each send — appropriate for rate metrics. **Gauges** track the last sent value and suppress redundant emissions when the value hasn't changed. **Events** (`|ms` timer type) bypass the periodic cycle entirely; `notify()` dispatches immediately to `do_notify()`, which formats and posts the buffer on the spot. + +### Thread-Safe Metric Mutation + +Public methods like `Counter::increment()` do not touch shared state directly. Instead they call `boost::asio::dispatch()` targeting the collector's `io_context`, binding a `do_increment()` call with a `shared_ptr` to the metric itself as the lifetime anchor. This ensures that even if the metric object is destroyed on another thread, the dispatched work holds a reference and executes safely. + +### UDP Batching + +`send_buffers()` packs accumulated StatsD lines into UDP datagrams up to `max_packet_size = 1472` bytes (Ethernet MTU minus IP/UDP headers — the previous comment shows 484 was the original value, updated to the practical maximum for LAN deployments). It moves `m_data` into a `shared_ptr>` captured by the `async_send` completion handler (`keepAlive`), ensuring string memory survives the async operation. Each send is fire-and-forget, logging errors through the `Journal` but not retrying. + +### Lifecycle and Shutdown + +The destructor cancels the timer, resets the `executor_work_guard` (allowing `io_context::run()` to drain), then joins the I/O thread. The final `m_io_context.poll()` after socket shutdown processes any remaining completions. Errors from `timer.cancel()` are explicitly swallowed since cancellation can race with an already-fired callback. + +A compile-time flag `BEAST_STATSDCOLLECTOR_TRACING_ENABLED` (default 0) enables stderr dumps of every outgoing UDP buffer — useful for debugging metric wire format without needing a live StatsD sink. \ No newline at end of file diff --git a/include/xrpl/beast/net/IPAddress.h.ai.json b/include/xrpl/beast/net/IPAddress.h.ai.json new file mode 100644 index 0000000000..5cd67e85ae --- /dev/null +++ b/include/xrpl/beast/net/IPAddress.h.ai.json @@ -0,0 +1,112 @@ +{ + "args": [ + { + "lineno": 18, + "name": "addr" + }, + { + "lineno": 24, + "name": "addr" + }, + { + "lineno": 30, + "name": "addr" + }, + { + "lineno": 36, + "name": "addr" + }, + { + "lineno": 42, + "name": "addr" + }, + { + "lineno": 48, + "name": "addr" + }, + { + "lineno": 56, + "name": "h" + }, + { + "lineno": 56, + "name": "addr" + } + ], + "classes": [ + { + "args": [], + "lineno": 71, + "name": "hash" + } + ], + "description": "Provides utility functions and hashing support for IP addresses using Boost.Asio, including string conversion, address type checks, and hash integration.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/net/IPAddress.h", + "functions": [ + { + "args": [ + "addr" + ], + "lineno": 18, + "name": "to_string" + }, + { + "args": [ + "addr" + ], + "lineno": 24, + "name": "is_loopback" + }, + { + "args": [ + "addr" + ], + "lineno": 30, + "name": "is_unspecified" + }, + { + "args": [ + "addr" + ], + "lineno": 36, + "name": "is_multicast" + }, + { + "args": [ + "addr" + ], + "lineno": 42, + "name": "is_private" + }, + { + "args": [ + "addr" + ], + "lineno": 48, + "name": "is_public" + }, + { + "args": [ + "h", + "addr" + ], + "lineno": 56, + "name": "hash_append" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "beast" + }, + { + "lineno": 13, + "name": "IP" + }, + { + "lineno": 69, + "name": "boost" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/net/IPAddress.h.ai.md b/include/xrpl/beast/net/IPAddress.h.ai.md new file mode 100644 index 0000000000..145f74c028 --- /dev/null +++ b/include/xrpl/beast/net/IPAddress.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/beast/net/IPAddress.h` + +## Role in the System + +This header is the central aggregation point for the `beast::IP` address layer. Its job is narrow but load-bearing: it defines the canonical `Address` type used throughout the XRPL peer networking stack, augments it with routing-classification predicates unavailable in Boost.Asio directly, and wires the type into both Beast's internal hashing infrastructure and `boost::hash`. + +The header sits at the top of a three-file address hierarchy. `IPAddressV4.h` and `IPAddressV6.h` define the version-specific aliases (`AddressV4`, `AddressV6`) and declare the `is_private`/`is_public` predicates for each protocol version. This file then provides a unified, version-agnostic surface by delegating through a version dispatch to those implementations. `IPEndpoint.h` sits above all three, combining an `Address` with a `Port` into the `Endpoint` type that most callers actually hold. + +## Type Alias Strategy + +Rather than wrapping `boost::asio::ip::address` in a new class, the file declares a simple type alias: + +```cpp +using Address = boost::asio::ip::address; +``` + +This is a deliberate non-wrapping design. The XRPL codebase interacts heavily with Boost.Asio's networking primitives (sockets, resolvers, acceptors), and those APIs all traffic in `boost::asio::ip::address` directly. Introducing a wrapper would require constant `.native()` extraction or implicit-conversion gymnastics. The alias keeps addresses compatible with the Asio ecosystem while allowing the `beast::IP` namespace to extend the type's behavior via free functions. + +## Free Function Set + +The six predicates follow a consistent pattern: they accept a `const Address&` and return `bool`. Three of them — `is_loopback`, `is_unspecified`, and `is_multicast` — are simple forwarding wrappers over the equivalent Boost.Asio member functions and exist purely to make calling code more uniform (a caller can invoke `beast::IP::is_loopback(addr)` without knowing whether `addr` is a Boost type or a future replacement). + +The remaining two, `is_private` and `is_public`, are more substantive. Boost.Asio does not expose these classifications natively, so the file dispatches through a runtime version check: + +```cpp +return (addr.is_v4()) ? is_private(addr.to_v4()) : is_private(addr.to_v6()); +``` + +The actual classification logic lives in the `.cpp` files backing `IPAddressV4.h` and `IPAddressV6.h`. For IPv4 this typically involves checking RFC 1918 ranges (10/8, 172.16/12, 192.168/16) and link-local (169.254/16). The `is_public` predicate is the logical complement, used by the peering layer to decide whether a discovered endpoint is Internet-routable and worth advertising to other nodes. + +## Hashing Design + +The file provides hashing support in two independent layers. + +**`beast::hash_append`** — This is the primary mechanism. The function is a template over any `Hasher` that satisfies the `hash_append` protocol used throughout the Beast hashing subsystem (see `hash_append.h`). It extracts the raw byte representation via `.to_bytes()` on the underlying v4 or v6 address object, letting the hasher consume the address as a plain byte sequence. The `else` branch with `UNREACHABLE` is a defensive invariant: `boost::asio::ip::address` is always one of v4 or v6, so this branch should never execute, but the assertion makes the assumption explicit and will catch any future protocol extensions at development time (the `LCOV_EXCL_START/STOP` markers exclude it from coverage reporting since it is unreachable by design). + +**`boost::hash`** — Many of rippled's containers use `boost::unordered_map` or otherwise depend on `boost::hash`. The explicit specialization placed in `namespace boost` routes through `beast::uhash<>`, which in turn calls `hash_append` using the xxhasher backend. This means both the generic `hash_append` pathway and the `boost::hash` pathway ultimately feed into the same xxhash computation, ensuring consistent hash values regardless of which container type is in use. + +Notably, no `std::hash` specialization is provided for `Address`. In contrast, `IPEndpoint.h` provides both `std::hash` and `boost::hash` for `Endpoint`. Since most code that needs to key on an address alone reaches for Boost containers, the omission is intentional and keeps the specializations minimal. + +## Relationship to `IPEndpoint` + +`IPEndpoint.h` includes this file and builds directly on the `Address` type alias. The `Endpoint::hash_append` implementation hashes `m_addr` using the `hash_append` defined here, meaning the peer-to-peer subsystem's endpoint hashing implicitly relies on this file's byte-level hashing of the address portion. The free function predicates defined here are also mirrored verbatim in `IPEndpoint.h` as pass-through wrappers, forming a consistent property-query API across both the bare-address and address+port representations. \ No newline at end of file diff --git a/include/xrpl/beast/net/IPAddressConversion.h.ai.json b/include/xrpl/beast/net/IPAddressConversion.h.ai.json new file mode 100644 index 0000000000..425af8b29d --- /dev/null +++ b/include/xrpl/beast/net/IPAddressConversion.h.ai.json @@ -0,0 +1,70 @@ +{ + "args": [ + { + "lineno": 11, + "name": "address" + }, + { + "lineno": 15, + "name": "endpoint" + }, + { + "lineno": 19, + "name": "endpoint" + }, + { + "lineno": 23, + "name": "endpoint" + } + ], + "classes": [ + { + "args": [], + "lineno": 28, + "name": "IPAddressConversion" + } + ], + "description": "This file provides utility functions and a deprecated struct for converting between Boost.Asio IP address and endpoint types and the beast::IP::Endpoint type.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/net/IPAddressConversion.h", + "functions": [ + { + "args": [ + "address" + ], + "lineno": 11, + "name": "from_asio" + }, + { + "args": [ + "endpoint" + ], + "lineno": 15, + "name": "from_asio" + }, + { + "args": [ + "endpoint" + ], + "lineno": 19, + "name": "to_asio_address" + }, + { + "args": [ + "endpoint" + ], + "lineno": 23, + "name": "to_asio_endpoint" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 7, + "name": "IP" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/net/IPAddressConversion.h.ai.md b/include/xrpl/beast/net/IPAddressConversion.h.ai.md new file mode 100644 index 0000000000..92dcf77754 --- /dev/null +++ b/include/xrpl/beast/net/IPAddressConversion.h.ai.md @@ -0,0 +1,32 @@ +# `IPAddressConversion.h` — Asio/Beast Endpoint Translation Layer + +This header declares the conversion boundary between Boost.Asio's IP address types and the XRPL-internal `beast::IP::Endpoint` abstraction. It is a small but architecturally significant file: it defines where the networking I/O layer (Asio) hands off to the application's canonical address representation. + +## Why This Exists + +XRPL's internal subsystems — PeerFinder, the Overlay layer, resource management, the RPC server — all reason about peers and clients in terms of `beast::IP::Endpoint`, a version-independent address-plus-port type that doesn't drag in Asio dependencies. Asio, meanwhile, is used for the actual socket I/O and resolves into its own `boost::asio::ip::address` and `boost::asio::ip::tcp::endpoint` types. This file provides the four translation functions that sit at the seam between those two worlds. + +## The Four Free Functions + +The primary API lives in the `beast::IP` namespace as free functions: + +- `from_asio(boost::asio::ip::address const&)` — wraps a bare Asio address into an `Endpoint` with port zero. The zero-port contract is explicit in the comment and reflects the fact that an `ip::address` carries no port. +- `from_asio(boost::asio::ip::tcp::endpoint const&)` — the full-fidelity conversion, preserving both address and port. +- `to_asio_address(Endpoint const&)` — extracts just the address component, discarding the port. Used when only the address is needed by an Asio API. +- `to_asio_endpoint(Endpoint const&)` — round-trips a `beast::IP::Endpoint` back to an Asio TCP endpoint with port intact. + +The implementation in `IPAddressConversion.cpp` is deliberately trivial: `from_asio(endpoint)` calls `Endpoint{endpoint.address(), endpoint.port()}`, and `to_asio_endpoint` calls `boost::asio::ip::tcp::endpoint{endpoint.address(), endpoint.port()}`. The simplicity is intentional — `beast::IP::Endpoint` is constructor-compatible with Asio's address type because `beast::IP::Address` is itself a thin alias over Asio's address internally. + +The asymmetry between the two directions (both `from_asio` variants versus `to_asio_address` and `to_asio_endpoint` as separate functions) reflects usage reality: incoming Asio data always arrives with a full endpoint, but outgoing calls sometimes need only an address. + +## The Deprecated `IPAddressConversion` Struct + +The `beast::IPAddressConversion` struct at the bottom of the file is marked `// DEPRECATED` and simply re-exposes the four free functions as `static` methods. Despite the deprecation marker, a grep across the codebase shows it is still the form used almost universally: `OverlayImpl.cpp`, `ConnectAttempt.cpp`, `Logic.h`, `ServerHandler.cpp`, `ResourceManager.cpp`, `WSInfoSub.h`, `ResolverAsio.cpp`, and `BaseHTTPPeer.h` all call `beast::IPAddressConversion::from_asio(...)` or `beast::IPAddressConversion::to_asio_endpoint(...)`. The intended migration is to call the free functions in `beast::IP` directly, but that cleanup has not been completed. + +## Relationship to `IPEndpoint.h` + +`IPAddressConversion.h` includes `IPEndpoint.h`, which defines `beast::IP::Endpoint` and its full interface. The `Endpoint` class stores an `Address` and a `Port` (`std::uint16_t`), supports comparison operators, hashing (`std::hash` and `boost::hash` specializations), and streaming. `IPAddressConversion.h` is purely a conversion adapter on top of that type — it adds no new state or logic, only the translation bridge to Asio. + +## Design Pattern + +The pattern here — internal canonical type, thin conversion layer at the I/O boundary — keeps application logic decoupled from Asio's type hierarchy. Code inside PeerFinder or the resource rate-limiter never needs to `#include `; they work with `beast::IP::Endpoint` throughout, and only the narrow I/O-facing code at socket accept/connect time calls these converters. This makes the internal logic independently testable and insulates it from Asio API evolution. \ No newline at end of file diff --git a/include/xrpl/beast/net/IPAddressV4.h.ai.json b/include/xrpl/beast/net/IPAddressV4.h.ai.json new file mode 100644 index 0000000000..d4fb5a01ea --- /dev/null +++ b/include/xrpl/beast/net/IPAddressV4.h.ai.json @@ -0,0 +1,53 @@ +{ + "args": [ + { + "lineno": 13, + "name": "addr" + }, + { + "lineno": 16, + "name": "addr" + }, + { + "lineno": 20, + "name": "address" + } + ], + "classes": [], + "description": "This header file provides utility functions for working with IPv4 addresses, including checking if an address is private or public and determining its address class.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/net/IPAddressV4.h", + "functions": [ + { + "args": [ + "addr" + ], + "lineno": 13, + "name": "is_private" + }, + { + "args": [ + "addr" + ], + "lineno": 16, + "name": "is_public" + }, + { + "args": [ + "address" + ], + "lineno": 20, + "name": "get_class" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 7, + "name": "IP" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/net/IPAddressV4.h.ai.md b/include/xrpl/beast/net/IPAddressV4.h.ai.md new file mode 100644 index 0000000000..df6285607f --- /dev/null +++ b/include/xrpl/beast/net/IPAddressV4.h.ai.md @@ -0,0 +1,29 @@ +# `include/xrpl/beast/net/IPAddressV4.h` + +## Role in the System + +This header is part of the `beast::IP` networking utility layer within the XRPL node software. It extends `boost::asio::ip::address_v4` — which the file aliases as `AddressV4` — with three free functions that encode IPv4 address-classification logic needed throughout the peer overlay system. Rather than scattering bitmask arithmetic across call sites, these declarations give every consumer a single, named vocabulary for the two questions the overlay layer asks most often: "is this peer reachable from the public internet?" and "what legacy address class does it fall into?" + +## `AddressV4` Type Alias + +`AddressV4` is a transparent alias for `boost::asio::ip::address_v4`. No wrapper class is introduced; the goal is purely to anchor the utility functions in the `beast::IP` namespace so they compose naturally with the parallel `IPAddress.h` abstraction for the polymorphic `Address` type. `IPAddress.h` dispatches `is_private` and `is_public` to the V4 and V6 variants by checking the address family at runtime — this header supplies the V4 half of that dispatch. + +## Function Semantics + +**`is_private(AddressV4 const& addr)`** returns `true` for addresses that are not globally routable under RFC 1918 and loopback conventions. The implementation in `IPAddressV4.cpp` tests three private CIDR blocks with raw 32-bit bitmasks: `10.0.0.0/8` (mask `0xff000000`), `172.16.0.0/12` (mask `0xfff00000`), and `192.168.0.0/16` (mask `0xffff0000`), plus delegates to `addr.is_loopback()` for `127.*`. The choice of bitmask arithmetic over CIDR helper utilities is deliberate — it avoids constructing temporary subnet objects on a hot path and keeps the check branch-free. + +**`is_public(AddressV4 const& addr)`** is defined simply as `!is_private(addr) && !addr.is_multicast()`. Multicast (`224.0.0.0/4`) is excluded because those addresses are neither routable in the normal unicast sense nor private, so neither `is_private` nor the negative alone would give the right answer. + +**`get_class(AddressV4 const& address)`** returns the traditional classful letter (`'A'`, `'B'`, `'C'`, `'D'`) based on the three high-order bits of the address. The implementation uses a compact lookup table `"AAAABBCD"` indexed by `(addr.to_uint() & 0xE0000000) >> 29`, mapping the eight possible 3-bit prefix values to their historical class. Class `'D'` (prefix bits `110x` and `111x` — indices 6 and 7) corresponds to multicast. This function is classically deprecated for real routing decisions but remains useful for diagnostics and logging. + +## Relationship to the Overlay Layer + +The primary consumer of `is_public` is the peer handshake logic in `overlay/detail/Handshake.cpp`. During connection establishment, the node inserts its own public IP into the HTTP upgrade headers only when the remote address is public — preventing disclosure of internal topology when a peer connects from a private network. The overlay also validates `Local-IP` / `Remote-IP` header consistency using `is_public`, rejecting connections where a publicly-reachable peer reports conflicting address information that could indicate a misconfigured or spoofed connection. + +## Dependency on `hash_append` + +The `#include ` inclusion at line 3 is shared infrastructure pulled in for the wider `beast::IP` module rather than being required by the three functions declared here. The hashing machinery is used by `IPAddress.h` and `IPEndpoint.h` when addresses appear as hash map keys. + +## Design Note + +The header introduces no classes and no template machinery — it is intentionally thin. The `boost::asio` type does all the heavy lifting for address representation, parsing, and comparison; this file only layers on the XRPL-specific semantic predicates (`is_private`, `is_public`) and the legacy diagnostic utility (`get_class`) that the rest of the codebase needs. Keeping the functions as non-member free functions in the `beast::IP` namespace means they compose uniformly with the `Address` (version-agnostic) overloads in `IPAddress.h` without any inheritance or virtual dispatch. \ No newline at end of file diff --git a/include/xrpl/beast/net/IPAddressV6.h.ai.json b/include/xrpl/beast/net/IPAddressV6.h.ai.json new file mode 100644 index 0000000000..6f75aac7b7 --- /dev/null +++ b/include/xrpl/beast/net/IPAddressV6.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [ + { + "lineno": 11, + "name": "addr" + }, + { + "lineno": 15, + "name": "addr" + } + ], + "classes": [], + "description": "Provides utility functions to check if an IPv6 address is private or public within the beast::IP namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/net/IPAddressV6.h", + "functions": [ + { + "args": [ + "AddressV6 const& addr" + ], + "lineno": 11, + "name": "is_private" + }, + { + "args": [ + "AddressV6 const& addr" + ], + "lineno": 15, + "name": "is_public" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 7, + "name": "IP" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/net/IPAddressV6.h.ai.md b/include/xrpl/beast/net/IPAddressV6.h.ai.md new file mode 100644 index 0000000000..f5ee209613 --- /dev/null +++ b/include/xrpl/beast/net/IPAddressV6.h.ai.md @@ -0,0 +1,44 @@ +# `IPAddressV6.h` — IPv6 Address Classification Utilities + +This header is part of the `beast::IP` networking abstraction layer within rippled's embedded Beast library. Its sole purpose is to declare two classification predicates for IPv6 addresses: `is_private()` and `is_public()`. It mirrors the parallel structure of `IPAddressV4.h`, which provides the same pair of predicates for IPv4 along with an additional `get_class()` helper. + +## Type Alias Strategy + +Rather than wrapping `boost::asio::ip::address_v6` in a new class, the header simply re-exports it as `beast::IP::AddressV6`: + +```cpp +using AddressV6 = boost::asio::ip::address_v6; +``` + +This is a deliberate zero-cost abstraction. Beast gets a stable name in its own namespace without paying any overhead — no wrapper class, no virtual dispatch, no conversion. The same pattern is used for `AddressV4` and the version-agnostic `Address` type. The uniformity means callers can use `beast::IP::AddressV6` throughout and remain insulated from any future change to the underlying Asio type. + +## The Classification Functions + +`is_private()` and `is_public()` are declared here but defined in `IPAddressV6.cpp`. The implementation reveals the actual logic: + +```cpp +bool is_private(AddressV6 const& addr) { + return ( + ((addr.to_bytes()[0] & 0xfd) != 0) || // TODO fc00::/8 too? + (addr.is_v4_mapped() && + is_private(boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, addr)))); +} +``` + +The first clause checks whether the most significant byte ANDed with `0xfd` is nonzero, targeting the `fc00::/7` Unique Local Address (ULA) range (RFC 4193), which covers `fc00::` through `fdff::`. The `TODO` comment flags genuine uncertainty: the mask `0xfd` catches `fd00::/8` but may not fully cover `fc00::/8`. The second clause handles IPv4-mapped IPv6 addresses (the `::ffff:0:0/96` prefix), delegating to `is_private(AddressV4)` after extracting the embedded IPv4 address — this correctly classifies RFC 1918 addresses embedded in IPv6 form. + +`is_public()` is defined as the logical complement: an address is public if it is neither private nor multicast. The `TODO` comment in the implementation acknowledges that this definition may not be fully rigorous. + +## Role in the Dispatch Chain + +The broader `IPAddress.h` header shows why these per-family predicates exist. Its version-agnostic `is_private(Address const&)` dispatches based on address family: + +```cpp +return (addr.is_v4()) ? is_private(addr.to_v4()) : is_private(addr.to_v6()); +``` + +`IPAddressV6.h` provides one half of that dispatch pair. The design avoids any runtime polymorphism — the dispatch is a simple conditional at the call site, and both branches resolve to non-virtual free functions. This keeps network classification paths entirely allocation-free and inline-friendly. + +## Usage Context + +These predicates are used throughout rippled to make peering and overlay decisions — for example, determining whether a discovered peer address should be treated as externally reachable, or whether a connection should be filtered as a private/internal node. The separation of IPv4 and IPv6 handling into distinct files (`IPAddressV4.h` / `IPAddressV6.h`) keeps classification logic clearly delineated by protocol version, making it straightforward to audit or extend either family independently. \ No newline at end of file diff --git a/include/xrpl/beast/net/IPEndpoint.h.ai.json b/include/xrpl/beast/net/IPEndpoint.h.ai.json new file mode 100644 index 0000000000..c960556800 --- /dev/null +++ b/include/xrpl/beast/net/IPEndpoint.h.ai.json @@ -0,0 +1,125 @@ +{ + "args": [ + { + "lineno": 17, + "name": "addr" + }, + { + "lineno": 17, + "name": "port" + }, + { + "lineno": 22, + "name": "s" + }, + { + "lineno": 41, + "name": "port" + }, + { + "lineno": 47, + "name": "port" + } + ], + "classes": [ + { + "args": [ + "Address const& addr, Port port = 0", + "std::string const& s" + ], + "lineno": 13, + "name": "Endpoint" + }, + { + "args": [], + "lineno": 124, + "name": "hash<::beast::IP::Endpoint>" + }, + { + "args": [], + "lineno": 134, + "name": "hash<::beast::IP::Endpoint>" + } + ], + "description": "Defines a version-independent IP endpoint class (address and port), utility functions for endpoint manipulation, and hash support for std and boost.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/net/IPEndpoint.h", + "functions": [ + { + "args": [ + "Endpoint const& endpoint" + ], + "lineno": 74, + "name": "is_loopback" + }, + { + "args": [ + "Endpoint const& endpoint" + ], + "lineno": 80, + "name": "is_unspecified" + }, + { + "args": [ + "Endpoint const& endpoint" + ], + "lineno": 86, + "name": "is_multicast" + }, + { + "args": [ + "Endpoint const& endpoint" + ], + "lineno": 92, + "name": "is_private" + }, + { + "args": [ + "Endpoint const& endpoint" + ], + "lineno": 98, + "name": "is_public" + }, + { + "args": [ + "Endpoint const& endpoint" + ], + "lineno": 104, + "name": "to_string" + }, + { + "args": [ + "OutputStream& os", + "Endpoint const& endpoint" + ], + "lineno": 109, + "name": "operator<<" + }, + { + "args": [ + "std::istream& is", + "Endpoint& endpoint" + ], + "lineno": 116, + "name": "operator>>" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "beast" + }, + { + "lineno": 9, + "name": "IP" + }, + { + "lineno": 123, + "name": "std" + }, + { + "lineno": 133, + "name": "boost" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/net/IPEndpoint.h.ai.md b/include/xrpl/beast/net/IPEndpoint.h.ai.md new file mode 100644 index 0000000000..5c1cdd7cfe --- /dev/null +++ b/include/xrpl/beast/net/IPEndpoint.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/beast/net/IPEndpoint.h` + +## Role in the System + +`IPEndpoint.h` defines `beast::IP::Endpoint`, the canonical representation of a network address and port used throughout the XRPL node. It pairs an IP address with a 16-bit port number in a single, version-independent value type — the primary unit exchanged when the node discovers, stores, connects to, and classifies peers. The `Resolver` interface, PeerFinder subsystem, and resource-tracking components all traffic in `vector`, making this a foundational type in the peer-to-peer networking stack. + +## Address Abstraction + +`Address` is a `using` alias for `boost::asio::ip::address`, which transparently represents both IPv4 and IPv6 addresses without requiring template polymorphism or inheritance. This choice is deliberate: `Endpoint` gains dual-stack capability at zero cost while keeping its API simple. The two convenience accessors `is_v4()` / `is_v6()` and the casting helpers `to_v4()` / `to_v6()` inline straight through to the underlying Boost.Asio address, ensuring no overhead at call sites that need to branch on protocol family. `Port` is simply `std::uint16_t`, mirroring the TCP/UDP port range exactly. + +## Construction and String Parsing + +There are three construction paths. The default constructor produces an unspecified endpoint (zero port, default `Address`). The explicit constructor takes an `Address` and an optional `Port` defaulting to `0`. String construction is handled by two static factories: `from_string_checked()` returns `std::optional` — the preferred path when the input is untrusted — while `from_string()` returns an unspecified default endpoint on failure rather than propagating an error, which is appropriate for contexts that have already validated input or can tolerate a silent fallback. + +The actual parsing is implemented in the companion `.cpp` file via `operator>>` on a `std::istream`. It handles three formats: + +- **IPv4**: `192.168.0.1:8080` — colon is the address/port separator detected after the first `.` character. +- **IPv6 (bracketed)**: `[fe80::1]:8080` — opening `[` signals IPv6 mode; parsing consumes until the matching `]` then expects an optional `:port`. +- **Legacy space-separated**: `192.168.0.1 8080` — a space terminates the address field, with the port read as an integer from the remaining stream. This format is preserved specifically for backward compatibility with stored peer data. + +`from_string_checked()` adds an outer guard: strings longer than 64 characters are rejected before touching the stream, providing a lightweight denial-of-service barrier. The stream parser also bounds-checks against `INET6_ADDRSTRLEN` internally and sets `failbit` on any invalid character or oversized token, ensuring parse failures are communicated cleanly. + +## Immutability and the `at_port()` Pattern + +`Endpoint` is a value type: it carries its data by value, and all accessors return by value or const reference. There is no `set_port()` mutator. Instead, `at_port(Port)` returns a new `Endpoint` with the same address but a different port. This functional style prevents accidental partial mutation and makes endpoint transformations explicit at the call site — useful, for example, when normalising an endpoint from a peer advertisement to a canonical port. + +## String Formatting + +`to_string()` mirrors the parsing contract: IPv4 endpoints format as `addr:port` and IPv6 endpoints as `[addr]:port`, in both cases omitting the port suffix when `port() == 0`. Buffer capacity is pre-reserved using `INET6_ADDRSTRLEN` to avoid reallocations for the common case. + +## Comparison and Ordering + +The full six-operator comparison suite is provided. Only `operator==` (exact address and port equality) and `operator<` (lexicographic: address first, then port) are defined in the `.cpp`; the remaining four operators are synthesised inline from those two following standard practice. The ordering makes `Endpoint` usable as a `std::map` key or in sorted containers without any additional comparator. + +## Hashing + +`Endpoint` participates in the beast `hash_append` protocol: the `hash_append` friend function feeds both `m_addr` and `m_port` into a `Hasher` object. The `Address` hash already handles the IPv4/IPv6 branch by hashing the respective byte array (`to_bytes()`). This is then wired to both `std::hash` and `boost::hash` through `beast::uhash<>`, which defaults to `xxhasher`. The dual specialisation allows `Endpoint` to be used as a key in `std::unordered_map`, `boost::unordered_map`, or any container using `uhash` directly, covering the full range of hash-table variants used in the rippled codebase. + +## Property Predicates + +The free functions `is_loopback`, `is_unspecified`, `is_multicast`, `is_private`, and `is_public` are thin inline delegates that call the corresponding `Address`-level predicate. They exist so that call sites holding only an `Endpoint` do not need to extract the address first, keeping peer classification code concise. `is_private` and `is_public` branch internally on IPv4 vs IPv6 to apply the correct RFC-defined private range checks. \ No newline at end of file diff --git a/include/xrpl/beast/rfc2616.h.ai.json b/include/xrpl/beast/rfc2616.h.ai.json new file mode 100644 index 0000000000..d9c3716f05 --- /dev/null +++ b/include/xrpl/beast/rfc2616.h.ai.json @@ -0,0 +1,210 @@ +{ + "args": [ + { + "lineno": 19, + "name": "c1" + }, + { + "lineno": 19, + "name": "c2" + }, + { + "lineno": 27, + "name": "c" + }, + { + "lineno": 34, + "name": "c" + }, + { + "lineno": 46, + "name": "first" + }, + { + "lineno": 46, + "name": "last" + }, + { + "lineno": 58, + "name": "s" + }, + { + "lineno": 74, + "name": "first" + }, + { + "lineno": 74, + "name": "last" + }, + { + "lineno": 74, + "name": "delim" + }, + { + "lineno": 120, + "name": "first" + }, + { + "lineno": 120, + "name": "last" + }, + { + "lineno": 126, + "name": "s" + }, + { + "lineno": 134, + "name": "begin" + }, + { + "lineno": 134, + "name": "end" + }, + { + "lineno": 222, + "name": "s1" + }, + { + "lineno": 222, + "name": "s2" + }, + { + "lineno": 227, + "name": "field" + }, + { + "lineno": 235, + "name": "value" + }, + { + "lineno": 235, + "name": "token" + }, + { + "lineno": 245, + "name": "m" + } + ], + "classes": [ + { + "args": [], + "lineno": 15, + "name": "ci_equal_pred" + }, + { + "args": [ + "begin", + "end" + ], + "lineno": 134, + "name": "list_iterator" + } + ], + "description": "Utilities for parsing and handling HTTP header lists and tokens according to RFC 2616, including case-insensitive comparison, whitespace trimming, splitting comma-separated values, and iterating over header lists.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/rfc2616.h", + "functions": [ + { + "args": [ + "c" + ], + "lineno": 27, + "name": "is_lws" + }, + { + "args": [ + "c" + ], + "lineno": 34, + "name": "is_white" + }, + { + "args": [ + "first", + "last" + ], + "lineno": 46, + "name": "trim_right" + }, + { + "args": [ + "s" + ], + "lineno": 58, + "name": "trim_right" + }, + { + "args": [ + "first", + "last", + "delim" + ], + "lineno": 74, + "name": "split" + }, + { + "args": [ + "first", + "last" + ], + "lineno": 120, + "name": "split_commas" + }, + { + "args": [ + "s" + ], + "lineno": 126, + "name": "split_commas" + }, + { + "args": [], + "lineno": 170, + "name": "increment" + }, + { + "args": [ + "s1", + "s2" + ], + "lineno": 222, + "name": "ci_equal" + }, + { + "args": [ + "field" + ], + "lineno": 227, + "name": "make_list" + }, + { + "args": [ + "value", + "token" + ], + "lineno": 235, + "name": "token_in_list" + }, + { + "args": [ + "m" + ], + "lineno": 245, + "name": "is_keep_alive" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "beast" + }, + { + "lineno": 13, + "name": "rfc2616" + }, + { + "lineno": 14, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/rfc2616.h.ai.md b/include/xrpl/beast/rfc2616.h.ai.md new file mode 100644 index 0000000000..702f5e1cf9 --- /dev/null +++ b/include/xrpl/beast/rfc2616.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/beast/rfc2616.h` + +## Role in the System + +This header provides HTTP header parsing primitives for the XRPL node's networking layers. RFC 2616 is the original HTTP/1.1 specification; many of its grammar rules for comma-separated header lists and token/quoted-string syntax remain the practical standard for headers like `Connection`, `Upgrade`, `Accept`, and XRPL-specific fields like `Connect-As`. The file lives under `beast::rfc2616` — a thin layer on top of Boost.Beast that fills gaps or provides more ergonomic interfaces than the library exposes directly. + +## Two Parsing Strategies: Eager vs. Lazy + +The header offers two approaches for consuming comma-delimited header values, and the distinction matters. + +**`split_commas()`** (lines 176–187) is the eager path. It fully materializes the parsed token list into a `std::vector`, processing quoted-string escapes and stripping linear whitespace along the way. The underlying `split()` template handles the RFC 2616 grammar directly: it interprets `"..."` as a quoted-string (collapsing backslash escape sequences), treats the delimiter as a separator, skips linear white space (`SP` and `HTAB` only, not arbitrary whitespace), and trims trailing whitespace from each token before collecting it. Callers that need random access or multiple traversals benefit from this form. + +**`list_iterator`** (lines 199–324) is the lazy path, implementing `ForwardIterator` over `boost::string_ref`. It avoids heap allocation for the token list: each `operator++()` call advances `it_` through the underlying character sequence and sets `value_` to a zero-copy `string_ref` window into the original data. This is intentionally allocation-free and suitable for one-pass checks. The tradeoff noted in the comments is that values may still contain raw backslash escapes — the iterator does *not* process escape sequences, unlike `split()`. + +`make_list()` wraps `list_iterator` into a `boost::iterator_range`, enabling range-for loops over a raw `boost::string_ref` header value without materializing anything. + +## Helper Utilities + +`ci_equal()` compares two `string_ref` values case-insensitively using `detail::ci_equal_pred`, which calls `std::tolower` with an explicit `static_cast` guard — a necessary defensive cast to avoid undefined behavior from passing a `char` (potentially negative on signed platforms) to a `` function. + +`token_in_list()` composes `make_list()` and `ci_equal()` into the canonical check: "does this header field contain this token?" HTTP header tokens are case-insensitive by spec, so the case-folding is correct by design, not convenience. + +`is_keep_alive()` encodes HTTP/1.0 vs. HTTP/1.1 persistence semantics in a single templated function over Boost.Beast's `http::message`. In HTTP/1.0, connections default to closing and require an explicit `Connection: keep-alive` to persist. In HTTP/1.1 and above, connections are persistent by default and require `Connection: close` to opt out. This dual logic is exactly what the spec mandates and is easy to get backwards. + +## Detail Namespace + +`detail::is_lws()` and `detail::is_white()` make a meaningful distinction: `is_lws` recognizes only space and horizontal tab — the "linear white space" characters that may appear within a header value — while `is_white` recognizes the full set of C whitespace characters. The `split()` function uses `is_lws` to skip intra-value whitespace, since `\n`, `\r`, and `\f` are not valid inside a header field value unless they are part of an obsolete line-folding sequence. `trim_right()` uses `is_white` to strip trailing whitespace from tokens before accumulation, which is slightly more permissive. + +## Call Sites + +In `OverlayImpl.cpp`, the `Connect-As` header of incoming peer HTTP requests is parsed with `split_commas()` to verify that `"peer"` appears in the list, gating XRPL overlay connection acceptance. `is_keep_alive()` is used there to decide whether to set the `handoff.keep_alive` flag on the resulting connection handoff. + +In `Handshake.cpp`, `token_in_list()` checks XRPL protocol feature negotiation headers for specific capability tokens during the peer handshake phase. + +In `ServerHandler.cpp` and `PlainHTTPPeer.h`, `is_keep_alive()` governs whether the RPC server keeps the TCP connection open after responding or performs a receive-side shutdown. + +## Design Notes + +The `increment()` method in `list_iterator` is defined as a `template ` member — a pattern used to defer instantiation to the `.cpp` equivalent within a header-only context, keeping the definition out of the class body for readability while still inlining at compile time. + +The file is titled `rfc2616.h` but imports `boost/beast/http/rfc7230.hpp`. RFC 7230 (2014) formally obsoletes RFC 2616 for HTTP/1.1; the `beast::http::token_list` used inside `is_keep_alive()` comes from the newer RFC. The header name reflects the historical origin of the parsing grammar rather than a strict standard version alignment. \ No newline at end of file diff --git a/include/xrpl/beast/test/yield_to.h.ai.json b/include/xrpl/beast/test/yield_to.h.ai.json new file mode 100644 index 0000000000..04b928df8a --- /dev/null +++ b/include/xrpl/beast/test/yield_to.h.ai.json @@ -0,0 +1,86 @@ +{ + "args": [ + { + "lineno": 32, + "name": "concurrency" + }, + { + "lineno": 67, + "name": "f0" + }, + { + "lineno": 67, + "name": "fn..." + }, + { + "lineno": 78, + "name": "f" + }, + { + "lineno": 78, + "name": "fn..." + } + ], + "classes": [ + { + "args": [ + "concurrency" + ], + "lineno": 18, + "name": "enable_yield_to" + } + ], + "description": "Provides a mix-in class to support testing asynchronous Boost.Asio code using coroutines, allowing test functions to be launched inside coroutines and synchronizing their completion.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/test/yield_to.h", + "functions": [ + { + "args": [ + "concurrency" + ], + "lineno": 32, + "name": "enable_yield_to" + }, + { + "args": [], + "lineno": 39, + "name": "~enable_yield_to" + }, + { + "args": [], + "lineno": 46, + "name": "get_io_context" + }, + { + "args": [ + "f0", + "fn..." + ], + "lineno": 67, + "name": "yield_to" + }, + { + "args": [], + "lineno": 74, + "name": "spawn" + }, + { + "args": [ + "f", + "fn..." + ], + "lineno": 78, + "name": "spawn" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "beast" + }, + { + "lineno": 14, + "name": "test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/test/yield_to.h.ai.md b/include/xrpl/beast/test/yield_to.h.ai.md new file mode 100644 index 0000000000..3feaaee453 --- /dev/null +++ b/include/xrpl/beast/test/yield_to.h.ai.md @@ -0,0 +1,35 @@ +# `yield_to.h` — Coroutine Test Harness Mix-in + +## Role and Problem Statement + +Testing asynchronous Boost.Asio code presents a structural challenge: async operations expect to run inside an `io_context` event loop, but unit test functions are synchronous entry points. Naively spinning up an `io_context` per test, manually posting work, and waiting for results produces boilerplate that obscures intent and is error-prone to write correctly. + +`enable_yield_to` solves this by providing a reusable mix-in base class that encapsulates the `io_context`, a thread pool, and a synchronization barrier. Test classes derive from it and call `yield_to()` to launch one or more async test functions — each wrapped in a Boost.Asio stackful coroutine — then block until all have completed. The result is test code that reads sequentially despite driving genuinely asynchronous operations. Both `io_latency_probe_test` and `ServerStatus_test` use this pattern, inheriting from both `beast::unit_test::suite` and `beast::test::enable_yield_to` simultaneously. + +## Class Design + +`enable_yield_to` is intentionally a protected-state mix-in rather than a standalone fixture helper. `ios_` is `protected` so derived test classes can post their own work or pass it directly to the components under test without requiring an accessor. The remaining synchronization state (`work_`, `threads_`, `m_`, `cv_`, `running_`) is `private`, enforcing that subclasses interact only through the `yield_to()` interface and `get_io_context()`. + +The constructor accepts a `concurrency` parameter (defaulting to 1) and immediately starts that many threads running `ios_.run()`. An `executor_work_guard` is installed on construction to prevent the `io_context` from returning prematurely before any work is posted. The destructor tears this down cleanly: resetting the `work_` guard (via `boost::none`) allows the `io_context` to drain and the threads to exit naturally, after which they are `join()`ed. This is the canonical Asio shutdown pattern, and using `boost::optional` for the guard makes the reset explicit without requiring a heap allocation. + +## The `yield_to` / `spawn` Split + +`yield_to()` is the public entry point. It takes one or more callable objects, sets `running_` to their count, recursively posts all of them via `spawn()`, then blocks on the condition variable until `running_` reaches zero. + +`spawn()` uses a recursive variadic template to peel callables one at a time, posting each to `ios_` via `boost::asio::spawn`. The base-case overload (empty parameter pack, no-op body) terminates the recursion. This pattern avoids the need for an intermediate container of type-erased callables — each lambda captures its specific callable by reference and is posted directly. + +Each spawned coroutine body calls `f(yield)`, then decrements `running_` under the mutex and notifies the condition variable if it reaches zero. This is the only cross-thread synchronization point: `yield_to()` blocks on the calling thread while all coroutines execute on the `io_context` threads. + +## Stack Sizing Decision + +Each coroutine is allocated a fixed 2 MB stack via `boost::context::fixedsize_stack(2 * 1024 * 1024)`. This is a deliberate tradeoff: the default segmented or pooled stack allocators can be tricky to tune, and test coroutines often call deeply into the system under test. 2 MB is generous enough to avoid stack overflow in almost any test scenario at the cost of higher virtual memory usage. Since this is a test utility and coroutine counts are small, the cost is acceptable. + +## Exception Handling + +The completion token passed to `boost::asio::spawn` includes an exception handler lambda that rethrows any non-null `std::exception_ptr`. This ensures that exceptions thrown inside a coroutine — such as a failing assertion — propagate out and cause the test to fail visibly, rather than being silently swallowed by the Asio machinery. Without this handler, exceptions from spawned coroutines would be lost entirely. + +## Concurrency Considerations + +`running_` is written before `spawn()` is called and before any coroutine can complete, so there is no race between the initial write and coroutine decrements. All decrements happen under `m_`, and the condition variable check in `yield_to()` holds the lock, making the termination detection race-free. However, `yield_to()` itself is not reentrant — calling it from multiple threads simultaneously would produce undefined behavior on `running_`. This is acceptable because test fixtures are normally driven from a single test thread. + +The `yield_context` type alias re-exported as a public member allows consuming test files to accept `enable_yield_to::yield_context` as the coroutine argument type without directly including Boost.Asio coroutine headers, keeping per-test includes minimal. \ No newline at end of file diff --git a/include/xrpl/beast/type_name.h.ai.json b/include/xrpl/beast/type_name.h.ai.json new file mode 100644 index 0000000000..90ec20b104 --- /dev/null +++ b/include/xrpl/beast/type_name.h.ai.json @@ -0,0 +1,25 @@ +{ + "args": [ + { + "lineno": 9, + "name": "T" + } + ], + "classes": [], + "description": "Provides a utility function to obtain the demangled type name of a C++ type, including qualifiers and reference types, primarily for debugging or introspection purposes.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/type_name.h", + "functions": [ + { + "args": [], + "lineno": 10, + "name": "type_name" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/type_name.h.ai.md b/include/xrpl/beast/type_name.h.ai.md new file mode 100644 index 0000000000..07ed8dfab8 --- /dev/null +++ b/include/xrpl/beast/type_name.h.ai.md @@ -0,0 +1,33 @@ +# `beast/type_name.h` — Human-Readable Type Name Demangling + +This header provides a single template utility, `beast::type_name()`, that returns a human-readable `std::string` representation of any C++ type, including its cv-qualifiers and reference category. Its purpose is purely diagnostic: producing legible type names for log messages, error strings, and counter labels rather than the raw mangled symbols that `typeid().name()` emits on GCC and Clang by default. + +## The Problem It Solves + +On MSVC, `typeid(T).name()` already yields something readable. On GCC and Clang, the same call returns a mangled symbol like `N4xrpl11KnownFormatsINS_18LedgerEntryFormatsEEE`, which is useless in a log file or error message. The standard C++ ABI exposes `abi::__cxa_demangle()` from `` to reverse this — but calling it correctly (allocating and freeing the result buffer, guarding against MSVC where the header doesn't exist) requires boilerplate that consumers shouldn't repeat. `type_name.h` encapsulates that boilerplate once. + +## How `type_name()` Works + +The function begins by stripping the reference from `T` via `std::remove_reference`, storing the result as `TR`. This is necessary because `typeid` ignores reference types entirely — `typeid(int&)` and `typeid(int)` return the same `type_info` — so applying `typeid` to the unreferenced type avoids a subtle loss of information while ensuring the demangler receives a concrete type. + +On non-MSVC platforms, `abi::__cxa_demangle()` is called with the mangled name. The function returns a heap-allocated C string that the caller must `free()`. The implementation captures this pointer, copies it into a `std::string`, and immediately releases it — avoiding both a leak and a raw owning pointer lifetime issue. On MSVC, the `#ifndef _MSC_VER` guard skips this block entirely, leaving the already-readable MSVC name intact. + +After demangling, the function manually reconstructs the qualifiers that were shed when the reference was stripped: `const` and `volatile` are appended as suffixes if `TR` carries them, and `&` or `&&` is appended depending on whether `T` was an lvalue or rvalue reference. This ordering — demangled base name, then cv-qualifiers, then reference category — matches the way C++ types are conventionally read right-to-left. + +## Usage Across the Codebase + +The function appears in four distinct contexts, each illustrating a different diagnostic use: + +**`contract.h`** calls `beast::type_name()` inside the `Throw()` template to emit a log warning like `"Throwing exception of type std::runtime_error: ..."` before each throw. This makes every exception event a traceable log entry, surviving even broad `catch(...)` handlers that would otherwise swallow both the exception and any evidence of its type. + +**`CountedObject.h`** uses `beast::type_name()` to name a function-local static `Counter` object that tracks how many instances of a class are alive. The static is initialized exactly once (C++11 thread-safe static initialization), and the demangled name is what appears in diagnostic dumps of live object counts — without it, the registry would contain meaningless mangled strings. + +**`KnownFormats.h`** uses it in a CRTP base class constructor: `KnownFormats` initializes `name_` with `beast::type_name()`, letting the base class self-label without requiring derived classes to pass their names explicitly. + +**`SlabAllocator.h`** uses it to generate a meaningful `std::runtime_error` message when a duplicate slab size is registered: `"SlabAllocatorSet: duplicate slab size"`. + +## Design Notes + +The function is not on any hot path — it is called during exception construction, counter registration (a one-time static init), or at object construction time in CRTP registries. The heap allocation inside `__cxa_demangle` is therefore acceptable. + +The decision to append qualifiers after demangling rather than before is intentional: `__cxa_demangle` takes a mangled name, not a qualified type, and the mangled name already encodes the base type. Qualifiers from the `T` template parameter must be re-derived from `std::is_const`, `std::is_volatile`, and the reference traits because they were discarded by the `remove_reference` strip. This two-phase approach — demangle the stripped type, then re-attach qualifier context — correctly reconstructs names like `"std::string const&"` from a `T = const std::string&` instantiation. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test.h.ai.json b/include/xrpl/beast/unit_test.h.ai.json new file mode 100644 index 0000000000..2452aab492 --- /dev/null +++ b/include/xrpl/beast/unit_test.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "This file includes various headers related to unit testing in the beast library and defines a macro BEAST_EXPECT for asserting test conditions with file and line information.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test.h.ai.md b/include/xrpl/beast/unit_test.h.ai.md new file mode 100644 index 0000000000..570bdca340 --- /dev/null +++ b/include/xrpl/beast/unit_test.h.ai.md @@ -0,0 +1,50 @@ +# `include/xrpl/beast/unit_test.h` — Beast Unit Test Framework Entry Point + +## Role in the System + +`unit_test.h` is the single-include umbrella for the Beast unit testing framework embedded inside the XRPL codebase. Rather than depending on an external framework such as Google Test or Boost.Test, rippled ships its own lightweight xUnit-style harness under the `beast::unit_test` namespace. This header stitches together all subsystem headers and defines the `BEAST_EXPECT` diagnostic macro, giving any translation unit full access to the framework with one `#include`. + +## Architecture Overview + +The framework is layered into four distinct concerns, each in its own header, all assembled here: + +**Registration** (`global_suites.h`, `suite_list.h`, `suite_info.h`) — Test suites are registered into a process-global singleton at static initialization time. `suite_info` bundles a suite's name, module, library, a `manual` flag, a numeric priority, and a `std::function` factory callable. The `operator<` on `suite_info` sorts by descending priority first, then alphabetically by library/module/name — a deliberate design choice to run longer-running suites earlier when tests execute in parallel. The `global_suites()` function returns a `const` reference to the singleton; a separate `detail::global_suites()` returns the mutable variant, used only during registration. This const/mutable split prevents tests from accidentally mutating the registry at runtime. + +**Suite base class** (`suite.h`) — Every test inherits from `beast::unit_test::suite` and overrides the pure-virtual `run()` method. The class provides three categories of assertion helpers: `expect()` for boolean conditions, `except()`/`unexcept()` (deprecated) for exception presence/absence, and `unexpected()` (deprecated, inverse of `expect`). All modern code should use `BEAST_EXPECT` or `BEAST_EXPECTS` macros, which forward `__FILE__` and `__LINE__` through to `expect(..., file, line)` so failure messages show the source location rather than the generic call site. + +The suite's `fail()` implementation checks an `abort_` flag set by the caller via `testcase(name, abort_on_fail)`. When set, a failure throws the private `abort_exception`, which is caught and silently swallowed by `suite::run(runner&)`, terminating the current suite without propagating outward. This lets a suite author declare that a particular testcase is so fundamental that further assertions are meaningless after the first failure, without killing the entire test run. + +**Runner interface** (`runner.h`) — `runner` is a pure-observer abstract base that receives a stream of lifecycle events: `on_suite_begin`, `on_suite_end`, `on_case_begin`, `on_case_end`, `on_pass`, `on_fail`, and `on_log`. All `pass()`, `fail()`, and `log()` entry points on the runner are mutex-protected with a `std::recursive_mutex`, supporting concurrent test execution from `thread` objects within a suite. The runner also carries a freeform argument string (`arg()`) that suites may read to customize behaviour — a lightweight parameterization mechanism that avoids the complexity of a full parameterized-test system. + +**Results data model** (`results.h`) — `case_results`, `suite_results`, and `results` form a three-level nested hierarchy mirroring Suite → Testcase → Condition. Each level maintains separate `total` and `failed` counters, updated incrementally as results are inserted. The `detail::const_container` adapter (in `detail/const_container.h`) exposes a read-only view of the underlying `std::vector` to outside code while granting write access only to the class itself — a common encapsulation pattern in this codebase. + +**Reporting** (`recorder.h`, `reporter.h`) — Concrete `runner` subclasses that collect results or render them to an output stream. + +## The Registration Macros + +The `BEAST_DEFINE_TESTSUITE` family of macros is the glue between user-written test classes and the global registry: + +```cpp +#define BEAST_DEFINE_TESTSUITE(Class, Module, Library) \ + BEAST_DEFINE_TESTSUITE_INSERT(Class, Module, Library, false, 0) +``` + +`BEAST_DEFINE_TESTSUITE_INSERT` expands to a namespace-scope `static` variable of type `detail::insert_suite`. Because its constructor calls `global_suites().insert(...)`, registration happens at program startup before `main()` runs — the classic self-registering test pattern that requires no central list of test classes. The variants `_MANUAL` and `_PRIO` set the `manual` flag (opt-in from the command line) and the numeric priority, respectively. Setting `BEAST_NO_UNIT_TEST_INLINE` disables all four macros, allowing build systems to exclude test suites from production binaries without conditional compilation scattered through source files. + +## The `BEAST_EXPECT` Macro + +The umbrella header defines `BEAST_EXPECT` in a slightly unusual way: + +```cpp +#define BEAST_EXPECT_S1(x) #x +#define BEAST_EXPECT_S2(x) BEAST_EXPECT_S1(x) +#define BEAST_EXPECT(cond) expect(cond, __FILE__ ":" BEAST_EXPECT_S2(__LINE__)) +``` + +The double-stringification trick (`S1`/`S2`) forces the preprocessor to expand `__LINE__` to its integer value before stringifying, which is the standard idiom for converting `__LINE__` to a string literal. The result is passed as the `reason` string (e.g., `"foo_test.cpp:42"`) to the two-argument `expect()` overload. `suite.h` also provides `BEAST_EXPECTS(cond, reason)` as a ternary-expression macro that adds user-supplied text alongside the location. + +## Key Design Decisions + +The framework avoids dynamic dispatch on the hot path for condition checking. `runner::pass()` and `runner::fail()` call the virtual `on_pass()`/`on_fail()` hooks, but the suite-level `expect()` is non-virtual and inlined — it only reaches the runner through direct pointer indirection set up at suite invocation time. This keeps assertion overhead minimal for the thousands of conditions exercised in each test run. + +The `p_this_suite()` accessor returns a pointer to a `static suite*`, establishing a per-process "currently running suite" slot. The `thread` class (declared in `suite.h` and defined in `thread.h`) is a friend of `suite` precisely so it can call `propagate_abort()` — thread-spawned work within a suite participates in the abort-on-fail protocol through this channel. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/amount.h.ai.json b/include/xrpl/beast/unit_test/amount.h.ai.json new file mode 100644 index 0000000000..6df6086d7f --- /dev/null +++ b/include/xrpl/beast/unit_test/amount.h.ai.json @@ -0,0 +1,84 @@ +{ + "args": [ + { + "lineno": 29, + "name": "n" + }, + { + "lineno": 29, + "name": "what" + }, + { + "lineno": 33, + "name": "s" + }, + { + "lineno": 33, + "name": "t" + } + ], + "classes": [ + { + "args": [ + "amount const&", + "std::size_t n, std::string const& what" + ], + "lineno": 12, + "name": "amount" + } + ], + "description": "Provides a utility class for producing nicely formatted output of amounts with units, primarily for unit testing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/amount.h", + "functions": [ + { + "args": [ + "amount const&" + ], + "lineno": 18, + "name": "operator=" + }, + { + "args": [ + "std::size_t n", + "std::string const& what" + ], + "lineno": 21, + "name": "amount" + }, + { + "args": [ + "std::ostream& s", + "amount const& t" + ], + "lineno": 24, + "name": "operator<<" + }, + { + "args": [ + "std::size_t n", + "std::string const& what" + ], + "lineno": 29, + "name": "amount" + }, + { + "args": [ + "std::ostream& s", + "amount const& t" + ], + "lineno": 33, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + }, + { + "lineno": 8, + "name": "unit_test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/amount.h.ai.md b/include/xrpl/beast/unit_test/amount.h.ai.md new file mode 100644 index 0000000000..b7a1a28e68 --- /dev/null +++ b/include/xrpl/beast/unit_test/amount.h.ai.md @@ -0,0 +1,33 @@ +# `beast/unit_test/amount.h` + +## Role and Purpose + +`amount.h` is a minimal formatting utility inside the `beast::unit_test` framework. Its sole job is to render a count and a unit label as a grammatically correct English phrase, automatically pluralizing the label when the count is not exactly one. For example, it turns `(1, "suite")` into `"1 suite"` and `(3, "suite")` into `"3 suites"`. The class exists because the test reporter's final summary line repeatedly formats counts of suites, cases, tests, and failures — keeping that logic centralized in a tiny helper avoids repetition and keeps the output consistent. + +## Design of the `amount` Class + +The class stores a `std::size_t` count (`n_`) and a **reference** to a `std::string` label (`what_`). Holding a reference rather than a copy is intentional: `amount` objects are meant to be short-lived temporaries created inline within a stream expression, so the referent (typically a string literal wrapped in a `std::string`) always outlives the `amount`. This is why copy-assignment (`operator=`) is explicitly deleted — assigning an `amount` that holds a reference to another object's string would be semantically dangerous, and the type is not meant to be stored or reassigned anyway. The copy constructor is defaulted for passing by value if needed, but in practice all uses in the codebase construct `amount` directly in stream expressions. + +The constructor uses a dummy template parameter (`template `) rather than being defined as a plain `inline` function. This pattern is a way to provide a function definition in a header file without the `inline` keyword while still satisfying the One Definition Rule. Because a function template is implicitly `inline`-equivalent for linkage purposes, this avoids multiple-definition linker errors when the header is included in multiple translation units. + +## Pluralization Logic + +The `operator<<` overload implements the formatting: + +```cpp +s << t.n_ << " " << t.what_ << ((t.n_ != 1) ? "s" : ""); +``` + +This appends a literal `"s"` suffix for any count other than 1. The approach assumes English regular pluralization and works correctly for all four unit labels used in practice: `"suite"`, `"case"`, `"test"`, and `"failure"`. + +## Relationship to `reporter.h` + +The only consumer of `amount` within this codebase is `reporter.h`, in the destructor of the `reporter` class template: + +```cpp +os_ << fmtdur(elapsed) << ", " << amount{results_.suites, "suite"} << ", " + << amount{results_.cases, "case"} << ", " << amount{results_.total, "test"} << " total, " + << amount{results_.failed, "failure"} << std::endl; +``` + +This produces a summary line like `"1.23s, 4 suites, 19 cases, 97 tests total, 0 failures"` at the end of every test run. Using `amount` here instead of ad-hoc ternary expressions keeps each field self-describing and uniform without duplicating the pluralization condition four times. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/detail/const_container.h.ai.json b/include/xrpl/beast/unit_test/detail/const_container.h.ai.json new file mode 100644 index 0000000000..4ad72704bc --- /dev/null +++ b/include/xrpl/beast/unit_test/detail/const_container.h.ai.json @@ -0,0 +1,76 @@ +{ + "args": [ + { + "lineno": 11, + "name": "Container" + } + ], + "classes": [ + { + "args": [ + "Container" + ], + "lineno": 11, + "name": "const_container" + } + ], + "description": "Defines a template adapter class 'const_container' that provides a constrained, read-only interface to a container, intended for use in unit testing within the beast::unit_test::detail namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/detail/const_container.h", + "functions": [ + { + "args": [], + "lineno": 17, + "name": "cont" + }, + { + "args": [], + "lineno": 22, + "name": "cont" + }, + { + "args": [], + "lineno": 29, + "name": "empty" + }, + { + "args": [], + "lineno": 34, + "name": "size" + }, + { + "args": [], + "lineno": 40, + "name": "begin" + }, + { + "args": [], + "lineno": 45, + "name": "cbegin" + }, + { + "args": [], + "lineno": 50, + "name": "end" + }, + { + "args": [], + "lineno": 55, + "name": "cend" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 7, + "name": "unit_test" + }, + { + "lineno": 8, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/detail/const_container.h.ai.md b/include/xrpl/beast/unit_test/detail/const_container.h.ai.md new file mode 100644 index 0000000000..8ad6357bb5 --- /dev/null +++ b/include/xrpl/beast/unit_test/detail/const_container.h.ai.md @@ -0,0 +1,25 @@ +# `detail/const_container.h` — Read-Only Container Adapter + +`const_container` is a small CRTP-style base class template in `beast::unit_test::detail` that solves a narrow but recurring problem in the unit-test framework's result hierarchy: how do you let a class own a standard container internally while exposing only read-only access to callers, yet still let the owning class itself mutate that container? + +## Design Intent + +The framework builds up a three-level result tree — individual test conditions aggregate into `case_results`, which aggregate into `suite_results`, which aggregate into the top-level `results` class. Each level needs to store a sequence (a `std::vector` or `std::set`) and provide public iteration and size queries, but mutation — inserting new entries, tracking failure counts — must be controlled by the class itself, not by external code. + +A naive approach would be to make the container a `private` member and write `begin()`/`end()` accessors that return `const_iterator`. `const_container` formalises exactly that pattern into a reusable base, eliminating boilerplate across the four distinct container-owning types in `results.h` and `suite_list.h`. + +## Structure + +`const_container` stores the underlying container as a `private` member `m_cont`. Two `protected` overloads of `cont()` expose it — one mutable, one `const` — so only derived classes can write to it. The public interface is intentionally narrow: `empty()`, `size()`, `begin()`, `cbegin()`, `end()`, and `cend()`. Critically, both `iterator` and `const_iterator` are aliased to `cont_type::const_iterator`, so even when a caller holds a non-const reference to the derived class, the iterators they receive are immutable. There is no `operator[]`, no `front()`/`back()`, and no mutation path whatsoever at the public level. + +## How Derived Classes Use It + +Derived classes call `cont()` to reach the underlying container. In `results.h`, `suite_results::insert()` calls `cont().emplace_back(...)` and `case_results::tests_t::pass()` calls `cont().emplace_back(true)` — mutating the container through the `protected` accessor while keeping the public interface read-only. In `suite_list.h`, `suite_list::insert(...)` calls `cont().emplace(...)` on the underlying `std::set`. The symmetry is clean: the template parameter changes the container type; the access discipline stays constant. + +## Why Not Inheritance from the Container Directly? + +Publicly inheriting from `std::vector` or `std::set` would expose the entire mutable interface — `push_back`, `insert`, `erase`, `clear` — to all callers. Private inheritance would suppress the mutable members but would also require `using` declarations to re-expose each desired read-only member, which is what `const_container` does implicitly by forwarding calls to `m_cont`. Composition with a `protected` accessor is the cleaner middle ground: derived classes get full write access, callers get none. + +## Scope and Placement + +The `detail` namespace signals that `const_container` is an implementation aid, not part of the framework's public API. Nothing outside `beast::unit_test` depends on it directly. Its template parameter is unconstrained — any type satisfying the standard container concept (providing `value_type`, `size_type`, `difference_type`, `const_iterator`, `empty()`, `size()`, `cbegin()`, `cend()`) will work, which is why it can be reused with both `std::vector` and `std::set` without modification. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/global_suites.h.ai.json b/include/xrpl/beast/unit_test/global_suites.h.ai.json new file mode 100644 index 0000000000..3aee8fe209 --- /dev/null +++ b/include/xrpl/beast/unit_test/global_suites.h.ai.json @@ -0,0 +1,66 @@ +{ + "args": [ + { + "lineno": 22, + "name": "name" + }, + { + "lineno": 23, + "name": "module" + }, + { + "lineno": 24, + "name": "library" + }, + { + "lineno": 25, + "name": "manual" + }, + { + "lineno": 26, + "name": "priority" + } + ], + "classes": [ + { + "args": [ + "name", + "module", + "library", + "manual", + "priority" + ], + "lineno": 19, + "name": "insert_suite" + } + ], + "description": "Provides infrastructure for registering and managing unit test suites during static initialization in the beast::unit_test namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/global_suites.h", + "functions": [ + { + "args": [], + "lineno": 11, + "name": "global_suites" + }, + { + "args": [], + "lineno": 34, + "name": "global_suites" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + }, + { + "lineno": 8, + "name": "unit_test" + }, + { + "lineno": 10, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/global_suites.h.ai.md b/include/xrpl/beast/unit_test/global_suites.h.ai.md new file mode 100644 index 0000000000..47d3840585 --- /dev/null +++ b/include/xrpl/beast/unit_test/global_suites.h.ai.md @@ -0,0 +1,40 @@ +# `global_suites.h` — Static Registration Infrastructure for Unit Test Suites + +This file provides the two-layer mechanism that accumulates all unit test suites into a process-wide registry during C++ static initialization, before `main()` ever runs. It is a small but load-bearing piece of the beast unit test framework used throughout XRPL. + +## The Registry: `detail::global_suites()` + +The mutable registry lives inside a function-local `static suite_list` returned by `detail::global_suites()`. Using a function-local static rather than a plain global variable is the canonical C++ solution to the *static initialization order fiasco*: because the `suite_list` object is constructed on first use, it is guaranteed to exist before any other translation unit's static initializer calls `insert()` on it, regardless of link order. This matters because test suites across many `.cpp` files all register themselves via static constructors, and none of them can be ordered relative to one another. + +The public overload `beast::unit_test::global_suites()` (outside `detail`) returns a `const suite_list&` to the same underlying object. This const-at-the-boundary design means the runner, reporter, and matcher infrastructure can iterate suites freely but cannot accidentally mutate the registry after startup is complete. Only `insert_suite`, which lives inside `detail` and directly accesses `detail::global_suites()`, can ever write to it. + +## The Registration Mechanism: `insert_suite` + +`insert_suite` is a trivial template struct whose sole purpose is to perform a side effect in its constructor: + +```cpp +static beast::unit_test::detail::insert_suite + Library##Module##Class##_test_instance(#Class, #Module, #Library, manual, priority); +``` + +This is exactly how `BEAST_DEFINE_TESTSUITE_INSERT` uses it in `suite.h`. When a translation unit is loaded, the static instance of `insert_suite` is constructed, which calls `global_suites().insert(name, module, library, manual, priority)` — funneling the suite's type and metadata into the global `suite_list`. The template parameter carries the concrete suite *type*, while the constructor arguments carry its human-readable identity (`name`, `module`, `library`), whether it requires explicit invocation (`manual`), and a scheduling hint (`priority`). + +The `priority` field was added specifically to support parallel test execution: suites known to be slow get higher priority values so they are scheduled earlier, preventing long-tail stragglers from stalling the overall run. + +## Relationship to the Macro Layer + +`suite.h` defines four public macros on top of this file: +- `BEAST_DEFINE_TESTSUITE` — automatic, priority 0 +- `BEAST_DEFINE_TESTSUITE_MANUAL` — manual, priority 0 +- `BEAST_DEFINE_TESTSUITE_PRIO` — automatic, with explicit priority +- `BEAST_DEFINE_TESTSUITE_MANUAL_PRIO` — manual, with explicit priority + +All four collapse to `BEAST_DEFINE_TESTSUITE_INSERT`, which declares the static `insert_suite` variable. The `#include ` inside that `#else` block is what makes the type visible at each call site. When the preprocessor symbol `BEAST_NO_UNIT_TEST_INLINE` is defined, all four macros expand to nothing — allowing a build to compile test classes without registering them, useful when a custom registration strategy is preferred. + +## Duplicate Detection + +`suite_list::insert()` (in `suite_list.h`) maintains two debug-only `unordered_set` members — one keyed on the fully-qualified string `"library.module.name"`, one on `std::type_index(typeid(Suite))`. Both are checked via `BOOST_ASSERT` before insertion, catching double-registration bugs that could arise from ODR violations or accidental macro reuse. These checks are stripped in release builds, so the only overhead at runtime is a single `emplace` into a `std::set`. + +## Design Summary + +The file deliberately has no public state and no mutable API surface. The division between `detail::global_suites()` (mutable, used only at registration time) and `beast::unit_test::global_suites()` (const, used by runners and reporters at test time) enforces a clean lifecycle: write during static init, read thereafter. The function-local static pattern eliminates initialization-order risk without requiring any synchronization, since all static constructors run on the main thread before `main()`. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/match.h.ai.json b/include/xrpl/beast/unit_test/match.h.ai.json new file mode 100644 index 0000000000..0ba513b333 --- /dev/null +++ b/include/xrpl/beast/unit_test/match.h.ai.json @@ -0,0 +1,83 @@ +{ + "args": [ + { + "lineno": 38, + "name": "mode" + }, + { + "lineno": 38, + "name": "pattern" + }, + { + "lineno": 41, + "name": "s" + }, + { + "lineno": 110, + "name": "name" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "selector" + } + ], + "description": "Implements a selector class and utility functions for matching and selecting unit test suites based on suite, module, or library names, with support for different matching modes.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/match.h", + "functions": [ + { + "args": [ + "mode", + "pattern" + ], + "lineno": 38, + "name": "selector::selector" + }, + { + "args": [ + "s" + ], + "lineno": 41, + "name": "selector::operator()" + }, + { + "args": [ + "name" + ], + "lineno": 110, + "name": "match_auto" + }, + { + "args": [], + "lineno": 117, + "name": "match_all" + }, + { + "args": [ + "name" + ], + "lineno": 122, + "name": "match_suite" + }, + { + "args": [ + "name" + ], + "lineno": 127, + "name": "match_library" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "beast" + }, + { + "lineno": 10, + "name": "unit_test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/match.h.ai.md b/include/xrpl/beast/unit_test/match.h.ai.md new file mode 100644 index 0000000000..11a1ccad15 --- /dev/null +++ b/include/xrpl/beast/unit_test/match.h.ai.md @@ -0,0 +1,53 @@ +# `include/xrpl/beast/unit_test/match.h` + +## Role and Purpose + +This header provides the test-suite selection mechanism for the `beast::unit_test` framework embedded in the XRPL codebase. Its central abstraction is the `selector` class — a stateful, callable predicate that takes a `suite_info` object and returns `true` if that suite should be run. The `selector` is designed to be passed to a runner's iteration loop over a `suite_list`, filtering in or out individual test suites based on a user-supplied string pattern. + +The file also exposes four convenience factory functions — `match_all()`, `match_auto()`, `match_suite()`, and `match_library()` — that construct `selector` instances with the appropriate mode, providing a readable API at call sites in `Main.cpp`. + +## The `mode_t` Enum and Its Lifecycle + +The `selector` carries a `mode_t` enum that governs exactly how each call to `operator()` is evaluated. Six modes are defined, but they split into two groups: + +- **User-facing modes** (`all`, `automatch`, `suite`, `library`): constructed directly via the factory functions. +- **Internal transition modes** (`module`, `none`): entered only through state changes that `automatch` performs on itself during iteration. + +This means a single `selector` object can change its own behavior mid-iteration. The `module` and `none` modes are not constructible directly via the public API — they emerge dynamically. + +## `automatch`: Stateful Pattern Resolution + +The most architecturally significant mode is `automatch`. Rather than statically committing to one field to compare, it applies a priority-ordered discovery process each time `operator()` is called: + +1. **Exact suite name or full name** (`library.module.suite`) — if matched, the selector transitions to `none` and returns `true`. This is a one-shot match: subsequent calls always return `false`, ensuring exactly one suite runs. + +2. **Module name** — if matched, the selector transitions to `module` mode and caches the suite's `library_`. Subsequent calls enter the `module` branch, matching all non-manual suites sharing that module name. + +3. **Library name** — if matched, the selector transitions to `library` mode. Subsequent calls match all non-manual suites in that library. + +4. **Prefix match** on suite name or full name — the selector stays in `automatch` and returns `true` for non-manual suites. No mode transition occurs, so the pattern continues to match prefixes across all remaining suites. + +This cascade means the *first* association found dictates interpretation for all subsequent calls, an important subtlety when the pattern is ambiguous. A pattern like `"XRPL"` could be a module or library prefix; whichever is encountered first in sorted `suite_list` order wins. + +The `automatch` constructor also handles the degenerate case: if the pattern is empty, it demotes itself to `all`, running all non-manual suites. This is the behavior when `rippled --unittest` is invoked with no argument. + +## Manual Test Exclusion + +Only two paths ever include manually-tagged suites: + +- An exact `suite` mode match (where the user typed the exact name). +- An `automatch` exact name or full-name hit (the one-shot case). + +All other paths — `all`, `library`, `module`, and prefix matches in `automatch` — gate on `!s.manual()`. This ensures that suites marked manual are invisible to broad sweeps and require explicit opt-in. + +## Template `` Pattern + +Both the constructor and `operator()` use the `template ` idiom. This defers template instantiation, allowing the method bodies to live in a header without triggering ODR violations when multiple translation units include the file. It is functionally equivalent to an `inline` definition but relies on the template instantiation rules rather than `inline` linkage. This is a recurring pattern throughout the beast unit test headers. + +## Usage in `Main.cpp` + +The `multi_selector` class in `Main.cpp` wraps a `std::vector` to support comma-separated patterns passed to `--unittest`. Each token becomes an independent `automatch` selector. When `operator()` is called on `multi_selector`, it iterates the vector and returns `true` on the first individual selector that matches — providing logical OR semantics across patterns. This shows that `selector` is designed to be both composable and independently stateful, since each `selector` in the vector transitions its own mode independently. + +## Relationship to `suite_info` + +The `selector` is entirely dependent on `suite_info`'s public interface: `name()`, `module()`, `library()`, `full_name()`, and `manual()`. The full name is constructed as `library + "." + module + "." + name`, which the `automatch` exact match checks directly. This three-level hierarchy (library → module → suite) maps to the organisational structure of the test registry, and the `selector`'s mode cascade mirrors it precisely. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/recorder.h.ai.json b/include/xrpl/beast/unit_test/recorder.h.ai.json new file mode 100644 index 0000000000..8c15526167 --- /dev/null +++ b/include/xrpl/beast/unit_test/recorder.h.ai.json @@ -0,0 +1,95 @@ +{ + "args": [ + { + "lineno": 29, + "name": "info" + }, + { + "lineno": 39, + "name": "name" + }, + { + "lineno": 54, + "name": "reason" + }, + { + "lineno": 58, + "name": "s" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "recorder" + } + ], + "description": "Implements a test runner class 'recorder' that stores and reports unit test results, inheriting from 'runner' and overriding event methods to collect suite, case, and test outcomes.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/recorder.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "recorder" + }, + { + "args": [], + "lineno": 22, + "name": "report" + }, + { + "args": [ + "info" + ], + "lineno": 29, + "name": "on_suite_begin" + }, + { + "args": [], + "lineno": 34, + "name": "on_suite_end" + }, + { + "args": [ + "name" + ], + "lineno": 39, + "name": "on_case_begin" + }, + { + "args": [], + "lineno": 44, + "name": "on_case_end" + }, + { + "args": [], + "lineno": 50, + "name": "on_pass" + }, + { + "args": [ + "reason" + ], + "lineno": 54, + "name": "on_fail" + }, + { + "args": [ + "s" + ], + "lineno": 58, + "name": "on_log" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "beast" + }, + { + "lineno": 9, + "name": "unit_test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/recorder.h.ai.md b/include/xrpl/beast/unit_test/recorder.h.ai.md new file mode 100644 index 0000000000..6f39294100 --- /dev/null +++ b/include/xrpl/beast/unit_test/recorder.h.ai.md @@ -0,0 +1,48 @@ +# `recorder.h` — Silent Result Accumulator for Beast Unit Tests + +## Role in the System + +`recorder` is a concrete implementation of the `runner` interface that silently accumulates all test outcomes into an in-memory data structure, producing a queryable `results` object after the run completes. It is the storage-focused counterpart to the stream-printing `reporter` class: where `reporter` writes human-readable output to a stream in real time, `recorder` captures everything in a structured form suitable for programmatic inspection — summary counts, per-case outcomes, failure reasons, and log messages. + +The class lives at the intersection of `runner.h` (the lifecycle interface) and `results.h` (the hierarchical data model), bridging the event-driven execution model with a persistent, queryable representation. + +## Design: Three-Level Accumulator + +`recorder` maintains three private state fields that each correspond to one level of the test hierarchy: + +- `m_results` — the top-level aggregate across all suites +- `m_suite` — the in-progress `suite_results` for the currently-executing suite +- `m_case` — the in-progress `case_results` for the currently-executing test case + +The six virtual overrides implement a straightforward state machine: `on_suite_begin` resets `m_suite` with the suite's full name; `on_suite_end` moves the completed suite into `m_results`. The same pattern repeats one level down for cases. This is a classic "build-and-commit" accumulator — live data is assembled in a temporary and ownership is transferred to the parent container at close time using `std::move`, avoiding any copies. + +Individual `on_pass()` and `on_fail(reason)` calls simply delegate to `m_case.tests.pass()` and `m_case.tests.fail(reason)`, which maintain both the per-condition record and a running failure count in `case_results::tests_t`. Log messages via `on_log` are appended to `m_case.log`. + +## Non-Obvious Design Decision: Empty Case Pruning + +`on_case_end` contains a guard that most readers might miss: + +```cpp +if (m_case.tests.size() > 0) + m_suite.insert(std::move(m_case)); +``` + +This silently drops cases that recorded no test conditions at all. The reason lies in the `runner` base class: `runner::run()` implicitly enters a "default" unnamed testcase at the start of every suite before the suite body can call `testcase()` itself. If the suite immediately opens a named testcase, the default one closes with zero conditions. Without this guard, every suite would litter the results with a spurious empty case entry. `reporter` makes the same choice independently by not printing anything until an actual condition fires. + +## Relationship to `runner` + +`runner` is a non-copyable abstract class that owns the execution lifecycle, including a `std::recursive_mutex` protecting the `pass()`, `fail()`, and `testcase()` methods. All of that concurrency machinery lives in the base. `recorder` adds no concurrency of its own — it only implements the virtual hooks that fire after the mutex is already released. This means `m_results`, `m_suite`, and `m_case` are written exclusively from the `runner`'s locked dispatch path, keeping `recorder` inherently thread-safe without any explicit synchronization of its own. + +## Public Interface + +`recorder` exposes a single public method beyond construction: + +```cpp +results const& report() const; +``` + +This returns a const reference to the accumulated `results` object. Callers typically invoke `run()` (or `run_each()`) on the recorder to drive execution, then call `report()` afterwards. The `results` type, along with its nested `suite_results` and `case_results` types, provides summary counts (`total()`, `failed()`, `cases()`) as well as full iteration over individual test records via the `const_container` adapter pattern used throughout `results.h`. + +## Usage Context + +`reporter.h` includes `recorder.h` directly, signalling that `recorder` is the foundational storage primitive. The two classes are siblings in the runner hierarchy — both derive from `runner` independently — and either can be used wherever a `runner&` is expected. `recorder` is the right choice when the test harness needs to programmatically inspect outcomes (e.g., comparing failure counts, extracting failure reasons, or presenting results in a custom format). `reporter` is the right choice for interactive terminal output. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/reporter.h.ai.json b/include/xrpl/beast/unit_test/reporter.h.ai.json new file mode 100644 index 0000000000..7415d187cf --- /dev/null +++ b/include/xrpl/beast/unit_test/reporter.h.ai.json @@ -0,0 +1,126 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::ostream& os = std::cout" + ], + "lineno": 18, + "name": "reporter" + }, + { + "args": [ + "std::string name_ = \"\"" + ], + "lineno": 23, + "name": "case_results" + }, + { + "args": [ + "std::string name_ = \"\"" + ], + "lineno": 30, + "name": "suite_results" + }, + { + "args": [], + "lineno": 43, + "name": "results" + } + ], + "description": "Implements a real-time test reporter for the Beast unit test framework, which outputs test progress and results to a stream, including timing and failure information.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/reporter.h", + "functions": [ + { + "args": [ + "case_results const& r" + ], + "lineno": 70, + "name": "reporter<_>::suite_results::add" + }, + { + "args": [ + "suite_results const& r" + ], + "lineno": 80, + "name": "reporter<_>::results::add" + }, + { + "args": [ + "std::ostream& os" + ], + "lineno": 104, + "name": "reporter<_>::reporter" + }, + { + "args": [], + "lineno": 108, + "name": "reporter<_>::~reporter" + }, + { + "args": [ + "typename clock_type::duration const& d" + ], + "lineno": 120, + "name": "reporter<_>::fmtdur" + }, + { + "args": [ + "suite_info const& info" + ], + "lineno": 132, + "name": "reporter<_>::on_suite_begin" + }, + { + "args": [], + "lineno": 137, + "name": "reporter<_>::on_suite_end" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 141, + "name": "reporter<_>::on_case_begin" + }, + { + "args": [], + "lineno": 147, + "name": "reporter<_>::on_case_end" + }, + { + "args": [], + "lineno": 151, + "name": "reporter<_>::on_pass" + }, + { + "args": [ + "std::string const& reason" + ], + "lineno": 155, + "name": "reporter<_>::on_fail" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 162, + "name": "reporter<_>::on_log" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 15, + "name": "beast" + }, + { + "lineno": 16, + "name": "unit_test" + }, + { + "lineno": 18, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/reporter.h.ai.md b/include/xrpl/beast/unit_test/reporter.h.ai.md new file mode 100644 index 0000000000..c28d8dab5d --- /dev/null +++ b/include/xrpl/beast/unit_test/reporter.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/beast/unit_test/reporter.h` + +## Role and Purpose + +`reporter.h` provides a concrete, streaming test runner for the Beast unit test framework embedded in the XRPL codebase. Where the sibling `recorder` collects structured test results for later programmatic inspection, `reporter` is the human-facing counterpart: it writes progress directly to an `std::ostream` as tests execute and emits a formatted summary when destroyed. This real-time output model is deliberate — developers watching a long test run see each suite and case name immediately as it begins, and failures are printed inline rather than queued for a final dump. + +## Class Hierarchy and the Template-Void Pattern + +`reporter` extends `runner`, the abstract base that defines the event-notification interface (`on_suite_begin`, `on_case_begin`, `on_pass`, `on_fail`, `on_log`, etc.) and the thread-safe dispatch logic. Rather than being a plain class, `reporter` is declared as `template`, with `using reporter = detail::reporter<>` aliased at namespace scope. This is a standard header-only library technique: by making the class a template, the full method bodies can be defined in the header without violating the One Definition Rule, since each translation unit will instantiate the same `reporter` specialization. The real class lives in `namespace detail` and the public alias exposes it cleanly. + +## Three-Level Aggregation + +The class maintains three nested private structs that mirror the three tiers of the test hierarchy: + +- **`case_results`** — tracks `total` assertions and `failed` assertions within a single named test case, reset on each `on_case_begin` call. +- **`suite_results`** — accumulates case counts and totals across all cases in a suite, plus a `start` timestamp captured at construction. Reset on each `on_suite_begin` call. +- **`results`** — the aggregate across all suites, also with a `start` timestamp for overall elapsed time. + +This three-level nesting means no intermediate state is ever lost: the live `case_results_` member is merged into `suite_results_` when a case ends, which in turn is merged into `results_` when a suite ends. The three member variables (`results_`, `suite_results_`, `case_results_`) are mutable state in the reporter, updated by the virtual callbacks. + +## Slowest-Suite Tracking + +`results::add` implements a bounded top-10 list of the slowest suites. Only suites taking at least one second are candidates. The insertion uses `std::lower_bound` against a descending-sorted vector of `(name, duration)` pairs, maintaining sorted order by inserting at the correct position and trimming to `max_top = 10` entries when the list is full. This avoids sorting the entire list on every suite completion — an insertion into a 10-element vector is effectively free. If a suite finishes too slowly to displace any entry and the list is already full, it is simply discarded. The resulting list is printed in the destructor as "Longest suite times:". + +## Destructor-Based Summary + +The `~reporter()` destructor is responsible for printing the final summary line. This RAII pattern ensures the summary appears even if test execution exits early — as long as the `reporter` goes out of scope normally, the totals are printed. The summary uses the `amount` utility (from `amount.h`) for grammatically correct pluralization: `amount{n, "suite"}` produces `"1 suite"` or `"5 suites"` depending on `n`. The elapsed time is formatted by `fmtdur`, which renders sub-second durations as milliseconds (`"42ms"`) and longer durations as a fixed-precision decimal (`"3.7s"`). + +## Real-Time Output Behavior + +`on_case_begin` immediately flushes the suite and case name to the stream — providing the critical property that if a test hangs or crashes, the last visible output identifies exactly which test case was running. `on_fail` writes the failure reason prefixed with the sequential assertion number (`#3 failed: ...`), letting developers distinguish multiple failures within the same case. `on_pass` is silent, keeping noise low for passing assertions. `on_log` forwards the string verbatim, with no additional formatting, leaving log message structure entirely to the test author. + +## Relationship to `recorder` + +`recorder` and `reporter` are parallel `runner` implementations with different consumers. `recorder` accumulates a structured `results` object (containing per-case `test_results` collections) intended for programmatic querying — for example, feeding into a higher-level framework that needs to know which specific assertion failed. `reporter` discards the same information after counting it, because its only consumer is a human reading a terminal. The two are compositionally independent: neither knows about the other, and both receive the same event stream from the shared `runner` base. + +## Non-Copyability + +The copy constructor and copy-assignment operator are explicitly deleted. This is necessary because `reporter` holds a reference to an external `std::ostream` (`os_`). Allowing copies would create dangling reference hazards; the deletion makes the constraint visible and enforced at compile time rather than discovered at runtime. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/results.h.ai.json b/include/xrpl/beast/unit_test/results.h.ai.json new file mode 100644 index 0000000000..dd05b7974a --- /dev/null +++ b/include/xrpl/beast/unit_test/results.h.ai.json @@ -0,0 +1,232 @@ +{ + "args": [ + { + "lineno": 18, + "name": "pass_" + }, + { + "lineno": 22, + "name": "pass_" + }, + { + "lineno": 22, + "name": "reason_" + }, + { + "lineno": 58, + "name": "reason" + }, + { + "lineno": 70, + "name": "s" + }, + { + "lineno": 77, + "name": "name" + }, + { + "lineno": 98, + "name": "name" + }, + { + "lineno": 118, + "name": "r" + }, + { + "lineno": 123, + "name": "r" + }, + { + "lineno": 159, + "name": "r" + }, + { + "lineno": 165, + "name": "r" + } + ], + "classes": [ + { + "args": [ + "std::string const& name" + ], + "lineno": 13, + "name": "case_results" + }, + { + "args": [ + "bool pass_" + ], + "lineno": 17, + "name": "case_results::test" + }, + { + "args": [], + "lineno": 32, + "name": "case_results::tests_t" + }, + { + "args": [], + "lineno": 67, + "name": "case_results::log_t" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 94, + "name": "suite_results" + }, + { + "args": [], + "lineno": 135, + "name": "results" + } + ], + "description": "Defines classes to hold and manage the results of unit test cases, test suites, and overall test runs, including tracking passed/failed conditions and logging messages.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/results.h", + "functions": [ + { + "args": [ + "bool pass_" + ], + "lineno": 20, + "name": "case_results::test::test" + }, + { + "args": [ + "bool pass_", + "std::string const& reason_" + ], + "lineno": 24, + "name": "case_results::test::test" + }, + { + "args": [], + "lineno": 36, + "name": "case_results::tests_t::tests_t" + }, + { + "args": [], + "lineno": 41, + "name": "case_results::tests_t::total" + }, + { + "args": [], + "lineno": 47, + "name": "case_results::tests_t::failed" + }, + { + "args": [], + "lineno": 53, + "name": "case_results::tests_t::pass" + }, + { + "args": [ + "std::string const& reason" + ], + "lineno": 58, + "name": "case_results::tests_t::fail" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 70, + "name": "case_results::log_t::insert" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 77, + "name": "case_results::case_results" + }, + { + "args": [], + "lineno": 82, + "name": "case_results::name" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 98, + "name": "suite_results::suite_results" + }, + { + "args": [], + "lineno": 103, + "name": "suite_results::name" + }, + { + "args": [], + "lineno": 108, + "name": "suite_results::total" + }, + { + "args": [], + "lineno": 113, + "name": "suite_results::failed" + }, + { + "args": [ + "case_results&& r" + ], + "lineno": 118, + "name": "suite_results::insert" + }, + { + "args": [ + "case_results const& r" + ], + "lineno": 123, + "name": "suite_results::insert" + }, + { + "args": [], + "lineno": 139, + "name": "results::results" + }, + { + "args": [], + "lineno": 144, + "name": "results::cases" + }, + { + "args": [], + "lineno": 149, + "name": "results::total" + }, + { + "args": [], + "lineno": 154, + "name": "results::failed" + }, + { + "args": [ + "suite_results&& r" + ], + "lineno": 159, + "name": "results::insert" + }, + { + "args": [ + "suite_results const& r" + ], + "lineno": 165, + "name": "results::insert" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "beast" + }, + { + "lineno": 10, + "name": "unit_test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/results.h.ai.md b/include/xrpl/beast/unit_test/results.h.ai.md new file mode 100644 index 0000000000..964f20e46f --- /dev/null +++ b/include/xrpl/beast/unit_test/results.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/beast/unit_test/results.h` + +## Purpose + +This file defines the three-level data hierarchy used to persist test outcomes after a test run completes in the beast unit test framework. The three classes — `case_results`, `suite_results`, and `results` — correspond directly to the three organizational levels of the framework: individual test conditions grouped into named test cases, test cases grouped into suites, and all suites collected into a single top-level report. + +These types are pure passive data holders. They store what happened; they do not participate in running tests or producing output. That separation is what allows different consumers (the `recorder` that retains all detail, or the streaming `reporter` that discards it) to exist independently. + +## Three-Level Hierarchy + +### `case_results` + +A `case_results` holds the accumulated data for one named test case within a suite. It contains two public "memberspaces": `tests` (of private type `tests_t`) and `log` (of private type `log_t`). The term memberspace is the author's idiom for a public member object used to scope related operations — externally these look like `m_case.tests.pass()` and `m_case.log.insert(s)`, keeping the mutation API explicit while avoiding free functions or polluting the outer namespace. + +Both `tests_t` and `log_t` derive from `detail::const_container>`. The key property of `const_container` is that it exposes only `const_iterator` to external callers, making the vector contents read-only from outside the class — but its `cont()` accessor is `protected`, available to derived classes, so `tests_t` and `log_t` can mutate their vectors internally through `pass()`, `fail()`, and `insert()`. The individual test result is the nested `case_results::test` struct — simply a `bool pass` and an optional `std::string reason` for failures. + +`tests_t` maintains a separate `failed_` counter that it increments in `fail()` and reads back in O(1) via `failed()`. This avoids iterating the full vector to count failures when computing suite totals. + +### `suite_results` + +A `suite_results` is itself a `const_container>` and adds `total_` and `failed_` counters. These are aggregated eagerly in both `insert()` overloads — when a `case_results` is pushed in, its `tests.total()` and `tests.failed()` values are immediately folded into the suite totals. This design means `suite_results::total()` and `suite_results::failed()` are always O(1), regardless of how many cases have been inserted. The `size()` method inherited from `const_container` returns the number of test cases (not conditions), which is how `results` tracks its `m_cases` counter. + +### `results` + +The top-level `results` class follows the same pattern: it is a `const_container>` and carries its own `total_`, `failed_`, and `m_cases` counters updated during `insert()`. Note that `m_cases` accumulates `r.size()` (the number of `case_results` items in the incoming suite), while `total_` and `failed_` accumulate condition counts — giving callers three distinct levels of granularity: number of conditions, number of cases, and (implicitly through `size()`) number of suites. + +The naming inconsistency — `m_cases` using the old `m_` Hungarian prefix while the same class uses `total_` and `failed_` with a trailing underscore — is a minor historical artifact with no functional consequence. + +## Move and Copy Insert Overloads + +Both `suite_results::insert()` and `results::insert()` provide rvalue and lvalue overloads. The rvalue paths use `emplace_back(std::move(r))`, which matters because both `suite_results` and `results` contain vectors of potentially large nested objects. The `recorder` class, which builds the hierarchy through runner callbacks, always uses the move overload (`m_results.insert(std::move(m_suite))`), so in practice the copy path is a correctness fallback rather than a performance path. + +There is a subtle ordering issue in the move overloads worth noting: both `suite_results::insert(case_results&&)` and `results::insert(suite_results&&)` read aggregate counts from `r` *before* moving it, which is correct since the move constructor for these standard containers leaves the source in a valid-but-empty state after the counts are captured. + +## Relationship to `recorder` and `runner` + +The `recorder` class in `recorder.h` is the primary consumer. It holds one `results`, one `suite_results`, and one `case_results` as live accumulators. The runner's virtual callbacks drive mutations — `on_suite_begin` resets `m_suite`, `on_case_end` inserts the completed `m_case` into `m_suite`, `on_suite_end` inserts `m_suite` into `m_results`. After the run completes, `recorder::report()` returns a `const results&` for post-run inspection. + +The `reporter` in `reporter.h` intentionally does not use these classes — it defines its own local shadow types with the same names inside a template scope. The reporter is designed for streaming output during a run and discards individual test records, so it has no need for the full storage these classes provide. + +## Read-Only External Interface + +The `const_container` base is the linchpin of the access model. External consumers iterate over suites, cases, and individual `test` structs using range-for or the provided `begin()`/`end()` iterators, all of which are `const_iterator`. Mutation is only available through the explicit `insert()`, `pass()`, `fail()`, and `log.insert()` methods, which are defined on the concrete classes, not exposed through the base. This creates a clear invariant: once the runner has finished and the `results` object is handed to a reporting consumer, that consumer cannot accidentally mutate it. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/runner.h.ai.json b/include/xrpl/beast/unit_test/runner.h.ai.json new file mode 100644 index 0000000000..af10ec793a --- /dev/null +++ b/include/xrpl/beast/unit_test/runner.h.ai.json @@ -0,0 +1,227 @@ +{ + "args": [ + { + "lineno": 36, + "name": "s" + }, + { + "lineno": 43, + "name": "s" + }, + { + "lineno": 49, + "name": "s" + }, + { + "lineno": 57, + "name": "first" + }, + { + "lineno": 57, + "name": "last" + }, + { + "lineno": 66, + "name": "first" + }, + { + "lineno": 66, + "name": "last" + }, + { + "lineno": 66, + "name": "pred" + }, + { + "lineno": 76, + "name": "c" + }, + { + "lineno": 85, + "name": "c" + }, + { + "lineno": 85, + "name": "pred" + }, + { + "lineno": 93, + "name": "suite_info const&" + }, + { + "lineno": 103, + "name": "std::string const&" + }, + { + "lineno": 134, + "name": "name" + }, + { + "lineno": 154, + "name": "reason" + }, + { + "lineno": 163, + "name": "s" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "runner" + } + ], + "description": "Defines the interface and implementation for a unit test runner, allowing customization of test reporting and execution of test suites and cases in a thread-safe manner.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/runner.h", + "functions": [ + { + "args": [], + "lineno": 27, + "name": "runner" + }, + { + "args": [], + "lineno": 28, + "name": "~runner" + }, + { + "args": [ + "runner const&" + ], + "lineno": 30, + "name": "operator=" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 36, + "name": "arg" + }, + { + "args": [], + "lineno": 43, + "name": "arg" + }, + { + "args": [ + "suite_info const& s" + ], + "lineno": 49, + "name": "run" + }, + { + "args": [ + "FwdIter first", + "FwdIter last" + ], + "lineno": 57, + "name": "run" + }, + { + "args": [ + "FwdIter first", + "FwdIter last", + "Pred pred" + ], + "lineno": 66, + "name": "run_if" + }, + { + "args": [ + "SequenceContainer const& c" + ], + "lineno": 76, + "name": "run_each" + }, + { + "args": [ + "SequenceContainer const& c", + "Pred pred" + ], + "lineno": 85, + "name": "run_each_if" + }, + { + "args": [ + "suite_info const&" + ], + "lineno": 93, + "name": "on_suite_begin" + }, + { + "args": [], + "lineno": 98, + "name": "on_suite_end" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 103, + "name": "on_case_begin" + }, + { + "args": [], + "lineno": 108, + "name": "on_case_end" + }, + { + "args": [], + "lineno": 113, + "name": "on_pass" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 118, + "name": "on_fail" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 123, + "name": "on_log" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 134, + "name": "testcase" + }, + { + "args": [], + "lineno": 146, + "name": "pass" + }, + { + "args": [ + "std::string const& reason" + ], + "lineno": 154, + "name": "fail" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 163, + "name": "log" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "beast" + }, + { + "lineno": 13, + "name": "unit_test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/runner.h.ai.md b/include/xrpl/beast/unit_test/runner.h.ai.md new file mode 100644 index 0000000000..b47e863f64 --- /dev/null +++ b/include/xrpl/beast/unit_test/runner.h.ai.md @@ -0,0 +1,43 @@ +# `runner.h` — Unit Test Runner Interface + +`runner.h` defines the central coordination class for the beast unit test framework embedded in the XRPL rippled codebase. It serves as the boundary between *test execution mechanics* and *result reporting*: the `runner` base class owns all state tracking, concurrency management, and invariant enforcement, while derived classes customize what happens at each event through a set of virtual `on_*` hooks. + +## Role in the Framework + +The beast unit test system has three primary collaborators: `suite_info` (metadata + a type-erased callable that constructs and runs a test), `suite` (the abstract base for test implementations), and `runner`. The `runner` is injected into every suite at execution time — `suite_info::run(runner&)` constructs a fresh suite instance and calls `operator()(runner&)` on it. The suite then drives the runner through the private interface: `testcase()`, `pass()`, `fail()`, and `log()`. By declaring `friend class suite`, `runner` keeps these four methods hidden from all other callers, ensuring that only a `suite` can advance the runner's state machine. + +## State Machine and Invariants + +The `runner` maintains three boolean flags that form a small protocol around testcase lifecycle: + +- `default_` starts `true` at the beginning of each suite run and collapses to `false` the first time a named testcase is opened. It represents an implicit unnamed testcase that exists before any explicit `testcase(name)` call. +- `cond_` is a "has this testcase recorded at least one result?" guard. It is reset to `false` each time a testcase opens and set to `true` on any `pass()` or `fail()` call. At the end of `run()`, a `BOOST_ASSERT(cond_)` fires in debug builds if a suite somehow reached its end without making any assertion in its last open testcase. +- `failed_` accumulates across an entire suite run; it is set `true` by any `fail()` call and is what `run()` returns to the caller. + +The auto-open behavior for the implicit default testcase is handled in `pass()`, `fail()`, and `log()`: if `default_` is still `true` when any of these is called, they first call `testcase("")` to formalize the unnamed case. This means test authors who write a single-case suite and never call `testcase()` explicitly get correct lifecycle callbacks without special-casing. + +## Thread Safety via Recursive Mutex + +All four private methods — `testcase()`, `pass()`, `fail()`, `log()` — acquire a `std::recursive_mutex` before touching shared state. A plain `std::mutex` would deadlock here because `pass()` and `fail()` may call `testcase("")` internally while already holding the lock. The recursive mutex avoids this without restructuring the locking. The `thread` helper (see `thread.h`) uses this same runner to let suites spin up internal threads that record concurrent pass/fail results safely. + +## Template-Based Header Definitions + +Every non-virtual method that has a body in this header uses the idiom `template `. This is a well-known trick for header-only libraries: function templates are implicitly `inline`-equivalent from the linker's perspective, so they can be defined in a header included by multiple translation units without violating the One Definition Rule. It avoids the need for a companion `.cpp` file while still keeping the full implementation visible in the header for inlining. + +## The `run` Family + +The public `run*` methods provide a convenience layer over the core single-suite `run(suite_info const&)`: + +- `run(FwdIter, FwdIter)` iterates over any range of `suite_info`-convertible values. +- `run_if(FwdIter, FwdIter, Pred)` adds a predicate filter, which pairs with the `match` predicates in `match.h` to run only suites whose name pattern matches a command-line argument. +- `run_each` and `run_each_if` operate on sequence containers rather than raw iterator pairs. + +A subtle but important detail in all of these: the accumulation is written as `failed = run(*first) || failed` rather than `failed || run(*first)`. The latter would short-circuit after the first failure and silently skip remaining suites. The chosen form ensures every suite runs to completion and their individual failure results are ORed together. + +## Concrete Implementations + +`reporter.h` provides the primary concrete subclass, `detail::reporter<>`, which overrides every `on_*` hook to stream human-readable output in real time and print timing summaries on destruction. The `recorder` (in `recorder.h`) captures structured results as data for programmatic inspection. Both demonstrate how the virtual hook set — `on_suite_begin`, `on_suite_end`, `on_case_begin`, `on_case_end`, `on_pass`, `on_fail`, `on_log` — cleanly separates what the test framework observes from how those observations are reported. The default no-op implementations in the base class mean derived classes only override the events they care about. + +## Argument Passthrough + +The `arg_` string is a simple parameterization mechanism allowing the test harness to pass a single string to all running suites. Each suite retrieves it via `runner_->arg()` (exposed through `suite::arg()`). The meaning is entirely suite-defined — one suite might treat it as a file path, another as a numeric seed — making it a lightweight escape hatch for test customization without requiring multiple runner configurations. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/suite.h.ai.json b/include/xrpl/beast/unit_test/suite.h.ai.json new file mode 100644 index 0000000000..f4ea51e95c --- /dev/null +++ b/include/xrpl/beast/unit_test/suite.h.ai.json @@ -0,0 +1,311 @@ +{ + "args": [ + { + "lineno": 19, + "name": "reason" + }, + { + "lineno": 19, + "name": "file" + }, + { + "lineno": 19, + "name": "line" + }, + { + "lineno": 53, + "name": "self" + }, + { + "lineno": 69, + "name": "self" + }, + { + "lineno": 76, + "name": "self" + }, + { + "lineno": 233, + "name": "name" + }, + { + "lineno": 233, + "name": "abort" + }, + { + "lineno": 245, + "name": "t" + }, + { + "lineno": 255, + "name": "ss" + }, + { + "lineno": 259, + "name": "t" + }, + { + "lineno": 279, + "name": "t" + } + ], + "classes": [ + { + "args": [], + "lineno": 34, + "name": "suite" + }, + { + "args": [], + "lineno": 43, + "name": "suite::abort_exception" + }, + { + "args": [ + "self" + ], + "lineno": 51, + "name": "suite::log_buf" + }, + { + "args": [ + "self" + ], + "lineno": 67, + "name": "suite::log_os" + }, + { + "args": [ + "self" + ], + "lineno": 74, + "name": "suite::testcase_t" + }, + { + "args": [ + "self", + "ss" + ], + "lineno": 253, + "name": "suite::scoped_testcase" + } + ], + "description": "Defines the core unit test suite class and related macros/utilities for the Beast unit test framework, including test case management, logging, and test result reporting.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/suite.h", + "functions": [ + { + "args": [ + "reason", + "file", + "line" + ], + "lineno": 18, + "name": "make_reason" + }, + { + "args": [], + "lineno": 74, + "name": "this_suite" + }, + { + "args": [ + "r" + ], + "lineno": 93, + "name": "operator()" + }, + { + "args": [], + "lineno": 99, + "name": "pass" + }, + { + "args": [ + "reason", + "file", + "line" + ], + "lineno": 108, + "name": "fail" + }, + { + "args": [ + "reason" + ], + "lineno": 113, + "name": "fail" + }, + { + "args": [ + "shouldBeTrue" + ], + "lineno": 120, + "name": "expect" + }, + { + "args": [ + "shouldBeTrue", + "reason" + ], + "lineno": 126, + "name": "expect" + }, + { + "args": [ + "shouldBeTrue", + "file", + "line" + ], + "lineno": 132, + "name": "expect" + }, + { + "args": [ + "shouldBeTrue", + "reason", + "file", + "line" + ], + "lineno": 137, + "name": "expect" + }, + { + "args": [ + "f", + "reason" + ], + "lineno": 146, + "name": "except" + }, + { + "args": [ + "f" + ], + "lineno": 150, + "name": "except" + }, + { + "args": [ + "f", + "reason" + ], + "lineno": 154, + "name": "except" + }, + { + "args": [ + "f" + ], + "lineno": 158, + "name": "except" + }, + { + "args": [ + "f", + "reason" + ], + "lineno": 162, + "name": "unexcept" + }, + { + "args": [ + "f" + ], + "lineno": 166, + "name": "unexcept" + }, + { + "args": [], + "lineno": 170, + "name": "arg" + }, + { + "args": [ + "shouldBeFalse", + "reason" + ], + "lineno": 177, + "name": "unexpected" + }, + { + "args": [ + "shouldBeFalse" + ], + "lineno": 182, + "name": "unexpected" + }, + { + "args": [], + "lineno": 191, + "name": "p_this_suite" + }, + { + "args": [], + "lineno": 196, + "name": "run" + }, + { + "args": [], + "lineno": 199, + "name": "propagate_abort" + }, + { + "args": [ + "r" + ], + "lineno": 202, + "name": "run" + }, + { + "args": [ + "name", + "abort" + ], + "lineno": 232, + "name": "operator()" + }, + { + "args": [ + "abort" + ], + "lineno": 238, + "name": "operator()" + }, + { + "args": [ + "t" + ], + "lineno": 244, + "name": "operator<<" + }, + { + "args": [ + "scoped_testcase" + ], + "lineno": 262, + "name": "operator=" + }, + { + "args": [ + "t" + ], + "lineno": 277, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "beast" + }, + { + "lineno": 14, + "name": "unit_test" + }, + { + "lineno": 16, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/suite.h.ai.md b/include/xrpl/beast/unit_test/suite.h.ai.md new file mode 100644 index 0000000000..f0da57ede9 --- /dev/null +++ b/include/xrpl/beast/unit_test/suite.h.ai.md @@ -0,0 +1,37 @@ +# `include/xrpl/beast/unit_test/suite.h` + +## Role in the System + +`suite.h` defines the foundational `suite` base class for the Beast unit test framework embedded in the XRPL codebase. Every test in the rippled test suite ultimately derives from `beast::unit_test::suite`, making this the central contract between test authors and the framework's execution machinery. The file also defines the macro infrastructure (`BEAST_DEFINE_TESTSUITE`, `BEAST_EXPECT`, etc.) that wires derived test classes into the global test registry with zero boilerplate at the call site. + +## Two-Phase Initialization + +A deliberate design choice separates *construction* from *execution*. `suite` keeps a raw `runner*` member that starts null. Rather than requiring derived classes to accept and forward a `runner&` in their constructors, the framework injects the runner at execution time via `operator()(runner& r)`. This sets `*p_this_suite()` to the current suite, calls the private `run(runner&)`, then clears the thread-local pointer — even under exceptions. The result is that test authors write a plain default-constructible struct with a single `run()` override, free of plumbing. + +## The `runner` Interface + +`suite` never writes output directly. Every observed event — `pass()`, `fail()`, `log()`, `testcase()` — delegates to the injected `runner*`. The `runner` class (`runner.h`) in turn dispatches to overridable `on_pass()`, `on_fail()`, `on_case_begin()`, etc. virtual methods. This decouples the test logic from any particular output format (console, XML, recorder), following the Strategy pattern. The `runner`'s internal mutex protects its state, which matters when tests spawn worker threads. + +## Abort-on-Fail Mechanism + +The `abort_` / `aborted_` pair implements an early-exit facility. When a testcase is opened with `abort_on_fail`, any subsequent `fail()` call sets `aborted_ = true` and throws an internal `abort_exception`. This exception is caught by the private `run(runner&)` overload, which silently ends the suite. The subtle part is `propagate_abort()`, called at the *start* of every `pass()` and `fail()`. This ensures that if a worker thread (via `beast::unit_test::thread`) caused the failure, subsequent calls from *any* thread re-throw — preventing the suite from recording spurious passes after the abort signal. Using a dedicated private exception type avoids interfering with real `std::exception` subclasses being tested. + +## Testcase Naming and `scoped_testcase` + +Opening a testcase via `testcase("name")` immediately forwards the name to the runner. But the framework also supports stream-style dynamic names: `testcase << "iteration: " << i`. The `testcase_t::operator<<` method returns a `scoped_testcase` RAII object that accumulates the full name in a `std::stringstream` and, upon destruction, calls `runner_->testcase(name)`. This deferred commit-on-destruction pattern means the name is finalized at the end of the full expression, after all `<<` operators have run, without any heap allocation or manual `str()` gymnastics in the derived class. + +## Logging Stream + +The `log` member is a `log_os`, a custom `std::basic_ostream` backed by `log_buf`. The `log_buf::sync()` override — called both when `std::endl` flushes the buffer and at destructor time — forwards the buffered text to `runner_->log()`. This lets test authors use idiomatic `log << "value = " << v << std::endl` without knowing the runner's concrete output target, and ensures all buffered data is flushed even if the log stream goes out of scope unexpectedly. + +## `BEAST_EXPECT` Macros and Source Location + +The `expect()` function has four overloads. The two-argument overloads accepting `char const* file, int line` produce failure messages annotated with the source location, formatted by the `detail::make_reason()` helper (which strips the directory from the path via `boost::filesystem::path::filename()`). In practice, test code should always use the `BEAST_EXPECT(cond)` and `BEAST_EXPECTS(cond, reason)` macros, which inject `__FILE__` and `__LINE__` automatically. The plain `expect(cond)` overload without file/line still records pass/fail, but failure messages lack location context — mainly kept for programmatic use. + +## Global Registration Macros + +The `BEAST_DEFINE_TESTSUITE(Class, Module, Library)` family of macros expands into a static `detail::insert_suite` object. Its constructor calls `global_suites().insert(...)` during static initialization, registering the test in the global `suite_list` before `main()` runs. The `MANUAL` variants mark a suite as opt-in (excluded from the default run), and the `PRIO` variants supply a scheduling priority so longer-running suites can be scheduled earlier in parallel execution. Defining `BEAST_NO_UNIT_TEST_INLINE` suppresses all insertions, useful for translation units that want to cherry-pick which suites to register. + +## Thread Safety and `beast::unit_test::thread` + +The `friend class thread` declaration gives `thread.h` access to `suite::abort_exception` and `propagate_abort()`. `beast::unit_test::thread` wraps `std::thread` and routes any `abort_exception` thrown in a worker thread into a silent swallow (the abort is recorded via `aborted_`), while routing any other exception into a `suite::fail()` call. When the test body calls `join()`, `propagate_abort()` is called again, re-throwing if the worker caused an abort — ensuring the main test fiber sees the failure. This design means multi-threaded tests interact safely with the abort mechanism without any additional synchronization in the test body itself. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/suite_info.h.ai.json b/include/xrpl/beast/unit_test/suite_info.h.ai.json new file mode 100644 index 0000000000..360685f7e6 --- /dev/null +++ b/include/xrpl/beast/unit_test/suite_info.h.ai.json @@ -0,0 +1,68 @@ +{ + "args": [ + { + "lineno": 18, + "name": "name" + }, + { + "lineno": 19, + "name": "module" + }, + { + "lineno": 20, + "name": "library" + }, + { + "lineno": 21, + "name": "manual" + }, + { + "lineno": 22, + "name": "priority" + }, + { + "lineno": 23, + "name": "run" + } + ], + "classes": [ + { + "args": [ + "name", + "module", + "library", + "manual", + "priority", + "run" + ], + "lineno": 13, + "name": "suite_info" + } + ], + "description": "Defines the suite_info class, which associates unit test types with metadata and provides utilities for managing and running test suites in the Beast unit test framework.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/suite_info.h", + "functions": [ + { + "args": [ + "name", + "module", + "library", + "manual", + "priority" + ], + "lineno": 66, + "name": "make_suite_info" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "beast" + }, + { + "lineno": 9, + "name": "unit_test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/suite_info.h.ai.md b/include/xrpl/beast/unit_test/suite_info.h.ai.md new file mode 100644 index 0000000000..6f386db72e --- /dev/null +++ b/include/xrpl/beast/unit_test/suite_info.h.ai.md @@ -0,0 +1,61 @@ +# `suite_info.h` — Test Suite Metadata and Type Erasure + +## Role in the System + +`suite_info` is the central value type of the Beast unit test framework's registry. Every test suite in the XRPL codebase is reduced to a `suite_info` instance before being stored in the global registry, and those instances are what the runner iterates over to execute tests. This file sits at the junction between the statically-typed test class hierarchy (everything derives from `beast::unit_test::suite`) and the dynamically-operated registry (`suite_list`, `runner`), providing the type erasure that makes a heterogeneous container of test types possible. + +## What `suite_info` Holds + +A `suite_info` instance carries six fields: + +- `name_`, `module_`, `library_` — three-level naming hierarchy used to form the canonical dotted name `library.module.name` returned by `full_name()`. +- `manual_` — a flag indicating the suite should not run automatically; it must be explicitly selected by name or filter. +- `priority_` — an integer scheduling hint used by parallel test runners. Higher values mean longer-running suites that benefit from early scheduling. +- `run_` — a `std::function` that captures the concrete test type and knows how to instantiate and invoke it. + +The class is a plain value type: copyable, movable, and all members are set in the constructor via `std::move`. There is no virtual dispatch, no inheritance. All polymorphic behavior lives inside the `run_` callable. + +## Type Erasure via `make_suite_info` + +The factory function `make_suite_info` is where the magic happens: + +```cpp +template +suite_info make_suite_info(std::string name, std::string module, + std::string library, bool manual, int priority) +{ + return suite_info(..., [](runner& r) { Suite{}(r); }); +} +``` + +The lambda `[](runner& r) { Suite{}(r); }` captures nothing, but it permanently encodes the knowledge of which concrete `Suite` type to default-construct and invoke. Once this lambda is wrapped inside `std::function`, the specific type is gone — `suite_info` only sees a `run_type`. This is the standard "type erasure through callable" idiom: it avoids virtual functions on test classes while still permitting a homogeneous container of different test types. + +The `suite` base class defines `operator()(runner&)` which sets up internal state, calls the derived `run()` method, and handles `abort_exception`. So `Suite{}(r)` creates a fresh instance on each invocation, runs it to completion (or exception), and destroys it — each `run()` call on a `suite_info` starts a clean test instance with no shared state across runs. + +## Ordering and Priority + +`suite_info` defines `operator<` for use in `std::set`: + +```cpp +friend bool operator<(suite_info const& lhs, suite_info const& rhs) +{ + return std::forward_as_tuple(-lhs.priority_, lhs.library_, lhs.module_, lhs.name_) < + std::forward_as_tuple(-rhs.priority_, rhs.library_, rhs.module_, rhs.name_); +} +``` + +The negation of `priority_` inverts the normal ascending sort: higher numeric priority sorts earlier in the set. Within the same priority tier, the ordering falls back to alphabetical by library, then module, then name. The comment in the code is explicit about the negation trick. The rationale (from `suite.h`) is that long-running suites get higher priorities so they can be dispatched first to parallel workers, minimizing wall-clock time in parallel test runs. Suites declared without an explicit priority default to 0 and sort last within their alphabetical group. + +## Integration with the Registry + +`suite_list` stores `suite_info` values in a `std::set`, exploiting the ordering above. Its template `insert` method calls `make_suite_info` to produce a `suite_info` and emplaces it. In debug builds it additionally checks for duplicate names and duplicate types using a pair of `unordered_set`s, asserting via `BOOST_ASSERT`. + +The `detail::insert_suite` struct in `global_suites.h` triggers this insertion from its constructor, which fires at program startup via a file-static instance. The macro chain in `suite.h` (`BEAST_DEFINE_TESTSUITE`, `BEAST_DEFINE_TESTSUITE_PRIO`, etc.) expands to create exactly such a static instance, so any translation unit that defines a test suite and invokes the macro will have its suite registered before `main()` runs. + +## The `manual` Flag + +The `manual()` accessor exposes whether a suite is opt-in only. Manual suites are skipped when running the full suite automatically (e.g., `runner::run_each`) but can be targeted by a filter predicate (e.g., `runner::run_each_if`). This lets developers define stress tests, network-dependent tests, or long-running benchmarks that would be inappropriate in CI without needing a separate binary — they live in the same registry but require explicit selection. + +## Design Trade-offs + +Using `std::function` for the callable introduces heap allocation and a potential virtual dispatch inside the function wrapper, but this is entirely acceptable for a test registry: suites are registered once at startup and the allocation is amortized over the entire suite run. The alternative — storing a `suite*` base pointer — would require factory functions or virtual clone methods, which is more invasive and still allocates. The lambda approach keeps test classes simple: they only need to derive from `suite` and implement `run()`, with no registration boilerplate beyond a single macro call at the bottom of the file. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/suite_list.h.ai.json b/include/xrpl/beast/unit_test/suite_list.h.ai.json new file mode 100644 index 0000000000..b23178fcfe --- /dev/null +++ b/include/xrpl/beast/unit_test/suite_list.h.ai.json @@ -0,0 +1,57 @@ +{ + "args": [ + { + "lineno": 30, + "name": "name" + }, + { + "lineno": 31, + "name": "module" + }, + { + "lineno": 32, + "name": "library" + }, + { + "lineno": 33, + "name": "manual" + }, + { + "lineno": 34, + "name": "priority" + } + ], + "classes": [ + { + "args": [], + "lineno": 15, + "name": "suite_list" + } + ], + "description": "Defines the suite_list class, a container for managing and inserting unit test suites, with debug checks for duplicate names and types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/suite_list.h", + "functions": [ + { + "args": [ + "name", + "module", + "library", + "manual", + "priority" + ], + "lineno": 29, + "name": "suite_list::insert" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "beast" + }, + { + "lineno": 12, + "name": "unit_test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/suite_list.h.ai.md b/include/xrpl/beast/unit_test/suite_list.h.ai.md new file mode 100644 index 0000000000..12bb63b1c7 --- /dev/null +++ b/include/xrpl/beast/unit_test/suite_list.h.ai.md @@ -0,0 +1,48 @@ +# `suite_list.h` — Ordered Registry of Unit Test Suites + +`suite_list` is the central registry that holds all test suites in the beast unit testing framework. It sits at the core of the static-registration pattern used throughout the XRPL codebase: suites declare themselves at program startup, and `suite_list` accumulates them into a sorted, deduplicated collection that test runners later iterate over. + +## Inheritance and the Read-Only Exposure Pattern + +`suite_list` inherits from `detail::const_container>`. This base class holds the actual `std::set` as a private member and exposes it to derived classes through a protected `cont()` accessor, while publishing only read-only operations (`begin`, `end`, `size`, `empty`) to external callers. The design enforces a clear invariant: consumers of the list can iterate and inspect suites, but only the controlled `insert()` path can add to it. This is why there is no `remove()`, `clear()`, or public `operator[]` — the registry is write-once during static initialization, then read-many at run time. + +The underlying container is `std::set`, which keeps suites sorted according to `suite_info`'s `operator<`. That comparator orders by *negated* priority first (so higher-priority suites sort to the front), then lexicographically by library, module, and name. This means that simply iterating the `suite_list` in order already produces a deterministic execution sequence with high-priority suites running first — no sorting step needed at run time. + +## The `insert()` Template Method + +```cpp +template +void insert(char const* name, char const* module, char const* library, bool manual, int priority); +``` + +The template parameter `Suite` is the concrete test class rather than a `suite_info` object, for two reasons. First, the debug checks need `std::type_index(typeid(Suite))` to verify that no two C++ types collide. Second, `make_suite_info` (from `suite_info.h`) captures the type in a lambda that instantiates `Suite{}` and calls it with a `runner&` — this deferred construction is what allows the test to be run on demand without storing a live object. + +The call to `cont().emplace(...)` inserts directly into the underlying `std::set`, relying on `suite_info`'s move constructor and the set's ordering to place the entry correctly. + +## Debug-Only Duplicate Guards + +Two fields exist only in debug builds: + +```cpp +#ifndef NDEBUG +std::unordered_set names_; +std::unordered_set classes_; +#endif +``` + +`names_` catches the case where the same fully-qualified string (`library.module.name`) is registered twice — perhaps through copy-paste of a registration macro. `classes_` catches the complementary case where the same C++ type is registered under two different names. A `std::set` insertion alone would silently ignore a second entry for an equivalent `suite_info` if its sort key matched, or accept a duplicate with a different name for the same type. The `BOOST_ASSERT` calls make both failure modes immediately visible during development without paying any cost in release builds. + +Using `std::unordered_set` for these guards rather than querying `names_` or `classes_` from the `std::set` itself is intentional: the ordered set is keyed by `operator<` (priority + name tuple), while duplicate detection needs a flat string and type key respectively — different access patterns warrant separate structures. + +## Integration with `global_suites.h` + +`suite_list` is consumed by `global_suites.h`, which defines the process-wide singleton: + +```cpp +inline suite_list& global_suites() { + static suite_list s; + return s; +} +``` + +The companion `insert_suite` struct calls `global_suites().insert(...)` in its constructor, so declaring a static `insert_suite` variable at namespace scope causes registration to happen before `main()`. Runners then call the public `beast::unit_test::global_suites()` (which returns a `const suite_list&`) and iterate over the sorted set to discover and execute suites. The constness of the public accessor enforces that no code outside the initialization path can mutate the global registry after startup. \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/thread.h.ai.json b/include/xrpl/beast/unit_test/thread.h.ai.json new file mode 100644 index 0000000000..01acecabba --- /dev/null +++ b/include/xrpl/beast/unit_test/thread.h.ai.json @@ -0,0 +1,99 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "thread" + } + ], + "description": "Defines a custom thread class for unit testing that wraps std::thread and handles exceptions, ensuring exceptions in threads are reported as test failures.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/unit_test/thread.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "thread" + }, + { + "args": [ + "thread const&" + ], + "lineno": 24, + "name": "operator=" + }, + { + "args": [ + "thread&& other" + ], + "lineno": 26, + "name": "thread" + }, + { + "args": [ + "thread&& other" + ], + "lineno": 30, + "name": "operator=" + }, + { + "args": [ + "suite& s", + "F&& f", + "Args&&... args" + ], + "lineno": 37, + "name": "thread" + }, + { + "args": [], + "lineno": 43, + "name": "joinable" + }, + { + "args": [], + "lineno": 48, + "name": "get_id" + }, + { + "args": [], + "lineno": 53, + "name": "hardware_concurrency" + }, + { + "args": [], + "lineno": 58, + "name": "join" + }, + { + "args": [], + "lineno": 64, + "name": "detach" + }, + { + "args": [ + "thread& other" + ], + "lineno": 68, + "name": "swap" + }, + { + "args": [ + "std::function f" + ], + "lineno": 74, + "name": "run" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "beast" + }, + { + "lineno": 9, + "name": "unit_test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/unit_test/thread.h.ai.md b/include/xrpl/beast/unit_test/thread.h.ai.md new file mode 100644 index 0000000000..5e1adc272d --- /dev/null +++ b/include/xrpl/beast/unit_test/thread.h.ai.md @@ -0,0 +1,35 @@ +# `include/xrpl/beast/unit_test/thread.h` + +This file provides `beast::unit_test::thread`, a thin wrapper around `std::thread` that bridges C++ thread semantics with the Beast unit testing framework's pass/fail reporting and abort propagation machinery. + +## Why This Exists + +The standard `std::thread` has a fatal flaw in test contexts: if an exception escapes a thread function, `std::terminate` is called. The unit test framework uses exceptions — specifically `suite::abort_exception` — as a control-flow mechanism to short-circuit a failing test suite when `abort_on_fail` is requested. A raw `std::thread` running test logic would crash the process on any unhandled exception rather than recording a test failure. `beast::unit_test::thread` solves both problems: it converts unexpected exceptions into test failures and silently absorbs deliberate abort signals. + +## Exception Handling in `run()` + +The core logic is the private `run()` method, which serves as the actual thread entry point. It wraps the user-supplied callable in a three-clause catch block: + +1. **`suite::abort_exception`** — caught and silently discarded. This exception is thrown internally by `suite::fail()` when the suite is configured with `abort_on_fail`. The fact that it reaches the thread boundary means the abort has already been recorded; eating the exception here prevents `std::terminate` from being called. + +2. **`std::exception`** — caught and forwarded to `s_->fail()`, which records a test failure in the runner with the exception message prefixed by `"unhandled exception: "`. + +3. **Catch-all (`...`)** — handles any other thrown object with a generic `"unhandled exception"` failure message. + +This mirrors the exception-handling pattern used in `suite::run(runner&)` itself, maintaining consistent failure semantics whether test code runs on the main suite thread or on a helper thread. + +## Abort Propagation Across Thread Boundaries + +The subtler design concern is handled in `join()`. After `t_.join()` completes, it immediately calls `s_->propagate_abort()`. This method checks whether the suite is in `abort_on_fail` mode and has already recorded an abort, and if so, re-throws `abort_exception` on the calling thread. + +This two-step dance is necessary because an abort originating in the worker thread is consumed by `run()` to avoid termination. The abort state (`aborted_` flag) is preserved on the `suite` object, and `propagate_abort()` on `join()` restores the abort signal to the parent thread. Without this, a test suite with `abort_on_fail` could silently continue after a worker thread failure rather than stopping the suite as intended. + +## Relationship to `suite` + +The `thread` class is a `friend` of `suite`, granting it access to `abort_exception` (a private nested struct) and `propagate_abort()` (a private method). `suite.h` forward-declares `class thread` before the `suite` class definition, and `thread.h` includes `suite.h`, creating a clean one-way dependency. + +The `s_` member is a raw non-owning pointer to the `suite` that launched the thread. This is safe because unit test threads are expected to be `join()`ed before the suite object is destroyed — a contract enforced by convention in test code rather than by any RAII mechanism here. Calling `join()` on a `thread` whose parent `suite` has already been destroyed would be undefined behavior, but this mirrors the same lifetime discipline required of `std::thread` with respect to its arguments. + +## Interface Fidelity + +The class deliberately mirrors the `std::thread` interface: it exposes `joinable()`, `get_id()`, `hardware_concurrency()`, `detach()`, and `swap()` as pass-throughs to the underlying `t_` member. Copy construction and assignment are deleted (as with `std::thread`); only move semantics are supported. This allows `beast::unit_test::thread` to act as a drop-in replacement for `std::thread` in test code with minimal friction, requiring only that the `suite` reference be supplied as an additional first constructor argument. \ No newline at end of file diff --git a/include/xrpl/beast/utility/Journal.h.ai.json b/include/xrpl/beast/utility/Journal.h.ai.json new file mode 100644 index 0000000000..e81d4e840e --- /dev/null +++ b/include/xrpl/beast/utility/Journal.h.ai.json @@ -0,0 +1,326 @@ +{ + "args": [ + { + "lineno": 61, + "name": "Severity level" + }, + { + "lineno": 71, + "name": "bool output" + }, + { + "lineno": 81, + "name": "Severity thresh" + }, + { + "lineno": 87, + "name": "Severity level" + }, + { + "lineno": 87, + "name": "std::string const& text" + }, + { + "lineno": 97, + "name": "Severity level" + }, + { + "lineno": 97, + "name": "std::string const& text" + }, + { + "lineno": 127, + "name": "ScopedStream const& other" + }, + { + "lineno": 131, + "name": "Sink& sink" + }, + { + "lineno": 131, + "name": "Severity level" + }, + { + "lineno": 134, + "name": "Stream const& stream" + }, + { + "lineno": 134, + "name": "T const& t" + }, + { + "lineno": 136, + "name": "Stream const& stream" + }, + { + "lineno": 136, + "name": "std::ostream& manip(std::ostream&)" + }, + { + "lineno": 155, + "name": "T const& t" + }, + { + "lineno": 251, + "name": "Sink& sink" + }, + { + "lineno": 251, + "name": "Severity level" + }, + { + "lineno": 258, + "name": "Severity level" + }, + { + "lineno": 340, + "name": "char const* s" + }, + { + "lineno": 345, + "name": "wchar_t const* s" + } + ], + "classes": [ + { + "args": [ + "Sink& sink" + ], + "lineno": 32, + "name": "Journal" + }, + { + "args": [ + "Sink(Sink const& sink)", + "Sink(Severity thresh, bool console)" + ], + "lineno": 49, + "name": "Journal::Sink" + }, + { + "args": [ + "ScopedStream(ScopedStream const& other)", + "ScopedStream(Sink& sink, Severity level)", + "ScopedStream(Stream const& stream, T const& t)", + "ScopedStream(Stream const& stream, std::ostream& manip(std::ostream&))" + ], + "lineno": 123, + "name": "Journal::ScopedStream" + }, + { + "args": [ + "Stream()", + "Stream(Sink& sink, Severity level)", + "Stream(Stream const& other)" + ], + "lineno": 170, + "name": "Journal::Stream" + }, + { + "args": [ + "logstream_buf(beast::Journal::Stream const& strm)" + ], + "lineno": 319, + "name": "detail::logstream_buf" + }, + { + "args": [ + "basic_logstream(beast::Journal::Stream const& strm)" + ], + "lineno": 370, + "name": "basic_logstream" + } + ], + "description": "This file defines a lightweight, runtime-configurable logging system for the beast namespace, including severity levels, log sinks, and stream-based logging interfaces.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/utility/Journal.h", + "functions": [ + { + "args": [ + "Severity level" + ], + "lineno": 61, + "name": "active" + }, + { + "args": [], + "lineno": 66, + "name": "console" + }, + { + "args": [ + "bool output" + ], + "lineno": 71, + "name": "console" + }, + { + "args": [], + "lineno": 76, + "name": "threshold" + }, + { + "args": [ + "Severity thresh" + ], + "lineno": 81, + "name": "threshold" + }, + { + "args": [ + "Severity level", + "std::string const& text" + ], + "lineno": 87, + "name": "write" + }, + { + "args": [ + "Severity level", + "std::string const& text" + ], + "lineno": 97, + "name": "writeAlways" + }, + { + "args": [], + "lineno": 116, + "name": "getNullSink" + }, + { + "args": [], + "lineno": 146, + "name": "ostream" + }, + { + "args": [ + "std::ostream& manip(std::ostream&)" + ], + "lineno": 151, + "name": "operator<<" + }, + { + "args": [ + "T const& t" + ], + "lineno": 155, + "name": "operator<<" + }, + { + "args": [], + "lineno": 202, + "name": "sink" + }, + { + "args": [], + "lineno": 207, + "name": "level" + }, + { + "args": [], + "lineno": 212, + "name": "active" + }, + { + "args": [], + "lineno": 217, + "name": "operator bool" + }, + { + "args": [ + "std::ostream& manip(std::ostream&)" + ], + "lineno": 224, + "name": "operator<<" + }, + { + "args": [ + "T const& t" + ], + "lineno": 228, + "name": "operator<<" + }, + { + "args": [], + "lineno": 246, + "name": "sink" + }, + { + "args": [ + "Severity level" + ], + "lineno": 251, + "name": "stream" + }, + { + "args": [ + "Severity level" + ], + "lineno": 258, + "name": "active" + }, + { + "args": [], + "lineno": 265, + "name": "trace" + }, + { + "args": [], + "lineno": 270, + "name": "debug" + }, + { + "args": [], + "lineno": 275, + "name": "info" + }, + { + "args": [], + "lineno": 280, + "name": "warn" + }, + { + "args": [], + "lineno": 285, + "name": "error" + }, + { + "args": [], + "lineno": 290, + "name": "fatal" + }, + { + "args": [ + "char const* s" + ], + "lineno": 340, + "name": "write" + }, + { + "args": [ + "wchar_t const* s" + ], + "lineno": 345, + "name": "write" + }, + { + "args": [], + "lineno": 355, + "name": "sync" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "beast" + }, + { + "lineno": 8, + "name": "severities" + }, + { + "lineno": 312, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/utility/Journal.h.ai.md b/include/xrpl/beast/utility/Journal.h.ai.md new file mode 100644 index 0000000000..3fdb1c1b52 --- /dev/null +++ b/include/xrpl/beast/utility/Journal.h.ai.md @@ -0,0 +1,53 @@ +# `beast/utility/Journal.h` — Lightweight Runtime-Configurable Logging Facade + +## Role in the System + +`Journal.h` defines the core logging abstraction used throughout `rippled`. Every subsystem — consensus, the ledger, RPC, peerfinder, transaction processing — receives a `beast::Journal` to emit diagnostic output. The header is entirely self-contained except for `instrumentation.h` (which provides `XRPL_ASSERT`), and it purposefully exposes no dependency on any particular log backend or output format. The design goal stated in the source comment says it all: be lightweight enough to copy by value, leave logging calls in source code permanently, and control them entirely at runtime via a severity threshold. + +## The `Sink` Contract + +`Journal::Sink` is the abstract output destination. It stores two pieces of state — a severity threshold (`thresh_`) and a boolean console flag — in a protected base; subclasses inherit this state and override the two pure virtual methods `write()` and `writeAlways()`. The distinction between these two methods is intentional: `write()` is expected to silently discard messages below the threshold, while `writeAlways()` bypasses the filter but must still format the output identically. This is used in the XRPL codebase to emit certain critical startup or diagnostic messages unconditionally without coupling the call site to any particular threshold decision. + +The `static_assert` block immediately following the `Sink` declaration enforces a strict value-semantics contract at compile time: `Sink` is neither default-constructible, copy-constructible, move-constructible, copy-assignable, nor move-assignable. This intentional immobility means every concrete sink must be created at a known lifetime scope (typically owned by the `Logs` manager) and shared only via raw reference or pointer — preventing accidental copies of a stateful I/O object. + +The null sink returned by `Journal::getNullSink()` is a Meyer's-singleton `NullJournalSink`, defined in `beast_Journal.cpp`. Its `active()` always returns `false`, all write methods are no-ops, and it is initialized with `kDisabled` threshold. This singleton ensures that a `Journal::Stream` constructed with no explicit sink (its default constructor) always has a valid, safe sink reference — eliminating any possibility of null-pointer dereference in the hot logging path. + +## `Stream`: The Severity-Tagged Logging Handle + +`Journal::Stream` is a thin, copyable wrapper around a `(Sink&, Severity)` pair. Methods like `j.info()` or `j.warn()` return a `Stream` by value. The active check — `m_sink.active(m_level)` — is deliberately inlined in the header so the compiler can inline the single comparison `level >= thresh_`. When a log message is below threshold, the entire call resolves to a boolean branch that the optimizer can collapse. + +`Stream` provides `operator bool()` which maps to `active()`. This powers the `JLOG` macro defined in `Log.h`: + +```cpp +#define JLOG(x) if (!x) {} else x +``` + +When `x` is a `Stream`, the `if (!x)` guard prevents the entire right-hand side of `JLOG(j.debug()) << expensiveFormatCall()` from being evaluated when the stream is inactive. This is the primary performance mechanism: formatting can involve string concatenation, hex encoding, or JSON serialization, none of which should run for suppressed messages. + +## `ScopedStream`: RAII Ostream Buffer + +`Journal::ScopedStream` is a temporary RAII object created when `operator<<` is first invoked on a `Stream`. It holds a `std::ostringstream` that accumulates everything chained together on a single logging statement. In its destructor it calls `m_sink.write(m_level, s)` — flushing the fully assembled string to the sink in one call. The destructor also handles the degenerate case where the buffered content is exactly `"\n"` (a bare newline with no preceding text), writing an empty string instead to avoid log backends emitting a blank formatted line. + +Both the `ScopedStream` constructor variants apply `std::boolalpha` and `std::showbase` to the underlying `ostringstream`. This means booleans always format as `true`/`false` (not `1`/`0`) and numeric bases are always annotated, which simplifies debugging without per-callsite formatting flags. + +`ScopedStream` is copy-constructible (needed for template deduction paths) but not assignable — the destructor must fire exactly once and at the right point. + +## The Three-Layer Architecture + +In practice, three layers compose the logging system: + +1. **`beast::Journal`** — the lightweight value-typed facade passed by value into subsystems. Holds a raw pointer to a `Sink` (non-owning). The invariant `m_sink always points to a valid Sink` is documented in the source and guaranteed by construction — there is no default constructor, and the null sink covers the "no output" case. + +2. **`beast::Journal::Sink`** (concrete: `Logs::Sink` in `basics/Log.h`) — owns the threshold state and routes formatted messages into the `Logs` file and console subsystem. Each named partition (`"Ledger"`, `"RPC"`, `"PeerFinder"`, etc.) gets its own `Sink` instance with an independent severity threshold. The `Logs` class stores these as `std::unique_ptr` in a case-insensitive `std::map`, and its `journal(name)` method creates a `Journal` pointing into that map. + +3. **`JLOG` / `CLOG` macros** — the call-site guards that avoid evaluation of arguments when the stream is inactive. + +## `basic_logstream`: Standard-Stream Bridge + +`basic_logstream` (aliased as `logstream` and `logwstream`) adapts a `Journal::Stream` into a `std::basic_ostream`. The underlying `detail::logstream_buf` overrides `sync()` to flush the buffer's accumulated string via the `Journal::Stream`. This is useful in contexts where a third-party interface accepts only a `std::ostream&` but the code wants all output routed through the journal system. The buffer's destructor also calls `sync()`, ensuring nothing is lost on scope exit. + +## Design Decisions Worth Noting + +Keeping `Journal` as a thin pointer wrapper (8 bytes on a 64-bit system) means it can be freely passed by value into lambdas, coroutines, and deeply nested call chains without any allocation or reference-counting overhead. The alternative — passing `Sink&` directly — would expose implementation details and prevent the convenient severity-accessor API (`j.warn()`, `j.error()`, etc.). + +The dual `write`/`writeAlways` interface on `Sink` provides an escape hatch without forcing call sites to manipulate threshold state directly, which would introduce a thread-safety race in the common case where thresholds are only written by the configuration thread and read by worker threads. Concrete `Sink` implementations are responsible for their own thread safety when `write()` is called concurrently. \ No newline at end of file diff --git a/include/xrpl/beast/utility/PropertyStream.h.ai.json b/include/xrpl/beast/utility/PropertyStream.h.ai.json new file mode 100644 index 0000000000..6d06372505 --- /dev/null +++ b/include/xrpl/beast/utility/PropertyStream.h.ai.json @@ -0,0 +1,709 @@ +{ + "args": [ + { + "lineno": 31, + "name": "key" + }, + { + "lineno": 31, + "name": "value" + }, + { + "lineno": 36, + "name": "Value" + }, + { + "lineno": 36, + "name": "key" + }, + { + "lineno": 36, + "name": "value" + }, + { + "lineno": 42, + "name": "key" + }, + { + "lineno": 42, + "name": "value" + }, + { + "lineno": 64, + "name": "value" + }, + { + "lineno": 69, + "name": "Value" + }, + { + "lineno": 69, + "name": "value" + }, + { + "lineno": 74, + "name": "value" + }, + { + "lineno": 113, + "name": "map" + }, + { + "lineno": 113, + "name": "key" + }, + { + "lineno": 114, + "name": "other" + }, + { + "lineno": 153, + "name": "value" + }, + { + "lineno": 157, + "name": "manip" + }, + { + "lineno": 161, + "name": "t" + }, + { + "lineno": 139, + "name": "stream" + }, + { + "lineno": 140, + "name": "parent" + }, + { + "lineno": 141, + "name": "key" + }, + { + "lineno": 141, + "name": "parent" + }, + { + "lineno": 142, + "name": "key" + }, + { + "lineno": 142, + "name": "stream" + }, + { + "lineno": 180, + "name": "key" + }, + { + "lineno": 180, + "name": "value" + }, + { + "lineno": 186, + "name": "Key" + }, + { + "lineno": 186, + "name": "key" + }, + { + "lineno": 186, + "name": "Value" + }, + { + "lineno": 186, + "name": "value" + }, + { + "lineno": 191, + "name": "key" + }, + { + "lineno": 194, + "name": "key" + }, + { + "lineno": 199, + "name": "Key" + }, + { + "lineno": 199, + "name": "key" + }, + { + "lineno": 220, + "name": "value" + }, + { + "lineno": 212, + "name": "key" + }, + { + "lineno": 212, + "name": "map" + }, + { + "lineno": 213, + "name": "key" + }, + { + "lineno": 213, + "name": "stream" + }, + { + "lineno": 230, + "name": "name" + }, + { + "lineno": 243, + "name": "stream" + }, + { + "lineno": 246, + "name": "stream" + }, + { + "lineno": 247, + "name": "source" + }, + { + "lineno": 252, + "name": "child" + }, + { + "lineno": 258, + "name": "child" + }, + { + "lineno": 264, + "name": "stream" + }, + { + "lineno": 267, + "name": "stream" + }, + { + "lineno": 271, + "name": "stream" + }, + { + "lineno": 271, + "name": "path" + }, + { + "lineno": 292, + "name": "path" + }, + { + "lineno": 299, + "name": "name" + }, + { + "lineno": 300, + "name": "path" + }, + { + "lineno": 301, + "name": "name" + }, + { + "lineno": 303, + "name": "path" + }, + { + "lineno": 304, + "name": "path" + }, + { + "lineno": 305, + "name": "path" + }, + { + "lineno": 312, + "name": "Map" + } + ], + "classes": [ + { + "args": [], + "lineno": 9, + "name": "PropertyStream" + }, + { + "args": [ + "Source* source" + ], + "lineno": 93, + "name": "PropertyStream::Item" + }, + { + "args": [ + "Map const& map", + "std::string const& key" + ], + "lineno": 112, + "name": "PropertyStream::Proxy" + }, + { + "args": [ + "PropertyStream& stream" + ], + "lineno": 137, + "name": "PropertyStream::Map" + }, + { + "args": [ + "std::string const& key", + "Map& map" + ], + "lineno": 210, + "name": "PropertyStream::Set" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 229, + "name": "PropertyStream::Source" + } + ], + "description": "Defines the PropertyStream class and related classes for structured, hierarchical property streaming and introspection, supporting map/set/array output and recursive source trees for diagnostics/logging.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/utility/PropertyStream.h", + "functions": [ + { + "args": [ + "std::string const& key", + "char const* value" + ], + "lineno": 32, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "Value value" + ], + "lineno": 37, + "name": "lexical_add" + }, + { + "args": [ + "std::string const& key", + "bool value" + ], + "lineno": 43, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "char value" + ], + "lineno": 44, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "signed char value" + ], + "lineno": 45, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "unsigned char value" + ], + "lineno": 46, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "short value" + ], + "lineno": 47, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "unsigned short value" + ], + "lineno": 48, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "int value" + ], + "lineno": 49, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "unsigned int value" + ], + "lineno": 50, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "long value" + ], + "lineno": 51, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "unsigned long value" + ], + "lineno": 52, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "long long value" + ], + "lineno": 53, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "unsigned long long value" + ], + "lineno": 54, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "float value" + ], + "lineno": 55, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "double value" + ], + "lineno": 56, + "name": "add" + }, + { + "args": [ + "std::string const& key", + "long double value" + ], + "lineno": 57, + "name": "add" + }, + { + "args": [ + "std::string const& value" + ], + "lineno": 63, + "name": "add" + }, + { + "args": [ + "char const* value" + ], + "lineno": 65, + "name": "add" + }, + { + "args": [ + "Value value" + ], + "lineno": 70, + "name": "lexical_add" + }, + { + "args": [ + "bool value" + ], + "lineno": 75, + "name": "add" + }, + { + "args": [ + "char value" + ], + "lineno": 76, + "name": "add" + }, + { + "args": [ + "signed char value" + ], + "lineno": 77, + "name": "add" + }, + { + "args": [ + "unsigned char value" + ], + "lineno": 78, + "name": "add" + }, + { + "args": [ + "short value" + ], + "lineno": 79, + "name": "add" + }, + { + "args": [ + "unsigned short value" + ], + "lineno": 80, + "name": "add" + }, + { + "args": [ + "int value" + ], + "lineno": 81, + "name": "add" + }, + { + "args": [ + "unsigned int value" + ], + "lineno": 82, + "name": "add" + }, + { + "args": [ + "long value" + ], + "lineno": 83, + "name": "add" + }, + { + "args": [ + "unsigned long value" + ], + "lineno": 84, + "name": "add" + }, + { + "args": [ + "long long value" + ], + "lineno": 85, + "name": "add" + }, + { + "args": [ + "unsigned long long value" + ], + "lineno": 86, + "name": "add" + }, + { + "args": [ + "float value" + ], + "lineno": 87, + "name": "add" + }, + { + "args": [ + "double value" + ], + "lineno": 88, + "name": "add" + }, + { + "args": [ + "long double value" + ], + "lineno": 89, + "name": "add" + }, + { + "args": [ + "Value value" + ], + "lineno": 154, + "name": "operator=" + }, + { + "args": [ + "std::ostream& manip(std::ostream&)" + ], + "lineno": 158, + "name": "operator<<" + }, + { + "args": [ + "T const& t" + ], + "lineno": 161, + "name": "operator<<" + }, + { + "args": [ + "std::string const& key", + "Value value" + ], + "lineno": 181, + "name": "add" + }, + { + "args": [ + "Key key", + "Value value" + ], + "lineno": 187, + "name": "add" + }, + { + "args": [ + "std::string const& key" + ], + "lineno": 192, + "name": "operator[]" + }, + { + "args": [ + "char const* key" + ], + "lineno": 195, + "name": "operator[]" + }, + { + "args": [ + "Key key" + ], + "lineno": 200, + "name": "operator[]" + }, + { + "args": [ + "Value value" + ], + "lineno": 221, + "name": "add" + }, + { + "args": [ + "Source& source" + ], + "lineno": 246, + "name": "add" + }, + { + "args": [ + "Derived* child" + ], + "lineno": 252, + "name": "add" + }, + { + "args": [ + "Source& child" + ], + "lineno": 258, + "name": "remove" + }, + { + "args": [ + "PropertyStream& stream" + ], + "lineno": 264, + "name": "write_one" + }, + { + "args": [ + "PropertyStream& stream" + ], + "lineno": 267, + "name": "write" + }, + { + "args": [ + "PropertyStream& stream", + "std::string const& path" + ], + "lineno": 271, + "name": "write" + }, + { + "args": [ + "std::string path" + ], + "lineno": 292, + "name": "find" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 299, + "name": "find_one_deep" + }, + { + "args": [ + "std::string path" + ], + "lineno": 300, + "name": "find_path" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 301, + "name": "find_one" + }, + { + "args": [ + "std::string* path" + ], + "lineno": 303, + "name": "peel_leading_slash" + }, + { + "args": [ + "std::string* path" + ], + "lineno": 304, + "name": "peel_trailing_slashstar" + }, + { + "args": [ + "std::string* path" + ], + "lineno": 305, + "name": "peel_name" + }, + { + "args": [ + "Map&" + ], + "lineno": 312, + "name": "onWrite" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/utility/PropertyStream.h.ai.md b/include/xrpl/beast/utility/PropertyStream.h.ai.md new file mode 100644 index 0000000000..f265458af6 --- /dev/null +++ b/include/xrpl/beast/utility/PropertyStream.h.ai.md @@ -0,0 +1,33 @@ +# `PropertyStream.h` — Structured Hierarchical Diagnostic Introspection + +This header defines the entire `PropertyStream` framework: an abstract streaming interface for exposing internal XRPL subsystem state as structured key-value trees, along with the RAII scope-guard wrappers and a self-registering `Source` tree that make it usable without coupling components to any particular output format. + +## The Core Problem + +XRPL's rippled process hosts dozens of concurrent subsystems — peer finders, ledger cleaners, resource managers, overlay management — all of which need to expose diagnostic state for operator inspection, typically over an RPC endpoint. The naive approach (returning hardcoded JSON from each component) couples the diagnostic logic to the serialization format. `PropertyStream` solves this by defining a pure-virtual interface that accepts structured writes, so subsystems describe *what* they want to emit while the concrete stream implementation decides *how*. The `JsonPropertyStream` class (in `include/xrpl/json/JsonPropertyStream.h`) is the primary concrete implementation, directing all calls into a `Json::Value` object tree. + +## The Abstract Stream Interface + +`PropertyStream` is an abstract base class with two families of pure-virtual methods: the `map_begin()`/`map_end()` pair for named object scopes, and the `array_begin()`/`array_end()` pair for anonymous array scopes. On top of these, an overloaded `add()` family covers every primitive C++ numeric type plus `bool` and `std::string`. Non-`string` types are routed through the protected `lexical_add()` template, which serializes into a `std::stringstream` then delegates to the pure-virtual `add(std::string const&, std::string const&)`. The `bool` overloads are deliberately special-cased at the virtual level to allow implementors to emit `true`/`false` as JSON booleans rather than integers. + +## RAII Scope Guards: `Map` and `Set` + +`Map` and `Set` are non-copyable RAII wrappers that bracket `PropertyStream` structural calls. `Map`'s constructor calls either `map_begin()` or `map_begin(key)` depending on whether the map is named, and the destructor calls `map_end()`. `Set` does the same for arrays. This guarantees structural balance — a requirement for any hierarchical format — without placing the burden on calling code. The multiple `Map` constructors accepting `PropertyStream&`, `Set&`, or another `Map& ` parent allow natural nesting: `Map inner("stats", outer)` opens a named sub-object within an existing object scope. + +`Map` also exposes `operator[]`, which returns a `Proxy` rather than accepting a value directly. The `Proxy` holds an `std::ostringstream` and commits to the stream only in its destructor, enabling the expression `map["key"] << computedValue` to work naturally: the temporary `Proxy` is streamed into, then destroyed at the end of the expression, at which point it calls `m_map->add(m_key, buffer.str())` if the buffer is non-empty. The `operator=` on `Proxy` provides the simpler assignment form `map["key"] = value`. + +## The `Source` Tree + +`Source` is the class that subsystems inherit from. Each `Source` has a string name, a `std::recursive_mutex`, a raw `parent_` pointer, an `Item` (its handle in the parent's child list), and a `List` of children. The relationship between `Item` and `Source` is intentionally indirect: `Item` inherits from `List::Node` (an intrusive doubly-linked list node from `beast::List`) and stores a back-pointer to its owning `Source`. This avoids separate heap allocations for list management while still keeping the parent's child list traversable via `Item::source()` dereferences. + +Registering a child calls `Source::add(Source& child)`, which acquires both `lock_` and `child.lock_` via `std::lock()` in deadlock-safe order. The destructor of `Source` acquires `lock_`, removes itself from its parent (if any), then calls `removeAll()` to detach all children. Children must outlive or be removed before the parent that holds their `Item` nodes, so the ownership discipline is entirely caller-managed by object lifetime. + +Concrete subsystems override `onWrite(Map&)`, which by default does nothing. When diagnostic output is requested, `write(PropertyStream&)` locks `lock_`, calls `onWrite()` to populate a `Map` from the current `Source`, then recursively calls `write()` on each child. `write_one()` skips the recursive descent. Both entry points bracket the output in a keyed `Map` named after the `Source`. + +## Path Navigation + +`find(std::string path)` implements a mini-language for targeting specific nodes in the tree. Paths are dot-delimited names; a trailing `*` signals recursive output. The static helpers `peel_leading_slash()`, `peel_trailing_slashstar()`, and `peel_name()` tokenize the path string by mutating a local copy. `find()` returns a `std::pair` where the bool indicates whether the `*` wildcard was present, allowing the caller to choose between `write_one()` and `write()`. This design lets a single HTTP endpoint serve both summary (`"ledgercleaner"`) and deep-dive (`"ledgercleaner.*"`) requests without any per-subsystem routing logic. + +## Relationship to Dependent Files + +`List.h` provides the intrusive doubly-linked list that `Source` uses to track children without heap allocation overhead. `JsonPropertyStream.h` provides the concrete implementation; its stack of `Json::Value*` pointers mirrors the nesting depth opened by `Map` and `Set` RAII guards. `LedgerCleaner` is a representative subsystem that inherits `PropertyStream::Source("ledgercleaner")` and implements `onWrite()` to stream current cleaner state into the diagnostic tree, illustrating how the framework integrates naturally into service class hierarchies. \ No newline at end of file diff --git a/include/xrpl/beast/utility/WrappedSink.h.ai.json b/include/xrpl/beast/utility/WrappedSink.h.ai.json new file mode 100644 index 0000000000..074576c24a --- /dev/null +++ b/include/xrpl/beast/utility/WrappedSink.h.ai.json @@ -0,0 +1,135 @@ +{ + "args": [ + { + "lineno": 13, + "name": "sink" + }, + { + "lineno": 13, + "name": "prefix" + }, + { + "lineno": 18, + "name": "journal" + }, + { + "lineno": 18, + "name": "prefix" + }, + { + "lineno": 23, + "name": "s" + }, + { + "lineno": 28, + "name": "level" + }, + { + "lineno": 36, + "name": "output" + }, + { + "lineno": 44, + "name": "thresh" + }, + { + "lineno": 48, + "name": "level" + }, + { + "lineno": 48, + "name": "text" + }, + { + "lineno": 53, + "name": "level" + }, + { + "lineno": 53, + "name": "text" + } + ], + "classes": [ + { + "args": [ + "sink", + "prefix" + ], + "lineno": 10, + "name": "WrappedSink" + }, + { + "args": [ + "journal", + "prefix" + ], + "lineno": 17, + "name": "WrappedSink" + } + ], + "description": "Defines the WrappedSink class, which wraps a Journal::Sink to prefix its output with a string, allowing log messages to be prefixed while preserving the original sink's behavior.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/utility/WrappedSink.h", + "functions": [ + { + "args": [ + "s" + ], + "lineno": 23, + "name": "prefix" + }, + { + "args": [ + "level" + ], + "lineno": 28, + "name": "active" + }, + { + "args": [], + "lineno": 32, + "name": "console" + }, + { + "args": [ + "output" + ], + "lineno": 36, + "name": "console" + }, + { + "args": [], + "lineno": 40, + "name": "threshold" + }, + { + "args": [ + "thresh" + ], + "lineno": 44, + "name": "threshold" + }, + { + "args": [ + "level", + "text" + ], + "lineno": 48, + "name": "write" + }, + { + "args": [ + "level", + "text" + ], + "lineno": 53, + "name": "writeAlways" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/utility/WrappedSink.h.ai.md b/include/xrpl/beast/utility/WrappedSink.h.ai.md new file mode 100644 index 0000000000..a3a7c54dc6 --- /dev/null +++ b/include/xrpl/beast/utility/WrappedSink.h.ai.md @@ -0,0 +1,41 @@ +# `WrappedSink.h` — Prefixing Decorator for Journal Sinks + +`WrappedSink` is a small but widely-used decorator in the XRPL logging infrastructure. Its sole purpose is to prepend a fixed string to every log message before forwarding the message to an underlying `Journal::Sink`, letting call sites tag log output with contextual identifiers (peer IP addresses, transaction hashes, slot identifiers, etc.) without changing any of the sink's behavioral policies. + +## The Decorator Pattern and Its Dual Inheritance + +The class comment captures the design concisely: + +> A WrappedSink both *is* a Sink and *has* a Sink. + +`WrappedSink` inherits from `beast::Journal::Sink` so it satisfies the `Sink` interface and can be passed anywhere a `Sink&` or used to construct a `Journal`. Simultaneously, it holds a *reference* to another `Sink` — the real, concrete destination — and delegates every virtual method to that reference. + +The consequence is deliberate and notable: the data members inherited from `Sink` (namely `thresh_` and `m_console`, the threshold severity and the console-output flag) are **never used**. They exist in the base class subobject because the `Sink(Sink const&)` copy constructor is called during `WrappedSink`'s initialization (`Sink(sink)` in the member-initializer list), but every query for threshold, active status, or console state immediately forwards to `sink_`. This means `WrappedSink` always reflects the live state of the underlying sink: if a system-level config change raises the threshold on the real sink after a `WrappedSink` has been constructed, the `WrappedSink` picks up the change automatically — something a value-copying approach would break. + +## Write Path + +Only two methods deviate from pure delegation: `write()` and `writeAlways()`. Both prepend `prefix_` to the text argument before passing it down: + +```cpp +void write(beast::severities::Severity level, std::string const& text) override { + sink_.write(level, prefix_ + text); +} +``` + +The distinction between `write()` and `writeAlways()` is preserved faithfully: `write()` is subject to the underlying sink's active threshold (the sink will silently drop messages below the threshold), while `writeAlways()` bypasses that check. `WrappedSink` is careful to call the *matching* method on `sink_` rather than routing both through `write()`, which would have incorrectly suppressed forced-output messages. + +## Constructors + +Two constructors are provided. The primary one accepts a `beast::Journal::Sink&` and a prefix string. A convenience overload accepts a `beast::Journal const&` and immediately extracts the sink via `journal.sink()`, delegating to the first constructor — this saves callers from having to unwrap the journal themselves when they already have a `Journal` in scope. + +The prefix defaults to an empty string, making a zero-prefix `WrappedSink` valid (though in practice the interesting use case is always a non-empty prefix). The prefix can also be changed at any time via the `prefix(const std::string&)` mutator. + +## Ownership and Lifetime + +`WrappedSink` holds a non-owning reference (`sink_`). Callers are responsible for ensuring the referenced sink outlives the `WrappedSink`. This is safe in practice because sinks are long-lived application objects owned by log managers or application-level infrastructure that outlive any individual peer or subsystem. The reference semantics avoid shared ownership overhead while keeping the interface clean. + +## Usage Patterns in the Codebase + +The dominant usage pattern is per-connection context tagging. In `PeerImp.h`, each network peer object owns two `WrappedSink` members (`sink_` and `p_sink_`) that wrap the shared overlay journal with a per-peer prefix, then vend those as `Journal` instances. In `PeerFinder::Logic`, temporary `WrappedSink` instances are stack-allocated at the top of each method to tag log messages with the relevant slot's prefix for the duration of a single operation. `Transactor.h` follows the same pattern to annotate transaction-processing log output with a transaction-specific identifier. + +This pattern keeps the core `Journal`/`Sink` interface immutable and free of prefix concerns, while letting higher-level components inject contextual tagging without coordinating with the logging backend. \ No newline at end of file diff --git a/include/xrpl/beast/utility/Zero.h.ai.json b/include/xrpl/beast/utility/Zero.h.ai.json new file mode 100644 index 0000000000..b2397b1537 --- /dev/null +++ b/include/xrpl/beast/utility/Zero.h.ai.json @@ -0,0 +1,200 @@ +{ + "args": [ + { + "lineno": 36, + "name": "t" + }, + { + "lineno": 45, + "name": "t" + }, + { + "lineno": 53, + "name": "t" + }, + { + "lineno": 58, + "name": "t" + }, + { + "lineno": 63, + "name": "t" + }, + { + "lineno": 68, + "name": "t" + }, + { + "lineno": 73, + "name": "t" + }, + { + "lineno": 78, + "name": "t" + }, + { + "lineno": 84, + "name": "t" + }, + { + "lineno": 89, + "name": "t" + }, + { + "lineno": 94, + "name": "t" + }, + { + "lineno": 99, + "name": "t" + }, + { + "lineno": 104, + "name": "t" + }, + { + "lineno": 109, + "name": "t" + } + ], + "classes": [ + { + "args": [], + "lineno": 27, + "name": "Zero" + } + ], + "description": "Provides a Zero struct and related comparison operators to allow efficient and type-safe comparisons to zero for user-defined types, using a signum() method or function.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/utility/Zero.h", + "functions": [ + { + "args": [ + "t" + ], + "lineno": 36, + "name": "signum" + }, + { + "args": [ + "t" + ], + "lineno": 45, + "name": "call_signum" + }, + { + "args": [ + "t", + "" + ], + "lineno": 53, + "name": "operator==" + }, + { + "args": [ + "t", + "" + ], + "lineno": 58, + "name": "operator!=" + }, + { + "args": [ + "t", + "" + ], + "lineno": 63, + "name": "operator<" + }, + { + "args": [ + "t", + "" + ], + "lineno": 68, + "name": "operator>" + }, + { + "args": [ + "t", + "" + ], + "lineno": 73, + "name": "operator>=" + }, + { + "args": [ + "t", + "" + ], + "lineno": 78, + "name": "operator<=" + }, + { + "args": [ + "", + "t" + ], + "lineno": 84, + "name": "operator==" + }, + { + "args": [ + "", + "t" + ], + "lineno": 89, + "name": "operator!=" + }, + { + "args": [ + "", + "t" + ], + "lineno": 94, + "name": "operator<" + }, + { + "args": [ + "", + "t" + ], + "lineno": 99, + "name": "operator>" + }, + { + "args": [ + "", + "t" + ], + "lineno": 104, + "name": "operator>=" + }, + { + "args": [ + "", + "t" + ], + "lineno": 109, + "name": "operator<=" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + }, + { + "lineno": 31, + "name": "" + }, + { + "lineno": 42, + "name": "detail" + }, + { + "lineno": 43, + "name": "zero_helper" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/utility/Zero.h.ai.md b/include/xrpl/beast/utility/Zero.h.ai.md new file mode 100644 index 0000000000..684ddab17d --- /dev/null +++ b/include/xrpl/beast/utility/Zero.h.ai.md @@ -0,0 +1,45 @@ +# `Zero.h` — Type-Safe Zero Comparison for Unit-Bearing Quantities + +## Role and Motivation + +`Zero.h` defines a sentinel tag type, `beast::Zero`, that enables clean, type-safe zero comparisons for classes where comparing against zero is meaningful but comparing against arbitrary integers is not. The canonical use case in the XRPL codebase is financial amount types — `XRPAmount`, `IOUAmount`, `MPTAmount`, and `base_uint` — which have a natural concept of positive, zero, or negative, but where an expression like `amount > 1` would be semantically wrong (what unit does `1` carry?). The header provides the `zero` constant and a full suite of comparison operators so code can write `amount > zero` or `amount != zero` without constructing a dummy amount object. + +## The `Zero` Struct + +`Zero` is an empty struct with an `explicit` default constructor. The `explicit` keyword is intentional: it prevents implicit conversions from integer literals or other types to `Zero`, keeping the sentinel precisely typed. A `static constexpr Zero zero{}` is declared in an anonymous namespace, providing a convenient expression-level constant that the compiler can treat as a no-cost token — no object is built, no memory is touched. + +## The `signum()` Contract + +Participation in the zero-comparison machinery requires a type to expose a `signum()` operation returning a negative, zero, or positive integer. This can be either a member function or a free function found by ADL in the type's own namespace. The default `beast::signum(T const& t)` template simply forwards to `t.signum()`, so member-based types work without any additional boilerplate. Types that cannot or should not expose a public `signum()` member can instead provide a free function in their own namespace. + +Across the XRPL protocol layer every core amount type implements this contract the same idiomatic way: + +```cpp +// XRPAmount +constexpr int signum() const noexcept { + return (drops_ < 0) ? -1 : (drops_ ? 1 : 0); +} + +// IOUAmount +inline int IOUAmount::signum() const noexcept { + return (mantissa_ < 0) ? -1 : (mantissa_ ? 1 : 0); +} +``` + +This three-way sign extraction is a consistent convention across all amount types in the codebase. + +## The ADL Indirection in `detail::zero_helper` + +The most subtle aspect of the design is `detail::zero_helper::call_signum`. The comment explains it directly: calls to `signum` must originate from a namespace that does not itself declare an overload of `signum`. The reason is classic two-phase lookup and ADL interaction. If the comparison operators called `signum(t)` directly from within `namespace beast`, the compiler would find `beast::signum` via ordinary unqualified name lookup — before ADL even runs — potentially shadowing a more specific free function defined in `T`'s own namespace. By delegating the call through `detail::zero_helper`, which declares no `signum` of its own, ordinary lookup finds nothing, and ADL is free to discover any namespace-level `signum` associated with `T`. This is the standard pattern for building ADL-friendly customization points in C++ prior to the `tag_invoke` era. + +## Operator Layout + +The twelve operator overloads are split into two symmetric groups. The first six handle `T op Zero` — the type under scrutiny is on the left — by calling `call_signum` and comparing the result against zero with the appropriate relational operator. The second six handle `Zero op T` — the zero constant is on the left — by simply reversing the operand order and delegating to the first group. For example, `zero < t` becomes `t > zero`. This keeps the sign-extraction logic entirely in the first group and avoids duplicating the `call_signum` call. + +## Why Not `operator==(T const&, int)`? + +The alternative — overloading comparison against `int` and checking for literal `0` — would allow expressions like `amount == 1` or `amount < 5`, which are semantically nonsensical for unit-bearing types. The `Zero` sentinel type closes this loophole at compile time: the only integer-like value the operator set accepts is the `zero` constant, and that constant has a distinct C++ type that no integer literal can implicitly become. This is the same technique used by `std::nullptr_t` to give `nullptr` a type that can never be confused with an integer zero. + +## Summary + +`Zero.h` is a small but architecturally principled header. Its value lies entirely in what it prevents: spurious numeric comparisons on quantity types that carry units or other semantic constraints. The `signum()` convention reduces all zero comparisons to a single integer sign check, the ADL indirection in `call_signum` ensures extensibility without namespace pollution, and the explicit constructor on `Zero` closes the implicit-conversion loophole. Together these decisions make the pattern robust enough that every financial primitive in the XRPL protocol layer relies on it. \ No newline at end of file diff --git a/include/xrpl/beast/utility/instrumentation.h.ai.json b/include/xrpl/beast/utility/instrumentation.h.ai.json new file mode 100644 index 0000000000..c696daddeb --- /dev/null +++ b/include/xrpl/beast/utility/instrumentation.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "Defines instrumentation and assertion macros for use in XRPL code, providing wrappers for assertions and fuzzing instrumentation, with conditional support for Antithesis SDK or fallback to assert-based macros.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/utility/instrumentation.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/beast/utility/instrumentation.h.ai.md b/include/xrpl/beast/utility/instrumentation.h.ai.md new file mode 100644 index 0000000000..b89223c9fc --- /dev/null +++ b/include/xrpl/beast/utility/instrumentation.h.ai.md @@ -0,0 +1,60 @@ +# `include/xrpl/beast/utility/instrumentation.h` + +## Role and Purpose + +This header is the single point of control for assertion and fuzzing-instrumentation macros across the XRPL codebase. It serves two distinct audiences simultaneously: the production codebase, where the macros behave like hardened `assert` calls, and the [Antithesis](https://antithesis.com/) continuous fuzzing platform, where the same macros become first-class *test properties* that the fuzzer can observe, track, and try to violate. + +The file contains no classes or functions — it is entirely macro-driven and pulls in at most one external header. + +## Two Compile-Time Personalities + +The entire file pivots on a single preprocessor guard: + +```cpp +#ifdef ENABLE_VOIDSTAR +#ifdef NDEBUG +#error "Antithesis instrumentation requires Debug build" +#endif +#include +#else +// fallback definitions using assert(...) +#endif +``` + +When `ENABLE_VOIDSTAR` is defined (activated by the `-Dvoidstar=ON` CMake option in CI on Linux/amd64 with Clang 16), the vendored `external/antithesis-sdk/antithesis_sdk.h` is included. This SDK — which requires C++20 and Clang ≥ 16 — instruments the binary so that when `xrpld` runs under the Antithesis platform, every contract call is forwarded via `libvoidstar.so` (loaded at `/usr/lib/libvoidstar.so`). The platform then treats each contract as a trackable property it can try to falsify. + +The `NDEBUG` guard enforces that Antithesis mode is Debug-only: the SDK's contract tracking is meaningless in an optimized build where code paths are eliminated. + +In all other configurations (Release builds, Windows/MSVC toolchains, developer machines) the else-branch provides local fallback stubs. The comment explains the duplication: "Visual Studio 2019 cannot compile that header even with the option `-Zc:__cplusplus` added." Rather than add a MSVC compatibility shim to the upstream SDK, the project simply copies the simplified fallback forms inline. + +## The Macro Set + +The fallback definitions reveal the contract semantics clearly: + +- **`ALWAYS(cond, message, ...)`** — asserts the condition is true *and* signals to the fuzzer that this line must be reached. Fallback: `assert((message) && (cond))`. The message string is ANDed so that a failure displays the contract name before aborting. +- **`ALWAYS_OR_UNREACHABLE(cond, message)`** — same assertion, but does not require the line to be reached during fuzzing. This distinction matters in dead-code or rare-path guards. +- **`SOMETIMES(cond, message, ...)`** — a fuzzer *hint* only: "try to find an execution where this is true." Fallback is a complete no-op, which is correct — it has no runtime effect in normal builds. +- **`REACHABLE(message, ...)`** — tells the fuzzer that this line should be reachable. No-op in fallback. +- **`UNREACHABLE(message, ...)`** — fallback: `assert((message) && false)`. Critically, this does **not** map to `std::unreachable`: execution continues past a failed `UNREACHABLE` in both Release builds and during fuzzing, rather than triggering undefined behaviour or an immediate process abort. The semantics are "this situation is contractually impossible; if it happens, report it — but don't create UB." + +## XRPL-Specific Aliases + +`XRPL_ASSERT` and `XRPL_ASSERT_PARTS` are thin wrappers over `ALWAYS_OR_UNREACHABLE` that exist to enforce the project's contract-naming convention: + +```cpp +#define XRPL_ASSERT ALWAYS_OR_UNREACHABLE +#define XRPL_ASSERT_PARTS(cond, function, description, ...) \ + XRPL_ASSERT(cond, function " : " description) +``` + +`XRPL_ASSERT_PARTS` is purely ergonomic: it separates the qualified function name from the brief description and joins them with `" : "` at the preprocessor level, producing the canonical form `"xrpl::LedgerTrie::insert : valid input ledger"`. In practice, both forms appear throughout the codebase; `LedgerTrie.h` for instance uses bare `XRPL_ASSERT` with pre-formatted string literals. + +## Contract Naming as Stable Identity + +The use of `ALWAYS_OR_UNREACHABLE` rather than `ALWAYS` for `XRPL_ASSERT` is intentional: many internal assertions guard conditions that may not be exercised in every fuzzing run, so demanding reachability would generate spurious failures. The "OR_UNREACHABLE" variant says "if this line is reached, the condition must hold" without penalising paths that skip it. + +The mandatory unique name serves a purpose beyond documentation. Contract names are stable identifiers on the Antithesis platform — unlike line numbers, they survive refactoring. CONTRIBUTING.md mandates the form `"qualified::scope::function : brief description"` and explicitly warns against renaming contracts without cause, since doing so severs the historical record of whether that property has ever been violated. `XRPL_ASSERT_PARTS` makes it syntactically harder to accidentally produce a malformed name. + +## Usage Boundaries + +The project draws a clear line about where these macros apply. Regular `assert` and `assert(false)` remain correct inside `constexpr` functions (where macros cannot be evaluated at compile time), inside unit tests under `src/test`, and in beast test infrastructure — contexts where Antithesis property tracking is either impossible or undesirable. Everywhere else, `XRPL_ASSERT` replaces `assert` and `UNREACHABLE` replaces `assert(false)`, with `std::unreachable` explicitly forbidden throughout the codebase. \ No newline at end of file diff --git a/include/xrpl/beast/utility/maybe_const.h.ai.json b/include/xrpl/beast/utility/maybe_const.h.ai.json new file mode 100644 index 0000000000..795b92378f --- /dev/null +++ b/include/xrpl/beast/utility/maybe_const.h.ai.json @@ -0,0 +1,20 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "maybe_const" + } + ], + "description": "Provides a template utility to conditionally make a type const or non-const based on a boolean value.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/utility/maybe_const.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/utility/maybe_const.h.ai.md b/include/xrpl/beast/utility/maybe_const.h.ai.md new file mode 100644 index 0000000000..81e17b29b6 --- /dev/null +++ b/include/xrpl/beast/utility/maybe_const.h.ai.md @@ -0,0 +1,46 @@ +# `maybe_const.h` — Conditional Const Type Trait + +## Role and Purpose + +`maybe_const.h` is a small but purposeful metaprogramming utility in the `beast` namespace. It solves a recurring C++ template design problem: when a single template class needs to expose both mutable and immutable views of internal data, controlled by a boolean template parameter, selecting `T` vs. `const T` inline with `std::conditional` becomes verbose and error-prone. `maybe_const` encapsulates that selection cleanly. + +## Design + +The template struct `maybe_const` wraps a single `std::conditional` to produce either `const T` or `T`: + +```cpp +template +struct maybe_const { + using type = typename std::conditional< + IsConst, + typename std::remove_const::type const, + typename std::remove_const::type>::type; +}; +``` + +A deliberate detail here is the `std::remove_const` applied to `T` before re-adding `const`. This normalises the input: if a caller accidentally passes `const U` as `T` with `IsConst == false`, the result is still plain `U` rather than `const U`. Symmetrically, when `IsConst == true`, `remove_const` strips any existing qualifier before the canonical `const` is applied, preventing accidental `const const T` constructions that some compilers warn about or that complicate downstream deduction. This defensive normalisation is the non-obvious part of the design. + +The companion alias `maybe_const_t` eliminates the `typename …::type` boilerplate at use sites, which is particularly valuable inside nested template contexts. + +## How It Is Used in the Codebase + +The only consumer found in the repository is `src/xrpld/peerfinder/detail/Livecache.h`, and it illustrates the pattern perfectly. `Livecache` maintains a fixed-size array of `list_type` buckets, one per hop count. It exposes iteration over those buckets through a `hops_t` helper that offers both mutable `iterator` and immutable `const_iterator` types via `boost::transform_iterator`. + +The transform functor is itself templatised on `bool IsConst`: + +```cpp +template +struct Transform { + Hop + operator()( + typename beast::maybe_const::type& list) const; +}; +``` + +Without `maybe_const`, this functor would need two separate specialisations — one taking `list_type&` and one taking `const list_type&` — doubling the boilerplate. With `maybe_const`, a single template covers both cases: `Transform` receives a mutable reference, `Transform` receives a const reference, and the compiler resolves the difference entirely through the boolean parameter. The same pattern repeats in `make_hop` and in `Hop`'s stored `std::reference_wrapper`. + +## Why This Pattern Over the Obvious Alternative + +The straightforward alternative — two separate iterator/functor types — works but violates DRY. Any logic change to the hop-iteration functor would need to be duplicated, and the mutable/immutable symmetry would need to be maintained manually. The `IsConst` boolean parameter approach keeps the invariant that const and non-const traversal are structurally identical, enforced at compile time by sharing a single template body. + +This is a well-known C++ idiom sometimes called the "const propagation template parameter" pattern. `maybe_const` gives it a named, reusable home rather than scattering `std::conditional` spellings across the codebase. \ No newline at end of file diff --git a/include/xrpl/beast/utility/rngfill.h.ai.json b/include/xrpl/beast/utility/rngfill.h.ai.json new file mode 100644 index 0000000000..5968ead977 --- /dev/null +++ b/include/xrpl/beast/utility/rngfill.h.ai.json @@ -0,0 +1,53 @@ +{ + "args": [ + { + "lineno": 10, + "name": "buffer" + }, + { + "lineno": 10, + "name": "bytes" + }, + { + "lineno": 10, + "name": "g" + }, + { + "lineno": 35, + "name": "a" + }, + { + "lineno": 35, + "name": "g" + } + ], + "classes": [], + "description": "Provides utility functions to fill a buffer or std::array with random data using a supplied generator.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/utility/rngfill.h", + "functions": [ + { + "args": [ + "buffer", + "bytes", + "g" + ], + "lineno": 9, + "name": "rngfill" + }, + { + "args": [ + "a", + "g" + ], + "lineno": 34, + "name": "rngfill" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/utility/rngfill.h.ai.md b/include/xrpl/beast/utility/rngfill.h.ai.md new file mode 100644 index 0000000000..ebaf7809b1 --- /dev/null +++ b/include/xrpl/beast/utility/rngfill.h.ai.md @@ -0,0 +1,48 @@ +# `include/xrpl/beast/utility/rngfill.h` + +## Role and Purpose + +`rngfill.h` provides two thin template functions for filling a contiguous region of memory with random bytes drawn from any C++ *UniformRandomNumberEngine*-compliant generator. Its goal is to bridge the conceptual gap between a typed random-number generator — which produces values of some fixed integer width — and raw byte buffers, which are the natural unit of cryptographic key material and binary test data. + +The file lives in the `beast` utility layer, a low-level support library used throughout the XRPL codebase. It is included by cryptographic primitives (`Seed.cpp`, `SecretKey.cpp`), networking infrastructure (`BaseWSPeer.h`), and test helpers (`TestBase.h`), making it one of the most broadly depended-upon utility headers in the project. + +## The Raw-Buffer Overload + +```cpp +template +void rngfill(void* const buffer, std::size_t const bytes, Generator& g); +``` + +This overload handles the general case where the buffer size may be any positive number of bytes. Because a standard generator produces values of `result_type` (e.g., 8 bytes for a `uint64_t`-based engine), the function divides the fill into two phases: + +1. **Complete iterations** — as many full `result_type`-sized chunks as fit in `bytes`, each copied via `std::memcpy`. +2. **Remainder** — if the buffer size is not a multiple of `sizeof(result_type)`, a final generator call provides one more value, of which only the first `bytes_remaining` bytes are copied. + +Using `std::memcpy` is the deliberate and correct choice here. Casting a `uint8_t*` buffer to `result_type*` and writing through it would violate strict-aliasing rules. The `memcpy` approach produces identical code after optimization while keeping the C++ type system satisfied. + +This overload is used in `randomSeed()` (filling a 16-byte array) and `randomSecretKey()` (filling a 32-byte buffer), both of which call into `crypto_prng()` — the XRPL cryptographically secure PRNG backed by OpenSSL, whose `result_type` is `uint64_t`. Sixteen bytes divides evenly by 8, so no remainder path executes there, but the general overload handles both cases correctly regardless. + +## The Typed Array Overload + +```cpp +template > +void rngfill(std::array& a, Generator& g); +``` + +This overload is restricted to `std::array` where `N` is known at compile time and is statically required — via `std::enable_if_t` — to be an exact multiple of `sizeof(Generator::result_type)`. The compile-time divisibility constraint eliminates the partial-fill branch entirely, allowing a tighter loop that writes directly through a `result_type*` pointer cast over the array's storage: + +```cpp +result_type* p = reinterpret_cast(a.data()); +while (i--) *p++ = g(); +``` + +This is safe because `std::array` provides `data()` which returns properly aligned storage, and the `enable_if` constraint ensures no out-of-bounds write is possible. The trade-off is a narrower call signature — only fixed-size byte arrays where the size divides cleanly into generator words qualify — in exchange for a simpler, branchless implementation. + +## Design Philosophy + +Both overloads are generator-agnostic by design. The same `rngfill` works with `crypto_prng()` for security-sensitive operations and with `xor_shift_engine` for high-throughput test-data generation (as seen in `TestBase.h`). This polymorphism via the *UniformRandomNumberEngine* concept keeps call sites clean and allows the fill strategy to be selected by substituting the generator, not by selecting a different fill function. + +The absence of any return value or error path reflects the assumption that a well-formed generator never fails. Any failure condition (e.g., entropy exhaustion in the CSPRNG) is the generator's responsibility to handle, not the fill utility's. + +The `#include ` header is pulled in as a precautionary dependency — `instrumentation.h` defines assertion macros (`XRPL_ASSERT`, `UNREACHABLE`, etc.) used throughout the beast utility layer — though `rngfill.h` itself contains no assertions. \ No newline at end of file diff --git a/include/xrpl/beast/utility/temp_dir.h.ai.json b/include/xrpl/beast/utility/temp_dir.h.ai.json new file mode 100644 index 0000000000..25d16d2045 --- /dev/null +++ b/include/xrpl/beast/utility/temp_dir.h.ai.json @@ -0,0 +1,48 @@ +{ + "args": [ + { + "lineno": 46, + "name": "name" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "temp_dir" + } + ], + "description": "Defines a RAII-style temporary directory class that creates a unique temporary directory on construction and deletes it (with all contents) on destruction.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/utility/temp_dir.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "temp_dir" + }, + { + "args": [], + "lineno": 29, + "name": "~temp_dir" + }, + { + "args": [], + "lineno": 37, + "name": "path" + }, + { + "args": [ + "name" + ], + "lineno": 45, + "name": "file" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/utility/temp_dir.h.ai.md b/include/xrpl/beast/utility/temp_dir.h.ai.md new file mode 100644 index 0000000000..fa78fd9445 --- /dev/null +++ b/include/xrpl/beast/utility/temp_dir.h.ai.md @@ -0,0 +1,23 @@ +# `include/xrpl/beast/utility/temp_dir.h` + +## Role and Purpose + +`temp_dir` is a small, focused RAII wrapper that creates a unique temporary directory on construction and unconditionally removes it — along with all of its contents — on destruction. It lives in the `beast` utility layer and serves exclusively as a test infrastructure primitive throughout the XRPL codebase: unit tests that need scratch space on disk (database backends, configuration files, ledger snapshots) instantiate a `temp_dir`, use it, and let the destructor clean up without any explicit teardown code. + +## Design + +The constructor delegates directory selection entirely to Boost.Filesystem. It queries `boost::filesystem::temp_directory_path()` for the OS-appropriate temp root (`/tmp` on Linux, `%TEMP%` on Windows), then generates a cryptographically random path component via `boost::filesystem::unique_path()`. The `do/while` loop guards against the astronomically unlikely case where a generated name already exists before calling `create_directory`. There is no user-supplied base name or prefix — the generated path is fully opaque, which prevents accidental collisions between parallel test runs. + +Copy construction and copy assignment are explicitly deleted. This is the correct choice for an ownership-of-resource type: two instances pointing at the same directory path would result in a double `remove_all` on destruction, and it would be ambiguous which object "owned" the lifetime of the files inside. Move semantics are also absent, keeping the interface minimal — callers hold the object by value and access the directory through the two accessor methods. + +The destructor calls `boost::filesystem::remove_all` with a `boost::system::error_code` out-parameter rather than letting exceptions propagate. Destructors that throw cause `std::terminate` in most C++ contexts, so swallowing the error here is the only safe option. The `TODO` comment acknowledging the silenced error is honest — a failed cleanup could leave orphaned temp directories, but that is an acceptable tradeoff for destructor safety in test code. + +## Accessor Interface + +`path()` returns the native string representation of the directory itself, suitable for passing directly to subsystems that accept `std::string` paths (e.g., NuDB and RocksDB backend configuration via `Section::set("path", ...)`). + +`file(name)` computes the path of a named entry inside the directory by appending the given name with `operator/` before converting to string. The method comment explicitly notes the file does not need to exist — this is intentional, since tests often need a pre-determined path string before actually creating the file (as seen in `Config_test.cpp`, where the path is passed to `std::ofstream` for writing). + +## Usage in Tests + +Every consumer follows the same idiom: declare `beast::temp_dir const td;` as a local variable in the test body, then use `td.path()` or `td.file("name")` to wire up paths. Because it is `const`, the object is immutable after construction — there is no API to rename, move, or replace the directory mid-test, reinforcing that its only job is to own a stable scratch location for the duration of a test case. When the scope exits, the destructor fires and the filesystem is cleaned up, leaving no artifacts regardless of whether the test passed or failed. \ No newline at end of file diff --git a/include/xrpl/beast/xor_shift_engine.h.ai.json b/include/xrpl/beast/xor_shift_engine.h.ai.json new file mode 100644 index 0000000000..a221249cc5 --- /dev/null +++ b/include/xrpl/beast/xor_shift_engine.h.ai.json @@ -0,0 +1,77 @@ +{ + "args": [ + { + "lineno": 22, + "name": "val" + }, + { + "lineno": 25, + "name": "seed" + }, + { + "lineno": 43, + "name": "x" + } + ], + "classes": [ + { + "args": [ + "xor_shift_engine const&", + "result_type val = 1977u" + ], + "lineno": 13, + "name": "xor_shift_engine" + } + ], + "description": "Implements a simple and fast XOR-shift based random number generator (RNG) called xor_shift_engine, which meets the requirements of a UniformRandomNumberGenerator.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/beast/xor_shift_engine.h", + "functions": [ + { + "args": [ + "val" + ], + "lineno": 22, + "name": "xor_shift_engine" + }, + { + "args": [ + "seed" + ], + "lineno": 25, + "name": "seed" + }, + { + "args": [], + "lineno": 28, + "name": "operator()" + }, + { + "args": [], + "lineno": 31, + "name": "min" + }, + { + "args": [], + "lineno": 36, + "name": "max" + }, + { + "args": [ + "x" + ], + "lineno": 43, + "name": "murmurhash3" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 8, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/beast/xor_shift_engine.h.ai.md b/include/xrpl/beast/xor_shift_engine.h.ai.md new file mode 100644 index 0000000000..7e56fbe451 --- /dev/null +++ b/include/xrpl/beast/xor_shift_engine.h.ai.md @@ -0,0 +1,57 @@ +# `xor_shift_engine.h` — XOR-Shift 128+ PRNG + +## Role in the System + +This header defines `beast::xor_shift_engine`, the XRPL ledger's default pseudo-random number generator. It is a self-contained, header-only implementation of the xorshift128+ algorithm, exposed as `beast::xor_shift_engine` — a type alias consumed directly by `xrpl::default_prng()` in `include/xrpl/basics/random.h`. Every non-cryptographic random number produced across the node — test data generation, shuffle operations, timing jitter, and buffer fills via `beast::rngfill` — ultimately flows through this engine. + +## Algorithm: xorshift128+ + +The `operator()` body is a verbatim implementation of xorshift128+ as published at [xorshift.di.unimi.it](http://xorshift.di.unimi.it/xorshift128plus.c): + +```cpp +result_type s1 = s_[0]; +result_type const s0 = s_[1]; +s_[0] = s0; +s1 ^= s1 << 23; +return (s_[1] = (s1 ^ s0 ^ (s1 >> 17) ^ (s0 >> 26))) + s0; +``` + +The state is two 64-bit words (`s_[0]`, `s_[1]`), giving a 128-bit combined state and a period of 2¹²⁸ − 1. The algorithm is deliberately built on unsigned integer arithmetic whose overflow wraps mod 2⁶⁴; this is why the file appears in the UBSAN suppression list (`ubsan.supp`) under both `unsigned-integer-overflow` and `undefined` categories — the overflow is load-bearing and intentional, not a bug. + +## Seeding and the MurmurHash3 Finalizer + +A direct consequence of xorshift is that a zero state will produce nothing but zeros forever. The `seed()` method therefore throws `std::domain_error` on a zero input, making the invariant explicit rather than silently producing a degenerate sequence. + +Beyond the zero-exclusion, the seed value is not used raw. Both state words are initialised through the MurmurHash3 finalizer (`murmurhash3`): + +```cpp +s_[0] = murmurhash3(seed); +s_[1] = murmurhash3(s_[0]); +``` + +MurmurHash3's finalizer (the two multiply-xor-shift avalanche rounds with constants `0xff51afd7ed558ccd` and `0xc4ceb9fe1a85ec53`) is a well-known technique for eliminating seed clustering. Without it, nearby seed values produce nearly identical initial states because xorshift is linear; the finalizer mixes the bits so that seeds differing by one bit produce uncorrelated initial states. The two sequential applications also ensure `s_[0] ≠ s_[1]` for any nonzero seed (since `murmurhash3` is a bijection), preventing an illegal all-zero state through the back door. + +## C++ Engine Interface + +The class satisfies the C++ `UniformRandomBitGenerator` named requirement: it provides `result_type`, `min()`, `max()`, and `operator()()`. The `min()` and `max()` return `std::numeric_limits::min/max()`, covering the full 64-bit range. This makes it directly usable with every `std::uniform_*_distribution` adapter, as demonstrated in `random.h`'s `rand_int` family and in `rngfill.h`'s raw-buffer fill loop, which chunks calls to `operator()()` into 8-byte writes. + +`random.h` also asserts these properties at compile time: + +```cpp +static_assert(std::is_integral::value && ...); +static_assert(std::numeric_limits::max() >= ...uint64_t::max()); +``` + +This ensures that the type alias can never be swapped for a narrower engine without a build failure. + +## Dummy Template Parameter + +The class is declared as `template class xor_shift_engine` inside `namespace detail`, then exposed as a type alias `using xor_shift_engine = detail::xor_shift_engine<>`. This is a common C++ header-only pattern: the dummy template parameter makes the class a class template, so its member function definitions in the same header are treated as implicit instantiations rather than external definitions, avoiding One Definition Rule violations when the header is included from multiple translation units. There is no intention of specialising the template; the `class _` parameter is permanently unused. + +## Threading Model + +The engine itself carries no locks or thread-safety guarantees — it is a plain value type with two `uint64_t` words of mutable state. Thread safety is the caller's responsibility. `default_prng()` in `random.h` addresses this with `thread_local` storage: each thread gets its own `xor_shift_engine` instance, seeded non-deterministically from `std::random_device` through a shared seeder that is itself an `xor_shift_engine` protected by a `std::mutex`. Concurrent reads from different threads are therefore isolated without any contention at the call site. + +## What It Is Not + +The comment in `random.h` is unambiguous: this engine is **not cryptographically secure**. Keys, IVs, secure cookies, and any material that must be unpredictable to an adversary must use a different source of randomness. `xor_shift_engine` is purely for performance-sensitive internal operations where statistical quality — not secrecy — is the requirement. \ No newline at end of file diff --git a/include/xrpl/conditions/Condition.h.ai.json b/include/xrpl/conditions/Condition.h.ai.json new file mode 100644 index 0000000000..53a6e1473d --- /dev/null +++ b/include/xrpl/conditions/Condition.h.ai.json @@ -0,0 +1,101 @@ +{ + "args": [ + { + "lineno": 33, + "name": "s" + }, + { + "lineno": 33, + "name": "ec" + }, + { + "lineno": 49, + "name": "t" + }, + { + "lineno": 49, + "name": "c" + }, + { + "lineno": 49, + "name": "fp" + }, + { + "lineno": 53, + "name": "t" + }, + { + "lineno": 53, + "name": "c" + }, + { + "lineno": 53, + "name": "fp" + }, + { + "lineno": 67, + "name": "lhs" + }, + { + "lineno": 67, + "name": "rhs" + }, + { + "lineno": 74, + "name": "lhs" + }, + { + "lineno": 74, + "name": "rhs" + } + ], + "classes": [ + { + "args": [ + "Type t, std::uint32_t c, Slice fp", + "Type t, std::uint32_t c, Buffer&& fp" + ], + "lineno": 15, + "name": "Condition" + } + ], + "description": "Defines the Condition class and related types for representing and handling crypto-conditions in the xrpl::cryptoconditions namespace, including serialization, deserialization, and comparison operators.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/conditions/Condition.h", + "functions": [ + { + "args": [ + "Slice s", + "std::error_code& ec" + ], + "lineno": 32, + "name": "deserialize" + }, + { + "args": [ + "Condition const& lhs", + "Condition const& rhs" + ], + "lineno": 66, + "name": "operator==" + }, + { + "args": [ + "Condition const& lhs", + "Condition const& rhs" + ], + "lineno": 73, + "name": "operator!=" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "cryptoconditions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/conditions/Condition.h.ai.md b/include/xrpl/conditions/Condition.h.ai.md new file mode 100644 index 0000000000..8891cb367f --- /dev/null +++ b/include/xrpl/conditions/Condition.h.ai.md @@ -0,0 +1,38 @@ +# `include/xrpl/conditions/Condition.h` + +## Role in the System + +This header defines the `Condition` type at the heart of the XRPL *cryptoconditions* subsystem — an implementation of [draft-thomas-crypto-conditions-02](https://tools.ietf.org/html/draft-thomas-crypto-conditions-02), the IETF specification that underpins XRPL's *EscrowCreate* / *EscrowFinish* transaction logic. A `Condition` is the commitment half of the two-part cryptoconditions protocol: it encodes *what* must be proven (a fingerprint and the type of proof required) without revealing *how* to prove it. The complementary `Fulfillment` type (in `Fulfillment.h`) carries the proof itself. + +## The `Type` Enum + +The `Type` enum maps directly onto the five condition types defined by the RFC — `preimageSha256 (0)` through `ed25519Sha256 (4)`. The underlying `uint8_t` tag values matter: they are the actual DER context-specific constructed tags used in the wire encoding, so the enum's integer values are protocol constants, not implementation conveniences. In practice, XRPL currently only supports `preimageSha256`; deserializing any other type results in `error::unsupported_type`, which is an intentional, forward-compatible design — the type taxonomy is fully declared so the ledger can recognise all RFC types and reject unsupported ones cleanly, rather than silently treating them as unknown. + +## The `Condition` Class + +`Condition` is a plain-data aggregate of four fields: + +- **`type`** — which of the five RFC condition types this is. +- **`fingerprint`** — a 32-byte `Buffer` that uniquely identifies the condition within its type. For `preimageSha256`, this is the SHA-256 hash of the preimage; the RFC guarantees that two conditions of the same type with the same fingerprint are equivalent. +- **`cost`** — a `uint32_t` upper-bounding the computational cost of verifying any fulfillment for this condition. For preimage conditions this equals the preimage length in bytes. The cost field lets a validator reject economically ruinous fulfillments before performing the actual verification. +- **`subtypes`** — a `std::set` that is only populated for compound condition types (prefix, threshold). It lists all condition types reachable through the compound tree, enabling validators to reject compound conditions that embed unsupported sub-types without recursing into the full structure. + +The default constructor is `= delete`. Every `Condition` must be constructed with all three essential fields — `type`, `cost`, and `fingerprint` — preventing partially-initialised instances from being created accidentally. Two constructors are provided: one taking `Slice fp` (copies the fingerprint bytes) and one taking `Buffer&& fp` (moves ownership), covering both the serialisation path (where the buffer was freshly parsed) and any in-memory construction path. + +## `maxSerializedCondition` + +The class-level constant `maxSerializedCondition = 128` caps the accepted wire size of a binary condition. The comment in the header makes an important asymmetry explicit: the value may increase in future versions to accommodate larger condition types, but it may *never decrease*, because doing so would invalidate conditions already accepted onto the ledger. This is a ledger-consensus invariant encoded as a comment — the numeric constant and its immutability contract must be kept in sync across protocol upgrades. + +## Deserialization + +`Condition::deserialize(Slice s, std::error_code& ec)` is a static factory that parses a DER-encoded condition from a raw byte slice. The implementation (in `Condition.cpp`) first reads the DER preamble to extract the context tag, maps it to one of the five `Type` values, then dispatches to `detail::loadSimpleSha256` or returns `error::unsupported_type` for compound types. The `loadSimpleSha256` helper extracts the 32-byte fingerprint and the 4-byte cost integer from the SEQUENCE fields, enforcing exact sizes (e.g., fingerprint must be exactly 32 bytes) and rejecting trailing garbage. + +Error reporting follows XRPL's standard pattern of an output `std::error_code&` parameter. `error.h` integrates with `` via the `is_error_code_enum` specialisation so that condition errors participate in the standard C++ error-category system. + +## Comparison Operators + +The free `operator==` compares all four fields — type, cost, subtypes, and fingerprint. The fingerprint comparison relies on `Buffer`'s own `operator==`. Including `cost` in equality is deliberate: two conditions with identical fingerprints but different costs would be semantically distinct (different verification budgets) and must not be treated as equivalent. This also means `Condition` equality is well-defined for use in `std::set` (if a comparator is provided) or flat comparison, but `Condition` has no `operator<`, so it cannot be stored directly in an ordered container without a custom comparator. + +## Relationship to `Fulfillment` + +`Fulfillment` is the abstract counterpart. Its virtual `condition()` method returns a `Condition` value object computed deterministically from the fulfillment — making `Condition` the canonical, storable commitment that lives on the ledger, while `Fulfillment` is the transient proof supplied in a transaction. The `match()` and `validate()` free functions in `Fulfillment.h` tie them together: `validate()` checks that a fulfillment's derived condition matches the stored condition *and* that the fulfillment's cryptographic claim holds against the message. This separation means the ledger never needs to store or recompute fulfillments after an escrow is created — only the compact `Condition` is persisted. \ No newline at end of file diff --git a/include/xrpl/conditions/Fulfillment.h.ai.json b/include/xrpl/conditions/Fulfillment.h.ai.json new file mode 100644 index 0000000000..68e3757647 --- /dev/null +++ b/include/xrpl/conditions/Fulfillment.h.ai.json @@ -0,0 +1,139 @@ +{ + "args": [ + { + "lineno": 23, + "name": "s" + }, + { + "lineno": 23, + "name": "ec" + }, + { + "lineno": 49, + "name": "data" + }, + { + "lineno": 68, + "name": "lhs" + }, + { + "lineno": 68, + "name": "rhs" + }, + { + "lineno": 80, + "name": "f" + }, + { + "lineno": 80, + "name": "c" + }, + { + "lineno": 85, + "name": "m" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "Fulfillment" + } + ], + "description": "Defines the Fulfillment struct and related functions for handling cryptographic condition fulfillments in the XRPL cryptoconditions module, including serialization, validation, and comparison.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/conditions/Fulfillment.h", + "functions": [ + { + "args": [ + "Slice s", + "std::error_code& ec" + ], + "lineno": 22, + "name": "Fulfillment::deserialize" + }, + { + "args": [], + "lineno": 30, + "name": "Fulfillment::~Fulfillment" + }, + { + "args": [], + "lineno": 38, + "name": "Fulfillment::fingerprint" + }, + { + "args": [], + "lineno": 45, + "name": "Fulfillment::type" + }, + { + "args": [ + "Slice data" + ], + "lineno": 48, + "name": "Fulfillment::validate" + }, + { + "args": [], + "lineno": 54, + "name": "Fulfillment::cost" + }, + { + "args": [], + "lineno": 61, + "name": "Fulfillment::condition" + }, + { + "args": [ + "Fulfillment const& lhs", + "Fulfillment const& rhs" + ], + "lineno": 67, + "name": "operator==" + }, + { + "args": [ + "Fulfillment const& lhs", + "Fulfillment const& rhs" + ], + "lineno": 74, + "name": "operator!=" + }, + { + "args": [ + "Fulfillment const& f", + "Condition const& c" + ], + "lineno": 79, + "name": "match" + }, + { + "args": [ + "Fulfillment const& f", + "Condition const& c", + "Slice m" + ], + "lineno": 84, + "name": "validate" + }, + { + "args": [ + "Fulfillment const& f", + "Condition const& c" + ], + "lineno": 99, + "name": "validate" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "cryptoconditions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/conditions/Fulfillment.h.ai.md b/include/xrpl/conditions/Fulfillment.h.ai.md new file mode 100644 index 0000000000..77015951ed --- /dev/null +++ b/include/xrpl/conditions/Fulfillment.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/conditions/Fulfillment.h` + +## Role in the System + +This header defines the abstract interface for the *fulfillment* side of the [Crypto-Conditions RFC](https://tools.ietf.org/html/draft-thomas-crypto-conditions-02) as implemented in the XRPL. Crypto-conditions are a two-part scheme: a **condition** is a compact commitment (hash + cost + type tag), and a **fulfillment** is the preimage or cryptographic proof that satisfies it. `Fulfillment.h` owns the polymorphic base type `Fulfillment` that all concrete condition types inherit from, plus the free functions that tie fulfillments and conditions together at the point of use. + +In the XRP Ledger, crypto-conditions gate escrow releases: a sender locks funds with a condition, and the claimer must supply the correct fulfillment to unlock them. This header, together with `Condition.h`, forms the complete public surface of the `xrpl::cryptoconditions` module. + +## `Fulfillment` Abstract Base + +`Fulfillment` is a pure-virtual struct with ownership semantics enforced via `std::unique_ptr`. Every concrete type must implement four methods: + +- **`type()`** — returns the `Type` enum tag (`preimageSha256`, `ed25519Sha256`, etc.) so that dispatch can be done without `dynamic_cast`. +- **`fingerprint()`** — returns an opaque `Buffer` that is type-specific. For `PreimageSha256` this is `SHA-256(preimage)`; for signature types it would be a hash of the public key material. The fingerprint is meaningful only within a type — two conditions of different types with the same fingerprint bytes are not equivalent. +- **`cost()`** — a `uint32_t` measure of the computational work required to evaluate the fulfillment. For preimage conditions it equals the preimage length; for threshold conditions it accumulates sub-condition costs. The ledger uses this to bound resource consumption per transaction. +- **`validate(Slice data)`** — checks whether the fulfillment is internally self-consistent given a message. For preimage conditions the message is irrelevant (the fulfillment *is* its own proof) and this always returns `true`; for signature-based conditions the message would be the signed payload. +- **`condition()`** — derives the matching `Condition` value from the fulfillment deterministically. The derived condition can then be compared against the committed condition stored on-ledger. + +The design choice to derive the condition *from* the fulfillment, rather than storing it, is intentional: it makes the relationship provably deterministic and removes a whole class of inconsistency bugs where a stored condition could disagree with the actual fulfillment. + +## Deserialization + +`Fulfillment::deserialize(Slice s, std::error_code& ec)` is the sole entry point for loading a fulfillment from its DER-encoded binary form. The implementation (in `Fulfillment.cpp`) reads the outer ASN.1 preamble, inspects the context-specific constructed tag to determine the type, enforces the `maxSerializedFulfillment = 256` byte cap, and delegates to the appropriate concrete deserializer. The size cap is declared with an explicit `@note` that it must never *decrease* — doing so would retroactively invalidate fulfillments already stored in closed ledgers. + +Currently only `PreimageSha256` is fully implemented; the remaining four RFC types (`prefixSha256`, `thresholdSha256`, `rsaSha256`, `ed25519Sha256`) all return `error::unsupported_type` immediately. This is a deliberate scope limitation — XRPL escrow only requires preimage conditions, and partially implementing the other types would risk subtle RFC non-compliance. + +## Free Functions: `match` and `validate` + +Three free functions compose the fulfillment/condition check: + +`match(f, c)` first fast-checks the type tag, then derives `f.condition()` and does a full `Condition` equality comparison (type + cost + fingerprint + subtypes). This two-step approach avoids computing `SHA-256` if the type tags already differ. + +`validate(f, c, m)` calls `match` followed by `f.validate(m)`, combining structural match and cryptographic self-validation in one call. A second overload `validate(f, c)` passes an empty `Slice` as the message, serving the **cryptoconditional trigger** pattern where conditions carry no external message context. The inline documentation specifically notes that signature-type conditions used as triggers should employ single-use keys — a security warning meaningful for ledger integrators. + +## Equality Operators + +`operator==` compares type, cost, and fingerprint. A `FIXME` comment acknowledges that compound conditions (threshold, prefix) also require comparison of the `subtypes` bitset, which `Condition::operator==` does include. This means comparing two `Fulfillment` objects directly may produce false positives for compound types if they share the same fingerprint but differ in subtypes — a known gap to be closed when compound fulfillments are implemented. + +## Relationship to Concrete Implementations + +`PreimageSha256` (in `detail/PreimageSha256.h`) is the canonical example of the pattern. It stores a raw `Buffer` payload, computes `fingerprint()` on demand via `sha256_hasher`, and reports `cost()` as payload length. Its `validate(Slice)` ignores the message entirely — the act of knowing the preimage is sufficient proof. This makes the base class's `validate` contract slightly unusual: the method is formally about message validation, but for preimage types it degenerates to a tautology. + +The `detail/error.h` error taxonomy covers the full range of parse failures: malformed DER preamble, under/overfull buffers, trailing garbage, oversized preimages, and unknown type tags. All deserialization paths propagate errors through `std::error_code` rather than exceptions, consistent with the rest of `libxrpl`. \ No newline at end of file diff --git a/include/xrpl/conditions/detail/PreimageSha256.h.ai.json b/include/xrpl/conditions/detail/PreimageSha256.h.ai.json new file mode 100644 index 0000000000..f68ddfde74 --- /dev/null +++ b/include/xrpl/conditions/detail/PreimageSha256.h.ai.json @@ -0,0 +1,90 @@ +{ + "args": [ + { + "lineno": 29, + "name": "s" + }, + { + "lineno": 29, + "name": "ec" + }, + { + "lineno": 67, + "name": "b" + } + ], + "classes": [ + { + "args": [ + "Buffer&& b", + "Slice s" + ], + "lineno": 13, + "name": "PreimageSha256" + } + ], + "description": "Implements the PreimageSha256 fulfillment for cryptographic conditions in the XRPL, including deserialization, fingerprinting, cost calculation, and validation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/conditions/detail/PreimageSha256.h", + "functions": [ + { + "args": [ + "Slice s", + "std::error_code& ec" + ], + "lineno": 27, + "name": "deserialize" + }, + { + "args": [ + "Buffer&& b" + ], + "lineno": 67, + "name": "PreimageSha256" + }, + { + "args": [ + "Slice s" + ], + "lineno": 71, + "name": "PreimageSha256" + }, + { + "args": [], + "lineno": 75, + "name": "type" + }, + { + "args": [], + "lineno": 80, + "name": "fingerprint" + }, + { + "args": [], + "lineno": 88, + "name": "cost" + }, + { + "args": [], + "lineno": 93, + "name": "condition" + }, + { + "args": [ + "Slice" + ], + "lineno": 98, + "name": "validate" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "cryptoconditions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/conditions/detail/PreimageSha256.h.ai.md b/include/xrpl/conditions/detail/PreimageSha256.h.ai.md new file mode 100644 index 0000000000..23b637c447 --- /dev/null +++ b/include/xrpl/conditions/detail/PreimageSha256.h.ai.md @@ -0,0 +1,39 @@ +# `PreimageSha256` — Cryptocondition Fulfillment Type 0 + +## Role in the System + +`PreimageSha256` is the simplest fulfillment type defined by the [Interledger Crypto-Conditions draft RFC](https://tools.ietf.org/html/draft-thomas-crypto-conditions-02). It implements the classic hash-lock pattern: the *condition* is the SHA-256 digest of a secret byte string, and the *fulfillment* is the preimage itself. Revealing the preimage proves knowledge of the secret without requiring any asymmetric cryptography. + +Within the XRPL `cryptoconditions` subsystem, five condition types are recognized (`preimageSha256`, `prefixSha256`, `thresholdSha256`, `rsaSha256`, `ed25519Sha256`). `PreimageSha256` is type 0 — the lowest-cost, lowest-complexity variant and the only one whose validation is entirely message-independent. Notably, `utils.h` states outright that its DER decoder "only implements the bare minimum needed to support PreimageSha256," making this class the foundational use case for the entire conditions detail subsystem. + +## Class Design + +`PreimageSha256` is a `final` concrete subclass of the abstract `Fulfillment` interface, which mandates four operations: `type()`, `fingerprint()`, `cost()`, `condition()`, and `validate()`. The class stores a single `Buffer payload_` — the raw preimage bytes. + +Two constructors are provided: one accepting a `Buffer&&` (used by `deserialize()` after parsing) and one accepting a `Slice` (for constructing a fulfillment directly from in-memory data, such as when building a transaction). Both are `noexcept` because `Buffer` construction from these sources cannot throw. + +## Deserialization + +`deserialize()` is a static factory that parses a DER-encoded preimage fulfillment. The RFC specifies the wire format as a context-specific, primitive, tag-0 octet string. The parser enforces this strictly in sequence: + +1. **Preamble check** — `parsePreamble()` extracts the type byte and length; both long-form tags and malformed lengths are rejected with specific error codes. +2. **Encoding class** — must be `contextSpecific` and `primitive` (bit patterns `0x80` and `~0x20`); any other combination yields `error::incorrect_encoding`. +3. **Tag value** — must be exactly `0`; any other tag yields `error::unexpected_tag`. +4. **Trailing data** — `s.size() != p.length` is checked after preamble consumption. If the input slice has bytes beyond what the length field declares, `error::trailing_garbage` is returned rather than silently ignoring them. +5. **Length cap** — the preimage must not exceed `maxPreimageLength` (128 bytes); overlong preimages yield `error::preimage_too_long`. + +All error paths return `nullptr` via `std::error_code` rather than throwing exceptions, consistent with the rest of the rippled error-handling convention. + +## Fingerprint and Condition Derivation + +`fingerprint()` computes SHA-256 of the stored preimage using the OpenSSL-backed `sha256_hasher` from `xrpl/protocol/digest.h`. This hash is returned as a freshly allocated `Buffer` on every call. There is no caching, which is a minor performance consideration, but acceptable given the 128-byte cap and the relatively infrequent call pattern. + +`condition()` assembles a `Condition` value from the type tag, `cost()`, and `fingerprint()`. This is the object that gets embedded in a transaction to declare what must be revealed to unlock funds. + +## Cost and DoS Resistance + +`cost()` returns `payload_.size()` — the raw byte length of the preimage. This directly follows the RFC's definition of cost for this type, and it serves a deliberate anti-DoS purpose: more expensive fulfillments require the submitter to pay proportionally more resources. The `maxPreimageLength` ceiling of 128 bytes is a policy bound that can be raised in future versions but must never be lowered, as that would retroactively invalidate previously accepted fulfillments. The same monotonicity constraint applies to `Fulfillment::maxSerializedFulfillment` (256 bytes) and `Condition::maxSerializedCondition` (128 bytes) throughout the subsystem. + +## Message Irrelevance + +`validate(Slice)` unconditionally returns `true`. This is counterintuitive at first glance, but is correct by design: a `PreimageSha256` fulfillment is self-validating. There is no message to verify a signature over — the act of providing the correct preimage *is* the proof. Higher-level validation (matching the derived condition fingerprint against the on-chain condition) is handled by the free function `match(Fulfillment const&, Condition const&)` in `Fulfillment.h`, which compares `type`, `cost`, and `fingerprint` fields. The `validate` overload taking both a fulfillment and a condition calls `match` first, then `validate` on the fulfillment itself — for `PreimageSha256`, the `match` call does all the real work. \ No newline at end of file diff --git a/include/xrpl/conditions/detail/error.h.ai.json b/include/xrpl/conditions/detail/error.h.ai.json new file mode 100644 index 0000000000..1a942aa094 --- /dev/null +++ b/include/xrpl/conditions/detail/error.h.ai.json @@ -0,0 +1,41 @@ +{ + "args": [ + { + "lineno": 23, + "name": "ev" + } + ], + "classes": [ + { + "args": [], + "lineno": 32, + "name": "is_error_code_enum" + } + ], + "description": "Defines error codes and error handling utilities for the xrpl::cryptoconditions module, including an error enum and integration with std::error_code.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/conditions/detail/error.h", + "functions": [ + { + "args": [ + "ev" + ], + "lineno": 23, + "name": "make_error_code" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "cryptoconditions" + }, + { + "lineno": 30, + "name": "std" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/conditions/detail/error.h.ai.md b/include/xrpl/conditions/detail/error.h.ai.md new file mode 100644 index 0000000000..ccfd68e713 --- /dev/null +++ b/include/xrpl/conditions/detail/error.h.ai.md @@ -0,0 +1,19 @@ +# `include/xrpl/conditions/detail/error.h` + +This header defines the error vocabulary for the `xrpl::cryptoconditions` module, which implements the [Crypto-Conditions](https://tools.ietf.org/html/draft-thomas-crypto-conditions) specification used by XRPL's `EscrowFinish` transaction type. + +## Error Enum + +The `error` scoped enum establishes a typed set of failure codes that callers receive when parsing or validating a cryptocondition or its fulfillment. The values fall into three natural groups: + +- **Specification violations** — `unsupported_type`, `unsupported_subtype`, `unknown_type`, `unknown_subtype`, `fingerprint_size`, `incorrect_encoding`: the input doesn't conform to a recognized or supported condition type. +- **Buffer / parse failures** — `trailing_garbage`, `buffer_empty`, `buffer_overfull`, `buffer_underfull`, `malformed_encoding`, `short_preamble`, `unexpected_tag`: the raw byte buffer is structurally invalid. These map closely to the X.690 Distinguished Encoding Rules (DER) decoder in the sibling file `detail/utils.h`, which decodes the binary preamble and length fields that wrap condition data. +- **Implementation limits** — `long_tag`, `large_size`, `preimage_too_long`: the input is technically well-formed but exceeds what this implementation is willing to handle. + +The catch-all `generic = 1` exists as a fallback; starting the enum at `1` (rather than `0`) is intentional — `0` is reserved by the `std::error_code` convention to mean "no error." + +## `std::error_code` Integration + +The two additions beyond the enum itself — the `make_error_code()` declaration and the `std::is_error_code_enum` specialization — wire `error` into the standard C++ error system. Specializing `is_error_code_enum` to `true` allows values of the enum to implicitly convert to `std::error_code` wherever one is expected, so callers throughout the conditions subsystem can write idiomatic C++ like `return error::buffer_empty` from a function returning `std::error_code`. + +The actual `make_error_code()` implementation lives in `src/libxrpl/conditions/error.cpp`, where it constructs a `std::error_code` backed by a Meyers-singleton `cryptoconditions_error_category` — a custom `std::error_category` subclass whose `message()` method maps each enum value to a human-readable diagnostic string. Keeping the category as a function-local static guarantees initialization-order safety while remaining allocation-free after first use. \ No newline at end of file diff --git a/include/xrpl/conditions/detail/utils.h.ai.json b/include/xrpl/conditions/detail/utils.h.ai.json new file mode 100644 index 0000000000..37fa61a337 --- /dev/null +++ b/include/xrpl/conditions/detail/utils.h.ai.json @@ -0,0 +1,154 @@ +{ + "args": [ + { + "lineno": 27, + "name": "p" + }, + { + "lineno": 32, + "name": "p" + }, + { + "lineno": 37, + "name": "p" + }, + { + "lineno": 42, + "name": "p" + }, + { + "lineno": 47, + "name": "p" + }, + { + "lineno": 52, + "name": "p" + }, + { + "lineno": 57, + "name": "s" + }, + { + "lineno": 57, + "name": "ec" + }, + { + "lineno": 101, + "name": "s" + }, + { + "lineno": 101, + "name": "count" + }, + { + "lineno": 101, + "name": "ec" + }, + { + "lineno": 115, + "name": "s" + }, + { + "lineno": 115, + "name": "count" + }, + { + "lineno": 115, + "name": "ec" + } + ], + "classes": [ + { + "args": [], + "lineno": 21, + "name": "Preamble" + } + ], + "description": "This file provides a minimal Distinguished Encoding Rules (DER) decoder for binary blobs, specifically to support PreimageSha256 in the XRPL cryptoconditions module. It includes utilities for parsing DER preambles, octet strings, and integers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/conditions/detail/utils.h", + "functions": [ + { + "args": [ + "p" + ], + "lineno": 27, + "name": "isPrimitive" + }, + { + "args": [ + "p" + ], + "lineno": 32, + "name": "isConstructed" + }, + { + "args": [ + "p" + ], + "lineno": 37, + "name": "isUniversal" + }, + { + "args": [ + "p" + ], + "lineno": 42, + "name": "isApplication" + }, + { + "args": [ + "p" + ], + "lineno": 47, + "name": "isContextSpecific" + }, + { + "args": [ + "p" + ], + "lineno": 52, + "name": "isPrivate" + }, + { + "args": [ + "s", + "ec" + ], + "lineno": 57, + "name": "parsePreamble" + }, + { + "args": [ + "s", + "count", + "ec" + ], + "lineno": 101, + "name": "parseOctetString" + }, + { + "args": [ + "s", + "count", + "ec" + ], + "lineno": 115, + "name": "parseInteger" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "cryptoconditions" + }, + { + "lineno": 15, + "name": "der" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/conditions/detail/utils.h.ai.md b/include/xrpl/conditions/detail/utils.h.ai.md new file mode 100644 index 0000000000..f8aedb6934 --- /dev/null +++ b/include/xrpl/conditions/detail/utils.h.ai.md @@ -0,0 +1,33 @@ +# `include/xrpl/conditions/detail/utils.h` + +## Role and Purpose + +This header provides a minimal, purpose-built decoder for binary data encoded with X.690 Distinguished Encoding Rules (DER). It sits inside the `xrpl::cryptoconditions::der` sub-namespace and exists solely to support deserialization of XRPL crypto-conditions — in practice, exclusively the `PreimageSha256` fulfillment type. The comment in the file makes this explicit: only "the bare minimum needed to support PreimageSha256" is implemented. This deliberate scope limitation is the right call for a consensus-critical system: a full ASN.1/DER stack would introduce unnecessary attack surface. + +## DER Background + +DER encodes data as TLV (Type-Length-Value) triplets. The first byte is the *identifier octet*, whose top two bits encode the tag class (Universal, Application, Context-specific, Private), bit 5 encodes whether the value is primitive or constructed, and the lower five bits hold the tag number. This is followed by one or more *length octets* and then the value bytes. The `Preamble` struct directly maps to this layout: `type` holds the top three bits of the identifier (class + encoding flag), `tag` holds the lower five, and `length` holds the decoded content length. + +## Preamble Parsing + +`parsePreamble()` consumes bytes from a `Slice` by reference — the "cursor" pattern common throughout this module. After a successful call, the `Slice` has been advanced past the identifier and length octets, positioned exactly at the start of the value. This composability is the reason the output is a `Preamble` value rather than individual fields: the caller gets the decoded metadata and a correctly positioned cursor in one step, ready to call `parseOctetString` or `parseInteger` next. + +The function handles both short-form length (single byte, MSB clear) and long-form length (MSB set, lower 7 bits = count of subsequent length bytes). It rejects several malformed inputs via `std::error_code`: buffers shorter than two bytes (`error::short_preamble`), long-form tags (`error::long_tag`), indefinite-length encodings (long-form count of zero, `error::malformed_encoding`), length values that exceed `sizeof(std::size_t)` (`error::large_size`), and long-form lengths that encode zero (`error::malformed_encoding`). + +Long-form tags (tag number 0x1F in the identifier byte) are explicitly unsupported and flagged with `error::long_tag`. This is appropriate because the conditions RFC only uses low-numbered context-specific tags, so supporting multi-byte tag encoding would only matter for a general-purpose ASN.1 parser. + +## Tag Class Predicates + +Six inline predicates — `isPrimitive`, `isConstructed`, `isUniversal`, `isApplication`, `isContextSpecific`, `isPrivate` — decode the class and encoding bits of `p.type` via direct bitmask operations. They are intentionally zero-cost (all `inline`, taking `const&`) and exist primarily for readable validation logic. `PreimageSha256::deserialize()` uses `isPrimitive(p) && isContextSpecific(p)` to verify that a parsed preamble has the exact tag form required by the RFC, rejecting anything else with `error::incorrect_encoding`. + +## `parseOctetString` + +This function copies `count` bytes from the front of a `Slice` into a heap-allocated `Buffer` and advances the slice. The 65535-byte hard cap is a second line of defense: `PreimageSha256` itself caps preimages at 128 bytes (`maxPreimageLength`), but `parseOctetString` refuses anything over 64 KiB regardless of caller intent. This guards against a malformed length field producing an allocation of an arbitrary size. + +## `parseInteger` + +The template `parseInteger()` reads `count` bytes into any integer type, respecting DER's two's complement encoding for signed types. The design handles a subtle but important DER convention: because integers are encoded in two's complement, an unsigned value whose high bit would be set must be prefixed with a `0x00` byte to distinguish it from a negative number. The function therefore permits `count == sizeof(Integer) + 1` for unsigned types, but only when the leading byte is zero; a nonzero leading byte with that length is `error::malformed_encoding`. Attempting to decode a negative (high-bit-set) DER integer into an unsigned C++ type also triggers `error::malformed_encoding`. Sign extension for signed types is handled manually: after reading the raw bytes, if the value is negative (high bit of the first byte set) and `count < sizeof(Integer)`, the upper bytes are filled with `0xFF`. + +## Error Handling Philosophy + +All three parsing functions take a `std::error_code&` output parameter rather than throwing exceptions. When any check fails, they set `ec` and return a zero-initialized or empty result immediately. Callers are expected to check `if (ec)` after each call, as seen in `PreimageSha256::deserialize()`. This approach is consistent with the rest of the XRPL codebase and avoids exception overhead in a hot path that runs on every ledger transaction involving crypto-conditions. \ No newline at end of file diff --git a/include/xrpl/core/ClosureCounter.h.ai.json b/include/xrpl/core/ClosureCounter.h.ai.json new file mode 100644 index 0000000000..a176e068c2 --- /dev/null +++ b/include/xrpl/core/ClosureCounter.h.ai.json @@ -0,0 +1,138 @@ +{ + "args": [ + { + "lineno": 19, + "name": "Ret_t" + }, + { + "lineno": 19, + "name": "Args_t" + }, + { + "lineno": 51, + "name": "Closure" + } + ], + "classes": [ + { + "args": [ + "Ret_t", + "Args_t..." + ], + "lineno": 19, + "name": "ClosureCounter" + }, + { + "args": [ + "Closure" + ], + "lineno": 51, + "name": "Substitute" + } + ], + "description": "Implements a template class ClosureCounter to assist in managing and waiting for the completion of asynchronous closures/callbacks, supporting safe shutdown and join semantics.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/ClosureCounter.h", + "functions": [ + { + "args": [], + "lineno": 27, + "name": "operator++" + }, + { + "args": [], + "lineno": 34, + "name": "operator--" + }, + { + "args": [ + "Substitute const& rhs" + ], + "lineno": 59, + "name": "Substitute" + }, + { + "args": [ + "Substitute&& rhs" + ], + "lineno": 63, + "name": "Substitute" + }, + { + "args": [ + "ClosureCounter& counter", + "Closure&& closure" + ], + "lineno": 68, + "name": "Substitute" + }, + { + "args": [], + "lineno": 76, + "name": "~Substitute" + }, + { + "args": [ + "Args_t... args" + ], + "lineno": 82, + "name": "operator()" + }, + { + "args": [], + "lineno": 89, + "name": "ClosureCounter" + }, + { + "args": [ + "ClosureCounter const&" + ], + "lineno": 91, + "name": "ClosureCounter" + }, + { + "args": [ + "ClosureCounter const&" + ], + "lineno": 93, + "name": "operator=" + }, + { + "args": [], + "lineno": 96, + "name": "~ClosureCounter" + }, + { + "args": [ + "char const* name", + "std::chrono::milliseconds wait", + "beast::Journal j" + ], + "lineno": 104, + "name": "join" + }, + { + "args": [ + "Closure&& closure" + ], + "lineno": 120, + "name": "wrap" + }, + { + "args": [], + "lineno": 132, + "name": "count" + }, + { + "args": [], + "lineno": 141, + "name": "joined" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/ClosureCounter.h.ai.md b/include/xrpl/core/ClosureCounter.h.ai.md new file mode 100644 index 0000000000..1e5a616b25 --- /dev/null +++ b/include/xrpl/core/ClosureCounter.h.ai.md @@ -0,0 +1,31 @@ +# `ClosureCounter.h` — Async Callback Lifecycle Management for Safe Shutdown + +## Role in the System + +`ClosureCounter` solves a specific and tricky shutdown problem: when a component is torn down, it may have outstanding asynchronous callbacks (timers, I/O handlers) that haven't fired yet and hold references to objects that are about to be destroyed. The component needs a way to either wait for those callbacks to complete or, in the join phase, signal that new callbacks should not be registered while draining the remaining ones. + +The pattern is most visible in `NetworkOPs`, which uses a `ClosureCounter` for its Boost.Asio timer handlers. When `NetworkOPs` shuts down it calls `waitHandlerCounter_.join(...)`, which blocks until every timer callback copy has been destroyed. New timer registrations that check `waitHandlerCounter_.wrap(...)` and receive `std::nullopt` know to cancel themselves immediately. + +## Two-Phase Lifecycle + +The design is intentionally linear and irreversible. During the **fork phase**, callers register callbacks by passing them to `wrap()`, receiving a `std::optional>` in return. So long as `join()` has not been called, the optional will contain a live `Substitute` and the internal counter will have been incremented. Once a `Substitute` is destroyed — even if it was copied multiple times — it decrements the counter once per live copy. + +Calling `join()` initiates the **join phase**. From that point on, every `wrap()` call returns `std::nullopt`, preventing new callbacks from entering the system. `join()` then blocks on a condition variable until the closure count reaches zero. Because this transition is one-way (there is no "unjoin"), `ClosureCounter` is not copyable or movable — outstanding counts tied to a specific instance would be impossible to reconcile across a move. + +## The `Substitute` Inner Class + +`Substitute` is the actual counted handle returned by `wrap()`. It holds a reference to its parent `ClosureCounter` and a copy of the wrapped closure. Every constructor — copy, move, and the primary construction from `ClosureCounter::wrap()` — calls `++counter_`, and the destructor calls `--counter_`. This means every live copy of a `Substitute` contributes exactly one to the counter, which is critical because Boost.Asio sometimes copies handlers internally before dispatching them. + +Assignment operators on `Substitute` are explicitly deleted. Allowing assignment would require atomically decrementing the old counter, potentially notifying if it reached zero, and incrementing the new one — a sequence that would be error-prone and is simply not needed in practice. + +`Substitute::operator()` has an important subtlety noted in the source: because `Args_t` is not deduced at the call site (it is fixed by the outer class template), the parameter pack `Args_t... args` does not undergo reference collapsing. The implementation uses `std::forward(args)...` to forward exactly the value categories the user declared in the template parameters, preserving move semantics for rvalue arguments while correctly passing lvalue references through. + +A `static_assert` inside `Substitute` verifies at compile time that the wrapped closure's signature is compatible with the declared `Ret_t` and `Args_t...`, giving a readable error rather than an obscure template instantiation failure. + +## Concurrency Design + +`closureCount_` is `std::atomic`, which allows `++` in the constructors to proceed without taking the mutex. The decrement in `operator--`, however, is performed under `mutex_` despite the atomicity of the variable itself. The comment explains why: if the decrement to zero happens between the time the waiting thread's `wait_for` predicate evaluates to `true` and when the thread re-acquires the lock (the classic spurious-wakeup window), the notified thread could observe `closureCount_ == 0` before it checks the predicate. By making the decrement and the `notify_all()` both occur under `mutex_`, the code ensures that any thread already inside `wait_for` will see the final zero once it re-evaluates the predicate under the lock. + +`join()` itself uses a two-stage wait. It first calls `wait_for` with a configurable timeout, and if that times out it logs a warning via `beast::Journal` and then falls back to an unconditional `wait`. This avoids hiding indefinite hangs silently while still eventually completing shutdown, which is important for distinguishing "slow but legitimate" shutdown from a genuine deadlock. + +The destructor calls `join("ClosureCounter", 1s, debugLog())` as a safety net, ensuring that any `ClosureCounter` that goes out of scope without an explicit `join()` still waits for outstanding closures to drain — preventing use-after-free on whatever resources the closures may reference. \ No newline at end of file diff --git a/include/xrpl/core/Coro.ipp.ai.json b/include/xrpl/core/Coro.ipp.ai.json new file mode 100644 index 0000000000..fb8064ab1e --- /dev/null +++ b/include/xrpl/core/Coro.ipp.ai.json @@ -0,0 +1,369 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "JobQueue::Coro::~Coro" + ], + "entry_point": "JobQueue::Coro::~Coro", + "purpose": "Destructor for Coro, ensures coroutine has finished before destruction.", + "validation_points": [ + "XRPL_ASSERT(finished_, ...) (debug only) - validates that the coroutine is finished before destruction" + ] + }, + { + "call_chain": [ + "JobQueue::Coro::resume", + "if (coro_) { coro_(); }" + ], + "entry_point": "JobQueue::Coro::resume", + "purpose": "Resumes the coroutine if it is still valid (not completed).", + "validation_points": [ + "if (coro_) - validates that the coroutine object is still valid before resuming" + ] + }, + { + "call_chain": [ + "JobQueue::Coro::expectEarlyExit" + ], + "entry_point": "JobQueue::Coro::expectEarlyExit", + "purpose": "Handles early exit from coroutine, ensures state is updated and nSuspend decremented.", + "validation_points": [ + "if (!finished_) (debug only) - validates that the coroutine is not already finished before marking as finished" + ] + }, + { + "call_chain": [ + "JobQueue::Coro::post", + "jq_.addJob(..., [this, sp = shared_from_this()]() { resume(); })", + "JobQueue::Coro::resume" + ], + "entry_point": "JobQueue::Coro::post", + "purpose": "Schedules the coroutine to be resumed by the job queue.", + "validation_points": [ + "resume() -> if (coro_) - validates coroutine validity before resuming" + ] + } + ], + "data_flows": [ + { + "field": "finished_", + "flow": [ + "Coro constructor (implicitly false)", + "Set to true at end of coroutine lambda (after fn(shared_from_this()))", + "Set to true in expectEarlyExit() if not already finished" + ], + "origin": "Set to false by default; set to true at end of coroutine lambda and in expectEarlyExit() (debug only)", + "transformations": [ + "Set to true when coroutine completes or early exit is expected" + ], + "validated_at": "XRPL_ASSERT in ~Coro (debug only); if (!finished_) in expectEarlyExit() (debug only)" + }, + { + "field": "coro_", + "flow": [ + "Coro constructor", + "Used in resume() to check validity before calling coro_()", + "Used in runnable() to check if coroutine is still valid" + ], + "origin": "Initialized in Coro constructor with boost::coroutines2::pull_type", + "transformations": [ + "May become invalid (empty) when coroutine completes" + ], + "validated_at": "if (coro_) in resume() and runnable()" + }, + { + "field": "running_", + "flow": [ + "Set to true in post() before scheduling job", + "Set to true in resume() before resuming coroutine", + "Set to false in resume() after coroutine resumes", + "Set to false in post() if job scheduling fails" + ], + "origin": "Set to true in post() and resume(), set to false after resume() completes or if post() fails", + "transformations": [ + "Tracks whether coroutine is currently running" + ], + "validated_at": "Used in join() to wait until running_ == false" + }, + { + "field": "yield_", + "flow": [ + "Set in coroutine lambda at start", + "Used in yield() to suspend coroutine" + ], + "origin": "Set in coroutine lambda to point to do_yield", + "transformations": [ + "Pointer assignment" + ], + "validated_at": "Not explicitly validated; assumed valid after construction" + }, + { + "field": "jq_.nSuspend_", + "flow": [ + "Incremented in yield()", + "Decremented in resume() and expectEarlyExit()" + ], + "origin": "JobQueue member, incremented/decremented to track suspended coroutines", + "transformations": [ + "Increment/decrement" + ], + "validated_at": "Not explicitly validated here" + } + ], + "description": "Implements the JobQueue::Coro coroutine class for managing asynchronous job execution and suspension/resumption in the XRPL job queue system.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "coro_ (by boost::coroutines2 type system)", + "validation", + "missing", + "check" + ], + "evidence": "Field coro_ (by boost::coroutines2 type system) validated by XRPL_ASSERT macro (custom assertion), C++ type system, boost::coroutines2", + "issue_pattern": "Missing validation for coro_ (by boost::coroutines2 type system)", + "why_false_positive": "XRPL_ASSERT macro (custom assertion), C++ type system, boost::coroutines2 validates coro_ (by boost::coroutines2 type system) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "type_ (by C++ enum type)", + "validation", + "missing", + "check" + ], + "evidence": "Field type_ (by C++ enum type) validated by XRPL_ASSERT macro (custom assertion), C++ type system, boost::coroutines2", + "issue_pattern": "Missing validation for type_ (by C++ enum type)", + "why_false_positive": "XRPL_ASSERT macro (custom assertion), C++ type system, boost::coroutines2 validates type_ (by C++ enum type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "name_ (by std::string type)", + "validation", + "missing", + "check" + ], + "evidence": "Field name_ (by std::string type) validated by XRPL_ASSERT macro (custom assertion), C++ type system, boost::coroutines2", + "issue_pattern": "Missing validation for name_ (by std::string type)", + "why_false_positive": "XRPL_ASSERT macro (custom assertion), C++ type system, boost::coroutines2 validates name_ (by std::string type) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "finished_ (Coro completion state)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at JobQueue::Coro::~Coro (destructor)", + "issue_pattern": "Missing empty string validation for finished_ (Coro completion state)", + "why_false_positive": "XRPL_ASSERT macro validates finished_ (Coro completion state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "coro_ (coroutine object validity)", + "empty", + "string", + "validation" + ], + "evidence": "if (coro_) check at JobQueue::Coro::resume", + "issue_pattern": "Missing empty string validation for coro_ (coroutine object validity)", + "why_false_positive": "if (coro_) check validates coro_ (coroutine object validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "finished_ (Coro completion state)", + "empty", + "string", + "validation" + ], + "evidence": "if (!finished_) check (debug only) at JobQueue::Coro::expectEarlyExit", + "issue_pattern": "Missing empty string validation for finished_ (Coro completion state)", + "why_false_positive": "if (!finished_) check (debug only) validates finished_ (Coro completion state) for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/Coro.ipp", + "functions": [ + { + "args": [ + "Coro_create_t", + "JobQueue& jq", + "JobType type", + "std::string const& name", + "F&& f" + ], + "lineno": 5, + "name": "JobQueue::Coro::Coro" + }, + { + "args": [], + "lineno": 22, + "name": "JobQueue::Coro::~Coro" + }, + { + "args": [], + "lineno": 29, + "name": "JobQueue::Coro::yield" + }, + { + "args": [], + "lineno": 37, + "name": "JobQueue::Coro::post" + }, + { + "args": [], + "lineno": 53, + "name": "JobQueue::Coro::resume" + }, + { + "args": [], + "lineno": 77, + "name": "JobQueue::Coro::runnable" + }, + { + "args": [], + "lineno": 81, + "name": "JobQueue::Coro::expectEarlyExit" + }, + { + "args": [], + "lineno": 97, + "name": "JobQueue::Coro::join" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is low-level coroutine infrastructure. Direct unit tests for Coro are unlikely; instead, coverage is expected via higher-level JobQueue and job/coroutine scheduling tests. Validation code (XRPL_ASSERT, debug checks) is only active in debug builds, so release builds do not check finished_ state. Test files likely to cover this indirectly include those for JobQueue, job scheduling, and possibly integration tests for asynchronous job handling. Gaps: No direct tests for validation logic (e.g., destructor assertion, expectEarlyExit), and no explicit tests for race conditions or invalid coroutine state.", + "validation_architecture": { + "auto_validated_fields": [ + "coro_ (by boost::coroutines2 type system)", + "type_ (by C++ enum type)", + "name_ (by std::string type)" + ], + "framework": "XRPL_ASSERT macro (custom assertion), C++ type system, boost::coroutines2", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts in debug builds)", + "field": "finished_ (Coro completion state)", + "location": "JobQueue::Coro::~Coro (destructor)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures coroutine has finished before destruction" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (skips execution if invalid)", + "field": "coro_ (coroutine object validity)", + "location": "JobQueue::Coro::resume", + "validated_by": "if (coro_) check", + "validates": [ + "Checks that coroutine is still valid before resuming" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "None (debug-only check, no error thrown)", + "field": "finished_ (Coro completion state)", + "location": "JobQueue::Coro::expectEarlyExit", + "validated_by": "if (!finished_) check (debug only)", + "validates": [ + "Checks if coroutine has finished before early exit logic" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/Coro.ipp.ai.md b/include/xrpl/core/Coro.ipp.ai.md new file mode 100644 index 0000000000..3a9a92634f --- /dev/null +++ b/include/xrpl/core/Coro.ipp.ai.md @@ -0,0 +1,60 @@ +# `JobQueue::Coro` — Coroutine Lifecycle Implementation (`Coro.ipp`) + +## Role in the System + +`Coro.ipp` provides the method bodies for `JobQueue::Coro`, the coroutine abstraction that allows XRPL server tasks — most notably RPC handlers — to suspend mid-execution, release their worker thread back to the pool, and resume later when an awaited event (such as a pathfinding result) arrives. It is an implementation-only `.ipp` file, `#include`d at the bottom of `JobQueue.h` after the class declaration, following the common XRPL pattern of separating templated or inline bodies into a companion file. + +The underlying mechanism is `boost::coroutines2`, which stores and restores a full call stack. This gives callers an imperative, synchronous-looking API: they call `yield()` to pause and `post()` to schedule resumption, without callbacks or state machines. + +## Construction: The First Yield is Mandatory + +The constructor is the most subtle part of the design. When `boost::coroutines2::coroutine::pull_type` is constructed, boost **immediately** transfers execution into the coroutine body before the constructor returns. This would be a problem if the calling thread (e.g., a network handler thread) were left spinning inside the coroutine. + +The lambda passed to the `pull_type` therefore starts with an unconditional `yield()` call: + +```cpp +yield_ = &do_yield; // capture the push_type handle for later calls +yield(); // immediately suspend — give control back to constructor +fn(shared_from_this()); // user code only runs later, on a worker thread +``` + +This bootstrap yield guarantees the constructor returns to the caller with the coroutine parked, ready to be dispatched via `post()`. Only after `post()` queues a job and that job is dispatched by a worker thread does `resume()` run, which re-enters the coroutine and falls through to `fn(shared_from_this())` — the user's code. + +A 1.5 MB stack size is requested via `boost::context::protected_fixedsize_stack`. The comment explains why: the default 1 MB was insufficient for deeply nested XRPL processing paths, which ASAN tests exposed. The extra headroom prevents stack overflow in production. + +## Yield and Resume: The Dual-Mutex Design + +`Coro` uses two mutexes for distinct purposes: + +- **`mutex_`** serializes access to the `coro_` object itself. `resume()` holds this lock for the entire duration the coroutine is executing. This is the key mechanism that prevents the documented post-before-yield race (described in detail in `JobQueue.h` lines 354–380). +- **`mutex_run_`** guards `running_` and is used by `join()` to wait until the coroutine finishes a scheduled execution slice. + +The race condition `mutex_` prevents is worth understanding explicitly: a coroutine may call `post()` (scheduling its own resumption) and then call `yield()`, but in a concurrent system the scheduled job can start and call `resume()` before `yield()` has executed on the original thread. Since `resume()` holds `mutex_` for the full run of the coroutine body and `yield()` releases control from within that same execution, any competing `resume()` that tries to re-enter is blocked at the lock until the coroutine has actually yielded (and `mutex_` is released). This transforms a potential double-execution bug into a harmless serialized wait. + +`resume()` also checks `if (coro_)` before calling `coro_()`. Once a `boost::coroutines2` pull_type's user function returns, the object converts to `false`. Calling `operator()` on a completed coroutine is undefined behavior, so this guard handles the case where the late-arriving resume job finds the coroutine has already run to completion. + +## Coroutine-Local Storage: Thread Identity Swapping + +`resume()` contains a pattern that deserves attention: + +```cpp +auto saved = detail::getLocalValues().release(); +detail::getLocalValues().reset(&lvs_); +// ... run coroutine ... +detail::getLocalValues().release(); +detail::getLocalValues().reset(saved); +``` + +`detail::getLocalValues()` is a `boost::thread_specific_ptr` — a per-thread slot. Because multiple coroutines can share a worker thread (each taking a turn), XRPL needs per-coroutine "thread-local" state, not per-thread state. The solution is to swap the thread's current `LocalValues` pointer with the coroutine's own `lvs_` before entering the coroutine body and restore the original pointer after the coroutine yields or returns. `LocalValue` objects throughout the codebase transparently read from whichever `LocalValues` is currently installed on the thread. + +## `nSuspend_`: Tracking the Drain Condition + +`jq_.nSuspend_` is a counter on the parent `JobQueue` that tracks how many coroutines are currently suspended (i.e., have called `yield()` but have not yet been resumed). `yield()` increments it under the queue's own mutex; `resume()` decrements it. The queue's shutdown logic uses this counter alongside the running-job count to know when it is fully quiescent and can stop its worker threads. + +`expectEarlyExit()` handles the case where a `Coro` is abandoned before it ever re-enters its user function — for example when `postCoro()` finds that `post()` fails (queue is stopping). The coroutine was never run past its bootstrap yield, so `nSuspend_` was incremented but will never be decremented by a `resume()`. `expectEarlyExit()` decrements it manually and sets `finished_` in debug builds to suppress the destructor assertion. + +## Lifecycle Invariants + +The debug-only `finished_` flag enforces a critical invariant: the `Coro` must not be destroyed while still suspended or mid-execution. The `XRPL_ASSERT` in `~Coro()` fires in debug builds if someone drops the last `shared_ptr` reference to a `Coro` that hasn't fully run to completion. Because `Coro` inherits from `std::enable_shared_from_this`, the `post()` lambda captures `shared_from_this()` to extend the coroutine's lifetime until the job runs, preventing premature destruction even if all external handles are dropped. + +`runnable()` simply checks whether `coro_` is still truthy — a thin predicate used by higher-level code to decide whether a `Coro` can still be dispatched, avoiding redundant `post()` calls on a completed coroutine. \ No newline at end of file diff --git a/include/xrpl/core/HashRouter.h.ai.json b/include/xrpl/core/HashRouter.h.ai.json new file mode 100644 index 0000000000..c0c2425f17 --- /dev/null +++ b/include/xrpl/core/HashRouter.h.ai.json @@ -0,0 +1,314 @@ +{ + "args": [ + { + "lineno": 23, + "name": "lhs" + }, + { + "lineno": 23, + "name": "rhs" + }, + { + "lineno": 29, + "name": "lhs" + }, + { + "lineno": 29, + "name": "rhs" + }, + { + "lineno": 35, + "name": "lhs" + }, + { + "lineno": 35, + "name": "rhs" + }, + { + "lineno": 41, + "name": "lhs" + }, + { + "lineno": 41, + "name": "rhs" + }, + { + "lineno": 47, + "name": "flags" + }, + { + "lineno": 71, + "name": "peer" + }, + { + "lineno": 81, + "name": "flagsToSet" + }, + { + "lineno": 98, + "name": "now" + }, + { + "lineno": 98, + "name": "relayTime" + }, + { + "lineno": 106, + "name": "now" + }, + { + "lineno": 106, + "name": "interval" + }, + { + "lineno": 132, + "name": "key" + }, + { + "lineno": 134, + "name": "key" + }, + { + "lineno": 134, + "name": "peer" + }, + { + "lineno": 139, + "name": "key" + }, + { + "lineno": 139, + "name": "peer" + }, + { + "lineno": 144, + "name": "key" + }, + { + "lineno": 144, + "name": "peer" + }, + { + "lineno": 144, + "name": "flags" + }, + { + "lineno": 148, + "name": "key" + }, + { + "lineno": 148, + "name": "peer" + }, + { + "lineno": 148, + "name": "flags" + }, + { + "lineno": 148, + "name": "tx_interval" + }, + { + "lineno": 157, + "name": "key" + }, + { + "lineno": 157, + "name": "flags" + }, + { + "lineno": 162, + "name": "key" + }, + { + "lineno": 170, + "name": "key" + } + ], + "classes": [ + { + "args": [ + "Setup const& setup", + "Stopwatch& clock" + ], + "lineno": 54, + "name": "HashRouter" + }, + { + "args": [], + "lineno": 62, + "name": "Setup" + }, + { + "args": [], + "lineno": 70, + "name": "Entry" + } + ], + "description": "Implements a HashRouter class for tracking which hashes have been received by which peers, managing message routing and suppression in a peer-to-peer overlay network. Includes flag management, relay timing, and peer tracking.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/HashRouter.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 23, + "name": "operator|" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 29, + "name": "operator|=" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 35, + "name": "operator&" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 41, + "name": "operator&=" + }, + { + "args": [ + "flags" + ], + "lineno": 47, + "name": "any" + }, + { + "args": [ + "peer" + ], + "lineno": 71, + "name": "addPeer" + }, + { + "args": [], + "lineno": 76, + "name": "getFlags" + }, + { + "args": [ + "flagsToSet" + ], + "lineno": 81, + "name": "setFlags" + }, + { + "args": [], + "lineno": 86, + "name": "releasePeerSet" + }, + { + "args": [], + "lineno": 91, + "name": "relayed" + }, + { + "args": [ + "now", + "relayTime" + ], + "lineno": 98, + "name": "shouldRelay" + }, + { + "args": [ + "now", + "interval" + ], + "lineno": 106, + "name": "shouldProcess" + }, + { + "args": [ + "key" + ], + "lineno": 132, + "name": "addSuppression" + }, + { + "args": [ + "key", + "peer" + ], + "lineno": 134, + "name": "addSuppressionPeer" + }, + { + "args": [ + "key", + "peer" + ], + "lineno": 139, + "name": "addSuppressionPeerWithStatus" + }, + { + "args": [ + "key", + "peer", + "flags" + ], + "lineno": 144, + "name": "addSuppressionPeer" + }, + { + "args": [ + "key", + "peer", + "flags", + "tx_interval" + ], + "lineno": 148, + "name": "shouldProcess" + }, + { + "args": [ + "key", + "flags" + ], + "lineno": 157, + "name": "setFlags" + }, + { + "args": [ + "key" + ], + "lineno": 162, + "name": "getFlags" + }, + { + "args": [ + "key" + ], + "lineno": 170, + "name": "shouldRelay" + }, + { + "args": [ + "uint256 const&" + ], + "lineno": 177, + "name": "emplace" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/HashRouter.h.ai.md b/include/xrpl/core/HashRouter.h.ai.md new file mode 100644 index 0000000000..bbc0a8e519 --- /dev/null +++ b/include/xrpl/core/HashRouter.h.ai.md @@ -0,0 +1,45 @@ +# `HashRouter.h` — Peer-Message Deduplication and Relay Control + +`HashRouter` solves a fundamental problem in any gossip-style P2P network: when many peers send the same message (identified by its hash), the node must decide whether to process it again, re-broadcast it, and to whom. This file defines the routing table that enforces those decisions in the XRPL overlay. + +## Role in the System + +The hash router sits at the intersection of the peer message handler (`PeerImp`) and the transaction-validity engine (`apply.cpp`). Every incoming transaction, proposal, or validation is fingerprinted by hash and registered here. The router answers three distinct questions: + +1. **Have I seen this hash before, and from which peer?** (`addSuppression*` family) +2. **Should I re-broadcast this item right now, and to whom?** (`shouldRelay`) +3. **Should I process this item right now?** (`shouldProcess`) + +## Data Structures + +The backbone is `suppressionMap_`, a `beast::aged_unordered_map` keyed on `uint256` (the message hash). The map uses a `Stopwatch` clock, which lets the real clock be substituted in tests. Each value is a private `Entry` holding four things: a `HashRouterFlags` bitfield, a `std::set` of peers that sent this hash, and two `std::optional` timestamps — one for the last relay event, one for the last processing event. + +Expiration is lazy. `emplace()` — the private bottleneck method called by every public API — first looks up the key. If found, it *touches* the entry (resetting its TTL in the aged map) and returns the existing entry. If not found, it calls `expire()` on the map to evict all entries older than `holdTime` (default 300 seconds), then inserts a fresh entry. Touching on every access means frequently-seen hashes live longer than rarely-seen ones, which naturally keeps hot messages in memory while cold ones age out. + +## Flag Design: Public vs. Private + +`HashRouterFlags` is a strongly-typed `uint16_t` bitfield split into two tiers. The public flags (`BAD`, `SAVED`, `HELD`, `TRUSTED`) carry semantic meaning used widely across the application. The private flags (`PRIVATE1`–`PRIVATE6`) are opaque at the header level but are given concrete meanings by individual subsystems. In `apply.cpp` they are aliased as `SF_SIGBAD`, `SF_SIGGOOD`, `SF_LOCALBAD`, and `SF_LOCALGOOD`, acting as a per-transaction signature verification cache: once the router records that a transaction's signature was checked, subsequent encounters skip the expensive cryptographic work. This pattern avoids coupling the router to transaction semantics while still providing a shared cache reachable from any code path that holds a `HashRouter` reference. + +The full set of bitwise operators (`|`, `|=`, `&`, `&=`) and the `any()` predicate are defined as `constexpr` free functions rather than member operators — a deliberate choice that keeps the enum scoped and type-safe while still supporting natural flag composition. + +## Relay Suppression + +`shouldRelay(uint256 const& key)` returns `std::optional>`. The unseated optional signals "do not relay at all" — the item was broadcast recently (within `relayTime`, default 30 seconds). The seated optional contains the exclusion set: all peers that already sent us this hash. The broadcaster can skip those peers, avoiding redundant sends back to the originators. Crucially, each call to `shouldRelay` that grants permission also atomically moves the peer set out of the entry via `releasePeerSet()`, resetting it for the next relay window. New peers that send the same hash after the relay will accumulate in the fresh set, so the next relay window will again exclude the right set. + +The `addSuppressionPeerWithStatus` variant adds a peer and simultaneously returns whether the entry was newly created and what the last relay timestamp was. `PeerImp` uses this for the reduce-relay mechanism: if a duplicate proposal or validation arrives within `reduce_relay::IDLED` seconds of the last relay, the slot/squelch system is updated — letting the network reduce redundant re-broadcasts from overly verbose peers. + +## Processing Rate-Limiting + +`shouldProcess` enforces a separate time gate on transaction application, distinct from relay. In `PeerImp`, the interval is hard-coded to 10 seconds: if the same transaction arrives again within that window, the router blocks re-processing and lets the caller check the flags to decide whether to penalize the peer (e.g., `BAD` flag means the peer is sending known-bad transactions, warranting a resource fee). The entry's `processed_` timestamp is set on first call and checked on subsequent ones. + +## Concurrency Model + +A single `mutable std::mutex` serializes all operations. This coarse-grained approach is intentional: `emplace()` itself is not thread-safe (it calls `expire()` and modifies the map), and entries are returned by reference, so callers holding the lock through the public API must not be interrupted. The tradeoff is that all peers sharing the same application object contend on one lock. Given that `HashRouter` operations are short — a hash lookup plus a few flag checks — this is acceptable. The virtual destructor on `HashRouter` allows future subclassing, though none exists in the current codebase. + +## Configuration Constraints + +`setup_HashRouter.cpp` enforces invariants on the configurable parameters: `holdTime ≥ 12s` (roughly three ledger closes, ensuring an entry outlives a single consensus round), `relayTime ≥ 8s` (two ledger closes), and `relayTime ≤ holdTime` (an entry must outlive the relay window). Violating any of these throws at startup. The defaults (300s hold, 30s relay) are described as requiring network-wide coordination to change, since they affect how many duplicate messages nodes will suppress across the entire network. + +## Naming Note + +The "suppression" terminology throughout the API (`addSuppression`, `suppressionMap_`) is an acknowledged historical artifact — a `VFALCO TODO` comment in the header flags it for renaming to something more semantically accurate. The router doesn't suppress messages in a filtering sense; it tracks them to decide routing and processing eligibility. \ No newline at end of file diff --git a/include/xrpl/core/Job.h.ai.json b/include/xrpl/core/Job.h.ai.json new file mode 100644 index 0000000000..8e7b706c94 --- /dev/null +++ b/include/xrpl/core/Job.h.ai.json @@ -0,0 +1,113 @@ +{ + "args": [ + { + "lineno": 59, + "name": "type" + }, + { + "lineno": 59, + "name": "index" + }, + { + "lineno": 62, + "name": "name" + }, + { + "lineno": 62, + "name": "lm" + }, + { + "lineno": 62, + "name": "job" + }, + { + "lineno": 79, + "name": "j" + } + ], + "classes": [ + { + "args": [], + "lineno": 51, + "name": "Job" + } + ], + "description": "Defines the JobType enum and Job class for managing and prioritizing CPU-bound jobs (primarily signature checking) in the XRPL system, including job metadata and execution logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/Job.h", + "functions": [ + { + "args": [], + "lineno": 54, + "name": "Job" + }, + { + "args": [ + "JobType type", + "std::uint64_t index" + ], + "lineno": 59, + "name": "Job" + }, + { + "args": [ + "JobType type", + "std::string const& name", + "std::uint64_t index", + "LoadMonitor& lm", + "std::function const& job" + ], + "lineno": 62, + "name": "Job" + }, + { + "args": [], + "lineno": 68, + "name": "getType" + }, + { + "args": [], + "lineno": 71, + "name": "queue_time" + }, + { + "args": [], + "lineno": 75, + "name": "doJob" + }, + { + "args": [ + "Job const& j" + ], + "lineno": 79, + "name": "operator<" + }, + { + "args": [ + "Job const& j" + ], + "lineno": 81, + "name": "operator>" + }, + { + "args": [ + "Job const& j" + ], + "lineno": 83, + "name": "operator<=" + }, + { + "args": [ + "Job const& j" + ], + "lineno": 85, + "name": "operator>=" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/Job.h.ai.md b/include/xrpl/core/Job.h.ai.md new file mode 100644 index 0000000000..212fc73a8e --- /dev/null +++ b/include/xrpl/core/Job.h.ai.md @@ -0,0 +1,42 @@ +# `include/xrpl/core/Job.h` — Job Abstraction for the XRPL Thread Pool + +This header defines the unit of work dispatched through XRPL's internal job queue: the `JobType` priority enumeration and the `Job` class that wraps a callable alongside the metadata the scheduler needs to order and measure it. + +## The Priority Scheme: Enum Ordering as First-Class Design + +The `JobType` enum is not a label — it is the priority table. The comment is direct: "the position in this enum indicates the job priority with earlier jobs having lower priority than later jobs." Adding a new job type at a specific priority level means inserting it at the right position in the list. There is no secondary priority field, no weight table, no configuration file. The enum value _is_ the priority. + +This makes the priority system visible, auditable, and diff-friendly: you can see the relative importance of every job type in a single glance. `jtPACK` (making fetch packs for peers) has the lowest dispatched priority, while `jtADMIN` sits at the top of the dispatchable tier. + +The enum also distinguishes two groups. The first group (`jtPACK` through `jtADMIN`) is dispatched by the job pool. The second group (`jtPEER`, `jtDISK`, `jtTXN_PROC`, etc.) are "special job types which are not dispatched by the job pool" — they are used only for load-monitoring purposes on work happening outside the queue. + +## Comparison Operators: Sorted Highest-Priority First + +The `operator<` and friends implement a deliberate inversion. Reading `Job.cpp`, `operator<` returns `true` when `this` should sort _after_ `j` — meaning a lower-priority job compares as "less than" a higher-priority job. When the `JobQueue` holds jobs in an ordered container such as `std::set`, the job at the "greatest" position is processed first, which is the highest-priority type. + +Within a type tier, jobs break ties with `mJobIndex`, a monotonically increasing counter assigned at construction. A lower index means the job was enqueued earlier, so it wins the tie: FIFO ordering within a priority class. + +## The `Job` Lifecycle and Timing Measurement + +The full constructor takes a `LoadMonitor&` and immediately creates a `shared_ptr` in the "not started" state (`shouldStart = false`). At this point `m_queue_time` is captured via `clock_type::now()`. The `LoadEvent` begins counting wait time from creation — the period between enqueue and execution. + +When `doJob()` is eventually called on a worker thread, it: + +1. Renames the current thread to `"j:" + mName` for debuggability. +2. Calls `m_loadEvent->start()`, which transitions the `LoadEvent` from waiting to running state, recording the queue-wait duration in `timeWaiting_`. +3. Invokes the stored `std::function`. +4. Explicitly sets `mJob = nullptr` — destroying the lambda and all its captures — _before_ returning. + +The explicit nullification on step 4 is the non-obvious detail. The `LoadEvent` reports its full timing to `LoadMonitor` when destroyed (via its destructor). By destroying the lambda before `doJob()` returns (and before the `Job` object and its `m_loadEvent` are eventually cleaned up), any time spent destructing captured state is folded into the measured job duration. Without this, lambda destruction happening after the timing window closes would be silently unaccounted for. + +## The Default Constructor Compromise + +`Job` has a default constructor that sets type to `jtINVALID` and index to `0`. The accompanying VFALCO comment acknowledges this is a design wart, present only to allow map-syntax assignment (`jobMap[key] = value`). A `Job` in this state has no callable, no name, and no `LoadEvent`. The comment explicitly wishes for a stricter invariant: "all Job objects refer to a job." This half-constructed state exists purely to satisfy container requirements and should not be treated as a valid work unit. + +## `JobCounter` and Shutdown Coordination + +The final line of the header defines `using JobCounter = ClosureCounter`. `ClosureCounter` is a two-phase reference-counting wrapper for closures (defined in `ClosureCounter.h`). In the "fork" phase, it wraps submitted closures and counts them. When `join()` is called at shutdown, it refuses new wraps and blocks until all outstanding wrapped closures are destroyed. `JobCounter` gives the job dispatch machinery a clean mechanism to wait for all submitted work to drain before tearing down the system, without requiring explicit per-job tracking. + +## Relationship to `LoadMonitor` and `LoadEvent` + +`LoadMonitor` aggregates timing samples across all completed jobs to maintain rolling average and peak latency statistics. Each `Job` holds one `LoadEvent`, which measures that single job's wait time and run time. On `LoadEvent` destruction the sample is submitted to the parent `LoadMonitor`. This design separates per-job timing (scoped RAII via `LoadEvent`) from system-wide load assessment (stateful aggregation in `LoadMonitor`), keeping both classes focused. \ No newline at end of file diff --git a/include/xrpl/core/JobQueue.h.ai.json b/include/xrpl/core/JobQueue.h.ai.json new file mode 100644 index 0000000000..f177f8b464 --- /dev/null +++ b/include/xrpl/core/JobQueue.h.ai.json @@ -0,0 +1,297 @@ +{ + "args": [ + { + "lineno": 77, + "name": "threadCount" + }, + { + "lineno": 78, + "name": "collector" + }, + { + "lineno": 79, + "name": "journal" + }, + { + "lineno": 80, + "name": "logs" + }, + { + "lineno": 81, + "name": "perfLog" + }, + { + "lineno": 88, + "name": "type" + }, + { + "lineno": 89, + "name": "name" + }, + { + "lineno": 90, + "name": "jobHandler" + }, + { + "lineno": 113, + "name": "t" + }, + { + "lineno": 113, + "name": "name" + }, + { + "lineno": 113, + "name": "f" + }, + { + "lineno": 127, + "name": "t" + }, + { + "lineno": 132, + "name": "t" + }, + { + "lineno": 137, + "name": "t" + }, + { + "lineno": 142, + "name": "t" + }, + { + "lineno": 142, + "name": "name" + }, + { + "lineno": 147, + "name": "t" + }, + { + "lineno": 147, + "name": "count" + }, + { + "lineno": 147, + "name": "elapsed" + }, + { + "lineno": 154, + "name": "c" + }, + { + "lineno": 180, + "name": "type" + }, + { + "lineno": 188, + "name": "type" + }, + { + "lineno": 188, + "name": "name" + }, + { + "lineno": 188, + "name": "func" + }, + { + "lineno": 203, + "name": "job" + }, + { + "lineno": 222, + "name": "type" + }, + { + "lineno": 239, + "name": "instance" + }, + { + "lineno": 248, + "name": "type" + } + ], + "classes": [ + { + "args": [], + "lineno": 15, + "name": "Coro_create_t" + }, + { + "args": [ + "threadCount", + "collector", + "journal", + "logs", + "perfLog" + ], + "lineno": 22, + "name": "JobQueue" + }, + { + "args": [ + "Coro_create_t", + "JobQueue&", + "JobType", + "std::string const&", + "F&&" + ], + "lineno": 29, + "name": "Coro" + } + ], + "description": "Defines the JobQueue class for managing a pool of threads and coroutines to execute jobs in the XRPL codebase, including coroutine management, job scheduling, and load monitoring.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/JobQueue.h", + "functions": [ + { + "args": [ + "type", + "name", + "jobHandler" + ], + "lineno": 87, + "name": "addJob" + }, + { + "args": [ + "t", + "name", + "f" + ], + "lineno": 112, + "name": "postCoro" + }, + { + "args": [ + "t" + ], + "lineno": 126, + "name": "getJobCount" + }, + { + "args": [ + "t" + ], + "lineno": 131, + "name": "getJobCountTotal" + }, + { + "args": [ + "t" + ], + "lineno": 136, + "name": "getJobCountGE" + }, + { + "args": [ + "t", + "name" + ], + "lineno": 141, + "name": "makeLoadEvent" + }, + { + "args": [ + "t", + "count", + "elapsed" + ], + "lineno": 146, + "name": "addLoadEvents" + }, + { + "args": [], + "lineno": 150, + "name": "isOverloaded" + }, + { + "args": [ + "c" + ], + "lineno": 153, + "name": "getJson" + }, + { + "args": [], + "lineno": 156, + "name": "rendezvous" + }, + { + "args": [], + "lineno": 158, + "name": "stop" + }, + { + "args": [], + "lineno": 160, + "name": "isStopping" + }, + { + "args": [], + "lineno": 167, + "name": "isStopped" + }, + { + "args": [], + "lineno": 178, + "name": "collect" + }, + { + "args": [ + "type" + ], + "lineno": 179, + "name": "getJobTypeData" + }, + { + "args": [ + "type", + "name", + "func" + ], + "lineno": 187, + "name": "addRefCountedJob" + }, + { + "args": [ + "job" + ], + "lineno": 202, + "name": "getNextJob" + }, + { + "args": [ + "type" + ], + "lineno": 221, + "name": "finishJob" + }, + { + "args": [ + "instance" + ], + "lineno": 238, + "name": "processTask" + }, + { + "args": [ + "type" + ], + "lineno": 247, + "name": "getJobLimit" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "perf" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/JobQueue.h.ai.md b/include/xrpl/core/JobQueue.h.ai.md new file mode 100644 index 0000000000..530f3cd9ab --- /dev/null +++ b/include/xrpl/core/JobQueue.h.ai.md @@ -0,0 +1,60 @@ +# `include/xrpl/core/JobQueue.h` + +## Role in the System + +`JobQueue` is the central work-dispatch mechanism for the XRPL node. Every significant operation — consensus steps, ledger validation, RPC handling, peer-message processing, disk I/O, path-finding — is dispatched as a typed job through this class. It sits between the event-driven networking layer (which produces work) and the `Workers` thread pool (which consumes it), adding priority ordering, per-type concurrency limits, load monitoring, and first-class coroutine support. + +The file contains three closely related declarations: `JobQueue` itself, the nested `Coro` class that embeds a stackful coroutine on top of the job queue, and the inline implementations of `postCoro` and the entire `Coro` body in the companion `Coro.ipp`. + +## Thread Pool Architecture + +`JobQueue` privately inherits `Workers::Callback` and overrides the single virtual method `processTask(int instance)`. This means `JobQueue` is not a general-purpose callback adapter — it _is_ the callback; the private inheritance expresses that relationship without polluting the public API. + +The `Workers` layer is purely a thread pool abstraction: it holds a semaphore (one unit per pending task), maintains a fixed number of threads, and calls `Callback::processTask` whenever a thread wakes. It knows nothing about priorities, types, or limits. All of that logic lives in `JobQueue::processTask`, which calls `getNextJob` to dequeue the highest-priority runnable job and then executes it. + +Jobs live in `m_jobSet`, a `std::set` ordered by priority (encoded in the `JobType` enum value). `getNextJob` takes the first job whose type has running-count below its configured limit, decrements the waiting count, increments the running count, and removes it from the set. Job types with a `limit` of 0 in `JobTypes.h` are unlimited (`getJobLimit` returns `INT_MAX`). Types with tighter limits — for example, `jtPACK` and `jtSWEEP` capped at 1, `jtLEDGER_DATA` at 3 — prevent expensive background work from crowding out latency-sensitive operations. + +## Job Lifecycle and Shutdown Safety + +`addJob` does not accept a raw closure. Instead it passes the handler through `jobCounter_.wrap()`, which returns a `ClosureCounter::Substitute`. This wrapper increments an atomic reference count on construction and decrements it on destruction. During shutdown, `ClosureCounter::join()` sets a "stop accepting" flag and blocks until the count reaches zero. Any `addJob` call that races with shutdown gets back `std::nullopt` from `wrap()`, and `addJob` returns `false` — no job is enqueued. This guarantees that the `Workers` thread pool is never given tasks after the `JobQueue` has decided to stop. + +The `stopping_` atomic flag is separate from `ClosureCounter::join()` and exists purely for callers who need to gate new work eagerly (e.g. checking `isStopping()` before even attempting `addJob`). + +`rendezvous()` blocks on `cv_` until both `m_processCount == 0` (no active `processTask` invocations) and `nSuspend_ == 0` (no suspended coroutines). This two-part condition is essential: a coroutine suspended mid-flight is not visible in `m_processCount`. + +## Coroutine Support (`Coro`) + +The `Coro` class wraps a `boost::coroutines2::coroutine::pull_type`, providing a suspendable job abstraction. Its primary consumer is the RPC handler: when an RPC request arrives on a network thread, `postCoro` creates a `Coro` and returns immediately; the actual RPC work runs later, on a `Workers` thread, and can yield mid-execution to wait for async events (e.g., path-finding results) without blocking the worker thread. + +### Construction Bootstrap + +Constructing a `boost::coroutines2::pull_type` immediately transfers execution _into_ the coroutine body. The `Coro` constructor exploits this: the lambda passed to `coro_` immediately assigns `yield_` (the push-type sink) and calls `yield()` to bounce back to the constructor's frame. The coroutine is now suspended, ready to run when a worker picks it up. Only after construction completes does `postCoro` call `coro->post()` to schedule a job that will call `resume()`. + +### The Post-Before-Yield Race + +The header documents this precisely at lines 354–380. Consider: the coroutine is running user code (`fn`), decides to suspend, calls `post()` to schedule a wakeup, and _then_ calls `yield()`. If the scheduler is fast, the wakeup job can execute `resume()` before `yield()` runs. Without protection this would drive two threads simultaneously into the same coroutine stack — undefined behavior. + +The fix is `mutex_`: `resume()` acquires `mutex_` before calling `coro_()`. The running coroutine itself holds `mutex_` (it was acquired when it was last resumed). So the competing `resume()` job blocks at the lock. After the coroutine yields, the lock is released, and the waiting `resume()` can proceed. By then, `coro_` may already be exhausted if the coroutine returned rather than yielded. `resume()` guards against this with `if (coro_)` — invoking `operator()` on a completed `pull_type` is undefined behavior. + +### `mutex_` vs `mutex_run_` + +Two mutexes serve different purposes. `mutex_` serializes re-entry into the coroutine body itself — it is held for the entire duration a coroutine is executing. `mutex_run_` protects the `running_` flag and the `cv_` condition variable used by `join()`, which only needs to know whether the coroutine is _currently executing_ (not whether it has finished). A caller that needs to wait for a coroutine to drain (e.g., during shutdown) calls `join()`, which blocks on `cv_` until `running_` is false. + +### `nSuspend_` Accounting + +`yield()` increments `jq_.nSuspend_` before suspending; `resume()` decrements it before re-entering. `expectEarlyExit()` decrements it without resuming — for the case where a `Coro` is destroyed while suspended (e.g., failed `postCoro`). This keeps `rendezvous()` accurate: a suspended coroutine that has not yet cleaned up is still "in flight" from the queue's perspective. + +### LocalValues Swap + +`resume()` saves and restores the thread's `detail::LocalValues` pointer around the coroutine body invocation. Each `Coro` owns its own `lvs_` storage so that per-thread state (e.g., job-scoped context) is isolated to the coroutine's logical thread of execution even though multiple coroutines share the same OS threads. + +## Load Monitoring and Statistics + +Each `JobType` has an associated `JobTypeData` holding a `LoadMonitor` configured with the target average and peak latencies from `JobTypes.h`. When a job dequeues or finishes, `LoadMonitor` accumulates timing data. `isOverloaded()` queries whether any type is consistently exceeding its peak latency threshold, and `getJson()` serialises current waiting/running/deferred counts for the admin RPC endpoint. Two `beast::insight` event handles (`dequeue`, `execute`) fire into the metrics collector for external monitoring. + +## Summary of Key Invariants + +- A job added to the queue always runs to completion unless the `Workers` object is destroyed or `ClosureCounter` is joined before the job starts. +- A `Coro` constructed and suspended by `postCoro` increments `nSuspend_`; it must eventually either resume-to-completion or call `expectEarlyExit()` to decrement the count. +- `mutex_` is never held across a `yield()` — that would deadlock on the next `resume()`. +- `m_mutex` (the queue-wide lock) is held only briefly for accounting updates, never while executing job logic. \ No newline at end of file diff --git a/include/xrpl/core/JobTypeData.h.ai.json b/include/xrpl/core/JobTypeData.h.ai.json new file mode 100644 index 0000000000..49cec31220 --- /dev/null +++ b/include/xrpl/core/JobTypeData.h.ai.json @@ -0,0 +1,85 @@ +{ + "args": [ + { + "lineno": 26, + "name": "info_" + }, + { + "lineno": 27, + "name": "collector" + }, + { + "lineno": 28, + "name": "logs" + }, + { + "lineno": 41, + "name": "other" + } + ], + "classes": [ + { + "args": [ + "JobTypeInfo const& info_", + "beast::insight::Collector::ptr const& collector", + "Logs& logs" + ], + "lineno": 8, + "name": "JobTypeData" + } + ], + "description": "Defines the JobTypeData struct, which holds metadata and statistics for a specific job type in the XRPL job queue system, including load monitoring, event tracking, and job counts.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/JobTypeData.h", + "functions": [ + { + "args": [ + "JobTypeInfo const& info_", + "beast::insight::Collector::ptr const& collector", + "Logs& logs" + ], + "lineno": 25, + "name": "JobTypeData" + }, + { + "args": [ + "JobTypeData const& other" + ], + "lineno": 41, + "name": "JobTypeData" + }, + { + "args": [ + "JobTypeData const& other" + ], + "lineno": 42, + "name": "operator=" + }, + { + "args": [], + "lineno": 45, + "name": "name" + }, + { + "args": [], + "lineno": 50, + "name": "type" + }, + { + "args": [], + "lineno": 55, + "name": "load" + }, + { + "args": [], + "lineno": 60, + "name": "stats" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/JobTypeData.h.ai.md b/include/xrpl/core/JobTypeData.h.ai.md new file mode 100644 index 0000000000..2d5b15e914 --- /dev/null +++ b/include/xrpl/core/JobTypeData.h.ai.md @@ -0,0 +1,29 @@ +# `include/xrpl/core/JobTypeData.h` + +## Role in the System + +`JobTypeData` is the runtime counterpart to `JobTypeInfo`. The XRPL job queue system divides per-type knowledge into two layers: static configuration that never changes (`JobTypeInfo` — limits, latency targets, name, type enum) and mutable runtime state that changes constantly as jobs flow through the system. `JobTypeData` owns that second layer for a single job category. Every `JobType` that the `JobQueue` manages gets exactly one `JobTypeData` instance, stored in a map keyed by `JobType`. + +## What It Tracks + +Three plain `int` members cover the lifecycle of jobs through the queue: `waiting` (enqueued but not yet dispatched), `running` (currently executing on a worker thread), and `deferred` (would have been enqueued but the running-job limit was hit). These are not atomic — they are protected by the `JobQueue`'s own mutex, which owns and manipulates them directly. The struct makes no concurrency promises of its own; it relies entirely on the caller. + +The private `m_load` member is a `LoadMonitor`, which tracks average and peak latency over a rolling window and can signal when a job type is overloaded. Its targets are wired up in the constructor via `m_load.setTargetLatency(info.getAverageLatency(), info.getPeakLatency())` — pulling the configured thresholds from the immutable `JobTypeInfo`. + +## Insight Event Integration + +The two `beast::insight::Event` public members — `dequeue` and `execute` — are notification hooks for the metrics collection infrastructure. When a job finishes, `JobQueue` checks elapsed times and calls `dequeue.notify(q_time)` and `execute.notify(x_time)` if either the queue wait or execution exceeded 10ms. The event names follow a naming convention: queue-time events get the suffix `_q` (e.g., `"clientCommand_q"`), and execution-time events use the bare name (e.g., `"clientCommand"`). This makes it straightforward to distinguish queueing latency from processing latency in any connected metrics backend. + +The constructor conditionally creates these events — only for non-special job types (`!info.special()`). Special jobs have a limit of zero, meaning they bypass normal queue dispatch entirely and are never timed in the standard way. Creating insight events for them would be both wasteful and potentially misleading. + +## Design: Reference to Static Config + +`info` is a `const&` to `JobTypeInfo`, not a copy. This is intentional: `JobTypeInfo` objects are themselves stored in a separate lookup table in `JobTypes.h`, constructed once at startup and never modified. Holding a reference instead of a copy avoids duplication while enforcing the contract that static configuration is not accidentally altered through a `JobTypeData` accessor. + +Copy construction and copy assignment are explicitly deleted. Because `JobTypeData` holds a reference member and owns a `LoadMonitor` with internal mutex state, copying would be semantically wrong — there is no meaningful interpretation of "a copy of the runtime state for job type X." The delete makes this a hard compiler error rather than a silent bug. + +## How `JobQueue` Uses It + +`JobQueue` exposes a `getJobTypeData(JobType)` method returning a `JobTypeData&` and keeps a `JobDataMap` (a `std::map`). The dispatch path consults `data.waiting + data.running < getJobLimit(type)` before submitting a task to the worker pool; if over limit, it increments `data.deferred`. When `finishJob()` is called, the queue decrements `running`, checks `deferred > 0` to requeue a previously held-back job, and triggers the appropriate insight events with actual timing data. + +The `stats()` method delegates to `m_load.getStats()`, returning a `LoadMonitor::Stats` snapshot containing count, average latency, peak latency, and an overloaded flag. `JobQueue` calls this during queue introspection (e.g., for RPC status responses) to surface per-type load information without exposing the `LoadMonitor` directly. \ No newline at end of file diff --git a/include/xrpl/core/JobTypeInfo.h.ai.json b/include/xrpl/core/JobTypeInfo.h.ai.json new file mode 100644 index 0000000000..be90ece484 --- /dev/null +++ b/include/xrpl/core/JobTypeInfo.h.ai.json @@ -0,0 +1,57 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "JobType type", + "std::string name", + "int limit", + "std::chrono::milliseconds avgLatency", + "std::chrono::milliseconds peakLatency" + ], + "lineno": 7, + "name": "JobTypeInfo" + } + ], + "description": "Defines the JobTypeInfo class, which holds static, immutable information about a job type in the XRPL job queue system, such as its type, name, concurrency limit, and latency expectations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/JobTypeInfo.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "type" + }, + { + "args": [], + "lineno": 43, + "name": "name" + }, + { + "args": [], + "lineno": 48, + "name": "limit" + }, + { + "args": [], + "lineno": 53, + "name": "special" + }, + { + "args": [], + "lineno": 58, + "name": "getAverageLatency" + }, + { + "args": [], + "lineno": 63, + "name": "getPeakLatency" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/JobTypeInfo.h.ai.md b/include/xrpl/core/JobTypeInfo.h.ai.md new file mode 100644 index 0000000000..c9ec143e36 --- /dev/null +++ b/include/xrpl/core/JobTypeInfo.h.ai.md @@ -0,0 +1,36 @@ +# `JobTypeInfo.h` — Immutable Metadata Descriptor for Job Queue Categories + +## Role in the System + +`JobTypeInfo` is the static configuration record for a single job type in the XRPL job queue subsystem. It answers three questions the scheduler needs before it ever touches a running thread: *what is this job called*, *how many can run at once*, and *what latency thresholds are considered healthy*. Nothing in this class changes after construction, which is enforced by declaring every member `const`. + +The class exists because the job queue distinguishes two kinds of information: the fixed, compile-time-stable attributes of a category (captured here), and the live runtime counters that fluctuate as jobs are enqueued, run, and finish (captured in `JobTypeData`). Keeping them separate lets the runtime state in `JobTypeData` hold a plain `const&` to its corresponding `JobTypeInfo` without any ownership complexity, while making it impossible to accidentally mutate the policy parameters at runtime. + +## What It Holds + +The four meaningful fields are: + +- **`m_type`** (`JobType` enum) — the canonical identifier, defined in `Job.h`. The enum's ordinal position doubles as dispatch priority: later entries in the enum have higher priority in the job set's sort order. +- **`m_name`** (`std::string`) — a human-readable label used in log output and metric event names. `JobTypeData` appends `_q` to this name for the queue-depth insight event. +- **`m_limit`** (`int`) — the maximum number of simultaneously running jobs of this type that the job queue will permit. Zero is a sentinel meaning "special" (see below). +- **`m_avgLatency` / `m_peakLatency`** (`std::chrono::milliseconds`) — target thresholds passed directly to `LoadMonitor::setTargetLatency()`. Zero means no threshold is tracked for this type. + +## The `special()` Predicate + +The most non-obvious design choice in this file is using `m_limit == 0` as the signal that a job type is *not dispatched through the job pool at all*. These special types — `jtPEER`, `jtDISK`, `jtTXN_PROC`, `jtPATH_FIND`, etc. — represent work that occurs on dedicated or external threads, but whose latency and activity the system still wants to monitor via `LoadMonitor`. By registering them in `JobTypes` with a zero limit, they participate in the metrics infrastructure without being subject to concurrency capping. + +`JobTypeData` uses `special()` to guard the creation of insight events: a special job type gets no `dequeue` or `execute` events, since there is no queue to measure. + +## Population and Lifetime + +All `JobTypeInfo` instances are created once inside `JobTypes::JobTypes()` (in `JobTypes.h`), which is a private constructor backing a Meyer's singleton. The constructor calls a local `add` lambda for each known `JobType`, assembling a `std::map`. An assertion guards against duplicate registrations. Because the singleton is `const` and `JobTypeInfo` members are all `const`, this data is effectively read-only for the lifetime of the process. + +`JobTypes::name(JobType)` is a common call site, used throughout the codebase when a string label is needed for a `JobType` without requiring a full `JobTypeData` context. + +## Relationship to `JobTypeData` + +`JobTypeData` is the runtime sibling: it holds a `JobTypeInfo const& info` reference alongside mutable `waiting`, `running`, and `deferred` counters, a `LoadMonitor`, and insight metric handles. The `LoadMonitor` is configured directly from `info.getAverageLatency()` and `info.getPeakLatency()` at `JobTypeData` construction. The clear separation means that the policy defined in `JobTypeInfo` is never accidentally modified when the runtime increments a counter or fires a metric. + +## Design Notes + +The class is non-default-constructible by explicit `= delete`, preventing accidental zero-initialized instances that would carry nonsensical metadata. The `std::string` name is moved in the constructor, avoiding a copy when called from the initializer lambda in `JobTypes`. Because all accessors return by value or `const&` and the class carries no mutable state, it is trivially thread-safe to read from multiple threads without synchronization. \ No newline at end of file diff --git a/include/xrpl/core/JobTypes.h.ai.json b/include/xrpl/core/JobTypes.h.ai.json new file mode 100644 index 0000000000..9a013ee831 --- /dev/null +++ b/include/xrpl/core/JobTypes.h.ai.json @@ -0,0 +1,70 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "JobTypes" + } + ], + "description": "Defines the JobTypes singleton class, which manages static information about all job types in the XRPL system, including their names, limits, and latency expectations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/JobTypes.h", + "functions": [ + { + "args": [], + "lineno": 74, + "name": "instance" + }, + { + "args": [ + "jt" + ], + "lineno": 80, + "name": "name" + }, + { + "args": [ + "jt" + ], + "lineno": 85, + "name": "get" + }, + { + "args": [], + "lineno": 95, + "name": "getInvalid" + }, + { + "args": [], + "lineno": 100, + "name": "size" + }, + { + "args": [], + "lineno": 105, + "name": "begin" + }, + { + "args": [], + "lineno": 109, + "name": "cbegin" + }, + { + "args": [], + "lineno": 113, + "name": "end" + }, + { + "args": [], + "lineno": 117, + "name": "cend" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/JobTypes.h.ai.md b/include/xrpl/core/JobTypes.h.ai.md new file mode 100644 index 0000000000..aff3676e1d --- /dev/null +++ b/include/xrpl/core/JobTypes.h.ai.md @@ -0,0 +1,37 @@ +# `include/xrpl/core/JobTypes.h` + +## Role in the System + +`JobTypes` is the central registry for all job-type metadata in the XRPL job queue subsystem. It is a compile-time-defined, read-only catalog that answers a single question for any `JobType` enum value: what are the static properties of this kind of work? Every other component that needs to schedule, monitor, or report on jobs queries this singleton for concurrency limits, human-readable names, and latency expectations. + +The file sits at the intersection of three subsystems: the `JobQueue` (which schedules and dispatches work), `JobTypeData` (which tracks runtime statistics per job category), and `PerfLog` (which emits telemetry). Each of those reads from `JobTypes` but none writes to it, keeping the registry strictly immutable after construction. + +## Design: Meyers Singleton with a Private Registrar + +The constructor is private and populates a `std::map` using a local `add` lambda rather than any kind of registration API or macro magic. This was an explicit design choice: the entire catalog is defined in one place, in one constructor, making it impossible to accidentally register the same type twice (both `XRPL_ASSERT` calls would fire in debug builds) and easy to audit the full set of job types at a glance. + +`instance()` returns a `const` reference to a function-local static, which gives thread-safe initialization for free under C++11 and beyond — important because `JobQueue` and `PerfLog` both call it during startup from potentially concurrent contexts. + +## The Two Job Categories + +All registered entries fall into one of two behavioral categories based on the `limit` field passed to `add`: + +**Dispatchable jobs** (limit > 0): These are actual work items placed on the queue. A limit of `std::numeric_limits::max()` means "unbounded concurrency" — the queue will run as many of these simultaneously as threads allow. A small integer limit caps concurrency to prevent expensive operations from dominating the thread pool; `jtPACK` (fetch pack generation) is capped at 1, `jtLEDGER_DATA` at 3, and `jtREPLAY_REQ` at 10. The `JobQueue::getJobLimit()` function queries this via `JobTypes::instance().get(type).limit()` before deciding whether to defer a new job. + +**Special jobs** (limit == 0): These are not dispatched through the queue at all. Types like `jtPEER`, `jtDISK`, `jtHO_READ`, and the node-store variants (`jtNS_SYNC_READ`, etc.) exist purely so that their execution times can be tracked by `LoadMonitor`. `JobTypeInfo::special()` returns `true` when `m_limit == 0`, and `JobTypeData` uses this flag to skip creating insight-collector events for them. + +## Latency Thresholds as SLAs + +Each job type carries an average-latency and peak-latency target, specified in milliseconds. These are passed into `JobTypeData`, which forwards them to a per-type `LoadMonitor` via `setTargetLatency`. The `LoadMonitor` uses them to determine whether a job type is running "hot" — the average and peak thresholds effectively encode the intended SLA for each work category. + +The choice of zero for both latency fields on many types (e.g. `jtPACK`, `jtLEDGER_DATA`, `jtACCEPT`) means those types opt out of latency alerting entirely. High-value interactive paths like `jtCLIENT` (2 s / 5 s) and `jtPUBLEDGER` (3 s / 4.5 s) have explicit thresholds reflecting the observable impact of delays on end users. + +## Iteration and Fallback + +`JobTypes` exposes `begin()`/`cbegin()` and `end()`/`cend()` so that `JobQueue`'s constructor can range-iterate over all registered types and create a corresponding `JobTypeData` for each one. This iterator-based design avoids any hard-coded list in `JobQueue` itself. + +The `m_unknown` member — initialized with `jtINVALID`, name `"invalid"`, zero limit, and zero latencies — acts as a safe fallback return value from `get()`. In debug builds, calling `get()` with an unregistered type triggers `XRPL_ASSERT`; in release builds, `m_unknown` is returned rather than dereferencing a null pointer. The public `getInvalid()` accessor lets `JobQueue` set up a dedicated `JobTypeData` for the invalid sentinel without special-casing it in iteration. + +## Relationship to `Job.h` + +The `JobType` enum in `Job.h` is worth noting: enum values are ordered by ascending priority, so earlier entries (like `jtPACK`) have lower priority than later entries (like `jtADMIN`). This ordering is used by `Job`'s comparison operators to sort pending work in the queue's priority set. `JobTypes` does not duplicate this ordering — it only adds the metadata layer on top of the enum values defined in `Job.h`. \ No newline at end of file diff --git a/include/xrpl/core/LoadEvent.h.ai.json b/include/xrpl/core/LoadEvent.h.ai.json new file mode 100644 index 0000000000..bcefd2dbb4 --- /dev/null +++ b/include/xrpl/core/LoadEvent.h.ai.json @@ -0,0 +1,84 @@ +{ + "args": [ + { + "lineno": 15, + "name": "monitor" + }, + { + "lineno": 15, + "name": "name" + }, + { + "lineno": 15, + "name": "shouldStart" + } + ], + "classes": [ + { + "args": [ + "LoadMonitor& monitor", + "std::string const& name", + "bool shouldStart" + ], + "lineno": 11, + "name": "LoadEvent" + } + ], + "description": "Defines the LoadEvent class in the xrpl namespace, which measures and tracks elapsed time for waiting and running states, typically for monitoring load or performance.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/LoadEvent.h", + "functions": [ + { + "args": [ + "LoadMonitor& monitor", + "std::string const& name", + "bool shouldStart" + ], + "lineno": 15, + "name": "LoadEvent" + }, + { + "args": [], + "lineno": 17, + "name": "~LoadEvent" + }, + { + "args": [], + "lineno": 19, + "name": "name" + }, + { + "args": [], + "lineno": 22, + "name": "waitTime" + }, + { + "args": [], + "lineno": 25, + "name": "runTime" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 28, + "name": "setName" + }, + { + "args": [], + "lineno": 34, + "name": "start" + }, + { + "args": [], + "lineno": 40, + "name": "stop" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/LoadEvent.h.ai.md b/include/xrpl/core/LoadEvent.h.ai.md new file mode 100644 index 0000000000..885951c0ba --- /dev/null +++ b/include/xrpl/core/LoadEvent.h.ai.md @@ -0,0 +1,40 @@ +# `LoadEvent.h` — Scoped Latency Measurement for the Job Queue + +`LoadEvent` is a scoped elapsed-time instrument that records how long a unit of work spends waiting to be dispatched versus actually executing. It exists to give `LoadMonitor` the per-sample data it needs to compute aggregate latency statistics and detect system overload. + +The in-source TODO comment captures the intent precisely: this class should eventually be renamed `ScopedLoadSample`. It acts as a RAII wrapper around a two-phase timing model — waiting and running — and automatically reports its accumulated measurements to the owning `LoadMonitor` when it stops. + +## Two-Phase Timing Model + +The distinguishing design choice is the split between *wait time* and *run time*. Most stopwatch abstractions track a single elapsed duration; `LoadEvent` tracks two: + +- **Wait time** (`timeWaiting_`) accumulates from object construction (or a second call to `start()`) up to the moment `start()` is called to begin the active phase. +- **Run time** (`timeRunning_`) accumulates from the `start()` call until `stop()` is called. + +A single `mark_` timestamp records the last state transition. On `start()`, the elapsed time since `mark_` is added to `timeWaiting_` and `mark_` is reset; on `stop()`, the elapsed time since `mark_` is added to `timeRunning_`. If `start()` is called a second time before `stop()`, the intermediate interval is again classified as waiting — a deliberate design that supports re-queuing or preemption scenarios without losing timing data. + +## Lifecycle in the Job Queue + +The primary consumer is `Job`. When a `Job` is constructed with a callable, it immediately creates a `LoadEvent` with `shouldStart=false`: + +```cpp +m_loadEvent = std::make_shared(std::ref(lm), name, false); +``` + +The event begins tracking from the moment the job enters the queue. Time spent sitting in the priority queue before a worker thread picks it up accumulates as wait time. When `Job::doJob()` executes, it calls `m_loadEvent->start()`, promoting the measurement from the waiting phase to the running phase. The `LoadEvent` is stored in a `shared_ptr` precisely because `Job` objects are movable and copied into the job map, yet the event must remain at a stable address when `doJob()` borrows a reference to it across thread boundaries. + +The destructor calls `stop()` if the event is still running. This ensures that even if a job throws or an early return bypasses an explicit `stop()`, the timing sample is always reported to `LoadMonitor`. + +## Reporting to `LoadMonitor` + +`stop()` calls `monitor_.addLoadSample(*this)`, passing itself by `const` reference. `LoadMonitor::addLoadSample()` computes total latency as `runTime() + waitTime()`, discards jitter below 2 ms, and logs a warning at `info` level for jobs exceeding 500 ms and at `warn` level for those exceeding 1 second. The sample then feeds the exponentially-decayed moving average and peak latency tracking maintained by `LoadMonitor`. + +This tight coupling — `LoadEvent` holds a non-owning reference to its `LoadMonitor` — is a recognised design debt flagged in the header's VFALCO comments. The coupling creates a lifetime requirement: the `LoadMonitor` must outlive every `LoadEvent` it is associated with. In practice this is satisfied because `LoadMonitor` instances are owned at application scope while `LoadEvent` instances are tied to individual jobs. + +## Copy Suppression + +The copy constructor is explicitly deleted. Because `LoadEvent` accumulates time state and holds a reference to an external `LoadMonitor`, copying would produce two objects that both attempt to report to the same monitor when stopped — double-counting latency. The delete forces callers to manage lifetime explicitly, which in practice means using `shared_ptr` (as `Job` does) or embedding directly in a fixed-address owner. + +## Relationship to `LoadMonitor` + +`LoadMonitor` is the aggregate consumer; `LoadEvent` is the per-sample producer. `LoadMonitor` maintains exponentially-decayed counts and latency histograms protected by a `mutex_`, while `LoadEvent` performs all its timing on the calling thread with no synchronisation overhead. The lock is only acquired at the moment `addSamples()` is called inside `stop()`, keeping the hot path — timing the work itself — lock-free. \ No newline at end of file diff --git a/include/xrpl/core/LoadMonitor.h.ai.json b/include/xrpl/core/LoadMonitor.h.ai.json new file mode 100644 index 0000000000..518ccd70e3 --- /dev/null +++ b/include/xrpl/core/LoadMonitor.h.ai.json @@ -0,0 +1,81 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "beast::Journal j" + ], + "lineno": 11, + "name": "LoadMonitor" + }, + { + "args": [], + "lineno": 29, + "name": "Stats" + } + ], + "description": "Defines the LoadMonitor class, which monitors load levels and response times, providing statistics and overload detection for the XRPL system.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/LoadMonitor.h", + "functions": [ + { + "args": [ + "beast::Journal j" + ], + "lineno": 14, + "name": "LoadMonitor" + }, + { + "args": [ + "LoadEvent const& sample" + ], + "lineno": 16, + "name": "addLoadSample" + }, + { + "args": [ + "int count", + "std::chrono::milliseconds latency" + ], + "lineno": 19, + "name": "addSamples" + }, + { + "args": [ + "std::chrono::milliseconds avg", + "std::chrono::milliseconds pk" + ], + "lineno": 22, + "name": "setTargetLatency" + }, + { + "args": [ + "std::chrono::milliseconds avg", + "std::chrono::milliseconds peak" + ], + "lineno": 25, + "name": "isOverTarget" + }, + { + "args": [], + "lineno": 36, + "name": "getStats" + }, + { + "args": [], + "lineno": 39, + "name": "isOver" + }, + { + "args": [], + "lineno": 43, + "name": "update" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/LoadMonitor.h.ai.md b/include/xrpl/core/LoadMonitor.h.ai.md new file mode 100644 index 0000000000..7f891059ef --- /dev/null +++ b/include/xrpl/core/LoadMonitor.h.ai.md @@ -0,0 +1,50 @@ +# `LoadMonitor` — Per-Job-Type Latency Tracking with Exponential Decay + +## Role in the System + +`LoadMonitor` is the statistical engine behind XRPL's job-queue backpressure and health monitoring. Each distinct job category in the system (network I/O, consensus, transaction processing, etc.) owns exactly one `LoadMonitor` instance, embedded inside a `JobTypeData` struct. The monitor accumulates timing samples from completed jobs, computes rolling average and peak latencies using exponential decay, and compares them against configurable thresholds to determine whether a given job category is "overloaded." + +It exists alongside `LoadEvent`, which is the RAII timing companion. A `LoadEvent` records how long a job spent waiting in the queue and how long it spent actually executing. When the event is destroyed, it calls back into its owning `LoadMonitor` to report the sample. + +## The Exponential Decay Model + +The most important design decision in `LoadMonitor` is how it maintains a rolling window without an explicit circular buffer or timestamp-indexed history. Instead, the private `update()` method is called at the start of every mutating operation and reads the current time from `UptimeClock` (a second-precision program-uptime clock). If more than one second has elapsed since the last update, the accumulators are decayed by 1/4 per elapsed second in a `do-while` loop: + +```cpp +mCounts -= ((mCounts + 3) / 4); // integer ceiling divide +mLatencyEvents -= ((mLatencyEvents + 3) / 4); +mLatencyMSAvg -= (mLatencyMSAvg / 4); +mLatencyMSPeak -= (mLatencyMSPeak / 4); +``` + +This is a deliberate choice: rather than keeping a window of the last N samples or the last N seconds, the monitor uses a leaky bucket. As the inline comment (attributed to David Schwartz) explains: if you add 10 units per second and reduce by 1/4 per second, the value stabilizes around 40, which represents 10 per second. The "true" rate is recovered at read time by dividing by 4. This design is lightweight, allocation-free, and naturally ages out stale data without bookkeeping. + +A second guard handles clock anomalies: if the current time is more than 8 seconds ahead of `mLastUpdate`, or if it goes backwards (clock reset), all accumulators are zeroed and the epoch is reset. This prevents stale data from persisting through long pauses or restarts. + +## The Factor of 4 + +The "×4 normalization" permeates the implementation and deserves explicit explanation. Raw accumulators hold values scaled by 4 at steady state because of the decay formula. When `getStats()` reads them out it divides by 4: + +```cpp +stats.count = mCounts / 4; +stats.latencyAvg = mLatencyMSAvg / (mLatencyEvents * 4); +stats.latencyPeak = mLatencyMSPeak / (mLatencyEvents * 4); +``` + +Similarly, `isOver()` computes the check using the same normalization. The `mLatencyMSPeak` accumulator gets an additional upward push on each sample — it is set to the maximum of its current value and `mLatencyEvents * latency * 4 / count`, which biases the peak toward recent high-latency events. This asymmetry (slow to decay via the normal path, fast to spike on a bad sample) makes the peak metric more sensitive to bursts than the average. + +## Jitter Filtering and Logging + +`addLoadSample()`, the entry point called from a `LoadEvent` destructor, suppresses sub-2ms samples entirely by treating them as zero-latency ("jitter"). Samples above 500ms trigger an info-level log entry; above 1 second, a warning. This is the only place in the monitor where the `Journal` is used — all other paths are silent. + +## Thread Safety + +All state mutations go through `mutex_`. The `update()` method is documented as requiring the caller to hold the lock, and every public mutating method (`addSamples`, `isOver`, `getStats`) acquires it before calling `update()`. `setTargetLatency` is the exception — it writes `mTargetLatencyAvg` and `mTargetLatencyPk` without the lock, which is safe only during initialization (before any samples arrive). `isOverTarget()` is a pure read of the target values and is only ever called while the lock is already held. + +## Integration with `JobTypeData` + +`JobTypeData` constructs its embedded `LoadMonitor` with the job category's journal, then immediately calls `setTargetLatency` with latency thresholds from `JobTypeInfo`. These thresholds differ by job type — consensus jobs tolerate longer latencies than RPC-serving jobs. The `JobQueue` infrastructure calls `load().addLoadSample(event)` after each completed job and checks `stats().isOverloaded` when deciding whether to defer new jobs in that category. This makes `LoadMonitor` a key input to the job admission control loop. + +## Design Notes and Known Rough Edges + +The codebase contains several `VFALCO TODO` comments acknowledging unresolved design debt: the name collision between `LoadMonitor` and `LoadManager` is flagged as confusing, `LoadEvent` is noted as a candidate for renaming to `ScopedLoadSample`, and the magic number 8 (the "way out of date" threshold in seconds) has no documented rationale. The `Stats` struct was already acknowledged as a partial improvement over a previous multi-out-parameter approach, with a note to complete the refactoring. Despite these rough edges, the core exponential-decay mechanism is compact, effective, and correctly integrated into the broader job scheduling system. \ No newline at end of file diff --git a/include/xrpl/core/NetworkIDService.h.ai.json b/include/xrpl/core/NetworkIDService.h.ai.json new file mode 100644 index 0000000000..54fc343384 --- /dev/null +++ b/include/xrpl/core/NetworkIDService.h.ai.json @@ -0,0 +1,26 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "NetworkIDService" + } + ], + "description": "Defines the NetworkIDService class, which provides read-only access to the configured network ID for the XRPL server, identifying which network (mainnet, testnet, devnet, or custom) the server is connected to.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/NetworkIDService.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "getNetworkID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/NetworkIDService.h.ai.md b/include/xrpl/core/NetworkIDService.h.ai.md new file mode 100644 index 0000000000..3428057526 --- /dev/null +++ b/include/xrpl/core/NetworkIDService.h.ai.md @@ -0,0 +1,41 @@ +# `NetworkIDService.h` — Network Identity Interface + +`NetworkIDService` is a minimal pure-abstract interface in the `xrpl` namespace that provides read-only access to the integer identifier of the XRPL network this server is configured to operate on. The file is intentionally tiny — one class, one method — because its real purpose is architectural: it decouples the transaction-processing pipeline from the heavyweight `Config` object and makes the network-identity concept independently injectable and testable. + +## Why This Interface Exists + +The XRPL protocol supports multiple independent ledger networks (mainnet, testnet, devnet, and an open-ended range of custom networks). Transactions submitted to one network must be rejected by all others, or a signed transaction could be replayed across network boundaries. The `sfNetworkID` transaction field enforces this, and validating it requires the node to know its own network ID at transaction-preflight time. + +Rather than pulling in a full `Config` reference wherever transaction validation runs, `NetworkIDService` narrows the dependency to the single fact that matters. The `Transactor`'s `preflight0()` function illustrates this directly: + +```cpp +uint32_t const nodeNID = ctx.registry.get().getNetworkIDService().getNetworkID(); +std::optional txNID = ctx.tx[~sfNetworkID]; +``` + +The boundary conditions enforced there capture the backward-compatibility story embedded in the well-known ID values: + +- **IDs 0–1024 (legacy networks):** The `sfNetworkID` field did not exist when these networks were established. If a transaction *contains* the field, it is rejected with `telNETWORK_ID_MAKES_TX_NON_CANONICAL` — presence of the field signals that the transaction was not built for a legacy network. +- **IDs > 1024 (custom networks):** The field is mandatory. A missing field returns `telREQUIRES_NETWORK_ID`; a mismatched value returns `telWRONG_NETWORK`. This enforces the replay-protection guarantee. + +The threshold of 1024 is not arbitrary — it's the protocol's designated boundary between pre-field and post-field networks. + +## Concrete Implementation and Lifecycle + +The concrete implementation, `NetworkIDServiceImpl` (in `src/xrpld/core/`), stores a `std::uint32_t networkID_` captured at construction time from `config_->NETWORK_ID`. The `Application` class instantiates and owns it: + +```cpp +networkIDService_(std::make_unique(config_->NETWORK_ID)) +``` + +`Config::NETWORK_ID` itself is parsed from the `[network_id]` section of the rippled configuration file, accepting the string aliases `"main"` (→ 0), `"testnet"` (→ 1), `"devnet"` (→ 2), or any raw integer. + +Caching the value at construction rather than reading `Config` on every call is the right trade-off here: the network ID is static for the lifetime of the process, and the cache enables the `noexcept` guarantee on `getNetworkID()`. That guarantee matters because `preflight0()` is called in contexts where exception propagation would be problematic. + +## Position in the Service Hierarchy + +`NetworkIDService` is one of the core infrastructure services listed in `ServiceRegistry`, which is the application's primary dependency-injection hub. `ServiceRegistry::getNetworkIDService()` returns a reference to the instance, giving any component that holds a `ServiceRegistry` reference a clean path to the network ID without coupling to `Application` or `Config`. + +The interface lives in `include/xrpl/core/` (the public `xrpl` library boundary), while `NetworkIDServiceImpl` lives in `src/xrpld/core/` (the private `xrpld` application layer that has access to `Config`). This respects the codebase's layering convention: the protocol-level concept of "a network has an ID" belongs in the public library; the detail of how that ID is read from a config file belongs in the application layer. + +This split also enables clean unit testing — test harnesses like `test/jtx/impl/Env.cpp` can supply a stub or mock `NetworkIDService` without any dependency on a real configuration file. \ No newline at end of file diff --git a/include/xrpl/core/PeerReservationTable.h.ai.json b/include/xrpl/core/PeerReservationTable.h.ai.json new file mode 100644 index 0000000000..a3d20f9070 --- /dev/null +++ b/include/xrpl/core/PeerReservationTable.h.ai.json @@ -0,0 +1,127 @@ +{ + "args": [ + { + "lineno": 13, + "name": "nodeId" + }, + { + "lineno": 14, + "name": "description" + }, + { + "lineno": 44, + "name": "journal" + }, + { + "lineno": 52, + "name": "nodeId" + }, + { + "lineno": 59, + "name": "connection" + }, + { + "lineno": 66, + "name": "reservation" + } + ], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "PeerReservation" + }, + { + "args": [], + "lineno": 33, + "name": "KeyEqual" + }, + { + "args": [ + "beast::Journal journal" + ], + "lineno": 41, + "name": "PeerReservationTable" + } + ], + "description": "Defines data structures and logic for managing peer reservations in the XRPL network, including reservation insertion, deletion, lookup, and listing, with thread safety and optional database integration.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/PeerReservationTable.h", + "functions": [ + { + "args": [], + "lineno": 16, + "name": "PeerReservation::toJson" + }, + { + "args": [ + "Hasher& h", + "PeerReservation const& x" + ], + "lineno": 19, + "name": "hash_append" + }, + { + "args": [ + "PeerReservation const& a", + "PeerReservation const& b" + ], + "lineno": 25, + "name": "operator<" + }, + { + "args": [ + "PeerReservation const& lhs", + "PeerReservation const& rhs" + ], + "lineno": 36, + "name": "KeyEqual::operator()" + }, + { + "args": [ + "beast::Journal journal" + ], + "lineno": 43, + "name": "PeerReservationTable::PeerReservationTable" + }, + { + "args": [], + "lineno": 48, + "name": "PeerReservationTable::list" + }, + { + "args": [ + "PublicKey const& nodeId" + ], + "lineno": 51, + "name": "PeerReservationTable::contains" + }, + { + "args": [ + "DatabaseCon& connection" + ], + "lineno": 58, + "name": "PeerReservationTable::load" + }, + { + "args": [ + "PeerReservation const& reservation" + ], + "lineno": 65, + "name": "PeerReservationTable::insert_or_assign" + }, + { + "args": [ + "PublicKey const& nodeId" + ], + "lineno": 71, + "name": "PeerReservationTable::erase" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/PeerReservationTable.h.ai.md b/include/xrpl/core/PeerReservationTable.h.ai.md new file mode 100644 index 0000000000..c22e030acb --- /dev/null +++ b/include/xrpl/core/PeerReservationTable.h.ai.md @@ -0,0 +1,49 @@ +# `include/xrpl/core/PeerReservationTable.h` + +## Purpose + +The XRPL peer overlay has a bounded slot pool — a node only maintains connections with a limited number of peers. `PeerReservationTable` exists to guarantee that specific, operator-designated peers can always connect even when that pool is saturated. A reserved peer bypasses the normal acceptance policy and is granted a connection unconditionally. + +This header defines two public types — the `PeerReservation` value type and the `PeerReservationTable` container — along with the `KeyEqual` comparator helper that bridges them. + +## `PeerReservation` — Value Type Design + +A reservation is simply a `PublicKey nodeId` paired with an optional human-readable `description`. The design decision that drives everything else is that **identity is the public key alone**: the `hash_append` friend delegates only to `nodeId`, and `KeyEqual` compares only `nodeId`. The `description` field is pure metadata and plays no role in lookup or equality. + +`operator<` provides a total ordering over `nodeId`, used exclusively by `list()` to return reservations in a deterministic, sorted order for display or API responses. It is not involved in the hash-based storage. + +## `KeyEqual` — Bridging the `unordered_set` Gap + +The standard library's `std::unordered_set` is parameterized on a full equality predicate, not a projection. Because `PeerReservation` is not equality-comparable by default on its full value (description could differ for the same node), `KeyEqual` provides a minimal comparator that compares only `nodeId`. A companion comment marks a C++20 improvement opportunity: heterogeneous lookup with "equivalence" would allow passing a bare `PublicKey` to `find()` without constructing a throwaway `PeerReservation{nodeId}`. Until then, callers use the workaround of brace-initializing a partial struct. + +## `PeerReservationTable` — Thread-Safe, DB-Backed Cache + +The table stores reservations as an `std::unordered_set, KeyEqual>` — an in-memory cache that is loaded from and persisted to a SQLite database via `DatabaseCon`. The `mutex_` and `journal_` members are both `mutable` so that `const`-qualified operations (`list()`, `contains()`) can still acquire the lock and log without requiring a non-const object. + +**Two-phase initialization** mirrors `ApplicationImp`'s own setup lifecycle: the constructor only accepts a `beast::Journal` and initializes a null `connection_` pointer; the actual database connection is not available until `ApplicationImp::setup()` runs, at which point `load(DatabaseCon&)` is called. This is acknowledged in a comment as a forced dependency: `load()` stores the `DatabaseCon*` for later mutations and bulk-reads the persisted reservation table via `getPeerReservationTable()` (defined in `src/libxrpl/server/Wallet.cpp`). The `load()` return type is `bool` to fit the error-handling convention of `ApplicationImp::setup`, though it unconditionally returns `true` — an empty table is always a valid starting state. + +## Core Operations + +`contains(PublicKey const&)` is the hot path. It is called by `OverlayImpl` during peer activation to decide whether an incoming connection should receive a reserved slot: + +```cpp +bool const reserved = + static_cast(app_.getCluster().member(publicKey)) || + app_.getPeerReservations().contains(publicKey); +auto const result = m_peerFinder->activate(slot, publicKey, reserved); +``` + +A reserved peer — whether by cluster membership or an explicit reservation — passes the `reserved` flag to `PeerFinder::activate`, which allows the connection to proceed past the normal slot limit. This is the reason the table needs to be in-memory rather than querying the DB on each incoming connection. + +`insert_or_assign(PeerReservation const&)` exposes a conceptual upsert, but `std::unordered_set` provides no native implementation. Since set elements are logically immutable (mutating them would break hash-bucket placement), updating a reservation requires erase-then-insert. The implementation is deliberate about iterator safety: it increments the found iterator before erasing — rather than decrementing — because decrement is illegal if the element is at `begin()`, while increment to `end()` is always valid and still serves as a useful insertion hint. The method returns the previous `PeerReservation` if one existed, giving callers confirmation that an update (rather than a fresh insert) occurred. The database layer uses an SQL upsert (`ON CONFLICT ... DO UPDATE`) for idempotency, so the in-memory erase-then-insert and the DB operation stay in sync without separate code paths. + +`erase(PublicKey const&)` follows the same return convention: it returns the erased reservation wrapped in `std::optional`, or `std::nullopt` if the key was absent. Database removal via `deletePeerReservation()` only happens if the key was found in memory, avoiding unnecessary DB round-trips. + +`list()` copies the set under lock, releases the lock, then sorts the copy using `operator<` before returning. Sorting outside the lock keeps the critical section short and avoids holding the lock across a potentially non-trivial `std::sort`. The result is a stable, ordered snapshot suitable for RPC responses. + +## Relationships + +- **`ApplicationImp`** owns the singleton `PeerReservationTable` as `peerReservations_`, constructs it with a dedicated `Journal`, calls `load()` during `setup()`, and exposes it via `getPeerReservations()`. +- **`OverlayImpl`** is the only consumer of `contains()`, querying it on every inbound peer handshake. +- **`Wallet.cpp`** (`src/libxrpl/server/`) implements the three free functions (`getPeerReservationTable`, `insertPeerReservation`, `deletePeerReservation`) that bridge SOCI/SQLite — the separation is flagged in a comment as an unfortunate constraint of where the `CREATE TABLE` schema lives. +- **`DatabaseCon`** provides the `checkoutDb()` mechanism for scoped SOCI session access, following the same checkout pattern used elsewhere in rippled's database layer. \ No newline at end of file diff --git a/include/xrpl/core/PerfLog.h.ai.json b/include/xrpl/core/PerfLog.h.ai.json new file mode 100644 index 0000000000..6a5ee7cd94 --- /dev/null +++ b/include/xrpl/core/PerfLog.h.ai.json @@ -0,0 +1,91 @@ +{ + "args": [ + { + "lineno": 65, + "name": "method" + }, + { + "lineno": 65, + "name": "requestId" + }, + { + "lineno": 77, + "name": "type" + }, + { + "lineno": 84, + "name": "dur" + }, + { + "lineno": 84, + "name": "startTime" + }, + { + "lineno": 84, + "name": "instance" + }, + { + "lineno": 109, + "name": "resize" + } + ], + "classes": [ + { + "args": [], + "lineno": 23, + "name": "PerfLog" + }, + { + "args": [], + "lineno": 41, + "name": "PerfLog::Setup" + } + ], + "description": "Defines the PerfLog class and related utilities for performance logging and counters in the XRPL application, including configuration, logging of RPC and job events, and JSON rendering of counters.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/PerfLog.h", + "functions": [ + { + "args": [ + "section", + "configDir" + ], + "lineno": 97, + "name": "setup_PerfLog" + }, + { + "args": [ + "setup", + "app", + "journal", + "signalStop" + ], + "lineno": 100, + "name": "make_PerfLog" + }, + { + "args": [ + "func", + "actionDescription", + "maxDelay", + "journal" + ], + "lineno": 107, + "name": "measureDurationAndLog" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 15, + "name": "beast" + }, + { + "lineno": 19, + "name": "xrpl" + }, + { + "lineno": 21, + "name": "xrpl::perf" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/PerfLog.h.ai.md b/include/xrpl/core/PerfLog.h.ai.md new file mode 100644 index 0000000000..fa9229d748 --- /dev/null +++ b/include/xrpl/core/PerfLog.h.ai.md @@ -0,0 +1,79 @@ +# `include/xrpl/core/PerfLog.h` + +## Role in the System + +`PerfLog` is the performance telemetry interface for the `rippled` node. It provides a centralized, always-on instrumentation layer that tracks two classes of activity: RPC method calls arriving from clients and internal jobs dispatched through the `JobQueue`. The interface is intentionally abstract — the concrete `PerfLogImp` lives in `src/xrpld/perflog/detail/` — which permits a true no-op implementation for contexts where the logging overhead or file I/O is unwanted. + +The node documentation describes this as a singleton that must exist before other `Application` objects are launched. That ordering guarantee matters because call sites in the job queue and RPC handler call into `PerfLog` without null-checking; the object must always be valid. + +## Configuration and Lifecycle + +The nested `Setup` struct captures the two knobs from the `[perf]` section of `xrpld.cfg`: + +- `perfLog` — path to the output file. If empty, the background writer thread is never started and all file I/O is suppressed, but counter tracking still works. +- `logInterval` — how frequently a full JSON snapshot is flushed to disk. Stored as `milliseconds` (rather than seconds) explicitly to support faster test cadences. + +`setup_PerfLog()` parses a raw config `Section` into this struct, resolving relative paths against the config directory. `make_PerfLog()` constructs the concrete `PerfLogImp` via factory, taking a `signalStop` callback that the implementation invokes if it cannot open its log file — a fatal condition that should halt the node rather than silently drop metrics. + +`start()` and `stop()` control the background writer thread. Both have default empty implementations in the base class so that a hypothetical no-op `PerfLog` subclass needs no extra plumbing. + +## Clock Strategy + +The header defines two clock type aliases: `steady_clock` and `system_clock`. The design separates their purposes. `steady_clock` (monotonic) measures elapsed durations — queue wait times, RPC execution times, current job run times. `system_clock` provides wall-clock timestamps for the JSON log entries written to disk. Using `steady_clock` for duration measurement prevents distortions from NTP adjustments, while `system_clock` is required for human-readable timestamps in the output. + +## Two-Track Instrumentation API + +### RPC Tracking + +RPC calls are tracked with a start/finish/error triple, keyed by both method name and a `requestId`: + +``` +rpcStart(method, requestId) // call begins, start time recorded +rpcFinish(method, requestId) // call completed successfully +rpcError(method, requestId) // call completed with an error +``` + +The `requestId` is a `uint64_t` that serves as a correlation key to pair the start event with its end event, allowing the implementation to compute exact per-call duration. The distinction between `rpcFinish` and `rpcError` matters for observability: operators can see the error rate per method separately from the success rate, and both contribute to cumulative duration. + +### Job Queue Tracking + +Job tracking follows a three-event lifecycle with additional context: + +``` +jobQueue(type) // job enters the queue +jobStart(type, dur, startTime, instance) // job begins executing +jobFinish(type, dur, instance) // job completes +``` + +The `instance` parameter identifies which `JobQueue` worker thread is running the job. This maps directly to a slot in a fixed-size `jobs_` vector (sized by `resizeJobs()`), letting the implementation record a per-worker snapshot of what's currently executing without a hash map lookup. `dur` in `jobStart` is the queued duration (time spent waiting); `dur` in `jobFinish` is the running duration. Both are accumulated in aggregate counters. + +`resizeJobs()` must be called when worker threads are added to the `JobQueue`. It extends the `jobs_` vector, filling new slots with `{jtINVALID, steady_time_point()}` sentinel values. The implementation only grows this vector, never shrinks it, avoiding invalidation issues. + +## JSON Output: Counters vs. Current + +Two query methods expose the collected data: + +- `countersJson()` returns aggregate historical counters — totals for each RPC method and job type, broken down by started/finished/errored counts and cumulative duration. Entries for methods/types that were never invoked are omitted to keep output compact. A synthetic `total` entry rolls up all RPC and job activity. + +- `currentJson()` returns a live snapshot of in-flight work — which jobs are currently executing and for how long, and which RPC methods are still pending. This is the data that appears in the `current_activities` field of each periodic log entry. + +The separation matters for different diagnostic use cases: counters answer "how busy has the node been?", while current answers "what is the node doing right now?". + +## Concurrency Model + +The implementation uses a fine-grained locking strategy. The `rpc_` and `jq_` maps are populated once at construction time (pre-threading) and never structurally modified afterward, so no lock is needed to look up a bucket. Each bucket is wrapped in a `Locked` box that bundles the counter value with its own `std::mutex`, allowing independent increment of unrelated method or job type counters. Global mutexes cover only the two cross-bucket structures: `methodsMutex_` for the in-flight RPC map and `jobsMutex_` for the per-worker job vector. + +The background writer thread (`run()`) sleeps on a `condition_variable` until either the log interval expires or `stop_`/`rotate_` flags are set. Log rotation (triggered by `rotate()`, typically on SIGHUP) sets `rotate_ = true` and wakes the thread, which reopens the file before writing the next snapshot. + +## `measureDurationAndLog` Utility + +The header also exposes a standalone template utility outside the `PerfLog` class: + +```cpp +template +auto measureDurationAndLog(Func&& func, std::string const& actionDescription, + std::chrono::duration maxDelay, + beast::Journal const& journal); +``` + +This wraps any callable, measures its wall time with `high_resolution_clock`, and emits a `JLOG(warn)` if execution exceeded `maxDelay`. It is not connected to the `PerfLog` counter infrastructure — it has no aggregation, no persistent state. Call sites include peer message dispatch (`PeerImp.cpp`, threshold 350ms), database session acquisition (`DatabaseCon.h`), inbound ledger acquisition (500ms), and consensus validation ledger lookup (10ms). The pattern suits transient instrumentation points where you want a warning but not a full counter, without needing access to the `PerfLog` singleton. \ No newline at end of file diff --git a/include/xrpl/core/ServiceRegistry.h.ai.json b/include/xrpl/core/ServiceRegistry.h.ai.json new file mode 100644 index 0000000000..9c639401e6 --- /dev/null +++ b/include/xrpl/core/ServiceRegistry.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 56, + "name": "ServiceRegistry" + } + ], + "description": "Defines the ServiceRegistry abstract interface in the xrpl namespace, providing access to various core, protocol, network, storage, ledger, transaction, server, and configuration services for dependency injection in the XRPL application.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/ServiceRegistry.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "NodeStore" + }, + { + "lineno": 13, + "name": "Resource" + }, + { + "lineno": 16, + "name": "perf" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/ServiceRegistry.h.ai.md b/include/xrpl/core/ServiceRegistry.h.ai.md new file mode 100644 index 0000000000..3cd81bfc1b --- /dev/null +++ b/include/xrpl/core/ServiceRegistry.h.ai.md @@ -0,0 +1,49 @@ +# `ServiceRegistry.h` — Dependency Injection Interface for the XRPL Application Layer + +## Role and Motivation + +`ServiceRegistry` solves a pervasive coupling problem in the XRPL codebase: historically, almost every component that needed access to any service held a reference to `Application`, the monolithic top-level object. This created a situation where a component needing only `LedgerMaster` and `JobQueue` would carry a full `Application&` dependency — dragging along lifecycle management, startup/shutdown logic, and every other subsystem as implicit dependencies. + +`ServiceRegistry` is the first step in decomposing that monolith. It acts as a pure service-locator interface, separating service *access* from application *lifecycle management*. Components that only need to look up services can now depend on `ServiceRegistry&` instead of `Application&`, making their actual dependencies more explicit and their testability significantly higher. The comment `// This is temporary until we migrate all code to use ServiceRegistry` and the `getApp()` escape hatch at the bottom of the class confirm this is a live, in-progress migration rather than a finished design. + +## Design: Why a Pure Abstract Interface? + +The class has no data members, all methods are pure virtual, and the constructor and destructor are defaulted. This is deliberate for two reasons: + +**Testability.** Any component that takes a `ServiceRegistry&` can be tested against a stub or mock implementation without instantiating the full XRPL application stack — no I/O threads, no databases, no network layer. The transaction processing pipeline has already migrated to this model: `Transactor`, `ApplyContext`, and the `apply()`/`preclaim()`/`doApply()` free functions in `include/xrpl/tx/` all take `ServiceRegistry&`, not `Application&`. + +**Decoupled contract.** The concrete implementation lives in `Application` (which inherits from `ServiceRegistry` alongside `beast::PropertyStream::Source`), but the interface contract is stable and documented independently of the sprawling `Application` implementation details. Any future reimplementation or test double only needs to satisfy this interface. + +The alternative — passing individual service references — would either cause constructor explosion or produce ad-hoc grouping structs that recreate the same coupling problem at a smaller scale. + +## Forward Declarations and Type Aliases + +The header deliberately uses forward declarations for all of its approximately 30 referenced types rather than including their headers. This is architecturally critical: `ServiceRegistry.h` is pulled in by a very large number of translation units, and including full headers for `LedgerMaster`, `Overlay`, `NetworkOPs`, and so on would create enormous transitive include chains. Forward declarations give callers the type name needed to hold a reference while deferring the full definition to the point of use. + +Two type aliases are defined inline rather than delegated to another header: `CachedSLEs` (`TaggedCache`) and `NodeCache` (`TaggedCache`). These are shared cache types accessed by multiple subsystems, and their full template signatures must be visible to any caller of `getCachedSLEs()` or `getTempNodeCache()`. Defining them here avoids duplication and keeps the interface self-consistent. For the same reason, the full `TaggedCache` template declaration is re-stated here even though it is also declared in ``. + +## Service Groupings + +The methods are organized into six logical categories reflecting the system's layered architecture: + +**Core infrastructure** — `getJobQueue()`, `getTimeKeeper()`, `getNodeFamily()`, `getTempNodeCache()`, `getCachedSLEs()`, `getNetworkIDService()`, `getCollectorManager()`. These are cross-cutting services that most subsystems depend on directly. + +**Protocol and validation** — `getAmendmentTable()`, `getHashRouter()`, `getFeeTrack()`, `getLoadManager()`, `getValidations()`, `getValidators()`, `getValidatorSites()`, `getValidatorManifests()`, `getPublisherManifests()`. Consensus and validator management, including separate caches for validator vs. publisher manifests. + +**Network** — `getOverlay()`, `getCluster()`, `getPeerReservations()`, `getResourceManager()`. The peer-to-peer layer and its resource accounting. + +**Storage** — `getNodeStore()`, `getSHAMapStore()`, `getRelationalDatabase()`. The persistence layer, with a clean separation between the key-value node store, the SHAMap-level storage management, and the relational (SQL) database. + +**Ledger** — `getInboundLedgers()`, `getInboundTransactions()`, `getAcceptedLedgerCache()`, `getLedgerMaster()`, `getLedgerCleaner()`, `getLedgerReplayer()`, `getPendingSaves()`, `getOpenLedger()`. The open ledger accessor is provided in both mutable and `const` overloads — the only method pair with this distinction. This reflects that open ledger state is read far more often than it is mutated, and const-correctness here allows the compiler to enforce that separation at call sites. + +**Transaction and server** — `getOPs()`, `getOrderBookDB()`, `getMasterTransaction()`, `getTxQ()`, `getPathRequestManager()`, `getServerHandler()`, `getPerfLog()`. Transaction processing and the RPC/WebSocket server. + +The final group of utility methods — `isStopping()`, `getJournal()`, `getIOContext()`, `getLogs()`, `getTrapTxID()`, `getWalletDB()` — provide cross-cutting infrastructure that does not fit cleanly into a single subsystem. + +## The `getApp()` Escape Hatch + +The `getApp()` method returns a reference to the underlying `Application` object and is explicitly marked temporary with a comment directing future engineers to remove it once migration is complete. This is an honest acknowledgment that the refactor is incremental. Rather than forcing a big-bang migration or introducing subtle bugs by prematurely severing `Application` access, the design provides a controlled, visible escape hatch. The technical debt is explicit and trackable, which is the correct engineering trade-off during a large incremental refactor. + +## Relationship to Sibling Files and Callers + +Within `include/xrpl/core/`, `ServiceRegistry.h` is the aggregation point for many of the other components defined there. `JobQueue`, `HashRouter`, `NetworkIDService`, `PeerReservationTable`, and `PerfLog` are all forward-declared or referenced here, with concrete definitions in sibling headers. `ServiceRegistry` is the interface through which all of those are accessed by the broader application layer, making it the central hub of the dependency injection architecture for this module. Beyond the core module, the transaction processing pipeline (`Transactor`, `ApplyContext`, `apply`, `preclaim`, `doApply`) and subsystems like `DatabaseCon` checkpointing and overlay slot management have already adopted `ServiceRegistry&` in their interfaces, demonstrating the migration path that all remaining `Application&` callers are expected to follow. \ No newline at end of file diff --git a/include/xrpl/core/StartUpType.h.ai.json b/include/xrpl/core/StartUpType.h.ai.json new file mode 100644 index 0000000000..d9c1f1680f --- /dev/null +++ b/include/xrpl/core/StartUpType.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 11, + "name": "os" + }, + { + "lineno": 11, + "name": "type" + } + ], + "classes": [], + "description": "Defines the StartUpType enum class and an output stream operator for it within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/StartUpType.h", + "functions": [ + { + "args": [ + "os", + "type" + ], + "lineno": 10, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/StartUpType.h.ai.md b/include/xrpl/core/StartUpType.h.ai.md new file mode 100644 index 0000000000..7e4a08b517 --- /dev/null +++ b/include/xrpl/core/StartUpType.h.ai.md @@ -0,0 +1,30 @@ +# `include/xrpl/core/StartUpType.h` + +## Role in the System + +This header defines the `StartUpType` scoped enumeration, which encodes how a `rippled` node should initialize its ledger state when it comes online. It is a small but structurally important piece of configuration: the value selected here drives branching logic in both the application bootstrap sequence (`Application.cpp`) and database-connection setup (`DatabaseCon.h`), making it a shared vocabulary type that must be visible to multiple layers of the stack without pulling in heavyweight dependencies. + +## The Enum Values + +`StartUpType` declares six mutually exclusive startup modes: + +- **`Normal`** — The default (`Config::START_UP` is initialized to this value). The node resumes from whatever ledger state is already persisted in its local databases, then syncs with the network as usual. +- **`Fresh`** — Triggered by the `--start` CLI flag. The node creates a brand-new genesis ledger, activating all currently desired amendments immediately. This is used when bootstrapping a new private network from scratch. +- **`Load`** — Triggered by `--ledger ` or `--load` / `FAST_LOAD`. The node loads a specific ledger by sequence number or hash from its local database and begins from there. +- **`LoadFile`** — Triggered by `--ledgerfile `. Similar to `Load`, but reads ledger state from an external file path rather than the internal database. +- **`Replay`** — Triggered by `--ledger ` combined with `--replay`. The node loads the specified ledger and replays its transactions, optionally with a transaction trap (`--trap_tx_hash`) for debugging. +- **`Network`** — Triggered by `--net`. The node requests the current ledger directly from network peers rather than relying on any local state. The comment in `Application.cpp` notes this "should probably become the default once we have a stable network." + +## Design Decisions + +The `operator<<` overload streams the enum as its raw underlying integer rather than a human-readable name. This is intentional for compactness in log output — the call site in `Application.cpp` is a debug-level log line (`JLOG(m_journal.debug()) << "startUp: " << startUp`), where a numeric value is acceptable. The implementation uses `static_cast>(type)`, which is the idiomatic, zero-overhead way to expose an enum's numeric identity without coupling to any string table. + +The `#include ` (rather than ``) keeps the header lightweight: `iosfwd` only provides the forward declaration of `std::ostream`, which is sufficient for the `operator<<` signature. The full `` definition is deferred to translation units, avoiding unnecessary header bloat in every file that includes `Config.h` or `DatabaseCon.h`. + +## Impact on Database Setup + +One non-obvious consequence of `StartUpType` appears in `DatabaseCon.h`. When the node is running in standalone mode, `DatabaseCon`'s constructor decides whether to use an ephemeral in-memory SQLite database or a real on-disk file. The rule is: standalone + any mode *except* `Load`, `LoadFile`, or `Replay` → use a temporary (in-memory) database; otherwise use the configured data directory. This means that even in standalone mode, if you are loading or replaying a historical ledger you need durable storage, so the on-disk path is kept. The enum values thus implicitly categorize into "needs persistent storage" (`Load`, `LoadFile`, `Replay`) versus "ephemeral-compatible" (`Fresh`, `Normal`, `Network`). + +## Relationship to Config + +`Config.h` holds the `START_UP` member of type `StartUpType`, defaulted to `StartUpType::Normal`. The CLI parsing logic in `Main.cpp` mutates this field based on command-line flags, and `ApplicationImp::setup()` reads it once to decide the initial ledger-loading strategy — either calling `startGenesisLedger()`, `loadOldLedger()`, or `setNeedNetworkLedger()` on the network operations subsystem. The enum thus cleanly separates the concern of *which mode was requested* (owned by `Config`) from the concern of *what to do about it* (owned by `Application`). \ No newline at end of file diff --git a/include/xrpl/core/detail/Workers.h.ai.json b/include/xrpl/core/detail/Workers.h.ai.json new file mode 100644 index 0000000000..b61e7df6cd --- /dev/null +++ b/include/xrpl/core/detail/Workers.h.ai.json @@ -0,0 +1,141 @@ +{ + "args": [ + { + "lineno": 77, + "name": "callback" + }, + { + "lineno": 78, + "name": "perfLog" + }, + { + "lineno": 79, + "name": "threadNames" + }, + { + "lineno": 80, + "name": "numberOfThreads" + }, + { + "lineno": 61, + "name": "instance" + }, + { + "lineno": 94, + "name": "numberOfThreads" + }, + { + "lineno": 142, + "name": "workers" + }, + { + "lineno": 142, + "name": "threadName" + }, + { + "lineno": 142, + "name": "instance" + }, + { + "lineno": 167, + "name": "stack" + } + ], + "classes": [ + { + "args": [ + "callback", + "perfLog", + "threadNames", + "numberOfThreads" + ], + "lineno": 36, + "name": "Workers" + }, + { + "args": [], + "lineno": 48, + "name": "Callback" + }, + { + "args": [], + "lineno": 132, + "name": "PausedTag" + }, + { + "args": [ + "workers", + "threadName", + "instance" + ], + "lineno": 139, + "name": "Worker" + } + ], + "description": "Defines a thread pool class 'Workers' for managing worker threads that execute tasks via a callback interface, with support for pausing, resuming, and dynamically adjusting the number of threads.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/detail/Workers.h", + "functions": [ + { + "args": [ + "instance" + ], + "lineno": 61, + "name": "processTask" + }, + { + "args": [], + "lineno": 86, + "name": "getNumberOfThreads" + }, + { + "args": [ + "numberOfThreads" + ], + "lineno": 94, + "name": "setNumberOfThreads" + }, + { + "args": [], + "lineno": 104, + "name": "stop" + }, + { + "args": [], + "lineno": 115, + "name": "addTask" + }, + { + "args": [], + "lineno": 123, + "name": "numberOfCurrentlyRunningTasks" + }, + { + "args": [], + "lineno": 154, + "name": "notify" + }, + { + "args": [], + "lineno": 158, + "name": "run" + }, + { + "args": [ + "stack" + ], + "lineno": 167, + "name": "deleteWorkers" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 13, + "name": "perf" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/detail/Workers.h.ai.md b/include/xrpl/core/detail/Workers.h.ai.md new file mode 100644 index 0000000000..8375e3501e --- /dev/null +++ b/include/xrpl/core/detail/Workers.h.ai.md @@ -0,0 +1,47 @@ +# `Workers.h` — Thread Pool with Pause/Resume Lifecycle + +## Role in the System + +`Workers` is the XRPL node's general-purpose thread pool. It sits inside `xrpl::core::detail` and is consumed by the job queue subsystem to dispatch work across a fixed or dynamically-adjusted set of OS threads. The design deliberately separates *task ownership* from *thread lifecycle*: callers never manage threads directly; they call `addTask()` and implement `Callback::processTask()`. The pool handles all the creation, pausing, reuse, and teardown of threads. + +## Callback Contract + +The `Callback` pure interface has a single method: `processTask(int instance)`. The `instance` parameter is the index of the worker thread that is executing the call, which allows callbacks to index into per-thread structures (such as `perf::PerfLog` job slots) without locking. The contract is strict: each call to `addTask()` produces exactly one call to `processTask()`, so the callback should process exactly one unit of work per invocation and return promptly. Work enqueued via `addTask()` is not lost even if the pool is shrunk, because the semaphore retains its count. + +## Three-State Thread Lifecycle + +Each `Worker` exists in one of three states: + +- **Active/Idle** — the thread is inside its task loop, either executing `processTask` or blocked on `m_semaphore.wait()` waiting for the next task. +- **Paused** — the thread has exited its task loop and is sleeping on its own per-worker `wakeup_` condition variable. The worker still exists as an OS thread; it is merely dormant. + +No worker thread is ever destroyed until `Workers` itself is destroyed. Reducing the thread count via `setNumberOfThreads()` pauses surplus workers; enlarging it later re-activates them from the `m_paused` stack. This worker-reuse strategy avoids the overhead of creating and joining OS threads on every resize, which matters because the job queue can resize frequently. + +## The Semaphore as a Unified Signal Channel + +`m_semaphore` (a `basic_semaphore` wrapping a mutex and condition variable — a temporary stand-in for `std::counting_semaphore` which has known bugs in GCC < 16 and Clang < 19.1) serves double duty. A call to `addTask()` does nothing but call `m_semaphore.notify()`, incrementing its count. Pause requests issued by `setNumberOfThreads()` also call `m_semaphore.notify()`, having first incremented `m_pauseCount`. The worker's inner loop cannot distinguish the two cases until after it has successfully acquired the semaphore — at that point it checks the atomic `m_pauseCount`. If `m_pauseCount > 0` and the decrement succeeds (guard against over-decrement by concurrent workers racing on the same pause signal), the worker breaks out to the pause path. Otherwise it assumes a real task and calls `processTask`. + +This design is elegant: the semaphore is the single point of activation for all worker wake-ups, and the `m_pauseCount` counter overlays control messages onto the same queue without a separate channel. + +## Dual LockFreeStack Membership via Tag Types + +`Worker` inherits from two instantiations of `beast::LockFreeStack::Node` simultaneously, distinguished by the empty `PausedTag` type: + +```cpp +class Worker : public beast::LockFreeStack::Node, + public beast::LockFreeStack::Node +``` + +`m_everyone` (untagged) tracks every worker ever created; `m_paused` (tagged `PausedTag`) tracks only those currently dormant. This two-stack pattern enables `deleteWorkers` to iterate all workers at teardown and `setNumberOfThreads` to pop reusable paused workers for cheap reactivation, all with lock-free compare-exchange semantics. The ABA hazard normally associated with lock-free stacks is avoided here because popped workers are either immediately re-pushed by the worker thread itself (into `m_paused`) or deleted — they are never re-pushed into a stack they just left without a full lifecycle transition. + +## `stop()` and the Double-Barrier Shutdown + +`stop()` calls `setNumberOfThreads(0)` to issue pause signals for every active thread, then waits on `m_cv` under `m_mut` for the predicate `m_allPaused && numberOfCurrentlyRunningTasks() == 0`. Both conditions are necessary and distinct. `m_allPaused` is set under `m_mut` by the last worker to leave the active loop, so the condition variable wait never misses it. However, `m_runningTaskCount` is an atomic updated *outside* `m_mut`; a worker could decrement `m_activeCount` to zero while still inside the tail of `processTask`. The combined predicate prevents `stop()` from returning while a task is still running. The task completion path also acquires `m_mut` before `notify_all()`, serialising against the predicate evaluation inside `stop()`'s `cv.wait()` and eliminating the lost-wakeup race. + +The destructor then calls `deleteWorkers(m_everyone)`, which pops each `Worker` from the untagged stack and `delete`s it. `~Worker` sets `shouldExit_ = true`, increments `wakeCount_`, signals `wakeup_`, then joins the thread — a clean blocking teardown for each worker. + +## Thread Safety Notes + +`addTask()` is explicitly documented as thread-safe (it only calls `m_semaphore.notify()`, which is internally locked). `getNumberOfThreads()`, `setNumberOfThreads()`, and `stop()` are *not* thread-safe and must be called from a single controlling thread. `numberOfCurrentlyRunningTasks()` is a bare atomic load and is safe but advisory only, as the comment warns. + +The thread name is reset at the top of every iteration of the worker's inner loop via `beast::setCurrentThreadName(threadName_)`, because the callback is permitted to change it for debugging purposes. Paused threads rename themselves `"(ThreadName)"` to make them visually distinct in debugger thread listings. \ No newline at end of file diff --git a/include/xrpl/core/detail/semaphore.h.ai.json b/include/xrpl/core/detail/semaphore.h.ai.json new file mode 100644 index 0000000000..dd9cc93fe3 --- /dev/null +++ b/include/xrpl/core/detail/semaphore.h.ai.json @@ -0,0 +1,43 @@ +{ + "args": [ + { + "lineno": 23, + "name": "count" + } + ], + "classes": [ + { + "args": [ + "size_type count = 0" + ], + "lineno": 18, + "name": "basic_semaphore" + } + ], + "description": "Implements a basic semaphore class template for synchronization, as a workaround for compiler bugs in std::counting_semaphore. Provides notify, wait, and try_wait methods. Intended for removal once minimum compiler versions are updated.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/core/detail/semaphore.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "notify" + }, + { + "args": [], + "lineno": 45, + "name": "wait" + }, + { + "args": [], + "lineno": 54, + "name": "try_wait" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/core/detail/semaphore.h.ai.md b/include/xrpl/core/detail/semaphore.h.ai.md new file mode 100644 index 0000000000..16e60c55aa --- /dev/null +++ b/include/xrpl/core/detail/semaphore.h.ai.md @@ -0,0 +1,27 @@ +# `include/xrpl/core/detail/semaphore.h` + +## Purpose + +This file provides `xrpl::basic_semaphore`, a counting semaphore built on a mutex and condition variable. It exists exclusively as a compiler-bug workaround: both GCC and Clang shipped broken implementations of `std::counting_semaphore` (GCC PR 104928 and LLVM PR 79265), and rippled's supported compiler range includes affected versions — GCC up through 14.x and Clang before 19.1. The file carries an explicit `TODO` to delete it and migrate to `std::counting_semaphore` once the minimum compiler floor advances past GCC 16 or Clang 19.1. + +## Design + +`basic_semaphore` is a class template parameterized on `Mutex` and `CondVar`, allowing the synchronization primitives to be injected for testing. The `semaphore` type alias fixes those to `std::mutex` and `std::condition_variable`, which is what all production code uses. + +The internal state is just three members: a mutex, a condition variable, and a `std::size_t` counter. All three operations follow the standard monitor pattern: + +- `notify()` locks the mutex, increments the count, then calls `notify_one()` so exactly one blocked `wait()` caller is woken. +- `wait()` acquires a `std::unique_lock`, then loops on `m_cond.wait(lock)` while the count is zero. The `while` loop (rather than a plain `if`) guards against spurious wakeups, which condition variables are permitted to deliver. Once the count is positive the method decrements it and returns. +- `try_wait()` takes a `std::lock_guard`, checks the count, and either decrements and returns `true` or returns `false` immediately without blocking. + +## Role in the Thread Pool + +The only consumer in this codebase is `Workers`, the thread-pool implementation in `include/xrpl/core/detail/Workers.h`. `Workers` holds a `semaphore m_semaphore` that acts as the work queue's depth counter: every call to `Workers::addTask()` calls `m_semaphore.notify()`, and every worker thread blocks on `m_semaphore.wait()` when idle. The same `notify()` path is also used to deliver "pause" tokens — signaling a worker that it should suspend itself rather than process a real task. This single semaphore therefore mediates both task dispatch and thread lifecycle management, making its correctness critical to the stability of the entire job-processing system. + +## Why Not `std::binary_semaphore` or a Raw `condition_variable`? + +A counting semaphore lets the producer race ahead of consumers: if `addTask()` is called three times before any worker wakes, the count becomes three and three workers will each claim one task without any signals being lost. A raw condition variable without the count would silently drop signals if no thread was waiting at notification time, requiring an additional queue or flag. The counting semantic is exactly what the thread pool needs to avoid missed wakeups under bursty load. + +## Transient Nature + +The header lives in `include/xrpl/core/detail/`, the `detail` subdirectory signalling that it is an implementation-internal facility not intended for external consumers. It is a deliberate stop-gap, not a permanent abstraction. Once the compiler floor is raised the file should be removed, all inclusion sites updated to ``, and occurrences of `xrpl::semaphore` replaced with `std::counting_semaphore<...>`. \ No newline at end of file diff --git a/include/xrpl/crypto/RFC1751.h.ai.json b/include/xrpl/crypto/RFC1751.h.ai.json new file mode 100644 index 0000000000..c6119c14ae --- /dev/null +++ b/include/xrpl/crypto/RFC1751.h.ai.json @@ -0,0 +1,181 @@ +{ + "args": [ + { + "lineno": 9, + "name": "strKey" + }, + { + "lineno": 9, + "name": "strHuman" + }, + { + "lineno": 12, + "name": "strHuman" + }, + { + "lineno": 12, + "name": "strKey" + }, + { + "lineno": 21, + "name": "blob" + }, + { + "lineno": 21, + "name": "bytes" + }, + { + "lineno": 28, + "name": "s" + }, + { + "lineno": 28, + "name": "start" + }, + { + "lineno": 28, + "name": "length" + }, + { + "lineno": 30, + "name": "strHuman" + }, + { + "lineno": 30, + "name": "strData" + }, + { + "lineno": 32, + "name": "s" + }, + { + "lineno": 32, + "name": "x" + }, + { + "lineno": 32, + "name": "start" + }, + { + "lineno": 32, + "name": "length" + }, + { + "lineno": 34, + "name": "strWord" + }, + { + "lineno": 36, + "name": "strWord" + }, + { + "lineno": 36, + "name": "iMin" + }, + { + "lineno": 36, + "name": "iMax" + }, + { + "lineno": 38, + "name": "strData" + }, + { + "lineno": 38, + "name": "vsHuman" + } + ], + "classes": [ + { + "args": [], + "lineno": 6, + "name": "RFC1751" + } + ], + "description": "Header file defining the RFC1751 class in the xrpl namespace, providing methods for converting between English words and cryptographic keys, as well as utility functions for word selection and manipulation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/crypto/RFC1751.h", + "functions": [ + { + "args": [ + "strKey", + "strHuman" + ], + "lineno": 9, + "name": "getKeyFromEnglish" + }, + { + "args": [ + "strHuman", + "strKey" + ], + "lineno": 12, + "name": "getEnglishFromKey" + }, + { + "args": [ + "blob", + "bytes" + ], + "lineno": 21, + "name": "getWordFromBlob" + }, + { + "args": [ + "s", + "start", + "length" + ], + "lineno": 28, + "name": "extract" + }, + { + "args": [ + "strHuman", + "strData" + ], + "lineno": 30, + "name": "btoe" + }, + { + "args": [ + "s", + "x", + "start", + "length" + ], + "lineno": 32, + "name": "insert" + }, + { + "args": [ + "strWord" + ], + "lineno": 34, + "name": "standard" + }, + { + "args": [ + "strWord", + "iMin", + "iMax" + ], + "lineno": 36, + "name": "wsrch" + }, + { + "args": [ + "strData", + "vsHuman" + ], + "lineno": 38, + "name": "etob" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/crypto/RFC1751.h.ai.md b/include/xrpl/crypto/RFC1751.h.ai.md new file mode 100644 index 0000000000..9f31b34324 --- /dev/null +++ b/include/xrpl/crypto/RFC1751.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/crypto/RFC1751.h` + +This header declares the `RFC1751` class, which wraps an XRPL-adapted implementation of [RFC 1751](https://datatracker.ietf.org/doc/html/rfc1751) — "A Convention for Human-Readable 128-bit Keys." The original RFC was designed to solve the usability problem of 64-bit one-time-password keys by encoding binary data as short English words. The XRPL adaptation extends it to handle 128-bit keys (as two consecutive 64-bit blocks) to cover the full seed space used for wallet key derivation. + +## Core Design: The 2048-Word Dictionary + +The implementation anchors on a static array `s_dictionary[2048]` containing exactly 2^11 words. This is not arbitrary: each word encodes precisely 11 bits of information. For a 64-bit data block, the scheme uses 6 words (6 × 11 = 66 bits) where 64 bits carry data and 2 bits carry parity. The dictionary is sorted and structurally split at index 571: words at indices 0–570 have 1–3 characters, and words 571–2047 are all exactly 4 characters. This split is exploited in `wsrch()`, which restricts the binary search range based on the word length of the input, halving the search space for 4-character words. + +## Public API + +**`getEnglishFromKey(strHuman, strKey)`** converts a 16-byte (128-bit) binary key into 12 space-separated English words by calling `btoe()` twice on consecutive 8-byte halves, then joining the results. Encoding cannot fail for valid input, so no return code is needed. + +**`getKeyFromEnglish(strKey, strHuman)`** is the inverse: it parses a 12-word string, splits it into two groups of 6, and calls `etob()` on each group. The return code explicitly distinguishes four outcomes: +- `1` — success +- `0` — a word was not found in the dictionary +- `-1` — malformed input (wrong word count, word too long) +- `-2` — words are valid but the 2-bit parity check fails + +This asymmetry in error handling between encoder and decoder is intentional. Encoding is deterministic and lossless; decoding must validate user-supplied strings that may contain typos or truncation. + +**`getWordFromBlob(blob, bytes)`** is a separate utility that maps arbitrary binary data to a single dictionary word. It applies the [Jenkins one-at-a-time hash](https://en.wikipedia.org/wiki/Jenkins_hash_function#one-at-a-time) to the input bytes and indexes into `s_dictionary` using the hash modulo 2048. The header comment is candid that this is not cryptographically secure — it is purely for identification purposes. In practice, `NetworkOPs.cpp` uses it to derive a stable short label (`shroudedHostId`) from the node's public key, giving each validator a consistent 4-character human-readable tag in log output. + +## Private Bit-Manipulation Internals + +The low-level private methods `extract()` and `insert()` are the bridge between byte arrays and dictionary indices. Both operate on arbitrary bit offsets and lengths (up to 11 bits), working across byte boundaries by loading 2–3 adjacent bytes and masking. They enforce correctness via `XRPL_ASSERT` rather than exceptions, treating out-of-range bit positions as programming errors. + +`btoe()` (binary-to-English) appends a 9th byte to the 8-byte block to hold parity, computed by summing all 32 pairs of bits from the 64-bit data, then uses `extract()` at six 11-bit offsets to build six dictionary indices. + +`etob()` (English-to-binary) reverses the process: it standardizes each word via `standard()` (uppercases letters, substitutes visually ambiguous characters: `1`→`L`, `0`→`O`, `5`→`S`), binary-searches for each word's index, and uses `insert()` to pack 11-bit values into the output buffer. After assembling all 6 words, it recomputes the parity and compares it to the two bits stored at offset 64, rejecting the input with `-2` on mismatch. + +## Usage in XRPL + +The class is used in two distinct contexts: + +1. **Seed encoding** (`src/libxrpl/protocol/Seed.cpp`): Human-readable mnemonic representation of wallet seeds. A 128-bit `Seed` can be serialized to 12 English words via `getEnglishFromKey()` and deserialized back via `getKeyFromEnglish()`. The seed bytes are reversed before encoding to match the expected endianness convention. + +2. **Node identity labeling** (`src/xrpld/app/misc/NetworkOPs.cpp`): A one-time static computation converts the node's public key into a single dictionary word using `getWordFromBlob()`, stored as `shroudedHostId`. This word appears in ledger-state log messages to identify the reporting node without exposing its full public key. + +All methods are static, making `RFC1751` a pure stateless utility namespace — instantiation is never needed or intended. \ No newline at end of file diff --git a/include/xrpl/crypto/csprng.h.ai.json b/include/xrpl/crypto/csprng.h.ai.json new file mode 100644 index 0000000000..31e41b1c9f --- /dev/null +++ b/include/xrpl/crypto/csprng.h.ai.json @@ -0,0 +1,70 @@ +{ + "args": [ + { + "lineno": 27, + "name": "buffer" + }, + { + "lineno": 27, + "name": "count" + }, + { + "lineno": 35, + "name": "ptr" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "csprng_engine" + } + ], + "description": "Defines a cryptographically secure, thread-safe random number engine (csprng_engine) for use in cryptographic routines, including entropy mixing and random data generation, within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/crypto/csprng.h", + "functions": [ + { + "args": [ + "buffer", + "count" + ], + "lineno": 27, + "name": "mix_entropy" + }, + { + "args": [], + "lineno": 31, + "name": "operator()" + }, + { + "args": [ + "ptr", + "count" + ], + "lineno": 35, + "name": "operator()" + }, + { + "args": [], + "lineno": 39, + "name": "min" + }, + { + "args": [], + "lineno": 45, + "name": "max" + }, + { + "args": [], + "lineno": 56, + "name": "crypto_prng" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/crypto/csprng.h.ai.md b/include/xrpl/crypto/csprng.h.ai.md new file mode 100644 index 0000000000..8fc7c4d2a1 --- /dev/null +++ b/include/xrpl/crypto/csprng.h.ai.md @@ -0,0 +1,51 @@ +# `include/xrpl/crypto/csprng.h` — Cryptographically Secure PRNG Interface + +## Role in the System + +This header defines the engine that feeds every piece of key material in the XRP Ledger: random wallet seeds, secret keys, nonces, and session identifiers all flow through `csprng_engine`. It exists as a thin, type-safe C++ wrapper around OpenSSL's `RAND_bytes` so the rest of the codebase never has to touch OpenSSL directly for randomness and always gets thread safety and standard-library compatibility for free. + +## `csprng_engine` Design + +`csprng_engine` satisfies the C++ *UniformRandomNumberEngine* named requirement. This means it exposes `result_type`, `operator()()`, and the `constexpr` `min()`/`max()` pair. That conformance lets the engine be passed directly to standard-library facilities like `std::uniform_int_distribution` or `beast::rngfill`, which is exactly how callers such as `randomSeed()` and `randomSecretKey()` use it in `Seed.cpp` and `SecretKey.cpp`. + +Copy and move construction and assignment are all explicitly deleted. The engine holds a `std::mutex`, is backed by a global OpenSSL state, and is exposed as a singleton — copying it would produce a second object with the same mutex but no coherent relationship to the underlying PRNG state. Deleting these operations makes that misuse impossible at compile time. + +## Construction and Teardown + +The constructor calls `RAND_poll()`, which on most platforms causes OpenSSL to harvest seed material from the OS (e.g., `/dev/urandom` on Linux, `CryptGenRandom` on Windows). The comment in the source notes this is "not strictly necessary" because OpenSSL auto-seeds lazily, but calling it eagerly surfaces seeding failures at startup rather than at first use. Any failure throws `std::runtime_error`. + +The destructor conditionally calls `RAND_cleanup()` only for OpenSSL versions older than 1.1.0, where cleanup is needed to release internal state. On modern OpenSSL the call was removed because the library manages cleanup internally through `atexit`. + +## Thread Safety + +Thread safety is version-conditioned at compile time: + +```cpp +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || !defined(OPENSSL_THREADS) + std::lock_guard lock(mutex_); +#endif +``` + +OpenSSL 1.1.0 made `RAND_bytes` internally thread-safe when compiled with thread support. On a modern build the mutex acquisition in `operator()(void*, size_t)` is entirely elided — the only overhead is the `RAND_bytes` call itself. The mutex is still present in the class for `mix_entropy`, which always holds it around `RAND_add`. This split avoids serializing the hot path (generating bytes) while still protecting the less-frequent entropy mixing path, which modifies shared pool state. + +## Entropy Mixing + +`mix_entropy(void* buffer, std::size_t count)` serves two purposes: it is called during initialization to stir in extra OS randomness, and it can be called by application code that has obtained high-quality entropy from an external source (hardware RNG, user input, etc.). + +The implementation allocates 128 values from `std::random_device` on the stack, then passes them to `RAND_add` with an entropy estimate of **zero**. This conservative choice is deliberate — on some platforms `std::random_device` may fall back to a PRNG, so claiming zero entropy ensures OpenSSL never raises its internal entropy-satisfied threshold based on potentially weak input. Real entropy is mixed in without degrading OpenSSL's accounting. The caller-supplied `buffer` is also added with a zero entropy claim for the same reason. + +## `crypto_prng()` Singleton + +```cpp +csprng_engine& crypto_prng(); +``` + +The free function returns a reference to a `static csprng_engine` instance. Meyers-singleton initialization is guaranteed to be thread-safe by the C++11 standard, so the first call from any thread safely constructs the engine exactly once. Returning a reference (not a value) ensures every caller shares the same OpenSSL RNG pool and the same mutex, preserving the thread-safety guarantees. Callers should never attempt to copy or store the engine by value — the deleted copy/move operations prevent this. + +## Failure Modes + +If `RAND_bytes` returns anything other than 1, the engine throws `std::runtime_error("CSPRNG: Insufficient entropy")`. This is an unrecoverable condition: generating cryptographic material from an entropy-exhausted pool would be a security failure, so aborting via an exception is the correct response. Callers that generate keys (e.g., `randomSecretKey`) do not catch this exception, allowing it to propagate and halt the operation. + +## Relationship to the Crypto Module + +Alongside `secure_erase.h` and `RFC1751.h`, this header forms the foundational crypto utilities layer. The pattern used throughout the codebase — generate into a local buffer via `crypto_prng()`, construct the key object, then immediately call `secure_erase` on the buffer — illustrates the intended lifecycle: `csprng_engine` produces the raw entropy, and `secure_erase` ensures it does not linger in memory after use. \ No newline at end of file diff --git a/include/xrpl/crypto/secure_erase.h.ai.json b/include/xrpl/crypto/secure_erase.h.ai.json new file mode 100644 index 0000000000..0705df295f --- /dev/null +++ b/include/xrpl/crypto/secure_erase.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 18, + "name": "dest" + }, + { + "lineno": 18, + "name": "bytes" + } + ], + "classes": [], + "description": "Provides a function to securely erase a block of memory, attempting to prevent compiler optimizations from skipping the memory clearing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/crypto/secure_erase.h", + "functions": [ + { + "args": [ + "dest", + "bytes" + ], + "lineno": 18, + "name": "secure_erase" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/crypto/secure_erase.h.ai.md b/include/xrpl/crypto/secure_erase.h.ai.md new file mode 100644 index 0000000000..6f19f3e6d4 --- /dev/null +++ b/include/xrpl/crypto/secure_erase.h.ai.md @@ -0,0 +1,23 @@ +# `include/xrpl/crypto/secure_erase.h` + +This header declares a single function, `secure_erase(void* dest, std::size_t bytes)`, that exists to solve a specific and subtle problem in cryptographic software: **compiler-optimized dead-store elimination silently removing memory-clearing code**. + +## The Problem It Solves + +When sensitive key material — private keys, seeds, derived intermediates — is held in a local buffer, good practice demands that buffer be zeroed before it goes out of scope. A plain `memset(buf, 0, size)` is the obvious approach, but a conforming C++ compiler is allowed to remove any write to memory that is never subsequently read. Since a zeroing `memset` right before a `return` or end-of-scope is definitionally a dead store, optimizers routinely eliminate it. The result is that sensitive bytes linger on the stack or heap far longer than the programmer intended, potentially surfacing in crash dumps, `swap` pages, or cold-boot attacks. + +The function declaration intentionally provides no inline body — it lives in a separate translation unit (`src/libxrpl/crypto/secure_erase.cpp`). This alone forces the compiler to treat the call as an opaque side effect, but the implementation goes further by delegating to `OPENSSL_cleanse()`, OpenSSL's purpose-built routine for this exact scenario. `OPENSSL_cleanse` uses platform-specific techniques (volatile pointer casts, memory barriers, or processor-level serialization depending on target) specifically designed to survive optimization passes that would kill a naive `memset`. + +The header comment is notably candid: it says the function "attempts to" clear memory and explicitly acknowledges that register contents, CPU caches, and other micro-architectural artifacts are outside its reach. This is a reference to Colin Percival's two-part 2014 analysis (linked in the comment), which established both the right technique for beating dead-store elimination *and* the uncomfortable truth that zeroing RAM is never a complete solution. + +## Usage Patterns in the Codebase + +`secure_erase` appears consistently in the two most security-sensitive files in the crypto layer: `SecretKey.cpp` and `Seed.cpp`. + +In both files, the pattern repeats in two forms. First, in destructors: `SecretKey::~SecretKey()` and `Seed::~Seed()` each call `secure_erase` on their internal byte buffers as the very first action, ensuring the raw key bytes are wiped whenever an object goes out of scope regardless of how the destructor is triggered. Second, in key derivation and generation functions: after deriving a key into a temporary stack or heap buffer and copying it into the final `SecretKey` object, the source buffer is immediately wiped. For example, after `randomKeyPair()` fills a 32-byte stack buffer with random data and wraps it in a `SecretKey`, it calls `secure_erase(buf, sizeof(buf))` before returning — the intent being that the raw entropy never persists beyond the call site even if the `SecretKey` copy is later destroyed. + +## Design Rationale + +Wrapping `OPENSSL_cleanse` behind an `xrpl`-namespace function (rather than calling it directly) provides two benefits: it insulates call sites from the OpenSSL header dependency, and it gives the codebase a single, auditable choke point if the underlying strategy ever needs to change (e.g., switching to `explicit_bzero` on platforms where that is available and preferred). The thin wrapper adds zero runtime overhead — it is a single call with no branching. + +The honest limitation documented in the comment is architecturally important: callers should not treat `secure_erase` as a guarantee of complete erasure, but rather as a best-effort mitigation that eliminates the most exploitable residue — heap and stack memory — while being transparent about what it cannot control. \ No newline at end of file diff --git a/include/xrpl/git/Git.h.ai.json b/include/xrpl/git/Git.h.ai.json new file mode 100644 index 0000000000..fe473d5064 --- /dev/null +++ b/include/xrpl/git/Git.h.ai.json @@ -0,0 +1,25 @@ +{ + "args": [], + "classes": [], + "description": "Header file declaring functions to retrieve git commit hash and build branch information within the xrpl::git namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/git/Git.h", + "functions": [ + { + "args": [], + "lineno": 7, + "name": "getCommitHash" + }, + { + "args": [], + "lineno": 10, + "name": "getBuildBranch" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl::git" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/git/Git.h.ai.md b/include/xrpl/git/Git.h.ai.md new file mode 100644 index 0000000000..57efbbb7a8 --- /dev/null +++ b/include/xrpl/git/Git.h.ai.md @@ -0,0 +1,34 @@ +# `include/xrpl/git/Git.h` — Build-Time Git Provenance Interface + +This header is the public face of a narrow compile-time provenance mechanism: it gives the rest of the `rippled` codebase a stable, type-safe API to query the exact git commit and branch that produced the running binary. + +## Why It Exists + +Distributed ledger software is particularly sensitive to version skew. Operators running `rippled` nodes need to confirm exactly which source revision is deployed, and the RPC layer needs to surface that information to network monitoring tools. Rather than embedding raw preprocessor macros throughout the codebase, the `xrpl::git` namespace wraps those macros behind two well-typed functions, hiding all build-system coupling behind a single include. + +## The Interface + +```cpp +namespace xrpl::git { + std::string const& getCommitHash(); + std::string const& getBuildBranch(); +} +``` + +Both functions return `const` references to process-lifetime strings. The implementation in `src/libxrpl/git/Git.cpp` backs each with a `static const std::string`, so the reference is always valid and the heap allocation happens at most once per process. The choice of `const&` over value return is a small but deliberate efficiency — callers like `NetworkOPs` call these functions repeatedly while building JSON responses, and avoiding copies is natural given the values never change. + +## Build-System Wiring + +The real work happens in `cmake/GitInfo.cmake`, which is included once (guarded by `include_guard()`) during CMake configuration. It shells out to `git rev-parse HEAD` and `git rev-parse --abbrev-ref HEAD` at *configure time*, captures the output into CMake variables `GIT_COMMIT_HASH` and `GIT_BUILD_BRANCH`, and propagates them as preprocessor definitions to the `Git.cpp` translation unit. If git is not installed, both variables are set to empty strings with a warning rather than failing the build. + +The implementation file enforces this contract with hard `#error` directives: if either macro is absent at compile time, the build fails immediately rather than silently producing a binary with missing provenance. This is a deliberate fail-loud design — an accidental misconfiguration cannot produce a quietly wrong binary. + +## Callers + +There are two call sites in the codebase. `src/xrpld/app/main/Main.cpp` prints both values to standard output when `--version` is passed on the command line, alongside `BuildInfo::getVersionString()`. This is the primary operator-facing diagnostic path. `src/xrpld/app/misc/NetworkOPs.cpp` includes both values in the JSON object returned by the `server_info` RPC command under the `git` key, but only when the strings are non-empty — a defensive guard that handles the "git not found at configure time" case gracefully at runtime without crashing or emitting null fields. + +## Design Notes + +The separation of the header from `BuildInfo.h` (`src/libxrpl/protocol/BuildInfo.cpp`) is intentional: `BuildInfo` tracks the semantic version string and amendment information, while `xrpl::git` tracks raw VCS state. The two concerns are independently useful and independently testable. Keeping the git provenance in its own `xrpl::git` namespace signals clearly that this information comes from a different source (the VCS) than the manually maintained version number. + +The configure-time capture means the strings reflect the tree state at CMake invocation, not at compile or link time. In a typical CI pipeline these are effectively the same moment, but developers who reconfigure without re-running git operations could observe stale values — a known, accepted trade-off of keeping the CMake logic simple. \ No newline at end of file diff --git a/include/xrpl/json/JsonPropertyStream.h.ai.json b/include/xrpl/json/JsonPropertyStream.h.ai.json new file mode 100644 index 0000000000..51287ebe24 --- /dev/null +++ b/include/xrpl/json/JsonPropertyStream.h.ai.json @@ -0,0 +1,293 @@ +{ + "args": [ + { + "lineno": 19, + "name": "key" + }, + { + "lineno": 23, + "name": "key" + }, + { + "lineno": 23, + "name": "value" + }, + { + "lineno": 25, + "name": "key" + }, + { + "lineno": 25, + "name": "value" + }, + { + "lineno": 27, + "name": "key" + }, + { + "lineno": 27, + "name": "value" + }, + { + "lineno": 29, + "name": "key" + }, + { + "lineno": 29, + "name": "value" + }, + { + "lineno": 31, + "name": "key" + }, + { + "lineno": 31, + "name": "value" + }, + { + "lineno": 33, + "name": "key" + }, + { + "lineno": 33, + "name": "v" + }, + { + "lineno": 35, + "name": "key" + }, + { + "lineno": 35, + "name": "v" + }, + { + "lineno": 37, + "name": "key" + }, + { + "lineno": 37, + "name": "v" + }, + { + "lineno": 41, + "name": "key" + }, + { + "lineno": 46, + "name": "value" + }, + { + "lineno": 48, + "name": "value" + }, + { + "lineno": 50, + "name": "value" + }, + { + "lineno": 52, + "name": "value" + }, + { + "lineno": 54, + "name": "value" + }, + { + "lineno": 56, + "name": "v" + }, + { + "lineno": 58, + "name": "v" + }, + { + "lineno": 60, + "name": "v" + } + ], + "classes": [ + { + "args": [ + "JsonPropertyStream()", + "Json::Value const& top()" + ], + "lineno": 8, + "name": "JsonPropertyStream" + } + ], + "description": "Defines the JsonPropertyStream class, a PropertyStream::Sink that produces a Json::Value object for structured property streaming in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/JsonPropertyStream.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "JsonPropertyStream" + }, + { + "args": [], + "lineno": 14, + "name": "top" + }, + { + "args": [], + "lineno": 17, + "name": "map_begin" + }, + { + "args": [ + "key" + ], + "lineno": 19, + "name": "map_begin" + }, + { + "args": [], + "lineno": 21, + "name": "map_end" + }, + { + "args": [ + "key", + "value" + ], + "lineno": 23, + "name": "add" + }, + { + "args": [ + "key", + "value" + ], + "lineno": 25, + "name": "add" + }, + { + "args": [ + "key", + "value" + ], + "lineno": 27, + "name": "add" + }, + { + "args": [ + "key", + "value" + ], + "lineno": 29, + "name": "add" + }, + { + "args": [ + "key", + "value" + ], + "lineno": 31, + "name": "add" + }, + { + "args": [ + "key", + "v" + ], + "lineno": 33, + "name": "add" + }, + { + "args": [ + "key", + "v" + ], + "lineno": 35, + "name": "add" + }, + { + "args": [ + "key", + "v" + ], + "lineno": 37, + "name": "add" + }, + { + "args": [], + "lineno": 39, + "name": "array_begin" + }, + { + "args": [ + "key" + ], + "lineno": 41, + "name": "array_begin" + }, + { + "args": [], + "lineno": 43, + "name": "array_end" + }, + { + "args": [ + "value" + ], + "lineno": 46, + "name": "add" + }, + { + "args": [ + "value" + ], + "lineno": 48, + "name": "add" + }, + { + "args": [ + "value" + ], + "lineno": 50, + "name": "add" + }, + { + "args": [ + "value" + ], + "lineno": 52, + "name": "add" + }, + { + "args": [ + "value" + ], + "lineno": 54, + "name": "add" + }, + { + "args": [ + "v" + ], + "lineno": 56, + "name": "add" + }, + { + "args": [ + "v" + ], + "lineno": 58, + "name": "add" + }, + { + "args": [ + "v" + ], + "lineno": 60, + "name": "add" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/json/JsonPropertyStream.h.ai.md b/include/xrpl/json/JsonPropertyStream.h.ai.md new file mode 100644 index 0000000000..adb646c4bc --- /dev/null +++ b/include/xrpl/json/JsonPropertyStream.h.ai.md @@ -0,0 +1,27 @@ +# `JsonPropertyStream.h` — JSON-backed Property Stream Sink + +`JsonPropertyStream` is a concrete implementation of `beast::PropertyStream` that captures a hierarchical diagnostic snapshot as a `Json::Value` object tree. Its only production consumer is the `doPrint` admin RPC handler (`src/xrpld/rpc/handlers/admin/status/Print.cpp`), where it bridges the `PropertyStream` tree-walk API and the JSON response envelope sent back to operators. + +## The `PropertyStream` Framework + +`beast::PropertyStream` (in `include/xrpl/beast/utility/PropertyStream.h`) is an abstract sink whose interface mirrors a generic property tree: `map_begin`/`map_end` delimit named objects, `array_begin`/`array_end` delimit sequences, and two families of overloaded `add` methods write leaf values — one family is keyed (object fields), the other is positional (array elements). `PropertyStream::Source` subclasses override `onWrite(Map&)` and can carry a tree of child `Source` objects, allowing the entire application-level component hierarchy to be dumped recursively by calling `Source::write(stream)`. + +`JsonPropertyStream` does nothing more than implement all of those pure virtual methods against a live `Json::Value` tree. + +## Stack-Based Tree Construction + +The class maintains two public members: `m_top`, a `Json::Value` initialized to `objectValue` in the constructor, and `m_stack`, a `std::vector` that tracks the currently-active nesting level. The constructor pre-reserves 64 entries in the stack — a practical upper bound on diagnostic nesting depth — to avoid heap reallocations during a write pass. + +The pointer discipline relies on a key property of `Json::Value`: once a child node is inserted via `operator[]` or `append()`, the returned reference remains stable because `Json::Value` manages its children through a separately allocated map/array rather than by value inside a resizing container. This means the raw `Json::Value*` pointers stored in `m_stack` stay valid for the lifetime of `m_top`. + +When `map_begin(key)` is called, the implementation does `top[key] = Json::objectValue` and pushes the address of that newly-created child. The keyless `map_begin()` instead calls `top.append(Json::objectValue)`, meaning the parent must already be an array. The symmetry holds for `array_begin`: the keyed variant attaches to a map, the keyless variant nests inside an array. Both `map_end` and `array_end` simply pop the stack. + +## Type Narrowing for `long` + +One non-obvious implementation detail: both `add(std::string const& key, long v)` and the array-mode `add(long v)` explicitly cast `v` to `int` before storing into `Json::Value`. `Json::Value` exposes `Int` and `UInt` storage (32-bit) alongside `Int64`/`UInt64`, and on platforms where `long` is 64 bits the jsoncpp API would select a different overload. The explicit cast is a deliberate narrowing to keep the generated JSON representation consistent across LP64 and ILP32 platforms. This is a subtle correctness tradeoff: values larger than `INT_MAX` will silently truncate, but the diagnostic properties this class serializes (counter values, sizes, peer counts) are all expected to fit. + +## Usage in the Admin RPC + +`doPrint` instantiates a `JsonPropertyStream` on the stack, calls `context.app.write(stream)` (or the path-filtered overload when a `params` argument is present), and returns `stream.top()` directly as the RPC result. This makes the entire `PropertyStream::Source` hierarchy of the running application introspectable over the admin interface with a single pass — every component that subclasses `Source` and overrides `onWrite` contributes its properties to the returned JSON object without any bespoke serialization code. + +The public exposure of `m_top` and `m_stack` (rather than keeping them private) is worth noting: it is not idiomatic for an encapsulated class, but it enables direct in-place construction of nested `Json::Value` nodes and avoids any intermediate copy. Since the class is a short-lived sink rather than a long-lived data structure, the loose encapsulation is acceptable. \ No newline at end of file diff --git a/include/xrpl/json/Output.h.ai.json b/include/xrpl/json/Output.h.ai.json new file mode 100644 index 0000000000..88fc3553a4 --- /dev/null +++ b/include/xrpl/json/Output.h.ai.json @@ -0,0 +1,52 @@ +{ + "args": [ + { + "lineno": 13, + "name": "s" + }, + { + "lineno": 15, + "name": "b" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "Value" + } + ], + "description": "Provides utilities for efficiently outputting minimal JSON representations, including streaming output and string conversion, for Json::Value objects.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/Output.h", + "functions": [ + { + "args": [ + "s" + ], + "lineno": 13, + "name": "stringOutput" + }, + { + "args": [ + "Json::Value const&", + "Output const&" + ], + "lineno": 22, + "name": "outputJson" + }, + { + "args": [ + "Json::Value const&" + ], + "lineno": 29, + "name": "jsonAsString" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/json/Output.h.ai.md b/include/xrpl/json/Output.h.ai.md new file mode 100644 index 0000000000..76b88a618d --- /dev/null +++ b/include/xrpl/json/Output.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/json/Output.h` + +This header defines the foundational output abstraction that the XRPL JSON streaming subsystem is built on. It is small by design: three declarations and a single inline factory, providing just enough interface to decouple JSON serialization from any specific output destination. + +## The `Output` Type Alias + +The central construct is a single type alias: + +```cpp +using Output = std::function; +``` + +`Output` is a type-erased callable that accepts a read-only string chunk and does something with it — appends it to a buffer, writes it to a socket, sends it down an HTTP response stream, or discards it during testing. Using `std::function` here means any lambda, functor, or function pointer that matches the signature qualifies. Using `boost::beast::string_view` rather than `std::string_view` keeps this compatible with Boost.Beast's network I/O primitives without requiring copies. + +The design deliberately separates *what to serialize* from *where to send the bytes*. `outputJson` and `Writer` (defined in `Writer.h`) know nothing about the transport — they call the `Output` object with consecutive string chunks as they traverse the JSON tree. This makes the serializer itself reusable across HTTP responses, WebSocket frames, log output, and in-memory buffers. + +## `stringOutput()` — the Common-Case Sink + +```cpp +inline Output +stringOutput(std::string& s) +{ + return [&](boost::beast::string_view const& b) { s.append(b.data(), b.size()); }; +} +``` + +This factory captures a `std::string` by reference and returns an `Output` that accumulates all chunks into it. Because it takes the string by reference and the lambda captures by reference, the caller must keep the string alive for the duration of serialization — this is an intentional lifetime contract. The function is `inline` because it is trivially small and called frequently enough that the factory overhead should disappear after inlining. + +## `outputJson()` and `jsonAsString()` + +`outputJson(Json::Value const&, Output const&)` is the streaming path. In `Output.cpp` it constructs a `Writer` around the provided sink and then recursively walks the `Json::Value` tree — dispatching over scalar types via `Writer::output()` overloads and managing structural punctuation (commas, braces, brackets) via `Writer::rawAppend()` and `Writer::rawSet()`. Critically, no intermediate string buffer is built: bytes flow directly from the value tree to the `Output` function. For large JSON payloads (e.g., full ledger dumps or transaction history) this avoids a potentially multi-megabyte allocation that would otherwise live alongside the already-resident `Json::Value`. + +`jsonAsString(Json::Value const&)` is the convenience wrapper for callers that need a self-contained `std::string`. It internally calls `stringOutput` to create a sink, drives the same `outputJson` path, and returns the accumulated string. The comments in the header are candid about the tradeoff: this form requires an allocation sized to the full output and should be avoided when the streaming variant is applicable. + +Both functions produce *minimal* JSON — no indentation, no extra whitespace — which is appropriate for wire-format output where byte count matters. For human-readable formatting, the older `to_string.h`/`pretty()` path using `StyledWriter` is the alternative. + +## Relationship to `Writer.h` + +`Output.h` sits one level below `Writer.h` in the dependency graph. `Writer` takes an `Output` in its constructor and owns all the structural logic (stack-based collection tracking, comma insertion, key quoting). `Output.h` defines the sink contract; `Writer.h` defines the producer. This separation means a caller can construct a `Writer` without `Output.h` being visible — but the type still flows through cleanly because `Output` is a plain `std::function` with no further dependencies. \ No newline at end of file diff --git a/include/xrpl/json/Writer.h.ai.json b/include/xrpl/json/Writer.h.ai.json new file mode 100644 index 0000000000..668de57ad0 --- /dev/null +++ b/include/xrpl/json/Writer.h.ai.json @@ -0,0 +1,191 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Output const& output" + ], + "lineno": 73, + "name": "Writer" + }, + { + "args": [], + "lineno": 164, + "name": "Impl" + } + ], + "description": "This file defines the Json::Writer class, an efficient, O(1)-space, O(1)-granular JSON writer for streaming JSON output with minimal memory usage. It provides methods to construct JSON objects and arrays incrementally, supporting both scalar and nested values, and ensures proper closure of all structures.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/Writer.h", + "functions": [ + { + "args": [ + "Output const& output" + ], + "lineno": 74, + "name": "Writer" + }, + { + "args": [ + "Writer&&" + ], + "lineno": 75, + "name": "Writer" + }, + { + "args": [ + "Writer&&" + ], + "lineno": 76, + "name": "operator=" + }, + { + "args": [], + "lineno": 78, + "name": "~Writer" + }, + { + "args": [ + "CollectionType" + ], + "lineno": 81, + "name": "startRoot" + }, + { + "args": [ + "CollectionType" + ], + "lineno": 84, + "name": "startAppend" + }, + { + "args": [ + "CollectionType", + "std::string const& key" + ], + "lineno": 87, + "name": "startSet" + }, + { + "args": [], + "lineno": 90, + "name": "finish" + }, + { + "args": [], + "lineno": 94, + "name": "finishAll" + }, + { + "args": [ + "Scalar t" + ], + "lineno": 101, + "name": "append" + }, + { + "args": [], + "lineno": 110, + "name": "rawAppend" + }, + { + "args": [ + "std::string const& tag", + "Type t" + ], + "lineno": 117, + "name": "set" + }, + { + "args": [ + "std::string const& key" + ], + "lineno": 128, + "name": "rawSet" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 134, + "name": "output" + }, + { + "args": [ + "char const*" + ], + "lineno": 137, + "name": "output" + }, + { + "args": [ + "Json::Value const&" + ], + "lineno": 140, + "name": "output" + }, + { + "args": [ + "std::nullptr_t" + ], + "lineno": 143, + "name": "output" + }, + { + "args": [ + "float" + ], + "lineno": 146, + "name": "output" + }, + { + "args": [ + "double" + ], + "lineno": 149, + "name": "output" + }, + { + "args": [ + "bool" + ], + "lineno": 152, + "name": "output" + }, + { + "args": [ + "Type t" + ], + "lineno": 155, + "name": "output" + }, + { + "args": [ + "Json::StaticString const& t" + ], + "lineno": 160, + "name": "output" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 165, + "name": "implOutput" + }, + { + "args": [ + "bool condition", + "std::string const& message" + ], + "lineno": 169, + "name": "check" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/json/Writer.h.ai.md b/include/xrpl/json/Writer.h.ai.md new file mode 100644 index 0000000000..e9f9e27bf9 --- /dev/null +++ b/include/xrpl/json/Writer.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/json/Writer.h` — Streaming JSON Writer + +## Role in the System + +`Json::Writer` exists to solve a specific performance problem in the XRPL node: when serializing large ledger objects or transaction sets to JSON for RPC responses, building an intermediate `Json::Value` tree first means allocating a heap structure proportional to the output size before any bytes reach the network. `Writer` eliminates this by emitting JSON directly into an `Output` callback — a `std::function` — as each field is written, never holding more than a fixed stack of `Collection` state objects. The class comment calls this O(1)-space and O(1)-granular, meaning both memory usage and per-step CPU work are bounded regardless of how large the JSON document becomes. + +## Architecture: Pimpl with a Collection Stack + +The public class is a thin shell over a `std::unique_ptr`. The implementation lives entirely in `Writer.cpp`'s anonymous namespace and `Writer::Impl`. This lets `Writer` be move-constructible (the `unique_ptr` transfers ownership) without exposing internal types in the header. + +`Impl` maintains a `std::stack` (backed by a `std::vector` for contiguous memory). Each `Collection` tracks: + +- `type` — whether the current scope is an `array` or an `object` +- `isFirst` — whether any item has been written yet, which controls whether a leading comma must be emitted before the next element +- `tags` — a `std::set` present **only in debug builds** (`#ifndef NDEBUG`) that catches duplicate object keys at runtime + +Every time a new array or object starts, a `Collection` is pushed. Every time `finish()` is called, the closing `]` or `}` is emitted and the top is popped. `finishAll()` drains the stack to empty, and the destructor calls `finishAll()` unconditionally — so partial writes caused by exceptions or coroutine cleanups always produce syntactically valid (if incomplete-content-wise) JSON. + +## Key API Patterns + +The API is split into three conceptual layers: + +**Collection control** — `startRoot()`, `startAppend()`, `startSet()`, `finish()`, `finishAll()`. The naming encodes both what kind of collection is being opened and where it appears: `startRoot` for top level, `startAppend` when the collection becomes an element of an array, `startSet` when it becomes the value of an object key. There is no separate `startObject`/`startArray` family; the `CollectionType` enum parameter carries that distinction. + +**Structured writes** — `append(Scalar)` and `set(tag, Value)` are templates that compose two lower-level calls: `rawAppend()` or `rawSet()` (which handle comma insertion and object-tag emission) followed by `output(t)` (which serializes the scalar value). This split enables the `raw*` variants for callers that want to write the value data themselves — useful when bridging with other serialization code. + +**Scalar output** — A family of overloaded `output()` methods covers `std::string`, `char const*`, `Json::Value`, `nullptr_t`, `float`, `double`, `bool`, and a template fallback using `std::to_string()`. String values go through `Impl::stringOutput()`, which scans the bytes for the eight JSON-special characters (quote, backslash, slash, and control characters) and emits escape sequences in-place without ever constructing a fully escaped copy of the string. Floats and doubles use `xrpl::to_string()` then `lengthWithoutTrailingZeros()` to strip cosmetic trailing zeros while preserving at least one decimal digit. + +## Invariant Enforcement and the `check()` Free Function + +The header exports a free `check(bool, std::string)` function that throws `std::logic_error` via `xrpl::Throw`. This is used throughout `Impl` to guard against illegal call sequences: calling `append` outside an array, calling `set` outside an object, calling any write after the writer is "finished" (stack drained after at least one write). These are programming errors, not runtime data errors, so `logic_error` is appropriate. In release builds the duplicate-tag detection is compiled out, but the structural checks (`empty()`, `isFinished()`) remain active. + +## Design Tradeoffs + +The central tradeoff is forward-only writing. The caller must visit fields in output order; there is no way to go back and insert a key earlier in the stream. This rules out use cases that need to compute a field's value lazily after other fields are known. In practice within XRPL, JSON serialization follows a determined field order, so this is not a real constraint. + +The existing `Json::Value`-based serialization path (`outputJson()` in `Output.h`) handles the reverse case — converting an already-built tree to a stream — and is used when `Writer::output(Json::Value const&)` is called, allowing arbitrary subtrees to be embedded mid-stream without requiring the caller to decompose them field by field. + +The `isStarted_` flag in `Impl` distinguishes a fresh `Writer` (no writes yet) from a finished one (stack empty after writing). Without this, destroying a never-used `Writer` would incorrectly try to call `finishAll()` in a way that could misbehave. The destructor checks `impl_` for null before calling through, which protects against the moved-from state where `impl_` has been transferred out. \ No newline at end of file diff --git a/include/xrpl/json/detail/json_assert.h.ai.json b/include/xrpl/json/detail/json_assert.h.ai.json new file mode 100644 index 0000000000..6a154ef488 --- /dev/null +++ b/include/xrpl/json/detail/json_assert.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "Header file defining a macro for JSON assertion with error throwing, and including necessary XRPL and JSON error headers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/detail/json_assert.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/json/detail/json_assert.h.ai.md b/include/xrpl/json/detail/json_assert.h.ai.md new file mode 100644 index 0000000000..0c3527a6ee --- /dev/null +++ b/include/xrpl/json/detail/json_assert.h.ai.md @@ -0,0 +1,27 @@ +# `include/xrpl/json/detail/json_assert.h` + +This tiny header defines a single macro that serves as the JSON subsystem's internal assertion mechanism. Its entire purpose is to bridge failed runtime conditions in JSON parsing and value-access code into XRPL's structured exception-throwing infrastructure. + +## What It Does + +```cpp +#define JSON_ASSERT_MESSAGE(condition, message) \ + if (!(condition)) \ + xrpl::Throw(message); +``` + +When `condition` evaluates to false, the macro calls `xrpl::Throw(message)`, which constructs and throws a `Json::error` exception — a thin `std::runtime_error` subtype defined in `json_errors.h`. + +## Why This Design + +The macro is intentionally narrow in scope. Rather than using a raw `throw` or a standard `assert()` (which would abort the process in debug builds and do nothing in release), this approach routes all JSON invariant failures through `xrpl::Throw`, the codebase's canonical exception-dispatching function. `xrpl::Throw` performs two things before throwing: it calls `LogThrow()` to log a call stack, and it constructs the exception with perfect-forwarded arguments. This means every failed JSON assertion produces a log entry with a stack trace — critical for diagnosing malformed or unexpected JSON values in a live ledger node. + +Throwing `Json::error` specifically (rather than a generic `std::runtime_error`) lets callers catch JSON failures distinctly from other XRPL runtime errors, enabling fine-grained error handling at API and RPC boundaries. + +## Usage Context + +In practice the macro appears exclusively in `src/libxrpl/json/json_value.cpp`, the implementation of the `Json::Value` type-conversion machinery. Every type accessor — `asString()`, `asInt()`, `asUInt()`, `asDouble()` — guards its type-dispatch branches with `JSON_ASSERT_MESSAGE`. For example, requesting an integer representation from a JSON value whose internal tag is neither integer, real, nor string triggers `JSON_ASSERT_MESSAGE(false, "Type is not convertible to int")`. This makes type mismatch errors immediately visible and attributable rather than silently producing garbage values. + +## Relationship to `detail/` + +The file lives in the `detail/` subdirectory, signaling it is an implementation concern not intended for direct inclusion by consumers of the public JSON API. The `detail/` convention in this codebase isolates internal helpers that could change without breaking the public interface contract. \ No newline at end of file diff --git a/include/xrpl/json/json_errors.h.ai.json b/include/xrpl/json/json_errors.h.ai.json new file mode 100644 index 0000000000..1666d8867b --- /dev/null +++ b/include/xrpl/json/json_errors.h.ai.json @@ -0,0 +1,22 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::runtime_error::runtime_error" + ], + "lineno": 6, + "name": "error" + } + ], + "description": "Defines a custom error struct for JSON-related exceptions, inheriting from std::runtime_error.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/json_errors.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/json/json_errors.h.ai.md b/include/xrpl/json/json_errors.h.ai.md new file mode 100644 index 0000000000..a2606b4921 --- /dev/null +++ b/include/xrpl/json/json_errors.h.ai.md @@ -0,0 +1,41 @@ +# `json_errors.h` — JSON Exception Type + +This header is the single point of definition for `Json::error`, the exception type thrown throughout the XRPL JSON subsystem whenever a structural or semantic constraint on JSON data is violated. + +## Role in the JSON Subsystem + +The file is deliberately minimal: one `struct`, one inheritance line, no other declarations. Its purpose is to give the JSON layer a *named* exception type that is distinct from the broader `std::runtime_error` hierarchy. Without this separation, callers could not selectively catch JSON-specific failures without also catching every other runtime error the system might raise. + +`Json::error` inherits from `std::runtime_error` via the `using` constructor declaration, meaning it accepts any argument that `std::runtime_error` accepts — most commonly a `const char*` or `std::string` error message — with no additional state or interface of its own. + +## How It Is Thrown + +`json_errors.h` is not used directly at throw sites. Instead, `detail/json_assert.h` wraps it in the `JSON_ASSERT_MESSAGE` macro: + +```cpp +#define JSON_ASSERT_MESSAGE(condition, message) \ + if (!(condition)) \ + xrpl::Throw(message); +``` + +`xrpl::Throw` (from `include/xrpl/basics/contract.h`) is the project-wide logging throw helper. Before re-raising the exception it calls `LogThrow`, which records a call stack. This means every `Json::error` automatically produces a diagnostic trace in the logs — a valuable property for a type that surfaces programmer errors (violated JSON invariants) rather than purely user-input errors. + +## How It Is Caught + +At the RPC boundary in `src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp`, `Json::error` is caught explicitly to convert it into a well-formed RPC error response: + +```cpp +catch (Json::error& e) +{ + if (context.apiVersion > 1u) + { ... } +} +``` + +This catch block is version-aware, reflecting the fact that API v1 and v2+ have different error-surface contracts. The named exception type is what makes this selective handling possible — a plain `std::runtime_error` catch would sweep up far too much. + +## Design Rationale + +The `struct` keyword (rather than `class`) and the single `using` declaration are intentional minimalism. There is no need for extra fields or methods because the error message string carried by `std::runtime_error::what()` is sufficient for all current throw sites. Keeping the type lean also means it remains trivially copyable into `std::exception_ptr` contexts and imposes no ABI friction when propagating across translation-unit boundaries. + +The placement of this definition in the `Json` namespace mirrors the broader project convention of scoping domain-specific exceptions close to the subsystem that raises them, rather than in a global error registry. \ No newline at end of file diff --git a/include/xrpl/json/json_forwards.h.ai.json b/include/xrpl/json/json_forwards.h.ai.json new file mode 100644 index 0000000000..f5fc316f7c --- /dev/null +++ b/include/xrpl/json/json_forwards.h.ai.json @@ -0,0 +1,40 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "StaticString" + }, + { + "args": [], + "lineno": 9, + "name": "Value" + }, + { + "args": [], + "lineno": 10, + "name": "ValueIteratorBase" + }, + { + "args": [], + "lineno": 11, + "name": "ValueIterator" + }, + { + "args": [], + "lineno": 12, + "name": "ValueConstIterator" + } + ], + "description": "Defines basic type aliases and forward declarations for JSON value handling within the Json namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/json_forwards.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/json/json_forwards.h.ai.md b/include/xrpl/json/json_forwards.h.ai.md new file mode 100644 index 0000000000..26317d7027 --- /dev/null +++ b/include/xrpl/json/json_forwards.h.ai.md @@ -0,0 +1,9 @@ +# `json_forwards.h` + +This header is a forward-declaration shim for the `Json` namespace. Its sole purpose is to let other headers reference `Json::Value`, `Json::StaticString`, and the iterator family without paying the compile-time cost of including the full `json_value.h` and all its transitive dependencies (``, ``, ``, `xrpl/basics/Number.h`, etc.). + +The two type aliases — `Int` as `int` and `UInt` as `unsigned int` — establish the canonical integer widths used throughout the JSON subsystem. Centralizing them here means any future platform-specific adjustment (e.g., switching to `int32_t`) is a single-point change rather than a scattered find-and-replace. + +The five forward-declared entities (`StaticString`, `Value`, `ValueIteratorBase`, `ValueIterator`, `ValueConstIterator`) all receive their full definitions in `json_value.h`. Headers that only need to mention `Json::Value` in a function signature — a pointer, a reference, or a return type — can include `json_forwards.h` alone and stay insulated from the heavier type definition. Both `json_reader.h` and `json_writer.h` include this header alongside `json_value.h`, following the standard pattern of declaring the forward header first so that any intermediate consumer relying solely on `json_forwards.h` remains compatible. + +Given how pervasive JSON manipulation is across the XRPL codebase — protocol serialization, RPC handling, transaction parsing — keeping this boundary clean has a measurable effect on incremental build times. The header is intentionally minimal by design. \ No newline at end of file diff --git a/include/xrpl/json/json_reader.h.ai.json b/include/xrpl/json/json_reader.h.ai.json new file mode 100644 index 0000000000..229c206743 --- /dev/null +++ b/include/xrpl/json/json_reader.h.ai.json @@ -0,0 +1,114 @@ +{ + "args": [ + { + "lineno": 22, + "name": "document" + }, + { + "lineno": 22, + "name": "root" + }, + { + "lineno": 31, + "name": "beginDoc" + }, + { + "lineno": 31, + "name": "endDoc" + }, + { + "lineno": 39, + "name": "is" + }, + { + "lineno": 48, + "name": "bs" + }, + { + "lineno": 48, + "name": "root" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "Reader" + }, + { + "args": [], + "lineno": 74, + "name": "Token" + }, + { + "args": [], + "lineno": 83, + "name": "ErrorInfo" + } + ], + "description": "Defines the Json::Reader class for parsing JSON documents into Json::Value objects, supporting multiple input types and error reporting.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/json_reader.h", + "functions": [ + { + "args": [ + "document", + "root" + ], + "lineno": 22, + "name": "parse" + }, + { + "args": [ + "beginDoc", + "endDoc", + "root" + ], + "lineno": 31, + "name": "parse" + }, + { + "args": [ + "is", + "root" + ], + "lineno": 39, + "name": "parse" + }, + { + "args": [ + "root", + "bs" + ], + "lineno": 48, + "name": "parse" + }, + { + "args": [], + "lineno": 58, + "name": "getFormattedErrorMessages" + }, + { + "args": [ + "root", + "bs" + ], + "lineno": 120, + "name": "parse" + }, + { + "args": [ + "std::istream&", + "Value&" + ], + "lineno": 143, + "name": "operator>>" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/json/json_reader.h.ai.md b/include/xrpl/json/json_reader.h.ai.md new file mode 100644 index 0000000000..8ad28b42e7 --- /dev/null +++ b/include/xrpl/json/json_reader.h.ai.md @@ -0,0 +1,36 @@ +# `include/xrpl/json/json_reader.h` — JSON Parser Interface + +## Role in the System + +`json_reader.h` defines `Json::Reader`, the single entry point for deserializing UTF-8 JSON text into the ledger's `Json::Value` tree. Every place in `rippled` that consumes external JSON — incoming RPC requests, peer-to-peer handshake bodies, validator-list manifests, ledger files loaded from disk, and CLI command arguments — routes through this class. It is a hand-rolled recursive-descent parser derived from the JsonCpp library, intentionally kept as a self-contained header plus one implementation file rather than pulling in a third-party JSON dependency. + +## Public Interface + +Four overloads of `parse()` cover every calling pattern present in the codebase: + +- `parse(std::string const&, Value&)` — the most common form, used directly by `ServerHandler`, `RPCCall`, and `Application`. +- `parse(char const* beginDoc, char const* endDoc, Value&)` — the lowest-level overload; all other overloads eventually delegate here. Accepting a half-open pointer range avoids any extra copy when the caller already owns a buffer. +- `parse(std::istream&, Value&)` — reads the entire stream to a `std::string` first, then delegates to the string overload. The comment in the implementation acknowledges this is not streaming: a true iterator approach would need `parse` to be a template, which was intentionally avoided for simplicity. +- `parse(Value& root, BufferSequence const& bs)` — the template overload defined inline in the header. It accepts any Boost.Asio `ConstBufferSequence`, assembles the scattered buffers into a single `std::string` (using `buffer_size` to pre-reserve), and then calls the string overload. This is the form used in `ServerHandler` to parse raw HTTP request bodies directly off the network without an intermediate allocation step for reassembly. + +All overloads return `bool`: `true` on success, `false` if any parse error was encountered. Error details are held inside the `Reader` instance and retrieved afterward via `getFormattedErrorMessages()`. This design deliberately separates parse result from diagnostics — call sites that care about error text (such as the RPC server returning a helpful message to the client) extract it separately; call sites that just need a go/no-go check can ignore it. + +The companion `operator>>(std::istream&, Value&)` is a convenience wrapper that throws `std::exception` on failure, useful where exception-based error handling is preferred over checking a return value. + +## Depth Limiting as a Security Invariant + +`nest_limit` is declared `static constexpr unsigned` with the value `25`. The `readValue` method receives a `depth` counter and returns an error immediately if `depth > nest_limit`, before any further recursive calls. This cap prevents a crafted payload — an arbitrarily nested `[[[[...]]]]` array or object — from exhausting the call stack. In a network-facing server context this is a DoS mitigation: without it, a single malformed request could overflow the stack. The value 25 is generous for any legitimate ledger payload while being far below typical stack depths. + +## Parser State and Token Design + +The parser maintains its scanning state as five raw `char const*` fields: `begin_`, `end_`, `current_`, `lastValueEnd_`, and `lastValue_`. This is a zero-copy scan: the parser works directly against the original string data, never producing substrings until a value actually needs to be decoded (e.g., a string token decoded from `\uXXXX` escape sequences). The private `Token` class is correspondingly thin — a `TokenType` enum tag plus two `Location` (i.e., `char const*`) pointers bounding the lexeme in the source buffer. No heap allocation occurs per token. + +The value tree is assembled using a `Nodes` stack (`std::stack`). The root `Value` is pushed at parse start; as objects and arrays open and close, the matching `Value` node is pushed and popped. `currentValue()` returns a reference to the top-of-stack node, allowing `readValue` to write directly into the correct position in the tree without needing to pass the destination down through every call. + +## Error Handling and Recovery + +Errors are collected in an `Errors` deque (`std::deque`), not thrown. Each `ErrorInfo` bundles a copy of the offending `Token`, a human-readable message string, and an optional secondary `Location` for context. `recoverFromError` and `addErrorAndRecover` implement best-effort continuation: after an unexpected token the parser attempts to skip ahead to a known safe point (e.g., the end of the current array or object), allowing it to accumulate multiple independent errors in one pass. `getFormattedErrorMessages()` formats the full deque into a single diagnostic string including line and column numbers — visible to callers like the RPC server that need to surface parse failures to clients. + +## Relationship to Sibling Files + +`json_forwards.h` provides the forward declaration of `Json::Value` that keeps this header's include footprint minimal. `json_value.h` defines the `Value` type that `Reader` populates; it carries all the `ValueType` variants (`nullValue`, `intValue`, `realValue`, `stringValue`, `booleanValue`, `arrayValue`, `objectValue`) that the parser maps its token types onto. `json_writer.h` provides the inverse direction — serializing a `Value` back to text. There is no shared state between `Reader` and `Writer`; they operate on `Value` objects independently. \ No newline at end of file diff --git a/include/xrpl/json/json_value.h.ai.json b/include/xrpl/json/json_value.h.ai.json new file mode 100644 index 0000000000..f1ea98aa84 --- /dev/null +++ b/include/xrpl/json/json_value.h.ai.json @@ -0,0 +1,160 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "char const* czString" + ], + "lineno": 23, + "name": "StaticString" + }, + { + "args": [ + "ValueType type = nullValue" + ], + "lineno": 77, + "name": "Value" + }, + { + "args": [ + "int index" + ], + "lineno": 109, + "name": "Value::CZString" + }, + { + "args": [], + "lineno": 355, + "name": "ValueAllocator" + }, + { + "args": [], + "lineno": 377, + "name": "ValueIteratorBase" + }, + { + "args": [], + "lineno": 427, + "name": "ValueConstIterator" + }, + { + "args": [], + "lineno": 470, + "name": "ValueIterator" + } + ], + "description": "Defines the core JSON value types, the Json::Value class (a discriminated union for JSON values), static string optimization, value iterators, and related utilities for representing and manipulating JSON data in C++.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/json_value.h", + "functions": [ + { + "args": [ + "StaticString x", + "StaticString y" + ], + "lineno": 38, + "name": "operator==" + }, + { + "args": [ + "StaticString x", + "StaticString y" + ], + "lineno": 43, + "name": "operator!=" + }, + { + "args": [ + "std::string const& x", + "StaticString y" + ], + "lineno": 48, + "name": "operator==" + }, + { + "args": [ + "std::string const& x", + "StaticString y" + ], + "lineno": 53, + "name": "operator!=" + }, + { + "args": [ + "StaticString x", + "std::string const& y" + ], + "lineno": 58, + "name": "operator==" + }, + { + "args": [ + "StaticString x", + "std::string const& y" + ], + "lineno": 63, + "name": "operator!=" + }, + { + "args": [ + "xrpl::Number const& number" + ], + "lineno": 324, + "name": "to_json" + }, + { + "args": [ + "Value const&", + "Value const&" + ], + "lineno": 329, + "name": "operator==" + }, + { + "args": [ + "Value const& x", + "Value const& y" + ], + "lineno": 331, + "name": "operator!=" + }, + { + "args": [ + "Value const&", + "Value const&" + ], + "lineno": 336, + "name": "operator<" + }, + { + "args": [ + "Value const& x", + "Value const& y" + ], + "lineno": 338, + "name": "operator<=" + }, + { + "args": [ + "Value const& x", + "Value const& y" + ], + "lineno": 343, + "name": "operator>" + }, + { + "args": [ + "Value const& x", + "Value const& y" + ], + "lineno": 348, + "name": "operator>=" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/json/json_value.h.ai.md b/include/xrpl/json/json_value.h.ai.md new file mode 100644 index 0000000000..3b70e18bbf --- /dev/null +++ b/include/xrpl/json/json_value.h.ai.md @@ -0,0 +1,53 @@ +# `json_value.h` — Core JSON Value Type for XRPL + +## Role in the System + +This header defines the entire in-memory representation of JSON data used throughout the XRPL (rippled) codebase. It is the foundation on which all RPC request/response handling, transaction serialization diagnostics, and configuration parsing are built. Rather than depending on a third-party JSON library at the API boundary, the XRPL codebase maintains its own lean JSON implementation descended from the early JsonCpp library, giving it tight control over allocation, lifetime, and custom XRPL-specific type integration. + +## The `Value` Class: A Discriminated Union + +`Value` is the single workhorse type. It holds one of eight JSON types — `nullValue`, `intValue`, `uintValue`, `realValue`, `stringValue`, `booleanValue`, `arrayValue`, and `objectValue` — described by the `ValueType` enum. Internally, `Value` stores its data in a flat `union ValueHolder` with branches for `Int int_`, `UInt uint_`, `double real_`, `bool bool_`, `char* string_`, and `ObjectValues* map_`. The active branch is determined by the 8-bit `type_` field. + +A single extra bit field, `allocated_` (declared as `int : 1` rather than `bool : 1` to avoid bitfield-boolean platform quirks), records whether `value_.string_` was heap-allocated and therefore needs freeing in the destructor. When a `Value` is constructed from a `StaticString`, `allocated_` stays `0` and the pointer is borrowed; when constructed from a `const char*` or `std::string`, the string is duplicated via `ValueAllocator` and `allocated_` is set to `1`. This allows a zero-overhead fast path for the common case of populating JSON objects with compile-time field names. + +The copy constructor and copy-assignment operator perform deep copies: strings are duplicated through the allocator, and `ObjectValues` maps are heap-allocated fresh. Move semantics are also implemented — move construction pilfers `value_`, `type_`, and `allocated_` from the source and resets the source to `nullValue`, avoiding any allocation. + +## The `CZString` Key Type and Unified Array/Object Storage + +One of the most notable design choices is that both `arrayValue` and `objectValue` share the same underlying storage: `ObjectValues`, which is `std::map`. Array elements are stored using integer-indexed `CZString` keys constructed from `CZString(int index)`, while object members use string-keyed `CZString` instances. This unification means the implementation only has one map to manage, at the cost that arrays are slightly heavier than a `std::vector` would be. The trade-off favors implementation simplicity and the ability to iterate both arrays and objects through the same `ValueIteratorBase` infrastructure. + +`CZString` itself is a private inner class implementing the map key. Its `DuplicationPolicy` enum (`noDuplication`, `duplicate`, `duplicateOnCopy`) dictates whether the key string is owned. When an object member is created from a `StaticString`, the `CZString` is built with `noDuplication`, avoiding a heap allocation for the member name. When a `const char*` key is used with a mutable `operator[]`, the name is duplicated via the allocator. The copy constructor of `CZString` correctly propagates ownership: a `noDuplication` key stays borrowed, a `duplicate` key triggers a fresh `makeMemberName()` call. + +## The `StaticString` Optimization + +`StaticString` is a lightweight tag type wrapping a `const char*`. Its constructor is `constexpr`, and its sole purpose is to signal to `Value`'s constructor and `operator[]` that the pointed-to string is a compile-time constant with program lifetime, so no duplication is needed. XRPL code commonly does: + +```cpp +static const StaticString sfAccount("account"); +object[sfAccount] = accountValue; +``` + +This avoids two heap allocations (for the member name and potentially the string value) that would occur if a `const char*` or `std::string` were used. The comparison operators between `StaticString` and `std::string` use `strcmp` directly on the underlying pointer, so no temporary strings are created during key lookup. + +## Memory Management and `ValueAllocator` + +All heap allocation for string values and member names flows through a global `ValueAllocator` singleton. The concrete implementation used at runtime is `DefaultValueAllocator` (defined in `json_value.cpp`), which uses `malloc`/`free` and `memcpy`. The abstract `ValueAllocator` interface exists as an extension point — labeled "Experimental do not use" in the header — that would allow a custom pool allocator to intercept all JSON string allocations. The singleton is initialized via a static `DummyValueAllocatorInitializer` to guarantee the allocator is available before `main()` runs. + +## `xrpl::Number` Integration + +The `Value(xrpl::Number const&)` constructor and the `to_json(xrpl::Number const&)` free function bridge XRPL's high-precision numeric type into JSON. Because JSON lacks a native type for XRPL's mantissa-exponent format, the conversion routes through `to_string(number)` and stores the result as a `stringValue`. This is the correct choice: XRPL amounts like IOU values exceed IEEE 754 double precision and must travel as strings to avoid rounding during serialization. + +## Comparison and Ordering + +`operator<` and `operator==` are `friend` functions comparing two `Value` instances. When types differ, there is special-case logic to correctly compare `intValue` against `uintValue` using `integerCmp`, which avoids the undefined behavior of mixing signed and unsigned in C++ comparisons. Same-type comparisons are type-switched directly against the union branch. For `arrayValue` and `objectValue`, comparison delegates to `std::map`'s own `operator<`, using `CZString::operator<` for key ordering, which falls back to `strcmp` for string keys and integer ordering for array indices. + +## Iterator Design + +`ValueIteratorBase` wraps `Value::ObjectValues::iterator` — the `std::map` iterator — and adds `key()`, `index()`, and `memberName()` accessors so callers can inspect whether they are iterating an array (integer key) or an object (string key). `ValueConstIterator` and `ValueIterator` extend the base with the standard bidirectional iterator interface. The `isNull_` flag on `ValueIteratorBase` handles the edge case of iterating over a `nullValue` — the iterators are valid but represent an empty range. Both iterator types expose `operator*` returning a reference to the mapped `Value`, with `ValueConstIterator`'s version being `const Value&`. The mutable `ValueIterator` can be constructed from a `ValueConstIterator`, but not the reverse, preserving const-correctness. + +## Behavioral Invariants + +- Accessing a non-existent member via non-const `operator[]` silently inserts a `nullValue` and returns a reference to it. This is the standard JSON library convention that allows nested construction like `root["tx"]["account"] = "r..."` without pre-creating intermediate objects. +- `clear()` removes all array elements or object members but does not change the `type_`. A `nullValue` on which `clear()` is called remains `nullValue`. +- `removeMember()` has a precondition that `type_` is `objectValue` or `nullValue`; it returns the removed value or `null`. +- `getMemberNames()` on a `nullValue` returns an empty `Members` vector without altering the type, matching the documented postcondition. \ No newline at end of file diff --git a/include/xrpl/json/json_writer.h.ai.json b/include/xrpl/json/json_writer.h.ai.json new file mode 100644 index 0000000000..85adc36d22 --- /dev/null +++ b/include/xrpl/json/json_writer.h.ai.json @@ -0,0 +1,169 @@ +{ + "args": [ + { + "lineno": 36, + "name": "root" + }, + { + "lineno": 67, + "name": "root" + }, + { + "lineno": 115, + "name": "out" + }, + { + "lineno": 115, + "name": "root" + }, + { + "lineno": 158, + "name": "value" + }, + { + "lineno": 158, + "name": "write" + }, + { + "lineno": 163, + "name": "write" + }, + { + "lineno": 163, + "name": "value" + }, + { + "lineno": 222, + "name": "jv" + }, + { + "lineno": 222, + "name": "write" + }, + { + "lineno": 237, + "name": "jv" + }, + { + "lineno": 243, + "name": "o" + }, + { + "lineno": 243, + "name": "cJv" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "WriterBase" + }, + { + "args": [], + "lineno": 29, + "name": "FastWriter" + }, + { + "args": [], + "lineno": 56, + "name": "StyledWriter" + }, + { + "args": [ + "std::string indentation = \"\\t\"" + ], + "lineno": 101, + "name": "StyledStreamWriter" + }, + { + "args": [ + "Json::Value&& jv" + ], + "lineno": 236, + "name": "Compact" + } + ], + "description": "Provides classes and functions for serializing Json::Value objects to JSON format, both in compact and human-friendly (styled) formats, including streaming support and helper utilities.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/json_writer.h", + "functions": [ + { + "args": [ + "Int value" + ], + "lineno": 143, + "name": "valueToString" + }, + { + "args": [ + "UInt value" + ], + "lineno": 144, + "name": "valueToString" + }, + { + "args": [ + "double value" + ], + "lineno": 145, + "name": "valueToString" + }, + { + "args": [ + "bool value" + ], + "lineno": 146, + "name": "valueToString" + }, + { + "args": [ + "char const* value" + ], + "lineno": 147, + "name": "valueToQuotedString" + }, + { + "args": [ + "std::ostream&", + "Value const& root" + ], + "lineno": 150, + "name": "operator<<" + }, + { + "args": [ + "Write const& write", + "std::string const& s" + ], + "lineno": 158, + "name": "write_string" + }, + { + "args": [ + "Write const& write", + "Value const& value" + ], + "lineno": 163, + "name": "write_value" + }, + { + "args": [ + "Json::Value const& jv", + "Write const& write" + ], + "lineno": 222, + "name": "stream" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "Json" + }, + { + "lineno": 155, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/json/json_writer.h.ai.md b/include/xrpl/json/json_writer.h.ai.md new file mode 100644 index 0000000000..68c4431047 --- /dev/null +++ b/include/xrpl/json/json_writer.h.ai.md @@ -0,0 +1,37 @@ +# `include/xrpl/json/json_writer.h` — JSON Serialization Strategies + +This header is the central serialization layer for the `Json::Value` type used throughout rippled. It provides four distinct mechanisms for converting in-memory JSON trees to text, each suited to a different production context: a legacy class hierarchy for string output, a stream-friendly variant, a low-level template sink for zero-copy I/O, and a lightweight stream decorator. Understanding why four mechanisms exist — rather than one — requires reading how each is consumed. + +## The Class Hierarchy: `WriterBase`, `FastWriter`, `StyledWriter` + +`WriterBase` is a pure-virtual interface with a single `write(Value const& root) → std::string` method. This is the JsonCpp-era design: a polymorphic writer that produces an owned string. Two concrete implementations refine it: + +`FastWriter` is the compact, single-line serializer. It accumulates all output into `document_` (a private `std::string`) via a recursive `writeValue()` helper, then returns the entire accumulated string. There is no indentation, no line breaks — the result is maximally dense. This is appropriate for machine consumption where byte count matters. + +`StyledWriter` applies human-readable formatting with a configurable indent size (default 3 spaces) and right margin (74 columns). Its `isMultilineArray()` method embodies the key formatting heuristic: arrays of scalars that fit on a single line are inlined; arrays containing nested objects or other arrays, or arrays that exceed the right margin, are split across lines. This logic is implemented by staging child values into the `childValues_` vector before deciding layout, which is why the class carries that intermediate buffer as member state. + +## `StyledStreamWriter`: Intentionally Outside the Hierarchy + +`StyledStreamWriter` applies the same formatting rules as `StyledWriter` but writes to a `std::ostream*` instead of accumulating a string. The design note in the header is explicit: "There is no point in deriving from Writer, since `write()` should not return a value." This is not an oversight — forcing a stream-sink writer to conform to a string-returning interface would either require buffering the entire output (defeating the purpose) or returning a sentinel. The class deliberately opts out of `WriterBase` inheritance. Its constructor accepts a custom indentation string (defaulting to `"\t"`), unlike `StyledWriter`'s fixed `indentSize_` integer, giving callers more control over the whitespace character. + +The `operator<<(std::ostream&, Value const&)` free function declared at the bottom of the public API surface delegates to `StyledStreamWriter`, making `std::cout << jv` work naturally at the cost of pretty-printing overhead. + +## Template-Based Compact Serialization: `detail::write_value` and `stream()` + +The more architecturally interesting addition is the `detail::write_value` template. It takes any callable with signature compatible with `void(void const*, std::size_t)` — a raw buffer sink — and dispatches on `Value::type()` with a `switch`. This avoids virtual dispatch entirely and allows the caller to wire in any sink: a socket buffer, an HTTP response body, a `boost::asio::streambuf`. The six `ValueType` cases (`nullValue`, `intValue`, `uintValue`, `realValue`, `stringValue`, `booleanValue`) are handled inline; `arrayValue` and `objectValue` recurse into child values. + +The public-facing `stream(jv, write)` template wraps `detail::write_value` and appends a trailing `"\n"`, producing a complete newline-terminated compact JSON document. This is the mechanism used by the WebSocket and HTTP RPC server handlers in `WSInfoSub.h` and `ServerHandler.cpp`, where the write callback populates a low-level I/O buffer directly — no intermediate `std::string` allocation is needed. + +## `Json::Compact`: A Value-Semantic Decorator + +`Compact` is a small RAII wrapper that adapts a `Json::Value` for `operator<<` with compact (non-styled) formatting. Its constructor accepts only `Json::Value&&` (rvalue), which the header comment explicitly justifies: lvalue support would require a potentially expensive copy, so the interface forces callers to be explicit about ownership transfer or to pass a temporary. The `friend operator<<` delegates to `detail::write_value` with a lambda that forwards writes to the stream's `write()` method. + +The usage pattern seen throughout the codebase confirms the intended role: `Json::Compact` appears ubiquitously in `JLOG` logging calls (consensus, dispute tracking, validation tries, performance logging) where pretty-printing would add unnecessary whitespace to log files, and where the JSON value is often a temporary produced by a `getJson()` call that would be discarded anyway. The rvalue-only constructor makes this pattern natural. + +## Helper Functions + +`valueToString()` is overloaded for `Int`, `UInt`, `double`, and `bool`. The integer variants use a stack-allocated 32-byte buffer with a manual right-to-left digit fill (via `uintToString()`), avoiding heap allocation and `std::to_string` overhead. The double variant applies 17-digit precision to guarantee round-trip fidelity. `valueToQuotedString()` handles JSON string escaping including control characters, and its implementation scans for control characters first to decide whether the slow escape path is needed — a small but real optimization for the common case of ASCII-clean strings. + +## Relationship to the Broader JSON Module + +Within the `include/xrpl/json/` module, `json_writer.h` is the serialization counterpart to `json_reader.h`. The `Output.h` file defines an alternative compact serializer (`outputJson` / `jsonAsString`) that uses a `std::function` sink rather than a template, trading compile-time flexibility for a stable ABI. The `Writer.h` streaming writer (`Json::Writer`) targets yet another use case — incremental, forward-only construction with O(1) memory — and is complementary rather than redundant. \ No newline at end of file diff --git a/include/xrpl/json/to_string.h.ai.json b/include/xrpl/json/to_string.h.ai.json new file mode 100644 index 0000000000..60d0def8e5 --- /dev/null +++ b/include/xrpl/json/to_string.h.ai.json @@ -0,0 +1,37 @@ +{ + "args": [], + "classes": [], + "description": "Provides functions for serializing Json::Value objects to strings and streams, including compact and pretty-printed output.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/json/to_string.h", + "functions": [ + { + "args": [ + "Value const&" + ], + "lineno": 11, + "name": "to_string" + }, + { + "args": [ + "Value const&" + ], + "lineno": 14, + "name": "pretty" + }, + { + "args": [ + "std::ostream&", + "Value const& root" + ], + "lineno": 17, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/json/to_string.h.ai.md b/include/xrpl/json/to_string.h.ai.md new file mode 100644 index 0000000000..4f75906e6e --- /dev/null +++ b/include/xrpl/json/to_string.h.ai.md @@ -0,0 +1,23 @@ +# `include/xrpl/json/to_string.h` — JSON Serialization Interface + +This header provides the public serialization surface for `Json::Value` objects: three free functions that cover the two most common output needs (compact wire-format strings and human-readable strings) plus standard stream integration. + +## Purpose and Placement + +Within the `xrpl/json` module, the actual JSON machinery lives in `json_value.h`, `json_writer.h`, and their implementations. `to_string.h` is a deliberately thin façade over that machinery. It forward-declares `class Value` rather than including `json_value.h`, so any translation unit that only needs to convert a `Value` to a string or write it to a stream can include this header without pulling in the full JSON value and writer infrastructure. The include cost is just `` and ``. + +## The Three Entry Points + +`to_string(Value const&)` produces a compact, single-line JSON string. Its implementation delegates directly to `FastWriter().write(value)` — the `FastWriter` omits all whitespace and newlines, making it appropriate for RPC wire formats where byte count matters. This is the function to reach for whenever a `Json::Value` must be serialized for network transmission or storage. + +`pretty(Value const&)` produces indented, human-readable JSON via `StyledWriter().write(value)`. The `StyledWriter` applies a 3-space indent per level, observes a 74-character right margin for deciding whether arrays expand to multiple lines, and generally produces output suitable for logging or debugging. When arrays are short and contain no objects or nested arrays, the styled writer keeps them on one line; otherwise it expands each element to its own line. This heuristic is baked into `StyledWriter::isMultilineArray()`. + +`operator<<(std::ostream&, Value const& root)` is the stream insertion operator. Per the comment in `json_writer.h`, it uses `StyledStreamWriter` — meaning undecorated streaming of a `Json::Value` produces pretty-printed output, not compact output. This is the opposite of what many callers might expect from the default. Code that needs compact output to a stream should use the `Json::Compact` decorator from `json_writer.h` (e.g., `out << Json::Compact{std::move(jv)}`) or the `Json::stream()` template, both of which bypass `StyledStreamWriter` and write minimally. + +## Design Observations + +The split between `to_string.h` and `json_writer.h` reflects a deliberate layering decision. `json_writer.h` is the complete writer interface — it defines `FastWriter`, `StyledWriter`, `StyledStreamWriter`, the `Compact` decorator, and the `stream()` template. It also re-declares `operator<<`. `to_string.h` re-exposes only the three most commonly used operations under names that are idiomatic in the XRPL codebase (`to_string`, `pretty`, `operator<<`), hiding the underlying writer class selection from callers. This means callers who only serialize to `std::string` or a stream never need to know whether `FastWriter` or `StyledWriter` is the right choice — the function name carries that intent. + +One subtle consequence: because `operator<<` is declared in both headers, any translation unit that includes only `to_string.h` will still find the operator, but the definition lives in the `json_writer` implementation. This works because both declarations refer to the same symbol, but it does mean `to_string.h` is implicitly relying on the linker to find the definition provided by `json_writer.cpp`. + +The absence of any exception specification or error-return contract is intentional: JSON serialization of a well-formed `Value` is infallible. The writers operate on a pre-validated in-memory tree, so there is no meaningful error path to expose at this level. \ No newline at end of file diff --git a/include/xrpl/ledger/AcceptedLedgerTx.h.ai.json b/include/xrpl/ledger/AcceptedLedgerTx.h.ai.json new file mode 100644 index 0000000000..d2a613cce4 --- /dev/null +++ b/include/xrpl/ledger/AcceptedLedgerTx.h.ai.json @@ -0,0 +1,92 @@ +{ + "args": [ + { + "lineno": 28, + "name": "ledger" + }, + { + "lineno": 29, + "name": "STTx" + }, + { + "lineno": 30, + "name": "STObject" + } + ], + "classes": [ + { + "args": [ + "ledger", + "STTx", + "STObject" + ], + "lineno": 25, + "name": "AcceptedLedgerTx" + } + ], + "description": "Defines the AcceptedLedgerTx class, representing a transaction in a closed ledger with additional metadata and utility methods for accessing transaction details, affected accounts, and JSON representation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/AcceptedLedgerTx.h", + "functions": [ + { + "args": [ + "ledger", + "STTx", + "STObject" + ], + "lineno": 27, + "name": "AcceptedLedgerTx" + }, + { + "args": [], + "lineno": 31, + "name": "getTxn" + }, + { + "args": [], + "lineno": 36, + "name": "getMeta" + }, + { + "args": [], + "lineno": 41, + "name": "getAffected" + }, + { + "args": [], + "lineno": 46, + "name": "getTransactionID" + }, + { + "args": [], + "lineno": 50, + "name": "getTxnType" + }, + { + "args": [], + "lineno": 54, + "name": "getResult" + }, + { + "args": [], + "lineno": 58, + "name": "getTxnSeq" + }, + { + "args": [], + "lineno": 62, + "name": "getEscMeta" + }, + { + "args": [], + "lineno": 65, + "name": "getJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/AcceptedLedgerTx.h.ai.md b/include/xrpl/ledger/AcceptedLedgerTx.h.ai.md new file mode 100644 index 0000000000..af6530ac55 --- /dev/null +++ b/include/xrpl/ledger/AcceptedLedgerTx.h.ai.md @@ -0,0 +1,39 @@ +# `AcceptedLedgerTx` — Closed-Ledger Transaction Wrapper + +## Purpose and Role + +`AcceptedLedgerTx` represents a single transaction that has been accepted into a closed (non-open) ledger, enriched with all the derived information the server needs to notify clients and persist state to storage. Its central job is to act as an immutable, eagerly-computed snapshot: raw transaction bytes, parsed metadata, affected accounts, a cached JSON representation, and a SQL-ready blob are all computed once at construction and then served read-only to multiple consumers. + +The class sits at the intersection of two major subsystems: the subscription/notification layer (`NetworkOPsImp::pubValidatedTransaction`, `pubAccountTransaction`) and the relational database backend (`Node.cpp` SQL insert path). Both consumers need different projections of the same transaction — one needs JSON with affected account lists for WebSocket fan-out, the other needs binary-escaped metadata for SQL INSERTs. Rather than recomputing these projections on every delivery, `AcceptedLedgerTx` pre-builds all of them in its constructor. + +## Construction and Data Flow + +The constructor takes three inputs: a shared pointer to the closed `ReadView` (the ledger snapshot), the `STTx` transaction object, and a raw `STObject` representing the transaction's metadata blob as stored in the ledger. + +From these, four derived data structures are built: + +1. **`TxMeta mMeta`** — the `STObject` metadata is parsed into a structured `TxMeta` via `TxMeta(txID, ledger->seq(), *met)`, giving typed access to affected nodes, result codes, and the transaction's position index within the ledger. + +2. **`mAffected`** — `mMeta.getAffectedAccounts()` returns a `boost::container::flat_set` of every account touched by this transaction. Using a `flat_set` (a sorted contiguous array rather than a tree) is a deliberate choice: the set is built once and only read afterwards, so cache-friendly linear traversal during fan-out outweighs the cost of sorted insertion at construction time. This set is what `InfoSub` uses to route notifications to account-specific subscribers. + +3. **`mRawMeta`** — the `STObject` is separately serialized back to a raw byte blob via `Serializer::add()`. This binary form coexists with the parsed `TxMeta` because each serves a different consumer: the parsed form enables field-level access, while the raw bytes are needed verbatim for database persistence and are included in the JSON as `raw_meta` (hex-encoded via `strHex`). + +4. **`mJson`** — the full JSON snapshot is assembled once, containing the transaction (`jss::transaction`), structured metadata (`jss::meta`), the hex-encoded raw metadata (`jss::raw_meta`), the human-readable result string (`jss::result`), and an array of affected account Base58 addresses (`jss::affected`). Computing JSON is relatively expensive; caching it here ensures the cost is paid once regardless of how many WebSocket subscribers receive this transaction. + +The constructor enforces a hard invariant via `XRPL_ASSERT(!ledger->open())` — an `AcceptedLedgerTx` can only be created from a closed ledger. This assertion documents a domain rule (you cannot "accept" a transaction into an open ledger) and guards against programming errors where the wrong ledger state is passed in. + +## The `owner_funds` Special Case + +For `ttOFFER_CREATE` transactions where the offer is not self-funded (i.e., the issuer of the `TakerGets` amount is not the offer creator), the constructor queries `accountFunds()` against the ledger snapshot and injects an `owner_funds` field into the JSON. This is a special accommodation for order-book subscribers: clients watching an order book need the offer creator's actual spendable balance to determine whether the offer is executable, and this balance is only available while the closed ledger's state is still in hand. Injecting it here avoids a later round-trip through the ledger when delivering the notification. + +## `getEscMeta()` and the Database Path + +`getEscMeta()` returns `mRawMeta` formatted as an escaped SQL blob literal via `sqlBlobLiteral()`. In `Node.cpp`, this is used directly in an `STTx::getMetaSQL()` insert statement — the output is pasted verbatim into a SQL string. The `XRPL_ASSERT(!mRawMeta.empty())` guard at the top of `getEscMeta()` documents that empty metadata is structurally impossible for an accepted transaction; since `mRawMeta` is populated from the `STObject` passed at construction (which must exist for a valid ledger entry), an empty blob would indicate severe ledger corruption upstream. + +## Relationship to `AcceptedLedger` + +`AcceptedLedger` owns a `std::vector>` — one entry per transaction in the accepted ledger, in order. `AcceptedLedger`'s constructor iterates the ledger's transaction map and builds an `AcceptedLedgerTx` for each entry. The `AcceptedLedger` is itself held in a cache and handed to `NetworkOPsImp` when it processes a newly validated ledger, which then iterates the vector and calls both `pubValidatedTransaction` and `pubAccountTransaction` for each `AcceptedLedgerTx`. + +## Instance Counting via `CountedObject` + +`AcceptedLedgerTx` inherits `CountedObject`, which uses a lock-free linked list of static counters to track live instance counts by type name. This is a server-wide diagnostic facility: operators can query how many `AcceptedLedgerTx` objects are alive at any moment, which is useful for detecting accumulation under load or slow subscriber drain that is holding ledger snapshots open longer than expected. \ No newline at end of file diff --git a/include/xrpl/ledger/AmendmentTable.h.ai.json b/include/xrpl/ledger/AmendmentTable.h.ai.json new file mode 100644 index 0000000000..4b0f7e4966 --- /dev/null +++ b/include/xrpl/ledger/AmendmentTable.h.ai.json @@ -0,0 +1,309 @@ +{ + "args": [ + { + "lineno": 18, + "name": "n" + }, + { + "lineno": 18, + "name": "f" + }, + { + "lineno": 18, + "name": "v" + }, + { + "lineno": 32, + "name": "name" + }, + { + "lineno": 35, + "name": "amendment" + }, + { + "lineno": 36, + "name": "amendment" + }, + { + "lineno": 38, + "name": "amendment" + }, + { + "lineno": 40, + "name": "amendment" + }, + { + "lineno": 41, + "name": "amendment" + }, + { + "lineno": 56, + "name": "isAdmin" + }, + { + "lineno": 59, + "name": "amendment" + }, + { + "lineno": 59, + "name": "isAdmin" + }, + { + "lineno": 62, + "name": "lastValidatedLedger" + }, + { + "lineno": 73, + "name": "seq" + }, + { + "lineno": 76, + "name": "ledgerSeq" + }, + { + "lineno": 76, + "name": "enabled" + }, + { + "lineno": 76, + "name": "majority" + }, + { + "lineno": 80, + "name": "allTrusted" + }, + { + "lineno": 84, + "name": "rules" + }, + { + "lineno": 84, + "name": "closeTime" + }, + { + "lineno": 84, + "name": "enabledAmendments" + }, + { + "lineno": 84, + "name": "majorityAmendments" + }, + { + "lineno": 84, + "name": "valSet" + }, + { + "lineno": 91, + "name": "enabled" + }, + { + "lineno": 108, + "name": "lastClosedLedger" + }, + { + "lineno": 108, + "name": "parentValidations" + }, + { + "lineno": 108, + "name": "initialPosition" + }, + { + "lineno": 108, + "name": "j" + }, + { + "lineno": 143, + "name": "registry" + }, + { + "lineno": 143, + "name": "majorityTime" + }, + { + "lineno": 143, + "name": "supported" + }, + { + "lineno": 143, + "name": "enabled" + }, + { + "lineno": 143, + "name": "vetoed" + }, + { + "lineno": 143, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "AmendmentTable" + }, + { + "args": [ + "n", + "f", + "v" + ], + "lineno": 16, + "name": "FeatureInfo" + } + ], + "description": "Defines the AmendmentTable interface for managing protocol amendments in the XRPL ledger, including voting, enabling, vetoing, and querying amendment status.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/AmendmentTable.h", + "functions": [ + { + "args": [ + "name" + ], + "lineno": 32, + "name": "find" + }, + { + "args": [ + "amendment" + ], + "lineno": 35, + "name": "veto" + }, + { + "args": [ + "amendment" + ], + "lineno": 36, + "name": "unVeto" + }, + { + "args": [ + "amendment" + ], + "lineno": 38, + "name": "enable" + }, + { + "args": [ + "amendment" + ], + "lineno": 40, + "name": "isEnabled" + }, + { + "args": [ + "amendment" + ], + "lineno": 41, + "name": "isSupported" + }, + { + "args": [], + "lineno": 48, + "name": "hasUnsupportedEnabled" + }, + { + "args": [], + "lineno": 53, + "name": "firstUnsupportedExpected" + }, + { + "args": [ + "isAdmin" + ], + "lineno": 56, + "name": "getJson" + }, + { + "args": [ + "amendment", + "isAdmin" + ], + "lineno": 59, + "name": "getJson" + }, + { + "args": [ + "lastValidatedLedger" + ], + "lineno": 62, + "name": "doValidatedLedger" + }, + { + "args": [ + "seq" + ], + "lineno": 73, + "name": "needValidatedLedger" + }, + { + "args": [ + "ledgerSeq", + "enabled", + "majority" + ], + "lineno": 76, + "name": "doValidatedLedger" + }, + { + "args": [ + "allTrusted" + ], + "lineno": 80, + "name": "trustChanged" + }, + { + "args": [ + "rules", + "closeTime", + "enabledAmendments", + "majorityAmendments", + "valSet" + ], + "lineno": 84, + "name": "doVoting" + }, + { + "args": [ + "enabled" + ], + "lineno": 91, + "name": "doValidation" + }, + { + "args": [], + "lineno": 97, + "name": "getDesired" + }, + { + "args": [ + "lastClosedLedger", + "parentValidations", + "initialPosition", + "j" + ], + "lineno": 108, + "name": "doVoting" + }, + { + "args": [ + "registry", + "majorityTime", + "supported", + "enabled", + "vetoed", + "journal" + ], + "lineno": 143, + "name": "make_AmendmentTable" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/AmendmentTable.h.ai.md b/include/xrpl/ledger/AmendmentTable.h.ai.md new file mode 100644 index 0000000000..2346020e44 --- /dev/null +++ b/include/xrpl/ledger/AmendmentTable.h.ai.md @@ -0,0 +1,49 @@ +# `include/xrpl/ledger/AmendmentTable.h` + +## Role in the System + +`AmendmentTable` is the central interface for XRPL's on-chain governance mechanism. The XRP Ledger evolves through *amendments* — protocol changes that require supermajority validator approval before they activate. This header defines the abstract contract that governs the full lifecycle of those amendments: registration, voting, activation, and the critical safety valve of detecting when a node is running software that lacks support for an amendment the network has already enabled (the "amendment blocked" condition). + +The class deliberately separates the interface layer (this header) from the ledger-reading infrastructure. This decoupling is explicitly called out in the source: the concrete `doVoting` and `doValidatedLedger` methods declared here exist specifically so the implementation does not need to import ledger-level types like `ReadView` or `SHAMap`. The comment in the file reads: "These APIs will merge when the view code supports a full ledger API" — an honest acknowledgment of an architectural seam that has not yet been closed. + +## The `FeatureInfo` Inner Struct + +`FeatureInfo` bundles the three things the table needs to know about each registered amendment: its human-readable `name`, its canonical 256-bit `feature` hash (the `uint256` used everywhere in the ledger), and its `VoteBehavior`. The `VoteBehavior` enum (defined in `Feature.h`) can be `DefaultNo`, `DefaultYes`, or `Obsolete`. This last value is subtle: obsolete amendments are still registered so that nodes running this software don't become amendment-blocked if those amendments get enabled, but the node will not emit votes for them. + +`FeatureInfo` is non-default-constructible by design — each instance must carry all three fields. + +## Two-Layer API Design + +The interface uses a two-layer pattern. The pure virtual methods form the internal amendment-table API that the implementation satisfies. Sitting on top of these are two concrete non-virtual methods — `doValidatedLedger(std::shared_ptr)` and `doVoting(std::shared_ptr, ...)` — that extract ledger state by calling `getEnabledAmendments()` and `getMajorityAmendments()` (both declared in `View.h`), then delegate to the implementation's pure-virtual overloads. + +`getEnabledAmendments()` returns the set of `uint256` amendment IDs currently active in a ledger. `getMajorityAmendments()` returns a `majorityAmendments_t` — a `std::map` associating each amendment that has crossed the validator majority threshold with the time it first achieved that majority. The time point is what drives the two-week activation window: if majority is held continuously until `closeTime >= majorityTime + firstMajorityTime`, the amendment is enabled. + +This adapter pattern keeps the implementation independent of the ledger view layer. The table's internal `doVoting` takes pre-extracted `std::set` and `majorityAmendments_t`; the public `doVoting` does the extraction itself from a `ReadView`. + +## The Voting Pipeline + +When consensus is building the initial transaction set for a new ledger, it calls the public `doVoting`. That method: + +1. Delegates to the abstract `doVoting` to compute a `std::map` of actions, where the key is an amendment ID and the value is a flags field (zero means "enable this amendment"). +2. For each action, constructs an `STTx` of type `ttAMENDMENT` — a pseudo-transaction that carries the amendment ID and target ledger sequence. These are not signed user transactions; they are injected directly by the consensus engine. +3. Adds each pseudo-transaction to the `initialPosition` `SHAMap` under `tnTRANSACTION_NM` node type. + +This is how amendments enter the ledger: they become pseudo-transactions in the consensus-agreed transaction set, which validators then process as part of closing the flag ledger. + +The companion `doValidation` method runs in the opposite direction — it's called when a validator is building its `STValidation` message, and it returns the list of amendment IDs the node wishes to vote for, which the consensus layer then embeds in the outgoing validation. + +## Amendment Blocking Detection + +Two methods guard against running unsupported code: `hasUnsupportedEnabled()` returns `true` if any amendment active on the network is not present in the node's supported list. `firstUnsupportedExpected()` returns an `optional` representing when the first such amendment is projected to reach its activation window. Together they allow the application layer to warn operators and, eventually, halt participation before the node starts misprocessing ledgers. + +## `needValidatedLedger` and State Efficiency + +`needValidatedLedger(LedgerIndex seq)` exists as an optimization gate. The abstract `doValidatedLedger(LedgerIndex, std::set, majorityAmendments_t)` only needs to run when something relevant could have changed — primarily at flag ledgers (multiples of 256 in XRPL). Calling `needValidatedLedger` first avoids the cost of extracting amendment state from every validated ledger when the overwhelming majority will have no effect on amendment voting outcomes. + +## `trustChanged` and Anti-Flapping + +`trustChanged(hash_set const& allTrusted)` notifies the table whenever the UNL (Unique Node List) changes. The implementation's `TrustedVotes` class (in `AmendmentTable.cpp`) uses this to update its per-validator vote cache. Rather than counting only the votes present in the current round's validations, `TrustedVotes` retains the last known vote from each trusted validator with a 24-hour expiration. This prevents amendment vote "flapping" where a temporarily offline validator causes an amendment's apparent support to oscillate across the 80% supermajority threshold on successive flag ledgers. + +## Factory Function + +`make_AmendmentTable` (declared both here and repeated in `AmendmentTableImpl.h`) creates the concrete implementation. Its parameters reflect the complete configuration surface: a `ServiceRegistry` reference, the `majorityTime` duration (the length of sustained supermajority required before activation), the vector of `FeatureInfo` for all supported amendments, and two `Section` objects from the server config representing externally-forced enabled and vetoed amendments. The vetoed set allows node operators to withhold votes from specific amendments regardless of the node's compiled-in `VoteBehavior`. \ No newline at end of file diff --git a/include/xrpl/ledger/ApplyView.h.ai.json b/include/xrpl/ledger/ApplyView.h.ai.json new file mode 100644 index 0000000000..b13616891a --- /dev/null +++ b/include/xrpl/ledger/ApplyView.h.ai.json @@ -0,0 +1,171 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 70, + "name": "ApplyView" + } + ], + "description": "Defines the ApplyView interface for writable views to an XRPL ledger, including transaction application flags, directory management, and hooks for credit and owner count changes. Also provides low-level directory helper functions in the xrpl::directory namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/ApplyView.h", + "functions": [ + { + "args": [ + "ApplyFlags const& lhs", + "ApplyFlags const& rhs" + ], + "lineno": 23, + "name": "operator|" + }, + { + "args": [ + "ApplyFlags const& lhs", + "ApplyFlags const& rhs" + ], + "lineno": 38, + "name": "operator&" + }, + { + "args": [ + "ApplyFlags const& flags" + ], + "lineno": 48, + "name": "operator~" + }, + { + "args": [ + "ApplyFlags& lhs", + "ApplyFlags const& rhs" + ], + "lineno": 54, + "name": "operator|=" + }, + { + "args": [ + "ApplyFlags& lhs", + "ApplyFlags const& rhs" + ], + "lineno": 59, + "name": "operator&=" + }, + { + "args": [ + "Keylet const& directory", + "Keylet const& key", + "std::function const&)> const& describe" + ], + "lineno": 168, + "name": "dirAppend" + }, + { + "args": [ + "Keylet const& directory", + "uint256 const& key", + "std::function const&)> const& describe" + ], + "lineno": 196, + "name": "dirInsert" + }, + { + "args": [ + "Keylet const& directory", + "Keylet const& key", + "std::function const&)> const& describe" + ], + "lineno": 203, + "name": "dirInsert" + }, + { + "args": [ + "Keylet const& directory", + "std::uint64_t page", + "uint256 const& key", + "bool keepRoot" + ], + "lineno": 217, + "name": "dirRemove" + }, + { + "args": [ + "Keylet const& directory", + "std::uint64_t page", + "Keylet const& key", + "bool keepRoot" + ], + "lineno": 220, + "name": "dirRemove" + }, + { + "args": [ + "Keylet const& directory", + "std::function const&" + ], + "lineno": 227, + "name": "dirDelete" + }, + { + "args": [ + "Keylet const& directory" + ], + "lineno": 234, + "name": "emptyDirDelete" + }, + { + "args": [ + "ApplyView& view", + "Keylet const& directory", + "uint256 const& key", + "std::function const&)> const& describe" + ], + "lineno": 246, + "name": "createRoot" + }, + { + "args": [ + "ApplyView& view", + "Keylet const& directory", + "SLE::ref start" + ], + "lineno": 252, + "name": "findPreviousPage" + }, + { + "args": [ + "ApplyView& view", + "SLE::ref node", + "std::uint64_t page", + "bool preserveOrder", + "STVector256& indexes", + "uint256 const& key" + ], + "lineno": 255, + "name": "insertKey" + }, + { + "args": [ + "ApplyView& view", + "std::uint64_t page", + "SLE::pointer node", + "std::uint64_t nextPage", + "SLE::ref next", + "uint256 const& key", + "Keylet const& directory", + "std::function const&)> const& describe" + ], + "lineno": 263, + "name": "insertPage" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 244, + "name": "directory" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/ApplyView.h.ai.md b/include/xrpl/ledger/ApplyView.h.ai.md new file mode 100644 index 0000000000..c1f990a909 --- /dev/null +++ b/include/xrpl/ledger/ApplyView.h.ai.md @@ -0,0 +1,97 @@ +# `include/xrpl/ledger/ApplyView.h` + +## Role in the System + +`ApplyView` is the central abstraction for applying a transaction to an XRPL ledger. It extends `ReadView` (the read-only layer) by adding mutating operations, directory management, and a hook mechanism that powers the payment sandbox. Everything that modifies ledger state during transaction processing goes through this interface — offer placement, trust line updates, account creation, fee destruction — all routed through `ApplyView` so changes can be journaled, committed, or discarded atomically. + +The class sits at the intersection of three design concerns: mutation, observability (hooks), and protocol-level directory structure. Its header bundles all three, plus the `ApplyFlags` bitmask that configures how each transaction is processed. + +--- + +## `ApplyFlags` — Transaction Processing Context + +The `ApplyFlags` enum is a bitmask carried through every transaction application call, shaping the engine's behavior: + +- `tapFAIL_HARD` (0x10): the transaction came from a local node with `fail_hard` set. The engine should not retry and should produce a hard failure that claims fees. +- `tapRETRY` (0x20): this is not the final pass; soft failures (insufficient balance, sequence mismatch) are allowed. +- `tapUNLIMITED` (0x400): from a trusted source with elevated privileges; certain limits are relaxed. +- `tapBATCH` (0x800): the transaction is part of a batch transaction. +- `tapDRY_RUN` (0x1000): simulate the transaction only — don't apply changes and skip signature checks. + +The bitwise operators (`|`, `&`, `~`, `|=`, `&=`) are all defined as `constexpr` using `safe_cast` from ``, which prevents implicit conversion to the underlying integer type. The module verifies both commutativity and correctness with `static_assert` at compile time, which is a useful guard against future flag value collisions. + +--- + +## The `ApplyView` Class: Checkout-Modify-Commit Pattern + +`ApplyView` inherits from `ReadView`, which provides read-only access to ledger state, fees, rules, and the transaction map. `ApplyView` adds the write protocol: + +**`peek(k)`** — "checks out" a `SLE` (Serialized Ledger Entry) for modification. The caller receives an owning `shared_ptr` that may be freely mutated in place. This is distinct from `read()`, which returns `shared_ptr`. + +**`update(sle)`** — "checks in" the modified SLE, signaling to the underlying implementation that the entry has changed and the delta must be journaled. + +**`insert(sle)`** — introduces a new entry not obtained from `peek()`. + +**`erase(sle)`** — removes an entry previously obtained from `peek()`. + +The invariant documented in the header is strict: `update` and `erase` may only 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. + +This pattern exists to support the layered view architecture: `ApplyViewImpl` wraps an `OpenView`, and `PaymentSandbox` can wrap another `ApplyView`. Each layer journals its own changes, and calling `apply()` flushes those changes to the parent. Discarding the view discards all changes without touching the parent — enabling transactional rollback at every level. + +--- + +## Payment Hooks — The `creditHook` and `adjustOwnerCountHook` Family + +`ApplyView` declares four virtual "hooks" that default to no-ops: + +```cpp +virtual void creditHookIOU(...); +virtual void creditHookMPT(...); +virtual void issuerSelfDebitHookMPT(...); +virtual void adjustOwnerCountHook(...); +``` + +These exist exclusively to support `PaymentSandbox`. The XRPL payment engine must prevent accounts from spending assets they received within the same payment path — otherwise a circular path could manufacture liquidity. `PaymentSandbox` overrides all four hooks to record credits and owner-count changes in a `DeferredCredits` table, and it overrides `balanceHookIOU` / `balanceHookMPT` on the read side to subtract those deferred credits from reported balances. + +The default no-op implementations carry `XRPL_ASSERT` checks that verify the `STAmount` holds the expected asset type, providing a cheap sanity guard when hooks are not active. + +The `issuerSelfDebitHookMPT` hook handles a more subtle MPT (Multi-Purpose Token) edge case: when an issuer holds an offer that sells MPT, executing that offer in reverse (as the payment engine does) would temporarily inflate the `OutstandingAmount` beyond `MaximumAmount`. The hook lets `PaymentSandbox` track the issuer's "self-debits" so that the net available-to-issue calculation remains correct across the entire payment. + +--- + +## Directory Management + +The XRPL ledger represents ordered collections — offer books, account-owned objects — as **paged linked-list directories** stored as `ltDIR_NODE` ledger entries. Each page holds up to `dirNodeMaxEntries` `uint256` keys. Pages are linked via `sfIndexNext` / `sfIndexPrevious` fields; page 0 is always the root and serves as the directory anchor. + +`ApplyView` provides the public interface for managing these directories. All variants delegate to the private `dirAdd(preserveOrder, ...)`: + +- **`dirAppend`** — enforces insertion order (append-to-tail). This is used only for offer book directories (`ltOFFER`), where chronological ordering affects priority during offer matching. The implementation guards against non-offer keys with `UNREACHABLE`, making misuse a compile-detectable logic error. + +- **`dirInsert`** — inserts in sorted order within each page. Used for account-owned object directories. Pages may be legacy unsorted pages, so `insertKey()` re-sorts on every touch. + +Both return the 0-indexed page number where the entry was stored, or `std::nullopt` if the page counter overflowed. Overflow detection uses deliberate unsigned wraparound arithmetic (`++page; if (page == 0)`) verified by a `static_assert` against signed-integer UB. + +- **`dirRemove`** — removes a single key, collapses now-empty non-root pages, and optionally preserves the root even if it becomes empty (`keepRoot = true`). It also contains legacy handling for older ledger data where empty trailing pages could be left behind. + +- **`dirDelete`** — bulk-removes all pages of a directory, invoking a callback for each key. Used when an entire account's offer set or similar collection must be cleaned up. + +- **`emptyDirDelete`** — removes the root only if the directory is truly empty; contains the same legacy empty-trailing-page cleanup. + +--- + +## The `xrpl::directory` Namespace + +The implementation in `ApplyView.cpp` factors out four low-level helpers into `xrpl::directory`: + +- `createRoot` — allocates the root page and inserts the first key +- `findPreviousPage` — walks the back-pointer to locate the last-used page +- `insertKey` — appends or binary-inserts a key into a page's `sfIndexes` vector +- `insertPage` — allocates a new trailing page and links it into the chain + +These functions are exposed in the header with an explicit warning: *"Don't use them unless you really, really know what you're doing."* They are declared in the header because some specialized callers (tests, tooling) need access to individual steps, but the intent is that transaction processing always goes through `dirAppend` / `dirInsert` / `dirRemove`. + +--- + +## Relationship to Sibling Types + +`ApplyViewImpl` is the concrete production implementation: it wraps a `ReadView const*`, stores its own delta map, and provides the `apply()` method that writes the journaled changes to an `OpenView`. `PaymentSandbox` is the other concrete implementation, layerable on top of any `ApplyView` to add the deferred-credit tracking needed for multi-hop payments. Both classes inherit through `detail::ApplyViewBase`, which provides the delta journaling. Neither is exposed directly to transaction processors — they always receive an `ApplyView&`, allowing the same transaction code to run correctly whether or not a payment sandbox is active. \ No newline at end of file diff --git a/include/xrpl/ledger/ApplyViewImpl.h.ai.json b/include/xrpl/ledger/ApplyViewImpl.h.ai.json new file mode 100644 index 0000000000..5ed1fdfad0 --- /dev/null +++ b/include/xrpl/ledger/ApplyViewImpl.h.ai.json @@ -0,0 +1,93 @@ +{ + "args": [ + { + "lineno": 33, + "name": "to" + }, + { + "lineno": 34, + "name": "tx" + }, + { + "lineno": 35, + "name": "ter" + }, + { + "lineno": 36, + "name": "parentBatchId" + }, + { + "lineno": 37, + "name": "isDryRun" + }, + { + "lineno": 38, + "name": "j" + }, + { + "lineno": 49, + "name": "amount" + }, + { + "lineno": 65, + "name": "target" + }, + { + "lineno": 66, + "name": "func" + } + ], + "classes": [ + { + "args": [ + "ReadView const* base", + "ApplyFlags flags" + ], + "lineno": 13, + "name": "ApplyViewImpl" + } + ], + "description": "Defines the ApplyViewImpl class, an editable and discardable ledger view used to build transaction metadata and apply transactions within the XRPL ledger implementation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/ApplyViewImpl.h", + "functions": [ + { + "args": [ + "to", + "tx", + "ter", + "parentBatchId", + "isDryRun", + "j" + ], + "lineno": 32, + "name": "apply" + }, + { + "args": [ + "amount" + ], + "lineno": 48, + "name": "deliver" + }, + { + "args": [], + "lineno": 59, + "name": "size" + }, + { + "args": [ + "target", + "func" + ], + "lineno": 63, + "name": "visit" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/ApplyViewImpl.h.ai.md b/include/xrpl/ledger/ApplyViewImpl.h.ai.md new file mode 100644 index 0000000000..2e58007af0 --- /dev/null +++ b/include/xrpl/ledger/ApplyViewImpl.h.ai.md @@ -0,0 +1,35 @@ +# `ApplyViewImpl` — Transaction Apply View + +## Role in the System + +`ApplyViewImpl` is the concrete, per-transaction scratch-pad that the XRPL ledger engine uses whenever it needs to execute a single transaction against a view of ledger state. It sits at the top of a three-layer inheritance chain: `ReadView` → `ApplyView` (adds mutation semantics) → `detail::ApplyViewBase` (adds buffered state storage) → `ApplyViewImpl` (adds metadata construction and final commit). The design separates *reading* ledger state from *mutating* it, and separates *mutating* from *committing* — giving transaction processing the ability to speculatively apply changes and then either commit them or discard the entire view without touching the parent `OpenView`. + +## Class Hierarchy + +`ApplyViewImpl` inherits from `detail::ApplyViewBase`, which itself inherits from both `ApplyView` and `RawView`. `ApplyViewBase` holds the three key protected members that `ApplyViewImpl` operates through: `flags_` (the `ApplyFlags` bitmask), `base_` (a non-owning `const` pointer to the underlying `ReadView`), and `items_` (an `ApplyStateTable` that buffers every insert, modify, erase, and XRP destruction operation as a map of `key → (Action, SLE)`). `ApplyViewImpl` adds only one data member of its own: `deliver_`, an `optional` for tracking payment delivery amounts. + +## `apply()` — The Point of No Return + +The central method is `apply(OpenView& to, STTx const& tx, TER ter, std::optional parentBatchId, bool isDryRun, beast::Journal j)`. The comment in the header is deliberate and important: after `apply()` is called, the only valid operation on the object is destruction. This is not enforced by the type system, but the intent is clear — `apply()` hands ownership of the buffered mutations down to `items_.apply(...)` in `ApplyStateTable`, which drains them into the target `OpenView` while simultaneously constructing and returning a `TxMeta` object capturing before/after state for every modified SLE. The `TxMeta` is returned as `std::optional` to handle `isDryRun` scenarios where metadata is computed but the ledger changes are not actually committed. + +The `parentBatchId` parameter reflects support for batch transactions: when a transaction is part of a batch, the metadata carries a reference to the parent batch transaction ID, linking individual results back to their containing batch context via `tapBATCH`. + +## `deliver()` — Payment Metadata Annotation + +`deliver(STAmount const& amount)` is a one-shot setter that stores the delivered currency amount for use when building transaction metadata. In XRPL payment transactions, the amount actually delivered to the destination can differ from the amount sent (due to pathfinding, exchange rates, or partial payments). The `DeliveredAmount` metadata field communicates this to clients. Callers set this *before* calling `apply()`; if it is never set, `deliver_` remains `std::nullopt` and the `DeliveredAmount` field is omitted from the resulting metadata. The actual threading of this value into `TxMeta` happens inside `ApplyStateTable::apply()`. + +## Copy/Move Semantics + +`ApplyViewImpl` carefully disables copy construction, copy assignment, and move assignment, while allowing move construction. This is consistent with the ownership model: the object exclusively owns its buffered mutation state, and only one view should ever be in a position to commit to the parent `OpenView`. Allowing moves but not copies prevents accidental duplication of a pending transaction's state table while still permitting the object to be constructed and returned from factory functions without heap allocation. + +## `size()` and `visit()` + +`size()` delegates to `items_.size()`, returning the count of modified entries in the state table. `visit()` delegates to `items_.visit(to, func)`, iterating every modified SLE and invoking a callback with the key, a deletion flag, and `shared_ptr`s to the before and after states. These are used during batch processing — callers can inspect the changes accumulated for one transaction before deciding whether to apply them to the shared `OpenView`, enabling partial-commit semantics for batch transactions. + +## Relationship to `OpenView` + +Both `apply()` and `visit()` take `OpenView&` as their target. `OpenView` is the writable accumulator for an entire ledger's worth of transactions; `ApplyViewImpl` provides the per-transaction isolation layer. This mirrors a standard copy-on-write pattern: the base `ReadView` is shared and immutable, all mutations are buffered in `items_`, and only `apply()` flushes them forward. Because `ApplyViewBase` holds `base_` as a raw `const*` rather than a shared pointer, the caller is responsible for ensuring the underlying view outlives the `ApplyViewImpl` — a deliberate performance choice that avoids atomic reference count traffic on the hot path. + +## `ApplyFlags` and Contextual Behavior + +The `ApplyFlags` passed at construction time flow through `flags_` and are accessible via `ApplyViewBase::flags()`. Flags such as `tapRETRY`, `tapFAIL_HARD`, `tapUNLIMITED`, `tapBATCH`, and `tapDRY_RUN` allow the transaction engine to signal its execution context to the view. For example, `tapDRY_RUN` causes the engine to compute outcomes without finalizing state, which is reflected in the `isDryRun` parameter ultimately passed to `ApplyStateTable`. The flag `tapBATCH` signals that the current view is operating within a batch context, which affects how `parentBatchId` is recorded in the resulting `TxMeta`. \ No newline at end of file diff --git a/include/xrpl/ledger/BookDirs.h.ai.json b/include/xrpl/ledger/BookDirs.h.ai.json new file mode 100644 index 0000000000..fbdd63c177 --- /dev/null +++ b/include/xrpl/ledger/BookDirs.h.ai.json @@ -0,0 +1,100 @@ +{ + "args": [ + { + "lineno": 58, + "name": "view" + }, + { + "lineno": 58, + "name": "root" + }, + { + "lineno": 58, + "name": "dir_key" + }, + { + "lineno": 34, + "name": "other" + } + ], + "classes": [ + { + "args": [ + "ReadView const&", + "Book const&" + ], + "lineno": 8, + "name": "BookDirs" + }, + { + "args": [], + "lineno": 27, + "name": "BookDirs::const_iterator" + } + ], + "description": "Defines the BookDirs class and its const_iterator for iterating over order book directories in the XRPL ledger, providing access to ledger entries representing order books.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/BookDirs.h", + "functions": [ + { + "args": [ + "ReadView const&", + "Book const&" + ], + "lineno": 17, + "name": "BookDirs" + }, + { + "args": [], + "lineno": 19, + "name": "begin" + }, + { + "args": [], + "lineno": 22, + "name": "end" + }, + { + "args": [ + "const_iterator const& other" + ], + "lineno": 34, + "name": "operator==" + }, + { + "args": [ + "const_iterator const& other" + ], + "lineno": 37, + "name": "operator!=" + }, + { + "args": [], + "lineno": 41, + "name": "operator*" + }, + { + "args": [], + "lineno": 44, + "name": "operator->" + }, + { + "args": [], + "lineno": 50, + "name": "operator++" + }, + { + "args": [ + "int" + ], + "lineno": 53, + "name": "operator++" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/BookDirs.h.ai.md b/include/xrpl/ledger/BookDirs.h.ai.md new file mode 100644 index 0000000000..e11dc429e7 --- /dev/null +++ b/include/xrpl/ledger/BookDirs.h.ai.md @@ -0,0 +1,37 @@ +## `BookDirs.h` — Forward Iterator Over XRPL Order Book Entries + +The XRPL decentralized exchange stores offers in a two-level ledger directory structure. A *book* groups all offers trading one currency pair in one direction, and within that book, separate directory pages are keyed by *quality* — the encoded exchange rate of the offer. `BookDirs` presents this multi-level structure as a flat, range-based sequence of `SLE` (state ledger entry) objects, letting callers iterate every offer in a book with a standard `for`-range loop without reasoning about quality boundaries or directory pagination. + +### The Ledger Directory Structure It Hides + +In the ledger, a book's root key is computed from `getBookBase(book)` via `keylet::page`. Within that keyspace, individual directory pages are addressed at quality-encoded offsets up to `getQualityNext(root)`, a sentinel key marking the end of the book's quality range. Each directory page holds a `sfIndexes` vector of `uint256` keys pointing to the actual `Offer` SLEs, and the page carries a `sfIndexNext` field linking it to overflow pages at the same quality level. + +The `BookDirs` constructor calls `view_->succ(root_, next_quality_)` to find the first existing quality directory in the key-space range. If the book is empty, `succ` returns no value and `key_` is set to `beast::zero`. When a directory is found, the constructor eagerly calls `cdirFirst` to load the first page and position `sle_`, `entry_`, and `index_` at the initial offer — state that is then copied into the iterator returned by `begin()`. + +### `begin()` and `end()` Sentinel Design + +Both `begin()` and `end()` construct a `const_iterator` with the same `root_` and `key_` arguments. The distinction is in what `begin()` additionally copies: `next_quality_`, `sle_`, `entry_`, and `index_` are only populated on the begin-side iterator. The end sentinel leaves `entry_` at zero and `index_` at `beast::zero`. Equality in `operator==` compares `entry_`, `cur_key_`, and `index_` — so when the iterator exhausts all offers and resets these fields back to the initial values (matching the end sentinel's state), the loop terminates. + +This is a non-obvious choice. Rather than using a dedicated "past-the-end" flag or a separate sentinel type, the iterator recycles the starting state as the end condition. The implementation in `operator++()` explicitly does this: when `cdirNext` signals exhaustion and no further quality directory exists via `view_->succ`, it sets `cur_key_ = key_`, `entry_ = 0`, and `index_ = zero` — mirroring the end sentinel exactly. + +### Iterator Advancement Across Quality Boundaries + +`operator++()` demonstrates the two-layer traversal logic. First it calls `cdirNext`, which walks within a single directory page and spills to overflow pages of the same quality via `sfIndexNext`. When `cdirNext` returns false, `index_` being zero signals that this quality directory is fully exhausted; the code then calls `view_->succ(++cur_key_, next_quality_)` to find the next quality bucket. If another quality exists, `cdirFirst` loads its first page. If not, the iterator resets to the sentinel state. + +The `index_` field thus plays a dual role: as the key of the current offer (when non-zero), and as a status signal from `cdirNext` about *why* iteration stopped (when zero). This is a low-level protocol between `BookDirs` and the `cdirFirst`/`cdirNext` helpers defined in `DirectoryHelpers.h`, both of which are explicitly marked deprecated in favor of iterator-based models — yet `BookDirs` itself is that iterator-based model, and it still leans on these helpers internally. + +### Lazy Dereference and `operator*` + +The iterator's `operator*()` does not return the SLE that `cdirNext` already loaded; instead it re-reads `view_->read(keylet::offer(index_))` each time and stores the result in `mutable std::optional cache_`. The cache is cleared in `operator++()`. This avoids holding a redundant reference to the directory page SLE alongside the offer SLE, and allows `operator->()` to safely return a pointer to the cached value without a temporary lifetime problem. + +### Static `Journal` on the Iterator + +`const_iterator` holds a `static beast::Journal j_` initialized to a null sink. Rather than threading a `Journal` through every iterator copy or through `BookDirs::BookDirs`, the class owns a single process-wide logger. This keeps iterator objects small and copyable without overhead, accepting that diagnostic output from iterator internals is opaque to the caller's logging context. + +### Relationship to `Dir.h` + +`Dir` is a general-purpose forward iterator over a single ledger directory — it was designed for NFTokenOffer directories and unit tests. `BookDirs` solves a harder problem: it must cross *multiple quality-keyed directories* within a book's keyspace, which requires the `succ` lookup at construction and at each quality boundary in `operator++`. `BookDirs` does not use `Dir` internally; both call the same `cdirFirst`/`cdirNext` primitives but handle directory traversal independently. + +### Lifetime and Invariant Constraints + +`BookDirs` and `const_iterator` both hold raw pointers to the `ReadView`. The caller must ensure the `ReadView` outlives any iterators. The `const_iterator` default constructor is public (required by the forward-iterator concept), but it leaves `view_` null; `operator==` checks for null views and returns false, making default-constructed iterators safe as placeholders. The private constructor taking `(view, root, dir_key)` is accessible only to `BookDirs` via the `friend class BookDirs` declaration, preventing external code from constructing iterators in arbitrary states. The constructor asserts `root_ != beast::zero` to catch misconfigured `Book` arguments early. \ No newline at end of file diff --git a/include/xrpl/ledger/BookListeners.h.ai.json b/include/xrpl/ledger/BookListeners.h.ai.json new file mode 100644 index 0000000000..177d552bd3 --- /dev/null +++ b/include/xrpl/ledger/BookListeners.h.ai.json @@ -0,0 +1,48 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "BookListeners" + } + ], + "description": "Defines the BookListeners class, which manages subscriptions and publishes messages to clients interested in updates from a specific order book.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/BookListeners.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "BookListeners" + }, + { + "args": [ + "sub" + ], + "lineno": 19, + "name": "addSubscriber" + }, + { + "args": [ + "sub" + ], + "lineno": 24, + "name": "removeSubscriber" + }, + { + "args": [ + "jvObj", + "havePublished" + ], + "lineno": 34, + "name": "publish" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/BookListeners.h.ai.md b/include/xrpl/ledger/BookListeners.h.ai.md new file mode 100644 index 0000000000..46709af686 --- /dev/null +++ b/include/xrpl/ledger/BookListeners.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/ledger/BookListeners.h` + +## Role in the System + +`BookListeners` is the fan-out layer that connects the XRPL decentralized exchange (DEX) to WebSocket subscribers. When a transaction modifies offers in an order book, the server must notify every client that has subscribed to that book via the `subscribe` RPC command. `BookListeners` is the per-book object that owns and manages that subscriber set, and dispatches serialized transaction JSON to each of them. + +One `BookListeners` instance exists for each `Book` (currency pair) that has at least one active subscriber. The `OrderBookDB` interface — and its concrete `OrderBookDBImpl` — is the registry that creates and looks up these instances. When `OrderBookDBImpl::processTxn()` encounters a modified offer node in a transaction's metadata, it resolves the affected book to a `BookListeners` pointer and calls `publish()`. + +## Class Design + +The class is deliberately minimal: a protected `hash_map` keyed by subscriber sequence number and a `recursive_mutex`. Subscribers are stored as `InfoSub::wptr` (i.e., `weak_ptr`), not as strong pointers. This is the critical ownership decision — `BookListeners` must not keep subscribers alive. `InfoSub` objects are owned by the connection layer; if a client disconnects, its `InfoSub` is destroyed. The weak pointer then expires, and `BookListeners` detects this lazily. + +Subscriber identity is tracked by `uint64_t` sequence number (`InfoSub::getSeq()`) rather than by raw pointer. This allows `removeSubscriber()` to erase by ID without needing the original `shared_ptr`, which is important during `InfoSub` destruction where the subscription cleanup path may not have a live pointer available. + +## The `publish()` Method and Duplicate Suppression + +```cpp +void publish(MultiApiJson const& jvObj, hash_set& havePublished); +``` + +The `havePublished` set is the key non-obvious mechanism here. A single transaction can touch multiple order books simultaneously — for example, a cross-currency payment may affect both the XRP/USD book and the USD/BTC book. If a client has subscribed to both books, a naïve implementation would deliver the same transaction notification twice. + +`OrderBookDBImpl::processTxn()` creates one `havePublished` set per transaction and passes it (by reference) to every `BookListeners::publish()` call for that transaction. Inside `publish()`, the code calls `havePublished.emplace(p->getSeq()).second`, which returns `true` only when the subscriber ID was freshly inserted. Only then is the message actually sent via `p->send()`. Subsequent calls for the same transaction on other books that share a subscriber are silently skipped. + +## Multi-Version JSON Delivery + +`publish()` receives a `MultiApiJson const& jvObj` rather than a plain `Json::Value`. `MultiApiJson` (aliased from `detail::MultiApiJson`) is a fixed-size array of `Json::Value` objects, one per supported API version. The transaction serialization layer fills this once, before `publish()` is ever called. + +Inside `publish()`, each subscriber's API version is fetched via `p->getApiVersion()`, and the correct per-version JSON is selected using `jvObj.visit(p->getApiVersion(), [&](Json::Value const& jv) { p->send(jv, true); })`. This avoids re-serializing the transaction for every subscriber and every API version — the version-specific JSON objects are computed once upstream and then indexed cheaply here. + +## Lazy Expiry of Dead Subscribers + +During `publish()`, after `it->second.lock()` fails (the `InfoSub` has been destroyed), the dead entry is erased in-place via `it = mListeners.erase(it)`. This means the map self-cleans during normal operation without requiring a separate sweep or background GC pass. `addSubscriber()` and `removeSubscriber()` handle explicit lifecycle events (connect and disconnect), but the lazy erase in `publish()` provides a safety net for connections that disappear without a clean unsubscribe. + +## Locking + +`std::recursive_mutex` is chosen over a plain `std::mutex`. In practice, none of the three public methods call each other (so no direct re-entrancy exists on `mLock`), but `recursive_mutex` leaves the door open for future callers that might already hold the lock on a different code path. All three public methods take a `std::lock_guard` for the entirety of their execution, which keeps the implementation straightforward at the cost of holding the lock across `p->send()` during publish. This means subscriber callbacks execute under the lock — a tradeoff that favors simplicity and correctness over throughput on high-subscriber-count books. + +## Relationship to `OrderBookDB` + +`BookListeners` has no knowledge of which `Book` it belongs to — it is a pure subscriber container. `OrderBookDB` (specifically `OrderBookDBImpl`) maps `Book` → `BookListeners::pointer` in its own `mListeners` hash map, and provides `getBookListeners()` and `makeBookListeners()` to retrieve or create them. The `shared_ptr` type alias (`BookListeners::pointer`) is how callers hold references without exposing raw owning pointers. \ No newline at end of file diff --git a/include/xrpl/ledger/CachedSLEs.h.ai.json b/include/xrpl/ledger/CachedSLEs.h.ai.json new file mode 100644 index 0000000000..aef881a5eb --- /dev/null +++ b/include/xrpl/ledger/CachedSLEs.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines a type alias CachedSLEs for a tagged cache of ledger entries within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/CachedSLEs.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/CachedSLEs.h.ai.md b/include/xrpl/ledger/CachedSLEs.h.ai.md new file mode 100644 index 0000000000..4b7a4e8583 --- /dev/null +++ b/include/xrpl/ledger/CachedSLEs.h.ai.md @@ -0,0 +1,17 @@ +# `include/xrpl/ledger/CachedSLEs.h` + +This file declares exactly one thing: a named type alias that pins together the two halves of the ledger object caching subsystem. + +```cpp +using CachedSLEs = TaggedCache; +``` + +`SLE` (`STLedgerEntry`) is the in-memory representation of a single entry in the ledger state tree — an account root, offer, trust line, escrow, and so on. Reading an SLE from the database requires deserializing a binary blob into a rich, heap-allocated object. The purpose of `CachedSLEs` is to ensure that once an entry has been deserialized, every code path that needs it during a given time window shares the same object rather than re-deserializing from disk. + +The `const` qualifier on the mapped type is load-bearing. `TaggedCache` documents that callers must not modify stored objects unless they hold a lock over all cache operations. Instantiating on `SLE const` enforces that invariant at compile time, making the entire cache effectively immutable from the user's perspective and safe to share across threads without additional locking on the objects themselves. + +The key type `uint256` is the cryptographic hash (digest) of the serialized SLE — not an account ID or ledger key. This integrates directly with `DigestAwareReadView`, which can return the on-disk hash for any ledger entry. `CachedView` (in `CachedView.h`) uses this: it wraps a `DigestAwareReadView`, delegates `read()` calls to `CachedSLEs::fetch(digest, handler)`, and only falls through to the underlying store on a cache miss. The handler supplies the freshly deserialized `SLE const` which is then inserted and returned. + +In `Application.cpp`, the single application-wide `CachedSLEs` instance is constructed with a target size of `0` (meaning no fixed count limit) and a one-minute expiration window. `TaggedCache::sweep()` is called periodically by `Application` to demote cached (strong-reference) entries to weak references and eventually reclaim them, with the sweep size logged for debugging. `OpenLedger` holds a non-owning reference to the same instance, passing it through to each `CachedView` it creates so that all open-ledger reads share the same warm cache. + +The alias also serves a documentation role: callers that include this header see `CachedSLEs` — a self-describing name — rather than a bare `TaggedCache` instantiation. Any future change to the underlying container type (key hasher, pointer policy, mutex type) can be made here in one place, opaquely to all consumers. \ No newline at end of file diff --git a/include/xrpl/ledger/CachedView.h.ai.json b/include/xrpl/ledger/CachedView.h.ai.json new file mode 100644 index 0000000000..93c842bf08 --- /dev/null +++ b/include/xrpl/ledger/CachedView.h.ai.json @@ -0,0 +1,188 @@ +{ + "args": [ + { + "lineno": 27, + "name": "base" + }, + { + "lineno": 27, + "name": "cache" + }, + { + "lineno": 34, + "name": "k" + }, + { + "lineno": 37, + "name": "k" + }, + { + "lineno": 60, + "name": "key" + }, + { + "lineno": 60, + "name": "last" + }, + { + "lineno": 75, + "name": "key" + }, + { + "lineno": 90, + "name": "key" + }, + { + "lineno": 95, + "name": "key" + }, + { + "lineno": 102, + "name": "key" + } + ], + "classes": [ + { + "args": [ + "DigestAwareReadView const* base", + "CachedSLEs& cache" + ], + "lineno": 13, + "name": "CachedViewImpl" + }, + { + "args": [ + "std::shared_ptr const& base", + "CachedSLEs& cache" + ], + "lineno": 109, + "name": "CachedView" + } + ], + "description": "Provides a CachedView wrapper around DigestAwareReadView to add caching of ledger entries using CachedSLEs, with thread safety and encapsulation features.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/CachedView.h", + "functions": [ + { + "args": [ + "DigestAwareReadView const* base", + "CachedSLEs& cache" + ], + "lineno": 27, + "name": "CachedViewImpl" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 34, + "name": "exists" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 37, + "name": "read" + }, + { + "args": [], + "lineno": 40, + "name": "open" + }, + { + "args": [], + "lineno": 45, + "name": "header" + }, + { + "args": [], + "lineno": 50, + "name": "fees" + }, + { + "args": [], + "lineno": 55, + "name": "rules" + }, + { + "args": [ + "key_type const& key", + "std::optional const& last" + ], + "lineno": 60, + "name": "succ" + }, + { + "args": [], + "lineno": 65, + "name": "slesBegin" + }, + { + "args": [], + "lineno": 70, + "name": "slesEnd" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 75, + "name": "slesUpperBound" + }, + { + "args": [], + "lineno": 80, + "name": "txsBegin" + }, + { + "args": [], + "lineno": 85, + "name": "txsEnd" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 90, + "name": "txExists" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 95, + "name": "txRead" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 102, + "name": "digest" + }, + { + "args": [ + "std::shared_ptr const& base", + "CachedSLEs& cache" + ], + "lineno": 117, + "name": "CachedView" + }, + { + "args": [], + "lineno": 127, + "name": "base" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/CachedView.h.ai.md b/include/xrpl/ledger/CachedView.h.ai.md new file mode 100644 index 0000000000..cfb87709cb --- /dev/null +++ b/include/xrpl/ledger/CachedView.h.ai.md @@ -0,0 +1,51 @@ +# `include/xrpl/ledger/CachedView.h` + +## Role in the System + +`CachedView.h` implements a transparent caching layer over the ledger's read-only view hierarchy. Its primary purpose is to avoid redundant deserialization of `SLE` (Serialized Ledger Entry) objects when the same ledger state entries are accessed repeatedly during transaction processing. The canonical instantiation, `CachedLedger`, defined in `Ledger.h` as `using CachedLedger = CachedView`, wraps the immutable closed ledger that serves as the base for applying new transactions. + +## Class Structure: Implementation Split + +The design separates a non-template base class `detail::CachedViewImpl` from the public-facing template `CachedView`. This is a deliberate architectural choice to avoid template bloat: all caching logic lives in `CachedViewImpl` and is compiled once, while `CachedView` is merely a thin wrapper that adds `shared_ptr` ownership and a `static_assert` that `Base` derives from `DigestAwareReadView`. + +`CachedViewImpl` holds a raw reference `base_` — safe because `CachedView` stores the `shared_ptr` that keeps the base alive. Both classes delete copy constructors and assignment operators, enforcing that a cached view always represents a unique, coherent view over a specific snapshot. + +## Two Levels of Caching + +The design uses two distinct caches that serve complementary roles: + +1. **`CachedSLEs`** (a `TaggedCache`) is an **externally owned, process-wide** LRU cache keyed by SLE *digest* (the SHA-512Half hash of the serialized entry). Multiple `CachedView` instances over different ledgers share this cache. If two ledgers share an unchanged SLE (as closed ledgers often do), only one deserialized copy is kept in memory. The caller injects this cache at construction time. + +2. **`map_`** (an `unordered_map>`) is a **per-instance** map from ledger key (a 256-bit position in the SHAMap) to SLE digest. Its sole purpose is to avoid repeated calls to `base_.digest()`. Once a key has been resolved to its digest, subsequent reads of that key don't need to touch the underlying SHAMap node. + +## The `read()` Path + +The `read()` implementation in `CachedView.cpp` is the only non-trivial method; `exists()` simply delegates to it. The flow is: + +1. Lock `mutex_` and look up `k.key` in `map_`. If found, the digest is known locally — skip the base lookup. +2. If not in `map_`, call `base_.digest(k.key)` *outside* the lock. This query walks the SHAMap to find the hash of the leaf node, but does not deserialize the SLE. +3. If no digest exists, the key is absent; return `nullptr`. +4. Call `cache_.fetch(digest, loader)` on the shared `CachedSLEs`. The `loader` calls `base_.read(k)` only if the digest is not already in the cache. This is the expensive path: it deserializes the raw SLE bytes into a live C++ object. +5. Update statistics via static `CountedObjects::Counter` instances — `CachedView::hit`, `CachedView::hitExpired`, and `CachedView::miss` — which distinguish a full hit, a digest-hit with an expired shared-cache entry, and a total miss. +6. If the key was a miss (not in `map_`), acquire the lock again and insert `{k.key, digest}` into `map_`. +7. Validate the returned SLE type with `k.check(*sle)` before returning. + +The lock on `mutex_` is deliberately not held across steps 2-4. Holding it through a potential SHAMap traversal or deserialization would unnecessarily serialize concurrent readers. The absence of double-checked locking is intentional: two concurrent threads missing `map_` will both call `base_.digest()`, but that is idempotent, and only one will win the race to populate `map_` in step 6. + +## Thread Safety + +`mutex_` protects only `map_`. The external `CachedSLEs` cache has its own internal lock (a `std::recursive_mutex` in `TaggedCache`). The base view is a `const` reference to an immutable ledger snapshot, so all access to it is inherently safe without locking. + +## `hardened_hash` in `map_` + +The internal `map_` uses `hardened_hash<>` rather than `std::hash`. Ledger keys are derived from account IDs and object types, all of which are network-visible. A predictable hash would allow an adversary to craft transactions that flood the same hash bucket, degrading `map_` lookups from O(1) to O(n). `hardened_hash` seeds its xxhasher with 128 bits of randomness at construction time, defeating such attacks. + +## `base()` and Encapsulation + +`CachedView` exposes a `base()` accessor that returns the underlying `shared_ptr`. Its comment explicitly flags this as breaking encapsulation: callers using `base()` interact with the underlying `DigestAwareReadView` directly, bypassing both the local `map_` and the shared `CachedSLEs`. This escape hatch exists because some operations — particularly those that need the full `Ledger` type rather than just the `ReadView` interface — cannot be expressed through the view abstraction alone. + +## Relationship to Other Files + +- **`CachedSLEs.h`** — defines `CachedSLEs` as a single-line type alias: `using CachedSLEs = TaggedCache`. Separating this alias from `CachedView.h` allows the shared cache to be created and owned at a higher level (the application) and passed in. +- **`ReadView.h`** — defines `ReadView` and `DigestAwareReadView`. The `digest()` virtual method on `DigestAwareReadView` is what makes two-level caching possible: without a stable content hash per key, the shared `CachedSLEs` could not safely be used across multiple views. +- **`Ledger.h`** — defines `CachedLedger = CachedView`, the production use of this template, confirming that `Ledger` satisfies the `DigestAwareReadView` contract. \ No newline at end of file diff --git a/include/xrpl/ledger/CanonicalTXSet.h.ai.json b/include/xrpl/ledger/CanonicalTXSet.h.ai.json new file mode 100644 index 0000000000..7e3b8122f0 --- /dev/null +++ b/include/xrpl/ledger/CanonicalTXSet.h.ai.json @@ -0,0 +1,191 @@ +{ + "args": [ + { + "lineno": 24, + "name": "account" + }, + { + "lineno": 24, + "name": "seqProx" + }, + { + "lineno": 24, + "name": "id" + }, + { + "lineno": 27, + "name": "lhs" + }, + { + "lineno": 27, + "name": "rhs" + }, + { + "lineno": 88, + "name": "account" + }, + { + "lineno": 94, + "name": "txn" + }, + { + "lineno": 100, + "name": "tx" + }, + { + "lineno": 110, + "name": "salt" + }, + { + "lineno": 116, + "name": "it" + } + ], + "classes": [ + { + "args": [ + "LedgerHash const& saltHash" + ], + "lineno": 18, + "name": "CanonicalTXSet" + }, + { + "args": [ + "uint256 const& account", + "SeqProxy seqProx", + "uint256 const& id" + ], + "lineno": 23, + "name": "CanonicalTXSet::Key" + } + ], + "description": "Defines the CanonicalTXSet class, which holds and orders deferred transactions for the next consensus pass in the XRPL ledger, ensuring transactions from the same account are ordered by SeqProxy.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/CanonicalTXSet.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "Key::getAccount" + }, + { + "args": [], + "lineno": 49, + "name": "Key::getTXID" + }, + { + "args": [ + "Key const& lhs", + "Key const& rhs" + ], + "lineno": 56, + "name": "operator<" + }, + { + "args": [ + "Key const& lhs", + "Key const& rhs" + ], + "lineno": 59, + "name": "operator>" + }, + { + "args": [ + "Key const& lhs", + "Key const& rhs" + ], + "lineno": 64, + "name": "operator<=" + }, + { + "args": [ + "Key const& lhs", + "Key const& rhs" + ], + "lineno": 69, + "name": "operator>=" + }, + { + "args": [ + "Key const& lhs", + "Key const& rhs" + ], + "lineno": 74, + "name": "operator==" + }, + { + "args": [ + "Key const& lhs", + "Key const& rhs" + ], + "lineno": 79, + "name": "operator!=" + }, + { + "args": [ + "AccountID const& account" + ], + "lineno": 88, + "name": "CanonicalTXSet::accountKey" + }, + { + "args": [ + "std::shared_ptr const& txn" + ], + "lineno": 94, + "name": "CanonicalTXSet::insert" + }, + { + "args": [ + "std::shared_ptr const& tx" + ], + "lineno": 100, + "name": "CanonicalTXSet::popAcctTransaction" + }, + { + "args": [ + "LedgerHash const& salt" + ], + "lineno": 110, + "name": "CanonicalTXSet::reset" + }, + { + "args": [ + "const_iterator const& it" + ], + "lineno": 116, + "name": "CanonicalTXSet::erase" + }, + { + "args": [], + "lineno": 121, + "name": "CanonicalTXSet::begin" + }, + { + "args": [], + "lineno": 125, + "name": "CanonicalTXSet::end" + }, + { + "args": [], + "lineno": 129, + "name": "CanonicalTXSet::size" + }, + { + "args": [], + "lineno": 133, + "name": "CanonicalTXSet::empty" + }, + { + "args": [], + "lineno": 137, + "name": "CanonicalTXSet::key" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/CanonicalTXSet.h.ai.md b/include/xrpl/ledger/CanonicalTXSet.h.ai.md new file mode 100644 index 0000000000..1f40baeb13 --- /dev/null +++ b/include/xrpl/ledger/CanonicalTXSet.h.ai.md @@ -0,0 +1,50 @@ +# `CanonicalTXSet` — Ordered Transaction Queue for Consensus Application + +## Role in the System + +`CanonicalTXSet` holds transactions that have been deferred from a previous ledger-building pass and need to be reapplied in the next pass of the consensus process. Its central responsibility is enforcing a *canonical*, deterministic ordering so that every validator that holds the same transaction set produces identical ledger results when applying those transactions. The class lives in `include/xrpl/ledger/CanonicalTXSet.h`, with its implementation in `src/libxrpl/ledger/CanonicalTXSet.cpp`. + +The name "canonical" refers specifically to the ordering guarantee: given the same inputs, every node applies transactions in the same sequence. This is a hard requirement for a Byzantine fault-tolerant ledger — divergent apply orders would cause validators to diverge on ledger hashes even when they agree on the transaction set. + +## The `Key` Type and its Sorting Logic + +The private `Key` struct is the architectural core. Each key holds three fields: a salted account identifier (`account_`), a `SeqProxy` (sequence or ticket), and the transaction hash (`txId_`). The `operator<` implementation in the `.cpp` file reveals the three-level sort: + +1. First by salted account ID — grouping all transactions from the same account together. +2. Then by `SeqProxy` within an account — applying sequence-ordered transactions before ticket transactions (a property enforced by `SeqProxy`'s own comparator, which sorts all `seq`-type values before any `ticket`-type values). +3. Finally by transaction ID as a tiebreaker. + +The equality operator (`operator==`) behaves differently from the less-than operator: it tests only `txId_`. This asymmetry is intentional. The map key space orders by account-then-sequence for retrieval logic, but identity is solely the transaction hash. This makes it safe to use iterator-based removal (`erase`) without accidentally conflating two different transactions that happen to share account/sequence context. + +## Salt: Preventing Position Mining + +A subtle but important security measure is account-key salting. The `accountKey()` method doesn't use the raw `AccountID` as the sort key. Instead it XORs the account bytes with a `LedgerHash` salt: + +```cpp +ret ^= salt_; // salt is a LedgerHash set at construction +``` + +Without salting, an attacker could craft account addresses with intentionally low byte values to ensure their transactions sort early in every ledger's apply order — gaining a persistent ordering advantage. By XORing with the current ledger's hash (which changes every ledger), the effective sort position of any given account is randomized per ledger. The salt is passed at construction time and can be refreshed via `reset()`. + +## `SeqProxy` Integration + +`SeqProxy` is a tagged union of a 32-bit sequence number and a 32-bit ticket number. Its comparator ensures sequence-based transactions always sort before ticket-based ones, regardless of numeric value. This ordering guarantee flows directly into `CanonicalTXSet`: for any given account, all regular-sequence transactions are attempted before any ticket-based transactions. This matters because ticket-creating transactions (which use a sequence) must be applied before ticket-consuming ones — the sort order enforces that dependency automatically. + +## `popAcctTransaction()`: Chaining Transactions from the Same Account + +After a transaction from a given account is successfully applied to the open ledger, the caller invokes `popAcctTransaction()` to retrieve and remove the next eligible transaction from that same account. The method uses `lower_bound` with a key whose `txId_` is `beast::zero` (sorting before any real transaction ID) to locate the first key greater than the just-applied transaction's position. It then returns that next transaction only if it belongs to the same account *and* satisfies one of: + +- It uses a ticket (tickets can be applied regardless of sequence gaps). +- Its sequence value is exactly one more than the current transaction's sequence. + +This prevents accidentally applying a transaction whose predecessor hasn't yet been processed, which would violate the XRPL account sequence invariant. When there is no suitable successor, `popAcctTransaction()` returns `nullptr`. + +## Usage in `applyTransactions()` + +In `BuildLedger.cpp`, `applyTransactions()` iterates the `CanonicalTXSet` in sorted order across multiple passes. Successful and definitively-failed transactions are erased; retryable ones remain for subsequent passes. Because the map's iteration order is the canonical order, every validator performs the same iteration. The `key()` accessor exposes the salt hash for logging purposes — the build log records the transaction set identity alongside the close time. + +## Lifecycle and Memory Accounting + +The class inherits from `CountedObject`, which increments a global atomic counter on construction and decrements it on destruction. This is a diagnostic utility: the counts are queryable via `CountedObjects::getInstance().getCounts()` for memory-pressure reporting. It has no impact on functional behavior. + +`reset()` allows the same `CanonicalTXSet` instance to be reused across ledger rounds by installing a new salt and clearing the map — avoiding repeated heap allocations for the container itself. \ No newline at end of file diff --git a/include/xrpl/ledger/Dir.h.ai.json b/include/xrpl/ledger/Dir.h.ai.json new file mode 100644 index 0000000000..8561ce1799 --- /dev/null +++ b/include/xrpl/ledger/Dir.h.ai.json @@ -0,0 +1,121 @@ +{ + "args": [ + { + "lineno": 80, + "name": "view" + }, + { + "lineno": 80, + "name": "root" + }, + { + "lineno": 80, + "name": "page" + }, + { + "lineno": 41, + "name": "other" + } + ], + "classes": [ + { + "args": [ + "ReadView const&, Keylet const&" + ], + "lineno": 15, + "name": "Dir" + }, + { + "args": [ + "ReadView const& view, Keylet const& root, Keylet const& page" + ], + "lineno": 36, + "name": "Dir::const_iterator" + } + ], + "description": "Provides the Dir class for iterating over ledger directory pages in the XRPL, with a forward iterator and accelerated page stepping, mainly used for NFTokenOffer directories and unit tests.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/Dir.h", + "functions": [ + { + "args": [ + "ReadView const&", + "Keylet const&" + ], + "lineno": 27, + "name": "Dir" + }, + { + "args": [], + "lineno": 29, + "name": "begin" + }, + { + "args": [], + "lineno": 32, + "name": "end" + }, + { + "args": [ + "const_iterator const& other" + ], + "lineno": 41, + "name": "operator==" + }, + { + "args": [ + "const_iterator const& other" + ], + "lineno": 44, + "name": "operator!=" + }, + { + "args": [], + "lineno": 48, + "name": "operator*" + }, + { + "args": [], + "lineno": 51, + "name": "operator->" + }, + { + "args": [], + "lineno": 56, + "name": "operator++" + }, + { + "args": [ + "int" + ], + "lineno": 59, + "name": "operator++" + }, + { + "args": [], + "lineno": 62, + "name": "next_page" + }, + { + "args": [], + "lineno": 65, + "name": "page_size" + }, + { + "args": [], + "lineno": 68, + "name": "page" + }, + { + "args": [], + "lineno": 73, + "name": "index" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/Dir.h.ai.md b/include/xrpl/ledger/Dir.h.ai.md new file mode 100644 index 0000000000..49a5e2449c --- /dev/null +++ b/include/xrpl/ledger/Dir.h.ai.md @@ -0,0 +1,51 @@ +# `include/xrpl/ledger/Dir.h` — Ledger Directory Iterator + +## Role in the System + +Ledger directories in XRPL are linked lists of `DirectoryNode` SLEs (`ltDIR_NODE`), each storing a `STVector256` (`sfIndexes`) — a page of 256-bit keys pointing to other ledger objects. These paged structures are used to associate collections of objects with a root key, such as all NFT buy or sell offers for a given token. `Dir` wraps that storage model in a clean C++ forward-iterable range, hiding the page-chasing and SLE loading behind a familiar `begin()`/`end()` interface. + +As of mid-2024 the class is used in two production contexts: `keylet::nft_buys()` and `keylet::nft_sells()` directories in `NFTokenHelpers.cpp`, and `keylet::ownerDir()` in several unit test files (Escrow, PayChan). The header comment notes this explicitly — the class was designed with generality in mind but its actual deployment is deliberately narrow. + +## Data Model + +A directory's root `Keylet` identifies the first `DirectoryNode` page. Each page SLE carries: + +- `sfIndexes` — a `STVector256` of object keys stored on this page. +- `sfIndexNext` — a `uint64` giving the page number of the next page (`0` = last page). + +Subsequent pages are fetched via `keylet::page(root, sfIndexNext)`. Dereferencing any entry calls `view_->read(keylet::child(index_))`, which produces the `SLE` the directory entry points to. + +## `Dir` Class + +`Dir` itself is a thin range adaptor. Its constructor takes a `ReadView const&` and a root `Keylet`, immediately reads the root SLE, and caches `sfIndexes`. Construction is cheap — no per-entry loading happens yet. + +`begin()` initialises a `const_iterator` that points at the first entry of the root page: it copies the cached root SLE and sets `it_` to `std::begin(*indexes_)`. If the root page is missing or empty the iterator is left in the same state as `end()`. `end()` returns an iterator with `page_.key == root_.key` and `index_ == beast::zero` (a zero-valued `uint256`), serving as a sentinel. + +## `const_iterator` Design + +The iterator carries two levels of state simultaneously: + +- An inner `std::vector::const_iterator it_` walking through the current page's `sfIndexes`. +- A `Keylet page_` identifying which `DirectoryNode` SLE is currently loaded. + +`operator++()` simply advances `it_`. When it reaches `std::end(*indexes_)`, it delegates to `next_page()`, which reads `sfIndexNext` from the current SLE. A value of zero means the directory is exhausted: `page_` is reset to `root_` and `index_` to `beast::zero`, converging the iterator to the end-sentinel state. A non-zero value constructs a new `keylet::page(root_, next)`, reads the corresponding SLE, and sets `it_` to the beginning of its `sfIndexes`. + +`operator*()` lazily loads the referenced object on first dereference using `view_->read(keylet::child(index_))`, storing the result in `mutable cache_`. The cache is cleared to `std::nullopt` on every advance (including page transitions), keeping invalidation tight without any more-complex bookkeeping. + +`operator==()` compares `page_.key` and `index_`. An iterator is equal to `end()` when both conditions match: the page key has been reset to root and `index_` has been zeroed. Note that `operator==()` returns `false` if either view pointer is null, and an `XRPL_ASSERT` confirms that non-null comparisons only occur between iterators from the same view and root. + +## `next_page()` as a Performance Accelerator + +`next_page()` is intentionally exposed as a public member rather than being internal to `operator++()`. This allows callers to skip an entire page's individual entries and jump directly to the next `DirectoryNode`. The canonical use is in `nft::notTooManyOffers()`: + +```cpp +Dir const buys(view, keylet::nft_buys(nftokenID)); +for (auto iter = buys.begin(); iter != buys.end(); iter.next_page()) + totalOffers += iter.page_size(); +``` + +Here, `page_size()` returns `indexes_->size()` — the count of entries on the current page — without loading any of the entries themselves. Calling `next_page()` as the loop increment skips the entire page instead of stepping through it entry by entry. This turns an O(n) traversal with n `ReadView::read()` calls into an O(p) traversal with only p page reads, where p is the number of pages. For the NFToken offer-count check this matters: burning an NFToken requires confirming the offer count is small enough to delete in a single transaction, a check that must be fast. + +## Const-Safety and Ownership + +Both `Dir` and `const_iterator` hold `ReadView const*`, ensuring directory traversal is strictly read-only. Write operations such as `dirInsert` and `dirRemove` are performed through `ApplyView` and are entirely separate concerns. `value_type` is `std::shared_ptr`, matching `ReadView::read()`'s return type and giving callers shared ownership of each SLE beyond the iterator's lifetime. \ No newline at end of file diff --git a/include/xrpl/ledger/Ledger.h.ai.json b/include/xrpl/ledger/Ledger.h.ai.json new file mode 100644 index 0000000000..7b9225b9cf --- /dev/null +++ b/include/xrpl/ledger/Ledger.h.ai.json @@ -0,0 +1,418 @@ +{ + "args": [ + { + "lineno": 44, + "name": "rules" + }, + { + "lineno": 45, + "name": "fees" + }, + { + "lineno": 46, + "name": "amendments" + }, + { + "lineno": 47, + "name": "family" + }, + { + "lineno": 49, + "name": "info" + }, + { + "lineno": 50, + "name": "loaded" + }, + { + "lineno": 51, + "name": "acquire" + }, + { + "lineno": 55, + "name": "j" + }, + { + "lineno": 60, + "name": "previous" + }, + { + "lineno": 61, + "name": "closeTime" + }, + { + "lineno": 65, + "name": "ledgerSeq" + }, + { + "lineno": 170, + "name": "closeResolution" + }, + { + "lineno": 171, + "name": "correctCloseTime" + }, + { + "lineno": 172, + "name": "rehash" + }, + { + "lineno": 192, + "name": "totDrops" + }, + { + "lineno": 212, + "name": "sle" + }, + { + "lineno": 219, + "name": "parallel" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "create_genesis_t" + }, + { + "args": [ + "Ledger const&", + "create_genesis_t, Rules const&, Fees const&, std::vector const&, Family&", + "LedgerHeader const&, Rules const&, Family&", + "LedgerHeader const&, bool&, bool, Rules const&, Fees const&, Family&, beast::Journal", + "Ledger const&, NetClock::time_point", + "std::uint32_t, NetClock::time_point, Rules const&, Fees const&, Family&" + ], + "lineno": 27, + "name": "Ledger" + }, + { + "args": [], + "lineno": 265, + "name": "sles_iter_impl" + }, + { + "args": [], + "lineno": 266, + "name": "txs_iter_impl" + } + ], + "description": "Defines the Ledger class, which represents an XRP Ledger instance, managing state and transaction SHAMaps, ledger metadata, and providing interfaces for reading, writing, and manipulating ledger data. Also defines related types and utility functions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/Ledger.h", + "functions": [ + { + "args": [], + "lineno": 74, + "name": "open" + }, + { + "args": [], + "lineno": 78, + "name": "header" + }, + { + "args": [ + "LedgerHeader const& info" + ], + "lineno": 82, + "name": "setLedgerInfo" + }, + { + "args": [], + "lineno": 86, + "name": "fees" + }, + { + "args": [], + "lineno": 90, + "name": "rules" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 94, + "name": "exists" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 96, + "name": "exists" + }, + { + "args": [ + "uint256 const& key", + "std::optional const& last" + ], + "lineno": 98, + "name": "succ" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 101, + "name": "read" + }, + { + "args": [], + "lineno": 103, + "name": "slesBegin" + }, + { + "args": [], + "lineno": 106, + "name": "slesEnd" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 109, + "name": "slesUpperBound" + }, + { + "args": [], + "lineno": 112, + "name": "txsBegin" + }, + { + "args": [], + "lineno": 115, + "name": "txsEnd" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 118, + "name": "txExists" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 120, + "name": "txRead" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 125, + "name": "digest" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 130, + "name": "rawErase" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 133, + "name": "rawInsert" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 135, + "name": "rawErase" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 137, + "name": "rawReplace" + }, + { + "args": [ + "XRPAmount const& fee" + ], + "lineno": 139, + "name": "rawDestroyXRP" + }, + { + "args": [ + "uint256 const& key", + "std::shared_ptr const& txn", + "std::shared_ptr const& metaData" + ], + "lineno": 146, + "name": "rawTxInsert" + }, + { + "args": [ + "uint256 const& key", + "std::shared_ptr const& txn", + "std::shared_ptr const& metaData" + ], + "lineno": 153, + "name": "rawTxInsertWithHash" + }, + { + "args": [], + "lineno": 163, + "name": "setValidated" + }, + { + "args": [ + "NetClock::time_point closeTime", + "NetClock::duration closeResolution", + "bool correctCloseTime" + ], + "lineno": 168, + "name": "setAccepted" + }, + { + "args": [ + "bool rehash" + ], + "lineno": 172, + "name": "setImmutable" + }, + { + "args": [], + "lineno": 175, + "name": "isImmutable" + }, + { + "args": [], + "lineno": 182, + "name": "setFull" + }, + { + "args": [ + "std::uint64_t totDrops" + ], + "lineno": 192, + "name": "setTotalDrops" + }, + { + "args": [], + "lineno": 196, + "name": "stateMap" + }, + { + "args": [], + "lineno": 200, + "name": "stateMap" + }, + { + "args": [], + "lineno": 204, + "name": "txMap" + }, + { + "args": [], + "lineno": 208, + "name": "txMap" + }, + { + "args": [ + "SLE const& sle" + ], + "lineno": 212, + "name": "addSLE" + }, + { + "args": [], + "lineno": 217, + "name": "updateSkipList" + }, + { + "args": [ + "beast::Journal j", + "bool parallel" + ], + "lineno": 219, + "name": "walkLedger" + }, + { + "args": [], + "lineno": 221, + "name": "isSensible" + }, + { + "args": [], + "lineno": 223, + "name": "invariants" + }, + { + "args": [], + "lineno": 224, + "name": "unshare" + }, + { + "args": [], + "lineno": 230, + "name": "negativeUNL" + }, + { + "args": [], + "lineno": 237, + "name": "validatorToDisable" + }, + { + "args": [], + "lineno": 244, + "name": "validatorToReEnable" + }, + { + "args": [], + "lineno": 251, + "name": "updateNegativeUNL" + }, + { + "args": [], + "lineno": 256, + "name": "isFlagLedger" + }, + { + "args": [], + "lineno": 259, + "name": "isVotingLedger" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 262, + "name": "peek" + }, + { + "args": [], + "lineno": 268, + "name": "setup" + }, + { + "args": [ + "SHAMapItem const& item" + ], + "lineno": 272, + "name": "deserializeTx" + }, + { + "args": [ + "SHAMapItem const& item" + ], + "lineno": 279, + "name": "deserializeTxPlusMeta" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/Ledger.h.ai.md b/include/xrpl/ledger/Ledger.h.ai.md new file mode 100644 index 0000000000..def9115eb2 --- /dev/null +++ b/include/xrpl/ledger/Ledger.h.ai.md @@ -0,0 +1,73 @@ +# `include/xrpl/ledger/Ledger.h` + +## Role in the System + +`Ledger.h` declares the `Ledger` class — the central data structure of the entire rippled server. Every validated or in-progress ledger is an instance of this class. It owns the full account state of the XRP Ledger at a given sequence number, plus the complete set of transactions that produced that state, and exposes both through a layered view hierarchy that the rest of the codebase depends on for transaction processing, consensus, and data access. + +## Structural Composition + +A `Ledger` holds exactly two `SHAMap` members: `stateMap_` and `txMap_`. Both are Merkle–radix trees with a fan-out of 16. `stateMap_` contains all serialized ledger state entries (SLEs) — account roots, trust lines, order book pages, escrows, amendments, fee settings, and more. `txMap_` contains serialized transaction blobs paired with their execution metadata, keyed by transaction ID. Every query about account balances, object existence, or transaction results ultimately reaches one of these two maps. + +The maps are declared `mutable` despite conceptual immutability because `setFull()` and the iterator implementations need to access them in `const` contexts. This is an acknowledged pattern in the code: the `SHAMap` state can evolve (e.g., fetching remote nodes during sync) without changing the logical ledger data. + +## Inheritance Design + +`Ledger` inherits from three base classes: + +- `DigestAwareReadView` (extends `ReadView`): provides the standard read interface for ledger state and transactions, plus a `digest()` method that returns the Merkle hash of an individual state item. This extends the basic read contract and is needed by `CachedView` to detect stale cache entries. +- `TxsRawView` (extends `RawView`): exposes raw mutation operations — `rawInsert`, `rawErase`, `rawReplace`, and `rawDestroyXRP` on the state map, plus `rawTxInsert` on the transaction map. This interface is used only while the ledger is mutable, during transaction application. +- `CountedObject`: intrusive reference counting for diagnostics and resource leak detection. + +The class is marked `final` because its constructors call virtual functions (specifically `setup()` through the virtual dispatch chain), making subclassing unsafe. + +## Mutable/Immutable Lifecycle + +The documentation block in the header captures the essential concurrency contract: a mutable `Ledger` must not be shared, while an immutable one can be shared freely without locks. This eliminates locking overhead for the overwhelmingly common read path. + +The transition happens in `setImmutable(bool rehash)`. When called (with `rehash = true`, the default), it computes the SHAMap hash of both maps, assembles those hashes into the `LedgerHeader`, calls `calculateLedgerHash()` to produce the canonical ledger hash, then locks both SHAMaps into immutable state and calls `setup()` to populate the `fees_` and `rules_` members from the state entries in `stateMap_`. After this point, no mutations are permitted — any attempt to call `rawInsert` et al. on an immutable SHAMap will assert. + +`setAccepted()` is a coordinated sequence that sets timing fields and then delegates to `setImmutable()`. It carries an assertion (`!open()`) because only closed ledgers can be accepted. + +## Constructor Paths + +Five distinct constructors serve different creation scenarios: + +**Genesis (`create_genesis_t`)**: Constructs ledger sequence 1 from scratch. Seeds the master account by deterministically deriving its ID from the string `"masterpassphrase"`, credits it with `INITIAL_XRP`, inserts the `sfAmendments` object for any pre-enabled amendments, and inserts the fee schedule SLE (using either drops-native or legacy fee field format depending on whether `featureXRPFees` is among the amendments). Ends with `setImmutable()`. + +**JSON-loaded (`LedgerHeader + bool& loaded + acquire`)**: Restores a ledger from its header. Constructs both SHAMaps with known root hashes from the header, then calls `fetchRoot()` on each. If roots are missing from the node store, the `loaded` out-parameter is set to `false` and, when `acquire = true`, triggers async acquisition via `family.missingNodeAcquireByHash()`. This path creates an already-immutable ledger. + +**Successor (`Ledger const& previous + closeTime`)**: Creates the next ledger in the chain. The new `txMap_` starts empty (a fresh SHAMap for the new ledger's transactions), while `stateMap_` is constructed by deep-copying the previous ledger's state map — `SHAMap(prevLedger.stateMap_, true)` where the `true` flag requests a copy-on-write snapshot. The header is populated with incremented sequence, updated parent hash, parent close time, and recalculated close time resolution. + +**Header-only (`LedgerHeader + Rules + Family`)**: Creates an immutable placeholder holding only the header, used for partial/skeleton ledgers from the database. The hash is computed immediately from the header fields. + +**Database placeholder (`ledgerSeq + closeTime + Rules + Fees + Family`)**: Creates a mutable empty ledger for database reconstruction scenarios; calls `setup()` to initialize fee/rules state from whatever state entries may already exist. + +## Key Mutation Methods + +`rawDestroyXRP(XRPAmount fee)` is defined inline as `header_.drops -= fee`. This implements the XRPL's deflationary model: transaction fees are burned rather than redistributed, permanently reducing `drops` in the header. There is no escrow account or validator payment. + +`rawTxInsertWithHash()` extends the `TxsRawView` interface by returning the hash of the SHAMap leaf node that stores the transaction. This enables a direct-lookup optimization: callers can bypass SHAMap tree traversal entirely when fetching a transaction they just inserted, using the leaf hash as a direct node store key. + +`addSLE()` is a convenience wrapper (returning bool to signal failure) used during ledger construction from external data sources. + +## Negative UNL + +Three methods — `negativeUNL()`, `validatorToDisable()`, and `validatorToReEnable()` — read the Negative UNL state from the ledger's state map. The Negative UNL is a consensus-level mechanism for temporarily removing chronically offline validators without breaking liveness. `updateNegativeUNL()` must be called exactly at flag ledgers (sequence divisible by 256) and before any `UNLModify` transaction is applied; the comments enforce this timing contract explicitly. + +`isFlagLedger()` and `isVotingLedger()` identify the two special positions in the 256-ledger cycle: flag ledgers carry out amendment votes, fee votes, and NegUNL updates; voting ledgers (flag − 1) are where validators cast their preferences. + +## `setFull()` and Node Store Semantics + +`setFull()` is declared `const` because fullness is not consensus data — it is a local node's storage policy, telling the node store that all SHAMap nodes for this ledger should be retained in durable storage. Marking it `const` acknowledges that this property can vary across nodes holding the identical ledger. + +## Iterator Implementation + +`sles_iter_impl` and `txs_iter_impl` are private nested classes (defined in the `.cpp` file) that bridge `SHAMap::const_iterator` to the abstract `iter_base` interfaces declared in `ReadView`. They deserialize `SHAMapItem` blobs on demand via `deserializeTx` and `deserializeTxPlusMeta`. The static deserializers are split into single-object and pair forms because open ledgers store transactions without metadata, while closed ledgers always store both. + +## `CachedLedger` + +The alias `using CachedLedger = CachedView` creates the standard shareable ledger type used at rest in most of the server. `CachedView` layers an `unordered_map` keyed on SLE key in front of the `Ledger`, avoiding repeated deserialization of frequently accessed state entries. This is the type that callers like the transaction engine and RPC handlers typically hold, not a raw `Ledger`. + +## Concurrency and Resource Ownership + +The deleted copy and move constructors enforce that `Ledger` objects are always owned through `std::shared_ptr`, consistent with `enable_shared_from_this`. The single `mutex_` member protects fee-variable access in a narrow window before immutability is fully established. Once `setImmutable()` completes, all shared readers proceed without locking. The asymmetry — mutable ledgers are exclusively owned, immutable ones are freely shared — eliminates contention from the hot read path entirely. \ No newline at end of file diff --git a/include/xrpl/ledger/LedgerTiming.h.ai.json b/include/xrpl/ledger/LedgerTiming.h.ai.json new file mode 100644 index 0000000000..3be39e1288 --- /dev/null +++ b/include/xrpl/ledger/LedgerTiming.h.ai.json @@ -0,0 +1,41 @@ +{ + "args": [], + "classes": [], + "description": "Provides utilities for managing and calculating ledger close time resolutions and effective close times in the XRPL protocol, including dynamic adjustment of time binning for consensus.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/LedgerTiming.h", + "functions": [ + { + "args": [ + "previousResolution", + "previousAgree", + "ledgerSeq" + ], + "lineno": 32, + "name": "getNextLedgerTimeResolution" + }, + { + "args": [ + "closeTime", + "closeResolution" + ], + "lineno": 81, + "name": "roundCloseTime" + }, + { + "args": [ + "closeTime", + "resolution", + "priorCloseTime" + ], + "lineno": 102, + "name": "effCloseTime" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/LedgerTiming.h.ai.md b/include/xrpl/ledger/LedgerTiming.h.ai.md new file mode 100644 index 0000000000..a3e6faac99 --- /dev/null +++ b/include/xrpl/ledger/LedgerTiming.h.ai.md @@ -0,0 +1,57 @@ +# `LedgerTiming.h` — Ledger Close Time Resolution and Binning + +This header provides the three free functions and set of compile-time constants that govern how XRPL records and agrees upon ledger close times. It sits at the intersection of consensus and ledger construction: every time a new ledger is accepted, these utilities translate a raw wall-clock observation into a canonical, network-agreed timestamp that is written into the immutable ledger record. + +## The Problem: Agreeing on Time Without Synchronized Clocks + +XRPL validators run on independent machines with imperfectly synchronized clocks. If each validator stamped a ledger with its own wall-clock reading, the resulting close times would differ slightly across the network, making it impossible to form consensus on a single ledger hash. The solution — also used in several other distributed systems — is **time binning**: rather than recording the exact close time, each validator rounds its observation to the nearest multiple of a fixed resolution, then votes on that rounded value. When the resolution is large enough, minor clock skew disappears and all validators naturally agree. + +The challenge becomes choosing the right bin size. Too coarse and ledger close times carry less useful information; too fine and even small clock differences cause disagreements. `LedgerTiming.h` implements an adaptive mechanism that adjusts the bin size dynamically based on whether the network agreed on close time in the previous round. + +## Constants and Their Relationships + +`ledgerPossibleTimeResolutions` is a constexpr array of six candidate resolutions: 10, 20, 30, 60, 90, and 120 seconds. They form a strictly increasing sequence that `getNextLedgerTimeResolution` traverses. The default resolution for ordinary ledgers is 30 seconds (index 2), while the genesis ledger starts at 10 seconds (index 0). Storing these as a flat array rather than a set or map is deliberate: membership checks and boundary navigation are O(n) over only six elements, and the array order directly encodes the "coarser/finer" direction needed by the adjustment logic. + +Two additional constants control the pace of change: `decreaseLedgerTimeResolutionEvery = 1` (react on every ledger) and `increaseLedgerTimeResolutionEvery = 8` (only increase resolution every eighth ledger). This asymmetry is intentional and conservative — when the network is disagreeing it needs to back off to a coarser bin quickly, but it should be cautious about tightening the resolution, since a premature increase could immediately cause fresh disagreements. + +## `getNextLedgerTimeResolution()` — Adaptive Resolution Selection + +This function takes the previous ledger's resolution, whether consensus agreed on that ledger's close time, and the new ledger's sequence number. It applies the two competing adjustment rules: + +- If the prior ledger saw **disagreement** and `ledgerSeq % decreaseLedgerTimeResolutionEvery == 0`, the resolution increases (moves toward coarser bins, i.e., toward the end of `ledgerPossibleTimeResolutions`). +- If the prior ledger saw **agreement** and `ledgerSeq % increaseLedgerTimeResolutionEvery == 0`, the resolution decreases (moves toward finer bins, i.e., toward the beginning). + +Crucially, neither rule fires if the iterator is already at the boundary, so the resolution saturates at the min (10 s) or max (120 s) rather than wrapping or asserting. The sequence-number modulo check ensures both conditions cannot simultaneously trigger on the same ledger even if `ledgerSeq` happens to be divisible by both constants (which can't happen given the values are 1 and 8, but the guard is still logically sound). The function is a header-only template parameterized on both the `std::chrono::duration` type and the ledger sequence type, allowing it to work with both built-in integers and XRPL's `tagged_integer` wrappers without casts. + +The `Consensus.h` engine calls this function at the start of every consensus round to compute `closeResolution_`, storing the result for the rest of that round's voting and for embedding in the accepted ledger: + +```cpp +closeResolution_ = getNextLedgerTimeResolution( + previousLedger_.closeTimeResolution(), + previousLedger_.closeAgree(), + previousLedger_.seq() + typename Ledger_t::Seq{1}); +``` + +## `roundCloseTime()` — Epoch-Anchored Binning + +This function rounds an arbitrary `time_point` to the nearest multiple of `closeResolution`. The rounding arithmetic is `(closeTime + resolution/2) - ((closeTime + resolution/2).time_since_epoch() % resolution)` — effectively a floor-after-offset operation. Two properties deserve attention: + +**Epoch anchoring.** The modulo is applied to `time_since_epoch()`, not to a relative offset from some local reference. This means bins are aligned to absolute epoch-relative boundaries (multiples of 30 s from the XRPL epoch), not to whenever this particular ledger happened to open. Any two validators computing this on the same raw time will produce the same bin, regardless of when they run the calculation — a correctness prerequisite. + +**Tie-breaking upward.** Adding half the resolution before truncating means a time exactly at the midpoint rounds up to the next bin, matching the standard "round half up" convention. The unit tests in `LedgerTiming_test.cpp` confirm this: `roundCloseTime(tp{30s}, 60s)` returns `tp{60s}`, not `tp{0s}`. + +**Zero sentinel.** A `time_point{}` (the epoch itself) is returned unchanged. The zero value is a protocol-level sentinel meaning the ledger has no agreed close time — used when consensus failed to agree — and binning must not accidentally produce a plausible timestamp from it. + +`roundCloseTime` is called internally by `effCloseTime` and also exposed so the consensus engine can canonicalize individual peer proposals via the `asCloseTime()` helper in `Consensus.h`. + +## `effCloseTime()` — Monotonicity Enforcement + +After rounding, a subtle edge case remains: if a ledger closes very quickly after its predecessor, `roundCloseTime` might produce a result equal to or earlier than the prior ledger's close time. This would violate the invariant that close times increase monotonically along the ledger chain, which downstream consumers (auditing, ordering, client-visible timestamps) rely on. + +`effCloseTime` resolves this with a single `std::max`: `max(roundCloseTime(closeTime, resolution), priorCloseTime + 1s)`. The `+ 1s` ensures strict ordering rather than merely ≥. When the rounded time is later than the prior close time the rounding result passes through unchanged; when it would tie or go backward, the function returns `priorCloseTime + 1s` instead. The consensus simulation framework (`src/test/csf/impl/ledgers.cpp`) and the real consensus adapter (`RCLConsensus.cpp`) both call `effCloseTime` at ledger acceptance time to compute the final value written into the ledger. + +The test for this function (`testEffCloseTime`) exercises the intersection point: `effCloseTime(tp{10s}, 30s, tp{0s})` returns `tp{1s}` because rounding 10 s to 30 s bins gives `tp{0s}`, which is not greater than `priorCloseTime + 1s = tp{1s}`, so the minimum applies. Meanwhile `effCloseTime(tp{16s}, 30s, tp{0s})` gives `tp{30s}` because the rounded value wins. + +## Design Summary + +The file is deliberately narrow: it encapsulates precisely the time-agreement logic that must be identical on every validator. Being a header-only template library means the logic is linked into any translation unit that needs it without a separate compilation dependency, and the template parameters allow the functions to operate on XRPL's network clock type (`NetClock`) without hardcoding it. The three constants controlling the adjustment rate (`decreaseLedgerTimeResolutionEvery`, `increaseLedgerTimeResolutionEvery`, and the resolution ladder itself) are the primary knobs for tuning network time-agreement behavior, and their values reflect a deliberate bias toward stability over precision. \ No newline at end of file diff --git a/include/xrpl/ledger/OpenView.h.ai.json b/include/xrpl/ledger/OpenView.h.ai.json new file mode 100644 index 0000000000..c1393c215c --- /dev/null +++ b/include/xrpl/ledger/OpenView.h.ai.json @@ -0,0 +1,162 @@ +{ + "args": [ + { + "lineno": 17, + "name": "open_ledger_t" + }, + { + "lineno": 27, + "name": "batch_view_t" + } + ], + "classes": [ + { + "args": [], + "lineno": 38, + "name": "OpenView" + } + ], + "description": "Defines the OpenView class, a writable ledger view in the XRPL codebase that accumulates state and transaction changes, presenting as a ReadView to clients. It supports open ledger and batch view construction, transaction insertion, and state modifications.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/OpenView.h", + "functions": [ + { + "args": [], + "lineno": 109, + "name": "open" + }, + { + "args": [], + "lineno": 117, + "name": "txCount" + }, + { + "args": [ + "TxsRawView& to" + ], + "lineno": 122, + "name": "apply" + }, + { + "args": [], + "lineno": 127, + "name": "header" + }, + { + "args": [], + "lineno": 130, + "name": "fees" + }, + { + "args": [], + "lineno": 133, + "name": "rules" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 136, + "name": "exists" + }, + { + "args": [ + "key_type const& key", + "std::optional const& last" + ], + "lineno": 139, + "name": "succ" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 142, + "name": "read" + }, + { + "args": [], + "lineno": 145, + "name": "slesBegin" + }, + { + "args": [], + "lineno": 148, + "name": "slesEnd" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 151, + "name": "slesUpperBound" + }, + { + "args": [], + "lineno": 154, + "name": "txsBegin" + }, + { + "args": [], + "lineno": 157, + "name": "txsEnd" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 160, + "name": "txExists" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 163, + "name": "txRead" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 167, + "name": "rawErase" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 170, + "name": "rawInsert" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 173, + "name": "rawReplace" + }, + { + "args": [ + "XRPAmount const& fee" + ], + "lineno": 176, + "name": "rawDestroyXRP" + }, + { + "args": [ + "key_type const& key", + "std::shared_ptr const& txn", + "std::shared_ptr const& metaData" + ], + "lineno": 180, + "name": "rawTxInsert" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/OpenView.h.ai.md b/include/xrpl/ledger/OpenView.h.ai.md new file mode 100644 index 0000000000..ac8d60de9f --- /dev/null +++ b/include/xrpl/ledger/OpenView.h.ai.md @@ -0,0 +1,52 @@ +# `include/xrpl/ledger/OpenView.h` + +## Role in the System + +`OpenView` is the primary mutable ledger surface used during transaction processing in the XRP Ledger. It models a ledger that has not yet been closed and validated — transactions are being applied to it, state objects are being modified, and the result is still in flux. Once processing completes, the accumulated changes are flushed to a target view via `apply()`. + +The design is a delta-accumulation pattern: `OpenView` holds a reference to an immutable base `ReadView` (typically the most recent closed ledger) and records all modifications as a pending diff on top of it. Nothing is written through to the base until `apply()` is called. This makes it safe to discard changes on failure, or to evaluate what changes a transaction would cause before committing. + +## Inheritance and Interface Exposure + +`OpenView` inherits from both `ReadView` and `TxsRawView`. The read interface lets transaction logic and validation code query the current apparent state (base + pending changes) without needing to know whether the ledger is settled. The `TxsRawView` write interface lets transaction processors insert SLE mutations (`rawErase`, `rawInsert`, `rawReplace`) and register applied transactions (`rawTxInsert`). + +Callers holding a `ReadView const*` see a coherent read-only snapshot that transparently merges base state and pending modifications. Callers with a `TxsRawView*` or `OpenView*` can write into it. This asymmetry is deliberate: much of the ledger traversal and validation code is written against the read-only interface, and `OpenView` plugs directly into that without modification. + +## Construction Modes + +Three construction paths reflect distinct lifecycle stages: + +**`open_ledger_t` tag**: Builds a fresh open ledger on top of a base. The header sequence is bumped by one, `parentCloseTime` and `parentHash` are set from the base, and `validated`/`accepted` flags are cleared. The `Rules` object is supplied explicitly because open ledger rules may differ from what the base recorded. An optional `hold` shared pointer keeps the underlying base object alive for the view's lifetime. + +**`ReadView const*` without tag**: Used to construct a last-closed-ledger view. It copies the base header and rules directly, and inherits the `open_` flag — so if the base was a closed ledger, this view will also report itself as closed. + +**`batch_view_t` tag**: Used during batch transaction processing. The new view wraps an existing `OpenView` rather than a base `ReadView`, and records the current transaction count of the parent (`baseTxCount_`). This ensures `txCount()`, which drives the apply-ordinal used in metadata, remains correct even as the batch stack grows. + +## State Change Buffering: `RawStateTable` and `txs_map` + +State object changes are buffered in `items_`, a `detail::RawStateTable`. `RawStateTable` maintains a `std::map` from ledger key to a tagged action (`erase`, `insert`, or `replace`) alongside the modified `SLE`. All `ReadView` queries — `exists()`, `read()`, `succ()`, iteration — go through `RawStateTable` passing the base view as a fallback, so the merged view is always consistent with both the base and pending mutations. + +Transaction records are held in `txs_`, a `std::map` where `txData` pairs a serialized transaction (`txn`) with optional serialized metadata (`meta`). Open ledgers omit metadata; closed ledger representations include it. The `open_` flag drives this distinction: `txsBegin()` and `txsEnd()` pass `!open()` to the `txs_iter_impl`, which controls whether `dereference()` deserializes the metadata field. + +The `rawTxInsert()` implementation calls `LogicError` on a duplicate key — duplicate transaction IDs are a hard invariant violation, not a recoverable error. + +## Memory: Monotonic PMR Allocation + +Both `OpenView` and `RawStateTable` allocate their internal maps using `boost::container::pmr::monotonic_buffer_resource` with a 256 KB initial buffer. The pool starts with a pre-allocated block and grows linearly, making allocation O(1) amortized with no per-element heap overhead. This is important because many small SLE-keyed map entries are inserted during block processing, and the arena strategy avoids the fragmentation and lock contention of the default allocator. + +The `monotonic_resource_` member is a `std::unique_ptr` rather than a value, for two reasons: the map's `polymorphic_allocator` holds a raw pointer to the resource and would break if the resource moved, and `unique_ptr` allows `OpenView` to be move-constructed while still maintaining stable resource addressing. The copy constructor allocates a fresh 256 KB arena and copies the map's contents into it using the new allocator. The comment notes this 256 KB size comes from the legacy `qalloc` allocator it replaced. + +## The `apply()` Commit Path + +`void apply(TxsRawView& to)` is the commit operation. It calls `items_.apply(to)` to replay all SLE-level mutations onto the target, 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. The two-phase structure means each transaction can be independently discarded without affecting the accumulator. + +## `txCount()` and Ordinal Tracking + +`txCount()` returns `baseTxCount_ + txs_.size()`. The apply ordinal embedded in transaction metadata must be globally unique and monotonically increasing within a ledger. In batch mode where views are stacked, `baseTxCount_` captures how many transactions had already been applied to the parent before this child view was constructed, preserving correct ordinals even when sub-views are committed incrementally. + +## Key Invariants + +- `rawTxInsert` rejects duplicate transaction IDs with a hard logic error. +- `items_` is always queried with a reference to `*base_`, ensuring that any key absent from the diff falls through to the authoritative base state. +- The `monotonic_resource_` is always constructed before `txs_` and `items_`, and outlives them — enforced by declaration order and the fact that both maps hold a raw pointer to the resource. +- Move assignment and copy assignment are deleted; only move construction and copy construction are available, ensuring the resource/map lifetime coupling cannot be inadvertently broken. \ No newline at end of file diff --git a/include/xrpl/ledger/OrderBookDB.h.ai.json b/include/xrpl/ledger/OrderBookDB.h.ai.json new file mode 100644 index 0000000000..4e310e9f7c --- /dev/null +++ b/include/xrpl/ledger/OrderBookDB.h.ai.json @@ -0,0 +1,107 @@ +{ + "args": [ + { + "lineno": 27, + "name": "ledger" + }, + { + "lineno": 34, + "name": "book" + }, + { + "lineno": 41, + "name": "asset" + }, + { + "lineno": 41, + "name": "domain" + }, + { + "lineno": 70, + "name": "alTx" + }, + { + "lineno": 70, + "name": "jvObj" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "OrderBookDB" + } + ], + "description": "Defines the OrderBookDB interface for tracking and querying order books in the XRPL ledger, including methods for updating, querying, and managing order book subscriptions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/OrderBookDB.h", + "functions": [ + { + "args": [ + "ledger" + ], + "lineno": 27, + "name": "setup" + }, + { + "args": [ + "book" + ], + "lineno": 34, + "name": "addOrderBook" + }, + { + "args": [ + "asset", + "domain" + ], + "lineno": 41, + "name": "getBooksByTakerPays" + }, + { + "args": [ + "asset", + "domain" + ], + "lineno": 52, + "name": "getBookSize" + }, + { + "args": [ + "asset", + "domain" + ], + "lineno": 61, + "name": "isBookToXRP" + }, + { + "args": [ + "ledger", + "alTx", + "jvObj" + ], + "lineno": 70, + "name": "processTxn" + }, + { + "args": [ + "book" + ], + "lineno": 80, + "name": "getBookListeners" + }, + { + "args": [ + "book" + ], + "lineno": 87, + "name": "makeBookListeners" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/OrderBookDB.h.ai.md b/include/xrpl/ledger/OrderBookDB.h.ai.md new file mode 100644 index 0000000000..dd55f86976 --- /dev/null +++ b/include/xrpl/ledger/OrderBookDB.h.ai.md @@ -0,0 +1,37 @@ +# `include/xrpl/ledger/OrderBookDB.h` + +## Purpose and Context + +`OrderBookDB` is a pure abstract interface that defines how the XRPL server tracks and queries all active order books across the ledger. An order book in XRPL terms is a directed trading pair — a set of open `ltOFFER` entries that all share the same "taker pays" and "taker gets" assets. Because pathfinding and client subscriptions both need fast lookups of which markets exist, this index is maintained separately from the ledger state itself. + +The interface lives in the public `include/xrpl/ledger/` layer, keeping it decoupled from the concrete implementation (`OrderBookDBImpl`, in `src/xrpld/app/ledger/`). Callers interact only with `OrderBookDB*`, while the concrete type is instantiated through `make_OrderBookDB(ServiceRegistry&, OrderBookDBConfig const&)` and injected via the service registry. This separation enables testing and keeps the heavy implementation details out of consumer headers. + +## The `Book` and `Asset` Types + +`Book` represents a directed trading pair: `in` (what the taker pays) and `out` (what the taker gets), plus an optional `domain`. The `domain` field is a `uint256` that scopes the book to a permissioned domain — a newer XRPL feature where certain books are only accessible to participants in a specific domain. Global books leave `domain` as `std::nullopt`. + +`Asset` is a `std::variant`, abstracting over the three asset kinds XRPL supports: XRP and IOU (both wrapped in `Issue`) and the newer Multi-Purpose Token standard (`MPTIssue`). The `OrderBookDB` interface uses `Asset` throughout rather than the older `Issue` type, so all query methods work uniformly across traditional and MPT token pairs. + +## Interface Methods + +**`setup()`** is the entry point called on each accepted ledger. Rather than always performing a full ledger scan, the implementation throttles these scans intelligently: it skips if the new ledger is within 25,600 sequences ahead of the last scanned ledger (incremental transactions keep the index current via `processTxn`) or within 16 sequences behind it (a small reorg). Outside these windows a full scan is enqueued. In non-standalone mode this scan runs as a background job on the job queue; in standalone mode it runs synchronously. The scan walks every `ltDIR_NODE` with an `sfExchangeRate` field (which marks order book directory roots) and every `ltAMM` object, rebuilding the entire in-memory book maps in local variables before swapping them under a lock, so readers are never blocked for long. + +**`getBooksByTakerPays()`** is the primary pathfinding query. Given an asset and optional domain, it returns every `Book` that has that asset as its "in" side — i.e., every market where you can spend that asset. The pathfinding engine calls this at each hop to enumerate possible next steps toward the destination currency. + +**`getBookSize()`** returns the number of distinct "out" assets available for a given "in" asset. This count is used as a heuristic to limit pathfinding breadth. + +**`isBookToXRP()`** answers a fast yes/no question: does any order book exist where the given asset can be sold for XRP? The implementation keeps a separate `xrpBooks_` set (and `xrpDomainBooks_` for permissioned variants) so this check is O(1) without scanning `allBooks_`. Pathfinding uses this to identify assets that can be liquidated directly to XRP without an intermediate hop. + +**`addOrderBook()`** allows callers to register a single book without triggering a full scan. This handles the case where a new book is discovered incrementally (e.g., from `processTxn`) before the next scheduled full update. + +## Transaction Processing and Subscriptions + +**`processTxn()`** is called for every transaction in a closed ledger. It walks the transaction's metadata nodes looking for `ltOFFER` entries that were created, modified, or deleted, then extracts the `TakerGets` and `TakerPays` fields from the appropriate snapshot (`sfNewFields`, `sfPreviousFields`, or `sfFinalFields`). For each touched offer, it looks up a `BookListeners` object keyed by the reversed book (`TakerGets` → `TakerPays`), and if subscribers exist, calls `publish()`. + +A critical correctness detail: a single transaction may touch multiple offers in the same book, or a client may have subscribed to multiple books that one transaction affects. Without deduplication, the same transaction would be delivered to a subscriber multiple times. `processTxn()` solves this by maintaining a `hash_set havePublished` local to each call, tracking the unique subscriber IDs that have already received the message during this invocation. `BookListeners::publish()` checks and updates this set, so each subscriber receives at most one notification per transaction regardless of how many of its books were touched. + +**`getBookListeners()`** / **`makeBookListeners()`** manage the subscription map. `getBookListeners()` returns `nullptr` if no subscribers exist for a book, while `makeBookListeners()` creates a new `BookListeners` entry on demand. The separation avoids creating empty listener objects for every book that passes through the system. + +## Concurrency Design + +All internal maps in `OrderBookDBImpl` are guarded by a `std::recursive_mutex`. The recursive nature is required because `makeBookListeners()` calls `getBookListeners()` under the same lock. The expensive `update()` scan builds new maps entirely outside the lock and then does a fast `swap()` inside a brief critical section, so reader calls like `getBooksByTakerPays()` and `processTxn()` are only briefly blocked during the final swap rather than for the duration of a full ledger traversal. \ No newline at end of file diff --git a/include/xrpl/ledger/PaymentSandbox.h.ai.json b/include/xrpl/ledger/PaymentSandbox.h.ai.json new file mode 100644 index 0000000000..c46ced295d --- /dev/null +++ b/include/xrpl/ledger/PaymentSandbox.h.ai.json @@ -0,0 +1,304 @@ +{ + "args": [ + { + "lineno": 54, + "name": "main" + }, + { + "lineno": 54, + "name": "other" + }, + { + "lineno": 54, + "name": "currency" + }, + { + "lineno": 57, + "name": "mptID" + }, + { + "lineno": 60, + "name": "sender" + }, + { + "lineno": 60, + "name": "receiver" + }, + { + "lineno": 60, + "name": "amount" + }, + { + "lineno": 60, + "name": "preCreditSenderBalance" + }, + { + "lineno": 65, + "name": "preCreditBalanceHolder" + }, + { + "lineno": 65, + "name": "preCreditBalanceIssuer" + }, + { + "lineno": 70, + "name": "issue" + }, + { + "lineno": 70, + "name": "origBalance" + }, + { + "lineno": 73, + "name": "id" + }, + { + "lineno": 73, + "name": "cur" + }, + { + "lineno": 73, + "name": "next" + }, + { + "lineno": 83, + "name": "to" + }, + { + "lineno": 88, + "name": "a1" + }, + { + "lineno": 88, + "name": "a2" + }, + { + "lineno": 120, + "name": "account" + }, + { + "lineno": 120, + "name": "issuer" + }, + { + "lineno": 132, + "name": "from" + }, + { + "lineno": 132, + "name": "preCreditBalance" + }, + { + "lineno": 148, + "name": "count" + }, + { + "lineno": 162, + "name": "view" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "DeferredCredits" + }, + { + "args": [], + "lineno": 101, + "name": "PaymentSandbox" + } + ], + "description": "Defines the PaymentSandbox class and supporting DeferredCredits logic for managing deferred credit adjustments during payment and pathfinding operations in the XRPL ledger, ensuring credits are not immediately available to balances and supporting complex payment flows.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/PaymentSandbox.h", + "functions": [ + { + "args": [ + "main", + "other", + "currency" + ], + "lineno": 54, + "name": "adjustmentsIOU" + }, + { + "args": [ + "mptID" + ], + "lineno": 57, + "name": "adjustmentsMPT" + }, + { + "args": [ + "sender", + "receiver", + "amount", + "preCreditSenderBalance" + ], + "lineno": 60, + "name": "creditIOU" + }, + { + "args": [ + "sender", + "receiver", + "amount", + "preCreditBalanceHolder", + "preCreditBalanceIssuer" + ], + "lineno": 65, + "name": "creditMPT" + }, + { + "args": [ + "issue", + "amount", + "origBalance" + ], + "lineno": 70, + "name": "issuerSelfDebitMPT" + }, + { + "args": [ + "id", + "cur", + "next" + ], + "lineno": 73, + "name": "ownerCount" + }, + { + "args": [ + "id" + ], + "lineno": 78, + "name": "ownerCount" + }, + { + "args": [ + "to" + ], + "lineno": 83, + "name": "apply" + }, + { + "args": [ + "a1", + "a2", + "currency" + ], + "lineno": 88, + "name": "makeKeyIOU" + }, + { + "args": [ + "account", + "issuer", + "amount" + ], + "lineno": 120, + "name": "balanceHookIOU" + }, + { + "args": [ + "account", + "issue", + "amount" + ], + "lineno": 124, + "name": "balanceHookMPT" + }, + { + "args": [ + "issue", + "amount" + ], + "lineno": 128, + "name": "balanceHookSelfIssueMPT" + }, + { + "args": [ + "from", + "to", + "amount", + "preCreditBalance" + ], + "lineno": 132, + "name": "creditHookIOU" + }, + { + "args": [ + "from", + "to", + "amount", + "preCreditBalanceHolder", + "preCreditBalanceIssuer" + ], + "lineno": 137, + "name": "creditHookMPT" + }, + { + "args": [ + "issue", + "amount", + "origBalance" + ], + "lineno": 142, + "name": "issuerSelfDebitHookMPT" + }, + { + "args": [ + "account", + "cur", + "next" + ], + "lineno": 145, + "name": "adjustOwnerCountHook" + }, + { + "args": [ + "account", + "count" + ], + "lineno": 148, + "name": "ownerCountHook" + }, + { + "args": [ + "to" + ], + "lineno": 154, + "name": "apply" + }, + { + "args": [ + "to" + ], + "lineno": 157, + "name": "apply" + }, + { + "args": [ + "view" + ], + "lineno": 162, + "name": "balanceChanges" + }, + { + "args": [], + "lineno": 165, + "name": "xrpDestroyed" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/PaymentSandbox.h.ai.md b/include/xrpl/ledger/PaymentSandbox.h.ai.md new file mode 100644 index 0000000000..e08961ca87 --- /dev/null +++ b/include/xrpl/ledger/PaymentSandbox.h.ai.md @@ -0,0 +1,41 @@ +# `PaymentSandbox.h` — Liquidity-Isolation Layer for XRPL Payments + +## Role in the System + +The XRPL payment engine processes complex multi-hop paths where value flows through chains of trust lines, order books, and AMM pools. Without a special guard, a credit arriving at an intermediate account mid-path could be immediately visible to later steps in the same path — a form of "double-counting" liquidity that would allow the ledger to produce phantom value. `PaymentSandbox` exists to prevent this: it is a speculative ledger view that tracks every credit made during payment execution and hides those credits from balance queries until the entire transaction commits. + +`PaymentSandbox` fits into the ledger view hierarchy alongside `Sandbox`. Both inherit from `detail::ApplyViewBase`, which itself implements the `ApplyView` + `RawView` dual interface. While a plain `Sandbox` is a straightforward journal of proposed ledger mutations, `PaymentSandbox` adds the deferred-credit bookkeeping layer via the hook protocol defined in `ApplyView`. + +## The Core Problem: Circular Liquidity + +Consider a path `A → B → A`. When the engine credits `A` in a later step, that credit must not appear as usable funds when computing whether `A` can send in the earlier step. The same applies to any path that touches an account more than once, or to multiple concurrent paths that share intermediate accounts. The fix is not to block credits — mutations must still propagate for the accounting to be correct — but to prevent freshly-arriving credits from being seen when computing the available balance for outgoing transfers. + +## `DeferredCredits` — The Bookkeeping Ledger + +The inner class `detail::DeferredCredits` maintains two separate tables: one for IOU trust lines (`creditsIOU_`) keyed by a canonical `(lowAccount, highAccount, currency)` triple, and one for MPT issuances (`creditsMPT_`) keyed by `MPTID`. Every time a credit is applied through `PaymentSandbox`, it is recorded in these tables along with the *pre-credit balance* at the point the first credit was recorded. Subsequent credits for the same pair only accumulate amounts; they do not overwrite the saved original balance. + +For IOU, each record stores separate credit accumulations for the "low" and "high" accounts (canonically ordered by `AccountID`) so that the `adjustmentsIOU()` query can correctly orient the debits and credits regardless of the direction of the query. + +For MPT, the structure is necessarily different because MPT does not have bidirectional trust lines. `IssuerValueMPT` tracks per-holder debit amounts plus an aggregate credit to the issuer's outstanding amount. A special `selfDebit` field handles the case where the issuer itself owns a sell offer — the payment engine runs in reverse, so crediting an MPT holder first can temporarily push `OutstandingAmount` above `MaximumAmount`. The `selfDebit` field captures how much the issuer has "sold" via offers so that `balanceHookSelfIssueMPT` can correctly limit what additional issuance is possible. + +Owner count tracking has its own entry in `DeferredCredits`. The `ownerCount` setter stores the *maximum* of the current and next counts. The comment explains the rationale: since payments only ever decrease owner counts, the highest remembered count is the conservative bound. `ownerCountHook` then returns the max across all sandboxes in the chain, preventing a transient low count from bypassing reserve checks mid-payment. + +## The Hook Protocol + +`ApplyView` declares virtual no-ops for `creditHookIOU`, `creditHookMPT`, `issuerSelfDebitHookMPT`, `adjustOwnerCountHook`, and `ownerCountHook`. Higher-level ledger mutation helpers (in `View.h`) call these hooks at every credit and owner-count transition. Ordinary `Sandbox` ignores them via the base class defaults. `PaymentSandbox` overrides all of them, delegating directly into its private `DeferredCredits tab_` instance. This design keeps the hook plumbing orthogonal to the general view machinery — no transaction type other than payments needs to pay for the overhead. + +## Balance Adjustment Logic + +`balanceHookIOU()` traverses the sandbox chain (via the `ps_` parent pointer) and accumulates total debits from all ancestor sandboxes. The implementation comments explain a deliberate numerical-stability choice: rather than computing `(B+C) - C` (subtracting credits from an already-credited balance), it stores the original balance `B` and subtracts debits. When `B` and `C` differ by many orders of magnitude, floating-point arithmetic causes `(B+C) - C ≠ B`. The adjusted amount is `min(amount, lastBalance - debits, minBalance)` — the three-way minimum ensures correctness in edge cases where the deferred table might overestimate by a small rounding error. A special-case clamps negative XRP results to zero; a large credit followed by the same debit can legitimately produce a negative computed value that is not actually an error. + +`balanceHookMPT()` and `balanceHookSelfIssueMPT()` follow the same principle for holders and issuers respectively, but work in raw `int64_t` arithmetic since MPT amounts are not signed `STAmount` values. + +## Stacking and Apply + +`PaymentSandbox` can be constructed on top of another `PaymentSandbox` using the explicit pointer constructors. The parent pointer `ps_` forms a singly-linked chain. This nesting is used inside the pathfinding engine, which runs each candidate strand in a disposable child sandbox and only commits to the parent on success. The comment in the header is emphatic: **if you are constructing on top of a `PaymentSandbox`, you must use the pointer constructors** — ordinary view-to-view construction would bypass the deferred-credit propagation and break invariants. + +`apply(RawView& to)` is the terminal commit: it asserts `ps_ == nullptr` (the sandbox has no parent) and flushes the state journal to the raw ledger. `apply(PaymentSandbox& to)` asserts that `&to == ps_` (you can only apply to your direct parent) and propagates both the state journal and the deferred credits via `tab_.apply(to.tab_)`. In `DeferredCredits::apply()`, original balances are never overwritten — only credit accumulators are merged and owner-count maximums are taken. + +## `balanceChanges()` and `xrpDestroyed()` + +These are observational APIs used after payment execution. `balanceChanges()` iterates the state journal via `items_.visit()`, diffing every modified `ltACCOUNT_ROOT` and `ltRIPPLE_STATE` ledger entry against the pre-payment view to compute net balance deltas per `(low, high, currency)` triple. It also records per-issuer totals using the diagonal key `(account, account, currency)`. `xrpDestroyed()` delegates to `items_.dropsDestroyed()` to report fees burned during the payment, distinct from transferred XRP. \ No newline at end of file diff --git a/include/xrpl/ledger/PendingSaves.h.ai.json b/include/xrpl/ledger/PendingSaves.h.ai.json new file mode 100644 index 0000000000..c2504a8e19 --- /dev/null +++ b/include/xrpl/ledger/PendingSaves.h.ai.json @@ -0,0 +1,55 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "PendingSaves" + } + ], + "description": "This file defines the PendingSaves class, which manages and tracks ledgers that are in the process of being saved but have not yet been fully written, providing thread-safe operations for starting, finishing, and querying the status of ledger saves.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/PendingSaves.h", + "functions": [ + { + "args": [ + "LedgerIndex seq" + ], + "lineno": 22, + "name": "startWork" + }, + { + "args": [ + "LedgerIndex seq" + ], + "lineno": 39, + "name": "finishWork" + }, + { + "args": [ + "LedgerIndex seq" + ], + "lineno": 51, + "name": "pending" + }, + { + "args": [ + "LedgerIndex seq", + "bool isSynchronous" + ], + "lineno": 59, + "name": "shouldWork" + }, + { + "args": [], + "lineno": 85, + "name": "getSnapshot" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/PendingSaves.h.ai.md b/include/xrpl/ledger/PendingSaves.h.ai.md new file mode 100644 index 0000000000..39e6ec4f8f --- /dev/null +++ b/include/xrpl/ledger/PendingSaves.h.ai.md @@ -0,0 +1,35 @@ +# `PendingSaves.h` — In-Flight Ledger Save Tracking + +`PendingSaves` solves a narrow but critical consistency problem: when a validated ledger is being written to the SQLite relational database, there is a window in which the ledger 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. + +The class lives in `include/xrpl/ledger/PendingSaves.h`, is instantiated once as a value member (`pendingSaves_`) inside the application object, and is exposed through the `ServiceRegistry` interface as `getPendingSaves()`. Two call sites drive its lifecycle: `pendSaveValidated()` / `saveValidatedLedger()` in `LedgerPersistence.cpp`, and `LedgerMaster::getValidatedRange()` which reads a snapshot to shrink the reported range. + +## Internal State Machine + +The core state is `std::map map_`, protected by a `std::mutex`. Each entry encodes one of three observable states: + +| Map state | Meaning | +|---|---| +| key absent | Not pending; safe for DB queries | +| key present, value `false` | Registered/dispatched, but no thread has started the DB write yet | +| key present, value `true` | A thread is actively writing to SQLite | + +The `std::condition_variable await_` is signaled in `finishWork()` so that threads blocked in `shouldWork()` can re-evaluate after a write completes. + +## The Four-Method Protocol + +**`shouldWork(seq, isSynchronous)`** is the entry point. If `seq` is absent, it inserts the entry as `false` and returns `true` — the caller owns the right to dispatch this work. If `seq` is already present with `false` (dispatched but unstarted), an asynchronous caller returns `false` (already dispatched, no duplication needed), but a *synchronous* caller returns `true` (it can "steal" the work before any thread claims it). If `seq` is present with `true` (actively in progress), a synchronous caller blocks on `await_` in a `do/while` loop, re-checking after each notification until the entry disappears. This blocking path ensures that a synchronous `pendSaveValidated` call does not return until the database write is complete. + +**`startWork(seq)`** atomically claims the work. It flips the entry from `false` to `true` and returns `true`. If the entry is absent or already `true`, it returns `false` — meaning either the save completed out from under the caller, or another thread already started it. The caller in `saveValidatedLedger()` uses this as a guard: a `false` return causes an early return with a "Save aborted" log, preventing double-writes. + +**`finishWork(seq)`** erases the entry from the map and calls `notify_all()`. Erasing rather than resetting the boolean is intentional: absence is the canonical "done" state used by `pending()` and the outer loop in `shouldWork()`. + +**`getSnapshot()`** returns a copy of the entire map under the lock. This is used by `LedgerMaster::getValidatedRange()` which iterates the snapshot to trim the min/max validated sequence range, excluding any ledger whose sequence number appears in the map (regardless of whether its flag is `false` or `true`). The trimming applies first to the boundary values, then uses a best-effort midpoint split for any interior pending sequences — trading range width for correctness. + +## Concurrency Design + +The combination of a single mutex with a condition variable is a deliberate simplicity choice: the critical section for any single operation is tiny (map lookup, insert, erase), so contention is negligible. The blocking synchronous path in `shouldWork()` re-acquires the same lock after waking, and re-checks in a loop because `notify_all()` can wake multiple waiters, only one of which will find the entry gone. + +The unit test in `PendingSaves_test.cpp` explicitly exercises the "work stealing" scenario: `shouldWork(0, false)` registers the entry, then `shouldWork(0, true)` by a synchronous caller returns `true` (steals the work), and `startWork(0)` by the first caller subsequently succeeds while a second `startWork(0)` returns `false`. This guards against duplicate DB writes when asynchronous dispatch races against synchronous demand. + +The class does not own or reference a `JobQueue` or thread pool — it is purely a coordination primitive. All scheduling decisions live in `pendSaveValidated()`, which decides whether to enqueue an async job or fall through to a synchronous write based on `isSynchronous` and `JobQueue` availability. `PendingSaves` is concerned only with tracking state, not policy. \ No newline at end of file diff --git a/include/xrpl/ledger/RawView.h.ai.json b/include/xrpl/ledger/RawView.h.ai.json new file mode 100644 index 0000000000..977042e81e --- /dev/null +++ b/include/xrpl/ledger/RawView.h.ai.json @@ -0,0 +1,92 @@ +{ + "args": [ + { + "lineno": 22, + "name": "sle" + }, + { + "lineno": 33, + "name": "sle" + }, + { + "lineno": 45, + "name": "sle" + }, + { + "lineno": 56, + "name": "fee" + }, + { + "lineno": 70, + "name": "key" + }, + { + "lineno": 70, + "name": "txn" + }, + { + "lineno": 70, + "name": "metaData" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "RawView" + }, + { + "args": [], + "lineno": 63, + "name": "TxsRawView" + } + ], + "description": "Defines interfaces for raw modification of ledger entries and transaction insertion in the XRPL ledger, including RawView and TxsRawView classes.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/RawView.h", + "functions": [ + { + "args": [ + "sle" + ], + "lineno": 22, + "name": "rawErase" + }, + { + "args": [ + "sle" + ], + "lineno": 33, + "name": "rawInsert" + }, + { + "args": [ + "sle" + ], + "lineno": 45, + "name": "rawReplace" + }, + { + "args": [ + "fee" + ], + "lineno": 56, + "name": "rawDestroyXRP" + }, + { + "args": [ + "key", + "txn", + "metaData" + ], + "lineno": 70, + "name": "rawTxInsert" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/RawView.h.ai.md b/include/xrpl/ledger/RawView.h.ai.md new file mode 100644 index 0000000000..4140a34d8b --- /dev/null +++ b/include/xrpl/ledger/RawView.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/ledger/RawView.h` — Raw Ledger Mutation Interface + +`RawView.h` defines two pure abstract interfaces — `RawView` and `TxsRawView` — that represent the lowest-level write surface in the XRPL ledger's view hierarchy. Where `ReadView` describes what can be observed about a ledger and `ApplyView` provides a journaled checkout/update cycle for transaction processing, `RawView` describes what it means to *commit* a mutation unconditionally to some backing store. + +## Role in the View Hierarchy + +The XRPL ledger uses a layered view architecture. Transaction processing never writes directly to a finalized ledger; instead it works through a sandbox (`ApplyView` / `ApplyViewBase`) that journals changes. When the sandbox is satisfied — either because a transaction succeeded or because a consensus round is closing — its buffered mutations need to be flushed down to the parent store. That flush path is `RawView`. + +`detail::RawStateTable::apply(RawView& to)` is the canonical consumer: it iterates its internal map of pending `erase`/`insert`/`replace` actions and dispatches each through the corresponding `raw*` method on whatever backing `RawView` was passed in. This design means the flushing logic is written once against the three-operation contract, and any concrete target — a finalizing `Ledger`, an open-ledger `OpenView`, or another sandbox — implements that contract without exposing higher-level checkout semantics. + +## `RawView` + +`RawView` exposes exactly four operations, one per fundamental mutation type: + +- `rawErase(sle)` — remove an existing state item. The full `SLE` is passed (not just its key) so that implementations can compute metadata like the change in owner count or deleted ledger object type. +- `rawInsert(sle)` — unconditionally insert; the key must not already exist. The key is read from the `SLE` itself rather than passed separately, which prevents key/value mismatches. +- `rawReplace(sle)` — unconditionally overwrite; the key must already exist. Same key-from-SLE convention. +- `rawDestroyXRP(fee)` — permanently remove a quantity of XRP drops from the ledger supply. This is the accounting hook for transaction fees, which are burned in XRPL rather than redistributed. Separating this from `rawErase` makes fee accounting explicit and auditable. + +The "raw" prefix is intentional and carries a semantic contract: these methods perform no pre-condition checking, no journaling, and no ownership tracking. They are the implementation side of the commit path, not the API that transaction logic should call directly. + +The copy constructor is defaulted while the assignment operator is deleted. For an abstract base, this is a deliberate asymmetry: it signals that subclasses may be copyable (useful when snapshotting a view's state) but that assignment across different concrete types should not silently succeed. + +## `TxsRawView` + +`TxsRawView` inherits `RawView` and adds one method: + +```cpp +virtual void rawTxInsert( + ReadView::key_type const& key, + std::shared_ptr const& txn, + std::shared_ptr const& metaData) = 0; +``` + +This inserts a serialized transaction — and optionally its execution metadata — into the ledger's transaction map. The `metaData` parameter is nullable by convention: open ledgers don't produce transaction metadata (consensus hasn't closed yet), while closed ledgers require it. The comment in the header makes this invariant explicit rather than leaving it to callers to discover. + +The split between `RawView` (state-only writes) and `TxsRawView` (state plus transaction map) is architecturally meaningful. `detail::ApplyViewBase` only needs to implement `RawView` — sandboxes accumulate state mutations but don't independently maintain a transaction map. `OpenView`, by contrast, inherits from both `ReadView` and `TxsRawView`: it is the accumulation point for an open ledger round and must track both state changes and the growing set of applied transactions. + +## Concrete Implementations + +`detail::ApplyViewBase` inherits from both `ApplyView` and `RawView`, realizing the full read-modify-write-and-commit stack. Its `rawErase`/`rawInsert`/`rawReplace` implementations delegate to an internal `detail::ApplyStateTable`, which in turn owns a `RawStateTable`. When an `ApplyViewBase` is applied to its parent, `RawStateTable::apply()` drives all mutations back through the parent's `RawView` interface. + +`OpenView` inherits `TxsRawView` and implements `rawTxInsert` to store serialized transaction data in an ordered map, while its state mutation methods delegate to its own `RawStateTable`. This architecture ensures `OpenView` can be used as the sink for closing a consensus round without requiring any knowledge of higher-level transaction logic. + +The result is a clean separation: `ApplyView` is for transaction code that needs safe, journaled mutation with ownership semantics; `RawView` is for infrastructure code that flushes committed changes unconditionally. Nothing in the `raw*` path touches checksums, performs existence validation, or participates in the SLE checkout protocol — it is the fast, trust-the-caller commit surface. \ No newline at end of file diff --git a/include/xrpl/ledger/ReadView.h.ai.json b/include/xrpl/ledger/ReadView.h.ai.json new file mode 100644 index 0000000000..e354f1e200 --- /dev/null +++ b/include/xrpl/ledger/ReadView.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "ReadView" + }, + { + "args": [], + "lineno": 168, + "name": "DigestAwareReadView" + } + ], + "description": "Defines the ReadView interface for read-only access to XRPL ledger state and transactions, including iterators and hooks for balance and owner count adjustments. Also defines DigestAwareReadView for digest-aware access, and utility functions for rules extraction.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/ReadView.h", + "functions": [ + { + "args": [ + "DigestAwareReadView const& ledger", + "Rules const& current" + ], + "lineno": 196, + "name": "makeRulesGivenLedger" + }, + { + "args": [ + "DigestAwareReadView const& ledger", + "std::unordered_set> const& presets" + ], + "lineno": 199, + "name": "makeRulesGivenLedger" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/ReadView.h.ai.md b/include/xrpl/ledger/ReadView.h.ai.md new file mode 100644 index 0000000000..1ab2860dc4 --- /dev/null +++ b/include/xrpl/ledger/ReadView.h.ai.md @@ -0,0 +1,77 @@ +# `include/xrpl/ledger/ReadView.h` + +## Role in the System + +`ReadView.h` defines the foundational read-only interface for accessing XRPL ledger state. It sits at the base of the entire ledger view hierarchy: every concrete ledger representation — whether a finalized `Ledger`, an in-progress `OpenView`, an apply-time `Sandbox`, or a payment-path `PaymentSandbox` — exposes its state to the rest of the engine through this interface. Nothing that only reads ledger data needs to know which concrete type it's working with, which is the key benefit of the abstraction. + +The design also includes `DigestAwareReadView`, a thin extension that adds per-entry cryptographic digests, and two `makeRulesGivenLedger` factory functions that derive the active amendment `Rules` from a ledger object. + +## `ReadView`: Pure Abstract Interface + +`ReadView` exposes two conceptually distinct maps: the **state map** (SLEs — Serialized Ledger Entries) and the **transaction map** (committed transactions plus their metadata). + +### Core Pure-Virtual Contract + +The state-map side requires four pure virtual methods: + +- `exists(Keylet const& k)`: checks whether a state entry of the given type and key is present. The `Keylet` structure bundles a raw `uint256` key with its `LedgerEntryType`, giving `exists` a chance to reject type mismatches without deserializing. This makes it more efficient than `read()` for pure presence checks. +- `succ(key_type, optional last)`: returns the smallest key strictly greater than the argument, optionally bounded by `last`. This enables range scans of the SHAMap without deserializing every entry. +- `read(Keylet const& k)`: returns `std::shared_ptr` — ownership of a non-modifiable SLE — or `nullptr` when the key is absent or when the ledger entry type doesn't match the keylet. The `const` qualifier on the SLE is the caller's view; the underlying object can still be mutated through `ApplyView` in a different code path. + +The transaction-map side provides `txExists()` and `txRead()`, which return a `tx_type` pair: `std::pair, std::shared_ptr>`. For open ledgers the metadata `STObject` is empty, since metadata is only finalized at close time. + +Convenience methods like `seq()`, `parentCloseTime()` are non-virtual inline wrappers that delegate to `header()`, keeping the virtual surface small. + +### Copy and Move Semantics — A Subtle Invariant + +`ReadView` holds two public member objects — `sles` and `txs` — of nested types `sles_type` and `txs_type`. Both are subclasses of `detail::ReadViewFwdRange`, which stores a raw pointer to the owning `ReadView`. This creates a well-known C++ trap: if a copy or move constructor defaulted to memberwise initialization, `sles.view_` and `txs.view_` would point at the *source* object, not the newly constructed one. + +The header prevents this by explicitly re-initializing both members with `*this` in every constructor: + +```cpp +ReadView(ReadView const& other) : sles(*this), txs(*this) {} +ReadView(ReadView&& other) : sles(*this), txs(*this) {} +``` + +Both assignment operators are deleted to prevent the same aliasing from arising post-construction. Derived classes must respect this pattern. + +### Iterable Ranges via Type Erasure + +`sles_type` and `txs_type` expose STL-style `begin()` / `end()` iterators, enabling range-based for loops over all state entries or transactions. The actual iteration is implemented through the abstract `ReadViewFwdIter` class in `detail/ReadViewFwdRange.h`, which virtualizes `copy()`, `equal()`, `increment()`, and `dereference()`. Concrete views implement the virtual factories `slesBegin()`, `slesEnd()`, `slesUpperBound()`, `txsBegin()`, and `txsEnd()` to return heap-allocated `unique_ptr` objects — one allocation per iterator, not per element. + +The `iterator` class in `ReadViewFwdRange` wraps that pointer and adds a mutable `optional cache_` to materialize the dereferenced value on demand. This type-erasure approach lets `sles_type` and `txs_type` present a uniform STL iterator interface regardless of whether the view is backed by a SHAMap, a flat delta list, or a layered sandbox. + +The `sles_type` also provides `upper_bound(key)`, which maps directly to `slesUpperBound()` — useful for iterating a sub-range of state entries without paying the cost of a full scan. + +### Balance and Owner-Count Hooks + +`ReadView` declares four virtual methods with default pass-through implementations: `balanceHookIOU`, `balanceHookMPT`, `balanceHookSelfIssueMPT`, and `ownerCountHook`. These are extension points, not core query methods. + +The XRPL payment engine executes paths in **reverse order** (destination side first). This means an intermediate account may be credited before it has actually redeemed the corresponding asset, temporarily inflating its balance. The rule is that accounts in a payment may not use assets acquired *during* that same payment — each step must see only the pre-payment balance. + +`PaymentSandbox` enforces this by overriding these hooks. When `balanceHookIOU` or `balanceHookMPT` is called during a payment, the sandbox subtracts deferred credits recorded in its `DeferredCredits` table. `ownerCountHook` returns the maximum owner count seen so far rather than the current count, preventing reserve-bypass exploits where a payment temporarily frees reserves that are logically still committed. The default implementations in `ReadView` simply return the arguments unchanged, making the hooks zero-cost for views that do not participate in payment processing. + +A complementary set of credit/debit hooks lives in `ApplyView` (`creditHookIOU`, `creditHookMPT`, `adjustOwnerCountHook`, `issuerSelfDebitHookMPT`). Those are called on the write path to *record* the adjustments, while the `ReadView` hooks are called on the read path to *apply* them. + +## `DigestAwareReadView` + +`DigestAwareReadView` adds a single pure virtual method: `digest(key_type const& key) -> optional`. This returns the cryptographic hash of a state entry's serialized content, without necessarily deserializing the entry itself. The `Ledger` class, which stores state in a SHAMap, can answer this question cheaply by inspecting the trie node without loading the leaf. Sandboxes and delta views may not implement this concept at all, which is why the capability is a separate subclass rather than part of `ReadView`. + +## `makeRulesGivenLedger` + +The two free functions construct a `Rules` object from a ledger. Both are `friend` of `Rules` and call its private three-argument constructor. The implementation in `ReadView.cpp` is instructive: + +```cpp +Keylet const k = keylet::amendments(); +std::optional const digest = ledger.digest(k.key); +if (digest) { + auto const sle = ledger.read(k); + if (sle) + return Rules(presets, digest, sle->getFieldV256(sfAmendments)); +} +return Rules(presets); +``` + +The digest is passed along with the amendment vector so that `Rules` can cache it and quickly detect when the amendments object has not changed between ledger closes, avoiding repeated re-parsing. This is why the function requires `DigestAwareReadView` rather than plain `ReadView`: the optimization depends on the ability to query a state entry's hash directly. + +The two overloads differ only in how they obtain the preset set. The first takes a `Rules const& current` and extracts its internal presets (used when updating rules at ledger close), while the second takes the preset set directly (used during initialization). Both paths feed into the same internal factory logic. \ No newline at end of file diff --git a/include/xrpl/ledger/Sandbox.h.ai.json b/include/xrpl/ledger/Sandbox.h.ai.json new file mode 100644 index 0000000000..6d1b968345 --- /dev/null +++ b/include/xrpl/ledger/Sandbox.h.ai.json @@ -0,0 +1,53 @@ +{ + "args": [ + { + "lineno": 22, + "name": "base" + }, + { + "lineno": 22, + "name": "flags" + }, + { + "lineno": 26, + "name": "base" + }, + { + "lineno": 32, + "name": "to" + } + ], + "classes": [ + { + "args": [ + "Sandbox() = delete", + "Sandbox(Sandbox const&) = delete", + "Sandbox& operator=(Sandbox&&) = delete", + "Sandbox& operator=(Sandbox const&) = delete", + "Sandbox(Sandbox&&) = default", + "Sandbox(ReadView const* base, ApplyFlags flags)", + "Sandbox(ApplyView const* base)" + ], + "lineno": 11, + "name": "Sandbox" + } + ], + "description": "Defines the xrpl::Sandbox class, a discardable, editable view to a ledger, inheriting from ApplyViewBase and used for temporary modifications before applying to a ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/Sandbox.h", + "functions": [ + { + "args": [ + "to" + ], + "lineno": 32, + "name": "apply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/Sandbox.h.ai.md b/include/xrpl/ledger/Sandbox.h.ai.md new file mode 100644 index 0000000000..8af0482be5 --- /dev/null +++ b/include/xrpl/ledger/Sandbox.h.ai.md @@ -0,0 +1,36 @@ +# `include/xrpl/ledger/Sandbox.h` + +`Sandbox` is the standard discardable staging layer used by XRPL transaction processors to attempt ledger mutations without committing them permanently. It represents the simplest complete implementation of `detail::ApplyViewBase`, delegating every read and write operation to the base view through a buffered change table, then offering a single commit-or-discard decision point via `apply()`. + +## Role in the View Hierarchy + +The XRPL ledger access model is built around a layered view hierarchy. `ReadView` provides read-only access. `ApplyView` adds the ability to peek at mutable `SLE` objects and issue insert/update/erase operations. `RawView` adds lower-level unconditional mutations. `detail::ApplyViewBase` fuses all three via multiple inheritance and stores all changes in a protected `ApplyStateTable items_` member that acts as a write buffer. + +`Sandbox` adds nothing to `ApplyViewBase` except two constructors and the `apply()` method. This minimalism is intentional — the full complexity lives in `ApplyStateTable`, and `Sandbox` simply exposes the "commit to a target" operation to callers. + +## Buffering and the Commit Model + +All modifications made through a `Sandbox` are accumulated in `items_` as tagged `(Action, SLE)` pairs, where `Action` is one of `cache`, `erase`, `insert`, or `modify`. The underlying `ReadView` base is never touched during this accumulation. When `apply(RawView& to)` is called, `ApplyStateTable::apply()` replays every buffered action against the target `RawView`, atomically promoting the tentative changes into it. + +This pattern appears throughout the transaction processing layer. In `AMMCreate::doApply()`, for example, a `Sandbox` is constructed over the transactor's current `ApplyView`, all ledger mutations for AMM pool creation are applied through the sandbox, and only if the operation reports success is `sb.apply(ctx_.rawView())` called. On failure, the sandbox is simply destroyed, leaving the ledger unchanged — no rollback needed. + +## Constructor Design + +Two constructors cover the two call patterns found in practice: + +```cpp +Sandbox(ReadView const* base, ApplyFlags flags); +Sandbox(ApplyView const* base); +``` + +The first is the general form: any read-only view plus explicit flags. The second convenience form is used when stacking a `Sandbox` on top of another `ApplyView` (including another `Sandbox`); it inherits flags from the parent, preserving properties like `tapNO_CHECK_SIGN` or `tapDRY_RUN` across layers without the caller having to re-specify them. + +Copy construction and both assignment operators are deleted to prevent accidental duplication of the change buffer — only move construction is permitted. This enforces a clear ownership model: a `Sandbox` is created, used, and either committed or discarded, never shared. + +## Relationship to Sibling Classes + +`Sandbox` has two closely related siblings. `ApplyViewImpl` is the heavier variant used at the outermost transaction application boundary: its `apply()` takes an `OpenView` along with the `STTx` and `TER`, constructs full `TxMeta` metadata, and threads ownership links — capabilities `Sandbox` intentionally omits because inner operations don't need metadata. `PaymentSandbox` extends `Sandbox`'s semantics with `DeferredCredits` tracking, overriding `balanceHook` and `creditHook` so that credits from one step of a payment path cannot be double-counted as available liquidity in a subsequent step. `Sandbox` itself carries none of this payment-specific logic and is the right choice wherever a transactor simply needs a safe scratchpad. + +## Key Invariant + +Because `Sandbox` inherits the flags of its base view (the comment "The sandbox inherits the flags of the base" in the class definition is precise), code that queries `flags()` on a `Sandbox` will see exactly the same flags as the parent view. This prevents the sandbox from accidentally changing the execution context — e.g., dry-run semantics propagate correctly through nested sandboxes without any additional plumbing. \ No newline at end of file diff --git a/include/xrpl/ledger/View.h.ai.json b/include/xrpl/ledger/View.h.ai.json new file mode 100644 index 0000000000..edecf22fba --- /dev/null +++ b/include/xrpl/ledger/View.h.ai.json @@ -0,0 +1,397 @@ +{ + "args": [ + { + "lineno": 33, + "name": "view" + }, + { + "lineno": 33, + "name": "exp" + }, + { + "lineno": 45, + "name": "view" + }, + { + "lineno": 45, + "name": "account" + }, + { + "lineno": 45, + "name": "mptShare" + }, + { + "lineno": 45, + "name": "depth" + }, + { + "lineno": 51, + "name": "view" + }, + { + "lineno": 51, + "name": "account" + }, + { + "lineno": 51, + "name": "asset" + }, + { + "lineno": 51, + "name": "asset2" + }, + { + "lineno": 54, + "name": "view" + }, + { + "lineno": 59, + "name": "view" + }, + { + "lineno": 73, + "name": "ledger" + }, + { + "lineno": 73, + "name": "seq" + }, + { + "lineno": 73, + "name": "journal" + }, + { + "lineno": 92, + "name": "requested" + }, + { + "lineno": 109, + "name": "validLedger" + }, + { + "lineno": 109, + "name": "testLedger" + }, + { + "lineno": 109, + "name": "s" + }, + { + "lineno": 109, + "name": "reason" + }, + { + "lineno": 116, + "name": "validHash" + }, + { + "lineno": 116, + "name": "validIndex" + }, + { + "lineno": 116, + "name": "testLedger" + }, + { + "lineno": 116, + "name": "s" + }, + { + "lineno": 116, + "name": "reason" + }, + { + "lineno": 128, + "name": "view" + }, + { + "lineno": 128, + "name": "owner" + }, + { + "lineno": 128, + "name": "object" + }, + { + "lineno": 128, + "name": "node" + }, + { + "lineno": 140, + "name": "view" + }, + { + "lineno": 140, + "name": "from" + }, + { + "lineno": 140, + "name": "to" + }, + { + "lineno": 140, + "name": "toSle" + }, + { + "lineno": 140, + "name": "amount" + }, + { + "lineno": 140, + "name": "hasDestinationTag" + }, + { + "lineno": 155, + "name": "view" + }, + { + "lineno": 155, + "name": "from" + }, + { + "lineno": 155, + "name": "to" + }, + { + "lineno": 155, + "name": "amount" + }, + { + "lineno": 155, + "name": "hasDestinationTag" + }, + { + "lineno": 170, + "name": "view" + }, + { + "lineno": 170, + "name": "tx" + }, + { + "lineno": 173, + "name": "view" + }, + { + "lineno": 173, + "name": "tx" + }, + { + "lineno": 173, + "name": "senderAcct" + }, + { + "lineno": 173, + "name": "dstAcct" + }, + { + "lineno": 173, + "name": "sourceAcct" + }, + { + "lineno": 173, + "name": "priorBalance" + }, + { + "lineno": 173, + "name": "amount" + }, + { + "lineno": 173, + "name": "j" + }, + { + "lineno": 192, + "name": "view" + }, + { + "lineno": 192, + "name": "ownerDirKeylet" + }, + { + "lineno": 192, + "name": "deleter" + }, + { + "lineno": 192, + "name": "j" + }, + { + "lineno": 192, + "name": "maxNodesToDelete" + }, + { + "lineno": 207, + "name": "now" + }, + { + "lineno": 207, + "name": "mark" + } + ], + "classes": [], + "description": "This file provides utility functions and types for working with the XRP Ledger, including expiration checks, amendment queries, ledger compatibility, withdrawal checks, and account deletion cleanup.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/View.h", + "functions": [ + { + "args": [ + "view", + "exp" + ], + "lineno": 32, + "name": "hasExpired" + }, + { + "args": [ + "view", + "account", + "mptShare", + "depth" + ], + "lineno": 44, + "name": "isVaultPseudoAccountFrozen" + }, + { + "args": [ + "view", + "account", + "asset", + "asset2" + ], + "lineno": 50, + "name": "isLPTokenFrozen" + }, + { + "args": [ + "view" + ], + "lineno": 53, + "name": "getEnabledAmendments" + }, + { + "args": [ + "view" + ], + "lineno": 58, + "name": "getMajorityAmendments" + }, + { + "args": [ + "ledger", + "seq", + "journal" + ], + "lineno": 72, + "name": "hashOfSeq" + }, + { + "args": [ + "requested" + ], + "lineno": 91, + "name": "getCandidateLedger" + }, + { + "args": [ + "validLedger", + "testLedger", + "s", + "reason" + ], + "lineno": 108, + "name": "areCompatible" + }, + { + "args": [ + "validHash", + "validIndex", + "testLedger", + "s", + "reason" + ], + "lineno": 115, + "name": "areCompatible" + }, + { + "args": [ + "view", + "owner", + "object", + "node" + ], + "lineno": 127, + "name": "dirLink" + }, + { + "args": [ + "view", + "from", + "to", + "toSle", + "amount", + "hasDestinationTag" + ], + "lineno": 139, + "name": "canWithdraw" + }, + { + "args": [ + "view", + "from", + "to", + "amount", + "hasDestinationTag" + ], + "lineno": 154, + "name": "canWithdraw" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 169, + "name": "canWithdraw" + }, + { + "args": [ + "view", + "tx", + "senderAcct", + "dstAcct", + "sourceAcct", + "priorBalance", + "amount", + "j" + ], + "lineno": 172, + "name": "doWithdraw" + }, + { + "args": [ + "view", + "ownerDirKeylet", + "deleter", + "j", + "maxNodesToDelete" + ], + "lineno": 191, + "name": "cleanupOnAccountDelete" + }, + { + "args": [ + "now", + "mark" + ], + "lineno": 206, + "name": "after" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/View.h.ai.md b/include/xrpl/ledger/View.h.ai.md new file mode 100644 index 0000000000..b27e879b1f --- /dev/null +++ b/include/xrpl/ledger/View.h.ai.md @@ -0,0 +1,61 @@ +# `include/xrpl/ledger/View.h` + +This header is the utility layer that sits above the `ReadView`/`ApplyView` abstraction hierarchy and provides the concrete, ledger-aware operations that transaction processors actually call. Where `ReadView.h` defines the abstract interface for querying ledger state and `ApplyView.h` extends it with a checkout/update mutation model, `View.h` supplies the *algorithms* built on top of those interfaces — expiration checks, amendment queries, ledger history navigation, withdrawal authorization, and account-deletion cleanup. The file is organized into two sections that mirror the underlying hierarchy: **Observers** (read-only, take `ReadView const&`) and **Modifiers** (mutating, take `ApplyView&`). + +## Time and Expiration + +`hasExpired()` encodes a subtle but critical ledger rule: expiration is measured against the **parent ledger's close time**, not the current ledger being built. This is intentional — consensus agrees on the close time of already-closed ledgers, but the close time of the ledger currently under construction is not yet known to validators. The implementation compares the `parentCloseTime()` from `ReadView::header()` against the XRPL epoch–based timestamp stored in the SLE. The `after()` helper performs the symmetric check (whether a given moment has passed a raw epoch-seconds mark) and is used internally by timing logic elsewhere in the codebase. + +## Skip List and Ledger History Navigation + +`hashOfSeq()` implements a three-tier lookup through the XRPL's skip-list structure to retrieve the hash of an arbitrary past ledger: + +1. **Trivial cases**: if the requested sequence equals the current ledger sequence, return its hash; if it's exactly one prior, return `parentHash` directly. +2. **Within 256**: Read the current ledger's `skip` keylet, which stores hashes for the 256 preceding ledgers, and index into the vector by offset. +3. **Deep history**: For sequences that are multiples of 256, read the dedicated `LedgerHashes` page for that epoch and index into it. + +`getCandidateLedger()` is a compact bit-manipulation helper that computes the nearest ledger sequence that is both ≥ the requested sequence and a multiple of 256. This is the ledger that permanently stores a `LedgerHashes` page and from which any arbitrary past hash can be recovered. The arithmetic `(requested + 255) & (~255)` rounds up to the next 256 boundary in a single instruction. + +Together these two functions underlie the network's ability to prove ledger ancestry without replaying the full chain. + +## Ledger Compatibility Checks + +`areCompatible()` provides two overloads for detecting ledger forks. The first takes two `ReadView` objects; the second accepts a known `(hash, index)` pair for the "valid" ledger alongside a candidate `ReadView` — useful when the valid ledger hasn't been fully loaded but its identity is known from consensus. In either case the logic uses `hashOfSeq()` to walk the skip list of whichever ledger is later and verify that the earlier ledger's hash appears in that skip list. A mismatch means the two ledgers cannot share an ancestry chain, i.e., one is a fork. Diagnostic output goes to a `beast::Journal::Stream` so callers control log severity. + +## Amendment State + +`getEnabledAmendments()` and `getMajorityAmendments()` both read the singleton `amendments` SLE from the ledger. The former returns the set of amendment hashes that are already active on-chain. The latter returns a map of amendment hash → `NetClock::time_point` for amendments that have achieved validator supermajority but have not yet activated — used by the amendment process to enforce the two-week waiting period. The type alias `majorityAmendments_t` is defined here alongside the function so callers don't need to repeat the verbose map type. + +## Freeze Checks for AMM and Vault Assets + +`isLPTokenFrozen()` checks whether either pool asset in an AMM liquidity-provider token pair is frozen for a given account. Because LP tokens derive their value from two underlying assets, a freeze on either one is sufficient to block transfers, so both assets are checked via `isFrozen()`. + +`isVaultPseudoAccountFrozen()` handles a more complex recursive case arising from the `SingleAssetVault` feature. Vault pseudo-accounts are synthetic accounts backed by an MPT issuance; determining whether such a pseudo-account is frozen requires resolving the vault's underlying asset and checking *that* asset for freezes — which may itself be a vault-backed MPT (hence the recursion). The `depth` parameter and `maxAssetCheckDepth` guard prevent unbounded recursion in pathological configurations, returning `true` (frozen) if the depth limit is hit as a conservative safe-side default. + +## Withdrawal Authorization: `canWithdraw` Overload Family + +Three overloads of `canWithdraw()` form a funnel of increasing abstraction: + +- The innermost form takes a pre-fetched `toSle` (the destination account's SLE), avoiding a redundant read when the caller already has it. +- The middle form looks up the destination account SLE and delegates to the first. +- The outermost form unpacks a full `STTx`, extracting `sfAccount`, `sfDestination`, `sfAmount`, and `sfDestinationTag`, then delegates to the middle. + +All three ultimately enforce the same rules: the destination must exist; if `lsfRequireDestTag` is set, a destination tag must be present even for self-sends; if `lsfDepositAuth` is set, the sender must have a pre-authorized `DepositPreauth` entry; and for IOU amounts the transfer must not push the receiver beyond their trust line credit limit. The MPT path deliberately skips the credit-limit check since withdrawals transfer existing tokens rather than minting new ones. + +## `doWithdraw`: Executing Vault/Broker Withdrawals + +`doWithdraw()` is the state-mutating complement to `canWithdraw()`. It handles two cases: withdrawal to self (the submitting account equals the destination) and withdrawal to a third party. For self-withdrawals it calls `addEmptyHolding()` to ensure a trust line or MPToken exists, tolerating `tecDUPLICATE` if one already does. For third-party withdrawals it re-validates deposit preauthorization under mutation semantics. The actual fund transfer is delegated to `accountSend()` with `WaiveTransferFee::Yes`, reflecting that vault/broker withdrawals do not charge transfer fees. + +## `dirLink`: Owner Directory Maintenance + +`dirLink()` inserts a newly created SLE into an account's owner directory and records the resulting page number in the SLE's `sfOwnerNode` field (or a custom field passed via the defaulted `node` parameter). Failure returns `tecDIR_FULL` if the directory has hit the protocol-defined page limit — a situation that can only arise in practice with very large accounts. + +## Account Deletion Cleanup + +`cleanupOnAccountDelete()` is the core of the `DeleteAccount` transaction's multi-step iteration. It iterates the owner directory using the exposed-internal-state `dirFirst`/`dirNext` pattern and calls the caller-supplied `EntryDeleter` for each node. The `EntryDeleter` typedef is a `std::function` returning `std::pair`, where `SkipEntry::Yes` tells the loop not to decrement the directory cursor — this handles cases where the deleter chose not to actually remove the current entry and the cursor therefore doesn't need to be rewound. The loop counter re-validation comment in the implementation explains the trick precisely: after a successful delete the entry that was at index `i+1` shifts to `i`, so the iterator must be decremented by one to stay valid. + +The optional `maxNodesToDelete` parameter supports incremental deletion: when the limit is reached the function returns `tecINCOMPLETE`, signaling that the account delete transaction should be retried in a future ledger. This prevents a single transaction from consuming unbounded execution time or exceeding the ledger's compute budget. + +## Design Observations + +The strict separation between observer functions (taking `ReadView const&`) and modifier functions (taking `ApplyView&`) is not cosmetic. It enables the same observer logic to run against any view in the hierarchy — the live ledger, a sandbox, or a `PaymentSandbox` — without the caller needing to know which. The `[[nodiscard]]` attribute on every `TER`-returning function ensures callers cannot silently ignore error codes, a common pitfall in transaction processing. \ No newline at end of file diff --git a/include/xrpl/ledger/detail/ApplyStateTable.h.ai.json b/include/xrpl/ledger/detail/ApplyStateTable.h.ai.json new file mode 100644 index 0000000000..842fdcbe16 --- /dev/null +++ b/include/xrpl/ledger/detail/ApplyStateTable.h.ai.json @@ -0,0 +1,218 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "void" + ], + "lineno": 13, + "name": "ApplyStateTable" + } + ], + "description": "Helper class ApplyStateTable buffers ledger modifications for the XRP Ledger, providing methods to apply, read, modify, and track changes to ledger state.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/detail/ApplyStateTable.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "ApplyStateTable" + }, + { + "args": [ + "ApplyStateTable&&" + ], + "lineno": 19, + "name": "ApplyStateTable" + }, + { + "args": [ + "ApplyStateTable const&" + ], + "lineno": 21, + "name": "ApplyStateTable" + }, + { + "args": [ + "ApplyStateTable&&" + ], + "lineno": 22, + "name": "operator=" + }, + { + "args": [ + "ApplyStateTable const&" + ], + "lineno": 24, + "name": "operator=" + }, + { + "args": [ + "RawView& to" + ], + "lineno": 26, + "name": "apply" + }, + { + "args": [ + "OpenView& to", + "STTx const& tx", + "TER ter", + "std::optional const& deliver", + "std::optional const& parentBatchId", + "bool isDryRun", + "beast::Journal j" + ], + "lineno": 28, + "name": "apply" + }, + { + "args": [ + "ReadView const& base", + "Keylet const& k" + ], + "lineno": 36, + "name": "exists" + }, + { + "args": [ + "ReadView const& base", + "key_type const& key", + "std::optional const& last" + ], + "lineno": 39, + "name": "succ" + }, + { + "args": [ + "ReadView const& base", + "Keylet const& k" + ], + "lineno": 42, + "name": "read" + }, + { + "args": [ + "ReadView const& base", + "Keylet const& k" + ], + "lineno": 45, + "name": "peek" + }, + { + "args": [], + "lineno": 48, + "name": "size" + }, + { + "args": [ + "ReadView const& base", + "std::function const& before, std::shared_ptr const& after)> const& func" + ], + "lineno": 51, + "name": "visit" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 57, + "name": "erase" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 60, + "name": "rawErase" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 63, + "name": "insert" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 66, + "name": "update" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 69, + "name": "replace" + }, + { + "args": [ + "XRPAmount const& fee" + ], + "lineno": 72, + "name": "destroyXRP" + }, + { + "args": [], + "lineno": 76, + "name": "dropsDestroyed" + }, + { + "args": [ + "TxMeta& meta", + "std::shared_ptr const& to" + ], + "lineno": 84, + "name": "threadItem" + }, + { + "args": [ + "ReadView const& base", + "key_type const& key", + "Mods& mods", + "beast::Journal j" + ], + "lineno": 87, + "name": "getForMod" + }, + { + "args": [ + "ReadView const& base", + "TxMeta& meta", + "AccountID const& to", + "Mods& mods", + "beast::Journal j" + ], + "lineno": 90, + "name": "threadTx" + }, + { + "args": [ + "ReadView const& base", + "TxMeta& meta", + "std::shared_ptr const& sle", + "Mods& mods", + "beast::Journal j" + ], + "lineno": 95, + "name": "threadOwners" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/detail/ApplyStateTable.h.ai.md b/include/xrpl/ledger/detail/ApplyStateTable.h.ai.md new file mode 100644 index 0000000000..e501d6f426 --- /dev/null +++ b/include/xrpl/ledger/detail/ApplyStateTable.h.ai.md @@ -0,0 +1,54 @@ +# `ApplyStateTable` — Transaction-Scoped Ledger State Buffer + +`ApplyStateTable` is the write-ahead buffer that sits between a single transaction's processing context and the underlying ledger state. It lives in the `xrpl::detail` namespace because it is an implementation detail of `ApplyViewBase`, not intended to be used directly by transaction handlers. Its two primary responsibilities are (1) accumulating state mutations in a way that can be committed or discarded atomically, and (2) producing the `TxMeta` object that XRPL embeds into every closed-ledger transaction — the machine-readable record of exactly what changed. + +## Core Data Model + +The central data structure is `items_`, a `std::map>>`. Every ledger entry (`SLE`) that is touched during transaction processing gets an entry in this map tagged with one of four `Action` values: + +- **`cache`** — the SLE was read from the base view and is held as a mutable copy, but has not yet been modified. This is the "read-intent" state produced by `peek()`. +- **`modify`** — the SLE was pre-existing and has been changed. +- **`insert`** — the SLE is newly created and does not exist in the base view. +- **`erase`** — the SLE should be deleted when changes are committed. + +The `cache` action is what distinguishes `ApplyStateTable` from its simpler sibling `RawStateTable` (used internally by `OpenView`). `RawStateTable` only has `erase`, `insert`, and `replace` — there is no concept of loading a mutable copy. `ApplyStateTable` adds `cache` to support the `peek()`/`update()` pattern expected by `ApplyView` clients: call `peek()` to get a writable handle, mutate it, then call `update()` to promote it from `cache` to `modify`. + +## State Transition Invariants + +The action tags obey carefully enforced invariants. Several state transitions are prohibited and trigger `LogicError`: + +- Double-erasing the same key. +- Calling `update()` on an SLE that was not previously loaded via `peek()` (i.e., the exact same `shared_ptr` must be present in the map). +- Inserting a key that is already `cache`d or in `modify` state. + +Some transitions intentionally collapse to their logical equivalent. If an entry is `insert`ed and then `erase`d before commit, `erase()` removes it from `items_` entirely — the net effect on the ledger is zero, and no commit overhead is incurred. Conversely, `rawErase` followed by `insert` (which is the sub-transaction nested view pattern) merges into a `modify` action because the underlying object existed, was deleted, and then a new version was created in the same batch. + +The `erase()` and `rawErase()` methods differ in precondition. `erase()` requires that the SLE was previously obtained via `peek()` — it verifies pointer identity (`item.second != sle` triggers `LogicError`). `rawErase()` is more permissive: it can create a new `erase` entry for an SLE that was never `peek()`d, which is needed when sub-views apply their changes upward via the `RawView` interface. + +## Commit Paths + +There are two overloads of `apply()`, each serving a different commit scenario. + +**`apply(RawView& to)`** is the simple path. It calls `to.rawDestroyXRP(dropsDestroyed_)` first, then walks `items_` and dispatches each entry to `to.rawErase()`, `to.rawInsert()`, or `to.rawReplace()`. Cache-only entries are skipped. This path is used when a nested `ApplyViewBase` (e.g., a sandboxed sub-transaction view) commits its changes up to the parent view. + +**`apply(OpenView& to, STTx const& tx, TER ter, ...)`** is the full metadata-building path, triggered when a transaction is applied to a closing ledger (or during a dry run). It constructs a `TxMeta` object and iterates `items_`, categorizing each change as `sfCreatedNode`, `sfModifiedNode`, or `sfDeletedNode`. For each, it computes `sfPreviousFields`, `sfFinalFields`, or `sfNewFields` by comparing the original entry from the base view against the buffered version — but only for fields whose `SField` metadata flags (`sMD_ChangeOrig`, `sMD_Always`, `sMD_DeleteFinal`, `sMD_Create`, `sMD_ChangeNew`) indicate they should appear in the metadata. An optimization skips `modify` entries where the before and after states are byte-equal. + +The `isDryRun` flag separates metadata generation from actual commitment. When `isDryRun` is true, the full `TxMeta` is built and returned, but the call to `apply(to)` and `to.rawTxInsert(...)` are skipped. This allows callers to simulate transaction effects and inspect metadata without mutating the ledger. + +## Transaction Threading + +The private `threadItem()`, `threadTx()`, and `threadOwners()` methods implement XRPL's account threading mechanism. Each `AccountRoot` SLE carries `PreviousTxnID` and `PreviousTxnLgrSeq` fields that form a reverse-linked list through every transaction that touched that account. When a transaction modifies or creates a ledger entry, `threadOwners()` determines which accounts should be threaded based on the entry type: + +- `ltACCOUNT_ROOT` entries thread themselves (handled by `threadItem` when the SLE is of threaded type). +- `ltRIPPLE_STATE` trust line entries thread to both the low and high limit account. +- All other entry types thread to the `sfAccount` field if present, and to `sfDestination` if present. + +`getForMod()` supports threading by fetching a mutable SLE for accounts that need their threading fields updated but were not otherwise part of the transaction's primary changes. It checks a local `Mods` accumulator first, then `items_`, then falls back to copying from the base view. These incidental modifications are tracked separately and flushed to the view via `rawReplace()` after all metadata is assembled — but only when not in dry-run mode. + +## Ownership and Lifecycle + +`ApplyStateTable` is move-constructible but copy-construction and all assignment operators are deleted. The `shared_ptr` instances returned by `peek()` are owned jointly by the caller and the table: `update()` verifies pointer identity, not value equality, to prevent callers from substituting a different SLE object for the same key. This ensures the mutable SLE handed out by `peek()` is the authoritative version that will be committed. + +The `dropsDestroyed_` counter is separate from the `items_` map. Fee destruction is accumulated via `destroyXRP()` and applied unconditionally as the first step of any `apply()` call, regardless of the other mutations. + +`ApplyStateTable` is used exclusively inside `ApplyViewBase`, which is the foundation of all per-transaction apply views in the codebase. It is the mechanism that makes XRPL transaction application both atomic and auditable. \ No newline at end of file diff --git a/include/xrpl/ledger/detail/ApplyViewBase.h.ai.json b/include/xrpl/ledger/detail/ApplyViewBase.h.ai.json new file mode 100644 index 0000000000..0549fb5ffe --- /dev/null +++ b/include/xrpl/ledger/detail/ApplyViewBase.h.ai.json @@ -0,0 +1,180 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ReadView const* base", + "ApplyFlags flags" + ], + "lineno": 8, + "name": "ApplyViewBase" + } + ], + "description": "Defines the ApplyViewBase class, which implements ApplyView and RawView interfaces for managing and applying ledger state changes in the XRPL ledger system.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/detail/ApplyViewBase.h", + "functions": [ + { + "args": [ + "ReadView const* base", + "ApplyFlags flags" + ], + "lineno": 20, + "name": "ApplyViewBase" + }, + { + "args": [], + "lineno": 27, + "name": "open" + }, + { + "args": [], + "lineno": 30, + "name": "header" + }, + { + "args": [], + "lineno": 33, + "name": "fees" + }, + { + "args": [], + "lineno": 36, + "name": "rules" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 39, + "name": "exists" + }, + { + "args": [ + "key_type const& key", + "std::optional const& last" + ], + "lineno": 42, + "name": "succ" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 46, + "name": "read" + }, + { + "args": [], + "lineno": 49, + "name": "slesBegin" + }, + { + "args": [], + "lineno": 52, + "name": "slesEnd" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 55, + "name": "slesUpperBound" + }, + { + "args": [], + "lineno": 58, + "name": "txsBegin" + }, + { + "args": [], + "lineno": 61, + "name": "txsEnd" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 64, + "name": "txExists" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 67, + "name": "txRead" + }, + { + "args": [], + "lineno": 71, + "name": "flags" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 74, + "name": "peek" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 77, + "name": "erase" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 80, + "name": "insert" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 83, + "name": "update" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 87, + "name": "rawErase" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 90, + "name": "rawInsert" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 93, + "name": "rawReplace" + }, + { + "args": [ + "XRPAmount const& feeDrops" + ], + "lineno": 96, + "name": "rawDestroyXRP" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/detail/ApplyViewBase.h.ai.md b/include/xrpl/ledger/detail/ApplyViewBase.h.ai.md new file mode 100644 index 0000000000..131f26ce35 --- /dev/null +++ b/include/xrpl/ledger/detail/ApplyViewBase.h.ai.md @@ -0,0 +1,47 @@ +# `ApplyViewBase.h` — Buffered Ledger State Foundation for Transaction Application + +`ApplyViewBase` is the core abstract-base implementation in the XRPL ledger view hierarchy, living in the `xrpl::detail` namespace to signal that it is internal infrastructure rather than public API. Its purpose is to buffer all ledger state mutations produced during transaction processing — insulating the underlying committed ledger from any partial or failed write — and to present that buffer as a fully-functional `ReadView` to the transaction logic running on top of it. + +## Architectural Position + +The XRPL ledger view system is layered deliberately. `ReadView` is read-only access to the committed state. `ApplyView` extends it with a peek/insert/update/erase API for transactional mutations. `RawView` provides an unconditional write interface (rawInsert/rawReplace/rawErase/rawDestroyXRP) used when applying a committed sandbox down to a lower layer. `ApplyViewBase` inherits from **both** `ApplyView` and `RawView`, making it the first class in the hierarchy that can function simultaneously as a mutable scratch-pad and as a raw sink for another view above it. + +The two raw write interfaces exist for a reason. `ApplyView::insert/update/erase` carry semantic constraints (you must `peek` before `erase`, the key must not pre-exist before `insert`, etc.), and `ApplyStateTable` enforces those invariants with assertions. `RawView::rawInsert/rawReplace/rawErase` bypass those constraints and are intended for use by `Sandbox::apply()` and similar commit-path code that already knows the state is consistent. `ApplyViewBase` satisfies both contracts through the same `items_` buffer. + +## The Three Protected Members + +All state is held in three protected members: + +- `base_` (`ReadView const*`) — a borrowed, non-owning pointer to the underlying committed ledger view. Every read-only query that doesn't involve pending mutations is forwarded here directly. +- `flags_` (`ApplyFlags`) — the set of processing flags for the current transaction (e.g., `tapRETRY`, `tapFAIL_HARD`, `tapUNLIMITED`, `tapDRY_RUN`). These affect how transaction logic interprets failures and are propagated by `flags()`. +- `items_` (`detail::ApplyStateTable`) — the write buffer. Every `insert`, `update`, `erase`, `peek`, `exists`, `succ`, and `read` call is delegated here (along with `*base_` so cache misses fall through to the committed state). `items_` tracks per-key actions as one of `{cache, erase, insert, modify}` and also accumulates the `dropsDestroyed_` counter for fee accounting. + +## Read Method Routing — Two Distinct Paths + +The `.cpp` implementation reveals an important split. Metadata and iteration methods — `header()`, `fees()`, `rules()`, `open()`, `slesBegin()`, `slesEnd()`, `slesUpperBound()`, `txsBegin()`, `txsEnd()`, `txExists()`, `txRead()` — all pass straight through to `base_`. They never consult `items_` because the pending transaction cannot change the ledger's header, fee schedule, amendment rules, or already-applied transaction set. + +State lookup methods — `exists()`, `succ()`, `read()` — all route through `items_`, passing `*base_` as a fallback for cache misses. This means transaction logic transparently sees its own in-flight changes when it queries state, which is essential for correctness: an insert followed by a read of the same key must return the just-inserted value. + +The `read()` vs `peek()` distinction is similarly intentional. `read()` returns `shared_ptr` — a snapshot, no mutation tracking, no ownership transfer. `peek()` returns `shared_ptr` and notifies `items_` that this key has been checked out for potential modification; the caller must subsequently call `update()` or `erase()`, or the state table will catch the violation. + +## `rawDestroyXRP` and Fee Accounting + +`rawDestroyXRP()` is not a state mutation in the usual sense — it doesn't modify any `SLE`. Instead it increments a separate `dropsDestroyed_` counter inside `ApplyStateTable`. When the buffer is later applied to a parent `RawView`, `destroyXRP` is replayed first, before any state entries, because the total XRP supply reduction must be recorded before the modified accounts are written. + +## Object Lifetime and Move Semantics + +Copy construction and all assignment operators are deleted. Move construction is retained (`= default`). The rationale: a view is a unique owner of its in-flight mutation set during a transaction; copying it would duplicate the change journal and risk double-applying mutations. The move constructor allows subclasses to be constructed with move semantics (e.g., returned from factory functions) without enabling accidental duplication. + +The constructor `ApplyViewBase(ReadView const* base, ApplyFlags flags)` is the only way to construct the object. `base` must outlive the `ApplyViewBase`; the class makes no attempt to extend its lifetime, relying on the invariant that the underlying ledger view always outlives any transaction-level view built on top of it. + +## Concrete Subclasses + +Three concrete subclasses build on `ApplyViewBase`: + +**`ApplyViewImpl`** is the commit path for a full transaction. It adds a `deliver()` setter for payment amount metadata and an `apply(OpenView&, STTx, TER, ...)` method that delegates to `items_.apply(OpenView&, ...)` — the overload that produces `TxMeta` and threads the transaction through affected account entries. This is the only subclass that generates ledger metadata. + +**`Sandbox`** is the discard-or-commit path for speculative or nested mutations. It adds only `apply(RawView& to)`, which calls `items_.apply(to)` — the overload that replays raw writes into any `RawView` target without generating metadata. Transaction logic uses `Sandbox` when it needs to attempt a multi-step operation atomically and roll back on failure. + +**`PaymentSandbox`** overrides the hook methods declared in `ApplyView` (`creditHookIOU`, `creditHookMPT`, `issuerSelfDebitHookMPT`, `adjustOwnerCountHook`) to record deferred credit information during payment path processing. This is the only subclass that exploits the hook extension points rather than accepting their default no-op implementations. + +The placement of `ApplyViewBase` in `xrpl::detail` is a deliberate encapsulation boundary: transaction processing code works with `ApplyView` or `ApplyViewImpl` references; only the three concrete subclasses themselves, and `ApplyStateTable`, need to reach into this layer. \ No newline at end of file diff --git a/include/xrpl/ledger/detail/RawStateTable.h.ai.json b/include/xrpl/ledger/detail/RawStateTable.h.ai.json new file mode 100644 index 0000000000..18ffe82f6e --- /dev/null +++ b/include/xrpl/ledger/detail/RawStateTable.h.ai.json @@ -0,0 +1,161 @@ +{ + "args": [ + { + "lineno": 86, + "name": "action_" + }, + { + "lineno": 86, + "name": "sle_" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "RawStateTable" + }, + { + "args": [], + "lineno": 80, + "name": "sles_iter_impl" + }, + { + "args": [ + "Action action_, std::shared_ptr const& sle_" + ], + "lineno": 83, + "name": "sleAction" + } + ], + "description": "Defines the RawStateTable helper class for buffering raw ledger modifications in XRPL, using a monotonic buffer resource for efficient memory allocation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/detail/RawStateTable.h", + "functions": [ + { + "args": [], + "lineno": 19, + "name": "RawStateTable" + }, + { + "args": [ + "RawStateTable const& rhs" + ], + "lineno": 26, + "name": "RawStateTable" + }, + { + "args": [ + "RawStateTable&&" + ], + "lineno": 32, + "name": "RawStateTable" + }, + { + "args": [ + "RawStateTable&&" + ], + "lineno": 35, + "name": "operator=" + }, + { + "args": [ + "RawStateTable const&" + ], + "lineno": 37, + "name": "operator=" + }, + { + "args": [ + "RawView& to" + ], + "lineno": 40, + "name": "apply" + }, + { + "args": [ + "ReadView const& base", + "Keylet const& k" + ], + "lineno": 43, + "name": "exists" + }, + { + "args": [ + "ReadView const& base", + "key_type const& key", + "std::optional const& last" + ], + "lineno": 46, + "name": "succ" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 49, + "name": "erase" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 52, + "name": "insert" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 55, + "name": "replace" + }, + { + "args": [ + "ReadView const& base", + "Keylet const& k" + ], + "lineno": 58, + "name": "read" + }, + { + "args": [ + "XRPAmount const& fee" + ], + "lineno": 61, + "name": "destroyXRP" + }, + { + "args": [ + "ReadView const& base" + ], + "lineno": 64, + "name": "slesBegin" + }, + { + "args": [ + "ReadView const& base" + ], + "lineno": 67, + "name": "slesEnd" + }, + { + "args": [ + "ReadView const& base", + "uint256 const& key" + ], + "lineno": 70, + "name": "slesUpperBound" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/detail/RawStateTable.h.ai.md b/include/xrpl/ledger/detail/RawStateTable.h.ai.md new file mode 100644 index 0000000000..bc0444835d --- /dev/null +++ b/include/xrpl/ledger/detail/RawStateTable.h.ai.md @@ -0,0 +1,49 @@ +# `include/xrpl/ledger/detail/RawStateTable.h` + +## Role in the System + +`RawStateTable` is the low-level write buffer sitting underneath every mutable ledger view in XRPL. It solves a specific problem: when a transaction — or a batch of transactions during consensus — modifies the ledger state, those changes must be accumulated in memory and only flushed to the underlying store after the transaction succeeds. This class is that accumulator. It lives in the `xrpl::detail` namespace because it is an implementation detail of `OpenView` and similar view classes, not a public-facing API. + +The class maintains an ordered map from `uint256` ledger keys to `sleAction` records. Each record pairs an `Action` tag (`erase`, `insert`, or `replace`) with a `shared_ptr` — the serialized ledger entry being acted on. All read operations against this table overlay these pending changes on top of a `ReadView const& base`, presenting a coherent merged picture of the current state without touching the base ledger until `apply()` is called. + +## Memory Strategy: Monotonic Allocation + +The internal `items_` map uses a `boost::container::pmr::polymorphic_allocator` backed by a `boost::container::pmr::monotonic_buffer_resource`. The rationale is explicit in the source comment: this replaces an earlier `qalloc` scheme. A monotonic resource simply bumps a pointer for each allocation — it never frees individual nodes — making map insertions extremely cheap and avoiding heap fragmentation during the burst of operations that constitute a single transaction round. The initial 256 KB buffer (`initialBufferSize`) was inherited from `qalloc` and covers the typical working set without triggering growth. + +Because `monotonic_buffer_resource` cannot be shared or assigned, the copy constructor allocates a **fresh** resource and then copy-constructs `items_` from the source. The `unique_ptr` wrapper on the resource is specifically noted in the code as allowing the type to be moveable while keeping the address of the resource stable (since `items_` stores a raw pointer to it). Both assignment operators are deleted, preventing accidental copies through a different path. Move construction transfers the `unique_ptr` directly, leaving the source empty. + +## Mutation Operations and State Transitions + +`erase()`, `insert()`, and `replace()` all follow the same pattern: attempt an `emplace` into `items_`. If the emplace succeeds (no prior entry), the operation is recorded directly. If the key already exists, the code applies a state-machine transition: + +- `insert` after `erase`: converts the record to `replace`. An item that was previously erased and is now re-inserted effectively becomes a replacement from the perspective of the base view. +- `erase` after `insert`: removes the entry entirely from the map. The two operations cancel each other out — the base never needs to know. +- `erase` after `replace`: updates the action to `erase` and updates the stored SLE. +- `insert` after `insert` or `replace`, and `erase` after `erase`, call `LogicError` — these represent invariant violations that indicate a bug upstream. +- `replace` after `insert` or `replace`: simply updates the stored SLE pointer, since the action type is already correct. + +This state-machine collapse is important because it keeps the map minimal. A sequence like "insert, then replace twice, then erase" leaves no entry rather than three to flush. + +## Read Operations Against the Overlay + +`read()` checks `items_` first. If the key is absent, it falls through to `base.read(k)`. If the key is present but the action is `erase`, it returns `nullptr` — the entry has been logically deleted. Otherwise it validates the type tag via `k.check(*sle)` before returning, ensuring that a key collision of different SLE types (which should never happen but is a defensive check) returns `nullptr` rather than the wrong object. + +`exists()` follows the same priority: check the overlay, consult the base only on a miss, and return `false` for erased entries. + +`succ()` is more involved. It finds the next key after a given one by running parallel searches on both the base and the overlay. The base successor is stepped forward, skipping any base keys that appear in `items_` with `Action::erase`. The overlay is scanned forward from `upper_bound(key)` skipping erased entries. The method then returns the lower of the two candidates. This merging logic ensures the successor function reflects the fully overlaid state. + +## Merged Iteration via `sles_iter_impl` + +Iteration over all SLEs requires merging the base view's sorted SLE sequence with the overlay's sorted `items_` map. The private `sles_iter_impl` class (defined entirely in the `.cpp`) implements `ReadView::sles_type::iter_base` using a two-pointer merge. It holds `(iter0, end0)` into the base SLE sequence and `(iter1, end1)` into `items_`. On `dereference()` it returns whichever current SLE has the smaller key, with the overlay winning ties. On `increment()` it advances the pointer whose current key was just yielded; when both point to the same key, both advance together. + +A `skip()` helper handles erased entries: if `iter1` points to an `Action::erase` record whose key matches `sle0_`'s key, both pointers advance in tandem and the entry is suppressed from the output. This correctly handles the case where a base entry has been locally deleted. The `slesBegin()`, `slesEnd()`, and `slesUpperBound()` factory methods on `RawStateTable` construct `sles_iter_impl` instances with the appropriate start positions. + +## Fee Destruction + +`destroyXRP()` accumulates drops into `dropsDestroyed_` rather than recording a map entry. This tracks the total XRP burned by fees during the buffered transaction set. On `apply()`, this accumulated amount is passed to `to.rawDestroyXRP()` as a single call before the per-entry loop, keeping fee accounting separate from state-entry accounting. + +## Relationship to `OpenView` and `ApplyStateTable` + +`OpenView` embeds a `RawStateTable items_` directly. Its `rawErase`, `rawInsert`, `rawReplace`, and `rawDestroyXRP` method overrides delegate straight into the table. When `OpenView::apply()` is called to commit to a parent, it calls `items_.apply(to)`, flushing the accumulated operations through the `RawView` interface. + +`ApplyStateTable` is a sibling class (also in `xrpl::detail`) that handles a higher-level view of mutations — including a `cache` action for read-only entries and support for transaction metadata threading. `RawStateTable` handles only the raw, unconditional layer; it knows nothing about metadata or caching, which keeps it lean and focused. \ No newline at end of file diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h.ai.json b/include/xrpl/ledger/detail/ReadViewFwdRange.h.ai.json new file mode 100644 index 0000000000..687ef02b73 --- /dev/null +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h.ai.json @@ -0,0 +1,184 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "ReadViewFwdIter" + }, + { + "args": [], + "lineno": 35, + "name": "ReadViewFwdRange" + }, + { + "args": [], + "lineno": 41, + "name": "iterator" + } + ], + "description": "Defines type-erased forward iterator and range classes for iterating over ReadView objects in the xrpl ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/detail/ReadViewFwdRange.h", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "ReadViewFwdIter" + }, + { + "args": [ + "ReadViewFwdIter const&" + ], + "lineno": 16, + "name": "ReadViewFwdIter" + }, + { + "args": [ + "ReadViewFwdIter const&" + ], + "lineno": 17, + "name": "operator=" + }, + { + "args": [], + "lineno": 19, + "name": "~ReadViewFwdIter" + }, + { + "args": [], + "lineno": 21, + "name": "copy" + }, + { + "args": [ + "ReadViewFwdIter const&" + ], + "lineno": 24, + "name": "equal" + }, + { + "args": [], + "lineno": 27, + "name": "increment" + }, + { + "args": [], + "lineno": 30, + "name": "dereference" + }, + { + "args": [], + "lineno": 44, + "name": "iterator" + }, + { + "args": [ + "iterator const&" + ], + "lineno": 45, + "name": "iterator" + }, + { + "args": [ + "iterator&&" + ], + "lineno": 46, + "name": "iterator" + }, + { + "args": [ + "ReadView const*", + "std::unique_ptr" + ], + "lineno": 49, + "name": "iterator" + }, + { + "args": [ + "iterator const&" + ], + "lineno": 51, + "name": "operator=" + }, + { + "args": [ + "iterator&&" + ], + "lineno": 54, + "name": "operator=" + }, + { + "args": [ + "iterator const&" + ], + "lineno": 57, + "name": "operator==" + }, + { + "args": [ + "iterator const&" + ], + "lineno": 60, + "name": "operator!=" + }, + { + "args": [], + "lineno": 63, + "name": "operator*" + }, + { + "args": [], + "lineno": 66, + "name": "operator->" + }, + { + "args": [], + "lineno": 69, + "name": "operator++" + }, + { + "args": [ + "int" + ], + "lineno": 72, + "name": "operator++" + }, + { + "args": [], + "lineno": 84, + "name": "ReadViewFwdRange" + }, + { + "args": [ + "ReadViewFwdRange const&" + ], + "lineno": 85, + "name": "ReadViewFwdRange" + }, + { + "args": [ + "ReadViewFwdRange const&" + ], + "lineno": 86, + "name": "operator=" + }, + { + "args": [ + "ReadView const&" + ], + "lineno": 88, + "name": "ReadViewFwdRange" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.h.ai.md b/include/xrpl/ledger/detail/ReadViewFwdRange.h.ai.md new file mode 100644 index 0000000000..d226b88543 --- /dev/null +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.h.ai.md @@ -0,0 +1,33 @@ +# `include/xrpl/ledger/detail/ReadViewFwdRange.h` + +## Role in the System + +This header defines the type-erased forward-iteration infrastructure that allows any concrete `ReadView` implementation — whether a fully-validated ledger, an open in-progress ledger, a cached view, or a sandbox — to expose its state entries and transactions through a single, stable iterator type. Without this mechanism, every `ReadView` subclass would need to publish its own concrete iterator type, making it impossible to write view-agnostic code that walks the ledger state or transaction set. + +The file lives in `xrpl::detail`, signalling that it is internal plumbing. Callers interact with it indirectly through `ReadView::sles` and `ReadView::txs`. + +## The Type-Erasure Pattern + +The design follows the classic "virtual concept" or "type-erased iterator" pattern. It has two layers. + +**`ReadViewFwdIter`** is an abstract base class that defines the four primitive operations any forward iterator must support: `copy()` (polymorphic clone), `equal()` (comparison against another base), `increment()` (advance), and `dereference()` (retrieve value). Each concrete `ReadView` subclass implements this interface privately and returns instances via the factory methods `slesBegin()`, `slesEnd()`, `slesUpperBound()`, `txsBegin()`, and `txsEnd()` declared in `ReadView.h`. Those factory methods return `std::unique_ptr` — the only place where the concrete type is visible. + +**`ReadViewFwdRange::iterator`** wraps a `unique_ptr` and exposes all the standard STL forward-iterator operators. From the perspective of calling code, the iterator is a regular value type with copy, move, equality, dereference, and increment — no virtual dispatch is visible. The virtual dispatch is entirely hidden inside the `impl_` pointer. + +## Key Design Decisions + +**Why `copy()` instead of relying on `unique_ptr` copy?** `std::unique_ptr` is intentionally non-copyable because it models unique ownership. The abstract `copy()` method provides a virtual clone that deep-copies the concrete iterator state. The `iterator`'s copy constructor calls `other.impl_->copy()` to produce a new, independent polymorphic instance. This is the standard workaround for value-semantics copy of a type-erased object. + +**Cached dereference via `mutable std::optional`**: The `operator*` implementation in the `.ipp` file checks `cache_` before calling `impl_->dereference()`. Once the value is loaded it is stored and subsequent `operator*` or `operator->` calls return it cheaply. The cache is cleared in `operator++`, ensuring stale data is never returned after advancing. This matters because ledger state entries (`std::shared_ptr`) involve a heap allocation and potentially a map lookup; avoiding repeated dereferences is a meaningful optimization in tight iteration loops over large ledgers. + +**Noexcept move semantics enforced by `static_assert`**: Two `static_assert` checks inside the class definition — and one on `ValueType` itself — guarantee that move construction and move assignment of the iterator are `noexcept`. This is essential for use in standard containers and algorithms that rely on noexcept-move for efficient reallocation. Because `std::unique_ptr` move and `std::optional` move are already noexcept, and `ValueType` is constrained, the guarantee holds without extra effort. + +**`view_` pointer carried on the iterator**: Each iterator stores a `ReadView const*` alongside the `impl_`. At first glance this seems redundant, since `impl_` knows the view internally. Its purpose is visible in `operator==` in the `.ipp`: it fires an `XRPL_ASSERT` that both sides of a comparison reference the same view. Comparing iterators from different views would be undefined behaviour; the assertion catches this programming error in debug builds without any cost in release. + +## Relationship to `ReadView` + +`ReadView.h` includes this header and uses both templates directly. `ReadView::sles_type` extends `ReadViewFwdRange>` and delegates `begin()`, `end()`, and `upper_bound()` to the virtual factory methods on the owning `ReadView`. `ReadView::txs_type` does the same for `std::pair, std::shared_ptr>`. The template implementations in `ReadViewFwdRange.ipp` are included at the bottom of `ReadView.h` — after the full `ReadView` definition is available — rather than from within this header itself, the standard pattern for avoiding circular dependencies with template bodies. + +## What This Enables + +Any code that holds a `ReadView const&` can write a range-for loop over `view.sles` or `view.txs` without knowing whether the underlying object is a `Ledger`, an `OpenView`, a `CachedView`, or a `PaymentSandbox`. The type-erased iterator handles all dispatch transparently. Concrete implementations only need to provide the four primitive `ReadViewFwdIter` virtual methods and the five factory methods — a clean, minimal extension point for a class hierarchy that spans the entire ledger layer. \ No newline at end of file diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.ipp.ai.json b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp.ai.json new file mode 100644 index 0000000000..f1cbc5a443 --- /dev/null +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp.ai.json @@ -0,0 +1,406 @@ +{ + "args": [ + { + "lineno": 6, + "name": "other" + }, + { + "lineno": 12, + "name": "other" + }, + { + "lineno": 18, + "name": "view" + }, + { + "lineno": 18, + "name": "impl" + }, + { + "lineno": 25, + "name": "other" + }, + { + "lineno": 37, + "name": "other" + }, + { + "lineno": 48, + "name": "other" + }, + { + "lineno": 62, + "name": "other" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ReadViewFwdRange::iterator::operator==" + ], + "entry_point": "ReadViewFwdRange::iterator::operator==", + "purpose": "Compares two iterators for equality, ensuring they refer to the same view and position.", + "validation_points": [ + "XRPL_ASSERT(view_ == other.view_, ...)" + ] + }, + { + "call_chain": [ + "ReadViewFwdRange::iterator::operator*", + "impl_->dereference()" + ], + "entry_point": "ReadViewFwdRange::iterator::operator*", + "purpose": "Dereferences the iterator to access the value it points to, using a cache for efficiency.", + "validation_points": [ + "impl_ is assumed non-null (no explicit validation here, but dereference would crash if null)" + ] + }, + { + "call_chain": [ + "ReadViewFwdRange::iterator::operator++", + "impl_->increment()" + ], + "entry_point": "ReadViewFwdRange::iterator::operator++", + "purpose": "Advances the iterator to the next element.", + "validation_points": [ + "impl_ is assumed non-null (no explicit validation here, but increment would crash if null)" + ] + }, + { + "call_chain": [ + "ReadViewFwdRange::iterator::operator!=", + "ReadViewFwdRange::iterator::operator==" + ], + "entry_point": "ReadViewFwdRange::iterator::operator!=", + "purpose": "Checks if two iterators are not equal by negating operator==.", + "validation_points": [ + "XRPL_ASSERT in operator==" + ] + } + ], + "data_flows": [ + { + "field": "view_", + "flow": [ + "constructor parameter", + "assigned to view_", + "used in operator== for validation" + ], + "origin": "Passed into iterator constructor (ReadView const* view)", + "transformations": [ + "Copied or moved in copy/move constructors and assignment operators" + ], + "validated_at": "operator== (XRPL_ASSERT)" + }, + { + "field": "impl_", + "flow": [ + "constructor parameter", + "assigned to impl_", + "copied/moved in copy/move constructors and assignment operators", + "used in operator==, operator*, operator++, etc." + ], + "origin": "Passed into iterator constructor (std::unique_ptr impl)", + "transformations": [ + "Moved, copied (via impl_->copy()), dereferenced, incremented" + ], + "validated_at": "operator== (nullptr check), copy/assignment (nullptr check)" + }, + { + "field": "cache_", + "flow": [ + "default-initialized", + "assigned in operator* if not set (cache_ = impl_->dereference())", + "reset in operator++" + ], + "origin": "Default-initialized (likely std::optional or similar)", + "transformations": [ + "Set, moved, reset" + ], + "validated_at": "No explicit validation; usage guarded by logic" + } + ], + "description": "This file implements the methods for the iterator class nested within the ReadViewFwdRange template, providing forward iteration over a range in a type-erased manner for the XRPL ledger's ReadView.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "RAII-managed pointers (std::unique_ptr, std::optional-like cache_)", + "validation", + "missing", + "check" + ], + "evidence": "Field RAII-managed pointers (std::unique_ptr, std::optional-like cache_) validated by XRPL_ASSERT macro (custom assertion, not a validation framework)", + "issue_pattern": "Missing validation for RAII-managed pointers (std::unique_ptr, std::optional-like cache_)", + "why_false_positive": "XRPL_ASSERT macro (custom assertion, not a validation framework) validates RAII-managed pointers (std::unique_ptr, std::optional-like cache_) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "view_ (ReadView const*)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at operator== (equality operator)", + "issue_pattern": "Missing empty string validation for view_ (ReadView const*)", + "why_false_positive": "XRPL_ASSERT macro validates view_ (ReadView const*) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "impl_ (std::unique_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "nullptr check at operator== (equality operator)", + "issue_pattern": "Missing empty string validation for impl_ (std::unique_ptr)", + "why_false_positive": "nullptr check validates impl_ (std::unique_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "impl_ (std::unique_ptr)", + "type", + "validation", + "check" + ], + "evidence": "nullptr check at operator== (equality operator)", + "issue_pattern": "Missing type validation for impl_ (std::unique_ptr)", + "why_false_positive": "nullptr check validates impl_ (std::unique_ptr) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "impl_ (std::unique_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "nullptr check at operator* (dereference operator)", + "issue_pattern": "Missing empty string validation for impl_ (std::unique_ptr)", + "why_false_positive": "nullptr check validates impl_ (std::unique_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "impl_ (std::unique_ptr)", + "type", + "validation", + "check" + ], + "evidence": "nullptr check at operator* (dereference operator)", + "issue_pattern": "Missing type validation for impl_ (std::unique_ptr)", + "why_false_positive": "nullptr check validates impl_ (std::unique_ptr) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/detail/ReadViewFwdRange.ipp", + "functions": [ + { + "args": [ + "iterator const& other" + ], + "lineno": 6, + "name": "ReadViewFwdRange::iterator::iterator" + }, + { + "args": [ + "iterator&& other" + ], + "lineno": 12, + "name": "ReadViewFwdRange::iterator::iterator" + }, + { + "args": [ + "ReadView const* view", + "std::unique_ptr impl" + ], + "lineno": 18, + "name": "ReadViewFwdRange::iterator::iterator" + }, + { + "args": [ + "iterator const& other" + ], + "lineno": 25, + "name": "ReadViewFwdRange::iterator::operator=" + }, + { + "args": [ + "iterator&& other" + ], + "lineno": 37, + "name": "ReadViewFwdRange::iterator::operator=" + }, + { + "args": [ + "iterator const& other" + ], + "lineno": 48, + "name": "ReadViewFwdRange::iterator::operator==" + }, + { + "args": [ + "iterator const& other" + ], + "lineno": 62, + "name": "ReadViewFwdRange::iterator::operator!=" + }, + { + "args": [], + "lineno": 68, + "name": "ReadViewFwdRange::iterator::operator*" + }, + { + "args": [], + "lineno": 76, + "name": "ReadViewFwdRange::iterator::operator->" + }, + { + "args": [], + "lineno": 82, + "name": "ReadViewFwdRange::iterator::operator++" + }, + { + "args": [ + "int" + ], + "lineno": 89, + "name": "ReadViewFwdRange::iterator::operator++" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "detail" + } + ], + "test_coverage_notes": "This file is a template implementation and likely tested indirectly via higher-level ReadViewFwdRange or ledger iteration tests. Direct unit tests for iterator behavior (copy, move, equality, dereference, increment) should exist in test files such as 'ReadViewFwdRange_test.cpp', 'Ledger_test.cpp', or similar. However, explicit validation of XRPL_ASSERT (view_ match) may not be directly tested unless tests intentionally compare iterators from different views. Null impl_ handling is partially tested via copy/move/assignment, but dereference/increment on null impl_ may not be robustly tested. Gaps may exist in negative testing (invalid/null inputs, assertion failures).", + "validation_architecture": { + "auto_validated_fields": [ + "RAII-managed pointers (std::unique_ptr, std::optional-like cache_)" + ], + "framework": "XRPL_ASSERT macro (custom assertion, not a validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "view_ (ReadView const*)", + "location": "operator== (equality operator)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures both iterators being compared belong to the same ReadView (input view match)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (safe nullptr comparison)", + "field": "impl_ (std::unique_ptr)", + "location": "operator== (equality operator)", + "validated_by": "nullptr check", + "validates": [ + "Checks if both impl_ pointers are non-null before dereferencing" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "Undefined behavior if impl_ is nullptr (no explicit error thrown in this code)", + "field": "impl_ (std::unique_ptr)", + "location": "operator* (dereference operator)", + "validated_by": "nullptr check", + "validates": [ + "Assumes impl_ is non-null before calling dereference()" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/detail/ReadViewFwdRange.ipp.ai.md b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp.ai.md new file mode 100644 index 0000000000..e3029a0556 --- /dev/null +++ b/include/xrpl/ledger/detail/ReadViewFwdRange.ipp.ai.md @@ -0,0 +1,35 @@ +# `ReadViewFwdRange.ipp` — Type-Erased Forward Iterator Method Bodies + +This file contains the template method implementations for `ReadViewFwdRange::iterator`, the concrete iterator type whose declaration lives in `ReadViewFwdRange.h`. It is included verbatim at the bottom of `ReadView.h` via `#include `, which is the standard `.ipp` pattern for separating template bodies from declarations without creating separate translation units. + +## Why This File Exists + +`ReadView` is an abstract ledger interface with multiple concrete backing types — open ledger, closed ledger, apply views, and so on. Its two exposed range types, `sles_type` (for state ledger entries) and `txs_type` (for transactions), both inherit from `detail::ReadViewFwdRange`. The challenge is that iteration over these ranges must work uniformly across all concrete `ReadView` implementations without exposing the backing type to callers. The solution is type erasure: `ReadViewFwdRange::iterator` stores a `std::unique_ptr` (where `iter_base` is an alias for `ReadViewFwdIter`), and all iteration operations are delegated through that polymorphic pointer. This file is the stable dispatch layer that implements that delegation for every iterator operation. + +## Copy Semantics and the Virtual Clone + +Copying a `std::unique_ptr` is impossible by definition, which creates a problem: the `ForwardIterator` concept requires copyability so callers can save and restore position. The solution is the virtual `copy()` method on `ReadViewFwdIter`, which performs a deep clone of the concrete implementation. Every copy constructor and copy assignment operator in this file calls `impl_->copy()` rather than attempting to copy `impl_` directly. This is the classic virtual-clone idiom — the only correct way to duplicate a polymorphic object through a pointer-to-base without knowing the derived type at the call site. + +Move operations are straightforward: `impl_` is moved out of the source, leaving it `nullptr`. This is intentional, since a null `impl_` is the convention for end-of-range. + +Both move constructor and move assignment are `noexcept`, which the header enforces with `static_assert(std::is_nothrow_move_constructible{})`. This matters because STL algorithms and containers prefer moves over copies during relocation, and the `noexcept` guarantee enables that optimization. + +## Equality and the Same-View Invariant + +`operator==` enforces a precondition via `XRPL_ASSERT`: both iterators must originate from the same `ReadView`. Comparing iterators from different ranges is undefined behavior for any `ForwardIterator`; rather than silently producing a wrong answer, the assert surfaces this as a bug at development time. + +When both `impl_` pointers are non-null, equality is delegated to `impl_->equal()`, keeping concrete comparison logic inside the type-erased layer. The null-pointer case covers end-of-range comparisons: two null `impl_` pointers compare equal (both represent end), and null vs. non-null is not equal. This makes `nullptr` a natural, zero-overhead sentinel for the end iterator. + +## Lazy Dereference Caching + +The `cache_` member (declared as `mutable std::optional`) is populated on first access in `operator*()`. The rationale is that `impl_->dereference()` is non-trivial — for ledger entries it typically involves a shared-pointer lookup and value construction. Caching the result avoids redundant work when the same position is dereferenced multiple times, as is common in range-based for loops and algorithm passes. The `mutable` qualifier allows `operator*()` and `operator->()` to populate the cache even on a `const` iterator, which is consistent with the `const` semantics of these operators while still enabling the optimization. + +`operator++()` calls `impl_->increment()` then immediately resets the cache via `cache_.reset()`. The invalidation is correct: once the iterator advances, the cached value from the previous position is stale and must not be returned. + +## Postfix Increment and Cache Transfer + +The postfix `operator++(int)` is worth examining closely. It constructs the saved-position copy using `impl_->copy()` directly and then moves `cache_` into it with `prev.cache_ = std::move(cache_)`. This is slightly more efficient than using the copy constructor, which would call `impl_->copy()` (already accounted for) but would also copy `cache_` rather than move it. By moving the cache into `prev`, the implementation avoids copying the cached value object — particularly relevant when `ValueType` is `std::shared_ptr` or the pair type used by `txs_type`. After constructing `prev`, `++(*this)` advances the current iterator and clears its cache in the normal way. + +## Relationship to the Rest of the Ledger Layer + +The two concrete range types declared in `ReadView.h` — `sles_type` and `txs_type` — inherit from `ReadViewFwdRange>` and `ReadViewFwdRange` respectively, and they produce iterators of the type implemented here. The concrete `iter_base` subclasses provided by each `ReadView` implementation (spanning files like `ApplyViewBase` and `RawStateTable` in the same `detail/` directory) supply the `copy()`, `equal()`, `increment()`, and `dereference()` overrides that this file calls through the virtual interface. This file never needs to know which of those implementations is active at runtime. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h.ai.json b/include/xrpl/ledger/helpers/AMMHelpers.h.ai.json new file mode 100644 index 0000000000..a52bd98e75 --- /dev/null +++ b/include/xrpl/ledger/helpers/AMMHelpers.h.ai.json @@ -0,0 +1,625 @@ +{ + "args": [ + { + "lineno": 15, + "name": "amount" + }, + { + "lineno": 29, + "name": "asset1" + }, + { + "lineno": 29, + "name": "asset2" + }, + { + "lineno": 29, + "name": "lptIssue" + }, + { + "lineno": 38, + "name": "asset1Balance" + }, + { + "lineno": 38, + "name": "asset1Deposit" + }, + { + "lineno": 38, + "name": "lptAMMBalance" + }, + { + "lineno": 38, + "name": "tfee" + }, + { + "lineno": 49, + "name": "lpTokens" + }, + { + "lineno": 61, + "name": "asset1Withdraw" + }, + { + "lineno": 74, + "name": "assetBalance" + }, + { + "lineno": 87, + "name": "calcQuality" + }, + { + "lineno": 87, + "name": "reqQuality" + }, + { + "lineno": 87, + "name": "dist" + }, + { + "lineno": 102, + "name": "calc" + }, + { + "lineno": 102, + "name": "req" + }, + { + "lineno": 102, + "name": "Amt" + }, + { + "lineno": 117, + "name": "a" + }, + { + "lineno": 117, + "name": "b" + }, + { + "lineno": 117, + "name": "c" + }, + { + "lineno": 132, + "name": "pool" + }, + { + "lineno": 132, + "name": "targetQuality" + }, + { + "lineno": 132, + "name": "tfee" + }, + { + "lineno": 132, + "name": "TIn" + }, + { + "lineno": 132, + "name": "TOut" + }, + { + "lineno": 249, + "name": "quality" + }, + { + "lineno": 249, + "name": "rules" + }, + { + "lineno": 249, + "name": "j" + }, + { + "lineno": 340, + "name": "assetIn" + }, + { + "lineno": 387, + "name": "assetOut" + }, + { + "lineno": 434, + "name": "n" + }, + { + "lineno": 445, + "name": "isDeposit" + }, + { + "lineno": 457, + "name": "amountBalance" + }, + { + "lineno": 457, + "name": "amount" + }, + { + "lineno": 457, + "name": "amount2" + }, + { + "lineno": 476, + "name": "frac" + }, + { + "lineno": 476, + "name": "rm" + }, + { + "lineno": 511, + "name": "noRoundCb" + }, + { + "lineno": 496, + "name": "balance" + }, + { + "lineno": 511, + "name": "productCb" + }, + { + "lineno": 527, + "name": "tokens" + }, + { + "lineno": 527, + "name": "lptAMMBalance" + }, + { + "lineno": 576, + "name": "view" + }, + { + "lineno": 576, + "name": "ammAccountID" + }, + { + "lineno": 576, + "name": "freezeHandling" + }, + { + "lineno": 576, + "name": "authHandling" + }, + { + "lineno": 587, + "name": "optAsset1" + }, + { + "lineno": 587, + "name": "optAsset2" + }, + { + "lineno": 587, + "name": "ammSle" + }, + { + "lineno": 597, + "name": "asset1" + }, + { + "lineno": 597, + "name": "asset2" + }, + { + "lineno": 597, + "name": "ammAccount" + }, + { + "lineno": 597, + "name": "lpAccount" + }, + { + "lineno": 610, + "name": "account" + }, + { + "lineno": 616, + "name": "asset" + }, + { + "lineno": 646, + "name": "sb" + }, + { + "lineno": 637, + "name": "ammIssue" + } + ], + "classes": [], + "description": "This file provides functions and utilities for Automated Market Maker (AMM) operations in the XRPL (XRP Ledger) codebase, including calculations for liquidity pool tokens, asset swaps, rounding, fee handling, and AMM pool state management.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/AMMHelpers.h", + "functions": [ + { + "args": [ + "amount" + ], + "lineno": 15, + "name": "reduceOffer" + }, + { + "args": [ + "asset1", + "asset2", + "lptIssue" + ], + "lineno": 29, + "name": "ammLPTokens" + }, + { + "args": [ + "asset1Balance", + "asset1Deposit", + "lptAMMBalance", + "tfee" + ], + "lineno": 38, + "name": "lpTokensOut" + }, + { + "args": [ + "asset1Balance", + "lptAMMBalance", + "lpTokens", + "tfee" + ], + "lineno": 49, + "name": "ammAssetIn" + }, + { + "args": [ + "asset1Balance", + "asset1Withdraw", + "lptAMMBalance", + "tfee" + ], + "lineno": 61, + "name": "lpTokensIn" + }, + { + "args": [ + "assetBalance", + "lptAMMBalance", + "lpTokens", + "tfee" + ], + "lineno": 74, + "name": "ammAssetOut" + }, + { + "args": [ + "calcQuality", + "reqQuality", + "dist" + ], + "lineno": 87, + "name": "withinRelativeDistance" + }, + { + "args": [ + "calc", + "req", + "dist" + ], + "lineno": 102, + "name": "withinRelativeDistance" + }, + { + "args": [ + "a", + "b", + "c" + ], + "lineno": 117, + "name": "solveQuadraticEqSmallest" + }, + { + "args": [ + "pool", + "targetQuality", + "tfee" + ], + "lineno": 132, + "name": "getAMMOfferStartWithTakerGets" + }, + { + "args": [ + "pool", + "targetQuality", + "tfee" + ], + "lineno": 191, + "name": "getAMMOfferStartWithTakerPays" + }, + { + "args": [ + "pool", + "quality", + "tfee", + "rules", + "j" + ], + "lineno": 249, + "name": "changeSpotPriceQuality" + }, + { + "args": [ + "pool", + "assetIn", + "tfee" + ], + "lineno": 340, + "name": "swapAssetIn" + }, + { + "args": [ + "pool", + "assetOut", + "tfee" + ], + "lineno": 387, + "name": "swapAssetOut" + }, + { + "args": [ + "n" + ], + "lineno": 434, + "name": "square" + }, + { + "args": [ + "lptAMMBalance", + "lpTokens", + "isDeposit" + ], + "lineno": 445, + "name": "adjustLPTokens" + }, + { + "args": [ + "amountBalance", + "amount", + "amount2", + "lptAMMBalance", + "lpTokens", + "tfee", + "isDeposit" + ], + "lineno": 457, + "name": "adjustAmountsByLPTokens" + }, + { + "args": [ + "a", + "b", + "c" + ], + "lineno": 474, + "name": "solveQuadraticEq" + }, + { + "args": [ + "amount", + "frac", + "rm" + ], + "lineno": 476, + "name": "multiply" + }, + { + "args": [ + "isDeposit" + ], + "lineno": 481, + "name": "getLPTokenRounding" + }, + { + "args": [ + "isDeposit" + ], + "lineno": 488, + "name": "getAssetRounding" + }, + { + "args": [ + "rules", + "balance", + "frac", + "isDeposit" + ], + "lineno": 496, + "name": "getRoundedAsset" + }, + { + "args": [ + "rules", + "noRoundCb", + "balance", + "productCb", + "isDeposit" + ], + "lineno": 511, + "name": "getRoundedAsset" + }, + { + "args": [ + "rules", + "balance", + "frac", + "isDeposit" + ], + "lineno": 527, + "name": "getRoundedLPTokens" + }, + { + "args": [ + "rules", + "noRoundCb", + "lptAMMBalance", + "productCb", + "isDeposit" + ], + "lineno": 537, + "name": "getRoundedLPTokens" + }, + { + "args": [ + "rules", + "balance", + "amount", + "lptAMMBalance", + "tokens", + "tfee" + ], + "lineno": 553, + "name": "adjustAssetInByTokens" + }, + { + "args": [ + "rules", + "balance", + "amount", + "lptAMMBalance", + "tokens", + "tfee" + ], + "lineno": 560, + "name": "adjustAssetOutByTokens" + }, + { + "args": [ + "rules", + "lptAMMBalance", + "tokens", + "frac" + ], + "lineno": 569, + "name": "adjustFracByTokens" + }, + { + "args": [ + "view", + "ammAccountID", + "asset1", + "asset2", + "freezeHandling", + "authHandling", + "j" + ], + "lineno": 576, + "name": "ammPoolHolds" + }, + { + "args": [ + "view", + "ammSle", + "optAsset1", + "optAsset2", + "freezeHandling", + "authHandling", + "j" + ], + "lineno": 587, + "name": "ammHolds" + }, + { + "args": [ + "view", + "asset1", + "asset2", + "ammAccount", + "lpAccount", + "j" + ], + "lineno": 597, + "name": "ammLPHolds" + }, + { + "args": [ + "view", + "ammSle", + "lpAccount", + "j" + ], + "lineno": 604, + "name": "ammLPHolds" + }, + { + "args": [ + "view", + "ammSle", + "account" + ], + "lineno": 610, + "name": "getTradingFee" + }, + { + "args": [ + "view", + "ammAccountID", + "asset" + ], + "lineno": 616, + "name": "ammAccountHolds" + }, + { + "args": [ + "view", + "asset", + "asset2", + "j" + ], + "lineno": 622, + "name": "deleteAMMAccount" + }, + { + "args": [ + "view", + "ammSle", + "account", + "lptAsset", + "tfee" + ], + "lineno": 629, + "name": "initializeFeeAuctionVote" + }, + { + "args": [ + "view", + "ammIssue", + "lpAccount" + ], + "lineno": 637, + "name": "isOnlyLiquidityProvider" + }, + { + "args": [ + "sb", + "lpTokens", + "ammSle", + "account" + ], + "lineno": 646, + "name": "verifyAndAdjustLPTokenBalance" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + }, + { + "lineno": 14, + "name": "detail" + }, + { + "lineno": 480, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/AMMHelpers.h.ai.md b/include/xrpl/ledger/helpers/AMMHelpers.h.ai.md new file mode 100644 index 0000000000..b654d0f7fa --- /dev/null +++ b/include/xrpl/ledger/helpers/AMMHelpers.h.ai.md @@ -0,0 +1,61 @@ +# `include/xrpl/ledger/helpers/AMMHelpers.h` + +This header is the mathematical and operational backbone of XRPL's Automated Market Maker (AMM) implementation. It provides every computation needed to run a constant-product AMM pool — from LP token minting and burning, to spot-price alignment against the central limit order book (CLOB), to swap execution with rigorous rounding guarantees. The functions here are consumed by `AMMLiquidity`, `AMMDeposit`, `AMMWithdraw`, and `AMMBid` transactors, as well as by the payment-path engine. + +## Pool Invariant and the Rounding Contract + +Every function in this file is written around one non-negotiable invariant: + +``` +sqrt(poolAsset1 × poolAsset2) >= LPTokensBalance +``` + +XRPL's `STAmount` type stores only 16 significant decimal digits, which means every multiply-and-convert step introduces a small ULP-level error. Violating the invariant — even by one drop — is a ledger inconsistency. The entire rounding strategy flows from this constraint: + +- **Swap-in** (`swapAssetIn`): rounds the output received by the trader **downward**, so the pool retains a tiny excess and the product stays large. +- **Swap-out** (`swapAssetOut`): rounds the input required from the trader **upward**, so again the pool collects slightly more than strictly necessary. +- **LP token issuance**: rounds tokens **downward** on deposit (the pool is worth slightly more per token) and the corresponding asset amounts **upward** (the depositor puts in slightly more). +- **LP token burning**: rounds tokens **upward** on withdrawal (the pool gives up as little as possible) and asset amounts **downward**. + +The `fixAMMv1_1` amendment made this rounding more granular. Before the amendment, `swapAssetIn` computed `pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee))` in a single expression with a single round-down at the end. After the amendment, the code explicitly sets `Number::upward` for numerator products and intermediate ratios that should be maximized (to minimize what is given out), and `Number::downward` for the denominator (to maximize the ratio) — each step individually guided. The pre-amendment code path is preserved for historic ledger replay. + +The `fixAMMv1_3` amendment extends this discipline to LP token and deposit/withdrawal formulas, replacing `toSTAmount(…, raw_value)` calls with `multiply(balance, frac, rounding_mode)`, ensuring the final multiplication — the step with the most numerical influence — is explicitly directed. + +## LP Token Deposit and Withdrawal Formulas + +`lpTokensOut`, `ammAssetIn`, `lpTokensIn`, and `ammAssetOut` implement the four quadrant operations: given an asset deposit amount find the tokens earned, given a token amount find the asset deposit required, given an asset withdrawal find the tokens to burn, and given a token burn find the asset returned. These are the XLS-30d AMM standard equations (labelled 3, 4, 7, 8 in the implementation comments). All four involve fees because a single-sided deposit is economically equivalent to a proportional deposit plus a fee-bearing swap; the fee is embedded in the formula through `feeMult(tfee)` and `feeMultHalf(tfee)` from `AMMCore.h`. + +`ammLPTokens` is the initial pool seeding formula: `sqrt(asset1 × asset2)`, which sets the geometric mean of pool reserves as the starting LP token supply. This is the standard Uniswap v2 approach and maintains the invariant at equality at creation. + +## LP Token Precision Adjustment + +`adjustLPTokens` addresses a subtle issue: adding newly-minted tokens to an already-large `LPTokensBalance` field loses significance in the least-significant digit. The workaround is to compute the difference as `(balance + tokens) - balance` rather than just `tokens`. This round-trips through the 16-digit representation and returns the value that will actually be committed to the ledger, which may be slightly less than the calculated tokens. The `adjustAmountsByLPTokens` layer then adjusts the corresponding asset amounts downward to match, preventing the ledger from granting assets that exceed what the LP token math supports. + +`getRoundedAsset` and `getRoundedLPTokens` are the amendment-gated wrappers for equal (two-sided) deposit/withdrawal rounding, each available in two overloads: a simple `(balance, frac, isDeposit)` form for direct use, and a callback-based `(noRoundCb, balance, productCb, isDeposit)` form that delays evaluation until inside the function, avoiding recomputing expensive intermediate values. The callbacks exist because the old path (`!fixAMMv1_3`) needs the same formula without controlled rounding, and factoring this out cleanly required deferring the computation. + +## Spot Price Quality Alignment + +The central challenge in AMM/CLOB co-execution is `changeSpotPriceQuality`. When the payment engine encounters both AMM pools and order book offers for the same currency pair, it asks the AMM to generate a synthetic offer whose quality exactly matches the best CLOB quality. This forces both pools to compete at the same marginal price rather than letting one undercut the other. + +The problem is: given current pool reserves `(I, O)` and a target quality `Qt`, find `(i, o)` — the taker-pays and taker-gets — such that the AMM's spot price after the swap equals `Qt` **or** the swap's effective price equals `Qt`, whichever is smaller. These are two different binding constraints: + +- **Scenario A** (post-swap spot price = Qt): substituting the swap equation into the spot price condition yields a quadratic in either `i` or `o`. +- **Scenario B** (effective offer price = Qt): the swap equation gives a closed-form linear constraint. + +Both constraints are solved and the smaller result is taken to maximize offer quality. `getAMMOfferStartWithTakerGets` is used when the pool pays XRP (IOU-in / XRP-out), while `getAMMOfferStartWithTakerPays` is used for XRP-in/IOU-out and IOU/IOU pools. The `fixAMMv1_1` amendment switched from always starting with `takerPays` to always starting with the XRP side. The reason is that XRP amounts are rounded to integer drops, so rounding the XRP side down has the largest discrete effect on quality. Computing XRP first and then deriving the IOU amount from `swapAssetIn`/`swapAssetOut` ensures the resulting offer quality stays at or above the target rather than falling one drop below it. + +The `detail::reduceOffer` helper applies a 99.99% multiplier (rounding toward zero) as a last-resort quality rescue: if the rounded offer still comes out below `targetQuality` due to XRP discretization, reducing the offer by 0.01% brings it back above the target without generating an implausibly small trade. + +## Proximity Checks and Tolerance + +`withinRelativeDistance` has two overloads: one for `Quality` objects and one for generic numeric types. The `Quality` version cannot use subtraction directly because `Quality` has no arithmetic operators — instead it uses `Quality::rate()`, which is the *inverse* of quality (output/input), converting the "is quality within X% of target?" question into a comparison of rates. The formula compensates for the inversion: `(min.rate - max.rate) / min.rate < dist`. The generic version works straightforwardly. These are used in `changeSpotPriceQuality` to emit a trace-level error only when the quality mismatch exceeds one part in ten million, preventing excessive log noise from harmless floating-point residuals. + +## Ledger State Queries + +`ammPoolHolds` and `ammHolds` read the AMM's actual trust line balances from a `ReadView`, supporting both frozen-account and authorization checks via `FreezeHandling` and `AuthHandling` flags. `ammHolds` returns an `Expected` — a success-or-error type from `xrpl/basics/Expected.h` — because reading the pool state can fail if the AMM's SLE is malformed. `getTradingFee` applies the auction-slot discount: the slot owner and up to four authorized accounts trade at `TRADING_FEE_THRESHOLD / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION` of the normal fee. + +## AMM Lifecycle + +`deleteAMMAccount` removes all trust lines held by the AMM account and, once the last trust line is gone, deletes the AMM SLE and its on-ledger account. Because each ledger transaction has a bounded work budget, not all trust lines may fit in one transaction; in that case `tecINCOMPLETE` is returned and the caller must submit additional transactions to finish deletion. + +`isOnlyLiquidityProvider` detects the single-LP edge case: when a sole LP withdraws, there should be exactly one LP token trust line and no other LPs. `verifyAndAdjustLPTokenBalance` handles the resulting rounding drift — the AMM's recorded `LPTokenBalance` may not equal the last LP's trust line balance after accumulated rounding, so the function corrects the ledger field within a tolerance, enabling the final withdrawal to succeed cleanly. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h.ai.json b/include/xrpl/ledger/helpers/AccountRootHelpers.h.ai.json new file mode 100644 index 0000000000..874be3756a --- /dev/null +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h.ai.json @@ -0,0 +1,155 @@ +{ + "args": [ + { + "lineno": 17, + "name": "issuer" + }, + { + "lineno": 16, + "name": "view" + }, + { + "lineno": 26, + "name": "id" + }, + { + "lineno": 26, + "name": "ownerCountAdj" + }, + { + "lineno": 26, + "name": "j" + }, + { + "lineno": 37, + "name": "sle" + }, + { + "lineno": 37, + "name": "amount" + }, + { + "lineno": 52, + "name": "pseudoOwnerKey" + }, + { + "lineno": 70, + "name": "sleAcct" + }, + { + "lineno": 70, + "name": "pseudoFieldFilter" + }, + { + "lineno": 80, + "name": "accountId" + }, + { + "lineno": 92, + "name": "ownerField" + }, + { + "lineno": 104, + "name": "toSle" + }, + { + "lineno": 104, + "name": "hasDestinationTag" + } + ], + "classes": [], + "description": "This file provides helper functions for working with accounts and pseudo-accounts in the XRPL ledger, including checking global freeze status, calculating liquid XRP, adjusting owner counts, handling transfer rates, and managing pseudo-accounts.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/AccountRootHelpers.h", + "functions": [ + { + "args": [ + "view", + "issuer" + ], + "lineno": 16, + "name": "isGlobalFrozen" + }, + { + "args": [ + "view", + "id", + "ownerCountAdj", + "j" + ], + "lineno": 25, + "name": "xrpLiquid" + }, + { + "args": [ + "view", + "sle", + "amount", + "j" + ], + "lineno": 36, + "name": "adjustOwnerCount" + }, + { + "args": [ + "view", + "issuer" + ], + "lineno": 43, + "name": "transferRate" + }, + { + "args": [ + "view", + "pseudoOwnerKey" + ], + "lineno": 51, + "name": "pseudoAccountAddress" + }, + { + "args": [], + "lineno": 59, + "name": "getPseudoAccountFields" + }, + { + "args": [ + "sleAcct", + "pseudoFieldFilter" + ], + "lineno": 69, + "name": "isPseudoAccount" + }, + { + "args": [ + "view", + "accountId", + "pseudoFieldFilter" + ], + "lineno": 78, + "name": "isPseudoAccount" + }, + { + "args": [ + "view", + "pseudoOwnerKey", + "ownerField" + ], + "lineno": 91, + "name": "createPseudoAccount" + }, + { + "args": [ + "toSle", + "hasDestinationTag" + ], + "lineno": 102, + "name": "checkDestinationAndTag" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/AccountRootHelpers.h.ai.md b/include/xrpl/ledger/helpers/AccountRootHelpers.h.ai.md new file mode 100644 index 0000000000..a73b9af38a --- /dev/null +++ b/include/xrpl/ledger/helpers/AccountRootHelpers.h.ai.md @@ -0,0 +1,35 @@ +# `include/xrpl/ledger/helpers/AccountRootHelpers.h` + +This header is the canonical source of free functions for querying and mutating `ltACCOUNT_ROOT` ledger entries (`SLE`s). It sits in the `ledger/helpers` module alongside analogous headers for AMMs, trust lines, NFTs, and other ledger-object types, providing a shared utility layer that transaction processors, payment engines, and invariant checkers all pull from. + +## Token Economics Helpers + +`isGlobalFrozen()` checks whether an IOU issuer has activated the global freeze flag (`lsfGlobalFreeze`). It short-circuits immediately for XRP — XRP is never frozen. This guard appears throughout the payment paths to prevent token movement when an issuer is in a frozen state. + +`transferRate()` returns the IOU transfer fee for an issuer as a `Rate` value — a fraction expressed in units of one billion (so 1% = 1,010,000,000). If the issuer account doesn't exist or hasn't set `sfTransferRate`, it falls back to `parityRate` (no fee), ensuring callers never have to handle a null case. + +## Owner Count and Reserve Accounting + +`adjustOwnerCount()` increments or decrements `sfOwnerCount` on an account SLE and notifies the view via `adjustOwnerCountHook()`, which lets sandboxed views (like `PaymentSandbox`) track in-flight count changes. Internally it delegates to the file-static `confineOwnerCount()`, which prevents both unsigned underflow and overflow with clamping to 0 and `UINT32_MAX` respectively, logging at `fatal` severity when clamping occurs on a real account. That defensive clamping is deliberate — the field is `uint32_t`, so arithmetic wraps silently without it. + +`xrpLiquid()` computes how much XRP an account can freely spend: balance minus reserve. The `ownerCountAdj` parameter is the key design decision here — callers often need to know available balance *before* or *after* they add/remove ledger entries that would change the reserve, so they pass a signed delta rather than relying on a state that isn't yet committed to the view. The function also calls `view.ownerCountHook()` and `view.balanceHookIOU()`, which are virtual hooks allowing the `PaymentSandbox` to intercept and adjust values reflecting uncommitted in-flight changes during multi-step payment processing. Crucially, pseudo-accounts bypass reserve requirements entirely (`XRPAmount{0}`), since they cannot submit transactions and should never be blocked by a reserve check. + +## Pseudo-Account Subsystem + +The most architecturally novel feature in this file is the pseudo-account mechanism — a way for first-class ledger objects (AMMs, Vaults, LoanBrokers) to own an `ltACCOUNT_ROOT` SLE that has no private key and is not controlled by any user. + +**Field discovery via `getPseudoAccountFields()`** returns a singleton `const std::vector`. The list is built once at startup by iterating the `ltACCOUNT_ROOT` format's `SOTemplate` and selecting any field that has the `SField::sMD_PseudoAccount` metadata bit set (bit 0x40). Currently that set is `sfAMMID`, `sfVaultID`, and `sfLoanBrokerID`, each defined with `SField::sMD_PseudoAccount | SField::sMD_Default` in `sfields.macro`. This design means adding a new pseudo-account type requires nothing more than tagging its key field with `sMD_PseudoAccount` — the discovery logic is fully data-driven, with no manual registration. + +**`isPseudoAccount()`** has two overloads. The primary overload takes an SLE directly and checks three things defensively: the pointer is non-null, the SLE's type is `ltACCOUNT_ROOT`, and at least one pseudo-account field is present. The optional `pseudoFieldFilter` set narrows the check to specific field(s), allowing callers to distinguish AMM pseudo-accounts from Vault pseudo-accounts. The inline convenience overload reads the account from a `ReadView` via `keylet::account()` and delegates. + +**`pseudoAccountAddress()`** derives a new `AccountID` using a loop of up to 256 tries. Each attempt hashes a counter, the parent ledger's hash, and the owner key through SHA-512 half-digest followed by a `ripesha_hasher` (RIPEMD-160(SHA-256(...))). The loop skips any address that already exists in the view. The 256-attempt cap is consensus-critical — the comment explicitly notes it cannot change without an amendment. In practice, collisions are astronomically unlikely; the loop is purely defensive. + +**`createPseudoAccount()`** returns `Expected, TER>`, the XRPL error-or-value pattern. It performs a debug assertion (`XRPL_ASSERT`) that the provided `ownerField` is registered as a pseudo-account field, providing a loud diagnostic instead of silent corruption. The created SLE gets `sfSequence = 0` (when `featureSingleAssetVault` or `featureLendingProtocol` is active), making it visually distinct from normal accounts and providing an extra barrier against accidental transaction submission. Flags are set to `lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth`: the master key is disabled so no private key can sign, default rippling is on so the pseudo-account can hold and transfer IOUs through trust lines, and deposit authorization blocks direct payments from external accounts. The comment in `createPseudoAccount` makes an important point: **amendment checks are the caller's responsibility** — this function is amendment-neutral by design, and callers like `VaultCreate` and `LoanBrokerSet` perform the relevant `view.rules().enabled(...)` checks before invoking it. + +## Destination Validation + +`checkDestinationAndTag()` is a small but widely reused guard: it returns `tecNO_DST` for a null SLE and `tecDST_TAG_NEEDED` if the destination account requires a tag (`lsfRequireDestTag`) but none was provided. Centralizing this check avoids duplicated logic across the many transaction types that send to an account destination. + +## Relationships + +The implementation file (`AccountRootHelpers.cpp`) is a direct dependency of AMM, Vault, and LoanBroker transactors. The `xrpLiquid()` and `adjustOwnerCount()` functions are called across nearly all transaction processors. The hook-based design for balance and owner-count queries (`balanceHookIOU`, `ownerCountHook`, `adjustOwnerCountHook`) allows `PaymentSandbox` to overlay uncommitted changes during complex multi-step payment flows without the helpers needing to know about sandboxing internals. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h.ai.json b/include/xrpl/ledger/helpers/CredentialHelpers.h.ai.json new file mode 100644 index 0000000000..7aabd77b0e --- /dev/null +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h.ai.json @@ -0,0 +1,119 @@ +{ + "args": [], + "classes": [], + "description": "This file provides helper functions for managing and validating credential objects (such as DepositPreauth and Credentials) in the XRPL ledger, including expiration checks, deletion, sorting, and authorization validation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/CredentialHelpers.h", + "functions": [ + { + "args": [ + "sleCredential", + "closed" + ], + "lineno": 15, + "name": "checkExpired" + }, + { + "args": [ + "view", + "arr", + "j" + ], + "lineno": 18, + "name": "removeExpired" + }, + { + "args": [ + "view", + "sleCredential", + "j" + ], + "lineno": 21, + "name": "deleteSLE" + }, + { + "args": [ + "tx", + "j" + ], + "lineno": 24, + "name": "checkFields" + }, + { + "args": [ + "tx", + "view", + "src", + "j" + ], + "lineno": 29, + "name": "valid" + }, + { + "args": [ + "view", + "domainID", + "subject" + ], + "lineno": 35, + "name": "validDomain" + }, + { + "args": [ + "view", + "ctx", + "dst" + ], + "lineno": 41, + "name": "authorizedDepositPreauth" + }, + { + "args": [ + "credentials" + ], + "lineno": 44, + "name": "makeSorted" + }, + { + "args": [ + "credentials", + "maxSize", + "j" + ], + "lineno": 47, + "name": "checkArray" + }, + { + "args": [ + "view", + "account", + "domainID", + "j" + ], + "lineno": 52, + "name": "verifyValidDomain" + }, + { + "args": [ + "tx", + "view", + "src", + "dst", + "sleDst", + "j" + ], + "lineno": 55, + "name": "verifyDepositPreauth" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "credentials" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/CredentialHelpers.h.ai.md b/include/xrpl/ledger/helpers/CredentialHelpers.h.ai.md new file mode 100644 index 0000000000..7395c269a3 --- /dev/null +++ b/include/xrpl/ledger/helpers/CredentialHelpers.h.ai.md @@ -0,0 +1,54 @@ +# `include/xrpl/ledger/helpers/CredentialHelpers.h` + +This header is the central contract for credential and deposit pre-authorization logic throughout the XRPL ledger. It lives alongside a family of per-feature helpers (`AMMHelpers.h`, `EscrowHelpers.h`, etc.) and is included by every fund-transfer transactor — `Payment`, `EscrowFinish`, `PaymentChannelClaim`, `VaultDeposit` — wherever those transactions must honor destination account access controls. + +## The Two-Phase Design Constraint + +The most important architectural fact about this file is that its functions divide cleanly along the `preclaim` / `doApply` transaction-processing boundary, and this division is non-negotiable. + +During `preclaim`, the ledger view is read-only (`ReadView`). Validators may execute preclaim in parallel, so no mutations are allowed. Expiration checks can happen here — you can read an `sfExpiration` field — but you cannot delete the expired SLE from the ledger. + +During `doApply`, the view is mutable (`ApplyView`). This is the only phase where expired credential objects can actually be removed, owner directories adjusted, and owner counts decremented. + +The header is explicit about this split in its inline comments. `credentials::valid()` checks existence, subject ownership, and the `lsfAccepted` flag, but deliberately defers expiration: "Expiration checks are in doApply." If you call `valid()` in preclaim and it passes, you must also call `verifyDepositPreauth()` in doApply to handle the deletion of any credential that has since expired. The same pattern applies to the domain pathway: `credentials::validDomain()` returns `tecEXPIRED` if it finds an expired credential but cannot remove it; `verifyValidDomain()` (outer namespace, takes `ApplyView`) is the doApply counterpart that actually prunes them. + +This split is why the two "verify" functions live in the outer `xrpl::` namespace while the lower-level checks live in `xrpl::credentials::`. The naming convention signals which layer operates on mutable state. + +## Credential Lifecycle: Validation and Deletion + +`checkExpired()` is the primitive: it reads the optional `sfExpiration` field from a credential SLE (defaulting to `uint32_t::max` if absent) and compares against the ledger's `parentCloseTime`. This is used by both `removeExpired()` and `validDomain()`. + +`deleteSLE()` is the most intricate function. A credential SLE belongs to two owner directories — one for the issuer, one for the subject — and the reserve-count ownership model depends on whether the credential has been accepted: + +- If the credential is **not yet accepted** (subject hasn't acknowledged it), only the issuer holds the reserve. Deleting decrements the issuer's owner count but not the subject's. +- If **accepted** and issuer ≠ subject, ownership transfers to the subject; the subject's count is decremented on deletion. +- If issuer == subject (self-issued credentials), the accepted flag is irrelevant for the issuer-side delete — the owner count adjustment uses `!accepted || (subject == issuer)` to capture both cases. + +The function removes the SLE from both owner directories (using `dirRemove`) and then erases it from the ledger. Internal errors that would indicate a corrupted ledger state are marked `LCOV_EXCL` because they are unreachable in a correctly functioning node. + +`removeExpired()` iterates a `STVector256` of credential IDs, peeks each via `ApplyView`, and calls `deleteSLE()` for any that are expired. It returns `true` if any were found, which the callers use to short-circuit with `tecEXPIRED` — deleting the credentials as a side effect even when the enclosing transaction ultimately fails. + +## Input Validation: Two Different Credential Array Shapes + +Two separate validation functions exist because credentials appear in two structurally different forms across the protocol: + +`checkFields()` validates `sfCredentialIDs` — a `STVector256` of opaque 256-bit hashes that directly reference existing credential SLEs. This is the form used in fund-transfer transactions (Payment, etc.). It enforces non-empty, bounded size, and uniqueness via an `unordered_set`. + +`checkArray()` validates the `STArray` form used in `DepositPreauth` and `PermissionedDomainSet` transactions, where credentials are declared as `(issuer, credentialType)` pairs rather than pre-computed hashes. Duplicate detection here hashes each pair with `sha512Half(issuer, credentialType)` to produce a canonical key — the same digest the ledger uses to derive the object key — ensuring logical duplicates are caught even if the raw bytes differ. + +## Authorization Check: DepositPreauth with Credentials + +`verifyDepositPreauth()` implements the full gate for deposit-authorization enforcement. It handles two pre-authorization modes: + +1. **Account-level**: a simple `keylet::depositPreauth(dst, src)` lookup — the destination has explicitly preauthorized the source account. +2. **Credential-based**: if account-level authorization fails and the transaction carries `sfCredentialIDs`, it delegates to `authorizedDepositPreauth()`, which checks whether the destination has preauthorized the specific credential set the source is presenting. + +The credential-based path in `authorizedDepositPreauth()` reconstructs a sorted `std::set>` from the credential SLEs. This sorted set is the canonical form used as the key for a `DepositPreauth` ledger object that was created against a credential specification. A subtle memory-safety detail: `Slice` is a non-owning pointer into the SLE's internal buffer, so the function maintains a `lifeExtender` vector of `shared_ptr` alongside the set to keep those SLEs alive for the duration of the `view.exists()` lookup. + +## Domain-Credential Pathway + +`validDomain()` (preclaim-safe) and `verifyValidDomain()` (doApply) cover the `PermissionedDomain` access model, which differs from `DepositPreauth`. Here the domain object itself carries an `sfAcceptedCredentials` array of `(issuer, credentialType)` specs. The check asks: does the subject hold an accepted, non-expired credential matching any entry in that array? + +`validDomain()` returns `tecEXPIRED` rather than `tecNO_AUTH` if at least one matching credential was found but expired — a signal to the caller that the condition might resolve (via deletion) in doApply. `verifyValidDomain()` then collects all matching credential keys, runs `removeExpired()` on them, and re-reads the survivors to confirm one is accepted. + +`makeSorted()` is the utility behind credential array deduplication in transaction setup: given a credential array, it builds the same sorted-pair set used by the authorization lookups, returning an empty set if any duplicates are encountered. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/DelegateHelpers.h.ai.json b/include/xrpl/ledger/helpers/DelegateHelpers.h.ai.json new file mode 100644 index 0000000000..802bac659d --- /dev/null +++ b/include/xrpl/ledger/helpers/DelegateHelpers.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [], + "classes": [], + "description": "Provides helper functions to check and load delegate account permissions for executing transactions in the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/DelegateHelpers.h", + "functions": [ + { + "args": [ + "delegate", + "tx" + ], + "lineno": 13, + "name": "checkTxPermission" + }, + { + "args": [ + "delegate", + "type", + "granularPermissions" + ], + "lineno": 23, + "name": "loadGranularPermission" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/DelegateHelpers.h.ai.md b/include/xrpl/ledger/helpers/DelegateHelpers.h.ai.md new file mode 100644 index 0000000000..2e7acb15cc --- /dev/null +++ b/include/xrpl/ledger/helpers/DelegateHelpers.h.ai.md @@ -0,0 +1,35 @@ +## `include/xrpl/ledger/helpers/DelegateHelpers.h` + +This header is part of the XRPL delegate account system, which allows one account to authorize another — a "delegate" — to submit certain transactions on its behalf with explicitly bounded permissions. The file declares the two functions that sit at the enforcement layer of that system: determining whether a transaction type is permitted at all, and loading the fine-grained permission flags that constrain what the delegate can do within that transaction type. + +### Context and motivation + +The delegate model solves a practical custody and automation problem. An account holder may want a hot wallet or automated agent to submit routine transactions without granting it full signing authority. Rather than relying solely on multisig or regular key mechanisms, XRPL's delegate system lets an account enumerate exactly which transaction types — and even which field-level sub-operations within those types — the delegate may exercise. `DelegateHelpers.h` provides the runtime lookup functions that individual transaction appliers invoke during permission validation. + +The underlying ledger state lives in a `Delegate` object (type `ltDELEGATE`), which records the authorizing account (`sfAccount`), the delegate account (`sfAuthorize`), and an `sfPermissions` array. Each element of that array holds an `sfPermissionValue`: an integer that encodes either a transaction-type permission (the `TxType` value plus one, by convention) or a granular sub-operation flag (a `GranularPermissionType` value, defined to exceed the maximum `uint16` to avoid collisions with transaction types). + +### `checkTxPermission` + +`checkTxPermission` answers the binary question: is this transaction type permitted by the delegate relationship at all? It receives the `Delegate` ledger state entry as a `shared_ptr` — a null pointer returns `terNO_DELEGATE_PERMISSION` immediately, acting as a guard against a missing ledger object. It then reads `sfPermissions` from the SLE and linearly scans for an entry whose `sfPermissionValue` equals `tx.getTxnType() + 1`, the transaction-type permission encoding. + +The return type is `NotTEC`, the XRPL error-code subset that excludes `TEC` (claimed fee) codes but includes both success codes and `ter` (retryable) codes. The meaningful outcomes are `tesSUCCESS` and `terNO_DELEGATE_PERMISSION`. Choosing a `ter` code rather than a `tef` (final) code is deliberate: the delegate ledger entry could change in a subsequent ledger, potentially making an identical transaction valid in the future without any modification. A `ter` result invites retry; a `tef` would definitively reject it. + +The `shared_ptr` convention for ledger object handles throughout XRPL ensures the delegate entry stays alive through the check without copying the object, while `const` prevents accidental mutation during this read-only operation. + +### `loadGranularPermission` + +Once `checkTxPermission` has confirmed that the transaction type is permitted in the broad sense, some transaction types support a second dimension of delegation: granular flags that authorize only specific sub-operations. For example, a delegate authorized for `TrustSet` might still be restricted to only the `TrustlineAuthorize` flag, or a delegate for `AccountSet` might only be permitted to modify specific account settings. + +`loadGranularPermission` walks the same `sfPermissions` array, but instead of matching on transaction-type values, it casts each `sfPermissionValue` to `GranularPermissionType` and asks `Permission::getInstance().getGranularTxType()` whether that granular type maps to the requested `TxType`. If it does, the value is inserted into the caller-provided `unordered_set`. + +The design choice of populating a caller-owned set rather than returning a freshly allocated one is significant. Callers in practice (e.g., `TrustSet::checkPermission`, `Payment::checkPermission`, `AccountSet::checkPermission`) declare the set on the stack and pass it in by reference, avoiding heap allocation. It also allows future callers to accumulate granular permissions from multiple calls in sequence if needed, though current usage calls it once per permission check. + +### Call pattern in transactors + +The two functions are always used in sequence. A typical transactor's `checkPermission` method first resolves the `Delegate` SLE from the ledger via `keylet::delegate`, guards against a missing object, then calls `checkTxPermission`. If that returns success, the permission check exits early — no granular inspection is needed. If it fails, the transactor falls through to `loadGranularPermission`, populates the granular set, and then checks each flag against the transaction's field values (e.g., `tfSetfAuth`, `tfSetFreeze` for TrustSet; `PaymentXRP`, `PaymentIOU` for Payment). + +This two-stage pattern keeps the fast path efficient: transactions whose delegate holds a blanket transaction-type permission skip the entire granular scan. The granular path is only entered when the delegate relationship is more restrictively configured, which is the exceptional case in practice. + +### Implementation location + +The implementations of both functions live in `src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp`. The permission schema — the `GranularPermissionType` enum, the mapping from granular values to transaction types, and the `txToPermissionType` encoding convention — is centralized in `xrpl/protocol/Permissions.h`, keeping schema definition separate from runtime enforcement. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/DirectoryHelpers.h.ai.json b/include/xrpl/ledger/helpers/DirectoryHelpers.h.ai.json new file mode 100644 index 0000000000..c4f2000d7d --- /dev/null +++ b/include/xrpl/ledger/helpers/DirectoryHelpers.h.ai.json @@ -0,0 +1,142 @@ +{ + "args": [], + "classes": [], + "description": "This file provides helper functions for iterating and interacting with directory objects in the XRPL ledger, including legacy directory traversal functions and utilities for iterating over account owner directories.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/DirectoryHelpers.h", + "functions": [ + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 16, + "name": "internalDirNext" + }, + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 49, + "name": "internalDirFirst" + }, + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 80, + "name": "cdirFirst" + }, + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 87, + "name": "dirFirst" + }, + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 101, + "name": "cdirNext" + }, + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 108, + "name": "dirNext" + }, + { + "args": [ + "view", + "root", + "f" + ], + "lineno": 120, + "name": "forEachItem" + }, + { + "args": [ + "view", + "root", + "after", + "hint", + "limit", + "f" + ], + "lineno": 126, + "name": "forEachItemAfter" + }, + { + "args": [ + "view", + "id", + "f" + ], + "lineno": 136, + "name": "forEachItem" + }, + { + "args": [ + "view", + "id", + "after", + "hint", + "limit", + "f" + ], + "lineno": 144, + "name": "forEachItemAfter" + }, + { + "args": [ + "view", + "k" + ], + "lineno": 155, + "name": "dirIsEmpty" + }, + { + "args": [ + "account" + ], + "lineno": 160, + "name": "describeOwnerDir" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 13, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/DirectoryHelpers.h.ai.md b/include/xrpl/ledger/helpers/DirectoryHelpers.h.ai.md new file mode 100644 index 0000000000..c1efc6c995 --- /dev/null +++ b/include/xrpl/ledger/helpers/DirectoryHelpers.h.ai.md @@ -0,0 +1,37 @@ +# `DirectoryHelpers.h` — Ledger Directory Traversal Utilities + +## Role in the System + +In the XRPL ledger, a *directory* (`ltDIR_NODE`) is the data structure through which sets of related ledger objects are tracked. Every account's owned objects — offers, trust lines, payment channels, tickets, and more — are registered in that account's *owner directory*. Order book listings live in their own directory trees. A directory is physically a linked list of pages: each page (`SLE` of type `ltDIR_NODE`) holds an `sfIndexes` field, a `STVector256` of ledger-entry keys, and an `sfIndexNext` field that chains to the next page. + +`DirectoryHelpers.h` provides all the machinery for walking these multi-page directories: a low-level legacy cursor API, higher-level callback-based iterators, an emptiness check, and a factory helper used when inserting new directory nodes. + +## Const-Aware Template Core + +The real implementation lives in two private templates under `namespace xrpl::detail`: `internalDirFirst` and `internalDirNext`. Both are constrained with `std::enable_if_t` so they only instantiate for a view type that inherits from `ReadView` and a node type that is exactly `SLE` or `SLE const`. + +The critical design choice is the `if constexpr (std::is_const_v)` branch inside both templates. When `N` is `SLE const`, the template calls `view.read(...)` to obtain a const smart pointer — appropriate for read-only traversal via `ReadView`. When `N` is `SLE`, it calls `view.peek(...)` to get a mutable smart pointer — required when transactors need to modify the SLE while iterating (possible through `ApplyView`). This single template body unifies the read and write paths without duplicating logic or requiring virtual dispatch. + +## The Legacy Cursor API + +The four public functions `cdirFirst`, `cdirNext`, `dirFirst`, and `dirNext` are thin wrappers that route to the private templates. They maintain traversal state entirely in the caller: a `shared_ptr` for the current page, an `unsigned int` cursor within that page, and a `uint256` that receives the current entry's key. The caller is responsible for holding all three between calls. + +These functions are marked deprecated in favour of an iterator-based model. Their continued existence is justified by one specific use case: deletion while iterating. In `cleanupOnAccountDelete` (in `View.cpp`) the code iterates with `dirFirst`/`dirNext` while deleting ledger entries underneath. Because deletion shifts remaining entries in the page, the code manually decrements `uDirEntry` after each deletion to keep the cursor aligned. A clean C++ iterator would be invalidated immediately and could not be repaired this way; the exposed-state design allows the caller to patch the cursor, trading abstraction for correctness in this narrow case. The source comment in `View.cpp` (line 485–509) documents this technique explicitly. + +`internalDirNext` handles page transitions transparently: when the cursor reaches the end of `sfIndexes`, it reads `sfIndexNext` from the current page and follows the chain. If `sfIndexNext` is zero the directory is exhausted. The function then tail-calls itself to advance into the new page's first entry in one logical step. + +## Higher-Level Callback Iterators + +`forEachItem(ReadView, Keylet, f)` walks every entry in a directory by iterating pages in a simple `while(true)` loop, calling `view.read(keylet::child(key))` to materialise each entry and passing the result to the callback `f`. It never exposes the page or index cursor, making it safe for read-only scanning. + +`forEachItemAfter` supports cursor-based pagination as needed by RPC handlers (`account_offers`, `account_lines`, `account_channels`). It accepts an `after` key and a `hint` page number. The hint represents the directory page last seen by the client — the implementation first jumps to that page and checks whether `after` appears in it. If found, traversal resumes from that page rather than page 0, reducing the O(n) cost of re-scanning all prior pages on each paginated request. If the hint doesn't pan out, the code falls back to a linear scan from the root. The callback `f` returns a `bool`; returning `true` signals "stop" and counts against the supplied `limit`, letting callers cap how many items they process in a single RPC call. The function returns `false` if the directory itself is missing or `after` was never found, which callers treat as a pagination error. + +The two `inline` overloads that accept `AccountID` simply call `keylet::ownerDir(id)` and forward to the `Keylet` versions, providing a convenience entry point for the common case of iterating an account's owner directory. + +## `dirIsEmpty` + +A subtlety of the directory structure is that the root page (the anchor page) can legitimately carry zero entries while still serving as the head of a chain with non-empty subsequent pages. `dirIsEmpty` accounts for this: it returns `true` only if both the root page's `sfIndexes` is empty *and* its `sfIndexNext` field is zero. An absent root page is also treated as empty. + +## `describeOwnerDir` + +`describeOwnerDir` returns a `std::function` that stamps `sfOwner = account` onto a newly created directory page. This callback is the conventional third argument to `ApplyView::dirInsert` throughout the codebase — from `RippleStateHelpers.cpp` to `PaymentChannelCreate.cpp` — and is invoked only when `dirInsert` creates a fresh directory node. Returning a function object rather than taking a direct `AccountID` argument allows the insertion logic to defer the initialisation step until it is certain a new page will be created. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h.ai.json b/include/xrpl/ledger/helpers/EscrowHelpers.h.ai.json new file mode 100644 index 0000000000..d381dc1818 --- /dev/null +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h.ai.json @@ -0,0 +1,47 @@ +{ + "args": [], + "classes": [], + "description": "Provides helper functions for applying escrow unlock logic for different issue types (Issue and MPTIssue) in the XRPL ledger, handling trust line creation, transfer fees, and asset transfers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/EscrowHelpers.h", + "functions": [ + { + "args": [ + "ApplyView& view", + "Rate lockedRate", + "std::shared_ptr const& sleDest", + "STAmount const& xrpBalance", + "STAmount const& amount", + "AccountID const& issuer", + "AccountID const& sender", + "AccountID const& receiver", + "bool createAsset", + "beast::Journal journal" + ], + "lineno": 19, + "name": "escrowUnlockApplyHelper" + }, + { + "args": [ + "ApplyView& view", + "Rate lockedRate", + "std::shared_ptr const& sleDest", + "STAmount const& xrpBalance", + "STAmount const& amount", + "AccountID const& issuer", + "AccountID const& sender", + "AccountID const& receiver", + "bool createAsset", + "beast::Journal journal" + ], + "lineno": 99, + "name": "escrowUnlockApplyHelper" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/EscrowHelpers.h.ai.md b/include/xrpl/ledger/helpers/EscrowHelpers.h.ai.md new file mode 100644 index 0000000000..b80eb71030 --- /dev/null +++ b/include/xrpl/ledger/helpers/EscrowHelpers.h.ai.md @@ -0,0 +1,37 @@ +# `include/xrpl/ledger/helpers/EscrowHelpers.h` + +This header implements the token-delivery half of escrow resolution for the `featureTokenEscrow` amendment. When an escrow holding an IOU or Multi-Purpose Token (MPT) is either finished (`EscrowFinish`) or cancelled (`EscrowCancel`), the ledger must credit the appropriate account with the locked tokens. That credit is non-trivial: it may require creating a new trust line or MPToken holding object, computing and deducting a transfer fee, and validating that the resulting balance respects the receiver's declared credit limit. `EscrowHelpers.h` centralises all of this logic in a single function, `escrowUnlockApplyHelper`, which is specialized for each asset type. + +## Template Architecture: Static Dispatch, No Runtime Overhead + +The file declares a primary template `escrowUnlockApplyHelper` constrained by `ValidIssueType` but provides no body for it. Only two explicit full specializations are defined — one for `Issue` (IOUs) and one for `MPTIssue` (MPTs) — both as `inline` functions, making them eligible for inlining into their callers. The calling code in `EscrowFinish.cpp` and `EscrowCancel.cpp` invokes this through `std::visit` on the `Asset` variant, letting the compiler resolve the correct specialization at compile time. This avoids virtual dispatch while keeping both code paths behind a single named interface, which makes future asset types easy to add: provide a new specialization and the `std::visit` dispatch picks it up automatically. + +## The `Issue` Specialization (IOU Trust-Line Path) + +The `Issue` path mirrors much of what a regular IOU payment does, but with several escrow-specific adaptations. + +**Issuer short-circuits.** If the `sender` is the `issuer`, the function returns `tecINTERNAL` — this should never happen because an issuer cannot hold their own obligations as an escrow sender. If the `receiver` is the `issuer`, the function returns `tesSUCCESS` immediately: delivery to the issuer is a redemption event and is handled by the calling transactor at the balance level, not here. + +**Trust line creation.** When `createAsset` is `true` — which the callers set only when the account submitting the transaction is also the beneficiary — the function will auto-create the receiver's trust line if it doesn't exist. This gating is a critical policy point: auto-creating a trust line for a third party would violate XRPL's rule that accounts control their own directory and reserve. The reserve check precedes creation: if `xrpBalance < accountReserve(ownerCount + 1)`, the call fails with `tecNO_LINE_INSUF_RESERVE`. The trust line is created via `trustCreate` with `sfDefaultRipple` behaviour derived from the destination's account flags. + +When `createAsset` is `false`, the function validates that the pending transfer will not push the receiver's balance above their declared trust-line limit, failing with `tecLIMIT_EXCEEDED` if it would. This check is skipped when the receiver auto-creates their line because a freshly created line starts at zero balance with a limit of zero — the amount would always fail the check spuriously. + +**Transfer fee deduction.** The `lockedRate` parameter is the transfer rate snapshotted at escrow creation time. The function caps it at the issuer's *current* transfer rate (`transferRate(view, amount)`), taking the lower of the two — protecting the receiver from a rate *increase* during the escrow's lifetime while preventing the issuer from artificially locking in a high rate. Fee is applied only when neither party is the issuer. Crucially, the fee is deducted *from* the escrowed amount rather than added on top: `finalAmt = amount - fee`. This differs from regular payments, where the sender covers the fee in addition to the principal. In an escrow, the locked principal is fixed; the fee absorbs a portion of it, leaving the receiver with less than the face value. + +The actual credit is delivered by `directSendNoFee(view, issuer, receiver, finalAmt, true, journal)`. This call originates the IOU from the issuer's perspective (the correct model for IOU accounting on XRPL) without applying an additional fee layer. + +## The `MPTIssue` Specialization (MPT Path) + +The MPT path follows the same structure as the IOU path but targets MPToken objects rather than RippleState trust lines. + +**MPToken creation.** If the receiver doesn't yet hold an MPToken for the issuance and `createAsset` is `true` and the receiver is not the issuer, the function creates one via `createMPToken` and increments the receiver's owner count with `adjustOwnerCount`. The reserve check uses `tecINSUFFICIENT_RESERVE` (consistent with the MPT framework) rather than the IOU-specific `tecNO_LINE_INSUF_RESERVE`. If no MPToken exists after the creation attempt and the receiver is not the issuer, the function returns `tecNO_PERMISSION`. + +**The `fixTokenEscrowV1` bug fix.** The call to `unlockEscrowMPT` passes two amounts: `finalAmt` (the net after fee deduction) for the receiver credit, and a gross amount for the outstanding-balance adjustment. Without `fixTokenEscrowV1`, the gross argument is also `finalAmt`, meaning the MPT's `sfOutstandingAmount` is only reduced by the net delivered amount even though the escrowed principal was the full `amount`. With the fix enabled, the gross argument is the original `amount`, so the outstanding amount correctly decreases by the face value of the escrow, with the fee portion being "burned" from the outstanding supply rather than silently retained. + +## `lockedRate` and Cancellation Semantics + +`EscrowFinish.cpp` reads `sfTransferRate` from the escrow object and passes it as `lockedRate`. `EscrowCancel.cpp` always passes `parityRate` (1:1, no fee) — the original sender is returning their own tokens to themselves, so charging a transfer fee on cancellation would be economically incoherent. + +## Relationship to Sibling Helpers + +This file is a pure consumer. It delegates to `RippleStateHelpers.h` for `trustCreate`, `directSendNoFee`, and `transferRate`; to `MPTokenHelpers.h` for `createMPToken` and `unlockEscrowMPT`; and to `AccountRootHelpers.h` for `adjustOwnerCount`. Its sole responsibility is the escrow-specific sequencing and conditional logic that wraps those primitives: reserve checks, rate capping, fee deduction arithmetic, and the `createAsset` gating that enforces account sovereignty over directory entries. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h.ai.json b/include/xrpl/ledger/helpers/MPTokenHelpers.h.ai.json new file mode 100644 index 0000000000..9bccea563e --- /dev/null +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h.ai.json @@ -0,0 +1,243 @@ +{ + "args": [], + "classes": [], + "description": "This file provides helper functions for handling Multi-Party Token (MPT) operations in the XRPL ledger, including freeze checks, transfer rates, authorization, holding management, escrow operations, overflow checks, and DEX-related logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/MPTokenHelpers.h", + "functions": [ + { + "args": [ + "view", + "mptIssue" + ], + "lineno": 17, + "name": "isGlobalFrozen" + }, + { + "args": [ + "view", + "account", + "mptIssue" + ], + "lineno": 20, + "name": "isIndividualFrozen" + }, + { + "args": [ + "view", + "account", + "mptIssue", + "depth" + ], + "lineno": 23, + "name": "isFrozen" + }, + { + "args": [ + "view", + "accounts", + "mptIssue", + "depth" + ], + "lineno": 26, + "name": "isAnyFrozen" + }, + { + "args": [ + "view", + "issuanceID" + ], + "lineno": 38, + "name": "transferRate" + }, + { + "args": [ + "view", + "mptIssue" + ], + "lineno": 46, + "name": "canAddHolding" + }, + { + "args": [ + "view", + "priorBalance", + "mptIssuanceID", + "account", + "journal", + "flags", + "holderID" + ], + "lineno": 51, + "name": "authorizeMPToken" + }, + { + "args": [ + "view", + "mptIssue", + "account", + "authType", + "depth" + ], + "lineno": 65, + "name": "requireAuth" + }, + { + "args": [ + "view", + "mptIssuanceID", + "account", + "priorBalance", + "j" + ], + "lineno": 77, + "name": "enforceMPTokenAuthorization" + }, + { + "args": [ + "view", + "mptIssue", + "from", + "to" + ], + "lineno": 87, + "name": "canTransfer" + }, + { + "args": [ + "view", + "asset" + ], + "lineno": 94, + "name": "canTrade" + }, + { + "args": [ + "view", + "accountID", + "priorBalance", + "mptIssue", + "journal" + ], + "lineno": 102, + "name": "addEmptyHolding" + }, + { + "args": [ + "view", + "accountID", + "mptIssue", + "journal" + ], + "lineno": 109, + "name": "removeEmptyHolding" + }, + { + "args": [ + "view", + "uGrantorID", + "saAmount", + "j" + ], + "lineno": 117, + "name": "lockEscrowMPT" + }, + { + "args": [ + "view", + "uGrantorID", + "uGranteeID", + "netAmount", + "grossAmount", + "j" + ], + "lineno": 122, + "name": "unlockEscrowMPT" + }, + { + "args": [ + "view", + "mptIssuanceID", + "account", + "flags" + ], + "lineno": 129, + "name": "createMPToken" + }, + { + "args": [ + "view", + "mptIssue", + "holder", + "j" + ], + "lineno": 134, + "name": "checkCreateMPT" + }, + { + "args": [ + "sleIssuance" + ], + "lineno": 143, + "name": "maxMPTAmount" + }, + { + "args": [ + "sleIssuance" + ], + "lineno": 146, + "name": "availableMPTAmount" + }, + { + "args": [ + "view", + "mptID" + ], + "lineno": 149, + "name": "availableMPTAmount" + }, + { + "args": [ + "sendAmount", + "outstandingAmount", + "maximumAmount", + "allowOverflow" + ], + "lineno": 158, + "name": "isMPTOverflow" + }, + { + "args": [ + "view", + "issue" + ], + "lineno": 175, + "name": "issuerFundsToSelfIssue" + }, + { + "args": [ + "view", + "issue", + "amount" + ], + "lineno": 181, + "name": "issuerSelfDebitHookMPT" + }, + { + "args": [ + "v", + "tx", + "asset", + "accountID" + ], + "lineno": 191, + "name": "checkMPTTxAllowed" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/MPTokenHelpers.h.ai.md b/include/xrpl/ledger/helpers/MPTokenHelpers.h.ai.md new file mode 100644 index 0000000000..1b05f1f619 --- /dev/null +++ b/include/xrpl/ledger/helpers/MPTokenHelpers.h.ai.md @@ -0,0 +1,47 @@ +# `include/xrpl/ledger/helpers/MPTokenHelpers.h` + +This header declares the MPT-specific layer of the XRPL token helper system. It sits directly below the generic `TokenHelpers.h`, which provides `Asset`-polymorphic dispatchers covering both IOU trust-line tokens and MPTs. The functions here implement the MPT side of that contract and also expose several operations that have no IOU equivalent: escrow mechanics, DEX permission checks, overflow arithmetic, and the two-phase authorization protocol that is central to MPT's design. + +`MPTIssue` — the type passed to almost every function — wraps a 192-bit `MPTID` that encodes the issuing account's sequence number and account ID, so the issuer identity can always be recovered from the token identifier without a separate ledger lookup. + +## Freeze Checking + +Three freeze predicates are exported at increasing levels of granularity. `isGlobalFrozen` reads the `MPTokenIssuance` SLE and tests `lsfMPTLocked`. `isIndividualFrozen` reads the per-holder `MPToken` SLE and tests the same flag on that object. `isFrozen` ORs both, and then additionally calls an internal `isVaultPseudoAccountFrozen` helper that descends into any vault the holder's account belongs to. The `depth` parameter on `isFrozen` and `isAnyFrozen` guards against infinite recursion when a vault holds MPT shares backed by another vault — a configuration the protocol currently forbids, but the code defends against anyway up to `maxAssetCheckDepth`. + +## Transfer Rate + +`transferRate(ReadView const&, MPTID const&)` reads `sfTransferFee` from the issuance SLE and converts it to the same `Rate` type used by IOU trust lines. The encoding: XRPL rates use a 1 000 000 000 base (parity = no fee), and the MPT `sfTransferFee` field is in units of 0.001%, so the formula `1,000,000,000 + 10,000 × fee` maps 0→parity, 50,000→2,000,000,000 (100% surcharge, i.e. 50% transfer fee on the gross). If the field is absent, `parityRate` is returned. + +## Holding Lifecycle + +`canAddHolding` performs a dry-run guard: it returns `tecOBJECT_NOT_FOUND` if the issuance does not exist, and `tecNO_AUTH` if `lsfMPTCanTransfer` is not set, which is the flag that controls whether anyone other than the issuer may hold the token. + +`addEmptyHolding` allocates a new `MPToken` SLE for the holder. For the issuer's own account it short-circuits to `tesSUCCESS` — issuers do not hold `MPToken` objects. For everyone else it delegates to `authorizeMPToken` (the creation path). The required XRP reserve follows the same rule as trust lines: the first two owner-directory entries are exempt, after which `accountReserve(ownerCount + 1)` is enforced against `priorBalance`. + +`removeEmptyHolding` requires the `MPToken`'s `sfMPTAmount` to be zero (and `sfLockedAmount` to be zero once `fixSecurity3_1_3` is enabled). It then calls `authorizeMPToken` with `tfMPTUnauthorize` to erase the object and remove it from the owner directory. + +## Authorization: `requireAuth` and `enforceMPTokenAuthorization` + +The split between these two functions mirrors XRPL's transaction pipeline division into `preclaim` (read-only feasibility check) and `doApply` (state-writing execution). + +`requireAuth` is the read-only check. It returns `tesSUCCESS` immediately for the issuer. For other accounts it supports two authorization modes. In the classic mode (`lsfMPTRequireAuth` set, no `sfDomainID`), the holder must have an `MPToken` SLE with `lsfMPTAuthorized` set by the issuer. In the domain-based mode (`sfDomainID` present), authorization comes from the holder's on-chain credentials being verified against that domain via `credentials::validDomain`. When the `featureSingleAssetVault` amendment is active, vault and loan-broker pseudo-accounts are always implicitly authorized, and the check recurses into the vault's underlying asset — descending through `depth + 1` each time. + +`enforceMPTokenAuthorization` runs during `doApply`. Its key responsibility beyond re-checking is *creating the `MPToken` SLE on-the-fly* when a domain-authorized holder does not yet have one — the caller provides `priorBalance` precisely so this lazy allocation can enforce the XRP reserve. It also handles the case where credentials have expired between `preclaim` and `doApply` by running `verifyValidDomain` (which deletes expired credentials as a side effect) and returning `tecEXPIRED` when appropriate. + +## Escrow Mechanics + +`lockEscrowMPT` moves a token amount from live to escrowed state. It decrements `sfMPTAmount` on the sender's `MPToken` and increments `sfLockedAmount` on both that object and the `MPTokenIssuance`. Critically, `sfOutstandingAmount` on the issuance does not change — escrowed tokens remain outstanding because the recipient has not yet received them. Underflow and overflow are checked defensively at each step. + +`unlockEscrowMPT` completes the escrow. It receives both `grossAmount` (what was locked) and `netAmount` (what the recipient gets after fees). The locked counters on the sender's `MPToken` and the issuance are decremented by `grossAmount`. The receiver's balance is credited separately by the payment machinery. If `grossAmount > netAmount` (a fee was charged), the difference is subtracted from `sfOutstandingAmount` on the issuance — the fee is effectively burned. + +## Overflow Arithmetic + +`isMPTOverflow` encodes a deliberate two-threshold strategy. For direct sends that bypass the payment engine (`AllowMPTOverflow::No`), it enforces the strict rule `OutstandingAmount + sendAmount ≤ MaximumAmount`. For sends through the payment engine (`AllowMPTOverflow::Yes`), the effective ceiling is raised to `UINT64_MAX`. This relaxation exists because offer-crossing paths can temporarily create an issuing step (outbound from the issuer) before a matching redemption step collapses the overflow in the same transaction. The comment in the header documents this explicitly. + +`availableMPTAmount` is a thin wrapper that computes `MaximumAmount − OutstandingAmount` from the SLE; the comment acknowledges that `OutstandingAmount` can transiently exceed `MaximumAmount`, making the result signed and potentially negative — callers must handle that. + +## DEX Integration + +`canTransfer` allows a transfer only if `lsfMPTCanTransfer` is set, or if at least one party is the issuer. `canTrade` guards DEX use by checking `lsfMPTCanTrade` on the issuance. `checkMPTTxAllowed` is the comprehensive gating function called by DEX and payment transaction types; it verifies that the issuance exists, is not globally locked, has the `lsfMPTCanTrade` flag, and (for non-issuer accounts) has `lsfMPTCanTransfer` set and the account's own `MPToken` is not individually locked. + +`issuerFundsToSelfIssue` and `issuerSelfDebitHookMPT` handle the tracking needed when an issuer owns an MPT sell offer on the DEX. During an issuing step the issuer's "available" balance is determined by how much more they can issue (i.e., `availableMPTAmount`), and the debit hook records how much has been sold in that step so the payment engine can accurately balance the final settlement. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h.ai.json b/include/xrpl/ledger/helpers/NFTokenHelpers.h.ai.json new file mode 100644 index 0000000000..61fc2a804c --- /dev/null +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h.ai.json @@ -0,0 +1,300 @@ +{ + "args": [ + { + "lineno": 13, + "name": "view" + }, + { + "lineno": 14, + "name": "directory" + }, + { + "lineno": 15, + "name": "maxDeletableOffers" + }, + { + "lineno": 20, + "name": "nftokenID" + }, + { + "lineno": 23, + "name": "owner" + }, + { + "lineno": 31, + "name": "token_" + }, + { + "lineno": 31, + "name": "page_" + }, + { + "lineno": 42, + "name": "nft" + }, + { + "lineno": 49, + "name": "page" + }, + { + "lineno": 62, + "name": "offer" + }, + { + "lineno": 78, + "name": "a" + }, + { + "lineno": 78, + "name": "b" + }, + { + "lineno": 82, + "name": "uri" + }, + { + "lineno": 91, + "name": "acctID" + }, + { + "lineno": 92, + "name": "amount" + }, + { + "lineno": 93, + "name": "dest" + }, + { + "lineno": 94, + "name": "expiration" + }, + { + "lineno": 95, + "name": "nftFlags" + }, + { + "lineno": 96, + "name": "rules" + }, + { + "lineno": 98, + "name": "txFlags" + }, + { + "lineno": 103, + "name": "nftIssuer" + }, + { + "lineno": 106, + "name": "xferFee" + }, + { + "lineno": 107, + "name": "j" + }, + { + "lineno": 117, + "name": "seqProxy" + }, + { + "lineno": 120, + "name": "priorBalance" + }, + { + "lineno": 127, + "name": "id" + }, + { + "lineno": 129, + "name": "issue" + } + ], + "classes": [ + { + "args": [ + "token_", + "page_" + ], + "lineno": 27, + "name": "TokenAndPage" + } + ], + "description": "This file provides helper functions and structures for managing NFTs (Non-Fungible Tokens) in the XRPL ledger, including token offer management, token directory operations, and preflight/preclaim checks for NFT-related transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/NFTokenHelpers.h", + "functions": [ + { + "args": [ + "view", + "directory", + "maxDeletableOffers" + ], + "lineno": 13, + "name": "removeTokenOffersWithLimit" + }, + { + "args": [ + "view", + "nftokenID" + ], + "lineno": 19, + "name": "notTooManyOffers" + }, + { + "args": [ + "view", + "owner", + "nftokenID" + ], + "lineno": 22, + "name": "findToken" + }, + { + "args": [ + "view", + "owner", + "nftokenID" + ], + "lineno": 36, + "name": "findTokenAndPage" + }, + { + "args": [ + "view", + "owner", + "nft" + ], + "lineno": 41, + "name": "insertToken" + }, + { + "args": [ + "view", + "owner", + "nftokenID" + ], + "lineno": 44, + "name": "removeToken" + }, + { + "args": [ + "view", + "owner", + "nftokenID", + "page" + ], + "lineno": 48, + "name": "removeToken" + }, + { + "args": [ + "view", + "offer" + ], + "lineno": 61, + "name": "deleteTokenOffer" + }, + { + "args": [ + "view", + "owner" + ], + "lineno": 73, + "name": "repairNFTokenDirectoryLinks" + }, + { + "args": [ + "a", + "b" + ], + "lineno": 77, + "name": "compareTokens" + }, + { + "args": [ + "view", + "owner", + "nftokenID", + "uri" + ], + "lineno": 79, + "name": "changeTokenURI" + }, + { + "args": [ + "acctID", + "amount", + "dest", + "expiration", + "nftFlags", + "rules", + "owner", + "txFlags" + ], + "lineno": 89, + "name": "tokenOfferCreatePreflight" + }, + { + "args": [ + "view", + "acctID", + "nftIssuer", + "amount", + "dest", + "nftFlags", + "xferFee", + "j", + "owner", + "txFlags" + ], + "lineno": 101, + "name": "tokenOfferCreatePreclaim" + }, + { + "args": [ + "view", + "acctID", + "amount", + "dest", + "expiration", + "seqProxy", + "nftokenID", + "priorBalance", + "j", + "txFlags" + ], + "lineno": 114, + "name": "tokenOfferCreateApply" + }, + { + "args": [ + "view", + "id", + "j", + "issue" + ], + "lineno": 126, + "name": "checkTrustlineAuthorized" + }, + { + "args": [ + "view", + "id", + "j", + "issue" + ], + "lineno": 132, + "name": "checkTrustlineDeepFrozen" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "nft" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/NFTokenHelpers.h.ai.md b/include/xrpl/ledger/helpers/NFTokenHelpers.h.ai.md new file mode 100644 index 0000000000..0470364736 --- /dev/null +++ b/include/xrpl/ledger/helpers/NFTokenHelpers.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/ledger/helpers/NFTokenHelpers.h` + +This header is the central API surface for NFT lifecycle management within the XRPL ledger. It lives in the `xrpl::nft` namespace and declares every function needed to insert, remove, and query NFTs in the on-ledger token directory, manage buy/sell offers, and execute the three transaction-processing phases (preflight, preclaim, doApply) for offer creation. The implementations live in `src/libxrpl/ledger/helpers/NFTokenHelpers.cpp`, which is the most complex helper module in the ledger subsystem. + +## NFToken Storage Architecture + +NFTs are not stored as individual ledger objects. Instead, an account's entire NFT holdings are packed into a doubly-linked list of `ltNFTOKEN_PAGE` SLEs (serialized ledger entries), each holding up to `dirMaxTokensPerPage` tokens as an `STArray`. Pages are keyed in the ledger by a `Keylet` derived from the account ID and the low 96-bits of the NFT ID (the `pageMask`). The chain is anchored by a deterministic "max" page at `keylet::nftpage_max(owner)`, which always exists as the final node. + +Within a page, tokens are sorted by `compareTokens()`, which orders first by the low 96-bits of the token ID and then by the full 256-bit value as a tiebreaker. This two-level sort exists because the page partitioning scheme uses only the low 96-bits for page boundaries: tokens with identical low 96-bits are "equivalent" and must all reside on the same page. The full-value fallback ensures a stable, deterministic ordering for those groups. + +## Token Directory Operations + +`insertToken()` delegates the hard work to the file-private `getPageForToken()`. If the correct page is full, the page is split. The split algorithm uses the `pageMask` to identify equivalent-token groups and never bisects such a group — it rounds the split point forward or backward as needed. If an entire page is filled with equivalent tokens and a new token would belong to the same group, `getPageForToken()` returns `nullptr`, causing `insertToken()` to return `tecNO_SUITABLE_NFTOKEN_PAGE`. Each page split increments the owner's reserve count via `adjustOwnerCount()`. + +`removeToken()` performs the inverse: it erases the token from the containing page, then attempts to merge the page with its neighbours using the file-private `mergePages()`. Merging fires a reserve credit. If the page becomes empty, it is unlinked and erased entirely. A special case applies under the `fixNFTokenPageLinks` amendment: if the empty page happens to be the last page in the directory (identifiable because its key matches `pageMask`), its contents are moved to the previous page and the empty last page is kept with the stable anchor key, preserving the invariant that the final page is always the `nftpage_max` sentinel. + +`findToken()` takes a `ReadView` (read-only) and returns an `std::optional`. `findTokenAndPage()` requires an `ApplyView` (read-write) and returns the `TokenAndPage` aggregate, which bundles the token `STObject` with the mutable `shared_ptr` page. Returning the page is a deliberate optimization: callers like `NFTokenModify` that need to alter the token in place can do so without a second ledger traversal to re-locate the page. + +## Offer Lifecycle + +Each NFT offer is tracked in exactly two directories: the owner's owner directory and either the token's buy or sell directory (`keylet::nft_buys` / `keylet::nft_sells`). `deleteTokenOffer()` removes the offer from both directories, decrements the owner count, and erases the SLE. It returns `false` if the SLE is the wrong type, acting as a type-safety guard. + +`removeTokenOffersWithLimit()` is the bulk-deletion primitive used when burning a token. It iterates the given offer directory page by page in reverse order within each page, calling `deleteTokenOffer()` until the `maxDeletableOffers` cap is reached. Reverse iteration within a page is necessary because NFTokenOffer directory pages use a vector-backed `sfIndexes` field and forward-erasing would corrupt iterators. + +`notTooManyOffers()` is a burn guard run during preclaim. It counts all open offers across both buy and sell directories (iterating page-by-page for efficiency) and returns `tefTOO_BIG` if the total exceeds `maxDeletableTokenOfferEntries`. This prevents a token with a pathologically large offer set from being impossible to burn. + +## Shared Transaction Phases + +`tokenOfferCreatePreflight`, `tokenOfferCreatePreclaim`, and `tokenOfferCreateApply` are shared between `NFTokenCreateOffer` and `NFTokenMint` (which can implicitly create a sell offer at mint time). Centralising this logic avoids duplicating the involved validation rules. + +`tokenOfferCreatePreflight` performs static checks requiring no ledger access: negative amounts, zero amounts for buy offers, zero-value IOU amounts, zero expiration, and malformed `Owner`/`Destination` combinations. A buy offer must name an `owner` (the token holder being targeted); a sell offer must not, because the seller is implicit. Neither side may designate itself as the destination. + +`tokenOfferCreatePreclaim` accesses the ledger. For non-XRP offers on tokens without `flagCreateTrustLines`, it verifies the NFT issuer's trust line exists for the IOU and is not frozen. Under the `featureNFTokenMintOffer` amendment, an issuer selling their own currency is exempted from the trust line check. Transferability is enforced: if `flagTransferable` is absent and the transacting account is neither the issuer nor the current `sfNFTokenMinter`, the preclaim returns `tefNFTOKEN_IS_NOT_TRANSFERABLE`. The `fixEnforceNFTokenTrustlineV2` amendment adds a call to `checkTrustlineAuthorized()`, closing a loophole where unauthorized trust lines with a positive balance could be used for buy offers. + +`tokenOfferCreateApply` reserves XRP for the new offer object, inserts the offer into both directories (owner directory and token offer directory), constructs the `ltNFTOKEN_OFFER` SLE, and increments the owner count. + +## Repair Utility + +`repairNFTokenDirectoryLinks()` is a ledger repair tool invoked by the `LedgerStateFix` transaction. It walks the entire NFToken page chain for an account and corrects any broken `sfNextPageMin` / `sfPreviousPageMin` links. It also handles the case where the last page does not have the expected `nftpage_max` key — in that scenario the page contents are migrated to a newly created SLE with the correct key, the old SLE is erased, and the chain is relinked. The function returns `true` if any correction was made, giving the caller a way to avoid unnecessary ledger updates on clean accounts. + +## Design Notes + +The two overloads of `removeToken()` reflect a deliberate API split: the single-argument overload performs page discovery internally (convenient for callers that don't already hold a page reference), while the overload that accepts a `shared_ptr page` is for callers like `NFTokenBurn` that already located the page through `findTokenAndPage()` and want to avoid repeating the traversal. The `tokenOfferCreate*` triad using `owner = std::nullopt` and `txFlags = tfSellNFToken` as defaults lets `NFTokenMint` pass through the same validation pipeline as `NFTokenCreateOffer` with minimal parameter adaptation. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/OfferHelpers.h.ai.json b/include/xrpl/ledger/helpers/OfferHelpers.h.ai.json new file mode 100644 index 0000000000..5354bb0eb5 --- /dev/null +++ b/include/xrpl/ledger/helpers/OfferHelpers.h.ai.json @@ -0,0 +1,37 @@ +{ + "args": [ + { + "lineno": 22, + "name": "view" + }, + { + "lineno": 22, + "name": "sle" + }, + { + "lineno": 22, + "name": "j" + } + ], + "classes": [], + "description": "Provides a function to delete an offer from the ledger in the XRPL codebase, ensuring the offer exists and permissions are checked.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/OfferHelpers.h", + "functions": [ + { + "args": [ + "view", + "sle", + "j" + ], + "lineno": 22, + "name": "offerDelete" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/OfferHelpers.h.ai.md b/include/xrpl/ledger/helpers/OfferHelpers.h.ai.md new file mode 100644 index 0000000000..e7fc47ca62 --- /dev/null +++ b/include/xrpl/ledger/helpers/OfferHelpers.h.ai.md @@ -0,0 +1,38 @@ +# `include/xrpl/ledger/helpers/OfferHelpers.h` + +This header is part of the `ledger/helpers` module — a collection of focused utility headers, each scoped to a single ledger object type. `OfferHelpers.h` exposes exactly one function: `offerDelete`, the canonical routine for removing an offer from the XRPL ledger state. Its narrow scope reflects the design philosophy of this module: each helper encapsulates the multi-step bookkeeping for its object type so that callers deal only with intent, not mechanics. + +## Why a dedicated helper exists + +Deleting an offer is not a single-object erasure. An offer entry (`ltOFFER`) participates in two separate ledger directory structures simultaneously: the owner's `ownerDir` (indexed by account, used to enforce reserve requirements and enumerate an account's objects) and the order-book directory (`keylet::page(sfBookDirectory)`, used by the DEX path-finding engine to enumerate offers at a given quality tier). Both directory back-references must be cleaned up atomically within the `ApplyView` transaction buffer before the SLE itself is erased, and the owner's reserve count must be decremented. Scattering this sequence across callers would be error-prone, so it lives here. + +## Callers and context + +`offerDelete` is invoked from three distinct contexts in the codebase: + +- **`OfferCancel::doApply`** — the explicit cancellation transaction. The caller first resolves the offer by sequence number via `keylet::offer(account_, offerSequence)` and then delegates the teardown entirely to `offerDelete`. +- **`OfferCreate::doApply`** — during offer crossing, fully-consumed counter-offers in `removableOffers` are deleted from the sandbox view. Partially-consumed offers that are replaced by the new order are also pruned here. +- **`BookTip::step`** — during payment path traversal. When the path engine advances through order-book pages, it deletes the previously-consumed (or expired/unfunded) offer from the previous `step` call before probing the next candidate. This is the hot path for AMM and DEX payments. + +The breadth of these call sites explains the deliberate choice to comment out `[[nodiscard]]`. The standard flow and `BookTip` callers do not always inspect the `TER` return, and making the attribute mandatory would have broken compilation across the engine. The comment preserves the intent without enforcing it. + +## Implementation walkthrough + +The implementation in `OfferHelpers.cpp` starts with a null guard: if `sle` is empty, it returns `tesSUCCESS` immediately. This handles the case where a caller peeks an offer that has already been deleted in the same transaction batch — a defensive idiom that prevents double-delete panics without requiring the caller to pre-check. + +The deletion sequence for a normal offer is: + +1. Remove the entry from the owner's directory via `view.dirRemove(keylet::ownerDir(owner), sfOwnerNode, offerIndex, false)`. +2. Remove the entry from the order-book directory via `view.dirRemove(keylet::page(uDirectory), sfBookNode, offerIndex, false)`. +3. Decrement the owner count by calling `adjustOwnerCount(..., -1, j)`, which writes back into the account root SLE and keeps the on-ledger reserve calculation accurate. +4. Erase the offer SLE itself via `view.erase(sle)`. + +If either `dirRemove` call returns false, the function returns `tefBAD_LEDGER`. Both such branches carry `LCOV_EXCL_LINE` annotations, signalling that they represent invariant violations — if an offer SLE exists with a valid `sfOwnerNode` and `sfBookNode`, its corresponding directory entries must exist. The error code is present for safety, not for expected operation. + +## Hybrid domain offers + +A notable extension handles offers flagged `lsfHybrid` — offers that participate in a Permissioned DEX domain in addition to the global order book. These offers carry an `sfAdditionalBooks` array, each element encoding an additional `sfBookDirectory` and `sfBookNode`. When present, the function iterates this array and issues one additional `dirRemove` call per extra book directory before proceeding to the owner-count adjustment and SLE erasure. An `XRPL_ASSERT` validates that the `lsfHybrid` flag and `sfDomainID` are both set whenever `sfAdditionalBooks` is present, enforcing the invariant that hybrid metadata is always consistent. + +## Contract and caller responsibilities + +The header comment states two preconditions explicitly: the offer must exist, and the caller must have already verified permissions. The function does not re-check whether the submitting account owns the offer — that is the responsibility of each calling transactor. This keeps `offerDelete` a pure bookkeeping utility, free of policy logic, which is exactly the right abstraction boundary for shared ledger-layer helpers. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h.ai.json b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h.ai.json new file mode 100644 index 0000000000..7febe3d897 --- /dev/null +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [ + { + "lineno": 9, + "name": "slep" + }, + { + "lineno": 10, + "name": "view" + }, + { + "lineno": 11, + "name": "key" + }, + { + "lineno": 12, + "name": "j" + } + ], + "classes": [], + "description": "Declares the closeChannel function for closing a payment channel in the XRPL ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/PaymentChannelHelpers.h", + "functions": [ + { + "args": [ + "slep", + "view", + "key", + "j" + ], + "lineno": 8, + "name": "closeChannel" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/PaymentChannelHelpers.h.ai.md b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h.ai.md new file mode 100644 index 0000000000..8225f4f261 --- /dev/null +++ b/include/xrpl/ledger/helpers/PaymentChannelHelpers.h.ai.md @@ -0,0 +1,51 @@ +# `PaymentChannelHelpers.h` — Payment Channel Close Logic + +This header is part of the `include/xrpl/ledger/helpers/` collection — a set of thin, focused headers that isolate reusable ledger-mutation operations from individual transaction implementations. The file declares a single function, `closeChannel`, which encapsulates the complete teardown sequence for an XRPL payment channel object. + +## Why a Standalone Helper + +Payment channel closure is not triggered by a single transaction type. Both `PaymentChannelClaim` and `PaymentChannelFund` need to close an expired channel: the Claim transactor may close on expiry, on a `tfClose` flag, or when a fully-drained channel is claimed; the Fund transactor must refuse to top up a channel whose `cancelAfter` or `expiration` has already passed and instead close it immediately. Rather than duplicate the multi-step teardown logic in each transactor, `closeChannel` is extracted here so both `doApply()` implementations can call it identically. + +## `closeChannel` — Anatomy of the Implementation + +```cpp +TER closeChannel( + std::shared_ptr const& slep, + ApplyView& view, + uint256 const& key, + beast::Journal j); +``` + +The implementation (in `src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp`) performs four distinct ledger mutations in order: + +**1. Remove the channel from the source's owner directory.** +The source account (`sfAccount`) always has a reference to the channel recorded in its owner directory under `sfOwnerNode`. `view.dirRemove` is called unconditionally. If that removal fails, the function returns `tefBAD_LEDGER` — a fatal internal-state error, not a user-visible transaction failure, hence the `LCOV_EXCL` annotation marking it as an unreachable path in normal testing. + +**2. Conditionally remove the channel from the destination's owner directory.** +`sfDestinationNode` is accessed as an optional field (`~sfDestinationNode`). This reflects a protocol evolution: older payment channels did not track the channel in the recipient's owner directory; newer ones do. The presence of the field determines whether a second `dirRemove` call is needed. This pattern avoids breaking backward compatibility while cleanly handling both old and new channel objects. + +**3. Refund the unspent balance to the source account.** +The key arithmetic is: + +```cpp +(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]; +``` + +Here `sfAmount` is the total XRP escrowed into the channel, and `sfBalance` on the channel SLE (`slep`) is the cumulative amount already paid out to the destination. The difference is the unspent portion that must be returned to the source. The immediately preceding `XRPL_ASSERT` enforces the invariant that `sfAmount >= sfBalance` — a channel balance can never exceed the channel's total funding. If this assertion were violated it would indicate ledger corruption, making it a justified hard check rather than recoverable error handling. + +**4. Decrement the source's owner count and erase the channel object.** +`adjustOwnerCount(view, sle, -1, j)` reduces the source's owner reservation by one, freeing up the XRP held in reserve for that object. Then `view.erase(slep)` removes the `ltPAYCHAN` entry from the ledger state entirely. + +## Error Handling and Failure Modes + +`closeChannel` can return three codes: + +- `tesSUCCESS` — normal path. +- `tefBAD_LEDGER` — owner directory removal failed; indicates corrupted ledger state. Both removal failure branches are marked `LCOV_EXCL_START/STOP`, signifying they should be unreachable during correct operation and are protected only as a defensive backstop. +- `tefINTERNAL` — the source account SLE could not be found via `view.peek`. Also marked as a theoretically unreachable branch; a payment channel cannot legitimately exist without its source account. + +The use of `tef` codes (transaction-execution fatal) rather than `tec` codes is deliberate: these are not conditions the user can correct by adjusting the transaction — they reflect unexpected ledger inconsistency. + +## Position in the Helpers Layer + +Compared to neighbors like `EscrowHelpers.h`, which contains complex templated logic for multi-asset escrow unlocking, `PaymentChannelHelpers.h` is intentionally minimal. Payment channels are XRP-only, so there is no asset type dispatch, no transfer-rate computation, and no trust-line creation to reason about. The helpers layer exists precisely to avoid embedding these shared operations inside the transactor classes, keeping each transactor focused on authorization and validation rather than raw ledger manipulation. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h.ai.json b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h.ai.json new file mode 100644 index 0000000000..3b5a0f361b --- /dev/null +++ b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h.ai.json @@ -0,0 +1,59 @@ +{ + "args": [ + { + "lineno": 7, + "name": "view" + }, + { + "lineno": 7, + "name": "account" + }, + { + "lineno": 7, + "name": "domainID" + }, + { + "lineno": 11, + "name": "offerID" + }, + { + "lineno": 14, + "name": "j" + } + ], + "classes": [], + "description": "This file provides helper functions to check if an account or offer is within a specified permissioned domain in the XRPL ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h", + "functions": [ + { + "args": [ + "view", + "account", + "domainID" + ], + "lineno": 7, + "name": "accountInDomain" + }, + { + "args": [ + "view", + "offerID", + "domainID", + "j" + ], + "lineno": 11, + "name": "offerInDomain" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "permissioned_dex" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h.ai.md b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h.ai.md new file mode 100644 index 0000000000..ae22fc3e72 --- /dev/null +++ b/include/xrpl/ledger/helpers/PermissionedDEXHelpers.h.ai.md @@ -0,0 +1,43 @@ +# `PermissionedDEXHelpers.h` — Domain Membership Gating for the Permissioned DEX + +This header declares two authorization predicates within the `xrpl::permissioned_dex` namespace that enforce membership rules for XRPL's Permissioned DEX feature. The Permissioned DEX allows a domain owner to create a restricted order book accessible only to accounts holding specific, non-expired credentials. These two functions are the sole gatekeepers for that access check, called from both transaction preclaim logic and live order-book traversal. + +## The Two Predicates + +### `accountInDomain` + +```cpp +[[nodiscard]] bool +accountInDomain(ReadView const& view, AccountID const& account, Domain const& domainID); +``` + +This function resolves a `PermissionedDomain` ledger object by its `domainID`, then tests whether `account` qualifies as a member. The logic has two tiers. First, the domain's `sfOwner` is always considered a member — this avoids a bootstrap problem where an owner couldn't trade in their own domain. Second, for all other accounts, the function iterates the domain's `sfAcceptedCredentials` array and searches for any credential issued to `account` that (a) bears the `lsfAccepted` flag and (b) has not expired according to `credentials::checkExpired` evaluated against the ledger's `parentCloseTime`. If any single credential satisfies both conditions, the account is in-domain. If the domain object itself doesn't exist, the function returns `false` immediately. + +Using `parentCloseTime` (the close time of the parent ledger) rather than wall time is deliberate: all XRPL validation is deterministic, and the "current" time for expiry evaluation must be a consensus-agreed timestamp, not a local clock. + +### `offerInDomain` + +```cpp +[[nodiscard]] bool +offerInDomain(ReadView const& view, uint256 const& offerID, Domain const& domainID, beast::Journal j); +``` + +This variant checks whether a specific offer object is still legitimately part of a permissioned domain. It is called during order-book traversal in `OfferStream` — not at offer-creation time, but at the moment the order book is being walked to fill a trade. The function reads the offer SLE, verifies that `sfDomainID` is present and matches the expected domain, then delegates the ultimate membership check to `accountInDomain` for the offer's owner. + +Several defensive checks guard against invariants that should never be violated by well-formed order books (offer missing, no `sfDomainID` field, mismatched domain ID). These paths are all marked `LCOV_EXCL_LINE` — the comment in the implementation explicitly labels them as impossible under normal operation but retained as safety nets. A separate check enforces consistency on hybrid offers: if `lsfHybrid` is set but `sfAdditionalBooks` is absent, it logs an error via `beast::Journal` and returns `false`. + +## Why a Separate Namespace + +Both functions live in `xrpl::permissioned_dex` rather than the broader `xrpl::credentials` namespace. The credential namespace handles the general-purpose credential lifecycle (expiry, deletion, DepositPreauth authorization). The permissioned DEX feature has a narrower, domain-centric contract: "is this account or offer currently valid within this specific domain?" Keeping that logic isolated reduces cognitive load for callers and makes it clear which subsystem is being invoked. + +## Callers and Enforcement Points + +The functions are used at two distinct enforcement stages: + +**Transaction preclaim** (`OfferCreate.cpp`, `Payment.cpp`) calls `accountInDomain` to reject transactions before they are applied. If the sender places a domain-scoped offer (`sfDomainID` present), `OfferCreate` verifies they are in that domain and returns `tecNO_PERMISSION` if not. For domain-scoped payments, `Payment.cpp` checks both the sender and the destination, since a domain payment requires both parties to be members. + +**Order-book consumption** (`OfferStream.cpp`) calls `offerInDomain` during path-finding traversal to handle the race between offer creation and credential expiry. An offer created by an account with valid credentials might still sit in the book after those credentials expire. When `OfferStream` encounters a domain-tagged offer, it calls `offerInDomain` to re-validate the owner's current membership. If the check fails, the offer is immediately removed from the book (`permRmOffer`) rather than matched. This is the critical correctness guarantee: domain boundaries remain enforced across the ledger's entire lifetime, not just at the moment of offer placement. + +## `[[nodiscard]]` as a Safety Invariant + +Both declarations carry `[[nodiscard]]`, making it a compile-time error to call either function without consuming the boolean result. Because these functions are security gates — silently ignoring a `false` would allow unauthorized trades — the attribute converts a class of potential audit failures into hard build failures. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h.ai.json b/include/xrpl/ledger/helpers/RippleStateHelpers.h.ai.json new file mode 100644 index 0000000000..3ef6b1538e --- /dev/null +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h.ai.json @@ -0,0 +1,382 @@ +{ + "args": [ + { + "lineno": 25, + "name": "view" + }, + { + "lineno": 25, + "name": "account" + }, + { + "lineno": 25, + "name": "issuer" + }, + { + "lineno": 25, + "name": "currency" + }, + { + "lineno": 32, + "name": "v" + }, + { + "lineno": 32, + "name": "acc" + }, + { + "lineno": 32, + "name": "iss" + }, + { + "lineno": 32, + "name": "cur" + }, + { + "lineno": 104, + "name": "bSrcHigh" + }, + { + "lineno": 104, + "name": "uSrcAccountID" + }, + { + "lineno": 104, + "name": "uDstAccountID" + }, + { + "lineno": 104, + "name": "uIndex" + }, + { + "lineno": 104, + "name": "sleAccount" + }, + { + "lineno": 104, + "name": "bAuth" + }, + { + "lineno": 104, + "name": "bNoRipple" + }, + { + "lineno": 104, + "name": "bFreeze" + }, + { + "lineno": 104, + "name": "bDeepFreeze" + }, + { + "lineno": 104, + "name": "saBalance" + }, + { + "lineno": 104, + "name": "saLimit" + }, + { + "lineno": 104, + "name": "uQualityIn" + }, + { + "lineno": 104, + "name": "uQualityOut" + }, + { + "lineno": 104, + "name": "j" + }, + { + "lineno": 120, + "name": "sleRippleState" + }, + { + "lineno": 120, + "name": "uLowAccountID" + }, + { + "lineno": 120, + "name": "uHighAccountID" + }, + { + "lineno": 128, + "name": "amount" + }, + { + "lineno": 128, + "name": "issue" + }, + { + "lineno": 146, + "name": "authType" + }, + { + "lineno": 167, + "name": "from" + }, + { + "lineno": 167, + "name": "to" + }, + { + "lineno": 176, + "name": "accountID" + }, + { + "lineno": 176, + "name": "priorBalance" + }, + { + "lineno": 176, + "name": "journal" + }, + { + "lineno": 191, + "name": "sleState" + }, + { + "lineno": 191, + "name": "ammAccountID" + }, + { + "lineno": 201, + "name": "sleMPT" + } + ], + "classes": [], + "description": "This file provides helper functions for managing RippleState (trustline) ledger entries in the XRPL, including credit limits, balances, freeze checks, trustline creation/deletion, IOU issuance/redemption, authorization checks, and AMM trustline/token operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/RippleStateHelpers.h", + "functions": [ + { + "args": [ + "view", + "account", + "issuer", + "currency" + ], + "lineno": 25, + "name": "creditLimit" + }, + { + "args": [ + "v", + "acc", + "iss", + "cur" + ], + "lineno": 32, + "name": "creditLimit2" + }, + { + "args": [ + "view", + "account", + "issuer", + "currency" + ], + "lineno": 39, + "name": "creditBalance" + }, + { + "args": [ + "view", + "account", + "currency", + "issuer" + ], + "lineno": 48, + "name": "isIndividualFrozen" + }, + { + "args": [ + "view", + "account", + "issue" + ], + "lineno": 54, + "name": "isIndividualFrozen" + }, + { + "args": [ + "view", + "account", + "currency", + "issuer" + ], + "lineno": 59, + "name": "isFrozen" + }, + { + "args": [ + "view", + "account", + "issue" + ], + "lineno": 65, + "name": "isFrozen" + }, + { + "args": [ + "view", + "account", + "issue", + "depth" + ], + "lineno": 71, + "name": "isFrozen" + }, + { + "args": [ + "view", + "account", + "currency", + "issuer" + ], + "lineno": 77, + "name": "isDeepFrozen" + }, + { + "args": [ + "view", + "account", + "issue", + "int" + ], + "lineno": 83, + "name": "isDeepFrozen" + }, + { + "args": [ + "view", + "account", + "issue" + ], + "lineno": 89, + "name": "checkDeepFrozen" + }, + { + "args": [ + "view", + "bSrcHigh", + "uSrcAccountID", + "uDstAccountID", + "uIndex", + "sleAccount", + "bAuth", + "bNoRipple", + "bFreeze", + "bDeepFreeze", + "saBalance", + "saLimit", + "uQualityIn", + "uQualityOut", + "j" + ], + "lineno": 104, + "name": "trustCreate" + }, + { + "args": [ + "view", + "sleRippleState", + "uLowAccountID", + "uHighAccountID", + "j" + ], + "lineno": 120, + "name": "trustDelete" + }, + { + "args": [ + "view", + "account", + "amount", + "issue", + "j" + ], + "lineno": 128, + "name": "issueIOU" + }, + { + "args": [ + "view", + "account", + "amount", + "issue", + "j" + ], + "lineno": 135, + "name": "redeemIOU" + }, + { + "args": [ + "view", + "issue", + "account", + "authType" + ], + "lineno": 146, + "name": "requireAuth" + }, + { + "args": [ + "view", + "issue", + "from", + "to" + ], + "lineno": 167, + "name": "canTransfer" + }, + { + "args": [ + "view", + "accountID", + "priorBalance", + "issue", + "journal" + ], + "lineno": 176, + "name": "addEmptyHolding" + }, + { + "args": [ + "view", + "accountID", + "issue", + "journal" + ], + "lineno": 183, + "name": "removeEmptyHolding" + }, + { + "args": [ + "view", + "sleState", + "ammAccountID", + "j" + ], + "lineno": 191, + "name": "deleteAMMTrustLine" + }, + { + "args": [ + "view", + "sleMPT", + "ammAccountID", + "j" + ], + "lineno": 201, + "name": "deleteAMMMPToken" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/RippleStateHelpers.h.ai.md b/include/xrpl/ledger/helpers/RippleStateHelpers.h.ai.md new file mode 100644 index 0000000000..b7306ece14 --- /dev/null +++ b/include/xrpl/ledger/helpers/RippleStateHelpers.h.ai.md @@ -0,0 +1,63 @@ +# `RippleStateHelpers.h` — IOU Trustline Operations + +## Role in the System + +`RippleStateHelpers.h` is the IOU-specific half of the XRPL's token helper layer. It declares (and `RippleStateHelpers.cpp` implements) every ledger operation that touches a `RippleState` (trustline) object: reading credit limits and balances, checking freeze status, creating and deleting lines, issuing and redeeming IOUs, enforcing authorization, managing zero-balance "holding" lines, and cleaning up AMM-owned lines. + +The file sits alongside `TokenHelpers.h` in `include/xrpl/ledger/helpers/`. `TokenHelpers.h` owns the asset-agnostic dispatcher layer — its `accountSend`, `isFrozen`, `requireAuth`, and related functions branch on whether the underlying asset is an `Issue` or an `MPTIssue` and then call into the IOU-specific functions declared here. `RippleStateHelpers.h` is therefore the leaf implementation for IOU paths; callers should generally go through the `TokenHelpers` dispatchers unless they are explicitly working at the IOU level. + +## Credit Querying + +`creditLimit` reads the `sfLowLimit` or `sfHighLimit` field of a `RippleState` SLE depending on whether `account < issuer` in raw account ID ordering. This low/high ordering is a ledger-wide invariant: the `RippleState` object always stores the "low" account's limit in `sfLowLimit`. Failure to apply this flip would silently return the wrong account's limit. The function normalises the result by rewriting the issuer field of the returned `STAmount` to `account`, making it safe to consume without knowing the binary ordering. `creditLimit2` is a thin convenience wrapper that casts the result to `IOUAmount`. + +`creditBalance` applies the same low/high normalisation in reverse: `sfBalance` is always stored in "low account sends to high account" orientation, so when the queried `account` is the high side, the balance is negated before being returned. This means callers always receive a balance expressed as "how much of this currency does `account` hold", regardless of which side of the line they sit on. + +## Freeze Semantics + +Three distinct freeze levels are modelled: + +- **`isIndividualFrozen`** checks only the issuer's side of the trustline flag (`lsfLowFreeze` or `lsfHighFreeze` depending on orientation). It does not check the issuer's global freeze flag, making it suitable for determining whether a specific line has been individually targeted. + +- **`isFrozen`** extends this by also consulting the issuer's `AccountRoot` for `lsfGlobalFreeze`. A globally frozen issuer renders every one of their lines frozen, regardless of individual line flags. This is the function that payment paths use to block movement. + +- **`isDeepFrozen`** checks `lsfHighDeepFreeze`/`lsfLowDeepFreeze` flags, which represent a stricter freeze that prevents the account from both sending and receiving the currency — a stronger condition than the ordinary freeze, which only prevents outbound transfers. Notably, it returns `false` if `issuer == account`, since an issuer cannot deep-freeze their own balance with themselves. + +The `isFrozen` overloads accepting a `depth` parameter exist purely for interface uniformity with the MPT equivalents in `TokenHelpers.h`, where vault-level recursion can require checking multiple layers. For IOUs the depth is unconditionally ignored. Similarly, `isDeepFrozen` has a defaulted `int depth = 0` overload for the same reason — the `Asset`-based dispatcher in `TokenHelpers.h` can forward a depth without needing to know the concrete type. + +`checkDeepFrozen` converts the boolean into a `TER` inline, returning `tecFROZEN` or `tesSUCCESS`, as a convenience for transactor preflight code. + +## Trust Line Lifecycle + +### `trustCreate` + +This is the lowest-level entry point for creating a new `RippleState` object and is invoked both directly (e.g., from `TrustSet` transactors) and indirectly from `issueIOU` when a line doesn't already exist. The function takes a `bSrcHigh` flag that tells it which of the two accounts occupies the "high" slot, and it derives `uLowAccountID`/`uHighAccountID` from that. All fields — limits, quality in/out, and the initial balance — are written with side-aware field selectors (`sfLowLimit`/`sfHighLimit`, etc.). + +A subtle design point: the caller supplies both `sleAccount` (the account that is being configured — i.e., whose limit and flags are being set) and the `uIndex` of the pre-calculated keylet. The function inserts the SLE into both accounts' owner directories and returns `tecDIR_FULL` if either directory is at capacity. It also sets the peer's `lsfNoRipple` flag on the new line if the peer account does not have `lsfDefaultRipple` enabled, enforcing the rule that the noRipple default is opt-in. + +### `trustDelete` + +Removes the RippleState SLE after removing directory backlinks from both the low and high owner directories via `view.dirRemove()`. The deletion hints (`sfLowNode`, `sfHighNode`) stored inside the SLE itself avoid a directory traversal lookup. Returns `tefBAD_LEDGER` if either removal fails, which would indicate a corrupt ledger. + +## IOU Issuance and Redemption + +`issueIOU` adjusts the trustline balance from the issuer's perspective and calls the internal `updateTrustLine` helper to handle the automatic cleanup case: if the sending side's balance crosses zero, its reserve was previously required but may now be unnecessary, and the line may be deleted entirely. If the trustline does not yet exist, `issueIOU` creates it via `trustCreate`, picking up the receiver's `lsfDefaultRipple` setting to determine the `noRipple` initial state. + +`redeemIOU` mirrors `issueIOU` but is called when a holder sends currency back toward the issuer. The critical asymmetry: unlike `issueIOU`, `redeemIOU` treats a missing trust line as a fatal internal error (`tefINTERNAL`) because you cannot redeem a balance on a line that doesn't exist. Both functions call `view.creditHookIOU()` after mutating the balance, which allows the `ApplyView` layer to observe the credit event for accounting or hook purposes. + +## Authorization and Transfer Checks + +`requireAuth` implements three distinct authorization modes via `AuthType`: + +- **`StrongAuth`**: If the trust line does not exist, return `tecNO_LINE` immediately. If it does exist and the issuer has `lsfRequireAuth` set, it must also have the corresponding auth flag on the line. This is used in contexts where you must guarantee the line exists before proceeding. + +- **`WeakAuth`** and **`Legacy`** (equivalent for IOUs): If `lsfRequireAuth` is set on the issuer and the trust line exists but is not authorized, return `tecNO_AUTH`. If the line doesn't exist and auth is required, return `tecNO_LINE`. But if `lsfRequireAuth` is not set, always return `tesSUCCESS` even if no line exists — appropriate for read-side checks during payment path finding where a line might be created on the fly. + +`canTransfer` enforces the rippling rule: a transfer between two non-issuer accounts is blocked only when *both* sides of their respective trust lines with the issuer have `lsfNoRipple` set. If either side allows rippling, or if either account is the issuer, the transfer is permitted. When a trust line doesn't exist for an account, the function falls back to the issuer's `lsfDefaultRipple` flag as the rippling preference. + +## Empty Holdings + +`addEmptyHolding` and `removeEmptyHolding` manage zero-balance trust lines, primarily used when a transactor (e.g., a DEX limit order) needs to guarantee a destination line exists before funds arrive, without any balance changing hands yet. The comment in the header makes the pairing contract explicit: any transactor calling `addEmptyHolding()` in `doApply` must call `canAddHolding()` (declared in `TokenHelpers.h`) in `preflight`. `addEmptyHolding` checks that the destination can afford the trust line reserve before calling `trustCreate`. `removeEmptyHolding` validates that the balance is actually zero before deleting, returning `tecHAS_OBLIGATIONS` if it is not. + +## AMM-Specific Cleanup + +`deleteAMMTrustLine` and `deleteAMMMPToken` are called during AMM pool withdrawal. `deleteAMMTrustLine` validates preconditions rigorously: the SLE must be an `ltRIPPLE_STATE`, exactly one of its two sides must be an AMM account (checked by the presence of `sfAMMID` in the `AccountRoot`), and if an `ammAccountID` is supplied, it must match one of the two sides. After calling `trustDelete`, it decrements the non-AMM side's owner count. `deleteAMMMPToken` handles the MPT-based equivalent by removing the `MPToken` SLE from the AMM account's owner directory and erasing it. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h.ai.json b/include/xrpl/ledger/helpers/TokenHelpers.h.ai.json new file mode 100644 index 0000000000..5dd7d79d2c --- /dev/null +++ b/include/xrpl/ledger/helpers/TokenHelpers.h.ai.json @@ -0,0 +1,434 @@ +{ + "args": [ + { + "lineno": 38, + "name": "view" + }, + { + "lineno": 38, + "name": "asset" + }, + { + "lineno": 41, + "name": "account" + }, + { + "lineno": 48, + "name": "depth" + }, + { + "lineno": 54, + "name": "issue" + }, + { + "lineno": 57, + "name": "mptIssue" + }, + { + "lineno": 63, + "name": "accounts" + }, + { + "lineno": 191, + "name": "from" + }, + { + "lineno": 191, + "name": "to" + }, + { + "lineno": 202, + "name": "uSenderID" + }, + { + "lineno": 202, + "name": "uReceiverID" + }, + { + "lineno": 202, + "name": "saAmount" + }, + { + "lineno": 202, + "name": "bCheckIssuer" + }, + { + "lineno": 104, + "name": "j" + }, + { + "lineno": 104, + "name": "zeroIfFrozen" + }, + { + "lineno": 104, + "name": "includeFullBalance" + }, + { + "lineno": 104, + "name": "currency" + }, + { + "lineno": 104, + "name": "issuer" + }, + { + "lineno": 122, + "name": "zeroIfUnauthorized" + }, + { + "lineno": 143, + "name": "id" + }, + { + "lineno": 143, + "name": "saDefault" + }, + { + "lineno": 143, + "name": "freezeHandling" + }, + { + "lineno": 151, + "name": "authHandling" + }, + { + "lineno": 159, + "name": "amount" + }, + { + "lineno": 170, + "name": "accountID" + }, + { + "lineno": 170, + "name": "priorBalance" + }, + { + "lineno": 170, + "name": "journal" + }, + { + "lineno": 185, + "name": "authType" + }, + { + "lineno": 222, + "name": "senderID" + }, + { + "lineno": 222, + "name": "receivers" + }, + { + "lineno": 210, + "name": "waiveFee" + }, + { + "lineno": 210, + "name": "allowOverflow" + } + ], + "classes": [], + "description": "This file provides helper functions and enums for handling token balances, freezes, authorizations, and transfers in the XRPL ledger, supporting both IOU and MPT (Multi-Party Token) assets.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/TokenHelpers.h", + "functions": [ + { + "args": [ + "view", + "asset" + ], + "lineno": 38, + "name": "isGlobalFrozen" + }, + { + "args": [ + "view", + "account", + "asset" + ], + "lineno": 41, + "name": "isIndividualFrozen" + }, + { + "args": [ + "view", + "account", + "asset", + "depth" + ], + "lineno": 48, + "name": "isFrozen" + }, + { + "args": [ + "view", + "account", + "issue" + ], + "lineno": 54, + "name": "checkFrozen" + }, + { + "args": [ + "view", + "account", + "mptIssue" + ], + "lineno": 57, + "name": "checkFrozen" + }, + { + "args": [ + "view", + "account", + "asset" + ], + "lineno": 60, + "name": "checkFrozen" + }, + { + "args": [ + "view", + "accounts", + "issue" + ], + "lineno": 63, + "name": "isAnyFrozen" + }, + { + "args": [ + "view", + "accounts", + "asset", + "depth" + ], + "lineno": 69, + "name": "isAnyFrozen" + }, + { + "args": [ + "view", + "account", + "mptIssue", + "depth" + ], + "lineno": 76, + "name": "isDeepFrozen" + }, + { + "args": [ + "view", + "account", + "asset", + "depth" + ], + "lineno": 84, + "name": "isDeepFrozen" + }, + { + "args": [ + "view", + "account", + "mptIssue" + ], + "lineno": 91, + "name": "checkDeepFrozen" + }, + { + "args": [ + "view", + "account", + "asset" + ], + "lineno": 94, + "name": "checkDeepFrozen" + }, + { + "args": [ + "view", + "account", + "currency", + "issuer", + "zeroIfFrozen", + "j", + "includeFullBalance" + ], + "lineno": 104, + "name": "accountHolds" + }, + { + "args": [ + "view", + "account", + "issue", + "zeroIfFrozen", + "j", + "includeFullBalance" + ], + "lineno": 113, + "name": "accountHolds" + }, + { + "args": [ + "view", + "account", + "mptIssue", + "zeroIfFrozen", + "zeroIfUnauthorized", + "j", + "includeFullBalance" + ], + "lineno": 122, + "name": "accountHolds" + }, + { + "args": [ + "view", + "account", + "asset", + "zeroIfFrozen", + "zeroIfUnauthorized", + "j", + "includeFullBalance" + ], + "lineno": 132, + "name": "accountHolds" + }, + { + "args": [ + "view", + "id", + "saDefault", + "freezeHandling", + "j" + ], + "lineno": 143, + "name": "accountFunds" + }, + { + "args": [ + "view", + "id", + "saDefault", + "freezeHandling", + "authHandling", + "j" + ], + "lineno": 151, + "name": "accountFunds" + }, + { + "args": [ + "view", + "amount" + ], + "lineno": 159, + "name": "transferRate" + }, + { + "args": [ + "view", + "asset" + ], + "lineno": 167, + "name": "canAddHolding" + }, + { + "args": [ + "view", + "accountID", + "priorBalance", + "asset", + "journal" + ], + "lineno": 170, + "name": "addEmptyHolding" + }, + { + "args": [ + "view", + "accountID", + "asset", + "journal" + ], + "lineno": 177, + "name": "removeEmptyHolding" + }, + { + "args": [ + "view", + "asset", + "account", + "authType" + ], + "lineno": 185, + "name": "requireAuth" + }, + { + "args": [ + "view", + "asset", + "from", + "to" + ], + "lineno": 191, + "name": "canTransfer" + }, + { + "args": [ + "view", + "uSenderID", + "uReceiverID", + "saAmount", + "bCheckIssuer", + "j" + ], + "lineno": 202, + "name": "directSendNoFee" + }, + { + "args": [ + "view", + "from", + "to", + "saAmount", + "j", + "waiveFee", + "allowOverflow" + ], + "lineno": 210, + "name": "accountSend" + }, + { + "args": [ + "view", + "senderID", + "asset", + "receivers", + "j", + "waiveFee" + ], + "lineno": 222, + "name": "accountSendMulti" + }, + { + "args": [ + "view", + "from", + "to", + "amount", + "j" + ], + "lineno": 233, + "name": "transferXRP" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/TokenHelpers.h.ai.md b/include/xrpl/ledger/helpers/TokenHelpers.h.ai.md new file mode 100644 index 0000000000..debb973032 --- /dev/null +++ b/include/xrpl/ledger/helpers/TokenHelpers.h.ai.md @@ -0,0 +1,60 @@ +# `include/xrpl/ledger/helpers/TokenHelpers.h` + +## Role in the System + +This header is the unified dispatcher layer for all token operations that must work across XRPL's three asset classes: XRP, IOU (trust-line-based), and MPT (Multi-Party Token, introduced later). It sits between transaction-processing code that wants to be asset-agnostic and the two type-specific implementation modules: `RippleStateHelpers.h` for IOU trust lines, and `MPTokenHelpers.h` for `MPToken`/`MPTokenIssuance` objects. Callers pass an `Asset` — a `std::variant` — and this module dispatches via `std::visit` or `Asset::visit` to the correct lower-level function, returning consistent result types (`STAmount`, `TER`, `bool`) regardless of asset kind. + +The design follows the same "shotgun surgery" avoidance pattern used elsewhere in the helpers directory: adding a new asset type only requires extending the `Asset` variant and adding a branch in these dispatchers, not touching every call site. + +## Enum Vocabulary + +Five enums establish a precise vocabulary for policy decisions that otherwise degrade into `bool` parameters: + +- `FreezeHandling` (`fhIGNORE_FREEZE` / `fhZERO_IF_FROZEN`) — whether to treat a frozen balance as zero or as its actual value. A caller interested in "what is legally spendable" uses `fhZERO_IF_FROZEN`; a cleanup path that needs to see the real balance regardless uses `fhIGNORE_FREEZE`. +- `AuthHandling` (`ahIGNORE_AUTH` / `ahZERO_IF_UNAUTHORIZED`) — parallel to freeze handling but for MPT authorization. +- `SpendableHandling` (`shSIMPLE_BALANCE` / `shFULL_BALANCE`) — for IOUs, the "full" balance includes the opposite side's credit limit (so an account that has borrowed can still spend up to that limit), and for the issuer it returns the issuance's `MaximumAmount − OutstandingAmount`. +- `WaiveTransferFee : bool` — whether the transfer fee is skipped. Using `enum class` over raw `bool` prevents accidental transposition at call sites. +- `AllowMPTOverflow : bool` — MPT `OutstandingAmount` can temporarily exceed `MaximumAmount` during payment-engine routing (because the redeeming leg will cancel the overshoot); this flag allows that transient state. Direct sends (`directSendNoFee`) use `No`, while `accountSend` via the payment engine uses `Yes`. + +`AuthType` (`StrongAuth`, `WeakAuth`, `Legacy`) encodes a subtle three-way distinction: +- `StrongAuth` checks that the holding object itself exists before asking whether authorization is set. +- `WeakAuth` skips the existence check, returning success if authorization isn't required at all even when no holding exists. +- `Legacy` maps to `StrongAuth` for MPT and `WeakAuth` for IOU, preserving historical behavior at existing call sites. + +## Freeze Checking + +The freeze surface is deliberately layered. `isGlobalFrozen` checks the issuer's account-level flag that freezes all holders simultaneously. `isIndividualFrozen` checks the specific trust line or `MPToken` object for a single account. `isFrozen` combines both. `isAnyFrozen` accepts an initializer list so an offer's two sides (taker and maker) can be checked with one call. + +For MPT, "deep freeze" (`isDeepFrozen`, `checkDeepFrozen`) is conceptually identical to `isFrozen`, because MPT semantics prohibit frozen accounts from both sending and receiving. For IOU, deep freeze is a distinct state (the `lsfDeepFreeze` trust-line flag) where the account cannot send but can still receive. The separate function family avoids conflating these semantics despite the MPT no-op. + +Both `isFrozen` and `isDeepFrozen` carry a `depth` parameter for recursive vault checking. Vaults can hold MPT shares backed by other assets; if those assets are frozen the shares should be treated as frozen too. The recursion is bounded by `maxAssetCheckDepth` and is described as "purely defensive" — the ledger does not currently allow such nested vaults to be created, so depth > 0 should not occur in practice. + +`checkFrozen` converts the boolean result to a `TER`, but importantly returns `tecFROZEN` for IOU and `tecLOCKED` for MPT. These are distinct error codes with distinct meanings in protocol responses, so the dispatching `checkFrozen(…, Asset const&)` overload delegates to the typed overloads rather than performing its own mapping. + +## Balance Queries + +`accountHolds` answers "what can this account actually spend right now?" The four overloads form a funnel: the most specific (by `Currency`/`AccountID` pair) implements the real logic; the `Issue`, `MPTIssue`, and `Asset` overloads are all adapters. + +For XRP the answer is `xrpLiquid(…)` — the reserve-adjusted liquid balance. For IOU with `shFULL_BALANCE` and `account == issuer`, the function short-circuits to `STAmount::cMaxValue` because the issuer is always the counterparty and can issue unlimited amounts. For MPT issuers with `shFULL_BALANCE`, the available issuance capacity (`MaximumAmount − OutstandingAmount`) is returned. For regular MPT holders, the `MPToken` ledger object's `sfMPTAmount` field is read, then conditionally zeroed based on freeze and authorization state. The freeze check (`fhZERO_IF_FROZEN`) and auth check (`ahZERO_IF_UNAUTHORIZED`) are independent policy inputs, allowing callers to compose them. + +`accountFunds` differs from `accountHolds` in one key way: if the account *is* the IOU issuer, it returns `saDefault` directly (the amount requested), rather than an artificially large value. This is the correct semantic for offer matching — an issuer can always fund an offer for their own currency up to whatever amount they specify. The MPT overload of `accountFunds` delegates to `accountHolds` with `shFULL_BALANCE`. + +`transferRate` dispatches on the `STAmount`'s embedded `Asset`, returning the issuer's transfer fee for IOU or the `MPTokenIssuance`'s fee for MPT, uniformly as a `Rate` (parts-per-billion). + +## Holding Lifecycle + +`canAddHolding` / `addEmptyHolding` / `removeEmptyHolding` manage the creation and deletion of the ledger objects that record a token relationship — trust lines for IOU, `MPToken` objects for MPT. The comment in `RippleStateHelpers.h` makes the protocol explicit: any transactor that calls `addEmptyHolding` in `doApply` must call `canAddHolding` in `preflight`, since the preflight check may reject the transaction before the apply phase writes to the ledger. `canAddHolding` for IOU verifies that the issuer has `lsfDefaultRipple` set; for MPT it delegates to the MPT-specific check. + +## Authorization and Transfer Checks + +`requireAuth` tests whether the account holds the required authorization to interact with the asset, returning `tecNO_AUTH` or `tecNO_LINE` on failure. `canTransfer` tests whether a particular sender→receiver pair is permitted: for IOU it checks rippling flags; for MPT it checks `lsfMPTCanTransfer` and authorization of the destination account. + +## Money Transfer Functions + +`directSendNoFee` is the primitive for redemption and intra-issuer transfers. It is intentionally not marked `[[nodiscard]]` — the comment explains this is for `DirectStep.cpp` compatibility — distinguishing it from the higher-level functions that enforce result checking. + +`accountSend` is the main asset transfer entry point. It dispatches to `accountSendIOU` or `accountSendMPT` based on the `STAmount`'s asset type. Transfer fees are applied unless `WaiveTransferFee::Yes` is passed. The `AllowMPTOverflow` flag gates whether the MPT outstanding-amount overflow check uses the stricter `MaximumAmount` threshold or the relaxed `UINT64_MAX` threshold, matching the two-phase (issue then redeem) structure of the payment engine. + +`accountSendMulti` handles the case where one sender distributes the same asset to multiple recipients simultaneously — used by vault operations and similar batch contexts. Batching avoids repeated round-trips through the ledger state for the sender's balance and the issuance's outstanding-amount field. + +`transferXRP` is the primitive XRP send, kept separate because XRP has no trust lines, no transfer fees, and no authorization model; mixing it into the Asset-dispatch path would only add unnecessary overhead. \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h.ai.json b/include/xrpl/ledger/helpers/VaultHelpers.h.ai.json new file mode 100644 index 0000000000..aa60dd4c66 --- /dev/null +++ b/include/xrpl/ledger/helpers/VaultHelpers.h.ai.json @@ -0,0 +1,111 @@ +{ + "args": [ + { + "lineno": 19, + "name": "vault" + }, + { + "lineno": 20, + "name": "issuance" + }, + { + "lineno": 21, + "name": "assets" + }, + { + "lineno": 35, + "name": "vault" + }, + { + "lineno": 36, + "name": "issuance" + }, + { + "lineno": 37, + "name": "shares" + }, + { + "lineno": 52, + "name": "vault" + }, + { + "lineno": 53, + "name": "issuance" + }, + { + "lineno": 54, + "name": "assets" + }, + { + "lineno": 55, + "name": "truncate" + }, + { + "lineno": 70, + "name": "vault" + }, + { + "lineno": 71, + "name": "issuance" + }, + { + "lineno": 72, + "name": "shares" + } + ], + "classes": [ + { + "args": [], + "lineno": 44, + "name": "TruncateShares" + } + ], + "description": "This file provides helper functions for converting between assets and shares in the context of a vault, including deposit and withdrawal scenarios, with support for rounding or truncation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/ledger/helpers/VaultHelpers.h", + "functions": [ + { + "args": [ + "vault", + "issuance", + "assets" + ], + "lineno": 16, + "name": "assetsToSharesDeposit" + }, + { + "args": [ + "vault", + "issuance", + "shares" + ], + "lineno": 32, + "name": "sharesToAssetsDeposit" + }, + { + "args": [ + "vault", + "issuance", + "assets", + "truncate" + ], + "lineno": 49, + "name": "assetsToSharesWithdraw" + }, + { + "args": [ + "vault", + "issuance", + "shares" + ], + "lineno": 67, + "name": "sharesToAssetsWithdraw" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/ledger/helpers/VaultHelpers.h.ai.md b/include/xrpl/ledger/helpers/VaultHelpers.h.ai.md new file mode 100644 index 0000000000..8136334266 --- /dev/null +++ b/include/xrpl/ledger/helpers/VaultHelpers.h.ai.md @@ -0,0 +1,45 @@ +# `VaultHelpers.h` — Vault Asset-to-Share Conversion Utilities + +This header belongs to the XRPL ledger's Single-Sided Vault feature (XLS-65d). It provides four pure arithmetic functions that translate between the two token types a vault deals with at all times: the underlying *asset* (XRP, IOU, or MPT that depositors contribute) and the *shares* (an MPT issued by the vault representing proportional ownership). Because MPTokens are always integers, every function in this file must make a deliberate rounding decision — and those decisions differ between the deposit path and the withdrawal path in ways that protect the vault's solvency. + +## The Model: Proportional Vault Shares + +A vault accumulates assets deposited by its participants and mints share tokens proportionally. The on-ledger `Vault` SLE tracks three key numeric fields that all four functions depend on: + +- `sfAssetsTotal` — the total asset value committed to the vault (including assets currently lent out), +- `sfLossUnrealized` — unrealized losses not yet reflected in `sfAssetsTotal`, and +- `sfScale` — a `uint8` that sets the initial exchange ratio when the vault is bootstrapped from empty. + +The MPTokenIssuance SLE for the vault's shares provides `sfOutstandingAmount`, the total shares currently in circulation. + +The core invariant across all calculations is the proportional exchange: one share is worth `assetsTotal / sharesOutstanding` assets, or equivalently, depositing `x` assets against `assetsTotal` earns `x × sharesOutstanding / assetsTotal` shares. + +## Deposit Functions: Ignoring Unrealized Losses + +`assetsToSharesDeposit()` and `sharesToAssetsDeposit()` implement the deposit-direction conversions. Both read `sfAssetsTotal` directly, *without* subtracting `sfLossUnrealized`. This is intentional: a depositor pays into the vault based on its full committed assets, including those pledged to lending arrangements. Unrealized losses are a risk borne by existing shareholders, not one that new depositors should get a discount for. + +`assetsToSharesDeposit()` handles the empty-vault bootstrap case specially. When `sfAssetsTotal == 0`, no outstanding shares exist and there is no ratio to apply. Instead, the initial share price is established via `sfScale`: the result is `assets × 10^scale`, computed using mantissa/exponent arithmetic on `STAmount` before calling `.truncate()`. This keeps the initial share-to-asset ratio tunable at vault creation. The non-bootstrap path uses the proportion `(shareTotal × assets) / assetTotal`, always truncated because shares must be integral MPT values — depositors always receive a whole number of shares, never more than the assets strictly warrant. + +`sharesToAssetsDeposit()` is the inverse: given a whole number of shares, it computes the exact asset cost. The vault deposit transactor (`VaultDeposit::doApply()`) uses these two functions in sequence: first determine how many shares are created (truncated), then back-calculate the true asset cost from those shares. The safety check `if (*maybeAssets > amount) return tecINTERNAL` ensures the vault never extracts more assets than the depositor offered — a consequence of the truncation in the forward direction. + +## Withdrawal Functions: Accounting for Unrealized Losses + +`assetsToSharesWithdraw()` and `sharesToAssetsWithdraw()` differ from their deposit counterparts in one critical way: both subtract `sfLossUnrealized` from `sfAssetsTotal` before computing the exchange rate. If the vault has recorded unrealized losses — for instance from a lending arrangement gone underwater — withdrawers receive fewer assets per share, reflecting the actual net asset value. This prevents early withdrawers from exiting at inflated prices and passing losses entirely to those who remain. + +`assetsToSharesWithdraw()` takes an optional `TruncateShares` enum parameter (default `TruncateShares::no`). When the caller asks for a fixed asset withdrawal, it needs to know how many shares to redeem. With default rounding (nearest), the resulting share count may round up, ensuring the vault is never shortchanged. The vault withdraw transactor then back-calculates actual assets from that share count via `sharesToAssetsWithdraw()`, so the withdrawer receives a precise amount. The `TruncateShares::yes` variant exists for callers that explicitly want floor behavior rather than rounding. + +If `sfAssetsTotal - sfLossUnrealized` is zero, both withdraw functions return a zero-valued `STAmount` rather than dividing by zero. This gracefully handles a fully-insolvent vault. + +## The `TruncateShares` Enum + +The header defines a scoped `enum class TruncateShares : bool`. Using a named enum rather than a bare `bool` prevents the classic boolean parameter legibility problem — call sites read `assetsToSharesWithdraw(vault, issuance, amount, TruncateShares::yes)` unambiguously. This is the only configurable parameter; all other rounding decisions are fixed by the deposit/withdraw semantic. + +## Error Handling and `[[nodiscard]]` + +All four functions are marked `[[nodiscard]]` and return `std::optional`. They return `std::nullopt` on precondition violations: negative input amounts, or an asset type mismatch between the provided amount and what the vault expects. Both conditions are guarded by `XRPL_ASSERT` macros and a hard conditional check in release builds — the assertions communicate invariants to developers, while the `if`-guard + `return nullopt` provides deterministic error propagation rather than undefined behavior. Callers are expected to treat `nullopt` as an internal error (they emit `tecINTERNAL` in the transactors). + +Arithmetic can also throw `std::overflow_error` from XRPL's `Number` class, especially when `sfScale` is large. Both transactors catch this and map it to `tecPATH_DRY`, with a debug-level log to avoid flooding under adversarial input. + +## Relationship to Callers + +`VaultDeposit.cpp` and `VaultWithdraw.cpp` are the only callers. The deposit transactor uses the deposit-path pair; the withdraw transactor uses both withdraw-path functions together in a two-step computation. The functions are deliberately stateless and side-effect-free: they read from already-fetched `shared_ptr` objects and perform arithmetic only, leaving all ledger mutations to the transactors. This separation makes the rounding logic independently testable and reusable if additional vault transaction types are introduced. \ No newline at end of file diff --git a/include/xrpl/net/AutoSocket.h.ai.json b/include/xrpl/net/AutoSocket.h.ai.json new file mode 100644 index 0000000000..4e6e5683d5 --- /dev/null +++ b/include/xrpl/net/AutoSocket.h.ai.json @@ -0,0 +1,221 @@ +{ + "args": [ + { + "lineno": 23, + "name": "s" + }, + { + "lineno": 23, + "name": "c" + }, + { + "lineno": 23, + "name": "secureOnly" + }, + { + "lineno": 23, + "name": "plainOnly" + }, + { + "lineno": 31, + "name": "s" + }, + { + "lineno": 31, + "name": "c" + }, + { + "lineno": 61, + "name": "s" + } + ], + "classes": [ + { + "args": [ + "boost::asio::io_context& s", + "boost::asio::ssl::context& c", + "bool secureOnly", + "bool plainOnly" + ], + "lineno": 15, + "name": "AutoSocket" + } + ], + "description": "Defines the AutoSocket class, a socket wrapper supporting both SSL and non-SSL connections with asynchronous read/write and handshake operations, enabling auto-detection of connection type.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/net/AutoSocket.h", + "functions": [ + { + "args": [ + "boost::asio::io_context& s", + "boost::asio::ssl::context& c", + "bool secureOnly", + "bool plainOnly" + ], + "lineno": 23, + "name": "AutoSocket" + }, + { + "args": [ + "boost::asio::io_context& s", + "boost::asio::ssl::context& c" + ], + "lineno": 31, + "name": "AutoSocket" + }, + { + "args": [], + "lineno": 36, + "name": "isSecure" + }, + { + "args": [], + "lineno": 39, + "name": "SSLSocket" + }, + { + "args": [], + "lineno": 42, + "name": "PlainSocket" + }, + { + "args": [], + "lineno": 46, + "name": "local_endpoint" + }, + { + "args": [], + "lineno": 51, + "name": "remote_endpoint" + }, + { + "args": [], + "lineno": 56, + "name": "lowest_layer" + }, + { + "args": [ + "AutoSocket& s" + ], + "lineno": 61, + "name": "swap" + }, + { + "args": [ + "boost::system::error_code& ec" + ], + "lineno": 67, + "name": "cancel" + }, + { + "args": [ + "handshake_type type", + "callback cbFunc" + ], + "lineno": 72, + "name": "async_handshake" + }, + { + "args": [ + "ShutdownHandler handler" + ], + "lineno": 93, + "name": "async_shutdown" + }, + { + "args": [ + "Seq const& buffers", + "Handler handler" + ], + "lineno": 106, + "name": "async_read_some" + }, + { + "args": [ + "Seq const& buffers", + "Condition condition", + "Handler handler" + ], + "lineno": 113, + "name": "async_read_until" + }, + { + "args": [ + "boost::asio::basic_streambuf& buffers", + "std::string const& delim", + "Handler handler" + ], + "lineno": 120, + "name": "async_read_until" + }, + { + "args": [ + "boost::asio::basic_streambuf& buffers", + "MatchCondition cond", + "Handler handler" + ], + "lineno": 127, + "name": "async_read_until" + }, + { + "args": [ + "Buf const& buffers", + "Handler handler" + ], + "lineno": 134, + "name": "async_write" + }, + { + "args": [ + "boost::asio::basic_streambuf& buffers", + "Handler handler" + ], + "lineno": 141, + "name": "async_write" + }, + { + "args": [ + "Buf const& buffers", + "Condition cond", + "Handler handler" + ], + "lineno": 148, + "name": "async_read" + }, + { + "args": [ + "boost::asio::basic_streambuf& buffers", + "Condition cond", + "Handler handler" + ], + "lineno": 155, + "name": "async_read" + }, + { + "args": [ + "Buf const& buffers", + "Handler handler" + ], + "lineno": 162, + "name": "async_read" + }, + { + "args": [ + "Seq const& buffers", + "Handler handler" + ], + "lineno": 169, + "name": "async_write_some" + }, + { + "args": [ + "callback cbFunc", + "error_code const& ec", + "size_t bytesTransferred" + ], + "lineno": 176, + "name": "handle_autodetect" + } + ], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/net/AutoSocket.h.ai.md b/include/xrpl/net/AutoSocket.h.ai.md new file mode 100644 index 0000000000..a876444931 --- /dev/null +++ b/include/xrpl/net/AutoSocket.h.ai.md @@ -0,0 +1,51 @@ +# `AutoSocket.h` — Protocol-Agnostic TCP Socket Wrapper + +`AutoSocket` exists to solve a practical deployment problem: XRPL nodes must accept incoming connections that may be either TLS-encrypted or plaintext without requiring the remote peer to declare its protocol upfront. Rather than running two separate listening ports or mandating coordinated configuration between peers, `AutoSocket` wraps a single `boost::asio::ssl::stream` and defers the SSL/plain decision to connection time, with an optional byte-sniffing auto-detection path. Its only known consumer in the codebase is `HTTPClient.cpp`, where it provides the transport layer for outbound HTTP/HTTPS client requests. + +## Core Design: Always Allocate the SSL Socket + +The central structural decision is that the `ssl_socket` — a `boost::asio::ssl::stream` — is always heap-allocated via `mSocket` (`std::unique_ptr`), regardless of whether the connection ultimately turns out to be plaintext. The plain TCP socket is the `next_layer_type` of the SSL stream, so it is always present as an embedded sub-object. There is no variant type, no polymorphic pointer hierarchy, and no conditional allocation based on mode. The `mSecure` boolean records the final protocol determination, and every subsequent I/O method (`async_read`, `async_write`, `async_read_some`, etc.) simply branches on it to dispatch to either `*mSocket` (SSL path) or `PlainSocket()` (the raw `next_layer()`). + +This is deliberately simple. Once the handshake phase resolves the mode, the socket behaves uniformly for its remaining lifetime. The cost is that every connection — even definitely-plain ones — carries the overhead of a full `ssl_socket` allocation. + +## Three Construction Modes + +The primary constructor accepts `secureOnly` and `plainOnly` flags that control both `mSecure` and the size of `mBuffer`: + +- **`secureOnly = true`**: `mSecure` is initialized to `true`, `mBuffer` is empty (size 0). SSL is assumed immediately; no byte-sniffing occurs. +- **`plainOnly = true`**: `mSecure` is left `false`, `mBuffer` is empty (size 0). The plaintext path is assumed immediately; no peek occurs. +- **Both false (default)**: `mSecure` starts `false`, `mBuffer` is allocated to 4 bytes. Auto-detection will be performed during `async_handshake`. + +The convenience constructor `AutoSocket(io_context&, ssl::context&)` delegates to the primary with both flags false, enabling auto-detection. + +## The `async_handshake` Decision Tree + +`async_handshake` is the central branching point, and its logic encodes all three modes: + +1. **Client role or already-secure**: If `type == ssl_socket::client` or `mSecure` is already `true`, SSL is set unconditionally and `mSocket->async_handshake` is called directly. Clients always know whether they intend to use TLS. + +2. **Empty buffer (forced-plain or forced-secure construction)**: A zero-size `mBuffer` means either `plainOnly` or `secureOnly` was set. In the plain case, `mSecure` is `false` and the handler is posted immediately with a success `error_code` via `boost::beast::bind_handler`. This lets callers call `async_handshake` uniformly without knowing the mode, and they will always receive an asynchronous callback. + +3. **Auto-detect**: A 4-byte buffer is present. The code issues an `async_receive` on the plain socket layer with `message_peek` — this reads bytes from the kernel's receive buffer without consuming them, so they remain available for subsequent reads (including the TLS `ClientHello` parser if SSL is detected). The result is dispatched to `handle_autodetect`. + +## The Auto-Detection Heuristic + +`handle_autodetect` applies a byte-range test: if all received bytes fall in the printable ASCII range (32–126 inclusive), the connection is classified as plaintext. If any byte falls outside that range, SSL is assumed and `mSocket->async_handshake(ssl_socket::server, cbFunc)` is issued. + +The heuristic is reliable in practice because TLS `ClientHello` records begin with byte `0x16` (22), which is a control character well below the printable range. Text-based protocols like HTTP begin with uppercase ASCII letters (`GET`, `POST`, `HTTP`), all in range. The 4-byte sample is large enough to distinguish these with high confidence while minimizing the peek overhead. + +A subtle correctness point: since `message_peek` does not consume the bytes, the peeked data remains in the kernel buffer. When the TLS handshake reader subsequently processes the socket, it will see the full `ClientHello` starting from byte 0 — nothing is lost. + +## Async Shutdown Asymmetry + +`async_shutdown` reveals an important asymmetry between the two modes. TLS shutdown is a protocol-level exchange (`ssl::stream::async_shutdown`) and is genuinely asynchronous. TCP shutdown for plain sockets is synchronous and immediate (`shutdown_both`). Rather than expose two different calling conventions, the plain path performs the synchronous `shutdown_both`, catches any `boost::system::system_error` into an `error_code`, and then posts the handler to the executor using `bind_handler`. This preserves the invariant — critical for correctness in Boost.Asio code — that completion handlers are never called synchronously from within an initiating function call. + +## Resource Management and Swap + +`mSocket` is owned exclusively via `std::unique_ptr`, giving `AutoSocket` clear sole ownership with no shared state. `swap()` provides a no-throw exchange of all members (`mBuffer`, `mSocket`, `mSecure`), useful for connection-accepting code that needs to transfer socket ownership to a session object. There is no copy constructor or copy assignment, consistent with unique resource ownership. + +The `j_` journal member is initialized to `beast::Journal::getNullSink()`, making all logging in `handle_autodetect` effectively a no-op by default. This reflects the class's age — it predates more systematic logger injection — and means the `JLOG` trace and warn calls in `handle_autodetect` are silent unless a caller explicitly wires in a real journal sink. + +## Notable Limitations + +There is no timeout on the auto-detection peek. A client that connects but sends nothing will hold the `async_receive` open indefinitely, preventing completion of `async_handshake`. Callers are responsible for imposing connection-level timeouts externally (e.g., via a `steady_timer`). Similarly, once `mSecure` is determined, there is no re-negotiation mechanism — the protocol mode is fixed for the socket's lifetime. \ No newline at end of file diff --git a/include/xrpl/net/HTTPClient.h.ai.json b/include/xrpl/net/HTTPClient.h.ai.json new file mode 100644 index 0000000000..115e96f7a4 --- /dev/null +++ b/include/xrpl/net/HTTPClient.h.ai.json @@ -0,0 +1,86 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "HTTPClient" + } + ], + "description": "Provides an asynchronous HTTP client implementation with optional SSL for the XRPL project, including methods for HTTP GET and custom requests, as well as SSL context management.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/net/HTTPClient.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "HTTPClient" + }, + { + "args": [ + "sslVerifyDir", + "sslVerifyFile", + "sslVerify", + "j" + ], + "lineno": 18, + "name": "initializeSSLContext" + }, + { + "args": [], + "lineno": 31, + "name": "cleanupSSLContext" + }, + { + "args": [ + "bSSL", + "io_context", + "deqSites", + "port", + "strPath", + "responseMax", + "timeout", + "complete", + "j" + ], + "lineno": 44, + "name": "get" + }, + { + "args": [ + "bSSL", + "io_context", + "strSite", + "port", + "strPath", + "responseMax", + "timeout", + "complete", + "j" + ], + "lineno": 58, + "name": "get" + }, + { + "args": [ + "bSSL", + "io_context", + "strSite", + "port", + "build", + "responseMax", + "timeout", + "complete", + "j" + ], + "lineno": 72, + "name": "request" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/net/HTTPClient.h.ai.md b/include/xrpl/net/HTTPClient.h.ai.md new file mode 100644 index 0000000000..2cbd807f6e --- /dev/null +++ b/include/xrpl/net/HTTPClient.h.ai.md @@ -0,0 +1,49 @@ +# `include/xrpl/net/HTTPClient.h` + +## Role in the System + +`HTTPClient` is the XRPL node's outbound HTTP/HTTPS client, used whenever the server needs to fetch data from an external URL — notably for fetching the Unique Node List (UNL) and for issuing RPC webhook callbacks. It presents a narrow, purely static public interface over a fully asynchronous Boost.Asio pipeline, hiding all connection state inside a private implementation type. + +## Design: Static Factory over a Hidden `shared_ptr` Object + +The header declares only static methods; there is no public way to construct or own an `HTTPClient` instance. The real work happens in `HTTPClientImp`, defined entirely inside `HTTPClient.cpp`. Each call to `get()` or `request()` calls `std::make_shared(...)` and immediately invokes its request pipeline. The `shared_ptr` keeps the object alive for the duration of the async chain through idiomatic `shared_from_this()` captures — callers never hold a reference, so there is no lifetime management burden on the call site. + +This pattern — factory function, opaque internal class, `shared_from_this` for self-lifetime — is the standard Asio idiom for fire-and-forget async operations. The alternative of exposing the internal state through the header would make every caller responsible for keeping the connection object alive across dozens of async continuations, which is error-prone. + +## Global SSL Context + +A single `std::optional` lives as a module-level static in `HTTPClient.cpp`. `initializeSSLContext()` must be called once at startup before any requests are made; it constructs the `HTTPClientSSLContext`, which configures a `boost::asio::ssl::context` with optional CA directories or files. Each `HTTPClientImp` receives a reference to this context at construction time. + +`cleanupSSLContext()` resets the optional, releasing the OpenSSL resources. The inline documentation is explicit that this must not be called while requests are in flight, and notes it is only called from tests — in production the context is effectively process-scoped. The `maxClientHeaderBytes` constant (32 KB) caps the `mHeader` streambuf to prevent runaway header reads. + +## Async Pipeline + +`HTTPClientImp` drives an ordered handler chain: + +1. **`httpsNext()`** — arms the deadline timer, then initiates DNS resolution via `mResolver.async_resolve`. +2. **`handleResolve()`** — on success, calls `HTTPClientSSLContext::preConnectVerify()` to set the TLS SNI hostname, then issues `boost::asio::async_connect`. +3. **`handleConnect()`** — calls `postConnectVerify()` to configure peer-certificate verification callbacks (RFC 6125 hostname matching), then either begins a TLS handshake (`async_handshake`) for SSL connections or jumps directly to `handleRequest()` for plaintext. +4. **`handleRequest()`** — invokes the `mBuild` function, which populates the `mRequest` streambuf with the HTTP message, then calls `async_write`. +5. **`handleWrite()`** — reads until `\r\n\r\n` to capture the entire HTTP response header into `mHeader`. +6. **`handleHeader()`** — parses the status line and optional `Content-Length` header using `boost::regex`, enforces the `maxResponseSize_` limit, and either completes immediately (zero-length body) or issues `async_read` for the remaining body bytes. +7. **`handleData()`** — assembles the full body and calls `invokeComplete`. + +The `mShutdown` error code acts as a one-way latch: once set by a timeout, resolve failure, connect failure, or verification error, every subsequent handler short-circuits to `invokeComplete` without touching the network. This prevents the common async bug of partially executing a pipeline after a prior stage has already signalled failure. + +## Site Failover via `std::deque` + +The `get()` overload accepting `std::deque deqSites` exposes a built-in retry/fallback mechanism. After each attempt — success or failure — `invokeComplete()` pops the front of the deque and inspects the bool returned by the completion callback. If the callback returns `true` and sites remain in the deque, `httpsNext()` is called again with the next hostname. This allows callers to supply a ranked list of mirrors; the completion callback decides per-attempt whether to keep trying. + +The single-site `get()` and `request()` overloads simply wrap their hostname in a one-element deque before delegating to the same mechanism. + +## `AutoSocket` and SSL Transparency + +`AutoSocket` wraps a `boost::asio::ssl::stream` and routes every async operation — `async_read`, `async_write`, `async_handshake`, `async_shutdown` — to either the SSL stream or its inner plain TCP layer depending on a `mSecure` flag. `HTTPClientImp` uses only the SSL path for outbound requests (`async_handshake` is always called with `client` as the handshake type, which forces SSL). The non-SSL branch in `handleConnect` exists for plaintext HTTP when the caller passes `bSSL = false`, bypassing the handshake step while still using `AutoSocket`'s unified read/write API. + +## `request()` vs `get()` + +`request()` is the primitive: callers supply a `build` lambda that writes directly into a `boost::asio::streambuf`. `get()` delegates to `request()` by binding `HTTPClientImp::makeGet()` as the builder, which emits an HTTP/1.0 `GET` with `Connection: close`. Using HTTP/1.0 is intentional — each request tears down the connection after a single exchange, which suits one-shot external fetches and avoids persistent-connection management complexity. The `RPCCall.cpp` usage demonstrates `request()` directly to send POST bodies for webhook delivery. + +## Response Size Safety + +`responseMax` is the caller's hard limit on body bytes. In `handleHeader()`, the `Content-Length` value is parsed and compared against `maxResponseSize_`; if `Content-Length` exceeds the limit, the connection is aborted with `value_too_large`. If `Content-Length` is absent, `responseMax` is used directly as the read buffer size. This two-stage guard prevents a malicious or misbehaving server from causing unbounded memory allocation regardless of whether it announces a size. \ No newline at end of file diff --git a/include/xrpl/net/HTTPClientSSLContext.h.ai.json b/include/xrpl/net/HTTPClientSSLContext.h.ai.json new file mode 100644 index 0000000000..40adaa3e61 --- /dev/null +++ b/include/xrpl/net/HTTPClientSSLContext.h.ai.json @@ -0,0 +1,116 @@ +{ + "args": [ + { + "lineno": 12, + "name": "sslVerifyDir" + }, + { + "lineno": 13, + "name": "sslVerifyFile" + }, + { + "lineno": 14, + "name": "sslVerify" + }, + { + "lineno": 15, + "name": "j" + }, + { + "lineno": 16, + "name": "method" + }, + { + "lineno": 57, + "name": "strm" + }, + { + "lineno": 57, + "name": "host" + }, + { + "lineno": 81, + "name": "strm" + }, + { + "lineno": 81, + "name": "host" + }, + { + "lineno": 106, + "name": "domain" + }, + { + "lineno": 107, + "name": "preverified" + }, + { + "lineno": 108, + "name": "ctx" + }, + { + "lineno": 109, + "name": "j" + } + ], + "classes": [ + { + "args": [ + "sslVerifyDir", + "sslVerifyFile", + "sslVerify", + "j", + "method" + ], + "lineno": 9, + "name": "HTTPClientSSLContext" + } + ], + "description": "Defines the HTTPClientSSLContext class, which manages SSL context setup and verification for HTTP clients, including certificate verification and hostname validation, using Boost.Asio and custom logging.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/net/HTTPClientSSLContext.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "context" + }, + { + "args": [], + "lineno": 43, + "name": "sslVerify" + }, + { + "args": [ + "strm", + "host" + ], + "lineno": 56, + "name": "preConnectVerify" + }, + { + "args": [ + "strm", + "host" + ], + "lineno": 80, + "name": "postConnectVerify" + }, + { + "args": [ + "domain", + "preverified", + "ctx", + "j" + ], + "lineno": 104, + "name": "rfc6125_verify" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/net/HTTPClientSSLContext.h.ai.md b/include/xrpl/net/HTTPClientSSLContext.h.ai.md new file mode 100644 index 0000000000..a96658bc7f --- /dev/null +++ b/include/xrpl/net/HTTPClientSSLContext.h.ai.md @@ -0,0 +1,50 @@ +# `HTTPClientSSLContext.h` — SSL Context Management for Outbound HTTPS Connections + +`HTTPClientSSLContext` is a self-contained SSL lifecycle manager for outbound HTTP(S) client connections in the XRPL node. Its responsibility is twofold: configure a `boost::asio::ssl::context` with the correct certificate trust anchors at construction time, then orchestrate the correct sequencing of TLS verification setup across the two-phase connection lifecycle. + +## Why This Class Exists + +Boost.Asio SSL requires careful ordering of API calls around a TLS connection: some settings must be applied before the TCP connect, others must be applied after TCP connect but before the TLS handshake. Placing this logic in a single class prevents callers from accidentally applying these in the wrong order, and ensures that the same verification policy is consistently enforced across the two distinct consumers in the codebase. + +## Certificate Trust Sources + +The constructor resolves certificate trust anchors through a priority hierarchy: + +1. If `sslVerifyFile` is non-empty, it loads a specific PEM CA bundle via `ssl_context_.load_verify_file()`. +2. Otherwise, it calls `registerSSLCerts()`, which on Linux/macOS calls Boost.Asio's `set_default_verify_paths` (using OS-standard locations), and on Windows reads from the CryptoAPI system store. +3. If `sslVerifyDir` is also non-empty, it additionally registers a directory of CA certificates via `ssl_context_.add_verify_path()`. + +Errors during certificate loading throw `std::runtime_error` immediately, with one deliberate exception: if `registerSSLCerts` fails but a `sslVerifyDir` was also provided, the error is suppressed — the directory path is treated as an authoritative fallback. If neither fallback is available, the failure is fatal. + +The `sslVerify` flag can suppress all peer verification entirely (used by `WorkSSL` when `Config::SSL_VERIFY` is false). This trades security for connectivity in development or internal-network scenarios. + +## Two-Phase Verification Protocol + +The class exposes `preConnectVerify()` and `postConnectVerify()`, designed to be called at specific points in the async connection lifecycle. + +**`preConnectVerify(strm, host)`** must be called before `async_connect`. It does two things: + +- Sets the TLS SNI (Server Name Indication) extension via `SSL_set_tlsext_host_name()`. SNI embeds the target hostname in the TLS ClientHello, which is required for virtual-hosting servers and CDNs that serve multiple certificates from a single IP. This must precede the handshake — there is no way to set it after the fact. +- If verification is disabled, immediately applies `verify_none` to the stream. + +**`postConnectVerify(strm, host)`** is called after TCP connect succeeds but before the TLS handshake. When verification is enabled, it applies `verify_peer` mode and binds `rfc6125_verify` as the per-certificate callback. + +This split is intentional. SNI must be in the ClientHello (before handshake), while the verify callback only has meaning once a stream is connected; configuring it earlier would have no effect and could be overwritten. + +## Hostname Verification via RFC 6125 + +`rfc6125_verify()` is a static callback that wraps Boost.Asio's `host_name_verification` functor. It checks that the server's certificate matches the expected hostname according to RFC 6125 (covering wildcard and SAN matching). On failure it logs a warning via the `beast::Journal` before returning `false`, ensuring that failed verifications are traceable in node logs without crashing the process. + +## Template Constraints + +`preConnectVerify` and `postConnectVerify` are templated but constrained via `std::enable_if_t` to accept only `boost::asio::ssl::stream` or `boost::asio::ssl::stream`. This covers both socket ownership models — an owned socket (`WorkSSL` holds a `socket_type` and wraps it by reference in `stream_type`) and a directly-owned stream — while preventing accidental use with incompatible ASIO types that would compile but behave incorrectly at runtime. + +## Consumers and Lifecycle + +There are two distinct usage patterns in the codebase: + +**`WorkSSL`** (in `src/xrpld/app/misc/detail/WorkSSL.cpp`) creates a private `HTTPClientSSLContext` instance per connection object, constructed from `Config::SSL_VERIFY_DIR`, `Config::SSL_VERIFY_FILE`, and `Config::SSL_VERIFY`. It explicitly requests `tlsv12_client` rather than the default `sslv23`, locking connections to TLS 1.2. `preConnectVerify` is called in the `WorkSSL` constructor, and `postConnectVerify` is called in `onConnect()` before the async handshake is dispatched. + +**`HTTPClient`** (in `src/libxrpl/net/HTTPClient.cpp`) uses a different pattern: a static `std::optional` — a single shared context — initialized once via `HTTPClient::initializeSSLContext()` and reused across all connections made through `HTTPClientImp`. The `mSocket` (an `AutoSocket` wrapping an SSL stream) is constructed with a reference to this shared context's `boost::asio::ssl::context`, relying on Boost.Asio's thread-safety guarantees for read-only context access from multiple connections. + +The distinction reflects the two use cases: `WorkSSL` is used for individual validator amendment/fee fetching tasks where per-connection configuration may vary, while `HTTPClient` handles general-purpose HTTPS fetches (e.g., validator list downloads) under a single application-wide TLS policy. \ No newline at end of file diff --git a/include/xrpl/net/RegisterSSLCerts.h.ai.json b/include/xrpl/net/RegisterSSLCerts.h.ai.json new file mode 100644 index 0000000000..a611bf8822 --- /dev/null +++ b/include/xrpl/net/RegisterSSLCerts.h.ai.json @@ -0,0 +1,24 @@ +{ + "args": [], + "classes": [], + "description": "Provides a function to register system default SSL root certificates for use with Boost.Asio SSL contexts, handling platform-specific details.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/net/RegisterSSLCerts.h", + "functions": [ + { + "args": [ + "boost::asio::ssl::context&", + "boost::system::error_code&", + "beast::Journal" + ], + "lineno": 13, + "name": "registerSSLCerts" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/net/RegisterSSLCerts.h.ai.md b/include/xrpl/net/RegisterSSLCerts.h.ai.md new file mode 100644 index 0000000000..3070d8255f --- /dev/null +++ b/include/xrpl/net/RegisterSSLCerts.h.ai.md @@ -0,0 +1,29 @@ +# `include/xrpl/net/RegisterSSLCerts.h` + +This header declares a single cross-platform utility function, `registerSSLCerts`, responsible for populating a Boost.Asio SSL context with the operating system's trusted root certificates. It exists because Boost.Asio does not abstract platform-specific certificate stores on its own — specifically, it has no built-in mechanism for reading the Windows CryptoAPI trust store — so XRPL provides this thin adapter to give the rest of the networking stack a uniform interface for setting up TLS trust anchors. + +## The Function + +```cpp +void registerSSLCerts(boost::asio::ssl::context&, boost::system::error_code&, beast::Journal); +``` + +The function follows Boost's error-handling convention: failure is reported through an output `error_code` rather than by throwing, which lets callers decide whether a cert-registration failure is fatal. The `beast::Journal` parameter enables diagnostic logging during the potentially fallible certificate enumeration on Windows. + +## Platform Implementations + +The implementation in `src/libxrpl/net/RegisterSSLCerts.cpp` diverges on `BOOST_OS_WINDOWS`. + +**POSIX (Linux/macOS):** The implementation is a single line — `ctx.set_default_verify_paths(ec)` — which instructs OpenSSL to search standard OS locations (e.g., `/etc/ssl/certs` on Linux, system keychain paths on macOS). The `// NOLINTNEXTLINE(bugprone-unused-return-value)` comment acknowledges that Boost.Asio's overload returns a value that's intentionally discarded in favor of the `error_code` path. + +**Windows:** The function manually bridges between two trust store representations. It opens the Windows "ROOT" system certificate store via `CertOpenSystemStore`, then allocates a new OpenSSL `X509_STORE`. For each DER-encoded certificate returned by `CertEnumCertificatesInStore`, it calls `d2i_X509` to decode it from the Windows binary format into an OpenSSL `X509*` object, adds it to the OpenSSL store with `X509_STORE_add_cert`, and finally installs that store into the SSL context via `SSL_CTX_set_cert_store`. Both the Windows HCERTSTORE and the OpenSSL `X509_STORE` are managed through `std::unique_ptr` with custom deleters, ensuring clean teardown on any error path. + +An important non-obvious detail lives at the end of the `.cpp` file: a cluster of `#undef` directives removes macros that `` defines with the same names as OpenSSL's X.509 types (`X509_NAME`, `X509_EXTENSIONS`, etc.). Without these undefs, including this translation unit into a unity build would silently corrupt OpenSSL symbol lookups in every subsequent translation unit that uses X.509 types — a subtle, hard-to-diagnose build failure. The undefs are placed after the closing `}` to ensure they take effect after the function body but cannot affect this TU itself. + +## Error Handling Design + +Individual certificate failures on Windows are non-fatal by design. If `d2i_X509` fails or `X509_STORE_add_cert` fails for a particular certificate, the function logs a warning through the `beast::Journal` and continues to the next certificate in the store. This is deliberate: a partially-populated trust store is still useful, and a single malformed certificate in the OS store should not prevent all outbound TLS connections. Fatal errors — failure to open the store or failure to allocate the OpenSSL store — set `ec` and return early. + +## Primary Consumer + +`HTTPClientSSLContext` (in `include/xrpl/net/HTTPClientSSLContext.h`) is the direct caller. Its constructor invokes `registerSSLCerts` on its `boost::asio::ssl::context` unless a specific verify file has been provided. If `registerSSLCerts` returns an error but a custom `sslVerifyDir` is also configured, the error is silently tolerated — the directory path is added as a fallback. If neither a verify file nor a verify directory is available and `registerSSLCerts` fails, the constructor throws `std::runtime_error`, making TLS context construction fail loudly rather than silently using an empty trust store. \ No newline at end of file diff --git a/include/xrpl/nodestore/Backend.h.ai.json b/include/xrpl/nodestore/Backend.h.ai.json new file mode 100644 index 0000000000..eb69b0f894 --- /dev/null +++ b/include/xrpl/nodestore/Backend.h.ai.json @@ -0,0 +1,173 @@ +{ + "args": [ + { + "lineno": 41, + "name": "createIfMissing" + }, + { + "lineno": 52, + "name": "createIfMissing" + }, + { + "lineno": 52, + "name": "appType" + }, + { + "lineno": 52, + "name": "uid" + }, + { + "lineno": 52, + "name": "salt" + }, + { + "lineno": 68, + "name": "hash" + }, + { + "lineno": 68, + "name": "pObject" + }, + { + "lineno": 76, + "name": "hashes" + }, + { + "lineno": 80, + "name": "object" + }, + { + "lineno": 87, + "name": "batch" + }, + { + "lineno": 97, + "name": "f" + } + ], + "classes": [ + { + "args": [], + "lineno": 16, + "name": "Backend" + } + ], + "description": "Defines the Backend abstract class for the xrpl NodeStore, providing an interface for pluggable database backends with methods for opening, closing, storing, fetching, and managing NodeObjects.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/Backend.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "~Backend" + }, + { + "args": [], + "lineno": 28, + "name": "getName" + }, + { + "args": [], + "lineno": 33, + "name": "getBlockSize" + }, + { + "args": [ + "createIfMissing" + ], + "lineno": 41, + "name": "open" + }, + { + "args": [], + "lineno": 47, + "name": "isOpen" + }, + { + "args": [ + "createIfMissing", + "appType", + "uid", + "salt" + ], + "lineno": 52, + "name": "open" + }, + { + "args": [], + "lineno": 62, + "name": "close" + }, + { + "args": [ + "hash", + "pObject" + ], + "lineno": 68, + "name": "fetch" + }, + { + "args": [ + "hashes" + ], + "lineno": 76, + "name": "fetchBatch" + }, + { + "args": [ + "object" + ], + "lineno": 80, + "name": "store" + }, + { + "args": [ + "batch" + ], + "lineno": 87, + "name": "storeBatch" + }, + { + "args": [], + "lineno": 92, + "name": "sync" + }, + { + "args": [ + "f" + ], + "lineno": 97, + "name": "for_each" + }, + { + "args": [], + "lineno": 104, + "name": "getWriteLoad" + }, + { + "args": [], + "lineno": 107, + "name": "setDeletePath" + }, + { + "args": [], + "lineno": 111, + "name": "verify" + }, + { + "args": [], + "lineno": 120, + "name": "fdRequired" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/Backend.h.ai.md b/include/xrpl/nodestore/Backend.h.ai.md new file mode 100644 index 0000000000..a637f32c37 --- /dev/null +++ b/include/xrpl/nodestore/Backend.h.ai.md @@ -0,0 +1,47 @@ +# `include/xrpl/nodestore/Backend.h` + +## Role in the NodeStore System + +`Backend` is the storage abstraction at the heart of the XRP Ledger's persistence layer. Every ledger object — account states, transactions, ledger headers — is a `NodeObject` that gets written to and read from a `Backend`. The class is a pure abstract interface that lets the higher-level `Database` layer remain entirely indifferent to which database engine is actually in use: RocksDB, NuDB, or a heap-allocated in-memory store for unit tests all satisfy this contract identically. + +The design choice here is deliberate and conservative. By fixing the interface to the narrowest possible surface — keyed blob storage with 256-bit hashes — it avoids leaking database-specific semantics upward into ledger logic. Backends can be swapped at deployment time through configuration without any code changes above this abstraction boundary. + +## The Data Model + +`Backend` is fixed at construction to a particular key size (always 32 bytes in practice, matching `NodeObject::keyBytes`). Values are `NodeObject` instances — a type tag (`hotLEDGER`, `hotACCOUNT_NODE`, `hotTRANSACTION_NODE`), a 256-bit hash that serves as the primary key, and an opaque `Blob` of serialized data. The `Status` return type from `fetch()` covers the full range of outcomes: `ok`, `notFound`, `dataCorrupt`, `unknown`, `backendError`, and a `customCode` escape hatch for backend-specific error conditions. + +## Lifecycle: Open and Close + +The interface separates construction from initialization via the `open(bool createIfMissing)` method. This two-phase pattern lets callers catch exceptions from file I/O or database initialization without wrapping constructors in try/catch. `isOpen()` provides a query for implementations that track this state explicitly. + +A second overload of `open()` accepts `appType`, `uid`, and `salt` for deterministic database creation. This overload exists exclusively to support NuDB's header-level application identification — all other backends provide a default implementation that throws `std::runtime_error`, clearly documenting that this capability is not part of the general interface but is available for callers that know they are working with NuDB. + +The destructor contract is strong: all open files are closed and flushed, and any batched writes or scheduled tasks will complete before the destructor returns. This ensures that callers who simply drop their `unique_ptr` cannot silently lose data. + +## Fetch and Store: Concurrency Contracts + +The inline `@note` comments in `fetch()` and `store()` document a concurrency invariant that is critical for correctness: both `fetch()` and `store()` **will be called concurrently** by multiple threads. Implementations must be internally thread-safe for these two operations. + +`storeBatch()` and `for_each()`, by contrast, are explicitly **not** called concurrently with each other or with other writes. This asymmetry reflects real usage: individual stores originate from ledger-processing threads (concurrent by nature), while batch writes and full-database iteration happen during controlled phases such as import or compaction. This layered concurrency contract allows backends to use coarse locking or lock-free structures selectively. + +`fetchBatch()` accepts a vector of hashes and returns a paired vector of results plus a `Status`. Bulk fetches exist primarily to amortize round-trip or I/O costs when prefetching sets of related objects. + +## Sync and Write Load + +`sync()` provides an explicit flush point — callers can ensure that all previously submitted stores are durable before proceeding. `getWriteLoad()` returns an estimate of pending write operations, which the `Database` layer uses for back-pressure and diagnostic reporting. Neither method has a return value with failure semantics; they are best-effort operational utilities. + +## Optional and NuDB-Specific Extensions + +`getBlockSize()` returns `std::optional`, defaulting to `std::nullopt`. NuDB organizes data into fixed-size blocks, and this method lets callers that care about alignment or prefetch granularity query the block size without requiring a downcast. Backends that have no concept of block size simply inherit the default. + +`verify()` performs consistency checking and is currently implemented only by `NuDBBackend`. The comment explicitly acknowledges the gap: it is not yet called at startup, but the intent is that it could one day be invoked at launch to detect corruption before a crash. Providing a no-op default rather than a pure virtual method allows other backends to exist without implementing a concept that does not apply to them. + +`setDeletePath()` marks the database for deletion of its on-disk files upon destruction. This is used by temporary databases — unit tests and ephemeral shard stores — where cleanup is required without explicit external management. + +## Relationship to Factory and Database + +`Backend` instances are never constructed directly. The `Factory` abstract class provides `createInstance()`, and the `Manager` singleton dispatches to the appropriate registered factory based on the `type` field in the `node_db` configuration section. `Factory::createInstance()` accepts a `Scheduler&` for deferred task execution — backends that batch writes or defer flushes to background threads use this scheduler rather than spawning their own threads. + +`Database` wraps one or more `Backend` instances and adds a `TaggedCache` read cache, async fetch queuing via a pool of reader threads, and ledger-sequence-aware routing (the `DatabaseRotating` subclass directs writes to the current shard and reads to any shard that might hold the target sequence). From `Backend`'s perspective, it sees only individual or batched object operations; the routing and caching logic entirely lives in `Database`. + +`fdRequired()` returns the number of file descriptors the backend expects to consume. The `Database` base class aggregates these values and exposes them so the process can pre-check against the OS file descriptor limit before opening any databases. \ No newline at end of file diff --git a/include/xrpl/nodestore/Database.h.ai.json b/include/xrpl/nodestore/Database.h.ai.json new file mode 100644 index 0000000000..83d7a10a0c --- /dev/null +++ b/include/xrpl/nodestore/Database.h.ai.json @@ -0,0 +1,300 @@ +{ + "args": [ + { + "lineno": 28, + "name": "scheduler" + }, + { + "lineno": 28, + "name": "readThreads" + }, + { + "lineno": 28, + "name": "config" + }, + { + "lineno": 28, + "name": "j" + }, + { + "lineno": 46, + "name": "source" + }, + { + "lineno": 59, + "name": "type" + }, + { + "lineno": 59, + "name": "data" + }, + { + "lineno": 59, + "name": "hash" + }, + { + "lineno": 59, + "name": "ledgerSeq" + }, + { + "lineno": 75, + "name": "s1" + }, + { + "lineno": 75, + "name": "s2" + }, + { + "lineno": 92, + "name": "fetchType" + }, + { + "lineno": 93, + "name": "duplicate" + }, + { + "lineno": 112, + "name": "callback" + }, + { + "lineno": 148, + "name": "obj" + }, + { + "lineno": 182, + "name": "count" + }, + { + "lineno": 182, + "name": "sz" + }, + { + "lineno": 188, + "name": "dstBackend" + }, + { + "lineno": 188, + "name": "srcDB" + }, + { + "lineno": 191, + "name": "fetches" + }, + { + "lineno": 191, + "name": "hits" + }, + { + "lineno": 191, + "name": "duration" + }, + { + "lineno": 202, + "name": "fetchReport" + }, + { + "lineno": 211, + "name": "f" + } + ], + "classes": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "Section const& config", + "beast::Journal j" + ], + "lineno": 19, + "name": "Database" + } + ], + "description": "Defines the xrpl::NodeStore::Database class, which provides a persistency layer for NodeObject instances in the XRP Ledger, handling storage, retrieval, and management of ledger node objects with support for asynchronous operations, statistics, and backend abstraction.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/Database.h", + "functions": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "Section const& config", + "beast::Journal j" + ], + "lineno": 27, + "name": "Database" + }, + { + "args": [], + "lineno": 36, + "name": "~Database" + }, + { + "args": [], + "lineno": 41, + "name": "getName" + }, + { + "args": [ + "Database& source" + ], + "lineno": 46, + "name": "importDatabase" + }, + { + "args": [], + "lineno": 51, + "name": "getWriteLoad" + }, + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash", + "std::uint32_t ledgerSeq" + ], + "lineno": 58, + "name": "store" + }, + { + "args": [ + "std::uint32_t s1", + "std::uint32_t s2" + ], + "lineno": 74, + "name": "isSameDB" + }, + { + "args": [], + "lineno": 82, + "name": "sync" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t ledgerSeq", + "FetchType fetchType", + "bool duplicate" + ], + "lineno": 89, + "name": "fetchNodeObject" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t ledgerSeq", + "std::function const&)>&& callback" + ], + "lineno": 106, + "name": "asyncFetch" + }, + { + "args": [], + "lineno": 122, + "name": "getStoreCount" + }, + { + "args": [], + "lineno": 127, + "name": "getFetchTotalCount" + }, + { + "args": [], + "lineno": 132, + "name": "getFetchHitCount" + }, + { + "args": [], + "lineno": 137, + "name": "getStoreSize" + }, + { + "args": [], + "lineno": 142, + "name": "getFetchSize" + }, + { + "args": [ + "Json::Value& obj" + ], + "lineno": 147, + "name": "getCountsJson" + }, + { + "args": [], + "lineno": 151, + "name": "fdRequired" + }, + { + "args": [], + "lineno": 157, + "name": "stop" + }, + { + "args": [], + "lineno": 160, + "name": "isStopping" + }, + { + "args": [], + "lineno": 164, + "name": "earliestLedgerSeq" + }, + { + "args": [ + "std::uint64_t count", + "std::uint64_t sz" + ], + "lineno": 181, + "name": "storeStats" + }, + { + "args": [ + "Backend& dstBackend", + "Database& srcDB" + ], + "lineno": 187, + "name": "importInternal" + }, + { + "args": [ + "uint64_t fetches", + "uint64_t hits", + "uint64_t duration" + ], + "lineno": 190, + "name": "updateFetchMetrics" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t ledgerSeq", + "FetchReport& fetchReport", + "bool duplicate" + ], + "lineno": 200, + "name": "fetchNodeObject" + }, + { + "args": [ + "std::function)> f" + ], + "lineno": 210, + "name": "for_each" + }, + { + "args": [], + "lineno": 218, + "name": "threadEntry" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/Database.h.ai.md b/include/xrpl/nodestore/Database.h.ai.md new file mode 100644 index 0000000000..024e9883c2 --- /dev/null +++ b/include/xrpl/nodestore/Database.h.ai.md @@ -0,0 +1,70 @@ +# `include/xrpl/nodestore/Database.h` — NodeStore Database Abstract Base Class + +## Role and Purpose + +`Database` is the abstract base class that defines the persistence contract for all node storage in the XRP Ledger. Every piece of ledger data — account states, transaction records, ledger headers — is serialized into a `NodeObject`, keyed by a 256-bit hash of its payload. Because the total set of node objects vastly exceeds available memory, any hash not found in an in-memory cache must be retrieved from disk. `Database` sits at this boundary: it owns the async read thread pool, tracks performance statistics, and defines the interface that concrete backends (NuDB, RocksDB, etc.) must implement. + +The class occupies the `xrpl::NodeStore` namespace and relies on three key collaborators: `NodeObject` (the data record), `Backend` (the pluggable storage engine), and `Scheduler` (the async task coordinator). + +## Two-Layer Fetch Design + +The most architecturally significant feature of `Database` is its split between a public non-virtual `fetchNodeObject()` and a private pure-virtual `fetchNodeObject()` of the same name. This is a classic Template Method pattern: + +```cpp +// Public, non-virtual — adds timing, metrics, and scheduler callback +std::shared_ptr +fetchNodeObject(uint256 const& hash, std::uint32_t ledgerSeq, + FetchType fetchType = FetchType::synchronous, + bool duplicate = false); + +// Private, pure-virtual — subclass provides actual backend lookup +virtual std::shared_ptr +fetchNodeObject(uint256 const& hash, std::uint32_t ledgerSeq, + FetchReport& fetchReport, bool duplicate) = 0; +``` + +The public wrapper measures wall-clock duration, increments hit/miss counters atomically, accumulates byte counts, and invokes `scheduler_.onFetch()` so the scheduler can observe latency. This guarantees that no subclass can bypass metrics — the instrumentation is structural, not optional. + +## Async Read Thread Pool + +The constructor spawns `readThreads` detached threads immediately. Each runs `threadEntry()`, which loops waiting on `readCondVar_` for work placed into the `read_` map: + +```cpp +std::map< + uint256, + std::vector const&)>>>> + read_; +``` + +The map key is the hash; the value is a vector of `(ledgerSeq, callback)` pairs. This structure deliberately coalesces multiple concurrent `asyncFetch()` requests for the same hash — all registered callbacks are satisfied by a single backend read. This is a meaningful optimization for scenarios like ledger acquisition where many validator nodes may simultaneously request the same objects. + +Each thread extracts up to `requestBundle_` entries per mutex acquisition (default 4, configurable via `rq_bundle` in the config file). This batching strategy amortizes the cost of mutex acquisition across multiple items, reducing lock contention under high read pressure. + +When the thread processes a batch, it also handles the multi-sequence case: if multiple callbacks were registered for the same hash but different ledger sequence numbers, the thread checks `isSameDB(req.first, seqn)` to determine whether those sequence numbers map to the same physical backend. If they do, it reuses the already-fetched object; otherwise it performs a second fetch. This is essential for `DatabaseRotating`, which may store different ledger ranges in separate backend files. + +## Shutdown Sequencing + +The destructor calls `stop()`, which sets `readStopping_` atomically, clears the pending `read_` queue, and broadcasts on `readCondVar_` to wake all threads. It then spin-waits (yielding) until `readThreads_` reaches zero, with an assertion that this completes within 30 seconds. + +There is a critical ordering constraint documented in the destructor comment: **derived classes must call `stop()` in their own destructor**, not rely on the base class destructor to do it. The reason is that background threads call the pure-virtual `fetchNodeObject()` through a subclass vtable. If the subclass is destroyed first and the base destructor calls `stop()` second, a thread that woke up between those two events would invoke a dangling vtable entry — undefined behavior. Calling `stop()` in the derived destructor ensures all threads have exited before the derived class's data members are destroyed. + +## Concurrency and Statistics + +All counters (`storeCount_`, `storeSz_`, `fetchTotalCount_`, `fetchHitCount_`, `fetchDurationUs_`, `storeSz_`, `fetchSz_`) are `std::atomic`, allowing lock-free increment from any thread. The `read_` map and `readCondVar_` are protected together by `readLock_`, and `getCountsJson()` acquires this lock briefly to snapshot the queue depth. + +The protected `storeStats()` helper enforces an invariant via `XRPL_ASSERT(count <= sz)`: the byte size of stored data must be at least as large as the item count, which rules out obvious accounting bugs when subclasses update store metrics. + +## Lifecycle Constraints and Configuration + +Two configuration parameters are validated strictly at construction time. `earliest_seq` sets `earliestLedgerSeq_` (default `XRP_LEDGER_EARLIEST_SEQ`, which is 32570 for the main network); zero is explicitly rejected. `rq_bundle` must be between 1 and 64 inclusive. Both are `const` after construction, making them safe to read from any thread without synchronization. + +The `isSameDB()` pure virtual method exists because some implementations (specifically `DatabaseRotating`) maintain multiple physical backend files covering different ledger sequence ranges. Callers and the internal async thread pool use `isSameDB()` to avoid redundant backend lookups when sequence numbers happen to fall in the same file. + +## Relationship to `DatabaseRotating` + +`DatabaseRotating` is the only concrete subclass visible in the header tree. It extends `Database` with a `rotate()` operation that swaps in a new writable backend while archiving the old one. The `isSameDB()` and `isSameDB`-dependent logic in the async thread entry function exists specifically to support this rotation scheme cleanly. The base `Database` never needs to know what rotation means; it only needs to know whether two sequence numbers resolve to the same underlying store. + +## Import Path + +`importDatabase()` is the public API for bulk migration, but the actual work is delegated to `importInternal(Backend& dstBackend, Database& srcDB)`. This helper calls `srcDB.for_each()` to iterate all source objects, assembles them into batches of `batchWritePreallocationSize`, and commits each batch via `dstBackend.storeBatch()`. Byte statistics are accumulated through `storeStats()` after each successful batch flush. Exception safety is basic: a caught exception logs the error and returns early without aborting the entire import. \ No newline at end of file diff --git a/include/xrpl/nodestore/DatabaseRotating.h.ai.json b/include/xrpl/nodestore/DatabaseRotating.h.ai.json new file mode 100644 index 0000000000..67e09356bc --- /dev/null +++ b/include/xrpl/nodestore/DatabaseRotating.h.ai.json @@ -0,0 +1,63 @@ +{ + "args": [ + { + "lineno": 17, + "name": "scheduler" + }, + { + "lineno": 18, + "name": "readThreads" + }, + { + "lineno": 19, + "name": "config" + }, + { + "lineno": 20, + "name": "journal" + }, + { + "lineno": 29, + "name": "newBackend" + }, + { + "lineno": 30, + "name": "f" + } + ], + "classes": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "Section const& config", + "beast::Journal journal" + ], + "lineno": 13, + "name": "DatabaseRotating" + } + ], + "description": "Defines the DatabaseRotating class, which manages two rotating key-value store Backend objects for persisting SHAMap records, facilitating online deletion of data in the XRPL NodeStore.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/DatabaseRotating.h", + "functions": [ + { + "args": [ + "std::unique_ptr&& newBackend", + "std::function const& f" + ], + "lineno": 28, + "name": "rotate" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/DatabaseRotating.h.ai.md b/include/xrpl/nodestore/DatabaseRotating.h.ai.md new file mode 100644 index 0000000000..1afa027b15 --- /dev/null +++ b/include/xrpl/nodestore/DatabaseRotating.h.ai.md @@ -0,0 +1,42 @@ +# `include/xrpl/nodestore/DatabaseRotating.h` + +## Role and Purpose + +`DatabaseRotating` is a minimal abstract interface that extends `Database` with exactly one additional operation: `rotate()`. Its sole purpose is to define the contract for the two-backend rotation scheme that enables **online deletion of ledger history** without taking the node offline. The class carries no state of its own; all the mechanism lives in its concrete subclass `DatabaseRotatingImp`. + +## Design Context: Two-Backend Storage + +The XRPL node store must keep ledger data beyond what fits in RAM, but running nodes indefinitely accumulates unbounded disk usage. The solution is an **online deletion** strategy: rather than deleting individual records (which would be expensive in key-value stores), the system maintains two physical backends in parallel — a *writable* backend that receives all new writes and a *read-only archive* backend holding older data. When enough new ledger history has accumulated in the writable backend, a rotation event swaps the backends and discards the old archive. + +`DatabaseRotating` is the abstract seam between this two-backend logic and the rest of the application. Components like `SHAMapStoreImp` hold only a `DatabaseRotating*` pointer, making the rotation mechanism pluggable and testable independently of the storage format. + +## The `rotate()` Method + +```cpp +virtual void rotate( + std::unique_ptr&& newBackend, + std::function const& f) = 0; +``` + +This is the entire API extension over `Database`. The caller supplies a freshly created backend that becomes the new writable store, and a callback `f` that fires after the in-memory rotation is complete but before the old archive directory is physically deleted. + +The callback design is deliberate and load-bearing. In `DatabaseRotatingImp::rotate()`, the pointer swap happens inside a `mutex_` lock — the old archive is marked for deletion and kept alive in a local `shared_ptr`, the current writable backend becomes the new archive, and the new backend becomes the new writable. After releasing the lock, `f` is called with the new names. Only after `f` returns does `oldArchiveBackend` go out of scope and the old files are removed. + +This sequencing lets `SHAMapStoreImp` durably persist the new backend names and `lastRotated` ledger sequence to a SQL state database inside `f`, creating an atomic checkpoint: if the process crashes between the pointer swap and state persistence, the on-disk backend directories still exist and can be recovered on restart. The old archive directory is never unlinked until the new state is committed. + +## Fetch Strategy + +The concrete `fetchNodeObject` implementation in `DatabaseRotatingImp` first attempts a fetch from the writable backend, then falls back to the archive backend if not found. When a node is retrieved from the archive, and the `duplicate` flag is set, the object is promoted — written back into the current writable backend — so that future fetches find it without traversing the archive. This matters because after the *next* rotation, the archive backend will be discarded; data that hasn't been promoted would be permanently lost unless it's been written to the new writable store. + +The `mutex_` in `DatabaseRotatingImp` guards the backend shared-pointer references. The fetch implementation takes a snapshot of both `writableBackend_` and `archiveBackend_` under the lock, then releases it before performing I/O. This is a deliberate choice: holding a lock across disk I/O would serialize concurrent fetches. The snapshot approach lets multiple threads read concurrently, with the tradeoff that a rotation can occur between snapshot and fetch — handled safely because `shared_ptr` reference counting keeps the old backend alive until all threads are done with it. + +## `isSameDB()` Returns True Unconditionally + +`DatabaseRotatingImp::isSameDB()` always returns `true`, ignoring both sequence number arguments. This reflects a semantic choice: from the application layer's perspective, the rotating store is one logical database, regardless of which physical backend holds a given ledger. Callers use `isSameDB` to decide whether two ledger sequences are co-located and can be fetched with identical results; the rotating store always satisfies this predicate because `fetchNodeObject` transparently spans both backends. + +## Relationship to `SHAMapStoreImp` + +The rotation is driven by `SHAMapStoreImp`, which runs a background thread that monitors validated ledger advancement. When enough ledgers have accumulated, it copies the minimum required ledger data into a freshly created backend via `makeBackendRotating()`, freshens in-memory caches, clears stale entries, and then calls `dbRotating_->rotate()`. Inside `rotate()`'s callback, `SHAMapStoreImp` writes the new `SavedState` (writable name, archive name, last-rotated sequence) to a soci/SQLite state database, providing crash-safe restart semantics. + +The minimum rotation interval enforced by `SHAMapStoreImp` (256 ledgers in networked mode, 8 in standalone) ensures at least one full epoch of history is always retained, protecting network health and preventing data loss during brief unavailability of peers. \ No newline at end of file diff --git a/include/xrpl/nodestore/DummyScheduler.h.ai.json b/include/xrpl/nodestore/DummyScheduler.h.ai.json new file mode 100644 index 0000000000..156b4f8b80 --- /dev/null +++ b/include/xrpl/nodestore/DummyScheduler.h.ai.json @@ -0,0 +1,56 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 9, + "name": "DummyScheduler" + } + ], + "description": "Defines a DummyScheduler class in the xrpl::NodeStore namespace that implements the Scheduler interface by performing tasks synchronously.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/DummyScheduler.h", + "functions": [ + { + "args": [], + "lineno": 10, + "name": "DummyScheduler" + }, + { + "args": [], + "lineno": 11, + "name": "~DummyScheduler" + }, + { + "args": [ + "task" + ], + "lineno": 12, + "name": "scheduleTask" + }, + { + "args": [ + "report" + ], + "lineno": 14, + "name": "onFetch" + }, + { + "args": [ + "report" + ], + "lineno": 16, + "name": "onBatchWrite" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/DummyScheduler.h.ai.md b/include/xrpl/nodestore/DummyScheduler.h.ai.md new file mode 100644 index 0000000000..2a1c620630 --- /dev/null +++ b/include/xrpl/nodestore/DummyScheduler.h.ai.md @@ -0,0 +1,41 @@ +# `DummyScheduler` — Synchronous No-Op Scheduler for NodeStore + +## Role and Purpose + +`DummyScheduler` is a minimal, test-friendly implementation of the `Scheduler` interface within the `xrpl::NodeStore` subsystem. Its sole purpose is to provide a concrete `Scheduler` that satisfies the interface contract without any thread management, queuing, or performance instrumentation — executing every task immediately on the calling thread instead. + +The `Scheduler` abstraction exists because the NodeStore's `BatchWriter` needs to hand off write work asynchronously in production: batching ledger object writes to the backend without stalling the caller. `DummyScheduler` strips all of that machinery away, collapsing the async boundary into a direct synchronous call. + +## Design: Intentional Minimalism + +The `Scheduler` base class defines three virtual hooks: + +- `scheduleTask(Task&)` — submit a unit of backend work for later (or immediate) execution +- `onFetch(FetchReport const&)` — observe completed fetch operations for performance monitoring +- `onBatchWrite(BatchWriteReport const&)` — observe completed batch writes for performance monitoring + +`DummyScheduler` overrides all three. The implementation of `scheduleTask` is a single line: + +```cpp +task.performScheduledTask(); +``` + +The two reporting callbacks, `onFetch` and `onBatchWrite`, are empty bodies. There is no thread pool, no queue, no timer, and no statistics collection. + +This is a deliberate design choice: the `Scheduler` contract explicitly allows implementations to invoke the task "on the current thread of execution," so `DummyScheduler` takes that permission to its logical extreme. A caller cannot distinguish between a `DummyScheduler` and a production scheduler from the perspective of correctness — only from the perspective of latency and throughput. + +## Usage Contexts + +`DummyScheduler` appears in two distinct call sites: + +**Database import during application startup** (`Application.cpp`): When the node launches with `doImport` set, it constructs a `DummyScheduler` as a transient scheduling context for the source database being read during import. Because this operation is already a sequential, offline migration step with no live peer traffic to serve, the overhead of synchronous scheduling is irrelevant and the simplicity is a net benefit. + +**Unit and integration tests** (`Backend_test.cpp`, `Database_test.cpp`, `Timing_test.cpp`, `NuDBFactory_test.cpp`, `shamap/common.h`): Test fixtures across the NodeStore test suite construct a `DummyScheduler` to stand in for the real scheduler. Tests want deterministic, single-threaded execution — a production scheduler that dispatches to a thread pool would introduce non-determinism and require careful teardown. `DummyScheduler` eliminates all of that complexity while still satisfying every interface requirement. + +## Relationship to `BatchWriter` + +The `BatchWriter` class (referenced in `Scheduler.h`'s `@see` annotation) is the primary consumer of `Scheduler` in production. It accumulates write requests and calls `scheduleTask` with a `Task` that flushes the batch to the backend. With `DummyScheduler`, each call to `scheduleTask` causes the flush to happen inline before `scheduleTask` returns — effectively disabling batching. This is acceptable for import and test workloads but would be a serious performance regression under normal ledger-processing load, which is why the production application uses a real async scheduler for its live database. + +## Summary + +`DummyScheduler` is a null-object pattern applied to the `Scheduler` interface: it satisfies every contract requirement while doing the minimum possible work. Its value is precisely its emptiness — it removes concurrency from the equation wherever concurrency would be an obstacle rather than a benefit, and it serves as the canonical test double for anything in the NodeStore that depends on scheduling. \ No newline at end of file diff --git a/include/xrpl/nodestore/Factory.h.ai.json b/include/xrpl/nodestore/Factory.h.ai.json new file mode 100644 index 0000000000..425b8e5099 --- /dev/null +++ b/include/xrpl/nodestore/Factory.h.ai.json @@ -0,0 +1,98 @@ +{ + "args": [ + { + "lineno": 22, + "name": "keyBytes" + }, + { + "lineno": 23, + "name": "parameters" + }, + { + "lineno": 24, + "name": "burstSize" + }, + { + "lineno": 25, + "name": "scheduler" + }, + { + "lineno": 26, + "name": "journal" + }, + { + "lineno": 35, + "name": "keyBytes" + }, + { + "lineno": 36, + "name": "parameters" + }, + { + "lineno": 37, + "name": "burstSize" + }, + { + "lineno": 38, + "name": "scheduler" + }, + { + "lineno": 39, + "name": "context" + }, + { + "lineno": 40, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "Factory" + } + ], + "description": "Defines the base class Factory for backend factories in the NodeStore module, providing interfaces for creating backend instances with various parameters.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/Factory.h", + "functions": [ + { + "args": [], + "lineno": 16, + "name": "getName" + }, + { + "args": [ + "keyBytes", + "parameters", + "burstSize", + "scheduler", + "journal" + ], + "lineno": 21, + "name": "createInstance" + }, + { + "args": [ + "keyBytes", + "parameters", + "burstSize", + "scheduler", + "context", + "journal" + ], + "lineno": 34, + "name": "createInstance" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/Factory.h.ai.md b/include/xrpl/nodestore/Factory.h.ai.md new file mode 100644 index 0000000000..1164f2426d --- /dev/null +++ b/include/xrpl/nodestore/Factory.h.ai.md @@ -0,0 +1,38 @@ +# `include/xrpl/nodestore/Factory.h` + +## Role in the System + +`Factory.h` defines the abstract base class `Factory`, the central abstraction point for pluggable storage backends in the XRPL NodeStore subsystem. The NodeStore is the key-value store responsible for persisting all ledger nodes (account state, transactions, and metadata objects) to disk. Rather than hard-coding a single storage engine, the system delegates construction of `Backend` instances to interchangeable `Factory` objects — one per storage engine type. This is the classic Abstract Factory pattern applied to a production-critical database layer. + +## The Two `createInstance` Overloads + +The core of the interface is two overloads of `createInstance`, both returning `std::unique_ptr`. They share four parameters: `keyBytes` (the fixed key width, always 32 bytes for SHA-512 Half hashes in XRPL), a `Section` of key/value configuration pairs parsed from `rippled.cfg`, a `burstSize` for the backend's write buffer, and a `Scheduler` for async task dispatch. The difference is the second overload additionally accepts a `nudb::context&`. + +The second overload is specifically designed for NuDB's asynchronous I/O threading model. A `nudb::context` owns the thread pool that services NuDB's background I/O; when a caller already has a shared context (e.g., for the rotating database that manages shard imports), it can pass it in to share threads across backends. The default implementation of this second overload simply `return {}`s — returning an empty `unique_ptr`. This is a deliberate design choice: non-NuDB backends (`MemoryFactory`, `NullFactory`, `RocksDBFactory`) inherit the default and silently produce a null result, signaling "this backend doesn't use a NuDB context." The caller (`ManagerImp`) falls back to the simpler overload in that case. This avoids forcing every factory subclass to implement a method that has no meaning for their engine. + +## Registration and Discovery + +Factories don't self-register by magic — each concrete factory registers itself with the `Manager` singleton in its constructor via `Manager::insert(*this)`. For example: + +```cpp +explicit NuDBFactory(Manager& manager) : manager_(manager) +{ + manager_.insert(*this); +} +``` + +A module-level free function like `registerNuDBFactory(Manager&)` creates a `static` factory instance, whose constructor immediately registers with the manager. `Manager::find(name)` then performs a case-insensitive name lookup, enabling the configuration string `type=NuDB` to resolve to `NuDBFactory` at startup. The `getName()` pure virtual method supplies the string key ("NuDB", "memory", "null") for this lookup table. + +## Relationship to `Backend` and `Manager` + +`Factory` sits at the intersection of two other abstractions. `Backend` (defined in `Backend.h`) is the runtime interface for all storage operations — `fetch`, `store`, `storeBatch`, `open`, `close`, etc. `Factory`'s only job is to *construct* those `Backend` instances from configuration; it has no storage methods itself. + +`Manager` (defined in `Manager.h`) is the singleton that owns the registry of `Factory` objects and dispatches `make_Backend()` and `make_Database()` calls by reading the `type=` key from the configuration section. `Manager` depends on `Factory`; `Factory` depends on `Backend` only through its return type. This layering keeps each concern isolated. + +## Design Trade-offs + +Passing `keyBytes` into `createInstance` rather than inferring it from the key type allows a single backend type to serve different hash-size use cases without subclassing. In practice, every `Backend` instance in production uses 32-byte keys (SHA-512 Half), but the interface is general enough to support alternative hash sizes in tests. + +The `burstSize` parameter flows directly into NuDB's `db_.set_burst()` call after the database is opened, controlling how much data NuDB buffers in memory before flushing. Exposing it at the `Factory` level — rather than burying it as a config-file-only setting — lets the `Manager` apply a node-wide policy (e.g., derived from the configured cache size) without the factory needing to re-parse it. + +The virtual destructor is the only concrete member in the class. Since `ManagerImp` stores a `std::vector` (raw pointers, not owning), factories must outlive the manager or be explicitly erased first. Concrete factories registered as function-local statics have program lifetime, which is the expected pattern. \ No newline at end of file diff --git a/include/xrpl/nodestore/Manager.h.ai.json b/include/xrpl/nodestore/Manager.h.ai.json new file mode 100644 index 0000000000..07f5786b9b --- /dev/null +++ b/include/xrpl/nodestore/Manager.h.ai.json @@ -0,0 +1,72 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "Manager" + } + ], + "description": "Defines the Manager singleton class for managing NodeStore factories and backends in the XRPL NodeStore subsystem.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/Manager.h", + "functions": [ + { + "args": [], + "lineno": 19, + "name": "instance" + }, + { + "args": [ + "factory" + ], + "lineno": 23, + "name": "insert" + }, + { + "args": [ + "factory" + ], + "lineno": 26, + "name": "erase" + }, + { + "args": [ + "name" + ], + "lineno": 31, + "name": "find" + }, + { + "args": [ + "parameters", + "burstSize", + "scheduler", + "journal" + ], + "lineno": 36, + "name": "make_Backend" + }, + { + "args": [ + "burstSize", + "scheduler", + "readThreads", + "backendParameters", + "journal" + ], + "lineno": 54, + "name": "make_Database" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/Manager.h.ai.md b/include/xrpl/nodestore/Manager.h.ai.md new file mode 100644 index 0000000000..5b5f033524 --- /dev/null +++ b/include/xrpl/nodestore/Manager.h.ai.md @@ -0,0 +1,29 @@ +# `include/xrpl/nodestore/Manager.h` + +## Role in the NodeStore Subsystem + +`Manager` is the central registry and abstract factory for the NodeStore persistence layer. It solves a plugin-registration problem: the XRPL node database supports multiple storage backends — NuDB, RocksDB, in-memory, and null — and something must map the string name in `xrpld.cfg` (e.g., `type=NuDB`) to the concrete `Backend` class that implements it. `Manager` owns that mapping and exposes the two creation points the rest of the application needs: `make_Backend()` for a raw storage engine and `make_Database()` for a fully-wired `Database` object. + +## Interface Design + +The class is declared as an abstract base whose `instance()` static method returns the concrete `ManagerImp` singleton defined in `ManagerImp.cpp`. This two-layer design — abstract interface in the header, concrete singleton in the `.cpp` — is deliberate: callers depending only on the header see a clean interface without being coupled to the implementation or forced to include its dependencies. It also makes the `Manager` interface mockable in tests without exposing the singleton machinery. + +The singleton itself uses a Meyer's static local (`static ManagerImp _`) in `ManagerImp::instance()`, which guarantees thread-safe initialization under C++11 and beyond. Copy construction and copy assignment are explicitly deleted to reinforce the single-instance contract. + +## Factory Registry + +`insert()` and `erase()` maintain a runtime list of `Factory*` pointers. Each `Factory` subclass knows how to create one kind of backend. `ManagerImp`'s constructor pre-populates the list by calling four registration functions — `registerNuDBFactory`, `registerRocksDBFactory`, `registerNullFactory`, and `registerMemoryFactory` — so the four built-in backend types are always available without the caller doing anything. + +`find()` performs a case-insensitive string comparison using `boost::iequals` so that config values like `NuDB`, `nudb`, or `NUDB` all resolve to the same factory. Both `insert()`, `erase()`, and `find()` acquire a `std::mutex` guard, making the registry safe for concurrent access during startup when multiple subsystems may race to register factories. + +There is an important lifetime comment in `ManagerImp.cpp` explaining why `erase()` is not called from `Factory` destructors: C++ does not define destruction order for objects with static storage duration, so `ManagerImp` could be destroyed before the `Factory` instances, making a call into `Manager::instance().erase()` undefined behaviour. The current design avoids this by having the `ManagerImp` constructor eagerly register all factories and accepting that the list may outlive them — a pragmatic trade-off for a static singleton that is alive for the entire program lifetime. + +## Backend and Database Construction + +`make_Backend()` reads the `type` key from the supplied `Section` parameters, looks up the matching factory, and delegates to `Factory::createInstance()`, passing the fixed key size (`NodeObject::keyBytes`), configuration, burst size, scheduler, and journal. If the `type` key is missing or unrecognised, it throws a `std::runtime_error` with a human-readable message pointing the operator to the configuration file — a defensive pattern that surfaces misconfiguration early at startup rather than silently failing later. + +`make_Database()` layers on top: it calls `make_Backend()` to get an opened backend, then wraps it in a `DatabaseNodeImp`, which adds the read-thread pool, write batching, and the higher-level `Database` API. The `burstSize` and `readThreads` parameters thread through both layers, giving the caller fine-grained control over I/O concurrency. + +## Relationship to Other Headers + +`Factory.h` defines the abstract `Factory` base, which `Manager` accepts by reference — the two are tightly coupled but kept in separate headers to limit inclusion cost. `DatabaseRotating.h` is pulled in to make `DatabaseRotating`'s full type available to callers who construct it via a subclass of `Manager`; `Manager.h` itself doesn't directly create rotating databases, but including the header here ensures downstream callers don't need an extra include. `Backend.h` and `Scheduler.h` flow in transitively through `Factory.h`, completing the set of abstractions `Manager` coordinates. \ No newline at end of file diff --git a/include/xrpl/nodestore/NodeObject.h.ai.json b/include/xrpl/nodestore/NodeObject.h.ai.json new file mode 100644 index 0000000000..e0af93e49b --- /dev/null +++ b/include/xrpl/nodestore/NodeObject.h.ai.json @@ -0,0 +1,65 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash", + "PrivateAccess" + ], + "lineno": 29, + "name": "NodeObject" + }, + { + "args": [], + "lineno": 36, + "name": "PrivateAccess" + } + ], + "description": "Defines the NodeObject class and NodeObjectType enum used for storing ledger entries in XRPL, including type, hash, and data blob.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/NodeObject.h", + "functions": [ + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash", + "PrivateAccess" + ], + "lineno": 38, + "name": "NodeObject" + }, + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash" + ], + "lineno": 47, + "name": "createObject" + }, + { + "args": [], + "lineno": 56, + "name": "getType" + }, + { + "args": [], + "lineno": 60, + "name": "getHash" + }, + { + "args": [], + "lineno": 64, + "name": "getData" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/NodeObject.h.ai.md b/include/xrpl/nodestore/NodeObject.h.ai.md new file mode 100644 index 0000000000..d5db9d2f43 --- /dev/null +++ b/include/xrpl/nodestore/NodeObject.h.ai.md @@ -0,0 +1,38 @@ +# `NodeObject.h` — The Atomic Storage Unit of the XRPL Node Store + +## Role in the System + +`NodeObject` is the fundamental data carrier for the XRPL ledger's persistent key-value store. Every piece of ledger state — account tree nodes, transaction tree nodes, and ledger headers themselves — is stored as a `NodeObject` when written to or read from the node store. The class is deliberately minimal: a type tag, a 256-bit hash key, and a raw binary blob. There is no higher-level interpretation of the content at this layer. + +The file-level comment calls out an intentional architectural decision: `NodeObject` lives in the `xrpl` namespace rather than the `xrpl::NodeStore` sub-namespace. This is because `NodeObject` is consumed broadly across the codebase — by the SHAMap layer, the ledger subsystem, and various serialization paths — while the rest of `NodeStore` is backend-plumbing. Hoisting it to the parent namespace avoids forcing every consumer to import the full nodestore API. + +## The `NodeObjectType` Enum + +The four meaningful values — `hotLEDGER`, `hotACCOUNT_NODE`, `hotTRANSACTION_NODE`, and the zero-value `hotUNKNOWN` — map to the three kinds of SHAMap content that the ledger needs to persist. The notable gap at value 2 (skipped between `hotLEDGER = 1` and `hotACCOUNT_NODE = 3`) reflects a historical removal. The `hotDUMMY = 512` sentinel is deliberately non-contiguous with the valid range: it signals an invalid or placeholder object and its value ensures it cannot be confused with a legitimate type by accident or by off-by-one arithmetic. + +## Immutability and Factory Construction + +All three data members — `mType`, `mHash`, and `mData` — are declared `const`. Once a `NodeObject` is constructed, it never changes. This is appropriate for content-addressed storage: the hash identifies the blob, and the blob is write-once. + +The class enforces construction exclusively through the `createObject()` static factory. The mechanism used to prevent direct construction while still being compatible with `std::make_shared` is a well-known C++ idiom: a private nested `PrivateAccess` tag struct. The constructor is technically `public` (required so `std::make_shared` can call it), but it demands a `PrivateAccess` argument. Because `PrivateAccess` itself is a `private` nested type, only code inside `NodeObject` can construct one — making the constructor effectively private to all external callers. The comment in the header explicitly acknowledges this as a "hack" necessitated by the lack of a portable way to friend `std::make_shared`. + +The factory signature is: + +```cpp +static std::shared_ptr +createObject(NodeObjectType type, Blob&& data, uint256 const& hash); +``` + +Taking `Blob&&` means the caller relinquishes ownership of the raw buffer, which is moved directly into `mData`. No copies of the payload occur during construction. All external references are then `std::shared_ptr`, and the `Batch` type defined in `Types.h` is `std::vector>` — shared ownership is the consistent idiom throughout the nodestore layer. + +## Hash Integrity + +The class comment explicitly states: *"No checking is performed to make sure the hash matches the data."* The hash is accepted on trust from the caller. This is a deliberate performance tradeoff — re-hashing every object on retrieval would be prohibitively expensive given the volume of node reads during ledger processing. The correctness guarantee is maintained at higher layers (SHAMap traversal, ledger validation) rather than at the storage primitive. + +## `CountedObject` Integration + +`NodeObject` inherits from `CountedObject`, a CRTP utility that maintains a global atomic live-instance count. Every constructor call increments the counter; every destructor decrements it. This feeds into the diagnostic reporting system (`CountedObjects::getCounts()`), allowing operators to observe how many `NodeObject` instances are alive at any moment — a useful signal for cache sizing and memory pressure monitoring. The counter itself is lock-free (backed by `std::atomic`), so the bookkeeping overhead on construction and destruction is negligible. + +## Relationship to the Backend Layer + +`NodeObject` is the payload type at every level of the nodestore stack. `Backend::fetch()` produces `std::shared_ptr` instances; `Backend::store()` and `Backend::storeBatch()` consume them. The `Database` interface above `Backend` caches these shared pointers, and the SHAMap layer above that reads them to reconstruct tree nodes. Despite sitting at the bottom of this stack, `NodeObject` itself has no knowledge of any of these consumers — it is a pure value type with no callbacks, virtual functions, or upward dependencies. \ No newline at end of file diff --git a/include/xrpl/nodestore/Scheduler.h.ai.json b/include/xrpl/nodestore/Scheduler.h.ai.json new file mode 100644 index 0000000000..f1f89b8b36 --- /dev/null +++ b/include/xrpl/nodestore/Scheduler.h.ai.json @@ -0,0 +1,41 @@ +{ + "args": [ + { + "lineno": 15, + "name": "fetchType_" + } + ], + "classes": [ + { + "args": [ + "FetchType fetchType_" + ], + "lineno": 13, + "name": "FetchReport" + }, + { + "args": [], + "lineno": 22, + "name": "BatchWriteReport" + }, + { + "args": [], + "lineno": 34, + "name": "Scheduler" + } + ], + "description": "Defines scheduling and reporting interfaces and data structures for asynchronous backend activity in the NodeStore, including fetch and batch write reporting.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/Scheduler.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/Scheduler.h.ai.md b/include/xrpl/nodestore/Scheduler.h.ai.md new file mode 100644 index 0000000000..ae2f3ce4a0 --- /dev/null +++ b/include/xrpl/nodestore/Scheduler.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/nodestore/Scheduler.h` + +## Role and Purpose + +This header defines the scheduling and telemetry interface for the NodeStore's asynchronous backend subsystem. It occupies a narrow but structurally important position: it decouples the database backends from any particular threading strategy, allowing the same backend code to run synchronously in tests or asynchronously on top of the production `JobQueue` without modification. + +The file defines three things: a discriminated enum (`FetchType`) that distinguishes synchronous from asynchronous fetches, two plain-old-data report structs (`FetchReport`, `BatchWriteReport`) that carry performance telemetry, and the abstract `Scheduler` interface that backends call to hand off work and report results. + +## The `Scheduler` Interface + +`Scheduler` is a pure abstract base class with three virtual methods: + +- **`scheduleTask(Task& task)`** — the core dispatch point. A backend calls this when it has deferred work ready to run (typically a batch flush). The contract is deliberately loose: the scheduler may invoke the task immediately on the calling thread, or post it to an unspecified foreign thread. This ambiguity is intentional — it lets production code post write jobs to the `JobQueue` while test code runs them inline. + +- **`onFetch(FetchReport const& report)`** — called after any fetch completes, reporting elapsed time and whether the object was found. This is a telemetry hook, not a control path; backends call it to give the scheduler visibility into I/O performance. + +- **`onBatchWrite(BatchWriteReport const& report)`** — called after a batch write completes, reporting elapsed time and the number of objects written. + +The interface takes the task by non-const reference rather than by value or smart pointer. This is deliberate: `BatchWriter` implements `Task` privately and manages its own lifetime, so no heap allocation is needed for the common case. + +## Report Structs + +`FetchReport` captures whether a fetch was synchronous or asynchronous (via `FetchType`), whether the object was found (`wasFound`), and how long it took (`elapsed`). The `fetchType` member is `const` and set at construction, reinforcing that a report's nature is fixed at the moment of creation. `elapsed` is left zero-initialized via the brace initializer so a partially-filled report can still be passed without undefined fields. + +`BatchWriteReport` captures elapsed time and a `writeCount`. Both structs are simple value types — no virtual methods, no reference members — so they can be created on the stack and passed directly to `onFetch` / `onBatchWrite` without allocating. + +## Two Concrete Implementations + +The header is paired with two concrete schedulers that reveal the full design intent: + +**`DummyScheduler`** (used in tests and unit benchmarks) runs `performScheduledTask()` synchronously and ignores the report callbacks entirely. Its `scheduleTask` is a single-line call-through, making test behavior completely deterministic. + +**`NodeStoreScheduler`** (production) wraps the application's `JobQueue`. Its `scheduleTask` posts a `jtWRITE` job and falls back to synchronous execution if the queue is stopped — a defensive measure to ensure pending flushes complete even during shutdown. Its `onFetch` and `onBatchWrite` call `jobQueue_.addLoadEvents(...)` to feed the load-balancing subsystem, mapping `FetchType::async` to `jtNS_ASYNC_READ` and `FetchType::synchronous` to `jtNS_SYNC_READ`. This means the `Scheduler` interface is simultaneously a dispatch mechanism and a metrics ingestion point, both concerns flowing through the same three methods. + +## Relationship to `BatchWriter` + +`BatchWriter` is the primary consumer of `Scheduler`. It implements `Task` privately, accumulates `NodeObject` stores under a mutex, and calls `scheduler_.scheduleTask(*this)` to trigger a deferred flush. After the flush completes, it constructs a `BatchWriteReport` and calls `scheduler_.onBatchWrite(report)`. This pattern means the scheduler sees every write batch complete without `BatchWriter` knowing anything about threads or job queues. + +## Design Observations + +The design cleanly separates three concerns: *work dispatch* (`scheduleTask`), *fetch telemetry* (`onFetch`), and *write telemetry* (`onBatchWrite`). Grouping all three into a single `Scheduler` interface is slightly surprising — a purist might split telemetry into a separate observer — but it avoids a second injection point and keeps the backends' constructor signatures simple. + +The choice of a raw reference for `scheduleTask(Task& task)` rather than `std::function` or `std::unique_ptr` is a performance-conscious one: it sidesteps heap allocation for the common batch-write case and relies instead on the caller (`BatchWriter`) to manage lifetime and ensure the task object outlives the scheduled execution. \ No newline at end of file diff --git a/include/xrpl/nodestore/Task.h.ai.json b/include/xrpl/nodestore/Task.h.ai.json new file mode 100644 index 0000000000..484c000aec --- /dev/null +++ b/include/xrpl/nodestore/Task.h.ai.json @@ -0,0 +1,30 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "Task" + } + ], + "description": "Defines the Task struct, an interface for scheduled tasks in the NodeStore module of the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/Task.h", + "functions": [ + { + "args": [], + "lineno": 12, + "name": "performScheduledTask" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/Task.h.ai.md b/include/xrpl/nodestore/Task.h.ai.md new file mode 100644 index 0000000000..badfc70fcf --- /dev/null +++ b/include/xrpl/nodestore/Task.h.ai.md @@ -0,0 +1,11 @@ +# `Task.h` — Scheduled Task Interface for NodeStore + +This header defines `Task`, the minimal abstract interface that anchors the NodeStore's asynchronous scheduling system. It is a pure command-pattern base: any piece of deferred backend work inherits from `Task` and implements `performScheduledTask()`. The `Scheduler` interface (`Scheduler.h`) then accepts a `Task&` reference and decides where and when to call that method, decoupling the work unit from any knowledge of threads or job queues. + +The design is deliberately as small as it can be — a single pure virtual method and a virtual destructor. This matters architecturally because the scheduler's contract is intentionally loose: `Scheduler::scheduleTask(Task& task)` may invoke the task synchronously on the calling thread or post it to an unspecified foreign thread depending on the runtime implementation. The only thing the scheduler needs to know is *that* the object is callable, not what it does or how it was allocated. A richer interface (e.g., accepting `std::function` or `std::unique_ptr`) would introduce heap allocation overhead on every scheduled operation and couple the interface to a specific ownership model. + +The primary concrete user of `Task` is `BatchWriter` (`include/xrpl/nodestore/detail/BatchWriter.h`), which inherits from `Task` *privately*. Private inheritance signals that `BatchWriter` is implemented in terms of `Task` — it *uses* the scheduling hook — but should never be treated as a `Task` by external callers. `BatchWriter` accumulates `NodeObject` stores into a buffer, then calls `scheduler_.scheduleTask(*this)` to request a flush. The scheduler later calls back through `performScheduledTask()`, which drains the buffer in a batch write. This cycle means the `BatchWriter` object itself is the task, so no separate allocation is needed and lifetime management stays entirely within `BatchWriter`. + +The virtual destructor follows standard C++ practice for polymorphic base classes. Although production code does not delete `Task` objects through a base pointer (the `Scheduler` holds only a reference, not ownership), the destructor is still required to be virtual in case future implementations do take ownership or the pattern is extended. Without it, deleting a derived object through a `Task*` would produce undefined behavior. + +`DummyScheduler` (`include/xrpl/nodestore/DummyScheduler.h`) implements `Scheduler` for tests and calls `task.performScheduledTask()` synchronously and inline, making test behavior deterministic without any thread infrastructure. This is possible precisely because `Task` carries no thread-affinity or cancellation state — it is a pure callable, and the `DummyScheduler` can invoke it whenever it likes. \ No newline at end of file diff --git a/include/xrpl/nodestore/Types.h.ai.json b/include/xrpl/nodestore/Types.h.ai.json new file mode 100644 index 0000000000..a6a81638ec --- /dev/null +++ b/include/xrpl/nodestore/Types.h.ai.json @@ -0,0 +1,18 @@ +{ + "args": [], + "classes": [], + "description": "Defines constants, enums, and a type alias related to batch writing and status codes for NodeStore backends in the XRPL project.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/Types.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/Types.h.ai.md b/include/xrpl/nodestore/Types.h.ai.md new file mode 100644 index 0000000000..14071ffe1e --- /dev/null +++ b/include/xrpl/nodestore/Types.h.ai.md @@ -0,0 +1,9 @@ +# `include/xrpl/nodestore/Types.h` + +This header is the shared vocabulary file for the `xrpl::NodeStore` subsystem. It sits at the bottom of the NodeStore include hierarchy — `Backend.h` and every other interface header pull it in — so it deliberately contains only the primitives that all participants need to agree on. + +**`Status`** is the return code enum used by every `Backend` operation. The five named values (`ok`, `notFound`, `dataCorrupt`, `unknown`, `backendError`) cover the expected outcomes of a key–value store lookup or write. The `customCode = 100` sentinel reserves the range 0–99 for the standard codes while letting any backend implementation define its own extended error values starting at 100, without risking collision. + +**`Batch`** is a type alias for `std::vector>`. Grouping objects under a shared alias rather than spelling out the type everywhere ensures that if the container or ownership model changes, callsites — including `Backend::storeBatch()` — update from a single definition. The shared-pointer element type reflects that `NodeObject` instances may be concurrently referenced by in-memory caches and the write pipeline at the same time. + +The two anonymous enum constants establish the batch write policy. `batchWritePreallocationSize` (256) is purely a performance hint for `vector::reserve` and does not constrain correctness. `batchWriteLimitSize` (65536) caps a single batch flush, but the inline comment is worth noting: because a new batch can accumulate while the previous one is being written to disk, peak memory for pending objects can reach twice this limit. This double-buffer pattern is a deliberate throughput tradeoff — writers are never blocked waiting for the flush to complete. \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/BatchWriter.h.ai.json b/include/xrpl/nodestore/detail/BatchWriter.h.ai.json new file mode 100644 index 0000000000..69322e7d16 --- /dev/null +++ b/include/xrpl/nodestore/detail/BatchWriter.h.ai.json @@ -0,0 +1,97 @@ +{ + "args": [ + { + "lineno": 32, + "name": "callback" + }, + { + "lineno": 32, + "name": "scheduler" + }, + { + "lineno": 43, + "name": "object" + }, + { + "lineno": 22, + "name": "batch" + } + ], + "classes": [ + { + "args": [ + "Callback& callback", + "Scheduler& scheduler" + ], + "lineno": 16, + "name": "BatchWriter" + }, + { + "args": [], + "lineno": 20, + "name": "Callback" + } + ], + "description": "Provides batch-writing assist logic for NodeStore backends in the xrpl project, allowing scheduled batch writes via a BatchWriter class.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/detail/BatchWriter.h", + "functions": [ + { + "args": [ + "Callback& callback", + "Scheduler& scheduler" + ], + "lineno": 32, + "name": "BatchWriter" + }, + { + "args": [], + "lineno": 36, + "name": "~BatchWriter" + }, + { + "args": [ + "std::shared_ptr const& object" + ], + "lineno": 43, + "name": "store" + }, + { + "args": [], + "lineno": 49, + "name": "getWriteLoad" + }, + { + "args": [], + "lineno": 53, + "name": "performScheduledTask" + }, + { + "args": [], + "lineno": 54, + "name": "writeBatch" + }, + { + "args": [], + "lineno": 55, + "name": "waitForWriting" + }, + { + "args": [ + "Batch const& batch" + ], + "lineno": 22, + "name": "writeBatch" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/BatchWriter.h.ai.md b/include/xrpl/nodestore/detail/BatchWriter.h.ai.md new file mode 100644 index 0000000000..fafc7d63a6 --- /dev/null +++ b/include/xrpl/nodestore/detail/BatchWriter.h.ai.md @@ -0,0 +1,41 @@ +# `BatchWriter.h` — Scheduled Batch-Write Helper for NodeStore Backends + +## Role in the System + +`BatchWriter` is a reusable helper that sits between a NodeStore backend and its storage engine, coalescing individual `NodeObject` writes into larger batches before flushing them to disk. It exists because individual key-value store writes carry per-operation overhead (system call, WAL append, compaction pressure in RocksDB), and amortizing that overhead across a batch of 64 K entries can dramatically reduce I/O latency under write bursts. The class is deliberately optional — a backend can ignore it and implement its own write strategy — but it provides a clean, production-tested path for backends that want standard batching behavior. + +## Architecture: Task + Callback Separation + +`BatchWriter` uses two cooperating abstractions from the same module. It *privately inherits* `Task`, turning the writer itself into a schedulable unit of work. The `Scheduler` interface (defined in `Scheduler.h`) is responsible for deciding *when* and *on which thread* to invoke `performScheduledTask()`. This separation means `BatchWriter` has no threading policy of its own: it produces batches and hands them to a scheduler, which may invoke the flush on the current thread or an asynchronous worker thread. + +The actual write operation is delegated through `BatchWriter::Callback`, a pure virtual inner interface with a single method `writeBatch(Batch const& batch)`. The concrete backend (e.g., `RocksDBBackend` in `RocksDBFactory.cpp`) inherits both `Backend` and `BatchWriter::Callback`, implementing `writeBatch` to call the storage engine. This indirection keeps batching logic entirely within `BatchWriter` while remaining storage-agnostic. + +## Write Flow and Double-Buffer Pattern + +When `store()` is called, the object is pushed into the live accumulation buffer `mWriteSet`. If no write is already scheduled (`mWritePending == false`), the writer sets the flag and calls `m_scheduler.scheduleTask(*this)`. The scheduler will eventually call `performScheduledTask()`, which calls the private `writeBatch()`. + +Inside `writeBatch()`, a critical design choice enables low-latency accumulation during the flush: the mutex is held only long enough to *swap* `mWriteSet` with a fresh local vector, then immediately released. The actual `m_callback.writeBatch(set)` call — which may be slow, involving disk I/O — happens *outside* the lock. This means callers can continue pushing new objects into `mWriteSet` concurrently while the previous batch is being committed to storage. + +The function loops (`for(;;)`) after each flush to check whether new objects accumulated while the write was in progress. This drains the queue completely before clearing `mWritePending` and broadcasting on the condition variable, preventing a race where a scheduler might not re-fire after the last item was enqueued. + +## Backpressure: Flow Control at the Limit + +The `batchWriteLimitSize` constant (65,536 entries, defined in `Types.h`) enforces an upper bound on `mWriteSet`. Inside `store()`, if the batch reaches this limit, the caller *blocks* on `mWriteCondition.wait()` until `writeBatch()` drains the queue and notifies. This provides backpressure that prevents unbounded memory growth during sustained write bursts, at the cost of blocking the calling thread. + +The comment in `Types.h` notes that actual in-flight memory can be up to *twice* `batchWriteLimitSize` — one batch being actively written plus a new one accumulating — which must be accounted for when sizing memory budgets. + +## Mutex Choice: `recursive_mutex` + `condition_variable_any` + +The lock type is `std::recursive_mutex`. This is notable because `condition_variable_any` (rather than the cheaper `condition_variable`) is required to work with non-standard lockable types like recursive mutexes. The recursion capability matters in the `waitForWriting()` path: the destructor calls `waitForWriting()`, which acquires the lock and blocks on the condition variable. If a scheduler happens to be running `writeBatch()` on the same thread (e.g., a synchronous scheduler for testing), recursive acquisition prevents a deadlock. + +## `getWriteLoad()` Metric + +`getWriteLoad()` returns `max(mWriteLoad, mWriteSet.size())`. `mWriteLoad` is set to the size of the batch handed to `writeBatch()` just before the lock is released for the actual write, and `mWriteSet.size()` is the pending accumulation count. The maximum of the two gives callers an estimate of total outstanding write work: either what is being committed right now, or what is waiting for the next scheduled flush. This is used externally to provide I/O load signals for scheduling decisions. + +## Destruction Guarantee + +The destructor calls `waitForWriting()`, which blocks until `mWritePending` is false — meaning the scheduler has flushed and cleared all pending data. This guarantees that no data is silently dropped on shutdown and that the backend's storage layer receives every object that was handed to `store()` before the `BatchWriter` is destroyed. + +## Relationship to `DummyScheduler` + +For unit testing and synchronous backends, the `DummyScheduler` (also in the nodestore module) invokes `performScheduledTask()` immediately on the calling thread inside `scheduleTask()`. This makes `BatchWriter` behave as a synchronous accumulator-then-flush, still correctand safe due to the recursive mutex design. \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/DatabaseNodeImp.h.ai.json b/include/xrpl/nodestore/detail/DatabaseNodeImp.h.ai.json new file mode 100644 index 0000000000..a99349eb3f --- /dev/null +++ b/include/xrpl/nodestore/detail/DatabaseNodeImp.h.ai.json @@ -0,0 +1,181 @@ +{ + "args": [ + { + "lineno": 18, + "name": "scheduler" + }, + { + "lineno": 19, + "name": "readThreads" + }, + { + "lineno": 20, + "name": "backend" + }, + { + "lineno": 21, + "name": "config" + }, + { + "lineno": 22, + "name": "j" + }, + { + "lineno": 44, + "name": "source" + }, + { + "lineno": 49, + "name": "type" + }, + { + "lineno": 49, + "name": "data" + }, + { + "lineno": 49, + "name": "hash" + }, + { + "lineno": 66, + "name": "ledgerSeq" + }, + { + "lineno": 67, + "name": "callback" + }, + { + "lineno": 73, + "name": "fetchReport" + }, + { + "lineno": 73, + "name": "duplicate" + }, + { + "lineno": 76, + "name": "f" + }, + { + "lineno": 62, + "name": "hashes" + } + ], + "classes": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "std::shared_ptr backend", + "Section const& config", + "beast::Journal j" + ], + "lineno": 10, + "name": "DatabaseNodeImp" + } + ], + "description": "Implements the DatabaseNodeImp class, a concrete implementation of the NodeStore::Database interface for XRPL, managing persistent key/value storage using a backend and providing methods for storing, fetching, and synchronizing node objects.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/detail/DatabaseNodeImp.h", + "functions": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "std::shared_ptr backend", + "Section const& config", + "beast::Journal j" + ], + "lineno": 17, + "name": "DatabaseNodeImp" + }, + { + "args": [], + "lineno": 29, + "name": "~DatabaseNodeImp" + }, + { + "args": [], + "lineno": 33, + "name": "getName" + }, + { + "args": [], + "lineno": 38, + "name": "getWriteLoad" + }, + { + "args": [ + "Database& source" + ], + "lineno": 43, + "name": "importDatabase" + }, + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash", + "std::uint32_t" + ], + "lineno": 49, + "name": "store" + }, + { + "args": [ + "std::uint32_t", + "std::uint32_t" + ], + "lineno": 51, + "name": "isSameDB" + }, + { + "args": [], + "lineno": 57, + "name": "sync" + }, + { + "args": [ + "std::vector const& hashes" + ], + "lineno": 62, + "name": "fetchBatch" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t ledgerSeq", + "std::function const&)>&& callback" + ], + "lineno": 65, + "name": "asyncFetch" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t", + "FetchReport& fetchReport", + "bool duplicate" + ], + "lineno": 73, + "name": "fetchNodeObject" + }, + { + "args": [ + "std::function)> f" + ], + "lineno": 76, + "name": "for_each" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/DatabaseNodeImp.h.ai.md b/include/xrpl/nodestore/detail/DatabaseNodeImp.h.ai.md new file mode 100644 index 0000000000..fab4e34739 --- /dev/null +++ b/include/xrpl/nodestore/detail/DatabaseNodeImp.h.ai.md @@ -0,0 +1,31 @@ +# `DatabaseNodeImp` — Single-Backend Node Store Implementation + +`DatabaseNodeImp` is the concrete, non-rotating implementation of the `NodeStore::Database` abstract interface. It represents the standard case in the XRPL node store hierarchy: a single persistent key/value backend (such as NuDB or RocksDB) that holds all ledger node objects regardless of their ledger sequence number. + +## Role in the NodeStore Hierarchy + +The NodeStore subsystem exposes two concrete `Database` implementations. `DatabaseNodeImp` targets deployments where a single, stable backend suffices — full history nodes or configurations that don't need online rotation of storage. Its counterpart, `DatabaseRotatingImp`, manages two backends simultaneously (a writable store and an archive) and supports hot rotation between them. Both classes derive from `Database`, which provides the thread pool for asynchronous reads, telemetry counters, and the public `fetchNodeObject()` dispatch. + +`DatabaseNodeImp` holds exactly one `std::shared_ptr backend_`, which is asserted non-null at construction. Ownership is shared so the same backend can potentially be observed elsewhere, but the database is the authoritative lifecycle manager: `stop()` is called in the destructor to drain all pending I/O before releasing the pointer. + +## Ledger Sequence Is Irrelevant Here + +A notable design point across most of `DatabaseNodeImp`'s overrides is that the `std::uint32_t` ledger sequence parameter is silently ignored. When there is one backend for all data, any sequence number resolves to the same physical store, making the sequence irrelevant at this layer. `isSameDB()` expresses this explicitly — it unconditionally returns `true` for any pair of sequence numbers, documenting the invariant directly in code. `DatabaseRotatingImp` also returns `true` for its analogous reason (the two-backend rotating store still acts as one logical database), but `DatabaseNodeImp`'s version is simpler because there are no locks or backend pointers to check. + +## Store and Fetch Paths + +`store()` is minimal: it updates telemetry via `storeStats()`, wraps the raw blob into a `NodeObject`, and forwards it to `backend_->store()`. The move semantics on `data` ensure the potentially large payload is transferred without copying. + +`fetchNodeObject()` is the private virtual that the base class calls from its public `fetchNodeObject()` dispatcher. It delegates directly to `backend_->fetch()` inside a `try/catch`, re-throwing via `Rethrow()` if an exception escapes the backend — this ensures the exception propagates faithfully up the call stack rather than being swallowed. The status code returned by the backend is checked for three cases: `ok` and `notFound` are both silent (the caller inspects the returned pointer), `dataCorrupt` logs at fatal severity, and any other unknown status code logs at warn. If a `NodeObject` was retrieved, `fetchReport.wasFound` is set to signal a cache hit to the base class telemetry machinery. + +`asyncFetch()` simply forwards to `Database::asyncFetch()`. The base class manages the read-thread pool and the pending-read map; `DatabaseNodeImp` does not need any additional indirection here, unlike a rotating database that would need to determine which backend to query. + +## Batch Fetching + +`fetchBatch()` is a public (non-virtual) method that fetches multiple node objects by hash in one backend call. After delegating to `backend_->fetchBatch()`, it applies a defensive resize: the assertion and subsequent `results.resize(hashes.size())` guard against backends that return fewer results than requested, keeping the output vector positionally aligned with the input hash vector. Missing entries (null pointers in the result) are logged at error level. Timing is measured with `steady_clock` and reported via `updateFetchMetrics()`, though hits are reported as zero because batch fetches don't interact with any cache layer at this level. + +## Delegation Pattern + +Every public method on `DatabaseNodeImp` is a thin delegation to either `backend_` or a base-class helper. `getName()` and `getWriteLoad()` pass through to the backend directly. `sync()` calls `backend_->sync()`. `importDatabase()` calls `importInternal()`, a protected base-class utility that iterates the source database via `for_each()` and bulk-stores objects into the given destination backend. `for_each()` itself simply calls `backend_->for_each()`. + +This strict delegation approach keeps `DatabaseNodeImp` cohesive: it owns no business logic beyond adapting the `Database` virtual interface onto the `Backend` interface. The architectural complexity lives in the `Database` base (thread pool, async dispatch, telemetry) and in the concrete `Backend` implementations (storage format, compression, file management). \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h.ai.json b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h.ai.json new file mode 100644 index 0000000000..0d06b84c6e --- /dev/null +++ b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h.ai.json @@ -0,0 +1,171 @@ +{ + "args": [ + { + "lineno": 19, + "name": "scheduler" + }, + { + "lineno": 20, + "name": "readThreads" + }, + { + "lineno": 21, + "name": "writableBackend" + }, + { + "lineno": 22, + "name": "archiveBackend" + }, + { + "lineno": 23, + "name": "config" + }, + { + "lineno": 24, + "name": "j" + }, + { + "lineno": 32, + "name": "newBackend" + }, + { + "lineno": 33, + "name": "f" + }, + { + "lineno": 45, + "name": "source" + }, + { + "lineno": 54, + "name": "type" + }, + { + "lineno": 54, + "name": "data" + }, + { + "lineno": 54, + "name": "hash" + }, + { + "lineno": 63, + "name": "fetchReport" + }, + { + "lineno": 63, + "name": "duplicate" + } + ], + "classes": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "std::shared_ptr writableBackend", + "std::shared_ptr archiveBackend", + "Section const& config", + "beast::Journal j" + ], + "lineno": 11, + "name": "DatabaseRotatingImp" + } + ], + "description": "This file defines the DatabaseRotatingImp class, an implementation of the DatabaseRotating interface for managing a rotating node store database in the XRPL NodeStore module.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/detail/DatabaseRotatingImp.h", + "functions": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "std::shared_ptr writableBackend", + "std::shared_ptr archiveBackend", + "Section const& config", + "beast::Journal j" + ], + "lineno": 18, + "name": "DatabaseRotatingImp" + }, + { + "args": [], + "lineno": 26, + "name": "~DatabaseRotatingImp" + }, + { + "args": [ + "std::unique_ptr&& newBackend", + "std::function const& f" + ], + "lineno": 30, + "name": "rotate" + }, + { + "args": [], + "lineno": 36, + "name": "getName" + }, + { + "args": [], + "lineno": 40, + "name": "getWriteLoad" + }, + { + "args": [ + "Database& source" + ], + "lineno": 44, + "name": "importDatabase" + }, + { + "args": [ + "std::uint32_t", + "std::uint32_t" + ], + "lineno": 48, + "name": "isSameDB" + }, + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash", + "std::uint32_t" + ], + "lineno": 54, + "name": "store" + }, + { + "args": [], + "lineno": 56, + "name": "sync" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t", + "FetchReport& fetchReport", + "bool duplicate" + ], + "lineno": 63, + "name": "fetchNodeObject" + }, + { + "args": [ + "std::function)> f" + ], + "lineno": 67, + "name": "for_each" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/DatabaseRotatingImp.h.ai.md b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h.ai.md new file mode 100644 index 0000000000..3c17f6eedb --- /dev/null +++ b/include/xrpl/nodestore/detail/DatabaseRotatingImp.h.ai.md @@ -0,0 +1,75 @@ +# `DatabaseRotatingImp` — Concrete Rotating NodeStore Backend + +## Role in the System + +The XRPL node store persists every ledger object (SHAMap nodes, transactions, account states) as keyed binary blobs identified by their 256-bit hash. Over time, old ledger data accumulates and must be pruned without downtime — the "online deletion" feature. `DatabaseRotatingImp` is the concrete class that implements this rotation strategy by maintaining two simultaneously live storage backends: a **writable** backend that accepts new writes and a read-only **archive** backend holding older data scheduled for eventual deletion. When online deletion fires, the old archive is discarded, the writable backend is demoted to archive, and a freshly created backend becomes the new writable target. The header declares the class interface; the implementation lives in `src/libxrpl/nodestore/DatabaseRotatingImp.cpp`. + +The class sits at the bottom of a three-level inheritance chain: `Database` → `DatabaseRotating` → `DatabaseRotatingImp`. `Database` provides the threading infrastructure (async read threads, statistics counters, `fetchNodeObject()` public façade), `DatabaseRotating` declares the pure-virtual `rotate()` contract, and `DatabaseRotatingImp` supplies every concrete method. + +## Backend Lifecycle and the `rotate()` Operation + +The heart of the class is `rotate()`. When called, it must atomically swap out the old archive, promote the current writable to archive, and install the new writable — all while concurrent reads and writes may be in flight. + +```cpp +void rotate( + std::unique_ptr&& newBackend, + std::function const& f) override; +``` + +The implementation captures the old archive in a local `shared_ptr` (`oldArchiveBackend`), performs the pointer swap under `mutex_`, then calls the callback `f` outside the lock. The critical design choice is that `oldArchiveBackend` remains alive until *after* `f` returns. This is intentional: the callback is used by the caller (online deletion logic) to update external metadata — for example, marking which backend directories are canonical in a configuration record. Only once `f` completes and `oldArchiveBackend` falls out of scope does the backend destructor run, physically deleting the old storage directory via the previously set `setDeletePath()` flag. This sequencing prevents the caller's metadata from pointing to a path that has already been removed. + +The three-step pointer shuffle under the lock is: +1. Mark old archive for deletion. +2. Move old archive into `oldArchiveBackend` (local RAII guard). +3. Demote `writableBackend_` → `archiveBackend_`. +4. Install `newBackend` → `writableBackend_`. + +## Locking Strategy: Snapshot-and-Release + +`DatabaseRotatingImp` uses `mutex_` only to snapshot `shared_ptr` references, never while doing actual I/O. In every read/write path the pattern is: + +```cpp +auto const backend = [&] { + std::lock_guard lock(mutex_); + return writableBackend_; // copy the shared_ptr +}(); +// I/O happens here, no lock held +backend->store(nObj); +``` + +This is the right design because backend I/O (especially to RocksDB or NuDB) can take milliseconds, and holding `mutex_` during I/O would serialize all access including `rotate()` calls. Instead, the `shared_ptr` copy keeps the backend alive even if a rotation fires concurrently. + +`fetchNodeObject()` takes this a step further: it snapshots *both* pointers together in a single lock acquisition, then attempts the writable backend first and the archive backend second — all without holding the lock. If the object is found only in the archive and the caller sets `duplicate = true`, the method re-acquires the lock to refresh the writable pointer (in case a rotation occurred during the archive lookup), then promotes the object back into the current writable backend. This forward-migration of archive data is how the system ensures hot data is not accidentally swept away by the next rotation cycle. + +## Fetch Fallthrough and Data Promotion + +The fetch path is a deliberate two-tier lookup: + +1. Check the writable backend (recent data, faster). +2. On miss, check the archive backend (older data, possibly on its way out). +3. If found in the archive and `duplicate == true`, write a copy back into the writable backend. + +The `duplicate` flag is supplied by the upper-layer cache logic in `Database::fetchNodeObject()` and enables background data migration: objects retrieved from the archive are refreshed into the writable tier so that when the archive is eventually rotated out, all accessed objects are already safe in the new backend. Objects that have not been accessed since the last rotation simply disappear with the old archive. + +## Logical Identity and Ignored Parameters + +`isSameDB()` unconditionally returns `true`. The base `Database` contract uses this to determine whether two ledger sequence numbers would be answered by the same physical store; for the rotating database both backends together form one logical namespace, so the answer is always yes. + +Similarly, the `std::uint32_t ledgerSeq` parameters throughout `store()` and `fetchNodeObject()` are deliberately unnamed and unused — the rotating store does not partition data by ledger sequence the way sharded databases do. + +## Resource Management + +The constructor aggregates `fdRequired_` from both backends: + +```cpp +if (writableBackend_) + fdRequired_ += writableBackend_->fdRequired(); +if (archiveBackend_) + fdRequired_ += archiveBackend_->fdRequired(); +``` + +This propagates the sum to the base class so the process can correctly reserve file descriptors at startup before both backends are open. + +The destructor calls `stop()` (inherited from `Database`), which drains async read threads before the backends are released. This ordering is essential: the async threads hold `shared_ptr` references to backends and would dereference freed memory if allowed to outlive the destructor. + +`sync()` flushes only the writable backend. The archive backend is read-only at rotation time and needs no explicit flush. \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/DecodedBlob.h.ai.json b/include/xrpl/nodestore/detail/DecodedBlob.h.ai.json new file mode 100644 index 0000000000..09f4cc4b61 --- /dev/null +++ b/include/xrpl/nodestore/detail/DecodedBlob.h.ai.json @@ -0,0 +1,61 @@ +{ + "args": [ + { + "lineno": 18, + "name": "key" + }, + { + "lineno": 18, + "name": "value" + }, + { + "lineno": 18, + "name": "valueBytes" + } + ], + "classes": [ + { + "args": [ + "key", + "value", + "valueBytes" + ], + "lineno": 15, + "name": "DecodedBlob" + } + ], + "description": "Defines the DecodedBlob class, which parses raw key/value blobs into NodeObject components, checks for consistency, and allows creation of NodeObject instances from the parsed data.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/detail/DecodedBlob.h", + "functions": [ + { + "args": [ + "key", + "value", + "valueBytes" + ], + "lineno": 18, + "name": "DecodedBlob" + }, + { + "args": [], + "lineno": 21, + "name": "wasOk" + }, + { + "args": [], + "lineno": 27, + "name": "createObject" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/DecodedBlob.h.ai.md b/include/xrpl/nodestore/detail/DecodedBlob.h.ai.md new file mode 100644 index 0000000000..3190e20f2a --- /dev/null +++ b/include/xrpl/nodestore/detail/DecodedBlob.h.ai.md @@ -0,0 +1,51 @@ +# `DecodedBlob.h` — NodeStore Binary Format Parser + +## Role in the System + +`DecodedBlob` is the deserialization half of the NodeStore binary format, paired symmetrically with `EncodedBlob`. Its job is to parse the raw key/value bytes that a storage backend (NuDB, RocksDB) has retrieved from disk and reconstruct the components needed to build a live `NodeObject`. The class comment makes an important guarantee explicit: this **defines the database on-disk format** for a `NodeObject`, making it the canonical reference for that contract. + +## The On-Disk Format + +The constructor comment in `DecodedBlob.cpp` documents the binary layout directly: + +``` +Bytes 0–7 Unused (historically stored ledger index in older versions) +Byte 8 NodeObjectType (one-byte enum) +Bytes 9–end Object payload (the raw serialized ledger data) +``` + +The 8-byte prefix is a legacy artifact. Earlier code used those bytes to store a ledger index. `EncodedBlob` now writes them as eight zero bytes, and `DecodedBlob` silently ignores them. A developer comment in the implementation (`VFALCO NOTE What about bytes 4 through 7 inclusive?`) reflects the surviving uncertainty around the original intent, but the current code treats all eight as padding. + +## Non-Throwing Validation Pattern + +Rather than throwing on malformed input, `DecodedBlob` stores a success flag (`m_success`) and exposes it through `wasOk()`. This is a deliberate design choice for a storage layer that must handle real-world corruption gracefully. Callers follow a two-step idiom: construct, check, then conditionally materialize: + +```cpp +DecodedBlob decoded(hash.data(), result.first, result.second); +if (!decoded.wasOk()) { + status = dataCorrupt; + return; +} +auto object = decoded.createObject(); +``` + +`createObject()` asserts `m_success` (via `XRPL_ASSERT`) rather than throwing, making it a programming error to call it on a failed decode. This separates the "is this valid?" question from the "give me the object" action. + +Validation is intentional and minimal: the constructor checks that `valueBytes > 8` (sufficient to read the type byte), then that `valueBytes > 9` (there is at least some payload), and that `m_objectType` maps to one of the four recognized `NodeObjectType` values — `hotUNKNOWN`, `hotLEDGER`, `hotACCOUNT_NODE`, or `hotTRANSACTION_NODE`. Notably, `hotDUMMY` (value 512, defined as an invalid sentinel) falls through the switch's default case and leaves `m_success = false`. The class header is honest that not all corruption is detected; this is a fast sanity check, not a cryptographic integrity proof. + +## Memory Ownership Model + +`DecodedBlob` is a non-owning view into the raw buffer provided by the caller. The private members `m_key` and `m_objectData` are raw `const` pointers that alias the incoming `key` and `value` arguments — no copies are made during parsing. The caller must keep the backing buffer alive while a `DecodedBlob` is live. + +`createObject()` breaks that constraint by copying the payload bytes into a newly allocated `Blob`: + +```cpp +Blob data(m_objectData, m_objectData + m_dataBytes); +object = NodeObject::createObject(m_objectType, std::move(data), uint256::fromVoid(m_key)); +``` + +The resulting `NodeObject` owns its data independently. This means storage backends can safely release their fetch buffers immediately after `createObject()` returns. + +## Relationship to `EncodedBlob` + +`EncodedBlob` performs the inverse operation: given a `NodeObject`, it serializes the type byte into offset 8 and copies the payload starting at offset 9, producing the exact byte layout that `DecodedBlob` expects to parse back. The test in `Basics_test.cpp` exercises this round-trip directly — encoding a batch of objects and decoding each one, asserting `wasOk()` and verifying field-by-field equality. Together these two classes define and enforce the full on-disk contract for node objects in the XRPL nodestore. \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/EncodedBlob.h.ai.json b/include/xrpl/nodestore/detail/EncodedBlob.h.ai.json new file mode 100644 index 0000000000..91add2b068 --- /dev/null +++ b/include/xrpl/nodestore/detail/EncodedBlob.h.ai.json @@ -0,0 +1,59 @@ +{ + "args": [ + { + "lineno": 44, + "name": "obj" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const& obj" + ], + "lineno": 32, + "name": "EncodedBlob" + } + ], + "description": "Defines the EncodedBlob class, which serializes a NodeObject into a database format with a specific prefix, type byte, and payload, optimizing for in-stack allocation and minimizing dynamic memory usage.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/detail/EncodedBlob.h", + "functions": [ + { + "args": [ + "obj" + ], + "lineno": 44, + "name": "EncodedBlob" + }, + { + "args": [], + "lineno": 59, + "name": "~EncodedBlob" + }, + { + "args": [], + "lineno": 70, + "name": "getKey" + }, + { + "args": [], + "lineno": 76, + "name": "getSize" + }, + { + "args": [], + "lineno": 82, + "name": "getData" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/EncodedBlob.h.ai.md b/include/xrpl/nodestore/detail/EncodedBlob.h.ai.md new file mode 100644 index 0000000000..0822c577d1 --- /dev/null +++ b/include/xrpl/nodestore/detail/EncodedBlob.h.ai.md @@ -0,0 +1,70 @@ +# `EncodedBlob` — NodeObject Serialization for Database Storage + +`EncodedBlob` is a short-lived serialization adapter in the `xrpl::NodeStore` detail layer. Its sole job is to transform an in-memory `NodeObject` into the raw binary format that backend storage engines (NuDB, RocksDB) expect, without involving any intermediate string or vector. The class lives in `include/xrpl/nodestore/detail/` alongside its inverse counterpart `DecodedBlob`, together defining the on-disk format for all ledger node objects. + +## Wire Format + +The serialized payload produced by `EncodedBlob` has a fixed 9-byte header followed by the raw object data: + +| Bytes | Content | +|---|---| +| 0–7 | Eight prefix bytes, always zero (historically stored the ledger index; zeroed since that was removed) | +| 8 | `NodeObjectType` cast to a single `uint8_t` — one of `hotLEDGER`, `hotACCOUNT_NODE`, `hotTRANSACTION_NODE`, etc. | +| 9–N | The raw payload blob from `NodeObject::getData()` | + +The 32-byte database key is the object's `uint256` hash, stored separately from the payload. The two accessor methods `getKey()` and `getData()` expose these distinct pieces to backend drivers as `void const*` pointers suitable for direct hand-off to NuDB or RocksDB slice APIs. + +## Stack-First Allocation Strategy + +The most important design decision in this class is how it manages memory. `EncodedBlob` is always constructed on the stack — typically as a local variable in a backend's `store` or `do_insert` method — so the object itself costs nothing to allocate. The class takes full advantage of this by embedding a 1033-byte inline buffer (`payload_`) directly in the struct: + +```cpp +std::array + payload_{}; +``` + +The `boost::alignment::align_up` call sizes the array to exactly cover the 9-byte header plus 1024 bytes of payload, rounded up to `uint32_t` alignment so that no compiler padding bytes follow. If the serialized size fits within this budget, `ptr_` points directly into `payload_` — no heap allocation occurs. If the payload exceeds 1024 bytes, the constructor falls back to `new uint8_t[size_]`. The comment in the source documents that roughly 94% of real-world node objects fall below the 1024-byte threshold, meaning heap allocation is rare in practice. + +```cpp +ptr_((size_ <= payload_.size()) ? payload_.data() : new std::uint8_t[size_]) +``` + +`ptr_` is declared `uint8_t* const` — it is set once at construction and never changes. This immutability makes the ownership semantics unambiguous: the destructor just tests `ptr_ != payload_.data()` to decide whether to `delete[]` the pointer. There is no copy constructor or move constructor; the class is inherently non-copyable because copying would require duplicating conditional ownership of a raw heap pointer. + +## Invariant Enforcement + +The destructor contains an `XRPL_ASSERT` that cross-checks both conditions simultaneously: + +```cpp +XRPL_ASSERT( + ((ptr_ == payload_.data()) && (size_ <= payload_.size())) || + ((ptr_ != payload_.data()) && (size_ > payload_.size())), + ...); +``` + +This catches any state where the pointer and size have drifted out of sync — a situation that would otherwise lead to either a double-free or a memory leak. The assert fires in debug builds; in release builds the `if (ptr_ != payload_.data()) delete[] ptr_` branch handles cleanup unconditionally. + +The constructor also defends against a null `shared_ptr` using `XRPL_ASSERT` followed by an explicit `throw`, which is an intentional redundancy: the assert fires early in debug builds, while the exception protects against the same bug in release builds where asserts are stripped. + +## Relationship to `DecodedBlob` + +`DecodedBlob` is the inverse transformation. It takes a raw `(key, value, valueBytes)` tuple from a database read, parses the 9-byte header, validates the type byte, and produces a `NodeObject` via `createObject()`. Where `EncodedBlob` produces the wire format, `DecodedBlob` consumes it — together they form the complete serialization boundary between the in-memory ledger graph and persistent storage. + +## Usage Pattern + +In both the NuDB and RocksDB backends, `EncodedBlob` is constructed immediately before the backend's insert call and discarded immediately after: + +```cpp +// NuDBBackend::do_insert +EncodedBlob const e(no); +auto const result = nodeobject_compress(e.getData(), e.getSize(), bf); +db_.insert(e.getKey(), result.first, result.second, ec); + +// RocksDBBackend::storeBatch +EncodedBlob const encoded(e); +wb.Put( + rocksdb::Slice(...encoded.getKey()..., m_keyBytes), + rocksdb::Slice(...encoded.getData()..., encoded.getSize())); +``` + +This pattern ensures the object's lifetime is scoped tightly around the I/O call, so any heap-allocated overflow buffer is freed immediately. The `[[nodiscard]]` attributes on the accessors prevent callers from accidentally discarding the return values of `getKey()`, `getSize()`, or `getData()`, which would make the entire operation a no-op. \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/ManagerImp.h.ai.json b/include/xrpl/nodestore/detail/ManagerImp.h.ai.json new file mode 100644 index 0000000000..107ec4f000 --- /dev/null +++ b/include/xrpl/nodestore/detail/ManagerImp.h.ai.json @@ -0,0 +1,87 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "ManagerImp" + } + ], + "description": "Defines the ManagerImp class, an implementation of the NodeStore::Manager interface for managing backend factories and creating Backend and Database instances in the XRPL node store.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/detail/ManagerImp.h", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "instance" + }, + { + "args": [], + "lineno": 18, + "name": "missing_backend" + }, + { + "args": [], + "lineno": 21, + "name": "ManagerImp" + }, + { + "args": [], + "lineno": 23, + "name": "~ManagerImp" + }, + { + "args": [ + "name" + ], + "lineno": 26, + "name": "find" + }, + { + "args": [ + "factory" + ], + "lineno": 29, + "name": "insert" + }, + { + "args": [ + "factory" + ], + "lineno": 32, + "name": "erase" + }, + { + "args": [ + "parameters", + "burstSize", + "scheduler", + "journal" + ], + "lineno": 35, + "name": "make_Backend" + }, + { + "args": [ + "burstSize", + "scheduler", + "readThreads", + "config", + "journal" + ], + "lineno": 41, + "name": "make_Database" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/ManagerImp.h.ai.md b/include/xrpl/nodestore/detail/ManagerImp.h.ai.md new file mode 100644 index 0000000000..26bf9fa4e2 --- /dev/null +++ b/include/xrpl/nodestore/detail/ManagerImp.h.ai.md @@ -0,0 +1,35 @@ +# `ManagerImp.h` — NodeStore Backend Registry and Factory Singleton + +## Role in the System + +`ManagerImp` is the concrete singleton that powers the NodeStore factory-registry pattern. It sits one layer beneath the public `Manager` interface, hidden inside the `detail/` directory to signal it is an implementation detail rather than a stable API. Its job is to maintain a runtime registry of `Factory` objects, each representing a pluggable storage backend (NuDB, RocksDB, Null, Memory), and to orchestrate the construction of `Backend` and `Database` instances from configuration data. + +The `Manager` abstract class, defined in `Manager.h`, declares the singleton interface and exposes `Manager::instance()`, but that method simply delegates to `ManagerImp::instance()`, keeping the implementation type invisible to callers outside the `nodestore` subsystem. + +## Singleton Lifecycle and Static Initialization Safety + +`ManagerImp::instance()` uses the Meyers' singleton pattern — a function-local `static ManagerImp _` — which is guaranteed thread-safe in C++11 and avoids the static initialization order fiasco for construction. The constructor immediately registers all four built-in factories by calling free functions: `registerNuDBFactory`, `registerRocksDBFactory`, `registerNullFactory`, and `registerMemoryFactory`, each of which calls `insert()` on the manager they receive. + +A critical design comment in the `.cpp` explains the choice to use free-function registration rather than global `Factory` objects: if a `Factory` were itself a global, its destructor might call `Manager::instance().erase()` after `ManagerImp` had already been destroyed (C++ gives no reliable order guarantee for global destructor calls). That would be undefined behavior. By calling the register functions from the `ManagerImp` constructor and not relying on `Factory` destructors to call `erase()`, the design avoids this pitfall entirely. + +## Factory Registry: Insertion, Removal, and Lookup + +The registry is a `std::vector` — raw, non-owning pointers — protected by a `std::mutex`. This pairing is the minimal mechanism needed: `insert()` and `erase()` are called during startup and shutdown, but `find()` is the hot path called whenever a backend is being instantiated. + +`find()` performs a case-insensitive linear scan using `boost::iequals`, which accommodates configuration files that might spell a backend name as `"NuDB"`, `"nudb"`, or `"NUDB"` interchangeably. With four registered backends, the linear scan cost is negligible. The mutex is held across the full scan, ensuring correctness even if `erase()` races with a lookup. + +`erase()` uses `XRPL_ASSERT` to enforce the invariant that only previously-inserted factories are removed. An attempt to erase an unknown factory is treated as a programming error, not a recoverable condition. + +## Backend and Database Construction + +`make_Backend()` reads the `"type"` key from the configuration `Section`, finds the matching factory, and delegates to `factory->createInstance()`, supplying `NodeObject::keyBytes` as the fixed key size. If the type is missing or unrecognized, `missing_backend()` throws a `std::runtime_error` with a clear user-facing message pointing to the `[node_db]` configuration stanza. This error function is `static` — it encapsulates the one diagnostic message that should be consistent everywhere a missing backend is detected, and is reused in both the empty-type and unrecognized-type code paths. + +`make_Database()` composes on top of `make_Backend()`: it creates and opens the raw `Backend`, then wraps it inside a `DatabaseNodeImp`, which provides the full `Database` interface (async read threads, caching, ledger-sequence-aware dispatch). The explicit `backend->open()` call before wrapping is architecturally significant — it separates the construction of a backend object from the act of opening the underlying storage, allowing the error to be surfaced before the full `DatabaseNodeImp` stack is built. + +## Relationship to Sibling Detail Classes + +Within the `detail/` directory, `ManagerImp.h` is the entry point for all database creation. `DatabaseNodeImp.h` defines what `make_Database()` ultimately returns: a single-backend, non-rotating database with async fetch and a persistent `Backend` held via `std::shared_ptr`. The rotating variant (`DatabaseRotatingImp`) is constructed elsewhere (for hot/cold storage separation) and is not managed by `ManagerImp::make_Database()`. `BatchWriter`, `EncodedBlob`, `DecodedBlob`, and the codec utilities are backend-level concerns used inside individual `Factory` implementations, not by `ManagerImp` itself. + +## Design Tradeoffs + +Storing `Factory*` raw pointers instead of `shared_ptr` or `unique_ptr` means the manager never owns the factories. Ownership is intentionally left to the caller (in practice, to static storage managed by the registration free-functions). This avoids double-free scenarios and keeps the registry lightweight. The tradeoff is that callers must ensure factories outlive the manager — a contract enforced by the singleton-lifetime design rather than by any runtime check. \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/codec.h.ai.json b/include/xrpl/nodestore/detail/codec.h.ai.json new file mode 100644 index 0000000000..f6c87e6533 --- /dev/null +++ b/include/xrpl/nodestore/detail/codec.h.ai.json @@ -0,0 +1,81 @@ +{ + "args": [ + { + "lineno": 15, + "name": "in" + }, + { + "lineno": 15, + "name": "in_size" + }, + { + "lineno": 15, + "name": "bf" + } + ], + "classes": [], + "description": "Provides compression and decompression utilities (including LZ4 and custom inner node codecs) for XRPL NodeStore NodeObject blobs, including functions for compressing, decompressing, and filtering node data.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/detail/codec.h", + "functions": [ + { + "args": [ + "in", + "in_size", + "bf" + ], + "lineno": 15, + "name": "lz4_decompress" + }, + { + "args": [ + "in", + "in_size", + "bf" + ], + "lineno": 36, + "name": "lz4_compress" + }, + { + "args": [ + "in", + "in_size", + "bf" + ], + "lineno": 74, + "name": "nodeobject_decompress" + }, + { + "args": [], + "lineno": 154, + "name": "zero32" + }, + { + "args": [ + "in", + "in_size", + "bf" + ], + "lineno": 161, + "name": "nodeobject_compress" + }, + { + "args": [ + "in", + "in_size" + ], + "lineno": 227, + "name": "filter_inner" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + }, + { + "lineno": 13, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/codec.h.ai.md b/include/xrpl/nodestore/detail/codec.h.ai.md new file mode 100644 index 0000000000..cacde0caee --- /dev/null +++ b/include/xrpl/nodestore/detail/codec.h.ai.md @@ -0,0 +1,64 @@ +# `include/xrpl/nodestore/detail/codec.h` + +## Role in the System + +This header is the compression gateway for the XRPL NodeStore. Every `NodeObject` blob written to or read from a NuDB backend passes through either `nodeobject_compress` or `nodeobject_decompress`. Its job is to define the on-disk wire format for stored blobs and provide the logic to encode and decode them, balancing storage efficiency against decoding speed. + +The file sits in `nodestore/detail/`, signaling it is an implementation concern rather than a public API — callers are the NuDB backend (`NuDBFactory.cpp`) and the database import tool (`import_test.cpp`). + +--- + +## On-Disk Format: Four Type Tags + +Every stored blob begins with a varint type tag, dispatched by both `nodeobject_compress` and `nodeobject_decompress`: + +| Type | Meaning | +|------|---------| +| `0` | Uncompressed raw data (legacy; never written now) | +| `1` | LZ4-compressed payload | +| `2` | Compressed inner-node encoding (sparse, with a 16-bit bitmask) | +| `3` | Full inner-node encoding (all 16 hashes present) | + +The type 0 path still exists in the decoder for backward compatibility with older on-disk data, but `nodeobject_compress` has a comment explicitly noting "we always compress now" — `codecType` is hardcoded to `1` for all non-inner-node objects. + +--- + +## Inner-Node Special Encoding (Types 2 and 3) + +The most architecturally interesting code handles SHAMap inner nodes. These are exactly 525 bytes: a 13-byte header (`uint32` index, `uint32` unused, `uint8` kind, `uint32` `HashPrefix`) followed by 16 × 32-byte child hash slots. Their fixed size and high zero density make them amenable to a compact custom codec that out-performs general-purpose LZ4 for this common case. + +**Detection.** The compressor detects inner nodes by checking two conditions: `in_size == 525` and that the 4-byte prefix field equals `HashPrefix::innerNode` (the `'M','I','N'` hash-prefix constant). This dual check guards against false positives from other 525-byte objects. + +**Sparse encoding (type 2).** When fewer than all 16 child slots are populated, `nodeobject_compress` scans the 16 slots using `zero32()` (a zero-initialized 32-byte static sentinel) to identify non-empty slots. It builds a `uint16_t` bitmask where bit `0x8000` is slot 0 and packs only the non-zero hashes contiguously. The result: for a half-filled inner node (8 children), the stored size drops from 512 bytes of hashes to 2 (mask) + 256 (hashes) = 258 bytes, plus the varint type tag. + +**Full inner node (type 3).** When all 16 slots are occupied, the bitmask is skipped entirely and all 512 hash bytes are stored directly after the type varint. This avoids spending 2 bytes on a mask that would be `0xFFFF`. + +**Reconstruction.** On decompression, both type 2 and type 3 paths allocate 525 bytes and reconstruct the full blob, but with a critical detail: the `index`, `unused`, and `kind` fields are written as zeros (`hotUNKNOWN`), while `prefix` is written from `HashPrefix::innerNode`. This means the stored format intentionally discards the mutable metadata (ledger sequence, object type) and only retains the structural hash prefix and the child hashes themselves. + +--- + +## LZ4 Primitives (`lz4_compress` / `lz4_decompress`) + +These two low-level templates wrap the LZ4 C API behind the `BufferFactory` pattern. `lz4_compress` calls `LZ4_compressBound` to determine worst-case output size, then asks the factory for a single allocation large enough for both a leading varint (the original uncompressed size) and the compressed data. The varint is stored first so the decompressor can pre-allocate the output buffer in a single step rather than guessing or resizing. + +`lz4_decompress` validates the varint first (including an explicit integer overflow check for both input and output sizes before casting to `int`), allocates the exact output size via the factory, then calls `LZ4_decompress_safe`. It verifies that the returned byte count matches the expected output size and throws `std::runtime_error` on any mismatch. + +The `#define LZ4_DISABLE_DEPRECATE_WARNINGS` at the top works around a known incompatibility between clang's deprecation attributes and certain LZ4 API declarations, a pragmatic suppression that avoids polluting build logs. + +--- + +## The `BufferFactory` Pattern + +All six functions accept a `BufferFactory&&` — a callable of the form `void*(std::size_t n)` that allocates `n` bytes and returns the pointer. This design decouples allocation policy from codec logic entirely. In production use (NuDB backend), the caller passes a `nudb::detail::buffer`, which provides stack-local scratch space for small allocations and falls back to the heap for larger ones, avoiding a heap round-trip for every read. The codec never frees memory; ownership belongs entirely to the factory object the caller controls. + +--- + +## `filter_inner()` and Codec Verification + +`filter_inner` modifies an inner-node blob in place, zeroing the first 9 bytes (the `index`, `unused`, and `kind` fields). This is used by the NuDB import tool before calling `nodeobject_compress` so that the round-trip verification `memcmp` succeeds: since the compressor reconstructs inner nodes with those fields zeroed, comparing the raw original against the decompressed output would fail unless the source is first normalized. The function is a no-op for any blob that is not exactly 525 bytes or does not carry the `HashPrefix::innerNode` marker. + +--- + +## Error Handling + +All error paths throw `std::runtime_error` with descriptive messages that include runtime values (`in_size`, index `i`, etc.). Integer overflow is checked explicitly before any `static_cast` conversion into the LZ4 API. Structural invariants (mask non-zero, exact byte counts consumed, exact output sizes) are each individually validated, so a corrupted on-disk blob produces a named error rather than undefined behavior. \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/varint.h.ai.json b/include/xrpl/nodestore/detail/varint.h.ai.json new file mode 100644 index 0000000000..077ccbad06 --- /dev/null +++ b/include/xrpl/nodestore/detail/varint.h.ai.json @@ -0,0 +1,112 @@ +{ + "args": [ + { + "lineno": 25, + "name": "buf" + }, + { + "lineno": 25, + "name": "buflen" + }, + { + "lineno": 25, + "name": "t" + }, + { + "lineno": 54, + "name": "v" + }, + { + "lineno": 66, + "name": "p0" + }, + { + "lineno": 82, + "name": "is" + }, + { + "lineno": 82, + "name": "u" + }, + { + "lineno": 92, + "name": "os" + }, + { + "lineno": 92, + "name": "t" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "varint" + }, + { + "args": [], + "lineno": 17, + "name": "varint_traits" + }, + { + "args": [], + "lineno": 21, + "name": "varint_traits" + } + ], + "description": "Implements base128 varint encoding/decoding utilities for unsigned integers, including reading, writing, and size calculation, for use in XRPL NodeStore serialization.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/nodestore/detail/varint.h", + "functions": [ + { + "args": [ + "buf", + "buflen", + "t" + ], + "lineno": 25, + "name": "read_varint" + }, + { + "args": [ + "v" + ], + "lineno": 54, + "name": "size_varint" + }, + { + "args": [ + "p0", + "v" + ], + "lineno": 66, + "name": "write_varint" + }, + { + "args": [ + "is", + "u" + ], + "lineno": 82, + "name": "read" + }, + { + "args": [ + "os", + "t" + ], + "lineno": 92, + "name": "write" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "NodeStore" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/nodestore/detail/varint.h.ai.md b/include/xrpl/nodestore/detail/varint.h.ai.md new file mode 100644 index 0000000000..12862f01bc --- /dev/null +++ b/include/xrpl/nodestore/detail/varint.h.ai.md @@ -0,0 +1,59 @@ +# `include/xrpl/nodestore/detail/varint.h` + +## Role and Purpose + +This header provides a compact variable-length integer encoding scheme for the XRPL NodeStore's on-disk serialization layer. Variable-length integers are essential when embedding small discriminant values (object type codes) or size prefixes directly into binary blobs without reserving a fixed field width — common values like `0`, `1`, or `2` cost only one byte, while larger values expand automatically up to 10 bytes for a full 64-bit value. + +The encoding is described in the file itself as a "variant of the base128 varint format from Google Protocol Buffers," though the actual implementation uses **base-127** rather than base-128. This is a subtle but intentional deviation: each 7-bit payload slot encodes digits in the range 0–126 (not 0–127), which means the byte value `0x7F` never appears as a payload byte. The continuation flag is still bit 7 (`0x80`), matching the structural appearance of protobuf LEB128. + +## Concrete Usage in the NodeStore + +`codec.h` is the immediate consumer. It uses varints in two roles: + +1. **Object-type discriminant**: Every serialized `NodeObject` blob is prefixed by a varint type tag — `0` for uncompressed, `1` for LZ4, `2` for compressed inner node, `3` for full inner node. Since these values are all tiny, the discriminant always occupies exactly one byte. + +2. **LZ4 decompressed-size prefix**: Before LZ4-compressed payload data, `lz4_compress` writes the original (decompressed) byte count as a varint. `lz4_decompress` reads it back to allocate the output buffer before calling `LZ4_decompress_safe`. The varint encoding avoids wasting a fixed 4 or 8 bytes for a size that is often small. + +## API + +### `varint_traits` + +A compile-time metafunction (SFINAE-guarded to `std::is_unsigned`) that provides `::max` — the largest number of bytes a value of type `T` can ever occupy as a varint. The formula `(8 * sizeof(T) + 6) / 7` correctly bounds base-127 encoding for any unsigned width. `codec.h` uses this to allocate stack-local `std::array` buffers of the right size without dynamic allocation: + +```cpp +std::array::max> vi{}; +``` + +### `write_varint(void* p0, std::size_t v)` → bytes written + +Encodes `v` into the buffer at `p0` using a do-while that extracts successive base-127 digits in least-significant-first order. Each byte is `v % 127` in bits 0–6, with bit 7 set if more bytes follow. Returns the number of bytes written. The caller is responsible for ensuring the buffer is at least `size_varint(v)` bytes — there is no bounds check on the write path. + +### `size_varint(T v)` → byte count + +Computes `write_varint`'s return value without actually writing anything. Used by `codec.h` to pre-compute output buffer sizes. + +### `read_varint(void const* buf, std::size_t buflen, std::size_t& t)` → bytes consumed or 0 on error + +Scans forward through `buf` until it finds a byte without the continuation flag, then decodes the value using Horner's method in reverse order — highest-indexed byte first — so the accumulation `t = t * 127 + (d & 0x7F)` correctly reconstructs the original value. Returns `0` on three error conditions: empty buffer, continuation extends past `buflen`, or arithmetic overflow detected by checking `t <= t0` after each accumulation step. + +The zero-value special case (`n == 1 && *p == 0` → return 1) is necessary because the overflow guard `t <= t0` would otherwise trigger when `t` stays zero after processing the single zero byte. + +### `varint` tag struct and stream overloads + +`varint` is a forward-declared empty struct used exclusively as a template tag. The two stream functions: + +```cpp +template ::value>* = nullptr> +void read(nudb::detail::istream& is, std::size_t& u); + +template ::value>* = nullptr> +void write(nudb::detail::ostream& os, std::size_t t); +``` + +integrate varint I/O with NuDB's streaming layer. The `read` overload advances the `istream` one byte at a time until the continuation bit clears, then calls `read_varint` on the accumulated span. The `write` overload allocates exactly `size_varint(t)` bytes in the `ostream` and calls `write_varint` directly into that region. Call sites in `codec.h` use these as `read(is, u)` and `write(os, type)`, with the tag distinguishing them from NuDB's own typed `read`/`write` overloads for `uint8_t`, `uint16_t`, etc. + +## Design Notes + +The `` default template parameter on `read_varint` and `write_varint` — functions that have no actual template behaviour — is an ODR workaround. Making them function templates rather than plain functions allows the header to be included in multiple translation units without violating the One Definition Rule, which would otherwise require moving the implementations into a `.cpp` file. + +The decision to use base-127 rather than the more standard base-128 (standard LEB128) wastes one value per byte position, very slightly increasing average encoded size. However, for the tiny type discriminants and moderate sizes actually stored, this has no practical impact — the maximum field size is identical (10 bytes for 64-bit values) and the difference in efficiency is negligible compared to the LZ4 payload compression applied to the object data itself. \ No newline at end of file diff --git a/include/xrpl/protocol/AMMCore.h.ai.json b/include/xrpl/protocol/AMMCore.h.ai.json new file mode 100644 index 0000000000..8c0acd4b91 --- /dev/null +++ b/include/xrpl/protocol/AMMCore.h.ai.json @@ -0,0 +1,187 @@ +{ + "args": [ + { + "lineno": 27, + "name": "asset1" + }, + { + "lineno": 27, + "name": "asset2" + }, + { + "lineno": 32, + "name": "asset1" + }, + { + "lineno": 32, + "name": "asset2" + }, + { + "lineno": 32, + "name": "ammAccountID" + }, + { + "lineno": 38, + "name": "amount" + }, + { + "lineno": 39, + "name": "pair" + }, + { + "lineno": 40, + "name": "validZero" + }, + { + "lineno": 46, + "name": "asset" + }, + { + "lineno": 47, + "name": "pair" + }, + { + "lineno": 50, + "name": "asset1" + }, + { + "lineno": 51, + "name": "asset2" + }, + { + "lineno": 52, + "name": "pair" + }, + { + "lineno": 56, + "name": "current" + }, + { + "lineno": 56, + "name": "auctionSlot" + }, + { + "lineno": 61, + "name": "Rules" + }, + { + "lineno": 67, + "name": "tfee" + }, + { + "lineno": 75, + "name": "tfee" + }, + { + "lineno": 82, + "name": "tfee" + } + ], + "classes": [ + { + "args": [], + "lineno": 23, + "name": "STObject" + }, + { + "args": [], + "lineno": 24, + "name": "STAmount" + }, + { + "args": [], + "lineno": 25, + "name": "Rules" + } + ], + "description": "Header file for XRPL Automated Market Maker (AMM) utilities, constants, and validation functions related to trading fees, auction slots, and asset pairs.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/AMMCore.h", + "functions": [ + { + "args": [ + "asset1", + "asset2" + ], + "lineno": 27, + "name": "ammLPTCurrency" + }, + { + "args": [ + "asset1", + "asset2", + "ammAccountID" + ], + "lineno": 32, + "name": "ammLPTIssue" + }, + { + "args": [ + "amount", + "pair", + "validZero" + ], + "lineno": 38, + "name": "invalidAMMAmount" + }, + { + "args": [ + "asset", + "pair" + ], + "lineno": 46, + "name": "invalidAMMAsset" + }, + { + "args": [ + "asset1", + "asset2", + "pair" + ], + "lineno": 50, + "name": "invalidAMMAssetPair" + }, + { + "args": [ + "current", + "auctionSlot" + ], + "lineno": 56, + "name": "ammAuctionTimeSlot" + }, + { + "args": [ + "Rules" + ], + "lineno": 61, + "name": "ammEnabled" + }, + { + "args": [ + "tfee" + ], + "lineno": 67, + "name": "getFee" + }, + { + "args": [ + "tfee" + ], + "lineno": 75, + "name": "feeMult" + }, + { + "args": [ + "tfee" + ], + "lineno": 82, + "name": "feeMultHalf" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/AMMCore.h.ai.md b/include/xrpl/protocol/AMMCore.h.ai.md new file mode 100644 index 0000000000..1118108dfc --- /dev/null +++ b/include/xrpl/protocol/AMMCore.h.ai.md @@ -0,0 +1,54 @@ +# `include/xrpl/protocol/AMMCore.h` + +This header is the shared kernel for the XRPL Automated Market Maker (AMM) feature. It centralises all protocol-level constants, the deterministic derivation of Liquidity Provider Token (LPT) identities, input-validation helpers, and the inline fee-conversion utilities that the constant-product swap formula depends on. Every AMM transactor (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMBid`, `AMMVote`) and the higher-level ledger helpers in `AMMHelpers.h` pull from this single file, making it the authoritative definition of how AMM numeric parameters are encoded and checked. + +## Fee Encoding + +Trading fees are stored as integers in the range `[0, 1000]`, where 1 unit equals 1/10 of a basis point (0.001%) and the maximum `TRADING_FEE_THRESHOLD = 1000` corresponds to exactly 1%. This integer-per-tenth-of-bps encoding avoids floating point in the ledger while still providing fine-grained control. + +Three inline helpers translate that integer into the `Number` type used by AMM arithmetic: + +- `getFee(tfee)` divides by `AUCTION_SLOT_FEE_SCALE_FACTOR` (100,000) to produce the fee fraction `f`. +- `feeMult(tfee)` returns `1 - f`, the standard swap multiplier applied when the full fee is charged. +- `feeMultHalf(tfee)` returns `1 - f/2`, used during single-asset deposits where only half the implied fee is deducted (see `AMMDeposit.cpp`, where `f1 = feeMult` and `f2 = feeMultHalf / f1` are combined in the constant-product formula). + +All three are `inline` and operate directly on the `Number` type (defined in `xrpl/basics/Number.h`), keeping the translation cost at the call site while preserving the high-precision arithmetic that `fixUniversalNumber` provides. + +## Auction Slot Constants + +An AMM auction slot gives its holder a discounted trading fee for 24 hours. That window (`TOTAL_TIME_SLOT_SECS = 86400`) is divided into `AUCTION_SLOT_TIME_INTERVALS = 20` equal intervals of `AUCTION_SLOT_INTERVAL_DURATION = 4320` seconds (72 minutes). The slot index (0–19) determines how much of the bid price is refunded to the outgoing slot holder when a new bidder takes over. + +Other auction-related constants: +- `AUCTION_SLOT_MAX_AUTH_ACCOUNTS = 4`: the maximum number of accounts a slot holder may authorise to trade at the discounted fee. +- `AUCTION_SLOT_DISCOUNTED_FEE_FRACTION = 10`: the slot holder's effective fee is `tradingFee / 10`. +- `AUCTION_SLOT_MIN_FEE_FRACTION = 25`: minimum bid is `lptAMMBalance × tradingFee / 25`. + +`ammAuctionTimeSlot(current, auctionSlot)` derives the slot index from the ledger's current time and the `sfExpiration` field stored in the slot object. It subtracts the 24-hour window from the expiration to find the slot start, then integer-divides the elapsed seconds by the interval duration. It returns `std::nullopt` when the current time falls outside the active window, signalling that the slot has expired or has not yet started. An `XRPL_ASSERT` guards against an impossible expiration value smaller than `TOTAL_TIME_SLOT_SECS`, providing a diagnostic checkpoint without altering control flow in production builds. + +## Vote Weight Constants + +Fee-governance voting uses `VOTE_MAX_SLOTS = 8` vote records and `VOTE_WEIGHT_SCALE_FACTOR = 100000` to represent each LP's proportional weight as a scaled integer, avoiding division until the weighted average is computed. + +## LP Token Identity + +`ammLPTCurrency` derives a deterministic 20-byte `Currency` code for the pool's LP token: + +1. The two assets are sorted via `std::minmax` to eliminate ordering ambiguity — the same pair always produces the same currency regardless of which asset was passed as `asset1` or `asset2`. +2. A `sha512Half` hash is computed over the canonical token identifiers (the `Currency` field for IOU/XRP assets, or the `MPTID` for MPT assets). +3. The first byte is set to the sentinel `0x03` (the AMM currency code), and the following 19 bytes are filled from the hash. + +This construction guarantees uniqueness within the ledger: different asset pairs produce different currencies, the byte prefix distinguishes LPT currencies from normal IOUs (`0x00`) or XRP, and the canonical sort makes the derivation purely deterministic. `ammLPTIssue` wraps the currency with the AMM's `AccountID` to form the full `Issue` that `STAmount` operations require. + +## Input Validation + +The three `invalidAMM*` functions follow the XRPL convention of returning `NotTEC` — a type restricted to `tesSUCCESS` or `tem*` (malformed transaction) error codes, as distinct from execution-phase `tec*` failures. This signals that the checks are preconditions evaluated during preflight, before any ledger state is modified. + +- `invalidAMMAsset` rejects MPT assets with a zero issuer (`temBAD_MPT`), rejects XRP with a non-zero issuer (`temBAD_ISSUER`), rejects bad currency codes (`temBAD_CURRENCY`), and optionally confirms the asset is one of the two expected pool assets (`temBAD_AMM_TOKENS`). +- `invalidAMMAssetPair` composes two `invalidAMMAsset` calls and additionally rejects a pair where both sides are the same asset. +- `invalidAMMAmount` delegates asset validation and then checks the `STAmount` value is non-negative (and non-zero unless `validZero` is explicitly permitted). + +The `pair` parameter, typed as `std::optional>`, threads a known-good pair through validation when callers need to confirm that user-supplied assets actually refer to the AMM pool they are operating on. + +## Feature Gating + +`ammEnabled(Rules const&)` requires both `featureAMM` and `fixUniversalNumber` to be active on the network. The second amendment is not incidental: AMM math involves intermediate results that overflow or lose precision with the older fixed-point arithmetic. Requiring `fixUniversalNumber` prevents AMM transactions from being processed on networks that have not yet adopted the corrected numeric library, making the dependency explicit and machine-checked rather than relying on deployment order alone. \ No newline at end of file diff --git a/include/xrpl/protocol/AccountID.h.ai.json b/include/xrpl/protocol/AccountID.h.ai.json new file mode 100644 index 0000000000..fb0c080bda --- /dev/null +++ b/include/xrpl/protocol/AccountID.h.ai.json @@ -0,0 +1,135 @@ +{ + "args": [ + { + "lineno": 23, + "name": "v" + }, + { + "lineno": 29, + "name": "s" + }, + { + "lineno": 82, + "name": "count" + }, + { + "lineno": 70, + "name": "os" + }, + { + "lineno": 70, + "name": "x" + }, + { + "lineno": 65, + "name": "account" + }, + { + "lineno": 59, + "name": "c" + }, + { + "lineno": 91, + "name": "field" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "AccountIDTag" + } + ], + "description": "Defines the AccountID type and related utility functions for handling XRPL account identifiers, including conversion to/from base58, special account constants, and cache initialization.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/AccountID.h", + "functions": [ + { + "args": [ + "v" + ], + "lineno": 23, + "name": "toBase58" + }, + { + "args": [ + "s" + ], + "lineno": 29, + "name": "parseBase58" + }, + { + "args": [], + "lineno": 41, + "name": "xrpAccount" + }, + { + "args": [], + "lineno": 45, + "name": "noAccount" + }, + { + "args": [ + "AccountID&", + "std::string const&" + ], + "lineno": 54, + "name": "to_issuer" + }, + { + "args": [ + "c" + ], + "lineno": 59, + "name": "isXRP" + }, + { + "args": [ + "account" + ], + "lineno": 65, + "name": "to_string" + }, + { + "args": [ + "os", + "x" + ], + "lineno": 70, + "name": "operator<<" + }, + { + "args": [ + "count" + ], + "lineno": 82, + "name": "initAccountIdCache" + }, + { + "args": [ + "v", + "field" + ], + "lineno": 91, + "name": "getOrThrow" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "detail" + }, + { + "lineno": 89, + "name": "Json" + }, + { + "lineno": 106, + "name": "std" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/AccountID.h.ai.md b/include/xrpl/protocol/AccountID.h.ai.md new file mode 100644 index 0000000000..18f2d0ff24 --- /dev/null +++ b/include/xrpl/protocol/AccountID.h.ai.md @@ -0,0 +1,68 @@ +# `include/xrpl/protocol/AccountID.h` + +## Purpose and Role + +`AccountID.h` defines the fundamental identity type for XRPL accounts. Every participant in the XRP Ledger — wallets, issuers, escrow destinations, multisig signers — is addressed by an `AccountID`. This header establishes the type, its serialization contract, two protocol-level sentinel constants, a performance cache, and the JSON integration hook. It is included pervasively throughout the rippled codebase wherever accounts appear. + +## The Type: Phantom Tagging on `base_uint` + +```cpp +using AccountID = base_uint<160, detail::AccountIDTag>; +``` + +`AccountID` is not a bare `typedef` — it is a distinct strong type. `base_uint` uses its second template parameter as a phantom tag, so `base_uint<160, AccountIDTag>` and a hypothetical `base_uint<160, SomeOtherTag>` are entirely separate types and cannot be mixed silently. `AccountIDTag` itself is an empty class in `namespace xrpl::detail`, existing only to instantiate this uniqueness. This design catches at compile time the class of bug where a raw 160-bit hash is accidentally used as an account address or vice versa. + +The underlying `base_uint` stores 160 bits as five `uint32_t` values in big-endian byte order — a layout that is part of the XRP Ledger's binary serialization protocol and cannot be changed without a hard fork. The type inherits iterator support (acting as a byte-range container), `beast::zero` comparison, hex parsing via `parseHex()`, and `hardened_hash<>` as its default hasher. + +## Deriving an AccountID from a Public Key + +`calcAccountID()` (declared in `PublicKey.h`, implemented in `AccountID.cpp`) applies the SHA-256 + RIPEMD-160 pipeline to a public key's raw bytes: + +```cpp +ripesha_hasher rsh; +rsh(pk.data(), pk.size()); +return AccountID{static_cast(rsh)}; +``` + +The code comments include a direct quote from David Schwartz explaining why this specific combination was chosen: XRPL inherited it from Bitcoin to avoid giving critics a reason to claim it was less secure, since RIPEMD-160 is considered safe at 160 bits and the double-hash avoids length-extension vulnerability. It was a deliberate conservative choice, not an independently derived cryptographic decision. + +## Serialization: Base58Check with TokenType + +XRPL uses a Base58Check encoding that is *not* identical to Bitcoin's — it uses a different alphabet and prepends a `TokenType` version byte. For accounts, `TokenType::AccountID` has value `0`. `toBase58()` calls `encodeBase58Token(TokenType::AccountID, ...)` and `parseBase58()` is a full template specialization that calls `decodeBase58Token`, then validates the decoded payload is exactly 20 bytes — a hard rejection of anything malformed. The `std::nullopt` return on parse failure (rather than an exception) reflects the expectation that input from external sources is frequently untrusted. + +`to_issuer()` is a deprecated convenience that accepts either a 40-character hex string or a base58 string, used in legacy configuration parsing. Its continued presence is a migration artifact. + +## Performance Cache + +Base58Check encoding requires a SHA-256 checksum computation on every call, which is expensive when processing thousands of transactions. `initAccountIdCache()` allocates a flat open-addressing cache of `CachedAccountID` entries, each holding an `AccountID` and a 40-char encoding buffer: + +```cpp +auto const index = hasher_(id) % cache_.size(); +packed_spinlock sl(locks_, index % 64); +``` + +The cache uses `hardened_hash<>` — a seeded hash — specifically to resist hash-flooding attacks where a crafted workload could degrade a naive modulo hash to O(n) collision chains. Fine-grained locking uses 64 spinlocks packed bitwise into a single `std::atomic` via `packed_spinlock`, eliminating per-entry lock memory overhead while still allowing concurrent access to different cache buckets. + +One subtle defensive guard appears in the cache lookup: + +```cpp +if (cache_[index].encoding[0] != 0 && cache_[index].id == id) +``` + +The `encoding[0] != 0` check prevents the all-zero `AccountID` (the XRP sentinel) from matching against a zero-initialized empty slot, since zero-initialized memory would otherwise look like a valid cache hit for `xrpAccount()`. + +The cache is strictly optional — if `initAccountIdCache(0)` is called or it was never initialized, `toBase58()` falls through directly to `encodeBase58Token`. The cache initializes exactly once; subsequent calls to `initAccountIdCache` are no-ops. + +## Sentinel Constants + +`xrpAccount()` returns the all-zero `AccountID` (backed by `beast::zero`), used as the canonical "issuer" for native XRP in amount fields. `noAccount()` returns `AccountID(1)`, a placeholder representing the absence of a meaningful account (for example in uninitialized offer or trust line fields). Both are function-local statics returned by `const&`, making them lazily initialized singletons without global constructor ordering issues. + +`isXRP()` tests whether an account ID equals `beast::zero`, essentially asking whether an amount's issuer is XRP rather than a token. It is marked deprecated because callers should prefer checking the currency or native flag directly — relying on the zero-account-as-issuer convention is a leaky abstraction. + +## JSON Integration + +The `Json::getOrThrow()` specialization lives in `namespace Json` alongside the type, bridging the ledger's JSON API into the type system. It reads a string field, parses it as base58, and throws `JsonTypeMismatchError` on failure — the same error type thrown for any other JSON type mismatch. This is the mechanism by which RPC handlers and transaction parsers convert incoming JSON account strings into `AccountID` values without writing repetitive parsing boilerplate. + +## Hash and Deprecation Notes + +`std::hash` is specialized to delegate to `AccountID::hasher` (which is `hardened_hash<>`), but is itself marked deprecated. New code should use `beast::uhash` or XRPL's hardened unordered containers directly. The presence of the `std::hash` specialization maintains compatibility with code that passes `AccountID` to standard library containers, while steering new code toward attack-resistant alternatives. \ No newline at end of file diff --git a/include/xrpl/protocol/AmountConversions.h.ai.json b/include/xrpl/protocol/AmountConversions.h.ai.json new file mode 100644 index 0000000000..9adb4913f0 --- /dev/null +++ b/include/xrpl/protocol/AmountConversions.h.ai.json @@ -0,0 +1,293 @@ +{ + "args": [ + { + "lineno": 9, + "name": "iou" + }, + { + "lineno": 9, + "name": "asset" + }, + { + "lineno": 17, + "name": "iou" + }, + { + "lineno": 21, + "name": "xrp" + }, + { + "lineno": 27, + "name": "xrp" + }, + { + "lineno": 27, + "name": "asset" + }, + { + "lineno": 32, + "name": "mpt" + }, + { + "lineno": 36, + "name": "mpt" + }, + { + "lineno": 36, + "name": "asset" + }, + { + "lineno": 42, + "name": "amt" + }, + { + "lineno": 45, + "name": "amt" + }, + { + "lineno": 50, + "name": "amt" + }, + { + "lineno": 62, + "name": "amt" + }, + { + "lineno": 74, + "name": "amt" + }, + { + "lineno": 91, + "name": "amt" + }, + { + "lineno": 94, + "name": "amt" + }, + { + "lineno": 98, + "name": "amt" + }, + { + "lineno": 101, + "name": "amt" + }, + { + "lineno": 105, + "name": "amt" + }, + { + "lineno": 108, + "name": "amt" + }, + { + "lineno": 112, + "name": "asset" + }, + { + "lineno": 112, + "name": "n" + }, + { + "lineno": 112, + "name": "mode" + }, + { + "lineno": 134, + "name": "asset" + }, + { + "lineno": 154, + "name": "asset" + }, + { + "lineno": 154, + "name": "n" + }, + { + "lineno": 154, + "name": "mode" + }, + { + "lineno": 158, + "name": "amt" + }, + { + "lineno": 172, + "name": "a" + } + ], + "classes": [], + "description": "Provides utility functions for converting between different XRPL amount types (IOUAmount, XRPAmount, MPTAmount, STAmount) and assets, as well as extracting assets and amounts from STAmount objects.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/AmountConversions.h", + "functions": [ + { + "args": [ + "iou", + "asset" + ], + "lineno": 9, + "name": "toSTAmount" + }, + { + "args": [ + "iou" + ], + "lineno": 17, + "name": "toSTAmount" + }, + { + "args": [ + "xrp" + ], + "lineno": 21, + "name": "toSTAmount" + }, + { + "args": [ + "xrp", + "asset" + ], + "lineno": 27, + "name": "toSTAmount" + }, + { + "args": [ + "mpt" + ], + "lineno": 32, + "name": "toSTAmount" + }, + { + "args": [ + "mpt", + "asset" + ], + "lineno": 36, + "name": "toSTAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 42, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 45, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 50, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 62, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 74, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 91, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 94, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 98, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 101, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 105, + "name": "toAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 108, + "name": "toAmount" + }, + { + "args": [ + "asset", + "n", + "mode" + ], + "lineno": 112, + "name": "toAmount" + }, + { + "args": [ + "asset" + ], + "lineno": 134, + "name": "toMaxAmount" + }, + { + "args": [ + "asset", + "n", + "mode" + ], + "lineno": 154, + "name": "toSTAmount" + }, + { + "args": [ + "amt" + ], + "lineno": 158, + "name": "getAsset" + }, + { + "args": [ + "a" + ], + "lineno": 172, + "name": "get" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/AmountConversions.h.ai.md b/include/xrpl/protocol/AmountConversions.h.ai.md new file mode 100644 index 0000000000..40a7533b57 --- /dev/null +++ b/include/xrpl/protocol/AmountConversions.h.ai.md @@ -0,0 +1,80 @@ +# `include/xrpl/protocol/AmountConversions.h` + +## Role in the System + +The XRPL protocol defines four distinct amount representations, each optimized for a different concern. `XRPAmount` and `MPTAmount` are simple integer wrappers — strongly-typed drop and multi-purpose token counts respectively. `IOUAmount` is a normalized floating-point type (signed mantissa + exponent) optimized for IOU arithmetic. `STAmount` is the wire-level union type that unifies all three under a single `Asset` tag and can be serialized to/from the ledger binary format. + +Generic algorithmic code — AMM pricing, path finding, offer crossing — needs to work with any amount type without duplicating logic. This header provides the glue: a focused set of inline conversion functions that let callers move freely between the four representations. It is intentionally a conversion-only header; all logic lives in the amount types themselves. + +## Direction One: Lean Types → `STAmount` + +The six `toSTAmount()` overloads wrap a lean value in a serializable `STAmount`. The IOU variant is worth examining closely: + +```cpp +inline STAmount +toSTAmount(IOUAmount const& iou, Asset const& asset) +{ + bool const isNeg = iou.signum() < 0; + std::uint64_t const umant = isNeg ? -iou.mantissa() : iou.mantissa(); + return STAmount(asset, umant, iou.exponent(), isNeg, STAmount::unchecked()); +} +``` + +`STAmount` stores an *unsigned* mantissa with a separate sign bit, while `IOUAmount` stores a *signed* mantissa. The conversion manually splits the sign, then uses the `STAmount::unchecked` sentinel constructor to bypass re-canonicalization. This is intentional: `IOUAmount` is already normalized (mantissa range `[10^15, 10^16 - 1]`), so running canonicalize again would be wasted work and could introduce subtle drift. The `XRPL_ASSERT` guarding `asset.holds()` confirms the caller hasn't accidentally passed an XRP or MPT asset — a mismatch that would silently produce wrong data if uncaught. + +The XRP overload with an `Asset` argument delegates immediately to the asset-less version after asserting `isXRP(asset)`. This exists solely to give generic code a uniform call signature — callers that hold an `Asset` and an amount type can always call `toSTAmount(amount, asset)` without branching on type. + +## Direction Two: `STAmount` → Lean Types + +The `toAmount(STAmount const&)` family uses explicit template specializations. The base template is deliberately `= delete`: + +```cpp +template +T +toAmount(STAmount const& amt) = delete; +``` + +This means calling `toAmount(stamt)` is a hard compile error rather than a linker error or silent instantiation of something wrong. Similarly, identity-conversion overloads (`toAmount(IOUAmount const&)` etc.) are provided so generic code calling `toAmount` doesn't need to branch on whether the source is already the target type. + +The MPT specialization is the strictest: + +```cpp +template <> +inline MPTAmount +toAmount(STAmount const& amt) +{ + XRPL_ASSERT( + amt.holds() && amt.mantissa() <= maxMPTokenAmount && amt.exponent() == 0, ...); + if (amt.mantissa() > maxMPTokenAmount || amt.exponent() != 0) + Throw("toAmount: invalid mantissa or exponent"); + ... +} +``` + +The assert fires in debug builds; the explicit throw fires in release builds. This double-check is unique to MPT because MPT amounts are integers (exponent must be 0) bounded by `maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull`. A non-zero exponent or out-of-range mantissa would indicate data corruption or a ledger encoding bug, both of which should surface loudly rather than silently truncate. + +## Generic `Number`-Based Construction and Rounding + +The two-phase template `toAmount(Asset, Number, mode)` is the most nuanced function in the file. It converts an intermediate `Number` result — the output of AMM pricing arithmetic — into a typed amount, applying a caller-specified rounding mode: + +```cpp +saveNumberRoundMode const rm(Number::getround()); +if (isXRP(asset)) + Number::setround(mode); +``` + +The rounding mode override is applied **only for XRP**. IOU amounts use arbitrary-precision internal representation that doesn't require rounding during construction; the `IOUAmount(n)` constructor handles normalization cleanly. But XRP is an integer count of drops, and converting a rational intermediate result (e.g., from AMM math) to an integer requires a deterministic rounding decision. By letting the caller specify `Number::downward` or `Number::upward`, the ledger can implement "give the taker less, charge the taker more" semantics without hardcoding a rounding direction into the amount type itself. + +`saveNumberRoundMode` is RAII — it captures the current thread-local rounding mode on construction and restores it on destruction, ensuring that even if the conversion throws, the rounding mode is not left in a mutated state. + +`toMaxAmount(Asset)` mirrors this template but returns the maximum representable value for each type. For `STAmount`, it dispatches through `asset.visit(...)`, which accepts typed lambdas for `Issue` and `MPTIssue` — the variant pattern that `Asset` uses internally to avoid dynamic dispatch. + +## Helper Utilities + +`getAsset(amt)` inverts the typical direction: given an amount, return the associated `Asset`. For `STAmount` this is a simple delegation to `amt.asset()`. For the lean types, which do not carry asset identity, the function returns placeholder sentinels (`noIssue()`, `xrpIssue()`, `noMPT()`). This is used in `AMMHelpers.h` where templated pool arithmetic needs to call `toAmount(getAsset(pool.out), result, rounding)` — the sentinel return from `getAsset` is immediately consumed by another call that knows the true asset from context. + +`get(STAmount)` extracts a typed value from an `STAmount` by delegating to the appropriate observer method (`a.iou()`, `a.xrp()`, `a.mpt()`, or identity for `STAmount`). The `static_assert` in the else branch — `constexpr bool alwaysFalse = !std::is_same_v` — is the canonical C++ pattern for a template-dependent `static_assert(false)` that triggers only when the unsupported branch is actually instantiated, not on every parse of the template body. + +## Summary + +`AmountConversions.h` is compact but load-bearing. It is the type-system bridge that lets the rest of the codebase treat XRP, IOU, and MPT amounts uniformly in generic algorithms while retaining strong type separation at the representation layer. The design favors compile-time errors over runtime surprises, uses `unchecked` construction where the source invariants are already established, and encapsulates all rounding-mode side-effect management in a single well-guarded location. \ No newline at end of file diff --git a/include/xrpl/protocol/ApiVersion.h.ai.json b/include/xrpl/protocol/ApiVersion.h.ai.json new file mode 100644 index 0000000000..6ebf5823c5 --- /dev/null +++ b/include/xrpl/protocol/ApiVersion.h.ai.json @@ -0,0 +1,81 @@ +{ + "args": [ + { + "lineno": 44, + "name": "parent" + }, + { + "lineno": 44, + "name": "apiVersion" + }, + { + "lineno": 44, + "name": "betaEnabled" + }, + { + "lineno": 69, + "name": "jv" + }, + { + "lineno": 69, + "name": "betaEnabled" + }, + { + "lineno": 104, + "name": "fn" + }, + { + "lineno": 104, + "name": "args" + } + ], + "classes": [], + "description": "Defines and manages API versioning for the XRPL RPC interface, including constants for supported versions, utilities for setting and retrieving API version information in JSON, and template utilities for iterating over API versions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/ApiVersion.h", + "functions": [ + { + "args": [ + "parent", + "apiVersion", + "betaEnabled" + ], + "lineno": 44, + "name": "setVersion" + }, + { + "args": [ + "jv", + "betaEnabled" + ], + "lineno": 69, + "name": "getAPIVersionNumber" + }, + { + "args": [ + "fn", + "args" + ], + "lineno": 104, + "name": "forApiVersions" + }, + { + "args": [ + "fn", + "args" + ], + "lineno": 120, + "name": "forAllApiVersions" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 32, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/ApiVersion.h.ai.md b/include/xrpl/protocol/ApiVersion.h.ai.md new file mode 100644 index 0000000000..40d3bb0b48 --- /dev/null +++ b/include/xrpl/protocol/ApiVersion.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/protocol/ApiVersion.h` + +## Role in the System + +This header is the single source of truth for XRPL's RPC API versioning scheme. It defines the integer constants that govern which API surface a client can access, provides the JSON parsing and serialization utilities that enforce those version boundaries on every incoming request, and exposes compile-time iteration primitives that let the rest of the codebase safely generate version-aware code without duplication. Every RPC handler that changes behavior between versions, every subscription publisher that formats data differently per client, and every test that validates multi-version correctness traces back to the constants and templates defined here. + +## Version Model + +The versioning system is built on a small set of named compile-time constants inside `namespace xrpl::RPC`. Rather than raw integers, they are typed as `std::integral_constant` instances, which enables overload resolution and template specialisation to distinguish versions at compile time while still implicitly decaying to `unsigned` in arithmetic and comparison contexts. + +The constants form a strict linear ordering that is enforced by `static_assert` at translation time: + +- `apiInvalidVersion` (0) — a sentinel returned when parsing fails or a version is out of range. +- `apiMinimumSupportedVersion` (1) — the oldest version still accepted from network clients. +- `apiMaximumSupportedVersion` (2) — the newest stable version. Normal production requests are capped here. +- `apiBetaVersion` (3) — an experimental version gated behind the `[beta_rpc_api]` config flag. +- `apiMaximumValidVersion` — always equal to `apiBetaVersion`; the absolute ceiling for template range loops. +- `apiVersionIfUnspecified` (1) — the implicit version assigned when a request omits `api_version`. It is fixed at 1 because the design rule is that any request at version 2 or above must carry an explicit field; omitting it is treated as a version-1 request, not an error. +- `apiCommandLineVersion` (1) — the version used for command-line invocations, with a `TODO` comment noting it should eventually be bumped to 2. + +The `static_assert` block at the bottom of the constant declarations is load-bearing, not decorative. It guarantees that invariants like `apiVersionIfUnspecified` lying within `[min, max]` and `apiBetaVersion >= apiMaximumSupportedVersion` hold whenever a developer adjusts these values. The companion test file `ApiVersion_test.cpp` validates the current actual values (e.g., min < 2, max ≥ 2, beta ≥ 3) so that a mistaken bump causes an immediately visible CI failure. + +## `getAPIVersionNumber()` + +This function is called at the RPC ingress point in `ServerHandler.cpp` on every incoming HTTP or WebSocket request. It inspects the incoming `Json::Value` for a top-level `api_version` field. If the field is absent the function returns `apiVersionIfUnspecified`. If the field is present but not an integer, or its integer value falls outside `[apiMinimumSupportedVersion, maxVersion]`, it returns `apiInvalidVersion`. Callers treat `apiInvalidVersion` as a signal to reject the request immediately with an appropriate error before any handler dispatch occurs. + +The `betaEnabled` parameter reflects the per-server configuration flag `BETA_RPC_API`. When false, `maxVersion` is `apiMaximumSupportedVersion` (2); when true, `maxVersion` is `apiBetaVersion` (3). This is the only mechanism through which experimental API surface is accessible, and it keeps the feature completely invisible to clients connecting to a production node that has not opted in. + +## `setVersion()` + +`setVersion()` populates the `version` sub-object in an RPC response. Its behaviour diverges sharply based on the negotiated version because version 1 used a legacy semver-string format (`first`/`good`/`last` keys whose values are `"1.0.0"` strings), while version 2 and above switched to simple integers. This bifurcation is why the function exists at all rather than being inlined at the call site: it encapsulates a backwards-compatibility shim so that callers can remain version-agnostic. The v1 path constructs static `beast::SemanticVersion` objects to avoid repeated string parsing; the v2+ path emits `apiMinimumSupportedVersion.value` as `first` and either `apiBetaVersion` or `apiMaximumSupportedVersion` as `last` depending on the beta flag. The handler in `src/xrpld/rpc/handlers/server_info/Version.h` is the primary consumer. + +## Compile-Time Version Iteration: `forApiVersions` and `forAllApiVersions` + +These two function templates are the most architecturally interesting part of the file. Their purpose is to call a callable object once for each API version in a range, passing the version as an `std::integral_constant` so that the callable can use the version as a template parameter, enabling compile-time branching without virtual dispatch or runtime switches. + +The implementation expands the range `[minVer, maxVer]` into a parameter pack using `std::make_index_sequence` and then folds over it with a comma-expression. Each element of the fold instantiates the callable with `std::integral_constant`. Because every instantiation carries a distinct type, the compiler produces a separate code path per version. This means that if a handler uses `if constexpr (Version >= 2)` in its lambda body, only the v2+ instantiations include that branch — dead branches are eliminated at compile time. + +The C++20 `requires` clause on `forApiVersions` enforces three constraints statically: the range must be non-empty, `minVer` must be at least `apiMinimumSupportedVersion`, and `maxVer` must be at most `apiMaximumValidVersion`. If a caller tries to iterate outside the known valid range the constraint fails to compile rather than producing a runtime out-of-bounds error. + +`forAllApiVersions` is a thin wrapper that fixes the range to `[apiMinimumSupportedVersion, apiMaximumValidVersion]`, which today spans versions 1 through 3. It is the standard way for test cases to run a scenario against every version and for publishers to prepare data for all potential subscribers. For example, `NetworkOPs.cpp` uses it to build a `MultiApiJson` fan-out when notifying subscribers of new transactions, calling `insertDeliverMax` only for the versions where that field is defined. + +## Relationship to `MultiApiJson` + +`MultiApiJson` (defined in `MultiApiJson.h`) depends directly on `ApiVersion.h`. It is parameterised on `[RPC::apiMinimumSupportedVersion, RPC::apiMaximumValidVersion]` and stores an `std::array` of `Json::Value` objects, one per version. The `forAllApiVersions` loop is the standard way to populate all slots of a `MultiApiJson` in a single pass, with each iteration receiving a typed version constant that the lambda can use to decide what fields to emit. The version constants in `ApiVersion.h` are therefore not just configuration — they dictate the size and index mapping of every `MultiApiJson` array in the system, so changing them automatically adjusts every data structure that stores per-version output. \ No newline at end of file diff --git a/include/xrpl/protocol/Asset.h.ai.json b/include/xrpl/protocol/Asset.h.ai.json new file mode 100644 index 0000000000..579d8ae436 --- /dev/null +++ b/include/xrpl/protocol/Asset.h.ai.json @@ -0,0 +1,303 @@ +{ + "args": [ + { + "lineno": 13, + "name": "T" + }, + { + "lineno": 101, + "name": "TIss" + }, + { + "lineno": 104, + "name": "TIss" + }, + { + "lineno": 117, + "name": "TIss" + }, + { + "lineno": 122, + "name": "TIss" + }, + { + "lineno": 129, + "name": "TIss" + }, + { + "lineno": 232, + "name": "h" + }, + { + "lineno": 232, + "name": "r" + }, + { + "lineno": 240, + "name": "os" + }, + { + "lineno": 240, + "name": "x" + }, + { + "lineno": 109, + "name": "asset" + }, + { + "lineno": 208, + "name": "asset" + }, + { + "lineno": 212, + "name": "asset" + }, + { + "lineno": 214, + "name": "jv" + }, + { + "lineno": 216, + "name": "jv" + }, + { + "lineno": 218, + "name": "asset" + }, + { + "lineno": 220, + "name": "asset" + }, + { + "lineno": 226, + "name": "asset" + }, + { + "lineno": 159, + "name": "lhs" + }, + { + "lineno": 159, + "name": "rhs" + }, + { + "lineno": 168, + "name": "lhs" + }, + { + "lineno": 168, + "name": "rhs" + }, + { + "lineno": 181, + "name": "lhs" + }, + { + "lineno": 181, + "name": "rhs" + }, + { + "lineno": 186, + "name": "lhs" + }, + { + "lineno": 186, + "name": "rhs" + }, + { + "lineno": 192, + "name": "lhs" + }, + { + "lineno": 192, + "name": "rhs" + } + ], + "classes": [ + { + "args": [ + "T" + ], + "lineno": 13, + "name": "AmountType" + }, + { + "args": [], + "lineno": 20, + "name": "BadAsset" + }, + { + "args": [], + "lineno": 34, + "name": "Asset" + } + ], + "description": "Defines the Asset abstraction in the XRPL protocol, representing XRP, IOU, and MPT assets, with utilities for type-safe access, comparison, serialization, and validation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Asset.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "badAsset" + }, + { + "args": [ + "asset" + ], + "lineno": 109, + "name": "to_json" + }, + { + "args": [], + "lineno": 101, + "name": "is_issue_v" + }, + { + "args": [], + "lineno": 104, + "name": "is_mptissue_v" + }, + { + "args": [], + "lineno": 117, + "name": "Asset::holds" + }, + { + "args": [], + "lineno": 122, + "name": "Asset::get" + }, + { + "args": [], + "lineno": 129, + "name": "Asset::get" + }, + { + "args": [], + "lineno": 135, + "name": "Asset::value" + }, + { + "args": [], + "lineno": 140, + "name": "Asset::token" + }, + { + "args": [], + "lineno": 148, + "name": "Asset::getAmountType" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 159, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 168, + "name": "operator<=>" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 181, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 186, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 192, + "name": "equalTokens" + }, + { + "args": [ + "asset" + ], + "lineno": 208, + "name": "isXRP" + }, + { + "args": [ + "asset" + ], + "lineno": 212, + "name": "to_string" + }, + { + "args": [ + "jv" + ], + "lineno": 214, + "name": "validJSONAsset" + }, + { + "args": [ + "jv" + ], + "lineno": 216, + "name": "assetFromJson" + }, + { + "args": [ + "asset" + ], + "lineno": 218, + "name": "to_json" + }, + { + "args": [ + "asset" + ], + "lineno": 220, + "name": "isConsistent" + }, + { + "args": [ + "asset" + ], + "lineno": 226, + "name": "validAsset" + }, + { + "args": [ + "h", + "r" + ], + "lineno": 232, + "name": "hash_append" + }, + { + "args": [ + "os", + "x" + ], + "lineno": 240, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Asset.h.ai.md b/include/xrpl/protocol/Asset.h.ai.md new file mode 100644 index 0000000000..a101386f00 --- /dev/null +++ b/include/xrpl/protocol/Asset.h.ai.md @@ -0,0 +1,71 @@ +# `include/xrpl/protocol/Asset.h` + +## Purpose and Context + +`Asset.h` introduces the unified `Asset` abstraction for the XRPL protocol — a type that can represent any of the three kinds of value that can move across the ledger: native XRP, IOU tokens (issued currencies), and Multi-Purpose Tokens (MPT). Historically the codebase used `Issue` for both XRP and IOUs, and all amount-related classes were templated or specialized around `Issue`. When MPT support was added, a wrapper was needed that could represent all three forms without an inheritance hierarchy, without dynamic dispatch, and with minimal disruption to existing `Issue`-based APIs. + +The file is deliberately header-only for its inline and constexpr methods. The non-trivial implementations (`getIssuer`, `operator()`, JSON parsing, streaming) live in the corresponding `Asset.cpp`. + +--- + +## The Core Type: `Asset` + +`Asset` wraps a `std::variant`. `Issue` already covers both XRP and IOU (its `native()` method distinguishes them), so the variant has exactly two arms but logically represents three asset kinds. This choice of `std::variant` over a polymorphic base class is central to the design: it enables `constexpr` comparisons, value semantics, and type-safe visitation without vtables or heap allocation. + +Conversions **to** `Asset` are intentionally implicit — any `Issue`, `MPTIssue`, or raw `MPTID` can silently upgrade to an `Asset`. This design lets callers pass any of those types into functions that accept `Asset` without explicit casts, preserving backward compatibility with legacy `Issue`-taking code. Conversions **out** of an `Asset` to a specific issue type are explicit via the `get()` template, which throws `std::logic_error` if the runtime type doesn't match. Callers should always guard with `holds()` before calling `get()` or use `visit()` instead. + +--- + +## The Visitor Pattern + +`Asset::visit()` accepts a variadic pack of lambdas and delegates to `detail::visit` defined in `Concepts.h`. That utility combines the lambdas into a `CombineVisitors` overload set using the `using Ts::operator()...` trick, then forwards to `std::visit`. This lets callers write multi-arm visitors inline without manually defining a visitor struct: + +```cpp +asset.visit( + [](Issue const& issue) { /* XRP or IOU path */ }, + [](MPTIssue const& mpt) { /* MPT path */ }); +``` + +The `native()` and `integral()` methods are implemented this way. `native()` is true only for XRP. `integral()` is true for both XRP and MPT — MPT amounts are always whole numbers, unlike IOU amounts which use a floating-point mantissa/exponent encoding. This distinction matters for serialization and arithmetic rounding. + +--- + +## `AmountType` and `getAmountType()` + +The `AmountType` struct is a pure tag type: it carries no data but encodes a numeric type (`XRPAmount`, `IOUAmount`, or `MPTAmount`) as a template parameter. `getAmountType()` returns a `std::variant, AmountType, AmountType>` that reflects the runtime asset kind. + +This pattern exists to bridge runtime type information back into compile-time template dispatch. Code doing amount arithmetic can `std::visit` over the result of `getAmountType()` to select the correct templated path. It is non-obvious but elegant: the variant never holds meaningful data, only type information, and its sole purpose is to enable overload resolution at call sites that need to template on the amount kind. + +--- + +## `BadAsset` Sentinel + +`BadAsset` is an empty tag struct, and `badAsset()` returns a static instance of it. The equality operator `operator==(BadAsset const&, Asset const&)` returns true when the `Asset` holds an `Issue` with `badCurrency()`, or holds an `MPTIssue` whose issuer is `xrpAccount()` (the null account used as a sentinel in MPT). This mirrors the pre-existing pattern where `badCurrency()` signals an invalid IOU. + +The pattern avoids `std::optional` or an extra validity flag — invalid states are represented as well-known sentinel values, and `BadAsset` provides a uniform way to test for any of them without knowing which sub-type the asset holds. + +--- + +## `equalTokens()` vs `operator==` + +`operator==` on two `Issue` values considers both currency and issuer: XRP always compares equal (no issuer), but two IOUs with the same currency and different issuers compare unequal. `equalTokens()` relaxes this: it tests only the `Currency` field for `Issue`-vs-`Issue` comparisons, ignoring the issuer. For `MPTIssue`-vs-`MPTIssue` it compares the full `MPTID` (which already encodes issuer and sequence, so there is no issuer-free concept of an MPT currency). Cross-type comparisons always return false. + +This distinction matters in path-finding and offer-matching logic where the token type must match but the issuer may differ (e.g., trust lines from different issuers in the same currency). + +--- + +## Ordering and Hashing + +`operator<=>` imposes a total order: when both assets hold the same variant arm, ordering is delegated to the arm's natural `<=>`. When arms differ, `Issue` sorts as greater than `MPTIssue`. This arbitrary but stable cross-type ordering allows `Asset` to be used as a key in sorted containers. The `hash_append` template dispatches hashing to whichever arm is active, enabling `Asset` in `unordered_map` and other hash-based structures. + +--- + +## Validation Utilities + +`isConsistent()` delegates to `Issue::isConsistent` for IOU/XRP assets (which checks that XRP has no account component) and trivially returns `true` for MPT. `validAsset()` is stricter: it rejects `badCurrency()` for issues and rejects the zero-issuer sentinel for MPT. `validJSONAsset()` enforces protocol rules at the JSON layer: an asset JSON object must contain either `currency` or `mpt_issuance_id`, but not both. + +--- + +## Relationship to `STAmount` + +`STAmount` — the ledger's serialized amount type — stores an `Asset` member (`mAsset`) and delegates `native()`, `integral()`, `holds()`, and `get()` directly to it. The `Asset::operator()(Number const&)` convenience method constructs an `STAmount` from the asset and a raw numeric value, allowing concise amount construction. This makes `Asset` effectively the type-identity half of `STAmount`, and the design allows `STAmount`'s constructors to be templated on `AssetType` (the concept from `Concepts.h`) while still storing a single unified `Asset` member internally. \ No newline at end of file diff --git a/include/xrpl/protocol/Batch.h.ai.json b/include/xrpl/protocol/Batch.h.ai.json new file mode 100644 index 0000000000..e9464bca14 --- /dev/null +++ b/include/xrpl/protocol/Batch.h.ai.json @@ -0,0 +1,37 @@ +{ + "args": [ + { + "lineno": 7, + "name": "msg" + }, + { + "lineno": 7, + "name": "flags" + }, + { + "lineno": 7, + "name": "txids" + } + ], + "classes": [], + "description": "Provides a utility function to serialize a batch of transaction IDs with associated flags into a Serializer object, using a specific hash prefix.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Batch.h", + "functions": [ + { + "args": [ + "msg", + "flags", + "txids" + ], + "lineno": 6, + "name": "serializeBatch" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Batch.h.ai.md b/include/xrpl/protocol/Batch.h.ai.md new file mode 100644 index 0000000000..7521406e7e --- /dev/null +++ b/include/xrpl/protocol/Batch.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/protocol/Batch.h` + +## Role and Purpose + +`Batch.h` defines the canonical wire-format serialization of a batch signing payload on the XRP Ledger. Its entire surface area is a single inline function, `serializeBatch()`, but that function is the authoritative definition of what a "batch" means at the cryptographic level: it is the byte sequence that every batch co-signer actually signs and that the validator verifies. + +The Batch feature allows multiple independent transactions to be grouped into a single outer transaction with an execution policy (expressed as flags, e.g., `tfAllOrNothing`). Each participant who authorizes that group must produce a signature over a payload that unambiguously encodes both the execution policy and the exact set of inner transactions. `serializeBatch()` produces that payload. + +## The Serialization Format + +The function appends four elements to a `Serializer`: + +1. **`HashPrefix::batch`** (`'B', 'C', 'H'` → `0x42434800`) — a 4-byte type discriminant that places batch hashes in their own hash-space, preventing any collision with signatures over raw transaction data, ledger nodes, or other protocol objects. This is a convention used uniformly across XRPL: every signable payload starts with its own `HashPrefix` constant so that the same binary content never accidentally validates as two different message types. + +2. **`flags`** — the `uint32_t` flags field of the outer batch transaction. These flags encode the execution policy (e.g., "apply all or none"). Including flags in the signed data is deliberate: a signer is not just authorizing the set of transactions, they are authorizing those transactions under a specific execution semantics. Stripping flags from the signed payload would let a malicious actor modify the policy after all signers committed. + +3. **`txids.size()`** — the inner-transaction count as a `uint32_t`. Serializing the count explicitly means that an adversary cannot extend or truncate the list without invalidating signatures; the signed byte string would have a different count prefix. + +4. **Each `uint256` transaction ID** — added via `addBitString`, the raw 32-byte hash of each inner transaction. Signers are committing to the exact set of inner transactions by their hashes, not by any mutable representation. + +## How It Is Used + +In `STTx.cpp`, both `checkBatchSingleSign()` and `checkBatchMultiSign()` construct a `Serializer`, call `serializeBatch()` to fill it, and then pass the resulting byte slice to the signature-verification helpers (`singleSignHelper` / `multiSignHelper`). This means signature checking and signature creation share a single serialization path — there is no risk of divergence between what the signer encoded and what the validator checks. + +The multi-sign path exploits an optimization: `serializeBatch()` is called once to produce `dataStart`, and then each per-signer verification appends only the per-signer account ID suffix via `finishMultiSigningData`. This avoids re-serializing the inner transaction list for every signer. + +In the test harness (`src/test/jtx/impl/batch.cpp`), signing helpers for both single-sign and multi-sign batch scenarios call `serializeBatch()` to assemble the message before calling `xrpl::sign()`. This ensures test-generated signatures exercise exactly the same format as production verification. + +## Why `inline` in a Header + +The function is marked `inline` and lives entirely in a header rather than a `.cpp` file. This is a pragmatic choice: the function is short, has no state, and needs to be callable from multiple translation units — the core library, test helpers, and potentially external tooling — without requiring a separate compilation unit or additional link dependencies. + +## Relationship to Dependencies + +`HashPrefix.h` provides the `batch` enum value and the broader `HashPrefix` type. Changing `HashPrefix::batch` would be a **protocol-breaking change** — it would render all existing batch signatures invalid — which is why `HashPrefix.h` explicitly notes that these values are part of the protocol and cannot be changed arbitrarily. + +`STVector256.h` is included as a convenience for callers. The function itself accepts a plain `std::vector`, but in practice the inner transaction IDs come from `STTx::getBatchTransactionIDs()`, which returns data ultimately stored in an `STVector256` field. Including the header here saves callers from having to pull it in themselves. + +`Serializer.h` provides the `Serializer` class that manages the growing byte buffer, owns the `add32()` and `addBitString()` methods used here, and exposes `slice()` so the completed payload can be passed directly to signature primitives. + +## Invariants Enforced + +The serialization order — prefix, then flags, then count, then IDs — is fixed by the code and must not be reordered without a corresponding protocol amendment. Any reordering would produce a different hash, breaking all previously issued signatures. Because the function is the only definition of this format in the codebase, any future change is naturally centralized here rather than scattered across signing and verification sites. \ No newline at end of file diff --git a/include/xrpl/protocol/Book.h.ai.json b/include/xrpl/protocol/Book.h.ai.json new file mode 100644 index 0000000000..a79eed824b --- /dev/null +++ b/include/xrpl/protocol/Book.h.ai.json @@ -0,0 +1,82 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "Book" + } + ], + "description": "Defines the xrpl::Book class representing an order book (pair of assets) and provides hashing, comparison, and utility functions for Book, Issue, MPTIssue, and Asset types, along with their std and boost hash specializations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Book.h", + "functions": [ + { + "args": [ + "book" + ], + "lineno": 29, + "name": "isConsistent" + }, + { + "args": [ + "book" + ], + "lineno": 31, + "name": "to_string" + }, + { + "args": [ + "os", + "x" + ], + "lineno": 33, + "name": "operator<<" + }, + { + "args": [ + "h", + "b" + ], + "lineno": 37, + "name": "hash_append" + }, + { + "args": [ + "book" + ], + "lineno": 48, + "name": "reversed" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 52, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 60, + "name": "operator<=>" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 81, + "name": "std" + }, + { + "lineno": 148, + "name": "boost" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Book.h.ai.md b/include/xrpl/protocol/Book.h.ai.md new file mode 100644 index 0000000000..a6bfec525c --- /dev/null +++ b/include/xrpl/protocol/Book.h.ai.md @@ -0,0 +1,52 @@ +# `include/xrpl/protocol/Book.h` + +## Role in the System + +`Book.h` defines the `Book` type, the fundamental unit of XRPL's decentralized exchange (DEX). An order book on the XRPL is an ordered set of offers to exchange one asset for another; `Book` encodes the identity of that book as a directed pair of `Asset` values — `in` (the asset being spent by a taker) and `out` (the asset being received). Every offer on the ledger belongs to exactly one such book, and ledger indexes, subscription filters, and offer-matching traversal all navigate books by this identity. + +The file also consolidates `std::hash` and `boost::hash` specializations for the three related protocol types — `Issue`, `MPTIssue`, and `Asset` — that are needed anywhere `Book` or its component types are stored in unordered containers. Placing all of these hash specializations here avoids scattered partial-template-specialization definitions across translation units. + +## The `Book` Class + +```cpp +class Book final : public CountedObject +{ +public: + Asset in; + Asset out; + std::optional domain; +}; +``` + +`Asset` is itself a `std::variant`, so a `Book` can represent any combination of the three asset classes the ledger supports: native XRP (via `Issue`), IOU tokens (via `Issue`), and Multi-Purpose Token issuances (via `MPTIssue`). This generalization was introduced when MPTs were added to the protocol; the original design only supported `Issue` pairs. + +The optional `domain` field represents a permissioned DEX domain — an on-ledger object (`PermissionedDomain`) that gates which accounts may participate in the book. When `domain` is set, the book is scoped to that domain's `uint256` ledger index, yielding an isolated order book namespace. Books with and without a domain hash and compare differently even if their `in`/`out` assets are identical, so the domain participates in equality, ordering, and hashing at every level. + +Inheriting from `CountedObject` is purely observational: the CRTP base maintains a global atomic counter of live `Book` instances, accessible through `CountedObjects::getCounts()`. This is used for diagnostic reporting under load, not for any protocol logic. + +## Consistency and Direction + +`isConsistent(book)` (implemented in `Book.cpp`) enforces two invariants: both component assets must individually be consistent (non-bad currency, valid MPT issuer), and `book.in != book.out`. A book where both legs name the same asset is meaningless and would cause infinite-loop offer matching. + +`reversed(book)` returns a new `Book` with `in` and `out` swapped while preserving `domain`. This is used when a subscription or traversal needs to walk the complementary book — for example, to find all offers that implicitly cross a given book by approaching from the other direction. + +`to_string(book)` formats as `"in->out"` using the underlying `to_string(Asset)` helpers, making log output and error messages readable. + +## Ordering and Equality + +`Book` provides both `operator==` and `operator<=>` (three-way comparison), enabling use in sorted containers such as `std::map` and `std::set`. The three-way comparison first orders by `in`, then by `out`, then by `domain`. The manual `std::optional` comparison in `operator<=>` treats an absent domain as less than any present domain. This ordering is important because the ledger's `BookDirs` traversal iterates over offer directories sorted by these keys, and off-ledger structures like subscription routing tables depend on deterministic ordering. + +The `std::optional` case is handled manually rather than relying on the standard library's built-in spaceship support for `optional` because the code needs explicit `std::weak_ordering` return type consistency with the `Asset` spaceship result, and to be safe against library versions where optional comparison behavior varies. + +## Hash Infrastructure + +The header provides four `std::hash` specializations (with corresponding `boost::hash` wrappers that simply inherit from them): + +- `std::hash` — combines currency hash with account hash, short-circuiting account for XRP (where all issuers are equivalent). +- `std::hash` — hashes only the 192-bit `MPTID`. +- `std::hash` — visits the variant and dispatches to the appropriate specialization. +- `std::hash` — hashes `in`, then `boost::hash_combine`s `out`, then conditionally combines `domain` if present. + +The pattern of `boost::hash` inheriting `std::hash` avoids code duplication while satisfying Boost.Unordered and Boost.MultiIndex containers that look up `boost::hash` rather than `std::hash`. The old comment `// VFALCO NOTE broken in vs2012` alongside disabled `using Base::Base` constructor inheritance is a legacy relic — Visual Studio 2012 did not support inheriting constructors, so explicit defaulted constructors are provided instead. + +The `hash_append` template function (used with beast's cryptographic hashing pipeline) is defined inline in the header and explicitly excludes `domain` from hashing only when absent, so the same `Book` with and without a domain produces different cryptographic fingerprints. This matters for ledger index derivation in `Indexes.cpp`, where `book.domain`'s presence conditionally changes the hash inputs used to locate the book's offer directory. \ No newline at end of file diff --git a/include/xrpl/protocol/BuildInfo.h.ai.json b/include/xrpl/protocol/BuildInfo.h.ai.json new file mode 100644 index 0000000000..42b86863a9 --- /dev/null +++ b/include/xrpl/protocol/BuildInfo.h.ai.json @@ -0,0 +1,68 @@ +{ + "args": [ + { + "lineno": 36, + "name": "versionStr" + }, + { + "lineno": 59, + "name": "version" + }, + { + "lineno": 69, + "name": "version" + } + ], + "classes": [], + "description": "Provides versioning information and utilities for the XRPL server build, including version string retrieval, encoding/decoding of version numbers, and version comparison.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/BuildInfo.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "getVersionString" + }, + { + "args": [], + "lineno": 19, + "name": "getFullVersionString" + }, + { + "args": [ + "versionStr" + ], + "lineno": 36, + "name": "encodeSoftwareVersion" + }, + { + "args": [], + "lineno": 54, + "name": "getEncodedVersion" + }, + { + "args": [ + "version" + ], + "lineno": 59, + "name": "isXrpldVersion" + }, + { + "args": [ + "version" + ], + "lineno": 69, + "name": "isNewerVersion" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "BuildInfo" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/BuildInfo.h.ai.md b/include/xrpl/protocol/BuildInfo.h.ai.md new file mode 100644 index 0000000000..3e0addfb36 --- /dev/null +++ b/include/xrpl/protocol/BuildInfo.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/protocol/BuildInfo.h` + +This header declares the `xrpl::BuildInfo` namespace, which owns all versioning concerns for the rippled server binary: human-readable version strings for protocol handshakes and HTTP headers, and a compact 64-bit encoding that lets validators in the network quickly report and compare software versions. + +## Why a Dedicated Version Module + +XRPL validators embed their software version in consensus validation messages — specifically in the `sfServerVersion` field of `STValidation` objects — so that the network can collectively detect when a majority of validators have upgraded. This requires a version representation that is compact enough to embed in wire-format messages, totally ordered (newer > older via integer comparison), and cross-implementation-aware (a validator running some other xrpl implementation should not be misread as a stale xrpld node). `BuildInfo` satisfies all three requirements. + +## String-Based API + +`getVersionString()` returns the canonical SemVer string (e.g., `"3.2.0-b0"`). Its implementation is a Meyers singleton: a static local `std::string` initialized once with a lambda that both builds the string and validates it through `beast::SemanticVersion`. If the hard-coded `versionString` constant fails to parse or fails a round-trip through the parser, `LogicError()` is thrown on first call. This is a start-up invariant check: a malformed version constant is caught immediately rather than producing silently wrong encoded integers. In `DEBUG` or sanitizer builds, the commit hash and build-mode metadata are appended as SemVer build metadata (e.g., `"3.2.0-b0+abc1234.DEBUG"`). + +`getFullVersionString()` prepends the system name, yielding `"xrpld-3.2.0-b0"`. This form appears verbatim in the `User-Agent` and `Server` HTTP headers during peer-protocol handshakes and in all HTTP responses from the JSON-RPC server — providing a single canonical identifier that third-party tooling can scrape. + +## 64-Bit Encoding + +`encodeSoftwareVersion(string_view)` packs a SemVer string into a `uint64_t` with the following layout: + +``` +[15:0] implementation identifier (0x183B for xrpld) +[23:16] major version (8 bits, 0-255) +[31:24] minor version (8 bits, 0-255) +[39:32] patch version (8 bits, 0-255) +[41:40] pre-release type (11 = release, 10 = RC, 01 = beta) +[47:42] pre-release number (6 bits, 1-63; 0 for releases) +[63:48] reserved zeros +``` + +The pre-release type encoding is deliberately chosen so that integer ordering matches semantic version ordering: a release (`0b11`) always compares greater than an RC (`0b10`), which compares greater than a beta (`0b01`). This makes `isNewerVersion()` a simple integer comparison, with no need to decode the fields. + +The implementation ID `0x183B` occupies the most-significant 16 bits. This lets the network accommodate alternative xrpl implementations: any node broadcasting a version whose top 16 bits are not `0x183B` is treated as an unknown implementation, and `isNewerVersion()` returns `false` for it unconditionally. This is a conservative design — rather than risk misinterpreting a foreign version encoding as older or newer, the node simply declines to make the comparison. + +`getEncodedVersion()` caches this node's own encoded value in a static, calling `encodeSoftwareVersion(getVersionString())` exactly once. + +## Role in Consensus + +At every "voting ledger" (every 256th ledger, when amendments are processed), `RCLConsensus` embeds `getEncodedVersion()` into each validation it broadcasts via the `sfServerVersion` field. `LedgerMaster` then inspects incoming validations: it calls `isXrpldVersion()` and `isNewerVersion()` on each validator's `sfServerVersion` to count how many validators are running a newer xrpld version. This count can be used to surface upgrade warnings — if a significant fraction of the network has already upgraded, an older node has actionable signal to do the same. + +## Deprecation Note + +The `// VFALCO The namespace is deprecated` comment on the `BuildInfo` namespace suggests a future intent to dissolve this sub-namespace and expose these utilities directly in `xrpl`. The API surface is small enough that such a migration would be purely mechanical. \ No newline at end of file diff --git a/include/xrpl/protocol/Concepts.h.ai.json b/include/xrpl/protocol/Concepts.h.ai.json new file mode 100644 index 0000000000..8aa863dd13 --- /dev/null +++ b/include/xrpl/protocol/Concepts.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ts..." + ], + "lineno": 34, + "name": "CombineVisitors" + } + ], + "description": "This file defines C++ concepts and utility templates for type-checking and visitation patterns related to XRPL protocol types such as assets, issues, and amounts. It provides concepts for compile-time validation of types and utilities for combining and applying multiple visitors to variants.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Concepts.h", + "functions": [ + { + "args": [ + "ts..." + ], + "lineno": 48, + "name": "make_combine_visitors" + }, + { + "args": [ + "v", + "visitors..." + ], + "lineno": 61, + "name": "visit" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 28, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Concepts.h.ai.md b/include/xrpl/protocol/Concepts.h.ai.md new file mode 100644 index 0000000000..58e40f9795 --- /dev/null +++ b/include/xrpl/protocol/Concepts.h.ai.md @@ -0,0 +1,79 @@ +# `include/xrpl/protocol/Concepts.h` + +## Role in the System + +This header is the compile-time type vocabulary for the XRPL protocol layer. It centralises all C++20 `concept` definitions that constrain the three payment asset families — XRP, IOU (trust-line), and MPT (Multi-Purpose Token) — and provides a small `std::variant` visitor utility that those same types rely on internally. Because it is included by both protocol-level types (`Asset`, `PathAsset`) and the payment path engine (`EitherAmount`, `OfferStream`), keeping it in a single header avoids duplicated constraints and ensures consistency across a large surface area. + +## Concepts + +### `StepAmount` + +```cpp +template +concept StepAmount = + std::is_same_v || std::is_same_v || std::is_same_v; +``` + +Constrains the three distinct numeric representations used as individual payment-step quantities. `EitherAmount`, the type-erased amount carrier used throughout the path-finding engine, restricts its constructor, `holds()`, and `get()` template parameters with `StepAmount`. This means the compiler rejects, at instantiation time, any attempt to store or query an amount type that doesn't belong to the sanctioned set. + +### `ValidIssueType` + +```cpp +template +concept ValidIssueType = std::is_same_v || std::is_same_v; +``` + +Scopes the `Asset::get()` and `Asset::holds()` template methods to exactly the two issue types that `Asset`'s internal `std::variant` can hold. Without this gate, a caller could instantiate `get()`, which would be nonsensical and produce a hard-to-understand compile error deep inside the variant machinery. The concept surfaces the constraint where it is intentional. + +`ValidIssueType` is also used for the `is_issue_v` and `is_mptissue_v` boolean constant templates in `Asset.h`, which drive `if constexpr` branches in the comparison operators. + +### `AssetType` + +```cpp +template +concept AssetType = std::is_convertible_v || std::is_convertible_v || + std::is_convertible_v || std::is_convertible_v; +``` + +This concept is intentionally broader than `ValidIssueType`. It uses `is_convertible_v` rather than `is_same_v`, which means it accepts any type with an implicit conversion path to one of the four named types. This enables generic code that accepts any "asset-like" value — including `Asset` itself — without forcing callers to normalise to a canonical form first. + +### `ValidPathAsset` + +```cpp +template +concept ValidPathAsset = (std::is_same_v || std::is_same_v); +``` + +`PathAsset` represents the currency/token specifier inside a payment path element — it explicitly does not carry issuer information, only the token identity. `Currency` covers both XRP (the zero currency) and IOU tokens; `MPTID` covers MPT issuances. `ValidPathAsset` constrains `PathAsset::get()`, `holds()`, and the `is_currency_v`/`is_mptid_v` helper constants in the same way `ValidIssueType` constrains `Asset`. + +### `ValidTaker` + +```cpp +template +concept ValidTaker = (... && !std::is_same_v || !std::is_same_v); +``` + +This two-parameter concept captures a fundamental DEX invariant: an offer cannot have both the `TakerPays` and `TakerGets` sides denominated in XRP. Both sides must independently be one of the three step-amount types, but the XRP/XRP combination is structurally illegal on the XRPL order book. `OfferStream::shouldRmSmallIncreasedQOffer()` uses this constraint to prevent the template from being instantiated for a nonsensical trading pair, encoding the rule at the type system level rather than as a runtime assertion. + +## The `detail::CombineVisitors` Visitor Utility + +Both `Asset` and `PathAsset` wrap `std::variant` internally and expose a `.visit(lambdas...)` member that accepts multiple lambdas and dispatches to the correct one based on the active alternative. This is the classical *overloaded pattern* for `std::visit`, and `CombineVisitors` is its implementation: + +```cpp +template +struct CombineVisitors : Ts... +{ + using Ts::operator()...; + constexpr CombineVisitors(Ts&&... ts) : Ts(std::forward(ts))...{} +}; +``` + +By inheriting from all lambda types simultaneously and pulling every `operator()` into the derived scope with `using Ts::operator()...`, `CombineVisitors` becomes a single callable that overload-resolution can dispatch correctly. `std::visit` then selects among those overloads based on the variant's active type at runtime. + +The factory function `make_combine_visitors` is preferred over a CTAD deduction guide. The comment in the file explains the reasoning: function-template argument deduction is more robust than class-template argument deduction (CTAD) when the template parameters involve parameter packs — CTAD for variadic class templates has historically had portability problems. The factory also applies `std::decay_t` to strip reference and cv-qualifiers from lambda types before they become base classes, which ensures the inherited `operator()` calls have the correct value categories. + +The entire utility lives in `xrpl::detail`, signalling that it is an implementation detail not intended for direct external use; callers always go through `Asset::visit()` or `PathAsset::visit()`, which delegate to `detail::visit()`. + +## Design Rationale + +Centralising these constraints in one header rather than repeating `std::is_same_v` chains in every consumer file has two practical benefits. First, adding a new payment asset family (a future fourth type alongside XRP, IOU, and MPT) requires updating `StepAmount`, `ValidIssueType`, and `ValidTaker` in one place; the compiler then propagates errors to every call site that needs to handle the new case. Second, the concepts produce clean diagnostic messages — when a caller passes an unsatisfied type, the error points to the named concept rather than to a deep template instantiation chain inside variant machinery. \ No newline at end of file diff --git a/include/xrpl/protocol/ErrorCodes.h.ai.json b/include/xrpl/protocol/ErrorCodes.h.ai.json new file mode 100644 index 0000000000..f9177fd9fb --- /dev/null +++ b/include/xrpl/protocol/ErrorCodes.h.ai.json @@ -0,0 +1,199 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 109, + "name": "ErrorInfo" + } + ], + "description": "Defines error and warning codes for XRPL RPC commands, provides structures and utility functions for error handling, and includes helpers for generating and injecting error messages into JSON responses.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/ErrorCodes.h", + "functions": [ + { + "args": [ + "code" + ], + "lineno": 120, + "name": "get_error_info" + }, + { + "args": [ + "code", + "json" + ], + "lineno": 123, + "name": "inject_error" + }, + { + "args": [ + "code", + "message", + "json" + ], + "lineno": 126, + "name": "inject_error" + }, + { + "args": [ + "code" + ], + "lineno": 130, + "name": "make_error" + }, + { + "args": [ + "code", + "message" + ], + "lineno": 131, + "name": "make_error" + }, + { + "args": [ + "message" + ], + "lineno": 134, + "name": "make_param_error" + }, + { + "args": [ + "name" + ], + "lineno": 138, + "name": "missing_field_message" + }, + { + "args": [ + "name" + ], + "lineno": 142, + "name": "missing_field_error" + }, + { + "args": [ + "name" + ], + "lineno": 146, + "name": "missing_field_error" + }, + { + "args": [ + "name" + ], + "lineno": 150, + "name": "object_field_message" + }, + { + "args": [ + "name" + ], + "lineno": 154, + "name": "object_field_error" + }, + { + "args": [ + "name" + ], + "lineno": 158, + "name": "object_field_error" + }, + { + "args": [ + "name" + ], + "lineno": 162, + "name": "invalid_field_message" + }, + { + "args": [ + "name" + ], + "lineno": 166, + "name": "invalid_field_message" + }, + { + "args": [ + "name" + ], + "lineno": 170, + "name": "invalid_field_error" + }, + { + "args": [ + "name" + ], + "lineno": 174, + "name": "invalid_field_error" + }, + { + "args": [ + "name", + "type" + ], + "lineno": 178, + "name": "expected_field_message" + }, + { + "args": [ + "name", + "type" + ], + "lineno": 182, + "name": "expected_field_message" + }, + { + "args": [ + "name", + "type" + ], + "lineno": 186, + "name": "expected_field_error" + }, + { + "args": [ + "name", + "type" + ], + "lineno": 190, + "name": "expected_field_error" + }, + { + "args": [], + "lineno": 194, + "name": "not_validator_error" + }, + { + "args": [ + "json" + ], + "lineno": 200, + "name": "contains_error" + }, + { + "args": [ + "code" + ], + "lineno": 203, + "name": "error_code_http_status" + }, + { + "args": [ + "jv" + ], + "lineno": 207, + "name": "rpcErrorString" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 106, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/ErrorCodes.h.ai.md b/include/xrpl/protocol/ErrorCodes.h.ai.md new file mode 100644 index 0000000000..2662438647 --- /dev/null +++ b/include/xrpl/protocol/ErrorCodes.h.ai.md @@ -0,0 +1,48 @@ +# `include/xrpl/protocol/ErrorCodes.h` + +## Role in the System + +This header is the single source of truth for every RPC error the XRPL node can emit. It defines the stable numeric code space (`error_code_i`), a parallel warning code space (`warning_code_i`), a structure that binds each code to a human-readable token and HTTP status, and the entire vocabulary of helper functions that RPC handlers use to produce well-formed JSON error objects. Every component that rejects an RPC call — from malformed parameter checks to ledger-not-found conditions — funnels through this file. + +## The `error_code_i` Enum and Stability Guarantee + +The enum runs from `rpcBAD_SYNTAX = 1` through `rpcLAST`, with `-1` reserved for `rpcUNKNOWN` (out-of-range codes) and `0` for `rpcSUCCESS`. Although the comments acknowledge the values were never formally promised to be stable, real API consumers started depending on them, so the file now carries an explicit policy: **only append new codes at the end; never fill gaps; never reuse values**. Dead codes are commented out with "// unused" rather than reassigned. This append-only discipline is enforced socially through comments but backed structurally by `rpcLAST`, which must always equal the highest valid code — if it is not updated when a new code is added the compile-time validation in the `.cpp` file will throw. + +The enum is deliberately divided into semantic groups: general failures, networking problems, ledger-state errors, malformed-command errors, bad-parameter errors, and internal errors. These categories don't affect runtime behaviour but help maintainers decide which group a new code belongs to, keeping gaps predictable. + +## `warning_code_i` + +A separate, smaller enum for warnings returned in the `warnings` array of some RPC responses (not in the top-level `error` field). Its values start at 1001 to be clearly distinct from error codes. The comment explicitly calls out `warnRPC_FIELDS_DEPRECATED = 2004` as needing to remain stable because Clio (the alternative XRPL API server) hardcodes this value — cross-implementation compatibility in a live network. + +## `ErrorInfo` and the Compile-Time Lookup Table + +`ErrorInfo` is a plain `constexpr`-constructible struct with four fields: the `error_code_i` value, a `Json::StaticString` token (e.g., `"invalidParams"`), a `Json::StaticString` default message, and an `int http_status` that defaults to 200. Using `Json::StaticString` avoids heap allocation when the token is injected into a JSON value, since `StaticString` holds a raw `const char*` pointer to a string literal. + +The implementation in `ErrorCodes.cpp` stores the canonical data in `unorderedErrorInfos`, an unordered `constexpr` array of `ErrorInfo` entries, then sorts and validates it at compile time via `sortErrorInfos(...)`. This function: + +1. Allocates a default-initialised `std::array`. +2. Places each entry at index `code - 1` (since `rpcSUCCESS == 0`, valid codes start at 1). +3. Throws `std::out_of_range` on out-of-range codes, and `std::invalid_argument` on duplicates or gaps — all at compile time, meaning mismatches are link errors not runtime surprises. +4. Validates the final entry count against `rpcLAST`. + +The result, `detail::sortedErrorInfos`, is a zero-runtime-cost array indexed by `code - 1`. `get_error_info(code)` is therefore an O(1) bounds check and a single array access — no hash table, no binary search. + +The comment in the implementation explains the reasoning behind HTTP status selection: the goal is **load balancer fail-over semantics**. Errors that indicate a node is temporarily unable to serve a request (ledger not found, server too busy, amendment blocked) return `503 Service Unavailable` or similar 4xx/5xx codes so a load balancer can redirect the client to a healthy peer. Errors that are definitively the client's fault (bad parameters, malformed account) return `400 Bad Request`, while some semantic states like "ledger not yet validated" return `202 Accepted`. Errors that historically returned 200 keep that default, preserving backward compatibility. + +## The JSON Error API + +`inject_error(code, json)` mutates an existing `Json::Value` object by writing three fields: `error` (the token string), `error_code` (the numeric code), and `error_message` (the default message). The overload `inject_error(code, message, json)` replaces the default message with a caller-supplied string, used when context-specific detail is needed (e.g., which specific field was malformed). + +`make_error` is a convenience wrapper that creates a new `Json::Value`, calls `inject_error`, and returns it. RPC handlers that construct a response from scratch use `make_error`; handlers that need to annotate an existing response object use `inject_error`. + +`contains_error(json)` checks for the presence of the `error` key — the canonical test for whether a JSON object represents an error response. `rpcErrorString(jv)` concatenates token and message for logging, with an `XRPL_ASSERT` asserting the input actually contains an error. + +## Field-Error Helper Family + +The inline helpers in `namespace RPC` (`missing_field_error`, `invalid_field_error`, `object_field_error`, `expected_field_error`, and their `_message` variants) exist purely to eliminate repetitive string formatting across the hundreds of RPC parameter-validation sites. They all bottom out in `make_param_error`, which is itself a thin wrapper around `make_error(rpcINVALID_PARAMS, message)`. Each helper is overloaded for both `std::string` and `Json::StaticString` to avoid unnecessary string construction when field names are compile-time literals. + +`not_validator_error()` is the only non-field-specific helper, hardcoding the message "not a validator" for the handful of commands that require the node to be a validator. + +## Relationship to `RPCErr.h` + +The adjacent `RPCErr.h` exposes `isRpcError` and `rpcError`, which are marked deprecated. These are older wrappers predating the current injection/make API. New code uses the functions in `ErrorCodes.h` directly. \ No newline at end of file diff --git a/include/xrpl/protocol/Feature.h.ai.json b/include/xrpl/protocol/Feature.h.ai.json new file mode 100644 index 0000000000..4277a7e635 --- /dev/null +++ b/include/xrpl/protocol/Feature.h.ai.json @@ -0,0 +1,128 @@ +{ + "args": [ + { + "lineno": 54, + "name": "fn" + }, + { + "lineno": 66, + "name": "fn" + }, + { + "lineno": 139, + "name": "name" + }, + { + "lineno": 142, + "name": "f" + }, + { + "lineno": 145, + "name": "i" + }, + { + "lineno": 148, + "name": "f" + }, + { + "lineno": 263, + "name": "bs" + }, + { + "lineno": 263, + "name": "f" + } + ], + "classes": [ + { + "args": [], + "lineno": 151, + "name": "FeatureBitset" + } + ], + "description": "This file defines the feature/amendment management system for the XRPL protocol, including compile-time and runtime utilities for registering, validating, and manipulating protocol features and amendments, as well as the FeatureBitset class for efficient feature set operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Feature.h", + "functions": [ + { + "args": [ + "fn" + ], + "lineno": 54, + "name": "validFeatureNameSize" + }, + { + "args": [ + "fn" + ], + "lineno": 66, + "name": "validFeatureName" + }, + { + "args": [], + "lineno": 81, + "name": "allAmendments" + }, + { + "args": [], + "lineno": 120, + "name": "supportedAmendments" + }, + { + "args": [], + "lineno": 127, + "name": "numDownVotedAmendments" + }, + { + "args": [], + "lineno": 134, + "name": "numUpVotedAmendments" + }, + { + "args": [ + "name" + ], + "lineno": 139, + "name": "getRegisteredFeature" + }, + { + "args": [ + "f" + ], + "lineno": 142, + "name": "featureToBitsetIndex" + }, + { + "args": [ + "i" + ], + "lineno": 145, + "name": "bitsetIndexToFeature" + }, + { + "args": [ + "f" + ], + "lineno": 148, + "name": "featureToName" + }, + { + "args": [ + "bs", + "f" + ], + "lineno": 263, + "name": "foreachFeature" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 44, + "name": "xrpl" + }, + { + "lineno": 87, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Feature.h.ai.md b/include/xrpl/protocol/Feature.h.ai.md new file mode 100644 index 0000000000..64a654064e --- /dev/null +++ b/include/xrpl/protocol/Feature.h.ai.md @@ -0,0 +1,51 @@ +# `include/xrpl/protocol/Feature.h` + +## Role in the System + +This header is the central registration and manipulation point for XRPL's **amendment system** — the protocol's mechanism for introducing new ledger behavior in a network-wide, validator-voted way. Every protocol extension, from NFT support to AMM integration, is controlled by an amendment identifier that gates conditional code paths at runtime. `Feature.h` defines how amendments are declared, how their identities are computed, and how sets of enabled amendments are represented efficiently at ledger-processing time. + +## The X-Macro Pattern: A Single Source of Truth + +The defining architectural choice in this file is the use of `features.macro` as a single declarative list of all amendments. This `.macro` file is `#include`d multiple times with different macro definitions each time, a classic X-macro pattern. Each invocation achieves a different goal: + +**Counting at compile time.** Inside `namespace detail`, the macros are redefined to expand each entry to `+1`. The resulting sum is the compile-time constant `detail::numFeatures`, which determines the template parameter of the underlying `std::bitset`. This constant must never be _less than_ the actual number of amendments — a `LogicError` on startup verifies this — but it may be larger, reserving headroom. + +**Declaring extern variables.** Near the bottom of `Feature.h`, the macros are redefined so each `XRPL_FEATURE(name, ...)` becomes `extern uint256 const featureName;` and each `XRPL_FIX(name, ...)` becomes `extern uint256 const fixName;`. These are the identifiers used everywhere in the codebase in `rules.enabled(featureName)` checks. Retired features expand to nothing — their conditional code has already been removed. + +**Registering at startup.** In `Feature.cpp`, the macros are expanded to call `registerFeature()`, which SHA-512/half-hashes the feature's string name to produce its `uint256` identifier and stores it in an internal `boost::multi_index_container`. Because these are static variable initializers, registration happens before `main()`. A final static variable calls `registrationIsDone()`, setting an atomic `readOnly` flag that prevents further registration and permits lookups to proceed safely. + +This three-context expansion means the list of amendments never needs to be maintained in more than one place. + +## Feature Name Validation at Compile Time + +The `consteval` functions `validFeatureNameSize()` and `validFeatureName()` run entirely during compilation. `validFeatureName()` rejects any name containing characters below `0x20` (non-printable control characters) or with the `0x80` bit set (non-ASCII / UTF-8 multibyte sequences). This guards against visually confusable Unicode identifiers that C++ technically permits. + +`validFeatureNameSize()` enforces two bounds: names must not exceed 63 characters, and names must not be _exactly_ 32 bytes. The 32-byte exclusion is a deliberate forward-compatibility reserve: a `uint256` (XRPL's 32-byte hash type) could theoretically appear as a compact feature selector in WASM or interop contexts, and allowing a human-readable 32-character name would create an ambiguous namespace collision. + +Both functions are called inside `enforceValidFeatureName()` in `Feature.cpp`, which wraps them in `static_assert` — any invalid feature name causes a hard compile error rather than a runtime failure. + +## `FeatureBitset`: Efficient Feature Sets + +`FeatureBitset` is a thin wrapper around `std::bitset` that replaces integer-index access with `uint256`-based access. It inherits the base `bitset` privately and selectively exposes its interface via `using` declarations. + +The fundamental insight is a two-level identity scheme. Externally, every amendment is a `uint256` hash; internally, it maps to a compact sequential index via `featureToBitsetIndex()`. This translation lives in `FeatureCollections::featureToBitsetIndex()`, which queries the `multi_index_container` by `uint256` and returns the element's position in the random-access index. Once translated, all bitset operations — `set`, `reset`, `flip`, `test`, and the full suite of bitwise operators — run in O(1) with no further hashing. + +The class provides overloaded `operator&`, `operator|`, and `operator^` for set-algebra across `FeatureBitset` objects and between `FeatureBitset` and individual `uint256` values. The `operator-` overload is defined as **set difference** (`lhs & ~rhs`), critical for amendment voting logic where the system needs to compute "amendments I support but that are not yet enabled." + +The `foreachFeature()` free function iterates all set bits and translates each index back to its `uint256` via `bitsetIndexToFeature()` — the inverse lookup — before passing it to a callback. + +## Amendment Lifecycle and Governance + +`Feature.h` documents a precise lifecycle enforced by the `VoteBehavior` and `AmendmentSupport` enumerations: + +- **`Unsupported` / `DefaultNo`**: new amendments enter in this state. The conditional code exists but validators do not vote for activation. +- **`Supported` / `DefaultNo`**: development complete; validators recognize the amendment but still abstain by default. External governance determines when the network is ready. +- **`Supported` / `DefaultYes`**: reserved for critical bug fixes, after off-chain consensus. Validators actively vote for activation. +- **`VoteBehavior::Obsolete`**: the amendment was once supported and votable but never passed; it must remain registered (to handle the theoretical case that it eventually activates) but no validator will vote for it. +- **`XRPL_RETIRE_*`**: amendments active for many years whose conditional code has been deleted. They are registered via `retireFeature()` — internally `registerFeature(name, Supported::yes, VoteBehavior::Obsolete)` — so that nodes encountering them in the ledger's Amendments object remain amendment-compatible rather than blocked. + +The rule that a `Supported::yes` amendment can **never** revert to `Supported::no` once released prevents a dangerous situation: a future validator binary that does not recognize an amendment it could plausibly encounter would become amendment-blocked on any network where that amendment is enabled. + +## Relationship to `Rules` + +The runtime consumer of `FeatureBitset` is `Rules` (in `Rules.h`), which holds the set of currently active amendments derived from the validated ledger. All feature-gated code calls `view.rules.enabled(featureName)`, where `featureName` is one of the `extern uint256 const` variables declared by the macro expansion in `Feature.h`. Because `Rules::enabled()` takes a `uint256` and `FeatureBitset` translates it to a bitset index, the hot path for every transaction application is a hash-map lookup followed by a bitset test — both effectively O(1) with small constants. \ No newline at end of file diff --git a/include/xrpl/protocol/Fees.h.ai.json b/include/xrpl/protocol/Fees.h.ai.json new file mode 100644 index 0000000000..8d19f9fcf0 --- /dev/null +++ b/include/xrpl/protocol/Fees.h.ai.json @@ -0,0 +1,49 @@ +{ + "args": [ + { + "lineno": 27, + "name": "base_" + }, + { + "lineno": 27, + "name": "reserve_" + }, + { + "lineno": 27, + "name": "increment_" + }, + { + "lineno": 36, + "name": "ownerCount" + } + ], + "classes": [ + { + "args": [ + "base", + "reserve", + "increment" + ], + "lineno": 13, + "name": "Fees" + } + ], + "description": "Defines the Fees struct representing ledger fee settings in the XRPL, including base transaction cost, account reserve, and reserve increment. Also includes a deprecated fee units constant for backward compatibility.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Fees.h", + "functions": [ + { + "args": [ + "ownerCount" + ], + "lineno": 36, + "name": "accountReserve" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Fees.h.ai.md b/include/xrpl/protocol/Fees.h.ai.md new file mode 100644 index 0000000000..4457d30b86 --- /dev/null +++ b/include/xrpl/protocol/Fees.h.ai.md @@ -0,0 +1,57 @@ +# `include/xrpl/protocol/Fees.h` + +## Purpose + +`Fees.h` defines the `Fees` struct — a lightweight, value-semantic snapshot of a ledger's fee schedule. Every ledger in the XRP Ledger carries three economically significant parameters: the minimum cost of a transaction, the base account reserve, and the per-object reserve increment. This header packages those three values together so that transaction processing code can query them in a consistent, type-safe way through the `ReadView::fees()` interface. + +The design reflects a fundamental ledger invariant: **fees are constant within a ledger**. Validators may vote to change fee parameters, but any change only takes effect at the next ledger boundary. The struct models this by being a plain aggregate with no mutation methods. Code that processes transactions simply captures the current `Fees const&` at the start of ledger application and uses it throughout. + +## `FEE_UNITS_DEPRECATED` + +```cpp +inline constexpr std::uint32_t FEE_UNITS_DEPRECATED = 10; +``` + +Before the `XRPFees` amendment, the XRPL expressed transaction costs in abstract "fee units" rather than raw drops. A reference transaction cost 10 fee units, and the actual drop cost was computed by multiplying fee units by a scaling factor stored on the ledger. After the amendment, fees are expressed natively in drops using `XRPAmount`. The constant `FEE_UNITS_DEPRECATED = 10` survives as a compatibility shim — it is inserted into JSON subscription messages and validation objects when code detects that `featureXRPFees` is not enabled on the active ledger, preserving the legacy `fee_ref` field consumed by older clients and tools. + +## `Fees` struct + +```cpp +struct Fees { + XRPAmount base{0}; + XRPAmount reserve{0}; + XRPAmount increment{0}; +}; +``` + +All three fields are `XRPAmount` — a strongly typed `int64_t` drop count defined in `XRPAmount.h`. Using a distinct type over a raw integer prevents accidental mixing of drop amounts with dimensionless values, and enables the arithmetic needed in `accountReserve()` to remain type-safe. Fields are zero-initialized by default, which is useful for constructing a placeholder or an empty fee schedule in tests. + +**`base`** is the minimum fee for a reference transaction in drops. Transactions paying less than this value are rejected. + +**`reserve`** is the base account reserve — the minimum XRP balance every account on the ledger must hold simply to exist. An account whose balance falls below its total reserve becomes "reserve-deficient" and cannot send payments. + +**`increment`** is the additional reserve required for each "owned" ledger object such as trust lines, offers, escrows, or NFT tokens. This creates a per-object cost that scales with the account's footprint on the shared ledger state. + +## `accountReserve()` + +```cpp +XRPAmount accountReserve(std::size_t ownerCount) const { + return reserve + ownerCount * increment; +} +``` + +This is the only non-trivial behavior in the file. It computes the total XRP an account must hold given its current `ownerCount` — the number of ledger objects it owns. The formula `reserve + ownerCount * increment` is applied ubiquitously across transactors before creating new ledger objects. Examples from the codebase: + +- `OfferCreate` calls `sb.fees().accountReserve(sfOwnerCount + 1)` before booking a new offer, checking that the account can afford the incremental reserve. +- `TrustSet` uses it to gate creation of a new trust line. +- `DIDSet`, `MPTokenIssuanceCreate`, and `VaultCreate` each guard new object creation with the same pattern. + +The `+ 1` idiom — passing `ownerCount + 1` rather than the current count — is deliberate: it checks whether the account will be able to afford the new object *after* its `ownerCount` increases, not just whether it currently meets the existing reserve. + +The multiplication `ownerCount * increment` relies on `XRPAmount::operator*(value_type)`, which returns a new `XRPAmount`. The subsequent addition uses `XRPAmount::operator+=(XRPAmount const&)`. Both are `constexpr`-friendly and overflow-silent at the type level; correctness depends on `ownerCount` being bounded by protocol limits (currently capped at 4,294,967,295 by the `uint32` on-ledger field). + +## Relationship to the rest of the system + +`Fees` enters the transaction processing path through `ReadView`, the abstract base class for all ledger views. `ReadView::fees()` returns a `Fees const&` so that any code holding a view — transactors, preflight checks, RPC handlers — can query fee parameters without knowing whether the view is a closed ledger, an open ledger, or a sandbox. The fee schedule itself is loaded from the `FeeSettings` SLE on each ledger by the view implementation. + +Fee *changes* are handled separately by `FeeVoteImpl`, which runs during the consensus round and embeds the validator's desired `base`, `reserve`, and `increment` values into validation messages. When a supermajority of validators agree on a new fee schedule, the change is applied as a pseudo-transaction that updates the `FeeSettings` SLE — after which the next ledger's `ReadView::fees()` will return the updated `Fees` snapshot. \ No newline at end of file diff --git a/include/xrpl/protocol/HashPrefix.h.ai.json b/include/xrpl/protocol/HashPrefix.h.ai.json new file mode 100644 index 0000000000..14d0a7703d --- /dev/null +++ b/include/xrpl/protocol/HashPrefix.h.ai.json @@ -0,0 +1,57 @@ +{ + "args": [ + { + "lineno": 10, + "name": "a" + }, + { + "lineno": 10, + "name": "b" + }, + { + "lineno": 10, + "name": "c" + }, + { + "lineno": 49, + "name": "h" + }, + { + "lineno": 49, + "name": "hp" + } + ], + "classes": [], + "description": "Defines hash prefixes used in the XRPL protocol to namespace different types of hashes, preventing collisions between different object types. Provides an enum for the prefixes and a utility for appending them to hashers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/HashPrefix.h", + "functions": [ + { + "args": [ + "a", + "b", + "c" + ], + "lineno": 10, + "name": "make_hash_prefix" + }, + { + "args": [ + "h", + "hp" + ], + "lineno": 49, + "name": "hash_append" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/HashPrefix.h.ai.md b/include/xrpl/protocol/HashPrefix.h.ai.md new file mode 100644 index 0000000000..8b9522fa9d --- /dev/null +++ b/include/xrpl/protocol/HashPrefix.h.ai.md @@ -0,0 +1,69 @@ +# `HashPrefix.h` — Protocol Hash Domain Separation + +## Purpose + +`HashPrefix.h` defines a set of 4-byte sentinel values that are prepended to binary data before it is hashed anywhere in the XRPL protocol. The fundamental problem it solves is **hash domain collision**: two structurally different objects that happen to share the same serialized bytes would otherwise hash to the same digest. By prefixing every hash input with a type-specific constant, the XRPL protocol guarantees that a transaction hash and an account-state hash with identical raw bytes will always be different values. + +These constants are **immutable protocol artifacts**. The file's own comment states this plainly: changing the type or value of any prefix breaks consensus and cross-node compatibility. + +## Construction: `make_hash_prefix` + +The private helper `detail::make_hash_prefix(char a, char b, char c)` packs three ASCII characters into the high 24 bits of a `uint32_t`, leaving the low 8 bits as zero: + +``` +result = (a << 24) | (b << 16) | (c << 8) +``` + +This is a deliberate engineering choice. The trailing zero byte acts as an implicit separator and ensures no prefix value can accidentally equal a valid one-byte or two-byte byte sequence. The ASCII mnemonic letters make the prefixes self-documenting in hex dumps — `HashPrefix::transactionID` becomes `0x54584E00` (`T`, `X`, `N`, `\0`), instantly recognizable when inspecting raw network data or database files. + +## The Enum + +`HashPrefix` is a strongly-typed `enum class : std::uint32_t` with twelve members, each covering a distinct context in which hashing occurs: + +| Enumerator | Mnemonic | Usage context | +|---|---|---| +| `transactionID` | TXN | Hashing a bare transaction to produce its canonical ID | +| `txNode` | SND | Hashing a transaction plus its execution metadata | +| `leafNode` | MLN | Hashing an account-state leaf node in the SHAMap | +| `innerNode` | MIN | Hashing a SHAMap inner (branch) node | +| `ledgerMaster` | LWR | Hashing ledger header data for signing | +| `txSign` | STX | Hashing a transaction body for single signing | +| `txMultiSign` | SMT | Hashing a transaction body for multi-signing | +| `validation` | VAL | Hashing a validator validation message | +| `proposal` | PRP | Hashing a consensus proposal | +| `manifest` | MAN | Hashing a validator manifest for signing | +| `paymentChannelClaim` | CLM | Hashing a payment channel off-ledger claim | +| `batch` | BCH | Hashing batch transaction data | + +The separation between `txSign` and `txMultiSign` is particularly important: a single-signature blob cannot be replayed as a multi-signature contribution, because the hash inputs differ even when the transaction serialization is byte-for-byte identical. + +## Integration with `hash_append` + +The file provides a template free function: + +```cpp +template +void hash_append(Hasher& h, HashPrefix const& hp) noexcept; +``` + +This conforms to the N3980 "Types Don't Know #" protocol used throughout the beast/xrpl hashing infrastructure. By implementing `hash_append`, a `HashPrefix` value can be composed seamlessly with other objects in a variadic call to `sha512Half` from `digest.h`: + +```cpp +sha512Half(HashPrefix::transactionID, data) +``` + +That call hashes the 32-bit prefix value followed immediately by the transaction bytes, all in a single SHA-512 pass. No temporary allocations, no explicit serialization step — the hash_append machinery feeds each argument directly into the running digest state. + +## Two Call Styles in Practice + +Callers use `HashPrefix` via two distinct patterns, both leading to the same prefix-then-data layout: + +1. **Serializer prefix** — used in `Sign.cpp`, `PayChan.h`, and ledger code: `ss.add32(HashPrefix::txSign)` writes the raw uint32 into a `Serializer` buffer before appending the object's signing fields. This is the style used when a signature or ledger hash must be computed over a Serializer-built blob. + +2. **`hash_append` composition** — used in `SHAMapInnerNode.cpp` and `sha512Half` calls in the SHAMap layer: `hash_append(h, HashPrefix::innerNode)` feeds the prefix directly into a streaming hasher alongside node data, avoiding an intermediate buffer. + +Both paths produce the same 4-byte prefix at position zero of the hash input, which is the only invariant that matters for domain separation. + +## Why This Matters for Security + +Without domain separation, a carefully crafted account state ledger object could be constructed with bytes identical to a transaction, letting an attacker claim a false transaction ID or forge a signing payload. The prefix scheme closes this class of attack at the protocol layer, not at the application layer, so every code path that hashes XRPL data automatically operates in an isolated namespace regardless of where or how it computes that hash. \ No newline at end of file diff --git a/include/xrpl/protocol/IOUAmount.h.ai.json b/include/xrpl/protocol/IOUAmount.h.ai.json new file mode 100644 index 0000000000..6c591d35bd --- /dev/null +++ b/include/xrpl/protocol/IOUAmount.h.ai.json @@ -0,0 +1,102 @@ +{ + "args": [ + { + "lineno": 61, + "name": "mantissa" + }, + { + "lineno": 61, + "name": "exponent" + }, + { + "lineno": 58, + "name": "other" + }, + { + "lineno": 93, + "name": "os" + }, + { + "lineno": 93, + "name": "x" + }, + { + "lineno": 97, + "name": "amount" + }, + { + "lineno": 104, + "name": "amt" + }, + { + "lineno": 104, + "name": "num" + }, + { + "lineno": 104, + "name": "den" + }, + { + "lineno": 104, + "name": "roundUp" + }, + { + "lineno": 113, + "name": "v" + } + ], + "classes": [ + { + "args": [], + "lineno": 16, + "name": "IOUAmount" + }, + { + "args": [ + "v" + ], + "lineno": 122, + "name": "NumberSO" + } + ], + "description": "Defines the IOUAmount class for representing high dynamic range floating point amounts in the XRPL protocol, along with related utility functions and a RAII helper for managing a global switchover flag.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/IOUAmount.h", + "functions": [ + { + "args": [ + "amount" + ], + "lineno": 97, + "name": "to_string" + }, + { + "args": [ + "amt", + "num", + "den", + "roundUp" + ], + "lineno": 104, + "name": "mulRatio" + }, + { + "args": [], + "lineno": 111, + "name": "getSTNumberSwitchover" + }, + { + "args": [ + "v" + ], + "lineno": 113, + "name": "setSTNumberSwitchover" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/IOUAmount.h.ai.md b/include/xrpl/protocol/IOUAmount.h.ai.md new file mode 100644 index 0000000000..2714e4ada7 --- /dev/null +++ b/include/xrpl/protocol/IOUAmount.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/protocol/IOUAmount.h` + +## Purpose and Role + +`IOUAmount` is XRPL's fixed-precision floating-point type for representing IOU (non-native asset) balances and trust line amounts. It models the ledger's custom numeric encoding: a 64-bit signed mantissa paired with an integer exponent, where the value is `mantissa × 10^exponent`. This is distinct from `XRPAmount`, which stores drops as a plain 64-bit integer, because IOU amounts must span an enormous dynamic range — from microscopic fractional token balances to astronomical supply totals — without the rounding artifacts that binary floating-point would introduce. + +The file also houses the `NumberSO` RAII guard and the `getSTNumberSwitchover()`/`setSTNumberSwitchover()` pair, which together manage a per-coroutine feature flag controlling which normalization path is active. + +## Representation and Normalization + +The encoding imposes strict invariants on a canonical form. The absolute value of the mantissa must lie in `[10^15, 10^16−1]` (i.e., `[1000000000000000, 9999999999999999]`), and the exponent must be in `[-96, 80]`. These bounds match the constants `STAmount::cMinValue`, `STAmount::cMaxValue`, `cMinOffset`, and `cMaxOffset`, tying `IOUAmount` directly to the on-wire serialization format used by `STAmount`. + +`normalize()` is private and called from the `(mantissa, exponent)` constructor. It enforces this canonical form by scaling the mantissa up (multiply by 10, decrement exponent) or down (divide by 10, increment exponent) until the mantissa is in range. If the value is too large after scaling, it throws `std::overflow_error`. If it is too small, the amount silently rounds to zero — this asymmetry is intentional: overflow is a programming error worth detecting, while sub-minimum amounts arise naturally from interest accrual calculations and must degrade gracefully to zero. + +The zero representation stores `mantissa_ = 0, exponent_ = -100`. The comment in `operator=(beast::Zero)` explains the sentinel: zero must sort *below* the smallest representable positive amount, whose exponent is `-96`. Using `-100` as the exponent for zero ensures that any numeric comparison via the exponent field gives the correct ordering. + +## Two Normalization Paths and the Switchover Flag + +`normalize()` has two code paths, selected by `getSTNumberSwitchover()`: + +**Legacy path** (switchover off): A simple loop adjusts the mantissa digit-by-digit in place. This was the original XRPL algorithm. + +**Number path** (switchover on, the default): Delegates entirely to `Number::normalizeToRange(minMantissa, maxMantissa)`. The `Number` class is a richer floating-point type that uses 128-bit intermediate arithmetic and supports configurable mantissa ranges (a "small" range matching IOUAmount's 10^15 precision, and a "large" range for 10^18 precision needed by newer features like SingleAssetVault and LendingProtocol). Similarly, `operator+=` under the switched-on path routes through `Number{*this} + Number{other}` rather than performing manual exponent alignment. + +This dual-path design is an amendment-gated migration. The switchover is stored in a `LocalValue` — a coroutine-local (not merely thread-local) key-value store that XRPL uses so that concurrent coroutines processing different transactions each see their own copy of the flag. The `NumberSO` RAII class wraps `getSTNumberSwitchover()`/`setSTNumberSwitchover()`, saving the current value on construction and restoring it on destruction. Test code and ledger replay can use this guard to temporarily force either path without global side effects. + +## Operator Design via Boost.Operators + +`IOUAmount` privately inherits from `boost::totally_ordered` and `boost::additive`. These policy mixins generate the full set of comparison operators (`>`, `<=`, `>=`, `!=`) from just `operator==` and `operator<`, and generate binary `operator+` and `operator-` from just `operator+=` and `-=`. This eliminates repetitive boilerplate while keeping the hand-written operators small and auditable. + +The equality check (`operator==`) directly compares the raw `(mantissa_, exponent_)` fields. This is valid because after normalization, every non-zero value has a unique canonical representation; zero always has `(0, -100)`. The less-than check (`operator<`) converts both operands to `Number` and defers to `Number`'s own comparison. The `Number` type handles the zero-sentinel case correctly (it tests `mantissa_ == 0` first) and is safer when comparing across the two normalization regimes. + +Unary negation (`operator-`) is implemented without calling `normalize()` — it simply flips the sign of the mantissa. This is safe because the negation of a normalized value is also normalized; the only edge case is negating zero, where `mantissa_ = 0` and the exponent sentinel remains unchanged. + +## `mulRatio`: Precision-Preserving Scaled Multiplication + +```cpp +IOUAmount mulRatio(IOUAmount const& amt, uint32_t num, uint32_t den, bool roundUp); +``` + +This free function computes `amt × num / den` while retaining more precision than the naïve approach of constructing intermediate `IOUAmount` values. The intermediate products are held in `boost::multiprecision::uint128_t`, which can accommodate the product of a 64-bit mantissa and a 32-bit numerator (up to ~96 bits), far beyond what `int64_t` alone could hold. The algorithm uses precomputed powers of ten to rescale the quotient and remainder so that the final mantissa fits back into the 64-bit range, then applies rounding at the bit that was lost. The `roundUp` flag follows directed rounding semantics: it rounds up for positive results and down (more negative) for negative results. + +## Conversion to `Number` + +`IOUAmount` provides a non-explicit conversion `operator Number()` that constructs a `Number` with `{mantissa_, exponent_}`. This is the bridge between the legacy type and the modern arithmetic layer. The conversion is implicit to allow `IOUAmount` values to participate naturally in `Number` arithmetic expressions — as noted in `Number.h`, "conversions to Number are implicit and conversions away from Number are explicit." The reverse conversion (from `Number` to `IOUAmount`) goes through the private static `fromNumber()` which calls `Number::normalizeToRange`, fitting the `Number` mantissa back into IOU's 10^15 range before storing it. \ No newline at end of file diff --git a/include/xrpl/protocol/Indexes.h.ai.json b/include/xrpl/protocol/Indexes.h.ai.json new file mode 100644 index 0000000000..06fe42ab48 --- /dev/null +++ b/include/xrpl/protocol/Indexes.h.ai.json @@ -0,0 +1,714 @@ +{ + "args": [ + { + "lineno": 23, + "name": "id" + }, + { + "lineno": 29, + "name": "key" + }, + { + "lineno": 45, + "name": "ledger" + }, + { + "lineno": 63, + "name": "b" + }, + { + "lineno": 75, + "name": "id0" + }, + { + "lineno": 75, + "name": "id1" + }, + { + "lineno": 75, + "name": "currency" + }, + { + "lineno": 79, + "name": "id" + }, + { + "lineno": 79, + "name": "issue" + }, + { + "lineno": 86, + "name": "seq" + }, + { + "lineno": 94, + "name": "k" + }, + { + "lineno": 94, + "name": "q" + }, + { + "lineno": 107, + "name": "ticketSeq" + }, + { + "lineno": 110, + "name": "ticketSeq" + }, + { + "lineno": 118, + "name": "account" + }, + { + "lineno": 128, + "name": "owner" + }, + { + "lineno": 128, + "name": "preauthorized" + }, + { + "lineno": 130, + "name": "authCreds" + }, + { + "lineno": 146, + "name": "root" + }, + { + "lineno": 146, + "name": "index" + }, + { + "lineno": 154, + "name": "src" + }, + { + "lineno": 157, + "name": "dst" + }, + { + "lineno": 185, + "name": "issue1" + }, + { + "lineno": 185, + "name": "issue2" + }, + { + "lineno": 190, + "name": "authorizedAccount" + }, + { + "lineno": 192, + "name": "bridge" + }, + { + "lineno": 192, + "name": "chainType" + }, + { + "lineno": 195, + "name": "seq" + }, + { + "lineno": 202, + "name": "documentID" + }, + { + "lineno": 204, + "name": "subject" + }, + { + "lineno": 204, + "name": "issuer" + }, + { + "lineno": 204, + "name": "credType" + }, + { + "lineno": 213, + "name": "issuanceID" + }, + { + "lineno": 216, + "name": "issuanceKey" + }, + { + "lineno": 219, + "name": "holder" + }, + { + "lineno": 222, + "name": "mptokenKey" + }, + { + "lineno": 230, + "name": "vaultKey" + }, + { + "lineno": 239, + "name": "loanBrokerID" + }, + { + "lineno": 239, + "name": "loanSeq" + }, + { + "lineno": 247, + "name": "domainID" + }, + { + "lineno": 251, + "name": "book" + }, + { + "lineno": 253, + "name": "uBase" + }, + { + "lineno": 256, + "name": "uBase" + }, + { + "lineno": 258, + "name": "uSequence" + }, + { + "lineno": 260, + "name": "ticketSeq" + }, + { + "lineno": 277, + "name": "sequence" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "SeqProxy" + }, + { + "args": [], + "lineno": 61, + "name": "book_t" + }, + { + "args": [], + "lineno": 98, + "name": "next_t" + }, + { + "args": [], + "lineno": 105, + "name": "ticket_t" + } + ], + "description": "This file provides keylet computation functions for the XRPL ledger, enabling type-safe and robust calculation of 256-bit locators (keylets) for various ledger entries such as accounts, offers, trust lines, NFTs, AMMs, and more. It also includes deprecated functions for direct index calculation and a descriptor for keylet functions used in tests.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Indexes.h", + "functions": [ + { + "args": [ + "AccountID const& id" + ], + "lineno": 23, + "name": "account" + }, + { + "args": [], + "lineno": 26, + "name": "amendments" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 29, + "name": "child" + }, + { + "args": [], + "lineno": 36, + "name": "skip" + }, + { + "args": [ + "LedgerIndex ledger" + ], + "lineno": 45, + "name": "skip" + }, + { + "args": [], + "lineno": 53, + "name": "fees" + }, + { + "args": [], + "lineno": 56, + "name": "negativeUNL" + }, + { + "args": [ + "Book const& b" + ], + "lineno": 63, + "name": "operator()" + }, + { + "args": [ + "AccountID const& id0", + "AccountID const& id1", + "Currency const& currency" + ], + "lineno": 75, + "name": "line" + }, + { + "args": [ + "AccountID const& id", + "Issue const& issue" + ], + "lineno": 79, + "name": "line" + }, + { + "args": [ + "AccountID const& id", + "std::uint32_t seq" + ], + "lineno": 86, + "name": "offer" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 89, + "name": "offer" + }, + { + "args": [ + "Keylet const& k", + "std::uint64_t q" + ], + "lineno": 94, + "name": "quality" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 100, + "name": "operator()" + }, + { + "args": [ + "AccountID const& id", + "std::uint32_t ticketSeq" + ], + "lineno": 107, + "name": "operator()" + }, + { + "args": [ + "AccountID const& id", + "SeqProxy ticketSeq" + ], + "lineno": 110, + "name": "operator()" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 113, + "name": "operator()" + }, + { + "args": [ + "AccountID const& account" + ], + "lineno": 118, + "name": "signers" + }, + { + "args": [ + "AccountID const& id", + "std::uint32_t seq" + ], + "lineno": 121, + "name": "check" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 124, + "name": "check" + }, + { + "args": [ + "AccountID const& owner", + "AccountID const& preauthorized" + ], + "lineno": 128, + "name": "depositPreauth" + }, + { + "args": [ + "AccountID const& owner", + "std::set> const& authCreds" + ], + "lineno": 130, + "name": "depositPreauth" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 134, + "name": "depositPreauth" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 140, + "name": "unchecked" + }, + { + "args": [ + "AccountID const& id" + ], + "lineno": 143, + "name": "ownerDir" + }, + { + "args": [ + "uint256 const& root", + "std::uint64_t index" + ], + "lineno": 146, + "name": "page" + }, + { + "args": [ + "Keylet const& root", + "std::uint64_t index" + ], + "lineno": 149, + "name": "page" + }, + { + "args": [ + "AccountID const& src", + "std::uint32_t seq" + ], + "lineno": 154, + "name": "escrow" + }, + { + "args": [ + "AccountID const& src", + "AccountID const& dst", + "std::uint32_t seq" + ], + "lineno": 157, + "name": "payChan" + }, + { + "args": [ + "AccountID const& owner" + ], + "lineno": 164, + "name": "nftpage_min" + }, + { + "args": [ + "AccountID const& owner" + ], + "lineno": 167, + "name": "nftpage_max" + }, + { + "args": [ + "Keylet const& k", + "uint256 const& token" + ], + "lineno": 169, + "name": "nftpage" + }, + { + "args": [ + "AccountID const& owner", + "std::uint32_t seq" + ], + "lineno": 173, + "name": "nftoffer" + }, + { + "args": [ + "uint256 const& offer" + ], + "lineno": 176, + "name": "nftoffer" + }, + { + "args": [ + "uint256 const& id" + ], + "lineno": 179, + "name": "nft_buys" + }, + { + "args": [ + "uint256 const& id" + ], + "lineno": 182, + "name": "nft_sells" + }, + { + "args": [ + "Asset const& issue1", + "Asset const& issue2" + ], + "lineno": 185, + "name": "amm" + }, + { + "args": [ + "uint256 const& amm" + ], + "lineno": 187, + "name": "amm" + }, + { + "args": [ + "AccountID const& account", + "AccountID const& authorizedAccount" + ], + "lineno": 190, + "name": "delegate" + }, + { + "args": [ + "STXChainBridge const& bridge", + "STXChainBridge::ChainType chainType" + ], + "lineno": 192, + "name": "bridge" + }, + { + "args": [ + "STXChainBridge const& bridge", + "std::uint64_t seq" + ], + "lineno": 195, + "name": "xChainClaimID" + }, + { + "args": [ + "STXChainBridge const& bridge", + "std::uint64_t seq" + ], + "lineno": 198, + "name": "xChainCreateAccountClaimID" + }, + { + "args": [ + "AccountID const& account" + ], + "lineno": 200, + "name": "did" + }, + { + "args": [ + "AccountID const& account", + "std::uint32_t const& documentID" + ], + "lineno": 202, + "name": "oracle" + }, + { + "args": [ + "AccountID const& subject", + "AccountID const& issuer", + "Slice const& credType" + ], + "lineno": 204, + "name": "credential" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 208, + "name": "credential" + }, + { + "args": [ + "std::uint32_t seq", + "AccountID const& issuer" + ], + "lineno": 211, + "name": "mptIssuance" + }, + { + "args": [ + "MPTID const& issuanceID" + ], + "lineno": 213, + "name": "mptIssuance" + }, + { + "args": [ + "uint256 const& issuanceKey" + ], + "lineno": 216, + "name": "mptIssuance" + }, + { + "args": [ + "MPTID const& issuanceID", + "AccountID const& holder" + ], + "lineno": 219, + "name": "mptoken" + }, + { + "args": [ + "uint256 const& mptokenKey" + ], + "lineno": 222, + "name": "mptoken" + }, + { + "args": [ + "uint256 const& issuanceKey", + "AccountID const& holder" + ], + "lineno": 225, + "name": "mptoken" + }, + { + "args": [ + "AccountID const& owner", + "std::uint32_t seq" + ], + "lineno": 227, + "name": "vault" + }, + { + "args": [ + "uint256 const& vaultKey" + ], + "lineno": 230, + "name": "vault" + }, + { + "args": [ + "AccountID const& owner", + "std::uint32_t seq" + ], + "lineno": 233, + "name": "loanbroker" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 236, + "name": "loanbroker" + }, + { + "args": [ + "uint256 const& loanBrokerID", + "std::uint32_t loanSeq" + ], + "lineno": 239, + "name": "loan" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 242, + "name": "loan" + }, + { + "args": [ + "AccountID const& account", + "std::uint32_t seq" + ], + "lineno": 245, + "name": "permissionedDomain" + }, + { + "args": [ + "uint256 const& domainID" + ], + "lineno": 247, + "name": "permissionedDomain" + }, + { + "args": [ + "Book const& book" + ], + "lineno": 251, + "name": "getBookBase" + }, + { + "args": [ + "uint256 const& uBase" + ], + "lineno": 253, + "name": "getQualityNext" + }, + { + "args": [ + "uint256 const& uBase" + ], + "lineno": 256, + "name": "getQuality" + }, + { + "args": [ + "AccountID const& account", + "std::uint32_t uSequence" + ], + "lineno": 258, + "name": "getTicketIndex" + }, + { + "args": [ + "AccountID const& account", + "SeqProxy ticketSeq" + ], + "lineno": 260, + "name": "getTicketIndex" + }, + { + "args": [ + "std::uint32_t sequence", + "AccountID const& account" + ], + "lineno": 277, + "name": "makeMptID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 22, + "name": "keylet" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Indexes.h.ai.md b/include/xrpl/protocol/Indexes.h.ai.md new file mode 100644 index 0000000000..d09b3c3866 --- /dev/null +++ b/include/xrpl/protocol/Indexes.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/protocol/Indexes.h` — Ledger Object Address Computation + +This header is the single authoritative source for computing the 256-bit addresses of every object in the XRPL ledger state. Any code that needs to read, write, or verify a ledger entry — whether it's an `AccountRoot`, an order-book directory, an NFT page, or a cross-chain bridge object — must go through the functions declared here. + +## The Keylet Abstraction + +The fundamental return type of this file is `Keylet`, a simple struct pairing a `uint256` key (the object's position in the ledger's SHAMap) with a `LedgerEntryType` enum value. Bundling the type tag with the key is the key design decision: it makes it impossible to look up an offer by using an account root's address and get back the wrong object. The `Keylet::check()` method validates the type against an actual `STLedgerEntry` at retrieval time, providing a runtime assertion against category errors that would otherwise silently corrupt ledger state. + +All functions are grouped under the `xrpl::keylet` namespace. Separate free functions below the namespace (`getBookBase()`, `getQualityNext()`, etc.) are explicitly marked deprecated — they predate the keylet system and expose raw `uint256` values without type information. + +## Tagged Hashing via `LedgerNameSpace` + +The implementation (in `Indexes.cpp`) uses a single internal template, `indexHash(LedgerNameSpace, args...)`, which calls `sha512Half()` with a two-byte namespace prefix prepended to all parameters. The `LedgerNameSpace` enum assigns every ledger object type a unique ASCII character (e.g., `ACCOUNT = 'a'`, `OFFER = 'o'`, `TRUST_LINE = 'r'`). This "tagged hashing" pattern ensures that an `AccountID` used to compute an account root key never collides with the same `AccountID` used to compute an owner directory key, even though both take a single `AccountID` as input. These namespace values are part of the consensus protocol — changing them would permanently relocate every affected object in the ledger, constituting a hard fork. + +## Fixed-Key Singletons + +Several singleton objects in the ledger (`amendments`, `fees`, `negativeUNL`, and the short-form `skip` list) have no parameters. Their keylets are returned as `Keylet const&` — a reference to a function-local static. This Meyers singleton approach means the hash is computed exactly once at first call and never again, and callers receive a stable reference rather than a by-value copy. + +## Symmetric Keys: Trust Lines and AMMs + +Both `keylet::line()` (for trust lines / `RippleState` objects) and `keylet::amm()` (for Automated Market Makers) use `std::minmax()` to sort their two account or asset parameters before hashing. A trust line is physically shared between Alice and Bob — there is one object, not two. Without canonical ordering, hashing `(Alice, Bob, USD)` and `(Bob, Alice, USD)` would produce different keys, and one of them would miss the object entirely. The `std::minmax()` canonicalization guarantees that both sides of any bilateral relationship produce the same ledger key. + +## Order-Book Quality Embedding + +The book and quality keylets use an unusual trick. `keylet::quality(k, q)` takes a directory-node keylet `k` and a 64-bit quality value `q`, then writes `q` in big-endian format into the last 8 bytes of the key by direct pointer manipulation (`((std::uint64_t*)x.end())[-1] = boost::endian::native_to_big(q)`). Because `uint256` keys are compared as big-endian integers in the SHAMap, this embeds the exchange rate directly into the sort key. All offers at adjacent quality levels land at adjacent 256-bit addresses, enabling O(1) iteration from one price level to the next without any secondary index. `keylet::next_t::operator()` adds the constant `0x...0001_0000_0000_0000_0000` to step to the directory for the next quality level. + +## NFT Pages: Composite Rather Than Hashed Keys + +NFT page keylets (`nftpage_min`, `nftpage_max`, `nftpage`) are conspicuously absent from `indexHash()`. Instead, they assemble a `uint256` by placing the 160-bit `AccountID` in the high bytes and a 96-bit token boundary mask in the low bytes, using `memcpy` and bitwise masking. This design is intentional: the XRPL NFT page structure is a linked list of pages where each page's key encodes its owner and the range of NFT IDs it can hold. The key must compare correctly relative to other pages' keys for range navigation, something that a hash function would destroy. `nftpage()` computes an exact key for the page that would hold a given token by masking the token ID against `nft::pageMask` and ORing it onto the owner prefix. + +## Multi-Credential Deposit Preauthorization + +The credential-set overload of `keylet::depositPreauth()` handles the case where a deposit is preauthorized for a set of credentials rather than a single account. It takes an already-sorted `std::set>`, hashes each `(AccountID, credentialType)` pair individually into a `uint256`, then hashes the resulting vector under the `DEPOSIT_PREAUTH_CREDENTIALS` namespace — distinct from the simple account-to-account `DEPOSIT_PREAUTH` namespace. This ensures there is no possible key collision between the two preauthorization modes. + +## MPT Composite Identifiers + +Multi-Purpose Tokens introduce `MPTID` — a 192-bit composite value created by `makeMptID()` that packs a big-endian 32-bit sequence number followed by the 160-bit issuer `AccountID`. The `mptIssuance()` family then hashes this raw ID under the `MPTOKEN_ISSUANCE` namespace. Individual token holdings (`mptoken()`) are keyed by hashing the issuance's ledger key together with the holder's `AccountID` under `MPTOKEN`. This two-level scheme means token balances are naturally grouped under their issuance in the hash space. + +## Testing Infrastructure: `keyletDesc` and `directAccountKeylets` + +At the bottom of the header sit `keyletDesc` and the `directAccountKeylets` array. These are not protocol machinery — they exist purely to drive invariant tests in `Invariants_test.cpp`. `keyletDesc` holds a `std::function` wrapping one keylet factory, the `Json::StaticString` name of the expected ledger entry type, and a boolean flag controlling test inclusion. The `directAccountKeylets` array enumerates every keylet function that accepts a single `AccountID` argument, including `nftpage_min` (noted as normally uncreateable but tested anyway for invariant coverage). New single-`AccountID` keylets should be added to this array so that invariant tests automatically exercise them. \ No newline at end of file diff --git a/include/xrpl/protocol/InnerObjectFormats.h.ai.json b/include/xrpl/protocol/InnerObjectFormats.h.ai.json new file mode 100644 index 0000000000..e31df2a4ab --- /dev/null +++ b/include/xrpl/protocol/InnerObjectFormats.h.ai.json @@ -0,0 +1,33 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "InnerObjectFormats" + } + ], + "description": "Defines the InnerObjectFormats class, which manages the list of known inner object formats in the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/InnerObjectFormats.h", + "functions": [ + { + "args": [], + "lineno": 16, + "name": "getInstance" + }, + { + "args": [ + "sField" + ], + "lineno": 19, + "name": "findSOTemplateBySField" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/InnerObjectFormats.h.ai.md b/include/xrpl/protocol/InnerObjectFormats.h.ai.md new file mode 100644 index 0000000000..9d433b110a --- /dev/null +++ b/include/xrpl/protocol/InnerObjectFormats.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/protocol/InnerObjectFormats.h` + +## Role in the System + +`InnerObjectFormats` is the central registry of structural schemas for all *inner object* types in the XRPL protocol. Where `TxFormats` catalogues top-level transaction layouts, `InnerObjectFormats` does the same for the serialized sub-objects that appear nested inside transactions and ledger entries — things like `sfSigner`, `sfSignerEntry`, `sfNFToken`, `sfAuctionSlot`, `sfPriceData`, and a dozen others. Without this registry, the serialization layer would have no authoritative way to know which fields are required, optional, or default-suppressed inside each kind of inner object. + +## Design: CRTP Singleton Over `KnownFormats` + +The class inherits from `KnownFormats`, the CRTP template that provides generic format-management infrastructure: a `std::forward_list` for stable storage, and two `boost::container::flat_map` indices for O(log n) lookup by name and by key. The key type is `int` — specifically the numeric field code returned by `SField::getCode()` — rather than the `TxType` enum used by `TxFormats`. This is intentional: inner objects are already identified by their `SField` descriptors in wire format, so reusing the field code as the registry key avoids a separate enumeration. + +The constructor is private. All 17 format entries (as of the current codebase) are registered in the constructor body via `KnownFormats::add()`, which simultaneously inserts each `Item` into both lookup maps. The `add()` call takes vectors of `SOElement` (field + `SOEStyle` pairs like `soeREQUIRED`, `soeOPTIONAL`, or `soeDEFAULT`) that together compose an `SOTemplate`. Because `std::forward_list` is node-based, the addresses of stored `Item` objects remain stable after insertion, so the flat maps can hold raw pointers safely. + +`getInstance()` returns a `const&` to the lazily-initialized, function-local static — the standard Meyer's singleton pattern. The object is effectively immutable after construction; no public mutating interface is exposed. + +## The Primary Lookup: `findSOTemplateBySField` + +```cpp +SOTemplate const* findSOTemplateBySField(SField const& sField) const; +``` + +This is the only method added on top of the base-class API. It accepts an `SField` reference, reads its integer code via `getCode()`, delegates to `KnownFormats::findByType()`, and returns a pointer to the corresponding `SOTemplate`, or `nullptr` if the field isn't a known inner object type. Returning a raw (nullable) pointer rather than a `std::optional>` is consistent with the rest of the XRP protocol codebase and avoids unnecessary wrapping overhead for what is essentially a table lookup on a hot serialization path. + +## How the Registry Is Consumed + +Three call sites illustrate the registry's role: + +**`STObject::makeInnerObject()`** in `STObject.cpp` calls `findSOTemplateBySField` to attach the correct `SOTemplate` to a freshly created inner object, which causes `STObject::set(SOTemplate const&)` to pre-populate the object's fields with the correct default/non-present sentinels. Notably, this only applies when the network amendments `fixInnerObjTemplate` (for AMM objects `sfVoteEntry` and `sfAuctionSlot`) or `fixInnerObjTemplate2` (for all others) are enabled. This amendment gate reveals a historical detail: the registry predated its enforcement on the network, so the data model and its validation were shipped separately. + +**`STObject::applyTemplateFromSField()`** calls `findSOTemplateBySField` to retroactively apply a template to an already-deserialized `STObject`. This is used when validating objects coming off the wire: unknown or disallowed fields are rejected, required fields are checked for presence, and `soeDEFAULT` fields are rejected if they carry their default value. + +**Transaction handlers** (e.g., `NFTokenMint.cpp`, `OracleSet.cpp`, `TransactionSign.cpp`) call `findSOTemplateBySField` directly to obtain the schema for a specific inner object type before constructing or validating it, bypassing the general `STObject` machinery when targeted access is needed. + +## Relationship to `SOTemplate` and `SOEStyle` + +Each registered entry wraps an `SOTemplate`, which pairs field codes with `SOEStyle` constraints: `soeREQUIRED` means the field must be present, `soeOPTIONAL` means it may be absent or present with a non-default value, and `soeDEFAULT` means it may be absent but — if present — must carry a non-default value. This distinction matters for canonical serialization: a field serialized with its default value wastes wire space and is treated as a protocol violation for `soeDEFAULT` fields. + +## Invariants and Safety + +The private constructor guarantees that the registry is complete and consistent at the point `getInstance()` first returns. The `KnownFormats::add()` implementation guards against duplicate key registration with a `LogicError` call, so double-registering the same inner object type would abort the process immediately. Because `getInstance()` returns `const&`, callers cannot mutate the registry after initialization, and thread safety for reads-after-construction is guaranteed by the C++11 static initialization rules that `function-local static` relies on. \ No newline at end of file diff --git a/include/xrpl/protocol/Issue.h.ai.json b/include/xrpl/protocol/Issue.h.ai.json new file mode 100644 index 0000000000..c5a4b9d22e --- /dev/null +++ b/include/xrpl/protocol/Issue.h.ai.json @@ -0,0 +1,111 @@ +{ + "args": [ + { + "lineno": 15, + "name": "c" + }, + { + "lineno": 15, + "name": "a" + }, + { + "lineno": 27, + "name": "jv" + } + ], + "classes": [ + { + "args": [], + "lineno": 9, + "name": "Issue" + } + ], + "description": "Defines the Issue class representing a currency issued by an account in the XRPL protocol, along with related functions and operators for comparison, hashing, and JSON conversion.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Issue.h", + "functions": [ + { + "args": [ + "ac" + ], + "lineno": 38, + "name": "isConsistent" + }, + { + "args": [ + "ac" + ], + "lineno": 40, + "name": "to_string" + }, + { + "args": [ + "is" + ], + "lineno": 42, + "name": "to_json" + }, + { + "args": [ + "v" + ], + "lineno": 44, + "name": "issueFromJson" + }, + { + "args": [ + "os", + "x" + ], + "lineno": 46, + "name": "operator<<" + }, + { + "args": [ + "h", + "r" + ], + "lineno": 49, + "name": "hash_append" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 58, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 65, + "name": "operator<=>" + }, + { + "args": [], + "lineno": 78, + "name": "xrpIssue" + }, + { + "args": [], + "lineno": 84, + "name": "noIssue" + }, + { + "args": [ + "issue" + ], + "lineno": 90, + "name": "isXRP" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Issue.h.ai.md b/include/xrpl/protocol/Issue.h.ai.md new file mode 100644 index 0000000000..f66d939235 --- /dev/null +++ b/include/xrpl/protocol/Issue.h.ai.md @@ -0,0 +1,63 @@ +# `Issue.h` — Currency-Issuer Pair in the XRPL Type System + +## Role and Purpose + +`Issue` is one of the foundational value types in the XRPL protocol layer. It models a **specific currency as issued by a specific account** — the minimal tuple needed to identify any IOU asset on the ledger. A USD amount issued by Bitstamp and an XRP amount are both expressible as `Issue` instances, but they differ in whether the issuer field carries meaning. + +This type sits below `Asset` (which generalizes over XRP, IOU, and MPT) and above raw `Currency`/`AccountID` pairs. It is a peer to `MPTIssue` and feeds into `Book` (an ordered pair of issues defining an order book). Understanding `Issue` is prerequisite to understanding how offers, trust lines, and AMM pools are keyed and compared throughout the codebase. + +## Class Design + +`Issue` is a simple aggregate of two public fields — `currency` and `account` — with no encapsulation beyond accessor convenience. This is an intentional design choice: `Issue` is a **value type** used pervasively for keying and comparison, and heavy encapsulation would add friction without benefit. The fields are public and default-constructible so the type works naturally with structured bindings, aggregate initialization, and serialization routines. + +`Currency` is a 160-bit strong typedef (`base_uint<160, CurrencyTag>`) and `AccountID` is likewise a 160-bit strong typedef (`base_uint<160, AccountIDTag>`). The distinct tag types prevent silent cross-assignment between them at compile time, a deliberate defensive pattern common throughout `UintTypes.h`. + +The `getIssuer()` accessor exists primarily for generic code that operates across `Issue` and `MPTIssue` (see `Concepts.h`), providing a uniform interface without requiring virtual dispatch or full abstraction. + +## The XRP Special Case in Equality and Ordering + +The most architecturally significant detail in this file is how XRP is handled in `operator==` and `operator<=>`: + +```cpp +[[nodiscard]] inline constexpr bool +operator==(Issue const& lhs, Issue const& rhs) +{ + return (lhs.currency == rhs.currency) && + (isXRP(lhs.currency) || lhs.account == rhs.account); +} +``` + +When both sides are XRP (identified by `xrpCurrency()`, which is the all-zero 160-bit value), the `account` field is **ignored**. This reflects a protocol invariant: XRP has no issuer, so any `Issue` with `xrpCurrency()` is semantically identical regardless of what happens to be stored in its `account` field. + +The same logic applies in `operator<=>`: after currencies compare equal, if the currency is XRP, `std::weak_ordering::equivalent` is returned immediately without inspecting `account`. This ensures XRP issues form a single equivalence class in any ordered container, regardless of minor inconsistencies in how legacy code populated the account field. + +The `isConsistent()` function (implemented in `Issue.cpp`) enforces the tighter invariant — `isXRP(ac.currency) == isXRP(ac.account)` — meaning a well-formed XRP issue must carry `xrpAccount()` (zero) and a well-formed IOU issue must carry a non-zero account. The comparison operators are deliberately more lenient than `isConsistent()`, providing defensive handling for partially-constructed or legacy data. + +## `native()` and `integral()` + +`native()` returns `true` when `*this == xrpIssue()`, and `integral()` simply delegates to `native()`. On the `Issue` type, these are always equivalent — the distinction matters more for the `Asset` hierarchy, where `MPTIssue` overrides `integral()` separately. These predicates surface throughout the codebase to branch on XRP-vs-IOU semantics in amount arithmetic and validation. + +## Text and JSON Serialization + +`getText()` and `to_string()` serve slightly different purposes. `getText()` produces a human-readable representation used for logging and debugging; for IOUs it formats as `"CURRENCY/ISSUER"` with special sentinel strings `"0"` for `xrpAccount()` and `"1"` for `noAccount()`, enabling detection of structurally inconsistent issues. `to_string()` by contrast uses `"ACCOUNT/CURRENCY"` order and simply emits the account's base-58 string, aligning with the external API format. + +`to_json()` and `setJson()` exist as two distinct serialization interfaces for different call-site patterns: `to_json` constructs and returns a new `Json::Value` suitable for standalone serialization, while `setJson` mutates an existing object in-place for incremental JSON construction. Both omit the `issuer` field for XRP — the JSON representation of XRP issue is `{"currency": "XRP"}` with no issuer key. + +`issueFromJson()` is the defensive parser: it validates that the input is a JSON object, rejects the `mpt_issuance_id` key (preventing accidental parsing of an `MPTIssue` as an `Issue`), validates that the currency string is neither `badCurrency()` nor `noCurrency()`, and enforces that XRP issues have no issuer field while IOU issues have a valid base-58 account. + +## Hashing + +`hash_append` feeds both `currency` and `account` into the hasher unconditionally, without applying the XRP special case that `operator==` uses. This is safe in practice because `isConsistent()` should ensure XRP issues always carry `xrpAccount()`, so consistent data produces consistent hashes. If an inconsistent XRP issue were hashed and then compared for equality, a hash collision could theoretically fail to resolve correctly — but the protocol ensures consistent data through validation at ingestion points. + +## Sentinel Values + +Two static singletons serve as well-known markers: + +- `xrpIssue()` — the canonical XRP asset, initialized once with `xrpCurrency()` and `xrpAccount()` (both all-zero 160-bit values) +- `noIssue()` — a null/empty marker using `noCurrency()` and `noAccount()`, used in contexts where an absent issue must be represented without `std::optional` + +Both are returned by `const&` to function-local statics, which is thread-safe under C++11's guaranteed initialization semantics and avoids repeated heap allocation. + +## Relationship to Siblings + +`Issue` is consumed directly by `Book` (a bid/ask pair of `Issue` values), generalized by `Asset` (which wraps `Issue`, `MPTIssue`, or XRP natively in a variant), and used extensively in amount arithmetic headers for type coercions. The `Concepts.h` file defines constraints like `IssueType` that `Issue` satisfies alongside `MPTIssue`, enabling generic algorithms to operate uniformly over both token types without virtual dispatch. The `isXRP(Issue const&)` free function defined here is a thin wrapper over `issue.native()`, providing a consistent naming convention used across the entire codebase for XRP detection at all abstraction levels. \ No newline at end of file diff --git a/include/xrpl/protocol/KeyType.h.ai.json b/include/xrpl/protocol/KeyType.h.ai.json new file mode 100644 index 0000000000..da77b9e34b --- /dev/null +++ b/include/xrpl/protocol/KeyType.h.ai.json @@ -0,0 +1,54 @@ +{ + "args": [ + { + "lineno": 12, + "name": "s" + }, + { + "lineno": 23, + "name": "type" + }, + { + "lineno": 34, + "name": "s" + }, + { + "lineno": 34, + "name": "type" + } + ], + "classes": [], + "description": "Defines the KeyType enum for cryptographic key types and provides utility functions for converting between KeyType and string representations in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/KeyType.h", + "functions": [ + { + "args": [ + "s" + ], + "lineno": 11, + "name": "keyTypeFromString" + }, + { + "args": [ + "type" + ], + "lineno": 22, + "name": "to_string" + }, + { + "args": [ + "s", + "type" + ], + "lineno": 33, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/KeyType.h.ai.md b/include/xrpl/protocol/KeyType.h.ai.md new file mode 100644 index 0000000000..1bf896318d --- /dev/null +++ b/include/xrpl/protocol/KeyType.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/protocol/KeyType.h` + +## Role in the System + +This header defines the foundational discriminant type for XRPL's dual-algorithm cryptographic infrastructure. The XRPL supports two independent signature schemes — the Bitcoin-lineage `secp256k1` elliptic curve and the modern `ed25519` Edwards curve — and `KeyType` is the enum that flows through every layer of key generation, signing, and verification to select between them. It is a deliberately minimal header: a single enum class and three inline conversion utilities, included by virtually every cryptographic interface in the protocol. + +## The Enum Design + +```cpp +enum class KeyType { + secp256k1 = 0, + ed25519 = 1, +}; +``` + +The explicit integer assignments (`0` and `1`) are not accidental. Although `KeyType` itself is not serialized directly to the wire, the choice of algorithm manifests 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`. The numeric values of the enum provide stable identifiers for any internal storage or configuration that maps an integer to a key type. + +Using `enum class` rather than a plain `enum` enforces scope discipline: callers must write `KeyType::secp256k1` rather than a bare `secp256k1`, preventing symbol-namespace pollution across a large codebase where "secp256k1" would otherwise shadow or collide with library-level identifiers. + +## Utility Functions + +`keyTypeFromString()` returns `std::optional` rather than throwing on an unrecognised string. This is deliberate: it is called at configuration-parse time and at RPC-request boundaries where user-supplied strings are inherently untrusted. Returning an empty optional instead of raising an exception allows callers to compose the parse result with their own error-reporting logic without forcing exception handling into tight validation paths. + +`to_string()` converts a `KeyType` back to a canonical C-string literal. It includes an explicit `"INVALID"` return path for values that match neither known enumerator — a defensive measure relevant because C++ allows any integer to be cast to an `enum class`, meaning a corrupt or maliciously crafted value can produce a `KeyType` that doesn't correspond to either variant. Returning `"INVALID"` instead of `nullptr` or undefined behaviour makes logging and diagnostics safe in those edge cases. + +The stream insertion operator is templated on `Stream` rather than fixed to `std::ostream`: + +```cpp +template +inline Stream& operator<<(Stream& s, KeyType type) { + return s << to_string(type); +} +``` + +This generality allows the operator to work with Beast's logging streams, test harness formatters, and any other stream-like type the codebase employs, without coupling this header to a concrete stream hierarchy. + +## Relationships to Sibling Headers + +`KeyType` is a first-class parameter across the entire key-management surface. In `SecretKey.h`, functions such as `generateSecretKey(KeyType, Seed const&)`, `derivePublicKey(KeyType, SecretKey const&)`, `generateKeyPair(KeyType, Seed const&)`, and `randomKeyPair(KeyType)` accept it to select the appropriate key-derivation algorithm. The XRPL's secp256k1 derivation path is custom (seed → generator → key pair at ordinal 0), while ed25519 uses a simpler direct derivation, so the `KeyType` parameter at those sites does real algorithmic switching, not just labeling. + +In `PublicKey.h`, the inverse direction is provided: `publicKeyType(Slice const&)` reads the lead byte(s) of a serialized public key and returns `std::optional` — so the type can be recovered from the wire format without any additional metadata. This self-describing encoding is why `KeyType` need not be stored separately alongside a public key. The function returning `std::nullopt` for an unrecognised lead byte mirrors the same safe-optional pattern as `keyTypeFromString()`. + +In `Sign.h`, the `sign(STObject&, HashPrefix const&, KeyType, SecretKey const&, ...)` overload takes `KeyType` explicitly because it must derive the public key internally to write into the object's signature field — an operation that requires knowing which algorithm the secret key belongs to. + +Together these headers form a coherent cryptographic key API: `KeyType.h` supplies the shared discriminant, `SecretKey.h` generates and uses keys, `PublicKey.h` verifies and identifies them, and `Sign.h` ties both together for serialized ledger objects. \ No newline at end of file diff --git a/include/xrpl/protocol/Keylet.h.ai.json b/include/xrpl/protocol/Keylet.h.ai.json new file mode 100644 index 0000000000..057e9ba337 --- /dev/null +++ b/include/xrpl/protocol/Keylet.h.ai.json @@ -0,0 +1,39 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "LedgerEntryType type_", + "uint256 const& key_" + ], + "lineno": 13, + "name": "Keylet" + } + ], + "description": "Defines the Keylet struct, which represents a pair of a SHAMap key and a LedgerEntryType, used to identify both a key in the state map and its ledger entry type in the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Keylet.h", + "functions": [ + { + "args": [ + "LedgerEntryType type_", + "uint256 const& key_" + ], + "lineno": 18, + "name": "Keylet" + }, + { + "args": [ + "STLedgerEntry const&" + ], + "lineno": 24, + "name": "check" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Keylet.h.ai.md b/include/xrpl/protocol/Keylet.h.ai.md new file mode 100644 index 0000000000..246b5bd21f --- /dev/null +++ b/include/xrpl/protocol/Keylet.h.ai.md @@ -0,0 +1,45 @@ +# `Keylet.h` — Type-Safe Ledger Object Locator + +## Role in the System + +The XRPL ledger state is stored as a SHAMap — an ordered, hash-mapped tree where every entry is addressed by a raw 256-bit key. Accessing an entry by its raw `uint256` alone is type-unsafe: nothing prevents the caller from treating an `AccountRoot` object as an `Offer`, or from confusing the key of one object type with the key of another. `Keylet` exists to close that gap. + +`Keylet` is a deliberately minimal struct — just a `uint256 key` and a `LedgerEntryType type` — that bundles together the SHAMap locator of a ledger object with the type that object is expected to have. The name is a portmanteau of "key" and "LET" (LedgerEntryType), coined explicitly to reflect this dual purpose. + +## Design Rationale + +The pairing of key and type is meaningful at every stage of ledger access. When code performs a lookup it constructs a `Keylet` (typically via one of the factory functions in the `keylet::` namespace defined in `Indexes.h`), then passes it to the ledger view. The view retrieves the raw entry at `key` and, if it finds one, can invoke `check()` to confirm that the entry's type matches the `Keylet`'s type before returning it. This makes it structurally impossible to accidentally retrieve the wrong kind of object through a correctly typed lookup path. + +The alternative — passing a raw `uint256` and performing type checks at call sites — is more error-prone. Because different ledger object types use different key derivation formulas (account roots hash from account IDs, offers hash from account ID plus sequence, etc.), keys derived for one type are normally not valid for another, but this is an implicit convention rather than an enforced invariant. `Keylet` makes the convention explicit and checkable. + +## `check()` — Type Validation Against a Live Entry + +The implementation of `check()` in `Keylet.cpp` reveals the three-case semantics: + +```cpp +bool Keylet::check(STLedgerEntry const& sle) const { + XRPL_ASSERT(sle.getType() != ltANY && sle.getType() != ltCHILD, ...); + + if (type == ltANY) return true; + if (type == ltCHILD) return sle.getType() != ltDIR_NODE; + return sle.getType() == type && sle.key() == key; +} +``` + +- **`ltANY` keylet**: Acts as a wildcard — any live entry passes. Used in contexts where type enforcement is deliberately relaxed (e.g., raw key lookups via `keylet::unchecked`). +- **`ltCHILD` keylet**: Matches any entry that is *not* a directory node (`ltDIR_NODE`). This reflects the semantics of owner directory children, which can be any ownable object type. +- **Concrete type**: Requires both an exact type match and an exact key match on the SLE. This is the common case and gives the strongest safety guarantee. + +Notice that the assertion guards against the reverse confusion: a live `STLedgerEntry` should never carry `ltANY` or `ltCHILD` as its stored type — those are query-side sentinels, not real on-ledger types. The assert fires in debug builds if such a corrupted entry is ever passed. + +## Relationship to `Indexes.h` + +`Keylet.h` defines the *type*; `Indexes.h` provides the *factory functions* that produce correct, type-specific `Keylet` values. The `keylet::` namespace contains functions like `keylet::account(AccountID const&)`, `keylet::offer(AccountID const&, std::uint32_t seq)`, `keylet::amendments()`, and many others. Each function encapsulates the correct SHA-512Half derivation for its object type and returns a `Keylet` with the matching `LedgerEntryType`. This means callers never compute raw keys by hand — they call the appropriate factory, and the type annotation rides along automatically. + +## LedgerEntryType Stability Requirement + +`LedgerEntryType` values are stored inside on-ledger objects and are part of the consensus protocol. Changing a type's numeric value would cause nodes running different software versions to compute different hashes for the same ledger state, triggering a hard fork. This constraint is called out explicitly in `LedgerFormats.h`. `Keylet`'s reliance on `LedgerEntryType` means it participates in this stability contract — any code that constructs a `Keylet` with a given type is implicitly depending on that type's numeric identity remaining fixed. + +## Summary + +`Keylet` is a small but architecturally important type. Its simplicity is intentional: it carries exactly two fields and exposes exactly one non-trivial method. The complexity lives in `Indexes.h`'s factory layer and in the ledger view's lookup plumbing. `Keylet` itself is the stable, shared contract between those two halves — the structure that lets the lookup layer know what it should find before it opens the SHAMap, and verify that what it found matches expectations. \ No newline at end of file diff --git a/include/xrpl/protocol/KnownFormats.h.ai.json b/include/xrpl/protocol/KnownFormats.h.ai.json new file mode 100644 index 0000000000..09f2119bc8 --- /dev/null +++ b/include/xrpl/protocol/KnownFormats.h.ai.json @@ -0,0 +1,104 @@ +{ + "args": [ + { + "lineno": 36, + "name": "name" + }, + { + "lineno": 42, + "name": "type" + }, + { + "lineno": 19, + "name": "uniqueFields" + }, + { + "lineno": 19, + "name": "commonFields" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "KnownFormats" + }, + { + "args": [ + "name", + "type", + "uniqueFields", + "commonFields" + ], + "lineno": 19, + "name": "Item" + } + ], + "description": "Defines a generic template class KnownFormats for managing a list of known formats, each identified by a key type and associated with a name and SOTemplate. Provides methods for adding, finding, and iterating over formats.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/KnownFormats.h", + "functions": [ + { + "args": [], + "lineno": 36, + "name": "getName" + }, + { + "args": [], + "lineno": 42, + "name": "getType" + }, + { + "args": [], + "lineno": 47, + "name": "getSOTemplate" + }, + { + "args": [ + "name" + ], + "lineno": 69, + "name": "findTypeByName" + }, + { + "args": [ + "type" + ], + "lineno": 81, + "name": "findByType" + }, + { + "args": [], + "lineno": 90, + "name": "begin" + }, + { + "args": [], + "lineno": 95, + "name": "end" + }, + { + "args": [ + "name" + ], + "lineno": 102, + "name": "findByName" + }, + { + "args": [ + "name", + "type", + "uniqueFields", + "commonFields" + ], + "lineno": 111, + "name": "add" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/KnownFormats.h.ai.md b/include/xrpl/protocol/KnownFormats.h.ai.md new file mode 100644 index 0000000000..3b5e82b0f8 --- /dev/null +++ b/include/xrpl/protocol/KnownFormats.h.ai.md @@ -0,0 +1,42 @@ +# `KnownFormats.h` — Protocol Format Registry Template + +## Purpose and Role + +`KnownFormats` is the central abstraction for managing XRPL's **protocol format registries** — the compile-time-registered catalogs that define which fields are valid, required, or optional for every transaction type, ledger object, and inner object on the network. It provides a type-safe, name-indexed, and type-indexed lookup structure that the serialization and validation layers consult constantly at runtime. + +Three concrete registries inherit from it: +- `TxFormats : public KnownFormats` — one `Item` per transaction type (`ttPayment`, `ttOfferCreate`, etc.) +- `LedgerFormats : public KnownFormats` — one `Item` per on-ledger object type +- `InnerObjectFormats : public KnownFormats` — one `Item` per inner STObject structure + +## The `Item` Inner Class + +Each registered format is an `Item`, bundling three things: a human-readable name string, a `KeyType` discriminant that maps to the wire-protocol integer, and an `SOTemplate` that codifies the field schema. The `SOTemplate` in turn holds `SOElement` entries, each pairing an `SField` reference with a style tag (`soeREQUIRED`, `soeOPTIONAL`, or `soeDEFAULT`) and, for amount/issue fields, an MPT-support annotation. + +The `Item` constructor enforces a `static_assert` that `KeyType` is either integral or an enum — a deliberate compile-time gate that prevents misuse of the template with arbitrary types. Since these keys are embedded in signed transactions and ledger objects (meaning they are part of the binary protocol), the constraint ensures the key is always something that maps directly to an integer wire value. + +## Storage Design: Why `std::forward_list` + +The central design decision is using `std::forward_list` as the owning container, with `flat_map` indices pointing into it. The code makes this explicit in a comment: the requirement is that each `Item`'s **memory address must be stable** after insertion. `std::vector` and `std::deque` can reallocate and invalidate pointers; `std::forward_list` is a node-based structure that never moves existing elements. + +The two `boost::container::flat_map` instances — `names_` and `types_` — store raw `Item const*` pointers, so pointer stability is not optional. `flat_map` is chosen over `std::map` because these registries are built once at startup and then read-only; the sorted contiguous storage of `flat_map` gives better cache behaviour for the frequent lookups that happen during transaction processing. + +`emplace_front` is used when adding items, which means items are stored in reverse-registration order in the list — but since the maps are the only lookup paths, iteration order over the list is irrelevant for correctness. The `begin()/end()` pair on the class itself, which exposes `forward_list` iteration, is explicitly annotated as being for testing purposes only. + +## CRTP for Error Messaging + +The second template parameter `Derived` follows the Curiously Recurring Template Pattern. Its sole runtime use is in the constructor, where `beast::type_name()` captures the concrete subclass name into `name_`. This string is then embedded in error messages from `findTypeByName`, making diagnostics like `"TxFormats: Unknown format name 'BadName'"` possible without any virtual dispatch overhead. + +## Lookup Asymmetry: Throw vs. nullptr + +`findTypeByName()` throws `std::runtime_error` when given an unknown name, while `findByType()` returns `nullptr`. This is a deliberate asymmetry. Name lookups occur when parsing externally supplied strings — JSON RPC calls, config files, user input — where a missing name is a recoverable application-level error worth propagating as an exception. Type lookups occur in internal code paths where the caller can meaningfully handle a miss by checking `nullptr`, and where the null-check pattern is idiomatic C++. + +The `findByName()` overload is `protected`, preventing direct callers outside the class hierarchy from bypassing the exception-throwing public interface. + +## Duplicate Registration Guard + +`add()` calls `findByType()` before inserting and invokes `LogicError()` if the key already exists. `LogicError` is a hard process termination — not an exception, but a fatal abort. This is appropriate here: a duplicate format registration is a programming error that cannot be recovered from, and it is caught at startup when the singleton constructors run, not at request-handling time. + +## Relationship to `SOTemplate` and `SField` + +`KnownFormats` sits directly above `SOTemplate` in the dependency chain. It does not interpret field semantics itself — it simply owns the templates and provides indexed access. The `SOTemplate` class merges unique fields (specific to one format) and common fields (shared across all formats in the registry, like transaction metadata fields) into a single flat element list with an index vector for O(1) field lookup by `SField` number. This split between unique and common fields in the `Item` constructor maps cleanly to the `TxFormats` and `LedgerFormats` APIs, which both expose `getCommonFields()` as a static method. \ No newline at end of file diff --git a/include/xrpl/protocol/LedgerFormats.h.ai.json b/include/xrpl/protocol/LedgerFormats.h.ai.json new file mode 100644 index 0000000000..65546b273c --- /dev/null +++ b/include/xrpl/protocol/LedgerFormats.h.ai.json @@ -0,0 +1,86 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 222, + "name": "LedgerFormats" + } + ], + "description": "Defines identifiers and flags for on-ledger objects in the XRPL protocol, including enums for ledger entry types and flags, macros for generating flag maps and accessors, and a LedgerFormats class for managing known ledger entry formats.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/LedgerFormats.h", + "functions": [ + { + "args": [], + "lineno": 120, + "name": "getAccountRootFlags" + }, + { + "args": [], + "lineno": 135, + "name": "getOfferFlags" + }, + { + "args": [], + "lineno": 141, + "name": "getRippleStateFlags" + }, + { + "args": [], + "lineno": 155, + "name": "getSignerListFlags" + }, + { + "args": [], + "lineno": 158, + "name": "getDirNodeFlags" + }, + { + "args": [], + "lineno": 161, + "name": "getNFTokenOfferFlags" + }, + { + "args": [], + "lineno": 164, + "name": "getMPTokenIssuanceFlags" + }, + { + "args": [], + "lineno": 172, + "name": "getMPTokenIssuanceMutableFlags" + }, + { + "args": [], + "lineno": 180, + "name": "getMPTokenFlags" + }, + { + "args": [], + "lineno": 185, + "name": "getCredentialFlags" + }, + { + "args": [], + "lineno": 188, + "name": "getVaultFlags" + }, + { + "args": [], + "lineno": 191, + "name": "getLoanFlags" + }, + { + "args": [], + "lineno": 197, + "name": "getAllLedgerFlags" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/LedgerFormats.h.ai.md b/include/xrpl/protocol/LedgerFormats.h.ai.md new file mode 100644 index 0000000000..96c361e07f --- /dev/null +++ b/include/xrpl/protocol/LedgerFormats.h.ai.md @@ -0,0 +1,50 @@ +# `include/xrpl/protocol/LedgerFormats.h` + +This header is the authoritative registry for every object type that can live in the XRP Ledger. It simultaneously defines three separate, tightly-coupled things: the numeric type identifiers stored inside ledger objects, the flag bitmasks that modify their behavior, and the `LedgerFormats` singleton that knows the serialized field layout of every object type. All three are protocol-level constants — changing any of them without careful amendment machinery would cause a hard fork. + +## `LedgerEntryType` — Object Type Identifiers + +The `LedgerEntryType` enum (`uint16_t`) assigns a stable integer ID to each on-ledger object type. These IDs are embedded in every serialized ledger object and are used during ledger iteration to determine an object's type and to verify that a hash lookup returned the expected kind of object. + +The concrete type values are generated via the `ledger_entries.macro` X-macro, which is included twice in the codebase with a different `LEDGER_ENTRY` definition each time. Here the macro expands to `tag = value,` to populate the enum. This single-source-of-truth pattern ensures the same set of (tag, value, name, fields) tuples is used consistently for enum membership, format registration in the constructor, and auto-generated API output. + +Beyond the macro-generated members, two sentinel pseudo-types appear manually: + +- `ltANY = 0` — used in keylet lookups where any object type is acceptable. Objects cannot be created with this type. +- `ltCHILD = 0x1CD2` — used in keylets where the object must not be a directory node but the precise type is irrelevant. + +Three legacy entries (`ltNICKNAME`, `ltCONTRACT`, `ltGENERATOR_MAP`) are retained as `[[deprecated]]` members rather than removed. The comment explains the rationale: even though these IDs were never used for real objects, deleting them from the enum would open their numeric slots for accidental reuse by future types. Keeping them marked deprecated ensures the compiler warns on any new usage while the slot remains claimed in the protocol namespace. + +The header's `@todo` acknowledges a gap: C++ enums cannot enforce uniqueness of values at compile time, so duplicate IDs can silently coexist. This is a known risk given the wide numeric gaps in the assigned values. + +## `LedgerSpecificFlags` — Flag Bitmasks via Nested X-Macros + +Flag definitions use a more elaborate multi-level X-macro strategy. The core table is the `XMACRO` macro, which lists every ledger object type alongside its named flags and their hex values. Three local helper macros (`TO_VALUE`, `NULL_NAME`, `TO_MAP`, etc.) are then applied to the same `XMACRO` to generate three distinct outputs in sequence: + +**1. The `LedgerSpecificFlags` enum** (`uint32_t`): `XMACRO(NULL_NAME, TO_VALUE, NULL_OUTPUT)` strips the object names and collects all flag names and values into a single flat enum. This is the enum code uses directly (`lsfRequireDestTag`, `lsfGlobalFreeze`, `lsfSellNFToken`, etc.). + +A subtle detail is `LSF_FLAG2` versus `LSF_FLAG`. `lsfMPTLocked` has the same bit value `0x00000001` in both `MPTokenIssuance` and `MPToken`. Emitting it twice via `LSF_FLAG` would create a duplicate enum value, which is well-defined in C++ but triggers warnings. `LSF_FLAG2` maps to `NULL_OUTPUT` in the enum pass, silently omitting the second occurrence from the enum while still including it in the per-object flag maps. + +**2. Per-object flag accessor functions**: `XMACRO(TO_MAP, VALUE_TO_MAP, VALUE_TO_MAP)` expands into one inline function per ledger object type — `getAccountRootFlags()`, `getOfferFlags()`, `getRippleStateFlags()`, and so on. Each returns a `const LedgerFlagMap&` (i.e., `std::map`), initialized once via Meyer's singleton. This avoids the static-initialization-order fiasco while providing efficient repeated access. + +**3. `getAllLedgerFlags()`**: The outermost aggregator collects all per-object maps into a `vector>` using the same singleton pattern. This function is the sole data source for the `server_definitions` RPC endpoint, which exposes the full flag catalogue to external API consumers. + +The entire macro block is bracketed by `#pragma push_macro` / `#pragma pop_macro` guards, protecting any prior definitions of the internal macro names (`XMACRO`, `TO_VALUE`, etc.) in case the header is included in a translation unit that has already defined them for its own purposes. + +## `LedgerFormats` — Format Registry + +`LedgerFormats` inherits from `KnownFormats` using CRTP. `KnownFormats` is the generic engine: it maintains a `std::forward_list` (node-stable storage so pointer identity is preserved), a flat name map, and a flat type map. `LedgerFormats` fills it in its constructor. + +The constructor (defined in `LedgerFormats.cpp`) performs the second expansion of `ledger_entries.macro`, this time with `LEDGER_ENTRY` defined as a call to `add(jss::name, tag, UNWRAP fields, getCommonFields())`. This registers each entry type's `SOTemplate` — the schema describing which serialized fields are required, optional, or default — alongside its name and numeric type ID. + +`getCommonFields()` returns the three fields shared by every ledger object: `sfLedgerIndex` (optional), `sfLedgerEntryType` (required), and `sfFlags` (required). These are injected as common fields into every `SOTemplate` via `KnownFormats::add`, so each entry type's schema automatically includes them without being listed individually in the macro. + +`getInstance()` is a Meyer's singleton returning the one global `LedgerFormats`. Code throughout `rippled` — particularly `STLedgerEntry`, `STParsedJSON`, and invariant checkers — calls it to retrieve the `SOTemplate` for a given `LedgerEntryType`, validate incoming serialized objects, or iterate all known types. + +The `LEDGER_ENTRY_DUPLICATE` macro exists to handle the name-collision problem: `DepositPreauth` names both a transaction type and a ledger entry type. The JSS system (which generates JSON string constants) cannot have two `JSS(DepositPreauth)` expansions in the same translation unit. `LEDGER_ENTRY_DUPLICATE` expands identically to `LEDGER_ENTRY` in the constructor context but can be given a no-op definition in JSS-generating contexts to skip the second occurrence. + +## Design Tradeoffs + +The overarching design decision is to make the `ledger_entries.macro` file the single source of truth for all per-type data. This means the enum, the constructor, the format schemas, and any future generated output (auto-generated builder classes exist in `protocol_autogen/`) all derive from the same table, making it impossible for them to drift apart. The cost is that the macro layer adds cognitive overhead: understanding what the header actually declares requires mentally expanding two separate macro passes. + +Flag values start at `0x00010000` for most object types (leaving the lower 16 bits for future use or system purposes), but `DirNode`, `NFTokenOffer`, and the MPToken family use the lower bits — a legacy of their respective feature design decisions. This inconsistency is visible but harmless as long as the flags are only interpreted in the context of a known object type. \ No newline at end of file diff --git a/include/xrpl/protocol/LedgerHeader.h.ai.json b/include/xrpl/protocol/LedgerHeader.h.ai.json new file mode 100644 index 0000000000..d9c50d2f53 --- /dev/null +++ b/include/xrpl/protocol/LedgerHeader.h.ai.json @@ -0,0 +1,60 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "LedgerHeader" + } + ], + "description": "Defines the LedgerHeader struct and related functions for handling ledger metadata in the XRPL protocol, including serialization, deserialization, and hash calculation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/LedgerHeader.h", + "functions": [ + { + "args": [ + "LedgerHeader const& info" + ], + "lineno": 46, + "name": "getCloseAgree" + }, + { + "args": [ + "LedgerHeader const&", + "Serializer&", + "bool includeHash" + ], + "lineno": 51, + "name": "addRaw" + }, + { + "args": [ + "Slice data", + "bool hasHash" + ], + "lineno": 54, + "name": "deserializeHeader" + }, + { + "args": [ + "Slice data", + "bool hasHash" + ], + "lineno": 57, + "name": "deserializePrefixedHeader" + }, + { + "args": [ + "LedgerHeader const& info" + ], + "lineno": 60, + "name": "calculateLedgerHash" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/LedgerHeader.h.ai.md b/include/xrpl/protocol/LedgerHeader.h.ai.md new file mode 100644 index 0000000000..367c7f738f --- /dev/null +++ b/include/xrpl/protocol/LedgerHeader.h.ai.md @@ -0,0 +1,58 @@ +# `include/xrpl/protocol/LedgerHeader.h` + +## Role in the System + +`LedgerHeader` is the compact, canonical summary of a single ledger — the XRPL equivalent of a blockchain block header. Every ledger that passes through the system, whether open (still accumulating transactions), closed (transaction set determined), or validated (confirmed by quorum), is identified and authenticated through this structure. The header encapsulates the cryptographic commitments (hashes) that link the ledger chain together, the timing metadata produced by consensus, and the lifecycle flags that track each ledger's progression through the validation pipeline. + +This file pairs with `src/libxrpl/protocol/LedgerHeader.cpp`, which provides the serialization, deserialization, and hash-calculation implementations. + +## The `LedgerHeader` Struct + +The fields divide naturally into two groups, and the source comments make this partitioning explicit. + +**Fields valid for all ledgers** (including open, in-progress ledgers): +- `seq` (`LedgerIndex`, a `uint32_t`) — the ledger's sequence number, the monotonically increasing chain counter. +- `parentCloseTime` — the close time of the prior ledger, expressed as a `NetClock::time_point`. `NetClock` is a custom clock whose epoch is January 1, 2000 (offset 946684800 seconds from Unix epoch), and its `duration` is integer seconds stored as `uint32_t`. This 32-bit second counter fits comfortably within four bytes on the wire. + +**Fields valid only for closed ledgers** (where the transaction set has been finalized): +- `hash`, `txHash`, `accountHash`, `parentHash` — all `uint256`. The hash chain is the backbone of ledger integrity: `parentHash` links this ledger to its predecessor; `txHash` and `accountHash` commit to the SHAMap roots of the transaction set and the account state tree, respectively; `hash` is the ledger's own identity, computed by `calculateLedgerHash`. +- `drops` (`XRPAmount`) — the total XRP in existence at this ledger, in drops (1 XRP = 1,000,000 drops). +- `closeTime`, `closeTimeResolution`, `closeFlags` — consensus-produced timing metadata (see below). + +The `validated` flag is declared `mutable`, which is an acknowledged design wart (the comment reads "VFALCO TODO Make this not mutable"). It's mutable because `LedgerHeader` is frequently embedded in `const`-qualified objects, yet the validation state — which transitions one-way from `false` to `true` and never reverts — must remain updatable after the fact. The `accepted` flag tracks a distinct local concept: whether this node has accepted the ledger's transaction set, independent of the network-wide validation quorum. + +## Consensus Close-Time Flags + +`sLCF_NoConsensusTime` (value `0x01`) is the sole defined close-flag bit. When the consensus round cannot agree on a close time, this bit is set and `getCloseAgree()` returns `false`. In practice, this happens when the validator set is small or when significant clock skew exists across validators. The application-level code in `Ledger.cpp` writes `header_.closeFlags = correctCloseTime ? 0 : sLCF_NoConsensusTime` after each consensus round, and `LedgerToJson.cpp` propagates this to RPC responses as `close_flags`. Code that needs a reliable close time (such as the ledger replay subsystem) guards against this flag explicitly. + +The field is declared as `int` in the struct but serialized as a single `uint8_t` on the wire, meaning only the low 8 bits are meaningful; the current protocol has room for seven more close-time flags without changing the wire format. + +## Wire Format and Serialization + +`addRaw()` writes the header in a fixed, canonical byte layout: + +| Field | Size | Notes | +|---|---|---| +| `seq` | 4 bytes | uint32 big-endian | +| `drops` | 8 bytes | uint64, in drops | +| `parentHash` | 32 bytes | raw bytes | +| `txHash` | 32 bytes | raw bytes | +| `accountHash` | 32 bytes | raw bytes | +| `parentCloseTime` | 4 bytes | seconds since epoch 2000-01-01 | +| `closeTime` | 4 bytes | seconds since epoch 2000-01-01 | +| `closeTimeResolution` | 1 byte | uint8, range 2–120 s | +| `closeFlags` | 1 byte | uint8 | + +Total: 118 bytes without hash, 150 with. The `includeHash` parameter appends the ledger's own `hash` field — used when storing headers to the database so that the hash can be read back without recomputation, but **not** used in the hash-calculation path itself (doing so would be circular). + +`deserializePrefixedHeader()` is a thin wrapper that simply advances the data pointer by 4 bytes before calling `deserializeHeader()`. Network messages prepend a 4-byte type discriminant before the raw header, so this variant handles peer-wire parsing while keeping the core deserialization path clean. + +## Hash Calculation + +`calculateLedgerHash()` produces the ledger's canonical identity hash using `sha512Half`, which computes SHA-512 over the input and returns the first 256 bits. The hash is domain-separated by prepending `HashPrefix::ledgerMaster` (the ASCII bytes `'L'`, `'W'`, `'R'` followed by a zero byte), preventing collisions with hashes computed over other XRPL object types (transactions, account state nodes, etc.) that may share binary structure. + +The hash input mirrors `addRaw` precisely — this invariant is even called out in the source comment "VFALCO This has to match addRaw in View.h." The notable design difference is that `calculateLedgerHash` feeds fields directly into `sha512Half`'s variadic template rather than going through the `Serializer` class. Both paths must produce identical byte sequences, and any divergence would silently break hash verification across the network. The hash does **not** include `validated`, `accepted`, `closeTime` for open ledgers, or the hash itself — only the consensus-finalized fields. + +## Relationship to Ledger Views + +`LedgerHeader` is embedded inside the `ReadView`/`ApplyView` hierarchy that provides access to ledger state. The header fields are accessible as `view.info()`, returning a `const LedgerHeader&`. The separation of the header struct from the view interface means metadata can be cheaply copied, compared, and transmitted without dragging in the full account-state SHAMap. This is exploited heavily in the overlay layer (peer synchronization), the database persistence layer, and RPC handlers that need to report ledger metadata without loading the full ledger object. \ No newline at end of file diff --git a/include/xrpl/protocol/LedgerShortcut.h.ai.json b/include/xrpl/protocol/LedgerShortcut.h.ai.json new file mode 100644 index 0000000000..afbd5da07c --- /dev/null +++ b/include/xrpl/protocol/LedgerShortcut.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines an enumeration LedgerShortcut for specifying commonly used ledger references (Current, Closed, Validated) in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/LedgerShortcut.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/LedgerShortcut.h.ai.md b/include/xrpl/protocol/LedgerShortcut.h.ai.md new file mode 100644 index 0000000000..d650953eb3 --- /dev/null +++ b/include/xrpl/protocol/LedgerShortcut.h.ai.md @@ -0,0 +1,25 @@ +# `LedgerShortcut.h` + +This header defines a small but semantically significant enumeration that captures the three canonical ledger states any XRPL consumer needs to distinguish. Rather than forcing callers to pass magic strings like `"current"`, `"closed"`, or `"validated"` — or to invent ad-hoc integer sentinels — `LedgerShortcut` gives the type system a precise vocabulary for expressing ledger selection intent without requiring a specific sequence number or hash. + +## The Three Ledger States + +The XRPL maintains three distinct ledger states at any point in time, and each has a distinct meaning: + +- **`Current`** — the open, in-progress ledger still accumulating new transactions. It has not yet been closed or validated, so its contents may change. +- **`Closed`** — the most recently closed ledger, meaning no new transactions are being accepted into it, but consensus validation has not yet completed. It is stable in structure but not yet authoritative. +- **`Validated`** — the most recently validated ledger, the fully consensus-confirmed chain tip. This is the only state considered immutable and trustworthy for finality purposes. + +This distinction matters operationally: querying the `Validated` ledger gives a guaranteed final answer, while `Current` or `Closed` may reflect intermediate state that could be rolled back during reorganization or consensus failure. + +## Role in the Ledger Lookup Infrastructure + +`LedgerShortcut` is the third leg of a three-way overload set in `RPCLedgerHelpers.h`. The `getLedger` family accepts a `uint256` hash, a `uint32_t` sequence number, or a `LedgerShortcut`, mirroring the three canonical ways an RPC caller can specify which ledger they want. In `RPCLedgerHelpers.cpp`, the `lookupLedger` parsing logic maps the JSON string fields `"current"`, `"closed"`, and `"validated"` onto the corresponding enum values before dispatching to the appropriate `getLedger` overload. The `AccountTx.cpp` RPC handler does the same when processing the `ledger_index` field. + +The enum also appears as one arm of `RelationalDatabase::LedgerSpecifier`, a `std::variant`. This variant type unifies all the ways a query layer caller can say "which ledger(s)?" into a single parameter, letting implementations dispatch via `std::visit` rather than maintaining parallel overloaded method families. `LedgerShortcut` being a first-class participant in that variant means symbolic ledger names flow cleanly through the database query layer without needing special-case handling. + +## Design Rationale + +Using a scoped `enum class` rather than a plain `enum` or string constants prevents implicit integer conversions and namespace pollution, which would be hazards in a codebase that also works extensively with raw integer ledger sequence numbers. The three values map directly onto the three states the XRPL consensus model defines, so there is no over-engineering — no "best" or "latest" alias that would duplicate semantics and create confusion. + +The file has no dependencies beyond `#pragma once` and the `xrpl` namespace, which is intentional: this is a pure vocabulary type. Any layer of the stack — RPC handlers, database helpers, gRPC adapters — can include it without pulling in heavyweight ledger or network machinery. \ No newline at end of file diff --git a/include/xrpl/protocol/MPTAmount.h.ai.json b/include/xrpl/protocol/MPTAmount.h.ai.json new file mode 100644 index 0000000000..4301dee1d4 --- /dev/null +++ b/include/xrpl/protocol/MPTAmount.h.ai.json @@ -0,0 +1,56 @@ +{ + "args": [ + { + "lineno": 77, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "MPTAmount const& other", + "beast::Zero", + "Number const& x", + "value_type value" + ], + "lineno": 13, + "name": "MPTAmount" + } + ], + "description": "Defines the MPTAmount class for representing signed integer amounts in the XRPL protocol, including arithmetic operations, conversions, and utility functions such as mulRatio for ratio multiplication with rounding.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/MPTAmount.h", + "functions": [ + { + "args": [ + "os", + "q" + ], + "lineno": 97, + "name": "operator<<" + }, + { + "args": [ + "amount" + ], + "lineno": 104, + "name": "to_string" + }, + { + "args": [ + "amt", + "num", + "den", + "roundUp" + ], + "lineno": 109, + "name": "mulRatio" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/MPTAmount.h.ai.md b/include/xrpl/protocol/MPTAmount.h.ai.md new file mode 100644 index 0000000000..b0017fdf52 --- /dev/null +++ b/include/xrpl/protocol/MPTAmount.h.ai.md @@ -0,0 +1,51 @@ +# `MPTAmount.h` — Typed Integer Amount for Multi-Purpose Tokens + +## Purpose and Context + +`MPTAmount` is the canonical numeric type for quantities of Multi-Purpose Tokens (MPTs), a token category introduced to XRPL alongside the existing native XRP and IOU trust-line tokens. It sits in the same tier as `XRPAmount` and `IOUAmount`, and the three together satisfy the `StepAmount` concept defined in `Concepts.h`, which allows generic payment-path and DEX algorithms to operate over all token types via C++20 templates. + +The fundamental design decision for MPT amounts is that they are plain signed integers, not floating-point or mantissa/exponent pairs. This matches XRP drops and contrasts sharply with `IOUAmount`, which stores a `(mantissa, exponent)` pair to handle the 10¹⁵ range of IOU values. MPT issuers set a precision at issuance time; at runtime the ledger simply tracks whole units up to the protocol cap of `maxMPTokenAmount = 0x7FFF'FFFF'FFFF'FFFFull` (equal to `INT64_MAX`), validated in `Protocol.h` with a `static_assert` that `Number::maxRep >= maxMPTokenAmount`. + +## Class Structure + +`MPTAmount` inherits privately from four Boost operator templates, a well-established CRTP pattern for composing arithmetic without writing every combination by hand: + +- `boost::totally_ordered` — synthesizes `!=`, `>`, `>=`, `<=` from the declared `==` and `<`. +- `boost::additive` — synthesizes `operator+` and `operator-` (binary) from `+=` and `-=`. +- `boost::equality_comparable` — heterogeneous `!=` from `operator==(value_type)`. +- `boost::additive` — heterogeneous `+`/`-` with raw integers. + +The field `value_` is declared `protected` rather than `private`, which is conspicuous given that `XRPAmount` makes `drops_` private. This opens a subclassing path without exposing the raw integer to unrelated code. No subclasses appear in the current codebase, so this is either forward-looking or a minor inconsistency. + +## Key Design Choices + +**`value()` is deliberately awkward to call.** The accessor carries a documented comment: "Code SHOULD NOT call this function unless the type has been abstracted away." The intention is to keep arithmetic in the typed domain. Callers who work with generic templates parameterized on amount type may call `value()` without knowing the concrete type; everyone else should use the type directly. This same convention appears word-for-word in `XRPAmount`. + +**`beast::Zero` integration** provides a conventional zero sentinel that avoids constructing `MPTAmount(0)` explicitly. Both the constructor `MPTAmount(beast::Zero)` and `operator=(beast::Zero)` set `value_` to 0. This is idiomatic throughout the XRPL codebase: `beast::zero` is a global constant of type `beast::Zero` used for zero-initialization in generic code. + +**Implicit conversion to `Number`** via `operator Number() const noexcept` allows `MPTAmount` to be passed anywhere a `Number` is expected — arithmetic operations, rounding, comparisons. The reverse direction (constructing from `Number`) is explicit and rounds to nearest with ties going to even, matching IEEE 754 default rounding. The `XRPAmount` class uses the identical comment and mechanism. + +## The `mulRatio` Function + +`mulRatio` is a free function (not a method) that computes `amt * num / den` with configurable rounding direction: + +```cpp +MPTAmount mulRatio(MPTAmount const& amt, uint32_t num, uint32_t den, bool roundUp); +``` + +The intermediate product is computed in `boost::multiprecision::int128_t` to avoid overflow — multiplying a 63-bit value by a 32-bit numerator can produce up to 95 bits. After division, if there is a remainder, the function adjusts the result according to the sign of `amt` and the `roundUp` flag: positive amounts round up when `roundUp=true`; negative amounts round away from zero (more negative) when `roundUp=false`. If the final result exceeds `std::numeric_limits::max()`, it throws `std::overflow_error`. This function is used for fee and reserve calculations that apply ratios (e.g., percentage-of-amount fees). + +A zero denominator throws `std::runtime_error` immediately, before any arithmetic, via the XRPL `Throw` mechanism which integrates with test-override hooks. + +## Integration with the Amount Type System + +`AmountConversions.h` shows where `MPTAmount` connects to the on-ledger `STAmount` representation: + +- `toSTAmount(MPTAmount const& mpt, Asset const& asset)` wraps the integer in an `STAmount` tagged with the `MPTIssue` asset. +- `toAmount(STAmount const& amt)` extracts back, asserting that the `STAmount` holds an `MPTIssue`, that the mantissa does not exceed `maxMPTokenAmount`, and that the exponent is exactly 0. A non-zero exponent would mean the value came from the floating-point IOU encoding path — a protocol invariant violation — so a runtime `Throw` fires in addition to the debug `XRPL_ASSERT`. + +This double-layer defense (assert + throw) is unique to MPT among the three amount types, reflecting that the integer-only constraint is a protocol rule that must be enforced even in release builds. + +## Relationship to `XRPAmount` + +The two classes are structurally near-identical: same `value_type`, same Boost bases, same `mulRatio` implementation, same `to_string` and streaming operator, same `minPositiveAmount()` returning the type holding `1`. The functional differences are that `XRPAmount` exposes the domain-specific name `drops()` alongside the generic `value()`, provides `dropsAs()` for safe narrowing conversions, `decimalXRP()` for human-readable display, and `jsonClipped()` for JSON serialization. `MPTAmount` omits these because MPT has no sub-unit naming convention and its JSON encoding is handled at the `STAmount` layer. \ No newline at end of file diff --git a/include/xrpl/protocol/MPTIssue.h.ai.json b/include/xrpl/protocol/MPTIssue.h.ai.json new file mode 100644 index 0000000000..1916902ea4 --- /dev/null +++ b/include/xrpl/protocol/MPTIssue.h.ai.json @@ -0,0 +1,177 @@ +{ + "args": [ + { + "lineno": 17, + "name": "issuanceID" + }, + { + "lineno": 19, + "name": "sequence" + }, + { + "lineno": 19, + "name": "account" + }, + { + "lineno": 38, + "name": "jv" + }, + { + "lineno": 49, + "name": "lhs" + }, + { + "lineno": 49, + "name": "rhs" + }, + { + "lineno": 63, + "name": "mptid" + }, + { + "lineno": 89, + "name": "h" + }, + { + "lineno": 89, + "name": "r" + }, + { + "lineno": 95, + "name": "mptIssue" + }, + { + "lineno": 97, + "name": "mptIssue" + }, + { + "lineno": 99, + "name": "jv" + }, + { + "lineno": 101, + "name": "os" + }, + { + "lineno": 101, + "name": "x" + } + ], + "classes": [ + { + "args": [ + "MPTID const&", + "std::uint32_t", + "AccountID const&" + ], + "lineno": 10, + "name": "MPTIssue" + } + ], + "description": "Defines the MPTIssue class, which adapts MPTID to provide an interface similar to Issue for use in static polymorphism. Includes utility functions for working with MPTID and MPTIssue, such as conversion, comparison, and JSON serialization.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/MPTIssue.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 49, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 54, + "name": "operator<=>" + }, + { + "args": [ + "MPTID const&" + ], + "lineno": 59, + "name": "isXRP" + }, + { + "args": [ + "MPTID const&" + ], + "lineno": 63, + "name": "getMPTIssuer" + }, + { + "args": [ + "MPTID const&&" + ], + "lineno": 74, + "name": "getMPTIssuer" + }, + { + "args": [ + "MPTID&&" + ], + "lineno": 75, + "name": "getMPTIssuer" + }, + { + "args": [], + "lineno": 77, + "name": "noMPT" + }, + { + "args": [], + "lineno": 83, + "name": "badMPT" + }, + { + "args": [ + "Hasher&", + "MPTIssue const&" + ], + "lineno": 89, + "name": "hash_append" + }, + { + "args": [ + "MPTIssue const&" + ], + "lineno": 95, + "name": "to_json" + }, + { + "args": [ + "MPTIssue const&" + ], + "lineno": 97, + "name": "to_string" + }, + { + "args": [ + "Json::Value const&" + ], + "lineno": 99, + "name": "mptIssueFromJson" + }, + { + "args": [ + "std::ostream&", + "MPTIssue const&" + ], + "lineno": 101, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 104, + "name": "std" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/MPTIssue.h.ai.md b/include/xrpl/protocol/MPTIssue.h.ai.md new file mode 100644 index 0000000000..18f070d85a --- /dev/null +++ b/include/xrpl/protocol/MPTIssue.h.ai.md @@ -0,0 +1,67 @@ +# `MPTIssue.h` — MPT Issuance Adapter for Protocol Polymorphism + +## Role in the System + +`MPTIssue` exists because the XRPL codebase originally modeled all fungible assets through the `Issue` type, which pairs a 160-bit `Currency` with an `AccountID` issuer. When Multi-Purpose Tokens (MPTs) were introduced, code throughout the engine needed to handle a third asset class alongside XRP and IOU. Rather than templating every function on a raw `MPTID`, the designers introduced `MPTIssue` as a thin wrapper that mirrors `Issue`'s public interface precisely, allowing `Asset` (and any generic template code constrained by `ValidIssueType`) to treat the two types uniformly. + +## The MPTID Encoding + +`MPTID` (defined in `UintTypes.h`) is a `base_uint<192>` — 24 bytes stored in big-endian order. Its layout is a simple concatenation: + +``` +[ 4 bytes: uint32_t sequence | 20 bytes: AccountID ] +``` + +This encoding is the canonical identifier for an MPT issuance on the ledger. The sequence number disambiguates multiple issuances from the same account. `MPTIssue` holds exactly one `MPTID mptID_` and exposes this structure through its API. + +## Interface Mirroring Strategy + +The class deliberately replicates the public surface of `Issue`: + +| Method | `Issue` behavior | `MPTIssue` behavior | +|---|---|---| +| `getIssuer()` | returns `account` field | extracts bytes 4–23 of `mptID_` | +| `getText()` | formats currency+account | formats `mptID_` as hex | +| `setJson(jv)` | writes `currency`/`issuer` fields | writes `mpt_issuance_id` field | +| `native()` | `true` for XRP | always `false` | +| `integral()` | `true` only for XRP | always `true` | + +`native()` returning `false` and `integral()` returning `true` is significant: MPTs carry 64-bit integer amounts (like `MPTAmount`), whereas IOUs use multi-precision rational arithmetic. The `Asset::getAmountType()` method in `Asset.h` dispatches on these flags to select between `XRPAmount`, `IOUAmount`, and `MPTAmount` at compile time. + +The `ValidIssueType` concept in `Concepts.h` constrains templates to accept only `Issue` or `MPTIssue`, which is how `Asset::get()` and `Asset::holds()` enforce type safety at compile time. The implicit conversion operator `operator MPTID const&()` allows an `MPTIssue` to be passed wherever a raw `MPTID` is expected without a cast. + +## Issuer Extraction: Two Approaches + +There are two ways to extract the `AccountID` from a `MPTID`, and the header deliberately provides both: + +**`getIssuer()` (member function):** Uses `reinterpret_cast` on the underlying byte buffer, skipping the first 4 bytes. This is zero-copy and returns a reference, but it works only because `AccountID` is a `base_uint<160>` with a compatible memory layout. The `static_assert` on sizes guards this assumption. + +**`getMPTIssuer()` (free function):** Uses `std::bit_cast` after copying 20 bytes into a `std::array`. The comment explicitly notes that `bit_cast` is a compiler intrinsic typically optimized to nothing in the final assembly, so the copy exists only at the C++ source level. The free function returns by value. + +Crucially, `getMPTIssuer()` **deletes its rvalue-reference overloads**: + +```cpp +inline AccountID const& getMPTIssuer(MPTID const&&) = delete; +inline AccountID const& getMPTIssuer(MPTID&&) = delete; +``` + +This prevents a dangling-reference bug: if a caller passed a temporary `MPTID`, the returned `AccountID const&` would immediately dangle. The deleted overloads turn this into a compile-time error. The member `getIssuer()` doesn't have this problem because it returns a reference into `mptID_`, which lives as long as the `MPTIssue` object. + +## Sentinel Values + +`noMPT()` and `badMPT()` replicate the sentinel pattern from `Issue.h`: + +- `noMPT()` encodes `{ sequence=0, account=noAccount() }` — all-zero bits, representing "no MPT". +- `badMPT()` encodes `{ sequence=0, account=xrpAccount() }` — the XRP account address, a conventionally invalid issuer for MPTs. + +The `BadAsset` comparison in `Asset.h` detects a bad MPT by checking `issue.getIssuer() == xrpAccount()`, matching this sentinel definition. + +## Comparisons and Hashing + +Equality and three-way comparison (`<=>`) are both `constexpr` and delegate directly to `MPTID`'s operators, which compare the raw 192-bit value. This means two `MPTIssue` instances are equal if and only if their full `MPTID` — sequence and account combined — matches. There is no partial equality ignoring the sequence, unlike `Issue` equality which ignores the issuer account when the currency is XRP. + +The `hash_append` template plugs `MPTIssue` into the Beast hashing framework, and the `std::hash` specialization placed in the `std` namespace at the bottom of the header makes `MPTID` usable directly as a key in `std::unordered_map` and similar containers without going through `MPTIssue`. + +## JSON Serialization + +`setJson()` writes a single `mpt_issuance_id` key, contrasted with `Issue::setJson()` which writes `currency` and `issuer` separately. `mptIssueFromJson()` strictly validates this distinction: it throws if `currency` or `issuer` keys are present, enforcing that MPT JSON cannot be confused with IOU JSON. The parsing path `parseHex` on `MPTID` validates the 48-character hex string before constructing the object. \ No newline at end of file diff --git a/include/xrpl/protocol/MultiApiJson.h.ai.json b/include/xrpl/protocol/MultiApiJson.h.ai.json new file mode 100644 index 0000000000..794099079c --- /dev/null +++ b/include/xrpl/protocol/MultiApiJson.h.ai.json @@ -0,0 +1,80 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Json::Value const& init = {}" + ], + "lineno": 16, + "name": "MultiApiJson" + } + ], + "description": "Defines a template struct MultiApiJson for managing collections of Json::Value objects indexed by API version, enabling versioned JSON manipulation and visitation for XRPL APIs.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/MultiApiJson.h", + "functions": [ + { + "args": [ + "v" + ], + "lineno": 23, + "name": "valid" + }, + { + "args": [ + "v" + ], + "lineno": 28, + "name": "index" + }, + { + "args": [ + "key", + "v" + ], + "lineno": 38, + "name": "set" + }, + { + "args": [ + "key" + ], + "lineno": 47, + "name": "isMember" + }, + { + "args": [], + "lineno": 109, + "name": "visit" + }, + { + "args": [], + "lineno": 117, + "name": "visit" + }, + { + "args": [ + "args..." + ], + "lineno": 124, + "name": "visit" + }, + { + "args": [ + "args..." + ], + "lineno": 133, + "name": "visit" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 13, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/MultiApiJson.h.ai.md b/include/xrpl/protocol/MultiApiJson.h.ai.md new file mode 100644 index 0000000000..2cbe9c82f4 --- /dev/null +++ b/include/xrpl/protocol/MultiApiJson.h.ai.md @@ -0,0 +1,80 @@ +# `include/xrpl/protocol/MultiApiJson.h` + +## Purpose + +`MultiApiJson` solves a specific distribution problem: the XRPL server maintains long-lived subscriptions from clients that speak different API versions, and a single ledger event (e.g., a confirmed transaction) must be delivered in slightly different JSON shapes to each subscriber. Rather than re-serializing on every delivery or branching inside the send path, `MultiApiJson` holds one pre-built `Json::Value` per supported API version, populated once at event-creation time. + +The public alias at the bottom of the file binds the template to the live version range: + +```cpp +using MultiApiJson = + detail::MultiApiJson; +``` + +`apiMinimumSupportedVersion` is 1 and `apiMaximumValidVersion` is the beta version 3, so the concrete type stores exactly three `Json::Value` objects in a `std::array`. + +## Storage and Indexing + +The array has `MaxVer + 1 - MinVer` elements. The mapping from version number to array slot is a simple offset — `index(v)` returns `v - MinVer`. Out-of-range values (versions below `MinVer`) clamp to index 0 at the `index()` level, though the real guards are in `valid()` and the assertions inside `visitor_t`. + +The constructor accepts an optional initializer. When supplied, every slot in the array is copy-initialized to the same `Json::Value`. This is the common pattern in `NetworkOPs.cpp`: + +```cpp +MultiApiJson multiObj{jvObj}; // clone the base JSON into all three slots +``` + +Subsequent version-specific mutations are then applied selectively. + +## Broadcast vs. Selective Operations + +`set(key, value)` writes a key to **all** slots at once, covering fields that are identical across every API version — the majority of transaction fields. This is cheaper than calling `visit` per version for shared data. + +`isMember(key)` returns a tri-state enum — `none`, `some`, or `all` — indicating how many slots contain a given key. The enum lives on the struct rather than a class enum deliberately, as the comment notes: `MultiApiJson` is narrow enough to serve as its own scope. + +Both methods iterate the fixed-size array, so their cost is O(number-of-versions), currently O(3). + +## The Visitor Pattern + +The interesting design is in `visitor_t`, a `constexpr`-constructed stateless function object that routes calls to the right array slot. It provides four `operator()` overloads, split along two axes: + +1. **Compile-time vs. runtime version** — A version passed as `std::integral_constant` triggers a `static_assert` that V is in range; a plain `unsigned` triggers a runtime `XRPL_ASSERT`. The `some_integral_constant` concept is used in the `requires` clauses to prevent the runtime overloads from being selected when an `integral_constant` is passed, disambiguating what would otherwise be an ambiguous partial ordering. + +2. **With or without extra arguments forwarded to the callable** — When extra args are present they're forwarded to `fn` after the selected `Json::Value` (and possibly the version value). This matches the calling convention of `forAllApiVersions` / `forApiVersions`, which pass each version as an `integral_constant` along with any extra args bound at the call site. + +## The `visit()` Interface + +`visit()` has two distinct call forms. Called with arguments it directly invokes `visitor_t`: + +```cpp +jvObj.visit(RPC::apiVersion<1>, [](Json::Value& jv) { /* mutate v1 slot */ }); +``` + +Called with no arguments, it returns a **closure** capturing `this`: + +```cpp +forAllApiVersions(multiObj.visit(), [&](Json::Value& jv, auto versionConst) { + RPC::insertDeliverMax(jv[jss::transaction], txType, Version); +}); +``` + +This two-form design is what makes `MultiApiJson` composable with the `forAllApiVersions`/`forApiVersions` utilities in `ApiVersion.h`. Those utilities expand the version range at compile time using `std::index_sequence`, passing each version as an `integral_constant` to the callable. The closure returned by `visit()` satisfies exactly that calling convention, so `forAllApiVersions(multiObj.visit(), lambda)` iterates all versions with a single consistent lambda. + +Both `visit()` and `visit(args...)` have `const` and non-`const` overloads propagating through to the underlying `Json::Value` reference. + +## Real-World Usage + +In `NetworkOPs.cpp`, `transJson()` constructs a `MultiApiJson` from a common base, then uses `visit(apiVersion<1>, ...)` to apply backwards-compatibility fixups (e.g., converting `ledger_index` from a number to a string for old API consumers). Delivery happens in `BookListeners::publish()`: + +```cpp +jvObj.visit( + p->getApiVersion(), // runtime unsigned from subscriber + [&](Json::Value const& jv) { p->send(jv, true); }); +``` + +Each subscriber queries its stored API version, `visit` picks the matching pre-built slot, and the value is sent without any re-serialization or conditional logic at the point of delivery. + +## Design Tradeoffs + +Keeping one copy per version trades memory for CPU. With three versions and typical transaction JSON, the extra allocations are modest. The alternative — computing the version-specific delta on every send — would add latency proportional to subscriber count for every transaction event. The current design amortizes the transformation cost to once per event regardless of subscriber count. + +Placing `MultiApiJson` in `detail::` and exposing only the aliased concrete type (`xrpl::MultiApiJson`) ensures callers outside the test suite use only the version range actually enforced by the running server, preventing accidental construction of ranges that don't match the live constants. \ No newline at end of file diff --git a/include/xrpl/protocol/NFTSyntheticSerializer.h.ai.json b/include/xrpl/protocol/NFTSyntheticSerializer.h.ai.json new file mode 100644 index 0000000000..bcbfa3a420 --- /dev/null +++ b/include/xrpl/protocol/NFTSyntheticSerializer.h.ai.json @@ -0,0 +1,28 @@ +{ + "args": [], + "classes": [], + "description": "Declares a function to add synthetic NFT-related fields to transaction-related JSON responses within the xrpl::RPC namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/NFTSyntheticSerializer.h", + "functions": [ + { + "args": [ + "Json::Value&", + "std::shared_ptr const&", + "TxMeta const&" + ], + "lineno": 15, + "name": "insertNFTSyntheticInJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/NFTSyntheticSerializer.h.ai.md b/include/xrpl/protocol/NFTSyntheticSerializer.h.ai.md new file mode 100644 index 0000000000..9c59238d44 --- /dev/null +++ b/include/xrpl/protocol/NFTSyntheticSerializer.h.ai.md @@ -0,0 +1,26 @@ +# `include/xrpl/protocol/NFTSyntheticSerializer.h` + +## Role + +This header declares a single aggregator function, `insertNFTSyntheticInJson`, that injects "synthetic" NFT-related fields into transaction RPC responses. The word *synthetic* is deliberate: these fields — `nftoken_ids` and `offer_id` — are not stored anywhere on-chain. They must be reconstructed at query time by examining the ledger state changes recorded in `TxMeta`. The function exists so that API consumers don't have to parse raw metadata diffs themselves. + +## What It Wraps + +The implementation (in `src/libxrpl/protocol/NFTSyntheticSerializer.cpp`) is a thin two-line aggregator: + +```cpp +insertNFTokenID(response[jss::meta], transaction, transactionMeta); +insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta); +``` + +`insertNFTokenID` (declared in `NFTokenID.h`) adds a `nftoken_ids` array to the meta block for successful `NFTokenMint`, `NFTokenAcceptOffer`, and `NFTokenCancelOffer` transactions by walking the metadata's modified ledger nodes. `insertNFTokenOfferID` (declared in `NFTokenOfferID.h`) similarly adds an `offer_id` field for successful `NFTokenCreateOffer` transactions. + +Both underlying inserters are non-static, explicitly to allow reuse by **Clio**, Ripple's secondary API server — a design constraint called out in the source comments. + +## Call Sites + +`insertNFTSyntheticInJson` is invoked in three RPC response paths — `Tx.cpp`, `NetworkOPs.cpp`, and `AccountTx.cpp` — always as part of a consistent metadata enrichment sequence alongside `insertDeliveredAmount` and `insertMPTokenIssuanceID`. The same enrichment block is also used in `Simulate.cpp`, so simulated transaction responses are structurally identical to real submission responses. + +## Why This File Exists + +The aggregator pattern avoids callers needing to remember the individual NFT inserter pair. As new NFT transaction types were added (mint, accept, cancel, create offer), bundling them here prevents each call site from accumulating an ever-growing list of per-type injector calls. The companion `MPTokenIssuanceID` injector follows the same convention but lives in the `xrpld` layer since it is not consumed by Clio. \ No newline at end of file diff --git a/include/xrpl/protocol/NFTokenID.h.ai.json b/include/xrpl/protocol/NFTokenID.h.ai.json new file mode 100644 index 0000000000..d8d3a0aa77 --- /dev/null +++ b/include/xrpl/protocol/NFTokenID.h.ai.json @@ -0,0 +1,46 @@ +{ + "args": [], + "classes": [], + "description": "Provides helper functions to extract and insert NFToken IDs into transaction metadata and JSON responses for specific XRPL NFT-related transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/NFTokenID.h", + "functions": [ + { + "args": [ + "serializedTx", + "transactionMeta" + ], + "lineno": 18, + "name": "canHaveNFTokenID" + }, + { + "args": [ + "transactionMeta" + ], + "lineno": 21, + "name": "getNFTokenIDFromPage" + }, + { + "args": [ + "transactionMeta" + ], + "lineno": 24, + "name": "getNFTokenIDFromDeletedOffer" + }, + { + "args": [ + "response", + "transaction", + "transactionMeta" + ], + "lineno": 27, + "name": "insertNFTokenID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/NFTokenID.h.ai.md b/include/xrpl/protocol/NFTokenID.h.ai.md new file mode 100644 index 0000000000..05a33e7e81 --- /dev/null +++ b/include/xrpl/protocol/NFTokenID.h.ai.md @@ -0,0 +1,23 @@ +# `include/xrpl/protocol/NFTokenID.h` + +This header declares a small cluster of helper functions whose sole purpose is to enrich NFT-related transaction metadata JSON responses with the token IDs that were affected — information not directly encoded in the raw ledger metadata. The sibling file `NFTokenOfferID.h` applies the same pattern for extracting created offer IDs, and the two files form a matched pair of post-processing utilities for the NFT subsystem. + +## Why this file exists + +XRPL's ledger metadata records the full before/after state of every modified ledger object, but it does not explicitly annotate *which* NFToken was created or consumed by a transaction. Applications like Clio and the rippled API server both need to surface a `nftoken_id` (or `nftoken_ids`) field in their JSON responses without forcing every consumer to re-derive it themselves. This file centralizes that derivation logic and keeps it shareable across codebases — hence the header comment that the helpers are deliberately non-static so Clio can link against them directly. + +## The four functions + +`canHaveNFTokenID()` acts as a cheap early-exit guard: it rejects any transaction that is not one of the three relevant types (`ttNFTOKEN_MINT`, `ttNFTOKEN_ACCEPT_OFFER`, `ttNFTOKEN_CANCEL_OFFER`) and also rejects any that did not succeed. Because all three extraction paths proceed from the same entry point `insertNFTokenID()`, centralising the eligibility check here prevents redundant checks and ensures the heavier diffing logic is never called on irrelevant transactions. + +`getNFTokenIDFromPage()` handles the `NFTokenMint` case, where the newly created token has been inserted into an `NFTokenPage` ledger object. The metadata does not tag the new token directly; instead the function must *derive* it by comparing the previous and final token lists across all affected `NFTokenPage` nodes. It collects `finalIDs` from both newly created pages (where the entire `NewFields` array is the final state) and modified pages (where `PreviousFields` and `FinalFields` contain before/after arrays), then finds the single element present in `finalIDs` but absent from `prevIDs` using `std::mismatch`. There is an important edge case handled explicitly: when a mint causes an existing page to split, the new page creation may also cause a *third* page's `PreviousPageMin` or `NextPageMin` pointer to be updated — that page will appear as a `ModifiedNode` but will have no `sfNFTokens` in its `PreviousFields`. The code skips such nodes with a `continue`, preventing false positives in the comparison. The function returns `std::nullopt` if the final ID count is not exactly one greater than the previous count, guarding against malformed or unexpected metadata. + +`getNFTokenIDFromDeletedOffer()` handles both `NFTokenAcceptOffer` and `NFTokenCancelOffer`. For these transactions the token ID is not inferred by diffing page state; instead it is read directly from the `sfNFTokenID` field on each deleted `NFTokenOffer` node. Since `NFTokenCancelOffer` can cancel multiple offers, and multiple cancelled offers could reference the same underlying NFT, the results are sorted and deduplicated before returning. + +`insertNFTokenID()` is the public entry point that coordinates the above. After confirming eligibility it dispatches by transaction type: mint emits `nftoken_id` (singular) from the page diff; accept offer also emits `nftoken_id` using only `result.front()` because accepting a single offer transfers exactly one NFT; cancel offer emits `nftoken_ids` (plural JSON array) because a single transaction can cancel many offers across multiple NFTs. The different field names — singular vs. plural — reflect meaningful semantic differences in the protocol, not inconsistency. + +## Design notes + +The functions take `std::shared_ptr` and `TxMeta const&` as separate parameters rather than a combined transaction-with-metadata type. This mirrors how the rest of the rippled codebase treats transactions and their metadata as independent objects that are only paired at the application layer, making the helpers composable with both the local node's processing pipeline and Clio's remote-fetch path. + +The extraction logic for `getNFTokenIDFromPage()` is inherently a set-difference operation but the implementation avoids sorting or hashing the IDs. It relies on the fact that `NFTokenPage` entries in XRPL are stored in sorted order by `NFTokenID`, so `finalIDs` and `prevIDs` are already ordered and `std::mismatch` on the parallel sequences directly finds the insertion point in linear time. \ No newline at end of file diff --git a/include/xrpl/protocol/NFTokenOfferID.h.ai.json b/include/xrpl/protocol/NFTokenOfferID.h.ai.json new file mode 100644 index 0000000000..1682835740 --- /dev/null +++ b/include/xrpl/protocol/NFTokenOfferID.h.ai.json @@ -0,0 +1,39 @@ +{ + "args": [], + "classes": [], + "description": "Provides helper functions to add an 'offer_id' field to the 'meta' output parameter for successful NFTokenCreateOffer transactions in XRPL, and to extract or insert the offer ID in transaction metadata or JSON responses.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/NFTokenOfferID.h", + "functions": [ + { + "args": [ + "std::shared_ptr const& serializedTx", + "TxMeta const& transactionMeta" + ], + "lineno": 14, + "name": "canHaveNFTokenOfferID" + }, + { + "args": [ + "TxMeta const& transactionMeta" + ], + "lineno": 18, + "name": "getOfferIDFromCreatedOffer" + }, + { + "args": [ + "Json::Value& response", + "std::shared_ptr const& transaction", + "TxMeta const& transactionMeta" + ], + "lineno": 21, + "name": "insertNFTokenOfferID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/NFTokenOfferID.h.ai.md b/include/xrpl/protocol/NFTokenOfferID.h.ai.md new file mode 100644 index 0000000000..ac35deff5b --- /dev/null +++ b/include/xrpl/protocol/NFTokenOfferID.h.ai.md @@ -0,0 +1,32 @@ +# `NFTokenOfferID.h` — Synthetic Offer ID Injection for NFTokenCreateOffer + +This header declares three free functions that collectively solve a metadata-enrichment problem: when a client submits an `NFTokenCreateOffer` transaction (or an `NFTokenMint` that carries an embedded offer via `sfAmount`), the raw transaction record does not include the resulting offer's ledger index. That ID must be recovered by scanning the transaction's affected-node metadata and injected into the RPC response as a synthetic `offer_id` field. + +## The Problem Being Solved + +On the XRPL, a newly-created `NFTokenOffer` ledger object is identified by its `LedgerIndex` (a `uint256` hash). This value is deterministic but is not echoed back in the transaction's canonical fields — it only appears implicitly in the `CreatedNode` entries of the transaction metadata. Without a helper like this, callers (wallet software, indexers, explorers) would have to traverse the entire `AffectedNodes` array themselves to find the offer they just created. The functions here encapsulate that scan once, in a reusable way. + +## Function Responsibilities + +`canHaveNFTokenOfferID` is the guard predicate. It first rejects null transactions, then filters to only the two transaction types that can create an `NFTokenOffer` object — `ttNFTOKEN_CREATE_OFFER` unconditionally, and `ttNFTOKEN_MINT` only when the `sfAmount` field is present (the mint-with-offer variant). Any transaction that failed (`!isTesSuccess`) is also rejected immediately. This tight pre-check prevents unnecessary metadata traversal and also ensures the `offer_id` field is never emitted for failed transactions where no object was created. + +`getOfferIDFromCreatedOffer` performs the actual extraction. It iterates over the transaction's affected nodes (`TxMeta::getNodes()`), skipping any node that is not of type `ltNFTOKEN_OFFER` or not a `sfCreatedNode` (i.e., modified or deleted nodes are ignored). The first qualifying node's `sfLedgerIndex` is returned as a `std::optional`. The `optional` return is meaningful: even after passing the `canHaveNFTokenOfferID` check there is a theoretical path where no `CreatedNode` exists (e.g., corrupt metadata), so callers must handle absence. + +`insertNFTokenOfferID` is the orchestrating entry point. It delegates to both of the above — short-circuiting via `canHaveNFTokenOfferID`, then conditionally writing the `offer_id` string into the `Json::Value` response when extraction succeeds. This is the only function typically called by higher-level code. + +## Architectural Position + +The comment in the header explicitly notes that these functions are *not* static "because they can be used by Clio." Clio is the separate read-optimized XRPL data API that consumes the same core library without running a full validator node. Making the helpers free functions in `libxrpl` (rather than hidden inside an RPC handler) allows Clio to call them directly without duplicating the logic. + +The actual call site in the rippled RPC layer is `insertNFTSyntheticInJson` in `NFTSyntheticSerializer.cpp`, which invokes both this and the parallel `insertNFTokenID` (from `NFTokenID.h`) back-to-back on `response[jss::meta]`: + +```cpp +insertNFTokenID(response[jss::meta], transaction, transactionMeta); +insertNFTokenOfferID(response[jss::meta], transaction, transactionMeta); +``` + +This sibling relationship with `NFTokenID.h` is deliberate: `NFTokenID.h` handles the analogous injection of `nftoken_ids` for mint/accept/cancel operations, while `NFTokenOfferID.h` handles `offer_id` for create-offer operations. Both follow the same three-function pattern (guard → extract → insert) and both enrich the `meta` sub-object of the JSON response rather than the top-level transaction fields. + +## Design Notes + +The use of `std::shared_ptr` (rather than a raw reference) for the transaction parameter reflects the ownership model of the broader RPC layer, where deserialized transactions are reference-counted. Passing `TxMeta` by const reference is appropriate since metadata is read-only here. The `optional` on `getOfferIDFromCreatedOffer` is preferable to an exception-based failure because missing metadata is a plausible but non-exceptional condition when processing historical or externally-sourced transactions. \ No newline at end of file diff --git a/include/xrpl/protocol/PathAsset.h.ai.json b/include/xrpl/protocol/PathAsset.h.ai.json new file mode 100644 index 0000000000..23d9c74103 --- /dev/null +++ b/include/xrpl/protocol/PathAsset.h.ai.json @@ -0,0 +1,157 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "PathAsset" + } + ], + "description": "Defines the PathAsset class in the xrpl namespace, representing an asset in a payment path (either Currency or MPTID), with utilities for construction, type checking, comparison, and visitation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/PathAsset.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "PathAsset" + }, + { + "args": [ + "Asset const& asset" + ], + "lineno": 16, + "name": "PathAsset" + }, + { + "args": [ + "Currency const& currency" + ], + "lineno": 17, + "name": "PathAsset" + }, + { + "args": [ + "MPTID const& mpt" + ], + "lineno": 20, + "name": "PathAsset" + }, + { + "args": [], + "lineno": 24, + "name": "holds" + }, + { + "args": [], + "lineno": 27, + "name": "isXRP" + }, + { + "args": [], + "lineno": 30, + "name": "get" + }, + { + "args": [], + "lineno": 33, + "name": "value" + }, + { + "args": [ + "Visitors&&... visitors" + ], + "lineno": 36, + "name": "visit" + }, + { + "args": [ + "PathAsset const& lhs", + "PathAsset const& rhs" + ], + "lineno": 45, + "name": "operator==" + }, + { + "args": [], + "lineno": 48, + "name": "is_currency_v" + }, + { + "args": [], + "lineno": 51, + "name": "is_mptid_v" + }, + { + "args": [ + "Asset const& asset" + ], + "lineno": 54, + "name": "PathAsset" + }, + { + "args": [], + "lineno": 60, + "name": "holds" + }, + { + "args": [], + "lineno": 66, + "name": "get" + }, + { + "args": [], + "lineno": 72, + "name": "value" + }, + { + "args": [], + "lineno": 76, + "name": "isXRP" + }, + { + "args": [ + "PathAsset const& lhs", + "PathAsset const& rhs" + ], + "lineno": 82, + "name": "operator==" + }, + { + "args": [ + "Hasher& h", + "PathAsset const& pathAsset" + ], + "lineno": 91, + "name": "hash_append" + }, + { + "args": [ + "PathAsset const& asset" + ], + "lineno": 96, + "name": "isXRP" + }, + { + "args": [ + "PathAsset const& asset" + ], + "lineno": 100, + "name": "to_string" + }, + { + "args": [ + "std::ostream& os", + "PathAsset const& x" + ], + "lineno": 102, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/PathAsset.h.ai.md b/include/xrpl/protocol/PathAsset.h.ai.md new file mode 100644 index 0000000000..66c20c0667 --- /dev/null +++ b/include/xrpl/protocol/PathAsset.h.ai.md @@ -0,0 +1,33 @@ +# `PathAsset.h` — Token Identifier for Payment Path Steps + +`PathAsset` is a lightweight discriminated union representing the asset side of a single hop within an XRPL payment path. It exists as a distinct type from the richer `Asset` class because path elements carry the token identifier separately from the issuer: `STPathElement` stores both a `PathAsset mAssetID` and an `AccountID mIssuerID` as independent members. `PathAsset` holds only the *which currency* or *which MPT* component — never the full issue specification. + +## The Core Distinction: `PathAsset` vs `Asset` + +`Asset` (from `Asset.h`) wraps `std::variant`, where `Issue` bundles a `Currency` together with an `AccountID` issuer, and `MPTIssue` bundles an `MPTID` with its issuing account. This makes `Asset` a complete tradeable instrument description. + +`PathAsset` reduces this to `std::variant` — purely the token-type identifiers defined in `UintTypes.h`: `Currency` is a 160-bit hash, `MPTID` is a 192-bit identifier. The issuer is not part of `PathAsset` because payment path serialization (and `STPathElement`) records the issuer in a separate field; collapsing them would duplicate data and complicate the encoding. + +The `PathAsset(Asset const& asset)` constructor performs the projection: it visits the incoming `Asset`, strips the issuer, and retains only the token identifier. XRP/IOU assets contribute their `currency` field; MPT assets contribute their `getMptID()`. + +## Type Safety via `ValidPathAsset` + +The `ValidPathAsset` concept in `Concepts.h` constrains template parameters to exactly `Currency` or `MPTID`. Both `holds()` and `get()` are gated by this concept, ensuring only those two types can be queried at compile time. The trait templates `is_currency_v` and `is_mptid_v` provide compile-time predicates for generic code that branches on asset kind. + +`get()` throws `std::runtime_error` if the wrong alternative is requested at runtime, making misuse detectable rather than silently undefined. This is appropriate given that callers are expected to check `holds()` or dispatch through `visit()` before calling `get()`. + +## Visitation Pattern + +`PathAsset::visit()` delegates to `detail::visit()` from `Concepts.h`, which employs the overloaded-lambda (`CombineVisitors`) pattern to synthesize a single callable from multiple lambdas and forward it to `std::visit`. This infrastructure is shared by `Asset` and `PathAsset`, giving both a uniform, clean visitation API without boilerplate. + +`isXRP()` illustrates the pattern: a `Currency` alternative consults `xrpl::isXRP(currency)`, while an `MPTID` alternative unconditionally returns `false`. An MPT can never be the native asset; encoding this directly as a `constexpr` lambda branch rather than a runtime dispatch makes the intent explicit and allows the compiler to eliminate the dead branch. + +## Equality and Hashing + +`operator==` uses a two-variant `std::visit` with a `if constexpr` guard: two `PathAsset` values holding different alternative types are *never* equal, even if their byte representations coincidentally match. This prevents cross-type false positives. The same logic appears in `Asset::operator==`. + +`hash_append` dispatches to the appropriate `hash_append` overload for the held type, enabling `PathAsset` to be used as keys in hash-based containers that rely on the `beast::uhash` infrastructure. + +## Role in Payment Path Processing + +In `PaySteps.cpp`, `PathAsset` is retrieved from path elements via `getPathAsset()` and compared directly with `curAsset` to track what currency/MPT flows through each step. When a path element specifies no explicit asset (`!(nodeType & typeAsset)`), the current asset propagates unchanged. When a step does specify an asset, the `PathAsset` extracted from it becomes the new current asset, enabling path-building logic to detect type changes and validate consistency across hops. The `STPathElement::typeAsset` bitmask is `typeCurrency | typeMPT`, reflecting exactly the two alternatives that `PathAsset` can hold. \ No newline at end of file diff --git a/include/xrpl/protocol/PayChan.h.ai.json b/include/xrpl/protocol/PayChan.h.ai.json new file mode 100644 index 0000000000..02cc0669af --- /dev/null +++ b/include/xrpl/protocol/PayChan.h.ai.json @@ -0,0 +1,37 @@ +{ + "args": [ + { + "lineno": 10, + "name": "msg" + }, + { + "lineno": 10, + "name": "key" + }, + { + "lineno": 10, + "name": "amt" + } + ], + "classes": [], + "description": "Provides a utility function to serialize a payment channel authorization message for the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/PayChan.h", + "functions": [ + { + "args": [ + "msg", + "key", + "amt" + ], + "lineno": 9, + "name": "serializePayChanAuthorization" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/PayChan.h.ai.md b/include/xrpl/protocol/PayChan.h.ai.md new file mode 100644 index 0000000000..48ed9e1bad --- /dev/null +++ b/include/xrpl/protocol/PayChan.h.ai.md @@ -0,0 +1,48 @@ +# `include/xrpl/protocol/PayChan.h` + +This file provides the single canonical function for producing the signed payload used in XRPL payment channel claim authorization. Its entire purpose is to ensure that every actor in the system — the channel sender constructing an off-ledger authorization, the RPC layer verifying one, and the ledger transaction engine validating a submitted claim — all sign and verify against exactly the same byte sequence. + +## Payment Channel Authorization Model + +Payment channels in XRPL allow a sender to lock up XRP that a counterparty (the recipient) can later claim in pieces by presenting a cryptographic authorization. Each authorization attests: "I, the channel owner, permit the recipient to redeem up to *N* drops from channel *C*." The security of this scheme depends entirely on the signed message being unambiguously bound to both a specific channel and a specific amount. A poorly formed message could be replayed across channels or re-interpreted for a different amount. + +`serializePayChanAuthorization` produces that canonical, unambiguous payload. + +## Design of `serializePayChanAuthorization` + +```cpp +inline void +serializePayChanAuthorization(Serializer& msg, uint256 const& key, XRPAmount const& amt) +{ + msg.add32(HashPrefix::paymentChannelClaim); + msg.addBitString(key); + msg.add64(amt.drops()); +} +``` + +The function writes three fields into a `Serializer` in strict order: + +1. **`HashPrefix::paymentChannelClaim` (`'C','L','M',0x00`)** — A 4-byte domain-separation prefix. Every category of signable object in XRPL uses a distinct `HashPrefix` so that a valid signature for one type of object can never be confused with a valid signature for another. The `paymentChannelClaim` prefix is `0x434C4D00`, constructed at compile time by `detail::make_hash_prefix`. This prefix is protocol-defined and immutable; changing it would break all existing payment channel authorizations. + +2. **`key` (the channel's `uint256` keylet)** — The 256-bit identifier of the specific payment channel ledger object. This binds the authorization to one channel only; an authorization cannot be replayed against a different channel even if the amounts and keys happen to match. + +3. **`amt.drops()`** — The authorized cumulative amount in drops (the smallest XRP unit), serialized as a 64-bit integer. This is the *ceiling* the recipient is permitted to claim; the on-ledger claim validation checks the running balance does not exceed it. + +The function is marked `inline` because it is defined in a header and called from multiple translation units (`ChannelAuthorize.cpp`, `ChannelVerify.cpp`, `PaymentChannelClaim.cpp`, and tests). The inline definition avoids link-time duplication without requiring a separate `.cpp`. + +## Where It Is Called + +**`ChannelAuthorize.cpp`** (RPC handler) — The channel sender calls this to construct the message before signing with their private key. The resulting signature is returned to the caller for out-of-band delivery to the recipient. + +**`ChannelVerify.cpp`** (RPC handler) — The recipient (or any third party) calls this to reconstruct the same message and verify the sender's signature before trusting the authorization. + +**`PaymentChannelClaim.cpp`** (transaction preflight) — When the recipient submits a `PaymentChannelClaim` transaction on-ledger, the transaction engine calls this during preflight to verify the embedded authorization signature is valid for the claimed channel and amount. The keylet is derived from the transaction's `sfChannel` field. + +The fact that all three call sites use exactly the same function is the key invariant. Any drift — even a byte-order difference — would cause signatures produced by the RPC layer to fail ledger validation. Centralizing the serialization in a single header function prevents that class of bug entirely. + +## Dependencies + +- **`Serializer`** — Provides `add32`, `addBitString`, and `add64` with well-defined endianness, ensuring consistent byte layout across platforms. +- **`HashPrefix`** — Supplies the domain-separation tag; `paymentChannelClaim` is the specific tag defined for this purpose. +- **`XRPAmount`** — Wraps the drop count as a typed integer; calling `.drops()` extracts the raw `int64_t` for serialization. +- **`base_uint.h`** — Provides the `uint256` type used for the channel keylet. \ No newline at end of file diff --git a/include/xrpl/protocol/Permissions.h.ai.json b/include/xrpl/protocol/Permissions.h.ai.json new file mode 100644 index 0000000000..91c3ef627b --- /dev/null +++ b/include/xrpl/protocol/Permissions.h.ai.json @@ -0,0 +1,83 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 23, + "name": "Permission" + } + ], + "description": "Defines granular and transaction-level permission types, a Permission singleton class for managing and querying permission mappings, and related enums for XRPL transaction permissions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Permissions.h", + "functions": [ + { + "args": [], + "lineno": 46, + "name": "getInstance" + }, + { + "args": [ + "value" + ], + "lineno": 52, + "name": "getPermissionName" + }, + { + "args": [ + "name" + ], + "lineno": 55, + "name": "getGranularValue" + }, + { + "args": [ + "value" + ], + "lineno": 58, + "name": "getGranularName" + }, + { + "args": [ + "gpType" + ], + "lineno": 61, + "name": "getGranularTxType" + }, + { + "args": [ + "txType" + ], + "lineno": 64, + "name": "getTxFeature" + }, + { + "args": [ + "permissionValue", + "rules" + ], + "lineno": 67, + "name": "isDelegable" + }, + { + "args": [ + "type" + ], + "lineno": 71, + "name": "txToPermissionType" + }, + { + "args": [ + "value" + ], + "lineno": 74, + "name": "permissionToTxType" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Permissions.h.ai.md b/include/xrpl/protocol/Permissions.h.ai.md new file mode 100644 index 0000000000..425b11f9ca --- /dev/null +++ b/include/xrpl/protocol/Permissions.h.ai.md @@ -0,0 +1,74 @@ +# `include/xrpl/protocol/Permissions.h` + +## Purpose + +This header is the central definition point for XRPL's permission delegation system, which underpins the `DelegateSet` transaction type. It enables an account owner to delegate specific transaction-signing authority to another account in a controlled, fine-grained way — without handing over full account control. The design problem it solves is distinguishing *which* capabilities are delegated: a coarse grant ("the delegate may submit any `TrustSet`") versus a surgical grant ("the delegate may only *freeze* trustlines, not authorize them"). + +--- + +## Two-Tier Permission Model + +The system maintains a strict numeric partition between two kinds of permissions, both represented as `uint32_t` values stored in `sfPermissionValue` on-ledger: + +**Transaction-level permissions** cover an entire transaction type. The encoding is `TxType + 1`, implemented by the static helpers `txToPermissionType()` and `permissionToTxType()`. The `+1` shift ensures zero is never a valid permission value. Because all `TxType` values fit within 16 bits (`uint16_t`), transaction-level permissions always fall in the range `[1, UINT16_MAX]`. + +**Granular permissions** cover sub-operations within a transaction type. Their values always exceed `UINT16_MAX` — the minimum value is `65537`. This is not merely a convention: it is asserted in the `Permission` constructor using `XRPL_ASSERT` for every entry in `granularPermissionMap_`. The numeric gap between the two ranges allows disambiguation without a type tag in the stored value. + +--- + +## `GranularPermissionType` Enum + +The `GranularPermissionType` enum is generated entirely from `detail/permissions.macro` using the X-macro pattern: + +```cpp +#define PERMISSION(type, txType, value) type = value, +#include +``` + +The macro file defines twelve granular permissions (as of this writing), grouped by parent transaction type. For example, `TrustlineAuthorize`, `TrustlineFreeze`, and `TrustlineUnfreeze` all map to `ttTRUST_SET`, while `AccountDomainSet`, `AccountEmailHashSet`, `AccountMessageKeySet`, `AccountTransferRateSet`, and `AccountTickSizeSet` all map to `ttACCOUNT_SET`. This is significant because `AccountSet` itself is marked `notDelegable` at the transaction level — you cannot delegate broad `AccountSet` authority — but specific account-property mutations are permitted as granular grants. + +The X-macro pattern is used twice: here in the header to generate the enum, and again five times in `Permissions.cpp` to build runtime lookup tables. Because the same `.macro` file drives all instantiations, adding a new granular permission requires only a single new `PERMISSION(...)` line. + +--- + +## `Delegation` Enum + +```cpp +enum Delegation { delegable, notDelegable }; +``` + +This simple tag appears in `detail/transactions.macro` as the fourth parameter of every `TRANSACTION(...)` entry. It statically encodes the policy decision of whether a transaction type is safe to delegate in bulk. Sensitive transaction types — `ttACCOUNT_SET`, `ttREGULAR_KEY_SET` — are `notDelegable`, while most operational types (`ttPAYMENT`, `ttESCROW_CREATE`, `ttOFFER_CREATE`, etc.) are `delegable`. + +--- + +## `Permission` Singleton + +`Permission` is a Meyer's singleton (constructed on first call to `getInstance()`). Its private constructor populates five immutable `unordered_map` tables, all driven by the same two macro files: + +- **`txFeatureMap_`** (`TxType → uint256`): Maps each transaction type to its enabling amendment hash. A zero `uint256` means the transaction requires no amendment — it is always available. The map is populated from `transactions.macro` using the `amendment` parameter. +- **`delegableTx_`** (`TxType → Delegation`): Maps each transaction type to its `delegable`/`notDelegable` tag, also from `transactions.macro`. +- **`granularPermissionMap_`** (`string → GranularPermissionType`): Bidirectional name lookup, string side. Used during JSON parsing to convert `"TrustlineFreeze"` → `65538`. +- **`granularNameMap_`** (`GranularPermissionType → string`): The reverse direction. Used during serialization to convert `65538` → `"TrustlineFreeze"`. +- **`granularTxTypeMap_`** (`GranularPermissionType → TxType`): Maps each granular permission to its parent transaction type. Used at execution time to determine which transactor context is relevant. + +After construction the maps are read-only, so all concurrent reads from transaction processing threads are safe without locking. + +--- + +## Key Methods + +**`getPermissionName(uint32_t value)`** first tries to resolve the value as a `GranularPermissionType` via `granularNameMap_`. If that fails, it decodes the value as a transaction-level permission using `permissionToTxType()` and looks up the human-readable name from `TxFormats`. This unified lookup is what lets `STUInt32::getText()` and `STUInt32::getJson()` render any `sfPermissionValue` field as a string instead of a raw number. + +**`isDelegable(uint32_t permissionValue, Rules const& rules)`** is the gatekeeper called by `DelegateSet` validation. Its logic has three layers: (1) if the value maps to a granular permission, it is always delegable — granular permissions are inherently restricted so there is no need to further qualify them; (2) for transaction-level permissions, the associated amendment must be enabled in the current ruleset — a delegate cannot be granted authority for a transaction type that isn't active on the network; (3) the transaction type must be marked `delegable` in `transactions.macro`. + +**`getTxFeature(TxType)`** returns the amendment required by a transaction type, or `nullopt` if none. The assertion inside guards against calling this with a type absent from `txFeatureMap_`, which would represent a programming error (a transaction that exists in one macro file but not the other). + +--- + +## Usage Across the Codebase + +The permission system integrates at three distinct layers: + +- **Submission validation** (`DelegateSet.cpp`): Iterates the `sfPermissions` array and calls `isDelegable()` on each value. Duplicate values are also rejected. This ensures that only legally delegable, amendment-enabled permissions can be stored on-ledger. +- **Execution** (`DelegateUtils.cpp`): `checkTxPermission()` verifies coarse transaction-level authorization by comparing `tx.getTxnType() + 1` against the stored permission array. `loadGranularPermission()` builds a set of active granular permissions for the relevant `TxType`, which the transactor then consults to allow or deny specific sub-operations. +- **Serialization** (`STInteger.cpp`, `STParsedJSON.cpp`): When serializing an `sfPermissionValue` to JSON, the numeric value is transparently replaced with its string name. When parsing JSON, a string value for `sfPermissionValue` is resolved back to its numeric representation via `getGranularValue()`. This round-trip ensures developer-facing JSON is always readable. \ No newline at end of file diff --git a/include/xrpl/protocol/Protocol.h.ai.json b/include/xrpl/protocol/Protocol.h.ai.json new file mode 100644 index 0000000000..1647a7d69d --- /dev/null +++ b/include/xrpl/protocol/Protocol.h.ai.json @@ -0,0 +1,96 @@ +{ + "args": [ + { + "lineno": 61, + "name": "percentage" + }, + { + "lineno": 66, + "name": "percentage" + }, + { + "lineno": 71, + "name": "value" + }, + { + "lineno": 71, + "name": "bips" + }, + { + "lineno": 77, + "name": "value" + }, + { + "lineno": 77, + "name": "bips" + }, + { + "lineno": 196, + "name": "seq" + }, + { + "lineno": 199, + "name": "seq" + } + ], + "classes": [], + "description": "Defines protocol-specific constants and utility functions for the XRPL (XRP Ledger) protocol, including transaction size limits, directory limits, NFT and DID field limits, lending parameters, and various helper functions for basis points calculations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Protocol.h", + "functions": [ + { + "args": [ + "percentage" + ], + "lineno": 61, + "name": "percentageToBips" + }, + { + "args": [ + "percentage" + ], + "lineno": 66, + "name": "percentageToTenthBips" + }, + { + "args": [ + "value", + "bips" + ], + "lineno": 71, + "name": "bipsOfValue" + }, + { + "args": [ + "value", + "bips" + ], + "lineno": 77, + "name": "tenthBipsOfValue" + }, + { + "args": [ + "seq" + ], + "lineno": 196, + "name": "isVotingLedger" + }, + { + "args": [ + "seq" + ], + "lineno": 199, + "name": "isFlagLedger" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 86, + "name": "Lending" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Protocol.h.ai.md b/include/xrpl/protocol/Protocol.h.ai.md new file mode 100644 index 0000000000..c650135f71 --- /dev/null +++ b/include/xrpl/protocol/Protocol.h.ai.md @@ -0,0 +1,71 @@ +# `include/xrpl/protocol/Protocol.h` — XRPL Protocol Constants and Utilities + +## Purpose and Role + +`Protocol.h` is the canonical source of truth for every hard-coded numeric limit and protocol constant in the XRP Ledger. It exists as a deliberate single point of definition: any value that, if changed silently, would create a **hard fork** — a ledger state disagreement between nodes running different software versions — belongs here. The file's own doxygen note makes this explicit: altering these values without pairing them with an amendment-gated detection mechanism will split the network. + +Because it is a header-only file (with the trivial exception of two small helper functions whose implementations live in `Protocol.cpp`), all constants are `constexpr`, available at compile time, and inlined everywhere they are used. + +## Dependency Chain + +The file includes three lightweight headers: +- `ByteUtilities.h` — provides `megabytes()` and `kilobytes()`, pure `constexpr` template functions used to express `txMaxSizeBytes` in human-readable units rather than the raw integer `1048576`. +- `base_uint.h` — brings in `uint256`, used as the typedef basis for `TxID`. +- `Units.h` — provides the `Bips` and `TenthBips` strongly-typed value wrappers, along with their concrete aliases (`Bips32`, `TenthBips16`, etc.) and arithmetic concepts. All fee-rate constants depend on these types. + +## Transaction and Ledger Boundary Constants + +The most fundamental constants are the transaction size bounds: `txMinSizeBytes = 32` and `txMaxSizeBytes = megabytes(1)`. These bound how much data a single transaction may carry on the wire; the lower bound prevents trivially malformed objects from entering validation, while the 1 MB cap protects node memory and network bandwidth. + +`LedgerIndex` is typedefed as `std::uint32_t`, giving the ledger sequence a clear, named type rather than a raw integer — making function signatures self-documenting wherever ledger positions are passed. `TxID` aliases `uint256`, emphasising that a transaction identifier is a 256-bit hash, not an opaque blob. + +`FLAG_LEDGER_INTERVAL = 256` drives the two helper functions `isFlagLedger()` and `isVotingLedger()`, both implemented identically in `Protocol.cpp` as `seq % FLAG_LEDGER_INTERVAL == 0`. Flag ledgers are the points at which network-wide fee and reserve voting takes effect; the fact that both functions share the same implementation body documents that voting happens on flag ledger boundaries. + +## Ledger-Object Structural Limits + +Several constants constrain the internal structure of ledger objects: + +- `dirNodeMaxEntries = 32` — each page of an owner directory or offer directory holds at most 32 entries. This keeps page-traversal O(32) per node hop. +- `dirNodeMaxPages = 262144` — a historical cap on total directory pages, superseded by the `fixDirectoryLimit` amendment. It remains in the header as a documented artifact, illustrating that some constants are vestigial once network amendments retire them. +- `dirMaxTokensPerPage = 32` — mirrors `dirNodeMaxEntries` for NFT pages. +- `oversizeMetaDataCap = 5200` — the maximum number of metadata entries a single transaction may produce. Transactions touching too many objects are rejected before their metadata can grow unbounded. +- `maxDeletableDirEntries = 1000` — limits how many owner-directory entries an account may have before it becomes un-deletable. This prevents account deletion from consuming excessive compute in a single transaction. + +## Offer and NFT Cleanup Limits + +`unfundedOfferRemoveLimit = 1000` and `expiredOfferRemoveLimit = 256` cap the number of stale offers that may be cleaned up opportunistically during a single transaction pass. These limits are an explicit **performance tradeoff**: cleaning up more offers per pass reduces ledger bloat, but processing too many in a single transaction makes that transaction expensive and unpredictable in execution time. The asymmetry between 1000 and 256 reflects that unfunded-offer removal was designed to handle larger batches; the lower cap for expired offers reflects their different discovery path. + +`maxTokenOfferCancelCount = 500` and `maxDeletableTokenOfferEntries = 500` apply the same principle to NFT offer cancellation and NFT burning, respectively — burning an NFT requires cleaning up all of its associated offers first, so an NFT with more than 500 live offers cannot be burned. + +## Basis Points Arithmetic System + +The header establishes a small but important type-safe arithmetic system around basis points (bips). Rather than manipulating raw integers that could silently represent percentages, drops, or fee-levels, the code uses `Bips` and `TenthBips` wrappers from `Units.h`. + +Two compile-time constants anchor the system: +```cpp +Bips32 constexpr bipsPerUnity(100 * 100); // 10,000 bps = 100% +TenthBips32 constexpr tenthBipsPerUnity(100'000); // 100,000 tenth-bps = 100% +``` +Both are validated by `static_assert`, making any accidental mis-initialisation a compile error rather than a runtime bug. + +Four `constexpr` helper functions build on these anchors: +- `percentageToBips(p)` and `percentageToTenthBips(p)` convert an integer percentage into the typed unit. +- `bipsOfValue(value, bips)` and `tenthBipsOfValue(value, bips)` compute a proportional share of a `value` in the appropriate unit. Both are plain integer-division templates, making them usable with any numeric type `T`. + +This design was chosen over floating-point arithmetic deliberately: all ledger calculations involving fees, rates, and percentages must be deterministic and reproducible on every validator. Integer basis-point arithmetic guarantees bit-identical results across platforms. + +## Lending Sub-namespace + +The `Lending` namespace groups rate limits specific to the on-ledger lending protocol. Every rate — management fee, coverage, interest, late interest, close interest, overpayment interest — is capped at 100% (expressed as `percentageToTenthBips(100)` = `TenthBips32(100'000)`), except `maxManagementFeeRate`, which is capped at 10% and stored as a `TenthBips16` (narrower type, since 10,000 fits in `uint16_t`). + +Two transaction-execution constants control `LoanPay` batching behaviour: +- `loanPaymentsPerFeeIncrement = 5` — one base fee unit is charged per five estimated payments. This is documented as "chosen arbitrarily" and amendment-locked once released. +- `loanMaximumPaymentsPerTransaction = 100` — a hard cap on how many payments a single `LoanPay` will actually process, independent of the fee estimate. The comment explicitly warns that the cap and the fee estimate are decoupled, so a poorly constructed transaction could be charged for more payments than it actually processes. Requiring `loanMaximumPaymentsPerTransaction` to be a multiple of `loanPaymentsPerFeeIncrement` is a documented invariant, not enforced at compile time. + +## Miscellaneous Field-Length and Feature Limits + +The remaining constants cap the byte length of variable-length fields for newer ledger object types: NFT URIs, DID documents and URIs, domain fields, Oracle provider strings and symbol classes, credential types, and MPToken metadata. All are set to 256 bytes except `maxCredentialTypeLength` (64 bytes) and `maxMPTokenMetadataLength` (1024 bytes). The `maxMPTokenAmount` constant (`0x7FFF'FFFF'FFFF'FFFF`) is immediately validated against `Number::maxRep` via `static_assert`, ensuring that the XRPL numeric type can represent every valid MPToken quantity. + +Vault-related constants (`vaultStrategyFirstComeFirstServe`, `vaultDefaultIOUScale`, `vaultMaximumIOUScale`, `maxAssetCheckDepth`) govern the Vault feature's IOU-to-share conversion scale (6–18) and recursive depth limit for nested vault assets (5 levels). + +`maxBatchTxCount = 8` caps how many transactions can be submitted as a single atomic batch, directly bounding the worst-case compute cost for batch validation. \ No newline at end of file diff --git a/include/xrpl/protocol/PublicKey.h.ai.json b/include/xrpl/protocol/PublicKey.h.ai.json new file mode 100644 index 0000000000..b4fae0645e --- /dev/null +++ b/include/xrpl/protocol/PublicKey.h.ai.json @@ -0,0 +1,217 @@ +{ + "args": [ + { + "lineno": 61, + "name": "os" + }, + { + "lineno": 61, + "name": "pk" + }, + { + "lineno": 66, + "name": "lhs" + }, + { + "lineno": 66, + "name": "rhs" + }, + { + "lineno": 77, + "name": "h" + }, + { + "lineno": 93, + "name": "type" + }, + { + "lineno": 112, + "name": "sig" + }, + { + "lineno": 132, + "name": "slice" + }, + { + "lineno": 135, + "name": "publicKey" + }, + { + "lineno": 142, + "name": "digest" + }, + { + "lineno": 142, + "name": "mustBeFullyCanonical" + }, + { + "lineno": 149, + "name": "m" + }, + { + "lineno": 153, + "name": "PublicKey" + }, + { + "lineno": 157, + "name": "pk" + }, + { + "lineno": 160, + "name": "address" + }, + { + "lineno": 160, + "name": "publicKey" + }, + { + "lineno": 160, + "name": "id" + }, + { + "lineno": 179, + "name": "v" + }, + { + "lineno": 179, + "name": "field" + } + ], + "classes": [ + { + "args": [ + "PublicKey(const& other)", + "operator=(const PublicKey& other)", + "PublicKey(Slice const& slice)" + ], + "lineno": 27, + "name": "PublicKey" + } + ], + "description": "Defines the PublicKey class and related functions for handling XRPL public keys, including creation, serialization, comparison, hashing, signature verification, and type detection.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/PublicKey.h", + "functions": [ + { + "args": [ + "os", + "pk" + ], + "lineno": 61, + "name": "operator<<" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 66, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 71, + "name": "operator<" + }, + { + "args": [ + "h", + "pk" + ], + "lineno": 77, + "name": "hash_append" + }, + { + "args": [ + "type", + "pk" + ], + "lineno": 93, + "name": "toBase58" + }, + { + "args": [ + "sig" + ], + "lineno": 112, + "name": "ecdsaCanonicality" + }, + { + "args": [ + "slice" + ], + "lineno": 132, + "name": "publicKeyType" + }, + { + "args": [ + "publicKey" + ], + "lineno": 135, + "name": "publicKeyType" + }, + { + "args": [ + "publicKey", + "digest", + "sig", + "mustBeFullyCanonical" + ], + "lineno": 142, + "name": "verifyDigest" + }, + { + "args": [ + "publicKey", + "m", + "sig" + ], + "lineno": 149, + "name": "verify" + }, + { + "args": [ + "PublicKey" + ], + "lineno": 153, + "name": "calcNodeID" + }, + { + "args": [ + "pk" + ], + "lineno": 157, + "name": "calcAccountID" + }, + { + "args": [ + "address", + "publicKey", + "id" + ], + "lineno": 160, + "name": "getFingerprint" + }, + { + "args": [ + "v", + "field" + ], + "lineno": 179, + "name": "getOrThrow" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 177, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/PublicKey.h.ai.md b/include/xrpl/protocol/PublicKey.h.ai.md new file mode 100644 index 0000000000..3e91042acb --- /dev/null +++ b/include/xrpl/protocol/PublicKey.h.ai.md @@ -0,0 +1,50 @@ +# `include/xrpl/protocol/PublicKey.h` + +## Role and Purpose + +This header defines the `PublicKey` class — the canonical representation of a public key anywhere in the XRPL protocol stack. Because XRPL supports two distinct elliptic curve cryptosystems (secp256k1 and Ed25519), every public key must carry its algorithm identity alongside the raw bytes. This file provides the type-safe wrapper that enforces that invariant, plus the full suite of operations that operate on public keys: algorithm detection, signature verification, ECDSA canonicality analysis, and derivation of both network node identities and on-ledger account identities. + +## The `PublicKey` Class + +`PublicKey` is an immutable, fixed-size value type holding exactly 33 bytes. The size is not arbitrary — secp256k1 compressed public keys are 33 bytes (a sign byte `0x02`/`0x03` followed by the 32-byte X coordinate), and Ed25519 keys are padded to 33 bytes by prepending the constant prefix `0xED`. This uniform size is a deliberate encoding decision: it makes the algorithm self-describing from the lead byte alone, enabling `publicKeyType()` to detect the cryptosystem in O(1) with no external metadata. + +The default constructor is explicitly deleted. `PublicKey` can only be constructed from a `Slice`, and the constructor validates the input by calling `publicKeyType()` before copying bytes into the internal buffer — if the slice does not match a known key format, it calls `LogicError`, which terminates under normal build configurations. This means any live `PublicKey` object is always a well-formed, algorithm-identified key; there is no "empty" or "default" state. Copy construction and assignment are provided (both using `std::memcpy` over the fixed 33-byte buffer) with an explicit self-assignment guard. + +The class exposes a `data()`/`size()` pair, iterator range via `begin()`/`end()`, and an implicit conversion to `Slice`. The implicit `Slice` conversion is intentional: it allows `PublicKey` to flow naturally into any API expecting raw byte ranges, including the serialization and hashing infrastructure, without explicit casting at every call site. + +## Algorithm Detection via `publicKeyType()` + +The free function `publicKeyType(Slice const&)` is the gateway for all algorithm detection. It checks three conditions: the slice is exactly 33 bytes, and the lead byte is `0xED` (Ed25519), `0x02`, or `0x03` (secp256k1 compressed forms). Anything else returns `std::nullopt`. The overload accepting `PublicKey const&` simply forwards to the slice overload — importantly, the `[[nodiscard]]` attribute on both ensures callers cannot silently ignore a failed type check. + +## ECDSA Signature Canonicality + +The `ECDSACanonicality` enum and `ecdsaCanonicality()` function address a well-known property of ECDSA: for any valid signature `(R, S)`, the tuple `(R, G-S)` is equally valid, where G is the curve group order. This means a transaction signed once actually has two valid signatures, enabling transaction malleability attacks where an adversary modifies the signature bytes while preserving cryptographic validity. + +`ecdsaCanonicality()` first validates the DER-encoded structure of the signature using the internal `sigPart()` helper, which checks the `0x30`/`0x02` DER framing, enforces length bounds (8–72 bytes total), rejects negative-encoded integers, and rejects redundant zero padding. It then extracts R and S as `boost::multiprecision` 264-bit integers (slightly wider than the 256-bit curve, to accommodate the big-endian representation safely) and compares against the curve order G. A signature is *canonical* if both R and S are in `[1, G)`. It is *fully canonical* if additionally `S ≤ G-S` — meaning S lies in the lower half of the group order, making the signature unique. The XRPL by default requires `fullyCanonical` for new transactions, though `verifyDigest()` exposes a `mustBeFullyCanonical` flag to allow relaxed verification in legacy contexts. + +## Signature Verification + +Two verification functions are provided with complementary levels of abstraction. + +`verifyDigest()` accepts a `uint256` pre-hashed digest and is secp256k1-only. It guards with a `LogicError` if called with an Ed25519 key. After checking canonicality, it parses the public key and signature through libsecp256k1's DER parser and calls `secp256k1_ecdsa_verify`. If the signature is only canonical (not fully canonical) and `mustBeFullyCanonical` is false, it normalizes the signature via `secp256k1_ecdsa_signature_normalize` before verifying — this handles the case where the lower-S form was not used. Both functions are marked `noexcept`; errors return `false` rather than throw. + +`verify()` is the higher-level dispatch: it calls `publicKeyType()` to branch on the cryptosystem. For secp256k1, it hashes the message with SHA512-Half and delegates to `verifyDigest()`. For Ed25519, it first checks `ed25519Canonical()` — which validates the S component of the signature against the Ed25519 subgroup order by byte-reversing the little-endian S value to big-endian for comparison — and then calls `ed25519_sign_open()` with `publicKey.data() + 1`, stripping the `0xED` prefix that XRPL adds for key-type tagging but that the underlying Ed25519 library does not understand. + +## Serialization Integration via `STExchange` + +The header provides a full specialization of `STExchange`. This fits into XRPL's typed serialization framework: `STExchange` is the bridge between C++ value types and their serialized `ST*` representations. The specialization allows `PublicKey` to be read from and written into `STBlob` fields in serialized ledger objects and transactions without any conversion boilerplate at call sites using `get<>` and `set<>` from `STExchange.h`. + +## Identity Derivation + +Two functions derive protocol-level identities from a public key: + +- `calcNodeID()` computes a 160-bit `NodeID` using RIPESHA (RIPEMD160 over SHA256 of the raw key bytes) — this is the identifier used for peer-to-peer network routing and consensus tracking. +- `calcAccountID()` derives the on-ledger account address. The implementation lives in `AccountID.cpp` rather than `PublicKey.cpp`; a comment in the header acknowledges this placement is a workaround for header dependency ordering, not a design preference. + +## Base58 Encoding and JSON Parsing + +`toBase58()` encodes the raw key bytes using XRPL's custom Base58Check alphabet with a caller-supplied `TokenType` prefix (typically `NodePublic` for validators or `AccountPublic` for signing keys). The `parseBase58` template specialization validates the decoded bytes through `publicKeyType()` before construction. + +The `Json::getOrThrow` specialization in the `Json` namespace provides flexible deserialization from JSON field values: it first attempts raw hex decoding, then falls back to trying `NodePublic` and `AccountPublic` Base58 encodings in order. This handles the variety of formats that appear in RPC requests and configuration files. + +`getFingerprint()` is a logging utility that formats a human-readable string combining a peer's IP address, optional node public key (as NodePublic Base58), and an optional session ID — used for diagnostic and audit logging of network peer connections. \ No newline at end of file diff --git a/include/xrpl/protocol/Quality.h.ai.json b/include/xrpl/protocol/Quality.h.ai.json new file mode 100644 index 0000000000..a674cd0006 --- /dev/null +++ b/include/xrpl/protocol/Quality.h.ai.json @@ -0,0 +1,103 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 17, + "name": "TAmounts" + }, + { + "args": [], + "lineno": 77, + "name": "Quality" + } + ], + "description": "Defines types and logic for representing and manipulating currency exchange qualities and paired amounts in the XRPL protocol, including the Quality class for offer book calculations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Quality.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 54, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 61, + "name": "operator!=" + }, + { + "args": [ + "q1", + "q2" + ], + "lineno": 210, + "name": "relativeDistance" + }, + { + "args": [ + "amount", + "limit", + "limit_cmp", + "ceil_function", + "roundUp" + ], + "lineno": 229, + "name": "ceil_TAmounts_helper" + }, + { + "args": [ + "amount", + "limit" + ], + "lineno": 246, + "name": "ceil_in" + }, + { + "args": [ + "amount", + "limit", + "roundUp" + ], + "lineno": 256, + "name": "ceil_in_strict" + }, + { + "args": [ + "amount", + "limit" + ], + "lineno": 266, + "name": "ceil_out" + }, + { + "args": [ + "amount", + "limit", + "roundUp" + ], + "lineno": 276, + "name": "ceil_out_strict" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 285, + "name": "composed_quality" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Quality.h.ai.md b/include/xrpl/protocol/Quality.h.ai.md new file mode 100644 index 0000000000..f0bf1d2ecf --- /dev/null +++ b/include/xrpl/protocol/Quality.h.ai.md @@ -0,0 +1,59 @@ +# `include/xrpl/protocol/Quality.h` + +## Purpose and Context + +`Quality.h` defines the core exchange-rate abstraction that powers XRPL's on-ledger decentralized exchange (DEX). Every offer on the order book expresses a willingness to swap one currency for another at some rate; `Quality` is the precise, sortable representation of that rate. The entire offer-crossing engine — deciding which offers are best, scaling partial fills, and composing multi-hop paths — is expressed in terms of `Quality` and its companion type `TAmounts`. + +## `TAmounts`: Typed Amount Pairs + +`TAmounts` is a simple template pair bundling an input amount (`in`) and an output amount (`out`). In the offer-book domain, `in` is always `TakerPays` and `out` is always `TakerGets`. The template parameters are intentionally generic: the codebase instantiates this over `STAmount`, `IOUAmount`, `XRPAmount`, and `MPTAmount` (the latter for multi-purpose token support). `Amounts` is the canonical `TAmounts` alias used by the STAmount-based offer-crossing path. + +`empty()` returns `true` if either side is non-positive — a guard used by the engine to skip exhausted or invalid offers early without further computation. + +## `Quality`: Inverted Floating-Point Rate + +`Quality` wraps a single `uint64_t` (`m_value`) using the same bit layout as `STAmount`: the top 8 bits hold a biased exponent (actual exponent + 100, so valid range is stored as 1–255), and the lower 56 bits hold an unsigned mantissa. The canonical value represents the rate `out/in` (TakerGets / TakerPays), i.e., how much output the taker receives per unit of input — a higher number is better for the taker. + +The critical non-obvious design choice is that the **integer value is stored inverted** relative to the economic concept: a *higher* quality (more favorable rate for the taker) corresponds to a *lower* `uint64_t`. This is why the comparison operators are counterintuitive on the surface: + +```cpp +friend bool operator<(Quality const& lhs, Quality const& rhs) noexcept { + return lhs.m_value > rhs.m_value; // larger integer = worse quality +} +``` + +This inversion is intentional and useful: when offers are stored in a sorted order book keyed on `m_value` as a raw integer (as they are in the ledger's offer directories), ascending integer order corresponds to descending quality, matching the convention that the best offers are processed first from the front of each directory. + +Construction from an `Amounts` pair calls `getRate(out, in)` from `STAmount.h`, which encodes the ratio into the compact floating-point format. The `composed_quality()` free function extends this to two-hop paths by multiplying the two rates via `mulRound` and re-encoding the result. + +## Increment/Decrement: Stepping the Rate Grid + +`operator++` on a `Quality` moves to the *next higher* quality level, which means it **decrements** `m_value` by one integer step. Similarly, `operator--` increments `m_value` to move to the next lower quality. The operators thus navigate the discrete floating-point grid of representable rates one ULP at a time. This is used in offer-book traversal to find the closest acceptable crossing price. The implementation correctly asserts against underflow and overflow with `XRPL_ASSERT`. + +## Scaled Amount Methods: `ceil_in` and `ceil_out` + +These four methods (two base, two strict variants) are the computational heart of offer crossing. Given an `Amounts` pair that describes a full offer, and a limit on one side, they scale the pair down proportionally to respect the limit while preserving the implied quality. + +`ceil_in(amount, limit)` caps the input at `limit`. If `amount.in > limit`, the new output is computed as `limit / rate` (the quality's rate is `out/in`, so dividing limit by the rate gives the proportional output). To prevent floating-point multiplication from synthesizing value out of rounding, the computed output is clamped to the original `amount.out` if the arithmetic produces a larger number. A symmetric clamping rule applies in `ceil_out`. + +The "strict" variants (`ceil_in_strict`, `ceil_out_strict`) differ only in which underlying rounding function they delegate to: the non-strict path uses `divRound`/`mulRound` which historically ignored low-order bits of `STAmount`, while the strict path uses `divRoundStrict`/`mulRoundStrict` which respect all bits and accept an explicit `roundUp` flag. This distinction matters for correctness in scenarios where borderline rounding could influence whether an offer crosses or not. + +## Template Delegation via `ceil_TAmounts_helper` + +All four methods have overloads templated on `` for working with typed amounts rather than raw `STAmount`. Rather than duplicating the logic four times, a single private helper `ceil_TAmounts_helper` performs the type conversion pattern: convert both sides of the `TAmounts` to `STAmount`, call the appropriate `Amounts`-based overload via a function pointer argument, then convert the result back using `toAmount` and `toAmount`. The function pointer type is passed as a template parameter; the optional `bool roundUp` is forwarded via a variadic `std::same_as...` pack, allowing both the strict and non-strict variants to share the same helper without separate instantiations. + +## `round()` and Tick Sizes + +`round(digits)` rounds the quality's mantissa upward to the specified number of significant decimal digits. XRPL's protocol supports a `TickSize` field (valid range `minTickSize=3` to `maxTickSize=16`) on currency issuers and gateways, coarsening the price grid to improve offer-book depth. The implementation uses a precomputed modulus table indexed by digit count and rounds up by adding the modulus minus one before truncating — a standard ceiling-division idiom applied to the 56-bit mantissa. + +## `relativeDistance` and the Encoding + +`relativeDistance` (used only in tests) is a window into the internal representation: it manually extracts the exponent and mantissa from two `Quality` values, scales them to a common exponent, and returns `|a - b| / min(a, b)`. The lambda `exponent = (rate >> 56) - 100` and `mantissa = rate & ~(255ull << 56)` confirm the 8-bit/56-bit split and the +100 exponent bias, matching the `STAmount` encoding documented elsewhere. + +## `QUALITY_ONE` + +The macro `QUALITY_ONE 1'000'000'000` represents the unity rate (one unit of input gives one unit of output, scaled to the 9-decimal-place precision used in XRPL's fixed-point arithmetic). It appears throughout offer parsing and fee calculations wherever a 1:1 exchange rate must be expressed as a raw integer. + +## Relationship to Other Files + +`Quality.h` depends on `STAmount.h` for `amountFromQuality()` and `getRate()`, and on `AmountConversions.h` for the `toSTAmount()` / `toAmount()` adapters that allow typed-amount templates to bridge into the `STAmount`-based computation core. `QualityFunction.h` builds on `Quality` to model the continuous price function of AMM pools, where quality varies with the pool's reserve ratio rather than being a fixed constant. \ No newline at end of file diff --git a/include/xrpl/protocol/QualityFunction.h.ai.json b/include/xrpl/protocol/QualityFunction.h.ai.json new file mode 100644 index 0000000000..4b1e84ea6f --- /dev/null +++ b/include/xrpl/protocol/QualityFunction.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 35, + "name": "quality" + }, + { + "lineno": 38, + "name": "qf" + }, + { + "lineno": 44, + "name": "quality" + }, + { + "lineno": 63, + "name": "amounts" + }, + { + "lineno": 63, + "name": "tfee" + } + ], + "classes": [ + { + "args": [ + "Quality const& quality, CLOBLikeTag", + "TAmounts const& amounts, std::uint32_t tfee, AMMTag" + ], + "lineno": 16, + "name": "QualityFunction" + }, + { + "args": [], + "lineno": 27, + "name": "AMMTag" + }, + { + "args": [], + "lineno": 31, + "name": "CLOBLikeTag" + } + ], + "description": "Defines the QualityFunction class, which models the average quality of a path in the XRPL protocol as a function of output, supporting both AMM and CLOB offer types. Provides methods to combine quality functions, compute output for a given average quality, and check if the function is constant.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/QualityFunction.h", + "functions": [ + { + "args": [ + "qf" + ], + "lineno": 38, + "name": "combine" + }, + { + "args": [ + "quality" + ], + "lineno": 44, + "name": "outFromAvgQ" + }, + { + "args": [], + "lineno": 51, + "name": "isConst" + }, + { + "args": [], + "lineno": 56, + "name": "quality" + }, + { + "args": [ + "amounts", + "tfee", + "AMMTag" + ], + "lineno": 63, + "name": "QualityFunction" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/QualityFunction.h.ai.md b/include/xrpl/protocol/QualityFunction.h.ai.md new file mode 100644 index 0000000000..1a1c140ded --- /dev/null +++ b/include/xrpl/protocol/QualityFunction.h.ai.md @@ -0,0 +1,81 @@ +# `QualityFunction.h` — Path Quality as a Linear Function of Output + +## Role in the System + +When the XRPL payment engine routes a payment through a strand that includes an AMM pool, the effective exchange rate (quality) is not constant — it degrades as more liquidity is consumed from the pool. `QualityFunction` models this relationship analytically, expressing the *average quality* of a path as a linear function of its output amount: `q(out) = m * out + b`. This model lets the engine compute, without simulation, exactly how much output a path can deliver while still satisfying a minimum quality (rate) requirement. + +The class lives at the boundary between AMM mathematics and path optimization. It is not a generic utility; it exists specifically to power the `limitOut()` optimization in `StrandFlow.h`, which caps strand output at the point where further consumption would violate the quality limit. + +## Mathematical Foundation + +For a single AMM step with current pool balances `poolPays` (output side) and `poolGets` (input side) and a trading fee factor `cfee = 1 - tfee`, the constant-product swap formula gives: + +``` +in = [(poolGets * poolPays) / (poolGets - out) - poolPays] / cfee +``` + +Substituting into `q = out / in` and linearising yields the slope and intercept stored in `m_` and `b_`: + +``` +m = -cfee / poolGets (always negative for a valid AMM step) +b = poolGets * cfee / poolGets = cfee * poolPays / poolGets +``` + +The template constructor (`AMMTag`) computes this directly from the `TAmounts` pool snapshot at the moment the step is evaluated, guarded by an explicit throw if either balance is zero — a degenerate pool would produce division-by-zero in downstream arithmetic. + +## Construction: Two Modes via Tag Dispatch + +Two empty tag structs, `AMMTag` and `CLOBLikeTag`, select construction semantics without relying on parameter-type overloading: + +- **`CLOBLikeTag`** produces a *constant* quality function (`m_ = 0`, `b_ = 1 / quality.rate()`), with `quality_` set. This covers two cases: a plain CLOB order whose rate doesn't change with output size, and a multi-path AMM offer — where the AMM offer size scales proportionally with quality just like a CLOB, so the function is effectively flat. Constructing with a zero-rate quality throws immediately, since a zero rate is undefined as a divisor. + +- **`AMMTag`** produces a *variable* quality function with the slope and intercept derived from pool balances. `quality_` is left empty, marking the function as non-constant. + +Tag dispatch here is idiomatic: call sites read clearly (`QualityFunction{q, QualityFunction::CLOBLikeTag{}}`) without needing to inspect argument types. + +## Combining Across Path Steps + +A payment strand can have multiple sequential steps (e.g., a transfer fee step preceding an AMM step). `combine()` composes two quality functions using the linear chain rule: + +```cpp +m_ += b_ * qf.m_; +b_ *= qf.b_; +if (m_ != 0) + quality_ = std::nullopt; +``` + +This is the analytic composition of `q1(out)` and `q2(out)` — the combined function represents the average quality across both steps as a function of the final output. If the incoming QF was constant but the new step introduces a slope, `quality_` is cleared to correctly reflect that the combined function is no longer flat. `StrandFlow.h`'s `limitOut()` calls this in a loop over all strand steps, building up a single QF that represents the entire strand. + +## Inverting the Function: `outFromAvgQ()` + +Given a quality limit `qlim`, the maximum output that still satisfies `q(out) >= qlim` is found by inverting the linear model: + +``` +out = (1 / qlim.rate() - b_) / m_ +``` + +The implementation sets the rounding mode to `upward` before computing the expression. Because `m_` is negative for AMM steps, dividing by it converts an upward-rounded numerator into a downward-rounded result, ensuring the computed output doesn't slightly exceed the limit — a defensive choice to prevent the engine from requesting marginally more output than the quality constraint allows. + +If `m_` is zero (constant quality function) or the result is non-positive (quality limit cannot be reached with positive output), `std::nullopt` is returned. `StrandFlow.h` treats `std::nullopt` or `isConst() == true` as a signal to skip the cap and pass `remainingOut` unchanged. + +## Integration with `StrandFlow.h` + +`limitOut()` in `StrandFlow.h` is the primary consumer: + +```cpp +for (auto const& step : strand) + if (auto [stepQF, dir] = step->getQualityFunc(v, dir); stepQF) + qf ? qf->combine(*stepQF) : qf = stepQF; + +if (!qf || qf->isConst()) + return remainingOut; + +auto out = qf->outFromAvgQ(limitQuality); +return std::min(out, remainingOut); +``` + +Each `BookStep` delegates to `tipOfferQualityF()`, which dispatches to `AMMOffer::getQualityFunc()` or wraps a CLOB offer in a `CLOBLikeTag` function. If any step returns no quality function (e.g., a non-offer step), the optimization is abandoned entirely and `remainingOut` is used unchanged. A tiny tolerance check (`withinRelativeDistance` to 1e-9) guards against floating-point noise producing spurious clipping. + +## Design Tradeoffs + +The linear approximation of AMM quality is exact for the *average* quality (`out / in`) but not for the *marginal* (instantaneous) quality, which is quadratic. The comment in `StrandFlow.h` is explicit about this: "average quality is linear and instant quality is quadratic function of output." Using the average keeps the composition rule simple (linear × linear = linear after algebraic manipulation) and avoids solving quadratics during path selection. The result is a conservative, analytically tractable bound that the engine then enforces precisely when it executes the actual swap. \ No newline at end of file diff --git a/include/xrpl/protocol/RPCErr.h.ai.json b/include/xrpl/protocol/RPCErr.h.ai.json new file mode 100644 index 0000000000..2afd5384bb --- /dev/null +++ b/include/xrpl/protocol/RPCErr.h.ai.json @@ -0,0 +1,29 @@ +{ + "args": [], + "classes": [], + "description": "Declares deprecated RPC error handling functions for the XRPL protocol, including checking for and generating RPC errors.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/RPCErr.h", + "functions": [ + { + "args": [ + "jvResult" + ], + "lineno": 8, + "name": "isRpcError" + }, + { + "args": [ + "iError" + ], + "lineno": 10, + "name": "rpcError" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/RPCErr.h.ai.md b/include/xrpl/protocol/RPCErr.h.ai.md new file mode 100644 index 0000000000..89e2501406 --- /dev/null +++ b/include/xrpl/protocol/RPCErr.h.ai.md @@ -0,0 +1,9 @@ +# `include/xrpl/protocol/RPCErr.h` + +This header is a thin compatibility shim, declaring two explicitly deprecated utility functions for RPC error construction and detection in the `xrpl` namespace. The `// VFALCO NOTE these are deprecated` comment makes the intent unambiguous: these functions exist only to avoid breaking call sites that predate the richer error-handling infrastructure now found in `ErrorCodes.h`. + +`rpcError(error_code_i iError)` constructs a fresh `Json::Value` object and populates it with canonical error fields by delegating to `RPC::inject_error()`. The return-by-value produces a self-contained error object ready for direct return from an RPC handler. The modern replacement is `RPC::make_error()`, which does the same thing under a name that belongs to the `RPC` sub-namespace where the rest of the error machinery lives. + +`isRpcError(Json::Value jvResult)` duck-types a JSON value as an error response by checking for membership of the `jss::error` key — the same structural sentinel that `RPC::contains_error()` uses, making it the direct modern equivalent. Notably, the parameter is taken by value rather than `const` reference, a minor inefficiency that was never corrected given the function's deprecated status. + +Both functions live outside the `RPC` namespace (a design anomaly noted in `ErrorCodes.h` with its own `VFALCO NOTE`), which is precisely why they were superseded. New code should use `RPC::make_error()`, `RPC::inject_error()`, and `RPC::contains_error()` from `ErrorCodes.h` directly. \ No newline at end of file diff --git a/include/xrpl/protocol/Rate.h.ai.json b/include/xrpl/protocol/Rate.h.ai.json new file mode 100644 index 0000000000..b9645efec4 --- /dev/null +++ b/include/xrpl/protocol/Rate.h.ai.json @@ -0,0 +1,145 @@ +{ + "args": [ + { + "lineno": 17, + "name": "rate" + }, + { + "lineno": 27, + "name": "lhs" + }, + { + "lineno": 27, + "name": "rhs" + }, + { + "lineno": 37, + "name": "os" + }, + { + "lineno": 43, + "name": "amount" + }, + { + "lineno": 46, + "name": "roundUp" + }, + { + "lineno": 49, + "name": "asset" + }, + { + "lineno": 63, + "name": "fee" + } + ], + "classes": [ + { + "args": [ + "rate" + ], + "lineno": 13, + "name": "Rate" + } + ], + "description": "Defines the Rate struct representing transfer rates in the XRPL protocol, along with related arithmetic functions and operators for manipulating transfer rates and amounts.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Rate.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 27, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 32, + "name": "operator<" + }, + { + "args": [ + "os", + "rate" + ], + "lineno": 37, + "name": "operator<<" + }, + { + "args": [ + "amount", + "rate" + ], + "lineno": 43, + "name": "multiply" + }, + { + "args": [ + "amount", + "rate", + "roundUp" + ], + "lineno": 46, + "name": "multiplyRound" + }, + { + "args": [ + "amount", + "rate", + "asset", + "roundUp" + ], + "lineno": 49, + "name": "multiplyRound" + }, + { + "args": [ + "amount", + "rate" + ], + "lineno": 52, + "name": "divide" + }, + { + "args": [ + "amount", + "rate", + "roundUp" + ], + "lineno": 55, + "name": "divideRound" + }, + { + "args": [ + "amount", + "rate", + "asset", + "roundUp" + ], + "lineno": 58, + "name": "divideRound" + }, + { + "args": [ + "fee" + ], + "lineno": 63, + "name": "transferFeeAsRate" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 61, + "name": "nft" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Rate.h.ai.md b/include/xrpl/protocol/Rate.h.ai.md new file mode 100644 index 0000000000..680b43d734 --- /dev/null +++ b/include/xrpl/protocol/Rate.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/protocol/Rate.h` — Transfer Rate Type and Arithmetic + +## Purpose + +`Rate.h` defines the `Rate` struct and its associated arithmetic free functions, which together form the protocol-level abstraction for XRPL transfer fees. Whenever an issuer charges a percentage on IOU transfers, or an NFT carries a creator royalty, the engine expresses and applies that fee through this type. The header is thin by design: the struct itself is barely more than a tagged integer, while the computation-heavy work lives in the matching implementation file `Rate2.cpp`. + +## Encoding Convention + +Transfer rates in the XRPL protocol are stored as fractions of one billion. A raw value of `1,000,000,000` means exactly 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, i.e., a 1% fee. This scale matches the `QUALITY_ONE` constant defined in `Quality.h` (`#define QUALITY_ONE 1'000'000'000`), which ties transfer rates directly to the ledger's quality/price representation. Rates are read straight from `sfTransferRate` on account ledger objects; the `transferRate()` helper in `AccountRootHelpers.cpp` returns `parityRate` (the `Rate{QUALITY_ONE}` sentinel) when the field is absent, meaning most accounts simply have no fee. + +The globally defined `parityRate` constant is the critical sentinel. Because it indicates a 1:1 exchange, every arithmetic function in `Rate2.cpp` short-circuits immediately when it detects this value — returning the input `STAmount` unchanged — which avoids the more expensive `STAmount` multiply/divide path for the common case of fee-free transfers. + +## The `Rate` Struct + +`Rate` wraps a single `std::uint32_t` and inherits from `boost::totally_ordered`. This CRTP mixin generates `!=`, `>`, `<=`, and `>=` from just the two manually provided operators (`==` and `<`), keeping the header concise while delivering a fully ordered type. The constructor is `explicit` to prevent accidental implicit conversion from raw integers — a meaningful guard given that rate values look like ordinary large numbers and could be confused with amounts. The default constructor is deleted because a `Rate` with an unspecified value is meaningless: zero would be nonsensical (it fails the `XRPL_ASSERT(rate.value)` guards in all arithmetic functions), and leaving it uninitialised would be silently dangerous. + +## Arithmetic Interface + +Six free functions apply a `Rate` to an `STAmount`: + +- `multiply` / `divide` — exact arithmetic (no rounding control) +- `multiplyRound(amount, rate, roundUp)` / `divideRound(amount, rate, roundUp)` — controlled rounding, preserving the asset type of the input amount +- `multiplyRound(amount, rate, asset, roundUp)` / `divideRound(amount, rate, asset, roundUp)` — controlled rounding with an explicit output asset, used when the result asset differs from the input (e.g., offer crossing through a gateway) + +The two-overload pattern exists because offer crossing in `OfferCreate.cpp` must compute fees where the input and output are different currencies, whereas simple transfer-fee accounting (IOU payment routing in `TokenHelpers.cpp`) always works in the same currency. The implementation converts a `Rate` into an `STAmount` via `detail::as_amount()`, which constructs `{noIssue(), rate.value, -9, false}` — expressing the billion-scale integer as a decimal with exponent −9 so that `STAmount`'s existing multiply/divide infrastructure handles the fixed-point arithmetic correctly. This reuse of `STAmount` arithmetic is intentional: it keeps fee calculation consistent with the same precision model used everywhere else in the ledger engine. + +## NFT Transfer Fees — `nft::transferFeeAsRate` + +NFT transfer fees are stored in a separate field as a `uint16_t` in units of basis points (0–50,000 representing 0%–50%). `nft::transferFeeAsRate()` converts this scale by multiplying by 10,000: + +``` +Rate{static_cast(fee) * 10'000} +``` + +A maximum NFT fee of 50,000 basis points (50%) becomes `500,000,000`, well within `uint32_t` range and below `QUALITY_ONE`. The result is then used directly with `multiply()` in `NFTokenAcceptOffer.cpp` to compute the creator's royalty cut before paying the seller. This conversion is intentionally placed in the `nft` sub-namespace to make the unit difference explicit at the call site — callers working with standard IOU transfer rates (already billion-scaled) should never call this function. + +## Design Notes + +The choice to make `Rate` a struct with a public `value` member rather than a full encapsulating class reflects the XRPL codebase's philosophy of transparency for protocol-level types: the raw value is the on-ledger representation, and code that checks `rate.value != QUALITY_ONE` (as in `OfferCreate.cpp`) is directly comparing against the wire format. This avoids indirection while the `explicit` constructor and deleted default constructor still prevent the most common misuse. The `boost::totally_ordered` mixin and the stream operator make the type behave naturally in sorted containers and log output without additional boilerplate. \ No newline at end of file diff --git a/include/xrpl/protocol/RippleLedgerHash.h.ai.json b/include/xrpl/protocol/RippleLedgerHash.h.ai.json new file mode 100644 index 0000000000..a9d9612223 --- /dev/null +++ b/include/xrpl/protocol/RippleLedgerHash.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines a type alias LedgerHash for uint256 within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/RippleLedgerHash.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/RippleLedgerHash.h.ai.md b/include/xrpl/protocol/RippleLedgerHash.h.ai.md new file mode 100644 index 0000000000..d97341c598 --- /dev/null +++ b/include/xrpl/protocol/RippleLedgerHash.h.ai.md @@ -0,0 +1,27 @@ +# `RippleLedgerHash.h` — Ledger Hash Type Alias + +This file occupies exactly nine lines and exists for a single purpose: establishing `LedgerHash` as a named alias for `uint256` within the `xrpl` namespace. Its brevity belies its importance — every layer of the ledger stack that refers to a ledger by its cryptographic identity uses this type. + +## What It Does and Why + +`LedgerHash` is defined as: + +```cpp +using LedgerHash = uint256; +``` + +where `uint256` is itself `base_uint<256>`, a 256-bit big-endian integer declared in ``. In practice, a ledger hash is the SHA-512/256 digest computed over a serialized `LedgerHeader` (account-state hash, transaction-set hash, sequence number, close time, drop amounts, and the parent ledger hash). The resulting 32-byte value uniquely identifies a closed ledger in the XRP Ledger network. + +## Design Rationale + +The alias serves two purposes that bare `uint256` cannot: + +**Semantic clarity.** Code that stores or passes a ledger hash communicates its intent through the type name rather than relying on comments or variable names. Interfaces like `CanonicalTXSet(LedgerHash const& saltHash)` become self-documenting at the call site. + +**Insulation against future specialization.** `base_uint` accepts an optional `Tag` template parameter precisely to allow distinct types with the same bit-width. By routing all ledger-hash usage through the `LedgerHash` alias, the codebase preserves the option to introduce a tagged variant (e.g., `base_uint<256, struct LedgerHashTag>`) that would be type-incompatible with `uint256` transaction hashes, account IDs, or node IDs — preventing accidental cross-domain substitution — without touching every call site. No such tag is applied today, but the indirection makes the migration zero-friction. + +## Relationship to Surrounding Code + +`LedgerHeader.h` — the struct that actually carries a ledger's metadata — stores its hashes as bare `uint256` members (`hash`, `txHash`, `accountHash`, `parentHash`) rather than through this alias. That inconsistency is a historical artifact; `calculateLedgerHash()` returns `uint256` for the same reason. Higher-level code, such as `CanonicalTXSet`, `LedgerHistory`, `InboundLedgers`, and the consensus layer (`RCLValidations`, `RCLConsensus`), consistently prefers `LedgerHash` when expressing API contracts, which is the intended usage pattern. + +The single `#include ` is intentional minimalism: including the full protocol stack in a header consumed across every ledger-facing subsystem would drive up compile times. `base_uint.h` itself is heavy (it pulls in hashing, endian conversion, and hex utilities), but it is already an unavoidable transitive dependency for virtually all protocol types. \ No newline at end of file diff --git a/include/xrpl/protocol/Rules.h.ai.json b/include/xrpl/protocol/Rules.h.ai.json new file mode 100644 index 0000000000..a03741ed73 --- /dev/null +++ b/include/xrpl/protocol/Rules.h.ai.json @@ -0,0 +1,78 @@ +{ + "args": [ + { + "lineno": 10, + "name": "feature" + }, + { + "lineno": 32, + "name": "presets" + }, + { + "lineno": 44, + "name": "ledger" + }, + { + "lineno": 44, + "name": "current" + }, + { + "lineno": 52, + "name": "digest" + }, + { + "lineno": 52, + "name": "amendments" + }, + { + "lineno": 68, + "name": "r" + } + ], + "classes": [ + { + "args": [ + "std::unordered_set> const& presets" + ], + "lineno": 14, + "name": "Rules" + }, + { + "args": [ + "Rules r" + ], + "lineno": 73, + "name": "CurrentTransactionRulesGuard" + } + ], + "description": "Defines the Rules class and related functions for managing protocol feature flags and rule sets in the XRPL (XRP Ledger) protocol, including feature enablement checks and transaction rule management.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Rules.h", + "functions": [ + { + "args": [ + "feature" + ], + "lineno": 10, + "name": "isFeatureEnabled" + }, + { + "args": [], + "lineno": 65, + "name": "getCurrentTransactionRules" + }, + { + "args": [ + "r" + ], + "lineno": 68, + "name": "setCurrentTransactionRules" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Rules.h.ai.md b/include/xrpl/protocol/Rules.h.ai.md new file mode 100644 index 0000000000..803fe46fcf --- /dev/null +++ b/include/xrpl/protocol/Rules.h.ai.md @@ -0,0 +1,53 @@ +# `include/xrpl/protocol/Rules.h` — Protocol Feature Rules + +## Purpose + +`Rules.h` defines the `Rules` class, the authoritative source-of-truth for which XRPL protocol amendments (feature flags) are active during transaction processing for a given ledger. Every behavioral branch in the transaction engine that depends on a conditionally-enabled feature — fee schedules, new transaction types, bug fixes, math semantics — ultimately gates through a `Rules::enabled()` call. The file also declares the thread-local plumbing (`getCurrentTransactionRules`, `setCurrentTransactionRules`, `CurrentTransactionRulesGuard`) that lets deeply nested code query active rules without passing a `Rules` reference through every call frame. + +## The `Rules` Class + +`Rules` is a value type with value semantics: it is default-copy/move-constructible and assignable. Internally it uses a `std::shared_ptr` (the classic pimpl idiom), which keeps copy cost to a single atomic refcount bump regardless of how many amendments are active. This matters because `Rules` is copied into every `ApplyContext` and flows through the entire transaction-application stack. + +### `Rules::Impl` (implementation detail, in Rules.cpp) + +The private `Impl` holds three members: + +- `set_` — an `unordered_set>` populated from the ledger's `sfAmendments` field. The `hardened_hash` hasher (instead of the lighter `beast::uhash`) is chosen deliberately to resist hash-flooding attacks in production — a set populated from network data should not be DoS-vulnerable through adversarial hash collisions. +- `digest_` — an `optional` holding the digest (state hash key) of the ledger's Amendments object. When present, two `Rules` instances can be compared in O(1) by comparing digests instead of iterating their amendment sets. +- `presets_` — a const reference to a caller-supplied `unordered_set` of always-enabled features. These represent features baked in at genesis or forced on in test/devnet configurations. + +`enabled(feature)` checks `presets_` first (features that are unconditionally on), then falls back to `set_`. The two-tier lookup lets test harnesses inject preset features without touching the ledger state. + +`operator==` on `Impl` is digest-based: two rule sets with no digest are considered equal (both represent the empty genesis state), and two rule sets with differing digests are unequal. A digest mismatch is a fast O(1) check. This makes `Rules` comparison cheap for the diagnostics path without requiring a full set-intersection walk. + +### Construction + +The public constructor `Rules(presets)` builds an empty rule set from a preset collection — intended for the genesis ledger, which has no amendments yet. The private constructor `Rules(presets, digest, amendments)` is used by the factory functions. + +### Factory functions: `makeRulesGivenLedger` + +Declared as `friend` and defined in `src/libxrpl/ledger/ReadView.cpp`, these two overloads construct a `Rules` from a live ledger view: + +```cpp +Rules makeRulesGivenLedger(DigestAwareReadView const& ledger, Rules const& current); +Rules makeRulesGivenLedger(DigestAwareReadView const& ledger, + std::unordered_set> const& presets); +``` + +The implementation reads `keylet::amendments()` from the ledger, extracts the `sfAmendments` vector, and constructs a `Rules` with the digest of the SLE for fast future comparisons. If the amendments SLE is absent (as in the genesis ledger), it returns a preset-only `Rules`. Keeping construction here rather than inside `Rules` itself keeps the ledger-access dependency out of the protocol library and satisfies the layering requirement. + +## Thread-Local Rules and `CurrentTransactionRulesGuard` + +`getCurrentTransactionRules()` and `setCurrentTransactionRules()` maintain a per-thread `optional` via `LocalValue>` — a `boost::thread_specific_ptr`-backed container that avoids static-initialization-order issues by constructing on first use. This design enables deeply-nested transaction processing code to call the freestanding `isFeatureEnabled(feature)` without threading a `Rules` parameter through every function. + +`setCurrentTransactionRules` is not a trivial setter. It also pushes a related side effect: it calls `Number::setMantissaScale(...)` based on whether `featureSingleAssetVault` or `featureLendingProtocol` is active. These amendments unlock a wider `MantissaRange::large` for arithmetic operations. The comment explains the deliberate push strategy: because `Number` operations happen far more often than rule changes, propagating the scale setting at rule-install time (push) is vastly cheaper than rechecking the rule on every arithmetic operation (pull). + +`CurrentTransactionRulesGuard` is a straightforward RAII wrapper: the constructor calls `setCurrentTransactionRules` with the new rules, saving the old value; the destructor restores the old value. It is non-copyable to prevent accidental aliasing. Callers in `applySteps.cpp` and `Transactor.cpp` use this guard to bracket transaction application, ensuring the thread-local state is always restored even on exception paths. + +## `isFeatureEnabled` — Global Convenience Query + +The freestanding `isFeatureEnabled(feature)` delegates to the thread-local current rules and safely returns `false` if no rules are installed (i.e., outside any transaction context). This is the single API used in lower-level protocol code (e.g., `STAmount.cpp`, `AMMHelpers.cpp`) that cannot easily take a `Rules` parameter, trading explicit dependency for convenience. The implicit reliance on thread-local state means callers must ensure `CurrentTransactionRulesGuard` is active on the call stack; calling it outside a transaction context silently returns `false`. + +## Design Notes + +The `Rules()` default constructor is deleted, enforcing that every `Rules` instance carries an explicit preset set — an invariant that prevents accidentally propagating a "no features" state. The `operator!=` is derived from `operator==` rather than independently implemented, keeping equality semantics consistent. The `operator==` is documented as diagnostic-only, acknowledging that the digest shortcut is correct in practice but not a strict semantic equality: two identical amendment sets from different ledgers with the same digest would compare equal even if their presets differed (an assertion guards this case). \ No newline at end of file diff --git a/include/xrpl/protocol/SField.h.ai.json b/include/xrpl/protocol/SField.h.ai.json new file mode 100644 index 0000000000..0a700a9bb5 --- /dev/null +++ b/include/xrpl/protocol/SField.h.ai.json @@ -0,0 +1,185 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 89, + "name": "SField" + }, + { + "args": [ + "private_access_tag_t pat", + "Args&&... args" + ], + "lineno": 254, + "name": "TypedField" + }, + { + "args": [ + "TypedField const& f_" + ], + "lineno": 263, + "name": "OptionaledField" + } + ], + "description": "Defines the SField class and related types for identifying and handling fields in XRPL protocol objects, including serialization type IDs, field codes, and typed fields for protocol serialization.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/SField.h", + "functions": [ + { + "args": [ + "SerializedTypeID id", + "int index" + ], + "lineno": 74, + "name": "field_code" + }, + { + "args": [ + "int id", + "int index" + ], + "lineno": 80, + "name": "field_code" + }, + { + "args": [ + "int fieldCode" + ], + "lineno": 134, + "name": "getField" + }, + { + "args": [ + "std::string const& fieldName" + ], + "lineno": 137, + "name": "getField" + }, + { + "args": [ + "int type", + "int value" + ], + "lineno": 140, + "name": "getField" + }, + { + "args": [ + "SerializedTypeID type", + "int value" + ], + "lineno": 145, + "name": "getField" + }, + { + "args": [], + "lineno": 150, + "name": "getName" + }, + { + "args": [], + "lineno": 155, + "name": "hasName" + }, + { + "args": [], + "lineno": 160, + "name": "getJsonName" + }, + { + "args": [], + "lineno": 165, + "name": "operator Json::StaticString const&" + }, + { + "args": [], + "lineno": 170, + "name": "isInvalid" + }, + { + "args": [], + "lineno": 175, + "name": "isUseful" + }, + { + "args": [], + "lineno": 180, + "name": "isBinary" + }, + { + "args": [], + "lineno": 188, + "name": "isDiscardable" + }, + { + "args": [], + "lineno": 197, + "name": "getCode" + }, + { + "args": [], + "lineno": 200, + "name": "getNum" + }, + { + "args": [], + "lineno": 203, + "name": "getNumFields" + }, + { + "args": [ + "int c" + ], + "lineno": 208, + "name": "shouldMeta" + }, + { + "args": [ + "bool withSigningField" + ], + "lineno": 213, + "name": "shouldInclude" + }, + { + "args": [ + "SField const& f" + ], + "lineno": 218, + "name": "operator==" + }, + { + "args": [ + "SField const& f" + ], + "lineno": 223, + "name": "operator!=" + }, + { + "args": [ + "SField const& f1", + "SField const& f2" + ], + "lineno": 228, + "name": "compare" + }, + { + "args": [], + "lineno": 231, + "name": "getKnownCodeToField" + }, + { + "args": [ + "TypedField const& f" + ], + "lineno": 271, + "name": "operator~" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/SField.h.ai.md b/include/xrpl/protocol/SField.h.ai.md new file mode 100644 index 0000000000..138b753a9a --- /dev/null +++ b/include/xrpl/protocol/SField.h.ai.md @@ -0,0 +1,67 @@ +# `include/xrpl/protocol/SField.h` — Protocol Field Identification and Typed-Field System + +## Role in the System + +`SField.h` defines the complete compile-time type system for identifying and categorizing every data field that appears in XRPL binary-serialized objects: transactions, ledger entries, validations, and transaction metadata. Every piece of protocol data — from a `sfSequence` integer to a `sfSigners` array — is identified by an `SField` singleton. This file is the authoritative source for the field registry, the wire-type catalog, and the metadata flags that govern how each field is captured in transaction metadata on-ledger. + +## `SerializedTypeID` — Wire Types via X-Macro + +The `SerializedTypeID` enum defines the wire types recognized by the binary serialization codec. It is generated from a single `XMACRO` definition using the classic X-macro pattern: the macro is expanded once with `TO_ENUM` to produce enum values and again with `TO_MAP` to populate a `sTypeMap` from string names to integers. This keeps the type list in one place — adding a new serialization type requires only one line in `XMACRO`, and both the enum and the string-lookup map update automatically. + +The encoding reflects XRPL's field encoding on the wire. The "common" types (codes 1–11) share a compact single-nibble representation, while "uncommon" types (16+) require an extra byte. Values 12–13 are reserved gaps, and high-level types (10001–10004) like `STI_TRANSACTION` and `STI_LEDGERENTRY` are container types that cannot be embedded inside other serialized objects. + +## The `field_code` Packing Scheme + +```cpp +inline int field_code(SerializedTypeID id, int index) { + return (safe_cast(id) << 16) | index; +} +``` + +Every `SField` has a single integer `fieldCode` that packs two dimensions into 32 bits: the upper 16 bits encode the `SerializedTypeID` and the lower 16 bits encode the field's position within that type. This combined key is both the lookup key in the registry and the canonical comparison value for serialization ordering. When the serializer writes fields in a canonical order (required for deterministic signatures), it sorts by `fieldCode` — which naturally groups all fields of the same type together and sequences them by their index within that type, exactly as the XRPL binary format specification requires. + +## `SField` — Singleton Field Descriptors + +Each `SField` instance describes one field in the protocol. All fields are created at static-initialization time in `SField.cpp` and live until program termination. Copy, move, and assignment are explicitly deleted — no duplicates can exist. This singleton guarantee is enforced at construction via `XRPL_ASSERT` checks on the `knownCodeToField` and `knownNameToField` maps. + +The key architectural decision is the `private_access_tag_t` pattern. The tag type is defined as a public nested struct in `SField`, but its only definition (the `struct` body) lives in `SField.cpp`. The `.cpp` file then creates a single file-scoped `static SField::private_access_tag_t access` variable. Because the tag's constructor is private to that translation unit, only `SField.cpp` can ever pass an access tag to the `SField` constructors, making it impossible for any other code to construct new `SField` instances — a compile-time enforcement of the singleton factory pattern without needing a traditional registry class. + +Fields are registered at construction into two static `unordered_map` tables — `knownCodeToField` (keyed by packed `fieldCode`) and `knownNameToField` (keyed by string) — enabling O(1) lookup in either direction via `getField()`. All overloads of `getField` converge on the integer-keyed map. + +### Field Metadata Flags (`fieldMeta`) + +Each `SField` carries an `int fieldMeta` bitmask controlling which ledger metadata events should record this field's value: + +- `sMD_ChangeOrig` / `sMD_ChangeNew` — record before/after values on modification +- `sMD_DeleteFinal` — record value at deletion (e.g., `sfPreviousTxnID` and `sfPreviousTxnLgrSeq` use `sMD_DeleteFinal` instead of the full default) +- `sMD_Create` — record value at creation +- `sMD_Always` — record whenever the containing node is touched (used by `sfRootIndex`) +- `sMD_BaseTen` — treat the value as base-10 for display rather than hex (used by MPT amount fields) +- `sMD_PseudoAccount` — signals that a 256-bit hash in this field represents a pseudo-account address (used by `sfAMMID`, `sfVaultID`, `sfLoanBrokerID`) +- `sMD_NeedsAsset` — the `STNumber` field requires an associated asset type before it can be serialized as a ledger object; used for vault/loan financial fields like `sfAssetsAvailable`, `sfDebtTotal` + +The default `sMD_Default` is `sMD_ChangeOrig | sMD_ChangeNew | sMD_DeleteFinal | sMD_Create`. + +### Signing vs. Non-Signing Fields + +The `IsSigning` enum (`yes`/`no`) marks whether a field is included when computing a transaction's signing digest. Fields like `sfTxnSignature`, `sfSigners`, `sfMasterSignature`, `sfSignature`, and `sfCounterpartySignature` are all marked `notSigning` — they are excluded from signature hashing because they are themselves the signature data (or carry auxiliary signatures). This prevents a bootstrap paradox where the signature covers itself. + +### Binary vs. Discardable Fields + +`isBinary()` returns true when `fieldValue < 256`, indicating the field has a valid wire representation and can be serialized to binary. `isDiscardable()` returns true when `fieldValue > 256` — fields like `sfHash` and `sfIndex` have artificially high field values (257, 258) and exist only in the JSON representation; they cannot be round-tripped through binary serialization and are silently dropped during serialization. + +## `TypedField` and `OptionaledField` + +`TypedField` extends `SField` with a compile-time `type` alias, allowing calling code to interact with a field with full knowledge of its C++ type at compile time (e.g., `SF_UINT32` is `TypedField>`). This enables type-safe access patterns in the serialized object API — a `TypedField` can only be used to retrieve an `STAmount` value. + +`OptionaledField` wraps a `const TypedField*` to indicate that the field may be absent. The `operator~` overload on `TypedField` provides a concise syntax for constructing `OptionaledField`, allowing callers to write `~sfAmount` to express "this field is optionally present." + +## The `sfields.macro` Two-Phase Include + +The macro file `include/xrpl/protocol/detail/sfields.macro` is included twice with completely different macro definitions. In `SField.h`, `TYPED_SFIELD` and `UNTYPED_SFIELD` expand to `extern SF_##stiSuffix const sfName` declarations, making all field names visible throughout the codebase as `extern` symbols. In `SField.cpp`, the same macros expand to actual object definitions with constructor invocations. This technique eliminates all duplication between declaration and definition, ensuring every field declaration in the header automatically has exactly one matching definition in the translation unit — adding a new field requires only one line in the macro file. + +The macro strips the `sf` prefix from the field name to produce the human-readable string name stored in `fieldName` (e.g., `sfSequence` → `"Sequence"`), using `std::string_view(#sfName).substr(2)` in `SField.cpp`. + +## Canonical Field Ordering + +`SField::compare()` returns -1, 0, or 1 based on `fieldCode` ordering, and returns 0 (illegal) for any comparison involving a sentinel field with a non-positive code. Because `fieldCode` encodes type in the high bits and field index in the low bits, the natural integer ordering produces the canonical XRPL binary serialization order required for deterministic transaction signing. \ No newline at end of file diff --git a/include/xrpl/protocol/SOTemplate.h.ai.json b/include/xrpl/protocol/SOTemplate.h.ai.json new file mode 100644 index 0000000000..e6e082cad1 --- /dev/null +++ b/include/xrpl/protocol/SOTemplate.h.ai.json @@ -0,0 +1,115 @@ +{ + "args": [ + { + "lineno": 28, + "name": "fieldName" + }, + { + "lineno": 80, + "name": "other" + }, + { + "lineno": 86, + "name": "uniqueFields" + }, + { + "lineno": 86, + "name": "commonFields" + }, + { + "lineno": 120, + "name": "sf" + } + ], + "classes": [ + { + "args": [ + "SField const& fieldName, SOEStyle style", + "TypedField const& fieldName, SOEStyle style, SOETxMPTIssue supportMpt = soeMPTNotSupported" + ], + "lineno": 23, + "name": "SOElement" + }, + { + "args": [ + "SOTemplate&& other", + "std::vector uniqueFields, std::vector commonFields = {}", + "std::initializer_list uniqueFields, std::initializer_list commonFields = {}" + ], + "lineno": 75, + "name": "SOTemplate" + } + ], + "description": "Defines the SOTemplate and SOElement classes used to describe the fields and their metadata for XRPL serialized objects, including field styles and MPT support.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/SOTemplate.h", + "functions": [ + { + "args": [ + "fieldName" + ], + "lineno": 28, + "name": "SOElement::init" + }, + { + "args": [], + "lineno": 56, + "name": "SOElement::sField" + }, + { + "args": [], + "lineno": 61, + "name": "SOElement::style" + }, + { + "args": [], + "lineno": 66, + "name": "SOElement::supportMPT" + }, + { + "args": [], + "lineno": 92, + "name": "SOTemplate::begin" + }, + { + "args": [], + "lineno": 97, + "name": "SOTemplate::cbegin" + }, + { + "args": [], + "lineno": 102, + "name": "SOTemplate::end" + }, + { + "args": [], + "lineno": 107, + "name": "SOTemplate::cend" + }, + { + "args": [], + "lineno": 112, + "name": "SOTemplate::size" + }, + { + "args": [ + "SField const&" + ], + "lineno": 117, + "name": "SOTemplate::getIndex" + }, + { + "args": [ + "SField const& sf" + ], + "lineno": 120, + "name": "SOTemplate::style" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/SOTemplate.h.ai.md b/include/xrpl/protocol/SOTemplate.h.ai.md new file mode 100644 index 0000000000..a5c89a7fc0 --- /dev/null +++ b/include/xrpl/protocol/SOTemplate.h.ai.md @@ -0,0 +1,76 @@ +# `include/xrpl/protocol/SOTemplate.h` + +## Role in the System + +`SOTemplate.h` provides the compile-time/startup schema machinery for XRPL's serialized objects. Every transaction type, ledger entry type, and inner object in the XRP Ledger carries a typed binary representation encoded as an `STObject`. Before any `STObject` instance can validate its fields, serialize itself, or apply defaults, it must consult a schema that answers three questions for each possible field: Is this field present in this object type at all? Is it mandatory or optional? Does it allow default-value encoding? + +This file defines the two building blocks for that schema: `SOElement` (a single field's schema entry) and `SOTemplate` (the complete ordered list of entries for one object type). + +## `SOElement` — A Single Field's Schema Entry + +`SOElement` pairs an `SField` reference with an `SOEStyle` value, optionally augmented with an `SOETxMPTIssue` tag for amount fields. + +```cpp +std::reference_wrapper sField_; +SOEStyle style_; +SOETxMPTIssue supportMpt_ = soeMPTNone; +``` + +The use of `std::reference_wrapper` rather than a raw pointer or a copy is deliberate: `SField` instances are immovable, non-copyable singletons that live for the process lifetime (see `SField.h`), and they are referenced throughout the system. Storing a reference wrapper instead of a pointer communicates the ownership model clearly and allows `SOElement` itself to be stored in `std::vector` — which requires copy/move semantics that `SField` cannot provide. + +The constructor validates the wrapped field immediately via the private `init()` helper: + +```cpp +void init(SField const& fieldName) const { + if (!sField_.get().isUseful()) + Throw(...); +} +``` + +`isUseful()` returns true only if `fieldCode > 0`, meaning the field is a known, named, serializable field — not the sentinel `sfInvalid` or `sfGeneric`. This guard prevents accidentally inserting placeholder fields into a live schema. + +A second constructor is constrained to `STAmount` and `STIssue` typed fields only (via a C++20 `requires` clause) and takes an additional `SOETxMPTIssue` argument. This enforces that MPT support annotations can only be applied to fields that actually carry amounts or issue specifiers, not arbitrary field types. + +## `SOEStyle` — Field Presence Semantics + +```cpp +enum SOEStyle { + soeINVALID = -1, + soeREQUIRED = 0, // must be present + soeOPTIONAL = 1, // may be absent; if present, may carry the type's default value + soeDEFAULT = 2, // may be absent; if present, must NOT carry the type's default value +}; +``` + +The distinction between `soeOPTIONAL` and `soeDEFAULT` is subtle and important. For some fields, the presence of the field with its default value and the absence of the field have different protocol-level meanings (e.g., `QualityIn` on a trust line). `soeDEFAULT` communicates that when the field is present, it is holding meaningful non-default state — the system will serialize and validate it differently. The comment also notes that inner objects with default fields must be created via `STObject::makeInnerObject()`, not directly, to preserve this invariant. + +## `SOETxMPTIssue` — Multi-Purpose Token Awareness + +```cpp +enum SOETxMPTIssue { soeMPTNone, soeMPTSupported, soeMPTNotSupported }; +``` + +This annotation on amount/issue fields records whether the transaction format supports Multi-Purpose Tokens (MPT) in that field. It allows the validation layer in `STObject` to check MPT compatibility at the schema level rather than in scattered per-transaction validation code. Fields that are not amount/issue types get the default `soeMPTNone`, which is never tested. + +## `SOTemplate` — The Immutable Schema Object + +`SOTemplate` holds the merged, ordered list of `SOElement` entries for one object type, plus a compact reverse-lookup index. + +```cpp +std::vector elements_; +std::vector indices_; // field num -> element index +``` + +The constructor (implemented in `SOTemplate.cpp`) takes two separate vectors — `uniqueFields` and `commonFields` — and concatenates them into `elements_`. The split exists because `KnownFormats` separates fields specific to one transaction/object type from fields shared across all formats. After merging, the constructor builds the `indices_` table: a dense array indexed by `SField::getNum()`, pre-sized to `SField::getNumFields() + 1` and initialized to `-1` (unmapped). Each `SOElement`'s field number maps to the element's position in `elements_`. This gives O(1) field lookup in `getIndex()` and `style()` — a critical property since these are called during deserialization and validation of every ledger object. + +The constructor enforces two invariants at initialization time: every field number must be within the valid range, and no field may appear twice in the same template. Both violations throw `std::runtime_error`, catching schema bugs at application startup rather than silently producing corrupt objects later. + +`SOTemplate` is declared move-only (copy constructor and copy-assignment are deleted). The comment explains this clearly: copying a vector of `SOElement`s is expensive, and there is no existing use case that requires it. All consumers hold a `const*` or `const&` to a template owned by the `KnownFormats` registry. + +## Relationship to `KnownFormats` and `STObject` + +The canonical lifecycle is: at startup, `KnownFormats` subclasses (e.g., `TxFormats`, `LedgerFormats`, `InnerObjectFormats`) construct `SOTemplate` objects during singleton initialization, each receiving the `SOElement` list for one object type. These templates are then stored inside `KnownFormats::Item` objects whose addresses never move (stored in a `std::forward_list`). + +At runtime, `STObject` holds a `SOTemplate const* mType` pointer. When an `STObject` is constructed or deserialized with a specific format, it calls `applyTemplate()` or `set(SOTemplate const&)`, which uses the template to validate present fields, fill in absent required fields, and reject unrecognized fields. The `style()` and `getIndex()` methods on `SOTemplate` are the workhorses of this validation pass. + +The result is a schema system that is fully lock-free at runtime (templates are read-only after construction), requires no heap allocation per field lookup, and catches schema errors at process startup rather than during transaction processing. \ No newline at end of file diff --git a/include/xrpl/protocol/STAccount.h.ai.json b/include/xrpl/protocol/STAccount.h.ai.json new file mode 100644 index 0000000000..24484e194b --- /dev/null +++ b/include/xrpl/protocol/STAccount.h.ai.json @@ -0,0 +1,180 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "STAccount" + } + ], + "description": "Defines the STAccount class, a serialized type representing an XRPL AccountID, with serialization, comparison, and assignment operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STAccount.h", + "functions": [ + { + "args": [], + "lineno": 19, + "name": "STAccount" + }, + { + "args": [ + "SField const& n" + ], + "lineno": 21, + "name": "STAccount" + }, + { + "args": [ + "SField const& n", + "Buffer const& v" + ], + "lineno": 22, + "name": "STAccount" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 23, + "name": "STAccount" + }, + { + "args": [ + "SField const& n", + "AccountID const& v" + ], + "lineno": 24, + "name": "STAccount" + }, + { + "args": [], + "lineno": 26, + "name": "getSType" + }, + { + "args": [], + "lineno": 29, + "name": "getText" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 32, + "name": "add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 35, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 38, + "name": "isDefault" + }, + { + "args": [ + "AccountID const& value" + ], + "lineno": 41, + "name": "operator=" + }, + { + "args": [], + "lineno": 44, + "name": "value" + }, + { + "args": [ + "AccountID const& v" + ], + "lineno": 47, + "name": "setValue" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 51, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 53, + "name": "move" + }, + { + "args": [ + "AccountID const& value" + ], + "lineno": 57, + "name": "operator=" + }, + { + "args": [], + "lineno": 62, + "name": "value" + }, + { + "args": [ + "AccountID const& v" + ], + "lineno": 66, + "name": "setValue" + }, + { + "args": [ + "STAccount const& lhs", + "STAccount const& rhs" + ], + "lineno": 71, + "name": "operator==" + }, + { + "args": [ + "STAccount const& lhs", + "STAccount const& rhs" + ], + "lineno": 76, + "name": "operator<" + }, + { + "args": [ + "STAccount const& lhs", + "AccountID const& rhs" + ], + "lineno": 81, + "name": "operator==" + }, + { + "args": [ + "STAccount const& lhs", + "AccountID const& rhs" + ], + "lineno": 85, + "name": "operator<" + }, + { + "args": [ + "AccountID const& lhs", + "STAccount const& rhs" + ], + "lineno": 89, + "name": "operator<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STAccount.h.ai.md b/include/xrpl/protocol/STAccount.h.ai.md new file mode 100644 index 0000000000..d988529ed1 --- /dev/null +++ b/include/xrpl/protocol/STAccount.h.ai.md @@ -0,0 +1,49 @@ +# `STAccount` — Serialized Account ID Type + +## Role and Purpose + +`STAccount` is the XRPL protocol's serialized-type wrapper around a 160-bit account identifier. It lives in the "ST" (Serialized Type) family of classes that form the building blocks of every XRPL transaction and ledger object. Its job is to carry an `AccountID` value through the protocol stack in a form that knows how to serialize itself, compare itself, and report whether it has ever been explicitly set. + +## Inheritance Design + +`STAccount` inherits from two bases simultaneously: + +- **`STBase`** — the abstract root of all serialized types, supplying field-name association, the `Serializer` interface (`add`, `addFieldID`), the virtual dispatch contract (`getSType`, `isEquivalent`, `isDefault`, `getText`), and the small-buffer `emplace` helper used by `copy` / `move`. +- **`CountedObject`** — a CRTP mixin that atomically tracks the number of live `STAccount` instances. Every constructor increment and every destructor decrement goes through a lock-free linked list of counters, giving the node operator diagnostic visibility into object lifetimes without any per-instance overhead beyond what a vtable already costs. + +The `final` specifier signals that no further derivation is expected. + +## Storage Optimization and Serialization Compatibility + +The most architecturally interesting aspect of `STAccount` is captured in a comment at the top of the private section: + +> *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 stays unchanged, we serialize and deserialize like an `STBlob`.* + +So the storage is an `AccountID` (a `base_uint<160>`) — a fixed-size, stack-friendly type with no heap allocation — while the wire format is the variable-length (VL-prefixed) blob encoding that `STBlob` uses. The `add()` method preserves this contract by calling `s.addVL()`, and the `Buffer`-accepting constructor plus the `SerialIter` constructor both parse the incoming bytes as a VL blob before copying the raw bits into the `uint160`. This avoids breaking any existing node on the network while eliminating the internal `std::vector` allocation that `STBlob` would carry. + +## The `default_` Flag + +Alongside `value_`, a `bool default_` tracks whether the account has been explicitly set. This is not merely `value_ == beast::zero`: the zero account (`noAccount()`) is a legal, meaningful XRPL value. `default_` instead encodes *was this field ever assigned*, which drives two behaviors: + +1. **Serialization**: `add()` serializes a default account as a zero-length VL blob, matching the STBlob convention for absent fields, rather than writing 20 bytes of zeros that might be misinterpreted as an explicitly-supplied zero account. +2. **Equivalence**: `isEquivalent()` checks both `default_` and `value_`, so two `STAccount` objects are equivalent only if their "set-ness" and their binary content match. + +`setValue()` (and the assignment operator that delegates to it) unconditionally clears `default_` to `false`, so any explicit assignment — even to the zero account — marks the field as having been set. + +## Constructors + +Four public constructors cover the main use cases: + +- `STAccount()` — default-constructs to zero / unset; used when building an empty STObject slot. +- `STAccount(SField const&)` — names the field but leaves it unset; the SField is mandatory metadata that identifies which account field this is in a transaction or ledger entry. +- `STAccount(SField const&, Buffer const&)` — deserialization path from raw bytes; validates the buffer is exactly 20 bytes and throws `std::runtime_error` otherwise; an empty buffer is accepted and leaves the field in default state. +- `STAccount(SerialIter&, SField const&)` — the primary deserialization constructor, delegates to the `Buffer` form by extracting a VL blob from the iterator. +- `STAccount(SField const&, AccountID const&)` — direct construction from a known account ID, immediately marking the field as set. + +## Operators and Comparisons + +The file exposes a full set of free comparison operators that allow `STAccount` and raw `AccountID` values to be compared interchangeably, covering `operator==` and `operator<` in both orderings (e.g. `AccountID < STAccount`). These are all thin wrappers around the `uint160` comparison operations and are defined `inline` in the header to allow the compiler to optimize them away at call sites. There is no `operator!=` because callers compose it from `!operator==` via standard convention. + +## Private `copy` / `move` Hooks + +`STBase::emplace()` implements a small-buffer optimization: if the object fits within `n` bytes of a caller-supplied stack buffer, it is placement-new'd there; otherwise it falls back to heap allocation. `STAccount` overrides `copy` and `move` to delegate to this mechanism, enabling `detail::STVar` — the discriminated union used inside `STObject` field slots — to embed small ST types on the stack rather than heap-allocating them, which is why `detail::STVar` is declared a `friend`. \ No newline at end of file diff --git a/include/xrpl/protocol/STAmount.h.ai.json b/include/xrpl/protocol/STAmount.h.ai.json new file mode 100644 index 0000000000..340453ca6f --- /dev/null +++ b/include/xrpl/protocol/STAmount.h.ai.json @@ -0,0 +1,334 @@ +{ + "args": [ + { + "lineno": 39, + "name": "sit" + }, + { + "lineno": 39, + "name": "name" + }, + { + "lineno": 44, + "name": "asset" + }, + { + "lineno": 44, + "name": "mantissa" + }, + { + "lineno": 44, + "name": "exponent" + }, + { + "lineno": 44, + "name": "negative" + }, + { + "lineno": 97, + "name": "amount" + }, + { + "lineno": 97, + "name": "issue" + }, + { + "lineno": 101, + "name": "mptIssue" + }, + { + "lineno": 443, + "name": "v" + }, + { + "lineno": 443, + "name": "field" + } + ], + "classes": [ + { + "args": [ + "SerialIter& sit, SField const& name", + "SField const& name, A const& asset, mantissa_type mantissa, exponent_type exponent, bool negative, unchecked", + "A const& asset, mantissa_type mantissa, exponent_type exponent, bool negative, unchecked", + "SField const& name, A const& asset, mantissa_type mantissa = 0, exponent_type exponent = 0, bool negative = false", + "SField const& name, std::int64_t mantissa", + "SField const& name, std::uint64_t mantissa = 0, bool negative = false", + "std::uint64_t mantissa = 0, bool negative = false", + "SField const& name, STAmount const& amt", + "A const& asset, std::uint64_t mantissa = 0, int exponent = 0, bool negative = false", + "A const& asset, std::uint32_t mantissa, int exponent = 0, bool negative = false", + "A const& asset, std::int64_t mantissa, int exponent = 0", + "A const& asset, int mantissa, int exponent = 0", + "A const& asset, Number const& number", + "IOUAmount const& amount, Issue const& issue", + "XRPAmount const& amount", + "MPTAmount const& amount, MPTIssue const& mptIssue" + ], + "lineno": 18, + "name": "STAmount" + }, + { + "args": [ + "explicit unchecked() = default" + ], + "lineno": 38, + "name": "unchecked" + } + ], + "description": "Defines the STAmount class and related functions for representing and manipulating amounts (XRP, IOU, MPT) in the XRPL protocol, including arithmetic, serialization, and rounding utilities.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STAmount.h", + "functions": [ + { + "args": [ + "rate" + ], + "lineno": 210, + "name": "amountFromQuality" + }, + { + "args": [ + "asset", + "amount" + ], + "lineno": 213, + "name": "amountFromString" + }, + { + "args": [ + "name", + "v" + ], + "lineno": 215, + "name": "amountFromJson" + }, + { + "args": [ + "result", + "jvSource" + ], + "lineno": 217, + "name": "amountFromJsonNoThrow" + }, + { + "args": [ + "a" + ], + "lineno": 221, + "name": "toSTAmount" + }, + { + "args": [ + "value" + ], + "lineno": 324, + "name": "isLegalNet" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 329, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 330, + "name": "operator<" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 332, + "name": "operator!=" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 336, + "name": "operator>" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 340, + "name": "operator<=" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 344, + "name": "operator>=" + }, + { + "args": [ + "value" + ], + "lineno": 348, + "name": "operator-" + }, + { + "args": [ + "v1", + "v2" + ], + "lineno": 353, + "name": "operator+" + }, + { + "args": [ + "v1", + "v2" + ], + "lineno": 354, + "name": "operator-" + }, + { + "args": [ + "v1", + "v2", + "asset" + ], + "lineno": 356, + "name": "divide" + }, + { + "args": [ + "v1", + "v2", + "asset" + ], + "lineno": 358, + "name": "multiply" + }, + { + "args": [ + "v1", + "v2", + "asset", + "roundUp" + ], + "lineno": 361, + "name": "mulRound" + }, + { + "args": [ + "v1", + "v2", + "asset", + "roundUp" + ], + "lineno": 364, + "name": "mulRoundStrict" + }, + { + "args": [ + "v1", + "v2", + "asset", + "roundUp" + ], + "lineno": 367, + "name": "divRound" + }, + { + "args": [ + "v1", + "v2", + "asset", + "roundUp" + ], + "lineno": 370, + "name": "divRoundStrict" + }, + { + "args": [ + "offerOut", + "offerIn" + ], + "lineno": 374, + "name": "getRate" + }, + { + "args": [ + "value", + "scale", + "rounding" + ], + "lineno": 386, + "name": "roundToScale" + }, + { + "args": [ + "asset", + "value" + ], + "lineno": 401, + "name": "roundToAsset" + }, + { + "args": [ + "asset", + "value", + "scale", + "rounding" + ], + "lineno": 414, + "name": "roundToAsset" + }, + { + "args": [ + "amount" + ], + "lineno": 432, + "name": "isXRP" + }, + { + "args": [ + "amt1", + "amt2" + ], + "lineno": 435, + "name": "canAdd" + }, + { + "args": [ + "amt1", + "amt2" + ], + "lineno": 437, + "name": "canSubtract" + }, + { + "args": [ + "v", + "field" + ], + "lineno": 442, + "name": "getOrThrow" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + }, + { + "lineno": 440, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STAmount.h.ai.md b/include/xrpl/protocol/STAmount.h.ai.md new file mode 100644 index 0000000000..3b4fb62dbc --- /dev/null +++ b/include/xrpl/protocol/STAmount.h.ai.md @@ -0,0 +1,50 @@ +# `STAmount.h` — Unified Serializable Amount for XRP, IOU, and MPT + +`STAmount` is the canonical on-ledger amount type in the XRPL codebase. It unifies three fundamentally different quantity representations — XRP drops, IOU floating-point amounts, and Multi-Purpose Token (MPT) integer amounts — behind a single serializable interface that plugs into the ledger's typed-field system via `STBase`. Almost every transaction field that names a quantity (`Amount`, `Fee`, `SendMax`, etc.) resolves to an `STAmount`. + +## Internal Representation + +The class stores four private members: `mAsset` (an `Asset` variant holding either an `Issue` or `MPTIssue`), an unsigned 64-bit `mValue`, a signed integer `mOffset`, and a boolean `mIsNegative`. The meaning of these fields depends on the asset type. + +For **IOU amounts**, value is stored as a normalized scientific notation: `amount = mValue × 10^mOffset`. The mantissa (`mValue`) is constrained to `[cMinValue, cMaxValue]` which is `[10^15, 10^16 − 1]`, and the exponent (`mOffset`) is constrained to `[-96, +80]`. Zero is a special case encoded as `mValue = 0, mOffset = -100`; the −100 sentinel is deliberately chosen so that zero sorts below any small positive IOU whose exponent is large-negative. This 15-digit decimal mantissa gives roughly 16 significant figures of precision over a range spanning 176 orders of magnitude. + +For **XRP and MPT** (collectively called "integral" types via `Asset::integral()`), `mOffset` is always 0 and `mValue` directly holds the raw drop or token count. The runtime enforces that XRP does not exceed `cMaxNativeN` (10^17 drops = 100 million XRP) and that MPT does not exceed the 63-bit signed maximum. + +## Wire Encoding + +The serialization format encodes the amount into a compact 64-bit word (plus trailing bytes for the currency/issuer identifiers if IOU, or a 192-bit MPTID if MPT). The flag bits in the high positions distinguish the three types: + +- Bit 63 = 0 → native (XRP or MPT); bit 61 further distinguishes XRP from MPT. +- Bit 63 = 1 → issued currency (IOU). +- Bit 62 encodes the sign (1 = positive) for IOU and MPT. +- For IOU, the next 8 bits encode `mOffset + 97` (shifting into a non-negative range of [0, 255]), and the low 54 bits hold the mantissa. + +The constants `cIssuedCurrency`, `cPositive`, `cMPToken`, and `cValueMask` are masks for picking these fields apart. The deserialization constructor (`STAmount(SerialIter&, SField const&)`) performs the full inverse decode including a throw on invalid wire forms (negative zero, mantissa out of range, invalid currency/account). + +## The `unchecked` Tag and `canonicalize()` + +Every public constructor that sets `mValue`/`mOffset` ends by calling `canonicalize()`, which normalises the mantissa into the `[cMinValue, cMaxValue]` band by repeatedly scaling up or down and adjusting the exponent. It throws `std::runtime_error` on overflow and silently truncates subnormals to zero. + +An `unchecked` nested tag struct provides a bypass: constructors tagged `unchecked` set the four fields verbatim without calling `canonicalize()`. This path exists for performance-sensitive code (e.g., reading a quantity from a known-good source, or inside arithmetic helpers that already maintain invariants). Callers must guarantee the representation is already canonical; the public-interface constructors should always be preferred. + +## Amendment-Gated Arithmetic Path + +`canonicalize()` and the addition operator branch on `getSTNumberSwitchover()`, which reflects whether the `SingleAssetVault` or `LendingProtocol` amendments are active. When enabled, all arithmetic is routed through the `Number` class — a high-precision floating-point type with a "large" mantissa range ([10^18, 10^19 − 1]) that can exactly represent the full int64 range needed by XRP and MPT. On the legacy path, IOU normalization is done with direct 64-bit scaling loops. This dual-path design is how XRPL maintains amendment-gated numerical behaviour: old ledger rules produce identical results to legacy code, while newer amendments unlock higher-precision arithmetic. The `NumberSO` and `NumberMantissaScaleGuard` RAII helpers manage the thread-local switchover state, preventing test interference. + +## `Asset` Polymorphism + +`STAmount` delegates asset-type queries entirely to the `Asset` variant member. `holds()` and `get()` are thin template forwarders to `Asset::holds()` and `Asset::get()`, where `TIss` is constrained by the `ValidIssueType` concept to either `Issue` or `MPTIssue`. `native()` returns true only for XRP. `integral()` returns true for both XRP and MPT. The `operator Number()` conversion dispatches via `Asset::visit()` to the appropriate extraction method (`xrp()`, `iou()`, or `mpt()`), each of which re-materialises the correctly typed amount object and throws if called on the wrong variant. + +## Arithmetic and Rounding + +The free functions `multiply`, `divide` and their rounding variants accept two `STAmount` operands and a result `Asset`, rather than operating in-place. This design is intentional: cross-currency math (e.g., quality calculations) naturally produces a result in a third currency, and the caller must specify which. Rounding variants come in two flavours: `mulRound`/`divRound` use the legacy fixed-mode approach, while `mulRoundStrict`/`divRoundStrict` honour the current thread-local `Number::rounding_mode` more precisely. The `getRate` function encodes the offer quality (in/out ratio) as a single `uint64_t` with the exponent packed in the high byte and mantissa in the low bits, suitable for offer-book ordering where a lower rate benefits the taker. + +The `roundToScale` function rounds an `STAmount` to a given decimal exponent, shedding precision beyond a reference scale. The two `roundToAsset` overloads (one in-place, one returning `Number`) apply asset-appropriate rounding: integral types (XRP, MPT) have their fractional parts dropped by `canonicalize()`; IOU types additionally call `roundToScale` to prevent accumulation of sub-precision dust. + +## Safety Pre-flight Checks + +`canAdd` and `canSubtract` provide pre-flight arithmetic safety checks without attempting the operation. For XRP and MPT, these perform 64-bit overflow/underflow bounds tests. For IOU, `canAdd` uses a relative-precision metric: it reconstructs both operands after round-tripping through addition and checks that the combined relative error does not exceed `10^-4`. This guards against silently losing significant digits when adding amounts whose exponents differ by more than 15. + +## Serialization and JSON + +`STAmount` satisfies the full `STBase` contract: `getSType()` returns `STI_AMOUNT`, `add()` writes the wire-format bytes to a `Serializer`, `getJson()` produces either a plain string (for XRP) or a `{value, currency, issuer}` / `{value, mpt_issuance_id}` object (for IOU/MPT). The free function `amountFromJson` parses all three formats — object, array, and slash-delimited string — accepting the ledger's historical string-based representation of XRP alongside the structured IOU and MPT formats. `amountFromJsonNoThrow` wraps this in a non-throwing overload for contexts where invalid input should produce a boolean result rather than an exception. The `Json::getOrThrow` specialisation in the `Json` namespace lets template code extract an `STAmount` from a JSON object by field name uniformly with other serialized types. \ No newline at end of file diff --git a/include/xrpl/protocol/STArray.h.ai.json b/include/xrpl/protocol/STArray.h.ai.json new file mode 100644 index 0000000000..2bf51df828 --- /dev/null +++ b/include/xrpl/protocol/STArray.h.ai.json @@ -0,0 +1,304 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "STArray" + } + ], + "description": "Defines the STArray class, a container for a sequence of STObject instances, providing array-like operations and serialization support for the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STArray.h", + "functions": [ + { + "args": [], + "lineno": 12, + "name": "STArray" + }, + { + "args": [ + "STArray const&" + ], + "lineno": 13, + "name": "STArray" + }, + { + "args": [ + "Iter first", + "Iter last" + ], + "lineno": 18, + "name": "STArray" + }, + { + "args": [ + "SField const& f", + "Iter first", + "Iter last" + ], + "lineno": 23, + "name": "STArray" + }, + { + "args": [ + "STArray const&" + ], + "lineno": 28, + "name": "operator=" + }, + { + "args": [ + "STArray&&" + ], + "lineno": 29, + "name": "STArray" + }, + { + "args": [ + "STArray&&" + ], + "lineno": 30, + "name": "operator=" + }, + { + "args": [ + "SField const& f", + "std::size_t n" + ], + "lineno": 32, + "name": "STArray" + }, + { + "args": [ + "SerialIter& sit", + "SField const& f", + "int depth" + ], + "lineno": 33, + "name": "STArray" + }, + { + "args": [ + "int n" + ], + "lineno": 34, + "name": "STArray" + }, + { + "args": [ + "SField const& f" + ], + "lineno": 35, + "name": "STArray" + }, + { + "args": [ + "std::size_t j" + ], + "lineno": 37, + "name": "operator[]" + }, + { + "args": [ + "std::size_t j" + ], + "lineno": 40, + "name": "operator[]" + }, + { + "args": [], + "lineno": 43, + "name": "back" + }, + { + "args": [], + "lineno": 46, + "name": "back" + }, + { + "args": [ + "Args&&... args" + ], + "lineno": 49, + "name": "emplace_back" + }, + { + "args": [ + "STObject const& object" + ], + "lineno": 53, + "name": "push_back" + }, + { + "args": [ + "STObject&& object" + ], + "lineno": 56, + "name": "push_back" + }, + { + "args": [], + "lineno": 59, + "name": "begin" + }, + { + "args": [], + "lineno": 62, + "name": "end" + }, + { + "args": [], + "lineno": 65, + "name": "begin" + }, + { + "args": [], + "lineno": 68, + "name": "end" + }, + { + "args": [], + "lineno": 71, + "name": "size" + }, + { + "args": [], + "lineno": 74, + "name": "empty" + }, + { + "args": [], + "lineno": 77, + "name": "clear" + }, + { + "args": [ + "std::size_t n" + ], + "lineno": 80, + "name": "reserve" + }, + { + "args": [ + "STArray& a" + ], + "lineno": 83, + "name": "swap" + }, + { + "args": [], + "lineno": 86, + "name": "getFullText" + }, + { + "args": [], + "lineno": 89, + "name": "getText" + }, + { + "args": [ + "JsonOptions index" + ], + "lineno": 92, + "name": "getJson" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 95, + "name": "add" + }, + { + "args": [ + "bool (*compare)(STObject const& o1, STObject const& o2)" + ], + "lineno": 98, + "name": "sort" + }, + { + "args": [ + "STArray const& s" + ], + "lineno": 101, + "name": "operator==" + }, + { + "args": [ + "STArray const& s" + ], + "lineno": 104, + "name": "operator!=" + }, + { + "args": [ + "iterator pos" + ], + "lineno": 107, + "name": "erase" + }, + { + "args": [ + "const_iterator pos" + ], + "lineno": 110, + "name": "erase" + }, + { + "args": [ + "iterator first", + "iterator last" + ], + "lineno": 113, + "name": "erase" + }, + { + "args": [ + "const_iterator first", + "const_iterator last" + ], + "lineno": 116, + "name": "erase" + }, + { + "args": [], + "lineno": 119, + "name": "getSType" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 122, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 125, + "name": "isDefault" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 129, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 130, + "name": "move" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STArray.h.ai.md b/include/xrpl/protocol/STArray.h.ai.md new file mode 100644 index 0000000000..426928eacd --- /dev/null +++ b/include/xrpl/protocol/STArray.h.ai.md @@ -0,0 +1,39 @@ +# `STArray.h` — Serialized Array of `STObject` Instances + +`STArray` is one of the fundamental composite types in the XRPL protocol type system. It represents an ordered, variable-length sequence of `STObject` instances — the protocol's mechanism for encoding repeated structured sub-fields within transactions and ledger entries. Real-world examples include the `Memos` field (a list of memo objects attached to a transaction) and `SignerEntries` (the ordered list of signers in a multisig account). Like every `STBase`-derived type, `STArray` participates in both the canonical binary wire format and the JSON representation used by the RPC layer. + +## Inheritance and Instance Tracking + +`STArray` inherits from both `STBase` and `CountedObject`. The `STBase` base class provides the field name (`SField`) association, the serialization interface (`add()`, `addFieldID()`), and the virtual `copy()`/`move()` protocol used by `detail::STVar`. The `CountedObject` mixin increments an atomic counter on every constructor call and decrements on destruction, feeding the diagnostics surface exposed through `GetCounts`. This imposes no runtime cost on hot paths — the counter is a single atomic increment/decrement per object lifetime. + +The storage is a plain `std::vector` aliased as `list_type`. Because `STObject` is itself a concrete class (not a pointer type), the vector holds values directly, which means copy and move operations on `STArray` copy or move all elements. The `STBase` comment in the base class warns against putting `STBase`-derived objects in ordinary vectors due to copy-assignment name-erasure semantics; `STArray` sidesteps this for its *elements* by storing concrete `STObject` values rather than base-class references. + +## Binary Deserialization + +The `SerialIter`-based constructor is the most architecturally significant piece of this class. XRPL's binary format encodes an array as a sentinel-terminated stream: each element begins with its field ID and ends with a per-object terminator (`STI_OBJECT, 1`), and the entire array ends with an array-level terminator (`STI_ARRAY, 1`). The constructor loops calling `sit.getFieldID(type, field)` and breaks when it sees the array terminator. Three classes of ill-formed input are rejected explicitly: + +- An `STI_ARRAY/1` marker that is not the array-end — caught by the loop's break condition. +- An `STI_OBJECT/1` marker appearing at the array element level (an end-of-object sentinel leaked outside its scope) — rejected with `"Illegal terminator in array"`. +- A non-`STI_OBJECT` field type inside the array — rejected with `"Non-object in array"`, enforcing the invariant that every array element is a typed object, never a scalar. + +The `depth` parameter passed through to `v_.emplace_back(sit, fn, depth + 1)` is a recursion guard. `STObject`'s own deserialization constructor enforces a maximum depth of 10; since `STArray` increments the counter before passing it down, a maximally nested structure hits the limit before it can blow the call stack. This is a deliberate defense against crafted payloads. + +After constructing each `STObject`, `applyTemplateFromSField(fn)` is called immediately. The outer `SField` associated with each array element (e.g., `sfMemo`, `sfSigner`) carries schema information (an `SOTemplate`), and applying it validates the just-deserialized object against the known field layout for that type. This call can throw, and the comment acknowledges it — a partially constructed array is simply abandoned via the exception unwind. + +## Binary Serialization + +`add()` mirrors the deserializer precisely: for each element it writes `object.addFieldID(s)` (the element's own field ID prefix), then `object.add(s)` (the element body including the inner `STI_OBJECT/1` per-object end marker from `STObject::add()`), then explicitly appends `s.addFieldID(STI_OBJECT, 1)`. The outer `STI_ARRAY/1` array-end marker is *not* written here — that is the enclosing `STObject`'s responsibility. This split keeps each level responsible only for its own content, matching the XRPL convention that a container writes its body but its parent closes the outer scope. + +## JSON Representation + +`getJson()` produces a JSON array where each element is a JSON object keyed by the inner `STObject`'s field name — for instance `[{"Memo": {...}}, {"Memo": {...}}]`. The outer key is always present, making the field type unambiguous to JSON consumers. Objects whose `getSType()` returns `STI_NOTPRESENT` are silently skipped, which handles optional or absent inner objects without introducing nulls into the output. + +## Move Semantics + +The explicit move constructor and move assignment operator in the `.cpp` file exist because `STBase` stores an `SField const*` that is *not* moved by default (the default move constructor of `STBase` would leave the field name in the moved-from state). Both operations explicitly call `STBase(other.getFName())` or `setFName(other.getFName())` before moving `v_`, preserving the `SField` association on the destination. The `copy()` and `move()` virtual overrides use `STBase::emplace()` to placement-construct into a caller-provided fixed-size buffer if the object fits, or heap-allocate otherwise — the mechanism by which `detail::STVar` implements small-buffer optimization for the XRPL type system. + +## Design Notes + +The `sort()` method accepts a raw C function pointer `bool (*compare)(STObject const&, STObject const&)` rather than a `std::function` or template parameter. This keeps the interface simple for the handful of call sites that need it (primarily sorting `SignerEntries` by account ID for canonical transaction form), without introducing template instantiation in the header. + +`isDefault()` returns `true` for an empty array. This matters because the enclosing `STObject` serializer can skip default-valued fields, so an empty `STArray` contributes nothing to the binary encoding — consistent with how absent optional array fields are represented in the ledger. \ No newline at end of file diff --git a/include/xrpl/protocol/STBase.h.ai.json b/include/xrpl/protocol/STBase.h.ai.json new file mode 100644 index 0000000000..41c7486ef8 --- /dev/null +++ b/include/xrpl/protocol/STBase.h.ai.json @@ -0,0 +1,124 @@ +{ + "args": [ + { + "lineno": 22, + "name": "v" + }, + { + "lineno": 38, + "name": "lh" + }, + { + "lineno": 38, + "name": "rh" + }, + { + "lineno": 44, + "name": "lh" + }, + { + "lineno": 44, + "name": "rh" + }, + { + "lineno": 49, + "name": "lh" + }, + { + "lineno": 49, + "name": "rh" + }, + { + "lineno": 54, + "name": "lh" + }, + { + "lineno": 54, + "name": "rh" + }, + { + "lineno": 59, + "name": "v" + }, + { + "lineno": 49, + "name": "t" + }, + { + "lineno": 143, + "name": "out" + }, + { + "lineno": 143, + "name": "t" + }, + { + "lineno": 147, + "name": "D" + }, + { + "lineno": 156, + "name": "D" + }, + { + "lineno": 165, + "name": "T" + }, + { + "lineno": 166, + "name": "n" + }, + { + "lineno": 166, + "name": "buf" + }, + { + "lineno": 166, + "name": "val" + } + ], + "classes": [ + { + "args": [ + "underlying_t v" + ], + "lineno": 13, + "name": "JsonOptions" + }, + { + "args": [], + "lineno": 74, + "name": "STBase" + } + ], + "description": "Defines the STBase class and related utilities for XRPL's serialization framework, including JSON export options, type-safe downcasting, and serialization helpers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STBase.h", + "functions": [ + { + "args": [ + "t" + ], + "lineno": 49, + "name": "to_json" + }, + { + "args": [ + "out", + "t" + ], + "lineno": 143, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + }, + { + "lineno": 56, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STBase.h.ai.md b/include/xrpl/protocol/STBase.h.ai.md new file mode 100644 index 0000000000..390216aad6 --- /dev/null +++ b/include/xrpl/protocol/STBase.h.ai.md @@ -0,0 +1,50 @@ +# `STBase.h` — Root of the XRPL Serialized Type Hierarchy + +## Purpose and Role + +`STBase` is the abstract base class for every serialized type ("ST") in the XRPL protocol layer. Every field that appears in a transaction, ledger entry, or validation — integers, amounts, account IDs, blobs, arrays, nested objects — is represented as a class that inherits from `STBase`. The header also defines `JsonOptions`, the bitmask type governing how those types render to JSON. + +The abbreviation "ST" means "Serialized Type." Understanding `STBase` is a prerequisite for understanding the entire protocol object model: `STInteger`, `STAmount`, `STBlob`, `STObject`, `STArray`, `STTx`, `STLedgerEntry`, and every other protocol field type all derive from it. + +## The Field Identity Invariant + +Each `STBase` instance carries a single private member: `SField const* fName`. An `SField` is an immutable global descriptor that binds together a human-readable field name, a numeric type code (`SerializedTypeID`), and a numeric field code. This pairing is what allows the binary wire format to tag every field unambiguously. `setFName()` and `getFName()` expose this, and `addFieldID()` writes the combined type+field prefix byte(s) to a `Serializer` as the first step of encoding any field. + +## The Virtual Interface + +Subclasses override a focused set of virtual methods: + +- `getSType()` — returns the `SerializedTypeID` enum value identifying this concrete type. +- `add(Serializer&)` — the primary serialization method: writes the field's binary payload into the given `Serializer` byte stream (the field ID header is written separately via `addFieldID()`). +- `isEquivalent(STBase const&)` — value equality ignoring field names. Used by `detail::STVar`'s `operator==`. +- `isDefault()` — returns `true` when the field holds its default value (zero for integers, etc.), enabling omission of optional fields during serialization. +- `getJson(JsonOptions)` — renders to a `Json::Value` with options controlling version and date inclusion. +- `getText()` / `getFullText()` — human-readable string forms; `getFullText()` typically prepends the field name. + +## The Intentionally Asymmetric Copy Assignment + +The comment at line 88 is critical and easily missed: `STBase::operator=` deliberately does **not** copy the field name from the source. It copies only the value. The rationale is that `STObject` (which stores fields in a `std::vector`) uses copy assignment to "slide down" remaining elements when a field is deleted. If copy assignment also moved the field name, an element at position *i* would acquire the name of its neighbour after every deletion. The current design keeps value-copy and identity-copy as separate operations, at the cost of making `STBase`-derived objects unsafe to store directly in standard containers — hence the prominent warning to use `boost::ptr_*` containers instead. + +## `downcast()`: Safe Narrowing + +Rather than exposing raw `dynamic_cast` at call sites, `STBase` provides the `downcast()` template. It calls `dynamic_cast(this)` and throws `std::bad_cast` (via the project-uniform `Throw<>` helper) if the cast fails. This centralizes the failure path and makes accidental silent null-pointer dereferences impossible. Concrete code that retrieves a field from an `STObject` and needs to treat it as, say, an `STAmount` uses this method. + +## `emplace()` and the Small-Object Optimization + +The protected static `emplace(n, buf, val)` template is the bridge between `STBase` and `detail::STVar`. `STVar` is a variant-like wrapper (with a 72-byte inline storage buffer) that owns an ST object. To avoid heap allocations for small types, `STVar` calls the virtual `copy()` or `move()` on an existing `STBase`, passing its inline buffer and its size as arguments. Every concrete subclass implements `copy()` and `move()` identically: + +```cpp +STBase* copy(std::size_t n, void* buf) const override { + return emplace(n, buf, *this); +} +``` + +`emplace` decides: if `sizeof(U) > n`, heap-allocate with `new U(...)`; otherwise, placement-new into `buf`. The `copy()`/`move()` virtuals are private and only accessible to `detail::STVar` (declared `friend`), enforcing that this low-level placement mechanism is never called from generic code. + +## `JsonOptions` Bitmask + +`JsonOptions` is a small strongly-typed bitmask wrapper around `unsigned int`. Its two meaningful bits control whether a JSON rendering includes a date field (`include_date`) and whether legacy pre-API-v2 formatting is suppressed (`disable_API_prior_V2`). The `operator~` is intentionally bounded by the `_all` mask so that bitwise complement doesn't set reserved or future bits. The free `to_json()` function template at namespace scope provides a uniform ADL-accessible entry point for any type that exposes `getJson(JsonOptions)`. + +## Relationship to `detail::STVar` and `STObject` + +`STBase` friends only `detail::STVar`. This single friendship grants `STVar` access to `copy()` and `move()`, which are otherwise private. `STObject` stores its fields as a `std::vector`, not a vector of raw `STBase` pointers — this is precisely the safe container pattern the warning at line 88 recommends. `STVar` manages object lifetime, handles the inline-vs-heap allocation decision, and presents a clean `STBase&` interface to callers. The combination of `STBase::emplace()`, the private virtuals, and the `STVar` friendship is the mechanism that makes type-erased, allocation-optimized storage of heterogeneous ST objects possible without a separate allocator or external memory pool. \ No newline at end of file diff --git a/include/xrpl/protocol/STBitString.h.ai.json b/include/xrpl/protocol/STBitString.h.ai.json new file mode 100644 index 0000000000..d33140de43 --- /dev/null +++ b/include/xrpl/protocol/STBitString.h.ai.json @@ -0,0 +1,179 @@ +{ + "args": [ + { + "lineno": 12, + "name": "Bits" + }, + { + "lineno": 23, + "name": "n" + }, + { + "lineno": 24, + "name": "v" + }, + { + "lineno": 25, + "name": "n" + }, + { + "lineno": 25, + "name": "v" + }, + { + "lineno": 26, + "name": "sit" + }, + { + "lineno": 26, + "name": "name" + }, + { + "lineno": 34, + "name": "t" + }, + { + "lineno": 37, + "name": "s" + }, + { + "lineno": 44, + "name": "Tag" + }, + { + "lineno": 44, + "name": "v" + }, + { + "lineno": 54, + "name": "n" + }, + { + "lineno": 54, + "name": "buf" + }, + { + "lineno": 56, + "name": "n" + }, + { + "lineno": 56, + "name": "buf" + } + ], + "classes": [ + { + "args": [ + "SField const& n", + "value_type const& v", + "SerialIter& sit", + "SField const& name" + ], + "lineno": 13, + "name": "STBitString" + } + ], + "description": "Defines the STBitString template class for representing fixed-size bit strings as serialized types in the XRPL protocol, along with type aliases for common bit lengths and related methods for serialization, comparison, and value access.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STBitString.h", + "functions": [ + { + "args": [ + "SField const& n" + ], + "lineno": 23, + "name": "STBitString" + }, + { + "args": [ + "value_type const& v" + ], + "lineno": 24, + "name": "STBitString" + }, + { + "args": [ + "SField const& n", + "value_type const& v" + ], + "lineno": 25, + "name": "STBitString" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 26, + "name": "STBitString" + }, + { + "args": [], + "lineno": 28, + "name": "getSType" + }, + { + "args": [], + "lineno": 31, + "name": "getText" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 34, + "name": "isEquivalent" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 37, + "name": "add" + }, + { + "args": [], + "lineno": 40, + "name": "isDefault" + }, + { + "args": [ + "base_uint const& v" + ], + "lineno": 44, + "name": "setValue" + }, + { + "args": [], + "lineno": 48, + "name": "value" + }, + { + "args": [], + "lineno": 50, + "name": "operator value_type" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 54, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 56, + "name": "move" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STBitString.h.ai.md b/include/xrpl/protocol/STBitString.h.ai.md new file mode 100644 index 0000000000..efc5c8bd1d --- /dev/null +++ b/include/xrpl/protocol/STBitString.h.ai.md @@ -0,0 +1,42 @@ +# `STBitString.h` — Fixed-Width Bit String Serialized Type + +## Role and Purpose + +`STBitString` is the XRPL serialization layer's representation of fixed-width opaque bit arrays. It bridges the raw `base_uint` arithmetic type — which holds values like transaction hashes, account IDs, and ledger indices — and the protocol's typed serialization framework built on `STBase`. Every field that travels over the wire or appears in a ledger object as a 128-, 160-, 192-, or 256-bit blob is handled through one of the four aliases this file exposes: `STUInt128`, `STUInt160`, `STUInt192`, and `STUInt256`. + +The name "bit string" rather than "integer" is deliberate: these values are not treated arithmetically by the serialization layer. They are opaque sequences of bits that happen to be compared and copied as units. The underlying `base_uint` does support arithmetic, but `STBitString` itself exposes only identity (`isEquivalent`), serialization (`add`), and value access — nothing numeric. + +## Template Design and the GDB Workaround + +The template parameter is declared `int Bits` rather than the more natural `unsigned int Bits`. A comment in the header explains why: GDB 12.1 (and earlier) cannot locate RTTI information for templates instantiated over unsigned types, which breaks the GDB pretty-printer infrastructure used during development. The `static_assert(Bits > 0)` enforces the semantically obvious constraint that a negative or zero bit-count is meaningless, compensating for the signed type allowing values that `unsigned` would have rejected outright at the type-system level. + +## Inheritance: STBase and CountedObject + +`STBitString` inherits from two bases. `STBase` provides the named-field abstraction: every serialized object carries an `SField` identifier that ties a value to a canonical field name, wire-type code, and binary codec behavior. This means an `STUInt256` isn't just a 256-bit value in memory — it knows whether it's a `LedgerIndex`, a `TransactionID`, a `ParentHash`, and so on. The `getSType()` method is specialized (not overridden via template) for each of the four concrete bit widths, returning the distinct wire-type codes `STI_UINT128`, `STI_UINT160`, `STI_UINT192`, and `STI_UINT256` registered in `SField.h`. + +`CountedObject>` is a lightweight CRTP mixin that maintains a global atomic instance counter per instantiation. This is purely diagnostic: the server can report live object counts by type, which helps track down memory leaks or unexpected retention of large STObject trees. + +## Constructors and Deserialization + +Four constructors cover the full lifecycle: + +- `STBitString(SField const& n)` — named field with a zero-initialized value, used when building objects programmatically before the value is known. +- `STBitString(value_type const& v)` — anonymous value, typically used in temporary computations where field identity doesn't matter. +- `STBitString(SField const& n, value_type const& v)` — the common case: a fully specified named field. +- `STBitString(SerialIter& sit, SField const& name)` — deserialization constructor; delegates to the third constructor by calling `sit.getBitString()`, which reads exactly `Bits/8` bytes from the incoming stream at the current cursor position. This keeps deserialization logic centralized in `SerialIter` rather than scattered across each type. + +## Serialization: the `add` Method + +`add(Serializer& s)` writes the value into a byte buffer for wire encoding. Two `XRPL_ASSERT` calls guard the precondition that the associated `SField` must be of binary type and must match the object's own `getSType()`. These checks exist because `add` is called polymorphically through the `STBase` interface — the field and type metadata must stay consistent with the actual concrete type, and detecting mismatches early prevents silent protocol corruption. + +## Value Mutability and the Tag Parameter + +`setValue` accepts `base_uint` where `Tag` is a free template parameter. `base_uint` supports a tag type for its second parameter that distinguishes semantically different values of the same bit width (e.g., an `AccountID` and a raw `uint160` are both 160-bit but have different tags). The member function template accepts any tag, allowing cross-tag assignment when the caller explicitly intends it while making accidental mixing visible in code review. The implicit conversion operator `operator value_type()` and the `value()` accessor both return the tag-free `value_type`, erasing tag information on the way out. + +## Copy/Move and STVar Integration + +The private `copy` and `move` overrides implement `STBase`'s small-buffer optimization protocol used by `detail::STVar`. `emplace` (inherited from `STBase`) placement-constructs the object into a caller-supplied buffer if it fits within `n` bytes; otherwise it heap-allocates. This avoids dynamic allocation for small serialized types that are frequently embedded in `STObject` containers. `STVar` is declared a `friend` so it can call these private methods directly. + +## Defaultness + +`isDefault()` returns `true` when the stored value equals `beast::zero`, which is a sentinel expression for the all-zeros bitstring. This is used by the serialization layer to decide whether a field can be omitted when encoding — default-valued fields typically do not need to be serialized in canonical binary form. \ No newline at end of file diff --git a/include/xrpl/protocol/STBlob.h.ai.json b/include/xrpl/protocol/STBlob.h.ai.json new file mode 100644 index 0000000000..a3c6f7bea9 --- /dev/null +++ b/include/xrpl/protocol/STBlob.h.ai.json @@ -0,0 +1,141 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "STBlob" + } + ], + "description": "Defines the STBlob class, a variable-length byte string type used in the XRPL protocol, providing serialization, copying, and assignment operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STBlob.h", + "functions": [ + { + "args": [ + "STBlob const& rhs" + ], + "lineno": 27, + "name": "STBlob" + }, + { + "args": [ + "SField const& f", + "void const* data", + "std::size_t size" + ], + "lineno": 29, + "name": "STBlob" + }, + { + "args": [ + "SField const& f", + "Buffer&& b" + ], + "lineno": 30, + "name": "STBlob" + }, + { + "args": [ + "SField const& n" + ], + "lineno": 31, + "name": "STBlob" + }, + { + "args": [ + "SerialIter&", + "SField const& name" + ], + "lineno": 32, + "name": "STBlob" + }, + { + "args": [], + "lineno": 34, + "name": "size" + }, + { + "args": [], + "lineno": 37, + "name": "data" + }, + { + "args": [], + "lineno": 40, + "name": "getSType" + }, + { + "args": [], + "lineno": 43, + "name": "getText" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 46, + "name": "add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 49, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 52, + "name": "isDefault" + }, + { + "args": [ + "Slice const& slice" + ], + "lineno": 55, + "name": "operator=" + }, + { + "args": [], + "lineno": 58, + "name": "value" + }, + { + "args": [ + "Buffer&& buffer" + ], + "lineno": 61, + "name": "operator=" + }, + { + "args": [ + "Buffer&& b" + ], + "lineno": 64, + "name": "setValue" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 68, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 69, + "name": "move" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STBlob.h.ai.md b/include/xrpl/protocol/STBlob.h.ai.md new file mode 100644 index 0000000000..ac8fbeb82b --- /dev/null +++ b/include/xrpl/protocol/STBlob.h.ai.md @@ -0,0 +1,46 @@ +# `STBlob` — Variable-Length Binary Field for XRPL Serialized Types + +## Role in the System + +`STBlob` is the serialized-type representation of an opaque, variable-length byte string within the XRPL binary protocol. It serves as the concrete type for any ledger or transaction field whose wire format is declared as `STI_VL` (variable-length), and — notably — also for `STI_ACCOUNT` fields (20-byte account IDs), both of which are serialized as length-prefixed blobs. It lives in the `include/xrpl/protocol/` layer alongside its sibling types (`STAmount`, `STArray`, `STObject`, etc.), all sharing the `STBase` contract for named, serializable fields. + +## Class Hierarchy and Responsibilities + +`STBlob` multiply-inherits from `STBase` and `CountedObject`. + +`STBase` provides the field-naming system (`SField`) and the virtual interface — `getSType()`, `getText()`, `add()`, `isEquivalent()`, `isDefault()`, `copy()`, `move()` — that the XRPL serialization engine calls polymorphically. Every `STBase` carries an `SField const*` that identifies which ledger field this instance represents (e.g., `sfSignature`, `sfAccount`). The class comment in `STBase.h` warns explicitly against using `STBase`-derived objects in standard containers due to copy-assignment semantics that deliberately do not propagate field names — only values — which is used by the transaction engine to "slide" field values during object mutation. + +`CountedObject` adds zero-overhead live-instance counting via a static atomic counter registered in a global lock-free linked list. This enables diagnostics via `CountedObjects::getInstance().getCounts(threshold)` without impacting hot paths. + +## Storage Model: Owning Buffer, Non-Owning View + +The sole data member is `Buffer value_`, a heap-owning wrapper around `unique_ptr`. The `Buffer` class provides value semantics with explicit copy and move, and an implicit conversion to `Slice`. + +`STBlob`'s public type alias `using value_type = Slice` and the `value()` accessor expose the contents as a `Slice` — a lightweight, non-owning `{pointer, size}` pair. This is the canonical read-only access pattern: callers inspect data through a cheap `Slice` without triggering any allocation or copy. Mutation must go through one of the two assignment operators: `operator=(Slice const&)` allocates a fresh `Buffer` and copies the bytes in, while `operator=(Buffer&&)` and `setValue(Buffer&&)` transfer ownership in O(1) via move. This duality makes the ownership model explicit at the call site. + +## Constructors and Deserialization + +Four construction paths exist: + +- **From raw memory**: `STBlob(SField const&, void const*, std::size_t)` copies bytes into an owned `Buffer`. Used when constructing blobs from already-decoded data. +- **From a moved `Buffer`**: `STBlob(SField const&, Buffer&&)` takes ownership without copying, preferred for performance-sensitive construction. +- **Empty blob**: `STBlob(SField const&)` creates a zero-length blob; `isDefault()` returns `true` in this state (the buffer is empty). +- **From a `SerialIter`**: The deserialization constructor `STBlob(SerialIter&, SField const&)` (defined in `STBlob.cpp`) calls `st.getVLBuffer()`, which reads the VL-prefix and returns a `Buffer` filled with the wire bytes. This is how blobs are reconstituted from a raw ledger stream. + +## Serialization and Wire Format + +`getSType()` returns `STI_VL`, the XRPL type tag for variable-length fields. `add(Serializer& s)` calls `s.addVL(value_.data(), value_.size())`, which writes the standard XRPL VL prefix followed by the raw bytes. The method asserts two invariants: the field must be declared binary (`getFName().isBinary()`), and the field's type must be either `STI_VL` or `STI_ACCOUNT`. The second assertion is the key design detail — account IDs on the XRPL wire are 20-byte opaque blobs, so `STBlob` serves double duty as the backing store for account fields. The VL encoding handles both cases uniformly. + +`getText()` returns the contents as an uppercase hex string via `strHex()`, used for human-readable logging and JSON output. + +`isEquivalent()` performs a `dynamic_cast` to confirm the other object is also an `STBlob`, then delegates to `Buffer::operator==` which does a `memcmp`. It does not compare field names — only content — consistent with the `STBase` contract. + +## In-Place Copy and Move for `STVar` + +The private `copy(std::size_t n, void* buf) const` and `move(std::size_t n, void* buf)` overrides delegate to `STBase::emplace()`, which does placement-new into `buf` if `sizeof(STBlob) <= n`, otherwise falls back to heap allocation. This is how `detail::STVar` — the variant-like field container used inside `STObject` — implements a small-buffer optimization: small serialized types are stored inline in a fixed-size buffer, avoiding a separate heap allocation per field. `detail::STVar` is declared a `friend` in both `STBase` and `STBlob` to access these private factory methods. + +## Design Tradeoffs + +The split between `Buffer` (owning) and `Slice` (non-owning) could have been collapsed into a single interface, but keeping them separate enforces at the type level that callers who hold a `Slice` have no ownership claim. The choice to expose `value_type = Slice` rather than `const Buffer&` is deliberate: it prevents callers from taking a mutable reference to the underlying storage and sidesteps object lifetime confusion when the `STBlob` is moved. + +The `setValue(Buffer&&)` method is redundant with `operator=(Buffer&&)` but exists as an explicit named setter for code sites where the intent of "set the content" is more readable than an assignment expression. \ No newline at end of file diff --git a/include/xrpl/protocol/STCurrency.h.ai.json b/include/xrpl/protocol/STCurrency.h.ai.json new file mode 100644 index 0000000000..5e7a36d8e4 --- /dev/null +++ b/include/xrpl/protocol/STCurrency.h.ai.json @@ -0,0 +1,86 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "STCurrency" + } + ], + "description": "Defines the STCurrency class for representing and manipulating currency types in the XRPL protocol, including serialization, comparison, and JSON conversion utilities.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STCurrency.h", + "functions": [ + { + "args": [ + "SField const& name", + "Json::Value const& v" + ], + "lineno": 56, + "name": "currencyFromJson" + }, + { + "args": [], + "lineno": 59, + "name": "STCurrency::currency" + }, + { + "args": [], + "lineno": 64, + "name": "STCurrency::value" + }, + { + "args": [ + "Currency const& currency" + ], + "lineno": 69, + "name": "STCurrency::setCurrency" + }, + { + "args": [ + "STCurrency const& lhs", + "STCurrency const& rhs" + ], + "lineno": 74, + "name": "operator==" + }, + { + "args": [ + "STCurrency const& lhs", + "STCurrency const& rhs" + ], + "lineno": 79, + "name": "operator!=" + }, + { + "args": [ + "STCurrency const& lhs", + "STCurrency const& rhs" + ], + "lineno": 84, + "name": "operator<" + }, + { + "args": [ + "STCurrency const& lhs", + "Currency const& rhs" + ], + "lineno": 89, + "name": "operator==" + }, + { + "args": [ + "STCurrency const& lhs", + "Currency const& rhs" + ], + "lineno": 94, + "name": "operator<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STCurrency.h.ai.md b/include/xrpl/protocol/STCurrency.h.ai.md new file mode 100644 index 0000000000..3453e5bbfa --- /dev/null +++ b/include/xrpl/protocol/STCurrency.h.ai.md @@ -0,0 +1,44 @@ +# `STCurrency.h` — Serialized Currency Field for XRPL Protocol + +`STCurrency` is a thin but essential serialization wrapper in the XRPL protocol type system. Its single job is to carry a `Currency` value — a 160-bit hash (`base_uint<160, detail::CurrencyTag>`) — inside the ledger's binary field framework. It exists because every field in a serialized XRPL object must be an `STBase` subclass; raw `Currency` values cannot appear in transaction or ledger-entry fields without being wrapped this way. + +## Role in the ST Type Hierarchy + +The file follows the same structural pattern as `STAccount` (wraps `AccountID`, 160-bit) and `STIssue` (wraps `Asset`). All three are `final` subclasses of `STBase` that hold a single domain value, expose `value_type` aliased to their inner type, and implement the four-method `STBase` contract: `getSType()`, `getText()`, `getJson()`, `add()`, and the two virtual predicates `isEquivalent()` and `isDefault()`. + +One subtle difference from `STAccount` and `STIssue`: `STCurrency` does **not** mix in `CountedObject`. This means instance counts are not tracked for this type — probably an intentional omission because `STCurrency` appears infrequently in well-formed transactions, but worth noting if you ever add diagnostic instrumentation. + +## The `Currency` Underlying Type + +`Currency` is defined in `UintTypes.h` as `base_uint<160, detail::CurrencyTag>`. The tag type prevents accidental mixing with `NodeID` (also 160 bits) at compile time. The all-zeros 160-bit value is XRP's canonical currency representation (`xrpCurrency()` returns `beast::zero`). Three-character ISO codes like "USD" are encoded into specific byte positions of the 160-bit field per the XRPL specification. The sentinel values `noCurrency()` and `badCurrency()` mark absent or invalid entries; `badCurrency()` encodes what looks like the string "XRP" in the ISO field, which is explicitly banned because early users confused it with native XRP. + +## Construction and Deserialization + +Three constructors cover all usage paths: + +- `STCurrency(SField const& name)` — creates a default (XRP) currency with a field name but no data yet; used when constructing template objects. +- `STCurrency(SField const& name, Currency const& currency)` — the normal programmatic constructor. +- `STCurrency(SerialIter& sit, SField const& name)` — deserializes from a wire stream by reading exactly 160 bits via `sit.get160()`. + +The private `construct(SerialIter&, SField const&)` static factory is the hook for `detail::STVar`, which dispatches object construction from a `SerializedTypeID` at runtime. `STVar` implements small-object optimization: its aligned storage holds up to 72 bytes, and the virtual `copy()`/`move()` overrides call `STBase::emplace()` to place the object in that buffer rather than heap-allocating when `sizeof(STCurrency)` fits. + +## Serialization and Default Semantics + +`add(Serializer& s)` writes the currency as a raw 160-bit string via `s.addBitString(currency_)`. There is no framing — just the 20-byte payload — consistent with how all fixed-width ST scalar types are encoded. + +`isDefault()` returns `true` when `isXRP(currency_)`, i.e., when the 160-bit value is zero. This drives the `STBase` equality convention: the default-constructed `STCurrency{}` represents XRP, and the ledger elides fields at their default values during serialization to reduce payload size. + +`getText()` and `getJson()` both delegate to `to_string(currency_)`, which returns `""` for XRP (all zeros), `"XRP"` for the XRP code, or the 3-character ISO string for IOU currencies. JSON emission intentionally ignores the `JsonOptions` flags — a currency code is always a plain string with no optional decoration. + +## JSON Parsing with `currencyFromJson()` + +The free function `currencyFromJson(SField const& name, Json::Value const& v)` is the input validation gateway. It enforces two invariants before constructing an `STCurrency`: + +1. The JSON value must be a string (not a number or object). +2. The resulting `Currency` must not be `badCurrency()` or `noCurrency()`. + +Notably, `to_currency()` has a legacy quirk documented in `UintTypes.h`: it may silently return `badCurrency()` for invalid inputs rather than failing. `currencyFromJson()` compensates by explicitly rejecting both sentinel values, acting as the defensive layer that protects the rest of the system from malformed input arriving through the API. + +## Comparison Operators + +The header provides two sets of comparison operators. The `STCurrency`-vs-`STCurrency` set gives `==`, `!=`, and `<`, enabling use in sorted containers and comparison chains. The mixed `STCurrency`-vs-`Currency` set provides `==` and `<` for comparing a wrapped field directly against an unwrapped value, which avoids unnecessary construction of a temporary `STCurrency` in lookup contexts. The asymmetric design — mixed comparators only go one direction (`STCurrency` on the left) — follows the same pattern as `STAccount`, keeping the operator set minimal while covering the most common use cases in the transaction engine. \ No newline at end of file diff --git a/include/xrpl/protocol/STExchange.h.ai.json b/include/xrpl/protocol/STExchange.h.ai.json new file mode 100644 index 0000000000..b617978f54 --- /dev/null +++ b/include/xrpl/protocol/STExchange.h.ai.json @@ -0,0 +1,98 @@ +{ + "args": [ + { + "lineno": 15, + "name": "U" + }, + { + "lineno": 15, + "name": "T" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "STExchange" + }, + { + "args": [], + "lineno": 17, + "name": "STExchange, T>" + }, + { + "args": [], + "lineno": 34, + "name": "STExchange" + }, + { + "args": [], + "lineno": 47, + "name": "STExchange" + } + ], + "description": "Provides utilities for converting between serialized XRPL protocol types and C++ types, and for getting, setting, and erasing fields in STObject instances.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STExchange.h", + "functions": [ + { + "args": [ + "st", + "f" + ], + "lineno": 61, + "name": "get" + }, + { + "args": [ + "st", + "f" + ], + "lineno": 77, + "name": "get" + }, + { + "args": [ + "st", + "f", + "t" + ], + "lineno": 84, + "name": "set" + }, + { + "args": [ + "st", + "f", + "size", + "init" + ], + "lineno": 91, + "name": "set" + }, + { + "args": [ + "st", + "f", + "data", + "size" + ], + "lineno": 98, + "name": "set" + }, + { + "args": [ + "st", + "f" + ], + "lineno": 105, + "name": "erase" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STExchange.h.ai.md b/include/xrpl/protocol/STExchange.h.ai.md new file mode 100644 index 0000000000..c3d9f0a205 --- /dev/null +++ b/include/xrpl/protocol/STExchange.h.ai.md @@ -0,0 +1,48 @@ +# `STExchange.h` — Type-Safe Bridge Between Serialized and Native C++ Types + +## Role in the System + +The XRPL wire protocol stores all ledger data in serialized form: integers as `STInteger`, variable-length byte sequences as `STBlob`, and so on — all ultimately derived from `STBase`. Application logic operates on plain C++ types: raw integer scalars, `Slice` (a non-owning view), `Buffer` (an owning byte container). `STExchange.h` is the thin adapter layer that bridges these two worlds. + +Without this file, every call site that reads or writes an `STObject` field would need to know which serialized subclass backs a particular field, perform its own `dynamic_cast`, and manually construct heap-allocated serialized objects. By centralizing that mapping in one traits header, the rest of the codebase can work with native C++ types through a uniform API. + +## The `STExchange` Traits Struct + +The primary abstraction is the `STExchange` traits struct, parametrized on `U` (the serialized type, e.g. `STInteger`) and `T` (the desired native C++ type). Each specialization provides: + +- `value_type` — the canonical C++ representation for that serialized type +- `static void get(std::optional&, U const&)` — extracts a native value from the serialized object +- `static std::unique_ptr set(field, T const&)` — constructs a heap-allocated serialized object ready for insertion into an `STObject` + +Using explicit template specializations rather than virtual dispatch or an inheritance hierarchy keeps all conversions fully resolved at compile time. There is no runtime polymorphism involved in the type mapping itself. + +The partial specialization `STExchange, T>` covers the entire family of integer types (`STUInt8`, `STUInt16`, `STUInt32`, `STUInt64`, `STInt32`) generically with a single template. The `get` simply calls `u.value()` and the `set` calls `std::make_unique>(f, t)` — straightforward, since integers carry no ownership complexity. + +The `STBlob` specializations are explicit full specializations rather than a partial one, and there are two of them: one for `Slice` and one for `Buffer`. This split reflects a deliberate ownership contract. `Slice` is a non-owning view over memory it does not control, so both `get` (which copies data out via `emplace`) and `set` (which copies data in via `data()/size()`) always make copies. `Buffer` owns its memory, so `STExchange` provides an additional move-semantic overload of `set`: when the caller passes an rvalue `Buffer&&`, the `STBlob` is constructed with `std::move(t)`, avoiding any heap allocation beyond the `STBlob` itself. This is important in hot paths that build transaction objects. + +## Free Functions: `get`, `set`, `erase` + +### `get` + +```cpp +template +std::optional get(STObject const& st, TypedField const& f); +``` + +The implementation uses `STObject::peekAtPField` — a non-mutating lookup that does not insert a default value for absent fields, which is critical when merely inspecting an object. Two separate absence conditions are then checked: 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). Only after both checks pass does the function `dynamic_cast` to `U const*`. The comment "This should never happen" on the failed-cast path is accurate: the `TypedField` descriptor already encodes the serialized type at compile time, so a mismatch would be a programming error, not a data validation failure. + +A second overload of `get` omits the explicit `T` parameter and infers it from `U::value_type`. This is the ergonomic default — callers write `get(st, sfSequence)` rather than `get(st, sfSequence)`. The explicit-`T` overload exists for cases where you want a different view of the same underlying wire type, most practically when reading an `STBlob` field as either `Slice` (for temporary inspection) or `Buffer` (when you need ownership of the bytes). + +### `set` + +The primary `set` template uses `std::decay` to strip cv-qualifiers and references before selecting the `STExchange` specialization, and `std::forward` to preserve the value category so the move-semantic `Buffer&&` overload in `STExchange` fires correctly when an rvalue is passed. Two additional overloads handle `STBlob` specifically: one accepts a `(size, init)` pair where `init` is a callable invoked to populate the blob in-place (useful for constructing large blobs without an intermediate copy), and one accepts a raw `(void const*, size_t)` pair for C-style interop. + +### `erase` + +`erase` delegates to `STObject::makeFieldAbsent`, which marks the field as not present in serialized output without removing it from the object's declared schema. This distinction matters for canonical serialization: the field slot still exists in the schema but contributes nothing to the wire encoding. + +## Design Rationale + +The separation of the type-mapping logic into a standalone traits layer — rather than embedding it in `STObject`, `STBlob`, or `STInteger` — is the key architectural decision here. If `STBlob` itself knew about `Buffer` and `Slice` conversions, or if `STInteger` knew about all the integer types, adding a new C++ view of an existing wire type would require modifying core protocol classes. With `STExchange`, a new specialization like `STExchange>` can be added in one place without touching any of the serialization infrastructure. + +The `TypedField` parameter used throughout all free functions enforces type safety at the field-descriptor level: passing a `TypedField` where a `TypedField` is expected is a compile error, not a runtime `dynamic_cast` failure. \ No newline at end of file diff --git a/include/xrpl/protocol/STInteger.h.ai.json b/include/xrpl/protocol/STInteger.h.ai.json new file mode 100644 index 0000000000..43d1bdc6d3 --- /dev/null +++ b/include/xrpl/protocol/STInteger.h.ai.json @@ -0,0 +1,181 @@ +{ + "args": [ + { + "lineno": 13, + "name": "Integer v" + }, + { + "lineno": 14, + "name": "SField const& n" + }, + { + "lineno": 14, + "name": "Integer v" + }, + { + "lineno": 15, + "name": "SerialIter& sit" + }, + { + "lineno": 15, + "name": "SField const& name" + }, + { + "lineno": 19, + "name": "JsonOptions" + }, + { + "lineno": 23, + "name": "Serializer& s" + }, + { + "lineno": 27, + "name": "STBase const& t" + }, + { + "lineno": 29, + "name": "value_type const& v" + }, + { + "lineno": 33, + "name": "Integer v" + }, + { + "lineno": 38, + "name": "std::size_t n" + }, + { + "lineno": 38, + "name": "void* buf" + }, + { + "lineno": 40, + "name": "std::size_t n" + }, + { + "lineno": 40, + "name": "void* buf" + } + ], + "classes": [ + { + "args": [ + "Integer v", + "SField const& n, Integer v", + "SerialIter& sit, SField const& name" + ], + "lineno": 8, + "name": "STInteger" + } + ], + "description": "Defines the STInteger template class for representing serialized integer types in the XRPL protocol, along with type aliases for common integer types. Provides serialization, deserialization, and utility methods for integer fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STInteger.h", + "functions": [ + { + "args": [ + "Integer v" + ], + "lineno": 13, + "name": "STInteger" + }, + { + "args": [ + "SField const& n", + "Integer v" + ], + "lineno": 14, + "name": "STInteger" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 15, + "name": "STInteger" + }, + { + "args": [], + "lineno": 17, + "name": "getSType" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 19, + "name": "getJson" + }, + { + "args": [], + "lineno": 21, + "name": "getText" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 23, + "name": "add" + }, + { + "args": [], + "lineno": 25, + "name": "isDefault" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 27, + "name": "isEquivalent" + }, + { + "args": [ + "value_type const& v" + ], + "lineno": 29, + "name": "operator=" + }, + { + "args": [], + "lineno": 31, + "name": "value" + }, + { + "args": [ + "Integer v" + ], + "lineno": 33, + "name": "setValue" + }, + { + "args": [], + "lineno": 35, + "name": "operator Integer" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 38, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 40, + "name": "move" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STInteger.h.ai.md b/include/xrpl/protocol/STInteger.h.ai.md new file mode 100644 index 0000000000..84412dcd39 --- /dev/null +++ b/include/xrpl/protocol/STInteger.h.ai.md @@ -0,0 +1,50 @@ +# `STInteger.h` — Serialized Integer Types for XRPL Protocol Fields + +## Role in the System + +`STInteger` is the template class that wraps plain C++ integer values inside the XRPL "Serialized Type" (ST) framework. Every integer-valued field in a ledger entry, transaction, or metadata object — sequence numbers, flags, fees, ledger indices, timestamps, transaction types — is represented at runtime as one of the five concrete instantiations defined at the bottom of this file: + +```cpp +using STUInt8 = STInteger; +using STUInt16 = STInteger; +using STUInt32 = STInteger; +using STUInt64 = STInteger; +using STInt32 = STInteger; +``` + +These aliases are the types that `STObject`'s `getFieldU32()`, `setFieldU32()`, `getFieldU64()`, `getFieldI32()`, etc., read and write through. + +## Inheritance Design + +`STInteger` inherits from two bases. `STBase` provides the field-name binding (an `SField const*`), the virtual serialization interface (`add()`, `getJson()`, `getText()`), and the `emplace()` helper. `CountedObject>` adds lock-free per-template-instantiation instance counting, maintaining a global linked list of live objects for diagnostics. Each template instantiation gets its own atomic counter, so the diagnostic system can report live `STUInt32` and `STUInt64` counts separately. + +## Split Between Header and Implementation + +The class declares all member functions in the header, but the implementations of `getSType()`, `getText()`, `getJson()`, and the deserialization constructor (`SerialIter&, SField const&`) are explicit full specializations in `STInteger.cpp`. The generic versions of `add()`, `isDefault()`, `isEquivalent()`, `operator=`, `value()`, `setValue()`, and `operator Integer()` are short enough to inline and live directly in the header. + +This split is intentional: the per-type specializations carry semantic knowledge that doesn't belong in a generic header. `STUInt8::getText()` checks whether the field name is `sfTransactionResult` and, if so, converts the raw byte through `transResultInfo()` to a human-readable TER string. `STUInt16::getJson()` maps `sfLedgerEntryType` and `sfTransactionType` values to their format-name strings via `LedgerFormats::getInstance()` and `TxFormats::getInstance()`. `STUInt32::getText()` and `getJson()` handle `sfPermissionValue` lookups. These concerns would drag heavy dependencies (`TER.h`, `LedgerFormats.h`, `TxFormats.h`, `Permissions.h`) into every translation unit that includes `STInteger.h` — the split keeps those out of the header entirely. + +`STUInt64::getJson()` has a subtler concern: JSON does not safely represent 64-bit integers in all environments. By default the method emits the value as a hex string; if the field carries the `SField::sMD_BaseTen` metadata flag it switches to a decimal string. Either way, the value is always a string in JSON, never a bare number. + +## Serialization and Invariant Assertions + +`add()` writes the raw integer into a `Serializer` using `s.addInteger(value_)`. Two `XRPL_ASSERT` guards fire in debug builds: one checks that the field is marked binary (no text-only fields should reach the serializer path), and one verifies that the field's declared `fieldType` matches the `getSType()` return for this instantiation. These assertions catch mis-wiring of field definitions early. + +`isDefault()` returns `value_ == 0`. The ST framework uses this to omit default-valued fields from serialized output, keeping wire representations canonical and compact. + +`isEquivalent()` performs a `dynamic_cast` to verify the runtime type matches before comparing values. This guards against comparing an `STUInt32` with an `STUInt64` that happen to hold the same bit pattern — a subtle correctness issue that a plain value compare would miss. + +## Small-Buffer Optimization via `emplace()` + +The private `copy()` and `move()` overrides delegate to `STBase::emplace()`: + +```cpp +STBase* copy(std::size_t n, void* buf) const override { return emplace(n, buf, *this); } +STBase* move(std::size_t n, void* buf) override { return emplace(n, buf, std::move(*this)); } +``` + +`emplace()` checks whether `sizeof(STInteger)` fits within `n` bytes; if it does, it placement-new's the object into the caller-supplied buffer rather than heap-allocating. This is the mechanism `detail::STVar` — the discriminated-union variant that stores polymorphic ST objects inside `STObject` — uses to avoid a heap allocation for small types. `STVar` is granted `friend` access so it can call these private methods directly. + +## Value Access + +Three orthogonal access points are provided for the wrapped value: `value() const noexcept` for explicit read, `operator Integer() const` for implicit conversion in arithmetic or comparison contexts, `operator=(value_type const&)` and `setValue(Integer)` for mutation. Having both an explicit accessor and an implicit conversion is intentional: code that explicitly wants a raw integer calls `.value()` for clarity, while code working with the ST object in a context that expects `Integer` (e.g., passing to a function taking `std::uint32_t`) benefits from the implicit conversion without a verbose cast. \ No newline at end of file diff --git a/include/xrpl/protocol/STIssue.h.ai.json b/include/xrpl/protocol/STIssue.h.ai.json new file mode 100644 index 0000000000..e735cbf923 --- /dev/null +++ b/include/xrpl/protocol/STIssue.h.ai.json @@ -0,0 +1,177 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "STIssue" + } + ], + "description": "Defines the STIssue class, a serialized type wrapper for Asset (representing an issued currency or XRP) in the XRPL protocol, with serialization, comparison, and type-safe access methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STIssue.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "STIssue" + }, + { + "args": [ + "STIssue const& rhs" + ], + "lineno": 14, + "name": "STIssue" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 16, + "name": "STIssue" + }, + { + "args": [ + "SField const& name", + "A const& issue" + ], + "lineno": 18, + "name": "STIssue" + }, + { + "args": [ + "SField const& name" + ], + "lineno": 20, + "name": "STIssue" + }, + { + "args": [ + "STIssue const& rhs" + ], + "lineno": 22, + "name": "operator=" + }, + { + "args": [], + "lineno": 25, + "name": "get" + }, + { + "args": [], + "lineno": 29, + "name": "holds" + }, + { + "args": [], + "lineno": 33, + "name": "value" + }, + { + "args": [ + "Asset const& issue" + ], + "lineno": 36, + "name": "setIssue" + }, + { + "args": [], + "lineno": 39, + "name": "getSType" + }, + { + "args": [], + "lineno": 42, + "name": "getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 44, + "name": "getJson" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 46, + "name": "add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 48, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 50, + "name": "isDefault" + }, + { + "args": [ + "STIssue const& lhs", + "STIssue const& rhs" + ], + "lineno": 53, + "name": "operator==" + }, + { + "args": [ + "STIssue const& lhs", + "STIssue const& rhs" + ], + "lineno": 56, + "name": "operator<=>" + }, + { + "args": [ + "STIssue const& lhs", + "Asset const& rhs" + ], + "lineno": 59, + "name": "operator==" + }, + { + "args": [ + "STIssue const& lhs", + "Asset const& rhs" + ], + "lineno": 62, + "name": "operator<=>" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 66, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 67, + "name": "move" + }, + { + "args": [ + "SField const& name", + "Json::Value const& v" + ], + "lineno": 74, + "name": "issueFromJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STIssue.h.ai.md b/include/xrpl/protocol/STIssue.h.ai.md new file mode 100644 index 0000000000..d379053e13 --- /dev/null +++ b/include/xrpl/protocol/STIssue.h.ai.md @@ -0,0 +1,42 @@ +# `STIssue` — Serialized Issue Type + +## Role in the Protocol + +`STIssue` is the canonical serialized representation of a fungible asset identifier on the XRP Ledger. It bridges two distinct layers of the codebase: the protocol's polymorphic `STBase` serialization framework (the "ST" in its name stands for Serialized Type) and the `Asset` abstraction that unifies the ledger's three asset species — native XRP, IOU tokens, and Multi-Purpose Tokens (MPT). Any time an asset identifier must be embedded in a ledger object or transaction field and travel through the binary codec or JSON API, it passes through `STIssue`. + +## What It Wraps + +The core data member is a single `Asset asset_` initialized to `xrpIssue()` by default. `Asset` is itself a thin `std::variant`: `Issue` encodes either XRP (currency code = zero) or an IOU (currency code + issuer AccountID), while `MPTIssue` encodes an MPT issuance identified by its 192-bit `MPTID`. This layering means `STIssue` is simultaneously aware of all three asset classes without duplicating their semantics — it delegates all asset-level logic to `Asset` and only adds the serialization machinery required by the `STBase` contract. + +## Construction and Validation + +Four constructors cover the expected usage patterns. The default constructor produces an XRP `STIssue`. The `SField`-only constructor creates an XRP `STIssue` tagged to a specific protocol field name, which is the typical way fields are named within a parent `STObject`. The templated constructor accepts any type satisfying the `AssetType` concept (`Issue`, `MPTIssue`, `MPTID`, or `Asset`) and performs an explicit consistency check via `isConsistent()` for the `Issue` case — throwing `std::runtime_error` if the currency/account native flag combination is invalid (e.g., XRP currency paired with a non-XRP account). This invariant is the constructor's primary defensive responsibility; MPT issuances are always considered consistent. + +The deserialization constructor `STIssue(SerialIter&, SField const&)` implements an important disambiguation protocol in the binary format. It reads a 160-bit field first. If that field is the XRP currency code (all zeros), the asset is XRP and deserialization is complete. Otherwise it reads a second 160-bit field: + +- If that second field is the **`noAccount()` sentinel** (the "black hole" address), the asset is an MPT. A 32-bit sequence number follows, and these three values are assembled into an `MPTID` via `memcpy`. +- Otherwise, the two fields form the `(currency, account)` pair of an IOU `Issue`, again verified with `isConsistent()`. + +This design embeds the asset type discriminator directly in the wire format without a separate tag byte, relying on the `noAccount()` sentinel as an otherwise-illegal value that would never appear as a real IOU issuer. + +## Type-Safe Access + +`holds()` and `get()` expose the same tagged-access pattern as `Asset`, constrained by the `ValidIssueType` concept to only `Issue` or `MPTIssue`. `holds()` delegates to `asset_.holds()`. `get()` throws `std::runtime_error` if the underlying variant does not hold the requested type before returning the typed reference. The raw `Asset` is also accessible via `value()`, which is `noexcept` and returns a `const` reference. + +`setIssue()` allows mutation of the underlying asset while re-running the consistency check. However, the check is guarded by `holds()` — only IOU issues are validated on set (MPT assignments always pass through), mirroring the constructor logic. + +## Serialization + +`add(Serializer& s)` serializes by visiting the `Asset` variant. XRP writes only its 160-bit currency code; IOUs write both the currency code and the issuer AccountID; MPTs write the issuer account, the `noAccount()` sentinel, and the 32-bit sequence extracted from the `MPTID`. This produces variable-length output (160, 320, or 352 bits) depending on asset type. + +`getSType()` returns `STI_ISSUE`, identifying this type in the protocol's type registry. `isDefault()` returns `true` only when the asset is exactly `xrpIssue()`, consistent with the field's default value being XRP — MPT issuances are never treated as defaults. + +## Comparison and JSON + +Four comparison operators are defined as `constexpr friend` functions. Two compare `STIssue` to `STIssue` and two compare `STIssue` directly to `Asset`, which avoids the need to unwrap the type manually when comparing against raw `Asset` values elsewhere in the engine. All comparisons delegate entirely to `Asset`'s operators, which in turn use `std::visit` across the variant to handle cross-type ordering (where `Issue` sorts after `MPTIssue`). + +`getJson(JsonOptions)` and `getText()` both forward to `Asset`'s equivalent accessors. The free function `issueFromJson(SField const&, Json::Value const&)` constructs an `STIssue` by parsing the JSON into an `Asset` via `assetFromJson()` and wrapping it, providing the canonical entry point when deserializing a ledger object's issue field from an API request. + +## Infrastructure Details + +`STIssue` inherits `CountedObject` alongside `STBase`, which instruments construction and destruction for runtime diagnostics. The class is declared `final`, preventing inheritance — the `STBase` hierarchy's polymorphism is already fully satisfied without extension. The private `copy()` and `move()` overrides use `STBase::emplace()`, a placement-new helper that places the object into a caller-supplied buffer when it fits, or heap-allocates otherwise. This pattern is used by `detail::STVar` (which is a `friend`) to efficiently store `STBase` subtypes within fixed-size inline storage. \ No newline at end of file diff --git a/include/xrpl/protocol/STLedgerEntry.h.ai.json b/include/xrpl/protocol/STLedgerEntry.h.ai.json new file mode 100644 index 0000000000..e64d761769 --- /dev/null +++ b/include/xrpl/protocol/STLedgerEntry.h.ai.json @@ -0,0 +1,217 @@ +{ + "args": [ + { + "lineno": 19, + "name": "k" + }, + { + "lineno": 20, + "name": "type" + }, + { + "lineno": 20, + "name": "key" + }, + { + "lineno": 21, + "name": "sit" + }, + { + "lineno": 21, + "name": "index" + }, + { + "lineno": 22, + "name": "sit" + }, + { + "lineno": 22, + "name": "index" + }, + { + "lineno": 23, + "name": "object" + }, + { + "lineno": 23, + "name": "index" + }, + { + "lineno": 34, + "name": "options" + }, + { + "lineno": 50, + "name": "rules" + }, + { + "lineno": 53, + "name": "txID" + }, + { + "lineno": 53, + "name": "ledgerSeq" + }, + { + "lineno": 53, + "name": "prevTxID" + }, + { + "lineno": 53, + "name": "prevLedgerID" + }, + { + "lineno": 66, + "name": "n" + }, + { + "lineno": 66, + "name": "buf" + }, + { + "lineno": 68, + "name": "n" + }, + { + "lineno": 68, + "name": "buf" + } + ], + "classes": [ + { + "args": [ + "Keylet const& k", + "LedgerEntryType type, uint256 const& key", + "SerialIter& sit, uint256 const& index", + "SerialIter&& sit, uint256 const& index", + "STObject const& object, uint256 const& index" + ], + "lineno": 13, + "name": "STLedgerEntry" + } + ], + "description": "Defines the STLedgerEntry class, representing a ledger entry in the XRPL, including its key, type, and serialization/deserialization logic. Provides methods for accessing entry metadata, threading, and JSON/text representations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STLedgerEntry.h", + "functions": [ + { + "args": [ + "Keylet const& k" + ], + "lineno": 19, + "name": "STLedgerEntry" + }, + { + "args": [ + "LedgerEntryType type", + "uint256 const& key" + ], + "lineno": 20, + "name": "STLedgerEntry" + }, + { + "args": [ + "SerialIter& sit", + "uint256 const& index" + ], + "lineno": 21, + "name": "STLedgerEntry" + }, + { + "args": [ + "SerialIter&& sit", + "uint256 const& index" + ], + "lineno": 22, + "name": "STLedgerEntry" + }, + { + "args": [ + "STObject const& object", + "uint256 const& index" + ], + "lineno": 23, + "name": "STLedgerEntry" + }, + { + "args": [], + "lineno": 25, + "name": "getSType" + }, + { + "args": [], + "lineno": 28, + "name": "getFullText" + }, + { + "args": [], + "lineno": 31, + "name": "getText" + }, + { + "args": [ + "JsonOptions options" + ], + "lineno": 34, + "name": "getJson" + }, + { + "args": [], + "lineno": 41, + "name": "key" + }, + { + "args": [], + "lineno": 47, + "name": "getType" + }, + { + "args": [ + "Rules const& rules" + ], + "lineno": 50, + "name": "isThreadedType" + }, + { + "args": [ + "uint256 const& txID", + "std::uint32_t ledgerSeq", + "uint256& prevTxID", + "std::uint32_t& prevLedgerID" + ], + "lineno": 53, + "name": "thread" + }, + { + "args": [], + "lineno": 61, + "name": "setSLEType" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 66, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 68, + "name": "move" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STLedgerEntry.h.ai.md b/include/xrpl/protocol/STLedgerEntry.h.ai.md new file mode 100644 index 0000000000..4a19fc1125 --- /dev/null +++ b/include/xrpl/protocol/STLedgerEntry.h.ai.md @@ -0,0 +1,41 @@ +# `STLedgerEntry.h` — The Typed Ledger Object + +## Role in the System + +`STLedgerEntry` (universally aliased as `SLE` throughout the codebase) is the C++ representation of a single object residing in the XRPL state ledger. The ledger state is an associative map — a `SHAMap` — where every object has a 256-bit key and a structured binary payload. `STLedgerEntry` wraps both: it carries the `uint256 key_` that identifies its position in that map and a `LedgerEntryType type_` that names what the object actually is (an account root, an offer, an escrow, a trust line, etc.). + +The class inherits from `STObject`, which provides the generic serialized-type field container, and from `CountedObject`, which hooks the instance into a lock-free global counter for diagnostic memory accounting. Inheriting from `STBase` (via `STObject`) gives it a uniform serialization interface shared with transaction fields and inner objects. + +## The Keylet Contract + +A `Keylet` is a struct that bundles a `uint256` key together with a `LedgerEntryType`. The primary constructor `STLedgerEntry(Keylet const& k)` enforces a critical invariant: it looks up the type in the `LedgerFormats` singleton. If the type is not registered, it throws immediately rather than producing an object in an unknown state. It then calls `set(format->getSOTemplate())` to populate the `STObject` field list with the correct template for that type, and writes `sfLedgerEntryType` into the object itself so the type is embedded in serialized form. This design means you cannot accidentally create an SLE whose type field disagrees with its in-memory `type_` member. + +The convenience constructor `STLedgerEntry(LedgerEntryType, uint256)` simply delegates to the `Keylet` path, providing a less type-safe but ergonomically shorter spelling used in contexts where the type is already known statically. + +## Deserialization and `setSLEType()` + +When an SLE is read from the ledger — either via a `SerialIter` (raw bytes from the SHAMap) or by wrapping an already-parsed `STObject` — the type is not known at construction entry. Both of those constructors initialize `type_` to the sentinel `ltANY` and then call the private `setSLEType()`. This method reads `sfLedgerEntryType` from the just-deserialized field data, finds the matching format, sets `type_` to the real value, and calls `applyTemplate()` to enforce the SOTemplate. `applyTemplate()` can throw, making deserialization of malformed or unrecognized ledger objects a hard failure rather than silent corruption. + +The rvalue `SerialIter&&` overload exists purely for convenience — it forwards to the lvalue overload via a `NOLINT` annotation acknowledging that the rvalue is not truly moved, since `SerialIter` is consumed by position rather than by move semantics. + +## Transaction Threading + +XRPL ledger objects support a *transaction threading* mechanism: each object records the ID and ledger sequence of the most recent transaction that modified it (`sfPreviousTxnID` / `sfPreviousTxnLgrSeq`). This creates a traceable chain of modifications per ledger object, independent of the transaction history tree. + +`isThreadedType(Rules const& rules)` gates this mechanism. Several object types — `ltDIR_NODE`, `ltAMENDMENTS`, `ltFEE_SETTINGS`, `ltNEGATIVE_UNL`, and `ltAMM` — only gained `PreviousTxnID` fields with the `fixPreviousTxnID` amendment. Before that amendment activates, `isThreadedType()` returns false for those types even if the field technically exists in the template, preventing premature use of the threading feature on objects that historically lacked it. + +`thread(txID, ledgerSeq, prevTxID, prevLedgerID)` performs the actual update: it captures the current `sfPreviousTxnID` into the caller's out-parameter `prevTxID`, writes the new transaction ID and ledger sequence, and returns `true`. If the object is already threaded to the same `txID`, it returns `false` — an idempotency guard that prevents double-application if a transaction is replayed. + +## JSON Representation + +`getJson()` delegates to `STObject::getJson()` and then injects the `index` field (the SHAMap key in hex). There is one special case: for `ltMPTOKEN_ISSUANCE` objects, it also computes and injects `mpt_issuance_id` using `makeMptID(sfSequence, sfIssuer)` — a derived identifier that API consumers need but that is not stored as a field inside the object itself. + +## Copy/Move and `STVar` Integration + +The private `copy()` and `move()` overrides satisfy the `STBase` interface required by `detail::STVar`, the polymorphic value type that `STObject` uses internally to store heterogeneous fields in a `std::vector` with small-buffer optimization. These methods delegate to the `emplace()` helper to construct the object in an externally provided buffer, enabling the owning `STVar` to avoid heap allocation for small objects. `friend class detail::STVar` is declared to enable this. + +## Key Design Observations + +The `final` keyword on `STLedgerEntry` signals that the class is not meant to be further subclassed despite its polymorphic base. Ledger entry type diversity is handled entirely through the `SOTemplate` / `LedgerFormats` registration system at runtime, not through C++ subclass hierarchies. This keeps the type system flat while still enforcing per-type field schemas. + +The `using SLE = STLedgerEntry` alias at the bottom of the header is pervasive throughout the rippled codebase — nearly all code that reads or writes ledger state refers to `SLE` rather than the full name. The shared-pointer typedefs (`SLE::pointer`, `SLE::ref`, `SLE::const_pointer`, `SLE::const_ref`) standardize ownership conventions across ledger-accessing code, where entries are almost always heap-allocated and passed by `shared_ptr`. \ No newline at end of file diff --git a/include/xrpl/protocol/STNumber.h.ai.json b/include/xrpl/protocol/STNumber.h.ai.json new file mode 100644 index 0000000000..c508f11bb1 --- /dev/null +++ b/include/xrpl/protocol/STNumber.h.ai.json @@ -0,0 +1,49 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 27, + "name": "STNumber" + }, + { + "args": [], + "lineno": 65, + "name": "NumberParts" + } + ], + "description": "Defines the STNumber class, a serializable number type for use in XRPL ledger entries and transactions, integrating the Number type with the serialization framework and allowing association with an Asset.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STNumber.h", + "functions": [ + { + "args": [ + "out", + "rhs" + ], + "lineno": 61, + "name": "operator<<" + }, + { + "args": [ + "number" + ], + "lineno": 68, + "name": "partsFromString" + }, + { + "args": [ + "field", + "value" + ], + "lineno": 71, + "name": "numberFromJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STNumber.h.ai.md b/include/xrpl/protocol/STNumber.h.ai.md new file mode 100644 index 0000000000..37cb19fc90 --- /dev/null +++ b/include/xrpl/protocol/STNumber.h.ai.md @@ -0,0 +1,37 @@ +# `STNumber.h` — Serializable Asset-Contextual Number + +## Role in the System + +`STNumber` solves a precise storage problem in XRPL's ledger model: how to store a numeric quantity whose asset type is *implicit from context* rather than embedded in the value itself. `STAmount` — the more familiar sibling — bundles a `Number` together with an `Asset` (XRP, IOU, or MPT) inside every ledger field. For objects like `Vault`, `LoanBroker`, and `Loan`, which have a single governing asset, repeating that asset in every field wastes space and couples each field value to asset data that already lives in the same object. `STNumber` eliminates that redundancy: it stores *only* the numeric value while the asset identity is injected at runtime through the `STTakesAsset` mechanism. + +## Class Hierarchy and Double Inheritance + +`STNumber` inherits from two bases: `STTakesAsset` (which itself extends `STBase`) and `CountedObject`. The `STBase` side provides the full serialization contract — `getSType()`, `add()`, `getText()`, `isEquivalent()`, `isDefault()`, and the placement-new `copy()`/`move()` pair. `CountedObject` is a lightweight RAII wrapper for memory diagnostics. The `STTakesAsset` layer adds a single data member, `std::optional asset_`, which is populated at runtime and deliberately never written to the ledger. + +## The `STTakesAsset` Contract and Asset Association + +`STTakesAsset` defines the abstract concept of a serialization type that *may* hold a transient asset reference. The association point is `associateAsset(Asset const&)`. `STNumber`'s override does two things: it stores the asset in `asset_` (inherited), and it immediately calls `roundToAsset(a, value_)` to round the internal `Number` to the precision that asset allows. This is a key design invariant — after association, the stored value is already rounded, so `add()` can assert that a second rounding during serialization produces no change. + +The flag `SField::sMD_NeedsAsset` is declared on all `NUMBER`-type SFields in `sfields.macro` (e.g., `sfAssetsAvailable`, `sfAssetsTotal`, `sfDebtTotal`, `sfPrincipalOutstanding`). The free function `associateAsset(STLedgerEntry&, Asset&)` iterates a ledger entry's fields, finds those with `sMD_NeedsAsset`, and calls `associateAsset` on each. Transactors for `Vault`, `LoanBroker`, and `Loan` invoke this near the end of `doApply()` after all modifications are complete — this is the intended association lifecycle. + +## Serialization Wire Format + +On the wire, `STNumber` writes exactly 96 bits: a 64-bit signed mantissa (`s.add64`) followed by a 32-bit signed exponent (`s.add32`). This contrasts with `STAmount`, which embeds asset metadata. The deserializing constructor reads the mantissa with `sit.geti64()` and the exponent with `sit.geti32()` in explicitly-separated statements (the comment documents that ordering matters for the stream iterator's side effects). + +The `add()` method contains a notable defensive pattern: when `sMD_NeedsAsset` is set but no asset has been associated (i.e., `asset_` is `std::nullopt`), a debug-only assertion verifies that the active `Number::getMantissaScale()` is `MantissaRange::large`. This guards the backward-compatibility boundary — `STNumber` should only be serialized when the `SingleAssetVault` or `LendingProtocol` amendments are active, which force the large mantissa scale. + +## The Underlying `Number` Type + +`Number` is a floating-point representation with an internal signed 64-bit unsigned mantissa and a signed integer exponent, supporting two normalization ranges. The "small" range (mantissa in `[10^15, 10^16−1]`) matches `STAmount`'s IOU range. The "large" range (mantissa in `[10^18, 10^19−1]`) provides sufficient precision to represent all valid XRP and MPT integer values and is required by the Lending Protocol. The active range is thread-local and amendment-gated. + +`STNumber` exposes the full `Number` interface implicitly through the `operator Number() const` conversion operator, allowing it to be passed directly wherever `Number` is expected. Assignment from `Number` is handled by `operator=(Number const& rhs)`, which delegates to `setValue()`. This design makes `STNumber` feel like a natural `Number` in arithmetic expressions while retaining its serialization identity. + +## JSON Parsing Utilities + +Two free functions support transaction submission. `partsFromString()` parses a decimal string (with optional sign, fractional part, and scientific-notation exponent) using a compiled `boost::regex` and returns a `NumberParts` struct containing a raw `uint64_t` mantissa, `int` exponent, and sign flag. `numberFromJson()` accepts an `SField` and a `Json::Value` (integer, unsigned, or string) and constructs a fully-normalized `STNumber`. It asserts that no active transaction rules are present, restricting its use to pre-transactor JSON deserialization paths where user-supplied values are parsed before ledger rules take effect. + +## Non-Obvious Design Decisions + +The decision to split `STAmount` into `STNumber + contextual Asset` rather than introducing a new opaque field type means that existing serialization infrastructure (field types, type IDs, `Serializer`/`SerialIter`) required only a new `STI_NUMBER` type code and a new SField metadata flag, not a new wire format mechanism. The `STTakesAsset` intermediate class is deliberately minimal — it only stores the optional asset and provides a virtual `associateAsset` — so other future field types could inherit the same pattern without pulling in `STNumber`'s rounding semantics. + +The `isDefault()` check compares against `Number()` (the default-constructed zero value), which uses a sentinel exponent of `std::numeric_limits::lowest()` rather than zero, ensuring that a zero-valued `STNumber` round-trips correctly through `isDefault()` without false positives. \ No newline at end of file diff --git a/include/xrpl/protocol/STObject.h.ai.json b/include/xrpl/protocol/STObject.h.ai.json new file mode 100644 index 0000000000..3fe88cb5ba --- /dev/null +++ b/include/xrpl/protocol/STObject.h.ai.json @@ -0,0 +1,243 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 22, + "name": "STObject" + }, + { + "args": [], + "lineno": 210, + "name": "Proxy" + }, + { + "args": [], + "lineno": 258, + "name": "ValueProxy" + }, + { + "args": [], + "lineno": 307, + "name": "OptionalProxy" + }, + { + "args": [], + "lineno": 373, + "name": "FieldErr" + } + ], + "description": "Defines the STObject class and related proxy classes for representing and manipulating XRPL serialized objects, including field access, mutation, and serialization logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STObject.h", + "functions": [ + { + "args": [ + "SField const& field" + ], + "lineno": 18, + "name": "throwFieldNotFound" + }, + { + "args": [ + "detail::STVar const& e" + ], + "lineno": 44, + "name": "operator()" + }, + { + "args": [], + "lineno": 312, + "name": "begin" + }, + { + "args": [], + "lineno": 317, + "name": "end" + }, + { + "args": [], + "lineno": 322, + "name": "empty" + }, + { + "args": [ + "std::size_t n" + ], + "lineno": 327, + "name": "reserve" + }, + { + "args": [], + "lineno": 332, + "name": "isFree" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 337, + "name": "addWithoutSigningFields" + }, + { + "args": [], + "lineno": 343, + "name": "getSerializer" + }, + { + "args": [ + "Args&&... args" + ], + "lineno": 350, + "name": "emplace_back" + }, + { + "args": [], + "lineno": 356, + "name": "getCount" + }, + { + "args": [ + "int offset" + ], + "lineno": 361, + "name": "peekAtIndex" + }, + { + "args": [ + "int offset" + ], + "lineno": 366, + "name": "getIndex" + }, + { + "args": [ + "int offset" + ], + "lineno": 371, + "name": "peekAtPIndex" + }, + { + "args": [ + "int offset" + ], + "lineno": 376, + "name": "getPIndex" + }, + { + "args": [ + "TypedField const& f" + ], + "lineno": 381, + "name": "operator[]" + }, + { + "args": [ + "OptionaledField const& of" + ], + "lineno": 386, + "name": "operator[]" + }, + { + "args": [ + "TypedField const& f" + ], + "lineno": 391, + "name": "operator[]" + }, + { + "args": [ + "OptionaledField const& of" + ], + "lineno": 396, + "name": "operator[]" + }, + { + "args": [ + "TypedField const& f" + ], + "lineno": 401, + "name": "at" + }, + { + "args": [ + "OptionaledField const& of" + ], + "lineno": 422, + "name": "at" + }, + { + "args": [ + "TypedField const& f" + ], + "lineno": 441, + "name": "at" + }, + { + "args": [ + "OptionaledField const& of" + ], + "lineno": 446, + "name": "at" + }, + { + "args": [ + "SField const& field", + "base_uint<160, Tag> const& v" + ], + "lineno": 451, + "name": "setFieldH160" + }, + { + "args": [ + "STObject const& o" + ], + "lineno": 466, + "name": "operator!=" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 471, + "name": "getFieldByValue" + }, + { + "args": [ + "SField const& field", + "V const& empty" + ], + "lineno": 491, + "name": "getFieldByConstRef" + }, + { + "args": [ + "SField const& field", + "V value" + ], + "lineno": 509, + "name": "setFieldUsingSetValue" + }, + { + "args": [ + "SField const& field", + "T const& value" + ], + "lineno": 527, + "name": "setFieldUsingAssignment" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 545, + "name": "peekField" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STObject.h.ai.md b/include/xrpl/protocol/STObject.h.ai.md new file mode 100644 index 0000000000..dccb91dc28 --- /dev/null +++ b/include/xrpl/protocol/STObject.h.ai.md @@ -0,0 +1,66 @@ +# `include/xrpl/protocol/STObject.h` + +## Role in the System + +`STObject` is the foundational container for all structured data in the XRPL protocol — transactions, ledger entries, and inner objects alike. It occupies the center of the Serialized Type ("ST") hierarchy defined in `STBase.h`, providing the recursive composition model through which heterogeneous named fields are stored, serialized, deserialized, hashed, and inspected. Both `STTx` (transaction wire format) and `STLedgerEntry` (on-ledger state) are `final` subclasses of `STObject`, meaning everything in ledger I/O flows through this type. + +## Architecture Overview + +`STObject` inherits from two bases: `STBase`, the polymorphic root of all serialized types, and `CountedObject`, a CRTP diagnostic aid that maintains a global lock-free count of live instances for health monitoring. Its internal storage is a `std::vector`, where `STVar` is a type-erased wrapper with a 72-byte small-object optimization — types that fit (most scalars, hashes, amounts) are stored directly in an aligned buffer without heap allocation; larger types fall back to `new`. + +An `STObject` can operate in one of two modes: + +1. **Free mode** (`isFree()` returns `true`, `mType == nullptr`): no schema constraint. Fields are added ad hoc, iteration order reflects insertion order, and optional-field semantics are not enforced. + +2. **Template mode**: an associated `SOTemplate const*` pointer imposes a schema. Each field in the template has a `SOEStyle`: + - `soeREQUIRED` — must be present; reading it always succeeds. + - `soeOPTIONAL` — may be absent; reads return a default-constructed value or throw depending on the access path. + - `soeDEFAULT` — present logically but elided from serialization when it equals the type's zero value; a space-saving canonicalization used for common fields like flags. + +The template pointer is assigned either at construction (via the `SOTemplate const& type` constructors) or later via `applyTemplate()`. This deferred binding allows objects to be deserialized from raw bytes and then validated against a known schema. + +## The Proxy System — Why It Exists + +The older access interface exposes named methods like `getFieldU32()`, `setFieldU32()`, `getAccountID()`, etc. These are type-unsafe in the sense that nothing at compile time prevents passing the wrong `SField` for a given type; the mismatch is caught at runtime via `dynamic_cast` and throws `std::runtime_error("Wrong field type")`. + +The modern interface, introduced through template machinery, uses `TypedField` (a subclass of `SField` parameterized on the concrete ST type it carries) and `OptionaledField` (a thin wrapper produced by `operator~(TypedField)`) as keys to `operator[]` and `at()`. These carry the ST type at compile time, enabling statically checked access patterns: + +```cpp +// Required field — returns T::value_type, throws FieldErr if absent +auto amount = obj[sfAmount]; + +// Optional field — returns std::optional +auto dest = obj[~sfDestination]; +``` + +The mutable overloads return proxy objects instead of raw values. `ValueProxy` wraps a non-optional field: it is implicitly convertible to `T::value_type` for reading and supports `operator=`, `operator+=`, and `operator-=` for writing (with arithmetic constraints enforced via C++20 concepts `IsArithmetic`, `IsArithmeticCompatible`). `OptionalProxy` additionally supports assignment from `std::nullopt_t` to remove the field, and exposes `operator bool()` / `operator~()` for presence testing. + +Both proxy types share a common base class `Proxy` that stores a pointer to the owning `STObject`, the `SOEStyle` of the field, and a typed pointer to the field descriptor. The `assign()` method on `Proxy` handles the subtlety of `soeDEFAULT` fields: if a value is being set to the type's zero value, the field is made absent (via `makeFieldAbsent`) rather than storing an explicit default — preserving canonical wire format. + +The design decision to return proxy objects rather than references is intentional: returning `T&` would expose internal storage whose address could be invalidated by concurrent modification elsewhere in the object (e.g., via `set()` or `emplace_back()`). Proxies also let the framework intercept writes to apply schema enforcement. + +## Serialization and Hashing + +The private `add(Serializer& s, WhichFields whichFields)` method controls whether signing-excluded fields are emitted. The `WhichFields` enum (values `omitSigningFields = false` and `withAllFields = true`) is carefully chosen to alias directly to the `bool` argument of `SField::shouldInclude()`, avoiding a translation step. `getSortedFields()` collects field pointers and sorts them by `SField::fieldCode` — this canonical ordering is critical for deterministic serialization and hash computation. + +`getHash()` and `getSigningHash()` produce `uint256` hashes prefixed by a `HashPrefix` discriminator that scopes each hash domain. `addWithoutSigningFields()` serializes only the signing-eligible subset, which underpins multi-sig and single-sig transaction validation. + +## Field Lifecycle and Presence Management + +`getPField(field, createOkay)` is the core lookup primitive: it searches `v_` for a field matching the given `SField`. When `createOkay` is true and the field is absent, it is created (in template mode, as an `STI_NOTPRESENT` placeholder; in free mode, via `emplace_back`). `makeFieldPresent()` promotes a placeholder to a live value; `makeFieldAbsent()` demotes a live value back to a placeholder (for template-mode optionals) or removes it entirely (for free mode). `delField()` removes an entry unconditionally. + +`emplace_back()` is a pass-through to the underlying `v_` vector, used during deserialization and by the proxy system when creating new fields in free objects. + +## Iteration + +The public `iterator` type is a `boost::transform_iterator` wrapping the inner `list_type::const_iterator`. The `Transform` functor maps each `STVar` to its contained `STBase const&` via `STVar::get()`. This hides the `STVar` indirection layer from callers iterating over the object's fields, presenting a clean sequence of `STBase` references. + +## Error Handling + +`FieldErr` (a `std::runtime_error` subclass declared inside `STObject`) is thrown by the proxy and `at()` accessors when a required field is absent or schema constraints are violated. The standalone `throwFieldNotFound()` helper is used by the legacy `getFieldByValue` / `getFieldByConstRef` / `setFieldUsingSetValue` private implementation templates, which all follow the same pattern: locate the field via `peekAtPField`, check for `STI_NOTPRESENT`, `dynamic_cast` to the concrete type, and throw on mismatch. + +The `disengage()` method on `OptionalProxy` explicitly prevents assignment of `std::nullopt` to `soeREQUIRED` or `soeDEFAULT` fields — required fields cannot be removed, and default-value fields are semantically always "present." + +## Relationship to Sibling Types + +`STArray` (forward-declared in this header) is a sibling ST container holding an ordered sequence of `STObject` instances. `STObject` provides `peekFieldArray()` / `peekFieldObject()` to obtain direct mutable references into nested containers without copying. The `makeInnerObject()` static factory creates a free-mode `STObject` intended as a nested structure inside an `STArray`, seeded with the `soeDEFAULT` fields from the enclosing schema. The `getFieldObject()` method, by contrast, returns an object by value — a copy that can be inspected but whose modification does not propagate back. \ No newline at end of file diff --git a/include/xrpl/protocol/STParsedJSON.h.ai.json b/include/xrpl/protocol/STParsedJSON.h.ai.json new file mode 100644 index 0000000000..9948a21366 --- /dev/null +++ b/include/xrpl/protocol/STParsedJSON.h.ai.json @@ -0,0 +1,31 @@ +{ + "args": [ + { + "lineno": 17, + "name": "name" + }, + { + "lineno": 17, + "name": "json" + } + ], + "classes": [ + { + "args": [ + "std::string const& name, Json::Value const& json" + ], + "lineno": 10, + "name": "STParsedJSONObject" + } + ], + "description": "Defines the STParsedJSONObject class, which parses and validates a JSON object into an optional STObject, storing errors if parsing fails.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STParsedJSON.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STParsedJSON.h.ai.md b/include/xrpl/protocol/STParsedJSON.h.ai.md new file mode 100644 index 0000000000..540af92b1b --- /dev/null +++ b/include/xrpl/protocol/STParsedJSON.h.ai.md @@ -0,0 +1,44 @@ +# `STParsedJSON.h` — JSON-to-STObject Parsing Interface + +## Purpose + +This header declares `STParsedJSONObject`, the single public entry point for converting a JSON representation of an XRPL protocol object into the ledger's internal serialized type (`STObject`). It sits at the boundary between the JSON-based RPC layer and the binary-canonical serialized type (ST) system, performing schema validation and type coercion in a single constructor call. + +The class is most prominently used in `TransactionSign.cpp`, where user-submitted `tx_json` from an RPC request must be parsed, validated, and converted into a properly typed `STTx` before signing or simulation can proceed. + +## Design Philosophy: No-Throw, Public-Member Result + +Rather than returning a value or throwing on failure, `STParsedJSONObject` communicates outcomes through two public data members: `std::optional object` and `Json::Value error`. The constructor is documented to not throw. All exceptions generated by the deeply recursive parsing logic are caught internally and translated into structured `RPC::make_error(rpcINVALID_PARAMS, ...)` JSON error values. This is a deliberate design choice for RPC-layer code: callers check `parsed.object.has_value()` and, on failure, can forward `parsed.error` directly back to the RPC client with no additional formatting work. + +The class is non-copyable and not default-constructible — a parse is a one-shot construction. This enforces that every `STParsedJSONObject` instance represents exactly one completed parse attempt with a definite success or failure state. + +## Implementation Structure + +The real complexity lives in `STParsedJSON.cpp`, entirely within the internal `STParsedJSONDetail` namespace. The public constructor simply calls `parseObject(name, json, sfGeneric, 0, error)` and assigns the result to `object`. + +`parseObject()` and `parseArray()` are mutually recursive. `parseObject()` iterates over every JSON member, looks each field name up via `SField::getField()` to validate it exists in the XRPL protocol schema (rejecting unknown fields with `rpcINVALID_PARAMS`), then dispatches on the field's `fieldType`: + +- **`STI_OBJECT`, `STI_TRANSACTION`, `STI_LEDGERENTRY`, `STI_VALIDATION`** — recurse into `parseObject()` at `depth + 1`, building a nested `STObject`. +- **`STI_ARRAY`** — recurse into `parseArray()`, which expects each array element to be a single-key JSON object (the XRPL convention for typed array entries), parses the inner object, and appends it to an `STArray`. +- **Everything else** — delegated to `parseLeaf()`, which handles all scalar and composite leaf types without further recursion. + +A depth guard (`maxDepth = 64`) prevents stack exhaustion from pathologically nested input, returning a `too_deep` error. + +## Leaf Type Handling + +`parseLeaf()` is a large `switch` on `field.fieldType` covering the full range of XRPL serialized types. Several non-obvious behaviors are worth noting: + +- **`STI_UINT16`** (`parseUint16`): When parsing `sfTransactionType` or `sfLedgerEntryType`, string values like `"Payment"` are resolved via `TxFormats::getInstance().findTypeByName()` or `LedgerFormats::getInstance().findTypeByName()` respectively. The `sfGeneric` template sentinel is also replaced with `sfTransaction` or `sfLedgerEntry` at this point, so that `applyTemplateFromSField()` later applies the correct field template for the transaction or ledger entry type. +- **`STI_UINT32`** (`parseUint32`): `sfPermissionValue` fields accept human-readable granular permission names or transaction type names, resolved through `Permission::getInstance()`. +- **`STI_UINT8`**: `sfTransactionResult` fields accept TER result code strings (e.g. `"tesSUCCESS"`), resolved through `transCode()`. +- **`STI_UINT64`**: Hexadecimal string input by default, but fields with the `sMD_BaseTen` metadata flag parse as base-10 decimal (used for amount fields that happen to carry 64-bit integers). +- **`STI_ACCOUNT`**: Accepts either 40-character hex (raw `AccountID`) or Base58Check-encoded r-addresses, trying hex first. +- **`STI_PATHSET`**: Handles the nested path structure with special support for both IOU paths (`currency`/`issuer`) and MPT (Multi-Purpose Token) paths (`mpt_issuance_id`), validating that the MPT issuer in a path element is consistent with the issuance ID. + +## Template Application + +After building an `STObject` from all JSON members, `parseObject()` calls `data.applyTemplateFromSField(inName)`. This enforces field templates — inner object types in the XRPL protocol have required and optional field sets, and this call validates the parsed object matches the expected template for its type. A mismatch throws `STObject::FieldErr`, caught and translated into a `template_mismatch` error. + +## Error Reporting + +The internal `STParsedJSONDetail` namespace defines a set of small helper functions (`not_an_object`, `unknown_field`, `bad_type`, `out_of_range`, `invalid_data`, `too_deep`, etc.) that construct `rpcINVALID_PARAMS` error `Json::Value` objects with dot-separated field path names (e.g. `"tx_json.Signers[0].Signer.Account"`). This precise path information in error messages is generated by threading the `json_name` string through recursive calls, accumulating field names at each level — making validation failures actionable for RPC clients. \ No newline at end of file diff --git a/include/xrpl/protocol/STPathSet.h.ai.json b/include/xrpl/protocol/STPathSet.h.ai.json new file mode 100644 index 0000000000..8c7178cde7 --- /dev/null +++ b/include/xrpl/protocol/STPathSet.h.ai.json @@ -0,0 +1,395 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "STPathElement" + }, + { + "args": [], + "lineno": 82, + "name": "STPath" + }, + { + "args": [], + "lineno": 121, + "name": "STPathSet" + } + ], + "description": "Defines classes and logic for representing and manipulating payment paths, path elements, and sets of paths in the XRPL protocol, including serialization, type handling, and JSON conversion.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STPathSet.h", + "functions": [ + { + "args": [], + "lineno": 19, + "name": "STPathElement" + }, + { + "args": [ + "STPathElement const&" + ], + "lineno": 20, + "name": "STPathElement" + }, + { + "args": [ + "STPathElement const&" + ], + "lineno": 21, + "name": "operator=" + }, + { + "args": [ + "std::optional const&", + "std::optional const&", + "std::optional const&" + ], + "lineno": 23, + "name": "STPathElement" + }, + { + "args": [ + "AccountID const&", + "PathAsset const&", + "AccountID const&", + "bool" + ], + "lineno": 28, + "name": "STPathElement" + }, + { + "args": [ + "unsigned int", + "AccountID const&", + "PathAsset const&", + "AccountID const&" + ], + "lineno": 34, + "name": "STPathElement" + }, + { + "args": [], + "lineno": 39, + "name": "getNodeType" + }, + { + "args": [], + "lineno": 41, + "name": "isOffer" + }, + { + "args": [], + "lineno": 43, + "name": "isAccount" + }, + { + "args": [], + "lineno": 45, + "name": "hasIssuer" + }, + { + "args": [], + "lineno": 47, + "name": "hasCurrency" + }, + { + "args": [], + "lineno": 49, + "name": "hasMPT" + }, + { + "args": [], + "lineno": 51, + "name": "hasAsset" + }, + { + "args": [], + "lineno": 53, + "name": "isNone" + }, + { + "args": [], + "lineno": 57, + "name": "getAccountID" + }, + { + "args": [], + "lineno": 60, + "name": "getPathAsset" + }, + { + "args": [], + "lineno": 63, + "name": "getCurrency" + }, + { + "args": [], + "lineno": 66, + "name": "getMPTID" + }, + { + "args": [], + "lineno": 69, + "name": "getIssuerID" + }, + { + "args": [ + "Type const&" + ], + "lineno": 72, + "name": "isType" + }, + { + "args": [ + "STPathElement const&" + ], + "lineno": 74, + "name": "operator==" + }, + { + "args": [ + "STPathElement const&" + ], + "lineno": 76, + "name": "operator!=" + }, + { + "args": [ + "STPathElement const&" + ], + "lineno": 80, + "name": "get_hash" + }, + { + "args": [], + "lineno": 84, + "name": "STPath" + }, + { + "args": [ + "std::vector" + ], + "lineno": 86, + "name": "STPath" + }, + { + "args": [], + "lineno": 88, + "name": "size" + }, + { + "args": [], + "lineno": 90, + "name": "empty" + }, + { + "args": [ + "STPathElement const&" + ], + "lineno": 92, + "name": "push_back" + }, + { + "args": [ + "Args&&..." + ], + "lineno": 95, + "name": "emplace_back" + }, + { + "args": [ + "AccountID const&", + "PathAsset const&", + "AccountID const&" + ], + "lineno": 98, + "name": "hasSeen" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 100, + "name": "getJson" + }, + { + "args": [], + "lineno": 102, + "name": "begin" + }, + { + "args": [], + "lineno": 104, + "name": "end" + }, + { + "args": [ + "STPath const&" + ], + "lineno": 106, + "name": "operator==" + }, + { + "args": [], + "lineno": 108, + "name": "back" + }, + { + "args": [], + "lineno": 110, + "name": "front" + }, + { + "args": [ + "int" + ], + "lineno": 112, + "name": "operator[]" + }, + { + "args": [ + "int" + ], + "lineno": 115, + "name": "operator[]" + }, + { + "args": [ + "size_t" + ], + "lineno": 118, + "name": "reserve" + }, + { + "args": [], + "lineno": 124, + "name": "STPathSet" + }, + { + "args": [ + "SField const&" + ], + "lineno": 126, + "name": "STPathSet" + }, + { + "args": [ + "SerialIter&", + "SField const&" + ], + "lineno": 127, + "name": "STPathSet" + }, + { + "args": [ + "Serializer&" + ], + "lineno": 129, + "name": "add" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 131, + "name": "getJson" + }, + { + "args": [], + "lineno": 133, + "name": "getSType" + }, + { + "args": [ + "STPath const&", + "STPathElement const&" + ], + "lineno": 135, + "name": "assembleAdd" + }, + { + "args": [ + "STBase const&" + ], + "lineno": 137, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 139, + "name": "isDefault" + }, + { + "args": [ + "std::vector::size_type" + ], + "lineno": 142, + "name": "operator[]" + }, + { + "args": [ + "std::vector::size_type" + ], + "lineno": 145, + "name": "operator[]" + }, + { + "args": [], + "lineno": 148, + "name": "begin" + }, + { + "args": [], + "lineno": 150, + "name": "end" + }, + { + "args": [], + "lineno": 152, + "name": "size" + }, + { + "args": [], + "lineno": 154, + "name": "empty" + }, + { + "args": [ + "STPath const&" + ], + "lineno": 156, + "name": "push_back" + }, + { + "args": [ + "Args&&..." + ], + "lineno": 159, + "name": "emplace_back" + }, + { + "args": [ + "std::size_t", + "void*" + ], + "lineno": 163, + "name": "copy" + }, + { + "args": [ + "std::size_t", + "void*" + ], + "lineno": 164, + "name": "move" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STPathSet.h.ai.md b/include/xrpl/protocol/STPathSet.h.ai.md new file mode 100644 index 0000000000..aec7c810a7 --- /dev/null +++ b/include/xrpl/protocol/STPathSet.h.ai.md @@ -0,0 +1,52 @@ +# `include/xrpl/protocol/STPathSet.h` — Payment Path Representation + +This header defines the three-class hierarchy used to represent payment paths in XRPL transactions: `STPathElement` (a single hop), `STPath` (an ordered sequence of hops), and `STPathSet` (the collection of alternate paths that the `Paths` field of a `Payment` transaction carries on the wire). Together they encode how a cross-currency payment can route through the order book and the trust-line graph from source to destination. + +## The `STPathElement` Node + +Each step in a payment path is an instance of `STPathElement`. A node is either an *account node* (rippling through an account's trust lines) or an *offer node* (matching against a DEX order book), and this distinction is captured by the `is_offer_` flag: an element whose `mAccountID` is the XRP "no account" sentinel is treated as an offer node; anything else is an account node. `isOffer()` and `isAccount()` expose this flag and are inverses. + +The `Type` bitmask drives both on-wire encoding and runtime dispatch: + +``` +typeAccount = 0x01 account field is present +typeCurrency = 0x10 a legacy IOU Currency follows +typeIssuer = 0x20 an issuer AccountID follows +typeMPT = 0x40 a Multi-Purpose Token MPTID follows +typeBoundary = 0xFF separator between alternate paths in a PathSet +typeNone = 0x00 path terminator +typeAsset = typeCurrency | typeMPT either kind of asset +``` + +`typeCurrency` and `typeMPT` are mutually exclusive — a single hop carries either a legacy IOU currency or an MPT, never both. The aggregate `typeAsset` constant lets callers check "does this node carry any asset kind?" without caring which. This design is the result of the MPT (Multi-Purpose Token) feature extension: rather than a separate node type, MPTs are grafted into the same bitmask namespace as currencies, preserving backward compatibility with the on-wire format. + +The asset field is stored as `PathAsset`, a `std::variant` wrapper defined in `PathAsset.h`. Its `visit()` method avoids `std::get` boilerplate; the constructors in `STPathElement` use it to set or clear the right type bit. The explicit-type constructor (taking `unsigned int uType`) is the one the deserializer calls, and it defensively rewrites the asset bits by visiting the actual `PathAsset` variant, preventing a caller from passing a contradictory type mask. + +### Hashing for Equality + +`STPathElement` pre-computes and caches `hash_value_` at construction. Equality comparison short-circuits on both the `typeAccount` bit and the hash before doing the full field-by-field comparison — critical because path deduplication iterates over every existing path. The hash function itself (implemented in `STPathSet.cpp`) uses FNV-style multiply-XOR over the raw bytes of each field with intentionally different multipliers (257, 509, 911). A code comment there explicitly notes this does not need to be cryptographically secure — speed matters and a few bytes of each 160-bit ID would suffice. The hash handles the MPT/currency union correctly by calling `PathAsset::visit()` rather than trusting the type bits, because in some pathfinder intermediate states the bits may not have been fully set yet. + +## The `STPath` Sequence + +`STPath` wraps a `std::vector` and exposes a standard container interface (`push_back`, `emplace_back`, `begin`, `end`, `front`, `back`, `operator[]`, `reserve`, `size`, `empty`). Most of this is thin delegation to the underlying vector. + +The one non-obvious operation is `hasSeen(account, asset, issuer)`, which the pathfinding engine uses to detect cycles. Before adding an intermediate node to a path under construction, `Pathfinder::addLink()` calls this to check whether that (account, asset, issuer) triple has already appeared earlier in the same path. If it has, the candidate extension is discarded. The scan is linear, but the XRPL protocol caps path length so the vector is short in practice (typically 2–6 elements). + +## `STPathSet` — The Serialized Type + +`STPathSet` is the only class here that participates in the ledger type system. It inherits from `STBase` (the protocol's generic serialized type base) and overrides: + +- `getSType()` — returns `STI_PATHSET`, identifying it to the serialization framework +- `add(Serializer&)` — writes the binary representation +- `isEquivalent(STBase const&)` — structural equality for ledger-object comparison +- `isDefault()` — returns `true` when the path set is empty (no `Paths` field needed) + +The binary format used by `add()` and the deserializing constructor `STPathSet(SerialIter&, SField const&)` encodes each element as its type byte followed by the present optional fields in order: account (160 bits), asset (160 bits for currency, 192 bits for MPT), issuer (160 bits). Paths within the set are separated by a `typeBoundary` (0xFF) byte; the entire PathSet terminates with a `typeNone` (0x00) byte. Deserialization validates that no unknown type bits are set (via `iType & ~typeAll`) and that no path in the stream is empty; violations throw `std::runtime_error`. + +### `assembleAdd()` Deduplication + +`assembleAdd(base, tail)` is the pathfinder's workhorse for incrementally extending paths. It appends the candidate `base + tail` to the set, then reverse-iterates over the existing paths to check for a duplicate. If one is found, the newly added path is popped back off and `false` is returned. This avoids a temporary `STPath` allocation by pushing first and comparing against the live entry, at the cost of a vector mutation on failure. The choice of reverse iteration is a minor optimization: the most recently added paths are most likely to collide with a new candidate, since `addLink()` extends from similar bases repeatedly. + +The `copy()` and `move()` private overrides, together with `friend class detail::STVar`, plug `STPathSet` into the `STVar` placement-allocation scheme that allows serialized type values to be stored inline in small buffers without heap allocation. + +Both `STPathElement` and `STPath` and `STPathSet` inherit from `CountedObject`, a lightweight CRTP mixin that tracks live instance counts for memory diagnostics — useful for detecting leaks in pathfinding code that creates and discards many transient path candidates. \ No newline at end of file diff --git a/include/xrpl/protocol/STTakesAsset.h.ai.json b/include/xrpl/protocol/STTakesAsset.h.ai.json new file mode 100644 index 0000000000..0ff811ac84 --- /dev/null +++ b/include/xrpl/protocol/STTakesAsset.h.ai.json @@ -0,0 +1,49 @@ +{ + "args": [ + { + "lineno": 32, + "name": "a" + }, + { + "lineno": 54, + "name": "sle" + }, + { + "lineno": 54, + "name": "asset" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "STTakesAsset" + } + ], + "description": "Defines an intermediate class STTakesAsset for STBase-derived classes to store an Asset at runtime (not serialized), and provides a function to associate an Asset with relevant fields in a ledger entry.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STTakesAsset.h", + "functions": [ + { + "args": [ + "a" + ], + "lineno": 32, + "name": "STTakesAsset::associateAsset" + }, + { + "args": [ + "sle", + "asset" + ], + "lineno": 54, + "name": "associateAsset" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STTakesAsset.h.ai.md b/include/xrpl/protocol/STTakesAsset.h.ai.md new file mode 100644 index 0000000000..52114b9fc6 --- /dev/null +++ b/include/xrpl/protocol/STTakesAsset.h.ai.md @@ -0,0 +1,40 @@ +# `STTakesAsset.h` — Runtime Asset Association Mixin for Serializable Types + +## Purpose and Context + +`STTakesAsset` exists to solve a specific tension in the XRPL serialization model: some ledger fields store numeric quantities whose precision depends on which token type (XRP, IOU, or MPT) they represent, but that token identity is already known from the enclosing ledger object and should not be duplicated in each field. This is the motivation for `STNumber`, which stores a bare `Number` without an `Asset`, but still needs the `Asset` at transaction processing time to round correctly. + +`STTakesAsset` is the bridge: an intermediate base class (used _instead of_ `STBase`) that holds a `std::optional` purely at runtime. It is never serialized. Derived classes call into it as needed, and the ecosystem that drives this is built around a coordinated metadata flag on `SField`. + +## The Class + +`STTakesAsset : public STBase` is a minimal mixin. Its only state is `std::optional asset_`, declared `protected` so derived classes can access it directly during serialization or value operations. Its only behavior is a virtual `associateAsset(Asset const& a)` that stores the asset via `asset_.emplace(a)`. + +The base implementation is deliberately inert — it just records the asset. Derived classes override `associateAsset` to act on the asset as appropriate for their type. The pattern is analogous to a template method hook: `STTakesAsset` defines the interface and storage; derived classes define the consequence. + +## The Only Concrete User: `STNumber` + +`STNumber` is currently the only class that inherits from `STTakesAsset`. It overrides `associateAsset` to first call the base (`STTakesAsset::associateAsset`) to store the asset, then immediately calls `roundToAsset(a, value_)` on its internal `Number`. This rounding step is the entire reason `STTakesAsset` exists: XRP amounts must be integers, MPT amounts are also integral, while IOU amounts have their own fixed precision. Without knowing the asset, an `STNumber` cannot correctly normalize its value. + +This rounding-on-association approach also propagates into `STNumber::add()` (serialization), which re-applies `roundToAsset` if `asset_` is populated, with a debug assertion that the value should already have been rounded by the time serialization occurs. If no asset is associated at serialization time, the code path proceeds but asserts that the mantissa scale is `large` — an indication that the number originated in a safe context (e.g., deserialized from an already-valid ledger entry). + +## The Free Function: `associateAsset(STLedgerEntry&, Asset const&)` + +The companion free function drives the entire mechanism at the ledger-entry level. It iterates over all fields in an `SLE` by offset (the only way to obtain non-const references), and for each field that carries the `SField::sMD_NeedsAsset` metadata flag (bit `0x80`), it: + +1. Skips fields that are not present (`STI_NOTPRESENT`). +2. Downcasts the `STBase&` to `STTakesAsset&` — a hard throw if the downcast fails, enforcing the invariant that `sMD_NeedsAsset` must only appear on fields whose types derive from `STTakesAsset`. +3. Calls `ta.associateAsset(asset)`, triggering the derived class's rounding logic. +4. Checks whether the field's style is `soeDEFAULT` and whether the value is now the default (e.g., rounded to zero). If so, it removes the field from the SLE via `makeFieldAbsent`. This cleanup is subtle but important: rounding can reduce a non-zero value to zero, and a zero default-style field should not persist in the ledger entry. + +The `sMD_NeedsAsset` flag is defined in `SField.h` with the comment "intended for `STNumber`." This flag-based dispatch means new field types can participate in the mechanism without modifying the free function. + +## Calling Convention + +The doc comment on the free function specifies that `associateAsset` should be called "near the end of `doApply()`" in transactor classes, after all modifications to the SLE have been made. Transactors such as `VaultDeposit`, `VaultCreate`, `LoanSet`, and `LoanPay` follow this pattern, calling `associateAsset(*vaultSle, asset)` as a final post-processing step. The reasoning is correct: if rounding happens before computations are complete, intermediate values may be distorted; rounding as a finalizer ensures the stored value is ready for consistent serialization. + +## Design Tradeoffs + +The optional nature of `asset_` — rather than requiring it to always be set — is a deliberate concession to the deserialization path. When a ledger entry is read back from disk, the `STNumber` fields are deserialized before any transactor context exists, so no asset is available. The field still round-trips correctly because the value was already rounded when originally written. The `std::optional` makes this valid-but-unset state explicit. + +The virtual `associateAsset` on a potentially stack-allocated serialized type is a performance consideration, but the call happens once per field per transaction, not in hot inner loops, so the virtual dispatch overhead is inconsequential relative to actual ledger I/O. \ No newline at end of file diff --git a/include/xrpl/protocol/STTx.h.ai.json b/include/xrpl/protocol/STTx.h.ai.json new file mode 100644 index 0000000000..7a3f529948 --- /dev/null +++ b/include/xrpl/protocol/STTx.h.ai.json @@ -0,0 +1,58 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "STTx" + } + ], + "description": "Defines the STTx class representing a transaction in the XRPL protocol, including methods for construction, serialization, signing, signature checking, and metadata handling.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STTx.h", + "functions": [ + { + "args": [ + "STObject const& st", + "std::string&" + ], + "lineno": 110, + "name": "passesLocalChecks" + }, + { + "args": [ + "STTx const& stx" + ], + "lineno": 119, + "name": "sterilize" + }, + { + "args": [ + "STObject const& tx" + ], + "lineno": 126, + "name": "isPseudoTx" + }, + { + "args": [], + "lineno": 131, + "name": "STTx::getTxnType" + }, + { + "args": [], + "lineno": 136, + "name": "STTx::getSigningPubKey" + }, + { + "args": [], + "lineno": 141, + "name": "STTx::getTransactionID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STTx.h.ai.md b/include/xrpl/protocol/STTx.h.ai.md new file mode 100644 index 0000000000..dee2b7e8dd --- /dev/null +++ b/include/xrpl/protocol/STTx.h.ai.md @@ -0,0 +1,61 @@ +# `include/xrpl/protocol/STTx.h` — Serialized Transaction + +`STTx` is the central transaction object in the XRPL protocol layer. It inherits from `STObject` (the generic serialized-field container) and adds transaction-specific identity, typing, signing, and persistence semantics. Every transaction that enters the ledger — whether submitted by a client, relayed between peers, or reconstructed from disk — is eventually represented as an `STTx`. + +## Class Structure and Inheritance + +`STTx` extends `STObject` (itself a polymorphic field container built on a vector of `STVar` tagged fields) and mixes in `CountedObject` for diagnostic instance tracking. The `final` qualifier prevents further subclassing, reflecting the design intent that `STTx` is the leaf representation of a transaction, not an extensible base for transaction variants — transaction-type-specific behavior lives in the transactor subsystem, not here. + +Two pieces of identity are cached on construction and kept current across mutations: `tid_` (a `uint256` SHA-512 half-hash of the serialized form prefixed with `HashPrefix::transactionID`) and `tx_type_` (a `TxType` enum value extracted from the `sfTransactionType` field). Caching both avoids repeated hash computation and repeated field lookups on hot paths. + +## Construction Paths + +There are three meaningful construction paths, each serving a distinct use case. + +`STTx(SerialIter&)` deserializes a transaction from a byte stream. It validates the wire length against the `txMinSizeBytes`/`txMaxSizeBytes` protocol constants before parsing, then applies the `SOTemplate` for the decoded `TxType`. The rvalue-reference overload (`SerialIter&&`) simply delegates to the lvalue version — the `// NOLINT` comment acknowledges that the rvalue is not moved from, since iterators are consumed by value semantics internally. + +`STTx(STObject&&)` promotes a generic `STObject` to a typed transaction. This path is used when a transaction arrives as a raw parsed object (e.g., from JSON deserialization) and must be "graduated" to a fully validated `STTx`. The `applyTemplate()` call may throw if the field layout doesn't match the required schema for the transaction type. + +`STTx(TxType, std::function)` is the programmatic construction path used by tests and internal transaction builders. The assembler callback receives the pre-templated object and fills in fields. A `LogicError` fires if the callback mutates `sfTransactionType` — a deliberate trap preventing type confusion bugs. + +Copy construction is allowed, but copy assignment is explicitly deleted. This asymmetry is intentional: assignment on a typed transaction would need to re-validate and re-hash, creating opportunities for invariant violations. Copies are safe because all invariants are established at construction time. + +## Signing and Signature Verification + +The signing model in XRPL supports two modes: **single-sign** (one key pair) and **multi-sign** (1–32 signers from an authorized signer list). `STTx` encodes which mode is in use using the `sfSigningPubKey` field: a non-empty value signals single-sign, an empty value signals multi-sign with signers in the `sfSigners` array. + +`checkSign(Rules const&)` dispatches to the appropriate private path based on `sfSigningPubKey`. The public overload also checks for `sfCounterpartySignature`, a second embedded signature object for two-party protocols — its verification failure message is prefixed with `"Counterparty: "` to distinguish it from the primary signature. + +For multi-sign, `checkMultiSign()` and the shared `multiSignHelper()` function enforce several invariants: signers must be sorted in ascending `AccountID` order (enabling binary-search rejection of duplicates), no duplicates are allowed, the transaction's own account cannot appear in its own multi-signer list, and the signer count must be in `[minMultiSigners=1, maxMultiSigners=32]`. Each signer's signature covers the serialized transaction body combined with the signer's account ID (via `finishMultiSigningData()`), which prevents signature reuse across accounts. + +All signature-checking methods return `Expected` — a `[[nodiscard]]` type backed by `boost::outcome_v2::result` that carries either success or a human-readable error string. This is a pre-C++23 approximation of `std::expected`, avoiding exceptions on the error path while still forcing callers to handle failure explicitly. + +## Batch Transaction Support + +`STTx` handles the `ttBATCH` transaction type, which wraps multiple inner transactions in a single outer envelope. `getBatchTransactionIDs()` lazily computes and caches in `mutable batchTxnIds_` the hash of each raw inner transaction from `sfRawTransactions`. The result is cached because hashing inner transactions is non-trivial and the list is immutable after construction. + +Batch signing uses a different data commitment than standard signing: `checkBatchSign()` iterates the `sfBatchSigners` array and for each signer verifies a signature over `serializeBatch()` output — the outer transaction's flags and the ordered list of inner transaction IDs (prefixed with `HashPrefix::batch`). This binds a batch signer to the exact set and order of inner transactions, not to the full outer transaction body. Nested `ttBATCH` transactions are explicitly rejected in `passesLocalChecks()`. + +## Sequence and Ticket Handling + +`getSeqProxy()` returns a `SeqProxy` that abstracts over both the classic `sfSequence` field and the newer `sfTicketSequence` mechanism. If `sfSequence` is non-zero it takes precedence; otherwise the optional `sfTicketSequence` field determines the proxy type. The `SeqProxy` comparison operators guarantee that sequence-based proxies sort before ticket-based ones, ensuring that ticket-creating transactions precede ticket-consuming transactions in processing order — an ordering property the protocol relies on. + +## Fee Payer and Delegate Accounts + +`getFeePayer()` returns the `sfDelegate` account if present, otherwise falling back to `sfAccount`. This supports a delegation model where one account authorizes another to act and pay fees on its behalf; the cryptographic validity and authorization of the delegate relationship are enforced separately in the transactor layer. + +## Local Checks and Sterilization + +`passesLocalChecks()` is a free function that validates structural correctness before a transaction is admitted to a node's local queue. It checks: memo field size and character legality (MemoType/MemoFormat must contain only RFC 3986 URL-safe characters, decoded from hex); account fields must not be the zero/default account; pseudo-transaction types (`ttAMENDMENT`, `ttFEE`, `ttUNL_MODIFY`) are rejected from client submission; MPT (Multi-Purpose Token) amounts must only appear in fields that explicitly support them; and batch inner transactions must not themselves be batch transactions. + +`sterilize()` performs a serialize-then-deserialize round trip, returning a `shared_ptr`. This canonicalization ensures that any programmatically assembled transaction ends up in the identical binary form it would have after a network round trip — normalizing field ordering, triggering template validation, and recomputing the cached `tid_`. Any code that synthesizes transactions and then submits them to the consensus pipeline should sterilize first. + +## SQL Persistence + +`TxnSql` is a `char`-backed enum used to tag a transaction's status in the local SQLite `Transactions` table: `N`=new, `C`=conflict, `H`=held, `V`=validated, `I`=included, `U`=unknown. `getMetaSQL()` generates the `INSERT OR REPLACE` value tuple used for persistence, embedding the raw serialized transaction as a SQL blob literal alongside ledger sequence, status, and escaped metadata from the transaction's `STObject` representation. + +## Design Notes + +The placement-new `copy()` and `move()` overrides exist to support `STVar`, the tagged discriminated union that `STObject` uses as its element type. These allow `STTx` instances to be cloned into pre-allocated buffers without going through the heap, which is important for the hot copy paths in ledger building. + +The assignment operator deletion, the `LogicError` in the assembler constructor, and the assertion in `getBatchTransactionIDs()` that the cached list always matches the raw array size are all examples of the defensive programming posture throughout: invariants are asserted immediately at the site where they could break, rather than letting inconsistent state propagate silently. \ No newline at end of file diff --git a/include/xrpl/protocol/STValidation.h.ai.json b/include/xrpl/protocol/STValidation.h.ai.json new file mode 100644 index 0000000000..cc900973b4 --- /dev/null +++ b/include/xrpl/protocol/STValidation.h.ai.json @@ -0,0 +1,102 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "SerialIter& sit, LookupNodeID&& lookupNodeID, bool checkSignature", + "NetClock::time_point signTime, PublicKey const& pk, SecretKey const& sk, NodeID const& nodeID, F&& f" + ], + "lineno": 19, + "name": "STValidation" + } + ], + "description": "Defines the STValidation class for representing and handling ledger validations in the XRPL protocol, including construction, signing, verification, and metadata access.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STValidation.h", + "functions": [ + { + "args": [], + "lineno": 74, + "name": "STValidation::render" + }, + { + "args": [], + "lineno": 92, + "name": "STValidation::validationFormat" + }, + { + "args": [ + "n", + "buf" + ], + "lineno": 94, + "name": "STValidation::copy" + }, + { + "args": [ + "n", + "buf" + ], + "lineno": 96, + "name": "STValidation::move" + }, + { + "args": [ + "SerialIter& sit", + "LookupNodeID&& lookupNodeID", + "bool checkSignature" + ], + "lineno": 101, + "name": "STValidation::STValidation" + }, + { + "args": [ + "NetClock::time_point signTime", + "PublicKey const& pk", + "SecretKey const& sk", + "NodeID const& nodeID", + "F&& f" + ], + "lineno": 126, + "name": "STValidation::STValidation" + }, + { + "args": [], + "lineno": 154, + "name": "STValidation::getSignerPublic" + }, + { + "args": [], + "lineno": 159, + "name": "STValidation::getNodeID" + }, + { + "args": [], + "lineno": 164, + "name": "STValidation::isTrusted" + }, + { + "args": [], + "lineno": 169, + "name": "STValidation::setTrusted" + }, + { + "args": [], + "lineno": 174, + "name": "STValidation::setUntrusted" + }, + { + "args": [ + "NetClock::time_point s" + ], + "lineno": 179, + "name": "STValidation::setSeen" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STValidation.h.ai.md b/include/xrpl/protocol/STValidation.h.ai.md new file mode 100644 index 0000000000..a73f301947 --- /dev/null +++ b/include/xrpl/protocol/STValidation.h.ai.md @@ -0,0 +1,35 @@ +# `STValidation.h` — Ledger Validation Protocol Object + +`STValidation` is the wire-format representation of a single ledger validation message in the XRPL consensus protocol. When a validator agrees with a particular closed ledger, it constructs one of these objects, signs it, and broadcasts it to its peers. Every other node that receives the message deserializes it into another `STValidation` instance, verifies its signature, and decides whether to count it toward quorum. The class therefore has two very different construction lifetimes — one for creation-and-signing by a local validator, one for deserialization-and-verification of a peer's message — and its API is shaped by that duality. + +Inheriting from `STObject` gives `STValidation` access to XRPL's typed-field serialization system: the same infrastructure used by transactions and ledger entries. The `CountedObject` mixin hooks into the instrumentation layer for live-instance tracking, which aids leak detection in a long-running rippled process. + +## Two Construction Paths + +**Deserialization from a peer** uses the template constructor taking `SerialIter&`. It delegates field parsing to `STObject`, then bootstraps `signingPubKey_` from the serialized `sfSigningPubKey` field via a lambda in the member-initializer list — a technique that allows `const` members to be computed from partially-constructed object state. The `lookupNodeID` callable is the key design point here: the constructor accepts a generic invocable `NodeID(PublicKey const&)` rather than a direct dependency on manifest resolution code. This decouples `STValidation` from the XRPL manifest system; the caller supplies the translation from an ephemeral signing key to the stable master `NodeID`. Because a validator may rotate its ephemeral keys without changing its identity (via the manifest mechanism), `signingPubKey_` and `nodeID_` can differ — `signingPubKey_` is what actually signed the bytes, while `nodeID_` is the stable validator identity used for UNL membership checks. If `checkSignature` is true and the signature is invalid, the constructor throws immediately, making it impossible to hold an object in an inconsistent state. + +**Self-construction and signing** uses the template constructor taking `PublicKey`, `SecretKey`, `NodeID`, and a filler callback `F&&`. Rather than requiring a fully populated data structure as input, it accepts an inversion-of-control callback `f(*this)` that runs after the mandatory bookkeeping fields are set but before the signature is computed. This lets callers attach arbitrary optional fields — fee votes, amendment bits, server version — without a separate builder type or a mutable pre-signature staging object. After `f` returns, the constructor sets `vfFullyCanonicalSig`, signs the content hash using `signDigest`, marks the validation as trusted (self-issued validations are always locally trusted), and caches `valid_ = true` to skip future signature checks. A format validation sweep then asserts that all `soeREQUIRED` fields were filled in. + +## Valid vs. Trusted: A Critical Distinction + +The class separates two concepts that are easy to conflate: + +**`valid_`** (`mutable std::optional`) reflects whether the cryptographic signature is correct. It starts as `std::nullopt` for deserialized validations and is lazily populated on the first call to `isValid()`. The `mutable` qualifier is justified because signature verification is a pure read of immutable byte content — it produces a cacheable result without mutating any observable state. Because secp256k1 verification is expensive and validations may be checked multiple times as they propagate through the validation cache, this lazy-cached pattern matters for performance. + +**`mTrusted`** is a runtime policy flag that reflects whether the issuing validator is on this node's current Unique Node List. A validation may be cryptographically valid but untrusted (validator not on the UNL), or trusted but structurally incomplete. The `setTrusted()` / `setUntrusted()` mutators exist because trust status can be toggled dynamically as the UNL changes during a node's lifetime. Keeping this separate from validity enforces that "has a good signature" and "should count toward quorum" are never silently conflated. + +## Sign Time vs. Seen Time + +`sfSigningTime` (inside the serialized `STObject` payload) records when the validator created and signed the validation. `seenTime_` is local metadata recording when *this* node received the message. These diverge under realistic network conditions: a validation signed at time T may arrive at a peer two seconds later. The seen time is never serialized or sent over the wire — it is set via `setSeen()` after receipt. For self-generated validations, the signing constructor initializes `seenTime_` to `signTime`, making the two identical when the local node is the author. + +## The `validationFormat()` Schema + +The private static `validationFormat()` returns the `SOTemplate` that constrains which fields may appear in a validation, and which are required vs. optional. Required fields include `sfFlags`, `sfLedgerHash`, `sfLedgerSequence`, `sfSigningTime`, `sfSigningPubKey`, and `sfSignature`. Optional fields include `sfCloseTime`, `sfLoadFee`, `sfAmendments`, `sfConsensusHash`, `sfValidatedHash`, `sfServerVersion`, and the newer XRPFees amendment fields. The `soeDEFAULT` entry for `sfCookie` means the field is always present in the serialized form but defaults to zero. Keeping this schema private and as a function-local static (rather than a namespace-scope static) avoids the static initialization order problem — `SOTemplate` construction depends on `SField` objects being initialized first. + +## Flag Constants + +The two constants at the top of the file — `vfFullValidation` and `vfFullyCanonicalSig` — are bit flags stored in the serialized `sfFlags` field and are thus part of the wire protocol. `vfFullValidation` (bit 0) distinguishes a complete ledger validation from a partial validation that only signals participation in consensus without fully endorsing a specific ledger. `vfFullyCanonicalSig` (bit 31) indicates that the DER-encoded signature uses the low-S canonical form required by XRPL's signature rules; the signing constructor always sets this flag, and `isValid()` passes it to `verifyDigest` to enforce strictness on inbound messages. + +## Integration with the Consensus Layer + +`STValidation` objects are owned via `std::shared_ptr` and wrapped by `RCLValidation` in the consensus machinery. `RCLValidation` provides the concept interface expected by the generic `Validations` template — forwarding calls like `ledgerID()`, `seq()`, `signTime()`, `seenTime()`, `key()`, and `nodeID()` to the underlying `STValidation`. This adapter pattern keeps `STValidation` free of consensus-specific logic while still allowing it to participate in the generic quorum-counting engine. \ No newline at end of file diff --git a/include/xrpl/protocol/STVector256.h.ai.json b/include/xrpl/protocol/STVector256.h.ai.json new file mode 100644 index 0000000000..fe53b94266 --- /dev/null +++ b/include/xrpl/protocol/STVector256.h.ai.json @@ -0,0 +1,217 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "SField const& n", + "std::vector const& vector", + "SerialIter& sit", + "SField const& name" + ], + "lineno": 8, + "name": "STVector256" + } + ], + "description": "Defines the STVector256 class in the xrpl namespace, representing a serialized type that holds a vector of 256-bit values (uint256), with methods for manipulation, serialization, and JSON conversion.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STVector256.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "STVector256" + }, + { + "args": [ + "SField const& n" + ], + "lineno": 15, + "name": "STVector256" + }, + { + "args": [ + "std::vector const& vector" + ], + "lineno": 16, + "name": "STVector256" + }, + { + "args": [ + "SField const& n", + "std::vector const& vector" + ], + "lineno": 17, + "name": "STVector256" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 18, + "name": "STVector256" + }, + { + "args": [], + "lineno": 20, + "name": "getSType" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 23, + "name": "add" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 25, + "name": "getJson" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 28, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 31, + "name": "isDefault" + }, + { + "args": [ + "std::vector const& v" + ], + "lineno": 34, + "name": "operator=" + }, + { + "args": [ + "std::vector&& v" + ], + "lineno": 37, + "name": "operator=" + }, + { + "args": [ + "STVector256 const& v" + ], + "lineno": 40, + "name": "setValue" + }, + { + "args": [], + "lineno": 44, + "name": "operator std::vector" + }, + { + "args": [], + "lineno": 47, + "name": "size" + }, + { + "args": [ + "std::size_t n" + ], + "lineno": 50, + "name": "resize" + }, + { + "args": [], + "lineno": 53, + "name": "empty" + }, + { + "args": [ + "std::vector::size_type n" + ], + "lineno": 56, + "name": "operator[]" + }, + { + "args": [ + "std::vector::size_type n" + ], + "lineno": 59, + "name": "operator[]" + }, + { + "args": [], + "lineno": 62, + "name": "value" + }, + { + "args": [ + "std::vector::const_iterator pos", + "uint256 const& value" + ], + "lineno": 65, + "name": "insert" + }, + { + "args": [ + "uint256 const& v" + ], + "lineno": 68, + "name": "push_back" + }, + { + "args": [], + "lineno": 71, + "name": "begin" + }, + { + "args": [], + "lineno": 74, + "name": "begin" + }, + { + "args": [], + "lineno": 77, + "name": "end" + }, + { + "args": [], + "lineno": 80, + "name": "end" + }, + { + "args": [ + "std::vector::iterator position" + ], + "lineno": 83, + "name": "erase" + }, + { + "args": [], + "lineno": 86, + "name": "clear" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 90, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 91, + "name": "move" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STVector256.h.ai.md b/include/xrpl/protocol/STVector256.h.ai.md new file mode 100644 index 0000000000..b54be36b69 --- /dev/null +++ b/include/xrpl/protocol/STVector256.h.ai.md @@ -0,0 +1,39 @@ +# `STVector256` — Serialized Vector of 256-Bit Hashes + +## Role in the System + +`STVector256` is the XRPL serialized type for ordered lists of 256-bit values (`uint256`). It occupies type identifier `STI_VECTOR256` (wire code 19) in the ledger's binary type system and is how the protocol represents multi-hash fields in ledger objects and transactions — for example, the set of active amendments in a validator vote (`sfAmendments`), the page contents of a `DirectoryNode` (`sfIndexes`), and hash collections in other ledger structures (`sfHashes`). + +Its existence is necessary because the XRPL protocol needs a way to pack an arbitrarily-sized list of 32-byte hashes into a single typed, named field that can be serialized, hashed, and round-tripped through JSON without losing field identity. A plain `std::vector` would have no schema awareness; wrapping it in an `STBase` subclass gives it a `SField` name, a wire type, and all the hooks the serialization infrastructure requires. + +## Inheritance and Design + +The class inherits from both `STBase` and `CountedObject`. The `STBase` ancestry is the core contract — it provides the `SField` name binding, the virtual dispatch table for serialization (`add`), type identification (`getSType`), JSON export (`getJson`), equivalence checking (`isEquivalent`), and the placement-new buffer mechanism (`copy`/`move`) used by `detail::STVar` for small-object-optimized storage inside `STObject` fields. The `CountedObject` mixin adds zero-overhead instance counting through an atomic lock-free linked list, enabling diagnostic reporting of live `STVector256` instances at runtime. + +The internal store is a simple `std::vector mValue`. The class deliberately exposes the full mutation surface of `std::vector` (`push_back`, `insert`, `erase`, `resize`, `clear`, indexed access, iterators) rather than hiding the container behind a narrow interface. This pragmatic choice reflects that callers — ledger object builders, amendment processors, directory management code — routinely need to manipulate the list in-place, and wrapping every vector operation would add friction with no protocol safety benefit. + +## Serialization Protocol + +The binary wire format for `STVector256` is a variable-length (VL-prefixed) blob containing the hashes packed back-to-back with no padding or separators. The `add()` method calls `s.addVL(begin, end, size * 32)`, writing the byte count as a VL prefix followed by the raw 32-byte values. Two assertions guard this path: the field must be marked binary and its `fieldType` must be `STI_VECTOR256`, catching any accidental field misuse at debug time. + +Deserialization in the `SerialIter` constructor is the inverse: it reads the VL-prefixed blob, checks that the byte count is an exact multiple of 32 (throwing `std::runtime_error` otherwise — a deliberate hard failure since a mis-aligned vector is a protocol violation, not a recoverable condition), then iterates through the slice constructing each `uint256` from its 32-byte substring. `mValue.reserve(cnt)` is called first to avoid reallocation during the loop. + +## Copy and Move for STVar + +`copy()` and `move()` are private virtuals called only by `detail::STVar`, the type-erased wrapper that `STObject` uses to hold heterogeneous field values without pointer indirection for small types. The `STBase::emplace` template checks whether the object fits in the caller-supplied buffer `buf` of size `n`; if it does, it placement-news the object there; if not, it heap-allocates. For `STVector256` this typically means heap allocation because the embedded `std::vector` exceeds the small-buffer size, but the machinery supports both paths uniformly. + +## Value Semantics and Assignment + +The class provides two `operator=` overloads taking `std::vector` by value and by rvalue reference, enabling efficient move assignment when callers have a temporary vector ready. The explicit `operator std::vector()` conversion produces a copy — it is marked `explicit` to prevent accidental implicit copies in generic contexts. The separate `setValue(STVector256 const&)` copies only the inner `mValue`, deliberately excluding the `SField` name; this mirrors the wider `STBase` design where assignment copies the value but callers control the field binding independently. + +`isDefault()` returns `true` when the vector is empty, which determines whether the field is omitted during canonical serialization of `STObject` fields marked optional. + +## JSON Representation + +`getJson()` renders the vector as a JSON array of hex strings, one per `uint256` entry using `to_string()`. The `JsonOptions` parameter is accepted but unused — `STVector256` has no API-version-dependent presentation because a list of hashes has no ambiguity across API versions. + +## Key Invariants + +- The byte length of any deserialized blob must be divisible by 32; any other value is a hard protocol error thrown at construction time. +- `add()` requires the associated `SField` to be binary and typed `STI_VECTOR256`; mismatches are caught by `XRPL_ASSERT` in debug builds. +- An empty `STVector256` is the default state and is treated as absent when serializing optional fields. \ No newline at end of file diff --git a/include/xrpl/protocol/STXChainBridge.h.ai.json b/include/xrpl/protocol/STXChainBridge.h.ai.json new file mode 100644 index 0000000000..8e1faf6476 --- /dev/null +++ b/include/xrpl/protocol/STXChainBridge.h.ai.json @@ -0,0 +1,342 @@ +{ + "args": [ + { + "lineno": 27, + "name": "ct" + }, + { + "lineno": 30, + "name": "wasLockingChainSend" + }, + { + "lineno": 33, + "name": "wasLockingChainSend" + }, + { + "lineno": 37, + "name": "name" + }, + { + "lineno": 39, + "name": "rhs" + }, + { + "lineno": 41, + "name": "o" + }, + { + "lineno": 43, + "name": "srcChainDoor" + }, + { + "lineno": 43, + "name": "srcChainIssue" + }, + { + "lineno": 43, + "name": "dstChainDoor" + }, + { + "lineno": 43, + "name": "dstChainIssue" + }, + { + "lineno": 48, + "name": "v" + }, + { + "lineno": 50, + "name": "name" + }, + { + "lineno": 50, + "name": "v" + }, + { + "lineno": 52, + "name": "sit" + }, + { + "lineno": 52, + "name": "name" + }, + { + "lineno": 54, + "name": "rhs" + }, + { + "lineno": 84, + "name": "s" + }, + { + "lineno": 87, + "name": "t" + }, + { + "lineno": 100, + "name": "n" + }, + { + "lineno": 100, + "name": "buf" + }, + { + "lineno": 102, + "name": "n" + }, + { + "lineno": 102, + "name": "buf" + }, + { + "lineno": 106, + "name": "lhs" + }, + { + "lineno": 106, + "name": "rhs" + }, + { + "lineno": 115, + "name": "lhs" + }, + { + "lineno": 115, + "name": "rhs" + } + ], + "classes": [ + { + "args": [ + "void", + "SField const& name", + "STXChainBridge const& rhs", + "STObject const& o", + "AccountID const& srcChainDoor, Issue const& srcChainIssue, AccountID const& dstChainDoor, Issue const& dstChainIssue", + "Json::Value const& v", + "SField const& name, Json::Value const& v", + "SerialIter& sit, SField const& name" + ], + "lineno": 9, + "name": "STXChainBridge" + } + ], + "description": "Defines the STXChainBridge class, representing a cross-chain bridge object in the XRPL protocol, including its fields, constructors, and utility methods for serialization, comparison, and access.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/STXChainBridge.h", + "functions": [ + { + "args": [ + "ct" + ], + "lineno": 27, + "name": "otherChain" + }, + { + "args": [ + "wasLockingChainSend" + ], + "lineno": 30, + "name": "srcChain" + }, + { + "args": [ + "wasLockingChainSend" + ], + "lineno": 33, + "name": "dstChain" + }, + { + "args": [], + "lineno": 35, + "name": "STXChainBridge" + }, + { + "args": [ + "SField const& name" + ], + "lineno": 37, + "name": "STXChainBridge" + }, + { + "args": [ + "STXChainBridge const& rhs" + ], + "lineno": 39, + "name": "STXChainBridge" + }, + { + "args": [ + "STObject const& o" + ], + "lineno": 41, + "name": "STXChainBridge" + }, + { + "args": [ + "AccountID const& srcChainDoor", + "Issue const& srcChainIssue", + "AccountID const& dstChainDoor", + "Issue const& dstChainIssue" + ], + "lineno": 43, + "name": "STXChainBridge" + }, + { + "args": [ + "Json::Value const& v" + ], + "lineno": 48, + "name": "STXChainBridge" + }, + { + "args": [ + "SField const& name", + "Json::Value const& v" + ], + "lineno": 50, + "name": "STXChainBridge" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 52, + "name": "STXChainBridge" + }, + { + "args": [ + "STXChainBridge const& rhs" + ], + "lineno": 54, + "name": "operator=" + }, + { + "args": [], + "lineno": 57, + "name": "getText" + }, + { + "args": [], + "lineno": 59, + "name": "toSTObject" + }, + { + "args": [], + "lineno": 61, + "name": "lockingChainDoor" + }, + { + "args": [], + "lineno": 64, + "name": "lockingChainIssue" + }, + { + "args": [], + "lineno": 67, + "name": "issuingChainDoor" + }, + { + "args": [], + "lineno": 70, + "name": "issuingChainIssue" + }, + { + "args": [ + "ChainType ct" + ], + "lineno": 73, + "name": "door" + }, + { + "args": [ + "ChainType ct" + ], + "lineno": 76, + "name": "issue" + }, + { + "args": [], + "lineno": 79, + "name": "getSType" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 81, + "name": "getJson" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 84, + "name": "add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 87, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 90, + "name": "isDefault" + }, + { + "args": [], + "lineno": 93, + "name": "value" + }, + { + "args": [ + "SerialIter&", + "SField const& name" + ], + "lineno": 97, + "name": "construct" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 100, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 102, + "name": "move" + }, + { + "args": [ + "STXChainBridge const& lhs", + "STXChainBridge const& rhs" + ], + "lineno": 106, + "name": "operator==" + }, + { + "args": [ + "STXChainBridge const& lhs", + "STXChainBridge const& rhs" + ], + "lineno": 115, + "name": "operator<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/STXChainBridge.h.ai.md b/include/xrpl/protocol/STXChainBridge.h.ai.md new file mode 100644 index 0000000000..f9a86b1bf0 --- /dev/null +++ b/include/xrpl/protocol/STXChainBridge.h.ai.md @@ -0,0 +1,53 @@ +# `STXChainBridge` — Cross-Chain Bridge Serialized Type + +## Role in the System + +`STXChainBridge` is a first-class serialized type in the XRPL protocol, analogous to `STAccount` or `STAmount`, that encodes the four-field specification of a cross-chain bridge. A bridge in XRPL Sidechains connects two independent ledgers: a *locking chain* (where XRP or tokens are locked in escrow) and an *issuing chain* (where a wrapped representation is minted). Each side of the bridge is described by a door account (`AccountID`) and an asset type (`Issue`). `STXChainBridge` bundles these four pieces — `LockingChainDoor`, `LockingChainIssue`, `IssuingChainDoor`, `IssuingChainIssue` — into a single, typed, serializable ledger object that appears in bridge-related transactions and ledger entries. + +The class inherits from `STBase` (the abstract serialized-type base, identified by its `STI_XCHAIN_BRIDGE` type ID) and from `CountedObject` (a debug/diagnostic mixin that tracks live instance counts via an atomic lock-free linked list managed by `CountedObjects`). + +## The `ChainType` Abstraction + +The most architecturally significant element of this header is the `ChainType` enum and its accompanying static helper trio: + +```cpp +enum class ChainType { locking, issuing }; +static ChainType otherChain(ChainType ct); +static ChainType srcChain(bool wasLockingChainSend); +static ChainType dstChain(bool wasLockingChainSend); +``` + +Cross-chain transfers are inherently directional, but the direction flips depending on which chain initiated the send. Witness servers record this as a boolean `wasLockingChainSend`. Rather than scattering `if (wasLockingChainSend)` branches through attestation and transaction processing code, `srcChain()` and `dstChain()` normalize that boolean into a `ChainType`, allowing callers to write: + +```cpp +auto src = bridge.door(STXChainBridge::srcChain(wasLockingChainSend)); +auto dst = bridge.door(STXChainBridge::dstChain(wasLockingChainSend)); +``` + +This is why `door(ChainType)` and `issue(ChainType)` exist as dispatch accessors alongside the four named getters. The named accessors (`lockingChainDoor()`, etc.) are for contexts with fixed structural knowledge; the `ChainType`-parameterized accessors support generic code in `XChainAttestations` and transaction handlers that operate on either chain uniformly. All six of these accessors are `inline`, avoiding function call overhead on what are effectively field reads. + +## Multiple Construction Paths + +`STXChainBridge` provides seven constructors, reflecting the multiple ingress points for serialized types in XRPL: + +- **Default / `SField`**: Creates an empty bridge bound to a field name, used when constructing container objects (`STObject`) before values are filled in. +- **`AccountID`+`Issue` quadruple**: Direct programmatic construction, used in tests and internal code. +- **`STObject const&`**: Extracts sub-fields from an existing generic `STObject`, used during ledger deserialization when the parent object has already been parsed. +- **`SerialIter&`**: Streams the four sub-fields directly from a binary iterator, the hot path for on-disk and network deserialization. +- **`Json::Value const&`**: Deserializes from API JSON input. This constructor includes a strict field-whitelist check — it constructs a canonical empty bridge, inspects its JSON keys, and throws `std::runtime_error` on any unrecognized key in the input. This "extra field detection" acts as an API contract guard, catching typos and version mismatches at parse time rather than silently ignoring unknown data. + +## Serialization and Interoperability + +`add(Serializer&)` writes the four sub-fields sequentially in canonical order: `LockingChainDoor`, `LockingChainIssue`, `IssuingChainDoor`, `IssuingChainIssue`. Each sub-field delegates to its own `STAccount::add()` or `STIssue::add()`, which prepend the XRPL field type/ID header before the raw bytes. The deserialization constructor mirrors this exactly by reading from `SerialIter` in the same order. + +`toSTObject()` is a conversion that wraps the bridge's four fields into a generic `STObject`, needed when the bridge must participate in parts of the codebase that operate on `STObject` graphs (such as transaction metadata construction). This is a one-way lossy conversion in the sense that `STObject` carries dynamic field sets; `STXChainBridge` is the strongly-typed, canonical form. + +## Comparison and Value Semantics + +`operator==` and `operator<` are both implemented via `std::tie` across all four member fields in declaration order. This makes `STXChainBridge` usable as a key in `std::map` and `std::set`, which matters because bridge objects are used as lookup keys in cross-chain claim processing. The virtual `isEquivalent()` satisfies the `STBase` polymorphic comparison interface (used when bridges are stored as `STBase*` in containers); it performs a safe `dynamic_cast` before delegating to the concrete `operator==`. + +The `value_type = STXChainBridge` self-alias and the `value()` accessor that returns `*this` follow a convention shared by all XRPL serialized types: template code that expects `.value()` to strip the ST wrapper works uniformly whether the underlying type is primitive (where `value_type` differs) or compound (where, as here, it is the type itself). + +## Memory Management + +`copy(n, buf)` and `move(n, buf)` override the `STBase` small-buffer optimization protocol. The inherited `STBase::emplace()` placement-news the object into a caller-provided buffer if it fits in `n` bytes, otherwise falls back to heap allocation. This pattern allows `STVar` (the internal type-erased variant used inside `STObject`) to avoid heap allocations for common small types, though `STXChainBridge` — with its four member fields — is larger than most primitives and will typically heap-allocate. \ No newline at end of file diff --git a/include/xrpl/protocol/SecretKey.h.ai.json b/include/xrpl/protocol/SecretKey.h.ai.json new file mode 100644 index 0000000000..ff86a25fbd --- /dev/null +++ b/include/xrpl/protocol/SecretKey.h.ai.json @@ -0,0 +1,179 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "SecretKey" + } + ], + "description": "Defines the SecretKey class and related cryptographic functions for XRPL, including key generation, signing, and encoding utilities.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/SecretKey.h", + "functions": [ + { + "args": [], + "lineno": 34, + "name": "SecretKey::~SecretKey" + }, + { + "args": [ + "std::array const& data" + ], + "lineno": 36, + "name": "SecretKey::SecretKey" + }, + { + "args": [ + "Slice const& slice" + ], + "lineno": 37, + "name": "SecretKey::SecretKey" + }, + { + "args": [], + "lineno": 39, + "name": "SecretKey::data" + }, + { + "args": [], + "lineno": 44, + "name": "SecretKey::size" + }, + { + "args": [], + "lineno": 52, + "name": "SecretKey::to_string" + }, + { + "args": [], + "lineno": 57, + "name": "SecretKey::begin" + }, + { + "args": [], + "lineno": 62, + "name": "SecretKey::cbegin" + }, + { + "args": [], + "lineno": 67, + "name": "SecretKey::end" + }, + { + "args": [], + "lineno": 72, + "name": "SecretKey::cend" + }, + { + "args": [ + "SecretKey const& lhs", + "SecretKey const& rhs" + ], + "lineno": 77, + "name": "operator==" + }, + { + "args": [ + "SecretKey const& lhs", + "SecretKey const& rhs" + ], + "lineno": 80, + "name": "operator!=" + }, + { + "args": [ + "TokenType type", + "std::string const& s" + ], + "lineno": 85, + "name": "parseBase58" + }, + { + "args": [ + "TokenType type", + "SecretKey const& sk" + ], + "lineno": 88, + "name": "toBase58" + }, + { + "args": [], + "lineno": 94, + "name": "randomSecretKey" + }, + { + "args": [ + "KeyType type", + "Seed const& seed" + ], + "lineno": 97, + "name": "generateSecretKey" + }, + { + "args": [ + "KeyType type", + "SecretKey const& sk" + ], + "lineno": 100, + "name": "derivePublicKey" + }, + { + "args": [ + "KeyType type", + "Seed const& seed" + ], + "lineno": 107, + "name": "generateKeyPair" + }, + { + "args": [ + "KeyType type" + ], + "lineno": 113, + "name": "randomKeyPair" + }, + { + "args": [ + "PublicKey const& pk", + "SecretKey const& sk", + "uint256 const& digest" + ], + "lineno": 120, + "name": "signDigest" + }, + { + "args": [ + "KeyType type", + "SecretKey const& sk", + "uint256 const& digest" + ], + "lineno": 123, + "name": "signDigest" + }, + { + "args": [ + "PublicKey const& pk", + "SecretKey const& sk", + "Slice const& message" + ], + "lineno": 131, + "name": "sign" + }, + { + "args": [ + "KeyType type", + "SecretKey const& sk", + "Slice const& message" + ], + "lineno": 134, + "name": "sign" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/SecretKey.h.ai.md b/include/xrpl/protocol/SecretKey.h.ai.md new file mode 100644 index 0000000000..d50634aa1e --- /dev/null +++ b/include/xrpl/protocol/SecretKey.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/protocol/SecretKey.h` + +This header is the gateway to XRPL's private-key cryptography. It declares the `SecretKey` value type and a suite of free functions for key generation, public-key derivation, and message signing. The two supported cryptosystems — secp256k1 and Ed25519 — share the same storage type but diverge in every algorithm detail, so this header carefully unifies them behind a common API while encoding their fundamental differences in the function signatures. + +## The `SecretKey` Class + +`SecretKey` is a thin, fixed-size wrapper around 32 raw bytes. Its design is shaped by two competing concerns: ergonomics (it should be copyable and passable like any value type) and security (the key material must never leak or be held longer than necessary). + +The destructor is intentionally non-trivial: the implementation calls `secure_erase(buf_, sizeof(buf_))`, zeroing the backing array before the memory is released. This defends against cold-boot attacks and memory dumps. The same defensive zeroisation appears throughout the implementation: `randomSecretKey()` zeroes its stack buffer after constructing the `SecretKey`, and the intermediate derivation results in `generateSecretKey()` and `detail::Generator` are likewise wiped immediately after use. + +The default constructor is deleted. A `SecretKey` must always be initialised with actual key material — either a 32-byte `std::array` or a `Slice`. The `Slice` constructor is strict: it throws a `LogicError` if the slice is not exactly 32 bytes, enforcing the invariant at construction time. + +Comparison operators (`operator==` and `operator!=`) are deleted both as member functions and as free functions. This is a deliberate security decision: comparing secret keys in application code is almost always a mistake (you compare public keys or account IDs instead), and any implementation of comparison risks timing-observable branches that could leak the key material through a side channel. + +`operator<<` is also intentionally absent. The comment in the header makes this explicit — streaming a secret key to a log or debug output is too easy an accident. `to_string()` exists for the rare legitimate case (e.g., CLI tooling), but the caller must explicitly request the hex representation. + +## Signing + +Two distinct signing entry points exist because the two cryptosystems have different security models around hashing. + +`sign()` takes a raw `Slice` message. For secp256k1 it first hashes the message with SHA512-Half and signs the resulting 256-bit digest; for Ed25519 it passes the raw message bytes directly to the `ed25519_sign` primitive, which incorporates its own deterministic internal hashing. Allowing callers to pre-hash an Ed25519 message would be cryptographically unsound — Ed25519's security proof depends on how the message is hashed — so the function dispatches on key type and handles the difference internally. + +`signDigest()` accepts a pre-computed `uint256` digest and is restricted to secp256k1. The signature is returned as a DER-encoded `Buffer` (up to 72 bytes). Both `sign()` and `signDigest()` have a two-argument convenience overload that accepts a `KeyType` instead of an explicit `PublicKey`, calling `derivePublicKey()` internally to identify the algorithm before signing. The ECDSA nonce is generated via RFC 6979 (`secp256k1_nonce_function_rfc6979`), making secp256k1 signatures deterministic and immune to the class of vulnerabilities caused by weak random nonces. + +## Key Generation and Derivation + +`randomSecretKey()` fills a 32-byte buffer from `crypto_prng()` (a CSPRNG), constructs the key, zeroes the temporary buffer, and returns. This is appropriate for Ed25519 keys or for secp256k1 keys that don't need to be re-derived from a seed. + +`generateSecretKey(type, seed)` is deterministic: +- For **Ed25519**, the secret key is simply SHA512-Half of the 16-byte seed. The resulting 32 bytes are directly usable as an Ed25519 private scalar. +- For **secp256k1**, the function calls `detail::deriveDeterministicRootKey()`, which concatenates the seed with a big-endian 32-bit sequence counter, hashes with SHA512-Half, and retries with incrementing counter values until the result is a valid secp256k1 scalar (i.e., non-zero and less than the curve order). In practice this loop almost never executes more than once. + +`generateKeyPair(type, seed)` is the main entry point for wallet-style key derivation: +- For **Ed25519** it is equivalent to calling `generateSecretKey` followed by `derivePublicKey`. +- For **secp256k1** it uses the `detail::Generator` class, which implements XRPL's custom two-level derivation. The generator derives a root private key from the seed, computes its compressed public key (the "generator point"), and then produces ordinal-0 output by computing a tweak via SHA512-Half of the generator concatenated with the ordinal and a sub-sequence counter, then adding that tweak to the root key with `secp256k1_ec_seckey_tweak_add`. This algorithm predates BIP-32 and is XRPL-specific; the comment in the implementation notes that third-party wallets are not required to implement it, but should to support account import. + +`derivePublicKey(type, sk)` converts a secret key to its corresponding `PublicKey`. For secp256k1 it calls `secp256k1_ec_pubkey_create` and serialises the result as a 33-byte compressed point. For Ed25519 it prepends the constant byte `0xED` to the 32-byte Edwards curve public key, producing the 33-byte XRPL encoding that `publicKeyType()` uses to distinguish Ed25519 keys from secp256k1 keys at runtime. + +## Serialisation + +`toBase58(type, sk)` and the specialisation `parseBase58(type, s)` provide the wire encoding. The `TokenType` argument (`AccountSecret`, `FamilySeed`, etc.) controls the version byte prepended during Base58Check encoding, consistent with the token system used throughout the XRPL protocol layer. `parseBase58` validates the decoded length and returns `std::nullopt` on any mismatch, never throwing. \ No newline at end of file diff --git a/include/xrpl/protocol/Seed.h.ai.json b/include/xrpl/protocol/Seed.h.ai.json new file mode 100644 index 0000000000..c0dcbafe82 --- /dev/null +++ b/include/xrpl/protocol/Seed.h.ai.json @@ -0,0 +1,143 @@ +{ + "args": [ + { + "lineno": 29, + "name": "slice" + }, + { + "lineno": 30, + "name": "seed" + }, + { + "lineno": 69, + "name": "passPhrase" + }, + { + "lineno": 80, + "name": "s" + }, + { + "lineno": 87, + "name": "str" + }, + { + "lineno": 87, + "name": "rfc1751" + }, + { + "lineno": 93, + "name": "seed" + } + ], + "classes": [ + { + "args": [ + "Slice const& slice", + "uint128 const& seed" + ], + "lineno": 10, + "name": "Seed" + } + ], + "description": "Defines the Seed class and related functions for generating, parsing, and encoding cryptographic seeds used to generate deterministic secret keys in XRPL.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Seed.h", + "functions": [ + { + "args": [], + "lineno": 25, + "name": "~Seed" + }, + { + "args": [ + "Slice const& slice" + ], + "lineno": 29, + "name": "Seed" + }, + { + "args": [ + "uint128 const& seed" + ], + "lineno": 30, + "name": "Seed" + }, + { + "args": [], + "lineno": 33, + "name": "data" + }, + { + "args": [], + "lineno": 38, + "name": "size" + }, + { + "args": [], + "lineno": 43, + "name": "begin" + }, + { + "args": [], + "lineno": 48, + "name": "cbegin" + }, + { + "args": [], + "lineno": 53, + "name": "end" + }, + { + "args": [], + "lineno": 58, + "name": "cend" + }, + { + "args": [], + "lineno": 65, + "name": "randomSeed" + }, + { + "args": [ + "std::string const& passPhrase" + ], + "lineno": 69, + "name": "generateSeed" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 80, + "name": "parseBase58" + }, + { + "args": [ + "std::string const& str", + "bool rfc1751 = true" + ], + "lineno": 87, + "name": "parseGenericSeed" + }, + { + "args": [ + "Seed const& seed" + ], + "lineno": 93, + "name": "seedAs1751" + }, + { + "args": [ + "Seed const& seed" + ], + "lineno": 97, + "name": "toBase58" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Seed.h.ai.md b/include/xrpl/protocol/Seed.h.ai.md new file mode 100644 index 0000000000..210f6fd8ac --- /dev/null +++ b/include/xrpl/protocol/Seed.h.ai.md @@ -0,0 +1,47 @@ +# `include/xrpl/protocol/Seed.h` — Cryptographic Seed Abstraction + +`Seed.h` defines the `Seed` class and its family of construction and encoding helpers. A seed in XRPL is the 128-bit secret from which all downstream key material — private keys, public keys, and account addresses — is deterministically derived. This file is the gateway between human-readable or randomly generated entropy and the protocol's signing infrastructure. + +## The `Seed` Class + +`Seed` wraps a `std::array` — exactly 128 bits — with two deliberate design constraints that reflect the sensitivity of the data it holds. + +First, the default constructor is deleted (`Seed() = delete`). A zero seed is dangerous because it could be mistaken for valid entropy, so the class enforces that every `Seed` object is initialized from real material — either a `Slice` or a `uint128`. The constructor validates the size and raises a `LogicError` if mismatched, catching integration bugs at development time. + +Second, the destructor calls `secure_erase()` on the internal buffer. The `secure_erase` implementation takes deliberate steps — including using volatile writes and memory barriers — to prevent the compiler from optimizing away the zeroing pass. The comment in `secure_erase.h` acknowledges honestly that this is best-effort: CPU caches and registers may still hold remnants, but heap/stack memory is overwritten. This mirrors industry practice (e.g., OpenSSL's `OPENSSL_cleanse`). + +Copy construction and copy assignment are explicitly defaulted. This is intentional: seeds must be passable by value across function boundaries (e.g., into key-derivation functions), and preventing copies would force raw pointer passing, which is more error-prone. The tradeoff is that callers must be mindful of how many copies exist in memory at once. + +Only read access is exposed externally: `data()` returns a `const uint8_t*`, and the iterator pair exposes `const_iterator` only. This prevents any code outside the class from accidentally mutating live key material. + +## Construction Paths + +`randomSeed()` fills a temporary local buffer using `beast::rngfill()` backed by `crypto_prng()` (the global CSPRNG), constructs a `Seed` from it, and then immediately calls `secure_erase()` on the local buffer before returning. This two-step pattern is important: the `Seed` destructor handles its own cleanup, but the transient staging buffer would otherwise linger on the stack until its scope ends — so it is explicitly zeroed first. + +`generateSeed(passPhrase)` implements the XRPL-specific passphrase-to-seed algorithm: compute SHA-512-Half of the raw passphrase bytes (no null terminator), then take the first 128 bits. The SHA-512-Half (`sha512_half_hasher_s`) uses a streaming hasher that writes to a stack-local buffer. This function deliberately does not attempt to detect whether the string is hex or Base58 — that disambiguation is the job of `parseGenericSeed()`. The suffix `_s` on the hasher type indicates it performs a secure erase of internal state on destruction. + +## Parsing: `parseGenericSeed()` and `parseBase58()` + +`parseBase58` is a template specialization (the primary template lives in `tokens.h`) that decodes a Base58Check-encoded string carrying the `TokenType::FamilySeed` prefix byte (value 33). A successful decode must produce exactly 16 bytes; anything else returns `std::nullopt`. + +`parseGenericSeed()` is the more interesting function. It operates as a cascading fallback parser designed to accept whatever format a caller might supply: + +1. **Reject other key types first.** Before attempting any format, it checks whether the string parses successfully as an `AccountID`, `NodePublic`, `AccountPublic`, `NodePrivate`, or `AccountSecret`. If it does, `parseGenericSeed` returns `std::nullopt` — this is a deliberate security guard preventing accidental use of an address or public key as a seed. + +2. **Try hex.** A 32-character hex string maps directly to a 128-bit seed via `uint128::parseHex()`. + +3. **Try Base58 family seed.** Standard encoded XRPL wallet seeds (the "s…" strings in the XRPL alphabet). + +4. **Try RFC1751 mnemonic** (when `rfc1751 = true`). RFC1751 encodes 64-bit keys as sequences of short English words. XRPL adopted this format early and maintains backward compatibility — note that the implementation reverses the byte order when converting between RFC1751 encoding and the internal buffer, which matches the historical XRPL convention. + +5. **Treat as passphrase.** The ultimate fallback is to run `generateSeed(str)`, turning an arbitrary string into a deterministic seed via SHA-512-Half. This ensures `parseGenericSeed` never returns `std::nullopt` for non-empty input (unless the string was recognized as another key type). + +## Encoding + +`toBase58()` is an inline function that delegates to `encodeBase58Token(TokenType::FamilySeed, ...)`. Seeds always use the `FamilySeed` token type in XRPL's Base58Check scheme, which produces the well-known "s"-prefixed wallet seed strings displayed to users. + +`seedAs1751()` encodes a seed in RFC1751 format. The implementation reverses the 16 bytes before passing them to `RFC1751::getEnglishFromKey()` — this byte-reversal is baked into the XRPL convention and must be matched symmetrically in `parseGenericSeed` when reading RFC1751 input. RFC1751 output is treated as deprecated; `parseGenericSeed` accepts it by default for backward compatibility but exposes an `rfc1751` flag so callers can disable the fallback when strict format enforcement is required. + +## Relationship to Key Derivation + +`Seed.h` is intentionally decoupled from the actual key-derivation step. The `Seed` object simply holds 128 bits; how those bits are used to derive a `SecretKey` depends on the key type (secp256k1 family-seed derivation, ed25519, etc.) and is handled in `SecretKey.h` / `PublicKey.h`. This separation means the seed representation is stable and format-agnostic even as cryptographic algorithms evolve. \ No newline at end of file diff --git a/include/xrpl/protocol/SeqProxy.h.ai.json b/include/xrpl/protocol/SeqProxy.h.ai.json new file mode 100644 index 0000000000..56cab8c63f --- /dev/null +++ b/include/xrpl/protocol/SeqProxy.h.ai.json @@ -0,0 +1,131 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Type t", + "std::uint32_t v" + ], + "lineno": 34, + "name": "SeqProxy" + } + ], + "description": "Defines the SeqProxy class in the xrpl namespace, representing a value that can be either a sequence or a ticket, with logic for comparison, advancement, and type checking.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/SeqProxy.h", + "functions": [ + { + "args": [ + "Type t", + "std::uint32_t v" + ], + "lineno": 38, + "name": "SeqProxy" + }, + { + "args": [ + "SeqProxy const& other" + ], + "lineno": 41, + "name": "SeqProxy" + }, + { + "args": [ + "SeqProxy const& other" + ], + "lineno": 44, + "name": "operator=" + }, + { + "args": [ + "std::uint32_t v" + ], + "lineno": 49, + "name": "sequence" + }, + { + "args": [], + "lineno": 54, + "name": "value" + }, + { + "args": [], + "lineno": 59, + "name": "isSeq" + }, + { + "args": [], + "lineno": 64, + "name": "isTicket" + }, + { + "args": [ + "std::uint32_t amount" + ], + "lineno": 71, + "name": "advanceBy" + }, + { + "args": [ + "SeqProxy lhs", + "SeqProxy rhs" + ], + "lineno": 84, + "name": "operator==" + }, + { + "args": [ + "SeqProxy lhs", + "SeqProxy rhs" + ], + "lineno": 91, + "name": "operator!=" + }, + { + "args": [ + "SeqProxy lhs", + "SeqProxy rhs" + ], + "lineno": 96, + "name": "operator<" + }, + { + "args": [ + "SeqProxy lhs", + "SeqProxy rhs" + ], + "lineno": 103, + "name": "operator>" + }, + { + "args": [ + "SeqProxy lhs", + "SeqProxy rhs" + ], + "lineno": 108, + "name": "operator>=" + }, + { + "args": [ + "SeqProxy lhs", + "SeqProxy rhs" + ], + "lineno": 113, + "name": "operator<=" + }, + { + "args": [ + "std::ostream& os", + "SeqProxy seqProx" + ], + "lineno": 118, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/SeqProxy.h.ai.md b/include/xrpl/protocol/SeqProxy.h.ai.md new file mode 100644 index 0000000000..a1759371d0 --- /dev/null +++ b/include/xrpl/protocol/SeqProxy.h.ai.md @@ -0,0 +1,38 @@ +# `SeqProxy.h` — Unified Sequence/Ticket Identifier + +## Role in the System + +`SeqProxy` is a small discriminated-value type — a type-tagged `uint32_t` — that represents either a traditional account **sequence number** or a **ticket sequence number**. It lives in `include/xrpl/protocol/SeqProxy.h` and has no dependencies beyond `` and ``, making it a foundational primitive that the rest of the protocol layer can include cheaply. + +The class was introduced when XRPL added the Tickets feature. Before tickets, every XRPL transaction consumed exactly one account sequence number in order, so a plain `uint32_t` sufficed as a transaction identifier. Tickets allow an account to pre-reserve sequence slots and use them out-of-order, which creates a second namespace of transaction identifiers. Rather than scattering `bool isTicket` flags throughout every piece of code that tracks transaction identity, `SeqProxy` encapsulates the choice in one place. + +## Safety of the Shared Numeric Space + +At first glance it seems dangerous to use `SeqProxy::value()` as a bare number for ledger-object keys (offers, checks, escrows, payment channels all use it this way). The class comment explains the two-part invariant that makes it safe: + +1. A `TicketCreate` transactor always creates tickets whose numeric values fall within the range that the account's root sequence has already advanced past — so a ticket value can never match any sequence value that will be used in the future for 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. This means every ticket in the batch has a unique value that is permanently retired from the sequence namespace. + +Together these guarantee that the numeric values of ticket proxies and sequence proxies for a given account never collide, even when stored without type metadata. + +## Design Decisions + +**`constexpr` throughout.** All constructors, accessors, and comparison operators are `constexpr`. The type is trivially copyable and small enough to pass by value everywhere. There is no indirection, no heap allocation, and no virtual dispatch. + +**Factory function only for the common case.** `SeqProxy::sequence(v)` is a named static factory for the `seq` type, but tickets are constructed directly with `SeqProxy{SeqProxy::ticket, value}`. This asymmetry reflects usage patterns: normal transaction processing almost always starts with a sequence number; ticket construction is an uncommon, explicit act. + +**`advanceBy()` instead of `operator+`.** The only mutating operation on a `SeqProxy` is `advanceBy(amount)`, which increments `value_` in place and returns `*this`. The deliberate choice of a named method over `operator+` or `operator+=` makes accidental arithmetic visible — you have to consciously invoke it. It is currently used only in tests to increment a `SeqProxy` across a sequence of dummy transactions. + +**Sorting: sequences always before tickets.** The `operator<` implementation first compares the `Type` tag. Since `seq == 0` and `ticket == 1`, all sequence-typed proxies sort strictly before all ticket-typed proxies, regardless of numeric value. The comment explicitly calls out the benefit: in `CanonicalTXSet`, transactions from the same account are sorted by their `SeqProxy`. This means `TicketCreate` transactions (which carry a sequence number) always precede the ticket-consuming transactions they enable, preventing ordering inversions during consensus replay. + +## Key Relationships + +**`STTx::getSeqProxy()`** (`src/libxrpl/protocol/STTx.cpp`) is the primary construction site in production code. It reads the raw transaction's `sfSequence` field; if non-zero that becomes a `SeqProxy::sequence`. If zero, it checks for `sfTicketSequence` and returns a ticket-typed proxy. The fallback to `SeqProxy::sequence(0)` for transactions with neither field set preserves backward compatibility. + +**`CanonicalTXSet`** (`include/xrpl/ledger/CanonicalTXSet.h`) stores a `SeqProxy` inside its internal `Key` struct. The set provides the ordered, per-account transaction queue used during consensus to apply deferred transactions in canonical order. The `SeqProxy` ordering guarantee is what makes that ordering correct for ticket-bearing transaction sets. + +**`Indexes.h`** forward-declares `SeqProxy` and uses it in `getTicketIndex()` to compute the ledger object key for a ticket entry. Passing the full `SeqProxy` (rather than a bare `uint32_t`) keeps the type-check in the call path and makes the interface self-documenting. + +## Summary + +`SeqProxy` is an intentionally minimal abstraction — just 5 bytes of data and a handful of `constexpr` methods — that eliminates an entire class of bugs that would arise from mixing sequence numbers and ticket numbers as bare integers. Its asymmetric sort order (sequences before tickets) is a deliberate, documented protocol-level contract that the consensus ordering machinery depends on. \ No newline at end of file diff --git a/include/xrpl/protocol/Serializer.h.ai.json b/include/xrpl/protocol/Serializer.h.ai.json new file mode 100644 index 0000000000..cbbe47a222 --- /dev/null +++ b/include/xrpl/protocol/Serializer.h.ai.json @@ -0,0 +1,521 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "n" + ], + "lineno": 13, + "name": "Serializer" + }, + { + "args": [ + "data", + "size" + ], + "lineno": 222, + "name": "SerialIter" + } + ], + "description": "Provides serialization and deserialization utilities for XRPL protocol objects, including the Serializer and SerialIter classes for assembling and parsing binary data.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Serializer.h", + "functions": [ + { + "args": [], + "lineno": 27, + "name": "Serializer::slice" + }, + { + "args": [], + "lineno": 32, + "name": "Serializer::size" + }, + { + "args": [], + "lineno": 37, + "name": "Serializer::data" + }, + { + "args": [ + "i" + ], + "lineno": 43, + "name": "Serializer::add8" + }, + { + "args": [ + "i" + ], + "lineno": 44, + "name": "Serializer::add16" + }, + { + "args": [ + "i" + ], + "lineno": 48, + "name": "Serializer::add32" + }, + { + "args": [ + "p" + ], + "lineno": 59, + "name": "Serializer::add32" + }, + { + "args": [ + "i" + ], + "lineno": 62, + "name": "Serializer::add64" + }, + { + "args": [ + "Integer" + ], + "lineno": 75, + "name": "Serializer::addInteger" + }, + { + "args": [ + "v" + ], + "lineno": 78, + "name": "Serializer::addBitString" + }, + { + "args": [ + "vector" + ], + "lineno": 83, + "name": "Serializer::addRaw" + }, + { + "args": [ + "slice" + ], + "lineno": 84, + "name": "Serializer::addRaw" + }, + { + "args": [ + "ptr", + "len" + ], + "lineno": 85, + "name": "Serializer::addRaw" + }, + { + "args": [ + "s" + ], + "lineno": 86, + "name": "Serializer::addRaw" + }, + { + "args": [ + "vector" + ], + "lineno": 88, + "name": "Serializer::addVL" + }, + { + "args": [ + "slice" + ], + "lineno": 89, + "name": "Serializer::addVL" + }, + { + "args": [ + "begin", + "end", + "len" + ], + "lineno": 91, + "name": "Serializer::addVL" + }, + { + "args": [ + "ptr", + "len" + ], + "lineno": 94, + "name": "Serializer::addVL" + }, + { + "args": [ + "int&", + "offset" + ], + "lineno": 97, + "name": "Serializer::get8" + }, + { + "args": [ + "number", + "offset" + ], + "lineno": 100, + "name": "Serializer::getInteger" + }, + { + "args": [ + "data", + "offset" + ], + "lineno": 116, + "name": "Serializer::getBitString" + }, + { + "args": [ + "type", + "name" + ], + "lineno": 124, + "name": "Serializer::addFieldID" + }, + { + "args": [ + "type", + "name" + ], + "lineno": 126, + "name": "Serializer::addFieldID" + }, + { + "args": [], + "lineno": 131, + "name": "Serializer::getSHA512Half" + }, + { + "args": [], + "lineno": 134, + "name": "Serializer::peekData" + }, + { + "args": [], + "lineno": 137, + "name": "Serializer::getData" + }, + { + "args": [], + "lineno": 140, + "name": "Serializer::modData" + }, + { + "args": [], + "lineno": 144, + "name": "Serializer::getDataLength" + }, + { + "args": [], + "lineno": 147, + "name": "Serializer::getDataPtr" + }, + { + "args": [], + "lineno": 150, + "name": "Serializer::getDataPtr" + }, + { + "args": [], + "lineno": 153, + "name": "Serializer::getLength" + }, + { + "args": [], + "lineno": 156, + "name": "Serializer::getString" + }, + { + "args": [], + "lineno": 160, + "name": "Serializer::erase" + }, + { + "args": [ + "num" + ], + "lineno": 163, + "name": "Serializer::chop" + }, + { + "args": [], + "lineno": 166, + "name": "Serializer::begin" + }, + { + "args": [], + "lineno": 169, + "name": "Serializer::end" + }, + { + "args": [], + "lineno": 172, + "name": "Serializer::begin" + }, + { + "args": [], + "lineno": 175, + "name": "Serializer::end" + }, + { + "args": [ + "n" + ], + "lineno": 178, + "name": "Serializer::reserve" + }, + { + "args": [ + "n" + ], + "lineno": 181, + "name": "Serializer::resize" + }, + { + "args": [], + "lineno": 184, + "name": "Serializer::capacity" + }, + { + "args": [ + "v" + ], + "lineno": 187, + "name": "Serializer::operator==" + }, + { + "args": [ + "v" + ], + "lineno": 190, + "name": "Serializer::operator!=" + }, + { + "args": [ + "v" + ], + "lineno": 193, + "name": "Serializer::operator==" + }, + { + "args": [ + "v" + ], + "lineno": 196, + "name": "Serializer::operator!=" + }, + { + "args": [ + "b1" + ], + "lineno": 199, + "name": "Serializer::decodeLengthLength" + }, + { + "args": [ + "b1" + ], + "lineno": 200, + "name": "Serializer::decodeVLLength" + }, + { + "args": [ + "b1", + "b2" + ], + "lineno": 201, + "name": "Serializer::decodeVLLength" + }, + { + "args": [ + "b1", + "b2", + "b3" + ], + "lineno": 202, + "name": "Serializer::decodeVLLength" + }, + { + "args": [ + "length" + ], + "lineno": 206, + "name": "Serializer::encodeLengthLength" + }, + { + "args": [ + "length" + ], + "lineno": 207, + "name": "Serializer::addEncoded" + }, + { + "args": [ + "begin", + "end", + "len" + ], + "lineno": 211, + "name": "Serializer::addVL" + }, + { + "args": [ + "data", + "size" + ], + "lineno": 229, + "name": "SerialIter::SerialIter" + }, + { + "args": [ + "slice" + ], + "lineno": 231, + "name": "SerialIter::SerialIter" + }, + { + "args": [ + "data" + ], + "lineno": 236, + "name": "SerialIter::SerialIter" + }, + { + "args": [], + "lineno": 240, + "name": "SerialIter::empty" + }, + { + "args": [], + "lineno": 244, + "name": "SerialIter::reset" + }, + { + "args": [], + "lineno": 247, + "name": "SerialIter::getBytesLeft" + }, + { + "args": [], + "lineno": 252, + "name": "SerialIter::get8" + }, + { + "args": [], + "lineno": 254, + "name": "SerialIter::get16" + }, + { + "args": [], + "lineno": 256, + "name": "SerialIter::get32" + }, + { + "args": [], + "lineno": 257, + "name": "SerialIter::geti32" + }, + { + "args": [], + "lineno": 259, + "name": "SerialIter::get64" + }, + { + "args": [], + "lineno": 260, + "name": "SerialIter::geti64" + }, + { + "args": [], + "lineno": 262, + "name": "SerialIter::getBitString" + }, + { + "args": [], + "lineno": 266, + "name": "SerialIter::get128" + }, + { + "args": [], + "lineno": 270, + "name": "SerialIter::get160" + }, + { + "args": [], + "lineno": 274, + "name": "SerialIter::get192" + }, + { + "args": [], + "lineno": 278, + "name": "SerialIter::get256" + }, + { + "args": [ + "type", + "name" + ], + "lineno": 282, + "name": "SerialIter::getFieldID" + }, + { + "args": [], + "lineno": 287, + "name": "SerialIter::getVLDataLength" + }, + { + "args": [ + "bytes" + ], + "lineno": 291, + "name": "SerialIter::getSlice" + }, + { + "args": [ + "size" + ], + "lineno": 295, + "name": "SerialIter::getRaw" + }, + { + "args": [], + "lineno": 298, + "name": "SerialIter::getVL" + }, + { + "args": [ + "num" + ], + "lineno": 301, + "name": "SerialIter::skip" + }, + { + "args": [], + "lineno": 304, + "name": "SerialIter::getVLBuffer" + }, + { + "args": [ + "size" + ], + "lineno": 307, + "name": "SerialIter::getRawHelper" + }, + { + "args": [], + "lineno": 312, + "name": "SerialIter::getBitString" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Serializer.h.ai.md b/include/xrpl/protocol/Serializer.h.ai.md new file mode 100644 index 0000000000..cfc7f46d4b --- /dev/null +++ b/include/xrpl/protocol/Serializer.h.ai.md @@ -0,0 +1,51 @@ +# `include/xrpl/protocol/Serializer.h` + +This file defines two complementary classes — `Serializer` (write side) and `SerialIter` (read side) — that together implement the XRPL canonical binary serialization format. Every transaction, ledger object, and signed message that travels across the XRP Ledger network is encoded using this format, making these classes among the most foundational in the protocol stack. + +## Role in the System + +The XRPL binary format must be deterministic across all nodes: the same object must hash to the same value everywhere, which requires byte-exact serialization in a well-defined byte order. `Serializer` builds those byte streams by appending typed values in big-endian order. `SerialIter` then consumes them linearly, acting as a forward-only cursor. Both classes are marked `DEPRECATED` in varying places, reflecting an ongoing migration toward lower-level types (`Slice`, `Buffer`) that avoid copying, but they remain the primary mechanism for constructing signable and hashable blobs throughout the codebase. + +## `Serializer` — The Write Side + +`Serializer` wraps a single private `Blob` (`std::vector`, itself marked DEPRECATED) and exposes an append-only API. Every `add*` method returns the byte offset at which it wrote, enabling callers to later locate specific fields for overwriting or inspection. The default constructor pre-reserves 256 bytes to avoid initial reallocations on typical transaction sizes. + +**Integer appending** is split into three tiers: + +- `add8` / `add16` are simple, non-templated methods. +- `add32` and `add64` are C++20-constrained templates accepting any type whose unsigned counterpart is exactly `uint32_t` or `uint64_t`. This lets the caller pass either `int32_t` or `uint32_t` to `add32` without casting, while the constraint blocks accidental narrowing from wider types at compile time. +- `addInteger` dispatches to the above via explicit template specializations in the `.cpp` file, covering `unsigned char`, `uint16_t`, `uint32_t`, `uint64_t`, and `int32_t`. + +A dedicated `add32(HashPrefix p)` overload exists for the four-byte hash space separators that prefix all XRPL hashing operations (e.g. `TXN` for transaction IDs, `STX` for signing). The static assert inside that overload guards that `HashPrefix`'s underlying type is permanently `uint32_t` — the comment notes this is an integral part of the protocol and must never change. + +**Variable-length encoding** (`addVL`) prepends a 1–3 byte length header before the payload data: +- 0–192 bytes → 1-byte header (the length itself) +- 193–12,480 bytes → 2-byte header (`193 + high + low`) +- 12,481–918,744 bytes → 3-byte header (`241 + top + mid + low`) +- Larger → `std::overflow_error` + +This compact encoding trades a lookup table for a small amount of arithmetic and ensures common short fields (account IDs, hashes, small blobs) need only a single prefix byte. `encodeLengthLength()` and `decodeLengthLength()` are the inverse functions; the three-overload family of `decodeVLLength` decodes the value from 1, 2, or 3 bytes respectively. + +**Field ID encoding** (`addFieldID`) implements the XRPL field type/name tagging scheme. Type IDs and field name IDs are integers in [1, 255]. When both fit in the range [1, 15], they are packed into a single byte (`(type << 4) | name`). When only one fits, two bytes are used; when neither fits, three bytes with a zero lead byte disambiguate. This compact scheme means the vast majority of protocol fields — all standard `STI_*` types and the most common fields — are tagged in one byte, minimising overhead for the most frequent case. + +**`getBitString`** provides direct memcpy-based reading at a given offset into a `base_uint` (the fixed-width integer type underlying `uint256`, `uint128`, etc.), used for random-access extraction after construction. + +**`getSHA512Half()`** is deprecated but still present. It computes the XRPL "SHA-512 half" hash (first 32 bytes of SHA-512) over the accumulated buffer, which was formerly the primary way to hash signable data before the digest utilities were factored out. + +## `SerialIter` — The Read Side + +`SerialIter` is a forward-only cursor over an external byte buffer. It stores three state values: `p_` (current position pointer), `remain_` (bytes not yet consumed), and `used_` (bytes consumed). The `reset()` method rewinds by subtracting `used_` from `p_` — this works because the underlying buffer is not owned and must outlive the iterator, a contract enforced by design (no copying of the input). + +All `get*` methods throw `std::runtime_error` on underflow rather than returning error codes. This is a deliberate asymmetry with the `Serializer` getter (`get8` returns `bool`): `SerialIter` is expected to be used in parsing paths where malformed data is an exceptional condition and callers do not check returns at every step. + +`getBitString()` is templated and returns `base_uint` constructed via `fromVoid`, providing zero-copy extraction of all the fixed-width types the protocol uses — `uint128`, `uint160`, `uint192`, `uint256`. The convenience wrappers `get128()`, `get160()`, `get192()`, `get256()` call it with the appropriate sizes. + +`getFieldID()` mirrors `addFieldID` exactly, decoding the 1–3 byte compact encoding back into separate type and name integers. `getVLDataLength()` reads and decodes the VL header, advancing the cursor to the start of the payload; `getVL()` combines this with `getRaw` to return the full payload as a `Blob` (deprecated: returns a copy), while `getVLBuffer()` returns a non-copying `Buffer`. + +`getSlice()` is the preferred zero-copy accessor: it returns a `Slice` pointing into the original buffer without allocating, advancing the cursor over the consumed bytes. + +## Design Tradeoffs and Deprecation Trajectory + +The `Serializer` class grew organically from XRPL's early codebase and carries significant surface area: multiple accessors for the same data (`peekData`, `getData`, `modData`, `getDataPtr`, `getDataLength`, `getLength`), vector-like methods (`begin`, `end`, `reserve`, `resize`, `capacity`), and even comparison operators against raw `Blob`. This breadth enabled easy integration across the codebase but also made the class a catch-all that mixes serialization concerns with container behavior. + +The `DEPRECATED` annotations throughout — on `mData` itself, on `getSHA512Half`, on `SerialIter::getRaw` and `SerialIter::getVL` — signal that newer code should prefer zero-copy patterns using `Slice` and `Buffer` directly. `SerialIter::getSlice` is the forward-looking alternative to `getRaw`. The `Serializer` class itself remains necessary wherever mutable accumulation into a `Blob` is needed, but the long-term direction is to avoid materializing copies where the data can instead be processed in place. \ No newline at end of file diff --git a/include/xrpl/protocol/Sign.h.ai.json b/include/xrpl/protocol/Sign.h.ai.json new file mode 100644 index 0000000000..8f649dfe3c --- /dev/null +++ b/include/xrpl/protocol/Sign.h.ai.json @@ -0,0 +1,96 @@ +{ + "args": [ + { + "lineno": 15, + "name": "st" + }, + { + "lineno": 16, + "name": "prefix" + }, + { + "lineno": 17, + "name": "type" + }, + { + "lineno": 18, + "name": "sk" + }, + { + "lineno": 19, + "name": "sigField" + }, + { + "lineno": 35, + "name": "pk" + }, + { + "lineno": 44, + "name": "obj" + }, + { + "lineno": 44, + "name": "signingID" + }, + { + "lineno": 59, + "name": "s" + } + ], + "classes": [], + "description": "Provides functions for signing and verifying STObject instances, as well as building and optimizing multisigning data for transactions in the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Sign.h", + "functions": [ + { + "args": [ + "st", + "prefix", + "type", + "sk", + "sigField" + ], + "lineno": 15, + "name": "sign" + }, + { + "args": [ + "st", + "prefix", + "pk", + "sigField" + ], + "lineno": 32, + "name": "verify" + }, + { + "args": [ + "obj", + "signingID" + ], + "lineno": 44, + "name": "buildMultiSigningData" + }, + { + "args": [ + "obj" + ], + "lineno": 54, + "name": "startMultiSigningData" + }, + { + "args": [ + "signingID", + "s" + ], + "lineno": 59, + "name": "finishMultiSigningData" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Sign.h.ai.md b/include/xrpl/protocol/Sign.h.ai.md new file mode 100644 index 0000000000..d0be8fd1af --- /dev/null +++ b/include/xrpl/protocol/Sign.h.ai.md @@ -0,0 +1,50 @@ +# `include/xrpl/protocol/Sign.h` + +## Purpose and Role + +`Sign.h` defines the signing and verification API for serialized XRPL protocol objects. It sits at the intersection of the cryptographic layer (`PublicKey`, `SecretKey`, `KeyType`) and the serialization layer (`STObject`, `HashPrefix`), providing the thin interface that everything from transaction submission to multisignature validation depends on. The implementation lives in `src/libxrpl/protocol/Sign.cpp`. + +## Core Signing Model + +Every signable object in the XRPL goes through the same pipeline: serialize the object's non-signature fields, prepend a domain-separation prefix, and feed the resulting bytes into the asymmetric signing function. The `sign()` function encapsulates this: + +```cpp +void sign(STObject& st, HashPrefix const& prefix, KeyType type, + SecretKey const& sk, SF_VL const& sigField = sfSignature); +``` + +Internally, a `Serializer` is constructed, the 4-byte `HashPrefix` is written first, then `st.addWithoutSigningFields(ss)` appends the canonical binary encoding of the object *with all signing-related fields omitted*. This break of the circular dependency — you cannot include the signature in the data you are signing — is handled entirely by `STObject::addWithoutSigningFields`. The resulting byte slice is passed to the lower-level `sign(KeyType, SecretKey const&, Slice)` function from `SecretKey.h`, which applies SHA512-Half hashing followed by secp256k1 or Ed25519 signing depending on `type`. The produced signature buffer is stored back into `st` at `sigField`. + +The `verify()` function mirrors this exactly: it reads the signature out of the object, regenerates the same serialized payload (prefix + fields-without-signatures), and calls the standalone `verify(PublicKey, Slice message, Slice sig)` function. + +## HashPrefix: Domain Separation + +The `HashPrefix` enum is a protocol-level invariant. Each prefix is a 4-byte big-endian value constructed from three ASCII characters, with the low byte fixed to zero (e.g., `txSign` = `"STX\0"`, `txMultiSign` = `"SMT\0"`). By injecting this prefix before the serialized payload, the protocol ensures that a valid signature over a transaction cannot be replayed as a valid signature over a ledger header, a validation, or a payment channel claim — even if they happen to share identical binary content after serialization. The `HashPrefix` enum is part of the wire protocol and must never be changed. + +## Multi-Signing: Two-Phase Optimization + +The protocol supports multi-signature transactions where a set of signers collectively authorize a transaction. Verifying N signers naively would require serializing the transaction body N times. The header exposes a split-phase API to avoid this: + +```cpp +Serializer startMultiSigningData(STObject const& obj); // serialize once +inline void finishMultiSigningData(AccountID const& signingID, Serializer& s); // append per-signer +``` + +`startMultiSigningData` prepends `HashPrefix::txMultiSign` and serializes the full transaction body without signing fields — the expensive shared work. `finishMultiSigningData` appends a single `AccountID` (32 bytes) to complete the per-signer payload. The convenience function `buildMultiSigningData` calls both in sequence for single-signer use. + +The `.cpp` includes an unusually detailed comment explaining *why* the signer's own `AccountID` must be appended. Without it, an attacker could substitute one `Signer.Account` for another account that happens to share the same `RegularKey` — for example, when a third-party service provides a common `RegularKey` across many accounts. Including the `AccountID` in the signed data binds each signature to a specific account identity, making that entire class of substitution attacks impossible. The comment also anticipates future multi-level signing hierarchies (Carol signs for Bob who signs for Alice), where distinguishing which intermediate account a signer is authorizing would require incorporating the "signing for" chain. + +## `SF_VL` and the `sfSignature` Default + +`SF_VL` is a `TypedField` — a named field descriptor for a variable-length binary blob within an `STObject`. The default parameter `sfSignature` is the standard transaction signature field, but the API accepts any `SF_VL` to support alternative signing contexts such as multi-signer inner structures, manifest signatures, or payment channel claims. The field-parameterized design avoids needing separate functions for each signing context. + +## SecretKey Safety Conventions + +`SecretKey` deliberately deletes `operator==` and `operator!=` to prevent timing-channel comparisons and to discourage accidental key equality checks. It also omits `operator<<` to prevent secret material from leaking into log streams. These are not accidental omissions — the destructor zeroes the key buffer, making `SecretKey` a security-sensitive RAII type. `Sign.h` operates directly with `SecretKey const&`, never copying or persisting the key material beyond the duration of the `sign()` call. + +## Relationship to Other Files + +- **`HashPrefix.h`** — supplies the domain-separation enum; this header cannot function without it. +- **`SecretKey.h`** — provides both the `SecretKey` type and the underlying `sign(KeyType, SecretKey, Slice)` free function that `Sign.h`'s `sign()` delegates to. +- **`PublicKey.h`** — provides `PublicKey` and the corresponding `verify(PublicKey, Slice, Slice)` function. +- **`STObject.h`** — provides `addWithoutSigningFields`, which is the serialization primitive that makes the circular-free encoding work. \ No newline at end of file diff --git a/include/xrpl/protocol/SystemParameters.h.ai.json b/include/xrpl/protocol/SystemParameters.h.ai.json new file mode 100644 index 0000000000..ca68e2c4ab --- /dev/null +++ b/include/xrpl/protocol/SystemParameters.h.ai.json @@ -0,0 +1,39 @@ +{ + "args": [], + "classes": [], + "description": "Defines protocol and system-specific constants and utility functions for the XRPL (XRP Ledger), including system name, currency code, initial XRP amount, and amendment thresholds.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/SystemParameters.h", + "functions": [ + { + "args": [], + "lineno": 10, + "name": "systemName" + }, + { + "args": [ + "amount" + ], + "lineno": 25, + "name": "isLegalAmount" + }, + { + "args": [ + "amount" + ], + "lineno": 32, + "name": "isLegalAmountSigned" + }, + { + "args": [], + "lineno": 39, + "name": "systemCurrencyCode" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/SystemParameters.h.ai.md b/include/xrpl/protocol/SystemParameters.h.ai.md new file mode 100644 index 0000000000..13eb8380b8 --- /dev/null +++ b/include/xrpl/protocol/SystemParameters.h.ai.md @@ -0,0 +1,37 @@ +# `include/xrpl/protocol/SystemParameters.h` + +This header is the canonical home for protocol-wide constants and the validation helpers that depend on them. Everything here is either a fixed property of the XRP Ledger network (total XRP supply, earliest known ledger, governance thresholds) or a convenience function built directly on those values. The file is intentionally small: it avoids heavy dependencies and is included across virtually the entire codebase, so keeping it lightweight matters. + +## Network Identity + +`systemName()` and `systemCurrencyCode()` both use the Meyers singleton pattern — a function-local `static const` — to return the strings `"xrpld"` and `"XRP"` respectively. This avoids the static initialization order fiasco that would arise from file-scope globals, while the `inline` keyword lets each translation unit include the header without ODR violations. Neither function is performance-sensitive; they exist to give the rest of the codebase a single authoritative source for these names rather than scattering string literals. + +## XRP Supply and Amount Validation + +`INITIAL_XRP` represents the entire XRP supply at ledger genesis: 100 billion XRP expressed in drops (the smallest indivisible unit). The calculation `100'000'000'000 * DROPS_PER_XRP` where `DROPS_PER_XRP = 1'000'000` produces exactly 100,000,000,000,000,000 drops (10¹⁷). Two `static_assert`s verify this at compile time: the first confirms the raw bit value, and the second asserts that `Number::maxRep` — the upper bound of XRPL's internal arbitrary-precision scalar type — can hold this value. If anyone changes either `Number`'s representation or the XRP total, the build fails immediately rather than silently producing wrong results at runtime. + +`isLegalAmount()` and `isLegalAmountSigned()` build directly on this constant. The unsigned variant simply checks `amount <= INITIAL_XRP`, while the signed variant extends the range to `[-INITIAL_XRP, INITIAL_XRP]` to accommodate deltas and fee calculations that may temporarily represent negative adjustments. Both appear in hot paths: `Transactor.cpp` calls `isLegalAmount` to reject transactions whose fee field would exceed the total XRP supply, and `InvariantCheck.cpp` uses them as post-transaction guards to ensure no ledger operation manufactures XRP out of thin air. + +## Historical Sequence Boundaries + +`XRP_LEDGER_EARLIEST_SEQ = 32570` encodes a historical accident of the XRP Ledger mainnet: ledgers 1 through 32569 were lost in an early incident and are no longer available anywhere. The `Database` class uses this as the default for the `earliest_seq` configuration parameter, clamping any node that does not configure a custom lower bound to refuse requests for pre-genesis sequences. + +`XRP_LEDGER_EARLIEST_FEES = 562177` marks the first mainnet ledger that contains a `FeeSettings` object. This constant appears in `XRPL_ASSERT` calls scattered across `BuildLedger.cpp`, `LedgerPersistence.cpp`, `InboundLedger.cpp`, and `Application.cpp`, always in the form: + +```cpp +XRPL_ASSERT( + ledger->header().seq < XRP_LEDGER_EARLIEST_FEES || ledger->read(keylet::fees()), + "..."); +``` + +The pattern means: if we are processing a modern ledger, a `FeeSettings` entry must exist; if we are replaying very old ledgers (before the object was introduced), we skip the check. Without this boundary, asserting the invariant unconditionally would always fail for historical replay of early ledgers. + +## Amendment Governance Constants + +`amendmentMajorityCalcThreshold` is declared as `std::ratio<80, 100>` rather than the obvious `constexpr double threshold = 0.8`. The reason is precision: `AmendmentTable.cpp` computes the required support count with `(trustedValidations_ * amendmentMajorityCalcThreshold.num) / amendmentMajorityCalcThreshold.den`, performing the multiplication before the division using plain integer arithmetic. A floating-point constant would introduce rounding error in what is effectively a consensus-critical gate; an off-by-one in the vote count threshold could enable or block an amendment incorrectly. + +`defaultAmendmentMajorityTime` is `weeks{2}` expressed as `std::chrono::seconds`. The `weeks` alias comes from `xrpl/basics/chrono.h` since the standard library did not provide it at the time (C++20 added it, but this alias predates that). This value seeds the `Config` field `AMENDMENT_MAJORITY_TIME`, which operators can override. An amendment must hold 80% validator support continuously for two weeks before it activates on mainnet. + +## Peer Port + +`DEFAULT_PEER_PORT = 2459` is the IANA-registered port for XRPL peer-to-peer connections. It lives **outside** the `xrpl` namespace, unlike every other constant in this file. This is intentional: networking code that constructs socket addresses often operates in a context where `xrpl::` namespace qualifications are absent, and the port value is generic enough to be treated as a plain system-level constant rather than a protocol abstraction. The `OverlayImpl` peer-discovery code and the `peer_connect` RPC handler both reference it directly as a fallback when no explicit port is given. \ No newline at end of file diff --git a/include/xrpl/protocol/TER.h.ai.json b/include/xrpl/protocol/TER.h.ai.json new file mode 100644 index 0000000000..1ec2b60207 --- /dev/null +++ b/include/xrpl/protocol/TER.h.ai.json @@ -0,0 +1,271 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Trait" + ], + "lineno": 349, + "name": "TERSubset" + }, + { + "args": [ + "FROM" + ], + "lineno": 419, + "name": "CanCvtToNotTEC" + }, + { + "args": [], + "lineno": 423, + "name": "CanCvtToNotTEC" + }, + { + "args": [], + "lineno": 426, + "name": "CanCvtToNotTEC" + }, + { + "args": [], + "lineno": 429, + "name": "CanCvtToNotTEC" + }, + { + "args": [], + "lineno": 432, + "name": "CanCvtToNotTEC" + }, + { + "args": [], + "lineno": 435, + "name": "CanCvtToNotTEC" + }, + { + "args": [ + "FROM" + ], + "lineno": 447, + "name": "CanCvtToTER" + }, + { + "args": [], + "lineno": 451, + "name": "CanCvtToTER" + }, + { + "args": [], + "lineno": 454, + "name": "CanCvtToTER" + }, + { + "args": [], + "lineno": 457, + "name": "CanCvtToTER" + }, + { + "args": [], + "lineno": 460, + "name": "CanCvtToTER" + }, + { + "args": [], + "lineno": 463, + "name": "CanCvtToTER" + }, + { + "args": [], + "lineno": 466, + "name": "CanCvtToTER" + }, + { + "args": [], + "lineno": 469, + "name": "CanCvtToTER" + } + ], + "description": "Defines transaction engine result (TER) codes and related enums, traits, and utility functions for handling transaction results in the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/TER.h", + "functions": [ + { + "args": [ + "v" + ], + "lineno": 312, + "name": "TERtoInt" + }, + { + "args": [ + "v" + ], + "lineno": 317, + "name": "TERtoInt" + }, + { + "args": [ + "v" + ], + "lineno": 322, + "name": "TERtoInt" + }, + { + "args": [ + "v" + ], + "lineno": 327, + "name": "TERtoInt" + }, + { + "args": [ + "v" + ], + "lineno": 332, + "name": "TERtoInt" + }, + { + "args": [ + "v" + ], + "lineno": 337, + "name": "TERtoInt" + }, + { + "args": [ + "v" + ], + "lineno": 342, + "name": "TERtoInt" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 429, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 438, + "name": "operator!=" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 447, + "name": "operator<" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 456, + "name": "operator<=" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 465, + "name": "operator>" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 474, + "name": "operator>=" + }, + { + "args": [ + "x" + ], + "lineno": 527, + "name": "isTelLocal" + }, + { + "args": [ + "x" + ], + "lineno": 533, + "name": "isTemMalformed" + }, + { + "args": [ + "x" + ], + "lineno": 539, + "name": "isTefFailure" + }, + { + "args": [ + "x" + ], + "lineno": 545, + "name": "isTerRetry" + }, + { + "args": [ + "x" + ], + "lineno": 551, + "name": "isTesSuccess" + }, + { + "args": [ + "x" + ], + "lineno": 557, + "name": "isTecClaim" + }, + { + "args": [], + "lineno": 561, + "name": "transResults" + }, + { + "args": [ + "code", + "token", + "text" + ], + "lineno": 565, + "name": "transResultInfo" + }, + { + "args": [ + "code" + ], + "lineno": 569, + "name": "transToken" + }, + { + "args": [ + "code" + ], + "lineno": 573, + "name": "transHuman" + }, + { + "args": [ + "token" + ], + "lineno": 577, + "name": "transCode" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/TER.h.ai.md b/include/xrpl/protocol/TER.h.ai.md new file mode 100644 index 0000000000..23ef4596bb --- /dev/null +++ b/include/xrpl/protocol/TER.h.ai.md @@ -0,0 +1,57 @@ +# `include/xrpl/protocol/TER.h` — Transaction Engine Result Codes + +## Purpose and System Role + +`TER.h` defines the complete taxonomy of transaction result codes used throughout the XRP Ledger transaction processing pipeline. Every transaction that passes through the engine — from initial validation to consensus application — produces a `TER` result. These codes determine whether a transaction is applied to the ledger, forwarded to peers, queued for later, rejected outright, or silently dropped. + +The file solves two related problems simultaneously: it defines the raw codes as enumerations aligned with the external `ripple-binary-codec` encoding (making them part of the wire protocol), and it provides a strongly-typed C++ wrapper that prevents misuse at compile time. + +## The Six Result Categories + +Six enum types partition a signed integer space into semantically distinct bands: + +| Prefix | Range | Meaning | +|--------|-------|---------| +| `tel` | −399..−300 | **Local** error — rejected by this node only, not forwarded, no fee check | +| `tem` | −299..−200 | **Malformed** — transaction is structurally corrupt; cannot succeed in any ledger | +| `tef` | −199..−100 | **Failure** — transaction failed due to current ledger state; no fee charged | +| `ter` | −99..−1 | **Retry** — might succeed after other transactions; may be queued | +| `tes` | 0 | **Success** — the single value `tesSUCCESS` | +| `tec` | 100..255 | **Claim** — the fee is consumed and sequence number spent, but no other effect | + +The ranges are not arbitrary — they are stable across releases because they are encoded into historical ledger metadata and referenced by external libraries like `ripple-binary-codec`. The comment `// DO NOT CHANGE THESE NUMBERS: They appear in ledger meta data` in `TECcodes` makes this constraint explicit. Adding new codes means appending within the range; removing or renumbering codes would corrupt historical ledger interpretation. + +`TECcodes` notably has a comment distinguishing `tecNO_ENTRY` (primary object not found) from `tecOBJECT_NOT_FOUND` (auxiliary object not found), which reflects an emergent naming convention the team is documenting to avoid future inconsistency. + +## The `TERSubset` Template + +The most architecturally significant design in this file is `TERSubset`, a policy-based wrapper around a plain `TERUnderlyingType` (a `typedef` for `int`). The template parameter `Trait` is a class template that, when specialized for a given enum type, inherits from `std::true_type` if that enum is allowed, or from `std::false_type` if it is not. + +Constructors and assignment operators use `std::enable_if_t::value>` to gate which enum values can be implicitly converted in. This means an attempt to assign a `TECcodes` value to a `NotTEC` variable fails at compile time — not at runtime — with no casts required by the caller. + +The file defines two concrete instantiations: + +- **`NotTEC`** — permits `tel`, `tem`, `tef`, `ter`, and `tes`, but explicitly **excludes `TECcodes`**. +- **`TER`** — permits all six categories, including `NotTEC` (allowing widening assignment from a `NotTEC` to a `TER`). + +The `NotTEC` restriction is a security invariant, not just a style choice. The comment in the header explains the attack vector: `preflight` validation runs *before* signature checking. If `preflight` could return a `tec` code, a malicious actor could craft a transaction with a very large fee and get that fee deducted from an account without providing a valid signature — fee theft via an unsigned transaction. Restricting `preflight` return types to `NotTEC` at the type level makes this class of exploit structurally impossible. + +## The `TERtoInt` Overload Set and Comparison Operators + +Rather than using an explicit conversion operator on `TERSubset`, the design provides a friend free function `TERtoInt(TERSubset v)` and overloads of the same name for each of the six raw enum types. This is deliberate: the comment in the header explains that explicit conversion operators on the class would allow silent implicit conversions in contexts like constructor initialization (`Status(TER ter) : code_(ter) {}`), which compiles silently even with `explicit`. A named function forces the conversion to be visible. + +The six comparison operators (`==`, `!=`, `<`, `<=`, `>`, `>=`) are all implemented as free function templates gated by `std::enable_if_t` on whether both operands have a valid `TERtoInt` overload returning `int`. This unified approach means comparisons work across *any combination* of raw enum types and `TERSubset` wrappers without writing a combinatorial set of operator overloads. + +## Boolean Semantics + +`TERSubset::operator bool()` is `explicit` and returns `code_ != tesSUCCESS` — i.e., truthy means "something went wrong." This mirrors conventional error-code idioms. The `isTesSuccess()` free function exploits this: it simply negates the boolean conversion, relying on `tesSUCCESS == 0`. + +## Category Inspection Helpers + +Six `inline bool isXxx(TER x)` functions perform range checks against the numeric boundaries between categories. These functions take a `TER` (not a raw enum), so they work on any code value regardless of how it arrived. `isTecClaim(x)` checks `x >= tecCLAIM`, correctly treating everything from 100 upward as a fee-claim result. + +## Lookup and Serialization Utilities + +`transResults()` in the `.cpp` file returns a static `const` `unordered_map` keyed by `TERUnderlyingType`, mapping each code to a `{token, description}` pair. The `MAKE_ERROR` macro stringifies the enum name via `#code` to avoid manually duplicating the token string, guaranteeing the token in the map matches the enum identifier exactly. The map is function-local static, ensuring thread-safe initialization under C++11 and later. + +`transResultInfo()`, `transToken()`, and `transHuman()` provide lookups from code to string. `transCode()` inverts the mapping — it builds a second `unordered_map` (keyed by token string, lazily initialized as a local static) by inverting the primary map at first call, then returns an `std::optional` to communicate lookup failures without exceptions. \ No newline at end of file diff --git a/include/xrpl/protocol/TxFlags.h.ai.json b/include/xrpl/protocol/TxFlags.h.ai.json new file mode 100644 index 0000000000..23fdbf3f76 --- /dev/null +++ b/include/xrpl/protocol/TxFlags.h.ai.json @@ -0,0 +1,130 @@ +{ + "args": [], + "classes": [], + "description": "Defines transaction flags, masks, and related utilities for the XRPL protocol, including universal and transaction-specific flags, flag masks, and helper functions for flag introspection and mapping. Also defines AccountSet SetFlag/ClearFlag values.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/TxFlags.h", + "functions": [ + { + "args": [], + "lineno": 210, + "name": "getAccountSetFlags" + }, + { + "args": [], + "lineno": 215, + "name": "getOfferCreateFlags" + }, + { + "args": [], + "lineno": 220, + "name": "getPaymentFlags" + }, + { + "args": [], + "lineno": 225, + "name": "getTrustSetFlags" + }, + { + "args": [], + "lineno": 230, + "name": "getEnableAmendmentFlags" + }, + { + "args": [], + "lineno": 235, + "name": "getPaymentChannelClaimFlags" + }, + { + "args": [], + "lineno": 240, + "name": "getNFTokenMintFlags" + }, + { + "args": [], + "lineno": 245, + "name": "getMPTokenIssuanceCreateFlags" + }, + { + "args": [], + "lineno": 250, + "name": "getMPTokenAuthorizeFlags" + }, + { + "args": [], + "lineno": 255, + "name": "getMPTokenIssuanceSetFlags" + }, + { + "args": [], + "lineno": 260, + "name": "getNFTokenCreateOfferFlags" + }, + { + "args": [], + "lineno": 265, + "name": "getAMMDepositFlags" + }, + { + "args": [], + "lineno": 270, + "name": "getAMMWithdrawFlags" + }, + { + "args": [], + "lineno": 275, + "name": "getAMMClawbackFlags" + }, + { + "args": [], + "lineno": 280, + "name": "getXChainModifyBridgeFlags" + }, + { + "args": [], + "lineno": 285, + "name": "getVaultCreateFlags" + }, + { + "args": [], + "lineno": 290, + "name": "getBatchFlags" + }, + { + "args": [], + "lineno": 295, + "name": "getLoanSetFlags" + }, + { + "args": [], + "lineno": 300, + "name": "getLoanPayFlags" + }, + { + "args": [], + "lineno": 305, + "name": "getLoanManageFlags" + }, + { + "args": [], + "lineno": 310, + "name": "getUniversalFlags" + }, + { + "args": [], + "lineno": 320, + "name": "getAllTxFlags" + }, + { + "args": [], + "lineno": 410, + "name": "getAsfFlagMap" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/TxFlags.h.ai.md b/include/xrpl/protocol/TxFlags.h.ai.md new file mode 100644 index 0000000000..1892be49b1 --- /dev/null +++ b/include/xrpl/protocol/TxFlags.h.ai.md @@ -0,0 +1,50 @@ +# `TxFlags.h` — Transaction Flag Constants and Validation Masks + +This header is the canonical, single-source-of-truth definition for every transaction flag in the XRPL protocol. It lives inside `include/xrpl/protocol/` alongside `LedgerFormats.h`, from which it borrows several `lsf*` and `lsmf*` ledger-state-flag values. Any code that validates a transaction's `Flags` field or that needs to enumerate flags for introspection (e.g., the `server_definitions` RPC) includes this header. + +## Protocol Safety Warning + +The file's own documentation delivers an unusually direct caution: flag values are part of the consensus protocol. Altering a constant without special amendment handling will cause a **hard fork** — transactions that were valid on old nodes become invalid on new ones, or vice versa. This explains several defensive patterns throughout the file, including the retention of the deprecated `tfTrustLine` constant and the careful layering of amendment-gated mask variants for NFToken minting. + +## Flag Namespace Layout + +All 32 bits of the `Flags` field (`FlagValue = std::uint32_t`) are partitioned by convention: + +- **Bits 31–24 (high 8 bits):** Universal flags, shared across all transaction types. Currently two are defined: `tfFullyCanonicalSig` (bit 31, now always enforced by the network but retained for backward compatibility) and `tfInnerBatchTxn` (bit 30, marks an inner transaction inside a `Batch`). The OR of these is `tfUniversal`; its complement is `tfUniversalMask`. +- **Bits 23–0 (low 24 bits):** Transaction-specific flags. The same numeric bit can mean entirely different things in an `AccountSet` versus an `OfferCreate`. + +## The X-Macro Engine + +The dominant design pattern in this file is a single master X-macro, `XMACRO`, that lists every transaction type together with its flags. Rather than writing out constants, masks, and accessor functions three times and risking them drifting apart, the file instantiates `XMACRO` three times — once for each concern — using a different set of helper macros. + +**Instantiation 1 — value declarations** (`TO_VALUE`/`NULL_NAME`/`NULL_OUTPUT`/`NULL_MASK_ADJ`): emits `inline constexpr FlagValue tfXxx = 0x...;` for every flag constant. The `TF_FLAG` macro introduces a new constant; `TF_FLAG2` suppresses the declaration, acting as a pure reference to a constant already declared by an earlier transaction (e.g., `tfLPToken` is declared in `AMMDeposit` and *referenced* in `AMMWithdraw` without redeclaration). + +**Instantiation 2 — mask generation** (`TO_MASK`/`VALUE_TO_MASK`/`MASK_ADJ_TO_MASK`): emits one `tfXxxMask` constant per transaction. A mask is the bitwise NOT of all valid flags for that type unioned with the universal flags: `~(tfUniversal | flag1 | flag2 | ...)`. During validation, any transaction whose `Flags` field has a bit set in the mask is rejected — it contains either an unknown flag or one that is illegal for this transaction type. + +**Instantiation 3 — per-type getter functions** (`TO_MAP`/`VALUE_TO_MAP`): emits `inline FlagMap const& getXxxFlags()` for each transaction, returning a `std::map` that names each flag. These are collected by `getAllTxFlags()` into a `FlagMapPairList` (vector of `{txTypeName, FlagMap}` pairs). All getter functions use Meyer's singleton (`static FlagMap const flags = {...}`) to avoid the static initialization order fiasco while remaining zero-overhead on subsequent calls. `getAllTxFlags()` feeds the `server_definitions` RPC endpoint, which clients use to auto-discover the protocol's flag vocabulary at runtime. + +The macro push/pop guard (`#pragma push_macro` / `#pragma pop_macro`) wrapping the entire block protects consuming translation units from having `XMACRO`, `TO_VALUE`, and similar short names leak into their scope, which would otherwise clobber any macros of the same name in headers included later. + +## The `MASK_ADJ` Mechanism + +Each `TRANSACTION(...)` invocation includes a `MASK_ADJ(value)` argument. For nearly all transactions this is `MASK_ADJ(0)` — a no-op. The exception is `Batch`, which specifies `MASK_ADJ(tfInnerBatchTxn)`. Because `tfInnerBatchTxn` is a universal flag (excluded from `tfUniversal`'s complement by default, meaning it would ordinarily be *allowed* on any transaction), `Batch` needs to actively reject it on the outer transaction wrapper. `MASK_ADJ` OR-ORs the specified bits *back into* the otherwise-clear positions of the generated mask, making them illegal for that specific transaction type. + +Two `static_assert`s enforce this invariant at compile time: `tfBatchMask & tfInnerBatchTxn` must equal `tfInnerBatchTxn` (the outer Batch rejects the flag), while `tfPaymentMask & tfInnerBatchTxn` and `tfAccountSetMask & tfInnerBatchTxn` must equal zero (inner transactions may legitimately carry it). The `Batch` transactor itself checks at runtime that every inner transaction *does* have `tfInnerBatchTxn` set, completing the bidirectional enforcement. + +## MPToken and Vault Flags — Ledger-State Mirroring + +`MPTokenIssuanceCreate` flags are intentionally set to the same numeric values as the corresponding ledger-state flags (`lsfMPTCanLock`, `lsfMPTRequireAuth`, etc.) from `LedgerFormats.h`. This mirroring means the issuance transaction's `Flags` field is copied almost verbatim into the created `MPTokenIssuance` object's flags field, eliminating a translation step. The `tfMPTLocked` flag is deliberately *omitted* from the transaction — the comment notes it is not allowed to be set at creation time. + +A second tier of MPToken-related constants uses the `tmf` prefix (transaction mutable flags): `tmfMPTCanMutate*` constants alias the `lsmf*` mutable-flag values from `LedgerFormats.h` and are used to decide which MPToken properties may be updated after issuance. The complementary `tmfMPTokenIssuanceSetMutableMask` and `tmfMPTokenIssuanceCreateMutableMask` follow the same inverted-mask validation pattern. + +## Backward Compatibility Layers + +The deprecated `tfTrustLine` (0x00000004, NFTokenMint) must remain defined for nodes processing historical ledger transactions minted before the `fixRemoveNFTokenAutoTrustLine` amendment. That amendment closed a reserve-exhaustion attack where two accounts could endlessly trade an NFToken, forcing unbounded trust lines onto the issuer. The file preserves three mask variants to accommodate the amendment timeline: `tfNFTokenMintMaskWithoutMutable` (base case), `tfNFTokenMintOldMask` (pre-amendment, allows `tfTrustLine`), and `tfNFTokenMintOldMaskWithMutable` (pre-amendment plus `featureDynamicNFT`). + +## AccountSet Set/Clear Flags + +A second, independent X-macro `ACCOUNTSET_FLAGS` defines the numeric *values* passed via the `SetFlag` and `ClearFlag` fields of `AccountSet` transactions. These are small integers (1–17) rather than bitmasks and are a distinct mechanism from the `Flags` bitmask system. They drive `getAsfFlagMap()`, another Meyer's singleton used by `server_definitions` to expose the `asf*` constants by name alongside the bitflag maps. + +## Additional Composite Masks + +Outside the X-macro, the file defines several convenience constants: `tfMPTPaymentMask` restricts payments involving MPTokens to only `tfPartialPayment`; `tfTrustSetPermissionMask` constrains the subset of `TrustSet` flags available when a transaction is used purely for permission operations; and `tfWithdrawSubTx` / `tfDepositSubTx` combine the mutually exclusive AMM mode flags into bitmasks used to validate that exactly one deposit or withdrawal mode is selected. \ No newline at end of file diff --git a/include/xrpl/protocol/TxFormats.h.ai.json b/include/xrpl/protocol/TxFormats.h.ai.json new file mode 100644 index 0000000000..3ec9775264 --- /dev/null +++ b/include/xrpl/protocol/TxFormats.h.ai.json @@ -0,0 +1,31 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 51, + "name": "TxFormats" + } + ], + "description": "Defines transaction type identifiers (TxType enum) used in the XRPL protocol and declares the TxFormats class for managing known transaction formats.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/TxFormats.h", + "functions": [ + { + "args": [], + "lineno": 61, + "name": "getInstance" + }, + { + "args": [], + "lineno": 64, + "name": "getCommonFields" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/TxFormats.h.ai.md b/include/xrpl/protocol/TxFormats.h.ai.md new file mode 100644 index 0000000000..585e35cfeb --- /dev/null +++ b/include/xrpl/protocol/TxFormats.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/protocol/TxFormats.h` + +## Role in the System + +This header is the authoritative definition point for two foundational protocol constructs: the `TxType` enum that identifies every transaction type in the XRPL binary format, and the `TxFormats` singleton that maps each type to its validated field schema. Together they form the schema registry that the ledger uses to deserialize, validate, and route every signed transaction it processes. + +## The `TxType` Enum + +`TxType` is declared as `enum TxType : std::uint16_t`. Its enumerators are generated almost entirely by the X-macro pattern: the `transactions.macro` file is included after defining `TRANSACTION(tag, value, ...)` to expand as `tag = value,`. This means the enum values (0 for `ttPAYMENT`, 1 for `ttESCROW_CREATE`, 3 for `ttACCOUNT_SET`, and so on) come from a single source of truth shared with the runtime registration code and, optionally, with transactor header inclusion. + +The file's documentation carries an explicit hard-fork warning: because these numeric identifiers are embedded inside **signed** transaction objects, they are immutable protocol surface. A validator that disagrees about what type code 7 means cannot participate in consensus with its peers. This is why deprecated types — `ttNICKNAME_SET` (6), `ttCONTRACT` (9), and `ttSPINAL_TAP` (11) — are not removed from the enum but are instead annotated `[[deprecated]]`. Their slots are tombstoned so no future transaction type can accidentally claim a numeric ID that already exists in historical ledger data, which would cause those old transactions to be misclassified. + +`ttHOOK_SET = 22` is annotated `[[maybe_unused]]`, reflecting its status as a Hooks amendment transaction type present in this codebase but not universally activated across all networks. + +## The X-Macro Expansion Strategy + +The `transactions.macro` file is the canonical list of every transaction type, with each entry carrying the full signature: + +``` +TRANSACTION(tag, value, name, delegable, amendments, privileges, fields) +``` + +The fields include whether the transaction can be *delegated* to another account, which amendment (if any) gates the transaction, a privileges bitfield used by `InvariantCheck`, and the transaction-specific `SOElement` fields. By redefining `TRANSACTION` before each `#include`, the same macro file serves three distinct purposes without duplication: generating the `TxType` enum (in this header), registering `KnownFormats::Item` objects at startup (in `TxFormats.cpp`), and pulling in transactor class headers (guarded by `#if TRANSACTION_INCLUDE`). + +The `UNWRAP(...)` helper in `TxFormats.cpp` is necessary because the field list argument is a brace-enclosed initializer list that would confuse the preprocessor's argument parsing if passed directly. + +## The `TxFormats` Class + +`TxFormats` inherits from `KnownFormats`, a CRTP-style template that manages a registry of `Item` objects. Each `Item` pairs a string name, a `TxType` key, and an `SOTemplate` — the ordered list of `SOElement` descriptors that defines which serialized object fields (SFields) the transaction accepts, whether each is required or optional, and whether amount fields support Multi-Purpose Tokens (MPT). + +Construction happens once through the private `TxFormats()` constructor invoked lazily by `getInstance()`, which returns a `static const` instance. The `static` local guarantees thread-safe initialization under C++11 and later. The class is non-copyable (inherited from `KnownFormats`), enforcing the singleton invariant. + +`getCommonFields()` returns the static list of fields shared by every transaction type regardless of which `TRANSACTION` entry registered it: the required `sfTransactionType`, `sfAccount`, `sfSequence`, `sfFee`, and `sfSigningPubKey`; and a range of optional fields covering flags, tags, memos, multi-signature data, network ID, and the delegate field. These common fields are merged with each transaction's unique fields when `KnownFormats::add()` constructs the `SOTemplate` during startup. + +## Storage in `KnownFormats` + +The base class stores `Item` objects in a `std::forward_list`, a deliberate choice: as a node-based container, it guarantees that inserting new items never invalidates existing pointers. The two lookup indices — `boost::container::flat_map` for name lookups and `boost::container::flat_map` for type lookups — store raw pointers into the list. This is safe precisely because `forward_list` never moves its elements. `flat_map` is chosen over `std::map` for cache-friendly iteration, which matters on hot lookup paths. + +The `add()` method actively guards against duplicate numeric IDs at startup: if a `TxType` value is already registered, it calls `LogicError()`, which causes an immediate process termination. This is the compile-time protection the header's `@todo` note acknowledges the language cannot provide — the safety net is pushed to the first execution of the singleton constructor. + +## Relationship to Validation and Serialization + +The `SOTemplate` produced for each transaction type is the schema consulted during deserialization and validation. When a transaction arrives over the wire, the deserializer looks up its `TransactionType` field value in `TxFormats`, retrieves the associated `SOTemplate`, and uses it to parse and validate the remaining fields — rejecting unknown fields and enforcing the `soeREQUIRED` / `soeOPTIONAL` / `soeDEFAULT` constraints encoded in `SOEStyle`. This makes `TxFormats` a critical chokepoint: every transaction that enters the ledger has its structure validated against a schema registered here. \ No newline at end of file diff --git a/include/xrpl/protocol/TxMeta.h.ai.json b/include/xrpl/protocol/TxMeta.h.ai.json new file mode 100644 index 0000000000..5cab82add6 --- /dev/null +++ b/include/xrpl/protocol/TxMeta.h.ai.json @@ -0,0 +1,136 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "uint256 const& transactionID, std::uint32_t ledger", + "uint256 const& txID, std::uint32_t ledger, Blob const&", + "uint256 const& txID, std::uint32_t ledger, STObject const&" + ], + "lineno": 9, + "name": "TxMeta" + } + ], + "description": "Defines the TxMeta class, which encapsulates metadata for a transaction in the XRPL ledger, including affected nodes, result codes, delivered amounts, and serialization/deserialization utilities.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/TxMeta.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "getTxID" + }, + { + "args": [], + "lineno": 22, + "name": "getLgrSeq" + }, + { + "args": [], + "lineno": 26, + "name": "getResult" + }, + { + "args": [], + "lineno": 30, + "name": "getResultTER" + }, + { + "args": [], + "lineno": 34, + "name": "getIndex" + }, + { + "args": [ + "uint256 const&", + "SField const&", + "std::uint16_t" + ], + "lineno": 38, + "name": "setAffectedNode" + }, + { + "args": [ + "SLE::ref", + "SField const&" + ], + "lineno": 39, + "name": "getAffectedNode" + }, + { + "args": [ + "uint256 const&" + ], + "lineno": 41, + "name": "getAffectedNode" + }, + { + "args": [], + "lineno": 44, + "name": "getAffectedAccounts" + }, + { + "args": [ + "JsonOptions p" + ], + "lineno": 47, + "name": "getJson" + }, + { + "args": [ + "Serializer&", + "TER", + "std::uint32_t" + ], + "lineno": 51, + "name": "addRaw" + }, + { + "args": [], + "lineno": 53, + "name": "getAsObject" + }, + { + "args": [], + "lineno": 54, + "name": "getNodes" + }, + { + "args": [], + "lineno": 57, + "name": "getNodes" + }, + { + "args": [ + "STObject const& obj" + ], + "lineno": 61, + "name": "setAdditionalFields" + }, + { + "args": [], + "lineno": 69, + "name": "getDeliveredAmount" + }, + { + "args": [ + "std::optional const& amount" + ], + "lineno": 73, + "name": "setDeliveredAmount" + }, + { + "args": [ + "std::optional const& id" + ], + "lineno": 77, + "name": "setParentBatchID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/TxMeta.h.ai.md b/include/xrpl/protocol/TxMeta.h.ai.md new file mode 100644 index 0000000000..d6dd674610 --- /dev/null +++ b/include/xrpl/protocol/TxMeta.h.ai.md @@ -0,0 +1,50 @@ +# `TxMeta` — Transaction Metadata for Validated XRPL Transactions + +## Role in the System + +Every transaction included in a closed XRPL ledger carries a `metaData` blob alongside the transaction itself. This blob records the transaction's result code, its ordering index within the ledger, and a detailed changelog of every ledger entry that the transaction created, modified, or deleted. `TxMeta` is the C++ object that owns and manages this metadata throughout the transaction application pipeline — from the moment a transaction begins processing through final serialization into consensus-agreed ledger state. + +The class sits at the boundary between transaction execution (where ledger state is mutated tentatively) and ledger storage (where those mutations become permanent record). Its most important consumer is `ApplyStateTable::apply()`, which constructs a `TxMeta`, populates it with per-node change records, and then finalizes it for storage. + +## Construction + +Three constructors serve three distinct lifecycle points: + +- `TxMeta(txid, ledger)` — builds an empty shell during transaction application. The `result_` field is initialized to the sentinel value `255` and `index_` to `UINT32_MAX`, both signaling "not yet finalized". The `nodes_` array pre-reserves 32 slots to avoid reallocations for typical transaction complexity. + +- `TxMeta(txid, ledger, Blob const&)` — deserializes from raw binary by constructing a `SerialIter` over the blob and parsing it as an `STObject` tagged `sfMetadata`. Used when loading existing metadata from storage. + +- `TxMeta(txid, ledger, STObject const&)` — constructs from an already-parsed object. Used in replay and RPC code paths where the metadata has already been deserialized into the `STObject` type hierarchy. + +## Affected-Node Tracking + +The heart of `TxMeta` is its `STArray nodes_` holding one entry per ledger entry touched by the transaction. Each node entry is an `STObject` whose outer field name (`sfCreatedNode`, `sfModifiedNode`, or `sfDeletedNode`) encodes the change type, and whose inner fields (`sfPreviousFields`, `sfFinalFields`, `sfNewFields`) record the before and after state. + +`setAffectedNode(node_key, field_type, entry_type)` either locates an existing node record by its `sfLedgerIndex` or appends a new one. It always forcibly sets the type, which allows the caller to upgrade or reclassify an entry — for example, if processing discovers that a previously registered modified node was actually deleted. + +`getAffectedNode(SLE::ref, SField&)` is the lazy-creation variant used by `ApplyStateTable` when it wants to attach field-level diff data to a node. It finds the node by key or creates it, copying the ledger-entry type from the live SLE. The overload `getAffectedNode(uint256 const&)` is the strict lookup used after a node is guaranteed to exist; it calls `UNREACHABLE` and throws `std::runtime_error` if the key is absent — a hard contract violation rather than silent failure. + +## Affected Accounts + +`getAffectedAccounts()` returns a `boost::container::flat_set` of every account implicated by the transaction's metadata. The implementation mirrors the behavior of the JavaScript `Meta#getAffectedAccounts` method (noted explicitly in a comment) to keep client libraries consistent. For each affected node it inspects `sfNewFields` (for created nodes) or `sfFinalFields` (for all others), then extracts accounts from: +- `STAccount` fields directly, +- the issuer embedded in `sfLowLimit`, `sfHighLimit`, `sfTakerPays`, and `sfTakerGets` amounts (trust-line and offer objects), +- the issuer encoded in `sfMPTokenIssuanceID` (for the MPToken amendment). + +Using `flat_set` over `std::set` is a deliberate performance choice: the account list is small and built once, so contiguous storage with sorted insertion beats a tree for both construction and lookup. + +## Finalization and Serialization + +`addRaw(Serializer&, TER, index)` is called exactly once to finalize metadata before it enters the ledger. It stores `result_` and `index_`, then **sorts `nodes_` by `sfLedgerIndex`** before serializing. This sort is non-obvious but critical: all validators processing the same transaction must produce byte-for-byte identical metadata for the SHAMap hash to agree. Because affected nodes are accumulated in insertion order (which reflects execution order, not deterministic key order), they must be sorted before serialization. Without this sort, different validators running the same transaction would hash to different metadata blobs and consensus would break. + +`getAsObject()` constructs the full `STObject sfTransactionMetaData`, embedding `sfTransactionResult`, `sfTransactionIndex`, the sorted node array, and optionally `sfDeliveredAmount` and `sfParentBatchID`. This object is both serialized for storage and emitted as JSON via `getJson()`. + +## Optional Fields + +`deliveredAmount_` (`std::optional`) records the actual amount delivered by a payment transaction when it may differ from the `Amount` field — the classic partial-payment distinction. It is set by `ApplyStateTable` after the payment engine resolves path execution, and is omitted from the serialized object when absent. + +`parentBatchID_` (`std::optional`) links a transaction to the Batch transaction that submitted it (the Batch amendment). It is propagated from `ApplyContext`, which receives it from the outermost batch-processing layer, and flows into `getAsObject()` as `sfParentBatchID` when present. + +## Relationship to `ApplyStateTable` + +`TxMeta` is a passive data container; all substantive metadata-building logic lives in `ApplyStateTable::apply()`. That function creates a `TxMeta`, iterates the pending ledger changes (tagged as `insert`, `modify`, or `erase`), classifies each as `sfCreatedNode`/`sfModifiedNode`/`sfDeletedNode`, calls `setAffectedNode` for each, then populates the per-node diff sub-objects by comparing original versus final SLE field values against `SField::sMD_*` metadata flags. `TxMeta` merely stores what `ApplyStateTable` computes — keeping concerns cleanly separated between change detection (in the view layer) and change recording (in the protocol type). \ No newline at end of file diff --git a/include/xrpl/protocol/TxSearched.h.ai.json b/include/xrpl/protocol/TxSearched.h.ai.json new file mode 100644 index 0000000000..b7a72b8755 --- /dev/null +++ b/include/xrpl/protocol/TxSearched.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines the TxSearched enum class within the xrpl namespace to indicate the search status of transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/TxSearched.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/TxSearched.h.ai.md b/include/xrpl/protocol/TxSearched.h.ai.md new file mode 100644 index 0000000000..4f20515e04 --- /dev/null +++ b/include/xrpl/protocol/TxSearched.h.ai.md @@ -0,0 +1,53 @@ +# `TxSearched.h` + +This file defines a single scoped enumeration, `TxSearched`, that communicates the coverage completeness of a transaction search operation across the XRPL node's local ledger history. + +## Why It Exists + +When a client queries a rippled node for a transaction by hash, the node may not have a complete ledger history. This creates a fundamental ambiguity: "transaction not found" could mean the transaction genuinely does not exist, or it could mean the node simply does not have the ledgers that would contain it. The `TxSearched` enum is a formal discriminant that resolves this ambiguity in the return type of every transaction-fetch API that supports ranged searches. + +## The Three States + +```cpp +enum class TxSearched { All, Some, Unknown }; +``` + +- **`All`** — the node searched its entire local history for the requested ledger range and confirmed the transaction is absent. The search was exhaustive. A client receiving this can be confident the transaction was not included in that range. +- **`Some`** — the node attempted the search but its local ledger store is incomplete for the requested range. Some ledgers in the range were missing from the database. Absence of the transaction in the results is not conclusive. +- **`Unknown`** — the search was performed without a ledger range constraint, a deserialization error occurred, or coverage information was otherwise unavailable. The `searched_all` field is suppressed in the JSON response when this state is active. + +## Usage Pattern + +`TxSearched` is the alternative type in a `std::variant` that pairs with the actual transaction result. In `TransactionMaster::fetch()` and `RelationalDatabase::getTransaction()`, the return type is: + +```cpp +std::variant, std::shared_ptr>, TxSearched> +``` + +When a transaction is found, the variant holds the `(Transaction, TxMeta)` pair. When it is not found, the variant holds a `TxSearched` value describing why — essentially encoding both the "not found" signal and the confidence level in a single type-safe value. This forces callers to explicitly handle the coverage-completeness question rather than ignoring it. + +The RPC handler `doTxHelp()` in `Tx.cpp` inspects the variant directly: + +```cpp +if (auto e = std::get_if(&v)) +{ + result.searchedAll = *e; + return {result, rpcTXN_NOT_FOUND}; +} +``` + +And `populateJsonResponse()` surfaces it to clients only when meaningful: + +```cpp +if (error.toErrorCode() == rpcTXN_NOT_FOUND && result.searchedAll != TxSearched::Unknown) +{ + response[jss::searched_all] = (result.searchedAll == TxSearched::All); + error.inject(response); +} +``` + +The `searched_all` field in the JSON response collapses `TxSearched` into a boolean — `true` for `All`, `false` for `Some` — and is omitted entirely when the state is `Unknown`. This means clients can unambiguously distinguish a confirmed miss from an inconclusive one, which is important for wallets and explorers that rely on node APIs to confirm transaction finality. + +## Design Choice: Enum Over Boolean + +Using a three-valued enum rather than `std::optional` makes the `Unknown` case explicit and distinct from both `true` and `false`. It avoids the common pitfall of treating `nullopt` as carrying the same semantics as `false` (not-found-completely), and it allows the database layer in `Node.cpp` and `SQLiteDatabase.cpp` to independently compute and communicate coverage without the caller needing to know how that was determined. The enum is defined in the `protocol` layer rather than the `app` or `rdb` layer precisely because it is part of the observable protocol contract exported to RPC clients. \ No newline at end of file diff --git a/include/xrpl/protocol/UintTypes.h.ai.json b/include/xrpl/protocol/UintTypes.h.ai.json new file mode 100644 index 0000000000..3c254c7ea2 --- /dev/null +++ b/include/xrpl/protocol/UintTypes.h.ai.json @@ -0,0 +1,120 @@ +{ + "args": [ + { + "lineno": 59, + "name": "c" + }, + { + "lineno": 64, + "name": "c" + }, + { + "lineno": 69, + "name": "Currency&" + }, + { + "lineno": 69, + "name": "std::string const&" + }, + { + "lineno": 78, + "name": "std::string const&" + }, + { + "lineno": 83, + "name": "os" + }, + { + "lineno": 83, + "name": "x" + } + ], + "classes": [ + { + "args": [], + "lineno": 9, + "name": "CurrencyTag" + }, + { + "args": [], + "lineno": 15, + "name": "DirectoryTag" + }, + { + "args": [], + "lineno": 21, + "name": "NodeIDTag" + } + ], + "description": "Defines strong typedefs for protocol-specific identifiers (Currency, Directory, NodeID, MPTID, Domain) and related utility functions for the XRPL protocol, including currency conversion and hashing support.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/UintTypes.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "xrpCurrency" + }, + { + "args": [], + "lineno": 48, + "name": "noCurrency" + }, + { + "args": [], + "lineno": 53, + "name": "badCurrency" + }, + { + "args": [ + "c" + ], + "lineno": 58, + "name": "isXRP" + }, + { + "args": [ + "c" + ], + "lineno": 63, + "name": "to_string" + }, + { + "args": [ + "Currency&", + "std::string const&" + ], + "lineno": 68, + "name": "to_currency" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 77, + "name": "to_currency" + }, + { + "args": [ + "os", + "x" + ], + "lineno": 82, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "detail" + }, + { + "lineno": 92, + "name": "std" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/UintTypes.h.ai.md b/include/xrpl/protocol/UintTypes.h.ai.md new file mode 100644 index 0000000000..bc03f6c466 --- /dev/null +++ b/include/xrpl/protocol/UintTypes.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/protocol/UintTypes.h` + +## Role in the System + +`UintTypes.h` is the canonical source for the XRPL protocol's fixed-width integer identifiers. Rather than passing plain `uint160` or `uint256` values throughout the codebase — which would let the compiler silently accept a `NodeID` where a `Currency` was expected — this header creates a family of **strongly typed** wrappers using the tag-parameter mechanism built into `base_uint`. The result is that a `Currency`, a `NodeID`, and an `AccountID` are all 160-bit values at the hardware level but are mutually incompatible at the type level. + +## Strong Typedef Mechanism + +The key pattern is `base_uint`. The `Tag` parameter is an arbitrary type whose sole purpose is to make two otherwise-identical instantiations name distinct types. The tags (`CurrencyTag`, `DirectoryTag`, `NodeIDTag`) live inside `namespace xrpl::detail` and have no members beyond a default constructor; they exist only to make the C++ type system treat them as different. Compare the parallel approach in `AccountID.h`, which uses `AccountIDTag` identically. + +The five named types defined here are: + +| Alias | Bits | Tag | Purpose | +|---|---|---|---| +| `Currency` | 160 | `CurrencyTag` | Identifies a currency (XRP or IOU) | +| `NodeID` | 160 | `NodeIDTag` | Identifies a validator node | +| `Directory` | 256 | `DirectoryTag` | Index into the DEX offer book; last 64 bits encode quality | +| `MPTID` | 192 | *(none)* | MPT Issuance ID: 32-bit sequence + 160-bit account | +| `Domain` | 256 | *(none)* | Generic domain hash | + +`MPTID` and `Domain` intentionally omit a tag, accepting the `void` default. This is a conscious tradeoff: there is no other 192-bit or 256-bit type to confuse them with (unlike the three competing 160-bit types), so the added friction of defining an empty tag class is not justified. + +## Currency Sentinel Values + +Three static singletons define the sentinel currencies used throughout the ledger: + +**`xrpCurrency()`** returns a `Currency` set to all-zero bits (`beast::zero`). XRP is not a token in the IOU sense; this all-zeros encoding is what the binary protocol and all in-memory checks use to mean "this is native XRP." The `isXRP(Currency const&)` predicate is a simple equality test against `beast::zero`. + +**`noCurrency()`** returns a `Currency` with a value of 1. This functions as a null / placeholder value used when no meaningful currency is present but the code still needs a valid `Currency` object to pass around. + +**`badCurrency()`** returns a `Currency` whose byte representation spells out the ASCII characters `"XRP"` in the exact position where a 3-character ISO code would appear — specifically, the hex constant `0x5852500000000000` places 'X', 'R', 'P' at bytes 12–14 of the 20-byte field. This type was explicitly forbidden because users kept trying to use the ISO-style `"XRP"` string to represent native XRP when they should have used the all-zero form. The sentinel exists so the codebase can detect and reject that mistake. + +## Currency Serialization + +The XRPL wire format packs a `Currency` into 20 bytes (160 bits). When a currency is a standard 3-character code it occupies bytes 12–14, with all surrounding bytes set to zero. `to_string()` detects this layout using a bitmask (`sIsoBits`) that covers every byte except positions 12–14; if the masked value is zero and the three characters are all in the allowed character set (`isoCharSet`), the ISO code is returned directly. Otherwise the full 40-character hex representation is returned, which covers arbitrary custom token codes that don't fit the ISO convention. + +`to_currency()` handles the inverse: an empty string or `"XRP"` maps to `beast::zero` (native XRP); a 3-character string maps through the ISO encoding; anything else is parsed as raw hex. The documented legacy caveat — that `to_currency()` accepts and returns `badCurrency()` without error — is preserved intentionally: the comment acknowledges that changing this would require auditing every call site, and the risk of unintentional breakage outweighs the cleanup benefit. + +## Hash Specializations + +The `std::hash` specializations at the bottom of the file follow the same pattern established in `AccountID.h`: they inherit from the `hasher` inner type that `base_uint` provides. This enables `Currency`, `NodeID`, and `Directory` to be used directly as keys in `std::unordered_map` and `std::unordered_set`. The `uint256` specialization is also included here even though `uint256` (which uses `base_uint<256>` with no tag) is not exclusively defined in this file, since it is a frequently used type in the same contexts. + +## Design Observations + +The empty-tag-class idiom is the idiomatic C++ alternative to `enum class` or `STRONG_TYPEDEF` macros for creating distinct fixed-width integer types. It has zero runtime overhead: the tag type contributes no bytes to the layout and no instructions to the generated code. The only cost is the boilerplate of defining a new empty class per distinct type. Given the importance of never mixing a `NodeID` with a `Currency` in protocol code, that boilerplate cost is clearly justified. \ No newline at end of file diff --git a/include/xrpl/protocol/Units.h.ai.json b/include/xrpl/protocol/Units.h.ai.json new file mode 100644 index 0000000000..c8aa865de8 --- /dev/null +++ b/include/xrpl/protocol/Units.h.ai.json @@ -0,0 +1,143 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 23, + "name": "BipsTag" + }, + { + "args": [], + "lineno": 24, + "name": "TenthBipsTag" + }, + { + "args": [ + "UnitTag", + "T" + ], + "lineno": 32, + "name": "ValueUnit" + } + ], + "description": "Defines unit-safe arithmetic types and utilities for XRP Ledger protocol, including tagged value types for drops, fee levels, basis points, and safe arithmetic operations with unit enforcement.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/Units.h", + "functions": [ + { + "args": [ + "amount" + ], + "lineno": 156, + "name": "to_string" + }, + { + "args": [ + "value" + ], + "lineno": 210, + "name": "scalar" + }, + { + "args": [ + "value", + "mul", + "div" + ], + "lineno": 217, + "name": "mulDivU" + }, + { + "args": [ + "value", + "mul", + "div" + ], + "lineno": 282, + "name": "mulDiv" + }, + { + "args": [ + "value", + "mul", + "div" + ], + "lineno": 289, + "name": "mulDiv" + }, + { + "args": [ + "value", + "mul", + "div" + ], + "lineno": 296, + "name": "mulDiv" + }, + { + "args": [ + "value", + "mul", + "div" + ], + "lineno": 304, + "name": "mulDiv" + }, + { + "args": [ + "value", + "mul", + "div" + ], + "lineno": 312, + "name": "mulDiv" + }, + { + "args": [ + "value", + "mul", + "div" + ], + "lineno": 320, + "name": "mulDiv" + }, + { + "args": [ + "s" + ], + "lineno": 328, + "name": "safe_cast" + }, + { + "args": [ + "s" + ], + "lineno": 334, + "name": "safe_cast" + }, + { + "args": [ + "s" + ], + "lineno": 340, + "name": "unsafe_cast" + }, + { + "args": [ + "s" + ], + "lineno": 346, + "name": "unsafe_cast" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "unit" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/Units.h.ai.md b/include/xrpl/protocol/Units.h.ai.md new file mode 100644 index 0000000000..0e96ad79d4 --- /dev/null +++ b/include/xrpl/protocol/Units.h.ai.md @@ -0,0 +1,66 @@ +# `include/xrpl/protocol/Units.h` — Type-Safe Unit Arithmetic for the XRP Ledger + +## Purpose + +`Units.h` solves a pervasive correctness problem in financial protocol code: raw integer types carry no information about what they measure. A `uint64_t` could be drops of XRP, a fee level, a basis-point rate, or a loop counter — and a misuse silently compiles. This file introduces a compile-time unit system that makes such mismatches impossible to write without an explicit cast. + +The design uses the *phantom-type* or *tagged-value* idiom: a single generic template `ValueUnit` wraps any arithmetic `T` and attaches an empty `UnitTag` type. The tag has no runtime cost, but the compiler uses it to reject operations between incompatible units. + +## Unit Tags + +Four tag types live in the nested `unit` namespace: + +- `dropTag` — The smallest divisible unit of XRP. Most balance and fee calculations work in drops. The concrete alias `XRPAmount` (defined elsewhere as `ValueUnit`) is the canonical drop-valued type. +- `feelevelTag` — A dimensionless ratio used by the transaction queue (`TxQ`) to compare relative fee cost across transactions that differ in processing effort. `FeeLevel64` (`ValueUnit`) and `FeeLevelDouble` are the primary aliases. +- `unitlessTag` — A plain scalar wrapped in `ValueUnit`. Used internally by `mulDiv` so that raw integers can participate in the unit-checked arithmetic without introducing new named units. +- `BipsTag` / `TenthBipsTag` — Basis points and tenth-of-a-basis-point values. `Bips16`, `Bips32`, `TenthBips16`, and `TenthBips32` are provided as aliases. + +## The `ValueUnit` Template + +`ValueUnit` privately inherits from several `boost::operators` mixins (`totally_ordered`, `additive`, `equality_comparable`, `dividable`, `modable`, `unit_steppable`). This generates the full set of relational and arithmetic operators from a small number of explicitly defined primitives, avoiding repetition while keeping the operators consistent. + +Arithmetic is carefully unit-typed: + +- **Addition and subtraction** between two `ValueUnit`s of the *same* unit type return a `ValueUnit` of the same unit. Adding a raw scalar to a `ValueUnit` is also supported (it shifts the value by the scalar amount). +- **Multiplication** by a raw scalar preserves the unit (scaling a quantity). Multiplication is made commutative via a `friend operator*`. +- **Division** of two same-unit `ValueUnit`s returns the raw `value_type` (a dimensionless ratio, since drops/drops = 1). Division by a scalar preserves the unit. + +The negation operator contains a `static_assert` that fires at compile time for unsigned `value_type`s — preventing silent integer wrapping when negating something like a `uint64_t`. + +### Implicit Widening Conversion + +A `ValueUnit` can be implicitly constructed from `ValueUnit` if and only if the `SafeToCast` concept is satisfied (same-sign and destination is at least as wide, or destination is wider when signs differ). This mirrors the safe widening rules of `safe_cast.h`: a `FeeLevel` can quietly become a `FeeLevel64`, but the reverse requires an explicit cast. + +### Zero Comparisons via `beast::Zero` + +`ValueUnit` integrates with `beast::Zero` by providing an explicit constructor from `beast::Zero` (sets value to 0) and a `signum()` method. The `beast` zero-comparison machinery then generates all six relational operators against `zero` for free, without constructing a temporary `ValueUnit`. The `explicit operator bool()` also follows — `if (amount)` is false exactly when `value_ == 0`. + +### JSON Serialisation Gate + +`jsonClipped()` converts a `ValueUnit` to a `Json::Value`, clamping to the JSON integer range (`Json::Int` or `Json::UInt`). It is only reachable when the `Usable` concept is satisfied. `Usable` is an explicit whitelist of the known unit tags; new tags do not automatically gain JSON serializability. This prevents accidentally exposing an internal intermediate type through the RPC layer. + +## Concepts Hierarchy + +The file defines a layered set of C++20 concepts: + +- `Valid` — requires `unit_type` and `value_type` nested types (i.e., it is a `ValueUnit`). +- `Usable` — `Valid` plus must be one of the four named unit tags. Used as a guard on `jsonClipped()` and implicit conversions. +- `Compatible` — `Other` is arithmetic and convertible to `VU::value_type`. Enables cross-scalar arithmetic. +- `IntegralValue` / `Integral` — value type is integral. Required for `%=` and for cast operations. +- `CastableValue` — both integral, same unit tag. Required for `safe_cast`/`unsafe_cast` between `ValueUnit`s. + +The `muldiv*` family of concepts (`muldivSource`, `muldivDest`, `muldivSources`, `muldivable`, `muldivCommutable`) specifically constrain which type combinations can appear in `mulDiv` arguments, enforcing that unit tags are consistent. + +## `mulDiv` — Overflow-Safe Scaled Arithmetic + +The free function `mulDiv(value, mul, div)` computes `(value * mul) / div` with no intermediate overflow using `boost::multiprecision::uint128_t` as the intermediate type. It returns `std::optional`, returning `std::nullopt` on overflow or on negative inputs (the latter protected by `XRPL_ASSERT` in addition to the nullopt path). + +Two fast-path shortcuts avoid the 128-bit multiply when `value == div` (result is just `mul`) or when `mul == div` (result is just `value`, after a range check). + +The public overloads handle multiple calling patterns — `ValueUnit`-to-`ValueUnit`, `ValueUnit`-to-`uint64_t`, and `uint64_t`-to-`ValueUnit` — with commutativity handled by reordering arguments rather than by duplicate implementations. Raw `uint64_t` arguments are wrapped in `unit::scalar()` (a `ValueUnit`) so the inner `mulDivU` function only needs to handle the typed case. + +The `muldivCommutable` concept restricts the commuted overload to cases where the `Dest` unit differs from the `Source` unit tags — this is the cross-unit case, e.g., converting a `FeeLevel64` and a base fee in drops to an output in drops. If `Dest` and sources share the same tag, the commuted overload would be ambiguous with the non-commuted one. + +## Cast Extension + +`safe_cast` and `unsafe_cast` (from `safe_cast.h`) are extended in the `xrpl` namespace with overloads for `ValueUnit` types. `safe_cast(src)` where both are `ValueUnit` with the same unit unwraps the source value, applies the scalar `safe_cast` to it, and rewraps it in `Dest`. This allows `safe_cast(someFeeLevel32)` to work seamlessly. The `unsafe_cast` variant asserts at compile time that the cast is *not* trivially safe (preventing misuse as a drop-in replacement for `safe_cast`). \ No newline at end of file diff --git a/include/xrpl/protocol/XChainAttestations.h.ai.json b/include/xrpl/protocol/XChainAttestations.h.ai.json new file mode 100644 index 0000000000..6bb74f88fe --- /dev/null +++ b/include/xrpl/protocol/XChainAttestations.h.ai.json @@ -0,0 +1,220 @@ +{ + "args": [ + { + "lineno": 28, + "name": "attestationSignerAccount_" + }, + { + "lineno": 29, + "name": "publicKey_" + }, + { + "lineno": 30, + "name": "signature_" + }, + { + "lineno": 31, + "name": "sendingAccount_" + }, + { + "lineno": 32, + "name": "sendingAmount_" + }, + { + "lineno": 33, + "name": "rewardAccount_" + }, + { + "lineno": 34, + "name": "wasLockingChainSend_" + }, + { + "lineno": 83, + "name": "claimID_" + }, + { + "lineno": 84, + "name": "dst_" + }, + { + "lineno": 90, + "name": "bridge" + }, + { + "lineno": 92, + "name": "secretKey_" + }, + { + "lineno": 139, + "name": "createCount_" + }, + { + "lineno": 141, + "name": "toCreate_" + }, + { + "lineno": 142, + "name": "rewardAmount_" + } + ], + "classes": [ + { + "args": [ + "AccountID attestationSignerAccount_", + "PublicKey const& publicKey_", + "Buffer signature_", + "AccountID const& sendingAccount_", + "STAmount const& sendingAmount_", + "AccountID const& rewardAccount_", + "bool wasLockingChainSend_" + ], + "lineno": 18, + "name": "AttestationBase" + }, + { + "args": [ + "AccountID attestationSignerAccount_", + "PublicKey const& publicKey_", + "Buffer signature_", + "AccountID const& sendingAccount_", + "STAmount const& sendingAmount_", + "AccountID const& rewardAccount_", + "bool wasLockingChainSend_", + "std::uint64_t claimID_", + "std::optional const& dst_" + ], + "lineno": 74, + "name": "AttestationClaim" + }, + { + "args": [], + "lineno": 119, + "name": "CmpByClaimID" + }, + { + "args": [ + "STObject const& o" + ], + "lineno": 128, + "name": "AttestationCreateAccount" + }, + { + "args": [], + "lineno": 180, + "name": "CmpByCreateCount" + }, + { + "args": [ + "AccountID const& keyAccount_", + "PublicKey const& publicKey_", + "STAmount const& amount_", + "AccountID const& rewardAccount_", + "bool wasLockingChainSend_", + "std::optional const& dst" + ], + "lineno": 200, + "name": "XChainClaimAttestation" + }, + { + "args": [ + "AccountID const& keyAccount_", + "PublicKey const& publicKey_", + "STAmount const& amount_", + "STAmount const& rewardAmount_", + "AccountID const& rewardAccount_", + "bool wasLockingChainSend_", + "AccountID const& dst_" + ], + "lineno": 308, + "name": "XChainCreateAccountAttestation" + }, + { + "args": [ + "AttCollection&& sigs" + ], + "lineno": 349, + "name": "XChainAttestationsBase" + }, + { + "args": [], + "lineno": 400, + "name": "XChainClaimAttestations" + }, + { + "args": [], + "lineno": 406, + "name": "XChainCreateAccountAttestations" + } + ], + "description": "Defines data structures and logic for cross-chain attestations in the XRPL protocol, including attestation types, comparison, serialization, and collections for claims and account creation events.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/XChainAttestations.h", + "functions": [ + { + "args": [ + "AttestationClaim const& lhs", + "AttestationClaim const& rhs" + ], + "lineno": 120, + "name": "operator()" + }, + { + "args": [ + "AttestationCreateAccount const& lhs", + "AttestationCreateAccount const& rhs" + ], + "lineno": 181, + "name": "operator()" + }, + { + "args": [ + "AttestationClaim const& lhs", + "AttestationClaim const& rhs" + ], + "lineno": 157, + "name": "operator==" + }, + { + "args": [ + "AttestationCreateAccount const& lhs", + "AttestationCreateAccount const& rhs" + ], + "lineno": 217, + "name": "operator==" + }, + { + "args": [ + "XChainClaimAttestation const& lhs", + "XChainClaimAttestation const& rhs" + ], + "lineno": 295, + "name": "operator==" + }, + { + "args": [ + "XChainCreateAccountAttestation const& lhs", + "XChainCreateAccountAttestation const& rhs" + ], + "lineno": 340, + "name": "operator==" + }, + { + "args": [ + "XChainAttestationsBase const& lhs", + "XChainAttestationsBase const& rhs" + ], + "lineno": 357, + "name": "operator==" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 15, + "name": "Attestations" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/XChainAttestations.h.ai.md b/include/xrpl/protocol/XChainAttestations.h.ai.md new file mode 100644 index 0000000000..0645df40e6 --- /dev/null +++ b/include/xrpl/protocol/XChainAttestations.h.ai.md @@ -0,0 +1,45 @@ +# XChainAttestations.h + +## Purpose and Context + +This file is the type-system foundation for XRPL's cross-chain bridge protocol. When assets move between a locking chain and an issuing chain, a quorum of off-chain *witness servers* must independently attest to each event before the destination chain will act. This header defines the full type hierarchy for those attestations: how they are constructed, signed, serialized, compared, and stored. + +There are two distinct cross-chain event kinds, each with its own attestation type: a regular transfer (identified by a monotonic `claimID`) and an account-creation transfer (identified by a `createCount` sequence). The design cleanly separates the "full" attestation objects used during transaction submission from the "stored" attestation objects that live in on-chain ledger entries. + +## Two Namespaces, Two Representations + +The header uses two levels deliberately. Inside `xrpl::Attestations` live `AttestationClaim` and `AttestationCreateAccount`, both inheriting from `AttestationBase`. These are the *full* attestations: they carry the raw cryptographic signature `Buffer`, the `sendingAccount` that originated the event, and all event details. A witness server constructs one of these, signs it, and submits it to the destination chain. + +Directly in the `xrpl` namespace are `XChainClaimAttestation` and `XChainCreateAccountAttestation`. These are the *stored* attestations that get placed into ledger objects like `XChainOwnedClaimID`. They shed the raw signature bytes and the sending account, retaining only the `keyAccount` (the signer's account ID), `publicKey`, amount, reward fields, and direction flag. The `TSignedAttestation` typedef on each links it back to its full counterpart, and a converting constructor accepts the full type, extracting what the ledger needs to keep. + +## Signature Verification and Message Construction + +`AttestationBase::verify()` calls the pure virtual `message(STXChainBridge const&)` to reconstruct the exact byte sequence that was signed, then calls `xrpl::verify()` against the stored `publicKey` and `signature`. Making `message()` pure virtual is the right call: `AttestationClaim` and `AttestationCreateAccount` sign different fields, and forcing each subclass to supply its own byte construction prevents the base class from ever silently producing an incorrect or incomplete message. + +Both concrete types also expose a `static` overload of `message()` taking explicit field values. This allows callers to verify a signature from raw data without first constructing an attestation object — useful for validation paths that receive untrusted input and want to check the signature before accepting any of it. + +The static `message()` implementations serialize their fields into a `STObject` using canonical SField ordering before calling `Serializer`. The comment in the .cpp is direct: this ordering exists to make Python serializers easier to write. Witness servers in the ecosystem are not necessarily C++ processes, so the message format must be deterministic and independently reproducible in other languages. + +## Constructor Overloads: Parsing vs. Signing + +Both `AttestationClaim` and `AttestationCreateAccount` offer two "full" constructors beyond the deserialization ones. One takes a pre-computed `Buffer signature_` for attestations arriving over the network. The other takes a `SecretKey const& secretKey_` and computes the signature inline by calling `message()` and `sign()`. The signing constructor is primarily for test harnesses and the witness-server implementation itself; the non-signing constructor is for the validation path on the destination chain that receives attestations it must verify. + +The signing constructor first delegates to the no-signature constructor (passing an empty `Buffer{}`), then overwrites the `signature` field immediately after construction. This avoids code duplication while ensuring the object is valid before signing. + +## Equality and Event Matching + +There is a meaningful distinction between `operator==` and `sameEvent()`. Full equality (`operator==`) checks every field including signer identity and raw signature bytes — two attestations are equal only if they came from the same witness signing the same data. `sameEvent()` checks only the event-describing fields (sending account, amount, direction, and the type-specific ID), ignoring who signed. This distinction is essential for quorum logic: the system needs to know whether two *different* signers attested to the *same* event to count towards consensus. + +The `AttestationMatch` enum adds a third dimension for the stored attestations. `match()` returns `nonDstMismatch` if core amount or direction fields differ, `matchExceptDst` if everything matches except the optional destination address, or `match` for full agreement. The `dst` field on claim attestations is `std::optional`, reflecting that the destination on the receiving chain may or may not be specified. Witnesses can attest to the same transfer but with different `dst` opinions, so the code must distinguish "everything matches" from "same transfer, different destination". + +## The `XChainAttestationsBase` Container + +The template `XChainAttestationsBase` wraps a `std::vector` and adds serialization to/from `STArray` and `Json::Value`. The `maxAttestations = 256` cap is enforced at both `STArray` and JSON parse paths; without this, an attacker could craft an oversized array to allocate memory and slow consensus processing. The value is deliberately far above practical quorum sizes. + +The destructor is `protected`, not `public`, to prevent slicing: neither `XChainClaimAttestations` nor `XChainCreateAccountAttestations` add virtual methods, so accidental deletion through a base pointer would silently be wrong. The two concrete classes are purely thin wrappers that inherit all constructors via `using TBase::TBase`, existing only to provide distinct named types for the type system. + +`CmpByClaimID` and `CmpByCreateCount` are comparator structs for sorting attestation collections by their sequencing key, enabling ordered iteration and binary search when collecting a quorum across multiple submitted attestations. + +## Relationship to Other Files + +`STXChainBridge` (from `STXChainBridge.h`) is the bridge descriptor — it identifies the locking and issuing chain door accounts and asset types. Every `message()` call embeds the full bridge description in the signed payload, which means attestations are cryptographically bound to a specific bridge instance; a valid signature for one bridge cannot be replayed on another. The `wasLockingChainSend` boolean threads through from `STXChainBridge::srcChain()` / `dstChain()` helpers, keeping directionality consistent across the system. \ No newline at end of file diff --git a/include/xrpl/protocol/XRPAmount.h.ai.json b/include/xrpl/protocol/XRPAmount.h.ai.json new file mode 100644 index 0000000000..fab4e0c43e --- /dev/null +++ b/include/xrpl/protocol/XRPAmount.h.ai.json @@ -0,0 +1,51 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "XRPAmount const& other", + "Number const& x", + "beast::Zero", + "value_type drops" + ], + "lineno": 13, + "name": "XRPAmount" + } + ], + "description": "Defines the XRPAmount class for representing and manipulating XRP amounts in drops, including arithmetic operations, conversions, and utility functions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/XRPAmount.h", + "functions": [ + { + "args": [ + "os", + "q" + ], + "lineno": 143, + "name": "operator<<" + }, + { + "args": [ + "amount" + ], + "lineno": 149, + "name": "to_string" + }, + { + "args": [ + "amt", + "num", + "den", + "roundUp" + ], + "lineno": 154, + "name": "mulRatio" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/XRPAmount.h.ai.md b/include/xrpl/protocol/XRPAmount.h.ai.md new file mode 100644 index 0000000000..6e462bee90 --- /dev/null +++ b/include/xrpl/protocol/XRPAmount.h.ai.md @@ -0,0 +1,75 @@ +# `XRPAmount.h` — Type-Safe Representation of XRP in Drops + +## Role in the System + +`XRPAmount` is the canonical type for representing XRP quantities throughout the XRPL C++ codebase. All XRP values are stored and computed in *drops*, where one XRP equals exactly 1,000,000 drops — a fixed-point decimal avoiding floating-point imprecision for ledger-critical arithmetic. This header defines the class itself along with `DROPS_PER_XRP`, the `mulRatio()` helper, and stream I/O utilities. + +The class sits at the center of a family of amount types — `IOUAmount`, `MPTAmount`, and the polymorphic `STAmount` — that together represent every asset class the XRPL supports. Where `IOUAmount` models issued-currency amounts with mantissa/exponent encoding for wide dynamic range, `XRPAmount` models XRP's native integer arithmetic directly. It integrates with `AmountConversions.h`'s `toSTAmount()` and `toAmount()` families, which serve as bridges among all three representations. + +## Design: Minimal Primitives, Derived Operators + +The class inherits privately from four `boost::operators` mixins: + +```cpp +class XRPAmount : private boost::totally_ordered, + private boost::additive, + private boost::equality_comparable, + private boost::additive +``` + +This is a classic CRTP operator-generation pattern. By implementing only `operator==`, `operator<`, `operator+=`, and `operator-=`, the class gets `!=`, `<=`, `>`, `>=`, `+`, and `-` for free. The two mixed `std::int64_t` variants allow direct comparison and arithmetic with raw integer literals — useful for fee and reserve computations in calling code. Private inheritance prevents these mixin types from appearing in the public interface. + +## Unit Type Contract + +`XRPAmount` declares: +```cpp +using unit_type = unit::dropTag; +using value_type = std::int64_t; +``` + +This exposes the same two-member interface expected by the `unit::Valid` and `unit::Usable` concepts defined in `Units.h`. Those concepts gate access to generic functions like `mulDiv()` and `jsonClipped()`. Despite sharing the concept interface, `XRPAmount` is *not* a specialization of the `ValueUnit` template — it predates or intentionally diverges from that template for reasons of API clarity (it names its accessor `drops()` rather than `value()`) and for the richer XRP-specific functionality (`mulRatio()`, `decimalXRP()`, `dropsAs()`). + +## API Walkthrough + +**Construction** is deliberately explicit from raw integers (`explicit XRPAmount(value_type drops)`) to prevent accidental silent coercions. Two special-purpose constructors exist: + +- `XRPAmount(beast::Zero)` — allows `XRPAmount x = beast::zero` in generic contexts where a zero sentinel is passed without knowing the concrete amount type. +- `XRPAmount(Number const& x)` — converts from the `Number` type (the codebase's high-precision arithmetic wrapper) using round-to-nearest-even, which is called out explicitly in a comment. This rounding mode matters for computed fees and reserves that may not land exactly on integer drop boundaries. + +**Accessing the value** is intentionally split across two names: +- `drops()` — the semantically correct name; callers should use this. +- `value()` — an escape hatch for templated code that treats `XRPAmount` as a generic `unit::Valid` type, with a comment warning against its use elsewhere. + +**`dropsAs()`** is a safe narrowing conversion that returns `std::optional`. It guards against both overflow (value exceeds `Dest::max`) and sign violations (negative drops into an unsigned type). This matters because JSON serialization uses 32-bit integers, and some internal fee fields use smaller types. The overload `dropsAs(defaultValue)` provides a fallback for contexts that can tolerate a clipped value. + +**`jsonClipped()`** takes a more direct approach: it saturates to `Json::Int` bounds rather than returning an optional. The comment is candid — this is only valid in contexts where the value is never expected to overflow 32 bits, specifically fees and reserves. It enforces the compile-time invariant that `value_type` is a signed integral type via `static_assert`. + +**`signum()`** returns -1, 0, or +1, following the mathematical signum convention. `AmountConversions.h` uses it when converting `XRPAmount` to `STAmount`, checking `signum() < 0` to extract the sign bit before passing the magnitude as an unsigned value. + +**`operator Number()`** is an *implicit* conversion to `Number`, deliberately non-explicit. This allows `XRPAmount` values to participate transparently in `Number`-based arithmetic expressions used throughout the payment engine. + +## `mulRatio()`: Safe Fixed-Ratio Scaling + +The free function `mulRatio()` scales an `XRPAmount` by a rational factor `num/den`: + +```cpp +XRPAmount mulRatio(XRPAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp) +``` + +The critical design choice is using `boost::multiprecision::int128_t` for the intermediate product. A 64-bit amount multiplied by a 32-bit numerator can produce up to 96 bits of result before division brings it back into range. Without the wider intermediate type, overflow would silently corrupt fee calculations. The function then applies sign-aware rounding: for positive amounts with a nonzero remainder, `roundUp=true` increments the result; for negative amounts, `roundUp=false` decrements (toward more negative), maintaining symmetric semantics. Overflow after division throws `std::overflow_error`. Division by zero throws `std::runtime_error`. + +## `DROPS_PER_XRP` and `decimalXRP()` + +The constant `DROPS_PER_XRP{1'000'000}` is declared as `constexpr XRPAmount` rather than a plain integer, which is a deliberate choice: it makes the constant participate in the type system and prevents inadvertently mixing it with unrelated integer values. `decimalXRP()` is defined *after* this constant precisely because it references `DROPS_PER_XRP.drops()` — a subtle declaration ordering dependency in an otherwise header-only file. + +## Relationship to `Fees` + +The `Fees` struct in `Fees.h` is the primary production consumer of `XRPAmount`. It stores `base`, `reserve`, and `increment` as `XRPAmount` fields and implements `accountReserve(ownerCount)` as: +```cpp +return reserve + ownerCount * increment; +``` +This works because `XRPAmount` overloads `operator*(value_type const& rhs)`, allowing a `std::size_t` owner count to scale the increment naturally, and the result adds to the reserve via the Boost-derived `operator+`. + +## Invariants and Failure Modes + +The class imposes no internal validity constraints — negative drops are representable and used (e.g., for internal calculations). The burden of ensuring valid ledger amounts falls on calling code. `mulRatio()` is the only function that throws, doing so on division by zero or post-division overflow. The `dropsAs()` family handles range violations via `std::optional` rather than exceptions, pushing the decision of how to handle out-of-range values to callers. \ No newline at end of file diff --git a/include/xrpl/protocol/detail/STVar.h.ai.json b/include/xrpl/protocol/detail/STVar.h.ai.json new file mode 100644 index 0000000000..5947a7d9a8 --- /dev/null +++ b/include/xrpl/protocol/detail/STVar.h.ai.json @@ -0,0 +1,280 @@ +{ + "args": [ + { + "lineno": 39, + "name": "other" + }, + { + "lineno": 40, + "name": "other" + }, + { + "lineno": 41, + "name": "rhs" + }, + { + "lineno": 43, + "name": "rhs" + }, + { + "lineno": 45, + "name": "t" + }, + { + "lineno": 50, + "name": "t" + }, + { + "lineno": 54, + "name": "name" + }, + { + "lineno": 55, + "name": "name" + }, + { + "lineno": 56, + "name": "sit" + }, + { + "lineno": 56, + "name": "name" + }, + { + "lineno": 56, + "name": "depth" + }, + { + "lineno": 87, + "name": "T" + }, + { + "lineno": 87, + "name": "args" + }, + { + "lineno": 97, + "name": "id" + }, + { + "lineno": 97, + "name": "depth" + }, + { + "lineno": 97, + "name": "arg" + }, + { + "lineno": 114, + "name": "lhs" + }, + { + "lineno": 114, + "name": "rhs" + }, + { + "lineno": 119, + "name": "lhs" + }, + { + "lineno": 119, + "name": "rhs" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "defaultObject_t" + }, + { + "args": [], + "lineno": 12, + "name": "nonPresentObject_t" + }, + { + "args": [], + "lineno": 23, + "name": "STVar" + } + ], + "description": "Defines the STVar class and related utilities for holding and managing serialized XRPL protocol objects with small-object optimization.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/detail/STVar.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "~STVar" + }, + { + "args": [ + "STVar const& other" + ], + "lineno": 39, + "name": "STVar" + }, + { + "args": [ + "STVar&& other" + ], + "lineno": 40, + "name": "STVar" + }, + { + "args": [ + "STVar const& rhs" + ], + "lineno": 41, + "name": "operator=" + }, + { + "args": [ + "STVar&& rhs" + ], + "lineno": 43, + "name": "operator=" + }, + { + "args": [ + "STBase&& t" + ], + "lineno": 45, + "name": "STVar" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 50, + "name": "STVar" + }, + { + "args": [ + "defaultObject_t", + "SField const& name" + ], + "lineno": 54, + "name": "STVar" + }, + { + "args": [ + "nonPresentObject_t", + "SField const& name" + ], + "lineno": 55, + "name": "STVar" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name", + "int depth" + ], + "lineno": 56, + "name": "STVar" + }, + { + "args": [], + "lineno": 58, + "name": "get" + }, + { + "args": [], + "lineno": 61, + "name": "operator*" + }, + { + "args": [], + "lineno": 64, + "name": "operator->" + }, + { + "args": [], + "lineno": 67, + "name": "get" + }, + { + "args": [], + "lineno": 70, + "name": "operator*" + }, + { + "args": [], + "lineno": 73, + "name": "operator->" + }, + { + "args": [], + "lineno": 80, + "name": "STVar" + }, + { + "args": [ + "SerializedTypeID id", + "SField const& name" + ], + "lineno": 82, + "name": "STVar" + }, + { + "args": [], + "lineno": 84, + "name": "destroy" + }, + { + "args": [ + "Args&&... args" + ], + "lineno": 87, + "name": "construct" + }, + { + "args": [ + "SerializedTypeID id", + "int depth", + "Args&&... arg" + ], + "lineno": 97, + "name": "constructST" + }, + { + "args": [], + "lineno": 102, + "name": "on_heap" + }, + { + "args": [ + "Args&&... args" + ], + "lineno": 108, + "name": "make_stvar" + }, + { + "args": [ + "STVar const& lhs", + "STVar const& rhs" + ], + "lineno": 114, + "name": "operator==" + }, + { + "args": [ + "STVar const& lhs", + "STVar const& rhs" + ], + "lineno": 119, + "name": "operator!=" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/detail/STVar.h.ai.md b/include/xrpl/protocol/detail/STVar.h.ai.md new file mode 100644 index 0000000000..3662f60e19 --- /dev/null +++ b/include/xrpl/protocol/detail/STVar.h.ai.md @@ -0,0 +1,47 @@ +# `include/xrpl/protocol/detail/STVar.h` + +## Role and Purpose + +`STVar` is the type-erased storage primitive that underlies the entire XRPL serialized-type system. Every field inside an `STObject` — which represents a transaction, ledger entry, or any structured protocol object — is stored as a `detail::STVar`. Because XRPL fields can be any one of roughly two dozen concrete types (integers of various widths, amounts, paths, blobs, nested objects, arrays, and more), the system needs a uniform container capable of holding any of them by value. `STVar` solves this problem by combining type erasure through the `STBase` polymorphic base class with a small-object optimization (SOO) that avoids heap allocation for most common field types. + +The class lives in `xrpl::detail` to signal that it is an implementation detail, not a public API surface, even though it is widely used internally by `STObject`, `STArray`, and their relatives. + +## Small-Object Optimization + +The core design tension `STVar` resolves is: how do you store an arbitrary polymorphic type by value without a heap allocation on every field access? The answer is an inline aligned-storage buffer of `max_size = 72` bytes, held as `std::aligned_storage<72>::type d_`. When a concrete ST type fits within 72 bytes, `construct()` placement-news it directly into `d_`. When it is larger, `construct()` falls back to a regular `new`. The `on_heap()` predicate distinguishes the two cases by comparing `p_` against `&d_` — if the pointer doesn't point into the local buffer, the object is on the heap. + +This threshold matters because the most common field types — `STUInt8`, `STUInt16`, `STUInt32`, `STUInt64`, `STAmount`, `STAccount`, `STUInt256`, and others — all fit within 72 bytes. Only large composite types like `STPathSet` or `STObject`/`STArray` themselves overflow onto the heap. A typical XRPL transaction has dozens of fields, so avoiding heap allocation for scalar fields has a meaningful cumulative effect on performance. + +## Copy and Move Semantics + +Because `STVar` holds a polymorphic object, copies and moves cannot be spelled as simple `memcpy` or `std::move`. The `STBase` class exposes two virtual methods — `copy(n, buf)` and `move(n, buf)` — that allow the concrete type to place-new a copy or move-construct a copy of itself into an external buffer if it fits within `n` bytes, or heap-allocate otherwise. `STVar` is a `friend class` of `STBase`, granting it exclusive access to these private virtual methods. + +The move constructor has an important asymmetry: if the source object is on the heap, `STVar` simply steals the pointer and nulls out the source (a zero-copy pointer transfer). If the source object is in the source's inline buffer, it must call `move()` to relocate it into the destination's buffer, because the source's `d_` address is not valid after the source object is destroyed. The copy and move assignment operators follow the same logic with an additional `destroy()` call first. + +`destroy()` is equally careful: for in-buffer objects it calls the destructor explicitly (`p_->~STBase()`); for heap objects it uses `delete`. This is the canonical pattern for manual SOO lifetime management. + +## Construction Modes and Dispatch + +`STVar` exposes three domain-specific constructors beyond copy/move: + +- `STVar(defaultObject_t, SField const& name)` — constructs a default-valued instance of the type indicated by `name.fieldType`. +- `STVar(nonPresentObject_t, SField const& name)` — constructs an `STBase` sentinel representing an absent optional field (`STI_NOTPRESENT`). +- `STVar(SerialIter& sit, SField const& name, int depth)` — deserializes a field directly from a binary stream. + +Both `defaultObject_t` and `nonPresentObject_t` are empty tag structs with explicit constructors, a common disambiguation idiom that prevents accidental implicit conversions. The global singletons `defaultObject` and `nonPresentObject` are the intended call-site tokens. + +All three routes converge on `constructST(SerializedTypeID, depth, args...)`, which is the central dispatch switch. It maps every `SerializedTypeID` enum value to the appropriate concrete template instantiation of `construct()`. The `ValidConstructSTArgs` concept enforces at compile time that `args` are either `(SField)` or `(SerialIter, SField)`, preventing misuse through template arguments alone. + +For `STObject` and `STArray` — which can contain nested `STVar` instances — the deserialization path passes `depth` through to their own constructors. The constructor immediately throws `std::runtime_error` if `depth > 10`, capping the recursion depth and guarding against malformed or malicious serialized data that encodes deeply nested structures. + +## `make_stvar()` Factory + +The free function `make_stvar(args...)` bypasses the type-ID dispatch and constructs a concrete type directly. It is used in well-typed contexts where the caller already knows the exact `STBase` subclass, avoiding the overhead of the switch statement and the runtime field-type resolution. + +## Equality + +`operator==` and `operator!=` delegate to `STBase::isEquivalent()`, the virtual comparison method. This compares field values but notably not field names, consistent with the semantic that two fields with the same value but different names (e.g., a source amount and a destination amount of equal magnitude) are considered equivalent at the `STVar` level. + +## Relationship to `STObject` + +`STObject` stores its fields as `std::vector`, making `STVar`'s value semantics — particularly its copy and move constructors — critical for vector resizing and field manipulation. The `STObject::Transform` functor unwraps `STVar` to `STBase` references for iterator adapters, exposing the field sequence through a pointer-stable view. The interaction between `STObject`'s field vector and `STVar`'s SOO means most field accesses into a transaction avoid any heap activity beyond what the transaction's own vector allocation already implies. \ No newline at end of file diff --git a/include/xrpl/protocol/detail/b58_utils.h.ai.json b/include/xrpl/protocol/detail/b58_utils.h.ai.json new file mode 100644 index 0000000000..d0e2753e26 --- /dev/null +++ b/include/xrpl/protocol/detail/b58_utils.h.ai.json @@ -0,0 +1,84 @@ +{ + "args": [ + { + "lineno": 13, + "name": "T" + } + ], + "classes": [], + "description": "This file provides optimized low-level arithmetic utilities for big integer operations, specifically for base58 encoding/decoding, including division, multiplication, addition, and conversion routines. It is part of the xrpl (XRP Ledger) codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/detail/b58_utils.h", + "functions": [ + { + "args": [ + "a", + "b" + ], + "lineno": 18, + "name": "div_rem" + }, + { + "args": [ + "a", + "b", + "carry" + ], + "lineno": 25, + "name": "carrying_mul" + }, + { + "args": [ + "a", + "b" + ], + "lineno": 32, + "name": "carrying_add" + }, + { + "args": [ + "a", + "b" + ], + "lineno": 41, + "name": "inplace_bigint_add" + }, + { + "args": [ + "a", + "b" + ], + "lineno": 62, + "name": "inplace_bigint_mul" + }, + { + "args": [ + "numerator", + "divisor" + ], + "lineno": 82, + "name": "inplace_bigint_div_rem" + }, + { + "args": [ + "input" + ], + "lineno": 120, + "name": "b58_10_to_b58_be" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 15, + "name": "b58_fast" + }, + { + "lineno": 16, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/detail/b58_utils.h.ai.md b/include/xrpl/protocol/detail/b58_utils.h.ai.md new file mode 100644 index 0000000000..e013640b4f --- /dev/null +++ b/include/xrpl/protocol/detail/b58_utils.h.ai.md @@ -0,0 +1,46 @@ +# `include/xrpl/protocol/detail/b58_utils.h` + +## Role in the System + +This file is the arithmetic foundation for XRPL's fast base58 codec. It lives in the `xrpl::b58_fast::detail` namespace and provides the multi-precision integer operations that make the optimized encoding and decoding path 10–15× faster than the reference Bitcoin-derived algorithm in `b58_ref`. + +The fundamental speed trick is explained at length in `tokens.cpp`: instead of working digit-by-digit between base-58 and base-256, the fast path uses base 58¹⁰ as an intermediate representation. Because 58¹⁰ = 430,804,206,899,405,824 — just under 2⁵⁹ — one coefficient of base 58¹⁰ fits comfortably in a `uint64_t`. Multi-precision arithmetic then operates on far fewer, much larger "digits", slashing the number of inner-loop iterations. The utilities here implement precisely the arithmetic needed: scalar multiply/add for decoding (base 58 → base 58¹⁰ → base 2⁶⁴) and divide/mod for encoding (base 2⁶⁴ → base 58¹⁰ → base 58). + +## Platform Guard + +The entire `b58_fast::detail` namespace is wrapped in `#ifndef _MSC_VER`. Every function in the namespace relies on GCC/Clang's `unsigned __int128` extension to perform 128-bit intermediate arithmetic without overflow. MSVC does not expose this type, so the fast path is silently absent on Windows builds; `tokens.cpp` falls back to the reference implementation in that case. + +## The `Result` Alias + +At the outer `xrpl` namespace scope (not gated by the platform check), the file defines: + +```cpp +template +using Result = boost::outcome_v2::result; +``` + +This is the codec's idiomatic return type throughout `tokens.cpp`, pairing a success value with a `std::error_code` drawn from the `TokenCodecErrc` enum (defined in `token_errors.h`). Using `boost::outcome` rather than exceptions keeps the codec allocation-free and gives callers fine-grained error inspection without the cost of try/catch overhead. + +## Primitive Arithmetic Helpers + +`div_rem(a, b)` is a trivial wrapper that returns `{a/b, a%b}` as a tuple. The comment is telling: the compiler optimizes a single C++ `div_rem` call into one hardware divide instruction (which produces both quotient and remainder), avoiding two separate divisions. + +`carrying_mul(a, b, carry)` multiplies two `uint64_t` values and adds a carry term using a `unsigned __int128` intermediate, then splits the 128-bit result into the low 64 bits (the new coefficient) and the high 64 bits (the new carry). This is exactly the operation hardware long-multiplication performs in each step, and GCC fuses the `__int128` arithmetic into a single `mulq` instruction. + +`carrying_add(a, b)` follows the same pattern for addition, also using `__int128` to detect overflow into the 65th bit, which becomes the carry propagated upward through the big-integer limbs. + +## Multi-Precision Big Integer Operations + +Big integers are represented as `std::span` with the **least significant limb first** (limb 0 holds the 2⁰ coefficient, limb n holds the 2⁶⁴ⁿ coefficient). This little-endian-limb layout lets carry propagation walk forward through the span. + +**`inplace_bigint_mul(a, b)`** multiplies the big integer `a` by the scalar `b` in place, writing the carry into `a[last_index]`. The invariant it enforces is that `a[last_index]` must be zero on entry — this acts as a reserved overflow slot. If it is non-zero, the operation returns `TokenCodecErrc::inputTooLarge` rather than silently truncating. The caller in `tokens.cpp` always passes a span one element larger than the "live" portion, ensuring this slot is available. + +**`inplace_bigint_add(a, b)`** adds a scalar `b` to the big integer `a` by first updating `a[0]`, then ripple-propagating carries through subsequent limbs. The function returns early as soon as the carry becomes zero, which is the common case after the first or second limb. A minimum span size of 2 is required (`inputTooSmall` if violated) to ensure there is at least one carry limb beyond the first. The `overflowAdd` error code is returned if carry propagates past the end — this is defined as a programming invariant violation (it cannot happen for valid-length base58 inputs), hence the comment in `token_errors.h` and the dedicated error code rather than an assertion that terminates the process. + +**`inplace_bigint_div_rem(numerator, divisor)`** performs in-place long division of the big integer by a `uint64_t` scalar. It works from the most significant limb down, maintaining a running remainder `prev_rem`. At each step it forms a 128-bit value `(prev_rem << 64) | numerator[i]` and divides it by `divisor`, using inner lambdas `to_u128` and `div_rem_64` that keep the `__int128` logic localized and let the compiler verify (via `XRPL_ASSERT`) that neither the quotient nor the remainder overflows 64 bits. The return value is the final remainder; `numerator` is overwritten in place with the quotient. This is the workhorse of the encoding path: `tokens.cpp` calls it in a loop to extract successive base-58¹⁰ coefficients from the binary representation. + +**`b58_10_to_b58_be(input)`** decomposes a single base-58¹⁰ coefficient (a `uint64_t` less than 58¹⁰) into exactly 10 individual base-58 digits stored most-significant-first (big-endian). It repeatedly calls `div_rem(input, 58)`, filling the result array from the back. The caller (`tokens.cpp`) then maps each digit through `alphabetForward[]` to get the final character. The `XRPL_ASSERT` guards the precondition that `input < 58¹⁰`; values at or above that bound indicate a logic error in the preceding division step. + +## Error Handling Philosophy + +Rather than throwing exceptions or calling `std::terminate`, every fallible operation returns a `TokenCodecErrc` value checked by the caller in `tokens.cpp`. The `[[nodiscard]]` attribute on every function ensures callers cannot silently discard failure. The `overflowAdd` case is the only one that represents an internal logic error (as opposed to malformed user input); the choice to report it as an error code rather than an assertion reflects the general XRPL preference for recoverable, testable error paths even for conditions that should never occur in correct usage. \ No newline at end of file diff --git a/include/xrpl/protocol/detail/secp256k1.h.ai.json b/include/xrpl/protocol/detail/secp256k1.h.ai.json new file mode 100644 index 0000000000..d56e9d2e90 --- /dev/null +++ b/include/xrpl/protocol/detail/secp256k1.h.ai.json @@ -0,0 +1,26 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "holder" + } + ], + "description": "Provides a singleton accessor for a secp256k1 cryptographic context used for signing and verification in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/detail/secp256k1.h", + "functions": [ + { + "args": [], + "lineno": 7, + "name": "secp256k1Context" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/detail/secp256k1.h.ai.md b/include/xrpl/protocol/detail/secp256k1.h.ai.md new file mode 100644 index 0000000000..6c00d08edf --- /dev/null +++ b/include/xrpl/protocol/detail/secp256k1.h.ai.md @@ -0,0 +1,23 @@ +# `include/xrpl/protocol/detail/secp256k1.h` + +## Role in the System + +This header provides a single, process-wide `secp256k1_context` instance for use throughout the XRPL protocol layer. The secp256k1 library — the same elliptic curve library used by Bitcoin — requires callers to allocate and initialize a context object before performing any cryptographic operations. Creating that context is relatively expensive and, more importantly, the context itself is safe to share across threads for read-only operations. This file centralizes that lifecycle into one place, ensuring the context is created once and destroyed cleanly. + +## Design: Template-Based Singleton Without a Separate `.cpp` + +The function `secp256k1Context()` is declared as a function template with a defaulted, unused type parameter (`template `). This is a well-known C++ idiom for defining a function with a `static` local variable in a header file without violating the One Definition Rule (ODR). A plain `inline` function would also work in C++17, but the template trick predates widespread `inline`-variable support and remains common in headers that need to be compatible with older toolchains and translation units that include the header independently. + +The internal `holder` struct owns the `secp256k1_context*` pointer and manages its lifetime through RAII. Construction calls `secp256k1_context_create` with both `SECP256K1_CONTEXT_VERIFY` and `SECP256K1_CONTEXT_SIGN` flags, pre-initializing the context for both signature signing and verification so the same instance can service either operation. The destructor calls `secp256k1_context_destroy`, ensuring the context is properly freed when the process exits. The `static holder const h` inside the function body is initialized once on the first call and lives for the remainder of the program. + +## Usage in the Protocol Layer + +Both `SecretKey.cpp` and `PublicKey.cpp` include this header and call `secp256k1Context()` heavily. In `SecretKey.cpp` it is passed to functions like `secp256k1_ec_seckey_verify`, `secp256k1_ec_pubkey_create`, `secp256k1_ec_seckey_tweak_add`, and `secp256k1_ecdsa_sign` — covering the full key derivation and signing path for the secp256k1 (ECDSA) key type that XRPL supports alongside Ed25519. In `PublicKey.cpp` it services the verification side: `secp256k1_ecdsa_signature_normalize` and `secp256k1_ecdsa_verify`. Both callers receive a `const*` to the context, which matches the secp256k1 library's thread-safety contract — concurrent reads from a const context require no additional locking. + +## Why `detail/`? + +Placement under `protocol/detail/` signals that this is an implementation convenience header, not part of the public API surface. Consumers of the XRPL library should interact with `SecretKey` and `PublicKey` directly; they have no reason to touch the raw secp256k1 context. Keeping this header in `detail/` enforces that convention by convention, even if the C++ language does not enforce directory-based access control. + +## Tradeoffs + +Combining `SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN` in a single context trades a small amount of additional initialization memory for the convenience of one shared instance. The alternative — separate sign and verify contexts — would double the allocation and add code paths for selecting the right one. For a node that signs transactions and verifies signatures continuously, a unified context is the pragmatic choice. The `const` qualifier on both the `holder` instance and the returned pointer makes the sharing intent explicit and prevents any caller from accidentally randomizing or otherwise mutating the shared context. \ No newline at end of file diff --git a/include/xrpl/protocol/detail/token_errors.h.ai.json b/include/xrpl/protocol/detail/token_errors.h.ai.json new file mode 100644 index 0000000000..f16a178201 --- /dev/null +++ b/include/xrpl/protocol/detail/token_errors.h.ai.json @@ -0,0 +1,50 @@ +{ + "args": [ + { + "lineno": 29, + "name": "c" + }, + { + "lineno": 55, + "name": "e" + } + ], + "classes": [ + { + "args": [], + "lineno": 23, + "name": "TokenCodecErrcCategory" + } + ], + "description": "Defines the TokenCodecErrc error code enum and its integration with std::error_code for XRPL token codec errors, including a custom error category and error code creation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/detail/token_errors.h", + "functions": [ + { + "args": [], + "lineno": 49, + "name": "TokenCodecErrcCategory" + }, + { + "args": [ + "e" + ], + "lineno": 55, + "name": "make_error_code" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 18, + "name": "std" + }, + { + "lineno": 22, + "name": "xrpl::detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/detail/token_errors.h.ai.md b/include/xrpl/protocol/detail/token_errors.h.ai.md new file mode 100644 index 0000000000..31ab42540f --- /dev/null +++ b/include/xrpl/protocol/detail/token_errors.h.ai.md @@ -0,0 +1,33 @@ +# `include/xrpl/protocol/detail/token_errors.h` + +## Role in the System + +This header defines the error type system for XRPL's Base58Check token codec — the encoding scheme used throughout the ledger for account addresses, node keys, seeds, and other typed tokens. It is a pure error-taxonomy header: it introduces no algorithms, only the vocabulary of failure modes that the codec can signal and the plumbing to make those failures composable via `std::error_code`. + +## The `TokenCodecErrc` Enum + +`TokenCodecErrc` is a scoped enum whose nine enumerators cover every distinct failure the codec can produce: + +- **Size failures** (`inputTooLarge`, `inputTooSmall`, `outputTooSmall`): bounds violations detected before or during a conversion pass. +- **Alphabet / character failures** (`badB58Character`, `invalidEncodingChar`): invalid bytes encountered in a Base58 string, distinguished by context (character not in alphabet vs. invalid in the specific encoding context). +- **Semantic failures** (`mismatchedTokenType`, `mismatchedChecksum`): structurally valid input that fails XRPL-specific validation — wrong one-byte type prefix or a four-byte checksum mismatch. +- **Arithmetic failure** (`overflowAdd`): bignum addition overflow during the multi-precision Base58 decode, signaling that the encoded value would exceed the expected output width. +- **Sentinel values** (`success = 0`, `unknown`): `success` is zero deliberately — `std::error_code` treats a zero value as no-error, which is the contract `make_error_code` relies on. + +## Integration with `std::error_code` + +The header wires `TokenCodecErrc` into the standard error-code machinery through three pieces that work together: + +1. **`std::is_error_code_enum` specialisation** (inside `namespace std`): by inheriting `true_type`, this marks `TokenCodecErrc` as a type that can be implicitly converted to `std::error_code`. Without this, callers would have to call `make_error_code` explicitly every time they stored or compared an error. + +2. **`TokenCodecErrcCategory`** (in `xrpl::detail`): a concrete `std::error_category` subclass. Its `name()` returns the stable string `"TokenCodecError"`, and its `message()` translates each enumerator into a human-readable string. Both overrides are declared `final` to prevent further subclassing — there is exactly one category for this error domain. + +3. **`make_error_code(TokenCodecErrc)`** and **`TokenCodecErrcCategory()`**: the two free functions complete the ADL-based protocol. `TokenCodecErrcCategory()` returns a reference to a function-local static, achieving thread-safe singleton semantics at zero cost after the first call (guaranteed by C++11 static initialisation). `make_error_code` pairs an integer value with that category into a `std::error_code`. + +This pattern — enum + `is_error_code_enum` + category + `make_error_code` — is the idiomatic C++11 way to create a custom error domain compatible with `std::error_code` without the overhead of exceptions. + +## Usage Context + +`b58_utils.h` (a sibling `detail` header) directly returns `TokenCodecErrc` values from its inline bignum helpers (`inplace_bigint_add`, `inplace_bigint_mul`). The main codec in `tokens.cpp` wraps results in `Unexpected(TokenCodecErrc::...)`, pairing with the `B58Result` alias (`Expected`) declared in `tokens.h`. The implicit conversion enabled by the `is_error_code_enum` specialisation means a bare `TokenCodecErrc` enumerator flows transparently into an `std::error_code`-typed `Unexpected` wrapper without a cast. Callers then compare the code against specific enumerators — again using implicit conversion — to distinguish recoverable from fatal decode failures. + +The `overflowAdd` error deserves special mention: it exists because the fast Base58 decoder in `b58_fast` represents intermediate values as fixed-size arrays of `uint64_t` words. If a caller passes an over-long encoded string, arithmetic on those words can overflow. This error code surfaces that hardware-level arithmetic condition as a clean protocol failure, preventing silent corruption. \ No newline at end of file diff --git a/include/xrpl/protocol/digest.h.ai.json b/include/xrpl/protocol/digest.h.ai.json new file mode 100644 index 0000000000..0dd8af807c --- /dev/null +++ b/include/xrpl/protocol/digest.h.ai.json @@ -0,0 +1,100 @@ +{ + "args": [ + { + "lineno": 27, + "name": "data" + }, + { + "lineno": 27, + "name": "size" + }, + { + "lineno": 44, + "name": "data" + }, + { + "lineno": 44, + "name": "size" + }, + { + "lineno": 61, + "name": "data" + }, + { + "lineno": 61, + "name": "size" + }, + { + "lineno": 89, + "name": "data" + }, + { + "lineno": 89, + "name": "size" + }, + { + "lineno": 120, + "name": "data" + }, + { + "lineno": 120, + "name": "size" + } + ], + "classes": [ + { + "args": [], + "lineno": 19, + "name": "openssl_ripemd160_hasher" + }, + { + "args": [], + "lineno": 36, + "name": "openssl_sha512_hasher" + }, + { + "args": [], + "lineno": 53, + "name": "openssl_sha256_hasher" + }, + { + "args": [], + "lineno": 77, + "name": "ripesha_hasher" + }, + { + "args": [], + "lineno": 104, + "name": "basic_sha512_half_hasher" + } + ], + "description": "This file defines various cryptographic hashers and digest functions (RIPEMD-160, SHA-256, SHA-512, SHA512-Half, and combinations) used throughout the XRPL codebase, including secure and non-secure variants, and provides utility functions for hashing sequences of objects.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/digest.h", + "functions": [ + { + "args": [ + "args" + ], + "lineno": 143, + "name": "sha512Half" + }, + { + "args": [ + "args" + ], + "lineno": 157, + "name": "sha512Half_s" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 102, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/digest.h.ai.md b/include/xrpl/protocol/digest.h.ai.md new file mode 100644 index 0000000000..2e6137ba8f --- /dev/null +++ b/include/xrpl/protocol/digest.h.ai.md @@ -0,0 +1,35 @@ +# `include/xrpl/protocol/digest.h` — Cryptographic Digest Primitives + +This header is the single authoritative source of cryptographic hash types for the XRPL protocol layer. It defines the concrete hasher structs used everywhere ledger object identifiers, transaction IDs, account addresses, and signing payloads are computed. Every type here is modeled to satisfy the `Hasher` concept from N3980 ("Types Don't Know #"), which is the `hash_append` interface used throughout the `beast` utility layer. + +## The Hasher Contract + +All structs in this file conform to the same interface pattern: a `static constexpr endian` member, a `result_type` typedef, an `operator()(void const*, size_t) noexcept` for feeding data, and an `explicit operator result_type() noexcept` for extracting the digest. This uniformity is not cosmetic — it allows `beast::hash_append` to drive any of these hashers generically, and it's what makes `sha512Half()` a single variadic template rather than dozens of overloads. + +## OpenSSL-Backed Primitives + +`openssl_ripemd160_hasher`, `openssl_sha256_hasher`, and `openssl_sha512_hasher` are thin wrappers around the corresponding OpenSSL EVP context state. The design choice to store the context as an opaque `char` array of fixed size (`char ctx_[96]`, `char ctx_[112]`, `char ctx_[216]`) is deliberate: it avoids including any OpenSSL headers in this widely-included protocol header, while still allocating context storage inline on the stack without heap overhead. The sizes correspond to the actual sizes of `EVP_MD_CTX` (or equivalent internal structs) for each algorithm. All three declare `endian = boost::endian::order::native`, since raw byte feeds don't carry endian meaning at the OpenSSL level. + +Type aliases `ripemd160_hasher`, `sha256_hasher`, and `sha512_hasher` abstract away the `openssl_` prefix. This naming layer exists so that an alternative implementation (e.g., a different crypto library) could be swapped in by changing only the alias definitions, leaving all call sites untouched. + +## `ripesha_hasher` — Account ID Derivation + +`ripesha_hasher` computes SHA-256 over the input, then RIPEMD-160 over that SHA-256 digest — the classic Bitcoin-lineage construction used in XRPL to derive a 160-bit account identifier from a public key. The comment in the struct explicitly states the design goal: account IDs are algorithm-agnostic. Whether the underlying key is secp256k1 or ed25519, the same `ripesha_hasher` formula yields the `AccountID`. This insulates the account identifier from the cryptographic scheme, allowing future key types to be added without changing the address format. + +Internally `ripesha_hasher` composes the two underlying hashers: data feeds into a `sha256_hasher`, and only when `operator result_type()` is called does it finalize SHA-256 and immediately feed the result into a fresh `ripemd160_hasher`. No intermediate buffer escapes the function. + +## `basic_sha512_half_hasher` — The Workhorse Hasher + +SHA-512-Half is the dominant hash construction in XRPL's protocol layer. It computes a full SHA-512 digest and returns only the first 256 bits as a `uint256`. The rationale for truncating SHA-512 rather than using SHA-256 directly is performance: SHA-512 is faster than SHA-256 on 64-bit hardware due to wider register operations, and 256 bits of output from SHA-512 still provides strong security guarantees. + +A critical difference from the OpenSSL wrappers is that `basic_sha512_half_hasher` declares `endian = boost::endian::order::big`. This matters when `hash_append` serializes multi-byte integers before feeding them to the hasher — big-endian is the canonical on-wire byte order for XRPL, matching the protocol's network representation. + +The `bool Secure` template parameter controls whether the destructor zeros internal state via `secure_erase()`. This is implemented via two overloads of a private `erase()` method dispatched through `std::integral_constant{}`. When `Secure = false`, `erase(std::false_type)` is an empty inline no-op with zero overhead. When `Secure = true`, `erase(std::true_type)` calls `secure_erase(&h_, sizeof(h_))` to clear the embedded SHA-512 context — important when the hasher has processed sensitive key material that must not linger in memory. + +Two public aliases expose this: +- `sha512_half_hasher` — the standard fast variant, used for most ledger computations. +- `sha512_half_hasher_s` — the secure variant, used when hashing private keys or seed material. + +## `sha512Half()` and `sha512Half_s()` — Convenience Entry Points + +These variadic function templates are the primary call sites in the rest of the codebase. They construct a `sha512_half_hasher` (or `_s`), drive it with `beast::hash_append` over all supplied arguments, and return the `uint256` result. Because `hash_append` is overloaded for all XRPL protocol types (including `HashPrefix`, `STObject`, `Serializer`, and primitive integers), a single `sha512Half(HashPrefix::transactionID, txSerializer)` call correctly serializes and hashes a complete transaction object. The `HashPrefix` enum (defined in `HashPrefix.h`) uses this mechanism to enforce domain separation — every distinct hash purpose (transaction IDs, ledger node hashes, signing payloads, manifests, payment channel claims) is distinguished by prepending a unique 4-byte prefix, preventing cross-context hash collisions even when the underlying data bytes are identical. \ No newline at end of file diff --git a/include/xrpl/protocol/json_get_or_throw.h.ai.json b/include/xrpl/protocol/json_get_or_throw.h.ai.json new file mode 100644 index 0000000000..08215442c6 --- /dev/null +++ b/include/xrpl/protocol/json_get_or_throw.h.ai.json @@ -0,0 +1,100 @@ +{ + "args": [ + { + "lineno": 13, + "name": "k" + }, + { + "lineno": 24, + "name": "k" + }, + { + "lineno": 24, + "name": "et" + }, + { + "lineno": 29, + "name": "v" + }, + { + "lineno": 29, + "name": "field" + } + ], + "classes": [ + { + "args": [ + "k" + ], + "lineno": 11, + "name": "JsonMissingKeyError" + }, + { + "args": [ + "k", + "et" + ], + "lineno": 22, + "name": "JsonTypeMismatchError" + } + ], + "description": "Provides utility functions and error classes for extracting and validating typed values from JSON objects, throwing exceptions on missing keys or type mismatches.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/json_get_or_throw.h", + "functions": [ + { + "args": [ + "v", + "field" + ], + "lineno": 29, + "name": "getOrThrow" + }, + { + "args": [ + "v", + "field" + ], + "lineno": 35, + "name": "getOrThrow" + }, + { + "args": [ + "v", + "field" + ], + "lineno": 48, + "name": "getOrThrow" + }, + { + "args": [ + "v", + "field" + ], + "lineno": 62, + "name": "getOrThrow" + }, + { + "args": [ + "v", + "field" + ], + "lineno": 89, + "name": "getOrThrow" + }, + { + "args": [ + "v", + "field" + ], + "lineno": 101, + "name": "getOptional" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "Json" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/json_get_or_throw.h.ai.md b/include/xrpl/protocol/json_get_or_throw.h.ai.md new file mode 100644 index 0000000000..d5b90dd21b --- /dev/null +++ b/include/xrpl/protocol/json_get_or_throw.h.ai.md @@ -0,0 +1,66 @@ +# `json_get_or_throw.h` — Typed JSON Extraction with Protocol-Field Keys + +This header provides a small but important safety layer over the raw `Json::Value` API: typed extraction functions that throw structured exceptions instead of silently returning default values or crashing on type mismatches. It lives in `include/xrpl/protocol/` because its interface is anchored to `xrpl::SField` — the XRPL protocol's typed field descriptor — rather than arbitrary string keys. + +## Why This Exists + +`Json::Value` is a dynamically typed container. Accessing a missing key returns a null value; accessing a value as the wrong type silently coerces or silently returns zero. In serialization/deserialization code — particularly for security-sensitive structures like cross-chain attestations — silent failures are dangerous. This header replaces that permissiveness with an explicit contract: either the key is present with the right type, or an exception is thrown with a clear diagnosis. + +## Error Types + +Two exception structs are defined in the `Json` namespace: + +- `JsonMissingKeyError` — carries the field name as `char const*` and is thrown when the key is entirely absent from the JSON object. +- `JsonTypeMismatchError` — carries the field name and an `expectedType` string, thrown when the key is present but the value's runtime type doesn't match the requested `T`. + +Both inherit from `std::exception` and implement `what()` with lazy message construction: the `msg` field is `mutable std::string` and only populated on the first call to `what()`. This avoids a string allocation at the throw site — the string is only built if a caller actually reads the exception message, which is the common case only in error handlers. + +## `getOrThrow` — Explicitly Specialization-Only Template + +The primary template: + +```cpp +template +T getOrThrow(Json::Value const& v, xrpl::SField const& field); +``` + +contains `static_assert(sizeof(T) == -1, ...)` in its body. Since `sizeof(T)` is never `-1`, instantiating any non-specialized `T` is a compile-time error, not a runtime surprise. This pattern is intentional: the function is only valid for types that have an explicit specialization, and the compiler enforces it. + +Specializations are provided for four types: + +**`std::string`** — Checks `isMember`, then requires `isString()`. No coercion. + +**`bool`** — Accepts a native JSON boolean (`isBool()`) or any integral value, where non-zero maps to `true`. This mirrors a common XRPL convention where boolean-semantic fields like `WasLockingChainSend` are encoded as `0`/`1` integers in JSON, not as JSON `true`/`false`. + +**`std::uint64_t`** — The most permissive specialization, because 64-bit integers exceed JavaScript's safe integer range (`2^53 - 1`). It accepts: + 1. Native JSON unsigned integer (`isUInt()`). + 2. Signed JSON integer that is non-negative (guards against negative values with an explicit range check). + 3. A hex-encoded string, parsed via `std::from_chars` in base 16. The parse validates that every character was consumed (`p == s.data() + s.size()`) — a partial match is treated as a type error. + +**`xrpl::Buffer`** — Delegates to `getOrThrow` to extract the raw hex string, then calls `strUnHex` to decode it to bytes. A decode failure (invalid hex) throws `JsonTypeMismatchError`. A TODO comment notes a conceptual mismatch between `Buffer` and `STBlob`/blob types in the broader XRPL type system. + +## `getOptional` — Exception-to-Optional Adapter + +```cpp +template +std::optional getOptional(Json::Value const& v, xrpl::SField const& field); +``` + +This wraps `getOrThrow` in a blanket `catch(...)` and returns `std::nullopt` on any exception. The function is explicitly documented as usable by external projects such as the witness server — a cross-chain bridge component that runs outside the rippled process and needs to parse XRPL JSON payloads. The catch-all is intentional: the caller's only question is whether the field is present and valid, not which specific error occurred. + +## `SField` as the Key Type + +Rather than accepting `std::string` or `const char*` keys, both functions require an `xrpl::SField` reference. The actual JSON key is retrieved via `field.getJsonName()`, which returns a `Json::StaticString` — a compile-time string wrapper that avoids dynamic allocation in the JSON library's hash map. This design ensures all key lookups are tied to declared XRPL protocol fields (`sfAttestationSignerAccount`, `sfPublicKey`, `sfAmount`, etc.), eliminating magic string literals and making typos a compile error. + +## Primary Consumer: Cross-Chain Attestations + +`XChainAttestations.cpp` is the dominant user, calling `getOrThrow` in constructor initializer lists to deserialize attestation objects received from witness servers: + +```cpp +attestationSignerAccount{Json::getOrThrow(v, sfAttestationSignerAccount)}, +publicKey{Json::getOrThrow(v, sfPublicKey)}, +signature{Json::getOrThrow(v, sfSignature)}, +wasLockingChainSend{Json::getOrThrow(v, sfWasLockingChainSend)}, +``` + +This pattern is clean and exception-safe: if any field is missing or malformed, the constructor throws before the object is constructed, preventing partially-initialized attestation objects from reaching validation logic. Additional specializations for `AccountID`, `PublicKey`, and `STAmount` are defined closer to those types but follow the same pattern established here. \ No newline at end of file diff --git a/include/xrpl/protocol/jss.h.ai.json b/include/xrpl/protocol/jss.h.ai.json new file mode 100644 index 0000000000..08f22b6201 --- /dev/null +++ b/include/xrpl/protocol/jss.h.ai.json @@ -0,0 +1,18 @@ +{ + "args": [], + "classes": [], + "description": "Defines a large set of static JSON field names (as StaticString constants) for use throughout the XRPL codebase, especially for RPC, transaction, and ledger entry JSON objects. These are used to optimize property access in Json::Value objects and to standardize field names across the codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/jss.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "jss" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/jss.h.ai.md b/include/xrpl/protocol/jss.h.ai.md new file mode 100644 index 0000000000..d27f1c459b --- /dev/null +++ b/include/xrpl/protocol/jss.h.ai.md @@ -0,0 +1,65 @@ +# `include/xrpl/protocol/jss.h` — Centralized JSON Field Name Registry + +## Purpose + +`jss.h` is the single authoritative source of truth for every JSON key name used across the XRPL rippled implementation. Rather than scattering string literals throughout hundreds of translation units, this header declares each key exactly once as a `constexpr Json::StaticString` inside the `xrpl::jss` namespace. Any code that builds or inspects a `Json::Value` object — RPC handlers, ledger serializers, transaction processors, network operation code — includes this header and writes `jss::account_data` instead of `"account_data"`. + +## The `JSS` Macro + +The entire file is built around a single macro defined at line 9: + +```cpp +#define JSS(x) constexpr ::Json::StaticString x(#x) +``` + +`JSS(account_data)` expands to `constexpr ::Json::StaticString account_data("account_data")`. The `#x` stringification operator ensures the C++ identifier and the JSON key string are always identical — it is impossible for a rename of the identifier to silently diverge from the string it carries. After all declarations, the macro is `#undef`-ed so it does not leak beyond the namespace block. + +## Why `StaticString`? + +`Json::StaticString` is a thin wrapper over `char const*` defined in `json_value.h`. Its sole purpose is to act as a tag type that routes `Json::Value::operator[]` through a distinct, optimized code path. When you index a `Json::Value` object with a regular `std::string` or `char const*` key, the JSON library copies the key string into its internal map node. When you index with a `StaticString`, the library stores only the pointer — no allocation, no copy — because it knows the pointed-to string has static lifetime (it came from a string literal in the binary). Since `jss.h` constants hold pointers into the compiler-generated string table, this contract is always satisfied. At the call site the difference looks like: + +```cpp +result[jss::ledger_index] = 42; // stores pointer, no allocation +result["ledger_index"] = 42; // copies "ledger_index" into heap storage +``` + +Across the thousands of JSON field accesses per second on a busy validator this matters, and the API makes the efficient path the default for all named fields. + +## Naming Conventions + +The file mixes two conventions that map onto distinct JSON domains. **PascalCase** names (e.g., `Account`, `Amount`, `Flags`, `TransactionType`, `SigningPubKey`) are canonical transaction and ledger-entry field names defined by the XRPL wire protocol. Their casing is part of the protocol specification and must match exactly what gets serialized into ledger objects. **snake_case** names (e.g., `account_data`, `ledger_index`, `engine_result_code`) belong to the RPC API layer — the JSON objects that flow between client applications and the server over HTTP or WebSocket. Keeping both in the same file means a developer can trace a field from RPC parameter, through processing logic, to serialized ledger entry or transaction without switching files. + +## Inline Documentation Convention + +Each declaration carries a trailing comment using a compact notation explained in the file header: + +- `in:` — the given RPC handler reads this field from its input `Json::Value` +- `out:` — the given handler writes this field into its response `Json::Value` +- `field:` — this is a protocol-level field of at least one transaction or ledger entry type +- `RPC:` — common infrastructure of RPC request/response envelopes +- `error:` — part of the standard error response shape + +This embedded cross-reference serves as a lightweight index into the codebase. Looking up `jss::engine_result_code` immediately reveals it is written by `NetworkOPs`, `TransactionSign`, and `Submit` — three distinct subsystems that all agree on the same key name precisely because they share this header. + +## Macro-Generated Transaction and Ledger Entry Names + +The tail of the file uses a pattern that appears elsewhere in the XRPL protocol infrastructure: it temporarily redefines the `TRANSACTION` and `LEDGER_ENTRY` X-macros to emit `JSS(name)` declarations, then includes the canonical macro tables: + +```cpp +#pragma push_macro("TRANSACTION") +#undef TRANSACTION +#define TRANSACTION(tag, value, name, ...) JSS(name); +#include +#undef TRANSACTION +#pragma pop_macro("TRANSACTION") +``` + +The same pattern repeats for `LEDGER_ENTRY` and `LEDGER_ENTRY_DUPLICATE`. This ensures that `jss::Payment`, `jss::EscrowCreate`, `jss::Offer`, and every other transaction or ledger-entry type name are automatically available as `StaticString` constants without manual duplication. Adding a new transaction type to `transactions.macro` automatically registers its name in `jss` — the registry stays in sync by construction. + +## ODR and Translation Unit Safety + +Because every declaration uses `constexpr`, the variables have internal linkage in C++17 and later. Including the header from dozens of `.cpp` files does not violate the One Definition Rule — each translation unit gets its own copy of the `char const*` wrapper, but all copies point to identical string literal data deduplicated by the linker. The `constexpr` qualifier also enables compile-time use of these values in template arguments or `static_assert` contexts if needed. + +## Relationship to the Rest of the Protocol Layer + +`jss.h` sits at the boundary between the protocol serialization layer (`include/xrpl/protocol/`) and the RPC/network layer (`src/xrpld/rpc/`). Almost every RPC handler file includes it directly. It has no dependencies beyond `json_value.h`, making it cheap to include anywhere. The absence of a corresponding `.cpp` file is intentional — this header is pure compile-time data with no runtime initialization cost. \ No newline at end of file diff --git a/include/xrpl/protocol/messages.h.ai.json b/include/xrpl/protocol/messages.h.ai.json new file mode 100644 index 0000000000..a31c348ba6 --- /dev/null +++ b/include/xrpl/protocol/messages.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "This file provides a workaround for a macro conflict with TYPE_BOOL generated by some versions of protobuf, and includes the xrpl protobuf definitions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/messages.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/protocol/messages.h.ai.md b/include/xrpl/protocol/messages.h.ai.md new file mode 100644 index 0000000000..4855d9b346 --- /dev/null +++ b/include/xrpl/protocol/messages.h.ai.md @@ -0,0 +1,25 @@ +# `messages.h` — Protobuf Include Shim with Macro Conflict Resolution + +This file exists as a single-purpose indirection layer between XRPL's C++ codebase and the protobuf-generated peer-to-peer protocol definitions. Its entire substance is four lines of preprocessor logic followed by one `#include`, yet it serves a critical portability role. + +## The Problem It Solves + +Some versions of `protoc` (the protobuf compiler) emit C++ code that uses `TYPE_BOOL` as a plain identifier. On certain platforms — particularly those that have pulled in Windows SDK headers or specific system headers — `TYPE_BOOL` is already defined as a preprocessor macro. When `xrpl.pb.h` is included in a translation unit where that macro is live, the preprocessor expands it mid-compilation, corrupting the generated symbol names and producing cryptic build errors. This is a known upstream issue tracked at [github.com/google/protobuf/issues/549](https://github.com/google/protobuf/issues/549). + +The fix cannot go into `xrpl.pb.h` itself because that file is generated by `protoc` and would be overwritten during any regeneration. Nor can it be delegated to individual callers, who would inevitably forget to apply it consistently. Instead, this shim centralizes the `#undef` in one canonical place. + +## Design + +The `#ifdef TYPE_BOOL` guard before the `#undef` is deliberate defensive practice: it silently no-ops in environments where the macro was never defined, preventing compilers that treat undef-of-undefined as a warning from flagging the file. The `TODO` comment honestly flags this as technical debt — the workaround is expected to be removed once the project upgrades to a `protoc` version that no longer generates conflicting code. + +All code that needs access to the XRPL wire protocol definitions should include `` rather than `` directly. This is the pattern enforced throughout the overlay layer: `ProtocolMessage.h`, `TrafficCount.h`, `TxMetrics.h`, `Slot.h`, and several test files all follow this indirection. + +## What the Included Proto Defines + +The underlying `xrpl.proto` (compiled to `xrpl.pb.h`) defines the complete peer-to-peer wire protocol for the XRP Ledger network. The `MessageType` enum enumerates every message that nodes exchange: `mtMANIFESTS` (ephemeral validator keys), `mtPING`, `mtCLUSTER`, `mtENDPOINTS` (peer discovery), `mtTRANSACTION` and `mtTRANSACTIONS` (individual and batched transaction relay), `mtGET_LEDGER` / `mtLEDGER_DATA` (ledger sync), `mtPROPOSE_LEDGER` (consensus proposals), `mtVALIDATION`, `mtHAVE_SET` / `mtHAVE_TRANSACTIONS` (set availability announcements), `mtSQUELCH` (validator message suppression), `mtVALIDATOR_LIST` / `mtVALIDATOR_LIST_COLLECTION` (UNL distribution), and newer messages for Merkle proof paths (`mtPROOF_PATH_REQ/RESPONSE`) and ledger replay deltas (`mtREPLAY_DELTA_REQ/RESPONSE`). + +The numeric IDs assigned to each `MessageType` value are stable and wire-format significant — gaps in the sequence (e.g., the jump from 5 to 15, or 42 to 54) represent previously used values that were retired. The proto file itself warns explicitly not to reassign retired numbers, since doing so could cause silent misinterpretation of messages from older peers still speaking the old protocol. + +Each message type has a corresponding protobuf `message` definition in the same file — for example, `TMTransaction` carries the raw serialized transaction bytes alongside a `TransactionStatus` enum that communicates whether the originating node has validated it, committed it to a closed ledger, or rejected it. `TMProposeSet` carries a consensus proposal with its sequence number, ledger hash, close time, and cryptographic signature. `TMSquelch` is used by the reduce-relay feature to instruct peers to suppress re-broadcasting of a specific validator's messages for a configurable duration. + +In summary, `messages.h` is a small but load-bearing shim: it enforces a single, safe entry point into XRPL's protobuf-defined peer protocol, hiding a known compiler incompatibility behind a stable include path. \ No newline at end of file diff --git a/include/xrpl/protocol/nft.h.ai.json b/include/xrpl/protocol/nft.h.ai.json new file mode 100644 index 0000000000..3183d8217c --- /dev/null +++ b/include/xrpl/protocol/nft.h.ai.json @@ -0,0 +1,119 @@ +{ + "args": [ + { + "lineno": 17, + "name": "i" + }, + { + "lineno": 22, + "name": "t" + }, + { + "lineno": 29, + "name": "id" + }, + { + "lineno": 36, + "name": "id" + }, + { + "lineno": 43, + "name": "id" + }, + { + "lineno": 50, + "name": "tokenSeq" + }, + { + "lineno": 50, + "name": "taxon" + }, + { + "lineno": 71, + "name": "id" + }, + { + "lineno": 81, + "name": "id" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "TaxonTag" + } + ], + "description": "Provides utility functions and types for handling NFT (Non-Fungible Token) taxons, flags, transfer fees, serials, and issuer extraction from a 256-bit NFT identifier in the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/nft.h", + "functions": [ + { + "args": [ + "i" + ], + "lineno": 17, + "name": "toTaxon" + }, + { + "args": [ + "t" + ], + "lineno": 22, + "name": "toUInt32" + }, + { + "args": [ + "id" + ], + "lineno": 29, + "name": "getFlags" + }, + { + "args": [ + "id" + ], + "lineno": 36, + "name": "getTransferFee" + }, + { + "args": [ + "id" + ], + "lineno": 43, + "name": "getSerial" + }, + { + "args": [ + "tokenSeq", + "taxon" + ], + "lineno": 50, + "name": "cipheredTaxon" + }, + { + "args": [ + "id" + ], + "lineno": 71, + "name": "getTaxon" + }, + { + "args": [ + "id" + ], + "lineno": 81, + "name": "getIssuer" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "nft" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/nft.h.ai.md b/include/xrpl/protocol/nft.h.ai.md new file mode 100644 index 0000000000..7de7ad5c55 --- /dev/null +++ b/include/xrpl/protocol/nft.h.ai.md @@ -0,0 +1,68 @@ +# `include/xrpl/protocol/nft.h` + +## Role in the System + +This header is the single source of truth for the binary layout of an XRPL NFT token identifier. Every 256-bit `NFTokenID` on the ledger is a precisely packed big-endian structure, and this file provides the constants, type aliases, and accessor functions that let the rest of the codebase extract fields from that structure without duplicating bit-offset arithmetic. It also houses `cipheredTaxon()`, the cryptographically lightweight but architecturally critical function that prevents NFT page hotspots. + +## NFToken ID Layout + +A `uint256` NFT identifier packs six fields sequentially, all in big-endian byte order: + +| Byte offset | Size | Field | +|---|---|---| +| 0–1 | 2 bytes | flags (`std::uint16_t`) | +| 2–3 | 2 bytes | transfer fee in basis points (`std::uint16_t`) | +| 4–23 | 20 bytes | issuer `AccountID` | +| 24–27 | 4 bytes | *ciphered* taxon (`std::uint32_t`) | +| 28–31 | 4 bytes | serial / mint sequence (`std::uint32_t`) | + +The companion file `nftPageMask.h` defines a `pageMask` constant with zeros in bytes 0–19 and `0xFF` in bytes 20–31. This makes the low 96 bits — the last four bytes of the issuer address plus the ciphered taxon plus the serial — the sort key that determines which `NFTokenPage` ledger object holds a given token. Flags and transfer fee are deliberately excluded from that key; they affect behaviour, not placement. + +## Type Safety: `Taxon` + +```cpp +struct TaxonTag {}; +using Taxon = tagged_integer; +``` + +`Taxon` is a `tagged_integer` wrapping `uint32_t`. The empty `TaxonTag` struct serves purely as a phantom type: the compiler will reject any accidental integer passed where a `Taxon` is expected, and vice versa. The two conversion helpers `toTaxon()` and `toUInt32()` are the only intended crossing points. This matters because taxons and serial numbers are the same underlying type and appear side by side in several call sites — a silent mix-up would produce valid-looking but wrong token IDs. + +## Flag Constants + +Five `constexpr` bit flags are defined: + +- `flagBurnable` (0x0001) — the issuer may burn the token even if held by someone else +- `flagOnlyXRP` (0x0002) — the token may only be traded for XRP +- `flagCreateTrustLines` (0x0004) — accepting a transfer may open trust lines on the holder's account +- `flagTransferable` (0x0008) — the token may be transferred to accounts other than the issuer +- `flagMutable` (0x0010) — the token's URI may be changed post-mint + +Because flags live in bytes 0–1 of the token ID, they are immutable after minting: changing a flag would change the token ID itself, breaking all references. `NFTokenModify.cpp` relies directly on `nft::getFlags()` to check `flagMutable` before permitting a URI update — the flag is the authorization. + +## The Taxon Cipher (`cipheredTaxon`) + +This is the most algorithmically significant function in the file. The ledger stores NFTs in `NFTokenPage` objects sorted by their low 96 bits. If an issuer mints thousands of tokens under the same taxon, all of those tokens would share the same taxon bytes (24–27) and differ only in serial (28–31). Without intervention they would cluster in the same few pages — a hotspot that increases ledger-write fan-out for every mint. + +The solution is a Linear Congruential Generator (LCG) seeded by the mint sequence number: + +```cpp +return taxon ^ toTaxon(((384160001 * tokenSeq) + 2459)); +``` + +The multiplier 384160001 is congruent to 1 mod 4 and the addend 2459 is odd, satisfying the Hull-Dobell theorem conditions for a full-period LCG over 2³² when arithmetic wraps naturally on `uint32_t`. The result is XORed with the user-supplied taxon to produce the stored value. Because `tokenSeq` advances monotonically per account (and the issuer cannot freely choose it), successive mints under the same taxon land in very different positions in the page sort order, distributing load across multiple pages. + +The decoding in `getTaxon()` exploits the fact that XOR is its own inverse: it calls `cipheredTaxon(serial, storedTaxon)` a second time, which cancels the original scramble and recovers the original taxon. No separate "decipher" function is needed. + +The comment carries an explicit warning: changing these LCG constants is a consensus-breaking change that would require an amendment and a protocol-level way to distinguish old from new token IDs, because the stored ciphered value is permanent on the ledger. + +## Field Accessor Functions + +All five accessors use `memcpy` rather than pointer casts for safe unaligned reads, then call `boost::endian::big_to_native` to produce host-endian values: + +- `getFlags(id)` — reads bytes 0–1 +- `getTransferFee(id)` — reads bytes 2–3; the value is in basis points (e.g., 50000 = 50%) +- `getIssuer(id)` — calls `AccountID::fromVoid(id.data() + 4)`, reading bytes 4–23 +- `getTaxon(id)` — reads bytes 24–27, then un-ciphers using the serial recovered from bytes 28–31 +- `getSerial(id)` — reads bytes 28–31 + +These are used throughout the NFT subsystem: `NFTokenMint::createNFTokenID()` packs the structure; `NFTokenHelpers.cpp` calls `getIssuer()` to resolve royalty recipients and `getFlags()` to enforce transfer restrictions; invariant checkers call `getFlags()` and `getTaxon()` to validate page ordering. The layout is a stable ABI shared across ledger storage, RPC output, and transaction validation. \ No newline at end of file diff --git a/include/xrpl/protocol/nftPageMask.h.ai.json b/include/xrpl/protocol/nftPageMask.h.ai.json new file mode 100644 index 0000000000..aa00e6151f --- /dev/null +++ b/include/xrpl/protocol/nftPageMask.h.ai.json @@ -0,0 +1,18 @@ +{ + "args": [], + "classes": [], + "description": "Defines a constant mask (pageMask) used to access the low 96 bits of an NFToken value for ordering NFT directory pages in the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/nftPageMask.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "nft" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/nftPageMask.h.ai.md b/include/xrpl/protocol/nftPageMask.h.ai.md new file mode 100644 index 0000000000..a54aab5b5f --- /dev/null +++ b/include/xrpl/protocol/nftPageMask.h.ai.md @@ -0,0 +1,54 @@ +# `nftPageMask.h` — NFToken Page Ordering Mask + +This tiny header defines a single `constexpr uint256` constant, `xrpl::nft::pageMask`, that acts as the authoritative boundary between two conceptual regions of an `NFTokenID`. + +## The NFToken ID Layout Problem + +Every `NFTokenID` in XRPL is a 256-bit value. By protocol convention the high 160 bits encode the issuing `AccountID` and the low 96 bits encode token-specific metadata (taxon and sequence number). NFToken ownership pages in the ledger — `ltNFTOKEN_PAGE` SLEs — are keyed by a `uint256` that fuses an owner's `AccountID` into the high bits while placing a token-derived value in the low 96 bits. To split those two regions consistently across every site that constructs or inspects page keys, the codebase needs a single, canonical bitmask. That is exactly what this file provides. + +## The Constant + +```cpp +uint256 constexpr pageMask( + "0000000000000000000000000000000000000000ffffffffffffffffffffffff"); +``` + +The mask is 96 bits of `1` in the least-significant position and 160 bits of `0` above. It is constructed via the `constexpr` `std::string_view` overload of `base_uint`, so the value is resolved at compile time with zero runtime overhead. + +## How It Is Used + +`pageMask` appears at every point where code must transition between the owner-identity portion and the token-ordering portion of a page key. + +**Page key construction** in `Indexes.cpp` (`keylet::nftpage`) computes: + +```cpp +(k.key & ~nft::pageMask) + (token & nft::pageMask) +``` + +The complement `~pageMask` isolates the owner's high 160 bits from an existing page keylet, while `token & pageMask` extracts the low 96 bits of the incoming token ID. Adding them (which is safe because the two regions are disjoint) forms the new page key. + +**The max-page sentinel** (`keylet::nftpage_max`) sets the entire `uint256` to `pageMask` (all-ones in the low 96 bits) and then overwrites the high bytes with the owner's `AccountID`, yielding the lexicographically largest page key for that owner. This sentinel is used as the tail anchor of the doubly-linked page chain. + +**Pagination in RPC handlers** (`AccountNFTs.cpp`, `AccountObjects.cpp`) applies the mask to validate that a client-supplied `marker` or `entryIndex` refers to the correct page: + +```cpp +uint256 const maskedMarker = marker & nft::pageMask; +// ... +if (firstNFTPage.key != (entryIndex & ~nft::pageMask)) +``` + +Using `~pageMask` isolates the owner part of the key; using `pageMask` itself isolates the token-ordering part. + +**Sort-order comparisons** in `NFTokenHelpers.cpp` use the mask to establish ordering among tokens on the same page: + +```cpp +if (auto const lowBitsCmp{(a & nft::pageMask) <=> (b & nft::pageMask)}; lowBitsCmp != 0) +``` + +Tokens with different low-96-bit values belong to different pages; tokens sharing those bits belong to the same page and are distinguished by their full 256-bit ID. + +**Invariant checking** in `NFTInvariant.cpp` verifies that a page being deleted has its low 96 bits all-ones (the sentinel value) and that non-sentinel pages satisfy structural constraints, both using direct comparison against `pageMask`. + +## Design Rationale + +Centralising this literal in one header eliminates the risk of transcription errors that would silently corrupt page key lookups. Marking it `constexpr` rather than `inline` or `static` means every translation unit that includes the header gets it as a true compile-time constant with no ODR concerns — `uint256` satisfies `constexpr` construction via `base_uint`'s `string_view` constructor, making this a zero-cost abstraction at the protocol boundary. \ No newline at end of file diff --git a/include/xrpl/protocol/serialize.h.ai.json b/include/xrpl/protocol/serialize.h.ai.json new file mode 100644 index 0000000000..060b96848f --- /dev/null +++ b/include/xrpl/protocol/serialize.h.ai.json @@ -0,0 +1,29 @@ +{ + "args": [], + "classes": [], + "description": "Provides utility functions to serialize XRPL protocol objects to blobs or hex strings.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/serialize.h", + "functions": [ + { + "args": [ + "o" + ], + "lineno": 10, + "name": "serializeBlob" + }, + { + "args": [ + "o" + ], + "lineno": 19, + "name": "serializeHex" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/serialize.h.ai.md b/include/xrpl/protocol/serialize.h.ai.md new file mode 100644 index 0000000000..4a65f0084f --- /dev/null +++ b/include/xrpl/protocol/serialize.h.ai.md @@ -0,0 +1,25 @@ +# `include/xrpl/protocol/serialize.h` + +## Role in the System + +`serialize.h` is a thin convenience header that sits at the top of the XRPL protocol serialization stack. It exposes two free functions — `serializeBlob` and `serializeHex` — that bridge the lower-level `Serializer` accumulator with the higher-level `STObject` (and any type that speaks the same `add(Serializer&)` protocol). The file's sole purpose is ergonomic: callers that need to turn a live protocol object into raw bytes or a hex string should not have to manually construct a `Serializer`, call `add`, and then extract the buffer every time they do it. + +## Key Functions + +**`serializeBlob(o)`** is a function template that works with any type implementing the `add(Serializer&) const` interface. The function constructs a default `Serializer` (pre-reserved to 256 bytes), invokes `o.add(s)` to let the object write its canonical wire encoding, and returns `s.peekData()` — a copy of the internal `Blob`. Because it is templated on `Object` rather than fixed to `STObject`, it can be called on any serializable protocol type (`STTx`, `STLedgerEntry`, metadata objects, etc.) without additional overloads. + +**`serializeHex(o)`** is a thin non-template overload that accepts an `STObject const&` and delegates directly to `serializeBlob` followed by `strHex`. It is `inline` to avoid a separate translation unit, and its narrower type signature (concrete `STObject` rather than a template parameter) is intentional: hex output is almost exclusively needed for RPC responses where callers already hold an `STObject`, so no template instantiation overhead is paid. + +## Design Decisions + +The template/non-template split reflects actual usage patterns in the codebase. In practice, `serializeBlob` is the general-purpose primitive, while `serializeHex` covers the narrow but frequent RPC case of rendering a full transaction, ledger entry, or metadata blob as a hex string for JSON responses — as seen in `LedgerToJson.cpp` (producing `tx_blob` and `tx_meta` fields) and `LedgerData.cpp` (producing `data` entries for ledger objects). + +The design deliberately avoids returning a `Serializer` object directly. `peekData()` returns a `Blob const&` (a `std::vector`) by value copy, which means callers own the buffer without any lifetime tie to the transient `Serializer`. This is a safe, correct default: `Serializer` itself is marked partially deprecated internally (`mData` is tagged `// DEPRECATED`), and the public contract here hides those internal details. + +The `add(Serializer&)` protocol is the canonical serialization interface across the XRPL type hierarchy — defined as a pure virtual in `STBase` and implemented by every concrete serializable type. By accepting any `Object` with that method in `serializeBlob`, the header remains open to future protocol types without modification, consistent with the rest of the XRPL type system's extensibility pattern. + +## Relationship to Neighboring Files + +- **`Serializer.h`** provides the byte accumulator (`Serializer`) and the `Blob`/`Slice` primitives that back the wire encoding. `peekData()` extracts the accumulated bytes as a `Blob const&`. +- **`STObject.h`** is the concrete object type accepted by `serializeHex`; its `add` implementation writes the full field-by-field canonical encoding. +- **`strHex.h`** provides the `strHex` utility that converts a byte range to an uppercase hex string, completing the `serializeHex` pipeline. \ No newline at end of file diff --git a/include/xrpl/protocol/st.h.ai.json b/include/xrpl/protocol/st.h.ai.json new file mode 100644 index 0000000000..770e182b3e --- /dev/null +++ b/include/xrpl/protocol/st.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "Header file that includes all core XRPL protocol serialization types, providing a central import point for protocol-related classes and definitions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/st.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/protocol/st.h.ai.md b/include/xrpl/protocol/st.h.ai.md new file mode 100644 index 0000000000..e51b10fffa --- /dev/null +++ b/include/xrpl/protocol/st.h.ai.md @@ -0,0 +1,47 @@ +# `include/xrpl/protocol/st.h` — Serialized Type Umbrella Header + +`st.h` is an aggregation header that exposes the entire XRPL *Serialized Type* (ST) system through a single `#include`. Every type that can appear inside a transaction, ledger entry, or consensus message is declared in one of the headers it pulls in. Consumers that work with protocol objects at any depth — transaction processing, serialization, JSON conversion, RPC formatting — can include this one file instead of tracking individual ST dependencies. + +## The Serialized Type System + +The ST system is XRPL's canonical binary-to-object representation. Every field in a transaction or ledger object has a known wire type and a symbolic name (`SField`). That pairing is the foundation from which all ST types are built. `SField.h` (included transitively) defines the complete set of named fields — `sfAmount`, `sfDestination`, `sfFlags`, and so on — each carrying a `SerializedTypeID` that identifies which ST class holds it on the wire. + +### `STBase` — the polymorphic root + +`STBase` is the abstract superclass for all serialized values. Every instance holds a pointer to its `SField`, which gives the field its name and type code. The virtual interface is minimal: `getSType()`, `add()` (write to a `Serializer`), `getJson()`, `getText()`, `isEquivalent()`, and `isDefault()`. + +Two private virtual methods, `copy(n, buf)` and `move(n, buf)`, implement a *small-buffer optimization* throughout the type tree. When an ST value needs to be copied into a container, the container provides a local buffer of size `n`. If the concrete type fits in that buffer it is placement-new'd there; otherwise it falls back to heap allocation. Every concrete ST class forwards to `STBase::emplace()` for this logic. The `detail::STVar` helper (declared in `STBase.h`, defined elsewhere) wraps one ST value inline, exploiting this protocol so that `STObject`'s internal field vector avoids a separate heap allocation per field for common small types. + +`STBase::operator=` is intentionally asymmetric: it copies the *value* but not the *field name*. This is called out explicitly in a code comment because `std::vector` uses copy-assignment to slide elements down on erase, and if the name were overwritten the field identity would silently change. Callers must use `setFName()` explicitly when name propagation is needed. + +### Scalar leaf types + +`STInteger` (in `STInteger.h`) is a template covering `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t`, and `int32_t`, with convenience aliases `STUInt8`, `STUInt16`, `STUInt32`, `STUInt64`, and `STInt32`. The template is fully header-inline; serialization asserts that the owning field is binary and that its declared type matches the template instantiation. + +`STBitString` handles fixed-width hash/ID values backed by `base_uint`. The four concrete aliases — `STUInt128`, `STUInt160`, `STUInt192`, `STUInt256` — cover account IDs (160-bit), transaction hashes (256-bit), and related digests. A notable comment explains that the template parameter is `int` rather than `unsigned` to work around a gdb RTTI bug that breaks pretty-printers for templates instantiated over unsigned types. + +`STBlob` wraps a variable-length byte sequence in a `Buffer` and exposes it as a `Slice`. It handles the transaction's `SigningPubKey`, `Signature`, memo data, and other variable-length fields. `STAccount` (from `STAccount.h`) is a specialized 160-bit serialized type carrying an `AccountID`. + +### Composite types + +`STObject` (in `STObject.h`) is the workhorse container. It stores an ordered heterogeneous sequence of `detail::STVar` values and optionally binds to an `SOTemplate` that describes which fields are required, optional, or default. The vector is accessed through a `boost::transform_iterator` that strips the `STVar` wrapper and exposes bare `STBase const&` to iterators. `STObject` provides typed accessors like `getFieldU32()`, `setFieldAmount()`, and field-presence queries; the `Proxy`/`ValueProxy`/`OptionalProxy` inner classes give those accessors reference-like semantics so field reads and writes look like value assignments. + +`STArray` (in `STArray.h`) is a flat vector of `STObject`, used for multi-signer lists, memo arrays, and path sets of inner objects. Unlike `STObject`, it holds concrete (not polymorphic) elements, which is why it can use a plain `std::vector` without the `STVar` indirection. + +`STVector256` holds a `std::vector` and is used for directory node pages and similar multi-hash fields. `STPathSet` and `STAmount` are more domain-specific: `STPathSet` encodes XRPL payment path graphs, while `STAmount` carries either XRP drops or an IOU `{currency, issuer, mantissa, exponent}` value in a single type. + +### High-level domain objects + +Three classes derive from `STObject` and represent the primary protocol objects: + +**`STTx`** (in `STTx.h`) is the serialized transaction. It is `final`, not copy-assignable, and caches the transaction ID (`tid_`) and type (`tx_type_`) on construction. It adds sign/verify support (`sign()`, `checkSign()`), handles both single-signature and multi-signature validation against `Rules`, and can produce SQL metadata rows. The `sterilize()` free function round-trips an `STTx` through canonical serialization to ensure all equivalent wire forms produce identical digests. + +**`STLedgerEntry`** (in `STLedgerEntry.h`) wraps an `STObject` as a ledger state entry. Its `key_` (`uint256`) is its position in the `SHAMap` tree; `type_` identifies the entry kind (AccountRoot, Offer, Escrow, etc.). The `SLE` alias is used ubiquitously in the codebase. `setSLEType()` (private) coerces the underlying `STObject` to the correct `SOTemplate` for the given `LedgerEntryType` on deserialization. + +**`STValidation`** (in `STValidation.h`) carries a consensus validation vote. It extends `STObject` with a lazy signature-validity cache (`mutable std::optional valid_`), a trust flag, a `PublicKey`, and a `NodeID`. The constructor accepting a `SerialIter` takes a `lookupNodeID` callable so the caller can resolve ephemeral signing keys to master node identities (manifests). The constructor that creates a new validation signs it inline and verifies all required fields are present before returning. + +`STParsedJSON` (in `STParsedJSON.h`) provides the reverse path: parsing a `Json::Value` tree into an `STObject` or `STArray`, used when transactions arrive over JSON-RPC. + +## Why a single umbrella header + +Most code that processes transactions or ledger entries needs many of these types simultaneously — a transaction contains amounts, account IDs, blob signatures, integer flags, and path sets. Requiring callers to enumerate each include individually would be fragile and create a maintenance burden as new ST subtypes are introduced. `st.h` makes the full type vocabulary available in one line and signals clearly that a translation unit is operating at the full protocol-object level rather than on individual primitives. \ No newline at end of file diff --git a/include/xrpl/protocol/tokens.h.ai.json b/include/xrpl/protocol/tokens.h.ai.json new file mode 100644 index 0000000000..18c41fb4d1 --- /dev/null +++ b/include/xrpl/protocol/tokens.h.ai.json @@ -0,0 +1,161 @@ +{ + "args": [ + { + "lineno": 16, + "name": "T" + }, + { + "lineno": 22, + "name": "T" + }, + { + "lineno": 27, + "name": "T" + } + ], + "classes": [], + "description": "This file provides functions and types for encoding and decoding Base58Check tokens using the XRPL (XRP Ledger) alphabet, supporting both reference and fast implementations, and handling various XRPL token types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol/tokens.h", + "functions": [ + { + "args": [ + "s" + ], + "lineno": 23, + "name": "parseBase58" + }, + { + "args": [ + "type", + "s" + ], + "lineno": 28, + "name": "parseBase58" + }, + { + "args": [ + "type", + "token", + "size" + ], + "lineno": 39, + "name": "encodeBase58Token" + }, + { + "args": [ + "s", + "type" + ], + "lineno": 44, + "name": "decodeBase58Token" + }, + { + "args": [ + "type", + "token", + "size" + ], + "lineno": 51, + "name": "encodeBase58Token" + }, + { + "args": [ + "s", + "type" + ], + "lineno": 54, + "name": "decodeBase58Token" + }, + { + "args": [ + "message", + "size", + "temp", + "temp_size" + ], + "lineno": 59, + "name": "encodeBase58" + }, + { + "args": [ + "s" + ], + "lineno": 62, + "name": "decodeBase58" + }, + { + "args": [ + "token_type", + "input", + "out" + ], + "lineno": 71, + "name": "encodeBase58Token" + }, + { + "args": [ + "type", + "s", + "outBuf" + ], + "lineno": 76, + "name": "decodeBase58Token" + }, + { + "args": [ + "type", + "token", + "size" + ], + "lineno": 80, + "name": "encodeBase58Token" + }, + { + "args": [ + "s", + "type" + ], + "lineno": 83, + "name": "decodeBase58Token" + }, + { + "args": [ + "input", + "out" + ], + "lineno": 88, + "name": "b256_to_b58_be" + }, + { + "args": [ + "input", + "out" + ], + "lineno": 91, + "name": "b58_to_b256_be" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 48, + "name": "b58_ref" + }, + { + "lineno": 57, + "name": "detail" + }, + { + "lineno": 68, + "name": "b58_fast" + }, + { + "lineno": 87, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol/tokens.h.ai.md b/include/xrpl/protocol/tokens.h.ai.md new file mode 100644 index 0000000000..f21a5debe0 --- /dev/null +++ b/include/xrpl/protocol/tokens.h.ai.md @@ -0,0 +1,57 @@ +# `include/xrpl/protocol/tokens.h` + +This header is the public interface for Base58Check encoding and decoding of XRPL cryptographic identifiers. It is the mechanism by which raw byte sequences — account IDs, node keys, seeds — are converted to and from the human-readable strings that appear in XRPL transactions and client APIs (e.g., `rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh` for an account). + +## Token Types and the Version Prefix + +The `TokenType` enum assigns each category of XRPL identifier a specific byte value that is prepended to the payload before Base58Check encoding. These values match the XRPL standard defined at [xrpl.org/base58-encodings.html](https://xrpl.org/base58-encodings.html): + +| Token type | Prefix byte | Typical use | +|-------------------|-------------|------------------------------| +| `AccountID` | 0 | Classic account addresses | +| `AccountPublic` | 35 | Account public keys | +| `AccountSecret` | 34 | Account private keys | +| `NodePublic` | 28 | Validator/peer public keys | +| `NodePrivate` | 32 | Validator/peer private keys | +| `FamilySeed` | 33 | Key-generation seeds | + +Two entries — `None` and `FamilyGenerator` — are marked unused; they exist to reserve their numeric values and prevent accidental reuse. + +The prefix byte is critical: it causes the Base58Check-encoded output to begin with a recognizable letter in the XRPL alphabet, providing a visual cue to the token's type. The 4-byte SHA256-double-hash checksum appended after the payload allows recipients to detect transcription errors before attempting to use a key. + +## Dual-Implementation Architecture + +The header exposes the same encode/decode surface three times under different namespaces, reflecting an intentional portability vs. performance trade-off: + +**Top-level (`xrpl::`)**: `encodeBase58Token` and `decodeBase58Token` are the functions callers should use. Internally they dispatch: on non-MSVC platforms they call `b58_fast`, on MSVC they fall back to `b58_ref`. This dispatch is a compile-time `#ifndef _MSC_VER` guard because the fast path relies on GCC/Clang's `unsigned __int128` extension. + +**`xrpl::b58_ref`**: The reference implementation, adapted from Bitcoin Core. It performs direct base-conversion digit-by-digit (256 → 58 for encoding, 58 → 256 for decoding). While correct and portable, each input byte requires iterating over the entire accumulator buffer, giving O(n²) behaviour that becomes measurable for long keys. + +**`xrpl::b58_fast`** (non-MSVC only): A redesigned algorithm described in detail in the implementation's comment block. Instead of converting directly from base 256 to base 58, it routes through an intermediate base 58^10 representation. The key insight is that 58^10 = 430,804,206,899,405,824 fits in a 64-bit register, so groups of 10 base-58 digits can be processed as a single 64-bit word using native multiplication. Conversions between bases that are powers of one another are trivial concatenations; the expensive multi-precision arithmetic is then performed on far fewer, larger coefficients. This achieves the 10–15× speedup cited in the header comment. + +The fast path's functions take `std::span` parameters and caller-supplied output buffers, avoiding heap allocation in the hot path. The legacy `std::string`-returning overloads (which match the `b58_ref` API) are provided for compatibility but do involve one allocation. + +## Error Handling + +The fast implementation returns `B58Result`, a type alias defined in this header: + +```cpp +template +using B58Result = Expected; +``` + +`Expected` is XRPL's pre-C++23 approximation of `std::expected`, backed by `boost::outcome`. It holds either a value of type `T` or an error of type `E`, and it is marked `[[nodiscard]]` so callers cannot silently drop failure information. + +The error codes are defined in `detail/token_errors.h` via the `TokenCodecErrc` enum, which integrates into the standard `` machinery through a `std::is_error_code_enum` specialization. Relevant codes include `badB58Character` (invalid character in encoded input), `mismatchedTokenType` (prefix byte doesn't match the expected `TokenType`), `mismatchedChecksum` (data corrupted or wrong key), and `outputTooSmall` (caller-supplied buffer insufficient). + +The reference implementation follows the older convention of returning an empty `std::string` on failure, which loses error detail. New call sites should prefer the span-based fast API that propagates typed errors. + +## Low-Level Detail Functions + +Both namespaces expose `detail` sub-namespaces containing the raw base-conversion primitives (`encodeBase58`/`decodeBase58` in `b58_ref::detail`, and `b256_to_b58_be`/`b58_to_b256_be` in `b58_fast::detail`). These are deliberately exposed for unit testing only — they operate on raw byte spans without any token-type prefix or checksum logic, allowing the numeric conversion to be tested independently of the XRPL protocol framing. + +The multi-precision arithmetic helpers in `detail/b58_utils.h` (`carrying_mul`, `inplace_bigint_mul`, `inplace_bigint_div_rem`) are implemented using `unsigned __int128` and are also guarded by `#ifndef _MSC_VER`. Their inline definitions in a header allow the compiler to produce single-instruction multiply/divide sequences on x86-64. + +## Template `parseBase58` + +The header declares two `parseBase58` overloads — one taking only a string, one also taking an explicit `TokenType` — but provides no definition. Definitions exist elsewhere in the codebase as explicit template specializations for concrete XRPL types (such as `AccountID` and public key types). This design keeps the generic interface in a shared header while allowing type-specific parsing logic to live alongside each type's own implementation. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/LedgerEntryBase.h.ai.json b/include/xrpl/protocol_autogen/LedgerEntryBase.h.ai.json new file mode 100644 index 0000000000..54f32484d4 --- /dev/null +++ b/include/xrpl/protocol_autogen/LedgerEntryBase.h.ai.json @@ -0,0 +1,75 @@ +{ + "args": [ + { + "lineno": 27, + "name": "sle" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 22, + "name": "LedgerEntryBase" + } + ], + "description": "Defines a base class LedgerEntryBase for type-safe, immutable wrappers around XRPL ledger entries, providing common field accessors and validation for serialized ledger entries (SLE).", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/LedgerEntryBase.h", + "functions": [ + { + "args": [ + "sle" + ], + "lineno": 27, + "name": "LedgerEntryBase" + }, + { + "args": [], + "lineno": 34, + "name": "validate" + }, + { + "args": [], + "lineno": 48, + "name": "getType" + }, + { + "args": [], + "lineno": 57, + "name": "getKey" + }, + { + "args": [], + "lineno": 68, + "name": "getLedgerIndex" + }, + { + "args": [], + "lineno": 80, + "name": "hasLedgerIndex" + }, + { + "args": [], + "lineno": 90, + "name": "getLedgerEntryType" + }, + { + "args": [], + "lineno": 100, + "name": "getFlags" + }, + { + "args": [], + "lineno": 110, + "name": "getSle" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/LedgerEntryBase.h.ai.md b/include/xrpl/protocol_autogen/LedgerEntryBase.h.ai.md new file mode 100644 index 0000000000..c37b160f22 --- /dev/null +++ b/include/xrpl/protocol_autogen/LedgerEntryBase.h.ai.md @@ -0,0 +1,38 @@ +# `LedgerEntryBase.h` — Base Class for Type-Safe Ledger Entry Wrappers + +## Role in the System + +`LedgerEntryBase` is the hand-maintained root of the autogenerated ledger entry class hierarchy in the `xrpl::ledger_entries` namespace. It lives in `include/xrpl/protocol_autogen/` alongside its counterpart `LedgerEntryBuilderBase.h`, and every generated wrapper class — `AccountRoot`, `Offer`, `RippleState`, and roughly two dozen others — extends it directly. + +The `protocol_autogen` directory is populated at CMake configure time by Python scripts that parse `include/xrpl/protocol/detail/ledger_entries.macro` and emit typed wrapper headers via Mako templates. The base classes (`LedgerEntryBase.h`, `LedgerEntryBuilderBase.h`) are explicitly excluded from code generation and must be maintained by hand; the README in the same directory documents this obligation. + +## Design: Immutable Wrapper over SLE + +The class owns a `std::shared_ptr` — a shared, read-only pointer to a `STLedgerEntry` (the serialized ledger object that lives in the state map). Taking a `shared_ptr` by value in the constructor rather than a raw reference reflects real ownership semantics: `LedgerEntryBase` keeps the underlying entry alive as long as any wrapper referencing it exists, and the `const` qualifier on `SLE` ensures that no write path can sneak through the base class. Mutation is intentionally delegated to the companion `LedgerEntryBuilderBase` CRTP template, which works with a mutable `STObject` and produces a finished wrapper only when `build(index)` is called. + +This separation of read wrappers from mutable builders is an intentional API contract: code that receives a `LedgerEntryBase&` (or any derived type) has a compile-time guarantee that it cannot modify the ledger state. + +## Field Accessors and the Required/Optional Split + +The base class exposes the three fields shared by every ledger entry format: + +- `getLedgerEntryType()` and `getFlags()` are required (`soeREQUIRED`) fields, so their getters return plain values and do not check presence. +- `getLedgerIndex()` is optional and follows the `get*/has*` accessor pattern used throughout the generated subclasses: `getLedgerIndex()` returns `std::optional`, and `hasLedgerIndex()` checks presence without extracting the value. + +There is a subtle but important distinction between `getKey()` and `getLedgerIndex()`. `getKey()` delegates to `sle_->key()`, which is the 256-bit identifier used as the actual key in the ledger state map — it is always present and is not a serialized field. `getLedgerIndex()` reads the `sfLedgerIndex` serialized field, which is an optional field that may or may not appear in the wire-format bytes. Code that needs to locate the entry in the ledger trie must use `getKey()`; code reading a client-facing JSON representation of the entry typically encounters `getLedgerIndex`. + +## Validation + +`validate()` performs a two-step check. It first reads `sfLedgerEntryType` as a `uint16_t`, downcasts it to `LedgerEntryType`, and uses `LedgerFormats::getInstance().findByType(...)` to retrieve the registered `SOTemplate` for that type. It then delegates to `protocol_autogen::validateSTObject()`, which iterates the template's field list and returns `false` if any `soeREQUIRED` field is absent, or if a field marked `soeMPTNotSupported` contains an MPT-denominated amount or issue. The `// LCOV_EXCL_LINE` annotation on the early-return guard for a missing `sfLedgerEntryType` signals that this branch is unreachable under normal conditions — a well-formed SLE always carries its type — but the check is retained as a defensive invariant. + +## Protected Member and Subclass Access + +`sle_` is `protected` rather than private so that generated subclasses can write their field getters compactly as `this->sle_->at(sfBalance)` without indirection through a virtual or accessor method. This is a deliberate performance and readability tradeoff: the entire class hierarchy is non-polymorphic and final in usage, so virtual dispatch would add overhead for no benefit, and requiring subclasses to go through a base-class accessor for every field read would bloat the generated code considerably. + +## Escape Hatch + +`getSle()` returns the `shared_ptr` directly. It exists for integration points where strongly typed wrappers haven't been adopted yet — transaction processing code, for example, that still works with raw `SLE` objects can receive a wrapper, call `getSle()`, and continue. The expectation is that these callsites migrate over time. + +## Relationship to `Utils.h` + +The `Optional` alias defined in `Utils.h` handles a subtle C++ issue: when the underlying `SLE::at()` returns a reference type, `std::optional` is ill-formed. `Optional` conditionally decays to `std::optional>` when `T` is a reference, making optional-return accessors uniform across both value and reference-typed fields in the generated subclasses. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h.ai.json b/include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h.ai.json new file mode 100644 index 0000000000..e52976f0e9 --- /dev/null +++ b/include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h.ai.json @@ -0,0 +1,70 @@ +{ + "args": [ + { + "lineno": 21, + "name": "ledgerEntryType" + }, + { + "lineno": 21, + "name": "flags" + }, + { + "lineno": 52, + "name": "value" + }, + { + "lineno": 62, + "name": "value" + } + ], + "classes": [ + { + "args": [], + "lineno": 15, + "name": "LedgerEntryBuilderBase" + } + ], + "description": "Defines a base template class for building XRPL ledger entries, providing common field setters and validation for derived ledger entry builder types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "LedgerEntryBuilderBase" + }, + { + "args": [ + "SF_UINT16::type::value_type ledgerEntryType", + "SF_UINT32::type::value_type flags" + ], + "lineno": 20, + "name": "LedgerEntryBuilderBase" + }, + { + "args": [], + "lineno": 36, + "name": "validate" + }, + { + "args": [ + "uint256 const& value" + ], + "lineno": 51, + "name": "setLedgerIndex" + }, + { + "args": [ + "uint32_t value" + ], + "lineno": 61, + "name": "setFlags" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h.ai.md b/include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h.ai.md new file mode 100644 index 0000000000..aaeb73276a --- /dev/null +++ b/include/xrpl/protocol_autogen/LedgerEntryBuilderBase.h.ai.md @@ -0,0 +1,42 @@ +# `LedgerEntryBuilderBase.h` — CRTP Base for Auto-Generated Ledger Entry Builders + +## Role in the System + +`LedgerEntryBuilderBase` is the CRTP (Curiously Recurring Template Pattern) base class shared by every auto-generated ledger entry builder in the `xrpl::ledger_entries` namespace. It lives in the `protocol_autogen` module alongside its read-only counterpart, `LedgerEntryBase`, and is instantiated by code generated from the Mako template at `scripts/templates/LedgerEntry.h.mako`. For every concrete ledger entry type — `AccountRoot`, `Offer`, `Escrow`, and so on — the code generator emits a `{Name}Builder` that extends this base. + +The class solves a narrow but important problem: how to build a well-formed `STObject` that can be promoted into an `SLE` (Serialized Ledger Entry) without running into the template-enforcement machinery before all required fields are present. + +## The `soeDEFAULT` Pitfall and the "Free Object" Strategy + +The most critical design decision is captured in the constructor comment: `object_.set(soTemplate)` is **deliberately not called**. In the XRPL serialization layer, calling `set(soTemplate)` on an `STObject` populates placeholder `STBase` entries for every field marked `soeDEFAULT`. When the `SLE` constructor later calls `applyTemplate()` on the incoming object and finds those placeholders already in place, it throws a `"may not be explicitly set to default"` exception — even though the placeholders were inserted by the framework, not by user code. + +By keeping `object_` as a "free object" (no template applied), the builder accumulates only the fields the caller explicitly sets. The `SLE` constructor — called from the derived builder's `build(index)` method as `std::make_shared(std::move(object_), index)` — then calls `applyTemplate()` once, cleanly, on a clean object with no spurious placeholders. The `TransactionBuilderBase` class in the same module carries an identical comment and uses the same strategy, confirming this is an intentional pattern for all auto-generated builders. + +## CRTP Method Chaining + +The template parameter `Derived` enables compile-time fluent APIs without virtual dispatch. Every setter in the base class returns `static_cast(*this)`, so callers can chain calls on the concrete type: + +```cpp +AccountRootBuilder(account, seq, balance, ownerCount, prevTxnID, prevTxnLgrSeq) + .setRegularKey(key) + .setFlags(lsfDefaultRipple) + .build(index); +``` + +The cast is always safe because `Derived` is constrained by the CRTP contract — only `AccountRootBuilder` can instantiate `LedgerEntryBuilderBase`. The alternative of returning `LedgerEntryBuilderBase&` would force callers to downcast after each base-class setter. + +## What the Base Class Handles + +The base provides exactly the fields that are universal to all ledger entries — the two fields that every `SOTemplate` shares regardless of entry type: `sfLedgerIndex` (optional, via `setLedgerIndex`) and `sfFlags` (required, pre-initialized to zero in the constructor via `setFlags`). The constructor also writes `sfLedgerEntryType` into the free object immediately, using the discriminant passed by the derived class, so the object is always self-describing from the first line. + +The `protected` member `object_` is an `STObject` initialized with the `sfLedgerEntry` field descriptor. Being `protected` rather than private lets generated derived classes write `object_[sfSomeField] = value;` directly without routing through the base class interface — this is intentional for performance and code-generation simplicity. + +## Validation + +`validate()` performs two checks. First, it confirms `sfLedgerEntryType` is present (the `LCOV_EXCL_LINE` annotation on the failure path signals that this branch is unreachable in practice, since the constructor always sets it). Second, it resolves the entry type to an `SOTemplate` by consulting the `LedgerFormats` singleton and delegates to `protocol_autogen::validateSTObject()`, the inline function in `STObjectValidation.h`. That helper iterates the template and confirms all `soeREQUIRED` fields are present, and also enforces MPT (Multi-Purpose Token) compatibility restrictions — fields marked `soeMPTNotSupported` must not hold an `MPTIssue` variant in their amount or issue types. + +## Relationship to `LedgerEntryBase` + +`LedgerEntryBase` is the immutable read side of the same abstraction: it wraps a `shared_ptr` and exposes typed getters. `LedgerEntryBuilderBase` is the mutable write side: it accumulates state in a plain `STObject` and materializes an `SLE` only when `build(index)` is called on the derived class. The two classes mirror each other's `validate()` implementation exactly — both call `validateSTObject` against the same `LedgerFormats`-sourced template. + +The separation means the type-safe read wrappers (`AccountRoot`, `Offer`, etc.) are always backed by an immutable, fully validated `SLE`, while the builders are transient construction aids that are consumed and discarded at the `build()` call site. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/STObjectValidation.h.ai.json b/include/xrpl/protocol_autogen/STObjectValidation.h.ai.json new file mode 100644 index 0000000000..2576ea62b4 --- /dev/null +++ b/include/xrpl/protocol_autogen/STObjectValidation.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 9, + "name": "obj" + }, + { + "lineno": 9, + "name": "format" + } + ], + "classes": [], + "description": "Provides a function to validate an STObject against a given SOTemplate, ensuring required fields are present and certain field constraints are met.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/STObjectValidation.h", + "functions": [ + { + "args": [ + "obj", + "format" + ], + "lineno": 7, + "name": "validateSTObject" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl::protocol_autogen" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/STObjectValidation.h.ai.md b/include/xrpl/protocol_autogen/STObjectValidation.h.ai.md new file mode 100644 index 0000000000..7c3b81d6ba --- /dev/null +++ b/include/xrpl/protocol_autogen/STObjectValidation.h.ai.md @@ -0,0 +1,33 @@ +# `STObjectValidation.h` — Schema-Conformance Check for Protocol Autogen Wrappers + +## Role in the System + +This header lives in the `include/xrpl/protocol_autogen/` module, which contains auto-generated type-safe C++ wrapper classes for every XRPL transaction and ledger-entry type. It is one of the few hand-maintained files in that directory, providing a shared validation primitive used by both `TransactionBase` and `LedgerEntryBase` — the base classes from which all generated wrappers inherit. + +`validateSTObject` answers a focused question: does a given `STObject` conform to its declared `SOTemplate`? Both required-field presence and MPT (Multi-Purpose Token) asset-type constraints are enforced here in a single pass. + +## What `validateSTObject` Does + +The function takes a constant `STObject` reference and a `SOTemplate` (the canonical schema for the object type) and iterates every `SOElement` in the template. Two independent checks apply to each element: + +**Required-field presence.** If the template marks a field as `soeREQUIRED` and that field is absent from the object, the function returns `false` immediately. Optional (`soeOPTIONAL`) and defaulted (`soeDEFAULT`) fields are silently ignored when absent. + +**MPT exclusion.** When a field is decorated with `soeMPTNotSupported`, the function checks whether that field's value contains an MPT issue. Only two serialized field types can carry an `MPTIssue`: `STI_AMOUNT` and `STI_ISSUE`. For `STI_AMOUNT`, the function retrieves the amount and inspects its asset via `amount.asset().holds()`. For `STI_ISSUE`, it obtains a typed pointer via `obj.peekAtPField()` and then calls `issue->holds()`. A `nullptr` result from the cast is treated as a validation failure to defend against unexpected field polymorphism. + +If all elements pass both checks, the function returns `true`. + +## Design Decisions + +**Why `soeMPTNotSupported` lives in the template, not the validator.** The decision to accept MPT or not is a per-field property of a specific transaction/ledger-entry schema, not a global rule. Embedding this flag inside `SOElement` (via `SOETxMPTIssue`) keeps the constraint co-located with the schema definition, so the validator never needs to know *which* transaction type it is examining. Adding or removing MPT support for a field is a one-line change in the macro definition files. + +**`LCOV_EXCL_LINE` on every `return false` path.** The autogenerated builder classes in this same module are designed to make it impossible for a well-formed object to reach these failure paths at all — required fields are enforced in constructors, and MPT-incompatible amounts cannot be set on restricted fields via the builder API. The exclusion markers acknowledge that these paths are theoretically reachable (e.g., if someone bypasses the builder and constructs an `STObject` directly) but not exercised in normal test coverage. + +**Inline in a header.** The function body is trivial — a single loop with a few comparisons — so making it `inline` avoids the overhead of a separate compilation unit while still being centrally defined rather than duplicated in each base class. + +**`[[nodiscard]]`** is applied to enforce that callers actually inspect the boolean result rather than discarding it silently. + +## Callers + +`TransactionBase::validate()` calls `validateSTObject` against the `SOTemplate` retrieved from `TxFormats::getInstance()` for the transaction's type before delegating to `passesLocalChecks`. `LedgerEntryBase::validate()` calls it against the template from `LedgerFormats::getInstance()`. Both patterns look up the template dynamically by type ID, so the same validator handles every known transaction and ledger-entry variant without specialization. + +This tight coupling means `STObjectValidation.h` is both the semantic gatekeeper and the single point of extension: if new field constraint types are added to `SOElement` in the future, this function is where their enforcement logic should land. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/TransactionBase.h.ai.json b/include/xrpl/protocol_autogen/TransactionBase.h.ai.json new file mode 100644 index 0000000000..7cef4051e4 --- /dev/null +++ b/include/xrpl/protocol_autogen/TransactionBase.h.ai.json @@ -0,0 +1,196 @@ +{ + "args": [ + { + "lineno": 22, + "name": "tx" + }, + { + "lineno": 29, + "name": "reason" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 18, + "name": "TransactionBase" + } + ], + "description": "Defines TransactionBase, a base class providing type-safe, read-only accessors for common XRPL transaction fields, wrapping an immutable STTx object and offering validation and field access methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/TransactionBase.h", + "functions": [ + { + "args": [ + "tx" + ], + "lineno": 22, + "name": "TransactionBase" + }, + { + "args": [ + "reason" + ], + "lineno": 29, + "name": "validate" + }, + { + "args": [], + "lineno": 51, + "name": "getTransactionType" + }, + { + "args": [], + "lineno": 62, + "name": "getAccount" + }, + { + "args": [], + "lineno": 73, + "name": "getSequence" + }, + { + "args": [], + "lineno": 84, + "name": "getFee" + }, + { + "args": [], + "lineno": 95, + "name": "getSigningPubKey" + }, + { + "args": [], + "lineno": 106, + "name": "getFlags" + }, + { + "args": [], + "lineno": 117, + "name": "hasFlags" + }, + { + "args": [], + "lineno": 128, + "name": "getSourceTag" + }, + { + "args": [], + "lineno": 139, + "name": "hasSourceTag" + }, + { + "args": [], + "lineno": 150, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 161, + "name": "hasPreviousTxnID" + }, + { + "args": [], + "lineno": 172, + "name": "getLastLedgerSequence" + }, + { + "args": [], + "lineno": 183, + "name": "hasLastLedgerSequence" + }, + { + "args": [], + "lineno": 194, + "name": "getAccountTxnID" + }, + { + "args": [], + "lineno": 205, + "name": "hasAccountTxnID" + }, + { + "args": [], + "lineno": 216, + "name": "getOperationLimit" + }, + { + "args": [], + "lineno": 227, + "name": "hasOperationLimit" + }, + { + "args": [], + "lineno": 238, + "name": "getMemos" + }, + { + "args": [], + "lineno": 249, + "name": "hasMemos" + }, + { + "args": [], + "lineno": 260, + "name": "getTicketSequence" + }, + { + "args": [], + "lineno": 271, + "name": "hasTicketSequence" + }, + { + "args": [], + "lineno": 282, + "name": "getTxnSignature" + }, + { + "args": [], + "lineno": 293, + "name": "hasTxnSignature" + }, + { + "args": [], + "lineno": 304, + "name": "getSigners" + }, + { + "args": [], + "lineno": 315, + "name": "hasSigners" + }, + { + "args": [], + "lineno": 326, + "name": "getNetworkID" + }, + { + "args": [], + "lineno": 337, + "name": "hasNetworkID" + }, + { + "args": [], + "lineno": 348, + "name": "getDelegate" + }, + { + "args": [], + "lineno": 359, + "name": "hasDelegate" + }, + { + "args": [], + "lineno": 370, + "name": "getSTTx" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/TransactionBase.h.ai.md b/include/xrpl/protocol_autogen/TransactionBase.h.ai.md new file mode 100644 index 0000000000..a33fd09ebc --- /dev/null +++ b/include/xrpl/protocol_autogen/TransactionBase.h.ai.md @@ -0,0 +1,43 @@ +# `TransactionBase.h` — Immutable Base for Auto-Generated Transaction Wrappers + +`TransactionBase` is the hand-authored root of the `xrpl::transactions` type hierarchy. It lives inside the `protocol_autogen` module, which generates a separate strongly-typed C++ class for every XRPL transaction kind (over seventy at present: `Payment`, `AMMBid`, `EscrowCreate`, etc.). Those concrete classes are produced at CMake configure time from `.macro` definition files via Mako templates and Python scripts, but `TransactionBase` itself is intentionally **not** generated — it is maintained by hand and listed in the README as a file that must be updated when new universal transaction fields are added. + +## Role in the System + +The class solves a specific coupling problem: `STTx` is the protocol's serialization-level representation of a transaction. It stores fields as a dynamic bag accessed via `sfXxx` identifiers, returns values by name at runtime, and throws on missing required fields. That interface is correct and necessary for the protocol engine, but it is fragile for higher-level application code because nothing in the C++ type system prevents accessing a field that doesn't belong to a given transaction type, or forgetting to check whether an optional field is present before reading it. + +`TransactionBase` wraps `STTx` behind a typed, read-only facade. The concrete subclass `Payment` inherits from it and adds `getDestination()`, `getAmount()`, etc.; everything in `TransactionBase` is available to every transaction type by virtue of inheritance. This mirrors exactly what `LedgerEntryBase` does for `SLE` (serialized ledger entry) objects in the parallel `xrpl::ledger_entries` namespace. + +## Ownership Model + +The constructor takes a `std::shared_ptr` by value and moves it into the protected `tx_` member. Wrapping a `const` `STTx` via `shared_ptr` has two consequences: first, there are no setters anywhere in `TransactionBase` or its subclasses — mutation goes through the companion `TransactionBuilderBase` and the generated `PaymentBuilder`, `AMMBidBuilder`, etc., which construct a fresh `STTx` and hand a `shared_ptr` to the read wrapper's constructor. Second, the `shared_ptr` allows multiple `TransactionBase`-derived objects to cheaply share ownership of the same underlying transaction without copies, with thread-safe reference counting. + +`getSTTx()` returns that same `shared_ptr` as an explicit escape hatch for callers that need to pass the raw transaction to protocol-engine APIs that have not yet been wrapped. + +## Field Accessor Pattern + +Required fields — `sfAccount`, `sfSequence`, `sfFee`, `sfSigningPubKey` — are exposed with plain value-returning getters that call `tx_->at(sfXxx)` directly. These will throw if the field is somehow absent, but since an `STTx` with a missing required field cannot survive deserialization or pass validation, the throw is treated as a programming error. + +Optional fields follow a uniform dual-accessor pattern: `getX()` returns `std::optional` (or `std::nullopt` when absent), and `hasX()` returns `bool`. This lets callers avoid the `std::optional::value()` overhead when they only need to check presence, and avoids unintentional default-construction of values. + +Array-typed optional fields — `getMemos()` and `getSigners()` — return `std::optional>` rather than `std::optional`. This is the correct choice because `STArray` is not cheaply copyable; returning a `reference_wrapper` avoids the copy while preserving optional semantics. Callers must be aware that the reference is only valid for the lifetime of the `TransactionBase` object (and by extension, the underlying `STTx`). + +## Validation Logic + +`validate(std::string& reason)` performs a two-stage check: + +1. **Schema conformance** via `protocol_autogen::validateSTObject`, which iterates the `SOTemplate` retrieved from `TxFormats::getInstance()` and verifies that every `soeREQUIRED` field is present and that fields marked `soeMPTNotSupported` do not carry MPT (Multi-Purpose Token) amounts or issues. This check is exclusively a guard against bugs in the code-generation pipeline — valid `STTx` objects constructed through the normal path will always pass, which is why the failure branch is annotated `LCOV_EXCL_START`/`LCOV_EXCL_STOP` to exclude it from coverage metrics. + +2. **Local checks** via `passesLocalChecks`, which enforces network-submission rules (signature presence, fee sanity, etc.). This stage is skipped for pseudo-transactions (identified by `isPseudoTx`) because those are injected internally by the consensus process and are never signed or submitted externally. + +## Notable Fields + +`sfTicketSequence` and its accessor `getTicketSequence()` deserve mention because when a ticket is consumed the regular `sfSequence` is set to `0` — the two concepts are mutually exclusive in practice, which is enforced in `TransactionBuilderBase::setTicketSequence()`. + +`sfNetworkID` guards against cross-network replay: a transaction signed for one XRPL sidechain is rejected by nodes running a different network ID. Exposing it through the base class ensures every transaction type can be inspected for network origin without downcasting. + +`sfDelegate` is a newer field that supports delegated transaction submission, allowing one account to act on behalf of another under protocol-enforced permission grants. + +## Relationship to Generated Classes + +Every generated transaction header (e.g., `transactions/Payment.h`) begins with `// This file is auto-generated. Do not edit.`, includes `TransactionBase.h` and `TransactionBuilderBase.h`, and defines exactly two classes: a read wrapper inheriting `TransactionBase` (which adds `getXxx()` accessors for type-specific fields and a `static constexpr TxType txType` discriminator), and a Builder inheriting `TransactionBuilderBase`. The Builder's `build()` method calls the protected `sign()` helper from `TransactionBuilderBase`, wraps the result in a `shared_ptr`, and constructs the read wrapper. This clean separation between mutation (Builder) and observation (`TransactionBase` subclass) is the central architectural commitment of the entire `protocol_autogen` layer. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/TransactionBuilderBase.h.ai.json b/include/xrpl/protocol_autogen/TransactionBuilderBase.h.ai.json new file mode 100644 index 0000000000..84921af241 --- /dev/null +++ b/include/xrpl/protocol_autogen/TransactionBuilderBase.h.ai.json @@ -0,0 +1,216 @@ +{ + "args": [ + { + "lineno": 22, + "name": "transactionType" + }, + { + "lineno": 23, + "name": "account" + }, + { + "lineno": 24, + "name": "sequence" + }, + { + "lineno": 25, + "name": "fee" + }, + { + "lineno": 45, + "name": "value" + }, + { + "lineno": 55, + "name": "value" + }, + { + "lineno": 65, + "name": "value" + }, + { + "lineno": 75, + "name": "value" + }, + { + "lineno": 86, + "name": "value" + }, + { + "lineno": 96, + "name": "value" + }, + { + "lineno": 106, + "name": "value" + }, + { + "lineno": 116, + "name": "value" + }, + { + "lineno": 126, + "name": "value" + }, + { + "lineno": 136, + "name": "value" + }, + { + "lineno": 146, + "name": "value" + }, + { + "lineno": 156, + "name": "value" + }, + { + "lineno": 166, + "name": "value" + }, + { + "lineno": 176, + "name": "value" + }, + { + "lineno": 197, + "name": "publicKey" + }, + { + "lineno": 197, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "TransactionBuilderBase()", + "TransactionBuilderBase(SF_UINT16::type::value_type transactionType, SF_ACCOUNT::type::value_type account, std::optional sequence, std::optional fee)" + ], + "lineno": 18, + "name": "TransactionBuilderBase" + } + ], + "description": "Defines a base template class for building XRPL transactions, providing common field setters and signing functionality for all transaction types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/TransactionBuilderBase.h", + "functions": [ + { + "args": [ + "AccountID const& value" + ], + "lineno": 44, + "name": "setAccount" + }, + { + "args": [ + "STAmount const& value" + ], + "lineno": 54, + "name": "setFee" + }, + { + "args": [ + "std::uint32_t const& value" + ], + "lineno": 64, + "name": "setSequence" + }, + { + "args": [ + "std::uint32_t const& value" + ], + "lineno": 74, + "name": "setTicketSequence" + }, + { + "args": [ + "std::uint32_t const& value" + ], + "lineno": 85, + "name": "setFlags" + }, + { + "args": [ + "std::uint32_t const& value" + ], + "lineno": 95, + "name": "setSourceTag" + }, + { + "args": [ + "std::uint32_t const& value" + ], + "lineno": 105, + "name": "setLastLedgerSequence" + }, + { + "args": [ + "uint256 const& value" + ], + "lineno": 115, + "name": "setAccountTxnID" + }, + { + "args": [ + "uint256 const& value" + ], + "lineno": 125, + "name": "setPreviousTxnID" + }, + { + "args": [ + "std::uint32_t const& value" + ], + "lineno": 135, + "name": "setOperationLimit" + }, + { + "args": [ + "STArray const& value" + ], + "lineno": 145, + "name": "setMemos" + }, + { + "args": [ + "STArray const& value" + ], + "lineno": 155, + "name": "setSigners" + }, + { + "args": [ + "std::uint32_t const& value" + ], + "lineno": 165, + "name": "setNetworkID" + }, + { + "args": [ + "AccountID const& value" + ], + "lineno": 175, + "name": "setDelegate" + }, + { + "args": [], + "lineno": 185, + "name": "getSTObject" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 195, + "name": "sign" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 15, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/TransactionBuilderBase.h.ai.md b/include/xrpl/protocol_autogen/TransactionBuilderBase.h.ai.md new file mode 100644 index 0000000000..24cf0e7124 --- /dev/null +++ b/include/xrpl/protocol_autogen/TransactionBuilderBase.h.ai.md @@ -0,0 +1,73 @@ +# `TransactionBuilderBase.h` — CRTP Base for Auto-generated Transaction Builders + +## Role and Context + +`TransactionBuilderBase` is the shared ancestor for all 85 auto-generated transaction builder classes in the `xrpl::transactions` namespace. It lives in `include/xrpl/protocol_autogen/`, a directory populated at CMake configure time by `scripts/generate_tx_classes.py`. Derived builders such as `PaymentBuilder` and `AccountSetBuilder` each inherit from `TransactionBuilderBase` via the Curiously Recurring Template Pattern (CRTP), gaining a full set of common-field setters without any virtual dispatch overhead. + +The file is the counterpart to `TransactionBase.h`, which provides immutable read accessors over a finished `STTx`. The split is intentional: mutable construction state lives entirely in the builder; once `build()` is called on a derived class the object is moved into an `STTx` and ownership transfers to the immutable wrapper. + +## Core Design: CRTP and the "Free Object" Invariant + +The template parameter `Derived` is the concrete builder class. Every setter casts `*this` to `Derived&` before returning, so call chains like + +```cpp +PaymentBuilder builder(account, dest, amount); +builder.setFee(drops).setLastLedgerSequence(seq).setMemos(memos); +``` + +stay typed as `PaymentBuilder&` throughout — no slicing, no intermediate base references. + +The single data member is: + +```cpp +STObject object_{sfTransaction}; +``` + +This constructs a *free* `STObject` tagged with the `sfTransaction` SField but **without** an `SOTemplate` bound to it. The constructor comment explains why this matters: calling `object_.set(soTemplate)` would cause `STObject` to pre-create `STBase` placeholder entries for every `soeDEFAULT` field in the transaction's schema. Later, when `STTx(STObject&&)` calls `applyTemplate()` internally, those pre-created defaults would trigger the exception "may not be explicitly set to default". By keeping the object free, the builder accumulates only the fields actually set by the caller; `applyTemplate()` then inserts defaults for everything else cleanly during `STTx` construction. + +## Constructor + +The four-parameter constructor initialises the mandatory fields common to every XRPL transaction: `sfTransactionType` (the `uint16` discriminator), `sfAccount`, and the two optional-but-nearly-universal fields `sfSequence` and `sfFee`. Derived constructors forward their required arguments here and then set their own type-specific fields via `object_[sfXxx] = value`. + +## Common Field Setters + +All setters follow the same pattern — assign a field on `object_` and return `static_cast(*this)` — so there is nothing architecturally surprising about most of them. The one exception worth highlighting is `setTicketSequence()`: + +```cpp +object_[sfSequence] = 0u; +object_[sfTicketSequence] = value; +``` + +The XRPL protocol requires that when a ticket is consumed, the transaction's regular sequence number must be exactly 0. `setTicketSequence()` enforces this invariant in one atomic call rather than leaving the caller to remember to also zero out `sfSequence`. + +`setMemos()` and `setSigners()` use `object_.setFieldArray()` instead of the `operator[]` shorthand because `STArray` fields require the dedicated accessor path. + +`setDelegate()` sets `sfDelegate`, which enables delegated transaction submission — a relatively recent addition allowing one account to act on behalf of another with explicit permission. + +## The `sign()` Method + +`sign()` is `protected`, so only derived builders can expose it (typically through a public `build()` method). Its signing pipeline follows the XRPL specification precisely: + +1. Write the public key into `sfSigningPubKey` as a variable-length blob. +2. Create a `Serializer`, prepend `HashPrefix::txSign` (a four-byte domain-separation prefix), and append the serialized object *excluding* its signing fields via `addWithoutSigningFields()`. +3. Call `xrpl::sign(publicKey, secretKey, s.slice())` to produce the ECDSA/Ed25519 signature. +4. Write the signature into `sfTxnSignature`. + +This ordering — public key first, signature last — matches the order in which the fields appear in the canonical XRPL binary serialisation, which is important because `STTx` validates the signature immediately on construction when given a populated `STObject`. + +## Relationship to `STTx` and the Builder Terminal + +Derived builders expose a `build()` method such as: + +```cpp +Payment build(PublicKey const& pk, SecretKey const& sk) { + sign(pk, sk); + return Payment{std::make_shared(std::move(object_))}; +} +``` + +`STTx(STObject&&)` moves the free object in, runs `applyTemplate()` to validate and fill defaults, and computes the transaction hash. From that point the `STObject` is consumed and the builder is invalid. The returned `Payment` (a `TransactionBase` subclass) holds a `shared_ptr` and provides only read accessors — enforcing immutability for signed, finalised transactions. + +## Parallel with `LedgerEntryBuilderBase` + +`LedgerEntryBuilderBase` in the same directory mirrors this design exactly for ledger-entry construction. Both share the "free object" invariant, both use CRTP for chainable setters, and both delegate the actual template application to the downstream type (`STTx` / `STLedgerEntry`). The parallel structure reflects that the two hierarchies were generated by sibling scripts and are maintained together. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/Utils.h.ai.json b/include/xrpl/protocol_autogen/Utils.h.ai.json new file mode 100644 index 0000000000..8dd35625fd --- /dev/null +++ b/include/xrpl/protocol_autogen/Utils.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines a type alias 'Optional' in the xrpl::protocol_autogen namespace that adapts std::optional for both value and reference types using type traits.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/Utils.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl::protocol_autogen" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/Utils.h.ai.md b/include/xrpl/protocol_autogen/Utils.h.ai.md new file mode 100644 index 0000000000..cb9655b063 --- /dev/null +++ b/include/xrpl/protocol_autogen/Utils.h.ai.md @@ -0,0 +1,50 @@ +# `include/xrpl/protocol_autogen/Utils.h` + +## Purpose + +This file provides a single utility type alias for the `xrpl::protocol_autogen` namespace: a reference-aware `Optional` wrapper. It exists to support the code generator that produces type-safe wrappers for every XRPL transaction and ledger entry type — so that generated getter methods can uniformly declare their return types without the generator needing to know whether a given field's value type is a reference or a plain value. + +## The `Optional` Alias + +```cpp +template +using Optional = std::conditional_t< + std::is_reference_v, + std::optional>>, + std::optional>; +``` + +The core problem it solves is a language restriction: `std::optional` is ill-formed in C++. If a code generator naively emits `std::optional` and `ValueType` turns out to be a reference type, the result is a hard compilation error. `Optional` sidesteps this by branching on `std::is_reference_v` at compile time: + +- **Value types** (`std::is_reference_v` is false): the alias collapses to a plain `std::optional`, identical to what one would write by hand. +- **Reference types** (`std::is_reference_v` is true): the alias strips the reference with `std::remove_reference_t` and wraps the result in `std::reference_wrapper`, producing `std::optional>>`. This is the idiomatic C++ pattern for an optional reference. + +## How It Is Used in Practice + +The autogenerated transaction and ledger entry classes — found under `protocol_autogen/transactions/` and `protocol_autogen/ledger_entries/` — use `protocol_autogen::Optional` as the return type for every optional field getter. For example, in `AMM.h`: + +```cpp +protocol_autogen::Optional +getPreviousTxnID() const; + +protocol_autogen::Optional +getPreviousTxnLgrSeq() const; +``` + +And in `Payment.h`: + +```cpp +protocol_autogen::Optional +getSendMax() const; + +protocol_autogen::Optional +getCredentialIDs() const; +``` + +In all these cases today, the `SField` value types are plain value types (`STAmount`, `uint256`, `uint32_t`, etc.), so `Optional` resolves to `std::optional`. The reference-branch exists as a forward-compatibility and correctness guarantee: if any `SField` specialization ever uses a reference as its `value_type`, the generated code will still compile and behave correctly without any change to the generator's output templates. + +## Design Rationale + +The handwritten base classes (`TransactionBase`, `LedgerEntryBase`) manage truly "untyped" fields — `STArray`, `STPathSet` — by directly writing `std::optional>` because those field access methods are authored manually and the author can choose the appropriate form. In contrast, the code generator for the typed subclasses cannot easily classify each field's value type at generation time, so `Optional` abstracts that distinction away entirely. The generator emits `protocol_autogen::Optional` uniformly and the type system handles the rest. + +This keeps the generator logic simple and the generated headers correct by construction, with zero runtime overhead — `std::conditional_t` is fully resolved at compile time and the resulting type is binary-identical to a hand-authored `std::optional` or `std::optional>`. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/AMM.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/AMM.h.ai.json new file mode 100644 index 0000000000..1b1e36c897 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/AMM.h.ai.json @@ -0,0 +1,284 @@ +{ + "args": [ + { + "lineno": 32, + "name": "sle" + }, + { + "lineno": 210, + "name": "account" + }, + { + "lineno": 210, + "name": "lPTokenBalance" + }, + { + "lineno": 210, + "name": "asset" + }, + { + "lineno": 210, + "name": "asset2" + }, + { + "lineno": 210, + "name": "ownerNode" + }, + { + "lineno": 221, + "name": "sle" + }, + { + "lineno": 234, + "name": "value" + }, + { + "lineno": 244, + "name": "value" + }, + { + "lineno": 254, + "name": "value" + }, + { + "lineno": 264, + "name": "value" + }, + { + "lineno": 274, + "name": "value" + }, + { + "lineno": 284, + "name": "value" + }, + { + "lineno": 294, + "name": "value" + }, + { + "lineno": 304, + "name": "value" + }, + { + "lineno": 314, + "name": "value" + }, + { + "lineno": 324, + "name": "value" + }, + { + "lineno": 334, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 25, + "name": "AMM" + }, + { + "args": [ + "account", + "lPTokenBalance", + "asset", + "asset2", + "ownerNode" + ], + "lineno": 200, + "name": "AMMBuilder" + } + ], + "description": "Defines the AMM (Automated Market Maker) ledger entry and its builder for XRPL, providing type-safe field access and construction methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/AMM.h", + "functions": [ + { + "args": [ + "sle" + ], + "lineno": 32, + "name": "AMM" + }, + { + "args": [], + "lineno": 46, + "name": "getAccount" + }, + { + "args": [], + "lineno": 56, + "name": "getTradingFee" + }, + { + "args": [], + "lineno": 67, + "name": "hasTradingFee" + }, + { + "args": [], + "lineno": 77, + "name": "getVoteSlots" + }, + { + "args": [], + "lineno": 89, + "name": "hasVoteSlots" + }, + { + "args": [], + "lineno": 99, + "name": "getAuctionSlot" + }, + { + "args": [], + "lineno": 111, + "name": "hasAuctionSlot" + }, + { + "args": [], + "lineno": 121, + "name": "getLPTokenBalance" + }, + { + "args": [], + "lineno": 131, + "name": "getAsset" + }, + { + "args": [], + "lineno": 141, + "name": "getAsset2" + }, + { + "args": [], + "lineno": 151, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 161, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 172, + "name": "hasPreviousTxnID" + }, + { + "args": [], + "lineno": 182, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 193, + "name": "hasPreviousTxnLgrSeq" + }, + { + "args": [ + "account", + "lPTokenBalance", + "asset", + "asset2", + "ownerNode" + ], + "lineno": 210, + "name": "AMMBuilder" + }, + { + "args": [ + "sle" + ], + "lineno": 221, + "name": "AMMBuilder" + }, + { + "args": [ + "value" + ], + "lineno": 234, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 244, + "name": "setTradingFee" + }, + { + "args": [ + "value" + ], + "lineno": 254, + "name": "setVoteSlots" + }, + { + "args": [ + "value" + ], + "lineno": 264, + "name": "setAuctionSlot" + }, + { + "args": [ + "value" + ], + "lineno": 274, + "name": "setLPTokenBalance" + }, + { + "args": [ + "value" + ], + "lineno": 284, + "name": "setAsset" + }, + { + "args": [ + "value" + ], + "lineno": 294, + "name": "setAsset2" + }, + { + "args": [ + "value" + ], + "lineno": 304, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 314, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 324, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 334, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/AMM.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/AMM.h.ai.md new file mode 100644 index 0000000000..7cc0a2c1fc --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/AMM.h.ai.md @@ -0,0 +1,49 @@ +# `AMM.h` — Auto-generated AMM Ledger Entry Wrapper + +## Role and Context + +This file is part of `xrpl/protocol_autogen/ledger_entries/`, a directory of auto-generated wrappers for every ledger entry type in the XRPL. `AMM.h` specifically encapsulates the **Automated Market Maker** ledger entry (`ltAMM`, type code `0x0079`), which represents a constant-product liquidity pool on the ledger. The file must not be edited by hand — it is generated from a schema that describes the AMM entry's field set, cardinalities, and types, and regenerated whenever that schema changes. + +The file lives in the `xrpl::ledger_entries` namespace and defines two classes: `AMM` (an immutable read-side view) and `AMMBuilder` (a write-side fluent constructor). This split enforces a clean read/write boundary: application code that only reads ledger state gets an `AMM`; code that creates or modifies entries uses `AMMBuilder`. + +## The `AMM` Wrapper Class + +`AMM` inherits from `LedgerEntryBase`, which holds a `std::shared_ptr` (a `const`-qualified Serialized Ledger Entry). All field access delegates to `sle_->at(sfField)` or `sle_->isFieldPresent(sfField)`, so the wrapper adds zero runtime overhead beyond the shared-pointer indirection already present in the underlying storage. + +The constructor accepts a `std::shared_ptr` and immediately validates that `sle_->getType() == ltAMM`, throwing `std::runtime_error` on mismatch. This guards against accidentally wrapping the wrong entry type — a risk that exists whenever raw `SLE` objects are passed around by the core ledger engine. + +### Field Schema and Getter Strategy + +The AMM entry's fields fall into three cardinality classes, and the getter design mirrors them exactly: + +**Required fields** (`soeREQUIRED`) — `sfAccount`, `sfLPTokenBalance`, `sfAsset`, `sfAsset2`, `sfOwnerNode` — are returned directly by value with no `std::optional`. Accessing them on a well-formed entry always succeeds; the type system reflects that guarantee. + +**Default fields** (`soeDEFAULT`) — `sfTradingFee` — may be absent from the serialized form when equal to their default value (zero in this case). `getTradingFee()` returns `protocol_autogen::Optional` and is guarded by `hasTradingFee()`. The `Optional` alias from `Utils.h` is a compile-time conditional: for reference types it becomes `std::optional>` to avoid dangling references; for value types like `uint16_t` it collapses to plain `std::optional`. + +**Optional fields** (`soeOPTIONAL`) — `sfVoteSlots`, `sfAuctionSlot`, `sfPreviousTxnID`, `sfPreviousTxnLgrSeq` — follow the same `has*()`/`get*()` pattern. Notably, `getVoteSlots()` returns `std::optional>` rather than a copy of the array. This avoids copying potentially large vote-slot arrays, while keeping the return type nullable for the absent case. `getAuctionSlot()` returns `std::optional` — a by-value copy — because `STObject` does not have stable lifetime tied to the SLE and must be returned as an independent snapshot. + +### AMM Domain Fields + +- `sfAccount`: the special-purpose AMM account that holds the pool's asset reserves. This is a separate account from any user; it cannot sign transactions and exists solely to carry the pool balances. +- `sfAsset` / `sfAsset2`: the two sides of the pool, typed as `SF_ISSUE::type::value_type` (i.e., an `Issue` struct identifying currency and issuer). Together they uniquely identify the trading pair. +- `sfLPTokenBalance`: total outstanding LP token supply as an `STAmount`. LP tokens represent proportional ownership of the pool and are minted/burned by deposit/withdrawal transactions. +- `sfTradingFee`: a `uint16_t` fee in basis points (0–1000 representing 0–1%), charged on each swap and distributed to liquidity providers. +- `sfVoteSlots`: an `STArray` of vote entries, one per top LP token holder, used by the AMM governance mechanism to collectively adjust the trading fee. +- `sfAuctionSlot`: an optional `STObject` representing the currently active auction slot, which grants the slot holder discounted swap fees for a time window. +- `sfOwnerNode`, `sfPreviousTxnID`, `sfPreviousTxnLgrSeq`: bookkeeping fields common to many ledger entry types — the owner directory back-pointer and audit-trail pair tracking the last modifying transaction. + +## The `AMMBuilder` Class + +`AMMBuilder` inherits from `LedgerEntryBuilderBase`, a CRTP base that holds the mutable `STObject object_{sfLedgerEntry}` being assembled. The CRTP pattern enables `setLedgerIndex()` and `setFlags()` (defined on the base) to return `AMMBuilder&` rather than `LedgerEntryBuilderBase&`, preserving the fluent chaining API without virtual dispatch. + +The constructor enforces required-field discipline: `account`, `lPTokenBalance`, `asset`, `asset2`, and `ownerNode` are all mandatory parameters. Optional and default fields are set via chained setter calls after construction. A second constructor takes an existing `std::shared_ptr`, copying the SLE's fields into `object_` via `object_ = *sle` — enabling round-trip editing patterns where existing ledger state is read, modified through builder setters, and rebuilt. + +One subtle but important design note lives in `LedgerEntryBuilderBase`'s constructor comment: it deliberately avoids calling `object_.set(soTemplate)`. That call would create `STBase` placeholder objects for every `soeDEFAULT` field, which causes `applyTemplate()` (called inside the `SLE` constructor during `build()`) to throw "may not be explicitly set to default." By leaving those slots absent in the free `STObject`, the SLE constructor's own template application handles them correctly. + +The `setAsset()` and `setAsset2()` setters explicitly wrap their `Issue` argument as `STIssue(sfAsset, value)` before storing it. This is necessary because `SF_ISSUE::type::value_type` resolves to the plain `Issue` struct, but `STObject` requires the fully-typed serialized wrapper `STIssue` (which pairs the field descriptor with the value) to be stored under the correct field key. + +`build(uint256 const& index)` finalizes construction by moving `object_` into a new `SLE` at the given ledger key, then wrapping it in an `AMM` view. After `build()` the builder's `object_` is moved-from and should not be reused. + +## Relationship to Surrounding Infrastructure + +Every file in `ledger_entries/` follows the identical `Foo` + `FooBuilder` pattern. The uniformity is deliberate — auto-generation guarantees that all entry types get the same correctness properties (type-checked construction, mandatory field enforcement, `has*()`/`get*()` pairing for nullable fields) without any per-entry hand-written code. The `LedgerEntryBase::validate()` method, shared by all entry types, cross-checks the assembled `STObject` against the live `LedgerFormats` schema template, providing a runtime safety net in test and debug builds. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h.ai.json new file mode 100644 index 0000000000..5e407993a9 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h.ai.json @@ -0,0 +1,528 @@ +{ + "args": [ + { + "lineno": 22, + "name": "sle" + }, + { + "lineno": 439, + "name": "account" + }, + { + "lineno": 439, + "name": "sequence" + }, + { + "lineno": 439, + "name": "balance" + }, + { + "lineno": 439, + "name": "ownerCount" + }, + { + "lineno": 439, + "name": "previousTxnID" + }, + { + "lineno": 439, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 450, + "name": "sle" + }, + { + "lineno": 457, + "name": "value" + }, + { + "lineno": 466, + "name": "value" + }, + { + "lineno": 475, + "name": "value" + }, + { + "lineno": 484, + "name": "value" + }, + { + "lineno": 493, + "name": "value" + }, + { + "lineno": 502, + "name": "value" + }, + { + "lineno": 511, + "name": "value" + }, + { + "lineno": 520, + "name": "value" + }, + { + "lineno": 529, + "name": "value" + }, + { + "lineno": 538, + "name": "value" + }, + { + "lineno": 547, + "name": "value" + }, + { + "lineno": 556, + "name": "value" + }, + { + "lineno": 565, + "name": "value" + }, + { + "lineno": 574, + "name": "value" + }, + { + "lineno": 583, + "name": "value" + }, + { + "lineno": 592, + "name": "value" + }, + { + "lineno": 601, + "name": "value" + }, + { + "lineno": 610, + "name": "value" + }, + { + "lineno": 619, + "name": "value" + }, + { + "lineno": 628, + "name": "value" + }, + { + "lineno": 637, + "name": "value" + }, + { + "lineno": 646, + "name": "value" + }, + { + "lineno": 655, + "name": "value" + }, + { + "lineno": 664, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 18, + "name": "AccountRoot" + }, + { + "args": [ + "std::decay_t const& account, std::decay_t const& sequence, std::decay_t const& balance, std::decay_t const& ownerCount, std::decay_t const& previousTxnID, std::decay_t const& previousTxnLgrSeq", + "std::shared_ptr sle" + ], + "lineno": 432, + "name": "AccountRootBuilder" + } + ], + "description": "Provides type-safe C++ wrappers and builder for the XRPL AccountRoot ledger entry, including field getters, setters, and construction logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h", + "functions": [ + { + "args": [], + "lineno": 36, + "name": "getAccount" + }, + { + "args": [], + "lineno": 45, + "name": "getSequence" + }, + { + "args": [], + "lineno": 54, + "name": "getBalance" + }, + { + "args": [], + "lineno": 63, + "name": "getOwnerCount" + }, + { + "args": [], + "lineno": 72, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 81, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 90, + "name": "getAccountTxnID" + }, + { + "args": [], + "lineno": 101, + "name": "hasAccountTxnID" + }, + { + "args": [], + "lineno": 110, + "name": "getRegularKey" + }, + { + "args": [], + "lineno": 121, + "name": "hasRegularKey" + }, + { + "args": [], + "lineno": 130, + "name": "getEmailHash" + }, + { + "args": [], + "lineno": 141, + "name": "hasEmailHash" + }, + { + "args": [], + "lineno": 150, + "name": "getWalletLocator" + }, + { + "args": [], + "lineno": 161, + "name": "hasWalletLocator" + }, + { + "args": [], + "lineno": 170, + "name": "getWalletSize" + }, + { + "args": [], + "lineno": 181, + "name": "hasWalletSize" + }, + { + "args": [], + "lineno": 190, + "name": "getMessageKey" + }, + { + "args": [], + "lineno": 201, + "name": "hasMessageKey" + }, + { + "args": [], + "lineno": 210, + "name": "getTransferRate" + }, + { + "args": [], + "lineno": 221, + "name": "hasTransferRate" + }, + { + "args": [], + "lineno": 230, + "name": "getDomain" + }, + { + "args": [], + "lineno": 241, + "name": "hasDomain" + }, + { + "args": [], + "lineno": 250, + "name": "getTickSize" + }, + { + "args": [], + "lineno": 261, + "name": "hasTickSize" + }, + { + "args": [], + "lineno": 270, + "name": "getTicketCount" + }, + { + "args": [], + "lineno": 281, + "name": "hasTicketCount" + }, + { + "args": [], + "lineno": 290, + "name": "getNFTokenMinter" + }, + { + "args": [], + "lineno": 301, + "name": "hasNFTokenMinter" + }, + { + "args": [], + "lineno": 310, + "name": "getMintedNFTokens" + }, + { + "args": [], + "lineno": 321, + "name": "hasMintedNFTokens" + }, + { + "args": [], + "lineno": 330, + "name": "getBurnedNFTokens" + }, + { + "args": [], + "lineno": 341, + "name": "hasBurnedNFTokens" + }, + { + "args": [], + "lineno": 350, + "name": "getFirstNFTokenSequence" + }, + { + "args": [], + "lineno": 361, + "name": "hasFirstNFTokenSequence" + }, + { + "args": [], + "lineno": 370, + "name": "getAMMID" + }, + { + "args": [], + "lineno": 381, + "name": "hasAMMID" + }, + { + "args": [], + "lineno": 390, + "name": "getVaultID" + }, + { + "args": [], + "lineno": 401, + "name": "hasVaultID" + }, + { + "args": [], + "lineno": 410, + "name": "getLoanBrokerID" + }, + { + "args": [], + "lineno": 421, + "name": "hasLoanBrokerID" + }, + { + "args": [ + "value" + ], + "lineno": 456, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 465, + "name": "setSequence" + }, + { + "args": [ + "value" + ], + "lineno": 474, + "name": "setBalance" + }, + { + "args": [ + "value" + ], + "lineno": 483, + "name": "setOwnerCount" + }, + { + "args": [ + "value" + ], + "lineno": 492, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 501, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 510, + "name": "setAccountTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 519, + "name": "setRegularKey" + }, + { + "args": [ + "value" + ], + "lineno": 528, + "name": "setEmailHash" + }, + { + "args": [ + "value" + ], + "lineno": 537, + "name": "setWalletLocator" + }, + { + "args": [ + "value" + ], + "lineno": 546, + "name": "setWalletSize" + }, + { + "args": [ + "value" + ], + "lineno": 555, + "name": "setMessageKey" + }, + { + "args": [ + "value" + ], + "lineno": 564, + "name": "setTransferRate" + }, + { + "args": [ + "value" + ], + "lineno": 573, + "name": "setDomain" + }, + { + "args": [ + "value" + ], + "lineno": 582, + "name": "setTickSize" + }, + { + "args": [ + "value" + ], + "lineno": 591, + "name": "setTicketCount" + }, + { + "args": [ + "value" + ], + "lineno": 600, + "name": "setNFTokenMinter" + }, + { + "args": [ + "value" + ], + "lineno": 609, + "name": "setMintedNFTokens" + }, + { + "args": [ + "value" + ], + "lineno": 618, + "name": "setBurnedNFTokens" + }, + { + "args": [ + "value" + ], + "lineno": 627, + "name": "setFirstNFTokenSequence" + }, + { + "args": [ + "value" + ], + "lineno": 636, + "name": "setAMMID" + }, + { + "args": [ + "value" + ], + "lineno": 645, + "name": "setVaultID" + }, + { + "args": [ + "value" + ], + "lineno": 654, + "name": "setLoanBrokerID" + }, + { + "args": [ + "index" + ], + "lineno": 663, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h.ai.md new file mode 100644 index 0000000000..def48993a1 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/AccountRoot.h.ai.md @@ -0,0 +1,39 @@ +# `AccountRoot.h` — Auto-Generated Type-Safe Wrapper for the AccountRoot Ledger Entry + +This file is part of the `xrpl/protocol_autogen` layer and provides the C++ interface for the XRPL `AccountRoot` ledger entry (`ltACCOUNT_ROOT`, type code `0x0061`). It is auto-generated — edits are not preserved — and lives alongside parallel files for every other ledger entry type (Offer, RippleState, AMM, Vault, etc.) in the `ledger_entries/` subdirectory. The file defines two classes that together implement a clean separation between reading and constructing ledger state: an immutable wrapper `AccountRoot` and a fluent builder `AccountRootBuilder`. + +## Role in the System + +The `AccountRoot` ledger entry is the most fundamental object in the XRPL: every account has exactly one, and it is the authoritative record of that account's XRP balance, transaction sequence number, owned-object count, signing configuration, and a growing set of protocol-level IDs linking to AMM pools, vaults, loan brokers, and NFT tracking state. Before this auto-generated layer existed, code would retrieve fields from raw `SLE` objects using untyped `at(sfSomeField)` calls scattered throughout the codebase. This wrapper layer enforces at the type system level that you cannot call `getBalance()` on an entry that is not an `AccountRoot`. + +## `AccountRoot` — Immutable Read Wrapper + +`AccountRoot` inherits from `LedgerEntryBase`, which holds the single protected member `std::shared_ptr sle_` and exposes common cross-entry getters: `getType()`, `getKey()`, `getFlags()`, `getLedgerIndex()`, and `getSle()`. It also owns the `validate()` method, which dispatches to `validateSTObject` against the format template registered in `LedgerFormats`. + +The `AccountRoot` constructor takes a `std::shared_ptr` and immediately checks `sle_->getType() != entryType`, throwing `std::runtime_error` if there is a mismatch. This eager validation is important because callers often extract SLEs from a general-purpose ledger lookup that returns any entry type; the constructor acts as a runtime assertion that the caller selected the right entry. Note the subtle `SLE const` — the const qualifier is intentional and propagates through the entire read interface, making accidental mutation impossible. + +Field getters divide into two categories based on field optionality in the protocol schema: + +**Required fields** (`soeREQUIRED`) — `getAccount()`, `getSequence()`, `getBalance()`, `getOwnerCount()`, `getPreviousTxnID()`, `getPreviousTxnLgrSeq()` — return their value types directly, with no `Optional` wrapping. The protocol guarantees these fields are always present in a well-formed SLE, so no presence check is needed. + +**Optional and default-valued fields** return `protocol_autogen::Optional`. The `Optional` alias, defined in `Utils.h`, resolves to `std::optional` for value types and `std::optional>` for reference types — a detail that lets the abstraction work correctly whether the underlying field type is a value or a reference. Each such getter delegates to a paired `has*()` method that calls `sle_->isFieldPresent(sfXxx)`, only extracting the field if present. This pattern applies to `sfAccountTxnID`, `sfRegularKey`, `sfEmailHash`, `sfWalletLocator`, `sfWalletSize`, `sfMessageKey`, `sfTransferRate`, `sfDomain`, `sfTickSize`, `sfTicketCount`, `sfNFTokenMinter`, `sfFirstNFTokenSequence`, `sfAMMID`, `sfVaultID`, and `sfLoanBrokerID`. + +Two fields — `sfMintedNFTokens` and `sfBurnedNFTokens` — are annotated `soeDEFAULT` in the schema, meaning they have a protocol-defined default (zero) that the serializer may omit when the field is at that default. Despite conceptually always being present for NFT-capable accounts, the getters still return `Optional` and guard via `isFieldPresent()`, which is the correct behavior: a legacy `AccountRoot` that has never interacted with NFTs may not serialize these fields at all. + +The richness of the optional field set reflects the evolution of the XRPL protocol. Fields like `sfEmailHash`, `sfWalletLocator`, and `sfWalletSize` are legacy from early XRPL design and rarely populated today. More recent additions — `sfAMMID`, `sfVaultID`, `sfLoanBrokerID` — link the account to specific DeFi protocol objects, allowing a single account to serve as the on-ledger identity for an AMM pool or lending vault without requiring a separate lookup strategy. + +## `AccountRootBuilder` — Fluent Construction + +`AccountRootBuilder` inherits from `LedgerEntryBuilderBase`, which uses the Curiously Recurring Template Pattern (CRTP). The base class exposes `setFlags()` and `setLedgerIndex()` and returns `Derived&` (i.e., `AccountRootBuilder&`), so method chaining on common fields still returns the concrete derived type. The internal state is an `STObject object_{sfLedgerEntry}`. + +A critical design choice in `LedgerEntryBuilderBase` is that it does *not* call `object_.set(soTemplate)` during construction. Setting the template would pre-populate `soeDEFAULT` fields with STBase placeholder objects. When the builder later constructs an `SLE` — which calls `applyTemplate()` internally — those placeholders would trigger an exception: "may not be explicitly set to default." By keeping `object_` as a free, template-less `STObject`, only fields that were explicitly assigned appear in it, and `applyTemplate()` can safely insert defaults for unset fields. + +The primary constructor accepts all six required fields as parameters, initialises the base with `ltACCOUNT_ROOT` (setting `sfLedgerEntryType` and zeroing `sfFlags`), and immediately calls the corresponding setters. A secondary constructor accepts an existing `std::shared_ptr` and copies the SLE's field data into `object_` via `object_ = *sle`, enabling round-trip editing workflows where an entry is read from the ledger, modified through the builder API, and then rebuilt. + +All optional field setters follow the identical signature pattern: accept a `std::decay_t const&` and write it into `object_[sfXxx]`, then return `*this`. Using `std::decay_t` strips any reference or cv-qualification from the field's native value type, ensuring the parameter type is always a plain value regardless of how the field type is defined in the protocol type system. + +The terminal `build(uint256 const& index)` method constructs a final `AccountRoot` by moving `object_` into an `SLE` keyed at `index`, wrapping it in a `shared_ptr`, and passing it to the `AccountRoot` constructor. After `build()`, the builder's `object_` has been moved-from and should not be reused. + +## Relationship to the Broader Auto-Gen Layer + +This file follows the exact same structural template as the other ~25 ledger entry files in the directory (`AMM.h`, `Offer.h`, `RippleState.h`, etc.) and is regenerated whenever the protocol schema changes. The separation of concerns is clean: `LedgerEntryBase` and `LedgerEntryBuilderBase` carry all reusable infrastructure, while each generated file provides only field-specific accessors and the mandatory-field constructor. Adding a new protocol field to `AccountRoot` means regenerating this file; no manual changes to the base classes are required. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Amendments.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Amendments.h.ai.json new file mode 100644 index 0000000000..3d58bebed9 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Amendments.h.ai.json @@ -0,0 +1,122 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 16, + "name": "Amendments" + }, + { + "args": [], + "lineno": 111, + "name": "AmendmentsBuilder" + } + ], + "description": "Provides an auto-generated, type-safe C++ wrapper and builder for the Amendments ledger entry in the XRPL, including field accessors and mutators.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Amendments.h", + "functions": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 23, + "name": "Amendments" + }, + { + "args": [], + "lineno": 38, + "name": "getAmendments" + }, + { + "args": [], + "lineno": 48, + "name": "hasAmendments" + }, + { + "args": [], + "lineno": 56, + "name": "getMajorities" + }, + { + "args": [], + "lineno": 67, + "name": "hasMajorities" + }, + { + "args": [], + "lineno": 75, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 85, + "name": "hasPreviousTxnID" + }, + { + "args": [], + "lineno": 93, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 103, + "name": "hasPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 117, + "name": "AmendmentsBuilder" + }, + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 122, + "name": "AmendmentsBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 136, + "name": "setAmendments" + }, + { + "args": [ + "STArray const& value" + ], + "lineno": 144, + "name": "setMajorities" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 152, + "name": "setPreviousTxnID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 160, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "uint256 const& index" + ], + "lineno": 168, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Amendments.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Amendments.h.ai.md new file mode 100644 index 0000000000..dcf3932e97 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Amendments.h.ai.md @@ -0,0 +1,31 @@ +# `include/xrpl/protocol_autogen/ledger_entries/Amendments.h` + +This file is an auto-generated type-safe wrapper for the XRPL `Amendments` ledger entry (`ltAMENDMENTS`, type `0x0066`). It belongs to the `xrpl::ledger_entries` namespace alongside roughly thirty similar wrappers — one per ledger object kind — all generated from a common schema. The file should never be edited by hand; changes belong in the generator. + +## Domain context + +The Amendments ledger entry is a singleton on the XRP Ledger that records the state of the amendment process. `sfAmendments` holds a vector of 256-bit hashes identifying every protocol amendment that has been fully enabled on the network. `sfMajorities` is a structured array tracking amendments that have crossed the validator supermajority threshold but have not yet been enabled — each element carries an amendment ID and the ledger timestamp at which majority support was first observed. Together these two fields power the two-week lockout that prevents feature activation until sufficient validators have upgraded. `sfPreviousTxnID` and `sfPreviousTxnLgrSeq` are the standard auditing fields that trace any ledger entry back to the `Change` pseudo-transaction that last mutated it. + +## Two-class pattern: wrapper + builder + +The file defines exactly two classes. `Amendments` is an **immutable read-only wrapper** that owns a `std::shared_ptr` (the serialized ledger entry). `AmendmentsBuilder` is the **mutable construction side** that accumulates field values and materializes them into an `SLE` only when `build()` is called. This strict separation prevents accidental mutation of a live SLE that is shared across consensus, validation, and view layers. + +`Amendments` inherits from `LedgerEntryBase`, which provides `getKey()`, `getType()`, `getFlags()`, `getLedgerIndex()`, `getSle()`, and `validate()` — the common denominator for all ledger entry types. `AmendmentsBuilder` inherits from `LedgerEntryBuilderBase`, which uses CRTP so that the inherited `setLedgerIndex()` and `setFlags()` methods return `AmendmentsBuilder&` and support uninterrupted method chaining without any casting at the call site. + +## Construction and type-safety guard + +Both constructors that accept an existing `SLE` enforce a type match at runtime. `Amendments(std::shared_ptr sle)` calls `sle_->getType()` and throws `std::runtime_error` if the result is not `ltAMENDMENTS`. `AmendmentsBuilder(std::shared_ptr sle)` does the same via `sle->at(sfLedgerEntryType)`. This deliberate eagerness means a wrong-type mistake surfaces immediately at construction rather than silently returning garbage data from a getter, as the unit tests explicitly verify. + +The default `AmendmentsBuilder()` constructor pre-populates only `sfLedgerEntryType` and `sfFlags` on a free `STObject`. It intentionally does **not** call `object_.set(soTemplate)`, which would install `soeDEFAULT` placeholder fields. Calling `applyTemplate()` later (inside the `SLE` constructor) would reject any field set to its default value with an "may not be explicitly set to default" error. Leaving the builder object as a schema-free container avoids this trap. + +## Field accessors and the Optional alias + +Every public field getter follows a paired pattern: a `has*()` predicate and a `get*()` that returns `std::nullopt` when the field is absent. This is consistent with the `soeOPTIONAL` nature of every field on the Amendments entry — even `sfAmendments` itself may be absent on a brand-new ledger before any amendments are enabled. + +The return type of scalar getters like `getAmendments()` and `getPreviousTxnID()` uses the `protocol_autogen::Optional` alias defined in `Utils.h`. That alias resolves to either `std::optional` or `std::optional>>` depending on whether `T` is a reference type. This ensures that large field values accessed through `SLE::at()` — which can return const references into the SLE's internal storage — do not get silently copied when wrapped in an optional. + +`getMajorities()` diverges from this pattern. Because `STArray` is a variable-length structured type not covered by the `SF_*` accessor templates, it uses `sle_->getFieldArray(sfMajorities)` and returns `std::optional>` directly, allowing callers to iterate the majority records by reference without copying the array. + +## Builder `build()` materialization + +`AmendmentsBuilder::build(uint256 const& index)` finalizes the entry by moving the accumulated `STObject` into an `SLE` constructor along with the caller-supplied 256-bit ledger index. It then wraps the resulting `SLE` in a `shared_ptr` and passes it to the `Amendments` constructor. From this point forward the entry is immutable, and the builder is consumed (its `object_` has been moved out). Clients that need to re-read the entry after construction must go through the returned `Amendments` wrapper. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Bridge.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Bridge.h.ai.json new file mode 100644 index 0000000000..c8739ef305 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Bridge.h.ai.json @@ -0,0 +1,192 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 19, + "name": "Bridge" + }, + { + "args": [ + "std::decay_t const& account, std::decay_t const& signatureReward, std::decay_t const& xChainBridge, std::decay_t const& xChainClaimID, std::decay_t const& xChainAccountCreateCount, std::decay_t const& xChainAccountClaimCount, std::decay_t const& ownerNode, std::decay_t const& previousTxnID, std::decay_t const& previousTxnLgrSeq", + "std::shared_ptr sle" + ], + "lineno": 142, + "name": "BridgeBuilder" + } + ], + "description": "Defines the Bridge ledger entry and its builder for the XRPL protocol, providing type-safe accessors and a fluent builder interface for constructing Bridge ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Bridge.h", + "functions": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 25, + "name": "Bridge::Bridge" + }, + { + "args": [], + "lineno": 38, + "name": "Bridge::getAccount" + }, + { + "args": [], + "lineno": 47, + "name": "Bridge::getSignatureReward" + }, + { + "args": [], + "lineno": 56, + "name": "Bridge::getMinAccountCreateAmount" + }, + { + "args": [], + "lineno": 67, + "name": "Bridge::hasMinAccountCreateAmount" + }, + { + "args": [], + "lineno": 76, + "name": "Bridge::getXChainBridge" + }, + { + "args": [], + "lineno": 85, + "name": "Bridge::getXChainClaimID" + }, + { + "args": [], + "lineno": 94, + "name": "Bridge::getXChainAccountCreateCount" + }, + { + "args": [], + "lineno": 103, + "name": "Bridge::getXChainAccountClaimCount" + }, + { + "args": [], + "lineno": 112, + "name": "Bridge::getOwnerNode" + }, + { + "args": [], + "lineno": 121, + "name": "Bridge::getPreviousTxnID" + }, + { + "args": [], + "lineno": 130, + "name": "Bridge::getPreviousTxnLgrSeq" + }, + { + "args": [ + "std::decay_t const& account", + "std::decay_t const& signatureReward", + "std::decay_t const& xChainBridge", + "std::decay_t const& xChainClaimID", + "std::decay_t const& xChainAccountCreateCount", + "std::decay_t const& xChainAccountClaimCount", + "std::decay_t const& ownerNode", + "std::decay_t const& previousTxnID", + "std::decay_t const& previousTxnLgrSeq" + ], + "lineno": 146, + "name": "BridgeBuilder::BridgeBuilder" + }, + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 162, + "name": "BridgeBuilder::BridgeBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 174, + "name": "BridgeBuilder::setAccount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 183, + "name": "BridgeBuilder::setSignatureReward" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 192, + "name": "BridgeBuilder::setMinAccountCreateAmount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 201, + "name": "BridgeBuilder::setXChainBridge" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 210, + "name": "BridgeBuilder::setXChainClaimID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 219, + "name": "BridgeBuilder::setXChainAccountCreateCount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 228, + "name": "BridgeBuilder::setXChainAccountClaimCount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 237, + "name": "BridgeBuilder::setOwnerNode" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 246, + "name": "BridgeBuilder::setPreviousTxnID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 255, + "name": "BridgeBuilder::setPreviousTxnLgrSeq" + }, + { + "args": [ + "uint256 const& index" + ], + "lineno": 264, + "name": "BridgeBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Bridge.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Bridge.h.ai.md new file mode 100644 index 0000000000..427fa7fe59 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Bridge.h.ai.md @@ -0,0 +1,47 @@ +# `Bridge.h` — Auto-Generated `ltBRIDGE` Ledger Entry Wrapper + +## Role in the System + +This file is part of a code-generated layer under `xrpl/protocol_autogen/ledger_entries/` that provides type-safe C++ wrappers for every XRPL ledger object type. The `Bridge` class (type code `ltBRIDGE`, `0x0069`) represents the on-ledger anchor for an XRP Ledger cross-chain bridge — the persistent state object that records which chains are bridged, tracks claim sequence counters, and holds the reward configuration for attestation servers. + +The `// This file is auto-generated. Do not edit.` comment at line 1 defines the contract: the schema for `ltBRIDGE` lives in `ledger_entries.macro`, and regenerating this file from that source is the correct way to evolve it. Hand-editing would immediately diverge from the canonical schema and would be overwritten on the next code generation pass. + +## Why This Layer Exists + +The underlying storage type, `SLE` (Serialized Ledger Entry), is a generic property-bag accessed via `SF_*` field descriptors with no compile-time type safety. Accessing a field that doesn't exist, accessing it under the wrong type, or forgetting to check for optional presence are all silent bugs at the `SLE` level. The `Bridge` wrapper eliminates that surface: every field has a named getter with a concrete return type derived from its `SF_*` descriptor's `value_type`, optional fields return `protocol_autogen::Optional` (an alias for `std::optional`), and the wrapper refuses construction on type mismatch. + +## `Bridge` — Immutable Read View + +`Bridge` inherits from `LedgerEntryBase`, which holds the `shared_ptr sle_` member and contributes getters for the universal fields present on every ledger entry: `getKey()`, `getType()`, `getFlags()`, `getLedgerEntryType()`, and `getLedgerIndex()`. `Bridge` adds the entry-specific accessors. + +The constructor takes ownership of a `shared_ptr` and immediately validates the type against `entryType = ltBRIDGE`, throwing `std::runtime_error` on mismatch. This fail-fast design prevents silent type confusion: it is impossible to construct a `Bridge` wrapping a `Check` or `Escrow` SLE and then read misinterpreted field data from it. + +The cross-chain-specific fields exposed by `Bridge` map directly to the schema defined in the `LEDGER_ENTRY(ltBRIDGE, ...)` macro: + +- **`getXChainBridge()`** — Returns the `SF_XCHAIN_BRIDGE` value that identifies the locking and issuing chains. This is the bridge's defining identity. +- **`getSignatureReward()`** — An `STAmount` denominating the XRP reward paid to witness servers that submit valid attestations. +- **`getMinAccountCreateAmount()`** / **`hasMinAccountCreateAmount()`** — The sole optional field (`soeOPTIONAL`). A bridge operator may or may not require a minimum deposit when creating accounts via the bridge. The dual pattern — a separate `has*` predicate alongside a conditional getter returning `std::nullopt` — makes optional field handling explicit and prevents callers from accidentally calling `sle_->at()` on an absent field, which would throw. +- **`getXChainClaimID()`**, **`getXChainAccountCreateCount()`**, **`getXChainAccountClaimCount()`** — Three `uint64` counters. `XChainClaimID` is a monotonically-increasing counter used to generate unique IDs for `XChainOwnedClaimID` entries. `XChainAccountCreateCount` and `XChainAccountClaimCount` track account-creation and account-claim operations, respectively. These sequence values are critical to the bridge's replay-prevention and ordering guarantees. +- **`getAccount()`**, **`getOwnerNode()`**, **`getPreviousTxnID()`**, **`getPreviousTxnLgrSeq()`** — Standard bookkeeping fields present on nearly all modifiable ledger objects: the owning account, the owner-directory page index, and the last transaction that touched the entry. + +All getters are `[[nodiscard]] const`, enforcing that `Bridge` is a purely read-only view with no mutation interface. + +## `BridgeBuilder` — Fluent Construction via CRTP + +`BridgeBuilder` inherits from `LedgerEntryBuilderBase`, a CRTP base that holds the mutable `STObject object_` and provides `setFlags()`, `setLedgerIndex()`, and the `validate()` method. The CRTP parameter ensures that these inherited setters return `BridgeBuilder&`, preserving the fluent chaining interface without virtual dispatch. + +The base class constructor notably avoids calling `object_.set(soTemplate)` on the `STObject`. This is a deliberate design choice documented in `LedgerEntryBuilderBase.h`: applying the SO template early would insert placeholder `STBase` values for `soeDEFAULT` fields, and when the `SLE` constructor subsequently calls `applyTemplate()` it would throw "may not be explicitly set to default." By keeping `object_` as a template-free `STObject` and letting the `SLE` constructor handle template application, the builder avoids this subtle ordering constraint. + +`BridgeBuilder` exposes two construction paths: + +1. **From required field values** — All nine required fields must be supplied to the primary constructor, which immediately calls the corresponding `set*` methods. This front-loads field enforcement: there is no way to reach `build()` without having provided every required field. + +2. **From an existing `SLE const`** — The second constructor copies the SLE's data into `object_`, enabling a read-modify-write workflow: deserialize an existing `Bridge` entry, mutate specific fields via the fluent setters, and build a new `SLE`. Type validation is applied immediately here too. + +`setMinAccountCreateAmount()` is intentionally absent from the primary constructor, as it is optional. Callers who need it chain it after construction: `BridgeBuilder{...}.setMinAccountCreateAmount(amount).build(index)`. + +`build(uint256 const& index)` finalizes construction by moving `object_` into a new `SLE` at the given ledger key, then wrapping it in a `Bridge`. The index must be externally computed — typically as a deterministic hash of the bridge's defining parameters — because the builder itself has no knowledge of the ledger's key scheme. + +## Fit in the Autogen Pattern + +This file is structurally identical to every sibling in `ledger_entries/` — `AMM.h`, `Escrow.h`, `Check.h`, and the rest. The uniformity is the point: a single code generator produces consistent, auditable wrapper pairs for every ledger entry type. Adding a new field to `ltBRIDGE` in the macro means regenerating this file, not patching it by hand. Callers working at a higher abstraction level see a clean typed interface; the raw `SLE` is accessible via `getSle()` on `LedgerEntryBase` for the rare cases where the generated accessors are insufficient. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Check.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Check.h.ai.json new file mode 100644 index 0000000000..8b991224b3 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Check.h.ai.json @@ -0,0 +1,30 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 22, + "name": "Check" + }, + { + "args": [ + "std::decay_t const& account, std::decay_t const& destination, std::decay_t const& sendMax, std::decay_t const& sequence, std::decay_t const& ownerNode, std::decay_t const& destinationNode, std::decay_t const& previousTxnID, std::decay_t const& previousTxnLgrSeq", + "std::shared_ptr sle" + ], + "lineno": 120, + "name": "CheckBuilder" + } + ], + "description": "Defines the Check ledger entry type for the XRPL, providing type-safe field access and a builder for constructing Check ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Check.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Check.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Check.h.ai.md new file mode 100644 index 0000000000..6c3f7a756a --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Check.h.ai.md @@ -0,0 +1,40 @@ +# `Check.h` — Auto-generated Check Ledger Entry Wrapper + +## Role in the System + +`Check.h` is a machine-generated file under `include/xrpl/protocol_autogen/ledger_entries/` that provides two classes — `Check` and `CheckBuilder` — for working with the XRPL Check ledger entry type (`ltCHECK`, wire value `0x0043`). A Check on the XRPL represents a deferred, pre-authorized payment instruction: the sender (account) commits to letting a specific destination cash up to `sfSendMax` at any time before an optional expiry. No funds are reserved when the Check is created; the payment only moves when the recipient submits a `CheckCash` transaction. + +The file sits alongside ~30 peer headers covering every first-class ledger entry type (Offer, Escrow, PayChannel, AccountRoot, etc.). All follow an identical structural pattern, which is the entire motivation for code generation: rather than hand-maintaining fragile ad-hoc accessors for each entry type, a single schema drives the generation of consistent, type-checked wrappers. + +## Class `Check` — Immutable Read Facade + +`Check` extends `LedgerEntryBase` and owns a `std::shared_ptr` (Serialized Ledger Entry), inherited as `sle_`. The `const`-qualified pointer is the key design choice: it enforces immutability at the type system level. All callers who receive a `Check` know that the ledger data underneath cannot change through this handle — a desirable property when the same `SLE` object may be referenced by multiple readers during transaction processing. + +The constructor accepts `std::shared_ptr` and immediately validates `sle_->getType() != entryType`, throwing `std::runtime_error` if the entry is not actually a Check. This is an eager type-guard: rather than silently accessing wrong fields on a mistyped entry, the mismatch is surfaced at construction time, keeping downstream getters unconditionally safe. + +### Required vs. Optional Field Access + +The accessor design distinguishes between `soeREQUIRED` and `soeOPTIONAL` fields in a first-class way: + +- **Required fields** (`sfAccount`, `sfDestination`, `sfSendMax`, `sfSequence`, `sfOwnerNode`, `sfDestinationNode`, `sfPreviousTxnID`, `sfPreviousTxnLgrSeq`) return their value type directly. Calling `sle_->at(sfXxx)` on a required field is safe by ledger invariant — if the field were absent the entry would be invalid and would not have passed ledger validation. +- **Optional fields** (`sfExpiration`, `sfInvoiceID`, `sfSourceTag`, `sfDestinationTag`) return `protocol_autogen::Optional`. This alias, defined in `Utils.h`, is a `std::conditional_t` that resolves to either `std::optional>` (when `T` is a reference type) or plain `std::optional` (when it is a value). The extra indirection for reference types prevents dangling references when wrapping fields accessed by reference from the underlying `SLE`. Each optional getter is paired with a `hasXxx()` predicate that calls `sle_->isFieldPresent(sfXxx)`, and the getter's body gates the `sle_->at(...)` call through that predicate before returning. + +All getters are `[[nodiscard]]` and `const`, which reinforces the read-only contract and warns callers who accidentally discard return values. + +## Class `CheckBuilder` — Fluent Construction + +`CheckBuilder` extends `LedgerEntryBuilderBase`, a CRTP template whose `object_` member is an `STObject{sfLedgerEntry}`. The base class deliberately avoids calling `object_.set(soTemplate)` on construction (noted in a comment): doing so would pre-populate `soeDEFAULT` placeholder fields, which the `SLE` constructor's internal `applyTemplate()` call would then reject as "may not be explicitly set to default." By leaving `object_` as a free `STObject`, the builder accumulates only the fields that are explicitly set, and the `SLE` constructor fills gaps according to the template on finalization. + +`CheckBuilder` takes all eight required fields in its primary constructor, calling the corresponding setters immediately. This guarantees that a `CheckBuilder` can never produce an incomplete `Check` lacking required data — the compiler enforces it. The secondary constructor takes `std::shared_ptr` to clone an existing entry into the builder for editing; it performs the same type check (`sfLedgerEntryType != ltCHECK`) before copying with `object_ = *sle`. + +Each setter returns `CheckBuilder&`, enabling method chaining: `builder.setExpiration(exp).setSourceTag(tag).build(index)`. The CRTP base's common setters (`setFlags`, `setLedgerIndex`) also return `Derived&` via `static_cast(*this)`, so they slot seamlessly into the same chain. + +The `build(uint256 const& index)` method finalizes construction by calling `std::make_shared(std::move(object_), index)` and wrapping the result in a `Check`. Moving `object_` into the `SLE` avoids a copy and signals that the builder is consumed — use-after-build would operate on an empty `STObject`. + +## Relationship to Validation + +Both `Check` (via `LedgerEntryBase::validate()`) and `CheckBuilder` (via `LedgerEntryBuilderBase::validate()`) delegate validation to `protocol_autogen::validateSTObject()`, which consults `LedgerFormats::getInstance()` to retrieve the canonical `SOTemplate` for `ltCHECK` and checks required/optional field presence against it. This shared path means builder and wrapper agree on exactly the same validity criteria. + +## Domain Semantics Encoded in the Schema + +The field set reflects the Check's place in the XRPL owner-directory graph. `sfOwnerNode` is a back-pointer into the sender's owner directory (a `DirectoryNode` ledger entry), and `sfDestinationNode` is a back-pointer into the recipient's owner directory — both required so the ledger can efficiently remove the Check object and update both directories when the check is cashed or cancelled. `sfPreviousTxnID` and `sfPreviousTxnLgrSeq` are standard audit fields present on all mutable ledger objects, allowing nodes to trace which transaction last touched this entry. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Credential.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Credential.h.ai.json new file mode 100644 index 0000000000..b07e599842 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Credential.h.ai.json @@ -0,0 +1,257 @@ +{ + "args": [ + { + "lineno": 32, + "name": "sle" + }, + { + "lineno": 158, + "name": "subject" + }, + { + "lineno": 158, + "name": "issuer" + }, + { + "lineno": 158, + "name": "credentialType" + }, + { + "lineno": 158, + "name": "issuerNode" + }, + { + "lineno": 158, + "name": "previousTxnID" + }, + { + "lineno": 158, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 171, + "name": "sle" + }, + { + "lineno": 184, + "name": "value" + }, + { + "lineno": 193, + "name": "value" + }, + { + "lineno": 202, + "name": "value" + }, + { + "lineno": 211, + "name": "value" + }, + { + "lineno": 220, + "name": "value" + }, + { + "lineno": 229, + "name": "value" + }, + { + "lineno": 238, + "name": "value" + }, + { + "lineno": 247, + "name": "value" + }, + { + "lineno": 256, + "name": "value" + }, + { + "lineno": 265, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 27, + "name": "Credential" + }, + { + "args": [ + "subject", + "issuer", + "credentialType", + "issuerNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 149, + "name": "CredentialBuilder" + } + ], + "description": "Defines the Credential ledger entry type for the XRPL, providing a type-safe wrapper and a builder class for constructing and accessing Credential ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Credential.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "Credential::getSubject" + }, + { + "args": [], + "lineno": 47, + "name": "Credential::getIssuer" + }, + { + "args": [], + "lineno": 56, + "name": "Credential::getCredentialType" + }, + { + "args": [], + "lineno": 65, + "name": "Credential::getExpiration" + }, + { + "args": [], + "lineno": 75, + "name": "Credential::hasExpiration" + }, + { + "args": [], + "lineno": 84, + "name": "Credential::getURI" + }, + { + "args": [], + "lineno": 94, + "name": "Credential::hasURI" + }, + { + "args": [], + "lineno": 103, + "name": "Credential::getIssuerNode" + }, + { + "args": [], + "lineno": 112, + "name": "Credential::getSubjectNode" + }, + { + "args": [], + "lineno": 122, + "name": "Credential::hasSubjectNode" + }, + { + "args": [], + "lineno": 131, + "name": "Credential::getPreviousTxnID" + }, + { + "args": [], + "lineno": 140, + "name": "Credential::getPreviousTxnLgrSeq" + }, + { + "args": [ + "subject", + "issuer", + "credentialType", + "issuerNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 157, + "name": "CredentialBuilder::CredentialBuilder" + }, + { + "args": [ + "sle" + ], + "lineno": 170, + "name": "CredentialBuilder::CredentialBuilder" + }, + { + "args": [ + "value" + ], + "lineno": 182, + "name": "CredentialBuilder::setSubject" + }, + { + "args": [ + "value" + ], + "lineno": 191, + "name": "CredentialBuilder::setIssuer" + }, + { + "args": [ + "value" + ], + "lineno": 200, + "name": "CredentialBuilder::setCredentialType" + }, + { + "args": [ + "value" + ], + "lineno": 209, + "name": "CredentialBuilder::setExpiration" + }, + { + "args": [ + "value" + ], + "lineno": 218, + "name": "CredentialBuilder::setURI" + }, + { + "args": [ + "value" + ], + "lineno": 227, + "name": "CredentialBuilder::setIssuerNode" + }, + { + "args": [ + "value" + ], + "lineno": 236, + "name": "CredentialBuilder::setSubjectNode" + }, + { + "args": [ + "value" + ], + "lineno": 245, + "name": "CredentialBuilder::setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 254, + "name": "CredentialBuilder::setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 263, + "name": "CredentialBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Credential.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Credential.h.ai.md new file mode 100644 index 0000000000..4ce2a5eb1a --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Credential.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/protocol_autogen/ledger_entries/Credential.h` + +## Role in the System + +This file is part of the `protocol_autogen` subsystem — a layer of auto-generated, type-safe wrappers over the raw XRPL serialized ledger state. It defines the `Credential` ledger entry (type code `ltCREDENTIAL`, `0x0081`) along with its companion `CredentialBuilder`. The file carries a "do not edit" notice because it is produced by a code generator that walks the XRPL ledger format definitions and emits one header per entry type. Every ledger entry in the `protocol_autogen/ledger_entries/` directory follows the same structural template. + +A `Credential` ledger object represents an on-chain verifiable credential: a statement made by an `issuer` account attesting something about a `subject` account. The `credentialType` field is a variable-length blob that acts as an application-defined tag — for example, a KYC-level identifier or a membership class. This is distinct from the earlier `DepositPreauth` mechanism in that a single issuer can issue multiple credential types to multiple subjects, and the credential can carry an optional URI pointing to off-chain metadata. + +## The `Credential` Wrapper + +`Credential` extends `LedgerEntryBase`, which holds a `shared_ptr` as its sole data member. Const-ness is load-bearing: the pointer is to an immutable `SLE`, so once wrapped, the ledger entry cannot be altered through this interface. This prevents accidental mutation of ledger state through the typed API. + +Type safety is enforced eagerly at construction time. The constructor checks `sle_->getType() != entryType` and throws `std::runtime_error` on mismatch. This is the same guard used in every sibling type (e.g. `DID`, `Ticket`): it means that passing a raw `SLE` of the wrong type fails loudly at the wrapping site rather than silently returning garbage values. + +The field accessors divide into two categories matching the XRPL `soeREQUIRED` / `soeOPTIONAL` split: + +**Required fields** — `getSubject()`, `getIssuer()`, `getCredentialType()`, `getIssuerNode()`, `getPreviousTxnID()`, `getPreviousTxnLgrSeq()` — return values directly. The underlying `SLE::at()` call will assert if the field is absent, which should be unreachable for a validated ledger entry. + +**Optional fields** — `getExpiration()`, `getURI()`, `getSubjectNode()` — return `protocol_autogen::Optional`, a typedef defined in `Utils.h`. That alias resolves to `std::optional` when `T` is a value type, or `std::optional>` when `T` is a reference type. This prevents dangling references for blob-like `SF_VL` fields whose `value_type` may be a const reference into the SLE's internal buffer. Each optional getter is paired with a corresponding `hasXxx()` predicate so callers can distinguish a missing field from a field with a default value without relying on the optional's `has_value()` alone. + +`sfIssuerNode` being required while `sfSubjectNode` is optional reflects ledger directory semantics: when a `CredentialCreate` transaction fires, the credential is always threaded into the issuer's owner directory (hence `sfIssuerNode` is always present), but it may also optionally appear in the subject's directory if the subject accepted it — `sfSubjectNode` is the locator for that second directory entry. This asymmetry is baked into the generated type schema. + +## The `CredentialBuilder` + +`CredentialBuilder` uses the Curiously Recurring Template Pattern (CRTP) through `LedgerEntryBuilderBase`. The base class holds an `STObject object_{sfLedgerEntry}` as a free (non-templated) object and initialises `sfLedgerEntryType` and `sfFlags`. The CRTP plumbing lets the base class's common setters (`setLedgerIndex`, `setFlags`) return a `CredentialBuilder&` rather than a `LedgerEntryBuilderBase&`, enabling fluent chaining through the derived type. + +The constructor enforces required fields by accepting them as positional arguments — subject, issuer, credentialType, issuerNode, previousTxnID, and previousTxnLgrSeq. These six cannot be omitted; optional fields are added post-construction via individual setters. The use of `std::decay_t` in setter signatures strips references from the declared field type before taking a const-ref parameter, ensuring that no temporary's lifetime is implicitly extended in a surprising way. + +A second constructor accepts a `shared_ptr` and copies the SLE's contents into the builder's internal `STObject` via `object_ = *sle`. This path supports a mutation workflow: read a live entry from the ledger, wrap it in a builder to modify fields, then call `build()` to produce an updated `Credential`. Type enforcement mirrors the read path — an `sfLedgerEntryType` mismatch throws immediately. + +The builder deliberately avoids calling `object_.set(soTemplate)` on the internal `STObject`. As the comment in `LedgerEntryBuilderBase` explains, calling `set()` would create placeholder `STBase` objects for `soeDEFAULT` fields, which then trip the `"may not be explicitly set to default"` guard inside `SLE::applyTemplate()` during `build()`. Keeping the object free of default placeholders lets the `SLE` constructor sort it out. + +`build(uint256 const& index)` calls `std::make_shared(std::move(object_), index)`, consuming the builder's internal state via move, and immediately wraps the resulting SLE in a `Credential`. The move means the builder is not reusable after `build()` — any subsequent use of the builder's setters would operate on a moved-from object, which is an easy invariant to respect since `build()` is the natural terminal call. + +## Relationship to the Broader Codebase + +The autogenerated test file `CredentialTests.cpp` covers four scenarios: builder setter round-trip, builder-from-SLE round-trip, `Credential` throws on wrong entry type, and `CredentialBuilder` throws on wrong entry type. A fifth test verifies that optional fields return `std::nullopt` when not set. These tests are mechanically generated in the same pass as the header and serve as a contract check rather than behavioural coverage. + +The `Credential` type participates in three transaction handlers visible in the codebase: `CredentialCreate`, `CredentialAccept`, and `CredentialDelete`, as well as `DepositAuthorized` — where a credential issued to an account can serve as authorisation evidence when a deposit is gated behind credential requirements. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/DID.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/DID.h.ai.json new file mode 100644 index 0000000000..3a5b52659f --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/DID.h.ai.json @@ -0,0 +1,194 @@ +{ + "args": [ + { + "lineno": 29, + "name": "sle" + }, + { + "lineno": 144, + "name": "account" + }, + { + "lineno": 144, + "name": "ownerNode" + }, + { + "lineno": 144, + "name": "previousTxnID" + }, + { + "lineno": 144, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 151, + "name": "sle" + }, + { + "lineno": 156, + "name": "value" + }, + { + "lineno": 165, + "name": "value" + }, + { + "lineno": 174, + "name": "value" + }, + { + "lineno": 183, + "name": "value" + }, + { + "lineno": 192, + "name": "value" + }, + { + "lineno": 201, + "name": "value" + }, + { + "lineno": 210, + "name": "value" + }, + { + "lineno": 219, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 28, + "name": "DID" + }, + { + "args": [ + "std::decay_t const& account, std::decay_t const& ownerNode, std::decay_t const& previousTxnID, std::decay_t const& previousTxnLgrSeq", + "std::shared_ptr sle" + ], + "lineno": 139, + "name": "DIDBuilder" + } + ], + "description": "Defines the DID ledger entry type and its builder for the XRPL, providing type-safe accessors and a fluent builder interface for constructing and manipulating DID ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/DID.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getAccount" + }, + { + "args": [], + "lineno": 48, + "name": "getDIDDocument" + }, + { + "args": [], + "lineno": 58, + "name": "hasDIDDocument" + }, + { + "args": [], + "lineno": 68, + "name": "getURI" + }, + { + "args": [], + "lineno": 78, + "name": "hasURI" + }, + { + "args": [], + "lineno": 88, + "name": "getData" + }, + { + "args": [], + "lineno": 98, + "name": "hasData" + }, + { + "args": [], + "lineno": 108, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 118, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 128, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 154, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 163, + "name": "setDIDDocument" + }, + { + "args": [ + "value" + ], + "lineno": 172, + "name": "setURI" + }, + { + "args": [ + "value" + ], + "lineno": 181, + "name": "setData" + }, + { + "args": [ + "value" + ], + "lineno": 190, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 199, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 208, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 217, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/DID.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/DID.h.ai.md new file mode 100644 index 0000000000..66817c5254 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/DID.h.ai.md @@ -0,0 +1,46 @@ +# `DID.h` — Decentralized Identifier Ledger Entry Wrapper + +Auto-generated file in `include/xrpl/protocol_autogen/ledger_entries/` that provides the `DID` read-only wrapper and `DIDBuilder` construction interface for the XRPL DID ledger entry type (`ltDID`, 0x0049). It sits in the `xrpl::ledger_entries` namespace alongside ~30 other type-specific files that follow the identical code-generation pattern. + +## What Problem It Solves + +The raw XRPL ledger representation (`SLE` — Serialized Ledger Entry) is untyped: any field can be read from any entry without compile-time guarantees. This file and its siblings impose a typed API on top of that dynamism. Code that holds a `DID` object knows it wraps exactly an `ltDID` entry and can call `getAccount()`, `getDIDDocument()`, etc., without spelling out `sfAccount` or `sfDIDDocument` literals or performing manual presence checks. + +## The DID Ledger Entry + +A DID entry anchors a W3C Decentralized Identifier on the XRPL. It carries: + +- **`sfAccount`** (required) — the XRPL account that owns this identifier. +- **`sfDIDDocument`** (optional) — the raw DID document body, stored inline as a variable-length blob (`SF_VL`). +- **`sfURI`** (optional) — a URI pointing to the DID document when it is stored off-ledger. +- **`sfData`** (optional) — arbitrary attestation or metadata blob associated with the identifier. +- **`sfOwnerNode`** (required) — back-pointer into the account's owner directory page, needed for efficient deletion. +- **`sfPreviousTxnID` / `sfPreviousTxnLgrSeq`** (required) — standard audit trail fields present on every mutable ledger entry. + +The three optional fields (`sfDIDDocument`, `sfURI`, `sfData`) reflect the DID spec's flexibility: a DID can store its document inline, reference it externally via URI, or carry opaque data — or any combination. + +## `DID` — Immutable Read Wrapper + +`DID` extends `LedgerEntryBase`, which holds a `std::shared_ptr` and exposes common field getters (`getType()`, `getKey()`, `getFlags()`, `getLedgerIndex()`). The subclass adds entry-specific accessors. + +Type safety is enforced eagerly in the constructor: it compares `sle_->getType()` against the `static constexpr LedgerEntryType entryType = ltDID` sentinel and throws `std::runtime_error` on mismatch. This makes it impossible to accidentally wrap an `Offer` or `AccountRoot` in a `DID` handle; the error surfaces at the point of construction, not at a later field read. + +Required fields (`getAccount()`, `getOwnerNode()`, `getPreviousTxnID()`, `getPreviousTxnLgrSeq()`) return their native C++ value types directly via `sle_->at(sf*)`. Optional fields return `protocol_autogen::Optional` — a thin alias for `std::optional` — and are paired with explicit `has*()` predicates (`hasDIDDocument()`, `hasURI()`, `hasData()`). The getter delegates to the predicate internally before calling `sle_->at(...)`, avoiding an unconditional access that would throw for absent fields. + +All getters are marked `[[nodiscard]]` and `const`, reinforcing the immutability contract. + +## `DIDBuilder` — Fluent Construction Interface + +`DIDBuilder` inherits from the CRTP base `LedgerEntryBuilderBase`, which provides `setFlags()` and `setLedgerIndex()` returning `Derived&` — enabling method chaining without virtual dispatch overhead. + +The constructor requires the four mandatory fields (account, ownerNode, previousTxnID, previousTxnLgrSeq) and immediately calls their setters, ensuring the entry is valid at minimum even before optional fields are added. A second constructor accepts an existing `SLE const` for the edit-then-rebuild pattern, verifying the entry type before copying the underlying `STObject`. + +A subtle but important design point lives in `LedgerEntryBuilderBase`: the internal `STObject object_{sfLedgerEntry}` is kept as a *free object* — never bound to a SOTemplate. Calling `object_.set(soTemplate)` would create `STBase` placeholders for `soeDEFAULT` fields, which would later cause the `SLE` constructor's `applyTemplate()` to throw *"may not be explicitly set to default"*. By omitting template binding, the builder accumulates only the fields that are explicitly set and leaves the SLE constructor responsible for handling all defaults. + +`build(uint256 const& index)` finalizes construction: it moves the assembled `STObject` into an `SLE` via `std::make_shared(std::move(object_), index)`, then wraps that SLE in a `DID` instance returned by value. After `build()`, the builder's internal state is consumed (moved out), so it should not be reused. + +Both classes expose a `validate()` method that delegates to `protocol_autogen::validateSTObject`, cross-checking the accumulated or wrapped fields against the canonical `SOTemplate` from `LedgerFormats`. The auto-generated tests exercise the full round-trip: builder → `build()` → `DID` getters, as well as SLE-construction and wrong-type rejection. + +## Relationship to Other Files + +`DID.h` is purely a projection of the same ledger entry schema defined elsewhere. The `sfDIDDocument`, `sfURI`, `sfData`, and `sfAccount` field descriptors come from `SField.h`; the `ltDID` constant comes from `LedgerFormats.h`. The auto-generation tooling reads the canonical field schema and emits one file per entry type — `Credential.h`, `Oracle.h`, `Offer.h`, and so on all follow the identical two-class pattern with `LedgerEntryBase` and `LedgerEntryBuilderBase` as the shared infrastructure. Nothing in this file should ever be edited by hand. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Delegate.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Delegate.h.ai.json new file mode 100644 index 0000000000..a56cd8f3cb --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Delegate.h.ai.json @@ -0,0 +1,197 @@ +{ + "args": [ + { + "lineno": 27, + "name": "sle" + }, + { + "lineno": 101, + "name": "account" + }, + { + "lineno": 101, + "name": "authorize" + }, + { + "lineno": 101, + "name": "permissions" + }, + { + "lineno": 101, + "name": "ownerNode" + }, + { + "lineno": 101, + "name": "previousTxnID" + }, + { + "lineno": 101, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 113, + "name": "sle" + }, + { + "lineno": 124, + "name": "value" + }, + { + "lineno": 133, + "name": "value" + }, + { + "lineno": 142, + "name": "value" + }, + { + "lineno": 151, + "name": "value" + }, + { + "lineno": 160, + "name": "value" + }, + { + "lineno": 169, + "name": "value" + }, + { + "lineno": 178, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 22, + "name": "Delegate" + }, + { + "args": [ + "std::decay_t const& account, std::decay_t const& authorize, STArray const& permissions, std::decay_t const& ownerNode, std::decay_t const& previousTxnID, std::decay_t const& previousTxnLgrSeq", + "std::shared_ptr sle" + ], + "lineno": 95, + "name": "DelegateBuilder" + } + ], + "description": "Defines the Delegate ledger entry type for the XRPL, providing an immutable wrapper for type-safe field access and a builder class for constructing Delegate ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Delegate.h", + "functions": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 27, + "name": "Delegate::Delegate" + }, + { + "args": [], + "lineno": 39, + "name": "Delegate::getAccount" + }, + { + "args": [], + "lineno": 48, + "name": "Delegate::getAuthorize" + }, + { + "args": [], + "lineno": 57, + "name": "Delegate::getPermissions" + }, + { + "args": [], + "lineno": 66, + "name": "Delegate::getOwnerNode" + }, + { + "args": [], + "lineno": 75, + "name": "Delegate::getPreviousTxnID" + }, + { + "args": [], + "lineno": 84, + "name": "Delegate::getPreviousTxnLgrSeq" + }, + { + "args": [ + "std::decay_t const& account", + "std::decay_t const& authorize", + "STArray const& permissions", + "std::decay_t const& ownerNode", + "std::decay_t const& previousTxnID", + "std::decay_t const& previousTxnLgrSeq" + ], + "lineno": 101, + "name": "DelegateBuilder::DelegateBuilder" + }, + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 113, + "name": "DelegateBuilder::DelegateBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 124, + "name": "DelegateBuilder::setAccount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 133, + "name": "DelegateBuilder::setAuthorize" + }, + { + "args": [ + "STArray const& value" + ], + "lineno": 142, + "name": "DelegateBuilder::setPermissions" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 151, + "name": "DelegateBuilder::setOwnerNode" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 160, + "name": "DelegateBuilder::setPreviousTxnID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 169, + "name": "DelegateBuilder::setPreviousTxnLgrSeq" + }, + { + "args": [ + "uint256 const& index" + ], + "lineno": 178, + "name": "DelegateBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Delegate.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Delegate.h.ai.md new file mode 100644 index 0000000000..e8e771ce4d --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Delegate.h.ai.md @@ -0,0 +1,33 @@ +# `include/xrpl/protocol_autogen/ledger_entries/Delegate.h` + +## Purpose and Domain Context + +This file is an auto-generated header in the `xrpl::ledger_entries` namespace that defines the C++ surface for the `ltDELEGATE` (type code `0x0083`) on-ledger object. The `Delegate` ledger entry is the persistent record of an account permission delegation relationship: it records that one XRPL account (`sfAccount`) has authorized another (`sfAuthorize`) to submit a constrained set of transactions on its behalf, with the permitted operations encoded as an `sfPermissions` array. + +The ledger entry exists to support a custody and automation pattern. Rather than sharing a private key or registering a regular key with full signing authority, an account owner submits a `DelegateSet` transaction to create or modify a `Delegate` object. The resulting object is what the enforcement layer — primarily `checkTxPermission()` and `loadGranularPermission()` in `DelegateHelpers.h` — reads at transaction-apply time to decide whether a delegate-submitted operation is allowed. + +## Class Design: Immutable Wrapper + +`Delegate` extends `LedgerEntryBase`, a thin base class that holds a `std::shared_ptr` as its only data member. The `const`-qualified pointer is the architectural commitment: once constructed, no field on the wrapped `SLE` can be mutated through this class. All six getter methods — `getAccount()`, `getAuthorize()`, `getPermissions()`, `getOwnerNode()`, `getPreviousTxnID()`, and `getPreviousTxnLgrSeq()` — are `[[nodiscard]] const` accessors that delegate directly to `sle_->at(sf...)` or `sle_->getFieldArray(sf...)`. + +The constructor takes a `std::shared_ptr` and immediately verifies `sle_->getType() == ltDELEGATE`, throwing `std::runtime_error` on mismatch. This is a defensive invariant: the auto-generation framework ensures the type check is always present, making it impossible to accidentally wrap an `AccountRoot` SLE inside a `Delegate` accessor without a hard failure. + +### The `sfPermissions` Asymmetry + +Five of the six fields are strongly typed primitives — `SF_ACCOUNT`, `SF_UINT64`, `SF_UINT256`, `SF_UINT32` — and their getters return their underlying `value_type` directly via `sle_->at()`. The `sfPermissions` field is different: it returns `STArray const&` via `sle_->getFieldArray()`. The inline comment acknowledges this is an "untyped field (unknown)," reflecting that `STArray` is a heterogeneous sequence of `STObject` entries, each containing an `sfPermissionValue` integer. The distinction matters because the permission system uses a two-tier numeric encoding: values in `[1, UINT16_MAX]` represent transaction-type permissions, while values above `UINT16_MAX` represent granular sub-operation flags (see `Permissions.h`). That structured complexity cannot be captured by a scalar `SField` type, so the raw `STArray` reference is surfaced and callers are expected to iterate it themselves. + +## Class Design: Fluent Builder + +`DelegateBuilder` extends `LedgerEntryBuilderBase` using CRTP, which allows the base class's `setLedgerIndex()` and `setFlags()` methods to return `DelegateBuilder&` for method chaining without requiring virtual dispatch or casts at call sites. + +The base class stores an `STObject object_{sfLedgerEntry}` rather than calling `object_.set(soTemplate)` during construction. The comment in `LedgerEntryBuilderBase` explains the deliberate non-call: invoking the template would pre-populate `soeDEFAULT` fields with placeholder `STBase` values, which would later cause `applyTemplate()` — called from the `SLE` constructor — to throw "may not be explicitly set to default." Keeping the object template-free avoids that trap while still allowing the `SLE` constructor to handle field defaults correctly. + +`DelegateBuilder` provides two construction paths. The primary one accepts all six required field values and calls each setter sequentially. The secondary one accepts an `std::shared_ptr` and copies the SLE's field data into the mutable `object_`, enabling in-place modification of an existing ledger entry (useful when `DelegateSet` needs to update a pre-existing delegate relationship). Both paths validate the ledger entry type and throw on mismatch. + +The terminal method, `build(uint256 const& index)`, creates a new `SLE` from the mutable `STObject` and the provided ledger index, then wraps it in a `Delegate` instance. The `std::move(object_)` means the builder is consumed by this call — it cannot be reused after `build()`, which is consistent with the builder-as-factory idiom. + +## Position in the Auto-Generated Layer + +All files in `include/xrpl/protocol_autogen/ledger_entries/` follow the same generated pattern: a read-only wrapper class paired with a builder class, both deriving from the same base templates. The `Delegate.h` content is structurally identical to siblings like `DepositPreauth.h` or `Credential.h`, differing only in entry type, field names, and field types. This uniformity is the direct benefit of code generation: adding a new ledger entry type to the schema produces a correct, consistently structured header without manual authorship. + +The keylet binding for this entry type is `keylet::delegate(AccountID const& account, AccountID const& authorizedAccount)`, declared in `Indexes.h`. The two-account key structure reflects the uniqueness constraint: only one `Delegate` object can exist per `(grantor, grantee)` pair, so the key derivation hashes both account IDs together. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h.ai.json new file mode 100644 index 0000000000..832936fa28 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h.ai.json @@ -0,0 +1,199 @@ +{ + "args": [ + { + "lineno": 27, + "name": "sle" + }, + { + "lineno": 127, + "name": "account" + }, + { + "lineno": 127, + "name": "ownerNode" + }, + { + "lineno": 127, + "name": "previousTxnID" + }, + { + "lineno": 127, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 137, + "name": "sle" + }, + { + "lineno": 148, + "name": "value" + }, + { + "lineno": 157, + "name": "value" + }, + { + "lineno": 166, + "name": "value" + }, + { + "lineno": 175, + "name": "value" + }, + { + "lineno": 184, + "name": "value" + }, + { + "lineno": 193, + "name": "value" + }, + { + "lineno": 202, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 24, + "name": "DepositPreauth" + }, + { + "args": [ + "account", + "ownerNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 124, + "name": "DepositPreauthBuilder" + } + ], + "description": "Defines the DepositPreauth ledger entry and its builder for the XRPL, providing type-safe accessors and a fluent builder interface for constructing and manipulating DepositPreauth ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h", + "functions": [ + { + "args": [ + "sle" + ], + "lineno": 27, + "name": "DepositPreauth" + }, + { + "args": [], + "lineno": 41, + "name": "getAccount" + }, + { + "args": [], + "lineno": 51, + "name": "getAuthorize" + }, + { + "args": [], + "lineno": 62, + "name": "hasAuthorize" + }, + { + "args": [], + "lineno": 71, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 80, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 89, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 98, + "name": "getAuthorizeCredentials" + }, + { + "args": [], + "lineno": 110, + "name": "hasAuthorizeCredentials" + }, + { + "args": [ + "account", + "ownerNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 127, + "name": "DepositPreauthBuilder" + }, + { + "args": [ + "sle" + ], + "lineno": 137, + "name": "DepositPreauthBuilder" + }, + { + "args": [ + "value" + ], + "lineno": 148, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 157, + "name": "setAuthorize" + }, + { + "args": [ + "value" + ], + "lineno": 166, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 175, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 184, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 193, + "name": "setAuthorizeCredentials" + }, + { + "args": [ + "index" + ], + "lineno": 202, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h.ai.md new file mode 100644 index 0000000000..0e54610915 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/DepositPreauth.h.ai.md @@ -0,0 +1,43 @@ +# `DepositPreauth.h` — Auto-generated Ledger Entry Wrapper + +## Purpose and Context + +This file is part of the `protocol_autogen` layer — a collection of auto-generated C++ headers that provide typed, ergonomic access to every XRPL ledger object. It defines two classes, `DepositPreauth` and `DepositPreauthBuilder`, that together wrap the raw `SLE` (Serialized Ledger Entry) for the `ltDEPOSIT_PREAUTH` (type code `0x0070`) ledger object. + +On the XRPL, a `DepositPreauth` object is created when an account with Deposit Authorization enabled pre-approves a specific counterparty — or a set of credential types — to send it payments without going through the normal deposit authorization gate. The ledger entry records that approval and is keyed by the owning account and the authorized party. Importantly, the entry supports two distinct authorization mechanisms: a simple account-to-account grant via `sfAuthorize`, and a more powerful credential-set grant via `sfAuthorizeCredentials` (introduced under the Credentials amendment). Only one of these two optional fields is expected to be present in any given entry. + +## Class Design: Immutable Wrapper + Fluent Builder + +The file follows the same architectural pattern used throughout `protocol_autogen`: a read-only view class paired with a separate builder class that handles construction. This separation enforces at the type level that an already-stored ledger entry cannot be mutated through its wrapper — callers cannot accidentally write through a `DepositPreauth` reference they received from the ledger state. + +`DepositPreauth` extends `LedgerEntryBase`, which holds a `std::shared_ptr` (note the `const`). All field getters on the wrapper delegate directly to the underlying `SLE` via `sle_->at(sfField)` or `sle_->isFieldPresent(sfField)`. The constructor validates the entry type immediately: if the caller wraps an `SLE` whose `getType()` doesn't return `ltDEPOSIT_PREAUTH`, a `std::runtime_error` is thrown. This fail-fast guard prevents silent type confusion when reading ledger state. + +`DepositPreauthBuilder` uses the CRTP pattern via `LedgerEntryBuilderBase`: the base class templatizes on the derived type so that common setters like `setFlags()` and `setLedgerIndex()` return `Derived&` instead of `LedgerEntryBuilderBase&`, enabling uninterrupted method chaining across both base and derived setters. The builder stores its working state in an `STObject object_{sfLedgerEntry}`, deliberately avoiding applying the SOTemplate at construction time (see the comment in `LedgerEntryBuilderBase`) to prevent the SLE constructor's `applyTemplate()` from rejecting default-valued fields. + +## Field Inventory + +Four fields are required and must be supplied to the primary constructor: + +- **`sfAccount`** — the owning account that granted the preauthorization. +- **`sfOwnerNode`** — the page index within the owner's directory that holds this entry, used for efficient deletion. +- **`sfPreviousTxnID`** — the 256-bit hash of the transaction that last modified this entry, forming an audit chain. +- **`sfPreviousTxnLgrSeq`** — the ledger sequence number of that last-modifying transaction. + +Two fields are optional and mutually exclusive in practice: + +- **`sfAuthorize`** — the single `AccountID` that has been pre-approved. Wrapped with `protocol_autogen::Optional`, which resolves to `std::optional` for non-reference value types. +- **`sfAuthorizeCredentials`** — an `STArray` of credential type descriptors granting access to any account holding matching credentials. Because `STArray` is a non-copyable object internally stored by reference in the SLE, `getAuthorizeCredentials()` returns `std::optional>` rather than a value copy. This is the one field that deviates from the uniform getter signature; the comment in the source marks it as an "untyped field (unknown)", meaning the code generator could not resolve a strongly-typed accessor for it. + +Companion `has*()` predicates (`hasAuthorize()`, `hasAuthorizeCredentials()`) allow presence checks without materializing the optional wrapper. + +## Builder Construction Paths + +The builder offers two entry points. The primary constructor takes the four required fields and immediately calls their corresponding setters, establishing a minimal valid in-progress object. Optional fields (`setAuthorize`, `setAuthorizeCredentials`) are available for chaining afterward. + +The second constructor accepts an existing `std::shared_ptr` and copies its state into the builder's internal `STObject`, enabling a read-modify-write pattern: load an entry from the ledger, wrap it in a builder, call setters to patch specific fields, then call `build()` to produce a new `DepositPreauth` wrapper containing a fresh `SLE`. The same type-guard check (`sfLedgerEntryType != ltDEPOSIT_PREAUTH`) is enforced here, throwing `std::runtime_error` on mismatch. + +The terminal `build(uint256 const& index)` method moves the internal `STObject` into a new `SLE` keyed by `index`, then wraps it in a `DepositPreauth`. This move means a builder is single-use: after `build()` the internal object is in a valid-but-unspecified state and should not be reused. + +## Relationship to the Transaction Layer + +The ledger entry wrapper in this file is purely a read/construct facility. The business logic that creates, validates, and destroys `DepositPreauth` objects lives in `src/libxrpl/tx/transactors/payment/DepositPreauth.cpp`, which operates directly on raw `SLE` pointers via `View`. The `sfAuthorizeCredentials` field in the ledger entry mirrors the credentials-amendment path in the transactor, where `preflight` enforces that exactly one of `sfAuthorize`, `sfUnauthorize`, `sfAuthorizeCredentials`, or `sfUnauthorizeCredentials` is present in the transaction — the dual optional structure of this wrapper reflects that protocol-level exclusivity. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h.ai.json new file mode 100644 index 0000000000..2718479189 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h.ai.json @@ -0,0 +1,407 @@ +{ + "args": [ + { + "lineno": 27, + "name": "sle" + }, + { + "lineno": 349, + "name": "indexes" + }, + { + "lineno": 349, + "name": "rootIndex" + }, + { + "lineno": 359, + "name": "sle" + }, + { + "lineno": 370, + "name": "value" + }, + { + "lineno": 380, + "name": "value" + }, + { + "lineno": 390, + "name": "value" + }, + { + "lineno": 400, + "name": "value" + }, + { + "lineno": 410, + "name": "value" + }, + { + "lineno": 420, + "name": "value" + }, + { + "lineno": 430, + "name": "value" + }, + { + "lineno": 440, + "name": "value" + }, + { + "lineno": 450, + "name": "value" + }, + { + "lineno": 460, + "name": "value" + }, + { + "lineno": 470, + "name": "value" + }, + { + "lineno": 480, + "name": "value" + }, + { + "lineno": 490, + "name": "value" + }, + { + "lineno": 500, + "name": "value" + }, + { + "lineno": 510, + "name": "value" + }, + { + "lineno": 520, + "name": "value" + }, + { + "lineno": 530, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 24, + "name": "DirectoryNode" + }, + { + "args": [ + "std::decay_t const& indexes", + "std::decay_t const& rootIndex" + ], + "lineno": 344, + "name": "DirectoryNodeBuilder" + } + ], + "description": "Provides an immutable wrapper and builder for the DirectoryNode ledger entry type in the XRPL, enabling type-safe field access and fluent construction of DirectoryNode ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h", + "functions": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 27, + "name": "DirectoryNode" + }, + { + "args": [], + "lineno": 41, + "name": "getOwner" + }, + { + "args": [], + "lineno": 51, + "name": "hasOwner" + }, + { + "args": [], + "lineno": 61, + "name": "getTakerPaysCurrency" + }, + { + "args": [], + "lineno": 71, + "name": "hasTakerPaysCurrency" + }, + { + "args": [], + "lineno": 81, + "name": "getTakerPaysIssuer" + }, + { + "args": [], + "lineno": 91, + "name": "hasTakerPaysIssuer" + }, + { + "args": [], + "lineno": 101, + "name": "getTakerPaysMPT" + }, + { + "args": [], + "lineno": 111, + "name": "hasTakerPaysMPT" + }, + { + "args": [], + "lineno": 121, + "name": "getTakerGetsCurrency" + }, + { + "args": [], + "lineno": 131, + "name": "hasTakerGetsCurrency" + }, + { + "args": [], + "lineno": 141, + "name": "getTakerGetsIssuer" + }, + { + "args": [], + "lineno": 151, + "name": "hasTakerGetsIssuer" + }, + { + "args": [], + "lineno": 161, + "name": "getTakerGetsMPT" + }, + { + "args": [], + "lineno": 171, + "name": "hasTakerGetsMPT" + }, + { + "args": [], + "lineno": 181, + "name": "getExchangeRate" + }, + { + "args": [], + "lineno": 191, + "name": "hasExchangeRate" + }, + { + "args": [], + "lineno": 201, + "name": "getIndexes" + }, + { + "args": [], + "lineno": 211, + "name": "getRootIndex" + }, + { + "args": [], + "lineno": 221, + "name": "getIndexNext" + }, + { + "args": [], + "lineno": 231, + "name": "hasIndexNext" + }, + { + "args": [], + "lineno": 241, + "name": "getIndexPrevious" + }, + { + "args": [], + "lineno": 251, + "name": "hasIndexPrevious" + }, + { + "args": [], + "lineno": 261, + "name": "getNFTokenID" + }, + { + "args": [], + "lineno": 271, + "name": "hasNFTokenID" + }, + { + "args": [], + "lineno": 281, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 291, + "name": "hasPreviousTxnID" + }, + { + "args": [], + "lineno": 301, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 311, + "name": "hasPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 321, + "name": "getDomainID" + }, + { + "args": [], + "lineno": 331, + "name": "hasDomainID" + }, + { + "args": [ + "std::decay_t const& indexes", + "std::decay_t const& rootIndex" + ], + "lineno": 349, + "name": "DirectoryNodeBuilder" + }, + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 359, + "name": "DirectoryNodeBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 370, + "name": "setOwner" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 380, + "name": "setTakerPaysCurrency" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 390, + "name": "setTakerPaysIssuer" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 400, + "name": "setTakerPaysMPT" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 410, + "name": "setTakerGetsCurrency" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 420, + "name": "setTakerGetsIssuer" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 430, + "name": "setTakerGetsMPT" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 440, + "name": "setExchangeRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 450, + "name": "setIndexes" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 460, + "name": "setRootIndex" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 470, + "name": "setIndexNext" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 480, + "name": "setIndexPrevious" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 490, + "name": "setNFTokenID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 500, + "name": "setPreviousTxnID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 510, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 520, + "name": "setDomainID" + }, + { + "args": [ + "uint256 const& index" + ], + "lineno": 530, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h.ai.md new file mode 100644 index 0000000000..b84b75164a --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/DirectoryNode.h.ai.md @@ -0,0 +1,36 @@ +# `DirectoryNode.h` — Auto-Generated Type-Safe Wrapper for the DirectoryNode Ledger Entry + +## Role and Purpose + +This file is part of the `xrpl/protocol_autogen/ledger_entries` layer — a set of auto-generated, type-safe C++ wrappers that sit above the raw `SLE` (Serialized Ledger Entry) API. It defines `DirectoryNode` (the read-only wrapper) and `DirectoryNodeBuilder` (the fluent construction interface) for the `ltDIR_NODE` (0x0064) ledger entry type, known in RPC contexts as `"directory"`. + +The `DirectoryNode` is one of the most structurally important ledger objects in the XRPL. It implements a paged, doubly-linked-list structure that organizes collections of ledger object keys: an account's owned objects (offers, escrows, trust lines, etc.), an order book's offers at a given quality level, or an NFToken's buy/sell offer listings. Because a single directory can grow beyond what fits in one ledger entry, the design breaks it into pages, each holding a batch of 256-bit entry keys in `sfIndexes` (a `VECTOR256`), chained together via `sfIndexNext` and `sfIndexPrevious` (both optional `uint64` page pointers). `sfRootIndex` is required on every page and always points back to the first page, giving O(1) navigation to the head of the chain regardless of which page is currently being read. + +## The Two Classes + +`DirectoryNode` extends `LedgerEntryBase`, which holds a `std::shared_ptr` — the `const` qualifier is the key architectural choice that enforces immutability throughout the read path. Every field accessor is `[[nodiscard]] const` and returns either a direct value (for required fields like `sfIndexes` and `sfRootIndex`) or `protocol_autogen::Optional` for optional ones. + +`protocol_autogen::Optional` (defined in `Utils.h`) is a thin type alias: for value types it resolves to `std::optional`, and for reference types it wraps in `std::optional>`. This handles both cases uniformly without forcing copies of reference fields. + +`DirectoryNodeBuilder` inherits from `LedgerEntryBuilderBase` via CRTP. The base class initializes an internal `STObject object_` with `sfLedgerEntryType` and `sfFlags`, avoiding the `SOTemplate` initialization intentionally — the comment in `LedgerEntryBuilderBase` explains that calling `object_.set(soTemplate)` would create placeholder `STBase` objects for `soeDEFAULT` fields, causing `applyTemplate()` to throw "may not be explicitly set to default" when the SLE is constructed. All setter methods return `DirectoryNodeBuilder&` by calling `static_cast(*this)` in the base, enabling clean method chaining. + +## Semantic Polymorphism in One Entry Type + +The most architecturally notable aspect of `DirectoryNode` is that it serves multiple semantically distinct purposes distinguished only by which optional fields are present: + +- **Owner directory**: `sfOwner` is set to the account ID; no exchange-rate fields. Used to track all ledger objects belonging to an account. +- **Order book directory**: `sfTakerPaysCurrency`, `sfTakerPaysIssuer` (or `sfTakerPaysMPT` for Multi-Purpose Token orders), `sfTakerGetsCurrency`, `sfTakerGetsIssuer` (or `sfTakerGetsMPT`), and `sfExchangeRate` are set. The `sfExchangeRate` encodes the quality tier and is physically embedded in the last 8 bytes of the ledger entry key via `keylet::quality()`, enabling lexicographic iteration across quality levels using simple key arithmetic. +- **NFToken offer directory**: `sfNFTokenID` identifies the token whose buy or sell offers this directory page lists. +- **Domain directory**: `sfDomainID` groups entries within a domain context. + +The generated wrapper exposes a parallel `hasX()` / `getX()` pair for each optional field, giving callers a clean way to determine which semantic role a given `DirectoryNode` is playing without inspecting raw serialized bytes. + +## Type Safety and Error Handling + +The `DirectoryNode` constructor validates the SLE type immediately on construction, throwing `std::runtime_error` if `sle_->getType() != ltDIR_NODE`. The same check appears in the `DirectoryNodeBuilder(std::shared_ptr)` constructor, which allows an existing SLE to be loaded into a builder for modification. This dual-path construction — either from required primitives or from an existing SLE — handles both the "create new" and "modify existing" workflows while maintaining the type guarantee in both cases. + +## Build Cycle + +`DirectoryNodeBuilder::build(uint256 const& index)` finalizes construction by moving the accumulated `STObject` into a new `SLE` keyed at `index`, then wrapping that `SLE` in a `DirectoryNode`. The `SLE` constructor calls `applyTemplate()` internally, which reconciles the free `STObject` fields against the `ltDIR_NODE` format template — at this point any missing required fields or invalid field types would produce an error from the ledger formats layer, not from the builder itself. + +Because the file header reads `// This file is auto-generated. Do not edit.`, the canonical source of the field list and requiredness annotations lives in the upstream code generator. Adding a new field to `DirectoryNode` requires updating the generator schema, not this file directly. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Escrow.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Escrow.h.ai.json new file mode 100644 index 0000000000..03bf0ffe56 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Escrow.h.ai.json @@ -0,0 +1,364 @@ +{ + "args": [ + { + "lineno": 31, + "name": "sle" + }, + { + "lineno": 285, + "name": "account" + }, + { + "lineno": 285, + "name": "destination" + }, + { + "lineno": 285, + "name": "amount" + }, + { + "lineno": 285, + "name": "ownerNode" + }, + { + "lineno": 285, + "name": "previousTxnID" + }, + { + "lineno": 285, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 295, + "name": "sle" + }, + { + "lineno": 301, + "name": "value" + }, + { + "lineno": 310, + "name": "value" + }, + { + "lineno": 319, + "name": "value" + }, + { + "lineno": 328, + "name": "value" + }, + { + "lineno": 337, + "name": "value" + }, + { + "lineno": 346, + "name": "value" + }, + { + "lineno": 355, + "name": "value" + }, + { + "lineno": 364, + "name": "value" + }, + { + "lineno": 373, + "name": "value" + }, + { + "lineno": 382, + "name": "value" + }, + { + "lineno": 391, + "name": "value" + }, + { + "lineno": 400, + "name": "value" + }, + { + "lineno": 409, + "name": "value" + }, + { + "lineno": 418, + "name": "value" + }, + { + "lineno": 427, + "name": "value" + }, + { + "lineno": 436, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 28, + "name": "Escrow" + }, + { + "args": [ + "account", + "destination", + "amount", + "ownerNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 277, + "name": "EscrowBuilder" + } + ], + "description": "Defines the Escrow ledger entry wrapper and builder for XRPL, providing type-safe field access and a fluent interface for constructing Escrow ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Escrow.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getAccount" + }, + { + "args": [], + "lineno": 48, + "name": "getSequence" + }, + { + "args": [], + "lineno": 59, + "name": "hasSequence" + }, + { + "args": [], + "lineno": 69, + "name": "getDestination" + }, + { + "args": [], + "lineno": 78, + "name": "getAmount" + }, + { + "args": [], + "lineno": 87, + "name": "getCondition" + }, + { + "args": [], + "lineno": 98, + "name": "hasCondition" + }, + { + "args": [], + "lineno": 108, + "name": "getCancelAfter" + }, + { + "args": [], + "lineno": 119, + "name": "hasCancelAfter" + }, + { + "args": [], + "lineno": 129, + "name": "getFinishAfter" + }, + { + "args": [], + "lineno": 140, + "name": "hasFinishAfter" + }, + { + "args": [], + "lineno": 150, + "name": "getSourceTag" + }, + { + "args": [], + "lineno": 161, + "name": "hasSourceTag" + }, + { + "args": [], + "lineno": 171, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 182, + "name": "hasDestinationTag" + }, + { + "args": [], + "lineno": 192, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 201, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 210, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 219, + "name": "getDestinationNode" + }, + { + "args": [], + "lineno": 230, + "name": "hasDestinationNode" + }, + { + "args": [], + "lineno": 240, + "name": "getTransferRate" + }, + { + "args": [], + "lineno": 251, + "name": "hasTransferRate" + }, + { + "args": [], + "lineno": 261, + "name": "getIssuerNode" + }, + { + "args": [], + "lineno": 272, + "name": "hasIssuerNode" + }, + { + "args": [ + "value" + ], + "lineno": 299, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 308, + "name": "setSequence" + }, + { + "args": [ + "value" + ], + "lineno": 317, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 326, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 335, + "name": "setCondition" + }, + { + "args": [ + "value" + ], + "lineno": 344, + "name": "setCancelAfter" + }, + { + "args": [ + "value" + ], + "lineno": 353, + "name": "setFinishAfter" + }, + { + "args": [ + "value" + ], + "lineno": 362, + "name": "setSourceTag" + }, + { + "args": [ + "value" + ], + "lineno": 371, + "name": "setDestinationTag" + }, + { + "args": [ + "value" + ], + "lineno": 380, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 389, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 398, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 407, + "name": "setDestinationNode" + }, + { + "args": [ + "value" + ], + "lineno": 416, + "name": "setTransferRate" + }, + { + "args": [ + "value" + ], + "lineno": 425, + "name": "setIssuerNode" + }, + { + "args": [ + "index" + ], + "lineno": 434, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 15, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Escrow.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Escrow.h.ai.md new file mode 100644 index 0000000000..239ef1f547 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Escrow.h.ai.md @@ -0,0 +1,43 @@ +# `Escrow.h` — Auto-Generated Ledger Entry Wrapper for XRPL Escrow + +## Role in the System + +This file is part of the `protocol_autogen` subsystem under `xrpl::ledger_entries`, which provides statically-typed C++ wrappers for every ledger entry type stored in the XRP Ledger. `Escrow.h` specifically encapsulates the `ltESCROW` (type code `0x0075`) ledger object — the on-chain record created when an XRP amount is held in a cryptographically or time-conditioned escrow. The file is auto-generated and must not be hand-edited; its source of truth is a schema definition that drives code generation for all ledger entry types in this directory. + +The file defines two classes: `Escrow`, an immutable read-only view, and `EscrowBuilder`, a fluent construction interface. This two-class pattern appears across every sibling file in the directory (e.g., `Check.h`, `PayChannel.h`, `Offer.h`) and is mandated by the base classes in `LedgerEntryBase.h` and `LedgerEntryBuilderBase.h`. + +## `Escrow` — The Immutable Wrapper + +`Escrow` inherits from `LedgerEntryBase`, which holds a `std::shared_ptr` — a shared, const-qualified pointer to a Serialized Ledger Entry. The `const` qualifier is load-bearing: it ensures that once an `Escrow` object is constructed from a live ledger state, the underlying serialized object cannot be mutated through this interface. The base class exposes common fields (`sfFlags`, `sfLedgerEntryType`, `sfLedgerIndex`, `getKey()`) and a `validate()` method that checks the SLE against the registered `SOTemplate` for `ltESCROW`. + +The constructor performs a runtime type guard: it verifies `sle_->getType() == ltESCROW` and throws `std::runtime_error` on mismatch. This is the only safety mechanism — there is no compile-time enforcement that an arbitrary `shared_ptr` contains the right entry type — so the check is essential to prevent silent field misreads when the wrong SLE is wrapped. + +### Field Access Pattern + +Fields divide into two categories based on their serialized optionality: + +**Required fields** (`soeREQUIRED`) — `sfAccount`, `sfDestination`, `sfAmount`, `sfOwnerNode`, `sfPreviousTxnID`, `sfPreviousTxnLgrSeq` — are accessed via a single `getX()` method that reads directly from the SLE with `sle_->at(sfField)`. These are guaranteed present by the ledger's own schema enforcement, so no null-check is needed. + +**Optional fields** (`soeOPTIONAL`) — `sfSequence`, `sfCondition`, `sfCancelAfter`, `sfFinishAfter`, `sfSourceTag`, `sfDestinationTag`, `sfDestinationNode`, `sfTransferRate`, `sfIssuerNode` — are surfaced as a pair: `hasX()` checks `sle_->isFieldPresent()`, and `getX()` returns `protocol_autogen::Optional`. That alias, defined in `Utils.h`, resolves to `std::optional>` when `T` is a reference type, or plain `std::optional` when it is a value type. This distinction matters for large or non-copyable field types where a reference wrapper avoids an unnecessary copy. + +### Escrow-Specific Field Semantics + +- **`sfCondition`** (`SF_VL`, variable-length blob): The binary PREIMAGE-SHA-256 crypto-condition defined in RFC 3230. When present, the escrow can only be finished by a transaction that supplies the matching fulfillment (`sfFulfillment`). When absent, the escrow is purely time-gated. +- **`sfCancelAfter` / `sfFinishAfter`**: Both are `uint32` values representing ripple epoch timestamps (seconds since January 1, 2000). The escrow becomes cancellable after `sfCancelAfter` passes, and becomes executable (finishable) after `sfFinishAfter` passes. Either or both may be present; a valid escrow must have at least one of these or a condition. +- **`sfSequence`**: The transaction sequence number of the originating `EscrowCreate` transaction. Together with `sfAccount`, it is used to compute the escrow's canonical ledger key, so it serves a structural identity role rather than just bookkeeping. +- **`sfOwnerNode` / `sfDestinationNode`**: 64-bit directory page indices. These are internal skip-list pointers used to efficiently locate the escrow's entries in the owner directory (from the creator's perspective) and the destination directory. `sfDestinationNode` is optional because early escrow entries pre-dating the destination directory feature do not have it. +- **`sfTransferRate` / `sfIssuerNode`**: These fields appear in the schema for escrowed IOU amounts where an issuer's transfer rate must be applied at settlement. For pure XRP escrows they will be absent. + +## `EscrowBuilder` — The Fluent Construction Interface + +`EscrowBuilder` inherits from `LedgerEntryBuilderBase`, a CRTP template whose `Derived` parameter ensures that the common setters (`setFlags()`, `setLedgerIndex()`) return `EscrowBuilder&` rather than a `LedgerEntryBuilderBase&`, preserving method-chain fluency without any virtual dispatch overhead. + +The base class initializes an `STObject object_{sfLedgerEntry}` — deliberately *not* bound to a schema template. This is a subtle but important design decision explained in the base class comments: calling `object_.set(soTemplate)` would insert `STBase` placeholder instances for `soeDEFAULT` fields, which causes the `SLE` constructor's `applyTemplate()` to throw "may not be explicitly set to default". By keeping the object "free" and only populating fields that are actually set, the builder lets `applyTemplate()` handle defaults correctly when `build()` finally constructs the `SLE`. + +The primary constructor accepts all six required fields and immediately delegates to the corresponding setters, preventing callers from forgetting mandatory state. The secondary constructor accepts an existing `std::shared_ptr` and copies its content into `object_`, enabling a read-modify-write workflow where an existing escrow entry is cloned into a builder, optional fields are adjusted, and a new `SLE` is produced. + +`build(uint256 const& index)` is the terminal operation. It constructs a `std::shared_ptr` by moving `object_` into the `SLE` constructor along with the caller-supplied ledger index, then wraps the result in an `Escrow`. The index is intentionally not accumulated inside the builder — it must be computed externally (typically as a function of `sfAccount` and `sfSequence`) and supplied at finalization, keeping the builder independent of key-derivation logic. + +## Relationship to the Wider `protocol_autogen` Layer + +Every file in `include/xrpl/protocol_autogen/ledger_entries/` follows this exact two-class pattern, generated from a common schema. The `LedgerEntryBase` and `LedgerEntryBuilderBase` base classes encapsulate all behavior that is truly shared — immutability, SLE ownership, flag access, validation against the live `LedgerFormats` registry — while each generated file contributes only the field-specific getters and setters. The result is a consistent, auditable API surface across all ~30 ledger entry types, with type safety enforced at the boundary where an untyped `SLE` is first wrapped into a concrete subclass. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h.ai.json new file mode 100644 index 0000000000..5a96c94020 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h.ai.json @@ -0,0 +1,195 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 19, + "name": "FeeSettings" + }, + { + "args": [], + "lineno": 224, + "name": "FeeSettingsBuilder" + }, + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 231, + "name": "FeeSettingsBuilder" + } + ], + "description": "Defines the FeeSettings ledger entry and its builder for the XRPL protocol, providing type-safe accessors and mutators for ledger entry fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getBaseFee" + }, + { + "args": [], + "lineno": 49, + "name": "hasBaseFee" + }, + { + "args": [], + "lineno": 59, + "name": "getReferenceFeeUnits" + }, + { + "args": [], + "lineno": 70, + "name": "hasReferenceFeeUnits" + }, + { + "args": [], + "lineno": 80, + "name": "getReserveBase" + }, + { + "args": [], + "lineno": 91, + "name": "hasReserveBase" + }, + { + "args": [], + "lineno": 101, + "name": "getReserveIncrement" + }, + { + "args": [], + "lineno": 112, + "name": "hasReserveIncrement" + }, + { + "args": [], + "lineno": 122, + "name": "getBaseFeeDrops" + }, + { + "args": [], + "lineno": 133, + "name": "hasBaseFeeDrops" + }, + { + "args": [], + "lineno": 143, + "name": "getReserveBaseDrops" + }, + { + "args": [], + "lineno": 154, + "name": "hasReserveBaseDrops" + }, + { + "args": [], + "lineno": 164, + "name": "getReserveIncrementDrops" + }, + { + "args": [], + "lineno": 175, + "name": "hasReserveIncrementDrops" + }, + { + "args": [], + "lineno": 185, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 196, + "name": "hasPreviousTxnID" + }, + { + "args": [], + "lineno": 206, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 217, + "name": "hasPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 246, + "name": "setBaseFee" + }, + { + "args": [ + "value" + ], + "lineno": 256, + "name": "setReferenceFeeUnits" + }, + { + "args": [ + "value" + ], + "lineno": 266, + "name": "setReserveBase" + }, + { + "args": [ + "value" + ], + "lineno": 276, + "name": "setReserveIncrement" + }, + { + "args": [ + "value" + ], + "lineno": 286, + "name": "setBaseFeeDrops" + }, + { + "args": [ + "value" + ], + "lineno": 296, + "name": "setReserveBaseDrops" + }, + { + "args": [ + "value" + ], + "lineno": 306, + "name": "setReserveIncrementDrops" + }, + { + "args": [ + "value" + ], + "lineno": 316, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 326, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 336, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h.ai.md new file mode 100644 index 0000000000..9029c2b216 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/FeeSettings.h.ai.md @@ -0,0 +1,44 @@ +# `FeeSettings.h` — Auto-Generated FeeSettings Ledger Entry Wrapper + +## Role in the System + +This auto-generated header (do not edit) defines the type-safe C++ interface for the `FeeSettings` ledger object (`ltFEE_SETTINGS`, type code `0x0073`). The `FeeSettings` ledger entry is a **singleton object** present in every XRPL ledger state — there is exactly one, and it stores the network's consensus-voted fee policy: the base transaction fee, owner reserve requirements, and their per-drop equivalents. Code that needs to read or write these network parameters uses this file's two classes: `FeeSettings` (immutable reader) and `FeeSettingsBuilder` (construction and mutation). + +The file lives in `include/xrpl/protocol_autogen/ledger_entries/`, alongside a parallel class for every other ledger entry type on XRPL. The code-generation pattern exists to avoid handwritten, error-prone field access scattered throughout the codebase — every ledger entry type gets the same disciplined wrapper/builder pair, enforcing type safety and optionality at compile time. + +## `FeeSettings` — Immutable Read Wrapper + +`FeeSettings` extends `LedgerEntryBase`, which holds a `std::shared_ptr` (Serialized Ledger Entry). Immutability is enforced by wrapping a `const`-qualified `SLE`: callers cannot accidentally mutate the ledger state through this object. + +The constructor validates the SLE type on construction, throwing `std::runtime_error` immediately if the wrapped `SLE` is not actually a `ltFEE_SETTINGS` entry. This is the primary defense against misuse — if code accidentally wraps the wrong entry type, it fails loudly at the call site rather than silently reading garbage field values later. + +Every field exposed by `FeeSettings` is marked `soeOPTIONAL` in the protocol definition. As a result, every getter returns `protocol_autogen::Optional`, which resolves to `std::optional` for value types (or `std::optional>` for reference types, via a `std::conditional_t` alias in `Utils.h`). Each getter is paired with a `has*()` predicate that directly calls `sle_->isFieldPresent(sf...)`. Callers are expected to check presence before dereferencing. The `[[nodiscard]]` attribute on every getter enforces that return values are not silently dropped. + +### The Two-Generation Field Sets + +A notable design feature is the dual representation of fee amounts. The entry exposes two parallel groups of fields: + +- **Legacy integer fields**: `sfBaseFee` (uint64), `sfReferenceFeeUnits` (uint32), `sfReserveBase` (uint32), `sfReserveIncrement` (uint32). These predate the `XRPFees` amendment and encode fees in "fee units" — an abstraction layer where 1 XRP equals a fixed number of fee units. +- **New drops-denominated fields**: `sfBaseFeeDrops`, `sfReserveBaseDrops`, `sfReserveIncrementDrops`, all typed as `STAmount`. Post `XRPFees` amendment, only the `*Drops` fields are populated, and the `Change` transactor in `Change.cpp` enforces that these fields must be present when the amendment is active. + +Exposing both groups as optional fields on the same class allows consumers to handle both pre- and post-amendment ledger states without branching on the type system — the `has*()` predicates serve as the runtime switch. + +`sfPreviousTxnID` (uint256) and `sfPreviousTxnLgrSeq` (uint32) complete the field set, tracking the hash and ledger sequence of the last transaction that modified this entry — a standard audit trail carried by many ledger objects. + +## `FeeSettingsBuilder` — Fluent Construction + +`FeeSettingsBuilder` extends `LedgerEntryBuilderBase`, which uses CRTP to return the concrete `FeeSettingsBuilder&` from every inherited setter, making fluent chaining type-correct without virtual dispatch or casting at the call site. + +The base class initializes an internal `STObject object_{sfLedgerEntry}` and sets `sfLedgerEntryType` and `sfFlags` in the constructor, but deliberately does **not** call `object_.set(soTemplate)`. The comment in `LedgerEntryBuilderBase` explains why: calling `set(soTemplate)` would create `STBase` placeholders for `soeDEFAULT` fields, which causes `applyTemplate()` to throw "may not be explicitly set to default" when the `SLE` is subsequently constructed. By omitting that call, the builder leaves the `STObject` as a free object; the `SLE` constructor applies the template correctly when `build()` is called. + +The builder provides two construction paths: +1. **Default construction** — starts fresh with only `sfLedgerEntryType` and `sfFlags`. +2. **From existing SLE** — copies the `SLE`'s fields into `object_` (via `object_ = *sle`), allowing modification of an existing entry before reconstructing it. The SLE-based constructor validates the entry type and throws on mismatch, matching the `FeeSettings` wrapper's own guard. + +The `build(uint256 const& index)` method moves `object_` into a new `SLE` keyed by `index`, wraps it in a `shared_ptr`, and constructs the final `FeeSettings` wrapper. After `build()` the builder's internal state has been moved from and should not be reused. + +## Validation + +Both `FeeSettings` and `FeeSettingsBuilder` inherit `validate()`, which compares the current field population against the `SOTemplate` retrieved from `LedgerFormats::getInstance()`. This confirms that required fields are present and no invalid fields are set — useful in tests and as an assertion before persisting an entry. + +The unit tests in `FeeSettingsTests.cpp` verify all four critical invariants: builder-setter round-trips, SLE-to-builder-to-wrapper round-trips, type mismatch exceptions from both the wrapper and the builder, and that absent optional fields correctly return `std::nullopt`. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h.ai.json new file mode 100644 index 0000000000..7f868b2d42 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h.ai.json @@ -0,0 +1,113 @@ +{ + "args": [ + { + "lineno": 30, + "name": "sle" + }, + { + "lineno": 94, + "name": "hashes" + }, + { + "lineno": 101, + "name": "sle" + }, + { + "lineno": 106, + "name": "value" + }, + { + "lineno": 115, + "name": "value" + }, + { + "lineno": 124, + "name": "value" + }, + { + "lineno": 133, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 27, + "name": "LedgerHashes" + }, + { + "args": [ + "std::decay_t const& hashes", + "std::shared_ptr sle" + ], + "lineno": 89, + "name": "LedgerHashesBuilder" + } + ], + "description": "Defines the LedgerHashes ledger entry type for XRPL, providing type-safe accessors and a builder for constructing or copying ledger entries of this type.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getFirstLedgerSequence" + }, + { + "args": [], + "lineno": 48, + "name": "hasFirstLedgerSequence" + }, + { + "args": [], + "lineno": 58, + "name": "getLastLedgerSequence" + }, + { + "args": [], + "lineno": 68, + "name": "hasLastLedgerSequence" + }, + { + "args": [], + "lineno": 78, + "name": "getHashes" + }, + { + "args": [ + "value" + ], + "lineno": 104, + "name": "setFirstLedgerSequence" + }, + { + "args": [ + "value" + ], + "lineno": 113, + "name": "setLastLedgerSequence" + }, + { + "args": [ + "value" + ], + "lineno": 122, + "name": "setHashes" + }, + { + "args": [ + "index" + ], + "lineno": 131, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h.ai.md new file mode 100644 index 0000000000..cfcd8c855e --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/LedgerHashes.h.ai.md @@ -0,0 +1,43 @@ +# `LedgerHashes.h` — Auto-Generated Type-Safe Wrapper for the `ltLEDGER_HASHES` Ledger Entry + +## Role in the System + +This file is part of the `protocol_autogen` layer — a set of code-generated headers that impose a typed, immutable-wrapper/builder pattern on top of raw `SLE` (Serialized Ledger Entry) objects. It targets `ltLEDGER_HASHES` (type code `0x0068`, RPC name `hashes`), the ledger entry type that underlies XRPL's **skip-list mechanism**. + +The skip list is how the XRP Ledger provides O(1) lookup of any historical ledger hash by sequence number. Rather than walking the full chain, two categories of `LedgerHashes` entries are maintained: a rolling window holding the most recent 256 ledger hashes (keyed by `keylet::skip()`), and permanent boundary checkpoints written every 256 ledgers (keyed by `keylet::skip(seq)` when `seq & 0xff == 0`). Both are populated by `Ledger::updateSkipList()` on every ledger close, and consumed by `hashOfSeq()` in `View.cpp` — which reads `sfLastLedgerSequence` to find the right skip-list page and then indexes directly into `sfHashes`. + +## Field Schema + +The entry carries three fields, mirroring the `LEDGER_ENTRY` macro definition in `ledger_entries.macro`: + +- **`sfHashes`** (`soeREQUIRED`) — a `STVector256` (typed as `SF_VECTOR256`) holding up to 256 ancestor ledger hashes in oldest-to-newest order. The rolling-window entry caps at exactly 256 and slides forward; checkpoint entries accumulate up to 256 boundary hashes for their epoch. +- **`sfFirstLedgerSequence`** (`soeOPTIONAL`) — a `uint32` marking the start of the range. +- **`sfLastLedgerSequence`** (`soeOPTIONAL`) — a `uint32` marking the end of the range. `hashOfSeq()` in `View.cpp` asserts that `sfLastLedgerSequence == ledger.seq() - 1` before indexing into the rolling window, making this an invariant the system relies on for correctness. + +## `LedgerHashes` — Immutable Wrapper + +`LedgerHashes` inherits from `LedgerEntryBase`, which holds a `std::shared_ptr`. The const-ness is load-bearing: it makes the wrapper read-only, preventing callers from mutating ledger state through this accessor. + +The constructor validates the SLE type immediately, throwing `std::runtime_error` if the entry is not `ltLEDGER_HASHES`. This is an explicit fail-fast design: because the ledger state tree can hold arbitrary entry types at arbitrary keys, wrapping the wrong type would otherwise silently return garbage from field accessors. + +Optional fields (`sfFirstLedgerSequence`, `sfLastLedgerSequence`) follow the XRPL autogen convention: a paired `hasX()` / `getX()` interface where `getX()` returns `protocol_autogen::Optional` — a `std::nullopt` alias in the autogen utility layer — avoiding callers having to test `isFieldPresent` separately. The required `sfHashes` field has no `has*` guard and returns the `STVector256` value type directly. + +## `LedgerHashesBuilder` — Fluent Constructor + +`LedgerHashesBuilder` inherits from the CRTP base `LedgerEntryBuilderBase`, which initialises an `STObject object_{sfLedgerEntry}` and stamps it with `sfLedgerEntryType` and `sfFlags`. The CRTP parameter makes common base setters (`setLedgerIndex`, `setFlags`) return a `LedgerHashesBuilder&` rather than a `LedgerEntryBuilderBase&`, preserving the fluent chain without casting. + +Two construction modes exist: + +1. **From required fields** — the primary constructor takes `sfHashes` and immediately calls `setHashes()`. Optional fields may be chained via `setFirstLedgerSequence()` / `setLastLedgerSequence()` before calling `build()`. The builder avoids calling `set(soTemplate)` on the internal `STObject`, which would create `soeDEFAULT` placeholder entries that would then cause `applyTemplate()` in the `SLE` constructor to throw "may not be explicitly set to default". This is a non-obvious invariant documented in `LedgerEntryBuilderBase`. + +2. **From an existing SLE** — copies the raw `STObject` out of an existing `SLE const`, after verifying `sfLedgerEntryType`. This path enables read-modify-write workflows: read the SLE from the ledger, wrap it in a builder, call setters for changed fields, then `build()` with the original index to produce a new wrapper ready for `rawReplace()`. + +`build(index)` constructs an `SLE` from the accumulated `STObject` and the provided `uint256` key, wraps it in a `shared_ptr`, and returns a `LedgerHashes` wrapper. The `SLE` constructor runs `applyTemplate()` at this point, filling in default values for any fields not explicitly set. + +## Design Patterns and Tradeoffs + +The separation between a read-only wrapper and a mutable builder enforces a disciplined update path: code that reads ledger entries never accidentally mutates them, and code that builds entries must explicitly commit via `build()`. This mirrors the broader XRPL pattern of treating the ledger state tree as an append/replace log rather than an in-place mutable map. + +The autogenerated nature of the file (declared at line 1) means all entries in the `ledger_entries/` directory share an identical structural pattern. Adding or removing fields only requires updating the macro definition and regenerating, keeping the typed wrappers in sync with the canonical schema without manual maintenance. + +The `[[nodiscard]]` annotations on all getters ensure call sites don't silently discard lookup results — particularly relevant for optional fields where a missing `has_value()` check would otherwise compile silently. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Loan.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Loan.h.ai.json new file mode 100644 index 0000000000..5ecd962e19 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Loan.h.ai.json @@ -0,0 +1,485 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 21, + "name": "Loan" + }, + { + "args": [ + "std::decay_t const& previousTxnID", + "std::decay_t const& previousTxnLgrSeq", + "std::decay_t const& ownerNode", + "std::decay_t const& loanBrokerNode", + "std::decay_t const& loanBrokerID", + "std::decay_t const& loanSequence", + "std::decay_t const& borrower", + "std::decay_t const& startDate", + "std::decay_t const& paymentInterval", + "std::decay_t const& periodicPayment" + ], + "lineno": 478, + "name": "LoanBuilder" + } + ], + "description": "Defines the Loan ledger entry type for the XRPL, providing a type-safe wrapper (Loan) for accessing fields of a Loan ledger entry and a builder class (LoanBuilder) for constructing new Loan ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Loan.h", + "functions": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 27, + "name": "Loan" + }, + { + "args": [], + "lineno": 39, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 48, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 57, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 66, + "name": "getLoanBrokerNode" + }, + { + "args": [], + "lineno": 75, + "name": "getLoanBrokerID" + }, + { + "args": [], + "lineno": 84, + "name": "getLoanSequence" + }, + { + "args": [], + "lineno": 93, + "name": "getBorrower" + }, + { + "args": [], + "lineno": 102, + "name": "getLoanOriginationFee" + }, + { + "args": [], + "lineno": 113, + "name": "hasLoanOriginationFee" + }, + { + "args": [], + "lineno": 122, + "name": "getLoanServiceFee" + }, + { + "args": [], + "lineno": 133, + "name": "hasLoanServiceFee" + }, + { + "args": [], + "lineno": 142, + "name": "getLatePaymentFee" + }, + { + "args": [], + "lineno": 153, + "name": "hasLatePaymentFee" + }, + { + "args": [], + "lineno": 162, + "name": "getClosePaymentFee" + }, + { + "args": [], + "lineno": 173, + "name": "hasClosePaymentFee" + }, + { + "args": [], + "lineno": 182, + "name": "getOverpaymentFee" + }, + { + "args": [], + "lineno": 193, + "name": "hasOverpaymentFee" + }, + { + "args": [], + "lineno": 202, + "name": "getInterestRate" + }, + { + "args": [], + "lineno": 213, + "name": "hasInterestRate" + }, + { + "args": [], + "lineno": 222, + "name": "getLateInterestRate" + }, + { + "args": [], + "lineno": 233, + "name": "hasLateInterestRate" + }, + { + "args": [], + "lineno": 242, + "name": "getCloseInterestRate" + }, + { + "args": [], + "lineno": 253, + "name": "hasCloseInterestRate" + }, + { + "args": [], + "lineno": 262, + "name": "getOverpaymentInterestRate" + }, + { + "args": [], + "lineno": 273, + "name": "hasOverpaymentInterestRate" + }, + { + "args": [], + "lineno": 282, + "name": "getStartDate" + }, + { + "args": [], + "lineno": 291, + "name": "getPaymentInterval" + }, + { + "args": [], + "lineno": 300, + "name": "getGracePeriod" + }, + { + "args": [], + "lineno": 311, + "name": "hasGracePeriod" + }, + { + "args": [], + "lineno": 320, + "name": "getPreviousPaymentDueDate" + }, + { + "args": [], + "lineno": 331, + "name": "hasPreviousPaymentDueDate" + }, + { + "args": [], + "lineno": 340, + "name": "getNextPaymentDueDate" + }, + { + "args": [], + "lineno": 351, + "name": "hasNextPaymentDueDate" + }, + { + "args": [], + "lineno": 360, + "name": "getPaymentRemaining" + }, + { + "args": [], + "lineno": 371, + "name": "hasPaymentRemaining" + }, + { + "args": [], + "lineno": 380, + "name": "getPeriodicPayment" + }, + { + "args": [], + "lineno": 389, + "name": "getPrincipalOutstanding" + }, + { + "args": [], + "lineno": 400, + "name": "hasPrincipalOutstanding" + }, + { + "args": [], + "lineno": 409, + "name": "getTotalValueOutstanding" + }, + { + "args": [], + "lineno": 420, + "name": "hasTotalValueOutstanding" + }, + { + "args": [], + "lineno": 429, + "name": "getManagementFeeOutstanding" + }, + { + "args": [], + "lineno": 440, + "name": "hasManagementFeeOutstanding" + }, + { + "args": [], + "lineno": 449, + "name": "getLoanScale" + }, + { + "args": [], + "lineno": 460, + "name": "hasLoanScale" + }, + { + "args": [ + "std::decay_t const& previousTxnID", + "std::decay_t const& previousTxnLgrSeq", + "std::decay_t const& ownerNode", + "std::decay_t const& loanBrokerNode", + "std::decay_t const& loanBrokerID", + "std::decay_t const& loanSequence", + "std::decay_t const& borrower", + "std::decay_t const& startDate", + "std::decay_t const& paymentInterval", + "std::decay_t const& periodicPayment" + ], + "lineno": 484, + "name": "LoanBuilder" + }, + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 499, + "name": "LoanBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 510, + "name": "setPreviousTxnID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 519, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 528, + "name": "setOwnerNode" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 537, + "name": "setLoanBrokerNode" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 546, + "name": "setLoanBrokerID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 555, + "name": "setLoanSequence" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 564, + "name": "setBorrower" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 573, + "name": "setLoanOriginationFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 582, + "name": "setLoanServiceFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 591, + "name": "setLatePaymentFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 600, + "name": "setClosePaymentFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 609, + "name": "setOverpaymentFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 618, + "name": "setInterestRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 627, + "name": "setLateInterestRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 636, + "name": "setCloseInterestRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 645, + "name": "setOverpaymentInterestRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 654, + "name": "setStartDate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 663, + "name": "setPaymentInterval" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 672, + "name": "setGracePeriod" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 681, + "name": "setPreviousPaymentDueDate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 690, + "name": "setNextPaymentDueDate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 699, + "name": "setPaymentRemaining" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 708, + "name": "setPeriodicPayment" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 717, + "name": "setPrincipalOutstanding" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 726, + "name": "setTotalValueOutstanding" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 735, + "name": "setManagementFeeOutstanding" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 744, + "name": "setLoanScale" + }, + { + "args": [ + "uint256 const& index" + ], + "lineno": 753, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Loan.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Loan.h.ai.md new file mode 100644 index 0000000000..9bc43c6810 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Loan.h.ai.md @@ -0,0 +1,51 @@ +# `include/xrpl/protocol_autogen/ledger_entries/Loan.h` + +## Role in the System + +This file is part of the XRPL `protocol_autogen` layer — a code-generated, type-safe API over the raw serialized ledger objects (`SLE`) that underlie the XRP Ledger's state. It defines two classes for the `ltLOAN` ledger entry type (code `0x0089`): an immutable read wrapper `Loan` and a mutable construction helper `LoanBuilder`. Both live in `xrpl::ledger_entries`. + +The `Loan` entry represents an on-ledger loan record issued through the XRPL Lending Protocol (gated by the `featureLendingProtocol` amendment). It stores the full economic and schedule state of a loan: the borrower's identity, the broker that originated it, payment schedule, fee structure, interest rates, and running outstanding balances. Its sibling entry `LoanBroker` (`ltLOAN_BROKER`, `0x0088`) acts as the broker registry; `Loan` points back to it via `sfLoanBrokerID` and tracks its position in the broker's directory via `sfLoanBrokerNode`. + +## `Loan` — Immutable SLE Wrapper + +`Loan` inherits from `LedgerEntryBase`, which holds a `std::shared_ptr` named `sle_`. Because the underlying SLE is const and reference-counted, a `Loan` object is inherently thread-safe to read and cannot mutate ledger state. The constructor verifies that the wrapped SLE is actually `ltLOAN` before accepting it, throwing `std::runtime_error` on mismatch — a fail-fast guard against type confusion when reading ledger state from the database or incoming transactions. + +All getters are marked `[[nodiscard]]`, which prevents callers from silently ignoring returned values and acts as a compile-time reminder that query results must be consumed. + +### Required vs. Optional Fields + +The design clearly separates two categories of fields: + +**Required fields** (`soeREQUIRED`) return their value directly: +- `getPreviousTxnID()` / `getPreviousTxnLgrSeq()` — standard ledger bookkeeping linking the entry to its last modifying transaction. +- `getOwnerNode()` — position in the borrower's owner directory. +- `getLoanBrokerNode()` / `getLoanBrokerID()` — back-reference to the originating `LoanBroker` entry and its directory slot. +- `getLoanSequence()` — per-broker monotonically increasing counter that disambiguates multiple loans from the same broker without ledger key collisions. +- `getBorrower()` — the `SF_ACCOUNT` address of the borrower. +- `getStartDate()` / `getPaymentInterval()` — temporal anchors for the repayment schedule, stored as XRPL epoch timestamps (`SF_UINT32`). +- `getPeriodicPayment()` — the fixed instalment amount, as an `SF_NUMBER` (XRPL's arbitrary-precision decimal type). + +**Optional/default fields** (`soeDEFAULT`) return `protocol_autogen::Optional` (a `std::optional` alias), always paired with a `has*()` predicate: +- **Fee schedule**: `getLoanOriginationFee()`, `getLoanServiceFee()`, `getLatePaymentFee()`, `getClosePaymentFee()` — all `SF_NUMBER`, capturing financial amounts with decimal precision. `getOverpaymentFee()` is `SF_UINT32`, suggesting a basis-points representation rather than an absolute amount. +- **Interest rates**: `getInterestRate()`, `getLateInterestRate()`, `getCloseInterestRate()`, `getOverpaymentInterestRate()` — all `SF_UINT32`, likely encoded as basis points. These are optional because a broker may offer zero-interest or interest-free loans. +- **Amortization state**: `getPrincipalOutstanding()`, `getTotalValueOutstanding()`, `getManagementFeeOutstanding()` — `SF_NUMBER` running balances updated as payments are applied. +- **Schedule tracking**: `getGracePeriod()`, `getPreviousPaymentDueDate()`, `getNextPaymentDueDate()`, `getPaymentRemaining()` — temporal and counter fields that advance through the repayment life cycle. +- **`getLoanScale()`** — the sole `SF_INT32` field, a signed decimal scaling factor for interpreting the loan's numeric fields. Optional to allow default (unscaled) behavior. + +The four interest-rate variants (normal, late, close, overpayment) and the parallel fee variants reflect distinct contract phases: active repayment, delinquency, early payoff, and overpayment scenarios. Keeping these as separate optional fields rather than a lookup table allows the protocol to omit rates that don't apply to a given loan product without allocating ledger space. + +## `LoanBuilder` — Fluent Construction + +`LoanBuilder` extends `LedgerEntryBuilderBase`, a CRTP base that holds a mutable `STObject object_{sfLedgerEntry}`. The CRTP pattern allows all `set*()` calls in the base to return `Derived&` for method chaining without virtual dispatch overhead. + +The primary constructor enforces the required-field contract by accepting all ten mandatory fields as arguments and calling the corresponding setters immediately. This prevents constructing a `Loan` in a partially-initialized, invalid state — the type system itself makes "missing required field" a compile error rather than a runtime failure. + +An important design choice in `LedgerEntryBuilderBase` (documented in its constructor) is that `object_.set(soTemplate)` is deliberately not called. Applying the SO template early would insert `soeDEFAULT` placeholder objects for all optional fields; these placeholders would later trigger `"may not be explicitly set to default"` errors inside `SLE::applyTemplate()`. By keeping `object_` as a free-form `STObject`, only fields explicitly set by the caller are present when `build()` finally constructs the `SLE`. + +A secondary constructor `LoanBuilder(std::shared_ptr sle)` enables mutation of an existing entry: it copies the SLE's field data into `object_` via `object_ = *sle`, stripping the const qualification and allowing subsequent `set*()` calls before re-building. This round-trip pattern is how ledger-modifying transaction handlers read-modify-write a `Loan` entry. + +`build(uint256 const& index)` consumes the builder by `std::move`-ing `object_` into a new `SLE` at the given ledger key, then wraps it in a `Loan`. After calling `build()`, the builder's internal state is moved-from; callers should treat the builder as expired. + +## Relationship to Surrounding Infrastructure + +`Loan.h` is one of ~30 auto-generated entry files under `protocol_autogen/ledger_entries/`. All share the same structural pattern — each file is machine-generated from a protocol schema definition and should never be edited by hand. The generation approach lets the schema source of truth drive both the serialization layer (`SField`, `STObject`) and the strongly-typed C++ API simultaneously, eliminating hand-written marshalling code. The `LoanBroker.h` counterpart follows an identical structure for `ltLOAN_BROKER` and exposes complementary fields like the broker's cover balances and loan limits that govern which `Loan` entries it may issue. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h.ai.json new file mode 100644 index 0000000000..cbd33e13a5 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h.ai.json @@ -0,0 +1,435 @@ +{ + "args": [ + { + "lineno": 27, + "name": "sle" + }, + { + "lineno": 262, + "name": "previousTxnID" + }, + { + "lineno": 262, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 262, + "name": "sequence" + }, + { + "lineno": 262, + "name": "ownerNode" + }, + { + "lineno": 262, + "name": "vaultNode" + }, + { + "lineno": 262, + "name": "vaultID" + }, + { + "lineno": 262, + "name": "account" + }, + { + "lineno": 262, + "name": "owner" + }, + { + "lineno": 262, + "name": "loanSequence" + }, + { + "lineno": 277, + "name": "sle" + }, + { + "lineno": 287, + "name": "value" + }, + { + "lineno": 295, + "name": "value" + }, + { + "lineno": 303, + "name": "value" + }, + { + "lineno": 311, + "name": "value" + }, + { + "lineno": 319, + "name": "value" + }, + { + "lineno": 327, + "name": "value" + }, + { + "lineno": 335, + "name": "value" + }, + { + "lineno": 343, + "name": "value" + }, + { + "lineno": 351, + "name": "value" + }, + { + "lineno": 359, + "name": "value" + }, + { + "lineno": 367, + "name": "value" + }, + { + "lineno": 375, + "name": "value" + }, + { + "lineno": 383, + "name": "value" + }, + { + "lineno": 391, + "name": "value" + }, + { + "lineno": 399, + "name": "value" + }, + { + "lineno": 407, + "name": "value" + }, + { + "lineno": 415, + "name": "value" + }, + { + "lineno": 423, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 22, + "name": "LoanBroker" + }, + { + "args": [ + "previousTxnID", + "previousTxnLgrSeq", + "sequence", + "ownerNode", + "vaultNode", + "vaultID", + "account", + "owner", + "loanSequence" + ], + "lineno": 256, + "name": "LoanBrokerBuilder" + } + ], + "description": "Defines the LoanBroker ledger entry and its builder for the XRPL protocol, providing type-safe accessors and a fluent builder interface for constructing and manipulating LoanBroker ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h", + "functions": [ + { + "args": [ + "sle" + ], + "lineno": 27, + "name": "LoanBroker" + }, + { + "args": [], + "lineno": 39, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 47, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 55, + "name": "getSequence" + }, + { + "args": [], + "lineno": 63, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 71, + "name": "getVaultNode" + }, + { + "args": [], + "lineno": 79, + "name": "getVaultID" + }, + { + "args": [], + "lineno": 87, + "name": "getAccount" + }, + { + "args": [], + "lineno": 95, + "name": "getOwner" + }, + { + "args": [], + "lineno": 103, + "name": "getLoanSequence" + }, + { + "args": [], + "lineno": 111, + "name": "getData" + }, + { + "args": [], + "lineno": 120, + "name": "hasData" + }, + { + "args": [], + "lineno": 129, + "name": "getManagementFeeRate" + }, + { + "args": [], + "lineno": 138, + "name": "hasManagementFeeRate" + }, + { + "args": [], + "lineno": 147, + "name": "getOwnerCount" + }, + { + "args": [], + "lineno": 156, + "name": "hasOwnerCount" + }, + { + "args": [], + "lineno": 165, + "name": "getDebtTotal" + }, + { + "args": [], + "lineno": 174, + "name": "hasDebtTotal" + }, + { + "args": [], + "lineno": 183, + "name": "getDebtMaximum" + }, + { + "args": [], + "lineno": 192, + "name": "hasDebtMaximum" + }, + { + "args": [], + "lineno": 201, + "name": "getCoverAvailable" + }, + { + "args": [], + "lineno": 210, + "name": "hasCoverAvailable" + }, + { + "args": [], + "lineno": 219, + "name": "getCoverRateMinimum" + }, + { + "args": [], + "lineno": 228, + "name": "hasCoverRateMinimum" + }, + { + "args": [], + "lineno": 237, + "name": "getCoverRateLiquidation" + }, + { + "args": [], + "lineno": 246, + "name": "hasCoverRateLiquidation" + }, + { + "args": [ + "previousTxnID", + "previousTxnLgrSeq", + "sequence", + "ownerNode", + "vaultNode", + "vaultID", + "account", + "owner", + "loanSequence" + ], + "lineno": 262, + "name": "LoanBrokerBuilder" + }, + { + "args": [ + "sle" + ], + "lineno": 277, + "name": "LoanBrokerBuilder" + }, + { + "args": [ + "value" + ], + "lineno": 287, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 295, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 303, + "name": "setSequence" + }, + { + "args": [ + "value" + ], + "lineno": 311, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 319, + "name": "setVaultNode" + }, + { + "args": [ + "value" + ], + "lineno": 327, + "name": "setVaultID" + }, + { + "args": [ + "value" + ], + "lineno": 335, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 343, + "name": "setOwner" + }, + { + "args": [ + "value" + ], + "lineno": 351, + "name": "setLoanSequence" + }, + { + "args": [ + "value" + ], + "lineno": 359, + "name": "setData" + }, + { + "args": [ + "value" + ], + "lineno": 367, + "name": "setManagementFeeRate" + }, + { + "args": [ + "value" + ], + "lineno": 375, + "name": "setOwnerCount" + }, + { + "args": [ + "value" + ], + "lineno": 383, + "name": "setDebtTotal" + }, + { + "args": [ + "value" + ], + "lineno": 391, + "name": "setDebtMaximum" + }, + { + "args": [ + "value" + ], + "lineno": 399, + "name": "setCoverAvailable" + }, + { + "args": [ + "value" + ], + "lineno": 407, + "name": "setCoverRateMinimum" + }, + { + "args": [ + "value" + ], + "lineno": 415, + "name": "setCoverRateLiquidation" + }, + { + "args": [ + "index" + ], + "lineno": 423, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h.ai.md new file mode 100644 index 0000000000..a84816ddba --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/LoanBroker.h.ai.md @@ -0,0 +1,52 @@ +# `LoanBroker.h` — Auto-Generated Ledger Entry Wrapper + +## Role and Context + +This file lives in `include/xrpl/protocol_autogen/ledger_entries/` and is part of a code-generated layer that provides type-safe C++ access to XRPL's serialized ledger state. It defines two classes — `LoanBroker` and `LoanBrokerBuilder` — that together cover the full lifecycle of the `ltLOAN_BROKER` (0x0088) ledger entry type. The comment at the top is unambiguous: **do not edit this file manually**; it is the output of a code generator driven by the protocol's field definitions. + +The `LoanBroker` ledger entry represents an intermediary entity that facilitates lending from a `Vault` to individual borrowers. It is distinct from the `Loan` entry (0x0089), which captures a single active loan. A `LoanBroker` acts as the configuration and accounting record for a brokering relationship between a vault and a set of borrowers, tracking aggregate exposure, coverage constraints, and management fees. + +## Field Structure + +The entry's fields divide cleanly into two groups reflecting their `SOE` requirements: + +**Required fields (`soeREQUIRED`):** `sfPreviousTxnID`, `sfPreviousTxnLgrSeq`, `sfSequence`, `sfOwnerNode`, `sfVaultNode`, `sfVaultID`, `sfAccount`, `sfOwner`, `sfLoanSequence`. Every `LoanBroker` on the ledger must carry all of these — they form the immutable identity and bookkeeping backbone. `sfVaultID` is a 256-bit hash linking to an associated `Vault` entry; `sfVaultNode` is the directory page index within that vault's owner directory, while `sfOwnerNode` tracks the position in the broker owner's own directory. The distinction between `sfAccount` and `sfOwner` allows the servicing account to differ from the entry's owning account. `sfLoanSequence` sequences individual loans issued through this broker. + +**Optional fields (`soeDEFAULT`):** `sfData` (a variable-length blob for broker metadata), `sfManagementFeeRate` (a `uint16` fee expressed as a basis-point-like rate), `sfOwnerCount`, `sfDebtTotal`, `sfDebtMaximum`, `sfCoverAvailable`, `sfCoverRateMinimum`, and `sfCoverRateLiquidation`. The trio of `sfDebtTotal`/`sfDebtMaximum`/`sfCoverAvailable` forms an aggregate risk picture: how much is currently lent, what the ceiling is, and how much collateral cover remains available. The two cover-rate fields (`sfCoverRateMinimum`, `sfCoverRateLiquidation`) express thresholds — the minimum cover ratio to accept new loans and the ratio at which existing positions become eligible for liquidation. + +## The `LoanBroker` Read Wrapper + +`LoanBroker` inherits from `LedgerEntryBase`, which holds a `std::shared_ptr` — the underlying serialized ledger entry. Immutability is enforced at the pointer level (`SLE const`), so there is no path to mutate ledger state through this class. + +The constructor takes a `shared_ptr` and immediately validates the entry type: + +```cpp +if (sle_->getType() != entryType) + throw std::runtime_error("Invalid ledger entry type for LoanBroker"); +``` + +This guard prevents the silent type confusion that would arise from wrapping the wrong SLE type. The check happens at construction rather than lazily, so the wrapper is always in a consistent state once constructed — a fail-fast design that makes misuse loudly visible. + +All getters are `[[nodiscard]]` and `const`. Required fields delegate directly to `sle_->at(sf...)`, which throws if the field is somehow absent (an invariant that should be maintained by the ledger's format validation). Optional fields follow a consistent paired pattern: `getData()` returns `protocol_autogen::Optional` and internally calls `hasData()` before accessing the field. `protocol_autogen::Optional` is a type alias defined in `Utils.h` — it resolves to `std::optional>` when `T` is a reference type, and plain `std::optional` otherwise, correctly handling reference value types that `SLE::at()` can return. + +## The `LoanBrokerBuilder` Fluent Builder + +`LoanBrokerBuilder` uses CRTP, inheriting from `LedgerEntryBuilderBase`. The base class holds a mutable `STObject object_{sfLedgerEntry}` and provides `setFlags()` and `setLedgerIndex()` chainable setters that return `Derived&` — which resolves to `LoanBrokerBuilder&` at the call site. + +The primary constructor enforces completeness at compile time: all nine required fields must be supplied as arguments. They are immediately written into `object_` via the setter methods, ensuring the builder starts in a fully valid state for required fields. The secondary constructor accepts an existing `SLE const` to initialize the builder from live ledger data, enabling a read-modify-rebuild pattern. + +A deliberate design decision in `LedgerEntryBuilderBase` is that the constructor does **not** call `object_.set(soTemplate)`. As the base class comment explains, calling `set(soTemplate)` would create `STBase` placeholder values for `soeDEFAULT` fields; when `SLE`'s constructor later calls `applyTemplate()`, those placeholders trigger a "may not be explicitly set to default" error. By leaving the `STObject` as a free (template-less) object and letting `SLE` apply the template during construction, the builder avoids this trap. This is a non-obvious interaction with the underlying serialization machinery. + +`build(uint256 const& index)` finalizes construction by moving the accumulated `STObject` into a new `SLE`, then wrapping the result in a `LoanBroker`: + +```cpp +LoanBroker build(uint256 const& index) { + return LoanBroker{std::make_shared(std::move(object_), index)}; +} +``` + +The `std::move` of `object_` invalidates the builder after `build()` is called — callers should treat the builder as consumed once `build()` returns. + +## Relationship to Sibling Entries + +`LoanBroker` sits between `Vault` (0x0084) and `Loan` (0x0089) in the lending subsystem. A `Vault` aggregates liquidity; a `LoanBroker` configures the terms and risk limits under which that liquidity is deployed; individual `Loan` entries record each borrower's obligation. The `sfVaultID` / `sfVaultNode` pair on `LoanBroker` creates the hard link back to the vault, while `sfLoanSequence` provides a monotonic counter used to derive deterministic IDs for child `Loan` entries. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h.ai.json new file mode 100644 index 0000000000..a01c39639c --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h.ai.json @@ -0,0 +1,196 @@ +{ + "args": [ + { + "lineno": 32, + "name": "sle" + }, + { + "lineno": 134, + "name": "account" + }, + { + "lineno": 134, + "name": "mPTokenIssuanceID" + }, + { + "lineno": 134, + "name": "ownerNode" + }, + { + "lineno": 134, + "name": "previousTxnID" + }, + { + "lineno": 134, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 144, + "name": "sle" + }, + { + "lineno": 153, + "name": "value" + }, + { + "lineno": 163, + "name": "value" + }, + { + "lineno": 173, + "name": "value" + }, + { + "lineno": 183, + "name": "value" + }, + { + "lineno": 193, + "name": "value" + }, + { + "lineno": 203, + "name": "value" + }, + { + "lineno": 213, + "name": "value" + }, + { + "lineno": 223, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 27, + "name": "MPToken" + }, + { + "args": [ + "account", + "mPTokenIssuanceID", + "ownerNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 128, + "name": "MPTokenBuilder" + } + ], + "description": "Defines the MPToken ledger entry type for the XRPL, providing a type-safe wrapper for accessing fields and a builder class for constructing new entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/MPToken.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getAccount" + }, + { + "args": [], + "lineno": 48, + "name": "getMPTokenIssuanceID" + }, + { + "args": [], + "lineno": 58, + "name": "getMPTAmount" + }, + { + "args": [], + "lineno": 68, + "name": "hasMPTAmount" + }, + { + "args": [], + "lineno": 78, + "name": "getLockedAmount" + }, + { + "args": [], + "lineno": 88, + "name": "hasLockedAmount" + }, + { + "args": [], + "lineno": 98, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 108, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 118, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 151, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 161, + "name": "setMPTokenIssuanceID" + }, + { + "args": [ + "value" + ], + "lineno": 171, + "name": "setMPTAmount" + }, + { + "args": [ + "value" + ], + "lineno": 181, + "name": "setLockedAmount" + }, + { + "args": [ + "value" + ], + "lineno": 191, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 201, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 211, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 221, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h.ai.md new file mode 100644 index 0000000000..872624661e --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h.ai.md @@ -0,0 +1,39 @@ +# `MPToken.h` — Auto-Generated MPT Holder Ledger Entry + +This file is part of the `protocol_autogen` layer in the XRP Ledger codebase and defines the `MPToken` ledger entry type (`ltMPTOKEN`, type code `0x007f`). It lives in `include/xrpl/protocol_autogen/ledger_entries/` alongside roughly thirty other auto-generated ledger entry headers, all following the same structural contract. The file is machine-generated from a schema and must not be edited by hand. + +## The MPT Data Model + +The Multi-Purpose Token (MPT) system splits responsibility across two ledger object types that are adjacent in the type code space: `MPTokenIssuance` (`0x007e`) defines the token class itself — issuer, metadata, maximum supply, transfer fees — while `MPToken` (`0x007f`) records a single account's balance within that class. Every account that holds any nonzero amount of a given MPT issuance has exactly one `MPToken` entry on the ledger. `sfMPTokenIssuanceID`, a 192-bit field, binds the holder record back to its issuance: the identifier is derived from the issuer's 160-bit account ID concatenated with the 32-bit sequence number of the `MPTokenIssuance` creation transaction, making it a compact, globally unique key without the full 256-bit overhead of a general ledger index. + +## `MPToken`: Immutable Read Wrapper + +`MPToken` extends `LedgerEntryBase`, which itself wraps a `std::shared_ptr`. The `const`-qualified smart pointer is the architectural foundation of this whole subsystem: once an `MPToken` object is constructed, it cannot mutate the underlying serialized ledger entry. All getter methods are `[[nodiscard]] const`. This is a deliberate tradeoff — the ledger's canonical view of on-disk state should never be accidentally modified through a wrapper class; mutations are exclusively the domain of the builder half of the pair. + +Type safety is enforced eagerly at construction time. The single-argument constructor calls `sle_->getType()` and throws `std::runtime_error` if the underlying SLE is not `ltMPTOKEN`. This prevents the silent misuse that would occur if, say, an `MPTokenIssuance` SLE were passed to `MPToken`. + +The field schema exposes three distinct optionality tiers that directly correspond to the XRPL serialized-object template opcodes: + +- **`soeREQUIRED`** fields (`sfAccount`, `sfMPTokenIssuanceID`, `sfOwnerNode`, `sfPreviousTxnID`, `sfPreviousTxnLgrSeq`) return their value types directly with no optional wrapping, because their presence is guaranteed by the ledger format rules. + +- **`soeDEFAULT`** fields (`sfMPTAmount`) are returned as `protocol_autogen::Optional` with a paired `hasMPTAmount()` predicate. A DEFAULT field is serialized only when it differs from its default value; `sfMPTAmount` defaults to zero. In practice, a holder entry with a zero balance would either not exist or would have `sfMPTAmount` absent from the serialized form. Callers that need the numeric value regardless of presence should fall back to zero when `getMPTAmount()` returns `std::nullopt`. + +- **`soeOPTIONAL`** fields (`sfLockedAmount`) are also returned as `protocol_autogen::Optional`. Unlike DEFAULT, an OPTIONAL field carries semantic weight when absent — it signals that no tokens are currently locked under any escrow or clawback mechanism, not merely that the value is zero. A caller must not equate absence of `sfLockedAmount` with a zero lock; it means the lock state is undefined for this entry. + +`protocol_autogen::Optional` is a thin type alias (defined in `Utils.h`) that resolves to `std::optional>` when `T` is a reference type, and to plain `std::optional` otherwise. This matters because several SF field `value_type`s are reference types, and wrapping a reference directly in `std::optional` is ill-formed in C++. + +`sfOwnerNode` is a back-index into the account's owner directory `DirectoryNode` chain. It provides O(1) access to the relevant page during ledger deletion, avoiding a linear scan through potentially thousands of directory entries. `sfPreviousTxnID` and `sfPreviousTxnLgrSeq` are standard bookkeeping fields present on nearly every ledger object, recording the last transaction that touched this entry and the ledger sequence at which that happened. They are used by clients for history reconstruction and by the ledger for metadata generation. + +## `MPTokenBuilder`: Fluent Construction + +`MPTokenBuilder` extends the CRTP base `LedgerEntryBuilderBase`. The CRTP pattern lets the base class return `Derived&` from shared setters like `setFlags()` and `setLedgerIndex()`, preserving the concrete derived type through a method-chaining call chain without virtual dispatch overhead. + +The primary constructor accepts all five required fields and sets them immediately, ensuring the internal `STObject` is always in a consistent state before any optional fields are added. The alternative constructor copies an existing `SLE` directly into `object_`, supporting the pattern of loading an existing ledger entry and rebuilding it with modifications — importantly, it still validates the entry type before copying. + +A subtle but critical detail lives in `LedgerEntryBuilderBase`: the constructor explicitly avoids calling `object_.set(soTemplate)`. Applying the SO template upfront would create `STBase` placeholder objects for every `soeDEFAULT` field in the template, and when the subsequent `SLE` constructor calls `applyTemplate()` internally, it would throw an exception complaining that a DEFAULT field "may not be explicitly set to default." By keeping `object_` as a free-form `STObject` and only writing fields that have actual values, the builder avoids this trap entirely. + +The `build(uint256 const& index)` method moves the accumulated `STObject` into a freshly allocated `SLE` keyed by the provided 256-bit ledger index, then wraps the result in an `MPToken` reader. After `build()` returns, the builder's internal state has been moved out and should not be reused. + +## Relationship to Adjacent Code + +Within the autogen layer, `MPToken.h` is structurally identical to every other ledger entry header in its directory — the same inheritance chain, the same optionality pattern, the same CRTP builder idiom. The only entry-specific content is the field list and the `ltMPTOKEN` type constant. This uniformity is intentional and is what makes the code generation approach tractable: the generator emits one header per ledger entry type, each self-contained and carrying no cross-entry dependencies beyond the two shared base headers. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h.ai.json new file mode 100644 index 0000000000..9a2de5b134 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h.ai.json @@ -0,0 +1,322 @@ +{ + "args": [ + { + "lineno": 31, + "name": "sle" + }, + { + "lineno": 259, + "name": "issuer" + }, + { + "lineno": 259, + "name": "sequence" + }, + { + "lineno": 259, + "name": "ownerNode" + }, + { + "lineno": 259, + "name": "outstandingAmount" + }, + { + "lineno": 259, + "name": "previousTxnID" + }, + { + "lineno": 259, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 271, + "name": "sle" + }, + { + "lineno": 283, + "name": "value" + }, + { + "lineno": 293, + "name": "value" + }, + { + "lineno": 303, + "name": "value" + }, + { + "lineno": 313, + "name": "value" + }, + { + "lineno": 323, + "name": "value" + }, + { + "lineno": 333, + "name": "value" + }, + { + "lineno": 343, + "name": "value" + }, + { + "lineno": 353, + "name": "value" + }, + { + "lineno": 363, + "name": "value" + }, + { + "lineno": 373, + "name": "value" + }, + { + "lineno": 383, + "name": "value" + }, + { + "lineno": 393, + "name": "value" + }, + { + "lineno": 403, + "name": "value" + }, + { + "lineno": 414, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 27, + "name": "MPTokenIssuance" + }, + { + "args": [ + "issuer", + "sequence", + "ownerNode", + "outstandingAmount", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 256, + "name": "MPTokenIssuanceBuilder" + } + ], + "description": "Defines the MPTokenIssuance ledger entry type for XRPL, providing a type-safe wrapper for accessing fields and a builder class for constructing new entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getIssuer" + }, + { + "args": [], + "lineno": 48, + "name": "getSequence" + }, + { + "args": [], + "lineno": 58, + "name": "getTransferFee" + }, + { + "args": [], + "lineno": 70, + "name": "hasTransferFee" + }, + { + "args": [], + "lineno": 80, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 90, + "name": "getAssetScale" + }, + { + "args": [], + "lineno": 102, + "name": "hasAssetScale" + }, + { + "args": [], + "lineno": 112, + "name": "getMaximumAmount" + }, + { + "args": [], + "lineno": 124, + "name": "hasMaximumAmount" + }, + { + "args": [], + "lineno": 134, + "name": "getOutstandingAmount" + }, + { + "args": [], + "lineno": 144, + "name": "getLockedAmount" + }, + { + "args": [], + "lineno": 156, + "name": "hasLockedAmount" + }, + { + "args": [], + "lineno": 166, + "name": "getMPTokenMetadata" + }, + { + "args": [], + "lineno": 178, + "name": "hasMPTokenMetadata" + }, + { + "args": [], + "lineno": 188, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 198, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 208, + "name": "getDomainID" + }, + { + "args": [], + "lineno": 220, + "name": "hasDomainID" + }, + { + "args": [], + "lineno": 230, + "name": "getMutableFlags" + }, + { + "args": [], + "lineno": 242, + "name": "hasMutableFlags" + }, + { + "args": [ + "value" + ], + "lineno": 282, + "name": "setIssuer" + }, + { + "args": [ + "value" + ], + "lineno": 292, + "name": "setSequence" + }, + { + "args": [ + "value" + ], + "lineno": 302, + "name": "setTransferFee" + }, + { + "args": [ + "value" + ], + "lineno": 312, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 322, + "name": "setAssetScale" + }, + { + "args": [ + "value" + ], + "lineno": 332, + "name": "setMaximumAmount" + }, + { + "args": [ + "value" + ], + "lineno": 342, + "name": "setOutstandingAmount" + }, + { + "args": [ + "value" + ], + "lineno": 352, + "name": "setLockedAmount" + }, + { + "args": [ + "value" + ], + "lineno": 362, + "name": "setMPTokenMetadata" + }, + { + "args": [ + "value" + ], + "lineno": 372, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 382, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 392, + "name": "setDomainID" + }, + { + "args": [ + "value" + ], + "lineno": 402, + "name": "setMutableFlags" + }, + { + "args": [ + "index" + ], + "lineno": 412, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h.ai.md new file mode 100644 index 0000000000..01acaf5f0a --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h.ai.md @@ -0,0 +1,53 @@ +# `MPTokenIssuance.h` — Auto-generated Ledger Entry Wrapper for MPT Issuances + +This file lives in the `xrpl::ledger_entries` namespace and is part of a machine-generated type-safe layer over the XRPL ledger's raw serialized-object infrastructure. It defines two classes — `MPTokenIssuance` and `MPTokenIssuanceBuilder` — that represent the `ltMPTOKEN_ISSUANCE` ledger entry type (wire code `0x007e`), which was introduced as part of XRPL's Multi-Purpose Token (MPT) feature. + +## Role in the MPT System + +XRPL's MPT feature introduces a more compact and efficient fungible-token model relative to the older trust-line / IOU approach. The on-ledger data for this model is split between two complementary entry types: `MPTokenIssuance` (this file, `0x007e`) describes the global parameters of a token class — its issuer, supply cap, outstanding supply, transfer fee, and metadata — while `MPToken` (`0x007f`, in the sibling `MPToken.h`) records the balance held by a single account. The relationship is one-to-many: one `MPTokenIssuance` anchors every individual `MPToken` holder entry, which references it via `sfMPTokenIssuanceID`. + +## `MPTokenIssuance`: Immutable Read Wrapper + +`MPTokenIssuance` inherits from `LedgerEntryBase`, which holds a `std::shared_ptr` — the `const` qualifier is load-bearing. This class is deliberately read-only: no setter exists, and the underlying `SLE` cannot be mutated through this interface. Every getter is marked `[[nodiscard]]`. + +The constructor accepts a `std::shared_ptr` and immediately checks that `sle_->getType() == ltMPTOKEN_ISSUANCE`, throwing `std::runtime_error` on mismatch. This guard converts a latent protocol-level invariant (only the right SLE type should ever be wrapped) into a hard C++ exception at the boundary, so callers don't silently operate on the wrong entry type. + +### Field Categories and Access Pattern + +Fields fall into three XRPL schema categories, each with a distinct access pattern: + +**`soeREQUIRED`** fields (`sfIssuer`, `sfSequence`, `sfOwnerNode`, `sfOutstandingAmount`, `sfPreviousTxnID`, `sfPreviousTxnLgrSeq`) are always present on any well-formed entry. Their getters return the value type directly with no `Optional` wrapper. + +**`soeDEFAULT`** fields (`sfTransferFee`, `sfAssetScale`, `sfMutableFlags`) have a protocol-defined default value (typically zero) when absent from the serialized bytes. The code nonetheless exposes them via `protocol_autogen::Optional` and a paired `has*()` predicate, treating presence as meaningful. This is intentional: a missing `sfTransferFee` field means "no fee configured" rather than "zero fee", a distinction that can matter for validation logic. + +**`soeOPTIONAL`** fields (`sfMaximumAmount`, `sfLockedAmount`, `sfMPTokenMetadata`, `sfDomainID`) may genuinely be absent. All four follow the same pattern: the getter checks `isFieldPresent()` and returns `std::nullopt` if the field is missing. Callers must explicitly handle the optional. + +The `protocol_autogen::Optional` alias (defined in `Utils.h`) is a thin conditional: if `T` is a reference type it wraps `std::reference_wrapper` inside `std::optional`, otherwise it is plain `std::optional`. This avoids the undefined behavior of `std::optional` holding a reference. + +### Notable Fields + +`sfOutstandingAmount` tracks the aggregate of all tokens currently in circulation across every holder's `MPToken` entry. It is always present and required, not optional, because any valid issuance must track supply. + +`sfLockedAmount` at the issuance level is an optional aggregate cache of locked balances held by individual `MPToken` holders (e.g., for escrow or DEX offers). Caching this at the issuance layer avoids needing to scan all holder entries to determine how much supply is encumbered. + +`sfAssetScale` (a `uint8`) records the number of decimal places between the token's base unit and the minimum representable quantity. A value of `6` means the internal `uint64` counter represents millionths of the display unit. This is absent when the token uses its raw integer representation directly. + +`sfMutableFlags` is structurally distinct from the standard `sfFlags` field inherited from `LedgerEntryBase`. The ledger separates flags that are fixed at issuance time (stored in `sfFlags`) from flags the issuer may change post-creation (stored in `sfMutableFlags`). Both are `uint32`, but only `sfMutableFlags` may be updated by subsequent transactions. + +`sfDomainID` optionally ties the issuance to a `PermissionedDomain` ledger entry, restricting which accounts may hold this token. Its type is `uint256`, identifying the domain entry by its ledger key. + +## `MPTokenIssuanceBuilder`: Fluent Construction + +`MPTokenIssuanceBuilder` inherits from the CRTP base `LedgerEntryBuilderBase`. The CRTP parameterization lets the base-class setters (`setFlags()`, `setLedgerIndex()`) return `Derived&` rather than `LedgerEntryBuilderBase&`, preserving the concrete type across the method chain so callers never need to cast. + +The primary constructor enforces the full required-field contract at construction time by accepting all six required fields as parameters and calling their setters immediately. Optional and default fields are set only if explicitly called afterward. This design prevents the builder from ever producing an SLE that is missing a required field, moving the validation error from `build()` time to construction time. + +The `STObject`-based internal representation (`object_` of type `STObject sfLedgerEntry`) is kept deliberately "free" — the base class comment notes that calling `object_.set(soTemplate)` is avoided. Setting a template would pre-populate `soeDEFAULT` fields as STBase placeholder objects, which would then cause the `SLE` constructor's `applyTemplate()` call to reject them with "may not be explicitly set to default". By starting field-free and only writing fields that are actually set, the builder remains compatible with `SLE`'s template validation. + +The secondary constructor accepts `std::shared_ptr` and copies the dereferenced `SLE` into `object_` via `object_ = *sle`. This enables a read-modify pattern: wrap an existing ledger entry in a builder, adjust fields, and produce a new entry via `build()`. + +`build(uint256 const& index)` consumes the builder's internal `STObject` via `std::move` and hands it — along with the entry's ledger key — to the `SLE` constructor, then wraps the result in an `MPTokenIssuance`. After `build()`, the builder's `object_` is in a moved-from state and should not be reused. + +## Relationship to the Autogeneration System + +The header comment "This file is auto-generated. Do not edit." signals that the source of truth for `MPTokenIssuance`'s field set is a schema elsewhere in the build system. Every ledger entry in `ledger_entries/` follows the same `Wrapper + Builder` pattern, generated from the same machinery. The uniform structure — immutable wrapper with `get*()` / `has*()` pairs, fluent builder with `set*()` methods, CRTP base — means any new field in the schema automatically produces consistent accessors without hand-written boilerplate or risk of omission. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h.ai.json new file mode 100644 index 0000000000..7bfc5f6043 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h.ai.json @@ -0,0 +1,238 @@ +{ + "args": [ + { + "lineno": 25, + "name": "sle" + }, + { + "lineno": 157, + "name": "owner" + }, + { + "lineno": 157, + "name": "nFTokenID" + }, + { + "lineno": 157, + "name": "amount" + }, + { + "lineno": 157, + "name": "ownerNode" + }, + { + "lineno": 157, + "name": "nFTokenOfferNode" + }, + { + "lineno": 157, + "name": "previousTxnID" + }, + { + "lineno": 157, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 170, + "name": "sle" + }, + { + "lineno": 182, + "name": "value" + }, + { + "lineno": 192, + "name": "value" + }, + { + "lineno": 202, + "name": "value" + }, + { + "lineno": 212, + "name": "value" + }, + { + "lineno": 222, + "name": "value" + }, + { + "lineno": 232, + "name": "value" + }, + { + "lineno": 242, + "name": "value" + }, + { + "lineno": 252, + "name": "value" + }, + { + "lineno": 262, + "name": "value" + }, + { + "lineno": 272, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 18, + "name": "NFTokenOffer" + }, + { + "args": [ + "owner", + "nFTokenID", + "amount", + "ownerNode", + "nFTokenOfferNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 153, + "name": "NFTokenOfferBuilder" + } + ], + "description": "Defines the NFTokenOffer ledger entry and its builder for the XRPL, providing type-safe accessors and a fluent builder interface for constructing and manipulating NFTokenOffer ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getOwner" + }, + { + "args": [], + "lineno": 48, + "name": "getNFTokenID" + }, + { + "args": [], + "lineno": 58, + "name": "getAmount" + }, + { + "args": [], + "lineno": 68, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 78, + "name": "getNFTokenOfferNode" + }, + { + "args": [], + "lineno": 88, + "name": "getDestination" + }, + { + "args": [], + "lineno": 99, + "name": "hasDestination" + }, + { + "args": [], + "lineno": 109, + "name": "getExpiration" + }, + { + "args": [], + "lineno": 120, + "name": "hasExpiration" + }, + { + "args": [], + "lineno": 130, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 140, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 180, + "name": "setOwner" + }, + { + "args": [ + "value" + ], + "lineno": 190, + "name": "setNFTokenID" + }, + { + "args": [ + "value" + ], + "lineno": 200, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 210, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 220, + "name": "setNFTokenOfferNode" + }, + { + "args": [ + "value" + ], + "lineno": 230, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 240, + "name": "setExpiration" + }, + { + "args": [ + "value" + ], + "lineno": 250, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 260, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 270, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h.ai.md new file mode 100644 index 0000000000..e10472414f --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/NFTokenOffer.h.ai.md @@ -0,0 +1,45 @@ +# `NFTokenOffer.h` — Auto-Generated NFT Offer Ledger Entry Wrapper + +## Role in the System + +This file defines the type-safe C++ interface for the `NFTokenOffer` ledger entry (`ltNFTOKEN_OFFER`, type code `0x0037`), one of the two NFT-related ledger entry types in the XRP Ledger (the other being `NFTokenPage`). An `NFTokenOffer` object is created on-ledger whenever an account submits an `NFTokenCreateOffer` transaction — either to sell an NFT they own or to bid on one owned by another account. The entry persists until the offer is accepted, cancelled, or expires. + +The file is auto-generated (the header comment makes this explicit) and lives in the `protocol_autogen/ledger_entries/` directory alongside similarly generated wrappers for every other ledger entry type (`Offer`, `Escrow`, `Check`, `PayChannel`, etc.). The code generation pipeline ensures that schema changes propagate consistently to all consumers without manual maintenance. + +## Two-Class Design: Immutable Wrapper + Fluent Builder + +The file exports two cooperating classes within the `xrpl::ledger_entries` namespace: + +**`NFTokenOffer`** is an immutable read wrapper around a `std::shared_ptr` — the serialized ledger entry. It inherits from `LedgerEntryBase`, which holds the `sle_` pointer and provides common accessors (`getKey()`, `getType()`, `getFlags()`, `validate()`). `NFTokenOffer` adds NFT-specific getters. The choice of `shared_ptr` is deliberate: the ledger state is immutable after being committed, and shared ownership allows many readers to reference the same on-ledger object without copying. + +**`NFTokenOfferBuilder`** is a CRTP-based fluent builder inheriting from `LedgerEntryBuilderBase`. It accumulates field assignments into an `STObject object_` and then materializes a fully constructed `NFTokenOffer` via `build(uint256 const& index)`. The builder also accepts an existing `SLE const` for copy-based modification. The CRTP base returns `Derived&` from its shared setters (`setFlags()`, `setLedgerIndex()`), enabling unbroken method chains across both base and derived setters. + +A key implementation note in `LedgerEntryBuilderBase`: the constructor intentionally does **not** call `object_.set(soTemplate)`. The comment explains why — calling it would create `STBase` placeholder values for `soeDEFAULT` fields, causing the `SLE` constructor's internal `applyTemplate()` to throw `"may not be explicitly set to default"`. By leaving the `STObject` as a free (untemplatized) object, missing optional fields are handled cleanly during SLE construction. + +## Field Structure + +Required fields — all enforced at construction time in the builder — are: + +- `sfOwner` (`SF_ACCOUNT`): the account that created the offer and is responsible for the reserve. +- `sfNFTokenID` (`SF_UINT256`): the 256-bit identifier of the specific NFT being offered. +- `sfAmount` (`SF_AMOUNT`): the price, expressed as XRP drops or an IOU amount. +- `sfOwnerNode` (`SF_UINT64`): a back-pointer into the owner's account directory page, enabling efficient deletion of the entry when the offer is cancelled or accepted without a full directory scan. +- `sfNFTokenOfferNode` (`SF_UINT64`): a back-pointer into the NFToken's dedicated buy/sell offer directory — a linked list structure tracking all active offers for a given NFT. +- `sfPreviousTxnID` / `sfPreviousTxnLgrSeq`: standard provenance fields recording the last transaction that touched this entry, required for all ledger objects. + +Optional fields are: + +- `sfDestination` (`SF_ACCOUNT`): when present, restricts acceptance of the offer to a single named account. This supports private/targeted sales. The getter returns `protocol_autogen::Optional`, paired with a `hasDestination()` predicate. +- `sfExpiration` (`SF_UINT32`): a Ripple epoch timestamp after which the offer is no longer valid. Like `sfDestination`, the getter returns `protocol_autogen::Optional<...>` and is accompanied by `hasExpiration()`. + +The `protocol_autogen::Optional` type alias (defined in `Utils.h`) is a small but important detail: it resolves to `std::optional>` for reference types and `std::optional` for value types, preventing accidental dangling references when returning values from optional `SLE` fields. + +## Type Safety and Failure Modes + +The `NFTokenOffer` constructor validates the wrapped SLE's type immediately, reading `sle_->getType()` against the `constexpr entryType = ltNFTOKEN_OFFER`. A mismatch throws `std::runtime_error`. Similarly, `NFTokenOfferBuilder`'s SLE-copy constructor checks `sle->at(sfLedgerEntryType) != ltNFTOKEN_OFFER` before accepting the entry. This eager validation means incorrect entry types are caught at construction rather than silently returning garbage field values — an important invariant for code that processes heterogeneous ledger state. + +All getters are marked `[[nodiscard]]` and `const`, reinforcing the immutability contract: nothing in `NFTokenOffer` can mutate the underlying `SLE`. Callers needing to produce a modified entry must go through `NFTokenOfferBuilder`, which constructs a fresh `SLE` on `build()`. + +## Relationship to Sibling Files + +`NFTokenOffer.h` follows the same structural template as every other entry in `protocol_autogen/ledger_entries/` — the pattern is uniform enough that the entire directory is machine-generated from a ledger schema. `NFTokenPage.h`, the companion file, holds the actual NFT tokens and their metadata; `NFTokenOffer.h` holds the market-side activity. The two work together in the NFT subsystem: `NFTokenPage` records ownership, while `NFTokenOffer` records intent to trade. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h.ai.json new file mode 100644 index 0000000000..0d8d051d0f --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h.ai.json @@ -0,0 +1,154 @@ +{ + "args": [ + { + "lineno": 31, + "name": "sle" + }, + { + "lineno": 110, + "name": "nFTokens" + }, + { + "lineno": 110, + "name": "previousTxnID" + }, + { + "lineno": 110, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 119, + "name": "sle" + }, + { + "lineno": 124, + "name": "value" + }, + { + "lineno": 133, + "name": "value" + }, + { + "lineno": 142, + "name": "value" + }, + { + "lineno": 151, + "name": "value" + }, + { + "lineno": 160, + "name": "value" + }, + { + "lineno": 169, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 27, + "name": "NFTokenPage" + }, + { + "args": [ + "nFTokens", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 106, + "name": "NFTokenPageBuilder" + } + ], + "description": "Defines the NFTokenPage ledger entry and its builder for the XRPL protocol, providing type-safe accessors and a fluent builder interface for constructing and manipulating NFTokenPage ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getPreviousPageMin" + }, + { + "args": [], + "lineno": 48, + "name": "hasPreviousPageMin" + }, + { + "args": [], + "lineno": 58, + "name": "getNextPageMin" + }, + { + "args": [], + "lineno": 68, + "name": "hasNextPageMin" + }, + { + "args": [], + "lineno": 78, + "name": "getNFTokens" + }, + { + "args": [], + "lineno": 87, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 96, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 122, + "name": "setPreviousPageMin" + }, + { + "args": [ + "value" + ], + "lineno": 131, + "name": "setNextPageMin" + }, + { + "args": [ + "value" + ], + "lineno": 140, + "name": "setNFTokens" + }, + { + "args": [ + "value" + ], + "lineno": 149, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 158, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 167, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h.ai.md new file mode 100644 index 0000000000..8620aaa514 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h.ai.md @@ -0,0 +1,52 @@ +# `NFTokenPage.h` — Auto-Generated Ledger Entry Wrapper + +**File:** `include/xrpl/protocol_autogen/ledger_entries/NFTokenPage.h` +**Namespace:** `xrpl::ledger_entries` +**Generated type:** `ltNFTOKEN_PAGE` (0x0050), RPC name `nft_page` + +## Role in the System + +This file is part of the `protocol_autogen` layer — a collection of auto-generated C++ wrappers that provide type-safe, structured access to raw XRPL ledger entries. It encodes the `NFTokenPage` ledger object, which is the on-ledger storage unit for Non-Fungible Tokens (NFTs) owned by a single account. Because the broader XRPL ledger stores data as serialized binary objects (`SLE`), these generated wrappers exist to avoid scattered, unchecked field lookups spread across application code. + +An account may own any number of NFTs, but the ledger packs them into pages of up to 32 tokens each. Multiple pages for the same account are linked together via their ledger keys, forming a doubly-linked list — which is exactly what `sfPreviousPageMin` and `sfNextPageMin` encode. + +## NFTokenPage Page Linking Design + +The `sfPreviousPageMin` and `sfNextPageMin` fields are both optional `uint256` values, and their semantics are subtler than their names suggest. Each NFTokenPage's ledger key is derived from the owner's account ID and the minimum NFToken ID that can legally reside in that page. The "min" suffix in the link fields refers to this minimum token ID boundary of the adjacent page — not a simple ordinal sequence number. + +When these fields are absent, the page is either the first or last in the chain for that account. The `NFTokenHelpers.cpp` implementation reads and writes these fields extensively during token minting, burning, and merging operations to maintain the doubly-linked structure. The optional nature of both fields cleanly handles the boundary cases: the head page has no `sfPreviousPageMin`, and the tail page has no `sfNextPageMin`. + +## Class: `NFTokenPage` + +`NFTokenPage` is an immutable, read-only wrapper around a `std::shared_ptr`. It inherits common field accessors (`getKey()`, `getType()`, `getFlags()`, `validate()`) from `LedgerEntryBase` and adds the NFTokenPage-specific getters. + +The constructor takes a `shared_ptr` and immediately verifies the entry type matches `ltNFTOKEN_PAGE`, throwing `std::runtime_error` if not. This eager validation is the key safety guarantee: once a `NFTokenPage` object exists, all subsequent field accesses are guaranteed to operate on a correctly-typed SLE, eliminating a whole class of field-mismatch bugs that could occur with raw SLE access. + +**Field accessors:** + +- `getNFTokens()` — returns `STArray const&` holding the packed NFT objects. This is a required field; there is always at least one token present in a live page. +- `getPreviousTxnID()` / `getPreviousTxnLgrSeq()` — standard audit fields required on all mutable ledger entries, recording the last transaction that modified this page. +- `getPreviousPageMin()` / `getNextPageMin()` — return `protocol_autogen::Optional` (i.e., `std::optional`). The `has*()` companions test presence before the `isFieldPresent()` call, avoiding the undefined behavior that raw SLE access would cause when reading an absent field. + +## Class: `NFTokenPageBuilder` + +`NFTokenPageBuilder` uses CRTP by inheriting from `LedgerEntryBuilderBase`, giving it the common `setLedgerIndex()` and `setFlags()` setters via the base template while enabling method chaining that returns the concrete derived type. The internal state is a free `STObject` (not bound to an SOTemplate) — a deliberate choice documented in `LedgerEntryBuilderBase`: binding to the SOTemplate too early would create `soeDEFAULT` placeholders that cause `applyTemplate()` to throw when the SLE is finally constructed. + +The builder has two construction paths: + +1. **From required fields** — the primary constructor takes `sfNFTokens` (the token array), `sfPreviousTxnID`, and `sfPreviousTxnLgrSeq`. Optional link fields are set later via `setPreviousPageMin()` / `setNextPageMin()`. + +2. **From an existing SLE** — allows constructing a builder by copying the state out of a live ledger entry, which `NFTokenHelpers.cpp` uses when merging or splitting pages during token operations. This path also verifies the entry type and throws on mismatch. + +The `build(uint256 const& index)` method consumes the builder's internal `STObject` (via `std::move`) to construct a new `SLE`, then wraps it in a `NFTokenPage`. The caller provides the ledger key explicitly because NFTokenPage keys are computed by `keylet::nftpage()` from the owner AccountID and the minimum token ID boundary — the builder has no way to derive that key internally. + +## Relationship to Other Files + +- **`LedgerEntryBase.h`** — provides the `sle_` member and common read accessors. `NFTokenPage` holds no additional state beyond what the base class provides. +- **`LedgerEntryBuilderBase.h`** — provides the `object_` member (`STObject`), common setters, and the CRTP scaffolding. All builder-specific NFToken field setters delegate directly to `object_`'s field API. +- **`NFTokenHelpers.cpp`** — the primary consumer of NFTokenPage's structure. It directly manipulates `sfPreviousPageMin` / `sfNextPageMin` on raw SLEs to maintain the doubly-linked page chain. The generated wrappers here exist to provide a cleaner API layer for any code that needs read-only structured access rather than raw mutation. +- **`Indexes.cpp`** — defines `keylet::nftpage_min` and `keylet::nftpage_max` which compute the range of valid keys for a given account's NFT pages; these are the keys passed to `build()`. + +## Error Handling and Invariants + +Both classes enforce the entry-type invariant at construction time via `std::runtime_error`, ensuring type safety is checked once at the boundary rather than scattered through field-access code. The test suite (`NFTokenPageTests.cpp`) verifies this explicitly: constructing either `NFTokenPage` or `NFTokenPageBuilder` from a `Ticket` SLE throws as expected. The `validate()` method (inherited by both the wrapper and exposed on the builder) delegates to `protocol_autogen::validateSTObject()`, which checks the field set against the official `SOTemplate` for `ltNFTOKEN_PAGE`, providing a second layer of structural correctness checking before any constructed entry reaches the ledger. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h.ai.json new file mode 100644 index 0000000000..509c49e21a --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h.ai.json @@ -0,0 +1,139 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 24, + "name": "NegativeUNL" + }, + { + "args": [], + "lineno": 143, + "name": "NegativeUNLBuilder" + } + ], + "description": "Defines the NegativeUNL ledger entry wrapper and builder for XRPL, providing type-safe accessors and a fluent builder interface for constructing NegativeUNL ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h", + "functions": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 27, + "name": "NegativeUNL" + }, + { + "args": [], + "lineno": 44, + "name": "getDisabledValidators" + }, + { + "args": [], + "lineno": 54, + "name": "hasDisabledValidators" + }, + { + "args": [], + "lineno": 63, + "name": "getValidatorToDisable" + }, + { + "args": [], + "lineno": 73, + "name": "hasValidatorToDisable" + }, + { + "args": [], + "lineno": 82, + "name": "getValidatorToReEnable" + }, + { + "args": [], + "lineno": 92, + "name": "hasValidatorToReEnable" + }, + { + "args": [], + "lineno": 101, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 111, + "name": "hasPreviousTxnID" + }, + { + "args": [], + "lineno": 120, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 130, + "name": "hasPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 146, + "name": "NegativeUNLBuilder" + }, + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 151, + "name": "NegativeUNLBuilder" + }, + { + "args": [ + "STArray const& value" + ], + "lineno": 163, + "name": "setDisabledValidators" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 171, + "name": "setValidatorToDisable" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 179, + "name": "setValidatorToReEnable" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 187, + "name": "setPreviousTxnID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 195, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "uint256 const& index" + ], + "lineno": 203, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h.ai.md new file mode 100644 index 0000000000..c7244af3de --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/NegativeUNL.h.ai.md @@ -0,0 +1,37 @@ +# `NegativeUNL.h` — Type-Safe Wrapper for the Negative UNL Ledger Entry + +## Role in the System + +The Negative UNL (Unique Node List) is a XRPL consensus mechanism that allows the network to temporarily mark validators as offline or unreliable, reducing the effective quorum threshold and preventing consensus stalls during validator outages. The `ltNEGATIVE_UNL` ledger entry (type code `0x004e`, RPC name `nunl`) is the singleton on-ledger object that records which validators are currently disabled, and which validator is currently being proposed for disable or re-enable during an in-progress vote. + +This file is **auto-generated** (the header comment states this explicitly) and lives in `include/xrpl/protocol_autogen/ledger_entries/`, alongside analogous wrappers for every other ledger entry type. Its purpose is to provide a type-safe C++ interface over the raw `SLE` (Serialized Ledger Entry) storage layer, replacing scattered, error-prone `getField*()` calls with checked accessors and a fluent builder. + +## Fields + +Every field in this ledger entry is `soeOPTIONAL`, matching the macro definition in `ledger_entries.macro`: + +- **`sfDisabledValidators`** — An `STArray` of objects, each recording a validator that is currently disabled. Accessed via `getDisabledValidators()` which returns `std::optional>`. The `reference_wrapper` return (rather than a value copy) is intentional: `STArray` objects can be large, and the wrapper is immutable, so returning a const reference avoids a deep copy while still safely expressing optionality. +- **`sfValidatorToDisable`** — A variable-length blob (`SF_VL`) holding the public key of a candidate for disabling in the current flag ledger voting round. +- **`sfValidatorToReEnable`** — A variable-length blob (`SF_VL`) holding the public key of a candidate for re-enablement. +- **`sfPreviousTxnID`** — A `uint256` hash linking this entry to the last transaction that modified it. This and `sfPreviousTxnLgrSeq` were added retroactively via the `fixPreviousTxnID` amendment, which is why they remain optional rather than required — the `STLedgerEntry` threading machinery in `isThreadedType()` guards their use behind amendment activation. +- **`sfPreviousTxnLgrSeq`** — A `uint32` identifying the ledger sequence number of that modifying transaction. + +## Class Design + +The file defines two classes in `namespace xrpl::ledger_entries`. + +`NegativeUNL` extends `LedgerEntryBase` and acts as an immutable read-only view. It stores a `std::shared_ptr` (ownership shared with the caller) and exposes typed `get*()` / `has*()` pairs for every field. The constructor enforces correct type at runtime: if the passed `sle->getType()` does not equal `ltNEGATIVE_UNL`, it throws `std::runtime_error`. This guards against accidentally wrapping a wrong SLE type — something impossible to detect at compile time since all SLE objects share the same concrete type. + +`NegativeUNLBuilder` extends the CRTP base `LedgerEntryBuilderBase`, which provides common field setters (`setLedgerIndex`, `setFlags`) and an internal `STObject object_{sfLedgerEntry}`. The builder deliberately does **not** call `object_.set(soTemplate)` on construction. This is a non-obvious but important detail documented in the base class: calling `set(soTemplate)` would pre-populate placeholder `STBase` entries for `soeDEFAULT` fields, which then causes `applyTemplate()` inside the `SLE` constructor to throw a "may not be explicitly set to default" error. By keeping `object_` as a free, template-less object, the builder lets the `SLE` constructor handle default-field initialization cleanly. + +The `build(uint256 const& index)` method finalises construction by move-constructing an `SLE` from the accumulated `STObject` and the provided ledger key, then wrapping it in a `NegativeUNL`. The builder also accepts an existing `std::shared_ptr` to copy-initialise from, enabling mutation workflows where a read-modify-write round-trip is needed. + +## Relationship to the Negative UNL Subsystem + +The `NegativeUNLVote` class in `src/xrpld/app/misc/NegativeUNLVote.cpp` is the primary consumer of this ledger entry's semantics. It reads `sfDisabledValidators` to determine the currently offline set and writes `sfValidatorToDisable` / `sfValidatorToReEnable` when the voting algorithm selects a candidate. The vote happens over flag ledger intervals; only one validator can be added to or removed from the Negative UNL per flag period, which is why the "candidate" fields are singletons rather than arrays. + +Worth noting: `NegativeUNL` as a feature (`XRPL_RETIRE_FEATURE(NegativeUNL)` in `features.macro`) is retired — it is permanently active. The ledger entry type and its associated vote machinery are therefore always enabled. + +## Type Safety and Error Handling + +The paired `has*()` / `get*()` pattern on the wrapper class is the idiomatic approach across all autogenerated entry types: `has*()` is a cheap presence check, while `get*()` returns `std::nullopt` for absent optional fields rather than throwing or returning a default value. All getter methods are marked `[[nodiscard]]` to catch silent discards of optional values. The builder-side type check in the SLE-copying constructor mirrors the wrapper's constructor check, ensuring that both entry points into this type enforce the `ltNEGATIVE_UNL` invariant immediately, rather than allowing a silently wrong object to propagate until a field access fails. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Offer.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Offer.h.ai.json new file mode 100644 index 0000000000..683f605649 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Offer.h.ai.json @@ -0,0 +1,204 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 33, + "name": "Offer" + }, + { + "args": [ + "account", + "sequence", + "takerPays", + "takerGets", + "bookDirectory", + "bookNode", + "ownerNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 186, + "name": "OfferBuilder" + } + ], + "description": "Defines the Offer ledger entry and its builder for XRPL, providing type-safe field access and construction for Offer objects in the ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Offer.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getAccount" + }, + { + "args": [], + "lineno": 53, + "name": "getSequence" + }, + { + "args": [], + "lineno": 62, + "name": "getTakerPays" + }, + { + "args": [], + "lineno": 71, + "name": "getTakerGets" + }, + { + "args": [], + "lineno": 80, + "name": "getBookDirectory" + }, + { + "args": [], + "lineno": 89, + "name": "getBookNode" + }, + { + "args": [], + "lineno": 98, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 107, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 116, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 125, + "name": "getExpiration" + }, + { + "args": [], + "lineno": 136, + "name": "hasExpiration" + }, + { + "args": [], + "lineno": 145, + "name": "getDomainID" + }, + { + "args": [], + "lineno": 156, + "name": "hasDomainID" + }, + { + "args": [], + "lineno": 165, + "name": "getAdditionalBooks" + }, + { + "args": [], + "lineno": 175, + "name": "hasAdditionalBooks" + }, + { + "args": [ + "value" + ], + "lineno": 210, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 219, + "name": "setSequence" + }, + { + "args": [ + "value" + ], + "lineno": 228, + "name": "setTakerPays" + }, + { + "args": [ + "value" + ], + "lineno": 237, + "name": "setTakerGets" + }, + { + "args": [ + "value" + ], + "lineno": 246, + "name": "setBookDirectory" + }, + { + "args": [ + "value" + ], + "lineno": 255, + "name": "setBookNode" + }, + { + "args": [ + "value" + ], + "lineno": 264, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 273, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 282, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 291, + "name": "setExpiration" + }, + { + "args": [ + "value" + ], + "lineno": 300, + "name": "setDomainID" + }, + { + "args": [ + "value" + ], + "lineno": 309, + "name": "setAdditionalBooks" + }, + { + "args": [ + "index" + ], + "lineno": 318, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Offer.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Offer.h.ai.md new file mode 100644 index 0000000000..30c221a404 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Offer.h.ai.md @@ -0,0 +1,49 @@ +# `include/xrpl/protocol_autogen/ledger_entries/Offer.h` + +## Purpose and Context + +This file is part of a code-generation pipeline that produces type-safe wrappers for every ledger entry type in the XRP Ledger protocol. The `Offer` ledger entry (type code `ltOFFER`, `0x006f`) represents a single live order in the XRPL's on-ledger decentralized exchange (DEX). Whenever an account places an `OfferCreate` transaction that isn't immediately filled, a persistent `Offer` ledger object is created and indexed in the order books — this file defines the strongly-typed C++ interface used to read and construct those objects throughout the rippled codebase. + +The file lives in `include/xrpl/protocol_autogen/`, a directory of ~30 similarly structured headers, one per ledger entry type. The `// This file is auto-generated. Do not edit.` header comment is authoritative — the entire directory is produced by a code generator and should not be modified directly. + +## Class Architecture: Immutable Wrapper + Builder + +The file defines two cooperating classes following a strict read/write separation. + +**`Offer`** extends `LedgerEntryBase` and holds a `std::shared_ptr` — the const qualifier is crucial. The underlying `SLE` (Serialized Ledger Entry) is the live ledger object; wrapping it as `const` means this class can never mutate ledger state. All getters are `[[nodiscard]]` `const` methods, making the immutability contract explicit at the type level. The constructor validates that the incoming `SLE` really is an offer via `sle_->getType() != entryType`, throwing `std::runtime_error` on a mismatch rather than silently wrapping the wrong object type. + +**`OfferBuilder`** uses CRTP, inheriting from `LedgerEntryBuilderBase`. The CRTP ensures common setters defined on the base (`setFlags`, `setLedgerIndex`) return `OfferBuilder&` rather than the base-class type, preserving the fluent method-chaining interface without virtual dispatch. The internal state is a plain `STObject object_`, initialized with `ltOFFER` and default flags. The base class deliberately avoids calling `object_.set(soTemplate)` at construction to prevent `applyTemplate()` from creating `soeDEFAULT` placeholder fields that would later cause the SLE constructor to throw "may not be explicitly set to default". + +The `build()` method terminates the chain: it moves the accumulated `STObject` into a freshly constructed `SLE` bound to the caller-supplied 256-bit ledger index, then wraps that in an `Offer` wrapper — closing the loop between builder and reader. + +## Required Fields and Their XRPL Semantics + +The nine required fields capture the full state of a DEX order: + +- **`sfAccount`** — the `AccountID` of the offer owner. +- **`sfSequence`** — the sequence number of the `OfferCreate` transaction that created this object. Together with `sfAccount` it uniquely identifies the offer and is used to compute the ledger key. +- **`sfTakerPays` / `sfTakerGets`** — both are `SF_AMOUNT`, meaning they can represent either XRP (in drops) or IOU token amounts from any issuer. `TakerPays` is what a party consuming the offer must deliver; `TakerGets` is what they receive. The ratio defines the exchange rate. +- **`sfBookDirectory`** — a `uint256` hash pointing to the `DirectoryNode` ledger entry that indexes this offer within its order book. Order books in XRPL are implemented as sorted linked lists of `DirectoryNode` pages; this field links the individual offer into that structure. +- **`sfBookNode`** — a `uint64` node index within the `DirectoryNode` page chain, used for O(1) deletion without a full scan. +- **`sfOwnerNode`** — a `uint64` index into the offer owner's account directory (another `DirectoryNode`). When an offer is consumed or cancelled, the ledger uses this to efficiently remove the entry from the owner's object list. +- **`sfPreviousTxnID` / `sfPreviousTxnLgrSeq`** — standard provenance fields present on all mutable ledger objects, recording the last transaction that touched this entry. + +## Optional Fields and the `Optional` Type Alias + +Three optional fields are surfaced with paired `has*()`/`get*()` methods: + +- **`sfExpiration`** (`SF_UINT32`) — a Ripple Epoch timestamp after which the offer is treated as expired. If absent the offer has no time limit. +- **`sfDomainID`** (`SF_UINT256`) — associates the offer with a permissioned domain, part of newer DEX access-control features. +- **`sfAdditionalBooks`** (`STArray`) — an array field enabling multi-leg or alternative order book associations, another recent protocol extension. + +Optional scalar fields return `protocol_autogen::Optional`, a type alias defined in `Utils.h` using `std::conditional_t`. If `T` is a reference type the alias resolves to `std::optional>`, allowing reference semantics inside an optional; if `T` is a value type it resolves to `std::optional`. This is necessary because some `SField` accessors return const references into the SLE's internal storage. `sfAdditionalBooks` bypasses this alias entirely and returns `std::optional>` directly, since `STArray` is always accessed by reference. + +## Round-Trip Mutation via the SLE Copy Constructor + +`OfferBuilder` offers a second constructor that takes `std::shared_ptr` and initializes `object_` by dereferencing the SLE: `object_ = *sle`. This enables a round-trip pattern: read an existing offer from the ledger as an immutable `Offer`, copy it into an `OfferBuilder`, modify optional fields (e.g., update remaining `TakerPays`/`TakerGets` after a partial fill), then call `build()` to produce a new `SLE` that can be written back. The type guard (`sle->at(sfLedgerEntryType) != ltOFFER`) applies here too, maintaining the invariant that `OfferBuilder` only ever holds offer-shaped data. + +## Relationship to Sibling Files + +Every file in the `ledger_entries/` directory follows this identical dual-class pattern. `DirectoryNode.h`, `AccountRoot.h`, `RippleState.h`, and the rest are structurally identical — the fields differ but the wrapper/builder split, CRTP inheritance, and `Optional` usage are uniform across all of them. This consistency is a direct consequence of the code-generation approach: correctness is enforced by generating from a single template rather than by convention across hand-written files. + +One minor discrepancy in the generated documentation: the `OfferBuilder` class comment states it "Uses Json::Value internally for flexible ledger entry construction," but the implementation uses `STObject` directly. This appears to be a stale comment artifact in the autogen template. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Oracle.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Oracle.h.ai.json new file mode 100644 index 0000000000..c70a57923b --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Oracle.h.ai.json @@ -0,0 +1,259 @@ +{ + "args": [ + { + "lineno": 38, + "name": "sle" + }, + { + "lineno": 174, + "name": "owner" + }, + { + "lineno": 174, + "name": "provider" + }, + { + "lineno": 174, + "name": "priceDataSeries" + }, + { + "lineno": 174, + "name": "assetClass" + }, + { + "lineno": 174, + "name": "lastUpdateTime" + }, + { + "lineno": 174, + "name": "ownerNode" + }, + { + "lineno": 174, + "name": "previousTxnID" + }, + { + "lineno": 174, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 185, + "name": "sle" + }, + { + "lineno": 192, + "name": "value" + }, + { + "lineno": 202, + "name": "value" + }, + { + "lineno": 212, + "name": "value" + }, + { + "lineno": 222, + "name": "value" + }, + { + "lineno": 232, + "name": "value" + }, + { + "lineno": 242, + "name": "value" + }, + { + "lineno": 252, + "name": "value" + }, + { + "lineno": 262, + "name": "value" + }, + { + "lineno": 272, + "name": "value" + }, + { + "lineno": 282, + "name": "value" + }, + { + "lineno": 292, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 33, + "name": "Oracle" + }, + { + "args": [ + "owner", + "provider", + "priceDataSeries", + "assetClass", + "lastUpdateTime", + "ownerNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 170, + "name": "OracleBuilder" + } + ], + "description": "Defines the Oracle ledger entry and its builder for XRPL, providing type-safe accessors and a fluent builder interface for constructing Oracle ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Oracle.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getOwner" + }, + { + "args": [], + "lineno": 54, + "name": "getOracleDocumentID" + }, + { + "args": [], + "lineno": 65, + "name": "hasOracleDocumentID" + }, + { + "args": [], + "lineno": 75, + "name": "getProvider" + }, + { + "args": [], + "lineno": 85, + "name": "getPriceDataSeries" + }, + { + "args": [], + "lineno": 95, + "name": "getAssetClass" + }, + { + "args": [], + "lineno": 105, + "name": "getLastUpdateTime" + }, + { + "args": [], + "lineno": 115, + "name": "getURI" + }, + { + "args": [], + "lineno": 126, + "name": "hasURI" + }, + { + "args": [], + "lineno": 136, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 146, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 156, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 191, + "name": "setOwner" + }, + { + "args": [ + "value" + ], + "lineno": 201, + "name": "setOracleDocumentID" + }, + { + "args": [ + "value" + ], + "lineno": 211, + "name": "setProvider" + }, + { + "args": [ + "value" + ], + "lineno": 221, + "name": "setPriceDataSeries" + }, + { + "args": [ + "value" + ], + "lineno": 231, + "name": "setAssetClass" + }, + { + "args": [ + "value" + ], + "lineno": 241, + "name": "setLastUpdateTime" + }, + { + "args": [ + "value" + ], + "lineno": 251, + "name": "setURI" + }, + { + "args": [ + "value" + ], + "lineno": 261, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 271, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 281, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 291, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Oracle.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Oracle.h.ai.md new file mode 100644 index 0000000000..05d2473587 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Oracle.h.ai.md @@ -0,0 +1,44 @@ +# `Oracle.h` — Auto-Generated Oracle Ledger Entry Wrapper + +## Role and Context + +This file defines the C++ interface for the `ltORACLE` ledger entry type (type code `0x0080`), introduced by the XRP Ledger Price Oracle feature. It lives in `include/xrpl/protocol_autogen/ledger_entries/` alongside ~30 other auto-generated files, one per ledger entry type. The header comment makes this explicit: **do not edit** — the file is produced by a code generator that reads XRPL field and format definitions and emits type-safe wrappers so consumers never have to touch raw `SLE` field accessors directly. + +The Oracle ledger object represents an on-chain price feed published by a trusted data provider. A single XRPL account can own multiple Oracle entries, each identified by an optional `sfOracleDocumentID`. The entry records a series of asset prices (`sfPriceDataSeries`), the provider's identity, the asset category being priced, and a Unix timestamp of the last update. + +## Two-Class Structure: Reader and Builder + +The file exports two classes into the `xrpl::ledger_entries` namespace: `Oracle` (the immutable reader) and `OracleBuilder` (the fluent construction interface). This split is a deliberate separation of concerns mirrored across every ledger entry in the autogen layer. + +### `Oracle` — Immutable Wrapper + +`Oracle` inherits from `LedgerEntryBase`, which holds a `std::shared_ptr` — a read-only reference to a Serialized Ledger Entry. The `const`-ness is enforced at the pointer's value type, not just the pointer itself, so the SLE cannot be mutated through this interface at all. + +Construction takes a `shared_ptr` and immediately validates that the underlying SLE's type matches `ltORACLE`, throwing `std::runtime_error` on mismatch. This is an early-detection guard: code that accidentally wraps the wrong ledger object type fails loudly at construction rather than silently returning garbage from field getters. + +All getters are marked `[[nodiscard]]` and `const`. The distinction between required and optional fields is enforced by the return type: + +- **Required fields** (`sfOwner`, `sfProvider`, `sfPriceDataSeries`, `sfAssetClass`, `sfLastUpdateTime`, `sfOwnerNode`, `sfPreviousTxnID`, `sfPreviousTxnLgrSeq`) return their value type directly via `sle_->at(field)`. +- **Optional fields** (`sfOracleDocumentID`, `sfURI`) return `protocol_autogen::Optional`, which resolves to `std::optional` for value types or `std::optional>` for reference types. Each optional getter is paired with a `has*()` predicate that calls `sle_->isFieldPresent()`. + +`sfPriceDataSeries` is notable: it returns `STArray const&` via `getFieldArray()` rather than using the typed `at()` accessor, because STArray fields don't fit into the generic field template. This is called out in the comment as an "untyped field (unknown)" — the generator recognizes it cannot produce a strongly-typed accessor and falls back to the raw array access. + +`sfProvider` and `sfAssetClass` are both `SF_VL` (variable-length blob) fields. In practice, `sfProvider` encodes the oracle provider's identifier (e.g., a string name or URL), and `sfAssetClass` names the category of assets being priced (e.g., `"currency"`). `sfURI` is an optional blob pointing to supplementary documentation. + +### `OracleBuilder` — Fluent Construction + +`OracleBuilder` inherits from `LedgerEntryBuilderBase`, a CRTP template that provides common setters (`setFlags()`, `setLedgerIndex()`) returning `Derived&` for method chaining. The base class stores an internal `STObject object_{sfLedgerEntry}` — a free-form serialized object not bound to any template yet. + +A critical design decision in the base constructor: it explicitly avoids calling `object_.set(soTemplate)`. If a template were applied early, the STObject would pre-populate `soeDEFAULT` fields as placeholders, and the subsequent `SLE` constructor's `applyTemplate()` call would throw "may not be explicitly set to default" for those fields. By keeping the internal object free until `build()`, the builder avoids this trap while still ensuring the final `SLE` is properly template-validated at construction time. + +The primary `OracleBuilder` constructor takes all eight required fields as parameters and calls each corresponding `set*()` method immediately. There is also a second constructor that accepts an existing `std::shared_ptr` and copies it into `object_` via `*sle` dereferencing — this supports the edit-and-rebuild pattern where a caller reads an existing Oracle from the ledger, modifies it through the builder interface, and produces a new SLE. + +`build(uint256 const& index)` finalizes construction by wrapping `std::move(object_)` and the provided ledger index into a new `SLE`, then constructing and returning an `Oracle` wrapper around it. The `std::move` here is significant: the builder is left in a moved-from state after `build()`, reinforcing single-use semantics. + +## Relationship to Other Files + +`LedgerEntryBase` provides `validate()` — calling `protocol_autogen::validateSTObject()` against the ledger format's `SOTemplate` — as well as the escape hatch `getSle()` returning the raw `shared_ptr` for contexts where type-safe accessors aren't sufficient. + +`LedgerEntryBuilderBase` supplies `setFlags()` and `setLedgerIndex()` as the only mutable common-field operations; everything else is Oracle-specific. The `std::decay_t` pattern used in setter signatures strips references and cv-qualifiers from the canonical value types, ensuring setters accept both lvalues and rvalues without overload proliferation. + +The `protocol_autogen::Optional` alias in `Utils.h` is a narrow but important utility: it handles the case where a field's `value_type` is itself a reference (which `std::optional` cannot directly hold), transparently wrapping it in `std::reference_wrapper`. This keeps optional getter signatures consistent across all field types without requiring per-type specializations. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h.ai.json new file mode 100644 index 0000000000..4d8e624a6d --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h.ai.json @@ -0,0 +1,364 @@ +{ + "args": [ + { + "lineno": 38, + "name": "sle" + }, + { + "lineno": 255, + "name": "account" + }, + { + "lineno": 255, + "name": "destination" + }, + { + "lineno": 255, + "name": "amount" + }, + { + "lineno": 255, + "name": "balance" + }, + { + "lineno": 255, + "name": "publicKey" + }, + { + "lineno": 255, + "name": "settleDelay" + }, + { + "lineno": 255, + "name": "ownerNode" + }, + { + "lineno": 255, + "name": "previousTxnID" + }, + { + "lineno": 255, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 266, + "name": "sle" + }, + { + "lineno": 273, + "name": "value" + }, + { + "lineno": 283, + "name": "value" + }, + { + "lineno": 293, + "name": "value" + }, + { + "lineno": 303, + "name": "value" + }, + { + "lineno": 313, + "name": "value" + }, + { + "lineno": 323, + "name": "value" + }, + { + "lineno": 333, + "name": "value" + }, + { + "lineno": 343, + "name": "value" + }, + { + "lineno": 353, + "name": "value" + }, + { + "lineno": 363, + "name": "value" + }, + { + "lineno": 373, + "name": "value" + }, + { + "lineno": 383, + "name": "value" + }, + { + "lineno": 393, + "name": "value" + }, + { + "lineno": 403, + "name": "value" + }, + { + "lineno": 413, + "name": "value" + }, + { + "lineno": 423, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 33, + "name": "PayChannel" + }, + { + "args": [ + "account", + "destination", + "amount", + "balance", + "publicKey", + "settleDelay", + "ownerNode", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 253, + "name": "PayChannelBuilder" + } + ], + "description": "Defines the PayChannel ledger entry wrapper and builder for XRPL, providing type-safe field access and construction for payment channels.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getAccount" + }, + { + "args": [], + "lineno": 53, + "name": "getDestination" + }, + { + "args": [], + "lineno": 62, + "name": "getSequence" + }, + { + "args": [], + "lineno": 73, + "name": "hasSequence" + }, + { + "args": [], + "lineno": 82, + "name": "getAmount" + }, + { + "args": [], + "lineno": 91, + "name": "getBalance" + }, + { + "args": [], + "lineno": 100, + "name": "getPublicKey" + }, + { + "args": [], + "lineno": 109, + "name": "getSettleDelay" + }, + { + "args": [], + "lineno": 118, + "name": "getExpiration" + }, + { + "args": [], + "lineno": 129, + "name": "hasExpiration" + }, + { + "args": [], + "lineno": 138, + "name": "getCancelAfter" + }, + { + "args": [], + "lineno": 149, + "name": "hasCancelAfter" + }, + { + "args": [], + "lineno": 158, + "name": "getSourceTag" + }, + { + "args": [], + "lineno": 169, + "name": "hasSourceTag" + }, + { + "args": [], + "lineno": 178, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 189, + "name": "hasDestinationTag" + }, + { + "args": [], + "lineno": 198, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 207, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 216, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 225, + "name": "getDestinationNode" + }, + { + "args": [], + "lineno": 236, + "name": "hasDestinationNode" + }, + { + "args": [ + "value" + ], + "lineno": 271, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 281, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 291, + "name": "setSequence" + }, + { + "args": [ + "value" + ], + "lineno": 301, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 311, + "name": "setBalance" + }, + { + "args": [ + "value" + ], + "lineno": 321, + "name": "setPublicKey" + }, + { + "args": [ + "value" + ], + "lineno": 331, + "name": "setSettleDelay" + }, + { + "args": [ + "value" + ], + "lineno": 341, + "name": "setExpiration" + }, + { + "args": [ + "value" + ], + "lineno": 351, + "name": "setCancelAfter" + }, + { + "args": [ + "value" + ], + "lineno": 361, + "name": "setSourceTag" + }, + { + "args": [ + "value" + ], + "lineno": 371, + "name": "setDestinationTag" + }, + { + "args": [ + "value" + ], + "lineno": 381, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 391, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 401, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 411, + "name": "setDestinationNode" + }, + { + "args": [ + "index" + ], + "lineno": 421, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h.ai.md new file mode 100644 index 0000000000..7eb8d8ca68 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/PayChannel.h.ai.md @@ -0,0 +1,35 @@ +# `PayChannel.h` — Auto-generated PayChannel Ledger Entry Wrapper + +`PayChannel.h` is a machine-generated header (do not edit manually) that provides the two-class pattern used throughout the `protocol_autogen` layer of the XRPL codebase: an immutable read-only accessor (`PayChannel`) and a fluent construction helper (`PayChannelBuilder`). Together they wrap the raw `SLE` (Serialized Ledger Entry) representation of a payment channel with type-safe, name-safe field access, living under `ltPAYCHAN` (discriminator `0x0078`). + +## What a PayChannel Ledger Entry Represents + +In the XRP Ledger, a payment channel allows a source account to pre-allocate XRP for a series of off-chain micropayments to a destination. The source signs individual claim messages off-chain; the destination can cash the highest-value one at any time, or the channel can be closed after a settlement delay elapses. The on-ledger entry holds the allocated `sfAmount`, the already-redeemed `sfBalance`, the cryptographic `sfPublicKey` used to verify off-chain claim signatures, and the `sfSettleDelay` (in seconds) that governs how long the source must wait before unilaterally closing the channel. + +## Class `PayChannel` + +`PayChannel` extends `LedgerEntryBase`, which holds a `std::shared_ptr` as `sle_`. The `const`-ness is structural: the shared pointer's pointee is immutable, so no field setter exists on the wrapper — all mutations go through `PayChannelBuilder`. + +The constructor performs a single defensive check against `sle_->getType() != ltPAYCHAN` and throws `std::runtime_error` on mismatch. This is the only runtime guard; all other correctness guarantees come from the type system. + +Required fields (`soeREQUIRED`) are exposed via plain getters that forward directly to `sle_->at(sfXxx)`: `getAccount()`, `getDestination()`, `getAmount()`, `getBalance()`, `getPublicKey()`, `getSettleDelay()`, `getOwnerNode()`, `getPreviousTxnID()`, and `getPreviousTxnLgrSeq()`. These will assert or throw internally if the field is somehow absent, consistent with the ledger's own invariant that required fields must always be present on a well-formed entry. + +Optional fields (`soeOPTIONAL`) follow a paired `has*()`/`get*()` pattern and return `protocol_autogen::Optional`. That alias, defined in `Utils.h`, is a conditional type: for reference-typed `T` it becomes `std::optional>>`, and for value types it collapses to plain `std::optional`. This ensures reference-typed SField results (which normally can't be stored in `std::optional` directly) are handled correctly. The optional fields are: `sfSequence` (channel-creating transaction's sequence number, used for deduplication), `sfExpiration` (mutable soft expiry adjustable by either party), `sfCancelAfter` (immutable hard deadline set once at creation), `sfSourceTag`, `sfDestinationTag`, and `sfDestinationNode` (the destination's owner-directory back-pointer, absent on channels created before that field was added). + +The distinction between `sfExpiration` and `sfCancelAfter` is semantically significant. `sfCancelAfter` is the absolute hard limit set at channel creation and is never updated. `sfExpiration` is a softer limit that either party can advance forward (never back) as a way to coordinate channel closure. Both are represented as 32-bit XRP Ledger time values (seconds since the Ripple epoch). + +`sfOwnerNode` and `sfDestinationNode` are 64-bit indices into the owner-directory B-tree pages for the source and destination accounts respectively. They are present to allow O(1) deletion from the directory when the channel is closed. `sfDestinationNode` is optional because it was introduced after the payment channel feature and may be absent on older entries. + +## Class `PayChannelBuilder` + +`PayChannelBuilder` is a CRTP builder inheriting from `LedgerEntryBuilderBase`. The base maintains an `STObject object_{sfLedgerEntry}` that accumulates field assignments, and it deliberately avoids calling `object_.set(soTemplate)` — keeping the object "free" — so that default-value fields are not pre-populated, which would conflict with `SLE`'s own `applyTemplate()` call in its constructor. + +The primary constructor accepts all nine required fields as arguments, setting them immediately via the corresponding `set*()` calls. This design enforces, at compile time, that required fields cannot be omitted: there is no default constructor, and the optional fields are only reachable via chained `set*()` calls after construction. Each setter returns `PayChannelBuilder&`, enabling fluent chaining. + +A secondary constructor takes an `std::shared_ptr` and copies the SLE's field state into `object_` via `object_ = *sle`. This supports the pattern of "load an existing channel entry, modify it, and rebuild" — relevant when processing `PaymentChannelClaim` or `PaymentChannelFund` transactions that update `sfBalance` or `sfExpiration`. + +`build(uint256 const& index)` moves `object_` into a freshly constructed `SLE` keyed at `index`, then wraps it in a `PayChannel`. The move means the builder is consumed after a single `build()` call; reusing it after that would access a moved-from `STObject`. + +## Relationship to Surrounding Infrastructure + +`PayChannel.h` is one of roughly 30 auto-generated files in `ledger_entries/`, each following an identical structural pattern. The code generator distinguishes required vs optional fields and emits the `Optional` wrapper only where needed. The common behaviors — `validate()`, `getKey()`, `getType()`, `getFlags()`, `getLedgerIndex()` — are handled entirely in `LedgerEntryBase` and `LedgerEntryBuilderBase`, keeping the per-entry files focused solely on the fields that differentiate each ledger object type. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h.ai.json new file mode 100644 index 0000000000..cf3b4f25fb --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h.ai.json @@ -0,0 +1,171 @@ +{ + "args": [ + { + "lineno": 29, + "name": "sle" + }, + { + "lineno": 104, + "name": "owner" + }, + { + "lineno": 104, + "name": "sequence" + }, + { + "lineno": 104, + "name": "acceptedCredentials" + }, + { + "lineno": 104, + "name": "ownerNode" + }, + { + "lineno": 104, + "name": "previousTxnID" + }, + { + "lineno": 104, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 113, + "name": "sle" + }, + { + "lineno": 118, + "name": "value" + }, + { + "lineno": 128, + "name": "value" + }, + { + "lineno": 138, + "name": "value" + }, + { + "lineno": 148, + "name": "value" + }, + { + "lineno": 158, + "name": "value" + }, + { + "lineno": 168, + "name": "value" + }, + { + "lineno": 178, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 27, + "name": "PermissionedDomain" + }, + { + "args": [ + "std::decay_t const& owner, std::decay_t const& sequence, STArray const& acceptedCredentials, std::decay_t const& ownerNode, std::decay_t const& previousTxnID, std::decay_t const& previousTxnLgrSeq", + "std::shared_ptr sle" + ], + "lineno": 99, + "name": "PermissionedDomainBuilder" + } + ], + "description": "Defines the PermissionedDomain ledger entry and its builder for the XRPL, providing type-safe accessors and a fluent builder interface for constructing and manipulating PermissionedDomain ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getOwner" + }, + { + "args": [], + "lineno": 48, + "name": "getSequence" + }, + { + "args": [], + "lineno": 58, + "name": "getAcceptedCredentials" + }, + { + "args": [], + "lineno": 68, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 78, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 88, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 116, + "name": "setOwner" + }, + { + "args": [ + "value" + ], + "lineno": 126, + "name": "setSequence" + }, + { + "args": [ + "value" + ], + "lineno": 136, + "name": "setAcceptedCredentials" + }, + { + "args": [ + "value" + ], + "lineno": 146, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 156, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 166, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 176, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h.ai.md new file mode 100644 index 0000000000..35bb3447ee --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/PermissionedDomain.h.ai.md @@ -0,0 +1,42 @@ +# `PermissionedDomain.h` — Auto-Generated Ledger Entry Wrapper + +This file is part of the `protocol_autogen/ledger_entries/` family — a collection of auto-generated, type-safe C++ wrappers for every ledger entry type recognized by the XRPL. It represents the `PermissionedDomain` ledger entry (type code `ltPERMISSIONED_DOMAIN`, `0x0082`), a feature that lets account owners define an access-controlled domain by specifying a set of accepted credential types. Only accounts presenting a credential from that set may participate in features gated behind the domain — such as permissioned AMMs or lending pools. The `// This file is auto-generated. Do not edit.` header comment means all changes belong in the upstream code-generation templates, not in this file directly. + +## `PermissionedDomain` — Immutable Read Wrapper + +`PermissionedDomain` inherits from `LedgerEntryBase` and holds a `std::shared_ptr` — a shared, immutable reference to a live ledger entry in node state. The `const` on the pointee is load-bearing: it prevents any code path from mutating a ledger entry through this wrapper, enforcing the read-only contract at the type system level. + +The constructor performs a single upfront type check: + +```cpp +if (sle_->getType() != entryType) + throw std::runtime_error("Invalid ledger entry type for PermissionedDomain"); +``` + +This fail-fast validation moves the invariant assertion to the wrapping point rather than scattering it across every call site. Because all six fields are declared `soeREQUIRED` in the ledger format definition, once the type check passes, all getters can call `sle_->at(...)` unconditionally without returning `std::optional` — if the `SLE` was accepted by consensus it is structurally complete. + +`getAcceptedCredentials()` is the one getter that returns `STArray const&` rather than a value type. `STArray` is a variable-length array of `STObject` elements — each element represents a credential issuer/credential-type pair — and returning it by reference avoids a potentially expensive copy. The `const` reference preserves the immutability contract while keeping access zero-cost. + +The common fields inherited from `LedgerEntryBase` — `getType()`, `getKey()`, `getFlags()`, `getLedgerEntryType()` — are shared across all ledger entry wrappers in the directory and are defined once in the base class. + +## `PermissionedDomainBuilder` — Fluent Construction Interface + +`PermissionedDomainBuilder` uses the CRTP pattern via `LedgerEntryBuilderBase`. The base class holds a mutable `STObject object_{sfLedgerEntry}` and provides common setters (`setFlags`, `setLedgerIndex`) that return a `Derived&`, enabling uniform method chaining across all builder types. The entry-specific setters defined here extend that chain with `setOwner`, `setSequence`, `setAcceptedCredentials`, `setOwnerNode`, `setPreviousTxnID`, and `setPreviousTxnLgrSeq`. + +The primary constructor requires all six required fields up front. This is stricter than a default-then-mutate approach, and it mirrors the XRPL protocol's own requirement: a `PermissionedDomain` SLE that is missing any required field would fail consensus validation anyway, so the builder makes a partial object impossible to construct rather than letting invalid state accumulate silently. + +The base class constructor deliberately avoids calling `object_.set(soTemplate)` before populating fields. That matters because setting a template would create `STBase` placeholder objects for `soeDEFAULT` fields, which causes `applyTemplate()` to throw "may not be explicitly set to default" during the `SLE` constructor. Keeping `object_` as a free (template-less) object and letting the `SLE` constructor handle `applyTemplate()` itself is the correct sequencing. + +A second constructor accepts `std::shared_ptr`, enabling a copy-and-modify workflow: deserialize an existing `PermissionedDomain` from the ledger, wrap it in a builder, update individual fields (such as replacing `sfAcceptedCredentials` after a `PermissionedDomainSet` transaction), and materialize a new SLE. The same type guard is applied here to maintain consistency with the read wrapper. + +Field setters use `std::decay_t const&` — stripping cv-qualifiers and reference from the field's canonical C++ type before accepting it as a `const&`. This pattern appears uniformly across all builders in the directory and prevents accidental reference-lifetime issues when callers pass temporaries. + +`setAcceptedCredentials` calls `object_.setFieldArray(sfAcceptedCredentials, value)` rather than `object_[sfAcceptedCredentials] = value`, consistent with how `STArray` fields are handled throughout the XRPL codebase — the array accessor has a distinct method rather than going through the subscript operator. + +`build(uint256 const& index)` finalizes construction by moving `object_` into a new `SLE` keyed by the given ledger index, then wrapping that `SLE` in a `PermissionedDomain`. The `std::move` ensures no unnecessary copy of the accumulated field data. + +## Relationship to Sibling Files and Broader System + +Every file in `protocol_autogen/ledger_entries/` — `AccountRoot.h`, `Credential.h`, `Offer.h`, and the rest — follows an identical structural pattern: an immutable wrapper that validates on construction and a CRTP fluent builder. The uniformity is a direct consequence of code generation: the template knows the field list and optionality for each entry type and emits the same skeleton. + +`PermissionedDomain` has a meaningful semantic relationship to `Credential.h`: the `sfAcceptedCredentials` array references credential types that must match `Credential` ledger entries owned by other accounts. This cross-entry referential integrity is not enforced at the wrapper layer; it is validated during transaction processing by the `PermissionedDomainSet` transactor, which calls `credentials::checkArray()` in preflight and verifies issuer account existence in preclaim. The wrapper layer is intentionally kept thin — it only guarantees type-correct field access, not protocol-level business rules. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h.ai.json new file mode 100644 index 0000000000..f2695ac526 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h.ai.json @@ -0,0 +1,203 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 28, + "name": "RippleState" + }, + { + "args": [ + "balance", + "lowLimit", + "highLimit", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 217, + "name": "RippleStateBuilder" + } + ], + "description": "Defines the RippleState ledger entry wrapper and builder for XRPL, providing type-safe field access and construction for RippleState ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/RippleState.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getBalance" + }, + { + "args": [], + "lineno": 48, + "name": "getLowLimit" + }, + { + "args": [], + "lineno": 58, + "name": "getHighLimit" + }, + { + "args": [], + "lineno": 68, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 78, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 88, + "name": "getLowNode" + }, + { + "args": [], + "lineno": 99, + "name": "hasLowNode" + }, + { + "args": [], + "lineno": 109, + "name": "getLowQualityIn" + }, + { + "args": [], + "lineno": 120, + "name": "hasLowQualityIn" + }, + { + "args": [], + "lineno": 130, + "name": "getLowQualityOut" + }, + { + "args": [], + "lineno": 141, + "name": "hasLowQualityOut" + }, + { + "args": [], + "lineno": 151, + "name": "getHighNode" + }, + { + "args": [], + "lineno": 162, + "name": "hasHighNode" + }, + { + "args": [], + "lineno": 172, + "name": "getHighQualityIn" + }, + { + "args": [], + "lineno": 183, + "name": "hasHighQualityIn" + }, + { + "args": [], + "lineno": 193, + "name": "getHighQualityOut" + }, + { + "args": [], + "lineno": 204, + "name": "hasHighQualityOut" + }, + { + "args": [ + "value" + ], + "lineno": 241, + "name": "setBalance" + }, + { + "args": [ + "value" + ], + "lineno": 251, + "name": "setLowLimit" + }, + { + "args": [ + "value" + ], + "lineno": 261, + "name": "setHighLimit" + }, + { + "args": [ + "value" + ], + "lineno": 271, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 281, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 291, + "name": "setLowNode" + }, + { + "args": [ + "value" + ], + "lineno": 301, + "name": "setLowQualityIn" + }, + { + "args": [ + "value" + ], + "lineno": 311, + "name": "setLowQualityOut" + }, + { + "args": [ + "value" + ], + "lineno": 321, + "name": "setHighNode" + }, + { + "args": [ + "value" + ], + "lineno": 331, + "name": "setHighQualityIn" + }, + { + "args": [ + "value" + ], + "lineno": 341, + "name": "setHighQualityOut" + }, + { + "args": [ + "index" + ], + "lineno": 351, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/RippleState.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h.ai.md new file mode 100644 index 0000000000..112a93feb5 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/RippleState.h.ai.md @@ -0,0 +1,37 @@ +# `RippleState.h` — Trust Line Ledger Entry Wrapper + +`RippleState` is the on-ledger representation of a trust line between two XRPL accounts. Every time two parties agree to hold an IOU balance in a shared currency, the ledger stores exactly one `RippleState` object keyed by the canonical hash of the two account IDs and the currency code. This file is auto-generated and lives inside the `xrpl::ledger_entries` namespace alongside every other ledger entry type in the `protocol_autogen` layer. + +## Role in the System + +Raw ledger state in rippled is stored as `SLE` (Serialized Ledger Entry) objects — essentially generic key-value bags whose fields are identified by `SField` descriptors at runtime. Working with bare `SLE` objects everywhere would mean scattering `sle_->at(sfLowLimit)` calls across the codebase with no compile-time assurance that the right entry type is in hand. The `protocol_autogen` layer solves this by generating a thin, immutable wrapper for each entry type. `RippleState` is that wrapper for `ltRIPPLE_STATE` (wire type `0x0072`). + +## Two-Class Design: Wrapper and Builder + +The file defines two cooperating classes that enforce a clean separation between construction and read-only access. + +`RippleState` extends `LedgerEntryBase` and holds a `std::shared_ptr` (the `const` is significant — there is no mutation path through this class). The constructor takes an already-live `SLE`, immediately checks `sle_->getType() != entryType`, and throws `std::runtime_error` on mismatch. This makes it impossible to accidentally wrap the wrong entry type and then call `getBalance()` on, say, an `Offer`. The `static constexpr` member `entryType` doubles as a compile-time marker that other template machinery can interrogate without instantiating the class. + +`RippleStateBuilder` extends `LedgerEntryBuilderBase` — a CRTP base that holds an `STObject object_{sfLedgerEntry}` and provides `setFlags()`/`setLedgerIndex()`. The CRTP trick means every setter in the base returns `Derived&` (i.e., `RippleStateBuilder&`) so method chains don't lose the concrete type. The builder's five-argument constructor accepts all required fields and immediately writes them into `object_`; the remaining optional fields are set via individual `set*` calls. A second constructor accepts an existing `SLE const` directly, performing the same type guard before copying the `STObject`, which enables in-place modification workflows. + +`build(uint256 const& index)` is the sole exit point from the builder. It moves the accumulated `STObject` into a freshly constructed `SLE` (which calls `applyTemplate()` internally, filling in any missing defaulted fields), then wraps the resulting `shared_ptr` in a `RippleState` value. The builder intentionally does *not* call `object_.set(soTemplate)` during initialization — a deliberate design choice noted in `LedgerEntryBuilderBase` to avoid creating `STBase` placeholders for `soeDEFAULT` fields, which would cause `applyTemplate()` to throw "may not be explicitly set to default." + +## Asymmetric Low/High Field Layout + +Every field on a trust line is duplicated: `sfLowLimit`/`sfHighLimit`, `sfLowNode`/`sfHighNode`, `sfLowQualityIn`/`sfLowQualityOut`/`sfHighQualityIn`/`sfHighQualityOut`. This reflects XRPL's design for trust lines as bidirectional relationships. The "low" party is the account whose 160-bit account ID is numerically smaller; the "high" party is the other. The balance in `sfBalance` is always stored from the low party's perspective — a positive balance means the low account is owed that amount, negative means it owes. + +The two `Node` fields (`sfLowNode` and `sfHighNode`) are page indices into the respective `DirectoryNode` entries that link this trust line into each account's list of trust lines. They are optional because very old entries on the ledger may predate the directory system. The four quality fields are also optional; when absent the effective quality is `1.0` (unity), meaning payments pass through at face value. + +## `Optional` and the Presence Pattern + +For every optional field, the class generates a paired `has*()` / `get*()` accessor. The getter returns `protocol_autogen::Optional`, a type alias defined in `Utils.h` that resolves to `std::optional>` when `T` is a reference type, and plain `std::optional` otherwise. This handles the nuance that some SField value types are returned by reference from `STObject::at()`, while others are returned by value, ensuring the optional never holds a dangling reference. + +Required fields — `sfBalance`, `sfLowLimit`, `sfHighLimit`, `sfPreviousTxnID`, `sfPreviousTxnLgrSeq` — return their values directly with no optional wrapping, since the type invariant (enforced at construction) guarantees they are always present in a valid `RippleState`. + +## Flags Encoding + +The `getFlags()` method is inherited from `LedgerEntryBase` and returns the raw 32-bit flags word. RippleState uses a pair-of-bits convention for every behavioral flag: `lsfLowReserve`/`lsfHighReserve`, `lsfLowAuth`/`lsfHighAuth`, `lsfLowNoRipple`/`lsfHighNoRipple`, `lsfLowFreeze`/`lsfHighFreeze`, and `lsfLowDeepFreeze`/`lsfHighDeepFreeze`. Each bit is independently set by the corresponding account, so the freeze state of a trust line is determined by reading one bit on behalf of each party rather than through a separate field. + +## Relationship to Surrounding Code + +`RippleState` is one of roughly thirty generated entry types in `include/xrpl/protocol_autogen/ledger_entries/`. All follow the identical structural pattern: an immutable wrapper inheriting `LedgerEntryBase`, a CRTP builder inheriting `LedgerEntryBuilderBase`, a five-or-fewer-argument required-field constructor, and a `build(uint256)` finalizer. The consistency comes from code generation, not convention — no human would reliably maintain this structure across all entry types without drift. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/SignerList.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/SignerList.h.ai.json new file mode 100644 index 0000000000..c818982ca1 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/SignerList.h.ai.json @@ -0,0 +1,196 @@ +{ + "args": [ + { + "lineno": 27, + "name": "sle" + }, + { + "lineno": 120, + "name": "ownerNode" + }, + { + "lineno": 120, + "name": "signerQuorum" + }, + { + "lineno": 120, + "name": "signerEntries" + }, + { + "lineno": 120, + "name": "signerListID" + }, + { + "lineno": 120, + "name": "previousTxnID" + }, + { + "lineno": 120, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 130, + "name": "sle" + }, + { + "lineno": 134, + "name": "value" + }, + { + "lineno": 144, + "name": "value" + }, + { + "lineno": 154, + "name": "value" + }, + { + "lineno": 164, + "name": "value" + }, + { + "lineno": 174, + "name": "value" + }, + { + "lineno": 184, + "name": "value" + }, + { + "lineno": 194, + "name": "value" + }, + { + "lineno": 204, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 23, + "name": "SignerList" + }, + { + "args": [ + "ownerNode", + "signerQuorum", + "signerEntries", + "signerListID", + "previousTxnID", + "previousTxnLgrSeq" + ], + "lineno": 115, + "name": "SignerListBuilder" + } + ], + "description": "Defines the SignerList ledger entry wrapper and builder for XRPL, providing type-safe field access and a fluent builder interface for constructing SignerList ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/SignerList.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getOwner" + }, + { + "args": [], + "lineno": 48, + "name": "hasOwner" + }, + { + "args": [], + "lineno": 58, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 67, + "name": "getSignerQuorum" + }, + { + "args": [], + "lineno": 76, + "name": "getSignerEntries" + }, + { + "args": [], + "lineno": 86, + "name": "getSignerListID" + }, + { + "args": [], + "lineno": 95, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 104, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 132, + "name": "setOwner" + }, + { + "args": [ + "value" + ], + "lineno": 142, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 152, + "name": "setSignerQuorum" + }, + { + "args": [ + "value" + ], + "lineno": 162, + "name": "setSignerEntries" + }, + { + "args": [ + "value" + ], + "lineno": 172, + "name": "setSignerListID" + }, + { + "args": [ + "value" + ], + "lineno": 182, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 192, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 202, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/SignerList.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/SignerList.h.ai.md new file mode 100644 index 0000000000..8f4f51073e --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/SignerList.h.ai.md @@ -0,0 +1,41 @@ +# `SignerList.h` — Auto-Generated Ledger Entry Wrapper + +## Role in the System + +This file is part of the `protocol_autogen` subsystem, which generates type-safe C++ wrappers for every XRPL ledger entry type. `SignerList.h` represents the `ltSIGNER_LIST` (0x0053) ledger entry — the on-ledger record that enables multi-signature authorization for an XRPL account. When an account activates multi-signing via a `SignerListSet` transaction, a `SignerList` object is written to the ledger containing the set of authorized co-signers and the quorum weight required to approve subsequent transactions. + +The file lives in `include/xrpl/protocol_autogen/ledger_entries/` alongside approximately thirty other generated entry wrappers (e.g., `AccountRoot.h`, `Offer.h`, `Escrow.h`). It is auto-generated — the comment at line 1 is literal — meaning the source-of-truth is an upstream schema definition, and manual edits would be overwritten. + +## The Two-Class Pattern + +Every autogen entry header defines exactly two classes: a read-only wrapper named after the entry type (`SignerList`) and a companion builder (`SignerListBuilder`). This strict separation between read and write paths is a deliberate architectural choice. The ledger state is immutable once written; wrapping it in a `const`-correct, read-only view prevents accidental mutation through the accessor layer. + +### `SignerList` — Immutable Wrapper + +`SignerList` inherits from `LedgerEntryBase`, which itself wraps a `std::shared_ptr` (SLE = Serialized Ledger Entry, the XRPL canonical on-wire object). The constructor takes shared ownership of an existing SLE and immediately validates that its type tag is `ltSIGNER_LIST`, throwing `std::runtime_error` on mismatch. This upfront check means any subsequent field access on a `SignerList` instance can assume the correct entry type without repeated defensive checks. + +The class exposes six domain-specific getters: + +- `getOwnerNode()` returns an `SF_UINT64` index hint into the owner directory tree, used for efficient ledger traversal when an account holds many objects. +- `getSignerQuorum()` returns the minimum total signer weight required to authorise a transaction — the core threshold of the multi-sig scheme. +- `getSignerEntries()` returns a `const STArray&` — an array of `SignerEntry` inner objects, each carrying an account ID and its weight (`sfAccount`, `sfSignerWeight`). This is the only field returned as an `STArray` rather than a scalar value type; the comment marks it as "untyped (unknown)", reflecting that the code generator has no first-class typed array accessor for inner objects. +- `getSignerListID()` returns a `uint32_t` identifier — currently always `0` per protocol. The field was introduced to reserve space for a potential future feature allowing multiple signer lists per account, though that extension was never deployed. +- `getPreviousTxnID()` and `getPreviousTxnLgrSeq()` are standard bookkeeping fields present on nearly every mutable ledger entry, recording which transaction last touched this object. + +The `sfOwner` field is optional (`soeOPTIONAL`), so `getOwner()` returns `protocol_autogen::Optional`. This type alias, defined in `Utils.h`, resolves to `std::optional` for value types and `std::optional>` for reference types — ensuring that both reference and value semantics for optional fields work correctly without separate specialisations. A `hasOwner()` predicate mirrors the pattern used by `LedgerEntryBase` for common optional fields like `sfLedgerIndex`. + +### `SignerListBuilder` — Fluent Builder + +`SignerListBuilder` inherits from the CRTP base `LedgerEntryBuilderBase`. The base holds an `STObject object_{sfLedgerEntry}` as mutable storage and provides common setters for `sfFlags` and `sfLedgerIndex`. The CRTP pattern lets each `setXxx()` return `SignerListBuilder&` rather than a `LedgerEntryBuilderBase&`, enabling unbroken method chaining without virtual dispatch or casts at call sites. + +The constructor enforces the required/optional split directly: all six required fields (`ownerNode`, `signerQuorum`, `signerEntries`, `signerListID`, `previousTxnID`, `previousTxnLgrSeq`) must be provided at construction time. The optional `sfOwner` has no corresponding constructor parameter and must be set explicitly via `setOwner()` afterward. This makes it impossible to accidentally omit a required field and only discover the problem at validation time. + +There is a secondary constructor accepting an existing `SLE const` — it copies the SLE's field values into the internal `STObject`, enabling a pattern where callers load an existing ledger entry, wrap it in a builder, mutate specific fields, and re-build a new entry. This is useful in amendment-driven upgrade logic or test helpers. + +A critical subtlety in `LedgerEntryBuilderBase`: the constructor deliberately does not call `object_.set(soTemplate)`. Doing so would pre-populate all `soeDEFAULT` fields with placeholder `STBase` values, which would then cause the `SLE` constructor's `applyTemplate()` call to throw "may not be explicitly set to default". Leaving those fields absent from the `STObject` lets the `SLE` constructor set them properly on finalisation. + +The `build(uint256 const& index)` method finalises construction by moving the internal `STObject` into an `SLE` keyed by the provided index, then wrapping it in the `SignerList` read-only type, completing the read/write lifecycle. + +## Design Tradeoffs + +The strict immutability of `SignerList` means callers cannot patch individual fields on an existing wrapper — they must go through the builder, which creates a new SLE. This adds a copy but eliminates entire classes of accidental state corruption in ledger processing code. The autogen approach also ensures that every entry type has a uniform interface contract: every entry always has `getType()`, `getKey()`, `getFlags()`, `validate()`, and `getSle()` inherited from `LedgerEntryBase`, while entry-specific fields are consistently exposed through named accessors with `[[nodiscard]]` annotations to discourage silently ignored return values. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Ticket.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Ticket.h.ai.json new file mode 100644 index 0000000000..65c136c545 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Ticket.h.ai.json @@ -0,0 +1,151 @@ +{ + "args": [ + { + "lineno": 32, + "name": "sle" + }, + { + "lineno": 98, + "name": "account" + }, + { + "lineno": 98, + "name": "ownerNode" + }, + { + "lineno": 98, + "name": "ticketSequence" + }, + { + "lineno": 98, + "name": "previousTxnID" + }, + { + "lineno": 98, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 107, + "name": "sle" + }, + { + "lineno": 112, + "name": "value" + }, + { + "lineno": 122, + "name": "value" + }, + { + "lineno": 132, + "name": "value" + }, + { + "lineno": 142, + "name": "value" + }, + { + "lineno": 152, + "name": "value" + }, + { + "lineno": 162, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "sle" + ], + "lineno": 28, + "name": "Ticket" + }, + { + "args": [ + "account, ownerNode, ticketSequence, previousTxnID, previousTxnLgrSeq", + "sle" + ], + "lineno": 91, + "name": "TicketBuilder" + } + ], + "description": "Defines the Ticket ledger entry and its builder for XRPL, providing type-safe accessors and a fluent builder interface for constructing Ticket ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Ticket.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getAccount" + }, + { + "args": [], + "lineno": 48, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 58, + "name": "getTicketSequence" + }, + { + "args": [], + "lineno": 68, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 78, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 110, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 120, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 130, + "name": "setTicketSequence" + }, + { + "args": [ + "value" + ], + "lineno": 140, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 150, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 160, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Ticket.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Ticket.h.ai.md new file mode 100644 index 0000000000..0c57d0f3bd --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Ticket.h.ai.md @@ -0,0 +1,40 @@ +# `include/xrpl/protocol_autogen/ledger_entries/Ticket.h` + +## Role and Context + +This auto-generated header defines the `Ticket` ledger entry type (`ltTICKET`, type code `0x0054`) and its companion builder for the XRP Ledger's typed access layer. A Ticket is a reserved sequence number: when an account issues a `TicketCreate` transaction, the ledger writes one `ltTICKET` object per reserved slot, each holding a specific sequence number that a future transaction may consume in lieu of the account's current sequence counter. This mechanism enables out-of-order transaction submission, which is useful for multi-party workflows where signers may apply transactions in any order without invalidating other pending ones. + +The file is entirely auto-generated — the comment at line 1 makes that explicit — and follows the same structural template as every other entry under `protocol_autogen/ledger_entries/`. Changing it by hand would be overwritten by the code-generation step. + +## `Ticket` — The Immutable Wrapper + +`Ticket` inherits from `LedgerEntryBase`, which holds a `std::shared_ptr` and exposes getters for the universal fields (`sfLedgerEntryType`, `sfFlags`, `sfLedgerIndex`) as well as `validate()` and `getSle()`. The subclass extends that with five required field accessors specific to `ltTICKET`: + +- `getAccount()` — the `AccountID` of the owning account. +- `getOwnerNode()` — a 64-bit page index into that account's owner directory, needed when the ledger must locate and remove this entry without a full directory scan. +- `getTicketSequence()` — the reserved sequence number this ticket represents. +- `getPreviousTxnID()` / `getPreviousTxnLgrSeq()` — the standard provenance pair recording which transaction last modified this object and in which ledger it appeared. + +All five are `[[nodiscard]]` to make silent value drops a compile-time warning rather than a runtime mystery. + +The constructor takes a `std::shared_ptr` and immediately checks `sle_->getType() != entryType`, throwing `std::runtime_error` if the underlying object is not actually an `ltTICKET`. This guard prevents subtle bugs where a caller retrieves the wrong entry type from the ledger view and silently reads garbage field values through the strongly-typed accessors. + +## `TicketBuilder` — Fluent Construction via CRTP + +`TicketBuilder` inherits from `LedgerEntryBuilderBase`, a CRTP base whose common setters (`setLedgerIndex`, `setFlags`) return `Derived&` rather than `LedgerEntryBuilderBase&`. This lets callers chain entry-specific setters with universal ones without breaking the fluent interface through upcasting. + +The builder holds an `STObject object_{sfLedgerEntry}` rather than an `SLE`. The base class intentionally avoids calling `object_.set(soTemplate)` at construction time because doing so would install `STBase` placeholder objects for `soeDEFAULT` fields — placeholders that would later cause `applyTemplate()` inside the `SLE` constructor to throw "may not be explicitly set to default." Keeping `object_` as a free `STObject` sidesteps this trap; the `SLE` constructor handles template application correctly when `build()` materialises it. + +`build(uint256 const& index)` is the terminal step: it moves the assembled `STObject` into a new `SLE` keyed at `index`, wraps it in a `shared_ptr`, and passes it to `Ticket`'s constructor. The result is a fully validated, immutable `Ticket` value. + +The secondary constructor `TicketBuilder(std::shared_ptr sle)` is a "copy-from-existing" path. It verifies the entry type via `sle->at(sfLedgerEntryType) != ltTICKET`, then copies the dereferenced `SLE` into `object_`. This allows code to round-trip through the builder — for example, to read an existing ticket, apply a field update, and produce a modified entry — without touching the raw `SLE` API. + +The setter parameter type `std::decay_t const&` ensures that reference qualifiers and cv-qualifiers from the SField's declared value type are stripped before the `const&` is applied. This prevents double-reference issues for field types that are themselves aliases for reference-qualified types. + +## Relationship to `TicketCreate` + +In `TicketCreate::doApply()` (in `src/libxrpl/tx/transactors/system/TicketCreate.cpp`), tickets are created using the raw `SLE` API rather than `TicketBuilder`. The transactor loop writes `sfAccount`, `sfOwnerNode`, and `sfTicketSequence` directly onto freshly allocated `SLE` objects, inserts each into the ledger view, and inserts it into the account's owner directory. `sfPreviousTxnID` and `sfPreviousTxnLgrSeq` are stamped later by the transaction application machinery on all modified entries. `TicketBuilder` is therefore primarily a consumer-facing construction tool — used in tests and higher-level protocol code — while the transactor layer works directly with the underlying serialisation primitives for performance. + +## Error Handling and Invariants + +Both `Ticket` and `TicketBuilder` enforce the same type-identity invariant on construction: an entry whose type field doesn't match `ltTICKET` is an immediate `std::runtime_error`. The test suite in `TicketTests.cpp` verifies this in two dedicated test cases (`WrapperThrowsOnWrongEntryType`, `BuilderThrowsOnWrongEntryType`) by constructing a `Check` entry and confirming that attempting to wrap it as either a `Ticket` or a `TicketBuilder` throws. The round-trip tests additionally call `validate()` on both the builder's in-progress `STObject` and the final `Ticket` wrapper to confirm that the `LedgerFormats` template for `ltTICKET` is fully satisfied before and after `build()`. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Vault.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/Vault.h.ai.json new file mode 100644 index 0000000000..95716869fa --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Vault.h.ai.json @@ -0,0 +1,364 @@ +{ + "args": [ + { + "lineno": 36, + "name": "sle" + }, + { + "lineno": 269, + "name": "previousTxnID" + }, + { + "lineno": 269, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 269, + "name": "sequence" + }, + { + "lineno": 269, + "name": "ownerNode" + }, + { + "lineno": 269, + "name": "owner" + }, + { + "lineno": 269, + "name": "account" + }, + { + "lineno": 269, + "name": "asset" + }, + { + "lineno": 269, + "name": "shareMPTID" + }, + { + "lineno": 269, + "name": "withdrawalPolicy" + }, + { + "lineno": 281, + "name": "sle" + }, + { + "lineno": 294, + "name": "value" + }, + { + "lineno": 304, + "name": "value" + }, + { + "lineno": 314, + "name": "value" + }, + { + "lineno": 324, + "name": "value" + }, + { + "lineno": 334, + "name": "value" + }, + { + "lineno": 344, + "name": "value" + }, + { + "lineno": 354, + "name": "value" + }, + { + "lineno": 364, + "name": "value" + }, + { + "lineno": 374, + "name": "value" + }, + { + "lineno": 384, + "name": "value" + }, + { + "lineno": 394, + "name": "value" + }, + { + "lineno": 404, + "name": "value" + }, + { + "lineno": 414, + "name": "value" + }, + { + "lineno": 424, + "name": "value" + }, + { + "lineno": 434, + "name": "value" + }, + { + "lineno": 444, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 32, + "name": "Vault" + }, + { + "args": [ + "previousTxnID", + "previousTxnLgrSeq", + "sequence", + "ownerNode", + "owner", + "account", + "asset", + "shareMPTID", + "withdrawalPolicy" + ], + "lineno": 267, + "name": "VaultBuilder" + } + ], + "description": "Defines the Vault ledger entry type for the XRPL, providing a type-safe wrapper (Vault) for accessing fields of a Vault ledger entry and a builder class (VaultBuilder) for constructing such entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/Vault.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 54, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [], + "lineno": 64, + "name": "getSequence" + }, + { + "args": [], + "lineno": 74, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 84, + "name": "getOwner" + }, + { + "args": [], + "lineno": 94, + "name": "getAccount" + }, + { + "args": [], + "lineno": 104, + "name": "getData" + }, + { + "args": [], + "lineno": 116, + "name": "hasData" + }, + { + "args": [], + "lineno": 126, + "name": "getAsset" + }, + { + "args": [], + "lineno": 136, + "name": "getAssetsTotal" + }, + { + "args": [], + "lineno": 148, + "name": "hasAssetsTotal" + }, + { + "args": [], + "lineno": 158, + "name": "getAssetsAvailable" + }, + { + "args": [], + "lineno": 170, + "name": "hasAssetsAvailable" + }, + { + "args": [], + "lineno": 180, + "name": "getAssetsMaximum" + }, + { + "args": [], + "lineno": 192, + "name": "hasAssetsMaximum" + }, + { + "args": [], + "lineno": 202, + "name": "getLossUnrealized" + }, + { + "args": [], + "lineno": 214, + "name": "hasLossUnrealized" + }, + { + "args": [], + "lineno": 224, + "name": "getShareMPTID" + }, + { + "args": [], + "lineno": 234, + "name": "getWithdrawalPolicy" + }, + { + "args": [], + "lineno": 244, + "name": "getScale" + }, + { + "args": [], + "lineno": 256, + "name": "hasScale" + }, + { + "args": [ + "value" + ], + "lineno": 292, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 302, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 312, + "name": "setSequence" + }, + { + "args": [ + "value" + ], + "lineno": 322, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 332, + "name": "setOwner" + }, + { + "args": [ + "value" + ], + "lineno": 342, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 352, + "name": "setData" + }, + { + "args": [ + "value" + ], + "lineno": 362, + "name": "setAsset" + }, + { + "args": [ + "value" + ], + "lineno": 372, + "name": "setAssetsTotal" + }, + { + "args": [ + "value" + ], + "lineno": 382, + "name": "setAssetsAvailable" + }, + { + "args": [ + "value" + ], + "lineno": 392, + "name": "setAssetsMaximum" + }, + { + "args": [ + "value" + ], + "lineno": 402, + "name": "setLossUnrealized" + }, + { + "args": [ + "value" + ], + "lineno": 412, + "name": "setShareMPTID" + }, + { + "args": [ + "value" + ], + "lineno": 422, + "name": "setWithdrawalPolicy" + }, + { + "args": [ + "value" + ], + "lineno": 432, + "name": "setScale" + }, + { + "args": [ + "index" + ], + "lineno": 442, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/Vault.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/Vault.h.ai.md new file mode 100644 index 0000000000..68c66ca817 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Vault.h.ai.md @@ -0,0 +1,49 @@ +# `Vault.h` — Auto-generated Vault Ledger Entry Wrapper + +## Role in the System + +This file is auto-generated (editing is prohibited) and provides two companion classes for the `ltVAULT` ledger entry type (type code `0x0084`): a read-only wrapper `Vault` and a fluent builder `VaultBuilder`. It lives in the `xrpl::ledger_entries` namespace alongside every other ledger entry type in the `protocol_autogen/ledger_entries/` directory, forming a layer of ergonomic, type-safe C++ API over the raw `SLE` (Serialized Ledger Entry) infrastructure. + +The Vault entry represents a DeFi-style tokenized yield vault on the XRP Ledger. Depositors receive Multi-Purpose Token (MPT) shares in exchange for depositing a designated asset; the vault tracks total, available, and maximum asset balances along with unrealized losses, and enforces a configurable withdrawal policy. The vault's share token is identified by a 192-bit `sfShareMPTID` linking it to an `MPTokenIssuance` object elsewhere on the ledger. + +## Class Design: Immutable Wrapper + Fluent Builder + +The split between `Vault` and `VaultBuilder` enforces a clear immutability boundary. `Vault` extends `LedgerEntryBase` and holds a `std::shared_ptr`, making every accessor a `const` operation. It is not constructible empty and cannot mutate the underlying entry. `VaultBuilder` extends the CRTP base `LedgerEntryBuilderBase`, which accumulates field values into an internal `STObject object_{sfLedgerEntry}` and delegates `setFlags()`/`setLedgerIndex()` to the base — again without ever touching an `SLE` until `build()` is explicitly called. + +The reason `LedgerEntryBuilderBase` deliberately avoids calling `object_.set(soTemplate)` is captured in a comment: pre-setting `soeDEFAULT` placeholders causes the `SLE` constructor's `applyTemplate()` to throw "may not be explicitly set to default." The builder accumulates only the fields the caller sets; the `SLE` construction at `build()` time then fills in defaults correctly. + +## Field Access Patterns + +Required fields (`soeREQUIRED`) — `sfPreviousTxnID`, `sfPreviousTxnLgrSeq`, `sfSequence`, `sfOwnerNode`, `sfOwner`, `sfAccount`, `sfAsset`, `sfShareMPTID`, `sfWithdrawalPolicy` — are exposed as simple getters that dereference directly through `sle_->at(sf...)` with no null check; callers may rely on them always being present for a validly constructed entry. + +Optional and defaultable fields use the paired `has*()`/`get*()` pattern. The getters return `protocol_autogen::Optional`, a small type alias defined in `Utils.h`: + +```cpp +template +using Optional = std::conditional_t< + std::is_reference_v, + std::optional>>, + std::optional>; +``` + +This handles the case where `sle_->at(sf...)` returns an internal reference (e.g., for blob fields) by wrapping it in `std::reference_wrapper` rather than a dangling `std::optional`. The caller checks the `has*()` variant before dereferencing. Fields in this category are `sfData` (`soeOPTIONAL`), `sfAssetsTotal`, `sfAssetsAvailable`, `sfAssetsMaximum`, `sfLossUnrealized`, and `sfScale` (all `soeDEFAULT`). + +## Asset Tracking with `SF_NUMBER` + +The balance fields (`sfAssetsTotal`, `sfAssetsAvailable`, `sfAssetsMaximum`, `sfLossUnrealized`) use `SF_NUMBER`, whose underlying type is `STNumber`. As documented in `STNumber.h`, `STNumber` is an `STAmount` without embedded asset information — it stores only the numeric value and acquires its token-type context (XRP, IOU, or MPT) at runtime via `associateAsset()`. For vault entries, all four fields represent amounts of the vault's `sfAsset`, so the asset association must be established before arithmetic operations during transaction processing. This design avoids duplicating the asset descriptor in every balance field at the cost of requiring explicit association before use. + +## The `sfOwner` / `sfAccount` Distinction + +The vault holds two account-typed fields. `sfOwner` is the human account that created the vault and holds administrative control over it. `sfAccount` is the vault's own pseudo-account on the ledger — on XRPL, vaults are represented as first-class ledger accounts to support direct balance holding and trust line interactions. This two-account structure mirrors the approach used by Automated Market Makers and bridges. + +## Builder Constructors + +`VaultBuilder` exposes two constructors. The primary constructor accepts all nine required fields explicitly, sets them immediately, and is the canonical path for creating new vault entries. The secondary constructor accepts an existing `std::shared_ptr` and reconstitutes the builder from it (copying the SLE data into `object_`), which supports mutation-via-copy workflows. Both constructors validate the `sfLedgerEntryType` and throw `std::runtime_error` on a mismatch — the same guard the `Vault` reader applies in its own constructor after moving the shared pointer into the base. + +The `setAsset()` setter has one notable divergence from the other setters: it wraps the value in `STIssue(sfAsset, value)` before assigning it into `object_`. This is necessary because `sfAsset` is typed as `SF_ISSUE`, whose serialization expects an `STIssue` wrapper rather than a raw `Issue` value type. + +`build()` finalizes construction by moving `object_` into a freshly allocated `SLE`, binding it to the caller-supplied `uint256` index, and returning a fully constructed `Vault` wrapper. The index is the entry's key in the ledger state map and is computed externally (typically via `keylet::vault()`), keeping the builder free of key-derivation logic. + +## Withdrawal Policy and Scale + +`sfWithdrawalPolicy` is a `uint8_t` required field. The only currently defined value is `vaultStrategyFirstComeFirstServe = 1` (from `Protocol.h`), meaning withdrawals are honored in submission order as long as liquidity is available. `sfScale` is a `uint8_t` defaultable field that governs the decimal precision of IOU-denominated shares, defaulting to `vaultDefaultIOUScale = 6` and capped at `vaultMaximumIOUScale = 18` — the cap is chosen so that 1 IOU can always be converted to at least one MPToken share given the maximum MPToken supply of 2⁶⁴ − 1. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h.ai.json new file mode 100644 index 0000000000..15240870b7 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h.ai.json @@ -0,0 +1,231 @@ +{ + "args": [ + { + "lineno": 32, + "name": "sle" + }, + { + "lineno": 137, + "name": "account" + }, + { + "lineno": 137, + "name": "xChainBridge" + }, + { + "lineno": 137, + "name": "xChainClaimID" + }, + { + "lineno": 137, + "name": "otherChainSource" + }, + { + "lineno": 137, + "name": "xChainClaimAttestations" + }, + { + "lineno": 137, + "name": "signatureReward" + }, + { + "lineno": 137, + "name": "ownerNode" + }, + { + "lineno": 137, + "name": "previousTxnID" + }, + { + "lineno": 137, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 147, + "name": "sle" + }, + { + "lineno": 156, + "name": "value" + }, + { + "lineno": 166, + "name": "value" + }, + { + "lineno": 176, + "name": "value" + }, + { + "lineno": 186, + "name": "value" + }, + { + "lineno": 196, + "name": "value" + }, + { + "lineno": 206, + "name": "value" + }, + { + "lineno": 216, + "name": "value" + }, + { + "lineno": 226, + "name": "value" + }, + { + "lineno": 236, + "name": "value" + }, + { + "lineno": 246, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 30, + "name": "XChainOwnedClaimID" + }, + { + "args": [ + "std::decay_t const& account, std::decay_t const& xChainBridge, std::decay_t const& xChainClaimID, std::decay_t const& otherChainSource, STArray const& xChainClaimAttestations, std::decay_t const& signatureReward, std::decay_t const& ownerNode, std::decay_t const& previousTxnID, std::decay_t const& previousTxnLgrSeq", + "std::shared_ptr sle" + ], + "lineno": 134, + "name": "XChainOwnedClaimIDBuilder" + } + ], + "description": "Defines the XChainOwnedClaimID ledger entry wrapper and builder for XRPL, providing type-safe field access and a fluent builder interface for constructing and manipulating XChainOwnedClaimID ledger entries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h", + "functions": [ + { + "args": [], + "lineno": 41, + "name": "getAccount" + }, + { + "args": [], + "lineno": 51, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 61, + "name": "getXChainClaimID" + }, + { + "args": [], + "lineno": 71, + "name": "getOtherChainSource" + }, + { + "args": [], + "lineno": 81, + "name": "getXChainClaimAttestations" + }, + { + "args": [], + "lineno": 91, + "name": "getSignatureReward" + }, + { + "args": [], + "lineno": 101, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 111, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 121, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 154, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 164, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 174, + "name": "setXChainClaimID" + }, + { + "args": [ + "value" + ], + "lineno": 184, + "name": "setOtherChainSource" + }, + { + "args": [ + "value" + ], + "lineno": 194, + "name": "setXChainClaimAttestations" + }, + { + "args": [ + "value" + ], + "lineno": 204, + "name": "setSignatureReward" + }, + { + "args": [ + "value" + ], + "lineno": 214, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 224, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 234, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 244, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h.ai.md new file mode 100644 index 0000000000..d645f99eaa --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedClaimID.h.ai.md @@ -0,0 +1,44 @@ +# `XChainOwnedClaimID.h` — Cross-Chain Claim ID Ledger Entry Wrapper + +## Purpose and Context + +This file is part of the `protocol_autogen` subsystem — a layer of auto-generated C++ that wraps the XRPL ledger's raw serialized objects (`SLE`) with type-safe, field-specific accessors. The `// This file is auto-generated. Do not edit.` pragma signals that it is produced by a code generator from a schema definition and must not be modified by hand. + +`XChainOwnedClaimID` (ledger type `ltXCHAIN_OWNED_CLAIM_ID`, tag `0x0071`) is a ledger entry created when an account initiates a cross-chain value transfer via the XRPL XChain Bridge feature. Conceptually, a user locks funds on a source chain and submits an `XChainCreateClaimID` transaction. This creates an `XChainOwnedClaimID` entry on the destination chain's ledger. That entry then accumulates signed attestations from the bridge's trusted witness servers until a quorum is reached, at which point the corresponding `XChainClaim` transaction can release the equivalent funds to the recipient. + +The entry therefore acts as an accumulation point for cross-chain evidence. It is distinct from `XChainOwnedCreateAccountClaimID` (type `0x0074`), which handles the more specialized case of creating a brand-new account on the destination chain rather than crediting an existing one. + +## Key Fields + +All nine fields exposed by `XChainOwnedClaimID` are marked `soeREQUIRED`: + +- **`sfAccount`** — the XRPL account that owns this claim ID on the destination chain. This account pays the reserve and will ultimately receive the transferred value. +- **`sfXChainBridge`** — a composite field (`SF_XCHAIN_BRIDGE`) identifying the specific bridge: it encodes the issuing account, locking chain door, and issuing chain door. +- **`sfXChainClaimID`** — a monotonically increasing `uint64` sequence number that uniquely identifies this claim within the bridge. It prevents replay attacks by ensuring each transfer has a unique handle. +- **`sfOtherChainSource`** — the account on the source chain that originated the transfer. Witnesses use this to correlate on-chain events. +- **`sfXChainClaimAttestations`** — an `STArray` of attestation objects collected from bridge witnesses. Each element encodes a witness's signature confirming the source-chain transaction. Because the field type is not a simple scalar, it is accessed via the `getFieldArray` / `setFieldArray` SLE methods rather than the generic `at()` operator; the comment labels it "untyped (unknown)" reflecting the code generator's handling of array fields that lack a precise static type. +- **`sfSignatureReward`** — an `STAmount` paid out to the witnesses in exchange for their attestations. This is the economic incentive that keeps the bridge's witness network functioning. +- **`sfOwnerNode`** — a `uint64` back-pointer into the owner directory that tracks which directory page holds this entry. Standard XRPL bookkeeping for owner reserve accounting. +- **`sfPreviousTxnID`** and **`sfPreviousTxnLgrSeq`** — the canonical transaction audit trail carried by all mutable ledger objects: the hash and ledger sequence of the last transaction that modified this entry. + +## Class Design: Wrapper and Builder + +The file defines two classes following the same pattern used across all ~30 auto-generated ledger entry types in this directory. + +### `XChainOwnedClaimID` (read-only wrapper) + +`XChainOwnedClaimID` inherits from `LedgerEntryBase`, which holds a `std::shared_ptr` and exposes common fields (`getType()`, `getKey()`, `getFlags()`, `validate()`). The `const` on the SLE pointer is deliberate: the wrapper is intentionally immutable. All field getters are marked `[[nodiscard]]` to prevent silent discard of returned values. The constructor immediately validates the SLE's type code against the class's `static constexpr entryType`, throwing `std::runtime_error` on mismatch. This is a hard fail rather than a silent no-op — consuming code that obtains the wrong SLE from the ledger will crash loudly rather than silently reading garbage field values. + +### `XChainOwnedClaimIDBuilder` (mutable construction) + +`XChainOwnedClaimIDBuilder` inherits from `LedgerEntryBuilderBase`, which uses CRTP (Curiously Recurring Template Pattern) so that `setLedgerIndex()` and `setFlags()` defined in the base return a reference to the concrete derived type rather than to the base. This enables uninterrupted method chaining even when mixing base-class and derived-class setters. + +The builder stores its in-progress state as an `STObject object_{sfLedgerEntry}` (a "free" object without a template applied). A deliberate design note in `LedgerEntryBuilderBase` explains why the SO template is *not* applied eagerly: setting `soeDEFAULT` placeholder values on the `STObject` before constructing the `SLE` would cause `applyTemplate()` to throw a "may not be explicitly set to default" error. By keeping the builder's `STObject` template-free and deferring template application to the `SLE` constructor, the builder avoids this hazard. + +The builder offers two construction paths: a full-argument constructor that enforces every required field at construction time, and a second constructor that ingests an existing `std::shared_ptr` by copying the `SLE`'s raw `STObject` into `object_`. The copy-from-SLE path enables round-trip mutation: read a ledger entry, wrap it in a builder, update specific fields, then call `build()` to produce a fresh immutable `XChainOwnedClaimID`. The `build()` method finalizes the entry by constructing an `SLE` from the accumulated `STObject` and the caller-supplied `uint256` index, then wrapping it in a new `XChainOwnedClaimID`. + +Field setter parameters use `std::decay_t` to strip const/reference qualifiers that the SField type machinery may carry, producing clean by-value parameter types suitable for assignment into the `STObject`. + +## Relationship to Sibling Files + +Within `include/xrpl/protocol_autogen/ledger_entries/`, every other ledger entry type — `Bridge.h`, `AccountRoot.h`, `Offer.h`, and so on — follows an identical structural pattern: a read-only `XxxEntry` class backed by `LedgerEntryBase` and a `XxxEntryBuilder` backed by `LedgerEntryBuilderBase`. The `XChainOwnedClaimID` and `XChainOwnedCreateAccountClaimIDBuilder` files together cover the two flavors of XChain claim ID: regular value transfers and account-creation transfers respectively. The companion `Bridge.h` entry represents the bridge configuration object that both claim ID types reference via `sfXChainBridge`. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h.ai.json b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h.ai.json new file mode 100644 index 0000000000..7d1c7961d0 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h.ai.json @@ -0,0 +1,196 @@ +{ + "args": [ + { + "lineno": 27, + "name": "sle" + }, + { + "lineno": 108, + "name": "account" + }, + { + "lineno": 109, + "name": "xChainBridge" + }, + { + "lineno": 110, + "name": "xChainAccountCreateCount" + }, + { + "lineno": 111, + "name": "xChainCreateAccountAttestations" + }, + { + "lineno": 112, + "name": "ownerNode" + }, + { + "lineno": 113, + "name": "previousTxnID" + }, + { + "lineno": 114, + "name": "previousTxnLgrSeq" + }, + { + "lineno": 120, + "name": "sle" + }, + { + "lineno": 126, + "name": "value" + }, + { + "lineno": 135, + "name": "value" + }, + { + "lineno": 144, + "name": "value" + }, + { + "lineno": 153, + "name": "value" + }, + { + "lineno": 162, + "name": "value" + }, + { + "lineno": 171, + "name": "value" + }, + { + "lineno": 180, + "name": "value" + }, + { + "lineno": 189, + "name": "index" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr sle" + ], + "lineno": 23, + "name": "XChainOwnedCreateAccountClaimID" + }, + { + "args": [ + "std::decay_t const& account", + "std::decay_t const& xChainBridge", + "std::decay_t const& xChainAccountCreateCount", + "STArray const& xChainCreateAccountAttestations", + "std::decay_t const& ownerNode", + "std::decay_t const& previousTxnID", + "std::decay_t const& previousTxnLgrSeq" + ], + "lineno": 104, + "name": "XChainOwnedCreateAccountClaimIDBuilder" + } + ], + "description": "Defines the XChainOwnedCreateAccountClaimID ledger entry and its builder for XRPL, providing type-safe field access and fluent construction for cross-chain account creation claim IDs.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getAccount" + }, + { + "args": [], + "lineno": 47, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 56, + "name": "getXChainAccountCreateCount" + }, + { + "args": [], + "lineno": 65, + "name": "getXChainCreateAccountAttestations" + }, + { + "args": [], + "lineno": 74, + "name": "getOwnerNode" + }, + { + "args": [], + "lineno": 83, + "name": "getPreviousTxnID" + }, + { + "args": [], + "lineno": 92, + "name": "getPreviousTxnLgrSeq" + }, + { + "args": [ + "value" + ], + "lineno": 124, + "name": "setAccount" + }, + { + "args": [ + "value" + ], + "lineno": 133, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 142, + "name": "setXChainAccountCreateCount" + }, + { + "args": [ + "value" + ], + "lineno": 151, + "name": "setXChainCreateAccountAttestations" + }, + { + "args": [ + "value" + ], + "lineno": 160, + "name": "setOwnerNode" + }, + { + "args": [ + "value" + ], + "lineno": 169, + "name": "setPreviousTxnID" + }, + { + "args": [ + "value" + ], + "lineno": 178, + "name": "setPreviousTxnLgrSeq" + }, + { + "args": [ + "index" + ], + "lineno": 187, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::ledger_entries" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h.ai.md b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h.ai.md new file mode 100644 index 0000000000..2379d4fe2b --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/XChainOwnedCreateAccountClaimID.h.ai.md @@ -0,0 +1,40 @@ +# `XChainOwnedCreateAccountClaimID.h` + +## Role in the System + +This auto-generated header defines the `ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID` ledger entry type (numeric type `0x0074`) — the on-ledger object that tracks the in-progress cross-chain account-creation claim process in XRPL's cross-chain bridge feature. It lives inside `include/xrpl/protocol_autogen/ledger_entries/`, a directory of code-generated wrappers, one per ledger entry type. + +The file exists because creating an account on a destination chain via a cross-chain bridge is a multi-step process that requires attestations from a quorum of bridge witnesses before the destination account is actually funded. This ledger entry acts as the accumulation point for those attestations, identified by the owner account and a monotonic sequence counter (`sfXChainAccountCreateCount`). It is the create-account analogue of `XChainOwnedClaimID` (type `0x0071`), which handles ordinary cross-chain value transfers. + +## The Immutable Wrapper / Builder Split + +The file defines two classes: `XChainOwnedCreateAccountClaimID` (the read-only wrapper) and `XChainOwnedCreateAccountClaimIDBuilder` (the construction vehicle). This split is a deliberate design choice seen across all ledger entry types in this autogen layer. + +`XChainOwnedCreateAccountClaimID` inherits from `LedgerEntryBase`, which wraps a `std::shared_ptr` — the `const` qualifier is load-bearing. Any caller who holds a wrapper object is guaranteed to observe a frozen snapshot of the ledger entry; the underlying `SLE` cannot be mutated through it. Type safety is enforced eagerly in the constructor: if the passed `SLE` does not carry `ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID` as its entry type, a `std::runtime_error` is thrown immediately rather than allowing a later field access to silently return garbage. + +`XChainOwnedCreateAccountClaimIDBuilder` inherits from `LedgerEntryBuilderBase`, a CRTP base that owns a mutable `STObject object_` field and provides common-field setters like `setFlags()` and `setLedgerIndex()`. The CRTP pattern makes those inherited setters return a reference to the concrete `XChainOwnedCreateAccountClaimIDBuilder&`, enabling uninterrupted method-chaining through mixed base/derived calls. The constructor of the base deliberately avoids calling `object_.set(soTemplate)`, because doing so would pre-populate `soeDEFAULT` placeholder fields which would in turn cause the `SLE` constructor's `applyTemplate()` call to reject them as "may not be explicitly set to default." All required fields are instead set explicitly during construction. + +The builder's `build(uint256 const& index)` method materializes the `STObject` into a proper `SLE` by move-constructing it with the given ledger key, then immediately wrapping it in the read-only `XChainOwnedCreateAccountClaimID` class. After `build()` the builder's internal state has been moved from, so it should not be reused. + +A second builder constructor accepts an existing `std::shared_ptr` to allow copying/modifying an already-existing ledger entry. The builder assigns the `SLE`'s serialized content into `object_` with `object_ = *sle`, providing a mutable copy to edit before re-building. This path also validates the entry type, throwing on mismatch. + +## Fields + +All seven fields are marked `soeREQUIRED`, meaning they must be present for the ledger entry to be valid: + +- `sfAccount` — the account on the destination chain that submitted the `XChainCreateAccountClaimID` transaction; this entry is tracked in that account's owner directory. +- `sfXChainBridge` — identifies the specific bridge (locking and issuing chain door accounts plus asset pair), scoping this claim to a particular cross-chain pathway. +- `sfXChainAccountCreateCount` — a `uint64` sequence counter that monotonically increments with each cross-chain account-creation submitted on this bridge. It functions as the unique identifier distinguishing one create-account claim accumulation object from another. +- `sfXChainCreateAccountAttestations` — an `STArray` of witness attestations collected so far for this create-account operation. Each element carries a witness public key, signature, and the amount being moved. Once sufficient attestations accumulate (meeting the quorum threshold), the destination account is funded and this ledger object is deleted. +- `sfOwnerNode` — a back-link into the owner's directory node list, required for O(1) deletion of the entry from the owner directory when the claim is fulfilled or cancelled. +- `sfPreviousTxnID` and `sfPreviousTxnLgrSeq` — standard audit fields tracking the last transaction that modified this ledger entry. + +The `sfXChainCreateAccountAttestations` getter is notable: because the field type is an `STArray` (a compound/array SField), it cannot be fetched via the generic `sle_->at()` template; instead it calls `sle_->getFieldArray()` directly and returns a `const&` rather than a value copy, avoiding an unnecessary deep copy of a potentially large array. + +## Validation and Error Handling + +Both the wrapper and the builder expose a `validate()` method (inherited from their respective base classes). In both cases, validation delegates to `protocol_autogen::validateSTObject()`, which checks the `STObject`'s fields against the canonical `SOTemplate` registered for `ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID` in `LedgerFormats`. This means the validation logic is data-driven and not duplicated here. The constructor-level type checks (`getType() != entryType`) are a separate, earlier guard that catches misuse before any field access is attempted, and they are tested explicitly in the generated test suite (`WrapperThrowsOnWrongEntryType`, `BuilderThrowsOnWrongEntryType`). + +## Relationship to Other Files + +This file is one of roughly 30 files in the `ledger_entries/` autogen directory, all following the same structural template. The cross-chain sibling `XChainOwnedClaimID.h` handles ordinary cross-chain value transfers, while this file specializes in the account-creation flow which requires an additional field (`sfXChainAccountCreateCount`) to sequence the operations. The `AccountObjects` RPC handler and the ledger indexing layer in `Indexes.cpp` reference `ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID` to enumerate and key these entries in the ledger state tree. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMBid.h.ai.json b/include/xrpl/protocol_autogen/transactions/AMMBid.h.ai.json new file mode 100644 index 0000000000..84825b30cf --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMBid.h.ai.json @@ -0,0 +1,196 @@ +{ + "args": [ + { + "lineno": 34, + "name": "tx" + }, + { + "lineno": 139, + "name": "account" + }, + { + "lineno": 139, + "name": "asset" + }, + { + "lineno": 139, + "name": "asset2" + }, + { + "lineno": 139, + "name": "sequence" + }, + { + "lineno": 139, + "name": "fee" + }, + { + "lineno": 150, + "name": "tx" + }, + { + "lineno": 162, + "name": "value" + }, + { + "lineno": 172, + "name": "value" + }, + { + "lineno": 182, + "name": "value" + }, + { + "lineno": 192, + "name": "value" + }, + { + "lineno": 202, + "name": "value" + }, + { + "lineno": 212, + "name": "publicKey" + }, + { + "lineno": 212, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "AMMBid" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& asset, std::decay_t const& asset2, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 134, + "name": "AMMBidBuilder" + } + ], + "description": "Defines the AMMBid transaction type and its builder for the XRPL protocol, providing type-safe field access and a fluent interface for constructing AMMBid transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/AMMBid.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 34, + "name": "AMMBid" + }, + { + "args": [], + "lineno": 51, + "name": "getAsset" + }, + { + "args": [], + "lineno": 61, + "name": "getAsset2" + }, + { + "args": [], + "lineno": 71, + "name": "getBidMin" + }, + { + "args": [], + "lineno": 82, + "name": "hasBidMin" + }, + { + "args": [], + "lineno": 92, + "name": "getBidMax" + }, + { + "args": [], + "lineno": 103, + "name": "hasBidMax" + }, + { + "args": [], + "lineno": 113, + "name": "getAuthAccounts" + }, + { + "args": [], + "lineno": 123, + "name": "hasAuthAccounts" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& asset", + "std::decay_t const& asset2", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 139, + "name": "AMMBidBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 150, + "name": "AMMBidBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 162, + "name": "setAsset" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 172, + "name": "setAsset2" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 182, + "name": "setBidMin" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 192, + "name": "setBidMax" + }, + { + "args": [ + "STArray const& value" + ], + "lineno": 202, + "name": "setAuthAccounts" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 212, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMBid.h.ai.md b/include/xrpl/protocol_autogen/transactions/AMMBid.h.ai.md new file mode 100644 index 0000000000..069bd2c858 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMBid.h.ai.md @@ -0,0 +1,51 @@ +# `include/xrpl/protocol_autogen/transactions/AMMBid.h` + +This file is part of the `protocol_autogen` subsystem — a layer of auto-generated, type-safe C++ wrappers over the raw `STTx` serialized transaction format. It defines two classes for the `AMMBid` transaction type: `AMMBid`, an immutable read accessor, and `AMMBidBuilder`, a fluent construction interface. The header carries an explicit `// This file is auto-generated. Do not edit.` guard and should never be modified by hand. + +## Purpose in the XRPL AMM Model + +`AMMBid` corresponds to transaction type `ttAMM_BID` (numeric code 39), introduced under the `featureAMM` amendment. On the XRP Ledger, each AMM pool has a single "auction slot" — a position that, when won via a bid of LP tokens, grants the slot holder a reduced trading fee for a fixed time window. The `AMMBid` transaction is how an LP token holder competes for that slot, optionally bounding their bid with a minimum (`sfBidMin`) and maximum (`sfBidMax`) amount, and optionally delegating the fee discount to a list of additional accounts (`sfAuthAccounts`). + +The transaction metadata in the class comment records that it is *delegable* (`Delegation::delegable`), meaning a delegate account (`sfDelegate`, inherited from `TransactionBase`) may submit it on behalf of the originating account. + +## Class Design: Immutable Wrapper + Builder Pair + +The file follows a deliberate split present throughout the `protocol_autogen/transactions/` directory: every transaction type is represented by a read-only wrapper and a separate builder, never a single mutable class. This enforces the immutability contract of `STTx` — once a transaction is signed and serialized, it must not change. + +**`AMMBid`** inherits from `TransactionBase`, which holds a `std::shared_ptr` (the `const` is load-bearing). The base exposes accessors for universal fields like `getAccount()`, `getSequence()`, `getFee()`, `getFlags()`, and others shared by all transaction types. `AMMBid` adds only the fields specific to this transaction type. + +The constructor validates the wrapped transaction's type immediately and throws `std::runtime_error` if it does not match `ttAMM_BID`. This is an early-failure defense — it prevents a misidentified `STTx` from silently producing wrong field reads later. + +## Field Model: Required vs. Optional + +The two asset fields — `getAsset()` and `getAsset2()` — are marked `soeREQUIRED` and return `SF_ISSUE::type::value_type` directly (no `optional`). Together they uniquely identify the AMM pool being bid on: `sfAsset` and `sfAsset2` name the two currencies in the pool. Both fields are declared as `SF_ISSUE` types, and the comments note that they support MPT (Multi-Purpose Token) amounts, anticipating the ledger's expanded token model. + +The bid range fields `getBidMin()` and `getBidMax()` are `soeOPTIONAL`. They use the `protocol_autogen::Optional` alias defined in `Utils.h`: + +```cpp +template +using Optional = std::conditional_t< + std::is_reference_v, + std::optional>>, + std::optional>; +``` + +This alias exists because some XRPL field types return values by reference (e.g., complex `STObject` wrappers), while others return plain values. Wrapping both uniformly in `std::optional` would fail for reference types, since `std::optional` is ill-formed in C++17. The `protocol_autogen::Optional` selects the right form at compile time. For `SF_AMOUNT` values (which are returned by value), `Optional` simply reduces to `std::optional`. + +The `sfAuthAccounts` field is the exception: it holds an `STArray` — the ledger's heterogeneous inner-object array type — and is therefore returned as `std::optional>` directly, bypassing `protocol_autogen::Optional`. This is consistent with how `getMemos()` and `getSigners()` work in `TransactionBase`. Returning a `std::reference_wrapper` instead of copying avoids potentially expensive deep copies of the array while still allowing the caller to detect absence cleanly. + +Every optional getter has a corresponding `has*()` predicate (`hasBidMin()`, `hasBidMax()`, `hasAuthAccounts()`). The pattern is consistent: call `isFieldPresent()` on the underlying `STTx` before forwarding to `tx_->at()` or `getFieldArray()`. + +## `AMMBidBuilder`: CRTP Fluent Construction + +`AMMBidBuilder` inherits from `TransactionBuilderBase` using the Curiously Recurring Template Pattern. The base class holds an `STObject object_{sfTransaction}` and provides setters for universal fields (`setAccount`, `setFee`, `setSequence`, `setFlags`, etc.), each returning `Derived&` — here, `AMMBidBuilder&` — enabling full method chaining without any virtual dispatch. + +The primary constructor takes `account`, `asset`, and `asset2` as required parameters, with `sequence` and `fee` as optional. It immediately calls `setAsset()` and `setAsset2()`, ensuring the two invariant fields are always present before the builder is used. A secondary constructor accepts an existing `std::shared_ptr` for editing a pre-built transaction, which is useful in test and tooling contexts. + +Field setters for `sfAsset` and `sfAsset2` explicitly construct `STIssue(sfField, value)` before assignment, handling the type conversion from the `SF_ISSUE::type::value_type` domain into the `STObject` field store. The `setBidMin` and `setBidMax` setters assign `SF_AMOUNT` values directly. `setAuthAccounts` delegates to `object_.setFieldArray()` since `STArray` fields require a dedicated path through the `STObject` API. + +The `build()` method calls the protected `sign()` helper from `TransactionBuilderBase`, which serializes the object with `HashPrefix::txSign` prefix, computes the ECDSA/Ed25519 signature, and embeds the signing public key. It then moves the `STObject` into a new `STTx` and wraps the result in an `AMMBid` instance. After `build()` is called, the builder's internal `object_` has been moved-from and should not be reused. + +## Position in the Autogen Layer + +`AMMBid.h` is one of roughly 70 transaction-specific headers in the `protocol_autogen/transactions/` directory. All follow the same structural template: the same include list, the same constructor guard pattern, the same `TransactionBase` / `TransactionBuilderBase` inheritance chain. The variation between files is entirely in which fields are `soeREQUIRED` versus `soeOPTIONAL` and what types they carry. This regularity is what makes automated generation feasible and reliable, and it is also why the `// Do not edit.` warning matters — any manual patch would be overwritten by the next code-generation run. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMClawback.h.ai.json b/include/xrpl/protocol_autogen/transactions/AMMClawback.h.ai.json new file mode 100644 index 0000000000..5036701b70 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMClawback.h.ai.json @@ -0,0 +1,163 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 101, + "name": "account" + }, + { + "lineno": 101, + "name": "holder" + }, + { + "lineno": 101, + "name": "asset" + }, + { + "lineno": 101, + "name": "asset2" + }, + { + "lineno": 101, + "name": "sequence" + }, + { + "lineno": 101, + "name": "fee" + }, + { + "lineno": 164, + "name": "publicKey" + }, + { + "lineno": 164, + "name": "secretKey" + }, + { + "lineno": 124, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 22, + "name": "AMMClawback" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& holder", + "std::decay_t const& asset", + "std::decay_t const& asset2", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 93, + "name": "AMMClawbackBuilder" + } + ], + "description": "Defines the AMMClawback transaction type and its builder for the XRPL protocol, providing type-safe accessors, mutators, and construction/signing logic for AMMClawback transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/AMMClawback.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "AMMClawback" + }, + { + "args": [], + "lineno": 39, + "name": "getHolder" + }, + { + "args": [], + "lineno": 49, + "name": "getAsset" + }, + { + "args": [], + "lineno": 59, + "name": "getAsset2" + }, + { + "args": [], + "lineno": 69, + "name": "getAmount" + }, + { + "args": [], + "lineno": 81, + "name": "hasAmount" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& holder", + "std::decay_t const& asset", + "std::decay_t const& asset2", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 101, + "name": "AMMClawbackBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 113, + "name": "AMMClawbackBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 124, + "name": "setHolder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 134, + "name": "setAsset" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 144, + "name": "setAsset2" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 154, + "name": "setAmount" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 164, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMClawback.h.ai.md b/include/xrpl/protocol_autogen/transactions/AMMClawback.h.ai.md new file mode 100644 index 0000000000..5d99cf0b49 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMClawback.h.ai.md @@ -0,0 +1,42 @@ +# `AMMClawback.h` — Auto-Generated AMMClawback Transaction Wrapper + +## Role and Context + +This file is part of the `protocol_autogen` subsystem — a code-generated layer that provides type-safe C++ wrappers over the raw `STTx` serialized transaction format used throughout the XRPL node (`rippled`). Every transaction type in the XRPL protocol gets its own header in the `include/xrpl/protocol_autogen/transactions/` directory, and `AMMClawback.h` represents transaction type `ttAMM_CLAWBACK` (numeric type 31). The header comment warns explicitly: **do not edit** — modifications will be overwritten by the generator. + +The `AMMClawback` transaction was introduced under the `featureAMMClawback` amendment. It allows a token issuer — one who has configured clawback authority — to recover tokens held by an account that has a position in an AMM (Automated Market Maker) pool. The transaction carries privileges beyond a normal transaction: `mayDeleteAcct | overrideFreeze | mayAuthorizeMPT`. This is significant because it means the engine grants elevated permissions during processing — the issuer can act across freeze rules and MPT authorization boundaries when reclaiming assets from an AMM. + +## Two-Class Design: Wrapper and Builder + +The file defines two classes that cleanly separate read and write concerns: + +**`AMMClawback`** is an immutable, read-only wrapper around a `std::shared_ptr`. It inherits from `TransactionBase`, which stores the `tx_` pointer and provides getters for universal fields like `sfAccount`, `sfSequence`, `sfFee`, and optional fields like `sfFlags`, `sfMemos`, `sfDelegate`. `AMMClawback` adds the four `AMMClawback`-specific accessors: `getHolder()`, `getAsset()`, `getAsset2()`, and `getAmount()`. + +The constructor validates the `STTx` type at runtime: +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for AMMClawback"); +``` +This guard prevents accidentally wrapping an unrelated transaction in a strongly-typed `AMMClawback` shell, which would otherwise silently return wrong field data through the typed accessors. + +**`AMMClawbackBuilder`** inherits from `TransactionBuilderBase` using CRTP (Curiously Recurring Template Pattern). The base class is templated on the derived type so that every common setter (`setAccount`, `setFee`, `setSequence`, `setMemos`, etc.) returns `Derived&` — preserving the fluent chaining interface through the full type hierarchy without virtual dispatch. The builder accumulates field data into an `STObject object_{sfTransaction}` and does not call `applyTemplate()` eagerly; the `STTx` constructor handles schema enforcement when `build()` is finally called. + +## Field Schema and Optional Handling + +The three required fields (`sfHolder`, `sfAsset`, `sfAsset2`) are set in the constructor, enforcing the protocol requirement that these must always be present. The optional field `sfAmount` follows the `getX()` / `hasX()` pair pattern used across all auto-generated types: `getAmount()` returns `protocol_autogen::Optional` (an alias for `std::optional<...>`), deferring the presence check to `hasAmount()` before reading via `tx_->at(sfAmount)`. + +The `setAsset` and `setAsset2` builder methods handle the `sfAsset` and `sfAsset2` fields with an explicit `STIssue` wrap: +```cpp +object_[sfAsset] = STIssue(sfAsset, value); +``` +This is a subtle but important distinction from a plain assignment. `STIssue` is the serialized representation of an issue (currency + issuer, or MPT ID), and constructing it with the field descriptor ensures the correct field code is embedded for binary serialization. Both asset fields are annotated as supporting MPT (Multi-Purpose Token) amounts, reflecting the AMM subsystem's dual support for IOU and MPT token types. + +## Build and Sign Flow + +The `build(PublicKey, SecretKey)` method in `AMMClawbackBuilder` performs signing in-place via `TransactionBuilderBase::sign()`: it sets `sfSigningPubKey`, serializes the object without signing fields (prepended with `HashPrefix::txSign`), computes a signature with `xrpl::sign()`, and stores it in `sfTxnSignature`. The `STObject` is then moved into a freshly constructed `STTx`, which becomes the immutable payload of the returned `AMMClawback` wrapper. This one-way transformation — builder constructs, `AMMClawback` consumes — makes the type boundary explicit and prevents post-signing mutation. + +There is also a second constructor for `AMMClawbackBuilder` that accepts an existing `std::shared_ptr`, copying its fields into the builder's mutable `STObject`. This round-trip path exists to support modifying and re-signing a previously built transaction. + +## Relationship to the Auto-Gen System + +`AMMClawback.h` is structurally identical to every other file in the `transactions/` directory (e.g., `AMMDeposit.h`, `AMMWithdraw.h`, `Clawback.h`). The only variation is the set of transaction-specific fields and the `txType` constant. `TransactionBase` and `TransactionBuilderBase` are the shared infrastructure; the per-transaction files are thin generated shells that expose exactly the fields defined in that transaction's protocol schema. This design means the generated code is predictable and auditable by diff, and the base classes can be improved independently without touching the ~70 generated transaction headers. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMCreate.h.ai.json b/include/xrpl/protocol_autogen/transactions/AMMCreate.h.ai.json new file mode 100644 index 0000000000..55692d3285 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMCreate.h.ai.json @@ -0,0 +1,128 @@ +{ + "args": [ + { + "lineno": 31, + "name": "tx" + }, + { + "lineno": 86, + "name": "account" + }, + { + "lineno": 87, + "name": "amount" + }, + { + "lineno": 88, + "name": "amount2" + }, + { + "lineno": 89, + "name": "tradingFee" + }, + { + "lineno": 90, + "name": "sequence" + }, + { + "lineno": 91, + "name": "fee" + }, + { + "lineno": 97, + "name": "tx" + }, + { + "lineno": 102, + "name": "value" + }, + { + "lineno": 113, + "name": "value" + }, + { + "lineno": 124, + "name": "value" + }, + { + "lineno": 134, + "name": "publicKey" + }, + { + "lineno": 134, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "AMMCreate" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& amount, std::decay_t const& amount2, std::decay_t const& tradingFee, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 77, + "name": "AMMCreateBuilder" + } + ], + "description": "Defines the AMMCreate transaction type and its builder for the XRPL protocol, providing type-safe field access and a fluent interface for constructing AMMCreate transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/AMMCreate.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getAmount" + }, + { + "args": [], + "lineno": 54, + "name": "getAmount2" + }, + { + "args": [], + "lineno": 64, + "name": "getTradingFee" + }, + { + "args": [ + "value" + ], + "lineno": 101, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 112, + "name": "setAmount2" + }, + { + "args": [ + "value" + ], + "lineno": 123, + "name": "setTradingFee" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 133, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMCreate.h.ai.md b/include/xrpl/protocol_autogen/transactions/AMMCreate.h.ai.md new file mode 100644 index 0000000000..89810dbaf1 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMCreate.h.ai.md @@ -0,0 +1,39 @@ +# `AMMCreate.h` — Auto-Generated AMM Pool Creation Transaction Wrapper + +## Role in the System + +This file is part of the `protocol_autogen` layer — a code-generated façade over the XRPL core protocol types. Its job is to provide a strongly-typed, ergonomic C++ interface for the `AMMCreate` transaction (`ttAMM_CREATE`, type 35), which creates a new Automated Market Maker (AMM) liquidity pool on the XRP Ledger. Without this layer, callers would interact directly with `STTx` using untyped field tags and raw serialization objects. The auto-generated wrapper ensures field access is statically checked, mandatory fields are present at construction time, and the build-then-seal lifecycle is enforced through the type system. + +The file lives in `xrpl::transactions` alongside dozens of sibling wrappers for every other transaction type on the ledger. All follow the same two-class pattern established here. + +## The Two-Class Design + +`AMMCreate` is an immutable read-only wrapper. It holds a `std::shared_ptr` inherited from `TransactionBase` and provides three typed getters — `getAmount()`, `getAmount2()`, and `getTradingFee()` — for the fields that are specific to this transaction type. The base class `TransactionBase` provides all universal getters: `getAccount()`, `getFee()`, `getSequence()`, `getMemos()`, and so on. Because `STTx` itself is immutable once constructed, there is no need for locking or defensive copying in the read path. + +`AMMCreateBuilder` is the mutable counterpart, inheriting from the CRTP base `TransactionBuilderBase`. It holds a live `STObject` and exposes fluent setters that each return `AMMCreateBuilder&`, enabling method chaining. When the caller has set all desired fields they call `build(publicKey, secretKey)`, which signs the transaction — serializing it with `HashPrefix::txSign` and computing the `TxnSignature` — then constructs an `AMMCreate` from the resulting `STTx`. After `build()` returns, mutation is no longer possible; the builder's `STObject` has been moved into the immutable `STTx`. + +The design consciously avoids a single mutable class. A class that is both a builder and a reader would allow callers to observe partially-constructed state, and would make it impossible to reason about field invariants. By splitting responsibilities, `AMMCreate` can be passed around freely knowing that its three required fields are always present. + +## AMM-Specific Fields + +`AMMCreate` requires exactly three transaction-specific fields: + +- **`sfAmount`** and **`sfAmount2`** — the initial deposit amounts for the two assets that seed the liquidity pool. Both are typed as `SF_AMOUNT::type::value_type`, which in the current codebase covers both `STAmount` (XRP or IOU) and MPT (Multi-Purpose Token) amounts. The `@note` in the doc comments explicitly flags this, since MPT support is a newer addition under the `featureAMM` amendment, and callers need to know that token types beyond the classical XRP/IOU dichotomy are valid. + +- **`sfTradingFee`** — a `uint16` fee rate expressed in basis points, charged on every trade against the pool. It is required at pool creation and cannot be changed without a separate `AMMVote` transaction. + +The constructor for `AMMCreateBuilder` demands all three up front. This is the correct design: there is no valid `AMMCreate` without two assets and a trading fee, so deferring them to optional setters would just push a runtime error to a later, harder-to-diagnose point. + +## Type Validation and Failure Modes + +Both `AMMCreate` and `AMMCreateBuilder` validate the transaction type on construction and throw `std::runtime_error` if the wrapped `STTx` is not a `ttAMM_CREATE`. This guard matters for the second constructor of `AMMCreateBuilder`, which accepts an existing `STTx` to create an editable copy — a pattern used in testing and transaction mutation workflows. Without the guard, a caller could accidentally wrap an unrelated transaction type and silently corrupt fields. + +`TransactionBase::validate()` provides a deeper schema check by running `validateSTObject` against the `TxFormats` template for this transaction type, followed by `passesLocalChecks`. This is not called automatically on construction; it is an explicit post-construction gate. + +## Amendment and Privilege Metadata + +The class doc records that `AMMCreate` requires the `featureAMM` amendment, carries the `Delegation::delegable` flag (meaning a delegated account can submit it on behalf of the primary), and holds the `createPseudoAcct | mayCreateMPT` privileges. These are not enforced in this header — they are enforced by the ledger's transaction processing logic — but documenting them inline on the wrapper class makes it immediately clear what ledger conditions must be met before this transaction type can be submitted. + +## Relationship to the Auto-Generated Layer + +This file is generated, not hand-authored. The comment on line 1 makes that explicit. The practical consequence is that it should never be edited directly; changes to `AMMCreate`'s field set belong in whatever template or schema drives code generation. The file's uniformity with every other sibling in `protocol_autogen/transactions/` — same two-class pattern, same constructor guards, same CRTP base — is a deliberate outcome of generation, making the entire transaction API consistent and predictable. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMDelete.h.ai.json b/include/xrpl/protocol_autogen/transactions/AMMDelete.h.ai.json new file mode 100644 index 0000000000..e289b76754 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMDelete.h.ai.json @@ -0,0 +1,114 @@ +{ + "args": [ + { + "lineno": 25, + "name": "tx" + }, + { + "lineno": 63, + "name": "account" + }, + { + "lineno": 64, + "name": "asset" + }, + { + "lineno": 64, + "name": "asset2" + }, + { + "lineno": 65, + "name": "sequence" + }, + { + "lineno": 66, + "name": "fee" + }, + { + "lineno": 74, + "name": "tx" + }, + { + "lineno": 82, + "name": "value" + }, + { + "lineno": 92, + "name": "value" + }, + { + "lineno": 102, + "name": "publicKey" + }, + { + "lineno": 102, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 23, + "name": "AMMDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& asset, std::decay_t const& asset2, std::optional sequence = std::nullopt, std::optional fee = std::nullopt" + ], + "lineno": 62, + "name": "AMMDeleteBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 73, + "name": "AMMDeleteBuilder" + } + ], + "description": "Defines the AMMDelete transaction type for XRPL, providing an immutable wrapper for type-safe field access and a builder class for constructing and signing AMMDelete transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/AMMDelete.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "AMMDelete::getAsset" + }, + { + "args": [], + "lineno": 48, + "name": "AMMDelete::getAsset2" + }, + { + "args": [ + "value" + ], + "lineno": 81, + "name": "AMMDeleteBuilder::setAsset" + }, + { + "args": [ + "value" + ], + "lineno": 91, + "name": "AMMDeleteBuilder::setAsset2" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 101, + "name": "AMMDeleteBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMDelete.h.ai.md b/include/xrpl/protocol_autogen/transactions/AMMDelete.h.ai.md new file mode 100644 index 0000000000..af1a808c82 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMDelete.h.ai.md @@ -0,0 +1,31 @@ +# `AMMDelete.h` — Auto-generated AMMDelete Transaction Wrapper + +## Role in the System + +`AMMDelete.h` is an auto-generated header in the `xrpl::transactions` namespace that provides two tightly coupled classes for working with the `AMMDelete` on-ledger transaction type (`ttAMM_DELETE`, type code 40). It lives in the `protocol_autogen/transactions/` layer, one of roughly 70 similar per-transaction headers that collectively expose the XRPL transaction set as first-class C++ types rather than loosely typed `STObject` bags. + +The transaction it wraps was introduced by the `featureAMM` amendment and serves a specific lifecycle role: it removes an Automated Market Maker (AMM) instance from the ledger, but *only* when the AMM's pool is already empty — no assets remain and no LP tokens are in circulation. The two required fields, `sfAsset` and `sfAsset2`, identify the asset pair that defined the AMM pool being deleted. The privilege flags `mustDeleteAcct | mayDeleteMPT` indicate that this operation tears down the AMM's pseudo-account and may also delete any related Multi-Purpose Token (MPT) structures. + +## Two-Class Design: Wrapper and Builder + +The file exposes two classes with complementary responsibilities: + +**`AMMDelete`** is a read-only, immutable wrapper around a `std::shared_ptr`. It inherits the full suite of common field getters from `TransactionBase` (account, sequence, fee, flags, signers, etc.) and adds only the two transaction-specific accessors: `getAsset()` and `getAsset2()`, both returning `SF_ISSUE::type::value_type`. The constructor verifies that the wrapped `STTx` actually carries `ttAMM_DELETE` and throws `std::runtime_error` otherwise — a hard invariant that prevents type confusion when routing deserialized transactions. + +**`AMMDeleteBuilder`** is the mutable counterpart, inheriting from `TransactionBuilderBase` via CRTP. This template pattern lets the base class return `Derived&` from every setter, enabling fluent method chaining without any virtual dispatch overhead. The builder accumulates fields into an `STObject object_{sfTransaction}` member and intentionally *avoids* calling `object_.set(soTemplate)`. The base class comment explains the reason: calling `applyTemplate()` on a free `STObject` would create `STBase` placeholders for `soeDEFAULT` fields, and those placeholders later cause `STTx`'s constructor to throw "may not be explicitly set to default." The design trusts `STTx`'s own `applyTemplate()` call to handle unset optional fields correctly at build time. + +The constructor requires `account`, `asset`, and `asset2` up front — matching the protocol specification where both fields carry `soeREQUIRED` — while `sequence` and `fee` are `std::optional` to accommodate scenarios like autofill or ticket-based submission. + +A secondary constructor accepts an existing `std::shared_ptr` and copies the underlying `STObject` into `object_`, enabling round-tripping: deserialize a transaction, wrap it in a builder, modify fields, then re-sign and re-build. + +## Build and Sign Flow + +`build(PublicKey const&, SecretKey const&)` finalises construction. It delegates to `sign()` in the base class, which sets `sfSigningPubKey` from the public key's slice, serialises the `STObject` with `addWithoutSigningFields()` prefixed by `HashPrefix::txSign`, signs the digest, and stores the resulting signature in `sfTxnSignature`. It then wraps the `STObject` in a new `STTx` (taking ownership via move) and passes that `shared_ptr` to `AMMDelete`'s constructor. The transition from builder (mutable `STObject`) to wrapper (immutable `shared_ptr`) is one-way and explicit — once built, the transaction cannot be further mutated without constructing a new builder. + +## MPT Support + +Both `setAsset`/`setAsset2` store values as `STIssue` objects (`STIssue(sfAsset, value)`). The `soeMPTSupported` annotation in the transaction macro (in `transactions.macro`) confirms that both asset fields accept either classic IOU issues or Multi-Purpose Token identifiers. This is relevant because AMM pools on the XRPL can be formed from MPT/MPT or MPT/IOU pairs under the extended AMM feature set, and `AMMDelete` must be capable of referencing either kind. + +## Auto-generation and Maintenance + +The file header declares `// This file is auto-generated. Do not edit.` The canonical source of truth is the `TRANSACTION(ttAMM_DELETE, 40, ...)` macro expansion in `protocol/detail/transactions.macro`, which lists the field schema. Any addition of optional fields to the `AMMDelete` schema would regenerate this file with corresponding `getField()`/`setField()` pairs, keeping the typed wrapper in sync without manual effort. This pattern is consistent across all ~70 transaction types in the directory, making the autogen layer a reliable, uniform boundary between the protocol schema definition and downstream C++ consumers. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMDeposit.h.ai.json b/include/xrpl/protocol_autogen/transactions/AMMDeposit.h.ai.json new file mode 100644 index 0000000000..47a85055d8 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMDeposit.h.ai.json @@ -0,0 +1,213 @@ +{ + "args": [ + { + "lineno": 31, + "name": "tx" + }, + { + "lineno": 175, + "name": "account" + }, + { + "lineno": 175, + "name": "asset" + }, + { + "lineno": 175, + "name": "asset2" + }, + { + "lineno": 175, + "name": "sequence" + }, + { + "lineno": 175, + "name": "fee" + }, + { + "lineno": 185, + "name": "tx" + }, + { + "lineno": 192, + "name": "value" + }, + { + "lineno": 203, + "name": "value" + }, + { + "lineno": 214, + "name": "value" + }, + { + "lineno": 225, + "name": "value" + }, + { + "lineno": 236, + "name": "value" + }, + { + "lineno": 247, + "name": "value" + }, + { + "lineno": 258, + "name": "value" + }, + { + "lineno": 269, + "name": "publicKey" + }, + { + "lineno": 269, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "AMMDeposit" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& asset, std::decay_t const& asset2, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 170, + "name": "AMMDepositBuilder" + } + ], + "description": "Defines the AMMDeposit transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing AMMDeposit transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/AMMDeposit.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getAsset" + }, + { + "args": [], + "lineno": 48, + "name": "getAsset2" + }, + { + "args": [], + "lineno": 58, + "name": "getAmount" + }, + { + "args": [], + "lineno": 70, + "name": "hasAmount" + }, + { + "args": [], + "lineno": 80, + "name": "getAmount2" + }, + { + "args": [], + "lineno": 92, + "name": "hasAmount2" + }, + { + "args": [], + "lineno": 102, + "name": "getEPrice" + }, + { + "args": [], + "lineno": 114, + "name": "hasEPrice" + }, + { + "args": [], + "lineno": 124, + "name": "getLPTokenOut" + }, + { + "args": [], + "lineno": 136, + "name": "hasLPTokenOut" + }, + { + "args": [], + "lineno": 146, + "name": "getTradingFee" + }, + { + "args": [], + "lineno": 158, + "name": "hasTradingFee" + }, + { + "args": [ + "value" + ], + "lineno": 191, + "name": "setAsset" + }, + { + "args": [ + "value" + ], + "lineno": 202, + "name": "setAsset2" + }, + { + "args": [ + "value" + ], + "lineno": 213, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 224, + "name": "setAmount2" + }, + { + "args": [ + "value" + ], + "lineno": 235, + "name": "setEPrice" + }, + { + "args": [ + "value" + ], + "lineno": 246, + "name": "setLPTokenOut" + }, + { + "args": [ + "value" + ], + "lineno": 257, + "name": "setTradingFee" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 268, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMDeposit.h.ai.md b/include/xrpl/protocol_autogen/transactions/AMMDeposit.h.ai.md new file mode 100644 index 0000000000..4b77ce7dc9 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMDeposit.h.ai.md @@ -0,0 +1,45 @@ +# `AMMDeposit.h` — Auto-generated AMM Deposit Transaction Wrapper + +## Role and Context + +This file is part of the `protocol_autogen` layer — a code-generated API that sits on top of XRPL's low-level `STTx`/`STObject` serialization infrastructure and exposes each transaction type as a pair of purpose-built C++ classes. The file is declared auto-generated (`// This file is auto-generated. Do not edit.`) and lives alongside analogous headers for every other XRPL transaction in `include/xrpl/protocol_autogen/transactions/`. + +`AMMDeposit` implements transaction type `ttAMM_DEPOSIT` (integer code 36), introduced by the `featureAMM` amendment. It allows an account to contribute liquidity to an existing Automated Market Maker pool, receiving LP (Liquidity Provider) tokens in return. The transaction is marked *delegable*, meaning it can be submitted on behalf of another account using the `sfDelegate` field inherited from `TransactionBase`. Its sibling `AMMWithdraw.h` mirrors this structure for the reverse operation. + +## Class Structure: Wrapper + Builder + +The file defines exactly two classes in the `xrpl::transactions` namespace: + +**`AMMDeposit`** is an immutable, read-only view of a fully-formed `STTx`. It extends `TransactionBase`, which holds a `std::shared_ptr` in its protected `tx_` member. Once constructed, the underlying transaction cannot be modified through this interface. The type-safety guarantee is enforced in the constructor: if the wrapped `STTx` is not of type `ttAMM_DEPOSIT`, a `std::runtime_error` is thrown immediately. A static `constexpr txType` enables compile-time dispatch when needed. + +**`AMMDepositBuilder`** inherits from `TransactionBuilderBase` via CRTP. This base holds a mutable `STObject object_{sfTransaction}` — notably not initialized from a schema template. The comment inside `TransactionBuilderBase` explains why: pre-applying the template inserts `soeDEFAULT` placeholders that cause `applyTemplate()` to throw when the final `STTx` is constructed. Instead, only explicitly-set fields are added to the object, and the `STTx` constructor's own `applyTemplate()` call handles missing optional fields correctly. + +## AMM Deposit Fields and Deposit Modes + +The AMM protocol supports several deposit modes, controlled by which optional fields are present. The field schema reflects this flexibility: + +- **`sfAsset` / `sfAsset2`** (required, `SF_ISSUE`): Identify the AMM pool by its two constituent token types. These are *issue identifiers* (currency + issuer), not amounts — hence `SF_ISSUE::type::value_type`, not `SF_AMOUNT`. In `setAsset()` and `setAsset2()`, the builder wraps values in `STIssue(sfAsset, value)` before assignment, whereas the amount setters assign directly — a subtle but important difference in how the serialization layer handles typed fields. + +- **`sfAmount` / `sfAmount2`** (optional, `SF_AMOUNT`): The actual token quantities being deposited. Providing both enables a proportional dual-asset deposit. Providing only `sfAmount` triggers a single-asset deposit, which typically incurs a swap fee on the unbalanced side. + +- **`sfLPTokenOut`** (optional, `SF_AMOUNT`): Specifies the exact quantity of LP tokens the depositor wants to receive, letting the protocol back-calculate the required input amounts. This is mutually exclusive with certain other optional fields in practice, though that constraint is enforced by ledger validation logic, not in this header. + +- **`sfEPrice`** (optional, `SF_AMOUNT`): Caps the effective price at which the single-asset deposit executes, serving as a slippage guard similar to a limit price on a DEX order. + +- **`sfTradingFee`** (optional, `SF_UINT16`): Allows the depositor to specify or influence the AMM's trading fee in certain deposit modes. Being a 16-bit integer (basis points), it differs in type from the amount fields — the corresponding getter returns `protocol_autogen::Optional`. + +## Optional Field Handling Pattern + +Every optional field follows the same dual-method pattern: `hasX()` returns `bool` via `STTx::isFieldPresent()`, and `getX()` returns `protocol_autogen::Optional` — calling `hasX()` internally before dereferencing the field. This guards against accessing absent fields, which would throw in the underlying `STTx::at()` call. + +`protocol_autogen::Optional` from `Utils.h` is a type alias that transparently handles reference-type fields: if `T` is a reference type, the optional wraps a `std::reference_wrapper`; otherwise it wraps by value. For `AMMDeposit`'s fields, all returned types are value types, so this resolves to plain `std::optional`. + +## Builder Construction and Signing + +`AMMDepositBuilder` provides two constructors. The primary one takes the required fields — account, `sfAsset`, `sfAsset2` — plus optional sequence and fee, reflecting the transaction's actual required/optional schema. The secondary constructor reconstructs a builder from an existing `STTx` (copying its `STObject`) after type-checking, useful for re-signing or modifying a pre-built transaction. + +All `setX()` methods return `AMMDepositBuilder&`, enabling fluent chaining. The inherited `sign()` method in `TransactionBuilderBase` materializes the signature: it prepends `HashPrefix::txSign` to the serialized fields (excluding signing fields themselves), signs with the provided key pair, and stores both the public key and signature in the object. The terminal `build()` method calls `sign()` then promotes the `STObject` into an `STTx` via move construction, producing an `AMMDeposit` wrapper — at which point the transaction becomes immutable. + +## Design Rationale + +The strict separation between the read-only `AMMDeposit` and the mutable `AMMDepositBuilder` is a deliberate invariant: a signed transaction should never be mutated after the fact, because doing so would invalidate the signature and violate ledger integrity. By making `AMMDeposit` wrap a `std::shared_ptr`, the type system makes post-signing mutation impossible. The CRTP builder pattern allows `TransactionBuilderBase` to provide strongly-typed common setters (account, fee, flags, memos, delegate, etc.) that return the concrete derived type rather than the base, preserving the fluent interface without virtual dispatch overhead. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMVote.h.ai.json b/include/xrpl/protocol_autogen/transactions/AMMVote.h.ai.json new file mode 100644 index 0000000000..5c2dbab4e7 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMVote.h.ai.json @@ -0,0 +1,142 @@ +{ + "args": [ + { + "lineno": 28, + "name": "tx" + }, + { + "lineno": 86, + "name": "account" + }, + { + "lineno": 87, + "name": "asset" + }, + { + "lineno": 88, + "name": "asset2" + }, + { + "lineno": 89, + "name": "tradingFee" + }, + { + "lineno": 90, + "name": "sequence" + }, + { + "lineno": 91, + "name": "fee" + }, + { + "lineno": 144, + "name": "publicKey" + }, + { + "lineno": 144, + "name": "secretKey" + }, + { + "lineno": 111, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "AMMVote" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& asset, std::decay_t const& asset2, std::decay_t const& tradingFee, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 85, + "name": "AMMVoteBuilder" + } + ], + "description": "Defines the AMMVote transaction type for XRPL, including a type-safe wrapper and a builder class for constructing and signing AMMVote transactions with required fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/AMMVote.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "AMMVote" + }, + { + "args": [], + "lineno": 43, + "name": "getAsset" + }, + { + "args": [], + "lineno": 54, + "name": "getAsset2" + }, + { + "args": [], + "lineno": 65, + "name": "getTradingFee" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& asset", + "std::decay_t const& asset2", + "std::decay_t const& tradingFee", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 86, + "name": "AMMVoteBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 98, + "name": "AMMVoteBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 110, + "name": "setAsset" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 121, + "name": "setAsset2" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 132, + "name": "setTradingFee" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 143, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMVote.h.ai.md b/include/xrpl/protocol_autogen/transactions/AMMVote.h.ai.md new file mode 100644 index 0000000000..ef5323b0e6 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMVote.h.ai.md @@ -0,0 +1,41 @@ +# `AMMVote.h` — Auto-Generated Type-Safe Wrapper for AMM Trading Fee Vote Transactions + +## Role and Context + +This header is part of the `xrpl/protocol_autogen/transactions/` layer — a code-generated family of transaction wrappers that provide type-safe, ergonomic access to XRPL's serialized transaction types. The file should never be hand-edited; changes belong at the generator level. + +`AMMVote` wraps the `ttAMM_VOTE` (type 38) transaction, which is the on-chain mechanism by which liquidity providers (LP token holders) participate in governance of an AMM pool's trading fee. The economics are documented in the transactor at `include/xrpl/tx/transactors/dex/AMMVote.h`: a weighted average of all active votes, weighted by each voter's LP token balance, determines the live `TradingFee`. Up to eight vote slots are tracked in the `ltAMM` ledger object's `VoteSlots` array. This file encodes that protocol's transaction structure into C++ types, gated by the `featureAMM` amendment. + +## Two-Class Design: Wrapper and Builder + +The file defines exactly two classes following a pattern replicated across every autogen transaction type: + +**`AMMVote`** extends `TransactionBase` and is an *immutable* read-only wrapper around a `std::shared_ptr`. Its only job is to provide named, strongly-typed field accessors over the opaque `STTx` map. There are no mutation methods — once constructed, the underlying transaction cannot be altered. The constructor performs a type guard by checking `tx_->getTxnType() != txType` via the protected `tx_` member inherited from `TransactionBase`, throwing `std::runtime_error` on mismatch. Because the base class initializer list runs before the constructor body, this check safely accesses the already-stored pointer. + +**`AMMVoteBuilder`** extends `TransactionBuilderBase` using CRTP (Curiously Recurring Template Pattern). The base uses `static_cast(*this)` on every setter to return the concrete subclass type, enabling fluent method chaining without virtual dispatch and without losing the concrete type in the chain. The builder holds a mutable `STObject object_` (initialized in the base as `sfTransaction`) and populates it field by field. + +## Required Fields + +The three transaction-specific required fields (`soeREQUIRED`) are: + +- **`sfAsset`** (`SF_ISSUE`) — identifies the first asset of the AMM pool being voted on. +- **`sfAsset2`** (`SF_ISSUE`) — identifies the second asset. Together, the two asset fields uniquely identify the AMM instance on-ledger, since there is exactly one AMM per ordered asset pair. +- **`sfTradingFee`** (`SF_UINT16`) — the voter's proposed fee, expressed in basis points. + +Both `sfAsset` fields carry a note that they support MPT (Multi-Purpose Token) amounts, reflecting the extension of AMM to handle non-IOU asset types. + +Setter methods on `AMMVoteBuilder` use `std::decay_t` as the parameter type. The `std::decay_t` strips const and reference qualifiers from whatever `SF_ISSUE::type::value_type` resolves to, ensuring the parameter type is a plain value type suitable for `const&` argument passing and copy construction. This is a recurring pattern across all autogen setters to handle field types that might be reference-like aliases. + +Asset fields are assigned as `STIssue(sfField, value)` explicitly rather than by direct operator assignment, because `STObject::operator[]` for an `SF_ISSUE` field expects the inner `STIssue` wrapper type, not the raw `value_type` directly. + +## Builder Construction Paths + +`AMMVoteBuilder` offers two constructors. The primary constructor accepts all required fields — `account`, `asset`, `asset2`, `tradingFee` — plus optional `sequence` and `fee`. This is the normal construction path. The secondary constructor accepts a `std::shared_ptr` directly, copying the existing `STTx` into `object_` via the `STObject` copy constructor (`object_ = *tx`). This path is useful for round-tripping or modifying a previously-deserialized transaction while retaining the builder API. + +## Finalizing with `build()` + +`AMMVoteBuilder::build(publicKey, secretKey)` calls the base class `sign()` method, which sets `sfSigningPubKey`, computes `HashPrefix::txSign + serialized fields (excluding signing fields)`, signs the payload with the provided keys, and stores the signature in `sfTxnSignature`. It then constructs a new `STTx` by moving `object_` and wraps it in an `AMMVote`. After `build()` returns, the builder's internal `object_` is in a moved-from state and should not be reused. + +## Relationship to `TransactionBase` + +`TransactionBase` supplies all common transaction field accessors: `getAccount()`, `getSequence()`, `getFee()`, `getFlags()`, `getLastLedgerSequence()`, `getMemos()`, `getSigners()`, `getDelegate()`, and others. The delegation-aware `sfDelegate` field accessor is present because `AMMVote` is marked `Delegation::delegable`, meaning another account can submit this vote transaction on behalf of the LP token holder. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h.ai.json b/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h.ai.json new file mode 100644 index 0000000000..57344437b5 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h.ai.json @@ -0,0 +1,217 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 151, + "name": "account" + }, + { + "lineno": 151, + "name": "asset" + }, + { + "lineno": 151, + "name": "asset2" + }, + { + "lineno": 151, + "name": "sequence" + }, + { + "lineno": 151, + "name": "fee" + }, + { + "lineno": 162, + "name": "tx" + }, + { + "lineno": 174, + "name": "value" + }, + { + "lineno": 184, + "name": "value" + }, + { + "lineno": 194, + "name": "value" + }, + { + "lineno": 204, + "name": "value" + }, + { + "lineno": 214, + "name": "value" + }, + { + "lineno": 224, + "name": "value" + }, + { + "lineno": 234, + "name": "publicKey" + }, + { + "lineno": 234, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 22, + "name": "AMMWithdraw" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& asset, std::decay_t const& asset2, std::optional sequence, std::optional fee", + "std::shared_ptr tx" + ], + "lineno": 146, + "name": "AMMWithdrawBuilder" + } + ], + "description": "Defines the AMMWithdraw transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing AMMWithdraw transactions with required and optional fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "AMMWithdraw::AMMWithdraw" + }, + { + "args": [], + "lineno": 44, + "name": "AMMWithdraw::getAsset" + }, + { + "args": [], + "lineno": 54, + "name": "AMMWithdraw::getAsset2" + }, + { + "args": [], + "lineno": 64, + "name": "AMMWithdraw::getAmount" + }, + { + "args": [], + "lineno": 75, + "name": "AMMWithdraw::hasAmount" + }, + { + "args": [], + "lineno": 84, + "name": "AMMWithdraw::getAmount2" + }, + { + "args": [], + "lineno": 95, + "name": "AMMWithdraw::hasAmount2" + }, + { + "args": [], + "lineno": 104, + "name": "AMMWithdraw::getEPrice" + }, + { + "args": [], + "lineno": 115, + "name": "AMMWithdraw::hasEPrice" + }, + { + "args": [], + "lineno": 124, + "name": "AMMWithdraw::getLPTokenIn" + }, + { + "args": [], + "lineno": 135, + "name": "AMMWithdraw::hasLPTokenIn" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& asset", + "std::decay_t const& asset2", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 151, + "name": "AMMWithdrawBuilder::AMMWithdrawBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 162, + "name": "AMMWithdrawBuilder::AMMWithdrawBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 174, + "name": "AMMWithdrawBuilder::setAsset" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 184, + "name": "AMMWithdrawBuilder::setAsset2" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 194, + "name": "AMMWithdrawBuilder::setAmount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 204, + "name": "AMMWithdrawBuilder::setAmount2" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 214, + "name": "AMMWithdrawBuilder::setEPrice" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 224, + "name": "AMMWithdrawBuilder::setLPTokenIn" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 234, + "name": "AMMWithdrawBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h.ai.md b/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h.ai.md new file mode 100644 index 0000000000..24062d5f93 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AMMWithdraw.h.ai.md @@ -0,0 +1,56 @@ +# `AMMWithdraw.h` — Auto-generated AMM Withdrawal Transaction Wrapper + +## Role and Context + +This header is part of the `xrpl/protocol_autogen/transactions` layer — a set of auto-generated, type-safe C++ wrappers for every XRPL transaction type. It defines the `AMMWithdraw` class (transaction type `ttAMM_WITHDRAW`, numeric ID 37) and its companion `AMMWithdrawBuilder`, both living in the `xrpl::transactions` namespace. The file implements the AMM (Automated Market Maker) withdrawal operation introduced by the `featureAMM` amendment, which allows liquidity providers to redeem LP tokens for the underlying pool assets. + +The header opens with `// This file is auto-generated. Do not edit.`, which is the decisive architectural choice here: rather than hand-maintaining one large transaction registry, the codebase generates one focused header per transaction type. This keeps each file minimal, makes diffs readable, and allows the generator to enforce uniform structure across all transaction kinds. + +## The Two-Class Pattern + +Every generated transaction header in this directory follows an identical structural pattern: an immutable **read wrapper** (`AMMWithdraw`) paired with a mutable **builder** (`AMMWithdrawBuilder`). This separation enforces a clear lifecycle — once a transaction has been signed and finalized into an `STTx`, it must be consumed only through the read-only wrapper. Mutation is only possible via the builder before signing. + +## `AMMWithdraw` — Immutable Read Wrapper + +`AMMWithdraw` inherits from `TransactionBase`, which provides all common-field accessors (`getAccount()`, `getSequence()`, `getFee()`, `getFlags()`, `getSigners()`, etc.) backed by a `std::shared_ptr`. The derived class adds withdrawal-specific field access. + +The constructor takes a `std::shared_ptr` and immediately guards against misuse with a type check: if the wrapped `STTx` is not of type `ttAMM_WITHDRAW`, a `std::runtime_error` is thrown. This is an important defensive invariant — the `shared_ptr` guarantees immutability at the type level, but the runtime check guarantees identity. Callers that dispatch from a generic `STTx` can safely construct this wrapper and rely on the exception rather than silent misinterpretation. + +The class exposes six field accessors divided into two groups: + +**Required fields** — always present and returned by value: +- `getAsset()` / `getAsset2()` — return `SF_ISSUE::type::value_type`, identifying the two asset sides of the target AMM pool. These fields use `sfAsset` and `sfAsset2` typed as `STIssue`, which can represent either a classic IOU token (currency + issuer) or a Multi-Purpose Token (MPT). The comment `@note This field supports MPT amounts` signals that the AMM implementation has been extended to support the `featureMPT` amendment's token type. + +**Optional fields** — guarded by a `hasXxx()` check and returned as `protocol_autogen::Optional`: +- `getAmount()` / `hasAmount()` — specifies the exact amount of asset 1 to withdraw (single-asset withdrawal mode). +- `getAmount2()` / `hasAmount2()` — specifies the exact amount of asset 2 to withdraw (single-asset or two-asset withdrawal mode). +- `getEPrice()` / `hasEPrice()` — the effective price, used in single-asset withdrawal to constrain the LP token burn rate. +- `getLPTokenIn()` / `hasLPTokenIn()` — the number of LP tokens the account is redeeming. This is the inverse of `sfLPTokenOut` found in `AMMDeposit`: withdrawal *takes in* LP tokens and *produces* pool assets; deposit *takes in* pool assets and *produces* LP tokens. + +The `protocol_autogen::Optional` alias wraps `std::optional`. Each optional getter checks field presence via `tx_->isFieldPresent(sf...)` before calling `tx_->at(...)` — this matters because calling `at()` on a missing field would throw, making the presence-check not just a convenience but a correctness requirement. + +## `AMMWithdrawBuilder` — Fluent Transaction Constructor + +`AMMWithdrawBuilder` inherits from `TransactionBuilderBase` using the Curiously Recurring Template Pattern (CRTP). This gives the builder all common field setters (`setFee()`, `setFlags()`, `setLastLedgerSequence()`, `setDelegate()`, etc.) while making each setter return `Derived&` — i.e., `AMMWithdrawBuilder&` — so callers can chain calls without casting. + +The primary constructor enforces that `sfAsset` and `sfAsset2` are always set, since the AMM pool is identified by this pair. Sequence and fee are optional at construction time and may be set later via the inherited setters — common when the caller obtains sequence or fee asynchronously from the network. + +A secondary constructor accepts a `std::shared_ptr` and copies the underlying `STObject` into `object_`. This exists for round-trip editing: a received or decoded transaction can be loaded into a builder, modified (for example to bump the fee), and re-signed. The type guard (`ttAMM_WITHDRAW`) prevents accidentally loading the wrong transaction into this builder. + +For the required `sfAsset` and `sfAsset2` setters, values are explicitly wrapped in `STIssue(sfAsset, value)` before assignment. This differs from the optional amount setters, which assign `STAmount` values directly. The `STIssue` construction is necessary because the `STObject` operator[] for issue fields requires the correct `STIssue` subtype rather than a raw `Issue` value. + +The `build()` method finalizes construction: it calls the protected `sign()` from `TransactionBuilderBase`, which serializes the `STObject` with `HashPrefix::txSign` prepended, computes the signature, sets `sfSigningPubKey` and `sfTxnSignature` on the object, and then constructs an `STTx` from the moved `STObject`. The result is immediately wrapped in a new `AMMWithdraw` instance and returned. After `build()` is called, `object_` has been moved-from and the builder should not be reused. + +## AMM Withdrawal Modes and the Optional Field Design + +The XRPL AMM supports several withdrawal modes, each determined by which combination of optional fields is present: + +- **LP token redemption** (`sfLPTokenIn` only): redeem a fixed number of LP tokens for a proportional share of both pool assets. +- **Single-asset withdrawal** (`sfAmount` + optionally `sfEPrice`): withdraw a specific amount of one asset, burning however many LP tokens the pool's math requires. `sfEPrice` constrains the maximum effective price to protect against slippage. +- **Two-asset withdrawal** (`sfAmount` + `sfAmount2`): withdraw specific amounts of both assets simultaneously. + +The presence-check API (`hasLPTokenIn()`, `hasAmount()`, etc.) reflects this protocol-level optionality directly in the C++ type system. The caller is responsible for supplying a valid combination; field-level validation against the AMM's transaction template is performed by `TransactionBase::validate()`, which delegates to `validateSTObject()` against the registered `SOTemplate`. + +## Privileges + +The transaction metadata notes `Privileges: mayDeleteAcct | mayAuthorizeMPT`, meaning `AMMWithdraw` is permitted in contexts where account deletion and MPT authorization are allowed. This contrasts with `AMMDeposit`, which carries `noPriv`. The elevated privilege for withdrawal is consistent with the operational expectation that exiting liquidity positions should be permitted even during administrative states where new deposits are restricted. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AccountDelete.h.ai.json b/include/xrpl/protocol_autogen/transactions/AccountDelete.h.ai.json new file mode 100644 index 0000000000..4dcf9b3aa3 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AccountDelete.h.ai.json @@ -0,0 +1,154 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 108, + "name": "account" + }, + { + "lineno": 108, + "name": "destination" + }, + { + "lineno": 108, + "name": "sequence" + }, + { + "lineno": 108, + "name": "fee" + }, + { + "lineno": 117, + "name": "tx" + }, + { + "lineno": 128, + "name": "value" + }, + { + "lineno": 137, + "name": "value" + }, + { + "lineno": 146, + "name": "value" + }, + { + "lineno": 155, + "name": "publicKey" + }, + { + "lineno": 155, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 25, + "name": "AccountDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& destination, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 99, + "name": "AccountDeleteBuilder" + } + ], + "description": "Defines the AccountDelete transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing AccountDelete transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/AccountDelete.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "AccountDelete" + }, + { + "args": [], + "lineno": 44, + "name": "getDestination" + }, + { + "args": [], + "lineno": 54, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 66, + "name": "hasDestinationTag" + }, + { + "args": [], + "lineno": 76, + "name": "getCredentialIDs" + }, + { + "args": [], + "lineno": 88, + "name": "hasCredentialIDs" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& destination", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 108, + "name": "AccountDeleteBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 117, + "name": "AccountDeleteBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 128, + "name": "setDestination" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 137, + "name": "setDestinationTag" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 146, + "name": "setCredentialIDs" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 155, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AccountDelete.h.ai.md b/include/xrpl/protocol_autogen/transactions/AccountDelete.h.ai.md new file mode 100644 index 0000000000..af0f044b9a --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AccountDelete.h.ai.md @@ -0,0 +1,68 @@ +# `AccountDelete.h` — Auto-Generated AccountDelete Transaction Wrapper + +## Role and Context + +This header is part of the `protocol_autogen` subsystem, a code-generated layer that provides a type-safe, ergonomic C++ API over the XRPL wire-protocol transaction types. It lives alongside roughly seventy other per-transaction headers in `include/xrpl/protocol_autogen/transactions/`, all following an identical structural pattern: an immutable reader class paired with a fluent builder class. The comment at line 1 — `// This file is auto-generated. Do not edit.` — signals that the source of truth is a code-generation template, not this file itself; hand-edits would be clobbered on regeneration. + +`AccountDelete` is the XRPL mechanism for permanently closing an account and sending its remaining XRP balance to a specified destination. Because closing an account is irreversible and has strict eligibility requirements (the account must have a low enough sequence number relative to the current ledger), the transaction type carries additional metadata: it is tagged `Delegation::notDelegable` (cannot be submitted on behalf of another account via the delegate mechanism) and requires the `mustDeleteAcct` privilege. Its transaction type constant is `ttACCOUNT_DELETE` (21). + +## Class Structure: Reader/Builder Split + +The file defines two classes with deliberately asymmetric responsibilities. + +### `AccountDelete` — Immutable Wrapper + +`AccountDelete` extends `TransactionBase` and wraps a `std::shared_ptr` — shared ownership over an immutable, already-signed transaction object. `TransactionBase` itself (defined in `TransactionBase.h`) provides accessors for the universal header fields common to every XRPL transaction: `sfAccount`, `sfSequence`, `sfFee`, `sfSigningPubKey`, and optional fields like `sfFlags`, `sfMemos`, `sfSigners`, `sfLastLedgerSequence`, `sfDelegate`, and others. `AccountDelete` adds only the fields specific to its transaction type. + +The constructor is the single enforcement point for type correctness: + +```cpp +explicit AccountDelete(std::shared_ptr tx) + : TransactionBase(std::move(tx)) +{ + if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for AccountDelete"); +} +``` + +Note the subtle bug-prevention here: `tx` is moved into the base class before the type check reads `tx_`. Since `move(tx)` leaves `tx` null, the guard uses `tx_` (the base class member), not the local parameter. This is correct because `TransactionBase` stores it immediately via `tx_(std::move(tx))`. + +The three transaction-specific accessors expose AccountDelete's fields: + +- **`getDestination()`** — returns the required `sfDestination` field as an `AccountID`. No nullopt path, since this field is `soeREQUIRED` and always present in a valid transaction. +- **`getDestinationTag()`** / **`hasDestinationTag()`** — an optional 32-bit tag that routes the payment within the destination account's system. The pattern of separating presence check from value retrieval (`has*` + `get*`) avoids exceptions from accessing absent fields and is consistent across the entire autogen layer. +- **`getCredentialIDs()`** / **`hasCredentialIDs()`** — an optional vector of 256-bit credential identifiers. The `sfCredentialIDs` field supports the XRPL Credentials amendment, allowing the sending account to prove eligibility for deletion via verifiable credentials rather than purely through ledger state checks. + +Return types for optional fields use `protocol_autogen::Optional`, a type alias defined in `Utils.h`: + +```cpp +template +using Optional = std::conditional_t< + std::is_reference_v, + std::optional>>, + std::optional>; +``` + +This alias transparently handles the case where `ValueType` is a reference (wrapping it in `std::reference_wrapper` to satisfy `std::optional`'s non-reference constraint) versus a value type (using `std::optional` directly). In practice, `SF_UINT32::type::value_type` and `SF_VECTOR256::type::value_type` are values, so these resolve to plain `std::optional`. + +All getters are marked `[[nodiscard]]`, making it a compile-time warning to silently discard a return value — a minor defensive measure appropriate for an API layer used in transaction processing code. + +### `AccountDeleteBuilder` — Fluent Mutable Builder + +`AccountDeleteBuilder` extends `TransactionBuilderBase`, a CRTP base class. The CRTP pattern is essential here: the base class setter methods return `Derived&` (not `TransactionBuilderBase&`), which means every call in a chain returns the concrete `AccountDeleteBuilder` type and can be continued with `AccountDeleteBuilder`-specific setters without casts. This is how fluent chaining works across the base and derived layers simultaneously. + +The builder holds a mutable `STObject object_{sfTransaction}` (declared in `TransactionBuilderBase`). The design deliberately avoids calling `object_.set(soTemplate)` during construction, which is explained in a comment in `TransactionBuilderBase`: + +> "This avoids creating STBase placeholders for soeDEFAULT fields, which would cause `applyTemplate()` to throw 'may not be explicitly set to default' when building the `STTx`." + +In other words, the `STObject` is kept in a "free" state until promoted to `STTx` at `build()` time, when the `STTx` constructor calls `applyTemplate()` to validate and fill default fields correctly. + +The primary constructor enforces that `sfDestination` is set immediately — it is a required field and would cause a malformed transaction if omitted. Optional fields `sfDestinationTag` and `sfCredentialIDs` are set through dedicated setters returning `AccountDeleteBuilder&` for chaining. + +The builder can also be constructed from an existing `std::shared_ptr`, copying the existing transaction's data into the mutable `object_` (`object_ = *tx`). This supports use cases like modifying an existing AccountDelete transaction — for example, updating the fee — before re-signing. + +**`build()`** is the terminal operation. It calls the `sign()` method inherited from `TransactionBuilderBase`, which serializes the transaction data with the `HashPrefix::txSign` prefix and signs it with the provided key pair, then constructs a new `STTx` from the (now signed) `STObject` and wraps it in an `AccountDelete` read-only wrapper. After `build()`, the `object_` has been moved away and the builder is effectively consumed. + +## Relationship to the Broader `protocol_autogen` Layer + +`AccountDelete.h` is one of roughly seventy identically-structured headers in the `transactions/` directory, covering every XRPL transaction type from `AMMBid` to `XChainModifyBridge`. The uniformity is deliberate: any code that processes or inspects XRPL transactions can rely on a consistent accessor contract. The base classes `TransactionBase` and `TransactionBuilderBase` centralize common logic (common field getters, the signing procedure, memos/signers handling) so the generated files contain only the minimum per-type differentiation: which fields exist, whether they are required or optional, and their wire types. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AccountSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/AccountSet.h.ai.json new file mode 100644 index 0000000000..d3d507e64e --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AccountSet.h.ai.json @@ -0,0 +1,301 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 292, + "name": "account" + }, + { + "lineno": 292, + "name": "sequence" + }, + { + "lineno": 292, + "name": "fee" + }, + { + "lineno": 301, + "name": "tx" + }, + { + "lineno": 314, + "name": "value" + }, + { + "lineno": 323, + "name": "value" + }, + { + "lineno": 332, + "name": "value" + }, + { + "lineno": 341, + "name": "value" + }, + { + "lineno": 350, + "name": "value" + }, + { + "lineno": 359, + "name": "value" + }, + { + "lineno": 368, + "name": "value" + }, + { + "lineno": 377, + "name": "value" + }, + { + "lineno": 386, + "name": "value" + }, + { + "lineno": 395, + "name": "value" + }, + { + "lineno": 404, + "name": "publicKey" + }, + { + "lineno": 404, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "AccountSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 285, + "name": "AccountSetBuilder" + } + ], + "description": "Defines the AccountSet transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing AccountSet transactions with specific fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/AccountSet.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "AccountSet" + }, + { + "args": [], + "lineno": 44, + "name": "getEmailHash" + }, + { + "args": [], + "lineno": 56, + "name": "hasEmailHash" + }, + { + "args": [], + "lineno": 68, + "name": "getWalletLocator" + }, + { + "args": [], + "lineno": 80, + "name": "hasWalletLocator" + }, + { + "args": [], + "lineno": 92, + "name": "getWalletSize" + }, + { + "args": [], + "lineno": 104, + "name": "hasWalletSize" + }, + { + "args": [], + "lineno": 116, + "name": "getMessageKey" + }, + { + "args": [], + "lineno": 128, + "name": "hasMessageKey" + }, + { + "args": [], + "lineno": 140, + "name": "getDomain" + }, + { + "args": [], + "lineno": 152, + "name": "hasDomain" + }, + { + "args": [], + "lineno": 164, + "name": "getTransferRate" + }, + { + "args": [], + "lineno": 176, + "name": "hasTransferRate" + }, + { + "args": [], + "lineno": 188, + "name": "getSetFlag" + }, + { + "args": [], + "lineno": 200, + "name": "hasSetFlag" + }, + { + "args": [], + "lineno": 212, + "name": "getClearFlag" + }, + { + "args": [], + "lineno": 224, + "name": "hasClearFlag" + }, + { + "args": [], + "lineno": 236, + "name": "getTickSize" + }, + { + "args": [], + "lineno": 248, + "name": "hasTickSize" + }, + { + "args": [], + "lineno": 260, + "name": "getNFTokenMinter" + }, + { + "args": [], + "lineno": 272, + "name": "hasNFTokenMinter" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 292, + "name": "AccountSetBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 301, + "name": "AccountSetBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 314, + "name": "setEmailHash" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 323, + "name": "setWalletLocator" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 332, + "name": "setWalletSize" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 341, + "name": "setMessageKey" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 350, + "name": "setDomain" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 359, + "name": "setTransferRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 368, + "name": "setSetFlag" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 377, + "name": "setClearFlag" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 386, + "name": "setTickSize" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 395, + "name": "setNFTokenMinter" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 404, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/AccountSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/AccountSet.h.ai.md new file mode 100644 index 0000000000..bde51c67f8 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/AccountSet.h.ai.md @@ -0,0 +1,35 @@ +# `AccountSet.h` — Auto-Generated AccountSet Transaction Wrapper + +## Role and Context + +This file is part of the `protocol_autogen` layer — a code-generated tier of the XRPL codebase that sits above the raw `STTx` serialization infrastructure and exposes each transaction type through a typed C++ API. The file lives alongside ~70 other transaction wrappers in `include/xrpl/protocol_autogen/transactions/`, all following the same two-class pattern: an immutable reader and a fluent builder. + +`AccountSet` is one of the oldest and most flexible XRPL transaction types (`ttACCOUNT_SET = 3`). It allows an account holder to modify a collection of account-level properties in a single on-ledger operation: display metadata like `sfDomain` and `sfEmailHash`, behavioral settings like `sfTransferRate` and `sfTickSize`, cryptographic fields like `sfMessageKey`, and account flags controlled through `sfSetFlag`/`sfClearFlag`. No field is required beyond what `TransactionBase` mandates — every AccountSet-specific field is `soeOPTIONAL`. + +## Class `AccountSet` — Immutable Type-Safe Wrapper + +`AccountSet` inherits from `TransactionBase`, which itself wraps a `std::shared_ptr` and provides getters for universal fields like `sfAccount`, `sfSequence`, `sfFee`, `sfMemos`, and `sfDelegate`. The `AccountSet` subclass adds only the transaction-specific optional fields. + +The constructor accepts a `shared_ptr` and immediately validates the transaction type against `ttACCOUNT_SET`, throwing `std::runtime_error` on mismatch. This fail-fast guard prevents a caller from accidentally wrapping the wrong transaction type and getting silent field-access failures later. + +Every AccountSet-specific field is exposed through a paired `get`/`has` interface. The `hasXxx()` method calls `isFieldPresent()` on the underlying `STTx`, and `getXxx()` returns `protocol_autogen::Optional` — a type alias defined in `Utils.h`. That alias resolves to `std::optional>>` when `T` is a reference type, and `std::optional` otherwise. This indirection handles the case where the field type is itself a reference (e.g., blob types returned by reference from the serialized object) without forcing a copy. All getters are marked `[[nodiscard]]` to prevent callers from silently ignoring returned values. + +The ten AccountSet-specific fields span several wire types: `sfEmailHash` (UINT128), `sfWalletLocator` (UINT256), `sfWalletSize` and `sfTransferRate` and `sfSetFlag` and `sfClearFlag` (UINT32), `sfTickSize` (UINT8), `sfMessageKey` and `sfDomain` (variable-length blobs, `SF_VL`), and `sfNFTokenMinter` (ACCOUNT). The variety of types highlights why the autogen layer is valuable — callers get correctly typed values rather than manually casting raw field data out of `STObject`. + +## Class `AccountSetBuilder` — Fluent CRTP Builder + +`AccountSetBuilder` extends `TransactionBuilderBase`, a CRTP template. The curiously recurring template pattern is the key design choice here: `TransactionBuilderBase` holds all the common field setters (`setAccount`, `setFee`, `setSequence`, `setFlags`, `setLastLedgerSequence`, etc.) and each returns `Derived&` — which resolves at compile time to `AccountSetBuilder&`. This means callers can chain AccountSet-specific setters with base-class setters interchangeably without any virtual dispatch or awkward down-casting. + +The builder maintains an `STObject object_{sfTransaction}` (declared in the base class) as its mutable scratch object. Importantly, `TransactionBuilderBase`'s constructor deliberately does not call `object_.set(soTemplate)`. As the comment in that constructor explains, calling `applyTemplate()` on an `STObject` before the `STTx` constructor runs creates `STBase` placeholder entries for `soeDEFAULT` fields, which then causes the `STTx` constructor to throw "may not be explicitly set to default." By keeping `object_` as a free (template-less) STObject and letting the `STTx` constructor call `applyTemplate()` itself, the builder avoids this trap. + +The builder offers two construction paths. The primary path takes an `AccountID`, and optional `sequence` and `fee`, setting up the required fields immediately. The second path accepts an existing `std::shared_ptr` and copies the underlying `STObject` content — useful for modifying a partially-constructed or externally-parsed transaction. This second path also validates the transaction type, throwing on mismatch. + +Setter methods use `std::decay_t const&` as parameter types. The `std::decay_t` strips any reference-ness from the SField's native value type, ensuring the setter always takes a value or const reference cleanly regardless of how the SField typedef is defined internally. + +The `build()` method calls the protected `sign()` helper from `TransactionBuilderBase`, which sets `sfSigningPubKey`, serializes the object (excluding signing fields) with `HashPrefix::txSign` prepended, signs the resulting buffer, stores the signature in `sfTxnSignature`, then constructs and returns an `AccountSet` wrapper around the finalized `STTx`. + +## Design Notes + +`AccountSet` is marked `Delegation::notDelegable`. Despite `TransactionBase` exposing `getDelegate()` (inherited from the universal field layer), this transaction cannot be submitted through the delegation mechanism — the ledger will reject it. This is enforced at the protocol/validation layer, not at the C++ type level, so the wrapper does not prevent setting `sfDelegate` through the builder; the ledger will reject the transaction at apply-time. + +The file header states "This file is auto-generated. Do not edit." The autogen layer is driven from a transaction schema definition that encodes field names, wire types, and optionality. Because all AccountSet-specific fields happen to be optional, the generated `AccountSet` class contains no required-field accessors — a non-obvious consequence of the schema. Validation of which fields actually need to be present for a semantically meaningful AccountSet operation (e.g., providing at least one of `SetFlag`/`ClearFlag`/`Domain`) is handled by the ledger's `doApply` logic, not by this wrapper. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/Batch.h.ai.json b/include/xrpl/protocol_autogen/transactions/Batch.h.ai.json new file mode 100644 index 0000000000..7190d9db81 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/Batch.h.ai.json @@ -0,0 +1,109 @@ +{ + "args": [ + { + "lineno": 34, + "name": "tx" + }, + { + "lineno": 89, + "name": "account" + }, + { + "lineno": 89, + "name": "rawTransactions" + }, + { + "lineno": 89, + "name": "sequence" + }, + { + "lineno": 89, + "name": "fee" + }, + { + "lineno": 98, + "name": "tx" + }, + { + "lineno": 102, + "name": "value" + }, + { + "lineno": 111, + "name": "value" + }, + { + "lineno": 121, + "name": "publicKey" + }, + { + "lineno": 121, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "Batch" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, STArray const& rawTransactions, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 80, + "name": "BatchBuilder" + } + ], + "description": "Defines the Batch transaction type for XRPL, including a type-safe wrapper (Batch) and a builder (BatchBuilder) for constructing and signing Batch transactions with required and optional fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/Batch.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "Batch::getRawTransactions" + }, + { + "args": [], + "lineno": 54, + "name": "Batch::getBatchSigners" + }, + { + "args": [], + "lineno": 66, + "name": "Batch::hasBatchSigners" + }, + { + "args": [ + "value" + ], + "lineno": 101, + "name": "BatchBuilder::setRawTransactions" + }, + { + "args": [ + "value" + ], + "lineno": 110, + "name": "BatchBuilder::setBatchSigners" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 119, + "name": "BatchBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/Batch.h.ai.md b/include/xrpl/protocol_autogen/transactions/Batch.h.ai.md new file mode 100644 index 0000000000..3f31a5216f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/Batch.h.ai.md @@ -0,0 +1,35 @@ +# `include/xrpl/protocol_autogen/transactions/Batch.h` + +## Role and Purpose + +This file is part of the auto-generated typed transaction layer for the XRPL C++ implementation. It provides two classes — `Batch` and `BatchBuilder` — that represent the `ttBATCH` transaction type (type code 71), introduced by the `featureBatch` amendment. The Batch transaction allows an account to submit multiple inner transactions atomically in a single on-ledger operation, a capability unique among XRPL transaction types in that no other transaction type may nest a Batch inside itself. + +Because this file is auto-generated (the first line explicitly warns against manual edits), it exists as the per-transaction-type output of a code generation step that produces one typed wrapper pair for every transaction kind defined in the XRPL protocol. This generation strategy avoids the error-prone alternative of hand-writing dozens of type-erased field accesses spread across the codebase. + +## Class Design: Immutable Wrapper + Separate Builder + +The design follows a two-class pattern shared by every transaction type in `protocol_autogen/transactions/`: + +`Batch` is a read-only view. It holds a `std::shared_ptr` inherited from `TransactionBase`, giving it immutable, reference-counted access to the serialised transaction object. All field getters are `[[nodiscard]] const` and operate directly on the underlying `STTx`. The constructor validates the transaction type immediately and throws `std::runtime_error` on mismatch, so a successfully constructed `Batch` is always a genuine `ttBATCH` transaction. + +`BatchBuilder` is the mutable counterpart. It inherits from the CRTP base `TransactionBuilderBase`, which provides chainable setters for all common transaction fields (`setAccount`, `setFee`, `setSequence`, `setFlags`, etc.) and the `sign()` helper that serialises the transaction body, prefixes it with `HashPrefix::txSign`, and writes the resulting signature into `sfTxnSignature`. The CRTP pattern ensures every inherited setter returns `BatchBuilder&` rather than the base type, preserving fluent chaining without virtual dispatch. The builder stores an `STObject` named `object_` (initialised to `sfTransaction`), and intentionally avoids calling `object_.set(soTemplate)` — this is a deliberate defensive choice noted in `TransactionBuilderBase`: pre-setting template fields would create `soeDEFAULT` placeholders that cause `applyTemplate()` to throw when the `STTx` constructor later processes them. + +The terminal `BatchBuilder::build()` method signs the transaction, wraps `object_` in a `std::make_shared`, and returns a `Batch` instance. Ownership transfers cleanly: the builder's `STObject` is moved into the shared `STTx`, after which the builder is logically consumed. + +## Batch-Specific Fields + +`Batch` exposes two fields beyond the common transaction fields inherited from `TransactionBase`: + +- **`sfRawTransactions`** (`soeREQUIRED`): An `STArray` of inner transactions. Each element is itself a complete serialised transaction object encoded as an `STObject`. The transactor in `Batch.cpp` iterates this array, reconstructs each inner `STTx`, validates it is not itself a `ttBATCH` (nesting is explicitly prohibited), and accumulates base fees. The accessor `getRawTransactions()` returns a `const` reference directly; there is no optional wrapper because the field is required. + +- **`sfBatchSigners`** (`soeOPTIONAL`): An `STArray` carrying additional signers who have authorised the batch. This field is marked `notSigning` in the sfields macro (`SField::notSigning`), which means it is excluded from the transaction signing hash. This is significant: it allows `sfBatchSigners` to be appended after the primary signature is computed, supporting multi-party batch authorisation flows where the original submitter cannot know all batch signers in advance. + +For the optional field, `getBatchSigners()` returns `std::optional>` — the reference wrapper avoids copying the potentially large array while the `optional` correctly models absence. The companion `hasBatchSigners()` predicate avoids the need to dereference and inspect the optional in boolean contexts. + +## Delegation and Amendment Constraints + +The class comment documents `Delegation::notDelegable`, meaning a `Batch` transaction cannot be submitted on behalf of another account via the delegation mechanism. This is architecturally consistent: a Batch wraps multiple inner transactions each of which may have their own delegation rules, so permitting batch-level delegation would create ambiguous authority chains. The `featureBatch` amendment guard means the entire transaction type is unavailable until that amendment activates on the network. + +## Relationship to the Broader Autogen Layer + +Every file in `protocol_autogen/transactions/` follows the identical `Foo` / `FooBuilder` pattern. The common infrastructure in `TransactionBase` and `TransactionBuilderBase` supplies all shared fields and the signing logic, so individual transaction files are thin — typically under 170 lines — and only add the fields unique to their type. The auto-generation approach ensures field names, optionality semantics, and return types remain consistent as the protocol evolves, and the round-trip test in `BatchTests.cpp` validates that every setter maps to a correct getter after `build()` and `validate()`. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CheckCancel.h.ai.json b/include/xrpl/protocol_autogen/transactions/CheckCancel.h.ai.json new file mode 100644 index 0000000000..a397c072c2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CheckCancel.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 61, + "name": "account" + }, + { + "lineno": 62, + "name": "checkID" + }, + { + "lineno": 63, + "name": "sequence" + }, + { + "lineno": 64, + "name": "fee" + }, + { + "lineno": 73, + "name": "tx" + }, + { + "lineno": 82, + "name": "value" + }, + { + "lineno": 92, + "name": "publicKey" + }, + { + "lineno": 92, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "CheckCancel" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& checkID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 56, + "name": "CheckCancelBuilder" + } + ], + "description": "Defines the CheckCancel transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing CheckCancel transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/CheckCancel.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getCheckID" + }, + { + "args": [ + "value" + ], + "lineno": 81, + "name": "setCheckID" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 91, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CheckCancel.h.ai.md b/include/xrpl/protocol_autogen/transactions/CheckCancel.h.ai.md new file mode 100644 index 0000000000..65c73e6fd2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CheckCancel.h.ai.md @@ -0,0 +1,46 @@ +# `CheckCancel.h` — Auto-Generated CheckCancel Transaction Wrapper + +## Role and Context + +This file lives in `include/xrpl/protocol_autogen/transactions/` and is one of a family of auto-generated headers, each encapsulating a single XRPL transaction type. The `// This file is auto-generated. Do not edit.` header makes the generation provenance explicit — the code is produced from a schema description of the protocol rather than written by hand, ensuring consistency across all transaction types. + +`CheckCancel` is part of the XRPL Checks feature (enabled without a named amendment, as indicated by `Amendment: uint256{}`). The feature introduces a three-transaction lifecycle: `CheckCreate` (type 16) establishes a deferred payment authorization on the ledger; `CheckCash` (type 17) lets the designated recipient claim it; and `CheckCancel` (type 18) removes the Check object from the ledger, recovering the owner's reserve. Either the original sender or the destination may submit `CheckCancel`, which is why the field set is minimal — just the ID of the check to delete. + +## Class Structure + +The file defines two classes that cleanly separate read from write concerns. + +### `CheckCancel` — Immutable Read Wrapper + +`CheckCancel` inherits `TransactionBase` and wraps a `std::shared_ptr`. The `const` qualifier on `STTx` propagates immutability through the entire accessor layer. The constructor enforces a hard invariant at construction time: + +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for CheckCancel"); +``` + +This type check happens even though the `STTx` was already typed — the guard ensures that a `CheckCancel` object can never silently wrap the wrong transaction kind. The `[[nodiscard]]` attribute on all getters prevents callers from accidentally discarding results. + +The only transaction-specific accessor is `getCheckID()`, returning the `SF_UINT256` value stored at `sfCheckID`. This field is marked `soeREQUIRED` in the protocol schema, so the field access via `tx_->at(sfCheckID)` is safe without an existence check — `STTx::at()` will throw if the field is absent, but a well-formed ledger transaction will always have it. All common fields — account, sequence, fee, flags, memos, signers, and the optional `sfDelegate` introduced for delegated transactions — are handled by `TransactionBase`. + +### `CheckCancelBuilder` — Fluent Construction via CRTP + +`CheckCancelBuilder` extends `TransactionBuilderBase`, which uses the Curiously Recurring Template Pattern so that every setter in the base returns `Derived&` (the concrete builder type) rather than the abstract base. This makes method chaining work without casts at call sites: + +```cpp +builder.setLastLedgerSequence(n).setCheckID(id).build(pubKey, secKey); +``` + +The base class holds a mutable `STObject object_{sfTransaction}` rather than an `STTx`. The reason is subtle: `STTx`'s constructor calls `applyTemplate()`, which would reject any fields set to their default value. Keeping the intermediate state as a free `STObject` sidesteps this — `applyTemplate()` only runs once, inside the `build()` call when the final `STTx` is constructed. + +There are two builder construction paths. The primary one takes `account` and `checkID` as required arguments (with optional `sequence` and `fee`), immediately populating the `STObject`. The secondary constructor accepts an existing `std::shared_ptr` and copies its contents into `object_`, which enables a round-trip edit pattern: wrap an existing transaction, modify fields, then rebuild and re-sign. Both constructors validate the transaction type and throw `std::runtime_error` on mismatch. + +`build()` calls `sign()` from the base class, which serializes the object with `HashPrefix::txSign` prepended (following RFC-style prefix signing), then attaches the resulting ECDSA/Ed25519 signature and `sfSigningPubKey` before wrapping the finalized `STTx` in a new `CheckCancel`. + +## Design Decisions + +The separation into immutable wrapper plus mutable builder is a deliberate API contract: once a transaction is signed and submitted to the network, nothing should be able to alter it. The `shared_ptr` ownership model also lets ledger processing code share the same transaction object across multiple subsystems without copying. + +The `sfCheckID` field uses `std::decay_t` as the parameter type for `setCheckID()`. `std::decay_t` strips references and cv-qualifiers, so the setter always takes the field's plain value type regardless of how the field descriptor expresses it — a defensive pattern against type aliasing differences across field descriptors in the protocol layer. + +Because this is an auto-generated file, its structure is strictly parallel to `CheckCreate.h` and `CheckCash.h`. Any divergence in pattern (e.g., optional vs. required field handling, CRTP usage, signing approach) would signal a regression in the code generator. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CheckCash.h.ai.json b/include/xrpl/protocol_autogen/transactions/CheckCash.h.ai.json new file mode 100644 index 0000000000..97f4ab49ff --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CheckCash.h.ai.json @@ -0,0 +1,130 @@ +{ + "args": [ + { + "lineno": 30, + "name": "tx" + }, + { + "lineno": 98, + "name": "account" + }, + { + "lineno": 99, + "name": "checkID" + }, + { + "lineno": 100, + "name": "sequence" + }, + { + "lineno": 101, + "name": "fee" + }, + { + "lineno": 109, + "name": "tx" + }, + { + "lineno": 113, + "name": "value" + }, + { + "lineno": 122, + "name": "value" + }, + { + "lineno": 131, + "name": "value" + }, + { + "lineno": 140, + "name": "publicKey" + }, + { + "lineno": 140, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "CheckCash" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& checkID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 93, + "name": "CheckCashBuilder" + } + ], + "description": "Defines the CheckCash transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing CheckCash transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/CheckCash.h", + "functions": [ + { + "args": [], + "lineno": 41, + "name": "CheckCash::getCheckID" + }, + { + "args": [], + "lineno": 51, + "name": "CheckCash::getAmount" + }, + { + "args": [], + "lineno": 62, + "name": "CheckCash::hasAmount" + }, + { + "args": [], + "lineno": 72, + "name": "CheckCash::getDeliverMin" + }, + { + "args": [], + "lineno": 83, + "name": "CheckCash::hasDeliverMin" + }, + { + "args": [ + "value" + ], + "lineno": 112, + "name": "CheckCashBuilder::setCheckID" + }, + { + "args": [ + "value" + ], + "lineno": 121, + "name": "CheckCashBuilder::setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 130, + "name": "CheckCashBuilder::setDeliverMin" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 139, + "name": "CheckCashBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CheckCash.h.ai.md b/include/xrpl/protocol_autogen/transactions/CheckCash.h.ai.md new file mode 100644 index 0000000000..33d43dbb52 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CheckCash.h.ai.md @@ -0,0 +1,63 @@ +# `CheckCash.h` — Auto-Generated CheckCash Transaction Wrapper + +## File Role and Context + +This file is part of the `xrpl/protocol_autogen/transactions/` layer — a collection of auto-generated C++ headers (one per XRPL transaction type) that provide strongly typed wrappers and builders over the ledger's underlying `STTx` serialization format. The file must never be edited by hand; it is regenerated from a schema definition whenever the protocol changes. + +`CheckCash.h` implements transaction type `ttCHECK_CASH` (type code 17), the second step in the XRPL Checks lifecycle. A sender first issues a `CheckCreate` transaction, creating a ledger object that resembles a paper check: it identifies a destination account and a maximum `SendMax` amount. The destination account then submits a `CheckCash` transaction — the subject of this file — to actually pull funds from the sender. The check is deleted from the ledger upon successful cashing. Either party may destroy an uncashed check via `CheckCancel`. + +## Two-Class Design: Wrapper + Builder + +The file declares exactly two classes: `CheckCash` (an immutable read-only view) and `CheckCashBuilder` (a mutable construction aid). This split is deliberate and consistent across every transaction type in the autogen layer. + +### `CheckCash` — the Immutable Wrapper + +`CheckCash` extends `TransactionBase`, which stores a `std::shared_ptr` as its sole data member. Sharing ownership of an immutable `STTx` is cheap: multiple observers can hold the same transaction without copying or locking. The `const`-qualified pointer ensures no code path can modify the underlying bytes after construction. + +The constructor enforces an immediate type invariant: + +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for CheckCash"); +``` + +This prevents a `CheckCreate`'s `STTx` from being accidentally wrapped in a `CheckCash`, a mistake that would otherwise compile and silently misinterpret fields at runtime. The base class does not perform this check — each subclass is responsible for its own type assertion. + +### Transaction-Specific Fields + +`CheckCash` exposes three transaction-specific fields beyond what `TransactionBase` provides: + +**`sfCheckID` (required)**: A 256-bit hash that identifies the Check ledger object to cash. `getCheckID()` returns it directly; there is no `hasCheckID()` because the field is mandatory and `STTx::at()` would throw on absence. + +**`sfAmount` (optional)**: When present, specifies an exact amount the cashing account wishes to receive. The ledger enforces that the sender is debited exactly this value. Both `getAmount()` and `hasAmount()` are provided, following the pattern used throughout the autogen layer for optional fields. + +**`sfDeliverMin` (optional)**: When present, specifies the minimum acceptable delivery amount. Unlike `sfAmount`, the ledger may deliver more than this floor (matching payment path logic). This is useful when the check's `SendMax` could yield variable amounts through order books. + +The protocol requires exactly one of `sfAmount` or `sfDeliverMin` to be present — not both, not neither. That mutual-exclusion rule is enforced at the ledger transaction processing level (not in this auto-generated file, which only provides field access). The `mayCreateMPT` privilege annotation on `CheckCash` (absent on `CheckCreate`) reflects that receiving a cashed check can establish a new MPT balance for the destination account; both amount fields carry `@note This field supports MPT` annotations accordingly. + +All getters are decorated with `[[nodiscard]]`, preventing callers from accidentally discarding return values. `getAmount()` and `getDeliverMin()` return `protocol_autogen::Optional`, where `Optional` is a conditional alias from `Utils.h` — if `T` is a reference type it wraps in `std::reference_wrapper`, otherwise it aliases `std::optional` directly. This guards against dangling-reference bugs when `T` is deduced as a reference. + +### `CheckCashBuilder` — the Fluent Builder + +`CheckCashBuilder` inherits from `TransactionBuilderBase` using CRTP. The template parameter lets `TransactionBuilderBase`'s common setters (account, fee, sequence, flags, memos, delegate, etc.) return `Derived&` — here `CheckCashBuilder&` — enabling uninterrupted method chaining without virtual dispatch or repeated casts in calling code. + +Internally, the builder holds a mutable `STObject object_{sfTransaction}`. The base class constructor deliberately does *not* call `object_.set(soTemplate)`, avoiding the creation of `soeDEFAULT` placeholder fields that would cause `STTx`'s `applyTemplate()` to throw "may not be explicitly set to default". Fields are added on-demand as setters are called. + +The primary constructor: + +```cpp +CheckCashBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& checkID, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt) +``` + +requires `account` and `checkID` (the only mandatory `CheckCash`-specific field) at construction time. `sequence` and `fee` are optional at this stage to support workflows where those fields are determined or auto-filled later by signing infrastructure. + +A secondary constructor accepts an `STTx const` directly, copying its `STObject` into `object_`. This allows round-tripping: deserializing an existing `CheckCash` transaction and modifying it before re-signing. It too performs a type-guard check. + +`build()` calls the protected `sign()` helper from `TransactionBuilderBase`, which serializes `object_` with a `HashPrefix::txSign` prefix (excluding signing fields), signs the result with the supplied `PublicKey`/`SecretKey`, writes back `sfSigningPubKey` and `sfTxnSignature`, then constructs a new `STTx` from the completed `STObject`. It returns a fully validated `CheckCash` wrapper — at which point the transaction is immutable. + +## Relationship to Siblings + +All three Check transaction types (`CheckCreate.h`, `CheckCancel.h`, `CheckCash.h`) share the same structural boilerplate: a `TransactionBase` subclass with `[[nodiscard]]` getters, and a CRTP builder. `CheckCreate` carries `sfDestination` and `sfSendMax` as required fields plus optional `sfExpiration`, `sfDestinationTag`, and `sfInvoiceID`. `CheckCash` references the created check by its ledger object hash (`sfCheckID`) and adds the settlement semantics through `sfAmount`/`sfDeliverMin`. The autogeneration discipline ensures these files remain consistent across the full set of ~70 transaction types in the directory. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CheckCreate.h.ai.json b/include/xrpl/protocol_autogen/transactions/CheckCreate.h.ai.json new file mode 100644 index 0000000000..935dca8786 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CheckCreate.h.ai.json @@ -0,0 +1,151 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 132, + "name": "account" + }, + { + "lineno": 133, + "name": "destination" + }, + { + "lineno": 134, + "name": "sendMax" + }, + { + "lineno": 135, + "name": "sequence" + }, + { + "lineno": 136, + "name": "fee" + }, + { + "lineno": 192, + "name": "publicKey" + }, + { + "lineno": 192, + "name": "secretKey" + }, + { + "lineno": 147, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "CheckCreate" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& destination, std::decay_t const& sendMax, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 126, + "name": "CheckCreateBuilder" + } + ], + "description": "Provides an immutable wrapper and builder for the XRPL CheckCreate transaction, enabling type-safe field access and fluent transaction construction.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/CheckCreate.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getDestination" + }, + { + "args": [], + "lineno": 48, + "name": "getSendMax" + }, + { + "args": [], + "lineno": 59, + "name": "getExpiration" + }, + { + "args": [], + "lineno": 70, + "name": "hasExpiration" + }, + { + "args": [], + "lineno": 81, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 92, + "name": "hasDestinationTag" + }, + { + "args": [], + "lineno": 103, + "name": "getInvoiceID" + }, + { + "args": [], + "lineno": 114, + "name": "hasInvoiceID" + }, + { + "args": [ + "value" + ], + "lineno": 146, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 155, + "name": "setSendMax" + }, + { + "args": [ + "value" + ], + "lineno": 164, + "name": "setExpiration" + }, + { + "args": [ + "value" + ], + "lineno": 173, + "name": "setDestinationTag" + }, + { + "args": [ + "value" + ], + "lineno": 182, + "name": "setInvoiceID" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 191, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CheckCreate.h.ai.md b/include/xrpl/protocol_autogen/transactions/CheckCreate.h.ai.md new file mode 100644 index 0000000000..b93c7831e4 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CheckCreate.h.ai.md @@ -0,0 +1,37 @@ +# `CheckCreate.h` — Auto-Generated CheckCreate Transaction Wrapper + +## Role in the System + +This file lives in `include/xrpl/protocol_autogen/transactions/` and is part of a code-generated layer that exposes every XRPL transaction type as a pair of C++ classes: an immutable read-only wrapper and a fluent builder. The header is explicitly marked `// This file is auto-generated. Do not edit.` — meaning the actual source of truth is a code-generation pipeline, not this file itself. The purpose of this entire layer is to eliminate the stringly-typed, field-name-lookup patterns scattered across the rippled codebase and replace them with compile-time-verified accessors. + +`CheckCreate` represents XRPL transaction type `ttCHECK_CREATE` (numeric type 16). In the XRPL protocol, a `CheckCreate` authorizes a potential payment: the sender writes a "check" specifying a destination and a maximum debit amount (`SendMax`). The check sits as a ledger object until the recipient cashes it via `CheckCash`, or it expires, or either party cancels it with `CheckCancel`. This file lives alongside `CheckCash.h` and `CheckCancel.h` in the same directory, forming the complete check lifecycle. + +## `CheckCreate` — Immutable Read Wrapper + +`CheckCreate` inherits from `TransactionBase`, which itself is a thin wrapper holding a `std::shared_ptr`. The `const` qualifier on `STTx` is the cornerstone of the design: once a `CheckCreate` is constructed, the underlying transaction data is frozen. Every getter is `[[nodiscard]]` and `const`, reinforcing the read-only contract. + +Construction takes a `std::shared_ptr` and immediately validates that `tx_->getTxnType() == ttCHECK_CREATE`, throwing `std::runtime_error` on mismatch. This guard is critical because `STTx` objects circulate throughout the ledger engine as type-erased pointers; the constructor enforces the narrowing. + +The transaction-specific field accessors split cleanly into two patterns driven by the XRPL field optionality model: + +**Required fields** — `getDestination()` and `getSendMax()` — call `tx_->at(sfField)` directly and return a value. There is no nullability; the underlying `STTx` schema validation guarantees these fields are present. + +**Optional fields** — `getExpiration()`, `getDestinationTag()`, and `getInvoiceID()` — each has a corresponding `hasXxx()` predicate that calls `tx_->isFieldPresent(sfField)`. The getter then returns `protocol_autogen::Optional`, a template alias defined in `Utils.h`. The alias resolves to `std::optional` for value types and `std::optional>` for reference types, handling STL's inability to store references directly in `std::optional`. All three optional getters check `hasXxx()` before calling `tx_->at(...)`, returning `std::nullopt` otherwise. + +Notable field semantics: `sfSendMax` is annotated as supporting MPT (Multi-Purpose Token) amounts — it uses `SF_AMOUNT::type::value_type`, which in XRPL can represent XRP drops, IOU amounts, or an MPT amount depending on the ledger feature state. `sfInvoiceID` is a 256-bit hash (`SF_UINT256`) that lets the check sender embed an application-level reference for invoice reconciliation. `sfDestinationTag` is the standard XRPL routing tag that identifies the final recipient when the destination account is a hosted wallet aggregator. + +Common fields (`sfAccount`, `sfSequence`, `sfFee`, `sfMemos`, `sfSigners`, `sfDelegate`, etc.) are inherited from `TransactionBase` and are not repeated here. + +## `CheckCreateBuilder` — Fluent Construction + +`CheckCreateBuilder` uses the Curiously Recurring Template Pattern (CRTP) via `TransactionBuilderBase`. The base class holds a mutable `STObject object_{sfTransaction}` and returns `Derived&` (i.e., `CheckCreateBuilder&`) from every setter, enabling method chaining without slicing. The base intentionally does *not* call `object_.set(soTemplate)` on the `STObject`, sidestepping a subtle trap: calling `applyTemplate()` on a partial object would insert `soeDEFAULT` placeholders that later cause the `STTx` constructor to throw "may not be explicitly set to default". Instead, the `STTx` constructor's own `applyTemplate()` pass handles field validation and defaults when `build()` is called. + +The primary constructor takes the two required fields (`destination` and `sendMax`) alongside the initiating account and optional `sequence`/`fee`. Requiring these at construction time prevents the builder from emitting a structurally invalid transaction — you cannot call `build()` without a destination and maximum send amount. The secondary constructor accepts an existing `std::shared_ptr` and copies its `STObject` into `object_`, enabling round-trip edit workflows where an already-constructed transaction needs modification. + +The `build(PublicKey, SecretKey)` method calls the protected `sign()` helper from the base, which serializes the object with `HashPrefix::txSign` prepended (the XRPL canonical signing prefix), signs the hash with the provided key pair, embeds the `sfSigningPubKey` and `sfTxnSignature` fields, and then wraps the result in a freshly constructed `STTx`. The returned `CheckCreate` wrapper thus carries a fully signed, immutable transaction ready for submission. + +## Design Tradeoffs + +The immutable-wrapper / mutable-builder split is a deliberate separation of concerns: code that inspects or relays transactions (validators, ledger processors, RPC handlers) works exclusively with `CheckCreate`, gaining compile-time guarantees that it cannot accidentally mutate live transaction state. Code that constructs transactions works exclusively with `CheckCreateBuilder`. The cost of this cleanliness is a memory copy at `build()` time — the `STObject` is moved into the `STTx`, which is then heap-allocated and wrapped in a `shared_ptr`. For a transaction that is constructed once and read many times, this is a sound trade. + +The `Delegation::delegable` annotation in the class-level comment indicates that `CheckCreate` participates in the XRPL delegation feature, meaning a delegate account (`sfDelegate`) may submit this transaction type on behalf of another account. The zero-value `Amendment` field (`uint256{}`) means `CheckCreate` requires no feature amendment — it is baseline protocol behavior. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/Clawback.h.ai.json b/include/xrpl/protocol_autogen/transactions/Clawback.h.ai.json new file mode 100644 index 0000000000..62fc771a45 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/Clawback.h.ai.json @@ -0,0 +1,105 @@ +{ + "args": [ + { + "lineno": 34, + "name": "tx" + }, + { + "lineno": 85, + "name": "account" + }, + { + "lineno": 86, + "name": "amount" + }, + { + "lineno": 87, + "name": "sequence" + }, + { + "lineno": 88, + "name": "fee" + }, + { + "lineno": 116, + "name": "publicKey" + }, + { + "lineno": 116, + "name": "secretKey" + }, + { + "lineno": 99, + "name": "value" + }, + { + "lineno": 108, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 32, + "name": "Clawback" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 77, + "name": "ClawbackBuilder" + } + ], + "description": "Defines the Clawback transaction type for XRPL, including an immutable wrapper for type-safe field access and a builder class for constructing Clawback transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/Clawback.h", + "functions": [ + { + "args": [], + "lineno": 41, + "name": "getAmount" + }, + { + "args": [], + "lineno": 52, + "name": "getHolder" + }, + { + "args": [], + "lineno": 63, + "name": "hasHolder" + }, + { + "args": [ + "value" + ], + "lineno": 97, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 106, + "name": "setHolder" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 114, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/Clawback.h.ai.md b/include/xrpl/protocol_autogen/transactions/Clawback.h.ai.md new file mode 100644 index 0000000000..938cc9c3d2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/Clawback.h.ai.md @@ -0,0 +1,53 @@ +# `Clawback.h` — Auto-Generated Clawback Transaction Wrapper + +## Role and Context + +This file is part of the `protocol_autogen` subsystem, a layer of auto-generated, type-safe C++ wrappers over XRPL's raw `STTx` serialization objects. It defines two classes — `Clawback` and `ClawbackBuilder` — representing the `ttCLAWBACK` (type 30) transaction, which allows token issuers to reclaim previously distributed tokens from holder accounts. The transaction is gated behind the `featureClawback` amendment and is marked delegable, meaning it can be authorized to a delegate account via `sfDelegate`. + +The file's header comment explicitly states it is machine-generated and must not be edited by hand. In practice it is one of ~70 transaction-specific files in the same `transactions/` directory, each following the same structural pattern: a read-only wrapper class paired with a corresponding builder. + +## The `Clawback` Read-Only Wrapper + +`Clawback` inherits from `TransactionBase`, which holds an `std::shared_ptr` — a const-qualified, reference-counted pointer to the underlying serialized transaction object. The `const` qualifier is the key design decision: it enforces that once a transaction is created and signed, it is immutable. No mutation is possible through this class. + +Construction validates the transaction type immediately: + +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for Clawback"); +``` + +This guard prevents accidental wrapping of a `Payment` or `TrustSet` in a `Clawback` shell, catching type mismatches at the earliest possible point rather than letting them silently corrupt field reads. + +The class exposes two transaction-specific fields beyond what `TransactionBase` provides: + +**`getAmount()`** returns the `sfAmount` field, which is `soeREQUIRED` — it always exists and the getter never returns an `optional`. Notably, the documentation marks it as supporting MPT (Multi-Purpose Token) amounts, meaning the field can hold either a traditional IOU `STAmount` or an MPT quantity. This is important because `Clawback` was extended to cover MPT when that feature was introduced. + +**`getHolder()` / `hasHolder()`** handle the optional `sfHolder` field, which carries an `AccountID` identifying the account whose tokens are being reclaimed. For IOU clawbacks the holder relationship is implicit in the amount's currency/issuer context, but for MPT clawback an explicit holder account must be named. The pair pattern — a `get` that returns `protocol_autogen::Optional` (an alias for `std::optional`) and a `has` predicate — is the standard idiom used throughout the autogen layer for all optional fields. + +## The `ClawbackBuilder` Class + +`ClawbackBuilder` inherits from `TransactionBuilderBase`, a CRTP base that owns a mutable `STObject object_{sfTransaction}` and provides fluent setters for the universal transaction fields (`sfAccount`, `sfFee`, `sfSequence`, `sfLastLedgerSequence`, `sfMemos`, `sfDelegate`, etc.). Each setter in the base returns `Derived&`, making method chains resolve to `ClawbackBuilder&` without any casts at the call site. + +The constructor accepts `sfAmount` as required and `sfSequence`/`sfFee` as optionals: + +```cpp +ClawbackBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& amount, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt) +``` + +The `std::decay_t` wrapper strips references and cv-qualifiers from `SF_AMOUNT::type::value_type` before rebinding it as a `const&`. This is a defensive pattern to avoid binding a `const reference` to a reference type, which would be ill-formed. + +The base class constructor deliberately avoids calling `object_.set(soTemplate)`. This is explained in an inline comment in `TransactionBuilderBase`: setting the template would insert `STBase` placeholders for `soeDEFAULT` fields, and when the `STTx` constructor later calls `applyTemplate()`, those placeholders would trigger an exception ("may not be explicitly set to default"). By keeping `object_` as a free, unconstrained object during construction, fields are set only when explicitly provided. + +A second constructor accepts an existing `std::shared_ptr` and copies its contents into `object_`, enabling a round-trip workflow where a received transaction can be deserialized back into builder form for modification before re-signing. + +The `build()` method finalizes construction: it delegates to `TransactionBuilderBase::sign()`, which serializes the object with `HashPrefix::txSign` prepended (per the XRPL signing specification) and computes the `sfTxnSignature`, then wraps the result in `std::make_shared` and returns a `Clawback` wrapper. After `build()` is called, the immutability guarantee of `Clawback` applies — there is no way to modify the signed transaction through the returned object. + +## Architectural Position + +`Clawback.h` is purely an interface file; no implementation logic beyond field access and type checking lives here. The actual ledger enforcement of clawback rules — validating that the issuer has the `lsfAllowTrustLineClawback` flag set, that no offer exists between the parties, that the amount is non-zero, etc. — resides in the transaction handler in `src/xrpl/app/tx/impl/Clawback.cpp`. This header concerns itself only with the structural contract of the transaction, not its business rules. + +The file sits at the boundary between raw protocol serialization (`STTx`, `STAmount`, `STObject`) and application-level code that needs to construct or inspect transactions without reaching into the serialization layer directly. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CredentialAccept.h.ai.json b/include/xrpl/protocol_autogen/transactions/CredentialAccept.h.ai.json new file mode 100644 index 0000000000..7896ffd3bc --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CredentialAccept.h.ai.json @@ -0,0 +1,104 @@ +{ + "args": [ + { + "lineno": 33, + "name": "tx" + }, + { + "lineno": 75, + "name": "account" + }, + { + "lineno": 76, + "name": "issuer" + }, + { + "lineno": 77, + "name": "credentialType" + }, + { + "lineno": 78, + "name": "sequence" + }, + { + "lineno": 79, + "name": "fee" + }, + { + "lineno": 109, + "name": "publicKey" + }, + { + "lineno": 109, + "name": "secretKey" + }, + { + "lineno": 89, + "name": "value" + }, + { + "lineno": 99, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 32, + "name": "CredentialAccept" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& issuer, std::decay_t const& credentialType, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 69, + "name": "CredentialAcceptBuilder" + } + ], + "description": "Defines the CredentialAccept transaction type for XRPL, including a type-safe wrapper and a builder class for constructing and signing CredentialAccept transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/CredentialAccept.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getIssuer" + }, + { + "args": [], + "lineno": 54, + "name": "getCredentialType" + }, + { + "args": [ + "value" + ], + "lineno": 87, + "name": "setIssuer" + }, + { + "args": [ + "value" + ], + "lineno": 97, + "name": "setCredentialType" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 107, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CredentialAccept.h.ai.md b/include/xrpl/protocol_autogen/transactions/CredentialAccept.h.ai.md new file mode 100644 index 0000000000..f47c8ffa7e --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CredentialAccept.h.ai.md @@ -0,0 +1,48 @@ +# `CredentialAccept.h` — Auto-generated Transaction Wrapper and Builder + +## Role and Context + +This file is part of the `protocol_autogen` subsystem — a set of auto-generated, strongly-typed C++ wrappers for every XRPL transaction type. It must not be edited by hand; its structure is identical across all transaction headers in `include/xrpl/protocol_autogen/transactions/`. + +`CredentialAccept` represents transaction type `ttCREDENTIAL_ACCEPT` (ordinal 59), one of three transactions belonging to the `featureCredentials` amendment. The credential lifecycle on the ledger follows a two-step issuance model: + +1. **`CredentialCreate` (type 58)** — An issuer account creates a credential record targeting a subject account, optionally including an expiration time and a URI. +2. **`CredentialAccept` (type 59)** — The subject account countersigns by submitting this transaction, acknowledging and activating the credential. +3. **`CredentialDelete` (type 60)** — Either party can remove the credential from the ledger. + +`CredentialAccept` is therefore the subject's half of the handshake. The submitting account (`sfAccount`) is implicitly the subject; the transaction identifies the credential by naming its `sfIssuer` and `sfCredentialType`. + +## Two-Class Design: Wrapper and Builder + +The file defines two classes in `xrpl::transactions`: + +**`CredentialAccept`** is an immutable, read-only view over a `std::shared_ptr`. It inherits the full complement of common field accessors from `TransactionBase` (`getAccount()`, `getFee()`, `getSequence()`, `getFlags()`, `getMemos()`, and others). On top of that base, it exposes two transaction-specific getters: + +- `getIssuer()` — returns an `SF_ACCOUNT::type::value_type` (an `AccountID`) for the `sfIssuer` field, which is `soeREQUIRED`. +- `getCredentialType()` — returns an `SF_VL::type::value_type` (a variable-length `Blob`) for the `sfCredentialType` field, also `soeREQUIRED`. + +Because both fields are required by the protocol schema, neither getter needs an optional return type or a presence check; accessing a missing required field through `STTx::at()` would indicate a malformed transaction that should never have passed validation in the first place. + +The class stores a `static constexpr xrpl::TxType txType = ttCREDENTIAL_ACCEPT` for compile-time type identity, while the constructor enforces the same invariant at runtime: it calls `tx_->getTxnType()` and throws `std::runtime_error` if there is a mismatch. This guards against misuse of the wrapper — for instance, accidentally constructing a `CredentialAccept` from a `CredentialCreate` transaction object. + +**`CredentialAcceptBuilder`** is the mutable counterpart, responsible for assembling a transaction before it is signed and finalized. It inherits from `TransactionBuilderBase`, which uses CRTP to ensure every inherited setter (e.g., `setFee()`, `setFlags()`, `setLastLedgerSequence()`) returns a `CredentialAcceptBuilder&` for clean method chaining without ugly casts in user code. + +## Builder Construction Paths + +`CredentialAcceptBuilder` offers two construction paths: + +1. **From scratch**: The primary constructor accepts `account`, `issuer`, and `credentialType` as required arguments, with `sequence` and `fee` as `std::optional` parameters. It delegates immediately to `TransactionBuilderBase`'s constructor (which sets `sfTransactionType` and `sfAccount`), then calls `setIssuer()` and `setCredentialType()` to populate the credential-specific fields. Keeping `sequence` and `fee` optional is deliberate — test harnesses often want to auto-fill these values rather than specifying them ahead of time. + +2. **From an existing `STTx`**: The second constructor takes a `std::shared_ptr`, validates the transaction type, and copies the entire `STTx` into `object_` via `object_ = *tx`. This path intentionally bypasses the `TransactionBuilderBase` constructor — it uses direct assignment rather than field-by-field setup — because the STTx already contains a fully formed set of fields and copying them individually would risk double-setting or missing subtleties in the internal `STObject` state. + +## Signing and Finalization + +`build(PublicKey const& publicKey, SecretKey const& secretKey)` is the terminal builder method. It delegates to `TransactionBuilderBase::sign()`, which serializes the mutable `STObject object_` with `HashPrefix::txSign`, computes the signature, and writes both `sfSigningPubKey` and `sfTxnSignature` back into the object. It then move-constructs an `STTx` from `object_` and wraps it in a `shared_ptr` before handing it to the `CredentialAccept` constructor. After `build()` is called, the resulting `CredentialAccept` is frozen and immutable. + +## Setter Parameter Types + +Both `setIssuer()` and `setCredentialType()` accept parameters typed as `std::decay_t const&` and `std::decay_t const&` respectively. The `std::decay_t` strips any reference or cv-qualifier from the trait's value type. This is necessary because the getter return types (`SF_VL::type::value_type`) may themselves be references internally, and forming a `const&` to a reference type would collapse incorrectly without decay. Using decayed types keeps the setters safe regardless of how the underlying field type trait resolves. + +## Relationship to Sibling Files + +All files in `include/xrpl/protocol_autogen/transactions/` follow the same structural template. `CredentialCreate.h` and `CredentialDelete.h` are the closest siblings. Comparing them reveals the minimal surface area of `CredentialAccept`: it has no optional fields (unlike `CredentialCreate`, which offers `sfExpiration` and `sfURI`), and its required fields (`sfIssuer`, `sfCredentialType`) are the minimal identifiers needed to locate the specific credential being accepted on the ledger. The transaction is also marked delegable, meaning a `sfDelegate` account can submit it on behalf of the subject using XRPL's delegation mechanism inherited through `TransactionBase`. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CredentialCreate.h.ai.json b/include/xrpl/protocol_autogen/transactions/CredentialCreate.h.ai.json new file mode 100644 index 0000000000..eccd1ec2b4 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CredentialCreate.h.ai.json @@ -0,0 +1,159 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 104, + "name": "account" + }, + { + "lineno": 105, + "name": "subject" + }, + { + "lineno": 106, + "name": "credentialType" + }, + { + "lineno": 107, + "name": "sequence" + }, + { + "lineno": 108, + "name": "fee" + }, + { + "lineno": 165, + "name": "publicKey" + }, + { + "lineno": 165, + "name": "secretKey" + }, + { + "lineno": 129, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 24, + "name": "CredentialCreate" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& subject, std::decay_t const& credentialType, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 98, + "name": "CredentialCreateBuilder" + } + ], + "description": "Defines the CredentialCreate transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing CredentialCreate transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/CredentialCreate.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "CredentialCreate::CredentialCreate" + }, + { + "args": [], + "lineno": 39, + "name": "CredentialCreate::getSubject" + }, + { + "args": [], + "lineno": 48, + "name": "CredentialCreate::getCredentialType" + }, + { + "args": [], + "lineno": 57, + "name": "CredentialCreate::getExpiration" + }, + { + "args": [], + "lineno": 68, + "name": "CredentialCreate::hasExpiration" + }, + { + "args": [], + "lineno": 77, + "name": "CredentialCreate::getURI" + }, + { + "args": [], + "lineno": 88, + "name": "CredentialCreate::hasURI" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& subject", + "std::decay_t const& credentialType", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 104, + "name": "CredentialCreateBuilder::CredentialCreateBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 117, + "name": "CredentialCreateBuilder::CredentialCreateBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 128, + "name": "CredentialCreateBuilder::setSubject" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 137, + "name": "CredentialCreateBuilder::setCredentialType" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 146, + "name": "CredentialCreateBuilder::setExpiration" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 155, + "name": "CredentialCreateBuilder::setURI" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 164, + "name": "CredentialCreateBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CredentialCreate.h.ai.md b/include/xrpl/protocol_autogen/transactions/CredentialCreate.h.ai.md new file mode 100644 index 0000000000..af143d517b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CredentialCreate.h.ai.md @@ -0,0 +1,56 @@ +# `CredentialCreate.h` — Auto-generated Transaction Wrapper and Builder + +## Role in the System + +This header is part of the `protocol_autogen` layer, a code-generated abstraction over the raw `STTx` serialized-transaction format. It lives alongside similar per-transaction-type files — `CredentialAccept.h`, `CredentialDelete.h`, `Payment.h`, and roughly sixty others — all following the same two-class pattern: a read-only typed wrapper and a fluent builder. + +`CredentialCreate` represents transaction type `ttCREDENTIAL_CREATE` (numeric value 58), introduced by the `featureCredentials` amendment. Within the XRP Ledger's Credentials feature, this transaction is how a credential issuer establishes a verifiable claim targeting a specific subject account. The lifecycle is three-part: an issuer submits `CredentialCreate` naming the subject and credential type, the subject responds with `CredentialAccept` (type 59), and either party can revoke via `CredentialDelete` (type 60). The file is explicitly marked `// This file is auto-generated. Do not edit.` — the source of truth is a code generator that reads the protocol's transaction schema and emits typed C++ for every transaction kind. + +## Class Structure + +### `CredentialCreate` — Immutable Wrapper + +`CredentialCreate` inherits `TransactionBase` and wraps a `std::shared_ptr`. The `const` on `STTx` is load-bearing: once a transaction is promoted to this typed wrapper, its content cannot be mutated. The constructor enforces the type invariant immediately by comparing `tx_->getTxnType()` against the class-scoped `txType` constant (`ttCREDENTIAL_CREATE`) and throwing `std::runtime_error` on mismatch. This makes it impossible to accidentally call `getSubject()` on a `Payment` or any other transaction type — misidentification is caught at construction, not silently at runtime. + +Field getters divide cleanly into two groups by the field's `soe` (serialized-object element) class: + +**Required fields** (`soeREQUIRED`) — `getSubject()` and `getCredentialType()` — call `tx_->at(sf...)` directly and return by value. Required fields are always present in a well-formed transaction, so no existence check is needed. + +**Optional fields** (`soeOPTIONAL`) — `getExpiration()` and `getURI()` — return `protocol_autogen::Optional`, which is a type alias defined in `Utils.h`: + +```cpp +template +using Optional = std::conditional_t< + std::is_reference_v, + std::optional>>, + std::optional>; +``` + +The alias exists to handle the edge case where `ValueType` itself is a reference (which `std::optional` cannot hold directly). For the concrete types in this file — `uint32_t` for `sfExpiration` and `Blob` for `sfURI` — neither is a reference, so `Optional` reduces to plain `std::optional`. Each optional getter is paired with a `has*()` predicate (`hasExpiration()`, `hasURI()`) that probes `tx_->isFieldPresent()`. The getter checks the predicate before calling `at()`, avoiding the exception that `STTx::at` would throw for a missing field. All getters are marked `[[nodiscard]]` to prevent silently discarding return values, a common defensive pattern in the autogen layer. + +### Field Schema + +| Field | Type | Required | Description | +|---|---|---|---| +| `sfSubject` | `SF_ACCOUNT` | Yes | The account receiving the credential | +| `sfCredentialType` | `SF_VL` (blob) | Yes | Opaque identifier for the credential type | +| `sfExpiration` | `SF_UINT32` | No | Ledger-time expiration; absent means no expiry | +| `sfURI` | `SF_VL` (blob) | No | External URI pointing to credential metadata | + +### `CredentialCreateBuilder` — Fluent Transaction Constructor + +`CredentialCreateBuilder` inherits the CRTP base `TransactionBuilderBase`. The CRTP ensures that setters inherited from the base (for `sfAccount`, `sfFee`, `sfSequence`, `sfLastLedgerSequence`, delegation fields, etc.) return `CredentialCreateBuilder&`, not a base-class reference, keeping the fluent chain unbroken without any casts at the call site. + +The builder maintains an `STObject object_{sfTransaction}` member (inherited from the base) that accumulates field assignments. Notably, the base constructor avoids calling `object_.set(soTemplate)` — a deliberate choice documented in the base: calling `applyTemplate()` early would create `STBase` placeholders for `soeDEFAULT` fields, which would then cause the `STTx` constructor to throw "may not be explicitly set to default" because those placeholders look like intentional assignments of default values. + +The primary constructor enforces required-field completeness: `sfSubject` and `sfCredentialType` must be provided upfront; `sfExpiration` and `sfURI` are set separately as optional. A secondary constructor accepts a `shared_ptr` and copies the existing serialized object into `object_`, enabling re-signing or field modification of a pre-existing raw transaction. This secondary path validates the transaction type the same way the wrapper does. + +`build(PublicKey, SecretKey)` finalizes the transaction: it calls the protected `sign()` method from the base, which serializes the `STObject` excluding signing fields, prepends `HashPrefix::txSign`, computes the signature with the provided key pair, and sets `sfSigningPubKey` and `sfTxnSignature`. It then constructs the `CredentialCreate` wrapper from `std::move(object_)`, meaning the builder's internal `STObject` is consumed — calling `build()` a second time would produce a wrapper over an empty object. + +## Delegation Support + +The transaction is marked `Delegation::delegable`, which means the common field `sfDelegate` (exposed via `TransactionBase::getDelegate()`) may be set to allow a third-party account to submit the transaction on behalf of the issuer. This is handled entirely by the base layer; `CredentialCreate` itself adds no delegation-specific logic. + +## Relationship to Sibling Files + +`CredentialCreate.h`, `CredentialAccept.h`, and `CredentialDelete.h` form the autogenerated face of the Credentials feature. The transactor logic that validates and applies `CredentialCreate` on the ledger lives in `src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp` and `include/xrpl/tx/transactors/credentials/CredentialCreate.h` — those files operate on raw `STTx` references and contain the actual invariant enforcement (duplicate credential checks, subject account existence, etc.). The autogenerated wrappers here are used in test harnesses and application code that needs to construct or inspect transactions without manipulating the serialized format directly. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CredentialDelete.h.ai.json b/include/xrpl/protocol_autogen/transactions/CredentialDelete.h.ai.json new file mode 100644 index 0000000000..9450efd383 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CredentialDelete.h.ai.json @@ -0,0 +1,130 @@ +{ + "args": [ + { + "lineno": 31, + "name": "tx" + }, + { + "lineno": 108, + "name": "account" + }, + { + "lineno": 109, + "name": "credentialType" + }, + { + "lineno": 110, + "name": "sequence" + }, + { + "lineno": 111, + "name": "fee" + }, + { + "lineno": 117, + "name": "tx" + }, + { + "lineno": 122, + "name": "value" + }, + { + "lineno": 131, + "name": "value" + }, + { + "lineno": 140, + "name": "value" + }, + { + "lineno": 149, + "name": "publicKey" + }, + { + "lineno": 149, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "CredentialDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& credentialType, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 99, + "name": "CredentialDeleteBuilder" + } + ], + "description": "Defines the CredentialDelete transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing CredentialDelete transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/CredentialDelete.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "CredentialDelete::getSubject" + }, + { + "args": [], + "lineno": 55, + "name": "CredentialDelete::hasSubject" + }, + { + "args": [], + "lineno": 66, + "name": "CredentialDelete::getIssuer" + }, + { + "args": [], + "lineno": 77, + "name": "CredentialDelete::hasIssuer" + }, + { + "args": [], + "lineno": 88, + "name": "CredentialDelete::getCredentialType" + }, + { + "args": [ + "value" + ], + "lineno": 120, + "name": "CredentialDeleteBuilder::setSubject" + }, + { + "args": [ + "value" + ], + "lineno": 129, + "name": "CredentialDeleteBuilder::setIssuer" + }, + { + "args": [ + "value" + ], + "lineno": 138, + "name": "CredentialDeleteBuilder::setCredentialType" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 147, + "name": "CredentialDeleteBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/CredentialDelete.h.ai.md b/include/xrpl/protocol_autogen/transactions/CredentialDelete.h.ai.md new file mode 100644 index 0000000000..a573b25559 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/CredentialDelete.h.ai.md @@ -0,0 +1,39 @@ +# `CredentialDelete.h` — Auto-Generated CredentialDelete Transaction Wrapper + +## File Role and Context + +This file is part of the `protocol_autogen` layer, a code-generated collection of type-safe C++ wrappers over XRPL's raw `STTx` serialized transaction objects. Every transaction type in the ledger gets its own generated header following an identical structural pattern: a read-only wrapper class paired with a fluent builder. `CredentialDelete.h` covers transaction type `ttCREDENTIAL_DELETE` (numeric code 60), which is gated behind the `featureCredentials` amendment and is used to remove on-ledger credential objects previously created by `CredentialCreate`. + +The file lives in `include/xrpl/protocol_autogen/transactions/` alongside roughly 70 other per-transaction headers. It should never be edited by hand — its counterpart `CredentialCreate.h` and `CredentialAccept.h` are structurally identical products of the same generator. + +## Two-Class Pattern: Wrapper and Builder + +The header defines two classes inside `namespace xrpl::transactions`: + +**`CredentialDelete`** is an immutable read-only wrapper. It holds a `std::shared_ptr` (inherited from `TransactionBase`) and exposes typed field getters with `[[nodiscard]]`. The constructor accepts a pre-existing `STTx` and immediately validates the transaction type, throwing `std::runtime_error` if the type does not match `ttCREDENTIAL_DELETE`. This fail-fast check prevents accidentally wrapping a payment or offer transaction with the wrong accessor class — a mistake the untyped `STTx::at()` API would silently permit. + +**`CredentialDeleteBuilder`** is the construction entry point. It extends `TransactionBuilderBase` using CRTP so every common setter (`setFee`, `setSequence`, `setLastLedgerSequence`, etc.) returns `CredentialDeleteBuilder&` without needing casts in the caller. The builder accumulates fields into an `STObject object_{sfTransaction}` until `build(publicKey, secretKey)` is called, which invokes `sign()` from the base class (serializing the object with `HashPrefix::txSign`, computing the signature, and embedding it) before constructing a `CredentialDelete` wrapper from the resulting `STTx`. + +A second builder constructor accepts an existing `std::shared_ptr` and copies the raw `STObject` fields into the mutable builder state. This supports a round-trip workflow: deserialize a network transaction, re-wrap it in the builder to modify optional fields, then re-sign and re-submit. + +## Fields and Their Optionality + +`CredentialDelete` has exactly three transaction-specific fields: + +- **`sfCredentialType`** (`soeREQUIRED`, blob) — identifies which credential class to delete. This is the only mandatory field beyond the universal transaction fields (`sfAccount`, `sfSequence`, `sfFee`). Its getter returns `SF_VL::type::value_type` directly, not wrapped in `Optional`. + +- **`sfSubject`** (`soeOPTIONAL`, account) — the account that holds the credential. Optional because either the issuer or the subject can initiate deletion. When the *issuer* sends this transaction, they specify `sfSubject` to target someone else's credential. When the *subject* sends it (deleting their own credential), the account is implicitly `sfAccount` and `sfSubject` is omitted. + +- **`sfIssuer`** (`soeOPTIONAL`, account) — the account that originally issued the credential. Optional for the symmetric reason: when the *subject* sends this transaction, they specify `sfIssuer` to identify which issuer's credential to remove. When the *issuer* sends it, `sfIssuer` is omitted. + +Contrast this with `CredentialCreate`, where `sfSubject` is `soeREQUIRED` — an issuer can only create credentials on behalf of others, so the subject must always be explicitly named. The optionality difference between the two transactions directly encodes the ledger's business rule that deletion is a bilateral privilege. + +## The `protocol_autogen::Optional` Alias + +Optional getters return `protocol_autogen::Optional` rather than a plain `std::optional`. The alias in `Utils.h` resolves to `std::optional>` when `T` is a reference type, and to `std::optional` otherwise. This avoids a subtle C++ pitfall: `std::optional` is ill-formed, so the template conditional ensures reference-typed fields are wrapped through `std::reference_wrapper` while value-typed fields use `std::optional` directly. `AccountID` is a value type, so both `getSubject()` and `getIssuer()` return `std::optional`. + +## Inheritance and Validation + +`TransactionBase` owns the shared pointer and provides all common read accessors (`getAccount()`, `getSequence()`, `getFee()`, `getFlags()`, `getMemos()`, the multi-sign `getSigners()`, and the newer `getDelegate()` for delegated transactions — consistent with the `Delegation::delegable` annotation on this transaction type). The `validate()` method in `TransactionBase` performs schema validation against the `TxFormats` singleton's `SOTemplate`, then runs `passesLocalChecks()` for non-pseudo transactions. + +`TransactionBuilderBase` deliberately avoids calling `object_.set(soTemplate)` during initialization. Doing so would pre-populate `soeDEFAULT` placeholders that later cause `applyTemplate()` to reject "explicitly set to default" fields when the `STTx` constructor runs. Keeping `object_` as a free-form `STObject` and letting the `STTx` constructor call `applyTemplate()` itself is the correct sequencing — a non-obvious invariant that affects every builder in the autogen layer. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/DIDDelete.h.ai.json b/include/xrpl/protocol_autogen/transactions/DIDDelete.h.ai.json new file mode 100644 index 0000000000..e61bb3f7aa --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/DIDDelete.h.ai.json @@ -0,0 +1,92 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 54, + "name": "account" + }, + { + "lineno": 54, + "name": "sequence" + }, + { + "lineno": 54, + "name": "fee" + }, + { + "lineno": 65, + "name": "tx" + }, + { + "lineno": 76, + "name": "publicKey" + }, + { + "lineno": 76, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "DIDDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 51, + "name": "DIDDeleteBuilder" + } + ], + "description": "Defines the DIDDelete transaction type and its builder for the XRPL protocol, providing type-safe access and construction for DIDDelete transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/DIDDelete.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "DIDDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 54, + "name": "DIDDeleteBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 65, + "name": "DIDDeleteBuilder" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 76, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/DIDDelete.h.ai.md b/include/xrpl/protocol_autogen/transactions/DIDDelete.h.ai.md new file mode 100644 index 0000000000..4036bfab72 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/DIDDelete.h.ai.md @@ -0,0 +1,37 @@ +# `DIDDelete.h` — Auto-Generated DIDDelete Transaction Wrapper + +## Role in the System + +`DIDDelete.h` belongs to the `xrpl/protocol_autogen/transactions/` layer — a code-generated tier of the XRPL C++ codebase that provides strongly-typed, transaction-specific wrappers over the underlying `STTx` serialized-type infrastructure. This file defines the machinery for transaction type `ttDID_DELETE` (type code 50), the operation that removes a Decentralized Identifier (DID) object from an XRPL account. DID support is gated behind the `featureDID` amendment. + +The file is stamped `// This file is auto-generated. Do not edit.`, meaning it is produced by a code-generation pipeline rather than handwritten. Its structure mirrors every other file in the same directory — an immutable read accessor class paired with a fluent builder class — making the whole family of transaction types mechanically consistent. + +## Two-Class Pattern: Wrapper and Builder + +The file cleanly separates the two usage modes for a transaction: + +**`DIDDelete`** is a read-only wrapper. It takes ownership of a `std::shared_ptr` and exposes typed accessors via its `TransactionBase` parent. The `const` qualifier on `STTx` is intentional and enforced: once wrapped, the transaction is immutable. The constructor performs an immediate type-check — if the caller somehow wraps an `STTx` of the wrong type, a `std::runtime_error` is thrown right there at construction rather than silently returning garbage from a field lookup later. This is a boundary-enforcement pattern that trades fail-fast behavior for the lack of compile-time guarantees that naturally arise from working with a generic serialized-type store. + +**`DIDDeleteBuilder`** is a mutable, CRTP-based builder inheriting from `TransactionBuilderBase`. It accumulates fields into a live `STObject object_` and, when ready, calls `build()`, which signs the payload and promotes the mutable object into an immutable `DIDDelete` by constructing a new `STTx`. + +This separation is deliberately asymmetric: the builder is mutable and makes no validity guarantees; the wrapper is immutable and trusted to represent a consistent, signed transaction. + +## Why `DIDDelete` Has No Transaction-Specific Fields + +Comparing this file with its sibling `DIDSet.h` (type 49) reveals a meaningful difference. `DIDSet` carries three optional fields — `sfDIDDocument`, `sfURI`, and `sfData` — corresponding to the content of the DID being established or updated. `DIDDelete` carries none. Deletion semantics on the XRPL require only the account identity (from `sfAccount`, inherited from `TransactionBase`) to identify which DID ledger object to remove; no payload data is needed. The generator therefore emits an empty `// Transaction-specific field getters` comment block and an equally empty `/** @brief Transaction-specific field setters */` comment on the builder, reflecting the schema faithfully rather than adding any scaffolding. + +## Builder Construction and Signing Flow + +`DIDDeleteBuilder` offers two construction paths. The primary path accepts an `AccountID`, optional sequence, and optional fee, immediately recording `ttDID_DELETE` as the `sfTransactionType` in the underlying `STObject`. The secondary path accepts an existing `STTx const` and copies it into `object_`, after type-checking, allowing a pre-existing transaction to be "reopened" for re-signing or parameter adjustment. This pattern is useful in test harnesses or situations where a transaction template must be cloned and modified. + +The `build()` method calls `sign()` from `TransactionBuilderBase`, which serializes the `STObject` under `HashPrefix::txSign`, computes the signature, stamps `sfSigningPubKey` and `sfTxnSignature`, then constructs an `STTx` via `std::move(object_)`. After `build()`, the builder's internal `object_` has been moved away, making the builder instance unusable — this is a one-shot design consistent with move semantics, and callers should not reuse the builder after calling `build()`. + +A subtle but important detail in `TransactionBuilderBase`'s constructor: it deliberately does not call `object_.set(soTemplate)`. Calling `applyTemplate()` prematurely would create `STBase` placeholder values for `soeDEFAULT` fields, which in turn causes the `STTx` constructor to throw "may not be explicitly set to default". By leaving `object_` as a free object, the template is applied correctly during `STTx` construction. + +## Delegation and Amendment Context + +The class comment documents `Delegation::delegable`, meaning a `DIDDelete` transaction can be submitted on behalf of an account by a delegate (via `sfDelegate`), which is why `TransactionBase` exposes `getDelegate()` and `hasDelegate()`. This is not specific to `DIDDelete` but is worth noting because not all transaction types are delegable. The `featureDID` amendment context means that attempting to submit this transaction type on a network without the amendment enabled will be rejected at the consensus layer; the C++ layer itself does not enforce amendment activation. + +## Relationship to Siblings + +`DIDDelete.h` and `DIDSet.h` together form the complete DID transaction family on the XRPL. `DIDSet` creates or updates a DID ledger object with document data, URI references, and raw byte payloads; `DIDDelete` tears that object down. The two files share an identical structural skeleton — same base classes, same constructor logic, same builder-to-wrapper `build()` flow — differing only in type code, field presence, and class name. This regularity is the direct consequence of code generation and makes the autogenerated layer predictable to read and easy to extend when new transaction types are added. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/DIDSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/DIDSet.h.ai.json new file mode 100644 index 0000000000..5761756bc5 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/DIDSet.h.ai.json @@ -0,0 +1,131 @@ +{ + "args": [ + { + "lineno": 32, + "name": "tx" + }, + { + "lineno": 122, + "name": "account" + }, + { + "lineno": 122, + "name": "sequence" + }, + { + "lineno": 122, + "name": "fee" + }, + { + "lineno": 132, + "name": "tx" + }, + { + "lineno": 139, + "name": "value" + }, + { + "lineno": 149, + "name": "value" + }, + { + "lineno": 159, + "name": "value" + }, + { + "lineno": 169, + "name": "publicKey" + }, + { + "lineno": 169, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "DIDSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 115, + "name": "DIDSetBuilder" + } + ], + "description": "Defines the DIDSet transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing DIDSet transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/DIDSet.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "DIDSet::getDIDDocument" + }, + { + "args": [], + "lineno": 56, + "name": "DIDSet::hasDIDDocument" + }, + { + "args": [], + "lineno": 68, + "name": "DIDSet::getURI" + }, + { + "args": [], + "lineno": 80, + "name": "DIDSet::hasURI" + }, + { + "args": [], + "lineno": 92, + "name": "DIDSet::getData" + }, + { + "args": [], + "lineno": 104, + "name": "DIDSet::hasData" + }, + { + "args": [ + "value" + ], + "lineno": 137, + "name": "DIDSetBuilder::setDIDDocument" + }, + { + "args": [ + "value" + ], + "lineno": 147, + "name": "DIDSetBuilder::setURI" + }, + { + "args": [ + "value" + ], + "lineno": 157, + "name": "DIDSetBuilder::setData" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 167, + "name": "DIDSetBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/DIDSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/DIDSet.h.ai.md new file mode 100644 index 0000000000..99e3ee1934 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/DIDSet.h.ai.md @@ -0,0 +1,38 @@ +# `DIDSet.h` — Auto-Generated DIDSet Transaction Wrapper and Builder + +This file is part of the `protocol_autogen` layer, a code-generated façade over the XRPL's core serialized transaction types. It provides two classes for the `DIDSet` transaction — `DIDSet` (an immutable, type-safe read wrapper) and `DIDSetBuilder` (a fluent construction interface) — following the same structural pattern used for every transaction type in the `include/xrpl/protocol_autogen/transactions/` directory. + +## Protocol Context + +`DIDSet` maps to transaction type `ttDID_SET` (type code 49), introduced under the `featureDID` amendment. It implements the W3C Decentralized Identifier (DID) standard on the XRP Ledger: an account submits a `DIDSet` transaction to create or update its on-ledger DID object, which can carry a raw DID document, a URI pointing to off-ledger DID metadata, or attestation data. Its counterpart, `DIDDelete` (type 50), removes the DID object entirely. The transaction is marked `delegable`, meaning another account can submit it on behalf of the DID owner when properly authorized, and requires no special privileges beyond a funded account. + +## `DIDSet` — Immutable Wrapper + +`DIDSet` inherits from `TransactionBase`, which wraps a `std::shared_ptr` and exposes read-only accessors for the common fields every transaction carries: `sfAccount`, `sfSequence`, `sfFee`, `sfFlags`, `sfMemos`, `sfSigners`, `sfDelegate`, and so on. `DIDSet` adds only the three DID-specific fields on top. + +Construction is strict: the single-argument constructor takes a `shared_ptr` and immediately verifies that `tx_->getTxnType() == ttDID_SET`, throwing `std::runtime_error` on mismatch. This makes it impossible to accidentally wrap a wrong transaction type — the type-check is enforced at the C++ object boundary rather than relying on callers to inspect the transaction themselves. + +The three transaction-specific fields are all optional (`soeOPTIONAL` in the XRPL schema) and all typed as `SF_VL` — variable-length blobs: + +- `getDIDDocument()` / `hasDIDDocument()` — the raw W3C DID document bytes +- `getURI()` / `hasURI()` — a URI to off-ledger DID data +- `getData()` / `hasData()` — attestation or supplementary data + +Every getter returns `protocol_autogen::Optional`. The `Optional` alias, defined in `Utils.h`, uses `std::conditional_t` to produce either `std::optional` or `std::optional>` depending on whether `T` is a reference type. This indirection matters for `STArray`-backed fields in other transaction types where direct `std::optional` over a reference would be ill-formed; for the blob fields here it simply resolves to `std::optional`. Each getter follows the same two-step pattern: call the `has*()` variant first, then delegate to `tx_->at(sf*)`. All getters are `[[nodiscard]]`, enforcing that callers actually use the returned value. + +Since all three payload fields are optional, a `DIDSet` transaction is valid even when all three are absent — the ledger rules govern what constitutes a semantically meaningful combination, not the C++ type. A transaction that sets only `sfURI`, for instance, partially updates the existing DID object without touching the document bytes. + +## `DIDSetBuilder` — Fluent Construction + +`DIDSetBuilder` inherits from `TransactionBuilderBase`, a CRTP template whose concrete type parameter enables each setter to return `Derived&` (i.e. `DIDSetBuilder&`) for method chaining without virtual dispatch. The base stores a mutable `STObject object_{sfTransaction}` and provides setters for all common fields. One subtle note in the base constructor: it deliberately avoids calling `object_.set(soTemplate)` so that fields left unset are absent rather than present with default sentinel values — the `STTx` constructor's own `applyTemplate()` call handles defaults correctly, but would reject explicitly-set defaults as invalid. + +`DIDSetBuilder` offers two construction paths: + +1. **Fresh construction**: takes `account` (required) plus optional `sequence` and `fee`. The transaction type is stamped into `object_[sfTransactionType]` immediately via the base constructor. +2. **Mutation of existing**: takes a `shared_ptr`, type-checks it, then copies the underlying `STObject` (`object_ = *tx`), allowing selective field updates before rebuilding. + +The three `set*()` methods assign directly into `object_[sf*]`, returning `*this` for chaining. `build()` calls the protected `sign()` method — which serializes the object without signing fields, prepends `HashPrefix::txSign`, signs with the provided key pair, and writes `sfSigningPubKey` and `sfTxnSignature` back into `object_` — then constructs a `DIDSet` from a freshly heap-allocated `STTx`. Ownership of the signed transaction is transferred via `std::move(object_)`, leaving the builder in a moved-from state; `build()` should be called only once. + +## Relationship to the Autogen Framework + +This file is auto-generated and must not be edited manually. The pattern it instantiates is uniform across all ~70 transaction headers in the same directory: a `FooBar` read wrapper plus a `FooBarBuilder` fluent builder, with transaction-specific fields as the only variation between files. The shared logic lives in `TransactionBase` and `TransactionBuilderBase`, keeping the generated code minimal and consistent. The `DIDSet`/`DIDDelete` pair is among the simpler generated files because all DID-specific fields are optional blobs with no nested structures or complex field types. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/DelegateSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/DelegateSet.h.ai.json new file mode 100644 index 0000000000..5036916862 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/DelegateSet.h.ai.json @@ -0,0 +1,104 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 68, + "name": "account" + }, + { + "lineno": 69, + "name": "authorize" + }, + { + "lineno": 70, + "name": "permissions" + }, + { + "lineno": 71, + "name": "sequence" + }, + { + "lineno": 72, + "name": "fee" + }, + { + "lineno": 101, + "name": "publicKey" + }, + { + "lineno": 101, + "name": "secretKey" + }, + { + "lineno": 83, + "name": "value" + }, + { + "lineno": 92, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "DelegateSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& authorize, STArray const& permissions, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 59, + "name": "DelegateSetBuilder" + } + ], + "description": "Defines the DelegateSet transaction type for XRPL, including a type-safe wrapper and a builder class for constructing and signing DelegateSet transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/DelegateSet.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getAuthorize" + }, + { + "args": [], + "lineno": 47, + "name": "getPermissions" + }, + { + "args": [ + "value" + ], + "lineno": 81, + "name": "setAuthorize" + }, + { + "args": [ + "value" + ], + "lineno": 90, + "name": "setPermissions" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 99, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/DelegateSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/DelegateSet.h.ai.md new file mode 100644 index 0000000000..265f523f78 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/DelegateSet.h.ai.md @@ -0,0 +1,30 @@ +# `DelegateSet.h` — Auto-generated Transaction Wrapper for Permission Delegation + +## Role in the System + +`DelegateSet.h` lives in the `protocol_autogen/transactions/` layer of the XRPL codebase, a directory of auto-generated, do-not-edit headers — one per transaction type — that provide strongly-typed C++ interfaces over the raw serialized transaction format. This file specifically covers the `DelegateSet` transaction (`ttDELEGATE_SET`, type 64), introduced under the `featurePermissionDelegationV1_1` amendment. The transaction allows an XRPL account holder to authorize a second account to submit certain transaction types on their behalf, creating a fine-grained permission delegation system. The resulting on-ledger state is stored in a `Delegate` ledger entry (`ltDELEGATE`, 0x0083), defined by the parallel auto-generated file `ledger_entries/Delegate.h`. + +## Two-Class Design: Wrapper + Builder + +Every auto-generated transaction header pairs an immutable read-only view class with a mutable builder class. This separation enforces that already-signed, canonical `STTx` objects can never be mutated after construction, while still offering a convenient fluent API for assembling new ones. + +**`DelegateSet`** extends `TransactionBase` and wraps a `std::shared_ptr`. Its constructor performs an eager type assertion — if the wrapped `STTx` does not carry `ttDELEGATE_SET`, a `std::runtime_error` is thrown immediately. This is the only defense needed at the wrapper layer because all `STTx` deserialization and schema validation are handled upstream by `TransactionBase::validate()`, which delegates to `validateSTObject` and `passesLocalChecks`. Beyond the common fields inherited from `TransactionBase` (account, sequence, fee, flags, memos, signers, network ID, and the new `sfDelegate` field for delegated submission), `DelegateSet` exposes exactly two transaction-specific accessors: + +- `getAuthorize()` — returns the `AccountID` of the account being granted permissions (`sfAuthorize`, required). +- `getPermissions()` — returns a `const STArray&` encoding the set of permitted transaction types (`sfPermissions`, required). Because `sfPermissions` is an opaque structured array in the protocol, the accessor bypasses field templates and calls `getFieldArray()` directly. + +**`DelegateSetBuilder`** extends `TransactionBuilderBase`, which uses CRTP so that all setter methods inherited from the base (sequence, fee, memos, network ID, delegate, etc.) return `DelegateSetBuilder&`, allowing fluent chaining without static casts in calling code. The builder's primary constructor requires all `soeREQUIRED` fields upfront: `account`, `authorize`, `permissions`, plus optional `sequence` and `fee`. This keeps the invariant that a fully-constructed builder always has the minimum viable transaction payload. There is also a secondary constructor accepting an existing `std::shared_ptr` for cases where a transaction should be reconstructed from an already-serialized form — useful in testing or transaction mutation workflows. + +The `build()` method finalizes the transaction by calling the base class `sign()`, which serializes the in-progress `STObject` with `HashPrefix::txSign`, signs the digest with the provided `PublicKey`/`SecretKey` pair, injects `sfSigningPubKey` and `sfTxnSignature` fields, then wraps the `STObject` in an `STTx` and passes it to the `DelegateSet` read wrapper constructor. + +## The `notDelegable` Constraint + +An important semantic property annotated in the class docblock: `DelegateSet` is marked `Delegation::notDelegable`. This means a delegated account cannot itself submit a `DelegateSet` on behalf of the granting account. The constraint closes the obvious privilege-escalation hole where a delegate could extend their own authority or create sub-delegates. Enforcement happens in the `DelegateSet` transactor (defined separately in `include/xrpl/tx/transactors/delegate/DelegateSet.h`), which performs `preflight`, `preclaim`, and `doApply` validation; the transactor also provides a static `deleteDelegate()` helper invoked during `AccountDelete` to clean up `Delegate` ledger entries owned by a departing account. + +## Relationship to `Delegate` Ledger Entry + +The fields `sfAuthorize` and `sfPermissions` that `DelegateSet` writes to the ledger end up mirrored almost exactly in the `Delegate` ledger entry (`ledger_entries/Delegate.h`). Where the transaction has only `sfAuthorize` and `sfPermissions` as required fields, the ledger entry additionally stores `sfAccount` (the granting account), `sfOwnerNode` (for the owner directory), `sfPreviousTxnID`, and `sfPreviousTxnLgrSeq` — the standard bookkeeping fields added by the ledger when committing any `SLE`. This direct structural mapping between the `DelegateSet` transaction type and the `Delegate` object it creates or modifies reflects the general XRPL design principle that each "Set" transaction type owns a corresponding named ledger object. + +## Auto-generation Implications + +The `// This file is auto-generated. Do not edit.` header and the mechanical uniformity of the code — constructor guard, `[[nodiscard]]` accessors, builder pattern with CRTP, `build()` finalizer — are intentional. The `protocol_autogen` directory contains roughly 70 transaction types following the same structure. Hand-coding that layer would introduce inconsistencies and drift from the protocol schema. The generator ensures that adding a new required field to `DelegateSet` propagates atomically to both the getter in `DelegateSet` and the setter and constructor parameter in `DelegateSetBuilder`, with no risk of mismatches. The cost is that the generated code is less idiomatic in places — for example, the constructor parameter list on line 92 is a single long line with inconsistent whitespace, a common artifact of template-driven generation. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/DepositPreauth.h.ai.json b/include/xrpl/protocol_autogen/transactions/DepositPreauth.h.ai.json new file mode 100644 index 0000000000..a0dfc3984c --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/DepositPreauth.h.ai.json @@ -0,0 +1,175 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 132, + "name": "account" + }, + { + "lineno": 132, + "name": "sequence" + }, + { + "lineno": 132, + "name": "fee" + }, + { + "lineno": 140, + "name": "tx" + }, + { + "lineno": 153, + "name": "value" + }, + { + "lineno": 162, + "name": "value" + }, + { + "lineno": 171, + "name": "value" + }, + { + "lineno": 180, + "name": "value" + }, + { + "lineno": 189, + "name": "publicKey" + }, + { + "lineno": 189, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 25, + "name": "DepositPreauth" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 129, + "name": "DepositPreauthBuilder" + } + ], + "description": "Defines the DepositPreauth transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and manipulating DepositPreauth transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/DepositPreauth.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "DepositPreauth" + }, + { + "args": [], + "lineno": 41, + "name": "getAuthorize" + }, + { + "args": [], + "lineno": 52, + "name": "hasAuthorize" + }, + { + "args": [], + "lineno": 62, + "name": "getUnauthorize" + }, + { + "args": [], + "lineno": 73, + "name": "hasUnauthorize" + }, + { + "args": [], + "lineno": 82, + "name": "getAuthorizeCredentials" + }, + { + "args": [], + "lineno": 93, + "name": "hasAuthorizeCredentials" + }, + { + "args": [], + "lineno": 102, + "name": "getUnauthorizeCredentials" + }, + { + "args": [], + "lineno": 113, + "name": "hasUnauthorizeCredentials" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 132, + "name": "DepositPreauthBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 140, + "name": "DepositPreauthBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 153, + "name": "setAuthorize" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 162, + "name": "setUnauthorize" + }, + { + "args": [ + "STArray const& value" + ], + "lineno": 171, + "name": "setAuthorizeCredentials" + }, + { + "args": [ + "STArray const& value" + ], + "lineno": 180, + "name": "setUnauthorizeCredentials" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 189, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/DepositPreauth.h.ai.md b/include/xrpl/protocol_autogen/transactions/DepositPreauth.h.ai.md new file mode 100644 index 0000000000..1476a0fa35 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/DepositPreauth.h.ai.md @@ -0,0 +1,42 @@ +# `DepositPreauth.h` — Auto-generated DepositPreauth Transaction Wrapper + +## Role in the System + +This file is part of the `protocol_autogen` layer — a code-generated family of per-transaction-type headers (roughly 70 in total) that provide C++ type-safe wrappers over XRPL's raw `STTx` serialization format. Each generated header follows an identical structural pattern: a read-only wrapper class and a paired builder class. `DepositPreauth.h` implements this pattern for transaction type `ttDEPOSIT_PREAUTH` (type code 19), which governs deposit pre-authorization on the XRP Ledger. + +The DepositPreauth transaction allows an account that has enabled the `asfDepositAuth` flag to explicitly pre-authorize certain counterparties to send funds to it — bypassing the normal deposit authorization barrier. The transaction can authorize or revoke authorization for either a specific account (`sfAuthorize`/`sfUnauthorize`) or a set of credential-bearing accounts (`sfAuthorizeCredentials`/`sfUnauthorizeCredentials`), the latter reflecting the `Credentials` amendment. All four fields are optional, and a given submission uses exactly one of the four. + +## `DepositPreauth` — Immutable Wrapper + +`DepositPreauth` extends `TransactionBase`, which itself holds a `std::shared_ptr` as its protected `tx_` member. The `const` qualifier on `STTx` and the use of `shared_ptr` are load-bearing: multiple parts of the validation and processing pipeline can hold references to the same underlying transaction without any risk of mutation. The wrapper constructor immediately verifies `tx_->getTxnType() != txType` and throws `std::runtime_error` on mismatch, making it impossible to wrap a mistyped transaction at the C++ layer. + +The four transaction-specific field accessors each follow the same two-method convention: a `has*()` predicate (`isFieldPresent`) and a `get*()` that returns an `Optional` (or `std::nullopt` if absent). The `protocol_autogen::Optional` alias defined in `Utils.h` is subtly important: for reference-typed fields it maps to `std::optional>>`, while for value types it collapses to `std::optional`. This lets the generated code use a single uniform return type regardless of whether the underlying field accessor returns by value or reference. + +The `sfAuthorize` and `sfUnauthorize` accessors use `Optional` — a typed account ID — because those fields hold a single `AccountID`. The `sfAuthorizeCredentials` and `sfUnauthorizeCredentials` accessors take a different path: they return `std::optional>` and call `getFieldArray()` directly. This departure from the `Optional` pattern reflects that `STArray` fields are structured composites (arrays of inner `STObject` entries representing credential type/issuer pairs) rather than scalar values with a compile-time type descriptor. The `[[nodiscard]]` attribute appears on every accessor, pushing callers to handle optional results explicitly rather than silently dropping them. + +`TransactionBase` provides all the common field accessors shared across every transaction type: `getAccount()`, `getSequence()`, `getFee()`, `getFlags()`, `getMemos()`, `getSigners()`, `getDelegate()`, and several others. `DepositPreauth` adds only what is unique to this transaction type. + +## `DepositPreauthBuilder` — Fluent Construction + +`DepositPreauthBuilder` inherits from `TransactionBuilderBase`, applying the Curiously Recurring Template Pattern (CRTP). The base class's setters all return `Derived&` via `static_cast(*this)`, making every method chain type-safe to the concrete builder rather than slicing back to the base. This allows fully-chained construction like: + +```cpp +auto tx = DepositPreauthBuilder(account, seq, fee) + .setLastLedgerSequence(1234) + .setAuthorize(counterpartyId) + .build(pubKey, secKey); +``` + +The builder holds an `STObject object_{sfTransaction}` (defined in `TransactionBuilderBase`) as mutable state. A deliberate design note in the base class comments explains why `object_.set(soTemplate)` is not called during construction: calling it would create `STBase` placeholders for `soeDEFAULT` fields, which would later cause `applyTemplate()` inside the `STTx` constructor to throw "may not be explicitly set to default." The builder deliberately keeps `object_` as a free object and relies on `STTx`'s constructor to apply the template correctly. + +There are two builder constructors. The primary one takes an `account`, optional `sequence`, and optional `fee` — the minimal required fields for any valid transaction. The secondary one accepts an existing `std::shared_ptr` and copies its contents into `object_` via `object_ = *tx`, enabling round-trip editing of a deserialized transaction. + +The `build()` method calls the protected `sign()` from `TransactionBuilderBase`, which serializes the transaction with `HashPrefix::txSign` prepended (as the XRPL signing protocol requires), computes the signature, sets `sfSigningPubKey` and `sfTxnSignature` on `object_`, then moves `object_` into a freshly constructed `STTx` wrapped in a `shared_ptr`. The resulting `STTx` is then handed to the `DepositPreauth` constructor, closing the loop between builder and wrapper. + +## Design Observations + +The split between immutable wrapper and mutable builder is a clean separation of concerns that matches the XRPL ledger's own model: transactions, once signed and submitted, are read-only records. Consumers of validated ledger data use `DepositPreauth`; code constructing new transactions uses `DepositPreauthBuilder`. + +The transaction's `Delegation::delegable` annotation (visible in the class doc comment) means it can appear with an `sfDelegate` field identifying a different account acting on behalf of the signer. This field is accessed through `TransactionBase::getDelegate()`, not through anything in this file, reinforcing that the generated layer only adds transaction-specific fields on top of the common base. + +Because the entire directory is auto-generated, modifying this file directly would be pointless — changes must be made upstream in whatever schema or template drives code generation. The consistent structure across all ~70 sibling files makes the generated layer straightforward for tooling and static analysis to process uniformly. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/EnableAmendment.h.ai.json b/include/xrpl/protocol_autogen/transactions/EnableAmendment.h.ai.json new file mode 100644 index 0000000000..bc08c93c95 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/EnableAmendment.h.ai.json @@ -0,0 +1,100 @@ +{ + "args": [ + { + "lineno": 31, + "name": "tx" + }, + { + "lineno": 69, + "name": "account" + }, + { + "lineno": 70, + "name": "ledgerSequence" + }, + { + "lineno": 71, + "name": "amendment" + }, + { + "lineno": 72, + "name": "sequence" + }, + { + "lineno": 73, + "name": "fee" + }, + { + "lineno": 104, + "name": "publicKey" + }, + { + "lineno": 104, + "name": "secretKey" + }, + { + "lineno": 83, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "EnableAmendment" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& ledgerSequence, std::decay_t const& amendment, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 62, + "name": "EnableAmendmentBuilder" + } + ], + "description": "Defines the EnableAmendment transaction type for the XRPL protocol, providing an immutable wrapper and a builder class for constructing and signing EnableAmendment transactions with type-safe field access.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/EnableAmendment.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getLedgerSequence" + }, + { + "args": [], + "lineno": 48, + "name": "getAmendment" + }, + { + "args": [ + "value" + ], + "lineno": 81, + "name": "setLedgerSequence" + }, + { + "args": [ + "value" + ], + "lineno": 91, + "name": "setAmendment" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 101, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/EnableAmendment.h.ai.md b/include/xrpl/protocol_autogen/transactions/EnableAmendment.h.ai.md new file mode 100644 index 0000000000..3b7de4fd62 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/EnableAmendment.h.ai.md @@ -0,0 +1,36 @@ +# `EnableAmendment.h` — Auto-generated Pseudo-Transaction Wrapper + +## Role in the System + +`EnableAmendment.h` is part of the auto-generated `xrpl::transactions` layer that provides type-safe C++ wrappers over the raw `STTx` serialized transaction format. It covers transaction type `ttAMENDMENT` (numeric code 100), the pseudo-transaction injected by the XRPL ledger's amendment voting machinery when a protocol change achieves validator supermajority and becomes active. + +This is not a transaction that any regular account submits to the network. The file header comment captures this with `Amendment: uint256{}` (the zero hash, meaning this transaction type is not itself guarded by any feature flag) and `Delegation::notDelegable`. The pseudo-transaction is inserted into a validated ledger by the consensus engine, which records which amendment was enabled (`sfAmendment`) and at which ledger (`sfLedgerSequence`). + +The `// This file is auto-generated. Do not edit.` banner on line 1 signals that the real source of truth is the `TRANSACTION(ttAMENDMENT, ...)` macro entry in `include/xrpl/protocol/detail/transactions.macro`. That macro expands through code generation tooling to produce this header; changes to the transaction's field list must go through the macro, not this file. + +## Class Design: Wrapper + Builder Pair + +The file exports two classes that follow the same paired pattern as every other transaction in the `protocol_autogen` directory: + +**`EnableAmendment`** is an immutable read-only wrapper around a `shared_ptr`. It inherits all common transaction field accessors (`getAccount()`, `getFee()`, `getSequence()`, etc.) from `TransactionBase`, then adds two transaction-specific getters: + +- `getLedgerSequence()` → `SF_UINT32::type::value_type`: returns `sfLedgerSequence`, the ledger index at which the amendment became effective. +- `getAmendment()` → `SF_UINT256::type::value_type`: returns `sfAmendment`, the 256-bit hash identifying the amendment, which corresponds to the SHA-512Half of the amendment's feature string. + +Both fields are `soeREQUIRED` per the macro definition, so `tx_->at(field)` is safe — there is no optional-return variant here. The constructor validates the transaction type at runtime with `tx_->getTxnType() != txType` and throws `std::runtime_error` on mismatch. This is a deliberate fail-fast approach: wrapping the wrong `STTx` in a typed view is a programming error, not a recoverable condition. + +**`EnableAmendmentBuilder`** inherits from `TransactionBuilderBase` via CRTP. The template enables the base class's fluent setters (e.g., `setFee()`, `setNetworkID()`) to return `Derived&` — that is, `EnableAmendmentBuilder&` — so calls chain without casts. The constructor accepts `account`, `ledgerSequence`, and `amendment` as required positional arguments, plus optional `sequence` and `fee`. Internally it builds on a mutable `STObject object_{sfTransaction}` rather than an `STTx`, deliberately deferring template application so that `soeDEFAULT` fields never get explicit placeholder entries that would cause `applyTemplate()` to throw "may not be explicitly set to default" during `STTx` construction — an important subtlety documented in `TransactionBuilderBase`. + +The second builder constructor accepting a `shared_ptr` allows round-tripping: load an existing serialized pseudo-transaction, copy its fields into the mutable `STObject`, modify if needed, and rebuild. This path also guards the type with an explicit check. + +`build()` calls the inherited `sign()` method, which serializes `HashPrefix::txSign || object_` without signing fields, computes an `secp256k1` signature, and sets both `sfSigningPubKey` and `sfTxnSignature`. For real pseudo-transactions produced by the consensus engine, these fields are empty (zero-length blobs) — the validate path in `TransactionBase::validate()` handles this correctly by detecting pseudo-transactions via `isPseudoTx(*tx_)` and returning `true` early, bypassing `passesLocalChecks()` which would otherwise reject an unsigned transaction. + +## Validation Shortcircuit for Pseudo-Transactions + +The `Change.cpp` transactor that processes `ttAMENDMENT` (and `ttFEE`) enforces unusually strict invariants: the source account must be `beast::zero`, the fee must be zero XRP, and the transaction must have no signature at all. These are the exact opposite of normal transaction requirements. The autogen layer's `validate()` correctly skips `passesLocalChecks()` for these transactions — the check `if (isPseudoTx(*tx_)) return true;` in `TransactionBase` is what makes the test suite's `EXPECT_TRUE(tx.validate(reason))` pass even when testing with a signing key pair. + +## Relationship to Other Files + +`EnableAmendment.h` sits in a directory of roughly 70 sibling auto-generated files — one per transaction type. All share the same inheritance hierarchy (`TransactionBase` / `TransactionBuilderBase`) and the same construction discipline. The `transactions.macro` file is the single authoritative definition: the field names, their optionality (`soeREQUIRED`/`soeOPTIONAL`/`soeDEFAULT`), and the delegation policy all flow from that one `TRANSACTION(...)` macro call into every generated artifact. + +The corresponding test file `EnableAmendmentTests.cpp` validates the full round-trip: build from scratch, verify fields; reconstruct from `STTx`, rebuild, verify fields again; confirm that constructing either class from the wrong transaction type throws. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/EscrowCancel.h.ai.json b/include/xrpl/protocol_autogen/transactions/EscrowCancel.h.ai.json new file mode 100644 index 0000000000..2819df2da3 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/EscrowCancel.h.ai.json @@ -0,0 +1,125 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 74, + "name": "account" + }, + { + "lineno": 75, + "name": "owner" + }, + { + "lineno": 76, + "name": "offerSequence" + }, + { + "lineno": 77, + "name": "sequence" + }, + { + "lineno": 78, + "name": "fee" + }, + { + "lineno": 116, + "name": "publicKey" + }, + { + "lineno": 116, + "name": "secretKey" + }, + { + "lineno": 98, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "EscrowCancel" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& owner, std::decay_t const& offerSequence, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 69, + "name": "EscrowCancelBuilder" + } + ], + "description": "Provides an immutable wrapper and builder for the EscrowCancel transaction type in the XRPL protocol, enabling type-safe field access and fluent transaction construction.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/EscrowCancel.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "EscrowCancel" + }, + { + "args": [], + "lineno": 44, + "name": "getOwner" + }, + { + "args": [], + "lineno": 53, + "name": "getOfferSequence" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& owner", + "std::decay_t const& offerSequence", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 74, + "name": "EscrowCancelBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 86, + "name": "EscrowCancelBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 97, + "name": "setOwner" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 106, + "name": "setOfferSequence" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 115, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/EscrowCancel.h.ai.md b/include/xrpl/protocol_autogen/transactions/EscrowCancel.h.ai.md new file mode 100644 index 0000000000..e15e87db53 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/EscrowCancel.h.ai.md @@ -0,0 +1,50 @@ +# `EscrowCancel.h` — Auto-Generated EscrowCancel Transaction Wrapper and Builder + +## Role in the System + +This file is part of the `xrpl/protocol_autogen/transactions/` layer — a code-generated set of per-transaction-type headers that give C++ consumers a strongly-typed, compile-time-checked interface over the XRPL `STTx` serialization format. The `EscrowCancel` transaction (`ttESCROW_CANCEL`, type 4) allows any account to reclaim XRP locked in an escrow object after that escrow's expiry condition is met. The two fields it requires beyond the common transaction fields — `sfOwner` and `sfOfferSequence` — uniquely identify the escrow: the account that originally created it and the sequence number of that `EscrowCreate` transaction. + +The file is marked `// This file is auto-generated. Do not edit.`, meaning the source of truth is a transaction schema definition file elsewhere in the build system, and this header is regenerated when the schema changes. Manual edits would be overwritten. + +## Class Structure: Wrapper/Builder Pair + +The file defines two classes that work as a pair. This pattern appears consistently across every transaction type in `protocol_autogen/transactions/` (e.g., `EscrowCreate.h`, `EscrowFinish.h`). + +**`EscrowCancel`** is an immutable read-side wrapper. It extends `TransactionBase`, which holds a `std::shared_ptr` as `tx_` and provides accessors for all fields common to every transaction (account, sequence, fee, flags, memos, signers, etc.). `EscrowCancel` adds only the two domain-specific getters: + +- `getOwner()` — returns the `AccountID` of the escrow's original creator via `sfOwner` +- `getOfferSequence()` — returns the `uint32_t` sequence number of the `EscrowCreate` via `sfOfferSequence` + +Both are annotated `[[nodiscard]]` and declared `const`, which is consistent with the immutability contract: once an `EscrowCancel` is constructed, its state cannot change. + +The constructor validates the transaction type immediately, throwing `std::runtime_error` if the wrapped `STTx` is not `ttESCROW_CANCEL`. This is a defensive guard against misuse when the object is constructed from a raw `shared_ptr` obtained from an external source (e.g., deserialization, ledger replay), where the type cannot be verified at compile time. + +**`EscrowCancelBuilder`** is the mutable write-side counterpart. It uses CRTP via `TransactionBuilderBase`, which stores a mutable `STObject object_{sfTransaction}` and provides fluent setters for all common fields (`setSequence()`, `setFee()`, `setFlags()`, `setLastLedgerSequence()`, `setDelegate()`, etc.), all returning `Derived&` so calls can be chained. + +The builder requires `owner` and `offerSequence` as mandatory constructor arguments (alongside `account`) because these are `soeREQUIRED` fields in the XRPL transaction schema — the transaction is invalid without them. Sequence and fee are `std::optional`, reflecting the common case where they are auto-filled by a library or server rather than specified by the caller. + +A second constructor overload accepts an existing `std::shared_ptr`, allowing a transaction to be loaded from the ledger and then modified (e.g., to re-sign with different keys). It performs the same type guard as the wrapper constructor. + +## The `build()` Method and Signing Flow + +`build(PublicKey const& publicKey, SecretKey const& secretKey)` finalises the transaction by: + +1. Calling the protected `sign()` method from `TransactionBuilderBase`, which sets `sfSigningPubKey`, serializes the object prefixed with `HashPrefix::txSign`, and stores the resulting `sfTxnSignature`. +2. Moving `object_` into a new `STTx`, then wrapping it in `EscrowCancel`. The move into `STTx` triggers `applyTemplate()`, which validates field presence against the registered `TxFormats` schema. + +Signing happens in the builder before constructing the `STTx`, specifically because `STTx` is immutable once created — you cannot amend a signature after the fact. The flow enforces that the final `EscrowCancel` wrapper is always in a signed, schema-validated state. + +## Key Design Decisions + +**Why not use `std::decay_t` in getter return types?** The getters return `SF_ACCOUNT::type::value_type` and `SF_UINT32::type::value_type` directly. These are the underlying primitive types (`AccountID` and `uint32_t` respectively). Returning by value from `tx_->at(...)` is safe because `STTx` is `const` and the accessor performs a copy. + +**Why `std::decay_t` in setter parameter types?** The builder's `setOwner` and `setOfferSequence` accept `std::decay_t const&`. The `std::decay_t` strips any reference or cv-qualifiers from the field's value type, ensuring the function signature binds to a plain const reference regardless of whether the underlying type is itself a reference type. This is defensive template hygiene for generated code. + +**Why separate wrapper and builder?** The split enforces the XRPL immutability contract at the type level: `STTx` objects are never modified after creation, which is critical for ledger integrity and thread safety. Code that reads transaction data is handed an `EscrowCancel`, not a builder; it cannot accidentally mutate state. + +## Relationship to Other Files + +- `TransactionBase.h` — superclass providing common field accessors and `validate()`, which calls `passesLocalChecks` and schema validation via `TxFormats`. +- `TransactionBuilderBase.h` — CRTP superclass providing the mutable `STObject` store, common setters, and the `sign()` implementation. +- `EscrowCreate.h` and `EscrowFinish.h` — sibling files following the identical pattern for the other two legs of the escrow lifecycle (creation and fulfilment). +- `xrpl/protocol/STTx.h` — the underlying serialized transaction type that both the wrapper and builder ultimately consume. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/EscrowCreate.h.ai.json b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h.ai.json new file mode 100644 index 0000000000..516a3c5190 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h.ai.json @@ -0,0 +1,192 @@ +{ + "args": [ + { + "lineno": 31, + "name": "tx" + }, + { + "lineno": 146, + "name": "account" + }, + { + "lineno": 147, + "name": "destination" + }, + { + "lineno": 147, + "name": "amount" + }, + { + "lineno": 148, + "name": "sequence" + }, + { + "lineno": 149, + "name": "fee" + }, + { + "lineno": 154, + "name": "tx" + }, + { + "lineno": 159, + "name": "value" + }, + { + "lineno": 169, + "name": "value" + }, + { + "lineno": 179, + "name": "value" + }, + { + "lineno": 189, + "name": "value" + }, + { + "lineno": 199, + "name": "value" + }, + { + "lineno": 209, + "name": "value" + }, + { + "lineno": 219, + "name": "publicKey" + }, + { + "lineno": 219, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "EscrowCreate" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& destination, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 143, + "name": "EscrowCreateBuilder" + } + ], + "description": "Defines the EscrowCreate transaction type for the XRPL protocol, providing an immutable wrapper for type-safe field access and a builder class for constructing EscrowCreate transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/EscrowCreate.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getDestination" + }, + { + "args": [], + "lineno": 48, + "name": "getAmount" + }, + { + "args": [], + "lineno": 59, + "name": "getCondition" + }, + { + "args": [], + "lineno": 70, + "name": "hasCondition" + }, + { + "args": [], + "lineno": 80, + "name": "getCancelAfter" + }, + { + "args": [], + "lineno": 91, + "name": "hasCancelAfter" + }, + { + "args": [], + "lineno": 101, + "name": "getFinishAfter" + }, + { + "args": [], + "lineno": 112, + "name": "hasFinishAfter" + }, + { + "args": [], + "lineno": 122, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 133, + "name": "hasDestinationTag" + }, + { + "args": [ + "value" + ], + "lineno": 157, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 167, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 177, + "name": "setCondition" + }, + { + "args": [ + "value" + ], + "lineno": 187, + "name": "setCancelAfter" + }, + { + "args": [ + "value" + ], + "lineno": 197, + "name": "setFinishAfter" + }, + { + "args": [ + "value" + ], + "lineno": 207, + "name": "setDestinationTag" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 217, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/EscrowCreate.h.ai.md b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h.ai.md new file mode 100644 index 0000000000..39c357f87b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/EscrowCreate.h.ai.md @@ -0,0 +1,44 @@ +# `EscrowCreate.h` — Auto-generated Transaction Wrapper and Builder + +## Role in the System + +This file is part of the `protocol_autogen` layer — a collection of auto-generated headers (one per XRPL transaction type) that sit above the raw `STTx` serialized-transaction machinery. Its purpose is to give consuming code a typed, self-documenting interface to the `EscrowCreate` transaction (`ttESCROW_CREATE`, type ID 1) without requiring callers to know field codes, option codes, or the `STTx` API directly. + +Every file in the `include/xrpl/protocol_autogen/transactions/` directory follows the same two-class pattern: an immutable *wrapper* that exposes typed getters, and a *builder* that accumulates fields and emits a signed `STTx`. `EscrowCreate.h` is the concrete instantiation of that pattern for on-ledger escrow creation. + +## `EscrowCreate` — The Immutable Wrapper + +`EscrowCreate` inherits from `TransactionBase`, which holds a `std::shared_ptr` (the `tx_` member). The `const`-ness of the pointer target is the key design choice: once a transaction exists on the ledger or is submitted to the network, it must never change, and the type system enforces this. + +The constructor takes a `std::shared_ptr` and immediately validates `tx_->getTxnType() == ttESCROW_CREATE`, throwing `std::runtime_error` on mismatch. This guard is the defensive boundary between the untyped world of raw `STTx` deserialization (which can receive arbitrary transaction bytes from the network) and the typed wrapper world. Every method after construction can rely on the transaction type being correct. + +The five escrow-specific fields are: + +- **`sfDestination`** (`soeREQUIRED`) — the recipient `AccountID`. Accessed via `getDestination()`, which directly returns the value from `tx_->at(sfDestination)`. +- **`sfAmount`** (`soeREQUIRED`) — the locked amount. Documented as supporting MPT (Multi-Purpose Token) amounts in addition to the traditional XRP or IOU `STAmount` forms. +- **`sfCondition`** (`soeOPTIONAL`) — a variable-length blob (`SF_VL`) encoding a PREIMAGE-SHA-256 or other Crypto-Conditions fulfillment. Its presence makes the escrow cryptographically gated: `EscrowFinish` must supply the matching `sfFulfillment`. +- **`sfCancelAfter`** (`soeOPTIONAL`) — a 32-bit XRPL ripple-epoch timestamp after which the escrow can be cancelled. +- **`sfFinishAfter`** (`soeOPTIONAL`) — a 32-bit XRPL ripple-epoch timestamp before which the escrow cannot be finished. +- **`sfDestinationTag`** (`soeOPTIONAL`) — a routing hint for the destination account, allowing exchanges and services to route the incoming funds without requiring a distinct account per payer. + +Optional fields follow a consistent paired pattern: a `hasX()` method checks `tx_->isFieldPresent(sfX)`, and `getX()` returns `protocol_autogen::Optional` — either `std::nullopt` or the field value. The `Optional` alias from `Utils.h` is a `std::conditional_t` that yields `std::optional>` when `T` is a reference type, or `std::optional` otherwise. This prevents dangling references when field types are returned by reference from the underlying `STObject`. + +`TransactionBase` also supplies getters for all universal transaction fields (`sfAccount`, `sfSequence`, `sfFee`, `sfFlags`, `sfMemos`, `sfSigners`, `sfDelegate`, etc.), a `validate()` method that runs schema validation against `TxFormats::getInstance()` and then `passesLocalChecks()`, and `getSTTx()` as an escape hatch when the typed API is insufficient. + +## `EscrowCreateBuilder` — The Fluent Builder + +`EscrowCreateBuilder` inherits from `TransactionBuilderBase`, which uses the Curiously Recurring Template Pattern (CRTP). The base class's common setters (`setAccount`, `setFee`, `setSequence`, `setLastLedgerSequence`, etc.) all return `Derived&` — concretely `EscrowCreateBuilder&` — enabling uniform method chaining without virtual dispatch or casting at the call site. + +The internal state is a `STObject object_{sfTransaction}` (declared in `TransactionBuilderBase`), populated by direct field assignment (`object_[sfX] = value`). The builder intentionally avoids calling `object_.set(soTemplate)` on this object. That would pre-populate `soeDEFAULT` fields with placeholder entries, which would then cause the `STTx` constructor's `applyTemplate()` call to reject them with "may not be explicitly set to default." The template is applied correctly by the `STTx` constructor itself; the builder's job is only to provide the fields the caller actually sets. + +The primary constructor enforces the same invariant as the wrapper: `sfDestination` and `sfAmount` are required and are set immediately via `setDestination()` and `setAmount()`. Optional fields (`sfCondition`, `sfCancelAfter`, `sfFinishAfter`, `sfDestinationTag`) have individual setters returning `EscrowCreateBuilder&`. + +A second constructor accepts an existing `std::shared_ptr` and copies its `STObject` contents into `object_`, allowing round-trip editing of a transaction that was already deserialized. It similarly throws on type mismatch. + +`build(PublicKey, SecretKey)` finalizes construction: it calls the protected `sign()` method from `TransactionBuilderBase`, which sets `sfSigningPubKey`, serializes the object with `HashPrefix::txSign` prepended (excluding signing fields), signs the bytes with the provided keys, and stores the resulting signature in `sfTxnSignature`. The signed `STObject` is then moved into a new `STTx`, which is wrapped in an `EscrowCreate` and returned. + +## Design Tradeoffs and Conventions + +The split between wrapper and builder reflects a deliberate immutability discipline: after `build()`, the transaction cannot be mutated, preventing accidental invalidation of the signature. The `[[nodiscard]]` annotation on every getter enforces that callers handle return values rather than discarding them silently. + +The `sfAmount` field's MPT support is a forward-looking design: the XRPL is extending escrow semantics to cover Multi-Purpose Token amounts, not just XRP drops and trust-line IOUs. The autogenerated nature of this file means that when the protocol definition changes, the tooling regenerates the header with the correct field annotations — callers never hand-maintain field codes or optionality rules. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/EscrowFinish.h.ai.json b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h.ai.json new file mode 100644 index 0000000000..a75eaa3ab2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h.ai.json @@ -0,0 +1,195 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 134, + "name": "account" + }, + { + "lineno": 134, + "name": "owner" + }, + { + "lineno": 134, + "name": "offerSequence" + }, + { + "lineno": 134, + "name": "sequence" + }, + { + "lineno": 134, + "name": "fee" + }, + { + "lineno": 146, + "name": "tx" + }, + { + "lineno": 159, + "name": "value" + }, + { + "lineno": 168, + "name": "value" + }, + { + "lineno": 177, + "name": "value" + }, + { + "lineno": 186, + "name": "value" + }, + { + "lineno": 195, + "name": "value" + }, + { + "lineno": 204, + "name": "publicKey" + }, + { + "lineno": 204, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 24, + "name": "EscrowFinish" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& owner, std::decay_t const& offerSequence, std::optional sequence = std::nullopt, std::optional fee = std::nullopt" + ], + "lineno": 131, + "name": "EscrowFinishBuilder" + } + ], + "description": "Defines the EscrowFinish transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing EscrowFinish transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/EscrowFinish.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "EscrowFinish" + }, + { + "args": [], + "lineno": 41, + "name": "getOwner" + }, + { + "args": [], + "lineno": 51, + "name": "getOfferSequence" + }, + { + "args": [], + "lineno": 61, + "name": "getFulfillment" + }, + { + "args": [], + "lineno": 73, + "name": "hasFulfillment" + }, + { + "args": [], + "lineno": 83, + "name": "getCondition" + }, + { + "args": [], + "lineno": 95, + "name": "hasCondition" + }, + { + "args": [], + "lineno": 105, + "name": "getCredentialIDs" + }, + { + "args": [], + "lineno": 117, + "name": "hasCredentialIDs" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& owner", + "std::decay_t const& offerSequence", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 134, + "name": "EscrowFinishBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 146, + "name": "EscrowFinishBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 159, + "name": "setOwner" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 168, + "name": "setOfferSequence" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 177, + "name": "setFulfillment" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 186, + "name": "setCondition" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 195, + "name": "setCredentialIDs" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 204, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/EscrowFinish.h.ai.md b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h.ai.md new file mode 100644 index 0000000000..7dbfb3b8eb --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/EscrowFinish.h.ai.md @@ -0,0 +1,48 @@ +# `EscrowFinish.h` — Auto-Generated EscrowFinish Transaction Wrapper + +This file is auto-generated and sits within the `xrpl/protocol_autogen/transactions/` layer, which provides type-safe C++ wrappers over the raw `STTx` serialization type for every XRPL transaction kind. `EscrowFinish.h` covers `ttESCROW_FINISH` (type code 2), the third of the three escrow lifecycle transactions alongside `EscrowCreate` (type 1) and `EscrowCancel` (type 4). + +## Role in the Escrow Lifecycle + +An escrow on XRPL is a conditional payment held in ledger state. `EscrowCreate` locks funds; `EscrowFinish` releases them to the destination once the release conditions are satisfied; `EscrowCancel` returns them to the sender after a deadline. `EscrowFinish` is the most field-rich of the three because it must carry proof that the release conditions have been met — either a time condition (verified by the ledger) or a PREIMAGE-SHA-256 crypto-condition (verified by presenting a cryptographic fulfillment). + +Comparing the three escrow headers makes the differences clear: +- `EscrowCancel` has only `sfOwner` and `sfOfferSequence` as required fields — no optional fields at all. +- `EscrowCreate` has `sfDestination` and `sfAmount` as required, plus optional time/condition locks. +- `EscrowFinish` has `sfOwner` and `sfOfferSequence` as required, plus optional `sfFulfillment`, `sfCondition`, and `sfCredentialIDs`. + +## The Two-Class Pattern + +The file defines two classes in `namespace xrpl::transactions`: + +**`EscrowFinish`** is an immutable read-only wrapper. It holds a `std::shared_ptr` — the `const` qualifier on the pointed-to type prevents any mutation through this class, making concurrent reads safe without any locking. The constructor eagerly validates the transaction type and throws `std::runtime_error` if a non-`ttESCROW_FINISH` `STTx` is passed in. This is a deliberate defensive check: the `STTx` type system does not enforce transaction kinds at compile time, so the runtime guard at construction prevents silent misuse of a wrapper with the wrong underlying data. + +**`EscrowFinishBuilder`** is the mutable construction side. It extends `TransactionBuilderBase` using CRTP. The CRTP choice matters for ergonomics: all inherited setters in `TransactionBuilderBase` return `Derived&` (resolved at compile time to `EscrowFinishBuilder&`) rather than a base-class reference, so method chains like `.setLastLedgerSequence(n).setFulfillment(blob).build(pub, sec)` compile without casts or intermediate variables. + +## Field Design + +The two required fields — `sfOwner` and `sfOfferSequence` — together identify the escrow object in the ledger. `sfOwner` is the account that created the escrow (not necessarily the finisher), and `sfOfferSequence` is the sequence number of the original `EscrowCreate` transaction, acting as a unique key for the escrow object within that owner's namespace. Both are mandatory in the builder constructor, preventing construction of a structurally invalid transaction. + +The three optional fields tell a richer story: + +- **`sfFulfillment`** (`SF_VL`, a variable-length blob): the PREIMAGE-SHA-256 preimage from the Crypto-Conditions RFC. It is only required when the escrow was created with a `sfCondition` lock. The ledger hashes the fulfillment and checks it against the stored condition. +- **`sfCondition`** (`SF_VL`): the SHA-256 condition that must be satisfied. Echoing the condition in `EscrowFinish` allows the ledger to verify the fulfillment without having to retrieve and decode the escrow's stored condition separately from the STTx itself during validation — though in practice the ledger does both. +- **`sfCredentialIDs`** (`SF_VECTOR256`, a vector of `uint256`): a newer field reflecting XRPL's deposit-preauth credential system. When the destination account requires credential-based authorization (via `DepositPreauth` with credential requirements), the finisher can supply credential object IDs proving they meet those requirements. + +Each optional field follows a paired accessor pattern: `hasFulfillment()` returns `bool` for a cheap presence check, while `getFulfillment()` returns `protocol_autogen::Optional` (an alias for `std::optional`) and returns `std::nullopt` if the field is absent. This avoids callers having to catch exceptions from `STTx::at()` on missing fields. + +All getter methods are annotated `[[nodiscard]]`, enforcing that callers actually consume the return value rather than accidentally discarding it. + +## Builder Construction Modes + +`EscrowFinishBuilder` offers two construction paths: + +1. **From scratch**: the primary constructor takes `account`, `owner`, and `offerSequence` as required values, then passes the transaction type and account on to `TransactionBuilderBase`. Optional fields are added via method chaining before calling `build()`. + +2. **From an existing `STTx`**: the secondary constructor copies the underlying `STObject` from a fully-formed transaction (`object_ = *tx`). This enables re-signing an already-serialized `EscrowFinish` — for example when bridging or reprocessing transactions — without re-parsing JSON. + +The `build()` method calls the protected `sign()` helper from `TransactionBuilderBase`, which serializes the `STObject` with the `HashPrefix::txSign` prefix, signs it with the provided key pair, then wraps the resulting `STTx` in an `EscrowFinish` wrapper. At this point the builder's `STObject` is `std::move`d into the `STTx`, so the builder should not be used after calling `build()`. + +## Relationship to the Broader Autogen Layer + +This file follows exactly the same structural template as every other header in `protocol_autogen/transactions/`. The "Do not edit" banner signals that the source of truth is a code-generation tool, not this file — changes belong upstream. The `TransactionBase` base class (read in `TransactionBase.h`) provides getters for all common XRPL transaction fields (`sfAccount`, `sfSequence`, `sfFee`, `sfFlags`, `sfMemos`, `sfSigners`, `sfDelegate`, and more), so `EscrowFinish` inherits a full read interface without any boilerplate. The `validate()` method on `TransactionBase` runs schema validation via `TxFormats::getInstance()` and local ledger checks, and is available on all wrappers including `EscrowFinish`. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h.ai.json b/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h.ai.json new file mode 100644 index 0000000000..71cd4c8ed0 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h.ai.json @@ -0,0 +1,109 @@ +{ + "args": [ + { + "lineno": 36, + "name": "tx" + }, + { + "lineno": 87, + "name": "account" + }, + { + "lineno": 88, + "name": "ledgerFixType" + }, + { + "lineno": 89, + "name": "sequence" + }, + { + "lineno": 90, + "name": "fee" + }, + { + "lineno": 97, + "name": "tx" + }, + { + "lineno": 102, + "name": "value" + }, + { + "lineno": 111, + "name": "value" + }, + { + "lineno": 121, + "name": "publicKey" + }, + { + "lineno": 121, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 33, + "name": "LedgerStateFix" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& ledgerFixType, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 80, + "name": "LedgerStateFixBuilder" + } + ], + "description": "Defines the LedgerStateFix transaction type for XRPL, including its immutable wrapper and builder class for constructing and signing transactions with type-safe field access.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "LedgerStateFix::getLedgerFixType" + }, + { + "args": [], + "lineno": 54, + "name": "LedgerStateFix::getOwner" + }, + { + "args": [], + "lineno": 65, + "name": "LedgerStateFix::hasOwner" + }, + { + "args": [ + "value" + ], + "lineno": 101, + "name": "LedgerStateFixBuilder::setLedgerFixType" + }, + { + "args": [ + "value" + ], + "lineno": 110, + "name": "LedgerStateFixBuilder::setOwner" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 119, + "name": "LedgerStateFixBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h.ai.md b/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h.ai.md new file mode 100644 index 0000000000..b82ae9b8b2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LedgerStateFix.h.ai.md @@ -0,0 +1,52 @@ +# `LedgerStateFix.h` — Auto-generated Transaction Wrapper for `ttLEDGER_STATE_FIX` + +## Role and Context + +This header is part of the `protocol_autogen` subsystem — a layer of machine-generated code that wraps every XRPL transaction type in a pair of C++ classes: an immutable read-only accessor (`LedgerStateFix`) and a fluent builder (`LedgerStateFixBuilder`). It lives in `xrpl::transactions`, distinct from the identically named `xrpl::LedgerStateFix` transactor class that implements the actual on-ledger logic. + +The transaction type it represents, `ttLEDGER_STATE_FIX` (numeric value 53), exists for a specific and narrow purpose: repairing corrupted state in the XRPL ledger itself. Its first and currently only fix variant (`nfTokenPageLink = 1`) addresses broken doubly-linked-list pointers in NFToken page chains — a class of corruption that can leave an account's NFTs inaccessible. This is an unusual type in the protocol: it is not about transferring value or creating objects, but about surgical correction of existing ledger state. + +The transaction is guarded by the `fixNFTokenPageLinks` amendment. It cannot be submitted on the main network until that amendment reaches supermajority consensus. It is marked `delegable`, meaning it can be submitted by a delegate account on behalf of the owning account. + +## Field Schema + +The transaction has exactly two fields beyond the universal common fields inherited from all transactions: + +- **`sfLedgerFixType`** (`UINT16`, required): Identifies which fix operation to perform. Currently only value `1` (`nfTokenPageLink`) is valid; any other value causes `preflight` to return `tefINVALID_LEDGER_FIX_TYPE`. This field acts as a discriminant that lets the protocol extend to new fix operations without introducing new transaction types. + +- **`sfOwner`** (`ACCOUNT`, optional): The target account whose state is being repaired. It is required when `sfLedgerFixType == nfTokenPageLink` — the `preflight` check enforces this relationship. The optionality at the schema level allows future fix types that operate without a specific account target. + +This two-field design is intentional: it keeps the transaction extensible. Adding a new kind of ledger repair only requires adding a new `FixType` enum value and handling it in the transactor's switch statement, not a new transaction type. + +## Class Structure + +### `LedgerStateFix` — Immutable Wrapper + +`LedgerStateFix` extends `TransactionBase`, which holds a `std::shared_ptr` and provides accessors for universal fields (account, fee, sequence, flags, memos, signers, delegate, etc.). The subclass adds two transaction-specific accessors: + +- `getLedgerFixType()` returns the `uint16_t` value directly, as the field is required and always present. +- `getOwner()` returns `protocol_autogen::Optional` — a thin alias for `std::optional` — paired with a `hasOwner()` predicate. This pattern is consistent across all optional fields in the autogen layer. + +The constructor validates the `STTx` type at construction time, throwing `std::runtime_error` if the transaction type doesn't match `ttLEDGER_STATE_FIX`. This is an eager fail-fast check: it catches programming errors at the point of wrapping rather than producing silent misbehavior during field access. + +All getters are marked `[[nodiscard]]`, preventing callers from accidentally discarding return values from predicate or value-returning functions. + +### `LedgerStateFixBuilder` — Fluent Constructor + +`LedgerStateFixBuilder` inherits from `TransactionBuilderBase` using the Curiously Recurring Template Pattern (CRTP). The base class uses `static_cast(*this)` in every setter to return the derived type, enabling method chaining without virtual dispatch and without losing type information. The result is that callers can chain `setLedgerFixType(...)`, `setOwner(...)`, `setFee(...)`, `setLastLedgerSequence(...)`, and any other common setter in a single expression, with the final `.build(publicKey, secretKey)` producing a signed `LedgerStateFix` wrapper. + +The builder offers two construction paths: +1. **From scratch**: The primary constructor takes `account` and `ledgerFixType` as required parameters, with `sequence` and `fee` as optional. It immediately calls `setLedgerFixType()` in the constructor body, ensuring the required field is always present regardless of subsequent chaining. +2. **From an existing `STTx`**: A secondary constructor copies the serialized transaction into the internal `STObject`, useful for re-signing or modifying a previously constructed transaction. + +The `build()` method calls the protected `sign()` helper from `TransactionBuilderBase`, which serializes the object with `HashPrefix::txSign` prepended, signs it with the provided key pair, and embeds the resulting signature. The signed `STTx` is then moved into a `shared_ptr` — making it immutable — and wrapped in the `LedgerStateFix` accessor type. + +A notable detail in `TransactionBuilderBase`: the internal `STObject` is intentionally kept as a "free object" (not bound to an `SOTemplate`). This avoids pre-populating default-valued fields, which would later cause `STTx::applyTemplate()` to throw when encountering fields explicitly set to their default. The template is only applied by the `STTx` constructor at `build()` time. + +## Fee Design + +The fee for `LedgerStateFix` is set to one owner reserve (the same as `AccountDelete`). This is deliberately expensive. The transaction performs ledger repair, which requires iterating and relinking NFToken pages — potentially a non-trivial operation. The high fee discourages frivolous use while remaining accessible when a genuine fix is needed. + +## Relationship to the Transactor + +The `xrpl::LedgerStateFix` transactor (in `src/libxrpl/tx/transactors/system/LedgerStateFix.cpp`) consumes the signed `STTx` that this builder produces. The transactor's `doApply()` dispatches on `sfLedgerFixType` and calls `nft::repairNFTokenDirectoryLinks()` for the `nfTokenPageLink` variant. The wrapper classes in this autogen header are entirely separate from that execution path — they exist to provide a safe, ergonomic construction and inspection API at the application and test layers, while the raw `STTx` flows through the existing transaction pipeline unchanged. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h.ai.json b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h.ai.json new file mode 100644 index 0000000000..14360880c3 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h.ai.json @@ -0,0 +1,133 @@ +{ + "args": [ + { + "lineno": 28, + "name": "tx" + }, + { + "lineno": 93, + "name": "account" + }, + { + "lineno": 93, + "name": "sequence" + }, + { + "lineno": 93, + "name": "fee" + }, + { + "lineno": 102, + "name": "tx" + }, + { + "lineno": 115, + "name": "value" + }, + { + "lineno": 124, + "name": "value" + }, + { + "lineno": 133, + "name": "publicKey" + }, + { + "lineno": 133, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 21, + "name": "LoanBrokerCoverClawback" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 87, + "name": "LoanBrokerCoverClawbackBuilder" + } + ], + "description": "Defines the LoanBrokerCoverClawback transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "LoanBrokerCoverClawback" + }, + { + "args": [], + "lineno": 41, + "name": "getLoanBrokerID" + }, + { + "args": [], + "lineno": 52, + "name": "hasLoanBrokerID" + }, + { + "args": [], + "lineno": 63, + "name": "getAmount" + }, + { + "args": [], + "lineno": 74, + "name": "hasAmount" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 93, + "name": "LoanBrokerCoverClawbackBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 102, + "name": "LoanBrokerCoverClawbackBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 115, + "name": "setLoanBrokerID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 124, + "name": "setAmount" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 133, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h.ai.md b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h.ai.md new file mode 100644 index 0000000000..943ad2ceb4 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverClawback.h.ai.md @@ -0,0 +1,35 @@ +# `LoanBrokerCoverClawback.h` — Auto-generated Transaction Wrapper + +## Purpose and Context + +This header defines the `LoanBrokerCoverClawback` transaction type (`ttLOAN_BROKER_COVER_CLAWBACK`, type code 78) introduced as part of the `featureLendingProtocol` amendment on the XRP Ledger. It belongs to a suite of five LoanBroker transaction types — alongside `LoanBrokerSet` (74), `LoanBrokerCoverDeposit` (76), `LoanBrokerCoverWithdraw`, and `LoanBrokerDelete` — that collectively manage on-ledger loan brokerage operations. Within this family, the cover transactions handle collateral pool mechanics: depositing cover assets, withdrawing them, and, as this file implements, clawing them back. + +The file is **auto-generated and must not be hand-edited**. It exposes two C++ classes in the `xrpl::transactions` namespace: an immutable read accessor (`LoanBrokerCoverClawback`) and a mutable builder (`LoanBrokerCoverClawbackBuilder`), following the same structural pattern applied uniformly across the entire autogenerated transaction library. + +## The `LoanBrokerCoverClawback` Wrapper + +`LoanBrokerCoverClawback` inherits from `TransactionBase`, which holds a `std::shared_ptr` — the canonical serialized form of any XRPL transaction. The choice to store a `const` shared pointer is intentional: it enforces immutability at the type-system level, ensuring callers cannot mutate a transaction that may already have been signed or submitted. + +The constructor performs a runtime type check against `txType` immediately after moving the pointer into the base. If the underlying `STTx` carries any transaction type other than `ttLOAN_BROKER_COVER_CLAWBACK`, a `std::runtime_error` is thrown. This guards against accidental downcasting from a generic `STTx` that holds a different operation. + +Both transaction-specific fields — `sfLoanBrokerID` and `sfAmount` — are declared `soeOPTIONAL`, so the accessor pattern follows a consistent two-method contract: a `hasX()` presence check and a `getX()` returning `protocol_autogen::Optional` (effectively `std::optional`). Contrast this with `LoanBrokerCoverDeposit`, where both fields are `soeREQUIRED`, and the getters return values directly without optionality. The distinction reflects a semantically important difference in the protocol: in a clawback operation the amount and even the broker target may be determined or capped by protocol logic rather than being mandatory caller inputs. + +`sfLoanBrokerID` is a `UINT256` hash identifying the target loan broker ledger object. `sfAmount` is typed as `SF_AMOUNT`, which explicitly supports Multi-Purpose Token (MPT) amounts in addition to XRP and issued currencies — this flexibility is noted in the inline documentation because MPT amounts have different serialization and validation rules than classic `STAmount` values. + +The `TransactionBase` base class provides the full set of standard field accessors common to all transactions: `getAccount()`, `getSequence()`, `getFee()`, `getSigningPubKey()`, and optional fields like `getFlags()`, `getMemos()`, `getSigners()`, and `getLastLedgerSequence()`. It also exposes `validate()`, which runs schema validation via `TxFormats` and then invokes `passesLocalChecks()` for non-pseudo transactions. + +## The `LoanBrokerCoverClawbackBuilder` + +`LoanBrokerCoverClawbackBuilder` inherits from `TransactionBuilderBase`, which uses CRTP (Curiously Recurring Template Pattern) so every base-class setter returns a `Derived&` — enabling fluent method chaining without losing the concrete type. The base holds an `STObject object_{sfTransaction}` that accumulates fields before final serialization. + +The builder offers two construction paths. The primary constructor accepts the mandatory `account`, plus optional `sequence` and optional `fee`. Since neither `sfLoanBrokerID` nor `sfAmount` is required for this transaction, neither appears in the constructor's parameter list — they are set exclusively through the optional fluent setters. This differs from `LoanBrokerCoverDepositBuilder`, where `loanBrokerID` and `amount` are required constructor arguments enforcing that callers supply them up front. + +The second constructor accepts an existing `std::shared_ptr` and copies the transaction's `STObject` state into the mutable `object_` member. This enables edit-and-rebuild workflows, such as fee bumps or re-signing, without needing to reconstruct the transaction from scratch. + +The `build()` method signs the accumulated `STObject` by calling the protected `sign()` method from `TransactionBuilderBase`. That method serializes the object without signing fields, prepends the `HashPrefix::txSign` prefix, computes the ECDSA/Ed25519 signature using the supplied `SecretKey`, and embeds both the public key and signature into the object. It then constructs an `STTx` from the signed `STObject` via move semantics and wraps it in the immutable `LoanBrokerCoverClawback` accessor. The `std::move` here is deliberate — it transfers ownership out of the builder, leaving it in a valid-but-empty state after `build()` is called. + +## Protocol Design Notes + +The transaction is marked `notDelegable`, meaning it cannot be submitted on behalf of another account via the `sfDelegate` field even though `TransactionBase` exposes `getDelegate()`. The `noPriv` designation indicates it requires no special account privileges beyond ordinary transaction submission capability. + +The fully optional nature of both `sfLoanBrokerID` and `sfAmount` implies that the XRPL transaction processor can resolve the target broker and/or the clawback amount from ledger state rather than requiring explicit caller specification. This is consistent with clawback semantics in other XRPL contexts (such as `AMMClawback` and `Clawback` transactions), where the issuer-side or protocol-side may enforce amounts that don't need external input to be determined. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h.ai.json b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h.ai.json new file mode 100644 index 0000000000..ab654e4432 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h.ai.json @@ -0,0 +1,100 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 67, + "name": "account" + }, + { + "lineno": 68, + "name": "loanBrokerID" + }, + { + "lineno": 68, + "name": "amount" + }, + { + "lineno": 69, + "name": "sequence" + }, + { + "lineno": 70, + "name": "fee" + }, + { + "lineno": 102, + "name": "publicKey" + }, + { + "lineno": 102, + "name": "secretKey" + }, + { + "lineno": 82, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "LoanBrokerCoverDeposit" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& loanBrokerID, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 61, + "name": "LoanBrokerCoverDepositBuilder" + } + ], + "description": "Defines the LoanBrokerCoverDeposit transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getLoanBrokerID" + }, + { + "args": [], + "lineno": 48, + "name": "getAmount" + }, + { + "args": [ + "value" + ], + "lineno": 81, + "name": "setLoanBrokerID" + }, + { + "args": [ + "value" + ], + "lineno": 91, + "name": "setAmount" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 101, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h.ai.md b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h.ai.md new file mode 100644 index 0000000000..36f340dd01 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverDeposit.h.ai.md @@ -0,0 +1,38 @@ +# `LoanBrokerCoverDeposit.h` — Auto-generated Transaction Wrapper + +## Role in the System + +This file is part of the `protocol_autogen` layer — a code-generated family of transaction-specific wrappers and builders that live under `include/xrpl/protocol_autogen/transactions/`. Its purpose is to give callers a type-safe, self-documenting C++ interface to the `ttLOAN_BROKER_COVER_DEPOSIT` transaction (numeric type 76), which is part of the `featureLendingProtocol` amendment family. + +The transaction itself represents a deposit of *First Loss Capital* into a LoanBroker object on the XRP Ledger. In the lending protocol, a LoanBroker operator must post cover (collateral) into the broker's pseudo-account before loans can be extended; this transaction is how that cover moves from the owner's balance into the broker's available reserve (`sfCoverAvailable`). The underlying transactor logic in `LoanBrokerCoverDeposit.cpp` performs the `accountSend` from the submitting account to the broker pseudo-account and increments the broker's cover balance. + +## Two-Class Autogen Pattern + +Every transaction in the autogen layer follows the same dual-class pattern defined by this file's base types: + +**`LoanBrokerCoverDeposit`** inherits from `TransactionBase`, which holds a `std::shared_ptr`. The `const` in the shared pointer is the critical design choice — the wrapper is permanently immutable. The type-check in the constructor (`tx_->getTxnType() != txType`) enforces the invariant that you cannot accidentally wrap the wrong transaction type; a mismatched `STTx` throws `std::runtime_error` at construction time rather than silently returning garbage from a field getter. + +`getLoanBrokerID()` and `getAmount()` are the only transaction-specific accessors exposed here. Everything else — account, sequence, fee, flags, memos, signers, network ID, delegate, etc. — is inherited from `TransactionBase`, which covers the full set of common XRPL transaction fields. + +**`LoanBrokerCoverDepositBuilder`** inherits from `TransactionBuilderBase`, a CRTP template. This curiously recurring template pattern is why every setter in `TransactionBuilderBase` can return `Derived&` (the concrete builder type) rather than a base-class reference, preserving the fluent method-chaining interface across the inheritance boundary without virtual dispatch or casting at the call site. + +The builder works with a mutable `STObject object_{sfTransaction}` inherited from the base. The comment in `TransactionBuilderBase` explains a subtle point: the object is left "free" (not initialized from the SOTemplate) intentionally. Initializing it from the template would inject `STBase` placeholders for `soeDEFAULT` fields, which then cause `applyTemplate()` to throw `"may not be explicitly set to default"` when the `STTx` constructor is called. The fix is to populate only the fields you actually set, and let the `STTx` constructor's own `applyTemplate()` handle defaults cleanly. + +## Required Fields + +The transaction macro in `transactions.macro` declares exactly two required fields: + +- `sfLoanBrokerID` (`soeREQUIRED`): A `uint256` identifying the target LoanBroker ledger object. The transactor rejects a zero value with `temINVALID`. +- `sfAmount` (`soeREQUIRED`, `soeMPTSupported`): The amount of cover to deposit. The `soeMPTSupported` annotation means the field accepts both traditional XRP/IOU amounts and Multi-Purpose Token (MPT) amounts — the `getAmount()` and `setAmount()` methods reflect this through `SF_AMOUNT::type::value_type`. + +Both fields are required in the builder constructor, so the fluent interface cannot be used to accidentally leave them unset before calling `build()`. + +## Build and Sign Flow + +`LoanBrokerCoverDepositBuilder::build(publicKey, secretKey)` calls the protected `sign()` method inherited from `TransactionBuilderBase`. That method serializes the `STObject` without its signing fields, prepends `HashPrefix::txSign`, computes the cryptographic signature, embeds it as `sfTxnSignature`, and sets `sfSigningPubKey`. The resulting signed `STObject` is then moved into a freshly constructed `STTx`, which is wrapped in a `shared_ptr` and handed to the `LoanBrokerCoverDeposit` constructor. + +The builder also provides a second constructor that takes an existing `std::shared_ptr`. This enables a round-trip workflow: decode a transaction from the wire, wrap it in a builder via `*tx` assignment into `object_`, modify fields, re-sign, and build a new wrapper. The type-check in that constructor mirrors the one in `LoanBrokerCoverDeposit`'s constructor for the same reason. + +## Amendment and Delegation + +The transaction is gated by `featureLendingProtocol` and is explicitly `Delegation::notDelegable` — an account cannot authorize a delegate to submit cover deposits on its behalf. This is enforced at the transactor layer (`checkLendingProtocolDependencies` and the delegation flag check in `Transactor`), but the `txType` constant and the amendment annotation in the class doc also make these constraints visible directly from the header to any reader or code generator consuming it. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h.ai.json b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h.ai.json new file mode 100644 index 0000000000..c482a522d2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h.ai.json @@ -0,0 +1,122 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 24, + "name": "LoanBrokerCoverWithdraw" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& loanBrokerID, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 104, + "name": "LoanBrokerCoverWithdrawBuilder" + } + ], + "description": "Defines the LoanBrokerCoverWithdraw transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "LoanBrokerCoverWithdraw" + }, + { + "args": [], + "lineno": 44, + "name": "getLoanBrokerID" + }, + { + "args": [], + "lineno": 54, + "name": "getAmount" + }, + { + "args": [], + "lineno": 64, + "name": "getDestination" + }, + { + "args": [], + "lineno": 75, + "name": "hasDestination" + }, + { + "args": [], + "lineno": 85, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 96, + "name": "hasDestinationTag" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& loanBrokerID", + "std::decay_t const& amount", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 112, + "name": "LoanBrokerCoverWithdrawBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 124, + "name": "LoanBrokerCoverWithdrawBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 136, + "name": "setLoanBrokerID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 146, + "name": "setAmount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 156, + "name": "setDestination" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 166, + "name": "setDestinationTag" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 176, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h.ai.md b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h.ai.md new file mode 100644 index 0000000000..8789c23c69 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerCoverWithdraw.h.ai.md @@ -0,0 +1,53 @@ +# `LoanBrokerCoverWithdraw.h` — Auto-generated Transaction Wrapper + +## Role and Context + +This file is part of the auto-generated `protocol_autogen` layer for the XRPL LendingProtocol amendment (`featureLendingProtocol`). It defines the C++ interface for transaction type `ttLOAN_BROKER_COVER_WITHDRAW` (numeric type 77), which withdraws First Loss Capital (FLC) from a Loan Broker ledger object. + +As its comment warns — *"This file is auto-generated. Do not edit."* — the canonical source of truth is the TRANSACTION macro entry in `include/xrpl/protocol/detail/transactions.macro`, which declares the field schema. The generated wrapper and builder in this file are derived mechanically from that schema, ensuring the two are always in sync. + +Within the LendingProtocol family of transactions, `LoanBrokerCoverWithdraw` is the counterpart to `LoanBrokerCoverDeposit` (type 76). The deposit adds cover capital to a broker; the withdraw reclaims it. The withdraw variant carries two additional optional fields (`sfDestination`, `sfDestinationTag`) that the deposit transaction lacks, allowing withdrawn funds to be directed to an account other than the transaction sender. + +## Class Design: Wrapper + Builder Pattern + +The file defines two classes in `namespace xrpl::transactions`: + +**`LoanBrokerCoverWithdraw`** inherits from `TransactionBase` and acts as an immutable, type-safe view over an existing `STTx`. It holds the transaction via a `std::shared_ptr` promoted from the base class and enforces the correct transaction type at construction: + +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for LoanBrokerCoverWithdraw"); +``` + +This fail-fast check ensures that code receiving a `LoanBrokerCoverWithdraw` object can trust the underlying `STTx` without further type checks — a defensive design that protects callers from accidentally wrapping the wrong transaction type. + +**`LoanBrokerCoverWithdrawBuilder`** inherits from the CRTP base `TransactionBuilderBase`. The Curiously Recurring Template Pattern is used so that common setter methods (like `setFee()`, `setSequence()`, `setMemos()`) defined in the base class return `Derived&` — the concrete builder type — enabling method chaining without virtual dispatch. The builder holds an `STObject object_{sfTransaction}` (declared in the base) as its mutable staging area. Calling `build(publicKey, secretKey)` invokes `sign()` on that object, moves it into a freshly constructed `STTx`, and wraps it in the immutable `LoanBrokerCoverWithdraw` type. + +A second constructor `LoanBrokerCoverWithdrawBuilder(std::shared_ptr tx)` reconstructs a mutable builder from an existing signed transaction by copying the `STTx` into the `object_` staging area. This supports round-trip scenarios where an existing transaction must be rebuilt or re-signed. + +## Field Schema + +The transaction carries four fields derived from the macro schema: + +| Field | Requirement | Type | Notes | +|---|---|---|---| +| `sfLoanBrokerID` | Required | `uint256` | Identifies the Loan Broker ledger object | +| `sfAmount` | Required | `STAmount` | Supports MPT; the amount of cover capital to withdraw | +| `sfDestination` | Optional | `AccountID` | Destination account for withdrawn funds | +| `sfDestinationTag` | Optional | `uint32_t` | Routing tag for destination account | + +For required fields the wrapper getters return values directly (`getLoanBrokerID()`, `getAmount()`). For optional fields the getter checks `isFieldPresent()` first and returns `protocol_autogen::Optional` — an alias for `std::optional` — with paired `has*()` predicates. This avoids an unguarded `at()` call that would throw if the field is absent, providing a safe access pattern. + +## The `mayAuthorizeMPT` Privilege + +The macro entry marks this transaction with the `mayAuthorizeMPT` privilege, unlike the deposit counterpart which carries `noPriv`. This matters because `sfAmount` uses the `soeMPTSupported` annotation: the amount field can carry a Multi-Purpose Token (MPT) quantity rather than an XRP or IOU amount. The `mayAuthorizeMPT` flag signals to the engine's trust-line and MPT authorization machinery that this transaction may modify MPT authorization state as a side effect of moving MPT amounts out of the broker's cover pool. + +The transaction is also marked `Delegation::notDelegable`, meaning no other account can be granted delegated authority to submit it. This restriction — shared with all LoanBroker transactions — reflects the sensitive nature of cover capital management. + +## Relationship to the Transactor + +The actual ledger logic for this transaction lives in `include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h` (and its `.cpp` implementation). That file defines a `Transactor` subclass with `preflight`, `preclaim`, and `doApply` hooks. The auto-generated wrapper in this file is consumed by the transactor's `doApply` to access transaction fields in a type-safe way, and by test code and external tooling to construct well-formed transactions for submission. The separation keeps the protocol field schema (auto-generated, locked to the macro) cleanly distinct from the application logic (hand-written, in the transactor). + +## Code Quality Notes + +All getter methods are annotated `[[nodiscard]]`, preventing callers from silently discarding return values. Builder setters use `std::decay_t` to accept values without imposing reference or const qualification concerns on callers. The constructor for the builder deliberately avoids calling `object_.set(soTemplate)` on the underlying `STObject` — a comment in `TransactionBuilderBase` explains this is intentional: setting a template before construction causes `applyTemplate()` inside the `STTx` constructor to reject fields that were left at their default values, so the base stays as a free `STObject` and the `STTx` constructor handles template application cleanly. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h.ai.json b/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h.ai.json new file mode 100644 index 0000000000..2737a69cf0 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h.ai.json @@ -0,0 +1,112 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 59, + "name": "account" + }, + { + "lineno": 59, + "name": "loanBrokerID" + }, + { + "lineno": 59, + "name": "sequence" + }, + { + "lineno": 59, + "name": "fee" + }, + { + "lineno": 70, + "name": "tx" + }, + { + "lineno": 81, + "name": "value" + }, + { + "lineno": 90, + "name": "publicKey" + }, + { + "lineno": 90, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 22, + "name": "LoanBrokerDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& loanBrokerID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 54, + "name": "LoanBrokerDeleteBuilder" + } + ], + "description": "Defines the LoanBrokerDelete transaction type for the XRPL protocol, including its builder class for constructing and signing transactions, with type-safe field access and transaction-specific field setters.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "LoanBrokerDelete::LoanBrokerDelete" + }, + { + "args": [], + "lineno": 41, + "name": "LoanBrokerDelete::getLoanBrokerID" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& loanBrokerID", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 59, + "name": "LoanBrokerDeleteBuilder::LoanBrokerDeleteBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 70, + "name": "LoanBrokerDeleteBuilder::LoanBrokerDeleteBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 81, + "name": "LoanBrokerDeleteBuilder::setLoanBrokerID" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 90, + "name": "LoanBrokerDeleteBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h.ai.md b/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h.ai.md new file mode 100644 index 0000000000..6f30c1dc24 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerDelete.h.ai.md @@ -0,0 +1,41 @@ +# `LoanBrokerDelete.h` — Auto-Generated Transaction Wrapper for `ttLOAN_BROKER_DELETE` + +## Role and Context + +This file lives in `include/xrpl/protocol_autogen/transactions/` alongside a family of similarly structured headers — one per XRPL transaction type. It is machine-generated (the opening comment warns against manual edits) and exposes two cooperating classes: `LoanBrokerDelete`, a read-only wrapper around an existing `STTx`, and `LoanBrokerDeleteBuilder`, a fluent builder that constructs and signs new transactions of this type. + +`LoanBrokerDelete` represents transaction type `ttLOAN_BROKER_DELETE` (numeric code 75), introduced under the `featureLendingProtocol` amendment. Its purpose within the lending protocol is to remove an existing LoanBroker ledger object, identified by its 256-bit hash (`sfLoanBrokerID`). Comparing it to the sibling `LoanBrokerSet` (type 74), which creates a broker and carries a rich set of optional configuration fields (`sfVaultID`, `sfDebtMaximum`, `sfManagementFeeRate`, etc.), the delete transaction is intentionally minimal: the only transaction-specific field is the broker's ID. This mirrors the general XRPL pattern where creation transactions are richer and deletion transactions carry just enough to identify the object to remove. + +## Design: Two-Class Split (Wrapper + Builder) + +The file enforces a sharp read/write boundary through two separate classes rather than a single mutable type: + +`LoanBrokerDelete` is immutable. It holds a `std::shared_ptr` inherited from `TransactionBase` and exposes only `[[nodiscard]]` const accessors. Callers cannot modify a `LoanBrokerDelete` after construction. The constructor validates the transaction type immediately and throws `std::runtime_error` if it doesn't match `ttLOAN_BROKER_DELETE` — this is the first line of defense against accidentally wrapping the wrong wire-format object. + +`LoanBrokerDeleteBuilder` is mutable. It inherits from `TransactionBuilderBase` via the Curiously Recurring Template Pattern (CRTP), which lets the base class's common setters (`setFee()`, `setSequence()`, `setMemos()`, etc.) return a `LoanBrokerDeleteBuilder&` instead of a base-class reference, preserving the fluent-chaining interface. The builder holds a plain `STObject object_{sfTransaction}` internally — not wrapped in `const` — so fields can be mutated freely until `build()` is called. + +The terminal step `build(publicKey, secretKey)` calls `sign()` (inherited from the base), which serializes the object with `HashPrefix::txSign`, signs it with the secret key, embeds the signature and public key into `object_`, then moves `object_` into a freshly constructed `STTx` and wraps it in a `LoanBrokerDelete`. At that point the signed transaction becomes immutable and the builder's `object_` is moved away. + +## Key Design Decisions + +**Required field enforced at construction.** `sfLoanBrokerID` is `soeREQUIRED` in the schema. The builder constructor takes it as a mandatory parameter (not `std::optional`), so a `LoanBrokerDeleteBuilder` cannot exist in a state where the field is absent. In contrast, `sequence` and `fee` are optional at construction time because they may be auto-populated by server-side logic in some submission paths. + +**`std::decay_t` for the 256-bit ID parameter.** The setter and constructor accept `std::decay_t const&` rather than the raw `value_type` directly. This strips reference and cv-qualifiers from the template argument, ensuring a consistent value-category regardless of how the underlying `SF_UINT256` type resolves — a defensive measure against implicit reference collisions in generic code. + +**`mustDeleteAcct` privilege flag.** Unlike `LoanBrokerSet`, which carries `createPseudoAcct | mayAuthorizeMPT`, `LoanBrokerDelete` is marked `mustDeleteAcct | mayAuthorizeMPT`. The `mustDeleteAcct` constraint means the transaction must reclaim the owner reserve held by the broker's ledger object — a restriction enforced at the transaction-application layer, not here. The wrapper layer is agnostic to these semantic rules; they exist in the amendment's apply logic. + +**Not delegable.** The `Delegation::notDelegable` annotation means this transaction cannot be submitted through the `sfDelegate` field mechanism. The `TransactionBase` base class still exposes `getDelegate()` and `hasDelegate()` for completeness (they are part of every transaction's potential field set), but the ledger will reject any `LoanBrokerDelete` that includes a non-empty delegate. + +## Relationship to Sibling Classes + +The five `LoanBroker*` transaction headers form the full lifecycle of a loan broker: + +- `LoanBrokerSet.h` (type 74) — create or modify a broker configuration +- `LoanBrokerDelete.h` (type 75) — remove a broker +- `LoanBrokerCoverDeposit.h`, `LoanBrokerCoverWithdraw.h`, `LoanBrokerCoverClawback.h` — manage collateral cover positions associated with a broker + +All five follow the same two-class pattern and share the same `TransactionBase` / `TransactionBuilderBase` infrastructure. The auto-generated nature of these files is architecturally significant: any change to the lending protocol schema (a new field, a changed optionality) is reflected by regenerating the file rather than requiring hand-edits, keeping the C++ API in lock-step with the protocol definition. + +## Error Handling + +Both constructors (on `LoanBrokerDelete` and `LoanBrokerDeleteBuilder`) perform an immediate type check and throw `std::runtime_error` on mismatch. `TransactionBase::validate()` can be called on the wrapper after construction to run the full schema check via `validateSTObject()` and `passesLocalChecks()`, catching field-level constraint violations that the constructor does not inspect. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h.ai.json new file mode 100644 index 0000000000..43b88508e5 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h.ai.json @@ -0,0 +1,210 @@ +{ + "args": [ + { + "lineno": 32, + "name": "tx" + }, + { + "lineno": 196, + "name": "account" + }, + { + "lineno": 196, + "name": "vaultID" + }, + { + "lineno": 196, + "name": "sequence" + }, + { + "lineno": 196, + "name": "fee" + }, + { + "lineno": 287, + "name": "publicKey" + }, + { + "lineno": 287, + "name": "secretKey" + }, + { + "lineno": 217, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 31, + "name": "LoanBrokerSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& vaultID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 193, + "name": "LoanBrokerSetBuilder" + } + ], + "description": "Defines the LoanBrokerSet transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing such transactions with specific fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 32, + "name": "LoanBrokerSet" + }, + { + "args": [], + "lineno": 45, + "name": "getVaultID" + }, + { + "args": [], + "lineno": 56, + "name": "getLoanBrokerID" + }, + { + "args": [], + "lineno": 67, + "name": "hasLoanBrokerID" + }, + { + "args": [], + "lineno": 78, + "name": "getData" + }, + { + "args": [], + "lineno": 89, + "name": "hasData" + }, + { + "args": [], + "lineno": 100, + "name": "getManagementFeeRate" + }, + { + "args": [], + "lineno": 111, + "name": "hasManagementFeeRate" + }, + { + "args": [], + "lineno": 122, + "name": "getDebtMaximum" + }, + { + "args": [], + "lineno": 133, + "name": "hasDebtMaximum" + }, + { + "args": [], + "lineno": 144, + "name": "getCoverRateMinimum" + }, + { + "args": [], + "lineno": 155, + "name": "hasCoverRateMinimum" + }, + { + "args": [], + "lineno": 166, + "name": "getCoverRateLiquidation" + }, + { + "args": [], + "lineno": 177, + "name": "hasCoverRateLiquidation" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& vaultID", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 196, + "name": "LoanBrokerSetBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 205, + "name": "LoanBrokerSetBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 217, + "name": "setVaultID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 227, + "name": "setLoanBrokerID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 237, + "name": "setData" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 247, + "name": "setManagementFeeRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 257, + "name": "setDebtMaximum" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 267, + "name": "setCoverRateMinimum" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 277, + "name": "setCoverRateLiquidation" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 287, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h.ai.md new file mode 100644 index 0000000000..83c89bea2b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanBrokerSet.h.ai.md @@ -0,0 +1,47 @@ +# `LoanBrokerSet.h` — Auto-generated Transaction Wrapper for the Lending Protocol + +## Role and Context + +This file is part of the `protocol_autogen` layer — a set of machine-generated C++ headers that wrap XRPL's generic `STTx`/`STObject` machinery in type-safe, self-documenting classes. It resides in `include/xrpl/protocol_autogen/transactions/` alongside analogous files for every other transaction kind in the ledger. + +`LoanBrokerSet` is transaction type `ttLOAN_BROKER_SET` (74), introduced under the `featureLendingProtocol` amendment. Its job is twofold: it both **creates** a new `LoanBroker` ledger object and **updates** an existing one, with the mode determined by the presence or absence of the optional `sfLoanBrokerID` field. A `LoanBroker` acts as a managed entity that sits between a `Vault` (a pooled-asset lending vault) and individual borrowers, controlling per-broker debt limits, fee rates, and collateral coverage thresholds. Because it requires the ability to spawn a pseudo-account and may authorize MPToken issuances, the transaction carries `createPseudoAcct | mayAuthorizeMPT` privileges, and it is explicitly marked `notDelegable`. + +## Class Structure + +The file follows the standard pattern used across all `protocol_autogen` transaction headers: a paired `LoanBrokerSet` (read-only wrapper) and `LoanBrokerSetBuilder` (fluent construction). + +### `LoanBrokerSet : TransactionBase` + +`TransactionBase` holds a `std::shared_ptr` called `tx_` and exposes accessors for every common transaction field (`sfAccount`, `sfSequence`, `sfFee`, `sfFlags`, `sfSigners`, etc.). `LoanBrokerSet` extends this with accessors for the six fields specific to this transaction type. + +The constructor takes a pre-existing `shared_ptr` and immediately validates `tx_->getTxnType() == ttLOAN_BROKER_SET`, throwing `std::runtime_error` on mismatch. This is necessary because `STTx` is a single, type-erased container for every transaction variant — the check converts a latent runtime hazard into an immediate, diagnosable failure at the construction site. + +Each field is exposed as a `[[nodiscard]]` `get*()` method and, for optional fields, a companion `has*()` predicate: + +- **`getVaultID()`** — Returns `SF_UINT256::type::value_type` (a `uint256`). This field is `soeREQUIRED` in the schema; every `LoanBrokerSet` must target a specific vault. +- **`getLoanBrokerID()` / `hasLoanBrokerID()`** — Optional `uint256`. When present, identifies the existing `LoanBroker` object to modify. When absent, the transaction is in *create* mode. +- **`getData()` / `hasData()`** — Optional `SF_VL` blob. Arbitrary metadata the operator can attach to the broker (subject to a maximum byte length enforced in `preflight`). +- **`getManagementFeeRate()` / `hasManagementFeeRate()`** — Optional `uint16`. A percentage-style rate the broker charges; valid only during creation — the transactor's `preflight` rejects this field if `sfLoanBrokerID` is also present, making `sfManagementFeeRate` effectively immutable after creation. +- **`getDebtMaximum()` / `hasDebtMaximum()`** — Optional `SF_NUMBER`. A ceiling on the aggregate outstanding debt this broker can hold. Unlike the rate fields, `sfDebtMaximum` is updatable but constrained: `preclaim` in the transactor prevents lowering it below the current `sfDebtTotal`. +- **`getCoverRateMinimum()` / `hasCoverRateMinimum()`** — Optional `uint32`. The minimum collateral-to-debt ratio before new loans may be issued. Creation-only, like the management fee. +- **`getCoverRateLiquidation()` / `hasCoverRateLiquidation()`** — Optional `uint32`. The collateral ratio at which positions become eligible for liquidation. Must always be specified alongside `sfCoverRateMinimum` (both zero or both non-zero), also creation-only. + +The `[[nodiscard]]` attribute on every accessor is deliberate: callers that ignore a returned optional silently lose the field value, which is almost always a logic bug. + +### `LoanBrokerSetBuilder : TransactionBuilderBase` + +`TransactionBuilderBase` is a CRTP template that holds a mutable `STObject object_{sfTransaction}` and returns `Derived&` from every setter, enabling fluent chaining. Deliberately, the base constructor does *not* call `object_.set(soTemplate)` — keeping the `STObject` in "free" mode avoids pre-populating default-valued `STBase` placeholders that would later trigger `applyTemplate()`'s "may not be explicitly set to default" assertion when the `STTx` constructor processes the object. + +`LoanBrokerSetBuilder` requires `account` and `vaultID` at construction time (enforcing the schema's `soeREQUIRED` constraint at the C++ level) while accepting optional `sequence` and `fee`. A second constructor accepts an existing `shared_ptr` and copies its `STObject` representation, enabling round-trip editing of a transaction that was previously built or deserialized. + +The `build(publicKey, secretKey)` method finalizes construction: it calls the protected `sign()` method inherited from `TransactionBuilderBase`, which serializes the `STObject` (excluding signing fields) with the `HashPrefix::txSign` prefix, computes the ECDSA/Ed25519 signature, embeds both `sfSigningPubKey` and `sfTxnSignature` into the object, then moves the signed `STObject` into a freshly allocated `STTx`, wraps it in a `shared_ptr`, and constructs the immutable `LoanBrokerSet` wrapper. + +## Dual-Mode Semantics and Immutability Design + +The create-vs-update duality encoded in `sfLoanBrokerID` is a notable design choice. An alternative would be separate `LoanBrokerCreate` and `LoanBrokerUpdate` transactions, but the single-transaction approach saves a transaction type slot and keeps the related logic co-located in the transactor. The price is a slightly more complex preflight — the transactor must explicitly check which fields are legal in each mode and reject cross-mode combinations. + +The immutability of `sfManagementFeeRate`, `sfCoverRateMinimum`, and `sfCoverRateLiquidation` after creation is enforced only at the transactor layer (`LoanBrokerSet.cpp`), not at the `STTx` schema layer. The `LoanBrokerSetBuilder` will happily accept these fields in update mode; it is the `preflight` check that rejects them. This is a standard XRPL pattern — the `protocol_autogen` wrappers describe *what* can be present structurally, while the transactor enforces *when* it is permissible. + +## Relationship to Other Files + +`LoanBrokerSet` integrates into a family of Lending Protocol transactions: `LoanBrokerDelete`, `LoanBrokerCoverDeposit`, `LoanBrokerCoverWithdraw`, and `LoanBrokerCoverClawback`. The corresponding `LoanBroker` ledger entry (in `ledger_entries/LoanBroker.h`) holds the fields that `LoanBrokerSet` initializes or updates, including `sfDebtTotal`, `sfCoverAvailable`, `sfLoanSequence`, and the pseudo-account's `sfAccount` field — none of which appear in the transaction schema because they are managed exclusively by the ledger engine, not by the submitting account. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanDelete.h.ai.json b/include/xrpl/protocol_autogen/transactions/LoanDelete.h.ai.json new file mode 100644 index 0000000000..bd56a809e8 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanDelete.h.ai.json @@ -0,0 +1,112 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 62, + "name": "account" + }, + { + "lineno": 62, + "name": "loanID" + }, + { + "lineno": 63, + "name": "sequence" + }, + { + "lineno": 64, + "name": "fee" + }, + { + "lineno": 72, + "name": "tx" + }, + { + "lineno": 83, + "name": "value" + }, + { + "lineno": 91, + "name": "publicKey" + }, + { + "lineno": 91, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "LoanDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& loanID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 59, + "name": "LoanDeleteBuilder" + } + ], + "description": "Defines the LoanDelete transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing LoanDelete transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LoanDelete.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "LoanDelete" + }, + { + "args": [], + "lineno": 44, + "name": "getLoanID" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& loanID", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 62, + "name": "LoanDeleteBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 72, + "name": "LoanDeleteBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 83, + "name": "setLoanID" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 91, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanDelete.h.ai.md b/include/xrpl/protocol_autogen/transactions/LoanDelete.h.ai.md new file mode 100644 index 0000000000..6a2143ba5f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanDelete.h.ai.md @@ -0,0 +1,31 @@ +# `LoanDelete.h` — Auto-generated LendingProtocol Transaction Wrapper + +`LoanDelete.h` is part of the `protocol_autogen` subsystem under `include/xrpl/protocol_autogen/transactions/`. The entire file is code-generated and carries a "Do not edit" comment at the top, which signals that the source of truth is an upstream schema or DSL rather than this C++ text itself. It defines the `ttLOAN_DELETE` transaction (type value 81), the teardown operation for the `featureLendingProtocol` amendment, and it follows the same structural template used throughout the autogen layer: a paired immutable wrapper class plus a fluent builder. + +## Role in the Lending Protocol + +Loans on the XRP Ledger are first-class ledger objects created by `LoanSet` (type 80). Once created, a loan has a unique 256-bit identifier stored in `sfLoanID`. `LoanDelete` is the mechanism for removing that ledger object. Compared to `LoanSet`, which carries over a dozen optional fields (interest rates, fees, payment schedules, counterparty signatures), `LoanDelete` is deliberately minimal: the only transaction-specific field is `sfLoanID`, and it is required. Nothing else is needed to unambiguously target a loan for deletion, which makes the schema intentionally narrow. + +The transaction is marked `notDelegable` — meaning no account may submit it on behalf of another via the XRPL delegation mechanism. It also carries `noPriv`, distinguishing it from `LoanSet` which declares `mayAuthorizeMPT | mustModifyVault` privileges. The absence of privilege flags in `LoanDelete` means the transaction engine applies no vault or MPT authorization logic beyond ordinary account validation, consistent with the idea that closing a loan primarily affects the account that holds it. + +## Class Design: Wrapper vs. Builder + +The file contains two complementary classes that deliberately separate reading from writing. + +`LoanDelete` extends `TransactionBase` and wraps a `std::shared_ptr` — the `const` qualifier in the pointee type is load-bearing. It is physically impossible to mutate the underlying serialized transaction through this class. All getter methods are marked `[[nodiscard]]` to prevent accidental ignored-return bugs at call sites. The class exposes exactly one transaction-specific accessor, `getLoanID()`, which directly calls `tx_->at(sfLoanID)` without an optional wrapper because the field is `soeREQUIRED` in the schema — if the field is absent the underlying `STTx` access would throw, which is the correct behavior for a required field. + +`LoanDeleteBuilder` extends the CRTP base `TransactionBuilderBase`. The CRTP pattern allows the base class's fluent setters (`setAccount`, `setFee`, `setSequence`, `setFlags`, etc.) to return `Derived&` rather than `TransactionBuilderBase&`, preserving the concrete type across method chains without virtual dispatch or replication of setter code. The builder accumulates state in an `STObject object_` member (declared in the base as `STObject object_{sfTransaction}`). This is a free `STObject`, not bound to any `SOTemplate`, which is intentional: binding too early would cause `applyTemplate()` to throw when `soeDEFAULT` fields are absent. The `STTx` constructor receives this free object and applies the template during construction. + +## Construction Paths + +The builder offers two entry points. The primary constructor takes the required fields by value — `account` as `SF_ACCOUNT::type::value_type` and `loanID` as `std::decay_t const&`. The `std::decay_t` wrapper strips references and cv-qualifiers from the CRTP-deduced template type, preventing reference collapsing issues that arise in generated code that needs to accept both value and reference arguments uniformly. The optional `sequence` and `fee` parameters are forwarded to the base and set only when present, consistent with the builder pattern of deferring optional fields. + +The second constructor takes an existing `std::shared_ptr` and copies the raw `STTx` into `object_`. This path exists for modification workflows where a caller deserializes an on-ledger transaction and wants to re-sign a modified version. Both constructors guard against type mismatches by checking `getTxnType() != ttLOAN_DELETE` and throwing `std::runtime_error`. This eagerly surfaces misuse rather than allowing a builder or wrapper to silently operate on the wrong transaction type, which could otherwise produce a malformed signed transaction that would be rejected by network validators. + +## Build and Sign + +`build(PublicKey const& publicKey, SecretKey const& secretKey)` finalizes the transaction. It calls the protected `sign()` method inherited from `TransactionBuilderBase`, which sets `sfSigningPubKey`, serializes the object without signing fields, prepends `HashPrefix::txSign`, signs with the provided secret key, and stores the resulting signature in `sfTxnSignature`. After signing, `build()` moves `object_` into a new `STTx` and wraps it in a `LoanDelete` instance. The move semantics here are deliberate: the builder is consumed, preventing any further mutation after the transaction has been signed and crystallized into the immutable wrapper. + +## Relationship to Other Files + +`TransactionBase.h` provides the common read-only field accessors shared across all transaction wrappers (account, sequence, fee, flags, memos, signers, etc.), so `LoanDelete` only needs to implement `getLoanID()` itself. `TransactionBuilderBase.h` provides all common builder setters in the same spirit. This means `LoanDelete.h` is genuinely minimal — the autogenerator only emits what is unique to this transaction type. Sibling files like `LoanManage.h`, `LoanPay.h`, and `LoanBrokerSet.h` follow the identical structural pattern, making the entire lending transaction family consistent and easy to navigate once you understand this template. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanManage.h.ai.json b/include/xrpl/protocol_autogen/transactions/LoanManage.h.ai.json new file mode 100644 index 0000000000..07bdc3535d --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanManage.h.ai.json @@ -0,0 +1,114 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 62, + "name": "account" + }, + { + "lineno": 62, + "name": "loanID" + }, + { + "lineno": 62, + "name": "sequence" + }, + { + "lineno": 62, + "name": "fee" + }, + { + "lineno": 72, + "name": "tx" + }, + { + "lineno": 82, + "name": "value" + }, + { + "lineno": 91, + "name": "publicKey" + }, + { + "lineno": 91, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "LoanManage" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& loanID", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 61, + "name": "LoanManageBuilder" + } + ], + "description": "Defines the LoanManage transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing LoanManage transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LoanManage.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "LoanManage" + }, + { + "args": [], + "lineno": 44, + "name": "getLoanID" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& loanID", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 62, + "name": "LoanManageBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 72, + "name": "LoanManageBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 82, + "name": "setLoanID" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 91, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanManage.h.ai.md b/include/xrpl/protocol_autogen/transactions/LoanManage.h.ai.md new file mode 100644 index 0000000000..3188915a09 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanManage.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/protocol_autogen/transactions/LoanManage.h` + +This file is part of the auto-generated transaction layer introduced by the `featureLendingProtocol` amendment. It defines two cooperating classes — `LoanManage` and `LoanManageBuilder` — that together provide the complete lifecycle interface for the `ttLOAN_MANAGE` (type 82) transaction: a read-only, type-safe wrapper for inspection and a fluent builder for construction and signing. + +## Role in the LendingProtocol System + +`LoanManage` is the administrative lifecycle transaction for loans originated through the XLS-66 Lending Protocol. While `LoanSet` creates a loan and `LoanPay` services it, `LoanManage` is the mechanism through which a Loan Broker formally changes the *health status* of a loan — marking it as impaired, unimpaired, or in default. This transaction may only be submitted by the account that owns the `LoanBroker` object associated with the loan; neither the borrower nor any third party can submit it, and it is explicitly marked `notDelegable`, preventing delegation to other accounts even through the `sfDelegate` mechanism. + +The minimal transaction schema — a single required field, `sfLoanID` — is deliberate. The actual operation is encoded entirely in the transaction's flags (`tfLoanDefault`, `tfLoanImpair`, `tfLoanUnimpair`), which are mutually exclusive. This design keeps the ledger-level record slim while making the intent unambiguous. A `LoanManage` submitted with no flags is a valid no-op. + +## `LoanManage` — Immutable Wrapper + +`LoanManage` inherits from `TransactionBase`, which holds a `std::shared_ptr` and exposes type-safe getters for all universal transaction fields (`sfAccount`, `sfSequence`, `sfFee`, `sfFlags`, `sfDelegate`, etc.). The subclass adds only `getLoanID()`, which returns the `uint256` value of `sfLoanID` directly from the underlying `STTx` via `tx_->at(sfLoanID)`. + +Construction validates the transaction type at runtime: if the passed `STTx` is not a `ttLOAN_MANAGE`, the constructor throws `std::runtime_error`. This check is intentional because `STTx` objects travel the system as type-erased shared pointers, and the wrapper must guarantee its static type assumption before exposing typed accessors. The `[[nodiscard]]` attribute on `getLoanID()` follows the pattern used uniformly across this layer to discourage silently discarded values. + +## `LoanManageBuilder` — Fluent Construction + +`LoanManageBuilder` inherits from `TransactionBuilderBase`, a CRTP template that supplies setters for all universal fields and stores state in an `STObject object_` member. Returning `Derived&` from every setter enables method chaining without virtual dispatch overhead. + +The primary constructor takes `account` and `loanID` as required arguments, with `sequence` and `fee` as `std::optional` parameters defaulting to `std::nullopt`. This reflects real-world usage where sequence and fee are often provided separately (e.g., resolved from account state by a client SDK). `setLoanID()` uses `std::decay_t` to strip references and cv-qualifiers from the value type, ensuring the stored `object_` field receives a copy rather than a dangling reference. + +A secondary constructor accepts an existing `std::shared_ptr` to allow round-tripping: copying a previously serialized transaction back into a builder for modification. It performs the same type guard as the `LoanManage` wrapper's constructor. + +The `build()` method calls the protected `sign()` helper from `TransactionBuilderBase`, which serializes the `STObject` without signing fields, prepends the `HashPrefix::txSign` tag, computes the signature, and stores it in `sfTxnSignature`. The object is then moved into an `STTx` (whose constructor calls `applyTemplate()` to validate the field set against the registered `TxFormats` schema) and wrapped in an immutable `LoanManage`. The fact that the builder's `object_` is **not** pre-initialized with an `SOTemplate` is a deliberate choice documented in `TransactionBuilderBase`: pre-initializing would create `soeDEFAULT` placeholder fields that cause `applyTemplate()` to throw when it encounters them as explicitly set. + +## Transactor Behavior (from `LoanManage.cpp`) + +The header's thin interface belies the financial complexity of the underlying transactor. `preflight()` rejects a zero-valued `sfLoanID` and enforces mutual exclusivity of the three flags via a bitmask check (`flags & (flags - 1)` is non-zero if more than one bit is set). `preclaim()` verifies the loan exists, looks up the associated `LoanBroker`, and confirms the submitting account is the broker owner. It also enforces a one-way state machine: a defaulted loan can never be modified; an impaired loan cannot be impaired again; an unimpaired loan cannot be unimpaired. + +In `doApply()`, the flag determines which of three internal methods executes: + +- **`impairLoan()`** books an unrealized loss on the `Vault` (`sfLossUnrealized`), sets `lsfLoanImpaired` on the Loan ledger object, and advances `sfNextPaymentDueDate` to now if the payment isn't already overdue — effectively starting the grace-period clock. +- **`unimpairLoan()`** reverses the unrealized loss, clears `lsfLoanImpaired`, and recalculates the next payment due date based on the payment interval. +- **`defaultLoan()`** applies the Loan Broker's first-loss capital (bounded by `sfCoverRateLiquidation` and `sfCoverRateMinimum`) against the amount owed to the vault, transfers those funds from the broker's pseudo-account back to the vault's pseudo-account, zeroes out all outstanding amounts on the Loan object, and sets `lsfLoanDefault` — a terminal state. + +## Design Observations + +The separation between the header-level wrapper/builder pair and the transactor implementation is characteristic of the whole `protocol_autogen` layer: the generated files handle field access and construction in a type-safe, schema-driven way, while the `tx/transactors/lending/` implementation handles business rules. The auto-generated comment (`// This file is auto-generated. Do not edit.`) signals that the field layout, accessor signatures, and builder constructors are derived from a schema definition rather than hand-authored — edits must be made upstream in that schema. + +The `mayModifyVault` privilege annotation in the class Doxygen is meaningful at the amendment-enforcement layer: it allows the transaction to write to `Vault` objects it does not own, which is necessary because impairment and default must update the vault's `sfLossUnrealized` and `sfAssetsTotal` accounting fields even though the vault's owner is a separate account from the Loan Broker submitting the transaction. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanPay.h.ai.json b/include/xrpl/protocol_autogen/transactions/LoanPay.h.ai.json new file mode 100644 index 0000000000..15691362c3 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanPay.h.ai.json @@ -0,0 +1,125 @@ +{ + "args": [ + { + "lineno": 28, + "name": "tx" + }, + { + "lineno": 74, + "name": "account" + }, + { + "lineno": 75, + "name": "loanID" + }, + { + "lineno": 75, + "name": "amount" + }, + { + "lineno": 76, + "name": "sequence" + }, + { + "lineno": 77, + "name": "fee" + }, + { + "lineno": 115, + "name": "publicKey" + }, + { + "lineno": 115, + "name": "secretKey" + }, + { + "lineno": 97, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 25, + "name": "LoanPay" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& loanID, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 68, + "name": "LoanPayBuilder" + } + ], + "description": "Defines the LoanPay transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing LoanPay transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LoanPay.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "LoanPay" + }, + { + "args": [], + "lineno": 43, + "name": "getLoanID" + }, + { + "args": [], + "lineno": 54, + "name": "getAmount" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& loanID", + "std::decay_t const& amount", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 74, + "name": "LoanPayBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 86, + "name": "LoanPayBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 97, + "name": "setLoanID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 106, + "name": "setAmount" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 115, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanPay.h.ai.md b/include/xrpl/protocol_autogen/transactions/LoanPay.h.ai.md new file mode 100644 index 0000000000..eeb3b58e36 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanPay.h.ai.md @@ -0,0 +1,42 @@ +# `LoanPay.h` — Loan Payment Transaction for the XRPL Lending Protocol + +This file is an **auto-generated** header (do not edit manually) that defines the `LoanPay` transaction type within the `xrpl::transactions` namespace. It is one of a suite of transaction types introduced by the `featureLendingProtocol` amendment, providing the on-ledger DeFi lending lifecycle on XRPL. `LoanPay` represents the payment side of that lifecycle: a borrower directing funds toward an outstanding loan. + +## Role in the Lending Protocol + +The `featureLendingProtocol` amendment introduces at minimum five related transaction types grouped in the `protocol_autogen/transactions/` directory: `LoanSet` (tt 80, loan origination), `LoanManage` (tt 82, lifecycle management), `LoanPay` (tt 84, repayment), `LoanDelete` (loan closure), and a family of `LoanBroker*` transactions for the broker entity. `LoanPay` sits specifically at transaction type `ttLOAN_PAY` (code 84) and carries two system-level privilege flags — `mayAuthorizeMPT | mustModifyVault` — reflecting that a repayment both requires authorization to move Multi-Purpose Tokens and must atomically update the lending vault's state. This same privilege combination appears on `LoanSet` but not on the simpler `LoanManage`, which only holds `mayModifyVault`. That asymmetry makes sense: creating a loan and repaying one both move tokens through vault accounting, while managing loan terms does not. + +The `Delegation::notDelegable` marker means no third party can submit this transaction on behalf of the originating account via the delegation mechanism — the borrower must sign directly. + +## Class Structure: Wrapper and Builder + +The file follows the pattern used uniformly across all `protocol_autogen` transaction types: a paired **immutable read-only wrapper** (`LoanPay`) and a **fluent mutable builder** (`LoanPayBuilder`). This separation is architecturally deliberate. Transactions on XRPL are value objects once serialized — immutability at the wrapper layer prevents accidental mutation after signing, which would invalidate the signature. The builder, by contrast, accumulates field assignments into an `STObject` before the transaction is finalized and signed. + +## `LoanPay` — The Immutable Wrapper + +`LoanPay` inherits from `TransactionBase`, which holds a `std::shared_ptr` and provides accessors for all universal transaction fields (`sfAccount`, `sfSequence`, `sfFee`, `sfFlags`, `sfMemos`, multi-signing fields, etc.). `LoanPay` adds only two transaction-specific accessors: + +- `getLoanID()` — returns the `sfLoanID` as a `uint256` value, identifying which on-ledger loan object this payment targets. +- `getAmount()` — returns the `sfAmount` field, annotated explicitly as supporting MPT (Multi-Purpose Token) amounts. This is significant: the lending protocol is built around MPTs, XRPL's newer extensible token standard, so repayment amounts may be denominated in an MPT rather than XRP or an IOU. + +The constructor performs an eager type-check: if the wrapped `STTx` does not carry `ttLOAN_PAY`, it throws `std::runtime_error` immediately. This guards against misuse of a generic `STTx` pointer to construct a `LoanPay` from a different transaction type — a defensive pattern applied identically across the entire `protocol_autogen` family. + +Both getters are marked `[[nodiscard]]` and `const`, reinforcing the immutable contract. + +## `LoanPayBuilder` — The Fluent Builder + +`LoanPayBuilder` inherits from `TransactionBuilderBase`, which uses CRTP (Curiously Recurring Template Pattern) so that setters inherited from the base return a `LoanPayBuilder&` rather than a `TransactionBuilderBase&`, preserving the fluent method-chaining interface without virtual dispatch overhead. + +The builder has two construction paths: + +1. **From scratch**: Takes `account`, `loanID`, `amount`, and optionally `sequence` and `fee`. The sequence and fee are optional because they may be auto-populated by a node on submission. Internally, the constructor delegates to `TransactionBuilderBase` (which sets `sfTransactionType`, `sfAccount`, and optionally `sfSequence`/`sfFee`), then calls `setLoanID()` and `setAmount()` immediately to satisfy the required-field invariant before `build()` is called. + +2. **From an existing `STTx`**: Takes a `std::shared_ptr` and copies its `STObject` content into `object_`. This path enables round-tripping — reconstructing a builder from a previously signed or deserialized transaction, for example when resubmitting or inspecting a failed transaction. It performs the same type-check guard and throws on mismatch. + +The `build()` method calls the protected `sign()` from `TransactionBuilderBase`, which serializes the accumulated `STObject` with `HashPrefix::txSign`, signs the result with the provided `PublicKey`/`SecretKey` pair, sets both `sfSigningPubKey` and `sfTxnSignature` on the object, then constructs a `std::shared_ptr` and wraps it in a `LoanPay` instance. The `STObject` is moved into the `STTx`, so the builder is left in a valid-but-consumed state after `build()`. + +## Design Notes + +The use of `std::decay_t` for `loanID` rather than a plain `uint256` is defensive: it strips reference and cv-qualifiers from whatever the `SField` type alias exposes, ensuring no accidental implicit conversions or dangling references when the value is stored into the `STObject`. This pattern appears consistently across every autogenerated builder in the module. + +The comment in `TransactionBuilderBase` explicitly warns against calling `object_.set(soTemplate)` to pre-populate the `STObject` with default field placeholders, because doing so would cause `STTx::applyTemplate()` to throw on fields with `soeDEFAULT` cardinality. The builder deliberately leaves `object_` as a free `STObject{sfTransaction}` and lets the `STTx` constructor handle template application — a non-obvious but critical design constraint that this generated file inherits without needing to document locally. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/LoanSet.h.ai.json new file mode 100644 index 0000000000..0ed1e3038d --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanSet.h.ai.json @@ -0,0 +1,448 @@ +{ + "args": [ + { + "lineno": 32, + "name": "tx" + }, + { + "lineno": 374, + "name": "account" + }, + { + "lineno": 375, + "name": "loanBrokerID" + }, + { + "lineno": 376, + "name": "principalRequested" + }, + { + "lineno": 377, + "name": "sequence" + }, + { + "lineno": 378, + "name": "fee" + }, + { + "lineno": 386, + "name": "tx" + }, + { + "lineno": 398, + "name": "value" + }, + { + "lineno": 407, + "name": "value" + }, + { + "lineno": 416, + "name": "value" + }, + { + "lineno": 425, + "name": "value" + }, + { + "lineno": 434, + "name": "value" + }, + { + "lineno": 443, + "name": "value" + }, + { + "lineno": 452, + "name": "value" + }, + { + "lineno": 461, + "name": "value" + }, + { + "lineno": 470, + "name": "value" + }, + { + "lineno": 479, + "name": "value" + }, + { + "lineno": 488, + "name": "value" + }, + { + "lineno": 497, + "name": "value" + }, + { + "lineno": 506, + "name": "value" + }, + { + "lineno": 515, + "name": "value" + }, + { + "lineno": 524, + "name": "value" + }, + { + "lineno": 533, + "name": "value" + }, + { + "lineno": 542, + "name": "value" + }, + { + "lineno": 551, + "name": "publicKey" + }, + { + "lineno": 551, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "LoanSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& loanBrokerID, std::decay_t const& principalRequested, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 371, + "name": "LoanSetBuilder" + } + ], + "description": "Defines the LoanSet transaction type for the XRPL lending protocol, providing a type-safe wrapper and a builder for constructing and accessing transaction fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/LoanSet.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 32, + "name": "LoanSet" + }, + { + "args": [], + "lineno": 46, + "name": "getLoanBrokerID" + }, + { + "args": [], + "lineno": 56, + "name": "getData" + }, + { + "args": [], + "lineno": 67, + "name": "hasData" + }, + { + "args": [], + "lineno": 77, + "name": "getCounterparty" + }, + { + "args": [], + "lineno": 88, + "name": "hasCounterparty" + }, + { + "args": [], + "lineno": 97, + "name": "getCounterpartySignature" + }, + { + "args": [], + "lineno": 108, + "name": "hasCounterpartySignature" + }, + { + "args": [], + "lineno": 117, + "name": "getLoanOriginationFee" + }, + { + "args": [], + "lineno": 128, + "name": "hasLoanOriginationFee" + }, + { + "args": [], + "lineno": 137, + "name": "getLoanServiceFee" + }, + { + "args": [], + "lineno": 148, + "name": "hasLoanServiceFee" + }, + { + "args": [], + "lineno": 157, + "name": "getLatePaymentFee" + }, + { + "args": [], + "lineno": 168, + "name": "hasLatePaymentFee" + }, + { + "args": [], + "lineno": 177, + "name": "getClosePaymentFee" + }, + { + "args": [], + "lineno": 188, + "name": "hasClosePaymentFee" + }, + { + "args": [], + "lineno": 197, + "name": "getOverpaymentFee" + }, + { + "args": [], + "lineno": 208, + "name": "hasOverpaymentFee" + }, + { + "args": [], + "lineno": 217, + "name": "getInterestRate" + }, + { + "args": [], + "lineno": 228, + "name": "hasInterestRate" + }, + { + "args": [], + "lineno": 237, + "name": "getLateInterestRate" + }, + { + "args": [], + "lineno": 248, + "name": "hasLateInterestRate" + }, + { + "args": [], + "lineno": 257, + "name": "getCloseInterestRate" + }, + { + "args": [], + "lineno": 268, + "name": "hasCloseInterestRate" + }, + { + "args": [], + "lineno": 277, + "name": "getOverpaymentInterestRate" + }, + { + "args": [], + "lineno": 288, + "name": "hasOverpaymentInterestRate" + }, + { + "args": [], + "lineno": 297, + "name": "getPrincipalRequested" + }, + { + "args": [], + "lineno": 307, + "name": "getPaymentTotal" + }, + { + "args": [], + "lineno": 318, + "name": "hasPaymentTotal" + }, + { + "args": [], + "lineno": 327, + "name": "getPaymentInterval" + }, + { + "args": [], + "lineno": 338, + "name": "hasPaymentInterval" + }, + { + "args": [], + "lineno": 347, + "name": "getGracePeriod" + }, + { + "args": [], + "lineno": 358, + "name": "hasGracePeriod" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& loanBrokerID", + "std::decay_t const& principalRequested", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 374, + "name": "LoanSetBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 386, + "name": "LoanSetBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 397, + "name": "setLoanBrokerID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 406, + "name": "setData" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 415, + "name": "setCounterparty" + }, + { + "args": [ + "STObject const& value" + ], + "lineno": 424, + "name": "setCounterpartySignature" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 433, + "name": "setLoanOriginationFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 442, + "name": "setLoanServiceFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 451, + "name": "setLatePaymentFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 460, + "name": "setClosePaymentFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 469, + "name": "setOverpaymentFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 478, + "name": "setInterestRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 487, + "name": "setLateInterestRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 496, + "name": "setCloseInterestRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 505, + "name": "setOverpaymentInterestRate" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 514, + "name": "setPrincipalRequested" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 523, + "name": "setPaymentTotal" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 532, + "name": "setPaymentInterval" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 541, + "name": "setGracePeriod" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 550, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/LoanSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/LoanSet.h.ai.md new file mode 100644 index 0000000000..596c62c00c --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/LoanSet.h.ai.md @@ -0,0 +1,72 @@ +# `LoanSet.h` — Lending Protocol Loan Origination Transaction + +## Role in the System + +This auto-generated header is part of the `featureLendingProtocol` amendment on the XRP Ledger and defines the `ttLOAN_SET` transaction (type code 80). A `LoanSet` transaction originates a new loan, binding a borrower to a registered loan broker and encoding the full economic terms of the loan — principal, interest rates, fee schedules, and repayment cadence — as on-ledger, consensus-validated data. + +The file is one of roughly seventy transaction wrappers in `include/xrpl/protocol_autogen/transactions/`. All of them follow the same machine-generated structure: an immutable wrapper class and a companion builder class, produced to enforce a consistent, type-safe API across the protocol layer. + +## Class Structure + +### `LoanSet` — Immutable Accessor + +`LoanSet` inherits from `TransactionBase`, which holds a `std::shared_ptr` and provides read-only accessors for fields common to all transactions (`sfAccount`, `sfSequence`, `sfFee`, `sfSigners`, `sfDelegate`, etc.). `LoanSet` extends this with accessors for the fourteen transaction-specific fields it declares. + +The constructor performs a runtime type guard: + +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for LoanSet"); +``` + +This check is the sole defensive boundary at the `LoanSet` layer. Because `STTx` is a generic container, the wrong transaction could be passed in without a compile-time error; the runtime throw makes construction fail-fast rather than silently serving garbage field values. + +Two fields are declared `soeREQUIRED` and therefore return values directly without `std::nullopt`: + +- `getLoanBrokerID()` — a `uint256` identifying the registered `LoanBroker` ledger object that governs this loan's terms. +- `getPrincipalRequested()` — an `SF_NUMBER` (the ledger's arbitrary-precision numeric type) representing the loan amount. + +Every other field is `soeOPTIONAL` and is accessed through paired `getX()` / `hasX()` methods. The getters return `protocol_autogen::Optional`, which is a `std::optional` for value types and `std::optional>` for reference types, defined in `Utils.h`. The `hasX()` companion avoids the cost of constructing an optional when the caller only needs a presence check. + +`sfCounterpartySignature` is the one outlier: it returns `std::optional` directly rather than `protocol_autogen::Optional<...>` because it is an untyped nested object with no corresponding `SF_` template instantiation. The getter calls `getFieldObject()` rather than `at()`. + +### `LoanSetBuilder` — Fluent Construction + +`LoanSetBuilder` inherits from `TransactionBuilderBase`, a CRTP base that holds a mutable `STObject object_{sfTransaction}` and provides setters for all standard transaction fields (`setAccount`, `setFee`, `setSequence`, `setFlags`, `setDelegate`, etc.). Each base-class setter returns `Derived&` so calls chain cleanly across both the base and derived layers without breaking the fluent interface. + +The primary constructor enforces the two required fields immediately: + +```cpp +LoanSetBuilder(account, loanBrokerID, principalRequested, sequence, fee) + : TransactionBuilderBase(ttLOAN_SET, account, sequence, fee) +{ + setLoanBrokerID(loanBrokerID); + setPrincipalRequested(principalRequested); +} +``` + +An alternative constructor accepts an existing `STTx` and copies it into `object_` via `object_ = *tx`, enabling round-trip editing: deserialize a transaction from the network, wrap it in a builder, modify fields, and rebuild. The same type guard as the wrapper class applies here. + +All setter parameters are taken as `std::decay_t const&`. The `std::decay_t` strips reference and cv-qualifiers before binding, so callers never have to worry about value-category mismatches between the field's natural storage type and what they pass in. + +`setCounterpartySignature` is again the exception — it takes `STObject const&` directly and uses `setFieldObject()` on the underlying `STObject` rather than the field-indexed `operator[]`. + +`build()` calls the base `sign()` method, which serializes the object with `HashPrefix::txSign` prepended, computes the signature, and stores it into `sfTxnSignature`. It then moves `object_` into a freshly constructed `STTx`, wraps it in a `shared_ptr`, and returns a `LoanSet` wrapper. After `build()` the builder's internal state has been moved out; it should not be used again. + +## Economic Fields and Their Design + +The optional field set encodes a structured loan product with penalty differentiation across several lifecycle states: + +- **Fees** (all `SF_NUMBER`): `sfLoanOriginationFee`, `sfLoanServiceFee`, `sfLatePaymentFee`, `sfClosePaymentFee`, `sfOverpaymentFee` — covering origination costs, ongoing servicing, delinquency penalties, early-close charges, and excess-payment penalties. +- **Interest rates** (all `SF_UINT32`): `sfInterestRate`, `sfLateInterestRate`, `sfCloseInterestRate`, `sfOverpaymentInterestRate` — allowing different rate tiers depending on payment status. +- **Repayment schedule** (`SF_UINT32`): `sfPaymentTotal` (total number of installments), `sfPaymentInterval` (time between payments), `sfGracePeriod` (allowable delay before a payment is considered late). + +The separation of fees from rates — and the mirroring of each penalty category in both a fee and a rate field — reflects a design where each economic scenario (normal, late, close-out, overpayment) can be parameterized independently, giving broker implementations fine-grained control over loan product terms. + +## Counterparty Signature Pattern + +The presence of both `sfCounterparty` and `sfCounterpartySignature` indicates a bilateral origination flow. When a loan requires counterparty authorization — for instance, if an institutional lender must pre-approve the terms — the borrower includes the counterparty's account address and an `STObject` carrying their cryptographic signature. The ledger can then verify bilateral consent before applying the transaction. This is consistent with the `mayAuthorizeMPT | mustModifyVault` privilege set, which indicates the transaction interacts with both MPToken authorization and an associated vault ledger object managed through the `LoanBroker`. + +## Amendment Gating and Delegation + +The `featureLendingProtocol` amendment gate means this transaction type does not exist on the ledger until the amendment is enabled. The `notDelegable` flag on `Delegation` means a `sfDelegate` account cannot submit a `LoanSet` on behalf of another account — the borrower must sign directly. This restriction is enforced at the privilege level; the `TransactionBase::getDelegate()` accessor is still present on the wrapper class (inherited from the common base), but the transaction processor will reject any `LoanSet` that includes `sfDelegate`. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h.ai.json b/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h.ai.json new file mode 100644 index 0000000000..4b0b681243 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h.ai.json @@ -0,0 +1,101 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 80, + "name": "account" + }, + { + "lineno": 81, + "name": "mPTokenIssuanceID" + }, + { + "lineno": 82, + "name": "sequence" + }, + { + "lineno": 83, + "name": "fee" + }, + { + "lineno": 116, + "name": "publicKey" + }, + { + "lineno": 116, + "name": "secretKey" + }, + { + "lineno": 98, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "MPTokenAuthorize" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& mPTokenIssuanceID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 75, + "name": "MPTokenAuthorizeBuilder" + } + ], + "description": "Defines the MPTokenAuthorize transaction type for XRPL, including a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getMPTokenIssuanceID" + }, + { + "args": [], + "lineno": 48, + "name": "getHolder" + }, + { + "args": [], + "lineno": 59, + "name": "hasHolder" + }, + { + "args": [ + "value" + ], + "lineno": 97, + "name": "setMPTokenIssuanceID" + }, + { + "args": [ + "value" + ], + "lineno": 106, + "name": "setHolder" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 115, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h.ai.md b/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h.ai.md new file mode 100644 index 0000000000..d8418901a9 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/MPTokenAuthorize.h.ai.md @@ -0,0 +1,45 @@ +# `MPTokenAuthorize.h` — Auto-Generated Transaction Wrapper for MPT Authorization + +## Role in the System + +This file lives in `include/xrpl/protocol_autogen/transactions/` and is part of the code-generated layer that wraps the XRPL ledger's serialized transaction types (`STTx`) into ergonomic, type-safe C++ classes. It covers transaction type `ttMPTOKEN_AUTHORIZE` (opcode 57), one of four transaction types introduced by the `featureMPTokensV1` amendment for Multi-Purpose Tokens (MPTs) — XRPL's fungible token primitive intended to supersede trust-line-based IOUs in certain use cases. + +The header is explicitly annotated `// This file is auto-generated. Do not edit.`, meaning the canonical source of truth is an upstream template or spec-file; hand-edits would be overwritten on regeneration. + +## The MPTokenAuthorize Transaction: Dual-Role Semantics + +Before understanding the wrapper, it helps to understand what the transaction itself does. As seen in `MPTokenAuthorize.cpp`, this single transaction type serves two distinct actors depending on the presence of the optional `sfHolder` field: + +- **Holder path** (no `sfHolder`): The submitting account is opting itself in or out of holding a specific MPT issuance. Submitting without the `tfMPTUnauthorize` flag creates an `MPToken` ledger object for the holder; submitting with it deletes that object (requiring a zero balance). +- **Issuer path** (`sfHolder` present): The issuer is granting or revoking allowlist authorization for a specific holder account. This path only applies when the issuance has the `lsfMPTRequireAuth` flag set, meaning the issuer controls who may hold the token at all. + +This dual-role pattern is unusual — a single transaction type encoding two different authorization directions — but it keeps the MPT authorization surface compact. + +## `MPTokenAuthorize`: The Immutable Read Wrapper + +`MPTokenAuthorize` extends `TransactionBase`, which holds a `std::shared_ptr` and provides read-only accessors for all common fields (account, sequence, fee, flags, memos, signers, etc.). The derived class adds two MPT-specific accessors: + +- `getMPTokenIssuanceID()` returns the 192-bit issuance identifier (`SF_UINT192`) unconditionally. This field is `soeREQUIRED`, so direct access via `tx_->at(sfMPTokenIssuanceID)` is always safe. +- `getHolder()` returns `protocol_autogen::Optional` — a `std::optional` wrapping — only when `hasHolder()` confirms the field is present. This mirrors the transaction's own conditional semantics: the presence of `sfHolder` is what shifts the transaction into issuer-authorization mode. + +The constructor enforces type safety by calling `tx_->getTxnType()` and throwing `std::runtime_error` if it does not match `ttMPTOKEN_AUTHORIZE`. This guard is intentionally the first thing after delegating to `TransactionBase`, ensuring no caller can accidentally wrap a `Payment` or `OfferCreate` in an `MPTokenAuthorize` shell and read garbage field data. + +All getters are `[[nodiscard]]` and `const`, reinforcing the immutability contract of the wrapper. + +## `MPTokenAuthorizeBuilder`: CRTP Fluent Builder + +`MPTokenAuthorizeBuilder` follows the Curiously Recurring Template Pattern (CRTP) through `TransactionBuilderBase`. The base class holds a mutable `STObject object_{sfTransaction}` and exposes chainable setters for all standard fields (`setFee`, `setSequence`, `setFlags`, `setLastLedgerSequence`, etc.), each returning `Derived&` so call chains compose across both base and derived setters without narrowing the type. + +The primary constructor takes the mandatory `account` and `mPTokenIssuanceID` arguments, forwarding to the base and immediately calling `setMPTokenIssuanceID()`. Sequence and fee are optional at construction time, acknowledging real-world workflows where these are filled in later (e.g., from a fee estimate RPC or after fetching account state). `setHolder()` is offered as an optional setter for the issuer-authorization case. + +A secondary constructor accepts an existing `std::shared_ptr` and copies it into `object_`, enabling round-trip editing: deserialize a transaction from the network, wrap it in a builder, modify fields, and re-sign. The same type guard applies here. + +`build(publicKey, secretKey)` finalizes construction by calling the protected `sign()` helper in `TransactionBuilderBase`. That method serializes the `STObject` without signing fields, prepends the `HashPrefix::txSign` prefix, signs with the provided key pair, and embeds both `sfSigningPubKey` and `sfTxnSignature` into the object before it is moved into a freshly constructed `STTx` and wrapped in the immutable `MPTokenAuthorize` type. + +## Design Decisions Worth Noting + +**Why `std::decay_t`?** The setter signatures use `std::decay_t` to strip reference and cv-qualifiers from the field's native value type. This prevents accidental binding of temporaries by value and keeps the setter signature consistent regardless of whether the underlying `STField` value type is itself a reference type. Passing by `const&` after decay is the canonical pattern across all generated setters in this directory. + +**Why not expose `sfHolder` as required in the constructor?** Because the two use-cases — holder opt-in and issuer allowlist — are fundamentally different roles. Forcing `sfHolder` in the constructor would either make the holder opt-in case awkward (passing a dummy value) or require two separate builder types. The design accepts the mild ambiguity of an optional field in exchange for a unified builder surface. + +**Auto-generation rationale**: The `protocol_autogen` directory contains one file per transaction type, all following the same structural template. Rather than maintaining 70+ near-identical wrapper classes by hand, the codebase generates them from a specification. Any deviation (field name, optionality, field type) is centrally controlled and reflected everywhere at once. The `// This file is auto-generated. Do not edit.` guard communicates this to contributors at a glance. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h.ai.json b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h.ai.json new file mode 100644 index 0000000000..af7209b84f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h.ai.json @@ -0,0 +1,194 @@ +{ + "args": [ + { + "lineno": 31, + "name": "tx" + }, + { + "lineno": 181, + "name": "account" + }, + { + "lineno": 181, + "name": "sequence" + }, + { + "lineno": 181, + "name": "fee" + }, + { + "lineno": 191, + "name": "tx" + }, + { + "lineno": 198, + "name": "value" + }, + { + "lineno": 208, + "name": "value" + }, + { + "lineno": 218, + "name": "value" + }, + { + "lineno": 228, + "name": "value" + }, + { + "lineno": 238, + "name": "value" + }, + { + "lineno": 248, + "name": "value" + }, + { + "lineno": 258, + "name": "publicKey" + }, + { + "lineno": 258, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "MPTokenIssuanceCreate" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 174, + "name": "MPTokenIssuanceCreateBuilder" + } + ], + "description": "Defines the MPTokenIssuanceCreate transaction type for XRPL, including a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h", + "functions": [ + { + "args": [], + "lineno": 41, + "name": "getAssetScale" + }, + { + "args": [], + "lineno": 52, + "name": "hasAssetScale" + }, + { + "args": [], + "lineno": 63, + "name": "getTransferFee" + }, + { + "args": [], + "lineno": 74, + "name": "hasTransferFee" + }, + { + "args": [], + "lineno": 85, + "name": "getMaximumAmount" + }, + { + "args": [], + "lineno": 96, + "name": "hasMaximumAmount" + }, + { + "args": [], + "lineno": 107, + "name": "getMPTokenMetadata" + }, + { + "args": [], + "lineno": 118, + "name": "hasMPTokenMetadata" + }, + { + "args": [], + "lineno": 129, + "name": "getDomainID" + }, + { + "args": [], + "lineno": 140, + "name": "hasDomainID" + }, + { + "args": [], + "lineno": 151, + "name": "getMutableFlags" + }, + { + "args": [], + "lineno": 162, + "name": "hasMutableFlags" + }, + { + "args": [ + "value" + ], + "lineno": 196, + "name": "setAssetScale" + }, + { + "args": [ + "value" + ], + "lineno": 206, + "name": "setTransferFee" + }, + { + "args": [ + "value" + ], + "lineno": 216, + "name": "setMaximumAmount" + }, + { + "args": [ + "value" + ], + "lineno": 226, + "name": "setMPTokenMetadata" + }, + { + "args": [ + "value" + ], + "lineno": 236, + "name": "setDomainID" + }, + { + "args": [ + "value" + ], + "lineno": 246, + "name": "setMutableFlags" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 256, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h.ai.md b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h.ai.md new file mode 100644 index 0000000000..f836ed02d2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceCreate.h.ai.md @@ -0,0 +1,36 @@ +# `MPTokenIssuanceCreate.h` — Auto-generated MPT Issuance Creation Transaction + +## Role and Context + +This file is part of the `protocol_autogen` layer — a code-generated set of strongly-typed transaction wrappers and builders that sit on top of the XRPL core `STTx` serialized-type system. It defines the typed interface for `MPTokenIssuanceCreate`, the transaction that bootstraps a Multi-Purpose Token (MPT) class on the XRP Ledger. MPTs are the `featureMPTokensV1` amendment's token standard — distinct from XLS-20 NFTs — providing an issuer-controlled fungible token with configurable supply caps, transfer fees, and metadata. Before any account can hold an MPT, its issuance object must be created on-ledger by this transaction type (`ttMPTOKEN_ISSUANCE_CREATE`, type code 54). + +The header contains two classes: `MPTokenIssuanceCreate`, a read-only view of an already-constructed `STTx`, and `MPTokenIssuanceCreateBuilder`, a fluent builder that assembles and signs new transactions. This split is the same pattern repeated across all ~70 autogen transaction headers in this directory, including the companion `MPTokenIssuanceDestroy.h` and `MPTokenIssuanceSet.h`. + +## `MPTokenIssuanceCreate` — Immutable Wrapper + +`MPTokenIssuanceCreate` inherits from `TransactionBase`, which owns a `shared_ptr` named `tx_`. Immutability is enforced by the `const`-qualified pointer — no field can be changed through this class. The constructor takes an existing `shared_ptr` and immediately validates that `tx_->getTxnType() == ttMPTOKEN_ISSUANCE_CREATE`, throwing `std::runtime_error` on mismatch. This fail-fast guard ensures the wrapper never silently misrepresents a different transaction type, which matters because `STTx` fields are accessed by index without further type checking. + +All six transaction-specific fields are **optional** (`soeOPTIONAL`). This is architecturally significant: unlike sibling operations `MPTokenIssuanceDestroy` and `MPTokenIssuanceSet`, which require an `sfMPTokenIssuanceID` to identify an existing issuance, the create transaction identifies the target by the sender's account and a derived ledger object ID — so no field is strictly required beyond the common account/sequence/fee fields inherited from `TransactionBase`. + +Each field follows a paired accessor pattern: a `has*()` predicate backed by `isFieldPresent()`, and a `get*()` that returns `protocol_autogen::Optional`. The `Optional` alias (defined in `Utils.h`) handles a subtlety: for value types it is `std::optional`, but for reference types it wraps in `std::optional>` to avoid returning dangling references to temporaries. All getters guard the field access through the corresponding `has*()` check before calling `tx_->at(...)`, preventing `STObject` from throwing on absent optional fields. + +The six MPT-specific fields control the economics and governance of the token class: + +- **`sfAssetScale`** (`uint8`) — decimal precision, analogous to ERC-20 `decimals()`. Determines how raw integer amounts map to human-readable values. +- **`sfTransferFee`** (`uint16`) — basis points deducted on every transfer and credited back to the issuer. Encoded as an integer (e.g., `1000` = 1%). +- **`sfMaximumAmount`** (`uint64`) — hard cap on total outstanding supply. If absent, the supply is unlimited. +- **`sfMPTokenMetadata`** (`VL` blob) — arbitrary byte payload, typically a URI or a hash pointing to off-ledger metadata. +- **`sfDomainID`** (`uint256`) — a reference to a `PermissionedDomain` object that governs who may hold this token, enabling compliance-constrained tokens. +- **`sfMutableFlags`** (`uint32`) — a subset of issuance flags that the issuer retains the right to change after creation, as opposed to the immutable `sfFlags` set at creation time. + +## `MPTokenIssuanceCreateBuilder` — Fluent Builder + +The builder inherits from `TransactionBuilderBase` using CRTP, which causes all common setter methods (`setFee()`, `setSequence()`, `setFlags()`, etc.) to return `MPTokenIssuanceCreateBuilder&` rather than the base type, enabling unbroken fluent chains without casts at the call site. + +Internally the builder holds a plain `STObject object_{sfTransaction}` — importantly, the `TransactionBuilderBase` constructor deliberately does **not** call `object_.set(soTemplate)`. This avoids creating `STBase` placeholders for `soeDEFAULT` fields, which would cause `applyTemplate()` to throw "may not be explicitly set to default" when the `STTx` constructor later enforces the schema. The builder accumulates fields freely into the bare `STObject`, and the `STTx` constructor applies the template at construction time. + +The builder has two construction paths: starting fresh from account/sequence/fee, or round-tripping from an existing `shared_ptr` by copying `*tx` into `object_`. The copy path is guarded by the same type check as the read-only wrapper, ensuring consistency. The `build()` method calls `sign()` (which computes `sfSigningPubKey` and `sfTxnSignature` over a `HashPrefix::txSign`-prefixed serialization), then wraps the result in a `shared_ptr` and hands it to the `MPTokenIssuanceCreate` constructor. + +## Relationship to the Autogen Layer + +The file opens with `// This file is auto-generated. Do not edit.` — it is produced from a schema description of the XRPL transaction format, not written by hand. Every transaction type in the `protocol_autogen/transactions/` directory follows the same structural template, making the layer a machine-maintainable typing layer over the dynamically-typed `STTx` core. The absence of required fields here (versus mandatory `sfMPTokenIssuanceID` on destroy/set operations) is a direct consequence of the schema: issuance creation targets an account, not a pre-existing ledger object. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h.ai.json b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h.ai.json new file mode 100644 index 0000000000..76e655b3e2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h.ai.json @@ -0,0 +1,112 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 59, + "name": "account" + }, + { + "lineno": 59, + "name": "mPTokenIssuanceID" + }, + { + "lineno": 59, + "name": "sequence" + }, + { + "lineno": 59, + "name": "fee" + }, + { + "lineno": 70, + "name": "tx" + }, + { + "lineno": 83, + "name": "value" + }, + { + "lineno": 92, + "name": "publicKey" + }, + { + "lineno": 92, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 24, + "name": "MPTokenIssuanceDestroy" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& mPTokenIssuanceID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 55, + "name": "MPTokenIssuanceDestroyBuilder" + } + ], + "description": "Defines the MPTokenIssuanceDestroy transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "MPTokenIssuanceDestroy" + }, + { + "args": [], + "lineno": 43, + "name": "getMPTokenIssuanceID" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& mPTokenIssuanceID", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 59, + "name": "MPTokenIssuanceDestroyBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 70, + "name": "MPTokenIssuanceDestroyBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 83, + "name": "setMPTokenIssuanceID" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 92, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h.ai.md b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h.ai.md new file mode 100644 index 0000000000..7b9a8af671 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h.ai.md @@ -0,0 +1,35 @@ +# `MPTokenIssuanceDestroy.h` + +Auto-generated header in `xrpl::transactions` that defines the type-safe C++ interface for the `MPTokenIssuanceDestroy` transaction — the ledger operation that permanently removes a Multi-Purpose Token (MPT) issuance object from the XRP Ledger. This file is one entry in a large family of per-transaction-type headers under `include/xrpl/protocol_autogen/transactions/`, each produced by the same code generator to guarantee uniform structure across all transaction kinds. + +## Role in the MPT Lifecycle + +MPT issuances are created by `MPTokenIssuanceCreate` (type 54, `featureMPTokensV1`). Once an issuer has wound down all outstanding balances and holder positions, they submit an `MPTokenIssuanceDestroy` transaction (type 55) to reclaim the on-ledger reserve the `MPTokenIssuance` object occupies. The sole required field — `sfMPTokenIssuanceID` — encodes which issuance to delete. This identifier is a 192-bit (`SF_UINT192`) value derived from the issuer's account and the sequence number of the originating `MPTokenIssuanceCreate`, making it globally unique and non-colliding. The transaction is gated behind the `featureMPTokensV1` amendment and requires the `destroyMPTIssuance` privilege, and it is marked `Delegation::delegable`, meaning the `sfDelegate` field (inherited from `TransactionBase`) may be used to allow a third-party account to submit the transaction on the issuer's behalf. + +## Two-Class Design: Wrapper and Builder + +The file follows the split pattern established by `TransactionBase` and `TransactionBuilderBase`. The rationale is strict separation between construction (mutable) and observation (immutable after signing). + +**`MPTokenIssuanceDestroy`** is the immutable reader. It inherits from `TransactionBase`, which holds a `std::shared_ptr` and exposes typed getters for all standard fields (account, sequence, fee, flags, memos, signers, delegate, and more). `MPTokenIssuanceDestroy` adds exactly one transaction-specific getter: + +```cpp +[[nodiscard]] +SF_UINT192::type::value_type +getMPTokenIssuanceID() const; +``` + +This unconditionally dereferences `sfMPTokenIssuanceID` via `tx_->at(...)`, which is safe because the field is `soeREQUIRED` in the transaction schema — it must be present in any well-formed `STTx` of this type. The constructor enforces the type precondition by comparing `tx_->getTxnType()` against the `txType` constant (`ttMPTOKEN_ISSUANCE_DESTROY`) and throwing `std::runtime_error` on mismatch, preventing a caller from accidentally wrapping a `Payment` or other unrelated transaction in this class. + +**`MPTokenIssuanceDestroyBuilder`** is the mutable side. It inherits from `TransactionBuilderBase` — a CRTP template that returns `Derived&` from every setter, enabling fluent method-chaining across all common fields (`setFee`, `setSequence`, `setLastLedgerSequence`, `setDelegate`, etc.). The builder stores its state in a plain `STObject object_` (not yet an `STTx`) to avoid triggering `applyTemplate()` validation prematurely; the `STTx` is only instantiated at `build()` time. + +The builder offers two construction paths: +- **From scratch**: takes a required `account` and the mandatory `mPTokenIssuanceID`, plus optional `sequence` and `fee`. The required field is immediately written into `object_` via `setMPTokenIssuanceID()`, enforcing the invariant that it is always present before signing. +- **From an existing `STTx`**: copies the object data out of a previously-deserialized transaction, after verifying the transaction type. This supports round-trip use cases where a partially-constructed or received transaction needs to be re-signed or modified before re-submission. + +The `build()` method calls the protected `sign()` helper (from `TransactionBuilderBase`), which serializes the object with `HashPrefix::txSign` prepended, signs it with the provided key pair, sets `sfSigningPubKey` and `sfTxnSignature`, then promotes `object_` into an `STTx` via `std::make_shared(std::move(object_))` and wraps it in the immutable `MPTokenIssuanceDestroy` reader. + +## Relationship to Sibling Files + +By comparison with `MPTokenIssuanceCreate.h`, the destroy variant is notably sparse: `MPTokenIssuanceCreate` carries six optional fields (`sfAssetScale`, `sfTransferFee`, `sfMaximumAmount`, `sfMPTokenMetadata`, `sfDomainID`, `sfMutableFlags`) that configure the issuance at birth, whereas `MPTokenIssuanceDestroy` carries only the single required identifier used to look up and delete the object. This asymmetry reflects the protocol: creation is rich and configurable, destruction is a targeted removal keyed by ID. + +The `static constexpr xrpl::TxType txType = ttMPTOKEN_ISSUANCE_DESTROY` member on the wrapper class allows callers to dispatch or introspect the type at compile time without instantiating an object, consistent with every other auto-generated transaction wrapper in this directory. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h.ai.json new file mode 100644 index 0000000000..447ff7351a --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h.ai.json @@ -0,0 +1,217 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 159, + "name": "account" + }, + { + "lineno": 159, + "name": "mPTokenIssuanceID" + }, + { + "lineno": 159, + "name": "sequence" + }, + { + "lineno": 159, + "name": "fee" + }, + { + "lineno": 170, + "name": "tx" + }, + { + "lineno": 181, + "name": "value" + }, + { + "lineno": 191, + "name": "value" + }, + { + "lineno": 201, + "name": "value" + }, + { + "lineno": 211, + "name": "value" + }, + { + "lineno": 221, + "name": "value" + }, + { + "lineno": 231, + "name": "value" + }, + { + "lineno": 241, + "name": "publicKey" + }, + { + "lineno": 241, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 21, + "name": "MPTokenIssuanceSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& mPTokenIssuanceID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 151, + "name": "MPTokenIssuanceSetBuilder" + } + ], + "description": "Defines the MPTokenIssuanceSet transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "MPTokenIssuanceSet" + }, + { + "args": [], + "lineno": 41, + "name": "getMPTokenIssuanceID" + }, + { + "args": [], + "lineno": 52, + "name": "getHolder" + }, + { + "args": [], + "lineno": 63, + "name": "hasHolder" + }, + { + "args": [], + "lineno": 72, + "name": "getDomainID" + }, + { + "args": [], + "lineno": 83, + "name": "hasDomainID" + }, + { + "args": [], + "lineno": 92, + "name": "getMPTokenMetadata" + }, + { + "args": [], + "lineno": 103, + "name": "hasMPTokenMetadata" + }, + { + "args": [], + "lineno": 112, + "name": "getTransferFee" + }, + { + "args": [], + "lineno": 123, + "name": "hasTransferFee" + }, + { + "args": [], + "lineno": 132, + "name": "getMutableFlags" + }, + { + "args": [], + "lineno": 143, + "name": "hasMutableFlags" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& mPTokenIssuanceID", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 159, + "name": "MPTokenIssuanceSetBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 170, + "name": "MPTokenIssuanceSetBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 181, + "name": "setMPTokenIssuanceID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 191, + "name": "setHolder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 201, + "name": "setDomainID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 211, + "name": "setMPTokenMetadata" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 221, + "name": "setTransferFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 231, + "name": "setMutableFlags" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 241, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h.ai.md new file mode 100644 index 0000000000..92bd424ecd --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/MPTokenIssuanceSet.h.ai.md @@ -0,0 +1,37 @@ +# `MPTokenIssuanceSet.h` — Auto-Generated Transaction Wrapper + +## Role and Context + +This header defines the type-safe C++ interface for the `MPTokenIssuanceSet` transaction (`ttMPTOKEN_ISSUANCE_SET`, type code 56), one of the four transaction types that constitute the MPTokens lifecycle under the `featureMPTokensV1` amendment. Where `MPTokenIssuanceCreate` brings a new multi-purpose token issuance into existence, `MPTokenIssuanceSet` modifies the mutable properties of an already-created issuance — changing its transfer fee, associating it with a compliance domain, updating its metadata, or toggling its `sfMutableFlags`. The file is auto-generated and must not be edited by hand; it follows the same structural template as every other transaction header in `include/xrpl/protocol_autogen/transactions/`. + +## Dual-Class Design + +The file exposes two cooperating classes in the `xrpl::transactions` namespace: + +**`MPTokenIssuanceSet`** is a read-only, immutable wrapper around a `std::shared_ptr`. It inherits from `TransactionBase`, which holds the shared pointer and exposes accessors for universal transaction fields (`sfAccount`, `sfFee`, `sfSequence`, `sfDelegate`, etc.). `MPTokenIssuanceSet` adds getters specific to this transaction type and enforces the type invariant in its constructor by calling `tx_->getTxnType()` and throwing `std::runtime_error` if the discriminant doesn't match `ttMPTOKEN_ISSUANCE_SET`. This eager validation means any code that holds an `MPTokenIssuanceSet` value is statically guaranteed to be looking at the right transaction type — no further runtime checks needed in consuming code. + +**`MPTokenIssuanceSetBuilder`** is a mutable builder that extends `TransactionBuilderBase` via CRTP. The base class holds an `STObject object_{sfTransaction}` and provides setters for all common fields, each returning `Derived&` so that call sites can chain `.setFee(...).setLastLedgerSequence(...)`. The derived builder adds `MPTokenIssuanceSet`-specific setters on top. Critically, `build(PublicKey, SecretKey)` is the only path to producing a signed `MPTokenIssuanceSet` value: it calls the inherited `sign()` method (which serializes the object with `HashPrefix::txSign`, signs it, and embeds `sfSigningPubKey` and `sfTxnSignature`), then wraps the moved `STObject` into a `shared_ptr` and constructs the immutable wrapper. There is no way to accidentally skip signing while using this API. + +## Field Inventory + +The single required field is `sfMPTokenIssuanceID`, typed as `SF_UINT192::type::value_type` — a 192-bit identifier that uniquely addresses an MPT issuance on the ledger. Because it is required, the primary builder constructor takes it as a mandatory argument and immediately calls `setMPTokenIssuanceID()`, ensuring no builder can exist in a state where the target issuance is unspecified. + +All remaining fields are optional and carry `has*` / `get*` paired accessors on the read side: + +- **`sfHolder`** (`SF_ACCOUNT`) — when present, scopes the operation to a specific token holder's balance object rather than the issuance itself. This enables the issuer to lock or unlock a specific holder's position, which is the per-holder authorization flow distinct from setting global issuance properties. +- **`sfDomainID`** (`SF_UINT256`) — associates or re-associates the issuance with a permissioned domain for compliance purposes. +- **`sfMPTokenMetadata`** (`SF_VL`) — arbitrary variable-length blob for off-chain metadata (e.g., a URI or hash). The `SF_VL` type means the getter returns `protocol_autogen::Optional`; if the field value type were a reference, the `protocol_autogen::Optional` alias would transparently wrap it in `std::reference_wrapper` to keep it storable in `std::optional`. +- **`sfTransferFee`** (`SF_UINT16`) — the per-transfer fee rate in units of 1/100,000 of the transferred amount. This field appearing on both `MPTokenIssuanceCreate` and `MPTokenIssuanceSet` reflects that transfer fees are a mutable property of an issuance — the issuer can adjust them after creation. +- **`sfMutableFlags`** (`SF_UINT32`) — a bitmask of flags that are permitted to change post-creation (in contrast to the immutable flags baked in at creation time). Allowing a dedicated mutable flags field rather than reusing `sfFlags` avoids ambiguity between transaction control flags and issuance-state flags. + +## Optional Getter Pattern + +Every optional field follows the same three-line pattern: `has*()` calls `tx_->isFieldPresent(sf*)`, and `get*()` delegates to `has*()` before calling `tx_->at(sf*)`, returning `std::nullopt` when absent. All getters are `[[nodiscard]]` and `const`. This guards against raw `at()` calls that would throw on missing optional fields while keeping the interface self-documenting about which fields are required versus conditional. + +## Delegability + +The transaction is marked `Delegation::delegable`, meaning a token holder or issuer can authorize another account to submit this transaction on their behalf by setting `sfDelegate`. The `getDelegate()` accessor for that field lives in `TransactionBase`, so it is uniformly available on all delegable transaction types without any per-type boilerplate. + +## Relationship to Sibling Files + +`MPTokenIssuanceSet.h` sits alongside `MPTokenIssuanceCreate.h`, `MPTokenIssuanceDestroy.h`, and `MPTokenAuthorize.h` as the full MPToken transaction family. The builder second-constructor overload — `MPTokenIssuanceSetBuilder(std::shared_ptr tx)` — exists so that a transaction received from the network (e.g., fetched from ledger history) can be deserialized, copied into the mutable `STObject`, and re-signed with different parameters. This round-trip pattern is shared by all auto-generated builders in the directory and enables tooling that needs to repackage or replay existing transactions. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h.ai.json b/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h.ai.json new file mode 100644 index 0000000000..410d6d1e09 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h.ai.json @@ -0,0 +1,119 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 105, + "name": "account" + }, + { + "lineno": 106, + "name": "sequence" + }, + { + "lineno": 107, + "name": "fee" + }, + { + "lineno": 151, + "name": "publicKey" + }, + { + "lineno": 151, + "name": "secretKey" + }, + { + "lineno": 122, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 23, + "name": "NFTokenAcceptOffer" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 97, + "name": "NFTokenAcceptOfferBuilder" + } + ], + "description": "Provides an immutable wrapper and builder for the NFTokenAcceptOffer transaction type in the XRPL protocol, enabling type-safe field access and fluent transaction construction.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getNFTokenBuyOffer" + }, + { + "args": [], + "lineno": 48, + "name": "hasNFTokenBuyOffer" + }, + { + "args": [], + "lineno": 58, + "name": "getNFTokenSellOffer" + }, + { + "args": [], + "lineno": 68, + "name": "hasNFTokenSellOffer" + }, + { + "args": [], + "lineno": 78, + "name": "getNFTokenBrokerFee" + }, + { + "args": [], + "lineno": 88, + "name": "hasNFTokenBrokerFee" + }, + { + "args": [ + "value" + ], + "lineno": 120, + "name": "setNFTokenBuyOffer" + }, + { + "args": [ + "value" + ], + "lineno": 130, + "name": "setNFTokenSellOffer" + }, + { + "args": [ + "value" + ], + "lineno": 140, + "name": "setNFTokenBrokerFee" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 150, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h.ai.md b/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h.ai.md new file mode 100644 index 0000000000..f69f62c981 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenAcceptOffer.h.ai.md @@ -0,0 +1,54 @@ +# `NFTokenAcceptOffer.h` — Auto-Generated NFT Offer Acceptance Transaction + +## File Role + +This header is part of the `protocol_autogen` layer, a family of auto-generated C++ wrappers sitting above the XRPL core serialization types (`STTx`, `STObject`). Every transaction type in `include/xrpl/protocol_autogen/transactions/` follows the same structural pattern; this file instantiates it for `ttNFTOKEN_ACCEPT_OFFER` (type code 29). + +The file exists to solve a recurring problem in XRPL transaction handling: the underlying `STTx` / `STObject` infrastructure stores fields in a loosely-typed map keyed by `SField` descriptors. Without a wrapper layer, every call site must know which field names to query, check presence manually, and cast to the right type — a pattern that is both verbose and fragile. `NFTokenAcceptOffer` and `NFTokenAcceptOfferBuilder` eliminate that friction for one specific transaction type. + +## The Transaction Semantics + +`NFTokenAcceptOffer` covers three distinct modes of NFT trade settlement on the ledger: + +- **Direct sell**: the account that received a sell offer accepts it by specifying `sfNFTokenSellOffer`. +- **Direct buy**: the original NFT owner accepts an incoming buy offer by specifying `sfNFTokenBuyOffer`. +- **Brokered**: a third-party broker submits the transaction specifying *both* a buy and a sell offer, optionally extracting `sfNFTokenBrokerFee` from the transaction proceeds. The broker need not own the NFT or be either original counterparty. + +All three fields are declared `soeOPTIONAL` in the ledger schema, which is why the wrapper returns `protocol_autogen::Optional<…>` (a thin alias for `std::optional`) from every getter rather than a value directly. The caller is forced to handle absent fields at compile time through the optional API, rather than relying on runtime checks or exception-based field access. + +## `NFTokenAcceptOffer` — Immutable Read Wrapper + +`NFTokenAcceptOffer` inherits `TransactionBase`, which holds a single `std::shared_ptr tx_` and exposes type-safe getters for every universal transaction field (`getAccount()`, `getFee()`, `getSequence()`, `getMemos()`, etc.). The derived class adds only the three transaction-specific getters. + +Construction accepts a `std::shared_ptr` and immediately validates the transaction type against the class-level constant `txType = ttNFTOKEN_ACCEPT_OFFER`. Passing a mismatched `STTx` throws `std::runtime_error`. This check is intentionally eager — it catches integration errors at the earliest possible moment rather than allowing a silently wrong wrapper to propagate through business logic. + +Each optional field follows a strict two-method pattern: + +```cpp +getNFTokenBuyOffer() // returns Optional, checks presence first +hasNFTokenBuyOffer() // cheap boolean field-presence query +``` + +The getter delegates presence checking to `hasNFTokenBuyOffer()` before accessing `tx_->at(sfNFTokenBuyOffer)`. This avoids the exception that `STObject::at` would throw on a missing field, translating it into an `std::nullopt` return instead. All getters carry `[[nodiscard]]` to prevent callers from silently discarding the returned optional. + +The wrapper is intentionally immutable — there are no setters. The separation between mutable construction and immutable reading is a deliberate design choice: once a transaction is wrapped in `NFTokenAcceptOffer`, its field contents are frozen, and only `NFTokenAcceptOfferBuilder` can produce new instances. + +## `NFTokenAcceptOfferBuilder` — Fluent Construction + +`NFTokenAcceptOfferBuilder` inherits `TransactionBuilderBase`, which uses CRTP so that every common setter in the base (`setFee()`, `setSequence()`, `setLastLedgerSequence()`, etc.) returns a `NFTokenAcceptOfferBuilder&` rather than a `TransactionBuilderBase&`. This preserves the concrete type across the entire method chain without virtual dispatch. + +The base class maintains a mutable `STObject object_{sfTransaction}` rather than an `STTx`. This distinction matters: `STTx` enforces the transaction schema template (`soTemplate`) at construction, which would reject missing or default-valued fields. By keeping state as a free `STObject` during the build phase, the builder can accumulate fields incrementally without triggering those constraints. Only at `build()` time, when the `STObject` is moved into `STTx`, does schema validation fire. + +The builder offers two construction paths: + +1. **From scratch**: `NFTokenAcceptOfferBuilder(account, sequence, fee)` — initializes `sfTransactionType`, `sfAccount`, and optionally `sfSequence` and `sfFee`. Sequence and fee are optional parameters with `std::nullopt` defaults, accommodating workflows where the fee is auto-filled by a server or the sequence comes from an account info query. + +2. **From an existing `STTx`**: the second constructor copies a validated `STTx` back into `object_` via `object_ = *tx`. This enables round-trip editing: deserialize a transaction from the wire, wrap it in the builder to modify fields, then call `build()` to produce a new signed transaction. The type check mirrors the one in `NFTokenAcceptOffer` — mismatched type throws immediately. + +The transaction-specific setters use `std::decay_t` as the parameter type. The `std::decay_t` strips references and cv-qualifiers from whatever the `SField` type system resolves, ensuring clean pass-by-const-reference semantics regardless of how the underlying type alias is defined. + +`build()` calls `sign()` (inherited from `TransactionBuilderBase`), which serializes the `STObject` without signing fields, prepends the `HashPrefix::txSign` tag, signs the resulting bytes with the provided key pair, and stores both `sfSigningPubKey` and `sfTxnSignature` back into `object_`. The `STObject` is then moved into a freshly constructed `STTx` and wrapped in the immutable `NFTokenAcceptOffer`. The move means the builder's internal state is consumed — it should not be reused after calling `build()`. + +## Relationship to the Broader `protocol_autogen` Layer + +This file is one of roughly 70 identically structured transaction headers in the `transactions/` directory, all generated from a common schema. The design contracts established by `TransactionBase` and `TransactionBuilderBase` are the same across all of them: immutable read-type on one side, CRTP builder on the other. The pattern makes transaction-type-specific code trivially thin — here the entire transaction-specific surface is three optional `uint256`/`STAmount` fields — while ensuring that common infrastructure such as schema validation (`validateSTObject`), local ledger checks (`passesLocalChecks`), and signing lives in the shared base classes rather than being duplicated. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h.ai.json b/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h.ai.json new file mode 100644 index 0000000000..1376c57316 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h.ai.json @@ -0,0 +1,101 @@ +{ + "args": [ + { + "lineno": 36, + "name": "tx" + }, + { + "lineno": 86, + "name": "account" + }, + { + "lineno": 87, + "name": "nFTokenID" + }, + { + "lineno": 88, + "name": "sequence" + }, + { + "lineno": 89, + "name": "fee" + }, + { + "lineno": 121, + "name": "publicKey" + }, + { + "lineno": 121, + "name": "secretKey" + }, + { + "lineno": 103, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 34, + "name": "NFTokenBurn" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& nFTokenID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 77, + "name": "NFTokenBurnBuilder" + } + ], + "description": "Defines the NFTokenBurn transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing NFTokenBurn transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getNFTokenID" + }, + { + "args": [], + "lineno": 54, + "name": "getOwner" + }, + { + "args": [], + "lineno": 64, + "name": "hasOwner" + }, + { + "args": [ + "value" + ], + "lineno": 101, + "name": "setNFTokenID" + }, + { + "args": [ + "value" + ], + "lineno": 110, + "name": "setOwner" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 119, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h.ai.md b/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h.ai.md new file mode 100644 index 0000000000..ec0c37c77a --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenBurn.h.ai.md @@ -0,0 +1,36 @@ +# `NFTokenBurn.h` — Auto-Generated NFTokenBurn Transaction Wrapper + +This file is part of the `protocol_autogen` subsystem of the XRPL codebase and is auto-generated — the comment at line 1 makes this explicit. It lives in `include/xrpl/protocol_autogen/transactions/` alongside roughly seventy sibling files that each define one XRPL transaction type using the same structural template. It should never be edited by hand; the source of truth is `include/xrpl/protocol/detail/transactions.macro`, which declares `ttNFTOKEN_BURN` as type 26 with the field schema that drove this file's generation. + +## Purpose + +The file provides two things: a read-only, type-safe C++ wrapper around an already-serialized `NFTokenBurn` transaction (`NFTokenBurn` class), and a fluent builder for constructing new such transactions (`NFTokenBurnBuilder` class). Both live in the `xrpl::transactions` namespace. + +`NFTokenBurn` is the XRPL transaction that permanently destroys an existing non-fungible token. The token is identified by the required `sfNFTokenID` (a `uint256` hash). Because issuer accounts sometimes need to reclaim tokens held by other accounts — for example, in regulated or permissioned NFT schemes — the transaction also accepts an optional `sfOwner` field (an `AccountID`): when present, it specifies a third-party account whose NFT page should be searched rather than the submitting account's own page. + +## The `NFTokenBurn` Wrapper + +`NFTokenBurn` extends `TransactionBase`, which holds a `shared_ptr` and exposes accessors for the universal transaction fields (`sfAccount`, `sfFee`, `sfSequence`, `sfDelegate`, etc.). The subclass adds only the NFT-specific fields: + +- `getNFTokenID()` returns the required `uint256` token ID by calling `tx_->at(sfNFTokenID)`. Because the field is `soeREQUIRED` in the schema, accessing it without a presence check is safe. +- `getOwner()` guards the optional `sfOwner` access behind `hasOwner()`, returning `protocol_autogen::Optional` — a thin alias for `std::optional` used consistently across the autogen layer — rather than returning a default-constructed value or throwing. + +The constructor enforces the type invariant eagerly: if the wrapped `STTx`'s `getTxnType()` does not equal `ttNFTOKEN_BURN`, it throws `std::runtime_error` immediately. This is a deliberate defensive choice over a compile-time guarantee, because the `STTx` type is not a template parameter — the check must happen at runtime when an arbitrary `shared_ptr` is handed in from deserialization or test code. + +## The `NFTokenBurnBuilder` + +`NFTokenBurnBuilder` extends `TransactionBuilderBase` using CRTP, which allows the base class to return `Derived&` from every common setter (`setFee`, `setSequence`, `setLastLedgerSequence`, `setDelegate`, etc.) without requiring virtual dispatch or down-casting at the call site. The derived class only adds the NFT-specific setters that call back into the underlying `STObject object_` held by the base. + +The primary constructor takes `account` and `nFTokenID` as mandatory parameters — matching the `soeREQUIRED` schema constraint — plus `sequence` and `fee` as `std::optional` arguments with `nullopt` defaults. Passing them as optionals, rather than omitting them entirely, makes the builder usable in environments like unit tests where the sequence and fee are provided explicitly, while still deferring them for cases where the network will autofill. The required `nFTokenID` is set immediately via `setNFTokenID()` inside the constructor body, so the builder is always in a valid partial state. + +The secondary constructor accepts an existing `shared_ptr` and copies its data into `object_`. This is a round-trip path: take a signed transaction off the wire, load it into a builder, modify a field, and re-sign. It performs the same type guard as the wrapper class. + +`setOwner()` is available but entirely optional. A caller constructs without it when burning their own token; they call `.setOwner(someAccount)` when acting as an issuer burning a token from another holder's page. + +`build(PublicKey, SecretKey)` finalizes construction: it delegates to the base class `sign()` method, which computes the serialization prefix (`HashPrefix::txSign`), serializes the `STObject` without signing fields, signs with `xrpl::sign`, and sets both `sfSigningPubKey` and `sfTxnSignature`. It then moves `object_` into a freshly constructed `STTx` (which calls `applyTemplate()` to fill any schema defaults) and returns an `NFTokenBurn` wrapper around it. Note that after `build()` is called `object_` has been moved-from, so the builder instance should not be reused. + +## Design Notes + +The use of `std::decay_t` in setter signatures strips reference qualifiers from the SField type alias, ensuring the parameter is always taken as a value and avoiding ambiguity when the underlying type is itself a reference typedef. This pattern repeats across all autogen builders. + +The transaction is annotated in `transactions.macro` as `Delegation::delegable`, meaning an `sfDelegate` field may be present and the network will validate delegated authority to burn NFTs on behalf of the account owner. It is also annotated with the `changeNFTCounts` privilege, reflecting that a successful burn decrements the NFToken page bookkeeping for the owning account — a ledger mutation that requires explicit privilege tracking in the XRPL permission model. The `uint256{}` amendment identifier (all zeros) signals that `NFTokenBurn` requires no specific amendment to be active; it is a base protocol transaction. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h.ai.json b/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h.ai.json new file mode 100644 index 0000000000..a35e341853 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h.ai.json @@ -0,0 +1,84 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 67, + "name": "account" + }, + { + "lineno": 68, + "name": "nFTokenOffers" + }, + { + "lineno": 69, + "name": "sequence" + }, + { + "lineno": 70, + "name": "fee" + }, + { + "lineno": 97, + "name": "publicKey" + }, + { + "lineno": 97, + "name": "secretKey" + }, + { + "lineno": 87, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "NFTokenCancelOffer" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& nFTokenOffers, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 59, + "name": "NFTokenCancelOfferBuilder" + } + ], + "description": "Defines the NFTokenCancelOffer transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getNFTokenOffers" + }, + { + "args": [ + "value" + ], + "lineno": 85, + "name": "setNFTokenOffers" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 95, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h.ai.md b/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h.ai.md new file mode 100644 index 0000000000..6f9ed14090 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenCancelOffer.h.ai.md @@ -0,0 +1,35 @@ +# `NFTokenCancelOffer.h` — Auto-generated Transaction Wrapper + +## Role and Context + +This header is part of the `protocol_autogen` layer in `xrpl::transactions`, a set of files generated from the XRPL transaction schema rather than hand-written. It defines two complementary classes for the `NFTokenCancelOffer` transaction type (`ttNFTOKEN_CANCEL_OFFER`, code 28): an immutable read-only wrapper and a mutable builder. This file is one of ~60 analogous files, each following the same structural pattern, located under `include/xrpl/protocol_autogen/transactions/`. + +An `NFTokenCancelOffer` transaction on the XRP Ledger removes one or more pending NFT buy or sell offer objects from the ledger. The submitting account does not need to own the offers; any account may cancel any offer, though the most common use is an offer creator cancelling their own open offers. + +## Class: `NFTokenCancelOffer` + +`NFTokenCancelOffer` extends `TransactionBase`, which wraps a `std::shared_ptr` — the canonical immutable XRPL serialized transaction type. The const qualifier on the shared pointer makes the entire class a read-only view: once constructed, no fields can be modified. + +Construction takes a `shared_ptr` and immediately verifies the transaction type via `tx_->getTxnType() != txType`. If the wrong transaction type is passed (for example, an `NFTokenCreateOffer` transaction accidentally routed to this constructor), a `std::runtime_error` is thrown at construction time rather than silently returning garbage from field accessors later. This fail-fast design keeps bugs close to their source. + +The only transaction-specific accessor is `getNFTokenOffers()`, which returns `SF_VECTOR256::type::value_type` — that is, a `std::vector`. Each `uint256` in the vector is the ledger object ID (key hash) of an `NFTokenOffer` ledger entry to be deleted. The field is `soeREQUIRED`, so no optional wrapper is needed; the underlying `STTx::at()` call will throw if the field is somehow absent, which should only happen if an improperly constructed transaction bypasses the builder. + +Common transaction fields (account, fee, sequence, signers, memos, etc.) are all inherited from `TransactionBase` and are not redeclared here. + +## Class: `NFTokenCancelOfferBuilder` + +`NFTokenCancelOfferBuilder` extends `TransactionBuilderBase` using CRTP (Curiously Recurring Template Pattern). This lets the base-class common setters — `setAccount()`, `setFee()`, `setFlags()`, `setLastLedgerSequence()`, and so on — return `Derived&` (i.e., `NFTokenCancelOfferBuilder&`) rather than `TransactionBuilderBase&`, preserving the fluent method-chaining interface across both base and derived setters without virtual dispatch overhead. + +The primary constructor accepts `account`, `nFTokenOffers`, and optionally `sequence` and `fee`. The `nFTokenOffers` argument uses `std::decay_t` — which strips references to avoid binding to temporaries — then passes `const&`. This is the standard pattern used across all vector-typed fields in this codebase. The constructor delegates to `TransactionBuilderBase`'s constructor to set the transaction type and account, then calls `setNFTokenOffers()`. + +A second constructor accepts an `std::shared_ptr`, copies the existing signed transaction into the internal `object_` (`STObject`), and allows re-editing. This path is guarded with the same type check. This is useful when deserializing a transaction from the wire or ledger and needing to produce a modified variant. + +The `build()` method is the terminal step: it calls the protected `sign()` helper from `TransactionBuilderBase`, which serializes the object with `HashPrefix::txSign` prepended (the XRPL signing prefix), computes the signature using the provided `PublicKey`/`SecretKey`, embeds the signature and public key into `object_`, then constructs and returns an `NFTokenCancelOffer` from a newly heap-allocated `STTx` that takes ownership of `object_` via `std::move`. After `build()` returns, `object_` is in a moved-from state; the builder must not be reused. + +## Design Observations + +This class pair illustrates a clean separation between construction-time mutability and runtime immutability. The builder owns a plain `STObject` (not yet a full `STTx`) to avoid the constraints that `STTx::applyTemplate()` would impose on unset default fields. Only at `build()` time does the `STObject` become a fully validated `STTx`. This avoids a subtle bug: calling `STTx` template application on an incomplete object would throw for fields that are `soeDEFAULT` but not explicitly set — a non-obvious failure mode that the comment in `TransactionBuilderBase`'s constructor explicitly calls out. + +The `NFTokenCancelOffer` transaction is marked as `Delegation::delegable` in its schema metadata, meaning the `sfDelegate` optional field (defined in `TransactionBase`) may be set, allowing a delegate account to submit this transaction on behalf of the originating account — a capability surfaced via `TransactionBase::setDelegate()` and `getDelegate()`. + +Because `sfNFTokenOffers` carries a vector of `uint256` IDs rather than a single ID, a single `NFTokenCancelOffer` can batch-cancel many offers in one ledger transaction, avoiding the fee overhead of submitting individual cancellations. The minimum meaningful content of this transaction is therefore one entry in the offers vector; the protocol enforces that the list is non-empty. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h.ai.json b/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h.ai.json new file mode 100644 index 0000000000..7cf26e3350 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h.ai.json @@ -0,0 +1,139 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "NFTokenCreateOffer" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& nFTokenID, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 126, + "name": "NFTokenCreateOfferBuilder" + } + ], + "description": "Defines the NFTokenCreateOffer transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing NFTokenCreateOffer transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 32, + "name": "NFTokenCreateOffer" + }, + { + "args": [], + "lineno": 48, + "name": "getNFTokenID" + }, + { + "args": [], + "lineno": 57, + "name": "getAmount" + }, + { + "args": [], + "lineno": 66, + "name": "getDestination" + }, + { + "args": [], + "lineno": 77, + "name": "hasDestination" + }, + { + "args": [], + "lineno": 86, + "name": "getOwner" + }, + { + "args": [], + "lineno": 97, + "name": "hasOwner" + }, + { + "args": [], + "lineno": 106, + "name": "getExpiration" + }, + { + "args": [], + "lineno": 117, + "name": "hasExpiration" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& nFTokenID", + "std::decay_t const& amount", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 134, + "name": "NFTokenCreateOfferBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 146, + "name": "NFTokenCreateOfferBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 157, + "name": "setNFTokenID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 166, + "name": "setAmount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 175, + "name": "setDestination" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 184, + "name": "setOwner" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 193, + "name": "setExpiration" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 202, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h.ai.md b/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h.ai.md new file mode 100644 index 0000000000..6a4447cb42 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenCreateOffer.h.ai.md @@ -0,0 +1,47 @@ +# `NFTokenCreateOffer.h` — Auto-generated Transaction Wrapper + +This file is part of a large family of auto-generated headers in `include/xrpl/protocol_autogen/transactions/` — one per XRPL transaction type. It defines two classes for the `NFTokenCreateOffer` transaction (type ID 27 / `ttNFTOKEN_CREATE_OFFER`): a read-only wrapper for inspecting existing signed transactions, and a fluent builder for constructing and signing new ones. The file itself carries the `// This file is auto-generated. Do not edit.` guard, meaning its structure is maintained by tooling rather than hand-written, but it is worth understanding deeply because all NFT secondary-market tooling in the codebase flows through these types. + +## The Wrapper/Builder Split + +Every transaction in this layer follows the same two-class pattern found across all siblings (`NFTokenAcceptOffer.h`, `Payment.h`, etc.): + +- **`NFTokenCreateOffer : TransactionBase`** — an immutable value object wrapping `std::shared_ptr`. All accessors are `const` and `[[nodiscard]]`. Once constructed it cannot be mutated. +- **`NFTokenCreateOfferBuilder : TransactionBuilderBase`** — a mutable staging object that accumulates field assignments and produces a signed `NFTokenCreateOffer` via `build()`. + +The separation enforces a clear lifecycle: build and sign once, then pass around only the immutable wrapper. There is no path to accidentally mutate a transaction after it leaves the builder. + +## Fields and Their Semantics + +The transaction carries two required fields and three optional ones, each reflecting a distinct aspect of the NFT offer protocol: + +**Required fields** (`soeREQUIRED`) — accessed directly, no `std::nullopt` path: + +- `sfNFTokenID` (`SF_UINT256`) — the 256-bit identifier of the NFToken being offered. This ID is the ledger object key for the token and encodes the issuer, taxon, and sequence in its bit layout. +- `sfAmount` (`SF_AMOUNT`) — the price being offered. `STAmount` on XRPL can represent either XRP drops or a fungible token IOU amount, so this single field covers both asset classes. + +**Optional fields** (`soeOPTIONAL`) — accessed via `getX()` returning `protocol_autogen::Optional`, guarded by a companion `hasX()`: + +- `sfDestination` (`SF_ACCOUNT`) — restricts the offer to a single counterparty. If set, only that account can accept the offer. This is important for private OTC deals or royalty-bearing transfers where the seller wants to control who receives the token. +- `sfOwner` (`SF_ACCOUNT`) — identifies the current holder of the NFToken. This field is absent when the submitter is creating a *sell offer* (they own the token themselves). It is required when creating a *buy offer* (the submitter wants to purchase a token from someone else), because the ledger needs to know which account's `NFTokenOffer` object to create the offer under. +- `sfExpiration` (`SF_UINT32`) — a ledger-close-time value after which the offer becomes invalid. Expressed as seconds since the Ripple epoch; the ledger rejects attempts to accept an expired offer. + +## `protocol_autogen::Optional` Alias + +Optional field getters return `protocol_autogen::Optional` rather than `std::optional` directly. The alias in `Utils.h` selects between `std::optional` (for non-reference types) and `std::optional>>` (for reference types). This matters because `STAmount` is returned by value from `STObject::at()`, so `getAmount()` yields a plain `STAmount` copy, while hypothetical reference-returning fields would yield a `reference_wrapper`. The alias handles this variance without requiring separate template specialisations at each call site. + +## Type-Safety Invariant + +Both the wrapper constructor and the STTx-deserialising builder constructor throw `std::runtime_error` immediately if `getTxnType() != ttNFTOKEN_CREATE_OFFER`. This fail-fast check means a mismatched STTx cannot silently masquerade as an `NFTokenCreateOffer`. The wrapper's `tx_` member is `const`-qualified via `shared_ptr`, so once the type check passes, the invariant holds for the lifetime of the object. + +## Builder Design and CRTP + +`NFTokenCreateOfferBuilder` inherits from `TransactionBuilderBase` using the Curiously Recurring Template Pattern. All setter methods in the base class return `Derived&` (i.e., `NFTokenCreateOfferBuilder&`) instead of a base reference, so method chains never lose their concrete type. The builder stores its work in `STObject object_{sfTransaction}`, deliberately without calling `object_.set(soTemplate)`: the base class comment explains that pre-populating `soeDEFAULT` fields would cause `STTx::applyTemplate()` to throw "may not be explicitly set to default". The `STTx` constructor itself handles missing fields correctly. + +The constructor enforces the two required fields at construction time — `nFTokenID` and `amount` are non-optional constructor parameters — while `sequence` and `fee` are `std::optional` to accommodate scenarios where the caller sets them later (e.g., when the ledger auto-fills `Sequence`). + +The secondary constructor `NFTokenCreateOfferBuilder(std::shared_ptr)` copies an existing signed transaction back into the mutable `object_` via `object_ = *tx`, enabling re-signing or amendment of a previously built transaction without re-specifying every field from scratch. + +## `build()` and Signing + +`build(PublicKey, SecretKey)` delegates to the protected `sign()` method on `TransactionBuilderBase`, which serialises the `STObject` with `HashPrefix::txSign` prepended (per the XRPL signing protocol), computes an `ed25519`/`secp256k1` signature, and stores it in `sfTxnSignature`. It then constructs an `STTx` by moving `object_`, wraps it in a `shared_ptr`, and forwards it to the `NFTokenCreateOffer` constructor. At that point the transaction is immutable and the builder is spent — `object_` has been moved from. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenMint.h.ai.json b/include/xrpl/protocol_autogen/transactions/NFTokenMint.h.ai.json new file mode 100644 index 0000000000..b3c107c7ec --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenMint.h.ai.json @@ -0,0 +1,186 @@ +{ + "args": [ + { + "lineno": 36, + "name": "tx" + }, + { + "lineno": 188, + "name": "account" + }, + { + "lineno": 189, + "name": "nFTokenTaxon" + }, + { + "lineno": 190, + "name": "sequence" + }, + { + "lineno": 191, + "name": "fee" + }, + { + "lineno": 267, + "name": "publicKey" + }, + { + "lineno": 267, + "name": "secretKey" + }, + { + "lineno": 204, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 33, + "name": "NFTokenMint" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& nFTokenTaxon, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 181, + "name": "NFTokenMintBuilder" + } + ], + "description": "Defines the NFTokenMint transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing NFTokenMint transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/NFTokenMint.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getNFTokenTaxon" + }, + { + "args": [], + "lineno": 54, + "name": "getTransferFee" + }, + { + "args": [], + "lineno": 65, + "name": "hasTransferFee" + }, + { + "args": [], + "lineno": 75, + "name": "getIssuer" + }, + { + "args": [], + "lineno": 86, + "name": "hasIssuer" + }, + { + "args": [], + "lineno": 96, + "name": "getURI" + }, + { + "args": [], + "lineno": 107, + "name": "hasURI" + }, + { + "args": [], + "lineno": 117, + "name": "getAmount" + }, + { + "args": [], + "lineno": 128, + "name": "hasAmount" + }, + { + "args": [], + "lineno": 138, + "name": "getDestination" + }, + { + "args": [], + "lineno": 149, + "name": "hasDestination" + }, + { + "args": [], + "lineno": 159, + "name": "getExpiration" + }, + { + "args": [], + "lineno": 170, + "name": "hasExpiration" + }, + { + "args": [ + "value" + ], + "lineno": 202, + "name": "setNFTokenTaxon" + }, + { + "args": [ + "value" + ], + "lineno": 211, + "name": "setTransferFee" + }, + { + "args": [ + "value" + ], + "lineno": 220, + "name": "setIssuer" + }, + { + "args": [ + "value" + ], + "lineno": 229, + "name": "setURI" + }, + { + "args": [ + "value" + ], + "lineno": 238, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 247, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 256, + "name": "setExpiration" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 265, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenMint.h.ai.md b/include/xrpl/protocol_autogen/transactions/NFTokenMint.h.ai.md new file mode 100644 index 0000000000..7293ba1d2f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenMint.h.ai.md @@ -0,0 +1,55 @@ +# `NFTokenMint.h` — Auto-generated NFT Minting Transaction Wrapper + +## Role and Context + +This file is part of the `protocol_autogen` layer inside `xrpl::transactions`, a collection of ~70 auto-generated transaction wrappers covering every XRPL transaction type. It is not intended to be edited by hand. The file defines two complementary classes for the `ttNFTOKEN_MINT` transaction type (opcode 25): `NFTokenMint`, an immutable read accessor, and `NFTokenMintBuilder`, a fluent construction interface. Together they enforce a strict separation between reading a transaction that already exists on-chain and building a new one to be submitted. + +The NFTokenMint transaction creates a Non-Fungible Token on the XRP Ledger. It is marked as `delegable` (meaning another account may be authorized to issue it on behalf of the token creator) and requires the `changeNFTCounts` privilege, reflecting the ledger-state mutations it entails. + +## `NFTokenMint` — Immutable Wrapper + +`NFTokenMint` inherits from `TransactionBase`, which itself holds a `std::shared_ptr`. Const-ness is threaded all the way through: the shared pointer owns a `const`-qualified `STTx`, so neither the wrapper nor any caller can mutate the underlying serialized object. This is deliberately immutable — any modification requires going through the builder. + +The constructor accepts a `std::shared_ptr` and immediately validates the transaction type. This guard exists because `TransactionBase` is a generic wrapper and the type is only known at the derived-class level; the runtime check at construction time is the only opportunity to enforce type safety before the typed accessors are exposed. + +The class exposes one required field and six optional fields: + +| Field | XRPL type | Optionality | Meaning | +|---|---|---|---| +| `sfNFTokenTaxon` | `uint32` | Required | Groups NFTs by semantic category | +| `sfTransferFee` | `uint16` | Optional | Royalty in units of 1/100,000 per transfer | +| `sfIssuer` | `AccountID` | Optional | Original issuer when minting on behalf of another account | +| `sfURI` | `Blob` | Optional | URI pointing to token metadata | +| `sfAmount` | `STAmount` | Optional | Asking price for an initial offer attached at mint time | +| `sfDestination` | `AccountID` | Optional | Restricts who may purchase the initial offer | +| `sfExpiration` | `uint32` | Optional | Ripple epoch after which the initial offer expires | + +Every optional field follows the `has*()`/`get*()` accessor pair pattern rather than a single `get*()` returning `Optional` with a conditional check inside. This is more than style: the `has*()` method calls `isFieldPresent()` on the `STTx` directly and is cheap; callers can branch on presence before paying the cost of field deserialization when they don't need the value. + +Return types for optional getters use the `protocol_autogen::Optional` alias defined in `Utils.h`: + +```cpp +template +using Optional = std::conditional_t< + std::is_reference_v, + std::optional>>, + std::optional>; +``` + +This alias exists because `std::optional` cannot hold a raw reference type. When a field's native C++ representation is a reference (e.g., `STArray const&` for array fields like `sfMemos`), the alias transparently wraps it in `std::reference_wrapper`. For value types — which all NFTokenMint fields are — the alias degenerates to plain `std::optional`. The uniform alias lets the code generator use the same getter template regardless of whether the underlying XRPL field is a value or reference type. + +All getter methods are marked `[[nodiscard]]`, preventing callers from silently discarding return values, a subtle class of bug that can occur when reading a field for side-effect (there are none, but the annotation costs nothing and documents intent). + +## `NFTokenMintBuilder` — Fluent Construction + +`NFTokenMintBuilder` inherits from the CRTP base `TransactionBuilderBase`. The Curiously Recurring Template Pattern is essential here: the base class exposes setters for common fields (`setAccount`, `setFee`, `setSequence`, `setLastLedgerSequence`, etc.) that return `Derived&` rather than `TransactionBuilderBase&`. This makes method chaining work transparently across the type boundary — a call to `setLastLedgerSequence(n).setNFTokenTaxon(t)` chains correctly even though `setLastLedgerSequence` is defined in the base. Without CRTP, the base would have to return `TransactionBuilderBase&` and the chain would lose access to derived setters. + +The builder stores work-in-progress state as an `STObject` named `object_` (initialized with `sfTransaction`), not as an `STTx`. Crucially, the base class constructor notes that it deliberately avoids calling `object_.set(soTemplate)`. The `STTx` constructor internally calls `applyTemplate()`, which enforces field requirements. If `soeDEFAULT` fields were pre-seeded as `STBase` placeholders in the `STObject`, `applyTemplate()` would throw "may not be explicitly set to default". Deferring to `STTx` construction at `build()` time means the template is applied exactly once, correctly, at the point where the object becomes final. + +The primary constructor requires `account` and `nFTokenTaxon` (the only mandatory transaction-specific field), while `sequence` and `fee` are `std::optional` to accommodate cases such as auto-filled transactions or delegated signing workflows where these values are provided later or externally. The builder also provides a secondary constructor that accepts an existing `std::shared_ptr`, enabling a round-trip: deserialize a transaction, reconstruct a builder, modify optional fields, and re-sign. This pattern is useful in testing and in relay scenarios. + +The `build()` method calls the protected `sign()` from `TransactionBuilderBase`, which serializes the object with `HashPrefix::txSign` prepended (following the XRPL signing convention), computes the signature over that payload, and embeds both the public key and signature into `object_`. The signed `STObject` is then moved into a new `STTx` and wrapped in a `NFTokenMint` value, consuming the builder state. The move into `STTx` avoids a copy of what can be a non-trivial serialized structure. + +## Relationship to the Rest of the Module + +This file is one of roughly 70 identically structured generated headers in `include/xrpl/protocol_autogen/transactions/`. Each one captures exactly the field schema of one transaction type and no more. The common infrastructure (`TransactionBase`, `TransactionBuilderBase`, `Utils.h`) is written once and parameterized through inheritance and templates, so the generated files remain minimal. The separation also means the hand-maintained base classes can evolve — adding support for new common fields or signing modes — without regenerating every transaction file. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenModify.h.ai.json b/include/xrpl/protocol_autogen/transactions/NFTokenModify.h.ai.json new file mode 100644 index 0000000000..a891fbc9fd --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenModify.h.ai.json @@ -0,0 +1,130 @@ +{ + "args": [ + { + "lineno": 36, + "name": "tx" + }, + { + "lineno": 110, + "name": "account" + }, + { + "lineno": 111, + "name": "nFTokenID" + }, + { + "lineno": 112, + "name": "sequence" + }, + { + "lineno": 113, + "name": "fee" + }, + { + "lineno": 119, + "name": "tx" + }, + { + "lineno": 121, + "name": "value" + }, + { + "lineno": 130, + "name": "value" + }, + { + "lineno": 139, + "name": "value" + }, + { + "lineno": 148, + "name": "publicKey" + }, + { + "lineno": 148, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 33, + "name": "NFTokenModify" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& nFTokenID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 101, + "name": "NFTokenModifyBuilder" + } + ], + "description": "Defines the NFTokenModify transaction and its builder for the XRPL protocol, providing type-safe accessors and a fluent builder interface for constructing and signing NFTokenModify transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/NFTokenModify.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getNFTokenID" + }, + { + "args": [], + "lineno": 54, + "name": "getOwner" + }, + { + "args": [], + "lineno": 65, + "name": "hasOwner" + }, + { + "args": [], + "lineno": 75, + "name": "getURI" + }, + { + "args": [], + "lineno": 86, + "name": "hasURI" + }, + { + "args": [ + "value" + ], + "lineno": 120, + "name": "setNFTokenID" + }, + { + "args": [ + "value" + ], + "lineno": 129, + "name": "setOwner" + }, + { + "args": [ + "value" + ], + "lineno": 138, + "name": "setURI" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 147, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/NFTokenModify.h.ai.md b/include/xrpl/protocol_autogen/transactions/NFTokenModify.h.ai.md new file mode 100644 index 0000000000..4d443dfb94 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/NFTokenModify.h.ai.md @@ -0,0 +1,47 @@ +# `NFTokenModify.h` — Auto-Generated Wrapper and Builder for the NFTokenModify Transaction + +This file is machine-generated from the XRPL transaction schema and lives inside the `protocol_autogen/transactions/` layer. Its purpose is to expose the `ttNFTOKEN_MODIFY` transaction (type 61) through two complementary C++ abstractions: an **immutable read-only wrapper** (`NFTokenModify`) and a **fluent mutable builder** (`NFTokenModifyBuilder`). Neither class should be hand-edited; both are regenerated whenever the transaction schema changes. + +## Ledger Context + +`NFTokenModify` is the transaction introduced by the `featureDynamicNFT` amendment to allow the `sfURI` field of an existing NFT to be changed after the token has been minted. The original XRPL NFT design treated all fields as immutable once minted; the `DynamicNFT` amendment lifts that constraint for URI metadata only. The canonical schema, confirmed in `transactions.macro`, is: + +``` +TRANSACTION(ttNFTOKEN_MODIFY, 61, NFTokenModify, + Delegation::delegable, featureDynamicNFT, noPriv, + ({ sfNFTokenID soeREQUIRED }, { sfOwner soeOPTIONAL }, { sfURI soeOPTIONAL })) +``` + +The transaction is marked `Delegation::delegable`, meaning a delegate account can submit it on behalf of the actual token owner without holding the owner's keys directly. + +## `NFTokenModify` — The Immutable Wrapper + +`NFTokenModify` inherits from `TransactionBase`, which wraps a `std::shared_ptr`. The `const` propagates throughout: callers can read fields but cannot alter the underlying serialized transaction. This is the representation used after a transaction has been deserialized or signed. + +The constructor takes a `shared_ptr` and immediately asserts that `getTxnType() == ttNFTOKEN_MODIFY`, throwing `std::runtime_error` on mismatch. This fail-fast check prevents type confusion when code routes arbitrary `STTx` objects to the wrong wrapper class. + +The three transaction-specific fields map directly to the schema: + +- **`getNFTokenID()`** — required; returns `SF_UINT256::type::value_type` directly (a `uint256`), no optionality. +- **`getOwner()` / `hasOwner()`** — optional; returns `protocol_autogen::Optional`. The `Optional` alias from `Utils.h` resolves to `std::optional` for non-reference types and `std::optional>` for reference types, keeping the interface uniform without requiring callers to reason about reference lifetime. +- **`getURI()` / `hasURI()`** — optional; returns `protocol_autogen::Optional` (a variable-length blob). The URI being optional is intentional: you can submit an `NFTokenModify` without a URI to clear it, or omit it entirely to leave it unchanged. + +All getters are marked `[[nodiscard]]`. The `has*` / `get*` pairing for optional fields mirrors the pattern used throughout `TransactionBase` — call `has*` first, then `get*` to avoid a `std::nullopt` return — though `get*` safely returns `std::nullopt` internally when the field is absent. + +`TransactionBase` also provides accessors for the entire common transaction field set: `getAccount()`, `getSequence()`, `getFee()`, `getFlags()`, `getMemos()`, `getSigners()`, `getDelegate()`, `getLastLedgerSequence()`, and more. `NFTokenModify` inherits all of these unchanged. + +## `NFTokenModifyBuilder` — The Fluent Builder + +`NFTokenModifyBuilder` extends `TransactionBuilderBase`. The base class is a CRTP template; all common setters return `Derived&` so that chains like `builder.setFlags(x).setLastLedgerSequence(y).setURI(uri)` resolve to the concrete builder type without casts. + +Internally, the builder holds a mutable `STObject object_{sfTransaction}` declared in the base class. This is deliberately kept as a free object — `object_.set(soTemplate)` is never called — because doing so would create `STBase` placeholders for `soeDEFAULT` fields, which would then cause `applyTemplate()` to throw "may not be explicitly set to default" when the `STTx` constructor is invoked. The `STTx` constructor calls `applyTemplate()` itself and handles missing fields correctly. + +The primary constructor requires `account` and `nFTokenID` (the only `soeREQUIRED` field beyond the common transaction fields), with `sequence` and `fee` as optional parameters. Required fields at the protocol level are surfaced as constructor parameters to make it impossible to call `build()` without them. + +A secondary constructor accepts an existing `std::shared_ptr` and copies its content into `object_` via `object_ = *tx`. This round-trip path — deserialize an existing transaction, mutate it, re-sign — is useful in testing and tooling contexts. + +The `build()` method calls the protected `sign()` from `TransactionBuilderBase`, which prefixes the serialized object with `HashPrefix::txSign`, calls `addWithoutSigningFields()` to exclude signing-related fields from the hash, signs the result with the provided keys, and stores the signature in `sfTxnSignature`. The builder then wraps the finalized `STObject` in a freshly constructed `STTx` and hands it to the `NFTokenModify` constructor, producing the immutable read side. + +## Design Philosophy + +The clean split between `NFTokenModify` (const, shared ownership, read-only) and `NFTokenModifyBuilder` (mutable, value-oriented, write-then-discard) ensures that a signed transaction can never be accidentally mutated after the fact. The autogeneration strategy means the field list, optionality, and types are always in sync with the canonical schema in `transactions.macro` without any manual synchronization. The cost is that the file cannot be customized — any protocol-level change to `NFTokenModify`'s field set automatically forces regeneration of this header. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/OfferCancel.h.ai.json b/include/xrpl/protocol_autogen/transactions/OfferCancel.h.ai.json new file mode 100644 index 0000000000..48a0e3ba62 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/OfferCancel.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 30, + "name": "tx" + }, + { + "lineno": 67, + "name": "account" + }, + { + "lineno": 68, + "name": "offerSequence" + }, + { + "lineno": 69, + "name": "sequence" + }, + { + "lineno": 70, + "name": "fee" + }, + { + "lineno": 77, + "name": "tx" + }, + { + "lineno": 86, + "name": "value" + }, + { + "lineno": 95, + "name": "publicKey" + }, + { + "lineno": 95, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "OfferCancel" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& offerSequence, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 61, + "name": "OfferCancelBuilder" + } + ], + "description": "Defines the OfferCancel transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing OfferCancel transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/OfferCancel.h", + "functions": [ + { + "args": [], + "lineno": 41, + "name": "OfferCancel::getOfferSequence" + }, + { + "args": [ + "value" + ], + "lineno": 85, + "name": "OfferCancelBuilder::setOfferSequence" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 94, + "name": "OfferCancelBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/OfferCancel.h.ai.md b/include/xrpl/protocol_autogen/transactions/OfferCancel.h.ai.md new file mode 100644 index 0000000000..f333e070c1 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/OfferCancel.h.ai.md @@ -0,0 +1,73 @@ +# `OfferCancel.h` — Auto-generated OfferCancel Transaction Wrapper + +## Role and Context + +This file is part of the `protocol_autogen` layer — a code-generated set of type-safe C++ wrappers over the XRPL's raw `STTx` serialized transaction format. Every transaction type in the ledger gets its own header here, following a uniform two-class pattern: an immutable reader and a fluent builder. `OfferCancel.h` encodes transaction type `ttOFFER_CANCEL` (8), which removes a previously placed order from the XRP Ledger's decentralized exchange (DEX). + +The file carries the `// This file is auto-generated. Do not edit.` directive at line 1, placing it firmly in the machine-managed portion of the codebase. The entire `protocol_autogen/transactions/` directory contains ~70 such headers, one per ledger transaction type, all sharing the same structural skeleton while varying only in their transaction-specific fields. + +## `OfferCancel` — The Immutable Wrapper + +`OfferCancel` extends `TransactionBase`, a thin immutable shell that holds a `std::shared_ptr` and exposes type-safe accessors for the fields common to every transaction (account, sequence, fee, flags, memos, signers, etc.). The subclass adds exactly one transaction-specific accessor: + +```cpp +[[nodiscard]] +SF_UINT32::type::value_type +getOfferSequence() const +{ + return this->tx_->at(sfOfferSequence); +} +``` + +`sfOfferSequence` is declared `soeREQUIRED`, meaning the ledger engine guarantees it is always present in a well-formed `OfferCancel`. The accessor therefore returns by value without an `std::optional` guard — a deliberate design choice that matches the field's optionality metadata and avoids forcing callers to unwrap an optional that can never be empty. + +The constructor takes a `std::shared_ptr` and immediately validates the embedded transaction type: + +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for OfferCancel"); +``` + +This guard is the boundary between the untyped world of wire-format deserialization and the strongly typed autogen layer. Once construction succeeds the object can only represent a valid `ttOFFER_CANCEL`, making subsequent dispatch by type unnecessary and preventing misuse by callers who might accidentally wrap an `STTx` of the wrong kind. + +The static `txType` constexpr member is available for compile-time dispatch or template specializations that need to map a C++ type back to its ledger integer constant. + +## `OfferCancelBuilder` — The Fluent Builder + +`OfferCancelBuilder` extends `TransactionBuilderBase`, which uses the Curiously Recurring Template Pattern (CRTP) to return `Derived&` from every setter, enabling method chaining without virtual dispatch or casting overhead. The base holds an `STObject object_{sfTransaction}` that accumulates field assignments before being moved into a final `STTx`. + +The builder's primary constructor enforces `sfOfferSequence` as a required argument at the call site: + +```cpp +OfferCancelBuilder( + SF_ACCOUNT::type::value_type account, + std::decay_t const& offerSequence, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt) +``` + +`account` and `offerSequence` are positional and required; `sequence` and `fee` are optional because in some testing or pseudo-transaction contexts they may be filled in later. The design mirrors the ledger's own field optionality rules: required fields are constructor arguments, optional fields are fluent setters inherited from `TransactionBuilderBase`. + +A second constructor takes an existing `std::shared_ptr` and copies its fields into the mutable `object_`, allowing an already-serialized transaction to be re-inflated into a builder for modification before re-signing. It performs the same type guard as the read-side wrapper. + +`setOfferSequence()` returns `OfferCancelBuilder&`, completing the fluent chain and making it possible to override the value set at construction if needed. + +The `build()` method finalizes the transaction: + +```cpp +OfferCancel build(PublicKey const& publicKey, SecretKey const& secretKey) +{ + sign(publicKey, secretKey); + return OfferCancel{std::make_shared(std::move(object_))}; +} +``` + +`sign()` is a protected method on `TransactionBuilderBase` that serializes the object with `HashPrefix::txSign`, signs it with the provided key pair, and stamps `sfSigningPubKey` and `sfTxnSignature` into the mutable `object_` before ownership is transferred. After `build()` the builder's `object_` is in a moved-from state and should not be reused. + +## Design Tradeoffs + +The immutability split between `OfferCancel` (read-only) and `OfferCancelBuilder` (write/build) is intentional. Once a transaction has been signed, its bytes must not change — any mutation would invalidate the cryptographic signature. By making `OfferCancel` wrap a `shared_ptr`, the type system enforces this invariant at compile time rather than relying on runtime checks. + +The autogen approach trades flexibility for safety and consistency. `OfferCancel` has only one transaction-specific field — `sfOfferSequence` — which is the sequence number of the `OfferCreate` transaction that originally placed the order. There are no optional DEX-specific fields: no amount, no expiration, no flags unique to this type. The cancel transaction is intentionally minimal, relying entirely on the base-class machinery. In contrast, `OfferCreate` adds five or more fields covering `sfTakerPays`, `sfTakerGets`, `sfExpiration`, and `sfOfferSequence` (as an optional reference to a prior offer to replace). The structural symmetry across the autogen layer means that tooling or introspection code can rely on the same base interface regardless of which transaction type it encounters. + +The `Delegation::delegable` annotation in the class comment indicates that `OfferCancel` supports the `sfDelegate` field, meaning a delegate account can submit this transaction on behalf of the originating account — a capability surfaced through `TransactionBase::getDelegate()` and `TransactionBuilderBase::setDelegate()` without any additional code in this file. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/OfferCreate.h.ai.json b/include/xrpl/protocol_autogen/transactions/OfferCreate.h.ai.json new file mode 100644 index 0000000000..47287b1ec5 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/OfferCreate.h.ai.json @@ -0,0 +1,151 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 139, + "name": "account" + }, + { + "lineno": 139, + "name": "takerPays" + }, + { + "lineno": 139, + "name": "takerGets" + }, + { + "lineno": 139, + "name": "sequence" + }, + { + "lineno": 139, + "name": "fee" + }, + { + "lineno": 206, + "name": "publicKey" + }, + { + "lineno": 206, + "name": "secretKey" + }, + { + "lineno": 156, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "OfferCreate" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& takerPays, std::decay_t const& takerGets, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 132, + "name": "OfferCreateBuilder" + } + ], + "description": "Defines the OfferCreate transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing OfferCreate transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/OfferCreate.h", + "functions": [ + { + "args": [], + "lineno": 41, + "name": "getTakerPays" + }, + { + "args": [], + "lineno": 52, + "name": "getTakerGets" + }, + { + "args": [], + "lineno": 63, + "name": "getExpiration" + }, + { + "args": [], + "lineno": 74, + "name": "hasExpiration" + }, + { + "args": [], + "lineno": 85, + "name": "getOfferSequence" + }, + { + "args": [], + "lineno": 96, + "name": "hasOfferSequence" + }, + { + "args": [], + "lineno": 107, + "name": "getDomainID" + }, + { + "args": [], + "lineno": 118, + "name": "hasDomainID" + }, + { + "args": [ + "value" + ], + "lineno": 154, + "name": "setTakerPays" + }, + { + "args": [ + "value" + ], + "lineno": 164, + "name": "setTakerGets" + }, + { + "args": [ + "value" + ], + "lineno": 174, + "name": "setExpiration" + }, + { + "args": [ + "value" + ], + "lineno": 184, + "name": "setOfferSequence" + }, + { + "args": [ + "value" + ], + "lineno": 194, + "name": "setDomainID" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 204, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/OfferCreate.h.ai.md b/include/xrpl/protocol_autogen/transactions/OfferCreate.h.ai.md new file mode 100644 index 0000000000..6d20c62fb5 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/OfferCreate.h.ai.md @@ -0,0 +1,41 @@ +# `OfferCreate.h` — Auto-Generated OfferCreate Transaction Wrapper + +## Role in the System + +This file is part of the `protocol_autogen` layer — a family of auto-generated headers under `include/xrpl/protocol_autogen/transactions/` that provide type-safe C++ wrappers for every XRPL transaction type. The directory contains over 70 such files, one per transaction kind. `OfferCreate.h` wraps `ttOFFER_CREATE` (type code 7), the transaction that places a limit order on XRPL's built-in decentralized exchange. It should never be edited by hand; regenerate it from the source schema instead. + +The file lives within `namespace xrpl::transactions` and exposes exactly two classes: an immutable read accessor (`OfferCreate`) and a fluent construction helper (`OfferCreateBuilder`). + +## `OfferCreate` — Immutable Transaction Wrapper + +`OfferCreate` extends `TransactionBase`, which holds a `std::shared_ptr` and provides accessors for all universal transaction fields (account, sequence, fee, flags, memos, signers, delegate, etc.). `OfferCreate` narrows that to the fields specific to DEX offers. + +Construction takes a `std::shared_ptr`. The constructor immediately validates the transaction's type tag against the class-level constant `txType = ttOFFER_CREATE` and throws `std::runtime_error` on mismatch. This is a runtime guard rather than a compile-time one — necessary because `STTx` is a polymorphic, schema-driven object decoded from wire bytes or JSON, so the type cannot be statically enforced at construction time. + +### Fields and Why They Are Typed This Way + +`getTakerPays()` and `getTakerGets()` both return `SF_AMOUNT::type::value_type`. On XRPL, `sfTakerPays` represents the asset the offer creator is willing to pay and `sfTakerGets` is what they want in return. Both are marked `soeREQUIRED` in the transaction schema and both are annotated with `@note This field supports MPT (Multi-Purpose Token) amounts`, reflecting the `mayCreateMPT` privilege listed in the class docblock. Since MPT amounts share the same `SF_AMOUNT` type as classic XRP/IOU amounts, no special branching is needed in the accessor — the underlying `STAmount` representation handles it. + +Optional fields follow a paired `get`/`has` pattern: + +- `getExpiration()` / `hasExpiration()` — a `uint32` ledger close time after which the offer is automatically considered expired. Callers must check `hasExpiration()` first, or use the returned `protocol_autogen::Optional` which is `std::nullopt` when absent. +- `getOfferSequence()` / `hasOfferSequence()` — if present, the offer at this sequence number on the submitting account is canceled atomically when this offer is placed. This is the standard one-step cancel-and-replace mechanism. +- `getDomainID()` / `hasDomainID()` — an optional `uint256` identifying a permissioned domain, a newer XRPL feature allowing restricted trading venues. + +The `protocol_autogen::Optional` alias (defined in `Utils.h`) is a thin template that resolves to `std::optional>>` for reference types and plain `std::optional` for value types. For the scalar fields used here the distinction is transparent, but it matters for `STArray`-typed optional fields in other transaction wrappers. + +## `OfferCreateBuilder` — Fluent Construction + +`OfferCreateBuilder` extends `TransactionBuilderBase`, a CRTP base that stores a mutable `STObject object_{sfTransaction}`. The CRTP trick makes all common setters (`setFlags`, `setLastLedgerSequence`, `setMemo`, etc.) return `OfferCreateBuilder&` rather than `TransactionBuilderBase&`, enabling unbroken method chains across both base and derived setters. + +**Intentional design detail:** `TransactionBuilderBase` deliberately does **not** call `object_.set(soTemplate)` during construction. The inline comment explains why: initializing with a template inserts `soeDEFAULT` field placeholders, and the `STTx` constructor later calls `applyTemplate()`, which throws "may not be explicitly set to default" for any field that carries a placeholder value. By keeping the `STObject` free-form and letting `STTx`'s own constructor apply the schema, the builder sidesteps that constraint entirely. + +The primary constructor requires `account`, `takerPays`, and `takerGets` (the two `soeREQUIRED` offer-specific fields), with `sequence` and `fee` as `std::optional` parameters routed to the base class. A secondary constructor accepts an existing `std::shared_ptr` and copies its `STObject` representation into `object_`, enabling round-trip editing of a transaction received from elsewhere — it too validates the type tag before copying. + +### Finalizing with `build()` + +`build(PublicKey, SecretKey)` calls the protected `sign()` inherited from `TransactionBuilderBase`. That method serializes the object **without** signing fields (via `addWithoutSigningFields`), prepends the `HashPrefix::txSign` magic bytes, computes an ECDSA/Ed25519 signature with the provided key pair, writes `sfSigningPubKey` and `sfTxnSignature` back onto the `STObject`, then constructs a `std::shared_ptr` from the now-final object. The returned `OfferCreate` wrapper is immediately validated against `ttOFFER_CREATE`, completing the construction loop. + +## Relationship to Sibling Files + +Every header in the `transactions/` directory follows this identical two-class layout (wrapper + builder). They are all generated from the same template, differing only in their field sets and type codes. Shared infrastructure lives in `TransactionBase.h` and `TransactionBuilderBase.h`; the `Utils.h` `Optional` alias and `STObjectValidation.h` validator are consumed indirectly through those bases. Nothing in `OfferCreate.h` is hand-authored beyond the generated output; the canonical source of truth for the field list is the upstream transaction schema definition. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/OracleDelete.h.ai.json b/include/xrpl/protocol_autogen/transactions/OracleDelete.h.ai.json new file mode 100644 index 0000000000..032374b32a --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/OracleDelete.h.ai.json @@ -0,0 +1,112 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 62, + "name": "account" + }, + { + "lineno": 62, + "name": "oracleDocumentID" + }, + { + "lineno": 62, + "name": "sequence" + }, + { + "lineno": 62, + "name": "fee" + }, + { + "lineno": 73, + "name": "tx" + }, + { + "lineno": 84, + "name": "value" + }, + { + "lineno": 92, + "name": "publicKey" + }, + { + "lineno": 92, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "OracleDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& oracleDocumentID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 57, + "name": "OracleDeleteBuilder" + } + ], + "description": "Defines the OracleDelete transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing OracleDelete transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/OracleDelete.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "OracleDelete" + }, + { + "args": [], + "lineno": 44, + "name": "getOracleDocumentID" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& oracleDocumentID", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 62, + "name": "OracleDeleteBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 73, + "name": "OracleDeleteBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 84, + "name": "setOracleDocumentID" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 92, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/OracleDelete.h.ai.md b/include/xrpl/protocol_autogen/transactions/OracleDelete.h.ai.md new file mode 100644 index 0000000000..b1007d7f8b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/OracleDelete.h.ai.md @@ -0,0 +1,40 @@ +# OracleDelete.h — Auto-Generated Transaction Wrapper for `ttORACLE_DELETE` + +This file is part of the `xrpl/protocol_autogen` layer, a code-generated collection of strongly-typed C++ wrappers for every XRPL transaction type. It defines two classes — `OracleDelete` and `OracleDeleteBuilder` — that together provide a safe, ergonomic API for consuming and constructing `ttORACLE_DELETE` transactions (type code 52) without touching the raw `STTx` machinery directly. + +## Role in the System + +`OracleDelete` is one half of the Price Oracle transaction family introduced by the `featurePriceOracle` amendment. Its counterpart, `OracleSet` (type 51), creates or updates an on-ledger oracle object, populating fields like `sfProvider`, `sfURI`, `sfAssetClass`, `sfLastUpdateTime`, and `sfPriceDataSeries`. `OracleDelete`, by contrast, is a deletion transaction: it requires only a single field — `sfOracleDocumentID` — to identify and remove the oracle object from the ledger. The stark difference in surface area between the two transaction types is reflected directly in the generated code: `OracleSet` exposes six field accessors and five field setters; `OracleDelete` exposes exactly one of each. + +## Immutable Wrapper: `OracleDelete` + +`OracleDelete` extends `TransactionBase`, which holds a `std::shared_ptr` and provides read-only accessors for the common transaction fields shared by all transaction types (`sfAccount`, `sfFee`, `sfSequence`, `sfFlags`, `sfMemos`, `sfDelegate`, etc.). The derived `OracleDelete` adds a single transaction-specific accessor, `getOracleDocumentID()`, which returns the `uint32_t` document ID directly via `tx_->at(sfOracleDocumentID)`. + +The constructor enforces type safety with an explicit runtime check: + +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for OracleDelete"); +``` + +This guard prevents an `OracleDelete` wrapper from accidentally being constructed around a different transaction type (e.g., an `OracleSet` or a `Payment`). Because the underlying `STTx` is `const`-qualified and held through a shared pointer, the wrapper itself is truly immutable after construction — callers cannot mutate the transaction through this class. + +## Builder: `OracleDeleteBuilder` + +`OracleDeleteBuilder` inherits from `TransactionBuilderBase` via CRTP (Curiously Recurring Template Pattern). This template base class provides all common field setters (`setAccount()`, `setFee()`, `setSequence()`, `setLastLedgerSequence()`, `setDelegate()`, etc.) and returns `Derived&` from each setter, enabling fluent method chaining without virtual dispatch overhead. + +The builder constructor accepts `sfOracleDocumentID` as a required parameter (alongside the account; sequence and fee are optional) and immediately calls `setOracleDocumentID()` to store it in the internal `STObject object_{sfTransaction}`. A critical implementation note is that the base class constructor deliberately avoids calling `object_.set(soTemplate)`. This ensures no `soeDEFAULT` placeholder fields are pre-populated, which would cause `STTx::applyTemplate()` to throw "may not be explicitly set to default" during the final `build()` step. + +A second constructor accepts an existing `std::shared_ptr`, copying the transaction's fields into `object_` so the builder can reconstruct or modify a previously deserialized transaction. This path also validates the transaction type and throws if mismatched. + +The `build()` method finalises the transaction lifecycle: +1. It calls `sign(publicKey, secretKey)` from the base class, which serializes the object (excluding signing fields), prepends `HashPrefix::txSign`, and appends the computed signature to `sfTxnSignature`. +2. It moves `object_` into a new `STTx` wrapped in a `shared_ptr`, then passes it to the `OracleDelete` constructor, returning an immutable wrapper ready for submission. + +## Auto-Generated Nature + +The file header reads `// This file is auto-generated. Do not edit.` The entire `protocol_autogen/transactions/` directory contains one file per XRPL transaction type following an identical structural template. Adding a new transaction type or changing an existing field schema regenerates these files from a canonical source-of-truth definition rather than requiring manual edits scattered across many files. This design eliminates the risk of accessor/setter drift and ensures that the type system precisely mirrors the protocol definition at all times. + +## Design Tradeoffs + +The wrapper/builder split is a deliberate design choice over a single mutable class. Keeping `OracleDelete` read-only means code that receives a completed transaction can never accidentally mutate it, even though `STTx` itself is not entirely immune to misuse through raw field access. The `[[nodiscard]]` attribute on all getters enforces that return values are not silently discarded. The use of `std::decay_t` for builder setter parameters is a defensive measure to handle reference-qualified type aliases cleanly regardless of how the `SField` type traits resolve in a given compilation context. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/OracleSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/OracleSet.h.ai.json new file mode 100644 index 0000000000..22a67e2593 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/OracleSet.h.ai.json @@ -0,0 +1,210 @@ +{ + "args": [ + { + "lineno": 36, + "name": "tx" + }, + { + "lineno": 147, + "name": "account" + }, + { + "lineno": 148, + "name": "oracleDocumentID" + }, + { + "lineno": 149, + "name": "lastUpdateTime" + }, + { + "lineno": 150, + "name": "priceDataSeries" + }, + { + "lineno": 151, + "name": "sequence" + }, + { + "lineno": 152, + "name": "fee" + }, + { + "lineno": 160, + "name": "tx" + }, + { + "lineno": 172, + "name": "value" + }, + { + "lineno": 181, + "name": "value" + }, + { + "lineno": 190, + "name": "value" + }, + { + "lineno": 199, + "name": "value" + }, + { + "lineno": 208, + "name": "value" + }, + { + "lineno": 217, + "name": "value" + }, + { + "lineno": 226, + "name": "publicKey" + }, + { + "lineno": 226, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 32, + "name": "OracleSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& oracleDocumentID, std::decay_t const& lastUpdateTime, STArray const& priceDataSeries, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 137, + "name": "OracleSetBuilder" + } + ], + "description": "Defines the OracleSet transaction type for the XRPL protocol, including a type-safe wrapper class and a builder class for constructing OracleSet transactions with required and optional fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/OracleSet.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "OracleSet::getOracleDocumentID" + }, + { + "args": [], + "lineno": 54, + "name": "OracleSet::getProvider" + }, + { + "args": [], + "lineno": 65, + "name": "OracleSet::hasProvider" + }, + { + "args": [], + "lineno": 75, + "name": "OracleSet::getURI" + }, + { + "args": [], + "lineno": 86, + "name": "OracleSet::hasURI" + }, + { + "args": [], + "lineno": 96, + "name": "OracleSet::getAssetClass" + }, + { + "args": [], + "lineno": 107, + "name": "OracleSet::hasAssetClass" + }, + { + "args": [], + "lineno": 117, + "name": "OracleSet::getLastUpdateTime" + }, + { + "args": [], + "lineno": 124, + "name": "OracleSet::getPriceDataSeries" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& oracleDocumentID", + "std::decay_t const& lastUpdateTime", + "STArray const& priceDataSeries", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 146, + "name": "OracleSetBuilder::OracleSetBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 159, + "name": "OracleSetBuilder::OracleSetBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 170, + "name": "OracleSetBuilder::setOracleDocumentID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 179, + "name": "OracleSetBuilder::setProvider" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 188, + "name": "OracleSetBuilder::setURI" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 197, + "name": "OracleSetBuilder::setAssetClass" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 206, + "name": "OracleSetBuilder::setLastUpdateTime" + }, + { + "args": [ + "STArray const& value" + ], + "lineno": 215, + "name": "OracleSetBuilder::setPriceDataSeries" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 224, + "name": "OracleSetBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/OracleSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/OracleSet.h.ai.md new file mode 100644 index 0000000000..7ca476fb20 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/OracleSet.h.ai.md @@ -0,0 +1,72 @@ +# `OracleSet.h` — Auto-Generated OracleSet Transaction Wrapper + +## Role and Context + +This file is part of the `protocol_autogen` layer, a code-generated subsystem that provides a typed C++ API over XRPL's loosely-typed `STTx` transaction model. Every transaction type on the ledger gets its own header here; `OracleSet.h` handles transaction type `ttORACLE_SET` (type code 51), the operation that creates or updates a Price Oracle object on the XRPL. The oracle feature — gated behind the `featurePriceOracle` amendment — allows account operators to publish a series of asset price data points to the ledger, forming the basis for on-chain price feeds. + +The file lives alongside 70+ sibling files in `include/xrpl/protocol_autogen/transactions/`, each following the same two-class pattern. The comment on line 1, `// This file is auto-generated. Do not edit.`, signals that this code is produced by a generator script from a transaction schema definition, not handwritten. Modifications belong in the generator or the schema, not here. + +## The Wrapper/Builder Pattern + +The design splits read and write concerns into two classes. `OracleSet` is an immutable view and `OracleSetBuilder` is the mutable construction surface. This separation is intentional: once a transaction is signed and submitted to the network, nothing should be able to mutate it through the same handle. The wrapper holds a `std::shared_ptr` (inherited as `tx_` from `TransactionBase`), making the const-ness structural rather than advisory. + +### `OracleSet` (Immutable Wrapper) + +`OracleSet` inherits `TransactionBase`, which provides type-safe getters for the universal fields shared by every transaction: `sfAccount`, `sfSequence`, `sfFee`, `sfSigningPubKey`, optional `sfFlags`, `sfMemos`, `sfSigners`, `sfLastLedgerSequence`, and others including the `sfDelegate` field added for the delegable transaction feature. `OracleSet` then adds its own oracle-specific getters on top. + +The constructor enforces type safety through a runtime check: + +```cpp +explicit OracleSet(std::shared_ptr tx) + : TransactionBase(std::move(tx)) +{ + if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for OracleSet"); +} +``` + +The check happens *after* `std::move(tx)` into the base, which is why it accesses `tx_` rather than the parameter. The `static constexpr txType` member lets callers compare types without an instance. + +**Field optionality is reflected in the API.** Required fields return a value directly: + +- `getOracleDocumentID()` → `uint32_t`: a per-account identifier that uniquely addresses a particular oracle object, allowing one account to maintain multiple independent oracles. +- `getLastUpdateTime()` → `uint32_t`: the ripple epoch timestamp of the data batch being submitted. +- `getPriceDataSeries()` → `STArray const&`: the array of price entries. Each element in the `STArray` represents a `PriceData` inner object containing asset pair and price fields. This is returned as a raw `STArray` rather than a typed container because the inner structure is an untyped heterogeneous array in the `STObject` model; the comment `@note This is an untyped field` calls this out explicitly. + +Optional fields — `Provider`, `URI`, and `AssetClass` — return `protocol_autogen::Optional`, a type alias defined in `Utils.h`. For non-reference value types this collapses to `std::optional`, but for reference types (like `STArray`) it becomes `std::optional>` to avoid copying. Each optional getter is paired with a `has*()` predicate, so callers can check presence without constructing a temporary: + +```cpp +if (tx.hasProvider()) + doSomethingWith(tx.getProvider().value()); +``` + +`Provider`, `URI`, and `AssetClass` all carry the `SF_VL` (variable-length blob) type, used to store arbitrary byte strings — the provider's name, a URL pointing to off-chain data, and a category label like `"currency"` or `"commodity"`. + +### `OracleSetBuilder` (Fluent Builder) + +`OracleSetBuilder` inherits `TransactionBuilderBase` using CRTP. The template parameter lets every setter in the base class return `Derived&` — that is, `OracleSetBuilder&` — so method chains stay coherent without any virtual dispatch. The base class stores a mutable `STObject object_{sfTransaction}` that accumulates fields before the final `STTx` is constructed. + +The primary constructor enforces required fields at construction time by accepting them as mandatory parameters: + +```cpp +OracleSetBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& oracleDocumentID, + std::decay_t const& lastUpdateTime, + STArray const& priceDataSeries, + std::optional sequence = std::nullopt, + std::optional fee = std::nullopt) +``` + +The use of `std::decay_t` decays the field type alias (removing references and cv-qualifiers) before taking a `const&`. This defensive pattern prevents the parameter from accidentally binding to a temporary field accessor return value and dangling. + +Sequence and fee are optional at construction time because in some workflows the network assigns these automatically (auto-fill mode). The builder's secondary constructor, taking an existing `std::shared_ptr`, enables round-trip modification: unwrap an existing oracle transaction, mutate optional fields, re-sign. Like the wrapper's constructor, it checks `getTxnType()` and throws on mismatch. + +`setPriceDataSeries()` uses `setFieldArray()` rather than the subscript operator because `STArray` fields require their own setter path in `STObject`. + +The `build()` method terminates the chain: it calls `sign()` (inherited from the base, which serializes without signing fields, prepends the `HashPrefix::txSign` prefix, and appends the signature), then wraps the finalized `STObject` in a `std::shared_ptr` and returns an `OracleSet` wrapper. After `build()`, the builder's internal `object_` has been moved out via `std::move`, so the builder should not be reused. + +## Relationship to the Broader System + +The file pairs with `OracleDelete.h` (type 52), which shares the same amendment gate and carries only `OracleDocumentID` as its required field — deletion only needs to identify the target oracle. Together they form the complete CRUD surface for Price Oracle objects. + +`TransactionBase::validate()` connects this layer back to the schema-enforcement machinery by calling `validateSTObject()` against the `SOTemplate` registered in `TxFormats`, ensuring that the auto-generated field set stays consistent with the canonical format definitions. This means any oracle transaction that passes through `OracleSet` has already been type-checked at the construction call site and can be re-validated at any time against the ledger's format rules. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/Payment.h.ai.json b/include/xrpl/protocol_autogen/transactions/Payment.h.ai.json new file mode 100644 index 0000000000..8160399a0b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/Payment.h.ai.json @@ -0,0 +1,255 @@ +{ + "args": [ + { + "lineno": 37, + "name": "tx" + }, + { + "lineno": 219, + "name": "account" + }, + { + "lineno": 220, + "name": "destination" + }, + { + "lineno": 221, + "name": "amount" + }, + { + "lineno": 222, + "name": "sequence" + }, + { + "lineno": 223, + "name": "fee" + }, + { + "lineno": 229, + "name": "tx" + }, + { + "lineno": 233, + "name": "value" + }, + { + "lineno": 243, + "name": "value" + }, + { + "lineno": 253, + "name": "value" + }, + { + "lineno": 263, + "name": "value" + }, + { + "lineno": 273, + "name": "value" + }, + { + "lineno": 283, + "name": "value" + }, + { + "lineno": 293, + "name": "value" + }, + { + "lineno": 303, + "name": "value" + }, + { + "lineno": 313, + "name": "value" + }, + { + "lineno": 323, + "name": "publicKey" + }, + { + "lineno": 323, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 33, + "name": "Payment" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& destination, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 210, + "name": "PaymentBuilder" + } + ], + "description": "Defines the Payment transaction type for XRPL, providing a type-safe wrapper and a builder class for constructing and accessing Payment transactions with specific fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/Payment.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getDestination" + }, + { + "args": [], + "lineno": 54, + "name": "getAmount" + }, + { + "args": [], + "lineno": 65, + "name": "getSendMax" + }, + { + "args": [], + "lineno": 77, + "name": "hasSendMax" + }, + { + "args": [], + "lineno": 86, + "name": "getPaths" + }, + { + "args": [], + "lineno": 97, + "name": "hasPaths" + }, + { + "args": [], + "lineno": 106, + "name": "getInvoiceID" + }, + { + "args": [], + "lineno": 118, + "name": "hasInvoiceID" + }, + { + "args": [], + "lineno": 127, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 139, + "name": "hasDestinationTag" + }, + { + "args": [], + "lineno": 148, + "name": "getDeliverMin" + }, + { + "args": [], + "lineno": 160, + "name": "hasDeliverMin" + }, + { + "args": [], + "lineno": 169, + "name": "getCredentialIDs" + }, + { + "args": [], + "lineno": 181, + "name": "hasCredentialIDs" + }, + { + "args": [], + "lineno": 190, + "name": "getDomainID" + }, + { + "args": [], + "lineno": 202, + "name": "hasDomainID" + }, + { + "args": [ + "value" + ], + "lineno": 232, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 242, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 252, + "name": "setSendMax" + }, + { + "args": [ + "value" + ], + "lineno": 262, + "name": "setPaths" + }, + { + "args": [ + "value" + ], + "lineno": 272, + "name": "setInvoiceID" + }, + { + "args": [ + "value" + ], + "lineno": 282, + "name": "setDestinationTag" + }, + { + "args": [ + "value" + ], + "lineno": 292, + "name": "setDeliverMin" + }, + { + "args": [ + "value" + ], + "lineno": 302, + "name": "setCredentialIDs" + }, + { + "args": [ + "value" + ], + "lineno": 312, + "name": "setDomainID" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 322, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/Payment.h.ai.md b/include/xrpl/protocol_autogen/transactions/Payment.h.ai.md new file mode 100644 index 0000000000..01729bf9a4 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/Payment.h.ai.md @@ -0,0 +1,49 @@ +# `Payment.h` — Auto-Generated Payment Transaction Wrapper + +## Role and Context + +`Payment.h` is part of the `protocol_autogen` layer, a code-generated subsystem under `include/xrpl/protocol_autogen/transactions/`. Over seventy transaction types follow the same structural pattern in this directory; `Payment` represents `ttPAYMENT` (ordinal 0), the most fundamental XRPL operation. The file header carries a strict `// This file is auto-generated. Do not edit.` guard — the source of truth is the generator, not this file. + +The file lives in `namespace xrpl::transactions` and defines two cooperating classes: the immutable `Payment` read-wrapper and the mutable `PaymentBuilder`, each inheriting from separate base templates in `TransactionBase.h` and `TransactionBuilderBase.h`. + +## Two-Class Design: Immutable Reader + Fluent Builder + +This pattern separates concerns cleanly. Once a `Payment` object exists, it cannot be mutated — it wraps a `std::shared_ptr`, inheriting this guarantee from `TransactionBase`. Callers read fields through typed getters that delegate to `STTx::at()` or `STTx::isFieldPresent()`. There is no way to accidentally modify a transaction that has already been signed and submitted. + +Construction goes through `PaymentBuilder`, which extends `TransactionBuilderBase` using the Curiously Recurring Template Pattern (CRTP). The base class defines setters for all fields common to every XRPL transaction (`setAccount`, `setFee`, `setSequence`, `setMemos`, `setLastLedgerSequence`, etc.) and makes them return `Derived&` instead of the base type. This means every setter in both the base and the derived builder participates in the same fluent chain without virtual dispatch and without slicing. + +The `PaymentBuilder` constructor requires the two fields that the XRPL protocol marks `soeREQUIRED` for Payment: `account` and `destination` as `SF_ACCOUNT::type::value_type`, and `amount` as `SF_AMOUNT::type::value_type`. Optional `sequence` and `fee` parameters allow immediate configuration at construction time. The base class deliberately avoids calling `object_.set(soTemplate)` on the internal `STObject` — a subtle but important detail explained in `TransactionBuilderBase.h`: calling it would create placeholder entries for `soeDEFAULT` fields (like `sfPaths`), causing `applyTemplate()` inside the `STTx` constructor to throw "may not be explicitly set to default". Deferring template application to `STTx` construction sidesteps the issue entirely. + +## Payment-Specific Fields + +The field set exposes the complete XRPL Payment schema: + +**Required fields** accessed directly (no `Optional` wrapper): + +- `getDestination()` — returns the recipient `AccountID` via `sfDestination`. +- `getAmount()` — returns `SF_AMOUNT::type::value_type`, which covers XRP drops, issued currency amounts, and MPT (Multi-Purpose Token) amounts. MPT support is called out explicitly in the comment because `sfAmount` in Payment is one of the few fields that allows the newer MPT amount type alongside legacy XRP/IOU amounts. + +**Optional fields** use a paired `hasX()` / `getX()` accessor idiom, where `getX()` returns `protocol_autogen::Optional` (`std::nullopt` when absent): + +- `getSendMax()` / `hasSendMax()` — an MPT-aware maximum spend amount used for cross-currency payments. When present alongside `sfPaths`, it instructs the pathfinding engine to find a route that delivers `sfAmount` while spending no more than `sfSendMax`. +- `getDeliverMin()` / `hasDeliverMin()` — the minimum the destination must receive for a partial-payment transaction to succeed. Also MPT-capable. +- `getDestinationTag()` / `hasDestinationTag()` — a 32-bit integer routing hint allowing the destination to multiplex incoming payments (e.g., per-user identifiers on an exchange). +- `getInvoiceID()` / `hasInvoiceID()` — a free-form `uint256` that can reference an invoice or order, enabling off-ledger reconciliation. +- `getCredentialIDs()` / `hasCredentialIDs()` — a `SF_VECTOR256` list of Credential object IDs. When the destination account requires Deposit Authorization, the sender can present credentials authorizing the payment without a pre-existing `DepositPreauth` ledger entry. +- `getDomainID()` / `hasDomainID()` — a `uint256` reference to a Permissioned Domain. Payments destined for accounts under a permissioned domain must supply the matching domain ID. + +**The `sfPaths` special case**: `getPaths()` diverges from the `protocol_autogen::Optional` pattern and returns `std::optional>`. This is because `sfPaths` is a structurally complex nested type (`STPathSet`) that doesn't map to the scalar `SF_*` field descriptor system used by the rest of the getters. The builder's `setPaths()` correspondingly calls `object_.setFieldPathSet(sfPaths, value)` rather than the generic `object_[sfPaths] = value` assignment. The field is annotated `soeDEFAULT`, meaning it is absent in the serialized form when empty rather than serialized as a zero-length container. + +## Type Safety and Error Handling + +Both `Payment(std::shared_ptr)` and `PaymentBuilder(std::shared_ptr)` validate the transaction type at construction, throwing `std::runtime_error` if `getTxnType() != ttPAYMENT`. This prevents a caller from accidentally wrapping, say, an `OfferCreate` in a `Payment` accessor. The `static constexpr txType = ttPAYMENT` constant ties the class permanently to that transaction code. + +Validation of full schema conformance lives in `TransactionBase::validate()`, which delegates to `protocol_autogen::validateSTObject` against the `TxFormats` SO template and then calls `passesLocalChecks`. This is not called at construction — it is offered as an explicit opt-in, consistent with how the broader `rippled` codebase treats local pre-submission checks. + +## Build and Sign Flow + +`PaymentBuilder::build(publicKey, secretKey)` finalizes the transaction in two steps. First it calls `sign()` from `TransactionBuilderBase`, which serializes the in-progress `STObject` with `HashPrefix::txSign` prepended (omitting signing fields), signs the resulting byte slice, and writes `sfSigningPubKey` and `sfTxnSignature` back into `object_`. Then it constructs a new `STTx` by moving `object_` into the `STTx` constructor — at which point `applyTemplate()` validates that all required fields are present — and wraps it in a `Payment`. The returned `Payment` is fully immutable and ready to serialize for network submission. + +## Delegation and Amendment Context + +The class comment records `Delegation::delegable`, meaning a `Payment` may include `sfDelegate` (inherited from `TransactionBase`) to exercise a delegated authority grant. `Amendment: uint256{}` (the zero hash) signals that this transaction type requires no amendment gate — it is a core protocol feature available on all ledger versions. The `createAcct | mayCreateMPT` privilege flags are metadata consumed by the code generator and the protocol's privilege-checking machinery, documenting that a Payment may implicitly create the destination account if funded above the reserve, and may create MPT-related state during payment processing. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h.ai.json b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h.ai.json new file mode 100644 index 0000000000..b50e65605b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h.ai.json @@ -0,0 +1,213 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 163, + "name": "account" + }, + { + "lineno": 164, + "name": "channel" + }, + { + "lineno": 165, + "name": "sequence" + }, + { + "lineno": 166, + "name": "fee" + }, + { + "lineno": 184, + "name": "value" + }, + { + "lineno": 193, + "name": "value" + }, + { + "lineno": 202, + "name": "value" + }, + { + "lineno": 211, + "name": "value" + }, + { + "lineno": 220, + "name": "value" + }, + { + "lineno": 229, + "name": "value" + }, + { + "lineno": 238, + "name": "publicKey" + }, + { + "lineno": 238, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "PaymentChannelClaim" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& channel, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 153, + "name": "PaymentChannelClaimBuilder" + } + ], + "description": "Defines the PaymentChannelClaim transaction type for the XRPL protocol, providing an immutable wrapper for type-safe field access and a builder class for constructing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "PaymentChannelClaim" + }, + { + "args": [], + "lineno": 43, + "name": "getChannel" + }, + { + "args": [], + "lineno": 54, + "name": "getAmount" + }, + { + "args": [], + "lineno": 65, + "name": "hasAmount" + }, + { + "args": [], + "lineno": 74, + "name": "getBalance" + }, + { + "args": [], + "lineno": 85, + "name": "hasBalance" + }, + { + "args": [], + "lineno": 94, + "name": "getSignature" + }, + { + "args": [], + "lineno": 105, + "name": "hasSignature" + }, + { + "args": [], + "lineno": 114, + "name": "getPublicKey" + }, + { + "args": [], + "lineno": 125, + "name": "hasPublicKey" + }, + { + "args": [], + "lineno": 134, + "name": "getCredentialIDs" + }, + { + "args": [], + "lineno": 145, + "name": "hasCredentialIDs" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& channel", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 163, + "name": "PaymentChannelClaimBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 172, + "name": "PaymentChannelClaimBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 183, + "name": "setChannel" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 192, + "name": "setAmount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 201, + "name": "setBalance" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 210, + "name": "setSignature" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 219, + "name": "setPublicKey" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 228, + "name": "setCredentialIDs" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 237, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h.ai.md b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h.ai.md new file mode 100644 index 0000000000..24947a5619 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelClaim.h.ai.md @@ -0,0 +1,38 @@ +# `PaymentChannelClaim.h` — Auto-Generated Transaction Wrapper + +## Purpose and Context + +This file is part of the `xrpl/protocol_autogen` layer — a code-generated facade over XRPL's raw serialized transaction objects. It defines two classes for the `PaymentChannelClaim` transaction type (`ttPAYCHAN_CLAIM`, type code 15): an immutable reader (`PaymentChannelClaim`) and a fluent builder (`PaymentChannelClaimBuilder`). + +In the XRPL payment channel lifecycle, a channel is opened with `PaymentChannelCreate` (type 13), optionally topped up with `PaymentChannelFund` (type 14), and then settled or drained by the destination party using `PaymentChannelClaim`. The claim transaction is the mechanism through which off-ledger micropayments are redeemed on-chain: the channel sender signs claim authorizations offline, and the receiver later submits one to the ledger, collecting XRP in a single on-chain transaction regardless of how many off-chain payments occurred. + +## The Immutable Wrapper: `PaymentChannelClaim` + +`PaymentChannelClaim` extends `TransactionBase`, which holds the underlying `std::shared_ptr tx_` and exposes all common transaction fields (account, sequence, fee, flags, memos, signers, etc.). The derived class adds the five payment-channel-specific fields: + +- **`sfChannel`** (`getChannel()`) — a required 256-bit identifier (`SF_UINT256`) for the payment channel ledger object being claimed against. This is the only required field beyond the universal transaction fields. +- **`sfAmount`** (`getAmount()`) — an optional `SF_AMOUNT` specifying how much XRP to deliver to the destination. When present, funds are transferred from the channel to the destination account. +- **`sfBalance`** (`getBalance()`) — an optional `SF_AMOUNT` representing the cumulative total XRP claimed to date, as asserted by the sender's signed authorization. The ledger verifies this against the sender's off-ledger signature. +- **`sfSignature`** (`getSignature()`) — an optional variable-length blob (`SF_VL`) carrying the channel sender's cryptographic signature authorizing the claimed balance. Without this, only the channel owner can close the channel. +- **`sfPublicKey`** (`getPublicKey()`) — an optional `SF_VL` blob containing the public key used to verify `sfSignature`. Together, `sfSignature` and `sfPublicKey` encode the full off-ledger authorization. +- **`sfCredentialIDs`** (`getCredentialIDs()`) — an optional `SF_VECTOR256` array of credential identifiers, used when the channel's destination requires deposit pre-authorization via verified credentials. + +Every optional field follows the same two-method pattern: `getX()` returns `protocol_autogen::Optional` (which resolves to `std::optional` for value types and `std::optional>` for reference types via the `Utils.h` type alias), while `hasX()` returns a plain `bool`. This pattern avoids the exception that `STTx::at()` would throw for a missing field, and it makes client code self-documenting about optionality. + +The constructor performs an eager type guard: it calls `tx_->getTxnType()` immediately and throws `std::runtime_error` if the wrapped `STTx` does not carry `ttPAYCHAN_CLAIM`. This prevents silent misuse where a caller might construct a `PaymentChannelClaim` around a `Payment` transaction and read garbage field values. + +## The Builder: `PaymentChannelClaimBuilder` + +`PaymentChannelClaimBuilder` inherits from `TransactionBuilderBase` using the Curiously Recurring Template Pattern (CRTP). The base class is parameterized on the concrete derived type so that every setter defined in the base (`setAccount()`, `setFee()`, `setSequence()`, `setFlags()`, etc.) can return `Derived&` rather than `TransactionBuilderBase&`, preserving fluent method chaining across both base and derived setters without virtual dispatch. + +The builder holds a mutable `STObject object_{sfTransaction}` (declared in `TransactionBuilderBase`). A deliberate design decision documented in the base's constructor comment is that the builder never calls `object_.set(soTemplate)`. If it did, `STTx`'s `applyTemplate()` would encounter explicit placeholders for `soeDEFAULT` fields and throw. Instead, the builder keeps a "free" object and lets the `STTx` constructor run `applyTemplate()` cleanly, which properly inserts or validates required fields based on the transaction format registry. + +`PaymentChannelClaimBuilder` offers two construction paths. The primary constructor takes `account`, `channel`, and optional `sequence` and `fee`, enforcing the minimum required data up front. The secondary constructor accepts an existing `std::shared_ptr` and copies it into `object_` — useful for modifying or re-signing a previously deserialized transaction. Both paths perform the same type check against `ttPAYCHAN_CLAIM`. + +The `build()` method calls the protected `sign()` helper (which serializes the object with `HashPrefix::txSign`, computes the signature via `xrpl::sign()`, and sets `sfSigningPubKey` and `sfTxnSignature`), then moves `object_` into a new `STTx` and wraps it in a `shared_ptr`. That pointer is passed to the `PaymentChannelClaim` wrapper constructor, transferring ownership and transitioning from mutable builder to immutable reader. The `STTx` is const from this point forward; the `PaymentChannelClaim` reader cannot mutate it. + +## Code Generation and Consistency + +The `// This file is auto-generated. Do not edit.` header and the uniform structure shared across all ~70 sibling transaction headers (e.g., `PaymentChannelCreate.h`, `EscrowFinish.h`, `OfferCreate.h`) confirm this file is produced by a schema-driven generator. Every transaction type in the `xrpl::transactions` namespace follows the same `Foo` / `FooBuilder` dual-class pattern with identical field access conventions, making the generated API predictable and easy to use programmatically. The generator encodes field cardinality (`soeREQUIRED` vs `soeOPTIONAL`) directly into the C++ types — required fields return values directly; optional fields return `Optional`. + +The transaction is marked `Delegation::delegable`, meaning it can be submitted by a delegate account on behalf of the originating account using the `sfDelegate` field inherited from `TransactionBase`. The `sfCredentialIDs` optional field further reflects integration with XRPL's permissioned deposit pre-authorization system, allowing channels to be restricted to holders of specific verifiable credentials. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h.ai.json b/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h.ai.json new file mode 100644 index 0000000000..d1993d8bbd --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h.ai.json @@ -0,0 +1,193 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 130, + "name": "account" + }, + { + "lineno": 130, + "name": "destination" + }, + { + "lineno": 130, + "name": "amount" + }, + { + "lineno": 130, + "name": "settleDelay" + }, + { + "lineno": 130, + "name": "publicKey" + }, + { + "lineno": 130, + "name": "sequence" + }, + { + "lineno": 130, + "name": "fee" + }, + { + "lineno": 154, + "name": "value" + }, + { + "lineno": 208, + "name": "publicKey" + }, + { + "lineno": 208, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "PaymentChannelCreate" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& destination, std::decay_t const& amount, std::decay_t const& settleDelay, std::decay_t const& publicKey, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 127, + "name": "PaymentChannelCreateBuilder" + } + ], + "description": "Provides an immutable wrapper and builder for the PaymentChannelCreate transaction in the XRPL protocol, enabling type-safe field access and fluent transaction construction.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "PaymentChannelCreate" + }, + { + "args": [], + "lineno": 43, + "name": "getDestination" + }, + { + "args": [], + "lineno": 52, + "name": "getAmount" + }, + { + "args": [], + "lineno": 61, + "name": "getSettleDelay" + }, + { + "args": [], + "lineno": 70, + "name": "getPublicKey" + }, + { + "args": [], + "lineno": 79, + "name": "getCancelAfter" + }, + { + "args": [], + "lineno": 90, + "name": "hasCancelAfter" + }, + { + "args": [], + "lineno": 99, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 110, + "name": "hasDestinationTag" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& destination", + "std::decay_t const& amount", + "std::decay_t const& settleDelay", + "std::decay_t const& publicKey", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 130, + "name": "PaymentChannelCreateBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 143, + "name": "PaymentChannelCreateBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 154, + "name": "setDestination" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 163, + "name": "setAmount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 172, + "name": "setSettleDelay" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 181, + "name": "setPublicKey" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 190, + "name": "setCancelAfter" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 199, + "name": "setDestinationTag" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 208, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h.ai.md b/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h.ai.md new file mode 100644 index 0000000000..79dc5d8ab4 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelCreate.h.ai.md @@ -0,0 +1,55 @@ +# `PaymentChannelCreate.h` — Auto-generated Transaction Wrapper and Builder + +## Role and Context + +This file is part of the `protocol_autogen` layer, a code-generated abstraction over XRPL's raw `STTx` serialized-transaction type. It defines two classes — `PaymentChannelCreate` and `PaymentChannelCreateBuilder` — representing the `ttPAYCHAN_CREATE` (type 13) transaction, which opens a unidirectional XRP payment channel on the ledger. + +Payment channels exist to enable high-throughput, low-latency off-chain micropayments: the sender locks XRP into a channel object on the ledger, then issues cryptographically signed off-chain "claims" to a receiver. Only a final settlement requires an on-ledger transaction. `PaymentChannelCreate` is the first step in this lifecycle, followed by `PaymentChannelFund` (adding more XRP) and `PaymentChannelClaim` (redeeming a claim or closing the channel). + +The file carries the `// This file is auto-generated. Do not edit.` guard and follows the identical structural pattern shared by every other transaction type in the `transactions/` directory. The generation ensures a uniform, consistent API surface across all ~70 transaction types. + +## The Wrapper/Builder Split + +`PaymentChannelCreate` is an immutable read accessor. It wraps a `std::shared_ptr` — the `const` qualifier on the template argument makes mutation impossible at the type level — and inherits common field getters (`getAccount()`, `getFee()`, `getSequence()`, etc.) from `TransactionBase`. Its only job is safe, named access to the fields of an existing, already-constructed transaction. + +`PaymentChannelCreateBuilder` is the mutable counterpart, holding a live `STObject object_` and inheriting fluent setters for all common fields from `TransactionBuilderBase`. The CRTP pattern in the base class (`template `) allows each `setXxx()` in the base to return `Derived&`, preserving the concrete type through the chain so callers never need a cast. + +The lifecycle ends at `build(PublicKey, SecretKey)`, which calls the protected `sign()` helper from `TransactionBuilderBase`, serializes the `STObject` into a new `STTx`, and wraps it in the immutable `PaymentChannelCreate` type. After `build()` the builder's internal `object_` has been moved into the `STTx`, making the builder effectively spent. + +## PaymentChannelCreate-Specific Fields + +Four fields are `soeREQUIRED`, exposed as direct-value getters: + +- **`sfDestination`** (`SF_ACCOUNT`): The account that can receive claims from this channel. Returned as `AccountID`. +- **`sfAmount`** (`SF_AMOUNT`): The amount of XRP (in drops) to lock into the channel at creation time. +- **`sfSettleDelay`** (`SF_UINT32`): The dispute window in seconds. When the destination requests channel closure, this is the grace period during which the source may still submit any outstanding claims. Choosing too short a value exposes the sender to race conditions; too long ties up XRP unnecessarily. +- **`sfPublicKey`** (`SF_VL`): The public key used to verify off-chain claim signatures. Critically, this is *not* the same key that signs the `PaymentChannelCreate` transaction itself — it is an application-level key dedicated to claim authentication and returned as a `Blob` (`SF_VL::type::value_type`). + +Two fields are `soeOPTIONAL`, each paired with a `hasXxx()` predicate and a getter returning `protocol_autogen::Optional`: + +- **`sfCancelAfter`** (`SF_UINT32`): A Ripple-epoch timestamp after which the channel expires automatically, regardless of whether the destination has claimed anything. Useful for time-bounded payment guarantees. +- **`sfDestinationTag`** (`SF_UINT32`): A 32-bit routing tag on the destination side, analogous to `sfSourceTag` on the sender side. + +## Constructor Type Enforcement + +Both classes guard against misuse in their constructors. `PaymentChannelCreate(std::shared_ptr)` calls `tx_->getTxnType() != txType` and throws `std::runtime_error` immediately if the wrapped transaction is not actually a `ttPAYCHAN_CREATE`. The same check appears in `PaymentChannelCreateBuilder(std::shared_ptr)`, which handles the case of reconstructing a mutable builder from an existing transaction (useful for mutation or re-signing). This fail-fast pattern means type mismatches are surfaced at the point of wrapping, not silently deferred to field-access time. + +## The `Optional` Alias + +Optional field getters return `protocol_autogen::Optional`, defined in `Utils.h` as: + +```cpp +template +using Optional = std::conditional_t< + std::is_reference_v, + std::optional>>, + std::optional>; +``` + +This handles a subtlety in the XRPL field type system: some `SF_*` value types are reference types. If `ValueType` is a reference, wrapping it directly in `std::optional` would be ill-formed, so the alias redirects to `reference_wrapper`. For the fields present in this file (`SF_UINT32`), the value types are plain integers, so `Optional` reduces straightforwardly to `std::optional`. + +## Builder Constructor and `std::decay_t` + +The primary builder constructor takes all four required fields alongside optional `sequence` and `fee` parameters. The field-value parameters are typed as `std::decay_t const&` — stripping any reference or cv-qualifiers from the field's native value type before taking a const-reference. This ensures the setter signatures are stable and copyable regardless of how the underlying serialized-type system exposes its value type, and avoids inadvertently forming references-to-references. + +The base class `TransactionBuilderBase` deliberately avoids calling `object_.set(soTemplate)` on the internal `STObject`. This is a non-obvious but important design decision: setting the SOTemplate would create `STBase` placeholder entries for `soeDEFAULT` fields, which then causes `applyTemplate()` — called later by the `STTx` constructor — to throw a "may not be explicitly set to default" error. By keeping the object "free" (no template applied), the builder allows the `STTx` constructor to handle template enforcement cleanly during finalization. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h.ai.json b/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h.ai.json new file mode 100644 index 0000000000..95d5bcb69d --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h.ai.json @@ -0,0 +1,129 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 91, + "name": "account" + }, + { + "lineno": 92, + "name": "channel" + }, + { + "lineno": 92, + "name": "amount" + }, + { + "lineno": 93, + "name": "sequence" + }, + { + "lineno": 94, + "name": "fee" + }, + { + "lineno": 100, + "name": "tx" + }, + { + "lineno": 105, + "name": "value" + }, + { + "lineno": 114, + "name": "value" + }, + { + "lineno": 123, + "name": "value" + }, + { + "lineno": 132, + "name": "publicKey" + }, + { + "lineno": 132, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "PaymentChannelFund" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& channel, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 87, + "name": "PaymentChannelFundBuilder" + } + ], + "description": "Defines the PaymentChannelFund transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing PaymentChannelFund transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "PaymentChannelFund::getChannel" + }, + { + "args": [], + "lineno": 47, + "name": "PaymentChannelFund::getAmount" + }, + { + "args": [], + "lineno": 56, + "name": "PaymentChannelFund::getExpiration" + }, + { + "args": [], + "lineno": 68, + "name": "PaymentChannelFund::hasExpiration" + }, + { + "args": [ + "value" + ], + "lineno": 104, + "name": "PaymentChannelFundBuilder::setChannel" + }, + { + "args": [ + "value" + ], + "lineno": 113, + "name": "PaymentChannelFundBuilder::setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 122, + "name": "PaymentChannelFundBuilder::setExpiration" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 131, + "name": "PaymentChannelFundBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h.ai.md b/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h.ai.md new file mode 100644 index 0000000000..c3e58c4bb8 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PaymentChannelFund.h.ai.md @@ -0,0 +1,47 @@ +# `PaymentChannelFund.h` — Auto-generated Payment Channel Funding Transaction + +## Role in the System + +This file is part of the `xrpl/protocol_autogen/transactions/` layer — a code-generated collection of per-transaction-type C++ headers that provide strongly-typed interfaces over the raw `STTx` serialized transaction format. It handles the `ttPAYCHAN_FUND` (type 14) transaction, which replenishes an existing XRPL payment channel with additional XRP and optionally extends its expiration deadline. The file lives alongside `PaymentChannelCreate.h` (type 13) and `PaymentChannelClaim.h`, together covering the full lifecycle of XRPL payment channels. + +The "do not edit" header is significant: this file is generated from a transaction schema definition, ensuring that as the protocol evolves, the C++ bindings remain consistent without hand-maintenance drift. + +## The Two-Class Pattern + +Every transaction type in this layer is expressed as a pair of classes following the same structural contract: + +**`PaymentChannelFund`** is the immutable read-side. It wraps a `std::shared_ptr` and delegates to `TransactionBase`, which itself stores that shared pointer. The `const` on the pointed-to object is the key invariant: once constructed, the underlying serialized transaction data cannot change. The constructor throws `std::runtime_error` immediately if the wrapped `STTx` is not of type `ttPAYCHAN_FUND`, so the type mismatch is caught at construction time rather than silently producing garbage field reads later. + +**`PaymentChannelFundBuilder`** is the mutable write-side. It inherits from `TransactionBuilderBase` using CRTP so that all common field setters (`setFee`, `setSequence`, `setLastLedgerSequence`, etc.) return `PaymentChannelFundBuilder&` rather than the base type — enabling fully typed method chaining without a cast. Internally the builder holds an `STObject` (not an `STTx`) until `build()` is called. + +This separation is deliberate: separating read access from write access means callers who only inspect transactions never have access to mutation methods, and callers constructing transactions work with a dedicated type that cannot be confused with a validated, signed wrapper. + +## Fields and Optionality + +`PaymentChannelFund` exposes three transaction-specific fields beyond the common ones inherited from `TransactionBase`: + +- **`sfChannel`** (`soeREQUIRED`) — a 256-bit identifier (`SF_UINT256::type::value_type`) that names the existing payment channel to fund. Passed by the builder constructor and enforced as required at the protocol level; `getChannel()` calls `tx_->at(sfChannel)` which will throw if somehow absent (guarded by the serialization layer upstream). + +- **`sfAmount`** (`soeREQUIRED`) — the quantity of XRP drops to add to the channel's reserve (`SF_AMOUNT::type::value_type` resolves to `STAmount`). Required both in the builder constructor and in the protocol definition; `getAmount()` uses the same direct `at()` accessor. + +- **`sfExpiration`** (`soeOPTIONAL`) — a `uint32_t` ripple epoch timestamp. When present, this sets a new upper bound on when the channel may be closed by the source account alone (the destination or transaction expiration can still close it earlier). The absence of this field is meaningful — it means no change to the current expiration — so the accessor returns `protocol_autogen::Optional` rather than a raw value. + +The `protocol_autogen::Optional` alias in `Utils.h` is a small but important detail. It uses `std::conditional_t` to produce either `std::optional>>` (when `T` is a reference type) or `std::optional` (when it is not). This handles the case where `SF_*::type::value_type` might be a reference to an internal buffer — returning an `optional>` avoids dangling references while still letting callers detect absence. For `sfExpiration`'s `uint32_t` this resolves to a plain `std::optional`, but the alias keeps the pattern uniform across all auto-generated files. + +The `hasExpiration()` / `getExpiration()` pairing is consistent with all optional fields in this layer: the `has*` method calls `isFieldPresent()`, and `get*` checks that same predicate before calling `at()`. This avoids an exception from `STObject::at()` when a field is absent. + +## Builder Construction and Signing + +`PaymentChannelFundBuilder` offers two construction paths: + +1. **Fresh construction** takes `account`, `channel`, and `amount` as required arguments, with `sequence` and `fee` defaulting to `std::nullopt`. The constructor delegates to `TransactionBuilderBase` (which sets `sfTransactionType`, `sfAccount`, and optionally `sfSequence`/`sfFee`) and then calls `setChannel` and `setAmount` immediately. Sequence and fee can be deferred because `TransactionBuilderBase` conditionally sets them only when the optionals carry values — this accommodates workflows where fee calculation or sequence lookup happens after initial construction. + +2. **Round-trip construction** from an existing `std::shared_ptr` copies the `STTx`'s field data into the builder's `STObject object_` via `object_ = *tx`. This enables a modify-and-resign workflow: deserialize a transaction, wrap it in the builder, call additional setters to mutate fields, then call `build()` to produce a freshly signed `PaymentChannelFund`. The type guard `tx->getTxnType() != ttPAYCHAN_FUND` applies here too. + +`build(PublicKey, SecretKey)` calls `sign()` (inherited from `TransactionBuilderBase`), which serializes the current `STObject` with `HashPrefix::txSign` and computes the `sfTxnSignature`. It then constructs a `STTx` from the mutated `STObject` via `std::make_shared(std::move(object_))` and wraps it in a `PaymentChannelFund`. The move is intentional — the builder's internal state is consumed and should not be used after `build()`. + +The `std::decay_t` pattern used in setter parameters strips away references and cv-qualifiers from the field's value type. This is necessary because `SF_*::type::value_type` may be defined as a `const T&` in some field descriptors; without `decay_t`, the setter parameter type would collapse to a reference-to-reference, which is ill-formed. Using `decay_t` ensures the setter always takes a `const T&` argument in the conventional sense. + +## Relationship to Sibling Files + +`PaymentChannelCreate.h` establishes a new channel with `sfDestination`, `sfSettleDelay`, and `sfPublicKey`. `PaymentChannelFund.h` references only `sfChannel` (the identifier of that already-created channel) to top it up. The structural symmetry between these files is exact by design — both follow the same generated pattern, both carry `Delegation::delegable` (the transaction can be executed by a delegate account via `sfDelegate`), and neither requires a protocol amendment (`uint256{}`). \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h.ai.json b/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h.ai.json new file mode 100644 index 0000000000..75ea19f31f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h.ai.json @@ -0,0 +1,112 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 65, + "name": "account" + }, + { + "lineno": 66, + "name": "domainID" + }, + { + "lineno": 67, + "name": "sequence" + }, + { + "lineno": 68, + "name": "fee" + }, + { + "lineno": 75, + "name": "tx" + }, + { + "lineno": 87, + "name": "value" + }, + { + "lineno": 96, + "name": "publicKey" + }, + { + "lineno": 96, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "PermissionedDomainDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& domainID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 61, + "name": "PermissionedDomainDeleteBuilder" + } + ], + "description": "Defines the PermissionedDomainDelete transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "PermissionedDomainDelete" + }, + { + "args": [], + "lineno": 44, + "name": "getDomainID" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& domainID", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 65, + "name": "PermissionedDomainDeleteBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 75, + "name": "PermissionedDomainDeleteBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 87, + "name": "setDomainID" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 96, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h.ai.md b/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h.ai.md new file mode 100644 index 0000000000..5220a1f970 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PermissionedDomainDelete.h.ai.md @@ -0,0 +1,31 @@ +# `PermissionedDomainDelete.h` + +## Role in the System + +This auto-generated header defines the C++ interface for the `PermissionedDomainDelete` transaction type (`ttPERMISSIONED_DOMAIN_DELETE`, ordinal 63) on the XRP Ledger. It lives in the `protocol_autogen` layer — a code-generated abstraction that wraps the low-level `STTx`/`STObject` serialization primitives with typed, named accessors. The `PermissionedDomains` amendment introduces a permissioned access-control mechanism on the ledger; this transaction removes a domain object previously created or configured via `PermissionedDomainSet` (type 62). Together the two transactions form the full lifecycle for permissioned domain management. + +Because this file is auto-generated (the header warns *do not edit*), it should be viewed as schema-driven output: the definitive source of truth for field cardinality and types lives in the amendment's specification, not in this file directly. + +## The Two-Class Pattern + +The file declares two closely related classes within `xrpl::transactions`: + +**`PermissionedDomainDelete`** is an immutable, read-only wrapper around a `std::shared_ptr`. It inherits all common-field accessors from `TransactionBase` (account, fee, sequence, flags, memos, signers, network ID, delegate, etc.) and adds exactly one domain-specific getter: `getDomainID()`. This getter returns `sfDomainID` as a `uint256` value directly — not as `std::optional` — because the field is declared `soeREQUIRED` for this transaction type. There is no null path. + +**`PermissionedDomainDeleteBuilder`** is the mutable counterpart, implementing CRTP via `TransactionBuilderBase`. It accumulates field assignments into an `STObject` and provides a fluent `setDomainID()` setter. Calling `build(publicKey, secretKey)` signs the accumulated object, wraps it in an `STTx`, and returns an immutable `PermissionedDomainDelete` wrapper. The split between wrapper and builder enforces that a transaction object, once constructed and signed, cannot be mutated — a critical correctness invariant for transaction handling. + +## Design Decisions Worth Noting + +**Required vs. optional `sfDomainID`**: Comparing with `PermissionedDomainSet`, where `getDomainID()` returns `protocol_autogen::Optional<...>` because a new domain is created when no ID is specified, the delete transaction requires the ID unconditionally. You cannot delete an unknown or unspecified domain. This asymmetry in optionality is intentional and reflects the semantics of each operation: set = create-or-update, delete = must-target-existing. + +**`std::decay_t` in the setter signature**: `setDomainID` takes `std::decay_t const&` rather than the raw typedef. `std::decay_t` strips reference qualifiers from the SField's native C++ type, preventing accidental reference-to-temporary issues if the SField type resolves to a reference type. The getter, by contrast, returns by value using the typedef directly, which is already a value type for `uint256`. + +**Type-check at construction, not at compile time**: Both `PermissionedDomainDelete` and `PermissionedDomainDeleteBuilder` validate the `TxType` at runtime in their constructors, throwing `std::runtime_error` on mismatch. This is necessary because `STTx` is a runtime-typed container — it carries its type as a serialized field — so a compile-time check is not available. The builder's alternate constructor (taking an existing `STTx`) enables round-trip deserialization: parse a raw transaction from the wire or ledger, then wrap it in the typed builder for further manipulation. + +**No `soTemplate` initialization in the builder**: `TransactionBuilderBase`'s constructor deliberately avoids calling `object_.set(soTemplate)`. Doing so would pre-populate the `STObject` with default-value placeholders for `soeDEFAULT` fields. When the builder later calls `std::make_shared(std::move(object_))`, the `STTx` constructor invokes `applyTemplate()`, which throws if it encounters a field explicitly set to its default value. Keeping `object_` as a "free object" sidesteps this and lets `applyTemplate()` handle defaults correctly. + +**Delegation support**: The transaction is marked `Delegation::delegable`, meaning the `sfDelegate` field (inherited from `TransactionBase`) may be set on it. This allows a delegate account to authorize a permissioned domain deletion on behalf of the domain owner. The field is gated behind the `featurePermissionedDomains` amendment along with the transaction type itself, so neither can appear on the ledger until the amendment is voted in. + +## Relationship to Sibling Files + +Within `protocol_autogen/transactions/`, each transaction type follows this exact two-class structure. The base types are not transaction-specific: `TransactionBase` provides the immutable field reader foundation, and `TransactionBuilderBase` provides the mutable CRTP builder foundation with signing. The auto-generated transaction files contribute only the transaction-type constant, the type-guard logic, and the domain-specific fields — keeping the architecture DRY and making the auto-generation schema straightforward. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h.ai.json new file mode 100644 index 0000000000..f9ad17da8f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h.ai.json @@ -0,0 +1,109 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 75, + "name": "account" + }, + { + "lineno": 75, + "name": "acceptedCredentials" + }, + { + "lineno": 75, + "name": "sequence" + }, + { + "lineno": 75, + "name": "fee" + }, + { + "lineno": 86, + "name": "tx" + }, + { + "lineno": 94, + "name": "value" + }, + { + "lineno": 103, + "name": "value" + }, + { + "lineno": 112, + "name": "publicKey" + }, + { + "lineno": 112, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "PermissionedDomainSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, STArray const& acceptedCredentials, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 71, + "name": "PermissionedDomainSetBuilder" + } + ], + "description": "Defines the PermissionedDomainSet transaction type for XRPL, including its immutable wrapper and builder for constructing and signing transactions with type-safe field access.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getDomainID" + }, + { + "args": [], + "lineno": 49, + "name": "hasDomainID" + }, + { + "args": [], + "lineno": 58, + "name": "getAcceptedCredentials" + }, + { + "args": [ + "value" + ], + "lineno": 93, + "name": "setDomainID" + }, + { + "args": [ + "value" + ], + "lineno": 102, + "name": "setAcceptedCredentials" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 111, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h.ai.md new file mode 100644 index 0000000000..b05264320d --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/PermissionedDomainSet.h.ai.md @@ -0,0 +1,39 @@ +# `PermissionedDomainSet.h` + +## Role in the System + +This auto-generated header defines the C++ interface for the `PermissionedDomainSet` transaction type (`ttPERMISSIONED_DOMAIN_SET`, ordinal 62) on the XRP Ledger. It resides in the `protocol_autogen` layer — a schema-driven abstraction that wraps the low-level `STTx`/`STObject` serialization primitives with typed, named accessors, eliminating direct field-key manipulation from application code. The file is governed by the `featurePermissionedDomains` amendment, which introduces a permissioned access-control mechanism on the ledger. Together with `PermissionedDomainDelete` (ordinal 63), this transaction forms the complete lifecycle for permissioned domain management: `PermissionedDomainSet` handles both creation of new domains and updates to existing ones, while the delete transaction removes them entirely. + +Because this file is auto-generated (the header warns *do not edit*), the field cardinality and type assignments here are outputs of a schema specification, not decisions made within the file itself. + +## The Two-Class Pattern + +The file declares two tightly coupled classes within `xrpl::transactions`: + +**`PermissionedDomainSet`** is an immutable, read-only wrapper around a `std::shared_ptr`. It inherits the full suite of common field accessors from `TransactionBase` — account, fee, sequence, flags, memos, signers, network ID, delegate, and more — and adds two domain-specific members. `getDomainID()` returns `protocol_autogen::Optional`, deferring to the `hasDomainID()` presence check before accessing `sfDomainID`; if the field is absent, `std::nullopt` is returned. `getAcceptedCredentials()` returns a `const STArray&` directly (no optional wrapper) because `sfAcceptedCredentials` is declared `soeREQUIRED` for this transaction type. + +**`PermissionedDomainSetBuilder`** is the mutable counterpart, implemented as a CRTP class inheriting `TransactionBuilderBase`. Field assignments accumulate in an `STObject`. The primary constructor requires `account` and `acceptedCredentials` as positional arguments — reflecting their required status — while `sequence` and `fee` are optional. `setDomainID()` exists as a separate optional setter, to be called when targeting an existing domain rather than creating a new one. Calling `build(publicKey, secretKey)` signs the accumulated object, wraps it in an `STTx`, and returns an immutable `PermissionedDomainSet` wrapper. This split enforces that a signed transaction cannot be further mutated. + +## Create-or-Update Semantics and the Optional `sfDomainID` + +The most semantically significant design point in this file — relative to its sibling `PermissionedDomainDelete` — is that `sfDomainID` is `soeOPTIONAL` here but `soeREQUIRED` there. The reason is the "set" verb: a `PermissionedDomainSet` with no `sfDomainID` creates a new permissioned domain object on the ledger, while one that includes `sfDomainID` updates the `sfAcceptedCredentials` list of an existing domain. This dual-mode behavior is encoded directly in the field optionality, making the create vs. update distinction visible at the C++ type level. A caller who receives a `PermissionedDomainSet` object and observes `getDomainID() == std::nullopt` knows unambiguously that it is a creation, not a modification. + +By contrast, `PermissionedDomainDelete`'s `getDomainID()` returns `SF_UINT256::type::value_type` by value with no optional wrapping, because you cannot delete an unspecified target. + +## `sfAcceptedCredentials` as a Required Array + +`sfAcceptedCredentials` is an `STArray` — an untyped heterogeneous array in XRPL's serialization layer — and is required on every `PermissionedDomainSet` transaction. It defines the set of credential types that an account must hold to be permitted within the domain. Both the getter (`getAcceptedCredentials()`) and the builder setter (`setAcceptedCredentials()`) use `STArray const&` directly; there is no `protocol_autogen::Optional` wrapper and no presence check needed. The builder's constructor enforces this: `acceptedCredentials` is a required positional parameter, so no `PermissionedDomainSetBuilder` can be constructed without providing it. + +## Shared Infrastructure and Design Invariants + +**Type-guard at construction**: Both classes validate `TxType` at runtime via `getTxnType() != txType`, throwing `std::runtime_error` on mismatch. This cannot be a compile-time check because `STTx` carries its type as a serialized field. The builder's alternate constructor (accepting an existing `std::shared_ptr`) enables round-trip deserialization: a transaction received from the wire or retrieved from the ledger can be loaded into the builder for further field inspection or re-signing. + +**No `soTemplate` initialization**: `TransactionBuilderBase` deliberately avoids pre-populating the internal `STObject` with `soeDEFAULT` placeholders. If those were present, the `STTx` constructor's `applyTemplate()` call would throw for any field explicitly set to its default value. Keeping `object_` as a free object sidesteps this, delegating proper default-field handling to the `STTx` constructor itself. + +**`std::decay_t` in setter signatures**: `setDomainID` takes `std::decay_t const&` rather than the raw typedef. This strips reference qualifiers from the SField's native type, guarding against dangling-reference issues if the typedef resolves to a reference type. The getter returns by value, which is already safe for a `uint256`. + +**Delegation support**: The transaction is marked `Delegation::delegable`, meaning the inherited `sfDelegate` field may be populated. This allows a delegate account to create or modify a permissioned domain on behalf of the domain owner, subject to the `featurePermissionedDomains` amendment being active. + +## Relationship to Sibling Files + +Every file in `protocol_autogen/transactions/` follows this identical two-class pattern. The base types — `TransactionBase` and `TransactionBuilderBase` — provide all common functionality (field accessors, signing, validation). Each auto-generated file contributes only the transaction-type constant, the type-guard, and the handful of domain-specific getters and setters. The result is a highly regular, easily diffable family of headers where the structural overhead is zero and the only variation between files is the field schema of each transaction type. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/SetFee.h.ai.json b/include/xrpl/protocol_autogen/transactions/SetFee.h.ai.json new file mode 100644 index 0000000000..4b48f9800c --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SetFee.h.ai.json @@ -0,0 +1,259 @@ +{ + "args": [ + { + "lineno": 28, + "name": "tx" + }, + { + "lineno": 224, + "name": "account" + }, + { + "lineno": 224, + "name": "sequence" + }, + { + "lineno": 224, + "name": "fee" + }, + { + "lineno": 233, + "name": "tx" + }, + { + "lineno": 246, + "name": "value" + }, + { + "lineno": 256, + "name": "value" + }, + { + "lineno": 266, + "name": "value" + }, + { + "lineno": 276, + "name": "value" + }, + { + "lineno": 286, + "name": "value" + }, + { + "lineno": 296, + "name": "value" + }, + { + "lineno": 306, + "name": "value" + }, + { + "lineno": 316, + "name": "value" + }, + { + "lineno": 326, + "name": "publicKey" + }, + { + "lineno": 326, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 22, + "name": "SetFee" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 211, + "name": "SetFeeBuilder" + } + ], + "description": "Defines the SetFee transaction type for XRPL, providing a type-safe wrapper (SetFee) and a fluent builder (SetFeeBuilder) for constructing and signing SetFee transactions, including accessors and mutators for transaction-specific fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/SetFee.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "SetFee::SetFee" + }, + { + "args": [], + "lineno": 39, + "name": "SetFee::getLedgerSequence" + }, + { + "args": [], + "lineno": 50, + "name": "SetFee::hasLedgerSequence" + }, + { + "args": [], + "lineno": 61, + "name": "SetFee::getBaseFee" + }, + { + "args": [], + "lineno": 72, + "name": "SetFee::hasBaseFee" + }, + { + "args": [], + "lineno": 83, + "name": "SetFee::getReferenceFeeUnits" + }, + { + "args": [], + "lineno": 94, + "name": "SetFee::hasReferenceFeeUnits" + }, + { + "args": [], + "lineno": 105, + "name": "SetFee::getReserveBase" + }, + { + "args": [], + "lineno": 116, + "name": "SetFee::hasReserveBase" + }, + { + "args": [], + "lineno": 127, + "name": "SetFee::getReserveIncrement" + }, + { + "args": [], + "lineno": 138, + "name": "SetFee::hasReserveIncrement" + }, + { + "args": [], + "lineno": 149, + "name": "SetFee::getBaseFeeDrops" + }, + { + "args": [], + "lineno": 160, + "name": "SetFee::hasBaseFeeDrops" + }, + { + "args": [], + "lineno": 171, + "name": "SetFee::getReserveBaseDrops" + }, + { + "args": [], + "lineno": 182, + "name": "SetFee::hasReserveBaseDrops" + }, + { + "args": [], + "lineno": 193, + "name": "SetFee::getReserveIncrementDrops" + }, + { + "args": [], + "lineno": 204, + "name": "SetFee::hasReserveIncrementDrops" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 224, + "name": "SetFeeBuilder::SetFeeBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 233, + "name": "SetFeeBuilder::SetFeeBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 246, + "name": "SetFeeBuilder::setLedgerSequence" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 256, + "name": "SetFeeBuilder::setBaseFee" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 266, + "name": "SetFeeBuilder::setReferenceFeeUnits" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 276, + "name": "SetFeeBuilder::setReserveBase" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 286, + "name": "SetFeeBuilder::setReserveIncrement" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 296, + "name": "SetFeeBuilder::setBaseFeeDrops" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 306, + "name": "SetFeeBuilder::setReserveBaseDrops" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 316, + "name": "SetFeeBuilder::setReserveIncrementDrops" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 326, + "name": "SetFeeBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/SetFee.h.ai.md b/include/xrpl/protocol_autogen/transactions/SetFee.h.ai.md new file mode 100644 index 0000000000..8e11aedf2c --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SetFee.h.ai.md @@ -0,0 +1,39 @@ +# `SetFee.h` — Auto-generated Type-Safe Wrapper for the SetFee Pseudo-Transaction + +## Role in the System + +`SetFee.h` is a machine-generated header in the `xrpl::transactions` namespace that provides two complementary classes for working with XRPL's `SetFee` pseudo-transaction (`ttFEE`, type code 101): a read-only wrapper (`SetFee`) and a fluent builder (`SetFeeBuilder`). The file belongs to a family of ~70 similar generated headers under `include/xrpl/protocol_autogen/transactions/`, one per transaction type in the protocol. + +The `SetFee` transaction is not a user-submitted transaction. It is a **pseudo-transaction** created by the consensus process during fee voting, where validators collectively propose and ratify changes to the network's fundamental cost parameters: the base fee charged for transaction processing and the account reserve requirements. Because it is generated by the network rather than submitted by users, it is flagged `notDelegable` and carries no amendment guard (`uint256{}`), making it a baseline protocol mechanism. + +## Field Schema and the Legacy/Modern Split + +The class exposes eight optional fields that reflect two historical generations of the fee schedule representation: + +**Legacy fields** (pre-`XRPFees` amendment): `sfBaseFee` (`uint64`), `sfReferenceFeeUnits` (`uint32`), `sfReserveBase` (`uint32`), and `sfReserveIncrement` (`uint32`). These encode fee values as unit-based integers, which required a separate reference unit interpretation to arrive at actual drop amounts. + +**Modern fields** (post-`XRPFees` amendment): `sfBaseFeeDrops`, `sfReserveBaseDrops`, and `sfReserveIncrementDrops`, all typed as `SF_AMOUNT` — the `STAmount` type that directly encodes drop amounts without unit indirection. + +Additionally, `sfLedgerSequence` (`uint32`) is exposed to indicate which ledger sequence the fee change targets. All fields are marked `soeOPTIONAL` in the schema, meaning their presence depends on which amendment era the transaction belongs to. Code reading a `SetFee` transaction must check `hasFoo()` before calling `getFoo()`. + +## `SetFee`: The Immutable Wrapper + +`SetFee` extends `TransactionBase`, which stores a `std::shared_ptr` to provide shared, immutable ownership semantics. The single constructor takes such a shared pointer and immediately validates that the wrapped transaction is actually a `ttFEE` type — throwing `std::runtime_error` on mismatch. This guard prevents silent type confusion: every downstream caller of `SetFee` can rely on the invariant that the wrapped object is the correct transaction kind. + +Field access follows a consistent two-method pattern for each field: a `hasFoo()` predicate that calls `tx_->isFieldPresent()`, and a `getFoo()` getter that returns `protocol_autogen::Optional`. The `Optional` alias, defined in `Utils.h`, resolves to `std::optional>` when `T` is a reference type and `std::optional` otherwise. This handles the case where `STObject::at()` returns a reference to an internal field rather than a copy, avoiding the ill-formed `std::optional`. The getter calls `hasFoo()` internally before accessing the field, so callers never risk throwing from a missing-field access. + +All getters are decorated with `[[nodiscard]]`, enforcing that callers actually use the returned value — a pattern appropriate for a pure read-only accessor layer. + +## `SetFeeBuilder`: CRTP Fluent Builder + +`SetFeeBuilder` inherits from `TransactionBuilderBase` using the Curiously Recurring Template Pattern. The base class stores an `STObject object_{sfTransaction}` and provides setters for universal transaction fields (`setAccount`, `setFee`, `setSequence`, `setMemos`, etc.). Each setter returns `Derived&` — resolved at compile time to `SetFeeBuilder&` — enabling fluent method chaining without virtual dispatch overhead. + +The `SetFeeBuilder` constructor accepting `(account, sequence, fee)` wires up the `ttFEE` transaction type and delegates initialization to `TransactionBuilderBase`. The second constructor, taking a `std::shared_ptr`, deserializes an existing transaction back into a mutable `STObject` by doing `object_ = *tx` — effectively a copy of the underlying serialized form. This allows reading a `SetFee` from the ledger, modifying it through the builder API, and re-signing it for a new purpose. + +Each type-specific setter uses `std::decay_t` for its parameter type. The `std::decay_t` strips references and cv-qualifiers from the field's native value type, so callers can pass rvalues or lvalues interchangeably without worrying about reference binding rules. + +The `build()` method calls the protected `sign(publicKey, secretKey)` inherited from the base class, which serializes the object with `HashPrefix::txSign` prepended (per XRPL's canonical signing scheme), computes a signature, then sets `sfSigningPubKey` and `sfTxnSignature` on the object. It then constructs a `SetFee` directly: `SetFee{std::make_shared(std::move(object_))}`. The builder is consumed at this point — `object_` has been moved out — so the returned `SetFee` is the sole owner of the resulting signed transaction. + +## Code Generation Context + +The leading comment `// This file is auto-generated. Do not edit.` explains the uniform mechanical structure: identical `hasFoo/getFoo` pairs, identical setter signatures, identical `build()` shape. Across all ~70 transaction headers in this directory, the same pattern repeats with only the field list changing. The generator uses `std::decay_t` consistently for setter parameters rather than spelling out `uint32_t` or `STAmount` directly, making the generated code robust to future changes in field type aliases. This file should be regenerated from the transaction schema definition rather than edited manually. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/SetRegularKey.h.ai.json b/include/xrpl/protocol_autogen/transactions/SetRegularKey.h.ai.json new file mode 100644 index 0000000000..b7739edfa9 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SetRegularKey.h.ai.json @@ -0,0 +1,89 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 76, + "name": "account" + }, + { + "lineno": 76, + "name": "sequence" + }, + { + "lineno": 76, + "name": "fee" + }, + { + "lineno": 85, + "name": "tx" + }, + { + "lineno": 90, + "name": "value" + }, + { + "lineno": 99, + "name": "publicKey" + }, + { + "lineno": 99, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "SetRegularKey" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 69, + "name": "SetRegularKeyBuilder" + } + ], + "description": "Provides an immutable wrapper and builder for the SetRegularKey transaction type in XRPL, enabling type-safe field access and fluent transaction construction.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/SetRegularKey.h", + "functions": [ + { + "args": [], + "lineno": 41, + "name": "getRegularKey" + }, + { + "args": [], + "lineno": 52, + "name": "hasRegularKey" + }, + { + "args": [ + "value" + ], + "lineno": 89, + "name": "setRegularKey" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 98, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/SetRegularKey.h.ai.md b/include/xrpl/protocol_autogen/transactions/SetRegularKey.h.ai.md new file mode 100644 index 0000000000..da70644cc4 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SetRegularKey.h.ai.md @@ -0,0 +1,34 @@ +# `SetRegularKey.h` — Auto-generated SetRegularKey Transaction Wrapper + +## Role in the System + +This file is part of the `protocol_autogen` layer — a family of auto-generated headers, one per transaction type, that impose a type-safe C++ interface on top of XRPL's dynamically-typed `STTx` serialization substrate. `SetRegularKey.h` covers transaction type `ttREGULAR_KEY_SET` (numeric value 5), one of the oldest and most security-sensitive transaction types on the XRP Ledger. + +The `SetRegularKey` transaction allows an account holder to assign a secondary *regular key* to their account, or to remove one by omitting the field entirely. Once a regular key is set, it can be used to sign subsequent transactions in place of the account's master key. This decouples routine signing credentials from the master key pair, which is critical for operational security: the master key can be kept in cold storage while the regular key handles day-to-day activity. The optional nature of `sfRegularKey` is therefore load-bearing — an absent field signals intent to *remove* any existing regular key, not merely a missing input. + +## Design: Immutable Wrapper + Builder Pair + +The file defines two classes in `xrpl::transactions` that together implement the read/write split common to all autogen transaction types. + +`SetRegularKey` extends `TransactionBase` and acts as an immutable, read-only view of a signed transaction. It holds a `std::shared_ptr` (inherited as `tx_`), making the wrapper cheap to copy and pass around while guaranteeing the underlying data cannot be mutated post-construction. The only transaction-specific accessor beyond what `TransactionBase` provides is `getRegularKey()`, which returns `protocol_autogen::Optional` — returning `std::nullopt` when `sfRegularKey` is absent. The companion `hasRegularKey()` method allows callers to distinguish between "field absent" and checking the value, following the pattern used for all optional fields across the autogen layer. + +`SetRegularKeyBuilder` extends `TransactionBuilderBase` via CRTP, which is why `setRegularKey()` returns `SetRegularKeyBuilder&` rather than the base type — the template machinery resolves `static_cast(*this)` to the concrete type at compile time, preserving full method chaining without virtual dispatch. The builder holds a mutable `STObject` internally (initialized as an `sfTransaction` object). Fields are written directly into this object by key (`object_[sfRegularKey] = value`), and the `STTx` is only materialized at `build()` time, which atomically signs the object and constructs the immutable `SetRegularKey` wrapper. + +## Type Validation as a Defensive Guard + +Both constructors that accept an existing `std::shared_ptr` check `getTxnType()` against `ttREGULAR_KEY_SET` and throw `std::runtime_error` on mismatch. This is necessary because `STTx` is a generic property bag; nothing in the type system prevents a caller from accidentally wrapping a `Payment` with a `SetRegularKey`. The check also appears in `SetRegularKeyBuilder`'s copy-from-existing constructor, which supports a round-trip workflow (deserializing a transaction and re-wrapping it in the builder for modification before re-signing). + +## Security Annotation: `notDelegable` + +The macro definition in `transactions.macro` marks this transaction type `Delegation::notDelegable`. Unlike most operational transactions (offers, payments, escrow), `SetRegularKey` cannot be executed by a delegate account on behalf of another. This is a hardcoded policy decision: allowing a delegate to change another account's signing key would let a limited-trust party permanently compromise the account. The `notDelegable` annotation is surfaced in the class docstring but enforced elsewhere in the ledger's transaction processing logic, not in this header. + +## Relationship to Other Files + +- **`TransactionBase.h`** provides the `tx_` member and all common field accessors (`getAccount()`, `getSequence()`, `getFee()`, `getSigners()`, etc.). `SetRegularKey` inherits these without override. +- **`TransactionBuilderBase.h`** provides all common field setters and the `sign()` method (which serializes with `HashPrefix::txSign`, computes the signature, and sets `sfSigningPubKey` and `sfTxnSignature`). The deliberate choice *not* to call `object_.set(soTemplate)` in the base constructor avoids creating placeholder `soeDEFAULT` fields that would cause `applyTemplate()` to reject them when the `STTx` is constructed. +- **`transactions.macro`** is the ground truth for the field schema: `sfRegularKey` is the only transaction-specific field, and it is `soeOPTIONAL`. This matches the design where a `SetRegularKeyBuilder` with no `setRegularKey()` call produces a valid transaction that clears the regular key. +- **`AccountRoot`** in `ledger_entries` is the ledger object mutated when this transaction is applied — its own `sfRegularKey` field (also `soeOPTIONAL`) is written or cleared accordingly. + +## Practical Usage Flow + +A typical usage constructs the builder with the signing account, chains any optional fields, calls `build(publicKey, secretKey)` to produce a signed `SetRegularKey` object, then passes it (or its underlying `getSTTx()`) to the transaction submission layer. The split means the caller cannot accidentally submit an unsigned transaction, and once `build()` returns, the result is immutable — there is no way to modify a `SetRegularKey` after it has been created without going back through the builder. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/SignerListSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/SignerListSet.h.ai.json new file mode 100644 index 0000000000..f477a0aa72 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SignerListSet.h.ai.json @@ -0,0 +1,101 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 77, + "name": "account" + }, + { + "lineno": 78, + "name": "signerQuorum" + }, + { + "lineno": 79, + "name": "sequence" + }, + { + "lineno": 80, + "name": "fee" + }, + { + "lineno": 113, + "name": "publicKey" + }, + { + "lineno": 113, + "name": "secretKey" + }, + { + "lineno": 94, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "SignerListSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& signerQuorum, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 70, + "name": "SignerListSetBuilder" + } + ], + "description": "Defines the SignerListSet transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing SignerListSet transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/SignerListSet.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getSignerQuorum" + }, + { + "args": [], + "lineno": 48, + "name": "getSignerEntries" + }, + { + "args": [], + "lineno": 59, + "name": "hasSignerEntries" + }, + { + "args": [ + "value" + ], + "lineno": 93, + "name": "setSignerQuorum" + }, + { + "args": [ + "value" + ], + "lineno": 102, + "name": "setSignerEntries" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 110, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/SignerListSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/SignerListSet.h.ai.md new file mode 100644 index 0000000000..3ca975805d --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/SignerListSet.h.ai.md @@ -0,0 +1,37 @@ +# `SignerListSet.h` — Auto-Generated Transaction Wrapper for `ttSIGNER_LIST_SET` + +## Role in the System + +This file is part of the `protocol_autogen` subsystem — a layer of generated C++ headers that give every XRPL transaction type a strongly-typed, compile-time-checked API. It defines two classes: `SignerListSet`, an immutable read-only wrapper over an `STTx` carrying transaction type 12 (`ttSIGNER_LIST_SET`), and `SignerListSetBuilder`, the fluent construction counterpart. + +At the protocol level, the `SignerListSet` transaction is how an XRPL account establishes or removes its multi-signature signer list. Sending this transaction with a populated `sfSignerEntries` array installs a set of cosigners; sending it with `sfSignerQuorum` set to zero and `sfSignerEntries` absent tears the list down. The two-field design (a quorum threshold and the list of signers) mirrors the ledger object structure of the `SignerList` ledger entry. + +## The Two-Class Pattern + +The split between `SignerListSet` (wrapper) and `SignerListSetBuilder` (builder) reflects a deliberate immutability contract. `SignerListSet` wraps a `std::shared_ptr` — the `const` is baked in at the pointer type — so no mutation can happen through the accessor layer. Callers that want to construct a new transaction use `SignerListSetBuilder`, which holds a mutable `STObject` internally and only promotes it to an immutable `STTx` at the moment `build()` is called. + +Both classes enforce transaction type correctness at construction time. The `SignerListSet` constructor calls `tx_->getTxnType()` and throws `std::runtime_error` if the result is not `ttSIGNER_LIST_SET`. `SignerListSetBuilder`'s second constructor (from an existing `STTx`) performs the same check. This makes type mismatches fail loudly at the wrapper boundary rather than silently producing incorrect field reads later. + +## Field Accessors + +`SignerListSet` exposes exactly the fields defined for this transaction type beyond the universal fields provided by `TransactionBase`: + +- `getSignerQuorum()` returns the raw `uint32_t` value of `sfSignerQuorum` directly — no `std::optional` wrapping because `sfSignerQuorum` is `soeREQUIRED`. If the underlying `STTx` is well-formed, this call never fails. + +- `getSignerEntries()` returns `std::optional>`. The `std::reference_wrapper` avoids copying the potentially large array while still returning it through an optional. The companion predicate `hasSignerEntries()` lets callers avoid the optional dereference when they only need a presence check. + +All getters are annotated `[[nodiscard]]`, which ensures callers cannot accidentally ignore a return value that might be `std::nullopt` — a subtle but important correctness nudge for optional fields. + +## Builder and CRTP Chain + +`SignerListSetBuilder` inherits from `TransactionBuilderBase`. The CRTP template parameter makes every common setter in `TransactionBuilderBase` (for fields like `sfFee`, `sfSequence`, `sfFlags`, `sfLastLedgerSequence`, etc.) return `SignerListSetBuilder&` rather than the base class reference, so method chains remain fluent throughout without any casting on the caller side. + +The constructor enforces that `sfSignerQuorum` must be supplied at build time — it is a required field and is wired up immediately via `setSignerQuorum()` in the constructor body. Optional `sequence` and `fee` arguments are forwarded to the base class, which applies them only if they have values, avoiding unintentional injection of unset fields into the `STObject`. + +A notable design note in `TransactionBuilderBase` is the decision *not* to call `object_.set(soTemplate)`. The comment explains that setting a template would insert `soeDEFAULT` placeholder fields, which would then make the `STTx` constructor's `applyTemplate()` call throw "may not be explicitly set to default." Keeping `object_` as a free, untemplatized `STObject` sidesteps that constraint entirely. + +## The `build()` Method and Signing + +`build(PublicKey, SecretKey)` finalises the transaction in two steps. First it calls `sign()` (inherited from `TransactionBuilderBase`), which serialises the object without signing fields, prepends the `HashPrefix::txSign` prefix, and computes the signature. Then it constructs an `STTx` from the moved `STObject` and wraps it in a `SignerListSet` value. The move into `STTx` is intentional: after `build()` the builder's internal `object_` is in a valid-but-unspecified state, making the builder non-reusable — a design that prevents double-signing mistakes. + +The second builder constructor (taking `std::shared_ptr`) allows an already-signed transaction to be re-opened into a builder. This supports scenarios like re-signing with a different key or applying additional fields, after which `build()` produces a fresh `STTx` with an updated signature. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/TicketCreate.h.ai.json b/include/xrpl/protocol_autogen/transactions/TicketCreate.h.ai.json new file mode 100644 index 0000000000..4956ff4134 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/TicketCreate.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 36, + "name": "tx" + }, + { + "lineno": 75, + "name": "account" + }, + { + "lineno": 76, + "name": "ticketCount" + }, + { + "lineno": 77, + "name": "sequence" + }, + { + "lineno": 78, + "name": "fee" + }, + { + "lineno": 87, + "name": "tx" + }, + { + "lineno": 98, + "name": "value" + }, + { + "lineno": 108, + "name": "publicKey" + }, + { + "lineno": 108, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 32, + "name": "TicketCreate" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& ticketCount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 67, + "name": "TicketCreateBuilder" + } + ], + "description": "Defines the TicketCreate transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing TicketCreate transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/TicketCreate.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "TicketCreate::getTicketCount" + }, + { + "args": [ + "value" + ], + "lineno": 97, + "name": "TicketCreateBuilder::setTicketCount" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 106, + "name": "TicketCreateBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/TicketCreate.h.ai.md b/include/xrpl/protocol_autogen/transactions/TicketCreate.h.ai.md new file mode 100644 index 0000000000..d95c70727b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/TicketCreate.h.ai.md @@ -0,0 +1,35 @@ +# `TicketCreate.h` — Auto-generated TicketCreate Transaction Wrapper + +## Role in the System + +This file is one of roughly 70 auto-generated transaction type headers in `include/xrpl/protocol_autogen/transactions/`. Each file follows an identical structural pattern: a read-only `Transaction` class paired with a fluent `TransactionBuilder` class. Together they form a type-safe C++ surface over XRPL's dynamic `STTx` object model, eliminating the need for callers to manipulate raw serialized fields by name. + +`TicketCreate` covers transaction type `ttTICKET_CREATE` (numeric type 10). In the XRPL protocol, this transaction reserves one or more sequence number *tickets* — pre-allocated placeholders that allow future transactions to be submitted out of order without gaps in the account's sequence space. Once created, a ticket can be consumed by any subsequent transaction in place of a normal sequence number, enabling use cases like parallel transaction submission and multi-party coordination. + +## Class: `TicketCreate` + +`TicketCreate` is an immutable wrapper around a `std::shared_ptr`. It inherits from `TransactionBase`, which already exposes getters for all fields common to every transaction type: `getAccount()`, `getSequence()`, `getFee()`, `getFlags()`, `getMemos()`, `getSigners()`, `getLastLedgerSequence()`, `getDelegate()`, and more. Optional fields return `std::optional` and are paired with `hasX()` predicates; required fields are returned by value directly. + +The only `TicketCreate`-specific accessor is `getTicketCount()`, which returns a `SF_UINT32::type::value_type`. This field is `soeREQUIRED` in the XRPL schema — it specifies the number of tickets to create (valid range 1–250 per protocol rules, though that enforcement lives in the ledger application layer, not here). + +Type safety is enforced at construction: the constructor verifies `tx_->getTxnType() == ttTICKET_CREATE` and throws `std::runtime_error` on mismatch. This guard prevents accidental wrapping of an unrelated `STTx` in a strongly-typed `TicketCreate` handle. + +## Class: `TicketCreateBuilder` + +`TicketCreateBuilder` inherits from `TransactionBuilderBase` using the Curiously Recurring Template Pattern (CRTP). The template base returns `Derived&` from every setter, making method chaining work without virtual dispatch or downcasting overhead. The mutable `STObject object_` lives in the base and is built up incrementally before being finalized into an immutable `STTx`. + +A deliberate design choice in `TransactionBuilderBase` is that the internal `object_` is never initialized from an `SOTemplate`. As the comment in `TransactionBuilderBase` explains, calling `set(soTemplate)` would create `STBase` placeholders for `soeDEFAULT` fields, causing `applyTemplate()` to throw "may not be explicitly set to default" when the `STTx` constructor runs. Instead, only fields that are actually set by the caller are populated; the `STTx` constructor then calls `applyTemplate()` to fill defaults and validate structure. + +The builder has two construction paths: +- **Primary constructor**: takes `account` and the required `ticketCount`, plus optional `sequence` and `fee`. This is the standard path for creating a new transaction from scratch. +- **STTx copy constructor**: wraps an existing `std::shared_ptr` for round-trip editing (read an existing transaction, modify, re-sign). It performs the same type-check as the `TicketCreate` wrapper. + +`setTicketCount()` assigns directly to `object_[sfTicketCount]` and returns `*this` (as `TicketCreateBuilder&`) to participate in the fluent chain. + +`build()` calls the protected `sign()` helper inherited from `TransactionBuilderBase`, which serializes the object without signing fields, prepends the `HashPrefix::txSign` prefix, computes the ECDSA/EdDSA signature using the provided key pair, and stores both `sfSigningPubKey` and `sfTxnSignature` back into `object_`. It then moves `object_` into a freshly allocated `STTx` and wraps it in a `TicketCreate` by value. Because `object_` is `std::move`d, the builder is consumed and must not be reused after `build()`. + +## Relationship to the Autogen Layer + +The file is explicitly marked `// This file is auto-generated. Do not edit.` The autogen framework generates one such file per transaction type defined in the XRPL protocol specification. The pattern keeps each file minimal: `TicketCreate.h` contains only the single field that distinguishes `TicketCreate` from the common transaction base. All structural decisions — immutability of the wrapper, CRTP on the builder, the signing flow, schema validation via `passesLocalChecks` — live in `TransactionBase.h` and `TransactionBuilderBase.h` and are shared uniformly across every transaction type. + +The `Delegation::delegable` annotation in the class comment indicates that `TicketCreate` participates in the XRPL delegate account feature, meaning a delegated signer can submit this transaction type on behalf of the originating account, subject to a matching `DelegateSet` permission grant. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/TrustSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/TrustSet.h.ai.json new file mode 100644 index 0000000000..5ca7423455 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/TrustSet.h.ai.json @@ -0,0 +1,154 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 116, + "name": "account" + }, + { + "lineno": 117, + "name": "sequence" + }, + { + "lineno": 118, + "name": "fee" + }, + { + "lineno": 124, + "name": "tx" + }, + { + "lineno": 137, + "name": "value" + }, + { + "lineno": 146, + "name": "value" + }, + { + "lineno": 155, + "name": "value" + }, + { + "lineno": 164, + "name": "publicKey" + }, + { + "lineno": 164, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "TrustSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 111, + "name": "TrustSetBuilder" + } + ], + "description": "Provides an immutable wrapper and builder for the TrustSet transaction in the XRPL protocol, enabling type-safe field access and fluent transaction construction.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/TrustSet.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "TrustSet" + }, + { + "args": [], + "lineno": 44, + "name": "getLimitAmount" + }, + { + "args": [], + "lineno": 55, + "name": "hasLimitAmount" + }, + { + "args": [], + "lineno": 65, + "name": "getQualityIn" + }, + { + "args": [], + "lineno": 76, + "name": "hasQualityIn" + }, + { + "args": [], + "lineno": 86, + "name": "getQualityOut" + }, + { + "args": [], + "lineno": 97, + "name": "hasQualityOut" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 116, + "name": "TrustSetBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 124, + "name": "TrustSetBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 136, + "name": "setLimitAmount" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 145, + "name": "setQualityIn" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 154, + "name": "setQualityOut" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 163, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/TrustSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/TrustSet.h.ai.md new file mode 100644 index 0000000000..50f482fdc9 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/TrustSet.h.ai.md @@ -0,0 +1,33 @@ +# `TrustSet.h` — Auto-Generated TrustSet Transaction Wrapper + +This file is part of the `protocol_autogen` layer, a code-generated collection of ~70 per-transaction headers under `include/xrpl/protocol_autogen/transactions/`. It provides two complementary classes for the `TrustSet` transaction type (`ttTRUST_SET`, numeric type 20): an immutable read-only wrapper for inspecting existing transactions and a fluent builder for constructing new ones. The file must not be hand-edited; any changes belong in the generator. + +## Role in the XRPL Protocol + +A `TrustSet` transaction creates or modifies a *trust line* between two accounts on the XRP Ledger. A trust line specifies the issuing account, the currency, and the maximum amount the submitting account is willing to hold of that issued currency (IOU). Without a trust line, an account cannot receive non-XRP assets. The three transaction-specific fields are all optional: `sfLimitAmount` encodes the currency, issuer, and credit limit as a single `STAmount`; `sfQualityIn` and `sfQualityOut` are legacy 32-bit fixed-point quality multipliers applied to incoming and outgoing transfers on the trust line. + +## `TrustSet` — The Immutable Wrapper + +`TrustSet` extends `TransactionBase`, which owns a `std::shared_ptr` — an already-signed, immutable serialized transaction object. The derived class adds three field-access pairs: a `hasX()` predicate and a `getX()` accessor returning `protocol_autogen::Optional`. + +The `Optional` alias in `Utils.h` is non-trivial: it resolves to `std::optional>>` when `T` is a reference type, and to `std::optional` otherwise. This matters because `STTx::at()` on some field types returns a const reference into the object rather than a copy, and wrapping it in a raw `std::optional` would be a dangling-reference trap. The autogen layer handles this uniformly without callers needing to think about it. + +The constructor takes a `std::shared_ptr` and immediately checks `getTxnType() != ttTRUST_SET`, throwing `std::runtime_error` on mismatch. This is a deliberate eager validation: a `TrustSet` instance can only ever wrap a `TrustSet` transaction, so no runtime guard is needed at each field access. The cost — one virtual dispatch at construction — is negligible compared to the safety guarantee. + +`TransactionBase` also provides common-field accessors shared across all transaction types: `getAccount()`, `getSequence()`, `getFee()`, `getFlags()`, `getMemos()`, `getSigners()`, `getDelegate()`, and others. The `validate()` method cross-checks the wrapped `STTx` against the ledger's `TxFormats` schema template and runs local checks, giving a complete correctness guarantee beyond type safety alone. + +## `TrustSetBuilder` — The Fluent Builder + +`TrustSetBuilder` inherits from `TransactionBuilderBase`, which uses CRTP so that the base-class setters (`setAccount()`, `setFee()`, `setSequence()`, `setFlags()`, etc.) return `Derived&` — a `TrustSetBuilder&` — preserving the concrete type through the chain. The internal state is a mutable `STObject object_{sfTransaction}`. + +A key design note in `TransactionBuilderBase`: the constructor explicitly avoids calling `object_.set(soTemplate)`. Without this, `STTx`'s own `applyTemplate()` call during construction would encounter pre-populated placeholder fields for `soeDEFAULT` entries and throw "may not be explicitly set to default". The builder therefore keeps `object_` as a free `STObject`, relying on `STTx`'s constructor to enforce the schema on finalization. + +`TrustSetBuilder` offers two construction paths: building fresh from account/sequence/fee, or reconstructing from an existing `STTx` (useful for editing a partially-built transaction). The second path copies the inner `STObject` via `object_ = *tx` and similarly validates the type upfront. + +The `build()` method calls the protected `sign()` from `TransactionBuilderBase`, which serializes the object without signing fields, prepends `HashPrefix::txSign`, signs the payload with the provided `PublicKey`/`SecretKey`, attaches the signature, then constructs a final `STTx` — promoted from the mutable `STObject` — and wraps it in a `TrustSet` instance. After `build()`, the builder's `object_` is moved away and should not be reused. + +The `setLimitAmount()`, `setQualityIn()`, and `setQualityOut()` setters take their values as `std::decay_t const&` and `std::decay_t const&`. Using `std::decay_t` strips any reference or cv-qualification from the field type's canonical form, which future-proofs these signatures against potential changes to the underlying `SField` type aliases without requiring regeneration of call sites. + +## Autogen Pattern and Broader Context + +Every transaction in this directory — from `Payment.h` to `VaultCreate.h` — follows the exact same dual-class pattern. The uniformity allows generic code consuming `TransactionBase*` to dispatch on `getTransactionType()` and then safely `static_cast` (or construct the appropriate typed wrapper) with confidence. The metadata in the `.ai.json` sidecar and the `// This file is auto-generated. Do not edit.` guard at the top signal that the generator is the single source of truth for field lists, optionality, and type mappings — any XRPL protocol amendment that adds or removes a `TrustSet` field is reflected here by regeneration, not manual edit. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/UNLModify.h.ai.json b/include/xrpl/protocol_autogen/transactions/UNLModify.h.ai.json new file mode 100644 index 0000000000..a9cea50e9a --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/UNLModify.h.ai.json @@ -0,0 +1,154 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 80, + "name": "account" + }, + { + "lineno": 81, + "name": "uNLModifyDisabling" + }, + { + "lineno": 82, + "name": "ledgerSequence" + }, + { + "lineno": 83, + "name": "uNLModifyValidator" + }, + { + "lineno": 84, + "name": "sequence" + }, + { + "lineno": 85, + "name": "fee" + }, + { + "lineno": 92, + "name": "tx" + }, + { + "lineno": 104, + "name": "value" + }, + { + "lineno": 113, + "name": "value" + }, + { + "lineno": 122, + "name": "value" + }, + { + "lineno": 131, + "name": "publicKey" + }, + { + "lineno": 131, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 25, + "name": "UNLModify" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& uNLModifyDisabling, std::decay_t const& ledgerSequence, std::decay_t const& uNLModifyValidator, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 74, + "name": "UNLModifyBuilder" + } + ], + "description": "Defines the UNLModify transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing UNLModify transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/UNLModify.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "UNLModify" + }, + { + "args": [], + "lineno": 41, + "name": "getUNLModifyDisabling" + }, + { + "args": [], + "lineno": 51, + "name": "getLedgerSequence" + }, + { + "args": [], + "lineno": 61, + "name": "getUNLModifyValidator" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& uNLModifyDisabling", + "std::decay_t const& ledgerSequence", + "std::decay_t const& uNLModifyValidator", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 80, + "name": "UNLModifyBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 92, + "name": "UNLModifyBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 104, + "name": "setUNLModifyDisabling" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 113, + "name": "setLedgerSequence" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 122, + "name": "setUNLModifyValidator" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 131, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/UNLModify.h.ai.md b/include/xrpl/protocol_autogen/transactions/UNLModify.h.ai.md new file mode 100644 index 0000000000..55e2c301e2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/UNLModify.h.ai.md @@ -0,0 +1,35 @@ +# `UNLModify.h` — Auto-generated Negative UNL Pseudo-Transaction Wrapper + +## Role in the System + +`UNLModify.h` defines the type-safe C++ interface for the `ttUNL_MODIFY` pseudo-transaction (type code 102). This transaction type is not submitted by end users: it is synthesized internally by the ledger's negative UNL consensus logic — specifically `NegativeUNLVote.cpp` — to record a network-agreed decision to either disable or re-enable a specific validator in the Unique Node List. Along with `ttAMENDMENT` and `ttFEE`, it forms one of three "pseudo-transaction" types that `isPseudoTx()` recognizes in `STTx.cpp`. The distinction matters: `TransactionBase::validate()` deliberately skips the `passesLocalChecks()` call for pseudo-transactions, because the normal submission rules (fee sufficiency, signature validity in the user sense) do not apply to ledger-generated entries. + +The file is part of the `protocol_autogen` layer, a code-generated family of per-transaction-type headers under `include/xrpl/protocol_autogen/transactions/`. Every header in this directory follows the same two-class pattern seen here: an immutable wrapper and a corresponding fluent builder. + +## The `UNLModify` Wrapper + +`UNLModify` extends `TransactionBase`, which holds a `std::shared_ptr` and exposes read-only accessors for the common fields shared by all transactions (`getAccount()`, `getSequence()`, `getFee()`, `getSigners()`, etc.). `UNLModify` layers on three additional required-field getters that are specific to this transaction type: + +- `getUNLModifyDisabling()` — returns the `sfUNLModifyDisabling` `uint8` flag. In practice `NegativeUNLVote.cpp` writes `1` to signal a validator should be added to the negative UNL (disabled) and `0` to signal it should be removed (re-enabled). +- `getLedgerSequence()` — returns `sfLedgerSequence` (`uint32`), the ledger at which the UNL modification is anchored. +- `getUNLModifyValidator()` — returns `sfUNLModifyValidator` as an `SF_VL::type::value_type` (a `Blob` / byte vector), carrying the raw serialized public key of the validator being acted on. + +The constructor accepts a `std::shared_ptr` and throws `std::runtime_error` if the wrapped transaction's type is not `ttUNL_MODIFY`. This guard is intentional: wrapping the wrong `STTx` would silently return garbage from every field getter without it. + +All getters are `[[nodiscard]]`, reinforcing that the wrapper is a pure value-access interface with no side effects. The `const`-only `STTx` pointer makes immutability a compile-time guarantee. + +## The `UNLModifyBuilder` + +`UNLModifyBuilder` inherits `TransactionBuilderBase`, using the Curiously Recurring Template Pattern (CRTP). The CRTP is the reason each `setX()` method in the base class can return `Derived&` — i.e., `UNLModifyBuilder&` — rather than a `TransactionBuilderBase&`, preserving method-chaining without requiring virtual dispatch or user-side casts. + +The primary constructor takes all three required fields plus optional `sequence` and `fee`, delegates to `TransactionBuilderBase`'s initializing constructor (which sets `sfTransactionType` and `sfAccount`, and conditionally sets `sfSequence`/`sfFee`), then immediately calls the three field setters. A second constructor accepts an existing `std::shared_ptr` and copies it into the internal `STObject object_` member, enabling round-trip mutation: load an existing pseudo-transaction, modify it, and re-sign. + +Field setter parameters use `std::decay_t const&` (and equivalents for the other field types) rather than the raw `value_type` directly. This pattern strips any reference or cv-qualifiers that might be embedded in the sfield's `value_type` typedef, ensuring the setter accepts a stable `const&` regardless of how the underlying sfield type is defined. + +The `build()` method calls the base class `sign()`, which appends `HashPrefix::txSign` before the serialized object body, invokes `xrpl::sign()` to produce the ECDSA/EdDSA signature, and writes both `sfSigningPubKey` and `sfTxnSignature` onto the `STObject`. It then constructs a final `STTx` from the now-signed `STObject` via move, wraps it in `std::make_shared`, and returns a freshly constructed `UNLModify` wrapper — the builder is consumed in the process. + +## Pseudo-Transaction Context + +Because `UNLModify` is a pseudo-transaction, several normal transaction constraints are relaxed or bypassed. `TransactionBase::validate()` checks `isPseudoTx(*tx_)` before calling `passesLocalChecks()`; pseudo-transactions pass validation unconditionally once their schema is verified. In `NegativeUNLVote.cpp` the transaction is constructed directly via `STTx(ttUNL_MODIFY, lambda)` — without going through `UNLModifyBuilder` — because the consensus layer predates this autogenerated layer. The autogenerated builder exists to provide a uniform, testable API surface for tools and tests that need to construct or inspect these transactions programmatically, as verified by the round-trip and type-guard tests in `UNLModifyTests.cpp`. + +The `Delegation::notDelegable` and empty-amendment-guard annotations in the class comment are metadata consumed by the code generator, not runtime guards; they document that `ttUNL_MODIFY` cannot be issued via an account-delegation mechanism and is not gated behind any feature amendment. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultClawback.h.ai.json b/include/xrpl/protocol_autogen/transactions/VaultClawback.h.ai.json new file mode 100644 index 0000000000..ff2b235cb2 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultClawback.h.ai.json @@ -0,0 +1,154 @@ +{ + "args": [ + { + "lineno": 28, + "name": "tx" + }, + { + "lineno": 91, + "name": "account" + }, + { + "lineno": 91, + "name": "vaultID" + }, + { + "lineno": 91, + "name": "holder" + }, + { + "lineno": 91, + "name": "sequence" + }, + { + "lineno": 91, + "name": "fee" + }, + { + "lineno": 104, + "name": "tx" + }, + { + "lineno": 116, + "name": "value" + }, + { + "lineno": 124, + "name": "value" + }, + { + "lineno": 132, + "name": "value" + }, + { + "lineno": 140, + "name": "publicKey" + }, + { + "lineno": 140, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 26, + "name": "VaultClawback" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& vaultID, std::decay_t const& holder, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 88, + "name": "VaultClawbackBuilder" + } + ], + "description": "Defines the VaultClawback transaction type and its builder for the XRPL protocol, providing type-safe field access and a fluent interface for constructing VaultClawback transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/VaultClawback.h", + "functions": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "VaultClawback" + }, + { + "args": [], + "lineno": 43, + "name": "getVaultID" + }, + { + "args": [], + "lineno": 52, + "name": "getHolder" + }, + { + "args": [], + "lineno": 61, + "name": "getAmount" + }, + { + "args": [], + "lineno": 73, + "name": "hasAmount" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& vaultID", + "std::decay_t const& holder", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 91, + "name": "VaultClawbackBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 104, + "name": "VaultClawbackBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 116, + "name": "setVaultID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 124, + "name": "setHolder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 132, + "name": "setAmount" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 140, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultClawback.h.ai.md b/include/xrpl/protocol_autogen/transactions/VaultClawback.h.ai.md new file mode 100644 index 0000000000..7f2b99147c --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultClawback.h.ai.md @@ -0,0 +1,64 @@ +# `VaultClawback.h` — Auto-Generated Transaction Wrapper for `ttVAULT_CLAWBACK` + +## Role and Context + +This file defines the typed interface for the `VaultClawback` transaction — XRPL transaction type `ttVAULT_CLAWBACK` (numeric code 70) — which allows a vault authority to reclaim assets held by a specific account inside a Single Asset Vault. It is part of the `featureSingleAssetVault` amendment and lives in the `include/xrpl/protocol_autogen/transactions/` directory alongside every other transaction type in the ledger's autogenerated layer. + +The header carries an explicit `// This file is auto-generated. Do not edit.` notice. Hand-writing this code for every one of the ~70+ transaction types would be error-prone and maintenance-heavy; code generation guarantees structural consistency across the entire transaction family while still allowing each type's distinct fields and privilege annotations to be expressed cleanly. + +## The Immutable Wrapper: `VaultClawback` + +`VaultClawback` extends `TransactionBase` and wraps a `std::shared_ptr`. The `const` in the shared pointer's element type is the core design choice: once constructed, the transaction bytes are immutable, and every accessor is marked `[[nodiscard]]`. This models the semantics of an already-submitted or already-signed transaction where in-place mutation would be unsafe. + +The constructor accepts a `std::shared_ptr` by value, moves it into the base class, and then immediately type-checks the wrapped transaction: + +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for VaultClawback"); +``` + +Because `txType` is a `static constexpr xrpl::TxType`, this check is a compile-time constant compared against a runtime value. The throw-on-mismatch pattern prevents a `VaultClawback` wrapper from ever being silently handed a `Payment` or `VaultDeposit` — the kind of type confusion that would produce nonsensical field reads rather than errors. + +### Transaction Fields + +`VaultClawback` exposes three transaction-specific fields: + +**`sfVaultID`** (required) — a `uint256` identifying the vault whose asset pool is being modified. Returned by `getVaultID()` as a direct value type; no optional wrapper because presence is mandatory. + +**`sfHolder`** (required) — an `AccountID` designating the account whose vault shares or balance are being revoked. Returned by `getHolder()` directly. The presence of an explicit holder field distinguishes this from the generic `Clawback` transaction (type 30), which encodes the target implicitly in `sfAmount`. + +**`sfAmount`** (optional, MPT-capable) — the quantity to reclaim. `getAmount()` returns `protocol_autogen::Optional` (i.e., `std::optional`) and delegates presence-checking to `hasAmount()`. The optionality here reflects the on-ledger semantic that an issuer may wish to claw back an entire position — leaving the amount unspecified signals "all available" — rather than a partial amount. The MPT note in the doc comment is significant: `sfAmount` in this context accepts Multi-Purpose Token amounts, consistent with the vault's role as an MPT-backed construct. + +## The Builder: `VaultClawbackBuilder` + +`VaultClawbackBuilder` extends `TransactionBuilderBase` using CRTP. The template parameter ensures that every setter inherited from the base — `setFee()`, `setSequence()`, `setLastLedgerSequence()`, `setDelegate()`, etc. — returns `VaultClawbackBuilder&` rather than the base type, preserving the fluent method-chaining contract across the full inheritance hierarchy. + +The primary constructor enforces both required fields upfront: + +```cpp +VaultClawbackBuilder(account, vaultID, holder, sequence, fee) +``` + +`sequence` and `fee` are `std::optional` because they may not be known at construction time — the caller can supply them later or let a signing library fill them in. `vaultID` and `holder`, being required by the transaction schema, are non-optional constructor parameters, making it structurally impossible to forget them. + +The `std::decay_t` pattern on the `vaultID` parameter is deliberate. `SF_UINT256::type::value_type` could resolve to a reference type depending on how the `SField` traits are defined; `std::decay_t` strips references and cv-qualifiers, ensuring the setter takes a const-ref to a plain value type rather than a double-reference. The same pattern appears on `setVaultID()` and `setHolder()`. + +A second constructor takes an existing `std::shared_ptr` and copies its `STObject` representation into the builder's mutable `object_` member. This enables cloning and re-signing an existing serialized transaction — useful in test infrastructure or transaction replay. + +### Build and Sign + +`build(PublicKey, SecretKey)` first calls the protected `sign()` method inherited from `TransactionBuilderBase`, which: + +1. Sets `sfSigningPubKey` to the raw public key bytes. +2. Serializes the object (excluding signing fields) prefixed with `HashPrefix::txSign`. +3. Computes the cryptographic signature and sets `sfTxnSignature`. + +Only then does it construct a `VaultClawback` wrapper around a freshly moved `STTx`. The sequence ensures the returned wrapper always holds a signed, immutable transaction — you cannot get a `VaultClawback` object that is unsigned, because the only path to one passes through `sign()`. + +## Privilege and Delegation Context + +The doc-comment block records `Privileges: mayDeleteMPT | mustModifyVault` and `Delegable: Delegation::notDelegable`. The `notDelegable` flag is notable because the related generic `Clawback` transaction (type 30, `featureClawback`) *is* delegable. The vault-specific variant is intentionally restricted — vault authority should not be delegatable, presumably because the vault model gives the issuer strong unilateral control that XRPL's delegation framework is not designed to safely relay. + +## Relationship to Sibling Files + +Every vault transaction — `VaultCreate`, `VaultDeposit`, `VaultWithdraw`, `VaultDelete`, `VaultSet`, and `VaultClawback` — shares the same structural template in this directory. `VaultClawback` is distinctive in carrying the `sfHolder` field alongside `sfVaultID`, making it the only vault transaction that directly names a third-party account as the subject of the operation, reflecting its regulatory / compliance-enforcement use case. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultCreate.h.ai.json b/include/xrpl/protocol_autogen/transactions/VaultCreate.h.ai.json new file mode 100644 index 0000000000..42a5665f4c --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultCreate.h.ai.json @@ -0,0 +1,153 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "VaultCreate" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& asset, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 181, + "name": "VaultCreateBuilder" + } + ], + "description": "Defines the VaultCreate transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing VaultCreate transactions with various fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/VaultCreate.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "VaultCreate::getAsset" + }, + { + "args": [], + "lineno": 54, + "name": "VaultCreate::getAssetsMaximum" + }, + { + "args": [], + "lineno": 65, + "name": "VaultCreate::hasAssetsMaximum" + }, + { + "args": [], + "lineno": 75, + "name": "VaultCreate::getMPTokenMetadata" + }, + { + "args": [], + "lineno": 86, + "name": "VaultCreate::hasMPTokenMetadata" + }, + { + "args": [], + "lineno": 96, + "name": "VaultCreate::getDomainID" + }, + { + "args": [], + "lineno": 107, + "name": "VaultCreate::hasDomainID" + }, + { + "args": [], + "lineno": 117, + "name": "VaultCreate::getWithdrawalPolicy" + }, + { + "args": [], + "lineno": 128, + "name": "VaultCreate::hasWithdrawalPolicy" + }, + { + "args": [], + "lineno": 138, + "name": "VaultCreate::getData" + }, + { + "args": [], + "lineno": 149, + "name": "VaultCreate::hasData" + }, + { + "args": [], + "lineno": 159, + "name": "VaultCreate::getScale" + }, + { + "args": [], + "lineno": 170, + "name": "VaultCreate::hasScale" + }, + { + "args": [ + "value" + ], + "lineno": 200, + "name": "VaultCreateBuilder::setAsset" + }, + { + "args": [ + "value" + ], + "lineno": 210, + "name": "VaultCreateBuilder::setAssetsMaximum" + }, + { + "args": [ + "value" + ], + "lineno": 220, + "name": "VaultCreateBuilder::setMPTokenMetadata" + }, + { + "args": [ + "value" + ], + "lineno": 230, + "name": "VaultCreateBuilder::setDomainID" + }, + { + "args": [ + "value" + ], + "lineno": 240, + "name": "VaultCreateBuilder::setWithdrawalPolicy" + }, + { + "args": [ + "value" + ], + "lineno": 250, + "name": "VaultCreateBuilder::setData" + }, + { + "args": [ + "value" + ], + "lineno": 260, + "name": "VaultCreateBuilder::setScale" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 270, + "name": "VaultCreateBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultCreate.h.ai.md b/include/xrpl/protocol_autogen/transactions/VaultCreate.h.ai.md new file mode 100644 index 0000000000..a167c7633b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultCreate.h.ai.md @@ -0,0 +1,44 @@ +# `VaultCreate.h` — Auto-Generated Vault Creation Transaction Wrapper + +## Role in the System + +This file is part of the `protocol_autogen` code-generation layer inside `xrpl::transactions`. It implements the `VaultCreate` transaction (type code `ttVAULT_CREATE = 65`), which creates a single-asset vault on the XRPL ledger. The vault primitive is gated behind the `featureSingleAssetVault` amendment and serves as the foundation for on-ledger asset pooling — holders deposit one asset class and receive a proportional share token (backed by an MPT issuance) in return. + +The file is marked `// This file is auto-generated. Do not edit.` and follows the same template as every other transaction in the `include/xrpl/protocol_autogen/transactions/` directory. The pattern is consistent across ~70 transaction types: a read-only wrapper class and a corresponding fluent builder, both living in the `xrpl::transactions` namespace. + +## Class Hierarchy and Design + +`VaultCreate` inherits from `TransactionBase`, which wraps a `std::shared_ptr` and exposes read-only accessors for all common transaction fields (`sfAccount`, `sfSequence`, `sfFee`, `sfFlags`, `sfSigners`, etc.). `VaultCreate` itself adds vault-specific field accessors on top of that shared foundation. + +`VaultCreateBuilder` inherits from the CRTP template `TransactionBuilderBase`. The CRTP is load-bearing here: every setter in the base class calls `static_cast(*this)` before returning, ensuring that method chaining on a `VaultCreateBuilder&` keeps the concrete derived type in scope rather than collapsing back to `TransactionBuilderBase&`. Without this, a call like `builder.setLastLedgerSequence(100).setAsset(issue)` would fail to compile because `setAsset` does not exist on the base. The builder accumulates field assignments into an `STObject object_{sfTransaction}` member and defers constructing the final `STTx` until `build()` is called. + +## Immutability Split + +There is a deliberate separation between reading and writing. `VaultCreate` is constructed only from an existing, validated `STTx const` pointer — it never mutates its contents. `VaultCreateBuilder` is the only construction path for new transactions: it accepts the required `account` and `asset` fields in its primary constructor, sets optional fields through chained setters, calls `sign()` inside `build()`, and then constructs a `VaultCreate` from the resulting signed `STTx`. + +Both classes validate the transaction type at construction. If the incoming `STTx` has a type other than `ttVAULT_CREATE`, both constructors throw `std::runtime_error`. This prevents the wrapper and builder from being misused as generic `STTx` containers. + +## Transaction Fields + +`sfAsset` is the only required field. It is typed as `SF_ISSUE::type::value_type` — an issue specifier that identifies the asset class the vault accepts. The `setAsset()` setter wraps the value in an `STIssue(sfAsset, value)` rather than assigning it directly; this is the only field setter in the class that wraps its argument, reflecting the ledger's treatment of `sfAsset` as a structured `STIssue` rather than a raw scalar. + +The optional fields cover the full configuration surface of a vault: + +- `sfAssetsMaximum` (`SF_NUMBER`) — a cap on the total assets the vault can hold, represented as a 64-bit number. +- `sfMPTokenMetadata` (`SF_VL`) — a variable-length blob that becomes the metadata attached to the MPT issuance created for the vault's share token. This ties directly to the `createMPTIssuance` privilege listed in the class comment. +- `sfDomainID` (`SF_UINT256`) — a 256-bit identifier linking the vault to a permissioned domain, controlling who is allowed to deposit. +- `sfWithdrawalPolicy` (`SF_UINT8`) — an 8-bit enum encoding the vault's withdrawal rules (e.g., first-come-first-served vs. proportional). +- `sfData` (`SF_VL`) — an arbitrary application-level blob, useful for off-chain integrations. +- `sfScale` (`SF_UINT8`) — the decimal precision for the vault share token's MPT issuance, giving the issuer control over fractional representation. + +## The `Optional` Alias + +Each optional field getter returns `protocol_autogen::Optional` rather than bare `std::optional`. The alias in `Utils.h` expands to `std::optional>>` when `T` is a reference type, or plain `std::optional` otherwise. This matters because some XRPL field accessors return references into the underlying `STObject` — wrapping a raw reference in `std::optional` would be undefined behaviour if the value were absent. The alias handles both cases uniformly, so the generated code works correctly regardless of whether the underlying field accessor returns by value or by reference. + +## Privilege Model + +The class comment records the privileges `createPseudoAcct | createMPTIssuance | mustModifyVault`. These are not enforced in this header — enforcement lives in the transaction-processing layer — but the annotations serve as a contract: `VaultCreate` is the sole transaction that may create the pseudo-account and MPT issuance associated with a vault. The `notDelegable` flag means this transaction cannot be submitted by a delegate account on behalf of another account, which is consistent with the privileged nature of vault creation. + +## Relationship to Sibling Vault Transactions + +The `VaultCreate` transaction represents only the creation step. The vault lifecycle continues through `VaultSet` (mutate configuration, type `ttVAULT_SET = 66`), `VaultDeposit`, `VaultWithdraw`, `VaultClawback`, and `VaultDelete`, each following the identical code-generation pattern in adjacent files. All share the same base classes and `Optional` alias. `VaultSet` notably requires `sfVaultID` (the 256-bit identifier assigned at creation) as its required field, illustrating how the two-phase create/mutate model distributes fields across transaction types. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultDelete.h.ai.json b/include/xrpl/protocol_autogen/transactions/VaultDelete.h.ai.json new file mode 100644 index 0000000000..f8abca39fa --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultDelete.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 35, + "name": "tx" + }, + { + "lineno": 75, + "name": "account" + }, + { + "lineno": 76, + "name": "vaultID" + }, + { + "lineno": 77, + "name": "sequence" + }, + { + "lineno": 78, + "name": "fee" + }, + { + "lineno": 86, + "name": "tx" + }, + { + "lineno": 92, + "name": "value" + }, + { + "lineno": 103, + "name": "publicKey" + }, + { + "lineno": 103, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 32, + "name": "VaultDelete" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& vaultID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 67, + "name": "VaultDeleteBuilder" + } + ], + "description": "Defines the VaultDelete transaction type for XRPL, including a type-safe wrapper and a builder class for constructing and signing VaultDelete transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/VaultDelete.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "VaultDelete::getVaultID" + }, + { + "args": [ + "value" + ], + "lineno": 91, + "name": "VaultDeleteBuilder::setVaultID" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 101, + "name": "VaultDeleteBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultDelete.h.ai.md b/include/xrpl/protocol_autogen/transactions/VaultDelete.h.ai.md new file mode 100644 index 0000000000..2700385e04 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultDelete.h.ai.md @@ -0,0 +1,33 @@ +# `VaultDelete.h` — Auto-generated VaultDelete Transaction Type + +## Role and Context + +This header defines the C++ representation of the `VaultDelete` transaction for the XRP Ledger's Single Asset Vault feature, introduced under the `featureSingleAssetVault` amendment. A vault is an on-ledger pool that holds a single asset and issues MPT (Multi-Purpose Token) vault shares to depositors. `VaultDelete` is the teardown operation: it destroys the vault, eliminates the associated MPT issuance that backed the shares, and optionally deletes the vault's pseudo-account — making it the most destructive transaction in the vault lifecycle family alongside `VaultCreate` (type 65), `VaultSet`, `VaultDeposit`, `VaultWithdraw`, and `VaultClawback`. + +The file is machine-generated and must not be edited by hand. Every transaction type in the `protocol_autogen/transactions/` directory follows the same two-class pattern: an immutable read-only wrapper and a corresponding builder. This consistency is enforced structurally rather than by convention, since all wrappers derive from `TransactionBase` and all builders derive from `TransactionBuilderBase`. + +## `VaultDelete` — Immutable Transaction Wrapper + +`VaultDelete` is a thin, read-only view over a `shared_ptr`. It inherits the full suite of common field accessors from `TransactionBase` — account, sequence, fee, flags, memos, signers, delegate, network ID, etc. — and adds exactly one transaction-specific accessor: `getVaultID()`, which returns the `sfVaultID` field as a `uint256` value. + +The minimalism of `VaultDelete` compared to `VaultCreate` (which carries six fields including asset type, withdrawal policy, metadata, and cap) is intentional: deletion only needs to identify *which* vault to remove. The vault ID is declared `soeREQUIRED`, so `getVaultID()` always returns a valid value without an optional wrapper. + +The constructor enforces a runtime type check against the static constant `txType = ttVAULT_DELETE`. This guard is necessary because `STTx` is a generic container: constructing a wrapper around the wrong transaction type would compile cleanly but silently return meaningless values from field accessors. Since these wrappers are often built from deserialized network objects, catching the mismatch at construction time is the earliest safe point. + +The transaction carries the privilege flags `mustDeleteAcct | destroyMPTIssuance | mustModifyVault`, reflecting its side effects on three different ledger object categories. It is marked `Delegation::notDelegable`, meaning it cannot be submitted on behalf of another account via the delegate mechanism exposed in `TransactionBase::getDelegate()`. + +## `VaultDeleteBuilder` — Fluent Builder + +`VaultDeleteBuilder` extends `TransactionBuilderBase` via the Curiously Recurring Template Pattern (CRTP). The base class provides all common field setters (`setAccount`, `setFee`, `setSequence`, `setFlags`, `setMemos`, `setDelegate`, etc.) which return `Derived&` to enable method chaining, while the derived builder adds only `setVaultID()`. + +The primary constructor accepts an account, the vault ID, and optionally a sequence number and fee. The vault ID is taken as `std::decay_t const&`, which strips reference qualifiers from the serialized field's underlying type — a consistent pattern across all generated builders that prevents accidental dangling references to temporary values. + +The secondary constructor accepts an existing `shared_ptr` and copies the transaction body into the builder's internal `STObject object_`. This copy-from-transaction path allows re-inflating a signed transaction into a mutable builder for inspection or re-signing, though it naturally drops the original signature. The type check here mirrors the one in the wrapper constructor. + +`build()` finalizes the transaction in two steps: it calls the inherited `sign()` method, which computes the canonical signing bytes as `HashPrefix::txSign` concatenated with the serialized object (excluding signing fields), then moves the completed `STObject` into an `STTx` via `std::make_shared(std::move(object_))`. The move means the builder's internal state is consumed: calling `build()` twice on the same builder without re-populating fields would produce a broken transaction. + +## Design Tradeoffs + +The split between an immutable wrapper type and a separate builder type is a deliberate separation of concerns. The wrapper's `const`-correctness guarantees that code receiving a `VaultDelete` reference cannot mutate it, which matters when the same `STTx` is shared across multiple processing stages in the ledger engine. Constructing a fresh `STTx` inside `build()` — rather than returning a mutable wrapper — keeps the immutability invariant intact even if the caller retains the builder. + +The `TransactionBuilderBase` deliberately avoids calling `object_.set(soTemplate)` on construction (as noted in its inline comment). Setting the SOTemplate on a free `STObject` would pre-populate all optional fields with their defaults, causing `STTx::applyTemplate()` to throw "may not be explicitly set to default." By leaving the object as a free container and letting `STTx`'s constructor apply the template during finalization, the builder avoids this pitfall without any per-field special-casing. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultDeposit.h.ai.json b/include/xrpl/protocol_autogen/transactions/VaultDeposit.h.ai.json new file mode 100644 index 0000000000..4f531c480a --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultDeposit.h.ai.json @@ -0,0 +1,104 @@ +{ + "args": [ + { + "lineno": 29, + "name": "tx" + }, + { + "lineno": 72, + "name": "account" + }, + { + "lineno": 73, + "name": "vaultID" + }, + { + "lineno": 73, + "name": "amount" + }, + { + "lineno": 74, + "name": "sequence" + }, + { + "lineno": 75, + "name": "fee" + }, + { + "lineno": 109, + "name": "publicKey" + }, + { + "lineno": 109, + "name": "secretKey" + }, + { + "lineno": 89, + "name": "value" + }, + { + "lineno": 99, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "VaultDeposit" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& vaultID, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 62, + "name": "VaultDepositBuilder" + } + ], + "description": "Defines the VaultDeposit transaction type and its builder for the XRPL protocol, providing type-safe field access and a fluent interface for constructing VaultDeposit transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/VaultDeposit.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "VaultDeposit::getVaultID" + }, + { + "args": [], + "lineno": 48, + "name": "VaultDeposit::getAmount" + }, + { + "args": [ + "value" + ], + "lineno": 87, + "name": "VaultDepositBuilder::setVaultID" + }, + { + "args": [ + "value" + ], + "lineno": 97, + "name": "VaultDepositBuilder::setAmount" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 107, + "name": "VaultDepositBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultDeposit.h.ai.md b/include/xrpl/protocol_autogen/transactions/VaultDeposit.h.ai.md new file mode 100644 index 0000000000..ac4ce27a41 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultDeposit.h.ai.md @@ -0,0 +1,41 @@ +# `VaultDeposit.h` — Auto-Generated Vault Deposit Transaction Wrapper + +This file is part of the `protocol_autogen` layer in the XRPL codebase — a set of auto-generated headers that expose every ledger transaction type as a pair of C++ classes: an immutable read wrapper and a mutable builder. `VaultDeposit.h` covers transaction type `ttVAULT_DEPOSIT` (type code 68), which deposits assets into a Single Asset Vault created by the `featureSingleAssetVault` amendment. + +## Role in the Vault Transaction Family + +Five sibling files in the same directory — `VaultCreate.h`, `VaultSet.h`, `VaultDelete.h`, `VaultWithdraw.h`, and `VaultClawback.h` — together form the complete lifecycle surface for on-ledger vaults. A vault is a yield-bearing container for a single asset type; accounts deposit assets and receive share tokens (MPTs) back proportional to the pool, then redeem those shares on withdrawal. `VaultDeposit` handles the ingress side: an account sends assets in and the ledger mints vault share tokens to them. + +The transaction carries only two required fields: `sfVaultID` (a 256-bit hash identifying the target vault ledger object) and `sfAmount` (the quantity being deposited). Compared with `VaultWithdraw`, which adds optional `sfDestination` and `sfDestinationTag` to route redeemed assets elsewhere, `VaultDeposit` has no routing fields — the deposited assets go into the vault and the share tokens are credited back to the sending account. + +The transaction is marked `notDelegable`, meaning it cannot be submitted via an `sfDelegate` account acting on another's behalf, and it carries the `mayAuthorizeMPT | mustModifyVault` privilege flags, which the XRPL transaction processor uses to decide which ledger objects to lock and which capabilities to pre-authorize. + +## `VaultDeposit` — Immutable Read Wrapper + +`VaultDeposit` extends `TransactionBase` and holds a `std::shared_ptr` — the `const` qualifier on the `STTx` being load-bearing. Once a transaction is wrapped, neither the pointer nor the underlying object can be mutated; all getters return by value. This prevents accidental modification after signing and makes the wrapper safe to pass and copy freely in contexts like validation, transaction replay, or fee estimation. + +The constructor receives an already-constructed `STTx` by `shared_ptr`, forwards it to `TransactionBase`, and then validates the transaction type against the compile-time `txType` constant. The validation reads from `tx_` rather than the local `tx` parameter because the `std::move` happens in the base class initialiser — by the time the body executes, the original `tx` pointer is empty and `tx_` holds the value. The `std::runtime_error` thrown on type mismatch is intentionally unchecked at the call site in most usage, because constructing a `VaultDeposit` from anything other than a vault deposit transaction is a programming error. + +`getVaultID()` returns an `SF_UINT256::type::value_type` — the concrete C++ type (`uint256`) that the serialized field system maps to this field kind. `getAmount()` returns an `SF_AMOUNT::type::value_type`, which can carry XRP drops, IOU amounts, or MPT amounts. The note that this field "supports MPT" is significant: vault shares are themselves MPTs, and a deposit can potentially name an MPT as the deposited asset if the vault was created with an MPT asset type. + +## `VaultDepositBuilder` — Fluent Construction + +`VaultDepositBuilder` inherits from `TransactionBuilderBase` via CRTP. The template parameter `Derived = VaultDepositBuilder` allows all setters in the base class to return `Derived&` through a `static_cast`, preserving the concrete type through method chains without virtual dispatch. This means callers can write: + +```cpp +VaultDepositBuilder builder(account, vaultID, amount, seq, fee); +builder.setFlags(tfSomeFlag).setLastLedgerSequence(lls); +auto tx = builder.build(pubKey, secKey); +``` + +and each call in the chain remains typed as `VaultDepositBuilder&`, not the base. + +The constructor deliberately avoids calling `object_.set(soTemplate)` on the internal `STObject`. As the base class comment explains, doing so would pre-populate default-value placeholders for `soeDEFAULT` fields, which then causes `applyTemplate()` inside the `STTx` constructor to throw "may not be explicitly set to default." By keeping `object_` as a free `STObject` and letting `STTx` apply the template itself at build time, optional fields that weren't set are handled cleanly. + +The alternative constructor `VaultDepositBuilder(std::shared_ptr tx)` round-trips an existing transaction back into mutable form by copying the `STTx` content into `object_`. This is useful for test fixtures or mutation-based testing patterns where a valid signed transaction needs to be tweaked and re-signed. + +`build()` calls the protected `sign()` method (which serialises the object under `HashPrefix::txSign`, signs it, and embeds the signature and public key), then wraps the resulting `STTx` in a `VaultDeposit`. Calling `build()` consumes the internal `STObject` via `std::move`, so the builder is in a valid-but-unspecified state afterwards and should not be reused. + +## Auto-Generation Notes + +The `// This file is auto-generated. Do not edit.` header signals that the source of truth is a schema or code-generation script, not this file. All Vault* transaction headers follow an identical structural pattern, and differences between them are purely in their field lists. This design ensures that adding or removing fields from a transaction type requires only updating the generator input, not manually maintaining parallel C++ class bodies. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultSet.h.ai.json b/include/xrpl/protocol_autogen/transactions/VaultSet.h.ai.json new file mode 100644 index 0000000000..6f282c5169 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultSet.h.ai.json @@ -0,0 +1,168 @@ +{ + "args": [ + { + "lineno": 36, + "name": "tx" + }, + { + "lineno": 125, + "name": "account" + }, + { + "lineno": 126, + "name": "vaultID" + }, + { + "lineno": 127, + "name": "sequence" + }, + { + "lineno": 128, + "name": "fee" + }, + { + "lineno": 135, + "name": "tx" + }, + { + "lineno": 146, + "name": "value" + }, + { + "lineno": 155, + "name": "value" + }, + { + "lineno": 164, + "name": "value" + }, + { + "lineno": 173, + "name": "value" + }, + { + "lineno": 182, + "name": "publicKey" + }, + { + "lineno": 182, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 32, + "name": "VaultSet" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& vaultID, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 115, + "name": "VaultSetBuilder" + } + ], + "description": "Defines the VaultSet transaction type for XRPL, including a type-safe wrapper and a builder class for constructing and signing VaultSet transactions with specific fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/VaultSet.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "VaultSet::getVaultID" + }, + { + "args": [], + "lineno": 54, + "name": "VaultSet::getAssetsMaximum" + }, + { + "args": [], + "lineno": 65, + "name": "VaultSet::hasAssetsMaximum" + }, + { + "args": [], + "lineno": 75, + "name": "VaultSet::getDomainID" + }, + { + "args": [], + "lineno": 86, + "name": "VaultSet::hasDomainID" + }, + { + "args": [], + "lineno": 96, + "name": "VaultSet::getData" + }, + { + "args": [], + "lineno": 107, + "name": "VaultSet::hasData" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& vaultID", + "std::optional sequence", + "std::optional fee" + ], + "lineno": 124, + "name": "VaultSetBuilder::VaultSetBuilder" + }, + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 134, + "name": "VaultSetBuilder::VaultSetBuilder" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 144, + "name": "VaultSetBuilder::setVaultID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 153, + "name": "VaultSetBuilder::setAssetsMaximum" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 162, + "name": "VaultSetBuilder::setDomainID" + }, + { + "args": [ + "std::decay_t const& value" + ], + "lineno": 171, + "name": "VaultSetBuilder::setData" + }, + { + "args": [ + "PublicKey const& publicKey", + "SecretKey const& secretKey" + ], + "lineno": 180, + "name": "VaultSetBuilder::build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultSet.h.ai.md b/include/xrpl/protocol_autogen/transactions/VaultSet.h.ai.md new file mode 100644 index 0000000000..12fe0fae91 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultSet.h.ai.md @@ -0,0 +1,48 @@ +# `VaultSet.h` — Auto-Generated VaultSet Transaction Wrapper + +## Role in the System + +This file defines the `VaultSet` transaction for the XRPL Single-Asset Vault feature (`featureSingleAssetVault`). A vault on XRPL is an on-ledger yield-bearing pool that accepts a single asset type; `VaultSet` is the mutation transaction — it modifies properties of an already-existing vault without touching its asset holdings or ownership structure. The transaction type code is `ttVAULT_SET` (66). + +The file is part of the `protocol_autogen` layer, a family of auto-generated headers that wrap XRPL's untyped `STTx` serialization objects behind strongly-typed, field-specific C++ APIs. Every vault lifecycle transaction (`VaultCreate`, `VaultDeposit`, `VaultWithdraw`, `VaultDelete`, `VaultClawback`) has a corresponding sibling file generated alongside this one. **The file must not be edited by hand** — the `// This file is auto-generated. Do not edit.` guard at the top signals that the source of truth lives in a separate generation template. + +## Two-Class Pattern: Reader and Builder + +The file follows the same two-class pattern shared across the entire `protocol_autogen` transaction family: + +**`VaultSet`** is the read-only wrapper. It extends `TransactionBase` (which holds the `std::shared_ptr tx_` and provides common field accessors like `getAccount()`, `getFee()`, `getSequence()`). `VaultSet` adds transaction-specific accessors on top. Its constructor accepts a `shared_ptr` and immediately validates the embedded type tag — if the caller passes the wrong transaction type it throws `std::runtime_error` rather than silently presenting corrupted field reads later. Because `STTx const` is stored by `const` pointer, the wrapper is inherently immutable after construction. + +**`VaultSetBuilder`** is the mutable construction half. It inherits from `TransactionBuilderBase` using CRTP so that all common setter methods (`setFee()`, `setSequence()`, `setLastLedgerSequence()`, `setDelegate()`, etc.) return `VaultSetBuilder&` rather than a base reference, enabling clean method chaining without casts at call sites. The builder stores a live `STObject object_{sfTransaction}` (from the base class) and accumulates fields into it until `build()` is called. + +The deliberate separation between reader and builder enforces a compile-time invariant: once a transaction is signed and wrapped in `VaultSet`, it cannot be mutated. Code that processes incoming transactions receives `VaultSet` and never inadvertently writes through it. + +## Fields and Semantics + +`VaultSet` carries one required field and three optional ones: + +- **`sfVaultID`** (`SF_UINT256`, required) — the 256-bit identifier of the vault ledger object to modify. Because this is `soeREQUIRED`, it is injected by the builder's constructor: `VaultSetBuilder(account, vaultID, ...)` calls `setVaultID(vaultID)` immediately, so a builder can never exist in a state where `VaultID` is absent. + +- **`sfAssetsMaximum`** (`SF_NUMBER`, optional) — a cap on the total assets the vault may hold. When omitted the vault is uncapped. Using `SF_NUMBER` (rather than `SF_AMOUNT`) allows the limit to be expressed as a raw numeric quantity independent of currency representation. + +- **`sfDomainID`** (`SF_UINT256`, optional) — references a permissioned domain object, restricting who may interact with the vault. Setting or clearing this on an existing vault changes its access policy without redeploying the vault itself. + +- **`sfData`** (`SF_VL`, optional) — an arbitrary variable-length blob for off-chain metadata. The ledger stores it verbatim; interpretation is left to applications. + +For all three optional fields the pattern is a paired `has*()` / `get*()` on `VaultSet`. The getter returns `protocol_autogen::Optional` (a thin alias for `std::optional`) and guards behind `has*()` to avoid `STObject::at()` throwing on a missing field. This mirrors the XRPL ledger's `soeOPTIONAL` schema semantics in a way that the C++ type system can check statically. + +## Access Constraints + +The transaction comment block records two important policy annotations: + +- `Delegation::notDelegable` — unlike some XRPL transactions, `VaultSet` cannot be submitted by a delegate account on behalf of the vault owner. Only the vault's controlling account may sign this transaction. +- Privilege `mustModifyVault` — the transaction processing layer enforces that the submitting account is the vault's owner before applying the modification. + +## Build and Sign Flow + +`VaultSetBuilder::build(publicKey, secretKey)` calls the inherited `sign()` from `TransactionBuilderBase`, which serializes the `STObject` with `HashPrefix::txSign` prepended, computes the cryptographic signature, and writes `sfSigningPubKey` and `sfTxnSignature` back into `object_`. It then constructs a `std::shared_ptr` from the now-complete `STObject` (via `STTx(std::move(object_))`), wraps it in `VaultSet`, and returns by value. The `STTx` constructor calls `applyTemplate()` internally — which is precisely why `TransactionBuilderBase` avoids pre-initialising `object_` with the `soTemplate`; doing so would cause `applyTemplate()` to throw when it encounters `soeDEFAULT` placeholders that were already explicitly set. + +A second builder constructor accepts an existing `shared_ptr` and copies the underlying object into `object_` via `object_ = *tx`. This path exists to re-open a signed transaction for modification, useful in testing or transaction relay scenarios where fields need adjustment before re-signing. + +## Relationship to Sibling Transactions + +`VaultCreate` (type 65, one below) is the creation counterpart. It requires `sfAsset` to specify what the vault holds and supports additional creation-time-only fields like `sfWithdrawalPolicy` and `sfScale`. `VaultSet` deliberately has no `sfAsset` field — the asset type of an existing vault is immutable by design. The subset of mutable vault properties (`sfAssetsMaximum`, `sfDomainID`, `sfData`) maps exactly to what `VaultSet` exposes. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h.ai.json b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h.ai.json new file mode 100644 index 0000000000..dc93cb69c0 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h.ai.json @@ -0,0 +1,134 @@ +{ + "args": [ + { + "lineno": 32, + "name": "tx" + }, + { + "lineno": 113, + "name": "account" + }, + { + "lineno": 114, + "name": "vaultID" + }, + { + "lineno": 115, + "name": "amount" + }, + { + "lineno": 116, + "name": "sequence" + }, + { + "lineno": 117, + "name": "fee" + }, + { + "lineno": 164, + "name": "publicKey" + }, + { + "lineno": 164, + "name": "secretKey" + }, + { + "lineno": 128, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "VaultWithdraw" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& vaultID, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 109, + "name": "VaultWithdrawBuilder" + } + ], + "description": "Defines the VaultWithdraw transaction type for the XRPL protocol, providing a type-safe wrapper and a builder class for constructing and signing VaultWithdraw transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getVaultID" + }, + { + "args": [], + "lineno": 54, + "name": "getAmount" + }, + { + "args": [], + "lineno": 64, + "name": "getDestination" + }, + { + "args": [], + "lineno": 75, + "name": "hasDestination" + }, + { + "args": [], + "lineno": 85, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 96, + "name": "hasDestinationTag" + }, + { + "args": [ + "value" + ], + "lineno": 126, + "name": "setVaultID" + }, + { + "args": [ + "value" + ], + "lineno": 135, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 144, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 153, + "name": "setDestinationTag" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 162, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h.ai.md b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h.ai.md new file mode 100644 index 0000000000..702617608f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/VaultWithdraw.h.ai.md @@ -0,0 +1,27 @@ +# `VaultWithdraw.h` — Auto-Generated VaultWithdraw Transaction Wrapper + +This file is machine-generated (the `// This file is auto-generated. Do not edit.` header is definitive) and lives in `include/xrpl/protocol_autogen/transactions/`. It defines two closely related classes — `VaultWithdraw` and `VaultWithdrawBuilder` — that together implement the full lifecycle of a `ttVAULT_WITHDRAW` (type 69) transaction for the XRPL Single-Asset Vault feature (`featureSingleAssetVault`). Its role is to translate the loosely-typed, schema-driven `STTx` layer into compile-time-checked C++ types, shielding application code from raw field manipulation. + +## Context: the Single-Asset Vault system + +`VaultWithdraw` is one of six vault transaction types (`VaultCreate`, `VaultDeposit`, `VaultWithdraw`, `VaultSet`, `VaultDelete`, `VaultClawback`), all sharing the same generated structure. A vault in this sense is an on-ledger pooled-asset construct; depositors put assets in (via `VaultDeposit`, type 68) and withdraw them (via `VaultWithdraw`, type 69). The amount field deliberately supports MPT (Multi-Purpose Token) quantities in addition to classic XRP/IOU `STAmount`, which is why getters and setters annotate it with `@note This field supports MPT amounts`. + +Compared to `VaultDeposit`, which only exposes `sfVaultID` and `sfAmount`, `VaultWithdraw` adds two optional fields — `sfDestination` and `sfDestinationTag` — allowing the withdrawal proceeds to be sent to a *different* account than the transaction sender. This mirrors the Payment transaction pattern and is the key structural difference between the deposit and withdrawal sides of the vault. + +The transaction carries the privilege flags `mayDeleteMPT | mayAuthorizeMPT | mustModifyVault` and is marked `Delegation::notDelegable`, meaning a delegated account cannot submit this on behalf of the vault owner. + +## Two-class design: wrapper + builder + +The split between an immutable read wrapper and a mutable builder is intentional and consistent across all generated transaction types. `VaultWithdraw` itself is constructed only from an existing `std::shared_ptr` — the `const`-qualified `STTx` makes mutation impossible after wrapping. The constructor immediately verifies the transaction type tag and throws `std::runtime_error` on mismatch, preventing a caller from accidentally treating a `VaultDeposit` as a `VaultWithdraw`. All getter methods are `[[nodiscard]]` to catch ignored return values at compile time. + +`VaultWithdrawBuilder` extends the CRTP base `TransactionBuilderBase`, which supplies setters for every universal transaction field (`sfAccount`, `sfFee`, `sfSequence`, `sfMemos`, `sfSigners`, etc.) while returning `Derived&` to keep the fluent chain typed correctly. The builder holds a mutable `STObject object_` rather than an `STTx`, deliberately avoiding `set(soTemplate)` on it — the comment in `TransactionBuilderBase` explains that calling `applyTemplate()` too early would create `STBase` placeholders for `soeDEFAULT` fields, which then cause `applyTemplate()` to throw "may not be explicitly set to default" when the `STTx` is finally constructed. Only the terminal `build()` call promotes the `STObject` into an `STTx`. + +The builder provides two constructors: the primary one takes the required fields (`account`, `vaultID`, `amount`) and optional `sequence`/`fee`, wiring them through the CRTP base and the vault-specific setters. The second constructor accepts an existing `std::shared_ptr` for round-trip editing — it copies the raw `STTx` back into `object_` via `object_ = *tx`, again guarded by a type-check throw. This round-trip path exists so that a transaction already present in the ledger can be re-signed or modified without starting from scratch. + +## Field handling pattern for optional fields + +`getDestination()` and `getDestinationTag()` follow a paired `hasX()` / `getX()` pattern that wraps the optional check explicitly rather than using `STTx::at()` with a default. Each getter returns `protocol_autogen::Optional` (a thin alias for `std::optional`), and the `has*()` helpers delegate to `STTx::isFieldPresent()`. Required fields (`sfVaultID`, `sfAmount`) skip the presence check entirely and call `tx_->at()` directly, which will throw on a missing field — a deliberate fail-hard choice since a required field's absence indicates a malformed transaction that already should have been rejected by schema validation in `TransactionBase::validate()`. + +## Build and sign terminal operation + +`VaultWithdrawBuilder::build(PublicKey, SecretKey)` is the only exit point for creating a signed transaction. It calls the protected `sign()` method from `TransactionBuilderBase`, which serialises the `STObject` with `HashPrefix::txSign` prepended and without signing fields (per XRPL's canonical signing scheme), then appends the resulting signature as `sfTxnSignature`. The fully signed `STObject` is moved into a fresh `STTx`, which is then wrapped in a `VaultWithdraw` and returned by value. After `build()` returns, the builder's internal `object_` has been moved-from, so the builder should not be reused. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h.ai.json b/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h.ai.json new file mode 100644 index 0000000000..7035d7ef5f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h.ai.json @@ -0,0 +1,132 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 89, + "name": "account" + }, + { + "lineno": 90, + "name": "xChainBridge" + }, + { + "lineno": 91, + "name": "destination" + }, + { + "lineno": 92, + "name": "amount" + }, + { + "lineno": 93, + "name": "signatureReward" + }, + { + "lineno": 94, + "name": "sequence" + }, + { + "lineno": 95, + "name": "fee" + }, + { + "lineno": 146, + "name": "publicKey" + }, + { + "lineno": 146, + "name": "secretKey" + }, + { + "lineno": 106, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 23, + "name": "XChainAccountCreateCommit" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& xChainBridge, std::decay_t const& destination, std::decay_t const& amount, std::decay_t const& signatureReward, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 83, + "name": "XChainAccountCreateCommitBuilder" + } + ], + "description": "Defines the XChainAccountCreateCommit transaction type for XRPL, including its immutable wrapper, builder class, and type-safe field accessors and mutators.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 48, + "name": "getDestination" + }, + { + "args": [], + "lineno": 58, + "name": "getAmount" + }, + { + "args": [], + "lineno": 68, + "name": "getSignatureReward" + }, + { + "args": [ + "value" + ], + "lineno": 104, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 114, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 124, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 134, + "name": "setSignatureReward" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 144, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h.ai.md b/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h.ai.md new file mode 100644 index 0000000000..9f50c77b8a --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainAccountCreateCommit.h.ai.md @@ -0,0 +1,38 @@ +# `XChainAccountCreateCommit.h` — Cross-Chain Account Creation Transaction + +## Purpose and Protocol Context + +This file is part of the `protocol_autogen` layer — a collection of auto-generated, type-safe C++ wrappers over XRPL's raw `STTx` transaction objects. It defines the `XChainAccountCreateCommit` transaction (`ttXCHAIN_ACCOUNT_CREATE_COMMIT`, type 44), which is gated behind the `featureXChainBridge` amendment. + +Within the XChain bridge protocol, this transaction is the first step in creating a new account on the destination chain. A user on the locking chain submits this transaction to commit funds; the bridge witnesses observe the event, attest to it (via `XChainAddAccountCreateAttestation`), and once a quorum is reached, the new account is funded on the issuing chain. The four required domain fields reflect this role exactly: + +- **`sfXChainBridge`** — identifies the specific bridge (locking-chain account, issuing-chain account, and asset pair) this commit targets. +- **`sfDestination`** — the account address to create on the destination chain. +- **`sfAmount`** — the funds being committed; this amount seeds the new account's balance after bridge processing. +- **`sfSignatureReward`** — a separate amount that is distributed to the witness servers that attest to this account creation on the destination chain, incentivizing the off-chain infrastructure. + +## Two-Class Design: Immutable Wrapper + Fluent Builder + +The file follows the same pattern used across all auto-generated transaction types in this directory: one read-only wrapper class and one builder class. This separation is architecturally deliberate. + +`XChainAccountCreateCommit` inherits from `TransactionBase`, which holds a `std::shared_ptr` — the `const` qualifier is key. Once constructed, the underlying transaction bytes are immutable. All four domain getters are marked `[[nodiscard]]` and return typed values directly from the `STTx` via `tx_->at(sfField)`, using the `SF_*` type system to return the correct C++ type for each XRPL field (e.g., `SF_ACCOUNT::type::value_type` resolves to `AccountID`, `SF_AMOUNT::type::value_type` to `STAmount`). This avoids stringly-typed field access while staying close to the underlying serialization layer. + +The constructor accepts an existing `std::shared_ptr` — this is the deserialization path. A type guard checks `tx_->getTxnType() != txType` and throws `std::runtime_error` on mismatch. Because the base class constructor runs before the check (consuming the `std::move`), the guard reads from `tx_` (the stored member in `TransactionBase`) rather than the moved-from local. The design is correct: the invariant that a wrapper only ever holds the matching transaction type is enforced at construction time. + +`XChainAccountCreateCommitBuilder` inherits from `TransactionBuilderBase` using CRTP, which gives it all common field setters (`setAccount`, `setFee`, `setSequence`, `setLastLedgerSequence`, `setMemos`, etc.) with fluent chaining that returns `Derived&` — the concrete builder type — rather than a base reference, so callers never lose type information mid-chain. The domain-specific setters (`setXChainBridge`, `setDestination`, `setAmount`, `setSignatureReward`) follow the same pattern and accept parameters as `std::decay_t const&`, stripping any reference or qualifier from the field's value type to ensure clean value semantics when assigning into the mutable `STObject`. + +Internally, `TransactionBuilderBase` stores an `STObject object_{sfTransaction}` — a mutable bag of serialized type fields. Crucially, the base class comment explains why `object_.set(soTemplate)` is deliberately not called: setting a template would create placeholder entries for `soeDEFAULT` fields, which would cause `STTx::applyTemplate()` (called during construction of the final `STTx`) to throw "may not be explicitly set to default." The builder keeps `object_` as a free, template-less `STObject`, relying on the `STTx` constructor to enforce the schema. + +## Construction and Signing Flow + +The `build(PublicKey, SecretKey)` method finalizes the transaction: + +1. Calls `sign()` from `TransactionBuilderBase`, which sets `sfSigningPubKey`, serializes the object prefixed with `HashPrefix::txSign` (excluding signing fields), computes the cryptographic signature, and stores it in `sfTxnSignature`. +2. Constructs a `std::shared_ptr` by moving the builder's `object_` into the `STTx` constructor — this triggers schema validation via `applyTemplate()`. +3. Wraps the result in an `XChainAccountCreateCommit` and returns it by value. + +The alternative builder constructor — `XChainAccountCreateCommitBuilder(std::shared_ptr tx)` — supports round-tripping an existing transaction back into a builder for modification. It copies the `STTx` into `object_` (`object_ = *tx`) after the same type guard check. This is less common but useful in testing or transaction amendment scenarios. + +## Relationship to Sibling Files + +The `transactions/` directory contains one header per XRPL transaction type, all generated by the same tool and following identical structural patterns. The XChain-related group — `XChainCreateBridge.h`, `XChainCommit.h`, `XChainClaim.h`, `XChainAddAccountCreateAttestation.h`, and this file — collectively cover the full lifecycle of the cross-chain bridge protocol. `XChainAccountCreateCommit.h` specifically handles the initiating user-side action in the account-creation flow, distinct from `XChainCommit.h` which handles asset transfers for existing accounts. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h.ai.json b/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h.ai.json new file mode 100644 index 0000000000..cbf19c84e3 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h.ai.json @@ -0,0 +1,240 @@ +{ + "args": [ + { + "lineno": 32, + "name": "tx" + }, + { + "lineno": 159, + "name": "account" + }, + { + "lineno": 160, + "name": "xChainBridge" + }, + { + "lineno": 161, + "name": "attestationSignerAccount" + }, + { + "lineno": 162, + "name": "publicKey" + }, + { + "lineno": 163, + "name": "signature" + }, + { + "lineno": 164, + "name": "otherChainSource" + }, + { + "lineno": 165, + "name": "amount" + }, + { + "lineno": 166, + "name": "attestationRewardAccount" + }, + { + "lineno": 167, + "name": "wasLockingChainSend" + }, + { + "lineno": 168, + "name": "xChainAccountCreateCount" + }, + { + "lineno": 169, + "name": "destination" + }, + { + "lineno": 170, + "name": "signatureReward" + }, + { + "lineno": 171, + "name": "sequence" + }, + { + "lineno": 172, + "name": "fee" + }, + { + "lineno": 303, + "name": "publicKey" + }, + { + "lineno": 303, + "name": "secretKey" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 29, + "name": "XChainAddAccountCreateAttestation" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& xChainBridge, std::decay_t const& attestationSignerAccount, std::decay_t const& publicKey, std::decay_t const& signature, std::decay_t const& otherChainSource, std::decay_t const& amount, std::decay_t const& attestationRewardAccount, std::decay_t const& wasLockingChainSend, std::decay_t const& xChainAccountCreateCount, std::decay_t const& destination, std::decay_t const& signatureReward, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 157, + "name": "XChainAddAccountCreateAttestationBuilder" + } + ], + "description": "Defines the XChainAddAccountCreateAttestation transaction type for XRPL, including its builder class, field accessors, and construction logic. Provides type-safe access to transaction fields and a fluent builder interface for constructing and signing transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 54, + "name": "getAttestationSignerAccount" + }, + { + "args": [], + "lineno": 64, + "name": "getPublicKey" + }, + { + "args": [], + "lineno": 74, + "name": "getSignature" + }, + { + "args": [], + "lineno": 84, + "name": "getOtherChainSource" + }, + { + "args": [], + "lineno": 94, + "name": "getAmount" + }, + { + "args": [], + "lineno": 104, + "name": "getAttestationRewardAccount" + }, + { + "args": [], + "lineno": 114, + "name": "getWasLockingChainSend" + }, + { + "args": [], + "lineno": 124, + "name": "getXChainAccountCreateCount" + }, + { + "args": [], + "lineno": 134, + "name": "getDestination" + }, + { + "args": [], + "lineno": 144, + "name": "getSignatureReward" + }, + { + "args": [ + "value" + ], + "lineno": 191, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 201, + "name": "setAttestationSignerAccount" + }, + { + "args": [ + "value" + ], + "lineno": 211, + "name": "setPublicKey" + }, + { + "args": [ + "value" + ], + "lineno": 221, + "name": "setSignature" + }, + { + "args": [ + "value" + ], + "lineno": 231, + "name": "setOtherChainSource" + }, + { + "args": [ + "value" + ], + "lineno": 241, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 251, + "name": "setAttestationRewardAccount" + }, + { + "args": [ + "value" + ], + "lineno": 261, + "name": "setWasLockingChainSend" + }, + { + "args": [ + "value" + ], + "lineno": 271, + "name": "setXChainAccountCreateCount" + }, + { + "args": [ + "value" + ], + "lineno": 281, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 291, + "name": "setSignatureReward" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 301, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h.ai.md b/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h.ai.md new file mode 100644 index 0000000000..c0f32a5b06 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainAddAccountCreateAttestation.h.ai.md @@ -0,0 +1,37 @@ +# `XChainAddAccountCreateAttestation.h` + +This auto-generated file defines the `XChainAddAccountCreateAttestation` transaction type (`ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION`, opcode 46), one of eight cross-chain bridge transaction types gated behind the `featureXChainBridge` amendment. Its specific purpose is to allow a trusted witness server to submit a cryptographic attestation that an account-creation event occurred on a counterpart chain, enabling the XRPL bridge to collectively authorize and fund a new account on the destination chain once a quorum of such attestations is received. + +## Role in the Cross-Chain Bridge Protocol + +The XChain bridge operates through a quorum model: a set of off-chain witness servers independently observe events on a source chain and each submit an attestation transaction to the destination chain. For account creation events — distinct from ordinary value transfers handled by `XChainAddClaimAttestation` — witnesses submit this transaction type. The distinction matters because creating a new account requires the `createAcct` privilege and involves a monotonically ordered counter (`sfXChainAccountCreateCount`) to prevent replay attacks and enforce processing order across independently submitted attestations. + +Each attestation carries the witness's own cryptographic identity (`sfPublicKey`, `sfSignature`) alongside the event details: `sfOtherChainSource` (originating account on the remote chain), `sfAmount` (funds being transferred), `sfDestination` (the new account to create on XRPL), and `sfWasLockingChainSend` (a `uint8` flag distinguishing which side of the bridge — locking or issuing — initiated the send). The `sfXChainBridge` field identifies which bridge configuration governs this attestation. + +The separation between `sfAttestationSignerAccount` and `sfAttestationRewardAccount` is a deliberate design choice: the signer account identifies the witness cryptographically, while the reward account is where the protocol pays the `sfSignatureReward` once quorum is reached. These can be different addresses, so witness operators can segregate their signing key from their compensation-receiving account. + +## Two-Class Design: Immutable Wrapper and CRTP Builder + +The file follows the uniform pattern across all transaction types in this directory, pairing an immutable read accessor class with a mutable builder. + +`XChainAddAccountCreateAttestation` inherits from `TransactionBase` and wraps a `shared_ptr`. Its constructor performs a strict transaction-type check, throwing `std::runtime_error` if the wrapped `STTx` is not `ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION`. This fail-fast guard is essential because `STTx` is a generic container and there is no compile-time mechanism to enforce transaction type — without the runtime check, a wrapper could silently misinterpret fields from a different transaction kind. All eleven field accessors are `[[nodiscard]]` and delegate to `tx_->at(sfField)`, which throws if the field is absent; this is sound because the ledger validates required-field presence before these wrappers would normally be instantiated over ingested transactions. + +`XChainAddAccountCreateAttestationBuilder` inherits from `TransactionBuilderBase` using CRTP. The base class template uses `static_cast(*this)` in its setters to return the correct derived type for method chaining without virtual dispatch overhead. The builder accumulates fields into a mutable `STObject object_` which is later promoted to a signed `STTx` on `build()`. + +## Constructor Completeness Guarantee + +The builder's primary constructor requires all eleven transaction-specific fields as non-optional parameters, alongside the submitting `account`, and optional `sequence` and `fee`. Every required field is set in the constructor body before the object is usable. This means a partially-constructed `XChainAddAccountCreateAttestationBuilder` cannot exist in a state where required fields are missing — the only intermediate state is an unsent `STObject` in the builder, not an invalid one. Optional `sequence` and `fee` cover the common cases where callers rely on ledger autofill for sequence management. + +The secondary constructor taking `shared_ptr` enables a roundtrip workflow: an existing serialized transaction can be deserialized back into a builder for re-signing or inspection, with the same type guard applied. + +## The `build()` Method and Move Semantics + +`build(publicKey, secretKey)` invokes `sign()` inherited from `TransactionBuilderBase`, which serializes the `STObject` with `HashPrefix::txSign` prepended, signs the bytes, and sets `sfSigningPubKey` and `sfTxnSignature` in the object. It then move-constructs an `STTx` from the now-owned `object_`, wraps it in a `shared_ptr`, and returns an `XChainAddAccountCreateAttestation` wrapper. The move is intentional: after `build()` the builder's internal state is consumed, making it structurally clear that `build()` is a terminal operation. + +## Use of `std::decay_t` in Setter Signatures + +All setter parameters are declared as `std::decay_t const&`. The `std::decay_t` strips any reference and cv-qualifiers from the field's underlying value type before applying `const&`. This is a defensive pattern: if a field descriptor's `value_type` were itself a reference type, naively adding `const&` could create a reference-to-reference or an unexpected qualified type. Decaying first guarantees the parameter is always a plain const lvalue reference regardless of how the underlying field type is specified. + +## Integration with the Auto-Generated Transaction Layer + +This file is entirely auto-generated and coexists with 70+ sibling transaction headers in the same directory. Its structural uniformity with files like `XChainAddClaimAttestation.h`, `XChainAccountCreateCommit.h`, and `XChainCreateBridge.h` is deliberate: the entire `xrpl::transactions` namespace provides a consistent, type-safe API surface over raw `STTx` objects without requiring callers to know field names or types at the point of use. The generation approach eliminates per-transaction boilerplate inconsistencies that would otherwise arise from manual authorship of dozens of similar wrappers. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h.ai.json b/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h.ai.json new file mode 100644 index 0000000000..e54ca9c5c9 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h.ai.json @@ -0,0 +1,239 @@ +{ + "args": [ + { + "lineno": 22, + "name": "tx" + }, + { + "lineno": 146, + "name": "account" + }, + { + "lineno": 147, + "name": "xChainBridge" + }, + { + "lineno": 148, + "name": "attestationSignerAccount" + }, + { + "lineno": 149, + "name": "publicKey" + }, + { + "lineno": 150, + "name": "signature" + }, + { + "lineno": 151, + "name": "otherChainSource" + }, + { + "lineno": 152, + "name": "amount" + }, + { + "lineno": 153, + "name": "attestationRewardAccount" + }, + { + "lineno": 154, + "name": "wasLockingChainSend" + }, + { + "lineno": 155, + "name": "xChainClaimID" + }, + { + "lineno": 156, + "name": "sequence" + }, + { + "lineno": 157, + "name": "fee" + }, + { + "lineno": 262, + "name": "publicKey" + }, + { + "lineno": 262, + "name": "secretKey" + }, + { + "lineno": 172, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 18, + "name": "XChainAddClaimAttestation" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account", + "std::decay_t const& xChainBridge", + "std::decay_t const& attestationSignerAccount", + "std::decay_t const& publicKey", + "std::decay_t const& signature", + "std::decay_t const& otherChainSource", + "std::decay_t const& amount", + "std::decay_t const& attestationRewardAccount", + "std::decay_t const& wasLockingChainSend", + "std::decay_t const& xChainClaimID", + "std::optional sequence = std::nullopt", + "std::optional fee = std::nullopt" + ], + "lineno": 143, + "name": "XChainAddClaimAttestationBuilder" + } + ], + "description": "Defines the XChainAddClaimAttestation transaction type for XRPL, including its immutable wrapper, field accessors, and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 47, + "name": "getAttestationSignerAccount" + }, + { + "args": [], + "lineno": 56, + "name": "getPublicKey" + }, + { + "args": [], + "lineno": 65, + "name": "getSignature" + }, + { + "args": [], + "lineno": 74, + "name": "getOtherChainSource" + }, + { + "args": [], + "lineno": 83, + "name": "getAmount" + }, + { + "args": [], + "lineno": 92, + "name": "getAttestationRewardAccount" + }, + { + "args": [], + "lineno": 101, + "name": "getWasLockingChainSend" + }, + { + "args": [], + "lineno": 110, + "name": "getXChainClaimID" + }, + { + "args": [], + "lineno": 119, + "name": "getDestination" + }, + { + "args": [], + "lineno": 130, + "name": "hasDestination" + }, + { + "args": [ + "value" + ], + "lineno": 170, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 179, + "name": "setAttestationSignerAccount" + }, + { + "args": [ + "value" + ], + "lineno": 188, + "name": "setPublicKey" + }, + { + "args": [ + "value" + ], + "lineno": 197, + "name": "setSignature" + }, + { + "args": [ + "value" + ], + "lineno": 206, + "name": "setOtherChainSource" + }, + { + "args": [ + "value" + ], + "lineno": 215, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 224, + "name": "setAttestationRewardAccount" + }, + { + "args": [ + "value" + ], + "lineno": 233, + "name": "setWasLockingChainSend" + }, + { + "args": [ + "value" + ], + "lineno": 242, + "name": "setXChainClaimID" + }, + { + "args": [ + "value" + ], + "lineno": 251, + "name": "setDestination" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 260, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h.ai.md b/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h.ai.md new file mode 100644 index 0000000000..3212a2357f --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainAddClaimAttestation.h.ai.md @@ -0,0 +1,67 @@ +# `XChainAddClaimAttestation.h` — Cross-Chain Transfer Attestation Transaction + +This file is auto-generated (marked with `// This file is auto-generated. Do not edit.`) and lives inside the `xrpl/protocol_autogen/transactions/` collection, which provides one header per XRPL transaction type. It implements transaction type `ttXCHAIN_ADD_CLAIM_ATTESTATION` (ordinal 45), introduced under the `featureXChainBridge` amendment. Its purpose is to model the message that a witness server submits to the XRPL to attest that a `XChainCommit` occurred on the paired chain. + +## Protocol Context + +The XChain bridge protocol requires a set of trusted external observers — called witness servers — to monitor both chains and relay proof of transfers between them. The workflow for a regular cross-chain value transfer proceeds as: + +1. A user submits `XChainCreateClaimID` on the destination chain to reserve a claim slot. +2. The user submits `XChainCommit` on the source chain, locking or burning assets. +3. Each witness server that sees the commit submits `XChainAddClaimAttestation` on the destination chain. +4. Once a quorum of attestations accumulates, the user (or the last attesting witness, if the destination was pre-specified) can trigger `XChainClaim` to release the funds. + +`XChainAddClaimAttestation` is thus the mechanism by which decentralized off-chain evidence is anchored on-chain. Each submission carries a cryptographic signature from the witness over the claim details, letting the ledger independently verify each attestor's commitment without trusting any central coordinator. + +## The Immutable Wrapper: `XChainAddClaimAttestation` + +`XChainAddClaimAttestation` inherits from `TransactionBase`, which wraps a `std::shared_ptr`. The wrapper is intentionally read-only — all fields are exposed through `[[nodiscard]]` const getters, and `tx_` is a `const` smart pointer. This prevents accidental mutation of a transaction after it has been submitted or retrieved from the ledger. + +The constructor validates the transaction type at runtime: + +```cpp +if (tx_->getTxnType() != txType) + throw std::runtime_error("Invalid transaction type for XChainAddClaimAttestation"); +``` + +The static `txType` constexpr member allows compile-time dispatch in template code (e.g. transaction visitors), while the runtime check guards against wrapping a wrong `STTx` object — a defensive pattern used uniformly across all autogen transaction types. + +### Field Access + +Nine fields are marked `soeREQUIRED` and return their native value types directly via `tx_->at(sf...)`: + +| Getter | Field | Type | +|---|---|---| +| `getXChainBridge()` | `sfXChainBridge` | `SF_XCHAIN_BRIDGE::type::value_type` | +| `getAttestationSignerAccount()` | `sfAttestationSignerAccount` | `AccountID` | +| `getPublicKey()` | `sfPublicKey` | `Blob` (VL) | +| `getSignature()` | `sfSignature` | `Blob` (VL) | +| `getOtherChainSource()` | `sfOtherChainSource` | `AccountID` | +| `getAmount()` | `sfAmount` | `STAmount` | +| `getAttestationRewardAccount()` | `sfAttestationRewardAccount` | `AccountID` | +| `getWasLockingChainSend()` | `sfWasLockingChainSend` | `uint8_t` | +| `getXChainClaimID()` | `sfXChainClaimID` | `uint64_t` | + +The single optional field, `sfDestination`, uses `protocol_autogen::Optional` — a type alias defined in `Utils.h`. Because `std::optional` cannot hold references, `Optional` uses `std::conditional_t` to substitute `std::optional>>` when `T` is a reference type, and `std::optional` otherwise. The pattern `hasDestination()` / `getDestination()` is consistent throughout the autogen layer: the presence check is separated from the accessor so callers can conditionally access without catching exceptions. + +`sfDestination` being optional reflects the protocol: the committing user on the source chain may pre-specify where funds land, or they may defer that to a later `XChainClaim`. Witness servers record whatever the commit stated. + +## The Builder: `XChainAddClaimAttestationBuilder` + +`XChainAddClaimAttestationBuilder` uses the Curiously Recurring Template Pattern (CRTP) through `TransactionBuilderBase`. Every setter in the base class and the derived class returns `Derived&` (i.e. `XChainAddClaimAttestationBuilder&`), enabling method chaining without repeated casts. + +The CRTP base (`TransactionBuilderBase`) holds the mutable `STObject object_{sfTransaction}` that accumulates field assignments before the transaction is finalized. Critically, the base class comment explains why it avoids calling `object_.set(soTemplate)`: doing so would create `STBase` placeholders for `soeDEFAULT` fields, which would later cause `applyTemplate()` to throw "may not be explicitly set to default" when the `STTx` constructor processes the object. This is a subtle interaction between the XRPL serialization type system and the autogen layer. + +The constructor enforces that all nine required fields are set at construction time, passing them as arguments rather than allowing them to be added lazily. Optional `sequence` and `fee` are `std::optional` parameters with `std::nullopt` defaults, because test or simulation environments sometimes omit these for later injection. + +A second constructor accepts an existing `std::shared_ptr` and copies the raw `STObject`, allowing round-trip mutation: deserialize a transaction, wrap it in a builder, modify a field, re-sign, and rebuild. + +The terminal operation is `build(PublicKey, SecretKey)`, which delegates to `TransactionBuilderBase::sign()`. That method serializes the object without signing fields (`addWithoutSigningFields`), prepends `HashPrefix::txSign`, signs with the provided key pair, stores `sfSigningPubKey` and `sfTxnSignature`, then constructs a `shared_ptr` and wraps it in a fresh immutable `XChainAddClaimAttestation`. After `build()`, the builder's internal state has been moved out; it should not be reused. + +## Relationship to Sibling Transactions + +The sister type `XChainAddAccountCreateAttestation` (type 46) serves the same attestation role but for `XChainAccountCreateCommit`, which creates a brand-new account on the destination chain instead of crediting an existing one. Both share the same structural pattern — the difference lies in their required fields. Together they cover the two ways a cross-chain transfer can complete. + +## Design Rationale + +Generating these files rather than hand-authoring them eliminates the class of bugs where a field is added to the protocol definition but forgotten in the C++ accessor layer. Every field declared `soeREQUIRED` gets a getter that returns the value directly; every `soeOPTIONAL` field gets a `has*()` / `get*()` pair returning `Optional`. The separation of immutable reader (`XChainAddClaimAttestation`) from mutable builder (`XChainAddClaimAttestationBuilder`) enforces the invariant that once a transaction exists as a signed `STTx`, its contents cannot change — matching the ledger's own semantics. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainClaim.h.ai.json b/include/xrpl/protocol_autogen/transactions/XChainClaim.h.ai.json new file mode 100644 index 0000000000..d1cd4d37f9 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainClaim.h.ai.json @@ -0,0 +1,149 @@ +{ + "args": [ + { + "lineno": 23, + "name": "tx" + }, + { + "lineno": 109, + "name": "account" + }, + { + "lineno": 110, + "name": "xChainBridge" + }, + { + "lineno": 111, + "name": "xChainClaimID" + }, + { + "lineno": 112, + "name": "destination" + }, + { + "lineno": 113, + "name": "amount" + }, + { + "lineno": 114, + "name": "sequence" + }, + { + "lineno": 115, + "name": "fee" + }, + { + "lineno": 167, + "name": "publicKey" + }, + { + "lineno": 167, + "name": "secretKey" + }, + { + "lineno": 122, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 19, + "name": "XChainClaim" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& xChainBridge, std::decay_t const& xChainClaimID, std::decay_t const& destination, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 104, + "name": "XChainClaimBuilder" + } + ], + "description": "Defines the XChainClaim transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing XChainClaim transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/XChainClaim.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 48, + "name": "getXChainClaimID" + }, + { + "args": [], + "lineno": 58, + "name": "getDestination" + }, + { + "args": [], + "lineno": 68, + "name": "getDestinationTag" + }, + { + "args": [], + "lineno": 78, + "name": "hasDestinationTag" + }, + { + "args": [], + "lineno": 88, + "name": "getAmount" + }, + { + "args": [ + "value" + ], + "lineno": 120, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 129, + "name": "setXChainClaimID" + }, + { + "args": [ + "value" + ], + "lineno": 138, + "name": "setDestination" + }, + { + "args": [ + "value" + ], + "lineno": 147, + "name": "setDestinationTag" + }, + { + "args": [ + "value" + ], + "lineno": 156, + "name": "setAmount" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 165, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainClaim.h.ai.md b/include/xrpl/protocol_autogen/transactions/XChainClaim.h.ai.md new file mode 100644 index 0000000000..cc30a4cf44 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainClaim.h.ai.md @@ -0,0 +1,43 @@ +# `XChainClaim.h` — Cross-Chain Claim Transaction Wrapper + +## Role in the System + +This auto-generated header defines the final transaction in the XRPL cross-chain bridge transfer workflow, governed by the `featureXChainBridge` amendment. Cross-chain transfers on XRPL proceed through three discrete on-ledger steps: a `XChainCreateClaimID` (type 41) to reserve a claim slot on the destination chain, a `XChainCommit` (type 42) to lock funds on the source chain, and finally `XChainClaim` (type 43) to release those funds to their destination. This file covers that last step. + +The file lives under `include/xrpl/protocol_autogen/transactions/`, a directory that contains one header per transaction type, all generated from the same schema-driven template. The identical structure across `XChainCommit.h`, `XChainCreateClaimID.h`, and `XChainClaim.h` makes this clear: do not edit these files by hand. + +## Two-Class Design: Wrapper and Builder + +The header defines two classes with sharply separated responsibilities. + +`XChainClaim` is a read-only wrapper. It holds a `std::shared_ptr` inherited from `TransactionBase` and exposes strongly-typed getters for the fields specific to this transaction type. Once an `XChainClaim` object exists, the underlying transaction data is immutable — this is intentional, because a signed `STTx` must not be mutated without invalidating the signature. Passing `const` around a shared pointer would normally be easy to circumvent, but by taking `std::shared_ptr` (pointer-to-const), the wrapper enforces immutability at the type system level. + +`XChainClaimBuilder` is the construction path. It inherits from `TransactionBuilderBase`, a CRTP base that provides all common transaction fields (`sfAccount`, `sfSequence`, `sfFee`, `sfLastLedgerSequence`, `sfMemos`, `sfDelegate`, etc.) via chainable setters. The CRTP pattern is the reason these shared setters can return `XChainClaimBuilder&` rather than `TransactionBuilderBase&` — each setter in the base casts `*this` to `Derived&` before returning, enabling uninterrupted method chaining without repeated downcasts at the call site. + +## Field Structure + +`XChainClaim` carries four required fields and one optional: + +- `sfXChainBridge` — identifies which bridge (locking/issuing chain door accounts and asset types) this claim is against. Its C++ type is `SF_XCHAIN_BRIDGE::type::value_type`, resolving to `STXChainBridge`. +- `sfXChainClaimID` — a `uint64` that references the specific claim ID created by the earlier `XChainCreateClaimID` transaction. This ID is the on-chain link tying the commit and the claim together across chains. +- `sfDestination` — the `AccountID` that will receive the funds on the destination chain. +- `sfAmount` — the `STAmount` to be released. Cross-chain bridges can carry XRP or IOU assets; `STAmount` is polymorphic enough to handle both. +- `sfDestinationTag` (optional) — a `uint32` routing tag. The getter returns `protocol_autogen::Optional` (an alias for `std::optional`), and a companion `hasDestinationTag()` guard is provided following the convention used across `TransactionBase` for all optional fields. + +The builder's constructor requires all four mandatory fields upfront, preventing callers from accidentally omitting one and then calling `build()`. The optional `sequence` and `fee` parameters default to `std::nullopt` so that callers who want the network to fill those fields (e.g., during testing) don't have to supply them. + +## Type Safety and Defensive Checks + +Both the `XChainClaim` constructor and the `XChainClaimBuilder(std::shared_ptr)` overload verify `getTxnType() == ttXCHAIN_CLAIM` and throw `std::runtime_error` otherwise. This guards against the common mistake of wrapping a deserialized transaction of the wrong type — for example, mistakenly passing an `XChainCommit` STTx to an `XChainClaim` wrapper. The check is redundant in the happy path (where the builder's `build()` method produces the object), but essential for the secondary constructor used when loading existing transactions from storage or the network. + +Builder setter parameters use `std::decay_t const&`. The `std::decay_t` strips reference qualifiers from the SField type alias before forming the parameter type. Without it, a double-reference (`T& const&`) would collapse in ways that could silently accept rvalues where they shouldn't be bound, or vice versa. Using `decay_t` gives explicit, predictable value semantics throughout. + +## Build and Sign Flow + +The builder's `build(PublicKey const& publicKey, SecretKey const& secretKey)` method calls the `sign()` helper inherited (as `protected`) from `TransactionBuilderBase`. That helper serializes the in-progress `STObject object_` with the `HashPrefix::txSign` prefix (without signing fields), computes an Ed25519 or secp256k1 signature, writes `sfSigningPubKey` and `sfTxnSignature` into the object, then constructs a final `STTx` from it. The completed `STTx` is immediately wrapped in an `XChainClaim` — transitioning from mutable builder state to the immutable read-only wrapper in a single expression. + +The base class deliberately avoids calling `object_.set(soTemplate)` during construction. Doing so would insert `soeDEFAULT` placeholder fields into the `STObject`, which would cause `STTx`'s `applyTemplate()` to throw when it encounters an explicitly-set default value. By leaving the object template-free, the builder stays compatible with `STTx`'s own schema enforcement at construction time. + +## Relationship to the XChain Bridge Protocol + +`XChainClaim` is the redemption leg of a multi-step protocol. The `sfXChainClaimID` it carries is created by `XChainCreateClaimID`, funded by `XChainCommit` on the source chain, and attested to by witness servers using `XChainAddClaimAttestation`. Only after a quorum of attestations exist on-ledger does submitting an `XChainClaim` with the matching ID actually release funds to `sfDestination`. The transaction type number (43) sequentially follows `XChainCommit` (42) and `XChainCreateClaimID` (41), reflecting its position as the terminal step in the protocol. The `Delegation::delegable` attribute means a delegate account — set via `sfDelegate` from the common base — may submit this transaction on behalf of the originating account. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainCommit.h.ai.json b/include/xrpl/protocol_autogen/transactions/XChainCommit.h.ai.json new file mode 100644 index 0000000000..37e746ae04 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainCommit.h.ai.json @@ -0,0 +1,145 @@ +{ + "args": [ + { + "lineno": 31, + "name": "tx" + }, + { + "lineno": 97, + "name": "account" + }, + { + "lineno": 98, + "name": "xChainBridge" + }, + { + "lineno": 98, + "name": "xChainClaimID" + }, + { + "lineno": 98, + "name": "amount" + }, + { + "lineno": 99, + "name": "sequence" + }, + { + "lineno": 100, + "name": "fee" + }, + { + "lineno": 147, + "name": "publicKey" + }, + { + "lineno": 147, + "name": "secretKey" + }, + { + "lineno": 111, + "name": "value" + }, + { + "lineno": 120, + "name": "value" + }, + { + "lineno": 129, + "name": "value" + }, + { + "lineno": 138, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 28, + "name": "XChainCommit" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& xChainBridge, std::decay_t const& xChainClaimID, std::decay_t const& amount, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 91, + "name": "XChainCommitBuilder" + } + ], + "description": "Defines the XChainCommit transaction type for XRPL, including its immutable wrapper, builder class, and type-safe field accessors and mutators.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/XChainCommit.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 48, + "name": "getXChainClaimID" + }, + { + "args": [], + "lineno": 58, + "name": "getAmount" + }, + { + "args": [], + "lineno": 68, + "name": "getOtherChainDestination" + }, + { + "args": [], + "lineno": 79, + "name": "hasOtherChainDestination" + }, + { + "args": [ + "value" + ], + "lineno": 110, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 119, + "name": "setXChainClaimID" + }, + { + "args": [ + "value" + ], + "lineno": 128, + "name": "setAmount" + }, + { + "args": [ + "value" + ], + "lineno": 137, + "name": "setOtherChainDestination" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 146, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainCommit.h.ai.md b/include/xrpl/protocol_autogen/transactions/XChainCommit.h.ai.md new file mode 100644 index 0000000000..e9eda47d22 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainCommit.h.ai.md @@ -0,0 +1,42 @@ +# `XChainCommit.h` — Auto-Generated Cross-Chain Commit Transaction + +## Role in the System + +`XChainCommit.h` is an auto-generated header (the `// This file is auto-generated. Do not edit.` marker makes this explicit) that defines the C++ interface for the `XChainCommit` transaction type (`ttXCHAIN_COMMIT`, numeric type 42). It lives in the `xrpl::transactions` namespace alongside every other transaction type in the `protocol_autogen/transactions/` directory, which contains one file per XRPL transaction type. + +In the cross-chain bridge protocol (gated behind the `featureXChainBridge` amendment), committing is the first on-chain step of a cross-chain value transfer. A sender on the source chain calls `XChainCommit` to lock funds against a pre-existing `XChainClaimID`. Witnesses observe this and attest to the locking. The recipient then redeems the value on the destination chain via `XChainClaim`. This file provides the C++ typed wrapper and construction machinery for that commit step. + +## Two-Class Pattern: Reader and Builder + +The file follows the same structural contract as every other transaction in this directory: a paired *immutable wrapper* class and a *builder* class, here `XChainCommit` and `XChainCommitBuilder`. + +`XChainCommit` extends `TransactionBase`, which stores a `std::shared_ptr` — a reference-counted, deep-const pointer to the underlying serialized transaction object. Constructing an `XChainCommit` from a shared pointer performs an immediate type guard: `tx_->getTxnType() != txType` throws `std::runtime_error`. This means invalid casts from the untyped `STTx` world are caught at construction time rather than silently yielding garbage from field lookups. The static member `txType = ttXCHAIN_COMMIT` serves as a compile-time constant for this check and for external callers who need to dispatch on type. + +All getters are marked `[[nodiscard]]` and `const`, reinforcing the immutability contract. There is no mutation path on `XChainCommit` itself — you cannot partially update a wrapped transaction. + +`XChainCommitBuilder` extends `TransactionBuilderBase`, a CRTP base that owns a mutable `STObject object_{sfTransaction}` and exposes fluent setters for common fields like `setAccount()`, `setFee()`, `setSequence()`, and `setLastLedgerSequence()`. Every setter in the CRTP base returns `Derived&` — i.e., `XChainCommitBuilder&` — enabling method chaining across both base and derived setters without casting. The `XChainCommitBuilder`-specific setters (`setXChainBridge`, `setXChainClaimID`, `setAmount`, `setOtherChainDestination`) follow the same pattern, all returning `XChainCommitBuilder&`. + +## Fields and Their Design + +The transaction has three required fields and one optional field: + +- **`sfXChainBridge`** (`SF_XCHAIN_BRIDGE`) — identifies the bridge ledger object defining the two-chain topology (issuing account, door accounts, currency on each side). Required. +- **`sfXChainClaimID`** (`SF_UINT64`) — a 64-bit counter identifying the cross-chain claim ID that was previously created via `XChainCreateClaimID`. This serializes the commit to a specific transfer slot. Required. +- **`sfAmount`** (`SF_AMOUNT`) — the XRP drops or IOU amount being committed and locked. Must match what the bridge was configured to transfer. Required. +- **`sfOtherChainDestination`** (`SF_ACCOUNT`) — the destination account on the other chain. Optional: if omitted, the destination recorded in the claim ID is used. When present, it can override the initially specified recipient. + +The optional field handling is idiomatic: `getOtherChainDestination()` delegates to `hasOtherChainDestination()` before calling `tx_->at(sfOtherChainDestination)`, and returns `protocol_autogen::Optional`, which aliases to `std::optional`. This avoids the silent default-value trap that `STObject::at()` would introduce for absent fields. + +Setter parameters uniformly use `std::decay_t const&`. The `std::decay_t` strips reference and cv-qualifiers from the field's canonical `value_type`, so the setters accept plain values (e.g., `AccountID`, `STAmount`, `STXChainBridge`) by const reference regardless of how those value types are declared inside the `SF_*` template hierarchy. + +## Construction and Signing + +The builder's primary constructor requires all three mandatory fields plus `account`, while `sequence` and `fee` are `std::optional` — a deliberate design choice because these fields are often filled by the calling library (e.g., after fetching the current account sequence from the ledger) rather than known at construction time. + +A secondary constructor accepts `std::shared_ptr` directly. This round-trip path is useful when a transaction was deserialized from the wire or loaded from a ledger and the caller wants to modify it before re-signing. The builder copies the `STTx` into its mutable `object_` with `object_ = *tx`. + +The `build()` method calls `sign()` from `TransactionBuilderBase`, which serializes the object without signing fields, prepends the `HashPrefix::txSign` prefix, signs it with the provided key pair, and embeds the resulting signature in `sfTxnSignature`. It then wraps the resulting `STObject` in a new `STTx` and hands it to the `XChainCommit` constructor — producing the final immutable wrapper. + +## Relationship to Sibling Files + +`XChainCommit.h` is one of eight cross-chain bridge transaction files in this directory (`XChainCreateBridge`, `XChainModifyBridge`, `XChainCreateClaimID`, `XChainCommit`, `XChainClaim`, `XChainAccountCreateCommit`, `XChainAddClaimAttestation`, `XChainAddAccountCreateAttestation`). `XChainClaim` is structurally very similar — same required field set minus `OtherChainDestination`, plus a `Destination` field — reflecting the second half of the two-step cross-chain transfer. The autogenerated uniformity across all these files means any tooling that processes the base classes operates correctly for all transaction types without per-type special casing. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h.ai.json b/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h.ai.json new file mode 100644 index 0000000000..a20651394b --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h.ai.json @@ -0,0 +1,117 @@ +{ + "args": [ + { + "lineno": 31, + "name": "tx" + }, + { + "lineno": 87, + "name": "account" + }, + { + "lineno": 88, + "name": "xChainBridge" + }, + { + "lineno": 89, + "name": "signatureReward" + }, + { + "lineno": 90, + "name": "sequence" + }, + { + "lineno": 91, + "name": "fee" + }, + { + "lineno": 130, + "name": "publicKey" + }, + { + "lineno": 130, + "name": "secretKey" + }, + { + "lineno": 103, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "XChainCreateBridge" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& xChainBridge, std::decay_t const& signatureReward, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 80, + "name": "XChainCreateBridgeBuilder" + } + ], + "description": "Defines the XChainCreateBridge transaction type for XRPL, including its type-safe wrapper and builder classes for constructing and accessing transaction fields.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 47, + "name": "getSignatureReward" + }, + { + "args": [], + "lineno": 56, + "name": "getMinAccountCreateAmount" + }, + { + "args": [], + "lineno": 67, + "name": "hasMinAccountCreateAmount" + }, + { + "args": [ + "value" + ], + "lineno": 101, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 110, + "name": "setSignatureReward" + }, + { + "args": [ + "value" + ], + "lineno": 119, + "name": "setMinAccountCreateAmount" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 128, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h.ai.md b/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h.ai.md new file mode 100644 index 0000000000..617bd75018 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainCreateBridge.h.ai.md @@ -0,0 +1,53 @@ +# `XChainCreateBridge.h` — Auto-generated XChain Bridge Creation Transaction + +## Role in the System + +This file, located in `include/xrpl/protocol_autogen/transactions/`, is an auto-generated header that provides the C++ interface for the `XChainCreateBridge` transaction — transaction type `ttXCHAIN_CREATE_BRIDGE` (type code 48). It belongs to the `featureXChainBridge` amendment family and operates within the `xrpl::transactions` namespace. + +The `XChainCreateBridge` transaction initiates the existence of a cross-chain bridge on the XRP Ledger, allowing assets to be moved between two separate ledgers (typically XRPL mainnet and a sidechain, or two independent XRPL networks). Once submitted and validated, this transaction establishes the on-ledger `Bridge` object that subsequent cross-chain transactions — commits, claims, and attestations — reference by its `sfXChainBridge` identity. + +The file pairs a read-only wrapper class with a fluent builder class, following the same autogeneration pattern applied uniformly across all ~70 transaction types in this directory. + +## Design: Immutable Wrapper + Fluent Builder + +The file follows the same split that every transaction type in `protocol_autogen/transactions/` uses: + +**`XChainCreateBridge`** is an immutable, read-only view over a `std::shared_ptr`. It inherits from `TransactionBase`, which holds the shared pointer as `tx_` and provides all common field accessors (`getAccount()`, `getSequence()`, `getFee()`, `getDelegate()`, etc.). `XChainCreateBridge` adds only the three fields specific to its transaction type. Immutability is enforced both at the pointer level (`const` `STTx`) and by the class offering no setter methods — the only way to mutate is through the builder. + +**`XChainCreateBridgeBuilder`** is a mutable accumulator using CRTP (curiously recurring template pattern) via `TransactionBuilderBase`. The base class stores an `STObject object_{sfTransaction}` and provides setters for all common fields. The derived builder adds transaction-specific setters and, importantly, the terminal `build()` method, which calls `sign()` on the base, then wraps the accumulated `STObject` into a freshly allocated `STTx` and returns it inside an `XChainCreateBridge` wrapper. This one-way flow from builder to signed wrapper enforces that callers cannot hold a mutable reference after signing. + +A deliberate design choice in `TransactionBuilderBase` is to *not* call `object_.set(soTemplate)` — the comment in that base explains why: calling `applyTemplate()` before `STTx` construction would create default-value placeholders for `soeDEFAULT` fields, which then cause `applyTemplate()` inside the `STTx` constructor to throw "may not be explicitly set to default." The builder therefore operates on a free `STObject` and lets `STTx`'s own constructor handle template application. + +## Fields + +`XChainCreateBridge` carries three fields beyond the universal base fields: + +**`sfXChainBridge`** (`soeREQUIRED`) — a composite field of type `STXChainBridge` that encodes the four identifiers defining the bridge: the locking-chain issuing account, the issuing-chain issuing account, the locking-chain door account, and the issuing-chain door account. This serves as the bridge's stable identifier referenced by all subsequent cross-chain operations. + +**`sfSignatureReward`** (`soeREQUIRED`) — an `STAmount` specifying the XRP paid to witnesses for each valid attestation they submit. Making this required at bridge creation is a deliberate protocol decision: a bridge without a reward has no economic mechanism to attract witness participation, so permitting its absence would create non-functional bridges. Compare `XChainModifyBridge`, where `sfSignatureReward` is *optional* because modifications may target only the `sfMinAccountCreateAmount` without changing the reward. + +**`sfMinAccountCreateAmount`** (`soeOPTIONAL`) — an `STAmount` specifying the minimum XRP that must be attached to an `XChainAccountCreateCommit` transaction. This field exists only for XRP-native bridges where the destination chain requires a funded new account. IOU/token bridges do not use this field because they require the destination account to exist already; that makes optionality load-bearing rather than cosmetic. + +## Optional Field Handling + +The return type of `getMinAccountCreateAmount()` is `protocol_autogen::Optional`, a type alias defined in `Utils.h`. That alias is: + +```cpp +template +using Optional = std::conditional_t< + std::is_reference_v, + std::optional>>, + std::optional>; +``` + +This distinguishes reference types (like `STArray const&` returned from `getMemos()`) from value types (like `STAmount`). For `sfMinAccountCreateAmount`, the result is simply `std::optional`. The getter guards access with `hasMinAccountCreateAmount()` before calling `tx_->at(...)`, preventing an exception from `STObject::at()` when the field is absent. + +## Type Safety and Fail-Fast Validation + +Both the wrapper constructor and the STTx-initializing builder constructor perform an explicit type check against `ttXCHAIN_CREATE_BRIDGE`, throwing `std::runtime_error` on mismatch. This fail-fast guard prevents silent misuse when, for example, a caller holds a generic `std::shared_ptr` retrieved from the ledger and passes it to the wrong wrapper type. + +The static `constexpr txType` member allows compile-time dispatch in template code that dispatches on transaction type without a virtual call, consistent with how the autogeneration layer avoids runtime polymorphism beyond what `STTx` itself already provides. + +## Relationship to Sibling XChain Files + +`XChainCreateBridge.h` sits alongside six other XChain transaction headers: `XChainModifyBridge`, `XChainCommit`, `XChainAccountCreateCommit`, `XChainCreateClaimID`, `XChainClaim`, `XChainAddClaimAttestation`, and `XChainAddAccountCreateAttestation`. The bridge created here is referenced by all of them. `XChainModifyBridge` is the closest sibling — it shares the same `sfXChainBridge` and `sfMinAccountCreateAmount` fields, but promotes `sfSignatureReward` from required to optional, reflecting that post-creation modifications are incremental. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h.ai.json b/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h.ai.json new file mode 100644 index 0000000000..dd14fc82e6 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h.ai.json @@ -0,0 +1,116 @@ +{ + "args": [ + { + "lineno": 31, + "name": "tx" + }, + { + "lineno": 75, + "name": "account" + }, + { + "lineno": 76, + "name": "xChainBridge" + }, + { + "lineno": 77, + "name": "signatureReward" + }, + { + "lineno": 78, + "name": "otherChainSource" + }, + { + "lineno": 79, + "name": "sequence" + }, + { + "lineno": 80, + "name": "fee" + }, + { + "lineno": 118, + "name": "publicKey" + }, + { + "lineno": 118, + "name": "secretKey" + }, + { + "lineno": 91, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 27, + "name": "XChainCreateClaimID" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& xChainBridge, std::decay_t const& signatureReward, std::decay_t const& otherChainSource, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 67, + "name": "XChainCreateClaimIDBuilder" + } + ], + "description": "Defines the XChainCreateClaimID transaction type for the XRPL protocol, including a type-safe wrapper and a builder class for constructing and signing such transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 47, + "name": "getSignatureReward" + }, + { + "args": [], + "lineno": 56, + "name": "getOtherChainSource" + }, + { + "args": [ + "value" + ], + "lineno": 89, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 98, + "name": "setSignatureReward" + }, + { + "args": [ + "value" + ], + "lineno": 107, + "name": "setOtherChainSource" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 116, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h.ai.md b/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h.ai.md new file mode 100644 index 0000000000..e85b346abd --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainCreateClaimID.h.ai.md @@ -0,0 +1,33 @@ +# `XChainCreateClaimID.h` — Cross-Chain Claim ID Creation Transaction + +This auto-generated header defines the `ttXCHAIN_CREATE_CLAIM_ID` transaction type (type code 41), gated behind the `featureXChainBridge` amendment. In the XRP Ledger's cross-chain bridge protocol, a "claim ID" is a reservation object that must exist on the destination chain before a cross-chain value transfer can complete. `XChainCreateClaimID` is the mandatory first step in that two-phase flow: the account that will *receive* funds on the destination chain submits this transaction to allocate the claim ID, after which the sending account on the source chain references it in an `XChainCommit` (type 42). The file lives in `include/xrpl/protocol_autogen/transactions/` and is part of the auto-generated layer that maps the canonical `transactions.macro` definitions into strongly typed C++ classes. + +## Dual-Class Pattern: Wrapper and Builder + +The file follows the pattern used uniformly across the `xrpl::transactions` namespace: one immutable wrapper class and one fluent builder class. The separation reflects a real lifecycle constraint — once a transaction is signed, it must not be modified, so the wrapper only exposes `const` getters over an opaque `STTx`. The builder accumulates field values in a mutable `STObject` before the one-way signing step that produces the final wrapper. Neither class is designed for the other role. + +## `XChainCreateClaimID`: Type-Safe Read-Only Wrapper + +`XChainCreateClaimID` inherits from `TransactionBase`, which holds a `std::shared_ptr` and provides all common transaction field accessors (`getAccount()`, `getFee()`, `getSequence()`, `getDelegate()`, etc.). The constructor performs a runtime type check against `ttXCHAIN_CREATE_CLAIM_ID` and throws `std::runtime_error` on mismatch — a defensive guard that prevents accidental wrapping of a wrong transaction type, which would otherwise silently read incorrect field values. The `static constexpr txType` member enables compile-time dispatch in generic code that templates over transaction types. + +The three transaction-specific required fields correspond directly to the fields declared `soeREQUIRED` in `transactions.macro`: + +- `getXChainBridge()` returns the `STXChainBridge` value identifying the bridge — encoding the locking-chain door account, the issuing-chain door account, and the asset being bridged. +- `getSignatureReward()` returns the `STAmount` that will be paid to bridge witness servers for their cross-chain attestations. This reward exists to provide economic incentive for witnesses to monitor the source chain and submit `XChainAddClaimAttestation` transactions. +- `getOtherChainSource()` returns the `AccountID` of the account on the *sending* chain that will lock or burn the funds. The destination chain uses this to correlate attestations to the correct claim ID when multiple transfers are in flight. + +Because all three fields are `soeREQUIRED`, the getters use `STTx::at()` rather than optional-returning accessors — access is safe by construction for any valid transaction object that has passed schema validation in `TransactionBase::validate()`. + +## `XChainCreateClaimIDBuilder`: Construction and Signing + +`XChainCreateClaimIDBuilder` inherits from `TransactionBuilderBase`, which uses CRTP (Curiously Recurring Template Pattern) so that the base class's common setters (`setFee()`, `setSequence()`, `setLastLedgerSequence()`, `setDelegate()`, etc.) return a `XChainCreateClaimIDBuilder&` rather than a `TransactionBuilderBase&`. This enables method chaining without virtual dispatch — a zero-cost abstraction important for transaction-heavy hot paths. + +The primary constructor requires all three domain-specific fields at construction time alongside the initiating `account`, with `sequence` and `fee` as optional parameters. This enforces that no builder instance can be in an under-specified state: the three bridge fields are set by delegating to their respective setters immediately in the constructor body. Optional fields can be added afterward via the inherited base setters. + +A second constructor accepts a `std::shared_ptr` and performs the same type guard check, allowing round-tripping — loading a signed transaction back into a builder to re-sign or modify. Internally this assigns `object_ = *tx`, copying the `STObject` content. + +The `build()` method calls `sign()` from `TransactionBuilderBase`, which serializes the object (excluding signing fields), prepends `HashPrefix::txSign`, computes the signature with the provided `SecretKey`, and sets both `sfSigningPubKey` and `sfTxnSignature`. It then moves `object_` into a freshly constructed `STTx` — move semantics mean the builder's internal state is left in a valid but unspecified state after `build()` returns, so callers should treat the builder as consumed. The resulting `XChainCreateClaimID` wrapper owns the signed transaction through `std::shared_ptr`. + +## Relationship to the Cross-Chain Bridge Protocol + +Within the bridge workflow, `XChainCreateClaimID` is the prerequisite for `XChainCommit` — the commit transaction references `sfXChainClaimID` (the numeric ID allocated by the ledger when this transaction executes) to associate the locked funds with the correct destination. The `delegable` attribute recorded in the comments (and enforced by the transaction processor layer) means this transaction supports the XRPL delegation mechanism, allowing a separate account to submit it on behalf of the originating account. The `featureXChainBridge` amendment guard ensures that ledgers which have not activated the amendment reject this transaction type entirely, keeping the protocol addition backward-compatible. \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h.ai.json b/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h.ai.json new file mode 100644 index 0000000000..062fdbbf08 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h.ai.json @@ -0,0 +1,118 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 98, + "name": "account" + }, + { + "lineno": 99, + "name": "xChainBridge" + }, + { + "lineno": 100, + "name": "sequence" + }, + { + "lineno": 101, + "name": "fee" + }, + { + "lineno": 139, + "name": "publicKey" + }, + { + "lineno": 139, + "name": "secretKey" + }, + { + "lineno": 112, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr tx" + ], + "lineno": 23, + "name": "XChainModifyBridge" + }, + { + "args": [ + "SF_ACCOUNT::type::value_type account, std::decay_t const& xChainBridge, std::optional sequence = std::nullopt, std::optional fee = std::nullopt", + "std::shared_ptr tx" + ], + "lineno": 91, + "name": "XChainModifyBridgeBuilder" + } + ], + "description": "Defines the XChainModifyBridge transaction type for XRPL, including its immutable wrapper, builder class, and type-safe field accessors and setters.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "getXChainBridge" + }, + { + "args": [], + "lineno": 48, + "name": "getSignatureReward" + }, + { + "args": [], + "lineno": 59, + "name": "hasSignatureReward" + }, + { + "args": [], + "lineno": 69, + "name": "getMinAccountCreateAmount" + }, + { + "args": [], + "lineno": 80, + "name": "hasMinAccountCreateAmount" + }, + { + "args": [ + "value" + ], + "lineno": 110, + "name": "setXChainBridge" + }, + { + "args": [ + "value" + ], + "lineno": 119, + "name": "setSignatureReward" + }, + { + "args": [ + "value" + ], + "lineno": 128, + "name": "setMinAccountCreateAmount" + }, + { + "args": [ + "publicKey", + "secretKey" + ], + "lineno": 137, + "name": "build" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl::transactions" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h.ai.md b/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h.ai.md new file mode 100644 index 0000000000..8b0780b8c7 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/XChainModifyBridge.h.ai.md @@ -0,0 +1,33 @@ +# `XChainModifyBridge.h` — Auto-Generated XChain Bridge Modification Transaction + +This file is part of the `protocol_autogen` layer — a code-generated set of strongly-typed transaction wrappers built on top of XRPL's raw `STTx` serialization objects. It defines `XChainModifyBridge` (transaction type `ttXCHAIN_MODIFY_BRIDGE`, code 47), which allows the owner of a cross-chain bridge to update its operational parameters after creation. The file is gated behind the `featureXChainBridge` amendment and lives in the `xrpl::transactions` namespace alongside the full family of XChain transaction types. + +## Transaction Purpose + +Once a cross-chain bridge is live (created via `XChainCreateBridge`), its two tunable parameters — `sfSignatureReward` and `sfMinAccountCreateAmount` — may need adjustment as network conditions change. `XChainModifyBridge` is the only mechanism for doing so. It identifies the bridge to modify via the required `sfXChainBridge` composite field, then accepts either or both optional parameters. Because only the fields that need updating need to be supplied, the transaction can act as a partial update; callers don't need to repeat unchanged values. + +## Wrapper/Builder Split + +The file follows a strict two-class pattern shared across every transaction in `protocol_autogen/transactions/`: + +**`XChainModifyBridge`** is the read-only, immutable wrapper. It takes ownership of a `shared_ptr` through `TransactionBase` and exposes typed accessors. The constructor performs a runtime type check, throwing `std::runtime_error` if the wrapped `STTx` is not actually `ttXCHAIN_MODIFY_BRIDGE`. This guard is necessary because `STTx` itself is a generic container — the wrapper's type safety would be meaningless without a validated construction point. + +**`XChainModifyBridgeBuilder`** is the mutable construction surface, using CRTP by inheriting from `TransactionBuilderBase`. The template base provides all common field setters (`setFee`, `setSequence`, `setFlags`, `setDelegate`, etc.) and the protected `sign()` method. Because `setters` in the base return `Derived&` via `static_cast`, each call correctly returns `XChainModifyBridgeBuilder&` rather than a slice of the base — enabling unbroken fluent chains across both base and derived setters. + +## Field Schema and Optionality + +`sfXChainBridge` is the only required field in this transaction beyond the universal ones (account, sequence, fee). Its type is `SF_XCHAIN_BRIDGE::type::value_type`, an alias for `STXChainBridge` — a composite structure encoding the locking and issuing chain accounts and currencies. The builder takes it by `std::decay_t<...> const&`, stripping reference qualifiers so the assignment into `STObject` works cleanly regardless of the source value category. + +`sfSignatureReward` and `sfMinAccountCreateAmount` are both `soeOPTIONAL` `SF_AMOUNT` fields, and the wrapper reflects this with paired accessor patterns: `getSignatureReward()` / `hasSignatureReward()` and `getMinAccountCreateAmount()` / `hasMinAccountCreateAmount()`. The getters return `protocol_autogen::Optional` (i.e. `std::optional`) and short-circuit via the corresponding `has*()` call before touching `STTx::at()`, avoiding any risk of accessing a missing field. This is a deliberate contrast to how `XChainCreateBridge` treats `sfSignatureReward` — there it is `soeREQUIRED` and returned directly without wrapping in `optional`. + +## Construction Paths + +The builder offers two construction paths. The primary path takes an account plus the required `sfXChainBridge` field, then accepts optional sequence and fee arguments before delegating to `TransactionBuilderBase`. The secondary path accepts an existing `shared_ptr`, validates its type, and copies the `STObject` contents into `object_` — this enables round-tripping: load an already-existing transaction, adjust fields, re-sign, and produce a fresh `XChainModifyBridge`. The `STObject object_` member (declared in `TransactionBuilderBase` as `object_{sfTransaction}`) intentionally starts as a "free object" without an applied `SOTemplate`, sidestepping the restriction that `soeDEFAULT` fields cannot be explicitly set; the template is only applied later by the `STTx` constructor. + +## Build and Sign + +Calling `build(publicKey, secretKey)` finalises the transaction: it invokes `sign()` from the base (which serialises the object with `HashPrefix::txSign`, computes the signature, and writes `sfSigningPubKey` and `sfTxnSignature` into `object_`), then constructs a `shared_ptr` from the mutated `STObject` and hands it to `XChainModifyBridge`'s constructor. After `build()` returns, the builder's internal `object_` has been moved out and is in a valid-but-unspecified state — the wrapper is the sole owner of the signed, immutable transaction. + +## Relationship to the XChain Family + +All eight XChain transaction headers in this directory share the same structural template, including the auto-generated comment at line 1. `XChainModifyBridge` is the narrowest of the set: it touches only an existing bridge's mutable parameters, while `XChainCreateBridge` establishes the bridge, `XChainCreateClaimID` / `XChainCommit` / `XChainClaim` move assets across it, and the attestation types (`XChainAddClaimAttestation`, `XChainAddAccountCreateAttestation`) handle witness quorum mechanics. Because the bridge identity itself (`sfXChainBridge`) is immutable once created, any request to change locking or issuing accounts must go through a full bridge teardown and recreation rather than through this transaction. \ No newline at end of file diff --git a/include/xrpl/rdb/DBInit.h.ai.json b/include/xrpl/rdb/DBInit.h.ai.json new file mode 100644 index 0000000000..9dfc1ed5a8 --- /dev/null +++ b/include/xrpl/rdb/DBInit.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines database initialization pragmas, constants, and SQL schema definitions for ledger, transaction, and wallet databases in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/rdb/DBInit.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/rdb/DBInit.h.ai.md b/include/xrpl/rdb/DBInit.h.ai.md new file mode 100644 index 0000000000..6b49ff967e --- /dev/null +++ b/include/xrpl/rdb/DBInit.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/rdb/DBInit.h` — SQLite Database Schema and Pragma Definitions + +`DBInit.h` is the single authoritative declaration of every SQLite schema and configuration string used by the three persistent relational databases in the XRPL node: the ledger database, the transaction database, and the wallet database. It contains no executable code — only `inline constexpr` string literals and compile-time arrays — so it functions as a machine-readable schema registry that is guaranteed to agree with the actual database files at every call site. + +## Why a Header-Only Schema? + +Embedding DDL and pragma strings as `constexpr` constants rather than in `.cpp` files or external SQL files is a deliberate tradeoff. It keeps the schema visible to the compiler for type checking on array sizes (the `DatabaseCon` constructor is templated on `std::size_t M` to enforce that the caller passes a properly-sized array), avoids file-system reads at startup, and makes it impossible for a deployment to diverge from the compiled binary's expectations. Every call to `DatabaseCon(...)` that passes one of these arrays is statically checked against the template parameter at compile time. + +## SQLite Pragma Format Strings + +Three format strings serve as the basis for the runtime SQLite tuning layer: + +- `CommonDBPragmaJournal` — controls journaling mode (`DELETE`, `WAL`, `MEMORY`, etc.) +- `CommonDBPragmaSync` — controls `fsync` discipline (`OFF`, `NORMAL`, `FULL`, `EXTRA`) +- `CommonDBPragmaTemp` — controls where SQLite stores its temporary tables (`DEFAULT`, `FILE`, `MEMORY`) + +All three use `%s` placeholders and are formatted by `boost::format` in `Config.cpp` before being stored in `DatabaseCon::Setup::globalPragma`. The populated strings are then applied to every new database connection via `DatabaseCon`'s private constructor, which iterates over the pragma list before running the DDL initialization SQL. + +`SQLITE_TUNING_CUTOFF` (10,000,000 ledgers) acts as a guard in `Config.cpp`: if any of the higher-risk options are selected (e.g. `journal_mode=memory` or `synchronous=off`) and the node is configured for at least this much ledger history, a warning is emitted. The comment in the header makes the reasoning explicit — a large dataset makes corruption recovery far more expensive, so operators should be aware before sacrificing durability for performance. + +## Ledger Database (`ledger.db`) + +`LgrDBInit` is a 5-element `std::array` that wraps all DDL in a single transaction. It creates the `Ledgers` table, which stores one row per validated ledger with the chain-linking fields needed to reconstruct ledger history: `LedgerHash` (primary key), `LedgerSeq`, `PrevHash`, `TotalCoins`, closing time metadata, and the two state/transaction set hashes (`AccountSetHash`, `TransSetHash`). The `SeqLedger` index on `LedgerSeq` supports efficient lookups by sequence number rather than by hash, which is the more common query pattern during sync and history serving. + +A `DROP TABLE IF EXISTS Validations` statement is retained but no longer creates anything — it is an artifact that removes a legacy table from pre-existing databases on first open. This kind of schema migration-in-place is common throughout the init arrays. + +## Transaction Database (`transaction.db`) + +`TxDBInit` is an 8-element array and the most index-heavy of the three schemas. It contains two tables and four indexes: + +The `Transactions` table holds raw ledger transactions. `TransID` is the primary key; `RawTxn` and `TxnMeta` are BLOBs holding the serialized transaction and its metadata. `TxLgrIndex` on `LedgerSeq` supports bulk access by ledger (used when serving full ledger data). + +`AccountTransactions` is a join table that maps every transaction to every account it touched — enabling the "account transaction history" API. Its three indexes reflect the three common query shapes: lookup by `TransID`, paginated history for an `Account` ordered by `(LedgerSeq, TxnSeq, TransID)`, and a secondary access path keyed on `(LedgerSeq, Account)`. The account history index in particular uses a composite covering index with `TransID` as the trailing key to allow efficient pagination without secondary lookups. + +## Wallet Database (`wallet.db`) + +`WalletDBInit` is a 6-element array covering three tables, all created in a single transaction. `NodeIdentity` stores the one-row keypair that identifies this server on the peer-to-peer network; the comment notes that this can be overridden by a `[node_seed]` config entry. `PeerReservations` associates peer public keys with human-readable descriptions, implementing the reserved-slot feature that lets operators guarantee connection slots for specific peers. `ValidatorManifests` and `PublisherManifests` persist raw signed manifest blobs, used by the validator-trust machinery to track ephemeral key rotations for both validators and manifest publishers. + +## Relationship to `DatabaseCon` + +`DatabaseCon.h` directly includes `DBInit.h` and uses the init arrays as template arguments. The flow at startup is: `Config.cpp` builds the global pragma vector from operator config using the format strings defined here; `Node.cpp` and `Wallet.cpp` call `DatabaseCon(...)` constructors passing `LgrDBInit`/`TxDBInit`/`WalletDBInit`; the constructor applies pragmas first, then the DDL SQL under an explicit `BEGIN TRANSACTION / END TRANSACTION` wrapping present in each array — ensuring that partial schema creation never leaves a database in an inconsistent state. \ No newline at end of file diff --git a/include/xrpl/rdb/DatabaseCon.h.ai.json b/include/xrpl/rdb/DatabaseCon.h.ai.json new file mode 100644 index 0000000000..894a121b69 --- /dev/null +++ b/include/xrpl/rdb/DatabaseCon.h.ai.json @@ -0,0 +1,101 @@ +{ + "args": [ + { + "lineno": 25, + "name": "it" + }, + { + "lineno": 25, + "name": "m" + }, + { + "lineno": 29, + "name": "rhs" + }, + { + "lineno": 62, + "name": "setup" + }, + { + "lineno": 63, + "name": "dbName" + }, + { + "lineno": 64, + "name": "pragma" + }, + { + "lineno": 65, + "name": "initSQL" + }, + { + "lineno": 66, + "name": "journal" + }, + { + "lineno": 74, + "name": "checkpointerSetup" + }, + { + "lineno": 83, + "name": "dataDir" + }, + { + "lineno": 120, + "name": "pPath" + }, + { + "lineno": 121, + "name": "commonPragma" + }, + { + "lineno": 164, + "name": "id" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr it, mutex& m", + "LockedSociSession&& rhs", + "LockedSociSession() = delete", + "LockedSociSession const& rhs = delete" + ], + "lineno": 18, + "name": "LockedSociSession" + }, + { + "args": [ + "Setup const& setup, std::string const& dbName, std::array const& pragma, std::array const& initSQL, beast::Journal journal", + "Setup const& setup, std::string const& dbName, std::array const& pragma, std::array const& initSQL, CheckpointerSetup const& checkpointerSetup, beast::Journal journal", + "boost::filesystem::path const& dataDir, std::string const& dbName, std::array const& pragma, std::array const& initSQL, beast::Journal journal", + "boost::filesystem::path const& dataDir, std::string const& dbName, std::array const& pragma, std::array const& initSQL, CheckpointerSetup const& checkpointerSetup, beast::Journal journal", + "boost::filesystem::path const& pPath, std::vector const* commonPragma, std::array const& pragma, std::array const& initSQL, beast::Journal journal" + ], + "lineno": 49, + "name": "DatabaseCon" + } + ], + "description": "This file defines classes and utilities for managing SQLite database connections and sessions in the XRPL codebase, including support for checkpointing and thread-safe session access.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/rdb/DatabaseCon.h", + "functions": [ + { + "args": [ + "id" + ], + "lineno": 164, + "name": "checkpointerFromId" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "soci" + }, + { + "lineno": 16, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/rdb/DatabaseCon.h.ai.md b/include/xrpl/rdb/DatabaseCon.h.ai.md new file mode 100644 index 0000000000..729e805cab --- /dev/null +++ b/include/xrpl/rdb/DatabaseCon.h.ai.md @@ -0,0 +1,52 @@ +# `DatabaseCon.h` — SQLite Connection Management for the XRP Ledger + +## Purpose + +`DatabaseCon.h` defines the two central abstractions the XRPL node uses to interact with its embedded SQLite databases: `LockedSociSession`, which makes individual database operations thread-safe, and `DatabaseCon`, which owns and configures a SOCI session from first open through optional WAL checkpointing. Every ledger, transaction, and wallet database in the node passes through this layer. + +## `LockedSociSession` — Thread-Safe Session Handle + +`LockedSociSession` is a move-only RAII type that pairs a `std::shared_ptr` with a `std::unique_lock`. The design is deliberate: callers acquire the session and its lock together as a single atomic step, hold them for the duration of their query or transaction, and release both automatically on destruction. + +Copying is explicitly deleted (`= delete`) while moving is allowed, which makes the ownership semantics clear: at any moment, exactly one active scope holds the lock. The `get()`, `operator*()`, and `operator->()` accessors all delegate to the underlying `soci::session*`, so callers use the object as if it were a raw session pointer without needing to manage locking separately. + +The use of `std::recursive_mutex` rather than a plain `std::mutex` accommodates code paths that may legitimately acquire a `LockedSociSession` while already holding the lock on the same connection — for example, in test scaffolding or in the wallet database where identity initialization may call back into the same connection. + +## `DatabaseCon` — Connection Owner and Lifecycle Manager + +`DatabaseCon` owns a single SQLite database connection and is responsible for opening it, applying SQLite PRAGMA settings, running initialization DDL, and optionally enabling WAL-mode checkpointing. + +### Construction and the `Setup` Struct + +The `Setup` struct captures everything about the node's configuration that affects how a database is opened: + +- `startUp` distinguishes between `Fresh`, `Normal`, `Load`, `LoadFile`, `Replay`, and `Network` startup modes (defined in `StartUpType.h`). +- `standAlone` and `startUp` together determine whether the node uses real on-disk database files or ephemeral files. When `standAlone` is true and the startup mode is neither `Load`, `LoadFile`, nor `Replay`, the path passed to SOCI is an empty string, causing SQLite to use a temporary database that disappears on close. This avoids polluting disk during testing or short-lived standalone runs. +- `globalPragma` is a `static std::unique_ptr const>` — shared across all connections. It holds node-wide SOCI PRAGMA settings (journal mode, sync mode, temp store) built once from the node configuration and applied to every connection via `commonPragma()`. The `useGlobalPragma` flag on each `Setup` controls whether these are applied to a specific connection, with an assertion that `globalPragma` is non-null whenever `useGlobalPragma` is true. +- `txPragma` (4 entries) and `lgrPragma` (1 entry) are per-database arrays for connection-specific tuning. + +The template constructors accept `std::array` for pragmas and `std::array` for initialization SQL. Using compile-time fixed-size arrays (rather than `std::vector`) lets callers pass literals like `TxDBInit` or `LgrDBInit` from `DBInit.h` without heap allocation, and the size is validated at compile time. + +There are four public constructors — two accepting `Setup` (which resolves the file path and temporary-vs-real decision) and two accepting a raw `boost::filesystem::path` (for callers who already know the location). Each of these has an overload that also accepts a `CheckpointerSetup`, enabling WAL checkpointing without requiring a separate call after construction. + +The private canonical constructor is where the actual work happens: it calls `open()` from `SociDB.h` with the backend name `"sqlite"`, then runs all pragmas (per-connection first, then shared global ones), then runs each `initSQL` statement in a prepared `soci::statement`. All of this happens inside the constructor body, so a fully-constructed `DatabaseCon` always has a live, initialized connection — no two-phase initialization. + +### Session Ownership and the Checkpointer's `weak_ptr` Invariant + +The underlying `soci::session` is stored as `std::shared_ptr` rather than a plain member or `unique_ptr`. This choice is driven entirely by the `Checkpointer` teardown hazard: a checkpoint job submitted to the `JobQueue` may still be running when the `DatabaseCon` is destroyed. If the session were owned by value or `unique_ptr`, the destructor would delete it while the job is executing. + +By holding a `shared_ptr` to the session, the `DatabaseCon` allows the `Checkpointer` to hold a corresponding `weak_ptr`. The checkpointer's job callback locks the `weak_ptr` before accessing the session — if the lock fails (the `DatabaseCon` has been destroyed and the session's refcount hit zero), the callback aborts cleanly. The `DatabaseCon` destructor then waits — via a spin loop in `DatabaseCon.cpp` — until all references to the session are gone before returning. This ensures no job ever writes through a dangling session pointer. + +The `checkpointer_` member is itself a `shared_ptr`, and checkpointers are registered in a global collection (maintained in `DatabaseCon.cpp`) keyed by a monotonically increasing numeric ID. The free function `checkpointerFromId()` provides safe external lookup into that collection, used by job callbacks that need to retrieve their checkpointer without coupling to the `DatabaseCon`. + +### `checkoutDb()` vs. `getSession()` + +`checkoutDb()` is the primary, safe access point. It constructs a `LockedSociSession` while wrapping the lock acquisition in `perf::measureDurationAndLog()` with a 10ms threshold. If acquiring the lock takes longer than 10 milliseconds — indicating contention — a warning is logged to `j_`. This makes lock contention on database connections observable in the node's performance logs without adding overhead to the common case. + +`getSession()` returns an unlocked reference to the session. It is retained for contexts where a higher-level lock already guarantees exclusive access, or during initial setup before multi-threaded operation begins. Callers using `getSession()` take on the responsibility of ensuring no concurrent access occurs. + +### Relationship to `DBInit.h` and `SociDB.h` + +`DBInit.h` defines the three standard XRPL databases (`ledger.db`, `transaction.db`, `wallet.db`) and their initialization SQL arrays. These arrays map directly onto the `initSQL` template parameter of `DatabaseCon`'s constructors. The pragma constants in `DBInit.h` (`CommonDBPragmaJournal`, `CommonDBPragmaSync`, `CommonDBPragmaTemp`) are formatted at startup and stored into `Setup::globalPragma`. + +`SociDB.h` provides the `open()` function used to connect the SOCI session, the `Checkpointer` abstract interface, and the `makeCheckpointer()` factory — all of which `DatabaseCon` delegates to rather than reimplementing. \ No newline at end of file diff --git a/include/xrpl/rdb/RelationalDatabase.h.ai.json b/include/xrpl/rdb/RelationalDatabase.h.ai.json new file mode 100644 index 0000000000..3975c4547e --- /dev/null +++ b/include/xrpl/rdb/RelationalDatabase.h.ai.json @@ -0,0 +1,38 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 17, + "name": "LedgerHashPair" + }, + { + "args": [], + "lineno": 22, + "name": "LedgerRange" + }, + { + "args": [], + "lineno": 27, + "name": "RelationalDatabase" + } + ], + "description": "Defines the RelationalDatabase interface for interacting with ledger and transaction data in a relational database for the XRPL project, including methods for querying, saving, and deleting ledger and transaction records, as well as utility types and a range-checked casting template.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/rdb/RelationalDatabase.h", + "functions": [ + { + "args": [ + "c" + ], + "lineno": 343, + "name": "rangeCheckedCast" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/rdb/RelationalDatabase.h.ai.md b/include/xrpl/rdb/RelationalDatabase.h.ai.md new file mode 100644 index 0000000000..a79a1c33ec --- /dev/null +++ b/include/xrpl/rdb/RelationalDatabase.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/rdb/RelationalDatabase.h` + +## Role in the System + +`RelationalDatabase` is the abstract interface that sits at the centre of the XRPL node's relational-database layer. It exists because the XRP Ledger software persists two categories of mutable data in a relational store — ledger headers and transaction records — and must expose a single, backend-agnostic surface to the rest of the application. By concentrating every SQL-touching query behind a pure-virtual class, the codebase enforces a strict boundary: no hard-coded SQL is permitted anywhere outside the `xrpld/app/rdb` directory tree. + +The class models three underlying tables — `Ledgers`, `Transactions`, and `AccountTransactions` — whose physical layout is owned by the concrete implementation. Presently the only production implementation is `SQLiteDatabase`, which keeps two separate `DatabaseCon` handles (`ledgerDb_` and `txdb_`), allowing the ledger-header and transaction stores to be managed as distinct SQLite files. The interface is deliberately database-agnostic: the README notes that a `PostgresDatabase` variant once existed alongside `SQLiteDatabase`, and the design accommodates future alternatives. + +## Query Vocabulary: Structs and Type Aliases + +Rather than proliferating function parameters, the interface defines a small set of descriptive value types that serve as a structured query vocabulary. + +`LedgerHashPair` bundles a ledger's own hash with its parent's hash into a single return value, which is the canonical output when the caller needs chain-continuity information. `LedgerRange` captures an inclusive `[min, max]` sequence-number window; a value of `0` for either bound means unbounded, letting callers express open-ended range queries without special-casing. + +`CountMinMax` is a combined aggregation result containing the row count, minimum sequence, and maximum sequence of the `Ledgers` table — useful for health and monitoring endpoints. + +The account-transaction side of the interface has a richer vocabulary. `AccountTxOptions` is the offset-based query descriptor: it carries the account ID, a `LedgerRange`, an `offset`, a `limit`, and a flag `bUnlimited`. The corresponding marker-based variant is `AccountTxPageOptions`, which replaces `offset` with an `std::optional`. `AccountTxMarker` is a `(ledgerSeq, txnSeq)` pair that encodes a cursor into the result set: when a paged query does not exhaust results, the returned optional marker lets the caller resume exactly where it stopped. The split into two option structs reflects the fact that offset-based and marker-based pagination impose different semantics on the underlying SQL. + +The return-type aliases complete the vocabulary. `AccountTx` is a `std::pair, shared_ptr>` — the deserialized, object-form result. `AccountTxs` is a vector of those pairs. For callers that need the raw bytes without incurring deserialization, `MetaTxsList` is a vector of `tuple` (raw transaction bytes, raw metadata bytes, ledger sequence). The "B" suffix on several method names consistently marks the binary variants. + +`LedgerSpecifier` is a `std::variant` that covers every way a caller might identify a ledger. `AccountTxArgs` and `AccountTxResult` aggregate inputs and outputs for the highest-level `account_tx` RPC path, where the caller additionally signals whether it wants binary output and which traversal direction (forward vs. backward) to use. + +## API Design: Eight Account-Transaction Accessors + +The account-transaction query surface is deliberately symmetric. Two access patterns (offset-based, marker-based) × two orderings (oldest-first, newest-first) × two output formats (object, binary) yields eight methods. Offset-based variants (`getOldestAccountTxs`, `getNewestAccountTxs`, `getOldestAccountTxsB`, `getNewestAccountTxsB`) return a plain vector; marker-based page variants (`oldestAccountTxPage`, `newestAccountTxPage`, `oldestAccountTxPageB`, `newestAccountTxPageB`) return a `std::pair>` so callers can detect whether the result set was truncated and where to resume. This explicit continuation rather than a separate "has more" boolean prevents ambiguity when the limit happens to divide the result set evenly. + +## `getTransaction` and Definitive Absence + +`getTransaction()` has an unusual signature: it returns `std::variant`. This encodes a three-way distinction that is important for RPC correctness. If the transaction is found, the variant holds the `AccountTx` pair. If not found, the `TxSearched` enum signals how thorough the search was: `TxSearched::All` means the caller provided a range and every ledger in that range is present in the database (authoritative absence), `TxSearched::Some` means the range has gaps so absence is non-conclusive, and `TxSearched::Unknown` means no range was provided or a deserialization error occurred — in this last case the `error_code_i& ec` out-parameter is populated. Returning an enum rather than `bool` lets callers reason about the quality of a negative result, which is essential for serving `tx` RPC calls that need to distinguish "definitely not in any ledger we have" from "we just don't know." + +## Deletion Methods + +Four deletion operations expose the table-by-table structure of the store: `deleteTransactionByLedgerSeq`, `deleteBeforeLedgerSeq`, `deleteTransactionsBeforeLedgerSeq`, and `deleteAccountTransactionsBeforeLedgerSeq`. These are kept separate because the three tables may lag each other during online database rotation (managed by `SHAMapStore`), and ledger headers, transaction records, and account-transaction index rows must sometimes be pruned independently. + +## Disk-Space Monitoring + +`getKBUsedAll()`, `getKBUsedLedger()`, and `getKBUsedTransaction()` surface disk usage in kilobytes, allowing the application to make capacity decisions without reaching into the underlying `DatabaseCon`. `closeLedgerDB()` and `closeTransactionDB()` are needed during the database rotation cycle, where the write-ahead log is flushed and a fresh database file takes over. + +## `rangeCheckedCast`: Defensive Numeric Conversion + +The free function template `rangeCheckedCast(C c)` is defined here rather than in a generic utilities header because it is tightly coupled to the database layer's habit of reading integer columns into types whose width differs from the column's actual precision. It enforces that the value fits in the target type at all three problematic boundaries — unsigned underflow, signed underflow, and overflow — and triggers `UNREACHABLE` plus an error log if any constraint is violated. The `LCOV_EXCL` markers confirm this is treated as an impossible path in normal execution, consistent with "this is a programming error, not a runtime error" semantics. + +## Relationship to Sibling Files + +`DatabaseCon.h` provides the `LockedSociSession` RAII wrapper that concrete implementations use to safely check out a `soci::session` under a recursive mutex. `DBInit.h` and `SociDB.h` supply schema initialization and the SOCI-backed session pool. The sole concrete class `SQLiteDatabase` (declared in `src/xrpld/app/rdb/backend/SQLiteDatabase.h`) holds two `std::unique_ptr` members and delegates all SQL to helper functions in `Node.cpp`, keeping this abstract interface free of any engine-specific details. \ No newline at end of file diff --git a/include/xrpl/rdb/SociDB.h.ai.json b/include/xrpl/rdb/SociDB.h.ai.json new file mode 100644 index 0000000000..5ea62d93d3 --- /dev/null +++ b/include/xrpl/rdb/SociDB.h.ai.json @@ -0,0 +1,199 @@ +{ + "args": [ + { + "lineno": 30, + "name": "dbPath" + }, + { + "lineno": 28, + "name": "config" + }, + { + "lineno": 28, + "name": "dbName" + }, + { + "lineno": 44, + "name": "s" + }, + { + "lineno": 44, + "name": "config" + }, + { + "lineno": 44, + "name": "dbName" + }, + { + "lineno": 56, + "name": "s" + }, + { + "lineno": 56, + "name": "beName" + }, + { + "lineno": 56, + "name": "connectionString" + }, + { + "lineno": 61, + "name": "s" + }, + { + "lineno": 62, + "name": "s" + }, + { + "lineno": 64, + "name": "from" + }, + { + "lineno": 64, + "name": "to" + }, + { + "lineno": 65, + "name": "from" + }, + { + "lineno": 65, + "name": "to" + }, + { + "lineno": 66, + "name": "from" + }, + { + "lineno": 66, + "name": "to" + }, + { + "lineno": 67, + "name": "from" + }, + { + "lineno": 67, + "name": "to" + }, + { + "lineno": 87, + "name": "id" + }, + { + "lineno": 87, + "name": "session" + }, + { + "lineno": 87, + "name": "JobQueue" + }, + { + "lineno": 87, + "name": "ServiceRegistry" + } + ], + "classes": [ + { + "args": [ + "BasicConfig const& config, std::string const& dbName" + ], + "lineno": 27, + "name": "DBConfig" + }, + { + "args": [], + "lineno": 70, + "name": "Checkpointer" + } + ], + "description": "An embedded database wrapper providing a type-safe, C++ interface to SQLite databases using SOCI, with utilities for configuration, session management, and checkpointing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/rdb/SociDB.h", + "functions": [ + { + "args": [ + "s", + "config", + "dbName" + ], + "lineno": 44, + "name": "open" + }, + { + "args": [ + "s", + "beName", + "connectionString" + ], + "lineno": 56, + "name": "open" + }, + { + "args": [ + "s" + ], + "lineno": 61, + "name": "getKBUsedAll" + }, + { + "args": [ + "s" + ], + "lineno": 62, + "name": "getKBUsedDB" + }, + { + "args": [ + "from", + "to" + ], + "lineno": 64, + "name": "convert" + }, + { + "args": [ + "from", + "to" + ], + "lineno": 65, + "name": "convert" + }, + { + "args": [ + "from", + "to" + ], + "lineno": 66, + "name": "convert" + }, + { + "args": [ + "from", + "to" + ], + "lineno": 67, + "name": "convert" + }, + { + "args": [ + "id", + "session", + "JobQueue", + "ServiceRegistry" + ], + "lineno": 87, + "name": "makeCheckpointer" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 18, + "name": "sqlite_api" + }, + { + "lineno": 22, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/rdb/SociDB.h.ai.md b/include/xrpl/rdb/SociDB.h.ai.md new file mode 100644 index 0000000000..3f58c24d6b --- /dev/null +++ b/include/xrpl/rdb/SociDB.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/rdb/SociDB.h` — SOCI Database Abstraction Layer + +This header is the public surface of the XRP Ledger's thin but carefully designed wrapper around the [SOCI](https://soci.sourceforge.net/) database library. It solves three distinct problems: normalizing session creation from the node's configuration system, bridging SOCI's binary column type to idiomatic C++ containers, and managing SQLite Write-Ahead Log (WAL) checkpointing through the XRPL job queue without blocking database operations. + +## Why This Layer Exists + +SOCI provides a cross-backend, C++ SQL interface, but it does not know about `BasicConfig`, XRPL's diagnostic memory reporting, or the job scheduling system. This file defines the glue that makes SOCI usable inside the XRPL server's operational model. Although the config section `[sqdb]` technically accepts a `backend` key, the implementation in `SociDB.cpp` throws on anything except `"sqlite"` — the abstraction is forward-looking, but only SQLite is supported today. + +## `DBConfig` — Deferred Session Opening + +`DBConfig` captures the information needed to open a `soci::session` without actually opening it yet. The public constructor takes a `BasicConfig` and a database name, reads the `[sqdb]` section to find the backend, reads `database_path` from the legacy config, and constructs a file path. Two special database names — `"validators"` and `"peerfinder"` — receive a `.sqlite` extension; all others get `.db`. This distinction is baked into `getSociInit()` in the `.cpp` and reflects historical naming conventions in the codebase. + +The private constructor `DBConfig(std::string const& dbPath)` takes a raw path and is the only way to store the connection string. This forces all public callers through the config-parsing path, making accidental misuse of bare paths impossible from outside the translation unit. + +When a caller is ready to open the session, it calls `DBConfig::open(soci::session& s)`, which delegates to `s.open(soci::sqlite3, connectionString_)`. Alternatively, the two free `open()` functions bypass `DBConfig` and open a session immediately — useful when the session and config are both available at the same point in initialization. + +## `getKBUsedAll` and `getKBUsedDB` + +These diagnostic functions expose SQLite's internal memory counters through the XRPL layer. `getKBUsedAll()` calls `sqlite3_memory_used()`, which reports total SQLite heap allocation across all connections. `getKBUsedDB()` calls `sqlite3_db_status()` with `SQLITE_DBSTATUS_CACHE_USED` for the per-connection page cache. Both reach the raw `sqlite3*` handle by dynamic-casting the SOCI session backend to `soci::sqlite3_session_backend` and extracting its `conn_` field — a necessary but brittle coupling to SOCI internals that can only be avoided by patching SOCI itself. + +## `convert` Overloads — Bridging `soci::blob` and C++ Types + +SOCI's `blob` type is the correct mapping for SQLite BLOB columns, but its read/write interface uses raw `char*` buffers. The four `convert()` overloads translate between `soci::blob` and `std::vector` or `std::string`, handling the empty-container edge case explicitly (calling `blob.trim(0)` on write, and early-returning on read). This normalization means callers never have to reason about SOCI's binary API directly. + +## `Checkpointer` — WAL Management via the Job Queue + +The `Checkpointer` abstract base class defines the interface for SQLite WAL checkpointing. Its concrete implementation, `WALCheckpointer` (private to `SociDB.cpp`), is where the interesting engineering lives. + +SQLite in WAL mode appends writes to a separate log file and periodically must "checkpoint" — copy completed WAL frames back into the main database file. If this never happens, the WAL grows without bound. SQLite does perform automatic checkpoints, but XRPL needs checkpointing to happen on the `JobQueue` thread (`jtWAL`), not inline during a write operation, to avoid latency spikes. + +`WALCheckpointer` registers a `sqlite3_wal_hook` during construction. This hook fires synchronously on the database connection thread whenever the WAL reaches `checkpointPageCount` (1000) pages. The hook cannot safely call `schedule()` on the checkpointer directly and also needs a way to find the checkpointer object — since the hook receives only a `void*` user data pointer. The design encodes the checkpointer's ID (a `uintptr_t` cast of its address) as the hook's user data, then looks up the live `shared_ptr` via `checkpointerFromId()` (declared in `DatabaseCon.h`). If the lookup fails — meaning the `DatabaseCon` has been destroyed — the hook removes itself by calling `sqlite3_wal_hook(conn, nullptr, nullptr)`. + +`schedule()` guards against double-queuing with a mutex-protected `running_` flag. Once the flag is set, it adds a `jtWAL` job to the `JobQueue`, capturing a `std::weak_ptr` (via `shared_from_this()`) to avoid keeping the object alive after `DatabaseCon` is destroyed. The actual checkpoint runs `SQLITE_CHECKPOINT_PASSIVE`, which checkpoints as many frames as possible without blocking readers or writers, then clears `running_`. + +## Lifetime Safety Design + +The most subtle aspect of this file is its ownership model. `DatabaseCon` (in `DatabaseCon.h`) holds a `std::shared_ptr` and passes a `std::weak_ptr` into the `WALCheckpointer`. This allows the `DatabaseCon` to be destroyed while a checkpoint job is still queued: the job locks the weak pointer, finds it null, and exits cleanly. Similarly, the job captures a `weak_ptr`, preventing a live job from accessing a destroyed checkpointer even in the rare race where `DatabaseCon` is torn down after the weak pointer is locked but before `checkpoint()` returns. + +## Clang Diagnostic Suppression + +The header wraps its SOCI include in `#pragma clang diagnostic push/pop` to silence `-Wdeprecated` warnings from SOCI's own headers. This is the correct pattern for suppressing third-party warnings at the include boundary without affecting diagnostics in XRPL's own code. + +## Relationship to `DatabaseCon` + +`SociDB.h` provides primitives; `DatabaseCon` (in `DatabaseCon.h`) assembles them into the operational database connection used throughout the server. `DatabaseCon` calls `open()` from this header, wraps the session in `LockedSociSession` for mutual exclusion, and optionally wires up a `Checkpointer` via `makeCheckpointer()`. Direct callers of `SociDB.h` outside `DatabaseCon` are limited to diagnostic tooling and tests. \ No newline at end of file diff --git a/include/xrpl/resource/Charge.h.ai.json b/include/xrpl/resource/Charge.h.ai.json new file mode 100644 index 0000000000..c8ec2551b8 --- /dev/null +++ b/include/xrpl/resource/Charge.h.ai.json @@ -0,0 +1,101 @@ +{ + "args": [ + { + "lineno": 15, + "name": "cost" + }, + { + "lineno": 15, + "name": "label" + }, + { + "lineno": 35, + "name": "m" + }, + { + "lineno": 41, + "name": "os" + }, + { + "lineno": 41, + "name": "v" + } + ], + "classes": [ + { + "args": [ + "value_type cost", + "std::string const& label" + ], + "lineno": 10, + "name": "Charge" + } + ], + "description": "Defines the Charge class representing a consumption charge in the xrpl::Resource namespace, including its interface and related operators.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/Charge.h", + "functions": [ + { + "args": [ + "value_type cost", + "std::string const& label" + ], + "lineno": 15, + "name": "Charge" + }, + { + "args": [], + "lineno": 18, + "name": "label" + }, + { + "args": [], + "lineno": 22, + "name": "cost" + }, + { + "args": [], + "lineno": 26, + "name": "to_string" + }, + { + "args": [ + "Charge const&" + ], + "lineno": 29, + "name": "operator==" + }, + { + "args": [ + "Charge const&" + ], + "lineno": 32, + "name": "operator<=>" + }, + { + "args": [ + "value_type m" + ], + "lineno": 35, + "name": "operator*" + }, + { + "args": [ + "std::ostream& os", + "Charge const& v" + ], + "lineno": 41, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/Charge.h.ai.md b/include/xrpl/resource/Charge.h.ai.md new file mode 100644 index 0000000000..69f70c133e --- /dev/null +++ b/include/xrpl/resource/Charge.h.ai.md @@ -0,0 +1,36 @@ +# `xrpl::Resource::Charge` — Consumption Charge Value Type + +`Charge` is the fundamental unit of resource accounting in the XRPL server's rate-limiting system. It pairs an integer cost value with a human-readable label, forming the atomic token that callers pass to `Consumer::charge()` whenever server work is performed on behalf of an external endpoint. + +## Role in the Resource Management System + +The broader `xrpl::Resource` module (documented in `README.md`) tracks load imposed by every inbound websocket client and peer-overlay connection. When any significant server operation is performed — validating a signature, executing an RPC call, processing peer data — the responsible handler applies one of the pre-defined `Charge` instances from `Fees.h`. The `Consumer` accumulates these charges into a running balance; when that balance crosses heuristic thresholds, the `ResourceManager` warns or disconnects the endpoint. + +`Charge` itself is intentionally passive: it carries no behaviour beyond holding a cost and a name. All of the policy logic — decaying balances, threshold evaluation, gossip propagation — lives in `Consumer`, `Logic`, and `Entry`. The separation keeps policy change isolated from the charge vocabulary. + +## Design Decisions + +**Default construction is deleted.** A `Charge()` with no cost and no label would be a meaningless value that could silently do nothing when applied. By deleting the default constructor, every `Charge` object must be created with an explicit integer cost, preventing accidental zero-cost charges from slipping through. The comment in the header notes that a default-constructed object would have no way to produce a meaningful label, reinforcing that this is by intent. + +**Comparison ignores the label.** The implementations of `operator==` and `operator<=>` in `Charge.cpp` compare only `m_cost`, completely ignoring `m_label`. Two charges with the same numeric cost but different labels are equal by this ordering. This is deliberate: the label is purely diagnostic — a convenience for logging and operator display — while the cost is what matters for rate-limiting arithmetic. Treating them as equal when costs match simplifies callers that need to sort or deduplicate charges by severity. + +**`operator*` scales cost, preserves label.** The multiplication operator produces a new `Charge` with `m_cost * m` but the original `m_label`. This allows a caller to express "twice the standard heavy RPC fee" without inventing a new named charge, while log output still identifies the charge family by its original label. + +## The Canonical Charge Schedule + +`Fees.h` declares sixteen `extern Charge const` objects that form the server-wide vocabulary of costs: + +- **Protocol charges** — `feeMalformedRequest`, `feeInvalidSignature`, `feeUselessData`, `feeInvalidData`, `feeRequestNoReply` — applied when peers send bad or unserviceable protocol messages. +- **RPC charges** — ranging from `feeReferenceRPC` (a baseline default) up through `feeMediumBurdenRPC` and `feeHeavyBurdenRPC`, with `feeMalformedRPC` and `feeExceptionRPC` for error cases. +- **Peer charges** — `feeTrivialPeer`, `feeModerateBurdenPeer`, `feeHeavyBurdenPeer` — for peer-overlay work that does not map neatly to RPC semantics. +- **Administrative signals** — `feeWarning` and `feeDrop` — applied when the `ResourceManager` issues a warning or forcibly drops a connection; these encode the cost of the administrative action itself in the same accounting ledger. + +By declaring these as `const` objects rather than plain integer constants, the type system ensures that every application of load carries a label traceable through logs and diagnostics. + +## Display and Diagnostics + +`to_string()` formats a `Charge` as `"label ($cost)"`, using the dollar-sign metaphor to signal that the number is a unitless resource credit value, not actual currency. The stream `operator<<` delegates to `to_string()`, making `Charge` objects directly printable in any `beast::Journal` or `std::ostream` context without further formatting helpers. + +## Integration Point + +The primary consumer of `Charge` outside this module is `Consumer::charge(Charge const& fee, ...)`, which applies the charge's cost to the endpoint's running balance and returns a `Disposition` indicating whether the endpoint should be warned, dropped, or allowed to continue. Because `Charge` is a simple value type, it is always passed by `const&` and imposes no ownership or lifetime concerns on callers. \ No newline at end of file diff --git a/include/xrpl/resource/Consumer.h.ai.json b/include/xrpl/resource/Consumer.h.ai.json new file mode 100644 index 0000000000..3e80c86480 --- /dev/null +++ b/include/xrpl/resource/Consumer.h.ai.json @@ -0,0 +1,133 @@ +{ + "args": [ + { + "lineno": 14, + "name": "logic" + }, + { + "lineno": 14, + "name": "entry" + }, + { + "lineno": 18, + "name": "other" + }, + { + "lineno": 29, + "name": "name" + }, + { + "lineno": 41, + "name": "fee" + }, + { + "lineno": 41, + "name": "context" + }, + { + "lineno": 51, + "name": "j" + }, + { + "lineno": 62, + "name": "publicKey" + }, + { + "lineno": 67, + "name": "os" + }, + { + "lineno": 67, + "name": "v" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "Consumer" + } + ], + "description": "Defines the Consumer class, representing an endpoint that consumes resources in the XRPL system, with methods for privilege management, charging, and status checking.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/Consumer.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "to_string" + }, + { + "args": [], + "lineno": 25, + "name": "isUnlimited" + }, + { + "args": [ + "name" + ], + "lineno": 29, + "name": "elevate" + }, + { + "args": [], + "lineno": 35, + "name": "disposition" + }, + { + "args": [ + "fee", + "context" + ], + "lineno": 41, + "name": "charge" + }, + { + "args": [], + "lineno": 47, + "name": "warn" + }, + { + "args": [ + "j" + ], + "lineno": 51, + "name": "disconnect" + }, + { + "args": [], + "lineno": 55, + "name": "balance" + }, + { + "args": [], + "lineno": 59, + "name": "entry" + }, + { + "args": [ + "publicKey" + ], + "lineno": 62, + "name": "setPublicKey" + }, + { + "args": [ + "os", + "v" + ], + "lineno": 67, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/Consumer.h.ai.md b/include/xrpl/resource/Consumer.h.ai.md new file mode 100644 index 0000000000..fdc8b10e1e --- /dev/null +++ b/include/xrpl/resource/Consumer.h.ai.md @@ -0,0 +1,47 @@ +# `include/xrpl/resource/Consumer.h` + +## Role in the Resource Management Subsystem + +`Consumer` is the public-facing handle for XRPL's peer and RPC rate-limiting system. The broader `Resource` subsystem assigns a running "balance" to every network endpoint — inbound peers, outbound connections, and privileged (unlimited) admin connections — and enforces disconnection or warning when that balance exceeds configured thresholds. `Consumer` is what the rest of the node interacts with: a lightweight, copyable object that represents one endpoint's resource consumption record and provides methods to charge it, query its disposition, and act on overload signals. + +The internal data lives in `Entry`, a heap-allocated node managed by `Logic`. `Consumer` is nothing more than a reference-counted pointer pair `{Logic*, Entry*}`. The `Manager` interface (declared in `ResourceManager.h`) is what the application layer uses to mint new `Consumer` objects; the actual construction is gated through `Logic`, which is declared a `friend` so it can invoke the private `Consumer(Logic&, Entry&)` constructor. + +## Reference Counting and Lifetime + +`Consumer`'s copy constructor, destructor, and copy-assignment operator together implement a manual reference count routed through `Logic::acquire()` and `Logic::release()`: + +- **Copy**: increments `Entry::refcount` via `Logic::acquire()`. +- **Destruction / reassignment**: decrements `refcount` via `Logic::release()`. +- **When refcount reaches zero**: `Logic::release()` moves the `Entry` from its active intrusive list (inbound, outbound, or admin) to the `inactive_` list and stamps a `whenExpires` time. A background `periodicActivity()` sweep then erases entries that have lingered in inactive for `secondsUntilExpiration` (300 seconds). + +This design avoids separate heap allocation for the refcount itself (the field lives directly in `Entry`) and keeps all mutation inside `Logic`, which holds a `std::recursive_mutex`. `Consumer` objects are therefore safe to copy and destroy from any thread, but they are not independently thread-safe for concurrent mutation of the same instance. + +The default constructor produces a null `Consumer` (`m_logic == nullptr`, `m_entry == nullptr`). Many methods short-circuit on null pointers, which allows `Consumer` to be used as an optional-style placeholder before a real endpoint is assigned. + +## Privilege Levels and the `charge()` Path + +Every `Entry` carries a `Kind`: `kindInbound`, `kindOutbound`, or `kindUnlimited`. The `kindUnlimited` kind is assigned to administrative connections (local or cluster-level) that should not be subject to resource limits. `isUnlimited()` delegates to `Entry::isUnlimited()`, which simply checks `key->kind == kindUnlimited`. + +The `charge(Charge const& fee, std::string const& context)` method is the primary operation: it applies a cost to the endpoint's balance and returns a `Disposition` (`ok`, `warn`, or `drop`). Critically, **unlimited consumers are silently exempted** — `charge()` short-circuits for them and always returns `ok`. The balance itself is maintained in `Entry::local_balance`, a `DecayingSample` with a 32-second exponential decay window. This means short bursts decay away quickly; sustained high-rate consumption accumulates. The combined balance (local plus any imported `remote_balance` from gossip peers) is then compared against the `warningThreshold` (5000) and `dropThreshold` (25000) defined in `Tuning.h`. + +`disposition()` performs a zero-cost charge (`Charge(0)`) to read the current balance-based state without incrementing it. This is intentionally a bit indirect — rather than exposing the raw integer balance as a `Disposition` directly, it routes through the same code path as a real charge, ensuring the same decay and rounding logic applies. + +## Warning and Disconnect Signals + +`warn()` and `disconnect()` serve as the action layer on top of `charge()`. Both delegate to `Logic::warn()` and `Logic::disconnect()` respectively, both of which are no-ops for unlimited consumers. + +`Logic::warn()` is edge-triggered: it only fires if the balance is at or above `warningThreshold` **and** the current clock time differs from `entry.lastWarningTime`. This prevents the same endpoint from receiving repeated warnings in rapid succession — the warning "consumes itself" by updating `lastWarningTime` and applying a `feeWarning` charge. The `warn()` method on `Consumer` reflects this: its doc comment notes "this consumes the warning," meaning callers should not call it speculatively in a tight loop. + +`disconnect()` checks whether the balance is at or above `dropThreshold`. If so, it additionally applies a `feeDrop` charge — a deliberate penalty designed to prevent a just-dropped peer from immediately reconnecting and passing the initial `disposition()` check cleanly. + +## Gossip Integration and `remote_balance` + +Each `Entry` carries both a `local_balance` (the decaying sample of this node's own observations) and a `remote_balance` (an integer summing contributions from other cluster nodes via the gossip mechanism). When `Logic::importConsumers()` receives gossip data from a peer, it increments `remote_balance` on the relevant entries by the reported values, and decrements the old values when the gossip expires or is superseded. `Consumer::entry()` exposes the raw `Entry&` specifically to allow `importConsumers()` to manipulate `remote_balance` directly — it is labeled as private in spirit even though it is public in declaration. + +## The `elevate()` Stub + +The header declares `elevate(std::string const& name)`, described as raising a consumer's privilege to a "Named endpoint" and releasing the reference to the original endpoint descriptor. No implementation exists in the codebase. This appears to be either a planned feature that was never completed or a vestigial declaration from an older design that removed the named-endpoint tier. Code calling `elevate()` would produce a linker error, making this a dead declaration rather than a runtime trap. + +## `setPublicKey()` + +When a connected peer completes its handshake, the server can associate a `PublicKey` with the resource entry via `setPublicKey()`. This feeds into `Entry::to_string()` via `getFingerprint(key->address, publicKey)`, making log messages and JSON output identify peers by their cryptographic identity rather than IP address alone — important in the XRPL context where multiple validators may be reachable through the same NAT address. \ No newline at end of file diff --git a/include/xrpl/resource/Disposition.h.ai.json b/include/xrpl/resource/Disposition.h.ai.json new file mode 100644 index 0000000000..217aa4ceae --- /dev/null +++ b/include/xrpl/resource/Disposition.h.ai.json @@ -0,0 +1,18 @@ +{ + "args": [], + "classes": [], + "description": "Defines the Disposition enum representing the possible outcomes for a consumer after applying a load charge in the xrpl::Resource namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/Disposition.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/Disposition.h.ai.md b/include/xrpl/resource/Disposition.h.ai.md new file mode 100644 index 0000000000..5dcee89dbb --- /dev/null +++ b/include/xrpl/resource/Disposition.h.ai.md @@ -0,0 +1,27 @@ +# `xrpl::Resource::Disposition` — Load-Control Decision Signal + +## Role in the System + +`Disposition.h` defines a three-state enum that is the return type of every load-charge evaluation in the XRPL resource-management subsystem. It is deliberately minimal: a single `enum` in the `xrpl::Resource` namespace with no dependencies whatsoever. Its job is to carry the verdict that `Logic` reaches about a network endpoint back through the `Consumer` API to whatever code is handling that peer connection. + +## The Three States + +`ok`, `warn`, and `drop` map cleanly onto the three actions the node software can take toward a misbehaving or overloaded peer: + +- **`ok`** — the endpoint's load balance is within acceptable bounds; do nothing. +- **`warn`** — consumption is elevated but not yet critical; the caller may signal the remote peer that it is approaching its limit. +- **`drop`** — the endpoint has exceeded its allowance; the caller must disconnect it. + +The ordering of the enumerators matters implicitly: higher numeric value signals a more severe outcome, which allows comparison logic in `Logic` to find the "worst" disposition across multiple charges without needing a separate severity table. + +## How It Flows Through the Codebase + +`Consumer::charge()` and `Consumer::disposition()` both return a `Disposition`. In `Consumer.cpp`, every charge path initialises a local `Disposition d = ok;` and then delegates to `Logic::charge()`, whose return value becomes the function's result. Privileged (unlimited) endpoints skip the charge entirely and always return `ok`. + +The calling code — typically peer-management layers like `OverlayImpl` or `PeerImp` — receives this value and decides whether to issue a warning message to the remote peer or close the connection outright. The enum keeps that decision boundary sharp: the resource subsystem computes the verdict, the network layer executes it, and neither bleeds into the other's concerns. + +## Design Rationale + +Using a plain unscoped `enum` (rather than `enum class`) is intentional: the values are used directly as comparison targets throughout the `Logic` and `Consumer` implementations without requiring casts. The three-state design separates the "approaching limit" signal from the hard "disconnect" signal, giving operators a grace window before connections are terminated. A binary `ok/drop` would lose that nuance and force more aggressive disconnection policies. + +Because `Disposition.h` has no `#include` directives of its own, it can be included anywhere in the resource subsystem — including in headers that are themselves included widely — without pulling in any additional dependencies. \ No newline at end of file diff --git a/include/xrpl/resource/Fees.h.ai.json b/include/xrpl/resource/Fees.h.ai.json new file mode 100644 index 0000000000..9541b916da --- /dev/null +++ b/include/xrpl/resource/Fees.h.ai.json @@ -0,0 +1,18 @@ +{ + "args": [], + "classes": [], + "description": "Declares external Charge constants representing various fees for different types of resource consumption and server load scenarios in the XRPL server.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/Fees.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/Fees.h.ai.md b/include/xrpl/resource/Fees.h.ai.md new file mode 100644 index 0000000000..19652b6e6a --- /dev/null +++ b/include/xrpl/resource/Fees.h.ai.md @@ -0,0 +1,46 @@ +# `include/xrpl/resource/Fees.h` — Resource Charge Schedule + +`Fees.h` is the fee schedule for the XRPL server's resource-management subsystem. It declares a fixed set of named `Charge` constants that quantify the load cost of every category of misbehaving, burdensome, or invalid request a node might receive. These constants serve as the vocabulary through which calling code communicates resource events to the `Resource::Manager`. + +## Role in the Resource System + +The `xrpl::Resource` subsystem implements a leaky-bucket rate-limiter over network endpoints. Each connected peer or RPC client is represented by a `Consumer` object. Every time the server processes a request from that consumer it calls `Consumer::charge(fee)`, passing one of the constants declared here. Charges accumulate in the consumer's running balance, which decays exponentially over a 32-second window (`Tuning.h`: `decayWindowSeconds = 32`). When the balance crosses `warningThreshold` (5000 units) the consumer receives a warning; when it crosses `dropThreshold` (25000 units) the connection is severed. `Fees.h` is not involved in that accounting machinery — it purely defines the numeric weights used as inputs. + +## The Constants and Their Magnitudes + +The file groups charges into four logical tiers, and the relative magnitudes reveal the threat model directly. + +**General protocol charges** cover events that span both RPC and peer paths: + +| Constant | Cost | Rationale | +|---|---|---| +| `feeRequestNoReply` | 10 | Unsatisfiable but not obviously malformed; very cheap. | +| `feeMalformedRequest` | 200 | Detectable as invalid immediately, low CPU, but clearly misbehaving. | +| `feeUselessData` | 150 | Data received that serves no purpose. | +| `feeInvalidData` | 400 | Data that required verification work before rejection. | +| `feeInvalidSignature` | 2000 | Cryptographic verification was done and failed — expensive CPU work was wasted. | + +The ordering here is important: `feeInvalidSignature` costs ten times more than `feeMalformedRequest` because crypto verification is a significant CPU operation, while malformation checking is a cheap parse step. + +**RPC-tier charges** range from `feeReferenceRPC` (20), a baseline for a well-formed but unspecified load, up to `feeHeavyBurdenRPC` (3000) for queries that require substantial server work. `feeMediumBurdenRPC` (400) and `feeHeavyBurdenRPC` (3000) are used by RPC handlers that do index scans, ledger traversals, or other resource-intensive operations. + +**Peer-tier charges** reflect the peer-to-peer gossip and data-propagation paths. `feeTrivialPeer` is deliberately set at 1 — the lowest possible unit — because many peer interactions (such as simple acknowledgments) impose negligible load and must not count against well-behaved peers. `feeModerateBurdenPeer` (250) and `feeHeavyBurdenPeer` (2000) cover validation messages and large data transfers. + +**Administrative charges**, `feeWarning` (4000) and `feeDrop` (6000), are sentinel values charged when the system issues a warning or drops a consumer. At 4000, `feeWarning` is just below `warningThreshold` (5000), meaning a consumer that already has any balance will be pushed into warning territory as soon as it receives one. At 6000, `feeDrop` itself exceeds the warning threshold, ensuring that any endpoint being forcibly dropped crosses into the warning band immediately. These charges serve as accounting bookkeeping that makes the consumer's balance reflect its history even after it reconnects. + +## Design: Named `extern` Constants vs. Enum + +A natural alternative would be to use an `enum` or `constexpr int` values directly. The choice of `extern Charge const` objects is deliberate: `Charge` bundles a numeric cost with a human-readable string label (`"invalid signature"`, `"heavy RPC"`, etc.). This label is surfaced in log output, in `Consumer::charge()` call sites, and in diagnostics without any additional lookup table. The label travels with the cost from the point of declaration all the way to log emission, which makes triage of abusive consumers significantly easier. + +The `Charge` class also supports `operator*(value_type)` multiplication, so callers can express proportional cost (e.g. `feeMediumBurdenRPC * 3`) while retaining the base label — a pattern useful when batch operations should proportionally penalize based on the number of items processed. + +## Call Site Pattern + +The typical use in `PeerImp.cpp` is: + +```cpp +if ((usage_.charge(fee, context) == Resource::drop) && usage_.disconnect(p_journal_)) + ... +``` + +The `fee` passed here is always one of the constants from this file, selected based on the nature of the violation or load event. `Logic.h` `#include`s `Fees.h` directly, making the entire schedule available to the core accounting engine, while individual protocol handlers only need to select the appropriate constant for each event type. \ No newline at end of file diff --git a/include/xrpl/resource/Gossip.h.ai.json b/include/xrpl/resource/Gossip.h.ai.json new file mode 100644 index 0000000000..6db2231416 --- /dev/null +++ b/include/xrpl/resource/Gossip.h.ai.json @@ -0,0 +1,29 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "Gossip" + }, + { + "args": [], + "lineno": 15, + "name": "Item" + } + ], + "description": "Defines the Gossip struct for exchanging resource consumption information across peers, including a nested Item struct representing individual consumers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/Gossip.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/Gossip.h.ai.md b/include/xrpl/resource/Gossip.h.ai.md new file mode 100644 index 0000000000..3b15345165 --- /dev/null +++ b/include/xrpl/resource/Gossip.h.ai.md @@ -0,0 +1,31 @@ +# `include/xrpl/resource/Gossip.h` + +## Role in the System + +`Gossip.h` defines the plain data structure used to share resource-consumption intelligence between nodes in an XRPL cluster. It is the interchange format for the resource manager's distributed rate-limiting mechanism: when a server is seeing heavy traffic from a particular IP address, it can warn its peers so they can proactively watch or shed load from the same source. + +The struct sits at the boundary between local accounting and inter-node communication. It carries no logic, no mutex, and no lifetime management — it is purely a typed envelope for serialization and transfer. + +## The Data Model + +`Gossip` holds a `std::vector`, where each `Item` pairs a `beast::IP::Endpoint` (the IP address of a consumer) with an `int balance`. The balance is a snapshot of that consumer's local load score as maintained by the `Entry` tracking objects inside `Logic`. A value of zero means no significant load; the scale is defined by `Tuning.h`, where `minimumGossipBalance = 1000` sets the entry bar for inclusion, `warningThreshold = 5000` triggers a warning, and `dropThreshold = 25000` causes disconnection. + +The choice to carry only the endpoint address and an integer score — and nothing else — is deliberate. The receiving node already has its own `Entry` for every address it talks to directly; gossip only augments that entry's `remote_balance` field, reflecting load being observed elsewhere in the cluster. No state machine, no sequence numbers, no acknowledgments: gossip is advisory and expires automatically. + +## Export: Selecting What to Share + +`Logic::exportConsumers()` iterates the `inbound_` intrusive list (tracking connections that originated from external peers) and builds a `Gossip` by copying out the current `local_balance` for each entry. Entries whose balance falls below `minimumGossipBalance` are silently excluded — sharing noise about lightly-loaded consumers would waste bandwidth and complicate the receiving node's accounting. The result is a compact list of only the endpoints that are causing meaningful stress. + +## Import: Applying Received Gossip + +`Logic::importConsumers()` takes a `Gossip` and an `origin` string (the sending peer's identifier) and applies the contained balances as `remote_balance` adjustments on the local `Entry` objects for those endpoints. If gossip from the same origin has been received before, the previous contribution is first subtracted before the new values are added — ensuring the remote-balance figure always reflects the *current* view from that peer, not an accumulating sum. Imported gossip expires after `gossipExpirationSeconds` (30 seconds), so a peer going silent automatically drains its influence. + +This import/export cycle is what connects `Gossip` to the rest of the rate-limiting system. An endpoint's total "threat score" in `Logic` combines both `local_balance` (what *this* node sees) and `remote_balance` (what peers reported via gossip). A botnet client that spreads its requests across many cluster nodes may never trigger a warning on any single node, but gossip aggregates the distributed signal so every node can act on it. + +## Wire Encoding + +`Gossip` itself has no serialization logic. The actual network transport happens in `PeerImp.cpp` using the `TMLoadSource` Protobuf message. Received load-source records are decoded into `Gossip::Item` instances (using `beast::IP::Endpoint::from_string` on the `name` field and mapping the `cost` field to `balance`) before being passed into `importConsumers()`. This keeps `Gossip` transport-neutral and easily testable in isolation — the `Logic_test.cpp` unit tests use it directly without any network layer. + +## Design Notes + +Both `Gossip` and `Gossip::Item` declare `explicit` default constructors, which prevents accidental aggregate-style initialization and makes the zero-initialized `balance` field intentional rather than implicit. The struct requires no destructor, copy constructor, or move semantics beyond defaults, since it holds only a vector of trivially-copyable-like items and a value-type endpoint. \ No newline at end of file diff --git a/include/xrpl/resource/ResourceManager.h.ai.json b/include/xrpl/resource/ResourceManager.h.ai.json new file mode 100644 index 0000000000..2a732ed6ee --- /dev/null +++ b/include/xrpl/resource/ResourceManager.h.ai.json @@ -0,0 +1,78 @@ +{ + "args": [ + { + "lineno": 19, + "name": "address" + }, + { + "lineno": 22, + "name": "address" + }, + { + "lineno": 23, + "name": "proxy" + }, + { + "lineno": 24, + "name": "forwardedFor" + }, + { + "lineno": 27, + "name": "address" + }, + { + "lineno": 30, + "name": "address" + }, + { + "lineno": 39, + "name": "threshold" + }, + { + "lineno": 45, + "name": "origin" + }, + { + "lineno": 45, + "name": "gossip" + }, + { + "lineno": 54, + "name": "collector" + }, + { + "lineno": 54, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "Manager" + } + ], + "description": "Defines the xrpl::Resource::Manager class, an abstract interface for tracking load and resource consumption by endpoints, including methods for creating consumers, exporting/importing consumer data, and reporting usage.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/ResourceManager.h", + "functions": [ + { + "args": [ + "collector", + "journal" + ], + "lineno": 54, + "name": "make_Manager" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/ResourceManager.h.ai.md b/include/xrpl/resource/ResourceManager.h.ai.md new file mode 100644 index 0000000000..d8863ff5f2 --- /dev/null +++ b/include/xrpl/resource/ResourceManager.h.ai.md @@ -0,0 +1,44 @@ +# `include/xrpl/resource/ResourceManager.h` + +## Role in the System + +`ResourceManager.h` declares `xrpl::Resource::Manager`, the abstract interface that governs **load tracking and resource consumption enforcement** for network endpoints in rippled. Its purpose is to protect the node from abuse — whether from a single misbehaving peer hammering expensive RPCs or from a distributed flood — by tracking cumulative resource usage per IP endpoint and signalling when a connection should be warned or cut. + +The interface sits at the boundary between the network layer (which creates connections) and the resource-enforcement logic (which decides when those connections become too costly). Callers never interact with the concrete implementation directly; they receive `Consumer` handles and periodically charge them for work performed. + +## The `Manager` Abstract Interface + +`Manager` inherits from `beast::PropertyStream::Source`, giving every concrete instance a slot in the node's live diagnostic property tree under the name `"resource"`. This inheritance is set up in the `.cpp` file (`Manager::Manager()` passes `"resource"` to the `PropertyStream::Source` constructor), meaning diagnostics are automatically wired in for any conforming implementation. + +The interface exposes four endpoint-creation methods that return `Consumer` objects: + +- **`newInboundEndpoint(address)`** — registers an inbound peer connection keyed purely by its IP address (port is ignored for inbound, since client ports are ephemeral). +- **`newInboundEndpoint(address, proxy, forwardedFor)`** — handles proxy-fronted connections. When `proxy` is true the manager parses the `X-Forwarded-For`-style string to recover the originating IP and tracks that instead of the proxy's address. If the forwarded string is malformed, the implementation falls back to the proxy's own address and logs a warning. This is a deliberate defensive default: a bad forward header never silently bypasses tracking. +- **`newOutboundEndpoint(address)`** — registers an outbound peer. Outbound connections are tracked separately from inbound ones; the underlying `Logic` maintains distinct intrusive lists for each category. +- **`newUnlimitedEndpoint(address)`** — creates a `Consumer` that is permanently exempt from resource limits, used for trusted validators and administrative connections. An "unlimited" entry is never placed on the inbound or outbound lists. + +The returned `Consumer` is a lightweight handle (a pointer pair to a `Logic` and an `Entry`) with value semantics — it is copy-constructible and reference-counted internally. Callers charge it for work via `Consumer::charge(Charge const&)`, which returns a `Disposition` (`ok`, `warn`, or `drop`). + +## Gossip: Cross-Peer Load Sharing + +`exportConsumers()` and `importConsumers()` implement a **gossip protocol** for propagating load information across the peer network. `Gossip` is a simple `vector` where each `Item` holds a `(balance, address)` pair. When a peer tells us that a given IP has been abusive on its end, the local manager can factor that imported balance into its own threshold calculations, making it much harder for an attacker to spread load across many trusted hubs and stay below per-node limits individually. + +`importConsumers` takes an `origin` string that uniquely identifies the peer supplying the data. The `Logic` layer stores these in a separate `importTable_` keyed by origin, so stale data from a disconnected peer can be invalidated cleanly. + +## Concrete Implementation: `ManagerImp` + +`make_Manager()` is the sole factory function and the only way to obtain a concrete instance. It returns a `std::unique_ptr` wrapping `ManagerImp`, which is defined entirely inside `ResourceManager.cpp`. This Pimpl-like separation means the heavy `Logic` header (with its hash maps and intrusive list machinery) is never transitively included by callers of the public interface. + +`ManagerImp` owns: +- A `Logic` instance — the stateful core that holds a `hash_map` plus four intrusive lists (`inbound_`, `outbound_`, `admin_`, `inactive_`) protected by a `std::recursive_mutex`. +- A **background thread** (`Resource::Mngr`) that calls `logic_.periodicActivity()` every second. This periodic sweep decays balances over time (preventing a burst from permanently blacklisting an endpoint) and promotes inactive entries to a cleanup list. The thread uses a `condition_variable` with a one-second timeout so it shuts down promptly when the destructor sets `stop_` and signals the condition. + +The destructor acquires the mutex, flips `stop_`, notifies the condition variable, then joins the thread — a clean shutdown sequence that guarantees the background thread never outlives the `Logic` it references. + +## `Charge` and `Disposition` + +`Charge` is an integer cost with an optional human-readable label (e.g. `"pathfinding"`, `"ledger_request"`). Predefined charges for the standard XRPL operations live in `Fees.h`. Each call to `Consumer::charge()` accumulates cost in the entry's balance and returns one of three `Disposition` values: `ok` (continue), `warn` (throttle warning to the client), or `drop` (disconnect). The `warn` and `drop` transitions are also metered via `beast::insight` counters so operators can monitor rate-limiting activity through the existing stats pipeline. + +## Design Rationale + +The interface is deliberately narrow: callers create consumers and charge them; the manager decides what to do. This separation means the threshold tuning, decay algorithm, and gossip integration are entirely internal to `Logic` and can evolve without touching call sites. The choice to key inbound endpoints by IP alone (rather than `(IP, port)`) is intentional — client ephemeral ports change on reconnection, so port-based tracking would let a misbehaving host escape throttling simply by reconnecting. \ No newline at end of file diff --git a/include/xrpl/resource/Types.h.ai.json b/include/xrpl/resource/Types.h.ai.json new file mode 100644 index 0000000000..5f91b435c4 --- /dev/null +++ b/include/xrpl/resource/Types.h.ai.json @@ -0,0 +1,29 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 6, + "name": "Key" + }, + { + "args": [], + "lineno": 7, + "name": "Entry" + } + ], + "description": "Declares forward declarations for Key and Entry structs within the xrpl::Resource namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/Types.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/Types.h.ai.md b/include/xrpl/resource/Types.h.ai.md new file mode 100644 index 0000000000..62d5ac56ac --- /dev/null +++ b/include/xrpl/resource/Types.h.ai.md @@ -0,0 +1,21 @@ +# `Types.h` — Resource Subsystem Forward Declarations + +Within the `xrpl::Resource` namespace, this file serves as the central forward-declaration header for the two core internal data structures that underpin the resource management subsystem: `Key` and `Entry`. + +## Role and Purpose + +The resource management subsystem tracks load imposed by network endpoints — both inbound client connections and peer-to-peer overlay connections — and applies warnings or disconnections when consumption exceeds configured thresholds. Internally, the subsystem revolves around two structs: + +- **`Key`** (defined in `detail/Key.h`): identifies a unique consumer endpoint, combining a `Kind` classification (inbound, outbound, or admin) with a `beast::IP::Endpoint` network address. It carries nested `hasher` and `key_equal` types so it can serve directly as the key in `hash_map` containers inside `Logic`. + +- **`Entry`** (defined in `detail/Entry.h`): the per-endpoint tracking record. It holds an exponentially decaying `DecayingSample` for local resource consumption, a normalized `remote_balance` contributed by cluster gossip, reference counts from active `Consumer` handles, and timestamps for the last warning and expiry of inactive entries. It also back-references the `Key` it was inserted under. + +## Design Decision: Forward Declarations Over Full Includes + +`Types.h` exposes only forward declarations — `struct Key;` and `struct Entry;` — keeping the full definitions confined to the `detail/` subdirectory. This is a deliberate layering choice: public-facing headers such as `Consumer.h` and `ResourceManager.h` need only to name these types (in pointer or reference position) without requiring their complete definitions, which would drag in heavy dependencies like `beast::IP::Endpoint`, `DecayingSample`, and the intrusive list machinery. The forward-declaration-only header is the lightweight bridge that makes this separation compile cleanly. + +In practice, `Consumer.h` carries its own inline `struct Entry;` forward declaration rather than including `Types.h`, and `Logic.h` pulls in the full `detail/Entry.h` and `detail/Key.h` definitions transitively. This suggests `Types.h` functions as an explicit, canonical declaration point — a statement of what the internal types *are* at the namespace level — rather than an actively included dependency chain anchor. + +## Relationship to the Broader Module + +The resource module is intentionally structured in two visibility tiers. The public API (`Consumer`, `ResourceManager`, `Charge`, `Disposition`, `Fees`) exposes the rate-limiting interface to the rest of `xrpld`. The `detail/` headers hold the implementation machinery. `Types.h` sits at the boundary, giving a name to the two structs that connect those tiers without blurring the separation. \ No newline at end of file diff --git a/include/xrpl/resource/detail/Entry.h.ai.json b/include/xrpl/resource/detail/Entry.h.ai.json new file mode 100644 index 0000000000..6bafd0801f --- /dev/null +++ b/include/xrpl/resource/detail/Entry.h.ai.json @@ -0,0 +1,84 @@ +{ + "args": [ + { + "lineno": 20, + "name": "now" + }, + { + "lineno": 50, + "name": "charge" + }, + { + "lineno": 65, + "name": "os" + }, + { + "lineno": 65, + "name": "v" + } + ], + "classes": [ + { + "args": [ + "now" + ], + "lineno": 14, + "name": "Entry" + } + ], + "description": "Defines the xrpl::Resource::Entry struct, representing an entry in a resource management table for tracking resource consumption, with decaying balance and related metadata.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/detail/Entry.h", + "functions": [ + { + "args": [ + "now" + ], + "lineno": 20, + "name": "Entry" + }, + { + "args": [], + "lineno": 28, + "name": "to_string" + }, + { + "args": [], + "lineno": 36, + "name": "isUnlimited" + }, + { + "args": [ + "now" + ], + "lineno": 44, + "name": "balance" + }, + { + "args": [ + "charge", + "now" + ], + "lineno": 50, + "name": "add" + }, + { + "args": [ + "os", + "v" + ], + "lineno": 65, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/detail/Entry.h.ai.md b/include/xrpl/resource/detail/Entry.h.ai.md new file mode 100644 index 0000000000..fb374ea49a --- /dev/null +++ b/include/xrpl/resource/detail/Entry.h.ai.md @@ -0,0 +1,37 @@ +# `include/xrpl/resource/detail/Entry.h` + +## Role in the Resource Management System + +`Entry` is the per-endpoint accounting record at the heart of XRPL's resource control subsystem. When a remote peer connects — whether inbound, outbound, or an administrative "unlimited" connection — the `Logic` table allocates exactly one `Entry` for it, keyed by `Kind` and IP address. Every charge assessed against that peer is posted here, and every disconnect or warning decision consults the values stored here. The struct is intentionally kept in the `detail` subdirectory because it is an implementation artifact of `Logic`; callers interact with the higher-level `Consumer` handle instead. + +## Intrusive List Membership + +`Entry` inherits from `beast::List::Node`, making it a node in a doubly-linked intrusive list. `Logic` maintains four such lists: `inbound_`, `outbound_`, `admin_`, and `inactive_`. Because the list is intrusive, a given `Entry` can occupy **at most one** list at a time — moving an entry from active to inactive requires explicit removal before re-insertion. This design avoids separate heap allocations for list nodes and gives O(1) traversal and migration, which matters for the periodic expiration sweep that runs across potentially thousands of connections. The comment in `Logic.h` calling out this single-membership invariant is a meaningful warning: violating it corrupts the list structure silently. + +## Balance Tracking: Local vs. Remote + +The core accounting uses two independent components that are summed to produce an effective load level: + +**`local_balance`** is a `DecayingSample` — an exponentially decaying accumulator templated on the 32-second window defined in `Tuning.h`. Each call to `add(charge, now)` ages the existing value by the elapsed time, then adds the new charge. The raw internal value is divided by the window size on every read, yielding a normalized "load rate" rather than a raw cumulative sum. The decay ensures that a burst of activity gradually fades; an entry that goes quiet for more than four window widths (≈128 seconds) is reset to zero entirely. This design correctly models a leaky-bucket rate limit without requiring periodic batch resets. + +**`remote_balance`** is a plain `int` representing load contributed by **gossip imports** from peer nodes. When the local node receives gossip about a misbehaving remote address, it inflates that address's effective balance by writing directly to `remote_balance`. Keeping this separate from `local_balance` is intentional: remote data arrives as a normalized snapshot (not a time-series), so it cannot be fed into the decaying accumulator meaningfully. The two are added together only at query time in `balance()` and `add()`, keeping the accounting models cleanly separated. + +## Key Back-Pointer and Reference Counting + +`key` is a raw pointer back to the `Key` stored as the hash-map key in `Logic::table_`. The comment calls this "a bit of a hack" — the pointer exists solely to make `to_string()` and `isUnlimited()` work without requiring a separate copy of the address and kind. Callers must ensure the `Entry` does not outlive its owning map entry, which is enforced by the `refcount`: the entry stays in the table as long as any `Consumer` holds a reference (`refcount > 0`), and is only eligible for expiration once the count reaches zero and `whenExpires` has passed. + +## `isUnlimited()` and the `kindUnlimited` Exemption + +`isUnlimited()` returns `true` when `key->kind == kindUnlimited`. Entries of this kind represent trusted administrative connections (e.g., local admin RPC callers) that bypass the warning and disconnect thresholds in `Tuning.h` — `warningThreshold = 5000` and `dropThreshold = 25000`. The exemption is checked at the `Logic` layer; `Entry` itself just reports the flag. Notably, even unlimited entries can still be blocked from certain RPC commands based on `Role`, which is a separate authorization layer. + +## Lifecycle Fields + +`lastWarningTime` is stamped when `Logic` emits a load warning to the peer, ensuring warnings are rate-limited and not repeated on every single charge. `whenExpires` is set when an entry's `refcount` drops to zero, marking it for cleanup after `secondsUntilExpiration` (300 seconds) of inactivity. Until that deadline passes, the entry lingers in the `inactive_` intrusive list so that a reconnecting peer can resume its accumulated balance rather than starting fresh — a deliberate anti-abuse measure that prevents short-disconnect-and-reconnect cycles from resetting load state. + +## Relationship to Sibling Files + +- **`Key.h`**: Defines the composite `{Kind, IP::Endpoint}` key that uniquely identifies an entry in the table. `Entry` holds a raw `const Key*` back-pointer. +- **`Kind.h`**: The `kindInbound / kindOutbound / kindUnlimited` enum that drives `isUnlimited()`. +- **`Tuning.h`**: Defines `decayWindowSeconds = 32` (used as the `DecayingSample` template argument), the warning and drop thresholds, and expiration timing constants. +- **`Logic.h`**: The owning component; allocates `Entry` objects into its `hash_map` and moves them between the four intrusive lists as connection state changes. +- **`DecayingSample.h`**: The time-aware accumulator powering `local_balance`. Its integer decay algorithm (`m_value -= (m_value + Window - 1) / Window` per elapsed second) performs ceiling-division decay, ensuring the value reaches zero in finite time rather than asymptotically approaching it. \ No newline at end of file diff --git a/include/xrpl/resource/detail/Import.h.ai.json b/include/xrpl/resource/detail/Import.h.ai.json new file mode 100644 index 0000000000..da2886bdac --- /dev/null +++ b/include/xrpl/resource/detail/Import.h.ai.json @@ -0,0 +1,36 @@ +{ + "args": [ + { + "lineno": 17, + "name": "int" + } + ], + "classes": [ + { + "args": [ + "int" + ], + "lineno": 9, + "name": "Import" + }, + { + "args": [], + "lineno": 11, + "name": "Item" + } + ], + "description": "Defines the Import struct for importing consumer data (with balances and consumers) from a gossip origin, including expiration time and a list of items.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/detail/Import.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/detail/Import.h.ai.md b/include/xrpl/resource/detail/Import.h.ai.md new file mode 100644 index 0000000000..ee0260e480 --- /dev/null +++ b/include/xrpl/resource/detail/Import.h.ai.md @@ -0,0 +1,31 @@ +## `Import.h` — Gossip-Originated Consumer Data Container + +`Import.h` defines the data structure that bridges the gossip protocol and the internal resource-management accounting within the `xrpl::Resource` subsystem. It is a small but architecturally meaningful type: the container through which one XRPL node's observation of peer load is absorbed into another node's own rate-limiting decisions. + +### Context: Why Gossip Imports Exist + +The resource management system tracks per-endpoint load using an exponentially decaying balance (see `Entry.h`). Load observed locally decays over a 32-second window. Without any other signal, a node can only act on behavior it witnesses directly. The gossip mechanism lets a cluster of servers share what they know: peer A tells peer B which inbound endpoints are consuming heavy resources. Peer B can then pre-emptively apply pressure or drop those connections before they accumulate enough local balance to trigger thresholds on their own. + +`Import` is the in-memory representation of one such gossip snapshot, keyed by the originating peer (`origin` string) in the `importTable_` hash map maintained by `Logic`. + +### Structure + +`Import` holds two fields: `whenExpires` (a `clock_type::time_point` controlling how long this snapshot stays valid) and `items` (a `std::vector`). + +Each nested `Item` pairs an `int balance` — the raw load score reported by the gossip origin — with a `Consumer` handle into the local `Entry` table. The `Consumer` is not merely a label; it is a live reference-counted handle to a local `Entry`, whose `remote_balance` field is modified in-place when gossip is imported or expired. This direct coupling is what makes imports efficient: rather than re-scanning all entries, `Logic` can debit the old contribution by walking the items of the expiring `Import` and decrementing `remote_balance` directly on each `Consumer`'s backing `Entry`. + +### The Dummy `int` Constructor + +The `Import(int = 0)` constructor exists to satisfy a subtle requirement from `importTable_.emplace()` in `Logic::importConsumers()`. The call uses `std::piecewise_construct` with `std::make_tuple(m_clock.now().time_since_epoch().count())` as the value arguments. That count is a `long long`, but `Import` only has the `int`-accepting constructor, so the integer implicit conversion path is used. The comment "Dummy argument required for zero-copy construction" signals that this is an in-place construction optimization — the `Import` is constructed directly inside the map node, avoiding a copy. The argument itself is not used; `whenExpires` is set immediately afterwards. + +### How `Logic` Uses `Import` + +`Logic::importConsumers()` processes each incoming `Gossip` and either creates a new `Import` (first-seen origin) or updates an existing one. The update path is particularly careful: new credits are applied to `remote_balance` first, then the old credits are debited. This avoids a window where an entry's `remote_balance` temporarily reads zero, which could briefly misrepresent load levels to concurrent callers evaluating `disposition()`. + +During `periodicActivity()`, any `Import` whose `whenExpires` has passed (30 seconds, per `gossipExpirationSeconds` in `Tuning.h`) has its items walked and their `remote_balance` contributions subtracted before the entry is erased. This rollback is the primary reason each `Item` stores its own `balance` snapshot rather than querying the entry at expiry — the entry's `remote_balance` may have been further modified by subsequent gossip rounds, so the original credited amount must be tracked explicitly to reverse it correctly. + +### Relationship to Sibling Types + +`Gossip.h` defines the wire-format counterpart: `Gossip::Item` carries a `beast::IP::Endpoint address` rather than a live `Consumer`. The conversion from `Gossip::Item` to `Import::Item` in `Logic::importConsumers()` is where an address is looked up or created as an inbound `Entry`, and the resulting `Consumer` handle is stored in the `Import`. `Import` thus acts as the resolved, live form of raw gossip data — it has resolved addresses to local tracking handles and is ready for direct balance manipulation. + +The `Consumer` field in `Import::Item` is intentional: `Consumer` is a reference-counted RAII wrapper around `Entry`. Keeping a `Consumer` alive inside `Import` ensures the underlying `Entry` is not freed during the gossip lifetime window, even if no peer connection is currently active for that endpoint. This is a subtle liveness guarantee: gossip data continues to suppress misbehaving endpoints that may have disconnected locally. \ No newline at end of file diff --git a/include/xrpl/resource/detail/Key.h.ai.json b/include/xrpl/resource/detail/Key.h.ai.json new file mode 100644 index 0000000000..c6c03c28d9 --- /dev/null +++ b/include/xrpl/resource/detail/Key.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 13, + "name": "k" + }, + { + "lineno": 13, + "name": "addr" + } + ], + "classes": [ + { + "args": [ + "Kind k", + "beast::IP::Endpoint const& addr" + ], + "lineno": 8, + "name": "Key" + }, + { + "args": [], + "lineno": 18, + "name": "Key::hasher" + }, + { + "args": [], + "lineno": 29, + "name": "Key::key_equal" + } + ], + "description": "Defines the Key struct used as a consumer key in the XRPL resource management system, including hashing and equality comparison logic based on kind and IP endpoint.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/detail/Key.h", + "functions": [ + { + "args": [ + "Key const& v" + ], + "lineno": 22, + "name": "Key::hasher::operator()" + }, + { + "args": [ + "Key const& lhs", + "Key const& rhs" + ], + "lineno": 33, + "name": "Key::key_equal::operator()" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/detail/Key.h.ai.md b/include/xrpl/resource/detail/Key.h.ai.md new file mode 100644 index 0000000000..a920f6c3e0 --- /dev/null +++ b/include/xrpl/resource/detail/Key.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/resource/detail/Key.h` + +## Role in the Resource Management System + +`Key` is the lookup key used by the XRPL resource manager to uniquely identify and track every network consumer — a peer making an inbound connection, a node on an outbound connection, or an administratively unlimited caller. Within `Logic`, the central consumer-tracking table is declared as: + +```cpp +using Table = hash_map; +``` + +`Key` exists precisely to satisfy the requirements of that `hash_map`: it pairs the two dimensions that distinguish consumers (`Kind` and IP endpoint) and provides the associated `hasher` and `key_equal` types inline as nested structs, following the convention expected by `hash_map` template arguments. + +## Structure and Invariants + +```cpp +struct Key { + Kind kind; + beast::IP::Endpoint address; +}; +``` + +The two fields together form the logical identity of a resource consumer. `Kind` (defined in `Kind.h`) is a three-valued enum — `kindInbound`, `kindOutbound`, `kindUnlimited` — representing the *class* of connection, while `beast::IP::Endpoint` carries the peer's IP address and port. Deleting the default constructor enforces that every `Key` is always fully initialized; a `Key` with a default-constructed (meaningless) endpoint and kind should never exist in the table. + +## Hashing vs. Equality: A Subtle Asymmetry + +The most architecturally interesting aspect of this file is that `hasher` and `key_equal` are *not* symmetric in what they examine: + +- `hasher::operator()` hashes **only on `address`**, ignoring `kind`. +- `key_equal::operator()` compares **both `kind` and `address`**. + +This is valid for an unordered container — hash equality is a necessary but not sufficient condition for key equality — but it has a concrete implication: two consumers at the same IP address but with different `Kind` values (e.g., one inbound and one outbound) will hash to the same bucket but resolve to different entries. The design deliberately treats the same IP address appearing in different roles as separate, independently-tracked consumers, while accepting the mild cost of occasional same-bucket collisions between inbound and outbound entries for the same host. + +Hashing solely on address also keeps the hash computation cheap: `beast::uhash<>` on a `beast::IP::Endpoint` is a well-known, fast path, and adding a branch on `kind` would provide negligible spread benefit since most entries share the dominant kind at any given time. + +## Relationship to `Entry` and `Logic` + +`Entry` (in `Entry.h`) stores a back-pointer `Key const* key` into the map's own key storage. This is acknowledged as "a bit of a hack" in the source but exists for a practical reason: `Entry` needs to query its own `kind` (via `isUnlimited()`) and compose its string fingerprint (via `to_string()`) without a separate copy of the key fields. Because the `Key` lives as the `hash_map`'s key — stable in memory for the lifetime of the entry — the raw pointer is safe, but the coupling is tight: `Entry` cannot outlive the `Table` that owns its `Key`. + +`Logic` uses the `Table` as the single source of truth for all active consumers. When a new peer connects, a `Key` is constructed from the peer's endpoint and connection direction, and `table_.emplace` either finds an existing entry or creates one. The nested `hasher` and `key_equal` types on `Key` mean callers never need to pass custom comparators separately — the types are self-contained, keeping instantiation of the table concise. \ No newline at end of file diff --git a/include/xrpl/resource/detail/Kind.h.ai.json b/include/xrpl/resource/detail/Kind.h.ai.json new file mode 100644 index 0000000000..4756fc4d0f --- /dev/null +++ b/include/xrpl/resource/detail/Kind.h.ai.json @@ -0,0 +1,18 @@ +{ + "args": [], + "classes": [], + "description": "Defines the Kind enum representing different types of consumers (inbound, outbound, unlimited) in the xrpl::Resource namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/detail/Kind.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/detail/Kind.h.ai.md b/include/xrpl/resource/detail/Kind.h.ai.md new file mode 100644 index 0000000000..bc6cc3bbd5 --- /dev/null +++ b/include/xrpl/resource/detail/Kind.h.ai.md @@ -0,0 +1,23 @@ +# `include/xrpl/resource/detail/Kind.h` + +## Role in the Resource Management System + +This file defines the foundational `Kind` enum used throughout the `xrpl::Resource` subsystem to classify every network connection that the XRPL node tracks for resource-consumption purposes. It is a pure vocabulary header — three enumeration values, no logic — but the distinction it encodes drives the entire policy layer of how the node manages load and enforces rate limits. + +## The Three Consumer Kinds + +`Kind` partitions all tracked endpoints into three behaviorally distinct categories: + +**`kindInbound`** represents a connection initiated by a remote peer toward this node. In `Logic.h`, inbound entries are keyed with the port number stripped (`address.at_port(0)`), because many connections from the same IP on different ephemeral ports represent the same logical peer. These consumers have full resource accounting and can be warned, penalized, or disconnected when their load balance exceeds configured thresholds. + +**`kindOutbound`** represents a connection this node initiated to a remote peer. The full address including port is preserved in the key (`address` as-is), since this node itself chose and controls the destination. Outbound peers are also subject to resource limits, but their billing semantics differ from inbound ones because the node trusts them to some degree by virtue of having explicitly connected. + +**`kindUnlimited`** represents a specially privileged inbound connection — typically an administrative or trusted local client — that is exempt from normal resource-consumption limits. The key uses port 1 (`address.at_port(1)`) to separate it from ordinary inbound entries for the same address. While load metering is bypassed, `Entry::isUnlimited()` (in `Entry.h`) reads this flag and the comment is careful to note that administrative RPC restrictions (such as the `stop` command) may still apply based on `Role`, not `Kind`. This separation of concerns is intentional: resource throttling and command authorization are distinct policy axes. + +## Why a Plain `enum` Rather Than `enum class` + +The values are used as unscoped identifiers throughout the detail layer (e.g., `kindInbound`, `kindOutbound`, `kindUnlimited` appear directly in `Logic.h` switch cases and constructor arguments without qualification). An unscoped `enum` keeps the call sites concise inside the `Resource` namespace where these names are always unambiguous. + +## Integration Points + +`Kind` is a member of `Key` (defined in `Key.h`), which pairs it with a `beast::IP::Endpoint` to form the composite lookup key for the consumer table. The `Key::key_equal` comparator checks both fields, meaning the same IP address registered as `kindInbound` and `kindUnlimited` produces two separate, independent table entries — the correct behavior because they represent different trust relationships despite sharing an address. In `Logic.h`, each of the three `Kind` values maps to a dedicated `beast::intrusive_list` (`inbound_`, `outbound_`, `admin_`), enabling the sweeper to age out entries using the appropriate list without a runtime type check on the full entry. \ No newline at end of file diff --git a/include/xrpl/resource/detail/Logic.h.ai.json b/include/xrpl/resource/detail/Logic.h.ai.json new file mode 100644 index 0000000000..481db80e87 --- /dev/null +++ b/include/xrpl/resource/detail/Logic.h.ai.json @@ -0,0 +1,261 @@ +{ + "args": [ + { + "lineno": 38, + "name": "collector" + }, + { + "lineno": 38, + "name": "clock" + }, + { + "lineno": 38, + "name": "journal" + }, + { + "lineno": 53, + "name": "address" + }, + { + "lineno": 75, + "name": "address" + }, + { + "lineno": 99, + "name": "address" + }, + { + "lineno": 128, + "name": "threshold" + }, + { + "lineno": 179, + "name": "origin" + }, + { + "lineno": 179, + "name": "gossip" + }, + { + "lineno": 259, + "name": "balance" + }, + { + "lineno": 270, + "name": "iter" + }, + { + "lineno": 277, + "name": "entry" + }, + { + "lineno": 282, + "name": "entry" + }, + { + "lineno": 308, + "name": "entry" + }, + { + "lineno": 308, + "name": "fee" + }, + { + "lineno": 308, + "name": "context" + }, + { + "lineno": 340, + "name": "entry" + }, + { + "lineno": 359, + "name": "entry" + }, + { + "lineno": 382, + "name": "entry" + }, + { + "lineno": 393, + "name": "now" + }, + { + "lineno": 393, + "name": "items" + }, + { + "lineno": 393, + "name": "list" + }, + { + "lineno": 404, + "name": "map" + } + ], + "classes": [ + { + "args": [ + "collector", + "clock", + "journal" + ], + "lineno": 17, + "name": "Logic" + } + ], + "description": "Implements the core logic for resource management in the XRPL server, handling consumer entries, resource charging, endpoint creation, gossip import/export, and periodic cleanup.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/detail/Logic.h", + "functions": [ + { + "args": [ + "collector", + "clock", + "journal" + ], + "lineno": 38, + "name": "Logic" + }, + { + "args": [], + "lineno": 46, + "name": "~Logic" + }, + { + "args": [ + "address" + ], + "lineno": 53, + "name": "newInboundEndpoint" + }, + { + "args": [ + "address" + ], + "lineno": 75, + "name": "newOutboundEndpoint" + }, + { + "args": [ + "address" + ], + "lineno": 99, + "name": "newUnlimitedEndpoint" + }, + { + "args": [], + "lineno": 123, + "name": "getJson" + }, + { + "args": [ + "threshold" + ], + "lineno": 128, + "name": "getJson" + }, + { + "args": [], + "lineno": 159, + "name": "exportConsumers" + }, + { + "args": [ + "origin", + "gossip" + ], + "lineno": 179, + "name": "importConsumers" + }, + { + "args": [], + "lineno": 221, + "name": "periodicActivity" + }, + { + "args": [ + "balance" + ], + "lineno": 259, + "name": "disposition" + }, + { + "args": [ + "iter" + ], + "lineno": 270, + "name": "erase" + }, + { + "args": [ + "entry" + ], + "lineno": 277, + "name": "acquire" + }, + { + "args": [ + "entry" + ], + "lineno": 282, + "name": "release" + }, + { + "args": [ + "entry", + "fee", + "context" + ], + "lineno": 308, + "name": "charge" + }, + { + "args": [ + "entry" + ], + "lineno": 340, + "name": "warn" + }, + { + "args": [ + "entry" + ], + "lineno": 359, + "name": "disconnect" + }, + { + "args": [ + "entry" + ], + "lineno": 382, + "name": "balance" + }, + { + "args": [ + "now", + "items", + "list" + ], + "lineno": 393, + "name": "writeList" + }, + { + "args": [ + "map" + ], + "lineno": 404, + "name": "onWrite" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + }, + { + "lineno": 13, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/detail/Logic.h.ai.md b/include/xrpl/resource/detail/Logic.h.ai.md new file mode 100644 index 0000000000..619492fc60 --- /dev/null +++ b/include/xrpl/resource/detail/Logic.h.ai.md @@ -0,0 +1,55 @@ +# `include/xrpl/resource/detail/Logic.h` + +## Role in the Resource Management Subsystem + +`Logic` is the central state machine of the XRPL resource-management subsystem. Its job is to track how much work each connected peer or RPC client has imposed on the server and decide whether to warn them or drop the connection. All of the meaningful logic in the `Resource` namespace — endpoint registration, balance tracking, gossip exchange, and periodic garbage collection — lives here. The `Manager` layer that external code interacts with delegates entirely to this class. + +## Entry Lifecycle and the Intrusive List Design + +Every peer or client is represented by an `Entry` object stored in `table_`, a hash map keyed by `Key` (a `{Kind, IP::Endpoint}` pair). The `Logic` class simultaneously maintains four mutually exclusive intrusive lists (`inbound_`, `outbound_`, `admin_`, `inactive_`) that categorize active versus dormant entries. The critical invariant — enforced throughout — is that an `Entry` can belong to at most one list at any moment. Moving an entry between lists always follows a strict remove-then-add sequence. + +Entries are reference-counted by `Consumer` handles. When `refcount` drops to zero in `release()`, the entry is moved from its typed active list into `inactive_` and given a 300-second expiration timestamp (`secondsUntilExpiration`). When `periodicActivity()` runs, any inactive entry whose expiration has passed is fully erased from `table_`. If a new connection arrives for an endpoint that is currently inactive, the entry is rescued from `inactive_` and re-promoted to the appropriate active list before the refcount is incremented — the table_ entry is reused rather than discarded. + +## Three Kinds of Endpoints and Their Key Normalization + +`newInboundEndpoint()` normalizes the address with `at_port(0)`, stripping the ephemeral source port entirely. This means all connections from the same IP address — regardless of which port they originate from — share one `Entry`. This is correct for inbound connections: the limiting unit of concern is the remote IP, not the socket. + +`newOutboundEndpoint()` uses the full address (host + port), which is appropriate since the node itself chose those outbound connections and they represent distinct peers. + +`newUnlimitedEndpoint()` uses `at_port(1)` as an arbitrary sentinel. Unlimited (admin) endpoints are grouped together in `admin_` and bypass enforcement in `warn()` and `disconnect()` via the `isUnlimited()` guard. They still accumulate balances for observability. + +## Balance and Disposition + +Each `Entry` carries two balance components: `local_balance`, an exponentially decaying `DecayingSample` (32-second window), and `remote_balance`, a plain integer representing load reported by other cluster peers via gossip. The composite `balance()` is their sum. + +The `charge()` method adds a fee to `local_balance` using the entry's `add()` method and then calls `disposition()` to classify the result: + +- Below 5000 (`warningThreshold`): `Disposition::ok` +- 5000–24999: `Disposition::warn` +- 25000+ (`dropThreshold`): `Disposition::drop` + +Charge severity also determines log verbosity: costs below 100 log at trace, 100–999 at debug, 1000–2999 at info, 3000+ at warn. This tiering lets operators distinguish casual query load from malformed or expensive requests without flooding logs. + +`warn()` checks if the balance has crossed `warningThreshold` and, if so, applies `feeWarning` to penalize the consumer and records `lastWarningTime`. The time-equality guard (`elapsed != entry.lastWarningTime`) ensures only one warning is issued per clock tick, preventing alarm storms from tight loops. + +`disconnect()` applies `feeDrop` on top of a balance already at or above `dropThreshold`. This is intentional: by inflating the balance at disconnect time, the system ensures that a reconnecting client must first decay down through the penalty before being treated normally again — a brief but effective reconnection backoff without any stateful timer. + +## Gossip: Cross-Node Load Propagation + +The gossip system allows a cluster of XRPL nodes to share load information about shared clients. `exportConsumers()` snapshots inbound entries with `local_balance >= minimumGossipBalance` (1000) and returns them as a `Gossip` value. Only entries above this threshold are exported to avoid propagating noise. + +`importConsumers()` receives gossip from a named origin node and applies it to `remote_balance` fields of local entries. The design handles incremental updates cleanly: when gossip from an already-seen origin arrives, the method first constructs the new set of weighted entries (incrementing their `remote_balance`), then walks the *previous* import set and decrements its `remote_balance` contributions, and finally swaps the new set into place. This add-new-then-remove-old ordering means balances are never transiently under-reported, which matters for enforcement correctness. + +Imported gossip data expires after 30 seconds (`gossipExpirationSeconds`). The `periodicActivity()` method handles expiration of both inactive entries and stale imports, reversing each expired import's remote balance adjustments before deleting it. + +## Concurrency + +The class uses a `std::recursive_mutex` rather than a plain `std::mutex`. The recursion is necessary because `warn()` and `disconnect()` both acquire `lock_` and then call `charge()`, which also acquires it. All public methods — including `acquire()`, `release()`, `getJson()`, `onWrite()`, and the gossip functions — take the lock, making `Logic` safe for concurrent use from multiple threads. + +## Destructor Ordering + +The destructor explicitly clears `importTable_` before `table_`. This order matters because `Import::Item` holds a `Consumer` value, and destroying a `Consumer` calls `Logic::release()`, which attempts to look up and modify an `Entry` in `table_`. If `table_` were destroyed first, those callbacks would access dangling memory. The ordered clear guarantees all import-held `Consumer` handles are properly released before their underlying `Entry` objects disappear. + +## Observability + +`onWrite()` serializes all four entry lists into a `beast::PropertyStream` for diagnostic inspection. `getJson()` (and its threshold-filtered overload) produces a JSON report of any endpoint whose composite balance meets a minimum, categorized by type. The `Stats` inner struct publishes `warn` and `drop` meters to the configured telemetry collector, giving operators a real-time view of enforcement activity. \ No newline at end of file diff --git a/include/xrpl/resource/detail/Tuning.h.ai.json b/include/xrpl/resource/detail/Tuning.h.ai.json new file mode 100644 index 0000000000..1aaa1543ee --- /dev/null +++ b/include/xrpl/resource/detail/Tuning.h.ai.json @@ -0,0 +1,18 @@ +{ + "args": [], + "classes": [], + "description": "Defines tunable constants and expiration durations for resource management in the xrpl::Resource namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/resource/detail/Tuning.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/resource/detail/Tuning.h.ai.md b/include/xrpl/resource/detail/Tuning.h.ai.md new file mode 100644 index 0000000000..64ba0fd376 --- /dev/null +++ b/include/xrpl/resource/detail/Tuning.h.ai.md @@ -0,0 +1,31 @@ +# `include/xrpl/resource/detail/Tuning.h` + +This file is the single authoritative source of every numeric threshold and duration used by the XRPL resource-management subsystem. Rather than scattering magic numbers across the `Logic`, `Entry`, and gossip-handling code, all policy knobs live here so they can be reviewed and adjusted without hunting through implementation files. + +## The Balance Thresholds + +The resource manager maintains a running "balance" for every connected endpoint — a unitless score that rises as the endpoint generates load and decays exponentially over time (via the `DecayingSample` class). Two integer constants gate the two levels of enforcement: + +- `warningThreshold = 5000` — when an endpoint's balance reaches this level, `Logic::disposition()` returns `Disposition::warn`. The server sends the peer a load-warning message and charges it an additional `feeWarning` penalty on the next cycle, adding a small compounding cost to continued misbehaviour. +- `dropThreshold = 25000` — five times the warning threshold. At this level `disposition()` returns `Disposition::drop`, triggering `Logic::disconnect()`, which charges `feeDrop` (making immediate reconnection costly) and severs the connection entirely. + +The 5:1 ratio between warning and drop is deliberate: it gives a legitimately bursty client — one catching up on missed transactions, fetching trust lines, etc. — room to generate elevated load without being immediately disconnected, while still cutting off endpoints that sustain that load long enough to saturate the decaying window. + +## Decay Window + +`decayWindowSeconds = 32` is the half-life window fed as a compile-time template argument to `DecayingSample` inside `Entry`. The comment demands this value be a power of two; that is because `DecayingSample` uses bit-shifts rather than division for its decay arithmetic. Changing this value alters how quickly past charges fade: a smaller window makes the system more reactive (burst tolerance shrinks), a larger window makes it more forgiving. + +## Gossip Filtering + +The cluster-wide load-sharing mechanism (gossip) also relies on constants from this file: + +- `minimumGossipBalance = 1000` — only inbound endpoints with a local balance at or above this level are included when `Logic::exportConsumers()` packages data for peer servers. This prevents the gossip payload from growing with every idle connection; only meaningfully loaded peers are worth advertising. +- `gossipExpirationSeconds = 30` — imported gossip records expire after 30 seconds. When they expire, `periodicActivity()` walks the import table, subtracts the remote balance contributions those records had applied to local `Entry` objects, and removes the record. This short TTL ensures that a cluster member's stale load picture cannot persistently inflate a consumer's perceived balance on other nodes. + +## Inactive Entry Lifetime + +`secondsUntilExpiration = 300` (five minutes) controls how long a zero-refcount `Entry` lingers in the `inactive_` list before `periodicActivity()` erases it from the main hash table. Retaining entries briefly means that a peer who disconnects and immediately reconnects can have its accumulated load recognised — it cannot reset its balance by cycling the TCP connection quickly. The much shorter gossip TTL (30 s) versus entry expiration (300 s) reflects the difference in cost: a stale local entry is cheap to keep, while stale remote load data can cause unnecessary disconnections across the cluster. + +## Design Note + +The integer constants are collected into a single anonymous `enum` rather than individual `constexpr int` values. This is a common C++ idiom for compile-time integer constants that avoids ODR concerns with `constexpr` variables in headers predating C++17 inline variables. The two `std::chrono::seconds` constants (`secondsUntilExpiration` and `gossipExpirationSeconds`) are `constexpr` at namespace scope because they need to be added directly to `clock_type::time_point` values, which require a typed duration rather than a bare integer. \ No newline at end of file diff --git a/include/xrpl/server/Handoff.h.ai.json b/include/xrpl/server/Handoff.h.ai.json new file mode 100644 index 0000000000..63c4e55e4f --- /dev/null +++ b/include/xrpl/server/Handoff.h.ai.json @@ -0,0 +1,26 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "Handoff" + } + ], + "description": "Defines the Handoff struct used to indicate the result of a server connection handoff in the xrpl namespace, along with HTTP request/response type aliases.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/Handoff.h", + "functions": [ + { + "args": [], + "lineno": 23, + "name": "handled" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/Handoff.h.ai.md b/include/xrpl/server/Handoff.h.ai.md new file mode 100644 index 0000000000..7e09e4bd65 --- /dev/null +++ b/include/xrpl/server/Handoff.h.ai.md @@ -0,0 +1,55 @@ +# `include/xrpl/server/Handoff.h` + +## Role in the System + +`Handoff.h` defines the `Handoff` struct — a lightweight result type that encodes the outcome of a server's connection handoff decision. It is the return value of `onHandoff()` callbacks throughout the XRPL server layer, and it also establishes the canonical type aliases `http_request_type` and `http_response_type` (both wrapping Boost.Beast's `dynamic_body` variants) that are used across the entire server module. + +The handoff concept exists because the XRPL server must classify each inbound HTTP connection at the moment a request arrives: is it a regular RPC call, a WebSocket upgrade, a peer-protocol connection, or a status probe? Each of these paths has a fundamentally different lifetime model for the underlying socket and response, so a single return value needs to express all of those outcomes cleanly to the dispatch layer. + +## The `Handoff` Struct + +The struct carries three fields that together encode which of three possible outcomes the handler has chosen: + +**`moved` (bool, default `false`)**: Signals that the handler has taken ownership of the underlying socket via `std::move`. When this is `true`, the `Session` layer must relinquish the socket and stop touching it — the handler is now responsible for its lifetime. This path is taken, for example, when `ServerHandler::onHandoff()` detects a WebSocket upgrade request and hands the socket off to a `PlainWSPeer` or `SSLWSPeer`, or when it routes a peer-protocol connection to the Overlay subsystem. + +**`response` (`std::shared_ptr`, default null)**: When non-null, the session should serialize and send this response back to the client rather than forwarding the request to higher-level RPC processing. The `Writer` abstraction (from `Writer.h`) provides an async-friendly pull-style interface with `prepare()`, `consume()`, and `data()` methods, so the response can be streamed in chunks without blocking the I/O thread. + +**`keep_alive` (bool, default `false`)**: Meaningful only when `response` is set. It tells the session layer whether to close the TCP connection after the response is flushed or to linger waiting for the next request. This mirrors the HTTP/1.1 `Connection: keep-alive` semantics. + +## The `handled()` Predicate + +```cpp +bool handled() const { return moved || response; } +``` + +This one-liner is the discriminated-union test: a `Handoff` is considered "handled" if the connection was taken over (`moved`) or if there is an inline response to send. A zero-value `Handoff{}` — both fields false and `response` null — signals to the dispatch layer that the handler did not recognize or claim the request, allowing the session to fall through to legacy `onRequest()` processing. + +## Dispatch Logic in Context + +In `PlainHTTPPeer::do_request()`, the dispatcher calls `handler_.onHandoff()` and then reads the returned `Handoff` in a concise three-way branch: + +```cpp +auto const what = this->handler_.onHandoff(...); +if (what.moved) return; // socket taken, do nothing +if (what.response) { // send inline response + if (!what.keep_alive) socket_.shutdown(...); + return this->write(what.response, what.keep_alive); +} +// else: fall through to handler_.onRequest() +``` + +This pattern avoids callbacks, virtual dispatch on the result type, or exceptions for routing decisions. The value is cheap (two bools and a `shared_ptr`) and its semantics are self-documenting at each call site. + +## How Producers Construct `Handoff` Values + +`ServerHandler::onHandoff()` is the primary producer. It examines the incoming request and returns: + +- `Handoff{.moved = true}` for WebSocket and peer-protocol upgrades, where ownership of the socket transfers to another subsystem. +- `Handoff{.response = writer, .keep_alive = ...}` for status requests (HTTP 200/500 load-balancing probes) and other self-contained HTTP responses that don't require the full RPC pipeline. +- A default `Handoff{}` (`handled() == false`) when none of these early-exit paths match, which causes the connection to proceed to normal RPC request processing via `onRequest()`. + +The `OverlayImpl` similarly uses `Handoff` when its own `onHandoff()` is called to service peer HTTP endpoints (`/crawl`, `/vl/`, `/health`). Each of these `process*()` helpers populates `handoff.response` with a `SimpleWriter` wrapping an HTTP response and returns `true`, causing the caller to return the filled `Handoff` immediately. + +## Design Rationale + +The key non-obvious choice is representing the outcome as a value type rather than an output parameter, a virtual method, or a `std::variant`. Because the struct is trivially constructible and holds only a `shared_ptr` plus two bools, returning it by value is zero-overhead relative to the I/O work that follows. The `handled()` predicate provides a single boolean gate that works regardless of which specific outcome was chosen, which is useful for early-exit tests in tests and in the overlay dispatch loop. The fact that `moved` and a non-null `response` are logically mutually exclusive is enforced by convention at the call sites, not by the type system — a deliberate trade-off for simplicity given the small number of producers. \ No newline at end of file diff --git a/include/xrpl/server/InfoSub.h.ai.json b/include/xrpl/server/InfoSub.h.ai.json new file mode 100644 index 0000000000..d006d95f76 --- /dev/null +++ b/include/xrpl/server/InfoSub.h.ai.json @@ -0,0 +1,386 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "InfoSubRequest" + }, + { + "args": [ + "Source& source" + ], + "lineno": 27, + "name": "InfoSub" + }, + { + "args": [], + "lineno": 33, + "name": "Source" + } + ], + "description": "Defines classes and interfaces for managing client subscriptions to data feeds in the XRPL server, including subscription sources, account and ledger subscriptions, and request handling.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/InfoSub.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "doClose" + }, + { + "args": [ + "Json::Value const&" + ], + "lineno": 20, + "name": "doStatus" + }, + { + "args": [ + "ref ispListener", + "hash_set const& vnaAccountIDs", + "bool realTime" + ], + "lineno": 34, + "name": "subAccount" + }, + { + "args": [ + "ref isplistener", + "hash_set const& vnaAccountIDs", + "bool realTime" + ], + "lineno": 39, + "name": "unsubAccount" + }, + { + "args": [ + "std::uint64_t uListener", + "hash_set const& vnaAccountIDs", + "bool realTime" + ], + "lineno": 44, + "name": "unsubAccountInternal" + }, + { + "args": [ + "ref ispListener", + "AccountID const& account" + ], + "lineno": 53, + "name": "subAccountHistory" + }, + { + "args": [ + "ref ispListener", + "AccountID const& account", + "bool historyOnly" + ], + "lineno": 62, + "name": "unsubAccountHistory" + }, + { + "args": [ + "std::uint64_t uListener", + "AccountID const& account", + "bool historyOnly" + ], + "lineno": 70, + "name": "unsubAccountHistoryInternal" + }, + { + "args": [ + "ref ispListener", + "Json::Value& jvResult" + ], + "lineno": 75, + "name": "subLedger" + }, + { + "args": [ + "std::uint64_t uListener" + ], + "lineno": 76, + "name": "unsubLedger" + }, + { + "args": [ + "ref ispListener" + ], + "lineno": 78, + "name": "subBookChanges" + }, + { + "args": [ + "std::uint64_t uListener" + ], + "lineno": 79, + "name": "unsubBookChanges" + }, + { + "args": [ + "ref ispListener" + ], + "lineno": 81, + "name": "subManifests" + }, + { + "args": [ + "std::uint64_t uListener" + ], + "lineno": 82, + "name": "unsubManifests" + }, + { + "args": [ + "Manifest const&" + ], + "lineno": 83, + "name": "pubManifest" + }, + { + "args": [ + "ref ispListener", + "Json::Value& jvResult", + "bool admin" + ], + "lineno": 85, + "name": "subServer" + }, + { + "args": [ + "std::uint64_t uListener" + ], + "lineno": 86, + "name": "unsubServer" + }, + { + "args": [ + "ref ispListener", + "Book const&" + ], + "lineno": 88, + "name": "subBook" + }, + { + "args": [ + "std::uint64_t uListener", + "Book const&" + ], + "lineno": 89, + "name": "unsubBook" + }, + { + "args": [ + "ref ispListener" + ], + "lineno": 91, + "name": "subTransactions" + }, + { + "args": [ + "std::uint64_t uListener" + ], + "lineno": 92, + "name": "unsubTransactions" + }, + { + "args": [ + "ref ispListener" + ], + "lineno": 94, + "name": "subRTTransactions" + }, + { + "args": [ + "std::uint64_t uListener" + ], + "lineno": 95, + "name": "unsubRTTransactions" + }, + { + "args": [ + "ref ispListener" + ], + "lineno": 97, + "name": "subValidations" + }, + { + "args": [ + "std::uint64_t uListener" + ], + "lineno": 98, + "name": "unsubValidations" + }, + { + "args": [ + "ref ispListener" + ], + "lineno": 100, + "name": "subPeerStatus" + }, + { + "args": [ + "std::uint64_t uListener" + ], + "lineno": 102, + "name": "unsubPeerStatus" + }, + { + "args": [ + "std::function const&" + ], + "lineno": 103, + "name": "pubPeerStatus" + }, + { + "args": [ + "ref ispListener" + ], + "lineno": 105, + "name": "subConsensus" + }, + { + "args": [ + "std::uint64_t uListener" + ], + "lineno": 106, + "name": "unsubConsensus" + }, + { + "args": [ + "std::string const& strUrl" + ], + "lineno": 112, + "name": "findRpcSub" + }, + { + "args": [ + "std::string const& strUrl", + "ref rspEntry" + ], + "lineno": 113, + "name": "addRpcSub" + }, + { + "args": [ + "std::string const& strUrl" + ], + "lineno": 114, + "name": "tryRemoveRpcSub" + }, + { + "args": [ + "Source& source" + ], + "lineno": 118, + "name": "InfoSub" + }, + { + "args": [ + "Source& source", + "Consumer consumer" + ], + "lineno": 119, + "name": "InfoSub" + }, + { + "args": [], + "lineno": 121, + "name": "~InfoSub" + }, + { + "args": [], + "lineno": 123, + "name": "getConsumer" + }, + { + "args": [ + "Json::Value const& jvObj", + "bool broadcast" + ], + "lineno": 126, + "name": "send" + }, + { + "args": [], + "lineno": 128, + "name": "getSeq" + }, + { + "args": [], + "lineno": 130, + "name": "onSendEmpty" + }, + { + "args": [ + "AccountID const& account", + "bool rt" + ], + "lineno": 132, + "name": "insertSubAccountInfo" + }, + { + "args": [ + "AccountID const& account", + "bool rt" + ], + "lineno": 134, + "name": "deleteSubAccountInfo" + }, + { + "args": [ + "AccountID const& account" + ], + "lineno": 137, + "name": "insertSubAccountHistory" + }, + { + "args": [ + "AccountID const& account" + ], + "lineno": 139, + "name": "deleteSubAccountHistory" + }, + { + "args": [], + "lineno": 141, + "name": "clearRequest" + }, + { + "args": [ + "std::shared_ptr const& req" + ], + "lineno": 143, + "name": "setRequest" + }, + { + "args": [], + "lineno": 145, + "name": "getRequest" + }, + { + "args": [ + "unsigned int apiVersion" + ], + "lineno": 147, + "name": "setApiVersion" + }, + { + "args": [], + "lineno": 149, + "name": "getApiVersion" + }, + { + "args": [], + "lineno": 159, + "name": "assign_id" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/InfoSub.h.ai.md b/include/xrpl/server/InfoSub.h.ai.md new file mode 100644 index 0000000000..ff0a254db9 --- /dev/null +++ b/include/xrpl/server/InfoSub.h.ai.md @@ -0,0 +1,42 @@ +# `include/xrpl/server/InfoSub.h` + +## Role in the System + +This header defines the core abstraction for client subscriptions in the XRPL server. When a client connects via WebSocket or JSON-RPC and issues a `subscribe` command, an `InfoSub` instance is created to represent that subscriber's ongoing session. The file defines three cooperating types: `InfoSubRequest`, `InfoSub`, and its inner `InfoSub::Source`. Together they implement a publish-subscribe interface between individual connected clients and the network event pipeline operated by `NetworkOPs`. + +## `InfoSubRequest`: In-Flight Request Tracking + +`InfoSubRequest` is a small abstract base class for requests that persist across multiple asynchronous callbacks — notably path-find operations, which stream progressive results until closed. It exposes `doClose()` to terminate the request and `doStatus()` to query its current state, both returning `Json::Value` for serialization directly to the client. Inheriting from `CountedObject` makes live instances visible in the node's diagnostic counters. The `shared_ptr` stored inside `InfoSub` via `setRequest()` ensures the in-flight computation is tied to the subscriber's lifetime. + +## `InfoSub`: The Subscriber Object + +`InfoSub` represents a single connected client endpoint. Its design centers on three responsibilities: maintaining a unique identity, tracking which feeds the client has subscribed to, and enforcing resource consumption limits. + +**Identity via atomic sequence number.** The private `assign_id()` function increments a `static std::atomic` and assigns the result to `mSeq` at construction time. This integer ID serves as the stable, non-owning identifier used in all "Internal" unsubscription calls during destruction, where passing a `shared_ptr` would be unsafe (the object is being torn down). The public `getSeq()` accessor exposes this to the `Source`. + +**Subscription bookkeeping on the subscriber side.** `InfoSub` maintains three `hash_set` members: `realTimeSubscriptions_` (transactions as they are applied, pre-consensus), `normalSubscriptions_` (validated/confirmed transactions), and `accountHistorySubscriptions_` (historical replay feeds). The methods `insertSubAccountInfo`, `deleteSubAccountInfo`, `insertSubAccountHistory`, and `deleteSubAccountHistory` mutate these sets; the destructor uses them to call back into the source for automatic cleanup. All mutations are protected by the `protected` `std::mutex mLock`. + +**Resource management.** Each `InfoSub` holds a `Resource::Consumer` — the rate-limiting token from the resource subsystem. Callers obtain it via `getConsumer()` and can charge load fees (`charge()`), check `disposition()` to decide whether to warn or disconnect, and call `disconnect()` if the client is misbehaving. + +**API versioning.** The `apiVersion_` field stores the API version negotiated at connection time. It starts at 0 and `getApiVersion()` asserts (via `XRPL_ASSERT`) that it is greater than zero before returning, catching cases where the version was never initialized. This drives downstream logic in the `send()` dispatch path so serialization can adapt to different client expectations. + +**The pure virtual `send()`.** The abstract `send(Json::Value const& jvObj, bool broadcast)` is the outbound delivery point. Subclasses implement it to push the JSON payload over their specific transport. `RPCSub` (defined in `src/xrpld/rpc/RPCSub.h`) handles HTTP callback delivery, while the WebSocket subclass handles framed WebSocket writes. The `broadcast` flag signals whether the message is being multicasted to many subscribers simultaneously, which may affect buffering behavior. + +## `InfoSub::Source`: The Publisher Interface + +`Source` is a pure abstract inner class that `InfoSub` depends on but doesn't implement. In practice, `NetworkOPs` is the sole implementation — it owns the authoritative maps that associate feed types (ledger, book, validation, peer status, consensus, transactions, manifests) to the set of current subscribers. + +Every subscription method comes in two forms that reveal an important design split: + +- **Normal-operation form**: accepts `ref ispListener` (i.e., `shared_ptr const&`). This is called while the subscriber is alive, so it can be inserted into or removed from the source's internal maps by shared pointer. +- **"Internal" destruction form**: accepts `std::uint64_t uListener` (the raw `mSeq`). This is called exclusively from `InfoSub::~InfoSub()`, where passing a `shared_ptr` would be unsafe because the object's control block is being unwound. The destructor iterates its local subscription sets and calls these internal variants to remove itself from the `NetworkOPs` maps without risking a dangling reference. + +This two-level unsubscription API is the key correctness invariant of the design. Normal `unsubXxx` methods clean up on both the `InfoSub` side and the `Source` side. The destructor only needs to clean up the `Source` side (the `InfoSub`'s own sets are about to be destroyed anyway), hence the `unsubXxxInternal` variants that bypass the subscriber's state. + +**Account history subscriptions** add a third mode of operation: `subAccountHistory()` streams past transactions to a client and returns an error code. `unsubAccountHistory()` accepts a `historyOnly` flag — when `true`, the client stops receiving historical replay data but continues receiving new confirmed transactions. This graduated exit supports the common usage pattern where clients first replay account history then transition to a live feed. + +**URL-based RPC subscriptions.** The `findRpcSub/addRpcSub/tryRemoveRpcSub` trio manages a secondary index keyed by URL string. The inline comment acknowledges this as a legacy feature ("added for one particular partner") that should eventually be removed. It enables server-push semantics over plain HTTP callbacks rather than WebSocket. + +## Relationship to Sibling Files + +`InfoSub` forms the bridge between the server transport layer (`Session`, `WSSession`) and the application event pipeline (`NetworkOPs`). `NetworkOPs.h` declares the concrete `Source` implementation. `RPCSub.h` shows the only concrete `InfoSub` subclass visible at the library boundary — it adds `setUsername`/`setPassword` for authenticated callback delivery. The `CountedObject` mixin connects both `InfoSubRequest` and `InfoSub` to the global instance-count diagnostics exposed via `CountedObjects::getCounts()`. \ No newline at end of file diff --git a/include/xrpl/server/LoadFeeTrack.h.ai.json b/include/xrpl/server/LoadFeeTrack.h.ai.json new file mode 100644 index 0000000000..c7ba68f292 --- /dev/null +++ b/include/xrpl/server/LoadFeeTrack.h.ai.json @@ -0,0 +1,46 @@ +{ + "args": [ + { + "lineno": 20, + "name": "journal" + }, + { + "lineno": 27, + "name": "f" + }, + { + "lineno": 74, + "name": "fee" + } + ], + "classes": [ + { + "args": [ + "journal" + ], + "lineno": 17, + "name": "LoadFeeTrack" + } + ], + "description": "Manages the current fee schedule for XRP transactions, including base, local, remote, and cluster fees, and provides utilities for scaling fees based on server load.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/LoadFeeTrack.h", + "functions": [ + { + "args": [ + "fee", + "feeTrack", + "fees", + "bUnlimited" + ], + "lineno": 87, + "name": "scaleFeeLoad" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/LoadFeeTrack.h.ai.md b/include/xrpl/server/LoadFeeTrack.h.ai.md new file mode 100644 index 0000000000..d00a798534 --- /dev/null +++ b/include/xrpl/server/LoadFeeTrack.h.ai.md @@ -0,0 +1,41 @@ +# `LoadFeeTrack.h` — Dynamic Transaction Fee Scaling + +`LoadFeeTrack` is the node-local component responsible for tracking and adjusting the transaction fee multiplier in response to server load. It sits at the intersection of the consensus protocol, the job queue, and the RPC layer: every transaction that passes through rippled must be validated against the effective fee that this class computes. + +## The Three-Factor Model + +The XRPL fee system separates *base fee* (a ledger-consensus parameter stored in `Fees::base`) from a *load factor* that reflects how busy a particular server or cluster currently is. `LoadFeeTrack` manages three independent load-factor dimensions, each stored as a `uint32_t` scale value where `lftNormalFee = 256` represents the baseline (1× multiplier): + +- **`localTxnLoadFee_`**: this server's own load pressure, driven by job-queue depth. +- **`remoteTxnLoadFee_`**: the load factor reported by connected peers, set externally via `setRemoteFee()`. +- **`clusterTxnLoadFee_`**: the highest fee reported within the server's validator cluster, set via `setClusterFee()`. + +The effective fee presented to the network is `getLoadFactor()`, which returns `max(cluster, local, remote)`. This conservatively-pessimistic design ensures that even a lightly loaded node will not accept transactions at below-network rates when the broader network or cluster is under stress. + +## Asymmetric Raise/Lower Mechanics and Hysteresis + +`raiseLocalFee()` and `lowerLocalFee()` implement deliberate asymmetry. The raise path starts from `max(local, remote)` — it "catches up" to the remote factor before adding 25% (`localTxnLoadFee_ + localTxnLoadFee_ / 4`). Critically, it requires two consecutive raise calls before the fee actually increases: `raiseCount_` must reach 2 before the multiplier moves. This hysteresis prevents a single transient overload spike from immediately penalizing users. The fee is hard-capped at `lftNormalFee * 1,000,000` (256 million), which translates to roughly 1 million times the base fee. + +The lower path is more direct: it always reduces by 25% toward the floor (`lftNormalFee`), and it resets `raiseCount_` to zero so a subsequent raise must again accumulate two ticks. The floor ensures the fee never drops below the ledger-consensus base rate regardless of how many lower operations are applied. + +`LoadManager` drives this machinery once per second: it calls `raiseLocalFee()` when `JobQueue::isOverloaded()` is true, and `lowerLocalFee()` otherwise. When either function reports a change (returns `true`), `NetworkOPs::reportFeeChange()` is triggered to broadcast the updated fee to subscribers. + +## The `scaleFeeLoad()` Free Function + +The companion function `scaleFeeLoad(fee, feeTrack, fees, bUnlimited)` applies the load factor to a specific fee amount using integer arithmetic without overflow, via `mulDiv`: + +``` +scaled_fee = fee * feeFactor / lftNormalFee +``` + +`getScalingFactors()` returns two values: `feeFactor = max(local, remote)` and `uRemFee = max(remote, cluster)`. The split matters for the `bUnlimited` path. Privileged clients (internal RPC users, server operators) receive a discount: if `feeFactor > uRemFee` but `feeFactor < 4 * uRemFee`, the privileged client is charged only the network/cluster rate rather than the locally-elevated rate. The threshold of 4× represents an explicit policy choice — trusted users absorb normal network load pressure but are shielded from moderate local overload, only paying the full rate when the server is under extreme stress relative to the network. + +## Thread Safety + +All mutable state (`localTxnLoadFee_`, `remoteTxnLoadFee_`, `clusterTxnLoadFee_`, `raiseCount_`) is protected by a `mutable std::mutex`. The `const` getters lock the same mutex, making concurrent reads and writes safe without requiring the caller to hold any external lock. This is intentional: `LoadManager` modifies the local fee from its own thread while the RPC and overlay layers read it concurrently from their threads. + +## Relationship to `Fees` + +`Fees` (from `include/xrpl/protocol/Fees.h`) stores the ledger-consensus parameters: `base`, `reserve`, and `increment`. These are fixed per-ledger and do not change mid-ledger. `LoadFeeTrack` is orthogonal — it holds the *multiplier* that scales `Fees::base` to reflect current server conditions. `scaleFeeLoad()` brings them together: the input `fee` is typically derived from `Fees::base`, and the output is the actual drop cost a transaction must pay under current load. + +The `isLoadedLocal()` and `isLoadedCluster()` predicates are used by higher layers (e.g., `RCLConsensus`, `LedgerCleaner`) to gate expensive operations or shed non-critical work when the server is under pressure, providing a back-pressure signal beyond pure fee elevation. \ No newline at end of file diff --git a/include/xrpl/server/Manifest.h.ai.json b/include/xrpl/server/Manifest.h.ai.json new file mode 100644 index 0000000000..a986c229f5 --- /dev/null +++ b/include/xrpl/server/Manifest.h.ai.json @@ -0,0 +1,116 @@ +{ + "args": [ + { + "lineno": 65, + "name": "serialized_" + }, + { + "lineno": 66, + "name": "masterKey_" + }, + { + "lineno": 67, + "name": "signingKey_" + }, + { + "lineno": 68, + "name": "seq" + }, + { + "lineno": 69, + "name": "domain_" + } + ], + "classes": [ + { + "args": [], + "lineno": 49, + "name": "Manifest" + }, + { + "args": [], + "lineno": 141, + "name": "ValidatorToken" + }, + { + "args": [ + "j" + ], + "lineno": 178, + "name": "ManifestCache" + } + ], + "description": "Defines structures and utilities for managing XRPL validator manifests, including manifest serialization, verification, revocation, and a cache for tracking the most recent valid manifests for each validator.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/Manifest.h", + "functions": [ + { + "args": [ + "m" + ], + "lineno": 97, + "name": "to_string" + }, + { + "args": [ + "s", + "journal" + ], + "lineno": 109, + "name": "deserializeManifest" + }, + { + "args": [ + "s", + "journal" + ], + "lineno": 113, + "name": "deserializeManifest" + }, + { + "args": [ + "v", + "journal" + ], + "lineno": 120, + "name": "deserializeManifest" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 130, + "name": "operator==" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 137, + "name": "operator!=" + }, + { + "args": [ + "blob", + "journal" + ], + "lineno": 144, + "name": "loadValidatorToken" + }, + { + "args": [ + "m" + ], + "lineno": 163, + "name": "to_string" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/Manifest.h.ai.md b/include/xrpl/server/Manifest.h.ai.md new file mode 100644 index 0000000000..14ae8824bb --- /dev/null +++ b/include/xrpl/server/Manifest.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/server/Manifest.h` + +## Role in the System + +This header defines the two-layer validator key infrastructure that allows XRPL validators to rotate their signing keys without requiring a network-wide configuration change. Without this mechanism, a compromised validator signing key would force every node on the network to update its UNL (Unique Node List) with the new public key simultaneously — a severe operational and security problem. The manifest system introduces an offline master key that issues signed certificates ("manifests") delegating signing authority to an online ephemeral key. The master key can be kept in cold storage, while the ephemeral key does the real-time work of signing validations. + +## The `Manifest` Struct + +`Manifest` holds the deserialized representation of a single validator certificate. Its fields capture the logical content of the certificate: `masterKey` (the long-lived identity key), `signingKey` (the current online key, absent on revocation manifests), a monotonically increasing `sequence` number, an optional `domain` string for validator identification, and the raw `serialized` bytes that include the digital signatures. + +Copy construction and copy assignment are deliberately deleted. Moving a manifest is cheap and safe (the serialized blob stays contiguous in memory); copying it would create ambiguity about which copy is authoritative, and there is no use case that requires it. + +The `verify()` method does two rounds of signature verification on the deserialized object. For non-revocation manifests it first verifies the ephemeral key's signature over the manifest body (`sfSignature`), then verifies the master key's signature (`sfMasterSignature`). A revocation manifest skips the ephemeral key check because no signing key is present. The implementation re-deserializes `serialized` to an `STObject` on each call rather than caching the parse result — this keeps the struct small and avoids extra lifetime concerns. + +Revocation is encoded as the sentinel sequence number `0xFFFFFFFF` (`std::numeric_limits::max()`). The choice of using the maximum value as the sentinel is elegant: it is impossible to supersede a revocation manifest because no higher sequence number exists. Both the static `revoked(uint32_t)` and the instance `revoked()` method expose this check; the static form lets callers test a candidate sequence number during deserialization before constructing the full `Manifest` object. + +## Deserialization + +`deserializeManifest` is a family of three overloads (accepting `Slice`, `std::string`, or `std::vector`) all funneling through the `Slice` variant. The implementation uses an inline `static SOTemplate` that defines the XRPL binary encoding schema for a manifest: mandatory `sfPublicKey`, `sfMasterSignature`, and `sfSequence`; optional `sfSigningPubKey`, `sfSignature`, and `sfDomain`; plus a `sfVersion` field that gates forward compatibility (only version 0 is currently understood). + +The function deliberately separates parsing from signature verification, returning a valid `Manifest` object without having checked the signatures. The caller must separately invoke `Manifest::verify()`. This design makes sense because the `ManifestCache` wants to perform the expensive cryptographic check only under a lightweight read lock, deferring the result-based write to a separate exclusive lock acquisition — an optimization that would be impossible if deserialization automatically verified. + +The domain string is validated against `isProperlyFormedTomlDomain()` during deserialization, preventing manifests with syntactically invalid domain claims from ever entering the system. The sanity check that the signing key and master key cannot be identical (`*signingKey == masterKey`) prevents a degenerate manifest where the two key layers collapse into one. + +## `ValidatorToken` + +`ValidatorToken` is a minimal bootstrap struct that pairs the local validator node's manifest (as a base64-encoded string) with its `SecretKey` for signing validations. It is loaded from the `[validator_token]` config entry via `loadValidatorToken()`, which base64-decodes the blob and parses it as JSON with `manifest` and `validation_secret_key` fields. This is the mechanism by which a validator operator configures their node — the manifest within the token is added to the local `ManifestCache` and the secret key is used to sign live validation messages. + +## `ManifestDisposition` + +This enum encodes the five outcomes of attempting to add a manifest to the cache: `accepted`, `stale` (sequence not strictly greater than what's known), `badMasterKey` (master key is already used as someone else's ephemeral key), `badEphemeralKey` (ephemeral key reused across validators), and `invalid` (cryptographic verification failed). The string conversion `to_string(ManifestDisposition)` supports structured logging of these outcomes without string-based branching at call sites. + +## `ManifestCache` + +`ManifestCache` is the runtime registry that maps validator master public keys to their current best manifest. It maintains two indexes: `map_` keyed by master public key (giving quick access to the current signing key and manifest metadata), and `signingToMasterKeys_` keyed by ephemeral signing key (providing the reverse lookup, used by `getMasterKey()`). This bidirectional structure allows the overlay code to receive a validation signed by an ephemeral key and efficiently resolve it back to the master key identity that appears on the trust list. + +The concurrency design deserves close attention. `applyManifest()` implements a double-checked locking pattern without an upgradable lock. It first runs `prewriteCheck` under a `std::shared_lock`, including the expensive `Manifest::verify()` call. If that passes, it releases the read lock and acquires a `std::unique_lock`, then re-runs `prewriteCheck` (without the signature check, since it has already passed) to guard against racing writers in the window between the two locks. The code comments explicitly note that an upgradable lock was considered and rejected as a deadlock risk — a correct choice given that `std::shared_mutex` does not support lock upgrades without releasing. + +The atomic `seq_` counter is a lightweight change-detection signal. It is incremented each time `applyManifest` successfully installs a new or updated manifest. External callers (e.g., `ValidatorList`) can snapshot this value and check it cheaply to detect whether new manifest data has arrived since they last synchronized, without holding the mutex. This avoids polling under lock for what is essentially a notification problem. + +The two `for_each_manifest` template overloads acquire a `shared_lock` for the duration of their iteration, explicitly warning callers not to call any `ManifestCache` member functions from within the callback (which would attempt to re-acquire `mutex_` from the same thread, causing undefined behavior). The two-parameter overload allows a caller to first receive the total count via `pf(map_.size())` before iterating — useful for pre-allocating output buffers without two separate lock acquisitions. + +`load()` and `save()` handle persistence against the database. On startup, the full overload of `load()` first reads all previously persisted manifests from the database table, then applies the locally configured `[validator_token]` manifest, then applies any `[validator_key_revocation]` entry. This ordering ensures database-cached gossip manifests don't accidentally win over the operator's own current configuration. \ No newline at end of file diff --git a/include/xrpl/server/NetworkOPs.h.ai.json b/include/xrpl/server/NetworkOPs.h.ai.json new file mode 100644 index 0000000000..9d4c7d701a --- /dev/null +++ b/include/xrpl/server/NetworkOPs.h.ai.json @@ -0,0 +1,306 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 49, + "name": "NetworkOPs" + } + ], + "description": "Defines the NetworkOPs interface, which provides core server functionality for clients interacting with the XRPL network, including transaction processing, ledger management, and network state tracking.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/NetworkOPs.h", + "functions": [ + { + "args": [ + "noMeansDont" + ], + "lineno": 44, + "name": "doFailHard" + }, + { + "args": [], + "lineno": 48, + "name": "~NetworkOPs" + }, + { + "args": [], + "lineno": 50, + "name": "stop" + }, + { + "args": [], + "lineno": 56, + "name": "getOperatingMode" + }, + { + "args": [ + "mode", + "admin" + ], + "lineno": 57, + "name": "strOperatingMode" + }, + { + "args": [ + "admin" + ], + "lineno": 58, + "name": "strOperatingMode" + }, + { + "args": [ + "const&" + ], + "lineno": 63, + "name": "submitTransaction" + }, + { + "args": [ + "transaction", + "bUnlimited", + "bLocal", + "failType" + ], + "lineno": 71, + "name": "processTransaction" + }, + { + "args": [ + "set" + ], + "lineno": 81, + "name": "processTransactionSet" + }, + { + "args": [ + "lpLedger", + "account" + ], + "lineno": 87, + "name": "getOwnerInfo" + }, + { + "args": [ + "lpLedger", + "book", + "uTakerID", + "bProof", + "iLimit", + "jvMarker", + "jvResult" + ], + "lineno": 93, + "name": "getBookPage" + }, + { + "args": [ + "peerPos" + ], + "lineno": 102, + "name": "processTrustedProposal" + }, + { + "args": [ + "val", + "source" + ], + "lineno": 104, + "name": "recvValidation" + }, + { + "args": [ + "map", + "fromAcquire" + ], + "lineno": 106, + "name": "mapComplete" + }, + { + "args": [ + "netLCL", + "clog" + ], + "lineno": 109, + "name": "beginConsensus" + }, + { + "args": [ + "clog" + ], + "lineno": 111, + "name": "endConsensus" + }, + { + "args": [], + "lineno": 112, + "name": "setStandAlone" + }, + { + "args": [], + "lineno": 113, + "name": "setStateTimer" + }, + { + "args": [], + "lineno": 115, + "name": "setNeedNetworkLedger" + }, + { + "args": [], + "lineno": 116, + "name": "clearNeedNetworkLedger" + }, + { + "args": [], + "lineno": 117, + "name": "isNeedNetworkLedger" + }, + { + "args": [], + "lineno": 118, + "name": "isFull" + }, + { + "args": [ + "om" + ], + "lineno": 119, + "name": "setMode" + }, + { + "args": [], + "lineno": 120, + "name": "isBlocked" + }, + { + "args": [], + "lineno": 121, + "name": "isAmendmentBlocked" + }, + { + "args": [], + "lineno": 122, + "name": "setAmendmentBlocked" + }, + { + "args": [], + "lineno": 123, + "name": "isAmendmentWarned" + }, + { + "args": [], + "lineno": 124, + "name": "setAmendmentWarned" + }, + { + "args": [], + "lineno": 125, + "name": "clearAmendmentWarned" + }, + { + "args": [], + "lineno": 126, + "name": "isUNLBlocked" + }, + { + "args": [], + "lineno": 127, + "name": "setUNLBlocked" + }, + { + "args": [], + "lineno": 128, + "name": "clearUNLBlocked" + }, + { + "args": [], + "lineno": 129, + "name": "consensusViewChange" + }, + { + "args": [], + "lineno": 131, + "name": "getConsensusInfo" + }, + { + "args": [ + "human", + "admin", + "counters" + ], + "lineno": 132, + "name": "getServerInfo" + }, + { + "args": [], + "lineno": 133, + "name": "clearLedgerFetch" + }, + { + "args": [], + "lineno": 134, + "name": "getLedgerFetchInfo" + }, + { + "args": [ + "consensusDelay" + ], + "lineno": 141, + "name": "acceptLedger" + }, + { + "args": [], + "lineno": 147, + "name": "reportFeeChange" + }, + { + "args": [ + "newValidLedger" + ], + "lineno": 149, + "name": "updateLocalTx" + }, + { + "args": [], + "lineno": 150, + "name": "getLocalTxCount" + }, + { + "args": [ + "lpAccepted" + ], + "lineno": 155, + "name": "pubLedger" + }, + { + "args": [ + "ledger", + "transaction", + "result" + ], + "lineno": 157, + "name": "pubProposedTransaction" + }, + { + "args": [ + "val" + ], + "lineno": 160, + "name": "pubValidation" + }, + { + "args": [ + "obj" + ], + "lineno": 162, + "name": "stateAccounting" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/NetworkOPs.h.ai.md b/include/xrpl/server/NetworkOPs.h.ai.md new file mode 100644 index 0000000000..8e96b34044 --- /dev/null +++ b/include/xrpl/server/NetworkOPs.h.ai.md @@ -0,0 +1,53 @@ +# `include/xrpl/server/NetworkOPs.h` — The Central Client-Facing Interface + +## Role and Purpose + +`NetworkOPs` is the primary abstraction boundary between client-facing code — RPC handlers, WebSocket sessions, backend applications — and the internal machinery of an XRP Ledger node. The class comment puts it plainly: "code that wants to do normal operations on the network such as creating and monitoring accounts, creating transactions, and so on should use this interface." In practice, this means most RPC command implementations are thin wrappers that delegate nearly all meaningful work to a `NetworkOPs` instance. + +The design is a pure-virtual abstract class, meaning the header defines a contract with no implementation. This is classical dependency inversion: the entire RPC layer depends on this abstraction, not on `NetworkOPsImp` (the concrete implementation in `src/xrpld/app/misc/NetworkOPs.cpp`). Consumers need only include this header and a handful of protocol headers, because all referenced types — `Peer`, `STTx`, `ReadView`, `LedgerMaster`, `RCLCxPeerPos` — appear only as forward declarations. The concrete implementation carries the full weight of consensus, ledger, and overlay includes. This architectural choice is especially important given how widely `NetworkOPs` is used across the codebase. + +## `OperatingMode` — The Server's Self-Assessment + +The `OperatingMode` enum defines five states representing the server's confidence in its relationship to the broader network: + +- `DISCONNECTED (0)` — cannot process requests at all +- `CONNECTED (1)` — in contact with the network but not yet aligned +- `SYNCING (2)` — briefly behind +- `TRACKING (3)` — aligned with network consensus +- `FULL (4)` — holds a complete ledger and can validate + +The explicit warning in the comment — "do not change them without verifying each use" — signals that these integer values are compared ordinally in downstream code. The `isFull()` convenience method tests for the `FULL` state, which is the only mode where a node can meaningfully participate in validation. Exposing this state through the interface lets callers and RPC responses communicate node health accurately rather than trusting blindly. The implementation's `StateAccounting` inner class (visible in the `.cpp`) tracks transition counts and cumulative microseconds spent in each mode — that metric surface is exposed here via `stateAccounting()`. + +## Transaction Processing Pipeline + +The interface exposes two distinct transaction entry points reflecting different origins and latency requirements. + +`submitTransaction()` bears the comment "must complete immediately" — a contract, not a suggestion. It handles transactions arriving from network peers or clients where the caller cannot block. `processTransaction()` is richer: it carries context about whether the submitting connection has elevated privileges (`bUnlimited`), whether the transaction originated locally (`bLocal`), and the `FailHard` disposition. Local transactions from privileged connections may receive different queuing or rejection behavior than peer-relayed traffic. + +`FailHard` is a scoped enum (`yes`/`no`) rather than a raw boolean, accompanied by the `doFailHard()` factory that converts a boolean parameter. This pattern addresses a real footgun: a parameter named `noMeansDont` inverted into a `FailHard` assignment would be silently wrong with a bare `bool`. The enum makes intent explicit at every call site. The `TransactionStatus` struct inside `NetworkOPsImp` stores the `FailHard` value alongside the transaction and carries an assertion enforcing that only local transactions may have `FailHard::yes` — network-received transactions never carry that flag. + +`processTransactionSet()` handles batch consensus-driven processing via a `CanonicalTXSet`, where ordering guarantees matter and the batch is expected to be applied atomically within a ledger round. + +## Consensus Integration + +The methods `beginConsensus()`, `endConsensus()`, `processTrustedProposal()`, `recvValidation()`, and `mapComplete()` expose the integration points where the consensus engine drives state through `NetworkOPs`. The `std::unique_ptr` passed into `beginConsensus()` and `endConsensus()` is a structured log accumulator threaded through the consensus round. Passing it explicitly through the call stack avoids global or thread-local logging state while still collecting fine-grained diagnostic output for each consensus event — important when debugging a distributed agreement protocol. + +`acceptLedger()` is explicitly scoped to standalone mode (its doc comment makes this clear) and supports the `ledger_accept` RPC command, which forces a virtual consensus round for development and testing. The optional `consensusDelay` parameter allows simulating time-dependent consensus behavior without modifying clock infrastructure. + +## Amendment and UNL Blocking + +Two independent blocking mechanisms are exposed at the interface level, each with get/set/clear methods: + +**Amendment blocking** (`isAmendmentBlocked`, `setAmendmentBlocked`) captures a critical safety invariant: if the network has activated ledger amendments this node doesn't understand, it must stop processing transactions to avoid diverging from the rest of the network. **Amendment warning** (`isAmendmentWarned`, `setAmendmentWarned`, `clearAmendmentWarned`) provides a softer early signal for upcoming amendments that have not yet activated. + +**UNL blocking** (`isUNLBlocked`, `setUNLBlocked`, `clearUNLBlocked`) handles the case where the Unique Node List configuration is problematic — for instance, when the node cannot reach a quorum of its trusted validators. The combined `isBlocked()` method checks both amendment-blocked and UNL-blocked in a single call, useful for gating operations that require the node to be fully operational. + +These are exposed at the abstract interface level because RPC responses — `server_info`, error replies to transaction submissions — must communicate these conditions to operators and monitoring systems. + +## Subscription Publishing + +`NetworkOPs` inherits from `InfoSub::Source` (defined in `InfoSub.h`), which provides the complete subscriber-management interface: subscribing and unsubscribing clients to account activity, ledger closes, book changes, validations, peer status, consensus events, and manifests. `NetworkOPs` provides the publisher half. When a ledger closes, `pubLedger()` is called; when a transaction enters the open ledger as a proposal, `pubProposedTransaction()` fires; when a validator vote is received, `pubValidation()` notifies subscribers. This dual role — operational interface for client commands and event source for streaming data feeds — means `NetworkOPs` is the single object that threads together the node's state machine and its outward-facing subscription system. + +## Design Notes + +The `clock_type` alias (`beast::abstract_clock`) reflects a broader pattern in the XRPL codebase of injecting abstract clocks to support deterministic testing. The `setNeedNetworkLedger()` / `clearNeedNetworkLedger()` pair manages a flag signaling that the node has not yet acquired a reference ledger from the network — a necessary bootstrapping condition before the node can track consensus. `updateLocalTx()` and `getLocalTxCount()` expose the local transaction cache, which retains transactions submitted by this node across ledger closings so they can be re-applied if dropped from a consensus round. \ No newline at end of file diff --git a/include/xrpl/server/Port.h.ai.json b/include/xrpl/server/Port.h.ai.json new file mode 100644 index 0000000000..dfedf794ee --- /dev/null +++ b/include/xrpl/server/Port.h.ai.json @@ -0,0 +1,91 @@ +{ + "args": [ + { + "lineno": 58, + "name": "os" + }, + { + "lineno": 58, + "name": "p" + }, + { + "lineno": 84, + "name": "port" + }, + { + "lineno": 84, + "name": "section" + }, + { + "lineno": 84, + "name": "log" + } + ], + "classes": [ + { + "args": [], + "lineno": 15, + "name": "Port" + }, + { + "args": [], + "lineno": 62, + "name": "ParsedPort" + } + ], + "description": "Defines configuration structures and functions for server listening ports in the XRPL server, including SSL, protocol, and admin network settings.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/Port.h", + "functions": [ + { + "args": [], + "lineno": 46, + "name": "Port::websockets" + }, + { + "args": [], + "lineno": 50, + "name": "Port::secure" + }, + { + "args": [], + "lineno": 54, + "name": "Port::protocols" + }, + { + "args": [ + "os", + "p" + ], + "lineno": 58, + "name": "operator<<" + }, + { + "args": [ + "port", + "section", + "log" + ], + "lineno": 84, + "name": "parse_Port" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "boost" + }, + { + "lineno": 12, + "name": "asio" + }, + { + "lineno": 13, + "name": "ssl" + }, + { + "lineno": 22, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/Port.h.ai.md b/include/xrpl/server/Port.h.ai.md new file mode 100644 index 0000000000..46bef5464e --- /dev/null +++ b/include/xrpl/server/Port.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/server/Port.h` — Server Port Configuration + +`Port.h` defines the core data structures that describe how a rippled node listens for incoming connections. Every network endpoint the server opens — whether for HTTP/JSON-RPC, WebSocket, HTTPS, WSS, or the peer-to-peer protocol — is governed by one `Port` instance. This file is the contract between the configuration layer and the runtime server machinery. + +## The Two-Struct Design: `ParsedPort` and `Port` + +The header declares two related but deliberately distinct structs. `ParsedPort` is the *mutable parsing target*, holding an `std::optional` and `std::optional` for `ip` and `port`. `Port` is the *resolved runtime configuration* where those same fields are non-optional bare values. This asymmetry is intentional and reflects the two-phase config loading pattern used in `ServerHandler.cpp`. + +Rippled's config file allows a `[server]` section to define common defaults, and individual `[port_rpc]`/`[port_peer]`/etc. sections to override them. The `parse_Port()` function writes into a `ParsedPort`, so a common-defaults instance can be copied into a per-port instance before `parse_Port()` is called again — inherited optionals remain unset if the specific section doesn't override them, but they were set by the common section copy. Once parsing is complete, `to_Port()` in `ServerHandler.cpp` transfers fields from `ParsedPort` into a fully-validated `Port`, throwing if the non-negotiable fields (`ip`, `port`, `protocol`) are still absent. + +`Port` alone holds the live `std::shared_ptr`. This context can only be constructed once the SSL key, cert, and chain paths are finalised; it doesn't belong in the partially-built `ParsedPort` stage. + +## Protocol Set and Case-Insensitive Comparison + +Both structs store the protocol list as `std::set`. The `iless` comparator makes protocol name matching case-insensitive, so `"WS"` and `"ws"` are treated as the same entry. The `parse_Port()` implementation splits the comma-separated `protocol =` config value using RFC 2616 comma parsing and inserts each token. The resulting set drives two query helpers on `Port`: + +- `websockets()` — returns `true` if any WebSocket protocol (`ws`, `wss`, `ws2`, `wss2`) is enabled. Used by the server's `Door` to decide whether to upgrade connections. +- `secure()` — returns `true` if any TLS-requiring protocol (`https`, `wss`, `wss2`, `peer`) is listed. The server uses this to decide whether to initialise the `ssl::context` and wrap connections in TLS. +- `protocols()` — returns a comma-joined string for logging (used by `operator<<`). + +## Network Access Control: Admin and Secure-Gateway Nets + +Two independent IP allowlists control elevated privilege. `admin_nets_v4` / `admin_nets_v6` restrict which source addresses can issue administrative RPC commands. `secure_gateway_nets_v4` / `secure_gateway_nets_v6` identify trusted reverse-proxy addresses whose `X-Forwarded-For` headers are trusted for rate-limiting and auth-bypass. + +Both sets are populated by the static `populate()` helper in Port.cpp. The parsing logic handles three forms of input: + +1. **Bare unspecified address** (`0.0.0.0` or `::`): expands to `0.0.0.0/0` and `::/0` immediately, then stops processing further entries — subsequent entries would be redundant. +2. **Single IP address**: promoted to a `/32` (IPv4) or `/128` (IPv6) host-route so it fits the `network_vN` container. +3. **CIDR subnet**: parsed directly, but validated against its canonical form. A misconfigured entry like `10.1.2.3/24` — where the host bits are non-zero — triggers an error showing both the invalid input and the correct canonical address `10.1.2.0/24`. + +Keeping both IPv4 and IPv6 in separate vectors avoids type-erasure overhead and allows direct CIDR membership tests against typed endpoints later. + +## Connection Limits + +`limit` bounds the total number of simultaneous connections on the port. The value `0` means unlimited, and the config parser accepts the string `"unlimited"` as a synonym. `ws_queue_limit` caps the size of the per-WebSocket-connection outbound send queue; if the queue fills — typically because a slow client is not reading — the server disconnects. Its default when omitted from config is 100 messages, and zero is explicitly rejected as invalid. + +## WebSocket Compression + +`pmd_options` of type `boost::beast::websocket::permessage_deflate` gives fine-grained control over per-message DEFLATE compression. `parse_Port()` maps seven individual config keys onto this struct: `permessage_deflate` (master enable), `compress_level`, `memory_level`, `client_max_window_bits`, `server_max_window_bits`, `client_no_context_takeover`, and `server_no_context_takeover`. The defaults — compression enabled, window bits 15, compress level 8, memory level 4 — favour throughput for large JSON responses at the cost of some memory. + +## Relationship to `Server` and `ServerImpl` + +`Server::ports()` in `Server.h` accepts `std::vector` and returns an `Endpoints` map (named endpoint → TCP endpoint). `ServerImpl` stores those ports as `std::vector ports_` and spawns one `Door` per entry. The `Door` reads the port's protocol set and SSL context to decide which connection types to accept, meaning `Port` is the single source of truth for all per-listener policy from config parse time through the full connection lifecycle. \ No newline at end of file diff --git a/include/xrpl/server/Server.h.ai.json b/include/xrpl/server/Server.h.ai.json new file mode 100644 index 0000000000..8544b6e0cf --- /dev/null +++ b/include/xrpl/server/Server.h.ai.json @@ -0,0 +1,24 @@ +{ + "args": [], + "classes": [], + "description": "Defines a template function to create an HTTP server using a specified handler, returning a unique pointer to a Server implementation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/Server.h", + "functions": [ + { + "args": [ + "handler", + "io_context", + "journal" + ], + "lineno": 11, + "name": "make_Server" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/Server.h.ai.md b/include/xrpl/server/Server.h.ai.md new file mode 100644 index 0000000000..bd659d01fd --- /dev/null +++ b/include/xrpl/server/Server.h.ai.md @@ -0,0 +1,61 @@ +# `include/xrpl/server/Server.h` + +## Role and Purpose + +`Server.h` is the public entry point for the XRPL multi-protocol HTTP server. The file itself is remarkably thin — just a single factory function — but it acts as the seam between callers and a deeply layered, template-heavy implementation. Its job is to let application code create a server without knowing the concrete type of the handler or the full implementation hierarchy. + +The single exported function is: + +```cpp +template +std::unique_ptr +make_Server(Handler& handler, boost::asio::io_context& io_context, beast::Journal journal) +``` + +The caller passes a handler by reference and receives an owning `unique_ptr` — an abstract base class. From that point on, the caller interacts with the server entirely through the narrow `Server` interface, which exposes only three operations: `ports()`, `close()`, and `journal()`. + +## Why a Factory Function? + +The key design decision here is the use of a non-member factory template rather than a public constructor or a class template. The handler is the customization point: `ServerImpl` is a concrete type parameterized over the handler, but the caller should not need to name that type. By returning `unique_ptr`, the factory erases the handler type entirely. The handler itself is never owned — it is taken by reference — so the caller retains full control of its lifetime. This avoids the complications of shared ownership while still decoupling the server's template machinery from its users. + +In practice, the only production caller is `ServerHandler` in `src/xrpld/rpc/detail/ServerHandler.cpp`, which passes `*this` as the handler during construction: + +```cpp +m_server(make_Server(*this, io_context, app_.getJournal("Server"))) +``` + +`ServerHandler` satisfies the Handler concept and handles all HTTP/WebSocket request dispatch for the node's RPC subsystem. + +## The `Server` Abstract Interface + +The `Server` class — defined inside `ServerImpl.h` and pulled in via the `#include` chain — is the interface the caller retains. Its design is intentionally sparse: + +- `ports(std::vector const&)` configures the listening endpoints. It may only be called once; calling it on a closed server throws `std::logic_error`. It returns an `Endpoints` map (a `std::unordered_map`) so callers can discover the actual bound port numbers, which matters when port 0 is configured (OS-chosen ephemeral port). +- `close()` initiates asynchronous shutdown. The server is considered stopped when all pending I/O completions have drained and all connections have closed. Calling `handler_.onStopped(*this)` signals completion. The destructor blocks until this is achieved. +- `journal()` returns the logging journal — a pattern used throughout XRPL to allow scoped, categorized logging. + +## `ServerImpl`: The Concrete Implementation + +`ServerImpl` is the template class that actually does the work. Its internal structure is worth understanding: + +- It holds a reference to `io_context_` and creates a `strand_` to serialize its own state changes. An `executor_work_guard` in `work_` keeps the `io_context` from exiting early while the server is live; releasing `work_` (by assigning `std::nullopt`) signals readiness to let the event loop finish. +- The `io_list ios_` member is the lifecycle manager for all active async objects. Every `Door` and every peer connection registers with this list. When `close()` is called, `io_list` propagates the close signal to all registered work items and invokes the finisher callback only after the last one has been destroyed. This avoids the classic race where a finisher fires before pending completions have run. +- The list of `Door` objects is stored as `std::vector>>`, not strong pointers. Ownership belongs to the `io_list`. The weak references are just for inspection; if a door has already been destroyed the weak pointer returns null. + +## `Door`: Listening Sockets and Protocol Detection + +Each configured `Port` spawns a `Door`. A `Door` is a Boost.Asio coroutine-based acceptor loop. On each accepted connection it must decide whether the connection is plain or TLS. If the port is configured for both (e.g., both `http` and `https` in its protocol set), `Door` creates a `Detector` — an inner class that reads up to 16 bytes with a 15-second timeout using `async_detect_ssl` from Boost.Beast. Based on the result, it instantiates either `PlainHTTPPeer` or `SSLHTTPPeer`, both of which are also `io_list::work` objects tracked by the same `io_list`. + +If the port is exclusively plain or exclusively SSL, detection is skipped and the peer is created directly, saving a round-trip read. + +`Door` also implements proactive file-descriptor throttling on POSIX systems. Before each accept it calls `should_throttle_for_fds()`, which reads `/proc/self/fd` (Linux) or `/dev/fd` (other POSIX) to count open file descriptors against the process `RLIMIT_NOFILE` limit. If fewer than 30% of file descriptors are free, the accept loop pauses for an exponentially increasing backoff starting at 50 ms and capping at 2 seconds. This prevents the server from hitting `EMFILE` under sustained connection load. + +## `io_list`: Coordinated Async Shutdown + +`io_list` is the linchpin of safe teardown. It maintains a `flat_map` of `work*` to `weak_ptr`. Every `Door`, `Detector`, `PlainHTTPPeer`, and `SSLHTTPPeer` inherits `io_list::work` and registers on construction via `io_list::emplace`. When the `work` object is destroyed, its destructor decrements the list's count and, if the count reaches zero after `closed_` is true, atomically swaps out and invokes the finisher. This guarantees the finisher fires exactly once and only after all live async objects are gone. + +The `close(Finisher&&)` overload takes a callable; `ServerImpl::close()` passes a lambda that releases the `work_` guard and calls `handler_.onStopped()`. The destructor path (when the server is dropped without an explicit `close()`) calls `ios_.close()` followed by `ios_.join()`, which blocks on a `condition_variable` until `n_ == 0`. The comment in the destructor notes that `onStopped` is deliberately not called in that path. + +## Summary of Key Design Choices + +The combination of a type-erasing factory, a virtual base interface, and a template implementation cleanly separates the public API surface from the template expansion required by the handler type. The `io_list` pattern avoids both reference cycles and use-after-free in shutdown by ensuring that outstanding async work keeps the objects alive through `shared_ptr` while the list itself holds only weak references. The fd-throttle in `Door` is a practical defensive measure against connection storms that would otherwise surface as cryptic `accept` errors. \ No newline at end of file diff --git a/include/xrpl/server/Session.h.ai.json b/include/xrpl/server/Session.h.ai.json new file mode 100644 index 0000000000..129c979bf0 --- /dev/null +++ b/include/xrpl/server/Session.h.ai.json @@ -0,0 +1,64 @@ +{ + "args": [ + { + "lineno": 38, + "name": "s" + }, + { + "lineno": 44, + "name": "buffers" + }, + { + "lineno": 51, + "name": "buffer" + }, + { + "lineno": 51, + "name": "bytes" + }, + { + "lineno": 54, + "name": "writer" + }, + { + "lineno": 54, + "name": "keep_alive" + }, + { + "lineno": 68, + "name": "graceful" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "Session" + } + ], + "description": "Defines the xrpl::Session class, representing persistent state and operations for a connection session, including methods for writing data, managing session lifecycle, and upgrading to WebSocket.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/Session.h", + "functions": [ + { + "args": [ + "s" + ], + "lineno": 38, + "name": "write" + }, + { + "args": [ + "buffers" + ], + "lineno": 44, + "name": "write" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/Session.h.ai.md b/include/xrpl/server/Session.h.ai.md new file mode 100644 index 0000000000..be95444b65 --- /dev/null +++ b/include/xrpl/server/Session.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/server/Session.h` + +## Role in the System + +`Session` is the abstract interface representing a single live HTTP connection in the XRPL server stack. Every TCP or TLS connection accepted by the server is surfaced to application-level handlers exclusively through this interface, isolating the handler from transport specifics (plain vs. SSL, buffer management, Asio strand serialization). The concrete implementation lives in `detail/BaseHTTPPeer.h`, which is a `CRTP`-like class template parameterized on both the `Handler` type and the transport type (`PlainHTTPPeer` / `SSLHTTPPeer`). Application code — chiefly `ServerHandler` — never touches those templates directly; it operates only against `Session&`. + +## Key Design Decisions + +### Pure-virtual interface with a `void* tag` escape hatch + +The interface is non-copyable and fully abstract: every method that must vary by transport is pure virtual. The one exception is the `void* tag` member. This is a deliberate, low-overhead extension point that lets the handler attach arbitrary per-connection state (an associated `WSInfoSub`, a request counter, a decoded auth token, etc.) without forcing `Session` to grow application-specific fields or requiring a `dynamic_cast`. The implementation guarantees `tag` is zero-initialized at construction and persisted between callbacks for the connection's lifetime — a classic C-style slot that trades type safety for zero coupling. + +### `write()` overload family + +Three overloads service the write path. The two concrete convenience methods — one for `std::string`, one for a generic `BufferSequence` — both bottom out in the single pure virtual `write(void const* buffer, std::size_t bytes)`. This reduces the implementation surface to one method while providing ergonomic call sites. The second pure virtual overload, `write(std::shared_ptr const& writer, bool keep_alive)`, handles streaming or lazily-materialized response bodies: `Writer` exposes a `prepare()/consume()` pull model that allows the session to apply I/O backpressure. The `keep_alive` flag is passed here rather than inferred from the request because the handler may override it based on application policy. + +### `detach()` for asynchronous response generation + +XRPL's RPC system dispatches request processing onto a job queue that runs on worker threads outside of the Asio `io_context`. Once the handler submits the job, it must keep the session alive until the worker eventually calls `complete()`. `detach()` solves this by returning a `shared_ptr` that the handler holds — extending the object's lifetime beyond the server's own reference. Critically, the docstring notes that `io_context::run()` will not return while any detached session remains open, which means detached sessions participate in the graceful shutdown protocol. This is the central async lifecycle mechanism for HTTP sessions. + +### Two-phase `write()` + `complete()` model + +Following HTTP/1.1 semantics, a handler writes response data incrementally and then calls `complete()` to signal the end of the response. `complete()` does not close the connection; instead, it triggers keep-alive cycling (issuing a new read for the next request) if the connection negotiated keep-alive, or a graceful shutdown otherwise. This two-phase model is what allows connection reuse across multiple request/response cycles. + +### `close(bool graceful)` for explicit teardown + +`close()` is asynchronous — it schedules closure after all pending write operations drain. The `graceful` flag controls whether the close waits for the send buffer to flush or aborts immediately. This is important for error paths (e.g., malformed request, auth failure) where the server wants to send a 4xx response and then close, versus administrative shutdown where it may want to drop immediately. + +### `websocketUpgrade()` — protocol transition + +When a handler determines an incoming HTTP request is a WebSocket upgrade (by inspecting `request()`), it calls `websocketUpgrade()` to hand off the socket to a `WSSession`. The returned `shared_ptr` owns the connection going forward; the `Session` is no longer valid for writes after this call. `WSSession` is a sibling interface (defined in `WSSession.h`) with its own `send(shared_ptr)` and `complete()` semantics suited to the WebSocket framing model. This clean protocol-switch design means the HTTP and WebSocket paths share the socket's physical connection but use entirely separate message-framing and lifecycle APIs. + +## Relationship to `Port`, `Writer`, and `WSSession` + +`Session::port()` returns a `const Port&` describing the server listener that accepted this connection — its protocol set, SSL context, admin IP ranges, WebSocket queue limits, and credentials. This lets a single `Handler` implementation distinguish how to treat a request based on which port it arrived on (e.g., an admin-only port vs. a public JSON-RPC port) without needing separate handler instances. + +`Writer` (from `Writer.h`) is the pull-based streaming abstraction used in the `write(shared_ptr, bool)` overload. It models an input sequence that the session drains in chunks, calling `prepare()` to fill and `consume()` to advance, stopping when `complete()` returns `true`. This lets response generators (like those producing large ledger data) avoid materializing the entire body in memory. + +`WSSession` (from `WSSession.h`) shares the `appDefined` void pointer slot under a different name but serves the same purpose as `Session::tag` — handler-defined per-connection state. Both interfaces also share `complete()` semantics, reinforcing that the notify-when-done pattern is a server-wide contract rather than HTTP-specific. \ No newline at end of file diff --git a/include/xrpl/server/SimpleWriter.h.ai.json b/include/xrpl/server/SimpleWriter.h.ai.json new file mode 100644 index 0000000000..74c5dd20e6 --- /dev/null +++ b/include/xrpl/server/SimpleWriter.h.ai.json @@ -0,0 +1,69 @@ +{ + "args": [ + { + "lineno": 13, + "name": "isRequest" + }, + { + "lineno": 13, + "name": "Body" + }, + { + "lineno": 13, + "name": "Fields" + }, + { + "lineno": 13, + "name": "msg" + }, + { + "lineno": 25, + "name": "bytes" + } + ], + "classes": [ + { + "args": [ + "boost::beast::http::message const& msg" + ], + "lineno": 10, + "name": "SimpleWriter" + } + ], + "description": "Defines a deprecated SimpleWriter class in the xrpl namespace that serializes HTTP/1 messages using Boost.Beast, providing methods to manage and access the serialized data.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/SimpleWriter.h", + "functions": [ + { + "args": [], + "lineno": 20, + "name": "complete" + }, + { + "args": [ + "bytes" + ], + "lineno": 24, + "name": "consume" + }, + { + "args": [ + "bytes", + "" + ], + "lineno": 28, + "name": "prepare" + }, + { + "args": [], + "lineno": 33, + "name": "data" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/SimpleWriter.h.ai.md b/include/xrpl/server/SimpleWriter.h.ai.md new file mode 100644 index 0000000000..b581095f26 --- /dev/null +++ b/include/xrpl/server/SimpleWriter.h.ai.md @@ -0,0 +1,33 @@ +# `SimpleWriter.h` — Synchronous HTTP Message Serialization for the `Writer` Interface + +`SimpleWriter` is a concrete, synchronous implementation of the abstract `Writer` interface that serializes an entire Boost.Beast HTTP message into an in-memory buffer at construction time. It exists to satisfy the `Handoff::response` contract when the full HTTP response is known immediately — the common case for short error messages, redirect responses, and simple JSON replies throughout the XRPL server and overlay subsystems. + +## The `Writer` Contract + +The abstract base class `Writer` defines a pull-based streaming interface with four virtual methods: `complete()`, `consume()`, `prepare()`, and `data()`. This design allows the network session layer to consume response data incrementally — useful when generating large or asynchronous payloads where data arrives in chunks. The session calls `prepare()` to signal readiness, reads buffer segments via `data()`, and advances the stream with `consume()`. + +`SimpleWriter` implements this interface for the degenerate but common case: the entire payload already exists in memory before the first byte is sent. + +## Design: Eager Serialization at Construction + +The constructor is a function template over `isRequest`, `Body`, and `Fields` — the three template parameters of `boost::beast::http::message`. This universality lets `SimpleWriter` wrap any valid Beast HTTP message (request or response, any body type, any field set) with a single class, without virtual dispatch or type erasure at the message layer. Serialization happens immediately via `boost::beast::ostream(sb_) << msg`, which writes the fully framed HTTP/1 wire representation into the internal `boost::beast::multi_buffer`. After the constructor returns, `SimpleWriter` holds no reference to the original message; ownership and lifetime are entirely self-contained. + +This eager approach has a deliberate tradeoff: the entire message must fit in memory and be ready at construction. If the message body is large, the allocation happens upfront. The payoff is that `prepare()` becomes trivially synchronous — it always returns `true` immediately and ignores the `resume` callback entirely, because there is never anything asynchronous to wait for. + +## Buffer Management via `multi_buffer` + +The backing store is a `boost::beast::multi_buffer`, which is a Boost.Asio dynamic buffer composed of multiple non-contiguous fixed-size chunks. The `data()` method iterates over those chunks and copies their `const_buffer` descriptors into a `std::vector`, producing a scatter-gather buffer sequence that the session layer can pass directly to `async_write`. The `consume()` method delegates to `multi_buffer::consume()`, which advances the read position through the chunk list without copying data. `complete()` checks `sb_.size() == 0` — the canonical way to detect that all bytes have been consumed. + +## Usage in Practice + +`SimpleWriter` is the go-to response writer across two major subsystems: + +In `ServerHandler.cpp`, it wraps protocol-rejection responses (e.g., "Invalid protocol") and final HTTP replies to RPC clients. In `OverlayImpl.cpp`, it wraps peer redirect responses (`/crawl`, `/vl/`, `/health`) and error rejections for bad peer handshakes. In both cases, the pattern is identical: build a fully populated Beast HTTP message, call `prepare_payload()` to compute content-length, then `std::make_shared(msg)` and store it in `Handoff::response`. The session layer then drains the writer without needing to know anything about message construction. + +## Deprecation + +The class is annotated `/// Deprecated`. The most likely reason is that the eagerly-in-memory approach is incompatible with large or streaming response bodies, and that newer or planned writer implementations handle those cases properly. Despite the deprecation tag, it remains widely used throughout the codebase wherever responses are small and immediately available — its simplicity is a feature in those contexts. Callers that need async or streaming behavior would require a different `Writer` subclass. + +## Error Handling + +There is intentionally no error handling within the class body itself. If `boost::beast::ostream` throws during construction (e.g., due to allocation failure), the exception escapes and no object is created. Subsequent operations on a successfully constructed `SimpleWriter` are infallible: they operate only on pre-allocated buffer memory with no further I/O. \ No newline at end of file diff --git a/include/xrpl/server/State.h.ai.json b/include/xrpl/server/State.h.ai.json new file mode 100644 index 0000000000..f9a53ff26c --- /dev/null +++ b/include/xrpl/server/State.h.ai.json @@ -0,0 +1,113 @@ +{ + "args": [ + { + "lineno": 20, + "name": "session" + }, + { + "lineno": 20, + "name": "config" + }, + { + "lineno": 20, + "name": "dbName" + }, + { + "lineno": 29, + "name": "session" + }, + { + "lineno": 37, + "name": "session" + }, + { + "lineno": 37, + "name": "canDelete" + }, + { + "lineno": 48, + "name": "session" + }, + { + "lineno": 58, + "name": "session" + }, + { + "lineno": 58, + "name": "state" + }, + { + "lineno": 68, + "name": "session" + }, + { + "lineno": 68, + "name": "seq" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "SavedState" + } + ], + "description": "This file provides functions and a struct for managing and interacting with the state database in the XRPL server, including initialization, saving and retrieving state, and managing ledger deletion and rotation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/State.h", + "functions": [ + { + "args": [ + "session", + "config", + "dbName" + ], + "lineno": 19, + "name": "initStateDB" + }, + { + "args": [ + "session" + ], + "lineno": 28, + "name": "getCanDelete" + }, + { + "args": [ + "session", + "canDelete" + ], + "lineno": 36, + "name": "setCanDelete" + }, + { + "args": [ + "session" + ], + "lineno": 47, + "name": "getSavedState" + }, + { + "args": [ + "session", + "state" + ], + "lineno": 57, + "name": "setSavedState" + }, + { + "args": [ + "session", + "seq" + ], + "lineno": 67, + "name": "setLastRotated" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/State.h.ai.md b/include/xrpl/server/State.h.ai.md new file mode 100644 index 0000000000..46f62a14d8 --- /dev/null +++ b/include/xrpl/server/State.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/server/State.h` — Online Deletion State Database Interface + +## Role in the System + +This header defines the persistence interface for the XRPL node's **online deletion** subsystem. When a rippled instance is configured to delete old ledger history as it advances (`[online_delete]` configuration), it needs a small metadata database to survive restarts safely: it must remember which database file is currently writable, which is the archive, and at which ledger sequence the last database rotation occurred. `State.h` provides exactly this — the `SavedState` aggregate and a thin set of free functions that read and write it through a SOCI session. + +## The `SavedState` Struct + +```cpp +struct SavedState { + std::string writableDb; + std::string archiveDb; + LedgerIndex lastRotated{}; +}; +``` + +This three-field struct is the entire persistent state of the online-delete rotation engine. The XRPL online deletion implementation (`SHAMapStoreImp`) maintains two rotating SQLite databases for node-store data — one actively receiving writes (`writableDb`) and one being archived or pruned (`archiveDb`). When rotation occurs, the names swap. `lastRotated` records the ledger sequence at which the most recent rotation happened, anchoring the deletion eligibility window. Without this checkpoint, a restart would lose track of where the sweep left off and could either skip deletions or double-rotate. + +## Free Function Design + +Rather than encapsulating the database behind an object, `State.h` exposes six free functions that each accept a raw `soci::session&`. This is a deliberate design choice: the caller (`SHAMapStoreImp::SavedStateDB`) owns the session directly as a plain `soci::session` member and wraps all calls with its own `std::mutex`. The free-function API lets the mutex layer sit at the call site without forcing any particular ownership model on the persistence layer itself. + +`initStateDB()` creates the schema if it does not exist and inserts the single seed row in each table. The implementation sets `PRAGMA synchronous=FULL` before any schema work — a SQLite durability setting that forces every write to be fully flushed to disk before the call returns. For a metadata database that checkpoints crash-critical state, this is the right tradeoff despite its write-latency cost. + +Both `DbState` and `CanDelete` tables use a fixed single-row pattern (always `Key = 1`). These tables function as named key-value pairs, not relational collections. This is efficient and correct for state that exists exactly once per server instance. + +## Advisory Delete Fence + +The `CanDelete` table stores a single `LedgerIndex` that acts as an operator-controlled fence: no ledger with sequence ≤ `canDelete` will be automatically purged. The `getCanDelete()` and `setCanDelete()` functions expose this fence. `setCanDelete()` returns the new sequence, which the caller (`SHAMapStoreImp`) also caches in the atomic `canDelete_` member for lock-free reads in the hot rotation loop. + +The `can_delete` RPC handler (`CanDelete.cpp`) exposes this fence to operators, accepting symbolic values like `always`, `never`, `now`, or a ledger hash — translating them to a `LedgerIndex` before calling `setCanDelete`. This gives operators precise control over which historical data the node may prune, without halting the node. + +## Relationship to `SHAMapStoreImp` + +The functions here are not called directly by most of the codebase. Instead, `SHAMapStoreImp` declares an inner class `SavedStateDB` that wraps each free function behind a `std::mutex`, exposing `getState()`, `setState()`, `setLastRotated()`, `getCanDelete()`, and `setCanDelete()` as thread-safe methods. The outer `SHAMapStoreImp::run()` loop — which runs on a dedicated thread — consults `lastRotated` to determine when the next rotation is due and reads `canDelete_` to verify operator permission before pruning. + +`setLastRotated()` exists as a dedicated function (separate from `setSavedState()`) because updating just the rotation sequence is a frequent, targeted operation during the sweep loop, while a full `setSavedState()` write is only needed when the database filenames change at rotation time. + +## Invariants and Failure Modes + +`initStateDB()` throws `std::runtime_error` if the row-count query returns a null result from SOCI — a condition that indicates a deeper database corruption or connection failure rather than a transient error. By throwing at initialization time, the system fails fast rather than silently operating with a zero-initialized state that could lead to incorrect deletion decisions. The default-zero `lastRotated` is handled explicitly in the rotation loop: a zero value means "not yet rotated," triggering an immediate bootstrap write of the current validated ledger sequence. \ No newline at end of file diff --git a/include/xrpl/server/Vacuum.h.ai.json b/include/xrpl/server/Vacuum.h.ai.json new file mode 100644 index 0000000000..f1b8f99613 --- /dev/null +++ b/include/xrpl/server/Vacuum.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 14, + "name": "setup" + }, + { + "lineno": 15, + "name": "j" + } + ], + "classes": [], + "description": "Provides a function to perform vacuum (cleanup and optimization) operations on a database within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/Vacuum.h", + "functions": [ + { + "args": [ + "setup", + "j" + ], + "lineno": 13, + "name": "doVacuumDB" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/Vacuum.h.ai.md b/include/xrpl/server/Vacuum.h.ai.md new file mode 100644 index 0000000000..a67163f77e --- /dev/null +++ b/include/xrpl/server/Vacuum.h.ai.md @@ -0,0 +1,35 @@ +# `include/xrpl/server/Vacuum.h` + +This header is the public interface for a single administrative utility: `doVacuumDB`. It exposes one free function that performs a SQLite `VACUUM` operation on the XRPL node's transaction database, reclaiming fragmented space and rebuilding the file in a compact, optimally-structured form. + +## Role in the System + +As an XRPL node runs, its `transaction.db` SQLite file accumulates dead pages from deletions and updates. SQLite does not automatically reclaim this space; the file can grow significantly larger than its live data set. `doVacuumDB` is the offline remedy — it is invoked as a standalone CLI command (`rippled --vacuum`) against a non-running node, not as part of normal ledger processing. The `Main.cpp` call site guards it explicitly: vacuum is rejected in standalone mode and wrapped in exception handling that propagates failure as a non-zero exit code. + +## Interface + +```cpp +bool doVacuumDB(DatabaseCon::Setup const& setup, beast::Journal j); +``` + +`DatabaseCon::Setup` carries the `dataDir` path, the `txPragma` array (WAL mode, sync level, etc.), and a pointer to `globalPragma` (the shared journal/sync settings applied to every database connection). The function returns `true` on success and `false` on any preflight failure, with diagnostic messages written to `std::cerr`. + +## What the Implementation Does + +The implementation (in `src/libxrpl/server/Vacuum.cpp`) follows a careful sequence: + +1. **Disk space preflight.** Before opening the database, it calls `boost::filesystem::file_size` on `transaction.db` and compares it to the available space on the same partition via `boost::filesystem::space`. SQLite's `VACUUM` rewrites the entire database into a temporary file before replacing the original, so it needs roughly as much free space as the current file size. If the check fails, the function returns `false` immediately rather than risk a partial vacuum that could corrupt the database. + +2. **Connection with `temp_store=file`.** A `DatabaseCon` is created using the standard `txPragma` array, then immediately overrides the `temp_store` pragma to `"file"`. The comment explains why: typical XRPL deployments have transaction databases that are too large to fit in RAM. Without this override, SQLite might attempt to keep its internal sort and index rebuild buffers in memory, risking an OOM failure mid-vacuum. Forcing disk-based temp storage trades speed for safety. + +3. **VACUUM execution.** The call `session << "VACUUM;"` rewrites the database file in place. SQLite performs the full repack — eliminating free pages, defragmenting B-tree nodes, and potentially shrinking the file by substantial margins. + +4. **Global pragma reapplication.** After `VACUUM`, the function iterates `setup.globalPragma` and re-executes each pragma statement. This is necessary because SQLite resets certain per-connection settings as a side effect of `VACUUM`, and the node's configured journal mode and sync level must be restored before the connection is handed back. The `XRPL_ASSERT` that `globalPragma` is non-null here is significant — in any non-vacuum startup path, `globalPragma` may legitimately be absent, but this code path asserts that it must be present when vacuum runs. + +5. **Diagnostic output.** The page size is read before and after the vacuum and printed to `std::cout`, giving operators a concrete signal that the operation completed and that the page layout was affected (a page size change indicates that global pragmas altered the page configuration after the vacuum). + +## Design Notes + +The function deliberately operates only on `transaction.db` (the `TxDBName` constant), not on the ledger database. This is because the transaction database is typically the one that accumulates the most fragmentation under normal operation, and vacuuming the ledger database while it contains WAL data that hasn't been checkpointed would be hazardous. + +The header lives under `include/xrpl/server/` rather than `include/xrpl/rdb/` because its concern is node administration (a server-level concept) rather than raw database connectivity. The dependency on `DatabaseCon.h` is direct since `Setup` is defined there, but the function itself is a thin orchestration layer, not part of the database abstraction. \ No newline at end of file diff --git a/include/xrpl/server/WSSession.h.ai.json b/include/xrpl/server/WSSession.h.ai.json new file mode 100644 index 0000000000..6a4320a5c4 --- /dev/null +++ b/include/xrpl/server/WSSession.h.ai.json @@ -0,0 +1,49 @@ +{ + "args": [ + { + "lineno": 38, + "name": "bytes" + }, + { + "lineno": 38, + "name": "resume" + }, + { + "lineno": 57, + "name": "bytes" + }, + { + "lineno": 57, + "name": "" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "WSMsg" + }, + { + "args": [ + "Streambuf&& sb" + ], + "lineno": 49, + "name": "StreambufWSMsg" + }, + { + "args": [], + "lineno": 80, + "name": "WSSession" + } + ], + "description": "Defines interfaces and classes for handling WebSocket messages and sessions in the XRPL server, including message preparation and session management.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/WSSession.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/WSSession.h.ai.md b/include/xrpl/server/WSSession.h.ai.md new file mode 100644 index 0000000000..f1e90ff57b --- /dev/null +++ b/include/xrpl/server/WSSession.h.ai.md @@ -0,0 +1,59 @@ +# `include/xrpl/server/WSSession.h` — WebSocket Message and Session Interfaces + +## Role in the System + +This header defines the two core abstractions that drive all WebSocket I/O in the XRPL server: `WSMsg`, the interface for a sendable WebSocket message, and `WSSession`, the interface for an active WebSocket connection. Together they form the boundary between the application layer (the RPC/subscription handler) and the network transport layer (`BaseWSPeer` and its TLS/plain concrete implementations). + +The file sits alongside `Session.h`, which covers HTTP. `Session::websocketUpgrade()` returns a `shared_ptr`, making the two interfaces the two branches of a single connection lifecycle: HTTP for one-shot request/response, WebSocket for persistent bidirectional streams. + +## `WSMsg` — Lazy, Chunked Message Abstraction + +`WSMsg` is a non-copyable abstract base with a single pure-virtual method: + +```cpp +virtual std::pair> +prepare(std::size_t bytes, std::function resume) = 0; +``` + +The `boost::tribool` return value carries the framing signal: + +- `indeterminate` (`maybe`) — data is not ready; the implementation will call `resume` later when it is. +- `false` — a partial chunk is ready; the caller should write it and call `prepare` again. +- `true` — the final chunk is ready; after this write the message is complete. + +This three-state protocol elegantly handles both synchronous and asynchronous data sources with one interface. An implementation backed by a live database cursor can return `indeterminate` and post the `resume` callback when the next page arrives, whereas a fully buffered implementation can always return `true` on the first call. Neither the transport layer (`BaseWSPeer`) nor the message sender needs to know which case applies. + +`BaseWSPeer::on_write()` drives this protocol directly, calling `prepare(65536, ...)` in a loop and dispatching either `async_write_some` (non-final) or `async_write_some` (final, triggering `on_write_fin`). The `resume` functor it passes is a bound call to `BaseWSPeer::do_write()`, which re-enters the write loop — so an async-ready `WSMsg` simply holds onto the functor and invokes it from its own callback. + +### `StreambufWSMsg` — The Common Case + +The only concrete implementation in this header wraps any Boost.Asio `Streambuf` (in practice, `boost::beast::multi_buffer`). It maintains a cursor `n_` tracking how many bytes were handed out in the previous call and uses `sb_.consume(n_)` at the start of each `prepare()` call to advance the buffer. Because the buffer is already fully populated at construction time, the `resume` callback is unused (ignored by the unnamed parameter) and the method always returns synchronously: `false` when the remaining data exceeds `bytes`, `true` when it fits or the buffer is empty. This is the path taken for JSON-encoded ledger events and RPC replies that are serialized before queuing. + +## `WSSession` — Abstract Connection Handle + +`WSSession` is a pure-virtual, non-copyable struct representing one live WebSocket connection. Its interface is deliberately minimal: + +```cpp +virtual void run() = 0; +virtual Port const& port() const = 0; +virtual http_request_type const& request() const = 0; +virtual boost::asio::ip::tcp::endpoint const& remote_endpoint() const = 0; +virtual void send(std::shared_ptr) = 0; +virtual void close() = 0; +virtual void close(boost::beast::websocket::close_reason const&) = 0; +virtual void complete() = 0; +``` + +`run()` initiates the Beast WebSocket handshake from the already-upgraded TCP/TLS stream. `send()` enqueues a `WSMsg` for asynchronous delivery; the concrete implementation in `BaseWSPeer` enforces `Port::ws_queue_limit` — if the queue depth exceeds the configured ceiling, it flushes the backlog, sets a policy-error close reason, and initiates a close, protecting the server from slow or stalled clients. + +`complete()` is the session's backpressure gate. After each incoming WebSocket frame, `BaseWSPeer::on_read()` calls `handler_.onWSMessage(...)` and then waits. The handler must call `complete()` to trigger `do_read()` again, ensuring the server never buffers more than one inbound message per connection at a time. This prevents runaway memory growth from fast-talking clients. + +The one data member, `std::shared_ptr appDefined`, is a type-erased application attachment point. The XRPL handler layer stores per-session subscription state here without requiring `WSSession` to know anything about the subscription model. Because it is a `shared_ptr`, the attached object is safely destroyed when the session is torn down, even if the handler still holds a reference to the `WSSession` itself. + +## Design Decisions Worth Noting + +**`WSMsg` vs `Writer`**: The HTTP counterpart `Writer` uses a classic pull model with separate `prepare()`, `data()`, and `consume()` calls. `WSMsg` collapses this into a single `prepare()` returning buffer and completion state together. WebSocket messages are inherently framed, so the transport layer already knows the message boundary; the simplified interface maps naturally onto `async_write_some` with the `fin` flag. + +**Strand enforcement in `BaseWSPeer`**: Every virtual method on `WSSession` is implemented with an upfront strand check (`if (!strand_.running_in_this_thread()) return post(...)`). This means callers may invoke `send()` or `close()` from any thread — a common requirement when subscription notifications arrive on ledger-processing threads — without additional synchronization at the call site. + +**Close ordering**: `BaseWSPeer::close(reason)` sets `do_close_ = true` but, if the write queue is non-empty, records the reason in `cr_` and defers the actual `ws_.async_close()` until `on_write_fin()` drains the queue. This guarantees that buffered outbound messages are fully delivered before the close frame is sent, satisfying the RFC 6455 requirement to complete pending sends before initiating closure. \ No newline at end of file diff --git a/include/xrpl/server/Wallet.h.ai.json b/include/xrpl/server/Wallet.h.ai.json new file mode 100644 index 0000000000..c671e74780 --- /dev/null +++ b/include/xrpl/server/Wallet.h.ai.json @@ -0,0 +1,125 @@ +{ + "args": [], + "classes": [], + "description": "This file provides functions for managing the wallet database, manifests, node identities, peer reservations, and feature votes in the XRPL server.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/Wallet.h", + "functions": [ + { + "args": [ + "setup", + "j" + ], + "lineno": 12, + "name": "makeWalletDB" + }, + { + "args": [ + "setup", + "dbname", + "j" + ], + "lineno": 22, + "name": "makeTestWalletDB" + }, + { + "args": [ + "session", + "dbTable", + "mCache", + "j" + ], + "lineno": 34, + "name": "getManifests" + }, + { + "args": [ + "session", + "dbTable", + "isTrusted", + "map", + "j" + ], + "lineno": 48, + "name": "saveManifests" + }, + { + "args": [ + "session", + "serialized" + ], + "lineno": 62, + "name": "addValidatorManifest" + }, + { + "args": [ + "session" + ], + "lineno": 66, + "name": "clearNodeIdentity" + }, + { + "args": [ + "session" + ], + "lineno": 74, + "name": "getNodeIdentity" + }, + { + "args": [ + "session", + "j" + ], + "lineno": 87, + "name": "getPeerReservationTable" + }, + { + "args": [ + "session", + "nodeId", + "description" + ], + "lineno": 97, + "name": "insertPeerReservation" + }, + { + "args": [ + "session", + "nodeId" + ], + "lineno": 106, + "name": "deletePeerReservation" + }, + { + "args": [ + "session" + ], + "lineno": 114, + "name": "createFeatureVotes" + }, + { + "args": [ + "session", + "callback" + ], + "lineno": 124, + "name": "readAmendments" + }, + { + "args": [ + "session", + "amendment", + "name", + "vote" + ], + "lineno": 139, + "name": "voteAmendment" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/Wallet.h.ai.md b/include/xrpl/server/Wallet.h.ai.md new file mode 100644 index 0000000000..f4798c2df9 --- /dev/null +++ b/include/xrpl/server/Wallet.h.ai.md @@ -0,0 +1,29 @@ +# `include/xrpl/server/Wallet.h` — Wallet Database Interface + +This header defines the persistence layer for a rippled node's local operational state: its cryptographic identity, validator manifests, peer reservations, and amendment vote preferences. Despite its name, this has nothing to do with cryptocurrency wallets in the user-facing sense. The "wallet" is a SQLite database (typically `wallet.db`) that stores the small set of configuration state that must survive restarts but doesn't belong in the consensus ledger itself. + +## What Gets Stored + +The wallet database is the authority for four distinct categories of node-local state, each backed by its own SQL table: + +**Node identity** (`NodeIdentity` table): A secp256k1 keypair that uniquely identifies this node on the peer-to-peer network. `getNodeIdentity()` implements lazy generation — it reads the existing keypair from the database, validates that the public key correctly derives from the private key, and only generates a fresh random keypair if none is found or if the stored pair fails that consistency check. Once generated, the identity is written back immediately so it persists across restarts. `clearNodeIdentity()` erases it, forcing regeneration on the next call. + +**Validator manifests** (`ValidatorManifests` and `NodeManifests` tables): Binary-encoded certificates that allow validators to rotate their ephemeral signing keys without changing their stable master public key identity. `getManifests()` loads raw blobs from a named table, deserializes each with `deserializeManifest()`, verifies the cryptographic signature, and feeds valid manifests into a `ManifestCache`. `saveManifests()` does the inverse: it wraps the operation in a transaction, deletes all existing rows, then re-inserts only manifests belonging to trusted validators plus all revocation manifests regardless of trust. The full-delete-before-insert pattern (rather than upsert) ensures the stored set stays clean when trust configuration changes. + +**Peer reservations** (`PeerReservations` table): A named allowlist of peer nodes by public key, used to guarantee connection slots for specific peers even under load. `insertPeerReservation()` uses SQLite's `ON CONFLICT DO UPDATE` upsert — a different strategy than manifest saving, appropriate here because individual entries are mutable in place. `getPeerReservationTable()` returns the full set as an `unordered_set, KeyEqual>`, matching the in-memory type used by `PeerReservationTable`. + +**Amendment votes** (`FeatureVotes` table): Records of the operator's preferences on XRPL protocol amendments. The design here has an interesting quirk: `voteAmendment()` always INSERTs a new row rather than updating an existing one, so the table can accumulate multiple entries per amendment hash. `readAmendments()` resolves this with a SQL window function — `RANK() OVER (PARTITION BY AmendmentHash ORDER BY ROWID DESC)` — selecting only the most recent vote per amendment. This append-only approach means a vote history is retained, but only the latest entry is ever acted upon. The `AmendmentVote` enum carries a notable comment: the integer representations are historically inverted (`up = 0`, `down = 1`), a legacy inconsistency that was never corrected. `createFeatureVotes()` conditionally creates the table and returns `true` if it already existed, letting callers distinguish first-time setup from re-initialization. + +## Database Setup + +`makeWalletDB()` constructs and opens the database by passing `WalletDBName` (a constant from `DBInit.h`) and `WalletDBInit` (the array of SQL `CREATE TABLE IF NOT EXISTS` statements) to `DatabaseCon`. This makes schema initialization automatic on first open. `makeTestWalletDB()` takes an explicit database name, enabling test cases to open isolated instances without colliding with each other or with production state. + +## Session Design + +All functions accept a raw `soci::session&` rather than a `DatabaseCon&` or `LockedSociSession`. This is a deliberate layering choice: callers obtain a `LockedSociSession` (which holds the mutex) via `DatabaseCon::checkoutDb()`, then dereference it to pass into these functions. The wallet functions themselves are stateless — they execute SQL against whatever session they receive. This avoids re-locking within the same thread (which would deadlock given `DatabaseCon`'s `recursive_mutex`) and keeps the functions usable in test contexts where callers control session lifetime directly. + +The `boost::optional` types scattered throughout the SOCI calls (rather than `std::optional`) reflect a SOCI library constraint noted in code comments: SOCI's output binding API predates C++17 and requires Boost's optional type. + +## Relationship to `ManifestCache` + +The wallet functions sit one layer below `ManifestCache`. The cache's own `load()` and `save()` methods delegate to `getManifests()` and `saveManifests()` respectively, passing their internal `hash_map`. The wallet header therefore acts as the SQL binding layer for manifest persistence, while `ManifestCache` handles the in-memory concurrency model (a `shared_mutex` protecting its two hash maps). \ No newline at end of file diff --git a/include/xrpl/server/Writer.h.ai.json b/include/xrpl/server/Writer.h.ai.json new file mode 100644 index 0000000000..054e4fc3cb --- /dev/null +++ b/include/xrpl/server/Writer.h.ai.json @@ -0,0 +1,59 @@ +{ + "args": [ + { + "lineno": 17, + "name": "bytes" + }, + { + "lineno": 24, + "name": "bytes" + }, + { + "lineno": 24, + "name": "resume" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "Writer" + } + ], + "description": "Defines the abstract Writer class in the xrpl namespace, which provides an interface for managing and serializing data buffers, typically for network communication.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/Writer.h", + "functions": [ + { + "args": [], + "lineno": 12, + "name": "complete" + }, + { + "args": [ + "bytes" + ], + "lineno": 17, + "name": "consume" + }, + { + "args": [ + "bytes", + "resume" + ], + "lineno": 24, + "name": "prepare" + }, + { + "args": [], + "lineno": 31, + "name": "data" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/Writer.h.ai.md b/include/xrpl/server/Writer.h.ai.md new file mode 100644 index 0000000000..89be3001c7 --- /dev/null +++ b/include/xrpl/server/Writer.h.ai.md @@ -0,0 +1,48 @@ +# `include/xrpl/server/Writer.h` + +## Role in the System + +`Writer` is a pure abstract interface that decouples HTTP response data *production* from data *transmission* in the XRPL server layer. It exists to enable streaming and asynchronous response generation: the server can begin draining a `Writer` over a socket without requiring the full response to be materialized in memory first. + +The interface sits at the boundary between the network I/O layer (`BaseHTTPPeer`, `Session`) and application-level response handlers. A handler constructs a `Writer` implementation (or receives one in a `Handoff`), hands it to the session via `Session::write(std::shared_ptr, bool keep_alive)`, and the session drives it to completion asynchronously. + +## The Four-Method Contract + +The four virtual methods implement a **pull-based streaming protocol** between the I/O peer and the response source: + +- **`complete()`** — Signals that the writer has no remaining data to emit. The I/O loop checks this after each consume cycle to know when to stop. + +- **`prepare(bytes, resume)`** — Asks the writer to make at least `bytes` bytes available in its internal buffer. If the data is already ready, it returns `true` immediately. If more data must be generated asynchronously (e.g., waiting on a database query or a chunked encoder), it stores the `resume` functor and returns `false`. The I/O coroutine interprets `false` as a suspension point and exits; the `resume` callback re-enters it when the writer is ready. + +- **`data()`** — Returns the current ready bytes as a `std::vector`. Because Boost.Asio `const_buffer` is a non-owning view, this avoids copying: the vector holds pointers into the writer's own storage. + +- **`consume(bytes)`** — Advances the writer's internal read pointer by the number of bytes actually sent by the network layer. This mirrors the Boost.Asio dynamic buffer convention (`prepare`/`commit`/`consume`) adapted for the cross-layer ownership model here. + +## How the I/O Peer Drives the Writer + +`BaseHTTPPeer::do_writer()` is the coroutine that consumes a `Writer`. It runs on a Boost.Asio `strand` inside a stackful coroutine (`yield_context`): + +``` +for (;;) { + if (!writer->prepare(bufferSize, resume)) + return; // suspend; resume() will re-enter this coroutine + async_write(stream, writer->data(), transfer_at_least(1), do_yield[ec]); + writer->consume(bytes_transferred); + if (writer->complete()) + break; +} +``` + +The `resume` functor captured inside `do_writer()` is a lambda that calls `util::spawn(strand_, ...)` to re-post the entire `do_writer` coroutine back onto the strand. This means the writer can produce data from any thread or async callback; it just calls `resume()` and the I/O peer picks up exactly where it left off. There is no shared mutable state between the writer and the peer during the suspension window — the writer holds its data, and the peer holds a `shared_ptr` keeping it alive. + +## `SimpleWriter`: The Synchronous Case + +`SimpleWriter` (`SimpleWriter.h`) is the only concrete implementation in this header directory. Its constructor eagerly serializes a complete Boost.Beast HTTP message into a `multi_buffer`. Because all data is available immediately, its `prepare()` unconditionally returns `true`, ignoring the `resume` functor entirely. `data()` fans the `multi_buffer` segments out into `const_buffer` views, and `consume()` delegates to `multi_buffer::consume()`. It is marked **deprecated** — presumably because eagerly serializing the full response to a `multi_buffer` defeats the incremental-production purpose of the `Writer` abstraction and is wasteful for large bodies. + +## Integration via `Handoff` + +`Handoff` (`Handoff.h`) carries a `std::shared_ptr` as its `response` field. When the server's connection-acceptance logic decides to respond without upgrading to WebSocket, it populates `Handoff::response` and the `BaseHTTPPeer` calls `write(handoff.response, handoff.keep_alive)`, which spawns `do_writer`. This lets protocol-level handlers (e.g., the upgrade negotiation path) produce responses without knowing anything about the socket or I/O model. + +## Design Rationale + +The `resume` functor approach is notable: rather than imposing a particular async model (promises, futures, coroutines) on writer implementations, it passes a plain `std::function`. Any implementation — whether producer-side threaded, Asio-based, or even synchronous — can call `resume()` at any time to re-activate the draining loop. The trade-off is that the writer must correctly call `resume()` exactly once per suspension; misuse (calling it zero times, or twice) would either stall the connection or cause a coroutine re-entry race. This contract is implicit and unguarded by the interface, placing correctness responsibility entirely on implementors. \ No newline at end of file diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h.ai.json b/include/xrpl/server/detail/BaseHTTPPeer.h.ai.json new file mode 100644 index 0000000000..0db7f3239e --- /dev/null +++ b/include/xrpl/server/detail/BaseHTTPPeer.h.ai.json @@ -0,0 +1,183 @@ +{ + "args": [ + { + "lineno": 13, + "name": "Handler" + }, + { + "lineno": 13, + "name": "Impl" + }, + { + "lineno": 37, + "name": "ConstBufferSequence" + } + ], + "classes": [ + { + "args": [ + "port", + "handler", + "executor", + "journal", + "remote_address", + "buffers" + ], + "lineno": 13, + "name": "BaseHTTPPeer" + } + ], + "description": "Defines the BaseHTTPPeer template class, which represents an active HTTP connection/session in the xrpl server, handling asynchronous read/write operations, timeouts, and session lifecycle management.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/BaseHTTPPeer.h", + "functions": [ + { + "args": [], + "lineno": 54, + "name": "session" + }, + { + "args": [], + "lineno": 59, + "name": "close" + }, + { + "args": [], + "lineno": 65, + "name": "impl" + }, + { + "args": [ + "ec", + "what" + ], + "lineno": 70, + "name": "fail" + }, + { + "args": [], + "lineno": 73, + "name": "start_timer" + }, + { + "args": [], + "lineno": 76, + "name": "cancel_timer" + }, + { + "args": [], + "lineno": 79, + "name": "on_timer" + }, + { + "args": [ + "do_yield" + ], + "lineno": 82, + "name": "do_read" + }, + { + "args": [ + "ec", + "bytes_transferred" + ], + "lineno": 85, + "name": "on_write" + }, + { + "args": [ + "writer", + "keep_alive", + "do_yield" + ], + "lineno": 88, + "name": "do_writer" + }, + { + "args": [], + "lineno": 91, + "name": "do_request" + }, + { + "args": [], + "lineno": 94, + "name": "do_close" + }, + { + "args": [], + "lineno": 98, + "name": "journal" + }, + { + "args": [], + "lineno": 103, + "name": "port" + }, + { + "args": [], + "lineno": 108, + "name": "remoteAddress" + }, + { + "args": [], + "lineno": 113, + "name": "request" + }, + { + "args": [ + "buffer", + "bytes" + ], + "lineno": 118, + "name": "write" + }, + { + "args": [ + "writer", + "keep_alive" + ], + "lineno": 121, + "name": "write" + }, + { + "args": [], + "lineno": 124, + "name": "detach" + }, + { + "args": [], + "lineno": 127, + "name": "complete" + }, + { + "args": [ + "graceful" + ], + "lineno": 130, + "name": "close" + }, + { + "args": [ + "port", + "handler", + "executor", + "journal", + "remote_address", + "buffers" + ], + "lineno": 137, + "name": "BaseHTTPPeer" + }, + { + "args": [], + "lineno": 151, + "name": "~BaseHTTPPeer" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/BaseHTTPPeer.h.ai.md b/include/xrpl/server/detail/BaseHTTPPeer.h.ai.md new file mode 100644 index 0000000000..1bb1f780a0 --- /dev/null +++ b/include/xrpl/server/detail/BaseHTTPPeer.h.ai.md @@ -0,0 +1,53 @@ +# `BaseHTTPPeer.h` — CRTP Base for Active HTTP Connections + +## Role in the System + +`BaseHTTPPeer` is the heart of the XRP Ledger's HTTP server layer. It sits between the transport socket (plain TCP or TLS) and the application-level `Handler`, implementing the complete lifecycle of a single HTTP connection: accept, read, dispatch, write, keep-alive, and close. It lives in `include/xrpl/server/detail/` because it is an internal building block, not part of the public API surface. + +Two concrete classes derive from it using the Curiously Recurring Template Pattern (CRTP): `PlainHTTPPeer` for unencrypted connections and `SSLHTTPPeer` for TLS. Both follow the same coroutine-driven read/write model; they differ only in the type of `stream_` they own and in their `do_close()` and `do_request()` implementations. + +## Inheritance Design + +`BaseHTTPPeer` inherits from two bases simultaneously. + +From `io_list::work` it gets lifecycle membership in the server's I/O registry. When the server shuts down, `io_list::close()` iterates over every live `work` item and calls `close()` on it, which closes the socket. The `work` destructor decrements the registry counter; the last item to be destroyed signals a condition variable that `io_list::join()` waits on. This gives the server a clean, blocking shutdown path. + +From `Session` it provides the public API that `Handler` implementations use: `journal()`, `port()`, `remoteAddress()`, `request()`, `write()`, `detach()`, `complete()`, and `close(bool)`. Three of these (`detach`, `complete`, and `close(bool)`) are marked `DEPRECATED` in the implementation, signalling a transitional design where the newer `onHandoff` callback returns an immediate response object rather than relying on the session's deferred write primitives. + +## CRTP Access to `stream_` + +The CRTP idiom solves a concrete problem: the stream type differs between plain and TLS peers, but all the I/O logic in the base class must reach the stream. The `impl()` helper performs an unchecked downcast to `Impl*`, giving the base access to `impl().stream_`. Every call to `boost::beast::get_lowest_layer(impl().stream_)` reaches the underlying `tcp_stream`'s timeout and close machinery, regardless of whether TLS is layered on top. + +## Read Loop + +`do_read()` is a Boost.Asio stackful coroutine spawned via `util::spawn`. The `util::spawn` wrapper ensures that unhandled exceptions propagate to `io_context::run()` rather than being silently swallowed, a regression that was introduced in Boost 1.84. + +Inside `do_read()`, the timer is armed before the async read and cancelled after it. Beast's `async_read` both parses the HTTP message into `message_` and accumulates bytes in `read_buf_`. On success, `do_request()` is called — a pure virtual method that the derived class implements to invoke the `Handler`. If the client closed the connection, `do_close()` is called; a timeout calls `on_timer()` which routes through `fail()`. + +The constructor accepts a `ConstBufferSequence` and pre-copies it into `read_buf_`. This seeds the read buffer with any bytes the `Door` (the acceptor) already captured during protocol detection — the bytes are not lost when ownership transfers to the peer. + +## Dual Write Queue + +The `write(void*, size_t)` path enqueues a heap-allocated copy of the data into `wq_`. The `buffer` inner struct owns a `unique_ptr` so the caller's memory can be released immediately. The queue is protected by `mutex_`. + +The write engine uses a deliberate two-queue rotation. `wq_` is the producer queue; `wq2_` is the in-flight queue. In `on_write()`, after a batch completes, `wq2_` is cleared, then atomically swapped with `wq_`. This means that while an async scatter-gather write is in progress over `wq2_`, producers can freely enqueue into `wq_` without contention. Only when a new write needs to start does the mutex matter — and even then, only for the moment needed to check whether the queues are empty and decide whether to post `on_write`. + +The check `wq_.size() == 1 && wq2_.size() == 0` in `write()` detects the idle state: if this is the first item in `wq_` and `wq2_` is also empty, no write loop is running, so it must be kicked off. If either queue was already non-empty, an in-flight `on_write` will pick up the new data when it drains. + +## Streaming Writer Path + +The `write(shared_ptr, bool)` overload handles large or lazily-generated responses. `do_writer()` runs as a second coroutine and calls `writer->prepare(bufferSize, resume)` in a loop. The `resume` callback, if invoked by the writer when data becomes available, re-enters `do_writer()` by spawning a new coroutine on the strand — allowing the writer to stall mid-response without blocking the strand itself. After the writer signals `complete()`, the connection either closes or re-enters `do_read()` depending on the `keep_alive` flag. + +## Timeout Strategy + +Timeouts are managed through Beast's built-in expiry mechanism on the lowest layer (`expires_after` / `expires_never`). Rather than maintaining a separate timer, the stream simply sets an expiry before every I/O operation and clears it after. A timed-out operation returns `boost::beast::error::timeout`, which is detected inline and routed to `on_timer()` → `fail()`. + +Loopback connections receive a three-second timeout (`timeoutSecondsLocal`) versus thirty seconds for remote clients. This is an explicit optimization: in-process test environments like `Env` run over `127.0.0.1` and should not tolerate slow connections; cutting the timeout also makes stuck-test detection faster. + +## Error Handling and Teardown + +`fail()` is idempotent: it records only the first error in `ec_` and ignores subsequent calls, including `operation_aborted` (which fires when a pending async op is cancelled because the socket is being closed). This prevents double-close races. The final value of `ec_` is forwarded to `handler_.onClose()` in the destructor so the handler can distinguish clean closures from error-driven teardowns. + +`close(bool graceful)` honours in-flight writes before closing. With `graceful = true` it sets `graceful_` and only proceeds to `do_close()` once `on_write()` finds both queues empty. With `graceful = false` it closes the socket immediately, which will cause any in-flight async operations to complete with `operation_aborted`, unwinding the call stack cleanly. + +The strand-safety pattern is consistent throughout: any method callable from outside the strand checks `strand_.running_in_this_thread()` and posts to the strand if needed. This ensures all mutable state (`wq_`, `graceful_`, `complete_`, `message_`) is modified only from the strand, while the `mutex_` exclusively guards the cross-thread enqueue path into `wq_`. \ No newline at end of file diff --git a/include/xrpl/server/detail/BasePeer.h.ai.json b/include/xrpl/server/detail/BasePeer.h.ai.json new file mode 100644 index 0000000000..53acb56dd1 --- /dev/null +++ b/include/xrpl/server/detail/BasePeer.h.ai.json @@ -0,0 +1,69 @@ +{ + "args": [ + { + "lineno": 29, + "name": "port" + }, + { + "lineno": 29, + "name": "handler" + }, + { + "lineno": 29, + "name": "executor" + }, + { + "lineno": 29, + "name": "remote_address" + }, + { + "lineno": 29, + "name": "journal" + } + ], + "classes": [ + { + "args": [ + "Port const& port", + "Handler& handler", + "boost::asio::executor const& executor", + "endpoint_type remote_address", + "beast::Journal journal" + ], + "lineno": 13, + "name": "BasePeer" + } + ], + "description": "Defines the BasePeer template class, providing common functionality for all peer connections in the XRPL server, including resource management, strand handling, and connection closing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/BasePeer.h", + "functions": [ + { + "args": [ + "Port const& port", + "Handler& handler", + "boost::asio::executor const& executor", + "endpoint_type remote_address", + "beast::Journal journal" + ], + "lineno": 29, + "name": "BasePeer" + }, + { + "args": [], + "lineno": 38, + "name": "close" + }, + { + "args": [], + "lineno": 44, + "name": "impl" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/BasePeer.h.ai.md b/include/xrpl/server/detail/BasePeer.h.ai.md new file mode 100644 index 0000000000..ce3d0b6cba --- /dev/null +++ b/include/xrpl/server/detail/BasePeer.h.ai.md @@ -0,0 +1,62 @@ +# `BasePeer.h` — Shared Foundation for WebSocket Peer Connections + +`BasePeer` is a compact, two-template-parameter mixin that captures the state and strand-safety logic shared by every WebSocket peer connection in the XRPL server. It sits one level below `BaseWSPeer` in the inheritance chain, providing the five fields and the thread-safe `close()` implementation that all concrete WebSocket peers — `PlainWSPeer` and `SSLWSPeer` — depend on. + +## Role in the Peer Hierarchy + +The XRPL server's connection type hierarchy splits at the protocol level. HTTP connections use `BaseHTTPPeer` (which independently owns its own strand and port reference), while WebSocket connections use `BaseWSPeer`, which in turn derives from `BasePeer`. This file is therefore the root of the WebSocket peer family only. + +`BasePeer` itself inherits from `io_list::work`, plugging every peer into the server's lifetime management system (described below). `BaseWSPeer` then extends `BasePeer` and also inherits `WSSession`, adding the full WebSocket read/write/ping/timer machinery. + +## The CRTP `impl()` Pattern + +Both template parameters carry specific responsibilities. `Handler` is the application-level callback sink — typically the object that processes XRPL protocol messages via `onWSMessage()`. `Impl` is the fully-resolved concrete class (`PlainWSPeer` or `SSLWSPeer`), and the class exploits **CRTP** (Curiously Recurring Template Pattern) through the private `impl()` accessor: + +```cpp +Impl& impl() { return *static_cast(this); } +``` + +This allows `BasePeer::close()` to reach into `impl().ws_` — the concrete WebSocket stream object — without a virtual dispatch. The alternative of a virtual getter for the stream would add indirection on the hot path and complicate the layering of SSL vs. plain streams. + +## Lifetime Management via `io_list::work` + +`io_list` is the server's registry of active asynchronous work objects. Every peer registers itself via `io_list::emplace()`, which atomically inserts a `shared_ptr` into the registry. When the server shuts down, `io_list::close()` iterates all registered work and calls the virtual `close()` on each, then blocks in `join()` until every work object's reference count reaches zero. + +`BasePeer::close()` is the implementation of that virtual `close()`. The registration/deregistration cycle is entirely automatic: `io_list::work::destroy()` (called from the destructor) removes the peer from the registry and fires the completion finisher if the count reaches zero. + +## Strand Enforcement in `close()` + +```cpp +void BasePeer::close() +{ + if (!strand_.running_in_this_thread()) + return post(strand_, std::bind(&BasePeer::close, impl().shared_from_this())); + error_code ec; + xrpl::get_lowest_layer(impl().ws_).socket().close(ec); +} +``` + +This pattern — check the strand, re-post if not running on it, then act — appears throughout the entire peer hierarchy. The strand serializes all async operations belonging to a single peer. Since `io_list::close()` calls peer `close()` from arbitrary threads, the re-post ensures the actual socket teardown happens in the correct execution context. Ignoring the error code on the `socket().close(ec)` call is intentional: by the time `close()` is invoked, the peer is being torn down regardless of whether the close operation itself fails. + +`get_lowest_layer()` in `LowestLayer.h` abstracts a Boost version incompatibility: before Boost 1.70, accessing the underlying TCP socket required `t.lowest_layer()`; afterward, it uses `boost::beast::get_lowest_layer(t)`. + +## Per-Connection Log Identity + +The constructor generates a globally unique, monotonically increasing connection ID using a local `static std::atomic`: + +```cpp +sink_(journal.sink(), [] { + static std::atomic id{0}; + return "##" + std::to_string(++id) + " "; +}()) +``` + +The `beast::WrappedSink` prefixes every log message from this peer with the ID string (e.g., `"##42 "`). The `j_` journal is then constructed from this wrapped sink, so all logging through `this->j_` in derived classes is automatically tagged. This makes it straightforward to correlate log lines from the same connection across reads, writes, and timeouts. + +## `executor_work_guard` and Executor Lifetime + +`work_` holds a `boost::asio::executor_work_guard` for the peer's executor. This prevents the `io_context` from running out of work and stopping while the peer still has pending async operations. The guard is held for the full lifetime of the `BasePeer` object and released when the peer is destroyed. + +## Relationship to `BaseHTTPPeer` + +Notably, `BaseHTTPPeer` does not derive from `BasePeer` — it re-declares its own `port_`, `handler_`, `work_`, `strand_`, and `remote_address_` fields independently. The two hierarchies share the same conceptual structure but were implemented separately, reflecting that HTTP and WebSocket peers have sufficiently different lifecycles (e.g., HTTP supports keep-alive and detach) that sharing a base class would require careful abstraction. `BasePeer` is exclusively for the WebSocket branch. \ No newline at end of file diff --git a/include/xrpl/server/detail/BaseWSPeer.h.ai.json b/include/xrpl/server/detail/BaseWSPeer.h.ai.json new file mode 100644 index 0000000000..92e3b7e477 --- /dev/null +++ b/include/xrpl/server/detail/BaseWSPeer.h.ai.json @@ -0,0 +1,197 @@ +{ + "args": [ + { + "lineno": 16, + "name": "Handler" + }, + { + "lineno": 16, + "name": "Impl" + }, + { + "lineno": 47, + "name": "Body" + }, + { + "lineno": 47, + "name": "Headers" + }, + { + "lineno": 138, + "name": "String" + } + ], + "classes": [ + { + "args": [ + "port", + "handler", + "executor", + "timer", + "remote_address", + "request", + "journal" + ], + "lineno": 18, + "name": "BaseWSPeer" + } + ], + "description": "Defines the BaseWSPeer template class, which implements the core logic for an active WebSocket peer/session in the XRPL server, handling WebSocket handshake, reading, writing, ping/pong, timers, and closure.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/BaseWSPeer.h", + "functions": [ + { + "args": [ + "port", + "handler", + "executor", + "timer", + "remote_address", + "request", + "journal" + ], + "lineno": 49, + "name": "BaseWSPeer" + }, + { + "args": [], + "lineno": 67, + "name": "run" + }, + { + "args": [], + "lineno": 73, + "name": "port" + }, + { + "args": [], + "lineno": 78, + "name": "request" + }, + { + "args": [], + "lineno": 83, + "name": "remote_endpoint" + }, + { + "args": [ + "w" + ], + "lineno": 88, + "name": "send" + }, + { + "args": [], + "lineno": 93, + "name": "close" + }, + { + "args": [ + "reason" + ], + "lineno": 98, + "name": "close" + }, + { + "args": [], + "lineno": 103, + "name": "complete" + }, + { + "args": [], + "lineno": 109, + "name": "impl" + }, + { + "args": [ + "ec" + ], + "lineno": 114, + "name": "on_ws_handshake" + }, + { + "args": [], + "lineno": 116, + "name": "do_write" + }, + { + "args": [ + "ec" + ], + "lineno": 118, + "name": "on_write" + }, + { + "args": [ + "ec" + ], + "lineno": 120, + "name": "on_write_fin" + }, + { + "args": [], + "lineno": 122, + "name": "do_read" + }, + { + "args": [ + "ec" + ], + "lineno": 124, + "name": "on_read" + }, + { + "args": [ + "ec" + ], + "lineno": 126, + "name": "on_close" + }, + { + "args": [], + "lineno": 128, + "name": "start_timer" + }, + { + "args": [], + "lineno": 130, + "name": "cancel_timer" + }, + { + "args": [ + "ec" + ], + "lineno": 132, + "name": "on_ping" + }, + { + "args": [ + "kind", + "payload" + ], + "lineno": 134, + "name": "on_ping_pong" + }, + { + "args": [ + "ec" + ], + "lineno": 136, + "name": "on_timer" + }, + { + "args": [ + "ec", + "what" + ], + "lineno": 139, + "name": "fail" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/BaseWSPeer.h.ai.md b/include/xrpl/server/detail/BaseWSPeer.h.ai.md new file mode 100644 index 0000000000..c3d6b2da10 --- /dev/null +++ b/include/xrpl/server/detail/BaseWSPeer.h.ai.md @@ -0,0 +1,95 @@ +# `BaseWSPeer.h` — WebSocket Peer Base Template + +## Role in the System + +`BaseWSPeer` is the shared implementation core for all active WebSocket connections in the XRPL server. It sits in the `include/xrpl/server/detail/` layer — the "plumbing" that is never exposed to application code — and provides everything needed to manage a live WebSocket session: the upgrade handshake, chunked message writing, read-dispatch, liveness pinging, and orderly teardown. The two concrete subclasses that instantiate it, `PlainWSPeer` and `SSLWSPeer`, differ only in whether the underlying socket is bare TCP or wrapped in a TLS stream; all protocol logic lives here. + +## Template Architecture (CRTP + Policy) + +The class is parameterized on two types: + +```cpp +template +class BaseWSPeer : public BasePeer, public WSSession +``` + +`Handler` is the application layer — it receives parsed messages via `handler_.onWSMessage()` and drives the session lifecycle. `Impl` is the concrete subclass (`PlainWSPeer` or `SSLWSPeer`), providing the `ws_` stream member that `BaseWSPeer` accesses through the CRTP helper `impl()`. Because `ws_` has different types in each subclass (`websocket::stream` vs. `websocket::stream&>`), making it a template parameter avoids virtual dispatch on every I/O call while still sharing all protocol logic. + +`BasePeer` contributes the `strand_`, `port_`, `handler_`, and `remote_address_` members, plus an `executor_work_guard` that keeps the Asio executor alive for the connection's lifetime. `WSSession` is the abstract interface visible to the rest of the server — it defines `run()`, `send()`, `close()`, and `complete()`, all of which `BaseWSPeer` implements. + +## Strand-Based Concurrency + +Every public entry point — `run()`, `send()`, `close()`, and `complete()` — begins with the same pattern: + +```cpp +if (!strand_.running_in_this_thread()) + return post(strand_, std::bind(&BaseWSPeer::run, impl().shared_from_this())); +``` + +If called from outside the strand (e.g., from a handler thread that wants to push a response), the call is posted back to the strand and the caller returns immediately. Once on the strand, operations can read and write shared state (`wq_`, `do_close_`, `ping_active_`, etc.) without any mutex. This is idiomatic Asio strand use — serialization through scheduling rather than locking. + +## Write Pipeline + +Outbound messages implement the `WSMsg` abstract interface, whose key method is: + +```cpp +virtual std::pair> +prepare(std::size_t bytes, std::function resume) = 0; +``` + +`BaseWSPeer` drives this with a `std::list>` write queue (`wq_`). When `send()` adds the first entry, it immediately calls `on_write({})` to start the pump. Subsequent messages wait until the front of the queue is fully drained. + +`on_write` calls `prepare(65536, ...)` — requesting at most 64 KB per call. The tribool return disambiguates three states: `indeterminate` means the data is not yet ready (the `resume` callback will restart the pump); `false` means more chunks follow (routes to `on_write` again); `true` means this is the final chunk (routes to `on_write_fin`). `on_write_fin` pops the queue head and either issues a WebSocket close (if `do_close_` is set) or starts writing the next queued message. + +This chunked design lets large ledger responses stream out without ever copying the full payload into a single contiguous buffer. + +## Backpressure and Client-Too-Slow Handling + +`send()` enforces a hard queue depth limit from the port configuration: + +```cpp +if (wq_.size() > port().ws_queue_limit) +{ + cr_.reason = "Policy error: client is too slow."; + wq_.erase(std::next(wq_.begin()), wq_.end()); + close(cr_); + return; +} +``` + +When a slow client allows the queue to grow beyond the limit, pending messages are dropped and the connection is closed with a policy error. Keeping the first queued message in place allows the current write to finish before the close handshake is issued, ensuring a clean WebSocket close frame reaches the client. + +## Deferred Close + +`close()` sets the `do_close_` flag rather than immediately issuing an async close: + +```cpp +if (wq_.empty()) + impl().ws_.async_close(reason, ...); +else + cr_ = reason; // defer: on_write_fin will close after draining +``` + +If there are queued writes, the close is deferred until `on_write_fin` drains the queue. This guarantees that any already-queued responses (e.g., the final message before a clean disconnect) are delivered before the connection tears down. + +## Read Side and Handler Handoff + +`do_read` issues a single `async_read` that accumulates a complete WebSocket message into `rb_` (a `boost::beast::multi_buffer`). On success, `on_read` extracts the buffer sequence and calls `handler_.onWSMessage()`. Critically, the next `do_read` is not posted immediately — it is triggered only when the handler calls `complete()` on the session. This creates natural backpressure: the server will not accept another message from a client until it has finished processing the previous one. + +## Liveness: Ping/Pong and Timer + +`start_timer()` arms a `waitable_timer` with a 30-second timeout for remote clients or 3 seconds for loopback connections. When the timer fires in `on_timer()`, two scenarios can occur: + +1. **First timeout, no ping outstanding**: A ping frame is sent with a cryptographically random 8-byte payload (using `crypto_prng()`). The comment acknowledges this is "probably overkill" but ensures the payload cannot be guessed and a spoofed pong cannot reset the timer. `close_on_timer_` is set and the timer restarts. + +2. **Second timeout, ping sent but no matching pong received**: The connection is closed with a timed-out error code. + +The `on_ping_pong` control callback (registered via Beast's `control_callback` mechanism) checks whether the received pong matches the sent payload and clears `close_on_timer_` if so. An important subtlety: the `control_callback_` is stored as a class member (`std::function`), not a temporary, because Beast holds only a non-owning reference to the callable object — storing it inline in `run()` would cause a use-after-free. + +## Error Handling + +All failure paths funnel through `fail()`, which is strand-assert guarded. It records only the first error (`ec_` check prevents double-failure), cancels the timer, and closes the raw TCP socket directly via `get_lowest_layer`. The `operation_aborted` error code is treated as benign — it signals that a pending async operation was cancelled intentionally (e.g., by `cancel_timer()`) rather than representing a real I/O failure, so it is silently ignored in several callbacks. + +## Relationship to Concrete Peers + +`PlainWSPeer` and `SSLWSPeer` each add exactly one member — the `ws_` stream — and a constructor that wires up the executor and timer. Neither adds any protocol logic. The entire behavioral surface is in `BaseWSPeer`, making this a textbook CRTP policy class that achieves static polymorphism without virtual dispatch in the hot I/O path. \ No newline at end of file diff --git a/include/xrpl/server/detail/Door.h.ai.json b/include/xrpl/server/detail/Door.h.ai.json new file mode 100644 index 0000000000..ddef3e7e68 --- /dev/null +++ b/include/xrpl/server/detail/Door.h.ai.json @@ -0,0 +1,124 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Handler& handler", + "boost::asio::io_context& io_context", + "Port const& port", + "beast::Journal j" + ], + "lineno": 22, + "name": "Door" + }, + { + "args": [ + "Port const& port", + "Handler& handler", + "boost::asio::io_context& ioc", + "stream_type&& stream", + "endpoint_type remote_address", + "beast::Journal j" + ], + "lineno": 36, + "name": "Detector" + }, + { + "args": [], + "lineno": 59, + "name": "FDStats" + } + ], + "description": "Defines the xrpl::Door class template, which represents a listening socket for accepting incoming TCP connections (HTTP/HTTPS/WebSocket) in the XRPL server, including SSL detection, resource management, and connection throttling based on file descriptor usage.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/Door.h", + "functions": [ + { + "args": [ + "Port const& port", + "Handler& handler", + "boost::asio::io_context& ioc", + "stream_type&& stream", + "endpoint_type remote_address", + "beast::Journal j" + ], + "lineno": 70, + "name": "Door::Detector::Detector" + }, + { + "args": [], + "lineno": 82, + "name": "Door::Detector::run" + }, + { + "args": [], + "lineno": 88, + "name": "Door::Detector::close" + }, + { + "args": [ + "boost::asio::yield_context do_yield" + ], + "lineno": 93, + "name": "Door::Detector::do_detect" + }, + { + "args": [], + "lineno": 120, + "name": "Door::reOpen" + }, + { + "args": [ + "Handler& handler", + "boost::asio::io_context& io_context", + "Port const& port", + "beast::Journal j" + ], + "lineno": 157, + "name": "Door::Door" + }, + { + "args": [], + "lineno": 167, + "name": "Door::run" + }, + { + "args": [], + "lineno": 174, + "name": "Door::close" + }, + { + "args": [ + "bool ssl", + "ConstBufferSequence const& buffers", + "stream_type&& stream", + "endpoint_type remote_address" + ], + "lineno": 185, + "name": "Door::create" + }, + { + "args": [ + "boost::asio::yield_context do_yield" + ], + "lineno": 197, + "name": "Door::do_accept" + }, + { + "args": [], + "lineno": 241, + "name": "Door::query_fd_stats" + }, + { + "args": [], + "lineno": 266, + "name": "Door::should_throttle_for_fds" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 18, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/Door.h.ai.md b/include/xrpl/server/detail/Door.h.ai.md new file mode 100644 index 0000000000..a21af74c55 --- /dev/null +++ b/include/xrpl/server/detail/Door.h.ai.md @@ -0,0 +1,58 @@ +# `Door.h` — TCP Listening Socket with Protocol Detection and FD-Aware Throttling + +## Role in the System + +`Door` is the outermost entry point for all inbound TCP connections in the XRPL server. Each configured `Port` (HTTP, HTTPS, WebSocket, WSS, peer protocol) gets one `Door` instance. Its job is to own a `boost::asio::ip::tcp::acceptor`, run a continuous accept loop, and hand accepted sockets off to the appropriate peer type — `PlainHTTPPeer` or `SSLHTTPPeer`. Everything downstream (request parsing, WebSocket upgrades, handler dispatch) is owned by those peer objects; `Door` is purely the front door. + +The class is a header-only template parameterized on `Handler`, the application-level callback type that implements `onAccept`, `onHandoff`, and `onRequest`. This templating avoids virtual dispatch on the hot path while keeping the networking layer decoupled from business logic. + +## Lifetime and the `io_list` Contract + +`Door` inherits from both `io_list::work` and `std::enable_shared_from_this`. The `io_list` is a reference-counted work registry: when `io_list::close()` is called it dispatches `close()` on every registered `work` item, and the destructor blocks until all work is destroyed. This lets the server shut down cleanly without ad-hoc join loops. + +Because `shared_from_this()` cannot be called inside a constructor, `Door` separates construction from activation: the constructor calls `reOpen()` to set up the acceptor (open, `SO_REUSEADDR`, bind, listen), while `run()` is called afterward to start the coroutine. The same split applies to `Detector`. + +`close()` is thread-safe: if called off the `Door`'s strand it posts back to the strand before canceling the backoff timer and closing the acceptor. Closing the acceptor causes the `async_accept` in progress to complete with `operation_aborted`, which breaks the accept loop. + +## The Accept Loop and Protocol Dispatch + +`do_accept` runs as a Boost.Asio stackful coroutine (`yield_context`). It loops while the acceptor is open, calling `async_accept` to obtain a raw `boost::beast::tcp_stream` and remote endpoint. + +After a successful accept, the loop checks the `ssl_` and `plain_` flags, which are computed once at construction from the `Port::protocol` set: + +```cpp +bool ssl_{ + port_.protocol.count("https") > 0 || port_.protocol.count("wss") > 0 || + port_.protocol.count("wss2") > 0 || port_.protocol.count("peer") > 0}; +bool plain_{ + port_.protocol.count("http") > 0 || port_.protocol.count("ws") > 0 || + port_.protocol.count("ws2")}; +``` + +If only one protocol family is configured (the common case), `create()` is called directly with the correct boolean — no sniffing overhead. If both SSL and plain are configured on the same port, a `Detector` is spawned to peek at the first bytes. + +## The `Detector` Inner Class + +`Detector` is itself an `io_list::work`, registered in the same `io_list` as `Door`. It owns the stream during the detection window. `do_detect` calls `boost::beast::async_detect_ssl`, which reads up to 16 bytes into a `multi_buffer` with a 15-second timeout. The buffer is passed directly into the `SSLHTTPPeer` or `PlainHTTPPeer` constructor, so those bytes are not lost — they become the head of the first read. This zero-copy design avoids any re-injection mechanism. + +If detection errors with anything other than `operation_aborted` (which is the normal shutdown path), only a trace-level log is emitted and the socket is silently dropped. This is intentional: a malformed or slow opener should not pollute error logs. + +## Exponential Backoff on Accept Errors + +`do_accept` implements two-tier backoff for resource exhaustion: + +**Reactive**: If `async_accept` returns `no_descriptors` (EMFILE) or `no_buffer_space` (ENOBUFS), the loop pauses on `backoff_timer_` and doubles `accept_delay_`, capped at `MAX_ACCEPT_DELAY` (2000 ms). After a successful accept, `accept_delay_` resets to `INITIAL_ACCEPT_DELAY` (50 ms). + +**Proactive (non-Windows)**: Before each `async_accept`, `should_throttle_for_fds()` checks whether free file descriptors have fallen below 30% of the process limit. It reads the FD count by enumerating `/proc/self/fd` (Linux) or `/dev/fd` (other POSIX) via `opendir`/`readdir`, subtracting 3 for `.`, `..`, and the `DIR*` itself. If the `FREE_FD_THRESHOLD` (0.70) is breached, the loop backs off before even attempting an accept, avoiding the EMFILE error entirely. + +The proactive path is silently skipped on Windows (returns `false`) and whenever `getrlimit` returns `RLIM_INFINITY` or fails — both cases where the check is meaningless or impossible. + +This two-layer design matters because EMFILE recovery on Linux is lossy: the kernel may queue further connections during the pause, but the OS backlog is finite. Catching the condition before it turns into an error gives the server a chance to drain existing connections before new ones arrive. + +## Thread Safety + +All mutable `Door` state is protected by a `strand_`. The `Detector` has its own independent strand. `close()` is explicitly strand-aware and posts to its strand if called from a foreign thread. The `io_list` mutex serializes `emplace` and `work` destruction. There is no shared mutable state between `Door`, `Detector`, and the peer types except the `io_list` itself. + +## Relationship to Peer Types + +`Door` is the only place where `PlainHTTPPeer` and `SSLHTTPPeer` are constructed. Once `create()` or `Detector::do_detect` calls `sp->run()`, `Door` has no further reference to the peer — the `io_list` holds the only tracked pointer. This clean ownership boundary means peer teardown is driven entirely by I/O completion and `io_list` shutdown, not by `Door`. \ No newline at end of file diff --git a/include/xrpl/server/detail/JSONRPCUtil.h.ai.json b/include/xrpl/server/detail/JSONRPCUtil.h.ai.json new file mode 100644 index 0000000000..514c7362c4 --- /dev/null +++ b/include/xrpl/server/detail/JSONRPCUtil.h.ai.json @@ -0,0 +1,25 @@ +{ + "args": [], + "classes": [], + "description": "Declares the HTTPReply function for sending HTTP replies with a status code, message, JSON output, and logging journal within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/JSONRPCUtil.h", + "functions": [ + { + "args": [ + "nStatus", + "strMsg", + "Output", + "j" + ], + "lineno": 8, + "name": "HTTPReply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/JSONRPCUtil.h.ai.md b/include/xrpl/server/detail/JSONRPCUtil.h.ai.md new file mode 100644 index 0000000000..e342b55ec1 --- /dev/null +++ b/include/xrpl/server/detail/JSONRPCUtil.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/server/detail/JSONRPCUtil.h` + +This header is the sole public interface for the XRPL server's JSON-RPC HTTP reply mechanism. It lives in the `server/detail` namespace, marking it as an internal implementation detail of the server subsystem — not intended for consumption outside of the RPC pipeline. + +## What It Declares + +The header declares a single function: + +```cpp +void HTTPReply(int nStatus, std::string const& strMsg, + Json::Output const&, beast::Journal j); +``` + +This function is responsible for serializing a complete HTTP/1.x response — status line, standard headers, and body — and streaming it to the caller-provided output sink. It is the only path through which JSON-RPC responses leave the rippled HTTP server layer. + +## The `Json::Output` Abstraction + +The third parameter, `Json::Output`, is a `std::function` alias defined in ``. Rather than writing to a socket or buffer directly, `HTTPReply` calls this callback incrementally for each chunk of response data. Callers construct an `Output` that targets whatever downstream sink they need — typically a `Session` write queue in `ServerHandler.cpp` via a `makeOutput(session)` factory. + +This design cleanly separates response formatting from transport. The function doesn't know or care whether it's writing to a TLS stream, a plain TCP connection, or an in-memory string for testing. It only knows how to compose a valid HTTP response. + +## Implementation Behavior + +The implementation in `src/libxrpl/server/JSONRPCUtil.cpp` handles two distinct cases: + +**Authentication challenge (401 with empty body):** When `nStatus == 401` and `strMsg` is empty, `HTTPReply` emits a full `WWW-Authenticate: Basic` challenge using HTTP/1.0 with a hardcoded HTML body. The source includes a prominent comment warning that the `Content-Length: 296` header is manually computed and must be updated if the body ever changes — a fragile pattern that was apparently never refactored. + +**All other responses:** A `switch` statement maps status codes 200, 202, 400, 401, 403, 404, 405, 429, 500, 501, and 503 to their canonical HTTP/1.1 status lines. The response always includes: +- An RFC 7231-compliant `Date:` header generated via `getHTTPHeaderTimestamp()` (platform-abstracted via `gmtime_r`/`gmtime_s`) +- `Connection: Keep-Alive` +- `Content-Length` computed from `strMsg.size() + 2` (accounting for the trailing `\r\n` appended after the body) +- `Content-Type: application/json; charset=UTF-8` +- A `Server:` header embedding the rippled system name and full version string from `BuildInfo` + +The `+2` in the `Content-Length` is subtle: `HTTPReply` unconditionally appends `"\r\n"` after the content body, so the declared length must account for those two bytes. Forgetting this would corrupt HTTP pipelining. + +## Usage in the RPC Pipeline + +`ServerHandler::onRequest()` and the internal `processRequest()` function in `ServerHandler.cpp` call `HTTPReply` at every decision point: protocol check failures (403), authorization failures (403), JSON parse errors (400), API version mismatches (400), resource throttling (503), method validation failures (400), and finally for the successful JSON response itself (200). In all cases the `Output` lambda is constructed from the active `Session` reference, funneling response data into the session's asynchronous write queue. + +The `beast::Journal j` parameter enables structured trace-level logging of every reply: `JLOG(j.trace()) << "HTTP Reply " << nStatus << " " << content;`. This ties every outbound response to the rippled logging infrastructure without coupling the utility to any specific application context. + +## Design Note + +The `switch` statement in the implementation has a `// NOLINTNEXTLINE(bugprone-switch-missing-default-case)` suppression, acknowledging that unrecognized status codes silently produce a response with no status line. This is intentional — callers are expected to pass only the enumerated codes — but it means a programming error would yield a malformed response rather than a compile-time or runtime error. \ No newline at end of file diff --git a/include/xrpl/server/detail/LowestLayer.h.ai.json b/include/xrpl/server/detail/LowestLayer.h.ai.json new file mode 100644 index 0000000000..a23846549c --- /dev/null +++ b/include/xrpl/server/detail/LowestLayer.h.ai.json @@ -0,0 +1,27 @@ +{ + "args": [ + { + "lineno": 13, + "name": "t" + } + ], + "classes": [], + "description": "Provides a utility function to obtain the lowest layer of a Boost.Beast stream, handling differences between Boost versions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/LowestLayer.h", + "functions": [ + { + "args": [ + "t" + ], + "lineno": 11, + "name": "get_lowest_layer" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/LowestLayer.h.ai.md b/include/xrpl/server/detail/LowestLayer.h.ai.md new file mode 100644 index 0000000000..2697741c3a --- /dev/null +++ b/include/xrpl/server/detail/LowestLayer.h.ai.md @@ -0,0 +1,40 @@ +# `include/xrpl/server/detail/LowestLayer.h` + +## Purpose and Context + +This 23-line header exists solely as a Boost version compatibility shim. The XRPL server's networking layer stacks Boost.Beast streams — a plain or TLS socket wrapped in an HTTP or WebSocket layer — and peer teardown logic regularly needs to reach the raw `tcp::socket` buried at the bottom of that stack. Beast provides a mechanism to do exactly that, but the API changed in a breaking way at Boost 1.70, requiring different code paths and different headers depending on which version is installed. + +Rather than scattering `#if BOOST_VERSION` guards throughout every call site, this file centralises the divergence into one wrapper function, giving the rest of the server a uniform, version-agnostic surface. + +## The API Divergence Being Bridged + +Prior to Boost 1.70, each layered stream type exposed the bottom of its stack via a member function: `t.lowest_layer()`. The calling code also needed to supply the template parameter explicitly. Starting in Boost 1.70, Beast introduced `boost::beast::get_lowest_layer(t)` as a free function with automatic type deduction, and moved the supporting traits from `` into the new `` header. The old member-function form was deprecated and eventually removed, so simply calling one or the other at compile time is not possible without a version check. + +## Design of `xrpl::get_lowest_layer` + +```cpp +template +decltype(auto) +get_lowest_layer(T& t) noexcept +{ +#if BOOST_VERSION >= 107000 + return boost::beast::get_lowest_layer(t); +#else + return t.lowest_layer(); +#endif +} +``` + +The function is a transparent forwarding wrapper. `decltype(auto)` is chosen deliberately over a plain `auto` return because the underlying calls return references to the lowest-layer object rather than copies — stripping the reference with `auto` would silently copy a socket, which would be both wrong and expensive. `noexcept` is correct for both branches: neither member access nor the Beast free function can throw. + +The preprocessor branch is the smallest possible divergence point: one `#include` and one expression differ between the two paths. Every other aspect — template parameter, function signature, `noexcept`, `decltype(auto)` — is shared. + +## Usage in the Server + +`BasePeer::close()` is the primary consumer. When a peer is asked to close, it calls `xrpl::get_lowest_layer(impl().ws_).socket().close(ec)` to peel through the WebSocket (and possibly TLS) layers and close the raw socket directly. The error code from `close()` is intentionally discarded — by the time `close()` is reached the goal is orderly resource release, and socket-level errors at that point are not actionable. + +`BaseWSPeer` uses the same call pattern from its failure path, again reaching the TCP socket to force a close when the WebSocket handshake or I/O has failed. Notably, `BaseHTTPPeer` calls `boost::beast::get_lowest_layer` directly rather than through this wrapper — that file predates or does not need the compatibility layer, or was updated independently when the minimum Boost version changed. + +## Architectural Note + +This file is a narrow, well-contained answer to a real problem: third-party library API churn. By isolating the version check here rather than in every peer class, any future change to the minimum supported Boost version — or a further API revision in Beast — requires a single-point update rather than a codebase-wide search-and-replace. It also keeps the peer implementations readable: `xrpl::get_lowest_layer(stream)` reads as intent, not as a version negotiation. \ No newline at end of file diff --git a/include/xrpl/server/detail/PlainHTTPPeer.h.ai.json b/include/xrpl/server/detail/PlainHTTPPeer.h.ai.json new file mode 100644 index 0000000000..539009a7eb --- /dev/null +++ b/include/xrpl/server/detail/PlainHTTPPeer.h.ai.json @@ -0,0 +1,71 @@ +{ + "args": [ + { + "lineno": 12, + "name": "Handler" + }, + { + "lineno": 28, + "name": "ConstBufferSequence" + } + ], + "classes": [ + { + "args": [ + "Port const& port", + "Handler& handler", + "boost::asio::io_context& ioc", + "beast::Journal journal", + "endpoint_type remote_address", + "ConstBufferSequence const& buffers", + "stream_type&& stream" + ], + "lineno": 13, + "name": "PlainHTTPPeer" + } + ], + "description": "Defines the PlainHTTPPeer class template for handling plain (non-SSL) HTTP peer connections in the XRPL server, including HTTP request handling, WebSocket upgrade, and connection management.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/PlainHTTPPeer.h", + "functions": [ + { + "args": [ + "Port const& port", + "Handler& handler", + "boost::asio::io_context& ioc", + "beast::Journal journal", + "endpoint_type remote_endpoint", + "ConstBufferSequence const& buffers", + "stream_type&& stream" + ], + "lineno": 29, + "name": "PlainHTTPPeer" + }, + { + "args": [], + "lineno": 38, + "name": "run" + }, + { + "args": [], + "lineno": 41, + "name": "websocketUpgrade" + }, + { + "args": [], + "lineno": 46, + "name": "do_request" + }, + { + "args": [], + "lineno": 49, + "name": "do_close" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/PlainHTTPPeer.h.ai.md b/include/xrpl/server/detail/PlainHTTPPeer.h.ai.md new file mode 100644 index 0000000000..0f5a6a61db --- /dev/null +++ b/include/xrpl/server/detail/PlainHTTPPeer.h.ai.md @@ -0,0 +1,61 @@ +# `PlainHTTPPeer.h` — Non-TLS HTTP Peer Connection + +`PlainHTTPPeer` is the concrete HTTP connection type for unencrypted (non-TLS) TCP clients. It sits at the leaf of a three-tier class hierarchy: the template base `BaseHTTPPeer` provides all async I/O machinery, `PlainHTTPPeer` supplies the plain-socket specialisation, and the parallel `SSLHTTPPeer` provides the TLS variant. Together they handle everything from connection acceptance through request dispatch, optional WebSocket upgrade, and graceful shutdown. + +## Inheritance and CRTP Design + +``` +BaseHTTPPeer> ← CRTP base (async loops, write queue, timers) + └── PlainHTTPPeer ← plain-TCP specialisation + └── enable_shared_from_this +``` + +`BaseHTTPPeer` uses the Curiously Recurring Template Pattern: it holds a reference to the concrete subtype via `impl()` and calls `impl().stream_` directly in methods like `boost::beast::get_lowest_layer(impl().stream_).close()` and in `do_read` / `on_write`. This avoids a virtual dispatch hot-path in the I/O loop while still allowing `do_request()` and `do_close()` to be pure-virtual overrides. `PlainHTTPPeer` is declared a `friend` of its own base so the base can access the private `stream_` member. + +## Stream Representation + +The class owns a `boost::beast::tcp_stream` (`stream_`), which wraps a raw TCP socket with Beast's built-in per-operation deadline support. A plain `boost::asio::ip::tcp::socket&` (`socket_`) is stored as a reference into `stream_.socket()`. This reference exists because some operations — socket options, half-close via `shutdown()` — require the raw socket interface that Beast's stream adaptor doesn't expose. The two members always refer to the same underlying file descriptor; `socket_` is never a separate object. + +## Constructor: TCP_NODELAY on Loopback + +```cpp +if (remote_endpoint.address().is_loopback()) + socket_.set_option(boost::asio::ip::tcp::no_delay{true}); +``` + +This is an intentional test-infrastructure optimisation. Nagle's algorithm buffers small writes in hope of coalescing them, which noticeably inflates round-trip latency in the `Env`-based integration test suite running on Linux. On real network paths the coalescing is usually desirable, so the option is only applied for loopback. The base class constructor also differentiates loopback clients with a shorter `timeoutSecondsLocal` (3 s vs. 30 s) for the same reason. + +## `run()`: Acceptance Gate + +`run()` is called by `Door` after a connection is accepted. It immediately calls `handler_.onAccept()` to give the application layer a chance to reject the connection (e.g., connection-limit enforcement). If the handler returns false, the peer schedules `do_close()` and exits. Because `onAccept` may itself close the socket, a second check on `socket_.is_open()` guards against double-close before posting `do_read`. + +All coroutine launches go through `util::spawn` with an explicit `strand_` to serialise I/O operations on a single logical thread of execution, even when the underlying `io_context` runs on a thread pool. + +## `do_request()`: Three-Way Dispatch + +After `BaseHTTPPeer::do_read` parses a complete HTTP message it calls the pure-virtual `do_request()`. The plain-HTTP implementation offers three dispatch paths: + +1. **Handler handoff** (`what.moved == true`): `onHandoff` takes ownership of the connection — used when upgrading to a peer overlay connection or similar out-of-band routing. The peer does nothing further. +2. **Immediate response** (`what.response != nullptr`): The handler synthesised a ready-made response (e.g., a redirect or 403). If `Connection: close` was requested, the receive side is half-closed before writing. The response is then queued via `BaseHTTPPeer::write`. +3. **Legacy `onRequest`**: For JSON-RPC and other application-layer handlers that pull the request from the `Session` interface and call `session.write()` themselves. + +The explicit half-close in paths 1 and 2 — `socket_.shutdown(shutdown_receive)` — signals to the remote that no further requests will be read. This is the correct TCP idiom for HTTP/1.1 `Connection: close` on a plain socket. The TLS counterpart (`SSLHTTPPeer::do_request`) omits this step because TLS shutdown requires an async bidirectional `close_notify` handshake handled separately in `do_close()`. + +## `do_close()`: Half-Close for Send + +```cpp +void PlainHTTPPeer::do_close() { + boost::system::error_code ec; + socket_.shutdown(socket_type::shutdown_send, ec); +} +``` + +For plain TCP, terminating the outbound direction with `shutdown_send` is sufficient. The remote will read EOF, complete any pending read, and then close its end. The TLS peer instead needs `stream_.async_shutdown()` to exchange `close_notify` alerts, which is why `do_close` is virtual. The error code from `shutdown` is intentionally discarded; by this point the peer's lifetime is already ending. + +## `websocketUpgrade()`: Protocol Handoff + +When `onHandoff` returns a WebSocket upgrade signal (detected from HTTP headers by the base layer), `websocketUpgrade()` is called. It constructs a `PlainWSPeer` via `ios().emplace<>()`, moving both the `stream_` and the already-parsed HTTP upgrade `message_` into the new peer. The move semantics are critical: after this call `stream_` is in a valid-but-empty state and the `PlainHTTPPeer` must not perform any further I/O. The returned `WSSession` shared pointer is registered with the `io_list` and takes over the connection's lifetime. + +## Relationship to `Door` and `SSLHTTPPeer` + +`Door` accepts raw TCP connections and uses `boost::beast::async_detect_ssl` to sniff the first bytes. If the connection looks like TLS it creates an `SSLHTTPPeer`; otherwise it creates a `PlainHTTPPeer`. Both types accept the same `ConstBufferSequence buffers` argument (the bytes already read during SSL detection) which `BaseHTTPPeer` replays into `read_buf_` so no data is lost. This sniffing-plus-prepend design allows a single port to serve both plain and TLS clients without the client needing to connect to different ports. \ No newline at end of file diff --git a/include/xrpl/server/detail/PlainWSPeer.h.ai.json b/include/xrpl/server/detail/PlainWSPeer.h.ai.json new file mode 100644 index 0000000000..e6ec10bd02 --- /dev/null +++ b/include/xrpl/server/detail/PlainWSPeer.h.ai.json @@ -0,0 +1,53 @@ +{ + "args": [ + { + "lineno": 8, + "name": "Handler" + }, + { + "lineno": 24, + "name": "Body" + }, + { + "lineno": 24, + "name": "Headers" + } + ], + "classes": [ + { + "args": [ + "Port const& port", + "Handler& handler", + "endpoint_type remote_address", + "boost::beast::http::request&& request", + "socket_type&& socket", + "beast::Journal journal" + ], + "lineno": 9, + "name": "PlainWSPeer" + } + ], + "description": "Defines the PlainWSPeer class template, which implements a plain (non-SSL) WebSocket peer for the XRPL server, inheriting from BaseWSPeer and managing WebSocket connections over TCP.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/PlainWSPeer.h", + "functions": [ + { + "args": [ + "Port const& port", + "Handler& handler", + "endpoint_type remote_address", + "boost::beast::http::request&& request", + "socket_type&& socket", + "beast::Journal journal" + ], + "lineno": 25, + "name": "PlainWSPeer" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/PlainWSPeer.h.ai.md b/include/xrpl/server/detail/PlainWSPeer.h.ai.md new file mode 100644 index 0000000000..75a2214d0f --- /dev/null +++ b/include/xrpl/server/detail/PlainWSPeer.h.ai.md @@ -0,0 +1,25 @@ +# `PlainWSPeer.h` — Plain (Non-TLS) WebSocket Peer + +`PlainWSPeer` is the concrete WebSocket peer implementation for unencrypted connections in the XRPL server. It sits at the leaf of a three-level inheritance hierarchy: `BasePeer → BaseWSPeer → PlainWSPeer`, and serves as the exact structural mirror of `SSLWSPeer` — the only difference being that it wraps a raw `boost::beast::tcp_stream` rather than an SSL-layered one. + +## Role in the Peer Hierarchy + +The server's peer design uses the Curiously Recurring Template Pattern (CRTP): `BaseWSPeer` and `BasePeer` both cast `this` down to `Impl*` whenever they need to touch the transport-specific `ws_` stream. This lets both base classes drive all async I/O — read loops, write queues, ping/pong timers, close sequences — without virtual dispatch, while remaining completely unaware of whether the underlying stream is plain TCP or TLS. + +`PlainWSPeer` satisfies that contract by holding: + +```cpp +boost::beast::websocket::stream ws_; +``` + +where `socket_type` is `boost::beast::tcp_stream`. The member is `private`, accessible only to the two base classes via explicit `friend` declarations. `BaseWSPeer` directly calls `impl().ws_.async_read(...)`, `impl().ws_.async_write_some(...)`, `impl().ws_.async_close(...)`, and so on — all resolved statically. + +## Constructor Design + +The constructor receives an already-HTTP-upgraded request, the raw TCP socket (by move), and the other standard peer parameters. Two ordering constraints matter here. First, `socket.get_executor()` must be extracted *before* the socket is moved, because the base class needs the executor to create its strand and timer — both of which are initialized in `BasePeer` and `BaseWSPeer` respectively. Second, `ws_` is initialized after the base, ensuring the base is fully constructed before the WebSocket stream exists. + +Compare this to `SSLWSPeer`, where the SSL stream is held in a `unique_ptr` and the WebSocket stream is `websocket::stream` — a reference wrapper. That extra indirection is needed because `ssl_stream` is not moveable after construction. The plain variant has no such constraint: `tcp_stream` is moveable, so `ws_` can own the socket directly without heap allocation. + +## Lifetime and Thread Safety + +`PlainWSPeer` inherits `std::enable_shared_from_this`, which is the mechanism by which every async callback captures a `shared_ptr` to itself. This prevents the peer from being destroyed while an operation is outstanding. The `BasePeer` constructor also holds a `boost::asio::executor_work_guard`, keeping the ASIO executor alive for the duration of the peer's existence. All public methods on `BaseWSPeer` (`send`, `close`, `complete`) guard against cross-thread access by re-posting to the strand when not already on it — `PlainWSPeer` inherits this safety without adding any locking of its own. \ No newline at end of file diff --git a/include/xrpl/server/detail/SSLHTTPPeer.h.ai.json b/include/xrpl/server/detail/SSLHTTPPeer.h.ai.json new file mode 100644 index 0000000000..9cdee6ead1 --- /dev/null +++ b/include/xrpl/server/detail/SSLHTTPPeer.h.ai.json @@ -0,0 +1,85 @@ +{ + "args": [ + { + "lineno": 13, + "name": "Handler" + }, + { + "lineno": 33, + "name": "ConstBufferSequence" + } + ], + "classes": [ + { + "args": [ + "port", + "handler", + "ioc", + "journal", + "remote_address", + "buffers", + "stream" + ], + "lineno": 15, + "name": "SSLHTTPPeer" + } + ], + "description": "Defines the SSLHTTPPeer class template, which handles HTTP connections over SSL/TLS for the XRPL server, including handshake, request processing, and WebSocket upgrade.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/SSLHTTPPeer.h", + "functions": [ + { + "args": [ + "port", + "handler", + "ioc", + "journal", + "remote_address", + "buffers", + "stream" + ], + "lineno": 34, + "name": "SSLHTTPPeer" + }, + { + "args": [], + "lineno": 49, + "name": "run" + }, + { + "args": [], + "lineno": 56, + "name": "websocketUpgrade" + }, + { + "args": [ + "do_yield" + ], + "lineno": 63, + "name": "do_handshake" + }, + { + "args": [], + "lineno": 82, + "name": "do_request" + }, + { + "args": [], + "lineno": 92, + "name": "do_close" + }, + { + "args": [ + "ec" + ], + "lineno": 98, + "name": "on_shutdown" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/SSLHTTPPeer.h.ai.md b/include/xrpl/server/detail/SSLHTTPPeer.h.ai.md new file mode 100644 index 0000000000..5c08635ddd --- /dev/null +++ b/include/xrpl/server/detail/SSLHTTPPeer.h.ai.md @@ -0,0 +1,49 @@ +# `SSLHTTPPeer.h` — TLS-Wrapped HTTP Connection Peer + +## Role in the System + +`SSLHTTPPeer` is one of the four concrete connection types in the XRPL server's peer hierarchy, sitting alongside `PlainHTTPPeer`, `SSLWSPeer`, and `PlainWSPeer`. It handles the full lifecycle of an inbound TLS-encrypted HTTP connection: SSL handshake negotiation, HTTP request reading, handler dispatch, optional in-place upgrade to a WebSocket session, and graceful TLS shutdown. Every `https`, `wss`, `wss2`, and `peer` protocol connection in the rippled server goes through this class. + +`Door`, the listening socket abstraction in the same `detail/` directory, creates instances of this class. When `Door`'s internal `Detector` detects SSL bytes at the front of the stream (using `boost::beast::async_detect_ssl`), it instantiates an `SSLHTTPPeer` and calls `run()` to kick off the connection state machine. + +## Class Structure and Ownership + +```cpp +template +class SSLHTTPPeer : public BaseHTTPPeer>, + public std::enable_shared_from_this> +``` + +The class uses the **Curiously Recurring Template Pattern (CRTP)** through `BaseHTTPPeer>`. `BaseHTTPPeer` is a template that accepts the concrete implementation type as its second parameter, which lets the base class call derived-class methods like `stream_` and `shared_from_this()` without virtual dispatch overhead. The `friend class BaseHTTPPeer` declaration is necessary for this access. + +The stream type hierarchy is three layers deep: + +- `socket_type` → raw `boost::asio::ip::tcp::socket` +- `middle_type` → `boost::beast::tcp_stream` (adds Beast's timeout support around the socket) +- `stream_type` → `boost::beast::ssl_stream` (wraps with TLS via Asio's SSL layer) + +The `stream_ptr_` is a `std::unique_ptr`, while `stream_` is a direct reference into it and `socket_` is a reference into `stream_.next_layer().socket()`. This split ownership-vs-reference pattern is intentional: `stream_ptr_` can be **moved out** of `SSLHTTPPeer` during a WebSocket upgrade (transferring TLS stream ownership to `SSLWSPeer`), while local operations throughout the peer's lifetime use the cheaper references. After the move, `stream_` and `socket_` would dangle, but that only happens at the point the peer is destroyed anyway. + +## Connection Lifecycle + +**Construction** receives a `middle_type&&` (an already-accepted `tcp_stream`) and a `Port` reference. The `Port::context` member supplies the `boost::asio::ssl::context` used to construct the `ssl_stream`. Any data bytes the `Detector` already consumed from the socket (its pre-read buffer) are passed as `buffers` and committed into `BaseHTTPPeer`'s `read_buf_` for replay during the handshake. + +**`run()`** is the entry point after acceptance. It first calls `handler_.onAccept()` to allow the application to reject the connection by IP address or other policy before any TLS is negotiated — an important early-exit gate. If the handler accepts, it spawns a coroutine to execute `do_handshake()`. + +**`do_handshake()`** performs the TLS server handshake asynchronously using `stream_.async_handshake()`, passing `read_buf_.data()` so the SSL library can consume those pre-peeked bytes. `verify_mode` is set to `ssl::verify_none` because XRPL clients are not expected to present certificates — mutual TLS is not used here. After the handshake, the method checks `port().protocol` to determine whether the connection should continue reading HTTP (`https`, `wss`, `wss2`, `peer`). If none of these protocols are configured, it simply returns and lets `this` be destroyed — a purposely silent drop used for protocol-sniffing scenarios where the connection is handled elsewhere. + +**`do_request()`** is called by `BaseHTTPPeer::do_read()` after a full HTTP message has been parsed. It increments the request counter and calls `handler_.onHandoff()`, passing the fully-negotiated `stream_ptr_` by move. If the handler signals `what.moved`, ownership has been transferred and this peer returns immediately. If the handler provides a `what.response`, it is written back. Otherwise, the legacy `onRequest()` path is invoked. Notably, SSL connections do **not** perform the half-close `socket_.shutdown(shutdown_receive)` that `PlainHTTPPeer` does for non-keep-alive connections — TLS shutdown is bidirectional and handled separately. + +**`websocketUpgrade()`** transitions an HTTP connection to a WebSocket session by constructing an `SSLWSPeer` via `ios().emplace<>()`, moving the HTTP message and `stream_ptr_` into it. This is a zero-copy handoff of the TLS stream; the new `SSLWSPeer` wraps it in a `boost::beast::websocket::stream` for the WebSocket framing layer. The contrasting plain variant, `PlainHTTPPeer::websocketUpgrade()`, moves just a `tcp_stream` value (no pointer indirection), because it does not need the pointer trick for detachability. + +## Shutdown and Error Handling + +**`do_close()`** initiates a TLS `async_shutdown` with the timer running, then dispatches to `on_shutdown()` on the strand. TLS shutdown is an active protocol exchange — both sides must send `close_notify` alerts — so unlike a plain TCP close it cannot be done synchronously. The timer guards against a peer that never sends `close_notify`. + +**`on_shutdown()`** cancels the timer and explicitly calls `stream_.next_layer().close()` on the underlying `tcp_stream`. The comment "in case this->destructor is delayed" reflects that `SSLHTTPPeer` is ref-counted: there may be outstanding callbacks holding `shared_ptr` copies that extend the object's lifetime past shutdown. Closing the socket at `on_shutdown()` time ensures the OS-level resource is released immediately rather than waiting for the last reference to drop. + +Error handling throughout follows the same pattern as `BaseHTTPPeer`: errors are stored in `ec_` and the lowest-layer stream is closed immediately, allowing all pending async operations to drain to their completion handlers with `operation_aborted`. + +## Concurrency + +All async operations are dispatched through `strand_` (inherited from `BaseHTTPPeer`), serializing all state mutations without explicit locking. The mutex in `BaseHTTPPeer` is limited to the write queue (`wq_`/`wq2_`), which may be accessed from non-strand threads via the `write(void*, size_t)` path when the application layer pushes data from an arbitrary thread. `SSLHTTPPeer` itself adds no new shared mutable state beyond `stream_ptr_`, which is only ever moved once, under strand protection, during `do_request()` or `websocketUpgrade()`. \ No newline at end of file diff --git a/include/xrpl/server/detail/SSLWSPeer.h.ai.json b/include/xrpl/server/detail/SSLWSPeer.h.ai.json new file mode 100644 index 0000000000..103c47b930 --- /dev/null +++ b/include/xrpl/server/detail/SSLWSPeer.h.ai.json @@ -0,0 +1,53 @@ +{ + "args": [ + { + "lineno": 12, + "name": "Handler" + }, + { + "lineno": 33, + "name": "Body" + }, + { + "lineno": 33, + "name": "Headers" + } + ], + "classes": [ + { + "args": [ + "port", + "handler", + "remote_endpoint", + "request", + "stream_ptr", + "journal" + ], + "lineno": 13, + "name": "SSLWSPeer" + } + ], + "description": "Defines the SSLWSPeer class template for handling SSL WebSocket peers in the xrpl server, including its constructor and relevant type aliases.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/SSLWSPeer.h", + "functions": [ + { + "args": [ + "port", + "handler", + "remote_endpoint", + "request", + "stream_ptr", + "journal" + ], + "lineno": 34, + "name": "SSLWSPeer" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/SSLWSPeer.h.ai.md b/include/xrpl/server/detail/SSLWSPeer.h.ai.md new file mode 100644 index 0000000000..ef0395528d --- /dev/null +++ b/include/xrpl/server/detail/SSLWSPeer.h.ai.md @@ -0,0 +1,59 @@ +# `SSLWSPeer.h` — TLS WebSocket Connection Peer + +## Role in the System + +`SSLWSPeer` is the concrete leaf class for TLS-encrypted WebSocket connections in the XRPL server. It sits at the bottom of a CRTP inheritance stack that separates transport-agnostic WebSocket logic from transport-specific wiring. Its sole structural responsibility is to own and expose the correctly layered TLS stream so that the base classes can drive all I/O through it without needing to know whether the underlying transport is encrypted or not. + +## Inheritance and CRTP Design + +The class inherits from `BaseWSPeer>`, which in turn inherits from `BasePeer>`. Both base classes are parameterized with the concrete derived type and use `static_cast(this)` (via the protected `impl()` helper) to access members of the derived class — the classic Curiously Recurring Template Pattern. + +This design avoids virtual dispatch for every I/O operation while still sharing the full lifecycle, write queue, ping/pong heartbeat, and read loop logic in `BaseWSPeer`. The tradeoff is that `BasePeer` and `BaseWSPeer` are both declared as `friend` classes of `SSLWSPeer`, since they reach directly into its private `ws_` and `stream_ptr_` members. + +`std::enable_shared_from_this>` is mixed in directly on the concrete class rather than on a base, which is intentional: `shared_from_this()` must return a pointer to the most-derived type so that the shared ownership count is correctly tied to the object's actual lifetime and so that `impl().shared_from_this()` in the base classes resolves to the right type. + +## Stream Layer Architecture + +The TLS WebSocket stack has three layers, all held within `SSLWSPeer`: + +``` +boost::beast::websocket::stream ws_ (WebSocket framing) +boost::beast::ssl_stream *stream_ptr_ (TLS encryption) +boost::beast::tcp_stream (underlying TCP) +``` + +`stream_ptr_` is a `std::unique_ptr`, and `ws_` is declared as `boost::beast::websocket::stream` — a websocket stream layered over a *reference* to the SSL stream, not over an owned value. This is the key structural difference from `PlainWSPeer`, where `ws_` owns its socket directly via `boost::beast::websocket::stream`. The reference-wrapper approach is necessary because the SSL stream is heap-allocated and its address must remain stable; embedding it by value inside the websocket stream would make move semantics unsafe. + +## Constructor and Initialization Order + +The constructor receives a `std::unique_ptr&&` — an already-constructed and TLS-handshaked stream transferred in from `SSLHTTPPeer::websocketUpgrade()`. The initialization order is load-bearing: + +```cpp +: BaseWSPeer( + port, + handler, + stream_ptr->get_executor(), // extract executor BEFORE move + waitable_timer{stream_ptr->get_executor()}, // extract executor BEFORE move + remote_endpoint, + std::move(request), + journal) +, stream_ptr_(std::move(stream_ptr)) // take ownership +, ws_(*stream_ptr_) // reference the now-owned object +``` + +The executor is extracted from `stream_ptr` (the parameter, still valid) before ownership is transferred. C++ guarantees that `BaseWSPeer` is initialized before `stream_ptr_`, which is initialized before `ws_`. So by the time `ws_(*stream_ptr_)` runs, `stream_ptr_` already holds the heap-allocated SSL stream. If the order were reversed — `ws_` initialized before `stream_ptr_` — `ws_` would hold a dangling reference. + +## Lifecycle and Upgrade Path + +`SSLWSPeer` instances are never constructed directly by application code. The creation path is: + +1. `Door` accepts an incoming TCP connection and detects TLS via Beast's `async_detect_ssl`. +2. An `SSLHTTPPeer` is created to perform the TLS handshake and read the HTTP Upgrade request. +3. When `SSLHTTPPeer::do_request()` determines the request is a WebSocket upgrade, it calls `websocketUpgrade()`, which constructs an `SSLWSPeer` by moving the already-authenticated TLS stream and the HTTP request into it. +4. `BaseWSPeer::run()` is then called to initiate the WebSocket handshake over the existing TLS connection. + +This upgrade path means `SSLWSPeer` never needs to manage TLS negotiation itself — it receives a fully-established TLS session and lifts it into WebSocket framing. + +## Relationship to Sibling Classes + +The four concrete peer types — `PlainHTTPPeer`, `SSLHTTPPeer`, `PlainWSPeer`, and `SSLWSPeer` — form a 2×2 matrix of transport (plain/TLS) by protocol (HTTP/WebSocket). `SSLWSPeer` is structurally symmetric to `PlainWSPeer` except for the extra indirection through `stream_ptr_` and the reference-based `ws_` type. The entire behavioral surface — reading, writing, heartbeating, queue management, and connection teardown — lives in `BaseWSPeer` and is shared between both WebSocket variants. \ No newline at end of file diff --git a/include/xrpl/server/detail/ServerImpl.h.ai.json b/include/xrpl/server/detail/ServerImpl.h.ai.json new file mode 100644 index 0000000000..6443e4a3ea --- /dev/null +++ b/include/xrpl/server/detail/ServerImpl.h.ai.json @@ -0,0 +1,159 @@ +{ + "args": [ + { + "lineno": 56, + "name": "handler" + }, + { + "lineno": 56, + "name": "io_context" + }, + { + "lineno": 56, + "name": "journal" + }, + { + "lineno": 31, + "name": "v" + }, + { + "lineno": 65, + "name": "ports" + }, + { + "lineno": 83, + "name": "x" + } + ], + "classes": [ + { + "args": [], + "lineno": 15, + "name": "Server" + }, + { + "args": [ + "Handler" + ], + "lineno": 44, + "name": "ServerImpl" + } + ], + "description": "Defines a multi-protocol server for XRPL that manages multiple listening ports and protocols (HTTP, HTTPS, WebSocket, Secure WebSocket, Peer protocol), including an abstract Server interface and a ServerImpl template implementation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/ServerImpl.h", + "functions": [ + { + "args": [], + "lineno": 23, + "name": "~Server" + }, + { + "args": [], + "lineno": 27, + "name": "journal" + }, + { + "args": [ + "v" + ], + "lineno": 31, + "name": "ports" + }, + { + "args": [], + "lineno": 38, + "name": "close" + }, + { + "args": [ + "handler", + "io_context", + "journal" + ], + "lineno": 56, + "name": "ServerImpl" + }, + { + "args": [], + "lineno": 58, + "name": "~ServerImpl" + }, + { + "args": [], + "lineno": 61, + "name": "journal" + }, + { + "args": [ + "ports" + ], + "lineno": 65, + "name": "ports" + }, + { + "args": [], + "lineno": 68, + "name": "close" + }, + { + "args": [], + "lineno": 71, + "name": "ios" + }, + { + "args": [], + "lineno": 75, + "name": "get_io_context" + }, + { + "args": [], + "lineno": 79, + "name": "closed" + }, + { + "args": [ + "x" + ], + "lineno": 83, + "name": "ceil_log2" + }, + { + "args": [ + "handler", + "io_context", + "journal" + ], + "lineno": 87, + "name": "ServerImpl" + }, + { + "args": [], + "lineno": 94, + "name": "~ServerImpl" + }, + { + "args": [ + "ports" + ], + "lineno": 101, + "name": "ports" + }, + { + "args": [], + "lineno": 124, + "name": "close" + }, + { + "args": [], + "lineno": 131, + "name": "closed" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/ServerImpl.h.ai.md b/include/xrpl/server/detail/ServerImpl.h.ai.md new file mode 100644 index 0000000000..4bb0f7ea01 --- /dev/null +++ b/include/xrpl/server/detail/ServerImpl.h.ai.md @@ -0,0 +1,66 @@ +# `include/xrpl/server/detail/ServerImpl.h` + +## Role in the System + +This file defines the multi-protocol network server at the heart of XRPL's node-facing communication layer. It provides two things: the `Server` abstract base class, which is the stable public interface callers hold onto, and the `ServerImpl` template, which is the concrete implementation. Together they govern how the rippled process binds to TCP ports, accepts inbound connections, and dispatches them across HTTP, HTTPS, WebSocket, Secure WebSocket, and the peer-to-peer gossip protocol. + +The public entry point is in the sibling `Server.h`, which exposes a single factory function: + +```cpp +template +std::unique_ptr make_Server(Handler& handler, boost::asio::io_context& io_context, beast::Journal journal); +``` + +This factory wraps `ServerImpl` behind the `Server` interface, giving callers a type-erased handle that survives across compilation units without exposing the Handler template. + +## The `Server` Abstract Interface + +`Server` is intentionally minimal — three pure virtual methods (`journal()`, `ports()`, `close()`) and a blocking virtual destructor. The sparse surface area is deliberate: callers configure ports once, then either wait for a graceful close or destroy the object. The interface hides all template machinery so that code outside the server subsystem never needs to know the `Handler` type. + +## `ServerImpl`: Ownership and Lifecycle + +`ServerImpl` holds three key resources by reference, not by value: + +- `handler_` — the application-layer callback object. The server sends events to it but does not own or manage its lifetime. +- `io_context_` — the Asio event loop driving all I/O. The server's existence must be a strict sub-interval of the `io_context`'s lifetime. +- `work_` — an `executor_work_guard` that prevents the `io_context` from returning from `run()` while the server is active. This is `std::optional` specifically so it can be released without destroying the object. + +The strand `strand_` is constructed but not heavily used in this file; it lives here to be passed to components that need a serialized executor context. + +## `ports()`: One-Shot Port Setup + +`ports()` is a one-shot call that configures all listening endpoints. For each `Port` configuration entry it calls `ios_.emplace>(...)`, which atomically creates the listener and registers it with the lifecycle tracker. If the port was configured with `port = 0` (OS-assigned), the actual bound port is back-filled into the stored `Port` struct after `get_endpoint()` resolves it. The method returns an `Endpoints` map (port name → `tcp::endpoint`) so the caller can learn the actual addresses — critical when ephemeral ports are used in tests or when binding to port 0. + +Calling `ports()` on a closed server throws `std::logic_error` rather than silently doing nothing. This enforces the single-setup contract. + +## `close()`: Asynchronous Graceful Shutdown + +`close()` initiates an asynchronous teardown by calling `ios_.close()` with a finisher lambda. The `io_list` machinery fans out `close()` calls to every registered `Door` (and transitively, every live connection), then invokes the finisher only after all work objects have been destroyed. The finisher lambda does two things in order: + +1. Sets `work_ = std::nullopt` — releases the `executor_work_guard`, allowing the `io_context` to drain. +2. Calls `handler_.onStopped(*this)` — notifies the application layer that the server is fully stopped. + +This sequencing is the non-obvious design choice: `onStopped` fires *after* all connection teardown, guaranteeing the handler sees a quiescent state when it receives the callback. + +## Destructor: Silent Teardown + +The destructor takes a different path than `close()`. The comment `// Handler::onStopped will not be called` is load-bearing documentation. It drops `work_`, calls `ios_.close()` (with a no-op finisher), and then synchronously blocks on `ios_.join()`. This is safe because the destructor must not return until all async operations that hold pointers into the server have completed, but it deliberately skips the application callback to prevent double-notification if `close()` was already called. + +## `io_list` and the Lifetime Graph + +`io_list` is the cohesion point for async lifetime management. Every object that performs I/O — `Door`, `Detector`, `PlainHTTPPeer`, `SSLHTTPPeer` — derives from `io_list::work` and is created through `ios_.emplace<>()`. The `io_list` tracks all live work objects in a flat map of `work* → weak_ptr`. When `close()` fires, it takes a snapshot of the map, releases the lock, and calls `close()` on every object via its `weak_ptr`. The reference count system ensures that if a connection completes naturally before the shutdown signal arrives, its destructor removes it from the count, and the finisher fires when `n_` reaches zero — whether that's before or after the close call. + +The `io_list ios_` is exposed via the public `ios()` accessor so that `Door` and its inner `Detector` class can register new peer objects directly into the same lifetime graph, not into a separate one. + +## `Door`: Per-Port Accept Loop + +Each `Door` wraps a single `tcp::acceptor` and runs a coroutine (`do_accept`) that loops calling `async_accept`. Two layers of backpressure are implemented: + +- **Proactive**: Before each accept, `should_throttle_for_fds()` queries `/proc/self/fd` (Linux) or `/dev/fd` (BSD/macOS) and compares used file descriptors against the process `RLIMIT_NOFILE`. If more than 30% of the FD budget is consumed (i.e., free ratio drops below `FREE_FD_THRESHOLD = 0.70`), the accept loop backs off before even attempting an accept. +- **Reactive**: On `EMFILE` or `ENOBUFS` errors from the OS, a backoff timer fires with exponential growth from 50 ms to a ceiling of 2000 ms, resetting to the initial value on any successful accept. + +Protocol dispatch happens at the `Door` level. If a port has both SSL and plain protocols enabled (`ssl_ && plain_`), a `Detector` object is spawned. `Detector` peeks at the first 16 bytes of the stream with a 15-second timeout using `boost::beast::async_detect_ssl`, then routes to `SSLHTTPPeer` or `PlainHTTPPeer` accordingly. If only one protocol family is configured, `create()` skips detection and dispatches directly. + +## Residual Members + +The `hist_` array of 64 `size_t` values, the `high_` counter, and the private `ceil_log2()` static method are declared but never referenced in active code. They appear to be scaffolding left from an earlier connection-count histogram feature that was removed or never completed. They add no behavior and impose negligible overhead. \ No newline at end of file diff --git a/include/xrpl/server/detail/Spawn.h.ai.json b/include/xrpl/server/detail/Spawn.h.ai.json new file mode 100644 index 0000000000..3b122e3ba5 --- /dev/null +++ b/include/xrpl/server/detail/Spawn.h.ai.json @@ -0,0 +1,40 @@ +{ + "args": [ + { + "lineno": 25, + "name": "ePtr" + }, + { + "lineno": 46, + "name": "ctx" + }, + { + "lineno": 46, + "name": "func" + } + ], + "classes": [], + "description": "Provides a utility function to spawn Boost.Asio coroutines with exception propagation and strand support, restoring behavior from Boost 1.83.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/Spawn.h", + "functions": [ + { + "args": [ + "ctx", + "func" + ], + "lineno": 44, + "name": "spawn" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl::util::impl" + }, + { + "lineno": 7, + "name": "xrpl::util" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/Spawn.h.ai.md b/include/xrpl/server/detail/Spawn.h.ai.md new file mode 100644 index 0000000000..0adf5a7e68 --- /dev/null +++ b/include/xrpl/server/detail/Spawn.h.ai.md @@ -0,0 +1,36 @@ +# `include/xrpl/server/detail/Spawn.h` + +This header exists to paper over a breaking behavioral change introduced in Boost.Asio 1.84, where `boost::asio::spawn` stopped propagating unhandled exceptions from coroutine bodies to the `io_context::run()` call site. The file lives in the `xrpl::util` namespace and is used throughout the server detail layer — `Door.h`, `BaseHTTPPeer.h`, `PlainHTTPPeer.h`, and `SSLHTTPPeer.h` — wherever a coroutine is launched. + +## The Problem Being Solved + +Before Boost 1.84, an unhandled exception inside a stackful coroutine would unwind through `io_context::run()`, giving callers a clear failure signal. Starting with 1.84, `boost::asio::spawn` accepts an optional completion token (the third argument); when omitted, exceptions are silently swallowed. The XRPL server has no per-connection exception handler, so silent swallowing would leave connections in broken states with no diagnostic trace. The fix is to always supply a completion handler that logs and re-throws. + +## `kPROPAGATE_EXCEPTIONS` + +The `impl::kPROPAGATE_EXCEPTIONS` inline constexpr lambda is that completion handler. It accepts the `std::exception_ptr` that Asio provides after the coroutine exits. If the pointer is non-null the exception is re-thrown inside a `try/catch` pair: `std::exception` derivatives are logged with `JLOG` before re-throwing; unknown types get an "Unknown" warning. In both paths the exception propagates back to `io_context::run()`, preserving the pre-1.84 contract. The `kPROPAGATE_EXCEPTIONS` object lives in `namespace impl` to signal it is an implementation detail, not a public interface. + +## `IsStrand` Concept + +The C++20 concept `impl::IsStrand` checks whether a decayed type is exactly `boost::asio::strand`. This single-check concept enables compile-time dispatch inside `spawn()` without a runtime branch. Wrapping an already-stranded executor in a second strand is harmless but wasteful and can produce subtle ordering issues if code elsewhere tests `strand_.running_in_this_thread()` — a check that only the outer strand would pass. The concept guards against that. + +## `xrpl::util::spawn()` + +The public `spawn()` function is a thin template: + +```cpp +template + requires std::is_invocable_r_v +void spawn(Ctx&& ctx, F&& func); +``` + +The `requires` clause enforces that the callable returns `void` when given a `yield_context`, catching mismatches at the earliest possible point. Inside the body, `if constexpr (impl::IsStrand)` branches: + +- **Strand path**: forwards `ctx` directly to `boost::asio::spawn`. The executor is already serialized; no additional wrapping needed. +- **Non-strand path**: calls `boost::asio::make_strand(boost::asio::get_associated_executor(ctx))` to create a fresh strand from whatever executor is associated with the context, then passes that strand to `spawn`. This restores the implicit-strand guarantee that older Boost versions provided by default. + +In both paths `impl::kPROPAGATE_EXCEPTIONS` is the third argument, ensuring exceptions are never silently dropped. + +## Usage Pattern + +Every coroutine entry point in the server detail layer — accepting connections in `Door`, reading HTTP requests in `BaseHTTPPeer::do_read`, writing streaming responses via `do_writer`, closing TLS/plain streams in `SSLHTTPPeer` and `PlainHTTPPeer` — is launched through `util::spawn(strand_, ...)`. The callers always already hold an explicit `boost::asio::strand` member, so they hit the fast strand path. The non-strand path exists for future callers that may only have an `io_context` or generic executor at hand. \ No newline at end of file diff --git a/include/xrpl/server/detail/io_list.h.ai.json b/include/xrpl/server/detail/io_list.h.ai.json new file mode 100644 index 0000000000..eb03ebb69b --- /dev/null +++ b/include/xrpl/server/detail/io_list.h.ai.json @@ -0,0 +1,111 @@ +{ + "args": [ + { + "lineno": 0, + "name": "nStatus" + }, + { + "lineno": 0, + "name": "strMsg" + }, + { + "lineno": 0, + "name": "Output" + }, + { + "lineno": 0, + "name": "j" + } + ], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "io_list" + }, + { + "args": [], + "lineno": 13, + "name": "io_list::work" + } + ], + "description": "Manages a set of objects performing asynchronous I/O, allowing for safe creation, destruction, and closure of work objects in a thread-safe manner.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/server/detail/io_list.h", + "functions": [ + { + "args": [], + "lineno": 19, + "name": "io_list::work::destroy" + }, + { + "args": [], + "lineno": 61, + "name": "io_list::destroy" + }, + { + "args": [], + "lineno": 77, + "name": "io_list::closed" + }, + { + "args": [ + "Args&&... args" + ], + "lineno": 97, + "name": "io_list::emplace" + }, + { + "args": [ + "Finisher&& f" + ], + "lineno": 117, + "name": "io_list::close" + }, + { + "args": [], + "lineno": 126, + "name": "io_list::close" + }, + { + "args": [], + "lineno": 139, + "name": "io_list::join" + }, + { + "args": [], + "lineno": 151, + "name": "io_list::work::destroy" + }, + { + "args": [], + "lineno": 167, + "name": "io_list::destroy" + }, + { + "args": [ + "Args&&... args" + ], + "lineno": 172, + "name": "io_list::emplace" + }, + { + "args": [ + "Finisher&& f" + ], + "lineno": 191, + "name": "io_list::close" + }, + { + "args": [], + "lineno": 210, + "name": "io_list::join" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/server/detail/io_list.h.ai.md b/include/xrpl/server/detail/io_list.h.ai.md new file mode 100644 index 0000000000..a72f1012db --- /dev/null +++ b/include/xrpl/server/detail/io_list.h.ai.md @@ -0,0 +1,37 @@ +# `io_list.h` — Lifecycle Manager for Asynchronous I/O Objects + +## Role in the System + +`io_list` is a thread-safe registry that tracks and terminates a set of asynchronous I/O objects. It lives in `include/xrpl/server/detail/` and is the backbone of the XRPL server's graceful-shutdown mechanism. Without it, there would be no safe way to signal every live connection and listener to stop, wait for their I/O to drain, and then resume teardown — all without data races or dangling pointers. + +`ServerImpl` (in `ServerImpl.h`) owns one `io_list` instance (`ios_`) and uses it to track every `Door` (the TCP acceptor) and every peer connection that the server spawns. `Door` itself is an `io_list::work`, and when it accepts a connection it creates child peers — also `io_list::work` objects — by calling `ios().emplace(...)`. This single registry therefore spans the entire live connection tree for a listening port. + +## The Two-Class Design + +The file defines exactly two classes: `io_list` and its inner base `io_list::work`. + +**`io_list::work`** is the required base class for anything the list tracks. It carries a single pointer back to its owning `io_list` (`ios_`), set by `emplace()` at registration time. Its destructor calls `destroy()`, which erases the work object from the parent map, decrements the outstanding-work counter `n_`, and — if this was the last item and the list is already closed — swaps out and fires the finisher callback, then wakes any threads blocked in `join()`. The finisher is invoked *outside* the lock (the swap happens inside, the call happens after) to avoid re-entrant locking. + +`work` requires every subclass to implement `close()`. This is the signal for a work object to begin cancelling its pending I/O (e.g., closing a socket). In `BasePeer`, `close()` posts to the strand to call the socket's `close()`. In `Door`, it cancels the backoff timer and closes the acceptor. Neither of these calls is blocking — they initiate cancellation and return. + +**`io_list`** maintains the registry as a `boost::container::flat_map>`. The raw pointer is the key for O(log n) erasure in `work::destroy()`, and the `weak_ptr` value lets `close()` attempt to extend the object's lifetime for long enough to call `close()` on it without assuming the object is still alive. Using `flat_map` (a sorted vector internally) is a cache-friendly choice for what is expected to be a relatively small collection that is iterated more than mutated during normal operation. + +## Atomicity of Registration + +`emplace(args...)` has a carefully constructed double-check pattern. It checks `closed_` once before acquiring the mutex as a fast-path bail-out, then checks again while holding the lock. If the list became closed between those two checks, the newly constructed object is swapped into a local `dead` variable and destroyed outside the lock. This ensures two invariants hold simultaneously: if `emplace` returns a non-null pointer, the object is guaranteed to be in the registry before the lock releases, so any subsequent `close()` call *will* reach it; and if `emplace` returns `nullptr`, the caller knows not to `run()` the object. The constructor is intentionally called before acquiring the lock, so slow or throwing constructors don't hold up other threads registering or closing work. + +## The Close/Join Protocol + +`close(Finisher&&)` is idempotent after the first call. It acquires the mutex, sets `closed_ = true`, and moves the entire map out before releasing the lock. This is key: it avoids holding the lock while iterating and calling `close()` on each work item (which may post to an executor). If the map is empty at close time, the finisher is called immediately and synchronously. Otherwise it is stored in `f_` and called by whichever `work::destroy()` decrements `n_` to zero. + +`join()` simply blocks on the condition variable until both `closed_` is true and `n_` is zero. The destructor calls `close()` then `join()` in sequence, ensuring that destroying an `io_list` always waits for all tracked work to finish — a critical property for shutdown safety. + +## Concurrency Contract + +The comments document the constraints precisely. `emplace()` is safe to call concurrently. `close()` must not be called concurrently (there is no internal guard against a double concurrent close; the idempotency check is not atomic). `join()` is safe to call concurrently, but callers must not be running an `io_context` that the work objects dispatch onto, or deadlock results — `join()` waits for work to be destroyed, but the work's async completions need `io_context::run()` to proceed. + +The `closed()` accessor deliberately has no mutex guard, with the comment that it has undefined behavior if called concurrently with `close()`. This is a performance compromise: reading a single `bool` that is only ever set once (from false to true) is safe in practice on all mainstream architectures, but the official contract is conservative. + +## Why This Pattern + +An alternative to `io_list` would be reference counting alone: let `shared_ptr` destruction trigger cleanup. But that gives no way to *initiate* cancellation — objects would only be destroyed once all external shared_ptr holders released them, which doesn't happen until I/O completes, which doesn't happen until the socket is closed, creating a chicken-and-egg problem. `io_list` breaks this by providing an explicit `close()` signal that propagates to all live work items, after which their natural lifetime (governed by `shared_ptr`) determines when the finisher fires. \ No newline at end of file diff --git a/include/xrpl/shamap/Family.h.ai.json b/include/xrpl/shamap/Family.h.ai.json new file mode 100644 index 0000000000..af0299361a --- /dev/null +++ b/include/xrpl/shamap/Family.h.ai.json @@ -0,0 +1,99 @@ +{ + "args": [ + { + "lineno": 40, + "name": "refNum" + }, + { + "lineno": 40, + "name": "nodeHash" + }, + { + "lineno": 48, + "name": "refHash" + }, + { + "lineno": 48, + "name": "refNum" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "Family" + } + ], + "description": "Defines the abstract base class 'Family' in the xrpl namespace, which provides an interface for managing caches, databases, and missing node acquisition in the context of the XRP Ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/Family.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "Family" + }, + { + "args": [], + "lineno": 14, + "name": "~Family" + }, + { + "args": [], + "lineno": 16, + "name": "db" + }, + { + "args": [], + "lineno": 19, + "name": "db" + }, + { + "args": [], + "lineno": 22, + "name": "journal" + }, + { + "args": [], + "lineno": 26, + "name": "getFullBelowCache" + }, + { + "args": [], + "lineno": 30, + "name": "getTreeNodeCache" + }, + { + "args": [], + "lineno": 33, + "name": "sweep" + }, + { + "args": [ + "refNum", + "nodeHash" + ], + "lineno": 39, + "name": "missingNodeAcquireBySeq" + }, + { + "args": [ + "refHash", + "refNum" + ], + "lineno": 47, + "name": "missingNodeAcquireByHash" + }, + { + "args": [], + "lineno": 53, + "name": "reset" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/Family.h.ai.md b/include/xrpl/shamap/Family.h.ai.md new file mode 100644 index 0000000000..affdc38278 --- /dev/null +++ b/include/xrpl/shamap/Family.h.ai.md @@ -0,0 +1,38 @@ +# `include/xrpl/shamap/Family.h` + +## Role in the System + +`Family` is the abstract interface that a `SHAMap` uses to reach everything outside itself: persistent storage, in-memory caches, a logging channel, and the recovery mechanism for gaps in local data. Every `SHAMap` holds a `Family&` reference (stored as `f_` in `SHAMap`), and all I/O and caching decisions flow through it. The interface exists to decouple the pure Merkle-radix-tree logic from the application infrastructure that backs it, making the map testable in isolation and allowing different deployment contexts (live node vs. unit test) to supply different implementations. + +## Why a Bundled Interface Rather Than Individual Injections? + +A `SHAMap` needs at least four external collaborators simultaneously: a database, two caches, and a log. Passing each one as a separate constructor argument would create a wide constructor signature that callers would have to assemble correctly on every construction. Wrapping them in `Family` gives a single dependency that carries a coherent set of resources with a shared lifetime. The caches and database for a given ledger family must be consistent with each other; bundling them prevents mismatched combinations from being constructed accidentally. + +## The Two Caches + +`getFullBelowCache()` returns a `FullBelowCache` (a `KeyCache`). An entry in this cache means "I have confirmed that every descendant of this tree node is already stored locally." During tree traversal or sync, if a node's hash is found in this cache, the subtree beneath it can be skipped entirely — there is nothing to fetch. The cache is generation-stamped (`m_gen`): calling `clear()` increments the generation, which invalidates all cached entries without purging them one-by-one. `reset()` clears and resets the generation back to 1, used when rebuilding from scratch. + +`getTreeNodeCache()` returns a `TreeNodeCache`, a `TaggedCache` that holds deserialized `SHAMapTreeNode` objects keyed by hash. When a node is read from the `NodeStore::Database`, it is deserialized into a `SHAMapTreeNode` and placed here. Subsequent lookups by the same hash retrieve the already-decoded object, avoiding redundant disk reads and deserialization. The use of `SharedWeakUnionPtr` as the internal pointer type lets the cache hold weak references that can be upgraded to strong ones — nodes can be evicted from the cache without immediately invalidating all live trees that share a pointer to them. + +## Missing Node Recovery + +The two `missingNode` methods are the error-recovery path triggered when a traversal reaches a node hash that is not in the cache and not in the local database. This signals an incomplete ledger — the local node joined the network after this ledger was validated and has not yet synced all its tree data. + +Two overloads exist because callers may have different identifying information available: + +- `missingNodeAcquireBySeq(refNum, nodeHash)` is called when the ledger is identified by its sequence number (the common case during normal validation). `nodeHash` is included for logging only. +- `missingNodeAcquireByHash(refHash, refNum)` is called when the ledger is identified by its hash (used during sync flows). + +In the concrete `NodeFamily` implementation, both paths eventually call an internal `acquire()` that forwards to `app_.getInboundLedgers().acquire(...)`, triggering peer-to-peer ledger fetching. The `missingNodeAcquireBySeq` path also maintains a `maxSeq_` high-water mark under `maxSeqMutex_` to avoid launching redundant acquisition requests when many concurrent SHAMap operations simultaneously discover missing nodes in the same or nearby ledgers. + +## Lifecycle: `sweep()` and `reset()` + +`sweep()` is called periodically by the application's maintenance loop to expire stale entries from both caches, preventing unbounded memory growth. `reset()` tears down the entire cache state — used when the family's data is being rebuilt (e.g., after a database wipe or during certain ledger-replaying scenarios). The clean separation of these two operations reflects different operational needs: sweeping is a routine background task, while reset is a destructive one-time action. + +## Non-Copyable, Non-Movable Design + +`Family` deletes all copy and move operations. This is intentional: `SHAMap` stores a `Family&` reference (not a pointer), and multiple `SHAMap` instances can share the same `Family`. If a `Family` could be moved, the stored references in all associated maps would dangle. The deleted operations enforce at compile time that `Family` instances have stable addresses for their full lifetime. + +## Relationship to `NodeFamily` + +The only production implementation, `NodeFamily` (in `src/xrpld/shamap/NodeFamily.h`), is constructed with an `Application&` and a `CollectorManager&`, wires the `db_` reference to the application's node store, and instantiates the two caches with appropriate sizes and expiration policies. Test code supplies lighter-weight implementations (see `src/test/shamap/common.h`) that use in-memory stores without the full application stack. \ No newline at end of file diff --git a/include/xrpl/shamap/FullBelowCache.h.ai.json b/include/xrpl/shamap/FullBelowCache.h.ai.json new file mode 100644 index 0000000000..95d7f72195 --- /dev/null +++ b/include/xrpl/shamap/FullBelowCache.h.ai.json @@ -0,0 +1,109 @@ +{ + "args": [ + { + "lineno": 22, + "name": "name" + }, + { + "lineno": 23, + "name": "clock" + }, + { + "lineno": 24, + "name": "j" + }, + { + "lineno": 25, + "name": "collector" + }, + { + "lineno": 26, + "name": "target_size" + }, + { + "lineno": 27, + "name": "expiration" + }, + { + "lineno": 55, + "name": "key" + }, + { + "lineno": 67, + "name": "key" + } + ], + "classes": [ + { + "args": [ + "name", + "clock", + "j", + "collector", + "target_size", + "expiration" + ], + "lineno": 13, + "name": "BasicFullBelowCache" + } + ], + "description": "Defines a cache class (BasicFullBelowCache) for tracking which tree keys have all descendants resident, optimizing the process of acquiring a complete tree in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/FullBelowCache.h", + "functions": [ + { + "args": [], + "lineno": 32, + "name": "clock" + }, + { + "args": [], + "lineno": 38, + "name": "size" + }, + { + "args": [], + "lineno": 46, + "name": "sweep" + }, + { + "args": [ + "key" + ], + "lineno": 54, + "name": "touch_if_exists" + }, + { + "args": [ + "key" + ], + "lineno": 66, + "name": "insert" + }, + { + "args": [], + "lineno": 77, + "name": "getGeneration" + }, + { + "args": [], + "lineno": 82, + "name": "clear" + }, + { + "args": [], + "lineno": 87, + "name": "reset" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/FullBelowCache.h.ai.md b/include/xrpl/shamap/FullBelowCache.h.ai.md new file mode 100644 index 0000000000..7244f5e340 --- /dev/null +++ b/include/xrpl/shamap/FullBelowCache.h.ai.md @@ -0,0 +1,43 @@ +# `FullBelowCache.h` — Subtree Completeness Cache for SHAMap Sync + +## Purpose and Context + +When an XRPL node needs to acquire or verify a complete SHAMap — the Merkle radix trie underpinning every ledger's transaction and account-state sets — it must traverse the trie depth-first to discover which nodes are absent from local storage. On a large, partially-synchronized tree this traversal is costly: every inner node potentially requires 16 child checks, each of which may hit the database. `FullBelowCache` exists to short-circuit this walk. Once a subtree rooted at some inner node is verified to have all descendants present, that inner node's hash is inserted into the cache. Subsequent traversals check the cache first; a hit means the entire subtree is complete and can be skipped without descending into it. + +The public alias `FullBelowCache = detail::BasicFullBelowCache` is what the rest of the codebase uses. The `detail::` wrapper is a conventional XRPL pattern for hiding implementation internals while keeping the public name clean. + +## Internal Structure + +`BasicFullBelowCache` wraps a `KeyCache`, which is itself a type alias for `TaggedCache`. This is a thread-safe, time-expiring key set — it stores only `uint256` hashes (the keys), not associated values. Items expire after a configurable duration (defaulting to two minutes) and the cache targets a configurable maximum size (defaulting to zero, meaning unbounded). All thread-safety guarantees are inherited from `TaggedCache`; every public method on `BasicFullBelowCache` delegates directly to it and is safe to call from any thread without external synchronization. + +## The Generation Mechanism + +The most non-obvious design element is the `m_gen` atomic counter. Each `SHAMapInnerNode` independently stores a `fullBelowGen_` field, and its `isFullBelow(generation)` check simply compares that field against the generation passed in. The generation is obtained at the start of a sync traversal via `getGeneration()` and then threaded through the entire walk. + +This two-layer scheme decouples the in-memory per-node markers from the persistent cache entries: + +- **`touch_if_exists(hash)`** is called during traversal. If an inner node's hash is already in `FullBelowCache`, the entire subtree is known complete; the traversal skips it entirely and returns `SHAMapAddNode::duplicate()`. This is the hot path during `SHAMap::addKnownNode`. + +- **`insert(hash)`** is called after a complete subtree traversal confirms no missing nodes. This records the fact persistently across SHAMap lifetimes and threads. + +- **`setFullBelowGen(gen)` on the inner node** records the same fact in the node's own memory. This short-circuits re-traversal within a single pass of `getMissingNodes` even before the hash is looked up in the cache. + +Together these two layers avoid redundant work at different granularities: the per-node generation handles intra-pass short-circuiting; the `FullBelowCache` handles inter-pass and cross-SHAMap reuse. + +## Invalidation with `clear()` vs. `reset()` + +`clear()` empties the cache and increments `m_gen`. The increment is the key action: all `SHAMapInnerNode` instances that stored the old generation in `fullBelowGen_` will no longer match the new generation, so `isFullBelow()` returns false for every node. This is a zero-cost global invalidation of all in-memory completeness markers — no tree walk is required to clear them. `NodeFamily::reset()` calls this when the family is being torn down and rebuilt between ledger replays or after missing-node recovery. + +`reset()` does the same cache purge but sets `m_gen = 1` rather than incrementing. This is used at initial construction and on full application restart, where it makes semantic sense to return to a canonical baseline generation rather than retaining a growing counter. + +The difference matters because any `SHAMapInnerNode` carrying `fullBelowGen_ > 1` would not match the reset-to-1 state, which is fine — those nodes are expected to be recreated fresh after a hard reset. + +## Integration Point: `Family` and `NodeFamily` + +`BasicFullBelowCache` is surfaced through the `Family` abstract interface via `getFullBelowCache()`, which returns a `shared_ptr`. The concrete implementation `NodeFamily` constructs a single instance owned as `fbCache_` and shares it across all SHAMaps in the same family. The `backed_` flag on each individual `SHAMap` controls whether the cache is consulted: unbacked (in-memory-only) maps bypass it, so only persistent maps that interact with the node store participate in cache sharing. + +Sweeping — expiring time-out entries — is triggered by `NodeFamily::sweep()`, which delegates to `fbCache_->sweep()` and then `tnCache_->sweep()` in tandem, keeping both caches aligned on the same housekeeping cycle. + +## Summary of Design Rationale + +Storing only the hash (key) rather than any tree structure keeps memory usage minimal. Time-based expiration via `TaggedCache` handles the case where a previously-complete subtree is later invalidated by a database eviction — entries age out naturally rather than requiring explicit notification. The generation counter provides a cheap, lock-free mechanism to globally invalidate all in-memory markers on demand. The result is a small, focused component that measurably reduces the cost of the most expensive operation in ledger synchronization. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMap.h.ai.json b/include/xrpl/shamap/SHAMap.h.ai.json new file mode 100644 index 0000000000..3b2efe407c --- /dev/null +++ b/include/xrpl/shamap/SHAMap.h.ai.json @@ -0,0 +1,590 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 38, + "name": "SHAMap" + }, + { + "args": [], + "lineno": 241, + "name": "SHAMap::const_iterator" + }, + { + "args": [ + "max", + "filter", + "maxDefer", + "generation" + ], + "lineno": 349, + "name": "SHAMap::MissingNodes" + } + ], + "description": "Defines the SHAMap class, a radix/Merkle tree used in XRPL for efficient storage and verification of ledger data. Provides methods for traversal, modification, synchronization, and proof generation/verification.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMap.h", + "functions": [ + { + "args": [], + "lineno": 74, + "name": "family" + }, + { + "args": [], + "lineno": 79, + "name": "family" + }, + { + "args": [], + "lineno": 377, + "name": "begin" + }, + { + "args": [], + "lineno": 382, + "name": "end" + }, + { + "args": [ + "isMutable" + ], + "lineno": 90, + "name": "snapShot" + }, + { + "args": [], + "lineno": 95, + "name": "setFull" + }, + { + "args": [ + "lseq" + ], + "lineno": 98, + "name": "setLedgerSeq" + }, + { + "args": [ + "hash", + "filter" + ], + "lineno": 101, + "name": "fetchRoot" + }, + { + "args": [ + "id" + ], + "lineno": 104, + "name": "hasItem" + }, + { + "args": [ + "id" + ], + "lineno": 107, + "name": "delItem" + }, + { + "args": [ + "type", + "item" + ], + "lineno": 110, + "name": "addItem" + }, + { + "args": [], + "lineno": 113, + "name": "getHash" + }, + { + "args": [ + "type", + "item" + ], + "lineno": 117, + "name": "updateGiveItem" + }, + { + "args": [ + "type", + "item" + ], + "lineno": 120, + "name": "addGiveItem" + }, + { + "args": [ + "id" + ], + "lineno": 124, + "name": "peekItem" + }, + { + "args": [ + "id", + "hash" + ], + "lineno": 126, + "name": "peekItem" + }, + { + "args": [ + "id" + ], + "lineno": 132, + "name": "upper_bound" + }, + { + "args": [ + "id" + ], + "lineno": 139, + "name": "lower_bound" + }, + { + "args": [ + "function" + ], + "lineno": 146, + "name": "visitNodes" + }, + { + "args": [ + "have", + "function" + ], + "lineno": 154, + "name": "visitDifferences" + }, + { + "args": [ + "function" + ], + "lineno": 162, + "name": "visitLeaves" + }, + { + "args": [ + "maxNodes", + "filter" + ], + "lineno": 172, + "name": "getMissingNodes" + }, + { + "args": [ + "wanted", + "data", + "fatLeaves", + "depth" + ], + "lineno": 177, + "name": "getNodeFat" + }, + { + "args": [ + "key" + ], + "lineno": 185, + "name": "getProofPath" + }, + { + "args": [ + "rootHash", + "key", + "path" + ], + "lineno": 194, + "name": "verifyProofPath" + }, + { + "args": [ + "s" + ], + "lineno": 201, + "name": "serializeRoot" + }, + { + "args": [ + "hash", + "rootNode", + "filter" + ], + "lineno": 204, + "name": "addRootNode" + }, + { + "args": [ + "nodeID", + "rawNode", + "filter" + ], + "lineno": 205, + "name": "addKnownNode" + }, + { + "args": [], + "lineno": 208, + "name": "setImmutable" + }, + { + "args": [], + "lineno": 209, + "name": "isSynching" + }, + { + "args": [], + "lineno": 210, + "name": "setSynching" + }, + { + "args": [], + "lineno": 211, + "name": "clearSynching" + }, + { + "args": [], + "lineno": 212, + "name": "isValid" + }, + { + "args": [ + "otherMap", + "differences", + "maxCount" + ], + "lineno": 217, + "name": "compare" + }, + { + "args": [], + "lineno": 220, + "name": "unshare" + }, + { + "args": [ + "t" + ], + "lineno": 223, + "name": "flushDirty" + }, + { + "args": [ + "missingNodes", + "maxMissing" + ], + "lineno": 226, + "name": "walkMap" + }, + { + "args": [ + "missingNodes", + "maxMissing" + ], + "lineno": 227, + "name": "walkMapParallel" + }, + { + "args": [ + "other" + ], + "lineno": 228, + "name": "deepCompare" + }, + { + "args": [], + "lineno": 230, + "name": "setUnbacked" + }, + { + "args": [ + "withHashes" + ], + "lineno": 232, + "name": "dump" + }, + { + "args": [], + "lineno": 233, + "name": "invariants" + }, + { + "args": [ + "hash" + ], + "lineno": 241, + "name": "cacheLookup" + }, + { + "args": [ + "hash", + "node" + ], + "lineno": 244, + "name": "canonicalize" + }, + { + "args": [ + "hash" + ], + "lineno": 247, + "name": "fetchNodeFromDB" + }, + { + "args": [ + "hash" + ], + "lineno": 249, + "name": "fetchNodeNT" + }, + { + "args": [ + "hash", + "filter" + ], + "lineno": 251, + "name": "fetchNodeNT" + }, + { + "args": [ + "hash" + ], + "lineno": 253, + "name": "fetchNode" + }, + { + "args": [ + "hash", + "filter" + ], + "lineno": 255, + "name": "checkFilter" + }, + { + "args": [ + "stack", + "target", + "terminal" + ], + "lineno": 259, + "name": "dirtyUp" + }, + { + "args": [ + "id", + "stack" + ], + "lineno": 265, + "name": "walkTowardsKey" + }, + { + "args": [ + "id" + ], + "lineno": 268, + "name": "findKey" + }, + { + "args": [ + "node", + "nodeID" + ], + "lineno": 271, + "name": "unshareNode" + }, + { + "args": [ + "node" + ], + "lineno": 275, + "name": "preFlushNode" + }, + { + "args": [ + "t", + "node" + ], + "lineno": 279, + "name": "writeNode" + }, + { + "args": [ + "node", + "stack", + "branch" + ], + "lineno": 282, + "name": "firstBelow" + }, + { + "args": [ + "node", + "stack", + "branch" + ], + "lineno": 287, + "name": "lastBelow" + }, + { + "args": [ + "node", + "stack", + "branch", + "loopParams" + ], + "lineno": 292, + "name": "belowHelper" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 299, + "name": "descend" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 301, + "name": "descendThrow" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 303, + "name": "descend" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 305, + "name": "descendThrow" + }, + { + "args": [ + "parent", + "branch", + "filter", + "pending", + "callback" + ], + "lineno": 310, + "name": "descendAsync" + }, + { + "args": [ + "parent", + "parentID", + "branch", + "filter" + ], + "lineno": 316, + "name": "descend" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 321, + "name": "descendNoStore" + }, + { + "args": [ + "node" + ], + "lineno": 324, + "name": "onlyBelow" + }, + { + "args": [ + "nodeID", + "hash" + ], + "lineno": 327, + "name": "hasInnerNode" + }, + { + "args": [ + "tag", + "hash" + ], + "lineno": 328, + "name": "hasLeafNode" + }, + { + "args": [ + "stack" + ], + "lineno": 330, + "name": "peekFirstItem" + }, + { + "args": [ + "id", + "stack" + ], + "lineno": 332, + "name": "peekNextItem" + }, + { + "args": [ + "node", + "otherMapItem", + "isFirstMap", + "differences", + "maxCount" + ], + "lineno": 334, + "name": "walkBranch" + }, + { + "args": [ + "doWrite", + "t" + ], + "lineno": 340, + "name": "walkSubTree" + }, + { + "args": [ + "MissingNodes", + "node" + ], + "lineno": 374, + "name": "gmn_ProcessNodes" + }, + { + "args": [ + "MissingNodes" + ], + "lineno": 376, + "name": "gmn_ProcessDeferredReads" + }, + { + "args": [ + "hash", + "object" + ], + "lineno": 379, + "name": "finishFetch" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 18, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMap.h.ai.md b/include/xrpl/shamap/SHAMap.h.ai.md new file mode 100644 index 0000000000..4889128311 --- /dev/null +++ b/include/xrpl/shamap/SHAMap.h.ai.md @@ -0,0 +1,58 @@ +# `include/xrpl/shamap/SHAMap.h` + +## Role and Purpose + +`SHAMap` is the foundational authenticated data structure in the XRP Ledger. Every ledger is composed of two `SHAMap` instances: one that maps transaction IDs to transaction data (type `TRANSACTION`), and one that maps account-state object keys to their serialized state (type `STATE`). The root hash of each tree is what a validator signs and what makes ledger agreement deterministic — two validators hold the same ledger if and only if their `SHAMap` root hashes match. + +The class is simultaneously a radix tree and a Merkle tree. As a radix tree with fan-out 16, each inner node selects one of 16 children based on a 4-bit nibble of the 256-bit key, giving a fixed tree depth of 64 levels (`leafDepth = 64`; keys are consumed 4 bits per level across a 256-bit key space). As a Merkle tree, every inner node's hash is derived from the combined hashes of its children. This combination gives O(log N) key lookup and O(log N) membership proofs while allowing any subset of nodes to be missing during synchronization. + +## Tree Structure + +The node hierarchy has three types, all rooted in `SHAMapTreeNode`: + +- **`SHAMapInnerNode`** — holds exactly `branchFactor = 16` logical child slots tracked by a 16-bit bitmask (`isBranch_`). Children are stored sparsely via `TaggedPointer hashesAndChildren_`, which manages the hash and child-pointer arrays together. A per-child atomic bitlock (`std::atomic lock_`) allows concurrent descent without coarser locking. + +- **`SHAMapLeafNode`** — wraps a `boost::intrusive_ptr` payload. Leaf nodes appear only at depth 64; the radix property guarantees their position already encodes the full key. + +- **`SHAMapItem`** — the actual ledger object stored in a leaf. Its `uint256 tag_` is the key, and the variable-length body is stored immediately after the object header in the same allocation (a struct-hack layout). Allocations are served from a `SlabAllocatorSet` pre-configured for seven common size classes up to 1052 bytes, falling back to `new[]` only for oversized items. + +## Copy-on-Write and Snapshots + +The most important design choice in `SHAMap` is copy-on-write (CoW) sharing of tree nodes across map instances. Every `SHAMapTreeNode` carries a `cowid_` field: when non-zero it identifies the single `SHAMap` instance that has the right to mutate that node. A `cowid_` of zero means the node is shared and cannot be modified. + +When `snapShot(isMutable)` is called, no tree nodes are copied. The snapshot and the original share the same physical nodes. The `cowid_` of the new map is set to the current `cowid_` of the original (for a mutable snapshot), and the original's `cowid_` is then incremented so it owns new nodes exclusively. Any subsequent write to either map that encounters a node owned by the other map calls `unshareNode()`, which clones the node and assigns it to the writer's `cowid_`. This makes snapshotting a closed ledger essentially free — the open ledger's `SHAMap` is snapshotted, the snapshot is made immutable, and the open ledger continues mutating under its own CoW identity. + +## Memory Management + +Two distinct intrusive pointer systems coexist. `boost::intrusive_ptr` manages leaf payloads with simple reference counting. Tree nodes use `intr_ptr::SharedPtr` (aliased as `SharedIntrusive`), a custom intrusive smart pointer that additionally supports weak references. The split matters: the node cache (`TreeNodeCache`) stores a `SharedWeakUnion` — a single pointer-sized value that holds either a strong or weak reference, toggled by a low-order tag bit — so that cached nodes can be promoted from weak to strong without a heap allocation. When a node's strong count drops to zero, `partialDestructor()` is called before the destructor proper; `SHAMapInnerNode` uses this hook to release its children early, allowing cascading collection without deep call stacks. + +## State Lifecycle + +`SHAMapState` is a four-state enum that controls what operations are legal: + +- **`Modifying`** — open ledger; items can be added, removed, and updated. +- **`Immutable`** — closed ledger; all writes are forbidden. Asserts guard against attempts to mutate. +- **`Synching`** — the root hash is known (received from a peer) but interior nodes may be missing. `addRootNode()` and `addKnownNode()` feed incoming wire data into the map; `getMissingNodes()` probes for gaps. +- **`Invalid`** — synchronization failed or the map is known corrupt. + +`setImmutable()` asserts that the current state is not `Invalid`, enforcing that only coherent maps are frozen. + +## Synchronization Engine (`getMissingNodes`) + +The peer-sync path is built around the private `MissingNodes` struct. It implements a depth-first traversal with bounded async I/O concurrency. A `std::stack>` drives the main DFS loop — the comment in the header explicitly calls out that `std::deque` is required here rather than `std::vector` because insertion and removal must not invalidate existing pointers or references. Nodes whose children are being fetched asynchronously are moved to a `resumes_` map, and a mutex+condition variable pair (`deferLock_`, `deferCondVar_`) coordinates the callback from `descendAsync()` back into `gmn_ProcessDeferredReads()`. The `generation_` field aligns with `SHAMapInnerNode::fullBelowGen_` — a monotonically increasing counter that marks subtrees already confirmed complete, allowing the traversal to skip entire branches on repeated calls. + +## Merkle Proof API + +`getProofPath(key)` returns the sequence of serialized nodes from the target leaf up to the root, with sibling hashes encoded in each parent's serialization. The companion static method `verifyProofPath(rootHash, key, path)` recomputes the root from the path without needing the full tree. These methods enable stateless light-client verification that a specific ledger object exists in a given ledger state. + +## The `Family` Interface and Backing + +`SHAMap` stores a reference to a `Family`, the abstract provider for the node store (`NodeStore::Database`), the `FullBelowCache`, the `TreeNodeCache`, and the "missing node" acquisition callbacks. The `Family` decouples storage policy from tree logic: two maps of the same ledger share a family and thus share their caches. Maps created with `setUnbacked()` (`backed_ = false`) skip nodestore writes entirely, useful for transient in-memory trees such as those built during transaction processing before they're committed. + +## Iterator Design + +`SHAMap::const_iterator` is a forward iterator over leaf nodes in key order. It carries its own `SharedPtrNodeStack` — a `std::stack` of `(SHAMapTreeNode, SHAMapNodeID)` pairs. This stack represents the path from the root to the current position, allowing `peekNextItem()` to resume descent without rescanning from the root. The iterator is always const because the tree's Merkle invariant requires that any write invalidate hashes all the way up to the root; a non-const iterator would either break the invariant silently or require expensive re-hashing on every dereference. + +## Delta Computation + +`compare(otherMap, differences, maxCount)` computes the symmetric difference between two maps, returning results as a `Delta` — a `std::map` where each `DeltaItem` is a `(before, after)` pair of `SHAMapItem` intrusive pointers. A null `before` means the item is new; a null `after` means it was deleted. This is used to determine exactly which account-state objects changed when a ledger closes, and which transactions are new relative to a peer's last-known ledger. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h.ai.json b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h.ai.json new file mode 100644 index 0000000000..32f0e61459 --- /dev/null +++ b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h.ai.json @@ -0,0 +1,72 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "boost::intrusive_ptr item", + "std::uint32_t cowid" + ], + "lineno": 11, + "name": "SHAMapAccountStateLeafNode" + } + ], + "description": "Defines the SHAMapAccountStateLeafNode class, a specialized leaf node for state objects in the SHAMap structure, providing serialization and hash update logic for account state leaves.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapAccountStateLeafNode.h", + "functions": [ + { + "args": [ + "boost::intrusive_ptr item", + "std::uint32_t cowid" + ], + "lineno": 13, + "name": "SHAMapAccountStateLeafNode" + }, + { + "args": [ + "boost::intrusive_ptr item", + "std::uint32_t cowid", + "SHAMapHash const& hash" + ], + "lineno": 18, + "name": "SHAMapAccountStateLeafNode" + }, + { + "args": [ + "std::uint32_t cowid" + ], + "lineno": 25, + "name": "clone" + }, + { + "args": [], + "lineno": 31, + "name": "getType" + }, + { + "args": [], + "lineno": 36, + "name": "updateHash" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 42, + "name": "serializeForWire" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 48, + "name": "serializeWithPrefix" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapAccountStateLeafNode.h.ai.md b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h.ai.md new file mode 100644 index 0000000000..36dea15637 --- /dev/null +++ b/include/xrpl/shamap/SHAMapAccountStateLeafNode.h.ai.md @@ -0,0 +1,57 @@ +# `SHAMapAccountStateLeafNode` + +## Role in the SHAMap + +The XRPL's SHAMap is a Patricia trie whose leaves hold arbitrary typed blobs. Account state — the serialized form of ledger objects like account roots, offers, and trust lines — occupies one specific leaf variant: `SHAMapAccountStateLeafNode`. Alongside `SHAMapTxLeafNode` (transaction, no metadata) and `SHAMapTxPlusMetaLeafNode` (transaction with metadata), this class is one of three concrete leaf types that close the otherwise-abstract `SHAMapLeafNode` / `SHAMapTreeNode` hierarchy. + +The separation into distinct concrete types rather than a single leaf class with a runtime flag is deliberate: each leaf type computes its Merkle hash using a different prefix and a different set of fields, and having the logic encoded statically via virtual dispatch eliminates branching in the hot path and makes the type distinction visible to the compiler. + +## Inheritance and Memory Management + +``` +SHAMapTreeNode (IntrusiveRefCounts, copy-on-write id, hash) + └── SHAMapLeafNode (holds item_, peekItem(), setItem()) + └── SHAMapAccountStateLeafNode (final, this file) +``` + +The class also inherits `CountedObject`, which provides lightweight global instance tracking. All tree nodes participate in intrusive reference counting via `IntrusiveRefCounts`, so they are managed through `boost::intrusive_ptr` and `intr_ptr::SharedPtr` rather than `std::shared_ptr`, avoiding the separate control-block allocation. + +## Copy-on-Write Semantics and the Two Constructors + +`SHAMapTreeNode` carries a `cowid_` field — a `uint32_t` that identifies which `SHAMap` instance owns this node. When `cowid_` is `0`, the node is unowned and shareable across multiple maps. When non-zero it belongs exclusively to one map and is considered dirty. + +`SHAMapAccountStateLeafNode` exposes two constructors to serve this protocol: + +1. **Construction with hash recomputation** (`item`, `cowid`): used when creating a brand-new node. It delegates to `SHAMapLeafNode` then immediately calls `updateHash()`. + +2. **Construction with a pre-supplied hash** (`item`, `cowid`, `hash`): used by `clone()`. When the SHAMap needs to mutate a shared node it calls `clone()` to produce an exclusive copy; since the underlying item hasn't changed yet, recomputing the hash would be wasteful. Passing the known `hash_` directly skips the SHA computation. + +`clone()` always uses the second form, forwarding the caller's new `cowid` and the current `hash_`: + +```cpp +return intr_ptr::make_shared(item_, cowid, hash_); +``` + +## Hash Computation — Why Account State Includes the Key + +`updateHash()` feeds three pieces of data to `sha512Half`: + +```cpp +hash_ = SHAMapHash{sha512Half(HashPrefix::leafNode, item_->slice(), item_->key())}; +``` + +Contrast this with `SHAMapTxLeafNode::updateHash()`, which feeds only the `HashPrefix::transactionID` and `item_->slice()` — the key is absent. The reason is fundamental to how each leaf type is identified: + +- A **transaction** is named by the hash of its own serialized content (i.e., the transaction ID *is* `sha512Half(prefix, blob)`). Including the key would double-count information already encoded in the blob. + +- An **account state** object is keyed by an externally assigned ID (e.g., an account address or offer index) that does not necessarily appear verbatim in the serialized blob. Omitting the key from the hash would mean two objects with identical data but different keys would produce identical hashes, undermining the integrity of the state Merkle root. Including the key in the hash commitment binds the object firmly to its position in the trie. + +`HashPrefix::leafNode` encodes the ASCII bytes `'M'`, `'L'`, `'N'` — a domain-separation prefix that prevents collisions between hash computations of different node types. The `serializeWithPrefix()` method replicates this exact sequence for external use (e.g., proof verification), emitting the same prefix, blob, and key in the same order. + +## Wire Serialization + +`serializeForWire()` is the peer-sync format: it writes the raw blob, then the key as a fixed-width bit string, then the single-byte wire type tag `wireTypeAccountState = 1`. The tag allows `SHAMapTreeNode::makeFromWire()` to reconstruct the correct concrete type on the receiving end. The `serializeWithPrefix()` method is used for hashing contexts and drops the wire tag (since the hash prefix already encodes the node type); it also uses `s.add32()` for the prefix rather than `s.add8()` so the domain prefix is exactly 4 bytes. + +## Summary + +`SHAMapAccountStateLeafNode` is a narrowly scoped, stateless policy class whose only responsibility is to encode the account-state-specific rules for hashing, cloning, type identification, and serialization. All mutable state lives in the base classes. Its `final` qualification, together with the two-constructor clone optimization and the key-inclusive hash formula, reflect careful attention to the performance and correctness requirements of the XRPL's cryptographic ledger state tree. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapAddNode.h.ai.json b/include/xrpl/shamap/SHAMapAddNode.h.ai.json new file mode 100644 index 0000000000..43786d577f --- /dev/null +++ b/include/xrpl/shamap/SHAMapAddNode.h.ai.json @@ -0,0 +1,119 @@ +{ + "args": [ + { + "lineno": 35, + "name": "good" + }, + { + "lineno": 35, + "name": "bad" + }, + { + "lineno": 35, + "name": "duplicate" + }, + { + "lineno": 26, + "name": "n" + } + ], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "SHAMapAddNode" + } + ], + "description": "Defines the SHAMapAddNode class, which tracks the results of adding nodes to a SHAMap, including counts of good, bad, and duplicate nodes, and provides utility methods for manipulating and querying these counts.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapAddNode.h", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "SHAMapAddNode" + }, + { + "args": [], + "lineno": 16, + "name": "incInvalid" + }, + { + "args": [], + "lineno": 17, + "name": "incUseful" + }, + { + "args": [], + "lineno": 18, + "name": "incDuplicate" + }, + { + "args": [], + "lineno": 19, + "name": "reset" + }, + { + "args": [], + "lineno": 20, + "name": "getGood" + }, + { + "args": [], + "lineno": 21, + "name": "isGood" + }, + { + "args": [], + "lineno": 22, + "name": "isInvalid" + }, + { + "args": [], + "lineno": 23, + "name": "isUseful" + }, + { + "args": [], + "lineno": 24, + "name": "get" + }, + { + "args": [ + "n" + ], + "lineno": 26, + "name": "operator+=" + }, + { + "args": [], + "lineno": 28, + "name": "duplicate" + }, + { + "args": [], + "lineno": 30, + "name": "useful" + }, + { + "args": [], + "lineno": 32, + "name": "invalid" + }, + { + "args": [ + "good", + "bad", + "duplicate" + ], + "lineno": 35, + "name": "SHAMapAddNode" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapAddNode.h.ai.md b/include/xrpl/shamap/SHAMapAddNode.h.ai.md new file mode 100644 index 0000000000..f7eabb9010 --- /dev/null +++ b/include/xrpl/shamap/SHAMapAddNode.h.ai.md @@ -0,0 +1,38 @@ +# `SHAMapAddNode.h` — Node-Sync Result Accumulator + +`SHAMapAddNode` is a small value-type accumulator used to report and aggregate the outcomes of adding nodes to a `SHAMap` during ledger synchronization. When a rippled node is catching up to the network, it requests raw trie nodes for transaction sets and account state maps from its peers. Each received node is either new and valid ("useful"), already known ("duplicate"), or corrupt/unexpected ("invalid"). This class collects those three counts so callers can assess the quality of a peer's response. + +## Design as Named Return Values + +The most notable design choice is the set of private-constructor static factory methods: `useful()`, `invalid()`, and `duplicate()`. These construct single-count instances (`mGood=1`, `mBad=1`, and `mDuplicate=1` respectively) and are used directly as return values from `SHAMap::addRootNode()` and `SHAMap::addKnownNode()`. This pattern gives the calling code self-documenting clarity: + +```cpp +if (!node || node->getHash() != hash) + return SHAMapAddNode::invalid(); +// ... +return SHAMapAddNode::useful(); +``` + +The alternative — returning a plain enum or boolean — would lose the ability to accumulate multiple outcomes, which matters in `InboundLedger::receiveNode()` where a batch of nodes is processed and the aggregate `SHAMapAddNode` is passed by reference, incremented incrementally via `incInvalid()`, `incUseful()`, and `incDuplicate()`, then inspected once at the call site. + +## The `isGood()` vs `isUseful()` Distinction + +The two boolean queries encode different questions. `isUseful()` answers "did we receive at least one new node we didn't already have?". `isGood()` answers "was this exchange not net-harmful?" — its implementation is `(mGood + mDuplicate) > mBad`. Duplicates count on the positive side because receiving something you already know is benign; it's not evidence of a misbehaving peer. Only `mBad` accumulates evidence of corruption or protocol violations. `TransactionAcquire::takeNodes()` uses `isGood()` as the gate on whether to continue processing a peer's contribution, while `isUseful()` signals whether the sync actually made forward progress. + +## Aggregation via `operator+=` + +`operator+=` combines two `SHAMapAddNode` instances by summing all three counters. This lets the ledger acquisition code build a running tally across a batch or across multiple calls, then log or evaluate the aggregate. The `get()` method formats a human-readable string (e.g., `"good:3 dupe:1"`) used in debug-level journal output when nodes arrive. + +## Header-Only, All Inline + +Every method is `inline` in the header. The class has no dependencies beyond `` and carries no heap allocations or virtual dispatch. This makes it safe and cheap to construct on the stack, pass by value, and return from functions without any concern about overhead — which is appropriate for a type that exists purely to carry a small status payload up the call stack. + +## Summary of Semantics + +| Counter | Incremented by | Meaning | +|---------|---------------|---------| +| `mGood` | `incUseful()` | Node was new and hash-verified | +| `mBad` | `incInvalid()` | Node was corrupt, hash-mismatched, or structurally wrong | +| `mDuplicate` | `incDuplicate()` | Node was valid but already present | + +The class sits at the boundary between the low-level `SHAMap` trie operations and the higher-level peer-reputation / acquisition-progress logic in `InboundLedger` and `TransactionAcquire`, providing a compact, composable status token that flows upward through the sync protocol machinery. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapInnerNode.h.ai.json b/include/xrpl/shamap/SHAMapInnerNode.h.ai.json new file mode 100644 index 0000000000..cc2e5a3c80 --- /dev/null +++ b/include/xrpl/shamap/SHAMapInnerNode.h.ai.json @@ -0,0 +1,337 @@ +{ + "args": [ + { + "lineno": 38, + "name": "toAllocate" + }, + { + "lineno": 52, + "name": "i" + }, + { + "lineno": 65, + "name": "f" + }, + { + "lineno": 75, + "name": "f" + }, + { + "lineno": 94, + "name": "cowid" + }, + { + "lineno": 94, + "name": "numAllocatedChildren" + }, + { + "lineno": 124, + "name": "m" + }, + { + "lineno": 128, + "name": "m" + }, + { + "lineno": 130, + "name": "child" + }, + { + "lineno": 132, + "name": "child" + }, + { + "lineno": 134, + "name": "branch" + }, + { + "lineno": 137, + "name": "branch" + }, + { + "lineno": 140, + "name": "branch" + }, + { + "lineno": 140, + "name": "node" + }, + { + "lineno": 143, + "name": "generation" + }, + { + "lineno": 145, + "name": "gen" + }, + { + "lineno": 153, + "name": "Serializer&" + }, + { + "lineno": 155, + "name": "Serializer&" + }, + { + "lineno": 157, + "name": "SHAMapNodeID const&" + }, + { + "lineno": 159, + "name": "is_root" + }, + { + "lineno": 162, + "name": "data" + }, + { + "lineno": 162, + "name": "hash" + }, + { + "lineno": 162, + "name": "hashValid" + }, + { + "lineno": 166, + "name": "data" + } + ], + "classes": [ + { + "args": [ + "cowid", + "numAllocatedChildren" + ], + "lineno": 10, + "name": "SHAMapInnerNode" + } + ], + "description": "Defines the SHAMapInnerNode class, representing an inner node in a SHAMap radix tree structure, including its children management, hash calculation, serialization, and related operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapInnerNode.h", + "functions": [ + { + "args": [ + "toAllocate" + ], + "lineno": 38, + "name": "resizeChildArrays" + }, + { + "args": [ + "i" + ], + "lineno": 52, + "name": "getChildIndex" + }, + { + "args": [ + "f" + ], + "lineno": 65, + "name": "iterChildren" + }, + { + "args": [ + "f" + ], + "lineno": 75, + "name": "iterNonEmptyChildIndexes" + }, + { + "args": [], + "lineno": 101, + "name": "partialDestructor" + }, + { + "args": [ + "cowid" + ], + "lineno": 104, + "name": "clone" + }, + { + "args": [], + "lineno": 107, + "name": "getType" + }, + { + "args": [], + "lineno": 112, + "name": "isLeaf" + }, + { + "args": [], + "lineno": 117, + "name": "isInner" + }, + { + "args": [], + "lineno": 122, + "name": "isEmpty" + }, + { + "args": [ + "m" + ], + "lineno": 124, + "name": "isEmptyBranch" + }, + { + "args": [], + "lineno": 126, + "name": "getBranchCount" + }, + { + "args": [ + "m" + ], + "lineno": 128, + "name": "getChildHash" + }, + { + "args": [ + "m", + "child" + ], + "lineno": 130, + "name": "setChild" + }, + { + "args": [ + "m", + "child" + ], + "lineno": 132, + "name": "shareChild" + }, + { + "args": [ + "branch" + ], + "lineno": 134, + "name": "getChildPointer" + }, + { + "args": [ + "branch" + ], + "lineno": 137, + "name": "getChild" + }, + { + "args": [ + "branch", + "node" + ], + "lineno": 140, + "name": "canonicalizeChild" + }, + { + "args": [ + "generation" + ], + "lineno": 143, + "name": "isFullBelow" + }, + { + "args": [ + "gen" + ], + "lineno": 145, + "name": "setFullBelowGen" + }, + { + "args": [], + "lineno": 148, + "name": "updateHash" + }, + { + "args": [], + "lineno": 151, + "name": "updateHashDeep" + }, + { + "args": [ + "Serializer&" + ], + "lineno": 153, + "name": "serializeForWire" + }, + { + "args": [ + "Serializer&" + ], + "lineno": 155, + "name": "serializeWithPrefix" + }, + { + "args": [ + "SHAMapNodeID const&" + ], + "lineno": 157, + "name": "getString" + }, + { + "args": [ + "is_root" + ], + "lineno": 159, + "name": "invariants" + }, + { + "args": [ + "data", + "hash", + "hashValid" + ], + "lineno": 162, + "name": "makeFullInner" + }, + { + "args": [ + "data" + ], + "lineno": 166, + "name": "makeCompressedInner" + }, + { + "args": [], + "lineno": 170, + "name": "isEmpty" + }, + { + "args": [ + "m" + ], + "lineno": 175, + "name": "isEmptyBranch" + }, + { + "args": [], + "lineno": 180, + "name": "getBranchCount" + }, + { + "args": [ + "generation" + ], + "lineno": 185, + "name": "isFullBelow" + }, + { + "args": [ + "gen" + ], + "lineno": 190, + "name": "setFullBelowGen" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapInnerNode.h.ai.md b/include/xrpl/shamap/SHAMapInnerNode.h.ai.md new file mode 100644 index 0000000000..fa653ee262 --- /dev/null +++ b/include/xrpl/shamap/SHAMapInnerNode.h.ai.md @@ -0,0 +1,43 @@ +# `SHAMapInnerNode.h` — Inner Node of the XRPL Merkle Radix Tree + +## Role in the System + +`SHAMapInnerNode` is the branching (non-leaf) node type of the SHAMap, the authenticated Merkle radix tree used by XRPL to represent all ledger state and transaction sets. Each inner node fans out into exactly 16 children (one per hexadecimal nibble of a 256-bit key), making the tree depth at most 64 levels. The class inherits from `SHAMapTreeNode`, which provides the copy-on-write identity (`cowid_`) and hash storage, and from `CountedObject` for per-type allocation tracking. + +## Sparse Child Storage via `TaggedPointer` + +The most significant architectural decision in this file is the use of `TaggedPointer hashesAndChildren_` to hold both the child hashes and child node pointers in a single pointer-sized field. `TaggedPointer` exploits the fact that pointers are naturally aligned to at least 4 bytes, so the two lowest bits of the pointer are always zero at runtime. Those bits store a tag (0–3) that encodes which of four capacity tiers the arrays use: 2, 4, 8, or 16 elements. The hash array and the shared-pointer array are laid out back-to-back in a single allocation. + +The `isBranch_` field (`uint16_t`, one bit per branch) is the authoritative record of which of the 16 branches are populated. When the arrays are smaller than 16 elements (sparse mode), children are packed in branch-index order: if only branches 2 and 14 are occupied, they sit at array positions 0 and 1 respectively. `getChildIndex(int i)` converts from logical branch number to physical array index; it returns `std::nullopt` when the branch is empty and the arrays are sparse. `iterNonEmptyChildIndexes(F)` supplies both the branch number and the array index to its callback, bridging the logical and physical views. + +The motivation is RAM. A production SHAMap has a large proportion of inner nodes that hold only a handful of children. Measurements cited in `TaggedPointer.h` show that sparse representation reduces inner-node memory to roughly 25% of the naive dense representation. `resizeChildArrays()` handles the lifecycle transition: when a child is added or removed, it reconstructs `hashesAndChildren_` via the `TaggedPointer` move constructor that accepts both source and destination branch bitsets, copying only the surviving children. + +## Copy-on-Write and `clone()` + +The `cowid_` field inherited from `SHAMapTreeNode` identifies the SHAMap that exclusively owns this node. A value of 0 signals that the node is clean and shareable across multiple map instances (snapshots, parallel reads). Any SHAMap that needs to modify a shared node must call `clone()` first, which allocates a new `SHAMapInnerNode` with the caller's `cowid` and copies all hashes and child pointers. + +The `clone()` implementation is careful about the sparse/dense distinction. If the source is sparse, it re-packs child hashes into sequential positions in the clone's arrays. It acquires the per-node `lock_` spinlock only for the child-pointer copy — hashes can be read without locking because they are immutable on shared nodes. + +## Fine-Grained Bit-Level Spinlocking + +`lock_` is a `mutable std::atomic` used as a 16-bit lock with one bit per child slot, not per node. `getChild()`, `getChildPointer()`, and `canonicalizeChild()` all use a `packed_spinlock` that spins on the single bit corresponding to the child's physical array index. This allows concurrent access to different children of the same node without global serialization. + +`canonicalizeChild()` is the deduplication primitive used when multiple threads simultaneously load the same child from backing storage. The first caller to lock the child's bit slot installs its freshly-loaded node pointer; any subsequent caller finds the slot already populated and returns the existing pointer instead — this is the "winner keeps it" pattern for concurrent lazy loading. The node hash is verified to match before installation as a consistency check. + +## Full-Below Optimization + +`fullBelowGen_` stores a generation counter. When `isFullBelow(generation)` returns true, the caller knows that every node in the entire subtree below this node is already present in local storage and does not need to be fetched from peers. `setFullBelowGen(gen)` marks the subtree as complete for the current synchronization pass. Because generations monotonically increase, a stale `fullBelowGen_` value automatically becomes invalid on the next sync cycle without requiring explicit invalidation. + +## Wire Serialization + +`serializeForWire()` chooses between two wire formats based on occupancy. Nodes with fewer than 12 populated branches use the *compressed inner* format: each non-empty branch is emitted as a 256-bit hash followed by a one-byte position, for 33 bytes per child. Denser nodes use the *full inner* format: all 16 hashes in order, 512 bytes total. The type byte appended at the end lets the receiver decode the format. `makeFullInner()` and `makeCompressedInner()` are the static factory methods that reconstruct an inner node from these respective wire formats; both call `resizeChildArrays()` after parsing to right-size the arrays for actual occupancy. + +`serializeWithPrefix()` always emits the full 16-hash form preceded by the `HashPrefix::innerNode` type prefix. This is the canonical hash-input form used by `updateHash()`. + +## Hash Computation + +`updateHash()` produces the node's Merkle hash as SHA-512/2 of `HashPrefix::innerNode` concatenated with all 16 child hashes, feeding zero-hashes for empty branches. The result is the commitment covering the entire subtree. `updateHashDeep()` first pulls hashes from in-memory child objects into the local `hashes` array (bridging the case where child nodes were set via pointer but `hashes` was zeroed by `setChild()`), then delegates to `updateHash()`. + +## Destruction and Intrusive Weak Pointers + +`partialDestructor()` is called by the intrusive reference-count infrastructure before memory is reclaimed but while weak references to the object may still exist. It explicitly resets all child `SharedPtr`s, breaking any reference cycles and ensuring clean resource teardown even when the object's storage outlives its strong references. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapItem.h.ai.json b/include/xrpl/shamap/SHAMapItem.h.ai.json new file mode 100644 index 0000000000..a935bcf577 --- /dev/null +++ b/include/xrpl/shamap/SHAMapItem.h.ai.json @@ -0,0 +1,70 @@ +{ + "args": [ + { + "lineno": 41, + "name": "tag" + }, + { + "lineno": 41, + "name": "data" + }, + { + "lineno": 31, + "name": "other" + } + ], + "classes": [ + { + "args": [ + "uint256 const& tag", + "Slice data" + ], + "lineno": 10, + "name": "SHAMapItem" + } + ], + "description": "Defines the SHAMapItem class, representing an item stored in a SHAMap, with custom memory management and reference counting using boost::intrusive_ptr and slab allocation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapItem.h", + "functions": [ + { + "args": [ + "SHAMapItem const* x" + ], + "lineno": 65, + "name": "intrusive_ptr_add_ref" + }, + { + "args": [ + "SHAMapItem const* x" + ], + "lineno": 73, + "name": "intrusive_ptr_release" + }, + { + "args": [ + "uint256 const& tag", + "Slice data" + ], + "lineno": 89, + "name": "make_shamapitem" + }, + { + "args": [ + "SHAMapItem const& other" + ], + "lineno": 110, + "name": "make_shamapitem" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 56, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapItem.h.ai.md b/include/xrpl/shamap/SHAMapItem.h.ai.md new file mode 100644 index 0000000000..2e5c58faba --- /dev/null +++ b/include/xrpl/shamap/SHAMapItem.h.ai.md @@ -0,0 +1,39 @@ +# `SHAMapItem.h` — Leaf Data Items for the SHAMap Trie + +`SHAMapItem` is the payload-bearing leaf object in the XRP Ledger's Merkle-Patricia trie (`SHAMap`). Every ledger object, transaction, or transaction-with-metadata that participates in consensus state is ultimately stored as a `SHAMapItem` keyed by its `uint256` hash. The file is entirely self-contained: it defines the class, the slab allocator pool that backs it, and the `boost::intrusive_ptr` lifetime hooks — all in the header. + +## Variable-Length Struct Layout + +The central design decision in `SHAMapItem` is the **struct-hack layout**: the item's payload bytes are placed in memory directly after the fixed-size struct fields. The constructor does this with a single `std::memcpy` into `reinterpret_cast(this) + sizeof(*this)`, and `data()` reads it back the same way. This avoids any separate heap allocation for the payload, keeping the header and payload in one contiguous block with one allocation lifetime. + +Because the payload must immediately follow the struct, objects cannot be constructed on the stack or by value — the constructor is `private`, copy and move operations are all explicitly deleted, and the only valid creation path is the `make_shamapitem()` factory, which pre-allocates the right amount of raw memory before calling placement new. `size_` is stored as `uint32_t` rather than `size_t` to save four bytes of struct size (the comment notes that no SHAMap item will ever exceed 4 GB), which directly reduces the amount of dead space per slab slot. + +## Slab Allocation Strategy + +For a production XRPL node, hundreds of thousands of `SHAMapItem` objects are live simultaneously. Routing each allocation through the system allocator would fragment the heap and lose cache locality. Instead, the file declares a module-level `detail::slabber` — a `SlabAllocatorSet` — with seven size tiers ranging from 128 to 1052 extra bytes (added to `sizeof(SHAMapItem)`): + +``` +128 B → 60 MiB 192 B → 46 MiB 272 B → 60 MiB +384 B → 56 MiB 564 B → 40 MiB 772 B → 46 MiB +1052 B → 60 MiB +``` + +The cutoffs and backing sizes were tuned to the empirical size distribution of ledger objects and to minimise intra-block padding. Each backing block is allocated at a 2 MiB boundary, allowing Linux's transparent huge-page support to engage automatically when available. Blocks are linked into a lock-free list per `SlabAllocator`; allocation within a block uses a per-block mutex only to pop from a freelist, keeping contention minimal. + +`make_shamapitem()` calls `detail::slabber.allocate(data.size())`, which walks the sorted allocator list and returns from the first tier whose slot size fits `sizeof(SHAMapItem) + data.size()`. If all tiers are exhausted or the payload exceeds the largest tier, the factory falls back to `new uint8_t[sizeof(SHAMapItem) + data.size()]`. The maximum allowed payload is asserted to be ≤ 16 MiB, serving as a sanity guard against corrupt inputs. + +## Intrusive Reference Counting + +`SHAMapItem` is always managed through `boost::intrusive_ptr`, embedding the reference count inside the object as `mutable std::atomic refcount_`. This avoids the separate control-block allocation that `std::shared_ptr` requires, which matters both for allocation overhead and for cache layout when many leaf nodes share the same item (copy-on-write in the trie creates sharing). + +The ADL-found friends `intrusive_ptr_add_ref` and `intrusive_ptr_release` implement the protocol. `add_ref` guards against a pathological race: if the refcount has already reached zero before the increment, it calls `LogicError` rather than silently resurrecting a dead object. The constructor initialises `refcount_` to `1`, so `make_shamapitem` passes `false` as the second argument to `boost::intrusive_ptr{ptr, false}` — explicitly suppressing the automatic increment that would otherwise bring the count to 2. + +`intrusive_ptr_release` handles the two-phase destruction that the layout demands: it first calls `std::destroy_at` to properly run the `SHAMapItem` destructor (needed because `CountedObject`'s destructor is not trivial — it decrements a global diagnostic counter), then returns the raw memory to `detail::slabber`. If the pointer wasn't slab-managed (because it came from the `new uint8_t[]` fallback path), `slabber.deallocate` returns `false` and the code falls through to `delete[]`. The `if constexpr (!std::is_trivially_destructible_v)` guard is forward-looking: if the destructor chain ever becomes trivial, the compile-time branch eliminates the call entirely. + +## Immutability and Use in SHAMapLeafNode + +`SHAMapLeafNode` holds `boost::intrusive_ptr` — the `const` is intentional. Once constructed, a `SHAMapItem`'s key and payload never change; the SHAMap copy-on-write protocol creates a new item rather than modifying an existing one. The duplicate `make_shamapitem(SHAMapItem const& other)` overload is the copy constructor the class itself refuses to provide, producing a freshly allocated, independently owned item with the same key and payload. + +## Diagnostics and Alignment + +`CountedObject` contributes a global atomic counter accessible via `CountedObjects::getInstance()`, allowing operators to observe how many `SHAMapItem` objects are alive at any given time — useful for diagnosing memory growth or cache pressure. The `static_assert` at the bottom of the file confirms that `alignof(SHAMapItem)` is exactly 4 or 8 (never, say, 40), which is a prerequisite for the slab allocator's alignment contracts. This assert would catch platform-specific or compiler-version surprises before they silently corrupt memory. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapLeafNode.h.ai.json b/include/xrpl/shamap/SHAMapLeafNode.h.ai.json new file mode 100644 index 0000000000..ecad1a7505 --- /dev/null +++ b/include/xrpl/shamap/SHAMapLeafNode.h.ai.json @@ -0,0 +1,105 @@ +{ + "args": [ + { + "lineno": 10, + "name": "item" + }, + { + "lineno": 10, + "name": "cowid" + }, + { + "lineno": 13, + "name": "hash" + }, + { + "lineno": 32, + "name": "is_root" + }, + { + "lineno": 44, + "name": "i" + } + ], + "classes": [ + { + "args": [ + "boost::intrusive_ptr item", + "std::uint32_t cowid" + ], + "lineno": 8, + "name": "SHAMapLeafNode" + } + ], + "description": "Defines the SHAMapLeafNode class, representing a leaf node in the SHAMap data structure, which holds a pointer to a SHAMapItem and provides methods for item access, mutation, and node property queries.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapLeafNode.h", + "functions": [ + { + "args": [ + "boost::intrusive_ptr item", + "std::uint32_t cowid" + ], + "lineno": 10, + "name": "SHAMapLeafNode" + }, + { + "args": [ + "boost::intrusive_ptr item", + "std::uint32_t cowid", + "SHAMapHash const& hash" + ], + "lineno": 13, + "name": "SHAMapLeafNode" + }, + { + "args": [ + "SHAMapLeafNode const&" + ], + "lineno": 19, + "name": "operator=" + }, + { + "args": [], + "lineno": 22, + "name": "isLeaf" + }, + { + "args": [], + "lineno": 27, + "name": "isInner" + }, + { + "args": [ + "bool is_root = false" + ], + "lineno": 32, + "name": "invariants" + }, + { + "args": [], + "lineno": 37, + "name": "peekItem" + }, + { + "args": [ + "boost::intrusive_ptr i" + ], + "lineno": 44, + "name": "setItem" + }, + { + "args": [ + "SHAMapNodeID const&" + ], + "lineno": 53, + "name": "getString" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapLeafNode.h.ai.md b/include/xrpl/shamap/SHAMapLeafNode.h.ai.md new file mode 100644 index 0000000000..9f002f8ddf --- /dev/null +++ b/include/xrpl/shamap/SHAMapLeafNode.h.ai.md @@ -0,0 +1,47 @@ +# `SHAMapLeafNode` — Abstract Base for SHAMap Leaf Nodes + +## Role in the System + +`SHAMapLeafNode` sits at the boundary between the structural layer of the SHAMap (tree topology and copy-on-write bookkeeping) and the semantic layer (what kind of ledger data a leaf actually holds). It is an intermediate abstract class — inheriting from `SHAMapTreeNode` and providing the shared mechanics of item storage, mutation, and identity queries — while deferring type-specific concerns like hash computation and serialization to three concrete subclasses: `SHAMapAccountStateLeafNode`, `SHAMapTxLeafNode`, and `SHAMapTxPlusMetaLeafNode`. + +Every value in the SHAMap trie is ultimately a leaf node carrying a `SHAMapItem` — the 256-bit keyed, variable-length byte blob that represents either a ledger account state object, a bare transaction, or a transaction bundled with its execution metadata. + +## Ownership and the Copy-on-Write Contract + +The `cowid_` field, inherited from `SHAMapTreeNode`, is the pivot of the COW semantics. A non-zero value signals that this node is exclusively owned by a specific `SHAMap` instance and is therefore safe to mutate. A value of zero means the node is unowned — shareable across multiple map snapshots without copying. + +`setItem()` enforces this invariant at the assertion level: `XRPL_ASSERT(cowid_, ...)` fires if you attempt to mutate a shared node. The caller is responsible for having cloned the node first (via the virtual `clone(cowid)` method defined in each concrete subclass), which produces a fresh owned copy with the new `cowid`. This design prevents accidental mutation of nodes that appear in historical or concurrent map views, which is critical for the ledger's snapshot and diff infrastructure. + +## Item Lifecycle + +`item_` is a `boost::intrusive_ptr` — deliberately `const`-qualified through the pointer. You can swap out which item a leaf points to (that is what `setItem()` does), but you can never modify the item itself in place. This immutability allows the same `SHAMapItem` to be referenced simultaneously from multiple nodes in different map versions without defensive copying. + +Both constructors assert that `item_->size() >= 12`. This minimum-size guard reflects a hard protocol constraint: any well-formed XRPL serialized object is at least 12 bytes long. Accepting shorter data would indicate corruption or a programming error upstream. + +The two-constructor split is intentional. The constructor that omits the hash is used for freshly created nodes; each concrete subclass calls `updateHash()` in its own constructor immediately afterward to compute and store the hash. The three-argument constructor that accepts a pre-computed `SHAMapHash` is used on deserialization paths where the hash is already known and need not be re-derived. + +## Concrete Subclasses and Hash Divergence + +The three concrete leaf types each implement `updateHash()` and `serializeForWire()` / `serializeWithPrefix()` differently, reflecting distinct wire-protocol formats: + +- `SHAMapAccountStateLeafNode` hashes the item payload together with the item's 256-bit key under `HashPrefix::leafNode`. Both key and data are included because account state objects derive their canonical hash from their key. +- `SHAMapTxLeafNode` hashes only the raw item data under `HashPrefix::transactionID`, omitting the key. Transactions are identified by the hash of their content alone. +- `SHAMapTxPlusMetaLeafNode` hashes item data plus key under `HashPrefix::txNode`, since the combined transaction-plus-metadata blob requires the transaction ID as part of its canonical form. + +This divergence is the reason `SHAMapLeafNode` cannot be made concrete: there is no single correct hash formula shared by all leaf types. + +## Public Interface + +`peekItem()` returns a const reference to the stored `intrusive_ptr`, giving callers access to the item's key and data without transferring ownership or bumping the reference count. The name "peek" is a deliberate convention in the XRPL codebase for zero-cost, non-owning access. + +`setItem()` replaces the stored item, calls `updateHash()`, and returns `true` if the hash actually changed or `false` if the new item produces the same hash as the old one. This boolean return allows callers to skip unnecessary dirty-marking or re-indexing when the effective content is unchanged — a practical optimization in ledger state update paths. + +`isLeaf()` and `isInner()` are sealed (`final override`) in `SHAMapLeafNode` itself, even though the class is not final. This is deliberate: the concrete subclasses are all leaf nodes, and overriding these methods further would be nonsensical. Sealing here prevents accidental polymorphic surprises in deep subclass chains. + +`invariants()` asserts that the hash is non-zero and the item pointer is non-null. The `is_root` argument, meaningful for inner nodes (the root of a SHAMap is always an inner node), is silently ignored for leaves. + +## Design Notes + +Copy construction and copy assignment are deleted. The class relies entirely on `clone(cowid)` for duplication, which is more explicit about intent (creating an owned copy for mutation) than a general-purpose copy constructor would be. This prevents silent, expensive copies and keeps ownership semantics visible at the call site. + +The `item_` field is `protected` rather than `private`, giving the concrete subclasses direct access for hashing and serialization without going through `peekItem()`. This trades encapsulation for the ability to write the concrete subclasses as headers-only (inline `updateHash()` and `serialize*()` implementations), which eliminates virtual call overhead in the hot hash-recomputation path. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapMissingNode.h.ai.json b/include/xrpl/shamap/SHAMapMissingNode.h.ai.json new file mode 100644 index 0000000000..d998b8a14e --- /dev/null +++ b/include/xrpl/shamap/SHAMapMissingNode.h.ai.json @@ -0,0 +1,36 @@ +{ + "args": [ + { + "lineno": 16, + "name": "t" + } + ], + "classes": [ + { + "args": [ + "SHAMapType t, SHAMapHash const& hash", + "SHAMapType t, uint256 const& id" + ], + "lineno": 32, + "name": "SHAMapMissingNode" + } + ], + "description": "Defines SHAMapType enum for different SHAMap tree types, provides a utility to convert SHAMapType to string, and defines an exception class for missing SHAMap nodes.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapMissingNode.h", + "functions": [ + { + "args": [ + "t" + ], + "lineno": 15, + "name": "to_string" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapMissingNode.h.ai.md b/include/xrpl/shamap/SHAMapMissingNode.h.ai.md new file mode 100644 index 0000000000..8896c390d8 --- /dev/null +++ b/include/xrpl/shamap/SHAMapMissingNode.h.ai.md @@ -0,0 +1,43 @@ +# `SHAMapMissingNode.h` — Missing-Node Exception and Map-Type Classification + +This small header forms the error-signalling contract for the entire SHAMap subsystem. It defines two things: the `SHAMapType` enum that classifies what kind of Merkle-Patricia tree a `SHAMap` instance represents, and the `SHAMapMissingNode` exception that is thrown whenever tree traversal encounters a node whose data is not locally available. + +## `SHAMapType` — Classifying Trees by Ledger Role + +```cpp +enum class SHAMapType { + TRANSACTION = 1, // A tree of transactions + STATE = 2, // A tree of state nodes + FREE = 3, // A tree not part of a ledger +}; +``` + +Every closed XRPL ledger contains two SHAMaps: a `TRANSACTION` tree holding the set of transactions included in that ledger (with or without metadata depending on the node type), and a `STATE` tree holding the full account-state database at that ledger's close. `FREE` trees are used for ephemeral or ad-hoc purposes — for instance, building a proposed transaction set during consensus — where the tree is not tied to a finalized ledger. + +The numeric values (`1`, `2`, `3`) are meaningful beyond this header: they appear in wire-protocol serialization for sync packets, so they must not be changed. The accompanying `to_string()` helper converts enum values to human-readable labels (`"Transaction Tree"`, `"State Tree"`, `"Free Tree"`) for log output; the `default` branch guards against any future out-of-range integer by falling back to `safe_cast>(t)` rather than silently producing garbage text. + +## `SHAMapMissingNode` — Signalling Incomplete Local State + +```cpp +class SHAMapMissingNode : public std::runtime_error +``` + +`SHAMapMissingNode` is the primary signal that a SHAMap tree traversal failed because a required node is absent from local storage. This is a routine operational condition in a distributed ledger node: since nodes do not necessarily possess every historical ledger in full, a traversal of an older or partially-synced ledger can legally hit a gap. The exception propagates upward through multiple call frames, decoupling the traversal code in `SHAMap.cpp` from the policy decisions about how to handle gaps. + +There are two constructors, each encoding a different point of failure during traversal: + +- `SHAMapMissingNode(SHAMapType t, SHAMapHash const& hash)` — used when the tree descends toward a child whose **hash** is known (stored in the parent inner node) but whose backing data has not been loaded into memory. The `SHAMapHash` is a strong typedef over `uint256` defined in `SHAMapHash.h`, which adds type safety while carrying a SHA-512Half digest identifying the absent node. + +- `SHAMapMissingNode(SHAMapType t, uint256 const& id)` — used when the failure is expressed at the level of an **item key** (a leaf identifier like an account ID or transaction ID) rather than a structural hash. This form appears when a lookup by key ID descends into the tree far enough to know the key should exist but cannot find the leaf node. + +Both constructors build the `std::runtime_error` message eagerly: `"Missing Node: : hash "` or `"Missing Node: : id "`. This is intentional — the `what()` string is the primary data surface available to catch sites, and catch handlers in `LedgerCleaner.cpp`, `LedgerMaster.cpp`, and `RCLConsensus.cpp` all log it directly via `mn.what()`. Constructing the message at throw time rather than on demand is the right tradeoff here because the exception represents an abnormal situation that occurs rarely relative to normal traversal. + +## Usage Pattern at Catch Sites + +The `SHAMapMissingNode` exception surfaces in roughly two modes in the application layer: + +**Recovery**: In `LedgerCleaner.cpp` and `LedgerMaster.cpp`, catching `SHAMapMissingNode` triggers a call into the inbound-ledger acquisition system (`getInboundLedgers().acquire()`), scheduling a peer fetch for the incomplete ledger. The exception message is logged at `warn` level so the operator can observe gaps. + +**Fatal signalling**: In `RCLConsensus.cpp`, catching `SHAMapMissingNode` during consensus timer processing is treated as "should never happen" — the message is logged at `error` level and the exception is re-thrown via `Rethrow()`, ultimately crashing the consensus round. `Ledger.cpp` catches it silently and returns a failure result, treating any incomplete state tree as an invalid ledger rather than attempting recovery. + +This layered catch strategy — enabled by the exception being a named type rather than a generic error code — lets each subsystem apply its own policy without the SHAMap traversal code being aware of those policies. The co-location of `SHAMapType` in this header is natural: the type is part of the exception's identity, passed to every `Throw` call in `SHAMap.cpp` so that log messages always carry the context of which tree (transaction vs. state) had the missing node. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapNodeID.h.ai.json b/include/xrpl/shamap/SHAMapNodeID.h.ai.json new file mode 100644 index 0000000000..13c2928d1c --- /dev/null +++ b/include/xrpl/shamap/SHAMapNodeID.h.ai.json @@ -0,0 +1,163 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "SHAMapNodeID" + } + ], + "description": "Defines the SHAMapNodeID class and related functions for identifying and manipulating nodes within a SHAMap (a specialized Merkle tree used in XRPL). Provides serialization, deserialization, comparison, and utility functions for node IDs.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapNodeID.h", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "SHAMapNodeID" + }, + { + "args": [ + "SHAMapNodeID const& other" + ], + "lineno": 16, + "name": "SHAMapNodeID" + }, + { + "args": [ + "unsigned int depth", + "uint256 const& hash" + ], + "lineno": 17, + "name": "SHAMapNodeID" + }, + { + "args": [ + "SHAMapNodeID const& other" + ], + "lineno": 19, + "name": "operator=" + }, + { + "args": [], + "lineno": 21, + "name": "isRoot" + }, + { + "args": [], + "lineno": 26, + "name": "getRawString" + }, + { + "args": [], + "lineno": 29, + "name": "getDepth" + }, + { + "args": [], + "lineno": 33, + "name": "getNodeID" + }, + { + "args": [ + "unsigned int m" + ], + "lineno": 37, + "name": "getChildNodeID" + }, + { + "args": [ + "int depth", + "uint256 const& key" + ], + "lineno": 45, + "name": "createID" + }, + { + "args": [ + "SHAMapNodeID const& n" + ], + "lineno": 53, + "name": "operator<" + }, + { + "args": [ + "SHAMapNodeID const& n" + ], + "lineno": 58, + "name": "operator>" + }, + { + "args": [ + "SHAMapNodeID const& n" + ], + "lineno": 63, + "name": "operator<=" + }, + { + "args": [ + "SHAMapNodeID const& n" + ], + "lineno": 68, + "name": "operator>=" + }, + { + "args": [ + "SHAMapNodeID const& n" + ], + "lineno": 73, + "name": "operator==" + }, + { + "args": [ + "SHAMapNodeID const& n" + ], + "lineno": 78, + "name": "operator!=" + }, + { + "args": [ + "SHAMapNodeID const& node" + ], + "lineno": 83, + "name": "to_string" + }, + { + "args": [ + "std::ostream& out", + "SHAMapNodeID const& node" + ], + "lineno": 90, + "name": "operator<<" + }, + { + "args": [ + "void const* data", + "std::size_t size" + ], + "lineno": 101, + "name": "deserializeSHAMapNodeID" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 104, + "name": "deserializeSHAMapNodeID" + }, + { + "args": [ + "SHAMapNodeID const& id", + "uint256 const& hash" + ], + "lineno": 111, + "name": "selectBranch" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapNodeID.h.ai.md b/include/xrpl/shamap/SHAMapNodeID.h.ai.md new file mode 100644 index 0000000000..bb5e1b8a5c --- /dev/null +++ b/include/xrpl/shamap/SHAMapNodeID.h.ai.md @@ -0,0 +1,46 @@ +# `SHAMapNodeID` — Node Position Identifier in the SHAMap Radix-Merkle Tree + +## Role in the System + +`SHAMapNodeID` encodes the precise location of a node within a `SHAMap`, which is XRPL's hybrid data structure combining a 16-way radix tree with a Merkle tree. The SHAMap uses 256-bit keys where each nibble (4 bits) selects one of 16 branches at each successive level. A `SHAMapNodeID` answers the question: *which node in the tree are we talking about?* — expressed as a (depth, prefix) pair rather than a raw hash. + +## The Two-Field Identity Model + +Every `SHAMapNodeID` carries exactly two fields: + +- `uint256 id_` — a 256-bit prefix, with only the top `depth_` nibbles populated; the rest are zero. +- `unsigned int depth_` — a level counter from 0 (root) to 64 (leaf level). + +The tree has 65 levels because a 256-bit key is navigated nibble by nibble, and 256 / 4 = 64 branch levels sit below the root. The `branchFactor` of 16 and `leafDepth` of 64 are both defined as `constexpr` in `SHAMap` (which forwards `branchFactor` from `SHAMapInnerNode`). + +The invariant that `id_` is always masked to `depth_` is enforced by the constructor via `XRPL_ASSERT`. This masking is what makes `id_` a *canonical path prefix* rather than an arbitrary 256-bit value — it uniquely and unambiguously identifies one node in the tree, not just any hash. + +## The Depth Mask + +The implementation-private `depthMask()` function (in `SHAMapNodeID.cpp`) builds a static table of 65 `uint256` bitmasks, one per depth. The mask for depth `d` has the top `d` nibbles set to all-ones and the remaining nibbles cleared. Concretely, the first byte holds `0xF0` at depth 1, `0xFF` at depth 2, the second byte adds `0xF0` at depth 3, `0xFF` at depth 4, and so on. + +This pattern is why the constructor checks `id_ == (id_ & depthMask(depth))` — it verifies the id carries no data below its declared depth. Violating this would mean two logically distinct paths could produce the same `SHAMapNodeID`, breaking correctness guarantees. + +## Child Navigation + +`getChildNodeID(unsigned int m)` advances one level deeper by incrementing `depth_` and setting the nibble corresponding to that new depth to branch number `m` (0–15). The bit-manipulation is straightforward: since each byte holds two nibbles, even depths write the high nibble (`m << 4`) and odd depths write the low nibble (`m & 0xf`). An exception is thrown (not just asserted) when called on a leaf-depth node, because constructing a child `SHAMapNodeID` at depth 65 would violate the structural invariant. + +The inverse operation, `selectBranch(SHAMapNodeID const& id, uint256 const& hash)`, is a free function: given a node's depth and a lookup key, it extracts the nibble at that depth from the key to determine which of the 16 branches to follow. This is the traversal primitive — every lookup, insert, or sync operation in `SHAMap` uses `selectBranch` to descend one level at a time. + +## The `createID` Factory + +`createID(int depth, uint256 const& key)` is the correct way to derive a `SHAMapNodeID` from a full leaf key when you need the ancestor at a specific depth. It applies `depthMask(depth)` to the key before constructing the object, discarding the lower nibbles automatically. This is essential during sync operations where you know the target key and the depth at which you want to reference an intermediate node. + +## Wire Format and Deserialization + +The on-wire representation is 33 bytes: 32 bytes of `id_` in big-endian uint256 format followed by a single byte for `depth_`. `getRawString()` produces this via the XRPL `Serializer`. The free function `deserializeSHAMapNodeID()` is the trusted deserialization entry point: it validates that the buffer is exactly 33 bytes, that the depth byte does not exceed 64, and critically, that the decoded `id_` satisfies the depth-mask invariant. Invalid input yields an empty `std::optional` rather than an exception, making it safe to use directly on untrusted peer data. + +## Ordering and Comparisons + +The full set of comparison operators is implemented manually (with a `FIXME-C++20` comment noting that the spaceship operator was not yet adopted). The primary comparator, `operator<`, sorts by `std::tie(depth_, id_)` — shallower nodes sort before deeper ones, and among same-depth nodes, ordering follows the 256-bit prefix value. This ordering is meaningful for `std::map` or `std::set` collections that track node frontiers during tree traversal or sync. + +## Diagnostics Support + +Inheriting from `CountedObject` registers every live instance with a global lock-free counter. This is XRPL's lightweight diagnostic mechanism — a server can query `CountedObjects::getInstance().getCounts()` at runtime to see how many `SHAMapNodeID` objects are alive, useful for detecting leaks during sync operations. + +The `to_string()` and `operator<<` overloads produce a human-readable `"NodeID(depth,hex_id)"` format (or `"NodeID(root)"` for the root), which is what appears in journal log messages throughout the tree traversal code. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapSyncFilter.h.ai.json b/include/xrpl/shamap/SHAMapSyncFilter.h.ai.json new file mode 100644 index 0000000000..fec8cff926 --- /dev/null +++ b/include/xrpl/shamap/SHAMapSyncFilter.h.ai.json @@ -0,0 +1,39 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "SHAMapSyncFilter" + } + ], + "description": "Defines the SHAMapSyncFilter abstract class, which provides a callback interface for filtering SHAMap nodes during synchronization in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapSyncFilter.h", + "functions": [ + { + "args": [ + "fromFilter", + "nodeHash", + "ledgerSeq", + "nodeData", + "type" + ], + "lineno": 18, + "name": "gotNode" + }, + { + "args": [ + "nodeHash" + ], + "lineno": 26, + "name": "getNode" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapSyncFilter.h.ai.md b/include/xrpl/shamap/SHAMapSyncFilter.h.ai.md new file mode 100644 index 0000000000..a53480cd89 --- /dev/null +++ b/include/xrpl/shamap/SHAMapSyncFilter.h.ai.md @@ -0,0 +1,35 @@ +# `SHAMapSyncFilter` — Callback Interface for SHAMap Node Synchronization + +`SHAMapSyncFilter` is a two-method abstract interface that sits at the boundary between the low-level SHAMap tree-traversal engine and the higher-level infrastructure that manages node persistence, caching, and fetch packs. It exists because the SHAMap code needs to be decoupled from knowledge about *where* nodes come from or *where* they should go when received — those concerns belong to the application layer. + +## The Synchronization Protocol + +When a rippled node is downloading a ledger from peers, the SHAMap it is constructing begins sparsely populated. As it walks the tree looking for missing nodes (`getMissingNodes`), it may encounter hashes for which the node data is neither in its in-memory cache nor in its backing database. The filter provides a third source: transient data obtained from peer fetch packs, in-flight consensus caches, or similar ephemeral stores. + +The interface captures this with exactly two pure virtual methods: + +**`getNode(nodeHash)`** — Called when the SHAMap needs a node that couldn't be resolved locally. If the filter has the raw serialized data for this hash, it returns it as an `std::optional`. Returning `std::nullopt` means the filter cannot help; the node is genuinely missing. + +**`gotNode(fromFilter, nodeHash, ledgerSeq, nodeData, type)`** — Called after a node has been successfully obtained, regardless of source. The `fromFilter` flag distinguishes the two cases: `true` when the data originated from this filter's own `getNode()` call, `false` when it arrived from the network (i.e., via `addRootNode` or `addKnownNode`). This distinction matters because a `false` call is the signal for the filter to persist the node somewhere durable; a `true` call would be redundant to re-store. + +The internal `SHAMap::checkFilter()` function makes the two-step contract explicit: it calls `getNode()` to retrieve, deserializes and validates the data, and then immediately calls `gotNode(true, ...)` to notify the filter that the node was consumed. When `addRootNode` or `addKnownNode` receives a fresh node from a peer, they call `gotNode(false, ...)` directly without going through `getNode()`. + +The `nodeData` parameter to `gotNode()` is passed by rvalue reference (`Blob&&`), and the comment "nodeData is overwritten by this call" reflects that the implementation is free to move or destroy the buffer — callers must not rely on the contents afterward. + +## Concrete Implementations + +Three concrete subclasses cover the ledger sync scenarios: + +- **`AccountStateSF`** — used when syncing an account-state SHAMap. It holds references to a `NodeStore::Database` (for durable storage) and an `AbstractFetchPackContainer` (for the transient fetch pack received from a peer). On `gotNode(false, ...)`, it writes to both. On `getNode()`, it checks the fetch pack. + +- **`TransactionStateSF`** — structurally identical to `AccountStateSF` but applied to the transaction tree of a ledger. Both classes are described as "only needed on add functions", meaning they only participate in the write path (`addRootNode` / `addKnownNode`), not in the read path (`getMissingNodes`). + +- **`ConsensusTransSetSF`** — used during consensus to build transaction sets. Unlike the ledger sync filters, this one is "needed on both add and check functions" because the underlying data source is a transient in-memory `TaggedCache` rather than a persistent store. It provides nodes from that cache during `getMissingNodes` and writes newly learned nodes back into it. + +## Design Rationale + +The interface is non-copyable (deleted copy constructor and assignment operator) because its implementations hold non-owning references to databases and caches that have independent lifetimes. Copying would create dangling-reference hazards with no benefit. + +The two-method split (pull vs. notify) avoids requiring filters to implement any tree logic: they operate purely in terms of flat `Blob` data keyed by `SHAMapHash`. The SHAMap engine handles all deserialization, canonicalization, and tree structure; the filter only sees opaque byte buffers. This keeps the filter implementations small and testable in isolation. + +The `ledgerSeq` argument passed to `gotNode()` allows filter implementations to associate stored nodes with a specific ledger sequence number, enabling them to make intelligent decisions about expiry or prioritization without requiring the filter to maintain its own sequence tracking. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapTreeNode.h.ai.json b/include/xrpl/shamap/SHAMapTreeNode.h.ai.json new file mode 100644 index 0000000000..12cb03dfa4 --- /dev/null +++ b/include/xrpl/shamap/SHAMapTreeNode.h.ai.json @@ -0,0 +1,162 @@ +{ + "args": [ + { + "lineno": 41, + "name": "cowid" + }, + { + "lineno": 44, + "name": "hash" + }, + { + "lineno": 103, + "name": "is_root" + }, + { + "lineno": 106, + "name": "rawNode" + }, + { + "lineno": 113, + "name": "hashValid" + }, + { + "lineno": 113, + "name": "data" + } + ], + "classes": [ + { + "args": [ + "SHAMapTreeNode const&", + "std::uint32_t cowid", + "std::uint32_t cowid, SHAMapHash const& hash" + ], + "lineno": 23, + "name": "SHAMapTreeNode" + } + ], + "description": "Defines the SHAMapTreeNode base class and related types for nodes in the SHAMap data structure, including serialization, copy-on-write semantics, and node type identification.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapTreeNode.h", + "functions": [ + { + "args": [], + "lineno": 61, + "name": "cowid" + }, + { + "args": [], + "lineno": 69, + "name": "unshare" + }, + { + "args": [ + "cowid" + ], + "lineno": 75, + "name": "clone" + }, + { + "args": [], + "lineno": 78, + "name": "updateHash" + }, + { + "args": [], + "lineno": 81, + "name": "getHash" + }, + { + "args": [], + "lineno": 86, + "name": "getType" + }, + { + "args": [], + "lineno": 89, + "name": "isLeaf" + }, + { + "args": [], + "lineno": 92, + "name": "isInner" + }, + { + "args": [ + "Serializer&" + ], + "lineno": 95, + "name": "serializeForWire" + }, + { + "args": [ + "Serializer&" + ], + "lineno": 98, + "name": "serializeWithPrefix" + }, + { + "args": [ + "SHAMapNodeID const&" + ], + "lineno": 101, + "name": "getString" + }, + { + "args": [ + "is_root" + ], + "lineno": 103, + "name": "invariants" + }, + { + "args": [ + "Slice rawNode", + "SHAMapHash const& hash" + ], + "lineno": 106, + "name": "makeFromPrefix" + }, + { + "args": [ + "Slice rawNode" + ], + "lineno": 109, + "name": "makeFromWire" + }, + { + "args": [ + "Slice data", + "SHAMapHash const& hash", + "bool hashValid" + ], + "lineno": 113, + "name": "makeTransaction" + }, + { + "args": [ + "Slice data", + "SHAMapHash const& hash", + "bool hashValid" + ], + "lineno": 116, + "name": "makeAccountState" + }, + { + "args": [ + "Slice data", + "SHAMapHash const& hash", + "bool hashValid" + ], + "lineno": 119, + "name": "makeTransactionWithMeta" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapTreeNode.h.ai.md b/include/xrpl/shamap/SHAMapTreeNode.h.ai.md new file mode 100644 index 0000000000..a83a7590a0 --- /dev/null +++ b/include/xrpl/shamap/SHAMapTreeNode.h.ai.md @@ -0,0 +1,42 @@ +# `SHAMapTreeNode.h` — Base Class for SHAMap Tree Nodes + +## Role in the System + +`SHAMapTreeNode` is the polymorphic root of the XRPL ledger's Merkle radix-tree node hierarchy. Every node stored in a `SHAMap` — whether a branching interior node or a data-carrying leaf — derives from this class. The file also anchors the serialization protocol by defining the wire-format type tags and the `SHAMapNodeType` enumeration, making it the single authoritative location for the shape of the tree's on-disk and on-wire representations. + +## Node Type System + +The file establishes two parallel classification schemes for node types. The `SHAMapNodeType` enum (`tnINNER`, `tnTRANSACTION_NM`, `tnTRANSACTION_MD`, `tnACCOUNT_STATE`) is the in-memory identity used for runtime dispatch. The five `wireType*` constants are single-byte identifiers appended to the end of a serialized node payload during peer-to-peer sync. The comment "should not be arbitrarily changed" signals that these constants are part of the XRPL wire protocol and carry backward-compatibility obligations — a change here would break ledger sync across all node versions. + +## Intrusive Reference Counting + +`SHAMapTreeNode` inherits from `IntrusiveRefCounts`, which packs both strong and weak reference counts plus two lifecycle-state bits into a single 32-bit atomic integer. The design trades a degree of readability for tight storage: 16 bits of strong count, 14 bits of weak count, and 2 flag bits indicating whether the `partialDestructor` has started and finished. This matters because SHAMap nodes are extremely numerous (a full ledger tree can contain millions of nodes) and live across thread boundaries, so minimizing per-node overhead is a primary concern. + +The `partialDestructor()` virtual method (overridden non-trivially only in `SHAMapInnerNode`) exists specifically to support weak intrusive pointers to inner nodes. When the last strong reference drops while weak references still exist, the partial destructor tears down the child pointers stored in the node — releasing child strong references — without freeing the memory, leaving the memory intact for weak pointer resolution until the last weak reference also drops. + +## Copy-on-Write Semantics + +The `cowid_` field (copy-on-write identifier) encodes which `SHAMap` instance "owns" a given node and is central to how the tree achieves both memory efficiency and mutation safety. A zero `cowid_` means the node is clean, unmodified, and eligible for sharing across multiple `SHAMap` instances simultaneously — for example, when taking a snapshot of the ledger state for parallel transaction processing. + +When a `SHAMap` needs to mutate a node that it does not own (i.e., `cowid_` differs from the map's own ID), it calls `clone(cowid)` to produce a private copy marked with the new owner's ID. The original shared node is left intact and continues to be part of other maps. The `unshare()` method resets `cowid_` to zero, making a previously exclusive node available for sharing again — this is called when a node is flushed to the database, after which it becomes immutable. + +Copy and assignment are deleted on `SHAMapTreeNode` itself, enforcing that duplication only ever happens through the controlled `clone()` factory, never by accident. + +## Dual Serialization Contract + +The class exposes two pure-virtual serialization methods that differ not in structure but in context and invariants: + +- `serializeForWire(Serializer&)` — appends the single-byte `wireType*` tag at the end. This format is used during peer-to-peer sync. The type byte is at the end, not the beginning, which means the receiver must read the entire payload before identifying the node type. +- `serializeWithPrefix(Serializer&)` — prepends a 4-byte `HashPrefix` constant before the node data. This format is used for hashing (and for database storage). The prefix allows the receiver to identify the type from the first four bytes. + +These two paths are matched by the two static factory constructors: `makeFromWire(Slice)` reads the last byte to dispatch to the appropriate private factory, while `makeFromPrefix(Slice, SHAMapHash)` reads the first four bytes as a `HashPrefix` enum and also accepts a pre-validated hash. The `hashValid` boolean propagated through the private `makeTransaction`, `makeAccountState`, and `makeTransactionWithMeta` helpers controls whether the known hash is passed directly to the concrete leaf node constructor or left to be computed later via `updateHash()`. + +## Concrete Subclass Topology + +`SHAMapTreeNode` is instantiated through three concrete leaf types — `SHAMapTxLeafNode` (bare transactions), `SHAMapTxPlusMetaLeafNode` (transactions with metadata), and `SHAMapAccountStateLeafNode` (account objects) — all of which pass through `SHAMapLeafNode` as an intermediate base that holds the `boost::intrusive_ptr` payload. `SHAMapInnerNode` is the branching node, holding a 16-way (`branchFactor = 16`) radix of child pointers stored in a compressed `TaggedPointer`-based representation. + +The three private static factories are `private` to `SHAMapTreeNode` even though the instantiated types are defined externally because they encapsulate the decision logic for which concrete subclass to allocate — centrally in one file, keeping the public API clean and ensuring that only `makeFromWire` and `makeFromPrefix` are the valid entry points from outside the class. + +## Invariant Checking + +The pure-virtual `invariants(bool is_root)` method is a debug-mode consistency checker threaded through the entire class hierarchy. Each concrete node is responsible for asserting its structural preconditions, and the `is_root` parameter lets the root node relax the constraint that a node must have at least two children (a single-child inner node at the root is valid for a tree holding exactly one leaf). This pattern is a deliberate extension hook: new concrete node types are forced to implement `invariants()` by the compiler, ensuring correctness checks are never accidentally omitted. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapTxLeafNode.h.ai.json b/include/xrpl/shamap/SHAMapTxLeafNode.h.ai.json new file mode 100644 index 0000000000..13f3b015ac --- /dev/null +++ b/include/xrpl/shamap/SHAMapTxLeafNode.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 12, + "name": "item" + }, + { + "lineno": 12, + "name": "cowid" + }, + { + "lineno": 17, + "name": "item" + }, + { + "lineno": 17, + "name": "cowid" + }, + { + "lineno": 18, + "name": "hash" + }, + { + "lineno": 23, + "name": "cowid" + }, + { + "lineno": 40, + "name": "s" + }, + { + "lineno": 46, + "name": "s" + } + ], + "classes": [ + { + "args": [ + "boost::intrusive_ptr item, std::uint32_t cowid", + "boost::intrusive_ptr item, std::uint32_t cowid, SHAMapHash const& hash" + ], + "lineno": 9, + "name": "SHAMapTxLeafNode" + } + ], + "description": "Defines the SHAMapTxLeafNode class, representing a leaf node in a SHAMap for transactions (without metadata) in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapTxLeafNode.h", + "functions": [ + { + "args": [ + "cowid" + ], + "lineno": 22, + "name": "clone" + }, + { + "args": [], + "lineno": 28, + "name": "getType" + }, + { + "args": [], + "lineno": 33, + "name": "updateHash" + }, + { + "args": [ + "s" + ], + "lineno": 39, + "name": "serializeForWire" + }, + { + "args": [ + "s" + ], + "lineno": 45, + "name": "serializeWithPrefix" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapTxLeafNode.h.ai.md b/include/xrpl/shamap/SHAMapTxLeafNode.h.ai.md new file mode 100644 index 0000000000..0e78aaacfa --- /dev/null +++ b/include/xrpl/shamap/SHAMapTxLeafNode.h.ai.md @@ -0,0 +1,38 @@ +# `SHAMapTxLeafNode` — SHAMap Leaf Node for Bare Transactions + +## Role in the System + +`SHAMapTxLeafNode` represents a leaf node in a `SHAMap` that stores a raw transaction **without** associated metadata. It is one of three concrete leaf types in the SHAMap node hierarchy, alongside `SHAMapTxPlusMetaLeafNode` (transaction + metadata) and `SHAMapAccountStateLeafNode` (ledger state). These three types cover every data element a SHAMap can hold, with the distinction between them being semantically significant: the transaction-only variant is used when building the transaction tree of an open or proposed ledger, before execution metadata is available. After a ledger closes and transactions are applied, the `SHAMapTxPlusMetaLeafNode` form is used instead. + +## Class Hierarchy and Design + +`SHAMapTxLeafNode` extends both `SHAMapLeafNode` and `CountedObject`. The `SHAMapLeafNode` base provides the shared `item_` field (an immutable, reference-counted `SHAMapItem`) and `SHAMapTreeNode` sits above that with the `hash_` and `cowid_` fields. The `CountedObject` mixin enables global tracking of live instances for memory diagnostics — a pattern shared across all three leaf node types and the inner node as well. + +The class is declared `final`, which matches all three concrete leaf types. Since the entire polymorphism needed by `SHAMap` is captured in `SHAMapTreeNode`'s virtual interface, nothing needs to further subclass `SHAMapTxLeafNode`. + +## Hashing: The Critical Distinction + +The most architecturally significant aspect of this class versus its siblings is `updateHash()`. The hash is computed as: + +```cpp +hash_ = SHAMapHash{sha512Half(HashPrefix::transactionID, item_->slice())}; +``` + +Two details matter here. First, the hash prefix `HashPrefix::transactionID` (the 4-byte big-endian encoding of `'T','X','N'`) is prepended before the raw transaction bytes are fed to SHA-512-half. This prefix-before-hash approach is a deliberate domain-separation technique used throughout XRPL to ensure hashes of one type of object cannot collide with hashes of another type, even if the underlying byte content were identical. Second — and this is the key contrast with `SHAMapTxPlusMetaLeafNode` and `SHAMapAccountStateLeafNode` — the item's **key** is **not** included in the hash input. Those types pass both `item_->slice()` and `item_->key()` to `sha512Half`, but here only the raw blob is hashed. This reflects the fact that for a bare transaction, the transaction ID (key) is itself derived from the transaction's content, so it would be redundant; the hash fully characterises the content without it. + +## Copy-on-Write Support + +The two-constructor design is a direct consequence of the SHAMap's copy-on-write (CoW) semantics. When a `SHAMapTxLeafNode` is first built from raw item data, the single-`cowid` constructor is used, which calls `updateHash()` to compute the hash from scratch. The two-`cowid`+`hash` constructor is used by `clone()` — it accepts a pre-computed `hash_` from the original node and skips recomputation, which is correct because `clone()` only changes the ownership (`cowid`) while the underlying `item_` and its hash remain unchanged. Every node in the SHAMap hierarchy follows this same pattern. + +## Serialization + +Two serialization paths exist for every node type: + +- `serializeForWire()` emits the raw item bytes followed by the constant `wireTypeTransaction` (`0`). This wire-type byte is the framing marker that `SHAMapTreeNode::makeFromWire()` uses to reconstruct the correct concrete node type on deserialization, routing to `makeTransaction()` in the factory. +- `serializeWithPrefix()` writes `HashPrefix::transactionID` followed by the raw item bytes, producing exactly the preimage used in `updateHash()`. This path supports rehashing from stored node data and canonical serialization for Merkle proof purposes. + +Notably, neither path includes the item key. This contrasts with `SHAMapTxPlusMetaLeafNode::serializeForWire()`, which appends `item_->key()` followed by `wireTypeTransactionWithMeta` (`4`), reflecting that once metadata is attached the key carries additional information needed for reconstruction. + +## Relationship to Sibling Types + +All three leaf node types are structurally near-identical, differing only in hash prefix, wire type constant, and whether the key participates in hashing and wire serialization. The design deliberately pushes every type-specific behavior into these small final overrides rather than using runtime branching inside the base class. This keeps the base class clean, avoids `switch` statements on `SHAMapNodeType`, and allows the compiler to inline all the critical paths in `updateHash()` and `serialize*()` when working with concrete types. The `SHAMapNodeType::tnTRANSACTION_NM` enum value (`2`, "no metadata") serves as the identity marker for this type across the rest of the system, distinguishing it from `tnTRANSACTION_MD` (`3`) at the point where SHAMap tree contents are interpreted. \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h.ai.json b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h.ai.json new file mode 100644 index 0000000000..655415a401 --- /dev/null +++ b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h.ai.json @@ -0,0 +1,55 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "boost::intrusive_ptr item, std::uint32_t cowid", + "boost::intrusive_ptr item, std::uint32_t cowid, SHAMapHash const& hash" + ], + "lineno": 10, + "name": "SHAMapTxPlusMetaLeafNode" + } + ], + "description": "Defines the SHAMapTxPlusMetaLeafNode class, representing a SHAMap leaf node for a transaction and its associated metadata, including methods for cloning, hashing, and serialization.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h", + "functions": [ + { + "args": [ + "cowid" + ], + "lineno": 23, + "name": "clone" + }, + { + "args": [], + "lineno": 29, + "name": "getType" + }, + { + "args": [], + "lineno": 34, + "name": "updateHash" + }, + { + "args": [ + "s" + ], + "lineno": 40, + "name": "serializeForWire" + }, + { + "args": [ + "s" + ], + "lineno": 46, + "name": "serializeWithPrefix" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h.ai.md b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h.ai.md new file mode 100644 index 0000000000..792723c879 --- /dev/null +++ b/include/xrpl/shamap/SHAMapTxPlusMetaLeafNode.h.ai.md @@ -0,0 +1,51 @@ +# `SHAMapTxPlusMetaLeafNode` + +`SHAMapTxPlusMetaLeafNode` is one of three concrete leaf node types in the XRPL's SHAMap Merkle tree structure. It represents a transaction paired with its execution metadata — the canonical form of a transaction entry in a validated ledger's transaction map. Its counterpart, `SHAMapTxLeafNode`, holds transactions without metadata (used in open/proposed ledgers where execution has not yet occurred), while `SHAMapAccountStateLeafNode` covers ledger state entries. + +## Role in the SHAMap Hierarchy + +The class sits at the bottom of a three-level inheritance chain: `SHAMapTreeNode` → `SHAMapLeafNode` → `SHAMapTxPlusMetaLeafNode`. The base `SHAMapTreeNode` owns the `hash_` and `cowid_` fields and declares the copy-on-write interface; `SHAMapLeafNode` adds the `item_` pointer to the underlying `SHAMapItem` payload; this class provides the type-specific behavior for hashing and serialization. + +The node is tagged `final`, so no further subclassing is permitted. It also inherits from `CountedObject`, which wires it into the global object telemetry system for tracking live instance counts — useful for diagnosing memory pressure in production. + +## Two Constructors, One Design Reason + +The class exposes two constructors intentionally. The single-hash-free constructor (taking only `item` and `cowid`) is used when building a new node from scratch: it calls `updateHash()` immediately after delegating to `SHAMapLeafNode`, so the node is always in a consistent hashed state. The second constructor (taking `item`, `cowid`, and an explicit `SHAMapHash`) is used when reconstituting a node from cached or network-received data where the hash is already known — it bypasses the recomputation and sets `hash_` directly via the base-class overload. This avoids the cost of redundant hashing during deserialization and is a common pattern across all three leaf node types. + +## Hashing: Domain Separation is Critical + +The `updateHash()` method is the most semantically significant part of the class: + +```cpp +hash_ = SHAMapHash{sha512Half(HashPrefix::txNode, item_->slice(), item_->key())}; +``` + +Three details matter here. First, the prefix `HashPrefix::txNode` encodes the bytes `'S'`, `'N'`, `'D'` — this is distinct from `HashPrefix::transactionID` (`'T'`, `'X'`, `'N'`) used by `SHAMapTxLeafNode` and `HashPrefix::leafNode` used by `SHAMapAccountStateLeafNode`. Hashing a prefix into every digest is the XRPL's standard defense against cross-context hash collisions: the same payload fed to different node types cannot produce the same hash, preventing forgery or confusion during Merkle proof verification. + +Second, the key — the transaction's 256-bit identifier — is included in the hash input alongside the raw data slice. The metadata-free `SHAMapTxLeafNode` omits the key from its hash. This means the same transaction with and without metadata produces different hashes even under the same prefix, which is correct because the two node types occupy different tree contexts. + +Third, this is marked `final override` even though the base class only requires `override`. This signals a deliberate design choice: the hashing algorithm for this node type is fixed and must not be overridden further. + +## Serialization: Wire vs. Prefix Formats + +Two serialization paths exist for fundamentally different purposes. + +`serializeForWire` encodes the node for peer-to-peer transmission during SHAMap sync. It emits the raw payload slice, then the 32-byte key via `addBitString`, then the single-byte type identifier `wireTypeTransactionWithMeta` (value `4`). The type byte at the end allows the receiver's `SHAMapTreeNode::makeFromWire` to reconstruct the correct concrete type via the static factory. Compare this to `SHAMapTxLeafNode`, which omits the key from the wire format entirely and uses `wireTypeTransaction` (value `0`) — an important wire-protocol distinction. + +`serializeWithPrefix` encodes the node for hashing, mirroring exactly what `updateHash()` computes. It prepends `HashPrefix::txNode` as a 32-bit big-endian value, then the slice and the key. This format is used to reconstruct and verify a hash from raw data without instantiating a full node — typically during Merkle proof validation. The agreement between `updateHash()` and `serializeWithPrefix` is an invariant the system relies on; any drift between them would silently corrupt hash verification. + +## Copy-on-Write via `clone()` + +```cpp +intr_ptr::SharedPtr +clone(std::uint32_t cowid) const override +{ + return intr_ptr::make_shared(item_, cowid, hash_); +} +``` + +This implements the copy-on-write protocol inherited from `SHAMapTreeNode`. When a SHAMap needs to modify a node it shares with another map instance (e.g., during ledger forking or state snapshots), it first calls `clone()` with its own `cowid`, producing a privately owned copy without recomputing the hash. The `item_` pointer is shared via `boost::intrusive_ptr` reference counting — the underlying payload is never deep-copied unless the item itself changes, keeping fork overhead minimal. + +## Relationship to the Broader Transaction Model + +In XRPL's ledger lifecycle, an open ledger's transaction map holds transactions as `tnTRANSACTION_NM` nodes (`SHAMapTxLeafNode`). Once a ledger closes and transactions are applied to the state, the resulting validated ledger's transaction map is rebuilt with `tnTRANSACTION_MD` nodes (`SHAMapTxPlusMetaLeafNode`), each pairing the original transaction with the `TxMeta` blob describing exactly what the transaction did. The node type distinction allows the same SHAMap infrastructure to serve both open and closed ledger contexts without ambiguity, with hash domain separation ensuring Merkle roots from the two contexts are structurally incompatible. \ No newline at end of file diff --git a/include/xrpl/shamap/TreeNodeCache.h.ai.json b/include/xrpl/shamap/TreeNodeCache.h.ai.json new file mode 100644 index 0000000000..0215ec62c8 --- /dev/null +++ b/include/xrpl/shamap/TreeNodeCache.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines a type alias TreeNodeCache as a TaggedCache for SHAMapTreeNode objects keyed by uint256, within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/TreeNodeCache.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/TreeNodeCache.h.ai.md b/include/xrpl/shamap/TreeNodeCache.h.ai.md new file mode 100644 index 0000000000..e042d7959d --- /dev/null +++ b/include/xrpl/shamap/TreeNodeCache.h.ai.md @@ -0,0 +1,34 @@ +# `TreeNodeCache.h` — SHAMap Tree Node Cache Type Alias + +## Role in the System + +`TreeNodeCache.h` defines a single type alias that gives a name to the in-memory cache of `SHAMapTreeNode` objects used throughout the XRP Ledger's Merkle-tree implementation. Every ledger's account state and transaction tree is a SHAMap, and every inner or leaf node in that tree is a `SHAMapTreeNode`. Traversing, verifying, or modifying a ledger requires fetching these nodes repeatedly; the `TreeNodeCache` is the hot layer that sits in front of persistent node storage, keyed by each node's `uint256` content hash. + +## The Type Alias + +```cpp +using TreeNodeCache = TaggedCache< + uint256, + SHAMapTreeNode, + /*IsKeyCache*/ false, + intr_ptr::SharedWeakUnionPtr, + intr_ptr::SharedPtr>; +``` + +`TaggedCache` is a map/cache hybrid: entries are keyed by hash, held by strong reference while they are "hot," and demoted to weak references as they age out. So long as anything external holds a strong reference, the entry survives in the map and any subsequent lookup returns the same canonical object — this deduplication is the core purpose of `canonicalize()` on the underlying cache. + +## Why Intrusive Pointers Instead of `std::shared_ptr` + +The default `TaggedCache` template uses `SharedWeakCachePointer` (backed by `std::shared_ptr`) for its internal map entries. `TreeNodeCache` deliberately overrides this with `intr_ptr::SharedWeakUnionPtr` and `intr_ptr::SharedPtr`. The distinction matters for two reasons: + +**Memory reclamation.** With `std::make_shared`, the control block and the object are co-allocated; even after the last strong reference drops and the destructor runs, the memory block cannot be freed until all weak references expire. `SHAMapInnerNode` children — potentially 16 child pointers — would linger in that block. The intrusive model stores ref counts directly inside the `SHAMapTreeNode` itself (via `IntrusiveRefCounts`), and `SHAMapTreeNode::partialDestructor()` is called the moment the strong count hits zero, releasing the expensive parts of the object while weak references (held by the map) are still live. Memory is reclaimed far sooner. + +**Single-word strong/weak duality.** `SharedWeakUnion` stores either a strong or a weak intrusive reference in one pointer-sized word, using the low-order bit as a tag (alignment guarantees this bit is always zero in a real pointer). When the cache sweeper demotes a hot entry to a tracking-only entry, it calls `convertToWeak()` in-place, flipping one bit rather than replacing a whole `shared_ptr`/`weak_ptr` pair. This is both compact and fast. + +## Consumers + +The `Family` interface (in `Family.h`) exposes `getTreeNodeCache()`, returning a `shared_ptr`. The concrete `NodeFamily` implementation holds one instance per application, shared across all live SHAMaps. Because multiple concurrent `SHAMap` instances can reference the same canonical nodes, the canonicalization logic in `TaggedCache` ensures that identical on-disk nodes are represented by a single in-memory object — which is essential for SHAMap's copy-on-write scheme, where unmodified nodes are shared freely between ledger generations. + +## Summary + +The file is intentionally minimal: it is purely a named instantiation point. Its value lies in binding together the right pointer machinery — intrusive, weak-capable, single-word-union pointers — with `TaggedCache`'s two-level strong/weak cache policy, producing an efficient and memory-safe cache for the most heavily accessed data structure in the XRP Ledger runtime. \ No newline at end of file diff --git a/include/xrpl/shamap/detail/TaggedPointer.h.ai.json b/include/xrpl/shamap/detail/TaggedPointer.h.ai.json new file mode 100644 index 0000000000..847fd55cbe --- /dev/null +++ b/include/xrpl/shamap/detail/TaggedPointer.h.ai.json @@ -0,0 +1,165 @@ +{ + "args": [ + { + "lineno": 41, + "name": "RawAllocateTag" + }, + { + "lineno": 44, + "name": "numChildren" + }, + { + "lineno": 56, + "name": "other" + }, + { + "lineno": 57, + "name": "isBranch" + }, + { + "lineno": 58, + "name": "toAllocate" + }, + { + "lineno": 68, + "name": "other" + }, + { + "lineno": 69, + "name": "srcBranches" + }, + { + "lineno": 70, + "name": "dstBranches" + }, + { + "lineno": 71, + "name": "toAllocate" + }, + { + "lineno": 136, + "name": "isBranch" + }, + { + "lineno": 137, + "name": "f" + }, + { + "lineno": 148, + "name": "isBranch" + }, + { + "lineno": 149, + "name": "f" + }, + { + "lineno": 161, + "name": "isBranch" + }, + { + "lineno": 161, + "name": "i" + }, + { + "lineno": 167, + "name": "a" + } + ], + "classes": [ + { + "args": [ + "RawAllocateTag, std::uint8_t numChildren", + "std::uint8_t numChildren", + "TaggedPointer&& other, std::uint16_t isBranch, std::uint8_t toAllocate", + "TaggedPointer&& other, std::uint16_t srcBranches, std::uint16_t dstBranches, std::uint8_t toAllocate", + "TaggedPointer(TaggedPointer const&)", + "TaggedPointer(TaggedPointer&&)", + "operator=(TaggedPointer&&)" + ], + "lineno": 19, + "name": "TaggedPointer" + }, + { + "args": [], + "lineno": 41, + "name": "RawAllocateTag" + } + ], + "description": "Implements the TaggedPointer class, which efficiently stores a pointer and a small tag in the low bits to manage sparse arrays of SHAMapHash and SHAMapTreeNode pointers for memory-efficient SHAMap inner nodes.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/detail/TaggedPointer.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "destroyHashesAndChildren" + }, + { + "args": [], + "lineno": 97, + "name": "decode" + }, + { + "args": [], + "lineno": 102, + "name": "capacity" + }, + { + "args": [], + "lineno": 108, + "name": "isDense" + }, + { + "args": [], + "lineno": 116, + "name": "getHashesAndChildren" + }, + { + "args": [], + "lineno": 122, + "name": "getHashes" + }, + { + "args": [], + "lineno": 127, + "name": "getChildren" + }, + { + "args": [ + "isBranch", + "f" + ], + "lineno": 134, + "name": "iterChildren" + }, + { + "args": [ + "isBranch", + "f" + ], + "lineno": 146, + "name": "iterNonEmptyChildIndexes" + }, + { + "args": [ + "isBranch", + "i" + ], + "lineno": 159, + "name": "getChildIndex" + }, + { + "args": [ + "a" + ], + "lineno": 166, + "name": "popcnt16" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/detail/TaggedPointer.h.ai.md b/include/xrpl/shamap/detail/TaggedPointer.h.ai.md new file mode 100644 index 0000000000..33cb7958e7 --- /dev/null +++ b/include/xrpl/shamap/detail/TaggedPointer.h.ai.md @@ -0,0 +1,68 @@ +# `TaggedPointer.h` — Sparse Child-Array Manager for SHAMap Inner Nodes + +## Why This File Exists + +A `SHAMapInnerNode` is a radix-trie node with up to 16 children (the `branchFactor`). In a dense representation, every inner node always allocates arrays of exactly 16 `SHAMapHash` values and 16 `SharedPtr` smart pointers — even when most slots are empty. Because a live XRP Ledger contains millions of these nodes, and empirical measurements show that most inner nodes hold only a handful of children, that dense layout is extremely wasteful. According to the class comment, the sparse representation cuts average inner-node memory to roughly 25% of the naive allocation. + +`TaggedPointer` is the mechanism that makes this possible. It owns a single heap allocation large enough to hold a packed, contiguous pair of arrays — `SHAMapHash[]` immediately followed by `SharedPtr[]` — and encodes the size class of that allocation in the two lowest bits of the pointer itself. Callers never see raw pointers or raw sizes; they ask the tagged pointer for its arrays. + +## The Pointer-Tag Trick + +All modern allocators guarantee that the natural alignment of objects is a power of two. `SHAMapHash` is asserted to have an alignment of at least 4, which means the raw address returned by `malloc` / `pool::malloc` always has its two low bits set to zero. Those two bits are reclaimed as a 2-bit **tag** field. The single `uintptr_t` member `tp_` is therefore both a pointer and a small integer simultaneously: + +``` +tp_ = reinterpret_cast(rawPtr) | tag; +``` + +`decode()` separates them with a pair of masks (`tagMask = 0x3`, `ptrMask = ~tagMask`). `getHashesAndChildren()` calls `decode()`, casts the pointer bits back to `SHAMapHash*`, and computes the `SharedPtr` array start as `hashes + numAllocated`. The two arrays live in one contiguous allocation with no gap or padding between them. + +A moved-from `TaggedPointer` leaves `tp_ = 0`, which `destroyHashesAndChildren()` treats as a sentinel meaning "nothing to free." + +## Size Classes and Pool Allocation + +The tag indexes into a compile-time array defined in `TaggedPointer.ipp`: + +```cpp +constexpr std::array boundaries{2, 4, 6, 16}; +``` + +Two bits yield exactly four values (tags 0–3), so the design is complete by construction. Tag 3 (`boundaries[3] == 16`) is the **dense** case — all 16 slots exist; `isDense()` checks for this. Tags 0–2 are **sparse** cases with 2, 4, or 6 slots, sized by rounding the requested child count up via `std::lower_bound` on `boundaries`. Because the tag is constrained to `boundaries.size() - 1`, the `static_assert` in the `.ipp` file enforces that nobody enlarges the `boundaries` array past 4 entries without reconsidering the 2-bit encoding. + +Each of the four size classes is backed by its own `boost::singleton_pool`, with 512 KB blocks and a `std::mutex` for thread safety. This avoids the fragmentation and overhead of general-purpose `new`/`delete` for these hot, fixed-size allocations. The pools for allocating and deallocating are captured as `std::function` arrays (`allocateArrayFuns`, `freeArrayFuns`, `isFromArrayFuns`) indexed by tag value, keeping dispatch O(1) and branch-free. + +## Sparse vs. Dense Layouts + +In the **dense** layout (tag 3), array index equals child branch number directly — child 7 is always at array index 7. In a **sparse** layout, only the non-empty children are stored, packed together in branch-number order. The owning `SHAMapInnerNode` maintains a separate `uint16_t isBranch_` bitset where bit `i` is set when branch `i` is non-empty. + +Translating a branch number to a sparse array index is the job of `getChildIndex(isBranch, i)`: + +```cpp +auto const mask = (1u << i) - 1; +return popcnt16(isBranch & mask); +``` + +This is a single popcount on the `isBranch` bits below position `i`. It is both correct and fast — the array position of child `i` is exactly the count of non-empty children that precede it. + +The two iteration helpers `iterChildren` and `iterNonEmptyChildIndexes` branch once on `isDense()` and then loop. For the dense case the callback receives a direct array index; for the sparse case the callback receives both the logical branch number and the compact array index, which the caller needs to index into the hash/children arrays. + +## Constructor Hierarchy and the `RawAllocateTag` Pattern + +`TaggedPointer` is move-only (copy is deleted). Three public constructors cover the common lifecycle events: + +- **`TaggedPointer(uint8_t numChildren)`** — normal creation. Rounds up `numChildren` to the nearest boundary, allocates, and runs default-constructors on all slots. Used when a new inner node is born. + +- **`TaggedPointer(TaggedPointer&& other, uint16_t isBranch, uint8_t toAllocate)`** — resize. Used by `SHAMapInnerNode::resizeChildArrays()` when a child is added or removed and the array needs to grow or shrink. It moves the old `TaggedPointer` in and, if the requested size class is the same, returns immediately. Otherwise it allocates a new block, copies/moves entries using `iterNonEmptyChildIndexes`, then runs default constructors on the remaining slots. + +- **`TaggedPointer(TaggedPointer&& other, uint16_t srcBranches, uint16_t dstBranches, uint8_t toAllocate)`** — the most general form. Handles simultaneous resize and set-difference on the branch bitsets (e.g., when an element is removed and the representation is compacted). When the old and new size classes match it operates **in-place**, shifting elements left or right inside the existing allocation. When they differ it allocates a new block and copies elements across with placement-new. + +The private **`TaggedPointer(RawAllocateTag, uint8_t numChildren)`** constructor allocates the chunk from the pool and encodes the tag but deliberately skips running constructors on the `SHAMapHash` and `SharedPtr` elements. This is safe only because every callsite immediately follows it with placement-new loops. The `RawAllocateTag` tag type is an empty struct used solely to select this overload, making the intent visible and preventing accidental misuse. The destructor always calls `destroyHashesAndChildren()`, which calls the explicit destructors for all `numAllocated` elements, so if the constructors are never run the destructor would be UB — hence the strict encapsulation. + +## The `popcnt16` Free Function + +Defined at file scope, `popcnt16` counts the set bits of a 16-bit value. It dispatches to `std::popcount` (C++20), `__builtin_popcount` (GCC/Clang), or a compile-time-generated lookup table as a fallback. It appears in `getChildIndex` and in `SHAMapInnerNode::getBranchCount()` (which calls `popcnt16(isBranch_)`) — both hot paths during trie traversal. + +## Relationship to `SHAMapInnerNode` + +`SHAMapInnerNode` holds a single `TaggedPointer hashesAndChildren_` member. Every public operation on children delegates to it, passing `isBranch_` as context. The `isBranch_` bitset is separate from `TaggedPointer` by design: the tagged pointer owns memory, not logical state. This separation keeps the two concerns orthogonal and allows the bitset to be read atomically (behind the node's `lock_` spinlock) without touching the allocation. + +The `.ipp` file rather than a `.cpp` file is used for the implementation because the two template methods (`iterChildren`, `iterNonEmptyChildIndexes`) must be instantiated in `SHAMapInnerNode.cpp`, which `#include`s the `.ipp` directly. This avoids explicit instantiation declarations while keeping the template bodies out of the header. \ No newline at end of file diff --git a/include/xrpl/shamap/detail/TaggedPointer.ipp.ai.json b/include/xrpl/shamap/detail/TaggedPointer.ipp.ai.json new file mode 100644 index 0000000000..34bdcadfd4 --- /dev/null +++ b/include/xrpl/shamap/detail/TaggedPointer.ipp.ai.json @@ -0,0 +1,495 @@ +{ + "args": [ + { + "lineno": 10, + "name": "boundaries" + }, + { + "lineno": 18, + "name": "elementSizeBytes" + }, + { + "lineno": 20, + "name": "blockSizeBytes" + }, + { + "lineno": 28, + "name": "arrayChunkSizeBytes" + }, + { + "lineno": 36, + "name": "chunksPerBlock" + }, + { + "lineno": 60, + "name": "allocateArrayFuns" + }, + { + "lineno": 70, + "name": "freeArrayFuns" + }, + { + "lineno": 80, + "name": "isFromArrayFuns" + }, + { + "lineno": 94, + "name": "zeroSHAMapHash" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "numAllocatedChildren" + ], + "entry_point": "numAllocatedChildren", + "purpose": "Determines the number of allocated children slots for a given n (number of children).", + "validation_points": [ + "XRPL_ASSERT(n <= SHAMapInnerNode::branchFactor) in numAllocatedChildren" + ] + }, + { + "call_chain": [ + "boundariesIndex" + ], + "entry_point": "boundariesIndex", + "purpose": "Finds the index in the boundaries array for a given number of children.", + "validation_points": [ + "XRPL_ASSERT(numChildren <= SHAMapInnerNode::branchFactor) in boundariesIndex" + ] + }, + { + "call_chain": [ + "static_assert(boundaries.size() <= 4)", + "static_assert(boundaries.back() == SHAMapInnerNode::branchFactor)" + ], + "entry_point": "static initialization", + "purpose": "Ensures at compile time that the boundaries array is valid for the tagged pointer format.", + "validation_points": [ + "static_assert(boundaries.size() <= 4)", + "static_assert(boundaries.back() == SHAMapInnerNode::branchFactor)" + ] + }, + { + "call_chain": [ + "initArrayChunkSizeBytes" + ], + "entry_point": "initArrayChunkSizeBytes", + "purpose": "Initializes array of chunk sizes for each boundary.", + "validation_points": [] + }, + { + "call_chain": [ + "initArrayChunksPerBlock" + ], + "entry_point": "initArrayChunksPerBlock", + "purpose": "Initializes array of chunk counts per block for each boundary.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "n (number of children)", + "flow": [ + "Input to numAllocatedChildren", + "XRPL_ASSERT validation", + "std::lower_bound(boundaries.begin(), boundaries.end(), n)", + "Return value (number of allocated children slots)" + ], + "origin": "Input parameter to numAllocatedChildren", + "transformations": [ + "Validated for upper bound", + "Mapped to nearest boundary value" + ], + "validated_at": "XRPL_ASSERT in numAllocatedChildren" + }, + { + "field": "numChildren", + "flow": [ + "Input to boundariesIndex", + "XRPL_ASSERT validation", + "std::lower_bound(boundaries.begin(), boundaries.end(), numChildren)", + "std::distance to get index", + "Return value (index in boundaries array)" + ], + "origin": "Input parameter to boundariesIndex", + "transformations": [ + "Validated for upper bound", + "Mapped to index in boundaries" + ], + "validated_at": "XRPL_ASSERT in boundariesIndex" + }, + { + "field": "boundaries", + "flow": [ + "Defined at file scope", + "Used in static_asserts for compile-time validation", + "Used in numAllocatedChildren and boundariesIndex for runtime logic", + "Used in arrayChunkSizeBytes and chunksPerBlock initialization" + ], + "origin": "constexpr std::array in file scope", + "transformations": [ + "Checked for size and last element at compile time", + "Used as lookup for allocation logic" + ], + "validated_at": "static_asserts at file scope" + }, + { + "field": "arrayChunkSizeBytes", + "flow": [ + "Computed from boundaries and elementSizeBytes", + "Used in initArrayChunksPerBlock and pool allocator templates" + ], + "origin": "initArrayChunkSizeBytes", + "transformations": [ + "Calculated as boundaries[I] * elementSizeBytes" + ], + "validated_at": "Indirectly via boundaries static_asserts" + } + ], + "description": "Implements memory management and manipulation utilities for the TaggedPointer class, which efficiently stores and manages child hashes and pointers for SHAMapInnerNode in the XRPL SHAMap structure, supporting both dense and sparse representations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "n", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at numAllocatedChildren", + "issue_pattern": "Missing empty string validation for n", + "why_false_positive": "XRPL_ASSERT validates n for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "n", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at numAllocatedChildren", + "issue_pattern": "Missing range validation for n", + "why_false_positive": "XRPL_ASSERT validates n range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "numChildren", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at boundariesIndex", + "issue_pattern": "Missing empty string validation for numChildren", + "why_false_positive": "XRPL_ASSERT validates numChildren for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "numChildren", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at boundariesIndex", + "issue_pattern": "Missing range validation for numChildren", + "why_false_positive": "XRPL_ASSERT validates numChildren range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "boundaries", + "empty", + "string", + "validation" + ], + "evidence": "static_assert at global scope (file-level)", + "issue_pattern": "Missing empty string validation for boundaries", + "why_false_positive": "static_assert validates boundaries for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "boundaries", + "empty", + "string", + "validation" + ], + "evidence": "static_assert at global scope (file-level)", + "issue_pattern": "Missing empty string validation for boundaries", + "why_false_positive": "static_assert validates boundaries for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/shamap/detail/TaggedPointer.ipp", + "functions": [ + { + "args": [ + "std::index_sequence" + ], + "lineno": 22, + "name": "initArrayChunkSizeBytes" + }, + { + "args": [ + "std::index_sequence" + ], + "lineno": 30, + "name": "initArrayChunksPerBlock" + }, + { + "args": [ + "std::uint8_t n" + ], + "lineno": 39, + "name": "numAllocatedChildren" + }, + { + "args": [ + "std::uint8_t numChildren" + ], + "lineno": 45, + "name": "boundariesIndex" + }, + { + "args": [ + "std::index_sequence" + ], + "lineno": 52, + "name": "initAllocateArrayFuns" + }, + { + "args": [ + "std::index_sequence" + ], + "lineno": 62, + "name": "initFreeArrayFuns" + }, + { + "args": [ + "std::index_sequence" + ], + "lineno": 72, + "name": "initIsFromArrayFuns" + }, + { + "args": [ + "std::uint8_t numChildren" + ], + "lineno": 82, + "name": "allocateArrays" + }, + { + "args": [ + "std::uint8_t boundaryIndex", + "void* p" + ], + "lineno": 88, + "name": "deallocateArrays" + }, + { + "args": [ + "std::uint16_t isBranch", + "F&& f" + ], + "lineno": 98, + "name": "iterChildren" + }, + { + "args": [ + "std::uint16_t isBranch", + "F&& f" + ], + "lineno": 117, + "name": "iterNonEmptyChildIndexes" + }, + { + "args": [], + "lineno": 137, + "name": "destroyHashesAndChildren" + }, + { + "args": [ + "std::uint16_t isBranch", + "int i" + ], + "lineno": 149, + "name": "getChildIndex" + }, + { + "args": [ + "RawAllocateTag", + "std::uint8_t numChildren" + ], + "lineno": 167, + "name": "TaggedPointer::TaggedPointer" + }, + { + "args": [ + "TaggedPointer&& other", + "std::uint16_t srcBranches", + "std::uint16_t dstBranches", + "std::uint8_t toAllocate" + ], + "lineno": 179, + "name": "TaggedPointer::TaggedPointer" + }, + { + "args": [ + "TaggedPointer&& other", + "std::uint16_t isBranch", + "std::uint8_t toAllocate" + ], + "lineno": 246, + "name": "TaggedPointer::TaggedPointer" + }, + { + "args": [ + "std::uint8_t numChildren" + ], + "lineno": 288, + "name": "TaggedPointer::TaggedPointer" + }, + { + "args": [ + "TaggedPointer&& other" + ], + "lineno": 297, + "name": "TaggedPointer::TaggedPointer" + }, + { + "args": [ + "TaggedPointer&& other" + ], + "lineno": 301, + "name": "TaggedPointer::operator=" + }, + { + "args": [], + "lineno": 310, + "name": "decode" + }, + { + "args": [], + "lineno": 315, + "name": "capacity" + }, + { + "args": [], + "lineno": 320, + "name": "isDense" + }, + { + "args": [], + "lineno": 325, + "name": "getHashesAndChildren" + }, + { + "args": [], + "lineno": 334, + "name": "getHashes" + }, + { + "args": [], + "lineno": 338, + "name": "getChildren" + }, + { + "args": [], + "lineno": 343, + "name": "~TaggedPointer" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file is a low-level implementation detail and is unlikely to be directly unit tested. Instead, its logic is exercised via higher-level SHAMap and SHAMapInnerNode tests. Typical test files would be in the SHAMap or SHAMapInnerNode test suites (e.g., SHAMap_test.cpp, SHAMapInnerNode_test.cpp). Direct validation of XRPL_ASSERT and static_asserts is not typically covered by runtime tests; static_asserts are compile-time only. There may be gaps in testing for edge cases (e.g., n == 0, n == branchFactor+1) unless explicitly tested in higher-level tests. No direct test hooks or test-specific code is present in this file.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT, static_assert (custom assertion and C++ built-in)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws in debug)", + "field": "n", + "location": "numAllocatedChildren", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that n <= SHAMapInnerNode::branchFactor" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws in debug)", + "field": "numChildren", + "location": "boundariesIndex", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that numChildren <= SHAMapInnerNode::branchFactor" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "compile-time error", + "field": "boundaries", + "location": "global scope (file-level)", + "validated_by": "static_assert", + "validates": [ + "Ensures boundaries.size() <= 4" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "compile-time error", + "field": "boundaries", + "location": "global scope (file-level)", + "validated_by": "static_assert", + "validates": [ + "Ensures boundaries.back() == SHAMapInnerNode::branchFactor" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/shamap/detail/TaggedPointer.ipp.ai.md b/include/xrpl/shamap/detail/TaggedPointer.ipp.ai.md new file mode 100644 index 0000000000..5752a326fd --- /dev/null +++ b/include/xrpl/shamap/detail/TaggedPointer.ipp.ai.md @@ -0,0 +1,43 @@ +# `TaggedPointer.ipp` — Sparse Child Storage for SHAMap Inner Nodes + +## Purpose and Context + +`TaggedPointer.ipp` provides the complete implementation of `TaggedPointer`, a low-level memory optimization used exclusively by `SHAMapInnerNode`. Every inner node in the XRPL SHAMap radix tree potentially has 16 child slots (`branchFactor = 16`), but in practice most nodes are sparsely populated. Allocating 16 `SHAMapHash` values and 16 `SharedPtr` pointers per node wastes large amounts of RAM when only 1 or 2 children actually exist. `TaggedPointer` solves this by dynamically choosing between four allocation sizes — {2, 4, 6, 16} child slots — and encoding the chosen size directly inside the pointer's unused low bits. + +## The Tagged Pointer Layout + +Modern platforms guarantee that objects with alignment ≥ 4 have their lower two address bits always zero. The `SHAMapHash` type is `static_assert`-verified to have `alignof >= 4`, so a raw pointer to it always has `tp_ & 3 == 0`. `TaggedPointer` repurposes those two bits as a tag — an index into the `boundaries` array `{2, 4, 6, 16}`. Decoding is trivial: `tp_ & tagMask` yields the tag, and `tp_ & ptrMask` recovers the raw pointer. + +The tag directly tells `capacity()` how large the arrays are: `boundaries[tag]`. Tag 3 (the last slot, mapping to 16) identifies the *dense* representation; tags 0–2 identify *sparse* representations. `isDense()` is therefore just `(tp_ & tagMask) == 3`. + +The pointed-to memory holds two co-located arrays: `N` instances of `SHAMapHash` followed immediately by `N` instances of `SharedPtr`, where `N = boundaries[tag]`. `getHashesAndChildren()` decodes the tag, computes the hash array start as a plain cast of the pointer, then advances by `N * sizeof(SHAMapHash)` to find the children array. + +## Pool Allocation Strategy + +Rather than calling `operator new` per node, the implementation uses four separate `boost::singleton_pool` instances — one for each boundary size. Each pool is templated on its exact chunk size (`boundaries[I] * elementSizeBytes`), where `elementSizeBytes` is `sizeof(SHAMapHash) + sizeof(SharedPtr)`. Pools are configured with 512 KiB blocks subdivided into as many fixed-size chunks as fit. + +The pool interface is materialized at startup into three `std::array, 4>` globals — `allocateArrayFuns`, `freeArrayFuns`, and `isFromArrayFuns` — using an `std::index_sequence` parameter pack expansion. This allows the allocator to dispatch by boundary index with a single array lookup at runtime, rather than a `switch` statement or virtual dispatch. The `isFromArrayFuns` array is used defensively in `deallocateArrays` to assert that memory is returned to the correct pool. + +This design matters for performance: SHAMap operations can create and destroy millions of inner nodes during ledger validation. Pool allocation gives O(1) fixed-overhead allocation and avoids heap fragmentation entirely, since all chunks of a given size are managed in a single segregated free list. + +## Sparse vs. Dense Representation + +In sparse mode, only the non-empty children are stored, packed in branch-index order. The `isBranch_` bitfield (a 16-bit integer on `SHAMapInnerNode`) tracks which branches are populated. Translating a branch index `i` into a sparse array index is done via `getChildIndex()`: it computes `popcnt16(isBranch & ((1 << i) - 1))` — the count of set bits below position `i` in the bitset equals the number of occupied slots before slot `i` in the sorted sparse array. + +`iterChildren()` exposes all 16 logical branches to its callback regardless of representation. In sparse mode it walks the bit positions of `isBranch`, emitting real hashes for set bits and the static `zeroSHAMapHash` singleton for unset bits. `iterNonEmptyChildIndexes()` gives both the branch number and the array index to its callback, letting callers work with either coordinate system. + +## Constructor Design and the In-Place Optimization + +`TaggedPointer` is move-only (copy is deleted). The most architecturally interesting part is its pair of restructuring constructors, used when `SHAMapInnerNode` needs to resize or reshape its child arrays. + +The three-argument constructor `TaggedPointer(TaggedPointer&&, isBranch, toAllocate)` resizes the arrays to fit `toAllocate` children. If the source already has the right capacity, it performs the conversion *in-place* using `iterNonEmptyChildIndexes` and placement moves. Otherwise it allocates a new chunk, moves children over, and destroys the old storage. + +The four-argument constructor `TaggedPointer(TaggedPointer&&, srcBranches, dstBranches, toAllocate)` handles a more general case: the caller specifies which children exist in the source (`srcBranches`) and which should exist in the destination (`dstBranches`). This is used when simultaneously changing the child set and the representation. The in-place path (same capacity) iterates all 16 branch positions and performs left/right shifts within the sparse array for insertions and deletions. The out-of-place path placement-constructs elements into the new allocation. Both paths handle all four intersection cases: kept (`inSrc && inDst`), removed (`inSrc && !inDst`), added (`!inSrc && inDst`), and absent from both. + +The private `RawAllocateTag` constructor variant allocates raw memory from a pool without running any constructors. It is used only internally, in pairs with explicit placement-new loops — a necessary pattern because `destroyHashesAndChildren()` always runs explicit destructors and must not be called on unconstructed objects. + +## Destruction and Invariants + +`destroyHashesAndChildren()` manually destructs each `SHAMapHash` and `SharedPtr` element before returning the chunk to its pool. This is mandatory because the objects were created with placement new into pool-allocated memory that the C++ runtime does not track. Move-from sets `tp_ = 0`, and the null check `if (!tp_) return` at the top of `destroyHashesAndChildren()` makes moved-from objects safely destructible. + +Two compile-time constraints guard the entire design: `boundaries.size() <= 4` (the tag field is exactly 2 bits, supporting at most 4 values) and `boundaries.back() == branchFactor` (the last boundary must be the full dense count, so the dense case is unambiguously the final tag value). If either constraint is violated — say, someone adds a fifth boundary — the build fails immediately. \ No newline at end of file diff --git a/include/xrpl/tx/ApplyContext.h.ai.json b/include/xrpl/tx/ApplyContext.h.ai.json new file mode 100644 index 0000000000..a4525bc749 --- /dev/null +++ b/include/xrpl/tx/ApplyContext.h.ai.json @@ -0,0 +1,196 @@ +{ + "args": [ + { + "lineno": 13, + "name": "registry" + }, + { + "lineno": 13, + "name": "base" + }, + { + "lineno": 13, + "name": "parentBatchId" + }, + { + "lineno": 13, + "name": "tx" + }, + { + "lineno": 13, + "name": "preclaimResult" + }, + { + "lineno": 13, + "name": "baseFee" + }, + { + "lineno": 13, + "name": "flags" + }, + { + "lineno": 13, + "name": "journal" + }, + { + "lineno": 71, + "name": "func" + }, + { + "lineno": 56, + "name": "amount" + }, + { + "lineno": 78, + "name": "fee" + }, + { + "lineno": 86, + "name": "result" + }, + { + "lineno": 86, + "name": "fee" + }, + { + "lineno": 94, + "name": "result" + } + ], + "classes": [ + { + "args": [ + "ServiceRegistry& registry", + "OpenView& base", + "std::optional const& parentBatchId", + "STTx const& tx", + "TER preclaimResult", + "XRPAmount baseFee", + "ApplyFlags flags", + "beast::Journal journal" + ], + "lineno": 12, + "name": "ApplyContext" + } + ], + "description": "Defines the ApplyContext class, which manages state and operations when applying a transaction in the XRPL ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/ApplyContext.h", + "functions": [ + { + "args": [ + "ServiceRegistry& registry", + "OpenView& base", + "std::optional const& parentBatchId", + "STTx const& tx", + "TER preclaimResult", + "XRPAmount baseFee", + "ApplyFlags flags", + "beast::Journal journal" + ], + "lineno": 13, + "name": "ApplyContext" + }, + { + "args": [ + "ServiceRegistry& registry", + "OpenView& base", + "STTx const& tx", + "TER preclaimResult", + "XRPAmount baseFee", + "ApplyFlags flags", + "beast::Journal journal" + ], + "lineno": 22, + "name": "ApplyContext" + }, + { + "args": [], + "lineno": 34, + "name": "view" + }, + { + "args": [], + "lineno": 39, + "name": "view" + }, + { + "args": [], + "lineno": 45, + "name": "rawView" + }, + { + "args": [], + "lineno": 50, + "name": "flags" + }, + { + "args": [ + "STAmount const& amount" + ], + "lineno": 56, + "name": "deliver" + }, + { + "args": [], + "lineno": 62, + "name": "discard" + }, + { + "args": [ + "TER" + ], + "lineno": 65, + "name": "apply" + }, + { + "args": [], + "lineno": 68, + "name": "size" + }, + { + "args": [ + "std::function const& before, std::shared_ptr const& after)> const& func" + ], + "lineno": 71, + "name": "visit" + }, + { + "args": [ + "XRPAmount const& fee" + ], + "lineno": 78, + "name": "destroyXRP" + }, + { + "args": [ + "TER const result", + "XRPAmount const fee" + ], + "lineno": 86, + "name": "checkInvariants" + }, + { + "args": [ + "TER const result" + ], + "lineno": 94, + "name": "failInvariantCheck" + }, + { + "args": [ + "TER const result", + "XRPAmount const fee", + "std::index_sequence" + ], + "lineno": 97, + "name": "checkInvariantsHelper" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/ApplyContext.h.ai.md b/include/xrpl/tx/ApplyContext.h.ai.md new file mode 100644 index 0000000000..4ee7e0cdd3 --- /dev/null +++ b/include/xrpl/tx/ApplyContext.h.ai.md @@ -0,0 +1,61 @@ +# `ApplyContext` — Transaction Application State Container + +`ApplyContext` is the central context object threading through the entire transaction-application pipeline in the XRPL ledger engine. Every time a signed transaction is applied to an `OpenView`, an `ApplyContext` bundles together the transaction, its pre-validated result, the sandboxed mutable view, logging infrastructure, and the invariant checking machinery needed to safely commit or discard ledger state changes. + +## Role in the System + +The XRPL transaction engine processes each transaction in two phases: a *preclaim* phase (authorization and fee validation, read-only) and an *apply* phase (actual state mutation). `ApplyContext` is created at the boundary between those phases and lives for the duration of the apply phase. It is passed by reference to every `Transactor` implementation, giving each transaction handler a uniform handle to the sandboxed ledger view, the fee information, and the ability to commit or roll back. + +## Sandboxed View Lifecycle + +The most important design choice in `ApplyContext` is how it manages the mutable ledger view. The working view is stored as `std::optional view_`, sitting on top of a reference to the underlying `OpenView& base_`. This optional wrapping is not incidental — it is the mechanism behind `discard()`: + +```cpp +void ApplyContext::discard() { + view_.emplace(&base_, flags_); +} +``` + +Rather than implementing rollback semantics (walking backwards through recorded mutations), `discard()` simply destroys the current `ApplyViewImpl` in-place and constructs a fresh one. The base view is never touched, so the sandboxed changes evaporate without any undo log. This is a deliberate performance and simplicity tradeoff: rollback in a complex ledger object model would require either copy-on-write snapshots or a redo log; instead the system just discards and restarts from the unmodified base. + +When a transaction handler is satisfied with its changes, it calls `apply(TER)`, which delegates to `ApplyViewImpl::apply()`. That method writes the accumulated state changes from the sandbox into `base_`, generates and returns `TxMeta` (the transaction metadata), and marks itself consumed. After `apply()` returns, the `ApplyViewImpl` is no longer usable — it has transferred ownership of its mutations to the base view. + +## Batch Transaction Support + +`ApplyContext` has two constructors. The simpler one (without `parentBatchId`) delegates to the full constructor with `std::nullopt` and includes an assertion that `tapBATCH` is not set: + +```cpp +XRPL_ASSERT((flags & tapBATCH) == 0, "Batch apply flag should not be set"); +``` + +When a transaction executes inside a batch, the fuller constructor receives the parent batch transaction's `uint256` ID. This ID is stored in `parentBatchId_` and forwarded through to `ApplyViewImpl::apply()`, where it gets embedded in the generated `TxMeta`. This creates an auditable parent-child relationship in ledger metadata between batch envelope transactions and their inner transactions. The constructor correspondingly asserts the inverse invariant — that `parentBatchId` is set if and only if `tapBATCH` is active. + +## Invariant Checking + +After every successful or fee-claiming transaction, `checkInvariants()` is called as the final safety gate before the result is accepted. This method operates over a compile-time tuple of checker types — `InvariantChecks` — defined in `InvariantCheck.h`. The implementation uses `std::index_sequence` to iterate: + +```cpp +template +TER ApplyContext::checkInvariantsHelper(TER result, XRPAmount fee, std::index_sequence) { + auto checkers = getInvariantChecks(); + visit([&checkers](...) { + (..., std::get(checkers).visitEntry(isDelete, before, after)); + }); + std::array const finalizers{ + {std::get(checkers).finalize(tx, result, fee, *view_, journal)...}}; + if (!std::all_of(...)) { return failInvariantCheck(result); } + return result; +} +``` + +A critical comment explains why a `&&` fold expression is explicitly avoided for the `finalize` calls: it would short-circuit on the first failure, suppressing log output from every subsequent failing invariant. The two-step approach — collect results into an array, then check the array — ensures every invariant that fails writes its fatal log message. + +The `failInvariantCheck()` static method encodes a two-tier failure response. If the current result is already `tecINVARIANT_FAILED` or `tefINVARIANT_FAILED`, it returns `tefINVARIANT_FAILED` — a transaction error code that causes the transaction to be *excluded* from the ledger entirely. If this is the first invariant failure, it returns `tecINVARIANT_FAILED`, which still results in a fee-charging ledger entry (the transaction appears in the ledger but has a failed result). This distinction matters for network consensus: a `tec` result is reproducible across validators and thus consensus-safe, while a `tef` result signals something so wrong the transaction must not be included at all. + +## The `rawView()` Accessor + +The header contains an explicit code comment — `// VFALCO Unfortunately this is necessary` — on the `rawView()` accessor. `ApplyView` provides higher-level ledger manipulation that enforces certain constraints. Some internal code in the transaction engine requires the lower-level `RawView` interface to write ledger entries without those guards. The accessor exposes this escape hatch while making its non-ideal nature visible. + +## Public State and Immutability + +Several fields are `const`-qualified public members: `tx`, `preclaimResult`, `baseFee`, and `journal`. This reflects the reality that these values are fixed for the lifetime of a transaction apply cycle — the transaction cannot change, the pre-claim result is read-only context, and the fee is determined before applying. Mutable state is confined to the private `view_`, `flags_`, and `parentBatchId_` members, with controlled access through the public interface. \ No newline at end of file diff --git a/include/xrpl/tx/SignerEntries.h.ai.json b/include/xrpl/tx/SignerEntries.h.ai.json new file mode 100644 index 0000000000..d6ad38a6a4 --- /dev/null +++ b/include/xrpl/tx/SignerEntries.h.ai.json @@ -0,0 +1,64 @@ +{ + "args": [ + { + "lineno": 27, + "name": "inAccount" + }, + { + "lineno": 27, + "name": "inWeight" + }, + { + "lineno": 27, + "name": "inTag" + }, + { + "lineno": 44, + "name": "obj" + }, + { + "lineno": 44, + "name": "journal" + }, + { + "lineno": 44, + "name": "annotation" + } + ], + "classes": [ + { + "args": [], + "lineno": 17, + "name": "SignerEntries" + }, + { + "args": [ + "inAccount", + "inWeight", + "inTag" + ], + "lineno": 22, + "name": "SignerEntry" + } + ], + "description": "Defines the SignerEntries class used to represent and deserialize signer entries (multi-signature signers) from ledger or transaction objects in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/SignerEntries.h", + "functions": [ + { + "args": [ + "obj", + "journal", + "annotation" + ], + "lineno": 44, + "name": "deserialize" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/SignerEntries.h.ai.md b/include/xrpl/tx/SignerEntries.h.ai.md new file mode 100644 index 0000000000..d5ab7bd3ed --- /dev/null +++ b/include/xrpl/tx/SignerEntries.h.ai.md @@ -0,0 +1,38 @@ +# `include/xrpl/tx/SignerEntries.h` — Multi-Signature Signer Entry Representation + +## Purpose and Context + +The XRPL multi-signature system lets an account authorize transactions through a quorum of weighted co-signers rather than a single private key. This header defines the minimal data model and deserialization logic for that co-signer roster. It exists as a shared building block used by at least three distinct sites: the `SignerListSet` transactor (which creates and destroys signer lists), the base `Transactor::checkMultiSign()` method (which authenticates incoming multi-sig transactions against the on-ledger list), and the `XChainBridge` transactor (which reads account signers during cross-chain operations). Centralizing this into one header prevents each transactor from re-implementing the same `STArray` iteration and field-extraction logic. + +## `SignerEntry` — The Core Record + +`SignerEntry` is a plain struct holding three fields: an `AccountID`, a `uint16_t` weight, and an `optional` tag that maps to the `sfWalletLocator` field. The optional tag supports destination tagging for phantom accounts (signers that may not yet have on-ledger account roots). + +The comparison operators are deliberately defined on `account` alone — `operator<` sorts by `AccountID`, and `operator==` tests equality purely by `AccountID`. This design is not accidental: `SignerListSet::determineOperation()` calls `std::sort()` on the deserialized vector immediately after `deserialize()` returns, and `Transactor::checkMultiSign()` then exploits that sorted order to perform a single O(n) linear merge between the sorted ledger signers and the sorted transaction signers. If the comparison included `weight` or `tag`, the duplicate-detection logic using `std::adjacent_find()` would silently miss two entries with the same account but differing weights, which is a category of malformed transaction that `validateQuorumAndSignerEntries()` explicitly rejects with `temBAD_SIGNER`. + +## `SignerEntries` — A Non-Constructible Utility Namespace + +The outer `SignerEntries` class has an `explicit`-deleted default constructor: it cannot be instantiated. It acts purely as a named scope for the inner `SignerEntry` type and the static `deserialize()` method. This is a deliberate design over a free function or plain namespace — keeping the type `SignerEntries::SignerEntry` and function `SignerEntries::deserialize()` co-located under the same identifier makes call sites self-documenting about what they are operating on. + +The comment in the header makes the data model explicit: a `std::vector` *is* the signer list representation; there is no richer container object wrapping it. + +## `deserialize()` — Validation and Extraction + +```cpp +static Expected, NotTEC> +deserialize(STObject const& obj, beast::Journal journal, std::string_view annotation); +``` + +The function accepts any `STObject` — the same call works against both an `STTx` (a transaction being preflight-checked) and an `SLE` (a live ledger entry being read during apply). The `annotation` parameter is passed as a `string_view` rather than constructed inline, and it feeds directly into journal log messages: callers pass `"transaction"` or `"ledger"` so that trace logs pinpoint whether the malformed data came from a submitted transaction or from ledger state, aiding debugging of both client errors and potential ledger corruption. + +The return type `Expected, NotTEC>` is the XRPL variant of the `std::expected` pattern. `NotTEC` is a strong typedef over the transaction error code type restricted to non-`tec` error codes, meaning errors that should abort the transaction without charging a fee (e.g., `temMALFORMED`). Using `Expected` forces callers to explicitly handle the error path before accessing the value; both `SignerListSet::determineOperation()` and `Transactor::checkMultiSign()` immediately test `if (!signers)` and propagate `signers.error()` before dereferencing. + +The implementation (`SignerEntries.cpp`) performs two layers of validation: it first checks that `sfSignerEntries` is present on the object at all, then iterates the `STArray` verifying that each element's field name is `sfSignerEntry`. It extracts three fields per entry — `sfAccount`, `sfSignerWeight`, and optionally `sfWalletLocator` — and appends them to a pre-reserved vector sized to `STTx::maxMultiSigners`. No business-logic validation (quorum reachability, duplicate detection, self-reference checking) happens here; that responsibility belongs to `SignerListSet::validateQuorumAndSignerEntries()`, which receives the already-deserialized vector. The separation keeps deserialization pure and composable. + +## Relationship to the Broader Signing Flow + +When a `SignerListSet` transaction arrives, `preflight` calls `determineOperation()` → `SignerEntries::deserialize(tx, j, "transaction")`, sorts the result, and passes it to `validateQuorumAndSignerEntries()`. At apply time, `preCompute()` repeats the deserialization to re-populate `signers_`. The read-then-re-deserialize pattern is intentional: preflight and apply run in different contexts and the apply phase needs the deserialized list available without storing it across the phase boundary. + +When a multi-signed transaction of *any* type is submitted, `Transactor::checkMultiSign()` reads the on-ledger `ltSIGNER_LIST` object and calls `SignerEntries::deserialize(*sleAccountSigners, j, "ledger")`. It then walks both the ledger signer vector and the transaction's `sfSigners` array in parallel — O(n) because both are sorted by `AccountID` — accumulating weight from matched signers and returning `tefBAD_SIGNATURE` on any mismatch. + +This header is thus the pivot between the protocol representation of signer lists (as `STArray` inside `STObject`) and the in-memory representation consumed by transaction processing logic throughout the codebase. \ No newline at end of file diff --git a/include/xrpl/tx/Transactor.h.ai.json b/include/xrpl/tx/Transactor.h.ai.json new file mode 100644 index 0000000000..ce4b7f9a64 --- /dev/null +++ b/include/xrpl/tx/Transactor.h.ai.json @@ -0,0 +1,432 @@ +{ + "args": [ + { + "lineno": 13, + "name": "registry" + }, + { + "lineno": 14, + "name": "tx" + }, + { + "lineno": 15, + "name": "rules" + }, + { + "lineno": 16, + "name": "flags" + }, + { + "lineno": 17, + "name": "parentBatchId" + }, + { + "lineno": 18, + "name": "j" + }, + { + "lineno": 43, + "name": "view" + }, + { + "lineno": 44, + "name": "preflightResult" + } + ], + "classes": [ + { + "args": [ + "registry", + "tx", + "rules", + "flags", + "parentBatchId", + "j" + ], + "lineno": 9, + "name": "PreflightContext" + }, + { + "args": [ + "registry", + "view", + "preflightResult", + "tx", + "flags", + "parentBatchId", + "j" + ], + "lineno": 38, + "name": "PreclaimContext" + }, + { + "args": [], + "lineno": 70, + "name": "TxConsequences" + }, + { + "args": [], + "lineno": 71, + "name": "PreflightResult" + }, + { + "args": [], + "lineno": 73, + "name": "Change" + }, + { + "args": [ + "ctx" + ], + "lineno": 75, + "name": "Transactor" + } + ], + "description": "Defines core transaction processing context structures and the Transactor base class for XRPL transaction preflight, preclaim, and application logic, including static and virtual methods for transaction validation, fee calculation, and signature checks.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/Transactor.h", + "functions": [ + { + "args": [], + "lineno": 74, + "name": "Transactor::view" + }, + { + "args": [], + "lineno": 79, + "name": "Transactor::view" + }, + { + "args": [ + "view", + "tx", + "j" + ], + "lineno": 97, + "name": "Transactor::checkSeqProxy" + }, + { + "args": [ + "ctx" + ], + "lineno": 101, + "name": "Transactor::checkPriorTxAndLastLedger" + }, + { + "args": [ + "ctx", + "baseFee" + ], + "lineno": 105, + "name": "Transactor::checkFee" + }, + { + "args": [ + "ctx" + ], + "lineno": 109, + "name": "Transactor::checkSign" + }, + { + "args": [ + "ctx" + ], + "lineno": 113, + "name": "Transactor::checkBatchSign" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 117, + "name": "Transactor::calculateBaseFee" + }, + { + "args": [ + "ctx" + ], + "lineno": 146, + "name": "Transactor::preclaim" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 152, + "name": "Transactor::checkPermission" + }, + { + "args": [ + "view", + "account", + "ticketIndex", + "j" + ], + "lineno": 157, + "name": "Transactor::ticketDelete" + }, + { + "args": [], + "lineno": 163, + "name": "Transactor::apply" + }, + { + "args": [], + "lineno": 166, + "name": "Transactor::preCompute" + }, + { + "args": [], + "lineno": 169, + "name": "Transactor::doApply" + }, + { + "args": [ + "registry", + "baseFee", + "fees", + "flags" + ], + "lineno": 177, + "name": "Transactor::minimumFee" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 183, + "name": "Transactor::calculateOwnerReserveFee" + }, + { + "args": [ + "view", + "flags", + "parentBatchId", + "idAccount", + "sigObject", + "j" + ], + "lineno": 187, + "name": "Transactor::checkSign" + }, + { + "args": [ + "ctx" + ], + "lineno": 192, + "name": "Transactor::checkExtraFeatures" + }, + { + "args": [ + "ctx" + ], + "lineno": 195, + "name": "Transactor::getFlagsMask" + }, + { + "args": [ + "ctx" + ], + "lineno": 198, + "name": "Transactor::preflightSigValidated" + }, + { + "args": [ + "slice", + "maxLength" + ], + "lineno": 201, + "name": "Transactor::validDataLength" + }, + { + "args": [ + "value", + "max", + "min" + ], + "lineno": 204, + "name": "Transactor::validNumericRange" + }, + { + "args": [ + "value", + "max", + "min" + ], + "lineno": 210, + "name": "Transactor::validNumericRange" + }, + { + "args": [ + "value", + "min" + ], + "lineno": 217, + "name": "Transactor::validNumericMinimum" + }, + { + "args": [ + "value", + "min" + ], + "lineno": 222, + "name": "Transactor::validNumericMinimum" + }, + { + "args": [ + "fee" + ], + "lineno": 227, + "name": "Transactor::reset" + }, + { + "args": [ + "sleAccount" + ], + "lineno": 230, + "name": "Transactor::consumeSeqProxy" + }, + { + "args": [], + "lineno": 231, + "name": "Transactor::payFee" + }, + { + "args": [ + "view", + "idSigner", + "idAccount", + "sleAccount", + "j" + ], + "lineno": 232, + "name": "Transactor::checkSingleSign" + }, + { + "args": [ + "view", + "flags", + "id", + "sigObject", + "j" + ], + "lineno": 237, + "name": "Transactor::checkMultiSign" + }, + { + "args": [ + "uint256" + ], + "lineno": 239, + "name": "Transactor::trapTransaction" + }, + { + "args": [ + "ctx", + "flagMask" + ], + "lineno": 247, + "name": "Transactor::preflight1" + }, + { + "args": [ + "ctx" + ], + "lineno": 253, + "name": "Transactor::preflight2" + }, + { + "args": [ + "ctx" + ], + "lineno": 257, + "name": "Transactor::checkExtraFeatures" + }, + { + "args": [ + "ctx", + "flagMask" + ], + "lineno": 262, + "name": "preflight0" + }, + { + "args": [ + "sigObject", + "j" + ], + "lineno": 268, + "name": "detail::preflightCheckSigningKey" + }, + { + "args": [ + "flags", + "sigObject", + "j" + ], + "lineno": 273, + "name": "detail::preflightCheckSimulateKeys" + }, + { + "args": [ + "ctx" + ], + "lineno": 278, + "name": "Transactor::invokePreflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 281, + "name": "Transactor::invokePreflight" + }, + { + "args": [ + "value", + "max", + "min" + ], + "lineno": 299, + "name": "Transactor::validNumericRange" + }, + { + "args": [ + "value", + "max", + "min" + ], + "lineno": 307, + "name": "Transactor::validNumericRange" + }, + { + "args": [ + "value", + "min" + ], + "lineno": 314, + "name": "Transactor::validNumericMinimum" + }, + { + "args": [ + "value", + "min" + ], + "lineno": 320, + "name": "Transactor::validNumericMinimum" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 265, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/Transactor.h.ai.md b/include/xrpl/tx/Transactor.h.ai.md new file mode 100644 index 0000000000..1813970d60 --- /dev/null +++ b/include/xrpl/tx/Transactor.h.ai.md @@ -0,0 +1,73 @@ +# `include/xrpl/tx/Transactor.h` + +## Role in the System + +This header is the architectural keystone of XRPL transaction processing. Every transaction type — payment, offer, AMM, NFT, escrow, and dozens more — is implemented as a class that publicly inherits `Transactor`. The header defines the three-phase validation-and-execution contract that all transactions must satisfy, along with the two context structures (`PreflightContext` and `PreclaimContext`) that carry state between phases. + +The three phases exist for a specific reason: they differ in what ledger state they require and in the consequences of failure. Code that can run without any ledger access runs cheapest; code that needs a final ledger read runs in the middle; and code that mutates ledger state runs last only when the cheaper checks have already passed. + +## The Three-Phase Pipeline + +**Phase 1 — preflight**: No ledger access. Validates transaction format, flags, fee field sanity, network ID, and cryptographic signature validity. Runs against a `PreflightContext` that has only the raw `STTx`, the active `Rules`, and apply flags. Because it requires no I/O, preflight can be called in parallel or cached. The output is a `PreflightResult` bundling the `NotTEC` result with `TxConsequences`, which the transaction queue (`TxQ`) uses to reason about how queuing the transaction would affect the account. + +**Phase 2 — preclaim**: Read-only ledger access via `ReadView`. Given a `PreclaimContext` that adds the ledger view and the preflight result, preclaim checks whether the transaction is likely to succeed against the current ledger state (correct sequence, sufficient fee balance, signature against account state). The key output is whether the transaction will claim a fee even on failure — ledger infrastructure uses this to decide whether to relay an unvalidated transaction. + +**Phase 3 — doApply**: Mutable ledger access via `ApplyView`. The virtual method that each concrete transactor must override. Only runs when preclaim returned `tesSUCCESS`. + +## Compile-Time Polymorphism via Name Hiding + +The most architecturally notable design in this file is that the preflight pipeline does **not** use virtual dispatch. Instead, derived classes override behavior by defining static methods with the same names — `preflight`, `preclaim`, `getFlagsMask`, `checkExtraFeatures`, `preflightSigValidated` — and the central `invokePreflight()` template resolves them at compile time: + +```cpp +template +NotTEC Transactor::invokePreflight(PreflightContext const& ctx) { + auto const feature = Permission::getInstance().getTxFeature(ctx.tx.getTxnType()); + if (feature && !ctx.rules.enabled(*feature)) return temDISABLED; + if (!T::checkExtraFeatures(ctx)) return temDISABLED; + if (auto const ret = preflight1(ctx, T::getFlagsMask(ctx))) return ret; + if (auto const ret = T::preflight(ctx)) return ret; + if (auto const ret = preflight2(ctx)) return ret; + return T::preflightSigValidated(ctx); +} +``` + +The pattern is deliberate: it gives each transaction type the ability to add or replace validation steps without virtual-function overhead or the accidental base-class call problem. The header comment explicitly warns not to define `invokePreflight` in derived classes, and not to call `preflight1` or `preflight2` directly — those are private plumbing called in the correct order by the template. + +`preflight1` checks the account field, fee field, signing key format, and ticket/AccountTxnID compatibility. `preflight2` validates the cryptographic signature via the hash router cache. The reason they are split is that the transaction-specific `T::preflight` runs between them, allowing type-level validation to happen before the expensive signature check. + +The one explicit template specialization, `invokePreflight`, is defined in `Change.cpp`. `Change` is a pseudo-transaction (validator-generated, no real sender) that requires entirely different preflight logic. + +## Context Structures and Batch Support + +Both `PreflightContext` and `PreclaimContext` have dual constructors: one for ordinary transactions and one for batch inner transactions. The batch variants accept a `parentBatchId` (the hash of the outer batch transaction) and assert that `tapBATCH` is set in `flags`. The non-batch constructors assert the opposite. This constructor-level assertion enforces the invariant at construction time — if you pass a batch ID without the flag, or the flag without an ID, you get a debug assertion rather than silent misbehavior. + +When `tapBATCH` is active, `preflight2` skips the cryptographic signature check entirely, since batch inner transactions are authorized through the outer transaction's signature. + +## `operator()` and the Apply Mechanics + +`Transactor::operator()()` is the final dispatch point called by `doApply()` (the free function in `applySteps.h`). It: + +1. Sets up per-transaction numeric rule guards (`fixUniversalNumber`, `CurrentTransactionRulesGuard`) as RAII objects. +2. In debug builds, roundtrips the transaction through serialization to detect corruption. +3. Checks if the transaction ID matches a debug trap, useful for stopping on a specific transaction during testing. +4. Passes the preclaimResult directly if it isn't `tesSUCCESS`; otherwise calls `apply()` → `preCompute()` → `doApply()`. +5. Handles `tecOVERSIZE` (metadata grew too large) by rolling back and rerunning to only collect removable offers, trust lines, and NFT offers. +6. Forces `applied = false` when `tapDRY_RUN` (simulation mode) is set, ensuring no state changes persist. + +The private `reset(fee)` method rolls back `ApplyContext` and deducts only the fee — the mechanism for charging a fee on a failed-but-fee-claiming (`tec`) result. + +## Fee Calculation + +`calculateBaseFee()` returns the base fee in drops: the ledger's configured base fee plus one additional base fee per multisigner. This is not scaled for server load. `minimumFee()` scales it using `LoadFeeTrack` from the `ServiceRegistry`. Separating the two allows the unscaled base fee to be computed in any phase, while load scaling only happens where current node load is accessible. + +`calculateOwnerReserveFee()` adds the owner reserve increment for transactions that will create a ledger object, preventing spam by requiring the sender to prove they can cover the reserve. + +## `ConsequencesFactoryType` and the Transaction Queue + +Each concrete transactor declares a `static constexpr ConsequencesFactoryType ConsequencesFactory` member with one of three values. `Normal` produces standard `TxConsequences` (fee only). `Blocker` signals that applying this transaction could prevent subsequent queued transactions from claiming fees (e.g., `SetRegularKey` changing the signing key). `Custom` requires the transactor to implement `makeTxConsequences(PreflightContext const&)` for transaction-specific cost modeling. The `with_txn_type` function in `applySteps.cpp` uses C++20 `requires` constraints to dispatch the correct `consequences_helper` at compile time. + +## Validation Utilities + +The protected template helpers `validNumericRange` and `validNumericMinimum` follow a deliberate convention: an absent optional (`std::nullopt`) is treated as valid, reflecting the rule that optional fields are legal to omit. Both are overloaded for `unit::ValueUnit` strong-unit types to maintain type safety across unit systems. + +`checkPermission()` integrates the Delegate feature: if the transaction carries a `sfDelegate` field, it verifies that a `Delegate` ledger object exists and that the delegated permissions cover this transaction type. This is called as a static method during preclaim so the read-only ledger check happens before any mutation. \ No newline at end of file diff --git a/include/xrpl/tx/apply.h.ai.json b/include/xrpl/tx/apply.h.ai.json new file mode 100644 index 0000000000..ce2aa13c81 --- /dev/null +++ b/include/xrpl/tx/apply.h.ai.json @@ -0,0 +1,56 @@ +{ + "args": [], + "classes": [], + "description": "This file provides functions and enums for validating and applying transactions to an XRPL ledger, including signature and local checks, caching validity, and applying transactions with error handling.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/apply.h", + "functions": [ + { + "args": [ + "router", + "tx", + "rules" + ], + "lineno": 27, + "name": "checkValidity" + }, + { + "args": [ + "router", + "txid", + "validity" + ], + "lineno": 44, + "name": "forceValidity" + }, + { + "args": [ + "registry", + "view", + "tx", + "flags", + "journal" + ], + "lineno": 62, + "name": "apply" + }, + { + "args": [ + "registry", + "view", + "tx", + "retryAssured", + "flags", + "journal" + ], + "lineno": 101, + "name": "applyTransaction" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/apply.h.ai.md b/include/xrpl/tx/apply.h.ai.md new file mode 100644 index 0000000000..e96642b69f --- /dev/null +++ b/include/xrpl/tx/apply.h.ai.md @@ -0,0 +1,45 @@ +# `include/xrpl/tx/apply.h` — Transaction Validation and Application API + +This header defines the top-level public interface for applying transactions to the XRP Ledger. It sits at the boundary between the consensus machinery (which decides *when* to process transactions) and the lower-level transaction pipeline (which decides *how* to process them). Everything in this file either gates a transaction before touching the ledger or orchestrates the full application sequence as a single call. + +## The Three-Stage Pipeline + +Transaction processing in the XRPL is deliberately split into three ordered stages, defined in the companion header `applySteps.h`: + +1. **`preflight`** — stateless validation against ledger rules (signature format, field constraints, fee structure). The result can be cached and reused safely across threads because it requires no ledger state. +2. **`preclaim`** — stateful validation against the current `OpenView` (account existence, sufficient balance, sequence correctness). Must run on the same thread as the view. +3. **`doApply`** — the actual mutation of ledger state. Must run on the same thread as `preclaim`, with the same view. + +The `apply()` function declared here composes all three into a single call for callers that don't need fine-grained control. Internally the implementation template-dispatches on a preflight callable: + +```cpp +return doApply(preclaim(preflightChecks(), registry, view), registry, view); +``` + +This structure means the hot path avoids unnecessary copies: the `PreflightResult` and `PreclaimResult` structs hold const references (not copies) of the original `STTx` and `ReadView`, and their copy-assignment operators are deleted to prevent accidental reuse with a stale view. + +## Validity Caching via HashRouter + +The `checkValidity()` function does not maintain its own cache. Instead, it piggybacks on the `HashRouter` — a network-layer routing table that already tracks transaction hashes for P2P broadcast deduplication. Four private flags (`PRIVATE1`–`PRIVATE4`) are reserved in `HashRouterFlags` and aliased internally as `SF_SIGBAD`, `SF_SIGGOOD`, `SF_LOCALBAD`, and `SF_LOCALGOOD`. On the first call for a given transaction ID, `checkValidity` performs `tx.checkSign(rules)` and `passesLocalChecks(tx, reason)`, then records the outcome as flags. Subsequent calls hit the cached flags directly. + +This design avoids a separate validity cache data structure and leverages the natural TTL already built into the `HashRouter`'s aged map. The tradeoff is coupling the tx-application layer to a network-layer component, justified by the fact that transactions that pass validity checks are exactly the ones that get routed to peers. + +The `Validity` enum encodes a strict three-level hierarchy — `SigBad < SigGoodOnly < Valid` — matching the real dependency between checks: local checks are only worth running if the signature is good. + +`forceValidity()` uses a deliberate fallthrough switch to enforce monotonicity: setting `Valid` also sets `SigGoodOnly`'s flag, because you cannot claim local checks pass without first affirming the signature is good. The comment "can only raise the validity to a more valid state" is enforced structurally, not just by convention. An attempt to call it with `SigBad` is a no-op (the flag is never set), and the function is marked with a `@warning` because forcibly declaring a transaction valid bypasses real verification — it exists to allow the transaction queue to mark locally-constructed transactions that were never signed by a remote peer. + +## Exception Safety and Fee Guarantee + +The `apply()` function guarantees it does not throw. For open ledgers, any exception inside a `Transactor` is caught and returned as `tefEXCEPTION`. For closed ledgers, the design goes further: even if the full application fails, the `Transactor` attempts to charge the fee and returns `tecFAILED_PROCESSING`. If even the fee-charge path throws, that exception is also caught and returned as `tefEXCEPTION`. This "best-effort fee deduction" is a network health requirement — a validator that silently drops a transaction without consuming a fee would allow fee-free spam vectors during consensus. + +## Batch Transaction Handling + +The implementation in `apply.cpp` exposes an `applyBatchTransactions()` static helper (not declared in the header) that handles the `ttBATCH` transaction type. When `applyTransaction()` sees a successfully applied `ttBATCH` outer transaction, it creates a whole-batch `OpenView` and applies each inner transaction through its own per-transaction `OpenView`. Only if an inner transaction's result is `tes` or `tec` (fee-claiming) are its view changes promoted to the batch view, and only if the batch as a whole succeeds do those changes promote into the main view. + +Batch mode flags (`tfAllOrNothing`, `tfUntilFailure`, `tfOnlyOne`) are read from the outer transaction's flags field and control short-circuit behavior during inner transaction iteration. + +## `applyTransaction()` — Retry Semantics for the Queue + +`applyTransaction()` wraps `apply()` with the retry/fail/success classification that the transaction queue needs. `tefFailure`, `temMalformed`, and `telLocal` errors map to `Fail` (no point retrying in this ledger); everything else that wasn't applied maps to `Retry`. This decoding is what allows the transaction queue to make intelligent decisions about eviction versus holding. + +The `retryAssured` parameter controls whether `tapRETRY` is added to the flags. With `tapRETRY` set, `tec` results are treated as soft failures rather than hard fee-claims, which affects whether `preclaim` reports `likelyToClaimFee` — a signal used upstream to decide whether a transaction is safe to relay without applying it to the open ledger first. \ No newline at end of file diff --git a/include/xrpl/tx/applySteps.h.ai.json b/include/xrpl/tx/applySteps.h.ai.json new file mode 100644 index 0000000000..387b9cf88d --- /dev/null +++ b/include/xrpl/tx/applySteps.h.ai.json @@ -0,0 +1,119 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "TER t", + "bool a", + "std::optional m = std::nullopt" + ], + "lineno": 10, + "name": "ApplyResult" + }, + { + "args": [ + "NotTEC pfResult", + "STTx const& tx", + "STTx const& tx, Category category", + "STTx const& tx, XRPAmount potentialSpend", + "STTx const& tx, std::uint32_t sequencesConsumed", + "TxConsequences const&", + "TxConsequences&&" + ], + "lineno": 36, + "name": "TxConsequences" + }, + { + "args": [ + "Context const& ctx_, std::pair const& result", + "PreflightResult const&" + ], + "lineno": 109, + "name": "PreflightResult" + }, + { + "args": [ + "Context const& ctx_, TER ter_", + "PreclaimResult const&" + ], + "lineno": 134, + "name": "PreclaimResult" + } + ], + "description": "Defines transaction preflight, preclaim, and application logic for the XRPL ledger, including result structures, fee calculations, and transaction consequence analysis.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/applySteps.h", + "functions": [ + { + "args": [ + "TER ter", + "ApplyFlags flags" + ], + "lineno": 22, + "name": "isTecClaimHardFail" + }, + { + "args": [ + "ServiceRegistry& registry", + "Rules const& rules", + "STTx const& tx", + "ApplyFlags flags", + "beast::Journal j" + ], + "lineno": 143, + "name": "preflight" + }, + { + "args": [ + "ServiceRegistry& registry", + "Rules const& rules", + "uint256 const& parentBatchId", + "STTx const& tx", + "ApplyFlags flags", + "beast::Journal j" + ], + "lineno": 150, + "name": "preflight" + }, + { + "args": [ + "PreflightResult const& preflightResult", + "ServiceRegistry& registry", + "OpenView const& view" + ], + "lineno": 172, + "name": "preclaim" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 194, + "name": "calculateBaseFee" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 210, + "name": "calculateDefaultBaseFee" + }, + { + "args": [ + "PreclaimResult const& preclaimResult", + "ServiceRegistry& registry", + "OpenView& view" + ], + "lineno": 232, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/applySteps.h.ai.md b/include/xrpl/tx/applySteps.h.ai.md new file mode 100644 index 0000000000..14e1579bc8 --- /dev/null +++ b/include/xrpl/tx/applySteps.h.ai.md @@ -0,0 +1,43 @@ +# `include/xrpl/tx/applySteps.h` + +This header defines the public interface for the XRPL transaction application pipeline — the structured sequence `preflight → preclaim → doApply` that every transaction must traverse before being committed to an open ledger. It is the architectural seam between the protocol's validation logic and the ledger's mutation layer. + +## The Three-Stage Pipeline + +The core design pattern in `applySteps.h` is the explicit decomposition of transaction processing into three sequential, independently cacheable stages. Each stage produces a typed result struct that must be passed to the next, making the pipeline both type-safe and auditable. + +**`preflight`** performs ledger-agnostic validation — format checks, signature structure, fee field sanity, and any static constraints that require only the transaction and current protocol rules. Critically, `preflight` produces a `TxConsequences` object alongside its `TER` result, which the Transaction Queue (TxQ) uses for worst-case XRP accounting before the transaction ever touches a ledger. The `preflight` result can be safely cached and reused across multiple ledger versions. There are two overloads: one for standard transactions and one that accepts a `parentBatchId`, used when a transaction belongs to a `Batch` transaction group. + +**`preclaim`** performs ledger-dependent validation — sequence/ticket checks, signature verification, fee sufficiency, and transaction-type-specific pre-conditions. Because ledger state can change between when `preflight` was cached and when `preclaim` runs, `preclaim` detects rules mismatches: if the rules embedded in `preflightResult` differ from the rules in the supplied `OpenView` (because the ledger advanced), `preclaim` automatically re-runs `preflight` with the updated rules before proceeding. This makes it safe for TxQ to hold stale `PreflightResult` values across ledger boundaries. + +**`doApply`** performs the actual ledger mutation. It only executes if `preclaimResult.likelyToClaimFee` is true — meaning the transaction either succeeded pre-checks (`tes`) or is a hard-fail `tec` that will charge a fee regardless. As a defensive measure, `doApply` checks that the view's sequence number matches what `preclaim` saw; if they differ, it returns `tefEXCEPTION` rather than applying to an inconsistent ledger snapshot. + +## `TxConsequences` — Pre-Application Cost Analysis + +`TxConsequences` answers a specific question the TxQ needs resolved before a transaction runs: *how much XRP can this transaction consume in the worst case?* This is not about the transaction's actual effects but its worst-case claim on the submitting account's balance, used to determine whether queued follow-on transactions remain viable. + +The class tracks five properties: whether the transaction is a `blocker`, the `fee_`, the `potentialSpend_` (XRP moved beyond the fee, e.g., in a Payment), a `SeqProxy` capturing the transaction's sequence or ticket, and `sequencesConsumed_` for transactions that burn multiple sequences (such as `TicketCreate`). The `followingSeq()` helper computes what sequence number should follow this transaction — essential for validating the queue ordering of subsequent transactions from the same account. + +The `blocker` category is a subtle but important flag. When `SetRegularKey` removes a regular key (or similar key-management operations run), subsequent queued transactions from that account may no longer be able to claim fees because their signature might become invalid. Marking a transaction as a blocker signals TxQ to stop processing further queued transactions from that account until the blocker is finalized. + +The constructors are deliberately split into five variants rather than using a single struct with defaulted fields. Each variant captures a specific invariant: the `NotTEC` constructor (for failed preflight) zeroes everything to ensure no cost estimate leaks from a rejected transaction; the category constructor delegates to the base and then flips `isBlocker_`; and the `potentialSpend` and `sequencesConsumed` constructors similarly extend the normal case. This prevents callers from accidentally constructing an inconsistent consequences object. + +## `PreflightResult` and `PreclaimResult` — Immutable Pipeline Tokens + +Both result structs make copy-assignment `= delete` while allowing copy-construction. This is a deliberate anti-tampering design noted in the comments: all fields are `const`, and the only way to obtain a valid result is to call the corresponding function. There is no way to construct a `PreflightResult` or `PreclaimResult` with arbitrary field values and pass it into the next stage — the template constructor pulls fields directly from the context object. + +`PreclaimResult` computes `likelyToClaimFee` in its constructor initializer list as `isTesSuccess(ter) || isTecClaimHardFail(ter, flags)`. This means the downstream `doApply` doesn't need to re-evaluate fee behavior — it simply reads this cached boolean. + +## `isTecClaimHardFail` — Soft vs. Hard Fee Claims + +A `tec` result is a protocol-level "claim a fee and fail" outcome — the transaction fails to execute but the network still charges the submitter. However, when `tapRETRY` is set in `ApplyFlags`, the TxQ is treating the transaction as a soft failure that might succeed later (e.g., after another transaction in the queue runs first). In that mode, a `tec` should not be treated as a definitive fee claim, because the transaction is not actually being applied yet. + +`isTecClaimHardFail` returns true when a `tec` will definitely result in a fee charge — i.e., the retry flag is absent. This inline predicate appears in `PreclaimResult`'s initializer and also in the TxQ's own fee reasoning, making it the authoritative definition of "this transaction will cost the submitter money." + +## Fee Calculation Utilities + +`calculateBaseFee` dispatches through the `with_txn_type` macro mechanism to the transaction-type-specific static method, returning the actual fee floor for that specific transaction. `calculateDefaultBaseFee` bypasses the type dispatch and calls `Transactor::calculateBaseFee` directly, returning what a plain "reference" transaction would cost. The TxQ uses this distinction when computing a transaction's fee *level* — the ratio of its actual fee to the reference fee — for prioritization and admission decisions. + +## Relationship to `apply.h` + +`apply.h` wraps the three-step pipeline into a single `apply()` call for callers that don't need intermediate results. `applySteps.h` exists because the TxQ is the principal reason to split the stages: it runs `preflight` once when a transaction arrives, caches the result, and may run `preclaim` and `doApply` much later (possibly after re-queuing across ledger closes). Without this split, the TxQ would need to re-validate transactions from scratch on every application attempt. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/AMMInvariant.h.ai.json b/include/xrpl/tx/invariants/AMMInvariant.h.ai.json new file mode 100644 index 0000000000..b02282cc6a --- /dev/null +++ b/include/xrpl/tx/invariants/AMMInvariant.h.ai.json @@ -0,0 +1,119 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "ValidAMM" + } + ], + "description": "Defines the ValidAMM class for validating Automated Market Maker (AMM) operations in the XRPL ledger, including methods for finalizing and checking invariants on AMM-related transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/AMMInvariant.h", + "functions": [ + { + "args": [], + "lineno": 17, + "name": "ValidAMM" + }, + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 20, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 23, + "name": "finalize" + }, + { + "args": [ + "bool", + "beast::Journal const&" + ], + "lineno": 27, + "name": "finalizeBid" + }, + { + "args": [ + "bool", + "beast::Journal const&" + ], + "lineno": 28, + "name": "finalizeVote" + }, + { + "args": [ + "STTx const&", + "ReadView const&", + "bool", + "beast::Journal const&" + ], + "lineno": 29, + "name": "finalizeCreate" + }, + { + "args": [ + "bool", + "TER", + "beast::Journal const&" + ], + "lineno": 30, + "name": "finalizeDelete" + }, + { + "args": [ + "STTx const&", + "ReadView const&", + "bool", + "beast::Journal const&" + ], + "lineno": 31, + "name": "finalizeDeposit" + }, + { + "args": [ + "STTx const&", + "ReadView const&", + "bool", + "beast::Journal const&" + ], + "lineno": 34, + "name": "finalizeWithdraw" + }, + { + "args": [ + "bool", + "beast::Journal const&" + ], + "lineno": 36, + "name": "finalizeDEX" + }, + { + "args": [ + "STTx const&", + "ReadView const&", + "ZeroAllowed", + "beast::Journal const&" + ], + "lineno": 37, + "name": "generalInvariant" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/AMMInvariant.h.ai.md b/include/xrpl/tx/invariants/AMMInvariant.h.ai.md new file mode 100644 index 0000000000..aa7f8fe779 --- /dev/null +++ b/include/xrpl/tx/invariants/AMMInvariant.h.ai.md @@ -0,0 +1,55 @@ +# `AMMInvariant.h` — Post-Transaction Validity Guard for AMM Ledger State + +## Role in the System + +`AMMInvariant.h` declares `ValidAMM`, one of many invariant-checker classes that the XRPL transaction engine runs as a last line of defense after applying every transaction. It lives in the `InvariantChecks` tuple defined in `InvariantCheck.h`, which is iterated uniformly after each transaction commits. Its sole job is to detect impossible or corrupt AMM state that should never arise from correct code — and, once the `fixAMMv1_3` amendment is active, to reject the transaction if that state is observed. + +## The Two-Phase Interface + +Like every invariant checker in the system, `ValidAMM` exposes exactly two public methods: `visitEntry` and `finalize`. The framework calls `visitEntry` once per modified ledger entry, then calls `finalize` to render a verdict. + +`visitEntry` collects the diff between ledger state before and after the transaction by inspecting the `before` and `after` SLE snapshots. It ignores deletions entirely and only records three pieces of state: + +- `ammAccount_` — the AMM's pseudo-account ID, populated when an `ltAMM` object is modified +- `lptAMMBalanceAfter_` / `lptAMMBalanceBefore_` — the LP token supply from the AMM object's `sfLPTokenBalance` field, snapped from both versions +- `ammPoolChanged_` — a flag set when any `ltRIPPLE_STATE` entry carrying `lsfAMMNode`, or any `ltACCOUNT_ROOT` with an `sfAMMID` field, is touched + +This compact snapshot is all that's needed to verify consistency across all AMM transaction types. + +## The Mathematical Core: `generalInvariant` + +The most important logic sits in the private `generalInvariant` method, used by both deposit and withdrawal paths. It re-reads the actual pool balances from the ledger via `ammPoolHolds`, then checks: + +``` +sqrt(amount × amount2) ≥ lptAMMBalanceAfter_ +``` + +This is the constant-product invariant: the geometric mean of pool reserves must never fall below the LP token supply. The check uses a strong comparison first, then falls back to `withinRelativeDistance` with a tolerance of `1e-11` to absorb floating-point rounding that can arise in fixed-point arithmetic. Crucially, logging for a failed `generalInvariant` is unconditional — it always emits a detailed error line including the transaction hash, individual pool amounts, geometric mean, LP token balance, and relative deviation — before the `enforce` flag gates whether to return `false`. + +## Per-Transaction Dispatch in `finalize` + +`finalize` first short-circuits on any failure result that isn't `tesSUCCESS` or `tecINCOMPLETE`. The `tecINCOMPLETE` carve-out exists because `AMMDelete` is allowed to return that code when there are too many trustlines to clean up in a single transaction, yet the partial deletion still needs validation. + +It then reads the `enforce` flag from `view.rules().enabled(fixAMMv1_3)`. Before this amendment, invariant failures are logged but the method still returns `true`, preserving backward compatibility. After the amendment, violations fail the transaction. The dispatch is a switch over the transaction type: + +- **`ttAMM_CREATE`** (`finalizeCreate`): Verifies that an AMM object was actually created, then re-derives the expected LP token balance as `sqrt(amount × amount2)` using `ammLPTokens` and asserts it matches the recorded balance. All three values (both pool amounts and LP supply) must be strictly positive. This is the only invariant using exact equality rather than a `≥` bound. + +- **`ttAMM_DEPOSIT`** (`finalizeDeposit`): Confirms the AMM object still exists, then delegates to `generalInvariant` with `ZeroAllowed::No`. A deposit can never produce a zero pool. + +- **`ttAMM_WITHDRAW` and `ttAMM_CLAWBACK`** (`finalizeWithdraw`): If `ammAccount_` is absent, the AMM was deleted by the last withdrawal — this is legitimate and the method returns `true` immediately. Otherwise it calls `generalInvariant` with `ZeroAllowed::Yes`, which additionally permits the all-zeros case (both pool amounts and LP supply simultaneously zero) as a valid terminal state during a full drain. + +- **`ttAMM_BID`** (`finalizeBid`): The pool itself must not change — bidding for the auction slot only burns LP tokens. If it did change, that's a violation. If the pool is untouched, it then verifies that `lptAMMBalanceAfter_ ≤ lptAMMBalanceBefore_` and that the post-bid balance is positive. LP tokens are consumed as the bid price, so they must decrease. + +- **`ttAMM_VOTE`** (`finalizeVote`): Both LP token balance and pool state must be unchanged. Voting only updates fee parameters on the AMM object; it must not touch reserves. + +- **`ttAMM_DELETE`** (`finalizeDelete`): On `tesSUCCESS`, `ammAccount_` must be absent (the object was deleted). On `tecINCOMPLETE`, the object must similarly not have been modified during the partial attempt. + +- **`ttCHECK_CASH`, `ttOFFER_CREATE`, `ttPAYMENT`** (`finalizeDEX`): DEX operations route through AMM pools but should never modify the `ltAMM` object itself. If `ammAccount_` is set, an object that should only be read was written — a serious bug. + +## Design Choices Worth Noting + +The `ZeroAllowed` scoped enum (`No` / `Yes`) instead of a bare `bool` makes the intent self-documenting at call sites, which matters because the two values represent meaningfully different economic scenarios. + +The `LCOV_EXCL_START/STOP` brackets around most failure branches signal that those code paths are considered unreachable under correct operation — they exist to catch implementation bugs, not expected error conditions, so test coverage tools rightly exclude them. The one exception is `finalizeCreate`'s error log, which lacks these guards and is therefore expected to be reachable in testing. + +The invariant's relationship to the broader `InvariantChecks` tuple is compositional and stateless at the framework level: each checker is constructed fresh per transaction, `visitEntry` accumulates deltas, and `finalize` renders its verdict. `ValidAMM` fits this pattern perfectly — its three `std::optional` members and one `bool` represent exactly the minimum diff state needed, with no heap allocation and no shared mutable state. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/FreezeInvariant.h.ai.json b/include/xrpl/tx/invariants/FreezeInvariant.h.ai.json new file mode 100644 index 0000000000..67f0ba4cf2 --- /dev/null +++ b/include/xrpl/tx/invariants/FreezeInvariant.h.ai.json @@ -0,0 +1,115 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 17, + "name": "TransfersNotFrozen" + }, + { + "args": [], + "lineno": 19, + "name": "BalanceChange" + }, + { + "args": [], + "lineno": 24, + "name": "IssuerChanges" + } + ], + "description": "Defines the TransfersNotFrozen invariant, which ensures that frozen trust line balances cannot be changed during a transaction. It tracks balance changes for affected trust lines and validates that no unauthorized changes occur if the trust line is frozen.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/FreezeInvariant.h", + "functions": [ + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 36, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 39, + "name": "finalize" + }, + { + "args": [ + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 44, + "name": "isValidEntry" + }, + { + "args": [ + "std::shared_ptr const&", + "std::shared_ptr const&", + "bool" + ], + "lineno": 47, + "name": "calculateBalanceChange" + }, + { + "args": [ + "Issue const&", + "BalanceChange" + ], + "lineno": 53, + "name": "recordBalance" + }, + { + "args": [ + "std::shared_ptr const&", + "STAmount const&" + ], + "lineno": 56, + "name": "recordBalanceChanges" + }, + { + "args": [ + "AccountID const&", + "ReadView const&" + ], + "lineno": 59, + "name": "findIssuer" + }, + { + "args": [ + "std::shared_ptr const&", + "IssuerChanges const&", + "STTx const&", + "beast::Journal const&", + "bool" + ], + "lineno": 62, + "name": "validateIssuerChanges" + }, + { + "args": [ + "BalanceChange const&", + "bool", + "STTx const&", + "beast::Journal const&", + "bool", + "bool" + ], + "lineno": 69, + "name": "validateFrozenState" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/FreezeInvariant.h.ai.md b/include/xrpl/tx/invariants/FreezeInvariant.h.ai.md new file mode 100644 index 0000000000..cc491d5579 --- /dev/null +++ b/include/xrpl/tx/invariants/FreezeInvariant.h.ai.md @@ -0,0 +1,61 @@ +# `FreezeInvariant.h` — Enforcing Frozen Trust Line Integrity + +## Role in the System + +`FreezeInvariant.h` declares the `TransfersNotFrozen` invariant checker, which ensures that no transaction can move token balances across frozen trust lines. It is one of roughly two dozen invariants registered in the `InvariantChecks` tuple (defined in `InvariantCheck.h`) and is executed as a post-transaction safety net after every transaction applied to the ledger. + +XRPL's invariant framework — described via the `InvariantChecker_PROTOTYPE` prototype in `InvariantCheck.h` — requires each checker to implement `visitEntry()` for streaming ledger-entry changes and `finalize()` to emit a pass/fail verdict. `TransfersNotFrozen` follows this contract exactly. Invariants are the *last line of defense*: they fire even on failed transactions, because a bug or exploit could mutate ledger state regardless of the transaction's declared result. + +## Why a Two-Phase Collect-Then-Validate Design + +The core insight driving the class's structure is stated in the implementation: + +> "A trust line freeze state alone doesn't determine if a transfer is frozen. The transfer must be examined end-to-end because both sides of the transfer may have different freeze states and freeze impact depends on the transfer direction." + +This makes a single-pass approach unworkable. During `visitEntry()`, the invariant can only observe individual trust line states; it cannot yet determine the full picture of what tokens moved where. So `visitEntry()` collects balance changes keyed by issuer and trust-line reference, deferring all policy decisions to `finalize()`. + +## Data Structures and Perspective Inversion + +The central state is `balanceChanges_`, a `ByIssuer` map keyed on `Issue` (a currency+account pair). For each issuer, an `IssuerChanges` record partitions changes into `senders` (trust lines with a decreasing balance) and `receivers` (trust lines with an increasing balance), each as `BalanceChange` structs pairing the trust line SLE with a sign value. + +A non-obvious subtlety in `recordBalanceChanges()` is that every trust line change is recorded *twice* — once for each side's perspective as an issuer. Because XRPL stores trust line balances from the "low" account's perspective, the sign is inverted when recording the entry under the high account's key: + +```cpp +recordBalance({currency, after->at(sfHighLimit).getIssuer()}, {after, balanceChangeSign}); +recordBalance({currency, after->at(sfLowLimit).getIssuer()}, {after, -balanceChangeSign}); +``` + +This ensures `validateIssuerChanges()` sees consistent, issuer-relative directionality regardless of which side of the trust line a given account sits on. + +A second map, `possibleIssuers_`, caches `ltACCOUNT_ROOT` entries observed during `visitEntry()`. Because `findIssuer()` first checks this cache before falling back to `view.read()`, the common case — where the issuer account was already touched by the transaction — avoids an extra ledger lookup. + +## When a Transfer Is Actually Frozen + +The key invariant rule, implemented in `validateIssuerChanges()`, is: + +- If `changes.senders` is empty, tokens are being issued from the issuer to holders. Allowed unconditionally. +- If `changes.receivers` is empty, holders are redeeming back to the issuer. Also allowed unconditionally. +- Only when *both* senders and receivers are present does the invariant check freeze state — this is a holder-to-holder transfer, and freeze rules apply. + +`validateFrozenState()` then evaluates three layered freeze conditions: +1. **Global freeze** (`lsfGlobalFreeze` on the issuer account): freezes all trust lines with that issuer universally. +2. **Deep freeze** (`lsfLowDeepFreeze`/`lsfHighDeepFreeze`): blocks all transfers regardless of direction for that specific trust line. +3. **Standard freeze** (`lsfLowFreeze`/`lsfHighFreeze`): only blocks outgoing transfers (`balanceChangeSign < 0`), allowing incoming tokens to still arrive. + +One carve-out exists for `AMMClawback` transactions: the `overrideFreeze` privilege (from `InvariantCheckPrivilege.h`) allows such transactions to move funds even across individually frozen or deep-frozen trust lines, but *not* when the issuer has set a global freeze. + +## Dynamic Trust Line Creation and Deletion + +`calculateBalanceChange()` handles two edge cases. When `before` is null (the trust line was created mid-transaction by a payment crossing offers), the pre-existing balance is treated as zero — the line had no prior balance to protect. When `isDelete` is true, the final balance is also treated as zero, correctly modeling the deletion as the balance "going to zero" and enforcing that even a trust-line deletion can't transfer frozen funds to a third party. + +## Amendment-Aware Enforcement with Early Warning + +The `enforce` flag in `finalize()` is controlled by the `featureDeepFreeze` amendment: + +```cpp +bool const enforce = view.rules().enabled(featureDeepFreeze); +``` + +Before the amendment activates, the invariant still runs and logs at `fatal` severity and fires `XRPL_ASSERT` in debug builds — but in release builds it does *not* return `false` and does not invalidate the transaction. The in-code comment explicitly documents the design intent: if an exploit allowing frozen-asset movement is discovered, the response is to log, alert operators monitoring fatal messages, and accelerate amendment activation (or introduce a new fix amendment). The enforcement switch is specifically wired to that amendment line to make the transition trivial. + +This pattern, also documented in `InvariantCheckPrivilege.h` as `assert(enforce)`, is a deliberate developer-facing tool: any code that violates this invariant in a development or test build without the amendment enabled will hit a debug assert, providing early-warning friction that is intentionally annoying so that bad code gets caught and fixed before mainnet deployment. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/InvariantCheck.h.ai.json b/include/xrpl/tx/invariants/InvariantCheck.h.ai.json new file mode 100644 index 0000000000..2b70f25d32 --- /dev/null +++ b/include/xrpl/tx/invariants/InvariantCheck.h.ai.json @@ -0,0 +1,96 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 38, + "name": "InvariantChecker_PROTOTYPE" + }, + { + "args": [], + "lineno": 97, + "name": "TransactionFeeCheck" + }, + { + "args": [], + "lineno": 117, + "name": "XRPNotCreated" + }, + { + "args": [], + "lineno": 143, + "name": "AccountRootsNotDeleted" + }, + { + "args": [], + "lineno": 170, + "name": "AccountRootsDeletedClean" + }, + { + "args": [], + "lineno": 202, + "name": "XRPBalanceChecks" + }, + { + "args": [], + "lineno": 224, + "name": "LedgerEntryTypesMatch" + }, + { + "args": [], + "lineno": 246, + "name": "NoXRPTrustLines" + }, + { + "args": [], + "lineno": 268, + "name": "NoDeepFreezeTrustLinesWithoutFreeze" + }, + { + "args": [], + "lineno": 290, + "name": "NoBadOffers" + }, + { + "args": [], + "lineno": 312, + "name": "NoZeroEscrow" + }, + { + "args": [], + "lineno": 334, + "name": "ValidNewAccountRoot" + }, + { + "args": [], + "lineno": 362, + "name": "ValidClawback" + }, + { + "args": [], + "lineno": 386, + "name": "ValidPseudoAccounts" + }, + { + "args": [], + "lineno": 408, + "name": "NoModifiedUnmodifiableFields" + } + ], + "description": "Defines a set of invariant checker classes for the XRPL transaction processing system, each enforcing a specific rule to ensure ledger integrity after transactions. Provides a tuple of all invariant checks and a prototype interface for documentation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/InvariantCheck.h", + "functions": [ + { + "args": [], + "lineno": 312, + "name": "getInvariantChecks" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/InvariantCheck.h.ai.md b/include/xrpl/tx/invariants/InvariantCheck.h.ai.md new file mode 100644 index 0000000000..c36d91b42c --- /dev/null +++ b/include/xrpl/tx/invariants/InvariantCheck.h.ai.md @@ -0,0 +1,66 @@ +# `include/xrpl/tx/invariants/InvariantCheck.h` + +## Role and Purpose + +This header is the central registry for the XRPL transaction invariant checking system. It exists to give the ledger a last line of defense: after every transaction has been applied (whether it succeeded or failed), a suite of invariant checkers scans the modified ledger entries and verifies that the result is internally consistent. If any check fails, the transaction is rolled back and replaced with a fee-only charge (`tecINVARIANT_FAILED`) or, if even that fails, excluded from the ledger entirely (`tefINVARIANT_FAILED`). + +The file declares the checker classes that live in this translation unit, then aggregates every checker — including those from sibling headers like `FreezeInvariant.h`, `NFTInvariant.h`, `AMMInvariant.h`, `VaultInvariant.h`, and others — into a single `std::tuple` alias called `InvariantChecks`. This tuple is the single source of truth for which invariants exist. + +## The Interface Contract — Duck-Typed, Not Virtual + +The system deliberately avoids inheritance. No base class, no vtable. Instead it relies on a duck-typed interface: every checker class must implement `visitEntry()` and `finalize()`. The `InvariantChecker_PROTOTYPE` class documents this interface but is guarded by `#if GENERATING_DOCS` — it is entirely absent at compile time. + +The two-phase execution model mirrors a streaming aggregation: + +- **Phase 1** — `visitEntry(isDelete, before, after)`: called once per modified ledger entry. Checkers use this to accumulate state (e.g., `XRPNotCreated` tracks a running `drops_` delta; `AccountRootsDeletedClean` records every deleted account root along with its before/after snapshots). +- **Phase 2** — `finalize(tx, tec, fee, view, j)`: called once per transaction after all entries have been visited. Returns `true` if the check passes, `false` if it fails. + +The actual dispatch happens in `ApplyContext::checkInvariantsHelper()`, which unpacks the tuple via `std::index_sequence` and fold expressions: + +```cpp +(..., std::get(checkers).visitEntry(isDelete, before, after)); +``` + +And for `finalize`, the results are collected into a `std::array` rather than short-circuiting with `&&`. This is intentional: every failed invariant should produce its own log entry at `fatal` level. Short-circuiting with a fold expression would silence all failures after the first. + +## Invariants Defined Locally + +The checkers declared directly in this file cover the core ledger properties: + +**`TransactionFeeCheck`** verifies that the fee charged is non-negative, less than the total XRP supply, and not greater than the fee the transaction itself authorized. Its `visitEntry()` is a no-op; all logic is in `finalize()`. + +**`XRPNotCreated`** tracks the net change in XRP across account roots, payment channels, and escrows. In `finalize()`, it verifies that the net change is exactly `-fee` — drops can only be destroyed as fees, never created. Payment channels and escrows have subtleties around deletion (the amount field isn't adjusted on delete), handled by skipping the `after` side when `isDelete` is true. + +**`XRPBalanceChecks`** sets a boolean `bad_` flag if any account root's XRP balance is outside `[0, INITIAL_XRP]`. The flag is sticky — once set it can't be cleared. + +**`AccountRootsNotDeleted`** counts deleted account roots. In `finalize()`, it uses `hasPrivilege()` (from `InvariantCheckPrivilege.h`) to distinguish: transactions with `mustDeleteAcct` (like `AccountDelete`) must delete exactly one; transactions with `mayDeleteAcct` (like `AMMWithdraw`) may delete at most one; all others must delete zero. + +**`AccountRootsDeletedClean`** stores before/after pairs for every deleted account root. In `finalize()`, it verifies the account had zero balance, zero owner count, and left behind no orphaned objects — including trust lines, escrows, offers, NFT pages, payment channels, and for pseudo-accounts, any linked protocol object (AMM, Vault, etc.). This checker is amendment-gated: it logs at fatal level regardless, but only returns `false` if one of `featureInvariantsV1_1`, `featureSingleAssetVault`, or `featureLendingProtocol` is enabled. + +**`LedgerEntryTypesMatch`** catches two categories of corruption: a modified entry whose type differs between `before` and `after`, and a newly created entry with an unrecognized type. The valid type list is generated via `ledger_entries.macro`. + +**`NoXRPTrustLines`** and **`NoDeepFreezeTrustLinesWithoutFreeze`** validate trust line properties. The former ensures no trust line references XRP as an asset (which would be nonsensical). The latter enforces that the `lsfLowDeepFreeze`/`lsfHighDeepFreeze` flags can only be set when the corresponding regular freeze flag is also set. + +**`NoBadOffers`** rejects offers with negative amounts or XRP-to-XRP exchange, both before and after modification. + +**`NoZeroEscrow`** (somewhat misnamed) validates that escrow amounts are strictly positive and within bounds. It also validates `ltMPTOKEN_ISSUANCE` and `ltMPTOKEN` entries for amount range and locked-amount consistency. + +**`ValidNewAccountRoot`** ensures that at most one account root is created per transaction, that it originates from a transaction with the correct privilege, starts with the right sequence number (current ledger sequence for normal accounts, zero for pseudo-accounts), and has the expected flags if it is a pseudo-account. + +**`ValidClawback`** is scoped to `ttCLAWBACK` transactions. On success, it confirms at most one trust line or MPToken was modified and that the holder's resulting balance is non-negative. On failure, it confirms nothing was modified at all. + +**`ValidPseudoAccounts`** enforces structural rules for pseudo-accounts (used by AMM and Vault): exactly one pseudo-account field must be set, the sequence number must not change, the correct flags must be present, and no regular key may exist. + +**`NoModifiedUnmodifiableFields`** checks that certain fields are never altered during modification. For `ltLOAN_BROKER` and `ltLOAN` entries, this covers a large set of creation-time fields like `sfInterestRate`, `sfBorrower`, etc. For all other entry types, `sfLedgerEntryType` and `sfLedgerIndex` are universally immutable. + +## The Privilege System + +`InvariantCheckPrivilege.h` defines a `Privilege` bitmask enum and `hasPrivilege(STTx const& tx, Privilege priv)`, which is implemented via `transactions.macro` — the same macro file used to enumerate all transaction types. Each transaction type carries a bitmask of its allowed privileges at the macro invocation site, so `hasPrivilege()` is essentially a compile-time-declared table of which operations each transaction type may legitimately perform. Invariant checkers call this function to distinguish legitimate operations from violations (e.g., distinguishing an `AccountDelete` from a bug that accidentally deletes an account). + +## Amendment Gating and the `assert(enforce)` Pattern + +Several checkers implement a soft-enforcement pattern: they log a `fatal` message and fire a debug-build `XRPL_ASSERT` when an invariant is violated, but only return `false` from `finalize()` when a relevant amendment is enabled. This allows early detection during development and testing without breaking network consensus on nodes that haven't enabled the amendment yet. The `assert(enforce)` is explicitly documented in `InvariantCheckPrivilege.h` as a developer-facing trap — it fires if an invariant is violated in a build that has the relevant amendments disabled, which is the typical state during development. + +## Extending the System + +Adding a new invariant requires two steps: declare the class (either in this file or a new sibling header) and add it to the `InvariantChecks` tuple. The tuple-based dispatch in `ApplyContext` is fully generic — it requires no other changes. The comment above the tuple definition makes this extensibility explicit. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h.ai.json b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h.ai.json new file mode 100644 index 0000000000..f5ed453770 --- /dev/null +++ b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h.ai.json @@ -0,0 +1,48 @@ +{ + "args": [ + { + "lineno": 49, + "name": "lhs" + }, + { + "lineno": 49, + "name": "rhs" + }, + { + "lineno": 57, + "name": "tx" + }, + { + "lineno": 57, + "name": "priv" + } + ], + "classes": [], + "description": "Defines transaction privilege flags and related utility functions for XRPL invariant checks.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/InvariantCheckPrivilege.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 49, + "name": "operator|" + }, + { + "args": [ + "tx", + "priv" + ], + "lineno": 57, + "name": "hasPrivilege" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/InvariantCheckPrivilege.h.ai.md b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h.ai.md new file mode 100644 index 0000000000..cb342fe0c3 --- /dev/null +++ b/include/xrpl/tx/invariants/InvariantCheckPrivilege.h.ai.md @@ -0,0 +1,81 @@ +# `InvariantCheckPrivilege.h` + +## Role in the System + +This header exists to answer a focused question that every invariant checker must ask: *what is this transaction type actually allowed to do?* The XRPL invariant checking system runs after every transaction is applied to detect impossible or forbidden ledger mutations — but not every mutation is forbidden to every transaction type. An `AccountDelete` must delete an account root; a `Payment` may create one; an ordinary `EscrowCreate` must never touch one. Without a way to query these per-type permissions, invariant checks would need hardcoded `switch` statements scattered across every checker that cares about transaction type, producing duplicated logic that drifts as new transaction types are added. + +`InvariantCheckPrivilege.h` solves this by centralising all transaction-type permissions into a single bitmask `Privilege` enum and a single query function `hasPrivilege()`. + +## The `Privilege` Enum + +`Privilege` is a plain `enum` (not `enum class`) whose enumerators are power-of-two bitmask values: + +```cpp +createAcct = 0x0001 // may create a new ACCOUNT_ROOT +createPseudoAcct = 0x0002 // may create a pseudo-account (implies createAcct) +mustDeleteAcct = 0x0004 // must delete an ACCOUNT_ROOT on success +mayDeleteAcct = 0x0008 // may optionally delete an ACCOUNT_ROOT +overrideFreeze = 0x0010 // may bypass certain freeze rules +changeNFTCounts = 0x0020 // may mint or burn an NFT +createMPTIssuance = 0x0040 // may create a new MPT issuance object +destroyMPTIssuance= 0x0080 // may destroy an MPT issuance +mustAuthorizeMPT = 0x0100 // must create or delete an MPT auth object +mayAuthorizeMPT = 0x0200 // may create or delete an MPT auth object +mayDeleteMPT = 0x0400 // may delete an MPT object (not create) +mustModifyVault = 0x0800 // must modify/create/delete a vault +mayModifyVault = 0x1000 // may modify/create/delete a vault +mayCreateMPT = 0x2000 // may create an MPT object (non-issuer) +``` + +The distinction between `must*` and `may*` variants is architecturally significant. An invariant checker examining account-root deletions (`AccountRootsNotDeleted`) must distinguish between a transaction that is *required* to delete exactly one account (e.g., `ttACCOUNT_DELETE` carries `mustDeleteAcct`) versus one that is *permitted* to delete one incidentally (e.g., `ttAMM_WITHDRAW` carries `mayDeleteAcct`). Conflating the two would produce either false failures or missed violations. + +## Composing Privileges with `operator|` + +Because `Privilege` is a plain `enum`, C++ does not automatically expose bitwise operators on it. The header provides a `constexpr operator|` that delegates through `safe_cast` to the underlying integer type and back: + +```cpp +constexpr Privilege operator|(Privilege lhs, Privilege rhs) +{ + return safe_cast( + safe_cast>(lhs) | + safe_cast>(rhs)); +} +``` + +`safe_cast` is used rather than a raw C-style cast to guard against accidental out-of-range integer conversions. The result is that privilege sets can be expressed naturally in `transactions.macro`: + +``` +ttPAYMENT → createAcct | mayCreateMPT +ttAMM_WITHDRAW → mayDeleteAcct | overrideFreeze | mayAuthorizeMPT +ttVAULT_CREATE → createPseudoAcct | createMPTIssuance | mustModifyVault +``` + +## How `hasPrivilege()` Is Implemented + +The function declared here is defined in `InvariantCheck.cpp` using a macro-expansion technique against `transactions.macro`. The macro file requires the caller to `#define TRANSACTION(tag, value, name, delegable, amendment, privileges, ...)` before including it. In `hasPrivilege`, the macro is redefined to emit a `switch` case that returns `(privileges) & priv` for each transaction type: + +```cpp +#define TRANSACTION(tag, value, name, delegable, amendment, privileges, ...) \ + case tag: { return (privileges) & priv; } + +bool hasPrivilege(STTx const& tx, Privilege priv) { + switch (tx.getTxnType()) { +#include + default: return false; + } +} +``` + +This design ensures that adding a new transaction type to `transactions.macro` with its privilege bitmask automatically makes `hasPrivilege()` correct for the new type without requiring any changes to the function body or any of the individual invariant checkers. The `default: return false` means unknown or deprecated transaction types carry no privileges, which is the safe conservative choice. + +## The `assert(enforce)` Pattern + +The file header documents a subtle convention used throughout the invariant implementation files. When an invariant fires, some checkers write `XRPL_ASSERT(enforce, ...)` where `enforce` is a boolean that is `true` only when the relevant amendment is active. This looks suspicious at first glance — why assert a variable rather than a condition? + +The rationale is a deliberate two-layer defence strategy. Invariant failures should *never* occur in production except in tests that deliberately corrupt the ledger to exercise the invariant machinery. The `assert(enforce)` fires only in debug builds and only when a developer has introduced code that violates an invariant *without* the protecting amendment being enabled. It is designed to be painful for developers while being invisible to validators — catching bugs early, in unit tests and debug builds, before they can reach consensus-critical paths. + +## Usage in Invariant Checkers + +`hasPrivilege()` is called in the `finalize()` methods of several invariant checkers defined in `InvariantCheck.h`. For example, `AccountRootsNotDeleted::finalize()` accepts the deletion of exactly one account root only when `hasPrivilege(tx, mustDeleteAcct)` returns true and the transaction succeeded; it accepts the optional deletion of one account root when `hasPrivilege(tx, mayDeleteAcct)` is set. Similarly, `ValidNewAccountRoot::finalize()` requires `hasPrivilege(tx, createAcct | createPseudoAcct)` before allowing a newly created account root to pass inspection. + +The privilege system thus acts as a declarative contract: each transaction type publishes what it is permitted to do, and the invariant checkers enforce that nothing else happens. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/LoanBrokerInvariant.h.ai.json b/include/xrpl/tx/invariants/LoanBrokerInvariant.h.ai.json new file mode 100644 index 0000000000..d98da98237 --- /dev/null +++ b/include/xrpl/tx/invariants/LoanBrokerInvariant.h.ai.json @@ -0,0 +1,55 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 17, + "name": "ValidLoanBroker" + }, + { + "args": [], + "lineno": 21, + "name": "BrokerInfo" + } + ], + "description": "Defines the ValidLoanBroker invariant check class for ensuring internal consistency of LoanBroker objects in the XRPL ledger, particularly regarding directory nodes and associated objects.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/LoanBrokerInvariant.h", + "functions": [ + { + "args": [ + "view", + "dir", + "j" + ], + "lineno": 32, + "name": "goodZeroDirectory" + }, + { + "args": [ + "", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 38, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 41, + "name": "finalize" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/LoanBrokerInvariant.h.ai.md b/include/xrpl/tx/invariants/LoanBrokerInvariant.h.ai.md new file mode 100644 index 0000000000..37b8f47867 --- /dev/null +++ b/include/xrpl/tx/invariants/LoanBrokerInvariant.h.ai.md @@ -0,0 +1,39 @@ +# `include/xrpl/tx/invariants/LoanBrokerInvariant.h` + +## Role in the System + +`LoanBrokerInvariant.h` declares `ValidLoanBroker`, one of the invariant checker classes registered in `InvariantChecks` (the master tuple in `InvariantCheck.h`). It is part of the XRPL Lending Protocol (XLS-0066) and runs unconditionally after every transaction, serving as a final correctness gate: if any `LoanBroker` ledger object is found to be internally inconsistent, the transaction is rolled back regardless of what the transaction logic returned. + +Like every invariant checker in the codebase, `ValidLoanBroker` exposes exactly two public methods — `visitEntry()` and `finalize()` — matching the `InvariantChecker_PROTOTYPE` interface. The infrastructure calls `visitEntry` once per modified ledger entry and then calls `finalize` exactly once to deliver a pass/fail verdict. + +## Key Design: Three-Channel Entry Collection + +The private state reveals how `ValidLoanBroker` handles the unusual topology of the Lending Protocol. During `visitEntry`, modified entries are sorted into three separate collections: + +- `brokers_` — a `std::map` keyed on the broker ledger index. Entries are added directly when a `ltLOAN_BROKER` SLE is touched, or indirectly when an `ltACCOUNT_ROOT` carrying `sfLoanBrokerID` is touched (pseudo-accounts representing the broker). +- `lines_` — modified `ltRIPPLE_STATE` (trust line) entries. +- `mpts_` — modified `ltMPTOKEN` entries. + +The trust lines and MPToken collections exist because a transaction may affect a broker's state without directly touching the `LoanBroker` SLE itself. In `finalize()`, both endpoints (high/low issuer) of each trust line are resolved via `view.read(keylet::account(...))`, and the MPToken's owner account is similarly resolved. If either endpoint carries `sfLoanBrokerID`, that broker is added to the `brokers_` map. This deferred discovery is necessary because the invariant must validate any broker implicated by a transaction, even if the broker object was only touched indirectly through associated trust lines or token balances. + +The `BrokerInfo` struct is minimal by design — it stores both the pre-transaction (`brokerBefore`) and post-transaction (`brokerAfter`) SLE snapshots. Only `brokerAfter` is used for absolute-value checks; `brokerBefore` is used for monotonicity checks that require a before/after comparison. + +## Invariants Enforced in `finalize()` + +Once all implicated brokers are gathered, `finalize()` performs several checks on each: + +**Zero-OwnerCount directory structure** — the most structurally unusual check. If `sfOwnerCount == 0`, the broker's owner directory must have at most a single root page with at most one entry, and that entry (if present) must be either a `ltRIPPLE_STATE` or `ltMPTOKEN`. This is enforced by the static helper `goodZeroDirectory()`, which reads `sfIndexNext` and `sfIndexPrevious` to confirm there is no chained page, then reads `sfIndexes` to verify the single-entry constraint, and finally resolves the referenced object to check its type. A broker with zero owner-count holding anything besides a trust line or token balance represents a structural corruption. + +**Sequence monotonicity** — `sfLoanSequence` must not decrease between transactions. Because `before` may be `nullptr` for newly-created brokers, this check is guarded. + +**Non-negative financial fields** — `sfDebtTotal` and `sfCoverAvailable` must each be ≥ 0. These represent the broker's outstanding debt and available coverage capacity; negative values would indicate arithmetic corruption. + +**Vault linkage** — every broker must reference a valid `ltVAULT` object via `sfVaultID`. A missing vault means the broker's backing store has been destroyed while the broker still exists, which is an illegal ledger state. + +**Cover–balance relationship** — `sfCoverAvailable` must not be less than the broker pseudo-account's actual asset balance (computed via `accountHolds`). When the `fixSecurity3_1_3` amendment is active, an additional upper-bound check is enforced: `sfCoverAvailable` must not exceed the pseudo-account balance, except during a `ttLOAN_BROKER_DELETE` transaction (where `sfCoverAvailable` is intentionally left un-zeroed at deletion). This two-sided bound is a security hardening: the available cover must precisely reflect what the pseudo-account actually holds. + +## Relationship to Sibling Invariants + +`ValidLoanBroker` is more structurally complex than its peers `ValidLoan` and `ValidVault`. `ValidLoan` uses a flat vector of before/after pairs and checks a single numeric invariant. `ValidVault` uses separate before/after vectors and cross-checks share and asset quantities. `ValidLoanBroker` uses a map keyed on broker ID because the same broker may be reached through multiple paths (direct SLE touch, pseudo-account modification, and trust line / MPToken resolution), and deduplication via `std::map::emplace` prevents double-checking. The `goodZeroDirectory` helper exists as a static method rather than inlined into `finalize` because the directory-page-chain traversal logic is non-trivial enough to warrant isolation and separate testing. + +The file itself is a thin declaration; all logic lives in `src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp`. The header exists so that `InvariantCheck.h` can include it alongside all other invariant checkers and add `ValidLoanBroker` to the `InvariantChecks` tuple without exposing implementation details to callers. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/LoanInvariant.h.ai.json b/include/xrpl/tx/invariants/LoanInvariant.h.ai.json new file mode 100644 index 0000000000..5defcb4a58 --- /dev/null +++ b/include/xrpl/tx/invariants/LoanInvariant.h.ai.json @@ -0,0 +1,41 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "ValidLoan" + } + ], + "description": "Defines the ValidLoan class, which enforces invariants to ensure loans are internally consistent, specifically that if PaymentRemaining is zero, PrincipalOutstanding must also be zero.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/LoanInvariant.h", + "functions": [ + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 22, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 25, + "name": "finalize" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/LoanInvariant.h.ai.md b/include/xrpl/tx/invariants/LoanInvariant.h.ai.md new file mode 100644 index 0000000000..f50ccd8335 --- /dev/null +++ b/include/xrpl/tx/invariants/LoanInvariant.h.ai.md @@ -0,0 +1,35 @@ +# `LoanInvariant.h` — ValidLoan Invariant Checker + +## Role in the System + +This header is part of the XRPL invariant-checking framework and defines `ValidLoan`, the post-transaction sanity checker for `ltLOAN` ledger entries introduced by the XLS-66 Lending Protocol amendment. The XRPL invariant system acts as a last line of defense: after every transaction is applied, a suite of checkers runs to ensure that ledger state has not drifted into an impossible or internally inconsistent configuration. `ValidLoan` is one entry in the `InvariantChecks` tuple declared in `InvariantCheck.h`, sitting alongside other domain-specific checkers like `ValidLoanBroker`, `ValidVault`, and `ValidAMM`. + +## The Two-Phase Checker Pattern + +All invariant checkers in this codebase share the same structural contract — two public methods that the framework calls in sequence: + +**`visitEntry(bool isDelete, before, after)`** is called once for every ledger object touched during transaction processing. `ValidLoan` uses this phase purely for data collection: if the post-transaction `SLE` exists and its type is `ltLOAN`, the `(before, after)` pair is pushed into `loans_`. The `before` pointer can be null (for newly created objects), and the `after` pointer can be null for deletions — the implementation checks both before dereferencing. + +**`finalize(tx, tec, fee, view, journal)`** is called once after all entries have been visited. This is where all actual validation logic runs. The method iterates over the collected `loans_` pairs and enforces each invariant in turn, returning `false` and logging a `fatal`-level journal message on any violation. + +The decision to collect entries in `visitEntry` and reason about them in `finalize` follows the pattern used consistently across every checker in this module. It separates data-gathering (which must be cheap and stateless per-entry) from validation (which may need to reason across multiple related entries or read the full ledger view). + +## Invariants Enforced + +The implementation checks four categories of constraint, all grounded in the XLS-66 spec (referenced explicitly in the source via a link to the XRPL Standards repo): + +**Payment-completion consistency**: If `sfPaymentRemaining` is zero, the loan must be fully settled — meaning `sfTotalValueOutstanding`, `sfPrincipalOutstanding`, and `sfManagementFeeOutstanding` must all be zero. The inverse is also checked: if any of those outstanding amounts are zero but `sfPaymentRemaining` is non-zero, that is equally invalid. These two symmetrical checks together enforce a strict bijection between the payment schedule being exhausted and the economic balance being cleared. + +**Immutability of the overpayment flag**: The `lsfLoanOverpayment` flag, once set or cleared, must not change during a transaction. This guards against any processing path that might inadvertently flip this flag as a side-effect. + +**Non-negative fee and balance fields**: Six `STNumber` fields — `sfLoanServiceFee`, `sfLatePaymentFee`, `sfClosePaymentFee`, `sfPrincipalOutstanding`, `sfTotalValueOutstanding`, and `sfManagementFeeOutstanding` — are checked to ensure they cannot fall below zero. Since `STNumber` can represent signed arbitrary-precision values, this guard is non-trivial. + +**Strictly positive periodic payment**: `sfPeriodicPayment` must be greater than zero. A zero or negative periodic payment would represent a structurally malformed loan that could produce undefined amortization behavior. + +## Design Observations + +The `loans_` member stores `std::pair` — shared pointers to the pre- and post-state of each loan. The `before` pointer is used only for the overpayment flag comparison (since that check is explicitly about change detection rather than absolute state). All other checks operate on `after`, meaning they validate the ledger state as it would be committed, not the intermediate diff. + +The `finalize` method ignores the transaction type (`STTx`), the fee (`XRPAmount`), and the `ReadView` entirely — a signal that loan consistency can be verified locally from the collected objects alone, without requiring lookup into the broader ledger. This is intentional: any information needed from the ledger should be collected during `visitEntry` when the framework walks the modified set. + +The comment at the top of `finalize` — "Loans will not exist on ledger if the Lending Protocol amendment is not enabled" — documents why there is no amendment guard around the logic itself. If the amendment is off, `visitEntry` will never collect any `ltLOAN` entries, so the loop is trivially empty and `finalize` returns `true` immediately. The checker is safe to include unconditionally in `InvariantChecks`. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/MPTInvariant.h.ai.json b/include/xrpl/tx/invariants/MPTInvariant.h.ai.json new file mode 100644 index 0000000000..43c4121dac --- /dev/null +++ b/include/xrpl/tx/invariants/MPTInvariant.h.ai.json @@ -0,0 +1,66 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "ValidMPTIssuance" + }, + { + "args": [], + "lineno": 22, + "name": "ValidMPTPayment" + } + ], + "description": "Defines invariant check classes for MPT (Multi-Party Token) issuance and payment in the XRPL, ensuring internal consistency and validity of MPT-related ledger changes.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/MPTInvariant.h", + "functions": [ + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 15, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 18, + "name": "finalize" + }, + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 41, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 44, + "name": "finalize" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/MPTInvariant.h.ai.md b/include/xrpl/tx/invariants/MPTInvariant.h.ai.md new file mode 100644 index 0000000000..c54d17dc55 --- /dev/null +++ b/include/xrpl/tx/invariants/MPTInvariant.h.ai.md @@ -0,0 +1,50 @@ +# `MPTInvariant.h` — Invariant Checkers for Multi-Purpose Token Ledger Consistency + +This header declares two invariant checker classes, `ValidMPTIssuance` and `ValidMPTPayment`, that guard the XRPL ledger's MPT (Multi-Purpose Token) subsystem from state corruption. Both classes plug into the general-purpose invariant framework defined in `InvariantCheck.h` and are included in the `InvariantChecks` tuple that the engine iterates after every transaction application. + +## The Invariant Framework Pattern + +Every invariant checker in the XRPL engine exposes the same two-phase contract: `visitEntry()` is called once per modified `SLE` (Signed Ledger Entry) during transaction application, and `finalize()` is called once after all entries have been visited to render a pass/fail verdict. The split exists because invariants must reason about the *aggregate* effect of a transaction — you cannot tell if MPT issuance counts are correct by looking at one ledger entry in isolation. + +Critically, `finalize()` must perform meaningful checks even when the transaction failed. A bug or exploit could cause a failed transaction to mutate ledger state in unexpected ways, so invariants are the last line of defence. Both classes in this file respect that contract. + +## `ValidMPTIssuance` — Structural Integrity of MPT Lifecycle Objects + +`ValidMPTIssuance` accumulates four counters during `visitEntry()`: + +- `mptIssuancesCreated_` / `mptIssuancesDeleted_` count `ltMPTOKEN_ISSUANCE` entries. +- `mptokensCreated_` / `mptokensDeleted_` count `ltMPTOKEN` holder entries. +- `mptCreatedByIssuer_` flags the edge case where an `MPToken` was auto-created for the issuance's own issuer account, which is always an error. + +In `finalize()`, the class consults the transaction's *privilege* flags (from `InvariantCheckPrivilege.h`) to determine exactly what structural changes were permitted. The privilege system is a bitmask enum: `createMPTIssuance`, `destroyMPTIssuance`, `mustAuthorizeMPT`, `mayAuthorizeMPT`, `mayCreateMPT`, and `mayDeleteMPT` encode what each transaction type is *allowed* to do to MPT ledger objects. + +The invariant then enforces tight accounting rules: + +- A transaction with `createMPTIssuance` privilege must have created exactly one `ltMPTOKEN_ISSUANCE` and deleted zero. +- A transaction with `destroyMPTIssuance` privilege must have deleted exactly one and created zero. +- A transaction with `mustAuthorizeMPT` (submitted by a holder) must have created or deleted exactly one `ltMPTOKEN`. +- Transactions with `mayAuthorizeMPT` (e.g., `ttAMM_WITHDRAW`, `ttAMM_CLAWBACK`) are subject to more permissive but still bounded limits: at most one `ltMPTOKEN` created, at most two deleted (because an empty two-asset AMM pool can shed both holder objects on withdrawal). +- Transactions with `mayCreateMPT` — including `ttAMM_CREATE` (up to two, for an MPT/MPT pool) and `ttCHECK_CASH` (at most one) — may auto-create `ltMPTOKEN` entries for receiving accounts that didn't already hold the token. +- Any transaction that has none of these privileges must have left all MPT object counts at zero. + +An important nuance is amendment gating. Several checks only *enforce* (i.e., return `false`) when specific feature flags such as `featureMPTokensV2`, `featureSingleAssetVault`, or `featureLendingProtocol` are enabled. Before these amendments activate, a failing assertion is still logged at fatal severity, but the transaction is not rejected. The `assert(enforce)` pattern documented in `InvariantCheckPrivilege.h` exploits the fact that asserts fire in debug/test builds but not production, providing an early-warning system for developers working on pre-amendment code paths. + +## `ValidMPTPayment` — Conservation of Outstanding Amounts + +`ValidMPTPayment` enforces the fundamental accounting invariant for MPT value flows: after any successful transaction, the `OutstandingAmount` field on each `ltMPTOKEN_ISSUANCE` must equal the sum of all individual `ltMPTOKEN` balances (the `MPTAmount` field plus the `LockedAmount` field for each holder entry). + +The class stores this as a `hash_map` keyed by MPT ID (`uint192` being the 192-bit issuance identifier). Each `MPTData` holds a two-element array for the before/after `OutstandingAmount` snapshot and a signed `mptAmount` accumulator representing the net delta across all holder `ltMPTOKEN` entries (`after - before`, summed). + +The conservation equation verified in `finalize()` is: + +``` +OutstandingAmount[After] == OutstandingAmount[Before] + sum(MPTAmount[After] - MPTAmount[Before]) +``` + +Overflow is treated as a first-class concern. Because MPT amounts can be up to `maxMPTokenAmount` (a large 64-bit quantity), the code checks for overflow at every arithmetic step during `visitEntry()` and sets the `overflow_` flag rather than risking undefined behaviour. In `finalize()`, an overflow immediately fails the invariant with a fatal log message. The enforcement is again amendment-gated: the check only hard-fails (returns `false`) when `featureMPTokensV2` is enabled. + +Unlike `ValidMPTIssuance`, the `finalize()` method on `ValidMPTPayment` is non-`const` — a consequence of the hash-map accumulator being mutated lazily during visit rather than being fully pre-built. + +## Relationship to the Broader Invariant System + +`ValidMPTIssuance` appears near the middle of the `InvariantChecks` tuple in `InvariantCheck.h`, while `ValidMPTPayment` is the last entry. This ordering matters because the engine runs all checks unconditionally and any failure causes the transaction to be reverted. The MPT issuance structural check and the MPT payment conservation check are intentionally separate classes rather than one combined checker because they address orthogonal concerns: one guards *object lifecycle* (did the right ledger entries appear or disappear?), while the other guards *numeric conservation* (are token balances consistent with the issuance's outstanding total?). Splitting them also keeps each class small and its logic straightforward to audit. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/NFTInvariant.h.ai.json b/include/xrpl/tx/invariants/NFTInvariant.h.ai.json new file mode 100644 index 0000000000..276a82a9e4 --- /dev/null +++ b/include/xrpl/tx/invariants/NFTInvariant.h.ai.json @@ -0,0 +1,66 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 19, + "name": "ValidNFTokenPage" + }, + { + "args": [], + "lineno": 46, + "name": "NFTokenCountTracking" + } + ], + "description": "Defines invariants for XRPL NFTokens, including checks for valid NFToken pages and tracking of minted/burned NFTokens across transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/NFTInvariant.h", + "functions": [ + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 32, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 35, + "name": "finalize" + }, + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 61, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 64, + "name": "finalize" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/NFTInvariant.h.ai.md b/include/xrpl/tx/invariants/NFTInvariant.h.ai.md new file mode 100644 index 0000000000..2056c0cbf9 --- /dev/null +++ b/include/xrpl/tx/invariants/NFTInvariant.h.ai.md @@ -0,0 +1,48 @@ +# `NFTInvariant.h` — NFToken Post-Transaction Integrity Guards + +This header declares two invariant checker classes that the XRPL transaction engine runs unconditionally after every transaction applies to the ledger. They are part of the broader invariant-checking framework defined in `InvariantCheck.h`, which assembles all checkers into a `std::tuple<..., ValidNFTokenPage, NFTokenCountTracking, ...>` and drives them through a uniform `visitEntry` / `finalize` protocol. + +The two-phase design is deliberate: because a transaction can touch an arbitrary number of ledger entries, invariant checkers accumulate observations during `visitEntry` calls (one call per modified `SLE`) and only render a verdict in `finalize` once the full picture is available. Both classes store only small, flat state — a handful of `bool` flags or `uint32_t` counters — making them cheap to run on every transaction regardless of outcome. + +## `ValidNFTokenPage` + +NFToken pages (`ltNFTOKEN_PAGE`) use a composite 256-bit key: the owning account occupies the high 160 bits, and the low 96 bits represent the page's *high limit* — the exclusive upper bound on which NFToken IDs (by their own low 96 bits) belong on that page. The `nft::pageMask` constant (`0x...ffffffffffffffffffffffff`) cleanly separates these two parts. + +`visitEntry` ignores entries that are not `ltNFTOKEN_PAGE` and applies a `check` lambda to both the `before` and `after` SLE states when they exist. This dual inspection matters: it catches not just a corrupted write but also confirms that entries which already existed on disk satisfy the structural invariants. + +**Linking checks** (`badLink_`): `sfPreviousPageMin` and `sfNextPageMin` links are verified in both directions. For each neighbour link, the checker masks out the account bits and confirms they match the current page's account — a cross-account link is impossible by design and would indicate serious corruption. It also enforces page ordering: `prev.hiLimit < current.hiLimit < next.hiLimit`, because the NFToken directory is a doubly-linked sorted list of pages. + +**Size checks** (`invalidSize_`): A page must contain at least one and at most `dirMaxTokensPerPage` (32) NFTokens. The only exception is during deletion (`isDelete == true`), where a page being removed may legitimately be empty. + +**Placement checks** (`badEntry_`): Each NFToken's ID, masked to its low 96 bits, must fall within `[loLimit, hiLimit)` of the page. `loLimit` is derived from `sfPreviousPageMin`'s low 96 bits, or zero if the page has no predecessor. An NFToken found outside this range would mean the page-splitting/merging logic placed a token on the wrong page. + +**Sort checks** (`badSort_`): Tokens within a page must be in ascending order under `nft::compareTokens()`. The check maintains a running lower bound `loCmp`, starting at `loLimit`, and flags any token that is not strictly greater. + +**URI checks** (`badURI_`): If a token carries an `sfURI` field, that field must be non-empty. An empty URI is explicitly prohibited by the NFToken specification. + +**Amendment-gated checks**: Two additional invariants are enabled only when the `fixNFTokenPageLinks` amendment is active, reflecting that these checks address a historical bug rather than a founding invariant: + +- `deletedFinalPage_`: The "final" page of an account's NFToken directory is identified by its low 96 bits all being `1` (equal to `nft::pageMask`). This page must not be deleted while `sfPreviousPageMin` still exists — doing so would orphan all earlier pages in the directory. + +- `deletedLink_`: If a non-final page transitions from having `sfNextPageMin` to not having it (without being deleted), a forward link has been silently lost, which would break forward traversal of the directory. + +Gating these behind the amendment avoids penalising historical ledger states where the bug may already have occurred while still protecting future transactions once the fix is deployed. + +## `NFTokenCountTracking` + +Every `ltACCOUNT_ROOT` entry carries two optional counters: `sfMintedNFTokens` and `sfBurnedNFTokens`. `NFTokenCountTracking` sums these fields across all account roots touched by the transaction — both the `before` state (summed into `beforeMintedTotal`/`beforeBurnedTotal`) and the `after` state (summed into `afterMintedTotal`/`afterBurnedTotal`). Absent fields are treated as zero via `.value_or(0)`. + +The `finalize` logic branches on whether the transaction holds the `changeNFTCounts` privilege (declared in `InvariantCheckPrivilege.h` as bit `0x0020`). Transactions without this privilege — the vast majority — must leave both totals completely unchanged. This is a strong guard against any code path that accidentally modifies mint/burn counters as a side effect. + +For `ttNFTOKEN_MINT`, three rules apply: +- A successful mint (`isTesSuccess(result)`) must strictly increase `afterMintedTotal` beyond `beforeMintedTotal`. +- A failed mint must not change `afterMintedTotal` at all. +- Neither success nor failure may change `afterBurnedTotal`. + +For `ttNFTOKEN_BURN`, a symmetric set of rules applies with the burned and minted roles swapped. + +The asymmetric treatment of success vs. failure is important: the invariant system intentionally checks failed transactions. A bug or exploit that mutates counters during a failed transaction would still be caught here, before the corrupted state is committed to the ledger. + +## Relationship to the Broader Invariant System + +Both classes conform to the `InvariantChecker_PROTOTYPE` interface documented in `InvariantCheck.h`. The engine instantiates all checkers as value types in a tuple, calls `visitEntry` for every modified SLE via tuple iteration, then calls `finalize` on each. A `false` return from any checker causes the transaction to be rolled back with a `tecINVARIANT_FAILED` error code, making these checks a last-resort safety net that fires after all normal transaction logic has completed. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h.ai.json b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h.ai.json new file mode 100644 index 0000000000..d1b7fb3bf8 --- /dev/null +++ b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h.ai.json @@ -0,0 +1,41 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 9, + "name": "ValidPermissionedDEX" + } + ], + "description": "Defines the ValidPermissionedDEX class, which checks invariants related to permissioned decentralized exchanges (DEX) in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/PermissionedDEXInvariant.h", + "functions": [ + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 15, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 18, + "name": "finalize" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/PermissionedDEXInvariant.h.ai.md b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h.ai.md new file mode 100644 index 0000000000..2a609876e0 --- /dev/null +++ b/include/xrpl/tx/invariants/PermissionedDEXInvariant.h.ai.md @@ -0,0 +1,46 @@ +# `PermissionedDEXInvariant.h` — Permissioned DEX Invariant Checker + +## Role in the System + +This header declares `ValidPermissionedDEX`, one entry in the `InvariantChecks` tuple registered in `InvariantCheck.h`. The XRPL transaction engine runs every invariant checker in that tuple as a post-processing safety net after applying each transaction. If any checker's `finalize()` returns `false`, the engine vetoes the transaction result and marks it with a fatal error, preventing corrupt state from being committed to the ledger. + +`ValidPermissionedDEX` enforces the isolation contract of the Permissioned DEX feature: a transaction that specifies a `sfDomainID` must interact exclusively with offers and order book directories tied to that exact domain. It also validates the structural integrity of "hybrid" offers, which participate simultaneously in a domain-specific order book and the global book. + +## Two-Phase Visitor Pattern + +Like all invariant checkers, `ValidPermissionedDEX` follows the standard two-phase protocol defined by `InvariantChecker_PROTOTYPE`: + +**Phase 1 — `visitEntry()`**: Called once for every `SLE` (Serialized Ledger Entry) touched by the transaction, providing snapshots before and after modification. The implementation accumulates three pieces of evidence: + +- For any `ltDIR_NODE` or `ltOFFER` entry that carries `sfDomainID`, the domain hash is recorded into `domains_` — a `hash_set`. +- For any `ltOFFER` without `sfDomainID`, `regularOffers_` is set to `true`. +- For any `ltOFFER` bearing the `lsfHybrid` flag, the offer is validated for structural completeness: a hybrid offer must carry both `sfDomainID` and `sfAdditionalBooks`, and `sfAdditionalBooks` must contain at most one entry. Violating any of these sets `badHybrids_` to `true`. + +**Phase 2 — `finalize()`**: Runs once after all entries have been visited, with access to the full transaction and its result. The check only fires for successful (`isTesSuccess`) `ttPAYMENT` and `ttOFFER_CREATE` transactions. If the transaction itself carries no `sfDomainID`, the check passes immediately — normal (unpermissioned) transactions are not constrained here. + +For domain-tagged transactions, three additional assertions are enforced: + +1. **Hybrid integrity**: If `badHybrids_` was set during visiting, the invariant fails. +2. **Domain existence**: The domain referenced by the transaction must exist as a `keylet::permissionedDomain` ledger object. This guards against a scenario where a transaction successfully uses a domain that was deleted by the same batch. +3. **Domain isolation**: Every domain hash collected in `domains_` must equal the transaction's own domain. Any foreign domain appearing in touched entries means the transaction bled into the wrong order book. +4. **No regular-offer contamination**: If `regularOffers_` is true, a domain-tagged transaction illegally touched an unpermissioned offer, and the invariant fails. + +## Design Decisions + +**Why collect domains in a `hash_set` rather than a counter?** The check requires knowing the exact identity of each touched domain, not just a count. Collecting distinct hashes allows a single loop at finalize time to confirm all of them match the transaction's own domain — a domain mismatch is caught regardless of how many foreign domains appear. + +**Why skip failed transactions?** Unlike some invariants that must fire on failure (as documented in `InvariantCheck.h`'s `InvariantChecker_PROTOTYPE`), permissioned DEX isolation is only meaningful for state changes that actually took effect. A failed payment cannot have modified any offers, so there is nothing to check. This is an intentional deviation from the general guidance, appropriate because the invariant's purpose is to verify the result of a committed mutation rather than the absence of one. + +**Why is `sfAdditionalBooks` size capped at 1?** Hybrid offers bridge a domain-specific book and one additional book. Allowing more than one additional book entry would indicate a malformed or maliciously crafted offer that the transaction processor should never have produced; the invariant catches it as a last resort. + +## State Members + +| Member | Type | Purpose | +|---|---|---| +| `regularOffers_` | `bool` | Set if any visited `ltOFFER` lacks `sfDomainID` | +| `badHybrids_` | `bool` | Set if any visited hybrid offer is structurally malformed | +| `domains_` | `hash_set` | All distinct domain IDs observed on touched offers and directory nodes | + +## Relationship to Sibling Invariants + +`ValidPermissionedDomain` (declared in the companion header `PermissionedDomainInvariant.h`) checks structural validity of the `PermissionedDomain` ledger object itself — credential list length, sorting, and uniqueness. `ValidPermissionedDEX` is complementary: it operates one layer up, ensuring that order-book activity respects domain boundaries during trade execution rather than checking the domain object's own structure. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/PermissionedDomainInvariant.h.ai.json b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h.ai.json new file mode 100644 index 0000000000..4dd28f0c20 --- /dev/null +++ b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h.ai.json @@ -0,0 +1,46 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 16, + "name": "ValidPermissionedDomain" + }, + { + "args": [], + "lineno": 18, + "name": "SleStatus" + } + ], + "description": "Defines the ValidPermissionedDomain invariant check for permissioned domains, ensuring that AcceptedCredentials have a length between 1 and 10, are sorted, unique, and not empty.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/PermissionedDomainInvariant.h", + "functions": [ + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 29, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 32, + "name": "finalize" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/PermissionedDomainInvariant.h.ai.md b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h.ai.md new file mode 100644 index 0000000000..96837aa3f5 --- /dev/null +++ b/include/xrpl/tx/invariants/PermissionedDomainInvariant.h.ai.md @@ -0,0 +1,47 @@ +# `PermissionedDomainInvariant.h` — Invariant Guard for Permissioned Domain Objects + +## Role in the System + +This header declares `ValidPermissionedDomain`, one of the invariant checkers registered in the `InvariantChecks` tuple in `InvariantCheck.h`. The XRPL invariant-checking framework runs every registered checker after every transaction application, regardless of whether the transaction succeeded or failed. If any checker returns `false`, the ledger mutation is rolled back and the node hard-fails to avoid propagating corrupt state. + +`ValidPermissionedDomain` enforces the structural integrity of `ltPERMISSIONED_DOMAIN` ledger entries, specifically the `sfAcceptedCredentials` array that defines which credential types are accepted by a domain. These structural guarantees are foundational: downstream logic that iterates the credentials array (for example, `credentials::validDomain`) depends on them to be non-empty, de-duplicated, and lexicographically sorted without re-validating on every access. + +## The Two-Phase Visitor Pattern + +Like all XRPL invariant checkers, `ValidPermissionedDomain` follows a two-phase pattern declared in `InvariantChecker_PROTOTYPE`: + +**Phase 1 — `visitEntry`:** Called once per modified ledger entry during the transaction's journal replay. For any SLE that is not of type `ltPERMISSIONED_DOMAIN`, the method returns immediately. For matching entries it records an `SleStatus` snapshot of the *post-modification* (`after`) state: how many credentials exist, whether they are free of duplicates (via `credentials::makeSorted`, which returns empty on duplicates), whether the existing order matches the canonical sorted order, and whether this is a deletion. All snapshots accumulate in `sleStatus_`, a `std::vector`. + +**Phase 2 — `finalize`:** Called once after all entries are visited. It interprets the collected `SleStatus` observations in the context of the full transaction (`STTx`) and its result code (`TER`). Only here does the invariant actually pass or fail. + +## The `SleStatus` Inner Struct + +The private `SleStatus` struct is a compact result bundle for a single ledger-entry observation: + +- `credentialsSize_` — raw credential count, used to detect empty (0) or oversized (> `maxPermissionedDomainCredentialsArraySize`, which is 10) arrays. +- `isSorted_` — true only when the iteration order of `sfAcceptedCredentials` matches the order produced by `credentials::makeSorted`; false immediately if any element is out of place. +- `isUnique_` — true when `makeSorted` returned a non-empty set (it returns empty to signal duplicates). +- `isDelete_` — propagates the `isDel` flag from `visitEntry` so `finalize` can distinguish a create/update from a deletion. + +The design choice to pre-compute `isSorted_` and `isUnique_` in `visitEntry` rather than in `finalize` keeps the final validation logic simple and avoids re-reading the SLE through `ReadView` a second time. The sorted-check short-circuits on the first out-of-order pair, making it O(n) rather than a full re-sort. + +## `finalize` Behavior and the Feature-Flag Split + +The most architecturally significant aspect of `finalize` is its branch on `view.rules().enabled(fixPermissionedDomainInvariant)`. + +**Without the amendment (legacy path):** The check is narrow — it only fires for a successful `ttPERMISSIONED_DOMAIN_SET` that touched at least one domain entry, validating the four credential constraints. Any other transaction type, or any failed transaction, is silently passed (`return true`). This matches the original, conservative scope. + +**With the amendment (strict path):** The invariant becomes substantially more comprehensive: +- A failed transaction must not have mutated any domain entries at all (`sleStatus_.empty()` on failure). +- No transaction may ever modify more than one domain entry in a single application. +- A `ttPERMISSIONED_DOMAIN_SET` must create or modify (not delete) exactly one domain entry, and that entry must satisfy all four credential constraints. +- A `ttPERMISSIONED_DOMAIN_DELETE` must delete exactly one domain entry and must not modify it. +- Any other transaction type that touches a domain entry is flagged as unauthorized. + +This bifurcation follows the standard XRPL amendment pattern: the amendment locks in the stricter invariant network-wide once a supermajority of validators enable it, while nodes running pre-amendment rules still validate correctly on pre-amendment ledgers. The test suite (`Invariants_test.cpp`) exercises both branches explicitly by toggling `fixPermissionedDomainInvariant` in the feature set. + +## Relationship to Adjacent Code + +`credentials::makeSorted` (from `CredentialHelpers.h`) is the shared utility that both the transactor (`PermissionedDomainSet`) and this invariant rely on to define canonical sort order. The transactor calls `credentials::checkArray` during `preclaim`/`doApply` to reject malformed submissions early; the invariant re-checks the persisted state as a last-resort defense, ensuring that no code path — including a future bug in the transactor — can write an invalid domain object into a finalized ledger. + +The constant `maxPermissionedDomainCredentialsArraySize = 10` is defined in `Protocol.h` and shared across the transactor, the invariant, and the test suite, making it the single source of truth for the capacity limit. \ No newline at end of file diff --git a/include/xrpl/tx/invariants/VaultInvariant.h.ai.json b/include/xrpl/tx/invariants/VaultInvariant.h.ai.json new file mode 100644 index 0000000000..5c7a55f63e --- /dev/null +++ b/include/xrpl/tx/invariants/VaultInvariant.h.ai.json @@ -0,0 +1,51 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 27, + "name": "ValidVault" + }, + { + "args": [], + "lineno": 33, + "name": "Vault" + }, + { + "args": [], + "lineno": 46, + "name": "Shares" + } + ], + "description": "Defines the ValidVault invariant check for XRPL, ensuring the consistency and correctness of Vault objects and MPTokenIssuance for vault shares during ledger transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/invariants/VaultInvariant.h", + "functions": [ + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 54, + "name": "visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const&" + ], + "lineno": 57, + "name": "finalize" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/invariants/VaultInvariant.h.ai.md b/include/xrpl/tx/invariants/VaultInvariant.h.ai.md new file mode 100644 index 0000000000..927f4fb1c2 --- /dev/null +++ b/include/xrpl/tx/invariants/VaultInvariant.h.ai.md @@ -0,0 +1,41 @@ +# `VaultInvariant.h` — Post-Transaction Consistency Guard for Single Asset Vaults + +## Role in the System + +`VaultInvariant.h` declares the `ValidVault` invariant checker, one of the entries in the `InvariantChecks` tuple registered in `InvariantCheck.h`. The invariant framework runs every registered checker against every successful transaction before committing it to the ledger. `ValidVault` is the dedicated guardian for the Single Asset Vault feature (`featureSingleAssetVault`): it ensures that the ledger objects backing a vault — the `ltVAULT` entry, its companion `ltMPTOKEN_ISSUANCE` for share tracking, and the various account/trust-line/MPToken entries affected by deposits and withdrawals — remain internally consistent after any transaction that touches them. + +## Two-Phase Architecture + +Like all checkers in the framework, `ValidVault` works in two phases driven by the two public methods. + +**`visitEntry`** is called once for each ledger entry touched by the transaction — before modification, after modification, or both. It collects snapshots into four private vectors (`beforeVault_`, `afterVault_`, `beforeMPTs_`, `afterMPTs_`) and, for balance-carrying objects (account roots, trust lines, `ltMPTOKEN`, `ltMPTOKEN_ISSUANCE`), accumulates a signed balance delta per ledger-object key into `deltas_`. The sign convention is non-obvious: `+1` for MPTokenIssuance (`sfOutstandingAmount` — decreasing this means shares are being minted to holders) and `-1` for individual balances (account root XRP, trust-line balance, MPToken amount), so that `deltas_[key] = balanceDelta * sign` yields a number whose sign represents whether the balance from the perspective of the vault's counterparty increased or decreased. + +A subtle but important design note is that during `visitEntry` there is no way to tell whether a modified `ltMPTOKEN_ISSUANCE` belongs to the vault being processed or is some unrelated MPT issuance; both are recorded into `beforeMPTs_` / `afterMPTs_` and resolved lazily in `finalize` by matching `shareMPTID` against the vault entry. + +**`finalize`** performs all the actual invariant assertions. It first checks whether the feature is enabled — storing the result in `enforce` — and uses this to decide between failing hard (return `false`) or soft-failing (returning `true` while firing a debug-only `XRPL_ASSERT`). This dual-mode pattern exists because invariant violations are expected during unit tests that deliberately disable the amendment, but must be fatal on mainnet. + +## Nested Data Structures + +`Vault` and `Shares` are private `final` structs with factory methods `Vault::make(SLE const&)` and `Shares::make(SLE const&)`. They extract and cache only the fields needed for the invariant checks — `assetsTotal`, `assetsAvailable`, `assetsMaximum`, `lossUnrealized`, `shareMPTID`, `pseudoId`, etc. — so that `finalize` can compare before/after values without re-reading ledger objects. Keeping them as aggregates with static factories rather than constructors makes the code straightforward to extend and avoids implicit conversion surprises. + +## Key Invariants Enforced + +`finalize` branches on `tx.getTxnType()` and applies a different set of rules for each vault-related transaction type: + +- **`ttVAULT_CREATE`**: the vault must be new (no `beforeVault_`), all balance fields must be zero, the `ltMPTOKEN_ISSUANCE` must exist, its issuer must be a pseudo-account, and that pseudo-account must carry a `sfVaultID` back-reference pointing at the newly created vault's key. This cross-referencing check catches any scenario where the pseudo-account linkage is broken. + +- **`ttVAULT_DELETE`**: the vault must be gone (`afterVault_` is empty), the matching share issuance must also have been deleted in the same transaction, and both `sharesTotal` and `assetsTotal` must be zero at the time of deletion. + +- **`ttVAULT_DEPOSIT`**: the vault's balance must increase, the depositor's balance must decrease by the same amount, the depositor's share count must increase, and the vault's outstanding-share count must decrease by exactly the same amount (shares flow from the vault's pseudo-account to the depositor). The implementation also handles the issuer-deposit edge case: when a deposit is made in the vault's own IOU currency by the currency issuer, no trust-line balance change occurs because the issuer's payments create funds rather than moving them, so the asset-delta check on the transaction sender is skipped. + +- **`ttVAULT_WITHDRAW`**: symmetric to deposit — vault balance decreases, recipient increases by the same amount, depositor's shares decrease, vault's outstanding shares increase by the same amount. A destination field may redirect proceeds to a different account, so the code checks exactly one of `tx[sfAccount]` or `tx[sfDestination]` for the balance increase rather than assuming the sender is always the recipient. Issuer-withdrawal has a corresponding carve-out. + +- **`ttVAULT_SET`**: a reconfiguration transaction that must not alter any asset or share balance; both the vault's asset fields and the pseudo-account's actual on-ledger balance are verified to be unchanged. + +- **`ttVAULT_CLAWBACK`**: enforced only when the caller is the asset's issuer, or when the vault owner is force-burning outstanding shares against a vault that holds no assets. In both cases, the holder's share count must decrease and the vault's outstanding shares must increase by the same amount; if the vault held assets, the vault balance must also decrease by the asset delta. + +Across all transaction types, three cross-cutting rules apply: `lossUnrealized` may only change during `ttLOAN_MANAGE` or `ttLOAN_PAY`; immutable fields (`sfAsset`, `sfAccount` pseudo-id, `sfShareMPTID`) must never change on an existing vault; and only transactions tagged with `mustModifyVault` or `mayModifyVault` privileges (via `hasPrivilege`) are permitted to touch vault state at all. These privilege checks act as a firewall against misconfigured or malicious transaction types somehow reaching vault objects. + +## Relationship to the Broader Invariant Framework + +`ValidVault` sits at position 24 in the `InvariantChecks` tuple. Its enforcement scope is deliberately narrower than the general-purpose checkers (like `XRPNotCreated` or `AccountRootsNotDeleted`) that run unconditionally. `ValidVault` gates all meaningful work behind an `afterVault_.empty() && beforeVault_.empty()` early exit, making it essentially free for the overwhelming majority of transactions that never touch a vault. The use of `Number` (a high-precision rational type from `xrpl/basics/Number.h`) rather than raw integers for all asset and loss fields reflects the fact that vault assets can be denominated in IOU currencies with fractional precision requirements that 64-bit integers cannot represent losslessly. \ No newline at end of file diff --git a/include/xrpl/tx/paths/AMMLiquidity.h.ai.json b/include/xrpl/tx/paths/AMMLiquidity.h.ai.json new file mode 100644 index 0000000000..d220be2560 --- /dev/null +++ b/include/xrpl/tx/paths/AMMLiquidity.h.ai.json @@ -0,0 +1,128 @@ +{ + "args": [ + { + "lineno": 27, + "name": "TIn" + }, + { + "lineno": 27, + "name": "TOut" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "AMMOffer" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& ammAccountID", + "std::uint32_t tradingFee", + "Asset const& in", + "Asset const& out", + "AMMContext& ammContext", + "beast::Journal j" + ], + "lineno": 27, + "name": "AMMLiquidity" + } + ], + "description": "Defines the AMMLiquidity class, which generates AMM offers for the BookStep class in the XRPL codebase, supporting both multi-path and single-path payment transactions with logic for offer sizing and quality.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/AMMLiquidity.h", + "functions": [ + { + "args": [ + "ReadView const& view", + "AccountID const& ammAccountID", + "std::uint32_t tradingFee", + "Asset const& in", + "Asset const& out", + "AMMContext& ammContext", + "beast::Journal j" + ], + "lineno": 32, + "name": "AMMLiquidity" + }, + { + "args": [], + "lineno": 39, + "name": "~AMMLiquidity" + }, + { + "args": [ + "AMMLiquidity const&" + ], + "lineno": 41, + "name": "operator=" + }, + { + "args": [ + "ReadView const& view", + "std::optional const& clobQuality" + ], + "lineno": 48, + "name": "getOffer" + }, + { + "args": [], + "lineno": 52, + "name": "ammAccount" + }, + { + "args": [], + "lineno": 57, + "name": "multiPath" + }, + { + "args": [], + "lineno": 62, + "name": "tradingFee" + }, + { + "args": [], + "lineno": 67, + "name": "context" + }, + { + "args": [], + "lineno": 72, + "name": "assetIn" + }, + { + "args": [], + "lineno": 77, + "name": "assetOut" + }, + { + "args": [ + "ReadView const& view" + ], + "lineno": 83, + "name": "fetchBalances" + }, + { + "args": [ + "TAmounts const& balances" + ], + "lineno": 90, + "name": "generateFibSeqOffer" + }, + { + "args": [ + "TAmounts const& balances", + "Rules const& rules" + ], + "lineno": 102, + "name": "maxOffer" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/AMMLiquidity.h.ai.md b/include/xrpl/tx/paths/AMMLiquidity.h.ai.md new file mode 100644 index 0000000000..1576d573c5 --- /dev/null +++ b/include/xrpl/tx/paths/AMMLiquidity.h.ai.md @@ -0,0 +1,49 @@ +# `AMMLiquidity.h` — AMM Offer Generation for the Payment Engine + +## Role in the System + +`AMMLiquidity` is the adapter that makes an Automated Market Maker (AMM) pool appear as a sequence of synthetic offers to the XRPL payment engine's `BookStep`. The payment engine was originally designed around the Central Limit Order Book (CLOB), where discrete offers are popped off in price-sorted order. AMMs, by contrast, offer continuous liquidity with a price that slides as the pool is consumed. `AMMLiquidity` bridges these two paradigms by fabricating virtual offers on demand, sized according to a strategy that depends on whether the payment has one path or multiple paths. + +## Template Structure and Instantiations + +The class is templated on `TIn` and `TOut`, representing the amount types for the two pool assets. The implementation file explicitly instantiates all eight valid combinations (`IOUAmount`/`XRPAmount`/`MPTAmount` pairs), making this a classic extern-template pattern: the header declares the interface, the `.cpp` defines it once, and the linker provides the symbols. This avoids bloating every translation unit that includes the header with redundant instantiations. + +## Construction and Snapshot of Initial Balances + +The constructor takes a `ReadView`, the AMM account ID, the trading fee in basis points, the two assets, a reference to the shared `AMMContext`, and a journal for logging. It immediately calls `fetchBalances()` and stores the result in `initialBalances_` — a `const` member. This snapshot matters: the Fibonacci offer-sizing logic in `generateFibSeqOffer()` scales each iteration's offer as a multiplier of `initialBalances_.in`, not the current (depleted) balances. The rationale is that offer sizes should be deterministic across iterations given the same starting state; using live balances would create feedback loops where earlier iterations change the sizes of later ones unpredictably. + +## Two Offer-Generation Strategies + +### Multi-Path: Fibonacci Sequence + +When the payment transaction specifies multiple paths (`ammContext_.multiPath()` is true), AMM offers must compete with CLOB offers across strands. In this regime, `getOffer()` calls `generateFibSeqOffer()`, which produces an offer whose output amount is: + +``` +out_i = initialOut × (fib[i−1]) +``` + +where `fib` is the standard Fibonacci sequence and `i` is `ammContext_.curIters()` — the count of payment engine iterations that have already consumed an AMM offer. The Fibonacci growth means early iterations get small offers (preserving price quality), and later iterations can consume exponentially larger slices if needed. The sequence is hard-coded to 30 entries, matching `AMMContext::MaxIterations`. If a computed output equals or exceeds the current pool balance, the function throws `std::overflow_error`, which `getOffer()` catches and converts to `std::nullopt`. + +The 30-iteration cap exists because AMM offers don't increment `BookStep`'s internal CLOB offer counter. Without an independent limit, the payment engine could loop indefinitely against an AMM pool. `AMMContext::maxItersReached()` gates every `getOffer()` call, returning `std::nullopt` before any work is done once the cap is hit. + +### Single-Path: Spot-Price Quality Matching + +With a single path, there is no cross-strand competition, so the goal shifts to maximising value extraction against the current CLOB. `getOffer()` uses `changeSpotPriceQuality()` (from `AMMHelpers.h`) to compute an offer sized so that, if fully consumed, the pool's new spot price equals the best competing CLOB offer's quality. This is the key insight: instead of taking a fixed slice, the AMM offer is sized to exactly meet the CLOB's price level, letting `BookStep` determine how much of it to actually use. + +If `changeSpotPriceQuality()` returns nothing (e.g., the CLOB quality is already worse than the AMM's spot price) but the `fixAMMv1_2` amendment is active, `getOffer()` falls back to `maxOffer()`. If there is no competing CLOB offer at all (`!clobQuality`), `maxOffer()` is used directly. + +## The `maxOffer()` Method and `fixAMMOverflowOffer` + +`maxOffer()` generates the largest safe synthetic offer against the pool. Under the `fixAMMOverflowOffer` amendment, it caps `takerGets` at 99% of `balances.out` (computed by the local `maxOut()` helper) and derives `takerPays` via `swapAssetOut()`. It returns `std::nullopt` if that 99% cap rounds to zero or equals the full balance — a safety valve against degenerate pool states. Before the amendment was active, the function used `maxAmount()` (the protocol-level ceiling) as `takerPays` and derived `takerGets` via `swapAssetIn()`, which could produce arithmetic overflow on large pools — the bug that motivated the amendment. + +## Quality Gating in `getOffer()` + +Before generating any offer, `getOffer()` computes the pool's current spot-price quality (`Quality{balances}`) and compares it to `clobQuality`. If the CLOB offer is at least as good — or within a relative threshold of 1e-7 — the AMM cannot profitably compete and `std::nullopt` is returned. The threshold prevents a degenerate oscillation where the spot price keeps approaching the CLOB quality without converging, burning iterations needlessly. + +## `AMMContext` as Shared State + +`AMMContext` is a single instance created in `flow()` and passed by reference throughout. It tracks whether the payment is multi-path, whether an AMM offer was consumed during the current engine iteration, and the count of such iterations. `AMMLiquidity` holds a non-owning reference to it (`ammContext_`) and consults it on every call. After each engine iteration `AMMContext::update()` increments `ammIters_` if `ammUsed_` is set, then clears the flag. `AMMLiquidity` itself does not update `ammContext_`; that responsibility belongs to `BookStep`. + +## Non-Copyable Design + +`AMMLiquidity` deletes its copy constructor and assignment operator. Because it holds a mutable reference to `AMMContext` (shared state across all strands) and an immutable snapshot of pool balances captured at construction, copying would produce objects with aliased state that do not reflect a coherent point in time. The `std::optional>` storage pattern in `BookStep` (using `emplace()`) avoids copies while still supporting deferred construction. \ No newline at end of file diff --git a/include/xrpl/tx/paths/AMMOffer.h.ai.json b/include/xrpl/tx/paths/AMMOffer.h.ai.json new file mode 100644 index 0000000000..c728cd6c50 --- /dev/null +++ b/include/xrpl/tx/paths/AMMOffer.h.ai.json @@ -0,0 +1,136 @@ +{ + "args": [ + { + "lineno": 12, + "name": "TIn" + }, + { + "lineno": 12, + "name": "TOut" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "AMMLiquidity" + }, + { + "args": [], + "lineno": 14, + "name": "QualityFunction" + }, + { + "args": [ + "AMMLiquidity const& ammLiquidity, TAmounts const& amounts, TAmounts const& balances, Quality const& quality" + ], + "lineno": 18, + "name": "AMMOffer" + } + ], + "description": "Defines the AMMOffer class template, representing a synthetic Automated Market Maker (AMM) offer for use in the BookStep logic of the XRPL ledger. The class provides methods for offer quality, asset access, consumption, limiting, and invariant checking, simulating AMM pool behavior in pathfinding and offer crossing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/AMMOffer.h", + "functions": [ + { + "args": [], + "lineno": 34, + "name": "quality" + }, + { + "args": [], + "lineno": 39, + "name": "assetIn" + }, + { + "args": [], + "lineno": 43, + "name": "assetOut" + }, + { + "args": [], + "lineno": 47, + "name": "owner" + }, + { + "args": [], + "lineno": 51, + "name": "key" + }, + { + "args": [], + "lineno": 56, + "name": "amount" + }, + { + "args": [ + "view", + "consumed" + ], + "lineno": 59, + "name": "consume" + }, + { + "args": [], + "lineno": 62, + "name": "fully_consumed" + }, + { + "args": [ + "offerAmount", + "limit", + "roundUp" + ], + "lineno": 69, + "name": "limitOut" + }, + { + "args": [ + "offerAmount", + "limit", + "roundUp" + ], + "lineno": 75, + "name": "limitIn" + }, + { + "args": [], + "lineno": 81, + "name": "getQualityFunc" + }, + { + "args": [ + "args" + ], + "lineno": 86, + "name": "send" + }, + { + "args": [], + "lineno": 93, + "name": "isFunded" + }, + { + "args": [ + "ofrInRate", + "ofrOutRate" + ], + "lineno": 98, + "name": "adjustRates" + }, + { + "args": [ + "consumed", + "j" + ], + "lineno": 106, + "name": "checkInvariant" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/AMMOffer.h.ai.md b/include/xrpl/tx/paths/AMMOffer.h.ai.md new file mode 100644 index 0000000000..0a0815ed98 --- /dev/null +++ b/include/xrpl/tx/paths/AMMOffer.h.ai.md @@ -0,0 +1,44 @@ +# `AMMOffer.h` — Synthetic AMM Offer Adapter for BookStep + +`AMMOffer` is a computational stand-in for an Automated Market Maker pool within the XRPL payment engine's offer-crossing logic. It does not correspond to any on-chain ledger entry; instead it presents an interface that mirrors `TOffer` (the Central Limit Order Book wrapper) so that the generic `BookStep` class can treat AMM liquidity and CLOB offers polymorphically, without any runtime dispatch. + +## The Design Problem + +The XRPL payment engine processes offers through `BookStep`, a generic template that was originally built for CLOB orders. When AMM support (XLS-30) was added, the challenge was to plug AMM pools into this existing machinery without forking or duplicating `BookStep`. The solution is structural duck-typing: `AMMOffer` exposes the exact same named methods (`quality()`, `amount()`, `consume()`, `fully_consumed()`, `limitIn()`, `limitOut()`, `send()`, `isFunded()`, `adjustRates()`, `checkInvariant()`) that `TOffer` provides, making both types usable as template arguments to the same `BookStep` logic. + +## Core State + +The offer holds four pieces of immutable data set at construction time: + +- `amounts_` — the synthetic offer size (TakerPays/TakerGets equivalent). For single-path transactions, this is sized so that if fully consumed, the AMM spot price after the swap equals the quality of the competing CLOB offer, or it is a "max offer" representing 99% of the output side of the pool. For multi-path transactions, it is generated from a Fibonacci-sequence progression so successive payment engine iterations probe progressively larger AMM liquidity slices. +- `balances_` — the current pool reserves at the time the offer was generated. Crucially, these are snapshotted separately from `amounts_` because the spot price quality (used as `quality_`) can diverge from the raw ratio of `amounts_` when the offer is sized relative to a competing CLOB. +- `quality_` — either the actual spot price quality derived from `balances_`, or the `amounts_` ratio when the two coincide. +- `consumed_` — a mutable boolean flag ensuring the offer is crossed at most once per payment engine iteration. + +The `ammLiquidity_` reference back to `AMMLiquidity` gives access to the trading fee, asset identifiers, the AMM account ID, and the `AMMContext` that tracks cross-iteration state. + +## Single-Path vs. Multi-Path Divergence + +The most architecturally significant behavior difference is in `limitOut()` and `limitIn()`. When `ammLiquidity_.multiPath()` is false (a single payment path), limiting is done using the actual AMM conservation function: `limitOut` calls `swapAssetOut(balances_, limit, tradingFee())` and `limitIn` calls `swapAssetIn(balances_, limit, tradingFee())`. This respects the true constant-product curve — as more of the pool is consumed, the price worsens nonlinearly. + +When multiple paths are present, AMM offers behave like fixed-quality CLOB offers: `limitOut` calls `quality().ceil_out_strict(offerAmount, limit, roundUp)` and `limitIn` calls `quality().ceil_in_strict`. This proportional scaling is deliberate: changing the AMM offer size according to its initial quality preserves the ordering between strands, ensuring that the taker pays slightly more than necessary (yielding a higher pool product than the original), rather than computing a new price along the AMM curve which would complicate multi-strand optimization. + +The same logic applies to `getQualityFunc()`: in single-path mode it constructs a `QualityFunction` with a nonzero slope (`QualityFunction::AMMTag`) derived from the current pool balances and trading fee, encoding the curve `q(out) = -cfee/poolIn * out + poolOut * cfee/poolIn`. In multi-path mode it constructs a constant quality function (`QualityFunction::CLOBLikeTag`) that behaves identically to a CLOB offer. + +## Consumption and State Updates + +`consume()` is deliberately minimal. It validates that the consumed amounts do not exceed the offer's `amounts_`, sets `consumed_ = true`, and calls `ammLiquidity_.context().setAMMUsed()` to inform `AMMContext` that an AMM offer was crossed during this payment iteration. It does **not** update the ledger pool balances — that update happens in `BookStep::consumeOffer()` when funds are actually transferred. The `key()` method returning `std::nullopt` is the structural signal of this: there is no ledger object to write back. + +`AMMContext::MaxIterations` (30) caps how many payment engine iterations may consume AMM offers in a single transaction, preventing an unbounded loop since AMM offers — unlike CLOB offers — are not exhausted from a queue. + +## Fee and Funding Semantics + +`isFunded()` unconditionally returns `true` because the AMM pool is always the issuer of its own synthetic offer; there is no owner account that could be underfunded. `adjustRates()` returns `{ofrInRate, QUALITY_ONE}`, zeroing out the output-side transfer fee — AMM swaps on Payment transactions are exempt from transfer fees, in contrast to `TOffer::adjustRates()` which passes both rates through unchanged. Similarly, `send()` calls `accountSend()` with `WaiveTransferFee::Yes`, which is the other half of the same invariant. + +## Invariant Checking + +`checkInvariant()` enforces the AMM constant-product rule after each swap: the new pool product `(balances_.in + consumed.in) × (balances_.out - consumed.out)` must be greater than or equal to the original product `balances_.in × balances_.out`, or the decrease must be within a 1e-7 relative tolerance. This guard exists because rounding in the swap formulas can produce tiny violations of the constant-product invariant; the tolerance absorbs those without masking genuinely broken swaps. + +## Instantiations + +The `.cpp` file explicitly instantiates eight combinations of `IOUAmount`, `XRPAmount`, and `MPTAmount` pairs, covering all token types that XRPL supports for AMM pools. This keeps template code out of headers and centralizes the compilation cost. \ No newline at end of file diff --git a/include/xrpl/tx/paths/BookTip.h.ai.json b/include/xrpl/tx/paths/BookTip.h.ai.json new file mode 100644 index 0000000000..8153b64e84 --- /dev/null +++ b/include/xrpl/tx/paths/BookTip.h.ai.json @@ -0,0 +1,64 @@ +{ + "args": [ + { + "lineno": 26, + "name": "view" + }, + { + "lineno": 26, + "name": "book" + }, + { + "lineno": 50, + "name": "j" + } + ], + "classes": [ + { + "args": [ + "ApplyView& view", + "Book const& book" + ], + "lineno": 13, + "name": "BookTip" + } + ], + "description": "Defines the BookTip class, which iterates and consumes raw offers in an order book, presenting offers from highest to lowest quality.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/BookTip.h", + "functions": [ + { + "args": [], + "lineno": 28, + "name": "dir" + }, + { + "args": [], + "lineno": 33, + "name": "index" + }, + { + "args": [], + "lineno": 38, + "name": "quality" + }, + { + "args": [], + "lineno": 43, + "name": "entry" + }, + { + "args": [ + "j" + ], + "lineno": 50, + "name": "step" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/BookTip.h.ai.md b/include/xrpl/tx/paths/BookTip.h.ai.md new file mode 100644 index 0000000000..de8ab42c1f --- /dev/null +++ b/include/xrpl/tx/paths/BookTip.h.ai.md @@ -0,0 +1,33 @@ +# `BookTip.h` — Raw Order Book Tip Iterator + +`BookTip` is the lowest-level iterator over a DEX order book in the XRPL ledger. It traverses quality-sorted offer directories from best (highest) to worst (lowest) quality, unconditionally exposing every offer it encounters — including missing, unfunded, and invalid ones. Filtering those away is deliberately the responsibility of its caller, `TOfferStreamBase` / `FlowOfferStream`. + +## Ledger Structure and Key-Space Design + +XRPL order books are stored as a contiguous range of `uint256` directory keys. Each "quality bucket" is a directory node whose position in the key space encodes the exchange rate for offers it contains. `getBookBase(book)` computes the base key of the book's range (best quality), and `getQualityNext(m_book)` computes the exclusive upper bound for a quality-prefix search. The constructor initialises `m_book` and `m_end` from these values, establishing the full key-space range the iterator will walk. + +Inside `step()`, the call `view_.succ(m_book, m_end)` does the heavy lifting: it returns the smallest existing key that is ≥ `m_book` and < `m_end` — in other words, the next occupied quality directory. This avoids linear key scanning and is O(log n) in the number of ledger entries. + +## The Consume-Then-Advance Contract + +`step()` operates in a *delete-then-seek* pattern. On every call after the first, if there is a current offer (`m_entry` is non-null), it immediately calls `offerDelete(view_, m_entry, j)` to remove that offer from the ledger view before searching for the next one. The `m_valid` flag gates this deletion on the first invocation (there is nothing to delete before the first `step()`). + +This is an intentional design: `BookTip` is a *consuming* iterator. It never presents the same offer twice and leaves no consumed offer behind in the view. `OfferStream` is explicitly aware of this contract — the comment in `OfferStream.cpp` reads: *"BookTip::step deletes the current offer from the view before…"* — and it coordinates accordingly. + +After finding a directory at `*first_page`, the code sets `m_book = *first_page` then decrements it (`--m_book`). This means the next `succ` call searches from just below the current directory. If the directory still has remaining offers (e.g., because `OfferStream` decided not to consume the offer via `BookTip` after all), `succ` will find it again on the next `step()`. If it was emptied, the decrement ensures `succ` naturally advances to the next quality bucket. + +## Quality Extraction and State Exposure + +When a valid directory is found, `step()` reads the quality directly out of the directory's key via `getQuality(*first_page)` — the quality value is embedded in the `uint256` index itself, which is a property of the XRPL key encoding scheme. The result is wrapped in a `Quality` value object and stored in `m_quality`. + +The four accessors — `dir()`, `index()`, `quality()`, and `entry()` — are all `noexcept` and return const references to the corresponding state fields. Together they give the caller everything needed to inspect and act on the current offer: the directory key (`m_dir`), the offer's own ledger index (`m_index`), its exchange rate (`m_quality`), and its full SLE (`m_entry` as a `shared_ptr`). + +## Defensive Handling of Empty Directories + +The `for (;;)` loop inside `step()` exists to handle an edge case the code explicitly acknowledges should never occur: an empty directory. If `dirFirst` finds no entries in a discovered directory, `m_book` is advanced to `*first_page` and the loop retries `succ` from there. Rather than asserting or crashing, `BookTip` silently skips the phantom directory. This is appropriate for consensus-critical ledger code where an assertion failure in a live validator would fork the network. + +## Relationship to `OfferStream` + +`BookTip` is always held as a member of `TOfferStreamBase`, never used standalone (except in `BookStep.cpp` for offer crossing). `OfferStream` wraps it with validity filtering: it discards offers whose SLE is missing, whose expiry has passed, and whose owner has insufficient funds. The clean separation — `BookTip` handles directory traversal and physical deletion, `OfferStream` handles semantic validity — keeps both classes focused and independently testable. + +The requirement for `ApplyView` (rather than read-only `ReadView`) is a direct consequence of this design: because `BookTip` calls `offerDelete`, it must participate in the transactional apply context that governs all ledger mutations during payment path execution. \ No newline at end of file diff --git a/include/xrpl/tx/paths/Flow.h.ai.json b/include/xrpl/tx/paths/Flow.h.ai.json new file mode 100644 index 0000000000..27758b5312 --- /dev/null +++ b/include/xrpl/tx/paths/Flow.h.ai.json @@ -0,0 +1,100 @@ +{ + "args": [ + { + "lineno": 33, + "name": "view" + }, + { + "lineno": 34, + "name": "deliver" + }, + { + "lineno": 35, + "name": "src" + }, + { + "lineno": 36, + "name": "dst" + }, + { + "lineno": 37, + "name": "paths" + }, + { + "lineno": 38, + "name": "defaultPaths" + }, + { + "lineno": 39, + "name": "partialPayment" + }, + { + "lineno": 40, + "name": "ownerPaysTransferFee" + }, + { + "lineno": 41, + "name": "offerCrossing" + }, + { + "lineno": 42, + "name": "limitQuality" + }, + { + "lineno": 43, + "name": "sendMax" + }, + { + "lineno": 44, + "name": "domainID" + }, + { + "lineno": 45, + "name": "j" + }, + { + "lineno": 46, + "name": "flowDebugInfo" + } + ], + "classes": [], + "description": "Provides the declaration for the 'flow' function, which executes a payment from a source account to a destination account using specified paths and options in the XRPL ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/Flow.h", + "functions": [ + { + "args": [ + "view", + "deliver", + "src", + "dst", + "paths", + "defaultPaths", + "partialPayment", + "ownerPaysTransferFee", + "offerCrossing", + "limitQuality", + "sendMax", + "domainID", + "j", + "flowDebugInfo" + ], + "lineno": 32, + "name": "flow" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "xrpl::path" + }, + { + "lineno": 8, + "name": "xrpl::path::detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/Flow.h.ai.md b/include/xrpl/tx/paths/Flow.h.ai.md new file mode 100644 index 0000000000..6b22791792 --- /dev/null +++ b/include/xrpl/tx/paths/Flow.h.ai.md @@ -0,0 +1,64 @@ +# `include/xrpl/tx/paths/Flow.h` + +## Role in the System + +`Flow.h` declares the single public entry point for executing payments on the XRP Ledger: the `flow()` function. It sits at the boundary between the transaction-processing layer (which decides *whether* to attempt a payment) and the path-engine layer (which decides *how* to route it). Every Payment transaction and offer-crossing operation in rippled ultimately resolves its actual movement of funds through this function. + +The file itself is minimal — a forward declaration of `FlowDebugInfo` and one function signature — but it is the linchpin that unifies path-finding, strand execution, AMM liquidity, and sandboxed ledger mutation into a single callable interface. + +## The `flow()` Function + +```cpp +path::RippleCalc::Output +flow( + PaymentSandbox& view, + STAmount const& deliver, + AccountID const& src, + AccountID const& dst, + STPathSet const& paths, + bool defaultPaths, + bool partialPayment, + bool ownerPaysTransferFee, + OfferCrossing offerCrossing, + std::optional const& limitQuality, + std::optional const& sendMax, + std::optional const& domainID, + beast::Journal j, + path::detail::FlowDebugInfo* flowDebugInfo = nullptr); +``` + +The return type, `path::RippleCalc::Output`, carries three things: the actual amount consumed from the source (`actualAmountIn`), the actual amount delivered to the destination (`actualAmountOut`), a set of `removableOffers` (unfunded or expired offers discovered during the search), and a `TER` result code. When the result is `tesSUCCESS`, the sandbox has been updated in-place; when it fails, the ledger is untouched. + +## Implementation Architecture + +The implementation in `Flow.cpp` proceeds in three phases: + +**1. Asset type resolution.** The source asset is inferred: if `sendMax` is given, its asset is used; otherwise, for IOU deliver amounts the source asset adopts the sender's account as issuer (implementing the "any issuer from src" semantic), and for MPT the delivery asset is used directly. XRP is a special case throughout. + +**2. Strand construction.** The `paths` (an `STPathSet`, the raw path hints from the transaction) are translated into `Strand` objects via `toStrands()`. A `Strand` is a `vector>`, where each `Step` is one of several concrete types: `DirectStepI` (IOU-to-IOU between accounts), `BookStepII`/`BookStepIX`/`BookStepXI` (order book crossings between IOU/XRP), `XRPEndpointStep`, `MPTEndpointStep`, and newer MPT-flavored book steps. If `toStrands()` fails, the error is returned immediately with no ledger changes. + +**3. Type-dispatched execution.** Because XRP, IOU, and MPT amounts are distinct C++ types, the function uses `std::visit` over the source and destination asset types to instantiate the correct `flow()` template from `StrandFlow.h`. This avoids branching inside the hot execution loop and lets the compiler optimize each amount-type combination separately. The inner template runs the actual payment: it iterates over all strands, uses a reverse pass (`Step::rev()`) to determine required input for desired output, then a forward pass (`Step::fwd()`) to commit the execution and handle rounding differences. Only if the overall result is `tesSUCCESS` does `finishFlow()` call `sandbox->apply(sb)` to propagate the speculative changes into the caller's sandbox. + +## Parameter Design Decisions + +**`PaymentSandbox& view`** — the function receives a mutable reference to a speculative ledger view. All offer consumption, balance changes, and trust line modifications are staged here. The caller owns the sandbox and can choose to commit or discard the result. This is the XRPL's standard mechanism for atomic, all-or-nothing transaction application. + +**`OfferCrossing offerCrossing`** — the `OfferCrossing` enum (`no`, `yes`, `sell`) distinguishes three operational modes. Regular payments (`no`) and offer crossing (`yes`/`sell`) share the same strand machinery but differ in fee attribution, quality constraints, and which offers are eligible. Passing this through to every step avoids duplicating the entire engine for offer-crossing. + +**`ownerPaysTransferFee`** — normally the sender of a payment pays IOU transfer fees; when crossing offers, fees are charged to the offer owner. This parameter propagates down to `StrandContext` so individual `BookStep` instances can apply the correct fee model without knowing their call context. + +**`limitQuality`** — a minimum exchange rate expressed as a `Quality` (output/input ratio). During offer crossing, if a `BookStep` finds that the best available offer quality falls below this threshold it stops consuming liquidity. This enforces the taker's price constraint without requiring the step to enumerate all offers. + +**`sendMax`** — an optional upper bound on the sender's spend, independent of the delivery amount. Its presence also drives source-asset inference: if absent, the source asset must be derived from the destination currency and the sender's account. + +**`domainID`** — a newer optional parameter supporting domain-scoped order books, where certain AMM or offer book lookups are restricted to a particular domain. It threads all the way down into `StrandContext` and individual `BookStep` constructors. + +**`flowDebugInfo`** — a nullable pointer defaulting to `nullptr` in production. When non-null (during testing or diagnostic runs), the inner flow template populates this structure with per-strand, per-step execution traces. The null default means the debug path has zero overhead in normal operation. + +## Relationship to `RippleCalc` + +`RippleCalc::rippleCalculate()` is the older, pre-Flow path engine still used for certain legacy code paths. `flow()` is the newer replacement and shares the same `Output` type as its return value to remain compatible with the rest of the transaction-processing infrastructure. Both functions operate on a `PaymentSandbox` and both produce actual in/out amounts with a `TER` result, but `flow()` supports MPT, AMM liquidity (via `AMMContext`), and the full step-abstraction model. + +## Invariants and Error Handling + +If `toStrands()` returns a non-success `TER`, `flow()` constructs a default `Output` with that error and returns without touching the sandbox. Within `finishFlow()`, the sandbox is applied only on success — failure leaves the sandbox pristine. Unfunded and expired offers discovered during a failed payment are collected in `removableOffers` so the transaction processor can still clean them from the ledger even though the payment itself did not go through, maintaining ledger hygiene without complicating the payment semantics. \ No newline at end of file diff --git a/include/xrpl/tx/paths/Offer.h.ai.json b/include/xrpl/tx/paths/Offer.h.ai.json new file mode 100644 index 0000000000..1b2bc261c8 --- /dev/null +++ b/include/xrpl/tx/paths/Offer.h.ai.json @@ -0,0 +1,226 @@ +{ + "args": [ + { + "lineno": 126, + "name": "entry" + }, + { + "lineno": 126, + "name": "quality" + }, + { + "lineno": 59, + "name": "view" + }, + { + "lineno": 59, + "name": "consumed" + }, + { + "lineno": 84, + "name": "offerAmount" + }, + { + "lineno": 84, + "name": "limit" + }, + { + "lineno": 84, + "name": "roundUp" + }, + { + "lineno": 101, + "name": "ofrInRate" + }, + { + "lineno": 101, + "name": "ofrOutRate" + }, + { + "lineno": 108, + "name": "j" + }, + { + "lineno": 92, + "name": "Args&&... args" + }, + { + "lineno": 181, + "name": "os" + }, + { + "lineno": 181, + "name": "offer" + } + ], + "classes": [ + { + "args": [ + "SLE::pointer const& entry, Quality quality" + ], + "lineno": 11, + "name": "TOffer" + } + ], + "description": "Defines the TOffer template class representing a limit order book offer in the XRPL, including methods for offer management, consumption, and quality calculations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/Offer.h", + "functions": [ + { + "args": [], + "lineno": 32, + "name": "quality" + }, + { + "args": [], + "lineno": 39, + "name": "owner" + }, + { + "args": [], + "lineno": 45, + "name": "amount" + }, + { + "args": [], + "lineno": 51, + "name": "fully_consumed" + }, + { + "args": [ + "view", + "consumed" + ], + "lineno": 59, + "name": "consume" + }, + { + "args": [], + "lineno": 71, + "name": "id" + }, + { + "args": [], + "lineno": 76, + "name": "key" + }, + { + "args": [], + "lineno": 81, + "name": "assetIn" + }, + { + "args": [], + "lineno": 82, + "name": "assetOut" + }, + { + "args": [ + "offerAmount", + "limit", + "roundUp" + ], + "lineno": 84, + "name": "limitOut" + }, + { + "args": [ + "offerAmount", + "limit", + "roundUp" + ], + "lineno": 88, + "name": "limitIn" + }, + { + "args": [ + "Args&&... args" + ], + "lineno": 92, + "name": "send" + }, + { + "args": [], + "lineno": 95, + "name": "isFunded" + }, + { + "args": [ + "ofrInRate", + "ofrOutRate" + ], + "lineno": 101, + "name": "adjustRates" + }, + { + "args": [ + "consumed", + "j" + ], + "lineno": 108, + "name": "checkInvariant" + }, + { + "args": [ + "entry", + "quality" + ], + "lineno": 126, + "name": "TOffer" + }, + { + "args": [], + "lineno": 134, + "name": "setFieldAmounts" + }, + { + "args": [ + "offerAmount", + "limit", + "roundUp" + ], + "lineno": 144, + "name": "limitOut" + }, + { + "args": [ + "offerAmount", + "limit", + "roundUp" + ], + "lineno": 153, + "name": "limitIn" + }, + { + "args": [ + "Args&&... args" + ], + "lineno": 166, + "name": "send" + }, + { + "args": [], + "lineno": 171, + "name": "assetIn" + }, + { + "args": [], + "lineno": 176, + "name": "assetOut" + }, + { + "args": [ + "os", + "offer" + ], + "lineno": 181, + "name": "operator<<" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/Offer.h.ai.md b/include/xrpl/tx/paths/Offer.h.ai.md new file mode 100644 index 0000000000..74b5c4aa04 --- /dev/null +++ b/include/xrpl/tx/paths/Offer.h.ai.md @@ -0,0 +1,41 @@ +# `include/xrpl/tx/paths/Offer.h` — `TOffer`: CLOB Offer Wrapper for Path Engine + +## Role in the System + +`TOffer` is the canonical representation of a Central Limit Order Book (CLOB) offer during path-finding and payment execution in the XRPL. It bridges the raw ledger object (an `SLE` — Shared Ledger Entry) and the generic payment-step machinery in `BookStep`, giving the engine a clean, typed interface for reading offer amounts, applying partial fills, and routing funds. The template parameters `TIn` and `TOut` — constrained by the `StepAmount` concept to `XRPAmount`, `IOUAmount`, or `MPTAmount` — let a single class body handle every combination of asset types the ledger supports, while allowing compile-time branches where the serialization path differs. + +Its sibling class `AMMOffer` (in `AMMOffer.h`) mirrors the exact same public interface but wraps an AMM liquidity pool instead of a ledger entry. Both types are consumed interchangeably by the generic `BookStep` template, which is the primary reason `TOffer`'s interface is designed as it is: the template parameters and method signatures are a deliberate duck-typing contract. + +## Construction and Data Layout + +The constructor takes a shared pointer to an `SLE` and a pre-computed `Quality`. Amounts are extracted immediately from the ledger entry's `sfTakerPays` (input) and `sfTakerGets` (output) fields and converted to the strongly-typed `TIn`/`TOut` values via `toAmount()`. The asset identities (`assetIn_`, `assetOut_`) are captured at this point from the `STAmount::asset()` accessors. After construction the object is self-contained; it no longer reads from the ledger until `consume()` writes back to it. + +## Quality Immutability — An Explicit Business Rule + +The inline `quality()` accessor returns `m_quality`, which is documented with care: quality is computed at the moment an offer is placed and **never recalculated**, even as the offer is partially filled. This is a deliberate ledger invariant. Partial fills only reduce the absolute amounts; the exchange rate stays fixed. This prevents accumulated rounding drift from silently worsening the effective rate for later takers and makes the order-book sort order stable. The `Quality` type stores the rate internally as an inverted ratio (input/output) so that ascending integer order corresponds to descending quality — a detail the path engine exploits when iterating the book. + +## Partial Consumption + +`consume()` is the mutation point. It decrements `m_amounts` by the consumed pair, calls `setFieldAmounts()` to write the updated values back into the `SLE` fields, and then calls `view.update(m_entry)` to stage the change in the `ApplyView`. The method throws `std::logic_error` (via `Throw<>`) if the caller tries to consume more than available — this is a hard invariant since the calling code in `BookStep` is supposed to clamp consumption first using `limitOut`/`limitIn`. `fully_consumed()` returns true the moment either side touches zero, handling the normal post-fill case where the offer must be removed. + +`setFieldAmounts()` uses `if constexpr` to branch on whether the type is `XRPAmount` (calls `toSTAmount(amount)` without an asset context) or IOU/MPT (calls `toSTAmount(amount, asset_)` with the asset). This is a compile-time dispatching strategy that avoids runtime polymorphism and ensures type safety while sharing the same function body. + +## Limiting Logic and Amendment Guards + +`limitOut()` always delegates to `Quality::ceil_out_strict()`, which uses a tighter rounding algorithm than the older `ceil_out`. `limitIn()` conditionally uses `ceil_in_strict()` only when the `fixReducedOffersV2` amendment is active, falling back to `ceil_in` otherwise. This guarded behavior preserves transaction-outcome compatibility: the stricter ceiling removes rounding slop that caused tiny residual amounts to keep offers alive longer than they should be, but because it changes observable outcomes it had to be deployed behind an amendment. The asymmetry between the two directions — `limitOut` always strict, `limitIn` amendment-gated — reflects the order in which these fixes were deployed on the network. + +## Transfer Fee Semantics: CLOB vs AMM + +The `send()` static method forwards to `accountSend(...)` with `WaiveTransferFee::No`, meaning CLOB offer owners **pay** the issuer's transfer fee on the output asset. This is contrast to `AMMOffer::send()`, which passes `WaiveTransferFee::Yes` to waive the fee, because AMM pools operate differently under the protocol rules. The static `adjustRates()` method reinforces this: `TOffer` returns both in-rate and out-rate unchanged, while `AMMOffer::adjustRates()` zeroes the out-rate to `QUALITY_ONE`, reflecting that the AMM pool itself absorbs no transfer fee on Payment transactions. + +## Funding Check + +`isFunded()` returns `true` only when the offer owner's `AccountID` equals the issuer of the output asset AND the output asset is an `Issue` (not MPT or XRP). An issuer can always deliver their own IOU without holding a balance, so the path engine can skip the real-balance check for those offers. For MPT and XRP offers this always returns `false` and the engine verifies actual owner funds through the usual `ownerFunds_` mechanism in `TOfferStreamBase`. + +## Invariant Check + +`checkInvariant()` is gated on the `fixAMMv1_3` amendment and verifies that the consumed amounts do not exceed `m_amounts`. While this check is logically a no-op for well-behaved callers (the `consume()` method already enforces this), having it as a separate call allows `BookStep` to invoke it in a uniform way across both `TOffer` and `AMMOffer` — the AMM version performs a far more expensive constant-product pool invariant check. The `// LCOV_EXCL_START` marker indicates this branch is considered unreachable under normal test coverage, existing purely as a defense-in-depth guard. + +## Relationship to `TOfferStreamBase` + +`TOfferStreamBase` (in `OfferStream.h`) holds a `TOffer` by value and advances through the order book one offer at a time. Each call to `step()` on the stream loads a new `SLE` via `BookTip`, constructs a fresh `TOffer`, and returns it via the `tip()` accessor. The stream owns the lifecycle; `TOffer` is a value type that is move-assigned into `offer_` each step and whose `consume()` writes go directly into the stream's `ApplyView`. This design keeps the offer class stateless beyond its own fields, making it straightforward to reason about correctness even in multi-step payment paths. \ No newline at end of file diff --git a/include/xrpl/tx/paths/OfferStream.h.ai.json b/include/xrpl/tx/paths/OfferStream.h.ai.json new file mode 100644 index 0000000000..6ccacef50e --- /dev/null +++ b/include/xrpl/tx/paths/OfferStream.h.ai.json @@ -0,0 +1,97 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyView& view", + "ApplyView& cancelView", + "Book const& book", + "NetClock::time_point when", + "StepCounter& counter", + "beast::Journal journal" + ], + "lineno": 11, + "name": "TOfferStreamBase" + }, + { + "args": [ + "std::uint32_t limit", + "beast::Journal j" + ], + "lineno": 13, + "name": "StepCounter" + }, + { + "args": [], + "lineno": 97, + "name": "FlowOfferStream" + } + ], + "description": "Defines templated classes for iterating, presenting, and consuming offers in an XRPL order book, including logic for removing invalid or unfunded offers and tracking offers to be permanently removed.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/OfferStream.h", + "functions": [ + { + "args": [], + "lineno": 29, + "name": "step" + }, + { + "args": [], + "lineno": 38, + "name": "count" + }, + { + "args": [ + "view" + ], + "lineno": 56, + "name": "erase" + }, + { + "args": [ + "offerIndex" + ], + "lineno": 59, + "name": "permRmOffer" + }, + { + "args": [], + "lineno": 63, + "name": "shouldRmSmallIncreasedQOffer" + }, + { + "args": [], + "lineno": 75, + "name": "tip" + }, + { + "args": [], + "lineno": 85, + "name": "step" + }, + { + "args": [], + "lineno": 90, + "name": "ownerFunds" + }, + { + "args": [ + "offerIndex" + ], + "lineno": 116, + "name": "permRmOffer" + }, + { + "args": [], + "lineno": 119, + "name": "permToRemove" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/OfferStream.h.ai.md b/include/xrpl/tx/paths/OfferStream.h.ai.md new file mode 100644 index 0000000000..e6ae4832ed --- /dev/null +++ b/include/xrpl/tx/paths/OfferStream.h.ai.md @@ -0,0 +1,59 @@ +# `include/xrpl/tx/paths/OfferStream.h` + +## Role in the System + +`OfferStream.h` defines the templated classes that serve as the order-book iterator for XRPL's payment engine. During a payment (either a simple `Payment` transaction or offer-crossing), the engine must traverse offers sorted by quality — highest quality first — consuming or removing them. `TOfferStreamBase` and `FlowOfferStream` encapsulate that traversal, including all the cleanup logic for invalid, expired, or unfunded offers discovered along the way. + +The file sits at the heart of the `tx/paths` subsystem, used directly by `BookStep.cpp` — the component that evaluates one leg of a multi-hop payment path. Without this abstraction, BookStep would have to inline all the offer validation, expiry handling, and ledger-cleanup decisions; OfferStream isolates that complexity cleanly. + +## Class Structure + +### `TOfferStreamBase` + +The base template is parameterized on the input and output amount types (`XRPAmount`, `IOUAmount`, or `MPTAmount`). This design allows a single implementation to serve all six canonical currency-pair combinations — XRP↔IOU, IOU↔IOU, MPT↔XRP, MPT↔IOU, MPT↔MPT, and their reverses — without virtual dispatch overhead on the hot path. The `.cpp` file explicitly instantiates all eight combinations. + +The base holds two `ApplyView` references: `view_` and `cancelView_`. This dual-view design is central to a subtle but critical distinction between "found unfunded" and "became unfunded" offers. `view_` accumulates changes from the in-progress transaction; `cancelView_` is a pristine snapshot of the ledger before the transaction. During `step()`, when an offer's owner has zero funds, the implementation checks the same balance in `cancelView_`. If the balance is zero there too, the offer was *already* unfunded before this transaction touched anything — it must be permanently removed from the ledger. If the balance is non-zero in `cancelView_` but zero in `view_`, the owner became unfunded because an earlier strand in the same payment consumed their balance; the offer should be skipped for now but not permanently deleted, since it might be valid if this strand is rolled back. + +The `permRmOffer()` virtual method is the hook for permanently scheduling an offer for removal. The base class calls it but leaves the storage to the concrete subclass — which is how `FlowOfferStream` accumulates the `permToRemove_` set. + +### `StepCounter` + +`StepCounter` is a nested guard with a single responsibility: enforcing a ceiling on the total number of offers examined per payment. Each call to `StepCounter::step()` increments an internal count and returns `false` once `limit_` is reached, causing the iteration to terminate. This is an essential denial-of-service protection: without a step budget, a pathological order book with thousands of tiny or invalid offers could force a validator to perform unbounded work while processing a single transaction. In `BookStep.cpp`, this limit is `MaxOffersToConsume`, and the final count is returned to the caller so the engine can track total resource usage across strands. + +### `FlowOfferStream` + +`FlowOfferStream` is the concrete implementation for the Flow payment engine (as opposed to the legacy `RippleCalc` path). Its sole addition over the base is `permToRemove_` — a `boost::container::flat_set` collecting offer indices that should be permanently erased from the ledger. `flat_set` is chosen deliberately: the set is small (typically just a few entries per payment), operations are cache-friendly, and iteration cost at cleanup time is trivial. The set is exposed read-only via `permToRemove()` so `BookStep` can apply the removals after the strand completes — even if the strand itself was discarded. + +The `permRmOffer()` override in `FlowOfferStream` supports self-crossed offer removal, referenced in comments pointing to `BookOfferCrossingStep::limitSelfCrossQuality()`. This is the mechanism by which self-crossing offers (where the same account is on both sides of a trade) can be permanently cleaned up during offer-crossing transactions, not just skipped. + +## The `step()` Loop + +The main `step()` method in `TOfferStreamBase` drives all offer validation and is labeled with a comment: *"Modifying the order or logic of these operations causes a protocol breaking change."* That comment matters: the sequence in which offers are removed or skipped directly determines ledger state, so the exact logic must be consensus-identical across all nodes. + +Each iteration of the loop: +1. Delegates to `BookTip::step()` (the raw order-book cursor) to advance to the next offer entry. +2. Enforces the `StepCounter` budget. +3. Removes the offer if its ledger entry is missing (defensive cleanup for corrupted directory state). +4. Removes the offer if it is expired, based on `sfExpiration` compared to the transaction's close time. +5. Skips or permanently removes the offer if it has zero amounts (corrupted offer). +6. Removes the offer if the asset's trust line is deep-frozen. +7. Removes the offer if it belongs to a permissioned DEX domain that no longer matches. +8. Computes owner funds and distinguishes "found unfunded" (permanent removal) from "became unfunded" (skip only). +9. Invokes `shouldRmSmallIncreasedQOffer()` to handle the rounding-granularity edge case. + +### Quality Degradation and `shouldRmSmallIncreasedQOffer()` + +Offer quality on the ledger is frozen at creation time (a deliberate XRPL business rule preserving fairness on partial fills). However, XRP's integer-drop granularity means that when an offer is partially funded — the owner has less than the offer's `TakerGets` — the effective amounts after rounding can yield a quality *worse* than the stored quality. Consumers earlier in the order book would have already been given the stated quality, so presenting an offer at a quality it can no longer deliver is misleading and can block the book. `shouldRmSmallIncreasedQOffer()` detects this: if the effective quality after accounting for owner funds and rounding is less than the stored quality, and the effective `in` amount is at or below `minPositiveAmount()`, the offer is stale and should be purged. + +This check is skipped when `TakerGets` is XRP, because an XRP-output offer can only get *better* in quality (a minimum of 1 drop remains deliverable at high quality for almost any realistic IOU amount). + +## The `erase()` Method and Technical Debt + +`erase()` handles the case where a directory entry exists but the corresponding offer ledger entry is missing. It manually removes the index from the directory page's `sfIndexes` vector. A comment in the implementation explicitly acknowledges that this *should* use `ApplyView::dirRemove`, which correctly handles empty-page cleanup, but that doing so would be a protocol-breaking change because it would alter the ledger entries touched by payment transactions. This is a tracked legacy constraint — the current implementation leaves orphaned empty directory pages in edge cases. + +## Relationships + +- **`BookTip`** (`BookTip.h`): provides raw offer iteration from the order book directory structure; `TOfferStreamBase` wraps it and adds all validation logic. +- **`TOffer`** (`Offer.h`): the typed offer abstraction, carrying amounts, quality, owner, and `consume()` logic; `TOfferStreamBase::offer_` is the current offer exposed via `tip()`. +- **`BookStep.cpp`**: the primary consumer; constructs a `FlowOfferStream`, calls `step()` in a loop to fill the payment strand, and after completion applies `permToRemove()` to purge invalid offers from the ledger. +- **`ApplyView`** (`ledger/View.h`): the transactional ledger view abstraction; both `view_` and `cancelView_` satisfy this interface. \ No newline at end of file diff --git a/include/xrpl/tx/paths/RippleCalc.h.ai.json b/include/xrpl/tx/paths/RippleCalc.h.ai.json new file mode 100644 index 0000000000..78b14aab71 --- /dev/null +++ b/include/xrpl/tx/paths/RippleCalc.h.ai.json @@ -0,0 +1,66 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "RippleCalc" + }, + { + "args": [], + "lineno": 21, + "name": "Input" + }, + { + "args": [], + "lineno": 29, + "name": "Output" + } + ], + "description": "Defines the RippleCalc class, which calculates the quality (exchange rate) of a payment path in the XRPL, including input/output amounts and removable offers for payment transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/RippleCalc.h", + "functions": [ + { + "args": [ + "PaymentSandbox& view", + "STAmount const& saMaxAmountReq", + "STAmount const& saDstAmountReq", + "AccountID const& uDstAccountID", + "AccountID const& uSrcAccountID", + "STPathSet const& spsPaths", + "std::optional const& domainID", + "ServiceRegistry& registry", + "Input const* const pInputs" + ], + "lineno": 54, + "name": "rippleCalculate" + }, + { + "args": [], + "lineno": 44, + "name": "result" + }, + { + "args": [ + "TER const value" + ], + "lineno": 48, + "name": "setResult" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "path" + }, + { + "lineno": 11, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/RippleCalc.h.ai.md b/include/xrpl/tx/paths/RippleCalc.h.ai.md new file mode 100644 index 0000000000..4c64a8f497 --- /dev/null +++ b/include/xrpl/tx/paths/RippleCalc.h.ai.md @@ -0,0 +1,43 @@ +# `RippleCalc.h` — Payment Path Quality Engine + +`RippleCalc` is the authoritative entry point for computing how funds move through the XRPL's trust-line and order-book network during a payment transaction. Its core responsibility is "quality" in the XRPL sense: given a requested output amount, determine how much input is actually required to deliver that output along a set of candidate paths — i.e., the effective exchange rate. + +## Architecture and Role + +The class sits at the boundary between transaction processing and the low-level path-flow engine. A Payment transaction handler calls the static `rippleCalculate()` factory, which orchestrates the full calculation without permanently modifying the ledger. The design deliberately separates concern: + +- The **caller** owns the `PaymentSandbox` and is responsible for applying the results to the real ledger. +- `RippleCalc` spawns a **nested** `PaymentSandbox` (`flowSB`) wrapping the caller's view, feeds it into `flow()`, then calls `flowSB.apply(view)` only after a successful computation. This nesting means the outer sandbox can still be discarded entirely if the enclosing transaction fails, preserving atomicity. + +The `flow()` function (declared in `Flow.h`, returning the same `RippleCalc::Output` type) does the heavy lifting — it walks paths, crosses order books, handles AMM liquidity, and aggregates actual in/out amounts. `rippleCalculate()` itself is a thin adapter that translates the `Input` flags into `flow()`'s parameter conventions and catches any exception from path computation, converting it to a `tecINTERNAL` result so the transaction is stored rather than silently dropped. + +## `Input` Flags + +The `Input` struct bundles four boolean knobs that alter calculation behavior: + +- **`partialPaymentAllowed`**: If the paths cannot deliver the full requested amount, allow delivery of a lesser amount rather than failing the payment outright. +- **`defaultPathsAllowed`**: Include the implicit direct path (sender → receiver) in addition to any explicitly specified paths. Defaulting to `true` matches normal payment semantics. +- **`limitQuality`**: When `true` and `saMaxAmountReq` is positive, the engine computes a minimum acceptable `Quality` threshold (`Amounts(saMaxAmountReq, saDstAmountReq)`) and rejects any liquidity below that rate. This prevents accepting worse exchange rates than the sender specified. +- **`isLedgerOpen`**: Distinguishes open-ledger (pre-consensus) processing from closed-ledger validation, which can affect rounding and fee logic downstream. + +A null `pInputs` pointer is a valid call — the implementation treats it as the conservative default (no partial payment, default paths enabled, no quality limit). + +## `Output` — Results and Side Effects + +`Output` captures two categories of information: the computed amounts and the set of stale offers discovered during traversal. + +`actualAmountIn` and `actualAmountOut` are the true amounts consumed and delivered — these may differ from the requested amounts when a partial payment is allowed or when rounding applies. The `calculationResult_` field is deliberately private with controlled access through `result()` / `setResult()`, preventing callers from accidentally stamping a success code over an internally-set error. + +`removableOffers` is a `boost::container::flat_set` holding offer IDs that were found to be expired or unfunded during path traversal. When a payment **succeeds**, those offers are deleted from the ledger as a side effect. When a payment **fails**, the ledger is not modified, but this set is returned so that offer-crossing logic (which operates differently) can still clean up the stale state. The flat_set provides ordered, compact storage suitable for deterministic iteration. + +## `permanentlyUnfundedOffers_` + +The public member `permanentlyUnfundedOffers_` on the `RippleCalc` class itself (not on `Output`) tracks offers that must be removed regardless of payment outcome — offers that are structurally unfunded rather than merely temporarily so. The comment stresses that removal must happen in a **deterministic order**, which is why an ordered container is used rather than an unordered set. This invariant is required for consensus: every validator must clean up the same offers in the same sequence to produce identical ledger hashes. + +## Domain-Scoped Payments and Service Registry + +The `domainID` optional parameter and `ServiceRegistry& registry` are extensions beyond the base XRPL payment model. They allow payment routing to be scoped to a named domain — a permissioned sub-network of trust lines and liquidity — rather than the global trust-line graph. The `registry` supplies cross-cutting services like the journal logger (`registry.getJournal("Flow")`), decoupling `RippleCalc` from global singletons and making the computation testable in isolation. + +## Design Choices Worth Noting + +`rippleCalculate()` is `static` and returns by value, making `RippleCalc` objects lightweight — the class holds only a `view` reference and the `permanentlyUnfundedOffers_` set. This avoids heap allocation for the common case and keeps the call site simple: no object lifetime to manage, no factory indirection. The exception catch around `flow()` returning `tecINTERNAL` is a pragmatic safety net; `flow()` should not throw in normal operation, but the ledger cannot tolerate an unhandled exception escaping transaction processing. \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/AmountSpec.h.ai.json b/include/xrpl/tx/paths/detail/AmountSpec.h.ai.json new file mode 100644 index 0000000000..f4a621a2dc --- /dev/null +++ b/include/xrpl/tx/paths/detail/AmountSpec.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/detail/AmountSpec.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/AmountSpec.h.ai.md b/include/xrpl/tx/paths/detail/AmountSpec.h.ai.md new file mode 100644 index 0000000000..97943fe8bc --- /dev/null +++ b/include/xrpl/tx/paths/detail/AmountSpec.h.ai.md @@ -0,0 +1,25 @@ +# `include/xrpl/tx/paths/detail/AmountSpec.h` + +This file is currently **empty** — it contains no declarations, definitions, or preprocessor directives. + +## Historical Context + +The file's name and its position within the payment-flow subsystem (`xrpl/tx/paths/detail/`) reveal that it once held two important structs central to XRPL's multi-currency path-payment engine. In older revisions (visible in git history up to commit `29e49abd3`), `AmountSpec.h` defined: + +- **`AmountSpec`** — a manual tagged union holding either an `XRPAmount` or an `IOUAmount`, distinguished by a `bool native` flag, plus optional `issuer` and `currency` metadata. This acted as a richly annotated amount description: it could represent both the numeric value *and* the asset denomination together, making it useful when a step needed to know not just "how much" but "of what." + +- **`EitherAmount`** — a lighter companion union also differentiating XRP from IOU amounts, used at the `Step` interface boundary where type metadata was already implicit in the step's template parameters. + +## What Replaced This File + +The introduction of MPT (Multi-Purpose Token) support (`feat: Add MPT support to DEX`, commit `dfcad6915`) drove a structural refactor. Both structs were overhauled: + +- `EitherAmount` was extracted to its own file (`EitherAmount.h`) and reimplemented using `std::variant` constrained by the `StepAmount` concept from `protocol/Concepts.h`. The raw `union` + `bool` pattern was replaced with a type-safe, three-way discriminated union, and `#ifndef NDEBUG` guards on the `native` flag were eliminated entirely. + +- `AmountSpec` was retired. Its role — pairing a numeric value with issuer/currency identity — is now handled through the type system directly: `Step` subclasses are templated on `TIn`/`TOut` (both constrained to `StepAmount`), and the `Asset` / `Issue` / `MPTIssue` hierarchy carries issuer and denomination identity without a wrapper struct. + +## Why the File Remains + +`AmountSpec.h` is still `#include`d by two sibling headers, `StrandFlow.h` and `FlowDebugInfo.h`, both of which included it when it was substantive. Rather than removing the `#include` directives — a change that could break any out-of-tree code that transitively relied on the inclusion — the file was left as an empty stub. It compiles harmlessly, introducing no symbols, and serves purely as an include compatibility shim. + +Any code that `#include`s `AmountSpec.h` directly should instead include `EitherAmount.h` for the `EitherAmount` type, or the appropriate protocol headers (`IOUAmount.h`, `XRPAmount.h`, `MPTAmount.h`) for concrete amount types. \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/EitherAmount.h.ai.json b/include/xrpl/tx/paths/detail/EitherAmount.h.ai.json new file mode 100644 index 0000000000..dbe1917da7 --- /dev/null +++ b/include/xrpl/tx/paths/detail/EitherAmount.h.ai.json @@ -0,0 +1,83 @@ +{ + "args": [ + { + "lineno": 14, + "name": "a" + }, + { + "lineno": 17, + "name": "T" + }, + { + "lineno": 23, + "name": "T" + }, + { + "lineno": 29, + "name": "T" + }, + { + "lineno": 34, + "name": "stream" + }, + { + "lineno": 34, + "name": "amt" + }, + { + "lineno": 35, + "name": "a" + }, + { + "lineno": 44, + "name": "T" + }, + { + "lineno": 44, + "name": "amt" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "EitherAmount" + } + ], + "description": "Defines the EitherAmount struct in the xrpl namespace, which can hold one of several amount types (XRPAmount, IOUAmount, MPTAmount) and provides type-safe access and utility functions for handling these amounts.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/detail/EitherAmount.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "holds" + }, + { + "args": [], + "lineno": 24, + "name": "get" + }, + { + "args": [ + "stream", + "amt" + ], + "lineno": 33, + "name": "operator<<" + }, + { + "args": [ + "amt" + ], + "lineno": 43, + "name": "get" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/EitherAmount.h.ai.md b/include/xrpl/tx/paths/detail/EitherAmount.h.ai.md new file mode 100644 index 0000000000..ed2ab90069 --- /dev/null +++ b/include/xrpl/tx/paths/detail/EitherAmount.h.ai.md @@ -0,0 +1,54 @@ +# `EitherAmount.h` — Type-Erased Amount Wrapper for the Payment Path Engine + +## Role in the System + +`EitherAmount` exists to solve a specific interface problem in XRPL's payment path engine: the engine's `Step` abstraction is polymorphic (virtual `fwd`/`rev` calls), yet each concrete step works with a specific, statically-typed amount — `XRPAmount`, `IOUAmount`, or `MPTAmount`. These three types have incompatible representations and no common base class. `EitherAmount` bridges that gap by wrapping `std::variant` behind a uniform value type that can flow freely through the virtual step interface. + +## The `StepAmount` Concept Constraint + +All template methods on `EitherAmount` are constrained by the `StepAmount` concept defined in `Concepts.h`: + +```cpp +template +concept StepAmount = + std::is_same_v || std::is_same_v || std::is_same_v; +``` + +This constraint closes the open variant: you cannot accidentally construct an `EitherAmount` from `STAmount` or any other numeric type, even though `std::variant` itself would accept any compatible type. The concept acts as a compile-time invariant — the set of acceptable amount types is explicitly enumerated and enforced at every call site. + +## Structure and Access Model + +The struct holds a single `std::variant` member named `amount`. Access is mediated through two member templates: + +- `holds()` delegates to `std::holds_alternative` and lets callers query the active type before extraction. +- `get()` checks `holds()` first and throws `std::logic_error` if the variant doesn't hold the requested type, then returns a `const` reference via `std::get`. + +This fail-fast design is deliberate. The alternative — letting `std::get` throw `std::bad_variant_access` directly — would produce the same failure outcome but with less diagnostic context. By throwing `std::logic_error` with an explicit message, the code signals that reaching a mismatched access represents a programming error in the flow engine itself, not a runtime data condition. A free function `get(EitherAmount const&)` at namespace scope provides a convenient alternative calling convention used throughout `FlowDebugInfo.h`. + +## Role in `Step` and the Flow Engine + +In `Steps.h`, `EitherAmount` appears as the boundary type for every amount exchange: + +```cpp +virtual std::pair +rev(PaymentSandbox& sb, ApplyView& afView, + boost::container::flat_set& ofrsToRm, + EitherAmount const& out) = 0; + +virtual std::pair +fwd(PaymentSandbox& sb, ApplyView& afView, + boost::container::flat_set& ofrsToRm, + EitherAmount const& in) = 0; +``` + +Concrete step types (`XRPEndpointStep`, `DirectStepI`, `BookStepXI`, etc.) unpack the variant at the top of their implementations using `get()` or `get()`, perform their typed arithmetic, and then re-wrap the result into a new `EitherAmount` for return. This pattern means the variant is always created and consumed at well-defined boundaries — the type is always known at the concrete step, and the type-erasure serves only the virtual dispatch layer. + +`cachedIn()` and `cachedOut()` on `Step` return `std::optional`, allowing a step to cache the last computed amount across forward/reverse passes. `XRPEndpointStep` demonstrates this: it stores an `std::optional` internally and wraps it in `EitherAmount` only when the `Step` interface demands it. + +## Debug Output + +The `operator<<` overload is conditionally compiled only in debug builds (`#ifndef NDEBUG`). It uses `std::visit` with a C++20 template lambda to dispatch `to_string` to whichever type the variant currently holds. Excluding this from release builds reflects the performance-conscious design of the path engine, where formatting strings would be dead weight on the hot path. + +## Design Choice: `std::variant` over Inheritance + +Using `std::variant` rather than a polymorphic base class gives `EitherAmount` value semantics: it can be stored in `std::vector`, returned by value, and passed cheaply without heap allocation or pointer indirection. `FlowDebugInfo.h` exploits this directly by storing `std::vector` to record per-pass input/output amounts during path simulation. A pointer-based design would have required careful ownership management and allocation overhead for what is essentially a diagnostic scratchpad. \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/FlatSets.h.ai.json b/include/xrpl/tx/paths/detail/FlatSets.h.ai.json new file mode 100644 index 0000000000..f3629e11be --- /dev/null +++ b/include/xrpl/tx/paths/detail/FlatSets.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 10, + "name": "dst" + }, + { + "lineno": 10, + "name": "src" + } + ], + "classes": [], + "description": "Provides a utility function to compute the union of two boost::container::flat_set objects within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/detail/FlatSets.h", + "functions": [ + { + "args": [ + "dst", + "src" + ], + "lineno": 10, + "name": "SetUnion" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/FlatSets.h.ai.md b/include/xrpl/tx/paths/detail/FlatSets.h.ai.md new file mode 100644 index 0000000000..07a0406142 --- /dev/null +++ b/include/xrpl/tx/paths/detail/FlatSets.h.ai.md @@ -0,0 +1,31 @@ +# `FlatSets.h` — In-Place Union for Sorted Flat Sets + +`FlatSets.h` is a small utility header in the `xrpl/tx/paths/detail` module, providing a single template function `SetUnion` that merges one `boost::container::flat_set` into another in place. Despite its brevity, the function is carefully written to exploit a performance characteristic of the `flat_set` container that a naive alternative would miss. + +## What Problem It Solves + +The XRPL payment-path engine (in `StrandFlow.h` and `BookStep.cpp`) tracks sets of offer IDs (`boost::container::flat_set`) that must be deleted from the ledger — either because they were consumed, expired, or found to be unfunded during a payment flow computation. As each strand or book step runs, it produces its own local set of such offers. These per-step sets must be merged into a single accumulator set so that all bad offers can be cleaned up atomically after the flow completes, regardless of whether the overall payment succeeded. + +`SetUnion` encapsulates that merge operation. + +## Why `flat_set` Instead of `std::set` + +`boost::container::flat_set` stores its elements in a contiguous sorted array rather than a tree, giving it better cache locality for iteration and binary-search lookups. This matches the payment engine's usage pattern: offer ID sets are built up during a traversal and then iterated over at the end for deletion. The flat layout means the entire set fits in cache lines, which matters when iterating over potentially hundreds of entries. + +## The Design of `SetUnion` + +The implementation has two deliberate optimizations: + +**Early exit on empty source.** The check `if (src.empty()) return;` avoids any allocation or work when the incoming set contributes nothing. This is the common case when a strand traverses a path that doesn't touch any bad offers. + +**`reserve` before insert.** Calling `dst.reserve(dst.size() + src.size())` pre-allocates exactly enough capacity for the worst case (no overlap between the sets) before the insert. Without this, inserting elements one by one into a `flat_set` can trigger repeated reallocation and copying of the underlying sorted array, turning an O(n) operation into an O(n²) one. + +**`ordered_unique_range_t` hint.** The insert uses `boost::container::ordered_unique_range_t{}` as a tag argument. This tells `flat_set` that the range being inserted is already sorted and contains no duplicates. Because `src` is itself a `flat_set`, this invariant is guaranteed. The tag lets Boost perform a merge-style insert (essentially `std::inplace_merge` semantics) instead of inserting elements one at a time, cutting the algorithmic cost from O(n log n) to O(n + m) where n and m are the sizes of the two sets. + +Together, these three choices ensure the union is performed with a single allocation and a single linear pass over both sets — a meaningful win in a hot path where strand flows may loop many times before the payment engine finds a satisfactory result. + +## Usage Context + +In `StrandFlow.h`, `SetUnion` is called in two places: once to fold the offers-to-remove from each individual strand flow result into the outer accumulator `ofrsToRm`, and again to fold `ofrsToRm` into `ofrsToRmOnFail` — the set of offers that must be purged even when the overall payment fails. In `BookStep.cpp`, it is called inside both the reverse and forward offer-iteration loops to accumulate bad offers found during order-book traversal into the step's own `ofrsToRm` set. This consistent aggregation pattern means that by the time control returns to the top-level flow loop, no bad offer ever escapes cleanup. + +The function is generic over element type `T` (inheriting whatever ordering `flat_set` requires), but in practice the entire codebase uses it only with `uint256` offer keys. \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/FlowDebugInfo.h.ai.json b/include/xrpl/tx/paths/detail/FlowDebugInfo.h.ai.json new file mode 100644 index 0000000000..5419e9dbee --- /dev/null +++ b/include/xrpl/tx/paths/detail/FlowDebugInfo.h.ai.json @@ -0,0 +1,169 @@ +{ + "args": [ + { + "lineno": 24, + "name": "nativeIn_" + }, + { + "lineno": 24, + "name": "nativeOut_" + }, + { + "lineno": 61, + "name": "nativeIn" + }, + { + "lineno": 61, + "name": "nativeOut" + }, + { + "lineno": 80, + "name": "tag" + }, + { + "lineno": 92, + "name": "tag" + }, + { + "lineno": 104, + "name": "name" + }, + { + "lineno": 104, + "name": "pi" + }, + { + "lineno": 113, + "name": "tag" + }, + { + "lineno": 120, + "name": "c" + }, + { + "lineno": 129, + "name": "in" + }, + { + "lineno": 129, + "name": "out" + }, + { + "lineno": 129, + "name": "activeStrands" + }, + { + "lineno": 134, + "name": "in" + }, + { + "lineno": 134, + "name": "out" + }, + { + "lineno": 143, + "name": "writePassInfo" + }, + { + "lineno": 168, + "name": "ostr" + }, + { + "lineno": 168, + "name": "elem" + }, + { + "lineno": 177, + "name": "ostr" + }, + { + "lineno": 177, + "name": "begin" + }, + { + "lineno": 177, + "name": "end" + }, + { + "lineno": 192, + "name": "sb" + }, + { + "lineno": 192, + "name": "rv" + }, + { + "lineno": 197, + "name": "bd" + } + ], + "classes": [ + { + "args": [ + "bool nativeIn", + "bool nativeOut" + ], + "lineno": 13, + "name": "FlowDebugInfo" + }, + { + "args": [ + "bool nativeIn_", + "bool nativeOut_" + ], + "lineno": 22, + "name": "PassInfo" + } + ], + "description": "This file provides debugging and performance tracking utilities for payment flows in the XRPL pathfinding logic, including structures to record timing, pass information, and balance differences during payment processing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/detail/FlowDebugInfo.h", + "functions": [ + { + "args": [ + "ostr", + "elem" + ], + "lineno": 168, + "name": "writeDiffElement" + }, + { + "args": [ + "ostr", + "begin", + "end" + ], + "lineno": 177, + "name": "writeDiffs" + }, + { + "args": [ + "sb", + "rv" + ], + "lineno": 192, + "name": "balanceDiffs" + }, + { + "args": [ + "bd" + ], + "lineno": 197, + "name": "balanceDiffsToString" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "path" + }, + { + "lineno": 12, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/FlowDebugInfo.h.ai.md b/include/xrpl/tx/paths/detail/FlowDebugInfo.h.ai.md new file mode 100644 index 0000000000..4a4d363648 --- /dev/null +++ b/include/xrpl/tx/paths/detail/FlowDebugInfo.h.ai.md @@ -0,0 +1,55 @@ +# `FlowDebugInfo.h` — Payment Flow Diagnostics + +**Location:** `include/xrpl/tx/paths/detail/FlowDebugInfo.h` +**Namespace:** `xrpl::path::detail` + +## Role and Purpose + +This header defines the diagnostic scaffolding used to instrument payment flow execution in the XRPL pathfinding engine. It lives in the `detail` subdirectory of the paths module because it is not part of the public payment API — it exists purely to give engineers visibility into how the flow engine consumed liquidity during a single payment, and how long each phase took. + +The file's contents divide into two concerns: the `FlowDebugInfo` struct (which accumulates telemetry during a single call to `flow()`), and a set of free functions (`balanceDiffs`, `balanceDiffsToString`, `writeDiffElement`, `writeDiffs`) that snapshot `PaymentSandbox` balance changes for post-hoc analysis. + +## `FlowDebugInfo` — Per-Payment Telemetry + +`FlowDebugInfo` is constructed once per payment execution and threaded down the call stack as a raw pointer. The `flow()` entry point in `Flow.cpp` accepts `path::detail::FlowDebugInfo* flowDebugInfo = nullptr`, making the entire instrumentation path opt-in with zero overhead when the pointer is null. `RippleCalc::rippleCalculate` currently passes `nullptr`, confirming that this data is never gathered in production consensus paths — it exists for testing, benchmarking, or developer tools. + +The struct stores two flat maps (from `boost::container`) keyed by string tags: `timePoints` mapping tags to `(start, end)` timestamp pairs, and `counts` mapping tags to occurrence counts. Using `boost::container::flat_map` rather than `std::map` is a deliberate performance choice — both maps are reserved upfront (`reserve(16)`) and are accessed by short string keys, so the cache-friendly flat layout pays off at the small sizes typical in one payment execution. + +### Timing with RAII: `timeBlock()` + +The most architecturally interesting piece is `timeBlock(std::string name)`, which returns a local `Stopper` object. On construction, `Stopper` records `clock::now()` as both start and end of the tag's entry. On destruction, it overwrites the end with a fresh `clock::now()`. This RAII pattern means a caller can write: + +```cpp +auto _ = flowDebugInfo->timeBlock("main"); +// ... rest of function ... +``` + +and the duration is captured automatically when the scope exits. Using `std::chrono::high_resolution_clock` gives nanosecond-resolution timing suitable for profiling individual payment passes. The `Stopper` is move-constructible (required because it's returned by value) but not copy-constructible, and it stores a raw pointer back to the parent `FlowDebugInfo` — callers must ensure the parent outlives the stopper. + +### Pass Tracking: `PassInfo` + +The nested `PassInfo` struct records one data point per "liquidity pass" — each iteration of the outer loop in `StrandFlow.h` where the engine selects the best-quality strand and routes an increment of the payment through it. For every pass, `push_back()` records the `EitherAmount` consumed (`in`) and delivered (`out`), as well as how many strands remained active. This lets a developer reconstruct the exact sequence of incremental fills the engine performed to complete the payment. + +Within each pass, `liquiditySrcIn` and `liquiditySrcOut` track per-strand contributions — `newLiquidityPass()` opens a new inner vector before each pass begins, and `pushLiquiditySrc()` appends the individual strand's amounts to that vector. The result is a nested structure: `liquiditySrcIn[pass][strand]` gives the amount consumed from each strand in each iteration. + +`PassInfo` uses `nativeIn` and `nativeOut` boolean flags (set at construction and declared `const`) to record whether the payment's source and destination currencies are XRP. These flags govern how amounts are serialized in `to_string()` — XRP amounts call `get()` while IOU amounts call `get()`. The `EitherAmount` type (defined in `EitherAmount.h`) is a `std::variant`, so this branching is required for correct extraction. + +### Serialization: `to_string()` + +`to_string(bool writePassInfo)` always emits the total duration of the `"main"` timed block and the pass count. When `writePassInfo` is true, it additionally emits the full sequence of per-pass in/out amounts, active strand counts, and per-strand liquidity amounts in a bracket-and-semicolon delimited format designed for log parsing. The nested liquidity arrays use `|` as the inner delimiter and `;` as the outer, making them machine-readable without a full JSON parser. + +### Latent Bug in `inc()` + +`inc()` contains a subtle defect: when a tag is not yet in `counts`, it inserts `counts[tag] = 1` but then attempts `++i->second` using the pre-insertion iterator `i`, which for `flat_map`'s vector-backed storage is now invalid (insertion can relocate elements). This results in undefined behavior on first use of any new tag. Since the struct is used only in diagnostic code paths, this has not caused observable failures, but it is worth noting. + +## Balance Diff Utilities + +The free functions at the bottom of the file operate on `PaymentSandbox` snapshots to produce human-readable balance change reports. `balanceDiffs()` calls `sb.balanceChanges(rv)` and `sb.xrpDestroyed()`, bundling the IOU trust-line changes (keyed by `(account, account, currency)` tuples) and the net XRP burned into a `BalanceDiffs` pair. `balanceDiffsToString()` wraps this in an `optional` and serializes it using `writeDiffs()`, which iterates over the map and calls `writeDiffElement()` for each entry. Each element is formatted as `[sender|receiver|currency|amount]`, providing a compact audit trail of every trust-line mutation the payment caused. + +## Relationship to Other Files + +- **`StrandFlow.h`** is the primary consumer: it calls `newLiquidityPass()`, `pushLiquiditySrc()`, and `pushPass()` inside the liquidity-selection loop, always guarded by `if (flowDebugInfo)` null checks. +- **`Flow.cpp`** receives `FlowDebugInfo*` as a parameter and passes it through to `StrandFlow.h`'s templated flow function. +- **`RippleCalc.cpp`** is the top-level caller and currently always passes `nullptr`, meaning production payments collect no telemetry. +- **`EitherAmount.h`** provides the `EitherAmount` variant type that `PassInfo` uses throughout — its `get()` method throws `std::logic_error` if the wrong type is requested, which the `to_string()` code avoids by checking `nativeIn`/`nativeOut` before dispatching. +- **`PaymentSandbox.h`** provides the `balanceChanges()` and `xrpDestroyed()` methods used by `balanceDiffs()`. \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/StepChecks.h.ai.json b/include/xrpl/tx/paths/detail/StepChecks.h.ai.json new file mode 100644 index 0000000000..dcc92308f4 --- /dev/null +++ b/include/xrpl/tx/paths/detail/StepChecks.h.ai.json @@ -0,0 +1,37 @@ +{ + "args": [], + "classes": [], + "description": "This file provides inline utility functions for checking freeze and noRipple constraints on trust lines and accounts in the XRPL ledger, used during payment pathfinding and transaction processing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/detail/StepChecks.h", + "functions": [ + { + "args": [ + "view", + "src", + "dst", + "currency" + ], + "lineno": 10, + "name": "checkFreeze" + }, + { + "args": [ + "view", + "prev", + "cur", + "next", + "currency", + "j" + ], + "lineno": 49, + "name": "checkNoRipple" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/StepChecks.h.ai.md b/include/xrpl/tx/paths/detail/StepChecks.h.ai.md new file mode 100644 index 0000000000..30e466b829 --- /dev/null +++ b/include/xrpl/tx/paths/detail/StepChecks.h.ai.md @@ -0,0 +1,52 @@ +# `StepChecks.h` — Freeze and NoRipple Guards for Payment Path Steps + +This header defines two inline validation predicates that form the front-line compliance checks during payment pathfinding. Every candidate path step through the XRPL ledger must clear both tests before a payment engine can use it; failing either one aborts path evaluation for that step with a retriable (`ter`-family) error code. + +## Role in the Payment Engine + +The XRPL payment engine represents multi-hop paths as a sequence of `Step` objects (defined in `Steps.h`). Each concrete step type — `DirectStepI` for IOU-to-IOU hops, `XRPEndpointStep` for terminal XRP legs, and so on — calls into these two functions from its `check()` method during path validation. The functions are inlined here rather than compiled into a translation unit because every step type includes this header directly, and the logic is short enough that the call overhead would be non-trivial relative to the body. + +## `checkFreeze` + +```cpp +TER checkFreeze(ReadView const& view, AccountID const& src, + AccountID const& dst, Currency const& currency) +``` + +This function answers: *is this trust line currently blocked by any freeze mechanism?* It enforces three distinct freeze layers in order: + +**Global freeze.** If the destination account has the `lsfGlobalFreeze` flag set, every IOU it issues is inaccessible. This is the nuclear option issuers use to halt all transfers during a crisis. The check is on `dst` rather than `src` because it is the issuer who declares a global freeze, and payments routed *toward* a globally-frozen issuer must be blocked. + +**Per-trust-line directional freeze.** A trust line between two accounts has a "high" side (the account whose `AccountID` is numerically larger) and a "low" side. Each side may independently freeze the line with `lsfHighFreeze` or `lsfLowFreeze`. The comparison `(dst > src) ? lsfHighFreeze : lsfLowFreeze` selects the correct flag based on which side `dst` occupies in the canonical ordering. This asymmetry is intentional: an issuer can freeze a customer's trust line without affecting their own ability to redeem from the customer. + +**Deep freeze.** Introduced more recently, `lsfHighDeepFreeze` and `lsfLowDeepFreeze` are checked unconditionally — the function returns `terNO_LINE` if *either* side's deep-freeze bit is set, regardless of directionality. Deep freeze is intended for scenarios where the issuer wants to completely prohibit any movement on the line from either direction, unlike a regular freeze which still permits outbound transfers from the freezing side. + +**AMM pool freeze via `fixFrozenLPTokenTransfer`.** When this amendment is active, there is a fourth check: if `dst` is itself an AMM account (identified by the presence of `sfAMMID` on its ledger entry), the function reads the corresponding AMM object and calls `isLPTokenFrozen()` to test whether the underlying pool assets are frozen. This check was added to close a gap where LP tokens representing a frozen pool could still be transferred by routing through the AMM account directly. A missing AMM ledger entry causes `tecINTERNAL` (marked `LCOV_EXCL_LINE` because it would indicate a ledger corruption invariant violation). + +The assertion `src != dst` at the top catches programmer error — a self-loop would make the flag-selection arithmetic meaningless and should never reach this function in a valid path. + +In `DirectStep.cpp`, the freeze check is intentionally skipped when `ctx.isFirst && ctx.isLast` are both true, meaning the step is simultaneously the first and last hop — a pure issue or redeem between the transaction's ultimate source and destination. That bilateral relationship is inherently authorized and cannot be frozen. + +## `checkNoRipple` + +```cpp +TER checkNoRipple(ReadView const& view, AccountID const& prev, + AccountID const& cur, AccountID const& next, + Currency const& currency, beast::Journal j) +``` + +NoRipple is an account-level setting that says "do not let payments pass through my trust lines without my explicit blessing." `checkNoRipple` evaluates whether the intermediate account `cur` in the triple (`prev` → `cur` → `next`) has blocked transit. + +The key insight is that NoRipple only prevents transit when **both** the incoming and outgoing trust lines for `cur` have the flag set from `cur`'s perspective. The flag is checked directionally, again using the `high`/`low` convention: `(cur > prev) ? lsfHighNoRipple : lsfLowNoRipple` extracts the NoRipple bit from the `prev`↔`cur` line, and similarly for `cur`↔`next`. If both lines block rippling, the path through `cur` is rejected with `terNO_RIPPLE`. + +This AND semantics is deliberate. A user may have one trust line to a major exchange (with NoRipple off, to participate in the payment network) and other lines to counterparties they wish to keep isolated (with NoRipple on). Transit is still permitted as long as at least one side of the intermediate account's path is "open." This lets intermediate accounts selectively participate in payment routing. + +Both trust lines must actually exist; if either is missing, `terNO_LINE` is returned — there is no line to route through in the first place. A diagnostic log at `info` level records which three accounts violated the constraint, which is useful for debugging path-finding failures. + +The function takes a `beast::Journal` precisely for this logging; unlike `checkFreeze`, which is a pure state query, `checkNoRipple` has an observational side-effect that helps operators trace why a seemingly valid path was rejected. + +## Design Notes + +Both functions take `ReadView const&` rather than a mutable view, signaling that path validation is a read-only probe — no ledger state is modified during the check phase. The `inline` linkage means each calling translation unit embeds its own copy, avoiding a shared-library dependency for two small functions that are on the hot path of payment execution. + +The `ter`-prefixed return codes (`terNO_LINE`, `terNO_RIPPLE`) are retriable transaction errors rather than fatal ones (`tec`/`tef`), reflecting that a frozen or noRipple-blocked path is not an error in the transaction itself — the engine should simply try the next candidate path before giving up. \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/Steps.h.ai.json b/include/xrpl/tx/paths/detail/Steps.h.ai.json new file mode 100644 index 0000000000..e2c43b950a --- /dev/null +++ b/include/xrpl/tx/paths/detail/Steps.h.ai.json @@ -0,0 +1,652 @@ +{ + "args": [ + { + "lineno": 20, + "name": "dir" + }, + { + "lineno": 25, + "name": "dir" + }, + { + "lineno": 181, + "name": "sb" + }, + { + "lineno": 181, + "name": "prevStepDir" + }, + { + "lineno": 194, + "name": "strand" + }, + { + "lineno": 205, + "name": "lhs" + }, + { + "lineno": 205, + "name": "rhs" + }, + { + "lineno": 217, + "name": "src" + }, + { + "lineno": 217, + "name": "dst" + }, + { + "lineno": 217, + "name": "deliver" + }, + { + "lineno": 217, + "name": "sendMaxAsset" + }, + { + "lineno": 217, + "name": "path" + }, + { + "lineno": 238, + "name": "sb" + }, + { + "lineno": 238, + "name": "src" + }, + { + "lineno": 238, + "name": "dst" + }, + { + "lineno": 238, + "name": "deliver" + }, + { + "lineno": 238, + "name": "limitQuality" + }, + { + "lineno": 238, + "name": "sendMaxAsset" + }, + { + "lineno": 238, + "name": "path" + }, + { + "lineno": 238, + "name": "ownerPaysTransferFee" + }, + { + "lineno": 238, + "name": "offerCrossing" + }, + { + "lineno": 238, + "name": "ammContext" + }, + { + "lineno": 238, + "name": "domainID" + }, + { + "lineno": 238, + "name": "j" + }, + { + "lineno": 266, + "name": "sb" + }, + { + "lineno": 266, + "name": "src" + }, + { + "lineno": 266, + "name": "dst" + }, + { + "lineno": 266, + "name": "deliver" + }, + { + "lineno": 266, + "name": "limitQuality" + }, + { + "lineno": 266, + "name": "sendMax" + }, + { + "lineno": 266, + "name": "paths" + }, + { + "lineno": 266, + "name": "addDefaultPath" + }, + { + "lineno": 266, + "name": "ownerPaysTransferFee" + }, + { + "lineno": 266, + "name": "offerCrossing" + }, + { + "lineno": 266, + "name": "ammContext" + }, + { + "lineno": 266, + "name": "domainID" + }, + { + "lineno": 266, + "name": "j" + }, + { + "lineno": 374, + "name": "expected" + }, + { + "lineno": 374, + "name": "actual" + }, + { + "lineno": 377, + "name": "expected" + }, + { + "lineno": 377, + "name": "actual" + }, + { + "lineno": 380, + "name": "expected" + }, + { + "lineno": 380, + "name": "actual" + }, + { + "lineno": 400, + "name": "step" + }, + { + "lineno": 400, + "name": "src" + }, + { + "lineno": 400, + "name": "dst" + }, + { + "lineno": 400, + "name": "currency" + }, + { + "lineno": 405, + "name": "step" + }, + { + "lineno": 405, + "name": "src" + }, + { + "lineno": 405, + "name": "dst" + }, + { + "lineno": 405, + "name": "mptid" + }, + { + "lineno": 410, + "name": "step" + }, + { + "lineno": 410, + "name": "acc" + }, + { + "lineno": 412, + "name": "step" + }, + { + "lineno": 412, + "name": "book" + }, + { + "lineno": 415, + "name": "ctx" + }, + { + "lineno": 415, + "name": "src" + }, + { + "lineno": 415, + "name": "dst" + }, + { + "lineno": 415, + "name": "c" + }, + { + "lineno": 420, + "name": "ctx" + }, + { + "lineno": 420, + "name": "src" + }, + { + "lineno": 420, + "name": "dst" + }, + { + "lineno": 420, + "name": "a" + }, + { + "lineno": 425, + "name": "ctx" + }, + { + "lineno": 425, + "name": "in" + }, + { + "lineno": 425, + "name": "out" + }, + { + "lineno": 430, + "name": "ctx" + }, + { + "lineno": 430, + "name": "in" + }, + { + "lineno": 435, + "name": "ctx" + }, + { + "lineno": 435, + "name": "out" + }, + { + "lineno": 440, + "name": "ctx" + }, + { + "lineno": 440, + "name": "acc" + }, + { + "lineno": 445, + "name": "ctx" + }, + { + "lineno": 445, + "name": "in" + }, + { + "lineno": 445, + "name": "out" + }, + { + "lineno": 450, + "name": "ctx" + }, + { + "lineno": 450, + "name": "in" + }, + { + "lineno": 455, + "name": "ctx" + }, + { + "lineno": 455, + "name": "out" + }, + { + "lineno": 460, + "name": "ctx" + }, + { + "lineno": 460, + "name": "in" + }, + { + "lineno": 460, + "name": "out" + }, + { + "lineno": 465, + "name": "ctx" + }, + { + "lineno": 465, + "name": "in" + }, + { + "lineno": 465, + "name": "out" + }, + { + "lineno": 468, + "name": "strand" + } + ], + "classes": [ + { + "args": [], + "lineno": 34, + "name": "Step" + }, + { + "args": [], + "lineno": 353, + "name": "StepImp" + }, + { + "args": [ + "TER t, std::string const& msg", + "TER t" + ], + "lineno": 370, + "name": "FlowException" + }, + { + "args": [ + "view_", + "strand_", + "strandSrc_", + "strandDst_", + "strandDeliver_", + "limitQuality_", + "isLast_", + "ownerPaysTransferFee_", + "offerCrossing_", + "isDefaultPath_", + "seenDirectAssets_", + "seenBookOuts_", + "ammContext_", + "domainID", + "j_" + ], + "lineno": 386, + "name": "StrandContext" + } + ], + "description": "Defines the core abstractions and utilities for payment path steps (Step) in the XRPL payment engine, including step types, strand construction, error handling, and related helpers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/detail/Steps.h", + "functions": [ + { + "args": [ + "dir" + ], + "lineno": 20, + "name": "redeems" + }, + { + "args": [ + "dir" + ], + "lineno": 25, + "name": "issues" + }, + { + "args": [ + "v", + "prevStepDir" + ], + "lineno": 181, + "name": "Step::getQualityFunc" + }, + { + "args": [ + "strand" + ], + "lineno": 194, + "name": "offersUsed" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 205, + "name": "operator==" + }, + { + "args": [ + "src", + "dst", + "deliver", + "sendMaxAsset", + "path" + ], + "lineno": 217, + "name": "normalizePath" + }, + { + "args": [ + "sb", + "src", + "dst", + "deliver", + "limitQuality", + "sendMaxAsset", + "path", + "ownerPaysTransferFee", + "offerCrossing", + "ammContext", + "domainID", + "j" + ], + "lineno": 238, + "name": "toStrand" + }, + { + "args": [ + "sb", + "src", + "dst", + "deliver", + "limitQuality", + "sendMax", + "paths", + "addDefaultPath", + "ownerPaysTransferFee", + "offerCrossing", + "ammContext", + "domainID", + "j" + ], + "lineno": 266, + "name": "toStrands" + }, + { + "args": [ + "expected", + "actual" + ], + "lineno": 374, + "name": "checkNear" + }, + { + "args": [ + "expected", + "actual" + ], + "lineno": 377, + "name": "checkNear" + }, + { + "args": [ + "expected", + "actual" + ], + "lineno": 380, + "name": "checkNear" + }, + { + "args": [ + "step", + "src", + "dst", + "currency" + ], + "lineno": 400, + "name": "test::directStepEqual" + }, + { + "args": [ + "step", + "src", + "dst", + "mptid" + ], + "lineno": 405, + "name": "test::mptEndpointStepEqual" + }, + { + "args": [ + "step", + "acc" + ], + "lineno": 410, + "name": "test::xrpEndpointStepEqual" + }, + { + "args": [ + "step", + "book" + ], + "lineno": 412, + "name": "test::bookStepEqual" + }, + { + "args": [ + "ctx", + "src", + "dst", + "c" + ], + "lineno": 415, + "name": "make_DirectStepI" + }, + { + "args": [ + "ctx", + "src", + "dst", + "a" + ], + "lineno": 420, + "name": "make_MPTEndpointStep" + }, + { + "args": [ + "ctx", + "in", + "out" + ], + "lineno": 425, + "name": "make_BookStepII" + }, + { + "args": [ + "ctx", + "in" + ], + "lineno": 430, + "name": "make_BookStepIX" + }, + { + "args": [ + "ctx", + "out" + ], + "lineno": 435, + "name": "make_BookStepXI" + }, + { + "args": [ + "ctx", + "acc" + ], + "lineno": 440, + "name": "make_XRPEndpointStep" + }, + { + "args": [ + "ctx", + "in", + "out" + ], + "lineno": 445, + "name": "make_BookStepMM" + }, + { + "args": [ + "ctx", + "in" + ], + "lineno": 450, + "name": "make_BookStepMX" + }, + { + "args": [ + "ctx", + "out" + ], + "lineno": 455, + "name": "make_BookStepXM" + }, + { + "args": [ + "ctx", + "in", + "out" + ], + "lineno": 460, + "name": "make_BookStepMI" + }, + { + "args": [ + "ctx", + "in", + "out" + ], + "lineno": 465, + "name": "make_BookStepIM" + }, + { + "args": [ + "strand" + ], + "lineno": 468, + "name": "isDirectXrpToXrp" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 398, + "name": "test" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/Steps.h.ai.md b/include/xrpl/tx/paths/detail/Steps.h.ai.md new file mode 100644 index 0000000000..b4d4e0f3bd --- /dev/null +++ b/include/xrpl/tx/paths/detail/Steps.h.ai.md @@ -0,0 +1,59 @@ +# `include/xrpl/tx/paths/detail/Steps.h` + +This header is the architectural backbone of XRPL's payment flow engine. It defines the `Step` polymorphic interface, the `Strand` type alias, the factory functions for every concrete step variant, and the context and exception types that tie the whole system together. Every transaction that moves value through more than one trust line or offer book — payments, offer crossings, AMM swaps — goes through these abstractions. + +## The `Step` Abstract Interface + +`Step` defines a bidirectional evaluation protocol. Payment paths on XRPL are resolved in two passes: a reverse pass determines how much input is needed to produce a desired output, and a forward pass confirms the output produced by a given input. This matches the two pure virtual methods: + +- `rev(sb, afView, ofrsToRm, out)` — given a desired output amount, return the `(actual_in, actual_out)` pair that is achievable given current liquidity. +- `fwd(sb, afView, ofrsToRm, in)` — given an available input, return the `(actual_in, actual_out)` that the step can produce. + +Both pass two views into the ledger: `sb` (`PaymentSandbox`) carries the *running* state as the strand executes, while `afView` holds the ledger state *before* the strand started. The distinction matters for offer funding checks — whether an offer was already unfunded before this payment started, or became unfunded because of it. Unfunded or error-state offers are collected in `ofrsToRm` for later deletion regardless of whether the payment succeeds. + +Returning a pair rather than throwing allows the caller (`StrandFlow.h`) to detect the *limiting step* — the step where liquidity first runs out. That step is re-executed to establish the correct amounts, then the forward pass resumes from there. `cachedIn()` and `cachedOut()` expose the amounts stored by the most recent `rev()` call so the forward pass can seed itself from them. + +## The `StepImp` CRTP Mixin + +The five concrete step classes (`DirectStepI`, `BookStepII/IX/XI`, `XRPEndpointStep`, `MPTEndpointStep`, and MPT book variants) are typed in their input and output amounts — a `BookStepIX` always consumes `IOUAmount` and produces `XRPAmount`, for instance. At the strand level, however, everything must be type-erased to let `Step` hold them uniformly. + +`EitherAmount` (from `EitherAmount.h`) provides the type erasure: it is a `std::variant` with a checked `get()` accessor. The `StepAmount` concept (from `Concepts.h`) constrains template parameters to these three types. + +`StepImp` bridges the gap via CRTP. It implements `rev()`, `fwd()`, `isZero()`, `equalOut()`, and `equalIn()` by unwrapping the variant (checked at each call boundary) and forwarding to the concrete class's `revImp()` and `fwdImp()` methods which receive strongly-typed amounts. The pattern avoids scattering variant unwrapping into every concrete class, and catches type mismatches at runtime with a clear `std::logic_error` rather than undefined behavior. + +## Directionality Enums + +`DebtDirection` (`issues` / `redeems`) is not merely metadata — it drives transfer fee logic. When an account *redeems* (receives back its own IOU to cancel debt), no transfer fee is assessed on that side of the hop; when an account *issues* new credit, a fee may apply. `debtDirection()` is queried by `qualityUpperBound()` at each step so the quality estimate reflects actual fee impacts. `QualityDirection` (`in`/`out`) and `StrandDirection` (`forward`/`reverse`) similarly parametrize how quality is computed depending on which pass is running. + +## Quality Estimation and the AMM Extension + +`qualityUpperBound(v, prevStepDir)` computes a theoretical best-case quality (out/in ratio) for each step, propagating the `DebtDirection` through the chain. The `StrandFlow.h` engine uses this to sort competing strands from best to worst before committing to any execution, enabling best-quality-first liquidity selection without executing expensive steps speculatively. + +For AMM book steps, quality is not constant but a function of output amount (due to the constant-product pricing formula). `getQualityFunc()` provides the richer `QualityFunction` that encodes this non-linearity. All non-AMM steps inherit a default implementation that wraps `qualityUpperBound`'s result into a constant `QualityFunction{quality, CLOBLikeTag{}}`. This distinction is critical for the `limitOut()` optimization in `StrandFlow.h`: when a single strand contains AMM liquidity and a `limitQuality` constraint is active, the engine back-calculates the precise output required to exactly hit the quality limit, rather than executing to dryness and checking after the fact. + +## Strand Construction + +`Strand` is a `std::vector>`. Building one requires two steps: + +1. `normalizePath()` fills in implied nodes — XRPL allows callers to omit obvious intermediate accounts (e.g., the issuer of a currency) and this function inserts them, ensuring the path is unambiguous. +2. `toStrand()` iterates the normalized path and calls the appropriate `make_*` factory for each hop, threading `StrandContext` through each call. `toStrands()` applies this to an entire `STPathSet` plus an optional default (direct) path. + +## `StrandContext`: Construction-Time Safety + +`StrandContext` bundles all inputs needed to construct and validate a single step. Its two loop-detection sets are worth noting: `seenDirectAssets` (a two-element array of flat sets, tracking assets seen in direct hops at src and dst positions) and `seenBookOuts` (assets output by offer book hops). These enforce the invariant that a strand may not pass through the same account+currency node or output the same issue from two book steps — cycles that would allow value to circulate indefinitely. + +`StrandContext` also carries `prevStep` so each factory can query the preceding step's `debtDirection()` when checking the `noRipple` constraint: a path that enters and exits an account through two trust lines both marked `noRipple` must be rejected. + +## `FlowException` and Error Handling + +`FlowException` wraps a `TER` in a `std::runtime_error`. It is the signal for truly unexpected failures inside a step — states that cannot be handled by returning a zero amount. The single-strand `flow()` in `StrandFlow.h` catches it and returns a failed `StrandResult`, which the multi-strand engine treats as a dry strand rather than propagating the exception further. + +## `checkNear` and Numeric Precision + +`checkNear()` is overloaded for `IOUAmount`, `XRPAmount`, and `MPTAmount`. For `IOUAmount` it applies a tolerance check — IOU arithmetic uses a floating-point mantissa/exponent representation where accumulated rounding can produce near-but-not-exactly-equal values. For XRP and MPT, which are 64-bit integers, it compares exactly. This asymmetry is exposed to concrete step implementations that validate their forward-pass results in debug builds. + +## Factory Declarations + +The `make_*` functions declared at the bottom of the file — `make_DirectStepI`, `make_BookStepII`, `make_BookStepIX`, `make_BookStepXI`, `make_XRPEndpointStep`, `make_MPTEndpointStep`, and the MPT/IOU cross-book variants — each return `std::pair>`. Their implementations live in `DirectStep.cpp`, `BookStep.cpp`, `XRPEndpointStep.cpp`, and `PaySteps.cpp`. The `test::` namespace helpers (`directStepEqual`, `bookStepEqual`, etc.) provide white-box inspection for unit tests without exposing internal state through the production API. + +The `isDirectXrpToXrp()` template closes the file with a compile-time short-circuit: a two-step XRP→XRP strand is detected at instantiation time via `if constexpr`, and the flow engine skips executing it entirely since it cannot change value. \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h.ai.json b/include/xrpl/tx/paths/detail/StrandFlow.h.ai.json new file mode 100644 index 0000000000..0597971364 --- /dev/null +++ b/include/xrpl/tx/paths/detail/StrandFlow.h.ai.json @@ -0,0 +1,78 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "StrandResult" + }, + { + "args": [], + "lineno": 217, + "name": "FlowResult" + }, + { + "args": [ + "std::vector const& strands" + ], + "lineno": 312, + "name": "ActiveStrands" + } + ], + "description": "Implements the core logic for executing payment flows through strands (paths) in the XRPL ledger, including liquidity search, offer consumption, quality limiting, and payment sandbox management.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/paths/detail/StrandFlow.h", + "functions": [ + { + "args": [ + "PaymentSandbox const& baseView", + "Strand const& strand", + "std::optional const& maxIn", + "TOutAmt const& out", + "beast::Journal j" + ], + "lineno": 54, + "name": "flow" + }, + { + "args": [ + "ReadView const& v", + "Strand const& strand" + ], + "lineno": 246, + "name": "qualityUpperBound" + }, + { + "args": [ + "ReadView const& v", + "Strand const& strand", + "TOutAmt const& remainingOut", + "Quality const& limitQuality" + ], + "lineno": 266, + "name": "limitOut" + }, + { + "args": [ + "PaymentSandbox const& baseView", + "std::vector const& strands", + "TOutAmt const& outReq", + "bool partialPayment", + "OfferCrossing offerCrossing", + "std::optional const& limitQuality", + "std::optional const& sendMaxST", + "beast::Journal j", + "AMMContext& ammContext", + "path::detail::FlowDebugInfo* flowDebugInfo" + ], + "lineno": 393, + "name": "flow" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/paths/detail/StrandFlow.h.ai.md b/include/xrpl/tx/paths/detail/StrandFlow.h.ai.md new file mode 100644 index 0000000000..49f02a2e88 --- /dev/null +++ b/include/xrpl/tx/paths/detail/StrandFlow.h.ai.md @@ -0,0 +1,59 @@ +# `StrandFlow.h` — Core Payment Flow Execution Engine + +This header is the heart of the XRPL payment engine. It implements the two-pass strand execution algorithm and the outer multi-strand search loop that together convert a set of abstract payment paths into concrete ledger mutations. Every XRP payment — whether a direct transfer, a cross-currency payment, or an offer cross — is ultimately executed through the functions defined here. + +## Conceptual Model + +A **strand** is an ordered list of `Step` objects describing one route through the ledger: a sequence of accounts to ripple through (direct IOU hops) and/or order books to consume. The job of `StrandFlow.h` is to determine, for a requested output amount, how much input a given strand consumes and what state changes it produces — all inside a transactional `PaymentSandbox` that can be rolled back or merged. + +## `StrandResult` — Strand Execution Output + +`StrandResult` bundles everything produced by a single strand execution: the actual in/out amounts, a moved `PaymentSandbox` holding the proposed ledger state, a set of offer IDs to delete (`ofrsToRm`), an offer-consumption count, and an `inactive` flag indicating the strand is exhausted. Two constructors handle the two outcomes: a successful execution (all fields populated) and a failed/dry execution (only the offer-removal set, so bad offers are cleaned up even on failure). + +## The Two-Pass `flow()` for a Single Strand + +The single-strand `flow()` implements a classical reverse-then-forward algorithm that is non-trivial to follow but elegant in purpose. + +**Reverse pass (right to left):** Starting from the desired output, each step is called via `rev()` in reverse order. Each step reports how much input it needs to produce the requested output; that input amount becomes the "desired output" for the preceding step. This determines, without committing anything, whether the full `out` is achievable. + +**Limiting step detection:** If any step cannot satisfy its requested output exactly, that step is the *limiting step* — the bottleneck. When found, the algorithm discards the partial sandbox (`sb.emplace(&baseView)`), re-executes just the limiting step with the capped amount, records that capped amount as `limitStepOut`, and continues the reverse pass from there leftward. The step at index 0 is treated specially: if it would exceed `maxIn`, it is re-executed forward (`fwd()`) with exactly `maxIn` rather than in reverse. + +**Forward pass (left to right from the limiting step):** After the reverse pass establishes what every step to the left of the limiting step will do, the forward pass calls `fwd()` on every step to the right of the limiting step, threading the output of each step as the input to the next. This completes the sandbox state for steps that were not part of the reverse limiting adjustment. If any forward step produces zero output (a "dust" amount such as 10⁻⁸⁰ IOU into an XRP offer), the strand is abandoned. + +A debug-only re-validation block (`#ifndef NDEBUG`) re-executes the entire strand forward using `validFwd()` to confirm the final cached values match expectations — a canary that detects inconsistencies in step implementations without affecting production performance. + +The function short-circuits early for two edge cases: an empty strand (returns immediately) and a direct XRP-to-XRP strand detected via `isDirectXrpToXrp()`, which requires no execution at all. All exceptions of type `FlowException` are caught and converted to a dry result so a bad offer or overflow in one strand does not abort the entire payment. + +## `FlowResult` — Multi-Strand Output + +`FlowResult` accumulates the aggregate result across all strands: total in/out, the merged sandbox, removable offers, and a `TER` error code. Three constructors express success, failure with amounts, and failure without amounts — covering the different completion paths in the outer loop. + +## `qualityUpperBound()` — Strand Quality Estimation + +Before actually executing a strand, the engine needs to rank candidates. `qualityUpperBound()` computes an upper bound on a strand's exchange rate by calling `qualityUpperBound()` on each step in sequence and composing the results via `composed_quality()`. It propagates `DebtDirection` between steps (distinguishing issuance from redemption, which affects fees) and returns `std::nullopt` if any step is provably dry. This estimate may be optimistic — unfunded offers at the tip of a book can make the actual quality lower — but it serves as a sound ranking heuristic. + +## `limitOut()` — AMM Quality-Function Optimization + +When a payment has exactly one active strand and a `limitQuality` threshold is set, `limitOut()` can reduce the output request to the amount that exactly satisfies the quality constraint. This matters specifically for AMM-backed strands where quality is not constant: the AMM's spot price is a quadratic function of output, so a smaller output yields a better average quality. The function collects per-step `QualityFunction` objects and combines them into a single strand-level quality function, then solves for the output that achieves the limit quality via `qf->outFromAvgQ(limitQuality)`. A relative-distance guard (`withinRelativeDistance(..., 1e-9)`) absorbs floating-point rounding and avoids spurious adjustments. If the quality function is constant (no AMM steps), the function is a no-op. + +## `ActiveStrands` — Lazy Strand Candidate Management + +`ActiveStrands` tracks which strands are still eligible to provide liquidity. It maintains two sets: `cur_` (strands being evaluated in the current round) and `next_` (strands to evaluate next round, including any strand that still has liquidity after partial use). + +`activateNext()` is called at the start of each outer iteration. It sorts `next_` by `qualityUpperBound` (best quality first) using a `stable_sort` — the stability is required for deterministic ordering across different C++ standard library implementations, which is critical for consensus. Strands whose quality falls below `limitQuality` are pruned here. The sorted result becomes `cur_` for the current round. + +The probe-and-push pattern in the outer loop deserves attention: the loop iterates over `cur_` in quality order, calls the single-strand `flow()`, and takes the *first* strand that returns usable liquidity (`best`). All remaining unchecked strands in `cur_` are pushed back to `next_` via `pushRemainingCurToNext()`, and a non-exhausted `best` strand is also pushed back via `push()`. This means only one strand is consumed per outer iteration, ensuring that a high-quality strand offering partial liquidity is given priority in the next round while other strands remain in contention. + +## The Outer Multi-Strand `flow()` Loop + +The public-facing `flow()` for a vector of strands is the top-level payment loop. It tracks `remainingOut` and optionally `remainingIn` (from `sendMax`), iterating until both are satisfied, all strands are dry, or safety limits are hit. + +**Safety limits:** `maxTries = 1000` bounds the total number of outer iterations; `maxOffersToConsider = 1500` bounds total offer consumption across all strands. Exceeding either returns `telFAILED_PROCESSING`. These limits prevent adversarial paths from causing unbounded ledger work. + +**Precision:** Rather than accumulating a running total (which loses precision in floating-point IOU arithmetic), the engine collects each round's in/out amounts into `flat_multiset` containers (`savedIns`, `savedOuts`) and recomputes the total by summing smallest-to-largest via `std::accumulate`. `remainingOut` is then recomputed as `outReq - sum(savedOuts)` each round, preventing drift. + +**Offer cleanup:** Bad offers (`ofrsToRm`) are deleted from the sandbox immediately via `offerDelete()` at the end of each iteration, even if the strand failed. A separate `ofrsToRmOnFail` set accumulates all offers to be removed if the payment ultimately fails — these are propagated back to the caller so the ledger can be cleaned up regardless of payment outcome. + +**FillOrKill semantics:** The final section handles offer-crossing edge cases around the `fixFillOrKill` amendment. When crossing without `tfSell`, the engine must deliver the full `TakerPays`; when `tfSell` is set, the engine must consume the entire `TakerGets`. The logic branches on both the amendment flag and the `OfferCrossing` mode to return `tecPATH_PARTIAL` appropriately. + +**AMM integration:** `AMMContext` is updated after each successful round via `ammContext.update()`, incrementing the AMM iteration counter if AMM liquidity was used. The `setMultiPath()` call before each round informs the AMM whether it is competing with other strands, which affects how it prices its virtual offers. The `ammContext.clear()` before each strand execution resets the per-strand used flag so a failure in one strand does not incorrectly mark the next strand as having consumed AMM liquidity. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/account/AccountDelete.h.ai.json b/include/xrpl/tx/transactors/account/AccountDelete.h.ai.json new file mode 100644 index 0000000000..38ef2c891a --- /dev/null +++ b/include/xrpl/tx/transactors/account/AccountDelete.h.ai.json @@ -0,0 +1,89 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "view" + }, + { + "lineno": 21, + "name": "tx" + }, + { + "lineno": 24, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "AccountDelete" + } + ], + "description": "Defines the AccountDelete transaction handler class for the XRPL, including its methods and integration with the transaction processing framework.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/account/AccountDelete.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "AccountDelete" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 21, + "name": "calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 24, + "name": "preclaim" + }, + { + "args": [], + "lineno": 27, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/account/AccountDelete.h.ai.md b/include/xrpl/tx/transactors/account/AccountDelete.h.ai.md new file mode 100644 index 0000000000..d61e5b3a94 --- /dev/null +++ b/include/xrpl/tx/transactors/account/AccountDelete.h.ai.md @@ -0,0 +1,64 @@ +# `AccountDelete.h` — AccountDelete Transaction Handler + +## Role in the System + +`AccountDelete.h` declares the `AccountDelete` transactor, the class responsible for processing the XRPL `AccountDelete` transaction type. It fits into the broader transaction processing framework by extending the `Transactor` base class and overriding the standard preflight/preclaim/`doApply` pipeline. AccountDelete is one of the most structurally complex transactors in the ledger because it must safely dismantle an account — wiping out all its owned ledger objects, transferring its remaining XRP balance, and then erasing the account root entry itself. + +## Class Declaration + +```cpp +class AccountDelete : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Blocker}; + ... +}; +``` + +The `ConsequencesFactory{Blocker}` declaration is significant. The `Blocker` type tells the transaction queuing system that if an `AccountDelete` is pending for an account, no other transaction from that same account should be queued behind it. This is a safety invariant: because account deletion clears sequence numbers and owner objects, allowing subsequent transactions to queue up behind a pending delete would create undefined or dangerous state. + +## Processing Pipeline + +### `checkExtraFeatures` + +Called by `invokePreflight` before the field-level checks, this method gates the entire transaction on a single condition: if the transaction includes the `sfCredentialIDs` field, the `featureCredentials` amendment must be enabled. Returning `false` causes `invokePreflight` to return `temDISABLED`. This is the preferred pattern for amendment gating in the new framework — implemented here instead of inside `preflight()` proper. + +### `preflight` + +The `preflight` implementation performs stateless early rejection. It rejects the trivial self-send case (`sfAccount == sfDestination` → `temDST_IS_SRC`) and delegates credential field format validation to `credentials::checkFields`. No ledger state is consulted here — `preflight` only has access to the raw transaction fields and active rule set. + +### `calculateBaseFee` + +```cpp +static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); +``` + +AccountDelete deliberately overrides the base class fee calculation. Instead of the standard reference fee, it calls `calculateOwnerReserveFee()`, which returns one owner reserve unit. This effectively prices account deletion at the cost of one reserve increment, making the operation economically meaningful and discouraging spam. The higher fee is also a design signal: account deletion is intentional and consequential, not routine. + +### `preclaim` + +`preclaim` is the heavyweight guard stage. It performs all stateful checks against a read-only view of the ledger: + +1. **Destination validation** — the destination account must exist (`tecNO_DST`), and if it requires a destination tag, the transaction must supply one. +2. **DepositAuth check** — if the destination has `lsfDepositAuth` set and no credentials are provided, the sender must have a pre-authorized deposit entry. If credentials are provided, this check is intentionally deferred to `doApply` so that expired credentials are caught at apply-time rather than claim-time. +3. **NFToken obligations** — two separate checks prevent deletion if the account has outstanding NFTs. First, if the number of minted NFTokens doesn't match burned tokens (the account is still an active issuer with live NFTs), it returns `tecHAS_OBLIGATIONS`. Second, the code checks for any owned NFToken page entries. +4. **Sequence delta guard** — the account's own sequence number must be at least 256 below the current ledger sequence. This prevents replay attacks: if an account is deleted and then recreated with the same address, any old signed transactions (with sequence numbers close to the deletion point) would otherwise become valid again. The 256-ledger window provides a safe buffer since transactions expire within that range. +5. **NFToken sequence guard** — a more subtle variant of the same anti-replay concern: `FirstNFTokenSequence + MintedNFTokens + 255` must not exceed the current ledger. Without this, an authorized minter could have minted NFTs on behalf of the issuer without advancing the issuer's own sequence, allowing duplicate NFTokenIDs after account recreation. +6. **Owner directory sweep** — the method enumerates every entry in the account's owner directory and calls `nonObligationDeleter()` on each type. If any entry has no registered deleter (e.g., a trust line with a non-zero balance, or an escrow), the entire transaction fails with `tecHAS_OBLIGATIONS`. If the directory has more than `maxDeletableDirEntries` entries, it returns `tefTOO_BIG` — avoiding unbounded work in a single transaction. + +The `nonObligationDeleter()` helper (file-local) is a dispatch table implemented as a `switch` over `LedgerEntryType`, returning a `DeleterFuncPtr` for each cleanable type: offers, signer lists, tickets, deposit preauth entries, NFToken offers, DIDs, oracles, credentials, and delegate objects. Any unrecognized type returns `nullptr`, which signals an obligation that blocks deletion. + +### `doApply` + +`doApply` is the mutation stage. It assumes `preclaim` has already validated the ledger state and proceeds to: + +1. **Credential-based DepositAuth** — if `sfCredentialIDs` are present, it now calls `verifyDepositPreauth` to check both authorization and credential expiry. This is the deferred check skipped in `preclaim`. +2. **Owner directory cleanup** — calls `cleanupOnAccountDelete()` with a lambda that invokes the appropriate `nonObligationDeleter` for each remaining directory entry. The lambda returns `SkipEntry::No` uniformly, meaning no entries are skipped during iteration. +3. **XRP transfer** — after all owned objects are removed, the remaining XRP balance is transferred to the destination account directly by adjusting both SLE balances and calling `ctx_.deliver()`. +4. **Directory deletion** — the (now presumably empty) owner directory root node is erased. A non-empty directory at this point is treated as a ledger integrity error. +5. **Password flag reset** — if XRP was transferred and the destination had spent its free password-change credit (`lsfPasswordSpent`), the flag is cleared. This is a minor courtesy side-effect of receiving XRP. +6. **Account erasure** — `view().erase(src)` removes the account root SLE from the ledger. + +## Design Relationships + +`AccountDelete.h` is tightly coupled to the broader transactor ecosystem. The `Transactor` base (defined in `Transactor.h`) provides the `invokePreflight` template that orchestrates `checkExtraFeatures` → `preflight1` → `preflight` → `preflight2`, as well as the `ticketDelete()` helper that `AccountDelete` delegates to for ticket removal. The implementation file pulls in helpers from `SignerListSet`, `DIDDelete`, `OracleDelete`, `DepositPreauth`, and credential utilities — each contributing a type-specific deletion routine that fits the `DeleterFuncPtr` signature. This delegation model keeps `AccountDelete` free of per-object implementation details while remaining the single authoritative orchestrator for account teardown. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/account/AccountSet.h.ai.json b/include/xrpl/tx/transactors/account/AccountSet.h.ai.json new file mode 100644 index 0000000000..fc7d71cb70 --- /dev/null +++ b/include/xrpl/tx/transactors/account/AccountSet.h.ai.json @@ -0,0 +1,76 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 8, + "name": "AccountSet" + } + ], + "description": "Defines the AccountSet transaction transactor class for handling AccountSet operations in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/account/AccountSet.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "AccountSet" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 24, + "name": "checkPermission" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 27, + "name": "preclaim" + }, + { + "args": [], + "lineno": 29, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/account/AccountSet.h.ai.md b/include/xrpl/tx/transactors/account/AccountSet.h.ai.md new file mode 100644 index 0000000000..43ca540231 --- /dev/null +++ b/include/xrpl/tx/transactors/account/AccountSet.h.ai.md @@ -0,0 +1,62 @@ +# `AccountSet.h` — AccountSet Transaction Transactor + +## Role in the System + +`AccountSet` is the transactor responsible for processing `AccountSet` transactions on the XRP Ledger — the primary mechanism by which account owners configure their account root entry. This covers a wide surface area: behavioral flags (require destination tags, disable master key, no-freeze, global freeze, deposit auth, disallow-incoming variants), metadata fields (domain, email hash, message key, wallet locator), economic parameters (transfer rate, tick size), and NFT-related settings (authorized minter). The class lives in the `account/` transactor group alongside `AccountDelete`, `SetRegularKey`, and `SignerListSet`. + +## Class Structure and Inheritance + +`AccountSet` extends `Transactor`, the abstract base for all transaction processors. The interface follows the framework's three-phase pipeline executed by `Transactor::operator()()`: + +1. **`preflight`** — stateless, fee-free validation against the transaction's raw fields. +2. **`preclaim`** — read-only ledger checks that may still reject the transaction. +3. **`doApply`** — the mutating phase that writes changes to the ledger. + +Two additional static methods complete the interface: `makeTxConsequences` and `getFlagsMask`. + +Because the base class uses compile-time polymorphism (via the `invokePreflight` template selecting static methods by name) rather than virtual dispatch, the `static` keyword on `preflight`, `preclaim`, `getFlagsMask`, `makeTxConsequences`, and `checkPermission` is architecturally significant, not incidental. The constructor is a trivial forwarding constructor to `Transactor(ctx)`. + +## Custom Consequences + +The `ConsequencesFactory{Custom}` declaration tells the framework to call `makeTxConsequences` rather than using the generic normal or blocker factories. `AccountSet` needs this because most AccountSet transactions are *normal* (they don't affect the ordering of other transactions in the queue), but transactions that set or clear `asfRequireAuth`, `asfDisableMaster`, or `asfAccountTxnID` — or use the legacy `tfRequireAuth`/`tfOptionalAuth` flags — are classified as *blockers*. Blockers prevent subsequent transactions from the same account from being queued until the blocker confirms, guarding against state-dependent sequences like establishing trust lines before enabling RequireAuth. + +## Preflight: Dual-Path Flag System + +`getFlagsMask` simply returns `tfAccountSetMask`, routing unknown transaction flags to rejection in the framework's `preflight1` call. + +`preflight` must handle a historical dual-path flag system: some flags can be specified as bitfield flags in the transaction's `Flags` field (the legacy `tfRequireAuth`, `tfRequireDestTag`, `tfDisallowXRP` family) *or* as a numeric value in the `sfSetFlag`/`sfClearFlag` fields (the modern `asf*` constants). The validation logic consolidates both paths into boolean variables like `bSetRequireAuth` and `bClearRequireAuth`, then checks they don't conflict. This duplication exists because the flag-field interface predates the `sfSetFlag` interface; both remain live for backward compatibility. + +Beyond flag coherence, `preflight` validates: +- `TransferRate`: must be zero (meaning "unset") or in the range `[QUALITY_ONE, 2×QUALITY_ONE]`. Values below `QUALITY_ONE` are invalid (a discount rate would let issuers create value from nothing). +- `TickSize`: must be zero (unset) or within `[Quality::minTickSize, Quality::maxTickSize]`. +- `MessageKey`: if present and non-empty, must be a valid public key type. +- `Domain`: bounded by `maxDomainLength`. +- `sfNFTokenMinter` presence is validated against the set/clear flag — setting `asfAuthorizedNFTokenMinter` requires the field present, clearing it requires it absent. + +## `checkPermission`: Granular Delegation Model + +`checkPermission` handles the delegate-signing case, where a third-party account submits an AccountSet on behalf of the account owner. The method is deliberately restrictive: if a `sfDelegate` field is present, the transaction is rejected unless the delegate's `DelegateObject` in the ledger explicitly lists granular permissions for each field being modified. + +The design reflects a deliberate policy choice: AccountSet is too sensitive to grant wholesale. Flags and `sfSetFlag`/`sfClearFlag` fields are categorically blocked for delegates — any attempt to set or clear behavioral flags returns `terNO_DELEGATE_PERMISSION`. Only the narrow metadata fields (`sfEmailHash`, `sfMessageKey`, `sfDomain`, `sfTransferRate`, `sfTickSize`) are delegatable, and only if the corresponding granular permission constant (e.g., `AccountDomainSet`, `AccountTransferRateSet`) has been granted. `sfWalletLocator` and `sfNFTokenMinter` are unconditionally blocked from delegates. + +## `preclaim`: Ledger-State Constraints + +Two important state-dependent checks occur here, after the ledger is readable but before any state is mutated: + +- **RequireAuth**: Setting `asfRequireAuth` is rejected if the account already has entries in its owner directory (`tecOWNERS`/`terOWNERS` depending on the retry flag). This prevents retroactively breaking existing trust relationships — you cannot mandate authorization after the fact. +- **Clawback / NoFreeze mutual exclusion**: When the `featureClawback` amendment is enabled, `asfAllowTrustLineClawback` cannot be set if `lsfNoFreeze` is already set (and vice versa). Clawback also requires an empty owner directory for the same reason as RequireAuth. These are enforced in `preclaim` rather than `doApply` because they need to produce consensus-safe error codes (`tecNO_PERMISSION`, `tecOWNERS`) that fee-charge the submitter. + +## `doApply`: The Mutation Phase + +`doApply` reads the current account SLE via `view().peek()`, computes the new `sfFlags` bitmask by applying each flag change in sequence, updates non-flag fields directly on the SLE, and calls `ctx_.view().update(sle)` once at the end. + +Two security-sensitive flags require extra checks at apply time: + +- **DisableMaster** (`asfDisableMaster`): The transaction must have been signed with the master key itself (`sigWithMaster` determined by comparing the signing public key's derived account ID against `account_`), and the account must already have a `sfRegularKey` or a multi-signer list (`keylet::signers`). This prevents an account from permanently locking itself out. +- **NoFreeze** (`asfNoFreeze`): Likewise requires a master key signature when master is still enabled. Note that `asfNoFreeze` is permanent — once set, it cannot be cleared, so the code only handles the set path. + +The interlock between `asfGlobalFreeze` and `asfNoFreeze` is notable: once `lsfNoFreeze` is active, the account cannot clear `lsfGlobalFreeze`. The rationale is anti-manipulation — without this constraint, an issuer with NoFreeze could still selectively freeze and unfreeze the market by toggling GlobalFreeze. + +Amendment-gated flag handling (`featureTokenEscrow`, `featureClawback`) uses runtime checks via `ctx_.view().rules().enabled(...)`, ensuring that new flag semantics only activate after the corresponding network amendment is live. + +For scalar fields (domain, email hash, message key, wallet locator, transfer rate, tick size), an empty or zero value signals deletion (`makeFieldAbsent`) rather than an explicit clear flag, which keeps the account SLE compact. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/account/SetRegularKey.h.ai.json b/include/xrpl/tx/transactors/account/SetRegularKey.h.ai.json new file mode 100644 index 0000000000..2781c58eba --- /dev/null +++ b/include/xrpl/tx/transactors/account/SetRegularKey.h.ai.json @@ -0,0 +1,50 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "SetRegularKey" + } + ], + "description": "Defines the SetRegularKey transaction transactor class for the XRPL, which handles logic for setting or removing a regular key on an account.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/account/SetRegularKey.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "SetRegularKey" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 19, + "name": "calculateBaseFee" + }, + { + "args": [], + "lineno": 22, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/account/SetRegularKey.h.ai.md b/include/xrpl/tx/transactors/account/SetRegularKey.h.ai.md new file mode 100644 index 0000000000..956c0b0495 --- /dev/null +++ b/include/xrpl/tx/transactors/account/SetRegularKey.h.ai.md @@ -0,0 +1,43 @@ +# `SetRegularKey.h` — SetRegularKey Transaction Transactor + +## Role in the System + +`SetRegularKey` is the transactor responsible for processing `SetRegularKey` transactions on the XRP Ledger. This transaction type allows an account holder to assign a secondary cryptographic keypair — the "regular key" — to their account, or to revoke a previously assigned one. By separating signing authority from account ownership, XRPL accounts can protect their master key in cold storage while using the regular key for day-to-day operations. This file declares the class; the implementation lives in `src/libxrpl/tx/transactors/account/SetRegularKey.cpp`. + +## Class Structure and Base Contract + +`SetRegularKey` extends `Transactor`, the abstract base for all XRPL transaction types. The `Transactor` base enforces a three-phase processing pipeline: + +- **Preflight**: stateless validity checks against the raw transaction (no ledger access) +- **Preclaim**: ledger-aware checks before fee deduction (inherited as a no-op here) +- **Apply** (`doApply`): the actual mutation of ledger state + +The static methods (`preflight`, `calculateBaseFee`) participate in a deliberate compile-time polymorphism scheme via name-hiding rather than virtual dispatch, as documented in `Transactor.h`. The `invokePreflight` template calls `T::preflight` and `T::calculateBaseFee` directly, so `SetRegularKey` overrides these without marking them `virtual`. + +## `ConsequencesFactory{Blocker}` + +The class declares `ConsequencesFactory` as `Blocker`, distinguishing it from `Normal` and `Custom` variants. A blocker transaction prevents other transactions from the same account from being queued ahead of or alongside it. This is semantically correct: changing which key signs for an account can invalidate the signatures of any other queued transactions, so the ledger conservatively blocks the queue until this transaction settles. + +## Custom Fee Logic: The One-Time Free Regular Key + +`calculateBaseFee` overrides the base implementation to implement a fee-waiver mechanic. If the transaction is signed directly with the account's master key — verified by checking that `calcAccountID(signingPublicKey) == accountID` — and the account's `lsfPasswordSpent` flag is not yet set, the function returns `XRPAmount{0}`, making the transaction free. + +This exists to support a bootstrapping pattern where an account is funded with a pre-configured "password" (a well-known or operator-assigned key), and the first act of the new account owner is to replace that key with their own. The ledger waives the fee for this first use, lowering the barrier to entry. Once used, `doApply` sets `lsfPasswordSpent` on the account SLE, ensuring the waiver is consumed exactly once. + +## Preflight Validation + +`preflight` performs a single but important semantic check: it rejects the transaction with `temBAD_REGKEY` if the regular key being assigned is the same as the account itself (`sfRegularKey == sfAccount`). Setting the regular key to the master key's account ID is a degenerate no-op that would likely indicate a mistake, so it is caught early before any ledger state is touched. + +## Apply Logic and Safety Invariants + +`doApply` handles two distinct operations depending on whether `sfRegularKey` is present in the transaction: + +**Setting the key**: If `sfRegularKey` is present, it is written directly to the account SLE via `setAccountID`. Before doing so, if the fee was not charged (i.e., `minimumFee` returns zero, signaling the password-waiver path was used), the `lsfPasswordSpent` flag is set on the account to prevent future reuse. + +**Removing the key**: If `sfRegularKey` is absent, the transactor interprets this as a removal request. Here, a critical safety invariant is enforced: before calling `makeFieldAbsent(sfRegularKey)`, the code checks whether the master key is disabled (`lsfDisableMaster`) and no multisig signer list exists (`keylet::signers(account_)`). If both conditions hold, removing the regular key would leave the account with no valid signing path, permanently locked. This is rejected with `tecNO_ALTERNATIVE_KEY`. Only when at least one alternative remains is the field removed. + +This guard is architecturally essential: the XRPL has no account recovery mechanism, so the ledger itself must prevent self-inflicted lockout. + +## Relationship to Sibling Transactors + +Among the account-management transactors (`AccountSet`, `AccountDelete`, `SignerListSet`, `SetRegularKey`), this one has the simplest interface — no `preclaim` override, no custom `getFlagsMask`. The `AccountSet` transactor, by contrast, uses `ConsequencesFactory{Custom}` and provides `makeTxConsequences`, reflecting its more complex flag interactions. `SetRegularKey`'s simplicity is appropriate: it mutates exactly one field on one SLE, with one safety gate. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/account/SignerListSet.h.ai.json b/include/xrpl/tx/transactors/account/SignerListSet.h.ai.json new file mode 100644 index 0000000000..ae54dd7d14 --- /dev/null +++ b/include/xrpl/tx/transactors/account/SignerListSet.h.ai.json @@ -0,0 +1,95 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 13, + "name": "SignerListSet" + } + ], + "description": "Implements the SignerListSet transaction for the XRPL, handling the setting and removal of signer lists for multi-signature accounts.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/account/SignerListSet.h", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 27, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 30, + "name": "preflight" + }, + { + "args": [], + "lineno": 33, + "name": "doApply" + }, + { + "args": [], + "lineno": 35, + "name": "preCompute" + }, + { + "args": [ + "ServiceRegistry& registry", + "ApplyView& view", + "AccountID const& account", + "beast::Journal j" + ], + "lineno": 39, + "name": "removeFromLedger" + }, + { + "args": [ + "STTx const& tx", + "ApplyFlags flags", + "beast::Journal j" + ], + "lineno": 46, + "name": "determineOperation" + }, + { + "args": [ + "std::uint32_t quorum", + "std::vector const& signers", + "AccountID const& account", + "beast::Journal j", + "Rules const&" + ], + "lineno": 51, + "name": "validateQuorumAndSignerEntries" + }, + { + "args": [], + "lineno": 58, + "name": "replaceSignerList" + }, + { + "args": [], + "lineno": 59, + "name": "destroySignerList" + }, + { + "args": [ + "SLE::pointer const& ledgerEntry", + "std::uint32_t flags" + ], + "lineno": 62, + "name": "writeSignersToSLE" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/account/SignerListSet.h.ai.md b/include/xrpl/tx/transactors/account/SignerListSet.h.ai.md new file mode 100644 index 0000000000..62272a7b60 --- /dev/null +++ b/include/xrpl/tx/transactors/account/SignerListSet.h.ai.md @@ -0,0 +1,55 @@ +# `SignerListSet.h` — Multi-Signature Signer List Transactor + +`SignerListSet` implements the XRPL transaction type that manages an account's multi-signature signer list. It allows an account holder to create or replace a signer list (establishing who can co-sign future transactions and with what voting weight), or to destroy the list entirely, reverting the account to single-key signing. The class lives in the account-management transactor group alongside `AccountDelete`, `AccountSet`, and `SetRegularKey`. + +## Inheritance and Role in the Transactor Framework + +`SignerListSet` extends `Transactor`, the abstract base for all XRPL transaction processors. The base class handles fee payment, sequence number consumption, and signature verification; derived classes fill in `preflight()` (stateless validation), `preCompute()` (state extraction before applying), and `doApply()` (ledger mutation). `SignerListSet` follows this three-phase contract faithfully. + +`ConsequencesFactory` is set to `Blocker`, meaning a queued `SignerListSet` transaction blocks any later transaction from the same account from being applied until this one is either applied or dropped. This is conservative but correct — changing who can sign an account has security implications that justify serializing processing. + +## Cached Computation via `preCompute()` + +Rather than re-parsing the transaction twice, the class caches three values as private members: `do_` (an `Operation` enum), `quorum_`, and `signers_`. These are populated by `preCompute()` before `doApply()` runs. The `Operation` enum has three values: `unknown`, `set`, and `destroy`. The choice of which operation to perform is determined entirely by the transaction's `sfSignerQuorum` and `sfSignerEntries` fields: + +- **Non-zero quorum + `sfSignerEntries` present** → `set` (create or replace) +- **Zero quorum + no `sfSignerEntries`** → `destroy` +- Any other combination → `unknown`, which maps to `temMALFORMED` + +This dispatch logic lives in the private static `determineOperation()`, which is called from both `preflight()` and `preCompute()`. Calling it in `preflight()` allows stateless validation to reject malformed transactions before any ledger access occurs; calling it again in `preCompute()` restores the parsed data in instance state for use in `doApply()`. The assertion in `preCompute()` confirms that by this stage the operation must be well-formed — validation has already passed. + +## Validation in `preflight()` + +The public static `preflight()` delegates to `determineOperation()` and, for `set` operations, to `validateQuorumAndSignerEntries()`. This second validator enforces several invariants: + +1. The signer count must fall within `[STTx::minMultiSigners, STTx::maxMultiSigners]` (currently 1–32). +2. No duplicate signers. Since `determineOperation()` sorts the deserialized list, duplicates are found in O(n) with `std::adjacent_find`. +3. No signer may reference the signing account itself (`temBAD_SIGNER`), preventing trivial bypass of the quorum mechanism. +4. Every signer must have a positive weight; a zero-weight signer contributes nothing and is disallowed (`temBAD_WEIGHT`). +5. The sum of all signer weights must be at least equal to the quorum (`temBAD_QUORUM`). This is a reachability check — a quorum that no combination of signers can reach would permanently lock the account. + +The validator deliberately does **not** verify that signer accounts exist in the ledger, because XRPL permits "phantom accounts" as signers. This is a documented design choice noted in the implementation. + +## Applying the Operation + +`doApply()` simply switches on the cached `do_` field and delegates to `replaceSignerList()` or `destroySignerList()`. + +**`replaceSignerList()`** handles both creation and replacement with a single code path: it first removes any existing signer list (via the internal `removeSignersFromLedger()` helper), then checks the updated reserve, and finally inserts a fresh `ltSIGNER_LIST` ledger entry. Removing the old list before checking the new reserve is intentional — the deletion may reduce the owner count, potentially making the reserve check pass when it otherwise would not. The new list always sets `lsfOneOwnerCount`, indicating that the `MultiSignReserve` amendment applies and the list occupies exactly one owner-count unit regardless of how many signers it contains. + +**`destroySignerList()`** has a critical safety check before removal: if the account has the master key disabled (`lsfDisableMaster`) and has no `sfRegularKey` set, destroying the signer list would leave the account with no signing mechanism, effectively bricking it. The operation is rejected with `tecNO_ALTERNATIVE_KEY` in that case. + +## Owner Count and the `MultiSignReserve` Amendment + +Owner-count accounting for signer lists has an amendment-aware dual mode. Before `MultiSignReserve`, the reserve cost was `2 + N` (two base units plus one per signer). After the amendment, new lists always pay exactly 1 unit (`lsfOneOwnerCount`). The removal path must handle both, because old ledger objects may pre-date the amendment. The `signerCountBasedOwnerCountDelta()` free function computes the legacy adjustment; the `removeSignersFromLedger()` helper inspects the `lsfOneOwnerCount` flag on the existing SLE to choose the correct calculation. + +## Cross-Transactor Interface: `removeFromLedger()` + +The static public `removeFromLedger()` method exposes signer list cleanup as a well-defined interface for `AccountDelete`. When an account is deleted, all its owned ledger objects must be removed first, and the `AccountDelete` transactor calls into `SignerListSet` rather than duplicating the removal logic. The signature takes an `ApplyView&` and `ServiceRegistry&` directly rather than going through an instance, keeping it usable from a different transactor's `doApply()`. + +## `writeSignersToSLE()` + +This helper serializes the in-memory signer list into the ledger SLE. It conditionally sets `sfOwner` only when the `fixIncludeKeyletFields` amendment is active, and writes `sfWalletLocator` (the signer tag) only when a tag is actually present. A comment explicitly calls this out as defensive: the optional write ensures no spurious tag field is ever serialized into the ledger even if the `tag` member is default-initialized. + +## Flags Mask Handling + +`getFlagsMask()` returns `tfUniversalMask` if the `fixInvalidTxFlags` amendment is enabled, otherwise it returns `0` (which in the framework means "allow any flags"). This backward-compatible choice lets the validator reject unknown flags on updated networks while preserving legacy behavior on older rulesets. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/bridge/XChainBridge.h.ai.json b/include/xrpl/tx/transactors/bridge/XChainBridge.h.ai.json new file mode 100644 index 0000000000..76beefd116 --- /dev/null +++ b/include/xrpl/tx/transactors/bridge/XChainBridge.h.ai.json @@ -0,0 +1,343 @@ +{ + "args": [ + { + "lineno": 16, + "name": "ApplyContext& ctx" + }, + { + "lineno": 31, + "name": "ApplyContext& ctx" + }, + { + "lineno": 55, + "name": "ApplyContext& ctx" + }, + { + "lineno": 80, + "name": "ApplyContext& ctx" + }, + { + "lineno": 102, + "name": "ApplyContext& ctx" + }, + { + "lineno": 125, + "name": "ApplyContext& ctx" + }, + { + "lineno": 140, + "name": "ApplyContext& ctx" + }, + { + "lineno": 165, + "name": "ApplyContext& ctx" + }, + { + "lineno": 19, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 22, + "name": "PreclaimContext const& ctx" + }, + { + "lineno": 34, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 37, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 40, + "name": "PreclaimContext const& ctx" + }, + { + "lineno": 62, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 65, + "name": "PreclaimContext const& ctx" + }, + { + "lineno": 77, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 81, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 84, + "name": "PreclaimContext const& ctx" + }, + { + "lineno": 104, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 107, + "name": "PreclaimContext const& ctx" + }, + { + "lineno": 127, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 130, + "name": "PreclaimContext const& ctx" + }, + { + "lineno": 142, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 145, + "name": "PreclaimContext const& ctx" + }, + { + "lineno": 170, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 173, + "name": "PreclaimContext const& ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 13, + "name": "XChainCreateBridge" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 28, + "name": "BridgeModify" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 52, + "name": "XChainClaim" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 72, + "name": "XChainCommit" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 99, + "name": "XChainCreateClaimID" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 122, + "name": "XChainAddClaimAttestation" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 137, + "name": "XChainAddAccountCreateAttestation" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 162, + "name": "XChainCreateAccountCommit" + } + ], + "description": "Defines classes for cross-chain transaction types and related logic in the XRPL (XRP Ledger) protocol, including bridge creation, modification, claims, commits, attestations, and account creation for cross-chain transfers.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/bridge/XChainBridge.h", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 19, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 22, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 34, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 37, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 40, + "name": "preclaim" + }, + { + "args": [], + "lineno": 42, + "name": "doApply" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 62, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 65, + "name": "preclaim" + }, + { + "args": [], + "lineno": 67, + "name": "doApply" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 77, + "name": "makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 81, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 84, + "name": "preclaim" + }, + { + "args": [], + "lineno": 86, + "name": "doApply" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 104, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 107, + "name": "preclaim" + }, + { + "args": [], + "lineno": 109, + "name": "doApply" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 127, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 130, + "name": "preclaim" + }, + { + "args": [], + "lineno": 132, + "name": "doApply" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 142, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 145, + "name": "preclaim" + }, + { + "args": [], + "lineno": 147, + "name": "doApply" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 170, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 173, + "name": "preclaim" + }, + { + "args": [], + "lineno": 175, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/bridge/XChainBridge.h.ai.md b/include/xrpl/tx/transactors/bridge/XChainBridge.h.ai.md new file mode 100644 index 0000000000..0703090a7d --- /dev/null +++ b/include/xrpl/tx/transactors/bridge/XChainBridge.h.ai.md @@ -0,0 +1,47 @@ +# `XChainBridge.h` — Cross-Chain Bridge Transaction Declarations + +This header is the authoritative declaration point for all eight transaction types implementing XRPL's cross-chain bridge protocol. It defines the complete lifecycle of a cross-chain value transfer: establishing bridge infrastructure, initiating transfers on the source chain, relaying witness attestations, and finalizing fund claims on the destination chain. The file lives in the `bridge/` subdirectory under `transactors/`, grouping it with other feature-specific transactor declarations across the codebase. + +## Architecture: The `Transactor` Pattern + +Every class here inherits from `Transactor` and follows the three-phase execution pipeline mandatory for all XRPL transaction processing: + +- **`preflight(PreflightContext const&)`** — Stateless validation run before any ledger access. Returns `NotTEC`, a type that encodes only non-TEC error codes. This is the cheapest gate and runs without ledger I/O. +- **`preclaim(PreclaimContext const&)`** — Read-only ledger state checks run after preflight passes. Returns `TER`. Can inspect account balances, existing objects, and signer lists without mutating state. +- **`doApply()`** — The state-mutating execution step, called only after both prior phases succeed. + +The `Transactor` base class enforces this pipeline through its `invokePreflight` template, which sequences amendment checks, `preflight1`/`preflight2` (the framework's own flag and signature checks), and then the derived class's `preflight()` via static name resolution — notably *not* virtual dispatch. Derived classes cannot skip framework checks by overriding `invokePreflight`. + +## Bridge Infrastructure Transactions + +`XChainCreateBridge` attaches a bridge definition to a "door account" — the custody point that holds locked assets on one side of the bridge. This is a one-time setup transaction. The door account then acts as the anchor for all subsequent cross-chain activity on that bridge. `BridgeModify` (aliased as `XChainModifyBridge`) handles post-creation parameter changes. Uniquely among the bridge transactors, it overrides `getFlagsMask()` — most transactors let the base class handle flag validation, but bridge modification supports flag-driven toggles (e.g., enabling or disabling features) that require a custom mask. + +Both use `ConsequencesFactory{Normal}`, indicating their ledger impact is fully predictable at consequence-calculation time: they create or modify a single ledger object with no contingent side effects. + +## The Normal Cross-Chain Transfer Sequence + +A standard cross-chain transfer is a four-step protocol that spans two independent ledgers: + +**Step 1 — `XChainCreateClaimID`**: The recipient creates a claim ID object on the *destination* chain first, reserving a monotonically-increasing sequence number. The source account that will commit funds must be specified here, binding authorization to a single account before any funds move. This ordering is the primary anti-replay mechanism: a claim ID is destroyed upon successful claim, making it single-use. + +**Step 2 — `XChainCommit`**: The sender locks funds on the *source* chain, referencing the previously-created claim ID. This transactor uses `ConsequencesFactory{Custom}` with an explicit `makeTxConsequences()` static method. The custom factory exists because the amount being committed must be accurately reflected in consequence calculations — the committed funds are effectively removed from the sender's balance, a non-standard consequence that `Normal` cannot model. + +**Step 3 — `XChainAddClaimAttestation`**: Off-chain witness servers submit cryptographic signatures to the destination chain, each attesting that the commit event occurred. Both attestation classes (`XChainAddClaimAttestation` and `XChainAddAccountCreateAttestation`) use `ConsequencesFactory{Blocker}`. The comment in the source is explicit: "Blocker since we cannot accurately calculate the consequences." Attestation submission may or may not push the running signature count past quorum, and crossing quorum triggers immediate fund movement. Because this outcome is not determinable at fee-calculation time, the engine conservatively marks the entire transaction as a blocker to prevent underestimated ordering constraints downstream. + +**Step 4 — `XChainClaim`**: Once a quorum of attestations is collected, the recipient submits this transaction to move funds on the destination chain. On success, the `XChainClaimID` ledger object is destroyed. The key design decision here is the failure-recovery path: if `XChainClaim` fails for any reason, the claim ID *survives* and the transaction can be re-submitted with different parameters. This is also `ConsequencesFactory{Blocker}` for the same non-deterministic-impact reasoning. The distinction from the account-creation path below is important — here, recovery is always possible. + +## Account Creation Flow and Its Operational Risks + +`XChainCreateAccountCommit` and `XChainAddAccountCreateAttestation` address the bootstrapping problem: how does a user who has no account on the destination chain receive funds? The normal flow requires the recipient to create a claim ID first, but that requires an existing destination account. + +The solution substitutes ordering-based replay prevention for the object-based mechanism: account creation commits are processed in strict source-chain sequence, enforced by the `createCount` field in `AttestationCreateAccount`. The constant `xbridgeMaxAccountCreateClaims = 128` caps the maximum queue depth for pending account-create claims — an important bound on both memory usage and processing latency. + +This approach carries an explicitly documented hazard: if any attestation in the sequence is not delivered to the destination chain, *all subsequent account creations are permanently blocked*, with no recovery mechanism. The bridge's `MinAccountCreateAmount` field doubles as a feature gate — its absence disables `XChainCreateAccountCommit` entirely on a given bridge, allowing operators to opt out of this risk. The comment also restricts this path to XRP-to-XRP bridges only. + +Critically, `XChainCreateAccountCommit` has no error recovery. If the destination-side claim fails, the committed XRP is permanently lost. This is a deliberate tradeoff: the ordering mechanism does not have a well-defined "undo" state, unlike the object-based claim ID whose survival enables retry. The comment advises treating this transaction solely as an account creation primitive, never as a general transfer path even when the destination account already exists. + +## Relationship to Supporting Types + +The file includes `XChainAttestations.h`, which provides `AttestationClaim` and `AttestationCreateAccount` — the cryptographic structures that witness servers produce and that `XChainAddClaimAttestation` and `XChainAddAccountCreateAttestation` validate and accumulate. The attestation base type carries a public key, a cryptographic signature, a `wasLockingChainSend` direction flag, and reward-distribution metadata. The `XChainAttestationsBase` template enforces a hard cap of 256 attestations per claim to bound memory allocation. + +The `using XChainModifyBridge = BridgeModify` and `using XChainAccountCreateCommit = XChainCreateAccountCommit` aliases exist for naming consistency with the broader XRPL transaction type registry, which uses `XChain`-prefixed names throughout. The underlying classes use shorter names; the aliases bridge the gap without code duplication. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/check/CheckCancel.h.ai.json b/include/xrpl/tx/transactors/check/CheckCancel.h.ai.json new file mode 100644 index 0000000000..6b7b09bc74 --- /dev/null +++ b/include/xrpl/tx/transactors/check/CheckCancel.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "CheckCancel" + } + ], + "description": "Defines the CheckCancel transaction transactor for the XRPL, including its interface and core methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/check/CheckCancel.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "CheckCancel" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 21, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/check/CheckCancel.h.ai.md b/include/xrpl/tx/transactors/check/CheckCancel.h.ai.md new file mode 100644 index 0000000000..e2e0d56b9f --- /dev/null +++ b/include/xrpl/tx/transactors/check/CheckCancel.h.ai.md @@ -0,0 +1,28 @@ +# `CheckCancel.h` — CheckCancel Transaction Transactor + +`CheckCancel` is the transactor responsible for removing a Check ledger object from the XRP Ledger. It participates in the standard three-phase transaction pipeline inherited from `Transactor`: `preflight` for stateless validation, `preclaim` for ledger-state validation, and `doApply` for committed state mutation. + +## Role in the Check Subsystem + +The `check/` directory contains exactly three transactors — `CheckCreate`, `CheckCash`, and `CheckCancel` — mirroring the full lifecycle of a Check object. `CheckCreate` writes the on-ledger object and reserves owner funds; `CheckCash` redeems it; `CheckCancel` tears it down without transferring value. `CheckCancel` is the only path for reclaiming the owner reserve when a check goes unused, whether because it expired or the parties agreed not to proceed. + +## Class Design + +`CheckCancel` inherits `Transactor` and adds no new data members; its constructor simply forwards `ApplyContext&` to the base. The `ConsequencesFactory` tag is set to `Normal`, meaning the transaction system treats a `CheckCancel` as a routine fee-paying transaction with no special blocking semantics. + +Unlike `CheckCreate` and `CheckCash`, `CheckCancel` does **not** override `checkExtraFeatures`. This is intentional: cancellation is always permitted regardless of which amendments are active — it is a cleanup operation, not a capability gate. + +## Validation Phases + +`preflight` is a stub that returns `tesSUCCESS` immediately. All meaningful validation is deferred to `preclaim`, which has access to the read-only ledger state. `preclaim` resolves the Check by `sfCheckID`, fails with `tecNO_ENTRY` if it does not exist, then enforces the permission model: if the check has **not** yet expired (tested against the parent ledger's close time, the only definitively known timestamp), only the **source account** or the **destination account** may cancel it. An expired check may be removed by anyone. This asymmetry keeps expired objects purgeable without requiring the original parties. + +## State Mutation in `doApply` + +`doApply` performs three coordinated writes against the mutable `ApplyView`: + +1. **Destination owner-directory removal** — the Check's `sfDestinationNode` field stores the page index in the destination's owner directory; `dirRemove` uses this for O(1) removal without a linear scan. This step is skipped if source and destination are the same account (a degenerate but theoretically valid case). +2. **Source owner-directory removal** — symmetrically uses `sfOwnerNode` to remove the entry from the creator's owner directory. +3. **Owner reserve adjustment** — calls `adjustOwnerCount` with `-1` to release the reserve increment that `CheckCreate` claimed. +4. **SLE erasure** — the Check ledger object itself is erased. + +The two `dirRemove` calls are each guarded by a `LCOV_EXCL` block that returns `tefBAD_LEDGER` on failure. These branches are unreachable in a correctly functioning ledger — the stored page indices are immutable after creation and `preclaim` already confirmed the Check exists — so they serve as defensive invariant checks rather than expected error paths. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/check/CheckCash.h.ai.json b/include/xrpl/tx/transactors/check/CheckCash.h.ai.json new file mode 100644 index 0000000000..b993cc539c --- /dev/null +++ b/include/xrpl/tx/transactors/check/CheckCash.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "CheckCash" + } + ], + "description": "Defines the CheckCash transaction transactor class for the XRPL, including its interface for preflight, preclaim, and application logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/check/CheckCash.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "CheckCash" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/check/CheckCash.h.ai.md b/include/xrpl/tx/transactors/check/CheckCash.h.ai.md new file mode 100644 index 0000000000..e04ed3796d --- /dev/null +++ b/include/xrpl/tx/transactors/check/CheckCash.h.ai.md @@ -0,0 +1,85 @@ +# `CheckCash.h` — Transactor Declaration for the CheckCash Transaction + +## Purpose and Context + +`CheckCash.h` declares the `CheckCash` class, which is the transactor responsible for executing `ttCHECK_CASH` (transaction type 17) on the XRP Ledger. It lives alongside `CheckCreate.h` and `CheckCancel.h` as one of three transactors that together implement the XRPL Checks feature — an asynchronous, pull-based payment mechanism where a sender authorizes a recipient to withdraw up to a specified amount at a future time of the recipient's choosing. + +The header itself is minimal by design: all three check transactors follow the same structural pattern, declaring only the pipeline hooks required by the `Transactor` framework. The real complexity lives in the corresponding `.cpp` file. + +## Class Structure + +```cpp +class CheckCash : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit CheckCash(ApplyContext& ctx) : Transactor(ctx) {} + + static bool checkExtraFeatures(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); + static TER preclaim(PreclaimContext const& ctx); + TER doApply() override; +}; +``` + +`CheckCash` inherits from `Transactor` and participates in the framework's compile-time polymorphism pipeline. The four methods correspond to four distinct execution phases; they are invoked through `invokePreflight` and `invoke_preclaim` template machinery using name hiding rather than virtual dispatch, which allows the framework to combine standard checks (signature verification, fee validation, sequence numbers) with transaction-specific logic without virtual call overhead at each step. + +## `ConsequencesFactory{Normal}` + +The `Normal` factory type means that a `CheckCash` transaction claims its fee on failure just as it would on success. This is the default posture for most transactors. The alternative, `Blocker`, would mark the transaction as preventing other transactions from the same account from processing — not appropriate for check cashing, which should fail cleanly without affecting the sender's queue. + +## `checkExtraFeatures` — Amendment-Gated Feature Detection + +`CheckCash` is one of the few transactors that overrides `checkExtraFeatures` (the base class default simply returns `true`). The override checks whether the transaction attempts to cash a check denominated in an MPT (Multi-Purpose Token) asset, and if so, requires the `featureMPTokensV2` amendment to be enabled: + +```cpp +return ctx.rules.enabled(featureMPTokensV2) || + (!(optAmount && optAmount->holds()) && + !(optDeliverMin && optDeliverMin->holds())); +``` + +The `Transactor` framework explicitly separates amendment checks from the `preflight` body: the comment in `Transactor.h` states "Do not check whether relevant amendments are enabled in preflight. Instead, define `checkExtraFeatures`." This separation keeps `preflight` focused on structural transaction validity and localizes all amendment gating to a single discoverable hook. + +Note that `CheckCancel` does not override `checkExtraFeatures`, which indicates cancellation of MPT checks does not require an additional amendment gate — only cashing them does. + +## `preflight` — Stateless Structural Validation + +Preflight runs before the transaction is applied to any ledger view and serves as the first line of defense. For `CheckCash`, it enforces two rules: + +1. **Mutual exclusivity of `sfAmount` and `sfDeliverMin`**: Exactly one must be present. `sfAmount` means "deliver exactly this amount"; `sfDeliverMin` means "deliver at least this much, up to the check's `sfSendMax`." Both present or neither present is `temMALFORMED`. + +2. **Amount validity**: The chosen amount must pass `isLegalNet()` and be strictly positive. The asset must not be a `badAsset()`. + +These checks are intentionally limited to data the transaction itself carries — no ledger access occurs here. + +## `preclaim` — Read-Only State Validation + +Preclaim receives a `ReadView` and runs after preflight succeeds. It performs a richer set of validations without modifying ledger state: + +- Verifies the check ledger entry (`keylet::check(sfCheckID)`) exists — `tecNO_ENTRY` otherwise. +- Confirms that the submitting account is the check's destination — `tecNO_PERMISSION` otherwise. +- Detects the degenerate self-send case (source equals destination) that should have been caught during `CheckCreate` — returns `tecINTERNAL` under `LCOV_EXCL` guards, signaling this is a "should never happen" path. +- Enforces the destination's `lsfRequireDestTag` flag if the check lacks a `sfDestinationTag`. +- Rejects expired checks via `hasExpired()`. +- Cross-checks the requested amount's asset and issuer against the check's `sfSendMax`, and ensures the requested amount does not exceed `sfSendMax`. +- Confirms the source has sufficient available funds. Notably, for XRP checks, it adds one reserve increment to the available balance — a forward-looking adjustment because cashing the check will delete the check ledger entry, releasing that reserve back to the source before the transfer is calculated. +- For IOU assets not self-issued by the destination, validates that the issuer exists, that trust line authorization is satisfied (`lsfRequireAuth` / `lsfLowAuth` / `lsfHighAuth`), and that the destination's trust line to the issuer is not frozen. +- For MPT assets, checks `requireAuth` with weak authorization and `isFrozen()` on the destination's MPT holding, plus `canTrade()` to confirm the asset's DEX trading is permitted. + +## `doApply` — Ledger Mutation + +`doApply` is the only virtual method and the only one that actually modifies state. It operates on a `PaymentSandbox` — a copy-on-write wrapper around the current `ApplyView` — and calls `psb.apply(ctx_.rawView())` only at the very end after all mutations succeed. This ensures atomicity: if anything fails mid-execution, the sandbox is simply discarded. + +**XRP path**: `flow()` does not handle XRP-to-XRP transfers, so `CheckCash` handles them directly. It calls `xrpLiquid(psb, srcId, -1, viewJ)` with the `-1` argument to account for the reserve that will be freed once the check is deleted, then computes the delivery amount. For `sfDeliverMin`, the delivery is `max(DeliverMin, min(sendMax, srcLiquid))` — the maximum the source can actually send, bounded by both the check cap and their available liquidity. `ctx_.deliver()` is called to set the `DeliveredAmount` metadata field. + +**IOU/MPT path**: The `flow()` payment engine handles the heavy lifting. A critical design decision here is that `CheckCash` will automatically create a trust line between the destination and the issuer if one doesn't exist yet, provided the destination has sufficient reserve. This is justified by the fact that the destination is actively signing the transaction — they clearly want the funds. The trust line's limit is then **temporarily set to `cMaxValue`** (the maximum possible `STAmount`) during the `flow()` call, regardless of whatever limit the destination may have previously set. A `scope_exit` guard restores the original limit once `flow()` returns. This design choice avoids the scenario where the destination's own trust line limit would block them from receiving funds they explicitly requested. + +For `sfDeliverMin` with IOUs, the "ask" amount passed to `flow()` is `cMaxValue / 2`. This upper bound exists to tolerate gateway transfer rates: since transfer rates cannot exceed 200%, dividing by two ensures that even with the maximum markup the actual delivery cannot overflow the `STAmount` representation. + +For MPT assets without an existing MPT holding on the destination, `checkCreateMPT()` is called to initialize the holding slot, mirroring the trust line creation logic. + +After a successful transfer, the check is removed from both the owner and destination account directories via `dirRemove`, the source's owner count is decremented with `adjustOwnerCount`, and the check SLE itself is erased. + +## Relationship to Sibling Transactors + +All three check transactors (`CheckCreate`, `CheckCash`, `CheckCancel`) share the same structural skeleton and all declare `ConsequencesFactory{Normal}`. `CheckCreate` and `CheckCash` both override `checkExtraFeatures` to gate MPT support on `featureMPTokensV2`; `CheckCancel` does not, treating cancellation as amendment-agnostic. `CheckCash` is the only one that invokes `flow()` and manages the `PaymentSandbox` pattern — consistent with it being the only transactor in the group that performs a value transfer rather than pure ledger object lifecycle management. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/check/CheckCreate.h.ai.json b/include/xrpl/tx/transactors/check/CheckCreate.h.ai.json new file mode 100644 index 0000000000..7e224967e1 --- /dev/null +++ b/include/xrpl/tx/transactors/check/CheckCreate.h.ai.json @@ -0,0 +1,61 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 8, + "name": "CheckCreate" + } + ], + "description": "Defines the CheckCreate transaction transactor class for the XRPL, including its interface for preflight, preclaim, and apply logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/check/CheckCreate.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "CheckCreate" + }, + { + "args": [ + "xrpl::PreflightContext const& ctx" + ], + "lineno": 16, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 19, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 22, + "name": "preclaim" + }, + { + "args": [], + "lineno": 25, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/check/CheckCreate.h.ai.md b/include/xrpl/tx/transactors/check/CheckCreate.h.ai.md new file mode 100644 index 0000000000..3b14f3ea53 --- /dev/null +++ b/include/xrpl/tx/transactors/check/CheckCreate.h.ai.md @@ -0,0 +1,37 @@ +# `CheckCreate.h` — Check Creation Transactor Interface + +## Role in the System + +`CheckCreate.h` declares the transactor responsible for handling `CheckCreate` transactions on the XRP Ledger. A Check is a deferred payment authorization: the sender (drawer) commits a `SendMax` amount to a named destination, which the destination may later cash via `CheckCash`, or which either party may cancel via `CheckCancel`. This file defines the interface of the `CheckCreate` class, while the full validation and ledger-mutation logic lives in `CheckCreate.cpp`. + +It is one of three sibling transactors in the `check/` subdirectory alongside `CheckCash` and `CheckCancel`, all of which share an identical structural interface. + +## The Transactor Framework + +`CheckCreate` inherits from `Transactor`, the common base for all XRPL transaction processors. The framework enforces a strict three-phase execution model: **preflight** (stateless validation, before any ledger read), **preclaim** (ledger-state validation, read-only), and **doApply** (ledger mutation). Each phase maps directly to a method in this class. + +A critical design detail: `preflight` and `preclaim` are **static** methods, not virtual. The base class `Transactor` invokes them through the `invokePreflight` template, which uses name hiding for compile-time polymorphism. The comment in `Transactor.h` is explicit — derived classes must not define `invokePreflight` themselves, and must not call `preflight1` or `preflight2` directly. This pattern trades the safety of virtual dispatch for zero-overhead static dispatch, which matters given how frequently these validation paths run. + +Only `doApply()` is virtual, reflecting the fact that ledger mutation requires runtime dispatch while validation does not. + +## `ConsequencesFactory` + +The class exposes `static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}`. This constant tells the framework how to classify the transaction's consequences — specifically, whether it blocks or merely consumes a sequence number. `Normal` means `CheckCreate` does not block subsequent transactions from the same account, unlike some transactors (e.g., escrow-related ones) that use `Blocker`. This affects how the transaction queue reasons about batching and ordering. + +## `checkExtraFeatures` + +The static `checkExtraFeatures(PreflightContext const&)` hook is called from `invokePreflight` before `preflight1` runs. The base `Transactor` implementation always returns `true`; `CheckCreate` overrides it to return `temDISABLED` when the transaction uses an `MPTIssue` (Multi-Purpose Token) as `SendMax` but the `featureMPTokensV2` amendment is not yet enabled. This keeps the feature-gating concern separate from the core field-validation logic in `preflight`, which is the intended pattern described in `Transactor.h`. + +## Validation Phases + +`preflight` performs pure field-level checks that require no ledger access: it rejects self-addressed checks (`temREDUNDANT`), validates that `SendMax` is a positive, legally-formed amount with a non-bad currency (`temBAD_AMOUNT`, `temBAD_CURRENCY`), and ensures any `Expiration` field is non-zero (`temBAD_EXPIRATION`). + +`preclaim` handles ledger-dependent validation. It verifies the destination account exists (`tecNO_DST`), checks that the destination has not set `lsfDisallowIncomingCheck`, and enforces that pseudo-accounts cannot receive checks. It also enforces `lsfRequireDestTag` on the destination. For non-XRP amounts, it checks global and per-trustline freeze state, distinguishing between `tecFROZEN` (IOU) and `tecLOCKED` (MPT). Notably, it permits creating a check for a currency even if the sender has no existing trustline — the check is speculative and the trustline need not exist at creation time. Finally, it rejects checks with an expiration already in the past (`tecEXPIRED`), and validates that the asset is tradeable via `canTrade`. + +## `doApply` Ledger Mutation + +`doApply()` constructs the `Check` SLE (Serialized Ledger Entry), keyed by `keylet::check(account_, seq)` where `seq` is the transaction's sequence or ticket value. It populates all optional fields (`SourceTag`, `DestinationTag`, `InvoiceID`, `Expiration`) only if present in the transaction, leaving them absent otherwise. The Check is inserted into **both** the sender's owner directory and the destination's owner directory, with `sfOwnerNode` and `sfDestinationNode` recording each directory page. Reserve sufficiency is checked against `preFeeBalance_` (the pre-fee balance set by the base class) rather than the post-fee balance, intentionally allowing fee payment to dip into the reserve while still requiring reserve coverage for the new ledger object. On success, `adjustOwnerCount` increments the sender's owner count, increasing their reserve requirement by one increment. + +## Relationship to Sibling Transactors + +`CheckCash` and `CheckCancel` have structurally identical declarations. Together they form a complete lifecycle: `CheckCreate` establishes the Check object on the ledger, `CheckCash` redeems it (consuming the object), and `CheckCancel` removes it without payment. All three use `ConsequencesFactory{Normal}` and override only `checkExtraFeatures`, `preflight`, `preclaim`, and `doApply` — the minimal surface dictated by the framework. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/credentials/CredentialAccept.h.ai.json b/include/xrpl/tx/transactors/credentials/CredentialAccept.h.ai.json new file mode 100644 index 0000000000..e0355d1335 --- /dev/null +++ b/include/xrpl/tx/transactors/credentials/CredentialAccept.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "CredentialAccept" + } + ], + "description": "Defines the CredentialAccept transaction class for the XRPL, inheriting from Transactor, with methods for transaction processing stages.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/credentials/CredentialAccept.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "CredentialAccept" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/credentials/CredentialAccept.h.ai.md b/include/xrpl/tx/transactors/credentials/CredentialAccept.h.ai.md new file mode 100644 index 0000000000..b73058a1c4 --- /dev/null +++ b/include/xrpl/tx/transactors/credentials/CredentialAccept.h.ai.md @@ -0,0 +1,39 @@ +# `CredentialAccept.h` — Subject-Side Credential Acceptance Transactor + +## Role and Context + +`CredentialAccept` is one of three credential transactors in the XRPL credential subsystem, alongside `CredentialCreate` and `CredentialDelete`. It implements the second step of the credential issuance lifecycle: after an issuer creates a credential directed at a subject account (`CredentialCreate`), the subject must explicitly accept it before the credential becomes active and usable in permission-gated operations. + +The header declares the class interface only; all logic lives in the corresponding `.cpp`. The three-stage transactor pipeline (`preflight` → `preclaim` → `doApply`) is mandated by the base `Transactor` class through compile-time polymorphism — the framework calls these stages via `invokePreflight` and the `preclaim`/`doApply` dispatch chain rather than virtual dispatch, which is why the static methods use name-hiding rather than `override`. + +## Class Design + +`CredentialAccept` inherits `Transactor` directly with no additional member data. The `ConsequencesFactory{Normal}` constant signals to the fee/consequence framework that this transaction follows standard consequence semantics: it consumes a sequence number and has no special blocking or custom consequence logic. + +The constructor simply forwards `ApplyContext` to the base, consistent with all three credential transactors. The uniformity here is intentional — the three credential types have identical structural signatures, differing only in their validation logic and ledger mutations. + +## Processing Stages + +**`getFlagsMask`** participates in flag validation during preflight. It returns `tfUniversalMask` when the `fixInvalidTxFlags` amendment is active, restricting the transaction to only universal flags and rejecting any unknown flags as malformed. When the amendment is absent (pre-fix ledgers), it returns `0`, which the framework interprets as "allow any flags" — preserving backward compatibility with transactions submitted before the flag-stricter rules were enabled. + +**`preflight`** performs structural validation against the transaction fields themselves, without touching ledger state. It enforces two invariants: the `sfIssuer` field must not be the zero account ID (which would indicate a malformed or defaulted field), and `sfCredentialType` must be non-empty and within the maximum credential type length. Both checks return `temINVALID_ACCOUNT_ID` or `temMALFORMED` respectively — `tem` errors signal permanent rejection that no future ledger state can cure, so the transaction will not be retried. + +**`preclaim`** performs stateful read-only checks against the current ledger view. It verifies that the issuer account actually exists (returning `tecNO_ISSUER` otherwise), that the credential object keyed by `(subject, issuer, credentialType)` exists in the ledger (`tecNO_ENTRY`), and that the credential has not already had its `lsfAccepted` flag set (`tecDUPLICATE`). These `tec` results still consume a fee but do not alter ledger state, appropriate for situations where on-chain state has changed between submission and processing. + +**`doApply`** executes the actual state transition under an `ApplyView` that buffers writes until success. Several design choices stand out: + +- **Reserve check at apply time**: Rather than checking the reserve in `preclaim`, the implementation defers it to `doApply` and compares `preFeeBalance_` (balance before fees, captured by the base class) against the post-increment owner reserve. This ensures the fee has already been deducted before the reserve calculation, reflecting actual affordability. + +- **Owner count ownership transfer**: When a credential is created by an issuer but not yet accepted, it is counted against the issuer's owner count (they "own" the pending credential object). On acceptance, `doApply` calls `adjustOwnerCount` twice: decrementing the issuer's count by 1 and incrementing the subject's count by 1. The reserve burden thus shifts atomically from issuer to subject at the moment of acceptance, which incentivizes subjects to promptly manage credentials they hold. + +- **Expiry handled at apply time**: If the credential has an expiration and that expiration has passed by `parentCloseTime`, `doApply` deletes the credential via `credentials::deleteSLE` and returns `tecEXPIRED`. This is an important cleanup mechanism: expired credentials are removed from ledger state even when the accepting transaction itself fails. The code carefully checks whether the deletion succeeded before returning `tecEXPIRED` versus propagating the deletion error. + +- **Accepted state flip**: On the happy path, the method simply sets `sfFlags` to `lsfAccepted` on the credential SLE and calls `view().update()`. There is no new object creation — the credential already exists from the `CredentialCreate` step; acceptance is a mutation of that existing object. + +## Error Handling and Defensive Patterns + +The `tefINTERNAL` guard at the top of `doApply` (marked `LCOV_EXCL_LINE`) protects against the theoretical case where `sleSubject` or `sleIssuer` cannot be loaded even though their existence was confirmed in `preclaim`. This path is unreachable under correct ledger invariants — the credential's existence implies both accounts exist — but the guard keeps the code safe against hypothetical corruption and documents the assumption explicitly. + +## Relationship to Siblings + +All three credential transactors (`CredentialCreate`, `CredentialCreate`, `CredentialDelete`) share an identical header shape: same constructor pattern, same static method signatures, `ConsequencesFactory{Normal}`. This structural uniformity is not coincidental — it means the transactor dispatch machinery can treat them identically at the framework level, with all semantic differences confined to the `.cpp` implementations. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/credentials/CredentialCreate.h.ai.json b/include/xrpl/tx/transactors/credentials/CredentialCreate.h.ai.json new file mode 100644 index 0000000000..d615b768a8 --- /dev/null +++ b/include/xrpl/tx/transactors/credentials/CredentialCreate.h.ai.json @@ -0,0 +1,61 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "CredentialCreate" + } + ], + "description": "Defines the CredentialCreate transactor class for handling credential creation transactions in the XRPL system, including preflight, preclaim, and apply logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/credentials/CredentialCreate.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "CredentialCreate" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 23, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/credentials/CredentialCreate.h.ai.md b/include/xrpl/tx/transactors/credentials/CredentialCreate.h.ai.md new file mode 100644 index 0000000000..33760a8147 --- /dev/null +++ b/include/xrpl/tx/transactors/credentials/CredentialCreate.h.ai.md @@ -0,0 +1,43 @@ +# `CredentialCreate.h` — CredentialCreate Transactor + +## Role in the System + +`CredentialCreate` is the transactor responsible for processing `CredentialCreate` transactions on the XRP Ledger, which implements W3C Verifiable Credentials (VC). The feature allows a trusted issuer to attest facts about a subject account — their identity, compliance status, or any other verifiable attribute — in a way that can be inspected by third parties without contacting the issuer again. This header declares the class interface; the implementation lives in the corresponding `.cpp`. + +Like every transaction type in rippled, `CredentialCreate` inherits from `Transactor` and participates in the three-phase pipeline: **preflight** (stateless validation), **preclaim** (read-only ledger checks), and **doApply** (mutating ledger state). + +## Class Design + +`CredentialCreate` follows the standard transactor pattern exactly. `ConsequencesFactory` is set to `Normal`, meaning the transaction claims a fee under ordinary fee-burning rules rather than acting as a queue blocker or using custom consequence computation. + +The constructor simply forwards `ApplyContext` to the base class — no additional state is needed because all inputs come from the transaction fields stored in `ctx_`. + +The four static/virtual members form the complete contract: + +- `getFlagsMask` — returns the permitted bitfield of transaction flags. When the `fixInvalidTxFlags` amendment is active it returns `tfUniversalMask`, restricting callers to only the universal flags. When the amendment is absent it returns `0`, which the framework interprets as "allow any flags" — a backward-compatibility escape hatch for transactions submitted before the fix was deployed. + +- `preflight(PreflightContext const&)` — runs before any ledger state is consulted, so it is cheap and can be cached across retries. It rejects transactions where `sfSubject` is absent, where the optional `sfURI` is present but empty or exceeds `maxCredentialURILength`, and where `sfCredentialType` is empty or exceeds `maxCredentialTypeLength`. All three return `temMALFORMED`, which prevents a fee from being claimed. + +- `preclaim(PreclaimContext const&)` — executes against a read-only view of the current ledger. It confirms that the target subject account actually exists (returning `tecNO_TARGET` otherwise) and that the (`subject`, `issuer`, `credentialType`) triple does not already identify an existing credential (`tecDUPLICATE`). These checks are separated from `preflight` because they require ledger lookups that must not be cached across ledger closes. + +- `doApply()` — the only virtual override, where the actual ledger mutation happens. + +## What `doApply` Does + +The implementation constructs a new `SLE` (serialized ledger entry) keyed by `keylet::credential(subject, account_, credType)`. Several invariants are enforced before the object is inserted: + +**Expiration validation** is deferred to `doApply` rather than `preflight` because whether a timestamp is in the past depends on the ledger's `parentCloseTime`, which is not available during preflight. If the optional `sfExpiration` field is present and already behind the ledger's parent close time, the transaction fails with `tecEXPIRED`. Notably, expiration uses the *parent* close time, not the current ledger's own close time — this is consistent with XRPL's convention for time-sensitive operations. + +**Reserve check** happens after the expiration guard. The issuer's current owner count is read from their `AccountRoot`, one is added prospectively, and the resulting reserve is compared against `preFeeBalance_` (the issuer's balance before fees were deducted). Failing this test returns `tecINSUFFICIENT_RESERVE`. + +**Directory insertion** follows the two-party structure of the VC model. The credential is always inserted into the *issuer's* owner directory, and the issuer's owner count is incremented by one — the issuer bears the reserve cost for the credential until the subject accepts it. If the subject and issuer are the same account (self-issuance), the credential is immediately marked `lsfAccepted` and inserted only into the single shared directory. For a third-party credential the object is also inserted into the *subject's* owner directory (without incrementing the subject's owner count), providing subject-side discoverability and enabling `CredentialAccept` to later transfer economic ownership. The `sfIssuerNode` and `sfSubjectNode` fields record the directory page indices needed for efficient deletion. + +## Relationship to Sibling Transactors + +`CredentialAccept.h` and `CredentialDelete.h` are structurally identical headers, each following the same `getFlagsMask`/`preflight`/`preclaim`/`doApply` pattern. Together the three transactors form the full lifecycle of a credential: `CredentialCreate` issues it, `CredentialAccept` lets the subject accept it (transferring the reserve obligation), and `CredentialDelete` removes it from both directories. The design makes the state transitions explicit on-ledger and independently auditable, which is essential for a compliance-oriented primitive. + +## Non-Obvious Design Decisions + +The choice to split validation across `preflight` and `preclaim` rather than consolidating it in `doApply` is deliberate: preflight results can be memoized by the transaction queue and re-used across ledger closes, whereas preclaim is re-executed each time the ledger state changes. Keeping expensive ledger lookups out of preflight improves throughput under load. + +Self-issuance being auto-accepted removes the need for a holder to issue a follow-up `CredentialAccept` transaction just to use credentials they issued to themselves — a practical simplification for single-entity deployments that issue and consume credentials on the same account. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/credentials/CredentialDelete.h.ai.json b/include/xrpl/tx/transactors/credentials/CredentialDelete.h.ai.json new file mode 100644 index 0000000000..fb86fa519b --- /dev/null +++ b/include/xrpl/tx/transactors/credentials/CredentialDelete.h.ai.json @@ -0,0 +1,54 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "CredentialDelete" + } + ], + "description": "Defines the CredentialDelete transaction transactor class for handling credential deletion operations in the XRPL system.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/credentials/CredentialDelete.h", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 14, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 20, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/credentials/CredentialDelete.h.ai.md b/include/xrpl/tx/transactors/credentials/CredentialDelete.h.ai.md new file mode 100644 index 0000000000..085671b65d --- /dev/null +++ b/include/xrpl/tx/transactors/credentials/CredentialDelete.h.ai.md @@ -0,0 +1,35 @@ +# `CredentialDelete.h` — Transaction Transactor for Credential Deletion + +## Role in the System + +`CredentialDelete.h` declares the `CredentialDelete` transactor, one of three credential-lifecycle transaction handlers in XRPL (alongside `CredentialCreate` and `CredentialAccept`). Its purpose is to remove a `Credential` ledger object — the on-chain record that an issuer has asserted a verifiable claim about a subject — from the ledger state. The file is intentionally minimal: a single class declaration that defers all logic to its `.cpp` counterpart. + +## Class Design and Inheritance + +`CredentialDelete` inherits from `Transactor`, the base class for all XRPL transaction processors. The `Transactor` framework mandates a three-phase execution model: + +1. **`preflight`** — stateless, runs before ledger access, rejects clearly malformed transactions early. +2. **`preclaim`** — read-only ledger access, checks whether the transaction is likely to succeed given current state. +3. **`doApply`** — mutates ledger state; only reached after both prior phases pass. + +The `static` qualifier on `preflight`, `preclaim`, and `getFlagsMask` is a deliberate design in the `Transactor` framework. These are called through `invokePreflight()` via C++ name-hiding rather than virtual dispatch, enabling compile-time polymorphism without vtable overhead. The base `Transactor` provides default no-op implementations; derived classes override by shadowing the names. This is documented explicitly in `Transactor.h` with the warning *"these are not really virtual and so don't have the compiler-time protection that comes with it."* + +The `ConsequencesFactory{Normal}` constant signals to the transaction queuing system that this transaction carries normal fee semantics — it does not block subsequent transactions from the same account in the queue (unlike a `Blocker`-typed transactor). + +## Validation Logic (from the Implementation) + +`getFlagsMask()` participates in the `fixInvalidTxFlags` amendment rollout. Before that amendment activated, it returned `0`, which the framework interprets as "accept any flags." After activation it returns `tfUniversalMask`, enforcing the standard constraint that no unknown flag bits may be set. This pattern appears across most credential transactors as a forward-compatible flag gating mechanism. + +`preflight()` enforces two structural invariants. First, at least one of `sfSubject` or `sfIssuer` must be present — their absence together means the transaction cannot identify which credential to delete, so it is rejected as `temMALFORMED`. Second, if either field is present its value must not be the zero `AccountID`, which would indicate a corrupt or intentionally poisoned transaction. The `sfCredentialType` blob is also checked: it must be non-empty and within `maxCredentialTypeLength` bytes. + +`preclaim()` performs the ledger existence check. When `sfSubject` or `sfIssuer` is absent, the transaction sender (`sfAccount`) is substituted as the default. This is the design for self-deletion: a subject can omit `sfIssuer` if the subject is themselves the sender, and similarly an issuer can omit `sfSubject`. The credential's keylet is derived from the triple `(subject, issuer, credentialType)` via `keylet::credential()`; if the object is absent the call returns `tecNO_ENTRY`. + +## Authorization Enforced in `doApply()` + +The most significant business rule lives in `doApply()`. A third party — an account that is neither the subject nor the issuer of the credential — is only permitted to delete the credential if it is expired, as determined by `checkExpired()` comparing `sfExpiration` against the ledger's `parentCloseTime`. The subject or issuer can always delete the credential unconditionally. This asymmetry is intentional: it allows orphaned expired credentials to be pruned by anyone (reclaiming reserve), while preventing unauthorized deletion of valid credentials. + +If the authorization check fails, `doApply()` returns `tecNO_PERMISSION`. On success, it delegates the actual removal to `credentials::deleteSLE()` from `CredentialHelpers.h`, which handles unlinking the object from the ledger's owner directory and decrementing the owner's reserve count. + +## Relationship to Sibling Files + +`CredentialCreate.h` and `CredentialAccept.h` have structurally identical declarations — all three credential transactors expose the same four-method interface (`getFlagsMask`, `preflight`, `preclaim`, `doApply`). This uniformity is enforced by the `Transactor` contract rather than a shared credential base class, keeping the credential group cohesive without adding an extra layer of inheritance. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/delegate/DelegateSet.h.ai.json b/include/xrpl/tx/transactors/delegate/DelegateSet.h.ai.json new file mode 100644 index 0000000000..9c4b872815 --- /dev/null +++ b/include/xrpl/tx/transactors/delegate/DelegateSet.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 16, + "name": "ctx" + }, + { + "lineno": 19, + "name": "ctx" + }, + { + "lineno": 26, + "name": "view" + }, + { + "lineno": 26, + "name": "sle" + }, + { + "lineno": 26, + "name": "account" + }, + { + "lineno": 26, + "name": "j" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "DelegateSet" + } + ], + "description": "Defines the DelegateSet transaction class for the XRPL, including its interface and lifecycle methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/delegate/DelegateSet.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "DelegateSet" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 19, + "name": "preclaim" + }, + { + "args": [], + "lineno": 22, + "name": "doApply" + }, + { + "args": [ + "ApplyView& view", + "std::shared_ptr const& sle", + "AccountID const& account", + "beast::Journal j" + ], + "lineno": 26, + "name": "deleteDelegate" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/delegate/DelegateSet.h.ai.md b/include/xrpl/tx/transactors/delegate/DelegateSet.h.ai.md new file mode 100644 index 0000000000..8a77f14ced --- /dev/null +++ b/include/xrpl/tx/transactors/delegate/DelegateSet.h.ai.md @@ -0,0 +1,35 @@ +# `DelegateSet.h` — Transaction Transactor for Account Permission Delegation + +`DelegateSet.h` declares the `DelegateSet` transactor, which implements the XRPL transaction type that allows an account owner to grant or revoke a set of named permissions to a delegate account. It sits in the `xrpl::tx::transactors::delegate` module alongside `DelegateUtils`, and follows the same three-phase pipeline contract (`preflight` → `preclaim` → `doApply`) mandated by the `Transactor` base class. + +## Purpose and Role + +The delegate feature enables one account to authorize another to submit specific transaction types on its behalf — a form of scoped delegation without relinquishing ownership. `DelegateSet` manages the lifecycle of the on-ledger `Delegate` SLE (Serialized Ledger Entry) that records this relationship: creating it, updating its permission array, or deleting it when the caller submits an empty permissions array. A single transaction type therefore serves three operations, distinguished entirely by whether the `sfPermissions` array is non-empty and whether an existing delegate object exists. + +## Transactor Lifecycle + +`DelegateSet` follows the standard static-dispatch preflight pattern. `ConsequencesFactory{Normal}` is declared at class scope, indicating that this transaction type claims a fee normally (not a blocker that should suppress other transactions). + +**`preflight`** runs without any ledger state and enforces structural validity. It rejects transactions with more permissions than `permissionMaxSize`, prevents self-authorization (`sfAccount == sfAuthorize`), detects duplicate permission values in the array using an `unordered_set`, and validates each permission value against `Permission::getInstance().isDelegable()` to ensure only delegable permission types are included. Because it has no ledger access, this phase is safe to run during initial transaction reception before any consensus context is established. + +**`preclaim`** runs against a read-only ledger view and checks for existence conditions that can only be verified with ledger state: both the grantor account and the `sfAuthorize` target account must exist. A specific guard rejects a delete-intent transaction (empty permissions array) when no delegate object exists yet — preventing a fee-charging no-op. + +**`doApply`** is the only virtual method and the sole ledger-mutating phase. It branches across three distinct paths based on what's present in the ledger: + +- **Update path**: If a `Delegate` SLE already exists and the permissions array is non-empty, the existing `sfPermissions` field is replaced in place. No reserve change occurs because the owner-count entry already exists. +- **Delete path**: If a `Delegate` SLE already exists and the permissions array is empty, execution delegates to the static `deleteDelegate()` method. +- **Create path**: If no SLE exists, the account's pre-fee XRP balance is checked against the reserve required for one additional owned object. If sufficient, a new `Delegate` SLE is constructed with both account IDs and the permission array, inserted into the owner directory, and `adjustOwnerCount` is called to increment the owner's object count. The `sfOwnerNode` field on the new SLE records the owner directory page index, which is later used for O(1) directory removal without scanning. + +## The `deleteDelegate` Static Interface + +The most architecturally notable element is `deleteDelegate()`. The comment `// Interface used by AccountDelete` signals an explicit cross-transactor contract: `AccountDelete.cpp` calls `DelegateSet::deleteDelegate` directly to remove delegate objects owned by an account being deleted. Examining the implementation confirms this — `AccountDelete.cpp` line 169 routes its delegate cleanup through this exact function. + +Making deletion a public static method rather than keeping it private to `doApply` reflects a deliberate design choice: the deletion logic requires an `ApplyView`, an SLE pointer, an account ID, and a journal, all of which are available to any transactor operating in the apply phase. By exposing it statically, `AccountDelete` can reuse the cleanup code without needing a fake `DelegateSet` transaction object. This avoids code duplication while keeping deletion logic co-located with the type that owns the `Delegate` SLE's structure. + +Internally, `deleteDelegate()` performs three steps: remove the SLE's entry from the owner directory using the stored `sfOwnerNode`, decrement the owner count via `adjustOwnerCount`, and erase the SLE from the ledger. A failure in `dirRemove` logs at `fatal` severity and returns `tefBAD_LEDGER`, indicating ledger state corruption rather than a recoverable user error — this branch is marked `LCOV_EXCL` because reaching it would represent an impossible-in-practice ledger inconsistency. + +## Invariants and Defensive Guards + +The `tecINTERNAL` return at the "no existing SLE, empty permissions" branch of `doApply` is a defensive invariant. The `preclaim` phase already blocks this case with `tecNO_ENTRY`, so `doApply` should never reach that branch. The guard exists because `doApply` does not trust the upstream contract absolutely — it defends against any hypothetical bypass of the preclaim check. + +Similarly, the `tefINTERNAL` guard when `sleOwner` cannot be found at apply time is marked `LCOV_EXCL_LINE`, confirming it is considered unreachable given correct ledger state (the account's existence was verified in `preclaim`). \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMBid.h.ai.json b/include/xrpl/tx/transactors/dex/AMMBid.h.ai.json new file mode 100644 index 0000000000..5ce21a2a33 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMBid.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 49, + "name": "ctx" + }, + { + "lineno": 54, + "name": "ctx" + }, + { + "lineno": 57, + "name": "ctx" + }, + { + "lineno": 60, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 47, + "name": "AMMBid" + } + ], + "description": "Implements the AMMBid transactor for XRPL, enabling auctioning of AMM trading fee slots to LPToken holders, handling slot states, bid pricing, and revenue distribution.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/AMMBid.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 49, + "name": "AMMBid" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 54, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 57, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 60, + "name": "preclaim" + }, + { + "args": [], + "lineno": 63, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMBid.h.ai.md b/include/xrpl/tx/transactors/dex/AMMBid.h.ai.md new file mode 100644 index 0000000000..664bf61634 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMBid.h.ai.md @@ -0,0 +1,52 @@ +# `AMMBid.h` — AMM Auction Slot Bid Transactor + +`AMMBid` declares the transactor that implements XRPL's continuous auction mechanism for AMM trading-fee slots, as specified in [XLS-30d](https://github.com/XRPLF/XRPL-Standards/discussions/78). Its purpose is to let liquidity providers (LPs) bid—using their own LP tokens—for a 24-hour window during which they trade against the pool at a heavily discounted fee. + +## Context in the DEX Transactor Family + +`AMMBid.h` lives alongside nine other DEX transactors (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMVote`, `AMMDelete`, `AMMClawback`, `OfferCreate`, `OfferCancel`). Every AMM transactor in this directory follows the same minimal declaration pattern: a single class that inherits `Transactor`, re-declares `ConsequencesFactory`, and exposes the three static phase entry-points plus the virtual `doApply`. The header is intentionally thin — all logic lives in the corresponding `.cpp`. + +## Auction Slot Semantics + +The slot's lifetime is 24 hours divided into 20 equal intervals (each ~72 minutes). At any moment the slot is in one of three states: + +- **Empty** — no current holder; minimum price applies. +- **Occupied** — a holder exists and is in interval 1–18 (at least 5 % of time remains); the outbid formula applies. +- **Tailing** — holder is in the final interval (< 5 % remaining); the holder retains privileges but receives no refund and effectively pays only the minimum. + +The implementation in `applyBid()` captures this with a `validOwner` lambda that returns true only when `timeSlot` is set and less than `tailingSlot` (interval 19). This means displacing a tailing holder costs nothing beyond the minimum price — a design choice that discourages camping on a nearly-expired slot. + +The pricing curve is: + +``` +computedPrice = pricePurchased * 1.05 * (1 - fractionUsed^60) + minSlotPrice +``` + +The `power(fractionUsed, 60)` term causes the curve to decay rapidly in early intervals (when `fractionUsed` is small) and flatten near expiry. The 5 % multiplier on `pricePurchased` ensures there is always a meaningful premium over the previous bid, preventing token-free slot squatting. + +## Three-Phase Processing + +`AMMBid` uses the standard XRPL three-phase transaction lifecycle: + +**`checkExtraFeatures`** runs inside `invokePreflight` before anything else. It gates the entire transaction on two amendment checks: `ammEnabled(ctx.rules)` for the base AMM feature, and `featureMPTokensV2` if either pool asset is an MPT. Returning `false` yields `temDISABLED`, which means the transaction type is treated as unknown — it won't claim a fee. This is the canonical XRPL pattern for tying a transaction type to a specific ledger amendment. + +**`preflight`** validates the transaction itself without ledger access. It rejects malformed asset pairs (via `invalidAMMAssetPair`), validates that any `BidMin`/`BidMax` amounts are well-formed LP token quantities, and enforces that `AuthAccounts` contains at most four accounts. Under `fixAMMv1_3` it additionally prevents duplicate account entries and self-authorization — a fix for an edge case in the original spec. + +**`preclaim`** performs read-only ledger validation. It confirms the AMM object exists for the specified asset pair, the pool is not empty (`sfLPTokenBalance != 0`), all `AuthAccounts` reference existing on-ledger accounts, and the submitting account actually holds LP tokens. It also cross-checks that any `BidMin`/`BidMax` amounts share the same LP token asset type and do not exceed the submitter's own balance — catching attempts to bid with someone else's tokens. + +**`doApply`** wraps execution in a `Sandbox` over the current ledger view. This allows the `applyBid` helper to build all ledger mutations — LP token burns, refunds to the previous slot holder, and the updated `AuctionSlot` object inside `ltAMM` — as a tentative set. Only on full success does `sb.apply(ctx_.rawView())` commit them atomically. Any internal failure leaves the ledger untouched. + +## Revenue Distribution Inside `applyBid` + +The static `applyBid` function, called exclusively by `doApply`, computes: + +1. **Refund to previous holder**: `(1 − fractionUsed) × pricePurchased` LP tokens transferred directly from the bidder to the outgoing slot-holder via `accountSend`. +2. **Burn**: `payPrice − refund` LP tokens are redeemed and removed from `sfLPTokenBalance` on the AMM object. Burning LP tokens increases the proportional share of all remaining LPs, so the pool itself benefits from each auction cycle. + +The `updateSlot` lambda writes the new `sfAccount`, `sfExpiration` (current time + `TOTAL_TIME_SLOT_SECS`), `sfDiscountedFee` (set to `tradingFee / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION`, or absent if zero), `sfPrice`, and `sfAuthAccounts`. Removing `sfDiscountedFee` when the fee is zero rather than storing a zero value keeps the ledger object compact. + +## `ConsequencesFactory` and Error Taxonomy + +`ConsequencesFactory{Normal}` signals to the transaction queue that `AMMBid` does not block other transactions from the same account while it is pending — it neither depletes the full account balance like a payment nor locks the account like an `AccountDelete`. The choice reflects that a bid only consumes a bounded amount of LP tokens. + +Error codes returned span both `NotTEC` (preflight: `temMALFORMED`, `temBAD_AMM_TOKENS`) and `TER` (preclaim/apply: `terNO_AMM`, `terNO_ACCOUNT`, `tecAMM_EMPTY`, `tecAMM_INVALID_TOKENS`, `tecAMM_FAILED`), following XRPL conventions where `ter*` codes may retry and `tec*` codes claim a fee. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMClawback.h.ai.json b/include/xrpl/tx/transactors/dex/AMMClawback.h.ai.json new file mode 100644 index 0000000000..5383e4a953 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMClawback.h.ai.json @@ -0,0 +1,85 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "AMMClawback" + } + ], + "description": "Defines the AMMClawback transactor class for handling AMM clawback transactions in the XRPL codebase, including methods for preflight checks, applying the transaction, and withdrawing assets.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/AMMClawback.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "AMMClawback" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 24, + "name": "preclaim" + }, + { + "args": [], + "lineno": 27, + "name": "doApply" + }, + { + "args": [ + "Sandbox& view" + ], + "lineno": 31, + "name": "applyGuts" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& holder", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& holdLPtokens", + "STAmount const& amount" + ], + "lineno": 39, + "name": "equalWithdrawMatchingOneAmount" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMClawback.h.ai.md b/include/xrpl/tx/transactors/dex/AMMClawback.h.ai.md new file mode 100644 index 0000000000..3a16cbbacd --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMClawback.h.ai.md @@ -0,0 +1,58 @@ +# `AMMClawback.h` — Regulatory Asset Recovery from AMM Pools + +## Role in the System + +`AMMClawback` implements the transactor for the `AMMClawback` transaction type defined in XLS-73, which allows a token issuer to reclaim assets from a specific holder's position in an Automated Market Maker (AMM) pool. It exists because issuers of regulated tokens (those with `lsfAllowTrustLineClawback` set, or MPT issuances with `lsfMPTCanClawback`) must retain the ability to exercise clawback even when a holder's assets are locked inside an AMM liquidity position. Without this, a holder could circumvent issuer clawback authority simply by depositing regulated tokens into a pool. + +The class inherits from `Transactor` and fits the standard three-phase transaction pipeline: static `preflight` validation, ledger-aware `preclaim` checks, and stateful `doApply` execution. + +## Design: Clawback as Forced Withdrawal + +The core design insight is that clawback from an AMM is implemented entirely as a forced *withdrawal* — the issuer compels the holder's LP tokens to be redeemed and the proceeds sent to the issuer rather than the holder. This reuses `AMMWithdraw::equalWithdrawTokens` and `AMMWithdraw::withdraw` as static helpers rather than duplicating AMM math, making the correctness argument depend on the well-tested withdrawal machinery. + +Two paths through `applyGuts` handle the two modes: + +1. **Full clawback** (no `sfAmount` field in the transaction): All of the holder's LP tokens are redeemed via `AMMWithdraw::equalWithdrawTokens` with `WithdrawAll::Yes`. This is a proportional two-asset withdrawal that burns every LP token the holder owns. + +2. **Partial clawback** (with `sfAmount` specifying a maximum asset1 quantity): The private `equalWithdrawMatchingOneAmount` method calculates what fraction of the pool corresponds to that asset1 amount, derives the matching asset2 and LP token quantities, and calls `AMMWithdraw::withdraw`. Critically, if the fraction exceeds what the holder actually holds in LP tokens, the code falls back to a full clawback of whatever the holder has — the issuer cannot over-claw, but gets at most everything available. + +In both modes the trading fee is explicitly passed as `0` (`tfee=0`). This is intentional: the withdrawal is a proportional equal-ratio removal of both assets, so no price-impact fee applies. Charging a trading fee would effectively penalize the issuer for exercising a regulatory right. + +## `equalWithdrawMatchingOneAmount` — Proportional Arithmetic + +This private method encapsulates the non-trivial case of computing a proportional withdrawal constrained by one asset amount. It computes: + +``` +frac = amount / amountBalance // fraction of pool to withdraw +amount2 = amount2Balance * frac // paired asset amount +lpTokens = lptAMMBalance * frac // LP tokens to burn +``` + +If `lpTokens > holdLPtokens` (the holder doesn't have enough LP tokens to satisfy the requested asset1 amount), the method degrades gracefully to a full clawback via `equalWithdrawTokens`. This prevents the transaction from failing on a precondition that would be hard to enforce atomically. + +When the `fixAMMClawbackRounding` amendment is active, the method invokes `getRoundedLPTokens` and `getRoundedAsset` helpers to snap values to representable amounts before calling `withdraw`, preventing dust accumulation or rounding-driven invariant violations. + +## Freeze and Auth Bypass + +Both code paths pass `FreezeHandling::fhIGNORE_FREEZE` and `AuthHandling::ahIGNORE_AUTH` to `AMMWithdraw`'s helpers. This is deliberate: an issuer exercising clawback must not be blocked by a trustline freeze or authorization state that they themselves may have set. Clawback is a higher-authority operation that supersedes normal trustline restrictions. + +## `preflight` — Static Validation + +Key invariants enforced before touching ledger state: +- `sfAsset` cannot be XRP (only issued assets can be clawed back). +- The transaction's `sfAccount` (issuer) must match `sfAsset`'s issuer field — issuers can only claw their own assets, not those of others. +- If `tfClawTwoAssets` is set, both assets must share the same issuer. A single issuer cannot claw an asset they don't control. +- If an `sfAmount` is provided, its asset subfield must match `sfAsset`, and the quantity must be positive. +- The holder (`sfHolder`) cannot be the same account as the issuer. + +## `preclaim` — Ledger-State Validation + +Permission gating occurs here, where ledger state is readable. For IOU-based assets, the issuer account must have `lsfAllowTrustLineClawback` set and must not have `lsfNoFreeze` set (an account that permanently waived freeze rights cannot reclaim clawback ability). For MPT-based assets, the specific MPT issuance must carry `lsfMPTCanClawback`. This check is encapsulated in the `checkClawAsset` lambda that dispatches over `Issue` vs `MPTIssue` variants. + +The `featureAMMClawback` amendment gate lives in `checkExtraFeatures`, which also restricts MPT assets in the transaction to require `featureMPTokensV2`. This guards new token type support behind its own amendment rollout. + +## Asset Transfer Mechanics + +After the withdrawal succeeds, `applyGuts` calls `directSendNoFee` to move the clawed-back asset1 amount from the holder's account to the issuer. For the second asset, transfer only occurs if `tfClawTwoAssets` is set in the transaction flags — otherwise asset2 stays with the holder. This means by default a single-issuer AMM pair results in only the issuer's own token being recovered, which is the conservative regulatory minimum. + +The `doApply` method wraps `applyGuts` in a `Sandbox`: all ledger mutations accumulate in the sandbox, and are flushed to the real `ApplyView` only if `applyGuts` returns success. Failed transactions leave no ledger trace beyond fee deduction, consistent with how all XRPL transactors handle partial-failure isolation. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMContext.h.ai.json b/include/xrpl/tx/transactors/dex/AMMContext.h.ai.json new file mode 100644 index 0000000000..400769c8ef --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMContext.h.ai.json @@ -0,0 +1,78 @@ +{ + "args": [ + { + "lineno": 31, + "name": "account" + }, + { + "lineno": 31, + "name": "multiPath" + }, + { + "lineno": 41, + "name": "fs" + } + ], + "classes": [ + { + "args": [ + "AccountID const& account, bool multiPath" + ], + "lineno": 13, + "name": "AMMContext" + } + ], + "description": "Defines the AMMContext class, which maintains Automated Market Maker (AMM) state and context during payment engine execution and iterations in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/AMMContext.h", + "functions": [ + { + "args": [], + "lineno": 36, + "name": "multiPath" + }, + { + "args": [ + "fs" + ], + "lineno": 41, + "name": "setMultiPath" + }, + { + "args": [], + "lineno": 46, + "name": "setAMMUsed" + }, + { + "args": [], + "lineno": 50, + "name": "update" + }, + { + "args": [], + "lineno": 58, + "name": "maxItersReached" + }, + { + "args": [], + "lineno": 63, + "name": "curIters" + }, + { + "args": [], + "lineno": 68, + "name": "account" + }, + { + "args": [], + "lineno": 75, + "name": "clear" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMContext.h.ai.md b/include/xrpl/tx/transactors/dex/AMMContext.h.ai.md new file mode 100644 index 0000000000..ff513d9423 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMContext.h.ai.md @@ -0,0 +1,55 @@ +# `AMMContext.h` — AMM State Tracking for the Payment Engine + +## Role in the System + +`AMMContext` is a small, non-copyable state container that threads through the XRPL payment engine for the duration of a single `flow()` call. Its existence solves a concrete safety problem: AMM (Automated Market Maker) offers generated by `AMMLiquidity` are not tracked by `BookStep`'s existing CLOB offer counter, so without a separate governor the engine could generate and consume AMM liquidity indefinitely across payment iterations. `AMMContext` is that governor — it counts how many engine iterations actually consumed an AMM offer and cuts off further AMM participation once `MaxIterations` (30) is reached. + +## Design Intent + +A single `AMMContext` instance is created at the top of `Flow.cpp::flow()`: + +```cpp +AMMContext ammContext(src, false); +``` + +The `src` argument is the initiating transaction's `AccountID`. This is not merely informational — `BookStep` uses it to retrieve the AMM's per-account trading fee, which can vary based on the sender's relationship to the pool. Carrying the account here avoids threading it separately through every level of the call stack. + +The object is then passed by reference into `toStrands()` and forwarded to `flowOne()` / `multiStrand()`, ultimately landing inside every `AMMLiquidity` instance associated with a book step. `AMMLiquidity` stores a mutable reference (`AMMContext&`), not a copy, so all parts of the engine share one authoritative counter for the entire payment. + +## Lifecycle of a Payment Iteration + +The engine's iteration loop in `StrandFlow.h` calls three `AMMContext` methods in a specific sequence that forms a mini state machine: + +1. **`clear()`** — called at the start of each *strand attempt*. Strand execution can fail (the sandbox is discarded), so `ammUsed_` must be reset to `false` before trying the next candidate strand. Without this, a failure in one strand would cause its AMM consumption to be double-counted when `update()` is called for the strand that actually succeeds. + +2. **`setAMMUsed()`** — called from `AMMOffer::consume()` the moment an AMM offer is accepted. This flips the per-iteration boolean flag without touching the counter. Separating the flag from the counter lets the engine distinguish "an AMM offer was consumed in *this* iteration" from "total AMM iterations so far." + +3. **`update()`** — called once per iteration after the best strand's sandbox has been applied to the main ledger view. It increments `ammIters_` only if `ammUsed_` is true, then unconditionally resets `ammUsed_` to `false`. Iterations that route entirely through CLOB offers leave `ammIters_` unchanged. + +## The `MaxIterations` Limit + +```cpp +constexpr static std::uint8_t MaxIterations = 30; +``` + +`maxItersReached()` is checked in the outer iteration loop before each new AMM offer is generated. This hard cap is necessary because AMM offers have fundamentally different exhaustion semantics than CLOB offers: a CLOB offer is removed from the book after consumption, naturally terminating the stream, but an AMM pool is continuous and can always produce another offer at the current spot price. If uncapped, a payment path through an AMM could spin indefinitely. The choice of 30 is a protocol-level constant that limits worst-case execution without unduly constraining practical liquidity extraction. + +## Multi-Path vs. Single-Path Offer Strategy + +`multiPath_` controls which offer-generation algorithm `AMMLiquidity` uses: + +- **Single-path** (`multiPath_ == false`): `AMMLiquidity` sizes each AMM offer so that the post-swap spot price equals the best competing CLOB offer's quality. This makes the AMM behave as a price-competitive peer to CLOB orders. +- **Multi-path** (`multiPath_ == true`): `AMMLiquidity` generates offers sized according to the Fibonacci sequence, keyed to `ammIters_`. Early iterations provide small probes; later iterations provide larger slices. + +The `multiPath_` flag is set (and updated) in two places: once immediately after `toStrands()` based on total strand count, and again inside `flowOne()` each time `activateNext()` is called, because the number of active strands can change mid-payment. `AMMContext::setMultiPath()` allows this dynamic update. + +## Why Non-Copyable? + +The copy constructor and copy assignment operator are explicitly deleted. This is deliberate: because all `AMMLiquidity` objects hold a reference to the single shared `AMMContext`, allowing copies would silently create divergent counters that the engine could not reconcile. The deletion makes this invariant a compile-time guarantee rather than a runtime risk. + +## Relationship to Sibling Files + +- **`AMMLiquidity.h`** — holds a `AMMContext&` member; consults `multiPath()` to pick offer sizing strategy; exposes `context()` so callers can call `setAMMUsed()` after offer consumption. +- **`AMMOffer` (via `AMMOffer.cpp`)** — calls `context().setAMMUsed()` inside `consume()`, making `AMMContext` the bridge between offer consumption and iteration accounting. +- **`StrandFlow.h`** — owns the iteration loop that calls `clear()`, `update()`, and checks `maxItersReached()`; sets `multiPath` after each `activateNext()`. +- **`Flow.cpp`** — the sole construction site; passes `src` and the initial `false` multiPath flag. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMCreate.h.ai.json b/include/xrpl/tx/transactors/dex/AMMCreate.h.ai.json new file mode 100644 index 0000000000..6c2e1b5cbb --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMCreate.h.ai.json @@ -0,0 +1,57 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 28, + "name": "AMMCreate" + } + ], + "description": "Implements the AMMCreate transactor for creating Automatic Market Maker (AMM) instances on the XRPL, allowing users to create AMMs with two tokens, manage liquidity, fees, and related ledger objects.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/AMMCreate.h", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 38, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 41, + "name": "preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 44, + "name": "calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 47, + "name": "preclaim" + }, + { + "args": [], + "lineno": 50, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMCreate.h.ai.md b/include/xrpl/tx/transactors/dex/AMMCreate.h.ai.md new file mode 100644 index 0000000000..3eb2dd570c --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMCreate.h.ai.md @@ -0,0 +1,45 @@ +# `AMMCreate.h` — AMM Instance Creation Transactor + +## Role in the System + +`AMMCreate.h` declares the `AMMCreate` transactor, one of the core DEX primitives in the XRPL. It handles the `AMMCreate` transaction type, which bootstraps a new Automatic Market Maker pool from scratch. Within the DEX module (alongside `AMMDeposit`, `AMMWithdraw`, `AMMVote`, `AMMBid`, and `AMMDelete`), `AMMCreate` is the entry point — no other AMM transaction can operate until this one successfully creates the pool's ledger objects and distributes initial LP tokens to the creator. + +The header is deliberately minimal: a `#pragma once`, a single `#include` of `Transactor.h`, and the class declaration. All implementation lives in `AMMCreate.cpp`. + +## The Transactor Lifecycle + +`AMMCreate` inherits from `Transactor` and participates in the four-phase transaction processing pipeline that the framework drives through compile-time polymorphism (name hiding, not virtual dispatch). The `invokePreflight` template in `Transactor.h` sequences the static methods automatically: + +**`checkExtraFeatures`** gates the entire transaction on feature flags before any validation occurs. The AMM subsystem itself (`ammEnabled`) must be active on the current rules set, and if either asset is an MPT issue, `featureMPTokensV2` must also be enabled. This separation from `preflight` keeps amendment checks centralized and avoids redundant ledger reads. + +**`preflight`** performs stateless structural checks on the transaction fields. It rejects duplicate assets (`sfAmount.asset() == sfAmount2.asset()`), validates both amounts via `invalidAMMAmount`, and enforces the `TRADING_FEE_THRESHOLD` ceiling on `sfTradingFee`. Because `preflight` is stateless — it has no `ReadView` access — these checks are cheap and deterministic. + +**`calculateBaseFee`** departs from the standard pattern: rather than returning the network's base fee in drops, it returns `calculateOwnerReserveFee`, which equals one owner reserve increment. This reflects the economic reality that `AMMCreate` permanently allocates a ledger object; charging only the base fee would undercharge for a scarce resource. + +**`preclaim`** performs the expensive ledger reads. It checks for a pre-existing AMM at the canonical keylet (`keylet::amm(asset1, asset2)`), verifies authorization and freeze status for both assets, checks that IOU issuers have `lsfDefaultRipple` set (required so balance can flow freely through the pool), confirms the creator has enough XRP for the LP token trustline reserve plus initial asset deposits, and rejects attempts to seed a pool with existing LP tokens (preventing AMM-of-AMM bootstrapping). It also enforces the clawback policy: before `featureAMMClawback` was enabled, any asset with clawback capability (`lsfAllowTrustLineClawback` or `lsfMPTCanClawback`) was forbidden entirely. + +**`doApply`** wraps execution in a `Sandbox` — a copy-on-write overlay over the live `ApplyView` — calling the file-local `applyCreate()` function. Only when that function succeeds does `sb.apply(ctx_.rawView())` flush the changes atomically. This is the standard XRPL rollback pattern: if anything inside `applyCreate` returns a failure `TER`, the sandbox is discarded and the ledger is untouched. + +## What `doApply` Actually Creates + +`applyCreate()` in the `.cpp` builds four classes of ledger objects: + +1. **A pseudo-account** (`AccountRoot`) created by `createPseudoAccount()` with a key derived from the AMM keylet. The account has no master key and is marked with `sfAMMID`, making it a non-user account that exists solely to hold XRP balances and issue LP tokens. + +2. **An `ltAMM` ledger object** keyed by `hash{asset1.currency, asset1.issuer, asset2.currency, asset2.issuer}` with the assets stored in canonical order (`std::minmax`). This object records `sfAccount` (the pseudo-account ID), `sfLPTokenBalance`, both assets, and the initial fee/auction/vote state via `initializeFeeAuctionVote`. The deterministic key means any transaction can look up the AMM without any stored pointer — the lookup is pure computation. + +3. **LP tokens** computed as `sqrt(A * B)` via `ammLPTokens()` and issued from the AMM pseudo-account to the creator via `accountSend`. The initial creator simultaneously receives the auction slot and voting slot. + +4. **Trustlines / MPToken entries** for each asset. For IOU assets, the trustline is flagged `lsfAMMNode` to signal it is pool-owned (credit limit stays at 0, deliberately preventing unsolicited LPToken sends). For MPT assets, `createMPToken` is called with `lsfMPTAMM` and, if needed, `lsfMPTAuthorized`. Transfer fees are waived for the initial asset sends (`WaiveTransferFee::Yes`). + +After successful object creation, both directions of the trading pair are registered in the `OrderBookDB`, enabling payment path finding and offer crossing to route through the new pool. + +## Design Decisions Worth Noting + +The `ConsequencesFactory = Normal` constant tells the transaction queue that `AMMCreate` does not block subsequent transactions from the same account — it is a one-shot creation, not an ongoing lock. + +The `DefaultRipple` requirement on IOU issuers is non-obvious but essential: without it, the AMM's trustlines cannot ripple balances between counterparties, breaking the pool's core exchange mechanic. + +The zero-credit-limit trustlines for LP tokens are a deliberate security constraint. A holder can only acquire LP tokens through affirmative actions (deposit, trustline setup, offer crossing) — the AMM cannot push tokens to an unwilling account. + +The clawback evolution across feature flags illustrates XRPL's incremental amendment strategy: the initial AMM release entirely forbade clawback-capable assets; `featureAMMClawback` later unlocked that restriction with proper accounting, but the older guard code remains active on ledgers where that amendment has not yet passed. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMDelete.h.ai.json b/include/xrpl/tx/transactors/dex/AMMDelete.h.ai.json new file mode 100644 index 0000000000..c33d326124 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMDelete.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 17, + "name": "ctx" + }, + { + "lineno": 22, + "name": "ctx" + }, + { + "lineno": 25, + "name": "ctx" + }, + { + "lineno": 28, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 14, + "name": "AMMDelete" + } + ], + "description": "Implements the AMMDelete transactor, which deletes an AMM instance in an empty state (when LP tokens are zero), removing trustlines up to a configured maximum and deleting the AMM and root account if all trustlines are removed.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/AMMDelete.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 17, + "name": "AMMDelete" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 22, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 25, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 28, + "name": "preclaim" + }, + { + "args": [], + "lineno": 31, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMDelete.h.ai.md b/include/xrpl/tx/transactors/dex/AMMDelete.h.ai.md new file mode 100644 index 0000000000..357b1f912b --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMDelete.h.ai.md @@ -0,0 +1,40 @@ +# `AMMDelete.h` — AMM Deletion Transactor Interface + +## Role and Context + +`AMMDelete` declares the transactor responsible for removing an Automated Market Maker instance from the XRPL ledger once it has been fully drained. It sits in the `include/xrpl/tx/transactors/dex/` directory alongside the rest of the AMM lifecycle transactors (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMBid`, `AMMVote`, `AMMClawback`) and inherits from the common `Transactor` base class. + +An AMM on the XRP Ledger consists of several ledger objects: an `ltAMM` entry keyed by the hash of the two asset identifiers, a synthetic `AccountRoot` with a disabled master key (the "AMM account"), and a collection of trustlines (or MPToken objects) representing LP token holdings. When all liquidity has been withdrawn via `AMMWithdraw` and the `sfLPTokenBalance` field on the `ltAMM` object reaches zero, none of those supporting ledger objects have been automatically cleaned up. `AMMDelete` handles that deferred cleanup. + +## Transactor Pipeline + +`AMMDelete` participates in the three-stage transactor pipeline mandated by the base class: + +**`checkExtraFeatures`** enforces two amendment preconditions: the base AMM feature must be enabled (`ammEnabled`), and if either asset in the pair is an MPT (Multi-Purpose Token), the `featureMPTokensV2` amendment must also be live. This gating is notable because it prevents deletion of MPT-based AMM pools in network versions that don't fully support MPT cleanup, avoiding partial or inconsistent ledger state even if such a pool somehow reached zero balance. + +**`preflight`** is deliberately trivial — it returns `tesSUCCESS` immediately. All amendment checks are already handled by `checkExtraFeatures`, and there is no field-level validation of the transaction payload that can be done without consulting ledger state. This is the correct design given the XRPL constraint that `preflight` must be purely stateless: it receives only rules, flags, and the raw transaction object. + +**`preclaim`** performs the real gate-check against the current ledger: it reads the `ltAMM` object for the submitted asset pair and verifies that `sfLPTokenBalance` is exactly zero. If the AMM doesn't exist it returns `terNO_AMM`; if it still holds LP tokens it returns `tecAMM_NOT_EMPTY`. Both failures prevent fee collection (`ter*` codes are soft failures that do not charge fees, while `tec*` codes do). The balance check here is important: it prevents a user from using `AMMDelete` as a shortcut to destroy a pool that still has liquidity — the proper drain path is `AMMWithdraw`. + +## Incremental Deletion in `doApply` + +The most architecturally significant aspect of `AMMDelete` is its chunked deletion model. `doApply()` wraps all work in a `Sandbox` (an isolated copy of the ledger view) and delegates to `deleteAMMAccount()` from `AMMHelpers`. That helper deletes AMM trustlines up to the constant `maxDeletableAMMTrustLines` (512) per invocation. If trustlines remain after the cap is reached, `deleteAMMAccount` returns `tecINCOMPLETE` rather than `tesSUCCESS`. + +Crucially, `doApply` applies the sandbox to the raw view on **both** `tesSUCCESS` and `tecINCOMPLETE`: + +```cpp +if (isTesSuccess(ter) || ter == tecINCOMPLETE) + sb.apply(ctx_.rawView()); +``` + +This means partial progress — the deletion of up to 512 trustlines — is committed to the ledger even when the job isn't finished. The `tec` return code causes the transaction to succeed (charging the submitter a fee) while signaling incompleteness. The caller must re-submit `AMMDelete` until the pool is fully removed. + +This incremental approach exists to respect the per-transaction ledger-modification limits. An AMM that has been active for a long time might accumulate hundreds or thousands of LP token trustlines from participants who never withdrew. Attempting to delete all of them atomically in a single transaction could exhaust ledger traversal limits or impose unbounded computational cost. The 512-entry cap bounds worst-case per-transaction work while still making guaranteed progress each round. + +After all trustlines are cleared, `deleteAMMAccount` also removes any MPToken objects (for MPT-based pools), erases the `ltAMM` entry from the owner directory, and finally erases both the `ltAMM` object and the AMM `AccountRoot` itself. + +## Relationship to Other Transactors + +`AMMDelete` shares the pattern of `ConsequencesFactory{Normal}`, the same as every other AMM transactor. This tells the consequences system that the transaction is neither a `Blocker` (which would prevent later transactions from the same account in a batch) nor a `Custom` cost calculator. The constructor simply forwards `ApplyContext` to the base `Transactor`, with no additional state needed beyond what the base class provides. + +The `deleteAMMAccount` helper is also called from `AMMWithdraw` in the full-withdrawal path, meaning `AMMDelete` is essentially the cleanup fallback for pools that were fully drained but whose accounts weren't automatically removed at withdrawal time — or where the auto-removal path failed due to the same trustline-count constraints. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMDeposit.h.ai.json b/include/xrpl/tx/transactors/dex/AMMDeposit.h.ai.json new file mode 100644 index 0000000000..c43bc3ae0f --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMDeposit.h.ai.json @@ -0,0 +1,168 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 36, + "name": "AMMDeposit" + } + ], + "description": "Implements the AMMDeposit transactor for adding liquidity to an AMM pool in the XRPL, supporting various deposit modes and updating pool and LP token balances accordingly.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/AMMDeposit.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 38, + "name": "AMMDeposit" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 43, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 46, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 49, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 52, + "name": "preclaim" + }, + { + "args": [], + "lineno": 55, + "name": "doApply" + }, + { + "args": [ + "Sandbox& view" + ], + "lineno": 59, + "name": "applyGuts" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amountDeposit", + "std::optional const& amount2Deposit", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokensDeposit", + "std::optional const& depositMin", + "std::optional const& deposit2Min", + "std::optional const& lpTokensDepositMin", + "std::uint16_t tfee" + ], + "lineno": 80, + "name": "deposit" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokensDeposit", + "std::optional const& depositMin", + "std::optional const& deposit2Min", + "std::uint16_t tfee" + ], + "lineno": 108, + "name": "equalDepositTokens" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "STAmount const& amount2", + "std::optional const& lpTokensDepositMin", + "std::uint16_t tfee" + ], + "lineno": 134, + "name": "equalDepositLimit" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "std::optional const& lpTokensDepositMin", + "std::uint16_t tfee" + ], + "lineno": 159, + "name": "singleDeposit" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokensDeposit", + "std::uint16_t tfee" + ], + "lineno": 179, + "name": "singleDepositTokens" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount", + "STAmount const& lptAMMBalance", + "STAmount const& ePrice", + "std::uint16_t tfee" + ], + "lineno": 199, + "name": "singleDepositEPrice" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amount", + "STAmount const& amount2", + "Asset const& lptIssue", + "std::uint16_t tfee" + ], + "lineno": 219, + "name": "equalDepositInEmptyState" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMDeposit.h.ai.md b/include/xrpl/tx/transactors/dex/AMMDeposit.h.ai.md new file mode 100644 index 0000000000..2a63471661 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMDeposit.h.ai.md @@ -0,0 +1,46 @@ +# `AMMDeposit.h` — AMM Liquidity Deposit Transactor + +## Role in the System + +`AMMDeposit` is the `Transactor` subclass responsible for processing `AMMDeposit` transactions on the XRP Ledger, as defined in [XLS-30d](https://github.com/XRPLF/XRPL-Standards/discussions/78). It implements the mechanism by which liquidity providers add assets to an AMM pool, receiving LP tokens that represent their fractional share of the pool's reserves. The file lives alongside its counterpart `AMMWithdraw.h` and the supporting `AMMCreate.h`, `AMMBid.h`, `AMMVote.h`, and `AMMDelete.h` transactors in the `dex/` directory. + +## Deposit Modes and Flag Dispatch + +The most architecturally significant aspect of `AMMDeposit` is that it exposes six distinct deposit modes, each selected by a single mutually-exclusive transaction flag (`tfDepositSubTx`). The `preflight` enforces this exclusivity using `std::popcount` — exactly one sub-mode bit must be set or the transaction is `temMALFORMED`. The six modes correspond to six private methods, selected in `applyGuts` by testing `subTxType & tfXxx`: + +| Flag | Method | Fee charged? | Description | +|---|---|---|---| +| `tfLPToken` | `equalDepositTokens` | No | Deposit proportional assets for a target LP token amount | +| `tfTwoAsset` | `equalDepositLimit` | No | Proportional deposit with per-asset maximum constraints | +| `tfSingleAsset` | `singleDeposit` | Yes | Single-asset deposit by amount | +| `tfOneAssetLPToken` | `singleDepositTokens` | Yes | Single-asset deposit targeting an LP token quantity | +| `tfLimitLPToken` | `singleDepositEPrice` | Yes | Single-asset deposit with an effective-price ceiling | +| `tfTwoAssetIfEmpty` | `equalDepositInEmptyState` | N/A | Pool bootstrapping for a zero-balance AMM | + +The fee asymmetry is economically motivated: proportional deposits preserve the pool's price ratio and create no arb opportunity, so no trading fee is warranted. Single-asset deposits are mathematically equivalent to a swap followed by a proportional deposit, so the pool's trading fee is applied to the swap component. + +## Three-Phase Validation Pipeline + +`AMMDeposit` follows the standard XRPL `Transactor` lifecycle: + +**`checkExtraFeatures`** guards the `featureAMM` amendment (via `ammEnabled`) and additionally enforces that MPT-backed (Multi-Purpose Token) pools are only permitted when `featureMPTokensV2` is active. This separates amendment gating from field validation. + +**`preflight`** performs purely structural checks against the transaction fields without touching ledger state: valid flag combination, no conflicting optional fields for each mode, asset-pair validity, non-zero LP token amounts, and trading fee within `TRADING_FEE_THRESHOLD`. Because `preflight` has no ledger view, it intentionally cannot check balances. + +**`preclaim`** performs ledger-state checks after signature verification. Critically, it distinguishes between two pool states: the `tfTwoAssetIfEmpty` flag requires `lptAMMBalance == 0` (returns `tecAMM_NOT_EMPTY` if populated), while all other modes require `lptAMMBalance > 0` (returns `tecAMM_EMPTY` if the pool is drained). It also validates account sufficiency, freeze state, and authorization. A comment in the source acknowledges that the balance check in `preclaim` is optimistic for modes where the actual deposit amount is derived from pool math — those modes re-validate inside `deposit()`. + +When `featureAMMClawback` is enabled, `preclaim` also checks whether either pool asset is individually frozen on the depositor's account, rejecting with `tecFROZEN`. + +## `applyGuts` and the Sandbox Pattern + +`doApply` creates a `Sandbox` (a copy-on-write ledger view) and delegates to `applyGuts`. All state mutations — balance transfers, trustline updates, LP token issuance — are staged against the sandbox. Only on `tesSUCCESS` does `applyGuts` call `sb.apply(ctx_.rawView())` to commit atomically. This is the standard XRPL pattern for ensuring ledger consistency: if any sub-operation fails, the sandbox is discarded without affecting consensus state. + +After a successful deposit, `applyGuts` updates the AMM ledger entry's `sfLPTokenBalance` field. In the empty-pool case (`lptAMMBalance == beast::zero`), it also calls `initializeFeeAuctionVote`, which initializes the auction slot and voting structure — granting the bootstrapping LP their initial fee-governance position. + +The trading fee used during `applyGuts` is determined by pool state: for non-empty pools, `getTradingFee` is called with the caller's `account_`, which accounts for any discounted fee won through the auction mechanism (`AMMBid`). For the empty-pool initialization path, the fee is taken directly from the optional `sfTradingFee` field in the transaction itself, defaulting to zero. + +## Structural Comparison with `AMMWithdraw` + +`AMMWithdraw` is the structural mirror of `AMMDeposit`, with a one-to-one mapping of deposit modes to withdrawal modes. One notable difference is that `AMMWithdraw` exposes its core `withdraw` and `equalWithdrawTokens` methods as `public static` functions. This allows `AMMDelete` to reuse the withdrawal logic when draining a pool for deletion. `AMMDeposit` has no such need and keeps all its mode-specific methods `private`, enforcing that the deposit path is only entered through the validated transactor lifecycle. + +The `deposit` private method acts as a shared execution kernel: after each mode-specific method calculates the precise asset amounts, it calls `deposit` to execute the actual token transfers, including creating LP token trustlines for new liquidity providers and XRP balance adjustments for XRP-denominated pool assets. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMVote.h.ai.json b/include/xrpl/tx/transactors/dex/AMMVote.h.ai.json new file mode 100644 index 0000000000..24b772f232 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMVote.h.ai.json @@ -0,0 +1,61 @@ +{ + "args": [ + { + "lineno": 32, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 22, + "name": "AMMVote" + } + ], + "description": "Implements the AMMVote transactor, allowing liquidity providers (LPs) holding LPTokens to vote on the TradingFee parameter of an AMM instance. Manages voting logic, updates, and TradingFee calculation based on weighted votes.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/AMMVote.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 32, + "name": "AMMVote" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 36, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 39, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 42, + "name": "preclaim" + }, + { + "args": [], + "lineno": 45, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMVote.h.ai.md b/include/xrpl/tx/transactors/dex/AMMVote.h.ai.md new file mode 100644 index 0000000000..16bc8698ec --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMVote.h.ai.md @@ -0,0 +1,33 @@ +# `AMMVote.h` — AMM Trading Fee Governance Transactor + +`AMMVote.h` declares the `AMMVote` transactor, the governance mechanism through which liquidity providers (LPs) collectively control the trading fee of an Automated Market Maker instance on the XRP Ledger. Rather than fixing the fee at pool creation or entrusting it to a single authority, the design distributes fee control proportionally to capital — each LP votes with a weight derived from their share of the pool's total `LPToken` supply. + +## Role in the System + +This header is part of the DEX transactor family alongside `AMMCreate`, `AMMDeposit`, `AMMBid`, and others. Each class in this directory inherits from `Transactor` and implements the same three-phase pipeline: `preflight` (stateless field validation), `preclaim` (read-only ledger checks), and `doApply` (state-mutating apply). `AMMVote` participates in this pattern without deviation, and its `ConsequencesFactory{Normal}` declaration signals that the framework should handle fee and sequence number consequences in the standard way — no custom fee scaling and no transaction blocking semantics. + +## The Three-Phase Contract + +**`checkExtraFeatures`** acts as an amendment gate. It returns `false` (causing `invokePreflight` to emit `temDISABLED`) when the core AMM amendment is not enabled. It also enforces a secondary check: if either pool asset is an MPT (`MPTIssue`), the `featureMPTokensV2` amendment must additionally be active. This guards against submitting MPT-based vote transactions before the network has upgraded. + +**`preflight`** is the stateless fast path. It validates that the asset pair is structurally coherent via `invalidAMMAssetPair` and that the proposed `sfTradingFee` does not exceed `TRADING_FEE_THRESHOLD` (1000, representing 1%). Catching malformed fees here — before any ledger reads — avoids burning preclaim resources on obviously invalid input. + +**`preclaim`** performs the first ledger-dependent checks with a read-only view. It verifies that the AMM object exists for the specified asset pair, that the pool is not empty (a zero `sfLPTokenBalance` means there are no LPs), and — critically — that the submitting account actually holds LPTokens via `ammLPHolds`. An account with no stake in the pool has no standing to influence its fee, so `tecAMM_INVALID_TOKENS` is returned early. + +**`doApply`** wraps execution in a `Sandbox` view so that all ledger mutations are applied atomically only on success. The actual logic is delegated to the file-scoped `applyVote` function in the implementation. + +## Vote Slot Management + +The `ltAMM` ledger object stores up to `VOTE_MAX_SLOTS` (8) `VoteEntry` objects in its `sfVoteSlots` array. Each entry records the voting account, the proposed `sfTradingFee`, and a `sfVoteWeight` — the LP's proportional token holding scaled by `VOTE_WEIGHT_SCALE_FACTOR` (100,000). + +During `applyVote`, stale entries (accounts that no longer hold LPTokens) are silently pruned on each vote transaction, keeping the slot array clean without requiring a separate maintenance transaction. When a new voter arrives and all eight slots are occupied, the entry with the fewest tokens is a candidate for eviction. The incoming vote only displaces it if the new LP holds *more* tokens than the current minimum holder, or holds equal tokens but proposes a higher fee. Tiebreaking by account ID (`account < minAccount`) ensures the eviction decision is deterministic across all validators. If no slot can be displaced, the transaction still succeeds but has no effect on the slot array — it simply refreshes existing entries. + +## Fee Calculation and Auction Slot Coupling + +The new `sfTradingFee` is computed as a weighted average: `sum(fee_i * lpTokens_i) / sum(lpTokens_i)` using running `num` and `den` accumulators over the updated slot array. Notably, the stored `sfVoteWeight` per entry is not used in this arithmetic — the fee is recalculated from raw token holdings each time. This avoids rounding drift that would accumulate if the fee were derived from previously stored (already-rounded) weights. + +After updating `sfTradingFee` on the AMM object, the transactor also propagates a change to `sfDiscountedFee` inside the `sfAuctionSlot` subobject if one is present. The discounted fee is `tradingFee / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION` (divided by 10), keeping the auction slot winner's price advantage in sync with any governance-driven fee change. When either fee rounds to zero, the field is removed rather than stored as zero, preserving ledger object compactness. + +## Design Rationale + +The eight-slot cap and stake-weighted eviction policy together prevent griefing: an attacker cannot cheaply flood the vote array with dust-sized positions to lock out larger LPs. The proportional weighting ensures that votes from well-capitalized LPs dominate, aligning fee governance with economic exposure to the pool's performance — a deliberate parallel to shareholder voting in traditional finance. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h.ai.json b/include/xrpl/tx/transactors/dex/AMMWithdraw.h.ai.json new file mode 100644 index 0000000000..fbd9c32bc0 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h.ai.json @@ -0,0 +1,210 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 36, + "name": "AMMWithdraw" + } + ], + "description": "Implements the AMMWithdraw transactor for the XRPL, enabling users to withdraw liquidity from an AMM pool in various ways, updating balances and pool composition accordingly.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/AMMWithdraw.h", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 44, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 47, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 50, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 53, + "name": "preclaim" + }, + { + "args": [], + "lineno": 56, + "name": "doApply" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const account", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokens", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee", + "FreezeHandling freezeHandling", + "AuthHandling authHandling", + "WithdrawAll withdrawAll", + "XRPAmount const& priorBalance", + "beast::Journal const& journal" + ], + "lineno": 75, + "name": "equalWithdrawTokens" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "AccountID const& account", + "STAmount const& amountBalance", + "STAmount const& amountWithdraw", + "std::optional const& amount2Withdraw", + "STAmount const& lpTokensAMMBalance", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee", + "FreezeHandling freezeHandling", + "AuthHandling authHandling", + "WithdrawAll withdrawAll", + "XRPAmount const& priorBalance", + "beast::Journal const& journal" + ], + "lineno": 101, + "name": "withdraw" + }, + { + "args": [ + "Sandbox& sb", + "std::shared_ptr const ammSle", + "STAmount const& lpTokenBalance", + "Asset const& asset1", + "Asset const& asset2", + "beast::Journal const& journal" + ], + "lineno": 123, + "name": "deleteAMMAccountIfEmpty" + }, + { + "args": [ + "Sandbox& view" + ], + "lineno": 130, + "name": "applyGuts" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amountWithdraw", + "std::optional const& amount2Withdraw", + "STAmount const& lpTokensAMMBalance", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee" + ], + "lineno": 143, + "name": "withdraw" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokens", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee" + ], + "lineno": 164, + "name": "equalWithdrawTokens" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "STAmount const& amount2", + "std::uint16_t tfee" + ], + "lineno": 185, + "name": "equalWithdrawLimit" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "std::uint16_t tfee" + ], + "lineno": 205, + "name": "singleWithdraw" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee" + ], + "lineno": 222, + "name": "singleWithdrawTokens" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "STAmount const& ePrice", + "std::uint16_t tfee" + ], + "lineno": 241, + "name": "singleWithdrawEPrice" + }, + { + "args": [ + "STTx const& tx" + ], + "lineno": 259, + "name": "isWithdrawAll" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/AMMWithdraw.h.ai.md b/include/xrpl/tx/transactors/dex/AMMWithdraw.h.ai.md new file mode 100644 index 0000000000..527367e3b7 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/AMMWithdraw.h.ai.md @@ -0,0 +1,51 @@ +# `AMMWithdraw.h` — AMM Liquidity Withdrawal Transactor + +## Role in the System + +`AMMWithdraw.h` declares the `AMMWithdraw` transactor, which handles the `AMMWithdraw` transaction type on the XRP Ledger. It is the mirror image of `AMMDeposit`: where deposit adds liquidity and issues LPTokens to the caller, withdrawal burns LPTokens and returns the underlying pool assets to the liquidity provider. The file lives in `include/xrpl/tx/transactors/dex/` alongside the complete family of AMM-related transactors (`AMMCreate`, `AMMDeposit`, `AMMBid`, `AMMVote`, `AMMDelete`, `AMMClawback`), all of which extend the central `Transactor` base class. + +## The Withdrawal Mode Design + +The most architecturally distinctive aspect of this class is that it implements **five distinct withdrawal modes**, each selected by the combination of fields the caller supplies in the transaction. Rather than a single monolithic path, each mode is a separate private method: + +| Fields supplied | Private method | Fee charged? | +|---|---|---| +| `LPTokens` only | `equalWithdrawTokens` | No | +| `Asset1Out` + `Asset2Out` | `equalWithdrawLimit` | No | +| `Asset1Out` only | `singleWithdraw` | Yes | +| `Asset1Out` + `LPTokens` | `singleWithdrawTokens` | Yes | +| `Asset1Out` + `EPrice` | `singleWithdrawEPrice` | Yes | + +The no-fee modes are those that remove liquidity proportionally without disturbing the pool's price ratio, i.e., they neither add nor remove arbitrage opportunity. Single-asset withdrawals are mechanically equivalent to a swap followed by a proportional withdrawal, so the trading fee applies — the same logic that makes single-asset deposits fee-bearing in `AMMDeposit`. + +The `equalWithdrawLimit` mode (both assets with caps) is distinctive: the trader specifies maximum amounts for each asset. The transactor computes the largest proportional withdrawal that fits within both caps, so the actual amounts withdrawn may be less than the stated maximums. This gives traders price-slippage protection when removing dual-asset liquidity. + +The `singleWithdrawEPrice` mode enforces two simultaneous constraints: a minimum output amount (`Asset1Out`, or zero meaning unconstrained) and a maximum effective price per LP-token burned (`EPrice`). This is the closest analogue to a limit order at exit time — the transaction fails if the market has moved beyond the price the trader was willing to accept. + +## The `WithdrawAll` Sentinel + +The file introduces a small but important `enum class WithdrawAll : bool { No = false, Yes }`. When a user redeems their entire LP position (all LPTokens), the implementation must handle the final-withdraw case carefully: rounding errors in pool math can leave dust amounts, and the pool itself may become empty. The `isWithdrawAll(STTx const&)` static helper decodes the transaction flags to produce this value, and it flows through to both `equalWithdrawTokens` and `withdraw` so the inner math can apply exact-zero semantics rather than ratio arithmetic. + +## Public Static Helpers and the Two-Layer Architecture + +`AMMWithdraw` exposes two public static overloads — `equalWithdrawTokens(...)` and `withdraw(...)` — whose signatures are broader than their private counterparts. They accept `FreezeHandling`, `AuthHandling`, `priorBalance`, and a `beast::Journal`, and return a four-tuple `std::tuple>` (error code, new LPToken balance, asset1 withdrawn, optionally asset2 withdrawn). These extra parameters come from `TokenHelpers.h`, where `FreezeHandling` (`fhIGNORE_FREEZE` / `fhZERO_IF_FROZEN`) and `AuthHandling` (`ahIGNORE_AUTH` / `ahZERO_IF_UNAUTHORIZED`) govern how the balance-transfer helpers behave when trustlines are frozen or unauthorised. Exposing these as static functions allows the `AMMDelete` transactor (which must drain a pool as part of account teardown) to call withdrawal logic without constructing a full `AMMWithdraw` transactor instance. + +The private counterparts of the same methods drop `FreezeHandling`, `AuthHandling`, `priorBalance`, and the journal — they are called from `applyGuts`, which already holds that context and passes it via the instance's `ApplyContext`. This layering avoids duplication: the public statics carry the full context for external callers; the private methods are thin wrappers that reuse `ctx_` state. + +## `deleteAMMAccountIfEmpty` + +The static `deleteAMMAccountIfEmpty` encapsulates the post-withdrawal cleanup check. After burning LPTokens, if the total supply of LP tokens has reached zero the AMM instance account and its associated `ltAMM` ledger entry become orphaned objects. This method detects that condition and triggers their deletion before returning, preventing ledger object leaks. It takes a `Sandbox` rather than the live view, which is consistent with the broader pattern in this codebase of staging all mutations in a sandbox that is only committed if `doApply()` succeeds. + +## Transaction Lifecycle + +The standard three-phase lifecycle mirrors every other transactor in the system: + +- `preflight` (static) — validates fields, flags, and feature gates without touching ledger state. +- `preclaim` (static, `ReadView` only) — verifies the AMM account exists, the LP position is valid, and enough LPTokens are held. +- `doApply` / `applyGuts` — selects the appropriate withdrawal mode based on the transaction's field combination, executes it against a `Sandbox`, then commits if successful. + +`ConsequencesFactory` is set to `Normal`, meaning the transaction claims a fee on failure in the standard way, not as a blocker or with custom fee logic. + +## Relationship to Sibling Files + +`AMMWithdraw.h` is structurally symmetric with `AMMDeposit.h`: both offer five field-combination modes, a fee/no-fee split along proportional vs. single-asset lines, `equalXxx` and `singleXxx` private methods, and the same `applyGuts` / `Sandbox` pipeline. `AMMDelete.h` is the downstream consumer of the public statics — it calls into the withdrawal machinery to drain a pool that has already reached zero LP supply but still has residual trust-line cleanup to perform. `AMMHelpers.h` provides the AMM constant-product math (`lpTokensOut`, `ammLPTokens`, etc.) that the private methods call to compute how many LP tokens to burn or how much asset to release. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/OfferCancel.h.ai.json b/include/xrpl/tx/transactors/dex/OfferCancel.h.ai.json new file mode 100644 index 0000000000..bccee0a924 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/OfferCancel.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 16, + "name": "ctx" + }, + { + "lineno": 19, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "OfferCancel" + } + ], + "description": "Defines the OfferCancel transactor class for handling offer cancellation transactions in the XRPL system.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/OfferCancel.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "OfferCancel" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 19, + "name": "preclaim" + }, + { + "args": [], + "lineno": 22, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/OfferCancel.h.ai.md b/include/xrpl/tx/transactors/dex/OfferCancel.h.ai.md new file mode 100644 index 0000000000..ee33c00731 --- /dev/null +++ b/include/xrpl/tx/transactors/dex/OfferCancel.h.ai.md @@ -0,0 +1,29 @@ +# `OfferCancel.h` — DEX Offer Cancellation Transactor + +`OfferCancel` is the transactor responsible for processing `OfferCancel` transactions on the XRP Ledger's decentralized exchange (DEX). It gives account holders a way to explicitly remove a standing offer they previously placed, identified by the offer's original sequence number. The implementation is deliberately minimal — the actual ledger bookkeeping is delegated to the shared `offerDelete` helper, and the three-phase pipeline (`preflight` → `preclaim` → `doApply`) performs only the checks that are specific to this transaction type. + +## Position in the Transactor Framework + +Like all transactors, `OfferCancel` inherits from `Transactor` and overrides `doApply()`, while exposing static `preflight` and `preclaim` methods that the framework invokes via `invokePreflight` and the preclaim dispatch. The base class handles signature verification, fee deduction, sequence number consumption, and the outer retry/reset logic — `OfferCancel` only adds domain-specific validation on top. + +The `ConsequencesFactory` is set to `Normal`, in contrast to `OfferCreate` which uses `Custom`. `Normal` means the framework can model this transaction's consequences without consulting the transactor: no funds are reserved or locked as a side effect of the cancellation itself beyond the fee. `OfferCreate` needs `Custom` because it may reserve owner reserves and lock funds in an order book, requiring per-transaction consequence computation. + +## Validation Pipeline + +**`preflight`** performs a single stateless check: it rejects the transaction with `temBAD_SEQUENCE` if `sfOfferSequence` is zero. A zero value is nonsensical — no valid offer can carry sequence number zero — and this is the only field-level validation needed beyond what `preflight1`/`preflight2` in the base already handle (account field, fee, flags, signature). + +**`preclaim`** reads the submitter's account from the ledger (returning `terNO_ACCOUNT` if absent, which would be highly unusual at this stage) and then enforces a temporal ordering invariant: the offer's sequence number must be strictly less than the account's current sequence number. If the `sfOfferSequence` value is greater than or equal to the account's current sequence, the offer could not yet exist on the ledger, so the transaction is rejected with `temBAD_SEQUENCE`. This check prevents clients from submitting cancellations for offers that haven't been created yet, which would otherwise succeed vacuously. + +## Application Logic and Idempotency + +`doApply()` resolves the target offer via `keylet::offer(account_, offerSequence)` — a deterministic ledger key constructed from the account ID and sequence number. If the offer entry exists in the current view, `offerDelete` is called to remove it along with its order book directory entries and any associated bookkeeping. If the offer is not found — because it was already consumed by a matching trade, previously cancelled, or expired — `doApply` returns `tesSUCCESS` anyway. + +This idempotent behavior is an intentional protocol design decision. From a client's perspective, a successful `OfferCancel` means the offer is no longer active, regardless of whether it was removed by this transaction or had already been cleaned up by other means. Returning a failure code in the "offer not found" case would be misleading and would require clients to distinguish between "offer was live and got cancelled" versus "offer was already gone," which has no practical value. The fee is still charged — the transaction is valid and was processed — but no ledger state changes result. + +## Relationship to `offerDelete` + +The actual removal work — unlinking the offer from the owner directory, removing it from the order book directory, updating owner counts — is entirely encapsulated in `offerDelete` from `xrpl/ledger/helpers/OfferHelpers.h`. `OfferCancel::doApply` is a thin policy layer: it determines *which* offer to delete and *whether* to call `offerDelete`, but not *how* deletion works. The same `offerDelete` primitive is shared with offer-crossing in `OfferCreate` and with expiry cleanup elsewhere in the engine, which keeps the teardown logic consistent and centralized. + +## Contrast with `OfferCreate` + +The structural contrast with `OfferCreate` makes the design philosophy clear. `OfferCreate` brings in `Quality.h`, defines `PaymentSandbox` and `Sandbox` interactions, exposes `applyGuts`, `flowCross`, and `applyHybrid` private methods, and uses a `Custom` consequences factory — all reflecting the complexity of order book insertion and potential immediate crossing. `OfferCancel` has none of this: it imports only `TxFlags.h` and `Transactor.h`, exposes the standard three static/virtual entry points, and fits in under 30 lines of implementation. This asymmetry is appropriate — creation is inherently complex while cancellation is a targeted deletion with a single ledger object as its subject. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/OfferCreate.h.ai.json b/include/xrpl/tx/transactors/dex/OfferCreate.h.ai.json new file mode 100644 index 0000000000..67fc84f28b --- /dev/null +++ b/include/xrpl/tx/transactors/dex/OfferCreate.h.ai.json @@ -0,0 +1,118 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 12, + "name": "OfferCreate" + } + ], + "description": "Defines the OfferCreate transactor class for creating offers in the XRPL ledger, including methods for preflight checks, applying the transaction, and offer crossing logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/dex/OfferCreate.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 15, + "name": "OfferCreate" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 20, + "name": "makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 23, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 26, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 30, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 34, + "name": "preclaim" + }, + { + "args": [], + "lineno": 38, + "name": "doApply" + }, + { + "args": [ + "Sandbox& view", + "Sandbox& view_cancel" + ], + "lineno": 42, + "name": "applyGuts" + }, + { + "args": [ + "ReadView const& view", + "ApplyFlags const flags", + "AccountID const id", + "beast::Journal const j", + "Asset const& asset" + ], + "lineno": 46, + "name": "checkAcceptAsset" + }, + { + "args": [ + "PaymentSandbox& psb", + "PaymentSandbox& psbCancel", + "Amounts const& takerAmount", + "std::optional const& domainID" + ], + "lineno": 52, + "name": "flowCross" + }, + { + "args": [ + "STAmount const& amount" + ], + "lineno": 58, + "name": "format_amount" + }, + { + "args": [ + "Sandbox& sb", + "std::shared_ptr sleOffer", + "Keylet const& offer_index", + "STAmount const& saTakerPays", + "STAmount const& saTakerGets", + "std::function)> const& setDir" + ], + "lineno": 61, + "name": "applyHybrid" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/dex/OfferCreate.h.ai.md b/include/xrpl/tx/transactors/dex/OfferCreate.h.ai.md new file mode 100644 index 0000000000..4bf769934e --- /dev/null +++ b/include/xrpl/tx/transactors/dex/OfferCreate.h.ai.md @@ -0,0 +1,54 @@ +# `OfferCreate.h` — DEX Offer Creation Transactor + +## Role in the System + +`OfferCreate` is the `Transactor` subclass responsible for processing `OfferCreate` transactions on the XRPL decentralized exchange. It sits in the `include/xrpl/tx/transactors/dex/` module alongside AMM transactors and `OfferCancel`, representing the core order-book primitive of the ledger. Its job is deceptively complex: it must validate an offer's fields, optionally cancel an existing offer, attempt to immediately cross against resting orders via the payment engine, and — if any unfilled amount remains — write a new offer ledger entry into the appropriate order book directory. + +## Transactor Lifecycle Hooks + +The class follows the standard three-phase pipeline mandated by the `Transactor` base class: + +**`checkExtraFeatures`** acts as the amendment gate before any field validation. Rather than sprinkling `rules.enabled(featureX)` checks throughout `preflight`, all feature guards live here. It rejects the transaction outright (`temDISABLED`) if `sfDomainID` is present without `featurePermissionedDEX` enabled, or if either amount holds an `MPTIssue` without `featureMPTokensV2`. This keeps `preflight` focused on structural correctness. + +**`getFlagsMask`** returns the bitmask of acceptable transaction flags, and does so dynamically. The `tfOfferCreateMask` is defined assuming PermissionedDEX is active. If it is not, `tfHybrid` is OR-ed into the mask — which signals to the base class infrastructure that this flag is *not* permitted, rejecting it in `preflight0` before `preflight` is even called. + +**`preflight`** validates the structural integrity of the transaction fields: mutual exclusivity of `tfImmediateOrCancel` and `tfFillOrKill`, non-zero expiration if present, non-zero and non-redundant amounts (no XRP-for-XRP, no same-asset IOUs, no negative amounts), and matching issuer/native consistency. It returns `NotTEC` error codes, meaning failures here cause the transaction to be rejected without charging a fee. + +**`preclaim`** runs against a read-only ledger view and checks runtime conditions that can only be evaluated once you can inspect account state: whether involved assets are globally frozen, whether the submitter has sufficient funds, whether a provided cancel sequence is valid relative to the account's current sequence, whether the offer has already expired, whether the submitter is authorized to receive the `TakerPays` asset (`checkAcceptAsset`), and — for domain offers — whether the account is a member of the referenced `PermissionedDEX` domain. + +## Custom Consequences + +Unlike `OfferCancel`, which uses `ConsequencesFactory{Normal}`, `OfferCreate` declares `ConsequencesFactory{Custom}` and provides `makeTxConsequences`. This matters for transaction queuing: the network needs to know the maximum XRP an offer could consume so it can calculate potential balance impacts. The implementation extracts `sfTakerGets`; if that amount is native (XRP), it reports that XRP value as the upper bound. If the offer is for IOUs, the XRP spend is zero. This avoids over-reserving slot capacity in the transaction queue for IOU-only offers. + +## Dual Sandbox Pattern in `doApply` + +`doApply` creates two `Sandbox` views over the apply context: + +- **`sb`** — the primary working sandbox where the full transaction (crossing, offer placement, directory updates) is applied. +- **`sbCancel`** — a secondary sandbox used only for minimal cleanup: stale or expired offers encountered during crossing are removed here too, so they are purged even if the full offer cannot be placed. + +`applyGuts` receives both and returns `{TER, bool}`. The boolean signals which sandbox to commit: `true` commits `sb` (the full result); `false` — used only for `tfFillOrKill` offers that couldn't fully cross — commits `sbCancel` instead. This ensures that when a Fill-or-Kill offer is killed, the ledger still benefits from the housekeeping work done during crossing (removing expired offers) without writing any new offer state. + +## Offer Crossing via `flowCross` + +The most architecturally significant decision in `OfferCreate` is that it does **not** contain its own offer-crossing loop. Instead, `flowCross` delegates entirely to the payment engine's `flow()` function — the same function used by `Payment` transactions. This is intentional: the payment path-finding code already knows how to walk order book directories, handle partial fills, apply gateway transfer rates, and enforce quality thresholds. + +To integrate with `flow()`, `flowCross` inverts `TakerPays` and `TakerGets` (because from the crossing perspective, the offer creator is acting as a taker), computes a quality threshold to enforce the passive flag, and — for IOU-to-IOU offers — injects an XRP intermediate path to enable crossing through two separate books (IOU→XRP→IOU). For `tfSell` offers it passes `STAmount::cMaxNative` or `cMaxValue/2` as the delivery limit, signalling that the taker will accept any amount of the `Gets` asset. + +After `flow()` returns, `flowCross` calculates the residual offer amount (what remains to be placed on the book) and adjusts it while preserving the original offer quality (exchange rate). Gateway transfer rates are factored out when computing the non-gateway-consumed amount, so the residual offer accurately reflects the remaining principal. + +## `checkAcceptAsset` — Authorization Verification + +This static helper determines whether the submitting account is permitted to hold the asset it would receive from a crossing. It handles three cases distinctly: + +- **XRP**: asserted invalid at entry (XRP never needs an accept check). +- **IOU with `lsfRequireAuth`**: verifies that a trust line exists and carries the appropriate `lsfLowAuth`/`lsfHighAuth` flag based on the canonical `>` ordering of the account IDs. Also checks `lsfLowDeepFreeze`/`lsfHighDeepFreeze` on any existing trust line. +- **MPT**: delegates to `requireAuth` with `WeakAuth` semantics, meaning an `MPToken` entry need not already exist — it will be created on first receipt. + +## Hybrid Offers and `applyHybrid` + +The `applyHybrid` method supports the `tfHybrid` flag introduced with PermissionedDEX. A hybrid offer belongs to a domain but is simultaneously indexed in the open order book, allowing it to be crossed by either domain participants or open-market takers. Mechanically, `applyHybrid` sets `lsfHybrid` on the offer ledger entry, creates a second book directory entry (without the domain ID) for the open book, and stores the additional directory reference in `sfAdditionalBooks` on the offer itself. This dual-indexing means the order book walk from the open side can find and consume hybrid domain offers. + +## Relationship to Sibling Transactors + +Compared to `OfferCancel`, `OfferCreate` is dramatically more involved: `OfferCancel` has no custom consequences, no crossing, and no hybrid logic. The AMM transactors in the same directory (`AMMCreate`, `AMMDeposit`, etc.) serve the automated market maker half of the DEX, while `OfferCreate` and `OfferCancel` serve the classic central-limit-order-book half. The two subsystems share the same `PaymentSandbox` and `flow()` infrastructure for actual asset movement, keeping the execution model consistent across both DEX mechanisms. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/did/DIDDelete.h.ai.json b/include/xrpl/tx/transactors/did/DIDDelete.h.ai.json new file mode 100644 index 0000000000..3777952c00 --- /dev/null +++ b/include/xrpl/tx/transactors/did/DIDDelete.h.ai.json @@ -0,0 +1,98 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 16, + "name": "ctx" + }, + { + "lineno": 19, + "name": "ctx" + }, + { + "lineno": 19, + "name": "sleKeylet" + }, + { + "lineno": 19, + "name": "owner" + }, + { + "lineno": 23, + "name": "view" + }, + { + "lineno": 23, + "name": "sle" + }, + { + "lineno": 23, + "name": "owner" + }, + { + "lineno": 23, + "name": "j" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 8, + "name": "DIDDelete" + } + ], + "description": "Defines the DIDDelete transaction class for deleting decentralized identifiers (DIDs) in the XRPL ledger, including its interface and related static/instance methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/did/DIDDelete.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "DIDDelete" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "preflight" + }, + { + "args": [ + "ApplyContext& ctx", + "Keylet sleKeylet", + "AccountID const owner" + ], + "lineno": 19, + "name": "deleteSLE" + }, + { + "args": [ + "ApplyView& view", + "std::shared_ptr sle", + "AccountID const owner", + "beast::Journal j" + ], + "lineno": 23, + "name": "deleteSLE" + }, + { + "args": [], + "lineno": 27, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/did/DIDDelete.h.ai.md b/include/xrpl/tx/transactors/did/DIDDelete.h.ai.md new file mode 100644 index 0000000000..c94e64c505 --- /dev/null +++ b/include/xrpl/tx/transactors/did/DIDDelete.h.ai.md @@ -0,0 +1,37 @@ +# `DIDDelete.h` — DID Deletion Transactor + +`DIDDelete` is the XRPL transactor responsible for processing `ttDID_DELETE` (type code 50) transactions, which remove a Decentralized Identifier (DID) ledger object previously created via `DIDSet`. It lives in the `did/` subdirectory alongside its write-side counterpart `DIDSet`, and is gated behind the `featureDID` amendment as declared in the auto-generated protocol header. + +## Inheritance and Lifecycle + +`DIDDelete` inherits `Transactor` and participates in the standard three-phase transactor lifecycle managed by `invokePreflight`: + +1. **`preflight`** (static) — validates the raw transaction before any ledger state is touched. +2. **`doApply`** (virtual override) — applies the state mutation once the transaction is accepted. + +The class declares `ConsequencesFactory{Normal}`, which tells the fee-queue machinery that this transaction has non-blocking consequences — it does not prevent other transactions from the same account from being processed behind it. + +## Why `preflight` Is Trivial + +`DIDDelete::preflight` returns `tesSUCCESS` unconditionally. This is intentional: a DID deletion carries no transaction-specific fields beyond the universal base fields (fee, sequence, signing key). All meaningful validation — account existence, sequence number, fee sufficiency, signature correctness, and amendment enablement — is handled by the base `invokePreflight` machinery and does not need to be repeated here. Attempting to do extra work in `preflight` for a no-field transaction would only add noise. + +## The `deleteSLE` Overload Pair + +The architectural heart of this header is the two static overloads of `deleteSLE`. This separation is a deliberate design for **reusability**. + +The first overload — `deleteSLE(ApplyContext& ctx, Keylet sleKeylet, AccountID const owner)` — is the convenience wrapper used by `doApply`. It peeks the DID ledger object by keylet through the apply context and delegates to the second overload. Returning `tecNO_ENTRY` if the object doesn't exist is the idiomatic XRPL response for a fee-claiming failure: the transaction consumed a sequence number and fee, but found nothing to delete. + +The second overload — `deleteSLE(ApplyView& view, std::shared_ptr sle, AccountID const owner, beast::Journal j)` — operates purely at the `ApplyView` level. It does three things in sequence: +1. Removes the DID SLE from the owner's directory via `dirRemove`, decrementing the account's directory entry. +2. Fetches the owner account's SLE, calls `adjustOwnerCount` with `-1` to decrement the reserve-adjusted owner count, and updates the account object. +3. Erases the DID SLE from the ledger. + +By accepting `ApplyView&` and a pre-resolved `std::shared_ptr` rather than an `ApplyContext`, this overload is callable from any transactor that has access to a mutable view — most notably `AccountDelete`. When an account is deleted, any owned DID must be cleaned up as a prerequisite; `AccountDelete` can call `DIDDelete::deleteSLE` directly without constructing a `DIDDelete` transactor or synthesizing a transaction context. + +## Error Handling + +Three failure modes are handled: `tecNO_ENTRY` when the DID object is absent (first overload, fee-claiming), `tefBAD_LEDGER` when `dirRemove` fails (annotated `LCOV_EXCL` because it signals an internal ledger inconsistency that should never occur in practice), and `tecINTERNAL` when the owner account SLE cannot be found (similarly `LCOV_EXCL`). The distinction between `tec` and `tef` codes matters: `tef` failures abort and do not claim a fee, while `tec` failures claim the fee but do not apply the mutation. + +## `doApply` + +`doApply` is one line: `return deleteSLE(ctx_, keylet::did(account_), account_)`. The DID keylet is deterministically derived from the submitting account's `AccountID`, so no field lookup is required — a DIDDelete transaction is wholly identified by its sender, and each account can hold at most one DID object. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/did/DIDSet.h.ai.json b/include/xrpl/tx/transactors/did/DIDSet.h.ai.json new file mode 100644 index 0000000000..c996ac4e20 --- /dev/null +++ b/include/xrpl/tx/transactors/did/DIDSet.h.ai.json @@ -0,0 +1,40 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "DIDSet" + } + ], + "description": "Defines the DIDSet transactor class for handling DID set transactions in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/did/DIDSet.h", + "functions": [ + { + "args": [ + "ctx" + ], + "lineno": 13, + "name": "preflight" + }, + { + "args": [], + "lineno": 16, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/did/DIDSet.h.ai.md b/include/xrpl/tx/transactors/did/DIDSet.h.ai.md new file mode 100644 index 0000000000..89b664d0f6 --- /dev/null +++ b/include/xrpl/tx/transactors/did/DIDSet.h.ai.md @@ -0,0 +1,27 @@ +# `DIDSet.h` — DID Set Transactor Declaration + +## Role in the System + +`DIDSet.h` declares the `DIDSet` transactor, which handles `DIDSet` transactions on the XRP Ledger. These transactions create or update a Decentralized Identifier (DID) ledger object owned by an account, conforming to the W3C DID v1.0 specification. The file is one of two transactors in the `did/` subdirectory — the other being `DIDDelete` — and both follow the standard two-phase transactor pattern used throughout the XRPL codebase. + +## Class Structure and Inheritance + +`DIDSet` inherits from `Transactor`, the abstract base class for all transaction types in the ledger engine. This inheritance grants access to the apply pipeline infrastructure: `ApplyContext`, ledger views, account state, fee handling, and signature verification. The constructor simply forwards the `ApplyContext&` reference to the base class, consistent with every other transactor — the context carries everything needed for execution. + +The `ConsequencesFactory` is set to `Normal`, meaning the transaction's consequences (for the purpose of local fee escalation and transaction queue management) are computed using the standard path. Transactors that block other transactions from the same account would use `Blocker` instead; `Normal` signals that this transaction neither invalidates nor dominates other pending transactions from the same sender. + +## Two-Phase Execution Contract + +Like all transactors, `DIDSet` exposes exactly two user-defined entry points: + +**`preflight(PreflightContext const& ctx)`** — a static method invoked before the transaction touches ledger state. It runs on the raw transaction fields without access to a mutable view. The implementation in `DIDSet.cpp` enforces three rules: at least one of `sfURI`, `sfDIDDocument`, or `sfData` must be present; all three cannot be simultaneously present but empty (which would create a semantically vacuous DID); and no individual field can exceed its maximum byte length (`maxDIDURILength`, `maxDIDDocumentLength`, `maxDIDDataLength`). Violations return `temEMPTY_DID` or `temMALFORMED`, which are `NotTEC` codes that cause the transaction to be rejected without claiming a fee. + +**`doApply()`** — the virtual method that performs the actual ledger mutation, called after fee deduction and sequence number consumption. It distinguishes two code paths: if a DID SLE already exists for the account, it performs a field-level update (an absent field in the transaction is a no-op; an empty field removes the attribute from the existing object; a non-empty field replaces it). If no DID SLE exists, it creates a fresh one and calls the file-local `addSLE()` helper, which checks owner reserve availability, inserts the object into the ledger, adds it to the account's owner directory, and increments the owner count. + +## Design Decisions + +The `preflight` / `doApply` split is a deliberate separation of concerns central to the XRPL transaction pipeline. `preflight` is cheap, stateless, and can be run on any node that sees the transaction before it enters a ledger. `doApply` is authoritative and runs only once per ledger close under consensus. Placing field-length and presence checks in `preflight` means malformed DID transactions are rejected early, before they consume ledger resources. + +The class itself has no data members beyond what `Transactor` provides — all state for the DID operation flows through `ctx_` (the `ApplyContext`), keeping the transactor stateless between construction and the call to `operator()()`. This is consistent across all transactor types and avoids any ambiguity about object lifetime relative to ledger apply batches. + +Compared to `DIDDelete`, which adds a static `deleteSLE()` helper used by cleanup paths elsewhere in the system, `DIDSet` exposes no additional static utilities. The upsert logic in `doApply()` is self-contained and not shared, which is appropriate given that creating or updating a DID object has no analogous reuse point in the codebase. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/escrow/EscrowCancel.h.ai.json b/include/xrpl/tx/transactors/escrow/EscrowCancel.h.ai.json new file mode 100644 index 0000000000..15566be3e2 --- /dev/null +++ b/include/xrpl/tx/transactors/escrow/EscrowCancel.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "EscrowCancel" + } + ], + "description": "Defines the EscrowCancel transaction transactor for the XRPL, including its interface and core methods for transaction processing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/escrow/EscrowCancel.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "EscrowCancel" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 21, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/escrow/EscrowCancel.h.ai.md b/include/xrpl/tx/transactors/escrow/EscrowCancel.h.ai.md new file mode 100644 index 0000000000..3f73deb0bf --- /dev/null +++ b/include/xrpl/tx/transactors/escrow/EscrowCancel.h.ai.md @@ -0,0 +1,31 @@ +# `EscrowCancel.h` — Escrow Cancellation Transactor + +## Role in the System + +`EscrowCancel` implements the transactor responsible for returning locked funds to their original owner when an escrow has passed its expiry time. It is the "reclaim" counterpart to `EscrowFinish` in the escrow lifecycle: where `EscrowFinish` releases funds to the intended recipient when conditions are met, `EscrowCancel` unwinds the escrow entirely and restores the held amount to the account that created it. + +The file is a minimal header declaring the class interface — the substantive logic lives in `EscrowCancel.cpp` — but the structural choices expressed here are architecturally meaningful. + +## Relationship to `Transactor` + +`EscrowCancel` inherits from `Transactor`, the base class for every transaction type processed by the XRPL node. The `Transactor` framework enforces a three-phase pipeline: `preflight` (stateless sanity checks on the raw transaction fields), `preclaim` (read-only checks against the current ledger state), and `doApply` (the state-mutating application). Each phase is invoked by the framework through template-based static dispatch via `invokePreflight`, meaning the compiler resolves the call chain at compile time without virtual dispatch overhead. + +## `ConsequencesFactory` Classification + +The `ConsequencesFactory` is declared as `Normal`, distinguishing it from `EscrowCreate`, which uses `Custom`. This tells the framework that `EscrowCancel` follows the standard fee-consumption model for transaction consequences: it can claim a fee, but it does not tie up additional XRP beyond that fee. `EscrowCreate` needs a `Custom` factory because it locks up an arbitrary amount of XRP or tokens, requiring a bespoke `makeTxConsequences` calculation. Cancelling an escrow unlocks funds rather than locking them, so the standard model applies. + +## The Three-Phase Interface + +**`preflight`** takes a `PreflightContext const&` and returns `NotTEC`. The implementation simply returns `tesSUCCESS` — there are no field-level constraints to validate beyond what the framework's `preflight1`/`preflight2` wrappers already check (fee, flags, signatures). Unlike `EscrowFinish`, which overrides `checkExtraFeatures` and `preflightSigValidated` for crypto-condition and partial-signature logic, `EscrowCancel` has no such domain-specific preflight concerns. + +**`preclaim`** takes a `PreclaimContext const&` and returns `TER`. Here the read-only ledger state is consulted. Under the `featureTokenEscrow` amendment, the method validates that the escrow object exists, and if the escrowed amount is non-XRP, it dispatches to one of two template specialisations — `escrowCancelPreclaimHelper` for IOU/trust-line assets or `escrowCancelPreclaimHelper` for MPT (Multi-Purpose Token) assets. Both specialisations guard against `requireAuth` violations: if the original escrow account is no longer authorized to hold the asset (e.g., the issuer enabled authorization requirements after the escrow was created), cancellation is blocked. Without the `featureTokenEscrow` amendment, preclaim unconditionally succeeds. + +**`doApply`** performs the mutating work. It locates the escrow object by `{sfOwner, sfOfferSequence}`, enforces that `sfCancelAfter` is set and has been passed (returning `tecNO_PERMISSION` otherwise), then removes the escrow from the owner directory and optionally from the recipient's owner directory if `sfDestinationNode` is present. For XRP escrows, the locked balance is added directly back to the owner's account balance. For non-XRP amounts (gated on `featureTokenEscrow`), `escrowUnlockApplyHelper` handles the token-type-specific transfer back to the owner, and an additional directory removal step purges the entry from the issuer's owner directory via `sfIssuerNode`. Finally, `adjustOwnerCount` decrements the escrow owner's reserve count and the escrow object is erased from the ledger. + +## Design Observations + +The `ConsequencesFactory` constant and the three static methods form a compile-time interface contract that the `Transactor` framework expects via name-hiding rather than virtual dispatch. The comment in `Transactor.h` makes this explicit: "these are not really virtual and so don't have the compiler-time protection that comes with it." The discipline required is that derived classes must match the exact signatures — the header enforces this implicitly by declaring them. + +The `preflight` returning unconditional success is a deliberate design choice: there is nothing about the `EscrowCancel` transaction format itself (beyond universal field rules) that can be checked without ledger state. All the meaningful constraints — does the escrow exist, has the cancel time passed, is authorization still valid — require state access and therefore belong in `preclaim` or `doApply`. + +The `preclaim` / `doApply` split for the existence check is also intentional. `preclaim` runs against a read-only view and short-circuits fee collection only when there is definitively no target. `doApply` re-checks existence with a writable `peek` because between the two phases, in theory, ledger state may differ. Finding no escrow in `doApply` after `featureTokenEscrow` is active returns `tecINTERNAL` (marked `LCOV_EXCL_LINE` indicating it is considered unreachable in practice), while the legacy path returns `tecNO_TARGET` for backward compatibility. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/escrow/EscrowCreate.h.ai.json b/include/xrpl/tx/transactors/escrow/EscrowCreate.h.ai.json new file mode 100644 index 0000000000..041455958c --- /dev/null +++ b/include/xrpl/tx/transactors/escrow/EscrowCreate.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "EscrowCreate" + } + ], + "description": "Defines the EscrowCreate transactor class for handling the creation of escrow transactions in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/escrow/EscrowCreate.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "EscrowCreate" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/escrow/EscrowCreate.h.ai.md b/include/xrpl/tx/transactors/escrow/EscrowCreate.h.ai.md new file mode 100644 index 0000000000..7f85ea53e8 --- /dev/null +++ b/include/xrpl/tx/transactors/escrow/EscrowCreate.h.ai.md @@ -0,0 +1,54 @@ +# `EscrowCreate.h` — Escrow Creation Transactor Declaration + +`EscrowCreate` is the transactor class responsible for processing the ledger transaction that locks XRP or tokens into an escrow object. It lives within the `escrow` subdirectory alongside its two counterparts — `EscrowCancel` and `EscrowFinish` — which together form the complete escrow lifecycle. The header itself is thin: a class declaration that inherits from `Transactor` and advertises the four entry points the ledger framework calls at different stages of transaction processing. + +## Role in the Transactor Framework + +Every transaction type in the XRPL is represented by a subclass of `Transactor`. The framework drives these subclasses through a strict three-phase pipeline: + +1. **`preflight`** — stateless validation using only the transaction fields and active rules. +2. **`preclaim`** — read-only ledger inspection after a fee has been provisionally claimed. +3. **`doApply`** — the mutating phase that writes ledger state changes. + +`EscrowCreate` participates in all three phases and additionally overrides the consequences computation with `ConsequencesFactory{Custom}`. This is the critical architectural difference from `EscrowCancel` and `EscrowFinish`, both of which use `ConsequencesFactory{Normal}`. The `Custom` factory tells the framework to call `makeTxConsequences()` rather than rely on a generic cost model. This is necessary because the potential XRP impact of an `EscrowCreate` scales with the `sfAmount` field: for XRP escrows the consequences include the locked principal, whereas for token escrows (IOU or MPT) the XRP impact is `beast::zero` since no XRP leaves the account. The framework uses this information to reason about fee-claiming eligibility before any ledger state is read. + +## The `preflight` Phase + +`preflight` validates the transaction without touching the ledger. For the amount field it branches on asset type: + +- **XRP**: must be strictly positive. +- **IOU trustline amounts** (`Issue`): requires the `featureTokenEscrow` amendment and rejects native or non-positive values, as well as the bad-currency sentinel. +- **MPT amounts** (`MPTIssue`): additionally requires `featureMPTokensV1`. + +Beyond amounts, three timing invariants are enforced. At least one of `sfCancelAfter` or `sfFinishAfter` must be present. When both are given, the cancel time must be strictly after the finish time, preventing a logically inverted escrow. Finally, if no `sfFinishAfter` is provided, a crypto-condition (`sfCondition`) must exist — this prevents the pathological case of an escrow that can be finished immediately with no meaningful unlock mechanism. If a condition is present its wire encoding is deserialized and rejected as `temMALFORMED` if parsing fails. + +## The `preclaim` Phase + +`preclaim` adds ledger-aware checks that require a `ReadView`. It first confirms the destination account exists and is not a pseudo-account. Pseudo-accounts are explicitly blocked — this check is unconditional rather than amendment-gated because the writes that create pseudo-account discriminator fields are themselves amendment-gated, so the behaviour is always self-consistent. + +For non-XRP amounts, the check is dispatched to template specializations via `std::visit` over the `Asset` variant: + +- **IOU escrow** (`escrowCreatePreclaimHelper`): verifies the issuer has `lsfAllowTrustLineLocking` set, that both source and destination accounts have a trust line, that neither is frozen, that both are authorized if the issuer requires auth, and that the sender has sufficient spendable balance. An additional precision-loss check (`canAdd`) guards against IOU arithmetic edge cases. +- **MPT escrow** (`escrowCreatePreclaimHelper`): verifies the issuance exists, that `lsfMPTCanEscrow` is set on the issuance, that the sender holds an `MPToken` object, and that neither side is frozen or lock-restricted. The transferability check (`canTransfer`) is also applied. + +## The `doApply` Phase + +`doApply` is where the escrow ledger object (`SLE`) is created and balances are moved. It begins by re-checking whether the `sfCancelAfter` and `sfFinishAfter` values have already passed relative to the ledger's `parentCloseTime`. This is not redundant: time advances between preflight and apply, and an escrow that expires between these two phases must be rejected here rather than creating an immediately-expired object. + +The function then checks XRP reserve requirements. The sender's account must hold enough XRP to cover the incremented owner-count reserve plus, for XRP escrows, the locked principal itself. This single-pass reserve check is intentional — token escrows debit the token balance through a separate helper, not the XRP balance. + +The `SLE` is constructed from the transaction fields and inserted into the ledger. The `fixIncludeKeyletFields` amendment gates whether `sfSequence` is copied into the escrow object (needed for `keylet` derivation by off-ledger clients). + +For token escrows, the transfer rate is snapshotted at creation time and stored in `sfTransferRate`. This is architecturally important: it freezes the fee that will apply at `EscrowFinish` time, preventing the issuer from changing the rate between escrow creation and execution. + +After inserting the SLE, `doApply` updates three owner directories: + +- Always adds the escrow to the sender's `ownerDir`. +- Adds it to the destination's `ownerDir` unless this is a self-escrow. +- For IOU escrows only, adds it to the issuer's `ownerDir` so the issuer can enumerate all locked balances. MPT escrows skip this because the MPT issuance object already tracks its locked balance directly. + +Finally, the sender's balance is debited — XRP directly from `sfBalance`, tokens via `escrowLockApplyHelper` which for IOU performs a `directSendNoFee` to the issuer and for MPT calls `lockEscrowMPT` to atomically update the locked field on the `MPToken` object. + +## Relationship to Sibling Transactors + +The class interface mirrors `EscrowCancel` and `EscrowFinish` closely, but neither sibling defines `makeTxConsequences`. `EscrowFinish` additionally overrides `checkExtraFeatures`, `preflightSigValidated`, and `calculateBaseFee` — the latter to charge an elevated fee proportional to the size of the fulfillment blob when completing a crypto-conditional escrow. `EscrowCreate` has no analogous fee scaling: its cost is driven entirely by the XRP amount reserved, which is why the `Custom` factory and its `makeTxConsequences` override exist. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/escrow/EscrowFinish.h.ai.json b/include/xrpl/tx/transactors/escrow/EscrowFinish.h.ai.json new file mode 100644 index 0000000000..03e04b3c74 --- /dev/null +++ b/include/xrpl/tx/transactors/escrow/EscrowFinish.h.ai.json @@ -0,0 +1,71 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "EscrowFinish" + } + ], + "description": "Defines the EscrowFinish transaction transactor class for the XRPL, including its interface for preflight checks, fee calculation, and application logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/escrow/EscrowFinish.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "EscrowFinish" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "preflightSigValidated" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 24, + "name": "calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 27, + "name": "preclaim" + }, + { + "args": [], + "lineno": 29, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/escrow/EscrowFinish.h.ai.md b/include/xrpl/tx/transactors/escrow/EscrowFinish.h.ai.md new file mode 100644 index 0000000000..ee066338b7 --- /dev/null +++ b/include/xrpl/tx/transactors/escrow/EscrowFinish.h.ai.md @@ -0,0 +1,64 @@ +# `EscrowFinish.h` — Transactor for Releasing Held Escrow Funds + +`EscrowFinish` is one of three escrow transactors in the XRPL, alongside `EscrowCreate` and `EscrowCancel`. Where `EscrowCreate` locks funds into an on-ledger escrow object and `EscrowCancel` returns them to the originator when conditions are not met, `EscrowFinish` is the success path: it verifies that release conditions are satisfied and transfers the locked funds to the intended recipient. + +## Role in the Transactor Hierarchy + +Like all transactors, `EscrowFinish` inherits from `Transactor` and participates in the four-phase processing pipeline: `preflight` → `preflightSigValidated` → `preclaim` → `doApply`. The base class drives this pipeline through `invokePreflight` and the `operator()()` call chain, using compile-time name hiding rather than virtual dispatch for the static methods. `EscrowFinish` fills in every hook of this pipeline, making it the most feature-rich of the three escrow transactors. + +The `ConsequencesFactory` is set to `Normal`, meaning the transaction is handled with standard consequence logic. This contrasts with `EscrowCreate`, which sets `Custom` and provides a `makeTxConsequences` factory because locking up funds affects fee-processing semantics differently than releasing them. + +## Amendment Gating via `checkExtraFeatures` + +`checkExtraFeatures` is an optional override that `EscrowFinish` uses to gate one specific capability: including `sfCredentialIDs` in the transaction is only permitted when the `featureCredentials` amendment is active. The base `Transactor` implementation always returns `true`; returning `false` here causes `invokePreflight` to short-circuit with `temDISABLED` before any other validation runs. This pattern keeps amendment gating cleanly separated from field-level validation in `preflight`. + +## Expensive Crypto-Condition Work in `preflightSigValidated` + +The most distinctive feature of `EscrowFinish` compared to its siblings is `preflightSigValidated`. Escrow supports PREIMAGE-SHA-256 crypto-conditions (from the Interledger Crypto-Conditions specification): when an escrow is created with a condition, finishing it requires supplying a fulfillment whose SHA-256 hash matches that condition. + +Validating a fulfillment is computationally expensive. Two design choices address this: + +1. **Post-signature placement**: `preflightSigValidated` runs after `preflight2` has verified the transaction's cryptographic signature. This ensures that condition validation is never performed on unauthenticated input — an attacker cannot force the node to do expensive crypto work by submitting a badly-signed transaction. + +2. **Hash router caching**: The result of condition validation is stored in the node's `HashRouter` using two private flag bits, `SF_CF_INVALID` and `SF_CF_VALID`. Once set against the transaction ID, subsequent phases of the pipeline (`doApply` in particular) read the cached verdict rather than re-running the validation. If the cache entry has expired by the time `doApply` runs (unlikely but possible), `doApply` repeats the check and re-caches it. Notably, a failed condition check does not cause `preflightSigValidated` to return an error — the result is cached for `doApply` to act on, keeping preflight non-blocking for broadcasting purposes. + +`preflightSigValidated` also validates credential fields via `credentials::checkFields` when `sfCredentialIDs` is present. + +## Fee Scaling for Fulfillments + +`calculateBaseFee` overrides the base implementation to add a surcharge when a fulfillment is attached: + +``` +extraFee = base_fee * (32 + fulfillment_size / 16) +``` + +This is intentional economic design: larger fulfillments impose more validation cost on the network, and the fee schedule ensures that cost is borne by the submitter. Without this surcharge, attackers could submit transactions with large fulfillments at standard fees, creating a denial-of-service vector for validators. + +## `preclaim`: Asset Eligibility Checks + +`preclaim` handles two independent concerns guarded by separate amendments. When `featureCredentials` is enabled, it validates credential authorization via `credentials::valid`. When `featureTokenEscrow` is enabled (the amendment allowing non-XRP assets to be held in escrow), it reads the escrow ledger object and, for non-XRP amounts, dispatches through a `std::visit` to one of two template specializations: + +- **`Issue` (IOU)**: Checks `requireAuth` authorization and verifies the destination is not deep-frozen by the issuer. +- **`MPTIssue` (MPToken)**: Checks that the issuance object exists, verifies `requireAuth` (using `WeakAuth` semantics), and checks for MPT-level freeze. + +The base `Transactor::preclaim` returns `tesSUCCESS` and does nothing; `EscrowFinish` overrides it because the asset eligibility checks must happen before the transaction is committed to avoid charging fees for certain precondition failures. + +## `doApply`: State Mutation + +The application phase enforces the temporal and cryptographic release conditions, then performs the ledger mutations: + +**Time window enforcement**: The ledger's `parentCloseTime` is compared against the optional `sfFinishAfter` (too early) and `sfCancelAfter` (too late) fields on the escrow object. Both checks returning `tecNO_PERMISSION` means the transaction is well-formed but timing prevents execution. + +**Condition re-check**: The hash router cache is consulted. If `SF_CF_INVALID` is set, execution fails with `tecCRYPTOCONDITION_ERROR`. Three additional semantic checks follow: a condition attached to the transaction must match one stored in the escrow object; a transaction must not supply a condition if none was recorded at creation; if a condition was recorded, the exact same bytes must be presented. + +**Deposit pre-auth**: `verifyDepositPreauth` ensures that if the destination account requires deposit authorization, the originating account is on the allow-list. + +**Directory cleanup**: The escrow object is removed from the originating account's owner directory and, if present, from the recipient's owner directory. For non-XRP escrows, it is also removed from the issuer's owner directory. Failure to remove from any directory returns `tefBAD_LEDGER`, flagging internal ledger corruption. + +**Fund transfer**: For XRP, the destination account's balance is incremented directly. For non-XRP assets, `escrowUnlockApplyHelper` handles transfer rate application, using `parityRate` when no locked transfer rate was recorded at escrow creation. + +**Owner count**: The originating account's owner count is decremented by one, reflecting that the escrow object no longer occupies reservation space. + +## Relationship to Sibling Transactors + +`EscrowFinish.h` is structurally the most complex of the three escrow headers. `EscrowCreate.h` is unique in its `Custom` consequences factory and the `makeTxConsequences` method it provides; `EscrowCancel.h` is the simplest, with only the standard `preflight`/`preclaim`/`doApply` trio. `EscrowFinish` sits between them in complexity, distinguished by its custom fee calculation, the post-signature crypto-condition pipeline hook, and multi-asset `preclaim` logic — all driven by the fact that this is the only path that both validates external cryptographic proof and actually moves locked value to its destination. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LendingHelpers.h.ai.json b/include/xrpl/tx/transactors/lending/LendingHelpers.h.ai.json new file mode 100644 index 0000000000..1054c93a36 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LendingHelpers.h.ai.json @@ -0,0 +1,650 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 13, + "name": "interestRate" + }, + { + "lineno": 13, + "name": "paymentInterval" + }, + { + "lineno": 17, + "name": "asset" + }, + { + "lineno": 17, + "name": "periodicPayment" + }, + { + "lineno": 17, + "name": "scale" + }, + { + "lineno": 54, + "name": "other" + }, + { + "lineno": 57, + "name": "other" + }, + { + "lineno": 109, + "name": "value" + }, + { + "lineno": 109, + "name": "adjustment" + }, + { + "lineno": 109, + "name": "asset" + }, + { + "lineno": 109, + "name": "vaultScale" + }, + { + "lineno": 119, + "name": "vaultSle" + }, + { + "lineno": 127, + "name": "vaultAsset" + }, + { + "lineno": 127, + "name": "principalRequested" + }, + { + "lineno": 127, + "name": "expectInterest" + }, + { + "lineno": 127, + "name": "paymentTotal" + }, + { + "lineno": 127, + "name": "properties" + }, + { + "lineno": 127, + "name": "j" + }, + { + "lineno": 135, + "name": "periodicPayment" + }, + { + "lineno": 135, + "name": "periodicRate" + }, + { + "lineno": 135, + "name": "paymentRemaining" + }, + { + "lineno": 135, + "name": "managementFeeRate" + }, + { + "lineno": 140, + "name": "totalValueOutstanding" + }, + { + "lineno": 140, + "name": "principalOutstanding" + }, + { + "lineno": 140, + "name": "managementFeeOutstanding" + }, + { + "lineno": 146, + "name": "loan" + }, + { + "lineno": 150, + "name": "asset" + }, + { + "lineno": 150, + "name": "interest" + }, + { + "lineno": 150, + "name": "managementFeeRate" + }, + { + "lineno": 150, + "name": "scale" + }, + { + "lineno": 156, + "name": "theoreticalPrincipalOutstanding" + }, + { + "lineno": 156, + "name": "periodicRate" + }, + { + "lineno": 156, + "name": "parentCloseTime" + }, + { + "lineno": 156, + "name": "paymentInterval" + }, + { + "lineno": 156, + "name": "prevPaymentDate" + }, + { + "lineno": 156, + "name": "startDate" + }, + { + "lineno": 156, + "name": "closeInterestRate" + }, + { + "lineno": 221, + "name": "p" + }, + { + "lineno": 221, + "name": "fee" + }, + { + "lineno": 221, + "name": "interest" + }, + { + "lineno": 332, + "name": "lhs" + }, + { + "lineno": 332, + "name": "rhs" + }, + { + "lineno": 335, + "name": "lhs" + }, + { + "lineno": 335, + "name": "rhs" + }, + { + "lineno": 338, + "name": "lhs" + }, + { + "lineno": 338, + "name": "rhs" + }, + { + "lineno": 341, + "name": "asset" + }, + { + "lineno": 341, + "name": "principalOutstanding" + }, + { + "lineno": 341, + "name": "interestRate" + }, + { + "lineno": 341, + "name": "paymentInterval" + }, + { + "lineno": 341, + "name": "paymentsRemaining" + }, + { + "lineno": 341, + "name": "managementFeeRate" + }, + { + "lineno": 341, + "name": "minimumScale" + }, + { + "lineno": 349, + "name": "asset" + }, + { + "lineno": 349, + "name": "principalOutstanding" + }, + { + "lineno": 349, + "name": "periodicRate" + }, + { + "lineno": 349, + "name": "paymentsRemaining" + }, + { + "lineno": 349, + "name": "managementFeeRate" + }, + { + "lineno": 349, + "name": "minimumScale" + }, + { + "lineno": 355, + "name": "asset" + }, + { + "lineno": 355, + "name": "value" + }, + { + "lineno": 355, + "name": "scale" + }, + { + "lineno": 366, + "name": "asset" + }, + { + "lineno": 366, + "name": "view" + }, + { + "lineno": 366, + "name": "loan" + }, + { + "lineno": 366, + "name": "brokerSle" + }, + { + "lineno": 366, + "name": "amount" + }, + { + "lineno": 366, + "name": "paymentType" + }, + { + "lineno": 366, + "name": "j" + } + ], + "classes": [ + { + "args": [], + "lineno": 34, + "name": "LoanPaymentParts" + }, + { + "args": [], + "lineno": 62, + "name": "LoanState" + }, + { + "args": [], + "lineno": 93, + "name": "LoanProperties" + }, + { + "args": [], + "lineno": 181, + "name": "PaymentComponents" + }, + { + "args": [ + "p", + "fee", + "interest" + ], + "lineno": 221, + "name": "ExtendedPaymentComponents" + }, + { + "args": [], + "lineno": 247, + "name": "LoanStateDeltas" + } + ], + "description": "This file defines data structures and functions for handling loan payment processing, amortization, and state management in an XRPL-based lending protocol, including calculations for interest, fees, overpayments, and loan state deltas.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LendingHelpers.h", + "functions": [ + { + "args": [ + "ctx" + ], + "lineno": 10, + "name": "checkLendingProtocolDependencies" + }, + { + "args": [ + "interestRate", + "paymentInterval" + ], + "lineno": 13, + "name": "loanPeriodicRate" + }, + { + "args": [ + "asset", + "periodicPayment", + "scale" + ], + "lineno": 17, + "name": "roundPeriodicPayment" + }, + { + "args": [ + "other" + ], + "lineno": 54, + "name": "operator+=" + }, + { + "args": [ + "other" + ], + "lineno": 57, + "name": "operator==" + }, + { + "args": [], + "lineno": 84, + "name": "interestOutstanding" + }, + { + "args": [ + "value", + "adjustment", + "asset", + "vaultScale" + ], + "lineno": 109, + "name": "adjustImpreciseNumber" + }, + { + "args": [ + "vaultSle" + ], + "lineno": 119, + "name": "getAssetsTotalScale" + }, + { + "args": [ + "vaultAsset", + "principalRequested", + "expectInterest", + "paymentTotal", + "properties", + "j" + ], + "lineno": 127, + "name": "checkLoanGuards" + }, + { + "args": [ + "periodicPayment", + "periodicRate", + "paymentRemaining", + "managementFeeRate" + ], + "lineno": 135, + "name": "computeTheoreticalLoanState" + }, + { + "args": [ + "totalValueOutstanding", + "principalOutstanding", + "managementFeeOutstanding" + ], + "lineno": 140, + "name": "constructLoanState" + }, + { + "args": [ + "loan" + ], + "lineno": 146, + "name": "constructRoundedLoanState" + }, + { + "args": [ + "asset", + "interest", + "managementFeeRate", + "scale" + ], + "lineno": 150, + "name": "computeManagementFee" + }, + { + "args": [ + "theoreticalPrincipalOutstanding", + "periodicRate", + "parentCloseTime", + "paymentInterval", + "prevPaymentDate", + "startDate", + "closeInterestRate" + ], + "lineno": 156, + "name": "computeFullPaymentInterest" + }, + { + "args": [], + "lineno": 202, + "name": "trackedInterestPart" + }, + { + "args": [], + "lineno": 259, + "name": "total" + }, + { + "args": [], + "lineno": 264, + "name": "nonNegative" + }, + { + "args": [ + "asset", + "loanScale", + "overpaymentComponents", + "roundedLoanState", + "periodicPayment", + "periodicRate", + "paymentRemaining", + "managementFeeRate", + "j" + ], + "lineno": 270, + "name": "tryOverpayment" + }, + { + "args": [ + "periodicRate", + "paymentsRemaining" + ], + "lineno": 282, + "name": "computeRaisedRate" + }, + { + "args": [ + "periodicRate", + "paymentsRemaining" + ], + "lineno": 285, + "name": "computePaymentFactor" + }, + { + "args": [ + "asset", + "interest", + "managementFeeRate", + "loanScale" + ], + "lineno": 288, + "name": "computeInterestAndFeeParts" + }, + { + "args": [ + "principalOutstanding", + "periodicRate", + "paymentsRemaining" + ], + "lineno": 294, + "name": "loanPeriodicPayment" + }, + { + "args": [ + "periodicPayment", + "periodicRate", + "paymentsRemaining" + ], + "lineno": 298, + "name": "loanPrincipalFromPeriodicPayment" + }, + { + "args": [ + "principalOutstanding", + "lateInterestRate", + "parentCloseTime", + "nextPaymentDueDate" + ], + "lineno": 302, + "name": "loanLatePaymentInterest" + }, + { + "args": [ + "principalOutstanding", + "periodicRate", + "parentCloseTime", + "startDate", + "prevPaymentDate", + "paymentInterval" + ], + "lineno": 308, + "name": "loanAccruedInterest" + }, + { + "args": [ + "asset", + "loanScale", + "overpayment", + "overpaymentInterestRate", + "overpaymentFeeRate", + "managementFeeRate" + ], + "lineno": 315, + "name": "computeOverpaymentComponents" + }, + { + "args": [ + "asset", + "scale", + "totalValueOutstanding", + "principalOutstanding", + "managementFeeOutstanding", + "periodicPayment", + "periodicRate", + "paymentRemaining", + "managementFeeRate" + ], + "lineno": 324, + "name": "computePaymentComponents" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 332, + "name": "operator-" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 335, + "name": "operator-" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 338, + "name": "operator+" + }, + { + "args": [ + "asset", + "principalOutstanding", + "interestRate", + "paymentInterval", + "paymentsRemaining", + "managementFeeRate", + "minimumScale" + ], + "lineno": 341, + "name": "computeLoanProperties" + }, + { + "args": [ + "asset", + "principalOutstanding", + "periodicRate", + "paymentsRemaining", + "managementFeeRate", + "minimumScale" + ], + "lineno": 349, + "name": "computeLoanProperties" + }, + { + "args": [ + "asset", + "value", + "scale" + ], + "lineno": 355, + "name": "isRounded" + }, + { + "args": [ + "asset", + "view", + "loan", + "brokerSle", + "amount", + "paymentType", + "j" + ], + "lineno": 366, + "name": "loanMakePayment" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 173, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LendingHelpers.h.ai.md b/include/xrpl/tx/transactors/lending/LendingHelpers.h.ai.md new file mode 100644 index 0000000000..93eb8d0ad6 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LendingHelpers.h.ai.md @@ -0,0 +1,45 @@ +# `LendingHelpers.h` — Lending Protocol Computation Primitives + +This header is the computational core of the XLS-66 lending protocol. Every transactor in the `lending/` directory — `LoanSet`, `LoanPay`, `LoanDelete`, and the `LoanBroker*` family — either calls these functions directly or depends on structures defined here. The file does not model network behavior or ledger I/O; instead it defines the mathematics of amortized loan payments, the data types that carry that mathematics through the protocol, and a small set of utility functions that translate between ledger objects and those types. + +## Protocol Gating + +`checkLendingProtocolDependencies()` is the single function that gates the entire lending protocol. It checks that the `featureSingleAssetVault` amendment is active and that vault creation is permitted in the current context. Every lending transactor's `checkExtraFeatures()` calls this function, so adding a new amendment requirement here affects all transactions atomically. + +## The Data Type Hierarchy + +Four structures form a layered view of a loan's financial state, each at a different level of abstraction: + +**`LoanState`** is the most fundamental: it holds the four numbers that describe where a loan stands — `valueOutstanding`, `principalOutstanding`, `interestDue`, and `managementFeeDue`. The key invariant is that `interestDue + managementFeeDue == valueOutstanding - principalOutstanding`; this is enforced at runtime by `XRPL_ASSERT_PARTS` inside `interestOutstanding()`. The `managementFeeDue` field tracks the broker's share of the interest, kept separate so the two recipients (vault and broker) can be paid the correct portions. + +**`LoanProperties`** bundles everything needed to describe a fully initialized loan or a re-amortized one: the `periodicPayment` (unrounded), the current rounded `LoanState`, the computed `loanScale`, and `firstPaymentPrincipal`. The `loanScale` is not fixed at loan creation — it is derived dynamically from the `STAmount` exponent of the total value outstanding, then clamped to a minimum. This ensures that all subsequent rounding of principal, interest, and fees uses a consistent number of decimal places, preventing a class of dust-accumulation bugs where tiny remainders can never be cleared. + +**`LoanPaymentParts`** is the output of a completed payment: how much went to principal (`principalPaid`), how much to the vault as interest (`interestPaid`), how much to the broker as fees (`feePaid`), and a `valueChange` that records whether the loan's total outstanding moved in an unexpected direction. For a well-timed regular payment, `valueChange` is zero. For a late payment, it is positive (the penalty interest increased the debt). For an overpayment, it is negative (extra principal payment reduced future interest). The `operator+=` is provided so that multiple consecutive payment rounds can be accumulated into a single result. + +**`detail::LoanStateDeltas`** is the difference between two `LoanState` objects, computed by `operator-(LoanState, LoanState)`. This allows `tryOverpayment()` to measure accumulated rounding error — the gap between what the loan theoretically should owe and what the ledger's rounded values actually record — and carry that error forward into the re-amortized state. + +## Tracked vs. Untracked Amounts + +A critical architectural distinction separates `detail::PaymentComponents` from `detail::ExtendedPaymentComponents`. Tracked amounts (`trackedValueDelta`, `trackedPrincipalDelta`, `trackedManagementFeeDelta`) reduce the Loan ledger object's stored fields (`sfTotalValueOutstanding`, `sfPrincipalOutstanding`, `sfManagementFeeOutstanding`). These are the numbers that appear in the amortization schedule and drive future payment calculations. Untracked amounts (`untrackedManagementFee`, `untrackedInterest`) are paid out to the broker and vault respectively but do not alter the amortization schedule — they represent charges that have no scheduled counterpart, such as late payment fees, overdue penalty interest, or service fees. `ExtendedPaymentComponents` is constructed from a `PaymentComponents` base plus these two extra numbers, with `totalDue` computed inline in its constructor as `trackedValueDelta + untrackedInterest + untrackedManagementFee`. + +## Rounding Policy + +The spec requires that borrowers never benefit from rounding: periodic payments are always rounded upward via `roundPeriodicPayment()`, which delegates to `roundToAsset(..., Number::upward)`. The `isRounded()` helper checks whether a value is already at the target precision by comparing the upward and downward rounded forms — if they match, no further precision exists. + +The template `adjustImpreciseNumber()` handles a subtler problem. Certain loan values are re-rounded to vault scale every time they are adjusted, preventing the accumulation of rounding dust across many payments. It additionally clamps the result to zero if it would go negative — a defensive guard against off-by-one rounding errors that could otherwise leave tiny negative balances. + +## Core Computation Functions + +`loanPeriodicRate()` converts an annualized rate (expressed in tenth-bips) to a per-period rate by prorating it against `secondsInYear` (the `constexpr` constant for 365 days in seconds). All amortization math flows from this single conversion. + +`computeLoanProperties()` (with two overloads, one taking a raw `TenthBips32` rate and one a pre-converted `periodicRate`) encapsulates the XLS-66 Section A-2 equations for computing the initial loan state. It calculates the periodic payment, derives the `loanScale` from the total value's `STAmount` exponent, rounds all components consistently, and populates `firstPaymentPrincipal` — the unrounded principal share of the very first payment. This last field is checked as a guard condition in `checkLoanGuards()`: if it would round to zero, no payment can ever reduce the principal, and the loan must be rejected. + +`computeTheoreticalLoanState()` produces what the ledger values *should* be at a given point in the schedule, without any rounding. `constructLoanState()` and `constructRoundedLoanState()` produce `LoanState` values from arbitrary inputs or directly from the SLE fields, respectively. + +## The Overpayment Path + +`detail::tryOverpayment()` is the most complex function in the file. When a borrower pays more principal than scheduled, the remaining payments must be re-amortized from the new lower principal. The function cannot simply recalculate from scratch — it must preserve the accumulated rounding errors from the loan's history to maintain consistency. It does this by computing the theoretical (unrounded) state, measuring the error gap against the current rounded ledger state, and then adding that same error back to the newly re-amortized theoretical state before rounding again. The entire calculation runs against local copies of the loan state (a "sandbox"), and only if the result passes all guard conditions — the principal decreased, the new periodic payment is positive, and `checkLoanGuards()` succeeds — are the ledger proxy objects updated. If the overpayment would leave the loan in an invalid state, the function returns `Unexpected(tesSUCCESS)`, which causes the overpayment to be silently ignored rather than failing the entire transaction. + +## Entry Point + +`loanMakePayment()` is the public entry point that `LoanPay::doApply()` calls. It accepts a `LoanPaymentType` enum value (`regular`, `late`, `full`, or `overpayment`) and dispatches to the appropriate internal calculation path, returning `Expected`. All the structures and helper functions in this file exist to make that single entry point correct across every payment scenario the spec defines. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h.ai.json b/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h.ai.json new file mode 100644 index 0000000000..974d41e617 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h.ai.json @@ -0,0 +1,61 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 8, + "name": "LoanBrokerCoverClawback" + } + ], + "description": "Defines the LoanBrokerCoverClawback transactor class for handling a specific lending protocol transaction in the XRPL codebase, including preflight, preclaim, and apply logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "LoanBrokerCoverClawback" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h.ai.md b/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h.ai.md new file mode 100644 index 0000000000..33bb0ec775 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h.ai.md @@ -0,0 +1,37 @@ +# `LoanBrokerCoverClawback.h` — Transactor Header for Lending Protocol Cover Clawback + +## Role and Context + +This header defines the `LoanBrokerCoverClawback` transactor, which handles transaction type `ttLOAN_BROKER_COVER_CLAWBACK` (type 78) in XRPL's lending protocol. It is one of roughly ten transactors under `include/xrpl/tx/transactors/lending/` that together implement the full lifecycle of the XLS-66 lending system, alongside sibling types such as `LoanBrokerCoverDeposit`, `LoanBrokerCoverWithdraw`, `LoanBrokerSet`, and `LoanPay`. + +In the lending protocol, a loan broker maintains a pool of "cover" funds — assets deposited as collateral to absorb potential losses on loans it brokers. These cover funds are held in a pseudo-account controlled by the broker's on-ledger object and tracked in `sfCoverAvailable`. This transaction gives the **issuer of the vault's underlying asset** the power to forcibly reclaim (claw back) a portion of those cover funds. This is analogous to how XRPL's existing clawback feature lets token issuers reclaim IOUs from ordinary accounts, but applied specifically to the broker's cover reserve. + +## Class Structure + +`LoanBrokerCoverClawback` inherits from `Transactor` and conforms to the standard three-phase validation + apply pattern enforced across the entire transactor framework: + +- **`checkExtraFeatures`** (static, called from `invokePreflight`) — delegates entirely to `checkLendingProtocolDependencies`, which verifies that the `featureLendingProtocol` amendment and all its required dependencies are enabled. This is the canonical mechanism for amendment-gating a transactor without embedding the feature check inside `preflight` itself. + +- **`preflight`** (static, stateless field-level validation) — validates that at least one of `sfLoanBrokerID` or `sfAmount` is present (both absent is invalid); that if a broker ID is given it is nonzero; and that any `sfAmount` is non-XRP, non-negative, and legally formed. A subtle rule: if no `sfLoanBrokerID` is given, the amount must be an IOU (not MPT), because the broker identity will be inferred from the IOU's issuer field. + +- **`preclaim`** (static, ledger-read-only validation) — resolves the broker identity if not directly specified, checks that the submitting account is the actual issuer of the vault asset, verifies clawback permission flags (`lsfAllowTrustLineClawback` for IOUs, `lsfMPTCanClawback` for MPTs), and calculates the effective claw amount against the broker's minimum required cover. + +- **`doApply`** (virtual, ledger mutation) — decrements `sfCoverAvailable` on the broker SLE and transfers the claw amount from the broker pseudo-account to the issuer via `accountSend` with `WaiveTransferFee::Yes`. + +`ConsequencesFactory` is set to `Normal`, indicating the standard account-sequence-based ordering semantics with no blocking of other transaction types. + +## Non-Obvious Design Decisions + +**Broker identity resolution without an explicit ID.** The transaction makes `sfLoanBrokerID` optional. When omitted, the implementation infers the broker from the IOU issuer field in `sfAmount`. Because trust lines are bidirectional, an IOU amount carries both currency and issuer. If the issuer field names the broker's pseudo-account (which carries `sfLoanBrokerID` on its account SLE), the code extracts the broker ID from there. This convenience path only works for IOUs — MPTs have no bidirectional issuer ambiguity, so they require an explicit `sfLoanBrokerID`. The `preflight` enforces this restriction, and `determineBrokerID` in the `.cpp` implements the lookup. + +**Amount semantics: zero means "take everything above minimum cover."** If `sfAmount` is zero or absent, `determineClawAmount` computes `sfCoverAvailable − (sfDebtTotal × sfCoverRateMinimum)` and claws back the entire surplus. This allows the issuer to efficiently drain excess cover without knowing the exact balance. Non-zero amounts are still capped at the same maximum, so the transaction can never drive the broker below its contractual minimum cover ratio. + +**Template-specialized `preclaim` helpers for IOU vs MPT.** Rather than branching with `if/else`, the code uses `std::visit` with a templated helper `preclaimHelper` specialised for `Issue` and `MPTIssue`. For IOUs it checks `lsfAllowTrustLineClawback` and the absence of `lsfNoFreeze`; for MPTs it reads the `MPTokenIssuance` SLE and checks `lsfMPTCanClawback`. This pattern avoids code duplication while keeping the asset-type-specific logic isolated. + +**Balance sanity check in `preclaim`.** The code explicitly calls `accountHolds` against the broker pseudo-account and compares the result with the computed claw amount before permitting the transaction to proceed. The comment acknowledges that this value should always match `sfCoverAvailable`, treating any mismatch as an internal error. This defensive check catches ledger state corruption before `doApply` runs, when the cost of failure is lower. + +**`doApply` re-runs `determineBrokerID` and `determineClawAmount`.** Because `doApply` works on a mutable ledger view that could theoretically diverge from what `preclaim` saw (in speculative apply), both helper functions are called again with `tecINTERNAL` as the fallback on failure. The real state should be identical, so these branches are marked `LCOV_EXCL_LINE` — they exist purely as a defensive layer against unexpected ledger divergence. + +## Relationship to Sibling Files + +Within the lending directory, `LoanBrokerCoverDeposit` and `LoanBrokerCoverWithdraw` are the natural counterparts: they add to and voluntarily remove from `sfCoverAvailable`, respectively. The clawback variant is privileged — only the asset issuer can trigger it — while deposit and withdraw are available to the broker operator. `LendingHelpers.h` provides the shared math infrastructure (amortization schedules, interest rates, rounding) used by the heavier transactors like `LoanPay` and `LoanManage`; the clawback transactor does not require that machinery since it performs no amortization calculations, only a simple cover-floor arithmetic. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h.ai.json b/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h.ai.json new file mode 100644 index 0000000000..0de13fe7cb --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h.ai.json @@ -0,0 +1,56 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "LoanBrokerCoverDeposit" + } + ], + "description": "Defines the LoanBrokerCoverDeposit transactor class for handling loan broker cover deposit transactions in the XRPL lending protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "LoanBrokerCoverDeposit" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h.ai.md b/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h.ai.md new file mode 100644 index 0000000000..a42d9a43dc --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h.ai.md @@ -0,0 +1,40 @@ +# `LoanBrokerCoverDeposit.h` — Transactor for Depositing Cover into a Loan Broker + +## Purpose + +`LoanBrokerCoverDeposit` is the transactor responsible for depositing cover assets into a `LoanBroker` object within the XRPL lending protocol (XLS-66). Cover represents capital held in a broker's pseudo-account that backs broker obligations — most notably fee payments and risk coverage that the broker provides to the associated vault. This transactor allows the broker owner to increase the broker's available cover balance by transferring assets from their own account. + +## Role in the Lending Module + +The lending module defines a family of transactors under `include/xrpl/tx/transactors/lending/`, all following the same `Transactor` base-class pattern. The cover lifecycle is managed by three complementary transactors: `LoanBrokerCoverDeposit` (this file), `LoanBrokerCoverWithdraw`, and `LoanBrokerCoverClawback`. Deposit and withdraw are the standard credit/debit operations; clawback handles forced recovery. This separation of concerns maps each operation to a distinct on-ledger transaction type, keeping authorization and side-effect logic cleanly isolated. + +## Three-Phase Validation Pattern + +Like every `Transactor` subclass, `LoanBrokerCoverDeposit` is invoked through the framework's `invokePreflight` / `preclaim` / `doApply` pipeline, which the base class documents as compile-time polymorphism through static name-hiding rather than virtual dispatch (only `doApply` is virtual). + +**`checkExtraFeatures`** delegates to `checkLendingProtocolDependencies`, which gates the entire lending feature family behind its amendment. This is the appropriate place for amendment checks — the base-class comment explicitly prohibits doing amendment checks inside `preflight` itself. + +**`preflight`** is deliberately lightweight and stateless: it works only against the transaction fields in `ctx.tx`, not the ledger. It rejects a zero `sfLoanBrokerID` (a null broker reference is always invalid) and validates `sfAmount` for positivity and legality via `isLegalNet`. This catches obviously malformed transactions before any ledger I/O is attempted. + +**`preclaim`** does the substantive validation against the current ledger state (a read-only `ReadView`): + +1. Confirms the `LoanBroker` object exists via `keylet::loanbroker`. +2. Enforces ownership — only `sfOwner` of the broker may make a cover deposit, returning `tecNO_PERMISSION` otherwise. +3. Looks up the broker's associated vault to resolve the canonical `sfAsset` type. A missing vault is treated as a fatal ledger corruption (`tefBAD_LEDGER`, excluded from coverage with `LCOV_EXCL_*` markers because it should be structurally impossible). +4. Verifies that the deposited `Amount` matches the vault's asset type (`tecWRONG_ASSET`). +5. Runs a chain of asset transfer guards — non-transferable asset check (`canTransfer`), source-side freeze check (`checkFrozen`), deep-freeze check on the broker pseudo-account (`checkDeepFrozen`), and strong authorization check (`requireAuth`). These mirror the checks performed for any asset movement on XRPL and ensure the deposit respects the token issuer's rules. +6. Finally confirms the depositor has sufficient spendable balance (`tecINSUFFICIENT_FUNDS`), using `FreezeHandling::fhZERO_IF_FROZEN` and `AuthHandling::ahZERO_IF_UNAUTHORIZED` so frozen or unauthorized balances are treated as zero for sufficiency purposes. + +## `doApply` — Ledger Mutation + +The apply phase is concise. It re-peeks the `LoanBroker` SLE for mutable access, then: + +1. Calls `accountSend` to transfer the specified amount from the transaction sender (`account_`) to the broker's `sfAccount` (its pseudo-account), passing `WaiveTransferFee::Yes` — cover deposits are exempt from transfer fees, which is economically sensible since the broker owner is moving their own capital into infrastructure they control rather than making a market transfer. +2. Increments `sfCoverAvailable` on the broker SLE by the deposited amount and flushes the update with `view().update(broker)`. +3. Calls `associateAsset` to record the association between the broker and the vault asset, ensuring the broker's asset tracking remains consistent after the deposit. + +## Design Notes + +`ConsequencesFactory` is set to `Normal`, meaning this transaction claims a fee under standard circumstances and doesn't block other transactions from the same account. The constructor is `explicit` and simply forwards `ApplyContext` to the base class — all state lives in the context, not in the transactor instance. + +The choice to store cover in a broker-owned pseudo-account (rather than a simple numeric field) means the assets are held on-ledger with the full XRPL trust line and freeze machinery intact. This is why `preclaim` must run the same asset transfer guards as any other movement — the pseudo-account is a first-class ledger account subject to all the usual rules, not an off-ledger accounting entry. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h.ai.json b/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h.ai.json new file mode 100644 index 0000000000..90bbd2116a --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h.ai.json @@ -0,0 +1,56 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "LoanBrokerCoverWithdraw" + } + ], + "description": "Defines the LoanBrokerCoverWithdraw transactor class for handling cover withdrawal operations in the XRPL lending protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "LoanBrokerCoverWithdraw" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h.ai.md b/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h.ai.md new file mode 100644 index 0000000000..ba8be209a1 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h.ai.md @@ -0,0 +1,47 @@ +# `LoanBrokerCoverWithdraw` — Loan Broker Cover Withdrawal Transactor + +## Role in the System + +`LoanBrokerCoverWithdraw` is the transactor that lets a loan broker owner reclaim cover funds that are no longer needed. Within the XRPL lending protocol (XLS-66), a `LoanBroker` ledger object acts as an intermediary between lenders (vaults) and borrowers. To guarantee the vault against borrower defaults, the broker must maintain a reserve of *cover*: assets held in the broker's own pseudo-account. This transactor handles the controlled exit path — withdrawing cover back to the owner's account or to a named third-party destination, while enforcing the minimum cover ratio that must remain in place as long as outstanding loans exist. + +The class lives in the `xrpl` namespace alongside its symmetric counterparts `LoanBrokerCoverDeposit` and `LoanBrokerCoverClawback`, all sharing the same structural contract and following the standard XRPL transactor pipeline. + +## Transactor Architecture + +`LoanBrokerCoverWithdraw` inherits from `Transactor` and participates in the three-phase execution model that every transactor follows: + +1. **`checkExtraFeatures`** (static, preflight-gating): Delegates directly to `checkLendingProtocolDependencies(ctx)`, which validates that all required feature amendments for the lending protocol are enabled in the current ledger rules. If any dependency is absent, `invokePreflight` will return `temDISABLED` before any further validation occurs. + +2. **`preflight`** (static, stateless validation): Performs purely syntactic checks against the serialized transaction fields without consulting the ledger. It rejects a zero `sfLoanBrokerID`, a non-positive or legally-invalid `sfAmount`, and a zero-value `sfDestination` if one is provided. Critically, `preflight` does *not* look up any ledger objects — that is reserved for `preclaim`. + +3. **`preclaim`** (static, read-only ledger checks): Performs the bulk of business-rule validation against the current `ReadView`. It: resolves the destination account (defaulting to the submitter's own account if `sfDestination` is absent); rejects withdrawals targeting a pseudo-account; loads and validates the `LoanBroker` ledger object and confirms ownership; loads the associated `Vault` to discover the underlying asset; enforces asset-transfer invariants (transferability, freeze, deep-freeze, authorization); and finally calculates the minimum cover constraint. + +4. **`doApply`** (instance, mutable ledger mutation): Decrements `sfCoverAvailable` on the `LoanBroker` SLE by the requested amount, calls `view().update(broker)`, invokes `associateAsset` to maintain asset-tracking bookkeeping, then delegates the actual token movement to the shared `doWithdraw` helper, passing the broker's pseudo-account as the source of funds. + +`ConsequencesFactory` is set to `Normal`, meaning this transaction does not block unrelated transactions from the same account from being applied. + +## Cover Minimum Enforcement + +The most significant invariant enforced in `preclaim` is the minimum cover ratio. The broker stores `sfCoverRateMinimum` as a 32-bit tenth-bips value (units of 0.001 basis points). Given the broker's current `sfDebtTotal` — the total principal outstanding across all its loans — the minimum allowable cover is: + +``` +minimumCover = roundUp(tenthBipsOfValue(debtTotal, coverRateMinimum)) +``` + +The `NumberRoundModeGuard` is deliberately set to `Number::upward` before this calculation so that the minimum requirement is always rounded conservatively: any fractional asset unit is rounded *up*, never truncated. After confirming that the withdrawal amount does not exceed `sfCoverAvailable`, the code then checks that `(coverAvail - amount) >= minimumCover`. A separate check against `accountHolds` on the pseudo-account provides a final guard against stale or inconsistent ledger state. + +## Third-Party Destination and Authorization Model + +The optional `sfDestination` field elevates the security requirements. When the destination is the same as the transaction submitter, only `WeakAuth` is required — the owner is reclaiming their own funds. When the destination is a third party, the withdrawal becomes effectively an asset transfer to an external account. In that case: +- `canWithdraw` is called to verify the submitting account holds any credentials or permissions needed for third-party asset movements. +- `authType` is upgraded to `StrongAuth`, meaning the destination account must have already consented to receive the asset by establishing a `RippleState` (for IOU assets) or an `MPToken` (for MPT assets). + +This two-tier model prevents the broker owner from inadvertently or maliciously pushing assets into accounts that have not opted in, while still allowing simple self-withdrawals with a lighter authorization burden. + +## Pseudo-Account Pattern + +The actual cover funds do not sit directly in the broker owner's account. Instead, the `LoanBroker` SLE carries an `sfAccount` field pointing to a broker-specific *pseudo-account* — a synthetic account that holds the vault-asset balance. The owner interacts with cover through deposit and withdrawal transactions rather than direct transfers. `doApply` reads `brokerPseudoID` from the SLE and passes it to `doWithdraw` as the debit-side account, so the standard payment machinery debits the pseudo-account and credits the destination. + +## Error Handling and Failure Modes + +`preclaim` returns well-defined `TER` codes for each failure scenario: `tecPSEUDO_ACCOUNT` for a pseudo-account destination, `tecNO_ENTRY` for a missing broker, `tecNO_PERMISSION` for ownership mismatches, `tefBAD_LEDGER` (marked `LCOV_EXCL`) for the theoretically impossible case of a broker existing without its vault, `tecWRONG_ASSET` for an amount/asset type mismatch, and `tecINSUFFICIENT_FUNDS` for both the cover-availability check and the minimum-cover-ratio check. In `doApply`, the defensive re-checks of `broker` and `vault` presence return `tecINTERNAL` (also `LCOV_EXCL`) because `preclaim` guarantees their existence; those paths exist only to satisfy the type system rather than to recover from real failures. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h.ai.json b/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h.ai.json new file mode 100644 index 0000000000..4d62924c58 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "LoanBrokerDelete" + } + ], + "description": "Defines the LoanBrokerDelete transactor class for handling loan broker deletion transactions in the XRPL lending protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "LoanBrokerDelete" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h.ai.md b/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h.ai.md new file mode 100644 index 0000000000..be24819c99 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerDelete.h.ai.md @@ -0,0 +1,48 @@ +# `LoanBrokerDelete.h` — Transactor for Deleting a Lending Protocol Broker + +## Role and Context + +`LoanBrokerDelete` is the transactor that processes `ttLOAN_BROKER_DELETE` (type 75) transactions within the XRPL lending protocol (amendment `featureLendingProtocol`). A `LoanBroker` is a ledger object created by the `LoanBrokerSet` transaction that acts as an intermediary between a vault (the lender-side liquidity pool) and individual borrowers: it holds cover collateral, tracks cumulative debt, and earns management fees. This transactor implements the teardown path — removing the broker, recovering any remaining cover assets, and cleaning up its associated pseudo-account. + +The class sits in the `include/xrpl/tx/transactors/lending/` module alongside `LoanBrokerSet`, `LoanBrokerCoverDeposit`, `LoanBrokerCoverWithdraw`, `LoanBrokerCoverClawback`, and the `Loan*` family, all of which share the `LendingHelpers` utility layer. + +## Class Design + +```cpp +class LoanBrokerDelete : public Transactor +``` + +`LoanBrokerDelete` follows the standard XRPL transactor pattern. It inherits from `Transactor` and overrides only `doApply()`, delegating signature validation, fee checks, and sequence number management entirely to the base class. The static member `ConsequencesFactory{Normal}` tells the transaction-processing infrastructure that this is a routine (non-blocking) transaction whose fee consequences are normal — it will not prevent other transactions in a batch from running. + +Like all transactors in this codebase, `LoanBrokerDelete` uses compile-time polymorphism via name hiding rather than virtual dispatch for `preflight` and `preclaim`. This is intentional: it allows the `invokePreflight` template in `Transactor` to call `T::checkExtraFeatures`, `T::preflight`, and so on at compile time without a vtable lookup, while still permitting per-transactor customization. + +## Three-Phase Processing + +**`checkExtraFeatures`** delegates immediately to `checkLendingProtocolDependencies(ctx)` from `LendingHelpers.h`. This centralizes the amendment-gate logic for the entire lending subsystem — rather than each lending transactor individually checking `featureLendingProtocol` (and any prerequisite amendments), this single call enforces all dependencies uniformly. Returning `false` causes `invokePreflight` to emit `temDISABLED`. + +**`preflight`** performs only the lightest possible validation: it rejects the transaction if `sfLoanBrokerID` is the zero hash (`beast::zero`), returning `temINVALID`. This is a pure field-validity check that requires no ledger access and can be performed before any state is read. + +**`preclaim`** performs stateful validation against a read-only ledger view: + +1. Looks up the `LoanBroker` SLE via `keylet::loanbroker(brokerID)`. If absent, returns `tecNO_ENTRY`. +2. Enforces ownership: `sfAccount` on the transaction must match `sfOwner` on the broker SLE. Any other account gets `tecNO_PERMISSION`. +3. Checks that `sfOwnerCount` on the broker is zero. A non-zero count means active `Loan` objects still reference this broker; deletion would orphan them, so `tecHAS_OBLIGATIONS` is returned. +4. Reads the associated `Vault` SLE (via `sfVaultID`) to obtain the vault's `sfAsset`. A missing vault is a ledger-consistency failure reported as `tefBAD_LEDGER` (guarded with `LCOV_EXCL` because it should be unreachable in practice). +5. Checks `sfDebtTotal` against rounding. The comment is explicit: any remaining dust-level debt should already have been zeroed out by the last `LoanDelete` transaction. This is a *defensive* guard — if rounded debt is non-zero, the broker still has economic obligations and `tecHAS_OBLIGATIONS` is returned. The double-`LCOV_EXCL` annotation signals this path should never be reached in normal operation. +6. If `sfCoverAvailable > 0`, the remaining cover will be returned to the broker owner during `doApply`. Before allowing that asset transfer, a `checkDeepFrozen` call verifies the owner is not frozen for that asset. Deep-freeze is a compliance mechanism in which an issuer can prevent an account from receiving tokens; attempting to pay a frozen account would fail, so this check surfaces `tecFROZEN` at the cheaper preclaim stage. + +**`doApply`** executes the actual ledger mutations: + +1. **Directory removal** — the broker is removed from two owner directories: the human owner's `ownerDir` (tracked by `sfOwnerNode`) and the vault pseudo-account's `ownerDir` (tracked by `sfVaultNode`). Directory removal failure is an internal inconsistency and returns `tefBAD_LEDGER`. +2. **Cover asset recovery** — any `sfCoverAvailable` amount is transferred from the broker's pseudo-account to the human owner via `accountSend(..., WaiveTransferFee::Yes)`. The fee waiver prevents the transfer from being charged, since this is a cleanup payment, not a user-initiated one. +3. **Empty holding removal** — `removeEmptyHolding` clears the trust line or MPT holding on the broker pseudo-account for the vault asset, if it is now empty. +4. **Pseudo-account validation** — before erasing the pseudo-account SLE, the code triple-checks that no residual balance, owner count, or owner directory remains. These checks are marked `LCOV_EXCL` because the prior steps should have eliminated all obligations. If any remain, `tecHAS_OBLIGATIONS` is returned rather than silently corrupting ledger state. +5. **SLE erasure** — both the broker pseudo-account SLE and the broker SLE are erased from the ledger in that order. +6. **Owner count adjustment** — the human owner's `sfOwnerCount` is decremented by 2: one for the `LoanBroker` object itself, and one for its pseudo-account. This is explicit in a comment in `doApply`, making the coupling between these two objects clear to future readers. +7. **Asset association** — `associateAsset(*broker, vaultAsset)` is called after erasure, recording the asset type touched by this transaction for downstream processing (e.g., fee or reserve tracking). + +## Design Notes + +The pseudo-account pattern — where a `LoanBroker` object is paired with an ephemeral `AccountRoot` SLE that holds the cover collateral — mirrors the vault design elsewhere in the lending protocol. This gives each broker its own on-ledger identity for holding trust lines and MPT positions, without those holdings appearing directly in the human owner's account. The deletion transactor must therefore coordinate a two-SLE teardown, which is why the owner count is decremented by two and the pseudo-account health is validated before erasure. + +The header itself is minimal by design: it exposes only the four public entry points required by the transactor framework (`checkExtraFeatures`, `preflight`, `preclaim`, `doApply`) and the `ConsequencesFactory` tag. All business logic lives in the corresponding `.cpp` file, keeping the include cost low for the many translation units that include transactor headers during registration. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerSet.h.ai.json b/include/xrpl/tx/transactors/lending/LoanBrokerSet.h.ai.json new file mode 100644 index 0000000000..7d1f217a1d --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerSet.h.ai.json @@ -0,0 +1,66 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "LoanBrokerSet" + } + ], + "description": "Defines the LoanBrokerSet transaction transactor class for the XRPL lending protocol, including its interface and related static methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LoanBrokerSet.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "LoanBrokerSet" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [], + "lineno": 21, + "name": "getValueFields" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 24, + "name": "preclaim" + }, + { + "args": [], + "lineno": 27, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanBrokerSet.h.ai.md b/include/xrpl/tx/transactors/lending/LoanBrokerSet.h.ai.md new file mode 100644 index 0000000000..01aaf40f95 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanBrokerSet.h.ai.md @@ -0,0 +1,62 @@ +# `LoanBrokerSet.h` — LoanBroker Create/Update Transactor + +## Role in the System + +`LoanBrokerSet` implements the XRPL transaction type responsible for creating and modifying `LoanBroker` ledger objects within the XLS-66 on-chain lending protocol. Following the ledger's "set" (upsert) convention, a single transaction type handles both the initial creation of a broker and subsequent updates to its mutable fields, discriminated at runtime by whether `sfLoanBrokerID` is present in the transaction. + +A `LoanBroker` is a protocol actor that intermediates between a `Vault` (the liquidity pool) and borrowers. Each broker is permanently associated with exactly one vault, earns a management fee from loan interest, and optionally enforces collateral cover-rate thresholds that trigger liquidation. `LoanBrokerSet` is how the vault owner provisions and reconfigures such a broker. + +## Class Structure + +`LoanBrokerSet` inherits from `Transactor` and follows the three-phase validation model common to all XRPL transactors: + +``` +checkExtraFeatures → preflight → preclaim → doApply +``` + +`ConsequencesFactory` is set to `Normal`, which means the transaction cannot block the account's sequence from advancing — it is a standard, displaceable operation. + +`checkExtraFeatures()` delegates entirely to `checkLendingProtocolDependencies()`, gating the entire lending feature set on its required amendments being enabled in the current rule set. This is consistent with every other transactor in the `lending/` subdirectory, ensuring that all lending operations share a common amendment check. + +## `preflight()` Validation + +The preflight phase validates fields that can be checked without ledger access: + +- **`sfData`**: Optional metadata blob. If present, its length is capped at `maxDataPayloadLength`. +- **Rate fields** (`sfManagementFeeRate`, `sfCoverRateMinimum`, `sfCoverRateLiquidation`): Each is independently validated against a protocol-defined maximum via `validNumericRange()`. +- **`sfDebtMaximum`**: Validated to lie within `[0, maxMPTokenAmount]` — the upper bound matches the maximum for MPToken amounts, covering the case where the vault asset is an MPToken. +- **Mutability constraint**: When `sfLoanBrokerID` is present (update mode), the three "fixed" fields — `sfManagementFeeRate`, `sfCoverRateMinimum`, `sfCoverRateLiquidation` — are rejected. These parameters are set once at creation and cannot be renegotiated after loans may be outstanding against them. +- **Cover-rate pairing invariant**: `sfCoverRateMinimum` and `sfCoverRateLiquidation` must either both be zero or both be non-zero. A minimum with no liquidation threshold (or vice versa) is incoherent and is rejected with `temINVALID`. + +## `getValueFields()` and Precision Checking + +`getValueFields()` returns a static `vector` containing `~sfDebtMaximum` (the optional `sfDebtMaximum` field). This is used in `preclaim()` to verify that the requested `sfDebtMaximum` value can be faithfully represented in the vault's asset type — important for non-IOU assets like XRP (integer drops) or MPTokens, where a fractional amount would silently lose precision. If `STAmount{asset, *value} != *value`, the transaction fails with `tecPRECISION_LOSS`. The static vector design avoids repeated heap allocation across calls. + +`LoanSet` declares the same `getValueFields()` pattern, indicating this is a protocol-level convention for numeric fields that must round-trip cleanly through the ledger's asset representation. + +## `preclaim()` Validation + +`preclaim()` has read access to the ledger and enforces ownership and consistency rules: + +- The referenced `sfVaultID` must resolve to an existing vault, and the transaction's `sfAccount` must match `sfOwner` on that vault. Only the vault owner can create or modify brokers against it. +- **In update mode** (`sfLoanBrokerID` present): The broker object must exist, its `sfVaultID` must match the transaction's `sfVaultID` (vault association is immutable), and `sfAccount` must own the broker. A guard prevents setting `sfDebtMaximum` to a non-zero value below the broker's current `sfDebtTotal`, which would strand outstanding loans above the new limit. +- **In creation mode**: `canAddHolding()` confirms the vault can accept a new trust line / holding, and `checkFrozen()` ensures the vault's pseudo-account is not frozen for the asset. These checks are skipped in update mode because the holding was already established at creation. + +## `doApply()` — Create vs. Update + +**Update path**: Only `sfData` (metadata) and `sfDebtMaximum` are written back to the existing broker SLE; then `view.update()` commits the change. Rate fields are deliberately absent here, enforcing the immutability constraint at apply time as a second guard. + +**Create path**: This is the more complex code path: + +1. The broker's `keylet` is derived from `(account_, sequence)` — making it unique per account and transaction sequence. +2. Two directory entries are created via `dirLink()`: one in the owner's account directory, and one in the vault's pseudo-account directory under `sfVaultNode`. The latter link allows the vault to enumerate all its brokers. +3. `adjustOwnerCount()` increments the owner count by **two** — one for the broker SLE and one for the broker's own pseudo-account — and the reserve check is deferred until after the increment, intentionally using `preFeeBalance_` (balance before the fee was deducted) to test the strict pre-fee balance. +4. A pseudo-account is created via `createPseudoAccount()` keyed on the broker's ledger key, with `sfLoanBrokerID` as the back-reference type. Pseudo-accounts give the broker an on-chain identity for holding collateral assets. +5. `addEmptyHolding()` establishes the broker's trust line / MPToken holding for the vault asset on the pseudo-account. +6. All fields (`sfSequence`, `sfVaultID`, `sfOwner`, `sfAccount`, `sfLoanSequence`, optional fields) are initialized. `sfLoanSequence` starts at `1` and is used by `LoanSet` to index loans issued through this broker. + +`associateAsset()` is called in both paths to register the broker's relationship to the vault asset in any asset-specific indexes. + +## Relationship to Sibling Transactors + +The lending subdirectory contains a coherent set of transactors: `LoanBrokerSet` (this file), `LoanBrokerDelete`, `LoanBrokerCoverDeposit/Withdraw/Clawback`, `LoanSet`, `LoanManage`, `LoanPay`, and `LoanDelete`. `LoanBrokerSet` establishes the broker entity that the rest of these transactors operate on. Its `sfLoanSequence` counter is consumed by `LoanSet` when opening new loans, tying the lifecycle of individual loans to their originating broker. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanDelete.h.ai.json b/include/xrpl/tx/transactors/lending/LoanDelete.h.ai.json new file mode 100644 index 0000000000..c9201fb5df --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanDelete.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 16, + "name": "ctx" + }, + { + "lineno": 19, + "name": "ctx" + }, + { + "lineno": 22, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "LoanDelete" + } + ], + "description": "Defines the LoanDelete transaction transactor class for the XRPL lending protocol, including its interface for preflight, preclaim, and application logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LoanDelete.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "LoanDelete" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 19, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 22, + "name": "preclaim" + }, + { + "args": [], + "lineno": 25, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanDelete.h.ai.md b/include/xrpl/tx/transactors/lending/LoanDelete.h.ai.md new file mode 100644 index 0000000000..3a423ebc4b --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanDelete.h.ai.md @@ -0,0 +1,36 @@ +# `LoanDelete.h` — Loan Deletion Transactor + +## Role in the System + +`LoanDelete` is the transactor responsible for removing a fully-repaid loan object from the XRPL ledger. It lives within the on-chain lending protocol (`xls-66`) alongside `LoanSet`, `LoanPay`, `LoanManage`, and the loan broker family of transactors. While `LoanSet` establishes a loan and `LoanPay` services it, `LoanDelete` closes out the loan's lifecycle once the borrower has satisfied all payment obligations, freeing the owner-count reservations that were held against both the borrower's account and the loan broker's pseudo-account. + +## Class Structure and the Transactor Pipeline + +`LoanDelete` publicly inherits from `Transactor` and follows the standard three-phase compile-time polymorphic dispatch that the framework mandates. The class exposes no constructor beyond the inline delegating one that simply forwards `ApplyContext` to the base. The `ConsequencesFactory` is set to `Normal`, indicating that failing this transaction does not block subsequent transactions from the same account in a batch — appropriate because a loan-deletion failure leaves the ledger state unchanged and has no impact on sequence numbers or fee eligibility for other operations. + +The four key methods — `checkExtraFeatures`, `preflight`, `preclaim`, and `doApply` — are static for the first three and virtual for the last, matching the pattern enforced by `Transactor::invokePreflight`. This is not polymorphism through vtables but name-hiding: the framework template instantiates the correct static overrides at compile time, so each phase runs in isolation from the others. + +## Validation Phases + +`checkExtraFeatures` delegates entirely to `checkLendingProtocolDependencies(ctx)`, defined in `LendingHelpers.h`. This call verifies that all required amendments enabling the lending protocol are active in the current rules set. By separating this check from `preflight`, the framework can gate the transaction at the feature-flag level before any field-level validation even begins. + +`preflight` is deliberately minimal: it only verifies that `sfLoanID` is not the zero hash. All other checks — existence, ownership, and state — would require ledger access and are therefore deferred to `preclaim`. This respects the phase contract: `preflight` runs without a ledger view and must stay cheap and stateless. + +`preclaim` performs the substantive pre-execution guards. It reads the loan object via `keylet::loan(loanID)` and enforces two invariants before allowing the transaction to proceed. First, the loan's `sfPaymentRemaining` field must be zero — an active loan with outstanding installments cannot be deleted, returning `tecHAS_OBLIGATIONS`. This is the core business rule: loan cleanup is only permitted once the borrower has fully amortized the debt. Second, the caller must be either the broker owner (identified by traversing the `LoanBroker` SLE from the loan's `sfLoanBrokerID`) or the direct borrower (`sfBorrower`). Any other account gets `tecNO_PERMISSION`. If the `LoanBroker` SLE is somehow absent despite the loan referencing it, the method returns `tecINTERNAL` and marks that branch `LCOV_EXCL_LINE` — a defensive impossible-path guard. + +## Application Logic + +`doApply` performs the multi-step ledger mutation. It resolves the loan, borrower account, loan broker, and vault objects via `view.peek()`. Each missing SLE returns `tefBAD_LEDGER` (all marked `LCOV_EXCL_LINE` since `preclaim` guaranteed these objects exist). The cleanup sequence is: + +1. **Directory removal**: The loan's key is evicted from the `ownerDir` of the broker's pseudo-account (using `sfLoanBrokerNode` as the directory node hint) and from the borrower's `ownerDir` (using `sfOwnerNode`). +2. **SLE erasure**: The loan ledger object is deleted with `view.erase(loanSle)`. +3. **Broker owner-count decrement**: `adjustOwnerCount` reduces the broker's `sfOwnerCount` by one. The broker's owner count specifically tracks outstanding loans, distinct from the pseudo-account's own count. +4. **Dust debt forgiveness**: If the decremented owner count reaches zero — meaning this was the last loan under the broker — the broker's `sfDebtTotal` is checked. Any residual non-zero amount is forcibly zeroed. The surrounding `XRPL_ASSERT_PARTS` call verifies that the remaining debt rounds to zero given the vault's asset scale, documenting the invariant that only rounding dust can remain when all loans are repaid. This prevents an accumulation of sub-asset-precision amounts from stranding the broker in an uncleanable state. +5. **Borrower owner-count decrement**: `adjustOwnerCount` also decreases the borrower's count, releasing the XRP reserve that was locked when the loan was created. +6. **Asset association**: `associateAsset` is called on the loan, broker, and vault SLEs as a safety measure. The comment notes these calls "shouldn't do anything" at this point but are included defensively to ensure any asset-tracking side effects are consistent. + +## Design Observations + +The permission model allows either party to initiate deletion. This is intentional: both the broker (seeking to reclaim administrative state) and the borrower (seeking to recover their owner reserve) have valid incentives to clean up after full repayment. Neither can do so prematurely because the `sfPaymentRemaining` guard in `preclaim` is absolute. + +The dust-forgiveness path for the final loan is a pragmatic safeguard against cumulative fixed-point rounding errors. The lending protocol uses `Number` arithmetic with explicit rounding modes, but over many payment periods, sub-precision residuals in `sfDebtTotal` can accumulate. Rather than leaving the broker's debt balance permanently non-zero after all loans are closed, the transactor zeroes it with an assertion that bounds the magnitude of the forgiven amount. This keeps the lending protocol's invariants clean without burdening individual payment transactions with perfect precision requirements. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanManage.h.ai.json b/include/xrpl/tx/transactors/lending/LoanManage.h.ai.json new file mode 100644 index 0000000000..d99d5a6ea3 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanManage.h.ai.json @@ -0,0 +1,90 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "LoanManage" + } + ], + "description": "Defines the LoanManage transactor class for managing loan-related operations (default, impair, unimpair) in the XRPL lending protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LoanManage.h", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 24, + "name": "preclaim" + }, + { + "args": [ + "ApplyView& view", + "SLE::ref loanSle", + "SLE::ref brokerSle", + "SLE::ref vaultSle", + "Asset const& vaultAsset", + "beast::Journal j" + ], + "lineno": 30, + "name": "defaultLoan" + }, + { + "args": [ + "ApplyView& view", + "SLE::ref loanSle", + "SLE::ref vaultSle", + "Asset const& vaultAsset", + "beast::Journal j" + ], + "lineno": 39, + "name": "impairLoan" + }, + { + "args": [ + "ApplyView& view", + "SLE::ref loanSle", + "SLE::ref vaultSle", + "Asset const& vaultAsset", + "beast::Journal j" + ], + "lineno": 47, + "name": "unimpairLoan" + }, + { + "args": [], + "lineno": 54, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanManage.h.ai.md b/include/xrpl/tx/transactors/lending/LoanManage.h.ai.md new file mode 100644 index 0000000000..2757d3f9c7 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanManage.h.ai.md @@ -0,0 +1,64 @@ +# `LoanManage.h` — Loan Status Management Transactor + +## Role in the System + +`LoanManage` is a transactor in the XRPL lending protocol (XLS-66 specification) responsible for transitioning a loan through its three credit-quality states: **unimpaired**, **impaired**, and **defaulted**. It sits in the `lending` transactor subdirectory alongside `LoanSet`, `LoanPay`, `LoanDelete`, `LoanBrokerSet`, and the cover-operation transactors, forming the lifecycle management layer of the protocol. + +While `LoanSet` originates a loan and `LoanPay` processes borrower payments, `LoanManage` is the mechanism by which the loan broker signals that a borrower is struggling to repay. Its authority is strictly limited to the owner of the `LoanBroker` object that issued the loan — no other party can trigger these state transitions. + +## Transactor Inheritance and Lifecycle Hooks + +`LoanManage` inherits from `Transactor` using the static-polymorphism pattern common throughout the XRPL codebase. Rather than virtual dispatch, `Transactor::invokePreflight()` calls into `T::checkExtraFeatures`, `T::getFlagsMask`, `T::preflight`, and `T::preflightSigValidated` by name, enforcing a compile-time protocol. `LoanManage` overrides all the relevant hooks: + +- **`checkExtraFeatures`** delegates to `checkLendingProtocolDependencies`, which gates the entire transactor on the amendment(s) required by the lending protocol. This is the correct place for amendment checks — the base class `invokePreflight` template returns `temDISABLED` if `checkExtraFeatures` returns false. +- **`getFlagsMask`** returns `tfLoanManageMask`, restricting which transaction flags are legal. The mask covers exactly the three action flags: `tfLoanDefault`, `tfLoanImpair`, and `tfLoanUnimpair`. +- **`preflight`** validates two things: that a non-zero `LoanID` is present, and that the three action flags are mutually exclusive. The mutual-exclusivity check uses the standard bit-twiddling idiom `(flags & (flags - 1)) != 0` to detect whether more than one bit is set. +- **`preclaim`** enforces the **loan state machine** before touching mutable state: a defaulted loan is permanently frozen; an already-impaired loan cannot be impaired again; an unimpaired loan cannot be unimpaired. Additionally, defaulting is only legal after the next payment due date plus grace period has expired, preventing premature default triggers. `preclaim` also verifies that the submitting account owns the `LoanBroker` associated with the loan. + +`ConsequencesFactory` is set to `Normal`, meaning the transaction fee is claimed even on failure in the standard way — appropriate since this transaction cannot block the queue. + +## The Loan State Machine + +The impairment lifecycle follows a strict directed acyclic graph: + +``` +unimpaired ──► impaired ──► defaulted + └─────────────────────────────► + impaired ◄── unimpaired (recovery) +``` + +Once a loan enters `lsfLoanDefault`, `preclaim` permanently prevents any further modification. A fully-paid loan (`sfPaymentRemaining == 0`) is also immutable through this transactor. These guards are checked in `preclaim` rather than `doApply`, so invalid transactions claim a fee but do not touch ledger state. + +## Static Helper Methods and Cross-Transactor Reuse + +The three core operations — `defaultLoan`, `impairLoan`, and `unimpairLoan` — are exposed as `public static` methods with `/** Helper function that might be needed by other transactors */` comments. This design acknowledges that other transactors (e.g., a cover-clawback or broker-delete operation) may need to trigger the same ledger mutations as part of their own logic, avoiding code duplication while keeping the accounting logic in one place. + +### `defaultLoan` + +This is the most complex operation, implementing XLS-66 spec section 3.2.3.2. When a loan defaults: + +1. **First-Loss Capital absorption**: The broker's cover capital (pledged as collateral) absorbs a portion of the loss. The absorbed amount is capped by two rates — `sfCoverRateMinimum` and `sfCoverRateLiquidation` (both in tenth-basis-points) — and further capped by `sfCoverAvailable`. The rounding mode is set to `upward` for the minimum required coverage, ensuring the broker always covers at least the required minimum. + +2. **Vault accounting**: The vault's `sfAssetsTotal` decreases by the unabsorbed default amount (rounded down to the vault's asset scale to avoid inflating the vault). The vault's `sfAssetsAvailable` increases by the `defaultCovered` amount, since first-loss capital is returned as liquid assets. A dust-tolerance check handles the case where floating-point imprecision makes `sfAssetsAvailable` fractionally exceed `sfAssetsTotal` — if the difference exponent is more than 13 places smaller, it is treated as rounding dust and both values are equalized upward. + +3. **Unrealized loss reconciliation**: If the loan was previously impaired (which records a "paper loss" in `sfLossUnrealized`), that paper loss is cleared since the loss is now realized. + +4. **Loan zeroing**: All outstanding balances (`sfTotalValueOutstanding`, `sfPrincipalOutstanding`, `sfManagementFeeOutstanding`, `sfPaymentRemaining`, `sfNextPaymentDueDate`) are set to zero and `lsfLoanDefault` is set. + +5. **Pseudo-account transfer**: `accountSend` moves the covered amount from the broker's pseudo-account back to the vault's pseudo-account with `WaiveTransferFee::Yes`, since this is an internal settlement, not a user-initiated transfer. + +### `impairLoan` + +`impairLoan` marks a loan as troubled without yet realizing the loss. It records the full amount owed to the vault (`sfTotalValueOutstanding - sfManagementFeeOutstanding`) as `sfLossUnrealized` in the vault — a "paper loss" that reduces the effective NAV of the vault's shares without moving any funds. If the next payment due date has not yet passed, it is advanced to the current ledger close time, accelerating the payment schedule. There is a guard: if the unrealized loss would exceed the vault's unavailable assets (total minus available), the operation is rejected with `tecLIMIT_EXCEEDED` to prevent the vault from entering an inconsistent state. + +### `unimpairLoan` + +`unimpairLoan` reverses an impairment. It clears the `sfLossUnrealized` entry in the vault (reversing the paper loss) and restores the `sfNextPaymentDueDate`. The restored date depends on timing: if the original next payment date has not yet passed, the loan returns to its normal amortization schedule; otherwise the due date is reset from the current ledger time plus one payment interval, giving the borrower a full interval to make the next payment. `unimpairLoan` is marked `[[nodiscard]]` to force callers to check its `TER` return — this is the only one of the three operations with this annotation, reflecting that its accounting rollback must not be silently ignored. + +## `doApply` and Amendment Gating + +`doApply` resolves the loan, broker, and vault SLEs via `view.peek()` (obtaining mutable references), then dispatches to the appropriate static helper based on the transaction flag. No flags is explicitly valid — a documented no-op. After the flag dispatch, a secondary check for amendment `fixSecurity3_1_3` calls `associateAsset` on all three SLEs. This amendment-gated call was added as a correctness fix; before the amendment, `associateAsset` was only called on the no-op path, which was a bug for the state-changing paths. + +## Invariants and Failure Modes + +Ledger consistency failures that "should be impossible" (unreachable in correct ledger state) return `tefBAD_LEDGER` or `tecINTERNAL` marked with `LCOV_EXCL_LINE`, indicating they are excluded from coverage requirements. This is a common defensive pattern in the XRPL codebase: guard impossible states with fatal-level log messages and appropriate error codes rather than assertions, so a corrupted ledger does not crash a validator node. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanPay.h.ai.json b/include/xrpl/tx/transactors/lending/LoanPay.h.ai.json new file mode 100644 index 0000000000..53d9ffec3a --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanPay.h.ai.json @@ -0,0 +1,71 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 8, + "name": "LoanPay" + } + ], + "description": "Defines the LoanPay transactor class for handling loan payment transactions in the XRPL lending protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LoanPay.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "LoanPay" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 24, + "name": "calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 27, + "name": "preclaim" + }, + { + "args": [], + "lineno": 30, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanPay.h.ai.md b/include/xrpl/tx/transactors/lending/LoanPay.h.ai.md new file mode 100644 index 0000000000..b6608c25aa --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanPay.h.ai.md @@ -0,0 +1,75 @@ +# `LoanPay.h` — Loan Repayment Transactor + +## Role in the System + +`LoanPay.h` declares the `LoanPay` transactor, the on-ledger entry point for borrowers repaying obligations created by the XRPL Lending Protocol (XLS-66). It fits into the standard XRPL transaction pipeline as a subclass of `Transactor`, adding lending-specific validation and multi-destination fund routing on top of the common sequencing, fee, and signature machinery. + +The header is deliberately thin — a forward declaration of three static lifecycle hooks (`checkExtraFeatures`, `preflight`, `preclaim`), a custom `calculateBaseFee`, and the virtual `doApply`. All implementation lives in `LoanPay.cpp`, which pulls in `LendingHelpers.h` for payment computation and `LoanManage.h` for loan impairment utilities. + +--- + +## Transaction Lifecycle + +### `checkExtraFeatures` — Amendment Gating + +Rather than scattering amendment checks throughout `preflight`, `LoanPay` delegates to `checkLendingProtocolDependencies`, a shared helper that verifies all feature flags the lending protocol depends on are active. If any dependency is absent, `invokePreflight` returns `temDISABLED` before any field parsing occurs. + +### `getFlagsMask` — Mutually Exclusive Payment Modes + +`LoanPay` declares `tfLoanPayMask` as its flags mask. Three behavioural modifiers are valid: `tfLoanLatePayment`, `tfLoanFullPayment`, and `tfLoanOverpayment`. A key design invariant — enforced with `std::popcount` in `preflight` — is that these flags are **mutually exclusive**. A borrower may choose exactly one payment mode per transaction. The static assert confirms that these three bits together cover exactly the bits not in `tfLoanPayMask | tfUniversal`, providing a compile-time guarantee that the mask and the flags stay in sync. Regular scheduled payments use no flag at all. + +### `preflight` — Structural Validity + +Two field checks run before signature verification: the `sfLoanID` field must be non-zero, and `sfAmount` must be positive. Flag mutual-exclusivity is confirmed here. These are pure structural checks; no ledger state is consulted. + +--- + +## Custom Fee Calculation + +`calculateBaseFee` is the most algorithmically interesting method in the header. The normal fee covers a single payment; but when a borrower submits a large amount — implying multiple installments will be processed in one transaction — the ledger charges proportionally more. + +The logic reads the loan's `sfPaymentRemaining`, checks whether the payment is late (which caps it at one installment's work), then estimates `numPaymentEstimate = amount / regularPayment`. Fees are charged at one base unit per `loanPaymentsPerFeeIncrement` payments, rounded up. The rationale is computational fairness: processing ten amortization steps costs the network roughly ten times the work. If the loan or vault objects cannot be found, the method falls back to the normal fee and lets `preclaim` produce the authoritative error. + +--- + +## `preclaim` — Ledger State Validation + +`preclaim` loads the `Loan`, `LoanBroker`, and `Vault` objects from the read-only ledger view and enforces: + +- **Ownership**: Only the loan's `sfBorrower` may submit this transaction (`tecNO_PERMISSION`). +- **Overpayment permission**: If `tfLoanOverpayment` is set but the loan's `lsfLoanOverpayment` flag is absent, the transaction fails. The error code is `tecNO_PERMISSION` when `fixSecurity3_1_3` is enabled, or the legacy `temINVALID_FLAG` otherwise — a versioned correction to historical behaviour. +- **Loan completeness**: If `sfPaymentRemaining == 0` or `sfPrincipalOutstanding == 0`, the loan is already fully discharged (`tecKILLED`). +- **Asset consistency**: The transaction's `sfAmount` asset must match the vault's `sfAsset`. +- **Freeze and authorization**: Both the borrower account and the vault's pseudo-account are checked for frozen and deep-frozen states; the borrower must also hold appropriate authorization to transact the asset. +- **Balance sufficiency**: The borrower must hold at least the full submitted amount, even if the actual payment applied consumes less. Partial payment semantics are explicitly rejected — if the transaction specifies amount X, the account must have X available. + +The `LCOV_EXCL_*` markers on the "vault does not exist" and "broker does not exist" paths confirm that referential integrity between Loan → LoanBroker → Vault is maintained by the protocol and treated as invariant. + +--- + +## `doApply` — Three-Object State Machine + +`doApply` coordinates simultaneous mutations across three mutable ledger objects (Loan, LoanBroker, Vault) and one or two fund movements. + +**Impairment unwind**: If the loan carries `lsfLoanImpaired`, `LoanManage::unimpairLoan` restores the loan's tracked fields to their pre-impairment state before any payment arithmetic runs. If unimpairing fails, the transaction aborts and the sandbox discards all changes. + +**Payment type dispatch**: The transaction flags determine which `LoanPaymentType` enum value (`regular`, `late`, `full`, `overpayment`) is passed to `loanMakePayment`. This function (defined in `LendingHelpers`) executes the amortization math and modifies the `loanSle` fields in place, returning a `LoanPaymentParts` structure describing how the payment breaks down into `principalPaid`, `interestPaid`, `feePaid`, and `valueChange`. + +**Broker fee routing**: Before moving funds, `doApply` decides whether the broker's service fee goes to the **broker owner** or to the **broker's pseudo-account** (the first-loss cover pool). The decision weighs: +1. Whether cover available ≥ minimum required (computed conservatively using upward rounding against `coverRateMinimum * debtTotal`). +2. Whether the broker owner is deep-frozen for the asset. +3. Whether the broker owner holds the required authorization. + +If all three conditions are satisfied, the fee flows to the owner's personal account; otherwise it accumulates in the broker pseudo-account as cover capital. This prevents a single frozen/unauthorized state from blocking an otherwise valid repayment. + +**Vault accounting**: `assetsAvailable` increases by `totalPaidToVaultRounded` (principal + interest, rounded down to vault scale to avoid crediting fractions smaller than the vault can represent). `assetsTotal` adjusts by `paymentParts.valueChange`, which is non-zero only for late, full, and overpayment modes. The broker's `sfDebtTotal` is reduced by `totalPaidToVaultForDebt = totalPaidToVaultRaw - valueChange`, ensuring that value changes that alter the loan's outstanding balance are correctly reflected in the broker's aggregate exposure. `adjustImpreciseNumber` re-rounds this field to vault scale and floors it at zero to absorb accumulated rounding drift when a broker carries loans at differing scales. + +**Fund movement**: A single `accountSendMulti` call transfers funds from the borrower to two destinations — the vault pseudo-account and the broker payee — atomically and without transfer fees (`WaiveTransferFee::Yes`). Transfer fee waiver is intentional: the lending protocol operates on asset amounts computed by the amortization schedule, and imposing an extra transfer fee would distort those calculations. + +**Debug conservation checks**: In `NDEBUG`-disabled builds, a series of `XRPL_ASSERT_PARTS` calls verify that the sum of borrower, vault, and broker balances is identical before and after the transfer, that no balance goes negative, and that the vault's `sfAssetsAvailable` field exactly matches the actual token balance held by the vault pseudo-account. These assertions catch rounding or accounting bugs without affecting production performance. + +--- + +## Design Notes + +`ConsequencesFactory{Normal}` means a failed `LoanPay` does not block subsequent transactions in the same account queue — the transactor does not guarantee state changes that would make later transactions impossible. The custom fee calculation is a deliberate departure from the base class's flat fee, motivated by ensuring that users who batch many installments into one large payment are not subsidised by the rest of the network. The strict no-partial-payment rule in `preclaim` simplifies the invariant that `doApply` can assume: the submitted amount is always fully available, so no fallback logic is needed to handle a borrower who runs out of funds mid-computation. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanSet.h.ai.json b/include/xrpl/tx/transactors/lending/LoanSet.h.ai.json new file mode 100644 index 0000000000..bae67e7e3d --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanSet.h.ai.json @@ -0,0 +1,116 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + }, + { + "lineno": 24, + "name": "ctx" + }, + { + "lineno": 27, + "name": "view" + }, + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 33, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 8, + "name": "LoanSet" + } + ], + "description": "Defines the LoanSet transactor class for handling loan-related transactions in the XRPL lending protocol, including configuration constants and transaction lifecycle methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/lending/LoanSet.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "LoanSet" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 24, + "name": "checkSign" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 27, + "name": "calculateBaseFee" + }, + { + "args": [], + "lineno": 30, + "name": "getValueFields" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 33, + "name": "preclaim" + }, + { + "args": [], + "lineno": 36, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/lending/LoanSet.h.ai.md b/include/xrpl/tx/transactors/lending/LoanSet.h.ai.md new file mode 100644 index 0000000000..6ba05cbd63 --- /dev/null +++ b/include/xrpl/tx/transactors/lending/LoanSet.h.ai.md @@ -0,0 +1,39 @@ +# `LoanSet.h` — Loan Creation Transactor for the XRPL Lending Protocol + +## Role and Context + +`LoanSet` is the transactor that creates a new loan object on the XRP Ledger, implementing the XLS-66 lending protocol. It sits in the `include/xrpl/tx/transactors/lending/` module alongside `LoanPay`, `LoanDelete`, `LoanManage`, and the `LoanBroker*` family of transactors. Where `LoanPay` services an existing loan and `LoanDelete` terminates one, `LoanSet` is the origination step: it validates loan terms, computes the full amortization schedule, transfers principal from a vault to a borrower, deducts any origination fee, and creates the persistent `Loan` ledger entry that all subsequent transactions operate against. + +The header is deliberately thin — it declares the class interface and its protocol constants, while the full logic lives in `LoanSet.cpp`. + +## Inheritance and the Static-Polymorphism Transactor Pattern + +`LoanSet` inherits from `Transactor` and uses the same static compile-time polymorphism that the entire transactor framework employs. The base class `invokePreflight` template calls `T::checkExtraFeatures`, `T::getFlagsMask`, `T::preflight`, and `T::preflightSigValidated` by name — no vtable, just name hiding. `LoanSet` overrides each of the meaningful hooks while the base class provides safe no-op defaults for the rest. `ConsequencesFactory` is set to `Normal`, meaning the transaction does not unconditionally block later transactions from an account (as opposed to the `Blocker` variant used by escrow or account deletions). + +## Transaction Lifecycle + +**`checkExtraFeatures`** delegates directly to `checkLendingProtocolDependencies`, which verifies that all XLS-66 prerequisite amendments are active. This is the canonical way to gate an entire sub-protocol behind a feature flag without scattering amendment checks throughout the preflight body. + +**`getFlagsMask`** returns `tfLoanSetMask`, restricting which transaction flags are valid. Only the `tfLoanOverpayment` flag is meaningful here (it controls whether the borrower is permitted to pay ahead of schedule); any other flag bits are rejected in `preflight0`. + +**`preflight`** performs the stateless validation pass. It enforces a critical design requirement of the lending protocol: every `LoanSet` must carry a second cryptographic signature from the counterparty (the broker's owner), because a loan binds both the lender's vault and the borrower. The one exception is when the transaction travels inside a `Batch` inner transaction (`tfInnerBatchTxn`), where batch-level authorization replaces the counterparty signature. Rate fields (`sfInterestRate`, `sfLateInterestRate`, `sfCloseInterestRate`, etc.) are range-checked against protocol maxima. The `sfGracePeriod` must fall between `defaultGracePeriod` (60 seconds) and the `paymentInterval`, and `paymentInterval` itself must be at least `minPaymentInterval` (60 seconds) — enforced via the public constants on the class. A zero `sfLoanBrokerID` is unconditionally rejected here rather than waiting for a ledger lookup failure. + +**`checkSign`** extends the base-class signature check with a second verification for the counterparty. The counterparty identity is resolved lazily: the transaction may provide `sfCounterparty` explicitly, or it falls back to reading the broker's `sfOwner` from the current ledger view. The actual cryptographic check — which supports both single signatures and multisignatures via the `sfCounterpartySignature` object — is then delegated back to `Transactor::checkSign` with the resolved identity. + +**`calculateBaseFee`** prices the extra cryptographic work: each signer in the `sfCounterpartySignature` (whether a single signer or each member of a multisig quorum) adds one base fee unit on top of the normal transaction cost. This directly parallels how the base class charges for `sfSigners` in multisig transactions. + +**`getValueFields`** returns a static list of `STNumber` fields — principal, origination fee, service fee, late payment fee, close payment fee — that must be representable without precision loss in the vault's asset type. This list is used in two places: `preclaim` checks coarse representability before computing the loan scale, and `doApply` re-checks after the final scale is known (IOU types, where the required decimal precision can push a value below the type's resolution, can fail the second check even if they pass the first). + +**`preclaim`** performs ledger-state-dependent validation. A notable early guard checks that the arithmetic of the payment schedule cannot overflow `uint32_t` timestamps — the final grace period deadline is `startDate + (interval × total) + grace`, and if any intermediate value would exceed `std::numeric_limits::max()`, the transaction is killed with `tecKILLED` before loading any objects. Later checks confirm the broker exists, that the vault has sufficient available assets, that the vault has not hit its asset maximum, and that neither the vault pseudo-account, the broker pseudo-account, the borrower, nor the broker owner is frozen for the loan asset. + +**`doApply`** is where the loan comes into existence. It first recomputes `computeLoanProperties` (including the full amortization schedule: periodic payment, total value outstanding, management fees) and then validates that the interest component would not push the vault over its asset ceiling. It checks that the broker's `sfDebtTotal` would not exceed `sfDebtMaximum` and that available first-loss capital (`sfCoverAvailable`) still meets the `sfCoverRateMinimum` after the new loan is added — with the minimum cover rounded upward, deliberately erring on the side of the broker's solvency. Principal is disbursed from the vault pseudo-account with a single `accountSendMulti` call: `(principalRequested - originationFee)` goes to the borrower, and `originationFee` goes to the broker owner, both in one atomic operation. Holdings (trust lines or MPT holdings) for the borrower and broker owner are created on demand if absent. After the `Loan` SLE is inserted and all fields populated, the vault's `sfAssetsAvailable` and `sfAssetsTotal` are updated in tandem, the broker's `sfDebtTotal` and `sfLoanSequence` are incremented, and the loan is linked into both the broker pseudo-account's directory and the borrower's owner directory. + +## Protocol Constants + +The three public `constexpr` values encode the minimum viable loan configuration and the relationship between timing parameters: + +- `minPaymentInterval = 60` and `defaultPaymentInterval = 60` establish one minute as the floor for payment cadence, preventing loans that fire faster than ledger close times can reliably track. +- `defaultGracePeriod = 60` must be at least `minPaymentInterval` — enforced by a `static_assert` — since a grace period shorter than the payment interval could produce negative-time schedules. +- `minPaymentTotal = 1` and `defaultPaymentTotal = 1` allow single-payment (balloon) loans as the degenerate case. + +These constants are referenced directly in both `preflight` (for validation bounds) and `preclaim` (as defaults when optional fields are absent), so they function as the authoritative source of truth for the payment schedule floor throughout the lending protocol. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h.ai.json b/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h.ai.json new file mode 100644 index 0000000000..61571ede3b --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h.ai.json @@ -0,0 +1,123 @@ +{ + "args": [ + { + "lineno": 9, + "name": "from" + }, + { + "lineno": 9, + "name": "to" + }, + { + "lineno": 9, + "name": "amount" + }, + { + "lineno": 13, + "name": "offer" + }, + { + "lineno": 17, + "name": "buy" + }, + { + "lineno": 17, + "name": "sell" + }, + { + "lineno": 21, + "name": "buyer" + }, + { + "lineno": 21, + "name": "seller" + }, + { + "lineno": 21, + "name": "nfTokenID" + }, + { + "lineno": 27, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "NFTokenAcceptOffer" + } + ], + "description": "Defines the NFTokenAcceptOffer transaction processor for handling NFT offer acceptance in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h", + "functions": [ + { + "args": [ + "from", + "to", + "amount" + ], + "lineno": 9, + "name": "pay" + }, + { + "args": [ + "offer" + ], + "lineno": 13, + "name": "acceptOffer" + }, + { + "args": [ + "buy", + "sell" + ], + "lineno": 17, + "name": "bridgeOffers" + }, + { + "args": [ + "buyer", + "seller", + "nfTokenID" + ], + "lineno": 21, + "name": "transferNFToken" + }, + { + "args": [ + "ctx" + ], + "lineno": 27, + "name": "NFTokenAcceptOffer" + }, + { + "args": [ + "ctx" + ], + "lineno": 32, + "name": "preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 36, + "name": "preclaim" + }, + { + "args": [], + "lineno": 40, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h.ai.md b/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h.ai.md new file mode 100644 index 0000000000..63ddd654f5 --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenAcceptOffer.h.ai.md @@ -0,0 +1,37 @@ +# `NFTokenAcceptOffer.h` — NFT Offer Acceptance Transactor + +This header declares `NFTokenAcceptOffer`, the transaction processor responsible for settling NFT trades on the XRP Ledger. It inherits from `Transactor` and implements the standard three-phase execution model — `preflight`, `preclaim`, `doApply` — while adding four private helpers that encapsulate the financial mechanics of an NFT sale. + +## Role in the NFT Transaction Family + +Within the `transactors/nft/` directory, the related transaction types (`NFTokenMint`, `NFTokenBurn`, `NFTokenCreateOffer`, `NFTokenCancelOffer`, `NFTokenModify`) each deal with a single actor and a single ledger object. `NFTokenAcceptOffer` is the most complex because a sale may involve up to four distinct parties simultaneously: the buyer, the seller, the NFT issuer collecting a royalty, and an optional broker. The interface reflects that complexity with helpers for routing payments separately from the NFT ownership transfer. + +## Operation Modes: Direct vs. Brokered + +The transaction supports two mutually exclusive logical modes: + +**Direct mode** supplies exactly one of `sfNFTokenBuyOffer` or `sfNFTokenSellOffer`. The transaction submitter is either the NFT owner selling directly into a buy offer, or a buyer purchasing directly from a sell offer. The `acceptOffer()` helper handles this path, computing the issuer's royalty cut before forwarding the remainder to the seller, then calling `transferNFToken()`. + +**Brokered mode** supplies both offer IDs. The submitting account is a broker who matches a buyer's bid against a seller's ask without owning the token. The broker may also claim a fee via `sfNFTokenBrokerFee`. Payment sequencing in this mode, implemented inline in `doApply()`, is architecturally deliberate: the broker collects their cut first, the issuer's royalty is computed on what remains, and the seller receives whatever is left after both deductions. Computing the royalty before removing the broker's fee would allow total payouts to exceed what the buyer authorised — a correctness invariant enforced by this ordering. + +## Preflight and Preclaim Validation + +`preflight()` performs stateless sanity checks: at least one offer ID must be present, and `sfNFTokenBrokerFee` is only valid in brokered mode and must be strictly positive. These rules are enforced before any ledger reads. + +`preclaim()` is the heavy validation stage. For each offer it verifies existence, that the offer's amount field is non-negative, and handles expiration. Pre-`fixExpiredNFTokenOfferRemoval`, expired offers return `tecEXPIRED` immediately from `preclaim`, leaving the ledger object stranded. After the amendment, `preclaim` allows expired offers through to `doApply`, where they are deleted before returning `tecEXPIRED` — guaranteeing proper garbage collection. + +In brokered mode, `preclaim` also confirms that the two offers reference the same token ID and the same payment asset, that the buyer's bid is at least as large as the seller's ask (and also at least as large as the seller's ask plus the broker's fee), and that neither party is trying to sell to themselves. In direct mode, it checks that the submitter actually owns the token (for buy-offer acceptance) or has sufficient funds (for sell-offer acceptance). + +Trust-line hygiene is incrementally enforced through two amendments. `fixEnforceNFTokenTrustline` prevents the issuer from being granted an unintended trust line if a royalty is due but no line exists. `fixEnforceNFTokenTrustlineV2` extends this to check that every payment recipient — seller, buyer, broker, and issuer — has an authorised, non-deep-frozen trust line for the IOU being transferred. + +## The `pay()` Helper and Post-Transfer Balance Checks + +Rather than calling `accountSend` directly, all money movement inside this transactor goes through `pay()`. After a successful transfer it re-checks both the sender's and receiver's balances with `accountFunds`. A successful `accountSend` can still leave a balance negative in pathological IOU-with-transfer-fee scenarios; the post-payment assertion in `pay()` catches this and aborts with `tecINSUFFICIENT_FUNDS` before the ledger write is committed. + +## `transferNFToken()` and the Reserve Invariant + +Ownership transfer is a two-step ledger operation: `nft::removeToken` strips the token from the seller's NFToken page, then `nft::insertToken` places it into the buyer's. After insertion, if `fixNFTokenReserve` is enabled and the buyer's owner count increased (meaning a new page was allocated), the method checks whether the buyer's current balance satisfies the reserve requirement for the new count. Using the current balance rather than `preFeeBalance_` is necessary here because the buyer may have already paid for the token — using the pre-fee balance would overstate their available reserve and allow an under-reserved purchase. + +## Design Constraints + +`ConsequencesFactory` is set to `Normal`, meaning this transaction does not impose any extraordinary sequencing constraints on other transactions in the same ledger. The constructor simply forwards `ApplyContext` to the base class `Transactor`, which holds the view, journal, and account state for the duration of apply. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenBurn.h.ai.json b/include/xrpl/tx/transactors/nft/NFTokenBurn.h.ai.json new file mode 100644 index 0000000000..84b1fb4707 --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenBurn.h.ai.json @@ -0,0 +1,54 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "NFTokenBurn" + } + ], + "description": "Defines the NFTokenBurn transactor class for handling the burning (destruction) of NFTs within the XRPL transaction processing framework.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/nft/NFTokenBurn.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "NFTokenBurn" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 21, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenBurn.h.ai.md b/include/xrpl/tx/transactors/nft/NFTokenBurn.h.ai.md new file mode 100644 index 0000000000..a6be57ee5c --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenBurn.h.ai.md @@ -0,0 +1,39 @@ +# `NFTokenBurn.h` — NFT Burn Transactor Declaration + +## Role in the System + +`NFTokenBurn.h` declares the `NFTokenBurn` transactor, which handles the `NFTokenBurn` transaction type on the XRP Ledger. Its sole responsibility is to permanently destroy an NFT — removing it from the ledger state, cancelling any outstanding offers associated with it, and bookkeeping the issuer's burned-token count. It sits in the `xrpl/tx/transactors/nft/` directory alongside the other NFT-lifecycle transactors (`NFTokenMint`, `NFTokenCreateOffer`, `NFTokenAcceptOffer`, etc.) and is a direct subclass of `Transactor`. + +## Class Design and Three-Phase Processing + +Like every transactor in the XRPL framework, `NFTokenBurn` participates in a strict three-phase pipeline: `preflight` → `preclaim` → `doApply`. The base class `Transactor` drives this via its `invokePreflight` template and `operator()`, using compile-time polymorphism through name hiding (not virtual dispatch) for the static phases. + +`ConsequencesFactory` is set to `Normal`, meaning this transaction has standard fee and sequence-number consequences — it does not block later transactions from the same account (`Blocker`) and doesn't require custom consequence computation (`Custom`). This is appropriate because a burn has no unusual side-effects on the sender's transaction queue. + +The constructor simply forwards `ApplyContext&` to the `Transactor` base, as is standard for the NFT transactor family. + +## `preflight` — Intentionally Trivial + +The `preflight` implementation returns `tesSUCCESS` unconditionally. This is deliberate: there are no transaction-field-level validity checks specific to an NFT burn that need to run stateless (before ledger access). Generic checks such as fee validation, sequence number format, and signature key well-formedness are handled by `preflight1` and `preflight2` inside `invokePreflight`, so `NFTokenBurn::preflight` has nothing left to do. Comparing this to `NFTokenMint`, which overrides `checkExtraFeatures` and `getFlagsMask` to enforce amendment gates and flag constraints, a burn carries no such concerns. + +## `preclaim` — Authorization Enforcement + +The substantive validation happens in `preclaim`, which has read-only access to the ledger. Two checks are performed: + +**Token existence:** `nft::findToken` searches the owner's NFToken page for the specific `NFTokenID`. If absent, `tecNO_ENTRY` is returned — the fee is still consumed, but the transaction fails gracefully without modifying state. + +**Permission logic:** The owner of a token can always burn it. If the transaction sender (`sfAccount`) differs from the token's current holder (`sfOwner` field, if present), the code checks whether the `flagBurnable` bit is set in the token's embedded flags. Without it, `tecNO_PERMISSION` is returned. Even with `flagBurnable` set, only the issuer — or an account designated as the issuer's `sfNFTokenMinter` — may destroy a token they do not own. This layered check reflects the real-world analogy: a token's creator can reserve the right to recall or destroy tokens they issued, but cannot unilaterally destroy tokens belonging to arbitrary parties without the `flagBurnable` flag being encoded at mint time (making the rule immutable post-issuance). + +## `doApply` — State Mutation + +`doApply` commits the burn in three steps: + +1. **Token removal:** `nft::removeToken` strips the NFToken from its owner's NFTokenPage SLE. A post-condition assert guards against the case where the token disappeared between `preclaim` and `doApply` (which should be impossible under normal consensus, hence the comment "should never happen"). + +2. **Issuer accounting:** The issuer account's `sfBurnedNFTokens` counter is incremented. This field is optional (`~sfBurnedNFTokens`), so `value_or(0)` seeds the count if this is the first token burned by that issuer. This counter is informational ledger state that wallets and explorers can surface. + +3. **Offer cleanup:** All sell and buy offers keyed to the now-destroyed `NFTokenID` are deleted, subject to a hard cap of `maxDeletableTokenOfferEntries` (500 total). Sell offers are prioritized over buy offers, since the sell-offer directory is expected to be smaller and its cleanup frees more critical ledger resources. Any remaining budget after sell-offer deletion is applied to buy offers. This bounded cleanup is a deliberate performance contract: a burn transaction cannot be weaponized to consume unbounded computation by accumulating thousands of offers against a token, and any offers beyond the cap simply remain in the ledger until other cleanup mechanisms handle them. + +## Relationship to Other Files + +The header depends only on ``, keeping the interface minimal. The implementation (`NFTokenBurn.cpp`) pulls in `NFTokenHelpers.h` for `nft::findToken`, `nft::removeToken`, `nft::removeTokenOffersWithLimit`, `nft::getFlags`, and `nft::getIssuer` — all ledger-level helpers that abstract the NFTokenPage data structure. Compared to sibling transactors, `NFTokenBurn` is notably lean: it does not override `checkExtraFeatures` (so the amendment gate is handled by the `Permission` registry lookup in `invokePreflight`) and does not override `getFlagsMask` (there are no burn-specific transaction flags). \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h.ai.json b/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h.ai.json new file mode 100644 index 0000000000..d4a763a573 --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "NFTokenCancelOffer" + } + ], + "description": "Defines the NFTokenCancelOffer transactor class for handling the cancellation of NFT offers in the XRPL system.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "NFTokenCancelOffer" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 21, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h.ai.md b/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h.ai.md new file mode 100644 index 0000000000..45e8d86836 --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenCancelOffer.h.ai.md @@ -0,0 +1,42 @@ +# `NFTokenCancelOffer` Transactor + +## Role in the System + +`NFTokenCancelOffer` is the transactor responsible for removing one or more outstanding `NFTokenOffer` ledger objects — the buy or sell offers created via `NFTokenCreateOffer`. It lives in the `nft/` transactor subdirectory alongside the rest of the NFT lifecycle operations (`NFTokenMint`, `NFTokenBurn`, `NFTokenCreateOffer`, `NFTokenAcceptOffer`, `NFTokenModify`), and it follows the same three-phase pipeline that all XRPL transactors use: `preflight` → `preclaim` → `doApply`. + +## Class Design + +`NFTokenCancelOffer` inherits from `Transactor` and adds no new data members. Construction simply forwards the `ApplyContext` to the base class. The `static constexpr ConsequencesFactory{Normal}` tag tells the framework that this transaction has ordinary reserve consequences — it does not block other transactions from the same account (`Blocker`) and does not need a custom consequences calculation (`Custom`). + +The absence of a `getFlagsMask` override (unlike `NFTokenCreateOffer`, which does define one) means this transactor accepts only the universal transaction flags; there are no NFT-cancel–specific flag bits. + +## Preflight: Stateless Validity Checks + +`preflight` runs before any ledger state is consulted and performs two lightweight structural checks on the `sfNFTokenOffers` vector: + +1. **Bounds check** — the list must be non-empty and must not exceed `maxTokenOfferCancelCount` (defined as 500 in `Protocol.h`). An empty list has no meaningful purpose, and an oversized list is rejected as a denial-of-service vector: transactions grow with the number of offer IDs, and processing 500 deletions per transaction is already a generous upper bound. + +2. **Duplicate check** — the list is sorted and scanned for adjacent duplicates via `std::adjacent_find`. Any duplicate returns `temMALFORMED`. This prevents a submitter from padding a transaction with repeated IDs to waste validator CPU or artificially inflate fee refunds, and it is the reason the check sorts a *copy* of the IDs rather than the original field. + +## Preclaim: Permission Verification Against Live Ledger State + +`preclaim` verifies that the submitting account is entitled to cancel every offer in the list. It iterates the IDs and for each one applies the following logic, short-circuiting on the first forbidden entry: + +- **Missing offer** — if the ledger object does not exist, the offer was already consumed or cancelled; silently skip it. This makes the operation idempotent with respect to already-gone offers. +- **Wrong object type** — if an ID resolves to a ledger object that is *not* an `ltNFTOKEN_OFFER`, the submitter has no permission. This guards against a submitter passing an ID that belongs to a different object type (an escrow, a check, etc.). +- **Expired offer** — any account may cancel an expired offer regardless of ownership, so the check returns `false` (allowed) immediately via `hasExpired`. +- **Owner or designated recipient** — the offer's `sfOwner` is always allowed to cancel their own offer. The optional `sfDestination` field, if present and matching the submitting account, grants the same right — a designated counterparty can withdraw from a directed offer. + +If any offer in the list fails all of the above permission checks, `preclaim` returns `tecNO_PERMISSION`, preventing the transaction from being applied. + +## doApply: Ledger Mutation + +`doApply` iterates the same offer ID list and calls `nft::deleteTokenOffer` on each live entry (peered via `keylet::nftoffer`). Missing offers are skipped silently (consistent with `preclaim`'s idempotency stance). If deletion fails for any offer — a situation deemed impossible under correct invariants and therefore excluded from code coverage — the transactor logs a fatal message and returns `tefBAD_LEDGER`, which signals internal ledger corruption rather than a user error. + +## Design Observations + +The split between `preclaim` and `doApply` is architecturally deliberate: `preclaim` reads ledger state through a `ReadView` (no mutation possible) and decides whether the fee will be claimed; `doApply` mutates through an `ApplyView`. This separation means the ledger is never partially modified when a permission error is detected. + +The duplicate-rejection in `preflight` is particularly worth noting: it uses a sort-then-adjacent-find pattern on a local copy, which is O(n log n) but bounded by 500 elements and completely stateless. Sorting the original field object would be incorrect (fields are conceptually immutable at this stage), so the copy is necessary. + +The permission model for offer cancellation is deliberately permissive compared to most ledger mutations: expiry removes access control entirely, and the designated destination can cancel just as easily as the creator. This reflects the NFT design goal that expired and directed offers should be easy to clean up without requiring the original creator to be online. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h.ai.json b/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h.ai.json new file mode 100644 index 0000000000..b09df4ac6a --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "NFTokenCreateOffer" + } + ], + "description": "Defines the NFTokenCreateOffer transactor class for creating NFT offers in the XRPL transaction processing system.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "NFTokenCreateOffer" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h.ai.md b/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h.ai.md new file mode 100644 index 0000000000..b945a3a7b6 --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenCreateOffer.h.ai.md @@ -0,0 +1,58 @@ +# `NFTokenCreateOffer.h` — NFT Offer Creation Transactor + +This header declares `NFTokenCreateOffer`, one of the six NFT-related transactor classes in the `xrpl/tx/transactors/nft/` directory. It handles transaction type `ttNFTOKEN_CREATE_OFFER` (type code 27), which places either a sell or buy offer for an NFT into the ledger's offer directory. The class is deliberately thin — the header is purely a declaration, and the implementation in the paired `.cpp` file delegates almost all work to shared helper functions in `NFTokenHelpers`. + +## The Three-Phase Transactor Pipeline + +`NFTokenCreateOffer` fits the standard `Transactor` lifecycle used throughout the XRPL codebase: + +1. **`preflight`** — stateless structural validation against the serialized transaction only. +2. **`preclaim`** — read-only ledger checks after signature verification. +3. **`doApply`** — ledger mutation. + +Each of these is invoked by the framework via compile-time polymorphism: `Transactor::invokePreflight` calls `T::preflight`, `T::getFlagsMask`, etc. using name hiding rather than virtual dispatch. This means the base class `preclaim` (which returns `tesSUCCESS` unconditionally) is properly overridden here because the framework calls `NFTokenCreateOffer::preclaim` by type, not through a vtable. Only `doApply` uses virtual dispatch and requires `override`. + +`ConsequencesFactory{Normal}` declares that this transaction follows standard fee-claiming behavior — it can claim a fee even when it fails, unlike a `Blocker` transaction that would hold up a multi-transaction sequence. + +## Flag Masking + +`getFlagsMask()` overrides the base class (which returns `tfUniversalMask`) to return `tfNFTokenCreateOfferMask`. This mask defines which transaction flags are legal for this type. The framework calls `preflight1()` passing this mask, which in turn calls `preflight0()` to reject any transaction carrying unrecognized flags — preventing a submitter from setting arbitrary bits that might be interpreted differently by future amendments. + +The primary relevant flag is `tfSellNFToken`. When set, the submitter is offering to sell an NFT they own. When unset, the submitter is offering to buy an NFT from its current owner. This distinction is central to the `preclaim` logic. + +## Delegation to Shared Helpers + +A key architectural decision is that all three implementation methods delegate to functions in `NFTokenHelpers` (`tokenOfferCreatePreflight`, `tokenOfferCreatePreclaim`, `tokenOfferCreateApply`). These same helpers are called by `NFTokenMint`, which can create an offer atomically as part of minting. Rather than duplicating validation logic — which would create a maintenance hazard where NFTokenMint and NFTokenCreateOffer could drift apart — the XRPL codebase factors all offer-creation logic into a single shared path. The transactor itself then acts as a thin adapter that extracts the relevant fields from `ctx_.tx` and passes them through. + +## `preclaim` Logic and the Buy/Sell Asymmetry + +`preclaim` performs two checks beyond what the shared helper does: + +First, it checks whether the offer has already expired before it can even be placed — `hasExpired(ctx.view, ctx.tx[~sfExpiration])`. This cannot be done in `preflight` because expiration is relative to ledger close time, which is part of ledger state, not available at stateless validation time. + +Second, it verifies that the NFT actually exists and is owned by the right account: + +```cpp +nft::findToken( + ctx.view, + ctx.tx[((txFlags & tfSellNFToken) != 0u) ? sfAccount : sfOwner], + nftokenID) +``` + +For sell offers, the submitter (`sfAccount`) must own the NFT. For buy offers, the `sfOwner` field must identify the current NFT holder. This asymmetry explains why `sfOwner` is an optional field on the transaction: it is irrelevant for sell offers and required for buy offers. + +## Transaction Fields + +The auto-generated `xrpl::transactions::NFTokenCreateOffer` wrapper (in `protocol_autogen/`) provides type-safe field access and confirms the on-wire structure: + +- `sfNFTokenID` (required): the 256-bit identifier of the NFT being offered. +- `sfAmount` (required): the price — zero is valid for a gift offer. +- `sfDestination` (optional): restricts acceptance to a specific counterparty. +- `sfOwner` (optional): for buy offers, identifies who currently holds the NFT. +- `sfExpiration` (optional): a ledger-time deadline after which the offer cannot be accepted. + +The transaction is marked delegable (`Delegation::delegable`), meaning another account may submit it on the owner's behalf if delegation is enabled via the relevant amendment. + +## Relationship to Sibling Classes + +All six NFT transactors (`NFTokenMint`, `NFTokenBurn`, `NFTokenCreateOffer`, `NFTokenAcceptOffer`, `NFTokenCancelOffer`, `NFTokenModify`) share the same directory and follow the same pattern of a thin header declaration plus a `.cpp` that delegates to helpers. `NFTokenCreateOffer` is the most closely related to `NFTokenMint` — both can create offer objects in the ledger, which is why they share the `tokenOfferCreate*` helper suite rather than each independently reimplementing identical validation paths. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenMint.h.ai.json b/include/xrpl/tx/transactors/nft/NFTokenMint.h.ai.json new file mode 100644 index 0000000000..3f97e94ba5 --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenMint.h.ai.json @@ -0,0 +1,115 @@ +{ + "args": [ + { + "lineno": 13, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + }, + { + "lineno": 24, + "name": "ctx" + }, + { + "lineno": 27, + "name": "ctx" + }, + { + "lineno": 33, + "name": "flags" + }, + { + "lineno": 33, + "name": "fee" + }, + { + "lineno": 33, + "name": "issuer" + }, + { + "lineno": 33, + "name": "taxon" + }, + { + "lineno": 33, + "name": "tokenSeq" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "NFTokenMint" + } + ], + "description": "Defines the NFTokenMint transactor class for minting NFTs on the XRPL, including methods for preflight checks, flag masking, and NFT ID creation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/nft/NFTokenMint.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 13, + "name": "NFTokenMint" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 24, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 27, + "name": "preclaim" + }, + { + "args": [], + "lineno": 29, + "name": "doApply" + }, + { + "args": [ + "std::uint16_t flags", + "std::uint16_t fee", + "AccountID const& issuer", + "nft::Taxon taxon", + "std::uint32_t tokenSeq" + ], + "lineno": 33, + "name": "createNFTokenID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenMint.h.ai.md b/include/xrpl/tx/transactors/nft/NFTokenMint.h.ai.md new file mode 100644 index 0000000000..515f184b95 --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenMint.h.ai.md @@ -0,0 +1,59 @@ +# `NFTokenMint.h` — NFT Minting Transactor + +`NFTokenMint` is the transactor that handles `NFTokenMint` transactions on the XRP Ledger. Its job is to create new non-fungible tokens, record them in the issuer's on-ledger state, and optionally produce an initial sell offer for the freshly-minted token in the same atomic step. The class lives inside `include/xrpl/tx/transactors/nft/` alongside the other NFT-specific transactors (`NFTokenBurn`, `NFTokenCreateOffer`, etc.) and follows the same lifecycle contract that every transactor in the system must satisfy. + +## Inheritance and Factory Type + +`NFTokenMint` publicly inherits from `Transactor`, which provides the common transaction-processing infrastructure: fee consumption, sequence-number management, signature checking, and the entry-point `operator()()`. The `ConsequencesFactory` is set to `Normal`, meaning the transaction claims a fee in the normal way — no special blocking or custom consequence logic is required. + +The `Transactor` base uses **compile-time polymorphism**, not virtual dispatch, for the preflight pipeline. The `invokePreflight` template calls `T::checkExtraFeatures`, `T::getFlagsMask`, `T::preflight`, and `T::preflightSigValidated` by name, resolved at compile time. Only `doApply()` uses a true virtual override. + +## Preflight Pipeline + +Three static methods participate in preflight and each covers a different concern. + +**`checkExtraFeatures`** guards the embedded-offer sub-feature. If the transaction contains `sfAmount`, `sfDestination`, or `sfExpiration` (the fields needed to simultaneously create a sell offer), it returns `false` unless the `featureNFTokenMintOffer` amendment is enabled. This ensures that the combined mint-and-offer flow cannot be activated on a network that hasn't voted for it. + +**`getFlagsMask`** is amendment-sensitive in a non-trivial way. The `tfTrustLine` flag was historically allowed to let the minting issuer opt into automatic trustline creation during NFT transfers, but this opened a denial-of-service attack: two cooperating accounts could trade an NFT back and forth, each transfer creating a new trustline on the issuer and unboundedly growing the issuer's reserve. The `fixRemoveNFTokenAutoTrustLine` amendment permanently disabled `tfTrustLine` minting. Because the valid flag mask therefore depends on which amendments are active, `getFlagsMask` checks both `fixRemoveNFTokenAutoTrustLine` and `featureDynamicNFT` (which adds the `tfMutable` flag for mutable-URI NFTs) and returns the appropriate bitmask from four possible combinations. + +**`preflight`** validates the transaction fields specific to minting: +- The `sfTransferFee` must not exceed `maxTransferFee`; if it is non-zero, `tfTransferable` must also be set (a non-transferable token with a transfer fee is contradictory). +- The `sfIssuer` field, when present, must not equal the signing account — the authorized-minter pattern requires the minter and issuer to be distinct. +- The `sfURI` must be non-empty and within `maxTokenURILength`. +- If offer fields are present, `sfAmount` is mandatory (destination and expiration alone without an amount make no sense), and validation is delegated to `nft::tokenOfferCreatePreflight()` from `NFTokenHelpers.h`, which is shared with `NFTokenCreateOffer`. + +## Preclaim + +`preclaim` performs the checks that need ledger state but no application-phase writes. It handles the authorized-minter check: when `sfIssuer` is present it reads the issuer's `AccountRoot` and verifies that `sfNFTokenMinter` matches the signing account. A missing issuer account returns `tecNO_ISSUER`; a mismatch returns `tecNO_PERMISSION`. If the transaction includes offer fields, it also invokes `nft::tokenOfferCreatePreclaim()` to check offer-specific ledger conditions (expiry, trustline authorization, deep-freeze status, etc.). + +## Token ID Construction — `createNFTokenID` + +The static `createNFTokenID` method is exposed publicly to enable unit testing. It packs five fields into a 32-byte big-endian buffer that becomes the `uint256` token ID: + +| Bytes | Content | +|-------|---------| +| 0–1 | Flags (2 bytes) | +| 2–3 | Transfer fee (2 bytes) | +| 4–23 | Issuer `AccountID` (20 bytes) | +| 24–27 | Ciphered taxon (4 bytes) | +| 28–31 | Token sequence number (4 bytes) | + +The taxon is **scrambled** before packing using `nft::cipheredTaxon()`, which applies a linear congruential transform keyed on the sequence number: `taxon ^ ((384160001 * tokenSeq) + 2459)`. This ensures that an issuer who mints many tokens with the same taxon does not pack them all into the same NFToken page, which would degrade lookup and deletion performance. The Hull-Dobell theorem guarantees that this transform is a permutation of the 32-bit integer space, so the scrambling is lossless and reversible. Crucially, those magic constants are **protocol-frozen** — changing them would break the ability to interpret existing token IDs, requiring a new amendment and a disambiguation mechanism. + +All fields are converted to big-endian before packing; the helper functions in `nft.h` (`getFlags`, `getTransferFee`, `getSerial`, `getTaxon`) reverse the process when reading a token ID. + +## Application Phase — `doApply` + +`doApply` performs all ledger mutations: + +1. **Sequence bookkeeping**: The issuer's `AccountRoot` tracks `sfFirstNFTokenSequence` and `sfMintedNFTokens`. On the very first mint, `sfFirstNFTokenSequence` is initialized to the issuer's current account sequence. There is a subtle edge case here: when a sequence-based (non-ticket) transaction is submitted by the issuer themselves, the sequence has already been pre-incremented by the time `doApply` runs, so the stored value must be decremented by one. When an authorized minter submits the transaction (or the issuer uses a ticket), the issuer's sequence is untouched and the raw value is used directly. Each subsequent mint increments `sfMintedNFTokens`; the token's unique sequence is `sfFirstNFTokenSequence + (sfMintedNFTokens - 1)`. Overflow is checked explicitly to return `tecMAX_SEQUENCE_REACHED` rather than wrap. + +2. **Token insertion**: The new `STObject` for the NFToken is assembled from the inner-object template registered for `sfNFToken`, populated with the computed ID and optional URI, then inserted via `nft::insertToken()`, which handles the underlying NFToken page structure. + +3. **Optional sell offer**: If `sfAmount` is present, `nft::tokenOfferCreateApply()` creates a sell offer for the new token in the same transaction, sharing all the offer-creation logic with `NFTokenCreateOffer`. + +4. **Reserve check**: The reserve is checked only if the owner count increased compared to its pre-mint value. This is intentional: packing additional NFTs into an existing page does not increase the owner count, so it should not require a reserve top-up. Only creating a new NFToken page (or a sell offer) triggers the reserve check, keeping the minting experience predictable for issuers operating near their reserve limit. + +## Shared Logic with NFTokenCreateOffer + +The three helper functions `nft::tokenOfferCreatePreflight`, `nft::tokenOfferCreatePreclaim`, and `nft::tokenOfferCreateApply` (declared in `NFTokenHelpers.h`) are consumed by both `NFTokenMint` and `NFTokenCreateOffer`. This avoids duplication of the offer-validation rules and ensures consistent behavior whether an offer is created standalone or bundled into a mint. `NFTokenMint` always passes `tfSellNFToken` as the transaction flags for these helpers because the embedded-offer path only supports sell offers — you cannot atomically mint and create a buy offer. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenModify.h.ai.json b/include/xrpl/tx/transactors/nft/NFTokenModify.h.ai.json new file mode 100644 index 0000000000..1df5a9d756 --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenModify.h.ai.json @@ -0,0 +1,49 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "NFTokenModify" + } + ], + "description": "Defines the NFTokenModify transactor class for handling NFT modification transactions in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/nft/NFTokenModify.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "NFTokenModify" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 21, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/nft/NFTokenModify.h.ai.md b/include/xrpl/tx/transactors/nft/NFTokenModify.h.ai.md new file mode 100644 index 0000000000..8d705b4e4a --- /dev/null +++ b/include/xrpl/tx/transactors/nft/NFTokenModify.h.ai.md @@ -0,0 +1,45 @@ +# `NFTokenModify.h` — NFT Modification Transactor + +## Role and Purpose + +`NFTokenModify.h` declares the `NFTokenModify` transactor, which handles the `NFTokenModify` ledger transaction — a purpose-built operation for updating the URI metadata of an existing, mutable NFT on the XRP Ledger. It sits alongside the other NFT transactors (`NFTokenMint`, `NFTokenBurn`, `NFTokenCreateOffer`, `NFTokenCancelOffer`, `NFTokenAcceptOffer`) in the `include/xrpl/tx/transactors/nft/` module, and follows the identical structural pattern used across all XRPL transaction types. + +## Class Design and Inheritance + +`NFTokenModify` inherits from `Transactor`, the abstract base class for all transaction processors in the XRPL codebase. `Transactor` enforces a three-phase processing pipeline: + +1. **`preflight`** — stateless, context-free validation run before even consulting the ledger. +2. **`preclaim`** — read-only ledger checks to determine whether the transaction can claim a fee. +3. **`doApply`** — the mutating phase that writes final state changes to the ledger. + +This pattern is enforced structurally: `preflight` and `preclaim` are `static` methods (they operate purely on their context argument, not on any instance state), while `doApply` is a virtual override that gains access to the mutable `ApplyView` through the inherited `ctx_` member. The constructor simply delegates to `Transactor(ctx)`, which is the norm for this family. + +## `ConsequencesFactory{Normal}` + +The `static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}` declaration tells the fee-claiming machinery that this transaction behaves in the standard way: it always claims its fee, and it neither blocks the account's sequence number queue (`Blocker`) nor computes custom consequences (`Custom`). For a URI modification that touches only the NFT page, this is exactly right. + +## What the Three Phases Do + +**`preflight`** performs two pure-data checks without touching ledger state: + +- If an `sfOwner` field is present and equals `sfAccount` (the transaction sender), the transaction is malformed. This guards against a degenerate form where a user redundantly specifies themselves as the owner; the field is only meaningful when a delegated minter is modifying an NFT they minted on behalf of the actual owner. +- If `sfURI` is present, it must be non-empty and no longer than `maxTokenURILength` (256 bytes as defined in `Protocol.h`). A present-but-empty URI would be ambiguous — is the caller trying to clear the URI or submitting a bug? The design forbids it. Omitting `sfURI` entirely remains the valid signal for a no-URI-change call. + +**`preclaim`** reads ledger state but does not modify it: + +- It resolves the effective owner: `sfOwner` if present, otherwise `sfAccount`. +- `nft::findToken` confirms the NFT exists in that owner's token directory; absent NFTs return `tecNO_ENTRY`. +- The NFT's `flagMutable` bit (bit 4 of the packed flags within the 256-bit token ID) is checked. This is critical: NFT immutability is an at-mint, permanent choice encoded directly into the token ID itself. If the flag is not set, `tecNO_PERMISSION` is returned immediately — no modification is possible. +- If the caller is not the original issuer (extracted from the token ID via `nft::getIssuer`), `preclaim` checks whether the `sfNFTokenMinter` field on the issuer's account SLE points to the caller. Only a designated minter may act as a proxy modifier. Any other account gets `tecNO_PERMISSION`. + +**`doApply`** is the state-mutation step. It resolves the owner (same `sfOwner`-or-`sfAccount` logic as `preclaim`) and delegates all ledger work to `nft::changeTokenURI(view(), owner, nftokenID, ctx_.tx[~sfURI])`, which navigates NFToken pages and writes the updated URI in place. + +## Relationship to NFT Protocol Helpers + +The implementation imports `` and ``. The helper `nft::findToken` and `nft::changeTokenURI` encapsulate the complex NFToken page navigation — NFTs are packed into shared ledger objects (`NFTokenPage` SLEs) sorted by token ID, and mutating them requires locating the right page and entry. By confining that logic to `NFTokenHelpers`, `NFTokenModify` stays focused on authorization and field validation, not data structure traversal. + +The `nft::getFlags` and `nft::getIssuer` functions decode fields directly from the 256-bit token ID, which encodes the issuer account ID, taxon, sequence number, and flags at fixed offsets. Permissions are therefore enforced without needing any separate on-ledger permission object — the token ID itself is the source of truth for mutability and issuer identity. + +## Absence of `checkExtraFeatures` and `getFlagsMask` + +Unlike `NFTokenMint`, which overrides both `checkExtraFeatures` (to gate on an amendment) and `getFlagsMask` (to declare supported transaction flags), `NFTokenModify` omits both. It inherits the base `Transactor` defaults: `checkExtraFeatures` returns `true` unconditionally, and `getFlagsMask` returns `tfUniversalMask`. This is appropriate for a transaction that defines no custom per-invocation flags and whose amendment gating (if any) is handled by the global permission registry checked in `invokePreflight` before the per-transactor hooks are called. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/oracle/OracleDelete.h.ai.json b/include/xrpl/tx/transactors/oracle/OracleDelete.h.ai.json new file mode 100644 index 0000000000..6f369cddce --- /dev/null +++ b/include/xrpl/tx/transactors/oracle/OracleDelete.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 20, + "name": "ApplyContext& ctx" + }, + { + "lineno": 24, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 27, + "name": "PreclaimContext const& ctx" + }, + { + "lineno": 33, + "name": "ApplyView& view" + }, + { + "lineno": 33, + "name": "std::shared_ptr const& sle" + }, + { + "lineno": 33, + "name": "AccountID const& account" + }, + { + "lineno": 33, + "name": "beast::Journal j" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 17, + "name": "OracleDelete" + } + ], + "description": "Implements the OracleDelete transactor for deleting Oracle objects in the XRPL, conforming to XLS-47d requirements.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/oracle/OracleDelete.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 20, + "name": "OracleDelete" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 24, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 27, + "name": "preclaim" + }, + { + "args": [], + "lineno": 30, + "name": "doApply" + }, + { + "args": [ + "ApplyView& view", + "std::shared_ptr const& sle", + "AccountID const& account", + "beast::Journal j" + ], + "lineno": 33, + "name": "deleteOracle" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/oracle/OracleDelete.h.ai.md b/include/xrpl/tx/transactors/oracle/OracleDelete.h.ai.md new file mode 100644 index 0000000000..acbb6c53a6 --- /dev/null +++ b/include/xrpl/tx/transactors/oracle/OracleDelete.h.ai.md @@ -0,0 +1,27 @@ +# `OracleDelete.h` — Oracle Deletion Transactor + +## Role in the System + +`OracleDelete` is the XRPL transactor responsible for removing Price Oracle objects from the ledger. It belongs to the Price Oracle feature defined by the XLS-47d specification, which allows accounts to publish off-chain price data (such as asset prices) on-chain so decentralized applications can consume them. The `OracleDelete` class pairs with `OracleSet` — together they cover the full lifecycle of an oracle object. + +Like all transactors, `OracleDelete` inherits from the `Transactor` base class and plugs into the three-phase transaction-processing pipeline: `preflight` (stateless sanity checks), `preclaim` (read-only ledger validation), and `doApply` (ledger mutation). The `ConsequencesFactory{Normal}` constant tells the framework to use standard fee and consequence computation for this transaction type. + +## The Three-Phase Pipeline + +**`preflight`** unconditionally returns `tesSUCCESS`. There is nothing to validate about a delete request without consulting ledger state — the transaction carries only an `sfOracleDocumentID` identifying which oracle to remove. All meaningful checks happen in `preclaim`. + +**`preclaim`** performs two read-only ledger lookups. First, it confirms the submitting account exists (the comment marks this path as unreachable under normal conditions since account existence is enforced earlier). Second, it derives the oracle's ledger key from `keylet::oracle(account, documentId)` and verifies the object exists; if not, it returns `tecNO_ENTRY` with a debug log. It also confirms the `sfOwner` field on the oracle SLE matches the submitting account — though again, this case is marked `LCOV_EXCL` because the key derivation itself embeds the account, making ownership mismatches structurally impossible through normal transaction flow. + +**`doApply`** is thin: it re-reads the oracle SLE via `ctx_.view().peek()` and delegates immediately to the static `deleteOracle` helper. + +## The `deleteOracle` Static Method + +The most architecturally notable feature of this header is the public static `deleteOracle(ApplyView&, SLE, AccountID, Journal)` method. Unlike `OracleSet`, which has no equivalent helper, `OracleDelete` exposes its core mutation logic as a free-standing static function. This matches the pattern of `Transactor::ticketDelete` in the base class — it exists so that other transactors (principally account deletion, which must clean up all objects owned by the account) can remove oracles without constructing a full `OracleDelete` transactor instance. + +The implementation of `deleteOracle` encodes a subtle ledger-reserve rule: oracle objects that contain more than five `sfPriceDataSeries` entries are charged two owner reserve slots instead of one when created. The deletion therefore adjusts the owner count by `-2` for large oracles and `-1` for smaller ones via `adjustOwnerCount`. This mirrors the creation logic in `OracleSet` and must remain in sync with it; the threshold of five entries is a protocol-level constant baked into XLS-47d. + +After adjusting the reserve count, `deleteOracle` removes the oracle from the account's owner directory with `view.dirRemove()` and then erases the SLE with `view.erase()`. A failure from `dirRemove` returns `tefBAD_LEDGER`, which signals an internal ledger consistency error — this path is also excluded from coverage because it indicates corruption rather than a user-facing error condition. + +## Relationship to `OracleSet` + +The two oracle transactors are mirror images in structure but differ in complexity. `OracleSet` handles both creation and update (idempotent upsert semantics), carries significant `preflight` validation (checking data types, sizes, and price data constraints), and has no reusable static helper. `OracleDelete` is intentionally minimal: deletion needs no field validation, and the static helper exists purely to support the account-deletion cleanup path that the broader transaction system requires. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/oracle/OracleSet.h.ai.json b/include/xrpl/tx/transactors/oracle/OracleSet.h.ai.json new file mode 100644 index 0000000000..2a76049373 --- /dev/null +++ b/include/xrpl/tx/transactors/oracle/OracleSet.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 22, + "name": "ctx" + }, + { + "lineno": 25, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 15, + "name": "OracleSet" + } + ], + "description": "Defines the OracleSet transactor class for creating or updating Oracle objects in the XRPL, conforming to XLS-47d.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/oracle/OracleSet.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 18, + "name": "OracleSet" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 22, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 25, + "name": "preclaim" + }, + { + "args": [], + "lineno": 28, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/oracle/OracleSet.h.ai.md b/include/xrpl/tx/transactors/oracle/OracleSet.h.ai.md new file mode 100644 index 0000000000..1d858ce1d8 --- /dev/null +++ b/include/xrpl/tx/transactors/oracle/OracleSet.h.ai.md @@ -0,0 +1,50 @@ +# `OracleSet.h` — Price Oracle Create/Update Transactor + +## Role in the System + +`OracleSet.h` declares the `OracleSet` transactor, which handles both creation and in-place update of Price Oracle ledger objects on the XRP Ledger. It is one of two oracle-related transactors in the `xrpl/tx/transactors/oracle/` directory — the counterpart being `OracleDelete`, which removes oracle objects. Together they implement the full lifecycle of on-ledger price feeds as specified by XLS-47d. + +A Price Oracle on the XRPL is an on-ledger object that stores a time-stamped series of token-pair prices submitted by an authorized account. It acts as a bridge between off-chain data sources (such as centralized exchange feeds) and decentralized applications that need price references without leaving the ledger. The `OracleSet` transactor makes this possible by providing a single transaction type that idempotently handles both initial creation and subsequent updates. + +## Transactor Base Class and Phase Architecture + +`OracleSet` publicly inherits from `Transactor` and participates in the standard three-phase validation pipeline defined by that base class: `preflight`, `preclaim`, and `doApply`. This separation is architectural: `preflight` operates without any ledger state and can run on any node for mempool filtering; `preclaim` reads (but does not modify) the current ledger to catch state-dependent errors before claiming a fee; and `doApply` performs the actual mutation. All three phases are required — none are inherited from the base no-op implementations. + +The `ConsequencesFactory` is set to `Normal`, meaning this transaction competes for fee priority normally and does not act as a "blocker" that would prevent subsequent transactions from the same account from being processed in the same batch. + +## `preflight` — Stateless Validation + +The implementation checks purely structural constraints on the transaction fields without touching the ledger: + +- `sfPriceDataSeries` must be non-empty and must not exceed `maxOracleDataSeries` entries (checked with `temARRAY_EMPTY` / `temARRAY_TOO_LARGE`). +- The optional string fields `sfProvider`, `sfURI`, and `sfAssetClass` — if present — must have lengths within their respective maximums (`maxOracleProvider`, `maxOracleURI`, `maxOracleSymbolClass`) and must not be zero-length. + +The decision to leave deeper token-pair consistency checks to `preclaim` (rather than `preflight`) is intentional: those checks require reading the existing oracle object from the ledger, which is unavailable at preflight time. + +## `preclaim` — Ledger-State Validation + +`preclaim` does the heavy stateful validation that separates a creation from an update, and it enforces the time-freshness invariant that gives oracle data its utility: + +**Timestamp freshness.** `sfLastUpdateTime` is stored in XRPL epoch seconds (offset from the Unix epoch by `epoch_offset`). The implementation converts the transaction value back to Unix time and requires it to fall within `±maxLastUpdateTimeDelta` seconds of the last closed ledger's `closeTime`. Values in the far past or future are rejected with `tecINVALID_UPDATE_TIME`. On update, the new timestamp must also be strictly greater than the existing oracle's `sfLastUpdateTime`, preventing replay or backdated feeds. + +**Create vs. update divergence.** The code reads the oracle SLE keyed by `(account, sfOracleDocumentID)`. If the SLE is absent, this is a creation: `sfProvider` and `sfAssetClass` are mandatory, and any price data entry lacking `sfAssetPrice` is malformed (you cannot delete a pair from a non-existent object). If the SLE exists, it is an update: `sfProvider` and `sfAssetClass` may be omitted, but if supplied they must match the stored values (enforced by the `isConsistent` lambda) — these fields are immutable after creation. + +**Token pair accounting and reserve.** During update, the code resolves the final set of pairs by merging existing pairs with the transaction's additions, updates, and deletions. Pairs in the transaction without `sfAssetPrice` signal deletion; any such pair that does not exist in the current oracle is rejected with `tecTOKEN_PAIR_NOT_FOUND`. The total resulting pair count drives a tiered owner reserve: objects with more than 5 pairs consume 2 owner count units instead of 1. The `adjustReserve` delta between old and new tier is computed here, and the submitting account's balance is checked against the updated reserve before the fee is charged. + +## `doApply` — Ledger Mutation + +The apply phase mirrors the create/update branching from `preclaim`: + +**Update path.** All current pairs are loaded into a `std::map, STObject>` keyed by `tokenPairKey`. The transaction's entries are then applied in a loop: entries without `sfAssetPrice` erase the key, entries whose key already exists update the price and optional scale in-place, and new keys are inserted. The map is then serialized back as `sfPriceDataSeries`. This map-based approach naturally deduplicates and orders the pairs, ensuring the ledger object remains consistent regardless of the order entries were submitted. + +**Create path.** A new SLE is constructed and populated. Under the `fixPriceOracleOrder` amendment, the same `std::map` sorting approach is used for the initial `sfPriceDataSeries` so that the canonical on-ledger ordering is consistent from creation onward; without this amendment the raw transaction order is preserved (legacy behavior). + +**Amendment-gated fields.** The `fixIncludeKeyletFields` amendment adds `sfOracleDocumentID` to the SLE on both creation and the first update of older objects. This amendment was a bug fix to ensure the document ID is always retrievable from the object itself without requiring callers to reconstruct it from the keylet. + +**Owner count.** On creation, the owner count is incremented by 1 or 2 (based on pair count). On update, only the delta between old and new tiers is applied. The owner directory is managed via `dirInsert` on creation; deletion is handled exclusively by the `OracleDelete` transactor. + +## Design Observations + +The use of `static` for `preflight` and `preclaim` (while `doApply` is a virtual override) reflects the XRPL framework's compile-time polymorphism pattern: `invokePreflight` and `invoke_preclaim` call these as static functions via template instantiation rather than virtual dispatch, avoiding vtable overhead during the validation phases where many transactions may be evaluated speculatively. The `doApply` method is virtual because it is invoked only once per committed transaction where the overhead is irrelevant. + +The tiered reserve design (1 vs. 2 owner count units at the 5-pair boundary) is a pragmatic ledger storage cost model: small oracles track a handful of pairs and are economical, while larger multi-pair oracles reflecting a broader price index carry proportionally higher on-ledger cost. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment/DepositPreauth.h.ai.json b/include/xrpl/tx/transactors/payment/DepositPreauth.h.ai.json new file mode 100644 index 0000000000..3368ad6ff1 --- /dev/null +++ b/include/xrpl/tx/transactors/payment/DepositPreauth.h.ai.json @@ -0,0 +1,94 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + }, + { + "lineno": 28, + "name": "view" + }, + { + "lineno": 28, + "name": "delIndex" + }, + { + "lineno": 28, + "name": "j" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "DepositPreauth" + } + ], + "description": "Defines the DepositPreauth transaction transactor for the XRPL, including its interface and related static methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/payment/DepositPreauth.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "DepositPreauth" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + }, + { + "args": [ + "ApplyView& view", + "uint256 const& delIndex", + "beast::Journal j" + ], + "lineno": 28, + "name": "removeFromLedger" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment/DepositPreauth.h.ai.md b/include/xrpl/tx/transactors/payment/DepositPreauth.h.ai.md new file mode 100644 index 0000000000..c28470c42b --- /dev/null +++ b/include/xrpl/tx/transactors/payment/DepositPreauth.h.ai.md @@ -0,0 +1,72 @@ +# `DepositPreauth.h` — Deposit Pre-Authorization Transactor + +## Role in the System + +`DepositPreauth` implements the transaction handler for the XRPL `DepositPreauth` transaction type. This transaction allows an account that has enabled the `lsfDepositAuth` flag (deposit authorization mode) to explicitly whitelist — or remove from the whitelist — other accounts or credential-bearing parties that may send it payments. Without such a preauthorization, a payment to a deposit-auth-enabled account would fail. This header declares the class interface; the full implementation lives in `DepositPreauth.cpp`. + +## Class Structure and Inheritance + +`DepositPreauth` inherits from `Transactor`, the base class for all XRPL transaction processors. Every transactor follows a three-phase pipeline: stateless validation (`preflight`), ledger-state checking (`preclaim`), and mutation (`doApply`). These phases are wired together by the framework via `invokePreflight` and `invoke_preclaim`, which use compile-time template dispatch (name hiding, not virtual dispatch) to call the correct per-class static methods without vtable overhead. + +```cpp +static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; +``` + +Setting `ConsequencesFactory` to `Normal` tells the framework that this transaction does not block other transactions in a fee queue (as opposed to `Blocker`, used by account-mutating transactions). + +## The Four Operations + +Despite being a single transaction type, `DepositPreauth` supports four distinct operations controlled by which of four mutually exclusive transaction fields is present: + +- **`sfAuthorize`** — preauthorizes a specific account by `AccountID`. +- **`sfUnauthorize`** — revokes a specific account's preauthorization. +- **`sfAuthorizeCredentials`** — preauthorizes any holder of a specific set of verifiable credentials (gated on the `featureCredentials` amendment). +- **`sfUnauthorizeCredentials`** — revokes a credential-set preauthorization. + +## `checkExtraFeatures` + +This optional override gates the credential-based operations on the `featureCredentials` amendment. If either `sfAuthorizeCredentials` or `sfUnauthorizeCredentials` is present in the transaction but the amendment is not active on the ledger, the method returns `false`, which causes `invokePreflight` to emit `temDISABLED`. This is the correct place for amendment checks — not inside `preflight` itself — as the base `Transactor` contract dictates. + +## `preflight` — Stateless Validation + +`preflight` enforces that exactly one of the four operation fields is present. The check is explicit: it counts how many of the four fields appear and returns `temMALFORMED` if the count is not exactly one. This guards against malformed transactions that omit all fields or combine incompatible ones. + +For account-based operations, it validates that the target `AccountID` is non-zero and that an account is not attempting to preauthorize itself (`temCANNOT_PREAUTH_SELF`). For credential-based operations, it delegates to `credentials::checkArray`, which validates the credential array size (bounded by `maxCredentialsArraySize`) and internal structure. + +## `preclaim` — Ledger-State Checks + +`preclaim` operates on a read-only ledger view. It verifies: + +- For `sfAuthorize`: the target account exists (`tecNO_TARGET`) and no duplicate `DepositPreauth` ledger entry already exists for this pair (`tecDUPLICATE`). +- For `sfUnauthorize`: the preauth entry being removed actually exists (`tecNO_ENTRY`). +- For `sfAuthorizeCredentials`: each credential issuer exists as a live account (`tecNO_ISSUER`), and no duplicate entry exists. The credentials are sorted canonically before computing the ledger key. +- For `sfUnauthorizeCredentials`: the canonical credential-set entry exists. + +The credential duplicate check is interesting: `preclaim` builds a sorted `std::set>` from the incoming credential array before calling `keylet::depositPreauth`. This normalization is critical — the ledger always stores credentials in a canonical order, so the key computation must use sorted input regardless of submission order. + +## `doApply` — Ledger Mutation + +`doApply` mirrors the four-way branch logic of `preclaim`, but mutates ledger state. + +For authorization (both account-based and credential-based), `doApply` first checks the owner reserve. The check uses `preFeeBalance_` (the account's XRP balance *before* fees were deducted), deliberately allowing accounts to dip into their base reserve to cover the transaction fee, but not to fund a new owned object beyond their current balance. This is consistent with how owner reserves work across all ledger object types. + +On success, the method creates a `SLE` (Serialized Ledger Entry) for the preauth, inserts it into the ledger, adds it to the account's owner directory via `dirInsert`, stores the resulting directory page number in `sfOwnerNode`, and increments the owner count with `adjustOwnerCount`. This reserve bookkeeping is why the object has a storage cost. + +For credential-based entries, the credentials are re-sorted before being stored in the `SLE`, ensuring the canonical representation on the ledger matches the key regardless of the order they were specified in the transaction. + +For de-authorization, the method delegates to the static `removeFromLedger`. + +## `removeFromLedger` — Shared Cleanup Interface + +```cpp +static TER +removeFromLedger(ApplyView& view, uint256 const& delIndex, beast::Journal j); +``` + +This static method accepts an `ApplyView` directly rather than using `this`, allowing it to be called from outside the transactor without instantiating a `DepositPreauth` object. As the comment in the header explicitly notes, this interface is "used by `AccountDelete`". When an account is deleted from the ledger, all owned objects — including preauth entries — must be cleaned up, and `AccountDelete` calls this method for each one. + +The method locates the `SLE`, extracts the owner `AccountID` and directory page from the object itself, removes it from the owner directory via `dirRemove`, decrements the owner count, and erases the entry. The `tefBAD_LEDGER` return for a failed directory removal is annotated `LCOV_EXCL_START`, reflecting that this case should be unreachable given correct ledger invariants — a defensive guard against internal corruption rather than expected user-triggered failures. + +## Relationship to the Credentials Amendment + +The credential-authorization paths (`sfAuthorizeCredentials` / `sfUnauthorizeCredentials`) represent a newer extension to the `DepositPreauth` mechanism. Rather than whitelisting a specific sender account, they whitelist any party that can present a verified credential from a given set of issuers. This enables more flexible access control (e.g., "allow anyone with a KYC credential from issuer X") without requiring the authorizing account to know all potential senders in advance. The amendment gating in `checkExtraFeatures` ensures that this feature activates atomically across all validators when the amendment passes. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment/Payment.h.ai.json b/include/xrpl/tx/transactors/payment/Payment.h.ai.json new file mode 100644 index 0000000000..22250d5eea --- /dev/null +++ b/include/xrpl/tx/transactors/payment/Payment.h.ai.json @@ -0,0 +1,111 @@ +{ + "args": [ + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 20, + "name": "ctx" + }, + { + "lineno": 23, + "name": "ctx" + }, + { + "lineno": 26, + "name": "ctx" + }, + { + "lineno": 29, + "name": "ctx" + }, + { + "lineno": 32, + "name": "view" + }, + { + "lineno": 32, + "name": "tx" + }, + { + "lineno": 35, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 8, + "name": "Payment" + } + ], + "description": "Defines the Payment transactor class for handling payment transactions in the XRPL codebase, including preflight checks, permission checks, and application logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/payment/Payment.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 15, + "name": "Payment" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 20, + "name": "makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 23, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 26, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 29, + "name": "preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 32, + "name": "checkPermission" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 35, + "name": "preclaim" + }, + { + "args": [], + "lineno": 37, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment/Payment.h.ai.md b/include/xrpl/tx/transactors/payment/Payment.h.ai.md new file mode 100644 index 0000000000..1d7d0b7ac0 --- /dev/null +++ b/include/xrpl/tx/transactors/payment/Payment.h.ai.md @@ -0,0 +1,56 @@ +# `Payment.h` — The Payment Transactor Interface + +## Role in the System + +`Payment.h` declares the `Payment` class, the transactor responsible for handling all XRPL `Payment` transaction type processing. It is one of the most structurally complex transactors in the codebase because a single `Payment` transaction must handle three fundamentally different execution paths: direct XRP-to-XRP transfers, direct MPToken (MPT) transfers, and cross-currency path-based payments routed through the DEX via `RippleCalc`. The header is minimal by design — it declares the interface mandated by the `Transactor` framework while hiding all logic in the `.cpp`. + +## Inheritance and the Transactor Pipeline + +`Payment` inherits publicly from `Transactor`, which provides a three-stage transaction processing pipeline invoked via static template dispatch rather than virtual dispatch. The base `Transactor::invokePreflight` template calls `T::checkExtraFeatures`, then the internal `preflight1` (sanity and flags), then `T::preflight`, then `preflight2` (signature validation), and finally `T::preflightSigValidated`. This compile-time polymorphism means that overriding pipeline steps requires only declaring the right static method with the right name — there is no vtable involved at the preflight and preclaim stages. Only `doApply` is a true virtual override. + +## `ConsequencesFactory` and `makeTxConsequences` + +The class declares `ConsequencesFactory{Custom}`, distinguishing it from transactors that use the `Normal` or `Blocker` factory. This flag causes the framework to invoke `Payment::makeTxConsequences` to compute the potential maximum XRP spend for fee-level ordering purposes. The implementation inspects `sfSendMax`: if it is present and denominated in XRP, that value becomes the maximum; otherwise it falls back to `sfAmount`. If neither is XRP, the maximum spend is zero. This matters for transaction consequence ordering — the engine needs a conservative upper bound on how much XRP could be consumed before it will apply the transaction. + +## Preflight Validation + +`Payment::preflight` is stateless and performs purely structural validation against the serialized transaction fields. It enforces several invariants: + +- MPT amounts in `sfAmount` require the `featureMPTokensV1` amendment to be active; without it, any MPT-denominated payment is rejected as `temDISABLED`. +- Direct XRP payments cannot carry `sfSendMax` (redundant), `sfPaths` (inapplicable), `tfPartialPayment` (meaningless), `tfLimitQuality` (meaningless), or `tfNoRippleDirect` (meaningless). These are rejected as distinct `temBAD_SEND_XRP_*` codes rather than a generic malformation, giving clients precise diagnostic information. +- When `sfDeliverMin` is specified, `tfPartialPayment` must also be set, `DeliverMin` must be positive, its asset must match `sfAmount`'s asset, and it must not exceed `sfAmount`. This prevents a scenario where `DeliverMin` is set but cannot be semantically fulfilled. +- Self-payments without paths are rejected as `temREDUNDANT`. A self-payment with paths is allowed because it could represent an arbitrage cycle. + +`checkExtraFeatures` gates two optional fields: `sfCredentialIDs` requires the `featureCredentials` amendment, and `sfDomainID` requires `featurePermissionedDEX`. This pattern cleanly separates feature-gating from structural validation. + +`getFlagsMask` is non-trivial: for MPT payments when `featureMPTokensV2` is not enabled, only `tfPartialPayment` is allowed beyond the universal flags. Once `MPTokensV2` is active, the full `tfPaymentMask` (which includes `tfLimitQuality`, `tfNoRippleDirect`, etc.) becomes valid, since MPT payments can then participate in path-finding. + +## Preclaim Checks + +`preclaim` runs with ledger read access and enforces runtime conditions that depend on current ledger state: + +- If the destination account does not exist and the payment is non-native (IOU or MPT), the transaction is rejected `tecNO_DST` rather than creating an account — only XRP can fund a new account. If the amount is XRP but below the base reserve, `tecNO_DST_INSUF_XRP` is returned. +- If the destination has `lsfRequireDestTag` set, `sfDestinationTag` must be present or the transaction fails `tecDST_TAG_NEEDED`. +- Path complexity is bounded here: paths exceeding `MaxPathSize = 6` or any individual path exceeding `MaxPathLength = 8` steps are rejected `telBAD_PATH_COUNT`. These constants live as private `static constexpr` members, documenting the limits as invariants of the class rather than magic numbers. +- Credential validity is checked, and `sfDomainID` (Permissioned DEX) is validated to confirm both sender and destination are members of the specified domain. + +## Delegate Permission Model + +`checkPermission` implements a layered delegation check. When a transaction carries `sfDelegate`, the method reads the delegate authorization ledger entry and applies two tiers: + +1. Full transaction permission: if the delegate SLE grants blanket `ttPAYMENT` permission, the payment proceeds. +2. Granular permissions: if full permission is not granted, `PaymentMint` allows a delegate to issue tokens where the delegating account is the issuer, and `PaymentBurn` allows a delegate to send tokens back to their issuer. Crucially, both granular permissions are only valid for *direct* payments — any payment with `sfPaths` set or with `sfSendMax` pointing to a different asset than `sfAmount` is denied `terNO_DELEGATE_PERMISSION`. This prevents granular grants from being leveraged to manipulate multi-hop paths where intermediate assets could be exploited. + +## `doApply` — Three Execution Paths + +The execution logic in `doApply` branches into three distinct paths based on asset type and flags: + +**RippleCalc path-based payments**: Any payment involving `sfPaths`, `sfSendMax`, or a non-native destination amount (except direct MPT without `MPTokensV2`) is routed through `path::RippleCalc::rippleCalculate`. All mutations are staged in a `PaymentSandbox` view and applied atomically only if the calculation succeeds. If `RippleCalc` returns a `TerRetry` code, the transactor converts it to `tecPATH_DRY` to ensure fee consumption — a retry code would prevent fee collection, so the conversion is a deliberate economic defense against path-spam attacks. The actual delivered amount is set via `ctx_.deliver()` when the delivered amount differs from the requested amount, enabling the `DeliveredAmount` metadata field. + +**Direct MPT payments** (when `featureMPTokensV2` is not active): These bypass `RippleCalc` entirely. The transactor checks authorization (`requireAuth`), transfer permission (`canTransfer`), and frozen state before computing the transfer rate. Partial payment is handled manually: if `tfPartialPayment` is set and the sender cannot cover the full transfer-rate-adjusted cost, the deliverable amount is scaled down. The `deliverMin` floor is then checked against the scaled amount. The fix `fixMPTDeliveredAmount` gates whether the `DeliveredAmount` metadata field is updated for MPT transfers that differ from their nominal amount. + +**Direct XRP payments**: The simplest path validates that the sender has sufficient balance above their owner reserve (factoring in whether the sender or a delegate account is the fee payer), rejects pseudo-accounts as recipients, checks deposit pre-authorization (with a bypass for small amounts to prevent accounts from becoming permanently wedged with no XRP to pay fees), then directly updates both SLE `sfBalance` fields. + +## Design Rationale + +The separation of `preflight` (stateless), `preclaim` (read-only ledger), and `doApply` (mutating) reflects the XRPL consensus engine's need to evaluate transactions at different points in the validation pipeline without always completing the full execution. The three payment modes within a single transactor class, rather than three separate transactors, reflect that from the perspective of the protocol and users, `Payment` is a single transaction type — the internal branching is an implementation detail hidden from the serialized transaction format and the validation framework. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h.ai.json b/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h.ai.json new file mode 100644 index 0000000000..d26ec35973 --- /dev/null +++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h.ai.json @@ -0,0 +1,84 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + }, + { + "lineno": 24, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "PaymentChannelClaim" + } + ], + "description": "Defines the PaymentChannelClaim transaction handler class for the XRPL, including its interface and key methods for transaction processing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "PaymentChannelClaim" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 24, + "name": "preclaim" + }, + { + "args": [], + "lineno": 27, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h.ai.md b/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h.ai.md new file mode 100644 index 0000000000..f726445aab --- /dev/null +++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelClaim.h.ai.md @@ -0,0 +1,54 @@ +# `PaymentChannelClaim.h` — Payment Channel Claim Transaction Handler + +## Role in the System + +`PaymentChannelClaim` is the transactor that processes the `PaymentChannelClaim` transaction type on the XRP Ledger. Payment channels are off-ledger scaling constructs: a sender locks XRP into an on-ledger channel object, then issues signed authorization vouchers off-ledger to a receiver. When the receiver (or the sender) wants to settle, they submit a `PaymentChannelClaim` transaction to move those vouchers on-chain. This transactor handles all three phases of that settlement: pre-validation, ledger-read checks, and final ledger mutation. + +The file declares the class within the `xrpl` namespace and brings in only `Transactor.h`, keeping the header minimal and compilation fast. + +## Class Design and Inheritance + +`PaymentChannelClaim` inherits from `Transactor` and follows the framework's compile-time polymorphism pattern. Rather than virtual dispatch for the preflight and preclaim phases, the base class `invokePreflight` template calls `T::checkExtraFeatures`, `T::getFlagsMask`, `T::preflight`, and then `T::preflightSigValidated` in sequence using name hiding. Only `doApply()` is a true virtual override and is the sole instance method on the class. + +The `ConsequencesFactory` is set to `Normal`, in contrast to `PaymentChannelCreate` and `PaymentChannelFund`, which both declare `Custom`. This distinction matters for fee/consequence analysis: Create and Fund introduce new XRP obligations (locking funds into the channel), requiring custom `TxConsequences` objects to describe their impact on account balances. A Claim only redistributes XRP that is already locked — it cannot add new obligations — so the standard `Normal` factory suffices. + +## Preflight Phase + +`checkExtraFeatures` gates credential support: if the transaction carries `sfCredentialIDs`, the `featureCredentials` amendment must be active, or the transaction is rejected with `temDISABLED`. This is the canonical pattern for amendment-gated optional fields. + +`getFlagsMask` returns `tfPaymentChannelClaimMask`, restricting which flags are legal on this transaction type. The base `preflight1` enforces this mask before calling the transactor-specific `preflight`. + +`preflight` itself enforces the consistency rules that can be checked without ledger state: + +- `sfBalance` and `sfAmount`, if present, must be positive XRP values. If both appear, `sfBalance` must not exceed `sfAmount` — the claimed balance cannot exceed the channel's total funding. +- `tfClose` and `tfRenew` are mutually exclusive flags; combining them is `temMALFORMED`. +- If `sfSignature` is present, both `sfPublicKey` and `sfBalance` must accompany it (you can't provide a partial authorization). The signature is cryptographically verified here in preflight against a canonical channel authorization message serialized via `serializePayChanAuthorization`. This is notable: signature verification normally happens in the framework's `preflight2`, but the payment channel authorization signature is a *separate* off-channel payment voucher distinct from the transaction signature itself, so it must be checked explicitly here. +- Credential field well-formedness is validated via `credentials::checkFields`. + +## Preclaim Phase + +`preclaim` overrides the base class no-op to perform credential validation against live ledger state when the `featureCredentials` amendment is enabled. This two-phase approach — structural check in `preflight`, live validity check in `preclaim` — is necessary because `preflight` runs without ledger access. + +## Apply Phase + +`doApply` is the heart of the transactor and implements four distinct behaviors selected by the transaction's content and flags. + +**Expiry check first.** Before any other logic, the channel's `sfCancelAfter` and `sfExpiration` fields are compared against the ledger's `parentCloseTime`. If the channel is expired by either deadline, it is closed immediately via `closeChannel`, regardless of whether a balance claim was requested. This ensures expired channels cannot be exploited. + +**Authorization check.** Only the channel source (`sfAccount`) or the channel destination (`sfDestination`) may submit a claim transaction. Any other account receives `tecNO_PERMISSION`. + +**Balance claim.** If `sfBalance` is present, the transactor transfers the incremental XRP. The destination must supply a signature (checked here against the channel's stored public key, not the transaction's signing key). The logic enforces monotonicity: `reqBalance` must exceed the channel's current `sfBalance` — otherwise there's nothing new to transfer and the transaction fails with `tecUNFUNDED_PAYMENT`. The difference (`reqDelta`) is credited to the destination's account balance. A `verifyDepositPreauth` call also enforces any DepositPreauth constraints on the destination account. + +**Renew (`tfRenew`).** Clears the channel's `sfExpiration` field, resetting any voluntary expiration the source had previously set. Only the source account may renew; a destination attempting this is rejected with `tecNO_PERMISSION`. + +**Close (`tfClose`).** If the destination is closing, or the channel is fully drawn (`Balance == Amount`), the channel closes immediately. Otherwise — when the source is closing a partially-funded channel — the transactor sets `sfExpiration` to `parentCloseTime + sfSettleDelay`, giving the destination a settlement window. Crucially, it only updates the expiration if no sooner expiration already exists, preventing a source from repeatedly extending its own close request. + +## Relationship to Sibling Transactors + +The three payment channel transactors form a lifecycle: + +- `PaymentChannelCreate` opens a channel and locks XRP into it (Custom consequences). +- `PaymentChannelFund` adds more XRP to an existing channel (Custom consequences). +- `PaymentChannelClaim` settles or closes the channel (Normal consequences). + +All three share the same header-only structure and `Transactor` inheritance pattern, but only `PaymentChannelClaim` overrides `checkExtraFeatures`, `getFlagsMask`, and `preclaim` — the extra complexity reflecting the richness of the claim operation compared to simple fund-and-create operations. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h.ai.json b/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h.ai.json new file mode 100644 index 0000000000..092d4b9dad --- /dev/null +++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h.ai.json @@ -0,0 +1,61 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "PaymentChannelCreate" + } + ], + "description": "Defines the PaymentChannelCreate transactor class for creating payment channels in the XRPL system, including its interface and related static methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "PaymentChannelCreate" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h.ai.md b/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h.ai.md new file mode 100644 index 0000000000..2dcb468919 --- /dev/null +++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelCreate.h.ai.md @@ -0,0 +1,52 @@ +# `PaymentChannelCreate.h` — Payment Channel Creation Transactor + +`PaymentChannelCreate` is the transactor responsible for opening a unidirectional XRP payment channel on the XRPL. It lives in the `payment_channel` transactor family alongside `PaymentChannelFund` and `PaymentChannelClaim`, and inherits from `Transactor` — the common base class that orchestrates the three-phase transaction pipeline. + +## Role in the System + +Payment channels allow two parties to exchange a stream of XRP payments off-ledger, checkpointed by signed claim messages, without committing each individual transfer to the global ledger. Only the open and close operations are on-ledger; individual micropayments travel off-chain. `PaymentChannelCreate` handles the first on-ledger step: locking the sender's XRP into a dedicated channel ledger object (`PayChannel` SLE) and notifying both sender and recipient by registering the channel in each party's owner directory. + +## Class Interface and Inheritance + +The class derives from `Transactor` and exposes the standard four-point contract: + +- `makeTxConsequences` — static, called before the ledger is read +- `preflight` — static, pure field-level validation +- `preclaim` — static, read-only ledger checks +- `doApply` — instance method, mutates the ledger + +The `ConsequencesFactory` tag is set to `Custom`, which is the telling design decision: unlike `PaymentChannelClaim` (tagged `Normal`), creating a channel locks up the full `sfAmount` of XRP beyond just the fee. `makeTxConsequences` therefore constructs a `TxConsequences` object that reports `ctx.tx[sfAmount].xrp()` as the consumed balance, not merely the transaction fee. This lets the transaction queue accurately represent how much XRP the account will be unable to use while this transaction is pending. + +## Preflight Validation + +`preflight` enforces three invariants before any ledger access occurs: + +1. The `sfAmount` field must be a positive XRP value — IOUs are not permitted in payment channels. +2. The sender (`sfAccount`) must differ from the destination (`sfDestination`); self-funded channels are nonsensical and rejected with `temDST_IS_SRC`. +3. The `sfPublicKey` must parse as a recognized key type. This key is the one the sender will use to sign off-ledger claims, so its validity must be established at creation time — it cannot be changed later. + +## Preclaim Checks + +`preclaim` reads the ledger without modifying it and enforces economic and permission constraints: + +- **Reserve and funding**: The sender must have enough balance to cover both the post-creation owner reserve (`ownerCount + 1`) and the full channel amount. The reserve check uses `tecINSUFFICIENT_RESERVE`, while the funding check returns `tecUNFUNDED`, preserving the distinction between "account is too lean to hold more objects" and "account can't actually transfer the requested amount." +- **Destination existence**: The destination account must already exist on the ledger (`tecNO_DST`). +- **Incoming channel permission**: If the destination account has set `lsfDisallowIncomingPayChan`, the creation is refused with `tecNO_PERMISSION`. +- **Destination tag requirement**: If the destination requires a tag (`lsfRequireDestTag`) and none was provided in the transaction, `tecDST_TAG_NEEDED` is returned. +- **Pseudo-account guard**: Pseudo-accounts (fee sink accounts, amendment accounts, etc.) cannot receive payment channels through this path. Notably, this check is *not* amendment-gated — the comment explains that writes to pseudo-account discriminator fields are themselves amendment-gated upstream, so the check's behavior automatically tracks the active amendments without needing its own gate. + +## Ledger Mutation in `doApply` + +The apply phase performs several coordinated mutations: + +**Expiry validation (amendment-gated)**: Under `fixPayChanCancelAfter`, an optional `sfCancelAfter` field is checked against `parentCloseTime`. This happens in `doApply`, not `preclaim`, because the canonical close time of the current ledger is only fully settled at apply time. + +**Channel SLE construction**: A new `PayChannel` ledger object is created at a keylet derived from `keylet::payChan(account, dst, ctx_.tx.getSeqValue())`. Using the transaction's sequence-or-ticket value as part of the key makes the channel ID fully deterministic before the transaction lands on the ledger — an important property for off-ledger parties who need to reference the channel in signed claims before the creating transaction has been confirmed. The SLE captures: total escrowed funds (`sfAmount`), the running paid balance initialized to zero (`sfBalance` via `zeroed()`), both account IDs, the settle delay, the signing public key, and the optional cancel-after, source tag, and destination tag fields. Under `fixIncludeKeyletFields`, the sequence value is also written directly into the SLE to allow key reconstruction from the object alone. + +**Dual directory registration**: The new channel is inserted into the owner directory of *both* the sender and the recipient. The sender's entry enables reserve accounting and channel discovery for funding or closure; the recipient's entry gives them a direct path to claim funds. Each insertion records the directory page in `sfOwnerNode` and `sfDestinationNode` respectively, enabling efficient removal during channel closure. + +**Balance and owner count update**: The sender's `sfBalance` is decremented by the full channel amount, and `adjustOwnerCount` increments the sender's `sfOwnerCount` by one, raising their minimum reserve to reflect the new ledger object. + +## Relationship to Sibling Transactors + +`PaymentChannelFund` shares the `Custom` consequences pattern with `PaymentChannelCreate` and uses the same `makeTxConsequences` signature, since adding funds to an existing channel also moves XRP. `PaymentChannelClaim`, by contrast, uses `Normal` consequences — a claim doesn't move XRP from the claimer's balance, only from the channel's escrowed pool. `PaymentChannelClaim` also overrides `checkExtraFeatures` and `getFlagsMask`, reflecting that it has its own feature-gate and supports additional transaction flags (e.g. closing or renewing the channel) that `PaymentChannelCreate` does not. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h.ai.json b/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h.ai.json new file mode 100644 index 0000000000..876b040229 --- /dev/null +++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h.ai.json @@ -0,0 +1,54 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "PaymentChannelFund" + } + ], + "description": "Defines the PaymentChannelFund transactor class for handling the funding of payment channels in the XRPL transaction processing system.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "PaymentChannelFund" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [], + "lineno": 21, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h.ai.md b/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h.ai.md new file mode 100644 index 0000000000..ccd99535c4 --- /dev/null +++ b/include/xrpl/tx/transactors/payment_channel/PaymentChannelFund.h.ai.md @@ -0,0 +1,41 @@ +# `PaymentChannelFund.h` — Payment Channel Funding Transactor + +`PaymentChannelFund` is one of three transactors governing the XRPL payment channel lifecycle, alongside `PaymentChannelCreate` and `PaymentChannelClaim`. Its specific responsibility is allowing a channel's owner to add XRP to an already-open channel and optionally extend its expiration deadline. It is a thin header that declares the class interface; all logic lives in the corresponding `PaymentChannelFund.cpp`. + +## Role in the Payment Channel Subsystem + +Payment channels pre-fund a pool of XRP that a sender can draw down off-ledger via signed claims, with the on-ledger balance acting as the upper bound. Once a channel is created with an initial amount, the owner may need to top it up without closing and reopening — this is exactly what `PaymentChannelFund` handles. The transactor also serves as a side-channel for extending the channel's voluntary expiration (`sfExpiration`), independent of adding funds. + +## Class Design and Inheritance + +`PaymentChannelFund` inherits from `Transactor` and follows the standard three-phase contract the framework imposes: + +- **`makeTxConsequences`** (static) — called before ledger application to declare the worst-case cost of the transaction. `PaymentChannelFund` sets `ConsequencesFactory` to `Custom` rather than `Normal` or `Blocker`, because the consequence amount is transaction-specific: it returns a `TxConsequences` carrying the full `sfAmount` value in XRP. This is necessary so the transaction queue can accurately account for funds that will be locked into the channel if this transaction succeeds. + +- **`preflight`** (static) — lightweight, context-free validation that runs before any ledger state is consulted. The implementation enforces just two rules: `sfAmount` must be XRP (not an IOU), and it must be strictly positive. Anything more complex deferred to `doApply`. + +- **`doApply`** (virtual override) — the ledger-mutating step, executing after preflight and preclaim pass. + +## `doApply` Logic + +The apply logic follows a layered guard structure: + +**1. Channel existence.** The channel is looked up by its ID from `sfChannel`. If the `ltPAYCHAN` object is absent the transaction fails with `tecNO_ENTRY`. + +**2. Expiry short-circuit.** Before doing anything else, the code checks both `sfCancelAfter` (the hard deadline set at creation) and `sfExpiration` (the voluntary soft deadline, settable by the owner). If the ledger's parent close time has passed either deadline, `doApply` immediately calls `closeChannel` and returns its result. This means a funding attempt on an expired channel cleanly triggers the channel's closure rather than silently failing — an important correctness property so expired channels don't linger indefinitely. + +**3. Ownership enforcement.** Only the account that originally opened the channel (`sfAccount` on the `ltPAYCHAN` object) may fund it or modify its expiration. Any other account receives `tecNO_PERMISSION`. This is intentional: third-party funding would create perverse incentives and complicate dispute resolution. + +**4. Optional expiration extension.** If `sfExpiration` is present in the transaction, the transactor enforces a minimum: the new expiration must be at least `parentCloseTime + sfSettleDelay` into the future (giving the destination sufficient time to claim after the owner tries to close). If the channel already has a custom expiration that is *earlier* than this computed minimum, the existing expiration becomes the floor instead. Attempting to set an expiration below this floor returns `temBAD_EXPIRATION`. + +**5. Reserve and balance check.** The owner's account balance must cover both the account reserve (based on current `sfOwnerCount`) and the additional `sfAmount` being deposited. Insufficient reserve returns `tecINSUFFICIENT_RESERVE`; insufficient total balance returns `tecUNFUNDED`. + +**6. Destination existence.** The transaction checks that the channel's destination account still exists on the ledger before accepting new funds. This prevents XRP from being locked into a channel whose beneficiary has been deleted, which would make the funds unclaimable. + +**7. Ledger mutation.** On success, `sfAmount` on the channel object is incremented by the transaction amount, and the owner's `sfBalance` is decremented by the same amount. Both the channel SLE and the account SLE are written back via `ctx_.view().update()`. + +## Design Observations + +The expiry check triggering `closeChannel` inside a fund attempt is a deliberate defense-in-depth measure: since any account can submit any transaction against any channel ID, this ensures that even a mistaken or adversarial funding attempt against an expired channel performs useful cleanup work. The lack of a `preclaim` override (unlike `PaymentChannelCreate`, which does define one) reflects that all meaningful preconditions can be efficiently checked during apply once the channel object is in hand. + +The `Custom` consequences type, shared with `PaymentChannelCreate`, distinguishes these transactors from simple fee-only transactions — the queue must treat them as locking up the declared XRP, not just the transaction fee. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h.ai.json b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h.ai.json new file mode 100644 index 0000000000..1a810f3be1 --- /dev/null +++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 16, + "name": "ctx" + }, + { + "lineno": 19, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "PermissionedDomainDelete" + } + ], + "description": "Defines the PermissionedDomainDelete transaction class for deleting a permissioned domain in the XRPL system, including its interface and lifecycle methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "PermissionedDomainDelete" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 19, + "name": "preclaim" + }, + { + "args": [], + "lineno": 23, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h.ai.md b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h.ai.md new file mode 100644 index 0000000000..9ac96bd5fc --- /dev/null +++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.h.ai.md @@ -0,0 +1,29 @@ +# `PermissionedDomainDelete.h` — Transactor for Removing a Permissioned Domain + +## Role in the System + +`PermissionedDomainDelete` is one of two transactors that manage the lifecycle of permissioned domains on the XRP Ledger — the other being `PermissionedDomainSet`, which handles creation and updates. Together they live under `include/xrpl/tx/transactors/permissioned_domain/` and follow the standard XRPL transactor pattern, where each transaction type is a concrete class inheriting from the `Transactor` base. + +A permissioned domain is a ledger object that constrains access for certain ledger operations (such as AMM or DEX interactions) to accounts that satisfy a set of credentials. This transactor provides the controlled teardown path: an account that owns a domain can remove it from the ledger, recovering the owner reserve that was locked when the domain was created. + +## Class Design and the Three-Phase Transaction Model + +`PermissionedDomainDelete` follows the mandatory three-phase pipeline that all XRPL transactors must implement: + +**`preflight`** runs before the ledger is touched and with no access to ledger state. For this transactor it performs only the cheapest possible validation: confirming that the `sfDomainID` field in the transaction is non-zero. A zero `DomainID` is structurally malformed and returns `temMALFORMED`. This is intentionally minimal — anything that requires reading ledger state is deferred to `preclaim`. + +**`preclaim`** has read-only access to the current ledger view. It resolves the `sfDomainID` to a `PermissionedDomain` SLE (Serialized Ledger Entry) via `keylet::permissionedDomain`. Two conditions are checked: the domain must actually exist (`tecNO_ENTRY` otherwise), and the submitting account must be the domain's owner (`tecNO_PERMISSION` otherwise). Splitting existence and ownership checks into `preclaim` — rather than `doApply` — is important because `preclaim` determines whether the transaction will *claim a fee* even if it ultimately fails; an account that submits a malformed or unauthorized delete still pays. + +**`doApply`** performs the irreversible ledger mutations: it removes the domain's entry from the owner's directory via `view().dirRemove()`, decrements the account's `sfOwnerCount` through `adjustOwnerCount()`, and erases the SLE with `view().erase()`. The directory removal step is critical — XRPL uses owner directories to enforce reserve requirements (each ledger object owned by an account locks a base reserve increment), and skipping this step would leave the account's reserve accounting permanently wrong. The fatal-level log on `dirRemove` failure is marked `LCOV_EXCL` because a corrupt directory at that point would represent a ledger invariant violation that cannot be recovered from gracefully. + +## `ConsequencesFactory` and Fee Consequences + +The `static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}` declaration tells the transaction consequences framework how to evaluate the potential impact of this transaction on account state when deciding how to queue or batch it. `Normal` means the transaction's worst-case effect on the account's spendable balance is limited to the transaction fee itself — appropriate here since deleting a domain returns a reserve but does not move any XRP between parties in an unbounded way. + +## Comparison with `PermissionedDomainSet` + +`PermissionedDomainSet` declares an additional `checkExtraFeatures` static override, which gates the transaction on a specific amendment being enabled. `PermissionedDomainDelete` omits this override, falling through to the `Transactor` base implementation that unconditionally returns `true`. This asymmetry reflects the general ledger design principle that once a feature's objects exist in the ledger, their deletion must always be possible regardless of amendment state — you should never create a situation where objects are stranded and unremovable. + +## Invariants and Failure Handling + +The `XRPL_ASSERT` calls in `doApply` encode two invariants that should always hold by the time execution reaches that method: the `sfDomainID` field is present (guaranteed by preflight), and the owner account's `sfOwnerCount` is positive (it was incremented when the domain was created). Both are defensive; reaching `doApply` with a violated precondition would indicate a serious bug in the pipeline above it, not a recoverable user error. The `tefBAD_LEDGER` return for a failed directory removal is similarly a hard internal error, distinct from the `tec` errors that represent valid but failed user requests. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h.ai.json b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h.ai.json new file mode 100644 index 0000000000..22318941b9 --- /dev/null +++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "PermissionedDomainSet" + } + ], + "description": "Defines the PermissionedDomainSet transactor class for creating a Permissioned Domain in the XRPL, including its interface and lifecycle methods.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "PermissionedDomainSet" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 25, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h.ai.md b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h.ai.md new file mode 100644 index 0000000000..61bb83f43b --- /dev/null +++ b/include/xrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.h.ai.md @@ -0,0 +1,37 @@ +# `PermissionedDomainSet.h` — Transactor for Creating and Modifying Permissioned Domains + +## Role in the System + +`PermissionedDomainSet` is the transactor responsible for both **creating** a new `PermissionedDomain` ledger object and **modifying** an existing one. Unlike many XRPL transaction types that separate creation and mutation, this single transactor serves dual purposes: the presence or absence of `sfDomainID` in the transaction body determines which path executes. The class lives alongside `PermissionedDomainDelete` in the `permissioned_domain` module — the two together form the full lifecycle management for the feature. + +Permissioned Domains are an XRPL construct that lets an account define a named set of accepted credentials (issuer + credential type pairs). Other protocol features can then scope access to holders of those credentials, rather than having to enumerate individual accounts. + +## Class Interface + +`PermissionedDomainSet` publicly inherits `Transactor` and declares `ConsequencesFactory{Normal}`, meaning it uses the standard fee/consequence model with no blocking behavior. The constructor merely forwards `ApplyContext&` to the base. + +The lifecycle follows the framework's three-phase pipeline — `checkExtraFeatures`, `preflight`, `preclaim`, and `doApply` — dispatched by the template `Transactor::invokePreflight` at the infrastructure layer. Only `doApply` is an actual virtual override; the static methods participate in compile-time polymorphism via name hiding, not vtable dispatch. + +## Transaction Lifecycle + +**`checkExtraFeatures`** gates the entire transaction on the `featureCredentials` amendment. If the amendment is not enabled in the current ruleset, `invokePreflight` returns `temDISABLED` immediately without entering `preflight`. This is notable because `PermissionedDomainDelete`, the sibling transactor, does *not* define its own `checkExtraFeatures` — it inherits the base class no-op that always returns `true`. This asymmetry presumably reflects that deletion is a safe cleanup operation that should remain available even if the feature gate were ever conditionally lifted. + +**`preflight`** performs stateless validation against the transaction fields only. It delegates to `credentials::checkArray` to verify that `sfAcceptedCredentials` respects `maxPermissionedDomainCredentialsArraySize` and that each credential entry is structurally sound. It also rejects any transaction that presents `sfDomainID` equal to `beast::zero` — a zero hash is not a valid domain identifier and would indicate a malformed client submission. + +**`preclaim`** adds read-only ledger state checks against the current view. It confirms the submitting account actually exists (a guard against internal inconsistency), then iterates every credential in `sfAcceptedCredentials` to verify each `sfIssuer` account is present on ledger — a domain referencing a non-existent issuer would be permanently unresolvable. For update operations (where `sfDomainID` is present), it reads the domain SLE directly and verifies both existence (`tecNO_ENTRY`) and ownership (`tecNO_PERMISSION`), preventing any account from modifying a domain they don't own. + +**`doApply`** is where the actual ledger mutation happens. It first sorts the incoming credentials via `credentials::makeSorted`, converting them into a canonical ordering before writing to the SLE. This normalization ensures ledger objects always store credentials in a deterministic sequence regardless of submission order, which matters for equality checks and hash stability. + +The method then branches on the presence of `sfDomainID`: + +- **Update path**: Peeks the existing domain SLE, replaces its `sfAcceptedCredentials` array in-place with the sorted result, and calls `view().update()`. No reserve change occurs — this is a pure field mutation. + +- **Create path**: First checks that the submitting account has sufficient XRP balance to cover the incremented owner reserve (`accountReserve(ownerCount + 1)`). The new `PermissionedDomain` SLE is keyed by `keylet::permissionedDomain(account_, sequence)` — the transaction sequence number is embedded in the domain's identity, making each created domain globally unique without requiring a separate ID generation mechanism. The SLE is inserted into the account's owner directory, `sfOwnerNode` is populated with the returned page index, and `adjustOwnerCount` increments the reserve counter by one before inserting the object. + +## Design Decisions + +The use of `sfSequence` as the domain key differentiator is idiomatic XRPL: since sequence numbers are monotonically increasing and unique per account, `(account, sequence)` is a collision-free domain identifier derivable at creation time without any ledger lookup. This also means the `sfDomainID` used in subsequent transactions is deterministic and auditable — it can be recomputed from the creating transaction's metadata. + +The sorted credential storage reflects a broader XRPL pattern of canonical normalization on write rather than on read. Because multiple parties may query or hash the same domain object, keeping the stored representation deterministic avoids subtle divergence between nodes. The sort happens inside `doApply` rather than in `preflight` or `preclaim` to minimize repeated work on transactions that ultimately don't apply. + +Error codes follow the `tec`/`tef`/`tem` hierarchy strictly: structural problems (malformed fields) return `tem` codes from `preflight`, missing-but-queryable state returns `tec` codes from `preclaim`, and invariant violations that "should never happen" return `tefINTERNAL` guarded by `LCOV_EXCL_LINE` annotations that acknowledge those paths are not reached in normal testing. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/system/Batch.h.ai.json b/include/xrpl/tx/transactors/system/Batch.h.ai.json new file mode 100644 index 0000000000..5411bd5689 --- /dev/null +++ b/include/xrpl/tx/transactors/system/Batch.h.ai.json @@ -0,0 +1,100 @@ +{ + "args": [ + { + "lineno": 12, + "name": "ApplyContext& ctx" + }, + { + "lineno": 16, + "name": "ReadView const& view" + }, + { + "lineno": 16, + "name": "STTx const& tx" + }, + { + "lineno": 19, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 22, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 25, + "name": "PreflightContext const& ctx" + }, + { + "lineno": 28, + "name": "PreclaimContext const& ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 8, + "name": "Batch" + } + ], + "description": "Defines the Batch transaction type for the XRPL, including its interface, fee calculation, flag masking, preflight checks, and disabled transaction types.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/system/Batch.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 12, + "name": "Batch" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 16, + "name": "calculateBaseFee" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 19, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 22, + "name": "preflight" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 25, + "name": "preflightSigValidated" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 28, + "name": "checkSign" + }, + { + "args": [], + "lineno": 31, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/system/Batch.h.ai.md b/include/xrpl/tx/transactors/system/Batch.h.ai.md new file mode 100644 index 0000000000..068b37d63a --- /dev/null +++ b/include/xrpl/tx/transactors/system/Batch.h.ai.md @@ -0,0 +1,79 @@ +# `include/xrpl/tx/transactors/system/Batch.h` + +## Role and Purpose + +`Batch` is the transactor implementing the `ttBATCH` transaction type, which allows a submitter to bundle two to eight inner XRPL transactions into a single outer transaction with a configurable execution policy. It solves the composability problem on XRPL: operations that logically belong together — such as a DEX offer paired with a trust line establishment — can be submitted atomically, avoiding the race conditions and ordering problems that arise from submitting them as independent transactions. + +The header defines the class interface; the implementation lives in `src/libxrpl/tx/transactors/system/Batch.cpp`. + +## Inheritance and the Transactor Framework + +`Batch` inherits from `Transactor`, which uses compile-time polymorphism rather than virtual dispatch for its pipeline stages. `invokePreflight` in the base class calls `T::getFlagsMask`, `T::preflight`, then `preflight2`, then `T::preflightSigValidated` in sequence — relying on name hiding rather than virtual functions. `Batch` overrides all of these static hooks, plus `checkSign` (in preclaim) and `doApply` (the only true virtual method). + +`ConsequencesFactory` is set to `Normal`, meaning the transaction does not unconditionally block a queue slot the way an `AccountDelete` does, and the base class consequence analysis suffices. + +## Fee Calculation: `calculateBaseFee` + +The fee model for Batch is deliberately additive and more complex than a standard transactor. The total base fee is: + +``` +batchBase (view.fees().base + own base fee) + + sum of each inner transaction's base fee + + (number of batch signatures × view.fees().base) +``` + +Charging each inner transaction's fee prevents Batch from being a cost-avoidance mechanism relative to submitting the transactions individually. Charging per batch-signer signature mirrors the multi-sign fee structure used elsewhere. The implementation performs explicit overflow checks at every accumulation step, returning `INITIAL_XRP` as a sentinel on overflow rather than throwing — a defensive pattern used throughout the XRPL fee pipeline. + +## Flag Semantics: `getFlagsMask` + +`getFlagsMask` returns `tfBatchMask`, which admits exactly four mutually exclusive execution policy flags and rejects `tfInnerBatchTxn` on the outer transaction (only inner transactions carry that flag). The four policies, enforced in `applyBatchTransactions()` in `apply.cpp`, are: + +- **`tfAllOrNothing`** — any inner failure aborts all; the batch either fully commits or fully rolls back. +- **`tfOnlyOne`** — stops at the first success; subsequent transactions are not executed. +- **`tfUntilFailure`** — processes transactions in order, stops on the first failure, commits all prior successes. +- **`tfIndependent`** — all transactions run regardless of individual failures; successful ones commit. + +`preflight` enforces that exactly one of these flags is set using `std::popcount`, rejecting any combination. + +## Preflight: `preflight` + +`preflight` is the structural integrity check run before signature verification. It validates: + +1. Exactly one execution policy flag is present. +2. The `sfRawTransactions` array contains at least two and no more than `maxBatchTxCount` (8) entries. +3. For each inner transaction: it must not be a `ttBATCH` itself (no nesting), must not be a disabled transaction type (see `disabledTxTypes`), must carry `tfInnerBatchTxn`, must have an empty `sfSigningPubKey` and no `sfTxnSignature` or `sfSigners`, must have a zero fee in XRP, and must pass its own `preflight` call with the `tapBATCH` flag set (passing the outer batch's transaction ID as `parentBatchId`). +4. Each inner transaction must have either a non-zero `sfSequence` or an `sfTicketSequence`, but not both. +5. For `tfAllOrNothing` and `tfUntilFailure` modes, duplicate sequence or ticket values across inner transactions from the same account are rejected at this stage — since those modes commit or abort as a unit, two inner transactions consuming the same account slot would be incoherent. + +The inner `preflight` calls are recursive invocations of the ledger's top-level `xrpl::preflight()`, which routes through `invokePreflight` for the appropriate inner transaction type. This ensures each inner transaction is individually well-formed. + +## Post-Signature Validation: `preflightSigValidated` + +This stage runs after the outer transaction's own signature has been cryptographically verified, so it has a degree of trust that the submitter is who they claim to be. It builds the set of accounts that must additionally sign the batch (`requiredSigners`): every inner transaction account that differs from the outer account, plus any `sfCounterparty` fields that differ from the outer account. + +It then validates `sfBatchSigners` against this set with a double-bookkeeping pass: every batch signer must match an entry in `requiredSigners` (no extraneous signers), and every required signer must appear in the batch signers array (no missing signers). Duplicates are rejected, and the outer account may not appear in `sfBatchSigners` since its authorization is conveyed by the outer signature itself. Finally, `ctx.tx.checkBatchSign()` verifies the cryptographic signatures of all batch signers. + +Separating this validation into `preflightSigValidated` rather than `preflight` is a deliberate design choice dictated by the framework: the framework only calls this hook after the outer signature has been validated, which is the right place to verify the cryptographic signatures of other parties. + +## Signature Checking: `checkSign` + +`Batch::checkSign` overrides `Transactor::checkSign` at the preclaim stage to run two checks: the standard outer-account signature via `Transactor::checkSign` and then `Transactor::checkBatchSign` which re-validates batch signer credentials against on-ledger account state. The separation from `preflightSigValidated` reflects the preclaim/preflight pipeline: preclaim has access to the ledger view and can check whether signing keys are authorized by on-ledger `RegularKey` or `SignerList` objects. + +## Apply: `doApply` + +`doApply()` returns `tesSUCCESS` immediately. The outer Batch transaction itself writes nothing directly to the ledger beyond its fee deduction and sequence increment (handled by the base class `apply()` method). The inner transaction execution occurs in `applyBatchTransactions()` inside `apply.cpp`, called by `applyTransaction()` only after the outer apply succeeds: + +```cpp +if (isTesSuccess(result.ter) && txn.getTxnType() == ttBATCH) +{ + OpenView wholeBatchView(batch_view, view); + if (applyBatchTransactions(registry, wholeBatchView, txn, j)) + wholeBatchView.apply(view); +} +``` + +Each inner transaction runs in its own `perTxBatchView` sandbox. On success, its changes are merged into `wholeBatchView`. Only if at least one inner transaction applied does `wholeBatchView` get merged into the authoritative ledger view. This two-level view isolation gives each inner transaction a clean snapshot to operate against while still seeing the cumulative effect of prior inner transactions in the batch. + +## Disabled Transaction Types + +The `disabledTxTypes` array is a `constexpr` compile-time list of `TxType` values that are forbidden as inner transactions. It currently blocks all Vault operations (`ttVAULT_*`), Loan Broker operations (`ttLOAN_BROKER_*`), and Loan operations (`ttLOAN_*`). These transaction families involve complex multi-ledger-object state transitions or are too new to have been validated in the batch execution context. The `preflight` implementation checks this array with `std::any_of` and returns `temINVALID_INNER_BATCH` on a match, giving the caller a specific error distinct from the general malformation errors. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/system/Change.h.ai.json b/include/xrpl/tx/transactors/system/Change.h.ai.json new file mode 100644 index 0000000000..47c2dba659 --- /dev/null +++ b/include/xrpl/tx/transactors/system/Change.h.ai.json @@ -0,0 +1,87 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 20, + "name": "view" + }, + { + "lineno": 20, + "name": "tx" + }, + { + "lineno": 25, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "Change" + } + ], + "description": "Defines the Change transactor class for system-level transactions in the XRPL, including amendment, fee, and UNL modification operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/system/Change.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "Change::Change" + }, + { + "args": [], + "lineno": 15, + "name": "Change::doApply" + }, + { + "args": [], + "lineno": 17, + "name": "Change::preCompute" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 20, + "name": "Change::calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 25, + "name": "Change::preclaim" + }, + { + "args": [], + "lineno": 29, + "name": "Change::applyAmendment" + }, + { + "args": [], + "lineno": 31, + "name": "Change::applyFee" + }, + { + "args": [], + "lineno": 33, + "name": "Change::applyUNLModify" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/system/Change.h.ai.md b/include/xrpl/tx/transactors/system/Change.h.ai.md new file mode 100644 index 0000000000..033894b08d --- /dev/null +++ b/include/xrpl/tx/transactors/system/Change.h.ai.md @@ -0,0 +1,53 @@ +# `Change` — System-Level Protocol Transactor + +## Role and Purpose + +`Change` is the single transactor responsible for applying all system-level, consensus-driven protocol mutations to a closed ledger. It handles three conceptually distinct operations — enabling an amendment, adjusting network fees, and modifying the Negative UNL validator set — under one class. The three type aliases declared at the bottom of the header make this explicit: + +```cpp +using EnableAmendment = Change; +using SetFee = Change; +using UNLModify = Change; +``` + +These aliases exist so that higher-level dispatch code can refer to each pseudo-transaction type by its semantic name without duplicating implementation. All three resolve to the same `Change` class, and `doApply()` switches on `getTxnType()` to route to `applyAmendment()`, `applyFee()`, or `applyUNLModify()`. + +## Pseudo-Transaction Identity + +Unlike ordinary user-submitted transactions, `Change` transactions are synthesized by the server during the ledger close process and are never sent by real accounts. The preflight specialization enforces this identity explicitly: the account field must be the zero account (`beast::zero`), the fee must be zero XRP, signatures must be absent, and the sequence number must be zero. Any deviation returns a `tem` error code immediately. + +This is why `calculateBaseFee()` is overridden to return `XRPAmount{0}`. Pseudo-transactions are not subject to fee markets, load scaling, or reserve checks — they represent network-internal protocol maintenance. + +The normal `invokePreflight` template checks amendment gating, flag masks, fee/signature validity, and signature validation in a fixed order. Because `Change` transactions violate almost all of those assumptions, the specialization `Transactor::invokePreflight` (defined in `Change.cpp`) replaces the entire default sequence with a minimal check: only `preflight0` (for txid and flag sanity) is called, followed by the account/fee/signature/sequence zero-checks described above. + +## Closed-Ledger Enforcement + +`preclaim()` is the first place where the open/closed ledger distinction is enforced. If `ctx.view.open()` is true, the transaction returns `temINVALID`. This guard exists because the mechanism that determines whether `tapOPEN_LEDGER` is set is not available during preflight, making `preclaim()` the appropriate checkpoint. Protocol-level changes are meaningless against an open ledger snapshot. + +`preclaim()` also validates fee format compatibility. The `ttFEE` transaction has two incompatible field sets depending on whether the `featureXRPFees` amendment is active: the old format uses fee-unit integers (`sfBaseFee`, `sfReferenceFeeUnits`, `sfReserveBase`, `sfReserveIncrement`), while the new format uses drop amounts (`sfBaseFeeDrops`, `sfReserveBaseDrops`, `sfReserveIncrementDrops`). The presence or absence of these fields is strictly enforced in both directions at `preclaim()` time, since `preclaim()` has access to the current rule set. + +## Amendment Lifecycle via `applyAmendment()` + +The amendment object in the ledger (accessed via `keylet::amendments()`) maintains two collections: `sfAmendments`, the list of already-active amendment hashes, and `sfMajorities`, the list of amendments that currently hold validator supermajority but have not yet been activated. + +`applyAmendment()` recognizes three mutually exclusive states signaled by the transaction's flags: + +- **`tfGotMajority`** — adds the amendment to the `sfMajorities` array with the current ledger's close time, recording when the supermajority was first achieved. If the amendment is already in that list, `tefALREADY` is returned. +- **`tfLostMajority`** — removes the amendment from `sfMajorities`, resetting its clock. If it was never in the list, `tefALREADY` is returned. +- **No flag set** — the amendment is being activated. It is added to `sfAmendments` and `AmendmentTable::enable()` is called on the service registry. If the server does not support this amendment, `NetworkOPs::setAmendmentBlocked()` is invoked, which prevents the node from participating further in consensus — the correct behavior when the network has advanced beyond the node's capabilities. + +## Fee Schedule Updates via `applyFee()` + +`applyFee()` reads from the transaction and writes directly to the ledger's fee schedule SLE (`keylet::fees()`). The dual-format handling mirrors what `preclaim()` validated: under `featureXRPFees`, it sets the drop-denominated fields and explicitly removes the old fee-unit fields using `makeFieldAbsent`, ensuring no stale data persists in the ledger. Without the feature, the reverse applies. + +## Negative UNL Modification via `applyUNLModify()` + +The Negative UNL (N-UNL) mechanism allows the network to continue making forward progress even when a subset of validators becomes unreliable. `applyUNLModify()` writes into the `keylet::negativeUNL()` ledger object to nominate a validator for disabling or re-enabling. + +Critically, this operation is only permitted on "flag ledgers" — specific ledger sequence numbers where the UNL adjustment protocol operates. Attempting it on any other ledger returns `tefFAILURE`. The method validates the transaction's `sfUNLModifyValidator` as a valid public key before proceeding. + +The object tracks at most one pending `sfValidatorToDisable` and one pending `sfValidatorToReEnable` at any time. Several consistency invariants are enforced: you cannot disable a validator already in the negative UNL; you cannot re-enable a validator not in it; and the same validator cannot simultaneously appear in both pending slots. Any violation returns `tefFAILURE` with a diagnostic log at warning level. + +## Design Rationale + +Consolidating three distinct pseudo-transaction types into a single `Change` class avoids three separate classes that would each need the same unusual preflight bypass, zero-fee override, and closed-ledger guard. The type aliases provide semantic clarity at the call site without code duplication. The dispatch in `doApply()` is exhaustive by design — the `default` branch is annotated `UNREACHABLE` and covered by `preclaim()`'s rejection of unknown transaction types, so it functions as a defensive assertion rather than live error handling. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/system/LedgerStateFix.h.ai.json b/include/xrpl/tx/transactors/system/LedgerStateFix.h.ai.json new file mode 100644 index 0000000000..c267314821 --- /dev/null +++ b/include/xrpl/tx/transactors/system/LedgerStateFix.h.ai.json @@ -0,0 +1,78 @@ +{ + "args": [ + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 20, + "name": "ctx" + }, + { + "lineno": 23, + "name": "view" + }, + { + "lineno": 23, + "name": "tx" + }, + { + "lineno": 26, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "LedgerStateFix" + } + ], + "description": "Defines the LedgerStateFix transactor class for applying specific ledger state fixes in the XRPL system, including fee calculation, preflight, and preclaim checks.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/system/LedgerStateFix.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 15, + "name": "LedgerStateFix" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 20, + "name": "preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 23, + "name": "calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 26, + "name": "preclaim" + }, + { + "args": [], + "lineno": 29, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/system/LedgerStateFix.h.ai.md b/include/xrpl/tx/transactors/system/LedgerStateFix.h.ai.md new file mode 100644 index 0000000000..9669cabbfc --- /dev/null +++ b/include/xrpl/tx/transactors/system/LedgerStateFix.h.ai.md @@ -0,0 +1,29 @@ +# `LedgerStateFix.h` — Ledger State Repair Transactor + +## Role and Purpose + +`LedgerStateFix` implements the `ttLEDGER_STATE_FIX` transaction type, a protocol-level maintenance mechanism that allows network participants to submit a transaction that corrects corrupted or inconsistent ledger state. It lives in `include/xrpl/tx/transactors/system/`, alongside peers like `Change` (validator/fee settings) and `Batch`, all of which perform protocol-level operations rather than ordinary value transfers. + +The class is gated on the `fixNFTokenPageLinks` amendment and currently supports a single fix operation: repairing broken linkage between NFToken page objects in an account's NFToken directory. The design uses an extensible `FixType` enum (`uint16_t`) to allow new repair operations to be added in future amendments without changing the transaction's overall structure. + +## Key Design Decisions + +**Dispatch via `FixType` enum.** The transaction carries a required `sfLedgerFixType` field (a `uint16_t`) that selects which repair operation to perform. All three lifecycle methods — `preflight`, `preclaim`, and `doApply` — switch on this value. Adding a new fix type in a future release means extending the `FixType` enum and adding a new case in each method. The `default` branch in each switch returns `tefINVALID_LEDGER_FIX_TYPE` (preflight) or `tecINTERNAL` (preclaim/doApply), with the latter two lines marked `// LCOV_EXCL_LINE` because preflight is supposed to prevent any unknown `FixType` from reaching those stages. This layered defense ensures that if preflight logic is ever broken, the subsequent stages still fail safely. + +**Owner-reserve fee, not base fee.** `calculateBaseFee()` delegates to `Transactor::calculateOwnerReserveFee()` rather than returning the network base fee. This is the same pricing strategy used by `AccountDelete`. The elevated fee (one owner reserve) serves as an economic guardrail: it deters frivolous invocations and ensures only accounts with genuinely broken state are worth the cost to fix. The caller pays this fee and it is non-refundable even if the repair finds nothing to correct. + +**`sfOwner` is conditionally required.** The transaction definition marks `sfOwner` as `soeOPTIONAL` at the protocol level to keep the schema flexible for future fix types. However, `preflight()` enforces that it is present when `FixType::nfTokenPageLink` is the selected operation, returning `temINVALID` otherwise. This way, future fix types that target a global or non-account resource need not carry a superfluous owner field. + +**`ConsequencesFactory{Normal}`.** This declaration tells the transaction queue that `LedgerStateFix` is not a blocker — it does not prevent other transactions from the same account from being processed. This is appropriate because the repair operation does not consume sequence numbers in a way that blocks subsequent queue entries. + +## Lifecycle + +`preflight` validates the `sfLedgerFixType` field and, for the `nfTokenPageLink` type, confirms that `sfOwner` is present. No amendment check is needed here because `invokePreflight` handles amendment gating via `checkExtraFeatures` and the permission registry before calling this method. + +`preclaim` reads the ledger view to confirm the target account identified by `sfOwner` actually exists (`keylet::account(owner)`), returning `tecOBJECT_NOT_FOUND` if not. This check happens at claim time rather than preflight time because ledger state can change between when a transaction is submitted and when it is applied. + +`doApply` calls `nft::repairNFTokenDirectoryLinks(view(), ctx_.tx[sfOwner])`, which walks the account's NFToken page chain and corrects any broken forward/backward links between pages. If the helper returns `false` (no repair was possible or an error occurred), `doApply` returns `tecFAILED_PROCESSING`; otherwise it returns `tesSUCCESS`. + +## Relationship to Related Files + +The implementation (`LedgerStateFix.cpp`) depends on `xrpl/ledger/helpers/NFTokenHelpers.h` for `nft::repairNFTokenDirectoryLinks`, which contains the actual page-link traversal and repair logic. The auto-generated `xrpl/protocol_autogen/transactions/LedgerStateFix.h` provides the type-safe `LedgerStateFix` wrapper and `LedgerStateFixBuilder` used by clients constructing these transactions; the builder enforces the required `sfLedgerFixType` at construction time. The transactor header itself is deliberately minimal: it declares only what the dispatch framework needs — the `FixType` enum, the `ConsequencesFactory` tag, and the four lifecycle methods — while all repair logic stays in the `.cpp`. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/system/TicketCreate.h.ai.json b/include/xrpl/tx/transactors/system/TicketCreate.h.ai.json new file mode 100644 index 0000000000..8eb59b16de --- /dev/null +++ b/include/xrpl/tx/transactors/system/TicketCreate.h.ai.json @@ -0,0 +1,56 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "TicketCreate" + } + ], + "description": "Defines the TicketCreate transactor class for the XRPL, which handles the creation of one or more Ticket objects in a single transaction, including constraints and limits for ticket creation.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/system/TicketCreate.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 38, + "name": "TicketCreate" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 43, + "name": "makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 46, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 50, + "name": "preclaim" + }, + { + "args": [], + "lineno": 53, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/system/TicketCreate.h.ai.md b/include/xrpl/tx/transactors/system/TicketCreate.h.ai.md new file mode 100644 index 0000000000..aa85b1b3d9 --- /dev/null +++ b/include/xrpl/tx/transactors/system/TicketCreate.h.ai.md @@ -0,0 +1,47 @@ +# `TicketCreate.h` — Ticket Batch Creation Transactor + +`TicketCreate` is the transactor responsible for minting one or more `Ticket` ledger objects in a single XRPL transaction. Tickets are placeholder sequence-number reservations that allow account owners to submit future transactions out-of-order or in parallel, bypassing the strict account sequence increment that governs normal transaction ordering. + +## Role in the Transactor Framework + +`TicketCreate` inherits from `Transactor` and participates in the standard three-phase processing pipeline: `preflight` → `preclaim` → `doApply`. Because it must express a non-standard relationship between a single transaction and multiple consumed sequence numbers, it declares `ConsequencesFactory{Custom}`, which signals the framework to invoke `makeTxConsequences` rather than deriving consequences from the default Normal or Blocker rules. + +## Calibrated Limits as `constexpr` Constants + +The header embeds three `constexpr` values that function as protocol-level policy: + +- **`minValidCount = 1`**: A TicketCreate must create at least one ticket; zero is nonsensical and rejected. +- **`maxValidCount = 250`**: The upper bound per transaction. The comment explains the empirical basis: on a MacBook Pro release build with assertions disabled, creating 250 tickets in `doApply()` averaged 1.21 ms, matching the 1.25 ms average for a compute-intensive three-path Payment. The cap exists to keep a single TicketCreate from consuming more validator CPU than any other class of transaction. Encoding this benchmark in a comment next to the constant is deliberate — it makes future reviewers aware of the performance contract being enforced. +- **`maxTicketThreshold = 250`**: An account-level ceiling on total held tickets, chosen to prevent ledger stuffing. This is a per-account invariant enforced during `preclaim`. + +These constants are `constexpr` rather than runtime parameters so that the compiler can use them in static assertions and the values are embedded in the binary without any runtime lookup. + +## Phase-by-Phase Design + +**`makeTxConsequences`** constructs a `TxConsequences` object using the transaction's `sfTicketCount` field. This is critical: the consequences object informs the transaction ordering machinery how many future sequence numbers this transaction "claims," enabling the open-ledger queue to correctly evaluate whether a later transaction referencing one of those ticket sequences can be ordered after this one. + +**`preflight`** performs cheap, stateless validation: it reads `sfTicketCount` from the transaction and returns `temINVALID_COUNT` if the value falls outside `[minValidCount, maxValidCount]`. No ledger state is touched here — this is intentional, as preflight runs without a write lock and may be called speculatively. + +**`preclaim`** introduces state-aware validation against the read-only ledger view. It computes the net ticket delta: + +```cpp +curTicketCount + addedTickets - consumedTickets > maxTicketThreshold +``` + +The `consumedTickets` term accounts for a subtle case: if the TicketCreate itself was submitted *using* a ticket (i.e., `getSeqProxy().isTicket()` is true), one existing ticket is consumed in the process, so the net increase to the account's ticket inventory is `addedTickets - 1` rather than `addedTickets`. Unsigned integer underflow is explicitly ruled out by the relationship between the three terms, which the comment annotates. Violation returns `tecDIR_FULL`. + +**`doApply`** carries out the actual mutation. Key design choices: + +1. **Reserve check against `preFeeBalance_`**: The balance check uses the *pre-fee* balance, allowing the account to dip into its reserve to pay the transaction fee without being rejected. This is the consistent XRPL policy across all reserve-sensitive transactors. + +2. **Sequence anchoring**: The first ticket sequence is read directly from the account root's current `sfSequence` after the transaction machinery has already incremented it. A sanity check confirms this invariant: `txSeq == firstTicketSeq - 1` (or `txSeq == 0` for ticket-submitted transactions). This ensures the generated ticket sequences form a contiguous, non-overlapping block aligned to the account's sequence history. + +3. **SLE creation loop**: For each ticket, a new `SLE` is created with `keylet::ticket(account_, curTicketSeq)`, populated with the account ID and ticket sequence, inserted into the ledger, and then wired into the account's owner directory via `dirInsert`. The `sfOwnerNode` field on each ticket records which directory page holds its entry, enabling efficient deletion later. + +4. **Bulk sequence advance**: After the loop, `sfSequence` is set to `firstTicketSeq + ticketCount`. This is explicitly noted in the implementation comment as the only transaction in the XRPL that advances an account's sequence by more than one in a single application — all other transactors advance it by exactly one. This bulk advance is what makes the ticket sequences permanently reserved. + +5. **Owner count and ticket count maintenance**: `adjustOwnerCount` is called with `+ticketCount` to update the account's reserve obligation, and `sfTicketCount` on the account root is incremented accordingly. Both fields must stay synchronized to make `preclaim`'s threshold check accurate in future transactions. + +## Relationship to Sibling Transactors + +Among the system-level transactors (`Batch.h`, `Change.h`, `LedgerStateFix.h`, `TicketCreate.h`), `TicketCreate` is the only one that directly allocates new ledger objects in bulk and requires the `Custom` consequences factory. The `Transactor` base also exposes `ticketDelete()` as a shared utility — it is used by `AccountDelete` to clean up outstanding tickets when an account is removed, maintaining the invariant that ticket SLEs cannot outlive their owning account root. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/Clawback.h.ai.json b/include/xrpl/tx/transactors/token/Clawback.h.ai.json new file mode 100644 index 0000000000..df00c23644 --- /dev/null +++ b/include/xrpl/tx/transactors/token/Clawback.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "Clawback" + } + ], + "description": "Defines the Clawback transaction class for the XRPL, including its interface and integration with the transaction processing framework.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/token/Clawback.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "Clawback" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 21, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/Clawback.h.ai.md b/include/xrpl/tx/transactors/token/Clawback.h.ai.md new file mode 100644 index 0000000000..8e1fd8e36d --- /dev/null +++ b/include/xrpl/tx/transactors/token/Clawback.h.ai.md @@ -0,0 +1,53 @@ +# `Clawback.h` — Clawback Transactor Declaration + +## Role in the System + +This header declares `Clawback`, the transactor class responsible for handling `ttCLAWBACK` transactions on the XRP Ledger. Its position in `include/xrpl/tx/transactors/token/` places it alongside the other token-lifecycle transactors — `TrustSet`, `MPTokenIssuanceCreate`, `MPTokenAuthorize`, and others — that collectively govern how fungible value is issued and managed on-ledger. The `Clawback` transactor enforces an issuer's right to reclaim distributed tokens from holder accounts, a feature introduced for regulatory compliance use cases where an issuer needs to reverse a distribution. + +The header is intentionally minimal: it exposes only the class declaration and the three well-defined hooks the transaction processing pipeline requires. All implementation logic lives in the corresponding `src/libxrpl/tx/transactors/token/Clawback.cpp`. + +## The `Transactor` Inheritance Contract + +`Clawback` inherits from `Transactor`, the abstract base that drives the entire transaction processing pipeline in rippled. The pipeline is strictly three-phase: + +1. **`preflight`** — stateless, read-only validation run before the transaction touches any ledger state. It checks structural correctness of the transaction fields themselves. +2. **`preclaim`** — read-only validation against the current ledger view. It verifies that the preconditions for success actually hold (the trust line exists, the issuer flag is set, the balance is non-zero). +3. **`doApply`** — the only mutable phase, where ledger state is actually modified. + +These three methods are not virtual in the traditional sense. The base class documents this explicitly: they are invoked through `invokePreflight`, a template function that uses name hiding (compile-time polymorphism) rather than vtable dispatch. This design keeps the hot path overhead-free while still allowing derived classes to override individual phases. Only `doApply()` is declared `override` — the other two are `static` and participate in the compile-time dispatch machinery. + +## `ConsequencesFactory` and Fee Semantics + +```cpp +static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; +``` + +This constant tells the transaction consequence system how to model the worst-case fee impact of the transaction before it is applied. `Normal` means the transaction claims a fee exactly equal to its declared fee field and does not block other transactions from the same account. The alternative `Blocker` type is used for transactions like `AccountDelete` that prevent further processing of transactions from the account, and `Custom` allows per-transaction consequence calculation. `Clawback` is `Normal` because it is a straightforward fee-paying operation with no unusual sequencing implications. + +## Dual Token Type Architecture + +The most architecturally significant aspect of `Clawback` — visible only by reading the implementation — is its complete dual support for both IOU trust-line tokens (`Issue`) and Multi-Purpose Tokens (`MPTIssue`). The implementation uses C++20 template specialization with `std::visit` to dispatch over the asset variant held in `sfAmount`: + +```cpp +return std::visit( + [&](T const&) { return preflightHelper(ctx); }, + ctx.tx[sfAmount].asset().value()); +``` + +Each phase (`preflight`, `preclaim`, `doApply`) has a pair of `preflightHelper` / `preflightHelper` specializations. This avoids a single monolithic function full of `if (isIOU) ... else if (isMPT) ...` branches, keeping the asset-type-specific logic isolated and independently testable. + +### IOU Clawback Path + +For IOU-based tokens, `preflight` validates that the `sfHolder` field is *absent* (for IOUs, the holder identity is embedded in the `sfAmount`'s issuer field — an intentional encoding choice that avoids adding a separate required field). `preclaim` then confirms that the issuer account has `lsfAllowTrustLineClawback` set and that `lsfNoFreeze` is *not* set — an account that permanently waived freeze rights cannot retain clawback capability, making the flags mutually exclusive at the protocol level. The trust line balance orientation is also validated: XRPL trust line balances are signed and stored from the perspective of the lower-address account, so `preclaim` checks that the sign of the balance matches the expected issuer/holder relationship before calling `accountHolds` (rather than reading the raw balance directly) to account for edge cases introduced by features like XLS-34. + +### MPT Clawback Path + +For MPT-based tokens, the encoding is different: the holder account is supplied in the separate `sfHolder` field rather than inside `sfAmount`. `preflight` requires `sfHolder` to be present and validates the amount against `maxMPTokenAmount`. `preclaim` reads the `MPTokenIssuance` ledger object and checks the `lsfMPTCanClawback` flag on the issuance itself (not on the issuer account), then confirms the `MPToken` object for that holder exists. MPT clawback is additionally gated on the `featureMPTokensV1` amendment check inside `preflightHelper`. + +### Application Phase + +`doApply()` calls `directSendNoFee` to move tokens from the holder's account back to the issuer — a ledger primitive that transfers IOU or MPT balances without applying transfer fees. The amount transferred is `min(spendableAmount, clawAmount)`, where `spendableAmount` is obtained via `accountHolds` with `fhIGNORE_FREEZE` to bypass any freeze state the issuer may have set. This means clawback always succeeds up to the available balance even if the account is frozen, reflecting the regulatory intent: the issuer is reclaiming their own issued value. + +## Guards Against Special Account Types + +The `preclaim` implementation contains two noteworthy defensive checks before dispatching to the asset-type helpers. If the `featureSingleAssetVault` amendment is active, clawback is explicitly rejected against pseudo-accounts (returning `tecPSEUDO_ACCOUNT`). If the holder is an AMM account (indicated by the presence of `sfAMMID` on the holder's account root), the transaction is rejected with `tecAMM_ACCOUNT`. AMM liquidity pool accounts hold tokens on behalf of the protocol and should not be subject to issuer clawback; the dedicated `AMMClawback` transactor exists for recovering tokens from an AMM position. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/MPTokenAuthorize.h.ai.json b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h.ai.json new file mode 100644 index 0000000000..825005109c --- /dev/null +++ b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h.ai.json @@ -0,0 +1,85 @@ +{ + "args": [ + { + "lineno": 8, + "name": "priorBalance" + }, + { + "lineno": 9, + "name": "mptIssuanceID" + }, + { + "lineno": 10, + "name": "account" + }, + { + "lineno": 11, + "name": "flags" + }, + { + "lineno": 12, + "name": "holderID" + }, + { + "lineno": 17, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "XRPAmount const& priorBalance", + "MPTID const& mptIssuanceID", + "AccountID const& account", + "std::uint32_t flags", + "std::optional holderID" + ], + "lineno": 6, + "name": "MPTAuthorizeArgs" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 15, + "name": "MPTokenAuthorize" + } + ], + "description": "Defines the MPTokenAuthorize transactor and its supporting argument struct for authorizing multiparty tokens in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/token/MPTokenAuthorize.h", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 22, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 25, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 28, + "name": "preclaim" + }, + { + "args": [], + "lineno": 31, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/MPTokenAuthorize.h.ai.md b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h.ai.md new file mode 100644 index 0000000000..3afb7b8b09 --- /dev/null +++ b/include/xrpl/tx/transactors/token/MPTokenAuthorize.h.ai.md @@ -0,0 +1,43 @@ +# `MPTokenAuthorize.h` — Multi-Purpose Token Authorization Transactor + +This header declares the `MPTokenAuthorize` transactor and its companion argument struct, together forming the entry point for the `MPTokenAuthorize` transaction (type `ttMPTOKEN_AUTHORIZE`, opcode 57) on the XRP Ledger. The transaction is gated behind the `featureMPTokensV1` amendment and handles the bidirectional authorization handshake that governs who may hold a given Multi-Purpose Token (MPT) issuance. + +## Why This Transactor Exists + +MPTs introduce a more structured ownership model than XRPL's older trust lines. An issuance marked with `lsfMPTRequireAuth` demands explicit issuer approval before a holder can receive tokens — a pattern used for regulated or permissioned assets. At the same time, any account that wants to hold an MPT (regardless of `RequireAuth`) must first opt in by creating an `MPToken` ledger object. The `MPTokenAuthorize` transaction serves both sides of this two-step handshake through a single transaction type, with the optional `sfHolder` field acting as the discriminator between the holder role and the issuer role. + +## `MPTAuthorizeArgs` + +`MPTAuthorizeArgs` is a plain aggregation struct that bundles the inputs required for the core authorization logic implemented in `authorizeMPToken()` (declared in `MPTokenHelpers.h`). It holds: + +- `priorBalance` — the submitting account's XRP balance before fees, used to check reserve requirements when creating a new `MPToken` object. +- `mptIssuanceID` — the 192-bit identifier of the target MPT issuance. +- `account` — the account that submitted the transaction. +- `flags` — transaction flags, most importantly `tfMPTUnauthorize`. +- `holderID` — present only when the issuer is acting on behalf of a specific holder. + +This struct exists so that `authorizeMPToken()` can be called from contexts other than the transactor itself — for example, vault creation code in `VaultCreate.cpp` and `VaultDeposit.cpp` reuse the same helper to implicitly authorize vault pseudo-accounts without going through a full transaction. Separating the arguments into a struct keeps the helper callable with named fields rather than a positional argument list. + +## `MPTokenAuthorize` Class + +`MPTokenAuthorize` inherits from `Transactor` and implements the standard three-phase XRPL transaction lifecycle: `preflight` → `preclaim` → `doApply`. + +**`getFlagsMask()`** returns `tfMPTokenAuthorizeMask`, which tells the base framework's `preflight1()` which flag bits are valid for this transaction type. This is a static method resolved at compile time through name hiding (not virtual dispatch) — the `invokePreflight()` template in `Transactor.h` calls `T::getFlagsMask()` directly. + +**`preflight()`** is intentionally minimal — it only rejects the degenerate case where `sfAccount == sfHolder` (an account authorizing itself is nonsensical and likely a client bug). All amendment feature checks are handled upstream by `invokePreflight()` via `Permission::getInstance().getTxFeature()`, so `preflight()` itself does not need to touch amendment state. + +**`preclaim()`** is where the meaningful validation happens against ledger state (after signature verification but before fee consumption). It branches on whether `sfHolder` is present: + +- **No `sfHolder` — holder path**: The submitting account is acting on its own behalf. If `tfMPTUnauthorize` is set, it attempts to delete the existing `MPToken` object; this is blocked if `sfMPTAmount` or `sfLockedAmount` is non-zero (`tecHAS_OBLIGATIONS`), and also blocked when `featureSingleAssetVault` is active and the token carries the `lsfMPTLocked` flag. Without `tfMPTUnauthorize`, it verifies the issuance exists, confirms the submitter is not the issuer (issuers cannot hold their own MPT), and rejects if an `MPToken` object already exists (`tecDUPLICATE`). + +- **With `sfHolder` — issuer path**: The submitting account is acting as the issuer to allowlist or revoke allowlist for the named holder. It confirms the submitter really is the issuer (`tecNO_PERMISSION` otherwise), verifies the issuance has `lsfMPTRequireAuth` set (otherwise `tecNO_AUTH`, since granting auth on a non-auth issuance is meaningless), and requires that the holder has already created their `MPToken` entry — the ledger enforces a holder-first, issuer-second handshake. A special guard prevents authorizing pseudo-accounts (vaults and loan brokers, identified via `isPseudoAccount()`), because those are implicitly always authorized by the protocol. + +**`doApply()`** is a thin delegation layer that calls `authorizeMPToken()` from `MPTokenHelpers`, passing `preFeeBalance_` (the pre-fee XRP balance stored by the base class) along with the transaction fields. The actual ledger mutations — creating or deleting the `MPToken` SLE, adjusting directory entries, and toggling the `lsfMPTAuthorized` flag — live entirely in that helper, making them reusable across multiple code paths. + +## Design Notes + +`ConsequencesFactory` is set to `Normal`, meaning a transaction that fails during `preclaim` or `doApply` still consumes its sequence number and fee — consistent with the standard XRPL policy for transactions that reach the network in a well-formed state. + +The auto-generated protocol layer (`xrpl::transactions::MPTokenAuthorize` in `protocol_autogen/`) marks this transaction as `Delegation::delegable`, allowing a credentialed delegate to submit it on behalf of the owner account. The required delegation privilege is `mustAuthorizeMPT`. + +The deliberate asymmetry in `preclaim()` — where the same transaction type, differentiated only by the presence of `sfHolder`, handles fundamentally different operations — avoids the overhead of two separate transaction types for what is logically one lifecycle event: establishing that a given account may hold a given MPT issuance. The two-step protocol (holder opts in first, issuer approves second) mirrors the trust line model's bidirectional consent while keeping the ledger object footprint predictable. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h.ai.json b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h.ai.json new file mode 100644 index 0000000000..5442af305b --- /dev/null +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h.ai.json @@ -0,0 +1,111 @@ +{ + "args": [ + { + "lineno": 10, + "name": "priorBalance" + }, + { + "lineno": 11, + "name": "account" + }, + { + "lineno": 12, + "name": "sequence" + }, + { + "lineno": 13, + "name": "flags" + }, + { + "lineno": 14, + "name": "maxAmount" + }, + { + "lineno": 15, + "name": "assetScale" + }, + { + "lineno": 16, + "name": "transferFee" + }, + { + "lineno": 17, + "name": "metadata" + }, + { + "lineno": 18, + "name": "domainId" + }, + { + "lineno": 19, + "name": "mutableFlags" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "MPTCreateArgs" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 25, + "name": "MPTokenIssuanceCreate" + } + ], + "description": "Defines the MPTokenIssuanceCreate transactor class and supporting argument struct for creating a new Multi-Party Token (MPT) issuance on the XRPL, including preflight checks, flag handling, and creation logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 27, + "name": "MPTokenIssuanceCreate" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 32, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 35, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 38, + "name": "preflight" + }, + { + "args": [], + "lineno": 41, + "name": "doApply" + }, + { + "args": [ + "ApplyView& view", + "beast::Journal journal", + "MPTCreateArgs const& args" + ], + "lineno": 44, + "name": "create" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h.ai.md b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h.ai.md new file mode 100644 index 0000000000..f51a3c8c8a --- /dev/null +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceCreate.h.ai.md @@ -0,0 +1,53 @@ +# `MPTokenIssuanceCreate.h` — MPT Issuance Creation Transactor + +## Role in the System + +This header defines the transactor class and its parameter aggregate for creating a new Multi-Party Token (MPT) issuance on the XRP Ledger. MPTs are a newer token primitive that carries significantly more metadata than classic trust-line IOUs: optional supply caps, decimal scaling, configurable transfer fees, arbitrary binary metadata, and permissioned-domain constraints. `MPTokenIssuanceCreate` is the entry point for producing the `MPTokenIssuance` ledger object that anchors all subsequent holder balances. + +## `MPTCreateArgs` — A Shared Parameter Struct + +The struct exists specifically to decouple the creation logic from the transaction-processing context. Rather than having `doApply()` call internal helpers that read directly from `ctx_.tx`, all inputs are first gathered into an `MPTCreateArgs` value and then forwarded to the static `create()` method. This lets other transactors that need to mint an MPT issuance as a side effect — most notably `VaultCreate`, which mints a share-token issuance when creating a single-asset vault — reuse the same creation path without going through a full transaction flow. + +Notable fields: + +- `priorBalance` — optional `XRPAmount` representing the account's balance before fee deduction. When present, `create()` uses it to enforce the reserve requirement (`tecINSUFFICIENT_RESERVE`). When absent (e.g., during vault share creation, where the pseudo-account's reserve is managed separately), the check is skipped. +- `sequence` — combined with `account` to derive the deterministic `MPTID` via `makeMptID()`. +- `flags` / `mutableFlags` — the transaction-level flags are stored stripped of universal bits (`~tfUniversal`) into the `sfFlags` field of the issuance SLE. The optional `mutableFlags` field (gated behind `featureDynamicMPT`) records which of those flags can be changed after issuance. +- `domainId` — links the issuance to a permissioned domain; requires both the `featurePermissionedDomains` and `featureSingleAssetVault` amendments. + +## `MPTokenIssuanceCreate` — Transactor Lifecycle + +The class follows the standard XRPL transactor pattern by inheriting `Transactor` and implementing three static preflight hooks and the `doApply()` virtual method: + +**`checkExtraFeatures()`** gates two optional field categories at the amendment level. If `sfDomainID` appears in the transaction but neither `featurePermissionedDomains` nor `featureSingleAssetVault` is enabled, the method returns `false`, which `invokePreflight` translates to `temDISABLED`. Similarly, `sfMutableFlags` requires `featureDynamicMPT`. This pattern keeps amendment checks entirely separate from semantic validation in `preflight()`. + +**`getFlagsMask()`** returns `tfMPTokenIssuanceCreateMask`, the bitmask of all flags legal for this transaction type. `invokePreflight()` passes this to `preflight1()`, which rejects any unknown flag bits before reaching the transaction's own `preflight()`. + +**`preflight()`** enforces four semantic constraints: + +1. If `sfMutableFlags` is present it must be non-zero and must not contain bits outside the mutable-flags mask — preventing zero-effect or invalid update grants. +2. A non-zero `sfTransferFee` (max `maxTransferFee` = 50,000 basis points) is only meaningful when `tfMPTCanTransfer` is also set; inconsistency returns `temMALFORMED`. +3. A `sfDomainID` of zero is malformed, and a non-zero domain ID implies the issuance is not public — `tfMPTRequireAuth` must be set, otherwise holders outside the permissioned domain could not legally be authorized. +4. `sfMPTokenMetadata` must be non-empty and at most `maxMPTokenMetadataLength` (1024) bytes. `sfMaximumAmount`, if provided, must be positive and within the unsigned 63-bit ceiling `maxMPTokenAmount` (0x7FFF_FFFF_FFFF_FFFF). + +## The Static `create()` Method and Its Return Type + +```cpp +static Expected +create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args); +``` + +Returning `Expected` rather than plain `TER` is deliberate: success carries a usable value (the new issuance ID) rather than requiring the caller to recompute it. The caller wraps failure with `Unexpected(tecINTERNAL)`, `Unexpected(tecINSUFFICIENT_RESERVE)`, or `Unexpected(tecDIR_FULL)`. + +Inside `create()`, the `MPTID` is deterministically computed as the keccak/sha512-half of `(sequence, account)` via `makeMptID()`, which is itself a 192-bit `base_uint`. The function then: + +1. Checks the account's XRP reserve if `priorBalance` is set. +2. Inserts an entry into the issuer's owner directory via `view.dirInsert()` — failing with `tecDIR_FULL` if the directory page is exhausted. +3. Constructs the `MPTokenIssuance` SLE with mandatory fields (`sfFlags`, `sfIssuer`, `sfOutstandingAmount = 0`, `sfOwnerNode`, `sfSequence`) and conditionally populates all optional fields. +4. Increments the account's `sfOwnerCount` through `adjustOwnerCount()`, which affects future reserve calculations. + +`doApply()` simply packages `ctx_.tx` fields into `MPTCreateArgs` and delegates to `create()`, collapsing the `Expected` result to `tesSUCCESS` or its embedded error. This thin wrapper is the reason `create()` takes an `ApplyView&` and `beast::Journal` directly — it needs no `ApplyContext` beyond those two, which keeps `VaultCreate` from needing to fabricate a full context. + +## Design Tradeoffs + +The separation of `MPTCreateArgs` and the static `create()` method is a deliberate composability pattern seen throughout the vault transactor family. It avoids copy-pasting ledger-mutation logic and means that any future transactor that needs to programmatically issue an MPT can do so with the same safety guarantees as the user-facing transaction, including reserve enforcement and directory insertion, simply by choosing whether to pass `priorBalance` or not. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h.ai.json b/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h.ai.json new file mode 100644 index 0000000000..46999cf9da --- /dev/null +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 14, + "name": "ctx" + }, + { + "lineno": 17, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "MPTokenIssuanceDestroy" + } + ], + "description": "Defines the MPTokenIssuanceDestroy transactor class for destroying Multi-Party Token issuances in the XRPL transaction processing system.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "MPTokenIssuanceDestroy" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 14, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 17, + "name": "preclaim" + }, + { + "args": [], + "lineno": 19, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h.ai.md b/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h.ai.md new file mode 100644 index 0000000000..ab48017c4a --- /dev/null +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceDestroy.h.ai.md @@ -0,0 +1,38 @@ +# `MPTokenIssuanceDestroy.h` — Transactor for Destroying MPToken Issuances + +## Role in the System + +This header declares the `MPTokenIssuanceDestroy` transactor, which handles transaction type `ttMPTOKEN_ISSUANCE_DESTROY` (opcode 55) on the XRP Ledger. Its sole responsibility is permanently deleting a Multi-Party Token (MPT) issuance object from the ledger — reclaiming the issuer's owner-count slot and removing the corresponding entry from the owner directory. The transaction is gated behind the `featureMPTokensV1` amendment and is classified as delegable with the `destroyMPTIssuance` privilege, meaning an authorized delegate account may submit it on the issuer's behalf. + +## Three-Phase Transactor Pipeline + +Like every `Transactor` subclass, `MPTokenIssuanceDestroy` participates in the framework's three-phase execution model. The class exposes `preflight`, `preclaim`, and `doApply` as the three checkpoints. The base `Transactor::invokePreflight` template wires these together at compile time, interleaving them with the framework's own signature validation and flag-mask checks — no virtual dispatch is involved at the `preflight`/`preclaim` layer. + +**`preflight`** unconditionally returns `tesSUCCESS`. This is an intentional design: every meaningful precondition for this transaction depends on reading current ledger state, which is unavailable during preflight (the stage that runs without a ledger view). There are no transaction-field validity checks beyond what the framework itself enforces (correct sequence, valid fee, well-formed signature). Keeping `preflight` trivial is correct and cheap. + +**`preclaim`** is where all substantive guards live. It reads the MPToken issuance object from the ledger using `keylet::mptIssuance(sfMPTokenIssuanceID)` and enforces three invariants before allowing the transaction to proceed: + +1. The issuance object must exist (`tecOBJECT_NOT_FOUND` if not). +2. The `sfIssuer` field of that object must match the transaction submitter (`tecNO_PERMISSION` if not). This prevents any account from destroying an issuance it does not own. +3. Both `sfOutstandingAmount` and `sfLockedAmount` must be zero (`tecHAS_OBLIGATIONS` if either is non-zero). An issuance can only be destroyed when no tokens remain in circulation and no tokens are locked — this protects token holders from having their holdings orphaned by a premature destruction. + +The `sfLockedAmount` branch carries `// LCOV_EXCL_LINE`, signalling that test coverage does not reach it. This reflects a protocol invariant: if `sfOutstandingAmount` is already zero, `sfLockedAmount` being non-zero would represent a corrupted ledger state that the normal transaction flow cannot produce. The check is nonetheless present as a defensive backstop. + +**`doApply`** executes the actual ledger mutation once both prior phases have cleared. It: +1. Peeks the MPT issuance SLE via `view().peek(keylet::mptIssuance(...))`. +2. Asserts the cached `account_` is the issuer (the `tecINTERNAL` branch is also `LCOV_EXCL_LINE` — unreachable in practice since `preclaim` already validated ownership). +3. Removes the issuance from the account's owner directory via `view().dirRemove(...)`. A failure here returns `tefBAD_LEDGER`, indicating ledger corruption rather than a user error. +4. Erases the issuance SLE with `view().erase(mpt)`. +5. Decrements the issuer's owner count by one via `adjustOwnerCount`, keeping the reserve accounting accurate. + +## Design Observations + +The `ConsequencesFactory` is set to `Normal`, meaning the transaction has standard fee consequences and does not act as a blocker in the transaction queue. This is appropriate: destroying an issuance is a low-priority cleanup operation that does not need to preempt other transactions. + +The absence of a `preclaim` override in the base `Transactor` (which simply returns `tesSUCCESS`) is overridden here with a `static TER preclaim(PreclaimContext const&)` — the static method pattern the framework uses for compile-time polymorphism rather than virtual dispatch. Derived transactors override by name-hiding, not by virtual override, so the signature exactly matches the base class declaration with the same static qualifier. + +Compared to `MPTokenIssuanceCreate`, this class is notably simpler: it carries no helper `create()`-style static utility, no custom `checkExtraFeatures`, and no `getFlagsMask` override. Destruction is a single-object, single-issuer operation with no fields beyond the issuance ID, so the minimal surface area is appropriate. + +## Relationship to Sibling Files + +Within `include/xrpl/tx/transactors/token/`, `MPTokenIssuanceDestroy.h` sits alongside `MPTokenIssuanceCreate.h`, `MPTokenIssuanceSet.h`, `MPTokenAuthorize.h`, `Clawback.h`, and `TrustSet.h`. Together these form the full lifecycle management surface for MPTs on the ledger — creation, configuration, holder authorization, clawback, and destruction. The auto-generated `include/xrpl/protocol_autogen/transactions/MPTokenIssuanceDestroy.h` provides a separate type-safe builder/wrapper (`MPTokenIssuanceDestroyBuilder`) for constructing the transaction's `STTx` representation, completely independent of this transactor header. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h.ai.json b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h.ai.json new file mode 100644 index 0000000000..ddf95bba89 --- /dev/null +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h.ai.json @@ -0,0 +1,93 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 14, + "name": "ctx" + }, + { + "lineno": 17, + "name": "ctx" + }, + { + "lineno": 20, + "name": "ctx" + }, + { + "lineno": 23, + "name": "view" + }, + { + "lineno": 23, + "name": "tx" + }, + { + "lineno": 26, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 6, + "name": "MPTokenIssuanceSet" + } + ], + "description": "Defines the MPTokenIssuanceSet transactor class for handling token issuance set transactions in the XRPL system.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 14, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 17, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 20, + "name": "preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 23, + "name": "checkPermission" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 26, + "name": "preclaim" + }, + { + "args": [], + "lineno": 28, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h.ai.md b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h.ai.md new file mode 100644 index 0000000000..ff171ce02b --- /dev/null +++ b/include/xrpl/tx/transactors/token/MPTokenIssuanceSet.h.ai.md @@ -0,0 +1,37 @@ +# `MPTokenIssuanceSet.h` — MPT Issuance Lifecycle Management Transactor + +## Role in the System + +`MPTokenIssuanceSet` is the transactor that handles `ttMPTOKEN_ISSUANCE_SET` transactions on the XRP Ledger. Its responsibility is twofold: it can either toggle the lock/unlock state on an existing MPTokenIssuance ledger object or on an individual holder's `MPToken` slot, and — when the `featureDynamicMPT` amendment is active — it can mutate post-creation properties of an issuance such as its `mutableFlags`, `sfTransferFee`, `sfMPTokenMetadata`, and `sfDomainID`. + +The class fits into the broader MPT (Multi-Purpose Token) transactor family, which also includes `MPTokenIssuanceCreate`, `MPTokenIssuanceDestroy`, and `MPTokenAuthorize`. Together these classes manage the full lifecycle of token issuances on the ledger. + +## Inheritance and the Static-Polymorphism Pattern + +`MPTokenIssuanceSet` inherits from the abstract base class `Transactor` and follows the framework's deliberate pattern of *static* (compile-time) polymorphism rather than virtual dispatch for the three main pipeline phases. The base class template `invokePreflight` drives the entire preflight pipeline — it calls `T::checkExtraFeatures`, then `preflight1` (account/fee sanity), then `T::preflight`, then `preflight2` (signature validation). By declaring each of these methods as `static`, derived transactors participate in the pipeline without the overhead or accidental-override risk of virtual functions. `ConsequencesFactory{Normal}` tells the framework that this transaction follows standard fee-consequence rules — it does not unconditionally block future transactions from the same account. + +## The Four Static Entry Points + +**`checkExtraFeatures`** guards amendment requirements that aren't already captured by the global transaction-to-feature map. For `MPTokenIssuanceSet`, the only extra requirement is that a transaction carrying `sfDomainID` must have both `featurePermissionedDomains` *and* `featureSingleAssetVault` enabled simultaneously. Returning `false` causes `invokePreflight` to return `temDISABLED` without ever touching the rest of the pipeline. + +**`getFlagsMask`** returns `tfMPTokenIssuanceSetMask`, restricting the flags field to only bits this transaction legitimately owns. `preflight1` uses this mask to reject any transaction that sets undefined flag bits, keeping the flags space tidy and extensible. + +**`preflight`** performs stateless semantic validation. Several invariants are checked here: `sfDomainID` and `sfHolder` are mutually exclusive; `tfMPTLock` and `tfMPTUnlock` cannot be set simultaneously; the submitting account cannot be the same as the `sfHolder`. Under `featureDynamicMPT`, additional rules apply: mutation fields (`sfMutableFlags`, `sfMPTokenMetadata`, `sfTransferFee`) cannot coexist with `sfHolder` or non-universal tx flags; you cannot simultaneously set a non-zero `sfTransferFee` and clear `tmfMPTClearCanTransfer`; and the `sfMutableFlags` value must not be zero and must not include unknown bits. Importantly, when either `featureSingleAssetVault` or `featureDynamicMPT` is enabled, a transaction that changes *nothing* (zero flags, no domain, no mutation fields) is rejected as `temMALFORMED` — an empty mutation is meaningless. + +**`checkPermission`** handles delegate authorization. When the transaction carries an `sfDelegate` field, it looks up the delegate SLE and first tries a broad transaction-level permission check via `checkTxPermission`. If that fails, it falls back to granular permissions: `MPTokenIssuanceLock` is required for `tfMPTLock` and `MPTokenIssuanceUnlock` for `tfMPTUnlock`. This layered approach lets a delegate be granted either blanket `MPTokenIssuanceSet` authority or fine-grained lock-only or unlock-only authority. + +## Preclaim: State-Dependent Validation + +`preclaim` runs after signature verification against a read-only ledger view. It verifies that the target `MPTokenIssuance` object exists and that the submitter is the issuer. The logic around `lsfMPTCanLock` deserves attention: if the issuance does not have `lsfMPTCanLock` set and neither `featureSingleAssetVault` nor `featureDynamicMPT` is enabled, any lock/unlock attempt fails with `tecNO_PERMISSION`. Under those newer amendments the lock flag is only required if the tx actually tries to lock or unlock. This backwards-compatible branching avoids breaking existing behaviour for older ledger states. + +When `sfHolder` is present, `preclaim` additionally verifies that the holder account exists and that their `MPToken` slot exists for this issuance. For `sfDomainID`, it enforces that the issuance has `lsfMPTRequireAuth` set (only auth-required issuances use domain-restricted holder sets), and that the referenced `PermissionedDomain` object exists (unless the zero value is supplied to clear the domain link). + +Mutation validation in `preclaim` uses a table-driven approach. The file-local `mptMutabilityFlags` array (defined in the `.cpp`) maps each set/clear tx flag pair to the corresponding `canMutateFlag` that must be present in the current `sfMutableFlags` of the ledger object. `std::any_of` over this array checks that every flag the transaction tries to change was granted as mutable at issuance time. `sfMPTokenMetadata` and `sfTransferFee` have their own dedicated mutable-flag checks (`lsmfMPTCanMutateMetadata` and `lsmfMPTCanMutateTransferFee`). One subtle rule: setting a *non-zero* `sfTransferFee` requires `lsfMPTCanTransfer` to already be set on the issuance — setting `tmfMPTSetCanTransfer` in the same transaction does not satisfy this requirement. + +## Applying the Transaction + +`doApply` performs the actual ledger mutation. It peeks at either the issuance SLE or the holder's `MPToken` SLE depending on whether `sfHolder` was provided. Flag transitions for `lsfMPTLocked` are straightforward OR/AND-NOT operations. Mutable-flag changes iterate over the `mptMutabilityFlags` table, setting or clearing each `canMutateFlag` as directed. + +A notable invariant: when `tmfMPTClearCanTransfer` is applied, `doApply` explicitly calls `sle->makeFieldAbsent(sfTransferFee)` to keep the ledger internally consistent — you cannot have a non-zero transfer fee on an issuance that no longer permits transfers. Similarly, when `sfTransferFee` is set to zero, the field is removed rather than stored as zero, because the field uses `soeDEFAULT` semantics where absent means zero. The same absent/empty idiom applies to `sfMPTokenMetadata`: an empty blob removes the field entirely. For `sfDomainID`, the zero sentinel value (`beast::zero`) removes the domain link, while any other value sets it. + +After all mutations are applied, `view().update(sle)` commits the change. The method always returns `tesSUCCESS` — all failure modes are caught earlier in the pipeline, so `doApply` reaching a failure path is treated as an internal consistency error and returns `tecINTERNAL`. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/TrustSet.h.ai.json b/include/xrpl/tx/transactors/token/TrustSet.h.ai.json new file mode 100644 index 0000000000..41320cc228 --- /dev/null +++ b/include/xrpl/tx/transactors/token/TrustSet.h.ai.json @@ -0,0 +1,82 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "view" + }, + { + "lineno": 21, + "name": "tx" + }, + { + "lineno": 24, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "TrustSet" + } + ], + "description": "Defines the TrustSet transaction transactor class for handling trust line settings in the XRPL, including preflight, preclaim, and application logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/token/TrustSet.h", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 21, + "name": "checkPermission" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 24, + "name": "preclaim" + }, + { + "args": [], + "lineno": 27, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/token/TrustSet.h.ai.md b/include/xrpl/tx/transactors/token/TrustSet.h.ai.md new file mode 100644 index 0000000000..c1478d8b33 --- /dev/null +++ b/include/xrpl/tx/transactors/token/TrustSet.h.ai.md @@ -0,0 +1,59 @@ +# `TrustSet.h` — TrustSet Transaction Transactor + +## Role in the System + +`TrustSet` is the transactor that processes `TrustSet` transactions on the XRP Ledger, which are the mechanism by which accounts create, modify, or implicitly delete trust lines. A trust line is the bilateral credit relationship that allows two accounts to hold non-XRP balances denominated in a specific currency; without one, no IOU balance can exist between those parties. This header declares the class and its static dispatch surface; the full implementation lives in `TrustSet.cpp`. + +## Inheritance and the Static Dispatch Pattern + +`TrustSet` inherits from `Transactor`, which orchestrates the three-phase processing pipeline for all transaction types: `preflight` → `preclaim` → `doApply`. Importantly, `Transactor.h` documents that the static methods (`preflight`, `preclaim`, `getFlagsMask`, `checkPermission`) are **not virtual**. Instead, `Transactor::invokePreflight` is a function template that calls `T::preflight`, `T::getFlagsMask`, etc. directly — compile-time name hiding substitutes for runtime polymorphism. This eliminates vtable overhead on the hot path while still allowing each transactor to override default behavior. Derived classes must not attempt to call `preflight1` or `preflight2` directly; the base template handles that sequencing automatically. + +The `ConsequencesFactory` is set to `Normal`, meaning this transaction type neither blocks subsequent transactions from the same account nor requires custom consequence computation. + +## `getFlagsMask` — Narrowing Allowed Flags + +`getFlagsMask` returns `tfTrustSetMask`, which is the union of all TrustSet-specific flags: `tfSetfAuth`, `tfSetNoRipple`, `tfClearNoRipple`, `tfSetFreeze`, `tfClearFreeze`, `tfSetDeepFreeze`, and `tfClearDeepFreeze`. The base class `preflight1` uses this mask to reject transactions that set any flag not in the mask, catching client errors before any ledger state is touched. `tfSetDeepFreeze` and `tfClearDeepFreeze` are included in the mask at all times, but `preflight` itself returns `temINVALID_FLAG` if those bits are set without the `featureDeepFreeze` amendment being enabled — keeping the mask additive while still gate-checking new features at runtime. + +## `preflight` — Stateless Sanity Checks + +`preflight` has no ledger access; it operates only on the transaction object. It validates that `sfLimitAmount` is a non-native, non-negative IOU amount in a valid currency with a real issuer account ID. Returning here with a `tem` error code costs the sender nothing (the fee is not charged on `tem` results), so this is the right phase for cheap structural rejections. The deep-freeze flag guard is also here: the flags are parsed, and if `featureDeepFreeze` is not enabled, attempting to set or clear those bits yields `temINVALID_FLAG`. + +## `checkPermission` — Delegate Authority Validation + +`checkPermission` is called with a read-only `ReadView` and the transaction, allowing it to inspect the ledger without risking mutation. It exists as a separate method (rather than being folded into `preflight` or `preclaim`) because the base `Transactor::checkPermission` does nothing, and only transactors that support delegation need to override it. + +When a `sfDelegate` field is present on the transaction, the method locates the `Delegate` ledger object and first checks full transaction-type permission via `checkTxPermission`. If that passes, everything is allowed. If not, it falls through to check **granular permissions** — a finer-grained delegation model: + +- If any flag in `tfTrustSetPermissionMask` (everything except `tfSetfAuth`, `tfSetFreeze`, `tfClearFreeze`) is set, the delegate lacks permission. +- If `sfQualityIn` or `sfQualityOut` fields are present, permission is denied — delegates cannot adjust quality settings under granular grants. +- If the trust line does not yet exist, granular permission is insufficient to create a new one. +- `TrustlineAuthorize`, `TrustlineFreeze`, and `TrustlineUnfreeze` are the recognized granular types; each maps to exactly one of the allowed flags. +- Critically, if the transaction attempts to change the limit amount (comparing the submitted `sfLimitAmount` against the current stored limit), it is rejected — granular delegates cannot modify credit limits. + +## `preclaim` — Ledger-Aware Pre-Application Checks + +`preclaim` reads ledger state to make decisions that `preflight` cannot. Key checks in order: + +1. **Auth flag guard**: If `tfSetfAuth` is set but the issuing account does not have `lsfRequireAuth` enabled on its account root, the transaction fails with `tefNO_AUTH_REQUIRED`. Authorization grants only make sense in the context of a permissioned issuance model. + +2. **Self-trust rejection**: Setting a trust line to oneself is caught here (`temDST_IS_SRC`). + +3. **Destination existence**: When AMM or `SingleAssetVault` features are enabled, the destination account must exist (`tecNO_DST`). + +4. **`lsfDisallowIncomingTrustline`**: If the destination has opted out of incoming trust lines, the transaction is normally blocked. The `fixDisallowIncomingV1` amendment relaxes this: if the trust line already exists (the user has a pre-existing relationship), modification is still permitted regardless of the opt-out flag. This amendment corrects an overly restrictive original design. + +5. **Pseudo-account restrictions**: AMM accounts, vault accounts, and loan broker accounts are pseudo-accounts that ordinary accounts should not freely create trust lines to. For AMM accounts, trust line creation is only permitted if the currency matches the AMM's LP token and the pool has non-zero liquidity; an existing trust line can always be modified. Vault and loan broker pseudo-accounts permit modification of existing lines but block creation of new ones. Any other pseudo-account type produces `tecPSEUDO_ACCOUNT`. + +6. **Deep-freeze invariants**: When `featureDeepFreeze` is active, `preclaim` enforces several invariants: an account with `lsfNoFreeze` cannot set any freeze; simultaneously setting and clearing freeze flags is rejected; and most importantly, a trust line cannot be deep-frozen unless it is also normally frozen (the post-transaction state is simulated via `computeFreezeFlags` to check the constraint rather than relying on current state alone). + +## `doApply` — Ledger Mutation + +`doApply` is the only virtual (non-static) method and the only place that writes to the ledger view. + +**Reserve policy**: The XRPL normally requires an incremental reserve for each ledger object an account owns, but `doApply` exempts accounts with fewer than two owned objects from the reserve requirement for trust line creation. The comment explains the rationale: gateways routinely fund new user accounts and immediately set up trust lines; if the full reserve were enforced, the gateway would need to send enough XRP to cover both the base reserve and the trust line reserve, and the user could pocket the surplus rather than use the gateway. The two-item exemption means a gateway only needs to fund the base reserve. + +**Existing trust line path**: The trust line (internally a `RippleState` SLE) stores all attributes from both parties' perspectives. Account IDs are compared numerically to determine which party is the "low" and which is the "high" side, and all field accesses use the appropriate side (`sfLowLimit`/`sfHighLimit`, `sfLowQualityIn`/`sfHighQualityIn`, etc.). Quality values of exactly `QUALITY_ONE` are treated as zero (absent), maintaining a canonical representation. The owner reserve flags (`lsfLowReserve`/`lsfHighReserve`) are recomputed from scratch after every update by evaluating whether the account's side of the trust line is in a non-default state; `adjustOwnerCount` is called with ±1 whenever these flags transition, keeping owner counts consistent with ledger reality. + +**Auto-deletion**: If both sides of the trust line end up in fully default state (zero limit, zero quality, no special flags, no balance owed), `trustDelete` is called to remove the `RippleState` SLE and decrement both parties' owner counts. This is an important garbage-collection mechanism that keeps the ledger size in check. + +**New trust line path**: If no `RippleState` exists and the transaction carries non-default parameters, `trustCreate` initializes the SLE. If the account's XRP balance falls below the reserve threshold needed to accommodate the new object, the transaction fails with `tecNO_LINE_INSUF_RESERVE`; trying to set default values on a non-existent line is short-circuited as `tecNO_LINE_REDUNDANT`. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultClawback.h.ai.json b/include/xrpl/tx/transactors/vault/VaultClawback.h.ai.json new file mode 100644 index 0000000000..4519a23b91 --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultClawback.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 26, + "name": "vault" + }, + { + "lineno": 27, + "name": "sleShareIssuance" + }, + { + "lineno": 28, + "name": "holder" + }, + { + "lineno": 29, + "name": "clawbackAmount" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "VaultClawback" + } + ], + "description": "Defines the VaultClawback transaction class for the XRPL, including its interface and core logic for clawing back assets from a vault.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/vault/VaultClawback.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "VaultClawback" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 21, + "name": "doApply" + }, + { + "args": [ + "std::shared_ptr const& vault", + "std::shared_ptr const& sleShareIssuance", + "AccountID const& holder", + "STAmount const& clawbackAmount" + ], + "lineno": 25, + "name": "assetsToClawback" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultClawback.h.ai.md b/include/xrpl/tx/transactors/vault/VaultClawback.h.ai.md new file mode 100644 index 0000000000..98edf73fdf --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultClawback.h.ai.md @@ -0,0 +1,71 @@ +# `VaultClawback.h` — Vault Clawback Transactor + +## Role in the System + +`VaultClawback` implements the XRPL transaction that allows authorized parties to forcibly recover assets or shares held inside a `Vault` ledger object. Vaults are pooled asset containers backed by an MPT (Multi-Purpose Token) share issuance; depositors receive vault shares proportional to their contribution. `VaultClawback` is the enforcement mechanism that bridges two otherwise separate clawback regimes — IOU/MPT clawback by asset issuers, and vault-owner share cleanup — into a single vault-aware transaction type. + +The class inherits from `Transactor` and follows the standard three-phase processing pipeline mandatory for all XRPL transactions: `preflight`, `preclaim`, and `doApply`. It declares `ConsequencesFactory{Normal}`, meaning it is not treated as a fee-blocker transaction. + +--- + +## Two Operating Modes + +The transaction operates in one of two fundamentally different modes, determined at execution time by comparing the submitting account against vault roles: + +**1. Vault owner burning shares.** If the submitting account is the vault owner and the targeted amount refers to the share MPT, the vault owner can burn the holder's shares directly. This mode exists exclusively to recover from a stuck-vault scenario: shares remain outstanding but the vault holds zero assets (both `sfAssetsTotal` and `sfAssetsAvailable` are zero). This can happen if a lending protocol has absorbed all vault assets and suffered a total loss, leaving phantom shares with no underlying value. The owner may clawback all of a holder's shares in one operation, with no partial burns permitted (any non-zero amount must equal the holder's entire balance). + +**2. Asset issuer clawing back vault assets.** If the submitter is the issuer of the vault's underlying asset (IOU or MPT), they can recover those assets from the vault's pseudo-account and proportionally destroy the holder's shares. This integrates with XRPL's pre-existing clawback capability: for IOU-backed vaults, the issuer account must carry `lsfAllowTrustLineClawback` and must not have set `lsfNoFreeze`; for MPT-backed vaults, the MPT issuance must carry `lsfMPTCanClawback`. XRP vaults are explicitly excluded from clawback at every validation stage, since XRP has no issuer. + +--- + +## Validation Phases + +### `preflight` + +This phase runs before any ledger state is read, so checks are confined to transaction fields alone. It rejects a zero/empty `sfVaultID` immediately. If an explicit `sfAmount` is supplied, it must be non-negative and must not be XRP — since clawback of XRP is categorically forbidden. + +### `preclaim` + +This phase has read-only ledger access and resolves the ambiguity between modes. After loading the vault SLE and its share MPT issuance, `preclaim` resolves the effective clawback amount via the file-local helper `clawbackAmount()`, which converts an absent `sfAmount` field into either all-shares (if the submitter is the vault owner) or the vault asset (for the issuer case). + +A subtle edge case is handled explicitly: if the vault's asset issuer is the same account as the vault owner, the transaction is ambiguous — it cannot determine whether shares or assets should be targeted — so the submitter must provide an explicit `sfAmount`. This returns `tecWRONG_ASSET` rather than a more generic error, which is meaningful to clients. + +For share-burn mode, `preclaim` enforces that the vault has shares outstanding but no assets whatsoever, preventing a vault owner from covertly extracting value via this path. The holder's share balance must exactly match the requested amount (or amount must be zero, meaning "all"). + +For asset-clawback mode, the permission check diverges by asset type: IOU trust lines consult the issuer's account flags, while MPT issuances consult their own issuance flags. This mirrors XRPL's existing `Clawback` transaction logic but is applied to the vault's pooled balance rather than a direct holder balance. + +### `doApply` + +Execution operates on the mutable ledger view. In share-burn mode, `sharesDestroyed` is set to the holder's full share balance; no assets move. In asset-clawback mode, the private `assetsToClawback()` helper computes the precise (shares, assets) pair to settle. + +The sequence of mutations is: +1. Update vault `sfAssetsTotal` and `sfAssetsAvailable` downward. +2. Transfer `sharesDestroyed` shares from the holder to the vault's pseudo-account (waiving transfer fees). +3. Attempt to clean up the holder's now-empty MPToken entry via `removeEmptyHolding()`, tolerating `tecHAS_OBLIGATIONS` (meaning the token object has other uses). +4. Transfer `assetsRecovered` assets from the vault pseudo-account to the submitting issuer (waiving transfer fees), with a post-transfer sanity check that the vault's asset balance has not gone negative. + +A zero `sharesDestroyed` at step 2 triggers `tecPRECISION_LOSS`, protecting against rounding-to-zero edge cases where the math would produce a no-op. + +--- + +## `assetsToClawback` — Share/Asset Conversion with Safety Clamping + +This private method is the mathematical core of asset-clawback mode. It uses `VaultHelpers.h`'s `assetsToSharesWithdraw` and `sharesToAssetsWithdraw` conversion functions, which compute the proportional shares/assets given the vault's current exchange rate. + +The conversion is not simply the inverse of each other due to integer rounding (shares are MPTs and therefore integral). For this reason, `assetsToClawback` performs a double-pass when an explicit amount is provided: it first converts the requested asset amount to shares, then converts those shares back to assets to obtain the true recoverable amount. This round-trip accounts for integer truncation correctly. + +When the computed `assetsRecovered` would exceed `sfAssetsAvailable` (possible in yield-bearing vaults where assets are partially deployed), the method clamps to `assetsAvailable` and recomputes shares with `TruncateShares::yes` — deliberately under-counting shares so the subsequent re-conversion of truncated shares back to assets cannot overshoot the cap. A second overflow check confirms the invariant held. + +Arithmetic overflow from large amounts or unusual vault scales is caught via `std::overflow_error` and returned as `tecPATH_DRY`, a deliberate choice to log at `debug` rather than `error` since this is a normal user-reachable condition. + +--- + +## Amendment Compatibility: `fixSecurity3_1_3` + +A legacy code path in `assetsToClawback` is preserved for ledger replay. Before the `fixSecurity3_1_3` amendment, a zero-amount clawback (meaning "all") would convert all of the holder's shares to assets **without clamping to `sfAssetsAvailable`**, potentially allowing an issuer to recover more assets than the vault held liquid when an outstanding loan was in place. The fix adds the `assetsAvailable` clamp uniformly. The pre-fix branch is kept under an explicit rules check so that historical transaction replay produces the original (incorrect) ledger outcome. + +--- + +## Relationship to Sibling Transactors + +Among the vault transactors (`VaultCreate`, `VaultSet`, `VaultDelete`, `VaultDeposit`, `VaultWithdraw`), `VaultClawback` is structurally closest to `VaultWithdraw` — both convert shares to assets — but its authorization model is inverted: withdrawal is initiated by the holder themselves, while clawback is imposed on the holder by an external authority. This distinction explains why `VaultClawback` independently re-verifies asset-type clawback permissions that the issuer's own trust-line or MPT flags encode, rather than relying on any shared helper. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultCreate.h.ai.json b/include/xrpl/tx/transactors/vault/VaultCreate.h.ai.json new file mode 100644 index 0000000000..b1ef8cbf57 --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultCreate.h.ai.json @@ -0,0 +1,84 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + }, + { + "lineno": 24, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "VaultCreate" + } + ], + "description": "Defines the VaultCreate transaction transactor class for the XRPL, including its interface for preflight, preclaim, and application logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/vault/VaultCreate.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "VaultCreate" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 24, + "name": "preclaim" + }, + { + "args": [], + "lineno": 27, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultCreate.h.ai.md b/include/xrpl/tx/transactors/vault/VaultCreate.h.ai.md new file mode 100644 index 0000000000..05a1ba1d8c --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultCreate.h.ai.md @@ -0,0 +1,74 @@ +# `VaultCreate.h` — Vault Creation Transactor + +## Role in the System + +`VaultCreate` is the transactor responsible for instantiating a new on-ledger Vault object in the XRPL. A Vault is a pooled-asset construct that allows an account to hold a designated asset inside a pseudo-account, while issuing share tokens (backed by an `MPTokenIssuance`) to depositors. `VaultCreate.h` declares the class interface; all logic lives in `VaultCreate.cpp`. + +The file is one of six vault-lifecycle transactors (`VaultCreate`, `VaultSet`, `VaultDeposit`, `VaultWithdraw`, `VaultClawback`, `VaultDelete`), each of which inherits from `Transactor` and follows the same three-phase validation pattern used throughout the XRPL transaction engine. + +## Class Design + +`VaultCreate` publicly inherits from `Transactor` and contributes the standard four entry points the framework expects: + +```cpp +static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; +``` + +The `Normal` consequence type tells the engine this transaction charges a regular fee and does not unconditionally block the account's sequence progression the way `Blocker` transactions do. + +The constructor simply forwards its `ApplyContext` reference to `Transactor`, which stores it as `ctx_`. No additional state is needed at construction time because all inputs are read from `ctx_.tx` during the three phases. + +## Validation Phases + +### `checkExtraFeatures` + +Unlike most transactors, `VaultCreate` overrides `checkExtraFeatures` (the base returns `true` by default). Here it gates the entire transaction on two amendments: the `MPTokensV1` feature must be enabled (vaults rely on MPT share issuance), and if the transaction carries a `sfDomainID` field, the `PermissionedDomains` feature must also be active. This design keeps amendment checks strictly out of `preflight` — the framework calls `checkExtraFeatures` before `preflight1`, so a disabled amendment yields `temDISABLED` before any field parsing occurs. + +### `getFlagsMask` + +Returns `tfVaultCreateMask`, which restricts the valid flag bits to those meaningful for vault creation (`tfVaultPrivate`, `tfVaultShareNonTransferable`). The base class framework passes this mask to `preflight0/preflight1`, which reject any unrecognised flag bits as `temINVALID_FLAG`. + +### `preflight` + +Performs stateless, ledger-free field validation: + +- `sfData` payload length must not exceed `maxDataPayloadLength`. +- `sfWithdrawalPolicy`, if present, must equal `vaultStrategyFirstComeFirstServe` — the only currently supported strategy. +- `sfDomainID`, if present, must be non-zero and the `tfVaultPrivate` flag must be set (domain-restricted access only applies to private vaults). +- `sfAssetsMaximum` must not be negative. +- `sfMPTokenMetadata` must be non-empty and within `maxMPTokenMetadataLength`. +- `sfScale` is only valid for IOU assets — it is rejected for MPT or native XRP assets, and must not exceed `vaultMaximumIOUScale`. + +### `preclaim` + +Performs read-only ledger checks after signature verification: + +- Calls `canAddHolding` to verify the asset can be held (e.g., asset exists, is not in a broken state). +- Rejects pseudo-account issuers (e.g., other vault share MPTs or AMM LP tokens) via `isPseudoAccount`. The comment explains the rationale: such assets would be impossible to claw back if ever needed. +- Checks that the asset is not frozen for the vault owner. +- If `sfDomainID` is present, confirms the referenced `PermissionedDomain` object exists on the ledger. +- Verifies that deriving a pseudo-account address from the vault's keylet does not produce a collision (`terADDRESS_COLLISION`). + +## `doApply` — Ledger State Mutation + +`doApply` performs the actual state changes inside a single atomic ledger view: + +1. **Vault SLE creation**: A new `SLE` is built at `keylet::vault(account_, sequence)` and linked into the owner's directory via `dirLink`. The owner count is incremented by 2 (one for the vault object, one for the pseudo-account) before the reserve check — this is intentional: the reserve must be checked against the *post-creation* count to ensure the owner can afford both objects. + +2. **Pseudo-account**: `createPseudoAccount` creates a synthetic account entry keyed from the vault's object ID. This pseudo-account holds the actual pooled asset on behalf of all depositors, keeping vault funds segregated from the owner's personal balance. + +3. **Asset holding**: `addEmptyHolding` creates either an `MPToken` or a `TrustLine`/`RippleState` entry on the pseudo-account for the asset being vaulted — ready to receive deposits but initially empty. + +4. **Share MPT issuance**: `MPTokenIssuanceCreate::create` is called on the pseudo-account (not the owner) at sequence 1, creating the share token that depositors receive in exchange for depositing the underlying asset. The `tfVaultShareNonTransferable` flag maps to clearing the `lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer` bits; `tfVaultPrivate` maps to setting `lsfMPTRequireAuth`. + +5. **Vault SLE population**: All fields — `sfAsset`, `sfFlags`, `sfSequence`, `sfOwner`, `sfAccount` (pseudo-account ID), `sfAssetsTotal`, `sfAssetsAvailable`, `sfLossUnrealized`, optional `sfAssetsMaximum`, `sfShareMPTID`, `sfData`, `sfWithdrawalPolicy`, and `sfScale` — are written to the vault SLE before `view().insert` makes it permanent. + +6. **Owner MPToken**: The vault creator is explicitly authorized for the share MPT via `authorizeMPToken`. For private vaults, the pseudo-account itself is also authorized (so it can receive its own share tokens internally), with the owner as the authorizing account. + +## Notable Design Decisions + +**Two-object owner reserve**: Incrementing the owner count by 2 before checking the reserve is a deliberate ordering choice. By checking the reserve after the increment, the code ensures the account genuinely has enough XRP to cover both the vault and pseudo-account entries simultaneously, preventing a scenario where creation succeeds only to leave the account underfunded. + +**Pseudo-account isolation**: Routing all pooled funds through a pseudo-account (rather than a sub-balance on the owner's account) keeps vault assets cleanly separable from the owner's personal holdings and makes clawback semantics unambiguous. The explicit rejection of pseudo-account issuers in `preclaim` closes a circular-dependency risk where vault shares from one vault could be deposited into another. + +**Compile-time polymorphism for static methods**: `checkExtraFeatures`, `getFlagsMask`, and `preflight` are all `static`. The base class comment in `Transactor.h` notes these methods use name hiding — not virtual dispatch — called through the `invokePreflight` template to achieve polymorphism without vtable overhead. `doApply` is the sole `virtual` method, resolved at runtime. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultDelete.h.ai.json b/include/xrpl/tx/transactors/vault/VaultDelete.h.ai.json new file mode 100644 index 0000000000..1659ad6336 --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultDelete.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "VaultDelete" + } + ], + "description": "Defines the VaultDelete transaction transactor class for the XRPL, including its interface for preflight, preclaim, and apply logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/vault/VaultDelete.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "VaultDelete" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 21, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultDelete.h.ai.md b/include/xrpl/tx/transactors/vault/VaultDelete.h.ai.md new file mode 100644 index 0000000000..aba0dc78d7 --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultDelete.h.ai.md @@ -0,0 +1,41 @@ +# `VaultDelete.h` — Vault Deletion Transactor + +## Role in the System + +`VaultDelete` is the transactor responsible for removing an XRPL Single-Sided AMM vault and all of its associated ledger objects. It sits alongside `VaultCreate`, `VaultSet`, `VaultDeposit`, `VaultWithdraw`, and `VaultClawback` in the vault sub-module of XRPL transaction processing, and it is the only one that performs a full multi-object teardown of the vault lifecycle. + +The file itself is a minimal header: it declares the class, wires up the constructor, and exposes the three static/virtual hooks that the `Transactor` framework calls during transaction processing. The substantive logic lives in `VaultDelete.cpp`. + +## Class Design + +`VaultDelete` inherits from `Transactor` and follows the standard three-phase dispatch pattern enforced by the base class template `invokePreflight`: + +1. **`preflight(PreflightContext const&)`** — a stateless, read-only sanity check that runs before ledger state is consulted. It confirms that `sfVaultID` is non-zero; a zero vault ID is an immediate `temMALFORMED` rejection. + +2. **`preclaim(PreclaimContext const&)`** — a read-only check against the current ledger view. This phase enforces the three-invariant guard for safe deletion: the submitting account must be the vault `sfOwner`; `sfAssetsAvailable` must be zero; `sfAssetsTotal` must be zero; and the MPTokenIssuance backing the vault shares must have `sfOutstandingAmount` equal to zero. Any nonzero asset or share count returns `tecHAS_OBLIGATIONS`, preventing destruction of a vault that still holds depositor funds. + +3. **`doApply()`** — the state-mutating phase. This is where the real complexity lives. + +`ConsequencesFactory` is set to `Normal`, matching `VaultCreate` and the other vault transactors. Unlike `VaultCreate`, `VaultDelete` does not override `checkExtraFeatures` or `getFlagsMask`, relying on the base-class defaults — meaning no additional amendment check is needed beyond what `invokePreflight` performs via `Permission::getInstance().getTxFeature`. + +## The `doApply()` Teardown Sequence + +Destroying a vault requires cleaning up five distinct ledger objects in a specific order that maintains invariants at each step: + +1. **Asset holding** — the vault's pseudo-account holds a zero-balance token or XRP holding representing the underlying asset. `removeEmptyHolding()` is called to remove it from the pseudo-account's directory. + +2. **Share MPTokenIssuance** — the vault's vault shares are tracked as an MPT issuance. The code explicitly avoids the `MPTokenIssuanceDestroy` transactor path ("no special logic needed") and instead directly removes the issuance's directory entry, adjusts the pseudo-account's owner count, and erases the SLE. + +3. **Vault owner's MPToken for shares** — if the vault owner holds an `MPToken` for the vault's share issuance (e.g., received shares they didn't redeem), `removeEmptyHolding()` clears that token too before the issuance is erased. + +4. **Pseudo-account** — each vault has an associated pseudo-account (an `AccountRoot` SLE bearing an `sfVaultID` back-reference). After the above steps, this account must have zero balance, zero owner count, and no remaining directory. Three defensive `tecHAS_OBLIGATIONS` guards confirm this before the SLE is erased. These guards are annotated `// LCOV_EXCL_START`, marking them as theoretically unreachable — they protect against bugs in the cleanup steps rather than legitimate user-triggered states. + +5. **Vault SLE + owner directory** — the vault object is removed from the owner's `ownerDir`, owner count is decremented by **2** (one for the vault object, one for the pseudo-account that was also tracked against the owner's reserve), and the vault SLE itself is erased. + +## Error Handling Philosophy + +The distinction between `tec` and `tef` errors in `doApply()` is deliberate. Conditions that a caller might legitimately trigger — vault not found, non-owner submitter, outstanding assets — return `tec` codes (claimable fee errors). Conditions that should be impossible given correct ledger state — missing pseudo-account, mismatched issuance owner, non-zero pseudo-account balance after cleanup — return `tefBAD_LEDGER` or `tefINTERNAL`, which signal ledger corruption and prevent the fee from being claimed. The `LCOV_EXCL_START` markers on these paths make explicit the authors' expectation that test coverage cannot reach them through valid transaction sequences. + +## Relationship to Other Vault Transactors + +`VaultDelete` is the inverse of `VaultCreate`. Where `VaultCreate` allocates the vault SLE, pseudo-account, and share MPTokenIssuance and charges owner reserves, `VaultDelete` reclaims all of those objects and releases two owner-count units. The symmetry is enforced by `preclaim`'s `tecHAS_OBLIGATIONS` guards, which ensure deletion is only possible once `VaultWithdraw` has reduced all balances to zero — making `VaultDelete` the terminal state of the vault lifecycle. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultDeposit.h.ai.json b/include/xrpl/tx/transactors/vault/VaultDeposit.h.ai.json new file mode 100644 index 0000000000..021192fad0 --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultDeposit.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "VaultDeposit" + } + ], + "description": "Defines the VaultDeposit transaction class for XRPL, including its constructor and transaction lifecycle methods (preflight, preclaim, doApply) within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/vault/VaultDeposit.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 11, + "name": "VaultDeposit" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "preclaim" + }, + { + "args": [], + "lineno": 20, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultDeposit.h.ai.md b/include/xrpl/tx/transactors/vault/VaultDeposit.h.ai.md new file mode 100644 index 0000000000..a097072b8a --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultDeposit.h.ai.md @@ -0,0 +1,35 @@ +# `VaultDeposit.h` — Vault Deposit Transactor Declaration + +## Role in the System + +`VaultDeposit.h` declares the `VaultDeposit` transactor, which handles the `VaultDeposit` transaction type on the XRP Ledger. This transaction allows an account to deposit assets into a vault ledger object and receive newly minted vault-share MPTokens in return — the essential liquidity-provision operation that makes vaults function as on-ledger asset pools or yield-bearing containers. + +The header itself is intentionally minimal: a single class declaration with three static/virtual method stubs and a constructor. All logic lives in `VaultDeposit.cpp`. + +## Transactor Lifecycle Pattern + +`VaultDeposit` inherits from `Transactor`, the XRPL base class for all transaction processors. The framework dispatches every transaction through three ordered phases, each called with a progressively more expensive context: + +- **`preflight(PreflightContext const&)`** — stateless, no ledger access. This is where format-level validation happens before any state is examined. For `VaultDeposit`, it rejects a zero `sfVaultID` and any non-positive `sfAmount`. Because it runs without the ledger, it can be safely parallelised across transactions in a batch. + +- **`preclaim(PreclaimContext const&)`** — read-only ledger access. This phase performs every check that requires inspecting current ledger state but does not need write access. The implementation validates that the vault exists, that the deposited asset matches the vault's configured asset type (`sfAsset`), that the vault's MPT issuance for shares is present and unlocked, that neither the asset nor the shares are frozen or locked for the depositor, and — for private vaults — that the depositor is either the vault owner or holds valid domain credentials. It also verifies the depositor holds sufficient spendable balance. + +- **`doApply()`** — state-mutating, called on the writable `ApplyView`. This phase commits the exchange: it creates or authorizes an MPToken account for the depositor to hold shares, calculates the share amount using `assetsToSharesDeposit()`, back-verifies the implied asset cost via `sharesToAssetsDeposit()`, updates the vault's asset totals, enforces the vault's maximum asset cap, and issues two atomic `accountSend` calls — assets from depositor to vault pseudo-account, and shares from vault pseudo-account to depositor. + +## Key Design Decisions + +**`ConsequencesFactory{Normal}`** marks this transaction as non-blocking. In the transaction queue, a `Normal` transaction from an account does not prevent later transactions from the same account from being queued or applied. This is the correct classification because a failed deposit does not invalidate subsequent operations. + +**No `checkExtraFeatures` or `getFlagsMask` override.** The base class defaults are used — the vault amendment check is handled centrally via `Permission::getInstance().getTxFeature()` in `Transactor::invokePreflight`, and standard flags apply. `VaultCreate.h` overrides both of these, but `VaultDeposit` has no additional amendment gating or custom flag bits beyond the universal set. + +**Two-step exchange calculation in `doApply`.** Rather than directly using the asset amount the depositor offered, `doApply` first converts assets to shares (truncated, because MPT shares are integral), then converts those shares back to the exact asset cost. This reverse-check guarantees the invariant that the vault never extracts more than the depositor offered (`assetsDeposited <= amount`). Any sub-share-unit remainder of the offered amount is effectively returned by not being taken. If shares round to zero, `tecPRECISION_LOSS` is returned, preventing dust deposits that would dilute share accounting. + +**Overflow maps to `tecPATH_DRY`.** The `assetsToSharesDeposit` and `sharesToAssetsDeposit` helpers can throw `std::overflow_error` when the vault's scale factor combined with large balances exceeds numeric limits. The implementation catches this and returns `tecPATH_DRY` — a semantically adjacent "exchange failed" code — while logging at `debug` level to avoid log spam from adversarial inputs. + +**Private vault authorization uses domain credentials, not MPT issuer authorization.** The vault's shares are issued by a pseudo-account derived from the vault itself, not a human-controlled account. This pseudo-account cannot proactively authorize holders via normal MPT mechanics. Instead, private vault access is governed by a `DomainID` on the MPT issuance, and `credentials::validDomain` is called in `preclaim` to check credential membership. The `tecEXPIRED` error is suppressed in `preclaim` (allowing the transaction to proceed) so that `doApply` can delete the expired credentials as a side effect. + +**Transfer fees waived on both legs.** Both the asset transfer (depositor → vault) and the share transfer (vault → depositor) are issued with `WaiveTransferFee::Yes`. This is intentional: the vault itself is the economic actor managing exchange, not an intermediary collecting fees. Allowing transfer fees on vault operations would break the exchange rate accounting and create systemic inaccuracies in `sfAssetsTotal`. + +## Relationship to Sibling Files + +The vault directory contains six transactors — `VaultCreate`, `VaultSet`, `VaultDelete`, `VaultDeposit`, `VaultWithdraw`, and `VaultClawback` — all following this same three-phase structure. `VaultDeposit` and `VaultWithdraw` are the mirror pair that manage liquidity: deposit mints shares and increases `sfAssetsTotal`/`sfAssetsAvailable`; withdraw burns shares and decreases them. The exchange-rate math in both directions is factored into `VaultHelpers.h`, keeping the conversion logic auditable in one place. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultSet.h.ai.json b/include/xrpl/tx/transactors/vault/VaultSet.h.ai.json new file mode 100644 index 0000000000..40a124049d --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultSet.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "VaultSet" + } + ], + "description": "Defines the VaultSet transaction transactor class for the XRPL, including its interface and key static and instance methods for transaction processing.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/vault/VaultSet.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "VaultSet" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "preclaim" + }, + { + "args": [], + "lineno": 24, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultSet.h.ai.md b/include/xrpl/tx/transactors/vault/VaultSet.h.ai.md new file mode 100644 index 0000000000..097cbd2685 --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultSet.h.ai.md @@ -0,0 +1,41 @@ +# `VaultSet.h` — Vault Configuration Transactor + +`VaultSet` is the transactor that allows a vault owner to modify the mutable properties of an existing XRPL Single-Sided AMM vault after it has been created. It lives alongside `VaultCreate`, `VaultDelete`, `VaultDeposit`, `VaultWithdraw`, and `VaultClawback` in the vault transactor family, all of which share the same `Transactor` base pipeline. + +## Role in the Vault Lifecycle + +While `VaultCreate` establishes a vault and fixes its immutable identity (the underlying asset, the share MPT issuance, private/public status), `VaultSet` governs the three fields that remain writable after creation: `sfData` (an arbitrary byte payload for off-chain metadata), `sfAssetsMaximum` (a deposit cap), and `sfDomainID` (a permissioned-domain gate for private vaults). Any other mutation requires destroying and recreating the vault. + +## Pipeline Phases + +`VaultSet` follows the standard XRPL three-phase validation pipeline inherited from `Transactor`. The phases use compile-time name hiding rather than virtual dispatch — `invokePreflight` calls `VaultSet::checkExtraFeatures`, `VaultSet::preflight`, and the base `preflight1`/`preflight2` in a fixed sequence. + +**`checkExtraFeatures`** provides a lightweight amendment gate. It returns `false` (causing `temDISABLED`) if the transaction carries an `sfDomainID` field but the `featurePermissionedDomains` amendment is not yet active on the network. This cleanly separates the domain feature's availability from the broader vault feature flag, without any per-field branching in `preflight`. + +**`preflight`** runs entirely against the transaction object with no ledger access. Three structural invariants are enforced here: + +1. `sfVaultID` must be non-zero — a zero vault ID cannot identify any real ledger object. +2. `sfData`, if present, must be non-empty and within `maxDataPayloadLength` bytes. Both bounds matter: an empty blob is rejected to prevent storing useless entries, and the upper bound caps chain bloat. +3. `sfAssetsMaximum`, if present, must be non-negative. + +A critical no-op guard rejects the transaction as `temMALFORMED` when none of the three mutable fields are present. This prevents fee-burning transactions that would accomplish nothing — a pattern that appears across several XRPL transactors. + +Unlike `VaultCreate`, `VaultSet` does not override `getFlagsMask`, so it inherits the base implementation returning `tfUniversalMask`. This signals that the transaction carries no transaction-type-specific flags. + +**`preclaim`** performs ledger-state checks with read-only access to the current view: + +- The vault ledger object must exist for the given `sfVaultID`; absent vaults return `tecNO_ENTRY`. +- The submitting account must match the vault's `sfOwner`; unauthorized updates return `tecNO_PERMISSION`. Only the original creator can alter the vault's configuration. +- If `sfDomainID` is being set, the vault must have been created with `lsfVaultPrivate`. This enforces a one-way door: a vault created as public can never be retroactively restricted to a permissioned domain. The reverse (lifting domain restriction from a private vault) is permitted — by sending a zero `sfDomainID`. +- A non-zero `sfDomainID` must resolve to an existing permissioned domain object (`tecOBJECT_NOT_FOUND` if not). +- The vault's associated `MPTokenIssuance` object must still exist. This path is guarded with `LCOV_EXCL_*` markers because it represents a defensive check against a state that `VaultCreate` and the invariant checker should prevent from ever occurring. + +**`doApply`** performs the actual ledger mutations. `sfData` and `sfAssetsMaximum` are updated directly on the vault SLE. A subtle constraint on `sfAssetsMaximum` prevents the cap from being lowered below the vault's current `sfAssetsTotal` — you cannot cap a vault below what it already holds (`tecLIMIT_EXCEEDED`). For `sfDomainID`, the field is written to or removed from the `MPTokenIssuance` SLE (not the vault SLE itself), because domain enforcement for MPT-based vaults lives at the issuance level. Sending zero clears the domain restriction by calling `makeFieldAbsent`. + +A deliberate design note in the implementation: the vault SLE is always marked dirty via `view().update(vault)` even when the only change was to the issuance SLE. The comment explains this is required so the vault invariant checker can observe the operation — without an update to the vault object, the invariant verifier has no signal that a `VaultSet` occurred in this ledger. + +## Relationship to `VaultCreate` + +The `ConsequencesFactory{Normal}` constant is identical to `VaultCreate`'s — both transactions produce normal consequences, meaning they do not block other transactions in a batch from the same account. This differs from transactors like account-level configuration changes that might use `Blocker`. + +The structure of `VaultSet.h` is intentionally minimal: it declares only the four pipeline hooks that differ from the base class defaults. The absence of `getFlagsMask` is meaningful — `VaultCreate` overrides it to expose vault-private creation flags, while `VaultSet` has no creation-time flags to expose. \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultWithdraw.h.ai.json b/include/xrpl/tx/transactors/vault/VaultWithdraw.h.ai.json new file mode 100644 index 0000000000..2338762704 --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultWithdraw.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 14, + "name": "ctx" + }, + { + "lineno": 17, + "name": "ctx" + } + ], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 7, + "name": "VaultWithdraw" + } + ], + "description": "Defines the VaultWithdraw transaction transactor class for XRPL, including its interface for preflight, preclaim, and apply logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/tx/transactors/vault/VaultWithdraw.h", + "functions": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 10, + "name": "VaultWithdraw" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 14, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 17, + "name": "preclaim" + }, + { + "args": [], + "lineno": 19, + "name": "doApply" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/include/xrpl/tx/transactors/vault/VaultWithdraw.h.ai.md b/include/xrpl/tx/transactors/vault/VaultWithdraw.h.ai.md new file mode 100644 index 0000000000..581fe1b379 --- /dev/null +++ b/include/xrpl/tx/transactors/vault/VaultWithdraw.h.ai.md @@ -0,0 +1,47 @@ +# `VaultWithdraw.h` — Vault Withdrawal Transactor Declaration + +`VaultWithdraw` is the transaction transactor that handles the `VaultWithdraw` ledger transaction on the XRP Ledger. It sits inside the vault subsystem alongside `VaultCreate`, `VaultDeposit`, `VaultSet`, `VaultDelete`, and `VaultClawback`, collectively implementing the on-ledger single-sided vault (yield-bearing pool) feature. + +## Role and Context + +A vault in XRPL is a pseudo-account that holds a pool of assets and issues fungible MPT-based shares to depositors in proportion to their contribution. `VaultWithdraw` is the inverse of `VaultDeposit`: it accepts either a fixed asset amount or a fixed share amount from the submitter, computes the complementary quantity via the vault's share-to-asset exchange rate, burns the shares, and credits the underlying assets to a destination account. + +The header itself is deliberately thin — it declares the class and its three pipeline methods. All logic lives in `VaultWithdraw.cpp`. + +## Transactor Pipeline + +Like every XRPL transactor, `VaultWithdraw` is processed through a mandatory three-phase pipeline orchestrated by `Transactor::invokePreflight` and the broader apply machinery: + +**`preflight(PreflightContext const& ctx)`** runs before any ledger state is consulted. It validates the raw transaction fields: a zero `sfVaultID` is rejected as `temMALFORMED`, a non-positive `sfAmount` returns `temBAD_AMOUNT`, and a zero explicit `sfDestination` is also malformed. This phase is intentionally cheap — no disk reads, no ledger lookups. + +**`preclaim(PreclaimContext const& ctx)`** is a read-only pass against an immutable `ReadView`. It resolves the vault object and checks: +- The vault exists (`tecNO_ENTRY` otherwise). +- The requested `sfAmount` is denominated in either the vault's underlying asset or its share MPT — any other asset fails with `tecWRONG_ASSET`. +- The underlying asset is transferable to the destination (`canTransfer` check). +- The vault's withdrawal policy is `vaultStrategyFirstComeFirstServe` (the only supported policy; any other triggers `tefINTERNAL` as an invariant violation). +- When the `fixSecurity3_1_3` amendment is active and the amount is share-denominated, the shares are first converted to an equivalent asset amount before running the withdrawal limit check via `canWithdraw`. This plugged a pre-amendment gap where share-denominated withdrawals bypassed limit enforcement entirely. Overflow errors during this conversion are caught and returned as `tecPATH_DRY`. +- The destination account's authorization: if withdrawing to self (`sfAccount == sfDestination`), `WeakAuth` suffices (the `doApply` phase will create any missing trust line or MPToken); if withdrawing to a third party, `StrongAuth` is required and the receiving account's trust line or MPToken must already exist. +- The vault's underlying asset must not be frozen for the destination account, and the vault's share MPT must not be frozen for the submitting account. + +**`doApply()`** is the state-mutation phase, called only when both prior phases return success. It: +1. Resolves the vault and share issuance SLEs with write access. +2. Branches on whether `sfAmount` is asset-denominated or share-denominated, calling `assetsToSharesWithdraw` or `sharesToAssetsWithdraw` from `VaultHelpers` to compute the complementary quantity. Both conversion paths then call `sharesToAssetsWithdraw` to establish the canonical `assetsWithdrawn` amount, which may differ slightly from the requested amount due to fixed-point truncation. +3. Returns `tecPRECISION_LOSS` if an asset-denominated request would resolve to zero shares — this prevents economically meaningless dust withdrawals. +4. Verifies the submitter holds at least `sharesRedeemed` shares via `accountHolds`, returning `tecINSUFFICIENT_FUNDS` if not. +5. Checks `sfAssetsAvailable` (not the raw pseudo-account balance) against `assetsWithdrawn`. Vaults can pledge assets to external lending positions, making some held assets unavailable; using `sfAssetsAvailable` correctly reflects only the liquid portion. +6. Decrements both `sfAssetsTotal` and `sfAssetsAvailable` on the vault by `assetsWithdrawn`. +7. Calls `accountSend` to transfer the shares from the submitter back to the vault's pseudo-account with `WaiveTransferFee::Yes` — shares are internal bookkeeping tokens, not economic transfers. +8. Attempts to remove the submitter's now-empty MPToken holding for the share via `removeEmptyHolding`. A result of `tecHAS_OBLIGATIONS` (balance non-zero) is silently tolerated; other errors are returned. This housekeeping is skipped when the submitter is the vault owner. +9. Finally calls `doWithdraw` from `View.h` to execute the asset credit from the vault pseudo-account to the destination. + +## Notable Design Decisions + +**Private vault bypass at withdrawal time.** `doApply` deliberately ignores the `lsfVaultPrivate` flag. The reasoning, stated in a code comment, is that possession of vault shares is itself proof of prior authorized deposit. Forcing a second authorization gate at withdrawal time would be user-hostile and is not part of the protocol semantics. + +**Dual-denomination flexibility.** Allowing users to specify either assets or shares in `sfAmount` is a deliberate UX choice. Asset-denomination means "I want exactly X of the underlying back"; share-denomination means "I want to redeem exactly N shares". Both are valid, and the rounding direction differs: asset amounts drive rounding in `assetsToSharesWithdraw`, while share redemptions are exact and only the resulting asset amount varies. + +**`sfAssetsAvailable` vs balance.** Using the vault's tracked `sfAssetsAvailable` field rather than the underlying pseudo-account balance is essential for correctness when vaults participate in lending — pledged assets appear in the balance but must not be counted as withdrawable. + +**Overflow handling.** The conversion math using `Number`-based arithmetic can overflow for extreme scale and total values. Both `preflight` and `doApply` explicitly catch `std::overflow_error` and map it to `tecPATH_DRY`, avoiding log spam by downgrading to `debug` severity. + +**`ConsequencesFactory{Normal}`** signals to the transaction consequence machinery that this transaction does not block other transactions from the same account — it is non-escalating and can be processed concurrently with other normal transactions in the queue. \ No newline at end of file diff --git a/src/libxrpl/basics/Archive.cpp.ai.json b/src/libxrpl/basics/Archive.cpp.ai.json new file mode 100644 index 0000000000..cf226e02c5 --- /dev/null +++ b/src/libxrpl/basics/Archive.cpp.ai.json @@ -0,0 +1,653 @@ +{ + "args": [ + { + "lineno": 11, + "name": "src" + }, + { + "lineno": 11, + "name": "dst" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "extractTarLz4" + ], + "entry_point": "extractTarLz4", + "purpose": "Extracts a .tar.lz4 archive from src to dst, validating inputs and archive operations at each step.", + "validation_points": [ + "if (!is_regular_file(src))", + "if (!ar)", + "if (archive_read_support_format_tar(ar.get()) < ARCHIVE_OK)", + "if (archive_read_support_filter_lz4(ar.get()) < ARCHIVE_OK)", + "if (archive_read_open_filename(ar.get(), src.string().c_str(), 10240) < ARCHIVE_OK)", + "if (!aw)", + "if (archive_write_disk_set_options(...) < ARCHIVE_OK)", + "if (archive_write_disk_set_standard_lookup(aw.get()) < ARCHIVE_OK)", + "if (result < ARCHIVE_OK) in archive_read_next_header", + "if (archive_write_header(aw.get(), entry) < ARCHIVE_OK)", + "if (result < ARCHIVE_OK) in archive_read_data_block", + "if (archive_write_data_block(aw.get(), buf, sz, offset) < ARCHIVE_OK)", + "if (archive_write_finish_entry(aw.get()) < ARCHIVE_OK)" + ] + } + ], + "data_flows": [ + { + "field": "src (source archive path)", + "flow": [ + "extractTarLz4 argument", + "is_regular_file(src) validation", + "src.string().c_str() passed to archive_read_open_filename" + ], + "origin": "Function argument to extractTarLz4", + "transformations": [ + "Checked for regular file", + "Converted to string for libarchive" + ], + "validated_at": "is_regular_file(src)" + }, + { + "field": "ar (archive read pointer)", + "flow": [ + "archive_read_new()", + "archive_read_support_format_tar(ar.get())", + "archive_read_support_filter_lz4(ar.get())", + "archive_read_open_filename(ar.get(), ...)", + "archive_read_next_header(ar.get(), ...)", + "archive_read_data_block(ar.get(), ...)" + ], + "origin": "archive_read_new()", + "transformations": [ + "Pointer allocation", + "Format and filter support enabled", + "Opened with filename", + "Used for reading headers and data blocks" + ], + "validated_at": [ + "if (!ar)", + "if (archive_read_support_format_tar(ar.get()) < ARCHIVE_OK)", + "if (archive_read_support_filter_lz4(ar.get()) < ARCHIVE_OK)", + "if (archive_read_open_filename(ar.get(), ...) < ARCHIVE_OK)", + "if (result < ARCHIVE_OK) in archive_read_next_header", + "if (result < ARCHIVE_OK) in archive_read_data_block" + ] + }, + { + "field": "aw (archive write pointer)", + "flow": [ + "archive_write_disk_new()", + "archive_write_disk_set_options(aw.get(), ...)", + "archive_write_disk_set_standard_lookup(aw.get())", + "archive_write_header(aw.get(), entry)", + "archive_write_data_block(aw.get(), ...)", + "archive_write_finish_entry(aw.get())" + ], + "origin": "archive_write_disk_new()", + "transformations": [ + "Pointer allocation", + "Options set for extraction", + "Standard lookup set", + "Used for writing headers and data blocks" + ], + "validated_at": [ + "if (!aw)", + "if (archive_write_disk_set_options(...) < ARCHIVE_OK)", + "if (archive_write_disk_set_standard_lookup(aw.get()) < ARCHIVE_OK)", + "if (archive_write_header(aw.get(), entry) < ARCHIVE_OK)", + "if (archive_write_data_block(aw.get(), buf, sz, offset) < ARCHIVE_OK)", + "if (archive_write_finish_entry(aw.get()) < ARCHIVE_OK)" + ] + }, + { + "field": "archive_entry (per file in archive)", + "flow": [ + "archive_read_next_header(ar.get(), &entry)", + "archive_entry_set_pathname(entry, ...)", + "archive_write_header(aw.get(), entry)", + "archive_entry_size(entry)", + "archive_read_data_block(ar.get(), ...)", + "archive_write_data_block(aw.get(), ...)", + "archive_write_finish_entry(aw.get())" + ], + "origin": "archive_read_next_header(ar.get(), &entry)", + "transformations": [ + "Pathname rewritten to be under dst", + "Header and data blocks written to disk" + ], + "validated_at": [ + "if (result < ARCHIVE_OK) in archive_read_next_header", + "if (archive_write_header(aw.get(), entry) < ARCHIVE_OK)", + "if (archive_write_data_block(aw.get(), buf, sz, offset) < ARCHIVE_OK)", + "if (archive_write_finish_entry(aw.get()) < ARCHIVE_OK)" + ] + } + ], + "description": "Provides a function to extract files from a .tar.lz4 archive to a destination directory using libarchive and Boost filesystem, with error handling.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "src (source file path)", + "empty", + "string", + "validation" + ], + "evidence": "is_regular_file() at extractTarLz4", + "issue_pattern": "Missing empty string validation for src (source file path)", + "why_false_positive": "is_regular_file() validates src (source file path) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ar (archive pointer allocation)", + "empty", + "string", + "validation" + ], + "evidence": "archive_read_new() at extractTarLz4", + "issue_pattern": "Missing empty string validation for ar (archive pointer allocation)", + "why_false_positive": "archive_read_new() validates ar (archive pointer allocation) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ar (archive pointer allocation)", + "type", + "validation", + "check" + ], + "evidence": "archive_read_new() at extractTarLz4", + "issue_pattern": "Missing type validation for ar (archive pointer allocation)", + "why_false_positive": "archive_read_new() validates ar (archive pointer allocation) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ar (tar format support)", + "empty", + "string", + "validation" + ], + "evidence": "archive_read_support_format_tar() at extractTarLz4", + "issue_pattern": "Missing empty string validation for ar (tar format support)", + "why_false_positive": "archive_read_support_format_tar() validates ar (tar format support) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ar (lz4 filter support)", + "empty", + "string", + "validation" + ], + "evidence": "archive_read_support_filter_lz4() at extractTarLz4", + "issue_pattern": "Missing empty string validation for ar (lz4 filter support)", + "why_false_positive": "archive_read_support_filter_lz4() validates ar (lz4 filter support) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ar (archive open)", + "empty", + "string", + "validation" + ], + "evidence": "archive_read_open_filename() at extractTarLz4", + "issue_pattern": "Missing empty string validation for ar (archive open)", + "why_false_positive": "archive_read_open_filename() validates ar (archive open) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "ar (archive open)", + "format", + "validation", + "invalid" + ], + "evidence": "archive_read_open_filename() at extractTarLz4", + "issue_pattern": "Missing format validation for ar (archive open)", + "why_false_positive": "archive_read_open_filename() validates ar (archive open) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "aw (archive write pointer allocation)", + "empty", + "string", + "validation" + ], + "evidence": "archive_write_disk_new() at extractTarLz4", + "issue_pattern": "Missing empty string validation for aw (archive write pointer allocation)", + "why_false_positive": "archive_write_disk_new() validates aw (archive write pointer allocation) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "aw (archive write pointer allocation)", + "type", + "validation", + "check" + ], + "evidence": "archive_write_disk_new() at extractTarLz4", + "issue_pattern": "Missing type validation for aw (archive write pointer allocation)", + "why_false_positive": "archive_write_disk_new() validates aw (archive write pointer allocation) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "aw (write disk options)", + "empty", + "string", + "validation" + ], + "evidence": "archive_write_disk_set_options() at extractTarLz4", + "issue_pattern": "Missing empty string validation for aw (write disk options)", + "why_false_positive": "archive_write_disk_set_options() validates aw (write disk options) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "aw (standard lookup)", + "empty", + "string", + "validation" + ], + "evidence": "archive_write_disk_set_standard_lookup() at extractTarLz4", + "issue_pattern": "Missing empty string validation for aw (standard lookup)", + "why_false_positive": "archive_write_disk_set_standard_lookup() validates aw (standard lookup) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ar (archive read next header)", + "empty", + "string", + "validation" + ], + "evidence": "archive_read_next_header() at extractTarLz4 (while loop)", + "issue_pattern": "Missing empty string validation for ar (archive read next header)", + "why_false_positive": "archive_read_next_header() validates ar (archive read next header) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "ar (archive read next header)", + "format", + "validation", + "invalid" + ], + "evidence": "archive_read_next_header() at extractTarLz4 (while loop)", + "issue_pattern": "Missing format validation for ar (archive read next header)", + "why_false_positive": "archive_read_next_header() validates ar (archive read next header) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "aw (archive write header)", + "empty", + "string", + "validation" + ], + "evidence": "archive_write_header() at extractTarLz4 (while loop)", + "issue_pattern": "Missing empty string validation for aw (archive write header)", + "why_false_positive": "archive_write_header() validates aw (archive write header) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "aw (archive write header)", + "format", + "validation", + "invalid" + ], + "evidence": "archive_write_header() at extractTarLz4 (while loop)", + "issue_pattern": "Missing format validation for aw (archive write header)", + "why_false_positive": "archive_write_header() validates aw (archive write header) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ar (archive read data block)", + "empty", + "string", + "validation" + ], + "evidence": "archive_read_data_block() at extractTarLz4 (while loop, if archive_entry_size(entry) > 0)", + "issue_pattern": "Missing empty string validation for ar (archive read data block)", + "why_false_positive": "archive_read_data_block() validates ar (archive read data block) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "ar (archive read data block)", + "format", + "validation", + "invalid" + ], + "evidence": "archive_read_data_block() at extractTarLz4 (while loop, if archive_entry_size(entry) > 0)", + "issue_pattern": "Missing format validation for ar (archive read data block)", + "why_false_positive": "archive_read_data_block() validates ar (archive read data block) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "aw (archive write data block)", + "empty", + "string", + "validation" + ], + "evidence": "archive_write_data_block() at extractTarLz4 (while loop, if archive_entry_size(entry) > 0)", + "issue_pattern": "Missing empty string validation for aw (archive write data block)", + "why_false_positive": "archive_write_data_block() validates aw (archive write data block) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "aw (archive write data block)", + "format", + "validation", + "invalid" + ], + "evidence": "archive_write_data_block() at extractTarLz4 (while loop, if archive_entry_size(entry) > 0)", + "issue_pattern": "Missing format validation for aw (archive write data block)", + "why_false_positive": "archive_write_data_block() validates aw (archive write data block) format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/Archive.cpp", + "functions": [ + { + "args": [ + "src", + "dst" + ], + "lineno": 10, + "name": "extractTarLz4" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage (no test code or references). Typical test coverage would be in a separate test suite, likely under a test/ or tests/ directory, possibly named Archive_test.cpp or similar. Gaps: No evidence of tests for invalid src, archive allocation failures, format/filter support failures, or error handling for corrupt/incomplete archives. Edge cases (e.g., permission errors, disk full, path traversal) may not be covered unless explicitly tested elsewhere.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "libarchive, boost::filesystem, custom Throw<>", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Invalid source file\")", + "field": "src (source file path)", + "location": "extractTarLz4", + "validated_by": "is_regular_file()", + "validates": [ + "Checks that the source path refers to a regular file (not directory, symlink, etc.)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Failed to allocate archive\")", + "field": "ar (archive pointer allocation)", + "location": "extractTarLz4", + "validated_by": "archive_read_new()", + "validates": [ + "Checks that the archive pointer was successfully allocated (not null)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (archive_error_string)", + "field": "ar (tar format support)", + "location": "extractTarLz4", + "validated_by": "archive_read_support_format_tar()", + "validates": [ + "Checks that the archive object supports tar format" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (archive_error_string)", + "field": "ar (lz4 filter support)", + "location": "extractTarLz4", + "validated_by": "archive_read_support_filter_lz4()", + "validates": [ + "Checks that the archive object supports lz4 compression" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (archive_error_string)", + "field": "ar (archive open)", + "location": "extractTarLz4", + "validated_by": "archive_read_open_filename()", + "validates": [ + "Checks that the archive file can be opened for reading" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Failed to allocate archive\")", + "field": "aw (archive write pointer allocation)", + "location": "extractTarLz4", + "validated_by": "archive_write_disk_new()", + "validates": [ + "Checks that the archive write pointer was successfully allocated (not null)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (archive_error_string)", + "field": "aw (write disk options)", + "location": "extractTarLz4", + "validated_by": "archive_write_disk_set_options()", + "validates": [ + "Checks that disk extraction options are set successfully" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (archive_error_string)", + "field": "aw (standard lookup)", + "location": "extractTarLz4", + "validated_by": "archive_write_disk_set_standard_lookup()", + "validates": [ + "Checks that standard user/group lookup is set for extraction" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (archive_error_string)", + "field": "ar (archive read next header)", + "location": "extractTarLz4 (while loop)", + "validated_by": "archive_read_next_header()", + "validates": [ + "Checks that the next archive entry header can be read" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (archive_error_string)", + "field": "aw (archive write header)", + "location": "extractTarLz4 (while loop)", + "validated_by": "archive_write_header()", + "validates": [ + "Checks that the archive entry header can be written to disk" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (archive_error_string)", + "field": "ar (archive read data block)", + "location": "extractTarLz4 (while loop, if archive_entry_size(entry) > 0)", + "validated_by": "archive_read_data_block()", + "validates": [ + "Checks that the next data block for the archive entry can be read" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (archive_error_string)", + "field": "aw (archive write data block)", + "location": "extractTarLz4 (while loop, if archive_entry_size(entry) > 0)", + "validated_by": "archive_write_data_block()", + "validates": [ + "Checks that the data block can be written to disk" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/Archive.cpp.ai.md b/src/libxrpl/basics/Archive.cpp.ai.md new file mode 100644 index 0000000000..cca78bb69a --- /dev/null +++ b/src/libxrpl/basics/Archive.cpp.ai.md @@ -0,0 +1,37 @@ +# `src/libxrpl/basics/Archive.cpp` + +## Purpose + +This file implements a single utility function, `extractTarLz4`, which decompresses and extracts a `.tar.lz4` archive from a source path to a destination directory. Its narrow scope reflects a specific operational need: XRPL nodes sometimes bootstrap their ledger databases by downloading pre-built compressed snapshots rather than replaying the full transaction history from genesis. This function provides the extraction primitive for that bootstrap path. + +## Design: Two-Handle libarchive Pattern + +The implementation follows the standard libarchive "copy" idiom, which requires two separate context objects: a read handle (`ar`, created with `archive_read_new()`) and a disk-write handle (`aw`, created with `archive_write_disk_new()`). This is a deliberate API design choice by libarchive — the reader is responsible for decompressing and parsing the archive format, while the disk writer handles the host filesystem semantics (permissions, timestamps, ACLs). Neither object can substitute for the other. + +Both handles are wrapped in `std::unique_ptr` with custom deleters (`archive_read_free` and `archive_write_free` respectively). This RAII ownership model is essential: if any subsequent libarchive call throws, C++ stack unwinding will invoke the deleters automatically, preventing handle leaks without requiring explicit cleanup code in error paths. + +## Extraction Pipeline + +Setup happens in three phases before the extraction loop begins: + +1. The reader is configured by calling `archive_read_support_format_tar()` and `archive_read_support_filter_lz4()` — explicitly narrowing the reader to the expected format rather than using auto-detect. This makes the function's purpose unambiguous and avoids accidentally accepting other archive types. The file is then opened with a 10 240-byte block size, which matches the value used in libarchive's own example code. + +2. The writer is configured with four extraction flags: `ARCHIVE_EXTRACT_TIME`, `ARCHIVE_EXTRACT_PERM`, `ARCHIVE_EXTRACT_ACL`, and `ARCHIVE_EXTRACT_FFLAGS`. Together these instruct the disk writer to faithfully restore timestamps, file permissions, access-control lists, and file flags from the archive metadata — a faithful extraction rather than just content. + +3. `archive_write_disk_set_standard_lookup()` enables the standard user/group name-to-UID/GID resolution, so ownership stored in the archive is remapped to the local system's user database. + +The extraction loop iterates over each entry in the archive via `archive_read_next_header()`. Before writing, it rewrites the entry's stored pathname by prepending `dst` with `archive_entry_set_pathname(entry, (dst / archive_entry_pathname(entry)).string().c_str())`. This ensures every extracted path lands under the caller-specified destination directory rather than at whatever absolute or relative path the archive records. The loop then writes the header and, for entries with non-zero size, streams data with `archive_read_data_block()` / `archive_write_data_block()`. The inner data-block loop is skipped for zero-size entries (directories, symlinks, device nodes) — calling `archive_read_data_block()` on such entries would be meaningless and could produce erroneous behavior. + +## Error Handling + +Every libarchive call returns an integer status. The code checks each return value against `ARCHIVE_OK` (0): anything strictly less than `ARCHIVE_OK` is treated as an error; `ARCHIVE_WARN` (positive) is silently tolerated as a non-fatal advisory. `ARCHIVE_EOF` is the expected termination signal for both the entry loop and the inner data-block loop. + +All errors are reported through `Throw()`, the XRPL contract mechanism defined in `contract.h`. Unlike a plain `throw`, `Throw<>` first calls `LogThrow()` to record a stack trace, giving operators a full call chain to diagnose failures. The error message is sourced from `archive_error_string()`, which returns libarchive's own human-readable description tied to the specific handle that failed — distinguishing reader-side failures (corrupt or wrong-format archive) from writer-side failures (permission denied, disk full). + +The pre-flight check `is_regular_file(src)` guards against accidentally passing a directory path or a non-existent path as the source, throwing immediately with a clear "Invalid source file" message rather than letting libarchive emit a cryptic error on open. + +## Invariants and Limitations + +The function throws on any failure; it has no partial-success state. If an error occurs mid-extraction, already-written files are not rolled back — the destination directory is left in a partial state. Callers are responsible for validating or cleaning up the destination if they need atomicity. + +The pathname rewriting prevents obvious path injection (`dst` is always prepended), but the code does not strip or reject `..` components within the stored entry names. An adversarially crafted archive could use a path like `../../etc/cron.d/evil` to escape the destination. Given that the archives in this context are expected to be trusted first-party snapshots, this is an acceptable trade-off, but it is worth noting for any future use against untrusted sources. \ No newline at end of file diff --git a/src/libxrpl/basics/BasicConfig.cpp.ai.json b/src/libxrpl/basics/BasicConfig.cpp.ai.json new file mode 100644 index 0000000000..88132e0140 --- /dev/null +++ b/src/libxrpl/basics/BasicConfig.cpp.ai.json @@ -0,0 +1,349 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "BasicConfig::section", + "Section::append" + ], + "entry_point": "BasicConfig::section(std::string const& name)", + "purpose": "Retrieves or creates a Section for a given name, then configuration lines are appended to the Section.", + "validation_points": [ + "Section::append (boost::regex re1 validation, remove_comment lambda)" + ] + }, + { + "call_chain": [ + "BasicConfig::overwrite", + "Section::set" + ], + "entry_point": "BasicConfig::overwrite(std::string const& section, std::string const& key, std::string const& value)", + "purpose": "Overwrites a key/value in a Section, creating the Section if needed.", + "validation_points": [] + }, + { + "call_chain": [ + "Section::append" + ], + "entry_point": "Section::append(std::vector const& lines)", + "purpose": "Processes and validates each configuration line, extracting key/value pairs or storing as raw values.", + "validation_points": [ + "remove_comment lambda", + "boost::regex re1" + ] + } + ], + "data_flows": [ + { + "field": "configuration line (string)", + "flow": [ + "Input vector to Section::append", + "remove_comment lambda (removes comments, trims whitespace)", + "boost::regex re1 (validates and extracts key/value)", + "Section::set (stores key/value in lookup_)", + "Section::values_ (stores unparsed lines)" + ], + "origin": "Input to Section::append (from config file or higher-level parser)", + "transformations": [ + "Comments removed (remove_comment)", + "Whitespace trimmed (trim_whitespace)", + "Regex extraction (key/value split)" + ], + "validated_at": "remove_comment lambda, boost::regex re1" + }, + { + "field": "lookup_ (std::unordered_map)", + "flow": [ + "Section::set", + "lookup_ (key/value stored)", + "Section::exists, operator<<, BasicConfig::section" + ], + "origin": "Section::set (called from append or overwrite)", + "transformations": [ + "Key/value inserted or updated" + ], + "validated_at": "Only indirectly, via regex in append" + }, + { + "field": "lines_ (std::vector)", + "flow": [ + "Section::append", + "lines_ (stores all processed lines, after comment removal and possible regex validation)" + ], + "origin": "Section::append", + "transformations": [ + "Comment removal, possible regex validation" + ], + "validated_at": "remove_comment lambda" + }, + { + "field": "values_ (std::vector)", + "flow": [ + "Section::append", + "values_ (stores lines that do not match key=value regex)" + ], + "origin": "Section::append", + "transformations": [ + "Lines not matching regex are stored as-is" + ], + "validated_at": "boost::regex re1 (if not matched, stored in values_)" + } + ], + "description": "Implements configuration file parsing and management for XRPL, including section handling, key-value storage, and legacy/INI-style config support.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "key (format: [a-zA-Z][_a-zA-Z0-9]*)", + "validation", + "missing", + "check" + ], + "evidence": "Field key (format: [a-zA-Z][_a-zA-Z0-9]*) validated by boost::regex", + "issue_pattern": "Missing validation for key (format: [a-zA-Z][_a-zA-Z0-9]*)", + "why_false_positive": "boost::regex validates key (format: [a-zA-Z][_a-zA-Z0-9]*) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "value (must have at least one non-whitespace character)", + "validation", + "missing", + "check" + ], + "evidence": "Field value (must have at least one non-whitespace character) validated by boost::regex", + "issue_pattern": "Missing validation for value (must have at least one non-whitespace character)", + "why_false_positive": "boost::regex validates value (must have at least one non-whitespace character) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "line (configuration line)", + "empty", + "string", + "validation" + ], + "evidence": "boost::regex (re1) at Section::append", + "issue_pattern": "Missing empty string validation for line (configuration line)", + "why_false_positive": "boost::regex (re1) validates line (configuration line) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "line (configuration line)", + "format", + "validation", + "invalid" + ], + "evidence": "boost::regex (re1) at Section::append", + "issue_pattern": "Missing format validation for line (configuration line)", + "why_false_positive": "boost::regex (re1) validates line (configuration line) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "line (configuration line)", + "empty", + "string", + "validation" + ], + "evidence": "remove_comment lambda at Section::append", + "issue_pattern": "Missing empty string validation for line (configuration line)", + "why_false_positive": "remove_comment lambda validates line (configuration line) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "line (configuration line)", + "format", + "validation", + "invalid" + ], + "evidence": "remove_comment lambda at Section::append", + "issue_pattern": "Missing format validation for line (configuration line)", + "why_false_positive": "remove_comment lambda validates line (configuration line) format" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/BasicConfig.cpp", + "functions": [ + { + "args": [ + "name" + ], + "lineno": 12, + "name": "Section::Section" + }, + { + "args": [ + "key", + "value" + ], + "lineno": 16, + "name": "Section::set" + }, + { + "args": [ + "lines" + ], + "lineno": 20, + "name": "Section::append" + }, + { + "args": [ + "name" + ], + "lineno": 70, + "name": "Section::exists" + }, + { + "args": [ + "os", + "section" + ], + "lineno": 74, + "name": "operator<<" + }, + { + "args": [ + "name" + ], + "lineno": 82, + "name": "BasicConfig::exists" + }, + { + "args": [ + "name" + ], + "lineno": 86, + "name": "BasicConfig::section" + }, + { + "args": [ + "name" + ], + "lineno": 91, + "name": "BasicConfig::section" + }, + { + "args": [ + "section", + "key", + "value" + ], + "lineno": 99, + "name": "BasicConfig::overwrite" + }, + { + "args": [ + "section" + ], + "lineno": 105, + "name": "BasicConfig::deprecatedClearSection" + }, + { + "args": [ + "section", + "value" + ], + "lineno": 111, + "name": "BasicConfig::legacy" + }, + { + "args": [ + "sectionName" + ], + "lineno": 115, + "name": "BasicConfig::legacy" + }, + { + "args": [ + "ifs" + ], + "lineno": 119, + "name": "BasicConfig::build" + }, + { + "args": [ + "ss", + "c" + ], + "lineno": 129, + "name": "operator<<" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, configuration parsing is tested in integration or unit tests for config loading (e.g., BasicConfig or Section tests). Gaps: No explicit test hooks for validation failures, comment handling, or malformed lines. Regex validation and comment removal logic should be tested with edge cases (escaped #, whitespace, invalid key names, etc.), but coverage cannot be confirmed from this file alone.", + "validation_architecture": { + "auto_validated_fields": [ + "key (format: [a-zA-Z][_a-zA-Z0-9]*)", + "value (must have at least one non-whitespace character)" + ], + "framework": "boost::regex", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (invalid lines are pushed to values_ vector)", + "field": "line (configuration line)", + "location": "Section::append", + "validated_by": "boost::regex (re1)", + "validates": [ + "Key must match [a-zA-Z][_a-zA-Z0-9]*", + "Line must have format: = (with optional whitespace)", + "Value must have at least one non-whitespace character" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "none (comments are stripped, empty lines are skipped)", + "field": "line (configuration line)", + "location": "Section::append", + "validated_by": "remove_comment lambda", + "validates": [ + "Removes comments starting with # unless escaped with \\", + "Trims whitespace after removing comments" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/BasicConfig.cpp.ai.md b/src/libxrpl/basics/BasicConfig.cpp.ai.md new file mode 100644 index 0000000000..bb8b1aadf9 --- /dev/null +++ b/src/libxrpl/basics/BasicConfig.cpp.ai.md @@ -0,0 +1,67 @@ +# `BasicConfig.cpp` — INI Section Parsing and Configuration Storage + +This file implements the two core classes of the XRPL configuration subsystem: `Section` and `BasicConfig`. Together they handle the mid-layer of config processing — between raw INI file text (tokenized by `parseIniFile()` in `Config.cpp`) and the typed, module-specific configuration objects that consume it. The file's job is to parse lines into structured key-value maps while preserving ordering and handling comment syntax. + +## Two-Tier Configuration Model + +`IniFileSections` (defined as `std::unordered_map>`) is the bridge type produced by `parseIniFile()`. It maps section names to their raw, unprocessed text lines. `BasicConfig::build()` consumes this structure by iterating entries and calling `Section::append()` on each, converting opaque line vectors into queryable `Section` objects stored in `map_`. This design keeps file I/O and INI tokenization entirely outside `BasicConfig` — the class only sees already-tokenized line vectors. + +`build()` is declared `protected`, not `public`. This is a deliberate architectural boundary: only `Config` (which subclasses `BasicConfig`) can call it during the load sequence. External callers interact only through the query interface and mutation methods like `overwrite()`. + +## `Section::append()` — The Core Parser + +The central logic lives in `Section::append()`. For each incoming line it performs two passes: + +First, an inline `remove_comment` lambda scans for `#` characters. If a `#` is preceded by `\`, the backslash is erased and scanning resumes from the same position — enabling escaped comment characters in values. A bare `#` truncates the line at that point via `trim_whitespace`, and the `had_trailing_comments_` flag is set to record that truncation occurred. A leading `#` (entire line is a comment) zeroes the string immediately. This is a manual scan rather than regex because the escape-handling and mutation-in-place logic requires iterative state. + +Second, the cleaned line is matched against a static compiled regex: + +``` +^(?:\s*)([a-zA-Z][_a-zA-Z0-9]*)(?:\s*)(?:=)(?:\s*)(.*\S+)(?:\s*) +``` + +The regex enforces that keys start with a letter and contain only alphanumerics and underscores, and that values contain at least one non-whitespace character. The `boost::regex_constants::optimize` flag is passed, and the regex object is `static const` — compiled exactly once across the process lifetime. + +Lines that fail the regex are **not discarded** — they are pushed to `values_`. This is intentional: many config sections contain positional entries that are not key-value pairs (peer IPs, validator keys, file paths). The `values_` vector captures these for module-specific downstream parsing, while `lines_` captures everything (including lines that matched as key-value pairs) to maintain the full input record. + +## Three Parallel Storage Vectors + +`Section` maintains three storage structures simultaneously: + +- `lookup_` (`std::unordered_map`): key-value pairs for named setting retrieval via `get()` and `value_or()` +- `lines_`: every processed line, after comment stripping, in order — the complete record +- `values_`: only non-key-value lines — bare positional data + +This tripartite design avoids forcing all config into the key-value paradigm while still providing O(1) lookups for named settings. The `lookup_` map uses `insert_or_assign` in `set()`, so repeated calls (e.g., from `append()` followed by `overwrite()`) always take the last value without error. + +## The Null-Object Pattern for Missing Sections + +The `const` overload of `BasicConfig::section()` returns a reference to a `static Section const none("")` when the requested name isn't found, rather than throwing or returning a pointer: + +```cpp +Section const& +BasicConfig::section(std::string const& name) const +{ + static Section const none(""); + auto const iter = map_.find(name); + if (iter == map_.end()) + return none; + return iter->second; +} +``` + +This enables the common call pattern `config["missing_section"].get("key")` to safely return `std::nullopt` without the caller needing null checks. The mutable overload, by contrast, uses `map_.emplace(name, name)` which auto-creates the section on demand — appropriate for mutation paths like `overwrite()` but not for const queries. + +## Mutation After Load: `overwrite()` and `deprecatedClearSection()` + +`overwrite()` creates the target section via `std::piecewise_construct` if it doesn't exist, then calls `Section::set()` directly — bypassing `append()` and its comment/regex machinery. This is the path used for command-line argument injection, where CLI-supplied values must take precedence over file-based config regardless of format concerns. + +`deprecatedClearSection()` replaces an existing `Section` object wholesale with a fresh empty one, erasing all key-value pairs and lines. The `deprecated` prefix in the name is an explicit signal that this operation pattern is considered a design smell and its callers are candidates for refactoring. + +## Legacy Mode + +Some older XRPL config sections hold a single bare value (not key=value pairs) — for example, a database path or a simple flag. The `legacy()` pair of methods handles this: `BasicConfig::legacy(section, value)` injects a raw string into a section's first `lines_` slot via `Section::legacy(string)`, and the getter enforces the invariant that `legacy()` is only valid on sections with exactly one line (throwing `std::runtime_error` otherwise). This is backward compatibility scaffolding, not a general pattern. + +## `had_trailing_comments_` and Round-Trip Fidelity + +The `had_trailing_comments_` flag on `Section`, aggregated at the `BasicConfig` level by `had_trailing_comments()`, indicates whether any loaded section had inline comments that were stripped from values. This is surfaced so that the application can warn users that round-tripping the config (rewriting it from the in-memory representation) would lose those comments — the `operator<<` overloads only emit `key=value` pairs and section headers, not comments. \ No newline at end of file diff --git a/src/libxrpl/basics/CountedObject.cpp.ai.json b/src/libxrpl/basics/CountedObject.cpp.ai.json new file mode 100644 index 0000000000..2108307f68 --- /dev/null +++ b/src/libxrpl/basics/CountedObject.cpp.ai.json @@ -0,0 +1,125 @@ +{ + "args": [ + { + "lineno": 18, + "name": "minimumThreshold" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "CountedObjects::getInstance", + "CountedObjects::CountedObjects (constructor, if first call)" + ], + "entry_point": "CountedObjects::getInstance", + "purpose": "Provides singleton access to the CountedObjects instance.", + "validation_points": [] + }, + { + "call_chain": [ + "CountedObjects::getCounts" + ], + "entry_point": "CountedObjects::getCounts", + "purpose": "Retrieves a list of counted object names and their counts, filtered by a minimum threshold.", + "validation_points": [ + "if (ctr->getCount() >= minimumThreshold)" + ] + } + ], + "data_flows": [ + { + "field": "m_count", + "flow": [ + "CountedObjects::CountedObjects (constructor)", + "m_count.load() in getCounts" + ], + "origin": "Initialized to 0 in CountedObjects::CountedObjects", + "transformations": [ + "Atomic load for reserving vector capacity" + ], + "validated_at": "No explicit validation; used for vector reservation" + }, + { + "field": "m_head", + "flow": [ + "CountedObjects::CountedObjects (constructor)", + "m_head.load() in getCounts", + "Iterated as linked list via ctr->getNext()" + ], + "origin": "Initialized to nullptr in CountedObjects::CountedObjects", + "transformations": [ + "Atomic load; traversed as linked list" + ], + "validated_at": "No explicit validation; null-checked in for loop" + }, + { + "field": "ctr->getCount()", + "flow": [ + "ctr->getCount()", + "Compared to minimumThreshold in getCounts", + "If passes, included in counts vector" + ], + "origin": "Each counted object in the linked list", + "transformations": [ + "Filtered by minimumThreshold" + ], + "validated_at": "Validated in: if (ctr->getCount() >= minimumThreshold)" + }, + { + "field": "minimumThreshold (parameter)", + "flow": [ + "Passed to getCounts", + "Used in filter: ctr->getCount() >= minimumThreshold" + ], + "origin": "Input parameter to getCounts", + "transformations": [ + "Used as lower bound filter" + ], + "validated_at": "Used directly; no explicit validation of parameter itself" + } + ], + "description": "Implements the CountedObjects singleton for tracking and retrieving counts of registered objects, providing thread-safe access and sorted retrieval of object counts.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/CountedObject.cpp", + "functions": [ + { + "args": [], + "lineno": 6, + "name": "CountedObjects::getInstance" + }, + { + "args": [], + "lineno": 13, + "name": "CountedObjects::CountedObjects" + }, + { + "args": [ + "minimumThreshold" + ], + "lineno": 17, + "name": "CountedObjects::getCounts" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is a utility for tracking object counts and does not perform input validation in the traditional sense (e.g., user input). The only 'validation' is filtering counts by minimumThreshold in getCounts. Test coverage would likely exist in unit tests for CountedObject or CountedObjects, possibly in files like CountedObject_test.cpp or similar. Tests would check that getCounts returns correct results for various thresholds and object counts. There is no explicit validation of input parameters (e.g., negative minimumThreshold), so edge cases like negative thresholds may not be tested. No direct validation code paths exist beyond the count filtering.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/basics/CountedObject.cpp.ai.md b/src/libxrpl/basics/CountedObject.cpp.ai.md new file mode 100644 index 0000000000..f4e38d8a39 --- /dev/null +++ b/src/libxrpl/basics/CountedObject.cpp.ai.md @@ -0,0 +1,51 @@ +# `CountedObject.cpp` — Object Instance Tracking Registry + +This file provides the implementation of the `CountedObjects` singleton, the runtime registry at the center of the XRPL ledger's live object-count telemetry system. Its purpose is to support operator diagnostics: any subsystem class that inherits from `CountedObject` automatically registers itself and has its live instance count reflected in the `get_counts` admin RPC command. + +## System Architecture + +The mechanism is a two-layer design defined in `CountedObject.h` and implemented here. + +**`CountedObject` (CRTP template)** is the user-facing mixin. Classes throughout the codebase inherit from it — `Ledger`, `STObject`, `SHAMapInnerNode`, `Job`, `NodeObject`, and dozens more. Each template instantiation owns a single function-local `static CountedObjects::Counter` identified by the demangled type name (via `beast::type_name()`). The mixin's constructor, copy constructor, and destructor call `increment()` and `decrement()` on that static counter, making instance tracking completely transparent to the derived class. + +**`CountedObjects`** is the singleton registry. It holds two atomic members: `m_count`, a rough count of how many distinct `Counter` objects exist (i.e., how many tracked types are in the process), and `m_head`, the head of a lock-free singly-linked list of all live `Counter` instances. + +## Lock-Free Registration + +The most important design detail lives in `Counter`'s constructor in the header: + +```cpp +do { + head = instance.m_head.load(); + next_ = head; +} while (instance.m_head.exchange(this) != head); +++instance.m_count; +``` + +This is a classic compare-and-swap loop for prepending to a lock-free linked list. Each new `Counter` (one per tracked type, created lazily on first construction of that type) reads the current list head, sets its own `next_` to it, then attempts to atomically swap itself in as the new head. If another thread has modified the head concurrently, the exchange returns a value that doesn't match the previously read head, and the loop retries. This avoids any mutex on the registration path, which matters because registration happens at program startup or on first use of a given type — potentially during global initialization where lock ordering is undefined. + +Note that `++instance.m_count` after the CAS loop is not itself atomic with the list insertion. This is intentional — the comment in `getCounts()` acknowledges it: + +> When other operations are concurrent, the count might be temporarily less than the actual count. + +`m_count` is used only to pre-allocate the return vector (`counts.reserve(m_count.load())`), so a transient undercount merely causes one extra reallocation in a rare race — a perfectly acceptable tradeoff for avoiding a more complex two-word CAS or lock. + +## `CountedObjects` Singleton + +`getInstance()` uses the Meyer's singleton idiom — a function-local `static CountedObjects instance` — which guarantees thread-safe initialization under C++11 and later without any explicit synchronization. The constructor initializes both atomic members to their zero/null states. + +## `getCounts()` — Diagnostic Snapshot + +`getCounts(int minimumThreshold)` is the only public query method. It traverses the lock-free linked list of `Counter` objects, collecting entries whose live count meets or exceeds the threshold, then sorts the result alphabetically by type name. + +The traversal is inherently a snapshot under concurrent mutation: `Counter` nodes are never removed (they are `static` objects with program lifetime), so the list only grows. A snapshot taken mid-traversal may miss a `Counter` that was prepended after the traversal started, but will never encounter a dangling pointer. This is safe but not strongly consistent — again an acceptable tradeoff for a purely diagnostic facility. + +The `minimumThreshold` filter exists so callers can suppress noise. The `get_counts` RPC handler in `GetCounts.cpp` passes `minObjectCount` from the request (defaulting to 10), meaning types with fewer than ten live instances are omitted from the JSON response. Tests use thresholds of 0 or 10 depending on whether they want an exhaustive view or a realistic operator view. + +## Relationship to Diagnostics + +The output of `getCounts()` feeds directly into the `get_counts` admin JSON-RPC command, where each `(name, count)` entry becomes a key-value pair in the response object. This gives operators a live snapshot of which XRPL internal object types are consuming memory at any moment — useful for diagnosing memory leaks, unexpected object retention (e.g., stale `SHAMapInnerNode` instances after ledger close), or verifying that cleanup paths are functioning correctly. The regression test in `Regression_test.cpp` exploits the same mechanism to assert that the SHAMap does not leak nodes between ledger applications. + +## Design Tradeoffs + +The design prioritizes zero overhead on the hot path. Each constructor/destructor call does exactly one `std::atomic` increment or decrement — a single fetch-add instruction on modern hardware. Registration of a new type incurs a CAS loop, but that happens at most once per type per process lifetime. The `getCounts()` snapshot path is slow (full list traversal + sort) but it is only ever called from admin RPC or test code, never from any ledger-critical path. \ No newline at end of file diff --git a/src/libxrpl/basics/FileUtilities.cpp.ai.json b/src/libxrpl/basics/FileUtilities.cpp.ai.json new file mode 100644 index 0000000000..9ce9063213 --- /dev/null +++ b/src/libxrpl/basics/FileUtilities.cpp.ai.json @@ -0,0 +1,562 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "getFileContents" + ], + "entry_point": "getFileContents", + "purpose": "Reads the contents of a file at sourcePath, optionally enforcing a maximum file size.", + "validation_points": [ + "sourcePath validated by boost::filesystem::canonical (checks path validity, existence, symlinks, etc.)", + "maxSize validated by file_size(fullPath, ec) > *maxSize (checks file is not too large)", + "fullPath validated by file_size(fullPath, ec) (checks file exists and is accessible)", + "fullPath validated by std::ifstream fileStream(fullPath.string(), std::ios::in); if (!fileStream) (checks file can be opened for reading)", + "fileStream validated by fileStream.bad() (checks for stream errors after reading)" + ] + }, + { + "call_chain": [ + "writeFileContents" + ], + "entry_point": "writeFileContents", + "purpose": "Writes the given contents to the file at destPath, overwriting any existing file.", + "validation_points": [ + "destPath validated by std::ofstream fileStream(destPath.string(), std::ios::out | std::ios::trunc); if (!fileStream) (checks file can be opened for writing)", + "fileStream validated by fileStream.bad() (checks for stream errors after writing)" + ] + } + ], + "data_flows": [ + { + "field": "sourcePath", + "flow": [ + "Input parameter", + "canonical(sourcePath, ec) \u2192 fullPath", + "file_size(fullPath, ec)", + "std::ifstream fileStream(fullPath.string(), std::ios::in)", + "std::istreambuf_iterator{fileStream} \u2192 result" + ], + "origin": "Input parameter to getFileContents", + "transformations": [ + "canonical() resolves symlinks, checks existence, returns absolute path", + "file_size() checks file size and existence", + "ifstream opens file for reading", + "istreambuf_iterator reads file contents into string" + ], + "validated_at": "canonical(sourcePath, ec), file_size(fullPath, ec), ifstream open, fileStream.bad()" + }, + { + "field": "maxSize", + "flow": [ + "Input parameter", + "if (maxSize && (file_size(fullPath, ec) > *maxSize || ec))" + ], + "origin": "Input parameter to getFileContents", + "transformations": [ + "file_size() retrieves file size", + "Comparison to *maxSize" + ], + "validated_at": "file_size(fullPath, ec) > *maxSize" + }, + { + "field": "fullPath", + "flow": [ + "Result of canonical()", + "file_size(fullPath, ec)", + "std::ifstream fileStream(fullPath.string(), std::ios::in)" + ], + "origin": "Result of canonical(sourcePath, ec)", + "transformations": [ + "Used as input to file_size() and ifstream" + ], + "validated_at": "file_size(fullPath, ec), ifstream open" + }, + { + "field": "fileStream", + "flow": [ + "ifstream open", + "istreambuf_iterator reads from fileStream", + "fileStream.bad() checked after reading" + ], + "origin": "std::ifstream fileStream(fullPath.string(), std::ios::in)", + "transformations": [ + "ifstream reads file into string" + ], + "validated_at": "if (!fileStream), fileStream.bad()" + }, + { + "field": "destPath", + "flow": [ + "Input parameter", + "std::ofstream fileStream(destPath.string(), std::ios::out | std::ios::trunc)", + "fileStream << contents" + ], + "origin": "Input parameter to writeFileContents", + "transformations": [ + "ofstream opens file for writing" + ], + "validated_at": "if (!fileStream), fileStream.bad()" + }, + { + "field": "contents", + "flow": [ + "Input parameter", + "fileStream << contents" + ], + "origin": "Input parameter to writeFileContents", + "transformations": [ + "Written directly to file" + ], + "validated_at": "fileStream.bad() (indirectly, after writing)" + } + ], + "description": "Provides utility functions for reading from and writing to files, with error handling, in the xrpl namespace.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sourcePath (via boost::filesystem::canonical)", + "validation", + "missing", + "check" + ], + "evidence": "Field sourcePath (via boost::filesystem::canonical) validated by Boost.Filesystem, std::fstream", + "issue_pattern": "Missing validation for sourcePath (via boost::filesystem::canonical)", + "why_false_positive": "Boost.Filesystem, std::fstream validates sourcePath (via boost::filesystem::canonical) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "fullPath (via boost::filesystem::file_size and std::ifstream)", + "validation", + "missing", + "check" + ], + "evidence": "Field fullPath (via boost::filesystem::file_size and std::ifstream) validated by Boost.Filesystem, std::fstream", + "issue_pattern": "Missing validation for fullPath (via boost::filesystem::file_size and std::ifstream)", + "why_false_positive": "Boost.Filesystem, std::fstream validates fullPath (via boost::filesystem::file_size and std::ifstream) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "destPath (via std::ofstream)", + "validation", + "missing", + "check" + ], + "evidence": "Field destPath (via std::ofstream) validated by Boost.Filesystem, std::fstream", + "issue_pattern": "Missing validation for destPath (via std::ofstream)", + "why_false_positive": "Boost.Filesystem, std::fstream validates destPath (via std::ofstream) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sourcePath", + "empty", + "string", + "validation" + ], + "evidence": "boost::filesystem::canonical at getFileContents", + "issue_pattern": "Missing empty string validation for sourcePath", + "why_false_positive": "boost::filesystem::canonical validates sourcePath for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sourcePath", + "format", + "validation", + "invalid" + ], + "evidence": "boost::filesystem::canonical at getFileContents", + "issue_pattern": "Missing format validation for sourcePath", + "why_false_positive": "boost::filesystem::canonical validates sourcePath format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "maxSize", + "empty", + "string", + "validation" + ], + "evidence": "file_size(fullPath, ec) > *maxSize at getFileContents", + "issue_pattern": "Missing empty string validation for maxSize", + "why_false_positive": "file_size(fullPath, ec) > *maxSize validates maxSize for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "maxSize", + "range", + "bounds", + "validation" + ], + "evidence": "file_size(fullPath, ec) > *maxSize at getFileContents", + "issue_pattern": "Missing range validation for maxSize", + "why_false_positive": "file_size(fullPath, ec) > *maxSize validates maxSize range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fullPath", + "empty", + "string", + "validation" + ], + "evidence": "file_size(fullPath, ec) at getFileContents", + "issue_pattern": "Missing empty string validation for fullPath", + "why_false_positive": "file_size(fullPath, ec) validates fullPath for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "fullPath", + "format", + "validation", + "invalid" + ], + "evidence": "file_size(fullPath, ec) at getFileContents", + "issue_pattern": "Missing format validation for fullPath", + "why_false_positive": "file_size(fullPath, ec) validates fullPath format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fullPath", + "empty", + "string", + "validation" + ], + "evidence": "std::ifstream fileStream(fullPath.string(), std::ios::in); if (!fileStream) at getFileContents", + "issue_pattern": "Missing empty string validation for fullPath", + "why_false_positive": "std::ifstream fileStream(fullPath.string(), std::ios::in); if (!fileStream) validates fullPath for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "fullPath", + "format", + "validation", + "invalid" + ], + "evidence": "std::ifstream fileStream(fullPath.string(), std::ios::in); if (!fileStream) at getFileContents", + "issue_pattern": "Missing format validation for fullPath", + "why_false_positive": "std::ifstream fileStream(fullPath.string(), std::ios::in); if (!fileStream) validates fullPath format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fileStream", + "empty", + "string", + "validation" + ], + "evidence": "fileStream.bad() at getFileContents", + "issue_pattern": "Missing empty string validation for fileStream", + "why_false_positive": "fileStream.bad() validates fileStream for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "fileStream", + "format", + "validation", + "invalid" + ], + "evidence": "fileStream.bad() at getFileContents", + "issue_pattern": "Missing format validation for fileStream", + "why_false_positive": "fileStream.bad() validates fileStream format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "destPath", + "empty", + "string", + "validation" + ], + "evidence": "std::ofstream fileStream(destPath.string(), std::ios::out | std::ios::trunc); if (!fileStream) at writeFileContents", + "issue_pattern": "Missing empty string validation for destPath", + "why_false_positive": "std::ofstream fileStream(destPath.string(), std::ios::out | std::ios::trunc); if (!fileStream) validates destPath for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "destPath", + "format", + "validation", + "invalid" + ], + "evidence": "std::ofstream fileStream(destPath.string(), std::ios::out | std::ios::trunc); if (!fileStream) at writeFileContents", + "issue_pattern": "Missing format validation for destPath", + "why_false_positive": "std::ofstream fileStream(destPath.string(), std::ios::out | std::ios::trunc); if (!fileStream) validates destPath format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fileStream", + "empty", + "string", + "validation" + ], + "evidence": "fileStream.bad() at writeFileContents", + "issue_pattern": "Missing empty string validation for fileStream", + "why_false_positive": "fileStream.bad() validates fileStream for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "fileStream", + "format", + "validation", + "invalid" + ], + "evidence": "fileStream.bad() at writeFileContents", + "issue_pattern": "Missing format validation for fileStream", + "why_false_positive": "fileStream.bad() validates fileStream format" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "file handle", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::ifstream", + "issue_pattern": "Missing file handle cleanup", + "why_false_positive": "std::ifstream provides automatic file handle cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "file handle", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::ofstream", + "issue_pattern": "Missing file handle cleanup", + "why_false_positive": "std::ofstream provides automatic file handle cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/FileUtilities.cpp", + "functions": [ + { + "args": [ + "ec", + "sourcePath", + "maxSize" + ], + "lineno": 11, + "name": "getFileContents" + }, + { + "args": [ + "ec", + "destPath", + "contents" + ], + "lineno": 44, + "name": "writeFileContents" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic file handle cleanup", + "false_positive_risk": "Missing file handle cleanup", + "resource": "file handle", + "type": "raii_wrapper", + "wrapper_type": "std::ifstream" + }, + { + "audit_implication": "Automatic file handle cleanup", + "false_positive_risk": "Missing file handle cleanup", + "resource": "file handle", + "type": "raii_wrapper", + "wrapper_type": "std::ofstream" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage (no test code or references). Typical tests would be in a corresponding test suite (e.g., FileUtilities_test.cpp or similar). Key validation paths (invalid path, file too large, file open failure, stream errors) should be tested. Gaps may include: edge cases for symlinks, permission errors, partial reads/writes, and error_code propagation. If tests do not explicitly check error_code values and all validation branches, coverage may be incomplete.", + "validation_architecture": { + "auto_validated_fields": [ + "sourcePath (via boost::filesystem::canonical)", + "fullPath (via boost::filesystem::file_size and std::ifstream)", + "destPath (via std::ofstream)" + ], + "framework": "Boost.Filesystem, std::fstream", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code set (ec)", + "field": "sourcePath", + "location": "getFileContents", + "validated_by": "boost::filesystem::canonical", + "validates": [ + "Checks if sourcePath exists and is a valid path", + "Resolves symbolic links and returns the canonical path" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code set (ec) with file_too_large", + "field": "maxSize", + "location": "getFileContents", + "validated_by": "file_size(fullPath, ec) > *maxSize", + "validates": [ + "Checks if file size exceeds maxSize" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code set (ec)", + "field": "fullPath", + "location": "getFileContents", + "validated_by": "file_size(fullPath, ec)", + "validates": [ + "Checks if file at fullPath exists and is accessible" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code set (ec) with errno", + "field": "fullPath", + "location": "getFileContents", + "validated_by": "std::ifstream fileStream(fullPath.string(), std::ios::in); if (!fileStream)", + "validates": [ + "Checks if file can be opened for reading" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code set (ec) with errno", + "field": "fileStream", + "location": "getFileContents", + "validated_by": "fileStream.bad()", + "validates": [ + "Checks for I/O errors during file reading" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code set (ec) with errno", + "field": "destPath", + "location": "writeFileContents", + "validated_by": "std::ofstream fileStream(destPath.string(), std::ios::out | std::ios::trunc); if (!fileStream)", + "validates": [ + "Checks if file can be opened for writing" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code set (ec) with errno", + "field": "fileStream", + "location": "writeFileContents", + "validated_by": "fileStream.bad()", + "validates": [ + "Checks for I/O errors during file writing" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/FileUtilities.cpp.ai.md b/src/libxrpl/basics/FileUtilities.cpp.ai.md new file mode 100644 index 0000000000..3295838bd7 --- /dev/null +++ b/src/libxrpl/basics/FileUtilities.cpp.ai.md @@ -0,0 +1,37 @@ +# `src/libxrpl/basics/FileUtilities.cpp` + +This file implements two thin utility functions — `getFileContents` and `writeFileContents` — that provide the XRPL codebase with a consistent, error-code-based interface for whole-file I/O. Both functions live in the `xrpl` namespace and are declared in `include/xrpl/basics/FileUtilities.h`. + +## Why This Exists + +XRPL's server startup and runtime both need to load files from disk: the node configuration (`Config.cpp`), the trusted validator list (`ValidatorList.cpp`), and file-based validator site responses (`WorkFile.h`). Rather than scattering ad-hoc `std::ifstream` boilerplate throughout those callers — each with slightly different error handling — this module centralises the pattern. It also gives callers a uniform `boost::system::error_code` output rather than requiring them to handle both C++ exceptions and POSIX errno. + +## `getFileContents` + +The read path is deliberately layered. Four distinct checks gate progress before a single byte is read into the result string: + +1. **`boost::filesystem::canonical(sourcePath, ec)`** — resolves the path to an absolute, symlink-free canonical form and verifies that the path exists. Failures (non-existent file, broken symlink, permission denied during resolution) set `ec` and return an empty string immediately. This is the most defensive step: using the canonical path for all subsequent operations ensures there is no TOCTOU race where the path could be swapped between the existence check and the open. + +2. **`file_size(fullPath, ec)` against `maxSize`** — when the caller supplies an `std::optional` upper bound, the file size is checked before any read attempt. On excess size the function injects `boost::system::errc::file_too_large` into `ec` and returns. This is significant: `file_too_large` is not an errno that most operating systems would naturally produce for an `fread`, so injecting it explicitly makes callers' error-checking code unambiguous. `WorkFile::run()` enforces a hard 1 MB cap this way. + +3. **`std::ifstream` construction and `!fileStream` check** — even if `canonical` succeeded and the file size is within bounds, the actual open can fail (e.g., due to permission changes between the two calls). On failure, `errno` is cast to `boost::system::errc_t` and wrapped into `ec`. This cast is sound because Boost defines its `errc` values to map directly to POSIX errno values. + +4. **`fileStream.bad()` post-read check** — `std::istreambuf_iterator` reads the entire file in a single expression. `bad()` detects hardware errors or stream corruption that occurred during that read. EOF is not an error here; `bad()` only triggers on actual I/O failures. + +## `writeFileContents` + +The write path is simpler and intentionally asymmetric with the read path. Notably, it **does not call `canonical()`**. This is the correct choice: `canonical()` requires the path to exist, but the destination file is often being created for the first time (e.g., writing a cached validator list). Instead, `writeFileContents` opens directly with `std::ios::out | std::ios::trunc`, which creates or truncates as needed. The same two-stage error pattern applies: check `!fileStream` on open, then check `fileStream.bad()` after writing. + +`std::ios::trunc` means the write is a full replacement — there is no atomic rename/replace-then-swap pattern here. Callers that need crash-safe writes must handle that at a higher level. + +## Error Reporting Contract + +Both functions communicate errors exclusively through the `boost::system::error_code& ec` output parameter. Neither throws. This matches the broader XRPL convention for low-level operations where the caller — not the callee — decides whether an error is fatal. Config loading converts a non-zero `ec` into a thrown `std::runtime_error`; `ValidatorList` also throws; `WorkFile` passes `ec` directly to its async callback. The same two functions serve all three idioms without any special-casing. + +## Resource Management + +Both `std::ifstream` and `std::ofstream` are stack-allocated local variables, so file handles are unconditionally released when the function returns, even on early-exit error paths. No explicit `close()` call is needed or present. + +## Test Coverage + +`FileUtilities_test.cpp` exercises the happy path (no size cap), a permissive size cap (1 KB on a 44-byte file), and the rejection case (`maxSize = 16`, expecting `errc::file_too_large`). `writeFileContents` is used as test setup but has no dedicated failure tests. Edge cases such as permission errors, broken symlinks, and partial-write failures are not covered by the unit suite. \ No newline at end of file diff --git a/src/libxrpl/basics/Log.cpp.ai.json b/src/libxrpl/basics/Log.cpp.ai.json new file mode 100644 index 0000000000..1b55b6a280 --- /dev/null +++ b/src/libxrpl/basics/Log.cpp.ai.json @@ -0,0 +1,598 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::string const& partition", + "beast::severities::Severity thresh", + "Logs& logs" + ], + "lineno": 13, + "name": "Logs::Sink" + }, + { + "args": [], + "lineno": 34, + "name": "Logs::File" + }, + { + "args": [ + "beast::severities::Severity thresh" + ], + "lineno": 77, + "name": "Logs" + }, + { + "args": [], + "lineno": 263, + "name": "DebugSink" + } + ], + "code_paths": [ + { + "call_chain": [ + "Logs::Sink::write", + "Logs::Sink::threshold()", + "Logs::Logs::write" + ], + "entry_point": "Logs::Sink::write", + "purpose": "Handles a log message for a given severity and partition; only writes if severity passes threshold.", + "validation_points": [ + "Logs::Sink::write: level < threshold()" + ] + }, + { + "call_chain": [ + "Logs::File::open", + "std::ofstream::good" + ], + "entry_point": "Logs::File::open", + "purpose": "Opens a log file for writing; validates stream is good before using.", + "validation_points": [ + "Logs::File::open: stream->good()" + ] + }, + { + "call_chain": [ + "Logs::File::write", + "m_stream != nullptr" + ], + "entry_point": "Logs::File::write", + "purpose": "Writes text to the log file if the file stream is valid.", + "validation_points": [ + "Logs::File::write: m_stream != nullptr" + ] + }, + { + "call_chain": [ + "Logs::File::writeln", + "m_stream != nullptr" + ], + "entry_point": "Logs::File::writeln", + "purpose": "Writes a line to the log file if the file stream is valid.", + "validation_points": [ + "Logs::File::writeln: m_stream != nullptr" + ] + }, + { + "call_chain": [ + "Logs::write", + "Logs::File::writeln" + ], + "entry_point": "Logs::write", + "purpose": "Formats and writes a log message to the file and possibly to stderr.", + "validation_points": [ + "Logs::File::writeln: m_stream != nullptr" + ] + } + ], + "data_flows": [ + { + "field": "level (log severity)", + "flow": [ + "Logs::Sink::write(level, text)", + "if (level < threshold()) return", + "logs_.write(level, partition_, text, console())" + ], + "origin": "Passed as argument to Logs::Sink::write", + "transformations": [ + "Compared to threshold() to determine if message should be logged" + ], + "validated_at": "Logs::Sink::write" + }, + { + "field": "m_stream (file stream pointer)", + "flow": [ + "Logs::File::open: creates std::ofstream", + "if stream->good(), m_stream = stream", + "Logs::File::write/writeln: if (m_stream != nullptr) use m_stream" + ], + "origin": "Set in Logs::File::open if stream->good()", + "transformations": [ + "Set to nullptr on close; set to valid stream on open" + ], + "validated_at": "Logs::File::write, Logs::File::writeln" + }, + { + "field": "stream (std::unique_ptr)", + "flow": [ + "Logs::File::open: new std::ofstream(path, app)", + "if stream->good(), m_stream = stream" + ], + "origin": "Created in Logs::File::open", + "transformations": [ + "Checked for good() before assignment" + ], + "validated_at": "Logs::File::open" + }, + { + "field": "partition (log partition name)", + "flow": [ + "Logs::Sink::write: partition_ member", + "Logs::Logs::write: used in format()" + ], + "origin": "Passed to Logs::Sink::Sink and Logs::Sink::write", + "transformations": [ + "Used as a label in log formatting" + ], + "validated_at": "No explicit validation" + }, + { + "field": "text (log message)", + "flow": [ + "Logs::Sink::write: text", + "Logs::Logs::write: text", + "Logs::Logs::format: s, text, level, partition", + "Logs::File::writeln: s" + ], + "origin": "Passed to Logs::Sink::write", + "transformations": [ + "Formatted with severity and partition" + ], + "validated_at": "No explicit validation" + } + ], + "description": "This file implements logging functionality for the XRPL project, including log sinks, file logging, log severity management, log formatting, and debug log handling. It provides mechanisms to write logs to files and the console, manage log severity thresholds, redact sensitive information from logs, and handle debug log sinks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "level (log severity)", + "empty", + "string", + "validation" + ], + "evidence": "comparison with threshold() at Logs::Sink::write", + "issue_pattern": "Missing empty string validation for level (log severity)", + "why_false_positive": "comparison with threshold() validates level (log severity) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_stream (file stream pointer)", + "empty", + "string", + "validation" + ], + "evidence": "nullptr check at Logs::File::write, Logs::File::writeln, Logs::File::isOpen", + "issue_pattern": "Missing empty string validation for m_stream (file stream pointer)", + "why_false_positive": "nullptr check validates m_stream (file stream pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "stream (ofstream pointer)", + "empty", + "string", + "validation" + ], + "evidence": "stream->good() at Logs::File::open", + "issue_pattern": "Missing empty string validation for stream (ofstream pointer)", + "why_false_positive": "stream->good() validates stream (ofstream pointer) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "file handle", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::ofstream", + "issue_pattern": "Missing file handle cleanup", + "why_false_positive": "std::ofstream provides automatic file handle cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/Log.cpp", + "functions": [ + { + "args": [ + "std::string const& partition", + "beast::severities::Severity thresh", + "Logs& logs" + ], + "lineno": 15, + "name": "Logs::Sink::Sink" + }, + { + "args": [ + "beast::severities::Severity level", + "std::string const& text" + ], + "lineno": 21, + "name": "Logs::Sink::write" + }, + { + "args": [ + "beast::severities::Severity level", + "std::string const& text" + ], + "lineno": 28, + "name": "Logs::Sink::writeAlways" + }, + { + "args": [], + "lineno": 35, + "name": "Logs::File::File" + }, + { + "args": [], + "lineno": 39, + "name": "Logs::File::isOpen" + }, + { + "args": [ + "boost::filesystem::path const& path" + ], + "lineno": 43, + "name": "Logs::File::open" + }, + { + "args": [], + "lineno": 56, + "name": "Logs::File::closeAndReopen" + }, + { + "args": [], + "lineno": 61, + "name": "Logs::File::close" + }, + { + "args": [ + "char const* text" + ], + "lineno": 65, + "name": "Logs::File::write" + }, + { + "args": [ + "char const* text" + ], + "lineno": 70, + "name": "Logs::File::writeln" + }, + { + "args": [ + "beast::severities::Severity thresh" + ], + "lineno": 78, + "name": "Logs::Logs" + }, + { + "args": [ + "boost::filesystem::path const& pathToLogFile" + ], + "lineno": 81, + "name": "Logs::open" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 85, + "name": "Logs::get" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 91, + "name": "Logs::operator[]" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 95, + "name": "Logs::journal" + }, + { + "args": [], + "lineno": 99, + "name": "Logs::threshold" + }, + { + "args": [ + "beast::severities::Severity thresh" + ], + "lineno": 103, + "name": "Logs::threshold" + }, + { + "args": [], + "lineno": 110, + "name": "Logs::partition_severities" + }, + { + "args": [ + "beast::severities::Severity level", + "std::string const& partition", + "std::string const& text", + "bool console" + ], + "lineno": 120, + "name": "Logs::write" + }, + { + "args": [], + "lineno": 132, + "name": "Logs::rotate" + }, + { + "args": [ + "std::string const& name", + "beast::severities::Severity threshold" + ], + "lineno": 139, + "name": "Logs::makeSink" + }, + { + "args": [ + "beast::severities::Severity level" + ], + "lineno": 143, + "name": "Logs::fromSeverity" + }, + { + "args": [ + "LogSeverity level" + ], + "lineno": 163, + "name": "Logs::toSeverity" + }, + { + "args": [ + "LogSeverity s" + ], + "lineno": 183, + "name": "Logs::toString" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 202, + "name": "Logs::fromString" + }, + { + "args": [ + "std::string& output", + "std::string const& message", + "beast::severities::Severity severity", + "std::string const& partition" + ], + "lineno": 218, + "name": "Logs::format" + }, + { + "args": [], + "lineno": 265, + "name": "DebugSink::DebugSink" + }, + { + "args": [ + "DebugSink const&" + ], + "lineno": 270, + "name": "DebugSink::DebugSink" + }, + { + "args": [ + "DebugSink const&" + ], + "lineno": 271, + "name": "DebugSink::operator=" + }, + { + "args": [ + "DebugSink&&" + ], + "lineno": 273, + "name": "DebugSink::DebugSink" + }, + { + "args": [ + "DebugSink&&" + ], + "lineno": 274, + "name": "DebugSink::operator=" + }, + { + "args": [ + "std::unique_ptr sink" + ], + "lineno": 276, + "name": "DebugSink::set" + }, + { + "args": [], + "lineno": 289, + "name": "DebugSink::get" + }, + { + "args": [], + "lineno": 296, + "name": "debugSink" + }, + { + "args": [ + "std::unique_ptr sink" + ], + "lineno": 302, + "name": "setDebugLogSink" + }, + { + "args": [], + "lineno": 307, + "name": "debugLog" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic file handle cleanup", + "false_positive_risk": "Missing file handle cleanup", + "resource": "file handle", + "type": "raii_wrapper", + "wrapper_type": "std::ofstream" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, logging code is tested via integration or system tests, not unit tests, due to side effects (file I/O, console output). Possible test files: test/log_test.cpp, test/logs_test.cpp, or integration tests that check log file creation and contents. Gaps: No explicit tests for validation logic (e.g., threshold filtering, file stream validity). Edge cases like file open failures, null streams, or invalid severity levels may not be directly tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation, standard library checks)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "level (log severity)", + "location": "Logs::Sink::write", + "validated_by": "comparison with threshold()", + "validates": [ + "level is at least threshold severity" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (early return or boolean result)", + "field": "m_stream (file stream pointer)", + "location": "Logs::File::write, Logs::File::writeln, Logs::File::isOpen", + "validated_by": "nullptr check", + "validates": [ + "file stream is open before writing" + ], + "validation_type": "type/presence" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false if not good)", + "field": "stream (ofstream pointer)", + "location": "Logs::File::open", + "validated_by": "stream->good()", + "validates": [ + "file stream is valid after opening" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/Log.cpp.ai.md b/src/libxrpl/basics/Log.cpp.ai.md new file mode 100644 index 0000000000..23ba0c1401 --- /dev/null +++ b/src/libxrpl/basics/Log.cpp.ai.md @@ -0,0 +1,49 @@ +# `src/libxrpl/basics/Log.cpp` — XRPL Logging System Implementation + +## Role in the System + +`Log.cpp` provides the concrete implementation of the XRPL node's logging infrastructure. It bridges the `beast::Journal` abstraction layer — a lightweight, copyable logging front-end — with the actual I/O facilities: an append-mode log file and `stderr`. Every subsystem in `xrpld` obtains a `beast::Journal` by calling `Logs::journal("PartitionName")`, which creates or retrieves a named channel and hands back an object cheap enough to store as a member variable and copy freely. + +## Architecture: Three Collaborating Classes + +### `Logs::Sink` + +This private class is the glue between `beast::Journal::Sink` (the abstract write target) and the `Logs` coordinator. Each named partition owns exactly one `Sink`. When `beast::Journal::Stream` triggers a write, it calls `Sink::write()`, which applies the per-sink severity threshold gate (`level < threshold()`) before delegating to `logs_.write()`. The design keeps the threshold check close to the point of formatted string construction, avoiding unnecessary formatting work. + +`writeAlways()` is the bypass path: it skips the threshold check but otherwise follows the same route. This exists to support administrative overrides where an operator needs a message emitted regardless of the current verbosity configuration. + +The back-reference to `Logs&` is safe by design: `Sink` instances are owned by the `Logs` object itself via the `sinks_` map, so they cannot outlive it. + +### `Logs::File` + +A thin RAII wrapper around `std::ofstream`. Its most important interface is `closeAndReopen()`, which exists specifically to interoperate with Unix log-rotation tools like `logrotate(8)`. When the log daemon renames the active log file, a SIGHUP handler can call `rotate()` on the `Logs` object; the file descriptor is released and reopened at the original path, picking up the freshly created file. + +`File::open()` validates the stream with `stream->good()` before committing the pointer, and `write()`/`writeln()` silently no-op if `m_stream` is null — so calling code never needs to check whether a file is configured before writing. + +### `Logs` (the Coordinator) + +`Logs` owns the sink registry (`sinks_`), the shared file, and the global threshold. The sink map is keyed by partition name using `boost::beast::iless`, making partition names case-insensitive at lookup time. `get()` is the lazily-creating accessor: it acquires the mutex and calls `sinks_.emplace()` — the standard library guarantees this is a no-op if the key already exists, returning the existing iterator, so there is no double-creation risk. + +Setting a new threshold via `threshold(Severity)` holds the mutex and iterates all existing sinks to push the new threshold to each — a global dial. This is the mechanism behind the `logLevel` admin command on a running node. + +`makeSink()` is `virtual`, which is the extension point for testing. A test harness can subclass `Logs` and override `makeSink()` to inject mock sinks without touching file I/O. + +## The `format()` Function — Security Scrubbing + +The most security-sensitive code in the file lives in `Logs::format()`. After assembling the timestamp, partition label, and severity abbreviation (e.g., `"NFO "`, `"WRN "`), it enforces a hard 12 KB cap on total message length and then runs a scrubber lambda over the formatted string. + +The scrubber searches for specific JSON key names — `"seed"`, `"seed_hex"`, `"secret"`, `"master_key"`, `"master_seed"`, `"master_seed_hex"`, `"passphrase"` — and replaces the value between the next pair of double quotes with asterisks. This prevents sensitive wallet credentials from appearing in log files if an RPC request is accidentally logged verbatim. The key names are the exact field names used in XRPL's wallet and key-generation RPC calls (`wallet_propose`, `sign`, etc.), so the list is deliberately narrow and explicit rather than using a general-purpose PII scanner. + +## Severity Translation + +The file maintains a two-way mapping between the older `LogSeverity` enum (deprecated, prefixed `ls`) and `beast::severities::Severity` (prefixed `k`). The `fromSeverity()` and `toSeverity()` static methods exist purely as a compatibility bridge for code that hasn't migrated off the old enum. Both use `UNREACHABLE()` on the default branch — an assertion macro that fires in debug builds — ensuring that any future severity value added to one enum without updating the other fails loudly. + +`fromString()` uses `boost::iequals` for case-insensitive parsing and accepts alias spellings (`"warn"`, `"warnings"`, `"information"`) to be forgiving of operator input from config files or admin commands. + +## Thread Safety + +All mutable state in `Logs` — the `sinks_` map, the threshold, and the file writes — is protected by a single `mutable mutex_`. This means `partition_severities()` (a const query) still takes the lock. The trade-off is simplicity over granularity: log writes from multiple threads serialize through one mutex, but since logging is never on the hot path for consensus or transaction processing, this is acceptable. The `Logs::File` methods are explicitly documented as *not* thread-safe; they are called only while the mutex is already held. + +## The `DebugSink` / `debugLog()` Facility + +The file-local `DebugSink` class wraps a `beast::Journal::Sink` reference behind a mutex, defaulting to the null sink. `setDebugLogSink()` atomically swaps in a new sink and returns the old one — a clean ownership-transfer idiom using `std::swap` on `unique_ptr`. This global injectable debug journal lets tests redirect debug output without any changes to production code paths. The header explicitly warns that `debugLog()` may write to a null sink, making it unsuitable for anything that must be observed. \ No newline at end of file diff --git a/src/libxrpl/basics/MallocTrim.cpp.ai.json b/src/libxrpl/basics/MallocTrim.cpp.ai.json new file mode 100644 index 0000000000..2a5d2f3dab --- /dev/null +++ b/src/libxrpl/basics/MallocTrim.cpp.ai.json @@ -0,0 +1,301 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "mallocTrim", + "readFile (lambda)", + "detail::parseStatmRSSkB", + "detail::mallocTrimWithPad", + "getRusageThread" + ], + "entry_point": "mallocTrim", + "purpose": "Performs a memory trim operation, collects before/after memory usage and page fault stats, and logs/report results.", + "validation_points": [ + "readFile: Validates statm file open with ifs.is_open()", + "detail::parseStatmRSSkB: Validates statm file format with istringstream >> long", + "detail::parseStatmRSSkB: Validates page size with sysconf(_SC_PAGESIZE)", + "getRusageThread: Validates getrusage success by return value" + ] + } + ], + "data_flows": [ + { + "field": "statm file contents", + "flow": [ + "readFile", + "detail::parseStatmRSSkB" + ], + "origin": "readFile('/proc/self/statm')", + "transformations": [ + "File is opened and read as string", + "String is parsed into fields using istringstream", + "Second field (resident pages) is extracted and converted to kB" + ], + "validated_at": "readFile (file open), detail::parseStatmRSSkB (format and page size)" + }, + { + "field": "resident set size (rssBeforeKB, rssAfterKB)", + "flow": [ + "parseStatmRSSkB", + "assigned to report.rssBeforeKB / report.rssAfterKB" + ], + "origin": "detail::parseStatmRSSkB output", + "transformations": [ + "Extracted from statm string, converted from pages to kB" + ], + "validated_at": "parseStatmRSSkB (format and page size)" + }, + { + "field": "struct rusage ru0 / ru1", + "flow": [ + "getRusageThread", + "ru0/ru1 fields used for minfltDelta/majfltDelta" + ], + "origin": "getRusageThread", + "transformations": [ + "getrusage fills struct rusage", + "ru_minflt and ru_majflt deltas computed" + ], + "validated_at": "getRusageThread (return value checked)" + }, + { + "field": "report.trimResult", + "flow": [ + "mallocTrimWithPad", + "assigned to report.trimResult" + ], + "origin": "detail::mallocTrimWithPad", + "transformations": [ + "Result of ::malloc_trim syscall" + ], + "validated_at": "No explicit validation, but only called on supported platforms" + } + ], + "description": "Implements memory trimming and reporting utilities for Linux/glibc platforms, including instrumentation for memory usage and page faults before and after malloc_trim, with logging support.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "statm file open", + "empty", + "string", + "validation" + ], + "evidence": "std::ifstream::is_open() at lambda readFile in mallocTrim", + "issue_pattern": "Missing empty string validation for statm file open", + "why_false_positive": "std::ifstream::is_open() validates statm file open for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "statm file format", + "empty", + "string", + "validation" + ], + "evidence": "std::istringstream >> long at detail::parseStatmRSSkB", + "issue_pattern": "Missing empty string validation for statm file format", + "why_false_positive": "std::istringstream >> long validates statm file format for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "statm file format", + "format", + "validation", + "invalid" + ], + "evidence": "std::istringstream >> long at detail::parseStatmRSSkB", + "issue_pattern": "Missing format validation for statm file format", + "why_false_positive": "std::istringstream >> long validates statm file format format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "page size", + "empty", + "string", + "validation" + ], + "evidence": "sysconf(_SC_PAGESIZE) at detail::parseStatmRSSkB", + "issue_pattern": "Missing empty string validation for page size", + "why_false_positive": "sysconf(_SC_PAGESIZE) validates page size for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "page size", + "range", + "bounds", + "validation" + ], + "evidence": "sysconf(_SC_PAGESIZE) at detail::parseStatmRSSkB", + "issue_pattern": "Missing range validation for page size", + "why_false_positive": "sysconf(_SC_PAGESIZE) validates page size range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "getrusage success", + "empty", + "string", + "validation" + ], + "evidence": "getrusage return value at getRusageThread", + "issue_pattern": "Missing empty string validation for getrusage success", + "why_false_positive": "getrusage return value validates getrusage success for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "file handle", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::ifstream", + "issue_pattern": "Missing file handle cleanup", + "why_false_positive": "std::ifstream provides automatic file handle cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/MallocTrim.cpp", + "functions": [ + { + "args": [ + "ru" + ], + "lineno": 18, + "name": "getRusageThread" + }, + { + "args": [ + "padBytes" + ], + "lineno": 38, + "name": "mallocTrimWithPad" + }, + { + "args": [ + "statm" + ], + "lineno": 42, + "name": "parseStatmRSSkB" + }, + { + "args": [ + "tag", + "journal" + ], + "lineno": 56, + "name": "mallocTrim" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic file handle cleanup", + "false_positive_risk": "Missing file handle cleanup", + "resource": "file handle", + "type": "raii_wrapper", + "wrapper_type": "std::ifstream" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 33, + "name": "xrpl" + }, + { + "lineno": 35, + "name": "detail" + } + ], + "test_coverage_notes": "This code is platform-specific (Linux/glibc only). The main entry point is mallocTrim. There is no evidence in this file of direct unit tests or test hooks. The validation code (file open, statm parsing, sysconf, getrusage) is not covered by LCOV (see LCOV_EXCL_LINE/START), suggesting these paths are not exercised by automated tests. Integration or system tests may exist elsewhere, but direct unit test coverage for error/validation paths is likely missing.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Standard C++/POSIX (no custom validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns empty string", + "field": "statm file open", + "location": "lambda readFile in mallocTrim", + "validated_by": "std::ifstream::is_open()", + "validates": [ + "Checks if /proc/self/statm can be opened before reading" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns -1", + "field": "statm file format", + "location": "detail::parseStatmRSSkB", + "validated_by": "std::istringstream >> long", + "validates": [ + "Checks if statm string can be parsed into two long integers (size, resident)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns -1", + "field": "page size", + "location": "detail::parseStatmRSSkB", + "validated_by": "sysconf(_SC_PAGESIZE)", + "validates": [ + "Checks if page size is positive" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "returns false", + "field": "getrusage success", + "location": "getRusageThread", + "validated_by": "getrusage return value", + "validates": [ + "Checks if getrusage call for RUSAGE_THREAD succeeds" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/MallocTrim.cpp.ai.md b/src/libxrpl/basics/MallocTrim.cpp.ai.md new file mode 100644 index 0000000000..9e949b7e56 --- /dev/null +++ b/src/libxrpl/basics/MallocTrim.cpp.ai.md @@ -0,0 +1,39 @@ +# `src/libxrpl/basics/MallocTrim.cpp` + +## Purpose and Context + +Long-running server processes like rippled accumulate fragmented free memory inside glibc's ptmalloc arena — pages that have been freed by the application but not yet returned to the kernel. The OS still charges this memory against the process's Resident Set Size (RSS), making the process appear to consume far more memory than it actually needs. `MallocTrim.cpp` implements a targeted call to `::malloc_trim(0)` that explicitly requests glibc to consolidate its arenas and return any eligible free pages back to the OS. The public entry point `mallocTrim()` is called from `Application::doSweep()`, the periodic ledger-maintenance sweep that runs after cache evictions and other operations that free significant amounts of memory. + +## Platform Gating + +The entire implementation is guarded by `#if defined(__GLIBC__) && BOOST_OS_LINUX`. On any other platform — macOS, Windows, or Linux with an alternative allocator preloaded — the function is a documented no-op that returns a `MallocTrimReport` with `supported = false`. The compile-time `#error` directive for `RUSAGE_THREAD` enforces an additional invariant: Linux/glibc builds that somehow lack per-thread resource accounting are rejected outright rather than silently degrading. This is a deliberately hard boundary; thread-scoped page-fault measurement is non-negotiable to the diagnostic design. + +## The `TRIM_PAD = 0` Decision + +`malloc_trim(pad)` accepts a padding argument that leaves `pad` bytes of free space unreleased in the arena, potentially reducing future syscall overhead on re-allocation. The constant `TRIM_PAD` is hardcoded to `0` with an inline comment noting that 12-hour Mainnet testing across four candidate values (0, 256 KB, 1 MB, 16 MB) showed no consistent RSS benefit from non-zero padding. Setting it to zero avoids introducing an opaque tuning surface while achieving the best observed balance of reclamation and latency stability. The comment is an intentional data trail for future maintainers who might revisit the decision. + +## Two-Mode Execution + +`mallocTrim()` branches on whether `journal.debug()` logging is active: + +**Without debug logging**: The code executes a bare `detail::mallocTrimWithPad(0)` — a single-line wrapper around `::malloc_trim`. No `/proc` I/O, no clock calls, no `getrusage` overhead. This is the production hot path. + +**With debug logging**: The function becomes a full instrumentation harness. Before calling `malloc_trim`, it reads `/proc/self/statm` as a raw string and calls `detail::parseStatmRSSkB()` to extract the resident-page count (the second whitespace-delimited field), converting pages to kilobytes via `sysconf(_SC_PAGESIZE)`. It also captures a `RUSAGE_THREAD` snapshot via `getrusage()`. After the trim call, it reads `/proc/self/statm` again and takes a second `getrusage` snapshot, then logs the before/after RSS, the RSS delta in KB, the trim duration in microseconds (via `std::chrono::steady_clock`), and the minor/major page-fault deltas. All of this is gated at runtime on debug logging being enabled, so operators can enable diagnostics without recompilation. + +## Defensive Data Handling + +Each piece of external data is treated skeptically: + +- `readFile()` checks `ifs.is_open()` before reading, returning an empty string on failure rather than throwing. +- `parseStatmRSSkB()` uses `std::istringstream` extraction that sets `failbit` on malformed input, returning `-1` on parse failure. It also validates that `sysconf(_SC_PAGESIZE)` returns a positive value before performing the page-to-KB multiplication. +- `getRusageThread()` checks the `getrusage` return value. The boolean `have_ru0` / `have_ru1` flags gate the page-fault delta computation, so a failed `getrusage` call simply leaves those fields at their default `-1` sentinel values rather than producing garbage deltas. + +The `MallocTrimReport` struct uses `-1` as the sentinel for "not available" across all numeric fields, and `deltaKB()` returns `0` rather than a misleading negative number when either RSS reading failed. + +## Relationship to the Header + +`MallocTrimReport` is defined entirely in `MallocTrim.h`. Notably, the struct's `deltaKB()` convenience method is `[[nodiscard]]` and `noexcept`, making it safe to call in performance-sensitive paths and impossible to silently discard the return value. The header also carries an explicit allocator-interaction note warning that alternative allocators (jemalloc, tcmalloc) make this call a no-op on the active heap, and that mmap-backed large allocations are already returned to the OS on `free()` regardless — scoping the promise the facility actually makes. + +## LCOV Exclusions + +The entire `mallocTrim()` body and the `getRusageThread()` helper are excluded from code-coverage analysis via `LCOV_EXCL_START` / `LCOV_EXCL_STOP` and `LCOV_EXCL_LINE`. The tests in `src/tests/libxrpl/basics/MallocTrim.cpp` exercise the public interface but the underlying platform syscalls are inherently environment-dependent and produce unreliable coverage numbers in CI, so they are intentionally excluded rather than obscuring the overall coverage metric. \ No newline at end of file diff --git a/src/libxrpl/basics/Number.cpp.ai.json b/src/libxrpl/basics/Number.cpp.ai.json new file mode 100644 index 0000000000..e489ebf774 --- /dev/null +++ b/src/libxrpl/basics/Number.cpp.ai.json @@ -0,0 +1,477 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 54, + "name": "Number::Guard" + } + ], + "code_paths": [ + { + "call_chain": [ + "Number::setMantissaScale" + ], + "entry_point": "Number::setMantissaScale", + "purpose": "Sets the mantissa scale for Number operations, switching between small and large ranges.", + "validation_points": [ + "Number::setMantissaScale: Validates that 'scale' is either MantissaRange::small or MantissaRange::large. Throws LogicError otherwise." + ] + }, + { + "call_chain": [ + "Number::getMantissaScale" + ], + "entry_point": "Number::getMantissaScale", + "purpose": "Retrieves the current mantissa scale from the thread-local range_ reference.", + "validation_points": [] + }, + { + "call_chain": [ + "Number::Guard::push", + "Number::Guard::doPush" + ], + "entry_point": "Number::Guard::push", + "purpose": "Adds a digit to the guard digits for precision/rounding purposes.", + "validation_points": [] + }, + { + "call_chain": [ + "Number::Guard::pop" + ], + "entry_point": "Number::Guard::pop", + "purpose": "Removes a digit from the guard digits.", + "validation_points": [] + }, + { + "call_chain": [ + "Number::Guard::doRoundUp" + ], + "entry_point": "Number::Guard::doRoundUp", + "purpose": "Rounds up the mantissa if required, adjusting sign, mantissa, and exponent.", + "validation_points": [] + }, + { + "call_chain": [ + "Number::Guard::doRoundDown" + ], + "entry_point": "Number::Guard::doRoundDown", + "purpose": "Rounds down the mantissa if required.", + "validation_points": [] + }, + { + "call_chain": [ + "Number::Guard::doRound" + ], + "entry_point": "Number::Guard::doRound", + "purpose": "Rounds the value in-place based on guard digits.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "MantissaRange::mantissa_scale scale", + "flow": [ + "Input to Number::setMantissaScale", + "Validated against MantissaRange::small and MantissaRange::large", + "Used to set thread_local Number::range_ to smallRange or largeRange" + ], + "origin": "Input parameter to Number::setMantissaScale", + "transformations": [ + "Checked for valid enum value", + "Used to select reference to smallRange or largeRange" + ], + "validated_at": "Number::setMantissaScale" + }, + { + "field": "Number::range_ (thread_local)", + "flow": [ + "Initialized to largeRange", + "Set by Number::setMantissaScale", + "Read by Number::getMantissaScale" + ], + "origin": "Initialized to largeRange at thread start", + "transformations": [ + "Reference wrapper updated to point to smallRange or largeRange" + ], + "validated_at": "Indirectly validated via setMantissaScale" + }, + { + "field": "Number::Guard::digits_", + "flow": [ + "Guard constructed", + "Digits pushed via Guard::push (calls doPush)", + "Digits popped via Guard::pop" + ], + "origin": "Default-initialized to 0 in Guard constructor", + "transformations": [ + "Digits shifted and masked in doPush and pop" + ], + "validated_at": "No explicit validation" + }, + { + "field": "Number::Guard::sbit_", + "flow": [ + "Set via set_positive or set_negative", + "Read via is_negative" + ], + "origin": "Default-initialized to 0 in Guard constructor", + "transformations": [ + "Set to 0 or 1" + ], + "validated_at": "No explicit validation" + } + ], + "description": "Implements arbitrary-precision decimal number arithmetic for the XRPL project, including normalization, rounding, arithmetic operations, and conversion to string, with support for different mantissa ranges and rounding modes.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "scale (MantissaRange::mantissa_scale)", + "empty", + "string", + "validation" + ], + "evidence": "if statement and LogicError exception at Number::setMantissaScale", + "issue_pattern": "Missing empty string validation for scale (MantissaRange::mantissa_scale)", + "why_false_positive": "if statement and LogicError exception validates scale (MantissaRange::mantissa_scale) for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/Number.cpp", + "functions": [ + { + "args": [], + "lineno": 23, + "name": "Number::getround" + }, + { + "args": [ + "mode" + ], + "lineno": 28, + "name": "Number::setround" + }, + { + "args": [], + "lineno": 33, + "name": "Number::getMantissaScale" + }, + { + "args": [ + "scale" + ], + "lineno": 37, + "name": "Number::setMantissaScale" + }, + { + "args": [], + "lineno": 67, + "name": "Number::Guard::set_positive" + }, + { + "args": [], + "lineno": 72, + "name": "Number::Guard::set_negative" + }, + { + "args": [], + "lineno": 77, + "name": "Number::Guard::is_negative" + }, + { + "args": [ + "d" + ], + "lineno": 82, + "name": "Number::Guard::doPush" + }, + { + "args": [ + "d" + ], + "lineno": 89, + "name": "Number::Guard::push" + }, + { + "args": [], + "lineno": 94, + "name": "Number::Guard::pop" + }, + { + "args": [], + "lineno": 101, + "name": "Number::Guard::round" + }, + { + "args": [ + "negative", + "mantissa", + "exponent", + "minMantissa" + ], + "lineno": 120, + "name": "Number::Guard::bringIntoRange" + }, + { + "args": [ + "negative", + "mantissa", + "exponent", + "minMantissa", + "maxMantissa", + "location" + ], + "lineno": 137, + "name": "Number::Guard::doRoundUp" + }, + { + "args": [ + "negative", + "mantissa", + "exponent", + "minMantissa" + ], + "lineno": 157, + "name": "Number::Guard::doRoundDown" + }, + { + "args": [ + "drops", + "location" + ], + "lineno": 172, + "name": "Number::Guard::doRound" + }, + { + "args": [ + "mantissa" + ], + "lineno": 192, + "name": "Number::externalToInternal" + }, + { + "args": [], + "lineno": 206, + "name": "Number::oneSmall" + }, + { + "args": [], + "lineno": 211, + "name": "Number::oneLarge" + }, + { + "args": [], + "lineno": 216, + "name": "Number::one" + }, + { + "args": [ + "negative", + "mantissa_", + "exponent_", + "minMantissa", + "maxMantissa" + ], + "lineno": 227, + "name": "doNormalize" + }, + { + "args": [ + "negative", + "mantissa", + "exponent", + "minMantissa", + "maxMantissa" + ], + "lineno": 282, + "name": "Number::normalize" + }, + { + "args": [ + "negative", + "mantissa", + "exponent", + "minMantissa", + "maxMantissa" + ], + "lineno": 289, + "name": "Number::normalize" + }, + { + "args": [ + "negative", + "mantissa", + "exponent", + "minMantissa", + "maxMantissa" + ], + "lineno": 296, + "name": "Number::normalize" + }, + { + "args": [], + "lineno": 303, + "name": "Number::normalize" + }, + { + "args": [ + "exponentDelta" + ], + "lineno": 314, + "name": "Number::shiftExponent" + }, + { + "args": [ + "y" + ], + "lineno": 329, + "name": "Number::operator+=" + }, + { + "args": [ + "u" + ], + "lineno": 389, + "name": "divu10" + }, + { + "args": [ + "y" + ], + "lineno": 404, + "name": "Number::operator*=" + }, + { + "args": [ + "y" + ], + "lineno": 448, + "name": "Number::operator/=" + }, + { + "args": [], + "lineno": 507, + "name": "Number::operator rep" + }, + { + "args": [], + "lineno": 534, + "name": "Number::truncate" + }, + { + "args": [ + "amount" + ], + "lineno": 548, + "name": "to_string" + }, + { + "args": [ + "f", + "n" + ], + "lineno": 601, + "name": "power" + }, + { + "args": [ + "f", + "d" + ], + "lineno": 617, + "name": "root" + }, + { + "args": [ + "f" + ], + "lineno": 678, + "name": "root2" + }, + { + "args": [ + "f", + "n", + "d" + ], + "lineno": 719, + "name": "power" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + } + ], + "test_coverage_notes": "The core validation (mantissa scale) is only checked in Number::setMantissaScale. Test coverage would require unit tests that call setMantissaScale with valid and invalid values to ensure LogicError is thrown as expected. There is no evidence in this file of test hooks or test-specific code. Tests would likely be in a file such as test_basics_Number.cpp or similar in the test suite. Guard logic (push/pop/round) is not validated for input range, so tests should cover edge cases for digit manipulation and rounding, but this is not enforced by validation code. Gaps: No validation for Guard digit values, no explicit tests referenced in this file, and no coverage for thread_local behavior or exception propagation.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LogicError", + "field": "scale (MantissaRange::mantissa_scale)", + "location": "Number::setMantissaScale", + "validated_by": "if statement and LogicError exception", + "validates": [ + "Checks that scale is either MantissaRange::small or MantissaRange::large", + "Rejects any unknown or invalid scale values" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/Number.cpp.ai.md b/src/libxrpl/basics/Number.cpp.ai.md new file mode 100644 index 0000000000..27b4311ef8 --- /dev/null +++ b/src/libxrpl/basics/Number.cpp.ai.md @@ -0,0 +1,110 @@ +# `src/libxrpl/basics/Number.cpp` + +## Role and Purpose + +`Number.cpp` provides the complete arithmetic implementation of `xrpl::Number`, the XRPL codebase's custom fixed-precision decimal floating-point type. `Number` was introduced to replace the ad-hoc arithmetic previously embedded in `STAmount` and related types, giving the ledger a single, auditable, and precisely-rounded numeric type capable of representing all asset classes — IOU amounts, XRP drops, and MPT (Multi-Purpose Token) amounts — with correct, amendment-controlled precision. + +The file is dense with deliberate design decisions driven by two overriding constraints: **exact decimal rounding** (to match on-ledger determinism requirements) and **support for two different mantissa precisions** selected at runtime based on active amendments. + +--- + +## Thread-Local State + +Two `thread_local` variables own all per-thread numeric behavior: + +```cpp +thread_local Number::rounding_mode Number::mode_ = Number::to_nearest; +thread_local std::reference_wrapper Number::range_ = largeRange; +``` + +`mode_` selects from four IEEE-754-style rounding modes (`to_nearest`, `towards_zero`, `downward`, `upward`). `range_` is a `std::reference_wrapper` pointing to either the `smallRange` or `largeRange` static constants — using a reference wrapper rather than copying avoids accidental mutation of the range values and makes reassignment a pointer swap. Callers change the active range through `setMantissaScale()`, which validates the input and updates the wrapper to point at the appropriate constant. + +The thread-local design is critical: XRPL processes transactions in parallel across worker threads. Each thread must have its own rounding mode and range so that amendment-gate changes at transaction start don't race with arithmetic in progress. + +--- + +## Dual Mantissa Ranges + +`MantissaRange` encodes two normalization regimes: + +- **`small`**: min = 10¹⁵, max = 10¹⁶ − 1 (16 significant decimal digits). This matches the legacy `STAmount` IOU representation and is used when neither `SingleAssetVault` nor `LendingProtocol` amendments are active. +- **`large`**: min = 10¹⁸, max = 10¹⁹ − 1 (19 significant decimal digits). This covers the full `int64_t` positive range (2⁶³ − 1 ≈ 9.2 × 10¹⁸), enabling precise integer representation of XRP drops and MPT amounts. 10¹⁹ − 1 is the largest value of the form 10^k − 1 that fits in an unsigned 64-bit integer, which is why this is the maximum. + +The `large` range's max (10¹⁹ − 1) intentionally exceeds `INT64_MAX` (≈ 9.22 × 10¹⁸). This is handled carefully: internal storage uses `uint64_t`, but the external `mantissa()` method divides by 10 before returning if the internal value exceeds `maxRep`, adjusting the exponent accordingly. This preserves the invariant that external callers always see a signed 63-bit mantissa. + +--- + +## The `Guard` Class + +`Guard` is an internal, file-local class that implements **guard digits** — extra decimal digits of precision kept during multi-step calculations so the final result can be correctly rounded without accumulating error. It stores 16 guard digits packed into a `uint64_t` as 4-bit BCD nibbles, plus an `xbit_` sticky bit that records whether any non-zero digit was ever shifted off the bottom. + +The `push(d)` / `pop()` protocol captures digits that fall out of the main mantissa during normalization: + +- `doPush(d)` shifts `digits_` right by 4 bits (losing the least significant nibble into `xbit_` if non-zero) and places the new digit at the top. +- `pop()` shifts `digits_` left, recovering the most significant guard digit. + +The `round()` method consults the current thread-local rounding mode to decide whether to round up (+1), round down (−1), or tie-break to even (0). For `to_nearest` it compares the packed guard value against `0x5000'0000'0000'0000` — exactly half, in the 4-bit-per-digit encoding — using `xbit_` to break ties toward "more than half." + +`doRoundUp` and `doRoundDown` apply the rounding decision by incrementing or decrementing the mantissa, then renormalizing: if incrementing pushes the mantissa beyond `maxMantissa`, the mantissa is divided by 10 and the exponent is incremented; if decrementing drops it below `minMantissa`, the mantissa is multiplied by 10 and the exponent is decremented. Overflow beyond `maxExponent` throws `std::overflow_error`. + +`doRound(rep& drops)` is a separate entry point used when converting a `Number` to integer `rep` — it rounds the already-scaled integer value and applies the sign. + +--- + +## Normalization (`doNormalize`) + +The free function `doNormalize` (a friend, called through the templated `Number::normalize` specializations) brings any `(negative, mantissa, exponent)` triple into canonical form for a given range: + +1. If the mantissa is zero, the result is canonical zero. +2. While the mantissa is below `minMantissa` and the exponent is above `minExponent`, multiply the mantissa by 10 and decrement the exponent. +3. While the mantissa exceeds `maxMantissa` or exceeds `maxRep`, push the last digit into the `Guard` and divide by 10. +4. A special extra division handles the case where the intermediate value exceeds `INT64_MAX` but the final stored value must not (relevant for the large range when the mantissa is, say, 9.9 × 10¹⁸). +5. `doRoundUp` is then called with the guard digits to produce the correctly-rounded result. + +The function is templated over the mantissa type (`uint128_t`, `unsigned long long`, `unsigned long`) to handle multiplication products in `operator*=` that temporarily require 128 bits. + +--- + +## Arithmetic Operators + +**Addition (`operator+=`)**: Aligns the two operands to the same exponent by repeatedly dividing the operand with the smaller exponent by 10, pushing guard digits. If both have the same sign, their mantissas are added (using `uint128_t` to avoid overflow) and the result is rounded up. If they have opposite signs, the smaller is subtracted from the larger, and digits recovered from the guard via `pop()` refine the result, followed by `doRoundDown`. + +**Multiplication (`operator*=`)**: Computes the product of the two mantissas as `uint128_t`, sums the exponents, and trims the product into range using `divu10` — a Hacker's Delight-derived routine that avoids the expensive native 128-bit modulo operation by approximating division by 10 with bit shifts and a correction step. + +**Division (`operator/=`)**: Pre-scales the numerator by a power of 10 (10¹⁷ for the small range, 10¹⁹ for the large range) to preserve precision through integer division by the denominator. For the large range, a further 1000× correction term using the division remainder is computed separately (because the combined scale factor would overflow `uint128_t`) and added before the final normalization. Division by zero throws `std::overflow_error`. + +--- + +## Integer Conversion and `truncate` + +`operator rep()` converts a `Number` to `int64_t` by scaling the mantissa to an integer via repeated division (pushing guard digits) or multiplication (checking for overflow), then calling `doRound` with the accumulated guard. This is the primary way XRP drop values are materialized from `Number` arithmetic. + +`truncate()` removes the fractional part without rounding — it divides the mantissa and increments the exponent until the exponent is non-negative, then renormalizes. + +--- + +## `to_string` Formatting + +`to_string(Number)` produces human-readable decimal output. For exponents far from zero it falls back to scientific notation (`Me+E`). For normal ranges it constructs a padded string representation of the integer mantissa, then uses pointer arithmetic to locate the decimal point position and strip leading and trailing zeros. The approach avoids repeated character-by-character operations by exploiting the fixed-length padding. + +--- + +## Power and Root Functions + +`power(f, n)` uses binary exponentiation (log₂(n) multiplications) to compute f^n. + +`root(f, d)` computes f^(1/d) via Newton–Raphson iteration. To ensure rapid convergence regardless of scale, it first brings `f` into the range (0, 1) by subtracting a multiple of d from the exponent (Euclidean remainder ensures the exponent shift is a multiple of d, so the root's exponent is exact). A quadratic least-squares curve fit provides the initial guess `r` for the iteration `r ← ((d−1)r + f/r^(d−1)) / d`, which continues until the result stabilizes or oscillates between two values (a cycle-of-2 detector prevents infinite loops near the rounding boundary). The result's exponent is then shifted back by `e/d`. + +`root2(f)` is a specialized, faster square root using the same pattern with hardcoded coefficients for d=2. + +`power(f, n, d)` composes the above: it reduces n/d by GCD, applies `power(f, n)` then `root(result, d)`, with appropriate handling for zero, one, and infinity corner cases consistent with IEEE 754 Annex F. + +--- + +## Key Design Tradeoffs + +**Separate `Guard` digits vs. extended mantissa**: Rather than storing a wider mantissa, the `Guard` is a thin wrapper around a single `uint64_t` BCD integer. This keeps `Number` objects small (one `bool`, one `uint64_t`, one `int`) while providing sufficient precision for correct rounding in all operations. + +**`externalToInternal` UB avoidance**: Converting `INT64_MIN` to its absolute value as a `uint64_t` is undefined behavior in C++ when done via negation of an `int64_t`. `externalToInternal` routes through `int128_t` for the one edge case where the value does not fit in the positive range of `int64_t`. + +**Amendment-gated precision via thread-local `range_`**: Rather than making `Number` a compile-time template over its precision, the range is a runtime, per-thread switch. This allows the same `Number` code to participate in both amendment-gated (small range) and post-amendment (large range) transaction contexts without code duplication or binary explosion. The cost is a thread-local dereference on every normalization, which is negligible compared to the arithmetic itself. \ No newline at end of file diff --git a/src/libxrpl/basics/ResolverAsio.cpp.ai.json b/src/libxrpl/basics/ResolverAsio.cpp.ai.json new file mode 100644 index 0000000000..b011e05c7b --- /dev/null +++ b/src/libxrpl/basics/ResolverAsio.cpp.ai.json @@ -0,0 +1,425 @@ +{ + "args": [ + { + "lineno": 18, + "name": "Derived" + }, + { + "lineno": 34, + "name": "owner" + }, + { + "lineno": 38, + "name": "other" + }, + { + "lineno": 297, + "name": "io_context" + }, + { + "lineno": 297, + "name": "journal" + }, + { + "lineno": 94, + "name": "names_" + }, + { + "lineno": 94, + "name": "handler_" + }, + { + "lineno": 151, + "name": "names" + }, + { + "lineno": 151, + "name": "handler" + }, + { + "lineno": 165, + "name": "CompletionCounter" + }, + { + "lineno": 174, + "name": "name" + }, + { + "lineno": 175, + "name": "ec" + }, + { + "lineno": 176, + "name": "results" + }, + { + "lineno": 200, + "name": "str" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "AsyncObject" + }, + { + "args": [ + "owner" + ], + "lineno": 33, + "name": "AsyncObject::CompletionCounter" + }, + { + "args": [ + "io_context", + "journal" + ], + "lineno": 75, + "name": "ResolverAsioImpl" + }, + { + "args": [ + "names_", + "handler_" + ], + "lineno": 92, + "name": "ResolverAsioImpl::Work" + } + ], + "code_paths": [ + { + "call_chain": [ + "ResolverAsioImpl::~ResolverAsioImpl", + "AsyncObject::~AsyncObject" + ], + "entry_point": "ResolverAsioImpl::~ResolverAsioImpl", + "purpose": "Destructor chain to ensure all async work and references are cleaned up before destruction.", + "validation_points": [ + "AsyncObject::~AsyncObject: XRPL_ASSERT(m_pending.load() == 0, ...)", + "ResolverAsioImpl::~ResolverAsioImpl: XRPL_ASSERT(m_work.empty(), ...), XRPL_ASSERT(m_stopped, ...)" + ] + }, + { + "call_chain": [ + "AsyncObject::CompletionCounter::CompletionCounter", + "AsyncObject::CompletionCounter::~CompletionCounter", + "Derived::asyncHandlersComplete" + ], + "entry_point": "AsyncObject::CompletionCounter (RAII)", + "purpose": "Tracks the number of outstanding async handlers. When the last handler completes, triggers asyncHandlersComplete.", + "validation_points": [ + "AsyncObject::CompletionCounter::~CompletionCounter: if (--m_owner->m_pending == 0) m_owner->asyncHandlersComplete()" + ] + }, + { + "call_chain": [ + "AsyncObject::addReference", + "AsyncObject::removeReference", + "Derived::asyncHandlersComplete" + ], + "entry_point": "AsyncObject::addReference / removeReference", + "purpose": "Manual reference counting for async operations, triggers completion when count reaches zero.", + "validation_points": [ + "AsyncObject::removeReference: if (--m_pending == 0) asyncHandlersComplete()" + ] + } + ], + "data_flows": [ + { + "field": "m_pending", + "flow": [ + "AsyncObject constructor", + "Incremented in CompletionCounter::CompletionCounter and addReference", + "Decremented in CompletionCounter::~CompletionCounter and removeReference", + "Checked in AsyncObject::~AsyncObject and in CompletionCounter::~CompletionCounter/removeReference" + ], + "origin": "AsyncObject constructor (initialized to 0)", + "transformations": [ + "Incremented for each async handler or reference", + "Decremented when handler completes or reference is removed" + ], + "validated_at": "AsyncObject::~AsyncObject (XRPL_ASSERT(m_pending.load() == 0, ...))" + }, + { + "field": "m_work", + "flow": [ + "ResolverAsioImpl constructor", + "Work items pushed in resolve/start (not shown in snippet)", + "Work items processed by async handlers", + "Checked in ResolverAsioImpl::~ResolverAsioImpl" + ], + "origin": "ResolverAsioImpl constructor (empty deque)", + "transformations": [ + "Work items added and removed as async operations are scheduled and completed" + ], + "validated_at": "ResolverAsioImpl::~ResolverAsioImpl (XRPL_ASSERT(m_work.empty(), ...))" + }, + { + "field": "m_stopped", + "flow": [ + "ResolverAsioImpl constructor", + "Set/reset in start/stop/do_stop (not shown in snippet)", + "Checked in ResolverAsioImpl::~ResolverAsioImpl" + ], + "origin": "ResolverAsioImpl constructor (set to true)", + "transformations": [ + "Set to false when starting, true when stopped" + ], + "validated_at": "ResolverAsioImpl::~ResolverAsioImpl (XRPL_ASSERT(m_stopped, ...))" + } + ], + "description": "This file implements an asynchronous DNS resolver for the XRPL project using Boost.Asio, providing asynchronous name resolution with work queuing, handler tracking, and clean shutdown semantics.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "m_pending (pending I/O count)", + "validation", + "missing", + "check" + ], + "evidence": "Field m_pending (pending I/O count) validated by XRPL_ASSERT (custom assertion macro)", + "issue_pattern": "Missing validation for m_pending (pending I/O count)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro) validates m_pending (pending I/O count) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_pending (pending I/O count)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at AsyncObject::~AsyncObject (destructor)", + "issue_pattern": "Missing empty string validation for m_pending (pending I/O count)", + "why_false_positive": "XRPL_ASSERT macro validates m_pending (pending I/O count) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/ResolverAsio.cpp", + "functions": [ + { + "args": [], + "lineno": 61, + "name": "AsyncObject::addReference" + }, + { + "args": [], + "lineno": 65, + "name": "AsyncObject::removeReference" + }, + { + "args": [], + "lineno": 108, + "name": "ResolverAsioImpl::asyncHandlersComplete" + }, + { + "args": [], + "lineno": 117, + "name": "ResolverAsioImpl::start" + }, + { + "args": [], + "lineno": 130, + "name": "ResolverAsioImpl::stop_async" + }, + { + "args": [], + "lineno": 140, + "name": "ResolverAsioImpl::stop" + }, + { + "args": [ + "names", + "handler" + ], + "lineno": 151, + "name": "ResolverAsioImpl::resolve" + }, + { + "args": [ + "CompletionCounter" + ], + "lineno": 165, + "name": "ResolverAsioImpl::do_stop" + }, + { + "args": [ + "name", + "ec", + "handler", + "results", + "CompletionCounter" + ], + "lineno": 174, + "name": "ResolverAsioImpl::do_finish" + }, + { + "args": [ + "str" + ], + "lineno": 200, + "name": "ResolverAsioImpl::parseName" + }, + { + "args": [ + "CompletionCounter" + ], + "lineno": 236, + "name": "ResolverAsioImpl::do_work" + }, + { + "args": [ + "names", + "handler", + "CompletionCounter" + ], + "lineno": 273, + "name": "ResolverAsioImpl::do_resolve" + }, + { + "args": [ + "io_context", + "journal" + ], + "lineno": 297, + "name": "ResolverAsio::New" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic (especially XRPL_ASSERT on m_pending, m_work, m_stopped) is primarily checked at destruction time. Typical test coverage would require integration or unit tests that create ResolverAsioImpl, schedule async work, and ensure all work completes before destruction. However, these destructors are only triggered on object deletion, so tests must ensure proper cleanup. There is no evidence in this file of direct test hooks or test-specific code. Test coverage gaps may exist if tests do not exercise abnormal shutdowns (e.g., destruction with pending work or references). Tests likely reside in files testing networking/resolver logic, such as 'test/ResolverAsio_test.cpp' or similar, but coverage of assertion failures (e.g., forced destruction with pending work) may be limited unless explicitly tested for error handling and resource leaks.", + "validation_architecture": { + "auto_validated_fields": [ + "m_pending (pending I/O count)" + ], + "framework": "XRPL_ASSERT (custom assertion macro)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (XRPL_ASSERT)", + "field": "m_pending (pending I/O count)", + "location": "AsyncObject::~AsyncObject (destructor)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that all pending I/O operations are complete (m_pending == 0) before object destruction" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/ResolverAsio.cpp.ai.md b/src/libxrpl/basics/ResolverAsio.cpp.ai.md new file mode 100644 index 0000000000..09e29f9df5 --- /dev/null +++ b/src/libxrpl/basics/ResolverAsio.cpp.ai.md @@ -0,0 +1,33 @@ +# `ResolverAsio.cpp` — Asynchronous DNS Resolution for XRPL + +This file provides XRPL's concrete implementation of asynchronous hostname resolution, `ResolverAsioImpl`, built on top of Boost.Asio's `tcp::resolver`. It sits at the foundation of the peer-discovery and overlay networking subsystems, translating string-form peer addresses (e.g. `r.ripple.com:51235`) into `beast::IP::Endpoint` objects that the rest of the stack can connect to. + +## Design Layers + +The file introduces two independent pieces before assembling them: + +**`AsyncObject`** is a CRTP mix-in that answers one recurring problem in Asio code: knowing when every outstanding async handler has finished so the owning object can safely be destroyed. It maintains an `std::atomic` counter, `m_pending`, and exposes two mechanisms for callers to interact with it. The `CompletionCounter` inner class is the primary one — it increments the counter on construction (including copy construction, which is what happens when Asio copies a bound handler into the queue) and decrements it in the destructor; when the decrement reaches zero it calls `asyncHandlersComplete()` on the derived object. The `addReference`/`removeReference` pair provides the same semantics for cases where a plain RAII object isn't the right fit, as used during `start()`. The destructor asserts `m_pending == 0`, making it impossible to accidentally destroy the object with live handlers. + +**`ResolverAsioImpl`** inherits from both `ResolverAsio` and `AsyncObject`, combining the abstract resolver interface with the lifecycle tracking. All mutable state — the work queue, resolver, and stop flags — is accessed exclusively on a `boost::asio::strand`, so there is no locking for I/O path operations. A `std::mutex` / `std::condition_variable` pair is used only for the one synchronous blocking call, `stop()`. + +## Work Queue and Serial Dispatch + +Callers submit a batch of hostnames with a single callback via `resolve()`, which serializes onto the strand and calls `do_resolve()`. There the batch is wrapped in a `Work` item. Critically, names are stored **reversed** in the internal `std::vector` using `std::reverse_copy`; this makes serving the next name from the front of the logical batch a `pop_back()` — O(1) without any shifting. Work items accumulate in a `std::deque` and are drained sequentially by `do_work()`. + +`do_work()` pulls one name at a time, parses it with `parseName()`, and issues a single `m_resolver.async_resolve()` call. After each resolution completes (via `do_finish()`), a new `do_work()` is posted back to the strand. This serial-one-at-a-time pattern is deliberate: it prevents a flood of simultaneous DNS queries from consuming excessive file descriptors or hitting resolver limits, and keeps back-pressure natural — new work items simply wait in the deque. + +## Name Parsing Strategy + +`parseName()` has a two-tier approach to address ambiguity. It first tries `beast::IP::Endpoint::from_string_checked()`, which correctly handles IPv6 addresses like `[::1]:6006` where a raw colon-split would fail. Only if that returns `nullopt` does it fall back to a generic whitespace-trimmed `host:port` scan using iterators. Whitespace is stripped from both ends before the colon search, and the port separator scan accepts both colons and whitespace, making it tolerant of `"r.ripple.com 51235"` as well as `"r.ripple.com:51235"`. An empty host string after parsing causes the name to be skipped with an error log, but processing of the remaining queue continues. + +## Lifecycle and Shutdown + +The object starts in a *stopped* state (`m_stopped = true`, `m_pending = 0`). Calling `start()` atomically clears the stopped flag and calls `addReference()`, bumping `m_pending` to 1 — this "lifetime reference" keeps `asyncHandlersComplete()` from firing prematurely while the resolver has no active handlers but is still logically running. + +`stop_async()` is idempotent via `m_stop_called.exchange(true)` and posts `do_stop()` to the strand. `do_stop()` clears the work queue, cancels any in-flight Asio resolution (causing pending handlers to be called back with `operation_aborted`), and calls `removeReference()` to drop the lifetime reference. `do_finish()` silently discards `operation_aborted` results, so cancelled resolutions produce no spurious callbacks to user code. + +`stop()` is the synchronous variant: it calls `stop_async()` then waits on a `condition_variable` for `m_asyncHandlersCompleted` to become true. This flag is set in `asyncHandlersComplete()` — the callback that fires when `m_pending` reaches zero — under the mutex, so `stop()` returns only after every `CompletionCounter` has been destroyed, i.e., every Asio handler has returned. The destructor then asserts both `m_work.empty()` and `m_stopped`, providing a hard fail-fast if shutdown is skipped. + +## Factory and Interface + +`ResolverAsio::New()` is the sole construction path, returning a `std::unique_ptr` backed by a `ResolverAsioImpl`. Callers never see the implementation type. The abstract `Resolver` base is pure-virtual with its destructor defined in this translation unit (`Resolver::~Resolver() = default;`), satisfying the ODR requirement for a virtual destructor declared as `= 0` in the header while keeping the vtable anchored to this file. \ No newline at end of file diff --git a/src/libxrpl/basics/StringUtilities.cpp.ai.json b/src/libxrpl/basics/StringUtilities.cpp.ai.json new file mode 100644 index 0000000000..94fc07bdca --- /dev/null +++ b/src/libxrpl/basics/StringUtilities.cpp.ai.json @@ -0,0 +1,382 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "parseUrl" + ], + "entry_point": "parseUrl", + "purpose": "Parses a URL string into its components, validating structure, port, and domain.", + "validation_points": [ + "boost::regex_match(strUrl, reUrl) - validates URL structure", + "beast::IP::Endpoint::from_string_checked(domain) - validates/normalizes domain", + "beast::lexicalCast(port) - validates port is a valid uint16", + "if (pUrl.port == 0) return false; - rejects invalid port" + ] + }, + { + "call_chain": [ + "to_uint64" + ], + "entry_point": "to_uint64", + "purpose": "Converts a string to uint64, validating input is a valid number.", + "validation_points": [ + "beast::lexicalCastChecked(result, s) - validates string is a valid uint64" + ] + }, + { + "call_chain": [ + "isProperlyFormedTomlDomain" + ], + "entry_point": "isProperlyFormedTomlDomain", + "purpose": "Validates a TOML domain string for length and format.", + "validation_points": [ + "domain.size() < 4 || domain.size() > 128 - length validation", + "boost::regex_match(domain, re) - domain format validation" + ] + }, + { + "call_chain": [ + "sqlBlobLiteral" + ], + "entry_point": "sqlBlobLiteral", + "purpose": "Converts a binary blob to a SQL hex literal. No validation.", + "validation_points": [] + }, + { + "call_chain": [ + "trim_whitespace" + ], + "entry_point": "trim_whitespace", + "purpose": "Trims whitespace from a string. No validation.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "strUrl", + "flow": [ + "parseUrl(strUrl)", + "boost::regex_match(strUrl, reUrl)", + "smMatch groups", + "parsedURL fields" + ], + "origin": "Input parameter to parseUrl", + "transformations": [ + "Regex parsing splits strUrl into scheme, username, password, domain, port, path" + ], + "validated_at": "boost::regex_match(strUrl, reUrl)" + }, + { + "field": "domain (from URL)", + "flow": [ + "smMatch[4]", + "beast::IP::Endpoint::from_string_checked(domain)", + "pUrl.domain" + ], + "origin": "Extracted from regex match group in parseUrl", + "transformations": [ + "If domain is IPv6 in brackets, brackets are stripped; otherwise, left as is" + ], + "validated_at": "beast::IP::Endpoint::from_string_checked(domain)" + }, + { + "field": "port", + "flow": [ + "smMatch[5]", + "beast::lexicalCast(port)", + "pUrl.port" + ], + "origin": "Extracted from regex match group in parseUrl", + "transformations": [ + "String port is cast to uint16_t; if invalid or out of range, set to 0" + ], + "validated_at": "beast::lexicalCast(port) and if (pUrl.port == 0)" + }, + { + "field": "s (string to convert to uint64)", + "flow": [ + "to_uint64(s)", + "beast::lexicalCastChecked(result, s)", + "return result or nullopt" + ], + "origin": "Input parameter to to_uint64", + "transformations": [ + "String is parsed to uint64_t if valid" + ], + "validated_at": "beast::lexicalCastChecked(result, s)" + }, + { + "field": "domain (TOML domain string)", + "flow": [ + "isProperlyFormedTomlDomain(domain)", + "length check", + "regex match" + ], + "origin": "Input parameter to isProperlyFormedTomlDomain", + "transformations": [ + "Checked for length, then regex-validated for domain format" + ], + "validated_at": "length check and regex match" + } + ], + "description": "Provides utility functions for string and blob manipulation, URL parsing, whitespace trimming, string-to-uint64 conversion, and domain name validation, primarily for use in the XRPL codebase.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "strUrl (input URL string)", + "empty", + "string", + "validation" + ], + "evidence": "boost::regex_match (with reUrl regex) at parseUrl", + "issue_pattern": "Missing empty string validation for strUrl (input URL string)", + "why_false_positive": "boost::regex_match (with reUrl regex) validates strUrl (input URL string) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "strUrl (input URL string)", + "format", + "validation", + "invalid" + ], + "evidence": "boost::regex_match (with reUrl regex) at parseUrl", + "issue_pattern": "Missing format validation for strUrl (input URL string)", + "why_false_positive": "boost::regex_match (with reUrl regex) validates strUrl (input URL string) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "port (parsed from URL)", + "empty", + "string", + "validation" + ], + "evidence": "beast::lexicalCast at parseUrl", + "issue_pattern": "Missing empty string validation for port (parsed from URL)", + "why_false_positive": "beast::lexicalCast validates port (parsed from URL) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "domain (parsed from URL)", + "empty", + "string", + "validation" + ], + "evidence": "beast::IP::Endpoint::from_string_checked at parseUrl", + "issue_pattern": "Missing empty string validation for domain (parsed from URL)", + "why_false_positive": "beast::IP::Endpoint::from_string_checked validates domain (parsed from URL) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "domain (parsed from URL)", + "format", + "validation", + "invalid" + ], + "evidence": "beast::IP::Endpoint::from_string_checked at parseUrl", + "issue_pattern": "Missing format validation for domain (parsed from URL)", + "why_false_positive": "beast::IP::Endpoint::from_string_checked validates domain (parsed from URL) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "s (string to convert to uint64)", + "empty", + "string", + "validation" + ], + "evidence": "beast::lexicalCastChecked at to_uint64", + "issue_pattern": "Missing empty string validation for s (string to convert to uint64)", + "why_false_positive": "beast::lexicalCastChecked validates s (string to convert to uint64) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "domain (TOML domain string)", + "empty", + "string", + "validation" + ], + "evidence": "explicit length check at isProperlyFormedTomlDomain", + "issue_pattern": "Missing empty string validation for domain (TOML domain string)", + "why_false_positive": "explicit length check validates domain (TOML domain string) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "domain (TOML domain string)", + "range", + "bounds", + "validation" + ], + "evidence": "explicit length check at isProperlyFormedTomlDomain", + "issue_pattern": "Missing range validation for domain (TOML domain string)", + "why_false_positive": "explicit length check validates domain (TOML domain string) range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/StringUtilities.cpp", + "functions": [ + { + "args": [ + "blob" + ], + "lineno": 10, + "name": "sqlBlobLiteral" + }, + { + "args": [ + "pUrl", + "strUrl" + ], + "lineno": 20, + "name": "parseUrl" + }, + { + "args": [ + "str" + ], + "lineno": 67, + "name": "trim_whitespace" + }, + { + "args": [ + "s" + ], + "lineno": 72, + "name": "to_uint64" + }, + { + "args": [ + "domain" + ], + "lineno": 80, + "name": "isProperlyFormedTomlDomain" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is utility code, so direct tests would be in unit test files for StringUtilities or related modules. Typical test files would be 'StringUtilities_test.cpp', 'parseUrl_test.cpp', or similar in the test/unit or test directory. Tests should cover: valid/invalid URLs (parseUrl), edge cases for port and domain, conversion failures for to_uint64, and domain format/length for isProperlyFormedTomlDomain. Gaps may exist if: (1) edge cases for IPv6 domains are not tested, (2) extremely large port numbers are not tested, (3) TOML domain regex edge cases (e.g., IDNs, unicode) are not tested, (4) error handling (exceptions) are not tested. No test code is shown here, so actual coverage must be checked in the repo.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "boost::regex, beast::lexicalCast, explicit checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (no exception); catches all exceptions from regex and returns false", + "field": "strUrl (input URL string)", + "location": "parseUrl", + "validated_by": "boost::regex_match (with reUrl regex)", + "validates": [ + "URL must match scheme://username:password@hostname:port/rest format", + "Scheme must be present and valid", + "Optional userinfo, host, port, and path must be in correct positions" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns false if port is 0 (invalid or out of range)", + "field": "port (parsed from URL)", + "location": "parseUrl", + "validated_by": "beast::lexicalCast", + "validates": [ + "Port must be convertible to uint16_t (0-65535)", + "Port must not be 0 (0 is considered invalid here)" + ], + "validation_type": "type|range" + }, + { + "confidence": 0.8, + "error_thrown": "none (fallback to original domain string if parsing fails)", + "field": "domain (parsed from URL)", + "location": "parseUrl", + "validated_by": "beast::IP::Endpoint::from_string_checked", + "validates": [ + "If domain is an IP address, must be valid (IPv4 or IPv6)", + "IPv6 addresses must be enclosed in brackets" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt if conversion fails", + "field": "s (string to convert to uint64)", + "location": "to_uint64", + "validated_by": "beast::lexicalCastChecked", + "validates": [ + "String must represent a valid uint64_t value" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "returns false if length is out of bounds", + "field": "domain (TOML domain string)", + "location": "isProperlyFormedTomlDomain", + "validated_by": "explicit length check", + "validates": [ + "Domain must be between 4 and 128 characters" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/StringUtilities.cpp.ai.md b/src/libxrpl/basics/StringUtilities.cpp.ai.md new file mode 100644 index 0000000000..5317c90814 --- /dev/null +++ b/src/libxrpl/basics/StringUtilities.cpp.ai.md @@ -0,0 +1,35 @@ +# `src/libxrpl/basics/StringUtilities.cpp` + +This file implements the five free functions declared in `StringUtilities.h` that constitute a focused toolkit for string and binary data manipulation across the XRPL codebase. The functions span three distinct concerns: safe binary-to-SQL encoding, full-featured URI parsing, and lightweight validation helpers. None of them belong to a class; they live directly in the `xrpl` namespace as utilities shared by ledger storage, networking, RPC, and TOML-based validator discovery. + +## `sqlBlobLiteral` — SQLite Hex Encoding + +`sqlBlobLiteral()` converts a `Blob` (`std::vector`) into an SQLite *blob literal* of the form `X'AABBCC...'`. SQLite requires this encoding whenever binary data is embedded directly in a query string. The implementation pre-reserves `(blob.size() * 2) + 3` bytes — exactly the right capacity for the leading `X'`, the hex-encoded body, and the closing `'` — then delegates the actual hex expansion to `boost::algorithm::hex`. The `+3` is an often-missed detail that prevents a reallocation on the final `push_back('\'')`. This function appears in `AcceptedLedgerTx.cpp` and related ledger-persistence paths. + +## `parseUrl` — URI Decomposition + +`parseUrl()` is the most architecturally interesting function in the file. It decomposes a URL string into a `parsedURL` struct (defined in the header) carrying `scheme`, `username`, `password`, `domain`, `port`, and `path`. The function is deliberately strict: it only accepts URIs whose `hier-part` uses the `//authority` form (`scheme://...`). This is not accidental — the XRPL daemon needs to connect to WebSocket, HTTPS, and PostgreSQL endpoints and must not silently accept opaque URIs. + +The regex is compiled once as a `static` local, which is important for performance given that `parseUrl` is called during config parsing, peer-handshake setup (see `Handshake.cpp`), RPC subscription setup (`RPCSub.cpp`), and validator site loading (`ValidatorSite.cpp`). The regex uses the `(?i)` inline flag for case-insensitive matching, which explains why the function then calls `boost::algorithm::to_lower` on the extracted scheme — the stored value is always lowercase regardless of what the caller supplied. + +IPv6 address handling deserves special attention. The regex captures the host component into `smMatch[4]` using a pattern that can match bracketed IPv6 addresses like `[::1]`. The code then attempts `beast::IP::Endpoint::from_string_checked(domain)`: if that succeeds, the result's `address().to_string()` is used, which strips the surrounding brackets and normalizes the address. If it fails (e.g., for a plain hostname), the raw regex capture is kept as-is. This makes the function correctly round-trip IPv6 literals while remaining transparent to hostname strings. + +Port validation layers two checks. `beast::lexicalCast` converts the port string to an unsigned 16-bit integer; per the implementation notes in the source, it returns `0` for any input that doesn't fit in a `uint16_t` (e.g., `65536` or `23498765`). A subsequent explicit `if (pUrl.port == 0)` then rejects port zero as invalid. This means ports outside `[1, 65535]` all fail, which is the correct behavior for network endpoints. + +The entire `boost::regex_match` call is wrapped in a bare `catch (...)` that returns `false`. This is a defensive pattern: Boost.Regex can throw `std::runtime_error` if a pathological input triggers an internal limit (which is why the test suite exercises a URL with 8,192 colons in the host). + +## `trim_whitespace` + +A thin wrapper around `boost::trim` that takes its argument by value and returns the trimmed copy. Taking by value rather than by reference is deliberate: the signature communicates that the caller gets a new string, avoids aliasing issues, and allows the compiler to elide a copy when the caller passes an rvalue. + +## `to_uint64` + +Converts a string to `std::optional` using `beast::lexicalCastChecked`. Where the header's template `strUnHex` handles hex-encoded binary, `to_uint64` handles decimal integers — primarily used in configuration parsing where the caller needs to distinguish between `"0"` (valid) and `"abc"` (invalid). Returning `std::nullopt` on failure rather than throwing or returning a sentinel value like `-1` is idiomatic modern C++ and avoids the trap of treating `0` as an error. + +## `isProperlyFormedTomlDomain` + +This function validates domain strings found in XRPL TOML files, the mechanism by which validators advertise their identity. Two fast-path checks bound the length to `[4, 128]` characters before invoking the regex, avoiding unnecessary regex execution for obviously invalid inputs. The `static` regex (compiled with `boost::regex_constants::optimize`) enforces RFC-like hostname structure: each label must be `[a-zA-Z0-9-]{1,63}` with no leading or trailing hyphens, and the TLD must be pure alpha with at least two characters. The header comment is explicit that this function is not a full RFC 5891 validator — it rejects some valid edge cases (notably internationalized domain names) and does not check IANA TLD lists. Its purpose is to filter obviously malformed inputs during TOML validation, not to definitively resolve domains. + +## Design Patterns and Tradeoffs + +Both regexes are `static` locals, ensuring they are compiled at first call and shared across all subsequent invocations — avoiding the significant overhead of Boost.Regex compilation on every call. The `parsedURL::operator==` in the header ignores `username` and `password` in its equality comparison, which is intentional: two URLs pointing at the same endpoint are considered equivalent for connection-deduplication purposes regardless of credentials. \ No newline at end of file diff --git a/src/libxrpl/basics/UptimeClock.cpp.ai.json b/src/libxrpl/basics/UptimeClock.cpp.ai.json new file mode 100644 index 0000000000..2bad595115 --- /dev/null +++ b/src/libxrpl/basics/UptimeClock.cpp.ai.json @@ -0,0 +1,101 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "UptimeClock::now", + "UptimeClock::start_clock (static init)", + "UptimeClock::update_thread (constructor)", + "lambda thread (updates now_)" + ], + "entry_point": "UptimeClock::now", + "purpose": "Provides the current uptime in seconds since first use of UptimeClock. On first call, starts a background thread that increments the uptime counter every second.", + "validation_points": [ + "No explicit validation in this chain. The only implicit check is thread-safe atomic access to now_ and stop_." + ] + }, + { + "call_chain": [ + "UptimeClock::update_thread::~update_thread", + "stop_ = true", + "join()" + ], + "entry_point": "UptimeClock::update_thread::~update_thread", + "purpose": "Ensures the background update thread is stopped and joined on shutdown.", + "validation_points": [ + "No explicit validation. Ensures thread is joinable before joining." + ] + } + ], + "data_flows": [ + { + "field": "UptimeClock::now_", + "flow": [ + "static initialization", + "incremented by background thread (lambda in start_clock)", + "read by UptimeClock::now", + "returned as time_point" + ], + "origin": "Initialized to 0 at static scope", + "transformations": [ + "Incremented atomically every second by background thread" + ], + "validated_at": "No explicit validation; atomic operations ensure thread safety" + }, + { + "field": "UptimeClock::stop_", + "flow": [ + "static initialization", + "set to true in update_thread::~update_thread", + "read by background thread loop condition" + ], + "origin": "Initialized to false at static scope", + "transformations": [ + "Set to true to signal thread to stop" + ], + "validated_at": "No explicit validation; atomic operations ensure thread safety" + } + ], + "description": "Implements an uptime clock for xrpld, tracking seconds since process start using a background thread that updates an atomic counter every second.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/UptimeClock.cpp", + "functions": [ + { + "args": [], + "lineno": 10, + "name": "UptimeClock::update_thread::~update_thread" + }, + { + "args": [], + "lineno": 20, + "name": "UptimeClock::start_clock" + }, + { + "args": [], + "lineno": 41, + "name": "UptimeClock::now" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is low-level infrastructure and does not contain explicit validation logic or input handling. It is likely tested indirectly via higher-level tests that depend on uptime reporting (e.g., server info, monitoring, or time-based logic). There are probably no direct unit tests for UptimeClock itself, and edge cases (e.g., thread start/stop races, timing accuracy) may not be directly tested. No validation of external input occurs in this code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/basics/UptimeClock.cpp.ai.md b/src/libxrpl/basics/UptimeClock.cpp.ai.md new file mode 100644 index 0000000000..4df24f4724 --- /dev/null +++ b/src/libxrpl/basics/UptimeClock.cpp.ai.md @@ -0,0 +1,48 @@ +# `UptimeClock.cpp` — Low-Cost Uptime Clock via Background Counter Thread + +## Purpose and Context + +`UptimeClock` provides a seconds-precision wall-clock that measures how long the `xrpld` process has been running. It is used throughout the server for diagnostics and time-based throttling — from the `get_counts` RPC handler that reports server uptime, to `LoadMonitor` tracking the interval between load events, to overlay and ledger subsystems tracking peer and ledger activity timing. Because these subsystems may call `now()` thousands of times per second collectively, the clock is designed around a single read of an atomic integer rather than a system call. + +## Design: Cached Counter vs. System Clock + +The fundamental tradeoff is read throughput vs. freshness granularity. A call to `std::chrono::system_clock::now()` ultimately invokes `clock_gettime`, a syscall that is fast but not free. When many components query the current time in tight loops, those calls accumulate. `UptimeClock` sidesteps this by maintaining a single `std::atomic` counter (`now_`) that is incremented once per second by a dedicated background thread. A call to `UptimeClock::now()` is just an atomic load — a few nanoseconds, no kernel transition. The tradeoff is that time is only accurate to ±1 second, which is entirely acceptable for uptime reporting and coarse rate-limiting. + +## Lazy Initialization via `static` Local + +The update thread is started exactly once, on the first call to `now()`, using a function-local `static`: + +```cpp +static auto const init = start_clock(); +``` + +C++11 guarantees that function-local `static` initialization is thread-safe and happens exactly once, even under concurrent calls. This avoids explicit startup sequencing — no `ApplicationImpl` constructor needs to call `UptimeClock::start()`. The first caller naturally bootstraps the clock. The header comment acknowledges this means the epoch is "first use" rather than true process start, but the difference is a negligible fraction of a second. + +## The `update_thread` RAII Wrapper + +Rather than exposing a raw `std::thread`, `start_clock()` returns an `update_thread` — a private type that inherits privately from `std::thread` and adds a custom destructor: + +```cpp +UptimeClock::update_thread::~update_thread() +{ + if (joinable()) + { + stop_ = true; + join(); + } +} +``` + +This is a clean RAII shutdown protocol. When `xrpld` exits and the `static init` object is destroyed, the destructor sets `stop_` to `true` and calls `join()`. The background thread will notice `stop_` on its next iteration — at most 1 second later — and exit cleanly. The comment in the source explicitly acknowledges this up-to-1s delay, noting it occurs only once at shutdown and is therefore acceptable. + +Using a private inheritance from `std::thread` (rather than composition) allows `update_thread` to inherit `std::thread`'s constructor through `using std::thread::thread`, while keeping the `thread` interface private to prevent external callers from accidentally detaching or re-joining. Only the custom destructor and the move constructor are exposed. + +## Thread and Memory Safety + +Both `now_` and `stop_` are `std::atomic`, so there is no data race between the background writer and the many concurrent readers. The background thread uses `std::this_thread::sleep_until` with a fixed `next` timestamp advanced by `1s` per iteration — this is more accurate than `sleep_for(1s)` because it avoids drift from the thread's own scheduling jitter accumulating over time. + +The `stop_` flag is checked in the `while` loop condition before sleeping, meaning on shutdown the thread exits after at most one more sleep cycle rather than running forever. There is no condition variable or explicit wake-up mechanism; the deliberate choice to wait up to 1 second keeps the implementation simple given the infrequency of process shutdown. + +## Relationship to `UptimeClock.h` + +The header defines `UptimeClock` as a type satisfying the `TrivialClock` concept from ``: it declares `rep`, `period`, `duration`, and `time_point` type aliases and a static `now()` function. This makes `UptimeClock` usable anywhere a `std::chrono`-compatible clock is expected — for example, storing `UptimeClock::time_point` values and doing duration arithmetic with standard `chrono` operators. The `is_steady` flag mirrors `system_clock::is_steady` rather than being hardcoded, correctly reflecting that `system_clock` is not guaranteed steady on all platforms. \ No newline at end of file diff --git a/src/libxrpl/basics/base64.cpp.ai.json b/src/libxrpl/basics/base64.cpp.ai.json new file mode 100644 index 0000000000..df9a263d8d --- /dev/null +++ b/src/libxrpl/basics/base64.cpp.ai.json @@ -0,0 +1,147 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "base64_encode", + "encode" + ], + "entry_point": "base64_encode", + "purpose": "Encodes a binary buffer into a base64 string.", + "validation_points": [ + "No explicit validation in base64_encode or encode; assumes input pointers and lengths are valid and output buffer is large enough." + ] + }, + { + "call_chain": [ + "base64_decode", + "decode" + ], + "entry_point": "base64_decode", + "purpose": "Decodes a base64 string into a binary buffer.", + "validation_points": [ + "decode: Validates each input character using get_inverse() table; skips invalid characters (those mapping to -1), but does not throw or error on malformed input." + ] + } + ], + "data_flows": [ + { + "field": "src (input buffer for encode)", + "flow": [ + "Caller provides src and len", + "encode reads src as input bytes", + "encode transforms bytes to base64 chars using get_alphabet()", + "Output written to dest" + ], + "origin": "Caller of base64_encode/encode", + "transformations": [ + "Groups input bytes in 3s, maps to 4 base64 chars", + "Pads output with '=' if input not multiple of 3" + ], + "validated_at": "No explicit validation; relies on caller to provide valid pointers and lengths" + }, + { + "field": "src (input string for decode)", + "flow": [ + "Caller provides src and len", + "decode reads src as input chars", + "decode uses get_inverse() to map chars to 6-bit values", + "decode reconstructs original bytes, writes to dest" + ], + "origin": "Caller of base64_decode/decode", + "transformations": [ + "Skips characters not in base64 alphabet (get_inverse() returns -1)", + "Handles padding ('=')" + ], + "validated_at": "decode: Each character is validated via get_inverse(); invalid chars are skipped" + } + ], + "description": "Provides base64 encoding and decoding utilities, including functions to encode and decode data to and from base64, as well as helper functions for base64 alphabet and inverse lookup.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/base64.cpp", + "functions": [ + { + "args": [], + "lineno": 32, + "name": "get_alphabet" + }, + { + "args": [], + "lineno": 39, + "name": "get_inverse" + }, + { + "args": [ + "n" + ], + "lineno": 90, + "name": "encoded_size" + }, + { + "args": [ + "n" + ], + "lineno": 95, + "name": "decoded_size" + }, + { + "args": [ + "dest", + "src", + "len" + ], + "lineno": 104, + "name": "encode" + }, + { + "args": [ + "dest", + "src", + "len" + ], + "lineno": 139, + "name": "decode" + }, + { + "args": [ + "data", + "len" + ], + "lineno": 191, + "name": "base64_encode" + }, + { + "args": [ + "data" + ], + "lineno": 198, + "name": "base64_decode" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 28, + "name": "xrpl" + }, + { + "lineno": 30, + "name": "base64" + } + ], + "test_coverage_notes": "Test coverage is not shown in this file. Typically, base64 encoding/decoding is tested in unit test files (e.g., test_basics_base64.cpp or similar in the test/ or src/test/ directories). Key gaps: No explicit validation for buffer overruns or malformed input; decode skips invalid chars but does not error. No evidence of fuzz or malformed input tests in this file. Test coverage should ensure: correct encoding/decoding, handling of invalid input, correct padding, and buffer size edge cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none", + "validation_layer": "none" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/basics/base64.cpp.ai.md b/src/libxrpl/basics/base64.cpp.ai.md new file mode 100644 index 0000000000..b61c99e82a --- /dev/null +++ b/src/libxrpl/basics/base64.cpp.ai.md @@ -0,0 +1,33 @@ +# `src/libxrpl/basics/base64.cpp` + +This file implements standard Base64 encoding and decoding (RFC 4648) for the XRPL ledger library. It is derived from René Nyffenegger's public domain implementation (2004–2008) with modifications to fit the `xrpl` namespace and C++ idiom conventions. The codec is used widely throughout the ledger: most visibly in the peer overlay handshake (`Handshake.cpp`), where node session signatures are Base64-encoded into HTTP headers, and in validator list and manifest handling. + +## Two-Layer API Design + +The file exposes two layers. The inner `xrpl::base64` anonymous-ish namespace contains low-level, buffer-oriented primitives (`encode`, `decode`), while the outer `xrpl` namespace exposes the caller-facing `base64_encode` / `base64_decode` functions that manage `std::string` memory automatically. This split allows the hot path to avoid heap allocation entirely when callers can supply their own pre-allocated buffers — the inner functions work on raw `void*`/`char*` pointers and return byte counts rather than strings. + +## Static Lookup Tables + +Two `constexpr` tables drive the codec. `get_alphabet()` returns the 64-character Base64 alphabet (`A–Z`, `a–z`, `0–9`, `+`, `/`), stored as a function-local `static constexpr` array and returned as a pointer. `get_inverse()` returns a 256-element `signed char` table mapping every possible byte value to its 6-bit Base64 value, or `-1` for characters that are not part of the alphabet. Storing the inverse as a flat 256-entry array makes character validation and value extraction a single array index — O(1) with no branching per character. + +## Encoding + +`encode()` processes input three bytes at a time, fanning each group of 24 bits out into four 6-bit indices into the alphabet table. The main loop handles `len / 3` full groups. The `switch (len % 3)` tail handles the one- or two-byte remainder with appropriate `=` padding. The `// NOLINTNEXTLINE(bugprone-switch-missing-default-case)` suppression is intentional: `len % 3` can only be 0, 1, or 2, so there is no missing case — the suppression documents that the omission is deliberate rather than an oversight. + +`encoded_size(n)` computes the exact output size as `4 * ((n + 2) / 3)`, which is the ceiling-of-thirds multiplied by four — this is always precise. + +## Decoding + +`decode()` reads one Base64 character at a time, accumulates four 6-bit values into `c4[]`, then recombines them into three bytes in `c3[]`. The loop exits on three conditions: input exhausted, a `=` padding character encountered, or an invalid character (one that maps to `-1` in the inverse table). When the loop exits with a partial group of 1–3 valid characters, the post-loop block reconstructs as many bytes as the partial group can produce (`i - 1` bytes, since a single accumulated Base64 character encodes no complete output byte by itself). + +`decoded_size(n)` deliberately returns an *upper bound* — `(n / 4) * 3 + 2` — not an exact count. The `+2` ensures the caller's pre-allocated buffer is always large enough regardless of padding or partial trailing groups. The actual number of bytes written is returned in the first element of the `std::pair`, and the second element reports how many input characters were consumed (useful if the caller wants to detect where decoding stopped). + +## Public Wrappers and Memory Management + +`base64_encode` and `base64_decode` follow a two-phase resize pattern common in the codebase: pre-allocate to the conservative maximum (`encoded_size` or `decoded_size`), invoke the low-level function, then `resize()` the string down to the actual byte count. This avoids a separate size-calculation pass while ensuring no reallocation occurs during the fill. + +## Error Handling Posture + +There is no exception thrown on invalid input. `decode` silently stops at the first unrecognized character and returns whatever was successfully decoded up to that point. The unit test in `src/tests/libxrpl/basics/base64.cpp` explicitly validates this: `base64_decode("not_base64!!")` must equal `base64_decode("not")`, confirming that the `_` and `!` characters both trigger early termination. Callers that need to distinguish successful from partial decodes must compare the returned length against the expected output size themselves; the API provides no status enum or error flag. + +There are no concurrency concerns — all mutable state is function-local, and the lookup tables are `constexpr`, making the functions fully re-entrant. \ No newline at end of file diff --git a/src/libxrpl/basics/contract.cpp.ai.json b/src/libxrpl/basics/contract.cpp.ai.json new file mode 100644 index 0000000000..8414976bb0 --- /dev/null +++ b/src/libxrpl/basics/contract.cpp.ai.json @@ -0,0 +1,110 @@ +{ + "args": [ + { + "lineno": 9, + "name": "title" + }, + { + "lineno": 14, + "name": "s" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LogThrow" + ], + "entry_point": "LogThrow", + "purpose": "Logs a warning message when a contract violation or exceptional condition is detected.", + "validation_points": [ + "LogThrow (logs but does not validate or throw)" + ] + }, + { + "call_chain": [ + "LogicError", + "UNREACHABLE", + "std::abort" + ], + "entry_point": "LogicError", + "purpose": "Handles logic errors by logging, reporting, and terminating the process. Used as a last-resort contract violation handler.", + "validation_points": [ + "LogicError (logs fatal, reports error, aborts execution)" + ] + } + ], + "data_flows": [ + { + "field": "title (LogThrow)", + "flow": [ + "caller", + "LogThrow(title)", + "JLOG(debugLog().warn()) << title" + ], + "origin": "Passed as argument to LogThrow by caller", + "transformations": [ + "No transformation; directly logged as warning" + ], + "validated_at": "No validation; only logging" + }, + { + "field": "s (LogicError)", + "flow": [ + "caller", + "LogicError(s)", + "JLOG(debugLog().fatal()) << s", + "std::cerr << s", + "UNREACHABLE(\"LogicError\", {{\"message\", s}})", + "std::abort()" + ], + "origin": "Passed as argument to LogicError by caller", + "transformations": [ + "Logged as fatal", + "Printed to std::cerr", + "Passed as 'message' to UNREACHABLE contract macro" + ], + "validated_at": "No explicit validation; function is for unrecoverable errors" + } + ], + "description": "Provides logging and error handling utilities for the xrpl project, including functions to log warnings and handle logic errors with fatal logging and process termination.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/contract.cpp", + "functions": [ + { + "args": [ + "title" + ], + "lineno": 8, + "name": "LogThrow" + }, + { + "args": [ + "s" + ], + "lineno": 13, + "name": "LogicError" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "These functions are intended for contract violation handling and process termination. They are not directly tested in normal unit tests due to their fatal behavior (process abort). Coverage tools (LCOV) explicitly exclude these paths (see LCOV_EXCL_START/STOP). Indirectly, they may be triggered by higher-level contract checks or assertions elsewhere in the codebase, but direct invocation is not covered by tests. Test files likely do not exist for these functions, and any coverage would be via integration or crash tests, not unit tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/basics/contract.cpp.ai.md b/src/libxrpl/basics/contract.cpp.ai.md new file mode 100644 index 0000000000..3bc413d15e --- /dev/null +++ b/src/libxrpl/basics/contract.cpp.ai.md @@ -0,0 +1,58 @@ +# `src/libxrpl/basics/contract.cpp` + +This file is the implementation half of the XRPL "programming by contract" facility. It provides two concrete functions — `LogThrow` and `LogicError` — that form the runtime backbone for all contract-violation handling across the ledger codebase. The companion header `contract.h` builds on these with the inline `Throw()` template and `Rethrow()`, but those functions delegate their logging to `LogThrow` defined here. + +## Role in the Contract System + +The header `contract.h` defines three layers of contract violation: + +- **Recoverable throws** — `Throw(args...)` constructs an exception, calls `LogThrow` to journal it, then throws. +- **Rethrows** — `Rethrow()` calls `LogThrow` with a fixed message, then re-throws the active exception. +- **Unrecoverable logic errors** — `LogicError(s)` terminates the process. + +The `.cpp` file supplies the two non-inline implementations, keeping the instrumentation details out of every translation unit that includes the header. + +## `LogThrow` + +```cpp +void LogThrow(std::string const& title) +{ + JLOG(debugLog().warn()) << title; +} +``` + +This function's job is deliberately narrow: emit a warning-level journal entry immediately before an exception propagates. It does not inspect or rethrow anything. `debugLog()` returns a `beast::Journal` that drains to a debug sink (which may be a null sink if logging hasn't been configured). The header comment on `debugLog()` explicitly notes this — the journal's output "may never be seen." `LogThrow` is therefore not a reliable audit trail; it is a best-effort diagnostic hint in development and staging environments. For throw paths where the message must be preserved, callers use the exception object itself. + +The ASAN annotation `XRPL_NO_SANITIZE_ADDRESS` lives on the callers in the header, not here, because this function does not itself perform the control-flow jump. + +## `LogicError` + +```cpp +[[noreturn]] void LogicError(std::string const& s) noexcept +{ + JLOG(debugLog().fatal()) << s; + std::cerr << "Logic error: " << s << std::endl; + UNREACHABLE("LogicError", {{"message", s}}); + std::abort(); +} +``` + +`LogicError` is called when a broken invariant has been detected and recovery is impossible — situations where continuing would risk data corruption or incorrect ledger state. Several design choices here are deliberate: + +**Dual output channels.** The function writes to both the XRPL journal (fatal level) and directly to `std::cerr`. This matters because the journal subsystem may itself be in a bad state, or may not have been initialized when a logic error fires during startup or shutdown. `std::cerr` is an unbuffered channel that will survive almost any application-level failure, ensuring the message reaches an operator even when logging infrastructure cannot. + +**`noexcept` on a `[[noreturn]]` function.** Marking `LogicError` as `noexcept` communicates to the compiler and to callers that this function will never propagate an exception — it only terminates. This is consistent with its purpose (catching broken invariants) and prevents callers from being tempted to wrap it in a try/catch. + +**`UNREACHABLE` before `std::abort()`.** The `UNREACHABLE` macro (from `instrumentation.h`) expands to `assert("LogicError" && false)` in non-Antithesis builds. In a debug build this immediately triggers the assert handler, giving debuggers a clean crash point with a readable message. In a release build the assert is a no-op, so `std::abort()` on the next line is the actual termination mechanism — providing a guaranteed crash regardless of build mode. This layered approach means debug builds surface the error through familiar assert machinery, while release builds still produce an `SIGABRT` that crash-reporting infrastructure can capture. + +**Special `UNREACHABLE` naming.** The in-code comment explains why the contract is named `"LogicError"` (without a namespace qualifier) rather than following the normal fully-qualified naming convention. `LogicError` is the single convergence point for many unrelated execution paths across the codebase — `SHAMap`, `LedgerCleaner`, `Application`, and others all call it. Using a plain name without namespace lets this contract stand out in instrumentation telemetry as a cross-cutting terminal condition rather than being attributed to one subsystem. + +**LCOV exclusion.** The entire function body is wrapped in `LCOV_EXCL_START` / `LCOV_EXCL_STOP`. This is correct: lines that are only reached when the program is about to die cannot be exercised by ordinary unit tests, and attempting to cover them would require instrumenting process-abort behavior. The exclusion keeps coverage metrics honest. + +## Relationship to Instrumentation + +When the codebase is compiled with `ENABLE_VOIDSTAR` (the Antithesis fuzzing platform), `UNREACHABLE` is replaced by the real `antithesis_sdk.h` macro, which reports the violation to the fuzzer's fault-injection framework rather than aborting immediately. This allows the fuzzer to observe and learn from logic-error paths without crashing its harness. The `std::abort()` that follows remains as a hard stop after the fuzzer has had its chance to record the event. + +## Usage Pattern + +Callers invoke `LogicError` at sites where a condition "can't happen" by design but the code cannot statically prove it — for example, `LedgerHolder` uses it if an uninitialized ledger reference is dereferenced, and `SHAMap` uses it when internal node accounting becomes inconsistent. It is never called in error-handling paths that are expected to trigger under normal load. \ No newline at end of file diff --git a/src/libxrpl/basics/make_SSLContext.cpp.ai.json b/src/libxrpl/basics/make_SSLContext.cpp.ai.json new file mode 100644 index 0000000000..e33ced69e1 --- /dev/null +++ b/src/libxrpl/basics/make_SSLContext.cpp.ai.json @@ -0,0 +1,342 @@ +{ + "args": [ + { + "lineno": 54, + "name": "context" + }, + { + "lineno": 143, + "name": "context" + }, + { + "lineno": 143, + "name": "key_file" + }, + { + "lineno": 143, + "name": "cert_file" + }, + { + "lineno": 143, + "name": "chain_file" + }, + { + "lineno": 222, + "name": "cipherList" + }, + { + "lineno": 246, + "name": "cipherList" + }, + { + "lineno": 254, + "name": "keyFile" + }, + { + "lineno": 254, + "name": "certFile" + }, + { + "lineno": 254, + "name": "chainFile" + }, + { + "lineno": 254, + "name": "cipherList" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "make_SSLContext", + "get_context", + "initAnonymous" + ], + "entry_point": "make_SSLContext", + "purpose": "Creates an anonymous SSL context for use in the server, initializing ephemeral keys and parameters.", + "validation_points": [ + "RSA_new failure checked in initAnonymous", + "RSA_generate_key_ex failure checked in initAnonymous", + "EVP_PKEY_new failure checked in initAnonymous", + "RSA_up_ref failure checked in initAnonymous" + ] + }, + { + "call_chain": [ + "make_SSLContextAuthed", + "get_context", + "initAuthenticated" + ], + "entry_point": "make_SSLContextAuthed", + "purpose": "Creates an authenticated SSL context, likely with certificate and key, for use in the server.", + "validation_points": [ + "Validation of certificate/key loading (not shown in provided code, but expected in initAuthenticated)" + ] + } + ], + "data_flows": [ + { + "field": "defaultRSAKeyBits", + "flow": [ + "defaultRSAKeyBits (constant)", + "used in RSA_generate_key_ex in initAnonymous", + "RSA key generated", + "assigned to EVP_PKEY" + ], + "origin": "Constant assignment in file (int defaultRSAKeyBits = 2048;)", + "transformations": [ + "Used as key size parameter for RSA key generation" + ], + "validated_at": "No runtime validation; only checked for success/failure of RSA_generate_key_ex" + }, + { + "field": "defaultDH", + "flow": [ + "defaultDH (constant)", + "used in context setup (not shown in provided code, but likely in initAnonymous/initAuthenticated)", + "DH parameters loaded into SSL context" + ], + "origin": "Constant assignment in file (static constexpr char const defaultDH[])", + "transformations": [ + "PEM string parsed to DH parameters" + ], + "validated_at": "No runtime validation; assumed correct as constant" + }, + { + "field": "boost::asio::ssl::context", + "flow": [ + "get_context creates context", + "passed to initAnonymous or initAuthenticated", + "context configured with keys, ciphers, DH params" + ], + "origin": "Created in get_context", + "transformations": [ + "SSL context is configured with ephemeral keys, ciphers, and DH parameters" + ], + "validated_at": "Errors in context configuration throw exceptions (e.g., LogicError on failure)" + } + ], + "description": "This file provides functions to create and initialize SSL/TLS contexts for secure communication, including support for anonymous and authenticated modes, default cipher lists, and ephemeral self-signed certificates using OpenSSL and Boost.Asio.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.3, + "detection_keywords": [ + "defaultRSAKeyBits", + "empty", + "string", + "validation" + ], + "evidence": "N/A (constant assignment, not runtime validation) at global variable initialization", + "issue_pattern": "Missing empty string validation for defaultRSAKeyBits", + "why_false_positive": "N/A (constant assignment, not runtime validation) validates defaultRSAKeyBits for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "defaultRSAKeyBits", + "range", + "bounds", + "validation" + ], + "evidence": "N/A (constant assignment, not runtime validation) at global variable initialization", + "issue_pattern": "Missing range validation for defaultRSAKeyBits", + "why_false_positive": "N/A (constant assignment, not runtime validation) validates defaultRSAKeyBits range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.3, + "detection_keywords": [ + "defaultDH", + "empty", + "string", + "validation" + ], + "evidence": "N/A (constant assignment, not runtime validation) at global variable initialization", + "issue_pattern": "Missing empty string validation for defaultDH", + "why_false_positive": "N/A (constant assignment, not runtime validation) validates defaultDH for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "defaultDH", + "format", + "validation", + "invalid" + ], + "evidence": "N/A (constant assignment, not runtime validation) at global variable initialization", + "issue_pattern": "Missing format validation for defaultDH", + "why_false_positive": "N/A (constant assignment, not runtime validation) validates defaultDH format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/make_SSLContext.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 54, + "name": "initAnonymous" + }, + { + "args": [ + "context", + "key_file", + "cert_file", + "chain_file" + ], + "lineno": 143, + "name": "initAuthenticated" + }, + { + "args": [ + "cipherList" + ], + "lineno": 222, + "name": "get_context" + }, + { + "args": [ + "cipherList" + ], + "lineno": 246, + "name": "make_SSLContext" + }, + { + "args": [ + "keyFile", + "certFile", + "chainFile", + "cipherList" + ], + "lineno": 254, + "name": "make_SSLContextAuthed" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 18, + "name": "xrpl" + }, + { + "lineno": 19, + "name": "openssl" + }, + { + "lineno": 20, + "name": "detail" + } + ], + "test_coverage_notes": "There is no evidence in this file of direct unit tests or test hooks. The code is low-level and relies on runtime exceptions for error handling. Test coverage would likely exist in higher-level integration or system tests that exercise SSL/TLS connections (e.g., server startup, RPC over TLS). There is no explicit validation of the constants (defaultRSAKeyBits, defaultDH) at runtime, so tests would not catch misconfiguration unless the server fails to start. Gaps: No direct tests for failure modes of key/cert generation, no validation of DH parameter correctness, and no config-driven validation paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected in this file", + "validation_layer": "N/A (no runtime input validation in this file)" + }, + "validations": [ + { + "confidence": 0.3, + "error_thrown": "N/A", + "field": "defaultRSAKeyBits", + "location": "global variable initialization", + "validated_by": "N/A (constant assignment, not runtime validation)", + "validates": [ + "RSA key bit length is set to 2048, which is considered secure per NIST" + ], + "validation_type": "range" + }, + { + "confidence": 0.3, + "error_thrown": "N/A", + "field": "defaultDH", + "location": "global variable initialization", + "validated_by": "N/A (constant assignment, not runtime validation)", + "validates": [ + "DH parameters are in PEM format" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/make_SSLContext.cpp.ai.md b/src/libxrpl/basics/make_SSLContext.cpp.ai.md new file mode 100644 index 0000000000..39a99931de --- /dev/null +++ b/src/libxrpl/basics/make_SSLContext.cpp.ai.md @@ -0,0 +1,42 @@ +# `src/libxrpl/basics/make_SSLContext.cpp` + +This file implements the TLS context factory for the XRP Ledger node. It provides two flavors of `boost::asio::ssl::context`: an **anonymous** variant used for peer overlay connections (where application-layer node identity makes certificate-based authentication redundant) and an **authenticated** variant used for operator-configured RPC and WebSocket endpoints where standard TLS certificate validation is expected. Both paths share a common `get_context()` base that enforces protocol version minimums, DH parameters, and a hardened cipher list. + +## Two-Mode Design + +The split between `make_SSLContext()` and `make_SSLContextAuthed()` reflects a deliberate architectural choice. In the overlay network (`OverlayImpl.cpp`), XRPL nodes authenticate each other through cryptographic node identities at the application layer — the TLS layer exists purely to prevent eavesdropping on the wire, not to verify who is speaking. Consequently, `make_SSLContext()` sets `verify_none` on the returned context and installs a self-generated ephemeral certificate. For the HTTP/WebSocket-facing RPC interface (`ServerHandler.cpp`), callers may be browsers or third-party tooling that expects real certificate chains, so `make_SSLContextAuthed()` loads operator-supplied key material from disk instead and does not set `verify_none`. + +## Base Context: `get_context()` + +Every context starts in `get_context()`. The context is created with the `sslv23` method identifier — a naming artifact in Boost.Asio that actually means "negotiate the best mutually supported version" — and then immediately flags out SSLv2, SSLv3, TLS 1.0, and TLS 1.1, leaving only TLS 1.2 and above. Compression is also disabled (`no_compression`), mitigating CRIME-class attacks. + +The cipher list defaults to `"TLSv1.2:!CBC:!DSS:!PSK:!eNULL:!aNULL"` if the caller passes an empty string. The explicit `!CBC` exclusion is notable: it strips all block-cipher-mode suites, leaving only AEAD constructions (GCM in practice), which sidestep the BEAST and POODLE families of attacks. Operators can override this list per-port via `ssl_ciphers` in the config file, making it a configurable policy floor rather than a hard constraint. + +Hardcoded 2048-bit DH parameters (generated by Nik Bougalis in May 2022 using `openssl dhparam 2048`) are loaded unconditionally via `use_tmp_dh`. The comment above `defaultRSAKeyBits` explains the deliberate choice to stay at 2048 bits rather than the more conservative 3072 or 4096: the keys are ephemeral (regenerated on every server restart) and the data being protected is not highly sensitive (RPC traffic, not private keys or seeds), so the CPU cost of larger keys is not justified. If `defaultRSAKeyBits` is ever raised, the DH blob must be regenerated to match — the comment notes this dependency explicitly. + +Finally, `SSL_CTX_set_options(ctx, SSL_OP_NO_RENEGOTIATION)` disables TLS 1.2 renegotiation as a mitigation for CVE-2021-3499, which affects OpenSSL versions before 1.1.1k. The code is guarded by comment rather than a version check, providing a belt-and-suspenders defense against older OpenSSL deployments. + +## Anonymous Certificate: `initAnonymous()` + +The anonymous path constructs an RSA key pair and a self-signed X.509 certificate using three function-local `static` lambdas that run once at first call. This means the expensive 2048-bit RSA key generation happens exactly once per process lifetime, with all subsequent calls to `make_SSLContext()` reusing the same certificate — appropriate since the certificate carries no meaningful identity and is replaced entirely on each restart. + +The RSA key is created with the standard `RSA_F4` (65537) public exponent, then wrapped in an `EVP_PKEY`. The reference count of the RSA object is explicitly incremented via `RSA_up_ref()` before assigning it to `EVP_PKEY_assign_RSA()`, because `EVP_PKEY_assign_RSA` transfers ownership and OpenSSL would free the key once the `EVP_PKEY` is released — leaving the `defaultRSA` static dangling for the next potential call. + +The X.509 certificate is built with several non-obvious defensive details: + +- **Validity window obfuscation**: The `notBefore` time is set to 25 hours *before* the actual current time (rounded to midnight UTC), so that an observer cannot infer the precise moment the server started from the certificate's inception date. +- **Two-year expiry**: The `notAfter` is two years out, long enough that a running server never hits it under normal conditions. +- **Random 128-bit serial**: `BN_rand()` generates a fresh serial number to prevent serial collisions across restarts. +- **X.509v3 extensions**: The certificate is marked `CA:FALSE` (it cannot sign other certificates), has `ext_key_usage` set to both `serverAuth` and `clientAuth` (since either peer in an overlay connection can be initiator), and `key_usage` is limited to `digitalSignature`. + +All OpenSSL allocation failures call `LogicError()` — a fatal, non-recoverable error function from `xrpl/basics/contract.h` — because a server that cannot create its TLS context at startup has no viable recovery path. + +## Authenticated Certificate Loading: `initAuthenticated()` + +For the authenticated path, `initAuthenticated()` accepts three optional file paths: a standalone certificate, a chain file, and a private key file. The interesting logic is the chain file reader: it opens the file with `fopen` (a known technical debt item, flagged with a `// VFALCO Replace fopen() with RAII` comment) and then calls `PEM_read_X509` in a loop, consuming one PEM certificate block per iteration. The first certificate loaded becomes the leaf via `SSL_CTX_use_certificate`; all subsequent ones are added as intermediate CA certificates via `SSL_CTX_add_extra_chain_cert`. This two-phase approach correctly handles the common deployment pattern of a single file containing the server cert followed by the CA chain. Notably, if a standalone `cert_file` was already loaded before the chain file is processed, the chain file loop skips promoting the first entry to leaf (because `cert_set` is already true) and adds everything directly to the chain. + +After loading key material, `SSL_CTX_check_private_key` validates that the loaded private key matches the certificate's public key, catching misconfiguration before the server begins accepting connections. + +## Thread Safety + +The TSAN suppression file (`tsan.supp`) contains an explicit entry for `make_SSLContext.cpp`, acknowledging a benign data race on the static local variables inside `initAnonymous()`. The race is benign because all competing initializations produce identical results (the same key and certificate values) and the statics are idempotent once set. This is the classic "initialization race on function-local statics in C++11 is safe, but TSAN still flags it" situation. \ No newline at end of file diff --git a/src/libxrpl/basics/mulDiv.cpp.ai.json b/src/libxrpl/basics/mulDiv.cpp.ai.json new file mode 100644 index 0000000000..e177537a2b --- /dev/null +++ b/src/libxrpl/basics/mulDiv.cpp.ai.json @@ -0,0 +1,181 @@ +{ + "args": [ + { + "lineno": 10, + "name": "value" + }, + { + "lineno": 10, + "name": "mul" + }, + { + "lineno": 10, + "name": "div" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "mulDiv" + ], + "entry_point": "mulDiv", + "purpose": "Performs (value * mul) / div with overflow checking, returning std::nullopt if result exceeds uint64_t max.", + "validation_points": [ + "if (result > xrpl::muldiv_max) return std::nullopt;" + ] + } + ], + "data_flows": [ + { + "field": "value", + "flow": [ + "mulDiv parameter", + "multiply(result, value, mul)", + "result /= div", + "if (result > xrpl::muldiv_max)", + "return static_cast(result) or std::nullopt" + ], + "origin": "Input parameter to mulDiv", + "transformations": [ + "Multiplied by mul using multiply()", + "Divided by div", + "Compared to xrpl::muldiv_max for overflow check", + "Casted to uint64_t if valid" + ], + "validated_at": "if (result > xrpl::muldiv_max)" + }, + { + "field": "mul", + "flow": [ + "mulDiv parameter", + "multiply(result, value, mul)", + "result /= div", + "if (result > xrpl::muldiv_max)", + "return static_cast(result) or std::nullopt" + ], + "origin": "Input parameter to mulDiv", + "transformations": [ + "Multiplied with value using multiply()", + "Result divided by div", + "Overflow checked", + "Casted to uint64_t if valid" + ], + "validated_at": "if (result > xrpl::muldiv_max)" + }, + { + "field": "div", + "flow": [ + "mulDiv parameter", + "result /= div", + "if (result > xrpl::muldiv_max)", + "return static_cast(result) or std::nullopt" + ], + "origin": "Input parameter to mulDiv", + "transformations": [ + "Divides the result of value * mul", + "Overflow checked", + "Casted to uint64_t if valid" + ], + "validated_at": "if (result > xrpl::muldiv_max)" + }, + { + "field": "result", + "flow": [ + "Initialized", + "Set by multiply(result, value, mul)", + "Divided by div", + "Compared to xrpl::muldiv_max", + "Returned as uint64_t or std::nullopt" + ], + "origin": "Local variable in mulDiv (boost::multiprecision::uint128_t)", + "transformations": [ + "Set to value * mul", + "Divided by div", + "Checked for overflow", + "Casted to uint64_t if valid" + ], + "validated_at": "if (result > xrpl::muldiv_max)" + } + ], + "description": "Provides a function to safely multiply two uint64_t values and divide by a third, returning an optional result if the computation fits in uint64_t.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result", + "empty", + "string", + "validation" + ], + "evidence": "explicit comparison (if statement) at mulDiv", + "issue_pattern": "Missing empty string validation for result", + "why_false_positive": "explicit comparison (if statement) validates result for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "result", + "range", + "bounds", + "validation" + ], + "evidence": "explicit comparison (if statement) at mulDiv", + "issue_pattern": "Missing range validation for result", + "why_false_positive": "explicit comparison (if statement) validates result range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/basics/mulDiv.cpp", + "functions": [ + { + "args": [ + "value", + "mul", + "div" + ], + "lineno": 9, + "name": "mulDiv" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "No test files are shown in the provided context. Typical tests would be in files like 'mulDiv_test.cpp' or similar under a 'test' or 'unittest' directory. The critical validation (overflow check) should be tested with values that: (a) do not overflow, (b) exactly hit the limit, (c) overflow. Gaps: No evidence of tests for division by zero, edge cases for all parameters, or negative/invalid input handling (though all inputs are uint64_t, so negatives are not possible).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, no framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception thrown)", + "field": "result", + "location": "mulDiv", + "validated_by": "explicit comparison (if statement)", + "validates": [ + "Checks if the computed result (after multiplication and division) exceeds xrpl::muldiv_max" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/basics/mulDiv.cpp.ai.md b/src/libxrpl/basics/mulDiv.cpp.ai.md new file mode 100644 index 0000000000..51c809e1f9 --- /dev/null +++ b/src/libxrpl/basics/mulDiv.cpp.ai.md @@ -0,0 +1,42 @@ +## `mulDiv.cpp` — Overflow-Safe Multiply-Then-Divide + +This file implements a single arithmetic utility: `mulDiv(value, mul, div)`, which computes `(value * mul) / div` over three `std::uint64_t` operands without losing precision or silently producing a wrong answer due to integer overflow. + +### The Problem It Solves + +Naive computation of `(value * mul) / div` in 64-bit arithmetic is unsafe when `value * mul` exceeds `UINT64_MAX` (~1.8 × 10¹⁹). This happens routinely in XRPL fee scaling: `LoadFeeTrack.cpp` calls `mulDiv(fee, feeFactor, feeBase)` to scale a base fee by a load factor, and the intermediate product can easily overflow. A silent 64-bit overflow would produce a wildly incorrect fee — a class of bug that is very hard to detect at runtime and dangerous in a financial system. + +The naive two-step alternative — dividing first to keep numbers small — is equally flawed because it introduces rounding error proportional to `mul / div`, which violates the ledger's need for exact integer arithmetic. + +### The 128-bit Intermediate Trick + +The implementation uses `boost::multiprecision::uint128_t` to hold the intermediate product: + +```cpp +boost::multiprecision::uint128_t result; +result = multiply(result, value, mul); // 64×64 → 128 bits, no overflow +result /= div; // back toward range +``` + +By widening to 128 bits before multiplying, any product of two `uint64_t` values fits without overflow (max product ≈ 3.4 × 10³⁸, well within 128 bits). Division is then performed on the full-precision 128-bit value, so no precision is lost before the final narrowing. + +Boost multiprecision is used rather than the `__uint128_t` GCC extension because the latter is not available on MSVC — this keeps the code portable across the compilers used in XRPL builds. + +### Overflow Detection and the `std::optional` Return + +After division, the 128-bit result may still exceed `uint64_t` range. The check: + +```cpp +if (result > xrpl::muldiv_max) + return std::nullopt; +``` + +`muldiv_max` is `std::numeric_limits::max()`, defined in the accompanying header. Returning `std::nullopt` rather than throwing communicates overflow to callers that should handle it as a control-flow condition rather than an exceptional event. Callers that need an exception — like `LoadFeeTrack.cpp`, which throws `std::overflow_error("scaleFeeLoad")` — wrap the call and throw themselves, keeping policy out of this utility. + +### Test Coverage + +The test file (`src/tests/libxrpl/basics/mulDiv.cpp`) validates commutativity of the input arguments, zero operands, large near-`max` inputs that should fit, and a case that intentionally overflows. A representative stress case, `mulDiv(max, 1000, max / 1000)`, confirms that an intermediate product roughly 1000× larger than `UINT64_MAX` is handled correctly and produces the exact integer `1,000,000`. + +### Callers and Context + +Beyond fee scaling, `mulDiv` appears in `TxQ.cpp` (transaction queue fee escalation) and `TransactionSign.cpp` (fee calculations during signing). In all cases the pattern is the same: a ratio `a/b` must be applied to a `uint64_t` value where the intermediate product is untrustworthy in 64-bit arithmetic. The function's simplicity — four lines of logic — belies how frequently 64-bit overflow quietly breaks financial arithmetic in less carefully written code. \ No newline at end of file diff --git a/src/libxrpl/beast/clock/basic_seconds_clock.cpp.ai.json b/src/libxrpl/beast/clock/basic_seconds_clock.cpp.ai.json new file mode 100644 index 0000000000..b5b15129e9 --- /dev/null +++ b/src/libxrpl/beast/clock/basic_seconds_clock.cpp.ai.json @@ -0,0 +1,230 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "seconds_clock_thread" + } + ], + "code_paths": [ + { + "call_chain": [ + "basic_seconds_clock::now", + "seconds_clock_thread::now" + ], + "entry_point": "basic_seconds_clock::now", + "purpose": "Provides the current time in seconds, using a background thread to update the time point.", + "validation_points": [] + }, + { + "call_chain": [ + "seconds_clock_thread::~seconds_clock_thread", + "XRPL_ASSERT(thread_.joinable())", + "thread_.join()" + ], + "entry_point": "seconds_clock_thread::~seconds_clock_thread", + "purpose": "Destructor for the clock thread, ensures the thread is joinable before joining and cleaning up.", + "validation_points": [ + "XRPL_ASSERT(thread_.joinable())" + ] + }, + { + "call_chain": [ + "seconds_clock_thread::run", + "cv_.wait_until(lock, when, [this] { return stop_; })" + ], + "entry_point": "seconds_clock_thread::run", + "purpose": "Background thread loop that updates the atomic time point every second, exits when stop_ is set.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "stop_", + "flow": [ + "constructor (false)", + "set to true in destructor under lock", + "checked in run()'s wait_until predicate" + ], + "origin": "Initialized as false in seconds_clock_thread", + "transformations": [ + "Set to true to signal thread to stop" + ], + "validated_at": "Not directly validated, but used as a predicate for thread exit" + }, + { + "field": "thread_", + "flow": [ + "constructor (std::thread created)", + "checked for joinable in destructor", + "joined in destructor" + ], + "origin": "Constructed in seconds_clock_thread constructor", + "transformations": [ + "Created, checked for joinability, joined" + ], + "validated_at": "XRPL_ASSERT(thread_.joinable()) in destructor" + }, + { + "field": "tp_", + "flow": [ + "constructor (set to Clock::now().time_since_epoch().count())", + "updated in run() loop every second", + "read in now()" + ], + "origin": "Initialized in constructor with current time", + "transformations": [ + "Atomic store of time value, atomic load for reads" + ], + "validated_at": "No explicit validation, but atomic for thread safety" + } + ], + "description": "Implements a thread-based clock that updates once per second, providing a thread-safe way to get the current time in seconds for the beast library.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "thread_.joinable()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at seconds_clock_thread::~seconds_clock_thread", + "issue_pattern": "Missing empty string validation for thread_.joinable()", + "why_false_positive": "XRPL_ASSERT macro validates thread_.joinable() for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/clock/basic_seconds_clock.cpp", + "functions": [ + { + "args": [], + "lineno": 23, + "name": "~seconds_clock_thread" + }, + { + "args": [], + "lineno": 24, + "name": "seconds_clock_thread" + }, + { + "args": [], + "lineno": 26, + "name": "now" + }, + { + "args": [], + "lineno": 30, + "name": "run" + }, + { + "args": [], + "lineno": 36, + "name": "~seconds_clock_thread" + }, + { + "args": [], + "lineno": 46, + "name": "seconds_clock_thread" + }, + { + "args": [], + "lineno": 51, + "name": "now" + }, + { + "args": [], + "lineno": 56, + "name": "run" + }, + { + "args": [], + "lineno": 71, + "name": "now" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "beast" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage (no test code or test macros). The destructor's XRPL_ASSERT is only triggered if the thread is not joinable at destruction, which would require a test that constructs and destructs basic_seconds_clock or seconds_clock_thread. Likely, tests would exist in higher-level clock or timing tests, possibly in files like 'test/clock_test.cpp' or similar. There is no explicit test for the validation path (XRPL_ASSERT) in this file, and error paths (e.g., thread not joinable) may not be covered unless specifically tested for destructor edge cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro)", + "validation_layer": "business_logic (resource cleanup/destruction)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "thread_.joinable()", + "location": "seconds_clock_thread::~seconds_clock_thread", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that the internal thread is joinable before attempting to join it in the destructor" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/beast/clock/basic_seconds_clock.cpp.ai.md b/src/libxrpl/beast/clock/basic_seconds_clock.cpp.ai.md new file mode 100644 index 0000000000..23959ccbd7 --- /dev/null +++ b/src/libxrpl/beast/clock/basic_seconds_clock.cpp.ai.md @@ -0,0 +1,62 @@ +# `basic_seconds_clock.cpp` — Low-Cost Second-Resolution Clock + +## Purpose + +This file implements a single-writer / many-reader optimization for time queries in the XRPL node. The public surface is tiny: one static `now()` function on `basic_seconds_clock`. The point is not precision — it is throughput. Hot code paths (network message timestamping, cache expiry checks, ledger timing) may call `now()` thousands of times per second. Routing each of those calls through the OS via `std::chrono::steady_clock::now()` is unnecessary when the required resolution is only one second. This implementation pre-computes the time point on a background thread and lets callers read it with a single atomic load. + +## The `seconds_clock_thread` Internal Class + +The entire implementation lives in an anonymous namespace, hidden from all translation units. `seconds_clock_thread` owns three concurrency primitives: a `std::mutex`, a `std::condition_variable`, and a `std::thread`, plus an `std::atomic` called `tp_` that is the sole shared value exposed to callers. + +A compile-time `static_assert` verifies that `std::atomic` is always lock-free. This is critical: if it were not lock-free, reading `tp_` from calling threads could silently involve an internal mutex, undermining the entire performance rationale. + +### Construction and Initialization + +The constructor initialises `tp_` to `Clock::now().time_since_epoch().count()` before launching the background thread. This ensures that any call to `now()` between construction and the first loop iteration returns a valid, current timestamp rather than a zero-epoch value. + +### The `run()` Loop + +``` +auto now = Clock::now(); +tp_ = now.time_since_epoch().count(); +auto const when = floor(now) + 1s; +cv_.wait_until(lock, when, [this] { return stop_; }); +``` + +Each iteration of the loop records the current time atomically, then computes the next second boundary (`floor(now) + 1s`) and sleeps until that moment. This is more precise than a naive `sleep_for(1s)`: it synchronises wake-ups to wall-clock second boundaries, preventing drift from accumulated sleep overhead over long uptimes. + +The `wait_until` predicate checks `stop_`, so a shutdown notification always wakes the thread immediately without waiting for the next second to roll over. If the timeout fires naturally, the predicate returns `false` and the loop continues. + +### Shutdown Sequence + +```cpp +{ + std::lock_guard const lock(mut_); + stop_ = true; +} // release lock before notify +cv_.notify_one(); +thread_.join(); +``` + +The destructor acquires the mutex, sets `stop_`, then immediately releases the lock before calling `notify_one()`. The inline comment explains the intent: publish `stop_` as quickly as possible so that if the sleeping thread happens to time out and re-evaluate the predicate before receiving the notification, it still sees the flag and exits. Releasing the lock before notifying is a standard pattern that reduces unnecessary contention. `XRPL_ASSERT(thread_.joinable())` guards against double-destruction or a corrupted object state before the blocking `join()`. + +## The Public `now()` Function + +```cpp +basic_seconds_clock::time_point +basic_seconds_clock::now() +{ + static seconds_clock_thread clk; + return clk.now(); +} +``` + +The singleton is a function-local `static`, constructed on first call. C++11 guarantees this initialization is thread-safe, so multiple threads racing on the very first `now()` call will not create multiple threads. Once alive, every call reduces to `Clock::time_point{Clock::duration{tp_.load()}}` — a single lock-free load from an `int64_t` register with no kernel involvement. + +The staleness bound is at most one second: the background thread wakes up at each second boundary and stores the new value. For the ledger-timing, cache-expiry, and connection-age use cases that consume this clock (see `include/xrpl/basics/chrono.h`, which aliases `beast::basic_seconds_clock` as the `Clock` type for ledger operations), one-second granularity is entirely sufficient. + +## Design Trade-offs + +The obvious alternative — just calling `std::chrono::steady_clock::now()` directly — is also fast on modern Linux kernels via the VDSO (no context switch), but still requires a memory barrier and a 64-bit multiply/shift for nanosecond conversion. Under high call rates the difference accumulates. The background-thread pattern trades one extra thread and one extra second of maximum time lag for O(1) atomic reads with no arithmetic, which is the right trade-off for a server that may run millions of time queries per second. + +One subtle correctness property: `tp_` is written by exactly one thread (the clock thread) and read by arbitrarily many callers. The `std::atomic` store uses the default `memory_order_seq_cst`, which provides a sequentially consistent view — every reader that completes a load sees a value no older than the most recent completed store. \ No newline at end of file diff --git a/src/libxrpl/beast/core/CurrentThreadName.cpp.ai.json b/src/libxrpl/beast/core/CurrentThreadName.cpp.ai.json new file mode 100644 index 0000000000..8dc80da352 --- /dev/null +++ b/src/libxrpl/beast/core/CurrentThreadName.cpp.ai.json @@ -0,0 +1,173 @@ +{ + "args": [ + { + "lineno": 13, + "name": "name" + }, + { + "lineno": 44, + "name": "name" + }, + { + "lineno": 56, + "name": "name" + }, + { + "lineno": 86, + "name": "name" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "setCurrentThreadName", + "detail::setCurrentThreadNameImpl" + ], + "entry_point": "setCurrentThreadName(std::string_view name)", + "purpose": "Sets the current thread's name, both in a thread-local variable and via the OS-specific API.", + "validation_points": [ + "detail::setCurrentThreadNameImpl (on Linux): manual truncation of name using std::snprintf" + ] + } + ], + "data_flows": [ + { + "field": "name (thread name)", + "flow": [ + "setCurrentThreadName(name) receives input", + "detail::threadName = name (thread-local storage)", + "detail::setCurrentThreadNameImpl(name) called", + "OS-specific thread name API invoked (with possible transformation)" + ], + "origin": "Input parameter to setCurrentThreadName(std::string_view name)", + "transformations": [ + "On Linux: name is truncated to maxThreadNameLength using std::snprintf into boundedName", + "On Windows: name is passed as-is to THREADNAME_INFO.szName", + "On macOS: name.data() is passed as-is to pthread_setname_np" + ], + "validated_at": "detail::setCurrentThreadNameImpl (on Linux): name is truncated to maxThreadNameLength" + } + ], + "description": "Provides cross-platform utilities to set and get the current thread's name, with platform-specific implementations for Windows, macOS, and Linux.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "name (thread name)", + "empty", + "string", + "validation" + ], + "evidence": "manual truncation using std::snprintf at beast::detail::setCurrentThreadNameImpl (Linux implementation)", + "issue_pattern": "Missing empty string validation for name (thread name)", + "why_false_positive": "manual truncation using std::snprintf validates name (thread name) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "name (thread name)", + "range", + "bounds", + "validation" + ], + "evidence": "manual truncation using std::snprintf at beast::detail::setCurrentThreadNameImpl (Linux implementation)", + "issue_pattern": "Missing range validation for name (thread name)", + "why_false_positive": "manual truncation using std::snprintf validates name (thread name) range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/core/CurrentThreadName.cpp", + "functions": [ + { + "args": [ + "name" + ], + "lineno": 13, + "name": "setCurrentThreadNameImpl" + }, + { + "args": [ + "name" + ], + "lineno": 44, + "name": "setCurrentThreadNameImpl" + }, + { + "args": [ + "name" + ], + "lineno": 56, + "name": "setCurrentThreadNameImpl" + }, + { + "args": [], + "lineno": 81, + "name": "getCurrentThreadName" + }, + { + "args": [ + "name" + ], + "lineno": 86, + "name": "setCurrentThreadName" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "beast::detail" + }, + { + "lineno": 42, + "name": "beast::detail" + }, + { + "lineno": 54, + "name": "beast::detail" + }, + { + "lineno": 78, + "name": "beast" + }, + { + "lineno": 80, + "name": "beast::detail" + } + ], + "test_coverage_notes": "There is no evidence of direct test coverage in this file. The code is platform-specific and interacts with OS APIs, making it difficult to unit test directly. Validation (truncation) is only performed on Linux, and there is no explicit error handling or assertion for name length on other platforms. Test coverage would likely exist in higher-level integration or system tests that verify thread naming, but such tests are not referenced or visible here. Edge cases such as overly long names on non-Linux platforms are not validated or tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (name is truncated, warning logged if TRUNCATED_THREAD_NAME_LOGS is defined)", + "field": "name (thread name)", + "location": "beast::detail::setCurrentThreadNameImpl (Linux implementation)", + "validated_by": "manual truncation using std::snprintf", + "validates": [ + "Ensures thread name does not exceed maxThreadNameLength characters", + "Truncates name if too long" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/beast/core/CurrentThreadName.cpp.ai.md b/src/libxrpl/beast/core/CurrentThreadName.cpp.ai.md new file mode 100644 index 0000000000..92d3cd78da --- /dev/null +++ b/src/libxrpl/beast/core/CurrentThreadName.cpp.ai.md @@ -0,0 +1,39 @@ +# `beast/core/CurrentThreadName.cpp` — Cross-Platform Thread Naming + +## Purpose + +This file provides the implementation of thread naming utilities for the XRPL codebase. Thread naming is a debugging and observability primitive: once a name is set, it appears in debuggers (Visual Studio, GDB/LLDB), profilers, and OS-level tools (`top`, `htop`, `ps`, Activity Monitor). Without it, all XRPL threads would appear as anonymous worker threads, making production diagnostics and crash investigation significantly harder. + +The file lives in the Beast utility layer, a legacy dependency within rippled originally derived from the JUCE framework. It pairs tightly with `CurrentThreadName.h`, which declares the public API and the Linux-specific `maxThreadNameLength` constant. + +## Architecture: One Interface, Three OS Backends + +The design follows a classic platform-dispatch pattern: a single public API (`setCurrentThreadName` / `getCurrentThreadName`) delegates to a hidden `beast::detail::setCurrentThreadNameImpl` whose body is selected at compile time via `#if BOOST_OS_WINDOWS` / `#if BOOST_OS_MACOS` / `#if BOOST_OS_LINUX` guards using Boost.Predef macros. Because each platform block defines the same function name in `namespace beast::detail`, exactly one definition is compiled into any given build, keeping the linker happy without any vtable or function pointer overhead. + +## Thread-Local Name Storage + +Regardless of platform, the name is always stored in `thread_local std::string detail::threadName`. This matters because `getCurrentThreadName()` simply reads that variable — it never queries the OS. This design choice avoids the complexity and potential failure modes of reverse-querying the OS (e.g., `pthread_getname_np` on Linux, which requires a fixed-size buffer), and it means the name returned is always exactly what was passed to `setCurrentThreadName`, never a silently-truncated OS copy. The tradeoff is that names set by external tools or the OS itself (not through this API) are invisible to `getCurrentThreadName()`, a documented limitation in the header. + +## Platform-Specific Implementations + +**Windows** (debug + MSVC only): Uses the classic structured exception trick documented by Microsoft — raising exception `0x406d1388` with a `THREADNAME_INFO` payload. This works because the Visual Studio debugger intercepts that specific exception code and reads the thread name from the payload. The implementation is guarded by `#if DEBUG && BOOST_COMP_MSVC`, so it compiles away in release builds or under non-MSVC compilers. The `#pragma pack(push, 8)` ensures the struct layout matches what the debugger expects precisely. + +**macOS**: Calls `pthread_setname_np(name.data())` — the one-argument Darwin variant, which names only the calling thread (unlike the POSIX two-argument form). A clang-tidy suppression comment (`NOLINT(bugprone-suspicious-stringview-data-usage)`) is present because the linter flags `std::string_view::data()` as potentially non-null-terminated; here the safety invariant is enforced by the caller who always passes a null-terminated string (either a string literal or a `std::string`). + +**Linux**: The most defensive implementation. Linux enforces a hard kernel limit of 16 bytes (including null terminator) for thread names via `pthread_setname_np`. Names exceeding 15 characters cause `pthread_setname_np` to return `ERANGE` and silently fail to set the name at all. To prevent this, the implementation manually truncates using `std::snprintf` into a stack buffer of `maxThreadNameLength + 1` bytes before calling `pthread_setname_np(pthread_self(), boundedName)`. The two-argument form is needed on Linux (unlike macOS). If the optional `TRUNCATED_THREAD_NAME_LOGS` macro is defined at build time, a warning is emitted to `std::cerr` when truncation occurs, aiding developers who may not realize their thread names are being silently clipped. + +The header complements this at compile time: a template overload of `setCurrentThreadName` for `char const (&)[N]` adds a `static_assert(N <= maxThreadNameLength + 1, ...)`, catching oversized string literals at compile time rather than silently truncating at runtime. The runtime truncation in the `.cpp` handles the `std::string_view` overload where the length is not known at compile time. + +## Call Flow + +``` +setCurrentThreadName(name) + ├─ detail::threadName = name // thread-local store, always complete + └─ detail::setCurrentThreadNameImpl(name) // platform-specific OS notification +``` + +`getCurrentThreadName()` reads only from `detail::threadName` — it never calls the OS. + +## Test Coverage + +`beast_CurrentThreadName_test.cpp` verifies two key properties: (1) thread-local isolation — two concurrently-named threads retain their individual names without cross-contamination, and (2) Linux truncation correctness — `pthread_getname_np` is used to read back the kernel-visible name and confirm it matches the expected (possibly truncated) value. The test does not cover Windows at all, reflecting the difficulty of unit-testing the debugger-exception approach. \ No newline at end of file diff --git a/src/libxrpl/beast/core/SemanticVersion.cpp.ai.json b/src/libxrpl/beast/core/SemanticVersion.cpp.ai.json new file mode 100644 index 0000000000..f3ad972f11 --- /dev/null +++ b/src/libxrpl/beast/core/SemanticVersion.cpp.ai.json @@ -0,0 +1,428 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 101, + "name": "SemanticVersion" + } + ], + "code_paths": [ + { + "call_chain": [ + "SemanticVersion::SemanticVersion(std::string_view)", + "SemanticVersion::parse", + "chopUInt", + "chop", + "chopUInt", + "chop", + "chopUInt", + "chop", + "extract_identifiers", + "extract_identifier" + ], + "entry_point": "SemanticVersion::SemanticVersion(std::string_view version)", + "purpose": "Constructs a SemanticVersion from a version string, validating format and extracting major, minor, patch, prerelease, and build identifiers.", + "validation_points": [ + "SemanticVersion::parse (validates whitespace, structure)", + "chopUInt (validates numeric fields: major, minor, patch)", + "extract_identifiers/extract_identifier (validates prerelease/build identifiers)" + ] + }, + { + "call_chain": [ + "SemanticVersion::parse", + "chopUInt", + "chop", + "chopUInt", + "chop", + "chopUInt", + "chop", + "extract_identifiers", + "extract_identifier" + ], + "entry_point": "SemanticVersion::parse(std::string_view input)", + "purpose": "Parses and validates a version string, populating the SemanticVersion fields.", + "validation_points": [ + "Whitespace check at start", + "chopUInt (major, minor, patch)", + "extract_identifiers (prerelease/build)" + ] + }, + { + "call_chain": [ + "extract_identifiers", + "extract_identifier" + ], + "entry_point": "extract_identifiers", + "purpose": "Splits a dot-separated identifier list and validates each identifier.", + "validation_points": [ + "extract_identifier (validates each identifier: allowed chars, leading zeroes)" + ] + } + ], + "data_flows": [ + { + "field": "majorVersion", + "flow": [ + "version string", + "SemanticVersion::parse", + "chopUInt (extracts and validates)", + "majorVersion field" + ], + "origin": "Input version string (constructor or parse)", + "transformations": [ + "String to int conversion", + "Validation: numeric, no leading zeroes, in range" + ], + "validated_at": "chopUInt" + }, + { + "field": "minorVersion", + "flow": [ + "version string", + "SemanticVersion::parse", + "chopUInt (extracts and validates)", + "minorVersion field" + ], + "origin": "Input version string", + "transformations": [ + "String to int conversion", + "Validation: numeric, no leading zeroes, in range" + ], + "validated_at": "chopUInt" + }, + { + "field": "patchVersion", + "flow": [ + "version string", + "SemanticVersion::parse", + "chopUInt (extracts and validates)", + "patchVersion field" + ], + "origin": "Input version string", + "transformations": [ + "String to int conversion", + "Validation: numeric, no leading zeroes, in range" + ], + "validated_at": "chopUInt" + }, + { + "field": "prerelease", + "flow": [ + "version string", + "SemanticVersion::parse", + "chop('-')", + "extract_identifiers (splits and validates)", + "prerelease field" + ], + "origin": "Input version string (after '-')", + "transformations": [ + "String split on '.'", + "Validation: allowed chars, no leading zeroes (unless allowed)" + ], + "validated_at": "extract_identifiers/extract_identifier" + }, + { + "field": "build", + "flow": [ + "version string", + "SemanticVersion::parse", + "chop('+')", + "extract_identifiers (splits and validates)", + "build field" + ], + "origin": "Input version string (after '+')", + "transformations": [ + "String split on '.'", + "Validation: allowed chars, leading zeroes allowed" + ], + "validated_at": "extract_identifiers/extract_identifier" + } + ], + "description": "Implements parsing, printing, and comparison logic for semantic version strings according to the Semantic Versioning specification, including handling of pre-release and metadata identifiers.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "version string (constructor input)", + "empty", + "string", + "validation" + ], + "evidence": "SemanticVersion::parse at SemanticVersion(std::string_view version) constructor", + "issue_pattern": "Missing empty string validation for version string (constructor input)", + "why_false_positive": "SemanticVersion::parse validates version string (constructor input) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "version string (constructor input)", + "format", + "validation", + "invalid" + ], + "evidence": "SemanticVersion::parse at SemanticVersion(std::string_view version) constructor", + "issue_pattern": "Missing format validation for version string (constructor input)", + "why_false_positive": "SemanticVersion::parse validates version string (constructor input) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "numeric identifier (string)", + "empty", + "string", + "validation" + ], + "evidence": "isNumeric at isNumeric", + "issue_pattern": "Missing empty string validation for numeric identifier (string)", + "why_false_positive": "isNumeric validates numeric identifier (string) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "input string for unsigned integer", + "empty", + "string", + "validation" + ], + "evidence": "chopUInt at chopUInt", + "issue_pattern": "Missing empty string validation for input string for unsigned integer", + "why_false_positive": "chopUInt validates input string for unsigned integer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "identifier (string)", + "empty", + "string", + "validation" + ], + "evidence": "extract_identifier at extract_identifier", + "issue_pattern": "Missing empty string validation for identifier (string)", + "why_false_positive": "extract_identifier validates identifier (string) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "identifier list (string)", + "empty", + "string", + "validation" + ], + "evidence": "extract_identifiers at extract_identifiers", + "issue_pattern": "Missing empty string validation for identifier list (string)", + "why_false_positive": "extract_identifiers validates identifier list (string) for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/core/SemanticVersion.cpp", + "functions": [ + { + "args": [ + "SemanticVersion::identifier_list const& list" + ], + "lineno": 8, + "name": "print_identifiers" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 19, + "name": "isNumeric" + }, + { + "args": [ + "std::string const& what", + "std::string& input" + ], + "lineno": 29, + "name": "chop" + }, + { + "args": [ + "int& value", + "int limit", + "std::string& input" + ], + "lineno": 38, + "name": "chopUInt" + }, + { + "args": [ + "std::string& value", + "bool allowLeadingZeroes", + "std::string& input" + ], + "lineno": 65, + "name": "extract_identifier" + }, + { + "args": [ + "SemanticVersion::identifier_list& identifiers", + "bool allowLeadingZeroes", + "std::string& input" + ], + "lineno": 83, + "name": "extract_identifiers" + }, + { + "args": [ + "SemanticVersion const& lhs", + "SemanticVersion const& rhs" + ], + "lineno": 154, + "name": "compare" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ], + "test_coverage_notes": "Typical test coverage for SemanticVersion would be in files like SemanticVersion_test.cpp or similar. Tests should cover valid/invalid version strings, edge cases (leading zeroes, empty identifiers, invalid characters, out-of-range numbers), and exception throwing. Gaps may exist if tests do not cover all invalid input cases (e.g., whitespace, numeric overflow, invalid identifier chars, empty prerelease/build). No test files are shown here, so actual coverage cannot be confirmed.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation functions (no external framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::invalid_argument", + "field": "version string (constructor input)", + "location": "SemanticVersion(std::string_view version) constructor", + "validated_by": "SemanticVersion::parse", + "validates": [ + "Checks if the version string is valid according to SemanticVersion::parse", + "Throws if parse returns false" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (caller may throw)", + "field": "numeric identifier (string)", + "location": "isNumeric", + "validated_by": "isNumeric", + "validates": [ + "Checks if string is convertible to integer", + "Checks for no leading zeroes" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (caller may throw)", + "field": "input string for unsigned integer", + "location": "chopUInt", + "validated_by": "chopUInt", + "validates": [ + "Input not empty", + "Extracts leading digits", + "Digits must be convertible to integer", + "No leading zeroes", + "Value in [0, limit]" + ], + "validation_type": "type|format|range" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (caller may throw)", + "field": "identifier (string)", + "location": "extract_identifier", + "validated_by": "extract_identifier", + "validates": [ + "Input not empty", + "No leading zero unless allowed", + "Only [a-zA-Z0-9-] allowed", + "Identifier not empty" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (caller may throw)", + "field": "identifier list (string)", + "location": "extract_identifiers", + "validated_by": "extract_identifiers", + "validates": [ + "Input not empty", + "Each identifier validated by extract_identifier", + "Identifiers separated by '.'" + ], + "validation_type": "format|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/beast/core/SemanticVersion.cpp.ai.md b/src/libxrpl/beast/core/SemanticVersion.cpp.ai.md new file mode 100644 index 0000000000..70969fb275 --- /dev/null +++ b/src/libxrpl/beast/core/SemanticVersion.cpp.ai.md @@ -0,0 +1,36 @@ +# `SemanticVersion.cpp` — SemVer Parsing, Printing, and Comparison + +This file implements `beast::SemanticVersion`, providing a strict, spec-compliant parser and comparator for [Semantic Versioning 2.0](https://semver.org/) strings. Its primary role in the XRPL codebase is version identification — protocol handshakes, feature negotiation, and software build tagging all rely on well-defined version ordering. The implementation deliberately rejects any input that deviates from the specification, including forms that humans might consider "obviously equivalent" like `01.2.3` or `1.2.3 `. + +## Parsing Architecture: Destructive Consumption + +The parsing strategy is to copy the input into a mutable `std::string`, then consume it left-to-right with a family of "chop" helpers that remove tokens from the front of the string as they recognize them. This approach is simple, side-effect-free from the caller's perspective, and makes the success condition in `parse()` trivially expressible: the string must be fully empty at the end. Any leftover characters mean the input contained something the parser didn't recognize. + +`chopUInt()` handles the three integer fields. It scans leading digits, converts them via `lexicalCastChecked`, then enforces two invariants beyond simple conversion: the value must not have leading zeroes (checked by round-tripping through `std::to_string`), and it must fall within a provided upper bound. The round-trip check is the idiomatic way to detect leading zeroes without explicit string inspection — `lexicalCastChecked("01", n)` succeeds with `n=1`, but `std::to_string(1) != "01"` catches the problem. + +`chop()` is a simple string-prefix consumer — it returns `true` only if the input begins with the exact literal being chopped, and removes it if so. This is used to consume the `.` separators between version numbers and the `-` / `+` sigils that introduce pre-release and build metadata sections. + +`extract_identifier()` reads a single alphanumeric+hyphen token from the input front. The character allowlist is hardcoded as the exact set permitted by SemVer: `[a-zA-Z0-9-]`. A notable design point is `allowLeadingZeroes`: it is `false` for pre-release identifiers (per spec: `0.1` is legal as pre-release but `01` is not) and `true` for build metadata (the spec places no constraint on metadata content beyond character validity). This asymmetry is enforced directly through the `extract_identifiers` call sites in `parse()`. + +`parse()` accepts `std::string_view` for zero-copy call sites, but immediately converts to `std::string` for mutable processing. The leading/trailing whitespace check is performed both by scanning to find the trimmed region and by comparing the result back to the input — if they differ, the version string had whitespace and is rejected. + +## Two-Level Error Reporting + +The design separates the failure mechanism between `parse()` and the constructing overload. `parse()` returns `bool`, allowing callers to probe validity without exceptions. The `SemanticVersion(std::string_view)` constructor calls `parse()` and converts `false` to a `std::invalid_argument` throw. This two-tier design mirrors the pattern used elsewhere in beast utilities — contexts that control their input (e.g., config loading) can call `parse()` and handle failures gracefully, while contexts where an invalid version string is a programming error can use the constructor and let the exception propagate. + +## Comparison: Faithful SemVer Precedence + +`compare()` returns an integer in the style of `strcmp`, with the full relational operator suite defined inline in the header by delegating to it. The comparison sequence follows the specification exactly: + +1. **Major, minor, patch** are compared numerically in order. +2. **Pre-release vs. release**: when the numeric cores are equal, a release version (empty `preReleaseIdentifiers`) always outranks a pre-release. This is the SemVer rule that `1.0.0` > `1.0.0-rc.1`. +3. **Pre-release identifier lists** are compared element by element. Purely numeric identifiers are compared as integers; mixed or all-alpha identifiers are compared lexicographically. When one side's identifier list is exhausted before the other, the longer list wins. `XRPL_ASSERT` macros guard the points where the code assumes both sides are either both numeric or both non-numeric — these fire only if `isNumeric` and the comparison branching above disagree, which would indicate a logic bug. +4. **Build metadata is ignored entirely.** The spec mandates this, and the test suite explicitly verifies it by confirming that `checkLess("1.0.0-alpha", "1.0.0-alpha.1")` holds regardless of whether `+meta` is appended to either or both sides. + +## Relationship to Other Files + +The header `SemanticVersion.h` declares the `SemanticVersion` struct with public integer fields (`majorVersion`, `minorVersion`, `patchVersion`) and `identifier_list` vectors for pre-release and metadata. The struct has no private state — it is a plain aggregate with a parser. The six relational operators are all inline in the header, each a one-liner forwarding to `compare()`. This means callers only need to link one function symbol while getting a full ordered-comparison interface. + +`LexicalCast.h` provides `lexicalCastChecked` (try-cast returning bool) and `lexicalCastThrow` (cast or throw), used for integer parsing in `chopUInt` and identifier numeric comparison in `compare()` respectively. Using `lexicalCastThrow` in `compare()` is safe because the numeric classification was already established by `isNumeric()` before the cast attempt, and `XRPL_ASSERT` guards the logical consistency of that classification. + +The test file `SemanticVersion_test.cpp` exercises parsing at multiple levels of composition: individual pass/fail checks, decomposition verification that parsed fields match expected values, and comparison tests against the canonical precedence chain from the SemVer spec (`1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < ... < 1.0.0`). The test also verifies the metadata-independence of all comparison results by re-running each comparison with `+meta` suffixes appended. \ No newline at end of file diff --git a/src/libxrpl/beast/insight/Collector.cpp.ai.json b/src/libxrpl/beast/insight/Collector.cpp.ai.json new file mode 100644 index 0000000000..e117432888 --- /dev/null +++ b/src/libxrpl/beast/insight/Collector.cpp.ai.json @@ -0,0 +1,58 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "beast::insight::Collector::~Collector" + ], + "entry_point": "beast::insight::Collector::~Collector", + "purpose": "Destructor for the Collector class; cleans up resources when a Collector object is destroyed.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "N/A", + "flow": [], + "origin": "N/A", + "transformations": [], + "validated_at": "N/A" + } + ], + "description": "Defines the destructor implementation for the Collector class within the beast::insight namespace.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/insight/Collector.cpp", + "functions": [ + { + "args": [], + "lineno": 6, + "name": "~Collector" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "beast" + }, + { + "lineno": 4, + "name": "insight" + } + ], + "test_coverage_notes": "The provided code only contains the default destructor for the Collector class, with no logic, validation, or data manipulation. There are no validation code paths or data flows present in this snippet. Test coverage for this destructor would be implicit via object lifecycle tests, but no explicit validation or data handling is present to test. Any validation or data flow would occur in other methods of Collector or its subclasses, not shown here.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/beast/insight/Collector.cpp.ai.md b/src/libxrpl/beast/insight/Collector.cpp.ai.md new file mode 100644 index 0000000000..fbae29f66b --- /dev/null +++ b/src/libxrpl/beast/insight/Collector.cpp.ai.md @@ -0,0 +1,7 @@ +# `beast/insight/Collector.cpp` + +This file provides the out-of-line definition of `Collector::~Collector()`, the pure-virtual destructor for the abstract `Collector` interface in the `beast::insight` metrics subsystem. + +The single line `Collector::~Destructor() = default;` exists for a non-obvious reason: in C++, a pure virtual destructor (declared `= 0` in the header) must still have an external definition because destructors always form a chain — when a derived class is destroyed, the compiler emits a call to every base class destructor in turn. Without this definition the linker would fail on any translation unit that destroys a `Collector`-derived object. Emitting it as `= default` in the `.cpp` rather than inline in the header keeps the vtable anchored to a single translation unit, avoiding duplicate-symbol and ODR issues that arise when a class with virtual functions is defined entirely in a header. + +The `Collector` class itself is the root abstraction for metrics collection in the XRPL node. Callers obtain a `std::shared_ptr` and call its `make_counter`, `make_gauge`, `make_event`, `make_meter`, and `make_hook` factory methods to register named metrics. Two concrete implementations exist: `NullCollector`, which silently discards all metrics (useful in tests or when telemetry is disabled), and `StatsDCollector`, which streams metrics over UDP to a StatsD-compatible backend. \ No newline at end of file diff --git a/src/libxrpl/beast/insight/Groups.cpp.ai.json b/src/libxrpl/beast/insight/Groups.cpp.ai.json new file mode 100644 index 0000000000..fd74578a52 --- /dev/null +++ b/src/libxrpl/beast/insight/Groups.cpp.ai.json @@ -0,0 +1,358 @@ +{ + "args": [ + { + "lineno": 18, + "name": "name_" + }, + { + "lineno": 18, + "name": "collector" + }, + { + "lineno": 30, + "name": "name" + }, + { + "lineno": 35, + "name": "handler" + }, + { + "lineno": 40, + "name": "name" + }, + { + "lineno": 45, + "name": "name" + }, + { + "lineno": 50, + "name": "name" + }, + { + "lineno": 55, + "name": "name" + }, + { + "lineno": 67, + "name": "collector" + }, + { + "lineno": 73, + "name": "name" + }, + { + "lineno": 89, + "name": "collector" + } + ], + "classes": [ + { + "args": [ + "std::string const& name_", + "Collector::ptr const& collector" + ], + "lineno": 13, + "name": "GroupImp" + }, + { + "args": [ + "Collector::ptr const& collector" + ], + "lineno": 61, + "name": "GroupsImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "GroupsImp::get", + "GroupImp::GroupImp (constructor)", + "GroupImp::make_counter / make_event / make_gauge / make_meter / make_hook", + "Collector::make_counter / make_event / make_gauge / make_meter / make_hook" + ], + "entry_point": "GroupsImp::get", + "purpose": "Retrieves or creates a Group by name, then allows creation of metrics (counter, event, gauge, meter, hook) within that group.", + "validation_points": [ + "GroupsImp::get: Checks if group exists in m_items (unordered_map); if not, creates new GroupImp.", + "GroupImp::make_*: No explicit validation of metric names; simply concatenates group name and metric name.", + "Collector::make_*: Any further validation would occur here, but not shown in this file." + ] + }, + { + "call_chain": [ + "make_Groups", + "GroupsImp::GroupsImp (constructor)" + ], + "entry_point": "make_Groups", + "purpose": "Factory function to create a Groups instance with a given Collector.", + "validation_points": [ + "No explicit validation in this chain." + ] + } + ], + "data_flows": [ + { + "field": "name (Group name)", + "flow": [ + "GroupsImp::get (input parameter)", + "m_items (unordered_map key)", + "GroupImp::GroupImp (constructor parameter)", + "GroupImp::m_name (member variable)", + "GroupImp::make_name (concatenation with metric name)", + "Collector::make_* (final usage)" + ], + "origin": "Parameter to GroupsImp::get", + "transformations": [ + "Concatenation: Group name + '.' + metric name in make_name" + ], + "validated_at": "No explicit validation; relies on unordered_map and string concatenation" + }, + { + "field": "collector (Collector::ptr)", + "flow": [ + "make_Groups (input parameter)", + "GroupsImp::GroupsImp (constructor parameter)", + "GroupsImp::m_collector (member variable)", + "GroupImp::GroupImp (constructor parameter)", + "GroupImp::m_collector (member variable)", + "GroupImp::make_* (calls m_collector->make_*)" + ], + "origin": "Parameter to GroupsImp constructor (from make_Groups)", + "transformations": [ + "Passed through as shared_ptr, no transformation" + ], + "validated_at": "No explicit validation; assumes valid Collector::ptr" + }, + { + "field": "metric name (e.g., for counter/event)", + "flow": [ + "GroupImp::make_counter (input parameter)", + "GroupImp::make_name (concatenation with m_name)", + "Collector::make_counter (final usage)" + ], + "origin": "Parameter to GroupImp::make_counter/make_event/etc.", + "transformations": [ + "Concatenation: Group name + '.' + metric name" + ], + "validated_at": "No explicit validation; assumes valid string" + } + ], + "description": "This file implements the insight metrics grouping system for the Beast library, providing classes and functions to create and manage metric groups (Groups, Group) and their associated metrics (Counter, Event, Gauge, Meter, Hook) using a Collector. It includes concrete implementations for Groups and Group, and a factory function to create Groups instances.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/insight/Groups.cpp", + "functions": [ + { + "args": [ + "std::string const& name_", + "Collector::ptr const& collector" + ], + "lineno": 18, + "name": "GroupImp::GroupImp" + }, + { + "args": [], + "lineno": 23, + "name": "GroupImp::~GroupImp" + }, + { + "args": [], + "lineno": 25, + "name": "GroupImp::name" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 30, + "name": "GroupImp::make_name" + }, + { + "args": [ + "HookImpl::HandlerType const& handler" + ], + "lineno": 35, + "name": "GroupImp::make_hook" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 40, + "name": "GroupImp::make_counter" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 45, + "name": "GroupImp::make_event" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 50, + "name": "GroupImp::make_gauge" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 55, + "name": "GroupImp::make_meter" + }, + { + "args": [ + "Collector::ptr const& collector" + ], + "lineno": 67, + "name": "GroupsImp::GroupsImp" + }, + { + "args": [], + "lineno": 71, + "name": "GroupsImp::~GroupsImp" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 73, + "name": "GroupsImp::get" + }, + { + "args": [], + "lineno": 87, + "name": "Groups::~Groups" + }, + { + "args": [ + "Collector::ptr const& collector" + ], + "lineno": 89, + "name": "make_Groups" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "beast" + }, + { + "lineno": 12, + "name": "insight" + }, + { + "lineno": 15, + "name": "detail" + } + ], + "test_coverage_notes": "This file contains only implementation, not tests. Validation is minimal: there is no explicit checking of group or metric names (e.g., for emptiness, invalid characters, or duplicates beyond unordered_map semantics). Any validation of metric names or collector pointers would need to occur in Collector or higher-level code. Test coverage would likely be in files testing beast::insight::Groups, Group, and Collector, but this file itself does not contain or reference tests. Gaps: No validation of input parameters, no error handling for invalid collector pointers, and no checks for malformed names.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/beast/insight/Groups.cpp.ai.md b/src/libxrpl/beast/insight/Groups.cpp.ai.md new file mode 100644 index 0000000000..a6afa28f9d --- /dev/null +++ b/src/libxrpl/beast/insight/Groups.cpp.ai.md @@ -0,0 +1,72 @@ +# `beast/insight/Groups.cpp` — Metric Group Registry and Namespaced Collector Facade + +## Role in the System + +The beast `insight` subsystem provides an abstraction layer for exporting runtime telemetry — counters, gauges, events, meters, and polling hooks — to backends such as StatsD or a null sink. `Groups.cpp` implements the grouping layer on top of this: a registry that maps logical names to scoped `Collector` facades, so that different subsystems of the node can each register metrics under their own dot-separated namespace prefix without knowing about or conflicting with one another. + +## Key Design: A Group IS a Collector + +The central design decision is that `Group` inherits from `Collector` rather than merely containing one. Defined in `Group.h`: + +```cpp +class Group : public Collector { +public: + using ptr = std::shared_ptr; + virtual std::string const& name() const = 0; +}; +``` + +This means a `Group::ptr` satisfies any interface that accepts a `Collector::ptr`. Code that wants metrics can be handed a group directly — it never needs to know whether it is talking to a root collector or a namespaced sub-collector. The grouping is completely transparent to metric consumers. + +## `GroupImp` — The Prefix-Injecting Facade + +`GroupImp` is the concrete `Group` implementation. It holds two members: the group's name string (`m_name`) and a `shared_ptr` to the underlying real `Collector`. Every `make_*` call intercepts the metric name, prepends `m_name + "."`, then delegates to the underlying collector: + +```cpp +std::string make_name(std::string const& name) { + return m_name + "." + name; +} + +Counter make_counter(std::string const& name) override { + return m_collector->make_counter(make_name(name)); +} +``` + +The effect is automatic namespacing. If a group is named `"ledger"` and a component asks for a counter called `"validations"`, the underlying collector sees `"ledger.validations"`. The caller never constructs this path manually. This matches StatsD's conventional dot-separated hierarchy, and it means metric names are guaranteed to be consistent: there is no way for a consumer to accidentally omit the prefix. + +`make_hook()` is the notable exception — it does not apply `make_name()`. Hooks are polling callbacks rather than named time-series, so they carry no name to prefix. The hook is forwarded verbatim to the underlying collector. + +`GroupImp` inherits `std::enable_shared_from_this` but does not call `shared_from_this()` directly in this file — the pattern is present for potential future use or for derived classes. + +## `GroupsImp` — Lazy-Creating Registry + +`GroupsImp` owns the group registry as an `unordered_map, uhash<>>`. The `uhash<>` type is beast's universal hash adapter, which here resolves to a standard string hasher. + +The `get()` method implements a find-or-create pattern using `emplace()`: + +```cpp +Group::ptr const& get(std::string const& name) override { + std::pair const result( + m_items.emplace(name, Group::ptr())); + Group::ptr& group(result.first->second); + if (result.second) + group = std::make_shared(name, m_collector); + return group; +} +``` + +`emplace()` inserts a null `Group::ptr` as a placeholder only if the key is new (`result.second == true`), then immediately replaces it with a real `GroupImp`. If the key already existed, the existing group is returned untouched. The method returns a `const&` to the stored `shared_ptr`, so the caller receives a stable reference into the map — valid as long as `GroupsImp` is alive and the map is not rehashed. Because `unordered_map` does not invalidate references on insertion (only on rehashing), this reference stability holds unless the map rehashes under a concurrent insertion — which points to the thread-safety concern below. + +## Lifetime and Ownership + +Both `GroupsImp` and each `GroupImp` independently hold a `Collector::ptr` (`shared_ptr`). This means the underlying collector's lifetime is bounded by the last reference among the groups registry and all individual groups. If a caller retains a `Group::ptr` after destroying the `Groups` container, it will continue to work correctly because both hold their own reference to the collector. There is no dangling pointer risk. + +The `make_Groups()` factory returns `std::unique_ptr`, giving the caller exclusive ownership of the registry itself. + +## Thread Safety + +Neither `GroupsImp` nor `GroupImp` provides any locking. Concurrent calls to `get()` with new group names would race on the `unordered_map`, and concurrent metric-creation calls on distinct `GroupImp` instances are safe only if the underlying `Collector` implementation is thread-safe. In practice, callers are expected to create groups during initialization on a single thread, not during hot-path concurrent operation. + +## Summary + +`Groups.cpp` is a small but structurally important file. It provides the metric namespace registry for the insight subsystem via two concrete types hidden in `namespace detail`: `GroupImp`, which wraps a `Collector` and transparently prefixes all metric names, and `GroupsImp`, which caches those wrappers by name. The `make_Groups()` factory is the sole public entry point. The design prioritizes simplicity and composability — because `Group` is a `Collector`, the grouping mechanism is invisible to any code that simply holds a `Collector::ptr`, making it easy to add or change grouping structure without touching metric consumers. \ No newline at end of file diff --git a/src/libxrpl/beast/insight/Hook.cpp.ai.json b/src/libxrpl/beast/insight/Hook.cpp.ai.json new file mode 100644 index 0000000000..c9e9ca2657 --- /dev/null +++ b/src/libxrpl/beast/insight/Hook.cpp.ai.json @@ -0,0 +1,50 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [], + "entry_point": "N/A (no functions defined in this file)", + "purpose": "This file only defines the destructor for HookImpl; no validation or functional logic is present.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "N/A", + "flow": [], + "origin": "N/A", + "transformations": [], + "validated_at": "N/A" + } + ], + "description": "This file provides the destructor implementation for the HookImpl class in the beast::insight namespace.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/insight/Hook.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "beast" + }, + { + "lineno": 5, + "name": "insight" + } + ], + "test_coverage_notes": "This file contains only the destructor definition for HookImpl and does not implement any logic, validation, or data handling. There are no validation code paths or data flows to test. Any validation or data flow would occur in the implementation files for Hook or HookImpl, not in this file. Test coverage for this file is not applicable; relevant tests would exist for the actual Hook/HookImpl logic elsewhere.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/beast/insight/Hook.cpp.ai.md b/src/libxrpl/beast/insight/Hook.cpp.ai.md new file mode 100644 index 0000000000..2aa7bbe8e7 --- /dev/null +++ b/src/libxrpl/beast/insight/Hook.cpp.ai.md @@ -0,0 +1,9 @@ +# `Hook.cpp` — HookImpl Pure Virtual Destructor + +`Hook.cpp` is a minimal translation unit within the `beast::insight` metrics subsystem, containing exactly one definition: the out-of-line destructor body for `HookImpl`. + +`HookImpl` is the abstract base class for polled-collection hooks — callbacks invoked at each metrics collection interval. It is declared in `HookImpl.h` with a pure virtual destructor (`virtual ~HookImpl() = 0`) and a `HandlerType` alias (`std::function`) that concrete subclasses use to store the user-supplied callback. + +The reason this file exists at all is a C++ subtlety: even though the destructor is declared pure virtual (forcing subclasses to override it), the base destructor is always invoked as part of the destruction chain, so it must have a definition. Placing `= default` here in the `.cpp` rather than inline in the header keeps the vtable and destructor body anchored to a single translation unit, which is the idiomatic pattern for abstract base classes. + +Concrete implementations — `StatsDHookImpl` in `StatsDCollector.cpp` and `NullHookImpl` in `NullCollector.cpp` — inherit from `HookImpl` and register themselves with the collector's polling loop. The user-facing `Hook` class in `Hook.h` is simply a `shared_ptr` wrapper; lifetime of the hook registration is tied directly to the lifetime of that shared pointer. \ No newline at end of file diff --git a/src/libxrpl/beast/insight/Metric.cpp.ai.json b/src/libxrpl/beast/insight/Metric.cpp.ai.json new file mode 100644 index 0000000000..1a70529fb0 --- /dev/null +++ b/src/libxrpl/beast/insight/Metric.cpp.ai.json @@ -0,0 +1,142 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "~CounterImpl" + ], + "entry_point": "~CounterImpl", + "purpose": "Destructor for CounterImpl; cleans up resources associated with a Counter metric.", + "validation_points": [] + }, + { + "call_chain": [ + "~EventImpl" + ], + "entry_point": "~EventImpl", + "purpose": "Destructor for EventImpl; cleans up resources associated with an Event metric.", + "validation_points": [] + }, + { + "call_chain": [ + "~GaugeImpl" + ], + "entry_point": "~GaugeImpl", + "purpose": "Destructor for GaugeImpl; cleans up resources associated with a Gauge metric.", + "validation_points": [] + }, + { + "call_chain": [ + "~MeterImpl" + ], + "entry_point": "~MeterImpl", + "purpose": "Destructor for MeterImpl; cleans up resources associated with a Meter metric.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "CounterImpl internal state", + "flow": [ + "CounterImpl constructor", + "CounterImpl usage", + "~CounterImpl" + ], + "origin": "Constructed elsewhere (not in this file)", + "transformations": [ + "May be incremented/decremented during usage; destroyed in destructor" + ], + "validated_at": null + }, + { + "field": "EventImpl internal state", + "flow": [ + "EventImpl constructor", + "EventImpl usage", + "~EventImpl" + ], + "origin": "Constructed elsewhere (not in this file)", + "transformations": [ + "May be updated during event recording; destroyed in destructor" + ], + "validated_at": null + }, + { + "field": "GaugeImpl internal state", + "flow": [ + "GaugeImpl constructor", + "GaugeImpl usage", + "~GaugeImpl" + ], + "origin": "Constructed elsewhere (not in this file)", + "transformations": [ + "May be set/updated during usage; destroyed in destructor" + ], + "validated_at": null + }, + { + "field": "MeterImpl internal state", + "flow": [ + "MeterImpl constructor", + "MeterImpl usage", + "~MeterImpl" + ], + "origin": "Constructed elsewhere (not in this file)", + "transformations": [ + "May be updated during metering; destroyed in destructor" + ], + "validated_at": null + } + ], + "description": "This file defines default destructors for the CounterImpl, EventImpl, GaugeImpl, and MeterImpl classes within the beast::insight namespace.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/insight/Metric.cpp", + "functions": [ + { + "args": [], + "lineno": 7, + "name": "~CounterImpl" + }, + { + "args": [], + "lineno": 9, + "name": "~EventImpl" + }, + { + "args": [], + "lineno": 11, + "name": "~GaugeImpl" + }, + { + "args": [], + "lineno": 13, + "name": "~MeterImpl" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "beast" + }, + { + "lineno": 6, + "name": "insight" + } + ], + "test_coverage_notes": "This file only contains defaulted destructors for metric implementation classes. There are no validation code paths or data transformations in this file. Validation, if any, would occur in the constructors or methods of CounterImpl, EventImpl, GaugeImpl, or MeterImpl, which are not present here. Test coverage for this file specifically is likely minimal or non-existent, as destructors with default implementations are rarely directly tested. Tests for metric behavior would be found in tests for the insight metrics subsystem, possibly in files like 'CounterImpl_test.cpp', 'EventImpl_test.cpp', etc., if they exist. No validation logic is present in this file, so there are no validation code paths or data flows to test here.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/beast/insight/Metric.cpp.ai.md b/src/libxrpl/beast/insight/Metric.cpp.ai.md new file mode 100644 index 0000000000..cbe442f80d --- /dev/null +++ b/src/libxrpl/beast/insight/Metric.cpp.ai.md @@ -0,0 +1,26 @@ +# `beast/insight/Metric.cpp` + +This file exists to satisfy a subtle but mandatory C++ requirement: **pure virtual destructors must have an out-of-line definition**. Its entire body is four defaulted destructor definitions — one each for `CounterImpl`, `EventImpl`, `GaugeImpl`, and `MeterImpl`. + +## Why This File Exists + +Each of the four metric implementation abstract base classes declares its destructor as pure virtual (`virtual ~Foo() = 0`). In C++, declaring a destructor pure virtual is a common technique for making a class abstract while still keeping it as the designated polymorphic base — but unlike other pure virtual functions, the destructor is always called implicitly by every derived class destructor. This means the linker will fail to resolve the symbol unless a body is provided somewhere. `Metric.cpp` is that somewhere. + +The alternative — defining each destructor inline in its own header — would technically work but would scatter the definitions across four files with no clear home. Consolidating them here makes the intent explicit: this single translation unit is the canonical anchor for all metric `Impl` destructor bodies. + +## The `beast::insight` Metric Hierarchy + +The four classes grounded here form the abstract interface layer for the `beast::insight` telemetry system: + +- **`CounterImpl`** — represents an integer metric that can be incremented or decremented (`value_type = int64_t`). The public-facing `Counter` class holds a `shared_ptr` and delegates all operations through it. +- **`GaugeImpl`** — represents an absolute value metric that can be set to a specific `uint64_t` or adjusted by a signed delta. Corresponds to the StatsD gauge concept. +- **`EventImpl`** — represents a timing metric, where `value_type = std::chrono::milliseconds`. Used to record discrete durations. +- **`MeterImpl`** — represents a rate metric, incrementable by `uint64_t` amounts. Analogous to a StatsD counter intended for throughput measurement. + +All four inherit from `std::enable_shared_from_this`, enabling implementations to safely produce `shared_ptr` handles to themselves — necessary because the public metric wrappers (`Counter`, `Gauge`, etc.) are lightweight reference types that hold the `shared_ptr` directly. When the last `Counter` referencing a `CounterImpl` is destroyed, the impl is destroyed too, automatically de-registering the metric. + +## Relationship to Collector and Implementations + +The `Collector` interface (in `Collector.h`) is the factory: callers invoke `make_counter()`, `make_gauge()`, etc. to receive the public wrapper types. Concrete backends — `StatsDCollector` (for live reporting over UDP) and `NullCollector` (a no-op sink) — implement `Collector` and return their own subclasses of `CounterImpl`, `GaugeImpl`, `EventImpl`, and `MeterImpl`. The `= default` destructors defined here in `Metric.cpp` are the base-class destructor entries through which all those concrete impl destructors will chain, regardless of which backend is active. + +The design enforces a clean separation: the public metric types (`Counter`, `Gauge`, etc.) are pure value handles with no knowledge of the backend; the `Impl` classes define the operations backends must support; and `Metric.cpp` provides the minimal glue that lets the compiler and linker close the loop on the virtual dispatch chain. \ No newline at end of file diff --git a/src/libxrpl/beast/insight/NullCollector.cpp.ai.json b/src/libxrpl/beast/insight/NullCollector.cpp.ai.json new file mode 100644 index 0000000000..5f673cd100 --- /dev/null +++ b/src/libxrpl/beast/insight/NullCollector.cpp.ai.json @@ -0,0 +1,303 @@ +{ + "args": [ + { + "lineno": 25, + "name": "value_type" + }, + { + "lineno": 41, + "name": "value_type const&" + }, + { + "lineno": 57, + "name": "value_type" + }, + { + "lineno": 61, + "name": "difference_type" + }, + { + "lineno": 77, + "name": "value_type" + }, + { + "lineno": 88, + "name": "HookImpl::HandlerType const&" + }, + { + "lineno": 93, + "name": "std::string const&" + }, + { + "lineno": 98, + "name": "std::string const&" + }, + { + "lineno": 103, + "name": "std::string const&" + }, + { + "lineno": 108, + "name": "std::string const&" + } + ], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "NullHookImpl" + }, + { + "args": [], + "lineno": 22, + "name": "NullCounterImpl" + }, + { + "args": [], + "lineno": 38, + "name": "NullEventImpl" + }, + { + "args": [], + "lineno": 54, + "name": "NullGaugeImpl" + }, + { + "args": [], + "lineno": 74, + "name": "NullMeterImpl" + }, + { + "args": [], + "lineno": 81, + "name": "NullCollectorImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "NullCollector::New", + "detail::NullCollectorImp::make_hook / make_counter / make_event / make_gauge / make_meter", + "NullHookImpl / NullCounterImpl / NullEventImpl / NullGaugeImpl / NullMeterImpl methods" + ], + "entry_point": "NullCollector::New", + "purpose": "Provides a 'null' implementation of the Collector interface for metrics, returning inert (no-op) metric objects.", + "validation_points": [] + }, + { + "call_chain": [ + "detail::NullCollectorImp::make_counter", + "std::make_shared", + "NullCounterImpl::increment" + ], + "entry_point": "detail::NullCollectorImp::make_counter (and similar make_* methods)", + "purpose": "Creates a no-op Counter object; increment does nothing.", + "validation_points": [] + }, + { + "call_chain": [ + "NullCounterImpl::increment", + "(no further calls)" + ], + "entry_point": "detail::NullCounterImpl::increment (and similar methods in other Null*Impl classes)", + "purpose": "Implements the metric interface but does nothing (no validation, no state change).", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "value_type (for Counter, Meter, Gauge)", + "flow": [ + "Caller of Counter/Meter/Gauge::increment or Gauge::set", + "Null*Impl::increment/set (no-op)" + ], + "origin": "Passed as argument to increment/set methods", + "transformations": [ + "No transformation; argument is ignored" + ], + "validated_at": "Never validated" + }, + { + "field": "value_type const& (for Event)", + "flow": [ + "Caller of Event::notify", + "NullEventImpl::notify (no-op)" + ], + "origin": "Passed as argument to Event::notify", + "transformations": [ + "No transformation; argument is ignored" + ], + "validated_at": "Never validated" + }, + { + "field": "std::string (metric name)", + "flow": [ + "Caller of make_*", + "NullCollectorImp::make_* (argument ignored)", + "Null*Impl constructed (no use of name)" + ], + "origin": "Passed as argument to make_counter, make_event, make_gauge, make_meter", + "transformations": [ + "No transformation; argument is ignored" + ], + "validated_at": "Never validated" + } + ], + "description": "Implements 'null' (no-op) versions of insight metrics (Hook, Counter, Event, Gauge, Meter) and a NullCollector for use when metrics collection is disabled or not needed.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/insight/NullCollector.cpp", + "functions": [ + { + "args": [ + "value_type" + ], + "lineno": 25, + "name": "increment" + }, + { + "args": [ + "value_type const&" + ], + "lineno": 41, + "name": "notify" + }, + { + "args": [ + "value_type" + ], + "lineno": 57, + "name": "set" + }, + { + "args": [ + "difference_type" + ], + "lineno": 61, + "name": "increment" + }, + { + "args": [ + "value_type" + ], + "lineno": 77, + "name": "increment" + }, + { + "args": [ + "HookImpl::HandlerType const&" + ], + "lineno": 88, + "name": "make_hook" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 93, + "name": "make_counter" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 98, + "name": "make_event" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 103, + "name": "make_gauge" + }, + { + "args": [ + "std::string const&" + ], + "lineno": 108, + "name": "make_meter" + }, + { + "args": [], + "lineno": 115, + "name": "New" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "beast" + }, + { + "lineno": 11, + "name": "insight" + }, + { + "lineno": 13, + "name": "detail" + } + ], + "test_coverage_notes": "This file implements a 'null object' pattern for metrics collection. There is no validation logic or state mutation; all methods are no-ops. As such, there are no validation code paths to test. Test coverage would only verify that these objects can be constructed and called without error, and that they do not throw or mutate state. Typical tests (if any) would be in files like test/insight/NullCollector_test.cpp or similar, but such tests would only check for interface compliance and not validation. There are no data-dependent behaviors to test, and no validation gaps, because no validation is performed.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/beast/insight/NullCollector.cpp.ai.md b/src/libxrpl/beast/insight/NullCollector.cpp.ai.md new file mode 100644 index 0000000000..4f986d14b9 --- /dev/null +++ b/src/libxrpl/beast/insight/NullCollector.cpp.ai.md @@ -0,0 +1,37 @@ +# `NullCollector.cpp` — No-Op Metrics Collector + +## Role in the System + +The `beast::insight` subsystem provides a thin abstraction layer for exporting runtime metrics — counters, gauges, events, meters, and polling hooks — to an external aggregator. In production, the concrete implementation is `StatsDCollector`, which serializes readings and ships them over UDP to a StatsD daemon. `NullCollector.cpp` fills the other side of that contract: a complete, correctly-typed implementation of every interface that does nothing at all. + +This matters because XRPL subsystems accept a `Collector::ptr` (a `std::shared_ptr`) at construction time and use it throughout their lifetime. Without a null object, every call site that doesn't want metrics would need its own `if (collector)` guard. Instead, code that doesn't need metrics simply receives a `NullCollector` and operates identically to instrumented code. + +## Structure and Encapsulation + +The file defines its implementation entirely inside the `beast::insight::detail` namespace, none of which leaks into headers. The public surface is exactly two things: the `NullCollector` header (which just declares the class and its `New()` factory) and the `New()` definition itself, which returns a `std::shared_ptr` pointing at the private `NullCollectorImp`. + +Callers never name `NullCollectorImp` or any `Null*Impl` type. The return type of `NullCollector::New()` is `std::shared_ptr` — the concrete class is invisible at every call site. This is a deliberate application of the factory + interface idiom: the only way to obtain a `NullCollector` is through `New()`, and the returned pointer immediately upcasts to the abstract base. + +## The Five Null Metric Types + +Each metric type in the `insight` framework follows a two-layer design. The public handle class (`Counter`, `Hook`, etc.) is a value type that holds a `std::shared_ptr` to an abstract `*Impl` base (`CounterImpl`, `HookImpl`, etc.). All `*Impl` classes inherit from `std::enable_shared_from_this` — they are always heap-allocated and reference-counted, never stack-owned. + +`NullCollectorImp` implements all five `make_*` virtual methods by constructing a shared pointer to the corresponding null impl and passing it to the public handle's constructor: + +- `NullCounterImpl` — overrides `increment(value_type)` as a no-op. +- `NullEventImpl` — overrides `notify(value_type const&)` as a no-op. +- `NullGaugeImpl` — overrides both `set(value_type)` and `increment(difference_type)` as no-ops, since gauges support both absolute assignment and relative adjustment. +- `NullMeterImpl` — overrides `increment(value_type)` as a no-op. +- `NullHookImpl` — has no virtual methods to override beyond the destructor. Hooks work by registering a `std::function` handler that the real collector calls on its collection interval; the null impl simply discards that handler at construction time. + +Every null impl also suppresses copy assignment (declared private with no definition). This is a common defensive pattern for `shared_from_this` types: the object must remain heap-allocated, so accidental value-copy semantics are blocked at compile time. + +## Why the Null Pattern Is the Right Choice Here + +An alternative design might use a nullable `Collector*` everywhere with a sentinel `nullptr` meaning "no metrics." The null object approach is strictly superior here: the consumer code has no branches, no pointer checks, no conditional metric updates. Subsystems that initialize their metrics in their constructor — allocating a `Counter` from the collector and storing it as a member — work identically whether the collector is null or real. The cost of a null collection call is just a virtual dispatch followed by an immediate return. + +A shared no-op singleton could save the per-call `make_*` allocations, but the current design allocates a fresh `Null*Impl` per metric. This is a deliberate trade-off: callers hold the returned `Counter` or `Gauge` by value (which wraps the shared pointer), so the lifetime of the null impl is tied to the holder's lifetime, which is consistent with how the real `StatsDCollector` impls are managed. + +## Relationship to `StatsDCollector` + +`NullCollector` and `StatsDCollector` are the two concrete implementations of `Collector` in the library. The entire `beast::insight` framework is built on the assumption that these two are interchangeable — any code that accepts a `Collector::ptr` will work with either. `NullCollector.cpp` is therefore both a usable component (for production nodes that disable metrics reporting) and an implicit specification test: if `NullCollector` compiles and satisfies all pure virtuals, the interface contract is correctly defined. \ No newline at end of file diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp.ai.json b/src/libxrpl/beast/insight/StatsDCollector.cpp.ai.json new file mode 100644 index 0000000000..8bef4923a1 --- /dev/null +++ b/src/libxrpl/beast/insight/StatsDCollector.cpp.ai.json @@ -0,0 +1,669 @@ +{ + "args": [ + { + "lineno": 30, + "name": "handler" + }, + { + "lineno": 30, + "name": "impl" + }, + { + "lineno": 49, + "name": "name" + }, + { + "lineno": 49, + "name": "impl" + }, + { + "lineno": 76, + "name": "name" + }, + { + "lineno": 76, + "name": "impl" + }, + { + "lineno": 94, + "name": "name" + }, + { + "lineno": 94, + "name": "impl" + }, + { + "lineno": 121, + "name": "name" + }, + { + "lineno": 121, + "name": "impl" + }, + { + "lineno": 254, + "name": "address" + }, + { + "lineno": 254, + "name": "prefix" + }, + { + "lineno": 254, + "name": "journal" + }, + { + "lineno": 265, + "name": "ep" + }, + { + "lineno": 293, + "name": "handler" + }, + { + "lineno": 298, + "name": "name" + }, + { + "lineno": 303, + "name": "name" + }, + { + "lineno": 308, + "name": "name" + }, + { + "lineno": 313, + "name": "name" + }, + { + "lineno": 319, + "name": "metric" + }, + { + "lineno": 324, + "name": "metric" + }, + { + "lineno": 337, + "name": "buffer" + }, + { + "lineno": 341, + "name": "buffer" + }, + { + "lineno": 350, + "name": "keepAlive" + }, + { + "lineno": 350, + "name": "ec" + }, + { + "lineno": 363, + "name": "buffers" + }, + { + "lineno": 420, + "name": "ec" + }, + { + "lineno": 498, + "name": "address" + }, + { + "lineno": 498, + "name": "prefix" + }, + { + "lineno": 498, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 14, + "name": "StatsDMetricBase" + }, + { + "args": [ + "handler", + "impl" + ], + "lineno": 29, + "name": "StatsDHookImpl" + }, + { + "args": [ + "name", + "impl" + ], + "lineno": 48, + "name": "StatsDCounterImpl" + }, + { + "args": [ + "name", + "impl" + ], + "lineno": 75, + "name": "StatsDEventImpl" + }, + { + "args": [ + "name", + "impl" + ], + "lineno": 93, + "name": "StatsDGaugeImpl" + }, + { + "args": [ + "name", + "impl" + ], + "lineno": 120, + "name": "StatsDMeterImpl" + }, + { + "args": [ + "address", + "prefix", + "journal" + ], + "lineno": 253, + "name": "StatsDCollectorImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "StatsDCounterImpl::increment", + "StatsDCounterImpl::do_increment", + "StatsDCounterImpl::do_process" + ], + "entry_point": "StatsDCounterImpl::increment", + "purpose": "Handles incrementing a counter metric, marking it dirty, and processing it for StatsD export.", + "validation_points": [ + "StatsDCounterImpl::do_increment (validates amount type and value)", + "StatsDCounterImpl::do_process (validates if m_dirty is true before processing)" + ] + }, + { + "call_chain": [ + "StatsDEventImpl::notify", + "StatsDEventImpl::do_notify", + "StatsDEventImpl::do_process" + ], + "entry_point": "StatsDEventImpl::notify", + "purpose": "Handles event notification, passing event value through to processing.", + "validation_points": [ + "StatsDEventImpl::do_notify (validates event value type/format)", + "StatsDEventImpl::do_process (validates event state before processing)" + ] + }, + { + "call_chain": [ + "StatsDGaugeImpl::set", + "StatsDGaugeImpl::do_set", + "StatsDGaugeImpl::do_process" + ], + "entry_point": "StatsDGaugeImpl::set", + "purpose": "Sets a gauge metric to a specific value and processes it for StatsD export.", + "validation_points": [ + "StatsDGaugeImpl::do_set (validates value type/range)", + "StatsDGaugeImpl::do_process (validates if value changed before processing)" + ] + }, + { + "call_chain": [ + "StatsDGaugeImpl::increment", + "StatsDGaugeImpl::do_increment", + "StatsDGaugeImpl::do_process" + ], + "entry_point": "StatsDGaugeImpl::increment", + "purpose": "Increments a gauge metric by a delta and processes it.", + "validation_points": [ + "StatsDGaugeImpl::do_increment (validates increment amount type/range)", + "StatsDGaugeImpl::do_process (validates if value changed before processing)" + ] + }, + { + "call_chain": [ + "StatsDHookImpl::do_process" + ], + "entry_point": "StatsDHookImpl::do_process", + "purpose": "Processes a hook metric, typically invoking a handler.", + "validation_points": [ + "StatsDHookImpl::do_process (validates handler state and arguments)" + ] + } + ], + "data_flows": [ + { + "field": "m_value (StatsDCounterImpl)", + "flow": [ + "constructor", + "increment() or do_increment() (adds amount)", + "do_process() (reads and possibly resets/exports value)" + ], + "origin": "Initialized to 0 in constructor", + "transformations": [ + "Incremented by amount in do_increment", + "Marked dirty if changed", + "Flushed/exported in do_process" + ], + "validated_at": "do_increment (amount type/limits), do_process (dirty check)" + }, + { + "field": "m_name (StatsDCounterImpl, StatsDEventImpl, StatsDGaugeImpl)", + "flow": [ + "constructor", + "stored in member", + "used in do_process() for StatsD export" + ], + "origin": "Passed as constructor argument", + "transformations": [ + "None (string copy)" + ], + "validated_at": "Constructor (may check for empty/invalid names, but not shown in snippet)" + }, + { + "field": "m_impl (shared_ptr)", + "flow": [ + "constructor", + "stored in member", + "used in do_process() to access collector implementation" + ], + "origin": "Passed as constructor argument", + "transformations": [ + "None (shared_ptr copy)" + ], + "validated_at": "Constructor (may check for null, but not shown in snippet)" + }, + { + "field": "value (StatsDGaugeImpl)", + "flow": [ + "set() or increment()", + "do_set() or do_increment()", + "do_process()" + ], + "origin": "Argument to set() or increment()", + "transformations": [ + "Set directly or incremented", + "Marked dirty if changed", + "Flushed/exported in do_process" + ], + "validated_at": "do_set/do_increment (type/range), do_process (dirty check)" + }, + { + "field": "value (StatsDEventImpl)", + "flow": [ + "notify()", + "do_notify()", + "do_process()" + ], + "origin": "Argument to notify()", + "transformations": [ + "Passed through, possibly formatted for StatsD" + ], + "validated_at": "do_notify (type/format), do_process (event state)" + } + ], + "description": "Implements a StatsD metrics collector for the Beast insight framework, providing classes for counters, gauges, meters, events, and hooks that send metrics over UDP using Boost.Asio.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/insight/StatsDCollector.cpp", + "functions": [ + { + "args": [], + "lineno": 19, + "name": "StatsDMetricBase::do_process" + }, + { + "args": [], + "lineno": 109, + "name": "StatsDHookImpl::do_process" + }, + { + "args": [ + "amount" + ], + "lineno": 130, + "name": "StatsDCounterImpl::increment" + }, + { + "args": [], + "lineno": 135, + "name": "StatsDCounterImpl::flush" + }, + { + "args": [ + "amount" + ], + "lineno": 139, + "name": "StatsDCounterImpl::do_increment" + }, + { + "args": [], + "lineno": 141, + "name": "StatsDCounterImpl::do_process" + }, + { + "args": [ + "value" + ], + "lineno": 163, + "name": "StatsDEventImpl::notify" + }, + { + "args": [ + "value" + ], + "lineno": 167, + "name": "StatsDEventImpl::do_notify" + }, + { + "args": [], + "lineno": 169, + "name": "StatsDEventImpl::do_process" + }, + { + "args": [ + "value" + ], + "lineno": 191, + "name": "StatsDGaugeImpl::set" + }, + { + "args": [ + "amount" + ], + "lineno": 193, + "name": "StatsDGaugeImpl::increment" + }, + { + "args": [], + "lineno": 196, + "name": "StatsDGaugeImpl::flush" + }, + { + "args": [ + "value" + ], + "lineno": 201, + "name": "StatsDGaugeImpl::do_set" + }, + { + "args": [ + "amount" + ], + "lineno": 205, + "name": "StatsDGaugeImpl::do_increment" + }, + { + "args": [], + "lineno": 217, + "name": "StatsDGaugeImpl::do_process" + }, + { + "args": [ + "amount" + ], + "lineno": 239, + "name": "StatsDMeterImpl::increment" + }, + { + "args": [], + "lineno": 244, + "name": "StatsDMeterImpl::flush" + }, + { + "args": [ + "amount" + ], + "lineno": 248, + "name": "StatsDMeterImpl::do_increment" + }, + { + "args": [], + "lineno": 250, + "name": "StatsDMeterImpl::do_process" + }, + { + "args": [ + "ep" + ], + "lineno": 266, + "name": "StatsDCollectorImp::to_endpoint" + }, + { + "args": [ + "handler" + ], + "lineno": 292, + "name": "StatsDCollectorImp::make_hook" + }, + { + "args": [ + "name" + ], + "lineno": 297, + "name": "StatsDCollectorImp::make_counter" + }, + { + "args": [ + "name" + ], + "lineno": 302, + "name": "StatsDCollectorImp::make_event" + }, + { + "args": [ + "name" + ], + "lineno": 307, + "name": "StatsDCollectorImp::make_gauge" + }, + { + "args": [ + "name" + ], + "lineno": 312, + "name": "StatsDCollectorImp::make_meter" + }, + { + "args": [ + "metric" + ], + "lineno": 318, + "name": "StatsDCollectorImp::add" + }, + { + "args": [ + "metric" + ], + "lineno": 323, + "name": "StatsDCollectorImp::remove" + }, + { + "args": [], + "lineno": 328, + "name": "StatsDCollectorImp::get_io_context" + }, + { + "args": [], + "lineno": 332, + "name": "StatsDCollectorImp::prefix" + }, + { + "args": [ + "buffer" + ], + "lineno": 336, + "name": "StatsDCollectorImp::do_post_buffer" + }, + { + "args": [ + "buffer" + ], + "lineno": 340, + "name": "StatsDCollectorImp::post_buffer" + }, + { + "args": [ + "keepAlive", + "ec", + "" + ], + "lineno": 349, + "name": "StatsDCollectorImp::on_send" + }, + { + "args": [ + "buffers" + ], + "lineno": 362, + "name": "StatsDCollectorImp::log" + }, + { + "args": [], + "lineno": 374, + "name": "StatsDCollectorImp::send_buffers" + }, + { + "args": [], + "lineno": 414, + "name": "StatsDCollectorImp::set_timer" + }, + { + "args": [ + "ec" + ], + "lineno": 419, + "name": "StatsDCollectorImp::on_timer" + }, + { + "args": [], + "lineno": 438, + "name": "StatsDCollectorImp::run" + }, + { + "args": [ + "address", + "prefix", + "journal" + ], + "lineno": 497, + "name": "StatsDCollector::New" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "beast" + }, + { + "lineno": 13, + "name": "insight" + }, + { + "lineno": 15, + "name": "detail" + } + ], + "test_coverage_notes": "The code is primarily implementation for metric collection and export. Typical test coverage would be in integration or unit tests for the insight/StatsDCollector, Counter, Gauge, and Event classes. Tests would likely exist in files such as test_beast_insight.cpp or similar, focusing on incrementing counters, setting gauges, and firing events. However, validation of input types/ranges is minimal in the code shown, and there is no explicit error handling for invalid names or null pointers in constructors. Edge cases (e.g., negative increments, invalid names, null impl) may not be fully tested unless covered in higher-level integration tests. There is no evidence of fuzz or property-based testing for malformed input.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/beast/insight/StatsDCollector.cpp.ai.md b/src/libxrpl/beast/insight/StatsDCollector.cpp.ai.md new file mode 100644 index 0000000000..221713a4de --- /dev/null +++ b/src/libxrpl/beast/insight/StatsDCollector.cpp.ai.md @@ -0,0 +1,45 @@ +# `StatsDCollector.cpp` — Beast Insight StatsD Backend + +## Role and Purpose + +This file is the concrete implementation of `StatsDCollector`, one of the two `Collector` backends in the Beast insight framework (the other being `NullCollector`). Its job is to accept runtime metrics emitted by XRPL subsystems — counters, gauges, meters, events, and hooks — and relay them over UDP to an external StatsD-compatible aggregator such as Graphite or Telegraf. The file is entirely self-contained: it exposes only the factory function `StatsDCollector::New()` through the public header while burying all implementation classes inside the anonymous `detail` namespace. + +## Architecture Overview + +The collector lives behind the pimpl idiom. `StatsDCollector::New()` constructs a `StatsDCollectorImp`, which is hidden from callers and returned as a `shared_ptr`. `StatsDCollectorImp` owns a private `boost::asio::io_context`, a UDP socket, and a dedicated background thread that runs the event loop. All metric state mutations and all network I/O happen exclusively on this thread, which is the core concurrency strategy of the entire implementation. + +### Thread Model and Synchronization + +When application code on any thread calls `counter.increment(n)`, the call enters `StatsDCounterImpl::increment()`, which posts a bound call to `do_increment()` via `boost::asio::dispatch` onto the collector's `io_context`. This means the counter's internal `m_value` is only ever touched on the I/O thread, requiring no per-metric mutex. The same pattern applies to gauges, meters, and events. + +The only shared state that crosses threads is the `List metrics_` registry, guarded by `metricsLock_` (a `std::recursive_mutex`). Metrics register themselves in their constructor via `m_impl->add(*this)` and deregister in their destructor via `m_impl->remove(*this)`. The registry is also iterated under the same lock inside `on_timer`, making registration and destruction safe from any thread. + +### The Polling Loop + +A 1-second repeating timer drives metric collection. `on_timer()` acquires `metricsLock_`, iterates every registered `StatsDMetricBase`, and calls `do_process()` on each. For counters, gauges, and meters, `do_process()` delegates to a `flush()` method that checks a `m_dirty` flag. If the metric changed since the last tick, `flush()` serializes the metric name, value, and StatsD type suffix (`|c`, `|g`, `|m`) into a string and calls `post_buffer()`. After all metrics are processed, `send_buffers()` ships the accumulated strings over UDP. + +Events (`StatsDEventImpl`) are handled differently: they do not join the registry list and do not wait for the timer. Calling `notify()` immediately dispatches `do_notify()` to the I/O thread, which formats and posts the buffer at once using `|ms` (millisecond timing) format. This is intentional — events represent discrete occurrences whose latency should be captured at notification time, not coalesced. + +### Intrusive List for the Metric Registry + +`StatsDMetricBase` inherits from `List::Node`, making it a node in Beast's intrusive doubly linked list. The intrusive design avoids separate heap allocations for list bookkeeping and, crucially, enables O(1) removal from the registry using `metrics_.iterator_to(metric)` in `remove()`. Because each metric object *is* its own node, no separate tracking data structure is needed. + +### UDP Buffer Packing + +`send_buffers()` implements a simple greedy packing strategy. It accumulates formatted metric strings into a `std::vector` until the total byte count would exceed 1472 bytes (chosen as a typical Ethernet MTU after IP and UDP headers), at which point it fires an `async_send` and starts a fresh batch. The source code comments mention an earlier limit of 484 bytes that was later raised. This approach maximizes throughput while keeping each datagram below fragmentation thresholds. + +The `keepAlive` pattern in `send_buffers()` and `on_send()` deserves attention. The deque of string data is moved into a `shared_ptr>` before launching `async_send`. This shared pointer is then passed as the first (ignored) argument to the `on_send` completion handler. Because Boost.Asio holds a copy of the completion handler until it fires, the underlying string data stays alive for the entire asynchronous operation, even though `m_data` has already been cleared and ownership transferred. + +### Gauge Arithmetic + +`StatsDGaugeImpl::do_increment()` applies saturating arithmetic rather than allowing unsigned overflow. Positive increments are capped at `numeric_limits::max() - m_value`, and negative decrements are floored at zero. Gauges additionally suppress redundant sends: `do_set()` compares the incoming value against `m_last_value` and only sets `m_dirty` if the value changed. This avoids flooding StatsD with unchanging gauge readings every second. + +### Lifecycle and Shutdown + +`StatsDCollectorImp` uses `std::enable_shared_from_this` so that each metric implementation can safely capture a `shared_ptr` to the collector. This ensures the collector outlives any outstanding metric objects. Shutdown is ordered carefully: the destructor cancels the timer, resets the `executor_work_guard` (`m_work`) to let the event loop drain, then joins the background thread. After `io_context::run()` returns, `m_socket.shutdown()` and `m_socket.close()` are called, followed by a final `io_context::poll()` to flush any trailing completion handlers. + +The `m_thread` member is declared last in the class body specifically to ensure it is initialized after all other members, since the thread immediately invokes `run()` which touches `m_socket`, `m_io_context`, and the timer. + +### Compile-Time Tracing + +The `BEAST_STATSDCOLLECTOR_TRACING_ENABLED` macro (off by default) gates a `log()` function that writes raw UDP payload contents to `std::cerr` before each send. This is useful during development but is entirely compiled out in production builds. \ No newline at end of file diff --git a/src/libxrpl/beast/net/IPAddressConversion.cpp.ai.json b/src/libxrpl/beast/net/IPAddressConversion.cpp.ai.json new file mode 100644 index 0000000000..76138b589d --- /dev/null +++ b/src/libxrpl/beast/net/IPAddressConversion.cpp.ai.json @@ -0,0 +1,161 @@ +{ + "args": [ + { + "lineno": 8, + "name": "address" + }, + { + "lineno": 13, + "name": "endpoint" + }, + { + "lineno": 18, + "name": "endpoint" + }, + { + "lineno": 23, + "name": "endpoint" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "from_asio(boost::asio::ip::address const& address)", + "Endpoint{address}" + ], + "entry_point": "from_asio(boost::asio::ip::address const& address)", + "purpose": "Converts a Boost.Asio IP address to a beast::IP::Endpoint.", + "validation_points": [] + }, + { + "call_chain": [ + "from_asio(boost::asio::ip::tcp::endpoint const& endpoint)", + "endpoint.address(), endpoint.port()", + "Endpoint{address, port}" + ], + "entry_point": "from_asio(boost::asio::ip::tcp::endpoint const& endpoint)", + "purpose": "Converts a Boost.Asio TCP endpoint to a beast::IP::Endpoint.", + "validation_points": [] + }, + { + "call_chain": [ + "to_asio_address(Endpoint const& endpoint)", + "endpoint.address()" + ], + "entry_point": "to_asio_address(Endpoint const& endpoint)", + "purpose": "Extracts the Boost.Asio IP address from a beast::IP::Endpoint.", + "validation_points": [] + }, + { + "call_chain": [ + "to_asio_endpoint(Endpoint const& endpoint)", + "endpoint.address(), endpoint.port()", + "boost::asio::ip::tcp::endpoint{address, port}" + ], + "entry_point": "to_asio_endpoint(Endpoint const& endpoint)", + "purpose": "Converts a beast::IP::Endpoint to a Boost.Asio TCP endpoint.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "address (boost::asio::ip::address)", + "flow": [ + "from_asio(address) input", + "Endpoint{address} constructor", + "Endpoint instance" + ], + "origin": "Input parameter to from_asio(address)", + "transformations": [ + "Wrapped in Endpoint" + ], + "validated_at": null + }, + { + "field": "endpoint (boost::asio::ip::tcp::endpoint)", + "flow": [ + "from_asio(endpoint) input", + "endpoint.address(), endpoint.port()", + "Endpoint{address, port} constructor", + "Endpoint instance" + ], + "origin": "Input parameter to from_asio(endpoint)", + "transformations": [ + "Extract address and port, wrap in Endpoint" + ], + "validated_at": null + }, + { + "field": "Endpoint", + "flow": [ + "Endpoint instance", + "endpoint.address() or endpoint.port()", + "Used to construct boost::asio::ip::address or boost::asio::ip::tcp::endpoint" + ], + "origin": "Output of from_asio() or input to to_asio_*()", + "transformations": [ + "Unwrapped to get address/port" + ], + "validated_at": null + } + ], + "description": "Provides conversion utilities between Boost.Asio IP address/endpoint types and the beast::IP::Endpoint type.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/net/IPAddressConversion.cpp", + "functions": [ + { + "args": [ + "address" + ], + "lineno": 8, + "name": "from_asio" + }, + { + "args": [ + "endpoint" + ], + "lineno": 13, + "name": "from_asio" + }, + { + "args": [ + "endpoint" + ], + "lineno": 18, + "name": "to_asio_address" + }, + { + "args": [ + "endpoint" + ], + "lineno": 23, + "name": "to_asio_endpoint" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 7, + "name": "IP" + } + ], + "test_coverage_notes": "There is no validation logic in this file; all functions are simple wrappers or converters with no input checking or error handling. Validation (if any) would occur in the Endpoint constructor or in upstream code. Test coverage for this file would likely be in unit tests for Endpoint or integration tests for networking code, but this file itself does not contain logic that can fail or be validated. If Endpoint performs validation, it is not visible here. There are likely no direct tests for these conversion functions unless Endpoint or higher-level networking code is tested for address/endpoint conversion correctness. Gaps: No validation of input data, no error handling, and no explicit tests for invalid input.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/beast/net/IPAddressConversion.cpp.ai.md b/src/libxrpl/beast/net/IPAddressConversion.cpp.ai.md new file mode 100644 index 0000000000..3770bf3920 --- /dev/null +++ b/src/libxrpl/beast/net/IPAddressConversion.cpp.ai.md @@ -0,0 +1,33 @@ +# `IPAddressConversion.cpp` — Boost.Asio / beast::IP Bridge + +## Role in the System + +This file is the narrow translation boundary between Boost.Asio's networking types and the XRPL codebase's own IP abstraction layer. The rest of the XRPL peer-networking stack works with `beast::IP::Endpoint`, a version-independent address+port type that makes no reference to Boost.Asio. When a raw socket or acceptor hands off a `boost::asio::ip::tcp::endpoint`, this file converts it into the internal representation. Symmetrically, when the networking layer needs to open or connect a socket, it converts `beast::IP::Endpoint` back to the Asio type. + +Keeping the conversion logic isolated here means all call sites can stay ignorant of each other's type system, and the `beast::IP::Endpoint` type remains testable and serializable without pulling in Boost.Asio headers. + +## The Four Conversion Functions + +All four functions live in `namespace beast::IP` and are intentionally trivial — each is a single-expression body: + +- **`from_asio(boost::asio::ip::address)`** wraps a bare Asio address in an `Endpoint` with port zero. This is useful when only the host is known at the point of construction (e.g., parsing a config entry that has no port), as noted explicitly in the header comment. + +- **`from_asio(boost::asio::ip::tcp::endpoint)`** decomposes the Asio endpoint into its address and port components and forwards both to the `Endpoint` constructor. The overload resolution makes this seamless at call sites. + +- **`to_asio_address(Endpoint)`** extracts just the `boost::asio::ip::address` stored inside the `Endpoint`, dropping the port. The header comment calls out this information loss ("the port is ignored"), which is intentional for contexts that only need the host address. + +- **`to_asio_endpoint(Endpoint)`** reconstructs a `boost::asio::ip::tcp::endpoint` from the address and port stored in the `beast::IP::Endpoint`. This is the primary path taken when the peer layer hands a resolved address to Boost.Asio to initiate a TCP connection. + +## Why `beast::IP::Endpoint` Exists + +`beast::IP::Endpoint` (defined in `IPEndpoint.h`) is a lightweight POD-like class holding a `beast::IP::Address` and a `uint16_t` port. It supports IPv4 and IPv6 transparently, is hashable (both `std::hash` and `boost::hash` specializations are provided), is totally ordered, and can be constructed from or converted to a string. These properties make it suitable as a map key, a serialization target, and a unit-testable value without any dependency on Boost.Asio — which is the whole point of the abstraction. + +## The Deprecated `IPAddressConversion` Struct + +The header also retains a struct `beast::IPAddressConversion` (marked `// DEPRECATED`) that exposes the same four conversions as `static` methods. This was the original API style; it was superseded by the free functions in `beast::IP`. The struct remains to avoid breaking old call sites that haven't been updated, but no new code should use it. + +## Design Notes + +No validation occurs anywhere in this file. The `Endpoint` constructor accepts whatever address and port it receives — range checking or policy enforcement (e.g., rejecting reserved addresses) is the responsibility of higher-level networking code. This is a deliberate design choice: conversion is a purely mechanical, lossless operation and should not impose policy. + +The implementation is split across a header (`IPAddressConversion.h`) and this `.cpp` file rather than being defined inline. Given that each function is one line, this is slightly unusual, but it keeps the Boost.Asio inclusion out of code paths that only need `IPEndpoint.h`, which itself does not include Boost.Asio headers. \ No newline at end of file diff --git a/src/libxrpl/beast/net/IPAddressV4.cpp.ai.json b/src/libxrpl/beast/net/IPAddressV4.cpp.ai.json new file mode 100644 index 0000000000..667beb5e18 --- /dev/null +++ b/src/libxrpl/beast/net/IPAddressV4.cpp.ai.json @@ -0,0 +1,228 @@ +{ + "args": [ + { + "lineno": 6, + "name": "addr" + }, + { + "lineno": 15, + "name": "addr" + }, + { + "lineno": 21, + "name": "addr" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "is_private" + ], + "entry_point": "is_private(AddressV4 const& addr)", + "purpose": "Determines if an IPv4 address is in a private range or is loopback.", + "validation_points": [ + "is_private (checks private ranges and loopback)" + ] + }, + { + "call_chain": [ + "is_public", + "is_private" + ], + "entry_point": "is_public(AddressV4 const& addr)", + "purpose": "Determines if an IPv4 address is public (not private and not multicast).", + "validation_points": [ + "is_private (checks private/loopback)", + "is_public (checks multicast)" + ] + }, + { + "call_chain": [ + "get_class" + ], + "entry_point": "get_class(AddressV4 const& addr)", + "purpose": "Returns the class (A, B, C, D, etc.) of the IPv4 address.", + "validation_points": [ + "get_class (classifies address by bitmask)" + ] + } + ], + "data_flows": [ + { + "field": "AddressV4::to_uint()", + "flow": [ + "AddressV4 instance creation", + "to_uint() called in is_private/is_public/get_class", + "Bitmask operations for validation/classification", + "Result used to determine address type/class" + ], + "origin": "AddressV4 object, likely constructed from user input, config, or network data", + "transformations": [ + "Converted from AddressV4 to uint32_t via to_uint()", + "Bitmasking to extract relevant bits for validation" + ], + "validated_at": "is_private, is_public, get_class" + }, + { + "field": "AddressV4::is_loopback()", + "flow": [ + "AddressV4 instance creation", + "is_loopback() called in is_private" + ], + "origin": "AddressV4 object", + "transformations": [ + "Checks if address is in loopback range" + ], + "validated_at": "is_private" + }, + { + "field": "AddressV4::is_multicast()", + "flow": [ + "AddressV4 instance creation", + "is_multicast() called in is_public" + ], + "origin": "AddressV4 object", + "transformations": [ + "Checks if address is in multicast range" + ], + "validated_at": "is_public" + } + ], + "description": "Provides utility functions for IPv4 address classification, such as checking if an address is private, public, or determining its class.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AddressV4 addr", + "empty", + "string", + "validation" + ], + "evidence": "is_private (custom logic) at is_private", + "issue_pattern": "Missing empty string validation for AddressV4 addr", + "why_false_positive": "is_private (custom logic) validates AddressV4 addr for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AddressV4 addr", + "empty", + "string", + "validation" + ], + "evidence": "is_public (custom logic) at is_public", + "issue_pattern": "Missing empty string validation for AddressV4 addr", + "why_false_positive": "is_public (custom logic) validates AddressV4 addr for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AddressV4 addr", + "empty", + "string", + "validation" + ], + "evidence": "get_class (custom logic) at get_class", + "issue_pattern": "Missing empty string validation for AddressV4 addr", + "why_false_positive": "get_class (custom logic) validates AddressV4 addr for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/net/IPAddressV4.cpp", + "functions": [ + { + "args": [ + "addr" + ], + "lineno": 5, + "name": "is_private" + }, + { + "args": [ + "addr" + ], + "lineno": 14, + "name": "is_public" + }, + { + "args": [ + "addr" + ], + "lineno": 20, + "name": "get_class" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "beast" + }, + { + "lineno": 4, + "name": "IP" + } + ], + "test_coverage_notes": "The code itself does not include tests. Typical test coverage would be in files like 'test/IPAddressV4_test.cpp' or similar in the test suite. Tests should cover: (1) private address detection (10.x.x.x, 172.16.x.x-172.31.x.x, 192.168.x.x, loopback), (2) public address detection (addresses outside private/multicast), (3) multicast detection, (4) class determination (A, B, C, D, etc.). Gaps may exist if edge cases (e.g., broadcast, reserved ranges, or malformed addresses) are not tested. If AddressV4 is constructed from untrusted input, fuzzing or negative tests are important but may not be present.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (custom logic, no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (returns bool)", + "field": "AddressV4 addr", + "location": "is_private", + "validated_by": "is_private (custom logic)", + "validates": [ + "Checks if addr is in private IPv4 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)", + "Checks if addr is loopback" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns bool)", + "field": "AddressV4 addr", + "location": "is_public", + "validated_by": "is_public (custom logic)", + "validates": [ + "Checks if addr is not private", + "Checks if addr is not multicast" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns char)", + "field": "AddressV4 addr", + "location": "get_class", + "validated_by": "get_class (custom logic)", + "validates": [ + "Determines IPv4 class (A, B, C, D) based on high bits" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/beast/net/IPAddressV4.cpp.ai.md b/src/libxrpl/beast/net/IPAddressV4.cpp.ai.md new file mode 100644 index 0000000000..83fe586aab --- /dev/null +++ b/src/libxrpl/beast/net/IPAddressV4.cpp.ai.md @@ -0,0 +1,63 @@ +# `IPAddressV4.cpp` — IPv4 Address Classification Utilities + +This file implements three free functions in the `beast::IP` namespace that classify IPv4 addresses by network scope and historical address class. It is a thin but meaningful layer on top of Boost.Asio's `boost::asio::ip::address_v4`, which is aliased as `AddressV4` in the corresponding header. + +## Role in the System + +The XRPL peer-finder and overlay subsystems need to distinguish publicly routable addresses from private or multicast ones before making connection decisions. For example, `peerfinder/detail/Logic.h` gates per-IP connection limits (`ipLimit`) on `is_public()`, and the overlay handshake logic uses the same predicate to validate advertised peer endpoints. Without these predicates, the network layer would have to replicate CIDR range arithmetic at every call site. + +## `is_private()` + +```cpp +bool is_private(AddressV4 const& addr) +``` + +Classifies an address as non-routable by checking three RFC 1918 private ranges and the loopback range: + +| Mask | Range | RFC | +|---|---|---| +| `/8` (`0xff000000`) | `10.0.0.0 – 10.255.255.255` | RFC 1918 | +| `/12` (`0xfff00000`) | `172.16.0.0 – 172.31.255.255` | RFC 1918 | +| `/16` (`0xffff0000`) | `192.168.0.0 – 192.168.255.255` | RFC 1918 | +| — | `127.0.0.0/8` (loopback) | handled by `addr.is_loopback()` | + +The implementation calls `addr.to_uint()` once per check, converting the address to a host-order `uint32_t` for direct bitmasking. Each condition ANDs the address with the appropriate prefix mask and compares the result to the network base address — the canonical, branch-free CIDR test. The loopback check is delegated to Boost.Asio's `is_loopback()` rather than replicating `127.0.0.0/8` arithmetic inline, which keeps the intent readable and avoids a subtle mistake (loopback is a full `/8`, not just `127.0.0.1`). + +Note that link-local (`169.254.0.0/16`) and `100.64.0.0/10` (RFC 6598 shared address space) are intentionally excluded. The function covers exactly the three RFC 1918 blocks the XRPL peer filter cares about. + +## `is_public()` + +```cpp +bool is_public(AddressV4 const& addr) +``` + +A simple compositional predicate: an address is public if and only if it is neither private nor multicast. This is expressed directly as: + +```cpp +return !is_private(addr) && !addr.is_multicast(); +``` + +The short-circuit evaluation means `is_multicast()` (which covers `224.0.0.0/4`) is only tested when `is_private()` returns false. In peer-connection logic this function acts as the primary gate: only addresses that pass `is_public()` are considered viable candidates for outbound connections or counted against per-IP limits. + +## `get_class()` + +```cpp +char get_class(AddressV4 const& addr) +``` + +Returns the historical IPv4 address class (`'A'`, `'B'`, `'C'`, or `'D'`) by examining the top three bits of the address. The implementation uses a static lookup table `"AAAABBCD"` indexed by `(addr.to_uint() & 0xE0000000) >> 29`, which shifts the three most significant bits into a 0–7 index. + +The bit-to-class mapping follows the original RFC 791 classful scheme: + +- Bits `000`–`011` (indices 0–3) → class `A` (0.0.0.0/1) +- Bits `100`–`101` (indices 4–5) → class `B` (128.0.0.0/2) +- Bit `110` (index 6) → class `C` (192.0.0.0/3) +- Bit `111` (index 7) → class `D` (224.0.0.0/4, multicast) + +The `// cspell:disable-line` annotation on the table is a spell-checker suppression; "AAAABBCD" would otherwise trigger a false alarm. The table is `static const`, so it is initialized once and lives in read-only memory for the process lifetime — a trivial but correct optimization for a hot path. + +Classful addressing is technically obsolete (superseded by CIDR), but `get_class()` remains useful in the XRPL codebase as a coarse diagnostic tool and for logging peer address metadata. + +## Design Notes + +All three functions are pure, stateless free functions. There is no object to construct, no error path, and no allocation — every call reduces to a handful of bitwise operations on a 32-bit integer. This makes them safe to call from any context including lock-holding code, which is exactly how `Logic.h` uses `is_public()`. The underlying `AddressV4` type is Boost.Asio's `address_v4`, so parsing and validation are already handled upstream before any of these predicates are reached. \ No newline at end of file diff --git a/src/libxrpl/beast/net/IPAddressV6.cpp.ai.json b/src/libxrpl/beast/net/IPAddressV6.cpp.ai.json new file mode 100644 index 0000000000..5fa3cd779e --- /dev/null +++ b/src/libxrpl/beast/net/IPAddressV6.cpp.ai.json @@ -0,0 +1,175 @@ +{ + "args": [ + { + "lineno": 9, + "name": "addr" + }, + { + "lineno": 18, + "name": "addr" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "is_private" + ], + "entry_point": "is_private(AddressV6 const& addr)", + "purpose": "Determines if an IPv6 address is private.", + "validation_points": [ + "Manual bitmask check on addr.to_bytes()[0]", + "is_v4_mapped() check", + "Delegation to is_private(AddressV4) for v4-mapped addresses" + ] + }, + { + "call_chain": [ + "is_public", + "is_private", + "is_multicast" + ], + "entry_point": "is_public(AddressV6 const& addr)", + "purpose": "Determines if an IPv6 address is public (not private and not multicast).", + "validation_points": [ + "is_private(AddressV6) call", + "is_multicast() method call on AddressV6" + ] + } + ], + "data_flows": [ + { + "field": "AddressV6 addr", + "flow": [ + "Input to is_private/is_public", + "addr.to_bytes()[0] bitmask check", + "addr.is_v4_mapped() check", + "If v4-mapped: converted to AddressV4 via boost::asio::ip::make_address_v4", + "Delegated to is_private(AddressV4) if v4-mapped", + "Result returned" + ], + "origin": "Passed as argument to is_private/is_public", + "transformations": [ + "Bitmask applied to first byte", + "Potential conversion to AddressV4" + ], + "validated_at": "is_private(AddressV6) (bitmask and v4-mapped check)" + }, + { + "field": "AddressV6 addr", + "flow": [ + "Input to is_public", + "is_private(addr) called", + "addr.is_multicast() called", + "Result returned" + ], + "origin": "Passed as argument to is_public", + "transformations": [ + "Boolean logic: !is_private && !is_multicast" + ], + "validated_at": "is_private(AddressV6), addr.is_multicast()" + } + ], + "description": "Provides functions to determine if an IPv6 address is private or public within the beast::IP namespace.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "AddressV6 addr", + "empty", + "string", + "validation" + ], + "evidence": "manual bitmask and function call at is_private", + "issue_pattern": "Missing empty string validation for AddressV6 addr", + "why_false_positive": "manual bitmask and function call validates AddressV6 addr for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "AddressV6 addr", + "empty", + "string", + "validation" + ], + "evidence": "is_multicast method at is_public", + "issue_pattern": "Missing empty string validation for AddressV6 addr", + "why_false_positive": "is_multicast method validates AddressV6 addr for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/net/IPAddressV6.cpp", + "functions": [ + { + "args": [ + "AddressV6 const& addr" + ], + "lineno": 8, + "name": "is_private" + }, + { + "args": [ + "AddressV6 const& addr" + ], + "lineno": 17, + "name": "is_public" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "beast" + }, + { + "lineno": 6, + "name": "IP" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage. Typical test files would be in a test/ or unit_test/ directory, possibly named IPAddressV6_test.cpp or similar. The code relies on correct behavior of AddressV6::to_bytes(), is_v4_mapped(), and is_multicast(), as well as the AddressV4 validation. Gaps: No explicit test hooks or asserts here; edge cases (e.g., fc00::/8, multicast, v4-mapped) may not be fully covered unless tested elsewhere. The TODO comment suggests incomplete handling of some private address ranges (fc00::/8).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation, some Boost usage for address conversion)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "none (returns bool)", + "field": "AddressV6 addr", + "location": "is_private", + "validated_by": "manual bitmask and function call", + "validates": [ + "Checks if the first byte of the IPv6 address, masked with 0xfd, is nonzero (potentially to detect private address ranges, but logic is unclear and marked TODO)", + "If the address is IPv4-mapped, calls is_private on the mapped IPv4 address" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (returns bool)", + "field": "AddressV6 addr", + "location": "is_public", + "validated_by": "is_multicast method", + "validates": [ + "Checks that the address is not private (using is_private)", + "Checks that the address is not multicast (using is_multicast method)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/beast/net/IPAddressV6.cpp.ai.md b/src/libxrpl/beast/net/IPAddressV6.cpp.ai.md new file mode 100644 index 0000000000..eba573f65d --- /dev/null +++ b/src/libxrpl/beast/net/IPAddressV6.cpp.ai.md @@ -0,0 +1,25 @@ +# `IPAddressV6.cpp` — IPv6 Address Classification + +This file provides two free functions in the `beast::IP` namespace — `is_private` and `is_public` — that classify an IPv6 address as routable or not. It is a direct sibling of `IPAddressV4.cpp`, which supplies the same interface for IPv4. Both files wrap `boost::asio` address types with the thin domain-specific logic the rest of the XRPL peer-networking layer needs to make decisions like whether to accept or advertise a discovered endpoint. + +`AddressV6` is nothing more than a type alias for `boost::asio::ip::address_v6`, defined in the header. The classification functions therefore operate purely on Boost.Asio's byte-level accessors and need no custom address representation. + +## `is_private` + +The function attempts to identify private, non-routable IPv6 addresses through two independent checks combined with short-circuit OR logic. + +The first check — `(addr.to_bytes()[0] & 0xfd) != 0` — is explicitly incomplete and annotated with a `// TODO fc00::/8 too ?` comment. The stated intent is to detect Unique Local Addresses (ULA), the IPv6 equivalent of RFC 1918 private space, which occupy `fc00::/7` (first bytes `0xfc` or `0xfd` per RFC 4193). However, the bitmask as written is logically unsound: `0xfd` in binary is `11111101`, so masking any first byte with it and testing for non-zero produces `true` for virtually every globally-routable IPv6 address (e.g., `0x20` for `2001::/3`, `0x26`, `0x2a`, etc.). The only first bytes that pass this mask as zero are `0x00` and `0x02`. In practice this means the function classifies almost all native IPv6 addresses, including public ones, as private — a known defect signaled by the dual TODO markers here and in `is_public`. + +The correct expression for `fc00::/7` would be `(addr.to_bytes()[0] & 0xfe) == 0xfc`; the existing code appears to have been written with the inverse comparison in mind and never corrected. + +The second check handles IPv4-mapped addresses (`::ffff:0:0/96`). When `addr.is_v4_mapped()` returns true, Boost.Asio's `make_address_v4` with the `v4_mapped` tag strips the mapping prefix and produces a plain `boost::asio::ip::address_v4`, which is then passed to the IPv4 overload of `is_private`. That overload properly handles `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and loopback, making the IPv4 private-address logic correct even when the address arrives through an IPv6 socket. + +## `is_public` + +`is_public` is defined as the logical complement of both `is_private` and `addr.is_multicast()`, inheriting the same `// TODO is this correct?` caveat. The pattern is identical to the IPv4 version in `IPAddressV4.cpp`. Multicast addresses (`ff00::/8`) are excluded from "public" because they are scoped group identifiers, not routable unicast destinations. Since `is_private` is currently over-broad, `is_public` in turn under-reports public addresses for non-mapped IPv6 — a consequence that propagates into any call site that gates peer connections or address advertisement on this predicate. + +## Design Notes + +The `beast::IP` namespace follows a consistent style: Boost.Asio types are re-exported as local aliases (`AddressV4`, `AddressV6`) and classification functions are implemented as non-member free functions rather than methods, keeping the types themselves thin wrappers and the policy logic separate. This allows the classification rules to evolve without touching the address representation. The IPv4 and IPv6 variants share the same header-declared API shape, so call sites can use overload resolution to dispatch on address family without conditionals. + +No exceptions are thrown, no heap allocations occur, and all operations are O(1) byte reads. The functions are safe to call from any context, including network I/O threads, with no synchronization concerns. \ No newline at end of file diff --git a/src/libxrpl/beast/net/IPEndpoint.cpp.ai.json b/src/libxrpl/beast/net/IPEndpoint.cpp.ai.json new file mode 100644 index 0000000000..5662b9982a --- /dev/null +++ b/src/libxrpl/beast/net/IPEndpoint.cpp.ai.json @@ -0,0 +1,440 @@ +{ + "args": [ + { + "lineno": 23, + "name": "s" + }, + { + "lineno": 36, + "name": "s" + } + ], + "classes": [ + { + "args": [], + "lineno": 14, + "name": "Endpoint" + } + ], + "code_paths": [ + { + "call_chain": [ + "Endpoint::from_string_checked", + "std::stringstream is >> endpoint", + "operator>>(std::istream&, Endpoint&)" + ], + "entry_point": "Endpoint::from_string_checked", + "purpose": "Parses and validates an endpoint string, returning std::optional if valid.", + "validation_points": [ + "Manual length check (s.size() <= 64) in from_string_checked", + "Parsing and character validation in operator>>", + "Address validation via boost::asio::ip::make_address in operator>>" + ] + }, + { + "call_chain": [ + "Endpoint::from_string", + "Endpoint::from_string_checked", + "std::stringstream is >> endpoint", + "operator>>(std::istream&, Endpoint&)" + ], + "entry_point": "Endpoint::from_string", + "purpose": "Parses an endpoint string, returns Endpoint (default if invalid).", + "validation_points": [ + "All validations in from_string_checked and operator>>" + ] + }, + { + "call_chain": [ + "operator>>(std::istream&, Endpoint&)" + ], + "entry_point": "operator>>(std::istream&, Endpoint&)", + "purpose": "Parses an endpoint from a stream, performing all validation.", + "validation_points": [ + "Manual character checks for address", + "Bracket checks for IPv6", + "Length checks for address string", + "Address validation via boost::asio::ip::make_address" + ] + } + ], + "data_flows": [ + { + "field": "endpoint string (input)", + "flow": [ + "Input string", + "Manual length check (<=64)", + "Trimmed and streamed into stringstream", + "operator>> parses and validates", + "Endpoint object constructed if valid" + ], + "origin": "User or caller input to Endpoint::from_string or Endpoint::from_string_checked", + "transformations": [ + "Trimmed (boost::trim_copy)", + "Streamed into stringstream", + "Parsed into address and port" + ], + "validated_at": "from_string_checked (length), operator>> (syntax, address, port)" + }, + { + "field": "address string (part of endpoint)", + "flow": [ + "Extracted from input stream", + "Manual character and bracket checks", + "Passed to boost::asio::ip::make_address" + ], + "origin": "Parsed from input string in operator>>", + "transformations": [ + "Manual character filtering", + "Bracket handling for IPv6", + "Converted to boost::asio::ip::address" + ], + "validated_at": "operator>> (character checks, bracket checks, make_address)" + }, + { + "field": "port", + "flow": [ + "Parsed from stream after address", + "Converted to integer" + ], + "origin": "Parsed from input string after address in operator>>", + "transformations": [ + "String to integer conversion" + ], + "validated_at": "operator>> (implicit, but not shown in provided code fragment)" + } + ], + "description": "This file implements the Endpoint class and related functions for parsing, formatting, and comparing IP endpoints (address and port), including string conversion and stream extraction, within the beast::IP namespace.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "input string (endpoint string)", + "empty", + "string", + "validation" + ], + "evidence": "manual length check at Endpoint::from_string_checked", + "issue_pattern": "Missing empty string validation for input string (endpoint string)", + "why_false_positive": "manual length check validates input string (endpoint string) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "input string (endpoint string)", + "range", + "bounds", + "validation" + ], + "evidence": "manual length check at Endpoint::from_string_checked", + "issue_pattern": "Missing range validation for input string (endpoint string)", + "why_false_positive": "manual length check validates input string (endpoint string) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "input string (endpoint string)", + "empty", + "string", + "validation" + ], + "evidence": "std::stringstream + operator>> at Endpoint::from_string_checked", + "issue_pattern": "Missing empty string validation for input string (endpoint string)", + "why_false_positive": "std::stringstream + operator>> validates input string (endpoint string) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "input string (endpoint string)", + "format", + "validation", + "invalid" + ], + "evidence": "std::stringstream + operator>> at Endpoint::from_string_checked", + "issue_pattern": "Missing format validation for input string (endpoint string)", + "why_false_positive": "std::stringstream + operator>> validates input string (endpoint string) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "address string (part of endpoint)", + "empty", + "string", + "validation" + ], + "evidence": "manual character checks at operator>>(std::istream&, Endpoint&)", + "issue_pattern": "Missing empty string validation for address string (part of endpoint)", + "why_false_positive": "manual character checks validates address string (part of endpoint) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "address string (part of endpoint)", + "format", + "validation", + "invalid" + ], + "evidence": "manual character checks at operator>>(std::istream&, Endpoint&)", + "issue_pattern": "Missing format validation for address string (part of endpoint)", + "why_false_positive": "manual character checks validates address string (part of endpoint) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "address string (IPv6)", + "empty", + "string", + "validation" + ], + "evidence": "manual bracket check at operator>>(std::istream&, Endpoint&)", + "issue_pattern": "Missing empty string validation for address string (IPv6)", + "why_false_positive": "manual bracket check validates address string (IPv6) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "address string (IPv6)", + "format", + "validation", + "invalid" + ], + "evidence": "manual bracket check at operator>>(std::istream&, Endpoint&)", + "issue_pattern": "Missing format validation for address string (IPv6)", + "why_false_positive": "manual bracket check validates address string (IPv6) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "address string", + "empty", + "string", + "validation" + ], + "evidence": "boost::asio::ip::address::from_string (implied, likely in Address::from_string) at likely in Address::from_string (not shown, but implied usage)", + "issue_pattern": "Missing empty string validation for address string", + "why_false_positive": "boost::asio::ip::address::from_string (implied, likely in Address::from_string) validates address string for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "address string", + "format", + "validation", + "invalid" + ], + "evidence": "boost::asio::ip::address::from_string (implied, likely in Address::from_string) at likely in Address::from_string (not shown, but implied usage)", + "issue_pattern": "Missing format validation for address string", + "why_false_positive": "boost::asio::ip::address::from_string (implied, likely in Address::from_string) validates address string format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "port", + "empty", + "string", + "validation" + ], + "evidence": "std::stoi or manual parsing (implied, not shown in snippet) at operator>>(std::istream&, Endpoint&)", + "issue_pattern": "Missing empty string validation for port", + "why_false_positive": "std::stoi or manual parsing (implied, not shown in snippet) validates port for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/net/IPEndpoint.cpp", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "Endpoint::Endpoint" + }, + { + "args": [ + "Address const& addr", + "Port port" + ], + "lineno": 19, + "name": "Endpoint::Endpoint" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 23, + "name": "Endpoint::from_string_checked" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 36, + "name": "Endpoint::from_string" + }, + { + "args": [], + "lineno": 43, + "name": "Endpoint::to_string" + }, + { + "args": [ + "Endpoint const& lhs", + "Endpoint const& rhs" + ], + "lineno": 59, + "name": "operator==" + }, + { + "args": [ + "Endpoint const& lhs", + "Endpoint const& rhs" + ], + "lineno": 64, + "name": "operator<" + }, + { + "args": [ + "std::istream& is", + "Endpoint& endpoint" + ], + "lineno": 73, + "name": "operator>>" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "beast" + }, + { + "lineno": 13, + "name": "IP" + } + ], + "test_coverage_notes": "The code is likely tested by unit tests for Endpoint parsing, such as tests for valid/invalid endpoint strings, IPv4/IPv6 formats, and edge cases (length, invalid chars, missing port, etc). Typical test files would be in the beast/net or xrpl/beast/net test directories, possibly named IPEndpoint_test.cpp or similar. Gaps may include: incomplete coverage of legacy formats (space as separator), malformed IPv6 with/without brackets, maximum length edge cases, and error handling for boost::asio::ip::make_address failures. No explicit test references are present in this file.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "manual validation, boost::asio for address parsing", + "validation_layer": "entry_point (string parsing and construction)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "input string (endpoint string)", + "location": "Endpoint::from_string_checked", + "validated_by": "manual length check", + "validates": [ + "input string length <= 64" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "input string (endpoint string)", + "location": "Endpoint::from_string_checked", + "validated_by": "std::stringstream + operator>>", + "validates": [ + "input string can be parsed into Endpoint using operator>>", + "no extra characters after endpoint" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "istream failbit set (no exception thrown)", + "field": "address string (part of endpoint)", + "location": "operator>>(std::istream&, Endpoint&)", + "validated_by": "manual character checks", + "validates": [ + "address only contains valid IPv4/IPv6 characters ('.', '0'-':', 'a'-'f', 'A'-'F')", + "address does not exceed INET6_ADDRSTRLEN" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "istream failbit set (no exception thrown)", + "field": "address string (IPv6)", + "location": "operator>>(std::istream&, Endpoint&)", + "validated_by": "manual bracket check", + "validates": [ + "IPv6 address must be enclosed in '[' and ']'" + ], + "validation_type": "format" + }, + { + "confidence": 0.8, + "error_thrown": "throws boost::system::system_error or sets error_code (not shown here)", + "field": "address string", + "location": "likely in Address::from_string (not shown, but implied usage)", + "validated_by": "boost::asio::ip::address::from_string (implied, likely in Address::from_string)", + "validates": [ + "address string is a valid IPv4 or IPv6 address" + ], + "validation_type": "format" + }, + { + "confidence": 0.7, + "error_thrown": "istream failbit set (no exception thrown)", + "field": "port", + "location": "operator>>(std::istream&, Endpoint&)", + "validated_by": "std::stoi or manual parsing (implied, not shown in snippet)", + "validates": [ + "port is a valid integer", + "port is non-negative" + ], + "validation_type": "type|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/beast/net/IPEndpoint.cpp.ai.md b/src/libxrpl/beast/net/IPEndpoint.cpp.ai.md new file mode 100644 index 0000000000..99f02d1c0b --- /dev/null +++ b/src/libxrpl/beast/net/IPEndpoint.cpp.ai.md @@ -0,0 +1,42 @@ +# `IPEndpoint.cpp` — IP Endpoint Parsing, Formatting, and Comparison + +This file provides the implementation of `beast::IP::Endpoint`, the XRPL node's representation of a network socket address: an IP address paired with a port number. It lives in the `beast::IP` namespace alongside `IPAddress.h`, `IPAddressV4.cpp`, and `IPAddressV6.cpp`, collectively forming the low-level networking identity layer used throughout the rippled peer-to-peer stack for peer tracking, connection management, and configuration parsing. + +## What `Endpoint` Is + +`Endpoint` is a thin value type holding a `beast::IP::Address` — itself a typedef for `boost::asio::ip::address` — and a `Port` (`std::uint16_t`). The header adds hashing support for both `std::hash` and `boost::hash`, making `Endpoint` directly usable as an unordered map key, which is critical for the peer table. The `.cpp` implements the non-trivial parts: constructors, string round-tripping, ordering, and stream I/O. + +## Parsing Pipeline + +The parsing design is deliberately layered. + +`from_string_checked()` is the safe entry point. It enforces a 64-character length cap before doing any further work, rejecting obviously malformed input cheaply. It trims whitespace with `boost::trim_copy`, wraps the result in a `std::stringstream`, and delegates to `operator>>`. After the stream extraction, it additionally checks `is.rdbuf()->in_avail() == 0` — confirming the entire input was consumed with no trailing garbage. On any failure, it returns `std::nullopt` rather than throwing. + +`from_string()` is a convenience shim that simply unwraps `from_string_checked()`, returning a default-constructed (zero-port, unspecified-address) `Endpoint` on failure. Callers that need to distinguish success from failure should prefer `from_string_checked`. + +The real logic lives in `operator>>(std::istream&, Endpoint&)`. Rather than handing the entire string to `boost::asio::ip::make_address` directly, the operator walks the stream character-by-character. This serves two purposes: detecting the address/port boundary correctly, and avoiding reliance on Boost's parser for delimiter handling. + +## Stream Extraction Design + +The operator reads the first character to decide the format: + +- If it is `[`, the input is a bracketed IPv6 endpoint (`[::1]:8080`), and `readTo` is set to `]`. +- Otherwise the character is appended to the address string and the operator infers the type lazily: the first `.` sets `readTo = ':'` (IPv4, colon separates port), while the first `:` sets `readTo = ' '` (bare IPv6, historically space-separated from port). + +The character whitelist — `'.'`, `'0'`–`':'` (covering digits and `:`), `'a'`–`'f'`, `'A'`–`'F'` — is deliberately minimal and matches the valid character set of both IPv4 dotted-decimal and IPv6 colon-hex notation. Any character outside this set causes `unget()` and `failbit` to be set. Length is bounded: if the address string hits `INET6_ADDRSTRLEN` characters without resolving, the stream is marked failed. + +A comment in the code explicitly acknowledges a **legacy format** where a space character serves as the address/port separator. This is honored via the `isspace` check in the loop-exit condition, which means space terminates the address portion just as cleanly as the expected delimiter. This backward compatibility is a deliberate protocol consideration for reading stored peer data. + +After bracket handling for IPv6 endpoints (checking that the character following `]` is either a space or `:`), the accumulated address string is validated through `boost::asio::ip::make_address()` with an error code. Only then is the port read from the stream using the stream's own integer extraction (`is >> port`), inheriting that operator's parsing and range semantics. + +## String Formatting + +`to_string()` produces RFC 5952-style output. It pre-reserves the string capacity based on address version and whether a port is present — avoiding reallocation for the common case. The IPv6-with-port form is `[addr]:port`; IPv4-with-port is `addr:port`; address-only omits the port entirely. This symmetry with the parser ensures round-trip fidelity. + +## Ordering + +`operator<` implements lexicographic address-then-port ordering. Address comparison is done first; only when addresses are equal does port break the tie. This ordering is consistent and total, enabling use of `Endpoint` in `std::set` or `std::map` without a custom comparator. The header derives `!=`, `>`, `<=`, and `>=` from the two primitive operators defined in this file. + +## Design Tradeoffs + +The manual character scanner in `operator>>` is more verbose than simply feeding the whole string to Boost's address parser, but it is necessary for correct delimiter detection — Boost's parser does not know where the address ends and the port begins. The 64-character input cap in `from_string_checked` is a cheap denial-of-service guard against pathologically long inputs reaching the more expensive parsing path. Failures propagate via `std::ios_base::failbit` rather than exceptions, consistent with standard C++ stream idioms and the no-exception policy typical in network I/O hot paths. \ No newline at end of file diff --git a/src/libxrpl/beast/utility/beast_Journal.cpp.ai.json b/src/libxrpl/beast/utility/beast_Journal.cpp.ai.json new file mode 100644 index 0000000000..3d03c6882d --- /dev/null +++ b/src/libxrpl/beast/utility/beast_Journal.cpp.ai.json @@ -0,0 +1,377 @@ +{ + "args": [ + { + "lineno": 18, + "name": "severities::Severity" + }, + { + "lineno": 27, + "name": "bool" + }, + { + "lineno": 35, + "name": "severities::Severity" + }, + { + "lineno": 39, + "name": "severities::Severity" + }, + { + "lineno": 39, + "name": "std::string const&" + }, + { + "lineno": 43, + "name": "severities::Severity" + }, + { + "lineno": 43, + "name": "std::string const&" + }, + { + "lineno": 56, + "name": "Severity" + }, + { + "lineno": 56, + "name": "bool" + }, + { + "lineno": 62, + "name": "Severity" + }, + { + "lineno": 71, + "name": "bool" + }, + { + "lineno": 79, + "name": "Severity" + }, + { + "lineno": 85, + "name": "Sink&" + }, + { + "lineno": 85, + "name": "Severity" + }, + { + "lineno": 90, + "name": "Stream const&" + }, + { + "lineno": 90, + "name": "std::ostream& manip(std::ostream&)" + }, + { + "lineno": 106, + "name": "std::ostream& manip(std::ostream&)" + }, + { + "lineno": 112, + "name": "std::ostream& manip(std::ostream&)" + } + ], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "NullJournalSink" + } + ], + "code_paths": [ + { + "call_chain": [ + "Journal::Sink::active", + "level >= thresh_" + ], + "entry_point": "Journal::Sink::active", + "purpose": "Determines if a log message at a given severity should be processed (i.e., is above the threshold).", + "validation_points": [ + "Journal::Sink::active (validates 'level' against 'thresh_')" + ] + }, + { + "call_chain": [ + "Journal::ScopedStream::~ScopedStream", + "m_sink.write(m_level, s)" + ], + "entry_point": "Journal::ScopedStream::~ScopedStream", + "purpose": "On destruction, writes the accumulated log message to the sink if not empty.", + "validation_points": [ + "Journal::Sink::active (indirectly, if called before writing in other code paths)" + ] + }, + { + "call_chain": [ + "Journal::Stream::operator<<", + "Journal::ScopedStream::ScopedStream" + ], + "entry_point": "Journal::Stream::operator<<", + "purpose": "Creates a scoped stream for logging with a manipulator.", + "validation_points": [] + }, + { + "call_chain": [ + "Journal::getNullSink", + "NullJournalSink::active" + ], + "entry_point": "Journal::getNullSink", + "purpose": "Provides a sink that disables all logging (used as a default or null object).", + "validation_points": [ + "NullJournalSink::active (always returns false, disables validation)" + ] + } + ], + "data_flows": [ + { + "field": "level (Severity)", + "flow": [ + "Input to Journal::Sink::active(level)", + "Compared to thresh_ (threshold)", + "Result determines if logging proceeds" + ], + "origin": "Passed as argument to Journal::Sink::active, Journal::Sink::write, etc.", + "transformations": [ + "Compared via operator>= to thresh_" + ], + "validated_at": "Journal::Sink::active" + }, + { + "field": "thresh_ (Severity threshold)", + "flow": [ + "Set in Sink(thresh, ...)", + "Possibly updated via threshold()", + "Used in active(level) for validation" + ], + "origin": "Set in Journal::Sink constructor or via threshold() setter", + "transformations": [ + "Direct assignment" + ], + "validated_at": "Used as validation reference in Journal::Sink::active" + }, + { + "field": "m_console (bool)", + "flow": [ + "Set in Sink(..., console)", + "Possibly updated via console(bool)", + "Queried via console()" + ], + "origin": "Set in Journal::Sink constructor or via console() setter", + "transformations": [ + "Direct assignment" + ], + "validated_at": "No validation, just state" + }, + { + "field": "log message (std::string)", + "flow": [ + "Written to m_ostream via operator<<", + "On destruction, extracted as s", + "Passed to m_sink.write(m_level, s)" + ], + "origin": "Accumulated in Journal::ScopedStream::m_ostream", + "transformations": [ + "String stream accumulation" + ], + "validated_at": "Not directly validated; only written if not empty" + } + ], + "description": "Implements a no-op (null) logging sink and related logging infrastructure for the beast::Journal system, including sink management and scoped stream handling.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "level (Severity)", + "empty", + "string", + "validation" + ], + "evidence": "operator>= (enum comparison) at Journal::Sink::active(Severity level) const", + "issue_pattern": "Missing empty string validation for level (Severity)", + "why_false_positive": "operator>= (enum comparison) validates level (Severity) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/utility/beast_Journal.cpp", + "functions": [ + { + "args": [], + "lineno": 12, + "name": "NullJournalSink::NullJournalSink" + }, + { + "args": [], + "lineno": 16, + "name": "NullJournalSink::~NullJournalSink" + }, + { + "args": [ + "severities::Severity" + ], + "lineno": 18, + "name": "NullJournalSink::active" + }, + { + "args": [], + "lineno": 23, + "name": "NullJournalSink::console" + }, + { + "args": [ + "bool" + ], + "lineno": 27, + "name": "NullJournalSink::console" + }, + { + "args": [], + "lineno": 31, + "name": "NullJournalSink::threshold" + }, + { + "args": [ + "severities::Severity" + ], + "lineno": 35, + "name": "NullJournalSink::threshold" + }, + { + "args": [ + "severities::Severity", + "std::string const&" + ], + "lineno": 39, + "name": "NullJournalSink::write" + }, + { + "args": [ + "severities::Severity", + "std::string const&" + ], + "lineno": 43, + "name": "NullJournalSink::writeAlways" + }, + { + "args": [], + "lineno": 49, + "name": "Journal::getNullSink" + }, + { + "args": [ + "Severity", + "bool" + ], + "lineno": 56, + "name": "Journal::Sink::Sink" + }, + { + "args": [], + "lineno": 60, + "name": "Journal::Sink::~Sink" + }, + { + "args": [ + "Severity" + ], + "lineno": 62, + "name": "Journal::Sink::active" + }, + { + "args": [], + "lineno": 67, + "name": "Journal::Sink::console" + }, + { + "args": [ + "bool" + ], + "lineno": 71, + "name": "Journal::Sink::console" + }, + { + "args": [], + "lineno": 75, + "name": "Journal::Sink::threshold" + }, + { + "args": [ + "Severity" + ], + "lineno": 79, + "name": "Journal::Sink::threshold" + }, + { + "args": [ + "Sink&", + "Severity" + ], + "lineno": 85, + "name": "Journal::ScopedStream::ScopedStream" + }, + { + "args": [ + "Stream const&", + "std::ostream& manip(std::ostream&)" + ], + "lineno": 90, + "name": "Journal::ScopedStream::ScopedStream" + }, + { + "args": [], + "lineno": 95, + "name": "Journal::ScopedStream::~ScopedStream" + }, + { + "args": [ + "std::ostream& manip(std::ostream&)" + ], + "lineno": 106, + "name": "Journal::ScopedStream::operator<<" + }, + { + "args": [ + "std::ostream& manip(std::ostream&)" + ], + "lineno": 112, + "name": "Journal::Stream::operator<<" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "beast" + } + ], + "test_coverage_notes": "This file is utility code for logging. Direct unit tests for NullJournalSink and Journal::Sink are unlikely to exist in this file. Instead, coverage would come from higher-level logging tests or integration tests that exercise logging at various severity levels. Test gaps: No explicit validation of edge cases for severity threshold logic, nor for NullJournalSink behavior. Tests would likely be found in files testing logging or Journal usage, e.g., 'test_beast_Journal.cpp', 'test_Journal.cpp', or broader system tests that check log output at different severities. NullJournalSink is a null object, so its behavior (always inactive) is trivial and may not be directly tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation, no framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "none (returns bool)", + "field": "level (Severity)", + "location": "Journal::Sink::active(Severity level) const", + "validated_by": "operator>= (enum comparison)", + "validates": [ + "Checks if the log level is active by comparing input 'level' to threshold 'thresh_'" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/beast/utility/beast_Journal.cpp.ai.md b/src/libxrpl/beast/utility/beast_Journal.cpp.ai.md new file mode 100644 index 0000000000..e46fa1688c --- /dev/null +++ b/src/libxrpl/beast/utility/beast_Journal.cpp.ai.md @@ -0,0 +1,51 @@ +# `src/libxrpl/beast/utility/beast_Journal.cpp` — Journal Logging Core Implementation + +## Role in the System + +This file provides the out-of-line definitions for `beast::Journal`'s logging infrastructure — specifically the `Journal::Sink` base class, the `Journal::ScopedStream` RAII message builder, and the static `Journal::getNullSink()` accessor. The `Journal` system is the universal logging layer in rippled: every subsystem receives a `Journal` by value, stores it cheaply, and writes through it without knowing where the output goes. This file implements the machinery that makes that abstraction concrete while keeping the header fast and compilable. + +## `NullJournalSink` — the Null Object + +`NullJournalSink` is a file-local class that implements `Journal::Sink` by doing absolutely nothing. It initialises with `severities::kDisabled`, always returns `false` from `active()`, ignores `threshold()` mutations, and no-ops both `write()` and `writeAlways()`. This is the Null Object pattern applied to a polymorphic interface: callers never need to guard against a null sink pointer because a valid — but silent — sink is always available. + +`Journal::getNullSink()` returns a reference to a function-local static instance of this type. The C++11 guarantee of thread-safe static initialisation means no mutex is needed, and the single shared instance is safe for concurrent callers. The static lifetime avoids lifetime hazards for callers that default-construct a `Journal::Stream` with no real sink configured. + +## `Journal::Sink` — Base Class Defaults + +`Journal::Sink` is a pure-virtual base, but most of its virtual methods have sensible default implementations defined here so concrete subclasses don't have to repeat boilerplate: + +- `active(Severity level)` compares `level >= thresh_` — a simple threshold gate. This is the hot path check; callers are encouraged to call it before doing any string formatting to skip the work entirely when a level is disabled. +- The `console()` getter and setter manage a boolean flag for whether messages should also appear in the MSVC Output Window — a portability-oriented hook. +- The `threshold()` getter and setter provide runtime control over the minimum severity the sink will emit, exposed by the admin interface in `Logs` (see `Log.cpp`). + +The two purely virtual methods, `write()` and `writeAlways()`, are intentionally left to subclasses. `write()` is permitted to honour the current threshold; `writeAlways()` must bypass it entirely — a deliberate escape hatch for administrative overrides where output is required regardless of verbosity setting. + +## `Journal::ScopedStream` — RAII Log Message Construction + +`ScopedStream` is the mechanism behind the common rippled logging idiom: + +```cpp +JLOG(j.debug()) << "accepted tx " << txid << " fee=" << fee; +``` + +Each `operator<<` on a `Journal::Stream` constructs a `ScopedStream`, accumulates the chained output into an internal `std::ostringstream`, and then — at destruction — calls `m_sink.write()` with the complete formatted string. This design has several consequences worth noting: + +**Atomic delivery.** The full message is assembled in memory before touching the sink. A concrete sink like `Logs::Sink` can then emit it atomically under a mutex, preventing interleaved output from concurrent threads. If the stream were wired directly to an `ostream` backed by a file, individual `<<` calls from different threads could interleave. + +**Consistent formatting defaults.** Both `ScopedStream` constructors call the base constructor which applies `std::boolalpha` and `std::showbase` to `m_ostream` immediately. Every log message in rippled therefore prints booleans as `true`/`false` and hexadecimal values with a `0x` prefix, without any per-callsite effort. + +**The lonely-newline special case.** The destructor checks whether the accumulated string is exactly `"\n"` and, if so, passes an empty string to `write()` instead. This prevents a blank `operator<<(std::endl)` call from producing a visually empty but severity-tagged log line. + +The `ScopedStream(Stream const&, std::ostream& manip(std::ostream&))` constructor delegates to the primary constructor and then immediately applies an `std::ostream` manipulator (e.g. `std::hex`). This is the entry point when `Journal::Stream::operator<<` is called with a manipulator — the `Stream` creates the `ScopedStream` with the manipulator pre-applied, after which further `<<` chaining continues accumulating into the same buffer. + +## `Journal::Stream::operator<<` — Entry Point for Manipulators + +The only non-trivial `Stream` method defined in this file is `operator<<(std::ostream& manip(std::ostream&))`. Template `operator<<` overloads (for arbitrary value types) are defined inline in the header; only the manipulator overload has its definition here because it calls `ScopedStream`'s constructor by value, which requires `ScopedStream` to be a complete type. This is a common header/source split pattern to avoid circular completeness requirements. + +## Relationship to `Log.cpp` + +This file defines the abstract framework; `src/libxrpl/basics/Log.cpp` supplies the concrete `Logs::Sink` that actually formats and writes to files and stderr. The `Journal` abstraction is deliberately kept clean of any file I/O, thread management, or string formatting for severity labels — all of that lives in `Log.cpp`. `beast_Journal.cpp` is thus a stable, low-dependency foundation that can be tested and reasoned about independently of the production logging infrastructure. + +## Static Assertions as Enforced Contracts + +The header surrounding this file contains a dense block of `static_assert` checks on the copyability and movability of `Sink`, `ScopedStream`, `Stream`, and `Journal`. `Sink` is explicitly non-copyable and non-movable — it is a heavyweight, potentially mutex-owning resource owned by `Logs`. `Journal` itself is copyable and assignable, reflecting its intended use as a cheap value type stored in every server component. These assertions turn API-contract violations into compile errors rather than subtle runtime bugs. \ No newline at end of file diff --git a/src/libxrpl/beast/utility/beast_PropertyStream.cpp.ai.json b/src/libxrpl/beast/utility/beast_PropertyStream.cpp.ai.json new file mode 100644 index 0000000000..74b0b8cae2 --- /dev/null +++ b/src/libxrpl/beast/utility/beast_PropertyStream.cpp.ai.json @@ -0,0 +1,979 @@ +{ + "args": [ + { + "lineno": 13, + "name": "source" + }, + { + "lineno": 31, + "name": "map" + }, + { + "lineno": 31, + "name": "key" + }, + { + "lineno": 35, + "name": "other" + }, + { + "lineno": 46, + "name": "manip" + }, + { + "lineno": 52, + "name": "stream" + }, + { + "lineno": 56, + "name": "parent" + }, + { + "lineno": 61, + "name": "key" + }, + { + "lineno": 61, + "name": "map" + }, + { + "lineno": 65, + "name": "key" + }, + { + "lineno": 65, + "name": "stream" + }, + { + "lineno": 87, + "name": "key" + }, + { + "lineno": 87, + "name": "map" + }, + { + "lineno": 91, + "name": "key" + }, + { + "lineno": 91, + "name": "stream" + }, + { + "lineno": 109, + "name": "name" + }, + { + "lineno": 124, + "name": "source" + }, + { + "lineno": 134, + "name": "child" + }, + { + "lineno": 154, + "name": "stream" + }, + { + "lineno": 159, + "name": "stream" + }, + { + "lineno": 167, + "name": "stream" + }, + { + "lineno": 167, + "name": "path" + }, + { + "lineno": 177, + "name": "path" + }, + { + "lineno": 191, + "name": "path" + }, + { + "lineno": 199, + "name": "path" + }, + { + "lineno": 210, + "name": "path" + }, + { + "lineno": 225, + "name": "name" + }, + { + "lineno": 236, + "name": "path" + }, + { + "lineno": 249, + "name": "name" + }, + { + "lineno": 258, + "name": "map" + }, + { + "lineno": 265, + "name": "key" + }, + { + "lineno": 265, + "name": "value" + }, + { + "lineno": 272, + "name": "key" + }, + { + "lineno": 272, + "name": "value" + }, + { + "lineno": 276, + "name": "key" + }, + { + "lineno": 276, + "name": "value" + }, + { + "lineno": 280, + "name": "key" + }, + { + "lineno": 280, + "name": "value" + }, + { + "lineno": 284, + "name": "key" + }, + { + "lineno": 284, + "name": "value" + }, + { + "lineno": 288, + "name": "key" + }, + { + "lineno": 288, + "name": "value" + }, + { + "lineno": 292, + "name": "key" + }, + { + "lineno": 292, + "name": "value" + }, + { + "lineno": 296, + "name": "key" + }, + { + "lineno": 296, + "name": "value" + }, + { + "lineno": 300, + "name": "key" + }, + { + "lineno": 300, + "name": "value" + }, + { + "lineno": 304, + "name": "key" + }, + { + "lineno": 304, + "name": "value" + }, + { + "lineno": 308, + "name": "key" + }, + { + "lineno": 308, + "name": "value" + }, + { + "lineno": 312, + "name": "key" + }, + { + "lineno": 312, + "name": "value" + }, + { + "lineno": 316, + "name": "key" + }, + { + "lineno": 316, + "name": "value" + }, + { + "lineno": 320, + "name": "key" + }, + { + "lineno": 320, + "name": "value" + }, + { + "lineno": 324, + "name": "key" + }, + { + "lineno": 324, + "name": "value" + }, + { + "lineno": 328, + "name": "value" + }, + { + "lineno": 335, + "name": "value" + }, + { + "lineno": 339, + "name": "value" + }, + { + "lineno": 343, + "name": "value" + }, + { + "lineno": 347, + "name": "value" + }, + { + "lineno": 351, + "name": "value" + }, + { + "lineno": 355, + "name": "value" + }, + { + "lineno": 359, + "name": "value" + }, + { + "lineno": 363, + "name": "value" + }, + { + "lineno": 367, + "name": "value" + }, + { + "lineno": 371, + "name": "value" + }, + { + "lineno": 375, + "name": "value" + }, + { + "lineno": 379, + "name": "value" + }, + { + "lineno": 383, + "name": "value" + }, + { + "lineno": 387, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "Source* source" + ], + "lineno": 12, + "name": "PropertyStream::Item" + }, + { + "args": [ + "Map const& map", + "std::string const& key" + ], + "lineno": 30, + "name": "PropertyStream::Proxy" + }, + { + "args": [ + "PropertyStream& stream" + ], + "lineno": 51, + "name": "PropertyStream::Map" + }, + { + "args": [ + "std::string const& key", + "Map& map" + ], + "lineno": 86, + "name": "PropertyStream::Set" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 108, + "name": "PropertyStream::Source" + } + ], + "code_paths": [ + { + "call_chain": [ + "PropertyStream::Source::add", + "XRPL_ASSERT (source.parent_ == nullptr)", + "children_.push_back(source.item_)", + "source.parent_ = this" + ], + "entry_point": "PropertyStream::Source::add", + "purpose": "Adds a child Source to the current Source, ensuring the child has no parent (i.e., is not already attached elsewhere).", + "validation_points": [ + "XRPL_ASSERT (source.parent_ == nullptr)" + ] + }, + { + "call_chain": [ + "PropertyStream::Source::~Source", + "std::lock_guard(lock_)", + "if (parent_ != nullptr) parent_->remove(*this)", + "removeAll()" + ], + "entry_point": "PropertyStream::Source::~Source", + "purpose": "Destructor for Source, ensures removal from parent and cleanup of children.", + "validation_points": [ + "if (parent_ != nullptr) parent_->remove(*this)" + ] + }, + { + "call_chain": [ + "PropertyStream::Proxy::~Proxy", + "std::string const s(m_ostream.str())", + "if (!s.empty()) m_map->add(m_key, s)" + ], + "entry_point": "PropertyStream::Proxy::~Proxy", + "purpose": "On destruction, flushes the Proxy's stream to the Map if not empty.", + "validation_points": [ + "if (!s.empty())" + ] + }, + { + "call_chain": [ + "PropertyStream::Map::Map(Set& parent)", + "m_stream(parent.stream())", + "m_stream.map_begin()" + ], + "entry_point": "PropertyStream::Map::Map", + "purpose": "Constructs a Map, starting a new map in the PropertyStream.", + "validation_points": [] + }, + { + "call_chain": [ + "PropertyStream::Set::Set(std::string const& key, Map& map)", + "m_stream(map.stream())", + "m_stream.array_begin(key)" + ], + "entry_point": "PropertyStream::Set::Set", + "purpose": "Constructs a Set, starting a new array in the PropertyStream.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "parent_ (PropertyStream::Source)", + "flow": [ + "Source::Source (parent_ = nullptr)", + "Source::add (XRPL_ASSERT parent_ == nullptr, then parent_ = this)", + "Source::~Source (if parent_ != nullptr, parent_->remove(*this))" + ], + "origin": "Initialized as nullptr in Source constructor", + "transformations": [ + "Set to 'this' when added as child", + "Reset to nullptr when removed" + ], + "validated_at": "Source::add (XRPL_ASSERT parent_ == nullptr)" + }, + { + "field": "children_ (PropertyStream::Source)", + "flow": [ + "Source::add (children_.push_back(source.item_))", + "Source::~Source (removeAll())" + ], + "origin": "Empty vector at Source construction", + "transformations": [ + "Child items are appended", + "Cleared on destruction" + ], + "validated_at": "Indirectly via XRPL_ASSERT in add" + }, + { + "field": "m_key (PropertyStream::Proxy)", + "flow": [ + "Proxy::Proxy (m_key = key)", + "Proxy::~Proxy (used in m_map->add(m_key, s))" + ], + "origin": "Set in Proxy constructor", + "transformations": [ + "No transformation, just passed through" + ], + "validated_at": "No explicit validation" + }, + { + "field": "m_ostream (PropertyStream::Proxy)", + "flow": [ + "Proxy::operator<< (writes to m_ostream)", + "Proxy::~Proxy (reads m_ostream.str())" + ], + "origin": "std::ostringstream, default constructed", + "transformations": [ + "Data is streamed in via operator<<", + "Converted to string on destruction" + ], + "validated_at": "Proxy::~Proxy (if (!s.empty()))" + } + ], + "description": "Implements the PropertyStream utility for structured property streaming, including classes for hierarchical property sources, maps, sets, and proxies, with thread-safe management and value addition for various types.", + "false_positive_patterns": [ + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/beast/utility/beast_PropertyStream.cpp", + "functions": [ + { + "args": [ + "Source* source" + ], + "lineno": 13, + "name": "PropertyStream::Item::Item" + }, + { + "args": [], + "lineno": 17, + "name": "PropertyStream::Item::source" + }, + { + "args": [], + "lineno": 21, + "name": "PropertyStream::Item::operator->" + }, + { + "args": [], + "lineno": 25, + "name": "PropertyStream::Item::operator*" + }, + { + "args": [ + "Map const& map", + "std::string const& key" + ], + "lineno": 31, + "name": "PropertyStream::Proxy::Proxy" + }, + { + "args": [ + "Proxy const& other" + ], + "lineno": 35, + "name": "PropertyStream::Proxy::Proxy" + }, + { + "args": [], + "lineno": 39, + "name": "PropertyStream::Proxy::~Proxy" + }, + { + "args": [ + "std::ostream& manip(std::ostream&)" + ], + "lineno": 46, + "name": "PropertyStream::Proxy::operator<<" + }, + { + "args": [ + "PropertyStream& stream" + ], + "lineno": 52, + "name": "PropertyStream::Map::Map" + }, + { + "args": [ + "Set& parent" + ], + "lineno": 56, + "name": "PropertyStream::Map::Map" + }, + { + "args": [ + "std::string const& key", + "Map& map" + ], + "lineno": 61, + "name": "PropertyStream::Map::Map" + }, + { + "args": [ + "std::string const& key", + "PropertyStream& stream" + ], + "lineno": 65, + "name": "PropertyStream::Map::Map" + }, + { + "args": [], + "lineno": 69, + "name": "PropertyStream::Map::~Map" + }, + { + "args": [], + "lineno": 73, + "name": "PropertyStream::Map::stream" + }, + { + "args": [], + "lineno": 77, + "name": "PropertyStream::Map::stream" + }, + { + "args": [ + "std::string const& key" + ], + "lineno": 81, + "name": "PropertyStream::Map::operator[]" + }, + { + "args": [ + "std::string const& key", + "Map& map" + ], + "lineno": 87, + "name": "PropertyStream::Set::Set" + }, + { + "args": [ + "std::string const& key", + "PropertyStream& stream" + ], + "lineno": 91, + "name": "PropertyStream::Set::Set" + }, + { + "args": [], + "lineno": 95, + "name": "PropertyStream::Set::~Set" + }, + { + "args": [], + "lineno": 99, + "name": "PropertyStream::Set::stream" + }, + { + "args": [], + "lineno": 103, + "name": "PropertyStream::Set::stream" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 109, + "name": "PropertyStream::Source::Source" + }, + { + "args": [], + "lineno": 113, + "name": "PropertyStream::Source::~Source" + }, + { + "args": [], + "lineno": 120, + "name": "PropertyStream::Source::name" + }, + { + "args": [ + "Source& source" + ], + "lineno": 124, + "name": "PropertyStream::Source::add" + }, + { + "args": [ + "Source& child" + ], + "lineno": 134, + "name": "PropertyStream::Source::remove" + }, + { + "args": [], + "lineno": 144, + "name": "PropertyStream::Source::removeAll" + }, + { + "args": [ + "PropertyStream& stream" + ], + "lineno": 154, + "name": "PropertyStream::Source::write_one" + }, + { + "args": [ + "PropertyStream& stream" + ], + "lineno": 159, + "name": "PropertyStream::Source::write" + }, + { + "args": [ + "PropertyStream& stream", + "std::string const& path" + ], + "lineno": 167, + "name": "PropertyStream::Source::write" + }, + { + "args": [ + "std::string path" + ], + "lineno": 177, + "name": "PropertyStream::Source::find" + }, + { + "args": [ + "std::string* path" + ], + "lineno": 191, + "name": "PropertyStream::Source::peel_leading_slash" + }, + { + "args": [ + "std::string* path" + ], + "lineno": 199, + "name": "PropertyStream::Source::peel_trailing_slashstar" + }, + { + "args": [ + "std::string* path" + ], + "lineno": 210, + "name": "PropertyStream::Source::peel_name" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 225, + "name": "PropertyStream::Source::find_one_deep" + }, + { + "args": [ + "std::string path" + ], + "lineno": 236, + "name": "PropertyStream::Source::find_path" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 249, + "name": "PropertyStream::Source::find_one" + }, + { + "args": [ + "Map&" + ], + "lineno": 258, + "name": "PropertyStream::Source::onWrite" + }, + { + "args": [ + "std::string const& key", + "bool value" + ], + "lineno": 265, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "char value" + ], + "lineno": 272, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "signed char value" + ], + "lineno": 276, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "unsigned char value" + ], + "lineno": 280, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "short value" + ], + "lineno": 284, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "unsigned short value" + ], + "lineno": 288, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "int value" + ], + "lineno": 292, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "unsigned int value" + ], + "lineno": 296, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "long value" + ], + "lineno": 300, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "unsigned long value" + ], + "lineno": 304, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "long long value" + ], + "lineno": 308, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "unsigned long long value" + ], + "lineno": 312, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "float value" + ], + "lineno": 316, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "double value" + ], + "lineno": 320, + "name": "PropertyStream::add" + }, + { + "args": [ + "std::string const& key", + "long double value" + ], + "lineno": 324, + "name": "PropertyStream::add" + }, + { + "args": [ + "bool value" + ], + "lineno": 328, + "name": "PropertyStream::add" + }, + { + "args": [ + "char value" + ], + "lineno": 335, + "name": "PropertyStream::add" + }, + { + "args": [ + "signed char value" + ], + "lineno": 339, + "name": "PropertyStream::add" + }, + { + "args": [ + "unsigned char value" + ], + "lineno": 343, + "name": "PropertyStream::add" + }, + { + "args": [ + "short value" + ], + "lineno": 347, + "name": "PropertyStream::add" + }, + { + "args": [ + "unsigned short value" + ], + "lineno": 351, + "name": "PropertyStream::add" + }, + { + "args": [ + "int value" + ], + "lineno": 355, + "name": "PropertyStream::add" + }, + { + "args": [ + "unsigned int value" + ], + "lineno": 359, + "name": "PropertyStream::add" + }, + { + "args": [ + "long value" + ], + "lineno": 363, + "name": "PropertyStream::add" + }, + { + "args": [ + "unsigned long value" + ], + "lineno": 367, + "name": "PropertyStream::add" + }, + { + "args": [ + "long long value" + ], + "lineno": 371, + "name": "PropertyStream::add" + }, + { + "args": [ + "unsigned long long value" + ], + "lineno": 375, + "name": "PropertyStream::add" + }, + { + "args": [ + "float value" + ], + "lineno": 379, + "name": "PropertyStream::add" + }, + { + "args": [ + "double value" + ], + "lineno": 383, + "name": "PropertyStream::add" + }, + { + "args": [ + "long double value" + ], + "lineno": 387, + "name": "PropertyStream::add" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "beast" + } + ], + "test_coverage_notes": "This file contains utility and data structure code for property streaming, with validation primarily via XRPL_ASSERT in Source::add. There is no direct evidence of test files in this snippet. Likely, tests would exist in higher-level integration or unit tests for PropertyStream, Source, Map, Set, and Proxy, possibly in files like test_PropertyStream.cpp or similar. Validation is minimal and mostly structural (parent_ checks). There is no input validation for string keys or stream data. Potential gaps: No explicit tests for invalid parent/child relationships, no tests for empty or malformed keys, and no tests for concurrent access (though locks are used).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/beast/utility/beast_PropertyStream.cpp.ai.md b/src/libxrpl/beast/utility/beast_PropertyStream.cpp.ai.md new file mode 100644 index 0000000000..566b36327c --- /dev/null +++ b/src/libxrpl/beast/utility/beast_PropertyStream.cpp.ai.md @@ -0,0 +1,41 @@ +# `beast_PropertyStream.cpp` — Hierarchical Diagnostic Property Emission + +## Role in the System + +This file implements the `beast::PropertyStream` family of classes: an infrastructure for exposing structured, hierarchical runtime state from XRPL subsystems. Components such as `LedgerCleaner`, `PeerFinder`, `OverlayImpl`, and `ResourceManager` inherit from `PropertyStream::Source` and override `onWrite()` to report their internal state (counters, flags, status strings) in a tree structure suitable for diagnostics, RPC, or logging. The design separates *what to emit* (the `Source` subclass hierarchy) from *how to emit it* (the abstract `PropertyStream` backend, which concrete implementations serialize to JSON or other formats). + +## RAII Scoping: `Map` and `Set` + +`Map` and `Set` are the primary user-facing types. They are pure RAII wrappers around the abstract `PropertyStream` protocol. `Map` calls `map_begin()` in its constructor and `map_end()` in its destructor; `Set` does the same with `array_begin()` / `array_end()`. Because callers instantiate them as stack variables, the open/close bracketing of the output format is enforced automatically — nested `Map` objects produce nested maps in the output stream without any explicit close calls. + +`Map` is deliberately non-copyable and non-movable. This enforces single ownership semantics: each `Map` instance corresponds to exactly one scope in the output. The multiple constructors (`Map(PropertyStream&)`, `Map(Set&)`, `Map(key, Map&)`, `Map(key, PropertyStream&)`) cover the compositional cases — a keyed sub-map within a parent map, an anonymous map within a set, and a root map wrapping a stream directly. + +## The `Proxy` Pattern + +`Map::operator[](key)` returns a `Proxy` rather than writing immediately. The `Proxy` holds a reference to the parent `Map`, the key string, and a `mutable std::ostringstream`. Callers write values into it using `<<` or `operator=`, and on destruction the proxy flushes the accumulated string to the map — but only if the string is non-empty. This deferred-commit pattern means `map["foo"] = 42` and `map["foo"] << "bar"` both work uniformly, and there is no entry emitted if nothing is written to the proxy. The `mutable` qualifier on `m_ostream` allows `const Proxy` objects returned from `const Map::operator[]` to still accumulate output. + +## The `Source` Tree + +`Source` forms a named tree of diagnostic providers. Each `Source` has a string name and a `List children_` intrusive list. Rather than storing `Source*` pointers directly, the list holds `Item` nodes, and each `Source` contains exactly one embedded `Item item_` that points back to itself. This indirection allows the `Source` object to be linked into the parent's `children_` list without allocating a separate node — the intrusive node is part of the `Source` itself. + +`Source::add(Source&)` wires a child into the parent's list. The design enforces a strict invariant: `parent_` must be `nullptr` before attachment, checked via `XRPL_ASSERT`. This prevents a `Source` from being added to two parents simultaneously, which would corrupt both lists. The dual-lock acquisition in `add()` and `remove()` uses `std::lock()` (not sequential `lock()` calls) to avoid deadlock when two threads try to cross-link sources concurrently. + +The `lock_` is a `std::recursive_mutex`. This is required because the destructor calls `parent_->remove(*this)` and then `removeAll()`, and `removeAll()` calls `remove()` while already holding `lock_` — the recursion must be re-entrant. The destructor's cleanup order (detach from parent, then detach children) prevents dangling `parent_` pointers in children and dangling child entries in the parent. + +## Path Navigation + +`Source::write(stream, path)` supports slash-delimited path queries for targeted diagnostic output. The path syntax is parsed using three mutating helper functions on a `std::string*`: + +- `peel_leading_slash`: detects and strips a leading `/`, signaling a rooted (absolute) path. +- `peel_trailing_slashstar`: detects and strips a trailing `/*`, signaling a deep (recursive) query. +- `peel_name`: consumes the next path component up to the first `/`. + +An unrooted path triggers `find_one_deep()`, which does a depth-first recursive search through the full subtree for the first `Source` whose name matches the first path component. Once a starting source is found, `find_path()` walks the remaining path components by repeatedly calling `find_one()` — which only examines immediate children. The `find()` function returns a `std::pair` where the boolean indicates whether the wildcard was present (meaning the write should recurse into children). + +## Type Dispatch in `PropertyStream::add()` + +The base class provides overloads of `add(key, value)` for every fundamental numeric type. All numeric overloads delegate to the template `lexical_add(key, value)`, which converts the value through a `std::stringstream` before calling the pure-virtual `add(key, std::string)`. Boolean values are special-cased to emit the strings `"true"` / `"false"` rather than `"1"` / `"0"`, which is consistent with how boolean state is expected to appear in XRPL diagnostic output. This overload structure keeps the subclass contract minimal: a concrete `PropertyStream` only needs to implement `add(key, std::string)`, `map_begin/end`, and `array_begin/end`. + +## Concurrency Considerations + +The design is multi-thread-aware but not designed for high-frequency concurrent writes. `Source::write()` acquires `lock_` only briefly while iterating over `children_` after calling `onWrite()`. This means the per-source write itself (`onWrite`) is not protected by the property stream lock — subclasses are responsible for protecting their own internal state (as `LedgerCleaner` does, using its own `mutex_` inside `onWrite`). The tree structure (parent/child linkage) is protected, but diagnostic emission is not serialized across the tree. A `Source` being destroyed concurrently with a traversal is safe only within the constraints of the per-`Source` lock; callers should ensure tree topology is stable during a full `write()` pass. \ No newline at end of file diff --git a/src/libxrpl/conditions/Condition.cpp.ai.json b/src/libxrpl/conditions/Condition.cpp.ai.json new file mode 100644 index 0000000000..49d2b0f192 --- /dev/null +++ b/src/libxrpl/conditions/Condition.cpp.ai.json @@ -0,0 +1,583 @@ +{ + "args": [ + { + "lineno": 28, + "name": "type" + }, + { + "lineno": 28, + "name": "s" + }, + { + "lineno": 28, + "name": "ec" + }, + { + "lineno": 91, + "name": "s" + }, + { + "lineno": 91, + "name": "ec" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Condition::deserialize", + "parsePreamble", + "isConstructed / isContextSpecific", + "detail::loadSimpleSha256 (for simple types)", + "parsePreamble", + "isPrimitive / isContextSpecific", + "parseOctetString", + "parsePreamble", + "isPrimitive / isContextSpecific", + "parseInteger" + ], + "entry_point": "Condition::deserialize", + "purpose": "Deserializes a binary-encoded Condition, validates structure, type, fingerprint, and cost, and constructs a Condition object.", + "validation_points": [ + "Condition::deserialize: checks for empty buffer, malformed encoding, buffer underfull, max size", + "parsePreamble: validates DER preamble", + "isConstructed / isContextSpecific: validates type encoding", + "detail::loadSimpleSha256: validates fingerprint preamble, tag, length, octet string, cost preamble, tag, integer, trailing garbage, and type-specific cost" + ] + } + ], + "data_flows": [ + { + "field": "DER preamble", + "flow": [ + "Slice s", + "parsePreamble(s, ec)", + "p (preamble struct)" + ], + "origin": "Slice s (input buffer)", + "transformations": [ + "Parsed from buffer", + "Checked for primitive/constructed, context-specific, tag, length" + ], + "validated_at": "parsePreamble, isPrimitive, isConstructed, isContextSpecific" + }, + { + "field": "preamble.tag", + "flow": [ + "p (from parsePreamble)", + "if (p.tag != expected)" + ], + "origin": "parsePreamble result", + "transformations": [ + "Compared to expected tag (0 for fingerprint, 1 for cost)" + ], + "validated_at": "if (p.tag != 0) / if (p.tag != 1)" + }, + { + "field": "preamble.length (fingerprint length)", + "flow": [ + "p.length", + "if (p.length != fingerprintSize)" + ], + "origin": "parsePreamble result", + "transformations": [ + "Compared to constant fingerprintSize (32)" + ], + "validated_at": "if (p.length != fingerprintSize)" + }, + { + "field": "fingerprint (octet string)", + "flow": [ + "Slice s", + "parseOctetString(s, p.length, ec)", + "Buffer b" + ], + "origin": "Slice s (after preamble)", + "transformations": [ + "Extracted as octet string of length p.length" + ], + "validated_at": "parseOctetString" + }, + { + "field": "cost (INTEGER)", + "flow": [ + "Slice s", + "parsePreamble(s, ec)", + "parseInteger(s, p.length, ec)", + "cost" + ], + "origin": "Slice s (after fingerprint)", + "transformations": [ + "Parsed as integer of length p.length" + ], + "validated_at": "parsePreamble (for cost), isPrimitive, isContextSpecific, p.tag == 1, parseInteger" + }, + { + "field": "type-specific cost check", + "flow": [ + "cost", + "if (type == preimageSha256 && cost > PreimageSha256::maxPreimageLength)" + ], + "origin": "cost (parsed integer)", + "transformations": [ + "Compared to maxPreimageLength for preimageSha256" + ], + "validated_at": "if (cost > PreimageSha256::maxPreimageLength)" + } + ], + "description": "Implements deserialization and parsing logic for cryptographic conditions (such as PreimageSha256) in the XRPL (XRP Ledger) using DER encoding, including error handling and type dispatch.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "preamble (DER preamble)", + "empty", + "string", + "validation" + ], + "evidence": "parsePreamble, isPrimitive, isContextSpecific at detail::loadSimpleSha256", + "issue_pattern": "Missing empty string validation for preamble (DER preamble)", + "why_false_positive": "parsePreamble, isPrimitive, isContextSpecific validates preamble (DER preamble) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "preamble (DER preamble)", + "format", + "validation", + "invalid" + ], + "evidence": "parsePreamble, isPrimitive, isContextSpecific at detail::loadSimpleSha256", + "issue_pattern": "Missing format validation for preamble (DER preamble)", + "why_false_positive": "parsePreamble, isPrimitive, isContextSpecific validates preamble (DER preamble) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "preamble.tag", + "empty", + "string", + "validation" + ], + "evidence": "p.tag != 0 at detail::loadSimpleSha256", + "issue_pattern": "Missing empty string validation for preamble.tag", + "why_false_positive": "p.tag != 0 validates preamble.tag for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "preamble.tag", + "format", + "validation", + "invalid" + ], + "evidence": "p.tag != 0 at detail::loadSimpleSha256", + "issue_pattern": "Missing format validation for preamble.tag", + "why_false_positive": "p.tag != 0 validates preamble.tag format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "preamble.length (fingerprint length)", + "empty", + "string", + "validation" + ], + "evidence": "p.length != fingerprintSize at detail::loadSimpleSha256", + "issue_pattern": "Missing empty string validation for preamble.length (fingerprint length)", + "why_false_positive": "p.length != fingerprintSize validates preamble.length (fingerprint length) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "preamble.length (fingerprint length)", + "range", + "bounds", + "validation" + ], + "evidence": "p.length != fingerprintSize at detail::loadSimpleSha256", + "issue_pattern": "Missing range validation for preamble.length (fingerprint length)", + "why_false_positive": "p.length != fingerprintSize validates preamble.length (fingerprint length) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "octet string (fingerprint)", + "empty", + "string", + "validation" + ], + "evidence": "parseOctetString at detail::loadSimpleSha256", + "issue_pattern": "Missing empty string validation for octet string (fingerprint)", + "why_false_positive": "parseOctetString validates octet string (fingerprint) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "octet string (fingerprint)", + "format", + "validation", + "invalid" + ], + "evidence": "parseOctetString at detail::loadSimpleSha256", + "issue_pattern": "Missing format validation for octet string (fingerprint)", + "why_false_positive": "parseOctetString validates octet string (fingerprint) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "preamble (cost field)", + "empty", + "string", + "validation" + ], + "evidence": "parsePreamble, isPrimitive, isContextSpecific at detail::loadSimpleSha256", + "issue_pattern": "Missing empty string validation for preamble (cost field)", + "why_false_positive": "parsePreamble, isPrimitive, isContextSpecific validates preamble (cost field) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "preamble (cost field)", + "format", + "validation", + "invalid" + ], + "evidence": "parsePreamble, isPrimitive, isContextSpecific at detail::loadSimpleSha256", + "issue_pattern": "Missing format validation for preamble (cost field)", + "why_false_positive": "parsePreamble, isPrimitive, isContextSpecific validates preamble (cost field) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "preamble.tag (cost field)", + "empty", + "string", + "validation" + ], + "evidence": "p.tag != 1 at detail::loadSimpleSha256", + "issue_pattern": "Missing empty string validation for preamble.tag (cost field)", + "why_false_positive": "p.tag != 1 validates preamble.tag (cost field) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "preamble.tag (cost field)", + "format", + "validation", + "invalid" + ], + "evidence": "p.tag != 1 at detail::loadSimpleSha256", + "issue_pattern": "Missing format validation for preamble.tag (cost field)", + "why_false_positive": "p.tag != 1 validates preamble.tag (cost field) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "cost (integer field)", + "empty", + "string", + "validation" + ], + "evidence": "parseInteger at detail::loadSimpleSha256", + "issue_pattern": "Missing empty string validation for cost (integer field)", + "why_false_positive": "parseInteger validates cost (integer field) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "trailing data", + "empty", + "string", + "validation" + ], + "evidence": "!s.empty() at detail::loadSimpleSha256", + "issue_pattern": "Missing empty string validation for trailing data", + "why_false_positive": "!s.empty() validates trailing data for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "trailing data", + "format", + "validation", + "invalid" + ], + "evidence": "!s.empty() at detail::loadSimpleSha256", + "issue_pattern": "Missing format validation for trailing data", + "why_false_positive": "!s.empty() validates trailing data format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "cost (preimageSha256 type)", + "empty", + "string", + "validation" + ], + "evidence": "cost > PreimageSha256::maxPreimageLength at detail::loadSimpleSha256", + "issue_pattern": "Missing empty string validation for cost (preimageSha256 type)", + "why_false_positive": "cost > PreimageSha256::maxPreimageLength validates cost (preimageSha256 type) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/conditions/Condition.cpp", + "functions": [ + { + "args": [ + "type", + "s", + "ec" + ], + "lineno": 28, + "name": "loadSimpleSha256" + }, + { + "args": [ + "s", + "ec" + ], + "lineno": 91, + "name": "Condition::deserialize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "cryptoconditions" + }, + { + "lineno": 7, + "name": "detail" + } + ], + "test_coverage_notes": "The code is likely covered by unit tests for Condition deserialization and error handling, typically found in files like 'test/conditions/Condition_test.cpp', 'test/conditions/PreimageSha256_test.cpp', or similar. These tests should cover valid and invalid encodings, boundary conditions (e.g., fingerprint size, cost limits), and error codes. However, gaps may exist for malformed DER encodings, rare error paths (e.g., trailing garbage), and compound condition types, as only simple SHA256 conditions are handled in this code. Full coverage would require tests for all error codes and edge cases in the validation logic.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation using DER parsing utilities (parsePreamble, parseOctetString, parseInteger)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "error::incorrect_encoding", + "field": "preamble (DER preamble)", + "location": "detail::loadSimpleSha256", + "validated_by": "parsePreamble, isPrimitive, isContextSpecific", + "validates": [ + "Checks that the DER preamble is primitive and context-specific" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "error::unexpected_tag", + "field": "preamble.tag", + "location": "detail::loadSimpleSha256", + "validated_by": "p.tag != 0", + "validates": [ + "Checks that the tag for the fingerprint field is 0" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "error::fingerprint_size", + "field": "preamble.length (fingerprint length)", + "location": "detail::loadSimpleSha256", + "validated_by": "p.length != fingerprintSize", + "validates": [ + "Checks that the fingerprint is exactly 32 bytes" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "parseOctetString sets error code", + "field": "octet string (fingerprint)", + "location": "detail::loadSimpleSha256", + "validated_by": "parseOctetString", + "validates": [ + "Checks that the fingerprint field is a valid octet string of the correct length" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "error::malformed_encoding", + "field": "preamble (cost field)", + "location": "detail::loadSimpleSha256", + "validated_by": "parsePreamble, isPrimitive, isContextSpecific", + "validates": [ + "Checks that the DER preamble for the cost field is primitive and context-specific" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "error::unexpected_tag", + "field": "preamble.tag (cost field)", + "location": "detail::loadSimpleSha256", + "validated_by": "p.tag != 1", + "validates": [ + "Checks that the tag for the cost field is 1" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "parseInteger sets error code", + "field": "cost (integer field)", + "location": "detail::loadSimpleSha256", + "validated_by": "parseInteger", + "validates": [ + "Checks that the cost field is a valid unsigned 32-bit integer" + ], + "validation_type": "type|range" + }, + { + "confidence": 1.0, + "error_thrown": "error::trailing_garbage", + "field": "trailing data", + "location": "detail::loadSimpleSha256", + "validated_by": "!s.empty()", + "validates": [ + "Checks that there is no extra data after expected fields" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "error::preimage_too_long", + "field": "cost (preimageSha256 type)", + "location": "detail::loadSimpleSha256", + "validated_by": "cost > PreimageSha256::maxPreimageLength", + "validates": [ + "Checks that the cost does not exceed the maximum allowed preimage length for preimageSha256 type" + ], + "validation_type": "business_logic|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/conditions/Condition.cpp.ai.md b/src/libxrpl/conditions/Condition.cpp.ai.md new file mode 100644 index 0000000000..90cbd49ec4 --- /dev/null +++ b/src/libxrpl/conditions/Condition.cpp.ai.md @@ -0,0 +1,44 @@ +# `Condition.cpp` — Cryptocondition Deserialization + +## Role in the System + +This file implements the binary deserialization entry point for the XRPL cryptoconditions subsystem, translating a raw byte buffer into a validated `Condition` object. Cryptoconditions (defined in [draft-thomas-crypto-conditions](https://tools.ietf.org/html/draft-thomas-crypto-conditions-02)) are a cross-chain interoperability primitive used in XRPL to express spending requirements—most commonly for Escrow transactions. A `Condition` is the commitment (hash plus metadata) that an off-ledger party must satisfy by submitting a matching `Fulfillment`. + +The file sits alongside the `Fulfillment` hierarchy but focuses on the far simpler task of decoding the *commitment side* of the pair. A condition contains no secret—it is stored on-ledger and must be decoded quickly and safely whenever a fulfillment is checked. + +## Binary Encoding and the CHOICE Dispatch + +The on-wire format is ASN.1 DER, using CONTEXT-SPECIFIC CONSTRUCTED tags to distinguish condition types (a DER `CHOICE` encoding). The outer tag value identifies the type: + +- `[0]` → `preimageSha256` (a `SimpleSha256Condition`) +- `[1]` → `prefixSha256` (a `CompoundSha256Condition`) +- `[2]` → `thresholdSha256` (a `CompoundSha256Condition`) +- `[3]` → `rsaSha256` (a `SimpleSha256Condition`) +- `[4]` → `ed25519Sha256` (a `SimpleSha256Condition`) + +`Condition::deserialize()` reads the outer preamble first, validates that it is CONTEXT-SPECIFIC and CONSTRUCTED (the RFC encoding for the outer `CHOICE` wrapper), and then dispatches on `p.tag`. Only tag `0` (`preimageSha256`) actually proceeds to parsing; tags 1–4 immediately return `error::unsupported_type`. This is a deliberate scope limitation—XRPL currently only uses preimage conditions for Escrow, and supporting the compound types (`prefixSha256`, `thresholdSha256`) would require parsing recursive structures that significantly expand the attack surface. The unknown-type path (`default:`) is kept separate from the unsupported-type path so that future RFC extensions can be distinguished from currently-known-but-unimplemented types in error logs. + +An overall size gate (`maxSerializedCondition = 128` bytes, defined in `Condition.h`) is checked before type dispatch. This limit exists to prevent allocating unbounded memory before any structural validation can occur, and its comment in the header explicitly notes it must never *decrease* across protocol versions, since doing so could invalidate previously-accepted conditions. + +## The `loadSimpleSha256` Inner Parser + +The file-private `detail::loadSimpleSha256()` handles the inner DER `SEQUENCE` for the `SimpleSha256Condition` structure, which contains exactly two fields: + +1. **Fingerprint** (tag `[0]`, OCTET STRING, exactly 32 bytes) — the SHA-256 hash identifying the condition. +2. **Cost** (tag `[1]`, INTEGER, `uint32_t`) — the computational/storage cost for fulfilling the condition. + +The function deliberately re-validates the inner preamble as PRIMITIVE and CONTEXT-SPECIFIC (not CONSTRUCTED), because the outer condition wrapper is CONSTRUCTED while the inner fields are PRIMITIVE. Conflating these would be a parsing bug. Each field is consumed from the `Slice` in order using the `der::` utilities from `detail/utils.h`: `parsePreamble()` advances the slice by the preamble bytes, `parseOctetString()` copies the next `count` bytes into a `Buffer`, and `parseInteger()` decodes the cost as a big-endian two's complement value. + +After both fields are consumed, the function checks `s.empty()` to reject trailing garbage. This matters because `loadSimpleSha256` is called with a sub-`Slice` exactly sized to `p.length` from the outer preamble—so any remaining bytes after the cost integer indicate a malformed encoding rather than unrelated data. + +The type-specific cost validation for `preimageSha256` is the only business-logic check: `cost > PreimageSha256::maxPreimageLength` (128 bytes) triggers `error::preimage_too_long`. For `preimageSha256`, cost equals the preimage length in bytes (as seen in `PreimageSha256::cost()` in `PreimageSha256.h`), so a cost exceeding 128 would mean no conforming fulfillment could ever satisfy the condition on this ledger. + +## Error Handling Design + +All errors are communicated through `std::error_code& ec` — no exceptions are thrown anywhere in this path. On any error, the function immediately returns an empty `std::unique_ptr`. This is the standard XRPL pattern for performance-critical deserialization that runs inside transaction processing, where exception overhead is unacceptable and callers are expected to always check the error code. The `cryptoconditions::error` enum is integrated with `` via the `is_error_code_enum` specialization in `error.h`, providing descriptive category-level messages. + +Ownership is managed entirely via `std::unique_ptr`, constructed at the very end of `loadSimpleSha256` only after all validation passes. The `Condition` struct stores the fingerprint as a heap-allocated `Buffer`, cost as a plain `uint32_t`, and type as the `Type` enum — a minimal, value-semantic representation with no shared ownership needed. + +## Relationship to Sibling Files + +`Condition.cpp` is the read path; it never *validates* that a fulfillment satisfies a condition — that responsibility belongs to `Fulfillment::validate()` in the `Fulfillment` hierarchy. `PreimageSha256` in `detail/PreimageSha256.h` provides both the fulfillment-side deserialization and the `maxPreimageLength` constant referenced here. The DER utility functions in `detail/utils.h` are header-only and shared with `PreimageSha256::deserialize()`, keeping the parsing primitives in one place rather than duplicated across condition and fulfillment parsing. \ No newline at end of file diff --git a/src/libxrpl/conditions/Fulfillment.cpp.ai.json b/src/libxrpl/conditions/Fulfillment.cpp.ai.json new file mode 100644 index 0000000000..73ffccc6b6 --- /dev/null +++ b/src/libxrpl/conditions/Fulfillment.cpp.ai.json @@ -0,0 +1,718 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "validate(Fulfillment, Condition, Slice)", + "match(Fulfillment, Condition)", + "Fulfillment::condition()", + "Fulfillment::validate(Slice)" + ], + "entry_point": "validate(Fulfillment const& f, Condition const& c, Slice m)", + "purpose": "Validates that a Fulfillment matches a Condition and is valid for a given message.", + "validation_points": [ + "match: f.type() != c.type (type check)", + "match: c == f.condition() (condition equivalence)", + "Fulfillment::validate(m) (fulfillment-specific validation)" + ] + }, + { + "call_chain": [ + "validate(Fulfillment, Condition)", + "validate(Fulfillment, Condition, {})" + ], + "entry_point": "validate(Fulfillment const& f, Condition const& c)", + "purpose": "Validates a Fulfillment against a Condition with no message (empty Slice).", + "validation_points": [ + "Delegates to validate(Fulfillment, Condition, Slice)" + ] + }, + { + "call_chain": [ + "Fulfillment::deserialize(Slice, ec)", + "parsePreamble(s, ec)", + "PreimageSha256::deserialize(Slice, ec) (if applicable)" + ], + "entry_point": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "purpose": "Deserializes a Fulfillment from a binary buffer, validating structure and type.", + "validation_points": [ + "if (s.empty()) (buffer non-empty check)", + "parsePreamble(s, ec) (DER preamble parse)", + "isConstructed(p), isContextSpecific(p) (DER structure checks)", + "p.length vs s.size() (buffer size checks)", + "p.length > maxSerializedFulfillment (size limit)", + "switch(p.tag) (type support and error handling)", + "if (!s.empty()) (trailing garbage check)" + ] + } + ], + "data_flows": [ + { + "field": "Fulfillment.type", + "flow": [ + "Fulfillment (input)", + "match(f, c): f.type()", + "compared to c.type" + ], + "origin": "Fulfillment object (input param)", + "transformations": [ + "Direct comparison" + ], + "validated_at": "match: if (f.type() != c.type)" + }, + { + "field": "Condition.type", + "flow": [ + "Condition (input)", + "match(f, c): c.type", + "compared to f.type()" + ], + "origin": "Condition object (input param)", + "transformations": [ + "Direct comparison" + ], + "validated_at": "match: if (f.type() != c.type)" + }, + { + "field": "Fulfillment-derived Condition", + "flow": [ + "Fulfillment (input)", + "f.condition() (derives Condition)", + "compared to input Condition c" + ], + "origin": "f.condition()", + "transformations": [ + "Derived from Fulfillment" + ], + "validated_at": "match: c == f.condition()" + }, + { + "field": "Slice s (input buffer)", + "flow": [ + "Slice s (input)", + "if (s.empty())", + "parsePreamble(s, ec)", + "p.length vs s.size() checks", + "switch(p.tag) for type", + "PreimageSha256::deserialize(Slice, ec) (if applicable)", + "s += p.length", + "if (!s.empty())" + ], + "origin": "deserialize(Slice s, ...)", + "transformations": [ + "Checked for emptiness", + "Parsed for DER preamble", + "Sliced for type-specific deserialization", + "Advanced by p.length" + ], + "validated_at": "Multiple: empty, parsePreamble, length checks, trailing garbage" + }, + { + "field": "DER preamble (p)", + "flow": [ + "parsePreamble(s, ec)", + "p.tag, p.length used for type and size checks", + "switch(p.tag) for type dispatch" + ], + "origin": "parsePreamble(s, ec)", + "transformations": [ + "Parsed from Slice s" + ], + "validated_at": "parsePreamble(s, ec) result checked for error" + }, + { + "field": "std::error_code ec", + "flow": [ + "deserialize(Slice, ec)", + "set on error conditions (empty, parse failure, malformed, unsupported, etc.)" + ], + "origin": "Passed by reference to deserialize", + "transformations": [ + "Set to error values on validation failures" + ], + "validated_at": "Throughout Fulfillment::deserialize" + } + ], + "description": "Implements functions for matching and validating cryptographic conditions and fulfillments, as well as deserialization logic for Fulfillment objects in the context of the XRPL (XRP Ledger) cryptoconditions.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Fulfillment.type vs Condition.type", + "empty", + "string", + "validation" + ], + "evidence": "match() at match(Fulfillment const& f, Condition const& c)", + "issue_pattern": "Missing empty string validation for Fulfillment.type vs Condition.type", + "why_false_positive": "match() validates Fulfillment.type vs Condition.type for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "Fulfillment.type vs Condition.type", + "type", + "validation", + "check" + ], + "evidence": "match() at match(Fulfillment const& f, Condition const& c)", + "issue_pattern": "Missing type validation for Fulfillment.type vs Condition.type", + "why_false_positive": "match() validates Fulfillment.type vs Condition.type type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Fulfillment-derived Condition vs input Condition", + "empty", + "string", + "validation" + ], + "evidence": "match() at match(Fulfillment const& f, Condition const& c)", + "issue_pattern": "Missing empty string validation for Fulfillment-derived Condition vs input Condition", + "why_false_positive": "match() validates Fulfillment-derived Condition vs input Condition for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Fulfillment validity against message", + "empty", + "string", + "validation" + ], + "evidence": "Fulfillment::validate() at validate(Fulfillment const& f, Condition const& c, Slice m)", + "issue_pattern": "Missing empty string validation for Fulfillment validity against message", + "why_false_positive": "Fulfillment::validate() validates Fulfillment validity against message for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Slice s (input buffer) non-empty", + "empty", + "string", + "validation" + ], + "evidence": "if (s.empty()) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing empty string validation for Slice s (input buffer) non-empty", + "why_false_positive": "if (s.empty()) validates Slice s (input buffer) non-empty for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Slice s (input buffer) non-empty", + "format", + "validation", + "invalid" + ], + "evidence": "if (s.empty()) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing format validation for Slice s (input buffer) non-empty", + "why_false_positive": "if (s.empty()) validates Slice s (input buffer) non-empty format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "DER preamble parse success", + "empty", + "string", + "validation" + ], + "evidence": "parsePreamble(s, ec) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing empty string validation for DER preamble parse success", + "why_false_positive": "parsePreamble(s, ec) validates DER preamble parse success for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "DER preamble parse success", + "format", + "validation", + "invalid" + ], + "evidence": "parsePreamble(s, ec) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing format validation for DER preamble parse success", + "why_false_positive": "parsePreamble(s, ec) validates DER preamble parse success format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "DER preamble constructed/context-specific", + "empty", + "string", + "validation" + ], + "evidence": "isConstructed(p), isContextSpecific(p) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing empty string validation for DER preamble constructed/context-specific", + "why_false_positive": "isConstructed(p), isContextSpecific(p) validates DER preamble constructed/context-specific for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "DER preamble constructed/context-specific", + "format", + "validation", + "invalid" + ], + "evidence": "isConstructed(p), isContextSpecific(p) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing format validation for DER preamble constructed/context-specific", + "why_false_positive": "isConstructed(p), isContextSpecific(p) validates DER preamble constructed/context-specific format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "p.length vs s.size() (buffer underfull)", + "empty", + "string", + "validation" + ], + "evidence": "if (p.length > s.size()) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing empty string validation for p.length vs s.size() (buffer underfull)", + "why_false_positive": "if (p.length > s.size()) validates p.length vs s.size() (buffer underfull) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "p.length vs s.size() (buffer underfull)", + "range", + "bounds", + "validation" + ], + "evidence": "if (p.length > s.size()) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing range validation for p.length vs s.size() (buffer underfull)", + "why_false_positive": "if (p.length > s.size()) validates p.length vs s.size() (buffer underfull) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "p.length vs s.size() (buffer overfull)", + "empty", + "string", + "validation" + ], + "evidence": "if (p.length < s.size()) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing empty string validation for p.length vs s.size() (buffer overfull)", + "why_false_positive": "if (p.length < s.size()) validates p.length vs s.size() (buffer overfull) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "p.length vs s.size() (buffer overfull)", + "range", + "bounds", + "validation" + ], + "evidence": "if (p.length < s.size()) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing range validation for p.length vs s.size() (buffer overfull)", + "why_false_positive": "if (p.length < s.size()) validates p.length vs s.size() (buffer overfull) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "p.length vs maxSerializedFulfillment", + "empty", + "string", + "validation" + ], + "evidence": "if (p.length > maxSerializedFulfillment) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing empty string validation for p.length vs maxSerializedFulfillment", + "why_false_positive": "if (p.length > maxSerializedFulfillment) validates p.length vs maxSerializedFulfillment for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "p.length vs maxSerializedFulfillment", + "range", + "bounds", + "validation" + ], + "evidence": "if (p.length > maxSerializedFulfillment) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing range validation for p.length vs maxSerializedFulfillment", + "why_false_positive": "if (p.length > maxSerializedFulfillment) validates p.length vs maxSerializedFulfillment range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "p.tag (fulfillment type tag)", + "empty", + "string", + "validation" + ], + "evidence": "switch (p.tag) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing empty string validation for p.tag (fulfillment type tag)", + "why_false_positive": "switch (p.tag) validates p.tag (fulfillment type tag) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "p.tag (fulfillment type tag)", + "type", + "validation", + "check" + ], + "evidence": "switch (p.tag) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing type validation for p.tag (fulfillment type tag)", + "why_false_positive": "switch (p.tag) validates p.tag (fulfillment type tag) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "PreimageSha256::deserialize() error", + "empty", + "string", + "validation" + ], + "evidence": "if (ec) after PreimageSha256::deserialize at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing empty string validation for PreimageSha256::deserialize() error", + "why_false_positive": "if (ec) after PreimageSha256::deserialize validates PreimageSha256::deserialize() error for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Trailing data in Slice s", + "empty", + "string", + "validation" + ], + "evidence": "if (!s.empty()) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing empty string validation for Trailing data in Slice s", + "why_false_positive": "if (!s.empty()) validates Trailing data in Slice s for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Trailing data in Slice s", + "format", + "validation", + "invalid" + ], + "evidence": "if (!s.empty()) at Fulfillment::deserialize(Slice s, std::error_code& ec)", + "issue_pattern": "Missing format validation for Trailing data in Slice s", + "why_false_positive": "if (!s.empty()) validates Trailing data in Slice s format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/conditions/Fulfillment.cpp", + "functions": [ + { + "args": [ + "Fulfillment const& f", + "Condition const& c" + ], + "lineno": 8, + "name": "match" + }, + { + "args": [ + "Fulfillment const& f", + "Condition const& c", + "Slice m" + ], + "lineno": 20, + "name": "validate" + }, + { + "args": [ + "Fulfillment const& f", + "Condition const& c" + ], + "lineno": 24, + "name": "validate" + }, + { + "args": [ + "Slice s", + "std::error_code& ec" + ], + "lineno": 28, + "name": "Fulfillment::deserialize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "cryptoconditions" + } + ], + "test_coverage_notes": "The code is likely covered by unit tests for Fulfillment and Condition validation, as well as deserialization. Tests should exist for: type mismatches, condition mismatches, invalid/empty buffers, malformed DER, unsupported types, and trailing garbage. However, only PreimageSha256 deserialization is supported; other types return unsupported errors, so those code paths may not be fully exercised. Edge cases like buffer over/underflow, large size, and unknown types should be tested. Test files likely include: test/conditions/Fulfillment_test.cpp, test/conditions/Condition_test.cpp, and possibly integration tests for transaction validation. Gaps may exist for unsupported types and error code propagation.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation logic, DER parsing utilities", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "Fulfillment.type vs Condition.type", + "location": "match(Fulfillment const& f, Condition const& c)", + "validated_by": "match()", + "validates": [ + "Checks that the fulfillment's type matches the condition's type" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "Fulfillment-derived Condition vs input Condition", + "location": "match(Fulfillment const& f, Condition const& c)", + "validated_by": "match()", + "validates": [ + "Checks that the condition derived from the fulfillment equals the input condition" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "Fulfillment validity against message", + "location": "validate(Fulfillment const& f, Condition const& c, Slice m)", + "validated_by": "Fulfillment::validate()", + "validates": [ + "Checks that the fulfillment is valid for the given message" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "ec = error::buffer_empty; returns nullptr", + "field": "Slice s (input buffer) non-empty", + "location": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "validated_by": "if (s.empty())", + "validates": [ + "Checks that the input buffer is not empty" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "ec set by parsePreamble; returns nullptr", + "field": "DER preamble parse success", + "location": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "validated_by": "parsePreamble(s, ec)", + "validates": [ + "Checks that the DER preamble can be parsed from the input buffer" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "ec = error::malformed_encoding; returns nullptr", + "field": "DER preamble constructed/context-specific", + "location": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "validated_by": "isConstructed(p), isContextSpecific(p)", + "validates": [ + "Checks that the DER preamble is constructed and context-specific" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "ec = error::buffer_underfull; returns {}", + "field": "p.length vs s.size() (buffer underfull)", + "location": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "validated_by": "if (p.length > s.size())", + "validates": [ + "Checks that the declared length does not exceed the buffer size" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "ec = error::buffer_overfull; returns {}", + "field": "p.length vs s.size() (buffer overfull)", + "location": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "validated_by": "if (p.length < s.size())", + "validates": [ + "Checks that the declared length is not less than the buffer size" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "ec = error::large_size; returns {}", + "field": "p.length vs maxSerializedFulfillment", + "location": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "validated_by": "if (p.length > maxSerializedFulfillment)", + "validates": [ + "Checks that the declared length does not exceed the maximum allowed size" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "ec = error::unsupported_type or error::unknown_type; returns {}", + "field": "p.tag (fulfillment type tag)", + "location": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "validated_by": "switch (p.tag)", + "validates": [ + "Checks that the tag corresponds to a supported fulfillment type", + "Returns error for unsupported or unknown types" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "returns {} if ec set", + "field": "PreimageSha256::deserialize() error", + "location": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "validated_by": "if (ec) after PreimageSha256::deserialize", + "validates": [ + "Checks that PreimageSha256 deserialization succeeded" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "ec = error::trailing_garbage", + "field": "Trailing data in Slice s", + "location": "Fulfillment::deserialize(Slice s, std::error_code& ec)", + "validated_by": "if (!s.empty())", + "validates": [ + "Checks that there is no extra data after the fulfillment" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/conditions/Fulfillment.cpp.ai.md b/src/libxrpl/conditions/Fulfillment.cpp.ai.md new file mode 100644 index 0000000000..baff03582e --- /dev/null +++ b/src/libxrpl/conditions/Fulfillment.cpp.ai.md @@ -0,0 +1,43 @@ +# `src/libxrpl/conditions/Fulfillment.cpp` + +## Role in the System + +This file implements the public API surface for the XRPL cryptoconditions subsystem — an implementation of the [Crypto-Conditions Internet Draft (draft-thomas-crypto-conditions-02)](https://tools.ietf.org/html/draft-thomas-crypto-conditions-02). Cryptoconditions allow ledger objects to attach unforgeable commitments ("conditions") that can only be satisfied by presenting a matching "fulfillment." In XRPL they are used by the `EscrowFinish` transaction type to gate fund release. + +The file provides three concerns: the factory method `Fulfillment::deserialize` that reconstructs a typed `Fulfillment` object from raw bytes, the `match` predicate that cryptographically ties a fulfillment to a condition, and the `validate` overloads that combine both checks into the single call used by transaction processing. + +## Key Abstractions + +`Fulfillment` (declared in `Fulfillment.h`) is a pure-virtual base: five virtual methods — `type()`, `fingerprint()`, `cost()`, `condition()`, and `validate(Slice)` — define everything a fulfillment must provide. `Condition` (in `Condition.h`) is a plain struct holding a `Type` enum, a `fingerprint` buffer, a `cost` integer, and a `subtypes` set for compound conditions. Equality on `Condition` compares all four fields. + +Currently the only concrete subclass is `PreimageSha256`, defined entirely in `detail/PreimageSha256.h`. The four other condition types defined by the RFC (`prefixSha256`, `thresholdSha256`, `rsaSha256`, `ed25519Sha256`) are intentionally unimplemented — their `switch` arms immediately set `error::unsupported_type` and return. This is a deliberate scope decision reflecting XRPL's use case: preimage conditions suffice for escrow unlock semantics and avoid the consensus risk of implementing complex compound conditions. + +## `Fulfillment::deserialize` — DER Decoding Chain + +The binary format is ASN.1 DER using a CHOICE structure where the outer tag identifies the fulfillment type. The function performs a strict sequence of checks before delegating to type-specific parsing: + +1. **Empty buffer** → `error::buffer_empty` +2. **Preamble parse** via `der::parsePreamble`, which advances the `Slice` and populates a `Preamble` struct with a `type` byte, `tag`, and `length`. Long-form tags (≥ 31) are rejected with `error::long_tag`. +3. **DER class checks** — the outer wrapper must be both constructed (`isConstructed`) and context-specific (`isContextSpecific`), matching the RFC's encoding of the CHOICE alternatives. A universal or application-class preamble signals `error::malformed_encoding`. +4. **Buffer length invariant** — `p.length` must equal `s.size()` exactly. Greater means the buffer was truncated (`error::buffer_underfull`); less means extra bytes follow (`error::buffer_overfull`). This enforces a canonical one-fulfillment-per-buffer contract. +5. **Size cap** — `p.length > maxSerializedFulfillment` (256 bytes) triggers `error::large_size`. The comment in `Fulfillment.h` emphasizes this limit may only increase: lowering it could retroactively invalidate previously accepted fulfillments, a ledger-integrity violation. +6. **Type dispatch** — `p.tag` is compared against the `Type` enum values using `safe_cast`. Only `preimageSha256` (tag 0) proceeds to `PreimageSha256::deserialize`; all others fail immediately. +7. **Trailing garbage** — after `PreimageSha256::deserialize` advances the slice by `p.length`, any remaining bytes indicate `error::trailing_garbage`. This check exists because `parsePreamble` mutates the `Slice` in place, so consumed bytes literally disappear from `s`. + +The entire function uses `std::error_code` rather than exceptions, consistent with how XRPL validates untrusted peer data throughout — no stack unwinding cost, no risk of exception escaping into consensus code. + +## `match` — Two-Phase Cryptographic Binding + +```cpp +bool match(Fulfillment const& f, Condition const& c) +``` + +The fast path checks `f.type() != c.type` first, discarding mismatched types before any hashing. If types agree, it calls `f.condition()` — which for `PreimageSha256` hashes the preimage payload with SHA-256 to reconstruct the fingerprint — and compares the derived `Condition` object against `c` using `operator==`, which checks type, cost, subtypes, and fingerprint together. This means a fulfillment that presents the correct preimage but was recorded under the wrong cost metadata will still fail matching. + +## `validate` — Transaction-Level Entry Points + +Two overloads are provided. The three-argument form `validate(f, c, m)` is the general case that sequences `match(f, c) && f.validate(m)`, where `m` is the transaction message being authorized. The two-argument form calls the three-argument one with an empty `Slice{}`. For `PreimageSha256`, `validate(Slice)` always returns `true` — the cryptographic proof is entirely in the preimage-to-condition binding, not in a per-message signature. The empty-message form is therefore the practical path for XRPL escrows, and the comment in `Fulfillment.h` explicitly recommends signature-type conditions use single-use keys when no message is provided. + +## Design Tradeoff: Unsupported Types + +Reserving `unsupported_type` rather than `unknown_type` for types 1–4 is a meaningful distinction: the implementation knows these types exist in the RFC but has chosen not to support them. Any binary data claiming to be one of those types fails immediately at the dispatch switch, well before any attempt to parse its contents. This prevents malformed payloads from triggering partial parsing of unsupported structures and gives callers a diagnostic error code that distinguishes "we recognize this but don't implement it" from "we have never seen this tag." \ No newline at end of file diff --git a/src/libxrpl/conditions/error.cpp.ai.json b/src/libxrpl/conditions/error.cpp.ai.json new file mode 100644 index 0000000000..9839de991a --- /dev/null +++ b/src/libxrpl/conditions/error.cpp.ai.json @@ -0,0 +1,232 @@ +{ + "args": [ + { + "lineno": 16, + "name": "ev" + }, + { + "lineno": 52, + "name": "ev" + }, + { + "lineno": 57, + "name": "ev" + }, + { + "lineno": 57, + "name": "condition" + }, + { + "lineno": 62, + "name": "error" + }, + { + "lineno": 62, + "name": "ev" + }, + { + "lineno": 74, + "name": "ev" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "cryptoconditions_error_category" + } + ], + "code_paths": [ + { + "call_chain": [ + "cryptoconditions_error_category::message", + "safe_cast(ev)" + ], + "entry_point": "cryptoconditions_error_category::message", + "purpose": "Converts an integer error code to a human-readable error message, validating the integer as a valid error enum.", + "validation_points": [ + "safe_cast(ev) (validates that ev is a valid error enum value)" + ] + }, + { + "call_chain": [ + "make_error_code", + "safe_cast::type>(ev)", + "get_cryptoconditions_error_category" + ], + "entry_point": "make_error_code", + "purpose": "Creates a std::error_code from an error enum, ensuring the enum is safely cast to its underlying integer type.", + "validation_points": [ + "safe_cast::type>(ev) (validates that ev is a valid error enum value)" + ] + }, + { + "call_chain": [ + "std::error_code", + "cryptoconditions_error_category::message / equivalent / default_error_condition" + ], + "entry_point": "std::error_code / std::error_condition usage", + "purpose": "Standard error handling using the custom error category, which may trigger validation when converting error codes to messages or comparing errors.", + "validation_points": [ + "safe_cast(ev) in message" + ] + } + ], + "data_flows": [ + { + "field": "ev (error code integer)", + "flow": [ + "external code (e.g., std::error_code machinery or direct call)", + "cryptoconditions_error_category::message(int ev)", + "safe_cast(ev)", + "switch on error enum", + "return error message string" + ], + "origin": "Passed as argument to cryptoconditions_error_category::message, default_error_condition, equivalent", + "transformations": [ + "safe_cast(ev): Converts and validates integer to error enum" + ], + "validated_at": "safe_cast(ev) inside message" + }, + { + "field": "ev (error enum)", + "flow": [ + "external code (calls make_error_code)", + "make_error_code(error ev)", + "safe_cast::type>(ev)", + "construct std::error_code" + ], + "origin": "Passed as argument to make_error_code(error ev)", + "transformations": [ + "safe_cast: Converts error enum to underlying integer type, validates enum" + ], + "validated_at": "safe_cast::type>(ev) inside make_error_code" + } + ], + "description": "Defines the error category and error code handling for the xrpl cryptoconditions library, providing error messages and integration with std::error_code.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "ev (error code integer)", + "empty", + "string", + "validation" + ], + "evidence": "safe_cast at cryptoconditions_error_category::message", + "issue_pattern": "Missing empty string validation for ev (error code integer)", + "why_false_positive": "safe_cast validates ev (error code integer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ev (error code integer)", + "type", + "validation", + "check" + ], + "evidence": "safe_cast at cryptoconditions_error_category::message", + "issue_pattern": "Missing type validation for ev (error code integer)", + "why_false_positive": "safe_cast validates ev (error code integer) type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/conditions/error.cpp", + "functions": [ + { + "args": [], + "lineno": 12, + "name": "cryptoconditions_error_category::name" + }, + { + "args": [ + "ev" + ], + "lineno": 16, + "name": "cryptoconditions_error_category::message" + }, + { + "args": [ + "ev" + ], + "lineno": 52, + "name": "cryptoconditions_error_category::default_error_condition" + }, + { + "args": [ + "ev", + "condition" + ], + "lineno": 57, + "name": "cryptoconditions_error_category::equivalent" + }, + { + "args": [ + "error", + "ev" + ], + "lineno": 62, + "name": "cryptoconditions_error_category::equivalent" + }, + { + "args": [], + "lineno": 67, + "name": "get_cryptoconditions_error_category" + }, + { + "args": [ + "ev" + ], + "lineno": 74, + "name": "make_error_code" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "cryptoconditions" + }, + { + "lineno": 7, + "name": "detail" + } + ], + "test_coverage_notes": "This file is a low-level error category implementation. Direct tests for this file are unlikely; instead, tests would exist for higher-level code that triggers error conditions (e.g., cryptographic condition parsing, encoding/decoding, or buffer handling). Look for tests in files like 'test/conditions/*', 'test/crypto/*', or integration tests that exercise error handling paths. Gaps: No direct unit tests for error category or message mapping; safe_cast validation is only as strong as its implementation and usage. Edge cases (invalid enum values, unknown error codes) may not be explicitly tested unless fuzz or negative tests exist.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation via safe_cast)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "undefined_behavior or exception if safe_cast fails (implementation dependent)", + "field": "ev (error code integer)", + "location": "cryptoconditions_error_category::message", + "validated_by": "safe_cast", + "validates": [ + "Ensures that the integer error code can be safely cast to the error enum type" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/conditions/error.cpp.ai.md b/src/libxrpl/conditions/error.cpp.ai.md new file mode 100644 index 0000000000..c935a08ada --- /dev/null +++ b/src/libxrpl/conditions/error.cpp.ai.md @@ -0,0 +1,34 @@ +# `src/libxrpl/conditions/error.cpp` + +## Role in the System + +This file wires the cryptoconditions subsystem into C++'s standard `` machinery. Its sole job is to define a custom `std::error_category` for the `xrpl::cryptoconditions::error` enum declared in `detail/error.h`, and to expose the `make_error_code()` factory that bridges the enum to `std::error_code`. The companion header also specializes `std::is_error_code_enum`, which allows `error` enumerators to implicitly convert to `std::error_code` in function call and comparison contexts — a pattern that makes error propagation throughout the conditions library idiomatic C++11 error handling without exceptions. + +## The Error Enum and Its Categories + +The `error` enum (defined in `include/xrpl/conditions/detail/error.h`) starts at value `1` — deliberately skipping `0` to avoid collisions with the "no error" sentinel that `std::error_code` uses. Its seventeen enumerators fall into four logical groups: + +- **Specification errors** (`unsupported_type`, `unsupported_subtype`, `unknown_type`, `unknown_subtype`, `fingerprint_size`, `incorrect_encoding`) — the request or data violates the Crypto-Conditions specification. +- **Buffer errors** (`trailing_garbage`, `buffer_empty`, `buffer_overfull`, `buffer_underfull`) — the raw byte buffer is malformed or incorrectly sized. +- **DER encoding errors** (`malformed_encoding`, `unexpected_tag`, `short_preamble`) — the DER/ASN.1 structure of an encoded condition or fulfillment is invalid. +- **Implementation limits** (`long_tag`, `large_size`, `preimage_too_long`) — valid-by-spec inputs that exceed what this implementation is willing to process. + +This grouping is visible in the message strings returned by `message()`: each prefix (`"Specification:"`, `"Bad buffer:"`, `"Malformed DER encoding:"`, `"Implementation limit:"`) signals to a caller not just what failed, but which layer of the stack is responsible. + +## `cryptoconditions_error_category` + +The class is a private implementation detail confined to `namespace xrpl::cryptoconditions::detail`. It overrides the three `std::error_category` virtual functions: + +- `name()` returns the static string `"cryptoconditions"`, used when printing or logging error codes. +- `message(int ev)` converts an integer back to a human-readable string via `safe_cast(ev)`. The use of `safe_cast` here is intentional: because the `` interface passes error values as raw `int`, this cast performs compile-time width and sign checks (verified in `include/xrpl/basics/safe_cast.h`) before delegating to a `static_cast`. If an integer falls through the switch, `default` returns `"generic error"`, matching the `error::generic` fallback. +- `equivalent()` uses strict identity comparison on the category pointer (`&condition.category() == this`) rather than name-string comparison. This is the recommended pattern in the standard — name strings are for human consumption, and pointer identity guarantees that two codes only compare equal if they come from the same category instance. + +The singleton is delivered by `get_cryptoconditions_error_category()`, which constructs the category object exactly once as a function-local `static const`. This is the canonical way to implement `std::error_category` singletons: it avoids the static-initialization-order fiasco, is thread-safe under C++11's rules for local statics, and guarantees the single address that pointer-identity comparison depends on. + +## `make_error_code()` + +This free function is the public seam between the enum and the standard error infrastructure. It uses `safe_cast::type>(ev)` to convert the enum to its underlying integer type before constructing the `std::error_code`. Because `safe_cast` operates at compile time for enum-to-integer conversions, there is no runtime overhead. The `std::is_error_code_enum` specialization in the header means callers can write `std::error_code ec = error::buffer_empty;` and the compiler silently calls `make_error_code` — this is the ADL-based implicit-conversion protocol defined by the `` standard. + +## Design Notes + +The entire implementation is intentionally minimal. There are no exceptions, no heap allocations, and no mutable state — the category object is `const` and the message strings are string literals. The `default_error_condition` override simply maps every code back to itself (no cross-category equivalence is claimed), which is the correct choice: cryptoconditions errors are a domain-specific vocabulary and there is no meaningful mapping to `std::errc` POSIX codes. \ No newline at end of file diff --git a/src/libxrpl/core/HashRouter.cpp.ai.json b/src/libxrpl/core/HashRouter.cpp.ai.json new file mode 100644 index 0000000000..3705787ec0 --- /dev/null +++ b/src/libxrpl/core/HashRouter.cpp.ai.json @@ -0,0 +1,297 @@ +{ + "args": [ + { + "lineno": 5, + "name": "key" + }, + { + "lineno": 25, + "name": "peer" + }, + { + "lineno": 38, + "name": "flags" + }, + { + "lineno": 46, + "name": "tx_interval" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "addSuppression", + "emplace" + ], + "entry_point": "addSuppression", + "purpose": "Adds a suppression entry for a given key, ensuring it exists in the suppression map.", + "validation_points": [] + }, + { + "call_chain": [ + "addSuppressionPeer", + "addSuppressionPeerWithStatus", + "emplace" + ], + "entry_point": "addSuppressionPeer", + "purpose": "Adds a peer to the suppression entry for a key, creating the entry if needed.", + "validation_points": [] + }, + { + "call_chain": [ + "addSuppressionPeer", + "emplace" + ], + "entry_point": "addSuppressionPeer (with flags)", + "purpose": "Adds a peer and retrieves flags for the suppression entry.", + "validation_points": [] + }, + { + "call_chain": [ + "shouldProcess", + "emplace" + ], + "entry_point": "shouldProcess", + "purpose": "Determines if a transaction should be processed, updating peer and flags.", + "validation_points": [] + }, + { + "call_chain": [ + "getFlags", + "emplace" + ], + "entry_point": "getFlags", + "purpose": "Retrieves flags for a suppression entry.", + "validation_points": [] + }, + { + "call_chain": [ + "setFlags", + "emplace" + ], + "entry_point": "setFlags", + "purpose": "Sets flags for a suppression entry, with validation.", + "validation_points": [ + "XRPL_ASSERT(static_cast(flags), ...)" + ] + }, + { + "call_chain": [ + "shouldRelay", + "emplace" + ], + "entry_point": "shouldRelay", + "purpose": "Determines if a message should be relayed and returns the peer set if so.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "key (uint256)", + "flow": [ + "function argument", + "emplace", + "suppressionMap_" + ], + "origin": "Function argument (all entry points)", + "transformations": [ + "Used as map key; may trigger expire() if not present" + ], + "validated_at": "No explicit validation" + }, + { + "field": "peer (PeerShortID)", + "flow": [ + "function argument", + "emplace", + "Entry.addPeer(peer)" + ], + "origin": "Function argument (addSuppressionPeer*, shouldProcess)", + "transformations": [ + "Added to Entry's peer set" + ], + "validated_at": "No explicit validation" + }, + { + "field": "flags (HashRouterFlags)", + "flow": [ + "function argument (setFlags) or Entry.getFlags() (others)", + "setFlags: XRPL_ASSERT validation", + "setFlags: Entry.setFlags(flags)", + "addSuppressionPeer/shouldProcess/getFlags: Entry.getFlags()" + ], + "origin": "Function argument (setFlags), or output (addSuppressionPeer, shouldProcess, getFlags)", + "transformations": [ + "setFlags: validated for non-zero, then set in Entry", + "others: retrieved from Entry" + ], + "validated_at": "setFlags (XRPL_ASSERT)" + }, + { + "field": "suppressionMap_", + "flow": [ + "used in emplace (find, insert, touch, expire)", + "Entry objects stored/updated here" + ], + "origin": "HashRouter member variable", + "transformations": [ + "expire() may remove old entries", + "touch() updates LRU" + ], + "validated_at": "No explicit validation" + } + ], + "description": "Implements methods for the xrpl::HashRouter class, which manages suppression and relay of hashed items (such as transactions) across peers, handling peer tracking, suppression timing, and relay logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "flags (HashRouterFlags)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at setFlags", + "issue_pattern": "Missing empty string validation for flags (HashRouterFlags)", + "why_false_positive": "XRPL_ASSERT validates flags (HashRouterFlags) for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/core/HashRouter.cpp", + "functions": [ + { + "args": [ + "key" + ], + "lineno": 5, + "name": "emplace" + }, + { + "args": [ + "key" + ], + "lineno": 19, + "name": "addSuppression" + }, + { + "args": [ + "key", + "peer" + ], + "lineno": 25, + "name": "addSuppressionPeer" + }, + { + "args": [ + "key", + "peer" + ], + "lineno": 29, + "name": "addSuppressionPeerWithStatus" + }, + { + "args": [ + "key", + "peer", + "flags" + ], + "lineno": 38, + "name": "addSuppressionPeer" + }, + { + "args": [ + "key", + "peer", + "flags", + "tx_interval" + ], + "lineno": 46, + "name": "shouldProcess" + }, + { + "args": [ + "key" + ], + "lineno": 57, + "name": "getFlags" + }, + { + "args": [ + "key", + "flags" + ], + "lineno": 63, + "name": "setFlags" + }, + { + "args": [ + "key" + ], + "lineno": 76, + "name": "shouldRelay" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is core infrastructure for transaction/message suppression and relay logic. Typical test coverage would be in unit tests for HashRouter, likely in files such as HashRouter_test.cpp or similar. The only explicit validation is the XRPL_ASSERT in setFlags, which checks that flags is non-zero before setting. There is no explicit validation for key or peer inputs. Test coverage should ensure: (1) setFlags rejects zero/invalid flags, (2) suppression and relay logic works as expected, (3) peer addition and flag propagation is correct. Gaps: No validation for key/peer, so malformed or default values may not be caught. If XRPL_ASSERT is compiled out, setFlags may not validate at runtime.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro)", + "notes": "No template, constructor, or framework-level input validation is present in this file. All other parameters (key, peer, flags) are assumed to be valid by type and are not explicitly validated for format, range, or type beyond C++ type system. Only business logic validation is performed via XRPL_ASSERT in setFlags.", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws in debug)", + "field": "flags (HashRouterFlags)", + "location": "setFlags", + "validated_by": "XRPL_ASSERT", + "validates": [ + "flags is not zero (truthy)", + "flags is a valid input for setting" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/core/HashRouter.cpp.ai.md b/src/libxrpl/core/HashRouter.cpp.ai.md new file mode 100644 index 0000000000..71e8169367 --- /dev/null +++ b/src/libxrpl/core/HashRouter.cpp.ai.md @@ -0,0 +1,39 @@ +# `HashRouter.cpp` — Broadcast Suppression and Relay Coordination + +`HashRouter` is the broadcast-deduplication layer for the XRPL peer-to-peer overlay. Every time a peer sends the local node a transaction, validation, or ledger object, the node must decide three things: have I already seen this? Should I bother processing it again? And if I'm relaying it outward, which peers already have it? `HashRouter` answers all three questions by maintaining a time-bounded map from 256-bit hashes to routing state for each observed object. + +## The Suppression Map and Lazy Expiration + +The backing store is a `beast::aged_unordered_map` called `suppressionMap_`. Every entry carries its own insertion timestamp, and the map supports a `touch()` operation that refreshes that timestamp on access — making it behave as an LRU-style expiry cache. The hash used is `hardened_hash`, preventing algorithmic complexity attacks against the hash table from untrusted peer data. + +Expiration is *lazy*: old entries are only evicted when `emplace()` needs to insert a new key and calls `beast::expire(suppressionMap_, setup_.holdTime)` first. The `expire()` utility (in `aged_container_utility.h`) walks the container's chronological iterator in insertion-time order, erasing entries older than the configured `holdTime` (default 300 s). This means a burst of new hashes can flush stale entries in bulk, but a quiet node retains its map indefinitely — an acceptable trade-off given that entries are cheap and holdTime is already bounded. + +The `touch()` call on cache hit extends an existing entry's lifetime by resetting its timestamp. The unit tests confirm this explicitly: accessing a key before its expiry deadline prevents it from being evicted even when the clock has otherwise passed its original insertion time. This means frequently-seen hashes never silently disappear while they're actively being observed. + +## The `emplace()` Pivot + +All seven public methods funnel through the private `emplace(key)`. It is not separately synchronized — all callers already hold `mutex_` before invoking it. The method returns a `std::pair` where the boolean signals whether the entry was *created* (true) or already existed (false). This creation flag is the primary suppression signal: the first arrival of a hash gets `true`; every subsequent arrival, regardless of which peer sent it, gets `false`. + +This asymmetry is intentional. The goal is not to track uniqueness per-peer but to deduplicate floods. When `addSuppressionPeer()` returns `false`, the caller at `PeerImp` knows the hash is a duplicate and can skip processing it entirely. The *peer identity* is stored inside the entry so the relay step can exclude peers that already have the object — not to gate whether processing occurs. + +## Entry State: Flags, Peers, and Two Timestamps + +The `Entry` inner class carries four distinct pieces of state: + +- **`flags_`** — a `HashRouterFlags` bitmask accumulating status bits. The public bits (`BAD`, `SAVED`, `HELD`, `TRUSTED`) signal why a transaction is in a particular processing state. The six `PRIVATE` bits are owned by `apply.cpp` for internal transaction-application bookkeeping. `setFlags()` in `HashRouter` is idempotent: it checks whether the bits are already set and returns `false` without modification if so, avoiding spurious re-processing signals to callers. + +- **`peers_`** — a `std::set` of every peer short-ID that has delivered this hash. Peer 0 is silently ignored (`addPeer` skips zero IDs) since zero is the local node's sentinel. When `shouldRelay()` fires, it calls `releasePeerSet()` which `std::move`s the set out and resets it, so subsequent peer arrivals accumulate a fresh exclusion list for the next relay window. + +- **`relayed_`** — an `optional` recording when this hash was last relayed outward. `shouldRelay()` enforces a `relayTime` cooldown (default 30 s); if the timestamp is set and hasn't aged out, it returns an empty optional signaling "do not relay." When the cooldown expires, it stamps the new time, returns the accumulated peer set, and resets that set — so relaying is self-throttling and always provides a fresh exclusion list. + +- **`processed_`** — an `optional` used by `shouldProcess()` to enforce a caller-supplied cooldown (`tx_interval`) independent of the relay timer. This prevents the same transaction ID from being re-submitted to the job queue more frequently than the ledger close rate allows. + +## The Relay Protocol + +The `shouldRelay()` flow is subtly different from suppression. The return type is `optional>`. A *seated* optional means "yes, relay — and skip these peers." An *empty* optional means "do not relay at all." An empty-but-seated set means "relay to everyone." Callers in `OverlayImpl` use this peer set as the exclusion list for broadcast, so peers that already delivered the item don't receive it back. + +`addSuppressionPeerWithStatus()` exposes both dimensions at once: whether the entry is new *and* whether it has already been relayed (the optional relay timestamp). This is used in `PeerImp` for squelch-aware relay: if a message has already been relayed, the node can skip re-relaying even if the entry was just created under race conditions. + +## Thread Safety + +The mutex is declared `mutable` so `getFlags()` — logically const — can still lock. Every public method acquires a `std::lock_guard` before touching `suppressionMap_` or `Entry` state. Because `emplace()` is private and only called under the lock, there is no recursive locking concern. The design is simple and correct for a high-contention hot path: a single coarse lock over the entire table, justified by the expected low lock-hold time of hash-table lookups and small entry updates. \ No newline at end of file diff --git a/src/libxrpl/core/detail/Job.cpp.ai.json b/src/libxrpl/core/detail/Job.cpp.ai.json new file mode 100644 index 0000000000..272ebecbaa --- /dev/null +++ b/src/libxrpl/core/detail/Job.cpp.ai.json @@ -0,0 +1,257 @@ +{ + "args": [ + { + "lineno": 10, + "name": "type" + }, + { + "lineno": 10, + "name": "index" + }, + { + "lineno": 14, + "name": "type" + }, + { + "lineno": 14, + "name": "name" + }, + { + "lineno": 14, + "name": "index" + }, + { + "lineno": 14, + "name": "lm" + }, + { + "lineno": 14, + "name": "job" + }, + { + "lineno": 44, + "name": "j" + }, + { + "lineno": 56, + "name": "j" + }, + { + "lineno": 68, + "name": "j" + }, + { + "lineno": 80, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Job::doJob", + "mJob()" + ], + "entry_point": "Job::doJob", + "purpose": "Executes the job's function, tracking load event timing and thread naming.", + "validation_points": [ + "No explicit validation in this chain; assumes mJob is valid (non-null) and m_loadEvent is initialized." + ] + }, + { + "call_chain": [ + "Job::Job(JobType, std::string const&, std::uint64_t, LoadMonitor&, std::function const&)", + "std::make_shared(...)" + ], + "entry_point": "Job::Job (constructor with job function)", + "purpose": "Constructs a Job object, initializes fields, and creates a LoadEvent for monitoring.", + "validation_points": [ + "No explicit validation of input parameters (e.g., job function, name, type)." + ] + }, + { + "call_chain": [ + "Job::operator<", + "Job::operator>", + "Job::operator<=", + "Job::operator>=" + ], + "entry_point": "Job::operator<, operator>, operator<=, operator>=", + "purpose": "Compares Job objects for ordering, typically for queueing or prioritization.", + "validation_points": [ + "No explicit validation; relies on correct initialization of mType and mJobIndex." + ] + } + ], + "data_flows": [ + { + "field": "mType", + "flow": [ + "Constructor parameter", + "Assigned to mType", + "Used in comparison operators and getType()" + ], + "origin": "Constructor parameter (JobType type)", + "transformations": [ + "Direct assignment; no transformation" + ], + "validated_at": "Not explicitly validated; assumed valid by caller" + }, + { + "field": "mJobIndex", + "flow": [ + "Constructor parameter", + "Assigned to mJobIndex", + "Used in comparison operators" + ], + "origin": "Constructor parameter (std::uint64_t index)", + "transformations": [ + "Direct assignment; no transformation" + ], + "validated_at": "Not explicitly validated; assumed valid by caller" + }, + { + "field": "mJob", + "flow": [ + "Constructor parameter", + "Assigned to mJob", + "Invoked in doJob()" + ], + "origin": "Constructor parameter (std::function const& job)", + "transformations": [ + "Direct assignment; set to nullptr after execution in doJob()" + ], + "validated_at": "Not explicitly validated; if null, calling mJob() in doJob() would crash" + }, + { + "field": "mName", + "flow": [ + "Constructor parameter", + "Assigned to mName", + "Used in thread naming and LoadEvent" + ], + "origin": "Constructor parameter (std::string const& name)", + "transformations": [ + "Direct assignment" + ], + "validated_at": "Not explicitly validated" + }, + { + "field": "m_loadEvent", + "flow": [ + "Constructed in Job constructor", + "Used in doJob() for start() and setName()" + ], + "origin": "Constructed in Job constructor via std::make_shared", + "transformations": [ + "None" + ], + "validated_at": "Not explicitly validated; assumed constructed successfully" + }, + { + "field": "m_queue_time", + "flow": [ + "Set in constructor", + "Returned by queue_time()" + ], + "origin": "Set to clock_type::now() in Job constructor", + "transformations": [ + "None" + ], + "validated_at": "Not validated" + } + ], + "description": "Implements the Job class for the xrpl namespace, providing job management, execution, and comparison operators for job scheduling.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/core/detail/Job.cpp", + "functions": [ + { + "args": [], + "lineno": 6, + "name": "Job" + }, + { + "args": [ + "type", + "index" + ], + "lineno": 10, + "name": "Job" + }, + { + "args": [ + "type", + "name", + "index", + "lm", + "job" + ], + "lineno": 14, + "name": "Job" + }, + { + "args": [], + "lineno": 23, + "name": "getType" + }, + { + "args": [], + "lineno": 28, + "name": "queue_time" + }, + { + "args": [], + "lineno": 33, + "name": "doJob" + }, + { + "args": [ + "j" + ], + "lineno": 44, + "name": "operator>" + }, + { + "args": [ + "j" + ], + "lineno": 56, + "name": "operator>=" + }, + { + "args": [ + "j" + ], + "lineno": 68, + "name": "operator<" + }, + { + "args": [ + "j" + ], + "lineno": 80, + "name": "operator<=" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file contains only basic data structure and logic for Job objects. There is no explicit validation logic (e.g., input checking, error handling) in this code. Test coverage would likely exist in higher-level tests that exercise the job queue, job execution, and load monitoring (e.g., tests for the job queue system, scheduler, or load monitor). There are no direct tests for validation paths in this file, and potential issues (such as null mJob or m_loadEvent) are not guarded against. Tests that pass invalid or null jobs would likely crash, indicating a gap in defensive programming and test coverage for invalid input handling.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/core/detail/Job.cpp.ai.md b/src/libxrpl/core/detail/Job.cpp.ai.md new file mode 100644 index 0000000000..fc6af7821f --- /dev/null +++ b/src/libxrpl/core/detail/Job.cpp.ai.md @@ -0,0 +1,43 @@ +# `Job.cpp` — Job Execution Unit for the XRPL Thread Pool + +`Job.cpp` implements the `Job` class, the atomic unit of work dispatched by the XRPL node's cooperative thread pool. Every asynchronous operation in rippled — from transaction validation and ledger acceptance to client RPC handling — is wrapped in a `Job` before being handed off to `JobQueue` and ultimately executed by a worker thread in `Workers`. This file is the implementation counterpart to `include/xrpl/core/Job.h`. + +## Constructors and Intended Usage + +Three constructors exist for different contexts. The **default constructor** (`jtINVALID`, index 0) satisfies the C++ requirement that map value types must be default-constructible — it exists solely so that code like `jobMap[key] = value` compiles. The header comment acknowledges this is a semantic compromise: a `Job` with no associated function has no meaningful invariant. The **two-argument constructor** (type + index) creates a lightweight job descriptor used in job set bookkeeping without an actual callable. The **full five-argument constructor** is the one that produces a dispatchable job: it captures the callable, records the queue timestamp via `clock_type::now()`, and creates a `LoadEvent` linked to the provided `LoadMonitor`. + +The `LoadEvent` is constructed with `shouldStart=false`, meaning the event starts in "waiting" mode with its internal `mark_` set to the current time. This is the beginning of the queue-wait measurement. + +## Priority Ordering via Comparison Operators + +`Job` objects live in a `std::set`, and the four comparison operators implement a deliberate priority inversion. The `JobType` enum in `Job.h` is ordered such that **higher enum values represent higher dispatch priority** — `jtADMIN` and `jtPROPOSAL_t` appear late in the enum and get dispatched before `jtPACK` or `jtPUBOLDLEDGER` which appear early. The `operator<` is written so that a job with a higher `mType` value is considered "less than" (earlier in set order), making the front of the set the highest-priority job. Within the same `JobType`, the `mJobIndex` tiebreaker provides FIFO ordering: a lower index means the job was submitted earlier and should run first. + +This design means callers never sort or rank jobs manually — inserting into the set automatically maintains the dispatch order. The `JobQueue` simply takes from the front of the set. + +## `doJob()` and the Load Measurement Window + +The `doJob()` method contains a subtle but intentional lifecycle pattern: + +```cpp +void Job::doJob() { + beast::setCurrentThreadName("j:" + mName); + m_loadEvent->start(); + m_loadEvent->setName(mName); + mJob(); + mJob = nullptr; // explicit destruction before LoadEvent stops +} +``` + +When `start()` is called on the `LoadEvent`, it accumulates `now - mark_` into `timeWaiting_` (the time spent in the queue since construction) and resets `mark_` to now. This captures the latency from submission to execution as the "wait" component. + +After `mJob()` returns, `mJob = nullptr` explicitly destroys the `std::function` lambda. The inline comment explains why: the `LoadEvent` is held via `shared_ptr` in the `Job` object and only calls `stop()` in its own destructor, which fires when the `Job` is later destroyed by `JobQueue` — *after* `doJob()` has already returned. If the lambda were not nulled out here, its destructor (which may release captured resources non-trivially) would execute during `Job` teardown, outside the load measurement window. By forcing destruction before `doJob()` returns, the lambda's cleanup time is included in `timeRunning_`, giving `LoadMonitor` an accurate picture of total execution cost. + +The thread name is set to `"j:" + mName` before execution and restored by the thread infrastructure afterward, enabling per-job visibility in debuggers and profiling tools. + +## Relationship to `LoadEvent` and `LoadMonitor` + +`LoadEvent` is a RAII timing object. Its destructor calls `stop()`, which computes `timeRunning_` and forwards both wait and run durations to `LoadMonitor::addLoadSample()`. The `LoadMonitor` aggregates these samples to detect when the node is under excessive load and to surface per-job-type latency metrics. The `Job` class acts as the container that ensures a `LoadEvent` lives exactly as long as the job's execution window, making the timing automatic and exception-safe. + +## Design Trade-offs + +The dependency on `LoadMonitor` in the full constructor is acknowledged in the header (`// VFALCO TODO try to remove the dependency on LoadMonitor`) as a layering concern — a pure task scheduler ideally wouldn't require a monitoring reference at construction time. In practice, the coupling is kept minimal: `Job` holds only a `shared_ptr`, and the `LoadMonitor` is passed by reference only during construction. The queue timestamp (`m_queue_time`) provides an independent, monitor-free way to measure queue age for external observers without going through the `LoadEvent` machinery. \ No newline at end of file diff --git a/src/libxrpl/core/detail/JobQueue.cpp.ai.json b/src/libxrpl/core/detail/JobQueue.cpp.ai.json new file mode 100644 index 0000000000..81300c451b --- /dev/null +++ b/src/libxrpl/core/detail/JobQueue.cpp.ai.json @@ -0,0 +1,720 @@ +{ + "args": [ + { + "lineno": 8, + "name": "threadCount" + }, + { + "lineno": 9, + "name": "collector" + }, + { + "lineno": 10, + "name": "journal" + }, + { + "lineno": 11, + "name": "logs" + }, + { + "lineno": 12, + "name": "perfLog" + }, + { + "lineno": 50, + "name": "type" + }, + { + "lineno": 50, + "name": "name" + }, + { + "lineno": 50, + "name": "func" + }, + { + "lineno": 86, + "name": "t" + }, + { + "lineno": 94, + "name": "t" + }, + { + "lineno": 102, + "name": "t" + }, + { + "lineno": 115, + "name": "t" + }, + { + "lineno": 115, + "name": "name" + }, + { + "lineno": 125, + "name": "t" + }, + { + "lineno": 125, + "name": "count" + }, + { + "lineno": 125, + "name": "elapsed" + }, + { + "lineno": 140, + "name": "c" + }, + { + "lineno": 183, + "name": "type" + }, + { + "lineno": 218, + "name": "job" + }, + { + "lineno": 244, + "name": "type" + }, + { + "lineno": 257, + "name": "instance" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "JobQueue::JobQueue", + "JobTypes::instance()", + "JobTypes::instance().getInvalid()", + "m_workers(*this, &perfLog, \"JobQueue\", threadCount)", + "m_collector->make_hook", + "m_collector->make_gauge" + ], + "entry_point": "JobQueue::JobQueue", + "purpose": "Initializes the JobQueue, sets up job types, worker threads, and metrics.", + "validation_points": [ + "JobTypes::instance() validated by XRPL_ASSERT (ensures job types are added)", + "threadCount validated by implicit usage/logging (JLOG)" + ] + }, + { + "call_chain": [ + "JobQueue::addRefCountedJob", + "XRPL_ASSERT(type != jtINVALID)", + "m_jobData.find(type)", + "XRPL_ASSERT(iter != m_jobData.end())", + "XRPL_ASSERT((type >= jtCLIENT && type <= jtCLIENT_WEBSOCKET) || m_workers.getNumberOfThreads() > 0)", + "m_jobSet.emplace(...)", + "XRPL_ASSERT(type != jtINVALID)", + "XRPL_ASSERT(m_jobSet.contains(job))", + "perfLog_.jobQueue(type)", + "getJobTypeData(type)", + "getJobLimit(type)", + "m_workers.addTask() / ++data.deferred" + ], + "entry_point": "JobQueue::addRefCountedJob", + "purpose": "Adds a job to the queue, validates job type, ensures threads are available, and updates job data.", + "validation_points": [ + "XRPL_ASSERT(type != jtINVALID)", + "XRPL_ASSERT(iter != m_jobData.end())", + "XRPL_ASSERT((type >= jtCLIENT && type <= jtCLIENT_WEBSOCKET) || m_workers.getNumberOfThreads() > 0)", + "XRPL_ASSERT(type != jtINVALID) (again after emplace)", + "XRPL_ASSERT(m_jobSet.contains(job))" + ] + }, + { + "call_chain": [ + "JobQueue::collect", + "job_count = m_jobSet.size()" + ], + "entry_point": "JobQueue::collect", + "purpose": "Collects and updates the current job count metric.", + "validation_points": [] + }, + { + "call_chain": [ + "JobQueue::getJobCount", + "m_jobData.find(t)" + ], + "entry_point": "JobQueue::getJobCount", + "purpose": "Returns the number of waiting jobs for a given type.", + "validation_points": [] + }, + { + "call_chain": [ + "JobQueue::getJobCountTotal", + "m_jobData.find(t)" + ], + "entry_point": "JobQueue::getJobCountTotal", + "purpose": "Returns the total number of waiting and running jobs for a given type.", + "validation_points": [] + }, + { + "call_chain": [ + "JobQueue::getJobCountGE", + "for (auto const& x : m_jobData) { if (x.first >= t) ... }" + ], + "entry_point": "JobQueue::getJobCountGE", + "purpose": "Returns the number of jobs at or above a given priority.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "threadCount", + "flow": [ + "JobQueue::JobQueue parameter", + "m_workers(*this, &perfLog, \"JobQueue\", threadCount)", + "JLOG(m_journal.info()) << \"Using \" << threadCount << \" threads\"" + ], + "origin": "JobQueue::JobQueue parameter", + "transformations": [ + "Passed to m_workers for thread pool creation", + "Logged for informational purposes" + ], + "validated_at": "Implicitly validated by usage/logging; explicit validation not shown" + }, + { + "field": "type (JobType)", + "flow": [ + "addRefCountedJob parameter", + "XRPL_ASSERT(type != jtINVALID)", + "m_jobData.find(type)", + "XRPL_ASSERT(iter != m_jobData.end())", + "m_jobSet.emplace(type, ...)", + "XRPL_ASSERT(type != jtINVALID)", + "XRPL_ASSERT(m_jobSet.contains(job))", + "perfLog_.jobQueue(type)", + "getJobTypeData(type)", + "getJobLimit(type)" + ], + "origin": "addRefCountedJob parameter", + "transformations": [ + "Checked for validity (not jtINVALID)", + "Looked up in m_jobData", + "Used to create and track job in m_jobSet", + "Used for performance logging and job limit checks" + ], + "validated_at": "Multiple XRPL_ASSERTs in addRefCountedJob" + }, + { + "field": "m_jobData", + "flow": [ + "JobQueue::JobQueue", + "for (auto const& x : JobTypes::instance())", + "m_jobData.emplace(jt.type(), ...)", + "Used in addRefCountedJob, getJobCount, getJobCountTotal, getJobCountGE" + ], + "origin": "Initialized in JobQueue::JobQueue from JobTypes::instance()", + "transformations": [ + "Populated with JobTypeInfo and metrics objects", + "Queried and updated as jobs are added and processed" + ], + "validated_at": "XRPL_ASSERT(result.second == true) in JobQueue::JobQueue" + }, + { + "field": "m_workers", + "flow": [ + "JobQueue::JobQueue", + "m_workers(*this, &perfLog, \"JobQueue\", threadCount)", + "Used in addRefCountedJob for getNumberOfThreads() and addTask()" + ], + "origin": "Constructed in JobQueue::JobQueue with threadCount", + "transformations": [ + "Thread pool management", + "Checked for available threads before adding jobs" + ], + "validated_at": "XRPL_ASSERT((type >= jtCLIENT && type <= jtCLIENT_WEBSOCKET) || m_workers.getNumberOfThreads() > 0)" + }, + { + "field": "job_count", + "flow": [ + "JobQueue::JobQueue", + "job_count = m_collector->make_gauge(\"job_count\")", + "Updated in JobQueue::collect() as job_count = m_jobSet.size()" + ], + "origin": "Created in JobQueue::JobQueue via m_collector->make_gauge", + "transformations": [ + "Metric gauge for current job count" + ], + "validated_at": "No explicit validation" + } + ], + "description": "Implements the JobQueue class for managing and processing jobs in a multi-threaded environment, including job scheduling, execution, monitoring, and statistics reporting for the XRPL (XRP Ledger) server.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "JobType (enum, type-checked)", + "validation", + "missing", + "check" + ], + "evidence": "Field JobType (enum, type-checked) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for JobType (enum, type-checked)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates JobType (enum, type-checked) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "threadCount (int, type-checked)", + "validation", + "missing", + "check" + ], + "evidence": "Field threadCount (int, type-checked) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for threadCount (int, type-checked)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates threadCount (int, type-checked) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "m_jobData (map, checked for existence)", + "validation", + "missing", + "check" + ], + "evidence": "Field m_jobData (map, checked for existence) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for m_jobData (map, checked for existence)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates m_jobData (map, checked for existence) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.5, + "detection_keywords": [ + "threadCount", + "empty", + "string", + "validation" + ], + "evidence": "implicit (usage/logging) at JobQueue constructor", + "issue_pattern": "Missing empty string validation for threadCount", + "why_false_positive": "implicit (usage/logging) validates threadCount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "JobTypes::instance()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at JobQueue constructor", + "issue_pattern": "Missing empty string validation for JobTypes::instance()", + "why_false_positive": "XRPL_ASSERT validates JobTypes::instance() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (JobType)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at addRefCountedJob", + "issue_pattern": "Missing empty string validation for type (JobType)", + "why_false_positive": "XRPL_ASSERT validates type (JobType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (JobType)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at addRefCountedJob", + "issue_pattern": "Missing empty string validation for type (JobType)", + "why_false_positive": "XRPL_ASSERT validates type (JobType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (JobType) and thread count", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at addRefCountedJob", + "issue_pattern": "Missing empty string validation for type (JobType) and thread count", + "why_false_positive": "XRPL_ASSERT validates type (JobType) and thread count for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "job (in m_jobSet)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at addRefCountedJob", + "issue_pattern": "Missing empty string validation for job (in m_jobSet)", + "why_false_positive": "XRPL_ASSERT validates job (in m_jobSet) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/core/detail/JobQueue.cpp", + "functions": [ + { + "args": [ + "threadCount", + "collector", + "journal", + "logs", + "perfLog" + ], + "lineno": 7, + "name": "JobQueue::JobQueue" + }, + { + "args": [], + "lineno": 38, + "name": "JobQueue::~JobQueue" + }, + { + "args": [], + "lineno": 43, + "name": "JobQueue::collect" + }, + { + "args": [ + "type", + "name", + "func" + ], + "lineno": 49, + "name": "JobQueue::addRefCountedJob" + }, + { + "args": [ + "t" + ], + "lineno": 85, + "name": "JobQueue::getJobCount" + }, + { + "args": [ + "t" + ], + "lineno": 93, + "name": "JobQueue::getJobCountTotal" + }, + { + "args": [ + "t" + ], + "lineno": 101, + "name": "JobQueue::getJobCountGE" + }, + { + "args": [ + "t", + "name" + ], + "lineno": 114, + "name": "JobQueue::makeLoadEvent" + }, + { + "args": [ + "t", + "count", + "elapsed" + ], + "lineno": 124, + "name": "JobQueue::addLoadEvents" + }, + { + "args": [], + "lineno": 134, + "name": "JobQueue::isOverloaded" + }, + { + "args": [ + "c" + ], + "lineno": 139, + "name": "JobQueue::getJson" + }, + { + "args": [], + "lineno": 176, + "name": "JobQueue::rendezvous" + }, + { + "args": [ + "type" + ], + "lineno": 182, + "name": "JobQueue::getJobTypeData" + }, + { + "args": [], + "lineno": 194, + "name": "JobQueue::stop" + }, + { + "args": [], + "lineno": 213, + "name": "JobQueue::isStopped" + }, + { + "args": [ + "job" + ], + "lineno": 217, + "name": "JobQueue::getNextJob" + }, + { + "args": [ + "type" + ], + "lineno": 243, + "name": "JobQueue::finishJob" + }, + { + "args": [ + "instance" + ], + "lineno": 256, + "name": "JobQueue::processTask" + }, + { + "args": [ + "type" + ], + "lineno": 292, + "name": "JobQueue::getJobLimit" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested in the XRPLF/rippled repository under test modules such as src/test/core/JobQueue_test.cpp or similar. These tests typically cover job addition, job limits, thread pool behavior, and validation failures (e.g., invalid job types). However, not all validation paths (especially XRPL_ASSERTs for internal invariants) may be directly tested, as some are intended to catch programming errors rather than user input errors. There may be limited coverage for edge cases like zero threads, invalid job types, or shutdown ordering. Tests for metrics (job_count, hooks) may also be limited.", + "validation_architecture": { + "auto_validated_fields": [ + "JobType (enum, type-checked)", + "threadCount (int, type-checked)", + "m_jobData (map, checked for existence)" + ], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.5, + "error_thrown": "none (just logs thread count)", + "field": "threadCount", + "location": "JobQueue constructor", + "validated_by": "implicit (usage/logging)", + "validates": [ + "threadCount is used to create worker threads", + "no explicit check for >0, but see below" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "contract violation (likely assertion failure)", + "field": "JobTypes::instance()", + "location": "JobQueue constructor", + "validated_by": "XRPL_ASSERT", + "validates": [ + "result.second == true", + "ensures each job type is added to m_jobData" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "contract violation (assertion failure)", + "field": "type (JobType)", + "location": "addRefCountedJob", + "validated_by": "XRPL_ASSERT", + "validates": [ + "type != jtINVALID", + "job type must be valid" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "contract violation (assertion failure)", + "field": "type (JobType)", + "location": "addRefCountedJob", + "validated_by": "XRPL_ASSERT", + "validates": [ + "iter != m_jobData.end()", + "job type must exist in m_jobData" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "contract violation (assertion failure)", + "field": "type (JobType) and thread count", + "location": "addRefCountedJob", + "validated_by": "XRPL_ASSERT", + "validates": [ + "if job type is not a client job, there must be worker threads", + "either (type is client job) OR (threads > 0)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "contract violation (assertion failure)", + "field": "job (in m_jobSet)", + "location": "addRefCountedJob", + "validated_by": "XRPL_ASSERT", + "validates": [ + "job.getType() != jtINVALID", + "job is present in m_jobSet after insertion" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/core/detail/JobQueue.cpp.ai.md b/src/libxrpl/core/detail/JobQueue.cpp.ai.md new file mode 100644 index 0000000000..aeaedfab81 --- /dev/null +++ b/src/libxrpl/core/detail/JobQueue.cpp.ai.md @@ -0,0 +1,59 @@ +# `src/libxrpl/core/detail/JobQueue.cpp` + +## Role in the System + +`JobQueue` is the central asynchronous dispatch engine for a running XRPL node. Nearly every non-trivial operation — processing transactions, publishing ledgers, responding to RPC calls, validating consensus proposals — is ultimately submitted here as a typed job and executed on a background worker thread. The file implements the `JobQueue` class declared in `include/xrpl/core/JobQueue.h`, which privately inherits from `Workers::Callback`. That inheritance is the key architectural seam: `JobQueue` owns the `Workers` thread pool and is simultaneously its sole callback, meaning `processTask()` is called by pool threads whenever work is available. + +## The `Workers` / `processTask` Contract + +`Workers` is a classic semaphore-based thread pool. Each call to `m_workers.addTask()` increments the semaphore and unblocks one idle thread, which then calls `JobQueue::processTask()` exactly once. The 1:1 mapping is strict — every `addTask()` call commits to exactly one `processTask()` invocation. This means the `JobQueue` cannot over-signal the pool; it must track when real work is actually ready to run, not merely enqueued. + +## Job Submission: `addRefCountedJob()` + +The public-facing `addJob()` template wraps a callable in a `ClosureCounter` (via `jobCounter_.wrap()`) before delegating to `addRefCountedJob()`. This ref-counting is what makes graceful shutdown possible: `jobCounter_.join()` in `stop()` blocks until every wrapped closure has been destroyed, meaning every job has completed. + +Inside `addRefCountedJob()`, the submission logic implements a per-type concurrency gate. Each `JobType` has a maximum running-jobs limit defined in `JobTypes.h` (e.g., `jtPACK` allows 1, `jtLEDGER_REQ` allows 3, most client types are unbounded). When a job is added: + +- If `data.waiting + data.running < limit`, `m_workers.addTask()` is called immediately — a worker thread will pick it up soon. +- Otherwise, `data.deferred` is incremented. No thread is woken. The job sits in `m_jobSet` but no `addTask()` is issued yet. + +This deferred mechanism prevents over-committing to the thread pool for high-volume job types. The corresponding release happens in `finishJob()`: when a running job of that type completes, if `deferred > 0`, one deferred count is consumed and `m_workers.addTask()` is called — restoring the 1:1 balance. + +## Job Selection: `getNextJob()` + +Jobs are stored in `m_jobSet`, a `std::set` ordered by job priority (lower enum value = higher priority, since the set is iterated from `begin()`). `getNextJob()` walks the set linearly, skipping any job type that is currently at its running limit. The first type with available slots is claimed: `waiting` is decremented, `running` is incremented, and the job is removed from the set. + +This scan-to-find approach is simple but has a subtle invariant: the `Workers` callback contract guarantees that when `processTask()` is called, at least one runnable job must exist in `m_jobSet`. The assertion `XRPL_ASSERT(iter != m_jobSet.end())` at the end of `getNextJob()` enforces this. Violating it would indicate a logic error in the deferred accounting. + +## `processTask()`: Execution and Timing + +`processTask()` runs on a pool worker thread. It acquires `m_mutex`, calls `getNextJob()` to claim the next runnable job, increments `m_processCount`, then releases the lock before executing the job. The timing structure is deliberate: + +- `q_time` is measured from the job's `queue_time()` to the moment execution starts — this captures queue latency. +- `x_time` is measured from execution start to completion — this captures execution latency. + +Both metrics are forwarded to `perfLog_` (a `PerfLog` instance) and, if either exceeds 10ms, to the per-type `dequeue` and `execute` event instruments exposed through `beast::insight`. These feed into the node's metrics/monitoring infrastructure. + +After `job.doJob()` returns, the `Job` object itself goes out of scope before the mutex is re-acquired. This ordering is intentional: the comment at line 367 explains that job destructors may have side effects (e.g., releasing `LoadEvent` references), and these must complete while the parent objects they may reference are still alive — before any shutdown path proceeds. + +The final block re-acquires the mutex, calls `finishJob()` to decrement `running` and potentially wake a deferred task, decrements `m_processCount`, and signals `cv_` if both `m_processCount == 0` and `m_jobSet.empty()` are true. That condition variable is the rendezvous point for both `rendezvous()` and `stop()`. + +## Shutdown Sequence + +`stop()` proceeds in three phases: + +1. `stopping_ = true` is set atomically, preventing new `addJob()` calls from being accepted (via the `jobCounter_` which stops wrapping closures once joined). +2. `jobCounter_.join()` blocks until all `ClosureCounter`-wrapped job functions have been destroyed — i.e., all job *executions* have finished and returned from `Job::doJob()`. +3. Even after all jobs have returned from `doJob()`, threads may still be between returning from `doJob()` and exiting `processTask()`. The code waits on `cv_` for `m_processCount == 0 && m_jobSet.empty()` before setting `stopped_ = true` and asserting all coroutine suspensions have also completed (`nSuspend_ == 0`). + +This three-phase drain is why the final assertions are trustworthy — each phase closes off a different window of concurrent activity. + +## Metrics and Observability + +The constructor registers a `beast::insight::Hook` (`collect()`) that fires periodically to push the current `m_jobSet.size()` into a gauge named `job_count`. Per-type latency data is surfaced through `getJson()`, which reports waiting counts, running counts, peak queue latency, and average execution latency for every active job type. The `isOverloaded()` predicate checks all `LoadMonitor` instances for whether any type has exceeded its target latency thresholds — this is used upstream to throttle or shed load. + +## Design Notes + +The `m_invalidJobData` sentinel and the associated `getJobTypeData()` fallback reflect an acknowledged technical debt: the codebase still has paths that can query `jtINVALID`, and returning a harmless default is safer than crashing. The comment ("I hate it. We must remove jtINVALID completely") documents the intent to fix this properly. + +The `XRPL_ASSERT` in `addRefCountedJob()` that permits zero-thread queues for `jtCLIENT` through `jtCLIENT_WEBSOCKET` is annotated as a workaround for "incorrect client shutdown ordering" — client-facing job types can be submitted during a shutdown window where worker threads have already been stopped, and silently dropping those jobs is preferable to an assertion failure. \ No newline at end of file diff --git a/src/libxrpl/core/detail/LoadEvent.cpp.ai.json b/src/libxrpl/core/detail/LoadEvent.cpp.ai.json new file mode 100644 index 0000000000..7ff0d3a15c --- /dev/null +++ b/src/libxrpl/core/detail/LoadEvent.cpp.ai.json @@ -0,0 +1,229 @@ +{ + "args": [ + { + "lineno": 6, + "name": "monitor" + }, + { + "lineno": 6, + "name": "name" + }, + { + "lineno": 6, + "name": "shouldStart" + } + ], + "classes": [ + { + "args": [ + "LoadMonitor& monitor", + "std::string const& name", + "bool shouldStart" + ], + "lineno": 6, + "name": "LoadEvent" + } + ], + "code_paths": [ + { + "call_chain": [ + "LoadEvent::stop" + ], + "entry_point": "LoadEvent::stop", + "purpose": "Stops the event timer, records running time, and notifies the monitor.", + "validation_points": [ + "XRPL_ASSERT(running_, ...): Ensures stop() is only called when the event is running." + ] + }, + { + "call_chain": [ + "LoadEvent::~LoadEvent", + "LoadEvent::stop" + ], + "entry_point": "LoadEvent::~LoadEvent", + "purpose": "Destructor ensures that if the event is still running, it is stopped and accounted for.", + "validation_points": [ + "XRPL_ASSERT(running_, ...): Indirectly, if running_ is true, stop() is called and validated." + ] + }, + { + "call_chain": [ + "LoadEvent::start" + ], + "entry_point": "LoadEvent::start", + "purpose": "Starts or restarts the event timer, accumulates waiting time.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "running_", + "flow": [ + "LoadEvent::LoadEvent (sets running_)", + "LoadEvent::start (sets running_ = true)", + "LoadEvent::stop (checks running_ via XRPL_ASSERT, sets running_ = false)", + "LoadEvent::~LoadEvent (checks running_, may call stop())" + ], + "origin": "Constructor parameter 'shouldStart'", + "transformations": [ + "Set at construction", + "Set to true in start()", + "Set to false in stop()" + ], + "validated_at": "LoadEvent::stop (XRPL_ASSERT)" + }, + { + "field": "name_", + "flow": [ + "LoadEvent::LoadEvent (sets name_)", + "LoadEvent::setName (can update name_)", + "LoadEvent::name (returns name_)" + ], + "origin": "Constructor parameter 'name'", + "transformations": [ + "Set at construction", + "Can be updated via setName()" + ], + "validated_at": "Not validated" + }, + { + "field": "mark_", + "flow": [ + "LoadEvent::LoadEvent (initializes mark_)", + "LoadEvent::start (updates mark_)", + "LoadEvent::stop (updates mark_)" + ], + "origin": "Set to std::chrono::steady_clock::now() in constructor", + "transformations": [ + "Updated to current time in start() and stop()" + ], + "validated_at": "Not validated" + }, + { + "field": "timeWaiting_", + "flow": [ + "LoadEvent::start (increments by now - mark_)", + "LoadEvent::waitTime (returns value)" + ], + "origin": "Zero-initialized in constructor", + "transformations": [ + "Incremented in start()" + ], + "validated_at": "Not validated" + }, + { + "field": "timeRunning_", + "flow": [ + "LoadEvent::stop (increments by now - mark_)", + "LoadEvent::runTime (returns value)" + ], + "origin": "Zero-initialized in constructor", + "transformations": [ + "Incremented in stop()" + ], + "validated_at": "Not validated" + } + ], + "description": "Implements the LoadEvent class, which tracks and records timing information for job events (waiting and running times) and reports them to a LoadMonitor in the xrpl namespace.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "running_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at stop()", + "issue_pattern": "Missing empty string validation for running_", + "why_false_positive": "XRPL_ASSERT macro validates running_ for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/core/detail/LoadEvent.cpp", + "functions": [ + { + "args": [ + "LoadMonitor& monitor", + "std::string const& name", + "bool shouldStart" + ], + "lineno": 6, + "name": "LoadEvent::LoadEvent" + }, + { + "args": [], + "lineno": 15, + "name": "LoadEvent::~LoadEvent" + }, + { + "args": [], + "lineno": 21, + "name": "LoadEvent::name" + }, + { + "args": [], + "lineno": 26, + "name": "LoadEvent::waitTime" + }, + { + "args": [], + "lineno": 31, + "name": "LoadEvent::runTime" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 36, + "name": "LoadEvent::setName" + }, + { + "args": [], + "lineno": 41, + "name": "LoadEvent::start" + }, + { + "args": [], + "lineno": 54, + "name": "LoadEvent::stop" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typical tests would be in files like 'LoadEvent_test.cpp' or similar, likely under a 'test' or 'unittest' directory. The critical validation (XRPL_ASSERT on running_ in stop()) should be tested by attempting to call stop() when not running, and ensuring it triggers the assertion. Gaps: No validation on name_, mark_, timeWaiting_, or timeRunning_. No explicit test coverage for edge cases like double start(), double stop(), or destructor behavior when running_ is true.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro)", + "notes": "No input, type, range, or format validation is performed on constructor parameters or setters. Only business logic validation is present via assertion.", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws depending on XRPL_ASSERT implementation)", + "field": "running_", + "location": "stop()", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures stop() is only called if the event is running" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/core/detail/LoadEvent.cpp.ai.md b/src/libxrpl/core/detail/LoadEvent.cpp.ai.md new file mode 100644 index 0000000000..7b784e0062 --- /dev/null +++ b/src/libxrpl/core/detail/LoadEvent.cpp.ai.md @@ -0,0 +1,29 @@ +# `LoadEvent.cpp` — Scoped Job Latency Measurement + +`LoadEvent` is a two-phase stopwatch that attributes elapsed time across a job's lifecycle into two buckets — *waiting* (time spent queued before execution) and *running* (time spent actively executing) — and reports both figures to a `LoadMonitor` when the measurement is closed. It exists because the XRPL job-queue needs to distinguish queue-stall latency from actual CPU work time in order to detect overload conditions at the right level. + +## Two-Phase Timing Model + +The class maintains three mutable state fields: a `running_` boolean, a `mark_` timestamp, and two accumulators `timeWaiting_` and `timeRunning_`. The `mark_` field always records the most recent state transition. On `start()`, the delta since the previous `mark_` is added to `timeWaiting_`, and `mark_` is reset to now. On `stop()`, the delta is added to `timeRunning_`, `mark_` is reset again, and the accumulated figures are pushed to `LoadMonitor::addLoadSample()`. + +This design means `start()` is idempotent in a useful way: calling it multiple times without an intervening `stop()` is explicitly supported. Each extra call simply reclassifies the additional elapsed time as waiting time and advances the mark. The comment in the source says "any time accumulated will be counted as 'waiting'" — this is not a bug, it's the intended model for jobs that get re-queued or delayed before they actually start executing. + +## Lifecycle in Practice + +In `Job.cpp`, a `LoadEvent` is constructed with `shouldStart = false`. At that point, `running_` is false, but `mark_` is already set to `now`. Time begins accumulating immediately in the *waiting* bucket — any delay before `start()` is called is queue-wait time. When the `JobQueue` dispatches the job to a worker thread, it calls `start()`, which flushes the queued time into `timeWaiting_` and begins tracking execution time. When the job finishes, `stop()` is called, flushing the execution time into `timeRunning_` and invoking `monitor_.addLoadSample(*this)`. + +`JobQueue::makeLoadEvent()` provides a parallel factory path for ad-hoc measurements and constructs with `shouldStart = true`, bypassing the wait phase. + +## RAII Guarantee via Destructor + +The destructor checks `running_` and calls `stop()` if still active. This ensures that a `LoadEvent` which goes out of scope during an exception unwind or early return will still report its timing data to the monitor, rather than silently dropping a sample. Because `stop()` contains an `XRPL_ASSERT(running_, ...)` guard, the destructor avoids a redundant stop by only firing when the event is actually still running. The copy constructor is deleted, preventing the monitor from ever receiving a sample twice from the same logical event. + +## Relationship to `LoadMonitor` + +`LoadMonitor::addLoadSample()` (in the sibling `.cpp`) receives the completed `LoadEvent` and computes `runTime() + waitTime()` as the total latency for that sample. Sub-2ms totals are treated as noise and collapsed to zero to avoid jitter inflating averages. Values above 500ms trigger a log warning (above 1s, a `warn`-level log). The latency is then fed into `addSamples()`, which applies an exponential-decay model to rolling average and peak figures — those rolled-up statistics are what ultimately feed the `isOver()` overload detector. + +## Design Observations + +The `setName()` method on a live event exists to allow job names to be refined after construction — useful when a generic job type is initially dispatched but the specific operation name only becomes known once it starts executing. The name is passed through to `LoadMonitor`'s log output, so late-binding the name improves diagnostic quality. + +A surviving comment in the header (`VFALCO TODO Rename LoadEvent to ScopedLoadSample`) accurately describes the intent: this is fundamentally a scoped RAII measurement, not an "event" in the observer-pattern sense. The `LoadMonitor` reference stored by the class creates a hard lifetime dependency — `LoadEvent` must never outlive its monitor, a constraint enforced structurally by the `JobQueue` owning both. \ No newline at end of file diff --git a/src/libxrpl/core/detail/LoadMonitor.cpp.ai.json b/src/libxrpl/core/detail/LoadMonitor.cpp.ai.json new file mode 100644 index 0000000000..0420c50e0f --- /dev/null +++ b/src/libxrpl/core/detail/LoadMonitor.cpp.ai.json @@ -0,0 +1,352 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "LoadMonitor::Stats" + }, + { + "args": [ + "beast::Journal j" + ], + "lineno": 22, + "name": "LoadMonitor" + } + ], + "code_paths": [ + { + "call_chain": [ + "LoadMonitor::addLoadSample", + "LoadMonitor::addSamples", + "LoadMonitor::update" + ], + "entry_point": "LoadMonitor::addLoadSample", + "purpose": "Adds a single load sample (from a LoadEvent), computes latency, logs if high, and updates internal counters with validation.", + "validation_points": [ + "addLoadSample: Validates latency (manual if-check: latency > 500ms)", + "addSamples: Implicitly validates count (int type, arithmetic)", + "update: Validates 'now' (manual if-check: now == mLastUpdate, now < mLastUpdate, now > mLastUpdate + 8s)" + ] + }, + { + "call_chain": [ + "LoadMonitor::addSamples", + "LoadMonitor::update" + ], + "entry_point": "LoadMonitor::addSamples", + "purpose": "Adds multiple samples and updates latency/counters, with validation on time and count.", + "validation_points": [ + "addSamples: Implicitly validates count (int type, arithmetic)", + "update: Validates 'now' (manual if-checks as above)" + ] + }, + { + "call_chain": [ + "LoadMonitor::isOver", + "LoadMonitor::update", + "LoadMonitor::isOverTarget" + ], + "entry_point": "LoadMonitor::isOver", + "purpose": "Checks if current load exceeds target latency thresholds, with validation on event count and latency.", + "validation_points": [ + "isOver: Validates mLatencyEvents != 0", + "isOverTarget: Validates avg/peak against target thresholds" + ] + }, + { + "call_chain": [ + "LoadMonitor::getStats", + "LoadMonitor::update" + ], + "entry_point": "LoadMonitor::getStats", + "purpose": "Retrieves current stats, with validation on event count and time.", + "validation_points": [ + "getStats: Validates mLatencyEvents == 0 (sets latencyAvg/latencyPeak to 0)", + "update: Validates 'now' (manual if-checks as above)" + ] + } + ], + "data_flows": [ + { + "field": "mLastUpdate", + "flow": [ + "constructor", + "update (compared to now, incremented in decay loop)", + "used in all update() calls" + ], + "origin": "Initialized in constructor to UptimeClock::now()", + "transformations": [ + "Set to now in constructor and after reset", + "Incremented by 1s in decay loop" + ], + "validated_at": "update (if now == mLastUpdate, if now < mLastUpdate, if now > mLastUpdate + 8s)" + }, + { + "field": "latency (in addLoadSample)", + "flow": [ + "addLoadSample (computed)", + "manual if-check for logging", + "passed to addSamples" + ], + "origin": "Computed from s.runTime() + s.waitTime()", + "transformations": [ + "Rounded to milliseconds", + "Set to 0ms if < 2ms" + ], + "validated_at": "addLoadSample (if latency > 500ms for logging)" + }, + { + "field": "mCounts", + "flow": [ + "addSamples (incremented by count)", + "update (decayed by 1/4 each second)", + "getStats (divided by 4 for stats.count)" + ], + "origin": "Initialized to 0 in constructor/reset", + "transformations": [ + "Incremented by count", + "Decayed by 1/4 per second in update" + ], + "validated_at": "addSamples (count is int, no explicit range check)" + }, + { + "field": "mLatencyEvents", + "flow": [ + "addSamples (incremented by count)", + "update (decayed by 1/4 each second)", + "isOver, getStats (used as divisor, checked for zero)" + ], + "origin": "Initialized to 0 in constructor/reset", + "transformations": [ + "Incremented by count", + "Decayed by 1/4 per second in update" + ], + "validated_at": "isOver, getStats (checked for zero before division)" + }, + { + "field": "mLatencyMSAvg / mLatencyMSPeak", + "flow": [ + "addSamples (incremented by latency)", + "update (decayed by 1/4 each second)", + "isOver, getStats (used for avg/peak calculations)" + ], + "origin": "Initialized to 0 in constructor/reset", + "transformations": [ + "Incremented by latency", + "Decayed by 1/4 per second in update" + ], + "validated_at": "isOverTarget (compared to target thresholds)" + }, + { + "field": "mTargetLatencyAvg / mTargetLatencyPk", + "flow": [ + "setTargetLatency (set)", + "isOverTarget (compared against computed avg/peak)" + ], + "origin": "Set via setTargetLatency", + "transformations": [ + "Direct assignment" + ], + "validated_at": "isOverTarget (manual if-checks for > 0ms and comparison)" + } + ], + "description": "Implements the LoadMonitor class for tracking and managing system load and latency statistics, including methods for updating, sampling, and querying load metrics.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "now (current time)", + "empty", + "string", + "validation" + ], + "evidence": "manual if-check at LoadMonitor::update", + "issue_pattern": "Missing empty string validation for now (current time)", + "why_false_positive": "manual if-check validates now (current time) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "latency (derived from s.runTime() + s.waitTime())", + "empty", + "string", + "validation" + ], + "evidence": "manual if-check at LoadMonitor::addLoadSample", + "issue_pattern": "Missing empty string validation for latency (derived from s.runTime() + s.waitTime())", + "why_false_positive": "manual if-check validates latency (derived from s.runTime() + s.waitTime()) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "count (number of samples)", + "empty", + "string", + "validation" + ], + "evidence": "implicit type (int) and arithmetic at LoadMonitor::addSamples", + "issue_pattern": "Missing empty string validation for count (number of samples)", + "why_false_positive": "implicit type (int) and arithmetic validates count (number of samples) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "count (number of samples)", + "type", + "validation", + "check" + ], + "evidence": "implicit type (int) and arithmetic at LoadMonitor::addSamples", + "issue_pattern": "Missing type validation for count (number of samples)", + "why_false_positive": "implicit type (int) and arithmetic validates count (number of samples) type" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/core/detail/LoadMonitor.cpp", + "functions": [ + { + "args": [], + "lineno": 32, + "name": "LoadMonitor::update" + }, + { + "args": [ + "LoadEvent const& s" + ], + "lineno": 61, + "name": "LoadMonitor::addLoadSample" + }, + { + "args": [ + "int count", + "std::chrono::milliseconds latency" + ], + "lineno": 77, + "name": "LoadMonitor::addSamples" + }, + { + "args": [ + "std::chrono::milliseconds avg", + "std::chrono::milliseconds pk" + ], + "lineno": 93, + "name": "LoadMonitor::setTargetLatency" + }, + { + "args": [ + "std::chrono::milliseconds avg", + "std::chrono::milliseconds peak" + ], + "lineno": 98, + "name": "LoadMonitor::isOverTarget" + }, + { + "args": [], + "lineno": 105, + "name": "LoadMonitor::isOver" + }, + { + "args": [], + "lineno": 116, + "name": "LoadMonitor::getStats" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in the provided context. Typically, LoadMonitor would be tested via unit tests in files like 'LoadMonitor_test.cpp' or integration tests that exercise load event handling. Key validation paths (time validation, latency threshold checks, count arithmetic) should be covered by tests that: (1) add samples with varying latencies, (2) simulate time jumps (to test update's decay/reset logic), (3) set target latencies and check isOver/isOverTarget, and (4) check stats output. Gaps may exist if tests do not cover edge cases such as negative/zero counts, extreme time jumps, or very high latency values. Manual validation (e.g., logging for high latency) may not be directly testable unless logs are captured and asserted.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation, no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "now (current time)", + "location": "LoadMonitor::update", + "validated_by": "manual if-check", + "validates": [ + "Checks if now == mLastUpdate (duplicate update)", + "Checks if now < mLastUpdate (clock skew or error)", + "Checks if now > mLastUpdate + 8s (too old, reset state)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logging only)", + "field": "latency (derived from s.runTime() + s.waitTime())", + "location": "LoadMonitor::addLoadSample", + "validated_by": "manual if-check", + "validates": [ + "If latency > 500ms, logs warning/info", + "If latency < 2ms, sets latency to 0ms (ignores jitter)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (no explicit check)", + "field": "count (number of samples)", + "location": "LoadMonitor::addSamples", + "validated_by": "implicit type (int) and arithmetic", + "validates": [ + "count is used as divisor, but not explicitly checked for zero or negative" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/core/detail/LoadMonitor.cpp.ai.md b/src/libxrpl/core/detail/LoadMonitor.cpp.ai.md new file mode 100644 index 0000000000..113e6d5cad --- /dev/null +++ b/src/libxrpl/core/detail/LoadMonitor.cpp.ai.md @@ -0,0 +1,61 @@ +# `LoadMonitor.cpp` — Exponential-Decay Load Statistics Tracker + +`LoadMonitor` tracks the latency and throughput of the XRPL job-processing pipeline using an exponentially-decayed statistics model. It sits between the raw event timing produced by `LoadEvent` and the policy decisions made by `JobQueue`, answering the question: "Is this subsystem overloaded right now?" + +## Role in the System + +`LoadEvent` is a RAII-style scoped timer. When it destructs (or when `stop()` is called explicitly), it calls `monitor_.addLoadSample(*this)`, handing its accumulated wait/run durations back to the `LoadMonitor` that owns it. `JobQueue` holds one `LoadMonitor` per job type and consults `isOver()` and `getStats()` to throttle work or expose overload state to callers. The `LoadMonitor` is the bridge between raw nanosecond-precision timing and a smoothed, human-interpretable load signal. + +## Two Clocks, Two Purposes + +A deliberate asymmetry in clock choice runs through this design. `LoadEvent` measures actual elapsed time with `std::chrono::steady_clock` — high-resolution, monotonic, suitable for microsecond intervals. `LoadMonitor::update()`, by contrast, uses `UptimeClock::now()`, which returns whole seconds since process start from a cached atomic integer. This coarse granularity is intentional: `update()` is called on every sample insertion, and `UptimeClock` avoids system-call overhead when nothing needs to change. The second-precision boundary is exactly the decay granularity, so there is no resolution loss in practice. + +## The Exponential Decay Model + +The central technique is a per-second exponential decay loop inside `update()`. Every accumulated counter — `mCounts`, `mLatencyEvents`, `mLatencyMSAvg`, `mLatencyMSPeak` — is reduced by one quarter each second: + +```cpp +mCounts -= ((mCounts + 3) / 4); +``` + +The `+3` provides rounding-up integer arithmetic equivalent to `ceil(x/4)`. As the code comment explains: if you add 10 to a value every second and subtract 1/4 per second, the value reaches equilibrium at 40. This means all accumulators idle at **4× their instantaneous per-second rate**. That factor of 4 propagates everywhere: `getStats()` divides by `mLatencyEvents * 4` to recover the true average, and `stats.count` is `mCounts / 4`. This stored-at-4x convention is not arbitrary — it is the mathematical consequence of the decay ratio chosen, and callers must not interpret raw fields directly. + +The loop advances `mLastUpdate` by one second per iteration, catching up to the current second. If the clock jumps backwards or more than 8 seconds have elapsed since the last update, all state is reset to zero. This protects against stale data after process pause or extended idle periods; the 8-second threshold is a heuristic without documented rationale (marked `// VFALCO TODO Why 8?`). + +## Latency Jitter Filtering and Logging + +`addLoadSample()` receives a `LoadEvent` and computes `total = runTime() + waitTime()`. Totals below 2ms are collapsed to zero before being submitted to `addSamples()`: + +```cpp +auto const latency = total < 2ms ? 0ms : round(total); +``` + +This suppresses scheduling jitter that would otherwise inflate the average. The rounding to whole milliseconds is also deliberate — the decay arithmetic depends on integer-compatible durations. + +When a sample exceeds 500ms, the event is logged: at `warn` level above 1 second, `info` level between 500ms and 1s. This is the only place in `LoadMonitor` that uses the `beast::Journal` passed at construction, and it logs per-event rather than throttling repeated slow events. + +## Peak Estimation + +Peak tracking inside `addSamples()` is more subtle than the average: + +```cpp +auto const latencyPeak = mLatencyEvents * latency * 4 / count; +if (mLatencyMSPeak < latencyPeak) + mLatencyMSPeak = latencyPeak; +``` + +This scales the incoming latency by the current event density (`mLatencyEvents / count`), amplified by 4, before comparing against the running peak. Effectively, a high-latency event arriving when there are already many events in flight registers as a higher peak than the same latency arriving in isolation. This means the peak metric captures burst conditions rather than just isolated slow events. + +## Locking Model + +The `mutex_` protects `mCounts`, `mLatencyEvents`, `mLatencyMSAvg`, `mLatencyMSPeak`, and `mLastUpdate`. The comment above `update()` notes it must be called with the mutex already held — `update()` itself does not acquire it. This means all three entry points that call `update()` — `addSamples()`, `isOver()`, and `getStats()` — acquire the lock first and then call `update()` internally. + +`setTargetLatency()` and `isOverTarget()` do **not** acquire the mutex. The target thresholds (`mTargetLatencyAvg`, `mTargetLatencyPk`) are written without synchronization and read in `isOverTarget()` while the caller may or may not hold the lock. This is a known design gap flagged in the original code comments. + +`addLoadSample()` is the one public entry point that does not directly lock — it delegates entirely to `addSamples()`, which does. The jitter-filtering and logging in `addLoadSample()` happen without holding the lock, which is safe since they only read from the immutable `LoadEvent` argument. + +## Overload Detection + +`isOver()` computes derived average and peak values and passes them to `isOverTarget()`. The threshold check in `isOverTarget()` short-circuits when a target is zero — a zero target means "no limit." This allows either the average or peak threshold to be independently disabled. + +The `Stats` struct returned by `getStats()` packages count, average latency, peak latency, and a pre-computed `isOverloaded` flag into a single snapshot taken under the lock, giving callers a consistent view without needing to hold the lock themselves. \ No newline at end of file diff --git a/src/libxrpl/core/detail/Workers.cpp.ai.json b/src/libxrpl/core/detail/Workers.cpp.ai.json new file mode 100644 index 0000000000..054778fc1c --- /dev/null +++ b/src/libxrpl/core/detail/Workers.cpp.ai.json @@ -0,0 +1,309 @@ +{ + "args": [ + { + "lineno": 8, + "name": "callback" + }, + { + "lineno": 9, + "name": "perfLog" + }, + { + "lineno": 10, + "name": "threadNames" + }, + { + "lineno": 11, + "name": "numberOfThreads" + }, + { + "lineno": 102, + "name": "workers" + }, + { + "lineno": 102, + "name": "threadName" + }, + { + "lineno": 102, + "name": "instance" + }, + { + "lineno": 88, + "name": "stack" + } + ], + "classes": [ + { + "args": [ + "Callback& callback", + "perf::PerfLog* perfLog", + "std::string const& threadNames", + "int numberOfThreads" + ], + "lineno": 7, + "name": "Workers" + }, + { + "args": [ + "Workers& workers", + "std::string const& threadName", + "int const instance" + ], + "lineno": 101, + "name": "Worker" + } + ], + "code_paths": [ + { + "call_chain": [ + "Workers::Workers", + "Workers::setNumberOfThreads" + ], + "entry_point": "Workers::Workers", + "purpose": "Constructs a Workers pool and sets up the initial number of threads.", + "validation_points": [ + "Workers::setNumberOfThreads" + ] + }, + { + "call_chain": [ + "Workers::setNumberOfThreads" + ], + "entry_point": "Workers::setNumberOfThreads", + "purpose": "Adjusts the number of worker threads, either increasing or decreasing as needed.", + "validation_points": [ + "Workers::setNumberOfThreads" + ] + }, + { + "call_chain": [ + "Workers::stop", + "Workers::setNumberOfThreads" + ], + "entry_point": "Workers::stop", + "purpose": "Stops all workers by setting thread count to zero and waiting for all to pause.", + "validation_points": [ + "Workers::setNumberOfThreads" + ] + } + ], + "data_flows": [ + { + "field": "numberOfThreads", + "flow": [ + "Workers::Workers (argument)", + "Workers::setNumberOfThreads (parameter)", + "m_numberOfThreads (class member)" + ], + "origin": "Constructor argument to Workers::Workers", + "transformations": [ + "Compared to current m_numberOfThreads", + "If increased: new Worker objects created or paused workers reused", + "If decreased: m_pauseCount incremented, m_semaphore notified" + ], + "validated_at": "Workers::setNumberOfThreads (checks if value is different from current)" + }, + { + "field": "m_numberOfThreads", + "flow": [ + "Workers::setNumberOfThreads", + "Used in thread management logic (increase/decrease workers)", + "Used in Workers::getNumberOfThreads" + ], + "origin": "Set in Workers::setNumberOfThreads", + "transformations": [ + "Updated to new value after thread pool adjustment" + ], + "validated_at": "Workers::setNumberOfThreads" + }, + { + "field": "m_semaphore", + "flow": [ + "Workers::setNumberOfThreads (notify when pausing threads)", + "Workers::addTask (notify to add work)" + ], + "origin": "Initialized in Workers constructor", + "transformations": [ + "Notified to wake up threads for pausing or new tasks" + ], + "validated_at": "Not directly validated, but usage is controlled by thread management logic" + } + ], + "description": "Implements a thread pool (Workers) for managing and executing tasks concurrently, with support for dynamic resizing, pausing, and stopping worker threads. Includes worker thread lifecycle management and synchronization.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "numberOfThreads", + "empty", + "string", + "validation" + ], + "evidence": "Workers::setNumberOfThreads at Workers::setNumberOfThreads", + "issue_pattern": "Missing empty string validation for numberOfThreads", + "why_false_positive": "Workers::setNumberOfThreads validates numberOfThreads for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/core/detail/Workers.cpp", + "functions": [ + { + "args": [ + "Callback& callback", + "perf::PerfLog* perfLog", + "std::string const& threadNames", + "int numberOfThreads" + ], + "lineno": 7, + "name": "Workers" + }, + { + "args": [], + "lineno": 18, + "name": "~Workers" + }, + { + "args": [], + "lineno": 22, + "name": "getNumberOfThreads" + }, + { + "args": [ + "int numberOfThreads" + ], + "lineno": 31, + "name": "setNumberOfThreads" + }, + { + "args": [], + "lineno": 67, + "name": "stop" + }, + { + "args": [], + "lineno": 80, + "name": "addTask" + }, + { + "args": [], + "lineno": 84, + "name": "numberOfCurrentlyRunningTasks" + }, + { + "args": [ + "beast::LockFreeStack& stack" + ], + "lineno": 88, + "name": "deleteWorkers" + }, + { + "args": [ + "Workers& workers", + "std::string const& threadName", + "int const instance" + ], + "lineno": 101, + "name": "Worker" + }, + { + "args": [], + "lineno": 106, + "name": "~Worker" + }, + { + "args": [], + "lineno": 113, + "name": "notify" + }, + { + "args": [], + "lineno": 118, + "name": "run" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in the provided code. Typically, this code would be tested by integration or unit tests in the XRPLF/rippled repository, possibly under test/unit/core/ or test/unit/detail/ directories. The main validation (numberOfThreads) is only checked for equality with the current value, not for range or invalid values (e.g., negative numbers), so edge cases may not be covered unless explicitly tested elsewhere. Test coverage gaps may include: negative or zero thread counts, rapid increase/decrease cycles, and thread pool exhaustion or race conditions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "numberOfThreads", + "location": "Workers::setNumberOfThreads", + "validated_by": "Workers::setNumberOfThreads", + "validates": [ + "Checks if new numberOfThreads equals current m_numberOfThreads", + "If equal, function returns early and does not proceed" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/core/detail/Workers.cpp.ai.md b/src/libxrpl/core/detail/Workers.cpp.ai.md new file mode 100644 index 0000000000..ec19364899 --- /dev/null +++ b/src/libxrpl/core/detail/Workers.cpp.ai.md @@ -0,0 +1,49 @@ +# Workers.cpp — XRPL Thread Pool Implementation + +## Role and Purpose + +`Workers.cpp` implements the core thread-pool machinery used throughout the XRPL node to process asynchronous work items. It lives in `src/libxrpl/core/detail/` as an internal implementation detail, providing a fixed-interface thread pool where callers submit tasks via `addTask()` and receive callbacks through a single virtual method `Callback::processTask(int instance)`. The design intentionally keeps task semantics opaque — a "task" is simply one call to `processTask`; what that call does is entirely the caller's concern. + +## Two-Class Architecture + +The file defines two closely coupled classes: the outer `Workers` managing the pool policy and the inner `Worker` representing each OS thread. `Worker` inherits from **two** instantiations of `beast::LockFreeStack::Node` — one untagged (for `m_everyone`, the ownership list) and one tagged with `PausedTag` (for `m_paused`, the reuse list). This dual-inheritance trick lets a single `Worker` object simultaneously live in both lock-free stacks without extra allocation, relying on the fact that each `LockFreeStack::Node` base carries its own independent `m_next` atomic pointer. + +## Semaphore as the Single Dispatch Channel + +The central synchronization primitive is a hand-rolled `semaphore` (a counting semaphore built on `std::mutex` + `std::condition_variable`). The project cannot use `std::counting_semaphore` because both GCC before 16.0 and Clang before 19.1 contained bugs in that facility (see `semaphore.h` for the linked bug reports). The custom type is an alias `xrpl::semaphore = basic_semaphore`. + +Crucially, `m_semaphore` carries **two kinds of signals** on the same channel: real work items posted by `addTask()`, and "please pause" signals posted by `setNumberOfThreads()` when the count is being reduced. There is no separate control channel. This is intentional: it keeps the worker loop simple. Every time a worker is woken, it first checks `m_pauseCount`. If the atomic is positive, the worker attempts a decrement-and-claim. If successful (`pauseCount >= 0` after decrement), it breaks out of the active loop and parks itself. If another racing thread already stole the pause slot (decrement went negative), the worker undoes its decrement and falls through to claim a real task. This compare-and-undo pattern at lines 196–207 is a lightweight optimistic-concurrency protocol that avoids a heavier CAS loop. + +## Worker Lifecycle: Active → Paused → Active + +`Workers` never destroys a thread until `Workers` itself is destroyed. When the target thread count drops, workers are paused rather than terminated. The lifecycle has three named states as documented in the header: + +- **Active/Idle**: The thread is in the `for(;;)` loop at line 180, blocked on `m_semaphore.wait()`. +- **Paused**: The thread has pushed itself onto `m_paused`, decremented `m_activeCount`, and is blocked on its own `wakeup_` condition variable at the `[1]` label (line 247). +- **Active (resumed)**: `setNumberOfThreads()` increasing the count calls `Worker::notify()`, which increments `wakeCount_` and signals `wakeup_`. The paused thread unblocks, the `do/while` loop continues, and it re-enters the active state incrementing `m_activeCount` again. + +The outer `do { ... } while (!shouldExit)` structure is the key: the exit check only happens when a thread wakes from a pause, not from a task wake. This is a deliberate invariant — a thread cannot be told to exit while it is idle-waiting for tasks; it must first be paused by `setNumberOfThreads(0)` (which `stop()` calls), then its destructor signals `shouldExit_`. + +## Thread Teardown and the Double-Condition Stop + +`stop()` calls `setNumberOfThreads(0)` to post one pause signal per active thread, then waits on a condition variable with a two-part predicate: + +```cpp +m_cv.wait(lk, [this] { return m_allPaused && numberOfCurrentlyRunningTasks() == 0; }); +``` + +Both conditions are necessary because `m_allPaused` (a `bool` protected by `m_mut`) and `m_runningTaskCount` (an `std::atomic`) are **not** synchronized under the same lock. There is a window where the last worker has set `m_allPaused = true` and pushed itself onto the paused list, but has not yet returned from `processTask()` — meaning `m_runningTaskCount` is still non-zero. Waiting on only `m_allPaused` would race against that task completing. + +The inverse race — missing the condition variable signal — is prevented by the code at lines 221–225. When `m_runningTaskCount` drops to zero, the worker acquires `m_mut` before calling `m_cv.notify_all()`. This lock acquisition serializes against the predicate evaluation inside `stop()`'s `cv.wait()`: it guarantees the notification cannot be delivered between the time `stop()` evaluates the predicate as false and the time it actually sleeps. + +After `stop()` returns, `~Workers()` calls `deleteWorkers(m_everyone)`, which pops workers off the ownership stack and `delete`s each one. `Worker::~Worker()` sets `shouldExit_ = true`, increments `wakeCount_`, signals `wakeup_`, and calls `thread_.join()` — a clean, blocking teardown per thread. + +## Dynamic Resize and Thread Reuse + +`setNumberOfThreads()` checks the delta between the old and new counts. On increase, it first tries to pop a paused worker from `m_paused` and call `notify()` on it before creating a new `Worker` object. This reuse strategy avoids OS thread creation overhead when the pool is merely being expanded back to a previous size. A `static int instance` counter assigns a monotonically increasing identity to each newly created (not reused) worker; this `instance_` value flows into every `processTask(instance)` call, letting the `PerfLog` subsystem track per-slot telemetry without dynamic dispatch or thread-local storage. + +The comment at line 36 flags an important limitation: rapid reduce-then-increase calls can create more paused threads than intended, because the pause signals are in-flight in the semaphore before any worker has consumed them. This is a known approximation, not a bug — the pool will eventually self-correct as workers claim the orphaned pause signals. + +## Thread Naming + +`beast::setCurrentThreadName()` is called at the top of each task-processing iteration to restore the configured name, guarding against callbacks that rename their own thread. Paused threads receive a parenthesized variant `"(Worker)"` to make them identifiable in debugger thread lists. \ No newline at end of file diff --git a/src/libxrpl/crypto/RFC1751.cpp.ai.json b/src/libxrpl/crypto/RFC1751.cpp.ai.json new file mode 100644 index 0000000000..a2602271dd --- /dev/null +++ b/src/libxrpl/crypto/RFC1751.cpp.ai.json @@ -0,0 +1,224 @@ +{ + "args": [ + { + "lineno": 1, + "name": "s" + }, + { + "lineno": 1, + "name": "start" + }, + { + "lineno": 1, + "name": "length" + }, + { + "lineno": 1, + "name": "strHuman" + }, + { + "lineno": 1, + "name": "strData" + }, + { + "lineno": 1, + "name": "x" + }, + { + "lineno": 1, + "name": "strWord" + }, + { + "lineno": 1, + "name": "iMin" + }, + { + "lineno": 1, + "name": "iMax" + }, + { + "lineno": 1, + "name": "vsHuman" + }, + { + "lineno": 1, + "name": "strKey" + }, + { + "lineno": 1, + "name": "strHuman" + }, + { + "lineno": 1, + "name": "blob" + }, + { + "lineno": 1, + "name": "bytes" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RFC1751::getKeyFromEnglish", + "boost::algorithm::split (splits input into words)", + "RFC1751::wsrch (validates each word against dictionary)", + "RFC1751::etob (converts words to binary key)" + ], + "entry_point": "RFC1751::getKeyFromEnglish", + "purpose": "Converts a space-separated list of RFC1751 words into a binary key. Validates that each word is in the dictionary.", + "validation_points": [ + "RFC1751::wsrch (checks if word is in dictionary, returns -1 if not found)", + "RFC1751::etob (checks word count, word validity, and binary conversion correctness)" + ] + }, + { + "call_chain": [ + "RFC1751::getEnglishFromKey", + "RFC1751::btoe (converts binary key to word indices)", + "RFC1751::getWordFromBlob (maps indices to words)" + ], + "entry_point": "RFC1751::getEnglishFromKey", + "purpose": "Converts a binary key into a space-separated list of RFC1751 words.", + "validation_points": [ + "RFC1751::btoe (validates key length and conversion correctness)" + ] + } + ], + "data_flows": [ + { + "field": "english_words (std::string)", + "flow": [ + "User input", + "boost::algorithm::split (splits into vector words)", + "RFC1751::wsrch (each word validated against dictionary)", + "RFC1751::etob (words converted to binary key)", + "Output: binary key (std::vector)" + ], + "origin": "User input to RFC1751::getKeyFromEnglish", + "transformations": [ + "Trimmed and split into words", + "Each word mapped to dictionary index", + "Indices packed into binary" + ], + "validated_at": "RFC1751::wsrch (word validity), RFC1751::etob (word count and conversion)" + }, + { + "field": "binary_key (std::vector)", + "flow": [ + "User input", + "RFC1751::btoe (binary to word indices)", + "RFC1751::getWordFromBlob (indices to words)", + "Output: space-separated words (std::string)" + ], + "origin": "User input to RFC1751::getEnglishFromKey", + "transformations": [ + "Binary unpacked to indices", + "Indices mapped to dictionary words" + ], + "validated_at": "RFC1751::btoe (key length and conversion)" + } + ], + "description": "Implements RFC 1751 English encoding/decoding for binary keys, including conversion between binary data and human-readable word sequences, using a fixed dictionary. Provides functions for encoding, decoding, and validating keys, as well as utility functions for word normalization and hashing.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/crypto/RFC1751.cpp", + "functions": [ + { + "args": [ + "s", + "start", + "length" + ], + "lineno": 1, + "name": "RFC1751::extract" + }, + { + "args": [ + "strHuman", + "strData" + ], + "lineno": 1, + "name": "RFC1751::btoe" + }, + { + "args": [ + "s", + "x", + "start", + "length" + ], + "lineno": 1, + "name": "RFC1751::insert" + }, + { + "args": [ + "strWord" + ], + "lineno": 1, + "name": "RFC1751::standard" + }, + { + "args": [ + "strWord", + "iMin", + "iMax" + ], + "lineno": 1, + "name": "RFC1751::wsrch" + }, + { + "args": [ + "strData", + "vsHuman" + ], + "lineno": 1, + "name": "RFC1751::etob" + }, + { + "args": [ + "strKey", + "strHuman" + ], + "lineno": 1, + "name": "RFC1751::getKeyFromEnglish" + }, + { + "args": [ + "strHuman", + "strKey" + ], + "lineno": 1, + "name": "RFC1751::getEnglishFromKey" + }, + { + "args": [ + "blob", + "bytes" + ], + "lineno": 1, + "name": "RFC1751::getWordFromBlob" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 1, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage for RFC1751 is typically found in unit tests for mnemonic encoding/decoding (e.g., test/crypto/RFC1751_test.cpp or similar). These tests usually cover valid conversions (key <-> words), invalid word detection, and edge cases (wrong word count, invalid words). Gaps may include: malformed input (extra spaces, mixed case), dictionary boundary cases, and fuzzed/invalid binary input. No evidence of property-based or fuzz testing in standard test suites.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/crypto/RFC1751.cpp.ai.md b/src/libxrpl/crypto/RFC1751.cpp.ai.md new file mode 100644 index 0000000000..958b97a1af --- /dev/null +++ b/src/libxrpl/crypto/RFC1751.cpp.ai.md @@ -0,0 +1,50 @@ +# `RFC1751.cpp` — Mnemonic Encoding of Binary Keys + +## Purpose and Origin + +This file implements [RFC 1751](https://www.rfc-editor.org/rfc/rfc1751), a 1994 IETF standard for converting raw binary data into pronounceable English words. Its primary use in XRPL is encoding 128-bit cryptographic seeds as sequences of 12 simple words, making them easier to write down, speak aloud, or recognize in a log. The entire implementation lives in the `xrpl` namespace as a purely static class, `RFC1751`, with no instance state. + +## The Dictionary and Encoding Scheme + +The foundation of RFC 1751 is a fixed 2048-word dictionary (`s_dictionary`) organized with careful structure. The first 571 entries are words of three characters or fewer; entries 571–2047 are four-character words. This split is not accidental — it allows `wsrch()` to halve the search space based solely on input word length, avoiding unnecessary comparisons across word-length boundaries. + +The mathematical motivation for the dictionary size is direct: 2^11 = 2048. Each word therefore represents exactly 11 bits of binary data. An 8-byte (64-bit) input maps to six words (6 × 11 = 66 bits), with the remaining 2 bits carrying a parity checksum. This scheme is applied twice to cover a full 128-bit key, producing 12 words total. + +## Bit Manipulation Primitives: `extract()` and `insert()` + +All the codec work flows through two sub-byte primitives. `extract(s, start, length)` reads `length` bits (up to 11) from a byte array starting at an arbitrary bit offset, using a three-byte window (`cl`, `cc`, `cr`) assembled into a 24-bit integer from which bits are right-justified and masked. `insert(s, x, start, length)` performs the inverse, OR-ing a value into the appropriate byte positions. Both are guarded by `XRPL_ASSERT` calls verifying that `length ≤ 11`, `start ≥ 0`, and `start + length ≤ 66`, enforcing the contract that callers never exceed the 9-byte parity buffer. + +The use of bitwise OR in `insert()` rather than assignment is deliberate — the output buffer starts zero-initialized and each call OR-s in its 11-bit chunk, making partial writes safe and accumulative. + +## Encoding: `btoe()` (Binary to English) + +`btoe()` takes an 8-byte binary string, copies it into a 9-byte local buffer, then computes a simple 2-bit parity value by summing all 32 pairs of bits across the 64-bit payload. This parity is stored in the high bits of the ninth byte. The function then calls `extract()` six times with starting positions 0, 11, 22, 33, 44, and 55, converting each 11-bit group into a dictionary index and concatenating the corresponding words with spaces. The parity bits occupy positions 64–65, using the ninth buffer byte. + +## Decoding: `etob()` (English to Binary) + +`etob()` reverses the process with rigorous validation at every step: + +1. Rejects inputs that don't contain exactly 6 words. +2. Rejects any word longer than 4 characters or shorter than 1. +3. Calls `standard()` to normalize each word before lookup. +4. Uses `wsrch()` to binary-search the dictionary, restricting the range to `[0, 570)` for short words and `[571, 2048)` for four-letter words. +5. Returns `0` (word not found) or `-1` (badly formed) on any lookup failure. +6. After reconstructing all 8 bytes, recomputes the parity and compares against the two parity bits extracted from position 64. A mismatch returns `-2`, distinguishing "plausible but corrupted" from "invalid word." + +This tiered error code system (`1` = success, `0` = unknown word, `-1` = malformed, `-2` = parity failure) allows callers to surface meaningful error messages to users. + +## Input Normalization: `standard()` + +`standard()` handles the ambiguity inherent in handwritten or OCR'd word sequences. It uppercases all letters, and maps three digit substitutions: `'1' → 'L'`, `'0' → 'O'`, `'5' → 'S'`. These substitutions reflect the visual similarity between the digit and letter forms. This small tolerance layer means a user who writes `0BEY` instead of `OBEY` will still decode successfully — an important usability consideration for a mnemonic that might be transcribed on paper. + +## Public Interface: `getKeyFromEnglish()` and `getEnglishFromKey()` + +The two public conversion functions operate on 128-bit (16-byte) keys by splitting them into two 8-byte halves. `getEnglishFromKey()` calls `btoe()` on each half and concatenates the results with a space, producing a 12-word string. `getKeyFromEnglish()` trims whitespace, splits on spaces using `boost::algorithm::split` with `token_compress_on` (handling extra spaces robustly), validates that exactly 12 words were found, and calls `etob()` on each 6-word half. In `Seed.cpp`, the reconstructed 16-byte string is reversed before being wrapped into a `uint128`, matching the big-endian convention described in the RFC. + +## `getWordFromBlob()`: A Secondary Utility + +This function is structurally separate from the RFC 1751 codec. It takes arbitrary binary data, runs a Jenkins one-at-a-time hash over all bytes, and reduces the 32-bit hash modulo 2048 to select a single word from the dictionary. The header comment explicitly notes this is *not* cryptographically secure; it is intended to produce a stable, memorable label for an opaque blob. In `NetworkOPs.cpp`, it is called to derive a single-word pseudonym from a validator node's public key, used as a privacy-preserving hostname alias for non-admin RPC responses. This reuse of the RFC 1751 dictionary for a non-RFC-1751 purpose is purely pragmatic — the dictionary already provides a vetted set of short, pronounceable English words. + +## Error Handling and Defensive Patterns + +The implementation makes no use of exceptions. All errors propagate through integer return codes. `XRPL_ASSERT` guards enforce preconditions in the bit-manipulation primitives but are stripped in release builds. The critical user-facing validation — word count, word length, dictionary membership, parity — is checked explicitly and returns distinct error codes, avoiding silent corruption of key material. The parity check is not a cryptographic integrity guarantee, but it catches the most common failure mode: a mis-transcribed or mis-ordered mnemonic. \ No newline at end of file diff --git a/src/libxrpl/crypto/csprng.cpp.ai.json b/src/libxrpl/crypto/csprng.cpp.ai.json new file mode 100644 index 0000000000..bcaeac6d6e --- /dev/null +++ b/src/libxrpl/crypto/csprng.cpp.ai.json @@ -0,0 +1,314 @@ +{ + "args": [ + { + "lineno": 23, + "name": "buffer" + }, + { + "lineno": 23, + "name": "count" + }, + { + "lineno": 43, + "name": "ptr" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "crypto_prng()", + "csprng_engine::csprng_engine()" + ], + "entry_point": "crypto_prng()", + "purpose": "Initializes the global cryptographically secure PRNG engine singleton.", + "validation_points": [ + "csprng_engine::csprng_engine() - Validates OpenSSL entropy pool initialization via RAND_poll()" + ] + }, + { + "call_chain": [ + "csprng_engine::operator()(void* ptr, std::size_t count)", + "RAND_bytes()" + ], + "entry_point": "csprng_engine::operator()(void* ptr, std::size_t count)", + "purpose": "Fills a buffer with cryptographically secure random bytes.", + "validation_points": [ + "csprng_engine::operator()(void* ptr, std::size_t count) - Validates entropy availability via RAND_bytes() result" + ] + }, + { + "call_chain": [ + "csprng_engine::mix_entropy(void* buffer, std::size_t count)", + "RAND_add(entropy.data(), ...)", + "if (buffer != nullptr && count != 0) RAND_add(buffer, count, 0)" + ], + "entry_point": "csprng_engine::mix_entropy(void* buffer, std::size_t count)", + "purpose": "Mixes additional entropy into the OpenSSL PRNG pool.", + "validation_points": [ + "csprng_engine::mix_entropy(void* buffer, std::size_t count) - Validates buffer pointer and count before using RAND_add" + ] + }, + { + "call_chain": [ + "csprng_engine::operator()()", + "csprng_engine::operator()(void* ptr, std::size_t count)", + "RAND_bytes()" + ], + "entry_point": "csprng_engine::operator()()", + "purpose": "Returns a single random value of result_type.", + "validation_points": [ + "csprng_engine::operator()(void* ptr, std::size_t count) - Validates entropy availability via RAND_bytes() result" + ] + } + ], + "data_flows": [ + { + "field": "buffer (void* buffer, std::size_t count)", + "flow": [ + "Input to csprng_engine::mix_entropy", + "Explicit check: if (buffer != nullptr && count != 0)", + "RAND_add(buffer, count, 0)" + ], + "origin": "Input parameter to csprng_engine::mix_entropy", + "transformations": [ + "Checked for null and nonzero size", + "Passed directly to RAND_add (no transformation)" + ], + "validated_at": "csprng_engine::mix_entropy - explicit check before RAND_add" + }, + { + "field": "ptr (void* ptr, std::size_t count)", + "flow": [ + "Input to csprng_engine::operator()(void* ptr, std::size_t count)", + "Passed to RAND_bytes", + "RAND_bytes fills ptr with random data" + ], + "origin": "Input parameter to csprng_engine::operator()(void* ptr, std::size_t count)", + "transformations": [ + "Casted to unsigned char* for RAND_bytes", + "Filled with random bytes" + ], + "validated_at": "csprng_engine::operator()(void* ptr, std::size_t count) - RAND_bytes return value checked" + }, + { + "field": "entropy (std::array entropy)", + "flow": [ + "Filled by std::random_device in csprng_engine::mix_entropy", + "Passed to RAND_add" + ], + "origin": "Local variable in csprng_engine::mix_entropy", + "transformations": [ + "Filled with random_device output", + "Added to OpenSSL entropy pool (RAND_add)" + ], + "validated_at": "No explicit validation; assumed good entropy from std::random_device" + }, + { + "field": "OpenSSL entropy pool", + "flow": [ + "RAND_poll called in csprng_engine::csprng_engine", + "RAND_add called in csprng_engine::mix_entropy", + "RAND_bytes called in csprng_engine::operator()" + ], + "origin": "Initialized in csprng_engine::csprng_engine via RAND_poll", + "transformations": [ + "Initialized, mixed with entropy, used to generate random bytes" + ], + "validated_at": "RAND_poll result checked in constructor; RAND_bytes result checked in operator()" + } + ], + "description": "Implements a cryptographically secure pseudorandom number generator (CSPRNG) engine using OpenSSL, providing entropy mixing and random byte generation for the XRPL project.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "OpenSSL entropy pool initialization", + "empty", + "string", + "validation" + ], + "evidence": "RAND_poll() at csprng_engine constructor", + "issue_pattern": "Missing empty string validation for OpenSSL entropy pool initialization", + "why_false_positive": "RAND_poll() validates OpenSSL entropy pool initialization for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "RAND_bytes result (entropy availability)", + "empty", + "string", + "validation" + ], + "evidence": "RAND_bytes() at csprng_engine::operator()(void*, std::size_t)", + "issue_pattern": "Missing empty string validation for RAND_bytes result (entropy availability)", + "why_false_positive": "RAND_bytes() validates RAND_bytes result (entropy availability) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "buffer pointer and count", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (buffer != nullptr && count != 0)) at csprng_engine::mix_entropy", + "issue_pattern": "Missing empty string validation for buffer pointer and count", + "why_false_positive": "explicit check (if (buffer != nullptr && count != 0)) validates buffer pointer and count for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "buffer pointer and count", + "type", + "validation", + "check" + ], + "evidence": "explicit check (if (buffer != nullptr && count != 0)) at csprng_engine::mix_entropy", + "issue_pattern": "Missing type validation for buffer pointer and count", + "why_false_positive": "explicit check (if (buffer != nullptr && count != 0)) validates buffer pointer and count type" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/crypto/csprng.cpp", + "functions": [ + { + "args": [], + "lineno": 9, + "name": "csprng_engine::csprng_engine" + }, + { + "args": [], + "lineno": 16, + "name": "csprng_engine::~csprng_engine" + }, + { + "args": [ + "buffer", + "count" + ], + "lineno": 23, + "name": "csprng_engine::mix_entropy" + }, + { + "args": [ + "ptr", + "count" + ], + "lineno": 43, + "name": "csprng_engine::operator()" + }, + { + "args": [], + "lineno": 56, + "name": "csprng_engine::operator()" + }, + { + "args": [], + "lineno": 63, + "name": "crypto_prng" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage (no test code or test macros). Typical test coverage would be in separate test files, likely under a test/ or unit_test/ directory, possibly named csprng_test.cpp or similar. Tests should cover: (1) successful random byte generation, (2) error handling when entropy is insufficient (hard to simulate), (3) mix_entropy with valid and invalid buffers. Gaps: Error paths (RAND_poll or RAND_bytes failure) are difficult to trigger in normal environments and may not be covered by standard tests. No explicit fuzzing or boundary tests are shown here.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, OpenSSL C API)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "OpenSSL entropy pool initialization", + "location": "csprng_engine constructor", + "validated_by": "RAND_poll()", + "validates": [ + "Checks if OpenSSL's entropy pool is successfully initialized", + "RAND_poll() must return 1 for success" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "RAND_bytes result (entropy availability)", + "location": "csprng_engine::operator()(void*, std::size_t)", + "validated_by": "RAND_bytes()", + "validates": [ + "Checks if OpenSSL's CSPRNG can provide the requested random bytes", + "RAND_bytes() must return 1 for success" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "buffer pointer and count", + "location": "csprng_engine::mix_entropy", + "validated_by": "explicit check (if (buffer != nullptr && count != 0))", + "validates": [ + "Ensures buffer is not null and count is not zero before passing to RAND_add" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/crypto/csprng.cpp.ai.md b/src/libxrpl/crypto/csprng.cpp.ai.md new file mode 100644 index 0000000000..a52d50e9bf --- /dev/null +++ b/src/libxrpl/crypto/csprng.cpp.ai.md @@ -0,0 +1,45 @@ +# `src/libxrpl/crypto/csprng.cpp` + +## Role in the System + +This file implements the XRPL project's cryptographically secure pseudorandom number generator. It wraps OpenSSL's `RAND_bytes` family behind a C++ class (`csprng_engine`) that satisfies the C++ standard `UniformRandomNumberEngine` named requirement, and exposes a global singleton through `crypto_prng()`. Nearly every security-sensitive operation in the codebase — key generation, seed creation, nonce production — flows through this single engine rather than scattering ad-hoc calls to OpenSSL directly. + +## `csprng_engine` Design + +The class is non-copyable and non-movable by explicit `= delete` on all copy/move operations. This is intentional: the engine holds a `std::mutex` member (`mutex_`) and owns a logical relationship with OpenSSL's internal entropy pool. Copying or moving it would produce dangerous aliasing with no meaningful semantics. + +The constructor calls `RAND_poll()`, an OpenSSL function that asks the OS for fresh entropy (e.g., `/dev/urandom`, `getrandom()`, `CryptGenRandom`). While technically optional — OpenSSL will seed itself lazily on first use — the explicit call at construction time surfaces any platform-level entropy failure immediately at startup rather than silently at the first key generation. A failed `RAND_poll()` throws `std::runtime_error` via `Throw<>`, the project-wide helper from `contract.h` that logs a call stack before rethrowing. + +The destructor conditionally calls `RAND_cleanup()`, but only when compiled against OpenSSL older than 1.1.0 (`OPENSSL_VERSION_NUMBER < 0x10100000L`). OpenSSL 1.1.0 deprecated this function because its cleanup is now handled automatically, and calling it explicitly on newer builds would be incorrect. + +## Entropy Mixing + +`mix_entropy()` exists to periodically stir additional randomness into the OpenSSL pool. It does two things: first, it allocates a stack array of 128 `std::random_device::result_type` values, fills them from `std::random_device`, and then feeds them to OpenSSL via `RAND_add`. Second, if the caller passes a non-null buffer, that data is also fed to `RAND_add`. + +The notable detail is the third argument to every `RAND_add` call: `0`. This is the entropy estimate — the caller is telling OpenSSL "I'm giving you data but I'm not vouching for how many unpredictable bits it actually contains." This conservative stance avoids overestimating entropy quality, which could prematurely satisfy OpenSSL's seeding requirements and weaken the pool. In practice, `std::random_device` is non-deterministic on all supported XRPL platforms, but the code deliberately declines to rely on that guarantee. + +`mix_entropy()` is called on a timer in `Application.cpp` — the production application stirs in fresh system entropy at regular intervals during the node's lifetime, not just at startup. + +The mutex is acquired for the `RAND_add` calls but not for the `std::random_device` reads, which are done before locking. This is correct because `std::random_device` is independently thread-safe and there is no invariant tying the OS reads to the OpenSSL pool state. + +## Thread Safety and the Version Guard + +The bulk byte-generation operator `operator()(void* ptr, std::size_t count)` wraps `RAND_bytes()`, which fills a caller-supplied buffer with cryptographically secure random bytes. The thread-safety strategy here involves a compile-time branch: + +```cpp +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || !defined(OPENSSL_THREADS) + std::lock_guard lock(mutex_); +#endif +``` + +OpenSSL 1.1.0 made `RAND_bytes` internally thread-safe when built with thread support, making an external mutex redundant and costly. The guard retains safety on older OpenSSL or in single-threaded builds where `OPENSSL_THREADS` is absent. Rather than always paying the locking cost, the code uses preprocessor dispatch to let the compiler eliminate dead paths entirely. + +The scalar `operator()()` — satisfying the `UniformRandomNumberEngine` `operator()` requirement that returns a single `result_type` — simply delegates to the buffer-filling overload with `sizeof(result_type)` (8 bytes, since `result_type` is `std::uint64_t`). This keeps the implementation DRY and ensures both overloads go through the same validation and error-handling path. + +## Singleton Accessor + +`crypto_prng()` returns a reference to a function-local `static csprng_engine`. The C++11 standard guarantees that function-local statics are initialized exactly once even in the presence of concurrent callers, making this a thread-safe Meyers singleton with no need for an explicit `std::once_flag` or double-checked locking. The singleton design also means all callers share a single OpenSSL entropy pool state, which is correct — multiple independent `csprng_engine` instances would each manage their own view of OpenSSL's global state while OpenSSL itself has only one, leading to unnecessary locking and confusion. + +## Error Handling + +Both failure points that can be detected — `RAND_poll()` returning non-1 in the constructor, and `RAND_bytes()` returning non-1 in the generation operator — throw `std::runtime_error` through `Throw<>`. Using `Throw<>` rather than a bare `throw` ensures the failure is logged with a stack trace before propagation, aiding post-mortem diagnostics. There is no silent fallback or retry: an insufficient-entropy condition is treated as unrecoverable because silently returning weak or repeated random data in a cryptographic context would be far more dangerous than crashing. \ No newline at end of file diff --git a/src/libxrpl/crypto/secure_erase.cpp.ai.json b/src/libxrpl/crypto/secure_erase.cpp.ai.json new file mode 100644 index 0000000000..65c1bd9c15 --- /dev/null +++ b/src/libxrpl/crypto/secure_erase.cpp.ai.json @@ -0,0 +1,86 @@ +{ + "args": [ + { + "lineno": 8, + "name": "dest" + }, + { + "lineno": 8, + "name": "bytes" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "xrpl::secure_erase", + "OPENSSL_cleanse" + ], + "entry_point": "xrpl::secure_erase", + "purpose": "Securely erases a memory region by overwriting it, typically used to clear sensitive data (e.g., cryptographic keys) from memory.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "dest (void*)", + "flow": [ + "Caller provides pointer", + "xrpl::secure_erase receives pointer", + "OPENSSL_cleanse erases memory at pointer" + ], + "origin": "Caller of xrpl::secure_erase (external code, likely cryptographic routines or memory management code)", + "transformations": [ + "No transformation; pointer is passed directly to OPENSSL_cleanse" + ], + "validated_at": "No explicit validation in this function" + }, + { + "field": "bytes (std::size_t)", + "flow": [ + "Caller provides size", + "xrpl::secure_erase receives size", + "OPENSSL_cleanse uses size to determine how many bytes to erase" + ], + "origin": "Caller of xrpl::secure_erase", + "transformations": [ + "No transformation; size is passed directly to OPENSSL_cleanse" + ], + "validated_at": "No explicit validation in this function" + } + ], + "description": "Provides a secure memory erasure function using OpenSSL's OPENSSL_cleanse to securely erase sensitive data from memory.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/crypto/secure_erase.cpp", + "functions": [ + { + "args": [ + "dest", + "bytes" + ], + "lineno": 7, + "name": "secure_erase" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no validation logic in this function (no null checks, bounds checks, etc.), so there are no validation code paths to test. Test coverage would depend on higher-level code that calls secure_erase. Typical tests would be in cryptographic key management or memory handling modules, ensuring that sensitive data is erased, but this function itself is a thin wrapper and likely not directly unit tested. Gaps: No direct tests for invalid pointers or zero-length erasure; relies on callers to provide valid arguments.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/crypto/secure_erase.cpp.ai.md b/src/libxrpl/crypto/secure_erase.cpp.ai.md new file mode 100644 index 0000000000..cd24d0ffa8 --- /dev/null +++ b/src/libxrpl/crypto/secure_erase.cpp.ai.md @@ -0,0 +1,35 @@ +# `src/libxrpl/crypto/secure_erase.cpp` + +## Role and Purpose + +This file provides the single-function implementation of `xrpl::secure_erase`, the codebase's canonical mechanism for wiping sensitive cryptographic material from memory. Its entire body is a one-line delegation to OpenSSL's `OPENSSL_cleanse`. The smallness is intentional: the hard problem being solved is not algorithmic but rather a battle against the C++ optimizer. + +## The Compiler-Optimization Problem + +A naive `memset(buf, 0, size)` on a buffer that is never read afterward will be silently removed by any optimizing compiler — the write is provably dead from the compiler's perspective, so it eliminates it as a no-op. The consequence in cryptographic code is that secret key material, seed bytes, and intermediate derivation buffers survive in process memory long after the developer intended them to be gone. They become exploitable via heap inspection, core dumps, swap files, cold-boot attacks, or speculative-execution side-channels. + +`OPENSSL_cleanse` is specifically designed to defeat this class of optimization. Its implementation uses techniques such as a volatile write or a call through a function pointer that the optimizer cannot inline away, ensuring the zeroing survives to the final generated machine code. The header `secure_erase.h` cites Colin Percival's pair of blog posts (2014) for the full theoretical background and notes that even this best-effort approach cannot guarantee data has not leaked into CPU registers or caches — it only prevents the memory itself from being left dirty. + +## Design Decision: Delegating to OpenSSL + +The XRPL codebase already takes a hard dependency on OpenSSL for ECDSA, SHA-512, and CSPRNG operations via `csprng.cpp`. Delegating `secure_erase` to `OPENSSL_cleanse` rather than rolling a platform-specific solution (`memset_s` on C11, `explicit_bzero` on BSDs, a `SecureZeroMemory` import on Windows) keeps portability logic in one place — the OpenSSL build system — while ensuring the semantics are correct across all supported platforms. + +## Usage Pattern in Sensitive Types + +The callers seen in `SecretKey.cpp` and `Seed.cpp` reveal a consistent RAII cleanup discipline: + +- **Destructor scrubbing**: `SecretKey::~SecretKey()` and `Seed::~Seed()` each call `secure_erase` on their internal fixed-size byte buffers as the very first and only action. This guarantees that every code path out of the object's lifetime — including exception unwind — wipes the raw key bytes. +- **Intermediate buffer scrubbing**: Ephemeral key material created during derivation (e.g., the `buf` array in `randomSecretKey()`, the SHA-512 half-digest in `derivePrivateKey()`, and the `rpk` scratch buffer in the secp256k1 generator loop) is erased immediately after the derived `SecretKey` object has taken ownership of a copy. The pattern is always: construct the permanent holder, then `secure_erase` the source buffer while it is still in scope. + +This usage makes `secure_erase` the final line of defense ensuring that raw secret key entropy never persists on the heap or stack beyond its minimum necessary lifetime, regardless of how many copies or transformations the key material undergoes on the way to its final form. + +## Interface + +```cpp +// include/xrpl/crypto/secure_erase.h +namespace xrpl { +void secure_erase(void* dest, std::size_t bytes); +} +``` + +The function accepts a raw `void*` and a byte count with no null-pointer or zero-length guards. Callers are expected to provide valid arguments; the function is a low-level primitive, not a safe wrapper. Passing a null pointer or zero bytes delegates directly to `OPENSSL_cleanse`, whose own behavior in those edge cases is implementation-defined but generally harmless. \ No newline at end of file diff --git a/src/libxrpl/git/Git.cpp.ai.json b/src/libxrpl/git/Git.cpp.ai.json new file mode 100644 index 0000000000..84bb459ed1 --- /dev/null +++ b/src/libxrpl/git/Git.cpp.ai.json @@ -0,0 +1,194 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Preprocessor", + "#error directive (GIT_COMMIT_HASH)", + "#error directive (GIT_BUILD_BRANCH)" + ], + "entry_point": "Compilation/Build", + "purpose": "Ensures that required Git metadata macros are defined at build time.", + "validation_points": [ + "#error \"GIT_COMMIT_HASH must be defined\"", + "#error \"GIT_BUILD_BRANCH must be defined\"" + ] + }, + { + "call_chain": [ + "getCommitHash()" + ], + "entry_point": "xrpl::git::getCommitHash()", + "purpose": "Returns the Git commit hash as a string.", + "validation_points": [] + }, + { + "call_chain": [ + "getBuildBranch()" + ], + "entry_point": "xrpl::git::getBuildBranch()", + "purpose": "Returns the Git build branch as a string.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "GIT_COMMIT_HASH", + "flow": [ + "Build system defines GIT_COMMIT_HASH", + "Preprocessor checks for definition (#ifndef/#error)", + "static constexpr char kGIT_COMMIT_HASH[] = GIT_COMMIT_HASH", + "getCommitHash() returns static std::string const kVALUE = kGIT_COMMIT_HASH" + ], + "origin": "Preprocessor macro (typically defined by build system, e.g., CMake or Makefile)", + "transformations": [ + "Macro value is assigned to a constexpr char array", + "Char array is used to initialize a static std::string" + ], + "validated_at": "#error directive at compile time" + }, + { + "field": "GIT_BUILD_BRANCH", + "flow": [ + "Build system defines GIT_BUILD_BRANCH", + "Preprocessor checks for definition (#ifndef/#error)", + "static constexpr char kGIT_BUILD_BRANCH[] = GIT_BUILD_BRANCH", + "getBuildBranch() returns static std::string const kVALUE = kGIT_BUILD_BRANCH" + ], + "origin": "Preprocessor macro (typically defined by build system, e.g., CMake or Makefile)", + "transformations": [ + "Macro value is assigned to a constexpr char array", + "Char array is used to initialize a static std::string" + ], + "validated_at": "#error directive at compile time" + } + ], + "description": "Provides functions to retrieve the current Git commit hash and build branch for the xrpl project, using compile-time macros.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "GIT_COMMIT_HASH", + "validation", + "missing", + "check" + ], + "evidence": "Field GIT_COMMIT_HASH validated by C++ preprocessor", + "issue_pattern": "Missing validation for GIT_COMMIT_HASH", + "why_false_positive": "C++ preprocessor validates GIT_COMMIT_HASH automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "GIT_BUILD_BRANCH", + "validation", + "missing", + "check" + ], + "evidence": "Field GIT_BUILD_BRANCH validated by C++ preprocessor", + "issue_pattern": "Missing validation for GIT_BUILD_BRANCH", + "why_false_positive": "C++ preprocessor validates GIT_BUILD_BRANCH automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "GIT_COMMIT_HASH", + "empty", + "string", + "validation" + ], + "evidence": "#error preprocessor directive at global scope (top of file, before namespace)", + "issue_pattern": "Missing empty string validation for GIT_COMMIT_HASH", + "why_false_positive": "#error preprocessor directive validates GIT_COMMIT_HASH for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "GIT_BUILD_BRANCH", + "empty", + "string", + "validation" + ], + "evidence": "#error preprocessor directive at global scope (top of file, before namespace)", + "issue_pattern": "Missing empty string validation for GIT_BUILD_BRANCH", + "why_false_positive": "#error preprocessor directive validates GIT_BUILD_BRANCH for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/git/Git.cpp", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "getCommitHash" + }, + { + "args": [], + "lineno": 21, + "name": "getBuildBranch" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl::git" + } + ], + "test_coverage_notes": "There is no runtime validation logic; all validation occurs at compile time via preprocessor directives. As such, there are likely no unit or integration tests directly covering the validation logic in this file. Tests for getCommitHash() and getBuildBranch() would only verify that the returned values match the macros defined at build time, but would not test the validation paths (i.e., missing macro definitions). Test coverage gaps: No automated tests can verify the #error validation paths, as these are enforced by the compiler and would cause build failures if triggered. Any test files would likely be in the form of build system tests or CI scripts that ensure the macros are always defined.", + "validation_architecture": { + "auto_validated_fields": [ + "GIT_COMMIT_HASH", + "GIT_BUILD_BRANCH" + ], + "framework": "C++ preprocessor", + "validation_layer": "entry_point (compile-time validation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "preprocessor error (compilation fails)", + "field": "GIT_COMMIT_HASH", + "location": "global scope (top of file, before namespace)", + "validated_by": "#error preprocessor directive", + "validates": [ + "checks that GIT_COMMIT_HASH macro is defined at compile time" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "preprocessor error (compilation fails)", + "field": "GIT_BUILD_BRANCH", + "location": "global scope (top of file, before namespace)", + "validated_by": "#error preprocessor directive", + "validates": [ + "checks that GIT_BUILD_BRANCH macro is defined at compile time" + ], + "validation_type": "presence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/git/Git.cpp.ai.md b/src/libxrpl/git/Git.cpp.ai.md new file mode 100644 index 0000000000..9f2d56641f --- /dev/null +++ b/src/libxrpl/git/Git.cpp.ai.md @@ -0,0 +1,21 @@ +# `src/libxrpl/git/Git.cpp` — Build-time Git Metadata Provider + +This file solves a narrow but important problem: making the exact source identity of a running `rippled` binary inspectable at runtime. It provides two accessor functions that return the Git commit hash and branch name baked into the binary at compile time, enabling diagnostic output, version strings, and log messages to carry precise provenance information. + +## Compile-time Injection via Preprocessor Macros + +The entire mechanism relies on two preprocessor macros, `GIT_COMMIT_HASH` and `GIT_BUILD_BRANCH`, being defined before the translation unit is compiled. The file enforces this contract hard — both macros are guarded by `#ifndef`/`#error` directives, so any attempt to compile without them is an immediate build failure rather than a silent empty string. This is the right design choice: a build that silently omits version information is worse than one that refuses to compile, because silently empty version strings would make deployed binaries indistinguishable from each other in production logs. + +The macros are populated by `cmake/GitInfo.cmake`, which is included early in the build via `cmake/XrplCore.cmake`. That CMake script shells out to `git rev-parse HEAD` for the commit hash and `git rev-parse --abbrev-ref HEAD` for the branch, then passes the resulting strings to the compiler as `target_compile_definitions` on the `xrpl.libxrpl.git` module. If Git is not found on the build host, CMake emits a warning and leaves both variables as empty strings — which will still satisfy the preprocessor guard (the macros are defined, just empty), though the resulting version information will be uninformative. + +## Static Local String Pattern + +Each accessor function — `getCommitHash()` and `getBuildBranch()` — follows the same two-step initialization pattern. First, the macro value is captured into a `static constexpr char[]` array at namespace scope; then inside each function, a `static const std::string` is initialized from that array on first call and returned by `const&` on every subsequent call. + +The intermediate `constexpr` char array (`kGIT_COMMIT_HASH`, `kGIT_BUILD_BRANCH`) is not strictly required — the macro could be used directly in the function — but it serves as a named, typed binding that makes the value inspectable in a debugger and removes the raw macro token from the function body. The function-local `static std::string` is the canonical C++ idiom for a lazily initialized singleton: it is thread-safe since C++11 (the standard guarantees exactly-once initialization for function-local statics under concurrent access), and it avoids the static-initialization-order-fiasco that would arise from a global `std::string`. Returning by `const&` to the static instance avoids copying and lets callers hold a stable reference. + +## Usage in the Broader System + +`BuildInfo.cpp` calls `xrpl::git::getCommitHash()` when constructing the version metadata string appended to the version number in debug and sanitizer builds. This surfaces in the `server_info` RPC response and in startup log messages, allowing operators and developers to match a running node's behavior directly to a specific commit. The `getBuildBranch()` function provides complementary context — knowing the branch name distinguishes release builds from development or feature-branch builds even when two commits have similar hashes. + +The `xrpl::git` namespace cleanly separates this low-level build metadata from higher-level version policy in `BuildInfo`, reflecting a correct layering: `BuildInfo` owns the semantics of the version string, while `Git.cpp` owns only the raw source-control facts. \ No newline at end of file diff --git a/src/libxrpl/json/JsonPropertyStream.cpp.ai.json b/src/libxrpl/json/JsonPropertyStream.cpp.ai.json new file mode 100644 index 0000000000..f686d7ff83 --- /dev/null +++ b/src/libxrpl/json/JsonPropertyStream.cpp.ai.json @@ -0,0 +1,403 @@ +{ + "args": [ + { + "lineno": 24, + "name": "key" + }, + { + "lineno": 33, + "name": "key" + }, + { + "lineno": 33, + "name": "v" + }, + { + "lineno": 37, + "name": "key" + }, + { + "lineno": 37, + "name": "v" + }, + { + "lineno": 41, + "name": "key" + }, + { + "lineno": 41, + "name": "v" + }, + { + "lineno": 45, + "name": "key" + }, + { + "lineno": 45, + "name": "v" + }, + { + "lineno": 49, + "name": "key" + }, + { + "lineno": 49, + "name": "v" + }, + { + "lineno": 53, + "name": "key" + }, + { + "lineno": 53, + "name": "v" + }, + { + "lineno": 57, + "name": "key" + }, + { + "lineno": 57, + "name": "v" + }, + { + "lineno": 61, + "name": "key" + }, + { + "lineno": 61, + "name": "v" + }, + { + "lineno": 71, + "name": "key" + }, + { + "lineno": 80, + "name": "v" + }, + { + "lineno": 84, + "name": "v" + }, + { + "lineno": 88, + "name": "v" + }, + { + "lineno": 92, + "name": "v" + }, + { + "lineno": 96, + "name": "v" + }, + { + "lineno": 100, + "name": "v" + }, + { + "lineno": 104, + "name": "v" + }, + { + "lineno": 108, + "name": "v" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "JsonPropertyStream::add(std::string const& key, T v)", + "(*m_stack.back())[key] = v" + ], + "entry_point": "JsonPropertyStream::add(std::string const& key, T v)", + "purpose": "Adds a key-value pair to the current JSON object on the stack.", + "validation_points": [] + }, + { + "call_chain": [ + "JsonPropertyStream::add(T v)", + "m_stack.back()->append(v)" + ], + "entry_point": "JsonPropertyStream::add(T v)", + "purpose": "Appends a value to the current JSON array on the stack.", + "validation_points": [] + }, + { + "call_chain": [ + "JsonPropertyStream::map_begin() or map_begin(key)", + "Json::Value& top = *m_stack.back()", + "Json::Value& map = ...", + "m_stack.push_back(&map)" + ], + "entry_point": "JsonPropertyStream::map_begin() / map_begin(key)", + "purpose": "Begins a new JSON object (map) in the current context (array or object).", + "validation_points": [] + }, + { + "call_chain": [ + "JsonPropertyStream::array_begin() or array_begin(key)", + "Json::Value& top = *m_stack.back()", + "Json::Value& vec = ...", + "m_stack.push_back(&vec)" + ], + "entry_point": "JsonPropertyStream::array_begin() / array_begin(key)", + "purpose": "Begins a new JSON array in the current context (array or object).", + "validation_points": [] + }, + { + "call_chain": [ + "JsonPropertyStream::map_end() or array_end()", + "m_stack.pop_back()" + ], + "entry_point": "JsonPropertyStream::map_end() / array_end()", + "purpose": "Ends the current JSON object or array context.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "m_stack", + "flow": [ + "constructor: m_stack.push_back(&m_top)", + "map_begin/array_begin: m_stack.push_back(&new_value)", + "map_end/array_end: m_stack.pop_back()" + ], + "origin": "Initialized in JsonPropertyStream constructor with m_top (a Json::Value objectValue)", + "transformations": [ + "Stack of pointers to current JSON context (object or array) is maintained as the structure is built." + ], + "validated_at": "No explicit validation; assumes correct usage order by caller." + }, + { + "field": "key (in add(key, v) and map_begin(key)/array_begin(key))", + "flow": [ + "add(key, v): (*m_stack.back())[key] = v", + "map_begin(key): top[key] = Json::objectValue", + "array_begin(key): top[key] = Json::arrayValue" + ], + "origin": "Passed as argument by caller", + "transformations": [ + "Key is used directly as a JSON object key." + ], + "validated_at": "No validation of key (e.g., for emptiness, duplicates, or invalid characters)." + }, + { + "field": "v (value in add)", + "flow": [ + "add(key, v): assigned to JSON object at key", + "add(v): appended to JSON array" + ], + "origin": "Passed as argument by caller", + "transformations": [ + "C++ type is converted to appropriate Json::Value type (int, float, string, etc.)" + ], + "validated_at": "No validation of value (e.g., range, type safety, or null checks)." + }, + { + "field": "m_top", + "flow": [ + "Acts as root JSON object", + "Returned by top()", + "Built up via stack operations" + ], + "origin": "Constructed as Json::objectValue in constructor", + "transformations": [ + "Populated via stack operations as JSON structure is built." + ], + "validated_at": "No validation; assumes correct stack discipline." + } + ], + "description": "Implements the JsonPropertyStream class for building and manipulating JSON objects and arrays using a stack-based approach within the xrpl namespace.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/json/JsonPropertyStream.cpp", + "functions": [ + { + "args": [], + "lineno": 7, + "name": "JsonPropertyStream::JsonPropertyStream" + }, + { + "args": [], + "lineno": 13, + "name": "JsonPropertyStream::top" + }, + { + "args": [], + "lineno": 18, + "name": "JsonPropertyStream::map_begin" + }, + { + "args": [ + "key" + ], + "lineno": 24, + "name": "JsonPropertyStream::map_begin" + }, + { + "args": [], + "lineno": 30, + "name": "JsonPropertyStream::map_end" + }, + { + "args": [ + "key", + "short v" + ], + "lineno": 33, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "key", + "unsigned short v" + ], + "lineno": 37, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "key", + "int v" + ], + "lineno": 41, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "key", + "unsigned int v" + ], + "lineno": 45, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "key", + "long v" + ], + "lineno": 49, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "key", + "float v" + ], + "lineno": 53, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "key", + "double v" + ], + "lineno": 57, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "key", + "std::string const& v" + ], + "lineno": 61, + "name": "JsonPropertyStream::add" + }, + { + "args": [], + "lineno": 65, + "name": "JsonPropertyStream::array_begin" + }, + { + "args": [ + "key" + ], + "lineno": 71, + "name": "JsonPropertyStream::array_begin" + }, + { + "args": [], + "lineno": 77, + "name": "JsonPropertyStream::array_end" + }, + { + "args": [ + "short v" + ], + "lineno": 80, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "unsigned short v" + ], + "lineno": 84, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "int v" + ], + "lineno": 88, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "unsigned int v" + ], + "lineno": 92, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "long v" + ], + "lineno": 96, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "float v" + ], + "lineno": 100, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "double v" + ], + "lineno": 104, + "name": "JsonPropertyStream::add" + }, + { + "args": [ + "std::string const& v" + ], + "lineno": 108, + "name": "JsonPropertyStream::add" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file contains only the implementation of a JSON-building utility. There is no validation logic (e.g., input checking, error handling) in this code. All functions assume correct usage by the caller (e.g., correct stack discipline, valid keys, and values). Test coverage would likely exist in higher-level code that uses JsonPropertyStream, not here. Typical tests would be in files like test/json/JsonPropertyStream_test.cpp or similar, but unless those tests explicitly check for misuse (e.g., popping an empty stack, invalid key usage), validation edge cases are not covered. There is no evidence of defensive programming or validation in this code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/json/JsonPropertyStream.cpp.ai.md b/src/libxrpl/json/JsonPropertyStream.cpp.ai.md new file mode 100644 index 0000000000..4ea1c6d77d --- /dev/null +++ b/src/libxrpl/json/JsonPropertyStream.cpp.ai.md @@ -0,0 +1,45 @@ +# `JsonPropertyStream.cpp` — JSON Sink for the PropertyStream Framework + +## Role and Context + +`JsonPropertyStream` is a concrete implementation of `beast::PropertyStream`, the abstract base class that models a write-once property tree. The `PropertyStream` hierarchy exists to let subsystems expose diagnostic state without coupling them to any particular output format — a `PropertyStream::Source` subclass just calls `map_begin`, `add`, `map_end`, etc. on whatever stream it receives, and the stream decides how to materialise the output. `JsonPropertyStream` is the XRPL node's production sink: it converts those calls into a `Json::Value` tree of type `objectValue`, suitable for returning directly as an RPC response. + +The sole real consumer is `doPrint()` in `src/xrpld/rpc/handlers/admin/status/Print.cpp`, which is the handler for the admin `print` command. It constructs a `JsonPropertyStream`, calls `context.app.write(stream, ...)` to traverse the application's entire `Source` hierarchy, then returns `stream.top()` — the completed JSON tree — to the caller. This makes `JsonPropertyStream` the bridge between the internal diagnostics graph and the JSON-RPC layer. + +## Stack-Based Construction + +The class maintains two member variables: `m_top`, a root `Json::Value` of type `objectValue`, and `m_stack`, a `vector`. The stack always holds raw pointers into the `Json::Value` tree rooted at `m_top`. Because `m_top` owns the entire tree (Json::Value uses value semantics and copies/moves children as the tree grows), pointer stability is ensured — appending a child via `top.append(...)` or `top[key] = ...` returns a reference to the child living inside `m_top`, and those child addresses remain stable for the lifetime of `m_top`. The stack thus tracks the "current insertion point" at any moment during the write traversal. + +The constructor pushes `&m_top` as the initial stack entry and pre-reserves 64 slots with `m_stack.reserve(64)`. The reservation avoids reallocation during a deep traversal; if the diagnostic tree is fewer than 64 levels deep — which it always is in practice — there will be no heap allocation after construction. + +## Map and Array Lifecycle + +`map_begin()` and `array_begin()` each come in two overloads that reflect the two contexts a writer can be in: + +- **Inside a map (keyed child):** `map_begin(key)` writes `top[key] = Json::objectValue` and pushes a pointer to that new child. `array_begin(key)` does the same with `arrayValue`. +- **Inside an array (anonymous child):** `map_begin()` calls `top.append(Json::objectValue)` and pushes a pointer to the appended element. `array_begin()` similarly appends an array. + +The comments in the source make the precondition explicit (`// top is array`, `// top is a map`), but there is no runtime enforcement. Misuse — calling the key-less variant when the current context is a map, or the keyed variant inside an array — would silently produce malformed JSON. The `beast::PropertyStream` base class's RAII wrappers (`Map` and `Set`) are what enforce correct pairing: they call `map_begin`/`map_end` and `array_begin`/`array_end` symmetrically in their constructors and destructors, so misuse is prevented structurally at the call sites rather than defensively inside this class. + +`map_end()` and `array_end()` are symmetric: both simply call `m_stack.pop_back()`, restoring the insertion point to the parent context. There is no type check on pop — the stack encodes context solely through the nesting depth, not through tagged pointers. + +## The `add()` Overloads + +Scalar insertion splits into two families: keyed (`add(key, value)`) for use inside a map, and unkeyed (`add(value)`) for use inside an array. Each numeric C++ type gets its own explicit overload, matching the virtual signatures declared in `beast::PropertyStream`. Most overloads are straightforward assignments or appends into the current top-of-stack `Json::Value`. + +One notable exception is the `long` overload, in both keyed and unkeyed forms: + +```cpp +void JsonPropertyStream::add(std::string const& key, long v) +{ + (*m_stack.back())[key] = Json::Value(int(v)); +} +``` + +`long` is explicitly narrowed to `int` before being stored. This is deliberate: the underlying `Json::Value` library has no native 64-bit signed integer type in this codebase, so a `long` that is wider than 32 bits on a 64-bit platform will silently truncate. The decision accepts this limitation to keep JSON compatibility with the rest of the node's JSON handling. + +## Ownership and Safety + +`m_stack` holds non-owning raw pointers into `m_top`. This is safe as long as the stack is not used after `m_top` is destroyed — both are members of the same `JsonPropertyStream` object, so they share the same lifetime. No entries in `m_stack` are ever deleted individually; they are all implicitly invalidated when the `JsonPropertyStream` itself is destroyed. The class does not expose `m_stack` to external callers (it is not used after `top()` is called), so there is no practical risk. + +There is no synchronization. The class is designed for single-threaded use within a single write traversal and is not safe to write to from multiple threads. \ No newline at end of file diff --git a/src/libxrpl/json/Output.cpp.ai.json b/src/libxrpl/json/Output.cpp.ai.json new file mode 100644 index 0000000000..47c0407101 --- /dev/null +++ b/src/libxrpl/json/Output.cpp.ai.json @@ -0,0 +1,160 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "outputJson(Json::Value const& value, Output const& out)", + "Writer writer(out)", + "outputJson(Json::Value const& value, Writer& writer)", + "switch (value.type())" + ], + "entry_point": "outputJson(Json::Value const& value, Output const& out)", + "purpose": "Serializes a Json::Value to an Output sink, dispatching on the value's type.", + "validation_points": [ + "switch (value.type()) in outputJson(Json::Value const&, Writer&)" + ] + }, + { + "call_chain": [ + "jsonAsString(Json::Value const& value)", + "Writer writer(stringOutput(s))", + "outputJson(Json::Value const& value, Writer& writer)", + "switch (value.type())" + ], + "entry_point": "jsonAsString(Json::Value const& value)", + "purpose": "Serializes a Json::Value to a std::string, dispatching on the value's type.", + "validation_points": [ + "switch (value.type()) in outputJson(Json::Value const&, Writer&)" + ] + } + ], + "data_flows": [ + { + "field": "Json::Value value", + "flow": [ + "external caller", + "outputJson(Json::Value const&, Output const&)/jsonAsString(Json::Value const&)", + "outputJson(Json::Value const&, Writer&)", + "switch (value.type())", + "writer.output()/writer.startRoot()/writer.rawAppend()/writer.rawSet()/writer.finish()" + ], + "origin": "Passed as argument to outputJson or jsonAsString (external caller)", + "transformations": [ + "Type-checked via value.type()", + "Depending on type, converted to int, uint, double, string, bool, or iterated as array/object", + "Written to output via Writer" + ], + "validated_at": "switch (value.type()) in outputJson(Json::Value const&, Writer&)" + }, + { + "field": "Json::Value array/object elements", + "flow": [ + "parent Json::Value", + "for (auto const& i : value) / for (auto const& tag : members)", + "outputJson(i, writer) / outputJson(value[tag], writer)", + "switch (value.type()) (recursive)" + ], + "origin": "Contained within parent Json::Value passed to outputJson", + "transformations": [ + "Recursively type-checked and serialized" + ], + "validated_at": "Each recursive call to outputJson, at switch (value.type())" + } + ], + "description": "Provides functions to serialize a Json::Value to an output stream or string using a Writer, handling all JSON types recursively.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Json::Value (input value)", + "empty", + "string", + "validation" + ], + "evidence": "value.type() (enum type check) at outputJson(Json::Value const& value, Writer& writer)", + "issue_pattern": "Missing empty string validation for Json::Value (input value)", + "why_false_positive": "value.type() (enum type check) validates Json::Value (input value) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "Json::Value (input value)", + "type", + "validation", + "check" + ], + "evidence": "value.type() (enum type check) at outputJson(Json::Value const& value, Writer& writer)", + "issue_pattern": "Missing type validation for Json::Value (input value)", + "why_false_positive": "value.type() (enum type check) validates Json::Value (input value) type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/json/Output.cpp", + "functions": [ + { + "args": [ + "Json::Value const& value", + "Writer& writer" + ], + "lineno": 8, + "name": "outputJson" + }, + { + "args": [ + "Json::Value const& value", + "Output const& out" + ], + "lineno": 56, + "name": "outputJson" + }, + { + "args": [ + "Json::Value const& value" + ], + "lineno": 62, + "name": "jsonAsString" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "Json" + } + ], + "test_coverage_notes": "This file is a serialization utility; direct tests may exist in unit tests for JSON serialization or Writer. Validation is limited to type dispatch (value.type()), so tests should cover all Json::Value types (null, int, uint, real, string, bool, array, object). Gaps: No explicit error handling for unknown types; no validation of value contents beyond type. Test files likely: test/json/Json_test.cpp, test/json/Writer_test.cpp, or similar. If these do not exist, coverage may be lacking for edge cases (e.g., deeply nested structures, unusual types).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual type dispatch via switch on Json::Value::type())", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (no exception thrown, default switch covers all types)", + "field": "Json::Value (input value)", + "location": "outputJson(Json::Value const& value, Writer& writer)", + "validated_by": "value.type() (enum type check)", + "validates": [ + "Checks the type of Json::Value (null, int, uint, real, string, bool, array, object)", + "Dispatches to appropriate Writer::output overload based on type" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/json/Output.cpp.ai.md b/src/libxrpl/json/Output.cpp.ai.md new file mode 100644 index 0000000000..4e7a793fc8 --- /dev/null +++ b/src/libxrpl/json/Output.cpp.ai.md @@ -0,0 +1,33 @@ +# `src/libxrpl/json/Output.cpp` + +## Role in the System + +This file is the bridge between the XRPL codebase's in-memory JSON representation (`Json::Value`) and the streaming `Writer` serialization engine. It answers the question "given a fully-built `Json::Value` tree, how do I write it to an arbitrary output sink efficiently?" — without requiring callers to understand the `Writer` API directly. + +The file is intentionally small: it contains exactly one non-trivial function and one convenience wrapper. Its purpose is separation of concerns rather than algorithmic complexity. + +## The `Output` Type and Streaming Design + +`Output` (defined in `Output.h`) is a type alias for `std::function`. It is a sink callback that receives arbitrarily-sized string fragments as they are produced. This design means serialized JSON is never buffered in a single contiguous allocation — fragments are pushed to the consumer as they are generated. + +This matters in the XRPL context because ledger objects and transaction results can be large, deeply nested JSON structures. Streaming to a network buffer or an HTTP response body without building a full intermediate string reduces peak memory pressure on a server handling many concurrent RPC clients. + +## Internal Structure + +The file contains three functions, two of which share the name `outputJson`: + +The private, anonymous-namespace overload `outputJson(Json::Value const&, Writer&)` is the core recursive engine. It dispatches on `value.type()` across all eight `Json::ValueType` enum cases — `nullValue`, `intValue`, `uintValue`, `realValue`, `stringValue`, `booleanValue`, `arrayValue`, and `objectValue` — calling the appropriate typed `writer.output()` overload for scalar values. For `arrayValue` and `objectValue` it calls `writer.startRoot()` to open a collection, iterates the children, and calls `writer.finish()` to close it. + +For arrays, each element is preceded by `writer.rawAppend()` before the recursive call. This low-level method tells the `Writer` to emit a comma separator if the element is not first, without yet committing any value. The value is then written by the recursive `outputJson` call. For objects, `writer.rawSet(tag)` emits the quoted key and colon, and the recursive call emits the value. This two-step pattern (raw prefix + value) allows the recursive call to itself open sub-collections correctly — the `Writer`'s internal stack of open collections handles nesting transparently. + +The public `outputJson(Json::Value const&, Output const&)` constructs a `Writer` on the stack (bound to the provided `Output` sink), then delegates to the private overload. The `Writer` destructor calls `finishAll()`, which closes any remaining open collections — this ensures a well-formed JSON document even in exceptional paths, though in practice the recursive walk will always call `finish()` explicitly for every collection it opens. + +`jsonAsString(Json::Value const&)` is a convenience entry point. It allocates a `std::string`, wraps it with `stringOutput()` (an inline lambda in `Output.h` that appends each fragment via `s.append()`), and calls the same internal path. The `Output.h` header explicitly notes this function requires a full-size allocation and recommends `outputJson()` with a direct sink when memory is a concern. + +## Design Decisions + +The decision to keep the recursive worker in an anonymous namespace, while exposing only the `Output`-taking overload publicly, is a clean API contract: callers never need to instantiate a `Writer` themselves when serializing a pre-built `Json::Value`. Callers who want fine-grained streaming control (building JSON incrementally without a pre-built tree) use `Writer` directly. + +No error handling exists in the switch statement, and none is needed. `Json::ValueType` is a closed enumeration; `Json::Value::type()` always returns one of the eight defined values. Adding a default case would be defensive noise rather than meaningful protection. + +The `Writer` itself is O(1)-space (it maintains a fixed-depth stack of open collection states internally) and emits output in O(1)-granular chunks. The recursive nature of `outputJson` mirrors the recursive depth of the `Json::Value` tree, so the call stack grows with nesting depth — but this matches the natural shape of the data and is bounded by practical JSON structure limits in the XRPL protocol. \ No newline at end of file diff --git a/src/libxrpl/json/Writer.cpp.ai.json b/src/libxrpl/json/Writer.cpp.ai.json new file mode 100644 index 0000000000..dba12f1ef6 --- /dev/null +++ b/src/libxrpl/json/Writer.cpp.ai.json @@ -0,0 +1,488 @@ +{ + "args": [ + { + "lineno": 28, + "name": "s" + }, + { + "lineno": 58, + "name": "ct" + }, + { + "lineno": 63, + "name": "bytes" + }, + { + "lineno": 68, + "name": "bytes" + }, + { + "lineno": 93, + "name": "type" + }, + { + "lineno": 93, + "name": "message" + }, + { + "lineno": 105, + "name": "tag" + }, + { + "lineno": 41, + "name": "output" + }, + { + "lineno": 170, + "name": "s" + }, + { + "lineno": 174, + "name": "s" + }, + { + "lineno": 178, + "name": "value" + }, + { + "lineno": 183, + "name": "f" + }, + { + "lineno": 188, + "name": "f" + }, + { + "lineno": 193, + "name": "nullptr" + }, + { + "lineno": 197, + "name": "b" + }, + { + "lineno": 201, + "name": "s" + }, + { + "lineno": 214, + "name": "tag" + }, + { + "lineno": 220, + "name": "type" + }, + { + "lineno": 224, + "name": "type" + }, + { + "lineno": 229, + "name": "type" + }, + { + "lineno": 229, + "name": "key" + } + ], + "classes": [ + { + "args": [ + "Output const& output" + ], + "lineno": 41, + "name": "Writer::Impl" + } + ], + "code_paths": [ + { + "call_chain": [ + "output", + "markStarted", + "check(!isFinished())", + "output_" + ], + "entry_point": "Writer::Impl::output", + "purpose": "Outputs a chunk of JSON data, ensuring the writer is not finished before writing.", + "validation_points": [ + "markStarted (calls check(!isFinished()))" + ] + }, + { + "call_chain": [ + "stringOutput", + "markStarted", + "check(!isFinished())", + "output_" + ], + "entry_point": "Writer::Impl::stringOutput", + "purpose": "Outputs a JSON string, escaping special characters, ensuring the writer is not finished before writing.", + "validation_points": [ + "markStarted (calls check(!isFinished()))" + ] + }, + { + "call_chain": [ + "nextCollectionEntry", + "check(!empty())", + "stack_.top().type", + "check(type matches)", + "output_" + ], + "entry_point": "Writer::Impl::nextCollectionEntry", + "purpose": "Handles writing the next entry in a collection (array/object), validates stack is not empty and type matches.", + "validation_points": [ + "check(!empty())", + "check(type matches stack_.top().type)" + ] + }, + { + "call_chain": [ + "writeObjectTag", + "check(!tags.contains(tag))", + "tags.insert(tag)", + "stringOutput", + "output_" + ], + "entry_point": "Writer::Impl::writeObjectTag", + "purpose": "Writes a JSON object key, ensuring the tag is unique in debug builds.", + "validation_points": [ + "check(!tags.contains(tag)) (debug only)" + ] + }, + { + "call_chain": [ + "finish", + "check(!empty())", + "stack_.top().type", + "output_", + "stack_.pop()" + ], + "entry_point": "Writer::Impl::finish", + "purpose": "Finishes the current collection, validates stack is not empty.", + "validation_points": [ + "check(!empty())" + ] + } + ], + "data_flows": [ + { + "field": "stack_", + "flow": [ + "start() pushes new Collection onto stack_", + "nextCollectionEntry() checks stack_ and type", + "writeObjectTag() accesses stack_.top().tags", + "finish() pops from stack_" + ], + "origin": "Writer::Impl constructor (empty stack)", + "transformations": [ + "push/pop Collection objects", + "update isFirst, type, tags fields" + ], + "validated_at": [ + "check(!empty()) in nextCollectionEntry and finish", + "check(type matches) in nextCollectionEntry" + ] + }, + { + "field": "isStarted_", + "flow": [ + "markStarted() sets isStarted_ = true", + "output()/stringOutput() call markStarted()", + "isFinished() checks isStarted_ && empty()" + ], + "origin": "Writer::Impl constructor (false)", + "transformations": [ + "set to true on first output" + ], + "validated_at": [ + "check(!isFinished()) in markStarted" + ] + }, + { + "field": "tag (object key)", + "flow": [ + "writeObjectTag() receives tag", + "checks tag not in stack_.top().tags (debug)", + "inserts tag into tags set", + "calls stringOutput(tag)" + ], + "origin": "writeObjectTag parameter", + "transformations": [ + "checked for uniqueness (debug)", + "escaped and output as JSON string" + ], + "validated_at": [ + "check(!tags.contains(tag)) (debug only)" + ] + }, + { + "field": "output_ (Output const&)", + "flow": [ + "output_ is called in output(), stringOutput(), nextCollectionEntry(), writeObjectTag(), finish()" + ], + "origin": "Writer::Impl constructor parameter", + "transformations": [ + "receives raw or escaped JSON data" + ], + "validated_at": [ + "Only indirectly, via stack_ and isStarted_ checks" + ] + } + ], + "description": "Implements a JSON Writer for serializing JSON values, handling special character escaping, collection management (arrays/objects), and output formatting for the XRPL project.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "stack_ (internal collection stack)", + "empty", + "string", + "validation" + ], + "evidence": "check function (likely an assert or custom error handler) at markStarted()", + "issue_pattern": "Missing empty string validation for stack_ (internal collection stack)", + "why_false_positive": "check function (likely an assert or custom error handler) validates stack_ (internal collection stack) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "stack_ (internal collection stack)", + "empty", + "string", + "validation" + ], + "evidence": "check function (likely an assert or custom error handler) at nextCollectionEntry()", + "issue_pattern": "Missing empty string validation for stack_ (internal collection stack)", + "why_false_positive": "check function (likely an assert or custom error handler) validates stack_ (internal collection stack) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/json/Writer.cpp", + "functions": [ + { + "args": [ + "s" + ], + "lineno": 28, + "name": "lengthWithoutTrailingZeros" + }, + { + "args": [], + "lineno": 54, + "name": "empty" + }, + { + "args": [ + "ct" + ], + "lineno": 58, + "name": "start" + }, + { + "args": [ + "bytes" + ], + "lineno": 63, + "name": "output" + }, + { + "args": [ + "bytes" + ], + "lineno": 68, + "name": "stringOutput" + }, + { + "args": [], + "lineno": 89, + "name": "markStarted" + }, + { + "args": [ + "type", + "message" + ], + "lineno": 93, + "name": "nextCollectionEntry" + }, + { + "args": [ + "tag" + ], + "lineno": 105, + "name": "writeObjectTag" + }, + { + "args": [], + "lineno": 116, + "name": "isFinished" + }, + { + "args": [], + "lineno": 120, + "name": "finish" + }, + { + "args": [], + "lineno": 127, + "name": "finishAll" + }, + { + "args": [], + "lineno": 136, + "name": "getOutput" + }, + { + "args": [ + "s" + ], + "lineno": 170, + "name": "output" + }, + { + "args": [ + "s" + ], + "lineno": 174, + "name": "output" + }, + { + "args": [ + "value" + ], + "lineno": 178, + "name": "output" + }, + { + "args": [ + "f" + ], + "lineno": 183, + "name": "output" + }, + { + "args": [ + "f" + ], + "lineno": 188, + "name": "output" + }, + { + "args": [ + "nullptr" + ], + "lineno": 193, + "name": "output" + }, + { + "args": [ + "b" + ], + "lineno": 197, + "name": "output" + }, + { + "args": [ + "s" + ], + "lineno": 201, + "name": "implOutput" + }, + { + "args": [], + "lineno": 205, + "name": "finishAll" + }, + { + "args": [], + "lineno": 210, + "name": "rawAppend" + }, + { + "args": [ + "tag" + ], + "lineno": 214, + "name": "rawSet" + }, + { + "args": [ + "type" + ], + "lineno": 220, + "name": "startRoot" + }, + { + "args": [ + "type" + ], + "lineno": 224, + "name": "startAppend" + }, + { + "args": [ + "type", + "key" + ], + "lineno": 229, + "name": "startSet" + }, + { + "args": [], + "lineno": 236, + "name": "finish" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 13, + "name": "Json" + } + ], + "test_coverage_notes": "The code is likely tested via higher-level JSON serialization tests, possibly in files like test/json/Writer_test.cpp or similar. Direct validation of stack_ integrity (empty, type matching) and tag uniqueness (in debug) would require tests that exercise error conditions: e.g., calling finish() on an empty stack, writing duplicate object keys, or writing to a finished writer. If such tests are missing, error paths and assertion failures may not be fully covered. There is no evidence in this file of explicit unit tests or test hooks; coverage depends on external test suites.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (check function, not standard framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "Exception or assertion (via check)", + "field": "stack_ (internal collection stack)", + "location": "markStarted()", + "validated_by": "check function (likely an assert or custom error handler)", + "validates": [ + "Ensures that isFinished() is false before outputting data" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "Exception or assertion (via check)", + "field": "stack_ (internal collection stack)", + "location": "nextCollectionEntry()", + "validated_by": "check function (likely an assert or custom error handler)", + "validates": [ + "Ensures stack_ is not empty before processing next collection entry" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/json/Writer.cpp.ai.md b/src/libxrpl/json/Writer.cpp.ai.md new file mode 100644 index 0000000000..3232fe3395 --- /dev/null +++ b/src/libxrpl/json/Writer.cpp.ai.md @@ -0,0 +1,47 @@ +# `src/libxrpl/json/Writer.cpp` + +## Role and Purpose + +`Writer.cpp` implements a streaming JSON serializer for the XRPL C++ codebase. Its defining property, stated in the header comment, is *O(1)-space, O(1)-granular output*: it never builds an intermediate JSON tree, and each write emits only a small, bounded chunk of bytes. This makes it the right tool for serializing large XRPL protocol messages, ledger state dumps, or RPC responses without materializing the entire JSON string in memory first. + +The alternative — building a `Json::Value` tree and then calling `jsonAsString()` — is available in the same module but allocates memory proportional to the full output size. `Writer` exists precisely to avoid that allocation on the hot path. + +## Architecture: Pimpl and the Output Sink + +The public `Writer` class holds only a `std::unique_ptr`. All state and logic live in `Writer::Impl`, defined entirely inside the `.cpp` file. This is a deliberate Pimpl pattern: callers depend on the public header without exposure to the `std::stack`, `std::set`, or `std::map` used internally. + +The output destination is `Output`, which is `std::function` (declared in `Output.h`). This indirection means the writer can stream bytes to a string buffer, a network socket, a file, or any custom sink — the writer itself has no opinion on where the bytes land. `Output.h` provides `stringOutput(std::string&)` as a convenience factory for the common case. + +## Collection Stack + +The core of `Impl` is `Stack stack_`, a `std::stack>`. Each `Collection` entry represents one open JSON array or object: + +- `type` distinguishes `array` from `object`. +- `isFirst` tracks whether a comma separator must precede the next entry. This is the standard trick for comma-separated lists: emit a comma before every entry except the first, rather than trying to suppress the trailing comma. +- `tags` (only present in `#ifndef NDEBUG` builds) is a `std::set` that accumulates all object keys seen at that nesting level, catching duplicate keys at runtime before they produce semantically invalid JSON. + +`start()` pushes a new collection and emits the opening `{` or `[`. `finish()` pops it and emits the matching `}` or `]`. `nextCollectionEntry()` validates that the stack is not empty and that the expected collection type matches the actual one, then either skips (for the first entry) or emits a comma. + +## State Machine + +`isStarted_` is a boolean that flips to `true` on the first call to any output method. `isFinished()` is `isStarted_ && stack_.empty()` — meaning: *we started writing and have closed all open collections*. The `markStarted()` function, called by every output path, asserts via `check()` that `isFinished()` is false. Attempting to write after the root collection closes throws a `std::logic_error`. This enforces a write-once contract: a `Writer` produces exactly one complete JSON value. + +## RAII Completion Guarantee + +`~Writer()` calls `impl_->finishAll()`, which closes any still-open arrays and objects. This makes the writer exception-safe: if a caller starts serializing a complex structure and throws mid-way, the destructor ensures the output stream still ends with a syntactically valid (though possibly semantically incomplete) JSON document. The header comment explicitly calls this out as useful for coroutines and exception-based control flow. + +`Writer` is move-only — copy construction and assignment are deleted. Moving transfers `impl_` ownership and leaves the source with a null `impl_`; the destructor and `finishAll()` both guard with `if (impl_)`. + +## String Escaping + +`stringOutput()` in `Impl` handles the eight JSON special characters defined in `jsonSpecialCharacterEscape` (a `std::map` at file scope). The implementation walks the input byte by byte but emits *runs* of clean characters in a single `output_()` call, only breaking the run when a special character is encountered. This means a typical ASCII string with no special characters is emitted in just three calls: the opening `"`, the entire string body, and the closing `"`. + +## Float Serialization + +`lengthWithoutTrailingZeros()` trims trailing zeros from floating-point string representations produced by `xrpl::to_string()`. The constant `integralFloatsBecomeInts` is hard-coded `false`, meaning `3.0` is serialized as `"3.0"` rather than `"3"`. The comment and constant name suggest this was a deliberate choice that was discussed and resolved — leaving the dead flag in place preserves that decision history without complicating the code. + +## Public API Split: Templates vs. Primitives + +The public header exposes `append()` and `set<>(tag, value)` as templates that call `rawAppend()`/`rawSet()` for structural plumbing (comma, colon, tag output) and then dispatch to one of the overloaded `output()` methods for the value. The `raw*` variants are exposed as an escape hatch for callers that handle value emission themselves — for instance, when writing a pre-formatted JSON fragment inline. + +The template `output(Type t)` in the header falls back to `std::to_string(t)` via `implOutput()`, handling integer types and anything else that converts to string. Specializations for `bool`, `float`, `double`, `std::nullptr_t`, `std::string`, `char const*`, `Json::Value`, and `Json::StaticString` are provided as non-template overloads, ensuring the right formatting (e.g., `"true"` not `"1"` for booleans, `"null"` not `"0"` for null pointers). \ No newline at end of file diff --git a/src/libxrpl/json/json_reader.cpp.ai.json b/src/libxrpl/json/json_reader.cpp.ai.json new file mode 100644 index 0000000000..bb1489bcc6 --- /dev/null +++ b/src/libxrpl/json/json_reader.cpp.ai.json @@ -0,0 +1,471 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Reader::parse(std::string const&, Value&)", + "Reader::parse(char const*, char const*, Value&)", + "Reader::readValue(unsigned)", + "Reader::skipCommentTokens(Token&)", + "Reader::readToken(Token&)", + "Reader::readObject/readArray/decodeNumber/decodeDouble/decodeString" + ], + "entry_point": "Reader::parse(std::string const&, Value&)", + "purpose": "Parses a JSON document from a string into a Value object, validating structure and types.", + "validation_points": [ + "Reader::parse(char const*, char const*, Value&) (root type validation)", + "Reader::readValue(unsigned) (type and structure validation)", + "decodeString (string/UTF-8 validation via codePointToUTF8)" + ] + }, + { + "call_chain": [ + "Reader::parse(std::istream&, Value&)", + "Reader::parse(std::string const&, Value&)", + "Reader::parse(char const*, char const*, Value&)", + "Reader::readValue(unsigned)" + ], + "entry_point": "Reader::parse(std::istream&, Value&)", + "purpose": "Parses a JSON document from an input stream, validating structure and types.", + "validation_points": [ + "Reader::parse(char const*, char const*, Value&) (root type validation)", + "Reader::readValue(unsigned) (type and structure validation)" + ] + }, + { + "call_chain": [ + "Reader::parse(char const*, char const*, Value&)", + "Reader::readValue(unsigned)" + ], + "entry_point": "Reader::parse(char const*, char const*, Value&)", + "purpose": "Core parsing logic for JSON, called by all public parse entry points.", + "validation_points": [ + "Manual check: root.isNull(), root.isArray(), root.isObject() (root type validation)", + "readValue: type/structure validation" + ] + }, + { + "call_chain": [ + "Reader::readValue(unsigned)", + "decodeString", + "codePointToUTF8(unsigned int)" + ], + "entry_point": "decodeString (called from readValue)", + "purpose": "Decodes JSON string values, including Unicode escape sequences.", + "validation_points": [ + "codePointToUTF8(unsigned int) (validates Unicode code point range)" + ] + } + ], + "data_flows": [ + { + "field": "document_", + "flow": [ + "parse(std::string const&, Value&) (sets document_)", + "parse(char const*, char const*, Value&) (uses document_.c_str())", + "readValue(unsigned) (parses into Value)" + ], + "origin": "Reader::parse(std::string const&, Value&)", + "transformations": [ + "Copied from input string", + "Pointer arithmetic to get begin/end", + "Tokenized and parsed" + ], + "validated_at": "parse(char const*, char const*, Value&) (root type validation)" + }, + { + "field": "root (Value&)", + "flow": [ + "parse() argument", + "nodes_.push(&root)", + "readValue(unsigned) (populates root)" + ], + "origin": "parse() argument", + "transformations": [ + "Initially empty", + "Populated by parsing logic" + ], + "validated_at": "parse(char const*, char const*, Value&) (root type validation)" + }, + { + "field": "Unicode code point", + "flow": [ + "decodeString", + "codePointToUTF8(unsigned int)", + "string result" + ], + "origin": "decodeString (parsing \\uXXXX escapes)", + "transformations": [ + "Parsed from escape sequence", + "Converted to UTF-8 bytes" + ], + "validated_at": "codePointToUTF8(unsigned int) (range check: cp <= 0x10FFFF)" + }, + { + "field": "Token", + "flow": [ + "readToken (parses next token)", + "skipCommentTokens (skips comments)", + "readValue (dispatches on token type)" + ], + "origin": "readToken", + "transformations": [ + "Lexical analysis", + "Type assignment" + ], + "validated_at": "readValue (switch on token.type_, error if unexpected)" + } + ], + "description": "This file implements a JSON parser (Reader) for parsing JSON documents into Value objects, including support for error reporting, Unicode, comments, and various JSON types.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "JSON root value", + "empty", + "string", + "validation" + ], + "evidence": "manual check in Reader::parse(char const*, char const*, Value&) at Reader::parse(char const*, char const*, Value&)", + "issue_pattern": "Missing empty string validation for JSON root value", + "why_false_positive": "manual check in Reader::parse(char const*, char const*, Value&) validates JSON root value for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "JSON root value", + "type", + "validation", + "check" + ], + "evidence": "manual check in Reader::parse(char const*, char const*, Value&) at Reader::parse(char const*, char const*, Value&)", + "issue_pattern": "Missing type validation for JSON root value", + "why_false_positive": "manual check in Reader::parse(char const*, char const*, Value&) validates JSON root value type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "Unicode code point (for UTF-8 encoding)", + "empty", + "string", + "validation" + ], + "evidence": "manual range check in codePointToUTF8(unsigned int) at codePointToUTF8(unsigned int)", + "issue_pattern": "Missing empty string validation for Unicode code point (for UTF-8 encoding)", + "why_false_positive": "manual range check in codePointToUTF8(unsigned int) validates Unicode code point (for UTF-8 encoding) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "Unicode code point (for UTF-8 encoding)", + "range", + "bounds", + "validation" + ], + "evidence": "manual range check in codePointToUTF8(unsigned int) at codePointToUTF8(unsigned int)", + "issue_pattern": "Missing range validation for Unicode code point (for UTF-8 encoding)", + "why_false_positive": "manual range check in codePointToUTF8(unsigned int) validates Unicode code point (for UTF-8 encoding) range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/json/json_reader.cpp", + "functions": [ + { + "args": [ + "unsigned int cp" + ], + "lineno": 10, + "name": "codePointToUTF8" + }, + { + "args": [ + "std::string const& document", + "Value& root" + ], + "lineno": 32, + "name": "Reader::parse" + }, + { + "args": [ + "std::istream& sin", + "Value& root" + ], + "lineno": 39, + "name": "Reader::parse" + }, + { + "args": [ + "char const* beginDoc", + "char const* endDoc", + "Value& root" + ], + "lineno": 50, + "name": "Reader::parse" + }, + { + "args": [ + "unsigned depth" + ], + "lineno": 72, + "name": "Reader::readValue" + }, + { + "args": [ + "Token& token" + ], + "lineno": 98, + "name": "Reader::skipCommentTokens" + }, + { + "args": [ + "TokenType type", + "Token& token", + "char const* message" + ], + "lineno": 105, + "name": "Reader::expectToken" + }, + { + "args": [ + "Token& token" + ], + "lineno": 112, + "name": "Reader::readToken" + }, + { + "args": [], + "lineno": 154, + "name": "Reader::skipSpaces" + }, + { + "args": [ + "Location pattern", + "int patternLength" + ], + "lineno": 167, + "name": "Reader::match" + }, + { + "args": [], + "lineno": 179, + "name": "Reader::readComment" + }, + { + "args": [], + "lineno": 186, + "name": "Reader::readCStyleComment" + }, + { + "args": [], + "lineno": 196, + "name": "Reader::readCppStyleComment" + }, + { + "args": [], + "lineno": 204, + "name": "Reader::readNumber" + }, + { + "args": [], + "lineno": 229, + "name": "Reader::readString" + }, + { + "args": [ + "Token& tokenStart", + "unsigned depth" + ], + "lineno": 242, + "name": "Reader::readObject" + }, + { + "args": [ + "Token& tokenStart", + "unsigned depth" + ], + "lineno": 292, + "name": "Reader::readArray" + }, + { + "args": [ + "Token& token" + ], + "lineno": 332, + "name": "Reader::decodeNumber" + }, + { + "args": [ + "Token& token" + ], + "lineno": 387, + "name": "Reader::decodeDouble" + }, + { + "args": [ + "Token& token" + ], + "lineno": 419, + "name": "Reader::decodeString" + }, + { + "args": [ + "Token& token", + "std::string& decoded" + ], + "lineno": 427, + "name": "Reader::decodeString" + }, + { + "args": [ + "Token& token", + "Location& current", + "Location end", + "unsigned int& unicode" + ], + "lineno": 478, + "name": "Reader::decodeUnicodeCodePoint" + }, + { + "args": [ + "Token& token", + "Location& current", + "Location end", + "unsigned int& unicode" + ], + "lineno": 507, + "name": "Reader::decodeUnicodeEscapeSequence" + }, + { + "args": [ + "std::string const& message", + "Token& token", + "Location extra" + ], + "lineno": 535, + "name": "Reader::addError" + }, + { + "args": [ + "TokenType skipUntilToken" + ], + "lineno": 545, + "name": "Reader::recoverFromError" + }, + { + "args": [ + "std::string const& message", + "Token& token", + "TokenType skipUntilToken" + ], + "lineno": 561, + "name": "Reader::addErrorAndRecover" + }, + { + "args": [], + "lineno": 567, + "name": "Reader::currentValue" + }, + { + "args": [], + "lineno": 572, + "name": "Reader::getNextChar" + }, + { + "args": [ + "Location location", + "int& line", + "int& column" + ], + "lineno": 579, + "name": "Reader::getLocationLineAndColumn" + }, + { + "args": [ + "Location location" + ], + "lineno": 599, + "name": "Reader::getLocationLineAndColumn" + }, + { + "args": [], + "lineno": 606, + "name": "Reader::getFormattedErrorMessages" + }, + { + "args": [ + "std::istream& sin", + "Value& root" + ], + "lineno": 622, + "name": "operator>>" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 16, + "name": "Json" + } + ], + "test_coverage_notes": "The core validation logic (root type, Unicode code point range, token type checks) is typically covered by unit tests for JSON parsing. Likely test files: json_reader_test.cpp, json_value_test.cpp, or similar in the test suite. Gaps may exist for edge cases: deeply nested structures (nest_limit), malformed Unicode escapes, or non-object/array root values. Manual validation checks (e.g., root type) should be explicitly tested for error reporting. Exception paths (addError) should be covered by negative tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation (no external validation framework)", + "validation_layer": "business_logic (inside Reader::parse and utility functions)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "addError (internal error collection, not exception); returns false", + "field": "JSON root value", + "location": "Reader::parse(char const*, char const*, Value&)", + "validated_by": "manual check in Reader::parse(char const*, char const*, Value&)", + "validates": [ + "Checks that the root value is either an array or an object", + "If not, adds error: 'A valid JSON document must be either an array or an object value.'" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "None (no error thrown for out-of-range, just returns empty string)", + "field": "Unicode code point (for UTF-8 encoding)", + "location": "codePointToUTF8(unsigned int)", + "validated_by": "manual range check in codePointToUTF8(unsigned int)", + "validates": [ + "Checks if code point is within valid Unicode ranges for UTF-8 encoding (<= 0x10FFFF)", + "No explicit error for out-of-range, but only valid ranges are encoded" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/json/json_reader.cpp.ai.md b/src/libxrpl/json/json_reader.cpp.ai.md new file mode 100644 index 0000000000..2e719a5ffa --- /dev/null +++ b/src/libxrpl/json/json_reader.cpp.ai.md @@ -0,0 +1,51 @@ +# `json_reader.cpp` — JSON Recursive-Descent Parser + +## Role in the System + +This file implements the `Json::Reader` class, the sole JSON deserialization component in `libxrpl`. Every inbound JSON document — RPC requests, configuration files, test fixtures — passes through this parser before any application logic sees it. It converts a raw text buffer into a `Json::Value` tree, the in-memory representation of structured data throughout the ledger codebase. Because `Reader` sits at the trust boundary for external input, it enforces several constraints stricter than the JSON specification requires. + +## Architecture: Lexer + Recursive Descent + +The implementation follows a classic two-phase design. `readToken()` acts as the lexer: it advances the `current_` pointer through the source buffer and classifies each syntactic unit into a `TokenType` enum (brace, bracket, string, integer, double, boolean, null, comment, etc.). The recursive-descent parser — `readValue()`, `readObject()`, and `readArray()` — then drives the lexer by calling `readToken()` and dispatching on the returned type. + +The two phases are not cleanly separated in time: decoding (converting token bytes to typed values) happens lazily, only when a token is consumed. `decodeNumber()`, `decodeDouble()`, `decodeString()`, and `decodeUnicodeCodePoint()` operate on a `Token`'s `start_`/`end_` pointers into the original source buffer. This avoids allocating intermediate strings for tokens that are immediately validated and discarded. + +## `nodes_` Stack and Value Population + +The parser writes into the caller-supplied `Value& root` through a `std::stack` named `nodes_`. On entry, the root pointer is pushed. Each time `readObject()` or `readArray()` descends into a child value, it takes a reference from the parent container (`currentValue()[name]` or `currentValue()[index]`) and pushes that child's address. After the recursive `readValue()` call returns, the child pointer is popped. + +This means the recursion depth and the stack depth are always in sync: `readValue()` is called recursively, but parent tracking is maintained through `nodes_` rather than function parameters. The hard `nest_limit` of 25 levels is enforced at the start of every `readValue()` call, preventing stack exhaustion from adversarially nested documents — critical for a network-facing service. + +## Deliberate Strictness: Root Type and Duplicate Keys + +Two places where `Reader` is more restrictive than the JSON specification are worth noting. First, `parse(char const*, char const*, Value&)` validates after parsing that the root value is either an object or an array. A bare string, number, boolean, or null at the document root triggers a parse error. This matches RFC 4627 semantics and reflects how XRPL message framing works: every valid protocol message is a JSON object. + +Second, `readObject()` calls `currentValue().isMember(name)` before inserting each key-value pair and returns an error if the name already exists. Standard JSON allows duplicate keys (with implementation-defined semantics for which value wins). Rejecting duplicates here prevents ambiguity in transaction parsing, where a second `"Amount"` field could silently shadow the first. + +## Number Decoding + +`readNumber()` classifies a token as `tokenInteger` or `tokenDouble` by scanning for floating-point indicators (`.`, `e`, `E`, `+`, `-`). Integers are then decoded by `decodeNumber()` using a `std::int64_t` accumulator, guarded by a `static_assert` that this type is wider than `Value::maxUInt`. If the absolute magnitude exceeds `Value::maxUInt`, the token is rejected as out of range. For non-negative values that fit in `Value::maxInt`, the result is stored as a signed `Value::Int`; larger values use `Value::UInt`. This preference for signed representation when possible reduces surprises in downstream comparisons. + +`decodeDouble()` uses `sscanf` rather than `std::stod`. A comment in the source traces this choice to an OS X crash involving string-constant format arguments — the workaround is storing the format string `"%lf"` in a `char[]` array rather than passing a literal. A buffer of 32 characters handles the common case without heap allocation; longer strings fall back to a temporary `std::string`. + +## String Decoding and Unicode + +`decodeString()` processes the raw token bytes character by character, handling all JSON escape sequences (`\"`, `\\`, `\/`, `\b`, `\f`, `\n`, `\r`, `\t`). The `\uXXXX` form is handled by `decodeUnicodeEscapeSequence()`, which manually converts four hex digits to a code point. `decodeUnicodeCodePoint()` wraps this and detects UTF-16 surrogate pairs: if the decoded value falls in the high-surrogate range U+D800–U+DBFF, a second `\uXXXX` sequence must immediately follow, and the two are combined using the standard formula `0x10000 + ((high & 0x3FF) << 10) + (low & 0x3FF)`. The resulting scalar is encoded to UTF-8 by `codePointToUTF8()`, which covers all four byte-length cases (up to U+10FFFF) following the RFC 3629 bit-layout directly. + +## Error Reporting and Recovery + +Parse errors are accumulated in a `std::deque` rather than terminating on the first fault. Each `ErrorInfo` records the bad `Token` (start and end pointers into the source), a human-readable message, and an optional secondary `Location` for context (used when the error site differs from the relevant position). `addError()` always returns `false`, enabling the idiomatic pattern `return addError("message", token)` throughout the call chain. + +`recoverFromError()` is invoked when a structural error (missing colon, missing closing brace, invalid value) is detected inside an object or array. It skips tokens until the expected terminator (`}` or `]`) or end of stream, discarding any secondary errors produced during the skip. This lets the parser continue and report multiple errors from a single malformed document. + +`getFormattedErrorMessages()` converts accumulated errors to human-readable text with one-based line and column numbers. It walks the source from the beginning to compute line/column positions on demand — O(n) per error, but acceptable given that errors are exceptional. + +## Comment Tolerance + +The lexer recognizes both C-style (`/* ... */`) and C++-style (`// ...`) comments, assigning them `tokenComment`. `skipCommentTokens()` loops until a non-comment token is seen, making comments transparent to the rest of the parser. This is a deliberate extension beyond strict JSON — XRPL configuration and some internal tooling embed comments in JSON files. + +## Public API and the `operator>>` Overload + +The public surface is three `parse()` overloads: one taking `std::string`, one taking raw `char const*` pointers, and one taking `std::istream`. The `istream` variant slurps the entire stream into a `std::string` and delegates to the string overload rather than streaming tokens incrementally — a tradeoff that simplifies the implementation at the cost of buffering the whole document. A fourth templated overload in the header accepts Boost.Asio buffer sequences, assembling them into a `std::string` before parsing. + +The free `operator>>(std::istream&, Value&)` provides stream extraction syntax. Unlike the member `parse()` methods that return `false` on failure, `operator>>` throws `std::runtime_error` via `xrpl::Throw<>`, making it suitable for code paths where failure is truly exceptional and propagation via return value would be burdensome. \ No newline at end of file diff --git a/src/libxrpl/json/json_value.cpp.ai.json b/src/libxrpl/json/json_value.cpp.ai.json new file mode 100644 index 0000000000..17ec648b83 --- /dev/null +++ b/src/libxrpl/json/json_value.cpp.ai.json @@ -0,0 +1,410 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "DefaultValueAllocator" + } + ], + "code_paths": [ + { + "call_chain": [ + "Value::CZString::CZString(char const*, DuplicationPolicy)", + "valueAllocator()->makeMemberName(cstr)", + "DefaultValueAllocator::makeMemberName", + "DefaultValueAllocator::duplicateStringValue" + ], + "entry_point": "Value::CZString::CZString(char const* cstr, DuplicationPolicy allocate)", + "purpose": "Constructs a CZString, optionally duplicating the input string if requested.", + "validation_points": [ + "duplicateStringValue: Validates and duplicates the input string (cstr)." + ] + }, + { + "call_chain": [ + "Value::CZString::CZString(CZString const&)", + "valueAllocator()->makeMemberName(other.cstr_)" + ], + "entry_point": "Value::CZString::CZString(CZString const& other)", + "purpose": "Copy constructor for CZString, duplicates string if needed.", + "validation_points": [ + "duplicateStringValue: Validates and duplicates the input string (other.cstr_)." + ] + }, + { + "call_chain": [ + "Value::CZString::~CZString()", + "valueAllocator()->releaseMemberName(const_cast(cstr_))", + "DefaultValueAllocator::releaseMemberName", + "DefaultValueAllocator::releaseStringValue" + ], + "entry_point": "Value::CZString::~CZString()", + "purpose": "Destructor for CZString, releases memory if string was duplicated.", + "validation_points": [ + "releaseStringValue: Validates pointer before freeing." + ] + }, + { + "call_chain": [ + "DefaultValueAllocator::makeMemberName", + "DefaultValueAllocator::duplicateStringValue" + ], + "entry_point": "DefaultValueAllocator::makeMemberName(char const* memberName)", + "purpose": "Duplicates a member name string.", + "validation_points": [ + "duplicateStringValue: Validates and duplicates the input string (memberName)." + ] + }, + { + "call_chain": [ + "DefaultValueAllocator::releaseMemberName", + "DefaultValueAllocator::releaseStringValue" + ], + "entry_point": "DefaultValueAllocator::releaseMemberName(char* memberName)", + "purpose": "Releases a duplicated member name string.", + "validation_points": [ + "releaseStringValue: Validates pointer before freeing." + ] + } + ], + "data_flows": [ + { + "field": "char const* value (string value)", + "flow": [ + "Input to duplicateStringValue", + "Length determined (strlen if unknown)", + "malloc for new string", + "memcpy to new buffer", + "Returned as duplicated string" + ], + "origin": "Passed to duplicateStringValue (from makeMemberName or directly)", + "transformations": [ + "String is duplicated (deep copy)", + "Null-terminated" + ], + "validated_at": "duplicateStringValue (checks for nullptr, length)" + }, + { + "field": "char* value (string value)", + "flow": [ + "Allocated in duplicateStringValue", + "Passed to releaseStringValue (via releaseMemberName)", + "free called if not nullptr" + ], + "origin": "Allocated by duplicateStringValue, passed to releaseStringValue", + "transformations": [ + "Memory is freed" + ], + "validated_at": "releaseStringValue (checks for nullptr)" + }, + { + "field": "char const* memberName", + "flow": [ + "Input to makeMemberName", + "Passed to duplicateStringValue", + "Duplicated and returned" + ], + "origin": "Passed to makeMemberName", + "transformations": [ + "String is duplicated" + ], + "validated_at": "duplicateStringValue" + }, + { + "field": "char* memberName", + "flow": [ + "Allocated in duplicateStringValue", + "Passed to releaseMemberName", + "Passed to releaseStringValue", + "Memory freed" + ], + "origin": "Allocated by makeMemberName/duplicateStringValue", + "transformations": [ + "Memory is freed" + ], + "validated_at": "releaseStringValue" + }, + { + "field": "char const* cstr (in CZString)", + "flow": [ + "Input to CZString constructor", + "If duplication requested, passed to makeMemberName", + "Duplicated via duplicateStringValue", + "Stored in cstr_ field" + ], + "origin": "Passed to CZString constructor", + "transformations": [ + "May be duplicated" + ], + "validated_at": "duplicateStringValue" + } + ], + "description": "Implements the core logic for the Json::Value class, which represents and manipulates JSON values of various types (null, int, uint, double, string, array, object, boolean) including memory management, type conversions, comparison, and accessors.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "char* and char const* pointers for null before use", + "validation", + "missing", + "check" + ], + "evidence": "Field char* and char const* pointers for null before use validated by Custom (no external validation framework detected)", + "issue_pattern": "Missing validation for char* and char const* pointers for null before use", + "why_false_positive": "Custom (no external validation framework detected) validates char* and char const* pointers for null before use automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "value (char const* value)", + "empty", + "string", + "validation" + ], + "evidence": "duplicateStringValue at DefaultValueAllocator::duplicateStringValue", + "issue_pattern": "Missing empty string validation for value (char const* value)", + "why_false_positive": "duplicateStringValue validates value (char const* value) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "memberName (char const* memberName)", + "empty", + "string", + "validation" + ], + "evidence": "makeMemberName at DefaultValueAllocator::makeMemberName", + "issue_pattern": "Missing empty string validation for memberName (char const* memberName)", + "why_false_positive": "makeMemberName validates memberName (char const* memberName) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "value (char* value)", + "empty", + "string", + "validation" + ], + "evidence": "releaseStringValue at DefaultValueAllocator::releaseStringValue", + "issue_pattern": "Missing empty string validation for value (char* value)", + "why_false_positive": "releaseStringValue validates value (char* value) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "memberName (char* memberName)", + "empty", + "string", + "validation" + ], + "evidence": "releaseMemberName at DefaultValueAllocator::releaseMemberName", + "issue_pattern": "Missing empty string validation for memberName (char* memberName)", + "why_false_positive": "releaseMemberName validates memberName (char* memberName) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "cstr (char const* cstr)", + "empty", + "string", + "validation" + ], + "evidence": "Value::CZString constructor at Value::CZString::CZString(char const* cstr, DuplicationPolicy allocate)", + "issue_pattern": "Missing empty string validation for cstr (char const* cstr)", + "why_false_positive": "Value::CZString constructor validates cstr (char const* cstr) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "other.cstr_ (char const* cstr_ in copy constructor)", + "empty", + "string", + "validation" + ], + "evidence": "Value::CZString copy constructor at Value::CZString::CZString(CZString const& other)", + "issue_pattern": "Missing empty string validation for other.cstr_ (char const* cstr_ in copy constructor)", + "why_false_positive": "Value::CZString copy constructor validates other.cstr_ (char const* cstr_ in copy constructor) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/json/json_value.cpp", + "functions": [ + { + "args": [ + "char const* memberName" + ], + "lineno": 18, + "name": "makeMemberName" + }, + { + "args": [ + "char* memberName" + ], + "lineno": 23, + "name": "releaseMemberName" + }, + { + "args": [ + "char const* value", + "unsigned int length" + ], + "lineno": 28, + "name": "duplicateStringValue" + }, + { + "args": [ + "char* value" + ], + "lineno": 41, + "name": "releaseStringValue" + }, + { + "args": [], + "lineno": 47, + "name": "valueAllocator" + }, + { + "args": [ + "Int i", + "UInt ui" + ], + "lineno": 210, + "name": "integerCmp" + }, + { + "args": [ + "Value const& x", + "Value const& y" + ], + "lineno": 220, + "name": "operator<" + }, + { + "args": [ + "Value const& x", + "Value const& y" + ], + "lineno": 266, + "name": "operator==" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "Json" + } + ], + "test_coverage_notes": "The code is low-level and likely tested indirectly via higher-level JSON parsing and manipulation tests. Direct unit tests for DefaultValueAllocator, duplicateStringValue, releaseStringValue, and CZString constructors/destructors are not visible here. Test coverage may exist in files like 'json_value_test.cpp', 'json_test.cpp', or integration tests for JSON parsing. Gaps: No explicit tests for edge cases (null pointers, zero-length strings, double-free, etc.) are evident from this file alone.", + "validation_architecture": { + "auto_validated_fields": [ + "char* and char const* pointers for null before use" + ], + "framework": "Custom (no external validation framework detected)", + "validation_layer": "business_logic (within allocator and constructor logic)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (null pointer handled gracefully)", + "field": "value (char const* value)", + "location": "DefaultValueAllocator::duplicateStringValue", + "validated_by": "duplicateStringValue", + "validates": [ + "Checks if value is nullptr before calling strlen or memcpy", + "If value is nullptr, length is set to 0 and memcpy is skipped" + ], + "validation_type": "type|null-check" + }, + { + "confidence": 0.9, + "error_thrown": "none (delegates to duplicateStringValue)", + "field": "memberName (char const* memberName)", + "location": "DefaultValueAllocator::makeMemberName", + "validated_by": "makeMemberName", + "validates": [ + "Indirectly checks if memberName is nullptr via duplicateStringValue" + ], + "validation_type": "type|null-check (delegated)" + }, + { + "confidence": 1.0, + "error_thrown": "none (null pointer handled gracefully)", + "field": "value (char* value)", + "location": "DefaultValueAllocator::releaseStringValue", + "validated_by": "releaseStringValue", + "validates": [ + "Checks if value is nullptr before calling free" + ], + "validation_type": "type|null-check" + }, + { + "confidence": 0.9, + "error_thrown": "none (delegates to releaseStringValue)", + "field": "memberName (char* memberName)", + "location": "DefaultValueAllocator::releaseMemberName", + "validated_by": "releaseMemberName", + "validates": [ + "Indirectly checks if memberName is nullptr via releaseStringValue" + ], + "validation_type": "type|null-check (delegated)" + }, + { + "confidence": 0.9, + "error_thrown": "none (delegates to makeMemberName)", + "field": "cstr (char const* cstr)", + "location": "Value::CZString::CZString(char const* cstr, DuplicationPolicy allocate)", + "validated_by": "Value::CZString constructor", + "validates": [ + "Indirectly checks if cstr is nullptr via makeMemberName/duplicateStringValue" + ], + "validation_type": "type|null-check (delegated)" + }, + { + "confidence": 0.9, + "error_thrown": "none (delegates to makeMemberName)", + "field": "other.cstr_ (char const* cstr_ in copy constructor)", + "location": "Value::CZString::CZString(CZString const& other)", + "validated_by": "Value::CZString copy constructor", + "validates": [ + "Checks if other.cstr_ is nullptr before duplicating" + ], + "validation_type": "type|null-check (delegated)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/json/json_value.cpp.ai.md b/src/libxrpl/json/json_value.cpp.ai.md new file mode 100644 index 0000000000..d9a1681ade --- /dev/null +++ b/src/libxrpl/json/json_value.cpp.ai.md @@ -0,0 +1,45 @@ +# `json_value.cpp` — Core JSON Value Type for XRPL + +## Role in the System + +This file implements `Json::Value`, the discriminated-union type that represents any JSON datum in the XRPL codebase. It is the foundational building block for all JSON construction, parsing, serialization, and RPC message handling throughout rippled. Every inbound API request and outbound response ultimately passes through `Value` objects; the class must therefore be both correct across all seven JSON types and efficient enough to be used freely in hot paths. + +## Discriminated Union Design + +`Value` stores its payload in a raw `union ValueHolder` tagged by a `ValueType` enum stored in an 8-bit bitfield. The union holds either a plain integer/double/bool scalar, a heap-allocated `char*` string, or a heap-allocated `ObjectValues*` — a `std::map`. Because both arrays and objects share the same `map_` branch, the distinction between `arrayValue` and `objectValue` is purely in how keys are interpreted: arrays use integer `CZString` keys, objects use string keys. This collapses two superficially different containers into one representation, keeping the union small and the switch-dispatch uniform. + +The `allocated_` member is a one-bit field that tracks whether the `value_.string_` pointer is heap-owned, enabling the `StaticString` optimization: a `Value` constructed from a `StaticString` stores the raw pointer without copying, setting `allocated_` to zero so the destructor skips `free`. This is intentional and safe because `StaticString` is a compile-time tag signalling that the underlying C-string has static or program-lifetime storage. Hot-path code in rippled frequently uses `static const StaticString` constants as object keys to avoid heap allocation entirely. + +## `CZString`: The Map Key Type + +Object and array keys are stored as `Value::CZString` (C-Zero String), a private inner class that wraps either a `char*` or an integer index. The `DuplicationPolicy` enum — `noDuplication`, `duplicate`, `duplicateOnCopy` — drives whether the `CZString` owns its memory. Numeric array keys use the integer-path constructor `CZString(int index)`, leaving `cstr_` null; string object keys use the `char const*` constructor with an appropriate policy. + +The copy constructor of `CZString` contains a subtle but important rule: if the source was marked `noDuplication` (i.e., it points at a static string), the copy preserves that status and does *not* copy the pointer. If the source owns a duplicate, the copy makes its own duplicate. This ownership discipline is what allows `operator[]` on an object to accept both transient `const char*` keys (which get duplicated into the map) and long-lived `StaticString` keys (which are stored by reference). + +## Memory Management via `DefaultValueAllocator` + +String memory is routed through a global `ValueAllocator*` obtained from the static function `valueAllocator()`, with `DefaultValueAllocator` as the sole concrete implementation. This allocator uses `malloc`/`free` directly rather than `new`/`delete`. The reason is historical: the original JsonCpp design allowed callers to swap in a custom allocator (a pool allocator, for example) without recompiling — hence the virtual interface. The `DummyValueAllocatorInitializer` global ensures the allocator singleton is constructed before `main()` to avoid static-initialization-order issues. + +`duplicateStringValue()` accepts an optional explicit length so that `std::string` values (which know their length without a `strlen` call) can skip that scan. The `nullptr` path for the input string is handled gracefully: length is forced to zero and `memcpy` is skipped, producing a heap-allocated empty string. + +## Copy, Move, and Swap Semantics + +Copy-assignment uses the classic copy-and-swap idiom: a copy-constructed temporary is swapped in via `swap()`, which performs three `std::swap` calls on the raw members. This provides strong exception safety at the cost of an extra allocation when copying a string or map. Move construction simply steals the union members and resets the source to `nullValue`/`allocated_=0`, avoiding any heap operation. Move-assignment also uses swap, ensuring the stolen-from temporary's destructor properly cleans up whatever was in `this` before the move. + +## Array Sizing Semantics + +`size()` on an `arrayValue` returns the index of the last map entry plus one, rather than the actual entry count. This reflects a sparse-array model: if you write `arr[5] = x` on an empty array, `size()` returns 6, even though only one slot is occupied. The non-const `operator[](UInt)` auto-inserts `null` entries when needed. This differs from typical C++ container behaviour and can surprise callers who expect `size()` to equal the number of non-null elements. + +## Type Coercion + +The `asXxx()` accessors implement a permissive coercion lattice. `asInt()` accepts `null` (returns 0), `uintValue` (with range check), `realValue` (with range check), `booleanValue` (0/1), and even `stringValue` (via `beast::lexicalCastThrow`). `asAbsUInt()` is a XRPL-specific addition that returns the absolute value of any numeric type as an unsigned integer, handling the edge case of `INT_MIN` by casting through `int64_t` before negating to avoid overflow. Arrays and objects are never coercible to scalars; such attempts fire `JSON_ASSERT_MESSAGE` which aborts in debug builds. + +`isConvertibleTo()` encodes these rules in predicate form without performing the conversion, allowing callers to check feasibility cheaply. The `realValue`-to-integer path additionally checks that the double has no fractional component (using `fabs(round(x) - x) < epsilon`) before approving the conversion. + +## Mixed-Type Comparison + +The free `operator<` and `operator==` handle the case where one operand is `intValue` and the other is `uintValue` through the helper `integerCmp(Int, UInt)`. Negative signed integers are immediately less than any unsigned value; non-negative signed integers are compared after safe widening. Without this, comparing `Value(-1) < Value(0u)` could yield the wrong answer due to implicit unsigned promotion. For all other type mismatches, comparison falls back to type-enum ordering, giving `Value` a total order suitable for use as a map key. + +## Iterator Architecture + +`ValueIteratorBase`, `ValueConstIterator`, and `ValueIterator` (implemented in the companion `json_valueiterator.cpp`, which is conceptually part of this translation unit) wrap `std::map::iterator` over `ObjectValues`. The `isNull_` flag handles the degenerate case of iterating over a `nullValue`: both `begin()` and `end()` return default-constructed iterators, and `computeDistance` between two null iterators returns zero rather than invoking undefined behaviour on an uninitialized `std::map::iterator`. The `key()` method on the iterator reconstructs a `Value` from the `CZString` — returning an integer `Value` for array indices or a string `Value` for object keys — making generic traversal straightforward. \ No newline at end of file diff --git a/src/libxrpl/json/json_valueiterator.cpp.ai.json b/src/libxrpl/json/json_valueiterator.cpp.ai.json new file mode 100644 index 0000000000..c95204e160 --- /dev/null +++ b/src/libxrpl/json/json_valueiterator.cpp.ai.json @@ -0,0 +1,445 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "ValueIteratorBase" + }, + { + "args": [], + "lineno": 95, + "name": "ValueConstIterator" + }, + { + "args": [], + "lineno": 107, + "name": "ValueIterator" + } + ], + "code_paths": [ + { + "call_chain": [ + "ValueIteratorBase::key", + "Value::CZString::c_str", + "Value::CZString::isStaticString", + "Value::CZString::index" + ], + "entry_point": "ValueIteratorBase::key", + "purpose": "Retrieves the key for the current iterator position, returning either a string or an index.", + "validation_points": [ + "ValueIteratorBase::key: czString.c_str() != nullptr (manual null pointer check)" + ] + }, + { + "call_chain": [ + "ValueIteratorBase::index", + "Value::CZString::c_str", + "Value::CZString::index" + ], + "entry_point": "ValueIteratorBase::index", + "purpose": "Retrieves the index for the current iterator position, or returns -1 if the key is a string.", + "validation_points": [ + "ValueIteratorBase::index: czString.c_str() == nullptr (manual null pointer check)" + ] + }, + { + "call_chain": [ + "ValueIteratorBase::isEqual" + ], + "entry_point": "ValueIteratorBase::isEqual", + "purpose": "Checks if two iterators are equal, with special handling for null iterators.", + "validation_points": [ + "ValueIteratorBase::isEqual: isNull_ (manual check)" + ] + }, + { + "call_chain": [ + "ValueIteratorBase::computeDistance" + ], + "entry_point": "ValueIteratorBase::computeDistance", + "purpose": "Computes the distance between two iterators, with special handling for null iterators.", + "validation_points": [ + "ValueIteratorBase::computeDistance: isNull_ && other.isNull_ (manual check)" + ] + }, + { + "call_chain": [ + "ValueIteratorBase::memberName", + "Value::CZString::c_str" + ], + "entry_point": "ValueIteratorBase::memberName", + "purpose": "Returns the member name as a C string, or an empty string if null.", + "validation_points": [ + "ValueIteratorBase::memberName: (*current_).first.c_str() != nullptr (manual null pointer check)" + ] + } + ], + "data_flows": [ + { + "field": "isNull_", + "flow": [ + "ValueIteratorBase::ValueIteratorBase", + "ValueIteratorBase::isEqual", + "ValueIteratorBase::computeDistance" + ], + "origin": "ValueIteratorBase constructor (default: true, with iterator: false)", + "transformations": [ + "Set to true in default constructor, false in iterator constructor", + "Checked in isEqual and computeDistance for special null handling" + ], + "validated_at": "isEqual, computeDistance" + }, + { + "field": "current_", + "flow": [ + "ValueIteratorBase::ValueIteratorBase", + "ValueIteratorBase::deref", + "ValueIteratorBase::increment", + "ValueIteratorBase::decrement", + "ValueIteratorBase::key", + "ValueIteratorBase::index", + "ValueIteratorBase::memberName" + ], + "origin": "ValueIteratorBase constructor (passed-in iterator)", + "transformations": [ + "Incremented/decremented in increment/decrement", + "Dereferenced for key/index/memberName" + ], + "validated_at": "Indirectly validated via isNull_ and null pointer checks on c_str()" + }, + { + "field": "Value::CZString (from (*current_).first)", + "flow": [ + "(*current_).first", + "ValueIteratorBase::key", + "ValueIteratorBase::index", + "ValueIteratorBase::memberName" + ], + "origin": "Underlying map key in ObjectValues", + "transformations": [ + "c_str() called to check if key is string or index", + "isStaticString() checked for static string optimization" + ], + "validated_at": "czString.c_str() != nullptr (manual null pointer check in key, index, memberName)" + } + ], + "description": "Implements iterator classes and methods for traversing JSON object values in the Json namespace, including base, const, and mutable iterators.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "isNull_", + "empty", + "string", + "validation" + ], + "evidence": "manual check at ValueIteratorBase::computeDistance", + "issue_pattern": "Missing empty string validation for isNull_", + "why_false_positive": "manual check validates isNull_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "isNull_", + "empty", + "string", + "validation" + ], + "evidence": "manual check at ValueIteratorBase::isEqual", + "issue_pattern": "Missing empty string validation for isNull_", + "why_false_positive": "manual check validates isNull_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "czString.c_str()", + "empty", + "string", + "validation" + ], + "evidence": "manual null pointer check at ValueIteratorBase::key", + "issue_pattern": "Missing empty string validation for czString.c_str()", + "why_false_positive": "manual null pointer check validates czString.c_str() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "czString.c_str()", + "type", + "validation", + "check" + ], + "evidence": "manual null pointer check at ValueIteratorBase::key", + "issue_pattern": "Missing type validation for czString.c_str()", + "why_false_positive": "manual null pointer check validates czString.c_str() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "czString.c_str()", + "empty", + "string", + "validation" + ], + "evidence": "manual null pointer check at ValueIteratorBase::index", + "issue_pattern": "Missing empty string validation for czString.c_str()", + "why_false_positive": "manual null pointer check validates czString.c_str() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "czString.c_str()", + "type", + "validation", + "check" + ], + "evidence": "manual null pointer check at ValueIteratorBase::index", + "issue_pattern": "Missing type validation for czString.c_str()", + "why_false_positive": "manual null pointer check validates czString.c_str() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "(*current_).first.c_str()", + "empty", + "string", + "validation" + ], + "evidence": "manual null pointer check at ValueIteratorBase::memberName", + "issue_pattern": "Missing empty string validation for (*current_).first.c_str()", + "why_false_positive": "manual null pointer check validates (*current_).first.c_str() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "(*current_).first.c_str()", + "type", + "validation", + "check" + ], + "evidence": "manual null pointer check at ValueIteratorBase::memberName", + "issue_pattern": "Missing type validation for (*current_).first.c_str()", + "why_false_positive": "manual null pointer check validates (*current_).first.c_str() type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/json/json_valueiterator.cpp", + "functions": [ + { + "args": [], + "lineno": 10, + "name": "ValueIteratorBase::ValueIteratorBase" + }, + { + "args": [ + "Value::ObjectValues::iterator const& current" + ], + "lineno": 13, + "name": "ValueIteratorBase::ValueIteratorBase" + }, + { + "args": [], + "lineno": 17, + "name": "ValueIteratorBase::deref" + }, + { + "args": [], + "lineno": 22, + "name": "ValueIteratorBase::increment" + }, + { + "args": [], + "lineno": 27, + "name": "ValueIteratorBase::decrement" + }, + { + "args": [ + "SelfType const& other" + ], + "lineno": 32, + "name": "ValueIteratorBase::computeDistance" + }, + { + "args": [ + "SelfType const& other" + ], + "lineno": 54, + "name": "ValueIteratorBase::isEqual" + }, + { + "args": [ + "SelfType const& other" + ], + "lineno": 62, + "name": "ValueIteratorBase::copy" + }, + { + "args": [], + "lineno": 67, + "name": "ValueIteratorBase::key" + }, + { + "args": [], + "lineno": 80, + "name": "ValueIteratorBase::index" + }, + { + "args": [], + "lineno": 88, + "name": "ValueIteratorBase::memberName" + }, + { + "args": [ + "Value::ObjectValues::iterator const& current" + ], + "lineno": 96, + "name": "ValueConstIterator::ValueConstIterator" + }, + { + "args": [ + "ValueIteratorBase const& other" + ], + "lineno": 100, + "name": "ValueConstIterator::operator=" + }, + { + "args": [ + "Value::ObjectValues::iterator const& current" + ], + "lineno": 108, + "name": "ValueIterator::ValueIterator" + }, + { + "args": [ + "ValueConstIterator const& other" + ], + "lineno": 112, + "name": "ValueIterator::ValueIterator" + }, + { + "args": [ + "ValueIterator const& other" + ], + "lineno": 115, + "name": "ValueIterator::ValueIterator" + }, + { + "args": [ + "SelfType const& other" + ], + "lineno": 119, + "name": "ValueIterator::operator=" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "Json" + } + ], + "test_coverage_notes": "This code is low-level iterator logic for JSON object traversal. Direct unit tests for these iterators are rare; instead, coverage is typically achieved via higher-level JSON parsing and manipulation tests (e.g., tests that iterate over JSON objects using the library's public API). Test files likely to cover this code include those for JSON parsing, object iteration, and key/index access (e.g., json_value_test.cpp, json_iterator_test.cpp). However, edge cases such as null iterators, empty objects, and non-string keys may not be exhaustively tested unless specifically targeted. Manual null pointer checks are present, but there is no explicit exception or error handling for invalid iterator usage, so robustness relies on upstream code and test coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "manual (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (special case handled)", + "field": "isNull_", + "location": "ValueIteratorBase::computeDistance", + "validated_by": "manual check", + "validates": [ + "Checks if both iterators are null (default constructed)", + "If both are null, returns distance 0" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "isNull_", + "location": "ValueIteratorBase::isEqual", + "validated_by": "manual check", + "validates": [ + "Checks if current iterator is null", + "If so, only returns true if other is also null" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns Value(czString.index()) if null)", + "field": "czString.c_str()", + "location": "ValueIteratorBase::key", + "validated_by": "manual null pointer check", + "validates": [ + "Checks if czString.c_str() is not null before dereferencing" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns czString.index() if null)", + "field": "czString.c_str()", + "location": "ValueIteratorBase::index", + "validated_by": "manual null pointer check", + "validates": [ + "Checks if czString.c_str() is null before returning index" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns empty string if null)", + "field": "(*current_).first.c_str()", + "location": "ValueIteratorBase::memberName", + "validated_by": "manual null pointer check", + "validates": [ + "Checks if member name pointer is null before returning" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/json/json_valueiterator.cpp.ai.md b/src/libxrpl/json/json_valueiterator.cpp.ai.md new file mode 100644 index 0000000000..387a35fd62 --- /dev/null +++ b/src/libxrpl/json/json_valueiterator.cpp.ai.md @@ -0,0 +1,38 @@ +# `json_valueiterator.cpp` — JSON Value Iterator Implementation + +This file implements the iterator infrastructure for traversing `Json::Value` objects in the XRPL codebase. It provides `ValueIteratorBase`, `ValueConstIterator`, and `ValueIterator` — the concrete iterator types that allow range-based and manual iteration over JSON object members and array elements. + +## Inclusion Model + +A notable structural quirk appears immediately in line 1: `// included by json_value.cpp`. This file is not compiled as a standalone translation unit. Instead, it is `#include`-d directly into `json_value.cpp`, making it a logical partition of that larger file rather than an independent compilation unit. This pattern is inherited from the upstream JsonCpp library and keeps the iterator implementation physically separate for readability while avoiding the need to expose internal types across translation unit boundaries. + +## Underlying Data Structure + +All three iterator classes operate over `Value::ObjectValues`, defined as `std::map`. `CZString` is a private inner class of `Value` that acts as a dual-mode key: it holds either a C-string pointer (for named object members) or an integer index (for array elements). The `c_str()` method returns `nullptr` when the key is an integer index, and a valid pointer when it is a string key. This null-pointer discriminant is used throughout the iterator logic to distinguish between object and array traversal. + +## `ValueIteratorBase` + +This CRTP-style base class encapsulates the raw `std::map::iterator` (`current_`) and a boolean `isNull_` flag. The flag is the linchpin of a defensive design: when a `Value` is null (the JSON null type), its `ObjectValues` map does not exist, so any iterator produced for it is default-constructed. Default-constructed `std::map` iterators are not reliably comparable to one another in a portable way — two `begin()`-equivalent defaults cannot be compared using `==` or arithmetic. The `isNull_` flag short-circuits this undefined behavior in two places: + +- **`isEqual()`**: If `isNull_` is true, equality is determined purely by whether the other iterator is also null, bypassing the underlying `current_` comparison entirely. +- **`computeDistance()`**: If both iterators are null, distance is immediately returned as 0 without touching `current_`. + +The `computeDistance()` method also contains an explicit portability note: `std::distance()` was not used because the Sun Studio 12 RogueWave STL (then the default on Solaris) failed to compile it for non-random-access iterators. The hand-rolled linear walk — incrementing from `current_` until reaching `other.current_` — is a deliberate compatibility tradeoff, accepting O(n) distance computation to preserve portability across historical compiler environments. + +## Key Introspection Methods + +`key()`, `index()`, and `memberName()` expose the iterator's current position in a type-safe way by interrogating `CZString`: + +- **`key()`** returns a `Value` that is either a string or an integer, depending on whether `czString.c_str()` is null. When it is a non-null string, the method additionally checks `isStaticString()` to decide whether to wrap it in `StaticString` — avoiding heap allocation for compile-time-constant member names that outlive the value. +- **`index()`** returns `Value::UInt(-1)` as a sentinel when the current position is a string-keyed member rather than an array element. This exploits unsigned wraparound to produce a value that no valid array index can equal, a common C idiom for "not applicable." +- **`memberName()`** returns a `const char*`, guarding against null by returning `""` rather than propagating a null pointer. This prevents callers from crashing on null-dereferencing and preserves the invariant that the returned pointer is always valid. + +## `ValueConstIterator` and `ValueIterator` + +Both concrete classes are thin wrappers over `ValueIteratorBase`. Their constructors, pre/post-increment/decrement operators, and dereference operators are defined in the header (`json_value.h`) as inline methods that delegate directly to the protected base methods `increment()`, `decrement()`, and `deref()`. The `.cpp` file only supplies the non-inline constructors and assignment operators. + +One design asymmetry stands out: `ValueConstIterator::operator=` accepts a `ValueIteratorBase const&` rather than `SelfType const&`. This allows a mutable `ValueIterator` — which inherits from `ValueIteratorBase` — to be implicitly converted to a const iterator through assignment, following the same pattern used by standard library iterator pairs. `ValueIterator`, by contrast, exposes a constructor taking `ValueConstIterator const&`, but this works only at construction time, not via assignment. + +## Error Handling and Invariants + +There is no exception-based error handling in this file. All out-of-bounds or invalid-state conditions are handled by returning sentinel values or short-circuiting comparisons. The responsibility for iterator validity — ensuring `current_` is not advanced past `end()` before dereferencing — lies entirely with the caller. This is consistent with standard C++ iterator semantics and the assumption that higher-level JSON traversal code (such as range-based for loops over `Value::begin()`/`Value::end()`) maintains correctness. \ No newline at end of file diff --git a/src/libxrpl/json/json_writer.cpp.ai.json b/src/libxrpl/json/json_writer.cpp.ai.json new file mode 100644 index 0000000000..6d6b2b10ec --- /dev/null +++ b/src/libxrpl/json/json_writer.cpp.ai.json @@ -0,0 +1,647 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ch" + }, + { + "lineno": 16, + "name": "str" + }, + { + "lineno": 27, + "name": "value" + }, + { + "lineno": 27, + "name": "current" + }, + { + "lineno": 39, + "name": "value" + }, + { + "lineno": 54, + "name": "value" + }, + { + "lineno": 66, + "name": "value" + }, + { + "lineno": 81, + "name": "value" + }, + { + "lineno": 85, + "name": "value" + }, + { + "lineno": 134, + "name": "root" + }, + { + "lineno": 140, + "name": "value" + }, + { + "lineno": 189, + "name": "root" + }, + { + "lineno": 196, + "name": "value" + }, + { + "lineno": 241, + "name": "value" + }, + { + "lineno": 277, + "name": "value" + }, + { + "lineno": 303, + "name": "value" + }, + { + "lineno": 322, + "name": "value" + }, + { + "lineno": 326, + "name": "value" + }, + { + "lineno": 330, + "name": "value" + }, + { + "lineno": 339, + "name": "indentation" + }, + { + "lineno": 343, + "name": "out" + }, + { + "lineno": 343, + "name": "root" + }, + { + "lineno": 350, + "name": "value" + }, + { + "lineno": 395, + "name": "value" + }, + { + "lineno": 431, + "name": "value" + }, + { + "lineno": 457, + "name": "value" + }, + { + "lineno": 478, + "name": "value" + }, + { + "lineno": 482, + "name": "value" + }, + { + "lineno": 486, + "name": "value" + }, + { + "lineno": 492, + "name": "sout" + }, + { + "lineno": 492, + "name": "root" + } + ], + "classes": [ + { + "args": [], + "lineno": 134, + "name": "FastWriter" + }, + { + "args": [], + "lineno": 186, + "name": "StyledWriter" + }, + { + "args": [ + "indentation" + ], + "lineno": 339, + "name": "StyledStreamWriter" + } + ], + "code_paths": [ + { + "call_chain": [ + "valueToQuotedString", + "strpbrk/containsControlCharacter", + "isControlCharacter" + ], + "entry_point": "valueToQuotedString", + "purpose": "Serializes a C-string to a JSON-quoted string, escaping special/control characters.", + "validation_points": [ + "strpbrk(value, ...) checks for special characters", + "containsControlCharacter(value) checks for control characters via isControlCharacter" + ] + }, + { + "call_chain": [ + "valueToString(Int)", + "uintToString", + "XRPL_ASSERT" + ], + "entry_point": "valueToString(Int)", + "purpose": "Converts an integer to a string, handling negatives and buffer overflow.", + "validation_points": [ + "XRPL_ASSERT(current >= buffer) ensures no buffer overflow" + ] + }, + { + "call_chain": [ + "valueToString(UInt)", + "uintToString", + "XRPL_ASSERT" + ], + "entry_point": "valueToString(UInt)", + "purpose": "Converts an unsigned integer to a string, checks buffer bounds.", + "validation_points": [ + "XRPL_ASSERT(current >= buffer) ensures no buffer overflow" + ] + }, + { + "call_chain": [ + "containsControlCharacter", + "isControlCharacter" + ], + "entry_point": "containsControlCharacter", + "purpose": "Checks if a string contains any control characters.", + "validation_points": [ + "isControlCharacter(ch) validates each character" + ] + } + ], + "data_flows": [ + { + "field": "char ch", + "flow": [ + "input to isControlCharacter", + "returns bool" + ], + "origin": "Passed to isControlCharacter (from containsControlCharacter or elsewhere)", + "transformations": [ + "Checks if ch > 0 && ch <= 0x1F" + ], + "validated_at": "isControlCharacter" + }, + { + "field": "char const* str", + "flow": [ + "input to containsControlCharacter", + "iterates over each char", + "calls isControlCharacter" + ], + "origin": "Passed to containsControlCharacter (from valueToQuotedString or elsewhere)", + "transformations": [ + "Iterates string, checks each char" + ], + "validated_at": "containsControlCharacter (calls isControlCharacter)" + }, + { + "field": "char const* value", + "flow": [ + "input to valueToQuotedString", + "checked by strpbrk for special chars", + "checked by containsControlCharacter for control chars", + "if clean, wrapped in quotes", + "if not, escapes applied and wrapped in quotes" + ], + "origin": "Input to valueToQuotedString", + "transformations": [ + "Escaping of special/control characters", + "String concatenation" + ], + "validated_at": "strpbrk and containsControlCharacter" + }, + { + "field": "buffer pointer (char* current)", + "flow": [ + "buffer allocated", + "current initialized to buffer+size", + "uintToString writes backwards", + "XRPL_ASSERT checks current >= buffer", + "returns string from current" + ], + "origin": "Stack buffer in valueToString(Int/UInt)", + "transformations": [ + "Digits written in reverse order", + "Negative sign prepended if needed" + ], + "validated_at": "XRPL_ASSERT" + } + ], + "description": "Implements JSON value-to-string serialization and pretty-printing for the XRPL project's JSON library, including FastWriter, StyledWriter, and StyledStreamWriter for various output formats.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "char ch (input character)", + "empty", + "string", + "validation" + ], + "evidence": "isControlCharacter (static function) at isControlCharacter", + "issue_pattern": "Missing empty string validation for char ch (input character)", + "why_false_positive": "isControlCharacter (static function) validates char ch (input character) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "char ch (input character)", + "range", + "bounds", + "validation" + ], + "evidence": "isControlCharacter (static function) at isControlCharacter", + "issue_pattern": "Missing range validation for char ch (input character)", + "why_false_positive": "isControlCharacter (static function) validates char ch (input character) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "char const* str (input string)", + "empty", + "string", + "validation" + ], + "evidence": "containsControlCharacter (static function) at containsControlCharacter", + "issue_pattern": "Missing empty string validation for char const* str (input string)", + "why_false_positive": "containsControlCharacter (static function) validates char const* str (input string) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "char const* str (input string)", + "format", + "validation", + "invalid" + ], + "evidence": "containsControlCharacter (static function) at containsControlCharacter", + "issue_pattern": "Missing format validation for char const* str (input string)", + "why_false_positive": "containsControlCharacter (static function) validates char const* str (input string) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "buffer overflow (internal buffer pointer)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at valueToString(Int), valueToString(UInt)", + "issue_pattern": "Missing empty string validation for buffer overflow (internal buffer pointer)", + "why_false_positive": "XRPL_ASSERT macro validates buffer overflow (internal buffer pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "char const* value (input string)", + "empty", + "string", + "validation" + ], + "evidence": "strpbrk, containsControlCharacter at valueToQuotedString", + "issue_pattern": "Missing empty string validation for char const* value (input string)", + "why_false_positive": "strpbrk, containsControlCharacter validates char const* value (input string) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "char const* value (input string)", + "format", + "validation", + "invalid" + ], + "evidence": "strpbrk, containsControlCharacter at valueToQuotedString", + "issue_pattern": "Missing format validation for char const* value (input string)", + "why_false_positive": "strpbrk, containsControlCharacter validates char const* value (input string) format" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/json/json_writer.cpp", + "functions": [ + { + "args": [ + "ch" + ], + "lineno": 11, + "name": "isControlCharacter" + }, + { + "args": [ + "str" + ], + "lineno": 16, + "name": "containsControlCharacter" + }, + { + "args": [ + "value", + "current" + ], + "lineno": 27, + "name": "uintToString" + }, + { + "args": [ + "value" + ], + "lineno": 39, + "name": "valueToString" + }, + { + "args": [ + "value" + ], + "lineno": 54, + "name": "valueToString" + }, + { + "args": [ + "value" + ], + "lineno": 66, + "name": "valueToString" + }, + { + "args": [ + "value" + ], + "lineno": 81, + "name": "valueToString" + }, + { + "args": [ + "value" + ], + "lineno": 85, + "name": "valueToQuotedString" + }, + { + "args": [ + "root" + ], + "lineno": 134, + "name": "FastWriter::write" + }, + { + "args": [ + "value" + ], + "lineno": 140, + "name": "FastWriter::writeValue" + }, + { + "args": [], + "lineno": 186, + "name": "StyledWriter::StyledWriter" + }, + { + "args": [ + "root" + ], + "lineno": 189, + "name": "StyledWriter::write" + }, + { + "args": [ + "value" + ], + "lineno": 196, + "name": "StyledWriter::writeValue" + }, + { + "args": [ + "value" + ], + "lineno": 241, + "name": "StyledWriter::writeArrayValue" + }, + { + "args": [ + "value" + ], + "lineno": 277, + "name": "StyledWriter::isMultilineArray" + }, + { + "args": [ + "value" + ], + "lineno": 303, + "name": "StyledWriter::pushValue" + }, + { + "args": [], + "lineno": 311, + "name": "StyledWriter::writeIndent" + }, + { + "args": [ + "value" + ], + "lineno": 322, + "name": "StyledWriter::writeWithIndent" + }, + { + "args": [], + "lineno": 326, + "name": "StyledWriter::indent" + }, + { + "args": [], + "lineno": 330, + "name": "StyledWriter::unindent" + }, + { + "args": [ + "indentation" + ], + "lineno": 339, + "name": "StyledStreamWriter::StyledStreamWriter" + }, + { + "args": [ + "out", + "root" + ], + "lineno": 343, + "name": "StyledStreamWriter::write" + }, + { + "args": [ + "value" + ], + "lineno": 350, + "name": "StyledStreamWriter::writeValue" + }, + { + "args": [ + "value" + ], + "lineno": 395, + "name": "StyledStreamWriter::writeArrayValue" + }, + { + "args": [ + "value" + ], + "lineno": 431, + "name": "StyledStreamWriter::isMultilineArray" + }, + { + "args": [ + "value" + ], + "lineno": 457, + "name": "StyledStreamWriter::pushValue" + }, + { + "args": [], + "lineno": 465, + "name": "StyledStreamWriter::writeIndent" + }, + { + "args": [ + "value" + ], + "lineno": 478, + "name": "StyledStreamWriter::writeWithIndent" + }, + { + "args": [], + "lineno": 482, + "name": "StyledStreamWriter::indent" + }, + { + "args": [], + "lineno": 486, + "name": "StyledStreamWriter::unindent" + }, + { + "args": [ + "sout", + "root" + ], + "lineno": 492, + "name": "operator<<" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "Json" + } + ], + "test_coverage_notes": "The code is low-level and likely tested indirectly via higher-level JSON serialization/deserialization tests. Direct unit tests for valueToString, valueToQuotedString, and control character handling may exist in test suites for the JSON library (e.g., json_writer_test.cpp, json_value_test.cpp). However, buffer overflow checks (XRPL_ASSERT) and edge cases for control characters may not be exhaustively tested. There is no evidence in this file of explicit test hooks or test-only code. Gaps may exist in testing malformed input, very large numbers, or strings with embedded control characters.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "char ch (input character)", + "location": "isControlCharacter", + "validated_by": "isControlCharacter (static function)", + "validates": [ + "Checks if character is a control character (0 < ch <= 0x1F)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "char const* str (input string)", + "location": "containsControlCharacter", + "validated_by": "containsControlCharacter (static function)", + "validates": [ + "Checks if string contains any control characters (calls isControlCharacter on each char)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts program)", + "field": "buffer overflow (internal buffer pointer)", + "location": "valueToString(Int), valueToString(UInt)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that buffer pointer has not underflowed (current >= buffer)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logic)", + "field": "char const* value (input string)", + "location": "valueToQuotedString", + "validated_by": "strpbrk, containsControlCharacter", + "validates": [ + "Checks if string contains any special JSON characters (\", \\, \\b, \\f, \\n, \\r, \\t)", + "Checks if string contains any control characters (via containsControlCharacter)" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/json/json_writer.cpp.ai.md b/src/libxrpl/json/json_writer.cpp.ai.md new file mode 100644 index 0000000000..a8de2cda06 --- /dev/null +++ b/src/libxrpl/json/json_writer.cpp.ai.md @@ -0,0 +1,51 @@ +# `src/libxrpl/json/json_writer.cpp` + +## Role in the System + +This file is the serialization engine for XRPL's embedded JSON library — a modified descendant of JsonCpp. It converts `Json::Value` trees into well-formed JSON text, providing three distinct output strategies suited to different runtime needs. It is the counterpart to `json_reader.cpp` (parsing) and works directly against the `Value` type defined in `json_value.cpp`. + +The public surface is straightforward: `FastWriter` for compact single-line output, `StyledWriter` for human-readable indented strings, and `StyledStreamWriter` for human-readable output directly to a `std::ostream`. The companion file `to_string.cpp` wraps the two string-returning writers behind the convenience functions `Json::to_string()` and `Json::pretty()`. + +## Primitive Serialization Helpers + +The file opens with a small set of standalone conversion functions that form the serialization substrate for all three writers. + +`uintToString()` writes digits in reverse into a stack-allocated 32-byte buffer by walking a pointer from the end toward the front, then returns that pointer as the start of the number string. This avoids heap allocation and string reversal, trading a slightly non-obvious pointer dance for efficiency. `XRPL_ASSERT(current >= buffer)` guards against the impossible case of integer overflow causing the pointer to escape the buffer — impossible in practice since 32 bytes is far larger than any 64-bit integer's decimal representation, but present as a safety net. + +`valueToString(double)` uses `%.16g` format to preserve full double precision without forcing a trailing decimal point. The comment explicitly notes that JSON doesn't distinguish reals from integers in its grammar, so the `#` alternative format flag is unnecessary. + +`valueToQuotedString()` is the most security-relevant helper. It takes a raw C-string and returns a properly JSON-escaped, double-quote-delimited string. The fast path uses `strpbrk` to scan for the common special characters and `containsControlCharacter()` for the U+0001–U+001F range in a single pass; if neither fires, the string is clean and the function simply wraps it in quotes. The slow path allocates a result string pre-reserved to `2 * strlen + 3` bytes (worst-case all characters needing escape, plus surrounding quotes and null), then walks character by character emitting the appropriate two-character JSON escape sequence. Control characters outside the named escapes emit Unicode escapes in the form `\uXXXX` via `std::ostringstream`. Notably, forward slashes are *not* escaped despite the comment acknowledging they could be for JavaScript `= rightMargin_` (74 by default) — meaning three characters per element would already saturate the line — or if any element is a non-empty object or array, the array is immediately considered multi-line. + +2. **Dry-run check**: if the quick check passes, the method needs to know the actual rendered widths of each element. It sets the `addChildValues_` flag to `true` and calls `writeValue()` on each element. When `addChildValues_` is set, `pushValue()` diverts output from `document_` into the `childValues_` vector instead of writing it. After the loop, if the computed total line length exceeds `rightMargin_`, the array becomes multi-line. The `childValues_` vector is then reused during the actual render pass. + +This is a clever re-use of the normal write machinery to perform a speculative measurement run without any special measurement code. The tradeoff is that `writeValue()` can be called twice for each element of a potentially single-line array: once during measurement and once during final output. For the typical short arrays of ledger data this is negligible, but it is worth noting as a potential inefficiency for deeply nested or wide JSON structures. + +The `writeIndent()` implementations differ in a subtle way: `StyledWriter`'s version checks whether the last character in `document_` is already a space or newline to avoid double-indenting, whereas `StyledStreamWriter::writeIndent()` simply unconditionally emits `'\n' + indentString_` — the commented-out block in the stream version acknowledges this simplification was intentional, trading correctness in the "last char was space" edge case for simplicity, which is acceptable since the stream variant is typically used for final human-readable output rather than intermediate composition. + +`StyledStreamWriter` accepts a custom `indentation` string in its constructor (defaulting to `"\t"`), whereas `StyledWriter` hard-codes a fixed `indentSize_` of 3 spaces. This makes `StyledStreamWriter` more flexible for consumers who need configurable indentation. + +## `operator<<` and the Header's `detail::write_value` + +The free `operator<<(std::ostream&, Value const&)` overload at the bottom of the file makes any `Json::Value` streamable, delegating to `StyledStreamWriter` with default tab indentation. This is the format used when values appear in log output. + +The header (`json_writer.h`) also defines a template `detail::write_value` and the `Json::Compact` wrapper class. These provide a third compact-output pathway that bypasses the class hierarchy entirely: `write_value` is a function template parameterized on a write callable with signature `void(void const*, std::size_t)`, enabling zero-overhead integration with websocket send buffers or other scatter-gather I/O without constructing intermediate strings. `Json::Compact` is a move-only wrapper that enables `out << Json::Compact{std::move(jv)}` syntax for inserting compact JSON into a stream without the default styled formatting — a neat design that avoids naming conflicts with `operator<<` for full `Value` objects while still reusing the stream insertion idiom. + +## Design Notes + +The three-class hierarchy balances competing concerns: `FastWriter` is stateful but reusable (reset on each `write()` call), `StyledWriter` similarly resets per call, and `StyledStreamWriter` avoids string accumulation entirely for large documents. The `WriterBase` abstract interface is defined in the header for `FastWriter` and `StyledWriter` but deliberately omitted for `StyledStreamWriter` — as the header comment explains, a stream writer's `write()` must accept an `ostream&` parameter and cannot meaningfully return `std::string`, so forcing it into the `WriterBase` hierarchy would require an awkward adapter. The XRPL codebase handles this by exposing the stream writer independently and providing the streaming `operator<<` as the primary integration point. \ No newline at end of file diff --git a/src/libxrpl/json/to_string.cpp.ai.json b/src/libxrpl/json/to_string.cpp.ai.json new file mode 100644 index 0000000000..58a3c85228 --- /dev/null +++ b/src/libxrpl/json/to_string.cpp.ai.json @@ -0,0 +1,84 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Json::to_string", + "FastWriter::write" + ], + "entry_point": "Json::to_string", + "purpose": "Serializes a Json::Value to a compact JSON string.", + "validation_points": [ + "Any validation would occur inside FastWriter::write, not in to_string itself." + ] + }, + { + "call_chain": [ + "Json::pretty", + "StyledWriter::write" + ], + "entry_point": "Json::pretty", + "purpose": "Serializes a Json::Value to a pretty-printed JSON string.", + "validation_points": [ + "Any validation would occur inside StyledWriter::write, not in pretty itself." + ] + } + ], + "data_flows": [ + { + "field": "value (Json::Value const&)", + "flow": [ + "Caller provides Json::Value", + "Passed by const reference to to_string/pretty", + "Forwarded to FastWriter::write or StyledWriter::write", + "Serialized to std::string and returned" + ], + "origin": "Caller of Json::to_string or Json::pretty", + "transformations": [ + "No transformation in to_string/pretty; transformation (serialization) occurs in Writer::write" + ], + "validated_at": "No validation in to_string/pretty; any validation would be inside Writer::write" + } + ], + "description": "Provides utility functions to convert a Json::Value to its string representation, either compact or pretty-printed.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/json/to_string.cpp", + "functions": [ + { + "args": [ + "value" + ], + "lineno": 7, + "name": "to_string" + }, + { + "args": [ + "value" + ], + "lineno": 12, + "name": "pretty" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "Json" + } + ], + "test_coverage_notes": "This file contains only thin wrappers around JSON serialization. There is no validation logic in these functions; they delegate to FastWriter and StyledWriter. Test coverage for these wrappers would be minimal and likely indirect, via tests that check JSON output formatting. Tests would exist in higher-level code that uses Json::to_string/pretty, or in unit tests for the JSON writer classes themselves (FastWriter, StyledWriter). There are likely no direct tests for validation in this file, and any validation coverage would depend on the underlying JSON library's tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/json/to_string.cpp.ai.md b/src/libxrpl/json/to_string.cpp.ai.md new file mode 100644 index 0000000000..0624000478 --- /dev/null +++ b/src/libxrpl/json/to_string.cpp.ai.md @@ -0,0 +1,14 @@ +# `src/libxrpl/json/to_string.cpp` + +This file provides the two canonical entry points for serializing a `Json::Value` to a `std::string` within the XRPL JSON library: `to_string` for compact output and `pretty` for human-readable output. Both functions live in the `Json` namespace and are declared in `include/xrpl/json/to_string.h`. + +The implementation is intentionally minimal — each function simply constructs a writer object on the stack and immediately calls its `write` method: + +- `to_string` delegates to `FastWriter`, which emits the entire JSON document on a single line with no extraneous whitespace. This is the workhorse serializer used wherever bandwidth or parse overhead matters, such as network RPC responses and log output. +- `pretty` delegates to `StyledWriter`, which applies indentation (3 spaces per level) and a right-margin heuristic (74 characters) to decide when arrays should break across lines. It is intended for diagnostic output and human inspection. + +Both writer classes inherit from `WriterBase` and encapsulate their intermediate state (the accumulating `document_` string, indentation tracking, child value buffers) as private member data. Constructing them on the stack per call keeps each serialization operation stateless and thread-safe — there is no shared mutable state between calls. + +A third, lower-level path also exists in `json_writer.h`: the `detail::write_value` template and the `stream()` / `Compact` helpers bypass the writer class hierarchy entirely, writing chunk-by-chunk to an arbitrary callable. That path is used when the caller owns the output sink (e.g., a `Beast` HTTP stream). `to_string` and `pretty` are the simpler, string-returning façade over the same underlying value-traversal logic. + +No validation occurs in either function; all type-dispatch and escaping happen inside `FastWriter::writeValue` and `StyledWriter::writeValue` respectively. The `to_string.cpp` layer is purely a naming and convenience concern. \ No newline at end of file diff --git a/src/libxrpl/ledger/AcceptedLedgerTx.cpp.ai.json b/src/libxrpl/ledger/AcceptedLedgerTx.cpp.ai.json new file mode 100644 index 0000000000..ada608c7f5 --- /dev/null +++ b/src/libxrpl/ledger/AcceptedLedgerTx.cpp.ai.json @@ -0,0 +1,546 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ledger" + }, + { + "lineno": 11, + "name": "txn" + }, + { + "lineno": 12, + "name": "met" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AcceptedLedgerTx::AcceptedLedgerTx", + "XRPL_ASSERT(!ledger->open())", + "txn->getTransactionID()", + "ledger->seq()", + "*met", + "met->add(s)", + "mTxn->getJson()", + "mMeta.getJson()", + "mMeta.getResultTER()", + "mMeta.getAffectedAccounts()", + "mTxn->getTxnType()", + "mTxn->getAccountID(sfAccount)", + "mTxn->getFieldAmount(sfTakerGets)", + "accountFunds()" + ], + "entry_point": "AcceptedLedgerTx::AcceptedLedgerTx", + "purpose": "Constructs an AcceptedLedgerTx object, validates ledger state, serializes metadata, builds JSON representation, and (for OfferCreate) computes owner funds.", + "validation_points": [ + "XRPL_ASSERT(!ledger->open())", + "C++ type system for txn, ledger, met (shared_ptr const&)", + "C++ type system for txn->getTransactionID(), ledger->seq(), *met", + "mTxn->getTxnType() == ttOFFER_CREATE (enum/type check)" + ] + }, + { + "call_chain": [ + "AcceptedLedgerTx::getEscMeta", + "XRPL_ASSERT(!mRawMeta.empty())", + "sqlBlobLiteral(mRawMeta)" + ], + "entry_point": "AcceptedLedgerTx::getEscMeta", + "purpose": "Returns SQL-escaped metadata blob, ensuring metadata is present.", + "validation_points": [ + "XRPL_ASSERT(!mRawMeta.empty())" + ] + } + ], + "data_flows": [ + { + "field": "ledger", + "flow": [ + "constructor argument", + "XRPL_ASSERT(!ledger->open())", + "ledger->seq()", + "accountFunds(*ledger, ...)" + ], + "origin": "AcceptedLedgerTx constructor argument (std::shared_ptr const&)", + "transformations": [ + "Checked for open() state", + "Sequence number extracted", + "Passed to accountFunds" + ], + "validated_at": "XRPL_ASSERT(!ledger->open())" + }, + { + "field": "txn", + "flow": [ + "constructor argument", + "txn->getTransactionID()", + "txn->getJson()", + "txn->getTxnType()", + "txn->getAccountID(sfAccount)", + "txn->getFieldAmount(sfTakerGets)" + ], + "origin": "AcceptedLedgerTx constructor argument (std::shared_ptr const&)", + "transformations": [ + "Transaction ID extracted", + "JSON representation generated", + "Transaction type checked", + "Account and amount fields extracted" + ], + "validated_at": "C++ type system (shared_ptr, STTx methods), enum/type check for getTxnType()" + }, + { + "field": "met", + "flow": [ + "constructor argument", + "met->add(s)", + "used in mMeta (AcceptedLedgerTxMeta)" + ], + "origin": "AcceptedLedgerTx constructor argument (std::shared_ptr const&)", + "transformations": [ + "Serialized into mRawMeta", + "Used to construct mMeta" + ], + "validated_at": "C++ type system (shared_ptr, STObject methods)" + }, + { + "field": "mRawMeta", + "flow": [ + "constructed from met->add(s)", + "assigned to mRawMeta", + "used in mJson[jss::raw_meta]", + "used in getEscMeta()" + ], + "origin": "Serializer s; met->add(s); s.modData()", + "transformations": [ + "Serialized to binary", + "Hex-encoded for JSON", + "SQL-escaped for getEscMeta" + ], + "validated_at": "XRPL_ASSERT(!mRawMeta.empty()) in getEscMeta" + }, + { + "field": "mJson", + "flow": [ + "initialized as Json::objectValue", + "populated with transaction, meta, raw_meta, result, affected, owner_funds" + ], + "origin": "Built in AcceptedLedgerTx constructor", + "transformations": [ + "Populated with various fields and transformations" + ], + "validated_at": "Indirectly via validations on source fields" + }, + { + "field": "mAffected", + "flow": [ + "extracted from mMeta", + "used to populate mJson[jss::affected]" + ], + "origin": "mMeta.getAffectedAccounts()", + "transformations": [ + "Converted to base58 for JSON" + ], + "validated_at": "C++ type system for mMeta" + } + ], + "description": "Implements the AcceptedLedgerTx class for the XRPL, representing a transaction accepted into a ledger, including its metadata, affected accounts, and JSON serialization.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "transaction type (via getTxnType)", + "validation", + "missing", + "check" + ], + "evidence": "Field transaction type (via getTxnType) validated by XRPL custom (XRPL_ASSERT, jss:: for JSON keys, C++ type system)", + "issue_pattern": "Missing validation for transaction type (via getTxnType)", + "why_false_positive": "XRPL custom (XRPL_ASSERT, jss:: for JSON keys, C++ type system) validates transaction type (via getTxnType) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "account fields (via getAccountID, getFieldAmount)", + "validation", + "missing", + "check" + ], + "evidence": "Field account fields (via getAccountID, getFieldAmount) validated by XRPL custom (XRPL_ASSERT, jss:: for JSON keys, C++ type system)", + "issue_pattern": "Missing validation for account fields (via getAccountID, getFieldAmount)", + "why_false_positive": "XRPL custom (XRPL_ASSERT, jss:: for JSON keys, C++ type system) validates account fields (via getAccountID, getFieldAmount) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "metadata presence (via XRPL_ASSERT)", + "validation", + "missing", + "check" + ], + "evidence": "Field metadata presence (via XRPL_ASSERT) validated by XRPL custom (XRPL_ASSERT, jss:: for JSON keys, C++ type system)", + "issue_pattern": "Missing validation for metadata presence (via XRPL_ASSERT)", + "why_false_positive": "XRPL custom (XRPL_ASSERT, jss:: for JSON keys, C++ type system) validates metadata presence (via XRPL_ASSERT) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger state (via XRPL_ASSERT)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger state (via XRPL_ASSERT) validated by XRPL custom (XRPL_ASSERT, jss:: for JSON keys, C++ type system)", + "issue_pattern": "Missing validation for ledger state (via XRPL_ASSERT)", + "why_false_positive": "XRPL custom (XRPL_ASSERT, jss:: for JSON keys, C++ type system) validates ledger state (via XRPL_ASSERT) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger->open()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at AcceptedLedgerTx constructor", + "issue_pattern": "Missing empty string validation for ledger->open()", + "why_false_positive": "XRPL_ASSERT validates ledger->open() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mRawMeta.empty()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at getEscMeta", + "issue_pattern": "Missing empty string validation for mRawMeta.empty()", + "why_false_positive": "XRPL_ASSERT validates mRawMeta.empty() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "txn, ledger, met (constructor arguments)", + "empty", + "string", + "validation" + ], + "evidence": "C++ type system (std::shared_ptr const&) at AcceptedLedgerTx constructor", + "issue_pattern": "Missing empty string validation for txn, ledger, met (constructor arguments)", + "why_false_positive": "C++ type system (std::shared_ptr const&) validates txn, ledger, met (constructor arguments) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "txn, ledger, met (constructor arguments)", + "type", + "validation", + "check" + ], + "evidence": "C++ type system (std::shared_ptr const&) at AcceptedLedgerTx constructor", + "issue_pattern": "Missing type validation for txn, ledger, met (constructor arguments)", + "why_false_positive": "C++ type system (std::shared_ptr const&) validates txn, ledger, met (constructor arguments) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "txn->getTransactionID(), ledger->seq(), *met", + "empty", + "string", + "validation" + ], + "evidence": "C++ type system, STTx/STObject methods at AcceptedLedgerTx constructor (mMeta initialization)", + "issue_pattern": "Missing empty string validation for txn->getTransactionID(), ledger->seq(), *met", + "why_false_positive": "C++ type system, STTx/STObject methods validates txn->getTransactionID(), ledger->seq(), *met for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "txn->getTransactionID(), ledger->seq(), *met", + "type", + "validation", + "check" + ], + "evidence": "C++ type system, STTx/STObject methods at AcceptedLedgerTx constructor (mMeta initialization)", + "issue_pattern": "Missing type validation for txn->getTransactionID(), ledger->seq(), *met", + "why_false_positive": "C++ type system, STTx/STObject methods validates txn->getTransactionID(), ledger->seq(), *met type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mTxn->getTxnType() == ttOFFER_CREATE", + "empty", + "string", + "validation" + ], + "evidence": "C++ enum/type check at AcceptedLedgerTx constructor (conditional block)", + "issue_pattern": "Missing empty string validation for mTxn->getTxnType() == ttOFFER_CREATE", + "why_false_positive": "C++ enum/type check validates mTxn->getTxnType() == ttOFFER_CREATE for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account != amount.getIssuer()", + "empty", + "string", + "validation" + ], + "evidence": "C++ operator==, AccountID/Issuer type at AcceptedLedgerTx constructor (OFFER_CREATE block)", + "issue_pattern": "Missing empty string validation for account != amount.getIssuer()", + "why_false_positive": "C++ operator==, AccountID/Issuer type validates account != amount.getIssuer() for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/AcceptedLedgerTx.cpp", + "functions": [ + { + "args": [ + "ledger", + "txn", + "met" + ], + "lineno": 9, + "name": "AcceptedLedgerTx" + }, + { + "args": [], + "lineno": 54, + "name": "getEscMeta" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "AcceptedLedgerTx is a core ledger class, likely tested in integration and unit tests. Typical test files: 'test/ledger/AcceptedLedgerTx_test.cpp', 'test/ledger/Transaction_test.cpp', or higher-level transaction/ledger tests. Validations using XRPL_ASSERT may not be directly tested unless assertions are enabled in test builds. Data flow through JSON and meta serialization is likely covered by transaction application and ledger acceptance tests. Gaps: Direct assertion failures (e.g., ledger->open() == true) may not be tested unless negative tests are written. Edge cases for malformed meta or empty mRawMeta may not be fully covered.", + "validation_architecture": { + "auto_validated_fields": [ + "transaction type (via getTxnType)", + "account fields (via getAccountID, getFieldAmount)", + "metadata presence (via XRPL_ASSERT)", + "ledger state (via XRPL_ASSERT)" + ], + "framework": "XRPL custom (XRPL_ASSERT, jss:: for JSON keys, C++ type system)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "ledger->open()", + "location": "AcceptedLedgerTx constructor", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures the ledger is not open (must be a closed/finalized ledger)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "mRawMeta.empty()", + "location": "getEscMeta", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that mRawMeta (serialized metadata) is not empty before use" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "undefined behavior if null, but not explicitly checked here", + "field": "txn, ledger, met (constructor arguments)", + "location": "AcceptedLedgerTx constructor", + "validated_by": "C++ type system (std::shared_ptr const&)", + "validates": [ + "Ensures arguments are shared pointers to expected types" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "undefined behavior if null, but not explicitly checked here", + "field": "txn->getTransactionID(), ledger->seq(), *met", + "location": "AcceptedLedgerTx constructor (mMeta initialization)", + "validated_by": "C++ type system, STTx/STObject methods", + "validates": [ + "Ensures that txn, ledger, and met are valid objects with required methods" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (just conditional logic)", + "field": "mTxn->getTxnType() == ttOFFER_CREATE", + "location": "AcceptedLedgerTx constructor (conditional block)", + "validated_by": "C++ enum/type check", + "validates": [ + "Checks if transaction type is OFFER_CREATE before accessing offer-specific fields" + ], + "validation_type": "type/business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (just conditional logic)", + "field": "account != amount.getIssuer()", + "location": "AcceptedLedgerTx constructor (OFFER_CREATE block)", + "validated_by": "C++ operator==, AccountID/Issuer type", + "validates": [ + "Checks if offer is not self-funded before calculating owner funds" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/AcceptedLedgerTx.cpp.ai.md b/src/libxrpl/ledger/AcceptedLedgerTx.cpp.ai.md new file mode 100644 index 0000000000..278e14b12f --- /dev/null +++ b/src/libxrpl/ledger/AcceptedLedgerTx.cpp.ai.md @@ -0,0 +1,35 @@ +# `AcceptedLedgerTx.cpp` — Transaction Envelope for Closed-Ledger Propagation + +## Role in the System + +`AcceptedLedgerTx` represents a single transaction that has been accepted into a closed (finalized) ledger. Its purpose is not transaction execution — that happens in the consensus and apply pipeline — but rather **post-acceptance packaging**: assembling all the information downstream consumers need to act on a confirmed transaction. + +Two major consumers drive the design. First, `NetworkOPsImp::pubValidatedTransaction()` and `pubAccountTransaction()` use the pre-built `mJson` payload to push events over WebSocket subscriptions without reconstructing JSON on every subscriber delivery. Second, the relational database backend (`Node.cpp`) calls `getEscMeta()` to obtain SQL-safe binary metadata for persistence. Both access patterns favor construction-time serialization over lazy computation, which is exactly what the constructor does. + +## Constructor Logic + +The constructor takes three `shared_ptr` inputs — the closed ledger view, the serialized transaction (`STTx`), and the raw metadata `STObject` — and fully materializes the object in one pass. + +The first thing it does is assert `!ledger->open()`. This is a hard invariant: `AcceptedLedgerTx` is only meaningful for finalized ledgers. An open ledger has no authoritative transaction ordering or result codes yet, so constructing this wrapper for an open ledger would produce incorrect metadata. The `XRPL_ASSERT` (rather than a thrown exception) reflects that callers in this path are trusted internal code where a violation indicates a programming error rather than bad input. + +`TxMeta` is constructed inline from the transaction ID, ledger sequence, and dereferenced metadata `STObject`. This is the parsed, structured representation of metadata (affected nodes, result code, delivery amounts). In parallel, the raw bytes of `met` are also captured via a `Serializer` pass into `mRawMeta`. Keeping both forms avoids re-serializing later: the `STObject` form serves `getJson()`, while the binary blob serves `getEscMeta()`. + +The JSON payload assembled in `mJson` is intentionally comprehensive. It embeds the transaction (`jss::transaction`), its parsed metadata (`jss::meta`), the hex-encoded raw metadata (`jss::raw_meta`), the human-readable result string (`jss::result`), and the set of affected accounts (`jss::affected`) in base58 form. This is the exact envelope that WebSocket subscription clients receive. + +## The OfferCreate Owner Funds Special Case + +The most interesting logic is the `ttOFFER_CREATE` branch. For offer creation transactions where the offer is not self-funded (i.e., the transaction's account is not the issuer of the asset being offered), the constructor queries `accountFunds()` against the closed ledger to compute the account's actual spendable balance of the asset at the time of acceptance. + +This `owner_funds` field is injected directly into `mJson[jss::transaction]` and exists specifically to help clients and order book subscribers assess whether an offer is fully funded at the moment of its creation. It is not part of the ledger state itself — it is a read-time annotation added to the event. The `fhIGNORE_FREEZE` and `ahIGNORE_AUTH` flags passed to `accountFunds()` indicate that this balance query intentionally bypasses trust-line freeze checks and authorization requirements, reporting the raw economic balance rather than the effective spendable amount under compliance restrictions. The `beast::Journal::getNullSink()` suppresses diagnostic output, consistent with this being a non-critical annotation rather than a protocol-required computation. + +The self-funded exclusion (`account != amount.getIssuer()`) avoids a redundant query: when an account creates an offer to sell its own issued currency, its "balance" of that asset is unbounded (it can issue freely), so the `owner_funds` annotation would be meaningless. + +## `getEscMeta()` + +`getEscMeta()` returns `mRawMeta` formatted as a SQL blob literal via `sqlBlobLiteral()`. It asserts that `mRawMeta` is non-empty before doing so — the metadata binary is populated unconditionally in the constructor, so a non-empty check failing would indicate object construction was somehow bypassed. The result is used verbatim in SQL `INSERT`/`REPLACE` statements for the transaction database (see `Node.cpp`). + +## Design Tradeoffs + +The choice to serialize everything at construction time (JSON, raw bytes, affected accounts) trades memory for CPU efficiency and simplicity: the object is immutable after construction, it can be shared freely across threads without locks, and every accessor is a trivial `const` reference return. The `CountedObject` base class provides cheap live-instance telemetry without affecting behavior. + +The `boost::container::flat_set` for `mAffected` is a deliberate space-time tradeoff: a sorted contiguous array is more cache-friendly than `std::set` for the small sets typical of XRPL transactions, and it supports efficient iteration for the subscription notification fan-out in `pubAccountTransaction()`. \ No newline at end of file diff --git a/src/libxrpl/ledger/ApplyStateTable.cpp.ai.json b/src/libxrpl/ledger/ApplyStateTable.cpp.ai.json new file mode 100644 index 0000000000..adb124f65f --- /dev/null +++ b/src/libxrpl/ledger/ApplyStateTable.cpp.ai.json @@ -0,0 +1,597 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ApplyStateTable::apply(RawView& to) const", + "to.rawDestroyXRP", + "for (item in items_)", + "switch (item.second.first)", + "to.rawErase/rawInsert/rawReplace" + ], + "entry_point": "ApplyStateTable::apply(RawView& to) const", + "purpose": "Applies all pending state changes in items_ to the RawView ledger, dispatching by Action enum.", + "validation_points": [ + "switch (item.second.first) // Validates Action enum value" + ] + }, + { + "call_chain": [ + "ApplyStateTable::apply(OpenView& to, ...)", + "if (!to.open() || isDryRun)", + "for (item in items_)", + "switch (item.second.first)", + "to.read(keylet::unchecked(item.first))", + "meta.setDeliveredAmount(deliver)", + "meta.setParentBatchID(parentBatchId)" + ], + "entry_point": "ApplyStateTable::apply(OpenView& to, STTx const& tx, TER ter, std::optional const& deliver, std::optional const& parentBatchId, bool isDryRun, beast::Journal j)", + "purpose": "Applies state changes and builds transaction metadata, including delivered amount and parent batch ID if present.", + "validation_points": [ + "to.open() // Validates ledger is open", + "std::optional presence check for deliver", + "std::optional presence check for parentBatchId", + "switch (item.second.first) // Validates Action enum value" + ] + }, + { + "call_chain": [ + "ApplyStateTable::visit(ReadView const& to, func)", + "for (item in items_)", + "switch (item.second.first)", + "func(...)" + ], + "entry_point": "ApplyStateTable::visit(ReadView const& to, std::function...) const", + "purpose": "Iterates over state changes and invokes a callback for each, passing before/after SLEs.", + "validation_points": [ + "switch (item.second.first) // Validates Action enum value" + ] + } + ], + "data_flows": [ + { + "field": "item.second.first (Action enum)", + "flow": [ + "items_", + "for (item in items_)", + "switch (item.second.first)", + "dispatch to rawErase/rawInsert/rawReplace or callback" + ], + "origin": "items_ container (ApplyStateTable member)", + "transformations": [ + "Used to determine which ledger operation to perform" + ], + "validated_at": "switch (item.second.first) in all main methods" + }, + { + "field": "deliver (std::optional)", + "flow": [ + "function argument", + "meta.setDeliveredAmount(deliver)" + ], + "origin": "ApplyStateTable::apply(OpenView&, ...)", + "transformations": [ + "Optional: only set if present" + ], + "validated_at": "Presence checked before use (std::optional)" + }, + { + "field": "parentBatchId (std::optional)", + "flow": [ + "function argument", + "meta.setParentBatchID(parentBatchId)" + ], + "origin": "ApplyStateTable::apply(OpenView&, ...)", + "transformations": [ + "Optional: only set if present" + ], + "validated_at": "Presence checked before use (std::optional)" + }, + { + "field": "to.open()", + "flow": [ + "ApplyStateTable::apply(OpenView&, ...)", + "if (!to.open() || isDryRun)" + ], + "origin": "OpenView& to (function argument)", + "transformations": [ + "Boolean check to determine if ledger is open" + ], + "validated_at": "Method call returns bool, used in conditional" + }, + { + "field": "items_ (container of state changes)", + "flow": [ + "ApplyStateTable instance", + "for (item in items_)", + "switch (item.second.first)", + "used in ledger operations or callbacks" + ], + "origin": "ApplyStateTable member, populated elsewhere", + "transformations": [ + "Iterated, filtered by Action, passed to ledger ops" + ], + "validated_at": "Switch on Action in all main methods" + } + ], + "description": "Implements the ApplyStateTable class and related functions for managing and applying state changes to the XRPL ledger, including inserting, modifying, erasing, and threading ledger entries, as well as generating transaction metadata.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Action (enum type)", + "validation", + "missing", + "check" + ], + "evidence": "Field Action (enum type) validated by C++ type system, enum, std::optional", + "issue_pattern": "Missing validation for Action (enum type)", + "why_false_positive": "C++ type system, enum, std::optional validates Action (enum type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "items_ (container type)", + "validation", + "missing", + "check" + ], + "evidence": "Field items_ (container type) validated by C++ type system, enum, std::optional", + "issue_pattern": "Missing validation for items_ (container type)", + "why_false_positive": "C++ type system, enum, std::optional validates items_ (container type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "deliver (std::optional)", + "validation", + "missing", + "check" + ], + "evidence": "Field deliver (std::optional) validated by C++ type system, enum, std::optional", + "issue_pattern": "Missing validation for deliver (std::optional)", + "why_false_positive": "C++ type system, enum, std::optional validates deliver (std::optional) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "parentBatchId (std::optional)", + "validation", + "missing", + "check" + ], + "evidence": "Field parentBatchId (std::optional) validated by C++ type system, enum, std::optional", + "issue_pattern": "Missing validation for parentBatchId (std::optional)", + "why_false_positive": "C++ type system, enum, std::optional validates parentBatchId (std::optional) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Action enum value (item.second.first)", + "empty", + "string", + "validation" + ], + "evidence": "switch statement (enum type) at ApplyStateTable::apply(RawView& to) and others", + "issue_pattern": "Missing empty string validation for Action enum value (item.second.first)", + "why_false_positive": "switch statement (enum type) validates Action enum value (item.second.first) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "items_ (container of state changes)", + "empty", + "string", + "validation" + ], + "evidence": "iteration and switch on Action at ApplyStateTable::apply, ::size, ::visit, ::apply(OpenView&...)", + "issue_pattern": "Missing empty string validation for items_ (container of state changes)", + "why_false_positive": "iteration and switch on Action validates items_ (container of state changes) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "deliver (optional STAmount)", + "empty", + "string", + "validation" + ], + "evidence": "std::optional presence check at ApplyStateTable::apply(OpenView&, ...)", + "issue_pattern": "Missing empty string validation for deliver (optional STAmount)", + "why_false_positive": "std::optional presence check validates deliver (optional STAmount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "parentBatchId (optional uint256)", + "empty", + "string", + "validation" + ], + "evidence": "std::optional presence check at ApplyStateTable::apply(OpenView&, ...)", + "issue_pattern": "Missing empty string validation for parentBatchId (optional uint256)", + "why_false_positive": "std::optional presence check validates parentBatchId (optional uint256) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "to.open()", + "empty", + "string", + "validation" + ], + "evidence": "method call (returns bool) at ApplyStateTable::apply(OpenView&, ...)", + "issue_pattern": "Missing empty string validation for to.open()", + "why_false_positive": "method call (returns bool) validates to.open() for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/ApplyStateTable.cpp", + "functions": [ + { + "args": [ + "RawView& to" + ], + "lineno": 9, + "name": "ApplyStateTable::apply" + }, + { + "args": [], + "lineno": 25, + "name": "ApplyStateTable::size" + }, + { + "args": [ + "ReadView const& to", + "std::function const& before, std::shared_ptr const& after)> const& func" + ], + "lineno": 39, + "name": "ApplyStateTable::visit" + }, + { + "args": [ + "OpenView& to", + "STTx const& tx", + "TER ter", + "std::optional const& deliver", + "std::optional const& parentBatchId", + "bool isDryRun", + "beast::Journal j" + ], + "lineno": 61, + "name": "ApplyStateTable::apply" + }, + { + "args": [ + "ReadView const& base", + "Keylet const& k" + ], + "lineno": 181, + "name": "ApplyStateTable::exists" + }, + { + "args": [ + "ReadView const& base", + "key_type const& key", + "std::optional const& last" + ], + "lineno": 196, + "name": "ApplyStateTable::succ" + }, + { + "args": [ + "ReadView const& base", + "Keylet const& k" + ], + "lineno": 221, + "name": "ApplyStateTable::read" + }, + { + "args": [ + "ReadView const& base", + "Keylet const& k" + ], + "lineno": 236, + "name": "ApplyStateTable::peek" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 259, + "name": "ApplyStateTable::erase" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 277, + "name": "ApplyStateTable::rawErase" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 295, + "name": "ApplyStateTable::insert" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 314, + "name": "ApplyStateTable::replace" + }, + { + "args": [ + "ReadView const& base", + "std::shared_ptr const& sle" + ], + "lineno": 332, + "name": "ApplyStateTable::update" + }, + { + "args": [ + "XRPAmount const& fee" + ], + "lineno": 349, + "name": "ApplyStateTable::destroyXRP" + }, + { + "args": [ + "TxMeta& meta", + "std::shared_ptr const& sle" + ], + "lineno": 356, + "name": "ApplyStateTable::threadItem" + }, + { + "args": [ + "ReadView const& base", + "key_type const& key", + "Mods& mods", + "beast::Journal j" + ], + "lineno": 377, + "name": "ApplyStateTable::getForMod" + }, + { + "args": [ + "ReadView const& base", + "TxMeta& meta", + "AccountID const& to", + "Mods& mods", + "beast::Journal j" + ], + "lineno": 406, + "name": "ApplyStateTable::threadTx" + }, + { + "args": [ + "ReadView const& base", + "TxMeta& meta", + "std::shared_ptr const& sle", + "Mods& mods", + "beast::Journal j" + ], + "lineno": 423, + "name": "ApplyStateTable::threadOwners" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + }, + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + }, + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + }, + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + }, + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "detail" + } + ], + "test_coverage_notes": "Core ApplyStateTable logic is likely tested in integration and unit tests for ledger application and transaction processing. Look for tests in files such as 'ApplyStateTable_test.cpp', 'Ledger_test.cpp', or 'Transactor_test.cpp' in the rippled codebase. However, edge cases for optional fields (deliver, parentBatchId), and all Action enum branches (especially 'cache') may not be fully covered. Direct tests for validation failures (e.g., invalid Action values, missing nodes on erase) may be limited or absent.", + "validation_architecture": { + "auto_validated_fields": [ + "Action (enum type)", + "items_ (container type)", + "deliver (std::optional)", + "parentBatchId (std::optional)" + ], + "framework": "C++ type system, enum, std::optional", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "None (default case is break/continue)", + "field": "Action enum value (item.second.first)", + "location": "ApplyStateTable::apply(RawView& to) and others", + "validated_by": "switch statement (enum type)", + "validates": [ + "Ensures only valid Action enum values are processed", + "Unknown values are ignored" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "None (invalid actions are skipped)", + "field": "items_ (container of state changes)", + "location": "ApplyStateTable::apply, ::size, ::visit, ::apply(OpenView&...)", + "validated_by": "iteration and switch on Action", + "validates": [ + "Only valid actions (erase, insert, modify) are counted or processed", + "cache is ignored" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "None (optional, not present is handled)", + "field": "deliver (optional STAmount)", + "location": "ApplyStateTable::apply(OpenView&, ...)", + "validated_by": "std::optional presence check", + "validates": [ + "Checks if deliver amount is present before using" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "None (optional, not present is handled)", + "field": "parentBatchId (optional uint256)", + "location": "ApplyStateTable::apply(OpenView&, ...)", + "validated_by": "std::optional presence check", + "validates": [ + "Checks if parentBatchId is present before using" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "None (branching logic)", + "field": "to.open()", + "location": "ApplyStateTable::apply(OpenView&, ...)", + "validated_by": "method call (returns bool)", + "validates": [ + "Checks if the OpenView is open before proceeding" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/ApplyStateTable.cpp.ai.md b/src/libxrpl/ledger/ApplyStateTable.cpp.ai.md new file mode 100644 index 0000000000..8909dfb26d --- /dev/null +++ b/src/libxrpl/ledger/ApplyStateTable.cpp.ai.md @@ -0,0 +1,71 @@ +# `ApplyStateTable` — Transaction Ledger Write Buffer + +`ApplyStateTable` is the write-staging layer that every XRPL transaction application relies on. It sits inside `xrpl::detail` and is the core member of `ApplyViewBase`, which in turn backs all the `ApplyView` and `ApplyViewImpl` types that transactors receive at execution time. Its purpose is to accumulate all ledger mutations a transaction wishes to make — without touching the real ledger — and then either commit them atomically or discard them if the transaction fails. + +## The Central Data Structure + +The entire class revolves around one map: + +```cpp +using items_t = std::map>>; +items_t items_; +``` + +Every ledger object (`SLE`) that the current transaction touches is represented here, keyed by its `uint256` ledger key. The `Action` enum (private to the class) records the fate of each entry: + +- `cache` — read from the base ledger and available for writes, but not yet marked dirty +- `insert` — a new object to be added +- `modify` — an existing object that has been mutated +- `erase` — an object to be deleted + +This four-state design lets the system distinguish a clean read (`cache`) from an actual write intent (`modify`). Callers receive a mutable `shared_ptr` from `peek()`, which enters the map as `cache`. Only when `update()` is called on it does the action upgrade to `modify`, ensuring no spurious write metadata is generated for objects that were merely inspected. If a key is erased and then re-inserted within the same transaction, `insert()` correctly detects the prior `erase` action and collapses the transition into a `modify` — the net effect for the base ledger is a replacement. + +A second field, `dropsDestroyed_`, tracks XRP taken permanently out of circulation by transaction fees within this transaction's scope. + +## Two Flavors of `apply()` + +The class has two `apply()` overloads with very different roles. + +`apply(RawView& to)` is the simple flush. It iterates `items_` and maps each action to a raw write: `rawErase`, `rawInsert`, or `rawReplace`. Cached-only entries are skipped. This is used when applying a sandbox or a nested view back to its parent. + +`apply(OpenView& to, STTx const& tx, TER ter, ...)` is the full transaction-commit path. It does everything the raw version does but first generates `TxMeta` — the transaction metadata that gets stored on-ledger and tells downstream clients exactly what changed. The condition `!to.open() || isDryRun` gates metadata generation: metadata is always produced for closed ledgers (where transactions are final), and also when `isDryRun` is true. In dry-run mode the metadata is produced but the state changes themselves are suppressed — this supports pre-flight validation and fee calculation without side effects. + +## Metadata Construction + +The metadata generation in the `apply(OpenView&...)` overload is the most complex part of the file. For each pending item, it classifies the change as `sfDeletedNode`, `sfCreatedNode`, or `sfModifiedNode` and populates the appropriate metadata fields using `SField` metadata flags baked into the XRPL protocol schema: + +- **Deleted nodes** capture `sfPreviousFields` (any field from the original that differs from the final state, controlled by `sMD_ChangeOrig`) and `sfFinalFields` (always-included fields plus delete-final fields, via `sMD_Always | sMD_DeleteFinal`). +- **Modified nodes** capture `sfPreviousFields` the same way and `sfFinalFields` with `sMD_Always | sMD_ChangeNew` to record both the stable identity fields and the newly changed values. +- **Created nodes** capture only `sfNewFields` — all non-default values that carry `sMD_Create | sMD_Always`. + +A subtle optimization prevents spurious `sfModifiedNode` entries: `if ((type == &sfModifiedNode) && (*curNode == *origNode)) continue;` — if the buffer holds a modify action but the content is byte-for-byte identical to the original, the node is silently omitted from metadata. This can happen when a transaction reads and re-writes a field with the same value. + +After the loop, any `Mods` entries accumulated by threading are written back via `rawReplace` (unless it is a dry run). + +## The Threading System + +XRPL ledger objects maintain a "thread" — a singly-linked history of the last transaction that touched each account root. The threading helpers implement this. + +`threadItem(TxMeta&, SLE&)` calls `sle->thread(txID, lgrSeq, prevTxID, prevLgrID)` which updates the SLE's `sfPreviousTxnID` / `sfPreviousTxnLgrSeq` fields in place. If there was a previous transaction, it adds those old fields to the metadata's `sfPreviousTxnID` / `sfPreviousTxnLgrSeq` entries on the affected node — so the chain of transactions is visible in metadata. + +`threadOwners()` figures out which accounts need threading for a given node type: + +- `ltACCOUNT_ROOT` objects thread only to themselves (handled by the caller). +- `ltRIPPLE_STATE` (trust lines) threads to both the low-limit and high-limit account — the two parties to the trust line. +- Everything else threads to `sfAccount` if present, and to `sfDestination` if present. + +`getForMod()` is the helper that retrieves an SLE for threading modification. It checks the local `Mods` map first (objects already being modified by threading in this same pass), then checks `items_` (objects being modified by the transaction itself), and finally falls back to reading from the base view. Objects found only in `items_` as `cache` (not actually written) are placed in `Mods` because their only modification is the threading metadata — they shouldn't be promoted to `Action::modify` in the primary items table. The function gracefully returns `nullptr` when threading to a deleted or nonexistent account, which is legal (e.g., the destination of an expired Escrow or PayChannel may have been deleted). + +## Snapshot Semantics and Invariant Enforcement + +The `erase()` and `update()` methods both perform pointer-identity checks (`item.second != sle`): they require the exact same `shared_ptr` that was handed out by `peek()`. This enforces a strict ownership protocol — a transactor cannot erase an SLE it didn't explicitly peek, preventing accidental mutation of a stale copy. Both methods call `LogicError` (a hard abort) on invariant violations like double-erase or erasing an unknown pointer, reflecting that these represent programming errors rather than runtime conditions. + +`rawErase()` deliberately skips the identity check — it is the unsafe bypass used when the caller provides its own SLE (e.g., during `rawInsert`/`rawErase` operations from `ApplyViewBase`). + +## Successor Navigation + +`succ()` merges two sorted key spaces: the base ledger's key space and the local `items_` map. It must find the smallest key strictly greater than the given key that actually exists after applying the pending changes. The algorithm first walks the base view's successor, skipping any keys that are pending deletion in `items_`, then independently walks `items_` for non-erased entries greater than the key, and returns whichever result is smaller. This O(log n) merge is necessary because the view must present a consistent ordered sequence of ledger objects to callers such as directory walkers. + +## Relationship to `RawStateTable` + +`RawStateTable` (used by `PaymentSandbox`) is a leaner cousin with only three actions (erase, insert, replace — no `cache`) and no metadata generation. `ApplyStateTable` is the richer layer specifically designed for the transactor path, where both the state changes and the metadata record of those changes must be produced together. \ No newline at end of file diff --git a/src/libxrpl/ledger/ApplyView.cpp.ai.json b/src/libxrpl/ledger/ApplyView.cpp.ai.json new file mode 100644 index 0000000000..f74886dbd2 --- /dev/null +++ b/src/libxrpl/ledger/ApplyView.cpp.ai.json @@ -0,0 +1,274 @@ +{ + "args": [], + "classes": [], + "code_paths": [], + "data_flows": [], + "description": "Implements directory management functions for the XRPL ledger, including adding, removing, and deleting directory entries and pages within the ledger's state tables.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "indexes (STVector256) - duplicate key insertion", + "empty", + "string", + "validation" + ], + "evidence": "std::find + LogicError at insertKey (preserveOrder branch)", + "issue_pattern": "Missing empty string validation for indexes (STVector256) - duplicate key insertion", + "why_false_positive": "std::find + LogicError validates indexes (STVector256) - duplicate key insertion for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "indexes (STVector256) - duplicate key insertion (sorted)", + "empty", + "string", + "validation" + ], + "evidence": "std::lower_bound + LogicError at insertKey (preserveOrder == false branch)", + "issue_pattern": "Missing empty string validation for indexes (STVector256) - duplicate key insertion (sorted)", + "why_false_positive": "std::lower_bound + LogicError validates indexes (STVector256) - duplicate key insertion (sorted) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "page (std::uint64_t) - unsigned overflow", + "empty", + "string", + "validation" + ], + "evidence": "static_assert at insertPage", + "issue_pattern": "Missing empty string validation for page (std::uint64_t) - unsigned overflow", + "why_false_positive": "static_assert validates page (std::uint64_t) - unsigned overflow for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node (SLE::pointer) - existence", + "empty", + "string", + "validation" + ], + "evidence": "view.peek + LogicError at findPreviousPage", + "issue_pattern": "Missing empty string validation for node (SLE::pointer) - existence", + "why_false_positive": "view.peek + LogicError validates node (SLE::pointer) - existence for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/ApplyView.cpp", + "functions": [ + { + "args": [ + "view", + "directory", + "key", + "describe" + ], + "lineno": 9, + "name": "createRoot" + }, + { + "args": [ + "view", + "directory", + "start" + ], + "lineno": 22, + "name": "findPreviousPage" + }, + { + "args": [ + "view", + "node", + "page", + "preserveOrder", + "indexes", + "key" + ], + "lineno": 41, + "name": "insertKey" + }, + { + "args": [ + "view", + "page", + "node", + "nextPage", + "next", + "key", + "directory", + "describe" + ], + "lineno": 65, + "name": "insertPage" + }, + { + "args": [ + "preserveOrder", + "directory", + "key", + "describe" + ], + "lineno": 116, + "name": "ApplyView::dirAdd" + }, + { + "args": [ + "directory" + ], + "lineno": 137, + "name": "ApplyView::emptyDirDelete" + }, + { + "args": [ + "directory", + "page", + "key", + "keepRoot" + ], + "lineno": 176, + "name": "ApplyView::dirRemove" + }, + { + "args": [ + "directory", + "callback" + ], + "lineno": 266, + "name": "ApplyView::dirDelete" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "directory" + } + ], + "test_coverage_notes": "", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (LogicError, static_assert, std algorithms)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LogicError(\"dirInsert: double insertion\")", + "field": "indexes (STVector256) - duplicate key insertion", + "location": "insertKey (preserveOrder branch)", + "validated_by": "std::find + LogicError", + "validates": [ + "Checks if key already exists in indexes before insertion when preserveOrder is true" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "LogicError(\"dirInsert: double insertion\")", + "field": "indexes (STVector256) - duplicate key insertion (sorted)", + "location": "insertKey (preserveOrder == false branch)", + "validated_by": "std::lower_bound + LogicError", + "validates": [ + "Sorts indexes, checks if key already exists before insertion when preserveOrder is false" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "static_assert failure (compile-time error)", + "field": "page (std::uint64_t) - unsigned overflow", + "location": "insertPage", + "validated_by": "static_assert", + "validates": [ + "Ensures page is unsigned type", + "Ensures unsigned overflow wraps to zero (modulo arithmetic)" + ], + "validation_type": "type|range" + }, + { + "confidence": 1.0, + "error_thrown": "LogicError(\"Directory chain: root back-pointer broken.\")", + "field": "node (SLE::pointer) - existence", + "location": "findPreviousPage", + "validated_by": "view.peek + LogicError", + "validates": [ + "Checks that the previous page node exists in the ledger view" + ], + "validation_type": "business_logic|null-check" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/ApplyView.cpp.ai.md b/src/libxrpl/ledger/ApplyView.cpp.ai.md new file mode 100644 index 0000000000..c274b4a2f2 --- /dev/null +++ b/src/libxrpl/ledger/ApplyView.cpp.ai.md @@ -0,0 +1,45 @@ +# `src/libxrpl/ledger/ApplyView.cpp` — Ledger Directory Management + +## Role in the System + +This file implements the XRPL ledger's **directory data structure** — the paged linked-list mechanism used to associate sets of ledger objects with an index key. Directories serve two distinct purposes: **owner directories** that track all objects (offers, trust lines, escrows, etc.) owned by a single account, and **book directories** that list all open offers at a specific price point in an order book. Both are stored as chains of `ltDIR_NODE` ledger entries. This file provides the concrete implementations of `ApplyView::dirAdd()`, `dirRemove()`, `emptyDirDelete()`, and `dirDelete()`, as well as the lower-level helpers in the anonymous `xrpl::directory` namespace. + +## The Directory Data Structure + +A directory is a circular doubly-linked list of pages, where page 0 is always the root. Each page is an `SLE` (`SLE` = State Ledger Entry) of type `ltDIR_NODE` holding up to `dirNodeMaxEntries` (32) `uint256` keys in its `sfIndexes` field. Pages carry `sfIndexNext` and `sfIndexPrevious` pointers; the root's `sfIndexPrevious` points to the last page in the chain, making it a circular list with O(1) access to both head and tail. The `sfRootIndex` field on every page records the directory's canonical key, enabling upward navigation. + +## Helper Functions in `namespace directory` + +These four functions are deliberately separated into a sub-namespace with a header-level warning ("Don't use them unless you really, really know what you're doing") because they expose the raw structural machinery that callers must sequence correctly. + +**`createRoot()`** bootstraps a brand-new directory with a single entry. It allocates the root `SLE`, records the root index via `sfRootIndex`, invokes the caller-supplied `describe` callback to stamp type-specific metadata (e.g., `sfOwner` for owner directories, or `sfTakerPays`/`sfTakerGets` for book directories), pushes the first key, and inserts the SLE into the view. This callback pattern is the idiomatic way to keep the generic directory machinery decoupled from the specific ledger object types it manages. + +**`findPreviousPage()`** navigates from the root to the last page via `sfIndexPrevious`. Because the root always points to the tail, this is O(1) regardless of chain length, which is important for write performance during heavy market-maker activity. If the previous page pointer is non-zero but the page cannot be found in the view, the function calls `LogicError` — a fatal termination indicating corrupted ledger state, not a recoverable error. + +**`insertKey()`** handles the actual vector-level insertion into an existing page. The `preserveOrder` flag governs which of two strategies is used. When `true` (used by `dirAppend()` for offer-book directories), the key is appended at the tail to maintain insertion order. When `false` (used by `dirInsert()` for owner directories), the page is sorted first via `std::sort` — a legacy concession to the fact that pages may have been written by older code that did not maintain sort order — and `std::lower_bound` finds the correct insertion point. Both branches call `LogicError` on a duplicate key, as double-insertion is a programming error, not a protocol error. + +**`insertPage()`** creates a new trailing page when the last page is full. It uses intentional unsigned arithmetic overflow of the `uint64_t` page counter to detect exhaustion: incrementing a max-value `uint64_t` wraps to zero, which is then treated as "out of pages." Two `static_assert` guards verify at compile time that the integer type is unsigned (making the overflow defined behavior per the C++ standard) and that the wrap-to-zero property actually holds. For post-`fixDirectoryLimit` ledgers, the older `dirNodeMaxPages` (262,144) ceiling is bypassed; the only limit is the `uint64_t` wrap. The new page is linked in at the tail by updating both the former-last-page's `sfIndexNext` and the root's `sfIndexPrevious`. The `describe` callback fires on each new page so type-specific fields propagate correctly beyond the first page. + +## `ApplyView::dirAdd()` — the Insertion Dispatcher + +The private `dirAdd()` method is the single entry point for all insertions. It first peeks at the root; if absent, it delegates to `createRoot()`. Otherwise it calls `findPreviousPage()` to retrieve the last page and its current entry count. If that page has room (`indexes.size() < dirNodeMaxEntries`), `insertKey()` is called directly. If the page is full, `insertPage()` is called with a hard-coded `nextPage = 0`, reflecting that new pages are always appended at the end of the chain — the reserved `sfIndexNext` field on a new non-root page is intentionally left unset (the commented-out code block documents this as reserved for a hypothetical future insertion-in-middle operation). + +The public surface exposes `dirAppend()` (for offers, `preserveOrder = true`) and two overloads of `dirInsert()` (for owned objects, `preserveOrder = false`). `dirAppend()` additionally asserts that only `ltOFFER` types should use it, enforcing the separation between the two directory use cases. + +## `dirRemove()` — Precise Removal with Page Cleanup + +`dirRemove()` locates the target key by page number (callers store the page number alongside the object's ledger entry), removes it from the `sfIndexes` vector preserving relative order, and writes the update back. If the page remains non-empty after removal, the function returns immediately. + +When a page becomes empty, the function enters a more complex cleanup path with distinct handling for the root page (page 0) versus interior/tail pages. The root is never deleted while `keepRoot` is true (callers like the owner-directory code retain the root when an account still exists). For non-root pages, the function unlinks the empty node from both its predecessor and successor, then erases it. A secondary check catches the edge case where the newly-adjacent `next` page is also empty and is the last page — a valid legacy state — in which case it too is reaped. Finally, if `keepRoot` is false and the chain has collapsed back to just an empty root, the root itself is erased, fully deleting the directory. + +Structural consistency is verified at each step: missing neighbor pages, self-referential `sfIndexNext`/`sfIndexPrevious` on non-root nodes, and asymmetric forward/reverse links all trigger `LogicError` termination. These are marked `LCOV_EXCL_*` because reaching them in a well-functioning node indicates ledger database corruption, not an exercisable code path. + +## `emptyDirDelete()` and `dirDelete()` + +`emptyDirDelete()` is a narrower variant: it only deletes a root that is already empty, optionally cleaning up a single trailing empty page left by legacy code. It validates that the keylet refers to an `ltDIR_NODE` root before proceeding. + +`dirDelete()` performs a bulk teardown by iterating all pages via `sfIndexNext` chaining, invoking a user-supplied callback for each `uint256` key encountered (allowing the caller to perform per-object cleanup), and erasing each page. This is used when an entire directory must be destroyed, such as when an account is permanently deleted from the ledger. + +## Design Observations + +The `describe` callback is a deliberate inversion of control: the directory machinery creates and links pages, but callers inject the type-specific fields, keeping `ltOFFER` and owner-directory logic out of generic code. The `keepRoot` flag on `dirRemove()` reflects the fact that an account's owner directory must persist as long as the account itself exists, even momentarily empty. The deliberate `uint64_t` overflow check in `insertPage()` — with both a conceptual comment and two compile-time assertions — is a rare, carefully documented instance of intentional unsigned wraparound used as a sentinel condition. \ No newline at end of file diff --git a/src/libxrpl/ledger/ApplyViewBase.cpp.ai.json b/src/libxrpl/ledger/ApplyViewBase.cpp.ai.json new file mode 100644 index 0000000000..eee27ec082 --- /dev/null +++ b/src/libxrpl/ledger/ApplyViewBase.cpp.ai.json @@ -0,0 +1,454 @@ +{ + "args": [ + { + "lineno": 6, + "name": "base" + }, + { + "lineno": 6, + "name": "flags" + } + ], + "classes": [ + { + "args": [ + "ReadView const* base", + "ApplyFlags flags" + ], + "lineno": 6, + "name": "ApplyViewBase" + } + ], + "code_paths": [ + { + "call_chain": [ + "ApplyViewBase::exists", + "items_.exists(*base_, k)" + ], + "entry_point": "ApplyViewBase::exists", + "purpose": "Checks if a ledger entry exists for a given Keylet.", + "validation_points": [ + "items_.exists(*base_, k) - likely validates the existence and possibly the integrity of the ledger entry." + ] + }, + { + "call_chain": [ + "ApplyViewBase::read", + "items_.read(*base_, k)" + ], + "entry_point": "ApplyViewBase::read", + "purpose": "Reads a ledger entry for a given Keylet.", + "validation_points": [ + "items_.read(*base_, k) - likely validates the presence and correctness of the entry before returning." + ] + }, + { + "call_chain": [ + "ApplyViewBase::peek", + "items_.peek(*base_, k)" + ], + "entry_point": "ApplyViewBase::peek", + "purpose": "Peeks at a mutable ledger entry for a given Keylet.", + "validation_points": [ + "items_.peek(*base_, k) - may validate mutability and existence." + ] + }, + { + "call_chain": [ + "ApplyViewBase::insert", + "items_.insert(*base_, sle)" + ], + "entry_point": "ApplyViewBase::insert", + "purpose": "Inserts a new ledger entry.", + "validation_points": [ + "items_.insert(*base_, sle) - likely validates that the entry does not already exist and is well-formed." + ] + }, + { + "call_chain": [ + "ApplyViewBase::erase", + "items_.erase(*base_, sle)" + ], + "entry_point": "ApplyViewBase::erase", + "purpose": "Erases a ledger entry.", + "validation_points": [ + "items_.erase(*base_, sle) - likely validates that the entry exists and can be erased." + ] + }, + { + "call_chain": [ + "ApplyViewBase::update", + "items_.update(*base_, sle)" + ], + "entry_point": "ApplyViewBase::update", + "purpose": "Updates an existing ledger entry.", + "validation_points": [ + "items_.update(*base_, sle) - likely validates that the entry exists and the update is valid." + ] + }, + { + "call_chain": [ + "ApplyViewBase::rawInsert", + "items_.insert(*base_, sle)" + ], + "entry_point": "ApplyViewBase::rawInsert", + "purpose": "Inserts a ledger entry without higher-level checks (raw operation).", + "validation_points": [ + "items_.insert(*base_, sle) - may skip some validation, but still checks for basic consistency." + ] + }, + { + "call_chain": [ + "ApplyViewBase::rawErase", + "items_.rawErase(*base_, sle)" + ], + "entry_point": "ApplyViewBase::rawErase", + "purpose": "Erases a ledger entry without higher-level checks (raw operation).", + "validation_points": [ + "items_.rawErase(*base_, sle) - may skip some validation, but still checks for basic consistency." + ] + } + ], + "data_flows": [ + { + "field": "Keylet k", + "flow": [ + "Function argument", + "items_.*(*base_, k)", + "Ledger entry lookup or modification" + ], + "origin": "Passed as argument to functions like exists, read, peek, insert, erase, update", + "transformations": [ + "Used as a key to look up or modify ledger entries in items_" + ], + "validated_at": "Within items_.* methods (e.g., exists, read, insert, erase, update)" + }, + { + "field": "std::shared_ptr sle", + "flow": [ + "Function argument", + "items_.*(*base_, sle)", + "Ledger entry modification" + ], + "origin": "Passed as argument to insert, erase, update, rawInsert, rawErase, rawReplace", + "transformations": [ + "Inserted, erased, or updated in the ledger view" + ], + "validated_at": "Within items_.* methods (e.g., insert, erase, update, rawErase, rawReplace)" + }, + { + "field": "ApplyFlags flags_", + "flow": [ + "ApplyViewBase::ApplyViewBase", + "flags_ member variable", + "ApplyViewBase::flags()" + ], + "origin": "Constructor argument to ApplyViewBase", + "transformations": [ + "Stored as member, returned as-is" + ], + "validated_at": "Not explicitly validated in this file" + }, + { + "field": "ReadView const* base_", + "flow": [ + "ApplyViewBase::ApplyViewBase", + "base_ member variable", + "Used in all methods as *base_" + ], + "origin": "Constructor argument to ApplyViewBase", + "transformations": [ + "Used as the underlying ledger view for all operations" + ], + "validated_at": "Not explicitly validated in this file; assumed to be valid pointer" + }, + { + "field": "LedgerHeader, Fees, Rules", + "flow": [ + "ApplyViewBase::{header,fees,rules}()", + "base_->header()/fees()/rules()", + "Returned to caller" + ], + "origin": "base_->header(), base_->fees(), base_->rules()", + "transformations": [ + "No transformation, just forwarding" + ], + "validated_at": "Validation (if any) occurs in base_ implementation" + } + ], + "description": "Implements the ApplyViewBase class, providing a base for ledger view modifications and access in the XRPL ledger, including methods for reading, writing, and iterating over ledger entries and transactions.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/ApplyViewBase.cpp", + "functions": [ + { + "args": [ + "ReadView const* base", + "ApplyFlags flags" + ], + "lineno": 6, + "name": "ApplyViewBase" + }, + { + "args": [], + "lineno": 11, + "name": "open" + }, + { + "args": [], + "lineno": 16, + "name": "header" + }, + { + "args": [], + "lineno": 21, + "name": "fees" + }, + { + "args": [], + "lineno": 26, + "name": "rules" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 31, + "name": "exists" + }, + { + "args": [ + "key_type const& key", + "std::optional const& last" + ], + "lineno": 36, + "name": "succ" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 42, + "name": "read" + }, + { + "args": [], + "lineno": 47, + "name": "slesBegin" + }, + { + "args": [], + "lineno": 52, + "name": "slesEnd" + }, + { + "args": [ + "uint256 const& key" + ], + "lineno": 57, + "name": "slesUpperBound" + }, + { + "args": [], + "lineno": 62, + "name": "txsBegin" + }, + { + "args": [], + "lineno": 67, + "name": "txsEnd" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 72, + "name": "txExists" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 77, + "name": "txRead" + }, + { + "args": [], + "lineno": 83, + "name": "flags" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 87, + "name": "peek" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 92, + "name": "erase" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 97, + "name": "insert" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 102, + "name": "update" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 108, + "name": "rawErase" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 113, + "name": "rawInsert" + }, + { + "args": [ + "std::shared_ptr const& sle" + ], + "lineno": 118, + "name": "rawReplace" + }, + { + "args": [ + "XRPAmount const& fee" + ], + "lineno": 123, + "name": "rawDestroyXRP" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "detail" + } + ], + "test_coverage_notes": "This file is a low-level implementation and does not contain explicit validation logic; validation is delegated to the items_ object and the base_ ReadView. Test coverage for these methods is likely indirect, via higher-level ApplyView or transaction application tests. Typical test files would be in the rippled/test/ or xrpl/test/ directories, such as ApplyView_test.cpp, Ledger_test.cpp, or integration tests that exercise ledger modifications. Gaps: There may be limited direct unit tests for ApplyViewBase itself; most validation is tested via higher-level transaction/ledger tests. Direct tests for error handling (e.g., invalid pointers, malformed SLEs) may be missing unless specifically targeted in the test suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/ApplyViewBase.cpp.ai.md b/src/libxrpl/ledger/ApplyViewBase.cpp.ai.md new file mode 100644 index 0000000000..3ec0f9a6cb --- /dev/null +++ b/src/libxrpl/ledger/ApplyViewBase.cpp.ai.md @@ -0,0 +1,39 @@ +# `src/libxrpl/ledger/ApplyViewBase.cpp` + +## Role and Purpose + +`ApplyViewBase` is the foundational implementation class for mutable ledger views in XRPL. It lives in `namespace xrpl::detail`, signaling that it is an infrastructure component rather than a public interface. The class is the shared concrete base for two important consumers: `ApplyViewImpl` (the per-transaction apply context used by the transaction engine) and `Sandbox` (a discardable scratch-pad view used for exploratory state changes). + +The problem it solves is staging ledger mutations. XRPL processes each transaction against a copy of the current ledger state rather than the ledger itself — changes are buffered, then either committed atomically or discarded depending on whether the transaction succeeds. `ApplyViewBase` provides that buffer, together with the full read interface over the combined "base + pending changes" state. + +## Class Hierarchy and Interface Obligations + +`ApplyViewBase` inherits from two abstract interfaces: `ApplyView` and `RawView`. `ApplyView` extends `ReadView` with validated, "checkout" style mutation methods (`peek`, `insert`, `update`, `erase`) and a `flags()` accessor that carries per-transaction policy flags. `RawView` provides a lower-level mutation surface (`rawInsert`, `rawErase`, `rawReplace`, `rawDestroyXRP`) used when building transaction metadata and committing state into an `OpenView`. `ApplyViewBase` implements every virtual method in both hierarchies, leaving subclasses only to add lifecycle logic (`apply()` in `ApplyViewImpl`, `apply(RawView&)` in `Sandbox`). + +## Two-Member Design: `base_` and `items_` + +The class holds exactly two non-trivial members, and the architecture is built around their division of responsibility: + +`ReadView const* base_` is the immutable snapshot of the ledger at the start of transaction processing. Every read-only query that doesn't need awareness of pending changes — `open()`, `header()`, `fees()`, `rules()`, and all transaction-map iterators — is forwarded directly to `base_`. The SLE iterators (`slesBegin`, `slesEnd`, `slesUpperBound`) are also delegated straight to `base_` rather than going through the change buffer. This is a deliberate design choice: the apply phase does not need to iterate over its own pending writes, and bypassing the buffer keeps iteration consistent with the ledger snapshot. + +`detail::ApplyStateTable items_` is the mutable change buffer. It maintains an internal `std::map` of `uint256 → (Action, SLE)` entries, where `Action` is one of `cache`, `erase`, `insert`, or `modify`. Every mutation routes through `items_`, which records the intended change without touching `base_`. Read operations that are change-aware — `exists()`, `succ()`, `read()`, and `peek()` — pass `*base_` to `items_` so the table can merge pending changes with the base state before answering. + +## Validated vs. Raw Mutation + +The implementation exposes two tiers of mutation, and their distinction matters: + +The *validated* tier (`insert`, `erase`, `update`) goes through the consistency-checking paths in `ApplyStateTable`. For example, `erase` asserts that the SLE was previously obtained via `peek()` on this very view, enforcing the API contract that prohibits sharing SLEs across view instances. `insert` requires that the key not already exist. + +The *raw* tier (`rawErase`, `rawInsert`, `rawReplace`, `rawDestroyXRP`) is lower-level and skips those invariant checks. It exists to serve the `RawView` interface, which is the mechanism through which a finalized `ApplyStateTable` commits its changes upstream — for instance, when `ApplyViewImpl::apply()` pumps the buffered changes into an `OpenView`. Notably, `rawInsert` calls the same `items_.insert()` as the high-level `insert()`, but `rawErase` routes to the distinct `items_.rawErase()` that bypasses the ownership check. + +## The `peek()` / `update()` / `erase()` Contract + +`peek()` returns a mutable `shared_ptr` checked out from the table. The caller may modify the SLE in place but must then signal the view via `update()` to mark the entry as modified, or `erase()` to mark it for deletion. Passing the peeked SLE to any other `ApplyView` instance violates the invariant enforced at the `ApplyStateTable` layer. `read()`, by contrast, returns a `shared_ptr` that carries no ownership obligation and is safe to inspect without any follow-up call. + +## `flags_` and Transaction Policy + +The `ApplyFlags` bitmask stored in `flags_` is set at construction and exposed read-only via `flags()`. Consumers use it to distinguish scenarios like retry mode (`tapRETRY`), privileged transaction sources (`tapUNLIMITED`), batch context (`tapBATCH`), or dry-run simulation (`tapDRY_RUN`). The base class merely carries and vends this value; policy logic is the concern of the callers. + +## Non-Copyability + +The class deletes the copy constructor and both assignment operators, permitting only move construction. This is appropriate because `ApplyStateTable` holds shared ownership of `SLE` objects and a raw `ReadView*` back-pointer: allowing copies would create aliasing hazards where two views simultaneously believe they own the same pending state. \ No newline at end of file diff --git a/src/libxrpl/ledger/ApplyViewImpl.cpp.ai.json b/src/libxrpl/ledger/ApplyViewImpl.cpp.ai.json new file mode 100644 index 0000000000..4d31313f2a --- /dev/null +++ b/src/libxrpl/ledger/ApplyViewImpl.cpp.ai.json @@ -0,0 +1,196 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ReadView const* base", + "ApplyFlags flags" + ], + "lineno": 4, + "name": "ApplyViewImpl" + } + ], + "code_paths": [ + { + "call_chain": [ + "ApplyViewImpl::apply", + "ApplyViewItems::apply", + "OpenView (possibly state changes or further validation)" + ], + "entry_point": "ApplyViewImpl::apply", + "purpose": "Applies a transaction (STTx) to an OpenView, possibly as part of ledger update or transaction processing. Handles transaction effects, metadata, and error codes.", + "validation_points": [ + "ApplyViewItems::apply (likely where transaction validation, effects, and error handling occur)" + ] + }, + { + "call_chain": [ + "ApplyViewImpl::visit", + "ApplyViewItems::visit", + "func (user-supplied function)" + ], + "entry_point": "ApplyViewImpl::visit", + "purpose": "Iterates over items in the ApplyView, calling a user-supplied function for each, possibly for validation, auditing, or state inspection.", + "validation_points": [ + "func (user-supplied function may perform validation on before/after SLEs)" + ] + } + ], + "data_flows": [ + { + "field": "tx (STTx const&)", + "flow": [ + "ApplyViewImpl::apply (receives tx)", + "ApplyViewItems::apply (tx passed through)", + "OpenView (tx effects applied)" + ], + "origin": "Input parameter to ApplyViewImpl::apply", + "transformations": [ + "May be checked for validity, effects computed, metadata generated" + ], + "validated_at": "ApplyViewItems::apply (likely, as ApplyViewImpl::apply is a thin wrapper)" + }, + { + "field": "ter (TER)", + "flow": [ + "ApplyViewImpl::apply", + "ApplyViewItems::apply" + ], + "origin": "Input parameter to ApplyViewImpl::apply", + "transformations": [ + "May be checked/updated based on transaction processing outcome" + ], + "validated_at": "ApplyViewItems::apply" + }, + { + "field": "deliver_", + "flow": [ + "ApplyViewImpl::apply (passed to ApplyViewItems::apply)" + ], + "origin": "Member of ApplyViewImpl", + "transformations": [ + "May be set/updated during transaction application" + ], + "validated_at": "ApplyViewItems::apply" + }, + { + "field": "items_", + "flow": [ + "ApplyViewImpl::* (all methods delegate to items_)", + "ApplyViewItems::*" + ], + "origin": "Member of ApplyViewImpl (ApplyViewItems)", + "transformations": [ + "Holds staged changes, applies them to OpenView" + ], + "validated_at": "ApplyViewItems::*" + } + ], + "description": "Implements the ApplyViewImpl class, which provides methods to apply transactions and manage state changes on top of a base ledger view in the XRPL ledger system.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/ApplyViewImpl.cpp", + "functions": [ + { + "args": [ + "ReadView const* base", + "ApplyFlags flags" + ], + "lineno": 4, + "name": "ApplyViewImpl::ApplyViewImpl" + }, + { + "args": [ + "OpenView& to", + "STTx const& tx", + "TER ter", + "std::optional parentBatchId", + "bool isDryRun", + "beast::Journal j" + ], + "lineno": 8, + "name": "ApplyViewImpl::apply" + }, + { + "args": [], + "lineno": 17, + "name": "ApplyViewImpl::size" + }, + { + "args": [ + "OpenView& to", + "std::function const& before, std::shared_ptr const& after)> const& func" + ], + "lineno": 21, + "name": "ApplyViewImpl::visit" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file (ApplyViewImpl.cpp) is a thin wrapper delegating to ApplyViewItems. Direct validation logic is not present here, but in ApplyViewItems::apply and related methods. Tests likely exist in files covering transaction application, ledger updates, and ApplyView logic, such as 'ApplyView_test.cpp', 'Transactor_test.cpp', or integration tests for transaction processing. Gaps: Direct unit tests for ApplyViewImpl may be minimal; most validation is tested indirectly via higher-level transaction/ledger tests. If ApplyViewItems is not thoroughly tested, validation coverage may be incomplete.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/ApplyViewImpl.cpp.ai.md b/src/libxrpl/ledger/ApplyViewImpl.cpp.ai.md new file mode 100644 index 0000000000..93fc116d00 --- /dev/null +++ b/src/libxrpl/ledger/ApplyViewImpl.cpp.ai.md @@ -0,0 +1,45 @@ +# `ApplyViewImpl.cpp` — Concrete Transaction Apply View + +## Role in the System + +`ApplyViewImpl` is the concrete, client-facing implementation of the ledger's write-staging layer for a single transaction. It sits at the top of a three-level class hierarchy: `ApplyView` (abstract interface) → `detail::ApplyViewBase` (read/write delegation to `ApplyStateTable`) → `ApplyViewImpl` (commit-and-metadata endpoint). The file is deliberately minimal — only 39 lines — because almost all substantive logic lives in `ApplyStateTable`, which buffers ledger-entry mutations as a keyed map of `(Action, SLE)` pairs. `ApplyViewImpl`'s distinct contribution is (1) the `deliver_` field that tracks payment delivery amounts for metadata generation, and (2) the `apply()` method that finalises the buffered changes by committing them into a live `OpenView` and producing a `TxMeta` record. + +## Class Hierarchy and Design Rationale + +`detail::ApplyViewBase` already wires up all the `ReadView`, `ApplyView`, and `RawView` virtual methods by routing every call through the `items_` member (`ApplyStateTable`). This means that during transaction execution, transactors read and mutate ledger state through the `ApplyView` interface without ever touching the real ledger; every mutation is buffered locally. `ApplyViewImpl` inherits all of that machinery and adds only what is needed at the commit boundary. + +This layering exists to support discard semantics: if transaction processing fails, the caller simply destroys the `ApplyViewImpl` and the buffered changes evaporate without modifying the base ledger. The design is intentional and safe because `ApplyViewBase` stores the base view as a raw `ReadView const*` — it never takes ownership — so destruction of the apply view is always trivially correct. + +## The `apply()` Method and its Finality Contract + +```cpp +std::optional +ApplyViewImpl::apply(OpenView& to, STTx const& tx, TER ter, + std::optional parentBatchId, + bool isDryRun, beast::Journal j) +{ + return items_.apply(to, tx, ter, deliver_, parentBatchId, isDryRun, j); +} +``` + +The header comment attached to this method carries a strict post-condition: *after calling `apply()`, the only valid operation is destruction*. This is a move-semantics-like contract without actually moving. The rationale is that `ApplyStateTable::apply()` transfers ownership of buffered SLEs into the target `OpenView`, generating transaction metadata in the process. Reusing the `ApplyViewImpl` afterwards would risk applying the same mutations twice or producing corrupt metadata. + +The method passes `deliver_` — an `std::optional` — directly into `ApplyStateTable::apply()`. When `deliver_` is set (via the `deliver()` setter), the emitted `TxMeta` will include the `DeliveredAmount` field, which is required for cross-currency payments and partial payment reporting. If not set, the field is absent from metadata, which is the correct behaviour for non-payment transactions. + +Two parameters added for batch processing — `parentBatchId` (`std::optional`) and `isDryRun` (bool) — pass through unchanged to `ApplyStateTable::apply()`. The `tapBATCH` and `tapDRY_RUN` flags in `ApplyFlags` mirror these at the flag level, but the explicit parameters allow finer-grained control at the metadata-generation layer without requiring flag inspection inside `ApplyStateTable`. + +## `size()` and `visit()` — Inspection Without Commitment + +`size()` and `visit()` both delegate directly to `items_`. They exist on `ApplyViewImpl` rather than being buried in `ApplyViewBase` because their signatures require access to `OpenView` and to the before/after SLE snapshot mechanism that is only meaningful at the moment of commit. `visit()` in particular provides a callback-based iterator over every buffered modification, exposing both the pre-modification and post-modification SLE through `shared_ptr` pairs — useful for audit, debugging, or incremental metadata building in callers that need to observe changes without yet committing them. + +## State Ownership and Resource Safety + +All mutated ledger entries are held as `std::shared_ptr` inside `ApplyStateTable::items_`. The `before` snapshots captured for metadata are also reference-counted. This means: + +- No manual memory management is required anywhere in the apply-view stack. +- Move construction of `ApplyViewImpl` is enabled (and `ApplyViewBase` is also move-constructible) to support efficient placement of apply-view objects in containers like the batch-apply orchestration layer. +- Copy and assignment operators are deleted throughout the hierarchy, enforcing that exactly one `ApplyViewImpl` owns the buffered state at any point. + +## Relationship to `OpenView` and `TxMeta` + +The `OpenView` passed to `apply()` and `visit()` is the live, mutable ledger view (the in-progress open ledger or canonical ledger). `ApplyStateTable::apply()` replays each buffered `Action` (insert, modify, erase, cache) into that `OpenView` and simultaneously threads the transaction through account ownership chains to produce the complete `TxMeta` structure returned as `std::optional`. The optional is absent only in dry-run mode, where changes are still staged but no metadata is emitted and effects are not committed to the target view. \ No newline at end of file diff --git a/src/libxrpl/ledger/BookDirs.cpp.ai.json b/src/libxrpl/ledger/BookDirs.cpp.ai.json new file mode 100644 index 0000000000..686af3aef4 --- /dev/null +++ b/src/libxrpl/ledger/BookDirs.cpp.ai.json @@ -0,0 +1,451 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "BookDirs::BookDirs", + "cdirFirst (if key_ != beast::zero)" + ], + "entry_point": "BookDirs::BookDirs", + "purpose": "Constructs a BookDirs object, initializes directory traversal state.", + "validation_points": [ + "XRPL_ASSERT(root_ != beast::zero)", + "if (!cdirFirst(...)) { UNREACHABLE(...) }" + ] + }, + { + "call_chain": [ + "BookDirs::begin", + "BookDirs::const_iterator constructor" + ], + "entry_point": "BookDirs::begin", + "purpose": "Returns an iterator to the beginning of the book directory.", + "validation_points": [ + "BookDirs::const_iterator constructor (no explicit validation, but fields are set from validated BookDirs)" + ] + }, + { + "call_chain": [ + "BookDirs::const_iterator::operator*", + "view_->read(keylet::offer(index_))" + ], + "entry_point": "BookDirs::const_iterator::operator*", + "purpose": "Dereferences the iterator to access the current offer.", + "validation_points": [ + "XRPL_ASSERT(index_ != beast::zero)" + ] + }, + { + "call_chain": [ + "BookDirs::const_iterator::operator++", + "cdirNext", + "if (!cdirNext) { ... cdirFirst ... }" + ], + "entry_point": "BookDirs::const_iterator::operator++", + "purpose": "Advances the iterator to the next offer in the directory.", + "validation_points": [ + "XRPL_ASSERT(index_ != beast::zero)", + "if (!cdirFirst(...)) { UNREACHABLE(...) }" + ] + }, + { + "call_chain": [ + "BookDirs::const_iterator::operator==" + ], + "entry_point": "BookDirs::const_iterator::operator==", + "purpose": "Compares two iterators for equality.", + "validation_points": [ + "if (view_ == nullptr || other.view_ == nullptr)", + "XRPL_ASSERT(view_ == other.view_ && root_ == other.root_)" + ] + }, + { + "call_chain": [ + "BookDirs::const_iterator::operator++(int)", + "operator++" + ], + "entry_point": "BookDirs::const_iterator::operator++(int)", + "purpose": "Post-increment: advances iterator and returns previous value.", + "validation_points": [ + "XRPL_ASSERT(index_ != beast::zero)" + ] + } + ], + "data_flows": [ + { + "field": "root_", + "flow": [ + "BookDirs::BookDirs", + "BookDirs::const_iterator (via begin/end)", + "Used in iterator operations" + ], + "origin": "keylet::page(getBookBase(book)).key in BookDirs::BookDirs", + "transformations": [ + "Set from book parameter", + "Passed to iterator" + ], + "validated_at": "XRPL_ASSERT(root_ != beast::zero) in BookDirs::BookDirs" + }, + { + "field": "key_", + "flow": [ + "BookDirs::BookDirs", + "BookDirs::const_iterator (via begin/end)", + "Used as cur_key_ in iterator" + ], + "origin": "view_->succ(root_, next_quality_).value_or(beast::zero) in BookDirs::BookDirs", + "transformations": [ + "Set from ledger view", + "Passed to iterator" + ], + "validated_at": "if (key_ != beast::zero) { if (!cdirFirst(...)) { UNREACHABLE } }" + }, + { + "field": "view_", + "flow": [ + "BookDirs::BookDirs", + "BookDirs::const_iterator (via begin/end)", + "Used in all iterator operations" + ], + "origin": "Constructor parameter (BookDirs::BookDirs)", + "transformations": [ + "Pointer assignment", + "Passed to iterator" + ], + "validated_at": "if (view_ == nullptr || other.view_ == nullptr) in operator==" + }, + { + "field": "index_", + "flow": [ + "BookDirs::BookDirs", + "BookDirs::const_iterator (via begin)", + "Used in operator*, operator++, operator++(int)" + ], + "origin": "Set by cdirFirst/cdirNext in BookDirs::BookDirs and iterator", + "transformations": [ + "Set by directory traversal helpers" + ], + "validated_at": [ + "XRPL_ASSERT(index_ != beast::zero) in operator*", + "XRPL_ASSERT(index_ != beast::zero) in operator++", + "XRPL_ASSERT(index_ != beast::zero) in operator++(int)" + ] + }, + { + "field": "sle_, entry_", + "flow": [ + "BookDirs::BookDirs", + "BookDirs::const_iterator (via begin)", + "Used in iterator traversal" + ], + "origin": "Set by cdirFirst/cdirNext", + "transformations": [ + "Set by directory traversal helpers" + ], + "validated_at": "Indirectly validated by cdirFirst/cdirNext return value" + } + ], + "description": "Implements the BookDirs class for iterating over order book directories in the XRPL ledger, providing iterator functionality to traverse offers in a given book.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "root_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at BookDirs::BookDirs (constructor)", + "issue_pattern": "Missing empty string validation for root_", + "why_false_positive": "XRPL_ASSERT validates root_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "directory existence for key_", + "empty", + "string", + "validation" + ], + "evidence": "cdirFirst + UNREACHABLE at BookDirs::BookDirs (constructor)", + "issue_pattern": "Missing empty string validation for directory existence for key_", + "why_false_positive": "cdirFirst + UNREACHABLE validates directory existence for key_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "view_ and other.view_", + "empty", + "string", + "validation" + ], + "evidence": "pointer null check at BookDirs::const_iterator::operator==", + "issue_pattern": "Missing empty string validation for view_ and other.view_", + "why_false_positive": "pointer null check validates view_ and other.view_ for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "view_ and other.view_", + "type", + "validation", + "check" + ], + "evidence": "pointer null check at BookDirs::const_iterator::operator==", + "issue_pattern": "Missing type validation for view_ and other.view_", + "why_false_positive": "pointer null check validates view_ and other.view_ type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "view_ and root_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at BookDirs::const_iterator::operator==", + "issue_pattern": "Missing empty string validation for view_ and root_", + "why_false_positive": "XRPL_ASSERT validates view_ and root_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "index_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at BookDirs::const_iterator::operator*", + "issue_pattern": "Missing empty string validation for index_", + "why_false_positive": "XRPL_ASSERT validates index_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "index_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at BookDirs::const_iterator::operator++", + "issue_pattern": "Missing empty string validation for index_", + "why_false_positive": "XRPL_ASSERT validates index_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "directory existence for cur_key_", + "empty", + "string", + "validation" + ], + "evidence": "cdirFirst + UNREACHABLE at BookDirs::const_iterator::operator++", + "issue_pattern": "Missing empty string validation for directory existence for cur_key_", + "why_false_positive": "cdirFirst + UNREACHABLE validates directory existence for cur_key_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "index_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at BookDirs::const_iterator::operator++(int)", + "issue_pattern": "Missing empty string validation for index_", + "why_false_positive": "XRPL_ASSERT validates index_ for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/BookDirs.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "Book const& book" + ], + "lineno": 7, + "name": "BookDirs::BookDirs" + }, + { + "args": [], + "lineno": 22, + "name": "BookDirs::begin" + }, + { + "args": [], + "lineno": 36, + "name": "BookDirs::end" + }, + { + "args": [ + "BookDirs::const_iterator const& other" + ], + "lineno": 42, + "name": "BookDirs::const_iterator::operator==" + }, + { + "args": [], + "lineno": 53, + "name": "BookDirs::const_iterator::operator*" + }, + { + "args": [], + "lineno": 61, + "name": "BookDirs::const_iterator::operator++" + }, + { + "args": [ + "int" + ], + "lineno": 87, + "name": "BookDirs::const_iterator::operator++" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code contains UNREACHABLE and XRPL_ASSERT checks, which are typically only triggered in error or edge cases. The LCOV_EXCL_START/STOP comments indicate that these branches are excluded from coverage, suggesting that normal tests do not cover these error paths. Likely, unit/integration tests exist for BookDirs and iterators in test files such as BookDirs_test.cpp, BookDirs_test.py, or ledger traversal tests, but the unreachable/failed validation paths are not directly tested. Null pointer and zero-value assertions are defensive and may not be exercised in standard test suites.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT, UNREACHABLE (custom assertion macros), cdirFirst/cdirNext (directory helpers)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (XRPL_ASSERT)", + "field": "root_", + "location": "BookDirs::BookDirs (constructor)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "root_ must not be beast::zero (nonzero root)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE (logic error/exception)", + "field": "directory existence for key_", + "location": "BookDirs::BookDirs (constructor)", + "validated_by": "cdirFirst + UNREACHABLE", + "validates": [ + "If key_ is not beast::zero, directory must not be empty (cdirFirst must succeed)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "view_ and other.view_", + "location": "BookDirs::const_iterator::operator==", + "validated_by": "pointer null check", + "validates": [ + "view_ and other.view_ must not be nullptr" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (XRPL_ASSERT)", + "field": "view_ and root_", + "location": "BookDirs::const_iterator::operator==", + "validated_by": "XRPL_ASSERT", + "validates": [ + "view_ and other.view_ must be equal; root_ and other.root_ must be equal" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (XRPL_ASSERT)", + "field": "index_", + "location": "BookDirs::const_iterator::operator*", + "validated_by": "XRPL_ASSERT", + "validates": [ + "index_ must not be beast::zero (nonzero index)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (XRPL_ASSERT)", + "field": "index_", + "location": "BookDirs::const_iterator::operator++", + "validated_by": "XRPL_ASSERT", + "validates": [ + "index_ must not be beast::zero (nonzero index)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE (logic error/exception)", + "field": "directory existence for cur_key_", + "location": "BookDirs::const_iterator::operator++", + "validated_by": "cdirFirst + UNREACHABLE", + "validates": [ + "If cdirFirst fails for cur_key_, directory must not be empty" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (XRPL_ASSERT)", + "field": "index_", + "location": "BookDirs::const_iterator::operator++(int)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "index_ must not be beast::zero (nonzero index)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/BookDirs.cpp.ai.md b/src/libxrpl/ledger/BookDirs.cpp.ai.md new file mode 100644 index 0000000000..4f698e4a83 --- /dev/null +++ b/src/libxrpl/ledger/BookDirs.cpp.ai.md @@ -0,0 +1,37 @@ +# BookDirs.cpp + +## Role in the System + +`BookDirs.cpp` implements the `BookDirs` class, which provides a standard forward-iterator range over every offer sitting in a specific XRPL order-book. An order book in XRPL is identified by a `Book` — a pair of currency/issuer specifications — and its offers are not stored in a flat list but in a two-level, quality-bucketed directory structure in the ledger's state map. `BookDirs` exists to hide that structure behind a clean range interface, making it possible to write `for (auto const& offer : BookDirs(view, book))` with no knowledge of the underlying directory layout. + +## The Directory Structure It Traverses + +XRPL encodes exchange rate (quality) directly into the ledger key of each offer's directory page. `getBookBase(book)` computes the starting 256-bit key that represents quality zero for the given book, and `getQualityNext(root_)` produces the key immediately after the highest possible quality value — effectively the exclusive upper bound of the quality key range for that book. Any ledger key in the half-open interval `[root_, next_quality_)` is, by construction, a directory page for one specific exchange rate in this book. + +Within a single quality directory there may be multiple linked pages, each holding a `sfIndexes` vector of offer keys. The `cdirFirst`/`cdirNext` helpers from `DirectoryHelpers` walk that page chain, exposing one entry at a time. + +## Constructor: Eager State Seeding + +The constructor does more than initialize fields — it performs the first real ledger lookup. `view_->succ(root_, next_quality_)` scans the ledger's SHAMap for the smallest key strictly greater than `root_` that is also less than `next_quality_`. This is the first quality directory that actually contains offers. If the book is entirely empty, `succ` returns nothing and `key_` is set to `beast::zero`, which propagates throughout the object as the "empty book" sentinel. + +When a directory is found, the constructor immediately calls `cdirFirst` to load the first page into `sle_` and position `entry_` and `index_` at the first offer. This pre-flight work is intentional: `begin()` copies this already-computed state directly into the returned iterator, so the first dereference costs only a single `view_->read` call to load the offer SLE. + +## begin() / end() Symmetry + +Both `begin()` and `end()` construct a `const_iterator` via the same private constructor that sets `cur_key_` equal to `key_`. The difference is that `begin()` additionally propagates `next_quality_`, `sle_`, `entry_`, and `index_` from the pre-seeded `BookDirs` state. An iterator at the end position is one where `index_` is `beast::zero` and `cur_key_` equals `key_` (the starting key) — a state that `operator++` deliberately reinstates when iteration is exhausted. This symmetric representation means the iterator range is well-defined and comparisons between begin and end naturally resolve to "equal when exhausted." + +## Increment: Cross-Directory Navigation + +`operator++` is the most complex part. It first attempts `cdirNext` to advance to the next offer within the current page chain of the current quality directory. If the page chain is exhausted (`cdirNext` returns false and sets `index_` to zero), the iterator must move to the next quality level. It does this by calling `view_->succ(++cur_key_, next_quality_)`, incrementing `cur_key_` first so the scan starts strictly after the current quality directory's key. If `succ` finds nothing, the loop is over and the iterator resets to the end-sentinel state. If a new quality directory is found, `cdirFirst` positions at its first entry. + +The guard `if (index_ == 0)` after `cdirNext` fails is necessary because `cdirNext` can return false in two situations: the current page has no `sfIndexNext`, and the page itself contained no entries. In the latter case `index_` remains non-zero and the fallback to `succ` is skipped — though in practice a well-formed ledger never has an empty directory, which is why the `cdirFirst` failure path is marked `UNREACHABLE` and excluded from coverage. + +## Lazy Offer Loading with Cache Invalidation + +`operator*` resolves `index_` (the offer key) to a full `SLE` via `view_->read(keylet::offer(index_))`, caching the result in `mutable cache_`. Every call to `operator++` clears `cache_` unconditionally. This lazy-load pattern avoids loading offer SLEs for entries that are iterated past with `operator++` without being dereferenced — important when scanning a large book looking for the first matching offer. + +## Equality and Validity Invariants + +`operator==` begins with a null-pointer check on both `view_` pointers, returning false rather than asserting if either iterator is default-constructed. This guards the sentinel-comparison in range-for loops where the end iterator may never have been assigned a view. After the null guard, an `XRPL_ASSERT` verifies that the two iterators share the same `view_` and `root_` — comparing iterators from different books or different ledger views is a programming error, not a recoverable condition. Equality itself is then determined by `entry_`, `cur_key_`, and `index_` together, which uniquely identifies a position in the two-level directory. + +The static `j_` member on `const_iterator` is a null `beast::Journal` used by deprecated helper functions that require a journal but whose log output is not needed here. It is defined at file scope to satisfy the one-definition rule without introducing a per-instance overhead. \ No newline at end of file diff --git a/src/libxrpl/ledger/BookListeners.cpp.ai.json b/src/libxrpl/ledger/BookListeners.cpp.ai.json new file mode 100644 index 0000000000..612a283a0d --- /dev/null +++ b/src/libxrpl/ledger/BookListeners.cpp.ai.json @@ -0,0 +1,449 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "BookListeners::addSubscriber" + ], + "entry_point": "BookListeners::addSubscriber", + "purpose": "Adds a new InfoSub subscriber to the mListeners map, keyed by its sequence number.", + "validation_points": [ + "sub validated by C++ type system (must be InfoSub::ref, a smart pointer/reference)", + "sub->getSeq() validated by C++ type system (must be valid InfoSub object)" + ] + }, + { + "call_chain": [ + "BookListeners::removeSubscriber" + ], + "entry_point": "BookListeners::removeSubscriber", + "purpose": "Removes a subscriber from mListeners by sequence number.", + "validation_points": [ + "seq validated by C++ type system (must be std::uint64_t)" + ] + }, + { + "call_chain": [ + "BookListeners::publish", + "it->second.lock()", + "p->getSeq()", + "havePublished.emplace()", + "jvObj.visit()", + "p->send()" + ], + "entry_point": "BookListeners::publish", + "purpose": "Publishes a JSON object to all active subscribers, ensuring each subscriber receives it only once per publish call.", + "validation_points": [ + "it->second.lock() validated by std::weak_ptr::lock() (returns nullptr if expired)", + "p->getSeq() validated by C++ type system (must be valid InfoSub object)", + "havePublished.emplace(p->getSeq()) validated by std::unordered_set::emplace (ensures uniqueness)" + ] + } + ], + "data_flows": [ + { + "field": "sub (InfoSub::ref)", + "flow": [ + "addSubscriber parameter", + "sub->getSeq() extracts sequence number", + "mListeners[sub->getSeq()] = sub stores sub in map" + ], + "origin": "Parameter to BookListeners::addSubscriber", + "transformations": [ + "sub is stored as a weak_ptr in mListeners" + ], + "validated_at": "C++ type system (must be InfoSub::ref); sub->getSeq() must be valid" + }, + { + "field": "seq (std::uint64_t)", + "flow": [ + "removeSubscriber parameter", + "mListeners.erase(seq) removes entry" + ], + "origin": "Parameter to BookListeners::removeSubscriber", + "transformations": [ + "Used as key for map erase" + ], + "validated_at": "C++ type system (must be std::uint64_t)" + }, + { + "field": "it->second (std::weak_ptr)", + "flow": [ + "mListeners map", + "it->second.lock() attempts to get shared_ptr", + "If lock() succeeds, p is used; else, entry is erased" + ], + "origin": "Value in mListeners map", + "transformations": [ + "weak_ptr upgraded to shared_ptr if not expired" + ], + "validated_at": "std::weak_ptr::lock() (returns nullptr if expired)" + }, + { + "field": "p->getSeq()", + "flow": [ + "p (InfoSub::pointer) from it->second.lock()", + "p->getSeq() retrieves sequence number", + "havePublished.emplace(p->getSeq()) checks uniqueness" + ], + "origin": "InfoSub object", + "transformations": [ + "Sequence number used to ensure unique publish" + ], + "validated_at": "C++ type system (must be valid InfoSub object)" + }, + { + "field": "havePublished (hash_set&)", + "flow": [ + "publish parameter", + "havePublished.emplace(p->getSeq())", + "If emplace returns true, publish proceeds" + ], + "origin": "Parameter to BookListeners::publish", + "transformations": [ + "Set is updated with new sequence numbers" + ], + "validated_at": "std::unordered_set::emplace (ensures uniqueness)" + }, + { + "field": "jvObj (MultiApiJson const&)", + "flow": [ + "publish parameter", + "jvObj.visit(p->getApiVersion(), lambda)", + "lambda calls p->send(jv, true)" + ], + "origin": "Parameter to BookListeners::publish", + "transformations": [ + "jvObj is visited and transformed to JSON for each subscriber" + ], + "validated_at": "Indirectly validated by type system and lambda signature" + } + ], + "description": "Implements the BookListeners class, which manages subscribers (InfoSub) to book events, allowing adding/removing subscribers and publishing JSON updates to them in a thread-safe manner.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sub (InfoSub::ref)", + "validation", + "missing", + "check" + ], + "evidence": "Field sub (InfoSub::ref) validated by C++ type system, std::weak_ptr, std::unordered_set", + "issue_pattern": "Missing validation for sub (InfoSub::ref)", + "why_false_positive": "C++ type system, std::weak_ptr, std::unordered_set validates sub (InfoSub::ref) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "seq (std::uint64_t)", + "validation", + "missing", + "check" + ], + "evidence": "Field seq (std::uint64_t) validated by C++ type system, std::weak_ptr, std::unordered_set", + "issue_pattern": "Missing validation for seq (std::uint64_t)", + "why_false_positive": "C++ type system, std::weak_ptr, std::unordered_set validates seq (std::uint64_t) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "InfoSub pointer lifetime (via weak_ptr)", + "validation", + "missing", + "check" + ], + "evidence": "Field InfoSub pointer lifetime (via weak_ptr) validated by C++ type system, std::weak_ptr, std::unordered_set", + "issue_pattern": "Missing validation for InfoSub pointer lifetime (via weak_ptr)", + "why_false_positive": "C++ type system, std::weak_ptr, std::unordered_set validates InfoSub pointer lifetime (via weak_ptr) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "duplicate publish prevention (via hash_set)", + "validation", + "missing", + "check" + ], + "evidence": "Field duplicate publish prevention (via hash_set) validated by C++ type system, std::weak_ptr, std::unordered_set", + "issue_pattern": "Missing validation for duplicate publish prevention (via hash_set)", + "why_false_positive": "C++ type system, std::weak_ptr, std::unordered_set validates duplicate publish prevention (via hash_set) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "sub (InfoSub::ref)", + "empty", + "string", + "validation" + ], + "evidence": "C++ type system (smart pointer/reference semantics) at BookListeners::addSubscriber", + "issue_pattern": "Missing empty string validation for sub (InfoSub::ref)", + "why_false_positive": "C++ type system (smart pointer/reference semantics) validates sub (InfoSub::ref) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sub (InfoSub::ref)", + "type", + "validation", + "check" + ], + "evidence": "C++ type system (smart pointer/reference semantics) at BookListeners::addSubscriber", + "issue_pattern": "Missing type validation for sub (InfoSub::ref)", + "why_false_positive": "C++ type system (smart pointer/reference semantics) validates sub (InfoSub::ref) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "sub->getSeq()", + "empty", + "string", + "validation" + ], + "evidence": "C++ type system (method call on InfoSub) at BookListeners::addSubscriber", + "issue_pattern": "Missing empty string validation for sub->getSeq()", + "why_false_positive": "C++ type system (method call on InfoSub) validates sub->getSeq() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sub->getSeq()", + "type", + "validation", + "check" + ], + "evidence": "C++ type system (method call on InfoSub) at BookListeners::addSubscriber", + "issue_pattern": "Missing type validation for sub->getSeq()", + "why_false_positive": "C++ type system (method call on InfoSub) validates sub->getSeq() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "seq (std::uint64_t)", + "empty", + "string", + "validation" + ], + "evidence": "C++ type system at BookListeners::removeSubscriber", + "issue_pattern": "Missing empty string validation for seq (std::uint64_t)", + "why_false_positive": "C++ type system validates seq (std::uint64_t) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "seq (std::uint64_t)", + "type", + "validation", + "check" + ], + "evidence": "C++ type system at BookListeners::removeSubscriber", + "issue_pattern": "Missing type validation for seq (std::uint64_t)", + "why_false_positive": "C++ type system validates seq (std::uint64_t) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "it->second.lock() (weak_ptr to InfoSub)", + "empty", + "string", + "validation" + ], + "evidence": "std::weak_ptr::lock() at BookListeners::publish", + "issue_pattern": "Missing empty string validation for it->second.lock() (weak_ptr to InfoSub)", + "why_false_positive": "std::weak_ptr::lock() validates it->second.lock() (weak_ptr to InfoSub) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "havePublished.emplace(p->getSeq())", + "empty", + "string", + "validation" + ], + "evidence": "std::unordered_set::emplace at BookListeners::publish", + "issue_pattern": "Missing empty string validation for havePublished.emplace(p->getSeq())", + "why_false_positive": "std::unordered_set::emplace validates havePublished.emplace(p->getSeq()) for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/BookListeners.cpp", + "functions": [ + { + "args": [ + "InfoSub::ref sub" + ], + "lineno": 5, + "name": "BookListeners::addSubscriber" + }, + { + "args": [ + "std::uint64_t seq" + ], + "lineno": 11, + "name": "BookListeners::removeSubscriber" + }, + { + "args": [ + "MultiApiJson const& jvObj", + "hash_set& havePublished" + ], + "lineno": 17, + "name": "BookListeners::publish" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "BookListeners is a core ledger notification mechanism. Typical test coverage would be in integration or unit tests for subscription and notification logic, likely in files such as 'BookListeners_test.cpp', 'InfoSub_test.cpp', or broader ledger/subscribe tests. Validation paths (pointer validity, uniqueness, weak_ptr expiration) are generally covered by C++ type safety and STL containers, but edge cases (expired weak_ptrs, duplicate sequence numbers, concurrent access) may not be exhaustively tested unless explicit tests exist for subscriber lifecycle and publish semantics. Gaps may exist in testing concurrent modifications, weak_ptr expiration, and havePublished set behavior under stress.", + "validation_architecture": { + "auto_validated_fields": [ + "sub (InfoSub::ref)", + "seq (std::uint64_t)", + "InfoSub pointer lifetime (via weak_ptr)", + "duplicate publish prevention (via hash_set)" + ], + "framework": "C++ type system, std::weak_ptr, std::unordered_set", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.7, + "error_thrown": "None (undefined behavior if null, but not explicitly checked)", + "field": "sub (InfoSub::ref)", + "location": "BookListeners::addSubscriber", + "validated_by": "C++ type system (smart pointer/reference semantics)", + "validates": [ + "sub is a valid InfoSub::ref (shared_ptr-like)" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "None (would throw if sub is null, but not explicitly checked)", + "field": "sub->getSeq()", + "location": "BookListeners::addSubscriber", + "validated_by": "C++ type system (method call on InfoSub)", + "validates": [ + "getSeq() returns std::uint64_t" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "None", + "field": "seq (std::uint64_t)", + "location": "BookListeners::removeSubscriber", + "validated_by": "C++ type system", + "validates": [ + "seq is a valid uint64_t" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns nullptr if expired)", + "field": "it->second.lock() (weak_ptr to InfoSub)", + "location": "BookListeners::publish", + "validated_by": "std::weak_ptr::lock()", + "validates": [ + "InfoSub still exists (not expired)" + ], + "validation_type": "lifetime/business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns pair, only acts if .second is true)", + "field": "havePublished.emplace(p->getSeq())", + "location": "BookListeners::publish", + "validated_by": "std::unordered_set::emplace", + "validates": [ + "Ensures each subscriber only receives one publish per call" + ], + "validation_type": "business_logic (duplicate prevention)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/BookListeners.cpp.ai.md b/src/libxrpl/ledger/BookListeners.cpp.ai.md new file mode 100644 index 0000000000..6197cd1b20 --- /dev/null +++ b/src/libxrpl/ledger/BookListeners.cpp.ai.md @@ -0,0 +1,33 @@ +# `BookListeners.cpp` — Order Book Subscription Fan-Out + +`BookListeners.cpp` implements the `BookListeners` class, which manages the set of WebSocket/RPC clients subscribed to trade events on a specific order book (a currency pair). It lives in `src/libxrpl/ledger/` and its interface is declared in `include/xrpl/ledger/BookListeners.h`. + +The XRPL subscription model lets clients call `subscribe` with a list of currency-pair books. When a transaction affects any of those books — a new offer placed, an offer consumed, a cross-currency payment executed — the server must push a JSON notification to every interested subscriber. `BookListeners` is the per-book half of that fan-out: one instance exists per tracked `Book`, and it holds the list of `InfoSub` objects that care about that particular pair. + +## Subscriber Storage and Lifetime + +Subscribers are stored as `InfoSub::wptr` (i.e., `std::weak_ptr`) values in a `hash_map mListeners`, keyed by each subscriber's monotonically-increasing sequence number (`mSeq` from `InfoSub`). Using a `weak_ptr` rather than a `shared_ptr` is the critical design choice here: `BookListeners` must not extend a subscriber's lifetime. When the network layer tears down a client connection, the corresponding `InfoSub` should be destroyed regardless of how many book subscription lists it appears in. The `weak_ptr` allows the map to hold a reference without keeping the object alive. + +`addSubscriber()` stores the incoming `InfoSub::ref` (a `shared_ptr const&`) as a weak reference, keyed by `sub->getSeq()`. `removeSubscriber()` erases by sequence number — the caller supplies only the `uint64_t` key, never the object itself, which is safe to do even after the `InfoSub` has been destroyed. Both methods take a `std::lock_guard` on `mLock` (declared as a `std::recursive_mutex` in the header, allowing safe reentrant locking if the mutex is ever held on the calling stack). + +## `publish()` — Lazy Cleanup and Deduplication + +```cpp +void BookListeners::publish(MultiApiJson const& jvObj, hash_set& havePublished) +``` + +`publish()` combines three responsibilities into a single locked pass over `mListeners`: + +**Lazy expiry cleanup.** For each entry, it calls `it->second.lock()` to attempt to promote the `weak_ptr` to a `shared_ptr`. If `lock()` returns null, the subscriber is gone; the entry is erased in-place with `it = mListeners.erase(it)` and iteration continues. This means `mListeners` never requires a separate housekeeping pass — dead entries are evicted the next time any transaction touches that book. The only downside is that a book with no traffic after disconnect will retain stale entries indefinitely, but such books also generate no publish load, so the tradeoff is sound. + +**Cross-book deduplication.** The `havePublished` parameter is a `hash_set` owned by the caller and shared across all `BookListeners::publish()` calls for a single transaction. A subscriber that has registered for multiple books affected by one transaction would otherwise receive the same JSON payload once per matching book. Before sending, `publish()` calls `havePublished.emplace(p->getSeq())`; `emplace` returns a pair whose `.second` is `true` only if the insertion actually happened (i.e., the sequence number was not already present). Only on first occurrence does the code proceed to send. This deduplication is invisible to the subscriber and requires no coordination between individual `BookListeners` instances — the caller provides the shared bookkeeping set. + +**API-version-aware serialization.** Rather than storing a single pre-serialized `Json::Value`, `publish()` receives a `MultiApiJson const&`. `MultiApiJson` is a template struct (`detail::MultiApiJson`) holding an array of `Json::Value` objects — one per supported API version. The `jvObj.visit(p->getApiVersion(), lambda)` call selects the version-specific element at index `apiVersion - MinVer` and passes it to the lambda, which calls `p->send(jv, true)`. This means the caller prepares a single `MultiApiJson` (possibly with version-specific field differences) and each subscriber automatically receives the representation matching its negotiated API version, without any branching in `publish()` itself. + +## Relationship to `OrderBookDB` + +`BookListeners` instances are created and retrieved through `OrderBookDB` (declared in `include/xrpl/ledger/OrderBookDB.h`), which tracks all live order books in the current ledger. When `OrderBookDB::processTxn()` processes an accepted transaction, it identifies which `Book` objects it touched, fetches the corresponding `BookListeners` via `getBookListeners()`, and calls `publish()` on each, threading the same `havePublished` set through every call. `BookListeners` is therefore a leaf node in that pipeline — it does no ledger parsing or book detection, only subscriber dispatch. + +## Thread Safety + +Every public method locks `mLock` with `std::lock_guard`, making `addSubscriber`, `removeSubscriber`, and `publish` safe to call concurrently. The mutex is `std::recursive_mutex` rather than `std::mutex`, which permits a thread that already holds the lock to reacquire it safely — a defensive choice given that callbacks from `InfoSub::send()` could theoretically re-enter ledger notification paths. The RAII guard ensures the mutex is released even if `send()` throws. \ No newline at end of file diff --git a/src/libxrpl/ledger/CachedView.cpp.ai.json b/src/libxrpl/ledger/CachedView.cpp.ai.json new file mode 100644 index 0000000000..53f032bb74 --- /dev/null +++ b/src/libxrpl/ledger/CachedView.cpp.ai.json @@ -0,0 +1,330 @@ +{ + "args": [ + { + "lineno": 7, + "name": "k" + }, + { + "lineno": 12, + "name": "k" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "exists", + "read" + ], + "entry_point": "exists", + "purpose": "Checks if a ledger entry exists for a given Keylet by attempting to read it.", + "validation_points": [ + "read: digest validation (if (!digest))", + "read: sle validation (if (!sle))", + "read: XRPL_ASSERT(sle || baseRead, ...)", + "read: k.check(*sle)" + ] + }, + { + "call_chain": [ + "read" + ], + "entry_point": "read", + "purpose": "Fetches a ledger entry for a given Keylet, using cache and base view, with multiple validation steps.", + "validation_points": [ + "digest validation (if (!digest))", + "sle validation (if (!sle))", + "XRPL_ASSERT(sle || baseRead, ...)", + "k.check(*sle)" + ] + } + ], + "data_flows": [ + { + "field": "k (Keylet)", + "flow": [ + "Parameter to exists/read", + "Used to look up digest in map_", + "Used to call base_.digest(k.key) if not found", + "Used to call base_.read(k)", + "Used in k.check(*sle)" + ], + "origin": "Parameter to exists/read", + "transformations": [ + "Passed as const reference", + "Used to extract k.key", + "Used to validate SLE with k.check" + ], + "validated_at": "k.check(*sle) at end of read" + }, + { + "field": "digest (std::optional)", + "flow": [ + "Looked up in map_ or computed", + "If not present, read returns nullptr", + "If present, used to fetch SLE from cache_" + ], + "origin": "Looked up in map_ or computed via base_.digest(k.key)", + "transformations": [ + "Optional: may be empty if not found", + "Dereferenced if present" + ], + "validated_at": "if (!digest) return nullptr;" + }, + { + "field": "sle (std::shared_ptr)", + "flow": [ + "Fetched from cache_ or base_.read(k)", + "If null, XRPL_ASSERT(sle || baseRead, ...)", + "If null, read returns nullptr", + "If present, validated by k.check(*sle)" + ], + "origin": "Fetched from cache_ or base_.read(k)", + "transformations": [ + "May be null if not found", + "Checked for validity" + ], + "validated_at": "if (!sle) return nullptr; and XRPL_ASSERT(sle || baseRead, ...)" + }, + { + "field": "baseRead (bool)", + "flow": [ + "Initialized false", + "Set true if cache miss and base_.read(k) is called", + "Used in XRPL_ASSERT(sle || baseRead, ...)" + ], + "origin": "Set to true if base_.read(k) is called", + "transformations": [ + "Boolean flag" + ], + "validated_at": "XRPL_ASSERT(sle || baseRead, ...)" + } + ], + "description": "Implements the CachedViewImpl class for XRPL, providing a cached view over ledger state with methods to check existence and read ledger entries, utilizing a cache and base view for efficient access.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "k (Keylet)", + "empty", + "string", + "validation" + ], + "evidence": "k.check(*sle) at CachedViewImpl::read", + "issue_pattern": "Missing empty string validation for k (Keylet)", + "why_false_positive": "k.check(*sle) validates k (Keylet) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "digest (std::optional)", + "empty", + "string", + "validation" + ], + "evidence": "if (!digest) at CachedViewImpl::read", + "issue_pattern": "Missing empty string validation for digest (std::optional)", + "why_false_positive": "if (!digest) validates digest (std::optional) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle (std::shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "if (!sle) at CachedViewImpl::read", + "issue_pattern": "Missing empty string validation for sle (std::shared_ptr)", + "why_false_positive": "if (!sle) validates sle (std::shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "baseRead (bool)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(sle || baseRead, ...) at CachedViewImpl::read", + "issue_pattern": "Missing empty string validation for baseRead (bool)", + "why_false_positive": "XRPL_ASSERT(sle || baseRead, ...) validates baseRead (bool) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/CachedView.cpp", + "functions": [ + { + "args": [ + "Keylet const& k" + ], + "lineno": 7, + "name": "exists" + }, + { + "args": [ + "Keylet const& k" + ], + "lineno": 12, + "name": "read" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "detail" + } + ], + "test_coverage_notes": "The code is likely covered by unit/integration tests for ledger views and caching in the XRPLF/rippled codebase. Tests would be found in files like 'test/ledger/CachedView_test.cpp', 'test/ledger/View_test.cpp', or similar. These would test cache hits/misses, correct validation of Keylet and SLE, and correct handling of missing entries. However, edge cases such as concurrent access, assertion failures (XRPL_ASSERT), and invalid SLEs may not be fully covered unless explicitly tested. There may be gaps in testing for race conditions or rare assertion failures.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns nullptr (no exception)", + "field": "k (Keylet)", + "location": "CachedViewImpl::read", + "validated_by": "k.check(*sle)", + "validates": [ + "Ensures the SLE (ledger entry) matches the Keylet's expected type/format", + "Prevents returning an SLE that does not correspond to the requested Keylet" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns nullptr (no exception)", + "field": "digest (std::optional)", + "location": "CachedViewImpl::read", + "validated_by": "if (!digest)", + "validates": [ + "Ensures a digest was found for the key", + "Prevents further processing if digest is missing" + ], + "validation_type": "type/presence" + }, + { + "confidence": 1.0, + "error_thrown": "returns nullptr (no exception)", + "field": "sle (std::shared_ptr)", + "location": "CachedViewImpl::read", + "validated_by": "if (!sle)", + "validates": [ + "Ensures the ledger entry was found and is not null" + ], + "validation_type": "type/presence" + }, + { + "confidence": 0.9, + "error_thrown": "XRPL_ASSERT (likely aborts or logs error, depending on build)", + "field": "baseRead (bool)", + "location": "CachedViewImpl::read", + "validated_by": "XRPL_ASSERT(sle || baseRead, ...)", + "validates": [ + "Ensures that if sle is null, baseRead must be true (i.e., a base read was attempted and failed)", + "Invariant check to catch unexpected null SLEs" + ], + "validation_type": "business_logic/invariant" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/CachedView.cpp.ai.md b/src/libxrpl/ledger/CachedView.cpp.ai.md new file mode 100644 index 0000000000..3a5e6e2309 --- /dev/null +++ b/src/libxrpl/ledger/CachedView.cpp.ai.md @@ -0,0 +1,47 @@ +# `CachedView.cpp` — Two-Level Ledger Entry Cache + +## Role in the System + +`CachedView.cpp` implements `CachedViewImpl::read()` and `CachedViewImpl::exists()`, the two methods that differentiate a `CachedView` from its underlying `DigestAwareReadView`. The file's entire purpose is to intercept ledger entry reads and serve them from a two-tier caching structure, avoiding redundant cryptographic lookups and deserialization during transaction processing. + +`CachedView` is used throughout the XRPL transaction engine whenever a ledger must be read many times during a single validation pass. Rather than repeatedly hitting the underlying SHAMap or database, callers construct a `CachedView` wrapping a closed ledger and pass it around for the duration of the processing batch. + +## The Two-Level Cache Design + +The caching strategy is split across two distinct stores, each with a different scope and key scheme: + +**Level 1 — `map_` (per-view, key → digest):** An `unordered_map` owned by the `CachedViewImpl` instance. It maps a ledger entry's raw 256-bit key to its content digest (hash). This mapping is per-view — it reflects which keys exist in *this* ledger version and what their hashes are. + +**Level 2 — `cache_` (shared, digest → SLE):** A `CachedSLEs` instance, which is a `TaggedCache`. This shared cache is keyed by the content digest, not the ledger key. Multiple `CachedView` instances, potentially for different ledger versions, share a single `CachedSLEs`. Since `SLE` objects are immutable and content-addressed by their hash, the same in-memory `SLE` can be safely reused across different ledger views when both versions contain an identical entry. + +## Read Path Through `read()` + +The `read()` method follows a precise sequence that reflects the cost hierarchy of each lookup: + +1. **Local digest lookup (under lock):** The method first checks `map_` under `mutex_` to see if this view has already resolved the key to a digest. If found, the lock is released immediately and no call to `base_` is needed for this phase. + +2. **Base digest lookup (lockless):** If the key isn't in `map_`, `base_.digest(key)` is called without holding any lock. For a SHAMap-backed ledger, this traverses the trie to produce the cryptographic hash of the entry. If the digest is absent, the key doesn't exist in this ledger and `nullptr` is returned. + +3. **SLE fetch from TaggedCache:** `cache_.fetch(digest, handler)` is called. The `TaggedCache` first checks its own internal store. If present (and not GC'd to a weak pointer), the SLE is returned without invoking the handler. If absent, the handler calls `base_.read(k)` to deserialize the SLE from the SHAMap, and the result is inserted into the `TaggedCache` under the digest key. + +4. **Key→digest backfill:** Only after a full miss — when the key was not found in `map_` — does the code re-acquire `mutex_` to insert `(key, digest)` into `map_`. The comment "Avoid acquiring this lock unless necessary" explicitly acknowledges this deferral: the expensive work (`base_.digest()`, `base_.read()`) is done outside the critical section, and the lock is taken only for the cheap map insertion. + +5. **Keylet type validation:** Even after retrieving a valid SLE, `k.check(*sle)` is called. A `Keylet` carries both a key and an expected SLE type. Because the shared `TaggedCache` is keyed by digest and not Keylet, a `read()` for one type could theoretically retrieve an SLE for another if two Keylets share the same raw key. `k.check()` catches this at the boundary and returns `nullptr` on a type mismatch. + +## Cache Hit Metrics + +Three `static` `CountedObjects::Counter` instances track hit quality: + +- **`CachedView::hit`**: The key was in `map_` (digest known), and the SLE was still live in `CachedSLEs`. Full cache satisfaction — no base reads. +- **`CachedView::hitExpired`**: The key was in `map_` (digest known from a prior read), but the SLE had been evicted from `CachedSLEs`. The view knew the entry existed but still had to call `base_.read()`. This "warm miss" is cheaper than a cold miss because `base_.digest()` is skipped. +- **`CachedView::miss`**: The key was not in `map_` at all — both `base_.digest()` and (on success) `base_.read()` were called. + +The `XRPL_ASSERT(sle || baseRead, ...)` guards against an impossible state: if `baseRead` is false, it means the `TaggedCache` claimed to have an entry but returned null — which would indicate corruption in the cache internals. + +## Concurrency Model + +`mutex_` protects only `map_`. The `TaggedCache` (`CachedSLEs`) manages its own internal mutex. The design is deliberately layered to minimize contention: the outer lock guards the cheap key→digest map, while the heavier `TaggedCache` lock is not held during `base_.digest()` or `base_.read()` calls. This allows concurrent readers on different keys to proceed in parallel through the expensive base-read path, serializing only briefly to update the local map and within `TaggedCache` for its own internals. + +## Class Hierarchy Note + +The `CachedViewImpl` implementation class (in `detail::`) is separated from the public `CachedView` template. `CachedViewImpl` holds a non-owning reference (`base_`) to the underlying view; `CachedView` adds an owning `shared_ptr` (`sp_`) to keep the base alive, and its `base()` accessor exposes the raw underlying view — intentionally bypassing the cache for callers that need the original object directly. This two-class split avoids template instantiation of the `read()`/`exists()` bodies for every concrete `Base` type. \ No newline at end of file diff --git a/src/libxrpl/ledger/CanonicalTXSet.cpp.ai.json b/src/libxrpl/ledger/CanonicalTXSet.cpp.ai.json new file mode 100644 index 0000000000..b908c5bc5b --- /dev/null +++ b/src/libxrpl/ledger/CanonicalTXSet.cpp.ai.json @@ -0,0 +1,204 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "CanonicalTXSet::insert", + "CanonicalTXSet::accountKey", + "txn->getAccountID(sfAccount)", + "txn->getSeqProxy()", + "txn->getTransactionID()" + ], + "entry_point": "CanonicalTXSet::insert", + "purpose": "Inserts a transaction into the canonical transaction set, using a composite key derived from account, sequence proxy, and transaction ID.", + "validation_points": [ + "No explicit validation in this chain; assumes input transaction is already validated elsewhere." + ] + }, + { + "call_chain": [ + "CanonicalTXSet::popAcctTransaction", + "CanonicalTXSet::accountKey", + "tx->getAccountID(sfAccount)", + "tx->getSeqProxy()", + "map_.lower_bound(after)", + "itrNext->second->getSeqProxy()" + ], + "entry_point": "CanonicalTXSet::popAcctTransaction", + "purpose": "Finds and removes the next valid transaction for an account, prioritizing sequence-based transactions over ticket-based ones.", + "validation_points": [ + "Checks if next transaction is for the same account and if sequence is consecutive or ticket is valid." + ] + }, + { + "call_chain": [ + "operator<" + ], + "entry_point": "operator< (CanonicalTXSet::Key)", + "purpose": "Defines ordering for transactions in the set, used for sorting and lookup.", + "validation_points": [ + "No explicit validation; pure comparison logic." + ] + } + ], + "data_flows": [ + { + "field": "AccountID", + "flow": [ + "STTx (txn) object", + "CanonicalTXSet::insert or popAcctTransaction", + "CanonicalTXSet::accountKey", + "uint256 ret (accountKey)", + "Key (for map_)" + ], + "origin": "txn->getAccountID(sfAccount)", + "transformations": [ + "Copied into uint256, XORed with salt_" + ], + "validated_at": "No explicit validation in this file; assumes AccountID is valid from STTx" + }, + { + "field": "SeqProxy", + "flow": [ + "STTx (txn) object", + "CanonicalTXSet::insert or popAcctTransaction", + "Key (for map_)" + ], + "origin": "txn->getSeqProxy()", + "transformations": [ + "Used as part of composite key" + ], + "validated_at": "Checked in popAcctTransaction for isSeq() and consecutive value" + }, + { + "field": "TransactionID", + "flow": [ + "STTx (txn) object", + "CanonicalTXSet::insert", + "Key (for map_)" + ], + "origin": "txn->getTransactionID()", + "transformations": [ + "Used as part of composite key" + ], + "validated_at": "No explicit validation in this file" + }, + { + "field": "Key (CanonicalTXSet::Key)", + "flow": [ + "CanonicalTXSet::insert", + "map_ (std::map)", + "CanonicalTXSet::popAcctTransaction (lookup and erase)" + ], + "origin": "Constructed from accountKey, SeqProxy, TransactionID", + "transformations": [ + "Ordering via operator<" + ], + "validated_at": "Checked in popAcctTransaction for account match and sequence/ticket logic" + } + ], + "description": "Implements logic for ordering and managing transactions in a canonical set for the XRPL ledger, including insertion, ordering, and popping of account transactions.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/CanonicalTXSet.cpp", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 5, + "name": "operator<" + }, + { + "args": [ + "account" + ], + "lineno": 20, + "name": "CanonicalTXSet::accountKey" + }, + { + "args": [ + "txn" + ], + "lineno": 28, + "name": "CanonicalTXSet::insert" + }, + { + "args": [ + "tx" + ], + "lineno": 36, + "name": "CanonicalTXSet::popAcctTransaction" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code in CanonicalTXSet.cpp is typically tested via higher-level transaction processing and ledger tests, such as those in 'test/ledger/CanonicalTXSet_test.cpp', 'test/app/tx/TransactionQueue_test.cpp', or integration tests that exercise transaction ordering and queuing. Direct unit tests for insert, popAcctTransaction, and ordering logic may exist, but edge cases (e.g., malformed AccountID, invalid sequence proxies, or salt collisions) may not be fully covered. There is no explicit validation of input data in this file; it relies on upstream validation (e.g., STTx construction and transaction submission). Gaps may exist in testing for unusual or malicious input, especially around accountKey salt handling and sequence/ticket edge cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None", + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/CanonicalTXSet.cpp.ai.md b/src/libxrpl/ledger/CanonicalTXSet.cpp.ai.md new file mode 100644 index 0000000000..ce6748ca16 --- /dev/null +++ b/src/libxrpl/ledger/CanonicalTXSet.cpp.ai.md @@ -0,0 +1,59 @@ +# `CanonicalTXSet.cpp` — Ordered Transaction Queue for Consensus Retry + +`CanonicalTXSet` is the data structure that holds transactions which could not be applied during one pass of consensus and need to be retried on the next. The word "canonical" refers not to uniqueness but to the deterministic, agreed-upon order in which every validating node will process these transactions — a prerequisite for reaching identical ledger states across the network. + +## The Core Sorting Problem + +When the network closes a ledger, it applies a set of agreed-upon transactions. Some transactions fail on the first attempt — often because a prerequisite transaction from the same account hasn't been applied yet — and these are deferred into a `CanonicalTXSet` for retry. The critical invariant is that every node must iterate through these deferred transactions in the same order, so the struct is built around a `std::map` sorted by a carefully designed three-part composite key. + +## The `Key` Structure and Its Ordering + +The `Key` type encodes three fields: a salted 256-bit account identifier, a `SeqProxy` (which is either a sequence number or a ticket number), and the transaction hash as a tiebreaker. The `operator<` in `CanonicalTXSet.cpp` makes the ordering explicit: + +``` +account_ → seqProxy_ → txId_ +``` + +This groups all transactions from the same account together, then sorts them within that group by `SeqProxy`. The tie on `txId_` is just for determinism when two transactions share an account and a `SeqProxy` value (an edge case that should not arise in valid traffic, but the key must still define a strict weak order). + +The `SeqProxy` comparison itself (defined in `SeqProxy.h`) has a notable property: sequences always sort before tickets regardless of their numeric values. A `SeqProxy` of type `seq` with value 1000 sorts before a `SeqProxy` of type `ticket` with value 1. This is intentional — `TicketCreate` transactions (which use sequences) must be applied before any transaction that consumes one of those tickets. The sort order enforces this dependency mechanically. + +## The Salt: Anti-Mining Protection + +`accountKey()` performs a small but important transformation: + +```cpp +uint256 ret = beast::zero; +memcpy(ret.begin(), account.begin(), account.size()); +ret ^= salt_; +return ret; +``` + +The `AccountID` (a 20-byte hash) is embedded in a 256-bit zero-initialized value and XORed with `salt_`, which is derived from the `LedgerHash` of the just-closed ledger. Without this salt, an attacker could deliberately mine an `AccountID` with a numerically low value, guaranteeing their transactions are processed first in every ledger forever. By rotating the salt each ledger, the account ordering is unpredictable from ledger to ledger, making such an attack economically infeasible. + +The salt is passed in at construction time and can be refreshed via `reset(LedgerHash const&)`, which also clears the map. This is how the set is recycled between consensus rounds. + +## `insert()` — Straightforward Key Construction + +`insert()` is thin: it constructs a `Key` from the salted account, the transaction's `SeqProxy` (sequence or ticket), and the transaction hash, then inserts the `(Key, shared_ptr)` pair into `map_`. There is no deduplication guard here; callers are expected to avoid inserting the same transaction twice. Validation that the transaction is well-formed has already happened upstream. + +## `popAcctTransaction()` — The Most Complex Logic + +This is the function the ledger-building code calls after successfully applying a transaction from the set. Given the transaction just applied, it looks for the next eligible transaction from the same account and removes it from the map. + +The lookup uses `map_.lower_bound(after)` where `after` is a key constructed with `txId_ = beast::zero`. Since `zero` is the smallest possible `uint256`, this finds the first key strictly after the current position for this account and `SeqProxy`. The code then checks two conditions on the candidate: + +1. **Same account** — the iterator must still be in the same account's slot. If the candidate belongs to a different account, there is no next transaction to return. +2. **Valid successor** — the next transaction is eligible if it uses a ticket (tickets can be applied in any order) *or* if its sequence number is exactly `seqProxy.value() + 1` (sequences must be consecutive). This models the XRPL rule that sequence-based transactions form a chain; you cannot apply sequence 5 if 4 has not been applied yet. + +If both conditions are met, the transaction is moved out of the map and returned to the caller, which can immediately attempt to apply it. If either condition fails, `nullptr` is returned and the caller knows there is no immediately eligible follow-up for this account. + +## Usage in `BuildLedger.cpp` + +`applyTransactions()` in `BuildLedger.cpp` iterates the `CanonicalTXSet` linearly across multiple passes (up to `LEDGER_TOTAL_PASSES`). On each pass it calls `applyTransaction()` for each entry and erases successes and definitive failures, leaving only retryable transactions for the next pass. `popAcctTransaction()` is used by the open-ledger path (`OpenLedger`) to chain an account's transactions without waiting for an explicit retry sweep. + +## Invariants and Design Tradeoffs + +The design trusts that transactions inserted into the set are already validated — there is no re-checking of signatures or format inside `CanonicalTXSet`. Ownership of the `STTx` objects is shared via `std::shared_ptr`, so the map holds a reference without taking exclusive ownership; the same transaction object may be referenced from multiple places while retry is in progress. + +`equality` on `Key` is defined solely on `txId_`, not on the full composite key. This means two `Key`s with the same transaction hash but different account/seqProxy values compare equal. In practice this should never occur (the transaction hash is derived from the full transaction content which includes the account and sequence), but the asymmetry between `operator==` and `operator<` is worth noting when reading the code. \ No newline at end of file diff --git a/src/libxrpl/ledger/Dir.cpp.ai.json b/src/libxrpl/ledger/Dir.cpp.ai.json new file mode 100644 index 0000000000..f7da9b5441 --- /dev/null +++ b/src/libxrpl/ledger/Dir.cpp.ai.json @@ -0,0 +1,367 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Dir::Dir" + ], + "entry_point": "Dir::Dir", + "purpose": "Constructs a Dir object, initializes pointers to SLE and indexes.", + "validation_points": [ + "if (sle_ != nullptr) // Validates sle_ before dereferencing", + "if (!indexes_->empty()) // Validates indexes_ before use" + ] + }, + { + "call_chain": [ + "Dir::begin", + "const_iterator::const_iterator" + ], + "entry_point": "Dir::begin", + "purpose": "Creates a const_iterator at the beginning of the directory.", + "validation_points": [ + "if (sle_ != nullptr) // Validates sle_ before use", + "if (!indexes_->empty()) // Validates indexes_ before use" + ] + }, + { + "call_chain": [ + "const_iterator::operator*" + ], + "entry_point": "const_iterator::operator*", + "purpose": "Dereferences the iterator to access the current ledger entry.", + "validation_points": [ + "XRPL_ASSERT(index_ != beast::zero, ...) // Validates index_ before use" + ] + }, + { + "call_chain": [ + "const_iterator::operator++", + "const_iterator::next_page" + ], + "entry_point": "const_iterator::operator++", + "purpose": "Advances the iterator to the next entry, possibly to the next page.", + "validation_points": [ + "XRPL_ASSERT(index_ != beast::zero, ...) // Validates index_ before increment", + "XRPL_ASSERT(sle_, ...) // Validates sle_ after reading next page" + ] + }, + { + "call_chain": [ + "const_iterator::operator==" + ], + "entry_point": "const_iterator::operator==", + "purpose": "Compares two iterators for equality.", + "validation_points": [ + "XRPL_ASSERT(view_ == other.view_ && root_.key == other.root_.key, ...) // Validates views and roots" + ] + } + ], + "data_flows": [ + { + "field": "sle_", + "flow": [ + "Dir::Dir: sle_ = view_->read(root_)", + "Dir::begin: if (sle_ != nullptr) { it.sle_ = sle_; ... }", + "const_iterator::next_page: sle_ = view_->read(page_)" + ], + "origin": "view_->read(root_) in Dir::Dir", + "transformations": [ + "Assigned from view_->read", + "Passed to iterator", + "Reassigned on page change" + ], + "validated_at": "Dir::Dir (nullptr check), Dir::begin (nullptr check), const_iterator::next_page (XRPL_ASSERT)" + }, + { + "field": "indexes_", + "flow": [ + "Dir::Dir: indexes_ = &sle_->getFieldV256(sfIndexes)", + "Dir::begin: if (!indexes_->empty()) { ... }", + "const_iterator::next_page: indexes_ = &sle_->getFieldV256(sfIndexes)" + ], + "origin": "sle_->getFieldV256(sfIndexes) in Dir::Dir", + "transformations": [ + "Pointer to V256 field extracted from SLE", + "Checked for emptiness before use", + "Reassigned on page change" + ], + "validated_at": "Dir::begin (empty() check), const_iterator::next_page (empty() check)" + }, + { + "field": "index_", + "flow": [ + "Dir::begin: index_ = *it_", + "const_iterator::operator*: XRPL_ASSERT(index_ != beast::zero)", + "const_iterator::operator++: XRPL_ASSERT(index_ != beast::zero)", + "const_iterator::next_page: index_ = *it_ or beast::zero" + ], + "origin": "Set from *it_ in Dir::begin or next_page", + "transformations": [ + "Set from iterator over indexes_", + "Checked for nonzero before dereference/increment" + ], + "validated_at": "const_iterator::operator*, operator++, operator++(int), next_page (XRPL_ASSERT)" + }, + { + "field": "view_", + "flow": [ + "Dir::Dir: view_ = &view", + "const_iterator::const_iterator: view_ = &view", + "const_iterator::operator==: XRPL_ASSERT(view_ == other.view_)" + ], + "origin": "Passed to Dir::Dir and const_iterator", + "transformations": [ + "Pointer assignment", + "Compared for equality" + ], + "validated_at": "const_iterator::operator== (XRPL_ASSERT)" + }, + { + "field": "root_.key", + "flow": [ + "Dir::Dir: root_ = key", + "const_iterator::const_iterator: root_ = root", + "const_iterator::operator==: XRPL_ASSERT(root_.key == other.root_.key)" + ], + "origin": "Passed to Dir::Dir from Keylet", + "transformations": [ + "Assignment", + "Compared for equality" + ], + "validated_at": "const_iterator::operator== (XRPL_ASSERT)" + } + ], + "description": "Implements the Dir and Dir::const_iterator classes for iterating over directory nodes in the XRPL ledger, providing methods for traversal and access to ledger entries.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle_ (pointer to SLE)", + "empty", + "string", + "validation" + ], + "evidence": "nullptr check at Dir::Dir (constructor), Dir::begin", + "issue_pattern": "Missing empty string validation for sle_ (pointer to SLE)", + "why_false_positive": "nullptr check validates sle_ (pointer to SLE) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sle_ (pointer to SLE)", + "type", + "validation", + "check" + ], + "evidence": "nullptr check at Dir::Dir (constructor), Dir::begin", + "issue_pattern": "Missing type validation for sle_ (pointer to SLE)", + "why_false_positive": "nullptr check validates sle_ (pointer to SLE) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "indexes_ (pointer to V256 field)", + "empty", + "string", + "validation" + ], + "evidence": "empty() check at Dir::begin, const_iterator::next_page", + "issue_pattern": "Missing empty string validation for indexes_ (pointer to V256 field)", + "why_false_positive": "empty() check validates indexes_ (pointer to V256 field) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "index_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at const_iterator::operator*(), const_iterator::operator++(), const_iterator::operator++(int)", + "issue_pattern": "Missing empty string validation for index_", + "why_false_positive": "XRPL_ASSERT validates index_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle_ (pointer to SLE)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at const_iterator::next_page", + "issue_pattern": "Missing empty string validation for sle_ (pointer to SLE)", + "why_false_positive": "XRPL_ASSERT validates sle_ (pointer to SLE) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "view_ and other.view_, root_.key and other.root_.key", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at const_iterator::operator==", + "issue_pattern": "Missing empty string validation for view_ and other.view_, root_.key and other.root_.key", + "why_false_positive": "XRPL_ASSERT validates view_ and other.view_, root_.key and other.root_.key for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/Dir.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "Keylet const& key" + ], + "lineno": 6, + "name": "Dir::Dir" + }, + { + "args": [], + "lineno": 13, + "name": "Dir::begin" + }, + { + "args": [], + "lineno": 28, + "name": "Dir::end" + }, + { + "args": [ + "const_iterator const& other" + ], + "lineno": 33, + "name": "const_iterator::operator==" + }, + { + "args": [], + "lineno": 43, + "name": "const_iterator::operator*" + }, + { + "args": [], + "lineno": 51, + "name": "const_iterator::operator++" + }, + { + "args": [ + "int" + ], + "lineno": 61, + "name": "const_iterator::operator++" + }, + { + "args": [], + "lineno": 68, + "name": "const_iterator::next_page" + }, + { + "args": [], + "lineno": 89, + "name": "const_iterator::page_size" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is core ledger iteration logic. Typical test coverage would be in integration and unit tests for directory traversal, such as in 'test/ledger/Directory_test.cpp', 'test/ledger/ReadView_test.cpp', or higher-level transaction/ledger tests. Direct validation paths (e.g., XRPL_ASSERTs, nullptr checks) are not always directly tested unless assertions are triggered by malformed data or edge cases (e.g., empty directories, missing SLEs, zero indexes). Gaps may exist in negative testing (e.g., what happens if sle_ is nullptr, or indexes_ is empty), and in coverage of multi-page directory traversal. Fuzzing or property-based tests may be needed for full validation path coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), nullptr checks, STL container checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (conditional logic, not exception)", + "field": "sle_ (pointer to SLE)", + "location": "Dir::Dir (constructor), Dir::begin", + "validated_by": "nullptr check", + "validates": [ + "Checks if sle_ is not nullptr before dereferencing or accessing fields" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logic, not exception)", + "field": "indexes_ (pointer to V256 field)", + "location": "Dir::begin, const_iterator::next_page", + "validated_by": "empty() check", + "validates": [ + "Checks if indexes_ is not empty before iterating" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "index_", + "location": "const_iterator::operator*(), const_iterator::operator++(), const_iterator::operator++(int)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures index_ is not beast::zero before dereferencing or incrementing" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "sle_ (pointer to SLE)", + "location": "const_iterator::next_page", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures sle_ is not null after reading next page" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "view_ and other.view_, root_.key and other.root_.key", + "location": "const_iterator::operator==", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures both iterators have matching views and roots before comparing" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/Dir.cpp.ai.md b/src/libxrpl/ledger/Dir.cpp.ai.md new file mode 100644 index 0000000000..09293e364e --- /dev/null +++ b/src/libxrpl/ledger/Dir.cpp.ai.md @@ -0,0 +1,48 @@ +# `src/libxrpl/ledger/Dir.cpp` + +## Role in the System + +XRPL's ledger organizes collections of related objects — such as all buy or sell offers for a given NFT — into paged linked-list structures called *directory nodes*. Each `DirectoryNode` (`ltDIR_NODE`) SLE holds a `STVector256` field (`sfIndexes`) containing a list of `uint256` keys pointing to the actual ledger objects on that page. Pages are chained together via a `sfIndexNext` field (a `uint64`; zero means last page). `Dir.cpp` wraps this storage model in a standard C++ forward-iterable range, hiding all the page-chasing and SLE loading behind a familiar `begin()`/`end()` interface suitable for range-based `for` loops. + +As of mid-2024, `Dir` is used specifically for NFTokenOffer directories and unit tests. The parallel class `BookDirs` serves order-book directories, which require the additional complexity of traversing across quality-keyed subdirectories. `Dir` handles the simpler case where a single root keylet anchors a flat, linearly-paged directory. + +## `Dir`: The Range Adaptor + +`Dir` is intentionally thin. Its constructor takes a `ReadView const&` and a root `Keylet`, immediately reads the root SLE via `view_->read(root_)`, and stores a pointer to the `sfIndexes` vector within that SLE. Construction is cheap — no per-entry loading occurs. If `sle_` is `nullptr` (the directory root doesn't exist in the ledger), `indexes_` stays null, and `begin()` returns an iterator in the end-sentinel state, making an absent directory safely iterable as an empty range. + +`end()` is always `const_iterator(*view_, root_, root_)` — a freshly-constructed iterator with `page_` set to `root_` and `index_` at its zero-initialized state. This choice matters for how equality comparison works. + +## `const_iterator`: State Machine Across Pages + +The iterator carries enough state to walk the multi-page structure: + +- `sle_` — shared ownership of the current page's SLE, keeping it alive. +- `indexes_` — a raw pointer directly into `sle_`'s `sfIndexes` field data. This is safe because `sle_` keeps the SLE alive for the iterator's lifetime. +- `it_` — a `std::vector::const_iterator` positioned within `*indexes_`. +- `index_` — the `uint256` key of the current entry (a copy, used as the canonical position marker). +- `page_` — the `Keylet` of the page currently being iterated. +- `cache_` — a `mutable std::optional` for the SLE that `index_` points to. + +### End Sentinel Encoding + +Rather than a separate boolean flag, the end state is encoded structurally: `page_.key == root_.key && index_ == beast::zero`. Both `begin()` (when the directory is empty or missing) and `end()` produce iterators in this state, so `operator==` compares `page_.key` and `index_` to determine equality. Crucially, both iterators must share the same `view_` pointer and `root_.key`, which is enforced via `XRPL_ASSERT` in `operator==`. Comparing iterators from different directories is a programming error caught at assertion level, not a silent logic bug. + +### Lazy Dereference with `cache_` + +`operator*()` does not read the target SLE eagerly. It only calls `view_->read(keylet::child(index_))` on first access, storing the result in `cache_`. Advancing the iterator clears `cache_` by resetting it to `std::nullopt`. This is a meaningful optimization: callers that iterate directories to filter by key, without always needing the full SLE, avoid the cost of loading entries they don't use. + +### Page Transitions via `next_page()` + +When `operator++()` exhausts the current page (`++it_` reaches `std::end(*indexes_)`), it delegates to `next_page()`. This method reads `sfIndexNext` from the current SLE. A value of zero means the directory is fully traversed: `page_` is reset to `root_` and `index_` to `beast::zero`, converging the iterator to the end-sentinel. A non-zero value constructs `keylet::page(root_, next)`, reads the corresponding SLE, updates `indexes_` and `it_`, and loads the first entry key. An `XRPL_ASSERT` guards against a missing page SLE, since a non-zero `sfIndexNext` pointing to a non-existent page is a ledger integrity violation. + +`next_page()` is also exposed as a public method on the iterator (declared in `Dir.h`), enabling callers to skip the remainder of the current page and jump directly to the start of the next one — an accelerated traversal pattern for code that processes one page at a time rather than one entry at a time. + +## Relationship to `BookDirs` + +`BookDirs.cpp` implements the analogous class for order books, using the lower-level `cdirFirst`/`cdirNext` helpers from `DirectoryHelpers.h`. `Dir` bypasses those helpers and accesses the directory SLE structure directly. The design tradeoff is that `Dir` is simpler and more transparent but assumes a single-root, quality-flat directory — assumptions that hold for NFT offer directories but not for order books spanning multiple quality levels. + +## Invariants and Guards + +- `index_ != beast::zero` is asserted before any dereference or increment, preventing use of a consumed or never-initialized iterator. +- `sle_ != nullptr` is checked softly (conditional) at construction time and asserted hard (`XRPL_ASSERT`) during page transitions, reflecting the difference between "directory doesn't exist yet" (legitimate) and "linked page is missing" (invariant violation). +- `indexes_->empty()` is checked before assigning `it_`, since an empty page at root level is valid (the root can be empty while subsequent pages are not), but advancing an iterator into an empty vector would be undefined behavior. \ No newline at end of file diff --git a/src/libxrpl/ledger/Ledger.cpp.ai.json b/src/libxrpl/ledger/Ledger.cpp.ai.json new file mode 100644 index 0000000000..033d78095b --- /dev/null +++ b/src/libxrpl/ledger/Ledger.cpp.ai.json @@ -0,0 +1,803 @@ +{ + "args": [ + { + "lineno": 12, + "name": "create_genesis_t" + } + ], + "classes": [ + { + "args": [ + "SHAMap::const_iterator" + ], + "lineno": 13, + "name": "Ledger::sles_iter_impl" + }, + { + "args": [ + "bool", + "SHAMap::const_iterator" + ], + "lineno": 41, + "name": "Ledger::txs_iter_impl" + } + ], + "code_paths": [ + { + "call_chain": [ + "Ledger::sles_iter_impl::equal", + "dynamic_cast(&impl)", + "operator== on SHAMap::const_iterator" + ], + "entry_point": "Ledger::sles_iter_impl::equal", + "purpose": "Checks if two sles_iter_impl iterators are equal (used for iteration/validation in SHAMap traversal)", + "validation_points": [ + "dynamic_cast ensures impl is of correct type before comparing iterators" + ] + }, + { + "call_chain": [ + "Ledger::txs_iter_impl::equal", + "dynamic_cast(&impl)", + "operator== on SHAMap::const_iterator" + ], + "entry_point": "Ledger::txs_iter_impl::equal", + "purpose": "Checks if two txs_iter_impl iterators are equal (used for iteration/validation in SHAMap traversal)", + "validation_points": [ + "dynamic_cast ensures impl is of correct type before comparing iterators" + ] + }, + { + "call_chain": [ + "Ledger::sles_iter_impl::dereference", + "SerialIter sit(iter_->slice())", + "std::make_shared(sit, iter_->key())" + ], + "entry_point": "Ledger::sles_iter_impl::dereference", + "purpose": "Dereferences the iterator to produce an SLE object from the SHAMap node", + "validation_points": [ + "Implicit validation: SerialIter construction will throw if data is malformed" + ] + }, + { + "call_chain": [ + "Ledger::txs_iter_impl::dereference", + "if (metadata_) Ledger::deserializeTxPlusMeta(item) else Ledger::deserializeTx(item)" + ], + "entry_point": "Ledger::txs_iter_impl::dereference", + "purpose": "Dereferences the iterator to produce a transaction (and optionally metadata) from the SHAMap node", + "validation_points": [ + "metadata_ is validated by constructor type system (bool)", + "SHAMap::const_iterator dereference is implicitly validated" + ] + }, + { + "call_chain": [ + "Ledger::Ledger", + "SHAMapType::TRANSACTION, SHAMapType::STATE (enum validation)", + "rawInsert(sle)" + ], + "entry_point": "Ledger::Ledger (constructor)", + "purpose": "Constructs a new Ledger, initializing SHAMaps and inserting genesis SLEs", + "validation_points": [ + "SHAMapType is validated by enum type system", + "SLE construction validates field types" + ] + } + ], + "data_flows": [ + { + "field": "SHAMap::const_iterator iter_", + "flow": [ + "SHAMap::const_iterator created by SHAMap traversal", + "Passed to sles_iter_impl/txs_iter_impl", + "Used in dereference() and equal()" + ], + "origin": "Passed into sles_iter_impl/txs_iter_impl constructors", + "transformations": [ + "None (iterator is copied and incremented)" + ], + "validated_at": "dynamic_cast in equal(), dereference via SerialIter construction" + }, + { + "field": "metadata_ (bool)", + "flow": [ + "txs_iter_impl(bool metadata, SHAMap::const_iterator iter)", + "Stored as member", + "Used in dereference() to select deserialization path" + ], + "origin": "Passed to txs_iter_impl constructor", + "transformations": [ + "None (bool flag)" + ], + "validated_at": "Constructor type system (bool)" + }, + { + "field": "SHAMapType (enum)", + "flow": [ + "Ledger::Ledger", + "SHAMapType::TRANSACTION, SHAMapType::STATE passed to SHAMap constructors", + "Used to distinguish between transaction and state maps" + ], + "origin": "Ledger constructor", + "transformations": [ + "None (enum value)" + ], + "validated_at": "Enum type system" + }, + { + "field": "SLE (Serialized Ledger Entry)", + "flow": [ + "SerialIter constructed from SHAMap node slice", + "SLE constructed from SerialIter and key", + "Returned to caller" + ], + "origin": "Created in sles_iter_impl::dereference and Ledger::Ledger", + "transformations": [ + "Deserialization from binary to SLE object" + ], + "validated_at": "SerialIter and SLE constructors (will throw on malformed data)" + } + ], + "description": "Implements the Ledger class for the XRPL, handling ledger state, transactions, fees, amendments, negative UNL, and related operations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "metadata_ (bool)", + "validation", + "missing", + "check" + ], + "evidence": "Field metadata_ (bool) validated by C++ type system, dynamic_cast, exception handling (RAII)", + "issue_pattern": "Missing validation for metadata_ (bool)", + "why_false_positive": "C++ type system, dynamic_cast, exception handling (RAII) validates metadata_ (bool) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "SHAMapType (enum)", + "validation", + "missing", + "check" + ], + "evidence": "Field SHAMapType (enum) validated by C++ type system, dynamic_cast, exception handling (RAII)", + "issue_pattern": "Missing validation for SHAMapType (enum)", + "why_false_positive": "C++ type system, dynamic_cast, exception handling (RAII) validates SHAMapType (enum) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "SHAMap::const_iterator (type, but not range/validity)", + "validation", + "missing", + "check" + ], + "evidence": "Field SHAMap::const_iterator (type, but not range/validity) validated by C++ type system, dynamic_cast, exception handling (RAII)", + "issue_pattern": "Missing validation for SHAMap::const_iterator (type, but not range/validity)", + "why_false_positive": "C++ type system, dynamic_cast, exception handling (RAII) validates SHAMap::const_iterator (type, but not range/validity) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "SHAMap::const_iterator iter (for sles_iter_impl and txs_iter_impl)", + "empty", + "string", + "validation" + ], + "evidence": "dynamic_cast in equal() at sles_iter_impl::equal, txs_iter_impl::equal", + "issue_pattern": "Missing empty string validation for SHAMap::const_iterator iter (for sles_iter_impl and txs_iter_impl)", + "why_false_positive": "dynamic_cast in equal() validates SHAMap::const_iterator iter (for sles_iter_impl and txs_iter_impl) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "SHAMap::const_iterator iter (for sles_iter_impl and txs_iter_impl)", + "type", + "validation", + "check" + ], + "evidence": "dynamic_cast in equal() at sles_iter_impl::equal, txs_iter_impl::equal", + "issue_pattern": "Missing type validation for SHAMap::const_iterator iter (for sles_iter_impl and txs_iter_impl)", + "why_false_positive": "dynamic_cast in equal() validates SHAMap::const_iterator iter (for sles_iter_impl and txs_iter_impl) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "SHAMap::const_iterator iter (for dereference)", + "empty", + "string", + "validation" + ], + "evidence": "implicit via dereference and SerialIter construction at sles_iter_impl::dereference, txs_iter_impl::dereference", + "issue_pattern": "Missing empty string validation for SHAMap::const_iterator iter (for dereference)", + "why_false_positive": "implicit via dereference and SerialIter construction validates SHAMap::const_iterator iter (for dereference) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "metadata_ (bool)", + "empty", + "string", + "validation" + ], + "evidence": "constructor type system at txs_iter_impl::txs_iter_impl", + "issue_pattern": "Missing empty string validation for metadata_ (bool)", + "why_false_positive": "constructor type system validates metadata_ (bool) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "metadata_ (bool)", + "type", + "validation", + "check" + ], + "evidence": "constructor type system at txs_iter_impl::txs_iter_impl", + "issue_pattern": "Missing type validation for metadata_ (bool)", + "why_false_positive": "constructor type system validates metadata_ (bool) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "SHAMapType enum (TRANSACTION, STATE)", + "empty", + "string", + "validation" + ], + "evidence": "enum type system at Ledger::Ledger (constructor)", + "issue_pattern": "Missing empty string validation for SHAMapType enum (TRANSACTION, STATE)", + "why_false_positive": "enum type system validates SHAMapType enum (TRANSACTION, STATE) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "SHAMapType enum (TRANSACTION, STATE)", + "type", + "validation", + "check" + ], + "evidence": "enum type system at Ledger::Ledger (constructor)", + "issue_pattern": "Missing type validation for SHAMapType enum (TRANSACTION, STATE)", + "why_false_positive": "enum type system validates SHAMapType enum (TRANSACTION, STATE) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/Ledger.cpp", + "functions": [ + { + "args": [ + "create_genesis_t", + "Rules const&", + "Fees const&", + "std::vector const&", + "Family&" + ], + "lineno": 54, + "name": "Ledger::Ledger" + }, + { + "args": [ + "LedgerHeader const&", + "bool&", + "bool", + "Rules const&", + "Fees const&", + "Family&", + "beast::Journal" + ], + "lineno": 99, + "name": "Ledger::Ledger" + }, + { + "args": [ + "Ledger const&", + "NetClock::time_point" + ], + "lineno": 134, + "name": "Ledger::Ledger" + }, + { + "args": [ + "LedgerHeader const&", + "Rules const&", + "Family&" + ], + "lineno": 163, + "name": "Ledger::Ledger" + }, + { + "args": [ + "std::uint32_t", + "NetClock::time_point", + "Rules const&", + "Fees const&", + "Family&" + ], + "lineno": 172, + "name": "Ledger::Ledger" + }, + { + "args": [ + "bool" + ], + "lineno": 184, + "name": "Ledger::setImmutable" + }, + { + "args": [ + "NetClock::time_point", + "NetClock::duration", + "bool" + ], + "lineno": 200, + "name": "Ledger::setAccepted" + }, + { + "args": [ + "SLE const&" + ], + "lineno": 210, + "name": "Ledger::addSLE" + }, + { + "args": [ + "SHAMapItem const&" + ], + "lineno": 217, + "name": "Ledger::deserializeTx" + }, + { + "args": [ + "SHAMapItem const&" + ], + "lineno": 222, + "name": "Ledger::deserializeTxPlusMeta" + }, + { + "args": [ + "Keylet const&" + ], + "lineno": 237, + "name": "Ledger::exists" + }, + { + "args": [ + "uint256 const&" + ], + "lineno": 243, + "name": "Ledger::exists" + }, + { + "args": [ + "uint256 const&", + "std::optional const&" + ], + "lineno": 247, + "name": "Ledger::succ" + }, + { + "args": [ + "Keylet const&" + ], + "lineno": 255, + "name": "Ledger::read" + }, + { + "args": [], + "lineno": 267, + "name": "Ledger::slesBegin" + }, + { + "args": [], + "lineno": 272, + "name": "Ledger::slesEnd" + }, + { + "args": [ + "uint256 const&" + ], + "lineno": 277, + "name": "Ledger::slesUpperBound" + }, + { + "args": [], + "lineno": 282, + "name": "Ledger::txsBegin" + }, + { + "args": [], + "lineno": 287, + "name": "Ledger::txsEnd" + }, + { + "args": [ + "uint256 const&" + ], + "lineno": 292, + "name": "Ledger::txExists" + }, + { + "args": [ + "key_type const&" + ], + "lineno": 296, + "name": "Ledger::txRead" + }, + { + "args": [ + "key_type const&" + ], + "lineno": 308, + "name": "Ledger::digest" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 318, + "name": "Ledger::rawErase" + }, + { + "args": [ + "uint256 const&" + ], + "lineno": 323, + "name": "Ledger::rawErase" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 328, + "name": "Ledger::rawInsert" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 335, + "name": "Ledger::rawReplace" + }, + { + "args": [ + "uint256 const&", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 342, + "name": "Ledger::rawTxInsert" + }, + { + "args": [ + "uint256 const&", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 353, + "name": "Ledger::rawTxInsertWithHash" + }, + { + "args": [], + "lineno": 368, + "name": "Ledger::setup" + }, + { + "args": [ + "Keylet const&" + ], + "lineno": 426, + "name": "Ledger::peek" + }, + { + "args": [], + "lineno": 434, + "name": "Ledger::negativeUNL" + }, + { + "args": [], + "lineno": 453, + "name": "Ledger::validatorToDisable" + }, + { + "args": [], + "lineno": 463, + "name": "Ledger::validatorToReEnable" + }, + { + "args": [], + "lineno": 473, + "name": "Ledger::updateNegativeUNL" + }, + { + "args": [ + "beast::Journal", + "bool" + ], + "lineno": 504, + "name": "Ledger::walkLedger" + }, + { + "args": [], + "lineno": 541, + "name": "Ledger::isSensible" + }, + { + "args": [], + "lineno": 553, + "name": "Ledger::updateSkipList" + }, + { + "args": [], + "lineno": 599, + "name": "Ledger::isFlagLedger" + }, + { + "args": [], + "lineno": 602, + "name": "Ledger::isVotingLedger" + }, + { + "args": [], + "lineno": 606, + "name": "Ledger::unshare" + }, + { + "args": [], + "lineno": 611, + "name": "Ledger::invariants" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "Ledger iteration and SHAMap traversal are typically tested in unit tests under 'src/test/ledger/' and 'src/test/shamap/'. Tests like Ledger_test.cpp, SHAMap_test.cpp, and Transaction_test.cpp cover iterator correctness, dereferencing, and error handling. However, dynamic_cast validation in equal() is rarely directly tested; most tests focus on iterator behavior and correctness, not explicit type safety. There may be limited or no tests that intentionally pass the wrong iterator type to equal() to test the dynamic_cast failure path. Also, implicit validation via SerialIter construction is only tested indirectly via deserialization tests; malformed data edge cases may not be exhaustively covered.", + "validation_architecture": { + "auto_validated_fields": [ + "metadata_ (bool)", + "SHAMapType (enum)", + "SHAMap::const_iterator (type, but not range/validity)" + ], + "framework": "C++ type system, dynamic_cast, exception handling (RAII)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns false if type mismatch)", + "field": "SHAMap::const_iterator iter (for sles_iter_impl and txs_iter_impl)", + "location": "sles_iter_impl::equal, txs_iter_impl::equal", + "validated_by": "dynamic_cast in equal()", + "validates": [ + "Checks that the base_type reference is actually of the expected derived type before comparing iterators" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "likely std::exception or custom exception if iter_ is invalid or points to corrupt data", + "field": "SHAMap::const_iterator iter (for dereference)", + "location": "sles_iter_impl::dereference, txs_iter_impl::dereference", + "validated_by": "implicit via dereference and SerialIter construction", + "validates": [ + "Assumes iter_ points to a valid SHAMap item", + "SerialIter expects valid serialized data", + "If not, construction of SLE or deserialization will throw" + ], + "validation_type": "type/format" + }, + { + "confidence": 1.0, + "error_thrown": "compile-time type error if not bool", + "field": "metadata_ (bool)", + "location": "txs_iter_impl::txs_iter_impl", + "validated_by": "constructor type system", + "validates": [ + "metadata_ must be a boolean, enforced by constructor signature" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "compile-time type error if not valid enum", + "field": "SHAMapType enum (TRANSACTION, STATE)", + "location": "Ledger::Ledger (constructor)", + "validated_by": "enum type system", + "validates": [ + "txMap_ and stateMap_ must be constructed with valid SHAMapType enum values" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/Ledger.cpp.ai.md b/src/libxrpl/ledger/Ledger.cpp.ai.md new file mode 100644 index 0000000000..07e6b28420 --- /dev/null +++ b/src/libxrpl/ledger/Ledger.cpp.ai.md @@ -0,0 +1,57 @@ +# `src/libxrpl/ledger/Ledger.cpp` + +## Role and Purpose + +`Ledger.cpp` provides the concrete implementation of the `Ledger` class — the central data structure in the XRPL that represents a complete, cryptographically-committed snapshot of global state at a single point in time. Every operation in the ledger pipeline, from genesis bootstrap to consensus acceptance, flows through this file. It sits at the boundary between the high-level view abstractions (`ReadView`, `TxsRawView`) and the low-level content-addressed storage layer (`SHAMap`). + +A `Ledger` object owns two `SHAMap` trees: `stateMap_` holds all persistent state entries (`SLE` objects — accounts, order books, escrows, etc.), and `txMap_` holds the transactions and their metadata that were applied to produce this ledger. The `LedgerHeader` bundles metadata including sequence number, close timestamps, the SHA-256 hash roots of both maps, total XRP in existence, and consensus timing parameters. + +## Construction Variants + +Five constructors exist, each serving a distinct lifecycle role: + +**Genesis constructor** (`create_genesis_t`) builds ledger 1 from scratch. It hard-derives the "master" account by calling `generateKeyPair` on the string seed `"masterpassphrase"` and assigns it the entire `INITIAL_XRP` supply. This is a deterministic, network-wide constant. The constructor also conditionally writes fee parameters in either the classic integer format (`sfBaseFee`) or the newer `featureXRPFees` drops format (`sfBaseFeeDrops`), depending on which amendments are enabled at genesis. This forward-compatibility is key for testing environments that boot with modern amendments already active. + +**Successor constructor** (`Ledger const& prevLedger, NetClock::time_point`) creates a new mutable ledger that follows a previous one. Crucially, it copies `prevLedger.stateMap_` with `true` (copy-on-write mode) so the new ledger starts with the full prior state without duplicating memory. The `txMap_` starts empty since no transactions have been applied yet. Close-time resolution is recalculated via `getNextLedgerTimeResolution`, which adjusts the resolution window up or down based on whether the previous ledger's close time achieved consensus. + +**Load constructor** (`LedgerHeader, bool& loaded, bool acquire`) reconstructs a ledger from its serialized header and known SHAMap root hashes. It fetches the roots from the node store and marks `loaded = false` if either is missing, optionally triggering acquisition from the network via `family.missingNodeAcquireByHash()`. + +**Header-only constructor** (`LedgerHeader, Rules, Family`) builds an immutable ledger for reference purposes when only the header is known — used in validation pipelines where the full state tree is not needed. It computes `header_.hash` immediately via `calculateLedgerHash`. + +**Database constructor** constructs a blank mutable ledger for a given sequence and close time, used when hydrating ledgers from local storage. + +## The Mutable/Immutable Transition + +The most architecturally significant design decision is the strict mutable-to-immutable state machine. While mutable, a ledger is exclusive to one writer and needs no locking. `setImmutable(rehash=true)` finalizes the ledger by computing `txHash` and `accountHash` from the respective `SHAMap::getHash()` roots, then combining them via `calculateLedgerHash` into the canonical `header_.hash`. After this call, both SHAMaps are also marked immutable, preventing any further modifications. Immutable ledgers can then be safely shared across threads. The header comment documents this explicitly: mutable ledgers cannot be shared; immutable ones need no locks. + +`setAccepted()` extends this by recording the consensus close time and setting `sLCF_NoConsensusTime` in `closeFlags` when the network did not agree on a precise close time. It then delegates to `setImmutable()` to finalize the hash. + +## `setup()`: Deriving Runtime State from Ledger Content + +`setup()` is called from both constructors and `setImmutable()`, and its job is to populate the in-memory `rules_` and `fees_` fields by reading the actual on-ledger SLEs. This design means network fee and amendment configuration is not stored separately — it is always derived from the ledger state itself, ensuring all nodes derive the same values from the same canonical data. + +The fee loading handles a format migration: old ledgers store fees as plain integers (`sfBaseFee` as `uint64`, `sfReserveBase` as `uint32`), while post-`featureXRPFees` ledgers store them as `STAmount` in drops (`sfBaseFeeDrops`). The function validates that a ledger does not contain both formats simultaneously, and that new-format fees only appear after the `featureXRPFees` amendment activates. Either condition sets the return value to `false`, signalling a malformed ledger. + +## Iterator Architecture + +The inner classes `sles_iter_impl` and `txs_iter_impl` implement the type-erased `iter_base` interface defined in `ReadView::detail::ReadViewFwdRange`. Rather than exposing SHAMap iterators directly (which would leak the internal representation), `Ledger` wraps them in heap-allocated polymorphic objects and returns `std::unique_ptr`. The `sles_type` and `txs_type` range types provide standard range-based `for` iteration to callers. + +`txs_iter_impl` carries a `metadata_` flag, initialized to `!open()`. Closed ledgers pack each transaction SHAMap item as `addVL(txBytes) || addVL(metaBytes)`, so iteration calls `deserializeTxPlusMeta` and returns both. Open ledgers store only the raw transaction bytes, so `metadata_` is `false` and iteration calls `deserializeTx`, returning a null metadata pointer. This dual-path deserialization is a protocol-level invariant: metadata only exists in a closed ledger. + +## Raw Mutation Primitives + +`rawInsert`, `rawReplace`, `rawErase`, and `rawTxInsert` are the low-level write interface. They directly manipulate the backing SHAMaps and throw `LogicError` on contract violations (duplicate inserts, missing keys for replace/erase). The `raw` prefix is intentional — callers are responsible for maintaining ledger invariants. These are not guarded against logical errors; they detect programming mistakes in the caller. + +`rawTxInsertWithHash` extends `rawTxInsert` by computing and returning the SHA-512 half-hash of the resulting SHAMap leaf node (using `HashPrefix::txNode`, the item data, and the key). This hash directly identifies the transaction's storage location in the tree and is used for efficient transaction proof generation without a full tree traversal. + +## Skip List Maintenance + +`updateSkipList()` implements a two-tier skip list for fast ledger hash lookup by sequence. It maintains two on-ledger SLEs: a rolling window of the last 256 parent hashes (sliding window, oldest evicted when full), and a sparse list where every 256th ledger stores its hash as a permanent record. This combination enables O(1) lookup for recent ledgers and O(1) lookup at 256-ledger granularity for deep history, supporting the `getLedgerHashForSeq` operation efficiently. + +## Negative UNL Support + +The negative UNL (nUNL) is a per-ledger list of validators temporarily removed from the effective quorum because they appear offline. `negativeUNL()` reads the `sfDisabledValidators` array from the `keylet::negativeUNL()` SLE and returns validator public keys. `updateNegativeUNL()` applies pending enable/disable transitions: it replaces the disabled list in-place, removes the `sfValidatorToDisable`/`sfValidatorToReEnable` pending fields, and either updates or erases the SLE if the resulting list is empty. Per the header documentation, this must only be called at flag ledgers and before applying `UNLModify` transactions. + +## Integrity Verification + +`walkLedger()` traverses both SHAMaps from their roots, collecting any missing nodes, and returns `false` if any gaps exist. It supports parallel traversal of the state map (using 32 worker threads via `walkMapParallel`), useful for validation of large ledgers during sync. `isSensible()` performs a quick consistency check — verifying that the header hash roots match the actual SHAMap hashes — and is used as a fast sanity gate before more expensive processing. \ No newline at end of file diff --git a/src/libxrpl/ledger/OpenView.cpp.ai.json b/src/libxrpl/ledger/OpenView.cpp.ai.json new file mode 100644 index 0000000000..33bf76107f --- /dev/null +++ b/src/libxrpl/ledger/OpenView.cpp.ai.json @@ -0,0 +1,517 @@ +{ + "args": [ + { + "lineno": 8, + "name": "metadata" + }, + { + "lineno": 8, + "name": "iter" + }, + { + "lineno": 44, + "name": "rhs" + }, + { + "lineno": 51, + "name": "base" + }, + { + "lineno": 52, + "name": "rules" + }, + { + "lineno": 53, + "name": "hold" + }, + { + "lineno": 155, + "name": "fee" + }, + { + "lineno": 143, + "name": "sle" + }, + { + "lineno": 65, + "name": "to" + }, + { + "lineno": 85, + "name": "k" + }, + { + "lineno": 89, + "name": "key" + }, + { + "lineno": 89, + "name": "last" + }, + { + "lineno": 164, + "name": "txn" + }, + { + "lineno": 164, + "name": "metaData" + } + ], + "classes": [ + { + "args": [ + "bool metadata", + "txs_map::const_iterator iter" + ], + "lineno": 5, + "name": "OpenView::txs_iter_impl" + }, + { + "args": [ + "OpenView const& rhs", + "open_ledger_t, ReadView const* base, Rules const& rules, std::shared_ptr hold", + "ReadView const* base, std::shared_ptr hold" + ], + "lineno": 44, + "name": "OpenView" + } + ], + "code_paths": [ + { + "call_chain": [ + "OpenView::OpenView", + "base->header()", + "base->rules()", + "base->open()" + ], + "entry_point": "OpenView::OpenView (constructor)", + "purpose": "Constructs an OpenView, initializing from a base ReadView and copying/deriving ledger state.", + "validation_points": [ + "base->header()", + "base->rules()", + "base->open()" + ] + }, + { + "call_chain": [ + "OpenView::txsBegin", + "txs_iter_impl constructor" + ], + "entry_point": "OpenView::txsBegin", + "purpose": "Begins iteration over transactions in the OpenView.", + "validation_points": [ + "txs_iter_impl::equal (uses dynamic_cast for type validation)" + ] + }, + { + "call_chain": [ + "OpenView::apply", + "items_.apply(to)", + "to.rawTxInsert" + ], + "entry_point": "OpenView::apply", + "purpose": "Applies all pending changes and transactions in the OpenView to another TxsRawView.", + "validation_points": [] + }, + { + "call_chain": [ + "OpenView::exists", + "items_.exists(*base_, k)" + ], + "entry_point": "OpenView::exists", + "purpose": "Checks if a ledger entry exists in the OpenView.", + "validation_points": [] + }, + { + "call_chain": [ + "OpenView::read", + "items_.read(*base_, k)" + ], + "entry_point": "OpenView::read", + "purpose": "Reads a ledger entry from the OpenView.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "base_ (ReadView const*)", + "flow": [ + "Constructor argument", + "Assigned to base_", + "Used in header_ initialization (base_->header())", + "Used in rules_ initialization (base_->rules())", + "Used in open_ initialization (base_->open())", + "Used in items_.* calls" + ], + "origin": "Constructor argument to OpenView", + "transformations": [ + "header_ fields may be modified (validated/accepted/seq/parentCloseTime/parentHash)" + ], + "validated_at": "base_->header(), base_->rules(), base_->open() (assume these validate base_)" + }, + { + "field": "impl (txs_iter_impl)", + "flow": [ + "Created in txsBegin()", + "Used in iteration", + "Compared in txs_iter_impl::equal via dynamic_cast" + ], + "origin": "txs_iter_impl instance in txs_type::iter_base", + "transformations": [ + "dynamic_cast(&impl) checks type" + ], + "validated_at": "txs_iter_impl::equal" + }, + { + "field": "header_ (LedgerHeader)", + "flow": [ + "Copied from base_->header() in constructor", + "Possibly modified (validated/accepted/seq/parentCloseTime/parentHash)", + "Returned by OpenView::header()" + ], + "origin": "base_->header()", + "transformations": [ + "Fields set to false or incremented in open_ledger_t constructor" + ], + "validated_at": "base_->header()" + }, + { + "field": "rules_ (Rules)", + "flow": [ + "Copied from base_->rules() or passed in", + "Returned by OpenView::rules()" + ], + "origin": "base_->rules() or constructor argument", + "transformations": [], + "validated_at": "base_->rules()" + }, + { + "field": "txs_ (transaction map)", + "flow": [ + "Populated by OpenView methods (not shown in this file)", + "Iterated in txsBegin()/txsEnd()", + "Applied in OpenView::apply" + ], + "origin": "Constructed empty or copied from rhs.txs_", + "transformations": [], + "validated_at": "No explicit validation in this file" + } + ], + "description": "Implements the OpenView class for managing an open ledger view in the XRPL, including transaction iteration, insertion, and ledger state manipulation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "impl (dynamic type of iterator implementation)", + "empty", + "string", + "validation" + ], + "evidence": "dynamic_cast(&impl) at txs_iter_impl::equal", + "issue_pattern": "Missing empty string validation for impl (dynamic type of iterator implementation)", + "why_false_positive": "dynamic_cast(&impl) validates impl (dynamic type of iterator implementation) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "impl (dynamic type of iterator implementation)", + "type", + "validation", + "check" + ], + "evidence": "dynamic_cast(&impl) at txs_iter_impl::equal", + "issue_pattern": "Missing type validation for impl (dynamic type of iterator implementation)", + "why_false_positive": "dynamic_cast(&impl) validates impl (dynamic type of iterator implementation) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "base (pointer to ReadView)", + "empty", + "string", + "validation" + ], + "evidence": "base->header(), base->rules(), base->open() at OpenView constructors", + "issue_pattern": "Missing empty string validation for base (pointer to ReadView)", + "why_false_positive": "base->header(), base->rules(), base->open() validates base (pointer to ReadView) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/OpenView.cpp", + "functions": [ + { + "args": [], + "lineno": 61, + "name": "OpenView::txCount" + }, + { + "args": [ + "to" + ], + "lineno": 65, + "name": "OpenView::apply" + }, + { + "args": [], + "lineno": 73, + "name": "OpenView::header" + }, + { + "args": [], + "lineno": 77, + "name": "OpenView::fees" + }, + { + "args": [], + "lineno": 81, + "name": "OpenView::rules" + }, + { + "args": [ + "k" + ], + "lineno": 85, + "name": "OpenView::exists" + }, + { + "args": [ + "key", + "last" + ], + "lineno": 89, + "name": "OpenView::succ" + }, + { + "args": [ + "k" + ], + "lineno": 95, + "name": "OpenView::read" + }, + { + "args": [], + "lineno": 99, + "name": "OpenView::slesBegin" + }, + { + "args": [], + "lineno": 103, + "name": "OpenView::slesEnd" + }, + { + "args": [ + "key" + ], + "lineno": 107, + "name": "OpenView::slesUpperBound" + }, + { + "args": [], + "lineno": 111, + "name": "OpenView::txsBegin" + }, + { + "args": [], + "lineno": 115, + "name": "OpenView::txsEnd" + }, + { + "args": [ + "key" + ], + "lineno": 119, + "name": "OpenView::txExists" + }, + { + "args": [ + "key" + ], + "lineno": 123, + "name": "OpenView::txRead" + }, + { + "args": [ + "sle" + ], + "lineno": 143, + "name": "OpenView::rawErase" + }, + { + "args": [ + "sle" + ], + "lineno": 147, + "name": "OpenView::rawInsert" + }, + { + "args": [ + "sle" + ], + "lineno": 151, + "name": "OpenView::rawReplace" + }, + { + "args": [ + "fee" + ], + "lineno": 155, + "name": "OpenView::rawDestroyXRP" + }, + { + "args": [ + "key", + "txn", + "metaData" + ], + "lineno": 164, + "name": "OpenView::rawTxInsert" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is core ledger infrastructure, so it is likely tested indirectly via higher-level ledger and transaction tests (e.g., in unit tests for ledger views, transaction application, and ledger state transitions). Direct tests for OpenView's validation logic (dynamic_cast in txs_iter_impl::equal, base_ pointer validity) are unlikely to exist unless there are explicit iterator or view tests. Gaps: No explicit tests for dynamic_cast failure in txs_iter_impl::equal, or for invalid base_ pointers. Most validation is defensive (type checks, pointer dereference), not business logic validation.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual C++ type checks, dynamic_cast, pointer dereference)", + "validation_layer": "business_logic (within class methods and constructors)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns false if type mismatch)", + "field": "impl (dynamic type of iterator implementation)", + "location": "txs_iter_impl::equal", + "validated_by": "dynamic_cast(&impl)", + "validates": [ + "Checks that the base_type reference is actually a txs_iter_impl before comparing iterators" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "undefined behavior if null (no explicit check)", + "field": "base (pointer to ReadView)", + "location": "OpenView constructors", + "validated_by": "base->header(), base->rules(), base->open()", + "validates": [ + "Assumes base is a valid, non-null pointer before dereferencing for header, rules, open" + ], + "validation_type": "type (pointer dereference, assumes non-null)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/OpenView.cpp.ai.md b/src/libxrpl/ledger/OpenView.cpp.ai.md new file mode 100644 index 0000000000..ba5833e1ac --- /dev/null +++ b/src/libxrpl/ledger/OpenView.cpp.ai.md @@ -0,0 +1,56 @@ +# `OpenView.cpp` — Mutable Ledger Scratchpad for Transaction Processing + +`OpenView` is the writable in-memory layer that sits above a read-only committed ledger during transaction processing on the XRPL. Where `ReadView` provides a snapshot of settled ledger state, `OpenView` accumulates pending mutations — state-object changes and new transactions — without touching the underlying base. When all desired transactions have been applied, the caller calls `apply()` to flush those changes into the next committed view. + +## Architecture: Two Independent Delta Layers + +`OpenView` maintains two separate deltas over its `base_` (`ReadView const*`): + +- **State delta** (`items_`, a `detail::RawStateTable`): tracks inserts, erases, and replacements of `SLE` (serialized ledger entries). Lookups in `exists()`, `read()`, `succ()`, and the SLE iterators all route through `RawStateTable`, which merges local mutations with the base view transparently. +- **Transaction delta** (`txs_`, a `txs_map`): an ordered map from transaction hash (`uint256`) to a `txData` struct holding serialized blobs for the transaction body and its metadata. + +This split is intentional. State entries and transaction entries have different lifecycles and access patterns: state is frequently read back during the same transaction processing pass, while transactions are write-once and only iterated at commit time. + +## Constructors and Ledger Header Setup + +There are three constructors covering distinct use cases: + +The `open_ledger_t`-tagged constructor creates a fresh view for building the *next* ledger on top of a closed parent. It explicitly mutates the copied header: `seq` is incremented by one, `parentCloseTime` and `parentHash` are set from the base header, and `validated`/`accepted` are cleared to false. This enforces the invariant that an open (in-flight) ledger is never mistakenly treated as final. + +The bare `ReadView const*` constructor copies the header and rules verbatim, inheriting `open_` from the base. This is used when building a last-closed ledger view where header mutation has already been done externally. + +The copy constructor makes a shallow structural copy — the `SLE` objects referenced by `items_` are shared (they are immutable, so sharing is safe) but the map itself is duplicated. This is used to take snapshots before speculative execution. Note that move assignment is deleted while move construction is allowed, which prevents accidental overwrites of existing views. + +The `batch_view_t` constructor wraps another `OpenView` as a stacking layer for batch transaction processing. It captures the current `txCount()` as `baseTxCount_`, which ensures that `txCount()` returns the total transaction count including any previously applied transactions. This ordinal is used when computing transaction metadata apply-order during ledger close. + +## Memory Management Strategy + +Both `items_` and `txs_` use `boost::container::pmr::monotonic_buffer_resource` for their allocators. This is a bump-pointer arena: a 256 KB block is pre-allocated at construction, and map nodes are carved out of it without individual heap allocations. When the arena fills, it falls back to heap. The design trades deallocation granularity (individual map entries cannot be freed) for throughput — transaction processing is latency-sensitive and map churn is high. + +The `monotonic_resource_` member is a `unique_ptr` declared before both `txs_` and `items_`, ensuring it is destroyed *after* them (C++ member destruction is reverse-declaration-order). This is the only lifetime guarantee the arena has, and the header comment makes this dependency explicit. + +The `hold_` member is a `shared_ptr` — an intentionally opaque ownership anchor. The caller can pass any reference-counted object (e.g., the base ledger itself, a cache entry) and it will be kept alive for as long as the `OpenView` exists, preventing the base pointer from dangling. + +## Transaction Iteration: `txs_iter_impl` + +The private `txs_iter_impl` class adapts the flat `txs_map` iterator into the polymorphic `txs_type::iter_base` interface used by `ReadView`. It carries two pieces of state: the underlying `txs_map::const_iterator` and a `metadata_` flag. + +The `metadata_` flag is set to `!open()`. Open ledgers do not produce transaction metadata (metadata is only computed at close time), so when iterating transactions on an open view the metadata blob is simply omitted. For closed-ledger views the metadata is deserialized alongside the transaction body. + +Deserialization in `dereference()` is on-demand: the raw `Serializer` blobs are decoded into `STTx` and `STObject` only when the iterator is dereferenced. This avoids paying parsing costs for transactions that are never inspected. + +The `equal()` method guards against iterator cross-type comparison by using `dynamic_cast`. If the passed `base_type` is not a `txs_iter_impl`, it returns false rather than undefined behavior. + +## `txRead()` and the Fallback Pattern + +`txRead()` demonstrates the two-layer lookup directly: it first checks the local `txs_` map. On a miss it falls back to `base_->txRead()`. This means a view can see transactions that came from the committed base ledger as well as newly inserted ones in the same interface. The metadata nullability check is explicit — during open-ledger processing, `meta` is a null `shared_ptr` by convention, and the caller receives `nullptr` for the second element of the returned pair. + +## `apply()` and Commit Flow + +`apply(TxsRawView& to)` is the one-way commit operation. It first delegates `items_.apply(to)` to flush all state mutations, then iterates `txs_` and calls `to.rawTxInsert()` for each transaction. This ordering means state changes always precede transaction insertion when composing into the target view, which is the expected ledger close sequence. + +## Error Handling and Open Issues + +`rawTxInsert()` enforces uniqueness: inserting a duplicate transaction hash triggers `LogicError`, which throws and is treated as a programming bug rather than a recoverable runtime condition. Duplicate transactions in a single ledger would violate fundamental XRPL consensus invariants. + +`rawDestroyXRP()` delegates fee burning to `items_.destroyXRP(fee)` but carries a `VFALCO`-attributed comment questioning whether `header_.totalDrops` should also be decremented and how child views should propagate that change. This unresolved design question suggests that XRP supply accounting across stacked views may not be fully consistent in all code paths. \ No newline at end of file diff --git a/src/libxrpl/ledger/PaymentSandbox.cpp.ai.json b/src/libxrpl/ledger/PaymentSandbox.cpp.ai.json new file mode 100644 index 0000000000..f66f80ff12 --- /dev/null +++ b/src/libxrpl/ledger/PaymentSandbox.cpp.ai.json @@ -0,0 +1,651 @@ +{ + "args": [ + { + "lineno": 9, + "name": "a1" + }, + { + "lineno": 9, + "name": "a2" + }, + { + "lineno": 9, + "name": "c" + }, + { + "lineno": 17, + "name": "sender" + }, + { + "lineno": 17, + "name": "receiver" + }, + { + "lineno": 17, + "name": "amount" + }, + { + "lineno": 17, + "name": "preCreditSenderBalance" + }, + { + "lineno": 48, + "name": "preCreditBalanceHolder" + }, + { + "lineno": 48, + "name": "preCreditBalanceIssuer" + }, + { + "lineno": 91, + "name": "issue" + }, + { + "lineno": 91, + "name": "origBalance" + }, + { + "lineno": 108, + "name": "id" + }, + { + "lineno": 108, + "name": "cur" + }, + { + "lineno": 108, + "name": "next" + }, + { + "lineno": 129, + "name": "main" + }, + { + "lineno": 129, + "name": "other" + }, + { + "lineno": 129, + "name": "currency" + }, + { + "lineno": 148, + "name": "mptID" + }, + { + "lineno": 154, + "name": "to" + }, + { + "lineno": 196, + "name": "account" + }, + { + "lineno": 196, + "name": "issuer" + }, + { + "lineno": 299, + "name": "from" + }, + { + "lineno": 299, + "name": "to" + }, + { + "lineno": 299, + "name": "preCreditBalance" + }, + { + "lineno": 288, + "name": "count" + }, + { + "lineno": 348, + "name": "view" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "PaymentSandbox::creditIOU (likely, not shown in snippet)", + "DeferredCredits::creditIOU" + ], + "entry_point": "DeferredCredits::creditIOU", + "purpose": "Records a deferred IOU credit between two accounts, updating internal credit tracking structures.", + "validation_points": [ + "DeferredCredits::creditIOU: XRPL_ASSERT(sender != receiver)", + "DeferredCredits::creditIOU: XRPL_ASSERT(!amount.negative())", + "DeferredCredits::creditIOU: XRPL_ASSERT(amount.holds())" + ] + }, + { + "call_chain": [ + "PaymentSandbox::creditMPT (likely, not shown in snippet)", + "DeferredCredits::creditMPT" + ], + "entry_point": "DeferredCredits::creditMPT", + "purpose": "Records a deferred MPT (Multi-Party Trustline) credit between two accounts, updating internal MPT credit tracking.", + "validation_points": [ + "DeferredCredits::creditMPT: XRPL_ASSERT(amount.holds())", + "DeferredCredits::creditMPT: XRPL_ASSERT(!amount.negative())", + "DeferredCredits::creditMPT: XRPL_ASSERT(sender != receiver)" + ] + } + ], + "data_flows": [ + { + "field": "sender", + "flow": [ + "Function argument", + "Validation (XRPL_ASSERT(sender != receiver))", + "Used to construct key for creditsIOU_ or creditsMPT_", + "Used to determine which account is high/low or issuer/holder" + ], + "origin": "Function argument to DeferredCredits::creditIOU/creditMPT", + "transformations": [ + "Compared to receiver for validation", + "Used in tuple for map key" + ], + "validated_at": "Immediately at function entry via XRPL_ASSERT" + }, + { + "field": "receiver", + "flow": [ + "Function argument", + "Validation (XRPL_ASSERT(sender != receiver))", + "Used to construct key for creditsIOU_ or creditsMPT_", + "Used to determine which account is high/low or issuer/holder" + ], + "origin": "Function argument to DeferredCredits::creditIOU/creditMPT", + "transformations": [ + "Compared to sender for validation", + "Used in tuple for map key" + ], + "validated_at": "Immediately at function entry via XRPL_ASSERT" + }, + { + "field": "amount", + "flow": [ + "Function argument", + "Validation (XRPL_ASSERT(!amount.negative()), XRPL_ASSERT(amount.holds()/amount.holds()))", + "amount.get() or amount.get() to extract currency or MPT ID", + "amount.zeroed() for zero value", + "amount.mpt().value() for MPT value" + ], + "origin": "Function argument to DeferredCredits::creditIOU/creditMPT", + "transformations": [ + "Checked for negativity", + "Checked for type (Issue or MPTIssue)", + "Extracted for currency or MPT ID", + "Zeroed for initialization" + ], + "validated_at": "Immediately at function entry via XRPL_ASSERT" + }, + { + "field": "preCreditSenderBalance / preCreditBalanceHolder / preCreditBalanceIssuer", + "flow": [ + "Function argument", + "Stored in ValueIOU.lowAcctOrigBalance or IssuerValueMPT.origBalance", + "Used for later balance calculations" + ], + "origin": "Function argument to DeferredCredits::creditIOU/creditMPT", + "transformations": [ + "Negated if sender > receiver (for IOU)", + "Stored as-is for MPT" + ], + "validated_at": "Not directly validated in shown code" + } + ], + "description": "Implements deferred credit and balance adjustment logic for IOU and MPT assets in the XRPL PaymentSandbox, including hooks for crediting, debiting, owner count adjustment, and balance change tracking.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sender != receiver", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at DeferredCredits::creditIOU", + "issue_pattern": "Missing empty string validation for sender != receiver", + "why_false_positive": "XRPL_ASSERT validates sender != receiver for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount.negative()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at DeferredCredits::creditIOU", + "issue_pattern": "Missing empty string validation for amount.negative()", + "why_false_positive": "XRPL_ASSERT validates amount.negative() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount.holds()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at DeferredCredits::creditIOU", + "issue_pattern": "Missing empty string validation for amount.holds()", + "why_false_positive": "XRPL_ASSERT validates amount.holds() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "amount.holds()", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT at DeferredCredits::creditIOU", + "issue_pattern": "Missing type validation for amount.holds()", + "why_false_positive": "XRPL_ASSERT validates amount.holds() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount.holds()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at DeferredCredits::creditMPT", + "issue_pattern": "Missing empty string validation for amount.holds()", + "why_false_positive": "XRPL_ASSERT validates amount.holds() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "amount.holds()", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT at DeferredCredits::creditMPT", + "issue_pattern": "Missing type validation for amount.holds()", + "why_false_positive": "XRPL_ASSERT validates amount.holds() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount.negative()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at DeferredCredits::creditMPT", + "issue_pattern": "Missing empty string validation for amount.negative()", + "why_false_positive": "XRPL_ASSERT validates amount.negative() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sender != receiver", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at DeferredCredits::creditMPT", + "issue_pattern": "Missing empty string validation for sender != receiver", + "why_false_positive": "XRPL_ASSERT validates sender != receiver for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/PaymentSandbox.cpp", + "functions": [ + { + "args": [ + "AccountID const& a1", + "AccountID const& a2", + "Currency const& c" + ], + "lineno": 9, + "name": "DeferredCredits::makeKeyIOU" + }, + { + "args": [ + "AccountID const& sender", + "AccountID const& receiver", + "STAmount const& amount", + "STAmount const& preCreditSenderBalance" + ], + "lineno": 17, + "name": "DeferredCredits::creditIOU" + }, + { + "args": [ + "AccountID const& sender", + "AccountID const& receiver", + "STAmount const& amount", + "std::uint64_t preCreditBalanceHolder", + "std::int64_t preCreditBalanceIssuer" + ], + "lineno": 48, + "name": "DeferredCredits::creditMPT" + }, + { + "args": [ + "MPTIssue const& issue", + "std::uint64_t amount", + "std::int64_t origBalance" + ], + "lineno": 91, + "name": "DeferredCredits::issuerSelfDebitMPT" + }, + { + "args": [ + "AccountID const& id", + "std::uint32_t cur", + "std::uint32_t next" + ], + "lineno": 108, + "name": "DeferredCredits::ownerCount" + }, + { + "args": [ + "AccountID const& id" + ], + "lineno": 118, + "name": "DeferredCredits::ownerCount" + }, + { + "args": [ + "AccountID const& main", + "AccountID const& other", + "Currency const& currency" + ], + "lineno": 129, + "name": "DeferredCredits::adjustmentsIOU" + }, + { + "args": [ + "xrpl::MPTID const& mptID" + ], + "lineno": 148, + "name": "DeferredCredits::adjustmentsMPT" + }, + { + "args": [ + "DeferredCredits& to" + ], + "lineno": 154, + "name": "DeferredCredits::apply" + }, + { + "args": [ + "AccountID const& account", + "AccountID const& issuer", + "STAmount const& amount" + ], + "lineno": 196, + "name": "PaymentSandbox::balanceHookIOU" + }, + { + "args": [ + "AccountID const& account", + "MPTIssue const& issue", + "std::int64_t amount" + ], + "lineno": 241, + "name": "PaymentSandbox::balanceHookMPT" + }, + { + "args": [ + "xrpl::MPTIssue const& issue", + "std::int64_t amount" + ], + "lineno": 273, + "name": "PaymentSandbox::balanceHookSelfIssueMPT" + }, + { + "args": [ + "AccountID const& account", + "std::uint32_t count" + ], + "lineno": 288, + "name": "PaymentSandbox::ownerCountHook" + }, + { + "args": [ + "AccountID const& from", + "AccountID const& to", + "STAmount const& amount", + "STAmount const& preCreditBalance" + ], + "lineno": 299, + "name": "PaymentSandbox::creditHookIOU" + }, + { + "args": [ + "AccountID const& from", + "AccountID const& to", + "STAmount const& amount", + "std::uint64_t preCreditBalanceHolder", + "std::int64_t preCreditBalanceIssuer" + ], + "lineno": 310, + "name": "PaymentSandbox::creditHookMPT" + }, + { + "args": [ + "MPTIssue const& issue", + "std::uint64_t amount", + "std::int64_t origBalance" + ], + "lineno": 321, + "name": "PaymentSandbox::issuerSelfDebitHookMPT" + }, + { + "args": [ + "AccountID const& account", + "std::uint32_t cur", + "std::uint32_t next" + ], + "lineno": 329, + "name": "PaymentSandbox::adjustOwnerCountHook" + }, + { + "args": [ + "RawView& to" + ], + "lineno": 336, + "name": "PaymentSandbox::apply" + }, + { + "args": [ + "PaymentSandbox& to" + ], + "lineno": 342, + "name": "PaymentSandbox::apply" + }, + { + "args": [ + "ReadView const& view" + ], + "lineno": 348, + "name": "PaymentSandbox::balanceChanges" + }, + { + "args": [], + "lineno": 429, + "name": "PaymentSandbox::xrpDestroyed" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "detail" + } + ], + "test_coverage_notes": "The PaymentSandbox and DeferredCredits logic is typically tested in unit tests under the 'test' or 'unittest' directories, such as PaymentSandbox_test.cpp or Ledger_test.cpp. These tests would cover scenarios like IOU/MPT credits, self-credit attempts, negative amounts, and type mismatches. However, the code relies on XRPL_ASSERT for validation, which may only be active in debug builds or with assertions enabled. There may be gaps in test coverage for assertion failures (e.g., sender == receiver, negative amounts, wrong type), especially if tests do not explicitly check for assertion-triggered aborts or exceptions. Fuzzing or negative tests may be needed to ensure all validation paths are exercised.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "sender != receiver", + "location": "DeferredCredits::creditIOU", + "validated_by": "XRPL_ASSERT", + "validates": [ + "sender and receiver must not be the same account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "amount.negative()", + "location": "DeferredCredits::creditIOU", + "validated_by": "XRPL_ASSERT", + "validates": [ + "amount must not be negative" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "amount.holds()", + "location": "DeferredCredits::creditIOU", + "validated_by": "XRPL_ASSERT", + "validates": [ + "amount must hold Issue type" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "amount.holds()", + "location": "DeferredCredits::creditMPT", + "validated_by": "XRPL_ASSERT", + "validates": [ + "amount must hold MPTIssue type" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "amount.negative()", + "location": "DeferredCredits::creditMPT", + "validated_by": "XRPL_ASSERT", + "validates": [ + "amount must not be negative" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "sender != receiver", + "location": "DeferredCredits::creditMPT", + "validated_by": "XRPL_ASSERT", + "validates": [ + "sender and receiver must not be the same account" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/PaymentSandbox.cpp.ai.md b/src/libxrpl/ledger/PaymentSandbox.cpp.ai.md new file mode 100644 index 0000000000..ea17fbb016 --- /dev/null +++ b/src/libxrpl/ledger/PaymentSandbox.cpp.ai.md @@ -0,0 +1,68 @@ +# PaymentSandbox.cpp + +## Role in the System + +`PaymentSandbox.cpp` implements the deferred credit accounting layer that makes multi-hop payments safe on the XRP Ledger. The core problem it solves is **circular liquidity exploitation**: in a multi-step payment path, if a credit to an account were immediately visible as usable balance, a later step in the same path could draw on that already-committed liquidity, violating the intended accounting invariants. `PaymentSandbox` prevents this by intercepting every credit during payment execution and recording it in a shadow table, then returning a "pre-credit" balance whenever a step queries an account's available funds. + +The file is split between two closely coupled entities: `detail::DeferredCredits`, which owns the credit accounting data structures, and `PaymentSandbox`, which integrates those structures into the ledger view hierarchy by overriding hook methods. + +## DeferredCredits: The Shadow Accounting Engine + +`DeferredCredits` maintains three independent maps: + +- `creditsIOU_` — keyed by `(AccountID, AccountID, Currency)` for IOU trust line credits. +- `creditsMPT_` — keyed by `MPTID` for Multi-Purpose Token credits. +- `ownerCounts_` — keyed by `AccountID`, tracking the peak owner count seen during the payment. + +### IOU Canonical Key Design + +`makeKeyIOU` always places the lexicographically-lower `AccountID` first, so the pair (A→B) and (B→A) share a single map entry. Within that entry, `ValueIOU` distinguishes credits from each direction via `lowAcctCredits` and `highAcctCredits` fields. This mirrors how the ledger itself stores IOU trust lines as bidirectional `RippleState` objects, where one side is arbitrarily the "low" account. + +When `creditIOU` records a new credit between a sender and receiver it never seen before, it also snapshots `lowAcctOrigBalance` — the sender's balance before the credit occurred. Critically, **only the first credit for a given pair captures this snapshot**; subsequent credits for the same pair accumulate on top of `lowAcctCredits`/`highAcctCredits` but leave `lowAcctOrigBalance` untouched. This is the "post-switchover" algorithm. + +### Numerical Stability: Post-Switchover Algorithm + +The comment in `balanceHookIOU` explains the design rationale clearly. A naive implementation would take the post-credit balance `B+C` passed in and subtract the accumulated credit `C` to recover the pre-credit balance: `(B+C) - C`. When the credit `C` is large relative to the original balance `B`, this floating-point subtraction suffers cancellation error. The post-switchover approach stores `B` directly as `lowAcctOrigBalance` and returns it, avoiding the cancellation entirely. + +`balanceHookIOU` walks the linked chain of sandboxes, accumulating `delta` (total debits observed) and `lastBal` (the original balance from the first record encountered). The returned value is `min(amount, lastBal - delta, minBal)`, where `minBal` is the minimum `lastBal` across all sandboxes in the chain. This triple minimum prevents the adjusted balance from exceeding any ancestor's snapshot of the pre-credit state. A special guard clears any computed negative XRP balance to zero — this can arise when a large XRP credit is recorded then debited within the same payment, producing a mathematically negative but not erroneous result. + +### MPT Accounting + +MPT (Multi-Purpose Token) tracking is structurally more complex because MPT relationships are not symmetric: there is a distinct issuer and one or more holders, with `OutstandingAmount` tracking total issued supply. + +`IssuerValueMPT` stores: +- A `holders` sub-map recording per-holder debits and original balances. +- A `credit` field representing total credits issued from the issuer to holders. +- An `origBalance` capturing the issuer's original `OutstandingAmount`. +- A `selfDebit` field for the special case of an issuer selling MPT through their own offer. + +The `selfDebit` case is necessary because the payment engine executes paths in **reverse** (credit first, then debit). If the issuer owns a sell offer, the credit step runs first and can temporarily overflow `OutstandingAmount` beyond `MaximumAmount`. The `issuerSelfDebitHookMPT` / `issuerSelfDebitMPT` pathway records how much the issuer has already self-debited, allowing `balanceHookSelfIssueMPT` to cap the issuer's available issuance to `origBalance - selfDebit`. `balanceHookMPT` applies analogous capping logic for individual holders or the issuer. + +### Owner Count Tracking + +During payment execution, trust lines may be created or destroyed, transiently changing an account's owner count and therefore its XRP reserve requirement. `DeferredCredits::ownerCount(setter)` always records `max(cur, next)` — the peak count between the current and target value. `ownerCountHook` walks the sandbox chain and returns the maximum recorded count, ensuring that reserve checks never undercount the peak obligation incurred mid-payment. + +## PaymentSandbox: Hook Integration + +`PaymentSandbox` inherits from `detail::ApplyViewBase` (itself extending `ApplyView` and `RawView`) and carries two members: a `DeferredCredits tab_` instance and a nullable `PaymentSandbox const* ps_` pointer to a parent sandbox. + +The hook overrides form a thin delegation layer: + +- `creditHookIOU` / `creditHookMPT` / `issuerSelfDebitHookMPT` — called by the payment engine whenever a credit flows; they forward directly into `tab_`. +- `balanceHookIOU` / `balanceHookMPT` / `balanceHookSelfIssueMPT` — called when a step queries available balance; they walk the `ps_` chain collecting adjustments from all ancestor sandboxes. +- `adjustOwnerCountHook` / `ownerCountHook` — owner count analogues of the above. + +The base class `ApplyView` provides no-op default implementations of all hook methods, so non-payment code paths that operate through a plain `ApplyViewBase` are completely unaffected. Only a `PaymentSandbox` activates the deferred credit behavior. + +## Nested Sandbox Chain and apply() + +`PaymentSandbox` supports nesting: constructing one on top of another via the `explicit PaymentSandbox(PaymentSandbox const* base)` constructor sets `ps_` to the parent. Nested sandboxes allow individual path segments to be tentatively applied and rolled back independently. The `ps_` pointer is checked by the two `apply()` overloads: + +- `apply(RawView& to)` — terminal apply to the underlying ledger state. Asserts `ps_ == nullptr` to confirm there is no unresolved parent. +- `apply(PaymentSandbox& to)` — merges into a parent sandbox. Asserts `ps_ == &to`, then calls both `items_.apply(to)` (to propagate ledger state changes) and `tab_.apply(to.tab_)` (to merge the deferred credit tables). + +`DeferredCredits::apply` merges by accumulating credits additively into the target and preserving the original balance snapshots already recorded there — it never overwrites the first-seen `origBalance`, consistent with the post-switchover invariant. + +## balanceChanges() and xrpDestroyed() + +`balanceChanges()` is a diagnostic/accounting utility that computes net balance deltas across all modified ledger objects by delegating to `items_.visit()`. For each `ltACCOUNT_ROOT` and `ltRIPPLE_STATE` object seen, it records `(lowID, highID, currency) → delta` and also populates `(lowID, lowID, currency)` and `(highID, highID, currency)` entries to capture per-issuer totals. This allows callers to reconstruct the full currency flow of a completed payment. `xrpDestroyed()` simply forwards to `items_.dropsDestroyed()` to report XRP burned as fees during the transaction. \ No newline at end of file diff --git a/src/libxrpl/ledger/RawStateTable.cpp.ai.json b/src/libxrpl/ledger/RawStateTable.cpp.ai.json new file mode 100644 index 0000000000..eba435217f --- /dev/null +++ b/src/libxrpl/ledger/RawStateTable.cpp.ai.json @@ -0,0 +1,453 @@ +{ + "args": [ + { + "lineno": 74, + "name": "to" + }, + { + "lineno": 92, + "name": "base" + }, + { + "lineno": 92, + "name": "k" + }, + { + "lineno": 110, + "name": "base" + }, + { + "lineno": 110, + "name": "key" + }, + { + "lineno": 110, + "name": "last" + }, + { + "lineno": 143, + "name": "sle" + }, + { + "lineno": 167, + "name": "sle" + }, + { + "lineno": 191, + "name": "sle" + }, + { + "lineno": 215, + "name": "base" + }, + { + "lineno": 215, + "name": "k" + }, + { + "lineno": 231, + "name": "fee" + }, + { + "lineno": 235, + "name": "base" + }, + { + "lineno": 241, + "name": "base" + }, + { + "lineno": 247, + "name": "base" + }, + { + "lineno": 247, + "name": "key" + } + ], + "classes": [ + { + "args": [ + "sles_iter_impl(const&)", + "sles_iter_impl(items_t::const_iterator, items_t::const_iterator, ReadView::sles_type::iterator, ReadView::sles_type::iterator)" + ], + "lineno": 6, + "name": "RawStateTable::sles_iter_impl" + } + ], + "code_paths": [ + { + "call_chain": [ + "RawStateTable::sles_iter_impl::equal" + ], + "entry_point": "RawStateTable::sles_iter_impl::equal", + "purpose": "Compares two iterator implementations for equality, ensuring they are iterating over the same range.", + "validation_points": [ + "XRPL_ASSERT(end1_ == p->end1_ && end0_ == p->end0_)" + ] + }, + { + "call_chain": [ + "RawStateTable::sles_iter_impl::increment", + "RawStateTable::sles_iter_impl::inc1/inc0/skip" + ], + "entry_point": "RawStateTable::sles_iter_impl::increment", + "purpose": "Advances the iterator to the next valid ledger entry, handling merges and erasures.", + "validation_points": [ + "XRPL_ASSERT(sle1_ || sle0_)" + ] + }, + { + "call_chain": [ + "RawStateTable::apply", + "RawView::rawDestroyXRP", + "RawView::rawErase/rawInsert/rawReplace" + ], + "entry_point": "RawStateTable::apply", + "purpose": "Applies all pending state changes (insert, erase, replace) and XRP destruction to a RawView.", + "validation_points": [ + "Base invariants checked by base during apply() (not shown here, but referenced in comment)" + ] + }, + { + "call_chain": [ + "RawStateTable::exists", + "ReadView::exists" + ], + "entry_point": "RawStateTable::exists", + "purpose": "Checks if a key exists in the local state table or the underlying view.", + "validation_points": [ + "XRPL_ASSERT(k.key.isNonZero())" + ] + } + ], + "data_flows": [ + { + "field": "end1_ / end0_", + "flow": [ + "sles_iter_impl constructor", + "stored in sles_iter_impl", + "used in equal() for validation" + ], + "origin": "Constructor of sles_iter_impl (from function arguments)", + "transformations": [ + "Assigned directly from constructor arguments" + ], + "validated_at": "sles_iter_impl::equal (XRPL_ASSERT)" + }, + { + "field": "sle1_ / sle0_", + "flow": [ + "sles_iter_impl constructor", + "set via iterators", + "used in increment(), dereference(), skip()" + ], + "origin": "sles_iter_impl constructor (from iterators over items_t and ReadView)", + "transformations": [ + "Set to nullptr when iterator reaches end", + "Updated via inc0()/inc1()" + ], + "validated_at": "sles_iter_impl::increment (XRPL_ASSERT)" + }, + { + "field": "items_", + "flow": [ + "RawStateTable", + "used in apply() to drive state changes", + "used in sles_iter_impl for iteration" + ], + "origin": "RawStateTable (populated externally, not shown in this file)", + "transformations": [ + "Iterated, actions dispatched (erase/insert/replace)" + ], + "validated_at": "Indirectly, via logic in apply() and iterator" + }, + { + "field": "dropsDestroyed_", + "flow": [ + "RawStateTable", + "used in apply()", + "passed to RawView::rawDestroyXRP" + ], + "origin": "RawStateTable (populated externally)", + "transformations": [ + "None in this file" + ], + "validated_at": "Not directly validated here" + }, + { + "field": "k.key", + "flow": [ + "RawStateTable::exists", + "XRPL_ASSERT(k.key.isNonZero())", + "lookup in items_", + "fallback to base.exists(k)" + ], + "origin": "Keylet argument to exists()", + "transformations": [ + "None" + ], + "validated_at": "RawStateTable::exists (XRPL_ASSERT)" + } + ], + "description": "Implements the RawStateTable class for managing staged changes to the XRPL ledger state, including insertion, replacement, erasure, and iteration over state ledger entries (SLEs), with logic for merging staged and base views.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "end1_ == p->end1_ && end0_ == p->end0_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at equal()", + "issue_pattern": "Missing empty string validation for end1_ == p->end1_ && end0_ == p->end0_", + "why_false_positive": "XRPL_ASSERT macro validates end1_ == p->end1_ && end0_ == p->end0_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle1_ || sle0_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at increment()", + "issue_pattern": "Missing empty string validation for sle1_ || sle0_", + "why_false_positive": "XRPL_ASSERT macro validates sle1_ || sle0_ for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/RawStateTable.cpp", + "functions": [ + { + "args": [ + "to" + ], + "lineno": 74, + "name": "RawStateTable::apply" + }, + { + "args": [ + "base", + "k" + ], + "lineno": 92, + "name": "RawStateTable::exists" + }, + { + "args": [ + "base", + "key", + "last" + ], + "lineno": 110, + "name": "RawStateTable::succ" + }, + { + "args": [ + "sle" + ], + "lineno": 143, + "name": "RawStateTable::erase" + }, + { + "args": [ + "sle" + ], + "lineno": 167, + "name": "RawStateTable::insert" + }, + { + "args": [ + "sle" + ], + "lineno": 191, + "name": "RawStateTable::replace" + }, + { + "args": [ + "base", + "k" + ], + "lineno": 215, + "name": "RawStateTable::read" + }, + { + "args": [ + "fee" + ], + "lineno": 231, + "name": "RawStateTable::destroyXRP" + }, + { + "args": [ + "base" + ], + "lineno": 235, + "name": "RawStateTable::slesBegin" + }, + { + "args": [ + "base" + ], + "lineno": 241, + "name": "RawStateTable::slesEnd" + }, + { + "args": [ + "base", + "key" + ], + "lineno": 247, + "name": "RawStateTable::slesUpperBound" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "detail" + } + ], + "test_coverage_notes": "The validation code paths (XRPL_ASSERTs) are primarily internal consistency checks and are not directly tested unless assertions are triggered (e.g., via debug builds or assertion-failure tests). Typical unit tests for RawStateTable would be found in files like 'RawStateTable_test.cpp', 'RawView_test.cpp', or broader ledger state tests. However, assertion failures are often not covered by standard tests unless there are explicit negative tests for iterator misuse or state corruption. The main data flows (items_, sle0_/sle1_, end0_/end1_) are exercised by tests that iterate over or apply RawStateTable changes, but coverage for assertion failures (e.g., mismatched iterators, null SLEs) may be incomplete unless specifically targeted. There may be a gap in tests that intentionally trigger these assertions to verify robustness against internal misuse.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro (custom assertion, not a general validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely contract violation, may abort or throw depending on XRPL_ASSERT implementation)", + "field": "end1_ == p->end1_ && end0_ == p->end0_", + "location": "equal()", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures that the compared iterators are from the same range (matching end iterators) before comparing positions" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely contract violation, may abort or throw depending on XRPL_ASSERT implementation)", + "field": "sle1_ || sle0_", + "location": "increment()", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures that at least one of the SLE pointers is non-null before incrementing" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/RawStateTable.cpp.ai.md b/src/libxrpl/ledger/RawStateTable.cpp.ai.md new file mode 100644 index 0000000000..c1f5b96bbe --- /dev/null +++ b/src/libxrpl/ledger/RawStateTable.cpp.ai.md @@ -0,0 +1,48 @@ +# `RawStateTable.cpp` — Staged Ledger Mutation Buffer + +`RawStateTable` lives in `xrpl::detail` and serves as the write-buffer that sits between a transaction's in-flight state changes and the actual ledger storage. Every write-capable ledger view (most visibly `OpenView`) holds a `RawStateTable` internally. Rather than mutating the underlying ledger immediately, all inserts, erases, and field replacements accumulate here. Only when `apply()` is called do those mutations fan out to a real `RawView` target. + +## Memory Layout + +The `items_` map is the central data structure: a `std::map` keyed by the SLE's hash key, storing both the pending `Action` enum (`erase`, `insert`, `replace`) and a `shared_ptr`. Rather than the standard heap allocator, the map uses a Boost PMR `polymorphic_allocator` backed by a `monotonic_buffer_resource` pre-allocated at 256 KB. This was inherited from an older `qalloc` scheme and exists purely for throughput: transaction processing creates and tears down many small map nodes, and bump-pointer allocation from a single arena is far cheaper than per-node `malloc`. The `monotonic_resource_` is stored as a `unique_ptr` so the object is moveable — the header makes `operator=(RawStateTable&&)` deleted but allows move construction, and the copy constructor allocates a fresh 256 KB arena for the destination. + +## Action State Machine in `erase`, `insert`, `replace` + +The three mutation methods encode an important state machine. Each SLE key can only ever be in one pending state, and the transitions enforce correctness before `apply()` is called rather than deferring errors. + +`insert()` on a key that was previously erased in this same transaction batch upgrades the action to `replace` — this handles the "delete then re-create at the same key" pattern. Inserting into a key that already has a pending insert or replace is a `LogicError`, because from the base view's perspective the object already exists. + +`erase()` on a key that was previously inserted (but not yet committed) simply removes the entry from `items_` entirely — the net effect is zero, and there is nothing for `apply()` to propagate. Erasing a `replace`d key downgrades it back to `erase`. Double-erasing is a `LogicError`. + +`replace()` on a pending-insert entry just updates the stored SLE pointer, preserving the `insert` action — from the base view's perspective the key is still being created. Replacing an erased key is a `LogicError`. + +This design catches misuse at the point of the second conflicting operation, not during `apply()`, which makes debugging significantly easier. + +## Merging Reads with the Base View + +`read()`, `exists()`, and `succ()` all take a `ReadView const& base` parameter and overlay the pending delta. The pattern for `read()` and `exists()` is a simple two-step: check `items_` first; if found and `action == erase`, return null/false; otherwise return from the local entry. If not found locally, delegate to `base`. The `Keylet` type-check (`k.check(*sle)`) guards against type mismatches — a key might match but the SLE's type might not conform to the requested keylet, so this additional filter prevents wrong-type reads. + +`succ()` is more involved: it searches both the base view and the local `items_` independently, skips over base results that are locally erased, and returns whichever candidate key is smaller. The loop that advances the base's successor skips each deleted key one at a time, which is safe because deletions in practice are sparse. + +## Merged Iteration via `sles_iter_impl` + +The nested class `sles_iter_impl` implements the virtual `ReadView::sles_type::iter_base` interface, providing a sorted merged view over the base ledger's SLEs and the pending changes. It maintains two parallel iterator pairs: + +- `iter0_` / `sle0_`: current position in the base view's SLE range (already sorted by key). +- `iter1_` / `sle1_`: current position in `items_` (also sorted, since `std::map` iterates in key order). Only non-null when `iter1_->second.sle` is valid. + +`dereference()` returns whichever of `sle0_` and `sle1_` has the smaller key — the local entry always wins on a tie, shadowing the base. `increment()` advances the "winning" iterator. On a key tie (local shadows base), both iterators advance simultaneously so the base entry is consumed. + +The `skip()` helper handles erasures: after `iter1_` points to an `Action::erase` entry, `skip()` loops, advancing both iterators in tandem until the local iterator no longer masks the base entry or one of them is exhausted. This ensures erased SLEs are invisible to callers iterating the merged view. + +The `equal()` implementation uses `dynamic_cast` to ensure both sides are `sles_iter_impl` instances, then asserts that both end-iterators match (a cross-view comparison would be meaningless). The iterator positions are compared as pairs — both `iter0_` and `iter1_` must agree. + +## `apply()` and `destroyXRP()` + +`apply()` is straightforward: it dispatches `rawDestroyXRP()` first to account for accumulated fee burning (`dropsDestroyed_`), then iterates `items_` dispatching each action to the target `RawView`. The comment "Base invariants are checked by the base during apply()" signals that the target `RawView` (e.g., a ledger's state map) is responsible for enforcing preconditions like "key must not already exist for rawInsert" — `RawStateTable` only enforces the *transition* invariants within its own pending buffer. + +`destroyXRP()` simply accumulates drops into `dropsDestroyed_`, which is replayed as a single `rawDestroyXRP` call at apply time rather than one per fee event. + +## Relationship to Sibling Components + +`ApplyStateTable` (in the same `detail` namespace) is the higher-level sibling used during transaction application. It adds a `cache` action (read-only peeked SLEs) and handles metadata threading. `RawStateTable` is the lower-level primitive used by `OpenView` directly and by `ApplyStateTable`'s own apply path when writing to a target view without metadata. Both share the same conceptual pattern of a delta-on-top-of-base, but `RawStateTable` handles the raw bytes layer while `ApplyStateTable` handles the semantic layer. \ No newline at end of file diff --git a/src/libxrpl/ledger/ReadView.cpp.ai.json b/src/libxrpl/ledger/ReadView.cpp.ai.json new file mode 100644 index 0000000000..b0ea17917e --- /dev/null +++ b/src/libxrpl/ledger/ReadView.cpp.ai.json @@ -0,0 +1,221 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "makeRulesGivenLedger(ledger, current)", + "makeRulesGivenLedger(ledger, current.presets())" + ], + "entry_point": "makeRulesGivenLedger(DigestAwareReadView const& ledger, Rules const& current)", + "purpose": "Creates a Rules object based on the current ledger state and preset amendments.", + "validation_points": [ + "makeRulesGivenLedger(DigestAwareReadView const&, std::unordered_set> const&) - checks if digest is present (if (digest)), then checks if sle is present (if (sle))" + ] + }, + { + "call_chain": [ + "makeRulesGivenLedger(ledger, presets)" + ], + "entry_point": "makeRulesGivenLedger(DigestAwareReadView const&, std::unordered_set> const&)", + "purpose": "Creates a Rules object using a set of amendment presets, validating the presence of digest and SLE.", + "validation_points": [ + "if (digest)", + "if (sle)" + ] + } + ], + "data_flows": [ + { + "field": "digest", + "flow": [ + "ledger.digest(k.key)", + "std::optional digest", + "if (digest) validation", + "used as argument to Rules constructor" + ], + "origin": "ledger.digest(k.key)", + "transformations": [ + "digest is wrapped in std::optional", + "validated for presence (if (digest))" + ], + "validated_at": "if (digest) in makeRulesGivenLedger" + }, + { + "field": "sle", + "flow": [ + "ledger.read(k)", + "std::optional sle", + "if (sle) validation", + "sle->getFieldV256(sfAmendments)", + "used as argument to Rules constructor" + ], + "origin": "ledger.read(k)", + "transformations": [ + "sle is wrapped in std::optional", + "validated for presence (if (sle))", + "getFieldV256 extracts amendment field" + ], + "validated_at": "if (sle) in makeRulesGivenLedger" + }, + { + "field": "presets", + "flow": [ + "current.presets() or direct argument", + "passed to makeRulesGivenLedger", + "used as argument to Rules constructor" + ], + "origin": "current.presets() or direct argument", + "transformations": [ + "none (passed through)" + ], + "validated_at": "not explicitly validated" + } + ], + "description": "Implements types and functions for iterating over state ledger entries (SLEs) and transactions in a ReadView, and provides utility functions for creating Rules objects based on ledger state.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "digest (from ledger.digest(k.key))", + "empty", + "string", + "validation" + ], + "evidence": "std::optional check (if (digest)) at makeRulesGivenLedger (DigestAwareReadView const& ledger, std::unordered_set> const& presets)", + "issue_pattern": "Missing empty string validation for digest (from ledger.digest(k.key))", + "why_false_positive": "std::optional check (if (digest)) validates digest (from ledger.digest(k.key)) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle (from ledger.read(k))", + "empty", + "string", + "validation" + ], + "evidence": "std::optional check (if (sle)) at makeRulesGivenLedger (DigestAwareReadView const& ledger, std::unordered_set> const& presets)", + "issue_pattern": "Missing empty string validation for sle (from ledger.read(k))", + "why_false_positive": "std::optional check (if (sle)) validates sle (from ledger.read(k)) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/ReadView.cpp", + "functions": [ + { + "args": [ + "ReadView const& view" + ], + "lineno": 5, + "name": "ReadView::sles_type::sles_type" + }, + { + "args": [], + "lineno": 9, + "name": "ReadView::sles_type::begin" + }, + { + "args": [], + "lineno": 14, + "name": "ReadView::sles_type::end" + }, + { + "args": [ + "key_type const& key" + ], + "lineno": 19, + "name": "ReadView::sles_type::upper_bound" + }, + { + "args": [ + "ReadView const& view" + ], + "lineno": 24, + "name": "ReadView::txs_type::txs_type" + }, + { + "args": [], + "lineno": 28, + "name": "ReadView::txs_type::empty" + }, + { + "args": [], + "lineno": 33, + "name": "ReadView::txs_type::begin" + }, + { + "args": [], + "lineno": 38, + "name": "ReadView::txs_type::end" + }, + { + "args": [ + "DigestAwareReadView const& ledger", + "Rules const& current" + ], + "lineno": 43, + "name": "makeRulesGivenLedger" + }, + { + "args": [ + "DigestAwareReadView const& ledger", + "std::unordered_set> const& presets" + ], + "lineno": 48, + "name": "makeRulesGivenLedger" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file contains utility and adapter code for ledger views and rules construction. The main validation logic (digest and sle presence) is likely tested indirectly via higher-level ledger/rules tests. Look for tests in files such as 'test/rules_test.cpp', 'test/ledger_test.cpp', or integration tests that exercise amendment/rules logic. There is no direct evidence of unit tests for these specific functions in this file. Potential gaps: absence of direct unit tests for negative cases (missing digest or sle), and for edge cases in Rules construction from ledger state.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "std::optional (C++17 standard library), manual checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (branching, not exception)", + "field": "digest (from ledger.digest(k.key))", + "location": "makeRulesGivenLedger (DigestAwareReadView const& ledger, std::unordered_set> const& presets)", + "validated_by": "std::optional check (if (digest))", + "validates": [ + "Checks if digest exists before using it", + "Prevents null/empty digest usage" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (branching, not exception)", + "field": "sle (from ledger.read(k))", + "location": "makeRulesGivenLedger (DigestAwareReadView const& ledger, std::unordered_set> const& presets)", + "validated_by": "std::optional check (if (sle))", + "validates": [ + "Checks if SLE (serialized ledger entry) exists before accessing fields", + "Prevents null/empty SLE usage" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/ReadView.cpp.ai.md b/src/libxrpl/ledger/ReadView.cpp.ai.md new file mode 100644 index 0000000000..862f585c4f --- /dev/null +++ b/src/libxrpl/ledger/ReadView.cpp.ai.md @@ -0,0 +1,48 @@ +# `src/libxrpl/ledger/ReadView.cpp` + +## Role in the System + +This file provides the concrete implementations for two concerns that live at the boundary of the `ReadView` abstraction: the range-protocol adapters that let callers iterate over ledger state entries and transactions using standard C++ range syntax, and the `makeRulesGivenLedger` factory functions that bootstrap a `Rules` object from the live state of the ledger. The file is deliberately thin — the real iteration mechanics and storage are pushed into the virtual interface — making this file purely about wiring. + +## Range Adapters: `sles_type` and `txs_type` + +`ReadView` exposes two public member variables, `sles` and `txs`, that provide range access over serialized ledger entries (SLEs) and transactions respectively. Both are nested `struct` types that inherit from `detail::ReadViewFwdRange`, a template that supplies a type-erased forward iterator wrapping a `std::unique_ptr`. The range itself holds only a raw `ReadView const*` pointer. + +The implementations here do nothing more than forward each range operation to corresponding virtual methods on the owning view: + +- `sles_type::begin()` → `view_->slesBegin()` +- `sles_type::end()` → `view_->slesEnd()` +- `sles_type::upper_bound(key)` → `view_->slesUpperBound(key)` +- `txs_type::begin()` / `end()` → `view_->txsBegin()` / `view_->txsEnd()` +- `txs_type::empty()` → `begin() == end()` (a convenience shortcut) + +This delegation pattern is what makes `ReadView` a true interface: concrete subclasses (`Ledger`, `OpenView`, `CachedView`, etc.) override the `slesBegin` family of methods to return their storage-specific iterators, while all callers interact uniformly through the `sles` and `txs` ranges. + +One subtle copy-safety property follows from how `ReadView` initializes these members. Looking at the header, all three `ReadView` constructors — default, copy, and move — initialize `sles(*this)` and `txs(*this)`. This means the range objects always point to their *containing* `ReadView` instance, not the source of a copy or move. Without this rebinding, copying a `ReadView` subclass would leave the `sles` and `txs` ranges dangling or pointing into the wrong object. + +## `makeRulesGivenLedger`: Bootstrapping Amendment Rules from Ledger State + +`Rules` governs which XRPL amendments (protocol features) are active during transaction processing. It is cheap to pass by value (backed by a `shared_ptr`) but must accurately reflect the set of enabled amendments stored on the ledger. `makeRulesGivenLedger` is the sole authorized path to construct a `Rules` with a live amendment set — the three-argument `Rules` constructor that accepts a digest and `STVector256` of amendment hashes is private, with `makeRulesGivenLedger` declared as a `friend` in `Rules`. This controlled construction pattern prevents callers from accidentally constructing a `Rules` object that diverges from actual ledger state. + +The function requires a `DigestAwareReadView` rather than the base `ReadView`. This is significant: `DigestAwareReadView` adds a `digest(key)` method that returns the hash of the SLE at that key without necessarily deserializing it. The implementation uses this in a deliberate two-step: + +```cpp +std::optional const digest = ledger.digest(k.key); +if (digest) +{ + auto const sle = ledger.read(k); + if (sle) + return Rules(presets, digest, sle->getFieldV256(sfAmendments)); +} +return Rules(presets); +``` + +The outer guard on `digest` is not just defensive null-checking — it carries caching semantics. The `Rules` implementation stores the digest internally so that callers can detect whether rules have changed between ledger closes without re-reading the full SLE. If the digest matches what was seen before, the `Rules` object is still valid. The inner guard on the SLE handles the genesis-ledger case: the amendments object (`ltAMENDMENTS`, addressed via `keylet::amendments()`) simply doesn't exist on the very first ledger, so a plain `Rules(presets)` — with no active amendments — is returned as the correct baseline. + +The two-overload design is a convenience split. The first overload accepts a `Rules const& current` (used by `Ledger.cpp` when refreshing its own rules on each new ledger build) and simply delegates to the second after extracting `current.presets()`. The second overload takes an `unordered_set` of preset amendments directly — used by `NetworkOPs` and `RCLConsensus` at consensus time, passing `app_.config().features` as the forced-enabled set that operators configure locally. + +## Failure Modes and Defensive Patterns + +Neither `makeRulesGivenLedger` overload throws. Both double-check the `std::optional` results before use. If the amendments ledger object is absent (no entry at the amendments keylet, or a ledger with no amendments history), the function silently returns a `Rules` constructed only from presets — an intentional graceful degradation rather than an error, because pre-amendment ledgers are a valid and common case during chain replay. + +There are no raw pointers owned by this file. The `sles_type` and `txs_type` range types store a `ReadView const*` borrowed from the enclosing object, and the lifetime of that pointer is guaranteed by the `ReadView` member layout — the ranges are destroyed before the view that owns them. \ No newline at end of file diff --git a/src/libxrpl/ledger/View.cpp.ai.json b/src/libxrpl/ledger/View.cpp.ai.json new file mode 100644 index 0000000000..626cfc4f0e --- /dev/null +++ b/src/libxrpl/ledger/View.cpp.ai.json @@ -0,0 +1,606 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "hasExpired" + ], + "entry_point": "hasExpired", + "purpose": "Checks if an optional expiration time has passed relative to the ledger's parent close time.", + "validation_points": [ + "hasExpired (validates 'exp' against view.parentCloseTime())" + ] + }, + { + "call_chain": [ + "isVaultPseudoAccountFrozen", + "view.rules().enabled(featureSingleAssetVault)", + "view.read(keylet::mptIssuance)", + "mptIssuance->getAccountID(sfIssuer)", + "view.read(keylet::account)", + "mptIssuer->isFieldPresent(sfVaultID)", + "view.read(keylet::vault)", + "isAnyFrozen" + ], + "entry_point": "isVaultPseudoAccountFrozen", + "purpose": "Determines if a vault pseudo-account is frozen, traversing related ledger objects and checking feature flags and recursion depth.", + "validation_points": [ + "view.rules().enabled(featureSingleAssetVault) (feature flag validation)", + "depth >= maxAssetCheckDepth (recursion depth validation)", + "view.read(keylet::mptIssuance) (existence validation)", + "view.read(keylet::account) (existence validation)", + "mptIssuer->isFieldPresent(sfVaultID) (vault pseudo-account check)", + "view.read(keylet::vault) (existence validation)" + ] + }, + { + "call_chain": [ + "isLPTokenFrozen", + "isFrozen (asset1)", + "isFrozen (asset2)" + ], + "entry_point": "isLPTokenFrozen", + "purpose": "Checks if either of two assets is frozen for a given account.", + "validation_points": [ + "isFrozen (validates asset freeze status)" + ] + }, + { + "call_chain": [ + "areCompatible", + "hashOfSeq" + ], + "entry_point": "areCompatible", + "purpose": "Checks if two ledgers are compatible by comparing their hashes at specific sequence numbers.", + "validation_points": [ + "hashOfSeq (validates hash consistency)" + ] + } + ], + "data_flows": [ + { + "field": "exp (expiration time)", + "flow": [ + "exp passed to hasExpired", + "converted to NetClock::time_point", + "compared to view.parentCloseTime()" + ], + "origin": "Function argument to hasExpired (std::optional)", + "transformations": [ + "exp is dereferenced and cast to NetClock::duration/time_point" + ], + "validated_at": "hasExpired" + }, + { + "field": "featureSingleAssetVault (feature flag)", + "flow": [ + "isVaultPseudoAccountFrozen", + "view.rules().enabled(featureSingleAssetVault)" + ], + "origin": "Protocol feature flag, checked via view.rules().enabled", + "transformations": [ + "Boolean check" + ], + "validated_at": "isVaultPseudoAccountFrozen" + }, + { + "field": "depth (recursion depth)", + "flow": [ + "isVaultPseudoAccountFrozen", + "depth >= maxAssetCheckDepth" + ], + "origin": "Function argument to isVaultPseudoAccountFrozen", + "transformations": [ + "Integer comparison" + ], + "validated_at": "isVaultPseudoAccountFrozen" + }, + { + "field": "mptIssuance (ledger object existence)", + "flow": [ + "isVaultPseudoAccountFrozen", + "view.read(keylet::mptIssuance)", + "nullptr check" + ], + "origin": "view.read(keylet::mptIssuance(mptShare.getMptID()))", + "transformations": [ + "Pointer dereference" + ], + "validated_at": "isVaultPseudoAccountFrozen" + }, + { + "field": "mptIssuer (ledger object existence)", + "flow": [ + "isVaultPseudoAccountFrozen", + "view.read(keylet::account(issuer))", + "nullptr check" + ], + "origin": "mptIssuance->getAccountID(sfIssuer)", + "transformations": [ + "Pointer dereference" + ], + "validated_at": "isVaultPseudoAccountFrozen" + } + ], + "description": "This file provides utility functions and logic for interacting with and modifying the XRPL ledger, including checks for account and asset states, ledger compatibility, amendment management, withdrawal permissions, and account cleanup operations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "exp (expiration time)", + "empty", + "string", + "validation" + ], + "evidence": "hasExpired (manual check) at hasExpired", + "issue_pattern": "Missing empty string validation for exp (expiration time)", + "why_false_positive": "hasExpired (manual check) validates exp (expiration time) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "featureSingleAssetVault (feature flag)", + "empty", + "string", + "validation" + ], + "evidence": "view.rules().enabled at isVaultPseudoAccountFrozen", + "issue_pattern": "Missing empty string validation for featureSingleAssetVault (feature flag)", + "why_false_positive": "view.rules().enabled validates featureSingleAssetVault (feature flag) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "depth (recursion depth)", + "empty", + "string", + "validation" + ], + "evidence": "manual check at isVaultPseudoAccountFrozen", + "issue_pattern": "Missing empty string validation for depth (recursion depth)", + "why_false_positive": "manual check validates depth (recursion depth) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "depth (recursion depth)", + "range", + "bounds", + "validation" + ], + "evidence": "manual check at isVaultPseudoAccountFrozen", + "issue_pattern": "Missing range validation for depth (recursion depth)", + "why_false_positive": "manual check validates depth (recursion depth) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mptIssuance (ledger object existence)", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::mptIssuance) at isVaultPseudoAccountFrozen", + "issue_pattern": "Missing empty string validation for mptIssuance (ledger object existence)", + "why_false_positive": "view.read(keylet::mptIssuance) validates mptIssuance (ledger object existence) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mptIssuer (ledger object existence)", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::account) at isVaultPseudoAccountFrozen", + "issue_pattern": "Missing empty string validation for mptIssuer (ledger object existence)", + "why_false_positive": "view.read(keylet::account) validates mptIssuer (ledger object existence) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID (field presence)", + "empty", + "string", + "validation" + ], + "evidence": "mptIssuer->isFieldPresent at isVaultPseudoAccountFrozen", + "issue_pattern": "Missing empty string validation for sfVaultID (field presence)", + "why_false_positive": "mptIssuer->isFieldPresent validates sfVaultID (field presence) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfVaultID (field presence)", + "format", + "validation", + "invalid" + ], + "evidence": "mptIssuer->isFieldPresent at isVaultPseudoAccountFrozen", + "issue_pattern": "Missing format validation for sfVaultID (field presence)", + "why_false_positive": "mptIssuer->isFieldPresent validates sfVaultID (field presence) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vault (ledger object existence)", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::vault) at isVaultPseudoAccountFrozen", + "issue_pattern": "Missing empty string validation for vault (ledger object existence)", + "why_false_positive": "view.read(keylet::vault) validates vault (ledger object existence) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account, asset, asset2 (frozen status)", + "empty", + "string", + "validation" + ], + "evidence": "isFrozen at isLPTokenFrozen", + "issue_pattern": "Missing empty string validation for account, asset, asset2 (frozen status)", + "why_false_positive": "isFrozen validates account, asset, asset2 (frozen status) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/View.cpp", + "functions": [ + { + "args": [ + "view", + "exp" + ], + "lineno": 17, + "name": "hasExpired" + }, + { + "args": [ + "view", + "account", + "mptShare", + "depth" + ], + "lineno": 23, + "name": "isVaultPseudoAccountFrozen" + }, + { + "args": [ + "view", + "account", + "asset", + "asset2" + ], + "lineno": 49, + "name": "isLPTokenFrozen" + }, + { + "args": [ + "validLedger", + "testLedger", + "s", + "reason" + ], + "lineno": 54, + "name": "areCompatible" + }, + { + "args": [ + "validHash", + "validIndex", + "testLedger", + "s", + "reason" + ], + "lineno": 91, + "name": "areCompatible" + }, + { + "args": [ + "view" + ], + "lineno": 124, + "name": "getEnabledAmendments" + }, + { + "args": [ + "view" + ], + "lineno": 137, + "name": "getMajorityAmendments" + }, + { + "args": [ + "ledger", + "seq", + "journal" + ], + "lineno": 157, + "name": "hashOfSeq" + }, + { + "args": [ + "view", + "owner", + "object", + "node" + ], + "lineno": 202, + "name": "dirLink" + }, + { + "args": [ + "view", + "from", + "to", + "amount" + ], + "lineno": 221, + "name": "withdrawToDestExceedsLimit" + }, + { + "args": [ + "view", + "from", + "to", + "toSle", + "amount", + "hasDestinationTag" + ], + "lineno": 247, + "name": "canWithdraw" + }, + { + "args": [ + "view", + "from", + "to", + "amount", + "hasDestinationTag" + ], + "lineno": 263, + "name": "canWithdraw" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 271, + "name": "canWithdraw" + }, + { + "args": [ + "view", + "tx", + "senderAcct", + "dstAcct", + "sourceAcct", + "priorBalance", + "amount", + "j" + ], + "lineno": 278, + "name": "doWithdraw" + }, + { + "args": [ + "view", + "ownerDirKeylet", + "deleter", + "j", + "maxNodesToDelete" + ], + "lineno": 312, + "name": "cleanupOnAccountDelete" + }, + { + "args": [ + "now", + "mark" + ], + "lineno": 374, + "name": "after" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + } + ], + "test_coverage_notes": "The functions in View.cpp are typically tested indirectly via higher-level transaction and ledger tests. Direct unit tests for these validation paths may exist in test files such as 'test/ledger/View_test.cpp', 'test/ledger/ReadView_test.cpp', or integration tests involving vaults, assets, and feature flags. However, some error branches (e.g., UNREACHABLE code, LCOV_EXCL_START/STOP) are explicitly marked as not covered by tests. Edge cases like null pointers for mptIssuance, mptIssuer, or vault are likely not tested. Feature flag and expiration logic are likely covered by protocol/feature and transaction tests, but recursion depth and multi-level vault freezing may lack exhaustive coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Manual validation, some use of UNREACHABLE for critical errors", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "exp (expiration time)", + "location": "hasExpired", + "validated_by": "hasExpired (manual check)", + "validates": [ + "Checks if exp is set (std::optional)", + "Compares parentCloseTime() >= exp" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "featureSingleAssetVault (feature flag)", + "location": "isVaultPseudoAccountFrozen", + "validated_by": "view.rules().enabled", + "validates": [ + "Checks if featureSingleAssetVault is enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns true)", + "field": "depth (recursion depth)", + "location": "isVaultPseudoAccountFrozen", + "validated_by": "manual check", + "validates": [ + "Checks if depth >= maxAssetCheckDepth" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "mptIssuance (ledger object existence)", + "location": "isVaultPseudoAccountFrozen", + "validated_by": "view.read(keylet::mptIssuance)", + "validates": [ + "Checks if mptIssuance exists (not nullptr)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE (assertion/logging, not exception)", + "field": "mptIssuer (ledger object existence)", + "location": "isVaultPseudoAccountFrozen", + "validated_by": "view.read(keylet::account)", + "validates": [ + "Checks if mptIssuer exists (not nullptr)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "sfVaultID (field presence)", + "location": "isVaultPseudoAccountFrozen", + "validated_by": "mptIssuer->isFieldPresent", + "validates": [ + "Checks if sfVaultID field is present in mptIssuer" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE (assertion/logging, not exception)", + "field": "vault (ledger object existence)", + "location": "isVaultPseudoAccountFrozen", + "validated_by": "view.read(keylet::vault)", + "validates": [ + "Checks if vault exists (not nullptr)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "account, asset, asset2 (frozen status)", + "location": "isLPTokenFrozen", + "validated_by": "isFrozen", + "validates": [ + "Checks if either asset is frozen for the account" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/View.cpp.ai.md b/src/libxrpl/ledger/View.cpp.ai.md new file mode 100644 index 0000000000..0cf32c23c4 --- /dev/null +++ b/src/libxrpl/ledger/View.cpp.ai.md @@ -0,0 +1,72 @@ +# `src/libxrpl/ledger/View.cpp` + +## Role in the System + +`View.cpp` implements the free-function utility layer that sits between the raw `ReadView`/`ApplyView` interfaces and higher-level transaction processing code. Where `ReadView` and `ApplyView` define *how* to access or write ledger state, this file defines *what* to do with that access — encoding business-logic queries and mutations that are common across many transaction types: expiration checks, asset freeze detection, ledger chain validation, amendment introspection, directory management, withdrawal validation, and account cleanup. + +The file is organized into two clear sections: read-only **observers** that take `ReadView const&`, and state-mutating **modifiers** that require `ApplyView&`. + +--- + +## Observers + +### `hasExpired()` + +Converts an `std::optional` XRPL-epoch timestamp to a `NetClock::time_point` and compares it against the view's `parentCloseTime()`. The design choice to use *parent* close time rather than the current ledger's time is deliberate: the parent ledger's close time is consensus-finalized and deterministic across all validators, whereas the in-flight ledger's close time is not yet agreed upon. This makes expiry evaluation reproducible across nodes. + +### `hashOfSeq()` + +Navigates the ledger's multi-level skip list to retrieve the hash of a historical ledger by sequence number. The skip list has two tiers: + +1. The most recent 256 predecessor hashes are stored in the current ledger's `keylet::skip()` node (the normal list). This allows direct O(1) lookup for any ledger within 256 back. +2. For ledgers older than 256, the code requires the sequence to be 256-aligned (`(seq & 0xff) == 0`). Those ledgers each carry a `keylet::skip(seq)` node holding hashes of every 256th ancestor up to their own position, making them permanent historical anchors. + +Non-aligned sequences older than 256 steps simply cannot be resolved — the function returns `std::nullopt` and logs at debug level. This is an intentional design constraint: the skip list is not an arbitrary-access historical index, but a space-efficient structure for the most common lookup patterns (recent ancestors and aligned milestones). + +### `areCompatible()` + +Two overloads check whether a test ledger is on the same chain as a known-valid ledger. The first form accepts both as `ReadView` references; the second accepts only the valid ledger's hash and index (for callers who have not yet fetched the valid ledger object). Both forms use `hashOfSeq()` to reconstruct the expected hash at the overlapping sequence number and compare. If they differ at the same sequence, the ledgers are on incompatible forks. This is used by consensus validation machinery to detect and log chain splits. + +### `isVaultPseudoAccountFrozen()` + +Determines whether a vault pseudo-account's MPT share token is indirectly frozen because the vault's underlying asset is frozen. The function traverses upward through the ledger graph: MPT issuance → issuer account root → vault object → vault asset, then delegates to `isAnyFrozen()`. A `depth` parameter guards against infinite recursion in pathological configurations. The `UNREACHABLE` macros at the null-check points for the issuer account and vault object reflect ledger invariants that should never be violated in practice — those paths are excluded from coverage intentionally. + +The feature-flag guard (`featureSingleAssetVault`) means the function unconditionally returns `false` before the amendment is enabled, providing clean backward compatibility. + +### `isLPTokenFrozen()` + +A thin wrapper that applies `isFrozen()` to both legs of an AMM pool. LP tokens are frozen if *either* of their constituent assets is frozen for the given account. + +### Amendment Queries + +`getEnabledAmendments()` and `getMajorityAmendments()` both read from the singleton `keylet::amendments()` SLE. The former returns a `std::set` of all enabled amendment hashes; the latter returns a `std::map` of amendments that have reached validator supermajority but have not yet been activated. These are thin ledger-state accessors that make the amendment state observable without requiring callers to understand SLE field layouts. + +--- + +## Modifiers + +### `dirLink()` + +Inserts an SLE into an account's owner directory and stores the returned page number back into the SLE's designated field (`sfOwnerNode` by default). Returns `tecDIR_FULL` if no page is available — a hard capacity limit enforced at the directory layer. + +### `canWithdraw()` and `withdrawToDestExceedsLimit()` + +Three overloads of `canWithdraw()` form a validation cascade. The most granular form takes a pre-read SLE for the destination account (avoiding a redundant `view.read()` when the caller already has it), checks for destination tag requirements via `checkDestinationAndTag()`, verifies deposit authorization via `lsfDepositAuth` + `keylet::depositPreauth`, and delegates asset-limit checking to the internal `withdrawToDestExceedsLimit()`. + +The IOU/MPT asymmetry in `withdrawToDestExceedsLimit()` is architecturally important and documented in a block comment: IOU withdrawals check that the recipient's trust line limit won't be exceeded. MPT withdrawals **skip this check entirely** because vault withdrawals transfer existing tokens rather than minting new ones — the `MaximumAmount` supply cap is a minting constraint, not a transfer constraint. Returning `tesSUCCESS` unconditionally for MPTs in `std::visit` makes this policy explicit and enforced at compile time via the exhaustive visitor. + +### `doWithdraw()` + +Executes the physical asset transfer from a source pseudo-account to a destination. When withdrawing to self, it calls `addEmptyHolding()` to create a trust line or MPToken record if needed (tolerating `tecDUPLICATE` if one already exists). For withdrawals to a third party, it delegates to `verifyDepositPreauth()`. Before calling `accountSend()`, it asserts via `accountHolds()` that the source actually has sufficient balance — a sanity check that, if triggered, surfaces `tefINTERNAL` rather than attempting an overdraft. + +### `cleanupOnAccountDelete()` + +Iterates an account's entire owner directory and deletes every entry using the caller-provided `EntryDeleter` callback. The design uses a `std::function` returning `std::pair` to decouple the generic traversal loop from type-specific deletion logic — the regular account delete path and AMM account delete path share this loop but inject different deleters. + +A subtle and well-commented iterator invalidation issue is handled explicitly. The `dirFirst()`/`dirNext()` functions use *exposed internal state* (a `uint256& page` and `unsigned int& uDirEntry`) rather than opaque iterators. After deleting the entry at index `it`, the entry that was at `it+1` shifts to `it`. The code compensates by decrementing `uDirEntry` so the next `dirNext()` call picks up the shifted position. The `XRPL_ASSERT` guarding `uDirEntry >= 1` and the `SkipEntry` flag (which bypasses the decrement when an entry was intentionally left in place) make the invariant explicit. + +The `maxNodesToDelete` parameter supports partial deletion — returning `tecINCOMPLETE` when the budget is exhausted — allowing expensive account cleanups to be spread across multiple transactions. + +### `after()` + +A one-liner that converts a `NetClock::time_point` to a raw tick count and compares it against a `uint32_t` mark. Factored out to avoid raw `time_since_epoch().count()` arithmetic scattered across callers. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/AMMHelpers.cpp.ai.json new file mode 100644 index 0000000000..899de6c609 --- /dev/null +++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp.ai.json @@ -0,0 +1,1058 @@ +{ + "args": [ + { + "lineno": 7, + "name": "asset1" + }, + { + "lineno": 7, + "name": "asset2" + }, + { + "lineno": 7, + "name": "lptIssue" + }, + { + "lineno": 22, + "name": "asset1Balance" + }, + { + "lineno": 22, + "name": "asset1Deposit" + }, + { + "lineno": 22, + "name": "lptAMMBalance" + }, + { + "lineno": 22, + "name": "tfee" + }, + { + "lineno": 54, + "name": "lpTokens" + }, + { + "lineno": 97, + "name": "asset1Withdraw" + }, + { + "lineno": 134, + "name": "assetBalance" + }, + { + "lineno": 170, + "name": "n" + }, + { + "lineno": 175, + "name": "lptAMMBalance" + }, + { + "lineno": 175, + "name": "isDeposit" + }, + { + "lineno": 186, + "name": "amountBalance" + }, + { + "lineno": 186, + "name": "amount" + }, + { + "lineno": 186, + "name": "amount2" + }, + { + "lineno": 246, + "name": "a" + }, + { + "lineno": 246, + "name": "b" + }, + { + "lineno": 246, + "name": "c" + }, + { + "lineno": 265, + "name": "frac" + }, + { + "lineno": 265, + "name": "rm" + }, + { + "lineno": 271, + "name": "rules" + }, + { + "lineno": 271, + "name": "noRoundCb" + }, + { + "lineno": 271, + "name": "balance" + }, + { + "lineno": 271, + "name": "productCb" + }, + { + "lineno": 284, + "name": "tokens" + }, + { + "lineno": 360, + "name": "view" + }, + { + "lineno": 360, + "name": "ammAccountID" + }, + { + "lineno": 360, + "name": "freezeHandling" + }, + { + "lineno": 360, + "name": "authHandling" + }, + { + "lineno": 360, + "name": "j" + }, + { + "lineno": 371, + "name": "ammSle" + }, + { + "lineno": 371, + "name": "optAsset1" + }, + { + "lineno": 371, + "name": "optAsset2" + }, + { + "lineno": 414, + "name": "lpAccount" + }, + { + "lineno": 414, + "name": "ammAccount" + }, + { + "lineno": 450, + "name": "account" + }, + { + "lineno": 474, + "name": "asset" + }, + { + "lineno": 495, + "name": "sb" + }, + { + "lineno": 495, + "name": "maxTrustlinesToDelete" + }, + { + "lineno": 555, + "name": "asset2" + }, + { + "lineno": 629, + "name": "ammIssue" + }, + { + "lineno": 701, + "name": "lpTokens" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ammLPTokens" + ], + "entry_point": "ammLPTokens", + "purpose": "Calculates the number of LP tokens to mint based on two asset balances and the LP token issue.", + "validation_points": [ + "asset1, asset2: validated by implicit type checking (STAmount)", + "lptIssue: validated by implicit type checking (Asset)" + ] + }, + { + "call_chain": [ + "lpTokensOut" + ], + "entry_point": "lpTokensOut", + "purpose": "Calculates the amount of LP tokens to mint when a deposit is made.", + "validation_points": [ + "asset1Balance, asset1Deposit, lptAMMBalance: validated by implicit type checking (STAmount)", + "tfee: validated by implicit type checking (std::uint16_t)", + "asset1Deposit / asset1Balance: division operator (runtime, may throw if denominator is zero)" + ] + }, + { + "call_chain": [ + "ammAssetIn", + "solveQuadraticEq" + ], + "entry_point": "ammAssetIn", + "purpose": "Calculates the amount of asset required for a given amount of LP tokens.", + "validation_points": [ + "asset1Balance, lptAMMBalance, lpTokens: validated by implicit type checking (STAmount)", + "tfee: validated by implicit type checking (std::uint16_t)", + "lpTokens / lptAMMBalance: division operator (runtime, may throw if denominator is zero)" + ] + }, + { + "call_chain": [ + "lpTokensIn" + ], + "entry_point": "lpTokensIn", + "purpose": "Calculates the amount of LP tokens to burn when a withdrawal is made.", + "validation_points": [ + "asset1Balance, asset1Withdraw, lptAMMBalance: validated by implicit type checking (STAmount)", + "tfee: validated by implicit type checking (std::uint16_t)", + "asset1Withdraw / asset1Balance: division operator (runtime, may throw if denominator is zero)" + ] + }, + { + "call_chain": [ + "ammAssetOut" + ], + "entry_point": "ammAssetOut", + "purpose": "Calculates the amount of asset to withdraw for a given amount of LP tokens burned.", + "validation_points": [ + "assetBalance, lptAMMBalance, lpTokens: validated by implicit type checking (STAmount)", + "tfee: validated by implicit type checking (std::uint16_t)", + "lpTokens / lptAMMBalance: division operator (runtime, may throw if denominator is zero)" + ] + } + ], + "data_flows": [ + { + "field": "asset1", + "flow": [ + "Function argument", + "root2(asset1 * asset2)", + "toSTAmount(lptIssue, tokens)", + "return" + ], + "origin": "Function argument to ammLPTokens", + "transformations": [ + "Multiplied with asset2", + "Square root taken", + "Converted to STAmount" + ], + "validated_at": "Function entry (implicit type checking)" + }, + { + "field": "asset1Deposit", + "flow": [ + "Function argument", + "asset1Deposit / asset1Balance", + "used in further calculations", + "toSTAmount or multiply", + "return" + ], + "origin": "Function argument to lpTokensOut", + "transformations": [ + "Divided by asset1Balance", + "Used in quadratic formula", + "Converted to STAmount" + ], + "validated_at": "Function entry (implicit type checking), division operator (runtime)" + }, + { + "field": "lptAMMBalance", + "flow": [ + "Function argument", + "used in division and multiplication", + "toSTAmount or multiply", + "return" + ], + "origin": "Function argument to lpTokensOut, ammAssetIn, lpTokensIn, ammAssetOut", + "transformations": [ + "Divided by or multiplied with other values", + "Converted to STAmount" + ], + "validated_at": "Function entry (implicit type checking), division operator (runtime)" + }, + { + "field": "tfee", + "flow": [ + "Function argument", + "feeMult, feeMultHalf, getFee", + "used in calculations", + "affects output" + ], + "origin": "Function argument to all main functions", + "transformations": [ + "Converted to fee multipliers", + "Used in arithmetic" + ], + "validated_at": "Function entry (implicit type checking)" + }, + { + "field": "lpTokens", + "flow": [ + "Function argument", + "lpTokens / lptAMMBalance", + "used in quadratic formula", + "toSTAmount or multiply", + "return" + ], + "origin": "Function argument to ammAssetIn, ammAssetOut", + "transformations": [ + "Divided by lptAMMBalance", + "Used in quadratic equation", + "Converted to STAmount" + ], + "validated_at": "Function entry (implicit type checking), division operator (runtime)" + } + ], + "description": "This file provides helper functions for Automated Market Maker (AMM) operations in the XRPL ledger, including calculations for LP tokens, asset deposits/withdrawals, rounding, AMM pool/account holds, trustline/MPT cleanup, fee auction initialization, and liquidity provider verification.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "STAmount (type, arithmetic validity)", + "validation", + "missing", + "check" + ], + "evidence": "Field STAmount (type, arithmetic validity) validated by None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers", + "issue_pattern": "Missing validation for STAmount (type, arithmetic validity)", + "why_false_positive": "None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers validates STAmount (type, arithmetic validity) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Asset (type)", + "validation", + "missing", + "check" + ], + "evidence": "Field Asset (type) validated by None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers", + "issue_pattern": "Missing validation for Asset (type)", + "why_false_positive": "None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers validates Asset (type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "std::uint16_t (type)", + "validation", + "missing", + "check" + ], + "evidence": "Field std::uint16_t (type) validated by None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers", + "issue_pattern": "Missing validation for std::uint16_t (type)", + "why_false_positive": "None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers validates std::uint16_t (type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "root2 argument (likely non-negative)", + "validation", + "missing", + "check" + ], + "evidence": "Field root2 argument (likely non-negative) validated by None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers", + "issue_pattern": "Missing validation for root2 argument (likely non-negative)", + "why_false_positive": "None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers validates root2 argument (likely non-negative) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "division denominators (likely non-zero)", + "validation", + "missing", + "check" + ], + "evidence": "Field division denominators (likely non-zero) validated by None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers", + "issue_pattern": "Missing validation for division denominators (likely non-zero)", + "why_false_positive": "None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers validates division denominators (likely non-zero) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "asset1, asset2", + "empty", + "string", + "validation" + ], + "evidence": "implicit type checking (STAmount) at ammLPTokens", + "issue_pattern": "Missing empty string validation for asset1, asset2", + "why_false_positive": "implicit type checking (STAmount) validates asset1, asset2 for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "asset1, asset2", + "type", + "validation", + "check" + ], + "evidence": "implicit type checking (STAmount) at ammLPTokens", + "issue_pattern": "Missing type validation for asset1, asset2", + "why_false_positive": "implicit type checking (STAmount) validates asset1, asset2 type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "lptIssue", + "empty", + "string", + "validation" + ], + "evidence": "implicit type checking (Asset) at ammLPTokens", + "issue_pattern": "Missing empty string validation for lptIssue", + "why_false_positive": "implicit type checking (Asset) validates lptIssue for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "lptIssue", + "type", + "validation", + "check" + ], + "evidence": "implicit type checking (Asset) at ammLPTokens", + "issue_pattern": "Missing type validation for lptIssue", + "why_false_positive": "implicit type checking (Asset) validates lptIssue type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "asset1Balance, asset1Deposit, lptAMMBalance", + "empty", + "string", + "validation" + ], + "evidence": "implicit type checking (STAmount) at lpTokensOut", + "issue_pattern": "Missing empty string validation for asset1Balance, asset1Deposit, lptAMMBalance", + "why_false_positive": "implicit type checking (STAmount) validates asset1Balance, asset1Deposit, lptAMMBalance for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "asset1Balance, asset1Deposit, lptAMMBalance", + "type", + "validation", + "check" + ], + "evidence": "implicit type checking (STAmount) at lpTokensOut", + "issue_pattern": "Missing type validation for asset1Balance, asset1Deposit, lptAMMBalance", + "why_false_positive": "implicit type checking (STAmount) validates asset1Balance, asset1Deposit, lptAMMBalance type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "tfee", + "empty", + "string", + "validation" + ], + "evidence": "implicit type checking (std::uint16_t) at lpTokensOut, ammAssetIn, lpTokensIn", + "issue_pattern": "Missing empty string validation for tfee", + "why_false_positive": "implicit type checking (std::uint16_t) validates tfee for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "tfee", + "type", + "validation", + "check" + ], + "evidence": "implicit type checking (std::uint16_t) at lpTokensOut, ammAssetIn, lpTokensIn", + "issue_pattern": "Missing type validation for tfee", + "why_false_positive": "implicit type checking (std::uint16_t) validates tfee type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "asset1Deposit / asset1Balance, lpTokens / lptAMMBalance, asset1Withdraw / asset1Balance", + "empty", + "string", + "validation" + ], + "evidence": "division operator (runtime) at lpTokensOut, ammAssetIn, lpTokensIn", + "issue_pattern": "Missing empty string validation for asset1Deposit / asset1Balance, lpTokens / lptAMMBalance, asset1Withdraw / asset1Balance", + "why_false_positive": "division operator (runtime) validates asset1Deposit / asset1Balance, lpTokens / lptAMMBalance, asset1Withdraw / asset1Balance for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "root2 argument", + "empty", + "string", + "validation" + ], + "evidence": "root2 function (likely asserts non-negative input) at ammLPTokens, lpTokensOut, lpTokensIn", + "issue_pattern": "Missing empty string validation for root2 argument", + "why_false_positive": "root2 function (likely asserts non-negative input) validates root2 argument for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "solveQuadraticEq arguments", + "empty", + "string", + "validation" + ], + "evidence": "solveQuadraticEq function (likely checks for valid quadratic roots) at ammAssetIn", + "issue_pattern": "Missing empty string validation for solveQuadraticEq arguments", + "why_false_positive": "solveQuadraticEq function (likely checks for valid quadratic roots) validates solveQuadraticEq arguments for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "isFeatureEnabled(fixAMMv1_3)", + "empty", + "string", + "validation" + ], + "evidence": "feature flag check at all functions", + "issue_pattern": "Missing empty string validation for isFeatureEnabled(fixAMMv1_3)", + "why_false_positive": "feature flag check validates isFeatureEnabled(fixAMMv1_3) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/AMMHelpers.cpp", + "functions": [ + { + "args": [ + "asset1", + "asset2", + "lptIssue" + ], + "lineno": 7, + "name": "ammLPTokens" + }, + { + "args": [ + "asset1Balance", + "asset1Deposit", + "lptAMMBalance", + "tfee" + ], + "lineno": 22, + "name": "lpTokensOut" + }, + { + "args": [ + "asset1Balance", + "lptAMMBalance", + "lpTokens", + "tfee" + ], + "lineno": 54, + "name": "ammAssetIn" + }, + { + "args": [ + "asset1Balance", + "asset1Withdraw", + "lptAMMBalance", + "tfee" + ], + "lineno": 97, + "name": "lpTokensIn" + }, + { + "args": [ + "assetBalance", + "lptAMMBalance", + "lpTokens", + "tfee" + ], + "lineno": 134, + "name": "ammAssetOut" + }, + { + "args": [ + "n" + ], + "lineno": 170, + "name": "square" + }, + { + "args": [ + "lptAMMBalance", + "lpTokens", + "isDeposit" + ], + "lineno": 175, + "name": "adjustLPTokens" + }, + { + "args": [ + "amountBalance", + "amount", + "amount2", + "lptAMMBalance", + "lpTokens", + "tfee", + "isDeposit" + ], + "lineno": 186, + "name": "adjustAmountsByLPTokens" + }, + { + "args": [ + "a", + "b", + "c" + ], + "lineno": 246, + "name": "solveQuadraticEq" + }, + { + "args": [ + "a", + "b", + "c" + ], + "lineno": 252, + "name": "solveQuadraticEqSmallest" + }, + { + "args": [ + "amount", + "frac", + "rm" + ], + "lineno": 265, + "name": "multiply" + }, + { + "args": [ + "rules", + "noRoundCb", + "balance", + "productCb", + "isDeposit" + ], + "lineno": 271, + "name": "getRoundedAsset" + }, + { + "args": [ + "rules", + "balance", + "frac", + "isDeposit" + ], + "lineno": 284, + "name": "getRoundedLPTokens" + }, + { + "args": [ + "rules", + "noRoundCb", + "lptAMMBalance", + "productCb", + "isDeposit" + ], + "lineno": 293, + "name": "getRoundedLPTokens" + }, + { + "args": [ + "rules", + "balance", + "amount", + "lptAMMBalance", + "tokens", + "tfee" + ], + "lineno": 312, + "name": "adjustAssetInByTokens" + }, + { + "args": [ + "rules", + "balance", + "amount", + "lptAMMBalance", + "tokens", + "tfee" + ], + "lineno": 332, + "name": "adjustAssetOutByTokens" + }, + { + "args": [ + "rules", + "lptAMMBalance", + "tokens", + "frac" + ], + "lineno": 352, + "name": "adjustFracByTokens" + }, + { + "args": [ + "view", + "ammAccountID", + "asset1", + "asset2", + "freezeHandling", + "authHandling", + "j" + ], + "lineno": 360, + "name": "ammPoolHolds" + }, + { + "args": [ + "view", + "ammSle", + "optAsset1", + "optAsset2", + "freezeHandling", + "authHandling", + "j" + ], + "lineno": 371, + "name": "ammHolds" + }, + { + "args": [ + "view", + "asset1", + "asset2", + "ammAccount", + "lpAccount", + "j" + ], + "lineno": 414, + "name": "ammLPHolds" + }, + { + "args": [ + "view", + "ammSle", + "lpAccount", + "j" + ], + "lineno": 445, + "name": "ammLPHolds" + }, + { + "args": [ + "view", + "ammSle", + "account" + ], + "lineno": 450, + "name": "getTradingFee" + }, + { + "args": [ + "view", + "ammAccountID", + "asset" + ], + "lineno": 474, + "name": "ammAccountHolds" + }, + { + "args": [ + "sb", + "ammAccountID", + "maxTrustlinesToDelete", + "j" + ], + "lineno": 495, + "name": "deleteAMMTrustLines" + }, + { + "args": [ + "sb", + "ammAccountID", + "j" + ], + "lineno": 523, + "name": "deleteAMMMPTokens" + }, + { + "args": [ + "sb", + "asset", + "asset2", + "j" + ], + "lineno": 555, + "name": "deleteAMMAccount" + }, + { + "args": [ + "view", + "ammSle", + "account", + "lptAsset", + "tfee" + ], + "lineno": 594, + "name": "initializeFeeAuctionVote" + }, + { + "args": [ + "view", + "ammIssue", + "lpAccount" + ], + "lineno": 629, + "name": "isOnlyLiquidityProvider" + }, + { + "args": [ + "sb", + "lpTokens", + "ammSle", + "account" + ], + "lineno": 701, + "name": "verifyAndAdjustLPTokenBalance" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested by integration and unit tests in the rippled codebase, especially in files such as 'test/AMM_test.cpp', 'test/AMMDepositWithdraw_test.cpp', or similar. These tests would cover normal and edge cases for AMM operations (deposits, withdrawals, LP token minting/burning). However, explicit validation of division-by-zero and type errors may not be directly tested unless negative/edge cases are included. There may be gaps in testing for runtime errors (e.g., division by zero) and for feature flag branches (e.g., fixAMMv1_3 enabled/disabled).", + "validation_architecture": { + "auto_validated_fields": [ + "STAmount (type, arithmetic validity)", + "Asset (type)", + "std::uint16_t (type)", + "root2 argument (likely non-negative)", + "division denominators (likely non-zero)" + ], + "framework": "None explicit; relies on C++ type system, runtime assertions, and business logic checks in math helpers", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "compile-time type error if wrong type", + "field": "asset1, asset2", + "location": "ammLPTokens", + "validated_by": "implicit type checking (STAmount)", + "validates": [ + "asset1 and asset2 must be STAmount" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "compile-time type error if wrong type", + "field": "lptIssue", + "location": "ammLPTokens", + "validated_by": "implicit type checking (Asset)", + "validates": [ + "lptIssue must be Asset" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "compile-time type error if wrong type", + "field": "asset1Balance, asset1Deposit, lptAMMBalance", + "location": "lpTokensOut", + "validated_by": "implicit type checking (STAmount)", + "validates": [ + "all must be STAmount" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "compile-time type error if wrong type", + "field": "tfee", + "location": "lpTokensOut, ammAssetIn, lpTokensIn", + "validated_by": "implicit type checking (std::uint16_t)", + "validates": [ + "tfee must be uint16_t" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "runtime exception (divide by zero or assertion in STAmount/Number)", + "field": "asset1Deposit / asset1Balance, lpTokens / lptAMMBalance, asset1Withdraw / asset1Balance", + "location": "lpTokensOut, ammAssetIn, lpTokensIn", + "validated_by": "division operator (runtime)", + "validates": [ + "denominator must not be zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "runtime exception/assertion if negative", + "field": "root2 argument", + "location": "ammLPTokens, lpTokensOut, lpTokensIn", + "validated_by": "root2 function (likely asserts non-negative input)", + "validates": [ + "argument to root2 must be non-negative" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "runtime exception/assertion if no real solution", + "field": "solveQuadraticEq arguments", + "location": "ammAssetIn", + "validated_by": "solveQuadraticEq function (likely checks for valid quadratic roots)", + "validates": [ + "discriminant must be non-negative" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (alters logic path)", + "field": "isFeatureEnabled(fixAMMv1_3)", + "location": "all functions", + "validated_by": "feature flag check", + "validates": [ + "feature flag must be set for new logic" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/AMMHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/AMMHelpers.cpp.ai.md new file mode 100644 index 0000000000..8d08342f46 --- /dev/null +++ b/src/libxrpl/ledger/helpers/AMMHelpers.cpp.ai.md @@ -0,0 +1,75 @@ +# `AMMHelpers.cpp` — AMM Mathematical Engine and Ledger Operations + +This file is the implementation core of XRPL's Automated Market Maker. It spans two distinct responsibilities: the closed-form mathematical formulas that determine how assets and LP tokens are exchanged during deposits and withdrawals, and the ledger-state utilities that read, validate, and clean up AMM accounts and trust lines. All code lives in the `xrpl` namespace; the companion header `AMMHelpers.h` additionally defines inline templates (`swapAssetIn`, `swapAssetOut`, `changeSpotPriceQuality`, `getAMMOfferStartWithTakerGets/Pays`) that depend on these primitives. + +## AMM Pool Invariant and Fee Encoding + +The central invariant enforced throughout this file is `sqrt(asset1 * asset2) >= LPTokenBalance`. `ammLPTokens()` computes the initial LP token supply as the geometric mean of both pool assets. Every subsequent deposit or withdrawal formula is derived from this same invariant, with trading fees baked in to make single-sided operations more expensive than proportional ones. + +Trading fees are stored as `uint16_t` in basis points scaled by `AUCTION_SLOT_FEE_SCALE_FACTOR` (100,000), so a value of 1000 represents 1%. The helpers `feeMult(tfee) = 1 - tfee/100000`, `feeMultHalf(tfee) = 1 - tfee/200000`, and `getFee(tfee) = tfee/100000` from `AMMCore.h` are used pervasively and are not re-derived here. + +## Single-Sided Deposit and Withdrawal Formulas + +The four paired formulas represent the heart of this file: + +**Equations 3 and 4** handle single-asset deposits. `lpTokensOut()` implements Equation 3, computing LP tokens minted for a given deposit amount `b` relative to pool balance `B`: + +``` +t = T * [(b/B - (sqrt(f2²-b/(B·f1))-f2)) / (1 + sqrt(f2²-b/(B·f1))-f2)] +``` + +`ammAssetIn()` solves the inverse (Eq. 4): given desired LP tokens, what asset deposit is required? The derivation reduces to a quadratic `(R/t2)² + R*(2d/t2 - 1/f1) + d² - f2² = 0`, solved by `solveQuadraticEq()`. + +**Equations 7 and 8** handle single-asset withdrawals. `lpTokensIn()` implements Equation 7, computing LP tokens to burn for a given withdrawal. `ammAssetOut()` solves the inverse (Eq. 8), which simplifies to a direct rational expression: `R = (t1² + t1*(f-2)) / (t1*f-1)`. + +The quadratic solver `solveQuadraticEq()` returns the positive root `(-b + sqrt(b²-4ac)) / 2a`. Its companion `solveQuadraticEqSmallest()`, used in offer generation (header templates), implements the numerically stable "citardauq" formula from Blinn's paper: when `b > 0` it uses `2c / (-b - sqrt(d))` instead of the standard form, avoiding catastrophic cancellation when both terms under subtraction are nearly equal. + +## The `fixAMMv1_3` Rounding Overhaul + +Every deposit/withdrawal formula has a pre-amendment and post-amendment code path, making this file a detailed record of how rounding semantics evolved. + +**Pre-amendment**: Arithmetic flows through `Number`'s default rounding mode and `toSTAmount()` converts the result at the end. + +**Post-amendment**: Directional rounding is applied at the final multiplication step using the `multiply()` function, which wraps `NumberRoundModeGuard` to set the mode for the duration of the call. The invariant dictates opposite directions: LP tokens on deposit round *downward* (we issue fewer tokens), assets on deposit round *upward* (the user pays more), assets on withdrawal round *downward* (the user gets less). The `detail::getLPTokenRounding()` and `detail::getAssetRounding()` inlines in the header encode this logic. + +`getRoundedAsset()` and `getRoundedLPTokens()` are the abstraction layer exposing this dispatch. Each has two overloads: one accepting a raw fraction (`Number`), one accepting a `std::function` callback. The lambda-based overload exists to avoid computing the formula twice in callers that don't know which code path will be taken — the callback is only evaluated inside the function once the rounding mode is set. + +`adjustAssetInByTokens()` and `adjustAssetOutByTokens()` address a secondary problem: after token adjustment (see below), the recalculated asset amount may exceed the original requested amount due to rounding working in an unexpected direction. The fix is to reduce the requested amount by the excess, recalculate tokens and then the asset, and return the minimum. + +## LP Token Precision Loss (`adjustLPTokens`) + +`STAmount` maintains 16 significant digits. When LP tokens are added to the AMM's total balance (e.g. `balance + tokens`), the sum may be rounded to fit 16 digits, meaning `(balance + tokens) - balance < tokens`. Naively recording this as the new token count would undercount the minted tokens. + +`adjustLPTokens()` fixes this by computing the amount the *other way around*: for a deposit it returns `(lptAMMBalance + lpTokens) - lptAMMBalance`, which applies the same precision loss to both sides and therefore cancels out the truncation. For withdrawal it uses `(lpTokens - lptAMMBalance) + lptAMMBalance`. The function forces `Number::downward` rounding so the adjusted token count is never more than the requested count. `adjustAmountsByLPTokens()` wraps this: under `fixAMMv1_3` it returns immediately (the new rounding strategy makes this adjustment unnecessary), but under older amendments it propagates the adjusted token count back into the asset amounts. + +## Ledger State Queries + +`ammHolds()` is the principal read function. It returns a three-tuple `(asset1, asset2, LPTokenBalance)` via `Expected`. The optional asset parameters let callers specify which pool side they care about — if only one is given, the function identifies the matching pool asset and returns the pair with the requested asset first. An invalid pair triggers `tecAMM_INVALID_TOKENS` and the unreachable error paths are annotated `LCOV_EXCL_START`. + +`ammLPHolds()` deliberately avoids reusing `accountHolds()`. The comment explains the distinction: `accountHolds()` checks whether the underlying pool assets are frozen (gated by `fixFrozenLPTokenTransfer`), but LP token *balance* queries should only check whether the LP token trustline itself is frozen, not the pool assets. + +`ammAccountHolds()` reads raw balances without the balance hook, using `Asset::visit()` to dispatch over both IOU and MPT cases. The AMM account can hold either type as its pool assets. + +`getTradingFee()` returns the effective fee for a specific account: if the account holds a valid (non-expired) auction slot, it returns `sfDiscountedFee`; otherwise the global `sfTradingFee`. The expiration check compares `parentCloseTime` in seconds against the slot's stored expiration, which is `parentCloseTime + TOTAL_TIME_SLOT_SECS` (24 hours) set at initialization time. + +## AMM Account Lifecycle and Cleanup + +`deleteAMMAccount()` orchestrates full account teardown in a specific order that matters for correctness: + +1. `deleteAMMTrustLines()` sweeps the owner directory, removing zero-balance IOU trustlines (`ltRIPPLE_STATE`). Any non-zero balance returns `tecINTERNAL`, which is annotated as unreachable under correct business logic. MPToken and AMM entries are skipped. + +2. Only if trustlines are fully deleted does `deleteAMMMPTokens()` run. The ordering is intentional: if the AMM cannot fully delete trustlines (e.g., `tecINCOMPLETE` is returned upstream), the AMM can be recreated via a new deposit, and any MPToken objects for the pool assets must remain for that path to work. + +3. After both passes, the owner directory link and both the AMM SLE and the AMM root account SLE are erased from the `Sandbox`. + +The `deleteAMMTrustLines` limit parameter (`maxDeletableAMMTrustLines`) allows partial deletion, returning `tecINCOMPLETE` if the directory isn't fully drained, enabling multi-transaction deletion. MPTokens allow at most three items (two pool-side MPTs plus the AMM object), so a fixed limit of 3 suffices there. + +## Fee Auction Initialization + +`initializeFeeAuctionVote()` is called both on `AMMCreate` and whenever a depleted AMM receives its first deposit. It writes a single vote entry with `VOTE_WEIGHT_SCALE_FACTOR` (100%) weight for the creator, then constructs the auction slot with a 24-hour expiration and a `sfPrice` of zero (the creator gets the slot for free). The discounted fee is `tfee / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION` (one-tenth of the full fee), and both fee fields are conditionally omitted via `makeFieldAbsent()` if the value is zero, preserving the canonical encoding of absent-field serialization. + +## Last-LP Balance Reconciliation + +`isOnlyLiquidityProvider()` walks the AMM account's owner directory (up to 10 pages, sufficient for at most four ledger objects) and classifies each entry as the AMM SLE, an LPToken trustline, an IOU pool-asset trustline, or an MPToken. If any non-LP LPToken trustline appears, it returns `false` immediately — there are other LPs. Final validation checks that exactly one LPToken trustline exists and between one and two pool-asset entries (IOU or MPT) are present. + +`verifyAndAdjustLPTokenBalance()` uses this to patch the AMM's `sfLPTokenBalance` during a final withdrawal. The stored balance and the LP's actual trustline balance may differ by a small fraction due to the 16-digit precision limit. If the discrepancy is within 0.1% (tolerance `Number{1, -3}`), the AMM balance is silently updated; if it exceeds this, `tecAMM_INVALID_TOKENS` is returned to reject the transaction. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp.ai.json new file mode 100644 index 0000000000..7a618c2edd --- /dev/null +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp.ai.json @@ -0,0 +1,580 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "isGlobalFrozen", + "isXRP", + "view.read(keylet::account(issuer))", + "sle->isFlag(lsfGlobalFreeze)" + ], + "entry_point": "isGlobalFrozen", + "purpose": "Checks if an account (issuer) is globally frozen.", + "validation_points": [ + "isXRP(issuer) - validates if issuer is XRP (special case, always not frozen)", + "view.read(keylet::account(issuer)) - validates issuer exists in ledger", + "sle->isFlag(lsfGlobalFreeze) - validates if global freeze flag is set" + ] + }, + { + "call_chain": [ + "confineOwnerCount" + ], + "entry_point": "confineOwnerCount", + "purpose": "Adjusts owner count, clamps to [0, UINT32_MAX], logs on overflow/underflow.", + "validation_points": [ + "if (adjusted < current) - detects overflow", + "if (adjusted > current) - detects underflow", + "if (id) { JLOG(j.fatal()) ... } - logs error if id is present" + ] + }, + { + "call_chain": [ + "xrpLiquid", + "view.read(keylet::account(id))", + "confineOwnerCount", + "isPseudoAccount", + "view.fees().accountReserve(ownerCount)", + "view.balanceHookIOU" + ], + "entry_point": "xrpLiquid", + "purpose": "Calculates liquid XRP for an account, considering reserve and owner count.", + "validation_points": [ + "view.read(keylet::account(id)) - validates account exists", + "confineOwnerCount - validates ownerCount adjustment" + ] + }, + { + "call_chain": [ + "adjustOwnerCount", + "confineOwnerCount", + "view.adjustOwnerCountHook", + "sle->at(sfOwnerCount) = adjusted", + "view.update(sle)" + ], + "entry_point": "adjustOwnerCount", + "purpose": "Adjusts the owner count for an account and updates the ledger entry.", + "validation_points": [ + "confineOwnerCount - validates adjustment" + ] + }, + { + "call_chain": [ + "transferRate", + "view.read(keylet::account(issuer))", + "sle->isFieldPresent(sfTransferRate)", + "sle->getFieldU32(sfTransferRate)" + ], + "entry_point": "transferRate", + "purpose": "Fetches the transfer rate for an issuer account.", + "validation_points": [ + "view.read(keylet::account(issuer)) - validates issuer exists" + ] + } + ], + "data_flows": [ + { + "field": "issuer (AccountID)", + "flow": [ + "Function argument", + "isXRP(issuer) or view.read(keylet::account(issuer))", + "sle->isFlag(lsfGlobalFreeze) or sle->getFieldU32(sfTransferRate)" + ], + "origin": "Function argument to isGlobalFrozen, transferRate", + "transformations": [ + "Checked if XRP (special case)", + "Looked up in ledger", + "Flag or field checked" + ], + "validated_at": "isXRP(issuer), view.read(keylet::account(issuer))" + }, + { + "field": "ownerCount (std::uint32_t)", + "flow": [ + "sle->getFieldU32(sfOwnerCount)", + "confineOwnerCount(current, adjustment, id, j)", + "adjusted value returned", + "Used in reserve calculation or written back to ledger" + ], + "origin": "Ledger entry field sfOwnerCount", + "transformations": [ + "Adjusted by signed int", + "Clamped to [0, UINT32_MAX]", + "Logged if overflow/underflow" + ], + "validated_at": "confineOwnerCount" + }, + { + "field": "id (AccountID, optional)", + "flow": [ + "Passed to confineOwnerCount", + "Used for logging if present" + ], + "origin": "Optional argument to confineOwnerCount", + "transformations": [ + "Only used for error reporting" + ], + "validated_at": "if (id) { JLOG(j.fatal()) ... }" + }, + { + "field": "lsfGlobalFreeze flag", + "flow": [ + "sle->isFlag(lsfGlobalFreeze)" + ], + "origin": "Ledger entry flag", + "transformations": [ + "Boolean check" + ], + "validated_at": "sle->isFlag(lsfGlobalFreeze)" + }, + { + "field": "sfTransferRate", + "flow": [ + "sle->isFieldPresent(sfTransferRate)", + "sle->getFieldU32(sfTransferRate)" + ], + "origin": "Ledger entry field", + "transformations": [ + "Presence check", + "Value extraction" + ], + "validated_at": "sle->isFieldPresent(sfTransferRate)" + } + ], + "description": "This file provides helper functions for working with AccountRoot objects in the XRPL ledger, including owner count adjustments, pseudo-account management, transfer rate retrieval, and destination tag checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "issuer (AccountID)", + "empty", + "string", + "validation" + ], + "evidence": "isXRP(issuer) at isGlobalFrozen", + "issue_pattern": "Missing empty string validation for issuer (AccountID)", + "why_false_positive": "isXRP(issuer) validates issuer (AccountID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "issuer (AccountID)", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::account(issuer)) at isGlobalFrozen", + "issue_pattern": "Missing empty string validation for issuer (AccountID)", + "why_false_positive": "view.read(keylet::account(issuer)) validates issuer (AccountID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfGlobalFreeze flag", + "empty", + "string", + "validation" + ], + "evidence": "sle->isFlag(lsfGlobalFreeze) at isGlobalFrozen", + "issue_pattern": "Missing empty string validation for lsfGlobalFreeze flag", + "why_false_positive": "sle->isFlag(lsfGlobalFreeze) validates lsfGlobalFreeze flag for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ownerCount (std::uint32_t)", + "empty", + "string", + "validation" + ], + "evidence": "confineOwnerCount at confineOwnerCount", + "issue_pattern": "Missing empty string validation for ownerCount (std::uint32_t)", + "why_false_positive": "confineOwnerCount validates ownerCount (std::uint32_t) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "ownerCount (std::uint32_t)", + "range", + "bounds", + "validation" + ], + "evidence": "confineOwnerCount at confineOwnerCount", + "issue_pattern": "Missing range validation for ownerCount (std::uint32_t)", + "why_false_positive": "confineOwnerCount validates ownerCount (std::uint32_t) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "id (AccountID, optional)", + "empty", + "string", + "validation" + ], + "evidence": "if (id) { JLOG(j.fatal()) ... } at confineOwnerCount", + "issue_pattern": "Missing empty string validation for id (AccountID, optional)", + "why_false_positive": "if (id) { JLOG(j.fatal()) ... } validates id (AccountID, optional) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle (SLE pointer)", + "empty", + "string", + "validation" + ], + "evidence": "if (sle == nullptr) at xrpLiquid", + "issue_pattern": "Missing empty string validation for sle (SLE pointer)", + "why_false_positive": "if (sle == nullptr) validates sle (SLE pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ownerCount (std::uint32_t)", + "empty", + "string", + "validation" + ], + "evidence": "confineOwnerCount at xrpLiquid", + "issue_pattern": "Missing empty string validation for ownerCount (std::uint32_t)", + "why_false_positive": "confineOwnerCount validates ownerCount (std::uint32_t) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "ownerCount (std::uint32_t)", + "range", + "bounds", + "validation" + ], + "evidence": "confineOwnerCount at xrpLiquid", + "issue_pattern": "Missing range validation for ownerCount (std::uint32_t)", + "why_false_positive": "confineOwnerCount validates ownerCount (std::uint32_t) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "reserve (XRPAmount)", + "empty", + "string", + "validation" + ], + "evidence": "isPseudoAccount(sle) at xrpLiquid", + "issue_pattern": "Missing empty string validation for reserve (XRPAmount)", + "why_false_positive": "isPseudoAccount(sle) validates reserve (XRPAmount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "balance (STAmount)", + "empty", + "string", + "validation" + ], + "evidence": "balance < reserve ? STAmount{0} : balance - reserve at xrpLiquid", + "issue_pattern": "Missing empty string validation for balance (STAmount)", + "why_false_positive": "balance < reserve ? STAmount{0} : balance - reserve validates balance (STAmount) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "AccountID const& issuer" + ], + "lineno": 9, + "name": "isGlobalFrozen" + }, + { + "args": [ + "std::uint32_t current", + "std::int32_t adjustment", + "std::optional const& id", + "beast::Journal j" + ], + "lineno": 19, + "name": "confineOwnerCount" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& id", + "std::int32_t ownerCountAdj", + "beast::Journal j" + ], + "lineno": 54, + "name": "xrpLiquid" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& issuer" + ], + "lineno": 77, + "name": "transferRate" + }, + { + "args": [ + "ApplyView& view", + "std::shared_ptr const& sle", + "std::int32_t amount", + "beast::Journal j" + ], + "lineno": 86, + "name": "adjustOwnerCount" + }, + { + "args": [ + "ReadView const& view", + "uint256 const& pseudoOwnerKey" + ], + "lineno": 99, + "name": "pseudoAccountAddress" + }, + { + "args": [], + "lineno": 117, + "name": "getPseudoAccountFields" + }, + { + "args": [ + "std::shared_ptr sleAcct", + "std::set const& pseudoFieldFilter" + ], + "lineno": 143, + "name": "isPseudoAccount" + }, + { + "args": [ + "ApplyView& view", + "uint256 const& pseudoOwnerKey", + "SField const& ownerField" + ], + "lineno": 155, + "name": "createPseudoAccount" + }, + { + "args": [ + "SLE::const_ref toSle", + "bool hasDestinationTag" + ], + "lineno": 191, + "name": "checkDestinationAndTag" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "These helpers are core to ledger/account logic and are likely tested indirectly via higher-level transaction and ledger tests. Direct unit tests may exist in files like AccountRootHelpers_test.cpp, Ledger_test.cpp, or Account_test.cpp. However, some edge cases (e.g., confineOwnerCount overflow/underflow with/without id, pseudo-account logic) may not be directly tested unless specifically targeted. Logging/error reporting paths (e.g., JLOG(j.fatal())) are often not covered by standard tests. Integration tests that exercise account creation, reserve changes, and freeze logic will cover most validation paths, but explicit negative/overflow/underflow cases should be checked for coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom C++ logic, no external validation framework", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "issuer (AccountID)", + "location": "isGlobalFrozen", + "validated_by": "isXRP(issuer)", + "validates": [ + "Checks if issuer is XRP, which cannot be frozen" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "issuer (AccountID)", + "location": "isGlobalFrozen", + "validated_by": "view.read(keylet::account(issuer))", + "validates": [ + "Checks if issuer account exists in ledger" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "lsfGlobalFreeze flag", + "location": "isGlobalFrozen", + "validated_by": "sle->isFlag(lsfGlobalFreeze)", + "validates": [ + "Checks if the global freeze flag is set on the account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (clamps value, logs fatal)", + "field": "ownerCount (std::uint32_t)", + "location": "confineOwnerCount", + "validated_by": "confineOwnerCount", + "validates": [ + "Prevents ownerCount from going negative (underflow)", + "Prevents ownerCount from exceeding uint32_t max (overflow)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (logs fatal)", + "field": "id (AccountID, optional)", + "location": "confineOwnerCount", + "validated_by": "if (id) { JLOG(j.fatal()) ... }", + "validates": [ + "Logs error if ownerCount would underflow or overflow for a specific account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns beast::zero)", + "field": "sle (SLE pointer)", + "location": "xrpLiquid", + "validated_by": "if (sle == nullptr)", + "validates": [ + "Checks if account exists before proceeding" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "none (see above)", + "field": "ownerCount (std::uint32_t)", + "location": "xrpLiquid", + "validated_by": "confineOwnerCount", + "validates": [ + "Ensures ownerCount is within valid range before using for reserve calculation" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "reserve (XRPAmount)", + "location": "xrpLiquid", + "validated_by": "isPseudoAccount(sle)", + "validates": [ + "Pseudo-accounts have no reserve requirement" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "balance (STAmount)", + "location": "xrpLiquid", + "validated_by": "balance < reserve ? STAmount{0} : balance - reserve", + "validates": [ + "Ensures liquid balance cannot be negative (clamps to zero)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp.ai.md new file mode 100644 index 0000000000..73500cf2d7 --- /dev/null +++ b/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp.ai.md @@ -0,0 +1,42 @@ +# `AccountRootHelpers.cpp` — AccountRoot Ledger Object Utilities + +This file implements the stateless helper functions declared in `AccountRootHelpers.h` for reading from and writing to `AccountRoot` ledger objects. It covers the full surface area of account-level concerns: freeze state, spendable balance, owner count bookkeeping, transfer fees, destination tag enforcement, and the creation and detection of pseudo-accounts. It sits at the intersection of the read-only `ReadView` and the mutable `ApplyView` interfaces, and nearly every transaction processor touches at least one function here. + +## Owner Count Arithmetic: `confineOwnerCount` + +The file-private `confineOwnerCount()` is the foundation for two public functions. It accepts the current `uint32_t` owner count and a signed adjustment, deliberately relying on **well-defined unsigned overflow/underflow semantics** to detect boundary violations rather than doing range-checked signed arithmetic. If `adjusted < current` after a positive `adjustment`, unsigned wrap has occurred; if `adjusted > current` after a negative `adjustment`, unsigned underflow has occurred. Both are clamped — overflow to `UINT32_MAX`, underflow to `0` — and a fatal-level log is emitted when an `AccountID` is provided. The optional `id` parameter is a design choice that allows `xrpLiquid` to call the function without an account ID (speculative call for reserve computation), while `adjustOwnerCount` always passes one for full diagnostics. + +## Liquid XRP Calculation: `xrpLiquid` + +`xrpLiquid()` answers the question "how much XRP can this account freely spend right now?" Its formula is straightforward: `max(0, balance − reserve)`. What makes the implementation non-obvious is that both the balance read and the owner-count read route through virtual hook methods on the view: + +```cpp +confineOwnerCount(view.ownerCountHook(id, sle->getFieldU32(sfOwnerCount)), ownerCountAdj); +// ... +view.balanceHookIOU(id, xrpAccount(), fullBalance); +``` + +In normal transaction processing, `ownerCountHook` and `balanceHookIOU` are identity functions that return their arguments unchanged. But when called from within a `PaymentSandbox`, they return conservative values that account for credits and owner-count changes made earlier in the same payment path. This hook-dispatch design lets `xrpLiquid` serve both contexts without branching on payment vs. non-payment logic. + +Pseudo-accounts receive special treatment: `isPseudoAccount(sle)` bypasses the reserve calculation entirely (`XRPAmount{0}`), because protocol-controlled accounts are not subject to base and owner reserve requirements. Normal accounts clamp the result at zero to prevent a negative spendable balance from being returned. + +## Owner Count Write Path: `adjustOwnerCount` + +`adjustOwnerCount()` is the write-side counterpart. After computing the new count through `confineOwnerCount`, it calls `view.adjustOwnerCountHook()` before writing to the SLE. `PaymentSandbox` overrides this hook to record the high-water-mark owner count — since payments can only decrease owner counts, the maximum observed count is the conservative bound used by `ownerCountHook` on subsequent reads within the same payment. This pairing of `adjustOwnerCountHook` and `ownerCountHook` prevents a transient count reduction mid-payment from bypassing reserve checks. + +## Pseudo-Account Infrastructure + +The pseudo-account system is the most architecturally significant responsibility in this file. Pseudo-accounts are `AccountRoot` objects controlled entirely by protocol logic rather than by a private key. They are used today for AMM pools (`sfAMMID`), single-asset Vaults (`sfVaultID`), and Loan Brokers (`sfLoanBrokerID`). + +**Address generation** (`pseudoAccountAddress`) derives a candidate `AccountID` from the parent object's key by hashing `(attempt_index, ledger_parentHash, pseudoOwnerKey)` through `sha512Half` then `ripesha_hasher`. Incorporating the ledger's `parentHash` prevents precomputation of collisions. The loop retries up to `maxAccountAttempts = 256` times — this constant is annotated as immutable without an amendment, since changing it would alter the address space. On exhaustion it returns `beast::zero`; `createPseudoAccount` propagates this as `tecDUPLICATE`. + +**Field-based pseudo detection** (`getPseudoAccountFields`, `isPseudoAccount`) relies on a metadata flag on `SField` definitions: `SField::sMD_PseudoAccount`. Any `SField` carrying that flag is a pseudo-account designator. `getPseudoAccountFields` builds and caches the authoritative list at first call by scanning the `ltACCOUNT_ROOT` SOTemplate from `LedgerFormats::getInstance()`. `isPseudoAccount` then checks whether any of those fields is present on a given SLE, with an optional filter to test for a specific pseudo-account type. The comment in the implementation explicitly names this "defensive coding" — the null check and `ltACCOUNT_ROOT` type guard are included even when callers might already guarantee them, keeping the semantics of a `true` return value unambiguous. + +**Account creation** (`createPseudoAccount`) assembles the `AccountRoot` SLE with: +- Zero balance and sequence number `0` (when `featureSingleAssetVault` or `featureLendingProtocol` is active) to make pseudo-accounts visually distinguishable and prevent them from submitting transactions even if the disable-master flag were somehow bypassed. +- `lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth` — disabling the master key prevents all transaction submission; enabling default ripple allows trust-line flows for AMM and vault assets; deposit authorization blocks unsolicited incoming payments. +- The `ownerField` back-link stores the parent object's key in the pseudo-account's ledger entry. The `XRPL_ASSERT` in `createPseudoAccount` verifies the caller passed a field that actually carries `sMD_PseudoAccount`, catching misuse at debug time. + +## Remaining Utilities + +`isGlobalFrozen` guards against operating on XRP (which cannot be frozen) before reading the `lsfGlobalFreeze` flag, returning `false` for non-existent accounts rather than asserting. `transferRate` defaults to `parityRate` (one billion, representing 1:1) when no `sfTransferRate` field is present, ensuring the caller never has to handle a missing-fee case. `checkDestinationAndTag` centralises the two-step destination validation — existence check followed by `lsfRequireDestTag` enforcement — that would otherwise be repeated across all payment-adjacent transactors. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp.ai.json new file mode 100644 index 0000000000..3026a0d452 --- /dev/null +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp.ai.json @@ -0,0 +1,413 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "removeExpired", + "checkExpired", + "deleteSLE", + "delSLE (lambda in deleteSLE)" + ], + "entry_point": "removeExpired", + "purpose": "Removes expired credential SLEs from the ledger.", + "validation_points": [ + "checkExpired: Validates if credential is expired (sfExpiration field).", + "deleteSLE: Validates sleCredential pointer.", + "delSLE (lambda): Validates sleAccount pointer and owner directory removal." + ] + }, + { + "call_chain": [ + "deleteSLE", + "delSLE (lambda)", + "view.dirRemove", + "view.erase" + ], + "entry_point": "deleteSLE", + "purpose": "Deletes a credential SLE and removes it from owner directories.", + "validation_points": [ + "deleteSLE: Validates sleCredential pointer.", + "delSLE (lambda): Validates sleAccount pointer and owner directory removal." + ] + }, + { + "call_chain": [ + "checkFields" + ], + "entry_point": "checkFields", + "purpose": "Validates the structure and uniqueness of the sfCredentialIDs array in a transaction.", + "validation_points": [ + "checkFields: Validates presence, size, and uniqueness of sfCredentialIDs." + ] + }, + { + "call_chain": [ + "valid" + ], + "entry_point": "valid", + "purpose": "Validates that credential IDs in a transaction are valid (partial code shown).", + "validation_points": [ + "valid: (Partial, but likely validates credential existence/ownership.)" + ] + } + ], + "data_flows": [ + { + "field": "sfExpiration", + "flow": [ + "SLE in ledger", + "(*sleCredential)[~sfExpiration]", + "checkExpired", + "removeExpired" + ], + "origin": "SLE (credential object in ledger)", + "transformations": [ + "Optional extraction (value_or max uint32_t)", + "Compared to current time" + ], + "validated_at": "checkExpired" + }, + { + "field": "sleCredential (pointer)", + "flow": [ + "view.peek", + "removeExpired", + "deleteSLE" + ], + "origin": "view.peek(keylet::credential(h))", + "transformations": [ + "Pointer checked for null" + ], + "validated_at": "deleteSLE" + }, + { + "field": "sleAccount (pointer)", + "flow": [ + "delSLE (lambda in deleteSLE)", + "view.peek", + "sleAccount" + ], + "origin": "view.peek(keylet::account(account))", + "transformations": [ + "Pointer checked for null" + ], + "validated_at": "delSLE (lambda in deleteSLE)" + }, + { + "field": "owner directory removal (dirRemove result)", + "flow": [ + "delSLE (lambda in deleteSLE)", + "view.dirRemove", + "result checked" + ], + "origin": "view.dirRemove(keylet::ownerDir(account), ...)", + "transformations": [ + "Boolean result checked" + ], + "validated_at": "delSLE (lambda in deleteSLE)" + }, + { + "field": "sfCredentialIDs", + "flow": [ + "tx.getFieldV256(sfCredentialIDs)", + "checkFields" + ], + "origin": "Transaction (STTx)", + "transformations": [ + "Checked for presence", + "Checked for size", + "Checked for duplicates" + ], + "validated_at": "checkFields" + } + ], + "description": "This file provides helper functions for managing and validating credentials in the XRPL ledger, including checking for expired credentials, removing expired credentials, validating credential fields, verifying domain and deposit preauthorization, and deleting credential objects from the ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfExpiration (Expiration field of credential SLE)", + "empty", + "string", + "validation" + ], + "evidence": "checkExpired (local function) at checkExpired", + "issue_pattern": "Missing empty string validation for sfExpiration (Expiration field of credential SLE)", + "why_false_positive": "checkExpired (local function) validates sfExpiration (Expiration field of credential SLE) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sleCredential (pointer validity)", + "empty", + "string", + "validation" + ], + "evidence": "deleteSLE (local function) at deleteSLE", + "issue_pattern": "Missing empty string validation for sleCredential (pointer validity)", + "why_false_positive": "deleteSLE (local function) validates sleCredential (pointer validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sleAccount (pointer validity)", + "empty", + "string", + "validation" + ], + "evidence": "delSLE (lambda inside deleteSLE) at deleteSLE", + "issue_pattern": "Missing empty string validation for sleAccount (pointer validity)", + "why_false_positive": "delSLE (lambda inside deleteSLE) validates sleAccount (pointer validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "owner directory removal (dirRemove result)", + "empty", + "string", + "validation" + ], + "evidence": "delSLE (lambda inside deleteSLE) at deleteSLE", + "issue_pattern": "Missing empty string validation for owner directory removal (dirRemove result)", + "why_false_positive": "delSLE (lambda inside deleteSLE) validates owner directory removal (dirRemove result) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/CredentialHelpers.cpp", + "functions": [ + { + "args": [ + "sleCredential", + "closed" + ], + "lineno": 10, + "name": "checkExpired" + }, + { + "args": [ + "view", + "arr", + "j" + ], + "lineno": 17, + "name": "removeExpired" + }, + { + "args": [ + "view", + "sleCredential", + "j" + ], + "lineno": 36, + "name": "deleteSLE" + }, + { + "args": [ + "tx", + "j" + ], + "lineno": 81, + "name": "checkFields" + }, + { + "args": [ + "tx", + "view", + "src", + "j" + ], + "lineno": 104, + "name": "valid" + }, + { + "args": [ + "view", + "domainID", + "subject" + ], + "lineno": 132, + "name": "validDomain" + }, + { + "args": [ + "view", + "credIDs", + "dst" + ], + "lineno": 164, + "name": "authorizedDepositPreauth" + }, + { + "args": [ + "credentials" + ], + "lineno": 185, + "name": "makeSorted" + }, + { + "args": [ + "credentials", + "maxSize", + "j" + ], + "lineno": 194, + "name": "checkArray" + }, + { + "args": [ + "view", + "account", + "domainID", + "j" + ], + "lineno": 227, + "name": "verifyValidDomain" + }, + { + "args": [ + "tx", + "view", + "src", + "dst", + "sleDst", + "j" + ], + "lineno": 251, + "name": "verifyDepositPreauth" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "credentials" + } + ], + "test_coverage_notes": "The code is likely tested by unit/integration tests for credential expiration, credential deletion, and transaction field validation. Tests should exist for: (1) expired credential removal, (2) credential deletion with missing/invalid pointers, (3) malformed credential arrays (empty, too large, duplicates). However, some error branches (e.g., fatal logs, internal errors, directory removal failures) are marked LCOV_EXCL_START/STOP, indicating they are not covered by tests. Test files may include: CredentialHelpers_test.cpp, Ledger_test.cpp, or transaction validation suites. Gaps: Error/failure branches and some internal errors are not tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation logic (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (returns bool)", + "field": "sfExpiration (Expiration field of credential SLE)", + "location": "checkExpired", + "validated_by": "checkExpired (local function)", + "validates": [ + "Checks if the credential's expiration time is set and not expired compared to closed ledger time", + "If sfExpiration is missing, uses max uint32_t (never expires)" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 1.0, + "error_thrown": "Returns tecNO_ENTRY if sleCredential is null", + "field": "sleCredential (pointer validity)", + "location": "deleteSLE", + "validated_by": "deleteSLE (local function)", + "validates": [ + "Ensures the credential SLE pointer is not null before proceeding" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Returns tecINTERNAL if sleAccount is null", + "field": "sleAccount (pointer validity)", + "location": "deleteSLE", + "validated_by": "delSLE (lambda inside deleteSLE)", + "validates": [ + "Ensures the owner account SLE exists before modifying directories" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Returns tefBAD_LEDGER if dirRemove fails", + "field": "owner directory removal (dirRemove result)", + "location": "deleteSLE", + "validated_by": "delSLE (lambda inside deleteSLE)", + "validates": [ + "Ensures the credential is successfully removed from the owner's directory" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/CredentialHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp.ai.md new file mode 100644 index 0000000000..f135b1bc77 --- /dev/null +++ b/src/libxrpl/ledger/helpers/CredentialHelpers.cpp.ai.md @@ -0,0 +1,50 @@ +# `CredentialHelpers.cpp` — Credential Lifecycle and Authorization Helpers + +This file implements the complete set of helper functions that govern credential lifecycle management and authorization checking in the XRPL ledger. Credentials on the XRP Ledger are ledger objects (SLEs) linking an issuer to a subject, optionally gated by expiration and requiring subject acceptance. This module is the shared logic layer consumed by payment, escrow, payment-channel, vault, and MPToken transactors whenever those transactions need to verify that a sender is authorized via credentials or deposit pre-authorization. + +## The `ReadView`/`ApplyView` Split and the Two-Phase Validation Pattern + +The single most important architectural decision in this file is the deliberate pairing of read-only `preclaim` functions with writable `doApply` counterparts. Ledger mutations — specifically deleting expired credential objects — are only legal during `doApply`, when the transactor holds an `ApplyView`. But authorization rejection must be decided in `preclaim` so the transaction can be rejected before fee collection. Expired credentials complicate this: a transaction may arrive at preclaim with credentials that have just expired, and those objects need to be cleaned up even if the transaction itself is eventually rejected. + +The design resolves this tension with an explicit two-phase protocol: + +- `credentials::validDomain()` (takes `ReadView const&`) scans the domain's accepted credential list, identifies any that are expired, and returns `tecEXPIRED` if all valid credentials are expired. The caller in preclaim is expected to suppress that specific error and allow the transaction to reach `doApply`. +- `verifyValidDomain()` (takes `ApplyView&`) re-runs the same check but this time calls `credentials::removeExpired()` to physically delete the expired SLEs. It then re-checks whether any live, accepted credential remains. + +This same pattern applies to deposit preauthorization: `credentials::valid()` checks credential existence and ownership in preclaim using a `ReadView`, while `verifyDepositPreauth()` handles the mutable side in doApply — calling `credentials::removeExpired()` on the credential IDs in the transaction before checking whether the destination account's `DepositPreauth` entry allows the sender. + +The effect is that any transaction touching credentials acts as a passive garbage collector for expired credential objects, even transactions that ultimately fail. + +## Credential Deletion: `deleteSLE()` and Dual Owner Directories + +Deleting a credential SLE is non-trivial because a credential is indexed in *two* owner directories: the issuer's and the subject's. The SLE stores `sfIssuerNode` and `sfSubjectNode` field offsets so it can be found in each directory without a search. The inner `delSLE` lambda encapsulates the per-account removal: it peeks the account SLE, calls `view.dirRemove()` to remove the credential from that account's directory, and conditionally adjusts the owner reserve count. + +The reserve accounting follows the credential lifecycle: before the subject accepts (`lsfAccepted` is unset), only the issuer holds the reserve burden; after acceptance, the subject takes ownership and the burden shifts. This is implemented in `deleteSLE()` by passing `isOwner = !accepted || (subject == issuer)` to the issuer-side call, and `isOwner = accepted` to the subject-side call. When issuer and subject are the same account, only one directory removal is performed. + +Internal invariant failures (missing account SLE, `dirRemove` returning false) are wrapped in `LCOV_EXCL_START`/`STOP` blocks — these paths indicate ledger corruption that is believed to be unreachable under normal operation, so they are intentionally excluded from coverage analysis. + +## Expiration: `checkExpired()` and the `uint32_t::max` Sentinel + +`checkExpired()` reads `sfExpiration` from the credential SLE using the optional-field accessor (`~sfExpiration`), defaulting to `std::numeric_limits::max()` when the field is absent. Comparing `now > max` is always false, meaning credentials without an expiration field never expire without any special-case branching. The time source is `view.header().parentCloseTime`, ensuring deterministic behavior across all validators — the *parent* ledger's close time rather than the current wall clock. + +`removeExpired()` iterates a `STVector256` of credential keys, peeks each SLE, and deletes any that are expired. The comment noting that credentials were already validated in preclaim explains why this loop only checks expiration: existence and ownership were already confirmed; here only the time gate matters. Expired credentials are deleted even if the outer transaction fails (`doApply` sets them up to be written regardless of the main transaction result because removal happens unconditionally before the main result is returned). + +## Transaction Field Validation: `checkFields()` and `checkArray()` + +Two sibling functions handle syntactic validation at preflight time, covering the two ways credentials appear in transactions. + +`checkFields()` validates the `sfCredentialIDs` field (a `STVector256` of 256-bit hashes referencing existing credential objects). It enforces non-empty, bounded size (at most `maxCredentialsArraySize = 8`), and uniqueness via an `unordered_set`. + +`checkArray()` validates credential arrays that appear in `DepositPreauth` and `PermissionedDomainSet` transactions, where credentials are specified as `(issuer, credentialType)` pairs rather than object hashes. It validates issuer account ID validity, `credentialType` length (1 to `maxCredentialTypeLength = 64` bytes), and duplicate detection using `sha512Half(issuer, credentialType)` — hashing the pair to catch logical duplicates even when the binary representations differ subtly. + +## Deposit Preauth Credential Authorization: `authorizedDepositPreauth()` + +This function implements the credential-based path of deposit preauthorization. It builds a `std::set>` of `(issuer, credentialType)` pairs from the submitted credential IDs, then checks whether `keylet::depositPreauth(dst, sorted)` exists in the ledger. The sorted set representation matches how `DepositPreauth` objects are keyed, allowing an O(log n) lookup against the single ledger entry. + +The `lifeExtender` vector is a deliberate lifetime management artifact. `Slice` is a non-owning view into the SLE's underlying storage. If the `shared_ptr` were allowed to drop reference count to zero before the lookup, the `Slice` in the sorted set would dangle. Keeping all SLE pointers alive in `lifeExtender` for the duration of the function prevents this. + +`makeSorted()` is the companion utility that builds the same sorted structure from an `STArray`, used at `DepositPreauth` creation time to normalize the set before generating the keylet. + +## Relationship to Transactors + +`verifyDepositPreauth()` is called from `Payment`, `EscrowFinish`, and `PaymentChannelClaim` transactors during `doApply` when the destination account has the `lsfDepositAuth` flag set. It short-circuits for self-payments (`src == dst`), then falls through to account-level preauth check (`keylet::depositPreauth(dst, src)`) before falling back to credential-based preauth via `authorizedDepositPreauth()`. `verifyValidDomain()` is called by `MPTokenHelpers` and vault transactors during `doApply` to enforce domain-based credential gating on MPToken issuances and vaults. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/DirectoryHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/DirectoryHelpers.cpp.ai.json new file mode 100644 index 0000000000..c4217fc0f2 --- /dev/null +++ b/src/libxrpl/ledger/helpers/DirectoryHelpers.cpp.ai.json @@ -0,0 +1,404 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "forEachItem" + ], + "entry_point": "forEachItem", + "purpose": "Iterates over all items in a directory node, applying a function to each child SLE.", + "validation_points": [ + "XRPL_ASSERT(root.type == ltDIR_NODE, ...)", + "if (root.type != ltDIR_NODE) return;" + ] + }, + { + "call_chain": [ + "forEachItemAfter" + ], + "entry_point": "forEachItemAfter", + "purpose": "Iterates over directory items after a given key, with optional hint and limit, applying a function to each.", + "validation_points": [ + "XRPL_ASSERT(root.type == ltDIR_NODE, ...)", + "if (root.type != ltDIR_NODE) return false;", + "if (after.isNonZero()) { ... }" + ] + }, + { + "call_chain": [ + "dirFirst", + "detail::internalDirFirst" + ], + "entry_point": "dirFirst", + "purpose": "Finds the first entry in a directory node.", + "validation_points": [ + "Validation likely in internalDirFirst (not shown here)" + ] + }, + { + "call_chain": [ + "dirNext", + "detail::internalDirNext" + ], + "entry_point": "dirNext", + "purpose": "Finds the next entry in a directory node.", + "validation_points": [ + "Validation likely in internalDirNext (not shown here)" + ] + }, + { + "call_chain": [ + "cdirFirst", + "detail::internalDirFirst" + ], + "entry_point": "cdirFirst", + "purpose": "Const version of dirFirst for read-only views.", + "validation_points": [ + "Validation likely in internalDirFirst (not shown here)" + ] + }, + { + "call_chain": [ + "cdirNext", + "detail::internalDirNext" + ], + "entry_point": "cdirNext", + "purpose": "Const version of dirNext for read-only views.", + "validation_points": [ + "Validation likely in internalDirNext (not shown here)" + ] + } + ], + "data_flows": [ + { + "field": "root.type", + "flow": [ + "Keylet root", + "XRPL_ASSERT", + "if-check", + "used for keylet::page and keylet::child" + ], + "origin": "Keylet root parameter (forEachItem, forEachItemAfter)", + "transformations": [ + "Checked for equality with ltDIR_NODE" + ], + "validated_at": "Start of forEachItem and forEachItemAfter" + }, + { + "field": "sle (shared_ptr), ownerDir, hintDir", + "flow": [ + "view.read", + "if (!sle) return", + "used for getFieldV256(sfIndexes)" + ], + "origin": "Result of view.read(pos) or view.read(keylet::page(...))", + "transformations": [ + "Null pointer check before dereference" + ], + "validated_at": "Immediately after view.read in forEachItem, forEachItemAfter" + }, + { + "field": "after (uint256)", + "flow": [ + "Parameter", + "after.isNonZero()", + "if (after.isNonZero()) { ... }" + ], + "origin": "Parameter to forEachItemAfter", + "transformations": [ + "Checked for non-zero to determine paging logic" + ], + "validated_at": "Start of forEachItemAfter" + }, + { + "field": "sfIndexes", + "flow": [ + "sle->getFieldV256(sfIndexes)", + "for (auto const& key : ...)", + "view.read(keylet::child(key))" + ], + "origin": "Field in SLE read from ledger", + "transformations": [ + "Iterated over, each key used to read child SLE" + ], + "validated_at": "Indirectly validated by sle non-null check" + }, + { + "field": "sfIndexNext", + "flow": [ + "sle->getFieldU64(sfIndexNext)", + "if (next == 0u) return", + "keylet::page(root, next)" + ], + "origin": "Field in SLE read from ledger", + "transformations": [ + "Used to determine next page in directory" + ], + "validated_at": "Indirectly validated by sle non-null check" + } + ], + "description": "This file provides helper functions for iterating over and interacting with directory nodes in the XRPL ledger, including functions to traverse, check, and describe directory entries.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "root.type", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro and explicit if-check at forEachItem", + "issue_pattern": "Missing empty string validation for root.type", + "why_false_positive": "XRPL_ASSERT macro and explicit if-check validates root.type for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "root.type", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro and explicit if-check at forEachItemAfter", + "issue_pattern": "Missing empty string validation for root.type", + "why_false_positive": "XRPL_ASSERT macro and explicit if-check validates root.type for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle (shared_ptr), ownerDir (shared_ptr), hintDir (shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null pointer check (!sle, !ownerDir, !hintDir) at forEachItem, forEachItemAfter", + "issue_pattern": "Missing empty string validation for sle (shared_ptr), ownerDir (shared_ptr), hintDir (shared_ptr)", + "why_false_positive": "explicit null pointer check (!sle, !ownerDir, !hintDir) validates sle (shared_ptr), ownerDir (shared_ptr), hintDir (shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "after (uint256)", + "empty", + "string", + "validation" + ], + "evidence": "isNonZero() method at forEachItemAfter", + "issue_pattern": "Missing empty string validation for after (uint256)", + "why_false_positive": "isNonZero() method validates after (uint256) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/DirectoryHelpers.cpp", + "functions": [ + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 7, + "name": "dirFirst" + }, + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 17, + "name": "dirNext" + }, + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 27, + "name": "cdirFirst" + }, + { + "args": [ + "view", + "root", + "page", + "index", + "entry" + ], + "lineno": 37, + "name": "cdirNext" + }, + { + "args": [ + "view", + "root", + "f" + ], + "lineno": 47, + "name": "forEachItem" + }, + { + "args": [ + "view", + "root", + "after", + "hint", + "limit", + "f" + ], + "lineno": 71, + "name": "forEachItemAfter" + }, + { + "args": [ + "view", + "k" + ], + "lineno": 130, + "name": "dirIsEmpty" + }, + { + "args": [ + "account" + ], + "lineno": 142, + "name": "describeOwnerDir" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "These helpers are core to directory traversal in the ledger. They are likely tested indirectly via higher-level ledger and transaction tests, especially those involving trust lines, offers, or owner directories. Direct unit tests for DirectoryHelpers.cpp may exist in files like Directory_test.cpp, Ledger_test.cpp, or integration tests for ledger traversal. However, explicit tests for validation failures (e.g., wrong root.type, null SLEs, after == 0) may be sparse unless negative tests are written. The code relies on XRPL_ASSERT, which may be a no-op in release builds, so some validation paths may not be covered in production. Gaps may exist in testing edge cases for invalid input parameters and error handling.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro (custom assertion), explicit C++ checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or logs error), otherwise function returns early", + "field": "root.type", + "location": "forEachItem", + "validated_by": "XRPL_ASSERT macro and explicit if-check", + "validates": [ + "Checks that root.type equals ltDIR_NODE before proceeding" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or logs error), otherwise function returns false", + "field": "root.type", + "location": "forEachItemAfter", + "validated_by": "XRPL_ASSERT macro and explicit if-check", + "validates": [ + "Checks that root.type equals ltDIR_NODE before proceeding" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "function returns early (void or false), no exception thrown", + "field": "sle (shared_ptr), ownerDir (shared_ptr), hintDir (shared_ptr)", + "location": "forEachItem, forEachItemAfter", + "validated_by": "explicit null pointer check (!sle, !ownerDir, !hintDir)", + "validates": [ + "Ensures that the ledger entry exists before dereferencing" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (conditional logic only)", + "field": "after (uint256)", + "location": "forEachItemAfter", + "validated_by": "isNonZero() method", + "validates": [ + "Checks if 'after' is a non-zero uint256 before using as a search key" + ], + "validation_type": "format|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/DirectoryHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/DirectoryHelpers.cpp.ai.md new file mode 100644 index 0000000000..6d76a3ece3 --- /dev/null +++ b/src/libxrpl/ledger/helpers/DirectoryHelpers.cpp.ai.md @@ -0,0 +1,35 @@ +# `src/libxrpl/ledger/helpers/DirectoryHelpers.cpp` + +## Role in the System + +The XRPL ledger organizes sets of related objects — an account's owned items, all open offers at a given exchange rate — into linked-list structures called *directory nodes* (`ltDIR_NODE`). Each `DirectoryNode` SLE holds a `STVector256` field (`sfIndexes`) pointing to the keys of actual ledger objects, and a `sfIndexNext` field linking to the next page in the chain (zero means end-of-directory). `DirectoryHelpers.cpp` is the low-level traversal and utility layer that every higher-level piece of ledger code uses to walk, probe, or annotate these structures. It sits below the iterator-based `Dir` class and above the raw `ReadView`/`ApplyView` SLE access. + +## Mutable vs. Read-Only Traversal + +The most architecturally interesting design in this file is how the four legacy step-iterator functions (`dirFirst`, `dirNext`, `cdirFirst`, `cdirNext`) collapse into two template implementations. Both pairs delegate immediately to `detail::internalDirFirst` and `detail::internalDirNext`, which are defined in the header as function templates constrained by `std::is_same_v, SLE>` and `std::is_base_of_v`. Inside those templates, an `if constexpr (std::is_const_v)` branch selects between `view.read()` (returning `shared_ptr`) for the const path and `view.peek()` (returning `shared_ptr`) for the mutable path. + +This approach eliminates duplication entirely while preserving type safety. The alternative — two separate implementations for const and mutable traversal — would double the maintenance surface. The `cdirFirst`/`cdirNext` variants take `ReadView const&` and `shared_ptr&`, while `dirFirst`/`dirNext` take `ApplyView&` and `shared_ptr&`, letting callers modify pages in-place during traversal when they hold an apply-view. The header explicitly marks all four functions `@deprecated`, noting they will be replaced by a proper iterator model. New code should use the `Dir` range adaptor instead. + +`internalDirNext` is recursive: when the current page's `sfIndexes` is exhausted, it reads `sfIndexNext` from the page SLE, builds a `keylet::page(root, next)` keylet, loads the new page, resets the index to zero, and calls itself again. This means a page boundary is crossed seamlessly from the caller's perspective — the next call to `dirNext` after exhausing a page returns the first entry of the following page without any special handling at the call site. + +## Higher-Level Iterators: `forEachItem` and `forEachItemAfter` + +`forEachItem` is the simplest traversal: it walks the entire directory unconditionally, calling a `void(shared_ptr)` callback for every entry. The implementation manually follows the `sfIndexNext` chain, reading each page in turn and calling `view.read(keylet::child(key))` for every key in `sfIndexes`. It terminates when `sfIndexNext` is zero or when a page SLE is missing. There is no early exit mechanism — `forEachItem` is intended for exhaustive scans. + +`forEachItemAfter` is more sophisticated and exists primarily to serve cursor-based RPC pagination (such as `account_offers`, `account_lines`, `account_channels`). It takes three pagination controls: `after` (a `uint256` cursor key), `hint` (a `uint64` page number), and `limit` (an upper bound on entries to deliver). When `after` is non-zero, the function first attempts to exploit the hint: it constructs `keylet::page(root, hint)` and scans that page's `sfIndexes` for the cursor key. If the hint is accurate — the common case in well-behaved clients that store it from the previous response — this allows the scan to start on the correct page immediately, skipping all preceding pages. If the hint is stale or wrong, the search falls back to a linear scan from the root, scanning each page until it finds the `after` key. + +The callback for `forEachItemAfter` returns `bool`, and the `limit` parameter decrements with each invocation. When the callback returns `true` and the count hits one, iteration stops early. This dual-signal design lets the callback control early exit (e.g., if it found what it needed) independently of the limit. The return value of `forEachItemAfter` itself indicates whether the `after` key was actually found — callers use this to detect invalid cursor values. + +The asymmetry in the `after == zero` branch is worth noting: when no cursor is provided, `forEachItemAfter` starts from the root page and returns `true` unconditionally (modulo missing SLEs). When a cursor is provided, it returns `found`, which is `false` until the cursor key is located. This makes the return value meaningful for cursor validation only in the paginated case, not in a fresh start. + +## `dirIsEmpty` and the Anchor Page Subtlety + +`dirIsEmpty` checks whether a directory contains no entries by reading the root SLE and inspecting `sfIndexes`. Crucially, an empty `sfIndexes` is not sufficient to conclude the directory is empty: the root acts as an anchor page and may legitimately have an empty index array while `sfIndexNext` still points to a populated subsequent page. The comment in the implementation makes this explicit. The check therefore requires both that `sfIndexes` is empty *and* that `sfIndexNext` is zero before declaring the directory empty. Missing this subtlety would produce a false positive for "directory is empty" in an uncommon but valid ledger state. + +## `describeOwnerDir` + +`describeOwnerDir` returns a `std::function` that simply sets `sfOwner` on the SLE it receives. Its existence is driven by the `dirInsert` API, which accepts a callback to initialize new directory pages. When a new page is allocated during insertion into an owner directory, this callback brands it with the owning account ID so that the ledger entry correctly records ownership. The factory pattern keeps the account ID out of the generic insertion logic while making the caller's intent explicit at the `dirInsert` call site. + +## Validation Strategy + +Both `forEachItem` and `forEachItemAfter` enforce the precondition that `root.type == ltDIR_NODE` with a two-tier defense: an `XRPL_ASSERT` that fires in debug/instrumented builds (potentially aborting or logging) and an explicit `if` guard that silently returns in release builds. This pattern, common throughout the XRPL codebase, catches programmer errors aggressively in testing while degrading gracefully — rather than crashing — in production if the assert were ever disabled. All SLE pointer results from `view.read()` are null-checked before use, since a missing SLE simply terminates iteration rather than being treated as an error. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp.ai.json new file mode 100644 index 0000000000..ae5d805ee6 --- /dev/null +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp.ai.json @@ -0,0 +1,654 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "canAddHolding" + ], + "entry_point": "canAddHolding", + "purpose": "Validates if a holding can be added for a given MPTIssue.", + "validation_points": [ + "view.read(keylet::mptIssuance(mptID)) - Validates MPTIssuance SLE existence", + "issuance->isFlag(lsfMPTCanTransfer) - Validates lsfMPTCanTransfer flag" + ] + }, + { + "call_chain": [ + "addEmptyHolding", + "authorizeMPToken" + ], + "entry_point": "addEmptyHolding", + "purpose": "Attempts to add an empty holding for an account and MPTIssue, authorizing if needed.", + "validation_points": [ + "view.peek(keylet::mptIssuance(mptID)) - Validates MPTIssuance SLE existence", + "mpt->isFlag(lsfMPTLocked) - Validates lsfMPTLocked flag", + "view.peek(keylet::mptoken(mptID, accountID)) - Checks for duplicate holding" + ] + }, + { + "call_chain": [ + "isGlobalFrozen" + ], + "entry_point": "isGlobalFrozen", + "purpose": "Checks if the MPTIssue is globally frozen.", + "validation_points": [ + "view.read(keylet::mptIssuance(mptIssue.getMptID())) - Validates MPTIssuance SLE existence", + "sle->isFlag(lsfMPTLocked) - Validates lsfMPTLocked flag" + ] + }, + { + "call_chain": [ + "isIndividualFrozen" + ], + "entry_point": "isIndividualFrozen", + "purpose": "Checks if a specific account's holding of an MPTIssue is frozen.", + "validation_points": [ + "view.read(keylet::mptoken(mptIssue.getMptID(), account)) - Validates MPToken SLE existence", + "sle->isFlag(lsfMPTLocked) - Validates lsfMPTLocked flag" + ] + }, + { + "call_chain": [ + "isFrozen", + "isGlobalFrozen", + "isIndividualFrozen", + "isVaultPseudoAccountFrozen" + ], + "entry_point": "isFrozen", + "purpose": "Checks if an MPTIssue is frozen for an account, globally, individually, or via vault pseudo-account.", + "validation_points": [ + "isGlobalFrozen: view.read(keylet::mptIssuance(mptIssue.getMptID()))", + "isIndividualFrozen: view.read(keylet::mptoken(mptIssue.getMptID(), account))" + ] + }, + { + "call_chain": [ + "isAnyFrozen", + "isGlobalFrozen", + "isIndividualFrozen", + "isVaultPseudoAccountFrozen" + ], + "entry_point": "isAnyFrozen", + "purpose": "Checks if any of a set of accounts are frozen for an MPTIssue.", + "validation_points": [ + "isGlobalFrozen: view.read(keylet::mptIssuance(mptIssue.getMptID()))", + "isIndividualFrozen: view.read(keylet::mptoken(mptIssue.getMptID(), account))" + ] + }, + { + "call_chain": [ + "authorizeMPToken" + ], + "entry_point": "authorizeMPToken", + "purpose": "Authorizes an account to hold an MPT, with optional unauthorization logic.", + "validation_points": [ + "view.peek(keylet::account(account)) - Validates Account SLE existence" + ] + } + ], + "data_flows": [ + { + "field": "mptID", + "flow": [ + "MPTIssue.getMptID()", + "keylet::mptIssuance(mptID) or keylet::mptoken(mptID, account)", + "view.read/peek(keylet)", + "SLE pointer" + ], + "origin": "MPTIssue.getMptID()", + "transformations": [ + "Used to construct keylet for SLE lookup" + ], + "validated_at": "view.read/peek(keylet::mptIssuance(mptID))" + }, + { + "field": "lsfMPTCanTransfer", + "flow": [ + "view.read(keylet::mptIssuance(mptID))", + "issuance->isFlag(lsfMPTCanTransfer)" + ], + "origin": "MPTIssuance SLE", + "transformations": [ + "Boolean flag check" + ], + "validated_at": "issuance->isFlag(lsfMPTCanTransfer)" + }, + { + "field": "lsfMPTLocked", + "flow": [ + "view.read/peek(keylet::mptIssuance(mptID)) or view.read(keylet::mptoken(mptID, account))", + "sle->isFlag(lsfMPTLocked)" + ], + "origin": "MPTIssuance or MPToken SLE", + "transformations": [ + "Boolean flag check" + ], + "validated_at": "sle->isFlag(lsfMPTLocked)" + }, + { + "field": "sfTransferFee", + "flow": [ + "view.read(keylet::mptIssuance(issuanceID))", + "sle->getFieldU16(sfTransferFee)", + "Rate calculation" + ], + "origin": "MPTIssuance SLE", + "transformations": [ + "U16 field read, multiplied by 10,000 and added to 1,000,000,000" + ], + "validated_at": "sle && sle->isFieldPresent(sfTransferFee)" + }, + { + "field": "accountID", + "flow": [ + "addEmptyHolding(accountID, ...)", + "keylet::mptoken(mptID, accountID)", + "view.peek(keylet::mptoken(mptID, accountID))" + ], + "origin": "Function argument", + "transformations": [ + "Used to construct keylet for SLE lookup" + ], + "validated_at": "view.peek(keylet::mptoken(mptID, accountID))" + } + ], + "description": "This file provides helper functions for managing Multi-Purpose Tokens (MPTokens) in the XRPL ledger, including authorization, freezing, transfer, escrow, creation, and validation logic for MPTokens and their associated ledger entries.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "MPTIssuance SLE existence (by mptID)", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::mptIssuance(mptID)) at canAddHolding", + "issue_pattern": "Missing empty string validation for MPTIssuance SLE existence (by mptID)", + "why_false_positive": "view.read(keylet::mptIssuance(mptID)) validates MPTIssuance SLE existence (by mptID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfMPTCanTransfer flag on MPTIssuance", + "empty", + "string", + "validation" + ], + "evidence": "issuance->isFlag(lsfMPTCanTransfer) at canAddHolding", + "issue_pattern": "Missing empty string validation for lsfMPTCanTransfer flag on MPTIssuance", + "why_false_positive": "issuance->isFlag(lsfMPTCanTransfer) validates lsfMPTCanTransfer flag on MPTIssuance for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "MPTIssuance SLE existence (by mptID)", + "empty", + "string", + "validation" + ], + "evidence": "view.peek(keylet::mptIssuance(mptID)) at addEmptyHolding", + "issue_pattern": "Missing empty string validation for MPTIssuance SLE existence (by mptID)", + "why_false_positive": "view.peek(keylet::mptIssuance(mptID)) validates MPTIssuance SLE existence (by mptID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfMPTLocked flag on MPTIssuance", + "empty", + "string", + "validation" + ], + "evidence": "mpt->isFlag(lsfMPTLocked) at addEmptyHolding", + "issue_pattern": "Missing empty string validation for lsfMPTLocked flag on MPTIssuance", + "why_false_positive": "mpt->isFlag(lsfMPTLocked) validates lsfMPTLocked flag on MPTIssuance for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfMPTLocked flag on MPTIssuance", + "empty", + "string", + "validation" + ], + "evidence": "sle->isFlag(lsfMPTLocked) at isGlobalFrozen", + "issue_pattern": "Missing empty string validation for lsfMPTLocked flag on MPTIssuance", + "why_false_positive": "sle->isFlag(lsfMPTLocked) validates lsfMPTLocked flag on MPTIssuance for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfMPTLocked flag on MPTToken", + "empty", + "string", + "validation" + ], + "evidence": "sle->isFlag(lsfMPTLocked) at isIndividualFrozen", + "issue_pattern": "Missing empty string validation for lsfMPTLocked flag on MPTToken", + "why_false_positive": "sle->isFlag(lsfMPTLocked) validates lsfMPTLocked flag on MPTToken for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTransferFee field on MPTIssuance", + "empty", + "string", + "validation" + ], + "evidence": "sle->isFieldPresent(sfTransferFee) at transferRate", + "issue_pattern": "Missing empty string validation for sfTransferFee field on MPTIssuance", + "why_false_positive": "sle->isFieldPresent(sfTransferFee) validates sfTransferFee field on MPTIssuance for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "MPTIssue const& mptIssue" + ], + "lineno": 9, + "name": "isGlobalFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "MPTIssue const& mptIssue" + ], + "lineno": 16, + "name": "isIndividualFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "MPTIssue const& mptIssue", + "int depth" + ], + "lineno": 23, + "name": "isFrozen" + }, + { + "args": [ + "ReadView const& view", + "std::initializer_list const& accounts", + "MPTIssue const& mptIssue", + "int depth" + ], + "lineno": 29, + "name": "isAnyFrozen" + }, + { + "args": [ + "ReadView const& view", + "MPTID const& issuanceID" + ], + "lineno": 48, + "name": "transferRate" + }, + { + "args": [ + "ReadView const& view", + "MPTIssue const& mptIssue" + ], + "lineno": 59, + "name": "canAddHolding" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& accountID", + "XRPAmount priorBalance", + "MPTIssue const& mptIssue", + "beast::Journal journal" + ], + "lineno": 70, + "name": "addEmptyHolding" + }, + { + "args": [ + "ApplyView& view", + "XRPAmount const& priorBalance", + "MPTID const& mptIssuanceID", + "AccountID const& account", + "beast::Journal journal", + "std::uint32_t flags", + "std::optional holderID" + ], + "lineno": 87, + "name": "authorizeMPToken" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& accountID", + "MPTIssue const& mptIssue", + "beast::Journal journal" + ], + "lineno": 176, + "name": "removeEmptyHolding" + }, + { + "args": [ + "ReadView const& view", + "MPTIssue const& mptIssue", + "AccountID const& account", + "AuthType authType", + "int depth" + ], + "lineno": 200, + "name": "requireAuth" + }, + { + "args": [ + "ApplyView& view", + "MPTID const& mptIssuanceID", + "AccountID const& account", + "XRPAmount const& priorBalance", + "beast::Journal j" + ], + "lineno": 266, + "name": "enforceMPTokenAuthorization" + }, + { + "args": [ + "ReadView const& view", + "MPTIssue const& mptIssue", + "AccountID const& from", + "AccountID const& to" + ], + "lineno": 323, + "name": "canTransfer" + }, + { + "args": [ + "ReadView const& view", + "Asset const& asset" + ], + "lineno": 338, + "name": "canTrade" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& sender", + "STAmount const& amount", + "beast::Journal j" + ], + "lineno": 352, + "name": "lockEscrowMPT" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& sender", + "AccountID const& receiver", + "STAmount const& netAmount", + "STAmount const& grossAmount", + "beast::Journal j" + ], + "lineno": 406, + "name": "unlockEscrowMPT" + }, + { + "args": [ + "ApplyView& view", + "MPTID const& mptIssuanceID", + "AccountID const& account", + "std::uint32_t const flags" + ], + "lineno": 497, + "name": "createMPToken" + }, + { + "args": [ + "xrpl::ApplyView& view", + "xrpl::MPTIssue const& mptIssue", + "xrpl::AccountID const& holder", + "beast::Journal j" + ], + "lineno": 513, + "name": "checkCreateMPT" + }, + { + "args": [ + "SLE const& sleIssuance" + ], + "lineno": 534, + "name": "maxMPTAmount" + }, + { + "args": [ + "SLE const& sleIssuance" + ], + "lineno": 538, + "name": "availableMPTAmount" + }, + { + "args": [ + "ReadView const& view", + "MPTID const& mptID" + ], + "lineno": 544, + "name": "availableMPTAmount" + }, + { + "args": [ + "std::int64_t sendAmount", + "std::uint64_t outstandingAmount", + "std::int64_t maximumAmount", + "AllowMPTOverflow allowOverflow" + ], + "lineno": 551, + "name": "isMPTOverflow" + }, + { + "args": [ + "ReadView const& view", + "MPTIssue const& issue" + ], + "lineno": 560, + "name": "issuerFundsToSelfIssue" + }, + { + "args": [ + "ApplyView& view", + "MPTIssue const& issue", + "std::uint64_t amount" + ], + "lineno": 570, + "name": "issuerSelfDebitHookMPT" + }, + { + "args": [ + "ReadView const& view", + "TxType txType", + "Asset const& asset", + "AccountID const& accountID" + ], + "lineno": 576, + "name": "checkMPTAllowed" + }, + { + "args": [ + "ReadView const& view", + "TxType txType", + "Asset const& asset", + "AccountID const& accountID" + ], + "lineno": 613, + "name": "checkMPTTxAllowed" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage for these helpers is likely found in integration/functional tests for MPT (Multi-Party Token) features, such as tests for freezing, transfer, and holding logic. Look for test files named like 'test_MPTokens.cpp', 'test_MPTIssuance.cpp', or similar in the test/ or src/test/ directories. Unit tests may not exist for all helpers, especially for internal validation logic (e.g., isGlobalFrozen, isIndividualFrozen), as these are often exercised indirectly via higher-level transaction tests. Gaps may exist in negative/error path coverage (e.g., missing SLEs, locked flags), and some error returns (tefINTERNAL, tecINTERNAL) are marked as LCOV_EXCL_LINE, indicating they may not be covered by tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND", + "field": "MPTIssuance SLE existence (by mptID)", + "location": "canAddHolding", + "validated_by": "view.read(keylet::mptIssuance(mptID))", + "validates": [ + "Checks if the MPTIssuance ledger entry exists for the given mptID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_AUTH", + "field": "lsfMPTCanTransfer flag on MPTIssuance", + "location": "canAddHolding", + "validated_by": "issuance->isFlag(lsfMPTCanTransfer)", + "validates": [ + "Checks if the MPTIssuance entry has the lsfMPTCanTransfer flag set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "MPTIssuance SLE existence (by mptID)", + "location": "addEmptyHolding", + "validated_by": "view.peek(keylet::mptIssuance(mptID))", + "validates": [ + "Checks if the MPTIssuance ledger entry exists for the given mptID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "lsfMPTLocked flag on MPTIssuance", + "location": "addEmptyHolding", + "validated_by": "mpt->isFlag(lsfMPTLocked)", + "validates": [ + "Checks if the MPTIssuance entry is locked (frozen)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "lsfMPTLocked flag on MPTIssuance", + "location": "isGlobalFrozen", + "validated_by": "sle->isFlag(lsfMPTLocked)", + "validates": [ + "Checks if the global freeze flag is set on the MPTIssuance entry" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "lsfMPTLocked flag on MPTToken", + "location": "isIndividualFrozen", + "validated_by": "sle->isFlag(lsfMPTLocked)", + "validates": [ + "Checks if the individual freeze flag is set on the MPTToken entry for the account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns default rate)", + "field": "sfTransferFee field on MPTIssuance", + "location": "transferRate", + "validated_by": "sle->isFieldPresent(sfTransferFee)", + "validates": [ + "Checks if the transfer fee field is present before using it" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp.ai.md new file mode 100644 index 0000000000..8295fdf9b4 --- /dev/null +++ b/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp.ai.md @@ -0,0 +1,49 @@ +# `src/libxrpl/ledger/helpers/MPTokenHelpers.cpp` + +This file implements all MPT-specific (Multi-Purpose Token) business logic that operates directly on ledger state — freeze checking, authorization, holding lifecycle, escrow accounting, transfer permission, and MPT supply overflow safety. It is the MPT counterpart to `RippleStateHelpers.cpp`, which handles the equivalent operations for IOU trust lines. Together both files feed into the asset-agnostic dispatch layer in `TokenHelpers.cpp`, which calls them via `std::visit` on the `Asset` variant type. + +## Freeze Checking + +The freeze model has three independent tiers, checked cheapest first. `isGlobalFrozen` reads the `MPTIssuance` SLE and checks `lsfMPTLocked`; if that flag is absent, the entire issuance is unfrozen regardless of individual holdings. `isIndividualFrozen` reads the per-account `MPToken` SLE and checks the same flag at that level. Both return `false` if the SLE is absent — a missing issuance or missing token entry is treated as unfrozen, not as an error. `isFrozen` combines these two plus a third call to `isVaultPseudoAccountFrozen` (defined in `VaultHelpers`), which recursively checks whether the issuer is a vault pseudo-account and whether the vault's underlying asset is frozen. The `depth` parameter passed through the call chain protects against theoretical infinite recursion in nested vault configurations; the header comment explicitly notes this is purely defensive because the ledger does not currently permit such nesting. + +`isAnyFrozen` takes an `initializer_list` and applies the same three-tier check, but deliberately separates the two loops: global freeze is checked once (short-circuiting immediately if true), then all accounts are checked for individual freeze, and only then are all accounts checked for vault pseudo-account freeze. This ordering avoids the most expensive recursive vault check unless none of the cheaper checks triggered. + +## Transfer Rate + +`transferRate` reads the `sfTransferFee` field (a `uint16` in the range 0–50,000 representing 0–50%) and converts it to the XRPL `Rate` representation: `1,000,000,000 + (10,000 × fee)`. A 50% fee becomes `1,500,000,000`. When no `sfTransferFee` field is present, the function returns `parityRate` (exactly `1,000,000,000`, meaning no fee). This encoding aligns MPT fees with the scale used for IOU transfer rates. + +## Holding Lifecycle + +`canAddHolding` is a read-only pre-check that validates two preconditions: the `MPTIssuance` must exist, and it must have `lsfMPTCanTransfer` set. This flag means the token allows third-party holders at all; tokens without it can only be transferred directly between the issuer and its counterparties, so adding an independent holding makes no sense. + +`addEmptyHolding` delegates to `authorizeMPToken` after verifying the issuance exists, is not globally locked, and no duplicate `MPToken` entry already exists. If the account is the issuer itself, the function returns `tesSUCCESS` immediately — the issuer never holds a `MPToken` SLE for their own issuance. The reserve check follows the same rule as trust lines: reserves are only enforced when the account already owns more than two objects; new accounts get the first two items free. + +`removeEmptyHolding` is the deletion mirror. It defensively checks for the issuer case (where a token SLE should not exist at all), verifies that both `sfMPTAmount` and `sfLockedAmount` are zero, then delegates to `authorizeMPToken` with the `tfMPTUnauthorize` flag rather than duplicating the SLE removal logic. + +`createMPToken` is a lower-level primitive (called by `checkCreateMPT`) that directly inserts an `MPToken` SLE and links it into the owner directory without the reserve or issuance validity checks that `addEmptyHolding`/`authorizeMPToken` perform. `checkCreateMPT` wraps it with an idempotent "create if not exists" pattern and adjusts the owner count, making it suitable for apply-phase callers that need to auto-create a holding. + +## Authorization: The Two-Phase Split + +Authorization is split across `requireAuth` (called in preclaim — read-only) and `enforceMPTokenAuthorization` (called in apply — mutating). + +`authorizeMPToken` does the actual ledger work. The `holderID` optional parameter distinguishes which side of the authorization relationship submitted the transaction: when `nullopt`, the submitter is the holder (who is either creating or deleting their own MPToken SLE); when set, the submitter is the issuer toggling `lsfMPTAuthorized` on the holder's existing `MPToken`. This dual role in a single function avoids duplicating the SLE lookup and directory management code. + +`requireAuth` handles the `lsfMPTRequireAuth` check in preclaim. Issuers are always treated as authorized (they have no `MPToken` SLE for their own issuance). Vault pseudo-accounts and `LoanBroker` pseudo-accounts are implicitly authorized without needing an explicit `MPToken`. If the issuance carries a `sfDomainID`, authorization by credential is checked first via `credentials::validDomain`; if that passes, the function succeeds even without an `MPToken` SLE. If the domain check passes but an `MPToken` exists, the token's `lsfMPTAuthorized` flag is ignored — domain-based authorization supersedes it. + +`enforceMPTokenAuthorization` is its apply-phase counterpart. The critical difference: preclaim cannot mutate the ledger, so if a domain-authorized account lacks an `MPToken` SLE, preclaim cannot create one. The apply phase calls `enforceMPTokenAuthorization`, which handles exactly this case by calling `authorizeMPToken` to materialize the `MPToken` entry on the fly. The function is structured as a complete case analysis over the four combinations of `(authorizedByDomain, sleToken != nullptr)`, with `XRPL_ASSERT` guards on each branch to document the expected invariant. An `UNREACHABLE` sentinel with `tefINTERNAL` guards the final branch, which the case analysis proves can never be reached. + +## Transfer and Trade Permissions + +`canTransfer` enforces the `lsfMPTCanTransfer` flag with a key carve-out: when the flag is absent, transfers that directly involve the issuer (either as sender or receiver) are still permitted. This mirrors how IOU trust lines allow issuer-direct payments regardless of transfer restrictions. `canTrade` wraps `lsfMPTCanTrade` with a type-safe `asset.visit` dispatch, always returning success for plain XRP/IOU assets. + +`checkMPTAllowed` (static, internal) implements a layered permission check for DEX and payment operations: issuance must exist, must not be globally locked, must have `lsfMPTCanTrade` set, and non-issuers must additionally have `lsfMPTCanTransfer` and an unlocked individual `MPToken`. It deliberately tolerates a missing `MPToken` (returning success) because some transaction types auto-create one during apply. `checkMPTTxAllowed` is the public wrapper that asserts the transaction is not a payment (payments go through a separate path) before delegating. + +## Escrow Accounting + +`lockEscrowMPT` and `unlockEscrowMPT` manage the `sfLockedAmount` field at both the `MPToken` (holder) level and the `MPTIssuance` level. When an MPT is placed in escrow, its balance is moved from `sfMPTAmount` to `sfLockedAmount` in the holder's `MPToken` SLE, and the issuance's `sfLockedAmount` is incremented — critically, `sfOutstandingAmount` is left unchanged because the tokens are still in circulation. Every arithmetic operation is guarded by a `canSubtract` or `canAdd` check to detect underflow and overflow before mutation; these guards are marked `LCOV_EXCL_LINE` because they represent invariant violations that should be unreachable in a correct implementation. + +`unlockEscrowMPT` handles two distinct redemption paths: if the receiver is the issuer, it decrements `sfOutstandingAmount` (the tokens are retiring back to the issuer); if the receiver is a third party, it increases that party's `sfMPTAmount`. The `grossAmount`/`netAmount` split (enabled by `fixTokenEscrowV1`) represents the transfer fee taken at escrow release — the difference is removed from `sfOutstandingAmount` because those tokens effectively returned to the issuer as fee income. + +## Supply Overflow Safety + +`isMPTOverflow` centralizes the two-threshold overflow design described in the header: direct send operations check against `MaximumAmount` strictly, while payment engine paths use `UINT64_MAX` as a temporary ceiling to allow transient in-flight values. `availableMPTAmount` computes the headroom as `MaximumAmount - OutstandingAmount`, using `value_or(maxMPTokenAmount)` for the common case where no cap was specified. The view-taking overload throws `std::runtime_error` if the issuance SLE is missing — since this is called mid-computation in the payment engine, a missing issuance at that point indicates a ledger consistency failure rather than a user error. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp.ai.json new file mode 100644 index 0000000000..9413e6cf6b --- /dev/null +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp.ai.json @@ -0,0 +1,589 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "getPageForToken", + "view.peek", + "keylet::nftpage_min", + "keylet::nftpage", + "keylet::nftpage_max", + "view.succ" + ], + "entry_point": "getPageForToken", + "purpose": "Finds or creates the NFT page for a given owner and token id, splitting pages if full.", + "validation_points": [ + "keylet::nftpage_min (validates owner)", + "keylet::nftpage (validates id)", + "view.peek (validates NFT page existence)", + "narr.size() != dirMaxTokensPerPage (validates NFT page capacity)" + ] + }, + { + "call_chain": [ + "locatePage", + "keylet::nftpage_min", + "keylet::nftpage", + "keylet::nftpage_max", + "view.succ", + "view.read" + ], + "entry_point": "locatePage (ReadView)", + "purpose": "Locates the NFT page for a given owner and token id (read-only).", + "validation_points": [ + "keylet::nftpage_min (validates owner)", + "keylet::nftpage (validates id)", + "view.read (validates NFT page existence)" + ] + }, + { + "call_chain": [ + "locatePage", + "keylet::nftpage_min", + "keylet::nftpage", + "keylet::nftpage_max", + "view.succ", + "view.peek" + ], + "entry_point": "locatePage (ApplyView)", + "purpose": "Locates the NFT page for a given owner and token id (mutable view).", + "validation_points": [ + "keylet::nftpage_min (validates owner)", + "keylet::nftpage (validates id)", + "view.peek (validates NFT page existence)" + ] + } + ], + "data_flows": [ + { + "field": "owner (AccountID)", + "flow": [ + "function argument", + "keylet::nftpage_min(owner)", + "keylet::nftpage_max(owner)", + "keylet::nftpage(base, id)" + ], + "origin": "Function argument (getPageForToken, locatePage)", + "transformations": [ + "Used to compute NFT page key boundaries" + ], + "validated_at": "keylet::nftpage_min, keylet::nftpage_max" + }, + { + "field": "id (uint256)", + "flow": [ + "function argument", + "keylet::nftpage(base, id)", + "view.succ(first.key, last.key.next())" + ], + "origin": "Function argument (getPageForToken, locatePage)", + "transformations": [ + "Used to compute specific NFT page key" + ], + "validated_at": "keylet::nftpage" + }, + { + "field": "NFT page existence", + "flow": [ + "view.read/peek(Keylet)", + "cp = ...", + "if (!cp) { ... create new page ... }" + ], + "origin": "Ledger state (view)", + "transformations": [ + "Checked for existence, created if missing" + ], + "validated_at": "view.read, view.peek" + }, + { + "field": "NFT page capacity", + "flow": [ + "cp->getFieldArray(sfNFTokens)", + "narr.size()", + "if (narr.size() != dirMaxTokensPerPage)" + ], + "origin": "cp->getFieldArray(sfNFTokens)", + "transformations": [ + "Checked for max capacity, triggers page split if full" + ], + "validated_at": "narr.size() != dirMaxTokensPerPage" + } + ], + "description": "This file provides helper functions for managing NFT (Non-Fungible Token) directories, pages, and offers in the XRPL ledger. It includes logic for inserting, removing, and finding NFTs, managing NFT offer creation and deletion, repairing directory links, and checking trustline authorization and freezing.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "AccountID", + "validation", + "missing", + "check" + ], + "evidence": "Field AccountID validated by None explicit; relies on C++ type system and business logic", + "issue_pattern": "Missing validation for AccountID", + "why_false_positive": "None explicit; relies on C++ type system and business logic validates AccountID automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "uint256", + "validation", + "missing", + "check" + ], + "evidence": "Field uint256 validated by None explicit; relies on C++ type system and business logic", + "issue_pattern": "Missing validation for uint256", + "why_false_positive": "None explicit; relies on C++ type system and business logic validates uint256 automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "owner (AccountID)", + "empty", + "string", + "validation" + ], + "evidence": "keylet::nftpage_min, keylet::nftpage_max at locatePage, getPageForToken", + "issue_pattern": "Missing empty string validation for owner (AccountID)", + "why_false_positive": "keylet::nftpage_min, keylet::nftpage_max validates owner (AccountID) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "owner (AccountID)", + "type", + "validation", + "check" + ], + "evidence": "keylet::nftpage_min, keylet::nftpage_max at locatePage, getPageForToken", + "issue_pattern": "Missing type validation for owner (AccountID)", + "why_false_positive": "keylet::nftpage_min, keylet::nftpage_max validates owner (AccountID) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "id (uint256)", + "empty", + "string", + "validation" + ], + "evidence": "keylet::nftpage at locatePage, getPageForToken", + "issue_pattern": "Missing empty string validation for id (uint256)", + "why_false_positive": "keylet::nftpage validates id (uint256) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "id (uint256)", + "type", + "validation", + "check" + ], + "evidence": "keylet::nftpage at locatePage, getPageForToken", + "issue_pattern": "Missing type validation for id (uint256)", + "why_false_positive": "keylet::nftpage validates id (uint256) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "NFT page existence", + "empty", + "string", + "validation" + ], + "evidence": "view.read, view.peek at locatePage, getPageForToken", + "issue_pattern": "Missing empty string validation for NFT page existence", + "why_false_positive": "view.read, view.peek validates NFT page existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "NFT page capacity", + "empty", + "string", + "validation" + ], + "evidence": "narr.size() != dirMaxTokensPerPage at getPageForToken", + "issue_pattern": "Missing empty string validation for NFT page capacity", + "why_false_positive": "narr.size() != dirMaxTokensPerPage validates NFT page capacity for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "NFT page capacity", + "range", + "bounds", + "validation" + ], + "evidence": "narr.size() != dirMaxTokensPerPage at getPageForToken", + "issue_pattern": "Missing range validation for NFT page capacity", + "why_false_positive": "narr.size() != dirMaxTokensPerPage validates NFT page capacity range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "AccountID const& owner", + "uint256 const& id" + ], + "lineno": 10, + "name": "locatePage" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& owner", + "uint256 const& id" + ], + "lineno": 22, + "name": "locatePage" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& owner", + "uint256 const& id", + "std::function const& createCallback" + ], + "lineno": 34, + "name": "getPageForToken" + }, + { + "args": [ + "uint256 const& a", + "uint256 const& b" + ], + "lineno": 99, + "name": "compareTokens" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& owner", + "uint256 const& nftokenID", + "std::optional const& uri" + ], + "lineno": 110, + "name": "changeTokenURI" + }, + { + "args": [ + "ApplyView& view", + "AccountID owner", + "STObject&& nft" + ], + "lineno": 134, + "name": "insertToken" + }, + { + "args": [ + "ApplyView& view", + "std::shared_ptr const& p1", + "std::shared_ptr const& p2" + ], + "lineno": 163, + "name": "mergePages" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& owner", + "uint256 const& nftokenID" + ], + "lineno": 210, + "name": "removeToken" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& owner", + "uint256 const& nftokenID", + "std::shared_ptr const& curr" + ], + "lineno": 219, + "name": "removeToken" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& owner", + "uint256 const& nftokenID" + ], + "lineno": 308, + "name": "findToken" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& owner", + "uint256 const& nftokenID" + ], + "lineno": 324, + "name": "findTokenAndPage" + }, + { + "args": [ + "ApplyView& view", + "Keylet const& directory", + "std::size_t maxDeletableOffers" + ], + "lineno": 342, + "name": "removeTokenOffersWithLimit" + }, + { + "args": [ + "ReadView const& view", + "uint256 const& nftokenID" + ], + "lineno": 382, + "name": "notTooManyOffers" + }, + { + "args": [ + "ApplyView& view", + "std::shared_ptr const& offer" + ], + "lineno": 404, + "name": "deleteTokenOffer" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& owner" + ], + "lineno": 426, + "name": "repairNFTokenDirectoryLinks" + }, + { + "args": [ + "AccountID const& acctID", + "STAmount const& amount", + "std::optional const& dest", + "std::optional const& expiration", + "std::uint16_t nftFlags", + "Rules const& rules", + "std::optional const& owner", + "std::uint32_t txFlags" + ], + "lineno": 522, + "name": "tokenOfferCreatePreflight" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& acctID", + "AccountID const& nftIssuer", + "STAmount const& amount", + "std::optional const& dest", + "std::uint16_t nftFlags", + "std::uint16_t xferFee", + "beast::Journal j", + "std::optional const& owner", + "std::uint32_t txFlags" + ], + "lineno": 563, + "name": "tokenOfferCreatePreclaim" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& acctID", + "STAmount const& amount", + "std::optional const& dest", + "std::optional const& expiration", + "SeqProxy seqProxy", + "uint256 const& nftokenID", + "XRPAmount const& priorBalance", + "beast::Journal j", + "std::uint32_t txFlags" + ], + "lineno": 627, + "name": "tokenOfferCreateApply" + }, + { + "args": [ + "ReadView const& view", + "AccountID const id", + "beast::Journal const j", + "Issue const& issue" + ], + "lineno": 677, + "name": "checkTrustlineAuthorized" + }, + { + "args": [ + "ReadView const& view", + "AccountID const id", + "beast::Journal const j", + "Issue const& issue" + ], + "lineno": 713, + "name": "checkTrustlineDeepFrozen" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "nft" + } + ], + "test_coverage_notes": "The core validation logic is exercised by higher-level transaction tests, typically in files like test/ledger/NFToken_test.cpp, test/app/ledger/NFTokenHelpers_test.cpp, or integration tests for NFT mint/burn/transfer. However, direct unit tests for edge cases (e.g., invalid owner, invalid id, page overflow, page creation) may be limited or absent. Exception handling paths (e.g., what happens if view.peek fails or returns unexpected data) may not be fully covered. Tests for page splitting logic and equivalent NFT grouping should be verified for completeness.", + "validation_architecture": { + "auto_validated_fields": [ + "AccountID", + "uint256" + ], + "framework": "None explicit; relies on C++ type system and business logic", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "none (relies on AccountID type safety)", + "field": "owner (AccountID)", + "location": "locatePage, getPageForToken", + "validated_by": "keylet::nftpage_min, keylet::nftpage_max", + "validates": [ + "AccountID is a valid type for keylet functions" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (relies on uint256 type safety)", + "field": "id (uint256)", + "location": "locatePage, getPageForToken", + "validated_by": "keylet::nftpage", + "validates": [ + "id is a valid uint256 for keylet functions" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns nullptr if not found)", + "field": "NFT page existence", + "location": "locatePage, getPageForToken", + "validated_by": "view.read, view.peek", + "validates": [ + "Checks if NFT page exists in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (splits page if full)", + "field": "NFT page capacity", + "location": "getPageForToken", + "validated_by": "narr.size() != dirMaxTokensPerPage", + "validates": [ + "NFT page does not exceed max tokens per page" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp.ai.md new file mode 100644 index 0000000000..36f8c5eb65 --- /dev/null +++ b/src/libxrpl/ledger/helpers/NFTokenHelpers.cpp.ai.md @@ -0,0 +1,59 @@ +# `NFTokenHelpers.cpp` — NFT Directory and Offer Management + +## Purpose and Place in the System + +This file is the core implementation layer for all NFT data-structure operations in the XRP Ledger. Every transaction that touches an NFToken — minting, burning, transferring, or creating/cancelling offers — ultimately calls into these helpers rather than manipulating ledger state directly. The file lives in `libxrpl/ledger/helpers/` alongside analogous helpers for regular directories and RippleState objects, a separation that keeps the NFT-specific page structure logic out of individual transactor files. + +## The NFT Page Data Structure + +NFTs are not stored as individual ledger objects. An account's entire NFT portfolio is packed into a doubly-linked list of `ltNFTOKEN_PAGE` SLEs, each holding up to `dirMaxTokensPerPage` (32) tokens as an `STArray`. The chain is anchored by a deterministic "max" page whose key is derived from `keylet::nftpage_max(owner)`. This page always acts as the tail; all real pages have keys less than it. + +Pages are keyed using a combination of the owner's `AccountID` and the low 96 bits of an NFToken ID, which are exposed through `nft::pageMask`. These low 96 bits encode the issuer and taxon — tokens that share the same masked value are considered **equivalent** and must be collocated on the same page. The sort comparator `compareTokens()` reflects this: it sorts on the low 96 bits first, then on the full ID as a tiebreaker, creating a stable total order. + +The page key invariant is that the low 96 bits of every NFToken stored in a page must be **strictly less than** the low 96 bits of the page's own key. This is why page keys are chosen one higher than the largest token they contain (using `uint256::next()`). + +## Page Location: `locatePage` + +The file provides two overloads — one taking `ReadView const&` (returning `shared_ptr`) and one taking `ApplyView&` (returning mutable `shared_ptr`). Both use the same strategy: compute the theoretical minimum page key for the token using `keylet::nftpage(keylet::nftpage_min(owner), id)`, then call `view.succ()` to find the first *actual* page key that is strictly greater than it and within the owner's range. If no such key exists, `succ()` returns `nullopt` and the code falls back to the max-page key, which will either be an existing page or produce a `nullptr` from `view.read/peek`. + +The choice to use `view.succ()` rather than scanning the linked list is critical for performance: it leverages the ledger's sorted B-tree structure to find the candidate page in O(log N) time rather than walking every page in the chain. + +## Page Creation and Splitting: `getPageForToken` + +`getPageForToken` is the mutable counterpart that also creates pages on demand. When no suitable page exists, it creates a new SLE at `keylet::nftpage_max(owner)` and invokes a `createCallback` (used by callers to increment the owner reserve count). When a suitable page is found but is already full, it must split. + +The splitting algorithm is the most complex logic in the file. Rather than splitting exactly at the midpoint, it must find a split point at an equivalent-group boundary. It starts at the midpoint and advances forward past any run of equivalent tokens. If the entire back half is equivalent, it searches from the front of the page instead. Two edge cases require special handling: + +- If `splitIter == narr.end()` after both searches, the page is entirely one equivalence class and the token cannot be placed — the function returns `nullptr`. +- If `splitIter == narr.begin()`, the entire page is one class, but the incoming token is a *different* class. The split is decided by comparing the new token's masked value to the page's: if the new token sorts higher, an empty `carr` is created and the new token will go into it; if lower, all content moves to `carr` and the new token occupies the otherwise-empty `narr` side. + +After splitting, the new page's key is set to `narr.back().next()` if `narr` is still full, or to `carr.front()` otherwise — preserving the invariant. The doubly-linked list pointers (`sfPreviousPageMin`, `sfNextPageMin`) are updated on up to three pages (new, existing, and predecessor), and the `createCallback` fires once to account for the extra reserved page. + +## Token Insertion and Removal + +`insertToken()` calls `getPageForToken()` and then inserts the `STObject` into the target page's array, keeping the array sorted via `compareTokens`. It returns `tecNO_SUITABLE_NFTOKEN_PAGE` if the page lookup returned `nullptr` (the equivalence-group-full edge case). + +`removeToken()` has two overloads — one discovers the page via `locatePage`, the other accepts a known page (used by `NFTokenBurn` which has already loaded the token). After erasing the entry from the array: + +- If the page is **non-empty**, it is updated, then the code attempts to merge it with both its predecessor and successor. This can reduce three pages to one; the owner count is decremented accordingly. +- If the page is **empty** and has a predecessor: under the `fixNFTokenPageLinks` amendment, and if the current page is the chain's tail (key ends with `pageMask`), the predecessor's content is moved into the tail page and the predecessor is erased. This preserves the invariant that the last page always sits at `keylet::nftpage_max`. Without the amendment, the empty page is simply unlinked and erased normally. + +The `mergePages()` helper enforces invariants by throwing `std::runtime_error` for out-of-order or link-broken pages before merging — these represent corrupted ledger state and should never occur in practice. + +## Offer Management + +`insertToken`/`removeToken` cover the NFT itself, but each NFToken can also have buy and sell offer queues. `deleteTokenOffer()` atomically removes a single offer from both the owner's owner-directory and the token's buy/sell directory, then decrements the owner count. `removeTokenOffersWithLimit()` iterates a directory page-by-page in reverse (to avoid invalidating iterators) and calls `deleteTokenOffer` up to a caller-supplied limit. It reads the next-page index before modifying the current page, which is necessary because a fully drained page is itself deleted. + +`notTooManyOffers()` is a burn guard: it counts all open offers across both buy and sell directories and returns `tefTOO_BIG` if the total exceeds `maxDeletableTokenOfferEntries` (500). This prevents a token with a pathologically large offer book from becoming impossible to burn. + +## Offer Creation: Shared Transaction Logic + +The three `tokenOfferCreate*` functions are shared between `NFTokenCreateOffer` and `NFTokenMint` (the latter supports an inline sell-offer at mint time). `tokenOfferCreatePreflight` performs stateless structural validation — negative amounts, zero IOU amounts, sell-offer-without-an-owner, destination equals account. `tokenOfferCreatePreclaim` handles stateful checks against the ledger: NFT issuer trust-line presence for royalty collection, transferability flags (with a minter-exception for the `sfNFTokenMinter` field), IOU frozen checks, destination/owner account existence and `lsfDisallowIncomingNFTokenOffer` flags, and (under `fixEnforceNFTokenTrustlineV2`) trust-line authorization. `tokenOfferCreateApply` inserts the offer SLE into both the owner directory and the token's buy/sell directory, then increments the owner count. + +## Directory Repair: `repairNFTokenDirectoryLinks` + +`repairNFTokenDirectoryLinks` is a defensive repair function introduced alongside the `fixNFTokenPageLinks` amendment to fix corrupted link fields. It walks the entire page chain for an account by repeatedly calling `view.succ()` from the current page key, comparing expected vs actual `sfNextPageMin`/`sfPreviousPageMin` values. If the discovered last page does not equal `keylet::nftpage_max(owner)` (which can happen due to the historical bug), it creates a new page at the correct key, copies all token data and backlinks into it, erases the imposter page, and returns `true`. The repair never changes the owner count since the page count is unchanged. + +## Trust-line Guards + +`checkTrustlineAuthorized` and `checkTrustlineDeepFrozen` are IOU-side guards for non-XRP offer amounts. Both are no-ops for XRP (asserted). `checkTrustlineAuthorized` checks that if the currency issuer has `lsfRequireAuth` set, the buyer's trust-line carries the appropriate `lsfLowAuth`/`lsfHighAuth` bit. `checkTrustlineDeepFrozen` checks for the `lsfLowDeepFreeze`/`lsfHighDeepFreeze` bits regardless of which side applied them. Issuers are always exempt from both checks since they cannot hold a trust-line to themselves. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/OfferHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/OfferHelpers.cpp.ai.json new file mode 100644 index 0000000000..a77643cc03 --- /dev/null +++ b/src/libxrpl/ledger/helpers/OfferHelpers.cpp.ai.json @@ -0,0 +1,470 @@ +{ + "args": [ + { + "lineno": 8, + "name": "view" + }, + { + "lineno": 8, + "name": "sle" + }, + { + "lineno": 8, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "offerDelete" + ], + "entry_point": "offerDelete", + "purpose": "Deletes an offer from the ledger, removing it from owner and book directories, handling hybrid offers, and adjusting owner count.", + "validation_points": [ + "if (!sle) return tesSUCCESS;", + "if (!view.dirRemove(keylet::ownerDir(owner), ...)) return tefBAD_LEDGER;", + "if (!view.dirRemove(keylet::page(uDirectory), ...)) return tefBAD_LEDGER;", + "XRPL_ASSERT(sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID), ...)", + "if (!view.dirRemove(keylet::page(dirIndex), ...)) return tefBAD_LEDGER;" + ] + } + ], + "data_flows": [ + { + "field": "sle (shared_ptr)", + "flow": [ + "offerDelete parameter", + "validated by if (!sle)", + "used to extract offerIndex, owner, uDirectory, and fields" + ], + "origin": "Input parameter to offerDelete", + "transformations": [ + "Checked for null", + "Fields extracted for further processing" + ], + "validated_at": "if (!sle)" + }, + { + "field": "sfOwnerNode", + "flow": [ + "sle->getFieldU64(sfOwnerNode)", + "passed to view.dirRemove for owner directory" + ], + "origin": "Field in sle", + "transformations": [ + "Extracted as uint64" + ], + "validated_at": "Implicitly validated by dirRemove return value" + }, + { + "field": "sfBookDirectory", + "flow": [ + "sle->getFieldH256(sfBookDirectory)", + "used as uDirectory for book directory removal" + ], + "origin": "Field in sle", + "transformations": [ + "Extracted as uint256" + ], + "validated_at": "Implicitly validated by dirRemove return value" + }, + { + "field": "sfBookNode", + "flow": [ + "sle->getFieldU64(sfBookNode)", + "passed to view.dirRemove for book directory" + ], + "origin": "Field in sle", + "transformations": [ + "Extracted as uint64" + ], + "validated_at": "Implicitly validated by dirRemove return value" + }, + { + "field": "sfAdditionalBooks", + "flow": [ + "sle->isFieldPresent(sfAdditionalBooks)", + "if present, getFieldArray(sfAdditionalBooks)", + "iterate over array, extract sfBookDirectory and sfBookNode" + ], + "origin": "Field in sle", + "transformations": [ + "Checked for presence", + "Array extracted and iterated" + ], + "validated_at": "XRPL_ASSERT(sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID), ...)" + }, + { + "field": "sfDomainID", + "flow": [ + "sle->isFieldPresent(sfDomainID)", + "checked in XRPL_ASSERT for hybrid offers" + ], + "origin": "Field in sle", + "transformations": [ + "Checked for presence" + ], + "validated_at": "XRPL_ASSERT(sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID), ...)" + }, + { + "field": "lsfHybrid", + "flow": [ + "sle->isFlag(lsfHybrid)", + "checked in XRPL_ASSERT for hybrid offers" + ], + "origin": "Flag in sle", + "transformations": [ + "Checked for flag presence" + ], + "validated_at": "XRPL_ASSERT(sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID), ...)" + }, + { + "field": "offerIndex", + "flow": [ + "extracted from sle", + "passed to all dirRemove calls" + ], + "origin": "sle->key()", + "transformations": [ + "None" + ], + "validated_at": "Used in dirRemove, which returns error if not found" + }, + { + "field": "owner", + "flow": [ + "extracted from sle", + "used for ownerDir keylet and adjustOwnerCount" + ], + "origin": "sle->getAccountID(sfAccount)", + "transformations": [ + "None" + ], + "validated_at": "Not directly validated, but used in directory operations" + } + ], + "description": "Provides helper functions for deleting offers from the XRPL ledger, handling directory removals and owner count adjustments.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount (type: AccountID, via getAccountID)", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccount (type: AccountID, via getAccountID) validated by Custom business logic (XRPL core, no external validation framework)", + "issue_pattern": "Missing validation for sfAccount (type: AccountID, via getAccountID)", + "why_false_positive": "Custom business logic (XRPL core, no external validation framework) validates sfAccount (type: AccountID, via getAccountID) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfBookDirectory (type: uint256, via getFieldH256)", + "validation", + "missing", + "check" + ], + "evidence": "Field sfBookDirectory (type: uint256, via getFieldH256) validated by Custom business logic (XRPL core, no external validation framework)", + "issue_pattern": "Missing validation for sfBookDirectory (type: uint256, via getFieldH256)", + "why_false_positive": "Custom business logic (XRPL core, no external validation framework) validates sfBookDirectory (type: uint256, via getFieldH256) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfOwnerNode (type: uint64, via getFieldU64)", + "validation", + "missing", + "check" + ], + "evidence": "Field sfOwnerNode (type: uint64, via getFieldU64) validated by Custom business logic (XRPL core, no external validation framework)", + "issue_pattern": "Missing validation for sfOwnerNode (type: uint64, via getFieldU64)", + "why_false_positive": "Custom business logic (XRPL core, no external validation framework) validates sfOwnerNode (type: uint64, via getFieldU64) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfBookNode (type: uint64, via getFieldU64)", + "validation", + "missing", + "check" + ], + "evidence": "Field sfBookNode (type: uint64, via getFieldU64) validated by Custom business logic (XRPL core, no external validation framework)", + "issue_pattern": "Missing validation for sfBookNode (type: uint64, via getFieldU64)", + "why_false_positive": "Custom business logic (XRPL core, no external validation framework) validates sfBookNode (type: uint64, via getFieldU64) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAdditionalBooks (type: STArray, via getFieldArray)", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAdditionalBooks (type: STArray, via getFieldArray) validated by Custom business logic (XRPL core, no external validation framework)", + "issue_pattern": "Missing validation for sfAdditionalBooks (type: STArray, via getFieldArray)", + "why_false_positive": "Custom business logic (XRPL core, no external validation framework) validates sfAdditionalBooks (type: STArray, via getFieldArray) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDomainID (type: present/absent, via isFieldPresent)", + "validation", + "missing", + "check" + ], + "evidence": "Field sfDomainID (type: present/absent, via isFieldPresent) validated by Custom business logic (XRPL core, no external validation framework)", + "issue_pattern": "Missing validation for sfDomainID (type: present/absent, via isFieldPresent)", + "why_false_positive": "Custom business logic (XRPL core, no external validation framework) validates sfDomainID (type: present/absent, via isFieldPresent) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle (shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "if (!sle) at offerDelete", + "issue_pattern": "Missing empty string validation for sle (shared_ptr)", + "why_false_positive": "if (!sle) validates sle (shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "owner directory removal", + "empty", + "string", + "validation" + ], + "evidence": "view.dirRemove(keylet::ownerDir(owner), sle->getFieldU64(sfOwnerNode), offerIndex, false) at offerDelete", + "issue_pattern": "Missing empty string validation for owner directory removal", + "why_false_positive": "view.dirRemove(keylet::ownerDir(owner), sle->getFieldU64(sfOwnerNode), offerIndex, false) validates owner directory removal for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "book directory removal", + "empty", + "string", + "validation" + ], + "evidence": "view.dirRemove(keylet::page(uDirectory), sle->getFieldU64(sfBookNode), offerIndex, false) at offerDelete", + "issue_pattern": "Missing empty string validation for book directory removal", + "why_false_positive": "view.dirRemove(keylet::page(uDirectory), sle->getFieldU64(sfBookNode), offerIndex, false) validates book directory removal for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hybrid domain offer structure", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID), ...) at offerDelete", + "issue_pattern": "Missing empty string validation for hybrid domain offer structure", + "why_false_positive": "XRPL_ASSERT(sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID), ...) validates hybrid domain offer structure for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "additional book directory removal", + "empty", + "string", + "validation" + ], + "evidence": "view.dirRemove(keylet::page(dirIndex), dirNode, offerIndex, false) at offerDelete (inside for loop over sfAdditionalBooks)", + "issue_pattern": "Missing empty string validation for additional book directory removal", + "why_false_positive": "view.dirRemove(keylet::page(dirIndex), dirNode, offerIndex, false) validates additional book directory removal for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/OfferHelpers.cpp", + "functions": [ + { + "args": [ + "view", + "sle", + "j" + ], + "lineno": 8, + "name": "offerDelete" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The function is likely tested indirectly via offer cancellation, offer expiration, and ledger mutation tests. Look for tests in files such as 'Offer.test.cpp', 'LedgerView.test.cpp', or integration tests that exercise offer creation and deletion. The LCOV_EXCL_LINE comments indicate that error paths (dirRemove failures) are not covered by standard tests. Hybrid domain offer logic (XRPL_ASSERT) may not be fully covered unless there are explicit hybrid offer tests. Null sle input is likely covered by basic offer deletion tests. AdditionalBooks logic may be under-tested if hybrid offers are rare in test suites.", + "validation_architecture": { + "auto_validated_fields": [ + "sfAccount (type: AccountID, via getAccountID)", + "sfBookDirectory (type: uint256, via getFieldH256)", + "sfOwnerNode (type: uint64, via getFieldU64)", + "sfBookNode (type: uint64, via getFieldU64)", + "sfAdditionalBooks (type: STArray, via getFieldArray)", + "sfDomainID (type: present/absent, via isFieldPresent)" + ], + "framework": "Custom business logic (XRPL core, no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns tesSUCCESS (early exit)", + "field": "sle (shared_ptr)", + "location": "offerDelete", + "validated_by": "if (!sle)", + "validates": [ + "checks if the pointer to the offer SLE is non-null before proceeding" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns tefBAD_LEDGER", + "field": "owner directory removal", + "location": "offerDelete", + "validated_by": "view.dirRemove(keylet::ownerDir(owner), sle->getFieldU64(sfOwnerNode), offerIndex, false)", + "validates": [ + "checks if the offer can be removed from the owner's directory" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns tefBAD_LEDGER", + "field": "book directory removal", + "location": "offerDelete", + "validated_by": "view.dirRemove(keylet::page(uDirectory), sle->getFieldU64(sfBookNode), offerIndex, false)", + "validates": [ + "checks if the offer can be removed from the book directory" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts in debug, may throw in release)", + "field": "hybrid domain offer structure", + "location": "offerDelete", + "validated_by": "XRPL_ASSERT(sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID), ...)", + "validates": [ + "if sfAdditionalBooks is present, ensures lsfHybrid flag is set", + "if sfAdditionalBooks is present, ensures sfDomainID field is present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns tefBAD_LEDGER", + "field": "additional book directory removal", + "location": "offerDelete (inside for loop over sfAdditionalBooks)", + "validated_by": "view.dirRemove(keylet::page(dirIndex), dirNode, offerIndex, false)", + "validates": [ + "checks if the offer can be removed from each additional book directory" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/OfferHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/OfferHelpers.cpp.ai.md new file mode 100644 index 0000000000..58ebf48000 --- /dev/null +++ b/src/libxrpl/ledger/helpers/OfferHelpers.cpp.ai.md @@ -0,0 +1,29 @@ +# `OfferHelpers.cpp` — Offer Deletion from the XRPL Ledger + +This file provides the single utility function `offerDelete`, which performs the complete and atomic removal of an offer ledger object from an `ApplyView`. It exists as a shared helper because offer deletion is not a self-contained operation — every offer is simultaneously indexed in multiple directory structures, and all of those registrations must be unwound together before the object itself can be erased. + +## Why Deletion Requires a Helper + +In the XRPL ledger model, an offer (`ltOFFER` SLE) is not a standalone object. When an offer is created, its key is inserted into two separate directories: the owning account's personal owner directory (used to enumerate all objects an account holds, and to enforce reserve requirements), and the order book directory corresponding to the offer's currency pair (used by the DEX matching engine to locate counterparty offers). Each insertion records a position hint — `sfOwnerNode` and `sfBookNode` respectively — so removal can be done in O(1) without scanning the entire directory. + +`offerDelete` encapsulates the complete removal sequence in the correct order: directories first, owner count adjustment next, and the object erasure last. Inverting this order would leave dangling directory entries or produce an incorrect reserve calculation. + +## Hybrid Domain Offers + +The most architecturally significant branch in this function handles the Permissioned DEX feature, where an offer can be classified as *hybrid*. A hybrid offer carries the `lsfHybrid` flag, a `sfDomainID` identifying which permissioned domain it belongs to, and a `sfAdditionalBooks` array containing extra book-directory registrations. + +The motivation is that a hybrid offer participates in both the permissioned domain's book *and* the global open order book simultaneously, giving it exposure to both matching pools. Consequently, removing a hybrid offer requires iterating over `sfAdditionalBooks` and calling `view.dirRemove` for each additional registration, beyond the two standard removals. The `XRPL_ASSERT` before this loop enforces the ledger-level invariant: if `sfAdditionalBooks` is present, the offer must also carry `lsfHybrid` and `sfDomainID`. Any violation signals ledger corruption or a programming error, not a recoverable runtime condition. + +## Error Handling and the Null Guard + +The null pointer guard at the top of the function (`if (!sle) return tesSUCCESS`) is deliberately lenient. The header contract states the offer must exist, but several callers — notably `OfferCreate.cpp`'s crossing loop and `BookTip.cpp`'s streaming logic — call `offerDelete` in contexts where the offer may have already been removed by a prior step. Returning `tesSUCCESS` rather than an error allows those callers to remain unconditional without requiring pre-checks. + +All `dirRemove` failures return `tefBAD_LEDGER`, indicating a structural inconsistency in the ledger state. These paths are annotated `LCOV_EXCL_LINE` — they represent situations that should be impossible in a correctly operating system (the offer's directory position was verified to exist when the offer was created), so they are not reachable in normal tests. They serve as defensive guards against ledger corruption detected late. + +## Owner Count and Reserve + +After all directory registrations are removed, `adjustOwnerCount` is called with a delta of `-1`. This function (from `AccountRootHelpers`) reads the current `sfOwnerCount` on the account SLE, clamps any underflow to zero while logging a fatal error, and writes the updated count back. The owner count directly determines the XRP reserve requirement for the account — each owned ledger object adds one incremental reserve. Decrementing it only *after* directory removal ensures there is never a window where the ledger shows the offer as gone from the book but still imposing a reserve. + +## Callers + +`offerDelete` is called from at least four sites: `OfferCancel::doApply` (explicit user cancellation), `OfferCreate` during offer crossing and prior-offer cancellation, `BookTip`'s offer streaming path when it encounters expired or unfunded offers, and `AccountDelete` when cleaning up an account's remaining offers. The function's signature takes an `ApplyView&`, so all callers operate within a transaction-scoped view that can be rolled back; the deletions are not committed to the ledger until the enclosing transaction succeeds. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp.ai.json new file mode 100644 index 0000000000..a05f5a3a11 --- /dev/null +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp.ai.json @@ -0,0 +1,315 @@ +{ + "args": [ + { + "lineno": 9, + "name": "slep" + }, + { + "lineno": 10, + "name": "view" + }, + { + "lineno": 11, + "name": "key" + }, + { + "lineno": 12, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "closeChannel" + ], + "entry_point": "closeChannel", + "purpose": "Closes a payment channel, removes it from owner directories, returns remaining funds to source, decrements owner count, and erases the channel from the ledger.", + "validation_points": [ + "view.dirRemove(keylet::ownerDir(src), page, key, true) // Validates src owner directory removal", + "view.dirRemove(keylet::ownerDir(dst), *page, key, true) // Validates dst owner directory removal", + "view.peek(keylet::account(src)) // Validates src account existence", + "XRPL_ASSERT((*slep)[sfAmount] >= (*slep)[sfBalance], ...) // Validates channel amount >= balance" + ] + } + ], + "data_flows": [ + { + "field": "sfAccount (src)", + "flow": [ + "(*slep)[sfAccount]", + "src", + "keylet::ownerDir(src)", + "view.dirRemove(...)", + "keylet::account(src)", + "view.peek(...)" + ], + "origin": "(*slep)[sfAccount] // From PaymentChannel SLE", + "transformations": [ + "Used as key for owner directory and account lookup" + ], + "validated_at": "view.dirRemove (owner directory removal), view.peek (account existence)" + }, + { + "field": "sfOwnerNode (page)", + "flow": [ + "(*slep)[sfOwnerNode]", + "page", + "view.dirRemove(keylet::ownerDir(src), page, key, true)" + ], + "origin": "(*slep)[sfOwnerNode] // From PaymentChannel SLE", + "transformations": [ + "Used as page index for directory removal" + ], + "validated_at": "view.dirRemove (owner directory removal)" + }, + { + "field": "sfDestinationNode (page)", + "flow": [ + "(*slep)[~sfDestinationNode]", + "page", + "view.dirRemove(keylet::ownerDir(dst), *page, key, true)" + ], + "origin": "(*slep)[~sfDestinationNode] // Optional field from PaymentChannel SLE", + "transformations": [ + "Used as page index for destination directory removal" + ], + "validated_at": "view.dirRemove (destination owner directory removal)" + }, + { + "field": "sfDestination (dst)", + "flow": [ + "(*slep)[sfDestination]", + "dst", + "keylet::ownerDir(dst)", + "view.dirRemove(...)" + ], + "origin": "(*slep)[sfDestination] // From PaymentChannel SLE", + "transformations": [ + "Used as key for destination owner directory" + ], + "validated_at": "view.dirRemove (destination owner directory removal)" + }, + { + "field": "sfAmount", + "flow": [ + "(*slep)[sfAmount]", + "XRPL_ASSERT((*slep)[sfAmount] >= (*slep)[sfBalance])", + "(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]" + ], + "origin": "(*slep)[sfAmount] // From PaymentChannel SLE", + "transformations": [ + "Checked against sfBalance for invariant", + "Used to compute new balance for src account" + ], + "validated_at": "XRPL_ASSERT" + }, + { + "field": "sfBalance", + "flow": [ + "(*slep)[sfBalance]", + "XRPL_ASSERT((*slep)[sfAmount] >= (*slep)[sfBalance])", + "(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance]" + ], + "origin": "(*slep)[sfBalance] // From PaymentChannel SLE", + "transformations": [ + "Checked against sfAmount for invariant", + "Used to compute new balance for src account" + ], + "validated_at": "XRPL_ASSERT" + } + ], + "description": "Implements the closeChannel function to close a payment channel in the XRPL ledger, handling directory removals, balance adjustments, and ledger updates.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "src owner directory removal", + "empty", + "string", + "validation" + ], + "evidence": "view.dirRemove at closeChannel", + "issue_pattern": "Missing empty string validation for src owner directory removal", + "why_false_positive": "view.dirRemove validates src owner directory removal for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "dst owner directory removal", + "empty", + "string", + "validation" + ], + "evidence": "view.dirRemove at closeChannel", + "issue_pattern": "Missing empty string validation for dst owner directory removal", + "why_false_positive": "view.dirRemove validates dst owner directory removal for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "src account existence", + "empty", + "string", + "validation" + ], + "evidence": "view.peek at closeChannel", + "issue_pattern": "Missing empty string validation for src account existence", + "why_false_positive": "view.peek validates src account existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "channel amount >= channel balance", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at closeChannel", + "issue_pattern": "Missing empty string validation for channel amount >= channel balance", + "why_false_positive": "XRPL_ASSERT validates channel amount >= channel balance for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp", + "functions": [ + { + "args": [ + "slep", + "view", + "key", + "j" + ], + "lineno": 8, + "name": "closeChannel" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The function closeChannel is likely tested indirectly via higher-level transaction tests for payment channel closure (e.g., PaymentChannelClaim, PaymentChannelClose). Tests should cover: successful closure, failure to remove from owner directories (src/dst), missing src account, and invariant violation (amount < balance). However, the LCOV_EXCL_* comments indicate that some error paths (e.g., directory removal failures, missing account) are not covered by standard tests. Test files to check include: PaymentChannel_test.cpp, PaymentChannelClose_test.cpp, and possibly integration/ledger/PaymentChannel*.json. Gaps: Directory removal failures and assertion failures may not be directly tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic, XRPL_ASSERT macro, ApplyView interface", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "src owner directory removal", + "location": "closeChannel", + "validated_by": "view.dirRemove", + "validates": [ + "Checks if the payment channel can be removed from the source account's owner directory" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "dst owner directory removal", + "location": "closeChannel", + "validated_by": "view.dirRemove", + "validates": [ + "Checks if the payment channel can be removed from the destination account's owner directory (if present)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "src account existence", + "location": "closeChannel", + "validated_by": "view.peek", + "validates": [ + "Checks if the source account exists in the ledger before updating balance" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts in debug)", + "field": "channel amount >= channel balance", + "location": "closeChannel", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures the channel's total amount is not less than its balance" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp.ai.md new file mode 100644 index 0000000000..653569882d --- /dev/null +++ b/src/libxrpl/ledger/helpers/PaymentChannelHelpers.cpp.ai.md @@ -0,0 +1,31 @@ +# `PaymentChannelHelpers.cpp` — Payment Channel Teardown Logic + +This file implements a single exported function, `closeChannel`, which serves as the shared teardown routine for XRPL payment channels. It is called from multiple payment-channel transactors (`PaymentChannelClaim` and `PaymentChannelFund`) whenever a channel must be removed from the ledger — whether due to explicit closure, expiration via `cancelAfter`, or a negotiated `expiration` timestamp reaching the current ledger close time. + +## Role in the Ledger Helpers Pattern + +The `src/libxrpl/ledger/helpers/` directory organizes reusable ledger-mutation logic by object type, keeping it separate from transaction-layer code. `PaymentChannelHelpers.cpp` follows the same pattern as `OfferHelpers.cpp` and `NFTokenHelpers.cpp`: transactors import focused helper functions rather than re-implementing multi-step mutations inline. This keeps each transactor's `doApply()` path focused on policy decisions while delegating structural ledger operations here. + +## What `closeChannel` Does + +`closeChannel` receives the payment channel's `SLE` (`slep`), the mutable `ApplyView`, its ledger `key` (a `uint256`), and a `beast::Journal` for diagnostics. It performs four ordered mutations and returns a `TER` result code: + +**1. Remove from source owner directory.** The channel's `sfOwnerNode` records which directory page holds its entry in the source account's owner directory. `view.dirRemove` locates and removes it. Failure here returns `tefBAD_LEDGER` — this indicates structural ledger corruption that cannot be recovered from, which is why the failure path is marked `LCOV_EXCL_START` (it is unreachable under correct ledger operation and deliberately excluded from coverage requirements). + +**2. Conditionally remove from destination owner directory.** The `sfDestinationNode` field is accessed via the tilde operator (`~`) as an optional field. Older payment channels created before the feature that added destination-side owner tracking may lack this field entirely. The conditional check `if (auto const page = (*slep)[~sfDestinationNode])` therefore serves dual purposes: it guards against older channels that have no destination directory entry, and it handles the case where the channel has no destination at all. Failure returns `tefBAD_LEDGER` for the same reasons as above. + +**3. Return unspent funds to the source account.** This is the key financial mutation. The channel holds two amounts: `sfAmount` (the total deposited into the channel) and `sfBalance` (the portion already claimed by the recipient). The unspent remainder is `sfAmount - sfBalance`. The source account's XRP balance is updated as: + +``` +(*sle)[sfBalance] = (*sle)[sfBalance] + (*slep)[sfAmount] - (*slep)[sfBalance] +``` + +An `XRPL_ASSERT` immediately before this line enforces the invariant `sfAmount >= sfBalance`. This invariant is upheld by all transaction logic that modifies channel state — claim amounts are capped at `sfAmount`, so the channel's running balance can never exceed the deposited total. Using `XRPL_ASSERT` rather than a guarded return signals that a violation here represents an impossible state under correct ledger rules, not a recoverable user error. After adjusting the balance, `adjustOwnerCount` (from `AccountRootHelpers`) decrements the source account's owner count by one, reflecting that the channel no longer counts against its reserve. + +**4. Erase the channel SLE.** `view.erase(slep)` removes the channel object itself from the ledger state. This must come last — the SLE is still read for `sfAmount` and `sfBalance` during step 3, and erasing it before that would leave the refund calculation without its inputs. + +## Call Sites and Context + +In `PaymentChannelClaim::doApply()`, `closeChannel` is invoked in two distinct situations. First, immediately upon discovering that the channel's `cancelAfter` or `expiration` has passed — in this case the entire claim is converted into a pure close, regardless of any balance or signature fields in the transaction. Second, when the `tfClose` flag is set and either the recipient is closing or the channel is fully drained (`sfBalance == sfAmount`); in those cases closure is immediate rather than deferred. In `PaymentChannelFund`, it handles the expired-channel fast path similarly, preventing anyone from adding funds to an already-expired channel while still cleaning up the object. + +The function intentionally holds no logic about *when* to close a channel — that judgment belongs to the transactors. `closeChannel` only knows *how* to perform the closure atomically within a given `ApplyView`. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp.ai.json new file mode 100644 index 0000000000..6f09d19b46 --- /dev/null +++ b/src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp.ai.json @@ -0,0 +1,442 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "offerInDomain", + "accountInDomain" + ], + "entry_point": "offerInDomain", + "purpose": "Validates if an offer belongs to a given permissioned domain, including offer structure and account credential checks.", + "validation_points": [ + "offerInDomain: sleOffer existence (view.read(keylet::offer(offerID)))", + "offerInDomain: sfDomainID presence and match", + "offerInDomain: lsfHybrid flag and sfAdditionalBooks presence", + "accountInDomain: sleDomain existence (view.read(keylet::permissionedDomain(domainID)))", + "accountInDomain: domain owner check (sleDomain->getAccountID(sfOwner) == account)", + "accountInDomain: credential existence (view.read(keylet::credential(...)))", + "accountInDomain: credential accepted flag (!sleCred->isFlag(lsfAccepted))", + "accountInDomain: credential expiration (!credentials::checkExpired(...))" + ] + }, + { + "call_chain": [ + "accountInDomain" + ], + "entry_point": "accountInDomain", + "purpose": "Checks if an account is part of a permissioned domain, either as owner or via accepted, non-expired credentials.", + "validation_points": [ + "sleDomain existence", + "domain owner check", + "credential existence", + "credential accepted flag", + "credential expiration" + ] + } + ], + "data_flows": [ + { + "field": "sleDomain", + "flow": [ + "view.read(keylet::permissionedDomain(domainID))", + "used for owner check and to get sfAcceptedCredentials" + ], + "origin": "view.read(keylet::permissionedDomain(domainID))", + "transformations": [ + "None (direct ledger read)" + ], + "validated_at": "Immediately after read (if (!sleDomain))" + }, + { + "field": "sfOwner", + "flow": [ + "sleDomain->getAccountID(sfOwner)", + "compared to input account" + ], + "origin": "sleDomain->getAccountID(sfOwner)", + "transformations": [ + "None (direct field access)" + ], + "validated_at": "Compared to input account" + }, + { + "field": "sfAcceptedCredentials", + "flow": [ + "sleDomain->getFieldArray(sfAcceptedCredentials)", + "iterated for credential checks" + ], + "origin": "sleDomain->getFieldArray(sfAcceptedCredentials)", + "transformations": [ + "Iterated as array" + ], + "validated_at": "Each credential checked in loop" + }, + { + "field": "credential", + "flow": [ + "credential[sfIssuer], credential[sfCredentialType]", + "used to construct keylet::credential for ledger read" + ], + "origin": "Element of sfAcceptedCredentials array", + "transformations": [ + "Used as parameters for keylet" + ], + "validated_at": "sleCred existence, accepted flag, expiration" + }, + { + "field": "sleCred", + "flow": [ + "ledger read", + "checked for existence, lsfAccepted flag, expiration" + ], + "origin": "view.read(keylet::credential(account, credential[sfIssuer], credential[sfCredentialType]))", + "transformations": [ + "None" + ], + "validated_at": "Immediately after read and in subsequent checks" + }, + { + "field": "sleOffer", + "flow": [ + "ledger read", + "checked for existence, sfDomainID, lsfHybrid flag, sfAdditionalBooks", + "account extracted for accountInDomain" + ], + "origin": "view.read(keylet::offer(offerID))", + "transformations": [ + "None" + ], + "validated_at": "Multiple points in offerInDomain" + }, + { + "field": "sfDomainID", + "flow": [ + "checked for presence", + "compared to input domainID" + ], + "origin": "sleOffer->getFieldH256(sfDomainID)", + "transformations": [ + "None" + ], + "validated_at": "offerInDomain" + }, + { + "field": "lsfHybrid", + "flow": [ + "checked for flag", + "if set, checks sfAdditionalBooks" + ], + "origin": "sleOffer->isFlag(lsfHybrid)", + "transformations": [ + "None" + ], + "validated_at": "offerInDomain" + }, + { + "field": "sfAdditionalBooks", + "flow": [ + "checked if present when lsfHybrid is set" + ], + "origin": "sleOffer->isFieldPresent(sfAdditionalBooks)", + "transformations": [ + "None" + ], + "validated_at": "offerInDomain" + } + ], + "description": "Provides helper functions for checking if an account or offer is part of a permissioned DEX domain in the XRPL ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sleDomain existence", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::permissionedDomain(domainID)) at accountInDomain", + "issue_pattern": "Missing empty string validation for sleDomain existence", + "why_false_positive": "view.read(keylet::permissionedDomain(domainID)) validates sleDomain existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "domain owner", + "empty", + "string", + "validation" + ], + "evidence": "sleDomain->getAccountID(sfOwner) == account at accountInDomain", + "issue_pattern": "Missing empty string validation for domain owner", + "why_false_positive": "sleDomain->getAccountID(sfOwner) == account validates domain owner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credential existence", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::credential(account, credential[sfIssuer], credential[sfCredentialType])) at accountInDomain (inside std::any_of)", + "issue_pattern": "Missing empty string validation for credential existence", + "why_false_positive": "view.read(keylet::credential(account, credential[sfIssuer], credential[sfCredentialType])) validates credential existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credential accepted flag", + "empty", + "string", + "validation" + ], + "evidence": "!sleCred->isFlag(lsfAccepted) at accountInDomain (inside std::any_of)", + "issue_pattern": "Missing empty string validation for credential accepted flag", + "why_false_positive": "!sleCred->isFlag(lsfAccepted) validates credential accepted flag for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credential expiration", + "empty", + "string", + "validation" + ], + "evidence": "!credentials::checkExpired(sleCred, view.header().parentCloseTime) at accountInDomain (inside std::any_of)", + "issue_pattern": "Missing empty string validation for credential expiration", + "why_false_positive": "!credentials::checkExpired(sleCred, view.header().parentCloseTime) validates credential expiration for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sleOffer existence", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::offer(offerID)) at offerInDomain", + "issue_pattern": "Missing empty string validation for sleOffer existence", + "why_false_positive": "view.read(keylet::offer(offerID)) validates sleOffer existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID presence", + "empty", + "string", + "validation" + ], + "evidence": "!sleOffer->isFieldPresent(sfDomainID) at offerInDomain", + "issue_pattern": "Missing empty string validation for sfDomainID presence", + "why_false_positive": "!sleOffer->isFieldPresent(sfDomainID) validates sfDomainID presence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID matches", + "empty", + "string", + "validation" + ], + "evidence": "sleOffer->getFieldH256(sfDomainID) != domainID at offerInDomain", + "issue_pattern": "Missing empty string validation for sfDomainID matches", + "why_false_positive": "sleOffer->getFieldH256(sfDomainID) != domainID validates sfDomainID matches for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Hybrid offer AdditionalBooks presence", + "empty", + "string", + "validation" + ], + "evidence": "sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks) at offerInDomain", + "issue_pattern": "Missing empty string validation for Hybrid offer AdditionalBooks presence", + "why_false_positive": "sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks) validates Hybrid offer AdditionalBooks presence for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp", + "functions": [ + { + "args": [ + "view", + "account", + "domainID" + ], + "lineno": 6, + "name": "accountInDomain" + }, + { + "args": [ + "view", + "offerID", + "domainID", + "j" + ], + "lineno": 25, + "name": "offerInDomain" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "permissioned_dex" + } + ], + "test_coverage_notes": "The code contains defensive checks (with LCOV_EXCL_LINE) indicating some branches are not covered by tests (e.g., missing offer, missing domainID, hybrid offer missing AdditionalBooks). The main validation logic (domain/credential checks) is likely tested in integration or unit tests for permissioned DEX features, possibly in files like test/ledger/PermissionedDEX_test.cpp, test/ledger/CredentialHelpers_test.cpp, or similar. However, explicit test coverage for all error branches and edge cases (e.g., expired credentials, hybrid offers with missing fields) may be lacking. Review of test files is needed to confirm full coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom logic (ledger view, keylet, field presence, flags, and credential helpers)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false", + "field": "sleDomain existence", + "location": "accountInDomain", + "validated_by": "view.read(keylet::permissionedDomain(domainID))", + "validates": [ + "Checks if the domain exists in the ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns true", + "field": "domain owner", + "location": "accountInDomain", + "validated_by": "sleDomain->getAccountID(sfOwner) == account", + "validates": [ + "Checks if the account is the owner of the domain" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false for this credential", + "field": "credential existence", + "location": "accountInDomain (inside std::any_of)", + "validated_by": "view.read(keylet::credential(account, credential[sfIssuer], credential[sfCredentialType]))", + "validates": [ + "Checks if the credential exists for the account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false for this credential", + "field": "credential accepted flag", + "location": "accountInDomain (inside std::any_of)", + "validated_by": "!sleCred->isFlag(lsfAccepted)", + "validates": [ + "Checks if the credential is marked as accepted" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false for this credential", + "field": "credential expiration", + "location": "accountInDomain (inside std::any_of)", + "validated_by": "!credentials::checkExpired(sleCred, view.header().parentCloseTime)", + "validates": [ + "Checks if the credential is not expired" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false", + "field": "sleOffer existence", + "location": "offerInDomain", + "validated_by": "view.read(keylet::offer(offerID))", + "validates": [ + "Checks if the offer exists in the ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false", + "field": "sfDomainID presence", + "location": "offerInDomain", + "validated_by": "!sleOffer->isFieldPresent(sfDomainID)", + "validates": [ + "Checks if the offer has a domain ID field" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false", + "field": "sfDomainID matches", + "location": "offerInDomain", + "validated_by": "sleOffer->getFieldH256(sfDomainID) != domainID", + "validates": [ + "Checks if the offer's domain ID matches the expected domain ID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (after logging error)", + "field": "Hybrid offer AdditionalBooks presence", + "location": "offerInDomain", + "validated_by": "sleOffer->isFlag(lsfHybrid) && !sleOffer->isFieldPresent(sfAdditionalBooks)", + "validates": [ + "Checks if hybrid offers have the AdditionalBooks field" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp.ai.md new file mode 100644 index 0000000000..d916bee6dd --- /dev/null +++ b/src/libxrpl/ledger/helpers/PermissionedDEXHelpers.cpp.ai.md @@ -0,0 +1,33 @@ +# `PermissionedDEXHelpers.cpp` + +## Role in the System + +This file implements the two membership-check predicates that enforce the Permissioned DEX feature in the XRPL: `accountInDomain` and `offerInDomain`. The Permissioned DEX allows a domain owner to create a credential-gated trading environment where only accounts holding specific issued credentials may create or consume offers. These helpers sit at the intersection of the credential system and the DEX, and are invoked both during transaction preclaim validation and at runtime during order book traversal. + +## `accountInDomain` + +This is the core authorization predicate. Given a `ReadView`, an `AccountID`, and a `domainID`, it resolves the domain's `PermissionedDomain` ledger entry via `keylet::permissionedDomain` and walks its `sfAcceptedCredentials` array. + +Two paths lead to membership. First, the domain owner is unconditionally considered to be inside the domain — no credential is required. This implicit membership is intentional: the owner created the domain and cannot be locked out by a missing credential. Second, any other account must hold at least one credential (keyed by `sfIssuer` and `sfCredentialType`) that matches an entry in the domain's accepted list, has the `lsfAccepted` flag set (i.e., the issuer has explicitly accepted the credential on-chain), and has not expired relative to the ledger's `parentCloseTime`. The `std::any_of` scan means a single valid credential is sufficient — the account does not need to satisfy all accepted credential types. + +The expiry check delegates to `credentials::checkExpired` from `CredentialHelpers`, which compares the credential's `sfExpiration` field against `view.header().parentCloseTime`. Using the parent close time rather than the current close time is a deliberate XRPL convention — the parent time is finalized and deterministic, while the current close time is not yet committed at preclaim phase. + +## `offerInDomain` + +This function validates that a specific offer, identified by its `uint256` key, belongs to a given domain. It is designed to be called during order book iteration in `OfferStream`, where the caller already knows the offer's domain from the directory traversal but needs to confirm the offer creator is still eligible for that domain. + +The function begins with three structural checks on the offer SLE: it must exist, it must carry an `sfDomainID` field, and that field must match the passed `domainID`. All three are annotated `LCOV_EXCL_LINE`, marking them as defensive branches that should never fire in normal operation because `OfferStream` only calls this function when `entry->isFieldPresent(sfDomainID)` is already true and passes the domain it read from the SLE. The checks exist as a safety net against future caller mistakes. + +A fourth structural guard handles hybrid offers. If `lsfHybrid` is set, the offer must also carry `sfAdditionalBooks`. A hybrid offer missing that field is considered malformed, and the function logs an error at `j.error()` before returning `false`. This mirrors the structural invariant checked by `ValidPermissionedDEX::visitEntry` in the invariant-check layer. + +After structural validation, `offerInDomain` delegates to `accountInDomain` for the actual credential/membership check on the offer's `sfAccount`. This delegation matters in practice: an offer may have been valid when placed, but the account's credentials can expire or be revoked after the fact. `OfferStream` uses this return value to permanently remove stale domain offers from the book via `permRmOffer`. + +## Call Sites and Integration + +`accountInDomain` is called from `OfferCreate::preclaim` and `Payment::preclaim`. In `OfferCreate`, it gates the offer creator: if the transaction carries `sfDomainID` but the creator is not in the domain, preclaim returns `tecNO_PERMISSION`. In `Payment`, both the sender and the destination are independently checked — a payment routed through a permissioned domain requires both parties to be members. This prevents a domain offer from being consumed by an ineligible payer or delivering to an ineligible recipient. + +`offerInDomain` is used exclusively in `OfferStream`'s offer-iteration loop, which backs both direct DEX consumption and path-based payments. The check occurs after the offer is loaded from the book but before funds are evaluated, ensuring that domain membership is live-verified against the current ledger state rather than relying solely on static order book filtering. + +## Error Handling and Invariants + +Neither function throws exceptions; both return `bool` and rely on `ReadView`'s returning a null `shared_ptr` for missing ledger entries. The `[[nodiscard]]` attributes on both declarations (in the header) enforce that callers cannot silently ignore the result. The `LCOV_EXCL_LINE` markers are an honest signal about test coverage: the defensive branches in `offerInDomain` are not exercised by the current test suite because their triggering preconditions can only arise from internal inconsistencies in the ledger state, not from any valid user-facing input path. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp.ai.json new file mode 100644 index 0000000000..4ccb77c320 --- /dev/null +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp.ai.json @@ -0,0 +1,683 @@ +{ + "args": [ + { + "lineno": 15, + "name": "view" + }, + { + "lineno": 16, + "name": "account" + }, + { + "lineno": 17, + "name": "issuer" + }, + { + "lineno": 18, + "name": "currency" + }, + { + "lineno": 32, + "name": "v" + }, + { + "lineno": 32, + "name": "acc" + }, + { + "lineno": 32, + "name": "iss" + }, + { + "lineno": 32, + "name": "cur" + }, + { + "lineno": 111, + "name": "bSrcHigh" + }, + { + "lineno": 112, + "name": "uSrcAccountID" + }, + { + "lineno": 113, + "name": "uDstAccountID" + }, + { + "lineno": 114, + "name": "uIndex" + }, + { + "lineno": 115, + "name": "sleAccount" + }, + { + "lineno": 116, + "name": "bAuth" + }, + { + "lineno": 117, + "name": "bNoRipple" + }, + { + "lineno": 118, + "name": "bFreeze" + }, + { + "lineno": 119, + "name": "bDeepFreeze" + }, + { + "lineno": 120, + "name": "saBalance" + }, + { + "lineno": 122, + "name": "saLimit" + }, + { + "lineno": 124, + "name": "uQualityIn" + }, + { + "lineno": 125, + "name": "uQualityOut" + }, + { + "lineno": 126, + "name": "j" + }, + { + "lineno": 177, + "name": "sleRippleState" + }, + { + "lineno": 178, + "name": "uLowAccountID" + }, + { + "lineno": 179, + "name": "uHighAccountID" + }, + { + "lineno": 206, + "name": "state" + }, + { + "lineno": 207, + "name": "bSenderHigh" + }, + { + "lineno": 208, + "name": "sender" + }, + { + "lineno": 209, + "name": "before" + }, + { + "lineno": 210, + "name": "after" + }, + { + "lineno": 237, + "name": "amount" + }, + { + "lineno": 238, + "name": "issue" + }, + { + "lineno": 365, + "name": "from" + }, + { + "lineno": 365, + "name": "to" + }, + { + "lineno": 390, + "name": "accountID" + }, + { + "lineno": 391, + "name": "priorBalance" + }, + { + "lineno": 392, + "name": "journal" + }, + { + "lineno": 471, + "name": "sleState" + }, + { + "lineno": 472, + "name": "ammAccountID" + }, + { + "lineno": 500, + "name": "sleMpt" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "creditLimit" + ], + "entry_point": "creditLimit", + "purpose": "Fetches the credit limit for an account/issuer/currency triple from the ledger.", + "validation_points": [ + "XRPL_ASSERT(result.getIssuer() == account)", + "XRPL_ASSERT(result.get().currency == currency)" + ] + }, + { + "call_chain": [ + "creditLimit2", + "creditLimit" + ], + "entry_point": "creditLimit2", + "purpose": "Fetches the credit limit and converts it to IOUAmount.", + "validation_points": [ + "XRPL_ASSERT(result.getIssuer() == account) in creditLimit", + "XRPL_ASSERT(result.get().currency == currency) in creditLimit" + ] + }, + { + "call_chain": [ + "creditBalance" + ], + "entry_point": "creditBalance", + "purpose": "Fetches the credit balance for an account/issuer/currency triple from the ledger.", + "validation_points": [ + "XRPL_ASSERT(result.getIssuer() == account)", + "XRPL_ASSERT(result.get().currency == currency)" + ] + }, + { + "call_chain": [ + "isIndividualFrozen" + ], + "entry_point": "isIndividualFrozen", + "purpose": "Checks if a trust line is individually frozen by the issuer.", + "validation_points": [ + "currency validated by isXRP" + ] + }, + { + "call_chain": [ + "isFrozen" + ], + "entry_point": "isFrozen", + "purpose": "Checks if a trust line is globally or individually frozen.", + "validation_points": [ + "currency validated by isXRP" + ] + }, + { + "call_chain": [ + "isDeepFrozen" + ], + "entry_point": "isDeepFrozen", + "purpose": "Checks if a trust line is deeply frozen (not XRP, not self-issued, and trust line exists).", + "validation_points": [ + "currency validated by isXRP" + ] + } + ], + "data_flows": [ + { + "field": "result.getIssuer()", + "flow": [ + "Constructed with account as issuer", + "Possibly overwritten by sleRippleState->getFieldAmount", + "Explicitly set to account after fetch", + "Validated by XRPL_ASSERT" + ], + "origin": "Constructed as Issue{currency, account} in creditLimit/creditBalance", + "transformations": [ + "May be replaced by ledger value, then forcibly set to account" + ], + "validated_at": "XRPL_ASSERT in creditLimit and creditBalance" + }, + { + "field": "result.get().currency", + "flow": [ + "Used to construct Issue", + "May be replaced by ledger value", + "Validated by XRPL_ASSERT" + ], + "origin": "Passed as argument to creditLimit/creditBalance", + "transformations": [ + "May be replaced by ledger value, but expected to match input" + ], + "validated_at": "XRPL_ASSERT in creditLimit and creditBalance" + }, + { + "field": "currency", + "flow": [ + "Passed as argument", + "Checked by isXRP", + "If XRP, returns false (not frozen)" + ], + "origin": "Function argument to isIndividualFrozen, isFrozen, isDeepFrozen", + "transformations": [ + "None, just checked" + ], + "validated_at": "isXRP check at function start" + }, + { + "field": "sleRippleState", + "flow": [ + "Fetched from ledger", + "Used to get field amounts or flags" + ], + "origin": "view.read(keylet::line(account, issuer, currency))", + "transformations": [ + "None, just used for field access" + ], + "validated_at": "Indirectly, via field checks and asserts" + } + ], + "description": "Implements helper functions for managing trust lines, IOU issuance/redemption, freeze checks, and related operations in the XRPL ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result.getIssuer()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at creditLimit", + "issue_pattern": "Missing empty string validation for result.getIssuer()", + "why_false_positive": "XRPL_ASSERT validates result.getIssuer() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result.get().currency", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at creditLimit", + "issue_pattern": "Missing empty string validation for result.get().currency", + "why_false_positive": "XRPL_ASSERT validates result.get().currency for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result.getIssuer()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at creditBalance", + "issue_pattern": "Missing empty string validation for result.getIssuer()", + "why_false_positive": "XRPL_ASSERT validates result.getIssuer() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result.get().currency", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at creditBalance", + "issue_pattern": "Missing empty string validation for result.get().currency", + "why_false_positive": "XRPL_ASSERT validates result.get().currency for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "currency", + "empty", + "string", + "validation" + ], + "evidence": "isXRP at isIndividualFrozen", + "issue_pattern": "Missing empty string validation for currency", + "why_false_positive": "isXRP validates currency for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp", + "functions": [ + { + "args": [ + "view", + "account", + "issuer", + "currency" + ], + "lineno": 15, + "name": "creditLimit" + }, + { + "args": [ + "v", + "acc", + "iss", + "cur" + ], + "lineno": 32, + "name": "creditLimit2" + }, + { + "args": [ + "view", + "account", + "issuer", + "currency" + ], + "lineno": 36, + "name": "creditBalance" + }, + { + "args": [ + "view", + "account", + "currency", + "issuer" + ], + "lineno": 56, + "name": "isIndividualFrozen" + }, + { + "args": [ + "view", + "account", + "currency", + "issuer" + ], + "lineno": 70, + "name": "isFrozen" + }, + { + "args": [ + "view", + "account", + "currency", + "issuer" + ], + "lineno": 87, + "name": "isDeepFrozen" + }, + { + "args": [ + "view", + "bSrcHigh", + "uSrcAccountID", + "uDstAccountID", + "uIndex", + "sleAccount", + "bAuth", + "bNoRipple", + "bFreeze", + "bDeepFreeze", + "saBalance", + "saLimit", + "uQualityIn", + "uQualityOut", + "j" + ], + "lineno": 110, + "name": "trustCreate" + }, + { + "args": [ + "view", + "sleRippleState", + "uLowAccountID", + "uHighAccountID", + "j" + ], + "lineno": 176, + "name": "trustDelete" + }, + { + "args": [ + "view", + "state", + "bSenderHigh", + "sender", + "before", + "after", + "j" + ], + "lineno": 205, + "name": "updateTrustLine" + }, + { + "args": [ + "view", + "account", + "amount", + "issue", + "j" + ], + "lineno": 236, + "name": "issueIOU" + }, + { + "args": [ + "view", + "account", + "amount", + "issue", + "j" + ], + "lineno": 288, + "name": "redeemIOU" + }, + { + "args": [ + "view", + "issue", + "account", + "authType" + ], + "lineno": 340, + "name": "requireAuth" + }, + { + "args": [ + "view", + "issue", + "from", + "to" + ], + "lineno": 364, + "name": "canTransfer" + }, + { + "args": [ + "view", + "accountID", + "priorBalance", + "issue", + "journal" + ], + "lineno": 389, + "name": "addEmptyHolding" + }, + { + "args": [ + "view", + "accountID", + "issue", + "journal" + ], + "lineno": 426, + "name": "removeEmptyHolding" + }, + { + "args": [ + "view", + "sleState", + "ammAccountID", + "j" + ], + "lineno": 470, + "name": "deleteAMMTrustLine" + }, + { + "args": [ + "view", + "sleMpt", + "ammAccountID", + "j" + ], + "lineno": 499, + "name": "deleteAMMMPToken" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "These helpers are core to trust line and IOU logic. They are likely tested indirectly via higher-level transaction tests (e.g., trust set, payment, freeze/unfreeze). Direct unit tests for these helpers may exist in files like 'RippleState_test.cpp', 'TrustSet_test.cpp', or 'Credit_test.cpp' in the rippled/test or xrpl/test directories. However, the XRPL_ASSERTs are runtime checks and may not be directly tested for failure cases unless assertions are specifically tested. The isXRP validation is simple and likely covered by tests involving XRP and IOU trust lines. Gaps may exist in testing assertion failures or edge cases where ledger data is malformed.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), isXRP (utility function)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely throws or aborts)", + "field": "result.getIssuer()", + "location": "creditLimit", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the issuer of the result STAmount matches the input account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely throws or aborts)", + "field": "result.get().currency", + "location": "creditLimit", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the currency of the result STAmount matches the input currency" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely throws or aborts)", + "field": "result.getIssuer()", + "location": "creditBalance", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the issuer of the result STAmount matches the input account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely throws or aborts)", + "field": "result.get().currency", + "location": "creditBalance", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the currency of the result STAmount matches the input currency" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "currency", + "location": "isIndividualFrozen", + "validated_by": "isXRP", + "validates": [ + "Checks if the currency is XRP, in which case freezing is not possible" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp.ai.md new file mode 100644 index 0000000000..114c1ffe99 --- /dev/null +++ b/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp.ai.md @@ -0,0 +1,53 @@ +# `RippleStateHelpers.cpp` — Trust Line Lifecycle and IOU Primitives + +This file implements the core ledger primitives for the `ltRIPPLE_STATE` object type — the on-ledger representation of a trust line between two accounts. Everything from reading a balance to creating, mutating, and deleting trust lines flows through here. It also contains the IOU issuance and redemption engine, freeze enforcement, authorization checks, and AMM-specific cleanup operations. The file was assembled from earlier split files (notably `Credit.cpp`), as the inline section comments indicate. + +## Trust Line Data Model + +An `ltRIPPLE_STATE` entry is shared by exactly two accounts. The protocol resolves ambiguity by canonically assigning roles based on `AccountID` comparison: the account with the numerically lower ID is the "low" side and the one with the higher ID is the "high" side. This ordering is pervasive — every per-side field has a paired `sfLow*`/`sfHigh*` variant, and every flag has a `lsfLow*`/`lsfHigh*` bit. The balance stored in `sfBalance` is always expressed from the low account's perspective: positive means the low account holds the IOU, negative means the high account holds it. Callers must negate if they are the high side. + +## Reading Balance and Limit + +`creditLimit` fetches `sfLowLimit` or `sfHighLimit` from the trust line, then resets the issuer field of the returned `STAmount` to the querying account. The post-condition asserts confirm that this reattachment is correct. `creditBalance` does the same for `sfBalance`, but negates the result when the querying account is the high side. `creditLimit2` is a thin adapter that converts the result to `IOUAmount` for callers that prefer that type. The design of always returning amounts in "caller's perspective" coordinates — rather than raw ledger-perspective — keeps callers free of the low/high bookkeeping. + +## Freeze Hierarchy + +Three levels of IOU freeze are implemented, each checking progressively more restrictive conditions: + +- **`isFrozen`** is the broadest check. It first reads the issuer's `AccountRoot` for `lsfGlobalFreeze` (the issuer has frozen all lines for this currency), then checks the per-line `lsfHighFreeze`/`lsfLowFreeze` flag set by the issuer side. XRP is always unfrozen. +- **`isIndividualFrozen`** skips the global freeze check and only looks at the per-line flag. This is used when the caller has already confirmed there is no global freeze, or when checking from the issuer's own perspective. +- **`isDeepFrozen`** checks `lsfHighDeepFreeze`/`lsfLowDeepFreeze`, a newer bilateral freeze. Unlike a regular freeze (which only prevents the frozen party from transacting), a deep freeze blocks both sides from moving funds through the line. It returns `false` for self-issued amounts (where `issuer == account`) because the issuer cannot freeze themselves out of their own obligation. + +The header exposes `checkDeepFrozen()` as a convenience that returns a `TER` directly, avoiding if/then boilerplate at call sites. + +## Trust Line Creation: `trustCreate` + +`trustCreate` is the workhorse that writes a new `ltRIPPLE_STATE` SLE to the ledger. It inserts the new entry into both accounts' owner directories via `view.dirInsert()`, capturing the returned 64-bit directory node hints in `sfLowNode`/`sfHighNode` — these are required later by `trustDelete` to find and remove the entry without scanning the full directory. The `bSrcHigh` parameter tells the function which account is the requesting side, which determines which half of every paired field to populate. Importantly, when the peer account has `lsfDefaultRipple` cleared, the function automatically sets the `lsfHighNoRipple`/`lsfLowNoRipple` flag on that side — propagating the peer's preference into the new line without requiring a separate transaction. The deep freeze parameter `bDeepFreeze` was added alongside the feature, making `trustCreate` the single place where all line flags are initialized consistently. + +`trustDelete` is the mirror operation. It removes the SLE from both owner directories using the stored node hints, then erases the SLE. Failure to remove from either directory returns `tefBAD_LEDGER`, signaling data corruption. + +## IOU Issuance and Redemption + +`issueIOU` handles the case where an issuer sends tokens to a holder. It expresses the balance in sender (issuer) perspective terms, subtracts the issued amount, then calls the static `updateTrustLine` to check whether the sender's reserve can be released. After `updateTrustLine` runs, `view.creditHookIOU()` is called — this hook is a no-op in most `ApplyView` subclasses but is intercepted by `PaymentSandbox` to record deferred credits during multi-path payment processing. If no trust line exists yet, `issueIOU` calls `trustCreate` to establish one — this is the only path through which issuance can implicitly create a trust line. + +`redeemIOU` is structurally symmetric but asymmetric in its error handling: a missing trust line during redemption is an `tefINTERNAL` fatal error, not a recoverable condition. The invariant is that you cannot hold an IOU balance without an existing trust line; if the line is gone, ledger state is corrupt. + +## Automatic Trust Line Cleanup: `updateTrustLine` + +This static helper, called from both `issueIOU` and `redeemIOU`, determines whether the sender's side of the trust line has become so minimal that the owner count reserve can be released. The condition is conjunctive across seven criteria: the balance crossed from positive to zero/negative, the sender had a reserve set, the sender's NoRipple flag is inconsistent with the peer's default ripple setting (meaning neither side wants the line kept alive), there is no freeze on the sender's side, the trust limit is zero, and both quality fields are zero. When all are satisfied, `adjustOwnerCount` decrements the sender's `sfOwnerCount` and the reserve flag is cleared. The function returns `true` if the other side also has no reserve — meaning neither account needs the line anymore and the caller should delete it. The caller still sets the final balance on the SLE before deletion, ensuring that the ledger metadata at deletion time accurately reflects the final state even for a line being removed. + +## Authorization and Transfer Checks + +`requireAuth` implements a three-way `AuthType` distinction. `StrongAuth` requires the trust line to exist before performing any other check, returning `tecNO_LINE` if absent. `WeakAuth` and `Legacy` only enforce authentication if the issuer's `AccountRoot` carries `lsfRequireAuth`, and only when a trust line is present; if the issuer doesn't require auth, they succeed regardless of whether a line exists. + +`canTransfer` enforces the rippling rules that govern whether an IOU can flow between two non-issuer parties. If the issuer is one of the endpoints, the transfer is direct and always allowed. Otherwise, it inspects both trust lines for the `lsfHighNoRipple`/`lsfLowNoRipple` flag (from the issuer's perspective), falling back to `lsfDefaultRipple` on the issuer's account when a line does not yet exist. If rippling is disabled on both sides, `terNO_RIPPLE` is returned. This covers the important edge case where a payment might create a new trust line — the default ripple flag provides the "intended" state before the line exists. + +## Empty Holding Management + +`addEmptyHolding` creates a zero-balance trust line so an account can receive an IOU it does not yet hold. It enforces several preconditions — the issuer must not have a global freeze, the issuer must have `lsfDefaultRipple` set (returning `tecINTERNAL` otherwise, enforcing a protocol invariant), the line must not already exist, and the recipient must have enough XRP balance to cover the incremented owner reserve. It always creates the line with `bNoRipple=true`, which is the conservative default for freshly created holding slots. + +`removeEmptyHolding` tears down such a line. For XRP it's a no-op if the balance is non-zero. For IOUs it checks that the balance is zero (for non-issuers), then manually adjusts the owner count for both the low and high reserve holders before calling `trustDelete`. The reserve flags are cleared on the SLE before deletion specifically to make the resulting ledger metadata reflect accurate owner count state at the moment of removal, even though the SLE is about to vanish. + +## AMM-Specific Deletions + +`deleteAMMTrustLine` wraps `trustDelete` with validation that exactly one side of the trust line is an AMM account (detected by the presence of `sfAMMID` on the `AccountRoot`) and that, if a specific AMM account is required, it is actually a party to the line. This prevents accidentally deleting trust lines during AMM withdrawal operations that don't belong to the target pool. `deleteAMMMPToken` is a simpler companion that removes an MPToken SLE from an AMM account's owner directory during AMM teardown. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/TokenHelpers.cpp.ai.json new file mode 100644 index 0000000000..afce469c63 --- /dev/null +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp.ai.json @@ -0,0 +1,685 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "checkFrozen(view, account, asset)", + "std::visit([&](auto const& issue) { return checkFrozen(view, account, issue); }, asset.value())", + "checkFrozen(view, account, Issue/MPTIssue)", + "isFrozen(view, account, Issue/MPTIssue)" + ], + "entry_point": "checkFrozen(ReadView const&, AccountID const&, Asset const&)", + "purpose": "Validates if an asset (of type Issue or MPTIssue) is frozen for a given account, returning a TER code.", + "validation_points": [ + "isFrozen(view, account, Issue/MPTIssue) (core validation)", + "checkFrozen(view, account, Issue/MPTIssue) (returns error code if frozen)" + ] + }, + { + "call_chain": [ + "isFrozen(view, account, asset, depth)", + "std::visit([&](auto const& issue) { return isFrozen(view, account, issue, depth); }, asset.value())", + "isFrozen(view, account, Issue/MPTIssue, depth)" + ], + "entry_point": "isFrozen(ReadView const&, AccountID const&, Asset const&, int)", + "purpose": "Checks if an asset is frozen for an account, possibly with recursion (depth).", + "validation_points": [ + "isFrozen(view, account, Issue/MPTIssue, depth)" + ] + }, + { + "call_chain": [ + "isAnyFrozen(view, accounts, asset, depth)", + "asset.visit([&](Issue const& issue) { ... }, [&](MPTIssue const& issue) { ... })", + "isAnyFrozen(view, accounts, Issue/MPTIssue, depth)", + "isFrozen(view, account, issue, depth) (inside loop)" + ], + "entry_point": "isAnyFrozen(ReadView const&, std::initializer_list const&, Asset const&, int)", + "purpose": "Checks if any account in a list has the asset frozen.", + "validation_points": [ + "isFrozen(view, account, issue, depth)" + ] + }, + { + "call_chain": [ + "isGlobalFrozen(view, asset)", + "asset.visit([&](Issue const& issue) { ... }, [&](MPTIssue const& issue) { ... })", + "isGlobalFrozen(view, issue.getIssuer()) / isGlobalFrozen(view, issue)" + ], + "entry_point": "isGlobalFrozen(ReadView const&, Asset const&)", + "purpose": "Checks if an asset is globally frozen (issuer-level or MPT-level).", + "validation_points": [ + "isGlobalFrozen(view, issue.getIssuer())", + "isGlobalFrozen(view, issue)" + ] + }, + { + "call_chain": [ + "isIndividualFrozen(view, account, asset)", + "std::visit([&](auto const& issue) { return isIndividualFrozen(view, account, issue); }, asset.value())" + ], + "entry_point": "isIndividualFrozen(ReadView const&, AccountID const&, Asset const&)", + "purpose": "Checks if an asset is individually frozen for a specific account.", + "validation_points": [ + "isIndividualFrozen(view, account, issue)" + ] + }, + { + "call_chain": [ + "isDeepFrozen(view, account, asset, depth)", + "std::visit([&](auto const& issue) { return isDeepFrozen(view, account, issue, depth); }, asset.value())", + "isDeepFrozen(view, account, MPTIssue, depth) (for MPTIssue: just calls isFrozen)" + ], + "entry_point": "isDeepFrozen(ReadView const&, AccountID const&, Asset const&, int)", + "purpose": "Checks if an asset is 'deep frozen' (for MPTs, same as frozen).", + "validation_points": [ + "isDeepFrozen(view, account, issue, depth)", + "isFrozen(view, account, mptIssue, depth)" + ] + }, + { + "call_chain": [ + "checkDeepFrozen(view, account, asset)", + "std::visit([&](auto const& issue) { return checkDeepFrozen(view, account, issue); }, asset.value())", + "checkDeepFrozen(view, account, MPTIssue)", + "isDeepFrozen(view, account, mptIssue)" + ], + "entry_point": "checkDeepFrozen(ReadView const&, AccountID const&, Asset const&)", + "purpose": "Validates if an asset is deep frozen for an account, returning a TER code.", + "validation_points": [ + "isDeepFrozen(view, account, mptIssue)" + ] + } + ], + "data_flows": [ + { + "field": "Asset", + "flow": [ + "Input to checkFrozen/isFrozen/isAnyFrozen/etc.", + "std::visit or .visit to extract Issue or MPTIssue", + "Passed to lower-level validation (isFrozen, isGlobalFrozen, etc.)" + ], + "origin": "Passed as function argument (from transaction context or ledger operation)", + "transformations": [ + "Variant visitation to extract underlying Issue or MPTIssue" + ], + "validated_at": "isFrozen, isGlobalFrozen, isIndividualFrozen, isAnyFrozen, isDeepFrozen" + }, + { + "field": "Issue", + "flow": [ + "Input to isFrozen/isGlobalFrozen/isIndividualFrozen", + "Used to look up issuer/account in ledger via ReadView" + ], + "origin": "Extracted from Asset or passed directly", + "transformations": [ + "May be constructed from Asset", + "Used to access issuer/account fields" + ], + "validated_at": "isFrozen, isGlobalFrozen, isIndividualFrozen" + }, + { + "field": "MPTIssue", + "flow": [ + "Input to isFrozen/isGlobalFrozen/isIndividualFrozen", + "Used to look up MPT issuer/account in ledger via ReadView" + ], + "origin": "Extracted from Asset or passed directly", + "transformations": [ + "May be constructed from Asset", + "Used to access issuer/account fields" + ], + "validated_at": "isFrozen, isGlobalFrozen, isIndividualFrozen" + }, + { + "field": "AccountID", + "flow": [ + "Input to all validation functions", + "Used to check freeze status for specific account" + ], + "origin": "Passed as function argument (from transaction context or ledger operation)", + "transformations": [], + "validated_at": "isFrozen, isIndividualFrozen, isAnyFrozen, isDeepFrozen" + }, + { + "field": "ReadView", + "flow": [ + "Input to all validation functions", + "Used to look up account/asset state in ledger" + ], + "origin": "Passed as function argument (ledger state at a point in time)", + "transformations": [], + "validated_at": "Throughout all validation functions" + }, + { + "field": "TER", + "flow": [ + "Result of validation (tesSUCCESS, tecFROZEN, tecLOCKED)", + "Used by transaction engine to determine transaction result" + ], + "origin": "Returned from checkFrozen/checkDeepFrozen", + "transformations": [ + "Set based on result of isFrozen/isDeepFrozen" + ], + "validated_at": "checkFrozen, checkDeepFrozen" + } + ], + "description": "Implements helper functions for token operations in the XRPL ledger, including freeze checks, account balance queries, trust line management, authorization, and money transfer logic for both IOUs and Multi-Party Tokens (MPTs).", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "asset (Asset), issue (Issue), mptIssue (MPTIssue)", + "empty", + "string", + "validation" + ], + "evidence": "isGlobalFrozen, isIndividualFrozen, isFrozen, checkFrozen, isAnyFrozen, isDeepFrozen at isGlobalFrozen, isIndividualFrozen, isFrozen, checkFrozen, isAnyFrozen, isDeepFrozen", + "issue_pattern": "Missing empty string validation for asset (Asset), issue (Issue), mptIssue (MPTIssue)", + "why_false_positive": "isGlobalFrozen, isIndividualFrozen, isFrozen, checkFrozen, isAnyFrozen, isDeepFrozen validates asset (Asset), issue (Issue), mptIssue (MPTIssue) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/TokenHelpers.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Asset const& asset", + "Asset const& asset2" + ], + "lineno": 13, + "name": "isLPTokenFrozen" + }, + { + "args": [ + "ReadView const& view", + "Asset const& asset" + ], + "lineno": 22, + "name": "isGlobalFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Asset const& asset" + ], + "lineno": 28, + "name": "isIndividualFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Asset const& asset", + "int depth" + ], + "lineno": 34, + "name": "isFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Issue const& issue" + ], + "lineno": 40, + "name": "checkFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "MPTIssue const& mptIssue" + ], + "lineno": 46, + "name": "checkFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Asset const& asset" + ], + "lineno": 52, + "name": "checkFrozen" + }, + { + "args": [ + "ReadView const& view", + "std::initializer_list const& accounts", + "Issue const& issue" + ], + "lineno": 58, + "name": "isAnyFrozen" + }, + { + "args": [ + "ReadView const& view", + "std::initializer_list const& accounts", + "Asset const& asset", + "int depth" + ], + "lineno": 68, + "name": "isAnyFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "MPTIssue const& mptIssue", + "int depth" + ], + "lineno": 75, + "name": "isDeepFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Asset const& asset", + "int depth" + ], + "lineno": 82, + "name": "isDeepFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "MPTIssue const& mptIssue" + ], + "lineno": 89, + "name": "checkDeepFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Asset const& asset" + ], + "lineno": 95, + "name": "checkDeepFrozen" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Currency const& currency", + "AccountID const& issuer", + "FreezeHandling zeroIfFrozen", + "beast::Journal j" + ], + "lineno": 109, + "name": "getLineIfUsable" + }, + { + "args": [ + "ReadView const& view", + "SLE::const_ref sle", + "AccountID const& account", + "Currency const& currency", + "AccountID const& issuer", + "bool includeOppositeLimit", + "beast::Journal j" + ], + "lineno": 143, + "name": "getTrustLineBalance" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Currency const& currency", + "AccountID const& issuer", + "FreezeHandling zeroIfFrozen", + "beast::Journal j", + "SpendableHandling includeFullBalance" + ], + "lineno": 170, + "name": "accountHolds" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Issue const& issue", + "FreezeHandling zeroIfFrozen", + "beast::Journal j", + "SpendableHandling includeFullBalance" + ], + "lineno": 191, + "name": "accountHolds" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "MPTIssue const& mptIssue", + "FreezeHandling zeroIfFrozen", + "AuthHandling zeroIfUnauthorized", + "beast::Journal j", + "SpendableHandling includeFullBalance" + ], + "lineno": 197, + "name": "accountHolds" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "Asset const& asset", + "FreezeHandling zeroIfFrozen", + "AuthHandling zeroIfUnauthorized", + "beast::Journal j", + "SpendableHandling includeFullBalance" + ], + "lineno": 241, + "name": "accountHolds" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& id", + "STAmount const& saDefault", + "FreezeHandling freezeHandling", + "beast::Journal j" + ], + "lineno": 255, + "name": "accountFunds" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& id", + "STAmount const& saDefault", + "FreezeHandling freezeHandling", + "AuthHandling authHandling", + "beast::Journal j" + ], + "lineno": 267, + "name": "accountFunds" + }, + { + "args": [ + "ReadView const& view", + "STAmount const& amount" + ], + "lineno": 277, + "name": "transferRate" + }, + { + "args": [ + "ReadView const& view", + "Issue const& issue" + ], + "lineno": 285, + "name": "canAddHolding" + }, + { + "args": [ + "ReadView const& view", + "Asset const& asset" + ], + "lineno": 299, + "name": "canAddHolding" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& accountID", + "XRPAmount priorBalance", + "Asset const& asset", + "beast::Journal journal" + ], + "lineno": 307, + "name": "addEmptyHolding" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& accountID", + "Asset const& asset", + "beast::Journal journal" + ], + "lineno": 317, + "name": "removeEmptyHolding" + }, + { + "args": [ + "ReadView const& view", + "Asset const& asset", + "AccountID const& account", + "AuthType authType" + ], + "lineno": 327, + "name": "requireAuth" + }, + { + "args": [ + "ReadView const& view", + "Asset const& asset", + "AccountID const& from", + "AccountID const& to" + ], + "lineno": 335, + "name": "canTransfer" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& uSenderID", + "AccountID const& uReceiverID", + "STAmount const& saAmount", + "bool bCheckIssuer", + "beast::Journal j" + ], + "lineno": 347, + "name": "directSendNoFeeIOU" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& uSenderID", + "AccountID const& uReceiverID", + "STAmount const& saAmount", + "STAmount& saActual", + "beast::Journal j", + "WaiveTransferFee waiveFee" + ], + "lineno": 426, + "name": "directSendNoLimitIOU" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& senderID", + "Issue const& issue", + "MultiplePaymentDestinations const& receivers", + "STAmount& actual", + "beast::Journal j", + "WaiveTransferFee waiveFee" + ], + "lineno": 464, + "name": "directSendNoLimitMultiIOU" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& uSenderID", + "AccountID const& uReceiverID", + "STAmount const& saAmount", + "beast::Journal j", + "WaiveTransferFee waiveFee" + ], + "lineno": 517, + "name": "accountSendIOU" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& senderID", + "Issue const& issue", + "MultiplePaymentDestinations const& receivers", + "beast::Journal j", + "WaiveTransferFee waiveFee" + ], + "lineno": 577, + "name": "accountSendMultiIOU" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& uSenderID", + "AccountID const& uReceiverID", + "STAmount const& saAmount", + "beast::Journal j" + ], + "lineno": 634, + "name": "directSendNoFeeMPT" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& uSenderID", + "AccountID const& uReceiverID", + "STAmount const& saAmount", + "STAmount& saActual", + "beast::Journal j", + "WaiveTransferFee waiveFee", + "AllowMPTOverflow allowOverflow" + ], + "lineno": 682, + "name": "directSendNoLimitMPT" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& senderID", + "MPTIssue const& mptIssue", + "MultiplePaymentDestinations const& receivers", + "STAmount& actual", + "beast::Journal j", + "WaiveTransferFee waiveFee" + ], + "lineno": 728, + "name": "directSendNoLimitMultiMPT" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& uSenderID", + "AccountID const& uReceiverID", + "STAmount const& saAmount", + "beast::Journal j", + "WaiveTransferFee waiveFee", + "AllowMPTOverflow allowOverflow" + ], + "lineno": 803, + "name": "accountSendMPT" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& senderID", + "MPTIssue const& mptIssue", + "MultiplePaymentDestinations const& receivers", + "beast::Journal j", + "WaiveTransferFee waiveFee" + ], + "lineno": 818, + "name": "accountSendMultiMPT" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& uSenderID", + "AccountID const& uReceiverID", + "STAmount const& saAmount", + "bool bCheckIssuer", + "beast::Journal j" + ], + "lineno": 828, + "name": "directSendNoFee" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& uSenderID", + "AccountID const& uReceiverID", + "STAmount const& saAmount", + "beast::Journal j", + "WaiveTransferFee waiveFee", + "AllowMPTOverflow allowOverflow" + ], + "lineno": 838, + "name": "accountSend" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& senderID", + "Asset const& asset", + "MultiplePaymentDestinations const& receivers", + "beast::Journal j", + "WaiveTransferFee waiveFee" + ], + "lineno": 849, + "name": "accountSendMulti" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& from", + "AccountID const& to", + "STAmount const& amount", + "beast::Journal j" + ], + "lineno": 862, + "name": "transferXRP" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is primarily helper logic for freeze/lock validation. Test coverage is likely in integration/functional tests for asset freezing, such as in 'rippled' test suites: 'test/ledger/FrozenAssets_test.cpp', 'test/app/tx/Offer_test.cpp', 'test/app/tx/Payment_test.cpp', and possibly 'test/app/ledger/Token_test.cpp'. Direct unit tests for TokenHelpers.cpp may be limited; most coverage is indirect via transaction tests that exercise freezing/locking logic. Gaps may exist for edge cases (e.g., deep freeze, MPTIssue-specific logic, recursion via depth parameter).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "TER (tecFROZEN, tecLOCKED, tesSUCCESS)", + "field": "asset (Asset), issue (Issue), mptIssue (MPTIssue)", + "location": "isGlobalFrozen, isIndividualFrozen, isFrozen, checkFrozen, isAnyFrozen, isDeepFrozen", + "validated_by": "isGlobalFrozen, isIndividualFrozen, isFrozen, checkFrozen, isAnyFrozen, isDeepFrozen", + "validates": [ + "Checks if an asset or issue is globally frozen", + "Checks if an asset or issue is individually frozen for an account", + "Checks if an asset or issue is frozen for an account (possibly recursively, via depth)", + "Checks if any account in a list is frozen for a given issue/asset", + "Returns error codes if frozen/locked" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/TokenHelpers.cpp.ai.md new file mode 100644 index 0000000000..ab4da9cae8 --- /dev/null +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp.ai.md @@ -0,0 +1,45 @@ +# `TokenHelpers.cpp` — Unified Token Dispatch Layer + +`TokenHelpers.cpp` serves as the central dispatch layer between the XRPL ledger's two non-XRP token systems — trust-line-based IOUs and the newer Multi-Party Token (MPT) standard. The XRPL protocol internally represents both as an `Asset` variant (`std::variant`), and this file provides the glue: every public function accepts an `Asset` (or both concrete types via overloading), decomposes it using `std::visit` or `Asset::visit`, and routes to the type-specific implementation in either `RippleStateHelpers` (IOU) or `MPTokenHelpers` (MPT). + +The file is organized into four sections: freeze checking, balance queries, holding lifecycle, and actual money transfers. Higher-level transaction code — payments, offers, AMM operations, vault operations — calls into this file so it never has to branch on token type itself. + +## Freeze Checking + +The freeze model differs meaningfully between IOUs and MPTs, and this file bridges the difference. + +For IOUs, `isFrozen` and `isDeepFrozen` are distinct. A frozen IOU trust line prevents the holder from sending but still allows redemption back to the issuer; "deep frozen" locks the line entirely. For MPTs, `isDeepFrozen` is explicitly identical to `isFrozen` (see the comment at line 104): an MPT lock prevents both sending and receiving, so there is no meaningful additional deep-freeze concept. + +`checkFrozen` returns different `TER` codes depending on asset type: `tecFROZEN` for IOUs, `tecLOCKED` for MPTs. This semantic difference surfaces in transaction error handling at the protocol level. + +The `isAnyFrozen` family is designed for multi-party checks — offer processing and AMM liquidity steps frequently need to verify that neither the buyer nor the seller side is frozen. The `std::initializer_list` parameter avoids an allocation and keeps the call sites terse. + +The `depth` parameter on `isFrozen` and `isDeepFrozen` exists for future-proofing vault recursion: MPT shares that represent vault positions may themselves be backed by assets that are frozen. The header comments note this is purely defensive — such vaults cannot currently be created — but the parameter is threaded through consistently so the architecture doesn't need to change if that changes. + +## Balance Queries + +`accountHolds` has four overloads unified around the `SpendableHandling` flag. The `shSIMPLE_BALANCE` path returns the raw balance; `shFULL_BALANCE` adds the opposite party's credit limit, representing what the account could spend if the counterparty were willing to absorb debt. For issuers, the semantics diverge: IOU issuers get `STAmount::cMaxValue` (infinite), while MPT issuers get the remaining issuance capacity (`MaximumAmount - OutstandingAmount`) from the issuance SLE. + +The `AuthHandling` parameter for MPT balances controls whether `accountHolds` zeroes the result when the MPT requires authorization and the holder lacks it. The `featureSingleAssetVault` amendment changes which path this takes: with the amendment enabled, the check is delegated to `requireAuth()`, which handles the vault's recursive authorization model; without it, the flag check is done inline. + +The private helper `getLineIfUsable` encapsulates the freeze-aware trust line lookup. A notable non-obvious piece is the `fixFrozenLPTokenTransfer` amendment path: if the trust line's issuer is an AMM account (detected via `sfAMMID` on the issuer SLE), the function calls `isLPTokenFrozen` to verify neither of the AMM's underlying pool assets is frozen. This check is retrofitted — earlier ledger versions could transfer LP tokens even when the underlying pool was frozen. + +## Money Transfers + +The transfer subsystem has two public entry points (`accountSend` and `accountSendMulti`) and one semi-public entry for fee-free sends (`directSendNoFee`). Each dispatches to a static IOU or MPT implementation. + +**IOU transfer pipeline.** The core IOU path is `directSendNoFeeIOU`, which modifies trust line balances directly. When a trust line doesn't exist, it calls `trustCreate` to create one on behalf of the receiver — this implicit trust line creation is unique to direct (issuer-involved) sends. When a send zeros out a sender's balance and the trust line meets a specific set of conditions (zero trust limit, zero quality flags, balance crossing zero, no freeze), the function releases the sender's ledger reserve and potentially deletes the trust line via `trustDelete`. The comment in the code correctly notes this logic "NEEDS to be cleaned up and simplified." + +Third-party IOU sends (sender, receiver, and issuer are all distinct) go through `directSendNoLimitIOU`, which applies a transfer fee by multiplying the amount by `transferRate(view, issuer)` and then executes two sequential `directSendNoFeeIOU` calls: first, issuer→receiver for the delivery amount; second, sender→issuer for the gross amount including the fee. + +**MPT transfer pipeline.** The equivalent `directSendNoFeeMPT` directly manipulates the `sfMPTAmount` field on `MPToken` SLEs and the `sfOutstandingAmount` field on the `MPTokenIssuance` SLE. When the sender is the issuer, the outstanding amount increases; when the receiver is the issuer, it decreases. Third-party MPT sends route through the issuer's outstanding balance the same way IOU sends route through trust lines, applying transfer fees identically. + +**Multi-destination sends.** `accountSendMulti` supports batched payments where one sender distributes a single asset to multiple recipients atomically. The IOU multi-path collects transfer fees into a running `takeFromSender` total per recipient, then issues a single debit from sender→issuer at the end. The MPT multi-path does the same but with a critical security fix via `fixSecurity3_1_3`: pre-fix code read `sfOutstandingAmount` from a stale `view.read()` snapshot on each loop iteration, meaning issuer-as-sender multi-sends could silently exceed `MaximumAmount`. The fix accumulates a `totalSendAmount` in exact `uint64_t` arithmetic (not `STAmount`/`Number`, which would lose precision near the 19-digit `maxMPTokenAmount` boundary) and performs an aggregate check per iteration. + +**Transfer fees and `WaiveTransferFee`.** Both IOU and MPT paths check `WaiveTransferFee::Yes` before applying fees. This waiver is used by vault and AMM operations that need to move tokens without incurring transfer charges. + +## Enumerations and Policy + +The header defines six policy enums that control behavior without adding Boolean parameters: `FreezeHandling`, `AuthHandling`, `SpendableHandling`, `WaiveTransferFee`, `AllowMPTOverflow`, and `AuthType`. These make call sites self-documenting and make it possible to extend policy without breaking existing callers. + +`AllowMPTOverflow` deserves particular attention: MPT vaults need to allow the `sfOutstandingAmount` to exceed `sfMaximumAmount` transiently during vault rebalancing. This enum gates the `isMPTOverflow` check inside `directSendNoLimitMPT`, and it is itself gated behind `featureMPTokensV2` — the overflow allowance only applies when the newer MPT ruleset is active. \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp.ai.json b/src/libxrpl/ledger/helpers/VaultHelpers.cpp.ai.json new file mode 100644 index 0000000000..6770ab5f54 --- /dev/null +++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp.ai.json @@ -0,0 +1,519 @@ +{ + "args": [ + { + "lineno": 9, + "name": "vault" + }, + { + "lineno": 10, + "name": "issuance" + }, + { + "lineno": 11, + "name": "assets" + }, + { + "lineno": 35, + "name": "shares" + }, + { + "lineno": 63, + "name": "truncate" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "assetsToSharesDeposit" + ], + "entry_point": "assetsToSharesDeposit", + "purpose": "Converts an asset amount to a share amount for deposit into a vault.", + "validation_points": [ + "XRPL_ASSERT(!assets.negative(), ...)", + "XRPL_ASSERT(assets.asset() == vault->at(sfAsset), ...)", + "if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt;" + ] + }, + { + "call_chain": [ + "sharesToAssetsDeposit" + ], + "entry_point": "sharesToAssetsDeposit", + "purpose": "Converts a share amount to an asset amount for deposit into a vault.", + "validation_points": [ + "XRPL_ASSERT(!shares.negative(), ...)", + "XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), ...)", + "if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) return std::nullopt;" + ] + }, + { + "call_chain": [ + "assetsToSharesWithdraw" + ], + "entry_point": "assetsToSharesWithdraw", + "purpose": "Converts an asset amount to a share amount for withdrawal from a vault.", + "validation_points": [ + "XRPL_ASSERT(!assets.negative(), ...)", + "XRPL_ASSERT(assets.asset() == vault->at(sfAsset), ...)", + "if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt;" + ] + }, + { + "call_chain": [ + "sharesToAssetsWithdraw" + ], + "entry_point": "sharesToAssetsWithdraw", + "purpose": "Converts a share amount to an asset amount for withdrawal from a vault.", + "validation_points": [ + "XRPL_ASSERT(!shares.negative(), ...)", + "XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), ...)", + "if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) return std::nullopt;" + ] + } + ], + "data_flows": [ + { + "field": "assets (STAmount)", + "flow": [ + "Function argument", + "Validated for non-negativity and correct asset type", + "Used in calculation with vault->at(sfAssetsTotal) and issuance->at(sfOutstandingAmount)", + "Result returned as shares (STAmount)" + ], + "origin": "Function argument (assetsToSharesDeposit, assetsToSharesWithdraw)", + "transformations": [ + "Checked for negative value", + "Checked for asset type match", + "Used in arithmetic to compute shares" + ], + "validated_at": "Immediately at function entry (asserts and if-check)" + }, + { + "field": "shares (STAmount)", + "flow": [ + "Function argument", + "Validated for non-negativity and correct share asset type", + "Used in calculation with vault->at(sfAssetsTotal) and issuance->at(sfOutstandingAmount)", + "Result returned as assets (STAmount)" + ], + "origin": "Function argument (sharesToAssetsDeposit, sharesToAssetsWithdraw)", + "transformations": [ + "Checked for negative value", + "Checked for asset type match", + "Used in arithmetic to compute assets" + ], + "validated_at": "Immediately at function entry (asserts and if-check)" + }, + { + "field": "vault->at(sfAsset)", + "flow": [ + "vault SLE", + "Used to validate assets.asset()", + "Used to construct STAmount for shares" + ], + "origin": "vault SLE field", + "transformations": [ + "Compared to assets.asset()", + "Used as asset type for STAmount" + ], + "validated_at": "At function entry (asserts and if-check)" + }, + { + "field": "vault->at(sfShareMPTID)", + "flow": [ + "vault SLE", + "Used to validate shares.asset()", + "Used to construct STAmount for assets" + ], + "origin": "vault SLE field", + "transformations": [ + "Compared to shares.asset()", + "Used as asset type for STAmount" + ], + "validated_at": "At function entry (asserts and if-check)" + }, + { + "field": "vault->at(sfAssetsTotal)", + "flow": [ + "vault SLE", + "Used in arithmetic to determine proportional shares/assets" + ], + "origin": "vault SLE field", + "transformations": [ + "Used directly or adjusted by subtracting vault->at(sfLossUnrealized) in withdraw functions" + ], + "validated_at": "Not directly validated in these functions" + }, + { + "field": "issuance->at(sfOutstandingAmount)", + "flow": [ + "issuance SLE", + "Used in arithmetic to determine proportional shares/assets" + ], + "origin": "issuance SLE field", + "transformations": [ + "Used directly in division/multiplication" + ], + "validated_at": "Not directly validated in these functions" + } + ], + "description": "This file provides helper functions for converting between assets and shares in the context of XRPL vaults, handling both deposit and withdrawal scenarios with appropriate checks and calculations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "assets (STAmount)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(!assets.negative(), ...) at assetsToSharesDeposit", + "issue_pattern": "Missing empty string validation for assets (STAmount)", + "why_false_positive": "XRPL_ASSERT(!assets.negative(), ...) validates assets (STAmount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "assets.asset() vs vault->at(sfAsset)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(assets.asset() == vault->at(sfAsset), ...) at assetsToSharesDeposit", + "issue_pattern": "Missing empty string validation for assets.asset() vs vault->at(sfAsset)", + "why_false_positive": "XRPL_ASSERT(assets.asset() == vault->at(sfAsset), ...) validates assets.asset() vs vault->at(sfAsset) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "assets (STAmount)", + "empty", + "string", + "validation" + ], + "evidence": "if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt; at assetsToSharesDeposit", + "issue_pattern": "Missing empty string validation for assets (STAmount)", + "why_false_positive": "if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt; validates assets (STAmount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "shares (STAmount)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(!shares.negative(), ...) at sharesToAssetsDeposit", + "issue_pattern": "Missing empty string validation for shares (STAmount)", + "why_false_positive": "XRPL_ASSERT(!shares.negative(), ...) validates shares (STAmount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "shares.asset() vs vault->at(sfShareMPTID)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), ...) at sharesToAssetsDeposit", + "issue_pattern": "Missing empty string validation for shares.asset() vs vault->at(sfShareMPTID)", + "why_false_positive": "XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), ...) validates shares.asset() vs vault->at(sfShareMPTID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "shares (STAmount)", + "empty", + "string", + "validation" + ], + "evidence": "if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) return std::nullopt; at sharesToAssetsDeposit", + "issue_pattern": "Missing empty string validation for shares (STAmount)", + "why_false_positive": "if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) return std::nullopt; validates shares (STAmount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "assets (STAmount)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(!assets.negative(), ...) at assetsToSharesWithdraw", + "issue_pattern": "Missing empty string validation for assets (STAmount)", + "why_false_positive": "XRPL_ASSERT(!assets.negative(), ...) validates assets (STAmount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "assets.asset() vs vault->at(sfAsset)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(assets.asset() == vault->at(sfAsset), ...) at assetsToSharesWithdraw", + "issue_pattern": "Missing empty string validation for assets.asset() vs vault->at(sfAsset)", + "why_false_positive": "XRPL_ASSERT(assets.asset() == vault->at(sfAsset), ...) validates assets.asset() vs vault->at(sfAsset) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "assets (STAmount)", + "empty", + "string", + "validation" + ], + "evidence": "if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt; at assetsToSharesWithdraw", + "issue_pattern": "Missing empty string validation for assets (STAmount)", + "why_false_positive": "if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt; validates assets (STAmount) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/ledger/helpers/VaultHelpers.cpp", + "functions": [ + { + "args": [ + "vault", + "issuance", + "assets" + ], + "lineno": 8, + "name": "assetsToSharesDeposit" + }, + { + "args": [ + "vault", + "issuance", + "shares" + ], + "lineno": 34, + "name": "sharesToAssetsDeposit" + }, + { + "args": [ + "vault", + "issuance", + "assets", + "truncate" + ], + "lineno": 60, + "name": "assetsToSharesWithdraw" + }, + { + "args": [ + "vault", + "issuance", + "shares" + ], + "lineno": 91, + "name": "sharesToAssetsWithdraw" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "These helpers are likely tested indirectly via higher-level vault/AMM/DeFi logic. Direct unit tests for these helpers may exist in files like 'VaultHelpers_test.cpp', 'Vault_test.cpp', or 'AMM_test.cpp' in the test directory. The validation paths (negative values, asset mismatches) are guarded by both XRPL_ASSERT (debug) and runtime checks (returning std::nullopt), but code coverage tools may not cover the assert-only lines (LCOV_EXCL_LINE). Gaps: If tests do not explicitly pass negative or mismatched asset types, those validation branches may not be covered. There is no evidence in this file of explicit test hooks or test-only code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), std::optional for soft validation", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely assertion failure/exception)", + "field": "assets (STAmount)", + "location": "assetsToSharesDeposit", + "validated_by": "XRPL_ASSERT(!assets.negative(), ...)", + "validates": [ + "assets must not be negative" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely assertion failure/exception)", + "field": "assets.asset() vs vault->at(sfAsset)", + "location": "assetsToSharesDeposit", + "validated_by": "XRPL_ASSERT(assets.asset() == vault->at(sfAsset), ...)", + "validates": [ + "assets asset type must match vault asset type" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "assets (STAmount)", + "location": "assetsToSharesDeposit", + "validated_by": "if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt;", + "validates": [ + "assets must not be negative", + "assets asset type must match vault asset type" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely assertion failure/exception)", + "field": "shares (STAmount)", + "location": "sharesToAssetsDeposit", + "validated_by": "XRPL_ASSERT(!shares.negative(), ...)", + "validates": [ + "shares must not be negative" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely assertion failure/exception)", + "field": "shares.asset() vs vault->at(sfShareMPTID)", + "location": "sharesToAssetsDeposit", + "validated_by": "XRPL_ASSERT(shares.asset() == vault->at(sfShareMPTID), ...)", + "validates": [ + "shares asset type must match vault share type" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "shares (STAmount)", + "location": "sharesToAssetsDeposit", + "validated_by": "if (shares.negative() || shares.asset() != vault->at(sfShareMPTID)) return std::nullopt;", + "validates": [ + "shares must not be negative", + "shares asset type must match vault share type" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely assertion failure/exception)", + "field": "assets (STAmount)", + "location": "assetsToSharesWithdraw", + "validated_by": "XRPL_ASSERT(!assets.negative(), ...)", + "validates": [ + "assets must not be negative" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely assertion failure/exception)", + "field": "assets.asset() vs vault->at(sfAsset)", + "location": "assetsToSharesWithdraw", + "validated_by": "XRPL_ASSERT(assets.asset() == vault->at(sfAsset), ...)", + "validates": [ + "assets asset type must match vault asset type" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "assets (STAmount)", + "location": "assetsToSharesWithdraw", + "validated_by": "if (assets.negative() || assets.asset() != vault->at(sfAsset)) return std::nullopt;", + "validates": [ + "assets must not be negative", + "assets asset type must match vault asset type" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/ledger/helpers/VaultHelpers.cpp.ai.md b/src/libxrpl/ledger/helpers/VaultHelpers.cpp.ai.md new file mode 100644 index 0000000000..99be164345 --- /dev/null +++ b/src/libxrpl/ledger/helpers/VaultHelpers.cpp.ai.md @@ -0,0 +1,57 @@ +# `VaultHelpers.cpp` — Asset/Share Conversion Math for XRPL Single Asset Vaults + +This file is the arithmetic core of XRPL's Single Asset Vault feature (also called the Lending Protocol). It implements four pure conversion functions that translate between a depositor's assets and the MPT-based vault shares they receive or redeem. Every `VaultDeposit` and `VaultWithdraw` transaction calls into these helpers to determine the exact exchange amounts before any ledger state is modified. + +## The Vault Share Model + +A Single Asset Vault accepts deposits of one underlying asset (XRP, an IOU, or an MPT) and mints proportional shares as an MPTokenIssuance. Each function receives two `SLE` (Serialized Ledger Entry) objects: the `vault` entry, which tracks the vault's total deposited assets (`sfAssetsTotal`) and unrealized losses (`sfLossUnrealized`), and the `issuance` entry for the share MPT, which tracks the current outstanding share supply (`sfOutstandingAmount`). The functions read these fields and perform the proportional exchange calculations. + +## Deposit vs. Withdrawal: The Loss Accounting Split + +The design choice that most distinguishes this file is the asymmetry between deposit and withdrawal functions. The deposit pair (`assetsToSharesDeposit`, `sharesToAssetsDeposit`) uses the raw `sfAssetsTotal` as the vault's total. The withdrawal pair (`assetsToSharesWithdraw`, `sharesToAssetsWithdraw`) subtracts `sfLossUnrealized` before using the total: + +```cpp +Number assetTotal = vault->at(sfAssetsTotal); +assetTotal -= vault->at(sfLossUnrealized); +``` + +This is the mechanism by which unrealized losses are socialised across all shareholders at withdrawal time. When a borrower defaults (or a loan is marked down), `sfLossUnrealized` grows. A departing depositor redeems their shares at the lower net asset value, which correctly reflects their pro-rata share of that loss. New depositors, however, are priced against the full gross `sfAssetsTotal` — their mint-in rate is based on the vault's book value rather than market value, a deliberate design choice that keeps the deposit formula consistent with vault accounting. + +## The Empty-Vault Bootstrap + +When `assetTotal == 0`, the vault has no assets and no shares can exist yet. Both deposit functions handle this case specially rather than dividing by zero: + +```cpp +if (assetTotal == 0) +{ + return STAmount{ + shares.asset(), + Number(assets.mantissa(), assets.exponent() + vault->at(sfScale)).truncate()}; +} +``` + +The `sfScale` field shifts the exponent of the incoming asset amount upward when computing the initial share allocation. This establishes the initial exchange rate as 1 asset = 10^scale shares, providing the vault operator a lever to set granularity. Scaling up the share count relative to the underlying asset reduces the impact of the classic "first depositor donation attack," where a malicious first depositor donates a tiny amount directly to inflate the share price and cause rounding losses for subsequent depositors. + +For withdrawals on an empty vault, both functions simply return zero — either zero shares or zero assets — which is correct: there is nothing to redeem. + +## Truncation Policy and `TruncateShares` + +Because share MPTs are integers (no fractional tokens), every result must be a whole number. The deposit direction always truncates: `assetsToSharesDeposit` calls `.truncate()` on the computed `Number` before returning it. This is vault-favorable — the depositor receives slightly fewer shares, and the rounding residue stays in the vault, protecting existing shareholders from dilution. + +The withdrawal direction introduces a choice via `TruncateShares`: + +```cpp +enum class TruncateShares : bool { no = false, yes = true }; +``` + +`assetsToSharesWithdraw` (converting an asset amount into the share cost for that withdrawal) truncates by default but supports `TruncateShares::yes` to force truncation. Callers use `TruncateShares::yes` when they need to find the minimum number of shares that covers a given asset withdrawal — for example, when checking if the user holds enough shares before committing the transaction. `sharesToAssetsWithdraw` (converting shares to assets) does not truncate: the user gets the full fractional asset value, which is then rounded by `STAmount` construction through XRPL's `Number` type. + +## Validation Architecture + +Each function opens with a two-tier guard. First, `XRPL_ASSERT` fires in debug builds if either the input is negative or the asset type doesn't match what the vault expects. Second, an identical runtime `if` guard immediately returns `std::nullopt` for the same conditions. This double-check pattern means the assert catches logic errors during development while the `std::nullopt` path gives the caller a clean failure signal in production — callers like `VaultDeposit::doApply` treat `nullopt` as `tecINTERNAL`. The redundant `// LCOV_EXCL_LINE` comments on the `nullopt` returns reflect that these branches are unreachable in correctly-functioning code and are intentionally excluded from coverage reports. + +The functions do not null-check the `shared_ptr` parameters themselves; `SLE` access through `->at()` is expected to succeed because callers have already fetched and validated the ledger entries in `preclaim`. This is a deliberate RAII contract: by the time these helpers are invoked, the state preconditions have been established. + +## Relationship to Callers + +`VaultDeposit::doApply` calls `assetsToSharesDeposit` before transferring any balances — the computed share count is stored first, then the actual asset transfer and MPT minting follow. `VaultWithdraw` calls both `sharesToAssetsWithdraw` (to compute the redemption value of a share-denominated withdrawal) and `assetsToSharesWithdraw` (to compute the minimum shares required for an asset-denominated withdrawal). This separation of the conversion step from the state-mutation step is deliberate: it ensures the exchange rate snapshot is taken at a single point in time and cannot drift mid-transaction. \ No newline at end of file diff --git a/src/libxrpl/net/HTTPClient.cpp.ai.json b/src/libxrpl/net/HTTPClient.cpp.ai.json new file mode 100644 index 0000000000..677800b8e8 --- /dev/null +++ b/src/libxrpl/net/HTTPClient.cpp.ai.json @@ -0,0 +1,959 @@ +{ + "args": [ + { + "lineno": 14, + "name": "sslVerifyDir" + }, + { + "lineno": 15, + "name": "sslVerifyFile" + }, + { + "lineno": 16, + "name": "sslVerify" + }, + { + "lineno": 17, + "name": "j" + }, + { + "lineno": 53, + "name": "bSSL" + }, + { + "lineno": 54, + "name": "deqSites" + }, + { + "lineno": 55, + "name": "build" + }, + { + "lineno": 56, + "name": "timeout" + }, + { + "lineno": 57, + "name": "complete" + }, + { + "lineno": 42, + "name": "strPath" + }, + { + "lineno": 43, + "name": "sb" + }, + { + "lineno": 44, + "name": "strHost" + }, + { + "lineno": 110, + "name": "ecResult" + }, + { + "lineno": 138, + "name": "result" + }, + { + "lineno": 206, + "name": "bytes_transferred" + }, + { + "lineno": 288, + "name": "iStatus" + }, + { + "lineno": 289, + "name": "strData" + }, + { + "lineno": 325, + "name": "io_context" + }, + { + "lineno": 328, + "name": "responseMax" + }, + { + "lineno": 338, + "name": "strSite" + }, + { + "lineno": 353, + "name": "setRequest" + } + ], + "classes": [ + { + "args": [ + "io_context", + "port", + "maxResponseSize", + "j" + ], + "lineno": 27, + "name": "HTTPClientImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "HTTPClient::initializeSSLContext", + "httpClientSSLContext.emplace" + ], + "entry_point": "HTTPClient::initializeSSLContext", + "purpose": "Initializes the global SSL context for HTTP clients, validating SSL parameters.", + "validation_points": [ + "httpClientSSLContext validated by std::optional emplace (ensures context is constructed with valid parameters)" + ] + }, + { + "call_chain": [ + "HTTPClient::cleanupSSLContext", + "httpClientSSLContext.reset" + ], + "entry_point": "HTTPClient::cleanupSSLContext", + "purpose": "Cleans up the global SSL context, ensuring no stale context remains.", + "validation_points": [ + "httpClientSSLContext validated by std::optional::reset (ensures context is destroyed)" + ] + }, + { + "call_chain": [ + "HTTPClientImp::request", + "HTTPClientImp::httpsNext", + "HTTPClientImp::handleResolve", + "HTTPClientImp::handleConnect", + "HTTPClientImp::handleShutdown" + ], + "entry_point": "HTTPClientImp::request", + "purpose": "Performs an HTTP(S) request, handling async resolution, connection, and shutdown.", + "validation_points": [ + "bSSL validated by type (bool)", + "httpClientSSLContext validated by httpClientSSLContext->context() dereference in HTTPClientImp constructor" + ] + }, + { + "call_chain": [ + "HTTPClientImp::get", + "HTTPClientImp::request", + "HTTPClientImp::httpsNext" + ], + "entry_point": "HTTPClientImp::get", + "purpose": "Convenience wrapper for GET requests, builds request and delegates to request().", + "validation_points": [ + "bSSL validated by type (bool)" + ] + } + ], + "data_flows": [ + { + "field": "httpClientSSLContext", + "flow": [ + "initializeSSLContext (emplace)", + "HTTPClientImp constructor (httpClientSSLContext->context())", + "used in mSocket initialization" + ], + "origin": "static std::optional httpClientSSLContext (file-scope variable)", + "transformations": [ + "Constructed with sslVerifyDir, sslVerifyFile, sslVerify, Journal", + "Dereferenced for context()" + ], + "validated_at": "initializeSSLContext (emplace), HTTPClientImp constructor (dereference)" + }, + { + "field": "bSSL", + "flow": [ + "HTTPClientImp::request (parameter)", + "assigned to mSSL", + "used in connection logic (not shown in snippet, but implied in connection handling)" + ], + "origin": "request/get function parameter", + "transformations": [ + "Direct assignment" + ], + "validated_at": "request/get (type validation: bool)" + }, + { + "field": "io_context", + "flow": [ + "passed to HTTPClientImp constructor", + "used to initialize mSocket, mResolver, mDeadline" + ], + "origin": "HTTPClientImp constructor parameter", + "transformations": [ + "No transformation, direct usage" + ], + "validated_at": "constructor parameter type" + }, + { + "field": "port", + "flow": [ + "passed to HTTPClientImp constructor", + "assigned to mPort", + "used in Query construction in httpsNext" + ], + "origin": "HTTPClientImp constructor parameter", + "transformations": [ + "converted to string for Query" + ], + "validated_at": "constructor parameter type" + }, + { + "field": "maxResponseSize", + "flow": [ + "passed to HTTPClientImp constructor", + "assigned to maxResponseSize_" + ], + "origin": "HTTPClientImp constructor parameter", + "transformations": [ + "No transformation" + ], + "validated_at": "constructor parameter type" + }, + { + "field": "Journal j", + "flow": [ + "passed to HTTPClientImp constructor", + "assigned to j_", + "used in JLOG(j_.trace())" + ], + "origin": "HTTPClientImp constructor parameter", + "transformations": [ + "No transformation" + ], + "validated_at": "constructor parameter type" + } + ], + "description": "Implements an HTTP/HTTPS client for fetching web pages, with SSL support, asynchronous operations, and timeout handling, as part of the xrpl (XRP Ledger) codebase.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "bSSL", + "validation", + "missing", + "check" + ], + "evidence": "Field bSSL validated by C++ type system, std::optional, constructor exceptions", + "issue_pattern": "Missing validation for bSSL", + "why_false_positive": "C++ type system, std::optional, constructor exceptions validates bSSL automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "deqSites", + "validation", + "missing", + "check" + ], + "evidence": "Field deqSites validated by C++ type system, std::optional, constructor exceptions", + "issue_pattern": "Missing validation for deqSites", + "why_false_positive": "C++ type system, std::optional, constructor exceptions validates deqSites automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "timeout", + "validation", + "missing", + "check" + ], + "evidence": "Field timeout validated by C++ type system, std::optional, constructor exceptions", + "issue_pattern": "Missing validation for timeout", + "why_false_positive": "C++ type system, std::optional, constructor exceptions validates timeout automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "strPath", + "validation", + "missing", + "check" + ], + "evidence": "Field strPath validated by C++ type system, std::optional, constructor exceptions", + "issue_pattern": "Missing validation for strPath", + "why_false_positive": "C++ type system, std::optional, constructor exceptions validates strPath automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "strHost", + "validation", + "missing", + "check" + ], + "evidence": "Field strHost validated by C++ type system, std::optional, constructor exceptions", + "issue_pattern": "Missing validation for strHost", + "why_false_positive": "C++ type system, std::optional, constructor exceptions validates strHost automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "io_context", + "validation", + "missing", + "check" + ], + "evidence": "Field io_context validated by C++ type system, std::optional, constructor exceptions", + "issue_pattern": "Missing validation for io_context", + "why_false_positive": "C++ type system, std::optional, constructor exceptions validates io_context automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "port", + "validation", + "missing", + "check" + ], + "evidence": "Field port validated by C++ type system, std::optional, constructor exceptions", + "issue_pattern": "Missing validation for port", + "why_false_positive": "C++ type system, std::optional, constructor exceptions validates port automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "maxResponseSize", + "validation", + "missing", + "check" + ], + "evidence": "Field maxResponseSize validated by C++ type system, std::optional, constructor exceptions", + "issue_pattern": "Missing validation for maxResponseSize", + "why_false_positive": "C++ type system, std::optional, constructor exceptions validates maxResponseSize automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Journal", + "validation", + "missing", + "check" + ], + "evidence": "Field Journal validated by C++ type system, std::optional, constructor exceptions", + "issue_pattern": "Missing validation for Journal", + "why_false_positive": "C++ type system, std::optional, constructor exceptions validates Journal automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "httpClientSSLContext", + "empty", + "string", + "validation" + ], + "evidence": "std::optional emplace at HTTPClient::initializeSSLContext", + "issue_pattern": "Missing empty string validation for httpClientSSLContext", + "why_false_positive": "std::optional emplace validates httpClientSSLContext for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "httpClientSSLContext", + "type", + "validation", + "check" + ], + "evidence": "std::optional emplace at HTTPClient::initializeSSLContext", + "issue_pattern": "Missing type validation for httpClientSSLContext", + "why_false_positive": "std::optional emplace validates httpClientSSLContext type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.6, + "detection_keywords": [ + "httpClientSSLContext", + "empty", + "string", + "validation" + ], + "evidence": "std::optional::reset at HTTPClient::cleanupSSLContext", + "issue_pattern": "Missing empty string validation for httpClientSSLContext", + "why_false_positive": "std::optional::reset validates httpClientSSLContext for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "httpClientSSLContext", + "type", + "validation", + "check" + ], + "evidence": "std::optional::reset at HTTPClient::cleanupSSLContext", + "issue_pattern": "Missing type validation for httpClientSSLContext", + "why_false_positive": "std::optional::reset validates httpClientSSLContext type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "io_context, port, maxResponseSize, Journal", + "empty", + "string", + "validation" + ], + "evidence": "constructor parameter types at HTTPClientImp::HTTPClientImp (constructor)", + "issue_pattern": "Missing empty string validation for io_context, port, maxResponseSize, Journal", + "why_false_positive": "constructor parameter types validates io_context, port, maxResponseSize, Journal for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "io_context, port, maxResponseSize, Journal", + "type", + "validation", + "check" + ], + "evidence": "constructor parameter types at HTTPClientImp::HTTPClientImp (constructor)", + "issue_pattern": "Missing type validation for io_context, port, maxResponseSize, Journal", + "why_false_positive": "constructor parameter types validates io_context, port, maxResponseSize, Journal type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "httpClientSSLContext", + "empty", + "string", + "validation" + ], + "evidence": "httpClientSSLContext->context() dereference at HTTPClientImp::HTTPClientImp (constructor)", + "issue_pattern": "Missing empty string validation for httpClientSSLContext", + "why_false_positive": "httpClientSSLContext->context() dereference validates httpClientSSLContext for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "bSSL", + "empty", + "string", + "validation" + ], + "evidence": "type (bool) at HTTPClientImp::request", + "issue_pattern": "Missing empty string validation for bSSL", + "why_false_positive": "type (bool) validates bSSL for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "bSSL", + "type", + "validation", + "check" + ], + "evidence": "type (bool) at HTTPClientImp::request", + "issue_pattern": "Missing type validation for bSSL", + "why_false_positive": "type (bool) validates bSSL type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "deqSites", + "empty", + "string", + "validation" + ], + "evidence": "type (std::deque) at HTTPClientImp::request", + "issue_pattern": "Missing empty string validation for deqSites", + "why_false_positive": "type (std::deque) validates deqSites for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "deqSites", + "type", + "validation", + "check" + ], + "evidence": "type (std::deque) at HTTPClientImp::request", + "issue_pattern": "Missing type validation for deqSites", + "why_false_positive": "type (std::deque) validates deqSites type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "timeout", + "empty", + "string", + "validation" + ], + "evidence": "type (std::chrono::seconds) at HTTPClientImp::request", + "issue_pattern": "Missing empty string validation for timeout", + "why_false_positive": "type (std::chrono::seconds) validates timeout for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "timeout", + "type", + "validation", + "check" + ], + "evidence": "type (std::chrono::seconds) at HTTPClientImp::request", + "issue_pattern": "Missing type validation for timeout", + "why_false_positive": "type (std::chrono::seconds) validates timeout type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "strPath, strHost", + "empty", + "string", + "validation" + ], + "evidence": "type (std::string) at HTTPClientImp::makeGet", + "issue_pattern": "Missing empty string validation for strPath, strHost", + "why_false_positive": "type (std::string) validates strPath, strHost for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "strPath, strHost", + "type", + "validation", + "check" + ], + "evidence": "type (std::string) at HTTPClientImp::makeGet", + "issue_pattern": "Missing type validation for strPath, strHost", + "why_false_positive": "type (std::string) validates strPath, strHost type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/net/HTTPClient.cpp", + "functions": [ + { + "args": [ + "sslVerifyDir", + "sslVerifyFile", + "sslVerify", + "j" + ], + "lineno": 13, + "name": "HTTPClient::initializeSSLContext" + }, + { + "args": [], + "lineno": 20, + "name": "HTTPClient::cleanupSSLContext" + }, + { + "args": [ + "strPath", + "sb", + "strHost" + ], + "lineno": 41, + "name": "HTTPClientImp::makeGet" + }, + { + "args": [ + "bSSL", + "deqSites", + "build", + "timeout", + "complete" + ], + "lineno": 52, + "name": "HTTPClientImp::request" + }, + { + "args": [ + "bSSL", + "deqSites", + "strPath", + "timeout", + "complete" + ], + "lineno": 67, + "name": "HTTPClientImp::get" + }, + { + "args": [], + "lineno": 82, + "name": "HTTPClientImp::httpsNext" + }, + { + "args": [ + "ecResult" + ], + "lineno": 109, + "name": "HTTPClientImp::handleDeadline" + }, + { + "args": [ + "ecResult" + ], + "lineno": 130, + "name": "HTTPClientImp::handleShutdown" + }, + { + "args": [ + "ecResult", + "result" + ], + "lineno": 137, + "name": "HTTPClientImp::handleResolve" + }, + { + "args": [ + "ecResult" + ], + "lineno": 159, + "name": "HTTPClientImp::handleConnect" + }, + { + "args": [ + "ecResult" + ], + "lineno": 186, + "name": "HTTPClientImp::handleRequest" + }, + { + "args": [ + "ecResult", + "bytes_transferred" + ], + "lineno": 205, + "name": "HTTPClientImp::handleWrite" + }, + { + "args": [ + "ecResult", + "bytes_transferred" + ], + "lineno": 222, + "name": "HTTPClientImp::handleHeader" + }, + { + "args": [ + "ecResult", + "bytes_transferred" + ], + "lineno": 266, + "name": "HTTPClientImp::handleData" + }, + { + "args": [ + "ecResult", + "iStatus", + "strData" + ], + "lineno": 287, + "name": "HTTPClientImp::invokeComplete" + }, + { + "args": [ + "bSSL", + "io_context", + "deqSites", + "port", + "strPath", + "responseMax", + "timeout", + "complete", + "j" + ], + "lineno": 324, + "name": "HTTPClient::get" + }, + { + "args": [ + "bSSL", + "io_context", + "strSite", + "port", + "strPath", + "responseMax", + "timeout", + "complete", + "j" + ], + "lineno": 337, + "name": "HTTPClient::get" + }, + { + "args": [ + "bSSL", + "io_context", + "strSite", + "port", + "setRequest", + "responseMax", + "timeout", + "complete", + "j" + ], + "lineno": 351, + "name": "HTTPClient::request" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested via integration or functional tests that exercise HTTP(S) client behavior, such as in test files named HTTPClient_test.cpp or integration tests for network fetches. However, direct unit tests for validation logic (e.g., SSL context initialization, parameter type validation) may be limited or absent. Edge cases such as invalid SSL parameters, missing context, or improper cleanup may not be fully covered unless explicitly tested in negative test cases. Exception handling paths (e.g., boost::system::system_error) may also lack direct test coverage.", + "validation_architecture": { + "auto_validated_fields": [ + "bSSL", + "deqSites", + "timeout", + "strPath", + "strHost", + "io_context", + "port", + "maxResponseSize", + "Journal" + ], + "framework": "C++ type system, std::optional, constructor exceptions", + "validation_layer": "entry_point (constructor/initialization), business_logic (SSL context usage)" + }, + "validations": [ + { + "confidence": 0.7, + "error_thrown": "std::bad_alloc or constructor exception", + "field": "httpClientSSLContext", + "location": "HTTPClient::initializeSSLContext", + "validated_by": "std::optional emplace", + "validates": [ + "Ensures HTTPClientSSLContext can be constructed with given parameters" + ], + "validation_type": "type" + }, + { + "confidence": 0.6, + "error_thrown": "none (safe reset)", + "field": "httpClientSSLContext", + "location": "HTTPClient::cleanupSSLContext", + "validated_by": "std::optional::reset", + "validates": [ + "Ensures SSL context is cleaned up and not used after reset" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "std::bad_alloc or constructor exception", + "field": "io_context, port, maxResponseSize, Journal", + "location": "HTTPClientImp::HTTPClientImp (constructor)", + "validated_by": "constructor parameter types", + "validates": [ + "Ensures correct types for constructor parameters" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "std::bad_optional_access (if not initialized)", + "field": "httpClientSSLContext", + "location": "HTTPClientImp::HTTPClientImp (constructor)", + "validated_by": "httpClientSSLContext->context() dereference", + "validates": [ + "Ensures SSL context is initialized before use" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (C++ type system)", + "field": "bSSL", + "location": "HTTPClientImp::request", + "validated_by": "type (bool)", + "validates": [ + "Ensures SSL flag is boolean" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (C++ type system)", + "field": "deqSites", + "location": "HTTPClientImp::request", + "validated_by": "type (std::deque)", + "validates": [ + "Ensures list of sites is a deque of strings" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (C++ type system)", + "field": "timeout", + "location": "HTTPClientImp::request", + "validated_by": "type (std::chrono::seconds)", + "validates": [ + "Ensures timeout is a chrono duration" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (C++ type system)", + "field": "strPath, strHost", + "location": "HTTPClientImp::makeGet", + "validated_by": "type (std::string)", + "validates": [ + "Ensures path and host are strings" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/net/HTTPClient.cpp.ai.md b/src/libxrpl/net/HTTPClient.cpp.ai.md new file mode 100644 index 0000000000..f4584e05ab --- /dev/null +++ b/src/libxrpl/net/HTTPClient.cpp.ai.md @@ -0,0 +1,45 @@ +# HTTPClient.cpp + +`HTTPClient.cpp` provides the XRP Ledger's outbound HTTP/HTTPS client — a self-contained asynchronous implementation used by the rippled process to fetch web content from external hosts. Its primary consumers include the validator list fetcher and any other subsystem that needs to retrieve data over HTTP(S) during node operation. + +## Architecture: Public Interface vs. Hidden Implementation + +The file uses a deliberate split between a thin public façade and a concrete private implementation class. `HTTPClient` in the header exposes only static factory methods; `HTTPClientImp` (defined entirely within the `.cpp` file) holds all mutable state and performs the actual work. This prevents callers from holding long-lived `HTTPClient` pointers and forces every request to be self-contained from creation through completion. + +`HTTPClientImp` doubly inherits from `std::enable_shared_from_this` and `HTTPClient`. The `shared_from_this()` capability is essential here: every async handler posted to Boost.Asio captures a `shared_ptr` to the `HTTPClientImp` instance, keeping the object alive across the entire async chain regardless of whether the original caller retains any reference. Each static `HTTPClient::get()` or `HTTPClient::request()` call creates a new `HTTPClientImp` on the heap via `make_shared`, fires the first async operation, and returns immediately. The object then sustains itself through captured `shared_ptr`s in Boost.Asio's handler queues. + +## Global SSL Context + +A module-scoped `std::optional` named `httpClientSSLContext` serves as a process-global singleton. `HTTPClient::initializeSSLContext()` constructs it with certificate verification paths and a `bool sslVerify` flag; `HTTPClient::cleanupSSLContext()` destroys it. The `HTTPClientImp` constructor dereferences this optional immediately via `httpClientSSLContext->context()`, which provides the `boost::asio::ssl::context` used to initialize the `AutoSocket`. If `initializeSSLContext()` has not been called, this dereference throws `std::bad_optional_access` — an implicit precondition that the process must satisfy before any request is issued. In practice, the server calls `initializeSSLContext()` at startup and the context lives for the entire process lifetime; `cleanupSSLContext()` is only exercised in test teardown. + +## The Async Pipeline + +Each HTTP(S) request executes as a sequential chain of Boost.Asio async operations: + +1. **`httpsNext()`** — arms the deadline timer and fires `async_resolve` on the first hostname in the sites deque. +2. **`handleResolve()`** — on successful resolution, calls `httpClientSSLContext->preConnectVerify()` to set the TLS SNI hostname before connecting, then fires `async_connect`. +3. **`handleConnect()`** — on successful connection, calls `postConnectVerify()` to install the RFC 6125 hostname verifier (using Boost.Asio's `host_name_verification`). If `mSSL` is true, fires `async_handshake`; otherwise calls `handleRequest` directly, bypassing TLS entirely. +4. **`handleRequest()`** — invokes the `mBuild` callable to write an HTTP request into `mRequest`, then fires `async_write`. +5. **`handleWrite()`** — fires `async_read_until` delimited on `"\r\n\r\n"` to capture the response headers into a 32 KB–capped `mHeader` buffer. +6. **`handleHeader()`** — parses the headers with three static `boost::regex` patterns: one for the status code, one for `Content-Length`, and one for any body bytes that arrived in the same read. If the declared `Content-Length` exceeds `maxResponseSize_`, the request is aborted with `errc::value_too_large`. If the full body is already in the header buffer, completion is invoked immediately; otherwise `async_read` retrieves the remainder. +7. **`handleData()`** — combines body bytes with any pre-read bytes and invokes the completion callback. `boost::asio::error::eof` is treated as success, since HTTP/1.0 servers signal end-of-body by closing the connection. + +## Timeout and Shutdown Semantics + +The `mDeadline` timer (`boost::asio::basic_waitable_timer`) is armed at the start of `httpsNext()` for the configured duration. If it fires before all async I/O completes, `handleDeadline()` sets `mShutdown` to `errc::bad_address`, cancels the resolver, and calls `mSocket.async_shutdown()` to tear down the connection. A single `boost::system::error_code mShutdown` field acts as a persistent error latch: once set by any handler, all subsequent handlers check it first and short-circuit to `invokeComplete()`. This avoids the complexity of cancelling in-flight operations individually and ensures only the first error is reported. + +## `AutoSocket`: Transparent SSL/Plain Dispatch + +`AutoSocket` is a thin adapter that wraps a `boost::asio::ssl::stream`. Every `async_write`, `async_read`, `async_read_until`, and `async_shutdown` call dispatches at runtime to either the SSL stream or its inner plain TCP layer depending on `mSecure`. In `HTTPClientImp`, the socket is always constructed with the SSL context (since `httpClientSSLContext->context()` is always passed), but TLS is only engaged when `mSSL` is `true` — non-SSL requests skip `async_handshake` entirely and write directly over the TCP layer. This design avoids duplicating the entire async pipeline for plain vs. SSL modes. + +## Fallback Retry Across Sites + +The `mDeqSites` deque allows the caller to supply a prioritized list of hostnames. After each `invokeComplete()` call, if the completion callback returns `true` (indicating the caller wants to continue) and the deque still has entries, `httpsNext()` is called again with the next hostname. This provides built-in failover across mirrors or CDN endpoints without requiring the caller to implement retry logic. The deque is consumed front-to-back: `invokeComplete()` pops the current entry before invoking the callback. + +## Request Generalization + +The `request()` method accepts a `std::function` callable (`mBuild`) that writes any HTTP method into the request buffer. `get()` is a convenience wrapper that binds `makeGet()` as that callable, producing a minimal HTTP/1.0 GET with `Connection: close`. This separation means POST, custom headers, or other methods can be issued through the same async pipeline by providing a different build function, without any changes to the connection or I/O layers. + +## Response Size Enforcement + +`maxResponseSize_` is enforced at two points. First, in `handleHeader()`, if `Content-Length` is present and exceeds the cap, the request aborts before reading any body. Second, if `Content-Length` is absent, `maxResponseSize_` is used as the exact number of bytes to read — the client will consume up to that many bytes and no more. This bounds both memory allocation and exposure to runaway servers. \ No newline at end of file diff --git a/src/libxrpl/net/RegisterSSLCerts.cpp.ai.json b/src/libxrpl/net/RegisterSSLCerts.cpp.ai.json new file mode 100644 index 0000000000..36f997133f --- /dev/null +++ b/src/libxrpl/net/RegisterSSLCerts.cpp.ai.json @@ -0,0 +1,459 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 11, + "name": "ec" + }, + { + "lineno": 11, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "registerSSLCerts", + "CertOpenSystemStore (Windows only)", + "X509_STORE_new", + "CertEnumCertificatesInStore (loop)", + "d2i_X509", + "X509_STORE_add_cert", + "SSL_CTX_set_cert_store" + ], + "entry_point": "registerSSLCerts", + "purpose": "Loads system SSL certificates into an OpenSSL context, validating each step to ensure proper loading and error handling.", + "validation_points": [ + "CertOpenSystemStore return value (hStore)", + "X509_STORE_new return value (store)", + "d2i_X509 return value (x509)", + "X509_STORE_add_cert return value", + "ctx.set_default_verify_paths (non-Windows)" + ] + } + ], + "data_flows": [ + { + "field": "hStore", + "flow": [ + "CertOpenSystemStore", + "unique_ptr hStore", + "used in CertEnumCertificatesInStore" + ], + "origin": "CertOpenSystemStore(0, \"ROOT\")", + "transformations": [ + "Wrapped in unique_ptr for RAII" + ], + "validated_at": "Immediately after CertOpenSystemStore call" + }, + { + "field": "store", + "flow": [ + "X509_STORE_new", + "unique_ptr store", + "passed to X509_STORE_add_cert", + "released to SSL_CTX_set_cert_store" + ], + "origin": "X509_STORE_new()", + "transformations": [ + "Wrapped in unique_ptr for RAII" + ], + "validated_at": "Immediately after X509_STORE_new call" + }, + { + "field": "x509", + "flow": [ + "d2i_X509", + "unique_ptr x509", + "passed to X509_STORE_add_cert" + ], + "origin": "d2i_X509(NULL, &pbCertEncoded, pContext->cbCertEncoded)", + "transformations": [ + "DER-encoded cert decoded to X509*" + ], + "validated_at": "Immediately after d2i_X509 call" + }, + { + "field": "X509_STORE_add_cert return value", + "flow": [ + "X509_STORE_add_cert", + "if success: x509.release()", + "if fail: warn()" + ], + "origin": "X509_STORE_add_cert(store.get(), x509.get())", + "transformations": [ + "Certificate added to store or error logged" + ], + "validated_at": "Immediately after X509_STORE_add_cert call" + }, + { + "field": "default SSL certificate paths", + "flow": [ + "ctx.set_default_verify_paths", + "ec set if error" + ], + "origin": "ctx.set_default_verify_paths(ec)", + "transformations": [ + "Sets default system certificate paths" + ], + "validated_at": "Return value of set_default_verify_paths" + } + ], + "description": "This file implements the registerSSLCerts function, which registers SSL certificates with a Boost.Asio SSL context, handling both Windows and non-Windows platforms. On Windows, it loads certificates from the system store and adds them to the OpenSSL context; on other platforms, it sets default verify paths.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "certificate store handle (hStore)", + "validation", + "missing", + "check" + ], + "evidence": "Field certificate store handle (hStore) validated by OpenSSL, Windows CryptoAPI, Boost.Asio SSL", + "issue_pattern": "Missing validation for certificate store handle (hStore)", + "why_false_positive": "OpenSSL, Windows CryptoAPI, Boost.Asio SSL validates certificate store handle (hStore) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "X509_STORE pointer (store)", + "validation", + "missing", + "check" + ], + "evidence": "Field X509_STORE pointer (store) validated by OpenSSL, Windows CryptoAPI, Boost.Asio SSL", + "issue_pattern": "Missing validation for X509_STORE pointer (store)", + "why_false_positive": "OpenSSL, Windows CryptoAPI, Boost.Asio SSL validates X509_STORE pointer (store) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "DER-encoded certificate decoding (x509)", + "validation", + "missing", + "check" + ], + "evidence": "Field DER-encoded certificate decoding (x509) validated by OpenSSL, Windows CryptoAPI, Boost.Asio SSL", + "issue_pattern": "Missing validation for DER-encoded certificate decoding (x509)", + "why_false_positive": "OpenSSL, Windows CryptoAPI, Boost.Asio SSL validates DER-encoded certificate decoding (x509) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "certificate addition to store", + "validation", + "missing", + "check" + ], + "evidence": "Field certificate addition to store validated by OpenSSL, Windows CryptoAPI, Boost.Asio SSL", + "issue_pattern": "Missing validation for certificate addition to store", + "why_false_positive": "OpenSSL, Windows CryptoAPI, Boost.Asio SSL validates certificate addition to store automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "default SSL certificate paths", + "validation", + "missing", + "check" + ], + "evidence": "Field default SSL certificate paths validated by OpenSSL, Windows CryptoAPI, Boost.Asio SSL", + "issue_pattern": "Missing validation for default SSL certificate paths", + "why_false_positive": "OpenSSL, Windows CryptoAPI, Boost.Asio SSL validates default SSL certificate paths automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hStore (certificate store handle)", + "empty", + "string", + "validation" + ], + "evidence": "CertOpenSystemStore return value at registerSSLCerts", + "issue_pattern": "Missing empty string validation for hStore (certificate store handle)", + "why_false_positive": "CertOpenSystemStore return value validates hStore (certificate store handle) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "hStore (certificate store handle)", + "type", + "validation", + "check" + ], + "evidence": "CertOpenSystemStore return value at registerSSLCerts", + "issue_pattern": "Missing type validation for hStore (certificate store handle)", + "why_false_positive": "CertOpenSystemStore return value validates hStore (certificate store handle) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "store (OpenSSL X509_STORE pointer)", + "empty", + "string", + "validation" + ], + "evidence": "X509_STORE_new return value at registerSSLCerts", + "issue_pattern": "Missing empty string validation for store (OpenSSL X509_STORE pointer)", + "why_false_positive": "X509_STORE_new return value validates store (OpenSSL X509_STORE pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "store (OpenSSL X509_STORE pointer)", + "type", + "validation", + "check" + ], + "evidence": "X509_STORE_new return value at registerSSLCerts", + "issue_pattern": "Missing type validation for store (OpenSSL X509_STORE pointer)", + "why_false_positive": "X509_STORE_new return value validates store (OpenSSL X509_STORE pointer) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "x509 (OpenSSL X509 certificate pointer)", + "empty", + "string", + "validation" + ], + "evidence": "d2i_X509 return value at registerSSLCerts (inside certificate enumeration loop)", + "issue_pattern": "Missing empty string validation for x509 (OpenSSL X509 certificate pointer)", + "why_false_positive": "d2i_X509 return value validates x509 (OpenSSL X509 certificate pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "x509 (OpenSSL X509 certificate pointer)", + "format", + "validation", + "invalid" + ], + "evidence": "d2i_X509 return value at registerSSLCerts (inside certificate enumeration loop)", + "issue_pattern": "Missing format validation for x509 (OpenSSL X509 certificate pointer)", + "why_false_positive": "d2i_X509 return value validates x509 (OpenSSL X509 certificate pointer) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "X509_STORE_add_cert return value", + "empty", + "string", + "validation" + ], + "evidence": "X509_STORE_add_cert return value at registerSSLCerts (inside certificate enumeration loop)", + "issue_pattern": "Missing empty string validation for X509_STORE_add_cert return value", + "why_false_positive": "X509_STORE_add_cert return value validates X509_STORE_add_cert return value for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "default SSL certificate paths", + "empty", + "string", + "validation" + ], + "evidence": "ctx.set_default_verify_paths at registerSSLCerts (non-Windows branch)", + "issue_pattern": "Missing empty string validation for default SSL certificate paths", + "why_false_positive": "ctx.set_default_verify_paths validates default SSL certificate paths for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/net/RegisterSSLCerts.cpp", + "functions": [ + { + "args": [ + "ctx", + "ec", + "j" + ], + "lineno": 10, + "name": "registerSSLCerts" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is platform-specific (main logic only runs on Windows). There is no direct evidence in this file of unit or integration tests. Testing would require mocking Windows APIs (CertOpenSystemStore, CertEnumCertificatesInStore) and OpenSSL APIs (X509_STORE_new, d2i_X509, X509_STORE_add_cert). Test coverage is likely limited, especially for error paths and Windows-specific logic. Tests may exist in higher-level integration tests or in files that exercise SSL context initialization, but direct unit tests for this function are unlikely unless special test harnesses are used. Non-Windows path (ctx.set_default_verify_paths) is trivial and may be covered by generic SSL context tests.", + "validation_architecture": { + "auto_validated_fields": [ + "certificate store handle (hStore)", + "X509_STORE pointer (store)", + "DER-encoded certificate decoding (x509)", + "certificate addition to store", + "default SSL certificate paths" + ], + "framework": "OpenSSL, Windows CryptoAPI, Boost.Asio SSL", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code(GetLastError(), boost::system::system_category())", + "field": "hStore (certificate store handle)", + "location": "registerSSLCerts", + "validated_by": "CertOpenSystemStore return value", + "validates": [ + "Checks if the Windows certificate store handle was successfully opened (not null)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code(static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category())", + "field": "store (OpenSSL X509_STORE pointer)", + "location": "registerSSLCerts", + "validated_by": "X509_STORE_new return value", + "validates": [ + "Checks if the OpenSSL X509_STORE was successfully created (not null)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "warn('Error decoding certificate') (logs warning, does not throw/return error)", + "field": "x509 (OpenSSL X509 certificate pointer)", + "location": "registerSSLCerts (inside certificate enumeration loop)", + "validated_by": "d2i_X509 return value", + "validates": [ + "Checks if the DER-encoded certificate can be decoded into an X509 structure" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "warn('Error adding certificate') (logs warning, does not throw/return error)", + "field": "X509_STORE_add_cert return value", + "location": "registerSSLCerts (inside certificate enumeration loop)", + "validated_by": "X509_STORE_add_cert return value", + "validates": [ + "Checks if the decoded X509 certificate can be added to the X509_STORE (e.g., not duplicate, valid cert)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "boost::system::error_code (set via ec parameter)", + "field": "default SSL certificate paths", + "location": "registerSSLCerts (non-Windows branch)", + "validated_by": "ctx.set_default_verify_paths", + "validates": [ + "Checks if the default system certificate paths can be set in the SSL context" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/net/RegisterSSLCerts.cpp.ai.md b/src/libxrpl/net/RegisterSSLCerts.cpp.ai.md new file mode 100644 index 0000000000..2b1229fd3a --- /dev/null +++ b/src/libxrpl/net/RegisterSSLCerts.cpp.ai.md @@ -0,0 +1,50 @@ +# `RegisterSSLCerts.cpp` — Bridging OS Trust Stores to OpenSSL + +`RegisterSSLCerts.cpp` solves a fundamental portability problem in SSL certificate trust: OpenSSL does not automatically consult the Windows certificate store, so TLS connections that would succeed on Linux or macOS silently fail peer verification on Windows unless the trust anchors are loaded explicitly. This single-function translation unit handles that gap while keeping non-Windows platforms on the zero-cost path provided by Boost.Asio itself. + +## The One Public Function + +`registerSSLCerts(ctx, ec, j)` populates a `boost::asio::ssl::context` with the platform's trusted root certificates. Its three parameters follow the XRPL convention: a Boost.Asio SSL context to configure, an error code to set on failure (rather than throwing), and a `beast::Journal` for structured logging. The function never throws — errors propagate through `ec` or are logged as warnings and skipped. + +## Non-Windows Path + +On Linux and macOS the entire body is a single line: + +```cpp +ctx.set_default_verify_paths(ec); +``` + +Boost.Asio delegates this to OpenSSL's `SSL_CTX_set_default_verify_paths`, which searches the standard system locations (`/etc/ssl/certs`, the directory pointed to by `SSL_CERT_DIR`, etc.). No bridging is required because OpenSSL was built to understand those locations natively. + +## Windows Path + +Windows stores its trusted root certificates in the CryptoAPI "ROOT" system store, in a format and location entirely opaque to OpenSSL. The Windows code path manually bridges the two APIs. + +**Opening the CryptoAPI store.** `CertOpenSystemStore(0, "ROOT")` retrieves a handle to the Windows root CA store. The handle is immediately wrapped in a `std::unique_ptr` with a custom deleter (`CertCloseStore`) so the store is released regardless of how the function exits. If this call fails, `GetLastError()` is translated into a `boost::system::error_code` in the `system_category` and the function returns early. + +**Creating an empty OpenSSL trust store.** `X509_STORE_new()` allocates a fresh `X509_STORE` (also wrapped in a `unique_ptr` with `X509_STORE_free`). This will accumulate the translated certificates before being installed into the SSL context. A failure here pulls the OpenSSL error via `ERR_get_error()` and assigns it to `ec` in Asio's SSL error category. + +**Iterating and translating certificates.** `CertEnumCertificatesInStore` walks every certificate in the Windows store. Each certificate's encoded bytes are in DER format — the same wire encoding OpenSSL uses — so `d2i_X509` can decode them directly without a format conversion. The decoded `X509*` is wrapped in a `unique_ptr`. Per-cert failures (decode or store-add errors) are non-fatal: the loop logs a warning via the journal, clears the OpenSSL error queue, and continues to the next certificate. This means a single corrupt or unsupported certificate in the Windows store doesn't abort the whole population process. + +**Ownership transfer on store-add.** `X509_STORE_add_cert` follows an unusual ownership convention: on success, the store takes ownership of the `X509*`. The code handles this explicitly — `x509.release()` is called only when `X509_STORE_add_cert` returns `1`, preventing the `unique_ptr` destructor from double-freeing the certificate. On failure, the `unique_ptr` retains ownership and cleans up normally. + +**Installing the store into the SSL context.** `SSL_CTX_set_cert_store` replaces the SSL context's existing trust store with the newly built one. This also transfers ownership, so `store.release()` is called to prevent the `unique_ptr` from freeing memory that is now owned by the context. + +## The Macro Collision Problem + +The file ends with a block of `#undef` directives that is easy to overlook but critical for unity builds: + +```cpp +#undef X509_NAME +#undef X509_EXTENSIONS +#undef X509_CERT_PAIR +#undef PKCS7_ISSUER_AND_SERIAL +#undef OCSP_REQUEST +#undef OCSP_RESPONSE +``` + +`` defines these names as macros, and OpenSSL uses the same names as struct identifiers and function names. Both headers are included in this translation unit, meaning the macros shadow the OpenSSL declarations. This is tolerable within a single `.cpp` file, but in a unity build — where multiple `.cpp` files are concatenated into one compilation unit — the macros would leak into subsequent files and corrupt the OpenSSL API. The `#undef`s at the bottom of the file, outside the `xrpl` namespace, surgically remove the pollution after it is no longer needed. + +## How It Is Used + +The primary caller is `HTTPClientSSLContext` (in `include/xrpl/net/HTTPClientSSLContext.h`), which calls `registerSSLCerts` in its constructor when no explicit verify-file is provided. If `registerSSLCerts` sets `ec` and no custom verify-directory is configured either, `HTTPClientSSLContext` converts the error into a `std::runtime_error` — failing fast rather than silently accepting an SSL context that cannot verify peers. \ No newline at end of file diff --git a/src/libxrpl/nodestore/BatchWriter.cpp.ai.json b/src/libxrpl/nodestore/BatchWriter.cpp.ai.json new file mode 100644 index 0000000000..e317a71c05 --- /dev/null +++ b/src/libxrpl/nodestore/BatchWriter.cpp.ai.json @@ -0,0 +1,341 @@ +{ + "args": [ + { + "lineno": 6, + "name": "callback" + }, + { + "lineno": 6, + "name": "scheduler" + }, + { + "lineno": 16, + "name": "object" + } + ], + "classes": [ + { + "args": [ + "Callback& callback", + "Scheduler& scheduler" + ], + "lineno": 6, + "name": "BatchWriter" + } + ], + "code_paths": [ + { + "call_chain": [ + "BatchWriter::store", + "BatchWriter::writeBatch" + ], + "entry_point": "BatchWriter::store", + "purpose": "Adds a NodeObject to the batch, triggers batch write if not already pending.", + "validation_points": [ + "BatchWriter::store: while (mWriteSet.size() >= batchWriteLimitSize)", + "BatchWriter::writeBatch: XRPL_ASSERT(mWriteSet.empty(), ...)" + ] + }, + { + "call_chain": [ + "BatchWriter::performScheduledTask", + "BatchWriter::writeBatch" + ], + "entry_point": "BatchWriter::performScheduledTask", + "purpose": "Executes the scheduled batch write operation.", + "validation_points": [ + "BatchWriter::writeBatch: XRPL_ASSERT(mWriteSet.empty(), ...)" + ] + }, + { + "call_chain": [ + "BatchWriter::~BatchWriter", + "BatchWriter::waitForWriting" + ], + "entry_point": "BatchWriter::~BatchWriter", + "purpose": "Ensures all pending writes are completed before destruction.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "mWriteSet", + "flow": [ + "BatchWriter::store (push_back)", + "BatchWriter::writeBatch (swap with local set)", + "m_callback.writeBatch(set)" + ], + "origin": "BatchWriter::store (push_back of NodeObject)", + "transformations": [ + "Accumulated via push_back in store", + "Swapped into local vector in writeBatch", + "Cleared (emptied) after swap" + ], + "validated_at": "BatchWriter::store (size check in while loop), BatchWriter::writeBatch (XRPL_ASSERT empty after swap)" + }, + { + "field": "mWritePending", + "flow": [ + "BatchWriter::store (set true if not pending)", + "BatchWriter::writeBatch (set false if set is empty)", + "BatchWriter::waitForWriting (waits while true)" + ], + "origin": "BatchWriter::store (set to true when scheduling)", + "transformations": [ + "Boolean flag for write-in-progress", + "Controls scheduling and wait logic" + ], + "validated_at": "Indirectly validated by logic in store, writeBatch, and waitForWriting" + }, + { + "field": "NodeObject (object)", + "flow": [ + "BatchWriter::store (input)", + "mWriteSet (push_back)", + "BatchWriter::writeBatch (swapped to local set)", + "m_callback.writeBatch(set)" + ], + "origin": "Input parameter to BatchWriter::store", + "transformations": [ + "Buffered in mWriteSet", + "Passed to callback for actual write" + ], + "validated_at": "Indirectly validated by batch size check in store" + } + ], + "description": "Implements the BatchWriter class for batching and scheduling write operations to the NodeStore in the xrpl project.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mWriteSet.size()", + "empty", + "string", + "validation" + ], + "evidence": "manual check (while loop) at store", + "issue_pattern": "Missing empty string validation for mWriteSet.size()", + "why_false_positive": "manual check (while loop) validates mWriteSet.size() for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "mWriteSet.size()", + "range", + "bounds", + "validation" + ], + "evidence": "manual check (while loop) at store", + "issue_pattern": "Missing range validation for mWriteSet.size()", + "why_false_positive": "manual check (while loop) validates mWriteSet.size() range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mWriteSet.empty()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at writeBatch", + "issue_pattern": "Missing empty string validation for mWriteSet.empty()", + "why_false_positive": "XRPL_ASSERT macro validates mWriteSet.empty() for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/BatchWriter.cpp", + "functions": [ + { + "args": [ + "Callback& callback", + "Scheduler& scheduler" + ], + "lineno": 6, + "name": "BatchWriter" + }, + { + "args": [], + "lineno": 12, + "name": "~BatchWriter" + }, + { + "args": [ + "std::shared_ptr const& object" + ], + "lineno": 16, + "name": "store" + }, + { + "args": [], + "lineno": 32, + "name": "getWriteLoad" + }, + { + "args": [], + "lineno": 40, + "name": "performScheduledTask" + }, + { + "args": [], + "lineno": 44, + "name": "writeBatch" + }, + { + "args": [], + "lineno": 77, + "name": "waitForWriting" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "NodeStore" + } + ], + "test_coverage_notes": "The code is likely tested by integration or unit tests for the NodeStore subsystem, especially those that test batch writing, concurrency, and scheduler interactions. However, specific validation paths (e.g., mWriteSet.size() limit, XRPL_ASSERT on empty set) may not be directly unit tested unless there are tests that force batch overflows or simulate scheduler delays. Edge cases such as destruction with pending writes (destructor/waitForWriting) may not be thoroughly tested unless explicitly covered in teardown/cleanup tests. Look for test files in the NodeStore or nodestore/test directories, such as BatchWriter_test.cpp or NodeStore_test.cpp. Gaps may exist in direct assertion failure testing and concurrent stress scenarios.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, XRPL_ASSERT macro)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (waits on condition variable)", + "field": "mWriteSet.size()", + "location": "store", + "validated_by": "manual check (while loop)", + "validates": [ + "batch size does not exceed batchWriteLimitSize" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts in debug)", + "field": "mWriteSet.empty()", + "location": "writeBatch", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "mWriteSet is empty after swap" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/BatchWriter.cpp.ai.md b/src/libxrpl/nodestore/BatchWriter.cpp.ai.md new file mode 100644 index 0000000000..5ed187db48 --- /dev/null +++ b/src/libxrpl/nodestore/BatchWriter.cpp.ai.md @@ -0,0 +1,33 @@ +# `BatchWriter.cpp` — Batched NodeStore Write Coalescing + +`BatchWriter` exists to solve a fundamental I/O efficiency problem: ledger processing generates a high-frequency stream of individual `NodeObject` writes, but backends like RocksDB perform far better when writes arrive in bulk. This file implements the coalescing buffer that bridges those two rates, scheduling one deferred batch flush per accumulation window rather than one disk operation per object. + +## Architecture: Task Inheritance and the Scheduler Contract + +`BatchWriter` privately inherits from `Task`, which is the `Scheduler`'s unit of deferred work. This single-dispatch interface — just `performScheduledTask()` — gives the backend-agnostic `Scheduler` a handle to invoke the flush without knowing anything about the writer's internals. In practice, the `RocksDBBackend` both owns a `BatchWriter` member and inherits `BatchWriter::Callback`, making itself the sink for the writes the batch drains into. + +The `Scheduler` is intentionally abstract; implementations range from the production thread-pool scheduler to `DummyScheduler`, which invokes the task *synchronously on the calling thread*. This is precisely why the mutex type is `std::recursive_mutex`: when `store()` holds the lock and calls `m_scheduler.scheduleTask(*this)`, a synchronous scheduler will immediately re-enter `writeBatch()` on the same thread, which then tries to re-acquire the same mutex. A plain `std::mutex` would deadlock. The recursive variant is a deliberate choice to support both synchronous and asynchronous scheduler backends. + +## The Double-Buffer Flow + +The central data structure is `mWriteSet` (a `Batch`, i.e., `std::vector>`), which accumulates objects from producer threads via `store()`. When `writeBatch()` runs, it atomically *swaps* `mWriteSet` with a local `set` under the lock, then releases the lock before calling `m_callback.writeBatch(set)`. This is the classic double-buffer pattern: the lock is held only for the O(1) swap, never during the actual I/O. After the swap, `mWriteSet` is empty and immediately available for producers to fill again while the previous batch is being written to disk. + +The inner loop in `writeBatch()` continues draining until it finds `mWriteSet` empty after a swap, at which point it clears `mWritePending` and signals `mWriteCondition`. An `XRPL_ASSERT` verifies the invariant that `mWriteSet` is indeed empty after the swap — a defensive check that guards against any future refactoring that might violate the atomicity of the swap. + +## Back-Pressure and Flow Control + +`store()` enforces an upper bound via `batchWriteLimitSize` (65,536 objects). When the pending batch reaches that ceiling, the caller blocks on `mWriteCondition`. This back-pressure prevents unbounded memory growth when the write thread falls behind producers — a critical safety valve in scenarios where disk I/O is temporarily stalled. Actual memory usage can reach roughly twice the limit because a second batch may be accumulating in `mWriteSet` while the first batch (already swapped out) is being written. + +The `mWritePending` flag ensures only one scheduler task is outstanding at a time. The first `store()` that finds the flag clear raises it and calls `scheduleTask()`; subsequent `store()` calls during the same window simply append to `mWriteSet` without re-scheduling. This single-task-at-a-time invariant is safe because `writeBatch()` loops until the buffer is empty before clearing `mWritePending`, so no objects are silently dropped. + +## Load Estimation + +`getWriteLoad()` returns `std::max(mWriteLoad, static_cast(mWriteSet.size()))`. `mWriteLoad` is set to the size of the batch at the moment of the swap and represents the in-flight write count; `mWriteSet.size()` represents the objects queued but not yet dispatched. Taking the maximum gives a conservative high-water estimate that correctly reflects pressure in both the "writing" and "accumulating" phases simultaneously. + +## Lifecycle and Shutdown Safety + +The destructor calls `waitForWriting()`, which blocks on `mWriteCondition` until `mWritePending` is false. This guarantees that no pending objects are silently abandoned when a backend is torn down — a critical correctness property for ledger data integrity. Because the condition variable is `std::condition_variable_any` (compatible with `std::recursive_mutex`), this wait integrates cleanly with the same mutex used by all other methods. + +## Performance Telemetry + +After each successful flush, `writeBatch()` records both the object count and the wall-clock duration in a `BatchWriteReport` and passes it to `m_scheduler.onBatchWrite()`. This gives the scheduler (or its owner, the production `NodeStore::Database`) a real-time view of write latency, enabling adaptive behavior or metric reporting without coupling the writer itself to any specific monitoring infrastructure. \ No newline at end of file diff --git a/src/libxrpl/nodestore/Database.cpp.ai.json b/src/libxrpl/nodestore/Database.cpp.ai.json new file mode 100644 index 0000000000..9f3617ceec --- /dev/null +++ b/src/libxrpl/nodestore/Database.cpp.ai.json @@ -0,0 +1,683 @@ +{ + "args": [ + { + "lineno": 11, + "name": "scheduler" + }, + { + "lineno": 12, + "name": "readThreads" + }, + { + "lineno": 13, + "name": "config" + }, + { + "lineno": 14, + "name": "journal" + }, + { + "lineno": 134, + "name": "dstBackend" + }, + { + "lineno": 134, + "name": "srcDB" + }, + { + "lineno": 123, + "name": "hash" + }, + { + "lineno": 123, + "name": "ledgerSeq" + }, + { + "lineno": 123, + "name": "cb" + }, + { + "lineno": 167, + "name": "fetchType" + }, + { + "lineno": 167, + "name": "duplicate" + }, + { + "lineno": 191, + "name": "obj" + } + ], + "classes": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "Section const& config", + "beast::Journal journal" + ], + "lineno": 10, + "name": "Database" + } + ], + "code_paths": [ + { + "call_chain": [ + "Database::Database" + ], + "entry_point": "Database::Database", + "purpose": "Constructs a Database object, initializes configuration, validates parameters, and spawns read threads.", + "validation_points": [ + "XRPL_ASSERT(readThreads, ...)", + "if (earliestLedgerSeq_ < 1) Throw(...)", + "if (requestBundle_ < 1 || requestBundle_ > 64) Throw(...)" + ] + }, + { + "call_chain": [ + "Database::Database", + "get(config, \"earliest_seq\", XRP_LEDGER_EARLIEST_SEQ)" + ], + "entry_point": "Database::Database", + "purpose": "Reads and validates 'earliest_seq' from config using a template-based getter.", + "validation_points": [ + "get(...) (type/range validation)", + "if (earliestLedgerSeq_ < 1) Throw(...)" + ] + }, + { + "call_chain": [ + "Database::Database", + "get(config, \"rq_bundle\", 4)" + ], + "entry_point": "Database::Database", + "purpose": "Reads and validates 'rq_bundle' from config using a template-based getter.", + "validation_points": [ + "get(...) (type/range validation)", + "if (requestBundle_ < 1 || requestBundle_ > 64) Throw(...)" + ] + }, + { + "call_chain": [ + "Database::Database", + "std::thread (lambda)", + "fetchNodeObject" + ], + "entry_point": "Database::Database", + "purpose": "Spawns read threads that process queued read requests, using validated configuration values.", + "validation_points": [ + "requestBundle_ used as loop bound (already validated)", + "XRPL_ASSERT(!it->second.empty(), ...)" + ] + } + ], + "data_flows": [ + { + "field": "readThreads", + "flow": [ + "constructor argument", + "XRPL_ASSERT(readThreads, ...)", + "readThreads_ = std::max(1, readThreads)", + "used as thread count in for loop" + ], + "origin": "Database::Database constructor argument", + "transformations": [ + "Asserted nonzero", + "Clamped to minimum 1" + ], + "validated_at": "XRPL_ASSERT(readThreads, ...)" + }, + { + "field": "earliestLedgerSeq_", + "flow": [ + "config['earliest_seq']", + "get(...)", + "earliestLedgerSeq_", + "if (earliestLedgerSeq_ < 1) Throw" + ], + "origin": "config['earliest_seq'] via get", + "transformations": [ + "Type conversion via template getter", + "Range checked for >= 1" + ], + "validated_at": "if (earliestLedgerSeq_ < 1) Throw" + }, + { + "field": "requestBundle_", + "flow": [ + "config['rq_bundle']", + "get(...)", + "requestBundle_", + "if (requestBundle_ < 1 || requestBundle_ > 64) Throw" + ], + "origin": "config['rq_bundle'] via get", + "transformations": [ + "Type conversion via template getter", + "Range checked for 1 <= x <= 64" + ], + "validated_at": "if (requestBundle_ < 1 || requestBundle_ > 64) Throw" + }, + { + "field": "read_", + "flow": [ + "populated by external code (not shown)", + "accessed in read thread lambda", + "extracted up to requestBundle_ items per loop" + ], + "origin": "Database member variable", + "transformations": [ + "Batch extraction (up to requestBundle_)" + ], + "validated_at": "requestBundle_ already validated at construction" + } + ], + "description": "Implements the xrpl::NodeStore::Database class, which manages asynchronous and batched access to a node storage backend, including thread management, async fetch, import, and statistics reporting.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "config['earliest_seq'] (type via get<> template)", + "validation", + "missing", + "check" + ], + "evidence": "Field config['earliest_seq'] (type via get<> template) validated by XRPL custom (XRPL_ASSERT, Throw<>, get<> template), possibly jss:: for JSON field names", + "issue_pattern": "Missing validation for config['earliest_seq'] (type via get<> template)", + "why_false_positive": "XRPL custom (XRPL_ASSERT, Throw<>, get<> template), possibly jss:: for JSON field names validates config['earliest_seq'] (type via get<> template) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "config['rq_bundle'] (type via get<> template)", + "validation", + "missing", + "check" + ], + "evidence": "Field config['rq_bundle'] (type via get<> template) validated by XRPL custom (XRPL_ASSERT, Throw<>, get<> template), possibly jss:: for JSON field names", + "issue_pattern": "Missing validation for config['rq_bundle'] (type via get<> template)", + "why_false_positive": "XRPL custom (XRPL_ASSERT, Throw<>, get<> template), possibly jss:: for JSON field names validates config['rq_bundle'] (type via get<> template) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "readThreads", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at Database::Database (constructor)", + "issue_pattern": "Missing empty string validation for readThreads", + "why_false_positive": "XRPL_ASSERT macro validates readThreads for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "earliestLedgerSeq_ (from config['earliest_seq'])", + "empty", + "string", + "validation" + ], + "evidence": "explicit range check and Throw at Database::Database (constructor)", + "issue_pattern": "Missing empty string validation for earliestLedgerSeq_ (from config['earliest_seq'])", + "why_false_positive": "explicit range check and Throw validates earliestLedgerSeq_ (from config['earliest_seq']) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "earliestLedgerSeq_ (from config['earliest_seq'])", + "range", + "bounds", + "validation" + ], + "evidence": "explicit range check and Throw at Database::Database (constructor)", + "issue_pattern": "Missing range validation for earliestLedgerSeq_ (from config['earliest_seq'])", + "why_false_positive": "explicit range check and Throw validates earliestLedgerSeq_ (from config['earliest_seq']) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "requestBundle_ (from config['rq_bundle'])", + "empty", + "string", + "validation" + ], + "evidence": "explicit range check and Throw at Database::Database (constructor)", + "issue_pattern": "Missing empty string validation for requestBundle_ (from config['rq_bundle'])", + "why_false_positive": "explicit range check and Throw validates requestBundle_ (from config['rq_bundle']) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "requestBundle_ (from config['rq_bundle'])", + "range", + "bounds", + "validation" + ], + "evidence": "explicit range check and Throw at Database::Database (constructor)", + "issue_pattern": "Missing range validation for requestBundle_ (from config['rq_bundle'])", + "why_false_positive": "explicit range check and Throw validates requestBundle_ (from config['rq_bundle']) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "config['earliest_seq']", + "empty", + "string", + "validation" + ], + "evidence": "get(config, \"earliest_seq\", XRP_LEDGER_EARLIEST_SEQ) at Database::Database (constructor)", + "issue_pattern": "Missing empty string validation for config['earliest_seq']", + "why_false_positive": "get(config, \"earliest_seq\", XRP_LEDGER_EARLIEST_SEQ) validates config['earliest_seq'] for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "config['earliest_seq']", + "type", + "validation", + "check" + ], + "evidence": "get(config, \"earliest_seq\", XRP_LEDGER_EARLIEST_SEQ) at Database::Database (constructor)", + "issue_pattern": "Missing type validation for config['earliest_seq']", + "why_false_positive": "get(config, \"earliest_seq\", XRP_LEDGER_EARLIEST_SEQ) validates config['earliest_seq'] type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "config['rq_bundle']", + "empty", + "string", + "validation" + ], + "evidence": "get(config, \"rq_bundle\", 4) at Database::Database (constructor)", + "issue_pattern": "Missing empty string validation for config['rq_bundle']", + "why_false_positive": "get(config, \"rq_bundle\", 4) validates config['rq_bundle'] for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "config['rq_bundle']", + "type", + "validation", + "check" + ], + "evidence": "get(config, \"rq_bundle\", 4) at Database::Database (constructor)", + "issue_pattern": "Missing type validation for config['rq_bundle']", + "why_false_positive": "get(config, \"rq_bundle\", 4) validates config['rq_bundle'] type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "it->second (data)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at Database::Database (thread lambda)", + "issue_pattern": "Missing empty string validation for it->second (data)", + "why_false_positive": "XRPL_ASSERT macro validates it->second (data) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/Database.cpp", + "functions": [ + { + "args": [ + "scheduler", + "readThreads", + "config", + "journal" + ], + "lineno": 10, + "name": "Database" + }, + { + "args": [], + "lineno": 74, + "name": "~Database" + }, + { + "args": [], + "lineno": 87, + "name": "isStopping" + }, + { + "args": [], + "lineno": 92, + "name": "stop" + }, + { + "args": [ + "hash", + "ledgerSeq", + "cb" + ], + "lineno": 123, + "name": "asyncFetch" + }, + { + "args": [ + "dstBackend", + "srcDB" + ], + "lineno": 134, + "name": "importInternal" + }, + { + "args": [ + "hash", + "ledgerSeq", + "fetchType", + "duplicate" + ], + "lineno": 167, + "name": "fetchNodeObject" + }, + { + "args": [ + "obj" + ], + "lineno": 191, + "name": "getCountsJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "NodeStore" + } + ], + "test_coverage_notes": "The validation logic is primarily in the Database constructor. Typical test coverage would be in integration or unit tests for NodeStore::Database, likely in files such as 'nodestore/Database_test.cpp' or similar. Tests should cover invalid 'earliest_seq' (e.g., 0), invalid 'rq_bundle' (e.g., 0, 65), and zero readThreads. Gaps may exist if tests do not explicitly check exception throwing for these invalid configurations, or if edge cases (e.g., negative values, missing config keys) are not tested. Template-based config getters may have their own tests elsewhere. Threading behavior is harder to test and may not be fully covered.", + "validation_architecture": { + "auto_validated_fields": [ + "config['earliest_seq'] (type via get<> template)", + "config['rq_bundle'] (type via get<> template)" + ], + "framework": "XRPL custom (XRPL_ASSERT, Throw<>, get<> template), possibly jss:: for JSON field names", + "validation_layer": "constructor (entry_point), business_logic (thread lambda)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "readThreads", + "location": "Database::Database (constructor)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "readThreads must be nonzero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error(\"Invalid earliest_seq\")", + "field": "earliestLedgerSeq_ (from config['earliest_seq'])", + "location": "Database::Database (constructor)", + "validated_by": "explicit range check and Throw", + "validates": [ + "earliestLedgerSeq_ must be >= 1" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error(\"Invalid rq_bundle\")", + "field": "requestBundle_ (from config['rq_bundle'])", + "location": "Database::Database (constructor)", + "validated_by": "explicit range check and Throw", + "validates": [ + "requestBundle_ must be >= 1 and <= 64" + ], + "validation_type": "range" + }, + { + "confidence": 0.9, + "error_thrown": "depends on get<> implementation (likely throws if type mismatch)", + "field": "config['earliest_seq']", + "location": "Database::Database (constructor)", + "validated_by": "get(config, \"earliest_seq\", XRP_LEDGER_EARLIEST_SEQ)", + "validates": [ + "config['earliest_seq'] must be convertible to std::uint32_t" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "depends on get<> implementation (likely throws if type mismatch)", + "field": "config['rq_bundle']", + "location": "Database::Database (constructor)", + "validated_by": "get(config, \"rq_bundle\", 4)", + "validates": [ + "config['rq_bundle'] must be convertible to int" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "it->second (data)", + "location": "Database::Database (thread lambda)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "data must not be empty" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/Database.cpp.ai.md b/src/libxrpl/nodestore/Database.cpp.ai.md new file mode 100644 index 0000000000..396d5ae130 --- /dev/null +++ b/src/libxrpl/nodestore/Database.cpp.ai.md @@ -0,0 +1,47 @@ +# `src/libxrpl/nodestore/Database.cpp` + +## Role in the System + +`Database.cpp` implements the non-virtual concrete behavior of the `Database` base class, which serves as XRPL's persistence layer for ledger node objects. Every piece of ledger state — accounts, transactions, ledger headers — is stored as a `NodeObject` identified by its 256-bit SHA-512/256 hash. The `Database` class abstracts over different storage backends (RocksDB, NuDB, SQLite, etc.) and provides the asynchronous read infrastructure shared by all of them. + +The file contains no backend-specific logic. It provides the thread pool management, async fetch queuing, batch import, performance instrumentation, and graceful shutdown that all concrete subclasses inherit. The actual storage and retrieval are delegated to the pure virtual `fetchNodeObject(hash, seq, fetchReport, duplicate)` and `for_each()` methods. + +## Constructor and Read Thread Pool + +The constructor validates three configuration parameters before starting the thread pool: `readThreads` (number of prefetch workers), `earliest_seq` (the minimum ledger sequence the store will serve, defaulting to `XRP_LEDGER_EARLIEST_SEQ` = 32570), and `rq_bundle` (the batch dequeue size, clamped between 1 and 64, defaulting to 4). + +Each read thread is spawned and immediately **detached** rather than stored for joining. This is a deliberate trade-off: the threads have a well-defined lifetime governed by the `readStopping_` atomic flag, and detachment avoids storing thread handles that would complicate the class layout. The downside is that shutdown must spin-wait rather than join, but the 30-second `XRPL_ASSERT` in `stop()` provides a hard upper bound on how long that can take. + +Within the thread loop, each worker acquires `readLock_`, checks for the stop signal, and if the queue is empty, calls `readCondVar_.wait()` — atomically releasing the lock and suspending. This is the standard condition-variable pattern, but the XRPL variant increments `runningThreads_` on wake and decrements before waiting, so `getCountsJson()` can distinguish threads that are actively processing from threads blocked on I/O. + +## Batched Dequeue: The `requestBundle_` Optimization + +Rather than processing one request at a time, each thread extracts up to `requestBundle_` entries from `read_` in a single lock acquisition. The comment in the code explains this clearly: the goal is to amortize mutex overhead. The default of 4 is a conservative choice that limits latency jitter while still providing meaningful throughput improvement under load. + +The extracted batch is a local `std::map>` (same type as `read_`), and the extraction uses `read_.extract()` — the C++17 node-handle operation — to move entries without copying keys or values. + +## Hash-Coalesced Async Fetch + +The `read_` map has a subtle but important invariant: the **map key is the object hash**, and the value is a vector of `(ledgerSeq, callback)` pairs. Multiple callers requesting the *same* hash result in a single map entry with multiple callbacks. When the worker thread processes that entry, it fetches the object once and then fires all callbacks. + +The `isSameDB()` virtual method provides a further optimization. When a multi-backend setup (such as `DatabaseRotatingImp` with a writable backend and an archive backend) receives requests for the same hash at different sequence numbers, the worker checks whether both sequence numbers map to the same physical backend. If they do, a single fetch result is reused for all callbacks. If they map to different backends, an additional fetch is issued for the mismatched sequence. The comment in the code flags this as an area for further optimization: grouping all requests to the same backend before issuing I/O could reduce round-trips further. + +`asyncFetch()` itself is minimal: it locks `readLock_`, inserts or appends to the `read_` entry, and signals one waiting thread via `readCondVar_.notify_one()`. It silently discards the request if `isStopping()` is already true — callers during shutdown simply get no callback. + +## Shutdown Protocol and the Derived-Class Ordering Problem + +`stop()` sets `readStopping_` under `readLock_`, clears the pending queue, broadcasts on `readCondVar_`, then spin-waits until `readThreads_` drops to zero. The threads decrement `readThreads_` on exit (not `runningThreads_`), so reaching zero guarantees all threads have fully exited. + +The header comment in the destructor calls out a critical design constraint: **any derived class must call `stop()` in its own destructor**. The read threads hold a raw pointer to `this` and will call the virtual `fetchNodeObject()` while running. If the derived class is destroyed before the threads exit, the vtable is partially dismantled and the call resolves to a destroyed object. Calling `stop()` in the derived destructor — before the derived members are torn down — is the correct fix. The base `~Database()` calls `stop()` as a safety net, but by that point the derived portion is already gone, which is why derived classes cannot rely solely on the base destructor. + +## Instrumented Fetch Wrapper + +The public `fetchNodeObject(hash, ledgerSeq, fetchType, duplicate)` is a non-virtual instrumentation shim around the private pure-virtual `fetchNodeObject(hash, ledgerSeq, fetchReport, duplicate)`. It measures wall-clock duration with `steady_clock`, accumulates `fetchDurationUs_`, counts hits via `fetchHitCount_`, tracks total bytes via `fetchSz_`, and reports the completed fetch to the `Scheduler` via `scheduler_.onFetch(fetchReport)`. The `Scheduler::onFetch()` hook allows the scheduling layer to monitor backend performance and tune task prioritization dynamically. + +## Batch Import + +`importInternal()` drives ledger data migration between databases. It iterates the source database using `for_each()` and accumulates objects into a `Batch` (a `std::vector>`), flushing to the destination `Backend::storeBatch()` every `batchWritePreallocationSize` objects. Exceptions from `storeBatch()` are caught and logged but do not abort the overall import — an intentional choice that favors partial progress over a complete rollback in what may be a long-running migration. After each flush, byte counts are accumulated via `storeStats()`. + +## Diagnostics + +`getCountsJson()` populates a JSON object with live operational metrics: read queue depth, total and running thread counts, the `rq_bundle` setting, write count and bytes, read count and bytes, cache hit counts, and total read duration in microseconds. This data surfaces through the XRPL server's `get_counts` RPC command and is valuable for diagnosing I/O bottlenecks in production nodes. \ No newline at end of file diff --git a/src/libxrpl/nodestore/DatabaseNodeImp.cpp.ai.json b/src/libxrpl/nodestore/DatabaseNodeImp.cpp.ai.json new file mode 100644 index 0000000000..c71f9acccd --- /dev/null +++ b/src/libxrpl/nodestore/DatabaseNodeImp.cpp.ai.json @@ -0,0 +1,376 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "DatabaseNodeImp::fetchNodeObject", + "backend_->fetch", + "switch(status)", + "JLOG / Rethrow" + ], + "entry_point": "DatabaseNodeImp::fetchNodeObject", + "purpose": "Fetches a single NodeObject from the backend by hash, validates backend status and handles errors.", + "validation_points": [ + "try-catch block around backend_->fetch (exception validation)", + "switch(status) (status validation)" + ] + }, + { + "call_chain": [ + "DatabaseNodeImp::fetchBatch", + "backend_->fetchBatch", + "XRPL_ASSERT on results.size() vs hashes.size()", + "results.resize", + "for loop: JLOG if !results[i]" + ], + "entry_point": "DatabaseNodeImp::fetchBatch", + "purpose": "Fetches a batch of NodeObjects by hash, validates that the number of results matches the number of input hashes.", + "validation_points": [ + "XRPL_ASSERT (results.size() == hashes.size() || results.empty())" + ] + }, + { + "call_chain": [ + "DatabaseNodeImp::store", + "NodeObject::createObject", + "backend_->store" + ], + "entry_point": "DatabaseNodeImp::store", + "purpose": "Stores a NodeObject in the backend. No explicit validation in this path.", + "validation_points": [] + }, + { + "call_chain": [ + "DatabaseNodeImp::asyncFetch", + "Database::asyncFetch" + ], + "entry_point": "DatabaseNodeImp::asyncFetch", + "purpose": "Asynchronously fetches a NodeObject. No explicit validation in this path.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "hash", + "flow": [ + "function argument", + "passed to backend_->fetch or backend_->fetchBatch", + "used as key to retrieve NodeObject", + "used in logging if errors occur" + ], + "origin": "Function argument (passed into fetchNodeObject, fetchBatch, asyncFetch, store)", + "transformations": [ + "None (passed through unchanged)" + ], + "validated_at": "Indirectly validated by backend status and by XRPL_ASSERT in fetchBatch" + }, + { + "field": "results (vector of NodeObject ptrs)", + "flow": [ + "backend_->fetchBatch", + "results", + "XRPL_ASSERT on size", + "resize to hashes.size()", + "for loop: check for missing objects", + "returned to caller" + ], + "origin": "Returned from backend_->fetchBatch(hashes).first", + "transformations": [ + "Resized to match input hashes", + "Missing entries set to nullptr" + ], + "validated_at": "XRPL_ASSERT (results.size() == hashes.size() || results.empty())" + }, + { + "field": "status", + "flow": [ + "backend_->fetch", + "status variable", + "switch(status)", + "logging/error handling" + ], + "origin": "Returned from backend_->fetch(hash, &nodeObject)", + "transformations": [ + "None (used for control flow)" + ], + "validated_at": "switch(status) in fetchNodeObject" + }, + { + "field": "nodeObject", + "flow": [ + "backend_->fetch", + "nodeObject", + "if (nodeObject) fetchReport.wasFound = true", + "returned to caller" + ], + "origin": "Output parameter from backend_->fetch", + "transformations": [ + "Set by backend_->fetch" + ], + "validated_at": "Indirectly validated by status and by null check" + } + ], + "description": "Implements the DatabaseNodeImp class methods for storing and fetching node objects from a backend in the XRPL NodeStore.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "results.size() vs hashes.size()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at fetchBatch", + "issue_pattern": "Missing empty string validation for results.size() vs hashes.size()", + "why_false_positive": "XRPL_ASSERT macro validates results.size() vs hashes.size() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "backend_->fetch(hash, &nodeObject) return status", + "empty", + "string", + "validation" + ], + "evidence": "switch-case on Status at fetchNodeObject", + "issue_pattern": "Missing empty string validation for backend_->fetch(hash, &nodeObject) return status", + "why_false_positive": "switch-case on Status validates backend_->fetch(hash, &nodeObject) return status for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "backend_->fetch(hash, &nodeObject) exception", + "empty", + "string", + "validation" + ], + "evidence": "try-catch block at fetchNodeObject", + "issue_pattern": "Missing empty string validation for backend_->fetch(hash, &nodeObject) exception", + "why_false_positive": "try-catch block validates backend_->fetch(hash, &nodeObject) exception for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "backend_->fetch(hash, &nodeObject) exception", + "type", + "validation", + "check" + ], + "evidence": "try-catch block at fetchNodeObject", + "issue_pattern": "Missing type validation for backend_->fetch(hash, &nodeObject) exception", + "why_false_positive": "try-catch block validates backend_->fetch(hash, &nodeObject) exception type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/DatabaseNodeImp.cpp", + "functions": [ + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash", + "std::uint32_t" + ], + "lineno": 6, + "name": "DatabaseNodeImp::store" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t ledgerSeq", + "std::function const&)>&& callback" + ], + "lineno": 13, + "name": "DatabaseNodeImp::asyncFetch" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t", + "FetchReport& fetchReport", + "bool duplicate" + ], + "lineno": 20, + "name": "DatabaseNodeImp::fetchNodeObject" + }, + { + "args": [ + "std::vector const& hashes" + ], + "lineno": 48, + "name": "DatabaseNodeImp::fetchBatch" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "NodeStore" + } + ], + "test_coverage_notes": "The code is likely covered by unit tests in the NodeStore or DatabaseNodeImp test suites, such as 'nodestore/DatabaseNodeImp_test.cpp', 'nodestore/Backend_test.cpp', or integration tests that exercise ledger fetch/store paths. Validation logic (e.g., XRPL_ASSERT, status switch, exception handling) may not be directly tested unless tests explicitly simulate backend errors, mismatched batch sizes, or corrupt data. Gaps may exist in testing exception handling and assertion failures, as these are often harder to trigger in standard test cases. There may also be limited coverage for edge cases where the backend returns more/fewer results than requested, or throws exceptions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro, manual switch-case, try-catch", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely throws or aborts)", + "field": "results.size() vs hashes.size()", + "location": "fetchBatch", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures that the number of output NodeObjects from backend_->fetchBatch matches the number of input hashes, or is empty" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "logs error, may throw via Rethrow() on exception", + "field": "backend_->fetch(hash, &nodeObject) return status", + "location": "fetchNodeObject", + "validated_by": "switch-case on Status", + "validates": [ + "Checks for dataCorrupt status and logs fatal error", + "Handles unknown status with warning log" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rethrows caught std::exception", + "field": "backend_->fetch(hash, &nodeObject) exception", + "location": "fetchNodeObject", + "validated_by": "try-catch block", + "validates": [ + "Catches and logs any std::exception thrown by backend_->fetch" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/DatabaseNodeImp.cpp.ai.md b/src/libxrpl/nodestore/DatabaseNodeImp.cpp.ai.md new file mode 100644 index 0000000000..009a31a4df --- /dev/null +++ b/src/libxrpl/nodestore/DatabaseNodeImp.cpp.ai.md @@ -0,0 +1,35 @@ +# `DatabaseNodeImp.cpp` — Single-Backend NodeStore Implementation + +## Role in the System + +`DatabaseNodeImp` is the concrete, single-backend implementation of the abstract `Database` class in the XRPL NodeStore subsystem. The NodeStore is the persistence layer for every ledger object in the XRP Ledger: account states, transactions, and all other ledger nodes are serialized as `NodeObject` values keyed by their 256-bit SHA-512 hash. This file provides the four core operations — `store`, `asyncFetch`, `fetchNodeObject`, and `fetchBatch` — that sit between the higher-level ledger machinery and a pluggable `Backend` (NuDB, RocksDB, in-memory, etc.). + +The counterpart class `DatabaseRotatingImp` handles a two-backend rotation scheme used for the historical shard store. `DatabaseNodeImp` is the simpler, single-backend path used for the primary node database. The architectural split is clean: routing logic by ledger sequence only appears in the rotating variant; here `isSameDB()` always returns `true` and the `ledgerSeq` parameters in `store()` and `fetchNodeObject()` are intentionally unnamed and ignored. + +## Store Path + +`store()` begins by calling `storeStats()` with a count of 1 and the blob's byte size before the data is moved. This ordering matters: because `data` is moved into `NodeObject::createObject()`, the size must be captured first. Once the `NodeObject` is constructed from the type, moved blob, and hash, it is handed to `backend_->store()`. The function signature accepts a `std::uint32_t` ledger sequence that is discarded — the single backend receives everything regardless of which ledger an object belongs to. + +## Single-Object Fetch + +`fetchNodeObject()` is the private virtual override called by the base class's thread pool machinery. It wraps `backend_->fetch()` in a try-catch: if the backend throws any `std::exception`, a fatal log entry is emitted with the hash and exception message before `Rethrow()` re-raises the exception to propagate it up through the calling thread. This makes backend I/O failures loudly visible while still letting the crash propagate correctly rather than swallowing exceptions. + +The `Status` returned by `backend_->fetch()` is inspected in a switch. Both `ok` and `notFound` are silent — a missing node is a normal condition during ledger history traversal. `dataCorrupt` gets a `fatal`-level journal entry, reflecting that corruption in the node store is a ledger integrity failure requiring operator attention. Any unknown status codes produce a `warn` log but do not abort. Only if the output `nodeObject` pointer was actually populated is `fetchReport.wasFound` set to `true`, which feeds the base class's hit-count metric. + +## Batch Fetch + +`fetchBatch()` is a public method that bypasses the async read queue and calls `backend_->fetchBatch()` directly. The result is a `pair>, Status>` from which only `.first` is used — the batch-level status is not checked separately. + +After the call, an `XRPL_ASSERT` enforces that the backend returned either exactly as many results as input hashes or an empty vector. This guards against a buggy backend returning a partial batch that would silently misalign position-keyed results. Following the assertion, `results.resize(hashes.size())` normalizes an empty-vector response (which some backends return when no objects are found) into a correctly sized vector of nullptrs, preserving the positional contract: `results[i]` corresponds to `hashes[i]`. Any null slot is then logged at `error` level, making cache misses or missing history visible in diagnostics without throwing. + +The entire batch fetch is bracketed by `steady_clock` timestamps, and the microsecond duration is fed to `updateFetchMetrics()` alongside the fetch count. This gives the base class's monitoring layer accurate latency data for operator dashboards. + +## Async Fetch + +`asyncFetch()` simply delegates to `Database::asyncFetch()`. The base class owns a thread pool whose workers dequeue hash-lookup requests and call the private `fetchNodeObject()` override. `DatabaseNodeImp` has nothing to add here — no per-backend scheduling considerations — so the delegation is unconditional. + +## Construction and Invariants + +The constructor immediately asserts `backend_` is non-null via `XRPL_ASSERT`, treating a null backend as a programming error. This is appropriate: a `DatabaseNodeImp` without a backend can serve no purpose, and failing loudly at construction time is preferable to a null-dereference deep in a fetch path. The destructor calls `stop()` (inherited from `Database`) to drain the async read queue before the backend is destroyed, preventing use-after-free in the reader threads. + +All `shared_ptr` usage is consistent with XRPL's general pattern: `NodeObject` instances are reference-counted because the same object may be simultaneously referenced by an in-flight read callback, the ledger cache, and a pending write — no single owner exists at the call site level. \ No newline at end of file diff --git a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp.ai.json b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp.ai.json new file mode 100644 index 0000000000..82e09c4d6b --- /dev/null +++ b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp.ai.json @@ -0,0 +1,752 @@ +{ + "args": [ + { + "lineno": 7, + "name": "scheduler" + }, + { + "lineno": 8, + "name": "readThreads" + }, + { + "lineno": 9, + "name": "writableBackend" + }, + { + "lineno": 10, + "name": "archiveBackend" + }, + { + "lineno": 11, + "name": "config" + }, + { + "lineno": 12, + "name": "j" + }, + { + "lineno": 20, + "name": "newBackend" + }, + { + "lineno": 21, + "name": "f" + }, + { + "lineno": 52, + "name": "source" + }, + { + "lineno": 65, + "name": "type" + }, + { + "lineno": 65, + "name": "data" + }, + { + "lineno": 65, + "name": "hash" + }, + { + "lineno": 75, + "name": "fetchReport" + }, + { + "lineno": 75, + "name": "duplicate" + } + ], + "classes": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "std::shared_ptr writableBackend", + "std::shared_ptr archiveBackend", + "Section const& config", + "beast::Journal j" + ], + "lineno": 6, + "name": "DatabaseRotatingImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "DatabaseRotatingImp::DatabaseRotatingImp" + ], + "entry_point": "DatabaseRotatingImp::DatabaseRotatingImp", + "purpose": "Constructor initializes writableBackend_ and archiveBackend_, validates them via implicit null checks, and accumulates fdRequired_.", + "validation_points": [ + "if (writableBackend_)", + "if (archiveBackend_)" + ] + }, + { + "call_chain": [ + "DatabaseRotatingImp::rotate", + "newBackend->getName()", + "archiveBackend_->setDeletePath()", + "archiveBackend_->getName()", + "writableBackend_->getName()", + "f(newWritableBackendName, newArchiveBackendName)" + ], + "entry_point": "DatabaseRotatingImp::rotate", + "purpose": "Rotates the backends, moving writable to archive, new to writable, and calls a callback with their names.", + "validation_points": [ + "newBackend->getName() (validates newBackend)", + "archiveBackend_->setDeletePath() (validates archiveBackend_)", + "archiveBackend_->getName() (validates archiveBackend_)" + ] + }, + { + "call_chain": [ + "DatabaseRotatingImp::getName", + "writableBackend_->getName()" + ], + "entry_point": "DatabaseRotatingImp::getName", + "purpose": "Returns the name of the current writable backend.", + "validation_points": [ + "writableBackend_->getName() (validates writableBackend_)" + ] + }, + { + "call_chain": [ + "DatabaseRotatingImp::getWriteLoad", + "writableBackend_->getWriteLoad()" + ], + "entry_point": "DatabaseRotatingImp::getWriteLoad", + "purpose": "Returns the write load of the current writable backend.", + "validation_points": [ + "writableBackend_->getWriteLoad() (validates writableBackend_)" + ] + }, + { + "call_chain": [ + "DatabaseRotatingImp::importDatabase", + "writableBackend_ (captured under lock)", + "importInternal(*backend, source)" + ], + "entry_point": "DatabaseRotatingImp::importDatabase", + "purpose": "Imports data from another database into the current writable backend.", + "validation_points": [ + "writableBackend_ (captured under lock, implicitly validated)" + ] + }, + { + "call_chain": [ + "DatabaseRotatingImp::sync", + "writableBackend_->sync()" + ], + "entry_point": "DatabaseRotatingImp::sync", + "purpose": "Synchronizes the writable backend.", + "validation_points": [ + "writableBackend_->sync() (validates writableBackend_)" + ] + }, + { + "call_chain": [ + "DatabaseRotatingImp::store", + "NodeObject::createObject", + "writableBackend_ (captured under lock)", + "backend->store(nObj)", + "storeStats" + ], + "entry_point": "DatabaseRotatingImp::store", + "purpose": "Stores a node object in the writable backend.", + "validation_points": [ + "writableBackend_ (captured under lock, implicitly validated)" + ] + }, + { + "call_chain": [ + "DatabaseRotatingImp::fetchNodeObject", + "fetch(writableBackend_)", + "fetch(archiveBackend_)" + ], + "entry_point": "DatabaseRotatingImp::fetchNodeObject", + "purpose": "Fetches a node object from the writable or archive backend.", + "validation_points": [ + "fetch(writableBackend_) (validates writableBackend_)", + "fetch(archiveBackend_) (validates archiveBackend_)" + ] + } + ], + "data_flows": [ + { + "field": "writableBackend_", + "flow": [ + "constructor argument", + "assigned to writableBackend_", + "used in rotate, getName, getWriteLoad, importDatabase, sync, store, fetchNodeObject" + ], + "origin": "Constructor argument (std::shared_ptr writableBackend)", + "transformations": [ + "Moved to archiveBackend_ in rotate", + "Replaced by newBackend in rotate" + ], + "validated_at": "Constructor (if (writableBackend_)), method calls (e.g., writableBackend_->getName())" + }, + { + "field": "archiveBackend_", + "flow": [ + "constructor argument", + "assigned to archiveBackend_", + "used in rotate, fetchNodeObject" + ], + "origin": "Constructor argument (std::shared_ptr archiveBackend)", + "transformations": [ + "setDeletePath() called in rotate", + "Replaced by writableBackend_ in rotate" + ], + "validated_at": "Constructor (if (archiveBackend_)), method calls (e.g., archiveBackend_->setDeletePath())" + }, + { + "field": "newBackend (rotate argument)", + "flow": [ + "rotate argument", + "getName() called for validation", + "assigned to writableBackend_" + ], + "origin": "rotate(std::unique_ptr&& newBackend)", + "transformations": [ + "Moved into writableBackend_" + ], + "validated_at": "rotate (newBackend->getName())" + }, + { + "field": "fdRequired_", + "flow": [ + "initialized to 0", + "incremented by writableBackend_->fdRequired() if writableBackend_", + "incremented by archiveBackend_->fdRequired() if archiveBackend_" + ], + "origin": "Member variable, initialized in constructor", + "transformations": [ + "Sum of fdRequired() from both backends" + ], + "validated_at": "Constructor (if (writableBackend_), if (archiveBackend_))" + }, + { + "field": "NodeObject", + "flow": [ + "store() argument", + "NodeObject::createObject", + "backend->store(nObj)" + ], + "origin": "Created in store() via NodeObject::createObject", + "transformations": [ + "Created from type, data, hash" + ], + "validated_at": "N/A (creation is not validated here)" + } + ], + "description": "Implements the DatabaseRotatingImp class, which manages a rotating pair of backends (writable and archive) for storing and retrieving NodeObjects in the XRPL node store, supporting backend rotation, thread safety, and data migration.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "writableBackend_ (constructor argument)", + "empty", + "string", + "validation" + ], + "evidence": "implicit null check (if (writableBackend_)) at DatabaseRotatingImp::DatabaseRotatingImp (constructor)", + "issue_pattern": "Missing empty string validation for writableBackend_ (constructor argument)", + "why_false_positive": "implicit null check (if (writableBackend_)) validates writableBackend_ (constructor argument) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "writableBackend_ (constructor argument)", + "type", + "validation", + "check" + ], + "evidence": "implicit null check (if (writableBackend_)) at DatabaseRotatingImp::DatabaseRotatingImp (constructor)", + "issue_pattern": "Missing type validation for writableBackend_ (constructor argument)", + "why_false_positive": "implicit null check (if (writableBackend_)) validates writableBackend_ (constructor argument) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "archiveBackend_ (constructor argument)", + "empty", + "string", + "validation" + ], + "evidence": "implicit null check (if (archiveBackend_)) at DatabaseRotatingImp::DatabaseRotatingImp (constructor)", + "issue_pattern": "Missing empty string validation for archiveBackend_ (constructor argument)", + "why_false_positive": "implicit null check (if (archiveBackend_)) validates archiveBackend_ (constructor argument) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "archiveBackend_ (constructor argument)", + "type", + "validation", + "check" + ], + "evidence": "implicit null check (if (archiveBackend_)) at DatabaseRotatingImp::DatabaseRotatingImp (constructor)", + "issue_pattern": "Missing type validation for archiveBackend_ (constructor argument)", + "why_false_positive": "implicit null check (if (archiveBackend_)) validates archiveBackend_ (constructor argument) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "newBackend (rotate argument)", + "empty", + "string", + "validation" + ], + "evidence": "dereference and method call (newBackend->getName()) at DatabaseRotatingImp::rotate", + "issue_pattern": "Missing empty string validation for newBackend (rotate argument)", + "why_false_positive": "dereference and method call (newBackend->getName()) validates newBackend (rotate argument) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "newBackend (rotate argument)", + "type", + "validation", + "check" + ], + "evidence": "dereference and method call (newBackend->getName()) at DatabaseRotatingImp::rotate", + "issue_pattern": "Missing type validation for newBackend (rotate argument)", + "why_false_positive": "dereference and method call (newBackend->getName()) validates newBackend (rotate argument) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "archiveBackend_ (member)", + "empty", + "string", + "validation" + ], + "evidence": "dereference and method call (archiveBackend_->setDeletePath()) at DatabaseRotatingImp::rotate", + "issue_pattern": "Missing empty string validation for archiveBackend_ (member)", + "why_false_positive": "dereference and method call (archiveBackend_->setDeletePath()) validates archiveBackend_ (member) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "archiveBackend_ (member)", + "type", + "validation", + "check" + ], + "evidence": "dereference and method call (archiveBackend_->setDeletePath()) at DatabaseRotatingImp::rotate", + "issue_pattern": "Missing type validation for archiveBackend_ (member)", + "why_false_positive": "dereference and method call (archiveBackend_->setDeletePath()) validates archiveBackend_ (member) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "writableBackend_ (member)", + "empty", + "string", + "validation" + ], + "evidence": "dereference and method call (writableBackend_->getName(), writableBackend_->getWriteLoad(), writableBackend_->sync()) at DatabaseRotatingImp::getName, getWriteLoad, sync, importDatabase, store, fetchNodeObject", + "issue_pattern": "Missing empty string validation for writableBackend_ (member)", + "why_false_positive": "dereference and method call (writableBackend_->getName(), writableBackend_->getWriteLoad(), writableBackend_->sync()) validates writableBackend_ (member) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "writableBackend_ (member)", + "type", + "validation", + "check" + ], + "evidence": "dereference and method call (writableBackend_->getName(), writableBackend_->getWriteLoad(), writableBackend_->sync()) at DatabaseRotatingImp::getName, getWriteLoad, sync, importDatabase, store, fetchNodeObject", + "issue_pattern": "Missing type validation for writableBackend_ (member)", + "why_false_positive": "dereference and method call (writableBackend_->getName(), writableBackend_->getWriteLoad(), writableBackend_->sync()) validates writableBackend_ (member) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "type, data, hash (store arguments)", + "empty", + "string", + "validation" + ], + "evidence": "NodeObject::createObject(type, std::move(data), hash) at DatabaseRotatingImp::store", + "issue_pattern": "Missing empty string validation for type, data, hash (store arguments)", + "why_false_positive": "NodeObject::createObject(type, std::move(data), hash) validates type, data, hash (store arguments) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/DatabaseRotatingImp.cpp", + "functions": [ + { + "args": [ + "Scheduler& scheduler", + "int readThreads", + "std::shared_ptr writableBackend", + "std::shared_ptr archiveBackend", + "Section const& config", + "beast::Journal j" + ], + "lineno": 6, + "name": "DatabaseRotatingImp" + }, + { + "args": [ + "std::unique_ptr&& newBackend", + "std::function const& f" + ], + "lineno": 19, + "name": "rotate" + }, + { + "args": [], + "lineno": 39, + "name": "getName" + }, + { + "args": [], + "lineno": 45, + "name": "getWriteLoad" + }, + { + "args": [ + "Database& source" + ], + "lineno": 51, + "name": "importDatabase" + }, + { + "args": [], + "lineno": 59, + "name": "sync" + }, + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash", + "std::uint32_t" + ], + "lineno": 64, + "name": "store" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t", + "FetchReport& fetchReport", + "bool duplicate" + ], + "lineno": 73, + "name": "fetchNodeObject" + }, + { + "args": [ + "std::function)> f" + ], + "lineno": 124, + "name": "for_each" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "NodeStore" + } + ], + "test_coverage_notes": "The code is likely tested by integration and backend rotation tests in the rippled codebase, such as those in 'src/test/nodestore/' (e.g., DatabaseRotating_test.cpp, Backend_test.cpp). However, not all validation paths (e.g., null pointer dereferences, error handling in rotate or fetchNodeObject) may be directly tested. Exception handling paths (e.g., backend->fetch throwing) may not be fully covered unless explicitly tested for error injection. There is no evidence of explicit unit tests for validation of constructor arguments or rotate argument nullness; these may rely on integration tests or higher-level system tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation, C++ type system, exceptions)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "none (no exception, just skips fdRequired_ increment)", + "field": "writableBackend_ (constructor argument)", + "location": "DatabaseRotatingImp::DatabaseRotatingImp (constructor)", + "validated_by": "implicit null check (if (writableBackend_))", + "validates": [ + "checks if writableBackend_ is not null before using" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (no exception, just skips fdRequired_ increment)", + "field": "archiveBackend_ (constructor argument)", + "location": "DatabaseRotatingImp::DatabaseRotatingImp (constructor)", + "validated_by": "implicit null check (if (archiveBackend_))", + "validates": [ + "checks if archiveBackend_ is not null before using" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "std::bad_function_call or std::runtime_error (if null)", + "field": "newBackend (rotate argument)", + "location": "DatabaseRotatingImp::rotate", + "validated_by": "dereference and method call (newBackend->getName())", + "validates": [ + "assumes newBackend is not null; will throw if null" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "std::runtime_error (if null)", + "field": "archiveBackend_ (member)", + "location": "DatabaseRotatingImp::rotate", + "validated_by": "dereference and method call (archiveBackend_->setDeletePath())", + "validates": [ + "assumes archiveBackend_ is not null; will throw if null" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "std::runtime_error (if null)", + "field": "writableBackend_ (member)", + "location": "DatabaseRotatingImp::getName, getWriteLoad, sync, importDatabase, store, fetchNodeObject", + "validated_by": "dereference and method call (writableBackend_->getName(), writableBackend_->getWriteLoad(), writableBackend_->sync())", + "validates": [ + "assumes writableBackend_ is not null; will throw if null" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "exception from NodeObject::createObject (likely std::invalid_argument or custom)", + "field": "type, data, hash (store arguments)", + "location": "DatabaseRotatingImp::store", + "validated_by": "NodeObject::createObject(type, std::move(data), hash)", + "validates": [ + "validates type, data, and hash for NodeObject creation" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/DatabaseRotatingImp.cpp.ai.md b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp.ai.md new file mode 100644 index 0000000000..2c8e6d77f1 --- /dev/null +++ b/src/libxrpl/nodestore/DatabaseRotatingImp.cpp.ai.md @@ -0,0 +1,56 @@ +# `DatabaseRotatingImp.cpp` — Concrete Rotating Node-Store Backend + +## Role in the System + +`DatabaseRotatingImp` is the concrete implementation that powers XRPL's **online deletion** feature. The broader mechanism is built around the insight that a running node cannot compact or delete ledger data from a single live database, because reads and writes to that database are continuous. The solution is a two-backend architecture: one backend is _writable_ (receiving all new stores) and one is _archive_ (holding older data). When enough time has elapsed and old ledgers have been validated beyond the configured deletion threshold, the archive backend is discarded and a freshly-created backend becomes the new writable target — the previous writable is demoted to archive in turn. This file contains the implementation of the class that makes that lifecycle work, sitting between the `SHAMapStore` sweep thread and the raw `Backend` storage layer. + +The class inherits from `DatabaseRotating`, which itself extends `Database` — the general XRPL node-store abstraction. The two members `writableBackend_` and `archiveBackend_` are `std::shared_ptr`, allowing them to be atomically replaced while outstanding I/O operations on the old handle remain valid for the lifetime of the shared reference. + +## The Rotation Protocol + +`rotate()` is the heart of the class and the subtlest method. It takes a `std::unique_ptr&&` (the freshly prepared replacement) and a callback that will receive the resulting backend names. The sequence inside the mutex is: + +1. Mark the existing archive backend for deletion on destruction (`setDeletePath()`), then move it out into a local `oldArchiveBackend`. +2. Promote the current writable backend to become the new archive. +3. Install `newBackend` as the writable backend. + +The lock is then **released before the callback runs**. This is intentional: the callback (in production, `SHAMapStoreImp`) persists the new writable/archive names to a SQLite state database. That disk write must happen while `oldArchiveBackend` is still alive — held by the local variable on the stack — so the old archive directory is not deleted until after the state database is updated. The `oldArchiveBackend` shared_ptr falls out of scope only after `f()` returns, at which point its destructor fires and cleans up the on-disk data. This sequencing makes the rotation crash-safe: if the process dies between the atomic swap and the state persistence, the node can recover by reading back the previous state from SQLite. + +A naming subtlety: `newWritableBackendName` is captured _before_ acquiring the lock (by calling `getName()` on the new backend), and `newArchiveBackendName` is captured _inside_ the lock (by reading the demoted former writable). Both values are passed to the callback so it can update persistent state without needing to re-query under lock. + +## Thread-Safety Pattern: Capture Under Lock, Use Outside Lock + +Every operation that touches either backend follows the same pattern: acquire the mutex, copy the `shared_ptr` into a local variable, release the lock, then call the backend through the local. For example in `store()`: + +```cpp +auto const backend = [&] { + std::lock_guard const lock(mutex_); + return writableBackend_; +}(); +backend->store(nObj); +``` + +This is not an oversight — it is deliberate. The lock only needs to protect the pointer swap, not the entire backend I/O operation, which may block. Holding the mutex across a disk write would serialize all readers and writers, eliminating concurrency. By capturing the `shared_ptr` under the lock and releasing before I/O, the code achieves safe pointer visibility without blocking unrelated threads. + +The exception is `sync()`, which does hold the lock for the entire backend sync call. This is acceptable because `sync()` is a maintenance operation, not a latency-sensitive read/write path. + +## Fetch Promotion + +`fetchNodeObject()` implements a two-tier lookup with optional write-back. It first tries the writable backend, then falls back to the archive backend. If the object is found only in the archive _and_ the `duplicate` flag is true, the object is copied back into the writable backend: + +```cpp +if (duplicate) + writable->store(nodeObject); +``` + +This promotion matters because recently-fetched archive objects are likely to be accessed again, and having them in the writable tier reduces future archive lookups. After the archive fetch succeeds, the code re-acquires the mutex to refresh the `writable` local pointer. This handles the race where a rotation occurs between when the archive lookup started and when the write-back is about to happen — without the refresh, the write-back would land in the now-demoted (and soon-to-be-deleted) former writable backend rather than the current one. + +The `fetchNodeObject` inner `fetch` lambda catches `std::exception`, logs it at fatal severity, and rethrows via `Rethrow()`. Backend corruption (`dataCorrupt` status) is logged fatally but does not throw — the null `nodeObject` propagates up as a cache miss. Unknown statuses are logged at warning level. This conservative approach avoids crashing on correctable backend errors while surfacing data corruption prominently. + +## File Descriptor Accounting + +The constructor accumulates `fdRequired_` by querying each backend for its file descriptor needs, guarded by null checks. This aggregate is surfaced to the system-level resource validator so the process can request enough file descriptors from the OS before opening any backend. + +## Relationship to `SHAMapStoreImp` + +The real orchestration of when to rotate, how to build `newBackend`, and how to clear in-memory caches lives in `SHAMapStoreImp`. `DatabaseRotatingImp` is deliberately narrow: it manages the thread-safe pointer swap and the deferred deletion lifecycle, but delegates policy entirely to its caller through the callback interface. This separation means the rotation logic can be tested in isolation — the tests in `SHAMapStore_test.cpp` create a `DatabaseRotatingImp` directly and call `rotate()` with custom callbacks, without needing a full `SHAMapStoreImp`. \ No newline at end of file diff --git a/src/libxrpl/nodestore/DecodedBlob.cpp.ai.json b/src/libxrpl/nodestore/DecodedBlob.cpp.ai.json new file mode 100644 index 0000000000..6b7c099515 --- /dev/null +++ b/src/libxrpl/nodestore/DecodedBlob.cpp.ai.json @@ -0,0 +1,396 @@ +{ + "args": [ + { + "lineno": 8, + "name": "key" + }, + { + "lineno": 8, + "name": "value" + }, + { + "lineno": 8, + "name": "valueBytes" + } + ], + "classes": [ + { + "args": [ + "void const* key", + "void const* value", + "int valueBytes" + ], + "lineno": 7, + "name": "DecodedBlob" + } + ], + "code_paths": [ + { + "call_chain": [ + "DecodedBlob::DecodedBlob" + ], + "entry_point": "DecodedBlob::DecodedBlob", + "purpose": "Constructs a DecodedBlob from raw key/value data, validates and parses the object type and data pointer.", + "validation_points": [ + "if (valueBytes > 8) { ... safe_cast(byte[8]); }", + "if (valueBytes > 9) { ... switch (m_objectType) { ... m_success = true; } }" + ] + }, + { + "call_chain": [ + "DecodedBlob::createObject", + "NodeObject::createObject" + ], + "entry_point": "DecodedBlob::createObject", + "purpose": "Creates a NodeObject from the decoded blob, only if validation (m_success) passed.", + "validation_points": [ + "XRPL_ASSERT(m_success, ...)", + "if (m_success) { ... }" + ] + } + ], + "data_flows": [ + { + "field": "valueBytes", + "flow": [ + "DecodedBlob::DecodedBlob(valueBytes)", + "used in if (valueBytes > 8) and if (valueBytes > 9)", + "used to compute m_dataBytes = max(0, valueBytes - 9)" + ], + "origin": "DecodedBlob constructor argument", + "transformations": [ + "Checked for >8 and >9 to validate presence of type and data", + "Used to calculate m_dataBytes" + ], + "validated_at": "if (valueBytes > 8), if (valueBytes > 9)" + }, + { + "field": "m_objectType", + "flow": [ + "byte[8] from value", + "safe_cast(byte[8])", + "assigned to m_objectType", + "used in switch (m_objectType) to set m_success" + ], + "origin": "byte[8] of value (after safe_cast)", + "transformations": [ + "Casted from raw byte to enum NodeObjectType" + ], + "validated_at": "safe_cast(byte[8]), switch (m_objectType)" + }, + { + "field": "m_success", + "flow": [ + "set to false at start", + "set to true in switch (m_objectType) for valid types", + "checked in createObject (XRPL_ASSERT and if (m_success))" + ], + "origin": "false (default in constructor)", + "transformations": [ + "Set to true only for known/allowed object types" + ], + "validated_at": "switch (m_objectType), XRPL_ASSERT(m_success)" + }, + { + "field": "m_objectData", + "flow": [ + "set to value + 9 if (valueBytes > 9)", + "used to construct Blob in createObject" + ], + "origin": "pointer arithmetic on value", + "transformations": [ + "Pointer offset by 9 bytes" + ], + "validated_at": "if (valueBytes > 9)" + }, + { + "field": "m_dataBytes", + "flow": [ + "calculated in constructor", + "used as length for Blob in createObject" + ], + "origin": "max(0, valueBytes - 9)", + "transformations": [ + "Ensures non-negative data length" + ], + "validated_at": "implicit via valueBytes validation" + } + ], + "description": "Implements the DecodedBlob class for decoding and constructing NodeObject instances from raw database blobs in the XRPL NodeStore.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "valueBytes", + "empty", + "string", + "validation" + ], + "evidence": "manual check (if statement) at DecodedBlob constructor", + "issue_pattern": "Missing empty string validation for valueBytes", + "why_false_positive": "manual check (if statement) validates valueBytes for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "valueBytes", + "range", + "bounds", + "validation" + ], + "evidence": "manual check (if statement) at DecodedBlob constructor", + "issue_pattern": "Missing range validation for valueBytes", + "why_false_positive": "manual check (if statement) validates valueBytes range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "valueBytes", + "empty", + "string", + "validation" + ], + "evidence": "manual check (if statement) at DecodedBlob constructor", + "issue_pattern": "Missing empty string validation for valueBytes", + "why_false_positive": "manual check (if statement) validates valueBytes for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "valueBytes", + "range", + "bounds", + "validation" + ], + "evidence": "manual check (if statement) at DecodedBlob constructor", + "issue_pattern": "Missing range validation for valueBytes", + "why_false_positive": "manual check (if statement) validates valueBytes range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "m_objectType (byte[8])", + "empty", + "string", + "validation" + ], + "evidence": "safe_cast at DecodedBlob constructor", + "issue_pattern": "Missing empty string validation for m_objectType (byte[8])", + "why_false_positive": "safe_cast validates m_objectType (byte[8]) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "m_objectType (byte[8])", + "type", + "validation", + "check" + ], + "evidence": "safe_cast at DecodedBlob constructor", + "issue_pattern": "Missing type validation for m_objectType (byte[8])", + "why_false_positive": "safe_cast validates m_objectType (byte[8]) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_objectType", + "empty", + "string", + "validation" + ], + "evidence": "switch statement at DecodedBlob constructor", + "issue_pattern": "Missing empty string validation for m_objectType", + "why_false_positive": "switch statement validates m_objectType for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_success", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at DecodedBlob::createObject", + "issue_pattern": "Missing empty string validation for m_success", + "why_false_positive": "XRPL_ASSERT macro validates m_success for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/DecodedBlob.cpp", + "functions": [ + { + "args": [ + "void const* key", + "void const* value", + "int valueBytes" + ], + "lineno": 8, + "name": "DecodedBlob::DecodedBlob" + }, + { + "args": [], + "lineno": 46, + "name": "DecodedBlob::createObject" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "NodeStore" + } + ], + "test_coverage_notes": "This code is typically tested via higher-level NodeStore or backend tests that exercise object storage and retrieval. Likely test files: src/test/nodestore/*, src/test/rpc/NodeStore_test.cpp, or integration tests that check ledger/transaction/account node persistence. Direct unit tests for DecodedBlob are unlikely unless specifically written; edge cases (e.g., valueBytes <= 8/9, invalid object types) may not be fully covered unless negative tests exist. Gaps: No explicit test coverage for malformed input, unknown object types, or boundary conditions unless covered in broader NodeStore tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "manual validation, XRPL_ASSERT macro, safe_cast utility", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (logic branch only)", + "field": "valueBytes", + "location": "DecodedBlob constructor", + "validated_by": "manual check (if statement)", + "validates": [ + "Checks if valueBytes > 8 before reading byte[8] for object type", + "Prevents out-of-bounds access" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (logic branch only)", + "field": "valueBytes", + "location": "DecodedBlob constructor", + "validated_by": "manual check (if statement)", + "validates": [ + "Checks if valueBytes > 9 before accessing object data at offset 9" + ], + "validation_type": "range" + }, + { + "confidence": 0.8, + "error_thrown": "implementation-defined (safe_cast may assert or throw if invalid)", + "field": "m_objectType (byte[8])", + "location": "DecodedBlob constructor", + "validated_by": "safe_cast", + "validates": [ + "Casts byte[8] to NodeObjectType safely" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (m_success remains false if not valid type)", + "field": "m_objectType", + "location": "DecodedBlob constructor", + "validated_by": "switch statement", + "validates": [ + "Only allows specific NodeObjectType values (hotUNKNOWN, hotLEDGER, hotACCOUNT_NODE, hotTRANSACTION_NODE) to set m_success = true" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "m_success", + "location": "DecodedBlob::createObject", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures createObject is only called if DecodedBlob is valid (m_success == true)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/DecodedBlob.cpp.ai.md b/src/libxrpl/nodestore/DecodedBlob.cpp.ai.md new file mode 100644 index 0000000000..3ef94912ae --- /dev/null +++ b/src/libxrpl/nodestore/DecodedBlob.cpp.ai.md @@ -0,0 +1,33 @@ +# `DecodedBlob.cpp` — NodeStore Binary Deserialization + +`DecodedBlob` is one half of the NodeStore's binary serialization layer. Together with `EncodedBlob`, it defines the on-disk format that bridges the raw key/value pairs stored in backends (NuDB, RocksDB) and the typed `NodeObject` instances that the rest of the ledger logic consumes. This file implements the read direction: parsing a raw byte buffer back into a `NodeObject`. + +## The On-Disk Format + +The binary layout is documented inside the constructor and elaborated in `EncodedBlob.h`: + +- **Bytes 0–7**: An 8-byte prefix, today always zeroed. Historically these bytes were used to store the ledger index (once or twice, in earlier versions of the code). The comment `// VFALCO NOTE What about bytes 4 through 7 inclusive?` reflects this legacy: the original field was probably 4 bytes, leaving the upper half undefined. `DecodedBlob` ignores all 8 bytes on read. +- **Byte 8**: A single `NodeObjectType` discriminant — `hotUNKNOWN`, `hotLEDGER`, `hotACCOUNT_NODE`, or `hotTRANSACTION_NODE`. +- **Bytes 9+**: The raw serialized payload of the node object. + +The minimum useful buffer is therefore 10 bytes (9-byte header plus at least 1 byte of data). A buffer of exactly 9 bytes passes the type byte check but produces `m_dataBytes = 0`, which is still treated as a valid empty-payload object for recognized types. + +## Two-Phase Decode-Then-Create Design + +The class deliberately separates parsing from allocation. The constructor validates the buffer and extracts metadata; `createObject()` performs the only heap allocation. This allows callers to call `wasOk()` and discard corrupted records cheaply, without ever constructing a `NodeObject`. The constructor itself is allocation-free: `m_objectData` is simply a pointer into the original `value` buffer — no copy occurs until `createObject()` invokes `Blob(m_objectData, m_objectData + m_dataBytes)`. + +## Validation Strategy + +The constructor uses a `m_success` flag rather than exceptions. The header explains why: "it is possible to determine if the data is corrupted without throwing an exception." Since the NodeStore may surface data read from physical storage backends that could be partially corrupted or misformatted, a silent flag is more appropriate than propagating an exception through the storage read path. + +Validation is layered: + +1. **Size guards** — `if (valueBytes > 8)` gates the type byte read; `if (valueBytes > 9)` gates data pointer assignment. `m_dataBytes` is computed as `std::max(0, valueBytes - 9)`, guarding against a negative length if `valueBytes` is somehow small. +2. **Type cast** — `safe_cast(byte[8])` converts the raw byte to the enum. In debug builds this asserts if the value is outside the enum's range, catching garbage bytes early. +3. **Type whitelist** — The `switch` statement is the semantic gate: only the four currently known object types flip `m_success` to `true`. An unrecognized byte — whether from a future format version or actual corruption — leaves `m_success = false` and prevents object creation. This makes the decoder safely forward-incompatible. + +In `createObject()`, `XRPL_ASSERT(m_success, ...)` fires in debug builds if the caller violates the contract by calling it on a failed parse. The `if (m_success)` guard that follows provides defense in release builds where the assert compiles away, ensuring `createObject()` always returns a null `shared_ptr` rather than undefined behavior on a bad parse. + +## Relationship to `EncodedBlob` + +`EncodedBlob` is the symmetric encoder. It takes a `NodeObject`, zeroes the 8-byte prefix, writes the type at byte 8, copies the payload from byte 9 onward, and uses an inline stack buffer for payloads under ~1024 bytes to avoid heap allocation in the common case. `DecodedBlob` inverts exactly that layout. The two classes together constitute the complete on-disk schema for a NodeStore entry, and any change to the format must be mirrored in both. \ No newline at end of file diff --git a/src/libxrpl/nodestore/DummyScheduler.cpp.ai.json b/src/libxrpl/nodestore/DummyScheduler.cpp.ai.json new file mode 100644 index 0000000000..547fddc22d --- /dev/null +++ b/src/libxrpl/nodestore/DummyScheduler.cpp.ai.json @@ -0,0 +1,134 @@ +{ + "args": [ + { + "lineno": 7, + "name": "task" + }, + { + "lineno": 13, + "name": "report" + }, + { + "lineno": 17, + "name": "report" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "DummyScheduler::scheduleTask", + "Task::performScheduledTask" + ], + "entry_point": "DummyScheduler::scheduleTask", + "purpose": "Schedules a task for execution. In this dummy implementation, the task is executed synchronously by directly calling its performScheduledTask method.", + "validation_points": [] + }, + { + "call_chain": [ + "DummyScheduler::onFetch" + ], + "entry_point": "DummyScheduler::onFetch", + "purpose": "Receives a FetchReport, presumably to report on fetch operations. In this dummy implementation, the function is a no-op.", + "validation_points": [] + }, + { + "call_chain": [ + "DummyScheduler::onBatchWrite" + ], + "entry_point": "DummyScheduler::onBatchWrite", + "purpose": "Receives a BatchWriteReport, presumably to report on batch write operations. In this dummy implementation, the function is a no-op.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "Task& task", + "flow": [ + "Caller constructs or obtains Task", + "Passes Task to DummyScheduler::scheduleTask", + "scheduleTask calls task.performScheduledTask()" + ], + "origin": "Caller of DummyScheduler::scheduleTask provides a Task reference.", + "transformations": [ + "No transformation; task is passed by reference and executed synchronously." + ], + "validated_at": "No validation occurs in DummyScheduler::scheduleTask." + }, + { + "field": "FetchReport const& report", + "flow": [ + "Caller constructs or obtains FetchReport", + "Passes FetchReport to DummyScheduler::onFetch" + ], + "origin": "Caller of DummyScheduler::onFetch provides a FetchReport reference.", + "transformations": [ + "No transformation; report is ignored." + ], + "validated_at": "No validation occurs in DummyScheduler::onFetch." + }, + { + "field": "BatchWriteReport const& report", + "flow": [ + "Caller constructs or obtains BatchWriteReport", + "Passes BatchWriteReport to DummyScheduler::onBatchWrite" + ], + "origin": "Caller of DummyScheduler::onBatchWrite provides a BatchWriteReport reference.", + "transformations": [ + "No transformation; report is ignored." + ], + "validated_at": "No validation occurs in DummyScheduler::onBatchWrite." + } + ], + "description": "Implements the DummyScheduler class for the xrpl::NodeStore namespace, providing no-op or synchronous implementations for scheduling and reporting methods.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/DummyScheduler.cpp", + "functions": [ + { + "args": [ + "task" + ], + "lineno": 6, + "name": "DummyScheduler::scheduleTask" + }, + { + "args": [ + "report" + ], + "lineno": 12, + "name": "DummyScheduler::onFetch" + }, + { + "args": [ + "report" + ], + "lineno": 16, + "name": "DummyScheduler::onBatchWrite" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "NodeStore" + } + ], + "test_coverage_notes": "This dummy implementation contains no validation logic or data transformation. It is likely used as a stub or mock in tests or as a placeholder in configurations where scheduling is not required. There are no validation code paths to test. Test coverage would only verify that the methods can be called without error, and that scheduleTask synchronously executes the provided task. Tests, if any, would be found in files testing the NodeStore::Scheduler interface or its consumers, but there is no evidence of direct validation logic to test. Gaps: No validation, error handling, or data processing is present or tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/DummyScheduler.cpp.ai.md b/src/libxrpl/nodestore/DummyScheduler.cpp.ai.md new file mode 100644 index 0000000000..334a00acfd --- /dev/null +++ b/src/libxrpl/nodestore/DummyScheduler.cpp.ai.md @@ -0,0 +1,25 @@ +# `DummyScheduler` — Synchronous No-Op Scheduler for NodeStore + +## Role and Purpose + +`DummyScheduler` is a minimal, concrete implementation of the abstract `Scheduler` interface in the `xrpl::NodeStore` namespace. Its sole purpose is to satisfy the `Scheduler` contract without introducing any real asynchrony or performance monitoring — making it the standard stand-in for test harnesses, benchmarks, and any context where backend scheduling overhead is unwanted or irrelevant. + +The `Scheduler` interface exists because the NodeStore backend supports asynchronous batch writes: rather than flushing every ledger object to disk immediately, the `BatchWriter` can queue writes and schedule them on a background thread. This requires a `Scheduler` to arbitrate when and how tasks run, and to receive telemetry reports (`FetchReport`, `BatchWriteReport`) that a production scheduler could use to adapt its behavior. `DummyScheduler` collapses all of that complexity to nothing. + +## Design of the Three Methods + +`scheduleTask(Task& task)` is the heart of the class. The `Scheduler` interface explicitly documents that a task *may* be invoked on the calling thread or on a foreign thread — the implementation decides. `DummyScheduler` always chooses the calling thread: it calls `task.performScheduledTask()` inline before returning. This turns every "scheduled" write into a synchronous, blocking call, which eliminates concurrency entirely. The consequence is that any `BatchWriter` backed by a `DummyScheduler` loses its batching advantage, but gains predictable, sequential behavior that is far easier to reason about in tests. + +`onFetch(FetchReport const& report)` and `onBatchWrite(BatchWriteReport const& report)` are both no-ops. In a production scheduler these callbacks carry timing data (`std::chrono::milliseconds elapsed`) and outcome flags (`wasFound`, `writeCount`) that could feed adaptive I/O strategies or metrics pipelines. `DummyScheduler` ignores them entirely — appropriate when the goal is correctness testing rather than performance tuning. + +## Why a Separate Class Instead of Nullptr or a Lambda? + +The `Scheduler` interface is passed by reference throughout the NodeStore subsystem (e.g., into `BatchWriter`). A null pointer would require defensive checks everywhere, and a lambda or `std::function` wrapper would still need a type to satisfy the virtual dispatch contract. A named concrete class like `DummyScheduler` cleanly satisfies the interface, is self-documenting at call sites, and costs nothing beyond a stack-allocated object. Its trivial constructor and destructor (`= default`) reinforce that it carries no state. + +## Usage Pattern + +`DummyScheduler` appears consistently in the NodeStore test suite — `Backend_test.cpp`, `Database_test.cpp`, `NuDBFactory_test.cpp`, and `Timing_test.cpp` all instantiate it on the stack before constructing a backend or database under test. The pattern is always the same: create a `DummyScheduler`, pass it (by reference) to the component under test, run assertions, and let the scheduler go out of scope. The synchronous `scheduleTask` behavior means test assertions about database state are valid immediately after any write operation, without needing barriers or condition variables. + +## Relationship to the Broader NodeStore Scheduler System + +The `Scheduler` abstraction exists at the boundary between the NodeStore's high-level database interface (`Database`) and its pluggable backends (NuDB, RocksDB, etc.). A real `Scheduler` implementation — such as the one wired into `Application` — runs a thread pool and uses the `onFetch`/`onBatchWrite` reports to surface latency metrics. `DummyScheduler` sits at the opposite end of this spectrum: zero threads, zero metrics, maximum simplicity. The interface's virtual dispatch means the NodeStore core never needs to know which scheduler it is talking to, making `DummyScheduler` a drop-in replacement that requires no changes to any of the code under test. \ No newline at end of file diff --git a/src/libxrpl/nodestore/ManagerImp.cpp.ai.json b/src/libxrpl/nodestore/ManagerImp.cpp.ai.json new file mode 100644 index 0000000000..bdad0ee8c8 --- /dev/null +++ b/src/libxrpl/nodestore/ManagerImp.cpp.ai.json @@ -0,0 +1,435 @@ +{ + "args": [ + { + "lineno": 23, + "name": "manager" + }, + { + "lineno": 24, + "name": "manager" + }, + { + "lineno": 25, + "name": "manager" + }, + { + "lineno": 26, + "name": "manager" + }, + { + "lineno": 35, + "name": "parameters" + }, + { + "lineno": 35, + "name": "burstSize" + }, + { + "lineno": 35, + "name": "scheduler" + }, + { + "lineno": 35, + "name": "journal" + }, + { + "lineno": 48, + "name": "burstSize" + }, + { + "lineno": 48, + "name": "scheduler" + }, + { + "lineno": 48, + "name": "readThreads" + }, + { + "lineno": 48, + "name": "config" + }, + { + "lineno": 48, + "name": "journal" + }, + { + "lineno": 58, + "name": "factory" + }, + { + "lineno": 63, + "name": "factory" + }, + { + "lineno": 71, + "name": "name" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ManagerImp::make_Backend", + "ManagerImp::find", + "Factory::createInstance" + ], + "entry_point": "ManagerImp::make_Backend", + "purpose": "Creates a Backend instance based on configuration parameters. Validates the 'type' parameter and the existence of a corresponding Factory.", + "validation_points": [ + "ManagerImp::make_Backend: Validates 'type' parameter is non-empty", + "ManagerImp::make_Backend: Validates 'factory' pointer is not null (from find(type))" + ] + }, + { + "call_chain": [ + "ManagerImp::make_Database", + "ManagerImp::make_Backend", + "ManagerImp::find", + "Factory::createInstance", + "Backend::open", + "DatabaseNodeImp::DatabaseNodeImp" + ], + "entry_point": "ManagerImp::make_Database", + "purpose": "Creates a Database instance, which includes creating and opening a Backend. Inherits all validations from make_Backend.", + "validation_points": [ + "ManagerImp::make_Backend: Validates 'type' parameter and 'factory' pointer" + ] + }, + { + "call_chain": [ + "ManagerImp::erase" + ], + "entry_point": "ManagerImp::erase", + "purpose": "Removes a Factory from the internal list. Validates that the Factory exists in the list before erasing.", + "validation_points": [ + "ManagerImp::erase: XRPL_ASSERT ensures the Factory pointer is present in list_" + ] + } + ], + "data_flows": [ + { + "field": "type (from Section parameters)", + "flow": [ + "Section config", + "get(parameters, \"type\") in make_Backend", + "std::string type", + "ManagerImp::find(type)", + "Factory* factory" + ], + "origin": "Section config parameter (likely from config file)", + "transformations": [ + "Extracted from config Section", + "Passed as string to find()" + ], + "validated_at": "ManagerImp::make_Backend (checked for empty string)" + }, + { + "field": "factory pointer", + "flow": [ + "find(type)", + "returns Factory* or nullptr", + "checked in make_Backend", + "used to create Backend" + ], + "origin": "ManagerImp::find(type)", + "transformations": [ + "Looked up in list_ by case-insensitive name" + ], + "validated_at": "ManagerImp::make_Backend (checked for nullptr)" + }, + { + "field": "factory pointer (in erase)", + "flow": [ + "erase(Factory& factory)", + "finds pointer in list_", + "XRPL_ASSERT checks pointer is present", + "erases from list_" + ], + "origin": "Passed as argument to ManagerImp::erase", + "transformations": [ + "Pointer compared for equality in list_" + ], + "validated_at": "ManagerImp::erase (XRPL_ASSERT)" + } + ], + "description": "Implements the ManagerImp class for managing NodeStore backend factories and creating Backend and Database instances in the XRPL node store subsystem.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (from parameters Section)", + "empty", + "string", + "validation" + ], + "evidence": "ManagerImp::make_Backend at ManagerImp::make_Backend", + "issue_pattern": "Missing empty string validation for type (from parameters Section)", + "why_false_positive": "ManagerImp::make_Backend validates type (from parameters Section) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "factory pointer (from find(type))", + "empty", + "string", + "validation" + ], + "evidence": "ManagerImp::make_Backend at ManagerImp::make_Backend", + "issue_pattern": "Missing empty string validation for factory pointer (from find(type))", + "why_false_positive": "ManagerImp::make_Backend validates factory pointer (from find(type)) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "factory pointer (from list_ in erase)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ManagerImp::erase", + "issue_pattern": "Missing empty string validation for factory pointer (from list_ in erase)", + "why_false_positive": "XRPL_ASSERT validates factory pointer (from list_ in erase) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/ManagerImp.cpp", + "functions": [ + { + "args": [], + "lineno": 8, + "name": "ManagerImp::instance" + }, + { + "args": [], + "lineno": 13, + "name": "ManagerImp::missing_backend" + }, + { + "args": [ + "manager" + ], + "lineno": 23, + "name": "registerNuDBFactory" + }, + { + "args": [ + "manager" + ], + "lineno": 24, + "name": "registerRocksDBFactory" + }, + { + "args": [ + "manager" + ], + "lineno": 25, + "name": "registerNullFactory" + }, + { + "args": [ + "manager" + ], + "lineno": 26, + "name": "registerMemoryFactory" + }, + { + "args": [], + "lineno": 28, + "name": "ManagerImp::ManagerImp" + }, + { + "args": [ + "parameters", + "burstSize", + "scheduler", + "journal" + ], + "lineno": 35, + "name": "ManagerImp::make_Backend" + }, + { + "args": [ + "burstSize", + "scheduler", + "readThreads", + "config", + "journal" + ], + "lineno": 48, + "name": "ManagerImp::make_Database" + }, + { + "args": [ + "factory" + ], + "lineno": 58, + "name": "ManagerImp::insert" + }, + { + "args": [ + "factory" + ], + "lineno": 63, + "name": "ManagerImp::erase" + }, + { + "args": [ + "name" + ], + "lineno": 71, + "name": "ManagerImp::find" + }, + { + "args": [], + "lineno": 83, + "name": "Manager::instance" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "NodeStore" + } + ], + "test_coverage_notes": "The code is core infrastructure and likely tested indirectly via higher-level NodeStore and Backend/Database tests. Direct unit tests for ManagerImp::make_Backend, make_Database, and erase may not exist, but their validation logic is exercised when invalid configs are loaded (e.g., missing or unknown 'type' in [node_db] section). Test files to check: nodestore/Manager_test.cpp, nodestore/Database_test.cpp, nodestore/Backend_test.cpp, and integration tests that load various xrpld.cfg files. Gaps: Direct negative tests for missing/invalid 'type' or erase with invalid pointer may be missing.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (no external validation framework, uses Throw<> and XRPL_ASSERT)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "type (from parameters Section)", + "location": "ManagerImp::make_Backend", + "validated_by": "ManagerImp::make_Backend", + "validates": [ + "Checks that the 'type' field is present and non-empty in the parameters Section", + "Checks that the 'type' field corresponds to a registered backend factory" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "factory pointer (from find(type))", + "location": "ManagerImp::make_Backend", + "validated_by": "ManagerImp::make_Backend", + "validates": [ + "Checks that a backend factory matching the 'type' field exists (factory != nullptr)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "Assertion failure (XRPL_ASSERT)", + "field": "factory pointer (from list_ in erase)", + "location": "ManagerImp::erase", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that the factory pointer to erase exists in the list_ container" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/ManagerImp.cpp.ai.md b/src/libxrpl/nodestore/ManagerImp.cpp.ai.md new file mode 100644 index 0000000000..b92fc4121a --- /dev/null +++ b/src/libxrpl/nodestore/ManagerImp.cpp.ai.md @@ -0,0 +1,38 @@ +# `NodeStore/ManagerImp.cpp` — Backend Factory Registry and Database Construction + +## Role in the System + +`ManagerImp.cpp` is the concrete implementation of the `Manager` singleton within XRPL's `NodeStore` subsystem. The `Manager` interface is the central switchboard that the rest of the node process uses to create persistent storage objects: it knows which storage backends are available (NuDB, RocksDB, Null, Memory), how to construct them from configuration, and how to wrap them in the higher-level `Database` abstraction. `ManagerImp` is the only implementation, hidden behind the abstract `Manager` interface to allow the rest of the codebase to remain decoupled from any particular backend. + +## Singleton Lifecycle and Factory Registration + +`ManagerImp::instance()` uses a Meyers singleton — a function-local static — to guarantee thread-safe, lazily initialized construction: + +```cpp +static ManagerImp _; +return _; +``` + +The constructor immediately registers all four built-in backends by calling free functions: `registerNuDBFactory`, `registerRocksDBFactory`, `registerNullFactory`, and `registerMemoryFactory`. Each of these functions contains its own function-local static factory object (for example, `static NullFactory const instance{manager}`), which calls `manager.insert(*this)` in its constructor. This layered approach is deliberate and the file contains an explicit comment explaining why. + +The critical design decision is that factories are **not registered via global variables**. If a `Factory` subclass were a global (with a static member or translation-unit scope variable), its C++ destruction order relative to the `ManagerImp` singleton would be undefined across translation units. When such a global's destructor called `Manager::instance().erase()`, the `ManagerImp` might already have been destroyed, resulting in undefined behavior. By calling the registration functions from `ManagerImp`'s own constructor, the factories are initialized after `ManagerImp` and — because function-local statics are destroyed in reverse initialization order — will be destroyed before it, making `erase()` calls in factory destructors safe. + +## Factory Lookup and Backend Construction + +`make_Backend` is the core factory dispatch method. It extracts the `"type"` key from the configuration `Section` and uses it to find a matching `Factory` via `find()`. The lookup uses `boost::iequals` for case-insensitive name matching, so `"NuDB"`, `"nudb"`, and `"NUDB"` all resolve to the same factory. If the `"type"` key is absent or the name doesn't correspond to any registered factory, `missing_backend()` throws a `std::runtime_error` with a user-facing message directing the operator to add a `[node_db]` entry to `xrpld.cfg`. This transforms a confusing crash or silent failure into actionable guidance. + +Once a factory is located, `make_Backend` calls `Factory::createInstance` with `NodeObject::keyBytes` as the fixed key size — a constant reflecting that all XRPL node objects use a 256-bit (32-byte) hash as the storage key. + +`make_Database` composes backend creation with database construction: it calls `make_Backend` to get an unopened backend, explicitly calls `backend->open()` on it, then wraps it in a `DatabaseNodeImp`. The separation between backend creation and opening is important — it allows `make_Backend` to hand back an object that can be inspected or configured before I/O begins, while `make_Database` handles the full ready-to-use database lifecycle in one call. + +## Thread Safety + +The internal factory list `list_` (a `std::vector`) is protected by `mutex_`. Every method that reads or modifies `list_` — `insert`, `erase`, and `find` — acquires a `std::lock_guard` before touching the container. This allows factories to be registered and deregistered safely from any thread, though in practice registration happens only during `ManagerImp`'s own construction on whichever thread first calls `instance()`. + +## Error Handling Strategy + +There is a deliberate asymmetry in how two classes of error are reported. Configuration errors (missing or unknown `"type"`) produce `std::runtime_error` via `Throw<>`, because they represent operator mistakes that must surface immediately with a meaningful message. In contrast, `erase()` uses `XRPL_ASSERT` to verify that the factory pointer being removed actually exists in the list — this is a programming invariant that should never be violated at runtime, so a debug assertion is appropriate rather than a recoverable exception. + +## Public Interface Forwarding + +`Manager::instance()` is defined in this file as a one-liner that forwards to `ManagerImp::instance()`. This keeps the abstract `Manager` header free of implementation details while still providing the global access point: callers that include only `Manager.h` can call `Manager::instance()` without knowing that `ManagerImp` exists. \ No newline at end of file diff --git a/src/libxrpl/nodestore/NodeObject.cpp.ai.json b/src/libxrpl/nodestore/NodeObject.cpp.ai.json new file mode 100644 index 0000000000..84f2b90173 --- /dev/null +++ b/src/libxrpl/nodestore/NodeObject.cpp.ai.json @@ -0,0 +1,172 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "NodeObject::createObject", + "NodeObject::NodeObject (constructor)" + ], + "entry_point": "NodeObject::createObject", + "purpose": "Creates a new NodeObject instance with specified type, data, and hash.", + "validation_points": [] + }, + { + "call_chain": [ + "NodeObject::getType / getHash / getData" + ], + "entry_point": "NodeObject::getType / getHash / getData", + "purpose": "Accessors for NodeObject fields.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "mType", + "flow": [ + "NodeObject::createObject (type param)", + "NodeObject::NodeObject (type param)", + "NodeObject::mType (member variable)", + "NodeObject::getType (returns mType)" + ], + "origin": "NodeObject::createObject (parameter: type)", + "transformations": [], + "validated_at": null + }, + { + "field": "mHash", + "flow": [ + "NodeObject::createObject (hash param)", + "NodeObject::NodeObject (hash param)", + "NodeObject::mHash (member variable)", + "NodeObject::getHash (returns mHash)" + ], + "origin": "NodeObject::createObject (parameter: hash)", + "transformations": [], + "validated_at": null + }, + { + "field": "mData", + "flow": [ + "NodeObject::createObject (data param)", + "NodeObject::NodeObject (data param, moved)", + "NodeObject::mData (member variable)", + "NodeObject::getData (returns mData)" + ], + "origin": "NodeObject::createObject (parameter: data)", + "transformations": [ + "std::move on Blob data in constructor" + ], + "validated_at": null + } + ], + "description": "Defines the NodeObject class for XRPL node storage, including its constructor, factory method, and accessors for type, hash, and data.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/NodeObject.cpp", + "functions": [ + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash", + "PrivateAccess" + ], + "lineno": 8, + "name": "NodeObject::NodeObject" + }, + { + "args": [ + "NodeObjectType type", + "Blob&& data", + "uint256 const& hash" + ], + "lineno": 13, + "name": "NodeObject::createObject" + }, + { + "args": [], + "lineno": 19, + "name": "NodeObject::getType" + }, + { + "args": [], + "lineno": 24, + "name": "NodeObject::getHash" + }, + { + "args": [], + "lineno": 29, + "name": "NodeObject::getData" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no validation logic in this code: no input checking, type validation, or data integrity checks. The code assumes all inputs are valid and simply stores and returns them. Test coverage would likely be in files testing NodeStore or NodeObject creation and accessors, such as 'nodestore/NodeObject_test.cpp' or similar, but these would only test construction and access, not validation. There are no validation code paths to test. Gaps: No tests or code for invalid input, malformed data, or type enforcement.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none", + "notes": "No explicit or implicit input validation is performed in this file. All fields are assigned directly without checks. No framework or type/format/range/business logic validation is present.", + "validation_layer": "none" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/NodeObject.cpp.ai.md b/src/libxrpl/nodestore/NodeObject.cpp.ai.md new file mode 100644 index 0000000000..4a3ad519db --- /dev/null +++ b/src/libxrpl/nodestore/NodeObject.cpp.ai.md @@ -0,0 +1,17 @@ +# `NodeObject.cpp` — Immutable Ledger Storage Unit + +`NodeObject` is the fundamental value type of the XRPL node store: a read-only triple of (type, hash, blob) representing a single serialized ledger entry. The implementation file is short because the design is deliberately constrained — instances are created once, never mutated, and identified entirely by their hash. + +## Factory Method and the `PrivateAccess` Sentinel + +The constructor is nominally `public` but effectively private. The header defines a nested `struct PrivateAccess` whose `explicit` default constructor makes it impossible to construct without naming it — and since `PrivateAccess` is declared `private` in the class, only code inside `NodeObject` can produce one. This is the standard portable workaround for the fact that C++ provides no mechanism to make `std::make_shared` a `friend`: the factory method `createObject()` passes a `PrivateAccess{}` token to `std::make_shared(...)`, which satisfies the constructor signature while preventing any external caller from doing the same. + +The caller passes `Blob&&` — a move reference — so ownership of the raw serialized payload transfers into the object with no copy. Combined with the `const` declarations on all three members (`mType`, `mHash`, `mData`), this guarantees that once a `NodeObject` is constructed its state cannot change. Every reference holder sees the same immutable view, which is important when the same object may be referenced from cache, the write queue, and in-flight read callbacks simultaneously. + +## No Hash Verification + +The header comment explicitly notes that no check is performed to confirm the hash actually matches the data. Validation is left to the caller. This keeps `NodeObject` lightweight and avoids redundant hashing when the object is reconstructed from a trusted backend store where the hash was already verified on write. + +## Instance Counting via `CountedObject` + +`NodeObject` inherits from `CountedObject`, which wires into a global lock-free linked list of per-type counters. Each constructor increments an `std::atomic` and the destructor decrements it, letting the node store report how many `NodeObject` instances are live at any time — useful for diagnosing cache pressure without adding any overhead to the fast path. \ No newline at end of file diff --git a/src/libxrpl/nodestore/backend/MemoryFactory.cpp.ai.json b/src/libxrpl/nodestore/backend/MemoryFactory.cpp.ai.json new file mode 100644 index 0000000000..2f101aa023 --- /dev/null +++ b/src/libxrpl/nodestore/backend/MemoryFactory.cpp.ai.json @@ -0,0 +1,568 @@ +{ + "args": [ + { + "lineno": 38, + "name": "manager" + }, + { + "lineno": 50, + "name": "path" + }, + { + "lineno": 66, + "name": "manager" + }, + { + "lineno": 77, + "name": "keyBytes" + }, + { + "lineno": 77, + "name": "keyValues" + }, + { + "lineno": 77, + "name": "journal" + }, + { + "lineno": 109, + "name": "hash" + }, + { + "lineno": 109, + "name": "pObject" + }, + { + "lineno": 120, + "name": "hashes" + }, + { + "lineno": 139, + "name": "object" + }, + { + "lineno": 144, + "name": "batch" + }, + { + "lineno": 152, + "name": "f" + }, + { + "lineno": 172, + "name": "manager" + }, + { + "lineno": 180, + "name": "keyBytes" + }, + { + "lineno": 180, + "name": "keyValues" + }, + { + "lineno": 180, + "name": "scheduler" + }, + { + "lineno": 180, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "MemoryDB" + }, + { + "args": [ + "Manager& manager" + ], + "lineno": 25, + "name": "MemoryFactory" + }, + { + "args": [ + "size_t keyBytes", + "Section const& keyValues", + "beast::Journal journal" + ], + "lineno": 73, + "name": "MemoryBackend" + } + ], + "code_paths": [ + { + "call_chain": [ + "MemoryFactory::createInstance", + "MemoryBackend::MemoryBackend", + "MemoryBackend::open" + ], + "entry_point": "MemoryFactory::createInstance", + "purpose": "Creates a new MemoryBackend instance, validates configuration, and opens the in-memory database.", + "validation_points": [ + "MemoryBackend::MemoryBackend (validates 'path' from keyValues)", + "MemoryFactory::open (validates db_.open flag, throws if already open)", + "MemoryBackend::open (assigns db_ pointer, which is later validated by XRPL_ASSERT in fetch/store)" + ] + }, + { + "call_chain": [ + "MemoryBackend::fetch|store|fetchBatch|storeBatch", + "XRPL_ASSERT(db_, ...)" + ], + "entry_point": "MemoryBackend::fetch / store / fetchBatch / storeBatch", + "purpose": "Performs operations on the in-memory database, ensuring db_ is valid before access.", + "validation_points": [ + "XRPL_ASSERT(db_, ...) in each method" + ] + } + ], + "data_flows": [ + { + "field": "name_ (MemoryBackend)", + "flow": [ + "keyValues Section (input to createInstance)", + "get(keyValues, \"path\")", + "name_ field in MemoryBackend", + "used as argument to MemoryFactory::open in MemoryBackend::open" + ], + "origin": "get(keyValues, \"path\") in MemoryBackend::MemoryBackend", + "transformations": [ + "Extracted from Section", + "Assigned to name_", + "Validated for non-empty" + ], + "validated_at": "MemoryBackend::MemoryBackend (throws if empty)" + }, + { + "field": "db_ (MemoryBackend)", + "flow": [ + "MemoryBackend::open", + "db_ assigned to MemoryDB*", + "Used in fetch/store/etc" + ], + "origin": "Assigned in MemoryBackend::open via memoryFactory->open(name_)", + "transformations": [ + "Pointer assignment", + "Checked for null via XRPL_ASSERT before use" + ], + "validated_at": "XRPL_ASSERT in fetch/store/fetchBatch/storeBatch" + }, + { + "field": "MemoryDB::open (bool)", + "flow": [ + "MemoryFactory::open", + "Checks db.open before returning MemoryDB&" + ], + "origin": "MemoryDB instance in MemoryFactory::map_", + "transformations": [ + "Checked for true (already open), throws if so" + ], + "validated_at": "MemoryFactory::open" + } + ], + "description": "This file implements an in-memory backend for the XRPL NodeStore, providing a MemoryBackend and MemoryFactory for storing and retrieving NodeObjects in memory, primarily for testing or ephemeral storage.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "path (from keyValues Section, used as name_)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in MemoryBackend constructor at MemoryBackend::MemoryBackend (constructor)", + "issue_pattern": "Missing empty string validation for path (from keyValues Section, used as name_)", + "why_false_positive": "explicit check in MemoryBackend constructor validates path (from keyValues Section, used as name_) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "db_.open (MemoryDB::open flag)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in MemoryFactory::open at MemoryFactory::open", + "issue_pattern": "Missing empty string validation for db_.open (MemoryDB::open flag)", + "why_false_positive": "explicit check in MemoryFactory::open validates db_.open (MemoryDB::open flag) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "db_ pointer (MemoryBackend internal pointer to MemoryDB)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at MemoryBackend::fetch", + "issue_pattern": "Missing empty string validation for db_ pointer (MemoryBackend internal pointer to MemoryDB)", + "why_false_positive": "XRPL_ASSERT macro validates db_ pointer (MemoryBackend internal pointer to MemoryDB) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "db_ pointer (MemoryBackend internal pointer to MemoryDB)", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT macro at MemoryBackend::fetch", + "issue_pattern": "Missing type validation for db_ pointer (MemoryBackend internal pointer to MemoryDB)", + "why_false_positive": "XRPL_ASSERT macro validates db_ pointer (MemoryBackend internal pointer to MemoryDB) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/backend/MemoryFactory.cpp", + "functions": [ + { + "args": [ + "Manager& manager" + ], + "lineno": 38, + "name": "MemoryFactory" + }, + { + "args": [], + "lineno": 41, + "name": "getName" + }, + { + "args": [ + "size_t keyBytes", + "Section const& keyValues", + "std::size_t burstSize", + "Scheduler& scheduler", + "beast::Journal journal" + ], + "lineno": 44, + "name": "createInstance" + }, + { + "args": [ + "std::string const& path" + ], + "lineno": 50, + "name": "open" + }, + { + "args": [ + "Manager& manager" + ], + "lineno": 66, + "name": "registerMemoryFactory" + }, + { + "args": [ + "size_t keyBytes", + "Section const& keyValues", + "beast::Journal journal" + ], + "lineno": 77, + "name": "MemoryBackend" + }, + { + "args": [], + "lineno": 83, + "name": "~MemoryBackend" + }, + { + "args": [], + "lineno": 87, + "name": "getName" + }, + { + "args": [ + "bool" + ], + "lineno": 91, + "name": "open" + }, + { + "args": [], + "lineno": 96, + "name": "isOpen" + }, + { + "args": [], + "lineno": 101, + "name": "close" + }, + { + "args": [ + "uint256 const& hash", + "std::shared_ptr* pObject" + ], + "lineno": 109, + "name": "fetch" + }, + { + "args": [ + "std::vector const& hashes" + ], + "lineno": 120, + "name": "fetchBatch" + }, + { + "args": [ + "std::shared_ptr const& object" + ], + "lineno": 139, + "name": "store" + }, + { + "args": [ + "Batch const& batch" + ], + "lineno": 144, + "name": "storeBatch" + }, + { + "args": [], + "lineno": 149, + "name": "sync" + }, + { + "args": [ + "std::function)> f" + ], + "lineno": 152, + "name": "for_each" + }, + { + "args": [], + "lineno": 158, + "name": "getWriteLoad" + }, + { + "args": [], + "lineno": 162, + "name": "setDeletePath" + }, + { + "args": [], + "lineno": 166, + "name": "fdRequired" + }, + { + "args": [ + "Manager& manager" + ], + "lineno": 172, + "name": "MemoryFactory" + }, + { + "args": [], + "lineno": 176, + "name": "getName" + }, + { + "args": [ + "size_t keyBytes", + "Section const& keyValues", + "std::size_t", + "Scheduler& scheduler", + "beast::Journal journal" + ], + "lineno": 180, + "name": "createInstance" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "NodeStore" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, tests for this code would be found in the XRPLF/rippled repository under test/nodestore/ or similar directories, possibly named MemoryBackend_test.cpp or Factory_test.cpp. The critical validation paths (missing path, already open, null db_) would need explicit tests. Gaps may exist if tests do not cover: (1) missing or empty 'path' in keyValues, (2) attempting to open the same path twice, (3) using fetch/store before open, (4) concurrent access to MemoryFactory or MemoryBackend. Review of the test suite is needed to confirm coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (explicit checks, XRPL_ASSERT macro, Throw utility)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Missing path in Memory backend\")", + "field": "path (from keyValues Section, used as name_)", + "location": "MemoryBackend::MemoryBackend (constructor)", + "validated_by": "explicit check in MemoryBackend constructor", + "validates": [ + "Checks that the 'path' field (used as the backend name) is not empty" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"already open\")", + "field": "db_.open (MemoryDB::open flag)", + "location": "MemoryFactory::open", + "validated_by": "explicit check in MemoryFactory::open", + "validates": [ + "Checks that the MemoryDB for a given path is not already open before opening" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws, depending on XRPL_ASSERT config)", + "field": "db_ pointer (MemoryBackend internal pointer to MemoryDB)", + "location": "MemoryBackend::fetch", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that db_ is not null before accessing the database" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/backend/MemoryFactory.cpp.ai.md b/src/libxrpl/nodestore/backend/MemoryFactory.cpp.ai.md new file mode 100644 index 0000000000..61aa52f41c --- /dev/null +++ b/src/libxrpl/nodestore/backend/MemoryFactory.cpp.ai.md @@ -0,0 +1,41 @@ +# `MemoryFactory.cpp` — In-Memory NodeStore Backend + +## Purpose and Context + +This file implements a fully in-memory backend for the XRPL NodeStore system. The NodeStore is the layer that persists `NodeObject` items (ledger nodes, transactions, account state) keyed by their 256-bit hash. Where production deployments use NuDB or RocksDB backends backed by disk, the Memory backend stores everything in a `std::map` in the process heap. Its primary use cases are unit testing (where test suites need a real, functioning store without filesystem side-effects) and any transient, ephemeral storage scenario where persistence across process restarts is explicitly unwanted. + +The file defines three cooperating types and one module-level registration function: `MemoryDB`, `MemoryBackend`, `MemoryFactory`, and `registerMemoryFactory`. + +## The Three-Layer Design + +### `MemoryDB` — the actual storage cell + +`MemoryDB` is deliberately minimal: a `bool open` flag, a `std::mutex`, and a `std::map>` named `table`. Separating raw storage into its own struct (rather than embedding it in `MemoryBackend`) is the key architectural move: it lets multiple `MemoryBackend` instances opened with the same path name share a single underlying map. The `MemoryFactory` owns all `MemoryDB` instances in its `map_` member, keyed by path string; the `MemoryBackend` only holds a raw pointer `db_` into that collection. When a backend closes, it nulls its pointer but the factory's map retains the `MemoryDB`, so a subsequent `open()` on the same path recovers the same data — the in-memory store survives backend lifecycle events within a single process. + +### `MemoryFactory` — factory and registry + +`MemoryFactory` implements the `Factory` interface and serves two roles: it acts as a registry of named `MemoryDB` instances, and it is the creator of `MemoryBackend` objects via `createInstance()`. The factory is a process-level singleton created by `registerMemoryFactory()` using a function-local static, which guarantees both lazy initialization and thread-safe construction under C++11 rules. A module-level raw pointer `memoryFactory` is set to the singleton's address so that `MemoryBackend::open()` can call back into it without holding a reference — a simple coupling that works because the factory's lifetime spans the process. + +The `map_` inside `MemoryFactory` is indexed with `boost::beast::iless`, making path lookups case-insensitive. This mirrors how other configuration-driven backends treat path names and avoids accidental duplication when callers use different capitalizations of the same logical store name. + +The `MemoryFactory::open()` method includes a guard: it throws `std::runtime_error("already open")` if `db.open` is `true`. However, `MemoryDB::open` is initialized to `false` and is **never set to `true`** anywhere in the file. The guard is therefore dead code in the current implementation — likely a vestigial remnant of a stricter ownership model that was never fully realized. In practice, nothing prevents the same `MemoryDB` from being pointed to by multiple `MemoryBackend` instances simultaneously. + +### `MemoryBackend` — the `Backend` interface implementation + +`MemoryBackend` wraps a `MemoryDB*` and implements the full `Backend` interface. Construction validates that a non-empty `"path"` key exists in the configuration `Section`; without it, a `std::runtime_error` is thrown immediately, consistent with how disk-based backends enforce their `"path"` requirement. The `bool` argument to `open()` (conventionally meaning "create if missing") is silently ignored — an in-memory store always starts empty and always "creates" implicitly. + +The `db_` pointer starts null and is assigned during `open()`; all substantive methods (`fetch`, `store`, `fetchBatch`, `storeBatch`, `for_each`) guard against a null pointer via `XRPL_ASSERT`, which will abort or throw depending on the build configuration. The `isOpen()` predicate simply casts `db_` to `bool`. + +## Concurrency Model + +Every mutating and reading method on `MemoryDB` acquires `db_->mutex` via `std::lock_guard` before touching `table` — except `for_each`. The iteration path reads `db_->table` without holding any lock, which is safe only if the caller guarantees no concurrent writes during enumeration. Since `for_each` is used in the XRPL codebase primarily for database sweep operations (e.g., replication or validation passes) that happen outside normal read/write activity, this is acceptable in practice, but it is an implicit contract rather than an enforced one. + +`fetchBatch()` is implemented by simply calling `fetch()` in a loop, acquiring and releasing the mutex for every element. There is no bulk lock optimization. For a testing backend where the map is small and contention is negligible, this is a reasonable tradeoff — simplicity over throughput. + +## No-Op Operations + +Several `Backend` interface methods are no-ops by design: `sync()` does nothing (there is no I/O to flush), `getWriteLoad()` returns 0 (no queue to measure), `setDeletePath()` does nothing (there is no path to delete), and `fdRequired()` returns 0 (no file descriptors are consumed). These stubs allow the memory backend to satisfy the full `Backend` interface contract without implementing concepts that have no meaning outside a disk-based store. + +## Registration Pattern + +The `registerMemoryFactory(Manager&)` free function is the intended entry point. Callers invoke it once at startup, passing the global `Manager` singleton. The function-local static `MemoryFactory instance` self-registers by calling `manager_.insert(*this)` in its constructor, making the factory discoverable by name (`"Memory"`) through `Manager::find()`. This registration-by-constructor pattern is shared with `NullFactory` and the other backends, enabling the `Manager` to act as a plugin registry without requiring explicit factory tables elsewhere. \ No newline at end of file diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp.ai.json b/src/libxrpl/nodestore/backend/NuDBFactory.cpp.ai.json new file mode 100644 index 0000000000..b8d93925b3 --- /dev/null +++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp.ai.json @@ -0,0 +1,529 @@ +{ + "args": [ + { + "lineno": 27, + "name": "keyBytes" + }, + { + "lineno": 27, + "name": "keyValues" + }, + { + "lineno": 27, + "name": "burstSize" + }, + { + "lineno": 27, + "name": "scheduler" + }, + { + "lineno": 27, + "name": "journal" + }, + { + "lineno": 39, + "name": "context" + }, + { + "lineno": 72, + "name": "createIfMissing" + }, + { + "lineno": 72, + "name": "appType" + }, + { + "lineno": 72, + "name": "uid" + }, + { + "lineno": 72, + "name": "salt" + }, + { + "lineno": 134, + "name": "hash" + }, + { + "lineno": 134, + "name": "pno" + }, + { + "lineno": 154, + "name": "hashes" + }, + { + "lineno": 171, + "name": "no" + }, + { + "lineno": 189, + "name": "batch" + }, + { + "lineno": 202, + "name": "f" + }, + { + "lineno": 257, + "name": "name" + }, + { + "lineno": 292, + "name": "manager" + } + ], + "classes": [ + { + "args": [ + "size_t keyBytes", + "Section const& keyValues", + "std::size_t burstSize", + "Scheduler& scheduler", + "beast::Journal journal" + ], + "lineno": 18, + "name": "NuDBBackend" + }, + { + "args": [ + "Manager& manager" + ], + "lineno": 289, + "name": "NuDBFactory" + } + ], + "code_paths": [ + { + "call_chain": [ + "NuDBBackend::NuDBBackend", + "get(keyValues, \"path\")", + "parseBlockSize(name_, keyValues, journal)" + ], + "entry_point": "NuDBBackend::NuDBBackend", + "purpose": "Constructs a NuDBBackend instance, extracting configuration from keyValues and validating required fields.", + "validation_points": [ + "Checks if name_ (path) is empty and throws if so.", + "parseBlockSize likely validates block size." + ] + }, + { + "call_chain": [ + "NuDBBackend::open", + "boost::filesystem::create_directories", + "nudb::create", + "db_.open" + ], + "entry_point": "NuDBBackend::open", + "purpose": "Opens (and possibly creates) the NuDB database, validating file existence and database header.", + "validation_points": [ + "Throws if database is already open.", + "Checks and throws on nudb::create error.", + "Checks and throws on db_.open error.", + "Checks db_.appnum() against expected appnum." + ] + } + ], + "data_flows": [ + { + "field": "name_", + "flow": [ + "keyValues (input Section)", + "get(keyValues, \"path\")", + "name_ (member variable)", + "used in open() as folder path" + ], + "origin": "get(keyValues, \"path\")", + "transformations": [ + "Direct assignment from config", + "Checked for emptiness in constructor" + ], + "validated_at": "NuDBBackend constructor (throws if empty)" + }, + { + "field": "blockSize_", + "flow": [ + "keyValues (input Section)", + "parseBlockSize", + "blockSize_ (member variable)", + "used in nudb::create" + ], + "origin": "parseBlockSize(name_, keyValues, journal)", + "transformations": [ + "Parsed and possibly validated in parseBlockSize" + ], + "validated_at": "parseBlockSize (details not shown, but likely validates block size)" + }, + { + "field": "db_", + "flow": [ + "db_ (member variable)", + "db_.open in open()", + "db_.appnum() checked after open" + ], + "origin": "nudb::store db_", + "transformations": [ + "Initialized in constructor", + "Opened in open()" + ], + "validated_at": "open() (checks is_open, error codes, appnum)" + } + ], + "description": "This file implements a NuDB backend for the XRPL NodeStore, providing persistent key-value storage using the NuDB database. It defines the NuDBBackend class for database operations and a NuDBFactory for backend instantiation and registration.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/backend/NuDBFactory.cpp", + "functions": [ + { + "args": [ + "size_t keyBytes", + "Section const& keyValues", + "std::size_t burstSize", + "Scheduler& scheduler", + "beast::Journal journal" + ], + "lineno": 27, + "name": "NuDBBackend" + }, + { + "args": [ + "size_t keyBytes", + "Section const& keyValues", + "std::size_t burstSize", + "Scheduler& scheduler", + "nudb::context& context", + "beast::Journal journal" + ], + "lineno": 39, + "name": "NuDBBackend" + }, + { + "args": [], + "lineno": 53, + "name": "~NuDBBackend" + }, + { + "args": [], + "lineno": 62, + "name": "getName" + }, + { + "args": [], + "lineno": 67, + "name": "getBlockSize" + }, + { + "args": [ + "bool createIfMissing", + "uint64_t appType", + "uint64_t uid", + "uint64_t salt" + ], + "lineno": 72, + "name": "open" + }, + { + "args": [], + "lineno": 108, + "name": "isOpen" + }, + { + "args": [ + "bool createIfMissing" + ], + "lineno": 113, + "name": "open" + }, + { + "args": [], + "lineno": 118, + "name": "close" + }, + { + "args": [ + "uint256 const& hash", + "std::shared_ptr* pno" + ], + "lineno": 134, + "name": "fetch" + }, + { + "args": [ + "std::vector const& hashes" + ], + "lineno": 154, + "name": "fetchBatch" + }, + { + "args": [ + "std::shared_ptr const& no" + ], + "lineno": 171, + "name": "do_insert" + }, + { + "args": [ + "std::shared_ptr const& no" + ], + "lineno": 179, + "name": "store" + }, + { + "args": [ + "Batch const& batch" + ], + "lineno": 189, + "name": "storeBatch" + }, + { + "args": [], + "lineno": 199, + "name": "sync" + }, + { + "args": [ + "std::function)> f" + ], + "lineno": 202, + "name": "for_each" + }, + { + "args": [], + "lineno": 227, + "name": "getWriteLoad" + }, + { + "args": [], + "lineno": 232, + "name": "setDeletePath" + }, + { + "args": [], + "lineno": 236, + "name": "verify" + }, + { + "args": [], + "lineno": 252, + "name": "fdRequired" + }, + { + "args": [ + "std::string const& name", + "Section const& keyValues", + "beast::Journal journal" + ], + "lineno": 257, + "name": "parseBlockSize" + }, + { + "args": [ + "Manager& manager" + ], + "lineno": 292, + "name": "NuDBFactory" + }, + { + "args": [], + "lineno": 296, + "name": "getName" + }, + { + "args": [ + "size_t keyBytes", + "Section const& keyValues", + "std::size_t burstSize", + "Scheduler& scheduler", + "beast::Journal journal" + ], + "lineno": 300, + "name": "createInstance" + }, + { + "args": [ + "size_t keyBytes", + "Section const& keyValues", + "std::size_t burstSize", + "Scheduler& scheduler", + "nudb::context& context", + "beast::Journal journal" + ], + "lineno": 307, + "name": "createInstance" + }, + { + "args": [ + "Manager& manager" + ], + "lineno": 314, + "name": "registerNuDBFactory" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + }, + { + "lineno": 16, + "name": "NodeStore" + } + ], + "test_coverage_notes": "NuDBBackend is a backend implementation for the NodeStore. Typical test coverage would be in integration tests for the NodeStore, possibly in files like 'NuDBBackend_test.cpp', 'NodeStore_test.cpp', or higher-level database/shard tests. The constructor's path validation and open()'s error handling are likely covered by tests that attempt to open with missing/invalid paths or corrupt files. However, edge cases such as file permission errors, partial file corruption, or concurrent open attempts may not be fully covered. LCOV_EXCL_START/STOP comments suggest some error paths are not covered by tests.", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/backend/NuDBFactory.cpp.ai.md b/src/libxrpl/nodestore/backend/NuDBFactory.cpp.ai.md new file mode 100644 index 0000000000..e7097bac10 --- /dev/null +++ b/src/libxrpl/nodestore/backend/NuDBFactory.cpp.ai.md @@ -0,0 +1,43 @@ +# NuDBFactory.cpp — NuDB Storage Backend for the XRPL NodeStore + +This file provides the concrete NuDB storage backend for the XRPL NodeStore subsystem. The NodeStore is the layer beneath the ledger that persists all `NodeObject` instances — SHAMap nodes, transactions, ledger headers — as an immutable content-addressed key-value store. `NuDBFactory.cpp` defines two classes: `NuDBBackend`, which wraps a NuDB database and implements the `Backend` interface, and `NuDBFactory`, which instantiates backends and registers them with the global `Manager`. + +## Why NuDB? + +NuDB is a hash-based, append-mostly key-value store designed specifically for workloads where keys are cryptographic hashes and values are small to medium-sized blobs. It keeps a separate data file (`nudb.dat`), a key-index file (`nudb.key`), and a write-ahead log (`nudb.log`). This layout matches the XRPL access pattern almost exactly: random-read-heavy, write-append-only, no deletes. Compared to RocksDB, NuDB trades away compaction, range iteration, and general-purpose flexibility in exchange for simpler I/O behavior and more predictable write amplification — which is why it remains the default backend for `rippled`. + +## Registration and Lifetime + +The `registerNuDBFactory()` free function creates a `static NuDBFactory` instance and passes a reference to it into the global `Manager` via `Manager::insert()`. The static local ensures registration happens exactly once regardless of how many translation units call it, and the factory outlives any backend instance it creates. This one-line idiom — `static NuDBFactory const instance{manager}` — is the standard pattern used by all backends in this directory. + +`NuDBFactory::createInstance()` has two overloads: one that constructs a `NuDBBackend` without a `nudb::context`, and one that accepts an existing `nudb::context`. A `nudb::context` owns background I/O threads that NuDB uses for asynchronous buffering; providing one enables shared I/O across multiple backends (relevant when multiple shards are open simultaneously). + +## Database Lifecycle + +`NuDBBackend` construction is lightweight — it parses configuration from a `Section` key-value map and validates the required `path` field, but does not touch the filesystem until `open()` is called. This two-phase initialization lets the caller catch I/O exceptions after construction. + +The full `open(bool createIfMissing, uint64_t appType, uint64_t uid, uint64_t salt)` overload supports deterministic database creation: the uid and salt are embedded in the NuDB file headers and must be reproduced consistently when reopening a database. The simplified `open(bool)` overload generates random uid/salt via `nudb::make_uid()` and `nudb::make_salt()`, which is appropriate for the main node store where only one instance is ever created for a given path. When `createIfMissing` is true, `nudb::create()` initializes the three files; if they already exist (`nudb::errc::file_exists`), the error is silently cleared and `db_.open()` proceeds normally. + +The `appnum` constant (`1`) is stored in the NuDB header at creation time and checked on every `open()`. Its only purpose now is a sanity check that the files were written by xrpld; historical shard-database differentiation code has been removed. After opening, `db_.set_burst(burstSize_)` configures NuDB's in-memory write buffer, which is a critical performance parameter: it determines how many bytes NuDB will accumulate before flushing to disk. + +`close()` logs at `fatal` level and throws on NuDB errors rather than silently swallowing them — a closed-with-error database is a serious condition. If `deletePath_` was set (via `setDeletePath()`, called for temporary databases), `boost::filesystem::remove_all()` deletes the entire database directory after the close succeeds. The `std::atomic` for `deletePath_` avoids a data race if the flag is set from a different thread than the one closing the backend. The destructor catches `nudb::system_error` from `close()` because destructors must not propagate exceptions; the error is already logged as `fatal` before being swallowed. + +## Compression Pipeline + +Every value passing through this backend is compressed before storage and decompressed on retrieval. The codec layer (`detail/codec.h`) uses LZ4 as the default compression algorithm, but has special-case handling for SHAMap inner nodes. When the input blob is exactly 525 bytes with a matching `HashPrefix::innerNode` prefix, `nodeobject_compress()` recognizes it as a SHAMap inner node and applies a sparse-hash encoding (type 2 or type 3) rather than LZ4: only non-zero 32-byte child hashes are stored along with a 16-bit presence bitmask, which is far more compact for partially-filled nodes. + +The full write path for a single object is: `store()` → `do_insert()` → `EncodedBlob(no)` (serializes `NodeObject` to raw bytes) → `nodeobject_compress()` (LZ4 or inner-node encoding) → `db_.insert()`. The read path inverts this: `fetch()` calls `db_.fetch()` with a callback lambda that receives a raw pointer into NuDB's internal buffer (no copy), decompresses inline with `nodeobject_decompress()`, then `DecodedBlob::createObject()` reconstructs the `NodeObject`. This zero-copy callback pattern is central to NuDB's design — the buffer is only valid within the callback, so decompression must happen there. + +Duplicate inserts are silently ignored: when `db_.insert()` returns `nudb::error::key_exists`, `do_insert()` discards the error. This is correct because the NodeStore is content-addressed — the same hash always maps to the same data. + +## Iteration and Verification + +`for_each()` and `verify()` share an unusual requirement: both must close the live database before operating and reopen it afterward. NuDB's `nudb::visit()` (used by `for_each`) reads the data file sequentially — a pattern incompatible with normal concurrent access through `nudb::store`. Similarly, `nudb::verify()` performs a consistency check by independently re-hashing every key and confirming it matches the stored key-file index. The `xxhasher` template parameter must match the one used at database creation time; the NuDB key file is organized around this hash function. `Backend.h` notes that `verify()` is not currently called at startup, though it would be valuable to do so. + +`fetchBatch()` is a sequential loop over individual `fetch()` calls — NuDB provides no native batch-read operation, so this offers no I/O parallelism. Missing or corrupt entries produce empty slots (`{}`) in the result vector rather than aborting the entire batch. + +## Configuration and Resource Accounting + +`parseBlockSize()` reads an optional `nudb_block_size` configuration key and validates that it is a power of 2 between 4096 and 32768 bytes. This value governs the page size of the NuDB key file; misaligned block sizes cause I/O amplification. The default comes from `nudb::block_size()`, which queries the filesystem for the native block size — typically 4096 bytes. `getBlockSize()` exposes this to higher-level callers so they can make storage-layout decisions. + +`fdRequired()` returns 3, directly reflecting the three physical files NuDB keeps open: data, key, and log. This lets the `NodeStore::Manager` pre-check that the process has enough file descriptors before attempting to open the database. `getWriteLoad()` returns 0 because NuDB's writes go through `do_insert()` synchronously — there is no internal write queue to measure, unlike the `BatchWriter` pattern used by RocksDB. \ No newline at end of file diff --git a/src/libxrpl/nodestore/backend/NullFactory.cpp.ai.json b/src/libxrpl/nodestore/backend/NullFactory.cpp.ai.json new file mode 100644 index 0000000000..21bbf098dc --- /dev/null +++ b/src/libxrpl/nodestore/backend/NullFactory.cpp.ai.json @@ -0,0 +1,411 @@ +{ + "args": [ + { + "lineno": 17, + "name": "createIfMissing" + }, + { + "lineno": 29, + "name": "uint256 const&" + }, + { + "lineno": 29, + "name": "std::shared_ptr*" + }, + { + "lineno": 34, + "name": "std::vector const& hashes" + }, + { + "lineno": 39, + "name": "std::shared_ptr const& object" + }, + { + "lineno": 43, + "name": "Batch const& batch" + }, + { + "lineno": 51, + "name": "std::function)> f" + }, + { + "lineno": 85, + "name": "Manager& manager" + }, + { + "lineno": 80, + "name": "size_t" + }, + { + "lineno": 80, + "name": "Section const&" + }, + { + "lineno": 80, + "name": "std::size_t" + }, + { + "lineno": 80, + "name": "Scheduler&" + }, + { + "lineno": 80, + "name": "beast::Journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "NullBackend" + }, + { + "args": [ + "Manager& manager" + ], + "lineno": 68, + "name": "NullFactory" + } + ], + "code_paths": [ + { + "call_chain": [ + "NullFactory::createInstance", + "std::make_unique()", + "NullBackend constructor" + ], + "entry_point": "NullFactory::createInstance", + "purpose": "Creates a new instance of NullBackend when requested by the Manager or other NodeStore infrastructure.", + "validation_points": [] + }, + { + "call_chain": [ + "NullBackend::fetch" + ], + "entry_point": "NullBackend::fetch", + "purpose": "Fetches a NodeObject by hash. In this implementation, always returns notFound.", + "validation_points": [] + }, + { + "call_chain": [ + "NullBackend::fetchBatch" + ], + "entry_point": "NullBackend::fetchBatch", + "purpose": "Fetches a batch of NodeObjects by hashes. Always returns empty.", + "validation_points": [] + }, + { + "call_chain": [ + "NullBackend::store" + ], + "entry_point": "NullBackend::store", + "purpose": "Stores a NodeObject. No-op in this implementation.", + "validation_points": [] + }, + { + "call_chain": [ + "NullBackend::storeBatch" + ], + "entry_point": "NullBackend::storeBatch", + "purpose": "Stores a batch of NodeObjects. No-op in this implementation.", + "validation_points": [] + }, + { + "call_chain": [ + "NullBackend::open" + ], + "entry_point": "NullBackend::open", + "purpose": "Opens the backend. No-op in this implementation.", + "validation_points": [] + }, + { + "call_chain": [ + "NullBackend::isOpen" + ], + "entry_point": "NullBackend::isOpen", + "purpose": "Checks if backend is open. Always returns false.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "uint256 const& (hash)", + "flow": [ + "fetch parameter", + "immediately ignored", + "returns notFound" + ], + "origin": "Parameter to NullBackend::fetch", + "transformations": [], + "validated_at": null + }, + { + "field": "std::vector const& (hashes)", + "flow": [ + "fetchBatch parameter", + "immediately ignored", + "returns empty vector and default Status" + ], + "origin": "Parameter to NullBackend::fetchBatch", + "transformations": [], + "validated_at": null + }, + { + "field": "std::shared_ptr const& (object)", + "flow": [ + "store parameter", + "immediately ignored", + "no storage or validation" + ], + "origin": "Parameter to NullBackend::store", + "transformations": [], + "validated_at": null + }, + { + "field": "Batch const& (batch)", + "flow": [ + "storeBatch parameter", + "immediately ignored", + "no storage or validation" + ], + "origin": "Parameter to NullBackend::storeBatch", + "transformations": [], + "validated_at": null + }, + { + "field": "Manager& manager", + "flow": [ + "NullFactory constructor", + "stored as member", + "used in manager_.insert(*this)" + ], + "origin": "Parameter to NullFactory constructor", + "transformations": [], + "validated_at": null + } + ], + "description": "Implements a NullBackend and NullFactory for the XRPL NodeStore, providing a no-op backend for testing or placeholder purposes.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/backend/NullFactory.cpp", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "NullBackend::getName" + }, + { + "args": [ + "createIfMissing" + ], + "lineno": 17, + "name": "NullBackend::open" + }, + { + "args": [], + "lineno": 21, + "name": "NullBackend::isOpen" + }, + { + "args": [], + "lineno": 25, + "name": "NullBackend::close" + }, + { + "args": [ + "uint256 const&", + "std::shared_ptr*" + ], + "lineno": 29, + "name": "NullBackend::fetch" + }, + { + "args": [ + "std::vector const& hashes" + ], + "lineno": 34, + "name": "NullBackend::fetchBatch" + }, + { + "args": [ + "std::shared_ptr const& object" + ], + "lineno": 39, + "name": "NullBackend::store" + }, + { + "args": [ + "Batch const& batch" + ], + "lineno": 43, + "name": "NullBackend::storeBatch" + }, + { + "args": [], + "lineno": 47, + "name": "NullBackend::sync" + }, + { + "args": [ + "std::function)> f" + ], + "lineno": 51, + "name": "NullBackend::for_each" + }, + { + "args": [], + "lineno": 55, + "name": "NullBackend::getWriteLoad" + }, + { + "args": [], + "lineno": 59, + "name": "NullBackend::setDeletePath" + }, + { + "args": [], + "lineno": 63, + "name": "NullBackend::fdRequired" + }, + { + "args": [], + "lineno": 76, + "name": "NullFactory::getName" + }, + { + "args": [ + "size_t", + "Section const&", + "std::size_t", + "Scheduler&", + "beast::Journal" + ], + "lineno": 80, + "name": "NullFactory::createInstance" + }, + { + "args": [ + "Manager& manager" + ], + "lineno": 85, + "name": "registerNullFactory" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "NodeStore" + } + ], + "test_coverage_notes": "This NullBackend is a stub/no-op implementation. There are no validation code paths: all inputs are ignored, and no data is processed or validated. Test coverage is likely minimal or non-existent for this file, as its purpose is to provide a 'do nothing' backend for testing or configuration purposes. If tested, it would be in integration tests ensuring the system can operate with a null backend, but there are no field-level or validation tests required or present. No explicit test files are referenced or implied by this code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/backend/NullFactory.cpp.ai.md b/src/libxrpl/nodestore/backend/NullFactory.cpp.ai.md new file mode 100644 index 0000000000..e302449c35 --- /dev/null +++ b/src/libxrpl/nodestore/backend/NullFactory.cpp.ai.md @@ -0,0 +1,23 @@ +# `NullFactory.cpp` — No-Op NodeStore Backend + +This file provides the "none" backend for the XRPL NodeStore: a complete, compilable implementation of `Backend` and `Factory` that does absolutely nothing. Every read returns `notFound`, every write is silently discarded, `open()` and `close()` are empty, and `isOpen()` always returns `false`. Its existence is not a mistake — it is a deliberate, necessary piece of the backend registry that supports configuration-driven operation without live storage. + +## The Two-Class Pattern + +The file follows the same two-class structure used by every other backend in the `backend/` directory (NuDB, RocksDB, Memory): an inner `Backend` subclass holds the actual storage logic, and a companion `Factory` subclass knows how to construct it and registers itself with the global `Manager` singleton. + +`NullBackend` inherits from `Backend` and satisfies every pure-virtual requirement in that interface. The `fetch()` method returns the `notFound` status immediately without touching its arguments. `fetchBatch()` returns a default-constructed `std::pair`, meaning an empty vector and a default `Status`. Both `store()` and `storeBatch()` are empty-bodied. `for_each()` never invokes the supplied callback. `getWriteLoad()` returns zero, `fdRequired()` returns zero, and `getName()` returns an empty string (since there is no named file or path to report). The `isOpen()` override always returns `false`, which is honest: the backend never transitions to an open state. + +`NullFactory` holds a reference to the `Manager` it was given at construction and registers itself by calling `manager_.insert(*this)` in its constructor. Its `getName()` override returns `"none"`, which is the configuration string that triggers this backend when a node operator sets `type=none` (case-insensitive, matched by `Manager::find()`). `createInstance()` simply heap-allocates a fresh `NullBackend` and returns it as a `std::unique_ptr`. + +## Registration and Lifetime + +`registerNullFactory()` is a free function that the `ManagerImp` constructor calls during the singleton's initialization, alongside the equivalent registration functions for NuDB, RocksDB, and the in-memory backend. Inside `registerNullFactory()`, the factory object is declared as a `static NullFactory const instance{manager}`. The `static` local variable guarantees thread-safe, once-only initialization under C++11 and later, and the `const` qualifier reinforces that the factory is never mutated after construction. The factory's self-registration into `manager_` happens at that first-construction moment and persists for the process lifetime. + +This design avoids any global-variable initialization-order hazard: the `Manager` reference is passed in explicitly from the already-constructed `ManagerImp`, and the factory object only comes alive at the point of the call, not at program startup. + +## Why This Backend Exists + +A null backend is architecturally useful in several scenarios. During testing, components that depend on a `Backend` interface can be wired to `NullBackend` without provisioning any real storage — all lookups will miss, which is predictable and deterministic. In production configurations where a node operator wants to operate without a persistent store (relying entirely on an ephemeral or in-memory layer), `type=none` provides a safe, named configuration value rather than an unconfigured or absent backend. Without a registered `"none"` factory, the manager's `find()` call for that type string would return `nullptr`, likely producing a fatal error at startup. + +The backend also serves as a clean documentation artifact: reading it defines the minimal contract that all `Backend` implementations must satisfy, since every pure-virtual method from `Backend.h` appears here in its simplest possible form. No caching, no I/O, no scheduling, no error paths — just the interface skeleton made concrete. \ No newline at end of file diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp.ai.json b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp.ai.json new file mode 100644 index 0000000000..ca7a42e38e --- /dev/null +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp.ai.json @@ -0,0 +1,778 @@ +{ + "args": [ + { + "lineno": 19, + "name": "f_" + }, + { + "lineno": 19, + "name": "a_" + }, + { + "lineno": 21, + "name": "f" + }, + { + "lineno": 22, + "name": "a" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "RocksDBEnv" + }, + { + "args": [ + "f_", + "a_" + ], + "lineno": 18, + "name": "RocksDBEnv::ThreadParams" + }, + { + "args": [ + "keyBytes", + "keyValues", + "scheduler", + "journal", + "env" + ], + "lineno": 52, + "name": "RocksDBBackend" + }, + { + "args": [ + "manager" + ], + "lineno": 292, + "name": "RocksDBFactory" + } + ], + "code_paths": [ + { + "call_chain": [ + "RocksDBBackend::RocksDBBackend", + "get_if_exists / get", + "Throw (on validation failure)" + ], + "entry_point": "RocksDBBackend::RocksDBBackend", + "purpose": "Constructs a RocksDBBackend, validates and extracts configuration from keyValues Section, sets up RocksDB options.", + "validation_points": [ + "get_if_exists(keyValues, \"path\", m_name) - validates 'path' exists", + "get(keyValues, \"cache_mb\") - validates and extracts 'cache_mb'", + "get(keyValues, \"hard_set\") - validates and extracts 'hard_set'", + "get(keyValues, \"filter_bits\") - validates and extracts 'filter_bits'", + "get(keyValues, \"filter_full\") - validates and extracts 'filter_full'", + "get_if_exists(keyValues, \"open_files\", m_options.max_open_files) - validates 'open_files' if present", + "get(keyValues, \"file_size_mb\") - validates and extracts 'file_size_mb'", + "get_if_exists(keyValues, \"file_size_mult\", m_options.target_file_size_multiplier) - validates 'file_size_mult' if present", + "get(keyValues, \"bg_threads\") - validates and extracts 'bg_threads'", + "get(keyValues, \"high_threads\") - validates and extracts 'high_threads'" + ] + } + ], + "data_flows": [ + { + "field": "m_name (path)", + "flow": [ + "keyValues['path']", + "get_if_exists(keyValues, 'path', m_name)", + "m_name" + ], + "origin": "keyValues Section (configuration input)", + "transformations": [ + "Direct assignment if exists, else throws" + ], + "validated_at": "get_if_exists(keyValues, 'path', m_name)" + }, + { + "field": "cache_mb", + "flow": [ + "keyValues['cache_mb']", + "get(keyValues, 'cache_mb')", + "size", + "table_options.block_cache = rocksdb::NewLRUCache(megabytes(size))" + ], + "origin": "keyValues Section", + "transformations": [ + "Casted to int, defaulted to 1024 if not hard_set and value is 256" + ], + "validated_at": "get(keyValues, 'cache_mb')" + }, + { + "field": "hard_set", + "flow": [ + "keyValues['hard_set']", + "get(keyValues, 'hard_set')", + "hard_set" + ], + "origin": "keyValues Section", + "transformations": [ + "Casted to bool" + ], + "validated_at": "get(keyValues, 'hard_set')" + }, + { + "field": "filter_bits", + "flow": [ + "keyValues['filter_bits']", + "get(keyValues, 'filter_bits')", + "v", + "table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(v, filter_blocks))" + ], + "origin": "keyValues Section", + "transformations": [ + "Casted to int" + ], + "validated_at": "get(keyValues, 'filter_bits')" + }, + { + "field": "filter_full", + "flow": [ + "keyValues['filter_full']", + "get(keyValues, 'filter_full')", + "used to determine filter_blocks" + ], + "origin": "keyValues Section", + "transformations": [ + "Casted to int, used in boolean logic" + ], + "validated_at": "get(keyValues, 'filter_full')" + }, + { + "field": "open_files", + "flow": [ + "keyValues['open_files']", + "get_if_exists(keyValues, 'open_files', m_options.max_open_files)", + "m_options.max_open_files" + ], + "origin": "keyValues Section", + "transformations": [ + "Direct assignment, defaulted to 8000 if not hard_set and value is 2000" + ], + "validated_at": "get_if_exists(keyValues, 'open_files', m_options.max_open_files)" + }, + { + "field": "file_size_mb", + "flow": [ + "keyValues['file_size_mb']", + "get(keyValues, 'file_size_mb')", + "file_size_mb", + "m_options.target_file_size_base = megabytes(file_size_mb)", + "m_options.max_bytes_for_level_base = 5 * m_options.target_file_size_base", + "m_options.write_buffer_size = 2 * m_options.target_file_size_base" + ], + "origin": "keyValues Section", + "transformations": [ + "Casted to int, defaulted to 256 if not hard_set and value is 8" + ], + "validated_at": "get(keyValues, 'file_size_mb')" + }, + { + "field": "file_size_mult", + "flow": [ + "keyValues['file_size_mult']", + "get_if_exists(keyValues, 'file_size_mult', m_options.target_file_size_multiplier)", + "m_options.target_file_size_multiplier" + ], + "origin": "keyValues Section", + "transformations": [ + "Direct assignment if exists" + ], + "validated_at": "get_if_exists(keyValues, 'file_size_mult', m_options.target_file_size_multiplier)" + }, + { + "field": "bg_threads", + "flow": [ + "keyValues['bg_threads']", + "get(keyValues, 'bg_threads')", + "m_options.env->SetBackgroundThreads(get(keyValues, 'bg_threads'), rocksdb::Env::LOW)" + ], + "origin": "keyValues Section", + "transformations": [ + "Casted to int, used as argument" + ], + "validated_at": "get(keyValues, 'bg_threads')" + }, + { + "field": "high_threads", + "flow": [ + "keyValues['high_threads']", + "get(keyValues, 'high_threads')", + "m_options.env->SetBackgroundThreads(get(keyValues, 'high_threads'), rocksdb::Env::HIGH)" + ], + "origin": "keyValues Section", + "transformations": [ + "Casted to int, used as argument" + ], + "validated_at": "get(keyValues, 'high_threads')" + } + ], + "description": "This file implements a RocksDB backend for the XRPL NodeStore, providing classes and logic to manage RocksDB environments, backend storage, and factory registration for use within the XRPL node database system.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "cache_mb", + "validation", + "missing", + "check" + ], + "evidence": "Field cache_mb validated by xrpl::get, xrpl::get_if_exists, xrpl::Throw", + "issue_pattern": "Missing validation for cache_mb", + "why_false_positive": "xrpl::get, xrpl::get_if_exists, xrpl::Throw validates cache_mb automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "hard_set", + "validation", + "missing", + "check" + ], + "evidence": "Field hard_set validated by xrpl::get, xrpl::get_if_exists, xrpl::Throw", + "issue_pattern": "Missing validation for hard_set", + "why_false_positive": "xrpl::get, xrpl::get_if_exists, xrpl::Throw validates hard_set automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "filter_bits", + "validation", + "missing", + "check" + ], + "evidence": "Field filter_bits validated by xrpl::get, xrpl::get_if_exists, xrpl::Throw", + "issue_pattern": "Missing validation for filter_bits", + "why_false_positive": "xrpl::get, xrpl::get_if_exists, xrpl::Throw validates filter_bits automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "filter_full", + "validation", + "missing", + "check" + ], + "evidence": "Field filter_full validated by xrpl::get, xrpl::get_if_exists, xrpl::Throw", + "issue_pattern": "Missing validation for filter_full", + "why_false_positive": "xrpl::get, xrpl::get_if_exists, xrpl::Throw validates filter_full automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "open_files", + "validation", + "missing", + "check" + ], + "evidence": "Field open_files validated by xrpl::get, xrpl::get_if_exists, xrpl::Throw", + "issue_pattern": "Missing validation for open_files", + "why_false_positive": "xrpl::get, xrpl::get_if_exists, xrpl::Throw validates open_files automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "path", + "empty", + "string", + "validation" + ], + "evidence": "get_if_exists at RocksDBBackend constructor", + "issue_pattern": "Missing empty string validation for path", + "why_false_positive": "get_if_exists validates path for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "cache_mb", + "empty", + "string", + "validation" + ], + "evidence": "get at RocksDBBackend constructor", + "issue_pattern": "Missing empty string validation for cache_mb", + "why_false_positive": "get validates cache_mb for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "cache_mb", + "type", + "validation", + "check" + ], + "evidence": "get at RocksDBBackend constructor", + "issue_pattern": "Missing type validation for cache_mb", + "why_false_positive": "get validates cache_mb type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "hard_set", + "empty", + "string", + "validation" + ], + "evidence": "get at RocksDBBackend constructor", + "issue_pattern": "Missing empty string validation for hard_set", + "why_false_positive": "get validates hard_set for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "hard_set", + "type", + "validation", + "check" + ], + "evidence": "get at RocksDBBackend constructor", + "issue_pattern": "Missing type validation for hard_set", + "why_false_positive": "get validates hard_set type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "filter_bits", + "empty", + "string", + "validation" + ], + "evidence": "get at RocksDBBackend constructor", + "issue_pattern": "Missing empty string validation for filter_bits", + "why_false_positive": "get validates filter_bits for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "filter_bits", + "type", + "validation", + "check" + ], + "evidence": "get at RocksDBBackend constructor", + "issue_pattern": "Missing type validation for filter_bits", + "why_false_positive": "get validates filter_bits type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "filter_full", + "empty", + "string", + "validation" + ], + "evidence": "get at RocksDBBackend constructor", + "issue_pattern": "Missing empty string validation for filter_full", + "why_false_positive": "get validates filter_full for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "filter_full", + "type", + "validation", + "check" + ], + "evidence": "get at RocksDBBackend constructor", + "issue_pattern": "Missing type validation for filter_full", + "why_false_positive": "get validates filter_full type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "open_files", + "empty", + "string", + "validation" + ], + "evidence": "get_if_exists at RocksDBBackend constructor (truncated in code)", + "issue_pattern": "Missing empty string validation for open_files", + "why_false_positive": "get_if_exists validates open_files for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/nodestore/backend/RocksDBFactory.cpp", + "functions": [ + { + "args": [ + "ptr" + ], + "lineno": 27, + "name": "thread_entry" + }, + { + "args": [ + "f", + "a" + ], + "lineno": 41, + "name": "StartThread" + }, + { + "args": [ + "createIfMissing" + ], + "lineno": 110, + "name": "open" + }, + { + "args": [], + "lineno": 130, + "name": "isOpen" + }, + { + "args": [], + "lineno": 135, + "name": "close" + }, + { + "args": [], + "lineno": 146, + "name": "getName" + }, + { + "args": [ + "hash", + "pObject" + ], + "lineno": 153, + "name": "fetch" + }, + { + "args": [ + "hashes" + ], + "lineno": 186, + "name": "fetchBatch" + }, + { + "args": [ + "object" + ], + "lineno": 208, + "name": "store" + }, + { + "args": [ + "batch" + ], + "lineno": 212, + "name": "storeBatch" + }, + { + "args": [], + "lineno": 237, + "name": "sync" + }, + { + "args": [ + "f" + ], + "lineno": 240, + "name": "for_each" + }, + { + "args": [], + "lineno": 270, + "name": "getWriteLoad" + }, + { + "args": [], + "lineno": 274, + "name": "setDeletePath" + }, + { + "args": [ + "batch" + ], + "lineno": 279, + "name": "writeBatch" + }, + { + "args": [], + "lineno": 285, + "name": "fdRequired" + }, + { + "args": [], + "lineno": 304, + "name": "getName" + }, + { + "args": [ + "keyBytes", + "keyValues", + "", + "scheduler", + "journal" + ], + "lineno": 308, + "name": "createInstance" + }, + { + "args": [ + "manager" + ], + "lineno": 316, + "name": "registerRocksDBFactory" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "NodeStore" + } + ], + "test_coverage_notes": "Testing for this code is likely found in integration or backend tests for NodeStore, such as 'nodestore/RocksDBBackend_test.cpp', 'nodestore/Database_test.cpp', or higher-level tests that configure the NodeStore with various Section parameters. Unit tests would need to cover: missing/invalid 'path', invalid types for 'cache_mb', 'filter_bits', etc., and boundary values for all validated fields. Gaps: There may be insufficient negative tests for missing/invalid config fields, and not all edge cases (e.g., extremely large/small values, type mismatches) may be covered. Exception paths (Throw) should be explicitly tested.", + "validation_architecture": { + "auto_validated_fields": [ + "cache_mb", + "hard_set", + "filter_bits", + "filter_full", + "open_files" + ], + "framework": "xrpl::get, xrpl::get_if_exists, xrpl::Throw", + "validation_layer": "business_logic (constructor)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "path", + "location": "RocksDBBackend constructor", + "validated_by": "get_if_exists", + "validates": [ + "checks if 'path' exists in keyValues Section" + ], + "validation_type": "business_logic|presence" + }, + { + "confidence": 0.9, + "error_thrown": "std::exception (if type mismatch in get)", + "field": "cache_mb", + "location": "RocksDBBackend constructor", + "validated_by": "get", + "validates": [ + "checks if 'cache_mb' exists and is convertible to int" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "std::exception (if type mismatch in get)", + "field": "hard_set", + "location": "RocksDBBackend constructor", + "validated_by": "get", + "validates": [ + "checks if 'hard_set' exists and is convertible to bool" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "std::exception (if type mismatch in get)", + "field": "filter_bits", + "location": "RocksDBBackend constructor", + "validated_by": "get", + "validates": [ + "checks if 'filter_bits' exists and is convertible to int" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "std::exception (if type mismatch in get)", + "field": "filter_full", + "location": "RocksDBBackend constructor", + "validated_by": "get", + "validates": [ + "checks if 'filter_full' exists and is convertible to int" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "unknown (code truncated, likely std::exception or similar)", + "field": "open_files", + "location": "RocksDBBackend constructor (truncated in code)", + "validated_by": "get_if_exists", + "validates": [ + "checks if 'open_files' exists and is convertible to expected type" + ], + "validation_type": "type|presence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/nodestore/backend/RocksDBFactory.cpp.ai.md b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp.ai.md new file mode 100644 index 0000000000..c6123e8ff5 --- /dev/null +++ b/src/libxrpl/nodestore/backend/RocksDBFactory.cpp.ai.md @@ -0,0 +1,47 @@ +# `RocksDBFactory.cpp` — RocksDB Backend for the XRPL NodeStore + +## Role in the System + +The XRPL NodeStore is a pluggable key-value storage layer that holds serialized ledger objects (transactions, account states, ledger headers) keyed by their 256-bit hash. `RocksDBFactory.cpp` provides one concrete storage backend — RocksDB — beneath that abstraction. The entire file is guarded by `#if XRPL_ROCKSDB_AVAILABLE`, making RocksDB an optional dependency that can be compiled out entirely. + +Three classes collaborate to deliver this backend: `RocksDBEnv` integrates RocksDB's threading into XRPL's naming conventions, `RocksDBBackend` implements the full `Backend` contract, and `RocksDBFactory` advertises the backend to the NodeStore's plugin registry. + +## `RocksDBEnv` — Thread Naming Shim + +`RocksDBEnv` derives from `rocksdb::EnvWrapper`, which is RocksDB's mechanism for intercepting OS-level operations. The only method it overrides is `StartThread`. When RocksDB creates an internal thread (compaction workers, flush threads, etc.), the overridden `StartThread` wraps the original function pointer and argument in a heap-allocated `ThreadParams` struct, then invokes `thread_entry` instead. Inside `thread_entry`, the struct is deleted, and a monotonically incrementing `std::atomic` counter assigns each thread a unique name in the form `"rocksdb #N"` via `beast::setCurrentThreadName`. This makes RocksDB's background threads visible and identifiable in profilers and crash dumps — a purely operational concern, not a correctness one. + +## `RocksDBBackend` — Storage Implementation + +`RocksDBBackend` implements both `Backend` (the NodeStore interface) and `BatchWriter::Callback` (the async-write protocol). The dual inheritance is deliberate: the backend exposes `writeBatch()` to `BatchWriter` while hiding it from the broader `Backend` consumers. The concrete `writeBatch()` just calls `storeBatch()`, making the async path a thin wrapper around the same synchronous write logic. + +### Configuration + +The constructor accepts a `Section` of key-value config pairs and translates them into `rocksdb::Options` and `rocksdb::BlockBasedTableOptions`. Several settings exhibit a "legacy default escalation" pattern controlled by the `hard_set` flag. For example, a configured `cache_mb = 256` is silently promoted to 1024 MB, and `open_files = 2000` becomes 8000, unless the config also sets `hard_set = true`. This reflects accumulated operational knowledge that the original documented defaults were too conservative for production; `hard_set` lets operators freeze the values at their literal specified amounts when they truly mean them. + +When `open_files` is set, `fdRequired_` is calculated as `max_open_files + 128` to give the process enough headroom in its file-descriptor table. The `fdRequired()` method exposes this count upward so the process can pre-check system limits before opening databases. + +Two escape hatches accept raw RocksDB option strings: `bbt_options` feeds `rocksdb::GetBlockBasedTableOptionsFromString`, and `options` feeds `rocksdb::GetOptionsFromString`. Both call `Throw` on parse failure. After the options object is fully assembled, the constructor logs the resolved `DBOptions` and `ColumnFamilyOptions` at debug level — useful for verifying that escalated defaults or raw string overrides took effect as intended. + +### Open/Close Lifecycle + +`open()` asserts the database is not already open (`UNREACHABLE` guard with `LCOV_EXCL_START/STOP` to exclude from coverage), sets `create_if_missing` from the caller's flag, and calls `rocksdb::DB::Open`, which returns a raw `rocksdb::DB*`. The pointer is immediately adopted into `m_db` (a `std::unique_ptr`). If the status indicates failure or the pointer is null, an exception is thrown. The destructor calls `close()`, which resets `m_db` (triggering RocksDB's own cleanup), then conditionally removes the database directory if `m_deletePath` was set. The flag is an `std::atomic` because `setDeletePath()` is part of the public `Backend` interface and may be called from threads different from the one that destroys the object. + +### Write Path + +Individual writes go through `store()` → `m_batch.store()`. `BatchWriter` accumulates objects and schedules a `Task` on the node's `Scheduler`. When the scheduler fires the task, `BatchWriter` calls back into `writeBatch()`, which calls `storeBatch()`. That method encodes each `NodeObject` via `EncodedBlob`, packs all encoded key/value pairs into a single `rocksdb::WriteBatch`, and commits with `m_db->Write()`. Writing in a single `WriteBatch` is atomically durable in RocksDB's WAL — either all objects in the group are recoverable or none are. Failure throws a `std::runtime_error`. + +The `sync()` method is deliberately empty. RocksDB's write-ahead log provides crash durability automatically; there is no need for an explicit fsync barrier at the NodeStore level. + +### Read Path + +`fetch()` constructs a `rocksdb::Slice` directly over the `uint256` hash bytes via `std::bit_cast`, avoiding any copy of the key. The returned value string is passed to `DecodedBlob`, which reconstructs the `NodeObject`. Three failure modes are distinguished: `dataCorrupt` for both a corrupt RocksDB status and a `DecodedBlob` that fails to parse, `notFound` for a clean miss, and a catch-all `customCode + status.code()` for other RocksDB errors — each mapped to the `Status` enum so callers can decide whether to fall through to another cache tier or surface the error. + +`fetchBatch()` is not atomic — it serially calls `fetch()` for each hash and inserts a null `shared_ptr` for any miss or error, always returning an overall status of `ok`. This contrasts with `storeBatch()`, where a single `WriteBatch` provides atomic group semantics. The asymmetry is intentional: reads are inherently independent, and RocksDB offers no multi-get API that would change the semantics meaningfully here. + +### Iteration + +`for_each()` creates a plain `rocksdb::Iterator` without snapshot pinning. Per the `Backend` contract, it is only called during database import and never concurrently with other operations. Entries with unexpected key sizes are logged at fatal level (a defensive guard left from early development) rather than throwing, allowing the iterator to continue past potential corruption. + +## `RocksDBFactory` — Plugin Registration + +`RocksDBFactory` holds a single `RocksDBEnv` instance, shared across all backends it creates. Its constructor calls `manager_.insert(*this)` to register with the NodeStore's `Manager` registry. The public entry point is the free function `registerRocksDBFactory(Manager&)`, which uses a `static` local variable to guarantee singleton initialization and exactly one registration per process. `createInstance()` ignores the `burstSize` parameter (second-to-last argument unnamed), as RocksDB manages its own internal buffering independently of any externally imposed burst limit. \ No newline at end of file diff --git a/src/libxrpl/protocol/AMMCore.cpp.ai.json b/src/libxrpl/protocol/AMMCore.cpp.ai.json new file mode 100644 index 0000000000..dbfb116fe0 --- /dev/null +++ b/src/libxrpl/protocol/AMMCore.cpp.ai.json @@ -0,0 +1,494 @@ +{ + "args": [ + { + "lineno": 12, + "name": "asset1" + }, + { + "lineno": 12, + "name": "asset2" + }, + { + "lineno": 29, + "name": "asset1" + }, + { + "lineno": 29, + "name": "asset2" + }, + { + "lineno": 29, + "name": "ammAccountID" + }, + { + "lineno": 34, + "name": "asset" + }, + { + "lineno": 34, + "name": "pair" + }, + { + "lineno": 52, + "name": "asset1" + }, + { + "lineno": 52, + "name": "asset2" + }, + { + "lineno": 52, + "name": "pair" + }, + { + "lineno": 63, + "name": "amount" + }, + { + "lineno": 63, + "name": "pair" + }, + { + "lineno": 63, + "name": "validZero" + }, + { + "lineno": 73, + "name": "current" + }, + { + "lineno": 73, + "name": "auctionSlot" + }, + { + "lineno": 91, + "name": "rules" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "invalidAMMAsset" + ], + "entry_point": "invalidAMMAsset", + "purpose": "Validates a single AMM asset for correctness (issuer, currency, etc.)", + "validation_points": [ + "MPTIssue.issuer validated by lambda in invalidAMMAsset", + "Issue.currency validated by lambda in invalidAMMAsset", + "Issue.issuer validated by lambda in invalidAMMAsset", + "asset validated by invalidAMMAsset" + ] + }, + { + "call_chain": [ + "invalidAMMAssetPair", + "invalidAMMAsset" + ], + "entry_point": "invalidAMMAssetPair", + "purpose": "Validates a pair of AMM assets for correctness and uniqueness", + "validation_points": [ + "asset1 == asset2 check in invalidAMMAssetPair", + "asset1 validated by invalidAMMAsset", + "asset2 validated by invalidAMMAsset" + ] + }, + { + "call_chain": [ + "invalidAMMAmount", + "invalidAMMAsset" + ], + "entry_point": "invalidAMMAmount", + "purpose": "Validates an AMM amount (including asset and value)", + "validation_points": [ + "amount.asset() validated by invalidAMMAsset", + "amount value validated in invalidAMMAmount" + ] + }, + { + "call_chain": [ + "ammLPTCurrency" + ], + "entry_point": "ammLPTCurrency", + "purpose": "Computes the LP token currency for a pair of assets (no validation, but uses asset fields)", + "validation_points": [] + }, + { + "call_chain": [ + "ammLPTIssue", + "ammLPTCurrency" + ], + "entry_point": "ammLPTIssue", + "purpose": "Computes the LP token Issue for a pair of assets and an account (no validation, but uses asset fields)", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "MPTIssue.issuer", + "flow": [ + "MPTIssue.issuer", + "invalidAMMAsset lambda", + "if (issue.getIssuer() == beast::zero)", + "return temBAD_MPT or std::nullopt" + ], + "origin": "MPTIssue object (input to Asset variant)", + "transformations": [ + "Checked for zero value" + ], + "validated_at": "invalidAMMAsset" + }, + { + "field": "Issue.currency", + "flow": [ + "Issue.currency", + "invalidAMMAsset lambda", + "if (badCurrency() == issue.currency)", + "return temBAD_CURRENCY or std::nullopt" + ], + "origin": "Issue object (input to Asset variant)", + "transformations": [ + "Compared to badCurrency()" + ], + "validated_at": "invalidAMMAsset" + }, + { + "field": "Issue.issuer", + "flow": [ + "Issue.issuer", + "invalidAMMAsset lambda", + "if (isXRP(issue) && issue.getIssuer().isNonZero())", + "return temBAD_ISSUER or std::nullopt" + ], + "origin": "Issue object (input to Asset variant)", + "transformations": [ + "Checked for nonzero issuer if currency is XRP" + ], + "validated_at": "invalidAMMAsset" + }, + { + "field": "asset", + "flow": [ + "asset", + "visit (MPTIssue/Issue)", + "validation lambdas", + "optional pair check", + "return error or tesSUCCESS" + ], + "origin": "Function argument to invalidAMMAsset", + "transformations": [ + "Type-dispatched validation", + "Optional pair membership check" + ], + "validated_at": "invalidAMMAsset" + }, + { + "field": "asset1, asset2", + "flow": [ + "asset1, asset2", + "invalidAMMAssetPair", + "asset1 == asset2 check", + "invalidAMMAsset(asset1)", + "invalidAMMAsset(asset2)", + "return error or tesSUCCESS" + ], + "origin": "Function arguments to invalidAMMAssetPair", + "transformations": [ + "Uniqueness check", + "Delegated to invalidAMMAsset" + ], + "validated_at": "invalidAMMAssetPair" + }, + { + "field": "amount", + "flow": [ + "amount", + "amount.asset()", + "invalidAMMAsset", + "amount value check (< 0 or == 0)", + "return error or tesSUCCESS" + ], + "origin": "Function argument to invalidAMMAmount", + "transformations": [ + "Asset validated", + "Value checked for negativity or zero" + ], + "validated_at": "invalidAMMAmount" + } + ], + "description": "This file provides utility functions for Automated Market Maker (AMM) logic in the XRPL codebase, including validation of AMM assets and amounts, generation of LP token currency and issue, and AMM auction time slot calculation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "MPTIssue.issuer", + "empty", + "string", + "validation" + ], + "evidence": "lambda in invalidAMMAsset at invalidAMMAsset", + "issue_pattern": "Missing empty string validation for MPTIssue.issuer", + "why_false_positive": "lambda in invalidAMMAsset validates MPTIssue.issuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Issue.currency", + "empty", + "string", + "validation" + ], + "evidence": "lambda in invalidAMMAsset at invalidAMMAsset", + "issue_pattern": "Missing empty string validation for Issue.currency", + "why_false_positive": "lambda in invalidAMMAsset validates Issue.currency for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Issue.issuer", + "empty", + "string", + "validation" + ], + "evidence": "lambda in invalidAMMAsset at invalidAMMAsset", + "issue_pattern": "Missing empty string validation for Issue.issuer", + "why_false_positive": "lambda in invalidAMMAsset validates Issue.issuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "asset", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAsset at invalidAMMAsset", + "issue_pattern": "Missing empty string validation for asset", + "why_false_positive": "invalidAMMAsset validates asset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "asset1, asset2", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAssetPair at invalidAMMAssetPair", + "issue_pattern": "Missing empty string validation for asset1, asset2", + "why_false_positive": "invalidAMMAssetPair validates asset1, asset2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "asset1", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAsset at invalidAMMAssetPair", + "issue_pattern": "Missing empty string validation for asset1", + "why_false_positive": "invalidAMMAsset validates asset1 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "asset2", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAsset at invalidAMMAssetPair", + "issue_pattern": "Missing empty string validation for asset2", + "why_false_positive": "invalidAMMAsset validates asset2 for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/AMMCore.cpp", + "functions": [ + { + "args": [ + "asset1", + "asset2" + ], + "lineno": 12, + "name": "ammLPTCurrency" + }, + { + "args": [ + "asset1", + "asset2", + "ammAccountID" + ], + "lineno": 29, + "name": "ammLPTIssue" + }, + { + "args": [ + "asset", + "pair" + ], + "lineno": 34, + "name": "invalidAMMAsset" + }, + { + "args": [ + "asset1", + "asset2", + "pair" + ], + "lineno": 52, + "name": "invalidAMMAssetPair" + }, + { + "args": [ + "amount", + "pair", + "validZero" + ], + "lineno": 63, + "name": "invalidAMMAmount" + }, + { + "args": [ + "current", + "auctionSlot" + ], + "lineno": 73, + "name": "ammAuctionTimeSlot" + }, + { + "args": [ + "rules" + ], + "lineno": 91, + "name": "ammEnabled" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation functions (invalidAMMAsset, invalidAMMAssetPair, invalidAMMAmount) are likely tested in unit tests for AMM logic, typically found in files like 'test/AMM_test.cpp', 'test/AMMAsset_test.cpp', or similar. These tests would cover valid/invalid assets, asset pairs, and amounts. However, coverage gaps may exist for edge cases such as: (1) MPTIssue with zero issuer, (2) Issue with bad currency, (3) Issue with XRP and nonzero issuer, (4) Asset not matching either in the pair, (5) Negative or zero amounts when not allowed. If these edge cases are not explicitly tested, bugs could slip through. Integration tests may also exercise these paths indirectly via AMM transaction flows. Direct unit tests for each validation error code path are recommended for full coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation via lambdas and explicit checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temBAD_MPT", + "field": "MPTIssue.issuer", + "location": "invalidAMMAsset", + "validated_by": "lambda in invalidAMMAsset", + "validates": [ + "issuer must not be beast::zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_CURRENCY", + "field": "Issue.currency", + "location": "invalidAMMAsset", + "validated_by": "lambda in invalidAMMAsset", + "validates": [ + "currency must not be badCurrency()" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_ISSUER", + "field": "Issue.issuer", + "location": "invalidAMMAsset", + "validated_by": "lambda in invalidAMMAsset", + "validates": [ + "if currency is XRP, issuer must be zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMM_TOKENS", + "field": "asset", + "location": "invalidAMMAsset", + "validated_by": "invalidAMMAsset", + "validates": [ + "asset must be one of the pair if pair is provided" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMM_TOKENS", + "field": "asset1, asset2", + "location": "invalidAMMAssetPair", + "validated_by": "invalidAMMAssetPair", + "validates": [ + "asset1 and asset2 must not be equal" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "see invalidAMMAsset", + "field": "asset1", + "location": "invalidAMMAssetPair", + "validated_by": "invalidAMMAsset", + "validates": [ + "see invalidAMMAsset" + ], + "validation_type": "delegated" + }, + { + "confidence": 1.0, + "error_thrown": "see invalidAMMAsset", + "field": "asset2", + "location": "invalidAMMAssetPair", + "validated_by": "invalidAMMAsset", + "validates": [ + "see invalidAMMAsset" + ], + "validation_type": "delegated" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/AMMCore.cpp.ai.md b/src/libxrpl/protocol/AMMCore.cpp.ai.md new file mode 100644 index 0000000000..4c0679c58e --- /dev/null +++ b/src/libxrpl/protocol/AMMCore.cpp.ai.md @@ -0,0 +1,46 @@ +# `src/libxrpl/protocol/AMMCore.cpp` + +## Role in the System + +This file is the shared validation and utility foundation for all AMM (Automated Market Maker) transaction logic in the XRPL. Every AMM transaction handler — `AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMBid`, `AMMVote`, and `AMMDelete` — calls into these functions during their `preflight` phase to perform stateless input validation before touching ledger state. The file also handles LP token identity derivation and auction slot time arithmetic. + +The functions are deliberately low-level: no ledger access, no state mutation, pure computation over in-memory data. This placement in `libxrpl/protocol` (rather than `xrpld`) makes them available to both the server and external client libraries. + +## LP Token Currency Derivation + +`ammLPTCurrency()` computes a deterministic `Currency` identifier for the liquidity provider token of an asset pair. The algorithm has three notable design choices. + +First, it uses `std::minmax` on the two `Asset` arguments before hashing. This canonical ordering ensures that `ammLPTCurrency(A, B)` and `ammLPTCurrency(B, A)` produce the same result — a necessary property since an AMM pool is unordered. The `Asset` comparison operators in `Asset.h` provide a consistent total order across `Issue` and `MPTIssue` variants (with `Issue` types ordered greater than `MPTIssue` types). + +Second, the hash input is type-dispatched via a `std::visit` lambda using C++20 template parameters (`auto&&` with `if constexpr`). For a classic `Issue` the hash input is the `Currency` field; for an `MPTIssue` it is the `MPTID` (a 192-bit identifier). This means the same function handles both legacy token types and the newer Multi-Purpose Token standard transparently. + +Third, the resulting currency uses the prefix byte `0x03`. Standard XRPL IOU currencies occupy bytes where the first byte is `0x00`; XRP is the all-zeros special case; the `0x03` prefix is reserved specifically for AMM LP tokens. This allows any participant to identify an LP token currency just by inspecting the first byte, without needing to look up the associated AMM account. + +`ammLPTIssue()` is a thin wrapper that bundles the computed currency with the AMM's account ID to form a complete `Issue`. + +## Asset Validation Hierarchy + +The three `invalid*` functions form a composable validation stack that returns `NotTEC` — a sub-type of TER restricted to `tem*` malformed-transaction codes. Returning `NotTEC` (rather than plain `TER`) documents at the type level that these checks can only fail due to structural malformedness, never due to transient ledger state. + +`invalidAMMAsset()` is the atomic validator. It uses `Asset::visit()` to dispatch separately on `MPTIssue` and `Issue`: + +- For `MPTIssue`: rejects a zero issuer (`temBAD_MPT`), which would denote an uninitialised or invalid MPT issuance. +- For `Issue`: rejects the sentinel `badCurrency()` value (`temBAD_CURRENCY`) and rejects any `Issue` that claims XRP but carries a non-zero issuer (`temBAD_ISSUER`) — a structurally impossible combination since XRP has no issuer on the XRPL. + +The optional `pair` argument adds a membership check: if the caller supplies the known pool's asset pair, the function also verifies the asset belongs to that pool (`temBAD_AMM_TOKENS`). This single parameter makes `invalidAMMAsset()` useful both as a general format check (no pair) and as a context-sensitive pool membership check (with pair). + +`invalidAMMAssetPair()` adds the distinctness constraint — the two assets must not be equal — then delegates each individual asset to `invalidAMMAsset`. Equality between assets of different types is always false (the `Asset::operator==` falls through to `false` on type mismatch in `std::visit`), so this naturally prevents a same-type self-pairing. + +`invalidAMMAmount()` validates an `STAmount` by first extracting its asset and passing it through `invalidAMMAsset`, then separately checking the numeric value. The `validZero` flag deserves attention: most AMM operations disallow zero amounts (depositing zero liquidity is meaningless), but the flag is set `true` in contexts like specifying a minimum bid of zero (`AMMBid`), where zero is a valid sentinel meaning "no minimum." + +## Auction Slot Time Arithmetic + +`ammAuctionTimeSlot()` maps a ledger close timestamp to a slot index within the 24-hour auction window, returning `std::nullopt` if the timestamp falls outside the current slot's window. + +The auction slot is parameterised by the constants defined in `AMMCore.h`: `TOTAL_TIME_SLOT_SECS = 86400` (one day), `AUCTION_SLOT_TIME_INTERVALS = 20`, giving `AUCTION_SLOT_INTERVAL_DURATION = 4320` seconds (72 minutes) per sub-interval. The slot starts at `expiration - TOTAL_TIME_SLOT_SECS` and ends at `expiration`. Dividing the elapsed seconds within the window by `AUCTION_SLOT_INTERVAL_DURATION` yields the interval index (0–19). + +The guard `expiration >= TOTAL_TIME_SLOT_SECS` is a defensive sanity check accompanied by an `XRPL_ASSERT`. The code comment acknowledges this should be structurally impossible, but the check prevents underflow on the subtraction `expiration - TOTAL_TIME_SLOT_SECS` if somehow a corrupted ledger object were processed. + +## Feature Gate + +`ammEnabled()` gates all AMM functionality behind two amendments: `featureAMM` (the primary AMM feature flag) and `fixUniversalNumber`. The second requirement is non-obvious. AMM math throughout the codebase relies on the `Number` type from `xrpl/basics/Number.h`, which provides unified arithmetic across integer and floating-point domains. The `fixUniversalNumber` amendment corrects edge cases in that type's behavior. Tying `ammEnabled()` to both amendments means AMM transactions are unavailable unless the numeric foundation is also sound, preventing subtle calculation bugs on ledgers that enabled `featureAMM` before `fixUniversalNumber` was applied. \ No newline at end of file diff --git a/src/libxrpl/protocol/AccountID.cpp.ai.json b/src/libxrpl/protocol/AccountID.cpp.ai.json new file mode 100644 index 0000000000..b180df3185 --- /dev/null +++ b/src/libxrpl/protocol/AccountID.cpp.ai.json @@ -0,0 +1,410 @@ +{ + "args": [ + { + "lineno": 23, + "name": "count" + }, + { + "lineno": 29, + "name": "id" + }, + { + "lineno": 67, + "name": "v" + }, + { + "lineno": 72, + "name": "s" + }, + { + "lineno": 97, + "name": "pk" + }, + { + "lineno": 120, + "name": "issuer" + } + ], + "classes": [ + { + "args": [ + "count" + ], + "lineno": 13, + "name": "AccountIdCache" + } + ], + "code_paths": [ + { + "call_chain": [ + "toBase58", + "accountIdCache->toBase58 (if cache exists) OR encodeBase58Token" + ], + "entry_point": "toBase58(AccountID const& v)", + "purpose": "Converts an AccountID to its base58 string representation, using a cache if available.", + "validation_points": [ + "XRPL_ASSERT in AccountIdCache::toBase58 (validates encoding result size)" + ] + }, + { + "call_chain": [ + "parseBase58", + "decodeBase58Token" + ], + "entry_point": "parseBase58(std::string const& s)", + "purpose": "Parses a base58 string into an AccountID, validating the input and returning std::nullopt if invalid.", + "validation_points": [ + "decodeBase58Token (validates base58 string input)", + "if (result.size() != AccountID::bytes) (validates decoded size)" + ] + }, + { + "call_chain": [ + "initAccountIdCache", + "AccountIdCache::AccountIdCache" + ], + "entry_point": "initAccountIdCache(std::size_t count)", + "purpose": "Initializes the AccountIdCache singleton with a specified size, only if not already initialized and count != 0.", + "validation_points": [ + "if (!accountIdCache && count != 0) (validates initialization count)" + ] + } + ], + "data_flows": [ + { + "field": "AccountID base58 string", + "flow": [ + "parseBase58(std::string const& s)", + "decodeBase58Token(s, TokenType::AccountID)", + "result (std::vector)", + "if (result.size() != AccountID::bytes)", + "AccountID{result} (if valid)" + ], + "origin": "External input (e.g., user input, API call)", + "transformations": [ + "Base58 string decoded to binary", + "Size checked against AccountID::bytes" + ], + "validated_at": "decodeBase58Token and size check in parseBase58" + }, + { + "field": "AccountID", + "flow": [ + "toBase58(AccountID const& v)", + "accountIdCache->toBase58 OR encodeBase58Token", + "encodeBase58Token(TokenType::AccountID, id.data(), id.size())", + "ret (std::string, base58 encoding)" + ], + "origin": "Internal (e.g., from ledger, constructed from public key, or from parseBase58)", + "transformations": [ + "AccountID binary encoded to base58 string" + ], + "validated_at": "XRPL_ASSERT(ret.size() <= 38) in AccountIdCache::toBase58" + }, + { + "field": "AccountIdCache initialization count", + "flow": [ + "initAccountIdCache(count)", + "if (!accountIdCache && count != 0)", + "AccountIdCache::AccountIdCache(count)" + ], + "origin": "External (initAccountIdCache parameter)", + "transformations": [ + "Count checked for nonzero and singleton not already initialized" + ], + "validated_at": "if (!accountIdCache && count != 0) in initAccountIdCache" + } + ], + "description": "Provides utilities for encoding, decoding, and caching XRPL AccountIDs, including base58 conversion, parsing, and calculation from public keys.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AccountID base58 string input", + "empty", + "string", + "validation" + ], + "evidence": "decodeBase58Token at parseBase58", + "issue_pattern": "Missing empty string validation for AccountID base58 string input", + "why_false_positive": "decodeBase58Token validates AccountID base58 string input for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "AccountID base58 string input", + "format", + "validation", + "invalid" + ], + "evidence": "decodeBase58Token at parseBase58", + "issue_pattern": "Missing format validation for AccountID base58 string input", + "why_false_positive": "decodeBase58Token validates AccountID base58 string input format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AccountID base58 encoding result size", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at detail::AccountIdCache::toBase58", + "issue_pattern": "Missing empty string validation for AccountID base58 encoding result size", + "why_false_positive": "XRPL_ASSERT validates AccountID base58 encoding result size for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "AccountID base58 encoding result size", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at detail::AccountIdCache::toBase58", + "issue_pattern": "Missing range validation for AccountID base58 encoding result size", + "why_false_positive": "XRPL_ASSERT validates AccountID base58 encoding result size range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "AccountIdCache initialization count", + "empty", + "string", + "validation" + ], + "evidence": "if (!accountIdCache && count != 0) at initAccountIdCache", + "issue_pattern": "Missing empty string validation for AccountIdCache initialization count", + "why_false_positive": "if (!accountIdCache && count != 0) validates AccountIdCache initialization count for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "AccountIdCache initialization count", + "range", + "bounds", + "validation" + ], + "evidence": "if (!accountIdCache && count != 0) at initAccountIdCache", + "issue_pattern": "Missing range validation for AccountIdCache initialization count", + "why_false_positive": "if (!accountIdCache && count != 0) validates AccountIdCache initialization count range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/AccountID.cpp", + "functions": [ + { + "args": [ + "count" + ], + "lineno": 23, + "name": "AccountIdCache::AccountIdCache" + }, + { + "args": [ + "id" + ], + "lineno": 29, + "name": "AccountIdCache::toBase58" + }, + { + "args": [ + "count" + ], + "lineno": 61, + "name": "initAccountIdCache" + }, + { + "args": [ + "v" + ], + "lineno": 67, + "name": "toBase58" + }, + { + "args": [ + "s" + ], + "lineno": 72, + "name": "parseBase58" + }, + { + "args": [ + "pk" + ], + "lineno": 97, + "name": "calcAccountID" + }, + { + "args": [], + "lineno": 108, + "name": "xrpAccount" + }, + { + "args": [], + "lineno": 114, + "name": "noAccount" + }, + { + "args": [ + "issuer", + "s" + ], + "lineno": 120, + "name": "to_issuer" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "detail" + } + ], + "test_coverage_notes": "The code's validation logic is primarily around base58 encoding/decoding and cache initialization. Typical test coverage would be in protocol-level or utility tests, such as 'AccountID_test.cpp', 'Base58_test.cpp', or 'AccountIdCache_test.cpp'. These would test: (1) valid/invalid base58 strings, (2) correct encoding/decoding roundtrips, (3) cache behavior, and (4) edge cases (e.g., all-zero account, max-length strings). Gaps may exist if there are no tests for: (a) oversized base58 strings (triggering XRPL_ASSERT), (b) cache collision/eviction logic, or (c) initialization with zero or duplicate cache. Fuzzing or property-based tests for decodeBase58Token would further strengthen coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "AccountID base58 string input", + "location": "parseBase58", + "validated_by": "decodeBase58Token", + "validates": [ + "Checks that the decoded result from base58 string is exactly AccountID::bytes (20 bytes)", + "Ensures input is a valid base58-encoded AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or logs error)", + "field": "AccountID base58 encoding result size", + "location": "detail::AccountIdCache::toBase58", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that the encoded base58 string is at most 38 characters" + ], + "validation_type": "range" + }, + { + "confidence": 0.8, + "error_thrown": "none (cache not created if count is 0)", + "field": "AccountIdCache initialization count", + "location": "initAccountIdCache", + "validated_by": "if (!accountIdCache && count != 0)", + "validates": [ + "Ensures cache is only initialized with non-zero size" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/AccountID.cpp.ai.md b/src/libxrpl/protocol/AccountID.cpp.ai.md new file mode 100644 index 0000000000..6cb1dbecf0 --- /dev/null +++ b/src/libxrpl/protocol/AccountID.cpp.ai.md @@ -0,0 +1,52 @@ +# `AccountID.cpp` — Account Identity Encoding, Derivation, and Caching + +This file implements the core operations on `AccountID`, the 160-bit identifier that represents an XRPL account. It bridges the cryptographic world (derivation from a public key) and the user-facing world (the familiar `r...` base58 address strings), while providing an optional performance cache that avoids redundant SHA-256 work during hot-path ledger processing. + +## The `AccountID` Type + +`AccountID` is a type alias for `base_uint<160, detail::AccountIDTag>` — a 20-byte fixed-size integer with a tag type for type safety. The tag prevents accidental interoperability with other 160-bit values in the codebase. The `.cpp` file provides the runtime behavior for this type; the header declares the interface and keeps the tag definition. + +## `calcAccountID` — Why SHA-256 then RIPEMD-160? + +The function `calcAccountID(PublicKey const& pk)` derives an account identifier by feeding the public key bytes through `ripesha_hasher`, which internally runs SHA-256 then RIPEMD-160. The source comment (quoting David Schwartz) explains that this is a deliberate choice to match Bitcoin's address derivation scheme, for two reasons: + +1. A double-hash prevents length-extension attacks that could be possible with a single SHA-256 output. +2. RIPEMD-160 is considered secure at 160 bits, whereas simply truncating SHA-512 or SHA-256 to 160 bits is a less well-analyzed approach. + +The comment is candid: "The historical reason was that in the very early days, we wanted to give people as few ways to argue that we were less secure than Bitcoin." This is a conservative engineering choice that avoids reopening a security debate, not a claim that the alternative schemes would be insecure. + +The `static_assert` before the implementation confirms that `AccountID::bytes` equals the size of `ripesha_hasher::result_type`, making the cast on the return line safe and self-documented. + +## `AccountIdCache` — High-Performance Direct-Mapped Cache + +Converting an `AccountID` to a base58 string requires a SHA-256 hash operation (to compute the checksum), which is non-trivial. In a validator processing thousands of transactions per second, the same account IDs appear repeatedly. The `AccountIdCache` inside `namespace detail` is a direct-mapped cache designed to amortize this cost. + +The cache is a flat `std::vector` of fixed size, where each slot holds an `AccountID` (20 bytes) and a `char[40]` encoding buffer. Crucially, no slot performs heap allocation — the encoding is stored inline. An XRPL base58 account address is always at most 34 characters plus a null terminator; the buffer is sized to 40 bytes with a comment and `XRPL_ASSERT` in `toBase58` confirming that the encoding never exceeds 38 characters. + +Cache lookup uses a `hardened_hash<>` to map an `AccountID` to a slot index. The `hardened_hash` uses xxHash seeded with a random value at startup, which prevents adversarial inputs from causing systematic hash collisions (a denial-of-service vector if a predictable hash were used). + +### Packed Spinlock Sharding + +Rather than a single mutex for the entire cache, `AccountIdCache` uses a single `std::atomic locks_` that encodes 64 independent spinlocks via `packed_spinlock`. Each cache slot acquires one of 64 spinlocks determined by `index % 64`. This is a space-efficient form of lock sharding: 64 distinct cache slots can be written concurrently without blocking each other, while the entire lock state fits in a single 64-bit word that lives on one cache line. + +The `packed_spinlock` implementation uses `fetch_or` with `memory_order_acquire` to claim a bit and `fetch_and` with `memory_order_release` to release it — standard acquire/release semantics for a spinlock. It also calls `detail::spin_pause()` (`_mm_pause` on x86, `yield` on AArch64) during the spin loop to avoid pipeline misprediction penalties from tight compare-branch loops. + +### The All-Zero Account Edge Case + +The cache check `cache_[index].encoding[0] != 0 && cache_[index].id == id` handles a subtle initialization problem. A freshly default-constructed `CachedAccountID` has all bytes zeroed, including both the `id` field and the `encoding` field. The `AccountID` for XRP (the special `xrpAccount()` sentinel) is also all zeros. Without the `encoding[0] != 0` guard, an uninitialized slot whose `id` happens to be zero would appear to be a cache hit for `xrpAccount()`, returning an empty string instead of the correct base58 encoding. The first-byte check is an elegant way to distinguish "never written" from "legitimately cached the all-zero account." + +## Special Sentinel Accounts + +`xrpAccount()` returns a statically initialized `AccountID` of `beast::zero` (all bits zero). It serves as the canonical issuer for native XRP — XRP balances use this value in the issuer field where a real account address would otherwise appear. + +`noAccount()` returns `AccountID(1)` — a value that cannot be derived from any real public key and is used as a placeholder when no account is applicable. Both are returned as `const&` to static locals, the standard Meyer's singleton pattern. + +## Cache Initialization and Singleton Lifecycle + +`initAccountIdCache(std::size_t count)` is intentionally one-shot: the `if (!accountIdCache && count != 0)` guard means only the first call has any effect. The caller at application startup decides the cache size; later calls (e.g., from tests or reconfiguration code) are silently ignored. Passing `count == 0` disables the cache entirely. The `toBase58` free function checks for the cache's existence and falls through to the uncached `encodeBase58Token` path if it was never initialized — so the cache is entirely optional and the API contract is unchanged without it. + +## `parseBase58` and `to_issuer` + +The `parseBase58` specialization decodes a base58 string via `decodeBase58Token`, then validates that the result is exactly 20 bytes (`AccountID::bytes`). Any mismatch — wrong token type prefix, invalid characters, wrong length — causes `decodeBase58Token` to return an empty or wrong-sized result, and the function returns `std::nullopt`. There are no exceptions in this path. + +`to_issuer()` is a legacy dual-format parser: it first tries to interpret the string as raw hex (via `parseHex`), then falls back to base58. It is marked `DEPRECATED` in the header, since newer code should prefer the explicit `parseBase58` path and avoid mixing formats at the call site. \ No newline at end of file diff --git a/src/libxrpl/protocol/Asset.cpp.ai.json b/src/libxrpl/protocol/Asset.cpp.ai.json new file mode 100644 index 0000000000..446dcd5cc1 --- /dev/null +++ b/src/libxrpl/protocol/Asset.cpp.ai.json @@ -0,0 +1,343 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "assetFromJson", + "issueFromJson OR mptIssueFromJson" + ], + "entry_point": "assetFromJson", + "purpose": "Constructs an Asset from a JSON value, dispatching to the correct constructor based on presence of 'currency' or 'mpt_issuance_id'.", + "validation_points": [ + "assetFromJson: Checks that at least one of 'currency' or 'mpt_issuance_id' is present. Throws if not.", + "issueFromJson/mptIssueFromJson: Further validation of fields (not shown in this file, but implied)." + ] + }, + { + "call_chain": [ + "validJSONAsset" + ], + "entry_point": "validJSONAsset", + "purpose": "Checks if a JSON value is a valid representation of an Asset.", + "validation_points": [ + "validJSONAsset: Ensures that if 'mpt_issuance_id' is present, 'currency' and 'issuer' are not, and vice versa." + ] + }, + { + "call_chain": [ + "Asset::operator()" + ], + "entry_point": "operator() (Asset::operator())", + "purpose": "Creates an STAmount from an Asset and a Number.", + "validation_points": [] + }, + { + "call_chain": [ + "Asset::setJson", + "issue.setJson" + ], + "entry_point": "setJson", + "purpose": "Serializes the Asset to JSON by delegating to the underlying issue type.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "currency", + "flow": [ + "Json::Value", + "assetFromJson", + "issueFromJson", + "Asset" + ], + "origin": "Json::Value input (possibly from RPC or config)", + "transformations": [ + "Checked for presence in assetFromJson", + "Used to construct Issue or MPTIssue" + ], + "validated_at": "assetFromJson (presence), validJSONAsset (mutual exclusion with mpt_issuance_id)" + }, + { + "field": "issuer", + "flow": [ + "Json::Value", + "issueFromJson", + "Asset" + ], + "origin": "Json::Value input", + "transformations": [ + "Checked for presence in issueFromJson (not shown here)" + ], + "validated_at": "validJSONAsset (mutual exclusion with mpt_issuance_id)" + }, + { + "field": "mpt_issuance_id", + "flow": [ + "Json::Value", + "assetFromJson", + "mptIssueFromJson", + "Asset" + ], + "origin": "Json::Value input", + "transformations": [ + "Checked for presence in assetFromJson", + "Used to construct MPTIssue" + ], + "validated_at": "assetFromJson (presence), validJSONAsset (mutual exclusion with currency/issuer)" + }, + { + "field": "issue_ (variant of Issue or MPTIssue)", + "flow": [ + "assetFromJson", + "Asset", + "various Asset methods (getIssuer, getText, setJson, operator())" + ], + "origin": "Constructed in assetFromJson via issueFromJson or mptIssueFromJson", + "transformations": [ + "std::visit used to dispatch to correct type" + ], + "validated_at": "assetFromJson (type selection), issueFromJson/mptIssueFromJson (field validation)" + } + ], + "description": "Implements the Asset class and related functions for handling XRPL assets, including JSON serialization, string conversion, and construction from JSON.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "currency", + "validation", + "missing", + "check" + ], + "evidence": "Field currency validated by jss:: (JSON field name constants), Throw<> (template exception helper)", + "issue_pattern": "Missing validation for currency", + "why_false_positive": "jss:: (JSON field name constants), Throw<> (template exception helper) validates currency automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "issuer", + "validation", + "missing", + "check" + ], + "evidence": "Field issuer validated by jss:: (JSON field name constants), Throw<> (template exception helper)", + "issue_pattern": "Missing validation for issuer", + "why_false_positive": "jss:: (JSON field name constants), Throw<> (template exception helper) validates issuer automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "mpt_issuance_id", + "validation", + "missing", + "check" + ], + "evidence": "Field mpt_issuance_id validated by jss:: (JSON field name constants), Throw<> (template exception helper)", + "issue_pattern": "Missing validation for mpt_issuance_id", + "why_false_positive": "jss:: (JSON field name constants), Throw<> (template exception helper) validates mpt_issuance_id automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "currency, mpt_issuance_id", + "empty", + "string", + "validation" + ], + "evidence": "explicit code at assetFromJson", + "issue_pattern": "Missing empty string validation for currency, mpt_issuance_id", + "why_false_positive": "explicit code validates currency, mpt_issuance_id for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "currency, issuer, mpt_issuance_id", + "empty", + "string", + "validation" + ], + "evidence": "explicit code at validJSONAsset", + "issue_pattern": "Missing empty string validation for currency, issuer, mpt_issuance_id", + "why_false_positive": "explicit code validates currency, issuer, mpt_issuance_id for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Asset.cpp", + "functions": [ + { + "args": [], + "lineno": 11, + "name": "Asset::getIssuer" + }, + { + "args": [], + "lineno": 16, + "name": "Asset::getText" + }, + { + "args": [ + "jv" + ], + "lineno": 21, + "name": "Asset::setJson" + }, + { + "args": [ + "number" + ], + "lineno": 26, + "name": "Asset::operator()" + }, + { + "args": [ + "asset" + ], + "lineno": 31, + "name": "to_string" + }, + { + "args": [ + "jv" + ], + "lineno": 36, + "name": "validJSONAsset" + }, + { + "args": [ + "v" + ], + "lineno": 43, + "name": "assetFromJson" + }, + { + "args": [ + "os", + "x" + ], + "lineno": 52, + "name": "operator<<" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage is not shown in this file. Likely test files: protocol/Asset_test.cpp, protocol/Issue_test.cpp, protocol/MPTIssue_test.cpp, or higher-level JSON/RPC tests. The main validation logic (field presence, mutual exclusion) is simple and should be covered by tests that check asset parsing from JSON, including error cases (missing fields, conflicting fields). Gaps may exist if tests do not cover all invalid combinations (e.g., both 'currency' and 'mpt_issuance_id' present, or neither present). No explicit test hooks or asserts are present in this file.", + "validation_architecture": { + "auto_validated_fields": [ + "currency", + "issuer", + "mpt_issuance_id" + ], + "framework": "jss:: (JSON field name constants), Throw<> (template exception helper)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<> template)", + "field": "currency, mpt_issuance_id", + "location": "assetFromJson", + "validated_by": "explicit code", + "validates": [ + "At least one of 'currency' or 'mpt_issuance_id' must be present in the input JSON" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "currency, issuer, mpt_issuance_id", + "location": "validJSONAsset", + "validated_by": "explicit code", + "validates": [ + "If 'mpt_issuance_id' is present, 'currency' and 'issuer' must NOT be present", + "If 'mpt_issuance_id' is NOT present, 'currency' must be present" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Asset.cpp.ai.md b/src/libxrpl/protocol/Asset.cpp.ai.md new file mode 100644 index 0000000000..030511dd17 --- /dev/null +++ b/src/libxrpl/protocol/Asset.cpp.ai.md @@ -0,0 +1,46 @@ +# `src/libxrpl/protocol/Asset.cpp` + +## Role in the System + +`Asset.cpp` provides the implementation body for XRPL's unified asset abstraction layer. The XRPL protocol historically distinguished between two token categories — XRP (native) and IOU (issued) — both of which were modelled by the `Issue` type. With the introduction of Multi-Purpose Tokens (MPT), a second distinct representation, `MPTIssue`, entered the picture. Rather than propagate a two-track conditional through every ledger subsystem, the codebase introduced `Asset` as a single polymorphic handle that can hold either type without exposing the distinction at call sites. + +This file is the out-of-line companion to `include/xrpl/protocol/Asset.h`. The header holds everything that can be inlined or templated; this `.cpp` contains the handful of methods whose implementations are non-trivial enough to keep out of the header, plus two free functions (`validJSONAsset`, `assetFromJson`) that gate JSON ingestion. + +## The Variant Core and `std::visit` Dispatch + +`Asset` stores a `std::variant` named `issue_`. Every method in this file delegates to the active alternative through `std::visit`, never branching on a type tag manually. This is intentional: it makes adding a third asset type a compile-error-driven process (the visitor won't compile until all alternatives are handled) rather than a silent runtime gap caused by a missed `if`-branch. + +`getIssuer()`, `getText()`, and `setJson()` are all single-line `std::visit` calls that forward to the same-named method on whichever underlying type is active. Both `Issue` and `MPTIssue` expose this interface deliberately — `MPTIssue`'s comment in its header says it "adapts MPTID to provide the same interface as Issue," enabling this static polymorphism without a virtual dispatch table. + +The `Asset::visit()` method in the header (not in the `.cpp`) wraps `detail::visit` from `Concepts.h`, which implements the *overloaded* pattern: multiple lambdas are combined into a single callable via `CombineVisitors : Ts...` with `using Ts::operator()...`. This gives call sites the ability to pass per-type lambdas inline without constructing a separate visitor struct. + +## JSON Boundary: `validJSONAsset` and `assetFromJson` + +These two free functions form the JSON ingestion gate. Their separation of concerns is deliberate: + +- `validJSONAsset(Json::Value const&)` is a **pure predicate** — it returns `bool` and throws nothing. Its job is to enforce mutual exclusion: if `mpt_issuance_id` is present, neither `currency` nor `issuer` may appear, because those fields belong to the legacy `Issue` representation. If `mpt_issuance_id` is absent, `currency` must be present. This check is typically called before attempting to construct an `Asset`, letting callers reject invalid input with a clean error rather than relying on a constructor exception. + +- `assetFromJson(Json::Value const&)` is the **constructor proxy** — it throws `std::runtime_error` (via the `Throw<>` helper from `xrpl/basics/contract.h`) if neither `currency` nor `mpt_issuance_id` is present, then dispatches to either `issueFromJson` or `mptIssueFromJson`. The dispatch is a simple `if (v.isMember(jss::currency))` test, with `currency` taking priority. Deeper field validation (format, consistency) is delegated to those two sub-parsers. + +The two-function design avoids throwing from a predicate while still providing a single authoritative construction path. Code that already validated with `validJSONAsset` can call `assetFromJson` with confidence. + +## Call-Operator Factory: `operator()(Number const&)` + +```cpp +STAmount Asset::operator()(Number const& number) const +{ + return STAmount{*this, number}; +} +``` + +This is a convenience factory: given an `Asset` and a `Number`, it produces a fully-typed `STAmount`. The intent is ergonomic — `myAsset(someQuantity)` reads like a constructor call and avoids repetitive `STAmount{asset, value}` syntax at call sites. No arithmetic happens here; it purely forwards into `STAmount`'s constructor. + +## Ordering and Identity + +The header defines comparison operators using `std::visit` with a two-argument lambda. `operator==` returns `false` immediately if the two active alternatives differ in type — an `Issue` can never equal an `MPTIssue`. `operator<=>` establishes a total order where `Issue` variants sort *after* `MPTIssue` variants when the types differ (`std::weak_ordering::greater` for `Issue` vs. `MPTIssue`), ensuring a deterministic ordering for sorted containers even across the type boundary. + +`equalTokens()` is a weaker comparison that ignores issuer identity for `Issue` — it asks only whether two assets share the same currency code (for IOUs) or the same `MPTID` (for MPTs). This is used in contexts like offer matching where the token denomination matters but the specific issuer pathway does not. + +## Implicit vs. Explicit Conversion Policy + +The header comments document a deliberate asymmetry: conversions *to* `Asset` are implicit (any `Issue` or `MPTIssue` silently becomes an `Asset`), while conversions *from* `Asset` to a concrete type are explicit (callers must call `get()` or `get()`, which throws `std::logic_error` on type mismatch). This makes `Asset` the widening type in the system — functions written to accept `Asset` work for all token kinds — while preventing accidental silent narrowing that would lose the type-safety the variant provides. \ No newline at end of file diff --git a/src/libxrpl/protocol/Book.cpp.ai.json b/src/libxrpl/protocol/Book.cpp.ai.json new file mode 100644 index 0000000000..45a6284c91 --- /dev/null +++ b/src/libxrpl/protocol/Book.cpp.ai.json @@ -0,0 +1,238 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "isConsistent(Book)", + "isConsistent(book.in)", + "isConsistent(book.out)", + "book.in != book.out" + ], + "entry_point": "isConsistent(Book const& book)", + "purpose": "Validates that a Book is well-formed: both in and out Issues are consistent and not equal.", + "validation_points": [ + "isConsistent(book.in)", + "isConsistent(book.out)", + "book.in != book.out" + ] + }, + { + "call_chain": [ + "to_string(Book)", + "to_string(book.in)", + "to_string(book.out)" + ], + "entry_point": "to_string(Book const& book)", + "purpose": "Converts a Book to a string representation.", + "validation_points": [] + }, + { + "call_chain": [ + "operator<<(ostream, Book)", + "to_string(Book)", + "to_string(book.in)", + "to_string(book.out)" + ], + "entry_point": "operator<<(std::ostream&, Book const&)", + "purpose": "Streams a Book to an output stream.", + "validation_points": [] + }, + { + "call_chain": [ + "reversed(Book)", + "Book(book.out, book.in, book.domain)" + ], + "entry_point": "reversed(Book const& book)", + "purpose": "Returns a new Book with in/out Issues swapped.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "book.in", + "flow": [ + "Book creation", + "book.in", + "isConsistent(book.in)", + "to_string(book.in)", + "Book(book.out, book.in, book.domain) in reversed" + ], + "origin": "Book constructor or deserialization", + "transformations": [ + "Validated for consistency in isConsistent", + "Converted to string in to_string", + "Swapped with book.out in reversed" + ], + "validated_at": "isConsistent(book.in) in isConsistent(Book)" + }, + { + "field": "book.out", + "flow": [ + "Book creation", + "book.out", + "isConsistent(book.out)", + "to_string(book.out)", + "Book(book.out, book.in, book.domain) in reversed" + ], + "origin": "Book constructor or deserialization", + "transformations": [ + "Validated for consistency in isConsistent", + "Converted to string in to_string", + "Swapped with book.in in reversed" + ], + "validated_at": "isConsistent(book.out) in isConsistent(Book)" + }, + { + "field": "book.domain", + "flow": [ + "Book creation", + "book.domain", + "Book(book.out, book.in, book.domain) in reversed" + ], + "origin": "Book constructor or deserialization", + "transformations": [ + "Passed through unchanged in reversed" + ], + "validated_at": "Not validated in this file" + } + ], + "description": "Provides utility functions for handling and representing 'Book' objects in the XRPL protocol, including consistency checks, string conversion, stream output, and reversal.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "book.in", + "empty", + "string", + "validation" + ], + "evidence": "isConsistent (xrpl::isConsistent) at isConsistent(Book const& book)", + "issue_pattern": "Missing empty string validation for book.in", + "why_false_positive": "isConsistent (xrpl::isConsistent) validates book.in for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "book.out", + "empty", + "string", + "validation" + ], + "evidence": "isConsistent (xrpl::isConsistent) at isConsistent(Book const& book)", + "issue_pattern": "Missing empty string validation for book.out", + "why_false_positive": "isConsistent (xrpl::isConsistent) validates book.out for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "book.in != book.out", + "empty", + "string", + "validation" + ], + "evidence": "operator!= (likely Issue::operator!=) at isConsistent(Book const& book)", + "issue_pattern": "Missing empty string validation for book.in != book.out", + "why_false_positive": "operator!= (likely Issue::operator!=) validates book.in != book.out for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Book.cpp", + "functions": [ + { + "args": [ + "book" + ], + "lineno": 7, + "name": "isConsistent" + }, + { + "args": [ + "book" + ], + "lineno": 12, + "name": "to_string" + }, + { + "args": [ + "os", + "x" + ], + "lineno": 17, + "name": "operator<<" + }, + { + "args": [ + "book" + ], + "lineno": 23, + "name": "reversed" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code in Book.cpp is utility/logic code and does not contain tests itself. Validation logic (isConsistent) is likely tested in higher-level Book or Issue tests. Look for tests in files like 'Book_test.cpp', 'Issue_test.cpp', or protocol-level tests in the test suite. Gaps: No direct evidence here of tests for reversed(Book), operator<<, or to_string(Book). Validation of book.domain is not present in this file. If Issue::isConsistent or Issue::operator!= are not thoroughly tested, Book validation may be incomplete.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (xrpl::isConsistent, operator!=)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (returns false)", + "field": "book.in", + "location": "isConsistent(Book const& book)", + "validated_by": "isConsistent (xrpl::isConsistent)", + "validates": [ + "book.in is consistent according to isConsistent(Issue const&)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns false)", + "field": "book.out", + "location": "isConsistent(Book const& book)", + "validated_by": "isConsistent (xrpl::isConsistent)", + "validates": [ + "book.out is consistent according to isConsistent(Issue const&)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns false)", + "field": "book.in != book.out", + "location": "isConsistent(Book const& book)", + "validated_by": "operator!= (likely Issue::operator!=)", + "validates": [ + "book.in and book.out are not the same Issue" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Book.cpp.ai.md b/src/libxrpl/protocol/Book.cpp.ai.md new file mode 100644 index 0000000000..7fa8e68ce6 --- /dev/null +++ b/src/libxrpl/protocol/Book.cpp.ai.md @@ -0,0 +1,62 @@ +# `src/libxrpl/protocol/Book.cpp` + +## Role in the System + +This file provides the four free-function utilities that give the `Book` type its protocol-layer behavior: validation, string conversion, stream output, and reversal. The file is intentionally thin — `Book` itself is a plain value type (two `Asset` legs plus an optional domain), so all logic here is stateless and algorithmic, delegating to the parallel `Issue`/`Asset` functions wherever possible. + +A `Book` models an order book on the XRP Ledger: a directional market between two assets where offers convert the `in` currency to the `out` currency. That directionality is central to why `reversed()` exists as a named operation rather than just a convenience constructor call elsewhere. + +## The `Book` Data Model + +`Book` (declared in `Book.h`) stores three fields: + +- `Asset in` — the asset offered by takers (what they pay). +- `Asset out` — the asset received by takers (what they get). +- `std::optional domain` — an optional permissioned-domain identifier, supporting domain-scoped order books introduced as a newer protocol feature. + +`Asset` is a variant that can hold either an `Issue` (a classic currency/issuer pair) or an `MPTIssue` (a multi-purpose token). The `Book.h` header builds on top of this by providing equality (`operator==`), three-way comparison (`operator<=>`), `hash_append`, and full `std::hash`/`boost::hash` specializations, making `Book` usable as a key in both ordered and unordered containers throughout the engine. + +## `isConsistent` + +```cpp +bool isConsistent(Book const& book) { + return isConsistent(book.in) && isConsistent(book.out) && book.in != book.out; +} +``` + +This is the primary validation guard for `Book` objects entering the system. It composes `Issue::isConsistent` — which checks that `isXRP(currency) == isXRP(account)`, i.e., a currency is either both-XRP or neither-XRP — across both legs, then adds the additional invariant that the two legs must differ. A book where `in == out` would represent trading a currency against itself: nonsensical and a likely sign of malformed input. + +Critically, validation returns `false` rather than throwing. This matches the pattern used throughout the XRPL protocol layer for soft validation: callers decide whether inconsistency is a fatal error or just a rejection signal. In practice, `isConsistent(book)` is called as a hard input guard in the Subscribe RPC handler (`Subscribe.cpp:255`) before processing any subscription, and as an assertion in `getBookBase()` in `Indexes.cpp` where an inconsistent book would produce a corrupted ledger index key. + +Notice that `book.domain` is not validated here — its semantic validity (whether the domain actually exists, whether it's accessible) belongs to higher-level transaction processing, not the protocol primitive layer. + +## `to_string` and `operator<<` + +```cpp +std::string to_string(Book const& book) { + return to_string(book.in) + "->" + to_string(book.out); +} +``` + +The format `->` is purely for diagnostic and logging purposes. The arrow makes directionality explicit, which matters because order books are one-way markets. The `operator<<` overload simply delegates to `to_string`, keeping a single formatting path. + +This mirrors the `Issue` utility surface exactly — `Issue` has its own `to_string` and `operator<<` — creating a consistent pattern across protocol primitives. + +## `reversed` + +```cpp +Book reversed(Book const& book) { + return Book(book.out, book.in, book.domain); +} +``` + +This function swaps `in` and `out` while preserving `domain` unchanged. It is used in at least two important call sites: + +1. **Subscribe/Unsubscribe RPC handlers** — when a client subscribes to a market with the `both` flag, the server subscribes to both `book` and `reversed(book)` so the client receives updates from both the bid and ask sides of the same market. +2. **`BookDirs` tests** — the `reversed` book is used to navigate offer directories from the opposite direction. + +The `domain` field carrying through unchanged is a deliberate design: a domain-scoped market is still the same market when viewed from either direction — the scope doesn't flip. + +## Design Consistency + +The entire file is intentionally minimal. `Book` is a value type, so its utility functions require no state, no resource management, and no complex error handling. The consistency with the `Issue` layer's design — the `isConsistent`/`to_string`/`operator<<` trio — means `Book` behaves uniformly with other protocol primitives, reducing cognitive overhead when working across the protocol layer. Validation returns `bool` (never throws), string conversion is diagnostic-only, and the `reversed` operation is pure (returns a new value, never mutating). \ No newline at end of file diff --git a/src/libxrpl/protocol/BuildInfo.cpp.ai.json b/src/libxrpl/protocol/BuildInfo.cpp.ai.json new file mode 100644 index 0000000000..f43b7b42e1 --- /dev/null +++ b/src/libxrpl/protocol/BuildInfo.cpp.ai.json @@ -0,0 +1,422 @@ +{ + "args": [ + { + "lineno": 69, + "name": "versionStr" + }, + { + "lineno": 122, + "name": "version" + }, + { + "lineno": 127, + "name": "version" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "getVersionString", + "buildVersionString" + ], + "entry_point": "getVersionString", + "purpose": "Returns the validated version string for the server, including build metadata if present.", + "validation_points": [ + "beast::SemanticVersion::parse(s) in getVersionString", + "beast::SemanticVersion::print() == s in getVersionString" + ] + }, + { + "call_chain": [ + "getFullVersionString", + "getVersionString", + "buildVersionString" + ], + "entry_point": "getFullVersionString", + "purpose": "Returns the full version string, including the system name and validated version string.", + "validation_points": [ + "beast::SemanticVersion::parse(s) in getVersionString", + "beast::SemanticVersion::print() == s in getVersionString" + ] + }, + { + "call_chain": [ + "encodeSoftwareVersion" + ], + "entry_point": "encodeSoftwareVersion", + "purpose": "Encodes a version string into a 64-bit integer, validating and extracting major, minor, patch, and pre-release info.", + "validation_points": [ + "beast::SemanticVersion::parse(versionStr) in encodeSoftwareVersion", + "majorVersion, minorVersion, patchVersion range checks (>=0 && <=255)", + "parsePreRelease: lexicalCastChecked and range checks for pre-release identifiers" + ] + }, + { + "call_chain": [ + "isXrpldVersion" + ], + "entry_point": "isXrpldVersion", + "purpose": "Checks if a given encoded version matches the implementation version identifier mask.", + "validation_points": [ + "Bitmask check (no semantic validation, just bitwise)" + ] + }, + { + "call_chain": [ + "isNewerVersion" + ], + "entry_point": "isNewerVersion", + "purpose": "Compares two encoded version numbers to determine if one is newer.", + "validation_points": [ + "Relies on encodeSoftwareVersion for validation" + ] + } + ], + "data_flows": [ + { + "field": "versionString", + "flow": [ + "versionString", + "buildVersionString (may append metadata)", + "getVersionString (validates via SemanticVersion::parse/print)", + "getFullVersionString (prepends systemName)" + ], + "origin": "Static const char* in anonymous namespace", + "transformations": [ + "May append commit hash, DEBUG, SANITIZERS metadata in buildVersionString" + ], + "validated_at": "getVersionString (SemanticVersion::parse and print)" + }, + { + "field": "versionStr (input to encodeSoftwareVersion)", + "flow": [ + "encodeSoftwareVersion (input)", + "beast::SemanticVersion::parse(versionStr)", + "Extract majorVersion, minorVersion, patchVersion, preReleaseIdentifiers", + "Range checks and bitwise encoding" + ], + "origin": "Function argument to encodeSoftwareVersion", + "transformations": [ + "Parsed into SemanticVersion struct", + "Major/minor/patch extracted and range-checked", + "Pre-release identifiers parsed and encoded" + ], + "validated_at": "encodeSoftwareVersion (SemanticVersion::parse, range checks, parsePreRelease)" + }, + { + "field": "majorVersion, minorVersion, patchVersion", + "flow": [ + "SemanticVersion::parse(versionStr)", + "encodeSoftwareVersion (range checks and bitwise encoding)" + ], + "origin": "Parsed from versionStr via SemanticVersion", + "transformations": [ + "Range-checked (>=0 && <=255)", + "Bit-shifted and OR'd into encoded version" + ], + "validated_at": "encodeSoftwareVersion (explicit range checks)" + }, + { + "field": "preReleaseIdentifiers", + "flow": [ + "SemanticVersion::parse(versionStr)", + "encodeSoftwareVersion (parsePreRelease lambda)" + ], + "origin": "Parsed from versionStr via SemanticVersion", + "transformations": [ + "Each identifier checked for prefix (rc/b), parsed as integer, range-checked, encoded" + ], + "validated_at": "encodeSoftwareVersion (parsePreRelease: prefix, lexicalCastChecked, range check)" + } + ], + "description": "This file provides build and version information utilities for the XRPL software, including version string management, encoding/decoding of software versions, and version comparison functions.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "version string (s)", + "empty", + "string", + "validation" + ], + "evidence": "beast::SemanticVersion::parse and beast::SemanticVersion::print at getVersionString", + "issue_pattern": "Missing empty string validation for version string (s)", + "why_false_positive": "beast::SemanticVersion::parse and beast::SemanticVersion::print validates version string (s) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "version string (s)", + "format", + "validation", + "invalid" + ], + "evidence": "beast::SemanticVersion::parse and beast::SemanticVersion::print at getVersionString", + "issue_pattern": "Missing format validation for version string (s)", + "why_false_positive": "beast::SemanticVersion::parse and beast::SemanticVersion::print validates version string (s) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "majorVersion", + "empty", + "string", + "validation" + ], + "evidence": "explicit range check (>=0 && <=255) at encodeSoftwareVersion", + "issue_pattern": "Missing empty string validation for majorVersion", + "why_false_positive": "explicit range check (>=0 && <=255) validates majorVersion for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "majorVersion", + "range", + "bounds", + "validation" + ], + "evidence": "explicit range check (>=0 && <=255) at encodeSoftwareVersion", + "issue_pattern": "Missing range validation for majorVersion", + "why_false_positive": "explicit range check (>=0 && <=255) validates majorVersion range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "minorVersion", + "empty", + "string", + "validation" + ], + "evidence": "explicit range check (>=0 && <=255) at encodeSoftwareVersion", + "issue_pattern": "Missing empty string validation for minorVersion", + "why_false_positive": "explicit range check (>=0 && <=255) validates minorVersion for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "minorVersion", + "range", + "bounds", + "validation" + ], + "evidence": "explicit range check (>=0 && <=255) at encodeSoftwareVersion", + "issue_pattern": "Missing range validation for minorVersion", + "why_false_positive": "explicit range check (>=0 && <=255) validates minorVersion range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "patchVersion", + "empty", + "string", + "validation" + ], + "evidence": "explicit range check (>=0 && <=255) at encodeSoftwareVersion", + "issue_pattern": "Missing empty string validation for patchVersion", + "why_false_positive": "explicit range check (>=0 && <=255) validates patchVersion for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "patchVersion", + "range", + "bounds", + "validation" + ], + "evidence": "explicit range check (>=0 && <=255) at encodeSoftwareVersion", + "issue_pattern": "Missing range validation for patchVersion", + "why_false_positive": "explicit range check (>=0 && <=255) validates patchVersion range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "versionStr", + "empty", + "string", + "validation" + ], + "evidence": "beast::SemanticVersion::parse at encodeSoftwareVersion", + "issue_pattern": "Missing empty string validation for versionStr", + "why_false_positive": "beast::SemanticVersion::parse validates versionStr for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "versionStr", + "format", + "validation", + "invalid" + ], + "evidence": "beast::SemanticVersion::parse at encodeSoftwareVersion", + "issue_pattern": "Missing format validation for versionStr", + "why_false_positive": "beast::SemanticVersion::parse validates versionStr format" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/BuildInfo.cpp", + "functions": [ + { + "args": [], + "lineno": 25, + "name": "buildVersionString" + }, + { + "args": [], + "lineno": 52, + "name": "getVersionString" + }, + { + "args": [], + "lineno": 62, + "name": "getFullVersionString" + }, + { + "args": [ + "versionStr" + ], + "lineno": 69, + "name": "encodeSoftwareVersion" + }, + { + "args": [], + "lineno": 116, + "name": "getEncodedVersion" + }, + { + "args": [ + "version" + ], + "lineno": 122, + "name": "isXrpldVersion" + }, + { + "args": [ + "version" + ], + "lineno": 127, + "name": "isNewerVersion" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 15, + "name": "BuildInfo" + } + ], + "test_coverage_notes": "The code relies heavily on beast::SemanticVersion for parsing and validation, so test coverage depends on both direct tests of BuildInfo.cpp and tests of SemanticVersion. Typical test files would be in the unit test suite for protocol/BuildInfo or protocol/BuildInfo_test.cpp, as well as tests for version encoding/decoding and error handling (e.g., invalid version strings, out-of-range values, malformed pre-release identifiers). Gaps may exist if there are no tests for malformed version strings, edge cases for range checks, or unusual pre-release identifiers. There is no evidence in this file of explicit test hooks or test-only code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "beast::SemanticVersion (custom class), explicit C++ checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LogicError (exception)", + "field": "version string (s)", + "location": "getVersionString", + "validated_by": "beast::SemanticVersion::parse and beast::SemanticVersion::print", + "validates": [ + "Checks if the version string can be parsed as a semantic version", + "Checks if the printed version matches the input string (round-trip format validation)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "None (value is only encoded if valid, otherwise skipped)", + "field": "majorVersion", + "location": "encodeSoftwareVersion", + "validated_by": "explicit range check (>=0 && <=255)", + "validates": [ + "Ensures majorVersion is between 0 and 255 before encoding" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "None (value is only encoded if valid, otherwise skipped)", + "field": "minorVersion", + "location": "encodeSoftwareVersion", + "validated_by": "explicit range check (>=0 && <=255)", + "validates": [ + "Ensures minorVersion is between 0 and 255 before encoding" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "None (value is only encoded if valid, otherwise skipped)", + "field": "patchVersion", + "location": "encodeSoftwareVersion", + "validated_by": "explicit range check (>=0 && <=255)", + "validates": [ + "Ensures patchVersion is between 0 and 255 before encoding" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "None (function returns default encoding if parse fails)", + "field": "versionStr", + "location": "encodeSoftwareVersion", + "validated_by": "beast::SemanticVersion::parse", + "validates": [ + "Checks if versionStr is a valid semantic version string" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/BuildInfo.cpp.ai.md b/src/libxrpl/protocol/BuildInfo.cpp.ai.md new file mode 100644 index 0000000000..4c93ffc38d --- /dev/null +++ b/src/libxrpl/protocol/BuildInfo.cpp.ai.md @@ -0,0 +1,35 @@ +# `BuildInfo.cpp` — Version Identity and Network Encoding for xrpld + +This file owns everything the XRPL daemon uses to identify itself: the human-readable release string, its derivation into a compact 64-bit wire encoding, and the comparison utilities that let validators detect when peers are running newer software. + +## The Version String Pipeline + +The raw version constant `versionString` (e.g., `"3.2.0-b0"`) is a `const char*` in an anonymous namespace — the single edit point a developer touches when cutting a release. From there it passes through two transforms before becoming public. + +`buildVersionString()` (private, never exported) optionally appends SemVer build metadata. In `DEBUG` or sanitizer builds, it queries `xrpl::git::getCommitHash()` to prepend the commit, then tacks on `DEBUG` and/or the stringified `SANITIZERS` macro. The result looks like `3.2.0-b0+a1b2c3d.DEBUG.address`. The `BOOST_PP_STRINGIZE` call is the only way to convert a preprocessor-defined token list (`-DSANITIZERS=address,undefined`) into a runtime string. + +`getVersionString()` is the public entry point and uses a `static const` local variable — C++11-guaranteed to initialize exactly once and thread-safely — to memoize the result. Before caching, it round-trips through `beast::SemanticVersion`: it parses the string, then checks `v.print() == s`. This double check catches both malformed strings (parse fails) and strings that are semantically valid but not in canonical form (parse succeeds but the canonical form differs). A failure calls `LogicError`, which throws unconditionally. Since `getVersionString()` is called during application startup, any version string typo kills the process immediately rather than silently propagating a bad identity. + +`getFullVersionString()` prepends the system name from `systemName()` (which returns `"xrpld"`) to produce identifiers like `xrpld-3.2.0-b0`. This composite form appears in the `User-Agent` header of HTTP requests made by the node and in startup log messages. + +## The 64-bit Wire Encoding + +The XRP Ledger peer protocol needs to include version information in consensus validation messages without the cost of string comparison. `encodeSoftwareVersion()` compresses an entire semantic version string into a `uint64_t` with a fixed layout described in the header: + +``` +0x183B | MAJOR(8) | MINOR(8) | PATCH(8) | TYPE(2) | NUMBER(6) | 0x0000 +``` + +The upper 16 bits `0x183B` act as an implementation fingerprint — a namespace that distinguishes xrpld versions from any other software that also publishes version numbers on the XRPL network. This makes integer comparison safe: without it, an alien implementation with a high version number could appear "newer" in a purely numeric comparison. + +The `TYPE` field (bits 23–22 within the lower 48) uses a deliberate encoding: `0b11` for releases, `0b10` for release candidates, `0b01` for betas. This ordering means a plain integer comparison on the entire `uint64_t` gives correct semantic ordering — a release is numerically greater than an RC of the same version, which is greater than a beta — without any special-case logic at comparison time. + +The `parsePreRelease` lambda handles extraction of the pre-release type and number. It checks for a `"rc"` or `"b"` prefix, then uses `beast::lexicalCastChecked` for safe string-to-integer conversion, and `std::clamp` to enforce the 0–63 range. On any failure — empty suffix, non-numeric suffix, out-of-range number — it returns zero silently. The pre-release byte in the encoded version then remains zero, which is correct: unknown or malformed pre-release identifiers sort below any known type. + +Both `getEncodedVersion()` (this node's own encoded version) and the result of `encodeSoftwareVersion()` for known versions are lazily computed and cached as statics. + +## Network Integration + +During consensus, every flag ledger (every 256 ledgers), the local node writes `getEncodedVersion()` into the `sfServerVersion` field of its validation message. `LedgerMaster` collects these values from all validators and calls `isXrpldVersion()` and `isNewerVersion()` to tally how many peers are running xrpld and how many are running a newer release. This drives version upgrade notifications and network health diagnostics. + +`isNewerVersion()` explicitly guards against non-xrpld versions by calling `isXrpldVersion()` first and returning `false` for any version with an unrecognized upper-16 fingerprint. This is the critical safety valve: a non-xrpld peer advertising a very large `uint64_t` cannot trick the local node into believing it is running outdated software. \ No newline at end of file diff --git a/src/libxrpl/protocol/ErrorCodes.cpp.ai.json b/src/libxrpl/protocol/ErrorCodes.cpp.ai.json new file mode 100644 index 0000000000..453e27b31a --- /dev/null +++ b/src/libxrpl/protocol/ErrorCodes.cpp.ai.json @@ -0,0 +1,281 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "make_error", + "get_error_info", + "sortErrorInfos" + ], + "entry_point": "make_error", + "purpose": "Creates a JSON error object for RPC responses, validating error codes and mapping them to error info.", + "validation_points": [ + "get_error_info: Validates that the error code exists in the sorted ErrorInfo array. Throws if not found." + ] + }, + { + "call_chain": [ + "inject_error", + "get_error_info", + "sortErrorInfos" + ], + "entry_point": "inject_error", + "purpose": "Injects error information into an existing JSON object, validating error codes.", + "validation_points": [ + "get_error_info: Validates error code existence." + ] + }, + { + "call_chain": [ + "contains_error", + "get_error_info", + "sortErrorInfos" + ], + "entry_point": "contains_error", + "purpose": "Checks if a given error code is recognized/valid.", + "validation_points": [ + "get_error_info: Validates error code existence." + ] + }, + { + "call_chain": [ + "error_code_http_status", + "get_error_info", + "sortErrorInfos" + ], + "entry_point": "error_code_http_status", + "purpose": "Maps an error code to its HTTP status, validating code.", + "validation_points": [ + "get_error_info: Validates error code existence." + ] + } + ], + "data_flows": [ + { + "field": "error_code", + "flow": [ + "User input or internal error triggers function call", + "Passed to get_error_info", + "get_error_info uses sortErrorInfos to find ErrorInfo", + "ErrorInfo used to populate JSON or return status" + ], + "origin": "Passed as argument to make_error, inject_error, contains_error, error_code_http_status", + "transformations": [ + "Mapped from integer code to ErrorInfo struct", + "ErrorInfo fields (token, message, http_status) extracted" + ], + "validated_at": "get_error_info (throws if code not found)" + }, + { + "field": "ErrorInfo", + "flow": [ + "unorderedErrorInfos", + "sortErrorInfos (sorts and caches array)", + "get_error_info (binary search for code)", + "Used in make_error/inject_error to populate JSON" + ], + "origin": "Static unorderedErrorInfos array", + "transformations": [ + "Sorted by code for efficient lookup", + "Extracted fields for JSON output" + ], + "validated_at": "get_error_info (ensures code exists)" + }, + { + "field": "JSON error object", + "flow": [ + "make_error/inject_error", + "Populated with ErrorInfo fields", + "Returned to RPC client" + ], + "origin": "Constructed in make_error/inject_error", + "transformations": [ + "Fields set: error, error_code, error_message, etc." + ], + "validated_at": "Only valid ErrorInfo used (via get_error_info)" + } + ], + "description": "Defines and manages RPC error codes, their metadata, and utilities for injecting and handling errors in JSON responses for the XRPL protocol.", + "false_positive_patterns": [ + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/ErrorCodes.cpp", + "functions": [ + { + "args": [ + "unordered" + ], + "lineno": 81, + "name": "sortErrorInfos" + }, + { + "args": [ + "code", + "json" + ], + "lineno": 134, + "name": "inject_error" + }, + { + "args": [ + "code", + "message", + "json" + ], + "lineno": 140, + "name": "inject_error" + }, + { + "args": [ + "code" + ], + "lineno": 146, + "name": "get_error_info" + }, + { + "args": [ + "code" + ], + "lineno": 153, + "name": "make_error" + }, + { + "args": [ + "code", + "message" + ], + "lineno": 159, + "name": "make_error" + }, + { + "args": [ + "json" + ], + "lineno": 165, + "name": "contains_error" + }, + { + "args": [ + "code" + ], + "lineno": 170, + "name": "error_code_http_status" + }, + { + "args": [ + "jv" + ], + "lineno": 175, + "name": "rpcErrorString" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "RPC" + }, + { + "lineno": 9, + "name": "detail" + } + ], + "test_coverage_notes": "Testing for this code is typically found in unit tests for the RPC error handling layer, e.g., tests that call RPC endpoints with invalid parameters and check for correct error codes/messages. Likely test files: 'test/rpc/ErrorCodes_test.cpp', 'test/rpc/Handler_test.cpp', or integration tests for RPC endpoints. Gaps: Direct unit tests for get_error_info edge cases (e.g., unknown error codes) may be missing; error code/HTTP status mapping may not be exhaustively tested for all codes. Template-based validation is not directly tested here, but via higher-level RPC tests.", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/ErrorCodes.cpp.ai.md b/src/libxrpl/protocol/ErrorCodes.cpp.ai.md new file mode 100644 index 0000000000..24663a968d --- /dev/null +++ b/src/libxrpl/protocol/ErrorCodes.cpp.ai.md @@ -0,0 +1,45 @@ +# `ErrorCodes.cpp` — RPC Error Code Registry and JSON Serialization + +## Role in the System + +This file is the single source of truth for every named error condition that the XRPL RPC layer can return to a client. It provides three things: a compile-time validated lookup table mapping `error_code_i` integers to human-readable metadata, a small set of JSON-serialization utilities that stamp that metadata onto response objects, and an HTTP-status mapping used by the transport layer to signal retryability to load balancers. + +## The `ErrorInfo` Lookup Table + +The central data structure is the `detail::sortedErrorInfos` array, a `constexpr std::array` that is built at compile time from the unordered initializer list `unorderedErrorInfos`. Each `ErrorInfo` entry holds an `error_code_i` code, a short camelCase `token` (the machine-readable name sent to API clients), a human-readable `message`, and an `http_status` integer that defaults to `200` when not explicitly specified. + +The deliberate separation of the authoring list from the storage array is the key architectural insight here. The developer-maintained `unorderedErrorInfos` list can be written in any order, grouped thematically, without worrying about the numeric gaps in `error_code_i`. The `sortErrorInfos()` template function then re-indexes every entry by `code - 1` into the correctly-sized output array. This turns what would otherwise be a maintenance burden — keeping a parallel list in strict numeric order — into a compiler responsibility. + +## Compile-Time Validation via `sortErrorInfos` + +`sortErrorInfos` is more than a sort; it is a full integrity check executed at compile time (the comment acknowledges it should become `consteval` in C++20). It enforces three invariants: + +1. **Range check** — every code must satisfy `rpcSUCCESS < code <= rpcLAST`. Codes outside that window throw `std::out_of_range`. +2. **Uniqueness** — if a slot in the output array is already occupied when a second entry tries to claim it, `std::invalid_argument` is thrown for a duplicate. +3. **Contiguity** — after placement, the function walks the array and checks that every non-`rpcUNKNOWN` entry exactly matches its expected index position. This rejects the subtle case where an entry was silently placed at the wrong index because of an integer typo. It also confirms the total count of recognized entries matches `N`, the size of the input array. + +The result is that a developer adding a new error code who accidentally reuses an integer, uses a value outside the declared range, or forgets to add the new code to `unorderedErrorInfos` will see a compile-time failure rather than a silent runtime bug. + +## O(1) Lookup + +`get_error_info()` exploits the layout guarantee established by `sortErrorInfos`: a code at value `k` lives at index `k - 1`. The implementation is therefore a direct array subscript, not a search. Out-of-range codes return a reference to `detail::unknownError`, the default-constructed `ErrorInfo` whose code is `rpcUNKNOWN` and whose HTTP status is 200. + +## HTTP Status Philosophy + +The file's own comment explains the design tension honestly. Every RPC error originally returned HTTP 200, which is semantically correct for a well-formed JSON-RPC transport — the HTTP transaction succeeded even though the application-level call failed. The current assignment of 4xx/5xx codes is deliberately narrow and targets a specific use case: load-balancer failover. + +When a request fails on one node but *might* succeed on another — for example because the node is amendment-blocked (`503`), not yet synced (`503`), or internally overwhelmed (`503 tooBusy` / `429 slowDown`) — a non-200 status causes upstream load balancers to retry on a healthy peer. Errors that reflect permanent or client-side conditions (`400 badSyntax`, `403 badSecret`, `404 lgrNotFound`) are not retryable and correctly signal that retrying elsewhere would be futile. `rpcLGR_NOT_VALIDATED` is `202` (Accepted but not finalized) — a nuanced choice indicating the ledger is real but consensus is still pending. Errors without an explicit `http_status_` constructor argument keep 200, preserving the original behavior for anything not yet categorized for failover purposes. + +## Public API + +`inject_error()` mutates an existing `Json::Value` by writing `error`, `error_code`, and `error_message` fields onto it. The overload accepting a `std::string message` replaces the default message with the caller-supplied one, enabling context-specific diagnostics while preserving the stable token and code. `make_error()` wraps `inject_error()` in a factory that constructs and returns a fresh `Json::Value`. + +`contains_error()` provides a lightweight probe — it checks only for the presence of an `"error"` member on a JSON object — useful for callers that receive a `Json::Value` and need to branch on success vs. failure without inspecting the specific code. + +`error_code_http_status()` extracts just the status integer, used by the HTTP transport layer when constructing the HTTP response header. + +`rpcErrorString()`, living one namespace level up in `xrpl` rather than `xrpl::RPC`, concatenates the `error` token and `error_message` fields of a JSON error value into a single string. It is accompanied by an `XRPL_ASSERT` that the input already contains an error, making misuse diagnosable in debug builds. + +## Stability Contract + +The `error_code_i` enum in the companion header carries an explicit warning: although the numeric values were never intended to be stable, they were inadvertently exposed through API responses, and some clients depend on them. The enum is therefore treated as append-only — gaps must not be filled, retired codes must not be reused, and the comment marks several such reserved slots explicitly. The `sortErrorInfos` validation enforces this implicitly: a reassigned integer would either collide with an existing entry or fail the contiguity check. \ No newline at end of file diff --git a/src/libxrpl/protocol/Feature.cpp.ai.json b/src/libxrpl/protocol/Feature.cpp.ai.json new file mode 100644 index 0000000000..62c5d1b5af --- /dev/null +++ b/src/libxrpl/protocol/Feature.cpp.ai.json @@ -0,0 +1,530 @@ +{ + "args": [ + { + "lineno": 14, + "name": "feature" + }, + { + "lineno": 66, + "name": "i" + }, + { + "lineno": 72, + "name": "feature" + }, + { + "lineno": 77, + "name": "feature" + }, + { + "lineno": 82, + "name": "name" + }, + { + "lineno": 97, + "name": "name" + }, + { + "lineno": 110, + "name": "name" + }, + { + "lineno": 110, + "name": "support" + }, + { + "lineno": 110, + "name": "vote" + }, + { + "lineno": 159, + "name": "f" + }, + { + "lineno": 169, + "name": "i" + }, + { + "lineno": 175, + "name": "f" + }, + { + "lineno": 216, + "name": "name" + }, + { + "lineno": 220, + "name": "name" + }, + { + "lineno": 220, + "name": "support" + }, + { + "lineno": 220, + "name": "vote" + }, + { + "lineno": 225, + "name": "name" + }, + { + "lineno": 235, + "name": "f" + }, + { + "lineno": 239, + "name": "i" + }, + { + "lineno": 243, + "name": "f" + }, + { + "lineno": 252, + "name": "fn" + }, + { + "lineno": 105, + "name": "condition" + }, + { + "lineno": 105, + "name": "logicErrorMessage" + } + ], + "classes": [ + { + "args": [], + "lineno": 38, + "name": "FeatureCollections" + }, + { + "args": [ + "name", + "feature" + ], + "lineno": 41, + "name": "Feature" + }, + { + "args": [], + "lineno": 49, + "name": "byIndex" + }, + { + "args": [], + "lineno": 51, + "name": "byName" + }, + { + "args": [], + "lineno": 53, + "name": "byFeature" + } + ], + "code_paths": [ + { + "call_chain": [ + "FeatureCollections::registerFeature", + "FeatureCollections::features.emplace(Feature(name, feature))", + "Feature::Feature(name, feature)" + ], + "entry_point": "FeatureCollections::registerFeature", + "purpose": "Registers a new feature (amendment) by name and feature hash into the features collection.", + "validation_points": [ + "Feature::Feature(name, feature) - C++ type system ensures correct types for name and feature.", + "Feature() = delete - Prevents default construction, ensuring all Feature objects are initialized with valid data." + ] + }, + { + "call_chain": [ + "FeatureCollections::getByName", + "features.get().find(name)" + ], + "entry_point": "FeatureCollections::getByName", + "purpose": "Retrieves a feature by its name from the features collection.", + "validation_points": [ + "features.get().find(name) - Implicit validation: returns end() if not found, caller must check." + ] + }, + { + "call_chain": [ + "FeatureCollections::getByFeature", + "features.get().find(feature)" + ], + "entry_point": "FeatureCollections::getByFeature", + "purpose": "Retrieves a feature by its uint256 feature hash.", + "validation_points": [ + "features.get().find(feature) - Implicit validation: returns end() if not found, caller must check." + ] + }, + { + "call_chain": [ + "FeatureCollections::getByIndex", + "features.get()[index]" + ], + "entry_point": "FeatureCollections::getByIndex", + "purpose": "Retrieves a feature by its index in the collection.", + "validation_points": [ + "Index bounds checking is required by caller; no explicit validation in this function." + ] + } + ], + "data_flows": [ + { + "field": "Feature::name", + "flow": [ + "registerFeature(name, feature)", + "Feature(name, feature)", + "features.emplace(Feature(name, feature))", + "features.get()" + ], + "origin": "Passed as argument to Feature constructor in registerFeature", + "transformations": [ + "Stored as std::string in Feature struct" + ], + "validated_at": "Feature constructor (type system); uniqueness enforced by multi_index" + }, + { + "field": "Feature::feature", + "flow": [ + "registerFeature(name, feature)", + "Feature(name, feature)", + "features.emplace(Feature(name, feature))", + "features.get()" + ], + "origin": "Passed as argument to Feature constructor in registerFeature", + "transformations": [ + "Stored as uint256 in Feature struct" + ], + "validated_at": "Feature constructor (type system); uniqueness enforced by multi_index" + }, + { + "field": "features (multi_index_container)", + "flow": [ + "registerFeature", + "features.emplace(Feature(name, feature))", + "getByName / getByFeature / getByIndex" + ], + "origin": "Constructed in FeatureCollections", + "transformations": [ + "Indexed by name, feature, and index for fast lookup" + ], + "validated_at": "Insertion: uniqueness constraints; Lookup: existence checked by caller" + }, + { + "field": "Feature (struct)", + "flow": [ + "registerFeature", + "features.emplace(Feature(name, feature))", + "Used in lookups and returned by getByName/getByFeature/getByIndex" + ], + "origin": "Constructed only via Feature(name, feature)", + "transformations": [ + "No mutation after construction" + ], + "validated_at": "Constructor (type system); default construction forbidden" + } + ], + "description": "This file manages the registration, lookup, and metadata of protocol features (amendments) in the XRPL (XRP Ledger) server, including their support status, voting behavior, and mapping between names, bitset indices, and feature IDs. It uses a multi-index container for efficient access and provides macros for feature registration.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Feature.name", + "validation", + "missing", + "check" + ], + "evidence": "Field Feature.name validated by C++ type system (no explicit validation framework detected)", + "issue_pattern": "Missing validation for Feature.name", + "why_false_positive": "C++ type system (no explicit validation framework detected) validates Feature.name automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Feature.feature", + "validation", + "missing", + "check" + ], + "evidence": "Field Feature.feature validated by C++ type system (no explicit validation framework detected)", + "issue_pattern": "Missing validation for Feature.feature", + "why_false_positive": "C++ type system (no explicit validation framework detected) validates Feature.feature automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Feature (constructor arguments: name, feature)", + "empty", + "string", + "validation" + ], + "evidence": "C++ type system (constructor signature) at Feature::Feature(std::string const& name_, uint256 const& feature_)", + "issue_pattern": "Missing empty string validation for Feature (constructor arguments: name, feature)", + "why_false_positive": "C++ type system (constructor signature) validates Feature (constructor arguments: name, feature) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "Feature (constructor arguments: name, feature)", + "type", + "validation", + "check" + ], + "evidence": "C++ type system (constructor signature) at Feature::Feature(std::string const& name_, uint256 const& feature_)", + "issue_pattern": "Missing type validation for Feature (constructor arguments: name, feature)", + "why_false_positive": "C++ type system (constructor signature) validates Feature (constructor arguments: name, feature) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Feature (default construction)", + "empty", + "string", + "validation" + ], + "evidence": "Feature() = delete at Feature struct definition", + "issue_pattern": "Missing empty string validation for Feature (default construction)", + "why_false_positive": "Feature() = delete validates Feature (default construction) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Feature.cpp", + "functions": [ + { + "args": [ + "feature" + ], + "lineno": 13, + "name": "hash_value" + }, + { + "args": [ + "i" + ], + "lineno": 65, + "name": "getByIndex" + }, + { + "args": [ + "feature" + ], + "lineno": 71, + "name": "getIndex" + }, + { + "args": [ + "feature" + ], + "lineno": 76, + "name": "getByFeature" + }, + { + "args": [ + "name" + ], + "lineno": 81, + "name": "getByName" + }, + { + "args": [], + "lineno": 93, + "name": "FeatureCollections" + }, + { + "args": [ + "name" + ], + "lineno": 96, + "name": "getRegisteredFeature" + }, + { + "args": [ + "name", + "support", + "vote" + ], + "lineno": 109, + "name": "registerFeature" + }, + { + "args": [], + "lineno": 153, + "name": "registrationIsDone" + }, + { + "args": [ + "f" + ], + "lineno": 158, + "name": "featureToBitsetIndex" + }, + { + "args": [ + "i" + ], + "lineno": 168, + "name": "bitsetIndexToFeature" + }, + { + "args": [ + "f" + ], + "lineno": 174, + "name": "featureToName" + }, + { + "args": [], + "lineno": 187, + "name": "allAmendments" + }, + { + "args": [], + "lineno": 195, + "name": "supportedAmendments" + }, + { + "args": [], + "lineno": 202, + "name": "numDownVotedAmendments" + }, + { + "args": [], + "lineno": 209, + "name": "numUpVotedAmendments" + }, + { + "args": [ + "name" + ], + "lineno": 215, + "name": "getRegisteredFeature" + }, + { + "args": [ + "name", + "support", + "vote" + ], + "lineno": 219, + "name": "registerFeature" + }, + { + "args": [ + "name" + ], + "lineno": 224, + "name": "retireFeature" + }, + { + "args": [], + "lineno": 230, + "name": "registrationIsDone" + }, + { + "args": [ + "f" + ], + "lineno": 234, + "name": "featureToBitsetIndex" + }, + { + "args": [ + "i" + ], + "lineno": 238, + "name": "bitsetIndexToFeature" + }, + { + "args": [ + "f" + ], + "lineno": 242, + "name": "featureToName" + }, + { + "args": [ + "fn" + ], + "lineno": 251, + "name": "enforceValidFeatureName" + }, + { + "args": [ + "condition", + "logicErrorMessage" + ], + "lineno": 104, + "name": "check" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "Feature registration and lookup are likely tested in protocol-level or amendment-related unit tests, e.g., in test files like Feature_test.cpp, AmendmentTable_test.cpp, or Application_test.cpp. However, the validation that Feature cannot be default-constructed is enforced by the C++ compiler and may not be directly tested. There may be limited or no explicit tests for multi_index uniqueness constraints or for error handling when lookups fail (e.g., not found cases). Edge cases such as duplicate registration or invalid input types are protected by the type system and container constraints, but explicit runtime tests for these scenarios may be lacking.", + "validation_architecture": { + "auto_validated_fields": [ + "Feature.name", + "Feature.feature" + ], + "framework": "C++ type system (no explicit validation framework detected)", + "validation_layer": "constructor (business_logic/type)" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "compile-time error (if types do not match)", + "field": "Feature (constructor arguments: name, feature)", + "location": "Feature::Feature(std::string const& name_, uint256 const& feature_)", + "validated_by": "C++ type system (constructor signature)", + "validates": [ + "name must be std::string", + "feature must be uint256" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "compile-time error (deleted constructor)", + "field": "Feature (default construction)", + "location": "Feature struct definition", + "validated_by": "Feature() = delete", + "validates": [ + "Feature cannot be default-constructed" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Feature.cpp.ai.md b/src/libxrpl/protocol/Feature.cpp.ai.md new file mode 100644 index 0000000000..9122be0c36 --- /dev/null +++ b/src/libxrpl/protocol/Feature.cpp.ai.md @@ -0,0 +1,70 @@ +# `src/libxrpl/protocol/Feature.cpp` — Amendment Registry + +## Purpose + +`Feature.cpp` implements the central registry for all XRPL protocol amendments (called "features" in code). On the XRP Ledger, validator voting activates amendments on-chain; every conditional code path gated by an amendment queries this registry at runtime. The file is responsible for three things: holding all amendment metadata in a queryable form, providing the bidirectional mapping between a feature's `uint256` on-chain identifier and a compact bitset index used by `FeatureBitset`, and enforcing that the registry is fully populated before any queries reach it. + +## The `FeatureCollections` Internal Singleton + +The entire registry lives in a file-scope `FeatureCollections featureCollections` instance. The class is deliberately anonymous within an unnamed namespace — callers reach it only through the thin free-function wrappers at the bottom of the file. + +`FeatureCollections` maintains three data structures simultaneously: + +- **`features`** — a `boost::multi_index_container` that stores `Feature` structs (name + `uint256`) with three simultaneous indexes: random-access by insertion order (`byIndex`), hash-unique by `uint256` (`byFeature`), and hash-unique by string name (`byName`). +- **`all`** — a `std::map` covering every registered amendment, including retired ones. +- **`supported`** — a `std::map` covering only amendments the server can vote on (`Supported::yes`). + +The `upVotes` and `downVotes` counters shadow the supported map's content so test code can verify vote tallies without iterating the map. + +The multi-index container is the architectural centerpiece. A simple `unordered_map` would support name→hash and hash→name lookups, but it cannot provide a stable integer index. The stable index is essential because `FeatureBitset` (defined in `Feature.h`) is a `std::bitset` — each bit corresponds to one amendment's *insertion-order position* in this container. The `featureToBitsetIndex()` / `bitsetIndexToFeature()` pair translate between `uint256` and bitset index at every hot-path amendment check. Using a random-access index inside a single container avoids maintaining a separate parallel array and keeps all three lookups O(1) without cache-thrashing. + +## Registration and the `readOnly` Fence + +The `std::atomic readOnly` member acts as a write-once fence. During registration, `readOnly` is false and all writes are allowed. Once the final static variable `readOnlySet` is initialized (the last statement in the file), `registrationIsDone()` flips `readOnly` to true. Every subsequent query method asserts `readOnly.load()` via `XRPL_ASSERT`. + +This design exploits C++'s guarantee that file-scope variables in a single translation unit are initialized top-to-bottom. All `uint256 const feature##name` variables are initialized in sequence before `readOnlySet`, so by the time anything outside the translation unit can run, registration is complete and the registry is permanently read-only. No runtime lock is ever needed. + +`registerFeature()` enforces several invariants at registration time using a small `check()` helper that calls `LogicError()` on failure: +- `Supported::no` features must have `VoteBehavior::DefaultNo` (you cannot vote for an unsupported feature). +- Duplicate names are a hard error — the duplicate check uses `getByName()` before inserting. +- The container size must remain within the pre-allocated `detail::numFeatures` bound. +- `upVotes + downVotes` must always equal `supported.size()`. + +## Feature ID Derivation + +Each amendment's `uint256` on-chain identifier is computed as `sha512Half(Slice(name.data(), name.size()))` at registration time. The name string *is* the canonical source — the hash is deterministic and reproducible from just the ASCII name. This means two builds will produce identical identifiers as long as the macro list hasn't changed, and the ledger can reference amendments by their hash without any out-of-band identifier distribution. + +## The X-Macro Pattern + +The master list of all amendments lives in `include/xrpl/protocol/detail/features.macro`. That single file is `#include`d three times with different macro bindings: + +1. **In `Feature.h` (inside `detail` namespace)**: `XRPL_FEATURE` and friends expand to `+1`, so `numFeatures` is computed as a `constexpr std::size_t` at compile time. This is the bitset size. + +2. **In `Feature.h` (near the bottom)**: `XRPL_FEATURE(name, ...)` expands to `extern uint256 const feature##name;` — public declarations that let any translation unit reference a specific amendment as a global constant. + +3. **In `Feature.cpp`**: `XRPL_FEATURE(name, supported, vote)` expands to `uint256 const feature##name = registerFeature(enforceValidFeatureName([] { return #name; }), supported, vote);` — the actual definitions that trigger registration via the initializer. + +The push/pop macro scaffolding (`#pragma push_macro`) around each inclusion ensures these redefinitions don't permanently pollute the macro namespace. + +## Compile-Time Name Validation + +`enforceValidFeatureName()` is a `consteval` wrapper that calls two `consteval` predicates from `Feature.h`: + +- `validFeatureName()` rejects any name containing bytes with the high bit set (non-ASCII) or below `0x20` (control characters). This prevents visually confusable Unicode identifiers from making it into a production build. +- `validFeatureNameSize()` rejects names longer than 63 bytes and names exactly 32 bytes long. The 32-byte exclusion reserves that length for raw `uint256` hashes in WASM/interop contexts, preventing a name-as-hash collision. + +Both checks fire as `static_assert` failures at compile time, so an invalid name in `features.macro` is a build error, not a runtime surprise. + +## Amendment Lifecycle States + +`VoteBehavior` and `AmendmentSupport` together express the full lifecycle: + +- **`Supported::no, DefaultNo`** — feature in active development; registered but neither voted for nor available in released software. +- **`Supported::yes, DefaultNo`** — complete and supported; validators can enable it but the server does not vote for it by default, leaving the decision to the community. +- **`Supported::yes, DefaultYes`** — critical bug fix; the server actively votes for it (reserved for urgent fixes with explicit community communication). +- **`Supported::yes, Obsolete`** — once-supported amendment that never passed; the server still understands it if somehow enabled, but no longer votes for it. +- **`XRPL_RETIRE_FEATURE` / `XRPL_RETIRE_FIX`** — calls `retireFeature()`, which registers the name as `Supported::yes, VoteBehavior::Obsolete` but also marks it `AmendmentSupport::Retired`. The conditional code guarded by the feature has been removed from the codebase. The amendment must remain registered because it may appear in the Amendments ledger object; removing it entirely would cause amendment-blocking. + +## Relationship to `FeatureBitset` + +`FeatureBitset`, defined entirely in `Feature.h`, is a `std::bitset` wrapper that overloads `set()`, `reset()`, `flip()`, and `operator[]` to accept `uint256` directly, internally calling `featureToBitsetIndex()` on each access. The bitwise operators (`&`, `|`, `^`, `-` for set difference) allow the ledger's `Rules` object to efficiently compute which amendments are active by intersecting sets. This is the data structure that every transaction-processing code path queries via `view.rules.enabled(featureFoo)`. \ No newline at end of file diff --git a/src/libxrpl/protocol/IOUAmount.cpp.ai.json b/src/libxrpl/protocol/IOUAmount.cpp.ai.json new file mode 100644 index 0000000000..ff70bd9643 --- /dev/null +++ b/src/libxrpl/protocol/IOUAmount.cpp.ai.json @@ -0,0 +1,577 @@ +{ + "args": [ + { + "lineno": 26, + "name": "v" + }, + { + "lineno": 36, + "name": "number" + }, + { + "lineno": 81, + "name": "other" + }, + { + "lineno": 85, + "name": "other" + }, + { + "lineno": 112, + "name": "amount" + }, + { + "lineno": 116, + "name": "amt" + }, + { + "lineno": 116, + "name": "num" + }, + { + "lineno": 116, + "name": "den" + }, + { + "lineno": 116, + "name": "roundUp" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "IOUAmount::fromNumber", + "Number::normalizeToRange" + ], + "entry_point": "IOUAmount::fromNumber", + "purpose": "Constructs an IOUAmount from a Number, normalizing mantissa and exponent to allowed ranges.", + "validation_points": [ + "Number::normalizeToRange (validates mantissa_ and exponent_ against min/max)" + ] + }, + { + "call_chain": [ + "IOUAmount::normalize", + "getSTNumberSwitchover", + "IOUAmount::fromNumber (if switchover is true)", + "Number::normalizeToRange" + ], + "entry_point": "IOUAmount::normalize", + "purpose": "Normalizes the mantissa and exponent to ensure they are within valid ranges, handling zero and overflow cases.", + "validation_points": [ + "IOUAmount::normalize (manual checks on mantissa_ and exponent_)", + "IOUAmount::fromNumber (calls Number::normalizeToRange)" + ] + }, + { + "call_chain": [ + "IOUAmount::IOUAmount(Number const&)", + "IOUAmount::fromNumber", + "Number::normalizeToRange" + ], + "entry_point": "IOUAmount::IOUAmount(Number const&)", + "purpose": "Constructs an IOUAmount from a Number, ensuring normalization and validation.", + "validation_points": [ + "IOUAmount::fromNumber (calls Number::normalizeToRange)", + "IOUAmount::IOUAmount(Number const&) (manual checks on exponent_)" + ] + }, + { + "call_chain": [ + "IOUAmount::operator+=", + "getSTNumberSwitchover", + "IOUAmount(Number{*this} + Number{other}) (if switchover is true)", + "normalize (if switchover is false)" + ], + "entry_point": "IOUAmount::operator+=", + "purpose": "Adds another IOUAmount, normalizing and validating the result.", + "validation_points": [ + "IOUAmount::normalize (manual checks on mantissa_ and exponent_)" + ] + } + ], + "data_flows": [ + { + "field": "mantissa_", + "flow": [ + "Input (Number or direct value)", + "Assigned to IOUAmount::mantissa_", + "Transformed in normalize (multiplied/divided by 10, sign adjusted)", + "Used in arithmetic (operator+=, etc.)", + "Validated in normalize and fromNumber" + ], + "origin": "Input Number or direct assignment in IOUAmount", + "transformations": [ + "Normalization (scaling to fit minMantissa/maxMantissa)", + "Sign adjustment (negative/positive)", + "Arithmetic operations (addition, multiplication)" + ], + "validated_at": "normalize, fromNumber (via Number::normalizeToRange)" + }, + { + "field": "exponent_", + "flow": [ + "Input (Number or direct value)", + "Assigned to IOUAmount::exponent_", + "Transformed in normalize (incremented/decremented as mantissa is scaled)", + "Used in arithmetic (operator+=, etc.)", + "Validated in normalize and fromNumber" + ], + "origin": "Input Number or direct assignment in IOUAmount", + "transformations": [ + "Normalization (incremented/decremented to keep mantissa in range)", + "Arithmetic operations (addition, etc.)" + ], + "validated_at": "normalize, fromNumber (via Number::normalizeToRange)" + } + ], + "description": "Implements arithmetic and normalization logic for IOUAmount in the XRPL protocol, including conversion, normalization, addition, and multiplication with ratio, using arbitrary precision math for safe calculations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "exponent_", + "empty", + "string", + "validation" + ], + "evidence": "manual check at IOUAmount::normalize", + "issue_pattern": "Missing empty string validation for exponent_", + "why_false_positive": "manual check validates exponent_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "exponent_", + "range", + "bounds", + "validation" + ], + "evidence": "manual check at IOUAmount::normalize", + "issue_pattern": "Missing range validation for exponent_", + "why_false_positive": "manual check validates exponent_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "exponent_", + "empty", + "string", + "validation" + ], + "evidence": "manual check at IOUAmount::normalize", + "issue_pattern": "Missing empty string validation for exponent_", + "why_false_positive": "manual check validates exponent_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "exponent_", + "range", + "bounds", + "validation" + ], + "evidence": "manual check at IOUAmount::normalize", + "issue_pattern": "Missing range validation for exponent_", + "why_false_positive": "manual check validates exponent_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mantissa_", + "empty", + "string", + "validation" + ], + "evidence": "manual check at IOUAmount::normalize", + "issue_pattern": "Missing empty string validation for mantissa_", + "why_false_positive": "manual check validates mantissa_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "mantissa_", + "range", + "bounds", + "validation" + ], + "evidence": "manual check at IOUAmount::normalize", + "issue_pattern": "Missing range validation for mantissa_", + "why_false_positive": "manual check validates mantissa_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mantissa_", + "empty", + "string", + "validation" + ], + "evidence": "manual check at IOUAmount::normalize", + "issue_pattern": "Missing empty string validation for mantissa_", + "why_false_positive": "manual check validates mantissa_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "mantissa_", + "range", + "bounds", + "validation" + ], + "evidence": "manual check at IOUAmount::normalize", + "issue_pattern": "Missing range validation for mantissa_", + "why_false_positive": "manual check validates mantissa_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mantissa_", + "empty", + "string", + "validation" + ], + "evidence": "manual check at IOUAmount::normalize", + "issue_pattern": "Missing empty string validation for mantissa_", + "why_false_positive": "manual check validates mantissa_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "exponent_", + "empty", + "string", + "validation" + ], + "evidence": "manual check at IOUAmount(Number const& other) (constructor)", + "issue_pattern": "Missing empty string validation for exponent_", + "why_false_positive": "manual check validates exponent_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "exponent_", + "range", + "bounds", + "validation" + ], + "evidence": "manual check at IOUAmount(Number const& other) (constructor)", + "issue_pattern": "Missing range validation for exponent_", + "why_false_positive": "manual check validates exponent_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "exponent_", + "empty", + "string", + "validation" + ], + "evidence": "manual check at IOUAmount(Number const& other) (constructor)", + "issue_pattern": "Missing empty string validation for exponent_", + "why_false_positive": "manual check validates exponent_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "exponent_", + "range", + "bounds", + "validation" + ], + "evidence": "manual check at IOUAmount(Number const& other) (constructor)", + "issue_pattern": "Missing range validation for exponent_", + "why_false_positive": "manual check validates exponent_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "mantissa_, exponent_", + "empty", + "string", + "validation" + ], + "evidence": "Number::normalizeToRange (called in IOUAmount::fromNumber) at IOUAmount::fromNumber", + "issue_pattern": "Missing empty string validation for mantissa_, exponent_", + "why_false_positive": "Number::normalizeToRange (called in IOUAmount::fromNumber) validates mantissa_, exponent_ for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/IOUAmount.cpp", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "getStaticSTNumberSwitchover" + }, + { + "args": [], + "lineno": 22, + "name": "getSTNumberSwitchover" + }, + { + "args": [ + "v" + ], + "lineno": 26, + "name": "setSTNumberSwitchover" + }, + { + "args": [ + "number" + ], + "lineno": 36, + "name": "fromNumber" + }, + { + "args": [], + "lineno": 44, + "name": "minPositiveAmount" + }, + { + "args": [], + "lineno": 49, + "name": "normalize" + }, + { + "args": [ + "other" + ], + "lineno": 81, + "name": "IOUAmount" + }, + { + "args": [ + "other" + ], + "lineno": 85, + "name": "operator+=" + }, + { + "args": [ + "amount" + ], + "lineno": 112, + "name": "to_string" + }, + { + "args": [ + "amt", + "num", + "den", + "roundUp" + ], + "lineno": 116, + "name": "mulRatio" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by unit tests for IOUAmount and STAmount, typically found in files like IOUAmount_test.cpp, STAmount_test.cpp, or protocol/IOUAmount_test.cpp. These tests should cover normalization, overflow, underflow, zero handling, and arithmetic operations. However, edge cases such as extreme exponent/mantissa values, switchover logic, and exception paths (e.g., overflow errors) may not be exhaustively tested unless specifically targeted. Manual validation logic (e.g., mantissa_ >= -10 && mantissa_ <= 10 triggers zeroing) should be checked for test coverage. There may be gaps in tests for the getSTNumberSwitchover/setSTNumberSwitchover logic and for all exception-throwing branches.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "manual validation (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::overflow_error (via Throw)", + "field": "exponent_", + "location": "IOUAmount::normalize", + "validated_by": "manual check", + "validates": [ + "exponent_ must not exceed maxExponent" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets to zero)", + "field": "exponent_", + "location": "IOUAmount::normalize", + "validated_by": "manual check", + "validates": [ + "exponent_ below minExponent results in zeroing the amount" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets to zero)", + "field": "mantissa_", + "location": "IOUAmount::normalize", + "validated_by": "manual check", + "validates": [ + "mantissa_ below minMantissa results in zeroing the amount" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::overflow_error (via Throw)", + "field": "mantissa_", + "location": "IOUAmount::normalize", + "validated_by": "manual check", + "validates": [ + "mantissa_ above maxMantissa triggers normalization loop, and if exponent_ >= maxExponent, throws" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets to zero)", + "field": "mantissa_", + "location": "IOUAmount::normalize", + "validated_by": "manual check", + "validates": [ + "mantissa_ == 0 results in zeroing the amount" + ], + "validation_type": "type/business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::overflow_error (via Throw)", + "field": "exponent_", + "location": "IOUAmount(Number const& other) (constructor)", + "validated_by": "manual check", + "validates": [ + "exponent_ must not exceed maxExponent" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets to zero)", + "field": "exponent_", + "location": "IOUAmount(Number const& other) (constructor)", + "validated_by": "manual check", + "validates": [ + "exponent_ below minExponent results in zeroing the amount" + ], + "validation_type": "range" + }, + { + "confidence": 0.8, + "error_thrown": "depends on Number::normalizeToRange (not shown here)", + "field": "mantissa_, exponent_", + "location": "IOUAmount::fromNumber", + "validated_by": "Number::normalizeToRange (called in IOUAmount::fromNumber)", + "validates": [ + "mantissa_ and exponent_ are normalized to allowed ranges" + ], + "validation_type": "range/business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/IOUAmount.cpp.ai.md b/src/libxrpl/protocol/IOUAmount.cpp.ai.md new file mode 100644 index 0000000000..3b8b2d1c4a --- /dev/null +++ b/src/libxrpl/protocol/IOUAmount.cpp.ai.md @@ -0,0 +1,56 @@ +# `IOUAmount.cpp` — IOU Amount Arithmetic and Normalization + +`IOUAmount.cpp` is the implementation layer for the XRPL's IOU (non-native token) amount type. `IOUAmount` represents all non-XRP balances in the ledger — trust line amounts, offers, AMM pools — as a **signed floating-point value** with a 64-bit signed mantissa and an integer exponent. This file provides the normalization engine, construction from the higher-precision `Number` type, addition, and the ratio-multiplication primitive `mulRatio`, which is used wherever fee calculations, transfer rates, and AMM math must multiply an IOU by a rational fraction. + +## Representation and Invariants + +The format encodes a value as `mantissa × 10^exponent`. Non-zero amounts are kept in a canonical form where the absolute value of the mantissa lies in `[10^15, 10^16 − 1]` and the exponent lies in `[-96, 80]`. These constants are imported directly from `STAmount::cMinValue`, `cMaxValue`, `cMinOffset`, and `cMaxOffset`, locking `IOUAmount`'s precision to the on-wire serialization format of the ledger. + +Zero is a special case: its canonical representation uses `mantissa_ = 0` and `exponent_ = -100`. The exponent of -100 is deliberately below `minExponent` (-96) so that zero sorts less than any representable positive value — essential for correct ordering when zero and sub-minimum amounts appear together in sorted structures. + +## The STNumber Switchover + +A central design feature of this file is the `getSTNumberSwitchover()` / `setSTNumberSwitchover()` pair. This acts as a runtime flag that selects between two arithmetic backends: + +- **Legacy path** (switchover `false`): Normalization and addition use manual base-10 digit-shifting loops, preserving the original `STAmount` rounding behavior. +- **Number path** (switchover `true`, the default): Operations delegate to the `Number` class, which uses the newer, more precise rounding model required for AMM and multi-asset calculations. + +The flag is stored in a `LocalValue` — the XRPL coroutine-aware thread-local mechanism. Because the rippled server processes multiple transactions concurrently across coroutines, a global `bool` would cause race conditions. `LocalValue` isolates the value per-coroutine, so each transaction can apply its own amendment-governed arithmetic mode without interfering with others. The `NumberSO` RAII guard (defined in the header) sets the flag and restores the previous value on scope exit — the idiomatic way to activate and deactivate the feature for a single transaction. + +The static accessor `getStaticSTNumberSwitchover()` uses the function-local static idiom specifically to avoid C++ static initialization order problems that could corrupt the flag before first use. + +## `normalize()` — Canonical Form Enforcement + +`normalize()` is the enforcement point for the invariants above. On every construction from a raw `(mantissa, exponent)` pair (see the inline constructor in the header), `normalize()` is called. + +Under the legacy path it works as a digit-shifting loop: while the mantissa is below `minMantissa`, multiply by 10 and decrement the exponent; while it is above `maxMantissa`, divide by 10 and increment the exponent. If the exponent would exceed `maxExponent` during scale-down, the function throws `std::overflow_error`. If the mantissa cannot be scaled up to `minMantissa` without pushing the exponent below `minExponent`, the amount silently becomes zero (underflow is not an error in IOU arithmetic — a sub-minimum payment simply rounds to nothing). + +Under the switchover path, the round-trip through `Number::normalizeToRange` replaces those loops with a single well-tested precision-preserving step. The same overflow/underflow policy applies: the code re-checks the resulting exponent after `fromNumber()` and either throws or zeroes accordingly. + +## `fromNumber()` — Construction Without Circular Recursion + +The static `fromNumber()` factory exists because the `(mantissa, exponent)` constructor calls `normalize()`, and the switchover path inside `normalize()` calls `fromNumber()`. If `fromNumber()` used the public constructor, the system would recurse infinitely. Instead, `fromNumber()` constructs a default-initialized (zeroed) `IOUAmount` by direct field assignment, bypassing `normalize()` entirely, then lets `Number::normalizeToRange()` write the final mantissa and exponent into those fields. This is the only legal way to break the construction/normalization cycle. + +## `operator+=` — Addition with Exponent Alignment + +The legacy addition path implements standard floating-point addition on a decimal representation. It aligns the two operands to the same exponent by truncating digits from the smaller-magnitude value (dividing its mantissa by 10 and incrementing its exponent until the exponents match), then adds mantissas. After addition, the near-cancellation guard checks whether the result is in `[-10, 10]`; values this small cannot be normalized to the required mantissa range and are zeroed. Otherwise, `normalize()` canonicalizes the sum. + +The switchover path avoids this alignment logic entirely by round-tripping through `Number` arithmetic, which handles alignment and rounding internally with higher precision. + +## `mulRatio()` — Ratio Multiplication with 128-bit Intermediate Arithmetic + +`mulRatio(amt, num, den, roundUp)` computes `amt × num / den` while preserving as much precision as possible and applying controlled rounding. It is the building block for transfer-rate application, fee calculation, and AMM invariant adjustment. + +The challenge is intermediate overflow: a 64-bit mantissa multiplied by a 32-bit numerator produces a 96-bit product that overflows `int64_t`. The function uses `boost::multiprecision::uint128_t` throughout, accumulating the product and remainder in 128 bits before scaling back down. + +A precomputed static table `powerTable` holds powers of ten from `10^0` to `10^29`, initialized exactly once via a lambda on first call. `log10Floor` and `log10Ceil` are binary-search lambdas over this table. Using a lookup table rather than `std::log10` is deliberate: floating-point log functions can produce rounding errors that make an integer boundary ambiguous; a table lookup is exact. + +The precision-maximization step is the non-obvious core of `mulRatio`. After dividing the 128-bit product by `den`, there is a quotient `low` and a remainder `rem`. `rem / den` would be zero in integer arithmetic. Instead, the code calculates how much headroom exists before `low` would overflow a 64-bit mantissa (`roomToGrow = fl64 - log10Ceil(low)`), then scales both `low` and `rem` up by that many powers of ten — effectively recovering fractional digits that would otherwise be lost. The remainder term `addRem = rem / den128` is then large enough to contribute meaningful precision before being added back to `low`. + +Finally, if `mustShrink > 0` (the result is still too large for 64 bits after all scaling), the code divides `low` down and tracks whether any set bits were lost (setting `hasRem` for rounding purposes). + +Rounding is applied to the last representable mantissa digit: when `roundUp` and the amount is positive, incrementing `mantissa() + 1` by one ULP is safe because the mantissa is already normalized and cannot overflow the range. When `roundUp` is true but the result rounded down to zero (meaning the true value is between 0 and `minPositiveAmount()`), `minPositiveAmount()` is returned rather than zero — ensuring that a non-zero fee is never silently dropped. + +## Relationships + +`IOUAmount.cpp` depends on `Number` for both its conversion path and its to-string output (`to_string(IOUAmount)` simply delegates to `to_string(Number{amount})`). It imports boundary constants from `STAmount` but does not depend on `STAmount` for any logic — only for the shared numeric range specification. The `LocalValue` mechanism it uses for the switchover flag is the same coroutine-aware TLS used throughout the rippled server to avoid global mutable state in concurrent contexts. \ No newline at end of file diff --git a/src/libxrpl/protocol/Indexes.cpp.ai.json b/src/libxrpl/protocol/Indexes.cpp.ai.json new file mode 100644 index 0000000000..179b155f6e --- /dev/null +++ b/src/libxrpl/protocol/Indexes.cpp.ai.json @@ -0,0 +1,883 @@ +{ + "args": [ + { + "lineno": 54, + "name": "space" + }, + { + "lineno": 54, + "name": "args" + }, + { + "lineno": 59, + "name": "book" + }, + { + "lineno": 108, + "name": "uBase" + }, + { + "lineno": 114, + "name": "uBase" + }, + { + "lineno": 121, + "name": "account" + }, + { + "lineno": 121, + "name": "ticketSeq" + }, + { + "lineno": 126, + "name": "account" + }, + { + "lineno": 126, + "name": "ticketSeq" + }, + { + "lineno": 131, + "name": "sequence" + }, + { + "lineno": 131, + "name": "account" + }, + { + "lineno": 142, + "name": "id" + }, + { + "lineno": 147, + "name": "key" + }, + { + "lineno": 157, + "name": "ledger" + }, + { + "lineno": 179, + "name": "b" + }, + { + "lineno": 184, + "name": "id0" + }, + { + "lineno": 184, + "name": "id1" + }, + { + "lineno": 184, + "name": "currency" + }, + { + "lineno": 200, + "name": "id" + }, + { + "lineno": 200, + "name": "seq" + }, + { + "lineno": 205, + "name": "k" + }, + { + "lineno": 205, + "name": "q" + }, + { + "lineno": 220, + "name": "k" + }, + { + "lineno": 225, + "name": "id" + }, + { + "lineno": 225, + "name": "ticketSeq" + }, + { + "lineno": 230, + "name": "id" + }, + { + "lineno": 230, + "name": "ticketSeq" + }, + { + "lineno": 237, + "name": "account" + }, + { + "lineno": 237, + "name": "page" + }, + { + "lineno": 242, + "name": "account" + }, + { + "lineno": 246, + "name": "id" + }, + { + "lineno": 246, + "name": "seq" + }, + { + "lineno": 251, + "name": "owner" + }, + { + "lineno": 251, + "name": "preauthorized" + }, + { + "lineno": 256, + "name": "owner" + }, + { + "lineno": 256, + "name": "authCreds" + }, + { + "lineno": 267, + "name": "key" + }, + { + "lineno": 271, + "name": "id" + }, + { + "lineno": 275, + "name": "key" + }, + { + "lineno": 275, + "name": "index" + }, + { + "lineno": 282, + "name": "src" + }, + { + "lineno": 282, + "name": "seq" + }, + { + "lineno": 286, + "name": "src" + }, + { + "lineno": 286, + "name": "dst" + }, + { + "lineno": 286, + "name": "seq" + }, + { + "lineno": 291, + "name": "owner" + }, + { + "lineno": 297, + "name": "owner" + }, + { + "lineno": 303, + "name": "k" + }, + { + "lineno": 303, + "name": "token" + }, + { + "lineno": 308, + "name": "owner" + }, + { + "lineno": 308, + "name": "seq" + }, + { + "lineno": 312, + "name": "id" + }, + { + "lineno": 316, + "name": "id" + }, + { + "lineno": 320, + "name": "asset1" + }, + { + "lineno": 320, + "name": "asset2" + }, + { + "lineno": 347, + "name": "id" + }, + { + "lineno": 351, + "name": "account" + }, + { + "lineno": 351, + "name": "authorizedAccount" + }, + { + "lineno": 355, + "name": "bridge" + }, + { + "lineno": 355, + "name": "chainType" + }, + { + "lineno": 363, + "name": "bridge" + }, + { + "lineno": 363, + "name": "seq" + }, + { + "lineno": 373, + "name": "bridge" + }, + { + "lineno": 373, + "name": "seq" + }, + { + "lineno": 383, + "name": "account" + }, + { + "lineno": 387, + "name": "account" + }, + { + "lineno": 387, + "name": "documentID" + }, + { + "lineno": 391, + "name": "seq" + }, + { + "lineno": 391, + "name": "issuer" + }, + { + "lineno": 395, + "name": "issuanceID" + }, + { + "lineno": 399, + "name": "issuanceID" + }, + { + "lineno": 399, + "name": "holder" + }, + { + "lineno": 403, + "name": "issuanceKey" + }, + { + "lineno": 403, + "name": "holder" + }, + { + "lineno": 407, + "name": "subject" + }, + { + "lineno": 407, + "name": "issuer" + }, + { + "lineno": 407, + "name": "credType" + }, + { + "lineno": 411, + "name": "owner" + }, + { + "lineno": 411, + "name": "seq" + }, + { + "lineno": 415, + "name": "owner" + }, + { + "lineno": 415, + "name": "seq" + }, + { + "lineno": 419, + "name": "loanBrokerID" + }, + { + "lineno": 419, + "name": "loanSeq" + }, + { + "lineno": 423, + "name": "account" + }, + { + "lineno": 423, + "name": "seq" + }, + { + "lineno": 427, + "name": "domainID" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "getBookBase", + "XRPL_ASSERT(isConsistent(book), ...)", + "std::visit([&](...) {...}, book.in, book.out)", + "getIndexHash(...)", + "indexHash(...)", + "sha512Half(...)" + ], + "entry_point": "getBookBase(Book const& book)", + "purpose": "Computes the ledger index for a given Book object, ensuring the Book is consistent before hashing its fields to generate the index.", + "validation_points": [ + "XRPL_ASSERT(isConsistent(book), ...)" + ] + } + ], + "data_flows": [ + { + "field": "book (Book const&)", + "flow": [ + "Input to getBookBase", + "Validated by isConsistent(book) via XRPL_ASSERT", + "Fields accessed via std::visit (book.in, book.out)", + "Fields passed to getIndexHash and indexHash", + "Used in sha512Half to compute ledger index" + ], + "origin": "Input parameter to getBookBase (likely constructed elsewhere, e.g., from user input, ledger state, or test fixtures)", + "transformations": [ + "Checked for consistency (isConsistent)", + "Fields extracted (currency, account, domain, etc.)", + "Fields possibly transformed (e.g., getMptID for MPTIssue)", + "Fields serialized and hashed" + ], + "validated_at": "XRPL_ASSERT(isConsistent(book), ...) in getBookBase" + }, + { + "field": "book.domain", + "flow": [ + "Checked in getIndexHash lambda", + "If present, included as extra argument to indexHash" + ], + "origin": "Optional field in Book struct", + "transformations": [ + "Conditional inclusion in hash input" + ], + "validated_at": "Indirectly, as part of isConsistent(book)" + }, + { + "field": "book.in / book.out", + "flow": [ + "Accessed via std::visit in getBookBase", + "Depending on type, fields extracted (currency, account, getMptID)", + "Passed to getIndexHash and indexHash" + ], + "origin": "Fields of Book, variant types (Issue or MPTIssue)", + "transformations": [ + "Type-dispatched via std::visit", + "getMptID called if MPTIssue" + ], + "validated_at": "Indirectly, as part of isConsistent(book)" + } + ], + "description": "This file provides functions and utilities for generating and working with ledger indices and keylets in the XRPL protocol, including type-specific hashing, index calculation for various ledger objects, and keylet construction for different ledger entry types.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "book (Book const& book)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(isConsistent(book), ...) at getBookBase", + "issue_pattern": "Missing empty string validation for book (Book const& book)", + "why_false_positive": "XRPL_ASSERT(isConsistent(book), ...) validates book (Book const& book) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Indexes.cpp", + "functions": [ + { + "args": [ + "space", + "args..." + ], + "lineno": 54, + "name": "indexHash" + }, + { + "args": [ + "book" + ], + "lineno": 59, + "name": "getBookBase" + }, + { + "args": [ + "uBase" + ], + "lineno": 108, + "name": "getQualityNext" + }, + { + "args": [ + "uBase" + ], + "lineno": 114, + "name": "getQuality" + }, + { + "args": [ + "account", + "ticketSeq" + ], + "lineno": 121, + "name": "getTicketIndex" + }, + { + "args": [ + "account", + "ticketSeq" + ], + "lineno": 126, + "name": "getTicketIndex" + }, + { + "args": [ + "sequence", + "account" + ], + "lineno": 131, + "name": "makeMptID" + }, + { + "args": [ + "id" + ], + "lineno": 142, + "name": "account" + }, + { + "args": [ + "key" + ], + "lineno": 147, + "name": "child" + }, + { + "args": [], + "lineno": 152, + "name": "skip" + }, + { + "args": [ + "ledger" + ], + "lineno": 157, + "name": "skip" + }, + { + "args": [], + "lineno": 164, + "name": "amendments" + }, + { + "args": [], + "lineno": 169, + "name": "fees" + }, + { + "args": [], + "lineno": 174, + "name": "negativeUNL" + }, + { + "args": [ + "b" + ], + "lineno": 179, + "name": "operator()" + }, + { + "args": [ + "id0", + "id1", + "currency" + ], + "lineno": 184, + "name": "line" + }, + { + "args": [ + "id", + "seq" + ], + "lineno": 200, + "name": "offer" + }, + { + "args": [ + "k", + "q" + ], + "lineno": 205, + "name": "quality" + }, + { + "args": [ + "k" + ], + "lineno": 220, + "name": "operator()" + }, + { + "args": [ + "id", + "ticketSeq" + ], + "lineno": 225, + "name": "operator()" + }, + { + "args": [ + "id", + "ticketSeq" + ], + "lineno": 230, + "name": "operator()" + }, + { + "args": [ + "account", + "page" + ], + "lineno": 237, + "name": "signers" + }, + { + "args": [ + "account" + ], + "lineno": 242, + "name": "signers" + }, + { + "args": [ + "id", + "seq" + ], + "lineno": 246, + "name": "check" + }, + { + "args": [ + "owner", + "preauthorized" + ], + "lineno": 251, + "name": "depositPreauth" + }, + { + "args": [ + "owner", + "authCreds" + ], + "lineno": 256, + "name": "depositPreauth" + }, + { + "args": [ + "key" + ], + "lineno": 267, + "name": "unchecked" + }, + { + "args": [ + "id" + ], + "lineno": 271, + "name": "ownerDir" + }, + { + "args": [ + "key", + "index" + ], + "lineno": 275, + "name": "page" + }, + { + "args": [ + "src", + "seq" + ], + "lineno": 282, + "name": "escrow" + }, + { + "args": [ + "src", + "dst", + "seq" + ], + "lineno": 286, + "name": "payChan" + }, + { + "args": [ + "owner" + ], + "lineno": 291, + "name": "nftpage_min" + }, + { + "args": [ + "owner" + ], + "lineno": 297, + "name": "nftpage_max" + }, + { + "args": [ + "k", + "token" + ], + "lineno": 303, + "name": "nftpage" + }, + { + "args": [ + "owner", + "seq" + ], + "lineno": 308, + "name": "nftoffer" + }, + { + "args": [ + "id" + ], + "lineno": 312, + "name": "nft_buys" + }, + { + "args": [ + "id" + ], + "lineno": 316, + "name": "nft_sells" + }, + { + "args": [ + "asset1", + "asset2" + ], + "lineno": 320, + "name": "amm" + }, + { + "args": [ + "id" + ], + "lineno": 347, + "name": "amm" + }, + { + "args": [ + "account", + "authorizedAccount" + ], + "lineno": 351, + "name": "delegate" + }, + { + "args": [ + "bridge", + "chainType" + ], + "lineno": 355, + "name": "bridge" + }, + { + "args": [ + "bridge", + "seq" + ], + "lineno": 363, + "name": "xChainClaimID" + }, + { + "args": [ + "bridge", + "seq" + ], + "lineno": 373, + "name": "xChainCreateAccountClaimID" + }, + { + "args": [ + "account" + ], + "lineno": 383, + "name": "did" + }, + { + "args": [ + "account", + "documentID" + ], + "lineno": 387, + "name": "oracle" + }, + { + "args": [ + "seq", + "issuer" + ], + "lineno": 391, + "name": "mptIssuance" + }, + { + "args": [ + "issuanceID" + ], + "lineno": 395, + "name": "mptIssuance" + }, + { + "args": [ + "issuanceID", + "holder" + ], + "lineno": 399, + "name": "mptoken" + }, + { + "args": [ + "issuanceKey", + "holder" + ], + "lineno": 403, + "name": "mptoken" + }, + { + "args": [ + "subject", + "issuer", + "credType" + ], + "lineno": 407, + "name": "credential" + }, + { + "args": [ + "owner", + "seq" + ], + "lineno": 411, + "name": "vault" + }, + { + "args": [ + "owner", + "seq" + ], + "lineno": 415, + "name": "loanbroker" + }, + { + "args": [ + "loanBrokerID", + "loanSeq" + ], + "lineno": 419, + "name": "loan" + }, + { + "args": [ + "account", + "seq" + ], + "lineno": 423, + "name": "permissionedDomain" + }, + { + "args": [ + "domainID" + ], + "lineno": 427, + "name": "permissionedDomain" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + }, + { + "lineno": 139, + "name": "keylet" + } + ], + "test_coverage_notes": "The validation path (XRPL_ASSERT(isConsistent(book), ...)) is only triggered when getBookBase is called. Test coverage depends on whether tests call getBookBase with both valid and invalid Book objects. Typical test files would be in the unit test suite for protocol/Indexes.cpp or protocol/Book.cpp, possibly named Indexes_test.cpp, Book_test.cpp, or similar. If tests do not explicitly check for assertion failures on inconsistent Book objects, the negative validation path may not be covered. There is no evidence in this file of test hooks or test-only code, so coverage must be checked in the test suite. Gaps may exist if edge cases for Book consistency are not tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro, isConsistent function", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "book (Book const& book)", + "location": "getBookBase", + "validated_by": "XRPL_ASSERT(isConsistent(book), ...)", + "validates": [ + "Checks that the input Book object is consistent via isConsistent(book)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Indexes.cpp.ai.md b/src/libxrpl/protocol/Indexes.cpp.ai.md new file mode 100644 index 0000000000..d83cb84c54 --- /dev/null +++ b/src/libxrpl/protocol/Indexes.cpp.ai.md @@ -0,0 +1,106 @@ +# `src/libxrpl/protocol/Indexes.cpp` + +## Role and Purpose + +Every object stored in the XRPL ledger state map is addressed by a 256-bit key. This file is the single authoritative source for computing those keys. It implements the complete set of keylet factory functions and the lower-level index utilities that the rest of the codebase uses to locate any ledger entry — from account roots and trust lines to NFT pages, cross-chain bridges, and automated market makers. + +## Tagged Hashing: The `LedgerNameSpace` Enum + +The foundation of the entire file is `LedgerNameSpace`, a `uint16_t`-backed enum that assigns a unique single-character discriminator to each ledger object type. Every index is derived from a `sha512Half` over the namespace tag prepended to the object's parameters: + +```cpp +template +static uint256 +indexHash(LedgerNameSpace space, Args const&... args) +{ + return sha512Half(safe_cast(space), args...); +} +``` + +This is "tagged hashing" — the same technique used elsewhere in cryptographic protocols to prevent cross-context collisions. Without the discriminator, two different object types sharing the same parameters (e.g., an offer and an escrow both keyed by account + sequence) could accidentally land on the same ledger key. The namespace prefix makes every hash domain-separated by construction. + +The `LedgerNameSpace` values are permanent protocol constants. The file comment is explicit: changing an existing value would make every on-ledger object of that type unaddressable without a coordinated migration. Three legacy entries (`CONTRACT`, `GENERATOR`, `NICKNAME`) are kept as `[[deprecated]]` not for backward compatibility, but to reserve their character codes so they are never accidentally reused for a new object type. + +## The `Keylet` Abstraction + +The public API returns `Keylet` values — a simple struct pairing a `uint256` key with a `LedgerEntryType`. This type-tagged key serves as a typed handle: any code that fetches a ledger entry using a `Keylet` knows statically what kind of object it expects, and `Keylet::check()` can validate the actual type at runtime. The pattern prevents bugs where, say, a function looking up a signer list accidentally accepts an account root that happens to sit at the same address in some edge case. Most factory functions in the `keylet::` namespace return owned `Keylet` values; singleton objects return `Keylet const&` to a static local computed exactly once. + +## Symmetric Object Identification + +Several ledger objects are intrinsically symmetric between two parties, and the key derivation must be order-independent. Two design patterns handle this. + +**Trust lines** (`keylet::line`) use `std::minmax` to sort the two account IDs before hashing. A trust line between Alice and Bob is the same on-ledger object regardless of which side you query from: + +```cpp +auto const accounts = std::minmax(id0, id1); +return {ltRIPPLE_STATE, + indexHash(LedgerNameSpace::TRUST_LINE, + accounts.first, accounts.second, currency)}; +``` + +**AMM pools** (`keylet::amm`) apply the same technique with `std::minmax` on the two `Asset` values, then dispatch on all four combinations of `Issue`/`MPTIssue` pairs through `if constexpr` branches inside a `std::visit`. This compile-time dispatch avoids virtual dispatch while handling the heterogeneous token type combinations introduced with multi-purpose tokens (MPTs). + +## Order Book Quality Encoding + +Order books in the XRPL ledger are implemented as sorted directories. Rather than store price data separately, the quality (exchange rate) of a directory page is embedded directly in the last 8 bytes of the key: + +```cpp +Keylet +quality(Keylet const& k, std::uint64_t q) noexcept +{ + uint256 x = k.key; + ((std::uint64_t*)x.end())[-1] = boost::endian::native_to_big(q); + return {ltDIR_NODE, x}; +} +``` + +The big-endian encoding is deliberate: since `uint256` values are stored and compared in memory order (most significant byte first), incrementing the embedded quality field via `getQualityNext` advances to the next order book page in natural sort order. `getQuality()` recovers the 64-bit value from the same position using `boost::endian::big_to_native`. The code itself acknowledges the ugliness of the raw pointer arithmetic (`// FIXME This is ugly`), but correctness relies on `base_uint`'s internal big-endian byte layout. + +## NFT Page Addressing: Composite Keys Without Hashing + +NFT token page identifiers break the pattern of every other object type. Instead of hashing inputs, the 256-bit key is constructed directly as a composite value: + +- The high 160 bits hold the owner's `AccountID`. +- The low 96 bits hold a range tag derived from a specific NFToken ID masked by `nft::pageMask`. + +```cpp +Keylet nftpage_min(AccountID const& owner) +{ + std::array buf{}; + std::memcpy(buf.data(), owner.data(), owner.size()); + return {ltNFTOKEN_PAGE, uint256{buf}}; +} + +Keylet nftpage(Keylet const& k, uint256 const& token) +{ + return {ltNFTOKEN_PAGE, (k.key & ~nft::pageMask) + (token & nft::pageMask)}; +} +``` + +This design gives all of a given owner's NFT pages a contiguous range in the SHAMap, bounded by `nftpage_min` (low 96 bits all zero) and `nftpage_max` (low 96 bits all one). Traversing an owner's NFT collection is therefore a bounded range scan rather than a linked-list walk. No two owners' page ranges can overlap because the high 160 bits uniquely identify the owner. + +## Singleton Keylets + +The `amendments()`, `fees()`, `negativeUNL()`, and no-arg `skip()` functions return `Keylet const&` to a function-local static. These objects are globally unique in any ledger — they take no differentiating parameters — so computing their hash once is correct and efficient. The pattern also serves as self-documenting intent: the `const&` return type signals to callers that they are retrieving a well-known fixed address, not constructing a new one. + +## Multi-Purpose Token (MPT) Identifiers + +`makeMptID` constructs the 192-bit `MPTID` for a token issuance by packing a big-endian sequence number into the first 4 bytes and an `AccountID` into the next 20 bytes. The explicit `native_to_big` conversion ensures the composite identifier has a canonical byte order regardless of the host platform. The `mptIssuance` and `mptoken` keylets build on this, with `mptoken` accepting either a raw `MPTID` or the pre-computed 256-bit issuance key to avoid redundant hashing when the issuance key is already available. + +## DepositPreauth with Credentials + +The credential-set overload of `depositPreauth` handles a variable number of (issuer, credentialType) pairs: + +```cpp +std::vector hashes; +for (auto const& o : authCreds) + hashes.emplace_back(sha512Half(o.first, o.second)); +return {ltDEPOSIT_PREAUTH, + indexHash(LedgerNameSpace::DEPOSIT_PREAUTH_CREDENTIALS, owner, hashes)}; +``` + +Each credential is hashed individually first. The resulting hashes are then passed as a vector to the outer `indexHash`. Because the caller is required to pass a `std::set` (which provides deterministic sorted order), the final hash is stable regardless of insertion order. A distinct namespace (`DEPOSIT_PREAUTH_CREDENTIALS` vs `DEPOSIT_PREAUTH`) ensures single-account and credential-set preauth objects occupy separate key spaces and cannot collide even with an identical owner. + +## Signer List Pagination + +The paginated form `signers(account, page)` is kept `static` — private to the translation unit — because only page 0 is ever allocated. The comment explicitly notes this as an architectural reservation: if signer list pagination is someday needed, the infrastructure to derive per-page keylets is already present, but the interface is intentionally not exposed until the feature exists. \ No newline at end of file diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp.ai.json b/src/libxrpl/protocol/InnerObjectFormats.cpp.ai.json new file mode 100644 index 0000000000..2173ddbd6f --- /dev/null +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp.ai.json @@ -0,0 +1,886 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "STObject::makeInnerObject", + "InnerObjectFormats::getInstance", + "InnerObjectFormats::findSOTemplateBySField", + "SOTemplate::add (during construction)", + "SOTemplate::validate (implied, not shown here)" + ], + "entry_point": "STObject::makeInnerObject", + "purpose": "Constructs an inner STObject (e.g., SignerEntry, Majority, etc.) and validates its fields against the template defined in InnerObjectFormats.", + "validation_points": [ + "SOTemplate::validate (called during or after STObject construction to ensure required/optional/default fields are present and correct)" + ] + }, + { + "call_chain": [ + "STObject::deserialize", + "InnerObjectFormats::getInstance", + "InnerObjectFormats::findSOTemplateBySField", + "SOTemplate::validate" + ], + "entry_point": "Deserialization of inner objects (e.g., from JSON or binary)", + "purpose": "When an inner object is parsed from input (JSON/binary), its fields are validated against the template.", + "validation_points": [ + "SOTemplate::validate" + ] + } + ], + "data_flows": [ + { + "field": "sfAccount", + "flow": [ + "Input (e.g., JSON field 'Account')", + "STObject field population", + "STObject::makeInnerObject or deserialization", + "InnerObjectFormats::findSOTemplateBySField", + "SOTemplate (template for object type)", + "SOTemplate::validate (checks presence/format of sfAccount)" + ], + "origin": "Input data (JSON, binary, or constructed in code)", + "transformations": [ + "Parsed from input", + "Checked for presence and type", + "Possibly normalized (e.g., address format)" + ], + "validated_at": "SOTemplate::validate" + }, + { + "field": "sfSignerWeight", + "flow": [ + "Input", + "STObject field", + "STObject::makeInnerObject", + "InnerObjectFormats", + "SOTemplate", + "SOTemplate::validate" + ], + "origin": "Input data (e.g., SignerEntry JSON)", + "transformations": [ + "Parsed as integer", + "Checked for required presence" + ], + "validated_at": "SOTemplate::validate" + }, + { + "field": "sfSigningPubKey", + "flow": [ + "Input", + "STObject field", + "STObject::makeInnerObject", + "InnerObjectFormats", + "SOTemplate", + "SOTemplate::validate" + ], + "origin": "Input data (e.g., Signer JSON)", + "transformations": [ + "Parsed as blob", + "Checked for required presence" + ], + "validated_at": "SOTemplate::validate" + }, + { + "field": "sfTxnSignature", + "flow": [ + "Input", + "STObject field", + "STObject::makeInnerObject", + "InnerObjectFormats", + "SOTemplate", + "SOTemplate::validate" + ], + "origin": "Input data (e.g., Signer JSON)", + "transformations": [ + "Parsed as blob", + "Checked for required presence" + ], + "validated_at": "SOTemplate::validate" + }, + { + "field": "sfWalletLocator", + "flow": [ + "Input", + "STObject field", + "STObject::makeInnerObject", + "InnerObjectFormats", + "SOTemplate", + "SOTemplate::validate" + ], + "origin": "Input data (e.g., SignerEntry JSON)", + "transformations": [ + "Parsed as blob", + "Checked for optional presence" + ], + "validated_at": "SOTemplate::validate" + } + ], + "description": "Defines and initializes the InnerObjectFormats class, which manages the formats and templates for various inner objects used in the XRPL protocol.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "All fields listed in each add() call, with validation type determined by soeREQUIRED, soeOPTIONAL, soeDEFAULT", + "validation", + "missing", + "check" + ], + "evidence": "Field All fields listed in each add() call, with validation type determined by soeREQUIRED, soeOPTIONAL, soeDEFAULT validated by SOTemplate (xrpl/protocol/SOTemplate.h)", + "issue_pattern": "Missing validation for All fields listed in each add() call, with validation type determined by soeREQUIRED, soeOPTIONAL, soeDEFAULT", + "why_false_positive": "SOTemplate (xrpl/protocol/SOTemplate.h) validates All fields listed in each add() call, with validation type determined by soeREQUIRED, soeOPTIONAL, soeDEFAULT automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "SOTemplate/add validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSignerWeight", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfSignerWeight", + "why_false_positive": "SOTemplate/add validates sfSignerWeight for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfWalletLocator", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfWalletLocator", + "why_false_positive": "SOTemplate/add validates sfWalletLocator for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSigningPubKey", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfSigningPubKey", + "why_false_positive": "SOTemplate/add validates sfSigningPubKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTxnSignature", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfTxnSignature", + "why_false_positive": "SOTemplate/add validates sfTxnSignature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmendment", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfAmendment", + "why_false_positive": "SOTemplate/add validates sfAmendment for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCloseTime", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfCloseTime", + "why_false_positive": "SOTemplate/add validates sfCloseTime for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfPublicKey", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfPublicKey", + "why_false_positive": "SOTemplate/add validates sfPublicKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfFirstLedgerSequence", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfFirstLedgerSequence", + "why_false_positive": "SOTemplate/add validates sfFirstLedgerSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfNFTokenID", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfNFTokenID", + "why_false_positive": "SOTemplate/add validates sfNFTokenID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfURI", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfURI", + "why_false_positive": "SOTemplate/add validates sfURI for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTradingFee", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfTradingFee", + "why_false_positive": "SOTemplate/add validates sfTradingFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfVoteWeight", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfVoteWeight", + "why_false_positive": "SOTemplate/add validates sfVoteWeight for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfExpiration", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfExpiration", + "why_false_positive": "SOTemplate/add validates sfExpiration for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDiscountedFee", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfDiscountedFee", + "why_false_positive": "SOTemplate/add validates sfDiscountedFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfPrice", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfPrice", + "why_false_positive": "SOTemplate/add validates sfPrice for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAuthAccounts", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfAuthAccounts", + "why_false_positive": "SOTemplate/add validates sfAuthAccounts for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAttestationSignerAccount", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfAttestationSignerAccount", + "why_false_positive": "SOTemplate/add validates sfAttestationSignerAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSignature", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfSignature", + "why_false_positive": "SOTemplate/add validates sfSignature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "SOTemplate/add validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAttestationRewardAccount", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfAttestationRewardAccount", + "why_false_positive": "SOTemplate/add validates sfAttestationRewardAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfWasLockingChainSend", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfWasLockingChainSend", + "why_false_positive": "SOTemplate/add validates sfWasLockingChainSend for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfXChainClaimID", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfXChainClaimID", + "why_false_positive": "SOTemplate/add validates sfXChainClaimID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfDestination", + "why_false_positive": "SOTemplate/add validates sfDestination for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfXChainAccountCreateCount", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfXChainAccountCreateCount", + "why_false_positive": "SOTemplate/add validates sfXChainAccountCreateCount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSignatureReward", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add at InnerObjectFormats::InnerObjectFormats", + "issue_pattern": "Missing empty string validation for sfSignatureReward", + "why_false_positive": "SOTemplate/add validates sfSignatureReward for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/InnerObjectFormats.cpp", + "functions": [ + { + "args": [], + "lineno": 6, + "name": "InnerObjectFormats" + }, + { + "args": [], + "lineno": 109, + "name": "getInstance" + }, + { + "args": [ + "sField" + ], + "lineno": 116, + "name": "findSOTemplateBySField" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage for InnerObjectFormats is typically indirect. Tests for transaction processing, serialization/deserialization, and validation (e.g., SignerLists, Amendments, NFTokens, XChain objects) will exercise these code paths. Look for tests in files like 'STObject_test.cpp', 'InnerObjectFormats_test.cpp', 'SignerListSet_test.cpp', 'AmendmentTable_test.cpp', 'XChainBridge_test.cpp', and transaction validation tests. Gaps may exist if there are no explicit tests for malformed or missing fields in inner objects, or if new inner object types are added without corresponding tests. Direct unit tests for InnerObjectFormats or SOTemplate validation logic may be limited.", + "validation_architecture": { + "auto_validated_fields": [ + "All fields listed in each add() call, with validation type determined by soeREQUIRED, soeOPTIONAL, soeDEFAULT" + ], + "framework": "SOTemplate (xrpl/protocol/SOTemplate.h)", + "validation_layer": "business_logic (object construction/registration)" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field (implementation-dependent, e.g., std::runtime_error or custom exception)", + "field": "sfAccount", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfSignerWeight", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "none if missing", + "field": "sfWalletLocator", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field may be omitted" + ], + "validation_type": "optional field (presence not required)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfSigningPubKey", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfTxnSignature", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfAmendment", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfCloseTime", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfPublicKey", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfFirstLedgerSequence", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfNFTokenID", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "none if missing", + "field": "sfURI", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field may be omitted" + ], + "validation_type": "optional field (presence not required)" + }, + { + "confidence": 0.9, + "error_thrown": "none if missing", + "field": "sfTradingFee", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field may be omitted, default value used" + ], + "validation_type": "default field (may be omitted, defaulted)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfVoteWeight", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfExpiration", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "none if missing", + "field": "sfDiscountedFee", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field may be omitted, default value used" + ], + "validation_type": "default field (may be omitted, defaulted)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfPrice", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "none if missing", + "field": "sfAuthAccounts", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field may be omitted" + ], + "validation_type": "optional field (presence not required)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfAttestationSignerAccount", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfSignature", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfAmount", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfAttestationRewardAccount", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfWasLockingChainSend", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfXChainClaimID", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "none if missing", + "field": "sfDestination", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field may be omitted" + ], + "validation_type": "optional field (presence not required)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfXChainAccountCreateCount", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field", + "field": "sfSignatureReward", + "location": "InnerObjectFormats::InnerObjectFormats", + "validated_by": "SOTemplate/add", + "validates": [ + "field is present in object" + ], + "validation_type": "presence (required field)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp.ai.md b/src/libxrpl/protocol/InnerObjectFormats.cpp.ai.md new file mode 100644 index 0000000000..79e65590d7 --- /dev/null +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp.ai.md @@ -0,0 +1,50 @@ +# `InnerObjectFormats.cpp` — Inner Object Schema Registry + +This file contains the sole definition of `InnerObjectFormats`, a singleton registry that declares the canonical field schemas for every structured sub-object that can appear inside an XRPL serialized object (`STObject`). It is the inner-object counterpart to `TxFormats` and `LedgerFormats` — those registries govern top-level transaction and ledger-entry shapes, while `InnerObjectFormats` governs the nested objects that those top-level structures embed. + +## Role in the Serialization System + +XRPL's serialized object model is built around `STObject`, a heterogeneous field container. When an `STObject` carries a known "inner object" sub-field (e.g. a `SignerEntry` inside a signer list, or an `NFToken` inside a page), the runtime needs a declared template to enforce which child fields are required, which are optional, and which default. `InnerObjectFormats` is that declaration point. + +The class inherits from the CRTP base `KnownFormats`. That base template manages a `std::forward_list` — chosen deliberately because `Item` objects must have stable addresses after insertion (they are referred to by pointer from two `boost::container::flat_map` indexes, one keyed by string name and one by integer type code). Each `Item` wraps an immutable `SOTemplate`, the name, and the type code derived from the corresponding global `SField` object via `getCode()`. + +## Construction and the `add()` Pattern + +The private constructor is the entire registration mechanism. Each call to `add()` supplies: + +1. `sField.jsonName` — the human-readable name used in JSON serialization and debug output. +2. `sField.getCode()` — the compact integer that uniquely identifies the field type. Using the field's own code as the key means the same `SField` that names the outer wrapper is also the lookup key; no separate enum is needed. +3. An initializer list of `SOElement` pairs `{sfFieldRef, style}` specifying child fields as `soeREQUIRED`, `soeOPTIONAL`, or `soeDEFAULT`. + +The choice of `int` as `KeyType` rather than a dedicated enum is intentional: inner objects don't form their own transaction-type space, so using an integral field code avoids coupling the format registry to a separate enumeration and reuses the existing `SField` identity mechanism. + +## `soeDEFAULT` vs `soeOPTIONAL` — A Subtle Distinction + +`soeREQUIRED` and `soeOPTIONAL` behave intuitively. `soeDEFAULT` is more subtle: a field marked `soeDEFAULT` *may* be absent in serialized form, but if it *is* present, it must **not** carry its default value. The serializer omits default-valued fields to keep the wire encoding compact. Both `sfTradingFee` in `sfVoteEntry` and `sfDiscountedFee` in `sfAuctionSlot` and `sfScale` in `sfPriceData` use `soeDEFAULT` for exactly this reason — a zero fee or zero scale is semantically the absence of that field, not an explicit zero. + +## Registered Inner Objects and Their Feature Context + +The constructor registers sixteen inner object types spanning the full breadth of XRPL features: + +**Core protocol objects**: `sfSignerEntry` (account + weight + optional locator for signer lists) and `sfSigner` (the cryptographic signature wrapper inside a multi-signed transaction) were among the earliest inner objects. `sfMajority` tracks amendment vote status — which amendment hash reached quorum and at what ledger close time. `sfDisabledValidator` records a validator that has been excluded from the UNL. + +**NFT support**: `sfNFToken` carries a token ID and optional URI, appearing in `NFTokenPage` ledger objects. + +**AMM (Automated Market Maker)**: `sfVoteEntry` and `sfAuctionSlot` represent AMM governance objects. These two received their templates in the *first* amendment wave (`fixInnerObjTemplate`) before the remaining inner objects were covered by `fixInnerObjTemplate2`. This phased rollout is explicitly called out in `STObject::makeInnerObject()`. + +**Cross-chain bridge attestations**: Four objects (`sfXChainClaimAttestationCollectionElement`, `sfXChainCreateAccountAttestationCollectionElement`, `sfXChainClaimProofSig`, `sfXChainCreateAccountProofSig`) encode witness signatures for XRPL's cross-chain bridge protocol. The "collection element" variants include a `sfSignature` field (the raw attestation) while the "proof sig" variants do not — they represent the condensed proof submitted to finalize a claim, where the signature has already been verified. + +**Newer additions**: `sfAuthAccount` (a single-account authorization entry used in AMM and other features), `sfPriceData` (an oracle price entry with asset pair, optional price, and scale), `sfCredential` (issuer + credential type for identity attestation), `sfPermission` (a single permission value in a delegated-permission list), `sfBatchSigner` (a signer in a batch transaction with optional multi-sig via nested `sfSigners`), `sfBook` (order-book reference with directory and node), and `sfCounterpartySignature` (a flexible optional-key structure supporting both single and multi-signature counterparty authorization). + +## Lookup and Template Application + +`getInstance()` returns a Meyer's singleton — the static local is initialized exactly once and is then immutable for the lifetime of the process. This is safe because `InnerObjectFormats` is not copyable (the `KnownFormats` base deletes copy constructor and assignment). + +`findSOTemplateBySField()` is the single external query point. It delegates to `findByType(sField.getCode())` and, if an `Item` is found, returns a pointer to its `SOTemplate`. Callers receive `nullptr` for any `SField` not in the registry. + +The two callers in `STObject.cpp` illustrate the two usage paths: + +- `STObject::makeInnerObject()` uses the template to initialize a freshly-constructed inner object, but only when the appropriate amendment rules (`fixInnerObjTemplate` or `fixInnerObjTemplate2`) are enabled — enforcing that template validation is applied consistently with the network's current rule set. +- `STObject::applyTemplateFromSField()` applies the template after deserialization, ensuring that binary data read from the network or ledger conforms to the declared schema before further processing. + +In both paths, the `SOTemplate` drives `STObject`'s field-presence validation: required fields that are absent, or unknown fields that appear, result in errors surfaced at object construction or deserialization time — making `InnerObjectFormats` a key enforcement layer in XRPL's type safety story. \ No newline at end of file diff --git a/src/libxrpl/protocol/Issue.cpp.ai.json b/src/libxrpl/protocol/Issue.cpp.ai.json new file mode 100644 index 0000000000..91d877a44a --- /dev/null +++ b/src/libxrpl/protocol/Issue.cpp.ai.json @@ -0,0 +1,273 @@ +{ + "args": [ + { + "lineno": 31, + "name": "jv" + }, + { + "lineno": 47, + "name": "ac" + }, + { + "lineno": 59, + "name": "is" + }, + { + "lineno": 64, + "name": "v" + }, + { + "lineno": 108, + "name": "os" + }, + { + "lineno": 108, + "name": "x" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "issueFromJson", + "to_currency", + "isXRP", + "parseBase58" + ], + "entry_point": "issueFromJson", + "purpose": "Parses a JSON object into an Issue, validating currency and issuer fields.", + "validation_points": [ + "issueFromJson: checks v.isObject()", + "issueFromJson: checks for mpt_issuance_id", + "issueFromJson: checks currency is string", + "issueFromJson: validates currency value", + "issueFromJson: checks issuer presence/absence for XRP", + "issueFromJson: checks issuer is string", + "issueFromJson: validates issuer via parseBase58" + ] + }, + { + "call_chain": [ + "to_json", + "Issue::setJson" + ], + "entry_point": "to_json", + "purpose": "Serializes an Issue to JSON, including currency and issuer if not XRP.", + "validation_points": [] + }, + { + "call_chain": [ + "to_string", + "isXRP", + "to_string (currency/account)" + ], + "entry_point": "to_string", + "purpose": "Converts Issue to string representation.", + "validation_points": [] + }, + { + "call_chain": [ + "isConsistent", + "isXRP" + ], + "entry_point": "isConsistent", + "purpose": "Checks if Issue's currency/account XRP-ness matches.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "currency", + "flow": [ + "JSON input", + "curStr = v[jss::currency]", + "to_currency(curStr.asString())", + "currency variable", + "Issue{currency, ...}" + ], + "origin": "Json::Value v[jss::currency] in issueFromJson", + "transformations": [ + "Extracted as string from JSON", + "Converted to Currency type via to_currency" + ], + "validated_at": "issueFromJson: checks string type and valid currency" + }, + { + "field": "issuer/account", + "flow": [ + "JSON input", + "issStr = v[jss::issuer]", + "parseBase58(issStr.asString())", + "issuer variable", + "Issue{..., issuer}" + ], + "origin": "Json::Value v[jss::issuer] in issueFromJson", + "transformations": [ + "Extracted as string from JSON", + "Parsed to AccountID via parseBase58" + ], + "validated_at": "issueFromJson: checks string type and valid account" + }, + { + "field": "Issue object", + "flow": [ + "Validated currency and issuer", + "Issue{currency, issuer}", + "Returned to caller" + ], + "origin": "Constructed in issueFromJson", + "transformations": [ + "Constructed from validated fields" + ], + "validated_at": "issueFromJson: after all field validations" + }, + { + "field": "Json::Value output", + "flow": [ + "Issue object", + "is.setJson(jv)", + "jv[jss::currency] = to_string(currency)", + "if not XRP: jv[jss::issuer] = toBase58(account)", + "return jv" + ], + "origin": "to_json(Issue const& is)", + "transformations": [ + "Fields converted to string/base58 for JSON" + ], + "validated_at": "No validation in to_json/setJson; assumes Issue is valid" + } + ], + "description": "Implements the xrpl::Issue class and related functions for handling currency/issuer pairs in the XRP Ledger, including serialization, JSON conversion, and consistency checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Issue.cpp", + "functions": [ + { + "args": [], + "lineno": 11, + "name": "Issue::getText" + }, + { + "args": [ + "jv" + ], + "lineno": 31, + "name": "Issue::setJson" + }, + { + "args": [], + "lineno": 39, + "name": "Issue::native" + }, + { + "args": [], + "lineno": 43, + "name": "Issue::integral" + }, + { + "args": [ + "ac" + ], + "lineno": 47, + "name": "isConsistent" + }, + { + "args": [ + "ac" + ], + "lineno": 52, + "name": "to_string" + }, + { + "args": [ + "is" + ], + "lineno": 59, + "name": "to_json" + }, + { + "args": [ + "v" + ], + "lineno": 64, + "name": "issueFromJson" + }, + { + "args": [ + "os", + "x" + ], + "lineno": 108, + "name": "operator<<" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this code would be in protocol/Issue_test.cpp or similar files. Tests should cover: valid/invalid JSON input to issueFromJson (missing fields, wrong types, invalid currency/account, XRP with issuer, etc.), serialization via to_json/setJson, and string conversion. Gaps may exist if tests do not cover all error branches (e.g., mpt_issuance_id presence, badCurrency, noCurrency, issuer not string, issuer parse failure). No direct evidence of test files in this snippet; coverage depends on the presence and thoroughness of protocol/Issue_test.cpp or related test suites.", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Issue.cpp.ai.md b/src/libxrpl/protocol/Issue.cpp.ai.md new file mode 100644 index 0000000000..4471efe6ea --- /dev/null +++ b/src/libxrpl/protocol/Issue.cpp.ai.md @@ -0,0 +1,41 @@ +# `Issue.cpp` — Currency/Issuer Pair Implementation + +## Role in the System + +`Issue.cpp` implements the `Issue` class, which is the fundamental representation of a fungible asset on the XRP Ledger that isn't a Multi-Purpose Token (MPT). An `Issue` encodes exactly two things: a `Currency` (a 160-bit tagged hash, defined in `UintTypes.h`) and an `AccountID` (the issuing account). Every IOU balance, offer, and trust line in the ledger is denominated in some `Issue`. XRP itself is also modeled as a special-case `Issue` whose currency and account fields are both the zero/XRP sentinel values. + +`Issue` sits at the centre of the protocol type hierarchy. The newer `Asset` type (`Asset.h`) is a `std::variant` that generalises across all three ledger asset kinds (XRP, IOU, MPT). `Issue` handles the first two. `MPTIssue` handles the third and shares the same interface contract — `getText()`, `setJson()`, `native()`, `integral()` — making static polymorphism possible without virtual dispatch. + +## The XRP Sentinel Convention + +XRP is not truly a separate type; it is an `Issue` whose `currency` field equals `beast::zero` (tested by `isXRP(currency)`). The matching `account` field is `xrpAccount()`, also the zero account. The `isConsistent()` free function enforces the critical invariant that both fields must agree: either both are the XRP sentinel or neither is. A cross-contamination (e.g., XRP currency with a real account, or a real currency with the XRP account sentinel) would silently corrupt amount comparisons and offer-book matching, so `isConsistent` acts as a sanity guard for newly constructed issues. + +`Issue::native()` checks `*this == xrpIssue()` — a full equality comparison that respects the special equality semantics defined in the header: for XRP, the account field is ignored in the comparison (`isXRP(lhs.currency) || lhs.account == rhs.account`). This short-circuit is necessary because the zero currency uniquely identifies XRP without any account qualifier. + +`Issue::integral()` delegates entirely to `native()`. For `Issue`, only XRP is integral (indivisible, stored as drops). This mirrors `MPTIssue::integral()` which always returns `true`, because MPT amounts are also integers. The shared method name enables generic code operating on either type to query precision behaviour without knowing which asset kind it holds. + +## Serialisation: Two Different String Formats + +The file exposes two string-rendering paths with subtly different shapes: + +`getText()` produces `currency_string/account_string`, using compact sentinel substitutions: `isXRP(account)` becomes the literal `"0"` and `noAccount()` becomes `"1"`. This format is primarily for human-readable diagnostics and logging. + +`to_string()` produces `account_string/currency_string` — the order is reversed — and only activates the slash form when the account is not the XRP sentinel. The asymmetry exists for historical reasons rooted in how offer-book keys and log lines have been formatted in the ledger engine for years; both formats are in active use in different parts of the codebase. + +`setJson()` and its wrapper `to_json()` produce the canonical wire format: a JSON object with a `"currency"` string field and, for non-XRP issues, an `"issuer"` field encoded as Base58Check. XRP issues emit only `"currency"` — omitting `"issuer"` entirely — which is the authoritative representation in transaction JSON, RPC responses, and the binary codec. + +## `issueFromJson`: Layered Validation at the Boundary + +`issueFromJson()` is the only place in this file that accepts untrusted input, and it validates in strict order: + +1. The input must be a JSON object, not a string or array. +2. The presence of `mpt_issuance_id` is rejected immediately — this field belongs to `MPTIssue` and its appearance signals that the caller has routed MPT data into the wrong parser. The explicit rejection prevents silent misinterpretation. +3. `"currency"` must be a JSON string, then must parse to something other than `badCurrency()` or `noCurrency()`. `badCurrency()` is the three-letter sequence `"XRP"` spelled out literally, which the ledger has historically forbidden as a currency code because it collides with native XRP. `noCurrency()` is returned by `to_currency()` on parse failure. +4. If the currency is XRP, the `"issuer"` field must be absent (`issStr.isNull()`). Providing an issuer for XRP is a protocol violation, and the error is thrown rather than silently ignored. +5. For non-XRP currencies, `"issuer"` must be a string and must decode via `parseBase58()`. Failure at any step throws either `std::runtime_error` or `Json::error` via the XRPL `Throw<>` macro, which ensures the exception carries file/line metadata for diagnostics. + +The use of two distinct exception types is intentional: `Json::error` is thrown for malformed JSON values (wrong type, missing fields), while `std::runtime_error` guards against structural misuse (passing an MPT JSON blob, passing a non-object). Callers can catch the narrower type if they only care about format errors. + +## Relationship to `MPTIssue` + +`MPTIssue` (`MPTIssue.h`) mirrors the `Issue` interface — `getText()`, `setJson()`, `native()` (always `false`), `integral()` (always `true`) — specifically to satisfy the same concept constraints used by `Asset` and amount types. The deliberate boundary between `issueFromJson` (rejects `mpt_issuance_id`) and `mptIssueFromJson` (expects it) means deserialization always produces the correct concrete type and the `Asset` variant is populated correctly. There is no runtime dispatch or `dynamic_cast`; the type is known at the point of JSON parsing. \ No newline at end of file diff --git a/src/libxrpl/protocol/Keylet.cpp.ai.json b/src/libxrpl/protocol/Keylet.cpp.ai.json new file mode 100644 index 0000000000..37ff2ee79b --- /dev/null +++ b/src/libxrpl/protocol/Keylet.cpp.ai.json @@ -0,0 +1,156 @@ +{ + "args": [ + { + "lineno": 8, + "name": "sle" + } + ], + "classes": [], + "code_paths": [], + "data_flows": [], + "description": "Implements the Keylet::check method to validate STLedgerEntry objects against Keylet type and key constraints within the xrpl namespace.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle.getType()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at Keylet::check", + "issue_pattern": "Missing empty string validation for sle.getType()", + "why_false_positive": "XRPL_ASSERT macro validates sle.getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type", + "empty", + "string", + "validation" + ], + "evidence": "if (type == ltANY) at Keylet::check", + "issue_pattern": "Missing empty string validation for type", + "why_false_positive": "if (type == ltANY) validates type for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type and sle.getType()", + "empty", + "string", + "validation" + ], + "evidence": "if (type == ltCHILD) at Keylet::check", + "issue_pattern": "Missing empty string validation for type and sle.getType()", + "why_false_positive": "if (type == ltCHILD) validates type and sle.getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle.getType() and sle.key()", + "empty", + "string", + "validation" + ], + "evidence": "return sle.getType() == type && sle.key() == key at Keylet::check", + "issue_pattern": "Missing empty string validation for sle.getType() and sle.key()", + "why_false_positive": "return sle.getType() == type && sle.key() == key validates sle.getType() and sle.key() for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Keylet.cpp", + "functions": [ + { + "args": [ + "sle" + ], + "lineno": 7, + "name": "Keylet::check" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro (custom assertion), manual checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "sle.getType()", + "location": "Keylet::check", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures that the STLedgerEntry type is not ltANY", + "Ensures that the STLedgerEntry type is not ltCHILD" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns true)", + "field": "type", + "location": "Keylet::check", + "validated_by": "if (type == ltANY)", + "validates": [ + "If Keylet type is ltANY, always passes validation" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns boolean)", + "field": "type and sle.getType()", + "location": "Keylet::check", + "validated_by": "if (type == ltCHILD)", + "validates": [ + "If Keylet type is ltCHILD, passes if sle.getType() is not ltDIR_NODE" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns boolean)", + "field": "sle.getType() and sle.key()", + "location": "Keylet::check", + "validated_by": "return sle.getType() == type && sle.key() == key", + "validates": [ + "Checks that sle.getType() matches Keylet type", + "Checks that sle.key() matches Keylet key" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Keylet.cpp.ai.md b/src/libxrpl/protocol/Keylet.cpp.ai.md new file mode 100644 index 0000000000..b8dcada7b6 --- /dev/null +++ b/src/libxrpl/protocol/Keylet.cpp.ai.md @@ -0,0 +1,23 @@ +# `Keylet.cpp` — Ledger Entry Type and Key Validation + +`Keylet.cpp` provides the sole out-of-line method for the `Keylet` struct, which is the fundamental handle used throughout the XRPL codebase to locate and type-check objects in the ledger's state map. The `Keylet` struct itself (defined in `include/xrpl/protocol/Keylet.h`) is deliberately minimal: it carries exactly two fields, a `uint256 key` (the SHAMap hash used to locate the object) and a `LedgerEntryType type` (the expected on-ledger type). The portmanteau name "Keylet" fuses "key" with "LET" (LedgerEntryType), making the dual purpose self-documenting. + +## The `check()` Method + +`Keylet::check(STLedgerEntry const& sle)` answers a single question: *does this ledger entry legitimately correspond to this keylet?* It is the enforcement point that keeps callers from accidentally retrieving an entry of the wrong type after a successful SHAMap lookup. + +The logic is a deliberate three-tier match, ordered from most-permissive to most-strict: + +1. **`ltANY` wildcard.** When `type == ltANY`, the keylet was constructed without caring about the entry's concrete type — the `keylet::unchecked` family uses this. `check()` returns `true` unconditionally, placing the burden of correctness on the caller. + +2. **`ltCHILD` pseudo-type.** When `type == ltCHILD`, the keylet represents a "child" of a directory structure. The only constraint is that the retrieved entry must *not* itself be a `ltDIR_NODE`. Directory nodes are structural bookkeeping objects that hold sorted lists of other entries; a child of a directory is definitionally something other than a directory node. Any other concrete type is accepted without checking the key. + +3. **Exact match.** For all concrete types, both `sle.getType() == type` and `sle.key() == key` must hold simultaneously. This is the normal case for typed lookups — it verifies that the SHAMap returned the exact object that was expected, at the exact slot, with the correct declared type embedded in the serialized entry itself. + +## Defensive Assertion + +An `XRPL_ASSERT` at the top of `check()` enforces that the *incoming* `STLedgerEntry` never carries `ltANY` or `ltCHILD` as its own type. These are pseudo-types that exist only for keylet construction and filtering; no real on-ledger serialized object may declare itself as one. Calling `check()` with such an entry would indicate a bug elsewhere in the pipeline — a malformed SLE read from the state map — and the assertion turns it into an immediate, diagnosable failure rather than a silent mis-classification. + +## Architectural Role + +The two-field design of `Keylet` is intentional: it keeps type information co-located with the lookup key, eliminating a class of bugs where a caller uses the correct hash but forgets to verify the type of the returned entry. The `check()` method is the runtime guardian of that invariant, and the pseudo-types `ltANY` and `ltCHILD` give callers a well-typed escape hatch for the minority of cases where some ambiguity is genuinely necessary. \ No newline at end of file diff --git a/src/libxrpl/protocol/LedgerFormats.cpp.ai.json b/src/libxrpl/protocol/LedgerFormats.cpp.ai.json new file mode 100644 index 0000000000..9da1ff72b1 --- /dev/null +++ b/src/libxrpl/protocol/LedgerFormats.cpp.ai.json @@ -0,0 +1,370 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LedgerFormats::getInstance", + "LedgerFormats::LedgerFormats", + "LedgerFormats::add (via LEDGER_ENTRY macro)", + "SOTemplate::SOTemplate", + "SOElement (constructor)" + ], + "entry_point": "LedgerFormats::getInstance", + "purpose": "Initializes the singleton LedgerFormats instance, registering all ledger entry formats and their validation templates.", + "validation_points": [ + "SOElement (constructor): Validates field type and requirements", + "SOTemplate: Aggregates SOElements for template-based validation" + ] + }, + { + "call_chain": [ + "STObject::deserialize (or similar)", + "LedgerFormats::getInstance", + "LedgerFormats::findByType / findByName", + "SOTemplate::validate", + "SOElement (field validation)" + ], + "entry_point": "Deserialization of a ledger entry (e.g., STObject::setField, STObject::deserialize)", + "purpose": "When a ledger entry is deserialized, the code looks up the format and validates fields against the SOTemplate.", + "validation_points": [ + "SOTemplate::validate: Checks required/optional fields, types, and constraints", + "SOElement: Per-field validation" + ] + } + ], + "data_flows": [ + { + "field": "sfLedgerIndex", + "flow": [ + "SField definition", + "getCommonFields() returns SOElement{sfLedgerIndex, soeOPTIONAL}", + "Passed to LEDGER_ENTRY macro via add()", + "Stored in SOTemplate for each ledger entry type", + "Used in SOTemplate::validate during deserialization" + ], + "origin": "Defined in SField.cpp/h, included in getCommonFields()", + "transformations": [ + "Wrapped as SOElement with validation requirement", + "Aggregated into SOTemplate" + ], + "validated_at": "SOTemplate::validate (template-based validation on deserialization)" + }, + { + "field": "sfLedgerEntryType", + "flow": [ + "SField definition", + "getCommonFields() returns SOElement{sfLedgerEntryType, soeREQUIRED}", + "Passed to LEDGER_ENTRY macro via add()", + "Stored in SOTemplate for each ledger entry type", + "Used in SOTemplate::validate during deserialization" + ], + "origin": "Defined in SField.cpp/h, included in getCommonFields()", + "transformations": [ + "Wrapped as SOElement with validation requirement", + "Aggregated into SOTemplate" + ], + "validated_at": "SOTemplate::validate (template-based validation on deserialization)" + }, + { + "field": "sfFlags", + "flow": [ + "SField definition", + "getCommonFields() returns SOElement{sfFlags, soeREQUIRED}", + "Passed to LEDGER_ENTRY macro via add()", + "Stored in SOTemplate for each ledger entry type", + "Used in SOTemplate::validate during deserialization" + ], + "origin": "Defined in SField.cpp/h, included in getCommonFields()", + "transformations": [ + "Wrapped as SOElement with validation requirement", + "Aggregated into SOTemplate" + ], + "validated_at": "SOTemplate::validate (template-based validation on deserialization)" + }, + { + "field": "fields from ledger_entries.macro", + "flow": [ + "ledger_entries.macro", + "Expanded by LEDGER_ENTRY macro in LedgerFormats::LedgerFormats", + "Passed to add() as SOTemplate", + "Stored in LedgerFormats registry", + "Used in SOTemplate::validate during deserialization" + ], + "origin": "Defined in ledger_entries.macro, expanded via LEDGER_ENTRY macro", + "transformations": [ + "Macro expansion to SOElement list", + "Aggregated into SOTemplate" + ], + "validated_at": "SOTemplate::validate (template-based validation on deserialization)" + } + ], + "description": "Defines and implements the LedgerFormats class, which manages the formats and common fields for ledger entries in the XRPL protocol.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLedgerIndex", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLedgerIndex validated by SOTemplate/SOElement/SField (xrpl protocol templates), jss:: for field naming", + "issue_pattern": "Missing validation for sfLedgerIndex", + "why_false_positive": "SOTemplate/SOElement/SField (xrpl protocol templates), jss:: for field naming validates sfLedgerIndex automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLedgerEntryType", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLedgerEntryType validated by SOTemplate/SOElement/SField (xrpl protocol templates), jss:: for field naming", + "issue_pattern": "Missing validation for sfLedgerEntryType", + "why_false_positive": "SOTemplate/SOElement/SField (xrpl protocol templates), jss:: for field naming validates sfLedgerEntryType automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfFlags", + "validation", + "missing", + "check" + ], + "evidence": "Field sfFlags validated by SOTemplate/SOElement/SField (xrpl protocol templates), jss:: for field naming", + "issue_pattern": "Missing validation for sfFlags", + "why_false_positive": "SOTemplate/SOElement/SField (xrpl protocol templates), jss:: for field naming validates sfFlags automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "All fields defined in ledger_entries.macro", + "validation", + "missing", + "check" + ], + "evidence": "Field All fields defined in ledger_entries.macro validated by SOTemplate/SOElement/SField (xrpl protocol templates), jss:: for field naming", + "issue_pattern": "Missing validation for All fields defined in ledger_entries.macro", + "why_false_positive": "SOTemplate/SOElement/SField (xrpl protocol templates), jss:: for field naming validates All fields defined in ledger_entries.macro automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLedgerIndex", + "empty", + "string", + "validation" + ], + "evidence": "SOElement/SOTemplate (template-based validation) at LedgerFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfLedgerIndex", + "why_false_positive": "SOElement/SOTemplate (template-based validation) validates sfLedgerIndex for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLedgerEntryType", + "empty", + "string", + "validation" + ], + "evidence": "SOElement/SOTemplate (template-based validation) at LedgerFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfLedgerEntryType", + "why_false_positive": "SOElement/SOTemplate (template-based validation) validates sfLedgerEntryType for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfFlags", + "empty", + "string", + "validation" + ], + "evidence": "SOElement/SOTemplate (template-based validation) at LedgerFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfFlags", + "why_false_positive": "SOElement/SOTemplate (template-based validation) validates sfFlags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "All fields defined in ledger_entries.macro", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/add() via LEDGER_ENTRY macro at LedgerFormats::LedgerFormats (constructor)", + "issue_pattern": "Missing empty string validation for All fields defined in ledger_entries.macro", + "why_false_positive": "SOTemplate/add() via LEDGER_ENTRY macro validates All fields defined in ledger_entries.macro for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/LedgerFormats.cpp", + "functions": [ + { + "args": [], + "lineno": 8, + "name": "LedgerFormats::getCommonFields" + }, + { + "args": [], + "lineno": 16, + "name": "LedgerFormats::LedgerFormats" + }, + { + "args": [], + "lineno": 38, + "name": "LedgerFormats::getInstance" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "Validation logic is indirectly tested via tests that deserialize or construct ledger entries. Likely test files: STObject_test.cpp, LedgerFormats_test.cpp, and various transaction/ledger entry tests in the rippled/test/ directory. Direct unit tests for LedgerFormats or SOTemplate are rare; most coverage is via integration tests that exercise ledger entry parsing and validation. Gaps: No explicit unit tests for getCommonFields or the macro expansion logic; edge cases in field validation (e.g., missing required fields, wrong types) may not be exhaustively tested.", + "validation_architecture": { + "auto_validated_fields": [ + "sfLedgerIndex", + "sfLedgerEntryType", + "sfFlags", + "All fields defined in ledger_entries.macro" + ], + "framework": "SOTemplate/SOElement/SField (xrpl protocol templates), jss:: for field naming", + "validation_layer": "business_logic (template-based validation at object construction/deserialization)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (field is optional)", + "field": "sfLedgerIndex", + "location": "LedgerFormats::getCommonFields", + "validated_by": "SOElement/SOTemplate (template-based validation)", + "validates": [ + "Field may be present or absent in the ledger entry" + ], + "validation_type": "presence (optional field)" + }, + { + "confidence": 1.0, + "error_thrown": "FieldNotPresent or similar (if missing, at deserialization/construction)", + "field": "sfLedgerEntryType", + "location": "LedgerFormats::getCommonFields", + "validated_by": "SOElement/SOTemplate (template-based validation)", + "validates": [ + "Field must be present in the ledger entry" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 1.0, + "error_thrown": "FieldNotPresent or similar (if missing, at deserialization/construction)", + "field": "sfFlags", + "location": "LedgerFormats::getCommonFields", + "validated_by": "SOElement/SOTemplate (template-based validation)", + "validates": [ + "Field must be present in the ledger entry" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "FieldNotPresent, FieldWrongType, or similar (at deserialization/construction)", + "field": "All fields defined in ledger_entries.macro", + "location": "LedgerFormats::LedgerFormats (constructor)", + "validated_by": "SOTemplate/add() via LEDGER_ENTRY macro", + "validates": [ + "Each field's presence (required/optional)", + "Each field's type (as defined in SField)", + "Business logic constraints as encoded in SOTemplate" + ], + "validation_type": "presence (required/optional), type (via SField), business logic (via template)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/LedgerFormats.cpp.ai.md b/src/libxrpl/protocol/LedgerFormats.cpp.ai.md new file mode 100644 index 0000000000..0fe72d3117 --- /dev/null +++ b/src/libxrpl/protocol/LedgerFormats.cpp.ai.md @@ -0,0 +1,37 @@ +# `LedgerFormats.cpp` — Ledger Entry Format Registry + +`LedgerFormats.cpp` is the sole registration point that connects every on-ledger object type to its protocol-level validation schema. Its three functions — `getCommonFields()`, the constructor, and `getInstance()` — collectively build the singleton registry that the rest of the codebase queries to serialize, deserialize, and validate any ledger entry. + +## Role in the Protocol Stack + +Every object stored on the XRP Ledger (account roots, offers, trust lines, escrows, payment channels, AMM pools, etc.) has a canonical field layout. When an `STObject` is deserialized from raw bytes, it looks up the matching `SOTemplate` in this registry via `LedgerFormats::getInstance().findByType(type)` and validates that every required field is present and every field has the correct type. Without this registry, there is no enforcement boundary between a well-formed ledger entry and arbitrary binary data. + +`LedgerFormats` inherits from `KnownFormats`, a CRTP-adjacent base template that manages two `boost::container::flat_map` indexes (by name and by numeric type) over a `std::forward_list`. The list is node-based by design: once an `Item` is emplaced, its address never changes, so the flat-map indexes can hold stable raw pointers. Each `Item` owns an `SOTemplate` (the combination of type-unique fields and common fields) plus the entry's string name and integer type tag. + +## Macro-Driven Registration + +The constructor is intentionally terse: it defines a local `LEDGER_ENTRY` macro and then `#include`s ``. That file expands one `LEDGER_ENTRY(tag, value, name, rpcName, fields)` invocation per entry type, and the macro expands each invocation into an `add(jss::name, tag, UNWRAP fields, getCommonFields())` call. + +This X-macro pattern serves as a single source of truth. The same `ledger_entries.macro` file is also included in `LedgerFormats.h` (with a different `LEDGER_ENTRY` definition) to generate the `LedgerEntryType` enum values. A new ledger entry type is therefore added in exactly one place — the macro file — and both the numeric identifier and the validation schema are derived from that single declaration. + +The `UNWRAP(...)` helper macro is needed because the fields argument is written as a doubly-parenthesised braced list, e.g. `({sfAccount, soeREQUIRED}, ...)`. The outer parentheses prevent the C preprocessor from treating commas inside the initializer list as macro argument separators; `UNWRAP` strips them so the result is a valid `std::initializer_list`-compatible expression for `std::vector`. The constructor uses `#pragma push_macro`/`pop_macro` around both `LEDGER_ENTRY` and `UNWRAP` to protect against pre-existing definitions in the translation unit — defensive macro hygiene that avoids hard-to-diagnose build failures. + +`LEDGER_ENTRY_DUPLICATE` handles one naming collision: `DepositPreauth` exists as both a transaction type and a ledger entry type. Because `jss.h` uses a `JSS()` macro to declare string constants, two `JSS(DepositPreauth)` expansions in the same translation unit would produce a duplicate symbol. `LEDGER_ENTRY_DUPLICATE` expands to the same `LEDGER_ENTRY` call but suppresses the `JSS` emission. + +## Common Fields + +`getCommonFields()` returns a local static `std::vector` containing three fields shared by every ledger entry: + +- `sfLedgerIndex` (`soeOPTIONAL`) — the object's position in the ledger; may be absent in some contexts. +- `sfLedgerEntryType` (`soeREQUIRED`) — the numeric discriminator used for lookup; must always be present. +- `sfFlags` (`soeREQUIRED`) — the object's flag bitmask; must always be present, even if zero. + +Separating these into `getCommonFields()` rather than repeating them in every `LEDGER_ENTRY` invocation means a change to universal fields touches one place and propagates to all ~30 registered types automatically. Using a function-local static also avoids static initialization order problems — the vector is constructed on first call, which happens inside the `LedgerFormats` constructor itself. + +## Singleton Initialization and Duplicate Detection + +`getInstance()` returns a function-local static `LedgerFormats const instance`. C++11 guarantees this is initialized exactly once, thread-safely, on first access. Because the constructor calls `KnownFormats::add()` for every entry, and `add()` calls `findByType()` before inserting, a duplicate numeric type ID in `ledger_entries.macro` triggers `LogicError` at program startup rather than silently producing an incorrect registry. This is a meaningful invariant: since type IDs are part of the serialization protocol and must never collide, a crash-on-startup is the correct failure mode. + +## Protocol Stability Concerns + +`LedgerFormats.h` documents explicitly that `LedgerEntryType` numeric values are on-ledger protocol data — changing or reusing them causes a hard fork. The macro file lists entries in ascending type-ID order and deliberately leaves gaps (e.g., `0x0084`–`0x0087` are reserved for future Vault-related objects). Legacy types (`ltNICKNAME = 0x006e`, `ltCONTRACT = 0x0063`, `ltGENERATOR_MAP = 0x0067`) are retained in the header with `[[deprecated]]` annotations rather than removed, precisely to prevent accidental reuse of those numeric slots. \ No newline at end of file diff --git a/src/libxrpl/protocol/LedgerHeader.cpp.ai.json b/src/libxrpl/protocol/LedgerHeader.cpp.ai.json new file mode 100644 index 0000000000..86356ef9df --- /dev/null +++ b/src/libxrpl/protocol/LedgerHeader.cpp.ai.json @@ -0,0 +1,222 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "deserializeHeader" + ], + "entry_point": "deserializeHeader", + "purpose": "Deserializes a LedgerHeader from a binary Slice, optionally including the hash.", + "validation_points": [ + "No explicit validation in this function; assumes input Slice is well-formed and large enough." + ] + }, + { + "call_chain": [ + "deserializePrefixedHeader", + "deserializeHeader" + ], + "entry_point": "deserializePrefixedHeader", + "purpose": "Deserializes a LedgerHeader from a Slice, skipping a 4-byte prefix (e.g., HashPrefix).", + "validation_points": [ + "No explicit validation; relies on deserializeHeader." + ] + }, + { + "call_chain": [ + "addRaw" + ], + "entry_point": "addRaw", + "purpose": "Serializes a LedgerHeader into a Serializer, optionally including the hash.", + "validation_points": [ + "No explicit validation; assumes LedgerHeader fields are valid." + ] + }, + { + "call_chain": [ + "calculateLedgerHash" + ], + "entry_point": "calculateLedgerHash", + "purpose": "Calculates the hash of a LedgerHeader using its fields and a hash prefix.", + "validation_points": [ + "No explicit validation; assumes LedgerHeader fields are valid and well-formed." + ] + } + ], + "data_flows": [ + { + "field": "seq", + "flow": [ + "Slice (deserializeHeader)", + "SerialIter::get32()", + "LedgerHeader.seq", + "Used in addRaw, calculateLedgerHash" + ], + "origin": "Slice input (deserializeHeader) or LedgerHeader struct (addRaw/calculateLedgerHash)", + "transformations": [ + "Read as uint32 from binary", + "Serialized as uint32", + "Used as uint32 in hash calculation" + ], + "validated_at": "Not explicitly validated" + }, + { + "field": "drops", + "flow": [ + "Slice (deserializeHeader)", + "SerialIter::get64()", + "LedgerHeader.drops", + "Used in addRaw, calculateLedgerHash" + ], + "origin": "Slice input (deserializeHeader) or LedgerHeader struct", + "transformations": [ + "Read as uint64", + "Serialized as uint64", + "Used as uint64 in hash calculation" + ], + "validated_at": "Not explicitly validated" + }, + { + "field": "parentHash / txHash / accountHash", + "flow": [ + "Slice (deserializeHeader)", + "SerialIter::get256()", + "LedgerHeader.{parentHash,txHash,accountHash}", + "Used in addRaw, calculateLedgerHash" + ], + "origin": "Slice input (deserializeHeader) or LedgerHeader struct", + "transformations": [ + "Read as 256-bit value", + "Serialized as bit string", + "Used as-is in hash calculation" + ], + "validated_at": "Not explicitly validated" + }, + { + "field": "parentCloseTime / closeTime", + "flow": [ + "Slice (deserializeHeader)", + "SerialIter::get32()", + "NetClock::time_point{NetClock::duration{...}}", + "LedgerHeader.{parentCloseTime,closeTime}", + "Used in addRaw, calculateLedgerHash" + ], + "origin": "Slice input (deserializeHeader) or LedgerHeader struct", + "transformations": [ + "Read as uint32", + "Wrapped in NetClock::duration/time_point", + "Serialized as uint32", + "Used as uint32 in hash calculation" + ], + "validated_at": "Not explicitly validated" + }, + { + "field": "closeTimeResolution", + "flow": [ + "Slice (deserializeHeader)", + "SerialIter::get8()", + "NetClock::duration{...}", + "LedgerHeader.closeTimeResolution", + "Used in addRaw, calculateLedgerHash" + ], + "origin": "Slice input (deserializeHeader) or LedgerHeader struct", + "transformations": [ + "Read as uint8", + "Wrapped in NetClock::duration", + "Serialized as uint8", + "Used as uint8 in hash calculation" + ], + "validated_at": "Not explicitly validated" + }, + { + "field": "closeFlags", + "flow": [ + "Slice (deserializeHeader)", + "SerialIter::get8()", + "LedgerHeader.closeFlags", + "Used in addRaw, calculateLedgerHash" + ], + "origin": "Slice input (deserializeHeader) or LedgerHeader struct", + "transformations": [ + "Read as uint8", + "Serialized as uint8", + "Used as uint8 in hash calculation" + ], + "validated_at": "Not explicitly validated" + }, + { + "field": "hash", + "flow": [ + "Slice (deserializeHeader, if hasHash)", + "SerialIter::get256()", + "LedgerHeader.hash", + "Used in addRaw (if includeHash)" + ], + "origin": "Slice input (deserializeHeader, if hasHash) or LedgerHeader struct", + "transformations": [ + "Read as 256-bit value", + "Serialized as bit string (if included)" + ], + "validated_at": "Not explicitly validated" + } + ], + "description": "Implements serialization, deserialization, and hash calculation for XRPL LedgerHeader structures.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/LedgerHeader.cpp", + "functions": [ + { + "args": [ + "LedgerHeader const& info", + "Serializer& s", + "bool includeHash" + ], + "lineno": 8, + "name": "addRaw" + }, + { + "args": [ + "Slice data", + "bool hasHash" + ], + "lineno": 21, + "name": "deserializeHeader" + }, + { + "args": [ + "Slice data", + "bool hasHash" + ], + "lineno": 44, + "name": "deserializePrefixedHeader" + }, + { + "args": [ + "LedgerHeader const& info" + ], + "lineno": 49, + "name": "calculateLedgerHash" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no validation of input sizes, field ranges, or field consistency in these functions. They assume the input Slice is well-formed and large enough. Typical test coverage would be in unit tests for LedgerHeader serialization/deserialization and hash calculation, likely in files such as LedgerHeader_test.cpp, Ledger_test.cpp, or Serializer_test.cpp. However, negative tests for malformed or truncated input, or for invalid field values, may be missing unless explicitly written elsewhere. The code is vulnerable to buffer overruns or undefined behavior if given malformed input, as there are no checks on Slice size or field validity.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/LedgerHeader.cpp.ai.md b/src/libxrpl/protocol/LedgerHeader.cpp.ai.md new file mode 100644 index 0000000000..efe0cec8e0 --- /dev/null +++ b/src/libxrpl/protocol/LedgerHeader.cpp.ai.md @@ -0,0 +1,54 @@ +# `LedgerHeader.cpp` — Ledger Header Serialization and Hash Calculation + +`LedgerHeader.cpp` implements the four canonical operations on `LedgerHeader`, the plain-data struct that captures all metadata about a closed (or closing) XRP Ledger: binary serialization via `addRaw`, binary deserialization via `deserializeHeader` and `deserializePrefixedHeader`, and canonical hash calculation via `calculateLedgerHash`. Every path a ledger takes through the system — network propagation, node-store persistence, validation, and replay — passes through one or more of these functions. + +## The `LedgerHeader` Struct + +Defined in `LedgerHeader.h`, the struct carries the complete identity of a ledger: + +- `seq` — the ledger index (a monotonically increasing `uint32`) +- `drops` — total XRP in existence, expressed in drops (`uint64`) +- `parentHash`, `txHash`, `accountHash` — 256-bit hashes identifying the parent ledger, the transaction Merkle root, and the account-state Merkle root respectively +- `parentCloseTime`, `closeTime` — `NetClock::time_point` values encoding seconds since XRPL epoch (1 January 2000, not Unix epoch) +- `closeTimeResolution` — a `NetClock::duration` specifying close-time rounding granularity, stored as a single byte (valid range 2–120 seconds) +- `closeFlags` — a byte-sized bitmask; bit `sLCF_NoConsensusTime` (0x01) signals that validators did not agree on close time +- `hash` — the ledger's own 256-bit identity, computed from all the above fields + +The `validated` and `accepted` booleans are runtime state flags that are deliberately *not* part of the wire format and therefore do not appear in any of the serialization functions. + +## `addRaw` — Canonical Serialization + +```cpp +void addRaw(LedgerHeader const& info, Serializer& s, bool includeHash) +``` + +Appends the header fields to a `Serializer` in network byte order. The field order is fixed by protocol: `seq` (32-bit), `drops` (64-bit), then the three 256-bit hashes, then `parentCloseTime` and `closeTime` as 32-bit epoch counts, then the single-byte `closeTimeResolution` and `closeFlags`. The `hash` field is appended last only if `includeHash` is true. + +Separating the hash from the body is a deliberate choice: `hash` is derived from the other fields, so including it in the authoritative serialization for hashing would be circular. However, when persisting a ledger to the node store or sending it over the wire, including the precomputed hash saves the receiver the cost of recomputing it. The boolean flag makes both use cases possible with a single function. + +## `calculateLedgerHash` — Protocol-Defined Identity + +```cpp +uint256 calculateLedgerHash(LedgerHeader const& info) +``` + +Computes the canonical ledger hash by feeding all header fields (except `hash` itself) into `sha512Half`, which returns the first 256 bits of a SHA-512 digest. The first input is `HashPrefix::ledgerMaster`, a four-byte constant `LWR\0`. This prefix namespaces the hash: even if two different object types happened to produce identical binary content, their hashes would differ because they use different `HashPrefix` values. All XRPL hash computations follow this pattern — `transactionID`, `txNode`, `leafNode`, `innerNode`, etc. each have their own prefix. + +The comment in the source is notable: "This has to match addRaw in View.h." The field ordering and integer widths in `calculateLedgerHash` must exactly mirror those in `addRaw`. This is an unenforced invariant — the compiler does not prevent them from diverging. A mismatch would cause the node to compute hashes that don't match what the rest of the network expects, leading to consensus failures. The explicit cast to `std::uint32_t`, `std::uint64_t`, and `std::uint8_t` in `calculateLedgerHash` makes the wire types unambiguous and prevents accidental widening or narrowing from silently breaking the hash. + +## `deserializeHeader` and `deserializePrefixedHeader` + +```cpp +LedgerHeader deserializeHeader(Slice data, bool hasHash) +LedgerHeader deserializePrefixedHeader(Slice data, bool hasHash) +``` + +`deserializeHeader` constructs a `SerialIter` over the incoming `Slice` and reads fields in the exact order `addRaw` writes them. The time fields require explicit wrapping: `sit.get32()` yields a raw integer which must be stuffed into `NetClock::duration` and then into `NetClock::time_point` to restore the typed representation. + +`deserializePrefixedHeader` is a thin wrapper: it advances the `Slice` by four bytes (`data + 4`) to skip a `HashPrefix` that was prepended during storage or transmission, then delegates to `deserializeHeader`. This variant is used in `InboundLedger.cpp` when decoding ledger data received over the peer-to-peer protocol, where the database node entry begins with a `HashPrefix` tag. + +Neither deserialization function performs explicit size or range validation. `SerialIter` will throw if the buffer is exhausted, but no semantic checking of field values occurs — the callers (`Ledger.cpp`, `InboundLedger.cpp`) are responsible for verifying that the deserialized hash matches `calculateLedgerHash` before trusting the data. + +## Callers and Integration Points + +In `Ledger.cpp`, `calculateLedgerHash` is called in three places after constructing or modifying a `Ledger` object to seal its identity into `header_.hash`. In `InboundLedger.cpp`, `deserializeHeader` and `deserializePrefixedHeader` reconstruct ledger headers from network-received byte buffers when replaying or assembling a ledger from peer responses. The symmetry between these sites defines the full lifecycle of a ledger header: constructed in memory → hash computed → serialized → transmitted or stored → deserialized → hash verified. \ No newline at end of file diff --git a/src/libxrpl/protocol/MPTAmount.cpp.ai.json b/src/libxrpl/protocol/MPTAmount.cpp.ai.json new file mode 100644 index 0000000000..c6b9b5dd79 --- /dev/null +++ b/src/libxrpl/protocol/MPTAmount.cpp.ai.json @@ -0,0 +1,143 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "MPTAmount::operator+=" + ], + "entry_point": "MPTAmount::operator+=", + "purpose": "Adds the value of another MPTAmount to this one.", + "validation_points": [] + }, + { + "call_chain": [ + "MPTAmount::operator-=" + ], + "entry_point": "MPTAmount::operator-=", + "purpose": "Subtracts the value of another MPTAmount from this one.", + "validation_points": [] + }, + { + "call_chain": [ + "MPTAmount::operator-" + ], + "entry_point": "MPTAmount::operator-", + "purpose": "Returns a new MPTAmount with the negated value.", + "validation_points": [] + }, + { + "call_chain": [ + "MPTAmount::operator==" + ], + "entry_point": "MPTAmount::operator==", + "purpose": "Compares two MPTAmount objects for equality.", + "validation_points": [] + }, + { + "call_chain": [ + "MPTAmount::operator<" + ], + "entry_point": "MPTAmount::operator<", + "purpose": "Compares two MPTAmount objects for ordering.", + "validation_points": [] + }, + { + "call_chain": [ + "MPTAmount::minPositiveAmount" + ], + "entry_point": "MPTAmount::minPositiveAmount", + "purpose": "Returns the minimum positive MPTAmount (value = 1).", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "value_", + "flow": [ + "MPTAmount constructor (e.g., MPTAmount{val})", + "Stored in value_", + "Accessed via value() or directly in operators", + "Used in arithmetic or comparison operators" + ], + "origin": "Set via MPTAmount constructor or assignment.", + "transformations": [ + "Incremented/decremented in operator+= and operator-=", + "Negated in operator-", + "Compared in operator== and operator<" + ], + "validated_at": "No explicit validation in this code" + } + ], + "description": "Implements arithmetic and comparison operators for the xrpl::MPTAmount class, which represents an amount type used in the XRPL protocol.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/MPTAmount.cpp", + "functions": [ + { + "args": [ + "other" + ], + "lineno": 5, + "name": "operator+=" + }, + { + "args": [ + "other" + ], + "lineno": 11, + "name": "operator-=" + }, + { + "args": [], + "lineno": 17, + "name": "operator-" + }, + { + "args": [ + "other" + ], + "lineno": 22, + "name": "operator==" + }, + { + "args": [ + "other" + ], + "lineno": 27, + "name": "operator==" + }, + { + "args": [ + "other" + ], + "lineno": 32, + "name": "operator<" + }, + { + "args": [], + "lineno": 37, + "name": "minPositiveAmount" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "No validation logic is present in this code; all operators assume valid input. There are no explicit checks for overflow, underflow, or negative values (except for negation). Test coverage would need to exist in unit tests for MPTAmount (likely in test files such as test_MPTAmount.cpp or protocol/MPTAmount_test.cpp), but this file itself does not contain or invoke validation logic. Gaps: No validation of input values, no error handling, and no boundary checks are present in this implementation.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none", + "validation_layer": "none" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/MPTAmount.cpp.ai.md b/src/libxrpl/protocol/MPTAmount.cpp.ai.md new file mode 100644 index 0000000000..852848346c --- /dev/null +++ b/src/libxrpl/protocol/MPTAmount.cpp.ai.md @@ -0,0 +1,35 @@ +# `MPTAmount.cpp` — Arithmetic and Comparison Operators for Multi-Purpose Token Amounts + +## Role in the System + +`MPTAmount.cpp` provides the out-of-line operator implementations for `xrpl::MPTAmount`, the ledger's amount type for Multi-Purpose Tokens (MPTs). MPTs are XRPL's integer-valued custom token primitive — a simpler alternative to IOU trust-line tokens that avoids the floating-point mantissa/exponent representation used by `IOUAmount`. Because MPT balances are plain 64-bit signed integers, arithmetic is direct and cheap, and this file reflects that simplicity: it is the entire runtime body of the class. + +## Class Design Philosophy + +`MPTAmount` is structurally parallel to `XRPAmount`. Both classes wrap a single `std::int64_t` field (`value_` / `drops_`), both declare the same four `boost::operators` mixin bases in their class head, and both expose `operator+=(MPTAmount)`, `operator-=(MPTAmount)`, `operator-()`, `operator==(MPTAmount)`, `operator==(value_type)`, and `operator<(MPTAmount)`. + +The Boost.Operators inheritance is deliberate: by privately inheriting from `boost::totally_ordered`, `boost::additive`, `boost::equality_comparable`, and `boost::additive`, the class gets `operator>`, `operator<=`, `operator>=`, `operator!=`, binary `operator+`, binary `operator-`, and their mixed-type counterparts for free — synthesized from the handful of primitives defined here. No hand-written boilerplate, no risk of inconsistency between `<` and `>`. + +The explicit constructor (`constexpr explicit MPTAmount(value_type)`) and the zero-valued helpers (`MPTAmount(beast::Zero)`, `operator=(beast::Zero)`) live in the header as `constexpr` so the compiler can constant-fold them. The comparison and mutation operators do runtime work — small, but not zero-cost for the inliner — so they are out-of-lined here. + +## Operator Implementations + +`operator+=` and `operator-=` perform direct integer addition and subtraction on `value_` with no overflow guard. This is intentional: MPT amounts flow through the same ledger constraint machinery as XRP drops, and callers are expected to validate bounds before mutating balances. The absence of overflow checks matches `XRPAmount`'s operators and keeps the hot path branch-free. + +`operator-()` (unary negation) returns a new `MPTAmount{-value_}`. This is meaningful in transaction arithmetic where a credit to one account and a debit to another are represented as equal-magnitude amounts of opposite sign before being applied, but callers must ensure the magnitude stays representable in `int64_t`. + +The two `operator==` overloads — one taking `MPTAmount const&` and one taking `value_type` — serve different call sites. The second overload enables comparisons like `amt == 0` without constructing a temporary, which the `boost::equality_comparable` mixin then uses to synthesize the mixed-type `!=`. + +`operator<` is the single total-order primitive from which Boost synthesizes `>`, `<=`, and `>=`. Because `value_type` is `int64_t`, the natural signed-integer ordering gives correct semantics for negative amounts (a negative MPT balance is less than zero). + +## `minPositiveAmount()` + +The static factory `minPositiveAmount()` returns `MPTAmount{1}`. In the XRPL protocol, this represents the smallest transferable unit of any MPT — analogous to one drop of XRP. The method exists as a named factory rather than a constant so code that works generically across amount types (`XRPAmount`, `IOUAmount`, `MPTAmount`) can call a uniform interface. For `IOUAmount` the equivalent is more involved (the smallest representable positive normalized value), so `minPositiveAmount()` abstracts that variation behind a common name. + +## Overflow Safety in Context + +The `.cpp` file itself performs no overflow detection. The safe multiplication path for MPT is `mulRatio`, defined inline in `MPTAmount.h`. It uses `boost::multiprecision::int128_t` to widen the intermediate product before dividing, then explicitly checks whether the result fits in `int64_t` before converting — throwing `std::overflow_error` if not. This split (unsafe primitives in `.cpp`, guarded ratio math in the header) mirrors the approach taken by `XRPAmount` and keeps the common-case operators branch-free while still providing a safe path for fee and proportion calculations. + +## Relationship to `IOUAmount` + +`MPTAmount` deliberately does not inherit from or compose with `IOUAmount`. IOU amounts carry a mantissa and a decimal exponent, support values across a wide dynamic range, and require normalization after every arithmetic operation. MPT amounts are whole integers bounded by `int64_t`, so that machinery would be dead weight. Keeping the types separate also allows the type system to reject, at compile time, mixing MPT and IOU amounts in expressions where only one is valid. \ No newline at end of file diff --git a/src/libxrpl/protocol/MPTIssue.cpp.ai.json b/src/libxrpl/protocol/MPTIssue.cpp.ai.json new file mode 100644 index 0000000000..382d9a2622 --- /dev/null +++ b/src/libxrpl/protocol/MPTIssue.cpp.ai.json @@ -0,0 +1,492 @@ +{ + "args": [ + { + "lineno": 13, + "name": "issuanceID" + }, + { + "lineno": 17, + "name": "sequence" + }, + { + "lineno": 17, + "name": "account" + }, + { + "lineno": 35, + "name": "jv" + }, + { + "lineno": 40, + "name": "mptIssue" + }, + { + "lineno": 51, + "name": "v" + }, + { + "lineno": 74, + "name": "os" + }, + { + "lineno": 74, + "name": "x" + } + ], + "classes": [ + { + "args": [ + "MPTID const& issuanceID", + "std::uint32_t sequence, AccountID const& account" + ], + "lineno": 13, + "name": "MPTIssue" + } + ], + "code_paths": [ + { + "call_chain": [ + "mptIssueFromJson", + "MPTID::parseHex", + "MPTIssue::MPTIssue" + ], + "entry_point": "mptIssueFromJson", + "purpose": "Constructs an MPTIssue from a JSON object, validating input and parsing the mpt_issuance_id.", + "validation_points": [ + "mptIssueFromJson: v.isObject() - checks input is a JSON object", + "mptIssueFromJson: v.isMember(jss::currency) || v.isMember(jss::issuer) - ensures forbidden fields are absent", + "mptIssueFromJson: idStr.isString() - checks mpt_issuance_id is a string", + "MPTID::parseHex - validates mpt_issuance_id string is a valid hex" + ] + }, + { + "call_chain": [ + "MPTIssue::getIssuer" + ], + "entry_point": "MPTIssue::getIssuer", + "purpose": "Extracts the issuer AccountID from the MPTID.", + "validation_points": [ + "static_assert(sizeof(MPTID) == (sizeof(std::uint32_t) + sizeof(AccountID))) - compile-time layout validation" + ] + }, + { + "call_chain": [ + "to_json", + "MPTIssue::setJson" + ], + "entry_point": "to_json", + "purpose": "Serializes an MPTIssue to JSON.", + "validation_points": [] + }, + { + "call_chain": [ + "to_string", + "MPTIssue::getMptID", + "to_string(MPTID)" + ], + "entry_point": "to_string", + "purpose": "Converts an MPTIssue to its string representation.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "v (input JSON)", + "flow": [ + "External JSON input", + "mptIssueFromJson (validation: isObject, forbidden fields, mpt_issuance_id presence/type)", + "idStr (mpt_issuance_id field)", + "MPTID::parseHex (validation: hex format)", + "MPTIssue::MPTIssue (constructs object)" + ], + "origin": "External input to mptIssueFromJson", + "transformations": [ + "Checked for object type", + "Checked for forbidden fields", + "Extracted as string", + "Parsed from hex to MPTID" + ], + "validated_at": "mptIssueFromJson (multiple points), MPTID::parseHex" + }, + { + "field": "mpt_issuance_id", + "flow": [ + "JSON input", + "Extracted as idStr", + "Validated as string", + "Parsed by MPTID::parseHex", + "Stored in MPTIssue::mptID_" + ], + "origin": "v[jss::mpt_issuance_id] in input JSON", + "transformations": [ + "Type checked (isString)", + "Parsed from hex string to MPTID" + ], + "validated_at": "mptIssueFromJson (isString), MPTID::parseHex" + }, + { + "field": "MPTID layout", + "flow": [ + "Compile-time static_assert in MPTIssue::getIssuer" + ], + "origin": "Definition of MPTID type", + "transformations": [ + "Ensures memory layout is as expected" + ], + "validated_at": "static_assert in MPTIssue::getIssuer" + }, + { + "field": "AccountID", + "flow": [ + "MPTIssue::mptID_", + "reinterpret_cast to AccountID*", + "Returned as issuer" + ], + "origin": "Extracted from MPTID in MPTIssue::getIssuer", + "transformations": [ + "Pointer arithmetic and reinterpret_cast" + ], + "validated_at": "static_assert ensures safe reinterpret_cast" + } + ], + "description": "Implements the MPTIssue class for representing and handling Multi-Party Trust (MPT) issuance identifiers in the XRPL protocol, including construction, serialization, deserialization, and JSON conversion.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "v (input JSON)", + "empty", + "string", + "validation" + ], + "evidence": "manual type check at mptIssueFromJson", + "issue_pattern": "Missing empty string validation for v (input JSON)", + "why_false_positive": "manual type check validates v (input JSON) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "v (input JSON)", + "type", + "validation", + "check" + ], + "evidence": "manual type check at mptIssueFromJson", + "issue_pattern": "Missing type validation for v (input JSON)", + "why_false_positive": "manual type check validates v (input JSON) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "v (input JSON)", + "empty", + "string", + "validation" + ], + "evidence": "manual field presence check at mptIssueFromJson", + "issue_pattern": "Missing empty string validation for v (input JSON)", + "why_false_positive": "manual field presence check validates v (input JSON) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mpt_issuance_id (in v)", + "empty", + "string", + "validation" + ], + "evidence": "manual type check at mptIssueFromJson", + "issue_pattern": "Missing empty string validation for mpt_issuance_id (in v)", + "why_false_positive": "manual type check validates mpt_issuance_id (in v) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "mpt_issuance_id (in v)", + "type", + "validation", + "check" + ], + "evidence": "manual type check at mptIssueFromJson", + "issue_pattern": "Missing type validation for mpt_issuance_id (in v)", + "why_false_positive": "manual type check validates mpt_issuance_id (in v) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mpt_issuance_id (in v)", + "empty", + "string", + "validation" + ], + "evidence": "MPTID::parseHex at mptIssueFromJson", + "issue_pattern": "Missing empty string validation for mpt_issuance_id (in v)", + "why_false_positive": "MPTID::parseHex validates mpt_issuance_id (in v) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "mpt_issuance_id (in v)", + "format", + "validation", + "invalid" + ], + "evidence": "MPTID::parseHex at mptIssueFromJson", + "issue_pattern": "Missing format validation for mpt_issuance_id (in v)", + "why_false_positive": "MPTID::parseHex validates mpt_issuance_id (in v) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "MPTID layout", + "empty", + "string", + "validation" + ], + "evidence": "static_assert at MPTIssue::getIssuer", + "issue_pattern": "Missing empty string validation for MPTID layout", + "why_false_positive": "static_assert validates MPTID layout for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "MPTID layout", + "type", + "validation", + "check" + ], + "evidence": "static_assert at MPTIssue::getIssuer", + "issue_pattern": "Missing type validation for MPTID layout", + "why_false_positive": "static_assert validates MPTID layout type" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/MPTIssue.cpp", + "functions": [ + { + "args": [ + "MPTID const& issuanceID" + ], + "lineno": 13, + "name": "MPTIssue::MPTIssue" + }, + { + "args": [ + "std::uint32_t sequence", + "AccountID const& account" + ], + "lineno": 17, + "name": "MPTIssue::MPTIssue" + }, + { + "args": [], + "lineno": 21, + "name": "MPTIssue::getIssuer" + }, + { + "args": [], + "lineno": 30, + "name": "MPTIssue::getText" + }, + { + "args": [ + "Json::Value& jv" + ], + "lineno": 35, + "name": "MPTIssue::setJson" + }, + { + "args": [ + "MPTIssue const& mptIssue" + ], + "lineno": 40, + "name": "to_json" + }, + { + "args": [ + "MPTIssue const& mptIssue" + ], + "lineno": 47, + "name": "to_string" + }, + { + "args": [ + "Json::Value const& v" + ], + "lineno": 51, + "name": "mptIssueFromJson" + }, + { + "args": [ + "std::ostream& os", + "MPTIssue const& x" + ], + "lineno": 74, + "name": "operator<<" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code relies on exceptions for error handling, which suggests that negative test cases (invalid JSON, missing fields, bad hex) should be tested. Typical test files would be in the unit test suite for protocol or MPTIssue, e.g., test/protocol/MPTIssue_test.cpp or similar. However, from the code provided, there is no direct evidence of test coverage. Gaps may include: (1) No explicit tests for forbidden fields (currency/issuer), (2) No tests for malformed or non-string mpt_issuance_id, (3) No tests for invalid hex in mpt_issuance_id, (4) No tests for correct extraction of AccountID from MPTID. Test coverage should ensure all validation branches throw as expected and that valid input is accepted.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation, with jss:: for JSON field names, Throw<> for error handling", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "v (input JSON)", + "location": "mptIssueFromJson", + "validated_by": "manual type check", + "validates": [ + "Checks that v is a JSON object" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "v (input JSON)", + "location": "mptIssueFromJson", + "validated_by": "manual field presence check", + "validates": [ + "Checks that v does NOT have 'currency' or 'issuer' fields" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Json::error (via Throw)", + "field": "mpt_issuance_id (in v)", + "location": "mptIssueFromJson", + "validated_by": "manual type check", + "validates": [ + "Checks that mpt_issuance_id is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "Json::error (via Throw)", + "field": "mpt_issuance_id (in v)", + "location": "mptIssueFromJson", + "validated_by": "MPTID::parseHex", + "validates": [ + "Checks that mpt_issuance_id string is a valid hex representation for MPTID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "compile-time error", + "field": "MPTID layout", + "location": "MPTIssue::getIssuer", + "validated_by": "static_assert", + "validates": [ + "Checks that MPTID is exactly sizeof(uint32_t) + sizeof(AccountID)" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/MPTIssue.cpp.ai.md b/src/libxrpl/protocol/MPTIssue.cpp.ai.md new file mode 100644 index 0000000000..20ab764c8a --- /dev/null +++ b/src/libxrpl/protocol/MPTIssue.cpp.ai.md @@ -0,0 +1,31 @@ +# `MPTIssue.cpp` — MPT Issuance Identity and Serialization + +## Role in the System + +`MPTIssue.cpp` implements the `MPTIssue` class, which wraps a single `MPTID` value to represent a Multi-Purpose Token (MPT) issuance on the XRP Ledger. Its purpose is to adapt the raw 192-bit `MPTID` type into an object with the same interface contract as the existing `Issue` class (which represents XRP or IOU currencies). This interface parity is the key architectural motivation: by sharing methods like `getIssuer()`, `native()`, `integral()`, `getText()`, and `setJson()`, `MPTIssue` can participate in the `Asset` variant and other static-polymorphism patterns throughout the ledger engine without requiring a rewrite of amount-handling code. + +## The `MPTID` Encoding + +`MPTID` is defined in `UintTypes.h` as `base_uint<192>` — a 192-bit fixed-width integer. Its binary layout is a direct concatenation of a 32-bit account sequence number followed by the 160-bit `AccountID` of the issuer. This encoding is not incidental; `Indexes.h` confirms it with the comment *"MPTID is a 192-bit concatenation of a 32-bit account sequence and a 160-bit account id."* The encoding lets a single opaque identifier carry both pieces of information needed to uniquely identify an issuance without any additional lookup. + +## Constructors + +Two constructors are provided. The primary one takes a pre-formed `MPTID` directly. The convenience overload accepts a `uint32_t sequence` and an `AccountID`, delegating to `xrpl::makeMptID(sequence, account)` (declared in `Indexes.h`) to perform the packing. The two-argument form exists so callers with the raw components don't have to manually invoke `makeMptID` before constructing the object. + +## Issuer Extraction via `reinterpret_cast` + +`getIssuer()` is the most low-level method in this file. It must recover the `AccountID` from the back 20 bytes of the packed `MPTID`. It does this with a `reinterpret_cast(mptID_.data() + sizeof(std::uint32_t))` and immediately dereferences the resulting pointer. This is exactly the kind of pointer aliasing that invites undefined behavior in C++ — but it is explicitly guarded by a compile-time `static_assert` that confirms `sizeof(MPTID) == sizeof(uint32_t) + sizeof(AccountID)`. The assert is both documentation and a guarantee: if the layout ever changes (e.g., padding is introduced), the build fails rather than silently reading the wrong bytes at runtime. + +It is worth noting that the header defines a standalone free function `getMPTIssuer()` which uses `std::copy_n` followed by `std::bit_cast` to achieve the same extraction. The `bit_cast` approach is stricter (it respects object-model rules, is constexpr-eligible) while the `reinterpret_cast` in the member method is older style but equally correct given the same `static_assert` guard. + +## JSON Serialization and Deserialization + +Serialization is straightforward: `setJson()` writes a single key `mpt_issuance_id` whose value is the hex string form of the `MPTID`. The standalone `to_json()` function constructs a fresh `Json::Value` object and delegates to `setJson()`. This split — a mutating `setJson` that writes into a caller-supplied object, plus a `to_json` wrapper that owns the object — lets higher-level code merge the MPT representation into a larger JSON structure without an intermediate copy. + +`mptIssueFromJson()` performs defensive parsing in a specific order. First it verifies the input is a `Json::Value` of object type, throwing `std::runtime_error` otherwise. Then it checks that neither `currency` nor `issuer` fields are present — a deliberate exclusion that guards against callers accidentally passing an IOU-style amount object instead of an MPT object. This is important because both types appear in quantity specifications throughout the protocol's JSON API, and the distinction matters. The third check ensures `mpt_issuance_id` is present and is a string type. Finally, `MPTID::parseHex()` validates that the string is a well-formed 192-bit hex encoding. + +Two distinct exception types are used: `std::runtime_error` covers structural problems (wrong JSON type, forbidden fields), while `Json::error` is reserved for field-level format failures (wrong value type, invalid hex). This separation lets callers that need to distinguish parse errors from protocol misuse catch at the right level. + +## Interface Contracts for Polymorphism + +`native()` always returns `false` and `integral()` always returns `true`. These are not trivial stubs — they carry semantic meaning that drives behavior elsewhere. `native()` returning `false` distinguishes MPT amounts from XRP in generic code. `integral()` returning `true` signals that MPT quantities are whole-number values (no fractional units), which affects amount arithmetic and display in the `STAmount` system. The implicit conversion `operator MPTID const&()` allows an `MPTIssue` to be passed wherever a raw `MPTID` is expected, reducing friction at call sites that deal with the underlying identifier directly. \ No newline at end of file diff --git a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp.ai.json b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp.ai.json new file mode 100644 index 0000000000..8866db4744 --- /dev/null +++ b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp.ai.json @@ -0,0 +1,474 @@ +{ + "args": [ + { + "lineno": 11, + "name": "response" + }, + { + "lineno": 12, + "name": "transaction" + }, + { + "lineno": 13, + "name": "transactionMeta" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "insertNFTSyntheticInJson", + "insertNFTokenID", + "insertNFTokenOfferID" + ], + "entry_point": "insertNFTSyntheticInJson", + "purpose": "Augments the JSON response with synthetic NFT fields by extracting and validating relevant data from the transaction and transaction meta.", + "validation_points": [ + "insertNFTokenID: Validates and inserts NFTokenID fields", + "insertNFTokenOfferID: Validates and inserts NFTokenOfferID fields" + ] + } + ], + "data_flows": [ + { + "field": "transaction", + "flow": [ + "insertNFTSyntheticInJson parameter", + "passed to insertNFTokenID", + "passed to insertNFTokenOfferID" + ], + "origin": "std::shared_ptr passed to insertNFTSyntheticInJson", + "transformations": [ + "Read for NFT-related fields", + "Potential validation of structure/content in downstream functions" + ], + "validated_at": "insertNFTokenID, insertNFTokenOfferID" + }, + { + "field": "transactionMeta", + "flow": [ + "insertNFTSyntheticInJson parameter", + "passed to insertNFTokenID", + "passed to insertNFTokenOfferID" + ], + "origin": "TxMeta const& passed to insertNFTSyntheticInJson", + "transformations": [ + "Read for meta fields relevant to NFTs", + "Potential validation of meta structure/content" + ], + "validated_at": "insertNFTokenID, insertNFTokenOfferID" + }, + { + "field": "response[jss::meta]", + "flow": [ + "response[jss::meta] in insertNFTSyntheticInJson", + "modified by insertNFTokenID", + "modified by insertNFTokenOfferID" + ], + "origin": "Json::Value& response (meta field)", + "transformations": [ + "NFT fields inserted/augmented based on validated transaction/meta" + ], + "validated_at": "insertNFTokenID, insertNFTokenOfferID" + }, + { + "field": "NFTokenID fields", + "flow": [ + "insertNFTokenID", + "validated and inserted into response[jss::meta]" + ], + "origin": "Extracted from transaction and/or transactionMeta", + "transformations": [ + "Validation of field format/content", + "Insertion into JSON" + ], + "validated_at": "insertNFTokenID" + }, + { + "field": "NFTokenOfferID fields", + "flow": [ + "insertNFTokenOfferID", + "validated and inserted into response[jss::meta]" + ], + "origin": "Extracted from transaction and/or transactionMeta", + "transformations": [ + "Validation of field format/content", + "Insertion into JSON" + ], + "validated_at": "insertNFTokenOfferID" + } + ], + "description": "Provides a utility function to insert synthetic NFT-related fields into a JSON response based on a transaction and its metadata, within the xrpl::RPC namespace.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "transaction (type-checked by C++ type system)", + "validation", + "missing", + "check" + ], + "evidence": "Field transaction (type-checked by C++ type system) validated by jss:: (JSON field keys), Json::Value (jsoncpp), C++ type system", + "issue_pattern": "Missing validation for transaction (type-checked by C++ type system)", + "why_false_positive": "jss:: (JSON field keys), Json::Value (jsoncpp), C++ type system validates transaction (type-checked by C++ type system) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "transactionMeta (type-checked by C++ type system)", + "validation", + "missing", + "check" + ], + "evidence": "Field transactionMeta (type-checked by C++ type system) validated by jss:: (JSON field keys), Json::Value (jsoncpp), C++ type system", + "issue_pattern": "Missing validation for transactionMeta (type-checked by C++ type system)", + "why_false_positive": "jss:: (JSON field keys), Json::Value (jsoncpp), C++ type system validates transactionMeta (type-checked by C++ type system) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "response[jss::meta] (created/validated by Json::Value and jss:: key)", + "validation", + "missing", + "check" + ], + "evidence": "Field response[jss::meta] (created/validated by Json::Value and jss:: key) validated by jss:: (JSON field keys), Json::Value (jsoncpp), C++ type system", + "issue_pattern": "Missing validation for response[jss::meta] (created/validated by Json::Value and jss:: key)", + "why_false_positive": "jss:: (JSON field keys), Json::Value (jsoncpp), C++ type system validates response[jss::meta] (created/validated by Json::Value and jss:: key) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "transaction", + "empty", + "string", + "validation" + ], + "evidence": "std::shared_ptr at insertNFTSyntheticInJson (parameter type)", + "issue_pattern": "Missing empty string validation for transaction", + "why_false_positive": "std::shared_ptr validates transaction for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "transaction", + "type", + "validation", + "check" + ], + "evidence": "std::shared_ptr at insertNFTSyntheticInJson (parameter type)", + "issue_pattern": "Missing type validation for transaction", + "why_false_positive": "std::shared_ptr validates transaction type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "transactionMeta", + "empty", + "string", + "validation" + ], + "evidence": "TxMeta (const reference) at insertNFTSyntheticInJson (parameter type)", + "issue_pattern": "Missing empty string validation for transactionMeta", + "why_false_positive": "TxMeta (const reference) validates transactionMeta for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "transactionMeta", + "type", + "validation", + "check" + ], + "evidence": "TxMeta (const reference) at insertNFTSyntheticInJson (parameter type)", + "issue_pattern": "Missing type validation for transactionMeta", + "why_false_positive": "TxMeta (const reference) validates transactionMeta type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.6, + "detection_keywords": [ + "response[jss::meta]", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value (via jss::meta key) at insertNFTSyntheticInJson (line: response[jss::meta])", + "issue_pattern": "Missing empty string validation for response[jss::meta]", + "why_false_positive": "Json::Value (via jss::meta key) validates response[jss::meta] for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "response[jss::meta]", + "format", + "validation", + "invalid" + ], + "evidence": "Json::Value (via jss::meta key) at insertNFTSyntheticInJson (line: response[jss::meta])", + "issue_pattern": "Missing format validation for response[jss::meta]", + "why_false_positive": "Json::Value (via jss::meta key) validates response[jss::meta] format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.5, + "detection_keywords": [ + "NFTokenID fields (implied)", + "empty", + "string", + "validation" + ], + "evidence": "insertNFTokenID (external function) at insertNFTokenID call", + "issue_pattern": "Missing empty string validation for NFTokenID fields (implied)", + "why_false_positive": "insertNFTokenID (external function) validates NFTokenID fields (implied) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.5, + "detection_keywords": [ + "NFTokenOfferID fields (implied)", + "empty", + "string", + "validation" + ], + "evidence": "insertNFTokenOfferID (external function) at insertNFTokenOfferID call", + "issue_pattern": "Missing empty string validation for NFTokenOfferID fields (implied)", + "why_false_positive": "insertNFTokenOfferID (external function) validates NFTokenOfferID fields (implied) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/NFTSyntheticSerializer.cpp", + "functions": [ + { + "args": [ + "response", + "transaction", + "transactionMeta" + ], + "lineno": 10, + "name": "insertNFTSyntheticInJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "RPC" + } + ], + "test_coverage_notes": "Direct unit tests for insertNFTSyntheticInJson are likely in files such as test/unit/protocol/NFTSyntheticSerializer_test.cpp or similar. However, since insertNFTSyntheticInJson is a thin wrapper delegating to insertNFTokenID and insertNFTokenOfferID, most validation logic is tested via those functions' tests. Gaps may exist if there are no integration tests covering the full call chain with real transaction/meta objects, or if edge cases (malformed transactions, missing fields) are not tested. No explicit test references are present in this file.", + "validation_architecture": { + "auto_validated_fields": [ + "transaction (type-checked by C++ type system)", + "transactionMeta (type-checked by C++ type system)", + "response[jss::meta] (created/validated by Json::Value and jss:: key)" + ], + "framework": "jss:: (JSON field keys), Json::Value (jsoncpp), C++ type system", + "validation_layer": "business_logic (function is not entry point, but part of business logic serialization)" + }, + "validations": [ + { + "confidence": 0.7, + "error_thrown": "std::bad_cast or nullptr dereference (if used improperly)", + "field": "transaction", + "location": "insertNFTSyntheticInJson (parameter type)", + "validated_by": "std::shared_ptr", + "validates": [ + "pointer is of correct type", + "const-correctness enforced" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "compile-time type error", + "field": "transactionMeta", + "location": "insertNFTSyntheticInJson (parameter type)", + "validated_by": "TxMeta (const reference)", + "validates": [ + "object is of type TxMeta" + ], + "validation_type": "type" + }, + { + "confidence": 0.6, + "error_thrown": "Json::LogicError (if misused, e.g., invalid key type)", + "field": "response[jss::meta]", + "location": "insertNFTSyntheticInJson (line: response[jss::meta])", + "validated_by": "Json::Value (via jss::meta key)", + "validates": [ + "meta field exists or is created in response", + "field is a valid Json::Value" + ], + "validation_type": "format" + }, + { + "confidence": 0.5, + "error_thrown": "unknown (depends on insertNFTokenID implementation)", + "field": "NFTokenID fields (implied)", + "location": "insertNFTokenID call", + "validated_by": "insertNFTokenID (external function)", + "validates": [ + "NFTokenID is valid and serializable", + "fields conform to expected format" + ], + "validation_type": "format|type|business_logic (assumed from function purpose)" + }, + { + "confidence": 0.5, + "error_thrown": "unknown (depends on insertNFTokenOfferID implementation)", + "field": "NFTokenOfferID fields (implied)", + "location": "insertNFTokenOfferID call", + "validated_by": "insertNFTokenOfferID (external function)", + "validates": [ + "NFTokenOfferID is valid and serializable", + "fields conform to expected format" + ], + "validation_type": "format|type|business_logic (assumed from function purpose)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/NFTSyntheticSerializer.cpp.ai.md b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp.ai.md new file mode 100644 index 0000000000..d1cfeb5cf1 --- /dev/null +++ b/src/libxrpl/protocol/NFTSyntheticSerializer.cpp.ai.md @@ -0,0 +1,38 @@ +# `NFTSyntheticSerializer.cpp` + +## Role and Purpose + +This file defines a single aggregating function, `insertNFTSyntheticInJson`, that sits inside the `xrpl::RPC` namespace and serves as the unified entry point for enriching a transaction JSON response with NFT-related fields that the XRPL ledger does not store directly. These fields are called "synthetic" because they are derived at query time by analyzing transaction metadata rather than being recorded as first-class ledger fields. + +The function is called by RPC handlers such as `Tx.cpp` immediately after writing the raw metadata to the response: + +```cpp +response[jss::meta] = meta->getJson(JsonOptions::none); +insertDeliveredAmount(response[jss::meta], context, result.txn, *meta); +RPC::insertNFTSyntheticInJson(response, sttx, *meta); +RPC::insertMPTokenIssuanceID(response[jss::meta], sttx, *meta); +``` + +This placement shows that `insertNFTSyntheticInJson` is part of a post-processing pipeline that layers derived context onto an already-serialized transaction response. + +## What It Computes + +The function delegates to two independent subsystems: + +**`insertNFTokenID`** (from `NFTokenID.cpp`) adds `nftoken_id` or `nftoken_ids` to `response[jss::meta]` for successful `NFTokenMint`, `NFTokenAcceptOffer`, and `NFTokenCancelOffer` transactions. The derivation is non-trivial: because the ledger stores NFTs packed into page objects rather than as individual entries, the function must compare the pre-transaction and post-transaction NFToken arrays across all affected ledger nodes in the metadata to identify which token was created or affected by the operation. For `NFTokenCancelOffer`, it scans deleted `NFTokenOffer` nodes and returns a deduplicated array of affected token IDs. + +**`insertNFTokenOfferID`** (from `NFTokenOfferID.cpp`) adds `offer_id` to `response[jss::meta]` for successful `NFTokenCreateOffer` transactions (and `NFTokenMint` transactions that include an `sfAmount` field, i.e., mints that create an immediate sell offer). It locates the newly created `NFTokenOffer` ledger node in the metadata and extracts its `sfLedgerIndex` as the offer identifier. + +Both helpers perform their own eligibility check (`canHaveNFTokenID`, `canHaveNFTokenOfferID`) that gates on transaction type and `tesSUCCESS`, meaning failed transactions produce no synthetic output at all. + +## Design Rationale + +The split into three files — this compositor plus two dedicated modules — reflects deliberate design for reusability. The comment in both `NFTokenID.h` and `NFTokenOfferID.h` explicitly states: *"Helper functions are not static because they can be used by Clio."* Clio is the XRPL History API server, which parses ledger data independently of `rippled`. By keeping the computation logic in header-exposed, non-static functions under `xrpl::` (not `xrpl::RPC::`), those helpers can be called directly by Clio without pulling in the RPC coupling of `insertNFTSyntheticInJson`. + +This file therefore plays the role of a composition point: it belongs to `xrpl::RPC` because its job is to mutate a JSON response object intended for external API consumers, while the underlying extraction logic belongs to the broader `xrpl::` namespace where it is accessible to any consumer of the library. + +## Input Handling and Safety + +The function takes `response` by non-const reference and writes into `response[jss::meta]`, which `Json::Value` creates on demand if absent. The `transaction` parameter is a `std::shared_ptr`, and both delegate functions guard against a null pointer at their first line. The `transactionMeta` is passed by const reference, providing safe read-only access. There is no explicit error handling at this layer because both delegates are designed to be no-ops when they cannot produce a meaningful result — they return early rather than throwing. + +As a result, `insertNFTSyntheticInJson` itself is unconditionally safe to call for any transaction type: non-NFT transactions simply produce no output because all eligibility checks inside the delegates will fail, and the response object is left unmodified for those fields. \ No newline at end of file diff --git a/src/libxrpl/protocol/NFTokenID.cpp.ai.json b/src/libxrpl/protocol/NFTokenID.cpp.ai.json new file mode 100644 index 0000000000..3bb2007775 --- /dev/null +++ b/src/libxrpl/protocol/NFTokenID.cpp.ai.json @@ -0,0 +1,669 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "canHaveNFTokenID" + ], + "entry_point": "canHaveNFTokenID", + "purpose": "Determines if a transaction and its metadata are eligible to have an NFTokenID (i.e., if the transaction is of the correct type and succeeded).", + "validation_points": [ + "serializedTx null check", + "serializedTx->getTxnType() enum check", + "transactionMeta.getResultTER() isTesSuccess check" + ] + }, + { + "call_chain": [ + "getNFTokenIDFromPage" + ], + "entry_point": "getNFTokenIDFromPage", + "purpose": "Extracts the NFTokenID that was added in a transaction by comparing previous and final NFT states in affected ledger nodes.", + "validation_points": [ + "node.getFieldU16(sfLedgerEntryType) enum check", + "node.getFName() enum check", + "previousFields.isFieldPresent(sfNFTokens) check", + "finalIDs.size() != prevIDs.size() + 1 check" + ] + }, + { + "call_chain": [ + "getNFTokenIDFromDeletedOffer" + ], + "entry_point": "getNFTokenIDFromDeletedOffer", + "purpose": "Extracts the NFTokenID from a deleted offer node in transaction metadata.", + "validation_points": [ + "node.getFieldU16(sfLedgerEntryType) enum check", + "node.getFName() enum check" + ] + }, + { + "call_chain": [ + "insertNFTokenID" + ], + "entry_point": "insertNFTokenID", + "purpose": "Inserts an NFTokenID into a JSON object if present.", + "validation_points": [ + "nftokenID.has_value() check" + ] + } + ], + "data_flows": [ + { + "field": "serializedTx", + "flow": [ + "Function argument", + "Null check", + "getTxnType()", + "Used for transaction type validation" + ], + "origin": "Function argument to canHaveNFTokenID", + "transformations": [ + "Dereferenced if not null", + "Transaction type extracted" + ], + "validated_at": "Null check at function start" + }, + { + "field": "TxType (from serializedTx->getTxnType())", + "flow": [ + "Extracted from serializedTx", + "Compared to allowed types (ttNFTOKEN_MINT, ttNFTOKEN_ACCEPT_OFFER, ttNFTOKEN_CANCEL_OFFER)" + ], + "origin": "serializedTx", + "transformations": [ + "Enum comparison" + ], + "validated_at": "Immediately after extraction" + }, + { + "field": "transactionMeta.getResultTER()", + "flow": [ + "Extracted from transactionMeta", + "Passed to isTesSuccess()" + ], + "origin": "transactionMeta", + "transformations": [ + "Boolean success/failure check" + ], + "validated_at": "Immediately after extraction" + }, + { + "field": "node.getFieldU16(sfLedgerEntryType)", + "flow": [ + "Extracted from node", + "Compared to ltNFTOKEN_PAGE (or ltOFFER in getNFTokenIDFromDeletedOffer)" + ], + "origin": "Each node in transactionMeta.getNodes()", + "transformations": [ + "Enum comparison" + ], + "validated_at": "At start of each node-processing loop" + }, + { + "field": "node.getFName()", + "flow": [ + "Extracted from node", + "Compared to sfCreatedNode, sfModifiedNode, or sfDeletedNode" + ], + "origin": "Each node in transactionMeta.getNodes()", + "transformations": [ + "Enum comparison" + ], + "validated_at": "After ledger entry type check" + }, + { + "field": "NFTokens arrays", + "flow": [ + "Extracted from node fields", + "Downcast to STObject", + "getFieldArray(sfNFTokens)", + "Transformed into vector via std::transform" + ], + "origin": "STObject fields in node (sfNewFields, sfPreviousFields, sfFinalFields)", + "transformations": [ + "Downcast", + "Array extraction", + "Mapping to uint256" + ], + "validated_at": "previousFields.isFieldPresent(sfNFTokens) check" + }, + { + "field": "finalIDs and prevIDs", + "flow": [ + "Built from NFTokens arrays", + "Compared in size", + "Compared element-wise to find new NFTokenID" + ], + "origin": "Vectors built from NFTokens arrays", + "transformations": [ + "Vector construction", + "Size and content comparison" + ], + "validated_at": "finalIDs.size() != prevIDs.size() + 1 check" + } + ], + "description": "This file provides utility functions for extracting NFTokenID(s) from transaction metadata in the XRPL protocol, specifically for NFT mint, accept offer, and cancel offer transactions. It analyzes affected ledger nodes to determine which NFT(s) were created, accepted, or canceled, and inserts the relevant NFTokenID(s) into a JSON response.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "STObject field types (via downcast, getFieldArray, getFieldH256)", + "validation", + "missing", + "check" + ], + "evidence": "Field STObject field types (via downcast, getFieldArray, getFieldH256) validated by xrpl protocol/STObject/STArray/STTx (custom type system), possible jss:: for JSON validation elsewhere", + "issue_pattern": "Missing validation for STObject field types (via downcast, getFieldArray, getFieldH256)", + "why_false_positive": "xrpl protocol/STObject/STArray/STTx (custom type system), possible jss:: for JSON validation elsewhere validates STObject field types (via downcast, getFieldArray, getFieldH256) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Transaction type (via getTxnType)", + "validation", + "missing", + "check" + ], + "evidence": "Field Transaction type (via getTxnType) validated by xrpl protocol/STObject/STArray/STTx (custom type system), possible jss:: for JSON validation elsewhere", + "issue_pattern": "Missing validation for Transaction type (via getTxnType)", + "why_false_positive": "xrpl protocol/STObject/STArray/STTx (custom type system), possible jss:: for JSON validation elsewhere validates Transaction type (via getTxnType) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Transaction result code (via isTesSuccess)", + "validation", + "missing", + "check" + ], + "evidence": "Field Transaction result code (via isTesSuccess) validated by xrpl protocol/STObject/STArray/STTx (custom type system), possible jss:: for JSON validation elsewhere", + "issue_pattern": "Missing validation for Transaction result code (via isTesSuccess)", + "why_false_positive": "xrpl protocol/STObject/STArray/STTx (custom type system), possible jss:: for JSON validation elsewhere validates Transaction result code (via isTesSuccess) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "serializedTx (transaction pointer)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at canHaveNFTokenID", + "issue_pattern": "Missing empty string validation for serializedTx (transaction pointer)", + "why_false_positive": "explicit null check validates serializedTx (transaction pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "serializedTx (transaction pointer)", + "type", + "validation", + "check" + ], + "evidence": "explicit null check at canHaveNFTokenID", + "issue_pattern": "Missing type validation for serializedTx (transaction pointer)", + "why_false_positive": "explicit null check validates serializedTx (transaction pointer) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "serializedTx->getTxnType()", + "empty", + "string", + "validation" + ], + "evidence": "explicit enum comparison at canHaveNFTokenID", + "issue_pattern": "Missing empty string validation for serializedTx->getTxnType()", + "why_false_positive": "explicit enum comparison validates serializedTx->getTxnType() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transactionMeta.getResultTER()", + "empty", + "string", + "validation" + ], + "evidence": "isTesSuccess function at canHaveNFTokenID", + "issue_pattern": "Missing empty string validation for transactionMeta.getResultTER()", + "why_false_positive": "isTesSuccess function validates transactionMeta.getResultTER() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node.getFieldU16(sfLedgerEntryType)", + "empty", + "string", + "validation" + ], + "evidence": "explicit enum comparison at getNFTokenIDFromPage", + "issue_pattern": "Missing empty string validation for node.getFieldU16(sfLedgerEntryType)", + "why_false_positive": "explicit enum comparison validates node.getFieldU16(sfLedgerEntryType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node.getFName()", + "empty", + "string", + "validation" + ], + "evidence": "explicit enum comparison at getNFTokenIDFromPage", + "issue_pattern": "Missing empty string validation for node.getFName()", + "why_false_positive": "explicit enum comparison validates node.getFName() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "previousFields.isFieldPresent(sfNFTokens)", + "empty", + "string", + "validation" + ], + "evidence": "isFieldPresent method at getNFTokenIDFromPage", + "issue_pattern": "Missing empty string validation for previousFields.isFieldPresent(sfNFTokens)", + "why_false_positive": "isFieldPresent method validates previousFields.isFieldPresent(sfNFTokens) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "node.peekAtField(sfNewFields).downcast()", + "empty", + "string", + "validation" + ], + "evidence": "downcast() at getNFTokenIDFromPage", + "issue_pattern": "Missing empty string validation for node.peekAtField(sfNewFields).downcast()", + "why_false_positive": "downcast() validates node.peekAtField(sfNewFields).downcast() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "node.peekAtField(sfNewFields).downcast()", + "type", + "validation", + "check" + ], + "evidence": "downcast() at getNFTokenIDFromPage", + "issue_pattern": "Missing type validation for node.peekAtField(sfNewFields).downcast()", + "why_false_positive": "downcast() validates node.peekAtField(sfNewFields).downcast() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "node.peekAtField(sfPreviousFields).downcast()", + "empty", + "string", + "validation" + ], + "evidence": "downcast() at getNFTokenIDFromPage", + "issue_pattern": "Missing empty string validation for node.peekAtField(sfPreviousFields).downcast()", + "why_false_positive": "downcast() validates node.peekAtField(sfPreviousFields).downcast() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "node.peekAtField(sfPreviousFields).downcast()", + "type", + "validation", + "check" + ], + "evidence": "downcast() at getNFTokenIDFromPage", + "issue_pattern": "Missing type validation for node.peekAtField(sfPreviousFields).downcast()", + "why_false_positive": "downcast() validates node.peekAtField(sfPreviousFields).downcast() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "toAddPrevNFTs.getFieldArray(sfNFTokens)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldArray at getNFTokenIDFromPage", + "issue_pattern": "Missing empty string validation for toAddPrevNFTs.getFieldArray(sfNFTokens)", + "why_false_positive": "getFieldArray validates toAddPrevNFTs.getFieldArray(sfNFTokens) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "nft.getFieldH256(sfNFTokenID)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldH256 at getNFTokenIDFromPage (lambda in std::transform)", + "issue_pattern": "Missing empty string validation for nft.getFieldH256(sfNFTokenID)", + "why_false_positive": "getFieldH256 validates nft.getFieldH256(sfNFTokenID) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/NFTokenID.cpp", + "functions": [ + { + "args": [ + "serializedTx", + "transactionMeta" + ], + "lineno": 13, + "name": "canHaveNFTokenID" + }, + { + "args": [ + "transactionMeta" + ], + "lineno": 25, + "name": "getNFTokenIDFromPage" + }, + { + "args": [ + "transactionMeta" + ], + "lineno": 74, + "name": "getNFTokenIDFromDeletedOffer" + }, + { + "args": [ + "response", + "transaction", + "transactionMeta" + ], + "lineno": 94, + "name": "insertNFTokenID" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "The functions in NFTokenID.cpp are typically tested via integration and unit tests that exercise NFT minting, offer creation/cancellation, and metadata parsing. Likely test files include: 'test/ledger/NFToken_test.cpp', 'test/app/tx/impl/NFToken_test.cpp', and possibly 'test/protocol/TxMeta_test.cpp'. These would cover positive and negative cases for transaction types, metadata parsing, and error handling. Gaps may exist in edge cases for malformed metadata, rare node types, or unusual ledger states. Direct unit tests for each validation branch (e.g., all enum mismatches, null pointers, and size mismatches) should be verified.", + "validation_architecture": { + "auto_validated_fields": [ + "STObject field types (via downcast, getFieldArray, getFieldH256)", + "Transaction type (via getTxnType)", + "Transaction result code (via isTesSuccess)" + ], + "framework": "xrpl protocol/STObject/STArray/STTx (custom type system), possible jss:: for JSON validation elsewhere", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "serializedTx (transaction pointer)", + "location": "canHaveNFTokenID", + "validated_by": "explicit null check", + "validates": [ + "Checks if serializedTx is not null before use" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "serializedTx->getTxnType()", + "location": "canHaveNFTokenID", + "validated_by": "explicit enum comparison", + "validates": [ + "Checks if transaction type is one of: ttNFTOKEN_MINT, ttNFTOKEN_ACCEPT_OFFER, ttNFTOKEN_CANCEL_OFFER" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "transactionMeta.getResultTER()", + "location": "canHaveNFTokenID", + "validated_by": "isTesSuccess function", + "validates": [ + "Checks if transaction result code is a success code" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "continues loop (no exception)", + "field": "node.getFieldU16(sfLedgerEntryType)", + "location": "getNFTokenIDFromPage", + "validated_by": "explicit enum comparison", + "validates": [ + "Checks if ledger entry type is ltNFTOKEN_PAGE" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "continues loop (no exception)", + "field": "node.getFName()", + "location": "getNFTokenIDFromPage", + "validated_by": "explicit enum comparison", + "validates": [ + "Checks if node is sfCreatedNode or sfModifiedNode" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "continues loop (no exception)", + "field": "previousFields.isFieldPresent(sfNFTokens)", + "location": "getNFTokenIDFromPage", + "validated_by": "isFieldPresent method", + "validates": [ + "Checks if sfNFTokens field is present in previousFields" + ], + "validation_type": "presence" + }, + { + "confidence": 0.8, + "error_thrown": "likely assertion or exception if downcast fails (implementation-dependent)", + "field": "node.peekAtField(sfNewFields).downcast()", + "location": "getNFTokenIDFromPage", + "validated_by": "downcast()", + "validates": [ + "Ensures sfNewFields is an STObject" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "likely assertion or exception if downcast fails (implementation-dependent)", + "field": "node.peekAtField(sfPreviousFields).downcast()", + "location": "getNFTokenIDFromPage", + "validated_by": "downcast()", + "validates": [ + "Ensures sfPreviousFields is an STObject" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "likely assertion or exception if field missing (implementation-dependent)", + "field": "toAddPrevNFTs.getFieldArray(sfNFTokens)", + "location": "getNFTokenIDFromPage", + "validated_by": "getFieldArray", + "validates": [ + "Ensures sfNFTokens is present and is an array" + ], + "validation_type": "type/presence" + }, + { + "confidence": 0.8, + "error_thrown": "likely assertion or exception if field missing or wrong type (implementation-dependent)", + "field": "nft.getFieldH256(sfNFTokenID)", + "location": "getNFTokenIDFromPage (lambda in std::transform)", + "validated_by": "getFieldH256", + "validates": [ + "Ensures sfNFTokenID is present and is a 256-bit hash" + ], + "validation_type": "type/presence/format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/NFTokenID.cpp.ai.md b/src/libxrpl/protocol/NFTokenID.cpp.ai.md new file mode 100644 index 0000000000..8794f05968 --- /dev/null +++ b/src/libxrpl/protocol/NFTokenID.cpp.ai.md @@ -0,0 +1,31 @@ +# `src/libxrpl/protocol/NFTokenID.cpp` + +## Purpose + +This file solves a specific gap in how the XRPL ledger records NFT transactions: the raw transaction metadata does not directly identify which NFT was minted, traded, or involved in a cancelled offer. It records ledger state changes — what the affected `NFTokenPage` objects looked like before and after the transaction — but does not annotate "this is the new token." The functions here bridge that gap by reconstructing the NFToken identity from first principles, then injecting it into the JSON response so that API consumers don't have to perform the same inference themselves. + +The public header explicitly notes that the helper functions are not `static` because they are also consumed by **Clio**, the external XRPL history node, which performs the same enrichment independently of `rippled`. This is a deliberate API boundary, not just internal decomposition. + +## Core Problem: The Missing Token ID + +When `ttNFTOKEN_MINT` succeeds, it adds one entry to a `NFTokenPage` ledger object. The transaction metadata records what the page's token list looked like in `sfPreviousFields` and `sfFinalFields`, but both contain every token in the page, not just the new one. The ledger format does not mark the inserted token explicitly. + +`getNFTokenIDFromPage()` resolves this with a set-difference approach. It iterates every metadata node, collecting all `uint256` token IDs from previous states into `prevIDs` and from final states into `finalIDs`. After the loop it asserts the invariant: `finalIDs.size() == prevIDs.size() + 1`. If that doesn't hold — meaning something unexpected happened — the function returns `std::nullopt` rather than guessing. When the sizes do match, `std::mismatch` finds the first position where the sorted-by-construction sequences diverge; the entry in `finalIDs` at that position is the newly minted token. + +There is a subtle edge case handled inside the loop: when a mint causes an existing page to split, the resulting linked-list rewiring may produce a `sfModifiedNode` for a third page whose `sfPreviousFields` doesn't include `sfNFTokens` at all — only its `NextPageMin` or `PreviousPageMin` pointers changed. The code guards against this with `previousFields.isFieldPresent(sfNFTokens)` before attempting to extract the token array, skipping such nodes silently. Without this guard, the size invariant check would incorrectly fail and return `std::nullopt` for legitimate mints. + +## Offer-Based Extraction + +For `ttNFTOKEN_ACCEPT_OFFER` and `ttNFTOKEN_CANCEL_OFFER`, the token identity is recoverable more directly. Both transaction types delete `ltNFTOKEN_OFFER` ledger objects, and each offer's `sfFinalFields` carries the `sfNFTokenID` it was created for. `getNFTokenIDFromDeletedOffer()` scans all metadata nodes for `sfDeletedNode` entries of type `ltNFTOKEN_OFFER` and collects those token IDs. + +The return type differs between the two call sites. `ttNFTOKEN_CANCEL_OFFER` can cancel many offers simultaneously, and multiple offers can reference the same NFT, so the function deduplicates with `sort` + `unique` + `erase` and returns a `std::vector`. `ttNFTOKEN_ACCEPT_OFFER` accepts exactly one offer, so `insertNFTokenID` uses only `result.front()`. The JSON output reflects this: mint and accept-offer inject a single `jss::nftoken_id` string, while cancel-offer injects a `jss::nftoken_ids` array. + +## Entry Point and Callsite + +`insertNFTokenID()` is the public entry point that ties everything together. It begins with `canHaveNFTokenID()`, which gates on three conditions: the transaction pointer is non-null, the transaction type is one of the three NFT types, and `isTesSuccess(transactionMeta.getResultTER())` is true. A failed transaction cannot have added or removed an NFT, so early return avoids spurious metadata diffs. + +In practice, `insertNFTokenID` is called from `insertNFTSyntheticInJson()` in `NFTSyntheticSerializer.cpp`, which also calls the sibling `insertNFTokenOfferID()`. Together they form the "synthetic" enrichment layer — fields that are added to the API response derived from metadata rather than existing directly in ledger objects. The JSON path they target is `response[jss::meta]`, meaning the enriched fields appear inside the `meta` sub-object of a transaction response. + +## Error Handling and Defensive Patterns + +All failure modes in the metadata-parsing functions produce graceful no-ops rather than exceptions. `getNFTokenIDFromPage()` returns `std::nullopt` if the size invariant is violated or if `std::mismatch` unexpectedly reaches the end of `finalIDs`. `insertNFTokenID()` simply omits the field from the response if no result is found; callers receive a valid but unenriched JSON object. No exceptions are thrown anywhere in this file. The `downcast()` calls on `sfNewFields`, `sfPreviousFields`, and `sfFinalFields` rely on the XRPL serialized-type system to enforce structural correctness; malformed metadata would throw at that layer, not here. \ No newline at end of file diff --git a/src/libxrpl/protocol/NFTokenOfferID.cpp.ai.json b/src/libxrpl/protocol/NFTokenOfferID.cpp.ai.json new file mode 100644 index 0000000000..913c8e985f --- /dev/null +++ b/src/libxrpl/protocol/NFTokenOfferID.cpp.ai.json @@ -0,0 +1,490 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "insertNFTokenOfferID", + "canHaveNFTokenOfferID", + "getOfferIDFromCreatedOffer" + ], + "entry_point": "insertNFTokenOfferID", + "purpose": "Determines if an NFToken offer ID should be inserted into a JSON response for a transaction, and if so, extracts and inserts it.", + "validation_points": [ + "canHaveNFTokenOfferID: Validates transaction pointer is not null", + "canHaveNFTokenOfferID: Validates transaction type is correct (ttNFTOKEN_MINT with sfAmount or ttNFTOKEN_CREATE_OFFER)", + "canHaveNFTokenOfferID: Validates transaction result is tesSUCCESS", + "getOfferIDFromCreatedOffer: Validates node type is ltNFTOKEN_OFFER and node is a CreatedNode", + "getOfferIDFromCreatedOffer: Validates presence and format of sfLedgerIndex" + ] + } + ], + "data_flows": [ + { + "field": "serializedTx (transaction)", + "flow": [ + "insertNFTokenOfferID argument", + "canHaveNFTokenOfferID argument", + "serializedTx->getTxnType() and isFieldPresent(sfAmount)" + ], + "origin": "Passed as argument to insertNFTokenOfferID (likely from transaction processing code)", + "transformations": [ + "Checked for null", + "Transaction type extracted", + "Field presence checked" + ], + "validated_at": "canHaveNFTokenOfferID" + }, + { + "field": "transactionMeta", + "flow": [ + "insertNFTokenOfferID argument", + "canHaveNFTokenOfferID argument", + "transactionMeta.getResultTER()", + "getOfferIDFromCreatedOffer argument", + "transactionMeta.getNodes()" + ], + "origin": "Passed as argument to insertNFTokenOfferID (from transaction processing code)", + "transformations": [ + "Result code extracted and checked for tesSUCCESS", + "Nodes iterated for CreatedNode of type ltNFTOKEN_OFFER" + ], + "validated_at": "canHaveNFTokenOfferID (result code), getOfferIDFromCreatedOffer (node type and field checks)" + }, + { + "field": "sfLedgerIndex", + "flow": [ + "getOfferIDFromCreatedOffer", + "node.getFieldH256(sfLedgerIndex)", + "returned as offer ID" + ], + "origin": "Field in STObject node from transactionMeta.getNodes()", + "transformations": [ + "Type/format checked by getFieldH256", + "Converted to string for JSON output" + ], + "validated_at": "getOfferIDFromCreatedOffer" + }, + { + "field": "response[jss::offer_id]", + "flow": [ + "insertNFTokenOfferID", + "set to to_string(result.value()) if offer ID found" + ], + "origin": "Json::Value response object passed to insertNFTokenOfferID", + "transformations": [ + "Offer ID converted to string and inserted into JSON" + ], + "validated_at": "insertNFTokenOfferID (only set if all prior validations pass)" + } + ], + "description": "Provides utility functions for handling NFTokenOfferID in XRPL transactions, including checking if a transaction can have an NFTokenOfferID, extracting the offer ID from transaction metadata, and inserting the offer ID into a JSON response.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLedgerEntryType", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLedgerEntryType validated by xrpl/protocol (STObject, STTx, SField, jss::)", + "issue_pattern": "Missing validation for sfLedgerEntryType", + "why_false_positive": "xrpl/protocol (STObject, STTx, SField, jss::) validates sfLedgerEntryType automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCreatedNode", + "validation", + "missing", + "check" + ], + "evidence": "Field sfCreatedNode validated by xrpl/protocol (STObject, STTx, SField, jss::)", + "issue_pattern": "Missing validation for sfCreatedNode", + "why_false_positive": "xrpl/protocol (STObject, STTx, SField, jss::) validates sfCreatedNode automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLedgerIndex", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLedgerIndex validated by xrpl/protocol (STObject, STTx, SField, jss::)", + "issue_pattern": "Missing validation for sfLedgerIndex", + "why_false_positive": "xrpl/protocol (STObject, STTx, SField, jss::) validates sfLedgerIndex automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAmount validated by xrpl/protocol (STObject, STTx, SField, jss::)", + "issue_pattern": "Missing validation for sfAmount", + "why_false_positive": "xrpl/protocol (STObject, STTx, SField, jss::) validates sfAmount automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "serializedTx (transaction)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at canHaveNFTokenOfferID", + "issue_pattern": "Missing empty string validation for serializedTx (transaction)", + "why_false_positive": "explicit null check validates serializedTx (transaction) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "serializedTx (transaction)", + "type", + "validation", + "check" + ], + "evidence": "explicit null check at canHaveNFTokenOfferID", + "issue_pattern": "Missing type validation for serializedTx (transaction)", + "why_false_positive": "explicit null check validates serializedTx (transaction) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "serializedTx->getTxnType()", + "empty", + "string", + "validation" + ], + "evidence": "explicit value check at canHaveNFTokenOfferID", + "issue_pattern": "Missing empty string validation for serializedTx->getTxnType()", + "why_false_positive": "explicit value check validates serializedTx->getTxnType() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transactionMeta.getResultTER()", + "empty", + "string", + "validation" + ], + "evidence": "isTesSuccess function at canHaveNFTokenOfferID", + "issue_pattern": "Missing empty string validation for transactionMeta.getResultTER()", + "why_false_positive": "isTesSuccess function validates transactionMeta.getResultTER() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transactionMeta.getNodes()", + "empty", + "string", + "validation" + ], + "evidence": "iteration and field checks at getOfferIDFromCreatedOffer", + "issue_pattern": "Missing empty string validation for transactionMeta.getNodes()", + "why_false_positive": "iteration and field checks validates transactionMeta.getNodes() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "node.getFieldH256(sfLedgerIndex)", + "empty", + "string", + "validation" + ], + "evidence": "type/format check via method at getOfferIDFromCreatedOffer", + "issue_pattern": "Missing empty string validation for node.getFieldH256(sfLedgerIndex)", + "why_false_positive": "type/format check via method validates node.getFieldH256(sfLedgerIndex) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result.has_value()", + "empty", + "string", + "validation" + ], + "evidence": "std::optional check at insertNFTokenOfferID", + "issue_pattern": "Missing empty string validation for result.has_value()", + "why_false_positive": "std::optional check validates result.has_value() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "result.has_value()", + "type", + "validation", + "check" + ], + "evidence": "std::optional check at insertNFTokenOfferID", + "issue_pattern": "Missing type validation for result.has_value()", + "why_false_positive": "std::optional check validates result.has_value() type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/NFTokenOfferID.cpp", + "functions": [ + { + "args": [ + "serializedTx", + "transactionMeta" + ], + "lineno": 11, + "name": "canHaveNFTokenOfferID" + }, + { + "args": [ + "transactionMeta" + ], + "lineno": 27, + "name": "getOfferIDFromCreatedOffer" + }, + { + "args": [ + "response", + "transaction", + "transactionMeta" + ], + "lineno": 43, + "name": "insertNFTokenOfferID" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by integration and unit tests for NFT offer creation and minting in the rippled codebase, especially those that check transaction metadata and JSON response fields. Tests would be found in files like 'test/tx/NFTokenCreateOffer_test.cpp', 'test/tx/NFTokenMint_test.cpp', or similar. However, direct unit tests for these specific utility functions (canHaveNFTokenOfferID, getOfferIDFromCreatedOffer, insertNFTokenOfferID) may not exist unless explicitly written. Edge cases such as null transactions, failed transactions, or malformed nodes may not be fully covered unless negative tests are present. Template-based validation is not directly tested here but would be exercised via higher-level transaction tests.", + "validation_architecture": { + "auto_validated_fields": [ + "sfLedgerEntryType", + "sfCreatedNode", + "sfLedgerIndex", + "sfAmount" + ], + "framework": "xrpl/protocol (STObject, STTx, SField, jss::)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "serializedTx (transaction)", + "location": "canHaveNFTokenOfferID", + "validated_by": "explicit null check", + "validates": [ + "checks if transaction pointer is not null" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "serializedTx->getTxnType()", + "location": "canHaveNFTokenOfferID", + "validated_by": "explicit value check", + "validates": [ + "checks if transaction type is ttNFTOKEN_MINT and has sfAmount field, or is ttNFTOKEN_CREATE_OFFER" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "transactionMeta.getResultTER()", + "location": "canHaveNFTokenOfferID", + "validated_by": "isTesSuccess function", + "validates": [ + "checks if transaction result code is a success code" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "transactionMeta.getNodes()", + "location": "getOfferIDFromCreatedOffer", + "validated_by": "iteration and field checks", + "validates": [ + "checks each node for sfLedgerEntryType == ltNFTOKEN_OFFER", + "checks node FName == sfCreatedNode" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "returns std::nullopt (no exception)", + "field": "node.getFieldH256(sfLedgerIndex)", + "location": "getOfferIDFromCreatedOffer", + "validated_by": "type/format check via method", + "validates": [ + "retrieves 256-bit hash field, expects correct type/format" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "does not insert field if not present (no exception)", + "field": "result.has_value()", + "location": "insertNFTokenOfferID", + "validated_by": "std::optional check", + "validates": [ + "checks if offer ID was found before inserting" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/NFTokenOfferID.cpp.ai.md b/src/libxrpl/protocol/NFTokenOfferID.cpp.ai.md new file mode 100644 index 0000000000..588fcde0f1 --- /dev/null +++ b/src/libxrpl/protocol/NFTokenOfferID.cpp.ai.md @@ -0,0 +1,42 @@ +# `NFTokenOfferID.cpp` — Synthetic Offer ID Injection for NFToken Transactions + +This file provides three utility functions that together inject a synthetic `offer_id` field into RPC JSON responses for transactions that create NFToken offers. The ID is not stored in the transaction itself — it is derived at query time by scanning the transaction's metadata — making these functions essential for API consumers who need to know which ledger object was created without having to manually parse the affected-nodes list. + +## Why Synthetic Fields Exist + +The XRPL canonical transaction format records only the inputs to a transaction, not the outputs. When a transaction creates a new ledger object, the key of that object is determined during consensus processing and encoded only in the transaction's metadata (`TxMeta`). For NFToken offers specifically, clients need the offer's ledger index (its `uint256` key) to subsequently accept or cancel it. Rather than requiring every API consumer to walk the `AffectedNodes` array themselves, `insertNFTokenOfferID` does this extraction and attaches the result as `offer_id` inside the `meta` JSON object. + +The header comment is explicit that the helper functions are deliberately non-static so that **Clio** — a separate read-optimized XRP Ledger data server — can reuse them directly. Static linkage would break that use case; this is a cross-component design constraint captured in the interface documentation. + +## Transaction Eligibility: `canHaveNFTokenOfferID` + +The guard function enforces three conditions in sequence, returning `false` immediately on any failure: + +1. **Null pointer check** — the `serializedTx` `shared_ptr` is tested before any dereference. This is the only defensive null check needed because `shared_ptr` construction does not guarantee a non-null stored pointer. +2. **Transaction type check** — only `ttNFTOKEN_CREATE_OFFER` always qualifies. `ttNFTOKEN_MINT` qualifies only when `sfAmount` is present, because that field signals that the mint transaction also creates a buy offer for immediate sale. Other mint transactions do not create an offer object and thus can never have an `offer_id`. +3. **Success check** — `isTesSuccess(transactionMeta.getResultTER())` filters out failed transactions. A failed transaction never modifies the ledger, so no offer object could have been created regardless of type. + +The combination of these checks makes the subsequent metadata scan safe and avoids wasted work on the vast majority of transactions. + +## Metadata Extraction: `getOfferIDFromCreatedOffer` + +This function iterates `transactionMeta.getNodes()` — the `STArray` of affected ledger nodes — and looks for the single node that: + +- Has `sfLedgerEntryType == ltNFTOKEN_OFFER` (confirming it is an NFToken offer object, not a different ledger entry type such as an account root or directory node), and +- Has its field name equal to `sfCreatedNode` (confirming the node was freshly created by this transaction, not modified or deleted). + +When found, the function returns `node.getFieldH256(sfLedgerIndex)` — the node's ledger key, which serves as the globally unique offer ID. The function returns `std::nullopt` if no qualifying node is found, which can happen legitimately even on an ostensibly eligible transaction (e.g., edge cases where metadata is absent or the offer creation path was not taken). + +Returning `std::optional` rather than throwing is consistent with the overall XRPL error-handling philosophy: protocol-level code avoids exceptions and signals absence through value types. + +## Integration Point: `insertNFTokenOfferID` + +The public-facing function composes the two helpers: it calls `canHaveNFTokenOfferID` as a fast pre-filter, then calls `getOfferIDFromCreatedOffer` and, if a value is present, inserts it into the `Json::Value` response under `jss::offer_id` as a hex string. The function is a no-op when any check fails — it never throws and never modifies the response if the offer ID cannot be determined. + +In practice this function is called from `insertNFTSyntheticInJson` in `NFTSyntheticSerializer.cpp`, which is the single entry point for adding all NFT-related synthetic fields to a transaction's `meta` JSON object. That wrapper also calls `insertNFTokenID` for the NFToken mint ID, so both synthetic enrichments follow the same pattern and share the same call site. + +## Key Design Observations + +The offer ID is recovered from `sfLedgerIndex` on the `CreatedNode`, not from a dedicated field on the transaction. This is intentional: the ledger index of an object is its canonical identifier, and reusing it avoids introducing a redundant field in the transaction format. The approach is also exact — there can be at most one `CreatedNode` of type `ltNFTOKEN_OFFER` per transaction, so the loop exits on the first match without ambiguity. + +The separation into three functions (guard, extractor, inserter) rather than a monolithic implementation ensures that `getOfferIDFromCreatedOffer` can be called independently by external consumers like Clio that may have already performed their own eligibility checks through different means. \ No newline at end of file diff --git a/src/libxrpl/protocol/PathAsset.cpp.ai.json b/src/libxrpl/protocol/PathAsset.cpp.ai.json new file mode 100644 index 0000000000..f26f86f9c1 --- /dev/null +++ b/src/libxrpl/protocol/PathAsset.cpp.ai.json @@ -0,0 +1,90 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "to_string(PathAsset)", + "std::visit([&](auto const& issue) { return to_string(issue); }, asset.value())", + "to_string(issue) (for each variant type in PathAsset)" + ], + "entry_point": "to_string(PathAsset const& asset)", + "purpose": "Converts a PathAsset to its string representation by dispatching to the appropriate to_string overload for the underlying variant type.", + "validation_points": [ + "No explicit validation in this chain. Assumes asset.value() holds a valid variant." + ] + }, + { + "call_chain": [ + "operator<<(std::ostream&, PathAsset)", + "to_string(PathAsset)", + "std::visit([&](auto const& issue) { return to_string(issue); }, asset.value())", + "to_string(issue)" + ], + "entry_point": "operator<<(std::ostream& os, PathAsset const& x)", + "purpose": "Streams a PathAsset to an output stream by converting it to a string.", + "validation_points": [ + "No explicit validation in this chain. Relies on to_string(PathAsset) and underlying variant." + ] + } + ], + "data_flows": [ + { + "field": "asset.value()", + "flow": [ + "PathAsset constructed (source unknown)", + "Passed to to_string(PathAsset)", + "asset.value() extracted (variant)", + "std::visit dispatches to to_string(issue)", + "String returned" + ], + "origin": "PathAsset instance (likely constructed elsewhere, possibly from user input or deserialization)", + "transformations": [ + "Variant visitation (type-dependent dispatch)", + "Conversion to string via to_string(issue)" + ], + "validated_at": "No validation in this file; assumes asset.value() is valid. Validation (if any) must occur at PathAsset construction or in to_string(issue)." + } + ], + "description": "Provides utility functions for converting PathAsset objects to string representations and for outputting them to streams within the xrpl namespace.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/PathAsset.cpp", + "functions": [ + { + "args": [ + "asset" + ], + "lineno": 6, + "name": "to_string" + }, + { + "args": [ + "os", + "x" + ], + "lineno": 11, + "name": "operator<<" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file contains only formatting/serialization logic (to_string, operator<<) and does not perform validation. Test coverage would be indirect, via tests that check string output for PathAsset or its variant types. Tests likely exist in files covering PathAsset, Issue, or serialization (e.g., PathAsset_test.cpp, Issue_test.cpp, or protocol/Path_test.cpp). There is no evidence of direct validation or error handling in this code, so validation coverage depends on upstream construction and variant handling. Gaps: No explicit tests for malformed or invalid PathAsset variants in this code; relies on upstream validation.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/PathAsset.cpp.ai.md b/src/libxrpl/protocol/PathAsset.cpp.ai.md new file mode 100644 index 0000000000..3910f9f89c --- /dev/null +++ b/src/libxrpl/protocol/PathAsset.cpp.ai.md @@ -0,0 +1,33 @@ +# `src/libxrpl/protocol/PathAsset.cpp` + +This file provides the non-inline serialization interface for `PathAsset` — specifically the `to_string()` free function and the `operator<<` stream overload. At nineteen lines, it is entirely a dispatch layer; all substantive logic lives in the header. + +## What `PathAsset` Represents + +`PathAsset` is purpose-built for `STPathElement`, the atom of a payment path in the XRPL protocol. A path element specifies a currency or token to route through, but it does not yet resolve an issuer — that happens during path-finding traversal. Accordingly, `PathAsset` holds a `std::variant` rather than the fuller `Asset` (`std::variant`). `Currency` covers both XRP (the zero currency sentinel) and classic IOU tokens; `MPTID` covers Multi-Purpose Token issuances introduced in later ledger versions. + +This narrower type is enforced at compile time via the `ValidPathAsset` concept in `Concepts.h`: + +```cpp +concept ValidPathAsset = (std::is_same_v || std::is_same_v); +``` + +Template members like `holds()` and `get()` are constrained by this concept, so any attempt to query for an unsupported type fails to compile. + +## Serialization via `std::visit` + +`to_string(PathAsset const&)` uses `std::visit` to dispatch to whichever `to_string` overload matches the active alternative: + +```cpp +return std::visit([&](auto const& issue) { return to_string(issue); }, asset.value()); +``` + +`asset.value()` returns the raw `std::variant` reference, and the generic lambda relies on overload resolution to call either `to_string(Currency const&)` (declared in `UintTypes.h`) or `to_string(MPTID const&)`. This is the same pattern used throughout the XRPL codebase for `Asset` and `Issue`, keeping variant serialization uniform without requiring a hand-rolled `if`/`else` type check. + +`operator<<` is a trivial forwarder to `to_string`, consistent with the rest of the protocol types (`Asset`, `Issue`, `MPTIssue` all follow the same pattern). + +## Why This Exists as a `.cpp` File + +Most `PathAsset` functionality — `holds()`, `get()`, `isXRP()`, `operator==`, `hash_append`, and the constructors — is implemented inline in the header. The `to_string` free function and `operator<<` are deliberately separated into a `.cpp` because they pull in `Indexes.h` (included here though not directly used by these two functions, likely for transitional or include-ordering reasons) and because non-template, non-`constexpr` functions benefit from a single translation-unit definition to avoid ODR concerns and reduce header-inclusion cost. + +The file performs no validation, error handling, or resource management. Correctness is fully delegated to the active variant's own `to_string` implementation and to wherever `PathAsset` was constructed — typically deserialization of an `STPathElement` field. \ No newline at end of file diff --git a/src/libxrpl/protocol/Permissions.cpp.ai.json b/src/libxrpl/protocol/Permissions.cpp.ai.json new file mode 100644 index 0000000000..a8b098537b --- /dev/null +++ b/src/libxrpl/protocol/Permissions.cpp.ai.json @@ -0,0 +1,347 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 5, + "name": "Permission" + } + ], + "code_paths": [ + { + "call_chain": [ + "Permission::Permission" + ], + "entry_point": "Permission::Permission (constructor)", + "purpose": "Initializes all internal maps (txFeatureMap_, delegableTx_, granularPermissionMap_, etc.) and validates their sizes and values.", + "validation_points": [ + "XRPL_ASSERT(txFeatureMap_.size() == delegableTx_.size())", + "XRPL_ASSERT(permission.second > UINT16_MAX) [for each granularPermissionMap_ entry]" + ] + }, + { + "call_chain": [ + "Permission::getTxFeature" + ], + "entry_point": "Permission::getTxFeature", + "purpose": "Retrieves the feature (amendment) associated with a transaction type, asserts that the txType exists in the map.", + "validation_points": [ + "XRPL_ASSERT(txFeaturesIt != txFeatureMap_.end())" + ] + }, + { + "call_chain": [ + "Permission::isDelegable", + "Permission::getGranularName", + "Permission::getGranularTxType" + ], + "entry_point": "Permission::isDelegable", + "purpose": "Checks if a permission value is delegable, using granular and transaction type maps.", + "validation_points": [ + "Indirect: relies on maps built and validated in constructor" + ] + }, + { + "call_chain": [ + "Permission::getPermissionName", + "Permission::getGranularName", + "Permission::permissionToTxType", + "TxFormats::getInstance().findByType" + ], + "entry_point": "Permission::getPermissionName", + "purpose": "Maps a permission value to its name, either as a granular permission or as a transaction type.", + "validation_points": [ + "Indirect: relies on maps built and validated in constructor" + ] + } + ], + "data_flows": [ + { + "field": "txFeatureMap_", + "flow": [ + "Permission::Permission", + "txFeatureMap_ initialized", + "Used in getTxFeature" + ], + "origin": "Constructed in Permission::Permission from transactions.macro", + "transformations": [ + "Populated from macro expansion", + "Queried by txType" + ], + "validated_at": "Permission::Permission (XRPL_ASSERT on size with delegableTx_)" + }, + { + "field": "delegableTx_", + "flow": [ + "Permission::Permission", + "delegableTx_ initialized", + "Used in isDelegable" + ], + "origin": "Constructed in Permission::Permission from transactions.macro", + "transformations": [ + "Populated from macro expansion", + "Queried by txType" + ], + "validated_at": "Permission::Permission (XRPL_ASSERT on size with txFeatureMap_)" + }, + { + "field": "granularPermissionMap_", + "flow": [ + "Permission::Permission", + "granularPermissionMap_ initialized", + "Used in getGranularValue" + ], + "origin": "Constructed in Permission::Permission from permissions.macro", + "transformations": [ + "Populated from macro expansion", + "Queried by name" + ], + "validated_at": "Permission::Permission (XRPL_ASSERT on value range for each entry)" + }, + { + "field": "granularNameMap_", + "flow": [ + "Permission::Permission", + "granularNameMap_ initialized", + "Used in getGranularName" + ], + "origin": "Constructed in Permission::Permission from permissions.macro", + "transformations": [ + "Populated from macro expansion", + "Queried by value" + ], + "validated_at": "Indirectly validated by correctness of macro and constructor" + }, + { + "field": "granularTxTypeMap_", + "flow": [ + "Permission::Permission", + "granularTxTypeMap_ initialized", + "Used in getGranularTxType" + ], + "origin": "Constructed in Permission::Permission from permissions.macro", + "transformations": [ + "Populated from macro expansion", + "Queried by permission type" + ], + "validated_at": "Indirectly validated by correctness of macro and constructor" + } + ], + "description": "Implements the xrpl::Permission class, which manages transaction and granular permissions, their mappings, and delegation logic for the XRPL protocol.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "txFeatureMap_ size", + "validation", + "missing", + "check" + ], + "evidence": "Field txFeatureMap_ size validated by XRPL_ASSERT macro (custom assertion, not a standard framework)", + "issue_pattern": "Missing validation for txFeatureMap_ size", + "why_false_positive": "XRPL_ASSERT macro (custom assertion, not a standard framework) validates txFeatureMap_ size automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "delegableTx_ size", + "validation", + "missing", + "check" + ], + "evidence": "Field delegableTx_ size validated by XRPL_ASSERT macro (custom assertion, not a standard framework)", + "issue_pattern": "Missing validation for delegableTx_ size", + "why_false_positive": "XRPL_ASSERT macro (custom assertion, not a standard framework) validates delegableTx_ size automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "granularPermissionMap_ values", + "validation", + "missing", + "check" + ], + "evidence": "Field granularPermissionMap_ values validated by XRPL_ASSERT macro (custom assertion, not a standard framework)", + "issue_pattern": "Missing validation for granularPermissionMap_ values", + "why_false_positive": "XRPL_ASSERT macro (custom assertion, not a standard framework) validates granularPermissionMap_ values automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "txFeatureMap_ and delegableTx_ size", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at Permission constructor", + "issue_pattern": "Missing empty string validation for txFeatureMap_ and delegableTx_ size", + "why_false_positive": "XRPL_ASSERT macro validates txFeatureMap_ and delegableTx_ size for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "granularPermissionMap_ values", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at Permission constructor (for loop over granularPermissionMap_)", + "issue_pattern": "Missing empty string validation for granularPermissionMap_ values", + "why_false_positive": "XRPL_ASSERT macro validates granularPermissionMap_ values for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "granularPermissionMap_ values", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT macro at Permission constructor (for loop over granularPermissionMap_)", + "issue_pattern": "Missing range validation for granularPermissionMap_ values", + "why_false_positive": "XRPL_ASSERT macro validates granularPermissionMap_ values range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Permissions.cpp", + "functions": [ + { + "args": [], + "lineno": 6, + "name": "Permission" + }, + { + "args": [], + "lineno": 61, + "name": "getInstance" + }, + { + "args": [ + "value" + ], + "lineno": 68, + "name": "getPermissionName" + }, + { + "args": [ + "name" + ], + "lineno": 81, + "name": "getGranularValue" + }, + { + "args": [ + "value" + ], + "lineno": 89, + "name": "getGranularName" + }, + { + "args": [ + "gpType" + ], + "lineno": 97, + "name": "getGranularTxType" + }, + { + "args": [ + "txType" + ], + "lineno": 105, + "name": "getTxFeature" + }, + { + "args": [ + "permissionValue", + "rules" + ], + "lineno": 117, + "name": "isDelegable" + }, + { + "args": [ + "type" + ], + "lineno": 146, + "name": "txToPermissionType" + }, + { + "args": [ + "value" + ], + "lineno": 150, + "name": "permissionToTxType" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage for this code is likely to be indirect, as the validation logic is in the constructor and relies on macro-generated data. Unit tests may exist for Permission methods (getPermissionName, getGranularValue, getTxFeature, isDelegable), but explicit tests for the XRPL_ASSERT validations in the constructor are unlikely unless there are tests that intentionally break macro invariants. Tests may be found in files like test/protocol/Permissions_test.cpp or similar, but coverage for constructor validation is typically only exercised if the macro data is faulty. There may be a gap in explicit negative tests for these assertions.", + "validation_architecture": { + "auto_validated_fields": [ + "txFeatureMap_ size", + "delegableTx_ size", + "granularPermissionMap_ values" + ], + "framework": "XRPL_ASSERT macro (custom assertion, not a standard framework)", + "validation_layer": "business_logic (constructor-level, not input API)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "txFeatureMap_ and delegableTx_ size", + "location": "Permission constructor", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures txFeatureMap_ and delegableTx_ have the same number of entries" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "granularPermissionMap_ values", + "location": "Permission constructor (for loop over granularPermissionMap_)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures each granular permission value is greater than UINT16_MAX" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Permissions.cpp.ai.md b/src/libxrpl/protocol/Permissions.cpp.ai.md new file mode 100644 index 0000000000..f190dfda2a --- /dev/null +++ b/src/libxrpl/protocol/Permissions.cpp.ai.md @@ -0,0 +1,47 @@ +# `src/libxrpl/protocol/Permissions.cpp` + +## Role and Context + +This file implements the `Permission` class, a read-only singleton that is the central authority for XRPL's account-delegation permission system. When one account grants another the right to submit transactions on its behalf (via `DelegateSet`), every permitted action is represented as a numeric `permissionValue` stored on-ledger. `Permission` owns the authoritative mapping between those numbers, their human-readable names, the underlying transaction types they correspond to, and the amendments that must be active before they can be delegated. + +The class is the single place where the permission number-space is defined and validated, so it is consulted by transaction preflight logic (directly in `DelegateSet::preflight()`) and by any code that needs to translate between the serialized ledger representation and meaningful protocol semantics. + +## Two Classes of Permission and Their Numeric Encoding + +The XRPL permission system has two orthogonal kinds of delegation grants: + +**Transaction-level permissions** allow the delegate to submit any transaction of a given type on the grantor's behalf. These are encoded as `TxType + 1`. Since `TxType` values are zero-based 16-bit integers (e.g. `ttPAYMENT = 0`), the offset of +1 shifts them to start at 1 and keeps them entirely within the `uint16` range. The static helpers `txToPermissionType()` and `permissionToTxType()` encapsulate this arithmetic. + +**Granular permissions** are sub-operation grants within a single transaction type. For example, `TrustlineAuthorize`, `TrustlineFreeze`, and `TrustlineUnfreeze` are three distinct granular permissions that all correspond to `ttTRUST_SET`, giving a grantor the ability to delegate individual TrustSet capabilities rather than the whole transaction type. These are defined in `permissions.macro` with values starting at 65537 (just above `UINT16_MAX`), placing them entirely outside the uint16 transaction-permission range. The constructor's per-entry assert — `permission.second > UINT16_MAX` — enforces this partition at program startup, making namespace collisions a hard failure rather than a silent bug. + +## Macro-Driven Table Construction + +The constructor is the most architecturally interesting part of the file. It populates five `unordered_map` members by expanding the same two macro files (`transactions.macro` and `permissions.macro`) with different `#define TRANSACTION` / `#define PERMISSION` definitions each time. This X-macro technique means all permission metadata — values, names, transaction type associations, amendment requirements, and delegability flags — live exclusively in the macro files. Adding a new transaction type or granular permission requires editing only those two files; `Permissions.cpp` and the `GranularPermissionType` enum update automatically. + +Each expansion is wrapped in `#pragma push_macro` / `#pragma pop_macro` to prevent the temporary macro definition from persisting or interfering with surrounding code — a defensive pattern that matters here because `transactions.macro` is included in many other translation units with different definitions. + +The five maps are: + +- **`txFeatureMap_`** — maps `TxType` (stored as `uint16_t`) to the `uint256` amendment that must be enabled before this transaction type can be used in delegation. A zero `uint256{}` means the transaction requires no specific amendment. +- **`delegableTx_`** — maps `TxType` to `Delegation::delegable` or `Delegation::notDelegable`. Governance and key-management transactions are typically marked `notDelegable` because allowing an agent to change signers or disable the master key would undermine the account owner's control. +- **`granularPermissionMap_`** — maps string names (e.g. `"TrustlineAuthorize"`) to `GranularPermissionType` values, for deserializing permission names from JSON. +- **`granularNameMap_`** — the inverse of the above, for serializing permission values to human-readable output. +- **`granularTxTypeMap_`** — maps `GranularPermissionType` to its parent `TxType`, which is needed so the amendment guard in `isDelegable()` can locate the feature requirement for any granular sub-operation. + +A final constructor assert verifies that `txFeatureMap_` and `delegableTx_` have the same number of entries, catching any hypothetical macro inconsistency where a transaction appears in one but not the other. + +## `isDelegable()`: The Delegation Gate + +This method is called from `DelegateSet::preflight()` for every entry in the `sfPermissions` array before a delegation is accepted. Its logic distinguishes granular from transaction-level permissions: + +For a **granular permission**, the check is simply whether the value resolves to a known `GranularPermissionType`. If it does, delegation is unconditionally allowed. The rationale is that granular permissions are already intentionally narrow by design. + +For a **transaction-level permission**, delegation is allowed only if all three of these hold: the decoded `TxType` is recognized in `delegableTx_`, the transaction's required amendment is currently active in `rules` (or no amendment is required), and the `Delegation` flag for that transaction type is `delegable`. The amendment check is significant: it prevents a transaction from being delegated before the ledger feature that introduces it is live, even if the macro data already includes the new transaction type. + +## Singleton and Thread Safety + +`getInstance()` returns a function-local `static const Permission` instance, which is initialized once and never mutated. C++11 and later guarantee that this initialization is thread-safe, making the pattern appropriate for a read-only registry that is consulted from multiple threads during transaction processing. Copy construction and copy assignment are explicitly deleted, enforcing the singleton contract. + +## Relationship to `TxFormats` + +`getPermissionName()` delegates to `TxFormats::getInstance().findByType()` for the transaction-level case. This keeps the string names of transaction types canonical — they are defined once in `TxFormats` and reused here rather than being duplicated in the permissions system. \ No newline at end of file diff --git a/src/libxrpl/protocol/Protocol.cpp.ai.json b/src/libxrpl/protocol/Protocol.cpp.ai.json new file mode 100644 index 0000000000..c6e4505b26 --- /dev/null +++ b/src/libxrpl/protocol/Protocol.cpp.ai.json @@ -0,0 +1,147 @@ +{ + "args": [ + { + "lineno": 5, + "name": "seq" + }, + { + "lineno": 10, + "name": "seq" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "isVotingLedger" + ], + "entry_point": "xrpl::isVotingLedger", + "purpose": "Determines if a given ledger sequence number is a 'voting ledger' (i.e., if it is a multiple of FLAG_LEDGER_INTERVAL). Used to trigger consensus voting logic at specific intervals.", + "validation_points": [ + "isVotingLedger" + ] + }, + { + "call_chain": [ + "isFlagLedger" + ], + "entry_point": "xrpl::isFlagLedger", + "purpose": "Determines if a given ledger sequence number is a 'flag ledger' (i.e., if it is a multiple of FLAG_LEDGER_INTERVAL). Used to trigger flag ledger logic at specific intervals.", + "validation_points": [ + "isFlagLedger" + ] + } + ], + "data_flows": [ + { + "field": "LedgerIndex seq", + "flow": [ + "LedgerIndex seq (from ledger object or consensus state)", + "passed to isVotingLedger/isFlagLedger", + "modulo operation with FLAG_LEDGER_INTERVAL", + "boolean result returned to caller" + ], + "origin": "Passed as an argument to isVotingLedger or isFlagLedger, typically sourced from the current ledger's sequence number in consensus or ledger management code.", + "transformations": [ + "seq is checked for divisibility by FLAG_LEDGER_INTERVAL (seq % FLAG_LEDGER_INTERVAL == 0)" + ], + "validated_at": "isVotingLedger or isFlagLedger" + } + ], + "description": "Provides utility functions to determine if a given ledger index corresponds to a voting or flag ledger in the XRPL protocol.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "seq", + "empty", + "string", + "validation" + ], + "evidence": "isVotingLedger (custom function) at isVotingLedger", + "issue_pattern": "Missing empty string validation for seq", + "why_false_positive": "isVotingLedger (custom function) validates seq for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "seq", + "empty", + "string", + "validation" + ], + "evidence": "isFlagLedger (custom function) at isFlagLedger", + "issue_pattern": "Missing empty string validation for seq", + "why_false_positive": "isFlagLedger (custom function) validates seq for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Protocol.cpp", + "functions": [ + { + "args": [ + "seq" + ], + "lineno": 4, + "name": "isVotingLedger" + }, + { + "args": [ + "seq" + ], + "lineno": 9, + "name": "isFlagLedger" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "The provided code is a simple utility with no direct tests in this file. Test coverage for these functions likely exists in higher-level consensus, ledger, or protocol tests that exercise voting and flag ledger logic. Look for tests in files such as Consensus_test.cpp, LedgerMaster_test.cpp, or Protocol_test.cpp. There may be a gap in direct unit tests for isVotingLedger and isFlagLedger; if so, consider adding explicit tests for edge cases (e.g., seq = 0, seq = FLAG_LEDGER_INTERVAL, seq = FLAG_LEDGER_INTERVAL - 1, negative values if allowed).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (custom logic, no framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "seq", + "location": "isVotingLedger", + "validated_by": "isVotingLedger (custom function)", + "validates": [ + "Checks if seq is a multiple of FLAG_LEDGER_INTERVAL" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "seq", + "location": "isFlagLedger", + "validated_by": "isFlagLedger (custom function)", + "validates": [ + "Checks if seq is a multiple of FLAG_LEDGER_INTERVAL" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Protocol.cpp.ai.md b/src/libxrpl/protocol/Protocol.cpp.ai.md new file mode 100644 index 0000000000..aa3f122aac --- /dev/null +++ b/src/libxrpl/protocol/Protocol.cpp.ai.md @@ -0,0 +1,48 @@ +# `src/libxrpl/protocol/Protocol.cpp` + +## Role and Purpose + +This file provides the two predicate functions that the rest of the XRPL node uses to identify **flag ledgers** and **voting ledgers** — the two special milestone points built into the ledger sequence at regular, protocol-defined intervals. Although the file is only 15 lines of code, the names it exposes anchor a surprisingly wide swath of consensus, fee governance, and validator-reliability logic throughout the codebase. + +## The `FLAG_LEDGER_INTERVAL` Heartbeat + +The key constant lives in the companion header, `Protocol.h`: + +```cpp +std::uint32_t constexpr FLAG_LEDGER_INTERVAL = 256; +``` + +Every 256 ledgers, the network reaches a boundary where accumulated validator votes are tallied and network-wide parameters are updated. This boundary is the **flag ledger**. Changing `FLAG_LEDGER_INTERVAL` without an amendment mechanism would be a hard fork — it is explicitly called out in `Protocol.h`'s comment block as an implicit part of the protocol. + +## The Two Predicates + +Both functions share an identical body: + +```cpp +bool isVotingLedger(LedgerIndex seq) { return seq % FLAG_LEDGER_INTERVAL == 0; } +bool isFlagLedger(LedgerIndex seq) { return seq % FLAG_LEDGER_INTERVAL == 0; } +``` + +Their names are intentionally distinct despite the identical implementation, because the **caller** provides different `seq` values for each purpose. The `Ledger` class illustrates this clearly: + +```cpp +bool Ledger::isFlagLedger() const { return ::xrpl::isFlagLedger(header_.seq); } +bool Ledger::isVotingLedger() const { return ::xrpl::isVotingLedger(header_.seq + 1); } +``` + +`isFlagLedger()` asks "is *this* ledger a flag ledger?" — it passes the ledger's own sequence number directly. `isVotingLedger()` asks "does *this* ledger's consensus session produce a flag ledger?" — it passes `seq + 1`, so a ledger is a voting ledger when the ledger being built on top of it will land on a flag boundary. This `+1` offset is the entire semantic difference between the two names, and it is resolved at the call site rather than inside the protocol functions. + +## How the Distinction Drives Consensus + +`RCLConsensus.cpp` uses both predicates in sequence when assembling the transaction set for a new consensus round: + +- If the **previous ledger** was a flag ledger, fee-vote and amendment pseudo-transactions are injected via `feeVote_->doVoting()` and `app_.getAmendmentTable().doVoting()`. These pseudo-transactions encode the outcome of the vote that was collected in the flag ledger's validation messages. +- If the **previous ledger** was a voting ledger (i.e., the session is building the flag ledger itself), Negative UNL pseudo-transactions are added via `nUnlVote_.doVoting()`. The Negative UNL update records which validators were unreliable over the preceding 256-ledger window. + +`FeeVoteImpl.cpp` independently gates its vote-casting on `isFlagLedger(lastClosedLedger->seq())`, and `Change.cpp` guards the application of fee/reserve updates behind the same check. This means all three subsystems — fee governance, amendment governance, and validator-reliability tracking — synchronize on the same 256-ledger clock tick defined here. + +## Why Two Names for the Same Check + +Separating `isFlagLedger` and `isVotingLedger` serves as **self-documenting intent**. A reader seeing `prevLedger->isFlagLedger()` immediately understands the context is "we just crossed a boundary, apply stored votes." A reader seeing `prevLedger->isVotingLedger()` understands "we are *about to* produce a flag ledger, inject the vote now." If the two calls used the same name, the `+1` offset in `Ledger::isVotingLedger()` would be invisible at the consensus call sites, making the phase relationship between vote collection and vote application harder to audit. + +The `NegativeUNLVote` subsystem extends this pattern further, deriving threshold constants directly from `FLAG_LEDGER_INTERVAL` (e.g., 50%, 80%, and 90% of 256 for reliability scoring watermarks), reinforcing that the 256-ledger period is the canonical unit of measurement for network health metrics. \ No newline at end of file diff --git a/src/libxrpl/protocol/PublicKey.cpp.ai.json b/src/libxrpl/protocol/PublicKey.cpp.ai.json new file mode 100644 index 0000000000..30202ccafe --- /dev/null +++ b/src/libxrpl/protocol/PublicKey.cpp.ai.json @@ -0,0 +1,267 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "slice" + ], + "lineno": 144, + "name": "PublicKey" + }, + { + "args": [ + "other" + ], + "lineno": 153, + "name": "PublicKey" + } + ], + "code_paths": [ + { + "call_chain": [ + "parseBase58", + "decodeBase58Token", + "makeSlice", + "publicKeyType", + "PublicKey ctor" + ], + "entry_point": "parseBase58", + "purpose": "Parses a base58-encoded public key string, validates its type, and constructs a PublicKey object if valid.", + "validation_points": [ + "decodeBase58Token (validates base58 format and token type)", + "publicKeyType (validates the slice is a recognized public key type)" + ] + }, + { + "call_chain": [ + "ecdsaCanonicality", + "sigPart (twice, for R and S)", + "sliceToHex", + "boost::multiprecision::number (parsing hex to big int)" + ], + "entry_point": "ecdsaCanonicality", + "purpose": "Validates that a DER-encoded ECDSA signature is canonical (protects against signature morphing).", + "validation_points": [ + "ecdsaCanonicality (checks signature structure, length, and canonicality)", + "sigPart (validates each integer part of the signature)" + ] + } + ], + "data_flows": [ + { + "field": "base58 public key string", + "flow": [ + "parseBase58 (input string)", + "decodeBase58Token (decodes base58, checks token type)", + "makeSlice (wraps result as Slice)", + "publicKeyType (checks if slice is valid public key type)", + "PublicKey ctor (constructs PublicKey object)" + ], + "origin": "External input (user, config, network)", + "transformations": [ + "Base58 decoding", + "Token type validation", + "Slice wrapping" + ], + "validated_at": "decodeBase58Token, publicKeyType" + }, + { + "field": "DER-encoded ECDSA signature", + "flow": [ + "ecdsaCanonicality (input Slice)", + "sigPart (parses R and S parts, validates structure and canonicality)", + "sliceToHex (converts to hex string)", + "boost::multiprecision::number (parses hex to big int)", + "ecdsaCanonicality (compares R and S to curve order)" + ], + "origin": "External input (transaction signature)", + "transformations": [ + "DER parsing", + "Length and value checks", + "Hex conversion", + "Big integer conversion" + ], + "validated_at": "sigPart, ecdsaCanonicality" + } + ], + "description": "This file implements cryptographic utilities and logic for handling XRPL public keys, including parsing, canonicality checks, signature verification for secp256k1 and ed25519, and node ID calculation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Base58-encoded public key string", + "empty", + "string", + "validation" + ], + "evidence": "decodeBase58Token, publicKeyType at parseBase58", + "issue_pattern": "Missing empty string validation for Base58-encoded public key string", + "why_false_positive": "decodeBase58Token, publicKeyType validates Base58-encoded public key string for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Base58-encoded public key string", + "format", + "validation", + "invalid" + ], + "evidence": "decodeBase58Token, publicKeyType at parseBase58", + "issue_pattern": "Missing format validation for Base58-encoded public key string", + "why_false_positive": "decodeBase58Token, publicKeyType validates Base58-encoded public key string format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "DER-encoded ECDSA signature part", + "empty", + "string", + "validation" + ], + "evidence": "sigPart at sigPart", + "issue_pattern": "Missing empty string validation for DER-encoded ECDSA signature part", + "why_false_positive": "sigPart validates DER-encoded ECDSA signature part for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/PublicKey.cpp", + "functions": [ + { + "args": [ + "os", + "pk" + ], + "lineno": 16, + "name": "operator<<" + }, + { + "args": [ + "type", + "s" + ], + "lineno": 22, + "name": "parseBase58" + }, + { + "args": [ + "buf" + ], + "lineno": 36, + "name": "sigPart" + }, + { + "args": [ + "slice" + ], + "lineno": 56, + "name": "sliceToHex" + }, + { + "args": [ + "sig" + ], + "lineno": 80, + "name": "ecdsaCanonicality" + }, + { + "args": [ + "sig" + ], + "lineno": 124, + "name": "ed25519Canonical" + }, + { + "args": [ + "slice" + ], + "lineno": 164, + "name": "publicKeyType" + }, + { + "args": [ + "publicKey", + "digest", + "sig", + "mustBeFullyCanonical" + ], + "lineno": 176, + "name": "verifyDigest" + }, + { + "args": [ + "publicKey", + "m", + "sig" + ], + "lineno": 210, + "name": "verify" + }, + { + "args": [ + "pk" + ], + "lineno": 241, + "name": "calcNodeID" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 14, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by unit tests for PublicKey parsing and signature validation, typically found in files like PublicKey_test.cpp, Protocol_test.cpp, or SignatureCanonicality_test.cpp. However, the provided code does not reference any test files directly. Potential gaps: edge cases for malformed base58 strings, unusual DER encodings, or non-canonical signatures may not be fully covered unless explicitly tested. There is no evidence of fuzz or property-based testing for malformed inputs in this snippet.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation via utility functions (no external framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt", + "field": "Base58-encoded public key string", + "location": "parseBase58", + "validated_by": "decodeBase58Token, publicKeyType", + "validates": [ + "Checks if the decoded Base58 string matches the expected TokenType", + "Checks if the resulting slice is a valid public key type" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt", + "field": "DER-encoded ECDSA signature part", + "location": "sigPart", + "validated_by": "sigPart", + "validates": [ + "Checks minimum buffer size (at least 3 bytes)", + "Checks first byte is 0x02 (DER integer tag)", + "Checks length byte is within buffer and between 1 and 33", + "Checks number is not negative (first data byte & 0x80 == 0)", + "Checks for zero value and padding rules" + ], + "validation_type": "format|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/PublicKey.cpp.ai.md b/src/libxrpl/protocol/PublicKey.cpp.ai.md new file mode 100644 index 0000000000..97411f573b --- /dev/null +++ b/src/libxrpl/protocol/PublicKey.cpp.ai.md @@ -0,0 +1,44 @@ +# `PublicKey.cpp` — Public Key Implementation for XRPL Cryptography + +This file implements the core cryptographic infrastructure for XRPL public keys: construction and validation of the `PublicKey` value type, signature canonicality enforcement, signature verification for both supported elliptic curve systems, and node identity derivation. It is the implementation counterpart to `include/xrpl/protocol/PublicKey.h`. + +## The `PublicKey` Value Type + +`PublicKey` stores exactly 33 bytes in a fixed inline buffer `buf_[33]`. This is not accidental — it is a deliberate encoding decision that unifies two incompatible key formats into a single uniform type. A secp256k1 compressed public key is natively 33 bytes (a sign byte `0x02` or `0x03` followed by the X coordinate). An Ed25519 public key is 32 bytes of key material, but XRPL prefixes it with a constant `0xED` byte, also giving it 33 bytes. The leading byte therefore acts as a self-describing type tag: `0x02`/`0x03` means secp256k1, `0xED` means Ed25519. + +The `publicKeyType()` free function encodes this discriminator logic. Its existence as a free function — rather than a member method — is important because it is also used to *validate* raw byte slices before constructing a `PublicKey`. The constructor enforces this as a precondition via `LogicError`, which terminates the process rather than returning an error. This is appropriate because receiving a malformed `PublicKey` would indicate a programming error (e.g., bypassed deserialization), not a recoverable runtime condition. + +Default construction is deleted. There is no such thing as an "empty" or "uninitialized" `PublicKey`, which eliminates a whole class of use-after-construction bugs where callers might forget to populate the key. + +Copy construction and copy assignment use `std::memcpy` directly rather than the compiler-generated copy, which is safe because the storage is a plain `uint8_t` array and avoids any potential overhead from element-wise copying. + +## Signature Canonicality — The Malleability Defense + +The most architecturally significant logic in this file is the dual-tier canonicality system for ECDSA signatures. XRPL's `ECDSACanonicality` enum has two values: `canonical` and `fullyCanonical`. Understanding why both exist requires understanding transaction malleability. + +For any signed message, an ECDSA signature `(R, S)` has a mathematical equivalent `(R, G-S)` where G is the secp256k1 curve order. Both are valid signatures, but they produce different serializations — and thus different transaction hashes. This was exploited historically to mutate a transaction's ID without invalidating the signature, breaking systems that tracked transactions by ID. XRPL's response is to mandate that valid signatures use only the *lower* value of S (i.e., `S ≤ G/2`), which is the `fullyCanonical` form. Old signatures with `S > G/2` are classified as merely `canonical` — structurally valid but not fully canonical. + +`ecdsaCanonicality()` implements this check. It first parses the DER-encoded structure by calling the `sigPart()` helper twice to extract R and S as byte slices. `sigPart()` is a stateful parser that advances its `Slice` argument in place, consuming input as it validates DER's `0x02 ` integer encoding. It rejects negatives (high bit set), zero values, and unnecessary zero-padding, all of which are real DER malformations seen in practice. + +The comparison of R and S against the curve order G requires big-integer arithmetic. The values are only available as raw byte strings, so `sliceToHex()` converts each to a hex literal string that `boost::multiprecision::number` can parse. The type alias `uint264` uses a 264-bit signed integer (33 bytes), which is one byte wider than the 32-byte curve order, to safely hold values up to G without overflow during the `G - S` computation. + +`ed25519Canonical()` performs the analogous check for Ed25519: the second 32 bytes of a signature encode the scalar S, which must be less than the Ed25519 subgroup order. The bytes arrive in little-endian order (per the Ed25519 spec), so `std::reverse_copy` produces a big-endian representation for `std::lexicographical_compare` against the hard-coded big-endian order constant. + +## Signature Verification + +`verify()` is the general-purpose verification entry point. It dispatches on key type: + +- **secp256k1**: Hashes the message with SHA-512 Half (truncated to 256 bits), then calls `verifyDigest()`. The indirection through `sha512Half` before hashing is an XRPL-wide convention — raw secp256k1 verification happens only against digests, never directly against messages. +- **Ed25519**: Checks canonicality first, then strips the `0xED` prefix byte (`publicKey.data() + 1`) before calling the external `ed25519_sign_open()` library function. The prefix is purely an XRPL encoding artifact; the underlying Ed25519 library has no knowledge of it. + +`verifyDigest()` handles the secp256k1 path in detail. If the signature is merely `canonical` rather than `fullyCanonical`, it calls `secp256k1_ecdsa_signature_normalize()` to convert S to its low form before verifying. This means the ledger will accept old-style non-fully-canonical signatures by normalizing them, rather than simply rejecting them — a deliberate backward-compatibility choice. + +The `secp256k1Context()` helper in `detail/secp256k1.h` manages the libsecp256k1 context as a function-local static with a RAII wrapper, initializing it once on first call with both `SECP256K1_CONTEXT_VERIFY` and `SECP256K1_CONTEXT_SIGN` flags. This avoids global constructor ordering issues while ensuring a single shared context across all callers. + +## Node Identity Derivation + +`calcNodeID()` maps a validator's public key to its 160-bit network identity by applying RIPEMD-160(SHA-256(pubkey)) — the same `ripesha_hasher` used to derive XRPL account IDs from public keys. Node IDs are used in the peer-to-peer layer for routing and identification, which is why a validator's node public key and its network address together form a fingerprint (see `getFingerprint()` in the header). + +## `parseBase58` Specialization + +The template specialization of `parseBase58` decodes a Base58Check-encoded public key from a string, validating first the Base58 framing (token type prefix) and then the decoded bytes via `publicKeyType`. It returns `std::nullopt` for any malformed input, making it safe to call on untrusted data. The `toBase58` free function in the header is its inverse, encoding a `PublicKey` for human-readable display (e.g., in configuration files or the JSON API). \ No newline at end of file diff --git a/src/libxrpl/protocol/Quality.cpp.ai.json b/src/libxrpl/protocol/Quality.cpp.ai.json new file mode 100644 index 0000000000..a23c0dd80e --- /dev/null +++ b/src/libxrpl/protocol/Quality.cpp.ai.json @@ -0,0 +1,538 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::uint64_t value", + "Amounts const& amount" + ], + "lineno": 8, + "name": "Quality" + } + ], + "code_paths": [ + { + "call_chain": [ + "Quality::operator++", + "XRPL_ASSERT(m_value > 0)", + "--m_value" + ], + "entry_point": "Quality::operator++", + "purpose": "Pre-decrement m_value, ensuring it does not underflow", + "validation_points": [ + "XRPL_ASSERT(m_value > 0)" + ] + }, + { + "call_chain": [ + "Quality::operator--", + "XRPL_ASSERT(m_value < max)", + "++m_value" + ], + "entry_point": "Quality::operator--", + "purpose": "Pre-increment m_value, ensuring it does not overflow", + "validation_points": [ + "XRPL_ASSERT(m_value < std::numeric_limits::max())" + ] + }, + { + "call_chain": [ + "Quality::ceil_in", + "ceil_in_impl", + "if (amount.in > limit)", + "Amounts result(limit, DivRoundFunc(...))", + "if (result.out > amount.out) result.out = amount.out", + "XRPL_ASSERT(result.in == limit)", + "return result" + ], + "entry_point": "Quality::ceil_in", + "purpose": "Clamp input amount to a limit, adjusting output accordingly", + "validation_points": [ + "XRPL_ASSERT(result.in == limit)", + "XRPL_ASSERT(amount.in <= limit)" + ] + }, + { + "call_chain": [ + "Quality::ceil_in_strict", + "ceil_in_impl", + "if (amount.in > limit)", + "Amounts result(limit, DivRoundFunc(...))", + "if (result.out > amount.out) result.out = amount.out", + "XRPL_ASSERT(result.in == limit)", + "return result" + ], + "entry_point": "Quality::ceil_in_strict", + "purpose": "Strict version of ceil_in with custom rounding", + "validation_points": [ + "XRPL_ASSERT(result.in == limit)", + "XRPL_ASSERT(amount.in <= limit)" + ] + }, + { + "call_chain": [ + "Quality::ceil_out", + "ceil_out_impl", + "if (amount.out > limit)", + "Amounts result(MulRoundFunc(...), limit)", + "if (result.in > amount.in) result.in = amount.in", + "XRPL_ASSERT(result.out == limit)", + "return result" + ], + "entry_point": "Quality::ceil_out", + "purpose": "Clamp output amount to a limit, adjusting input accordingly", + "validation_points": [ + "XRPL_ASSERT(result.out == limit)", + "XRPL_ASSERT(amount.out <= limit)" + ] + }, + { + "call_chain": [ + "Quality::ceil_out_strict", + "ceil_out_impl", + "if (amount.out > limit)", + "Amounts result(MulRoundFunc(...), limit)", + "if (result.in > amount.in) result.in = amount.in", + "XRPL_ASSERT(result.out == limit)", + "return result" + ], + "entry_point": "Quality::ceil_out_strict", + "purpose": "Strict version of ceil_out with custom rounding", + "validation_points": [ + "XRPL_ASSERT(result.out == limit)", + "XRPL_ASSERT(amount.out <= limit)" + ] + }, + { + "call_chain": [ + "composed_quality", + "lhs.rate()", + "XRPL_ASSERT(lhs_rate != beast::zero)", + "rhs.rate()", + "XRPL_ASSERT(rhs_rate != beast::zero)", + "mulRound(lhs_rate, rhs_rate, ...)", + "XRPL_ASSERT((stored_exponent > 0) && (stored_exponent <= 255))", + "return Quality(...)" + ], + "entry_point": "composed_quality", + "purpose": "Compose two Quality objects, validating nonzero rates and exponent range", + "validation_points": [ + "XRPL_ASSERT(lhs_rate != beast::zero)", + "XRPL_ASSERT(rhs_rate != beast::zero)", + "XRPL_ASSERT((stored_exponent > 0) && (stored_exponent <= 255))" + ] + } + ], + "data_flows": [ + { + "field": "m_value", + "flow": [ + "Quality::Quality(std::uint64_t value) or Quality::Quality(Amounts const& amount)", + "m_value set", + "Used in operator++/--, ceil_in_impl, ceil_out_impl, rate()" + ], + "origin": "Quality constructor (from uint64_t or from Amounts via getRate)", + "transformations": [ + "Incremented/decremented in operator++/--", + "Used to compute rates in ceil_in_impl/ceil_out_impl" + ], + "validated_at": [ + "XRPL_ASSERT(m_value > 0) in operator++", + "XRPL_ASSERT(m_value < max) in operator--" + ] + }, + { + "field": "Amounts.in", + "flow": [ + "Passed as argument to ceil_in_impl/ceil_out_impl", + "Compared to limit", + "Used to construct result Amounts" + ], + "origin": "Input to ceil_in_impl/ceil_out_impl", + "transformations": [ + "Clamped to limit if amount.in > limit", + "Used in DivRoundFunc/MulRoundFunc" + ], + "validated_at": [ + "XRPL_ASSERT(result.in == limit) or XRPL_ASSERT(amount.in <= limit)" + ] + }, + { + "field": "Amounts.out", + "flow": [ + "Passed as argument to ceil_in_impl/ceil_out_impl", + "Compared to limit", + "Used to construct result Amounts" + ], + "origin": "Input to ceil_in_impl/ceil_out_impl", + "transformations": [ + "Clamped to limit if amount.out > limit", + "Used in DivRoundFunc/MulRoundFunc" + ], + "validated_at": [ + "XRPL_ASSERT(result.out == limit) or XRPL_ASSERT(amount.out <= limit)" + ] + }, + { + "field": "result.in", + "flow": [ + "Constructed from limit or Mul/DivRoundFunc", + "Possibly clamped", + "Returned as part of Amounts" + ], + "origin": "Constructed in ceil_in_impl/ceil_out_impl", + "transformations": [ + "Set to limit or result of rounding function", + "Clamped if necessary" + ], + "validated_at": [ + "XRPL_ASSERT(result.in == limit)" + ] + }, + { + "field": "result.out", + "flow": [ + "Constructed from limit or Mul/DivRoundFunc", + "Possibly clamped", + "Returned as part of Amounts" + ], + "origin": "Constructed in ceil_in_impl/ceil_out_impl", + "transformations": [ + "Set to limit or result of rounding function", + "Clamped if necessary" + ], + "validated_at": [ + "XRPL_ASSERT(result.out == limit)" + ] + } + ], + "description": "Implements the Quality class and related functions for handling quality/rate calculations in the XRPL protocol, including rounding, increment/decrement, and composing qualities.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_value", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at Quality::operator++()", + "issue_pattern": "Missing empty string validation for m_value", + "why_false_positive": "XRPL_ASSERT validates m_value for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "m_value", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at Quality::operator++()", + "issue_pattern": "Missing range validation for m_value", + "why_false_positive": "XRPL_ASSERT validates m_value range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_value", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at Quality::operator--()", + "issue_pattern": "Missing empty string validation for m_value", + "why_false_positive": "XRPL_ASSERT validates m_value for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "m_value", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at Quality::operator--()", + "issue_pattern": "Missing range validation for m_value", + "why_false_positive": "XRPL_ASSERT validates m_value range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result.in", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ceil_in_impl (template function)", + "issue_pattern": "Missing empty string validation for result.in", + "why_false_positive": "XRPL_ASSERT validates result.in for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount.in", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ceil_in_impl (template function)", + "issue_pattern": "Missing empty string validation for amount.in", + "why_false_positive": "XRPL_ASSERT validates amount.in for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result.out", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ceil_out_impl (template function)", + "issue_pattern": "Missing empty string validation for result.out", + "why_false_positive": "XRPL_ASSERT validates result.out for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount.out", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ceil_out_impl (template function)", + "issue_pattern": "Missing empty string validation for amount.out", + "why_false_positive": "XRPL_ASSERT validates amount.out for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Quality.cpp", + "functions": [ + { + "args": [ + "std::uint64_t value" + ], + "lineno": 8, + "name": "Quality::Quality" + }, + { + "args": [ + "Amounts const& amount" + ], + "lineno": 12, + "name": "Quality::Quality" + }, + { + "args": [], + "lineno": 16, + "name": "Quality::operator++" + }, + { + "args": [ + "int" + ], + "lineno": 22, + "name": "Quality::operator++" + }, + { + "args": [], + "lineno": 29, + "name": "Quality::operator--" + }, + { + "args": [ + "int" + ], + "lineno": 37, + "name": "Quality::operator--" + }, + { + "args": [ + "Amounts const& amount", + "STAmount const& limit", + "bool roundUp", + "Quality const& quality" + ], + "lineno": 43, + "name": "ceil_in_impl" + }, + { + "args": [ + "Amounts const& amount", + "STAmount const& limit" + ], + "lineno": 57, + "name": "Quality::ceil_in" + }, + { + "args": [ + "Amounts const& amount", + "STAmount const& limit", + "bool roundUp" + ], + "lineno": 62, + "name": "Quality::ceil_in_strict" + }, + { + "args": [ + "Amounts const& amount", + "STAmount const& limit", + "bool roundUp", + "Quality const& quality" + ], + "lineno": 68, + "name": "ceil_out_impl" + }, + { + "args": [ + "Amounts const& amount", + "STAmount const& limit" + ], + "lineno": 82, + "name": "Quality::ceil_out" + }, + { + "args": [ + "Amounts const& amount", + "STAmount const& limit", + "bool roundUp" + ], + "lineno": 87, + "name": "Quality::ceil_out_strict" + }, + { + "args": [ + "Quality const& lhs", + "Quality const& rhs" + ], + "lineno": 93, + "name": "composed_quality" + }, + { + "args": [ + "int digits" + ], + "lineno": 111, + "name": "Quality::round" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The core validation logic is likely tested in protocol/Quality_test.cpp or protocol/Amounts_test.cpp, which would cover edge cases for operator++/--, ceil_in, ceil_out, and composed_quality. However, coverage gaps may exist for extreme edge cases (e.g., m_value at 0 or max, exponent boundaries in composed_quality, and strict rounding behaviors). Tests should ensure all XRPL_ASSERTs are triggered under invalid input, but if asserts are compiled out in release builds, runtime validation may be missing. No explicit test references are present in this file.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "m_value", + "location": "Quality::operator++()", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures m_value > 0 before decrementing (prevents underflow)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "m_value", + "location": "Quality::operator--()", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures m_value < std::numeric_limits::max() before incrementing (prevents overflow)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "result.in", + "location": "ceil_in_impl (template function)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures result.in == limit when clamping input amount" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "amount.in", + "location": "ceil_in_impl (template function)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures amount.in <= limit when not clamping" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "result.out", + "location": "ceil_out_impl (template function)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures result.out == limit when clamping output amount" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "amount.out", + "location": "ceil_out_impl (template function)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures amount.out <= limit when not clamping" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Quality.cpp.ai.md b/src/libxrpl/protocol/Quality.cpp.ai.md new file mode 100644 index 0000000000..e8b39f65f0 --- /dev/null +++ b/src/libxrpl/protocol/Quality.cpp.ai.md @@ -0,0 +1,64 @@ +# `src/libxrpl/protocol/Quality.cpp` + +## Role in the System + +`Quality.cpp` implements the core arithmetic of XRPL's offer-matching engine. A `Quality` represents the exchange ratio between two currencies — specifically `out / in`, the amount of output currency a taker receives per unit of input. This ratio drives how the order book is sorted and how path payments are evaluated: better offers (higher output per input) rank first. The file is tightly coupled to `STAmount` and the path-finding subsystem, and its calculations directly affect whether a trade is filled and at what price. + +## Internal Encoding + +`Quality` stores its value in a single `uint64_t` `m_value` that mirrors the wire encoding of `STAmount`. The top 8 bits hold a biased exponent (actual exponent + 100, so valid stored exponents are 1–255), and the bottom 56 bits hold the mantissa. This packing is the same layout used by `amountFromQuality()` and `getRate()` — making conversion to and from `STAmount` a cheap bit operation. + +The most non-obvious aspect is **inverted ordering**: a *higher* quality (better deal for the taker) corresponds to a *lower* integer in `m_value`. This is explicit in the comparison operators defined in the header, where `operator<` compares `lhs.m_value > rhs.m_value`. The consequence flows through the arithmetic operators: + +```cpp +Quality& Quality::operator++() { --m_value; return *this; } // advances to higher quality +Quality& Quality::operator--() { ++m_value; return *this; } // retreats to lower quality +``` + +Incrementing `m_value` would make quality worse; decrementing it makes quality better. `XRPL_ASSERT` guards protect both directions against underflow (`m_value > 0`) and overflow (`m_value < UINT64_MAX`), since `m_value == 0` or full saturation would be invalid encoded values. + +## Clamping: `ceil_in` and `ceil_out` + +These four methods (plus their `_strict` variants) are the engine of proportional scaling during offer execution. When path payment fills an offer partially, the engine needs to shrink either the input or output to a limit while keeping the ratio consistent. + +Both directions share a private template: + +```cpp +template +static Amounts ceil_in_impl(Amounts const& amount, STAmount const& limit, bool roundUp, Quality const& quality) +``` + +**`ceil_in`** caps the input side: if `amount.in > limit`, it sets `in = limit` and computes the proportional output via division by the rate (`DivRoundFunc(limit, quality.rate(), ...)`). A secondary clamp `if (result.out > amount.out) result.out = amount.out` then prevents rounding from producing more output than the original offer promised — this is the "no money creation" invariant. An `XRPL_ASSERT` confirms `result.in == limit` after clamping. + +**`ceil_out`** caps the output side symmetrically: it computes the required input via multiplication (`MulRoundFunc(limit, quality.rate(), ...)`), then clamps `result.in` to not exceed the original `amount.in`. + +The template parameter approach (taking a function pointer as a template argument) lets `ceil_in` and `ceil_out` share logic with their `_strict` siblings. The difference between `divRound`/`mulRound` and `divRoundStrict`/`mulRoundStrict` is precision: the non-strict variants ignore low-order bits that could influence rounding decisions, while the strict variants consider all bits. The `_strict` variants were introduced to fix subtle rounding bugs without changing the default behavior for existing callers. + +## Composing Qualities Across Hops + +`composed_quality(lhs, rhs)` computes the effective exchange rate for a two-hop path: if one leg converts A→B at quality `lhs` and the second converts B→C at quality `rhs`, the end-to-end rate is their product. The implementation multiplies the two `STAmount` rates with `mulRound`, then re-encodes the result into the 64-bit packed format: + +```cpp +std::uint64_t stored_exponent(rate.exponent() + 100); +std::uint64_t stored_mantissa(rate.mantissa()); +return Quality((stored_exponent << (64 - 8)) | stored_mantissa); +``` + +An assertion verifies the exponent fits in the 8-bit field (1–255); this would fire if the composed rate is astronomically large or small, indicating a broken path. Both input rates are asserted non-zero to prevent division-by-zero in `mulRound`. + +## Tick-Size Rounding with `round()` + +`round(digits)` truncates quality precision for tick-size enforcement. It extracts the exponent and mantissa from `m_value`, then does a ceiling round of the mantissa to `digits` significant decimal digits using a precomputed power-of-ten table: + +```cpp +mantissa += mod[digits] - 1; +mantissa -= (mantissa % mod[digits]); +``` + +This is a classic ceiling-via-bias technique: adding `mod - 1` before masking ensures any non-zero remainder rounds up rather than down. Rounding up the mantissa means the encoded rate is slightly higher (worse for the taker), which prevents the rounded quality from being mistakenly treated as better than the original. Valid `digits` range from `minTickSize = 3` to `maxTickSize = 16` (enforced by callers). + +## Design Tradeoffs + +The decision to store quality as an inverted integer is a deliberate space and performance optimization: it lets the XRPL order book sort offer IDs directly using unsigned integer comparison without decoding the floating-point representation. The tradeoff is programmer confusion — every place that manipulates `m_value` directly must remember that "bigger integer = worse quality." + +The `ceil_*_strict` variants represent a common XRPL pattern for backward-compatible precision fixes: the old rounding path (`divRound`/`mulRound`) is preserved for code already deployed, while new code can opt into the more precise path without risking consensus-breaking changes on existing transactions. \ No newline at end of file diff --git a/src/libxrpl/protocol/QualityFunction.cpp.ai.json b/src/libxrpl/protocol/QualityFunction.cpp.ai.json new file mode 100644 index 0000000000..ace0f0c02e --- /dev/null +++ b/src/libxrpl/protocol/QualityFunction.cpp.ai.json @@ -0,0 +1,280 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "QualityFunction::QualityFunction" + ], + "entry_point": "QualityFunction::QualityFunction", + "purpose": "Constructs a QualityFunction object from a Quality, validates the rate, and initializes internal fields.", + "validation_points": [ + "if (quality.rate() <= beast::zero) Throw(...)" + ] + }, + { + "call_chain": [ + "QualityFunction::combine" + ], + "entry_point": "QualityFunction::combine", + "purpose": "Combines another QualityFunction into this one, updating internal state and possibly invalidating cached quality.", + "validation_points": [ + "if (m_ != 0) quality_ = std::nullopt;" + ] + }, + { + "call_chain": [ + "QualityFunction::outFromAvgQ" + ], + "entry_point": "QualityFunction::outFromAvgQ", + "purpose": "Calculates an output value based on the average quality, with validation on input and result.", + "validation_points": [ + "if (m_ != 0 && quality.rate() != beast::zero)", + "if (out <= 0) return std::nullopt;" + ] + } + ], + "data_flows": [ + { + "field": "quality.rate()", + "flow": [ + "QualityFunction::QualityFunction parameter", + "quality.rate() checked for <= 0", + "used to compute b_ = 1 / quality.rate()" + ], + "origin": "Passed as argument to QualityFunction constructor or outFromAvgQ", + "transformations": [ + "Checked for <= 0 (validation)", + "Inverted (1 / rate) to compute b_" + ], + "validated_at": "QualityFunction::QualityFunction (constructor)" + }, + { + "field": "b_", + "flow": [ + "Set in constructor", + "Used in combine: b_ *= qf.b_", + "Used in outFromAvgQ: (1 / quality.rate() - b_) / m_" + ], + "origin": "Computed in QualityFunction::QualityFunction as 1 / quality.rate()", + "transformations": [ + "Initial inversion of rate", + "Multiplied by other b_ in combine", + "Subtracted from 1 / quality.rate() in outFromAvgQ" + ], + "validated_at": "Indirectly validated via quality.rate() in constructor and outFromAvgQ" + }, + { + "field": "m_", + "flow": [ + "Set to 0 in constructor", + "Updated in combine: m_ += b_ * qf.m_", + "Used in outFromAvgQ: denominator in output calculation" + ], + "origin": "Initialized to 0 in constructor", + "transformations": [ + "Incremented by b_ * qf.m_ in combine" + ], + "validated_at": "Checked for nonzero in outFromAvgQ" + }, + { + "field": "out (result of calculation in outFromAvgQ)", + "flow": [ + "Calculated in outFromAvgQ", + "Validated: if (out <= 0) return std::nullopt", + "Returned as optional if valid" + ], + "origin": "Computed as (1 / quality.rate() - b_) / m_", + "transformations": [ + "Division and subtraction based on current state" + ], + "validated_at": "outFromAvgQ (explicit if-check for out <= 0)" + } + ], + "description": "Implements the QualityFunction class for handling quality calculations in the XRPL protocol, including construction, combination, and output calculation based on quality rates.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "quality.rate()", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at QualityFunction::QualityFunction (constructor)", + "issue_pattern": "Missing empty string validation for quality.rate()", + "why_false_positive": "explicit if-check validates quality.rate() for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "quality.rate()", + "range", + "bounds", + "validation" + ], + "evidence": "explicit if-check at QualityFunction::QualityFunction (constructor)", + "issue_pattern": "Missing range validation for quality.rate()", + "why_false_positive": "explicit if-check validates quality.rate() range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "quality.rate()", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at QualityFunction::outFromAvgQ", + "issue_pattern": "Missing empty string validation for quality.rate()", + "why_false_positive": "explicit if-check validates quality.rate() for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "quality.rate()", + "range", + "bounds", + "validation" + ], + "evidence": "explicit if-check at QualityFunction::outFromAvgQ", + "issue_pattern": "Missing range validation for quality.rate()", + "why_false_positive": "explicit if-check validates quality.rate() range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "out (result of calculation)", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at QualityFunction::outFromAvgQ", + "issue_pattern": "Missing empty string validation for out (result of calculation)", + "why_false_positive": "explicit if-check validates out (result of calculation) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "out (result of calculation)", + "range", + "bounds", + "validation" + ], + "evidence": "explicit if-check at QualityFunction::outFromAvgQ", + "issue_pattern": "Missing range validation for out (result of calculation)", + "why_false_positive": "explicit if-check validates out (result of calculation) range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/QualityFunction.cpp", + "functions": [ + { + "args": [ + "Quality const& quality", + "QualityFunction::CLOBLikeTag" + ], + "lineno": 9, + "name": "QualityFunction::QualityFunction" + }, + { + "args": [ + "QualityFunction const& qf" + ], + "lineno": 16, + "name": "QualityFunction::combine" + }, + { + "args": [ + "Quality const& quality" + ], + "lineno": 25, + "name": "QualityFunction::outFromAvgQ" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage. Typically, tests for QualityFunction would be found in files like 'QualityFunction_test.cpp', 'Quality_test.cpp', or broader protocol/ledger tests. The main validation paths (rate <= 0, m_ != 0, out <= 0) should be tested for both valid and invalid inputs. Gaps may exist if tests do not cover exception throwing in the constructor, edge cases for zero/negative rates, or the nullopt return paths in outFromAvgQ. Review of the test suite is needed to confirm coverage of all validation branches.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual/explicit validation)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "quality.rate()", + "location": "QualityFunction::QualityFunction (constructor)", + "validated_by": "explicit if-check", + "validates": [ + "Ensures that the rate of the provided Quality object is greater than zero", + "Prevents division by zero or invalid QualityFunction state" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "quality.rate()", + "location": "QualityFunction::outFromAvgQ", + "validated_by": "explicit if-check", + "validates": [ + "Ensures that the rate of the provided Quality object is not zero before division" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "out (result of calculation)", + "location": "QualityFunction::outFromAvgQ", + "validated_by": "explicit if-check", + "validates": [ + "Ensures that the calculated output is positive (greater than zero)" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/QualityFunction.cpp.ai.md b/src/libxrpl/protocol/QualityFunction.cpp.ai.md new file mode 100644 index 0000000000..b643b9b475 --- /dev/null +++ b/src/libxrpl/protocol/QualityFunction.cpp.ai.md @@ -0,0 +1,73 @@ +# `QualityFunction.cpp` — Path Quality as a Linear Function of Output + +## Role in the System + +During payment path optimization, the XRPL engine needs to determine how much output a given path strand can produce while still satisfying a caller-specified quality limit (minimum exchange rate). `QualityFunction` models the *average quality* of a path step — or a composed chain of steps — as a linear function of the output amount: `q(out) = m * out + b`. This file implements the three non-trivial methods of that class: the CLOB-like constructor, `combine()`, and `outFromAvgQ()`. + +The mathematical need for this abstraction arises from AMM liquidity. A CLOB offer has a fixed rate regardless of fill size, but an AMM pool's effective exchange rate degrades as more output is drawn from it (price impact). Modeling quality as a linear function of output lets the path engine efficiently invert the relationship to find the exact output amount that hits a desired quality target, without iterative approximation. + +## Two Construction Modes + +The class uses tag-dispatch to distinguish two fundamentally different step types. + +The `AMMTag` constructor (defined inline in the header) handles a true AMM liquidity step in a single-path scenario. It derives slope and intercept from the AMM's current pool balances and trading fee: + +``` +m_ = -fee / poolIn +b_ = poolOut * fee / poolIn +``` + +This follows from substituting the AMM swap-in formula (`in = (poolGets * poolPays) / (poolGets - out) - poolPays`, adjusted for fees) into `q = out / in`, yielding a linear approximation over `out`. + +The `CLOBLikeTag` constructor — the one implemented in this `.cpp` — handles two logically different cases that share the same math: a CLOB offer (constant quality at any fill size) and a multi-path AMM offer. In both cases, quality does not vary with output, so `m_ = 0` and `b_ = 1 / quality.rate()`. The intercept stores the reciprocal of the quality rate because the composition arithmetic works in reciprocal-rate space. The constructor guards against a zero rate with a `std::runtime_error`, since `b_` is computed by inversion and a zero rate would be meaningless. + +The reason AMM offers in multi-path mode are treated as CLOB-like is deliberate: when an AMM participates alongside other paths, its per-path allocation is fixed proportionally, so its quality is effectively constant from the perspective of that sub-path. The `AMMOffer::getQualityFunc()` makes this dispatch explicitly, returning a `CLOBLikeTag`-constructed function when `ammLiquidity_.multiPath()` is true. + +## `combine()`: Composing Steps Across Hops + +```cpp +void QualityFunction::combine(QualityFunction const& qf) +{ + m_ += b_ * qf.m_; + b_ *= qf.b_; + if (m_ != 0) + quality_ = std::nullopt; +} +``` + +When a payment strand has multiple steps (e.g., XRP → USD → EUR), each step contributes its own quality function. `combine()` chains the calling object (the accumulated function so far) with the next step's function. The update rules implement linear function composition in reciprocal-rate space: the combined slope accumulates the product of the prior intercept and the new step's slope, while the combined intercept multiplies. + +The `quality_` field acts as a cached constant-quality marker: it is only populated for pure CLOB-like steps where `m_ = 0`. Once `combine()` is called with an AMM step (where `qf.m_ != 0`), the combined slope becomes nonzero and `quality_` is cleared to `std::nullopt`. This invalidation is checked by `isConst()`, which in turn signals to the calling code in `StrandFlow.h` that the non-trivial `outFromAvgQ()` computation is needed. + +## `outFromAvgQ()`: Inverting the Quality Function + +```cpp +std::optional QualityFunction::outFromAvgQ(Quality const& quality) +{ + if (m_ != 0 && quality.rate() != beast::zero) + { + saveNumberRoundMode const rm(Number::setround(Number::rounding_mode::upward)); + auto const out = (1 / quality.rate() - b_) / m_; + if (out <= 0) + return std::nullopt; + return out; + } + return std::nullopt; +} +``` + +Given a quality limit (the minimum acceptable average exchange rate), this solves for the output amount `out` at which the path's average quality equals exactly that limit. Algebraically, setting `q(out) = 1/rate` and solving: `out = (1/rate - b_) / m_`. The caller in `StrandFlow.h` uses the result to cap `remainingOut`, ensuring the strand does not produce more output than would violate the quality constraint. + +Three guard conditions all return `std::nullopt`: + +1. **`m_ == 0`**: The function is constant (CLOB-like), meaning quality doesn't depend on output. There's no meaningful `out` to solve for; the limit either passes or fails uniformly, so no capping is needed. +2. **`quality.rate() == zero`**: Guards against division by zero when forming `1/rate`. +3. **`out <= 0`**: A non-positive result means the quality limit is already unachievable at any positive output — the path is effectively dead for this quality constraint. + +The rounding mode is deliberately set to `upward` during the calculation. Because `out` represents an upper bound on how much output to request, rounding it upward is conservative: the actual quality achieved will be at or *better* than the limit, which is the safe direction. Rounding down would risk requesting slightly more than the path can deliver at the required quality, potentially creating rounding-induced money imbalances in ledger accounting. + +## Design Observations + +The `saveNumberRoundMode` RAII guard in `outFromAvgQ()` scopes the rounding-mode change to just the single computation that needs it, restoring the previous mode on exit. This is a defensive pattern that prevents the upward-rounding mode from leaking into unrelated arithmetic elsewhere in the call stack. + +The deliberate separation of `m_` and `b_` as raw `Number` fields (rather than, say, a `std::pair`) reflects their distinct roles in the linear model: `m_` is the AMM price-impact slope (zero for CLOB steps) and `b_` is the baseline reciprocal-rate intercept. The `quality_` optional serves double duty as both a cached quality value for the CLOB fast-path and a boolean flag (`isConst()` uses `has_value()`), avoiding a separate boolean member. \ No newline at end of file diff --git a/src/libxrpl/protocol/RPCErr.cpp.ai.json b/src/libxrpl/protocol/RPCErr.cpp.ai.json new file mode 100644 index 0000000000..24cb5db41a --- /dev/null +++ b/src/libxrpl/protocol/RPCErr.cpp.ai.json @@ -0,0 +1,236 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 6, + "name": "RPCErr" + } + ], + "code_paths": [ + { + "call_chain": [ + "rpcError", + "RPC::inject_error" + ], + "entry_point": "rpcError", + "purpose": "Creates a JSON object representing an RPC error, injecting error details into the object.", + "validation_points": [] + }, + { + "call_chain": [ + "isRpcError" + ], + "entry_point": "isRpcError", + "purpose": "Checks if a given Json::Value represents an RPC error object by validating its structure.", + "validation_points": [ + "jvResult.isObject()", + "jvResult.isMember(jss::error)" + ] + } + ], + "data_flows": [ + { + "field": "jvResult", + "flow": [ + "rpcError: jvResult created", + "RPC::inject_error: error fields injected into jvResult", + "rpcError: jvResult returned" + ], + "origin": "Created as Json::Value(Json::objectValue) in rpcError", + "transformations": [ + "Initialized as empty JSON object", + "Populated with error fields by RPC::inject_error" + ], + "validated_at": "isRpcError: validated by isObject() and isMember(jss::error)" + }, + { + "field": "jvResult (input to isRpcError)", + "flow": [ + "Produced by rpcError or similar", + "Passed to isRpcError for validation" + ], + "origin": "Typically output of rpcError or similar error-producing function", + "transformations": [ + "No transformation in isRpcError; only validation" + ], + "validated_at": "isRpcError: isObject() and isMember(jss::error)" + } + ], + "description": "Provides deprecated utility functions for handling and identifying RPC errors in XRPL, using JSON values.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "error (via jss::error)", + "validation", + "missing", + "check" + ], + "evidence": "Field error (via jss::error) validated by JsonCpp (Json::Value), jss:: (field name constants)", + "issue_pattern": "Missing validation for error (via jss::error)", + "why_false_positive": "JsonCpp (Json::Value), jss:: (field name constants) validates error (via jss::error) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "jvResult", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::isObject() at isRpcError", + "issue_pattern": "Missing empty string validation for jvResult", + "why_false_positive": "Json::Value::isObject() validates jvResult for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "jvResult", + "type", + "validation", + "check" + ], + "evidence": "Json::Value::isObject() at isRpcError", + "issue_pattern": "Missing type validation for jvResult", + "why_false_positive": "Json::Value::isObject() validates jvResult type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "jvResult", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::isMember(jss::error) at isRpcError", + "issue_pattern": "Missing empty string validation for jvResult", + "why_false_positive": "Json::Value::isMember(jss::error) validates jvResult for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/RPCErr.cpp", + "functions": [ + { + "args": [ + "iError" + ], + "lineno": 9, + "name": "rpcError" + }, + { + "args": [ + "jvResult" + ], + "lineno": 16, + "name": "isRpcError" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is marked as deprecated and is a thin wrapper around error object creation and validation. Direct unit tests for rpcError and isRpcError may not exist, but their behavior is likely covered indirectly by higher-level RPC error handling tests. Look for tests in files like 'test/rpc/ErrorHandling_test.cpp', 'test/rpc/JsonRpc_test.cpp', or similar. Gaps: No explicit tests for deprecated wrappers; edge cases (e.g., malformed JSON, missing error fields) may not be directly tested.", + "validation_architecture": { + "auto_validated_fields": [ + "error (via jss::error)" + ], + "framework": "JsonCpp (Json::Value), jss:: (field name constants)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "jvResult", + "location": "isRpcError", + "validated_by": "Json::Value::isObject()", + "validates": [ + "Checks if jvResult is a JSON object" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "jvResult", + "location": "isRpcError", + "validated_by": "Json::Value::isMember(jss::error)", + "validates": [ + "Checks if jvResult contains the 'error' field (using jss::error for field name)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/RPCErr.cpp.ai.md b/src/libxrpl/protocol/RPCErr.cpp.ai.md new file mode 100644 index 0000000000..33c0faa42d --- /dev/null +++ b/src/libxrpl/protocol/RPCErr.cpp.ai.md @@ -0,0 +1,21 @@ +# `RPCErr.cpp` — Deprecated RPC Error Utilities + +This file is a thin compatibility shim, providing two free functions that predate the `RPC` namespace error API. Both are explicitly marked deprecated by the original author (VFALCO), and callers should migrate to the richer facilities in `ErrorCodes.h` instead. + +## Role in the System + +XRPL's RPC layer communicates errors to clients as JSON objects. The canonical, current API for constructing and inspecting those objects lives in `RPC::inject_error()`, `RPC::make_error()`, and `RPC::contains_error()` — all declared in `include/xrpl/protocol/ErrorCodes.h`. The two functions here, `rpcError()` and `isRpcError()`, are older entry points that wrap that same machinery but expose a less expressive interface. They exist only to avoid breaking call sites that have not yet been updated. + +## Function Details + +`rpcError(error_code_i iError)` constructs a fresh `Json::Value` object and delegates immediately to `RPC::inject_error()`. That function consults the statically-compiled `ErrorInfo` table — also in `ErrorCodes.h` — to populate the result with the error code's canonical `token` (a machine-readable string like `"invalidParams"`), its human-readable `message`, and the numeric code itself. The returned JSON value is what gets sent back to an API client. The newer equivalent is `RPC::make_error(code)`, which does exactly the same thing but with an explicit and non-deprecated name. + +`isRpcError(Json::Value jvResult)` performs a two-step structural check: it verifies the value is a JSON object and that it contains a member named `jss::error`. Because `RPC::inject_error()` always writes the error details under the `error` key, this check is sufficient to distinguish an error response from a normal result. The replacement is `RPC::contains_error(json)`, declared in `ErrorCodes.h`, which serves the same purpose. Note that `isRpcError` takes its argument by value rather than const reference — a minor inefficiency that is inconsequential given the deprecated status of the function. + +## The `RPCErr` Struct Forward Declaration + +The `.cpp` file contains `struct RPCErr;` — a forward declaration with no corresponding definition anywhere in the translation unit. This is an orphaned artifact, likely left over from an earlier refactoring. It has no effect on compilation or behaviour. + +## Migration Guidance + +New code should use `RPC::make_error(code)` instead of `rpcError(code)`, and `RPC::contains_error(json)` instead of `isRpcError(json)`. For invalid-parameter errors specifically, `RPC::make_param_error(message)` and the family of `missing_field_error`, `invalid_field_error`, and `expected_field_error` helpers provide more expressive, self-documenting alternatives. All of these are inline functions defined directly in `ErrorCodes.h`, so there is no additional link-time cost to switching. \ No newline at end of file diff --git a/src/libxrpl/protocol/Rate2.cpp.ai.json b/src/libxrpl/protocol/Rate2.cpp.ai.json new file mode 100644 index 0000000000..b03b376df8 --- /dev/null +++ b/src/libxrpl/protocol/Rate2.cpp.ai.json @@ -0,0 +1,500 @@ +{ + "args": [ + { + "lineno": 14, + "name": "rate" + }, + { + "lineno": 21, + "name": "fee" + }, + { + "lineno": 27, + "name": "amount" + }, + { + "lineno": 27, + "name": "rate" + }, + { + "lineno": 38, + "name": "amount" + }, + { + "lineno": 38, + "name": "rate" + }, + { + "lineno": 38, + "name": "roundUp" + }, + { + "lineno": 49, + "name": "amount" + }, + { + "lineno": 49, + "name": "rate" + }, + { + "lineno": 49, + "name": "asset" + }, + { + "lineno": 49, + "name": "roundUp" + }, + { + "lineno": 62, + "name": "amount" + }, + { + "lineno": 62, + "name": "rate" + }, + { + "lineno": 73, + "name": "amount" + }, + { + "lineno": 73, + "name": "rate" + }, + { + "lineno": 73, + "name": "roundUp" + }, + { + "lineno": 84, + "name": "amount" + }, + { + "lineno": 84, + "name": "rate" + }, + { + "lineno": 84, + "name": "asset" + }, + { + "lineno": 84, + "name": "roundUp" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "multiply", + "XRPL_ASSERT(rate.value)", + "detail::as_amount(rate)", + "multiply(amount, STAmount, Asset)" + ], + "entry_point": "multiply", + "purpose": "Multiplies an STAmount by a Rate, returning a new STAmount. Validates that the rate is nonzero.", + "validation_points": [ + "multiply (XRPL_ASSERT(rate.value))" + ] + }, + { + "call_chain": [ + "multiplyRound", + "XRPL_ASSERT(rate.value)", + "detail::as_amount(rate)", + "mulRound(amount, STAmount, Asset, roundUp)" + ], + "entry_point": "multiplyRound", + "purpose": "Multiplies an STAmount by a Rate with rounding, returning a new STAmount. Validates that the rate is nonzero.", + "validation_points": [ + "multiplyRound (XRPL_ASSERT(rate.value))" + ] + }, + { + "call_chain": [ + "multiplyRound(amount, rate, asset, roundUp)", + "XRPL_ASSERT(rate.value)", + "detail::as_amount(rate)", + "mulRound(amount, STAmount, asset, roundUp)" + ], + "entry_point": "multiplyRound (with Asset)", + "purpose": "Multiplies an STAmount by a Rate with rounding and explicit Asset, returning a new STAmount. Validates that the rate is nonzero.", + "validation_points": [ + "multiplyRound (with Asset) (XRPL_ASSERT(rate.value))" + ] + }, + { + "call_chain": [ + "divide", + "XRPL_ASSERT(rate.value)", + "detail::as_amount(rate)", + "divide(amount, STAmount, Asset)" + ], + "entry_point": "divide", + "purpose": "Divides an STAmount by a Rate, returning a new STAmount. Validates that the rate is nonzero.", + "validation_points": [ + "divide (XRPL_ASSERT(rate.value))" + ] + }, + { + "call_chain": [ + "divideRound", + "XRPL_ASSERT(rate.value)", + "detail::as_amount(rate)", + "divRound(amount, STAmount, Asset, roundUp)" + ], + "entry_point": "divideRound", + "purpose": "Divides an STAmount by a Rate with rounding, returning a new STAmount. Validates that the rate is nonzero.", + "validation_points": [ + "divideRound (XRPL_ASSERT(rate.value))" + ] + }, + { + "call_chain": [ + "divideRound(amount, rate, asset, roundUp)", + "XRPL_ASSERT(rate.value)", + "detail::as_amount(rate)", + "divRound(amount, STAmount, asset, roundUp)" + ], + "entry_point": "divideRound (with Asset)", + "purpose": "Divides an STAmount by a Rate with rounding and explicit Asset, returning a new STAmount. Validates that the rate is nonzero.", + "validation_points": [ + "divideRound (with Asset) (XRPL_ASSERT(rate.value))" + ] + }, + { + "call_chain": [ + "transferFeeAsRate", + "Rate{static_cast(fee) * 10000}" + ], + "entry_point": "nft::transferFeeAsRate", + "purpose": "Converts a uint16_t fee to a Rate object. No validation here.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "rate.value", + "flow": [ + "Function parameter (Rate const& rate)", + "XRPL_ASSERT(rate.value)", + "detail::as_amount(rate) \u2192 STAmount", + "Used in multiply/divide/mulRound/divRound" + ], + "origin": "Passed as part of Rate parameter to multiply/divide/multiplyRound/divideRound", + "transformations": [ + "Checked for nonzero (validation)", + "Converted to STAmount via as_amount" + ], + "validated_at": "Immediately at function entry (XRPL_ASSERT in each function)" + }, + { + "field": "amount", + "flow": [ + "Function parameter (STAmount const& amount)", + "Passed through to multiply/divide/mulRound/divRound" + ], + "origin": "Passed as STAmount parameter to multiply/divide/multiplyRound/divideRound", + "transformations": [ + "Combined with rate (as STAmount) in arithmetic operation" + ], + "validated_at": "Not directly validated in this file" + }, + { + "field": "fee (uint16_t)", + "flow": [ + "Function parameter (fee)", + "Converted to uint32_t and multiplied by 10000", + "Used to construct Rate" + ], + "origin": "Passed to nft::transferFeeAsRate", + "transformations": [ + "Casted and scaled" + ], + "validated_at": "Not validated in this file" + } + ], + "description": "Implements utility functions for multiplying and dividing STAmount values by Rate, including rounding and asset-specific variants, primarily for use in XRPL NFT and asset protocols.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "rate.value", + "validation", + "missing", + "check" + ], + "evidence": "Field rate.value validated by XRPL_ASSERT (custom assertion macro/function)", + "issue_pattern": "Missing validation for rate.value", + "why_false_positive": "XRPL_ASSERT (custom assertion macro/function) validates rate.value automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rate.value", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at multiply", + "issue_pattern": "Missing empty string validation for rate.value", + "why_false_positive": "XRPL_ASSERT validates rate.value for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rate.value", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at multiplyRound (STAmount const&, Rate const&, bool)", + "issue_pattern": "Missing empty string validation for rate.value", + "why_false_positive": "XRPL_ASSERT validates rate.value for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rate.value", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at multiplyRound (STAmount const&, Rate const&, Asset const&, bool)", + "issue_pattern": "Missing empty string validation for rate.value", + "why_false_positive": "XRPL_ASSERT validates rate.value for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rate.value", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at divide", + "issue_pattern": "Missing empty string validation for rate.value", + "why_false_positive": "XRPL_ASSERT validates rate.value for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rate.value", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at divideRound (STAmount const&, Rate const&, bool)", + "issue_pattern": "Missing empty string validation for rate.value", + "why_false_positive": "XRPL_ASSERT validates rate.value for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rate.value", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at divideRound (STAmount const&, Rate const&, Asset const&, bool)", + "issue_pattern": "Missing empty string validation for rate.value", + "why_false_positive": "XRPL_ASSERT validates rate.value for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Rate2.cpp", + "functions": [ + { + "args": [ + "rate" + ], + "lineno": 14, + "name": "as_amount" + }, + { + "args": [ + "fee" + ], + "lineno": 21, + "name": "transferFeeAsRate" + }, + { + "args": [ + "amount", + "rate" + ], + "lineno": 27, + "name": "multiply" + }, + { + "args": [ + "amount", + "rate", + "roundUp" + ], + "lineno": 38, + "name": "multiplyRound" + }, + { + "args": [ + "amount", + "rate", + "asset", + "roundUp" + ], + "lineno": 49, + "name": "multiplyRound" + }, + { + "args": [ + "amount", + "rate" + ], + "lineno": 62, + "name": "divide" + }, + { + "args": [ + "amount", + "rate", + "roundUp" + ], + "lineno": 73, + "name": "divideRound" + }, + { + "args": [ + "amount", + "rate", + "asset", + "roundUp" + ], + "lineno": 84, + "name": "divideRound" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "detail" + }, + { + "lineno": 19, + "name": "nft" + } + ], + "test_coverage_notes": "The validation of rate.value (nonzero) is enforced at the entry of all multiply/divide/multiplyRound/divideRound functions via XRPL_ASSERT. These functions are likely tested in unit tests for Rate, STAmount, and NFT transfer fee logic, typically found in files like Rate_test.cpp, STAmount_test.cpp, or NFT-related test files. However, the code does not validate the range or upper bound of rate.value, only that it is nonzero. There is no explicit test coverage shown here for invalid (zero) rates, negative values, or overflow scenarios. The transferFeeAsRate function does not validate its input. Test coverage gaps may include: passing zero or invalid rates, edge cases for fee conversion, and ensuring that XRPL_ASSERT triggers as expected.", + "validation_architecture": { + "auto_validated_fields": [ + "rate.value" + ], + "framework": "XRPL_ASSERT (custom assertion macro/function)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely abort or exception, depending on XRPL_ASSERT implementation)", + "field": "rate.value", + "location": "multiply", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the Rate input to multiply is nonzero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely abort or exception, depending on XRPL_ASSERT implementation)", + "field": "rate.value", + "location": "multiplyRound (STAmount const&, Rate const&, bool)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the Rate input to multiplyRound is nonzero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely abort or exception, depending on XRPL_ASSERT implementation)", + "field": "rate.value", + "location": "multiplyRound (STAmount const&, Rate const&, Asset const&, bool)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the Rate input to multiplyRound (with Asset) is nonzero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely abort or exception, depending on XRPL_ASSERT implementation)", + "field": "rate.value", + "location": "divide", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the Rate input to divide is nonzero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely abort or exception, depending on XRPL_ASSERT implementation)", + "field": "rate.value", + "location": "divideRound (STAmount const&, Rate const&, bool)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the Rate input to divideRound is nonzero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely abort or exception, depending on XRPL_ASSERT implementation)", + "field": "rate.value", + "location": "divideRound (STAmount const&, Rate const&, Asset const&, bool)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the Rate input to divideRound (with Asset) is nonzero" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Rate2.cpp.ai.md b/src/libxrpl/protocol/Rate2.cpp.ai.md new file mode 100644 index 0000000000..32147fea21 --- /dev/null +++ b/src/libxrpl/protocol/Rate2.cpp.ai.md @@ -0,0 +1,54 @@ +# `src/libxrpl/protocol/Rate2.cpp` — Transfer Rate Arithmetic Implementation + +## Role in the System + +This file is the implementation counterpart to `include/xrpl/protocol/Rate.h`. It provides the concrete math for applying XRPL transfer rates — the per-transfer fees charged by IOU issuers and NFT creators — to `STAmount` values. The header declares the interface and the `parityRate` extern; this file defines both, making it the single translation unit responsible for fee application across the payment engine, offer-crossing logic, and NFT royalty calculation. + +## The `parityRate` Constant + +```cpp +Rate const parityRate(QUALITY_ONE); +``` + +`QUALITY_ONE` is `1'000'000'000` (10⁹), the fixed-point identity value for the ledger's rate encoding. A `Rate` equal to this value means 1:1 — the sender pays exactly what the recipient receives, no fee. Defining `parityRate` as a file-scope constant rather than recomputing it inline lets every arithmetic function short-circuit with a single equality check before entering the more expensive `STAmount` arithmetic path. + +## The `detail::as_amount` Bridge + +The central design question in this file is: how do you multiply or divide an `STAmount` by a dimensionless ratio stored as a billion-scale integer? The answer is `detail::as_amount()`: + +```cpp +STAmount as_amount(Rate const& rate) +{ + return {noIssue(), rate.value, -9, false}; +} +``` + +This constructs an `STAmount` representing `rate.value × 10⁻⁹` — a dimensionless decimal. For example, a `rate.value` of `1,010,000,000` (1% fee) becomes the `STAmount` `1.010000000`. With this encoding, the existing `STAmount::multiply` and `STAmount::divide` infrastructure handles all the fixed-point precision correctly without any custom arithmetic. The `noIssue()` sentinel signals that the value carries no currency identity, which is correct since a rate is dimensionless. This approach keeps fee computation consistent with the same precision model used everywhere else in the ledger engine. + +## Arithmetic Functions and the Parity Short-Circuit + +All six arithmetic functions share the same structural pattern: assert nonzero rate, short-circuit on parity, then delegate to the underlying `STAmount` arithmetic: + +```cpp +if (rate == parityRate) + return amount; +return multiply(amount, detail::as_amount(rate), amount.asset()); +``` + +The parity short-circuit is a meaningful performance optimisation. The vast majority of accounts have no transfer fee (the engine returns `parityRate` when `sfTransferRate` is absent), so most calls in payment routing never reach the `STAmount` arithmetic path at all. + +The two overloads of `multiplyRound` and `divideRound` serve distinct use cases. The single-asset overload (taking only `amount`, `rate`, and `roundUp`) preserves the currency of the input amount — used when fee calculation stays in one currency, as in IOU payment routing. The dual-asset overload (additionally taking an explicit `Asset`) specifies a different output currency, used during offer crossing where the input and output are denominated in different assets. Both delegate to `mulRound`/`divRound` from `STAmount.h`. + +## NFT Transfer Fees — `nft::transferFeeAsRate` + +NFT royalties are encoded in the ledger as a `uint16_t` in basis points (hundredths of a percent, 0–50,000). The billion-scale `Rate` encoding expects units where 10⁹ equals 100%, so the conversion factor is 10,000: + +```cpp +return Rate{static_cast(fee) * 10000}; +``` + +A fee of 50,000 basis points (the protocol maximum of 50%) becomes `500,000,000`, safely within `uint32_t` range and below `QUALITY_ONE`. The cast to `uint32_t` before multiplication prevents overflow since `50000 × 10000 = 500,000,000`, which fits in 32 bits. This function lives in the `nft` sub-namespace to make the unit distinction explicit: code handling ordinary IOU transfer rates (already billion-scaled from `sfTransferRate`) should never call it. + +## Validation and Invariants + +Every arithmetic function opens with `XRPL_ASSERT(rate.value, ...)`, catching zero rates at the debug boundary. A zero `Rate` is semantically undefined (it would represent an infinitely large fee or division by zero) and should never reach this layer from well-formed ledger data. The assertion fires in debug builds; in production, a zero rate would silently produce garbage arithmetic, making the guard critical for catching upstream construction errors during development. `transferFeeAsRate` carries no such guard — its input range is enforced by transaction validation before the value ever reaches the ledger. \ No newline at end of file diff --git a/src/libxrpl/protocol/Rules.cpp.ai.json b/src/libxrpl/protocol/Rules.cpp.ai.json new file mode 100644 index 0000000000..be888d90a3 --- /dev/null +++ b/src/libxrpl/protocol/Rules.cpp.ai.json @@ -0,0 +1,289 @@ +{ + "args": [ + { + "lineno": 26, + "name": "r" + }, + { + "lineno": 44, + "name": "presets" + }, + { + "lineno": 48, + "name": "presets" + }, + { + "lineno": 49, + "name": "digest" + }, + { + "lineno": 50, + "name": "amendments" + }, + { + "lineno": 60, + "name": "feature" + }, + { + "lineno": 65, + "name": "other" + }, + { + "lineno": 68, + "name": "presets" + }, + { + "lineno": 73, + "name": "presets" + }, + { + "lineno": 74, + "name": "digest" + }, + { + "lineno": 75, + "name": "amendments" + }, + { + "lineno": 80, + "name": "feature" + }, + { + "lineno": 86, + "name": "feature" + }, + { + "lineno": 90, + "name": "other" + }, + { + "lineno": 94, + "name": "other" + }, + { + "lineno": 98, + "name": "feature" + } + ], + "classes": [ + { + "args": [ + "presets", + "digest", + "amendments" + ], + "lineno": 36, + "name": "Rules::Impl" + }, + { + "args": [ + "presets", + "digest", + "amendments" + ], + "lineno": 67, + "name": "Rules" + } + ], + "code_paths": [ + { + "call_chain": [ + "setCurrentTransactionRules", + "Number::setMantissaScale", + "getCurrentTransactionRulesRef" + ], + "entry_point": "setCurrentTransactionRules", + "purpose": "Sets the global transaction rules, updates mantissa scale based on enabled features, and stores the new Rules object.", + "validation_points": [] + }, + { + "call_chain": [ + "Rules::enabled", + "XRPL_ASSERT(impl_, ...)", + "Rules::Impl::enabled" + ], + "entry_point": "Rules::enabled", + "purpose": "Checks if a feature is enabled in the current Rules object.", + "validation_points": [ + "XRPL_ASSERT(impl_, ...)" + ] + }, + { + "call_chain": [ + "Rules::operator==", + "XRPL_ASSERT(impl_ && other.impl_, ...)", + "Rules::Impl::operator==", + "XRPL_ASSERT(presets_ == other.presets_, ...)" + ], + "entry_point": "Rules::operator==", + "purpose": "Compares two Rules objects for equality, ensuring both are initialized and have matching presets if digests are present.", + "validation_points": [ + "XRPL_ASSERT(impl_ && other.impl_, ...)", + "XRPL_ASSERT(presets_ == other.presets_, ...)" + ] + }, + { + "call_chain": [ + "isFeatureEnabled", + "getCurrentTransactionRules", + "Rules::enabled" + ], + "entry_point": "isFeatureEnabled", + "purpose": "Checks if a feature is enabled in the globally set Rules.", + "validation_points": [ + "Rules::enabled: XRPL_ASSERT(impl_, ...)" + ] + } + ], + "data_flows": [ + { + "field": "presets_", + "flow": [ + "Rules::Rules(presets)", + "Rules::Impl(presets)", + "Impl::presets_" + ], + "origin": "Passed as parameter to Rules::Rules and Rules::Impl constructors", + "transformations": [ + "Stored as const reference in Impl" + ], + "validated_at": "XRPL_ASSERT(presets_ == other.presets_, ...) in Rules::Impl::operator==" + }, + { + "field": "impl_ (Rules::impl_)", + "flow": [ + "Rules::Rules", + "Rules::impl_", + "Used in Rules::enabled, Rules::operator==, etc." + ], + "origin": "Constructed in Rules::Rules (via std::make_shared)", + "transformations": [ + "Shared pointer assignment" + ], + "validated_at": "XRPL_ASSERT(impl_, ...) in Rules::enabled; XRPL_ASSERT(impl_ && other.impl_, ...) in Rules::operator==" + }, + { + "field": "digest_", + "flow": [ + "Rules::Rules(presets, digest, amendments)", + "Rules::Impl(presets, digest, amendments)", + "Impl::digest_" + ], + "origin": "Passed as parameter to Rules::Rules and Rules::Impl constructors", + "transformations": [ + "Optional assignment" + ], + "validated_at": "Checked for presence in Rules::Impl::operator==" + }, + { + "field": "feature", + "flow": [ + "isFeatureEnabled(feature)", + "Rules::enabled(feature)", + "Impl::enabled(feature)" + ], + "origin": "Parameter to Rules::enabled and isFeatureEnabled", + "transformations": [ + "Checked for presence in presets_ and set_" + ], + "validated_at": "Indirectly validated by XRPL_ASSERT(impl_, ...) in Rules::enabled" + } + ], + "description": "Implements the Rules class and related functions for managing protocol feature flags and amendments in the XRPL system, including global transaction rules and feature enablement checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Type safety for all parameters and members via C++ type system", + "validation", + "missing", + "check" + ], + "evidence": "Field Type safety for all parameters and members via C++ type system validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for Type safety for all parameters and members via C++ type system", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates Type safety for all parameters and members via C++ type system automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "presets_ (presets parameter)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at Rules::Impl::operator==", + "issue_pattern": "Missing empty string validation for presets_ (presets parameter)", + "why_false_positive": "XRPL_ASSERT validates presets_ (presets parameter) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Rules.cpp", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "getCurrentTransactionRulesRef" + }, + { + "args": [], + "lineno": 20, + "name": "getCurrentTransactionRules" + }, + { + "args": [ + "r" + ], + "lineno": 25, + "name": "setCurrentTransactionRules" + }, + { + "args": [ + "feature" + ], + "lineno": 97, + "name": "isFeatureEnabled" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file contains core logic for feature flag validation and rule management. Typical test coverage would be in unit tests for Rules, likely in files such as 'Rules_test.cpp', 'Feature_test.cpp', or protocol/amendment-related tests. The XRPL_ASSERT validations are runtime checks and may not be directly tested unless assertions are specifically checked in tests. There is no evidence in this file of explicit test hooks or test-only code. Gaps: No direct validation of presets_ on construction (only checked for equality in operator==), and no explicit error handling for invalid presets or null impl_ beyond assertions. Test coverage may miss assertion failures unless assertions are enabled and tested for.", + "validation_architecture": { + "auto_validated_fields": [ + "Type safety for all parameters and members via C++ type system" + ], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "Assertion failure (XRPL_ASSERT)", + "field": "presets_ (presets parameter)", + "location": "Rules::Impl::operator==", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that the presets_ set in both compared Impl objects are equal before comparing digests" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Rules.cpp.ai.md b/src/libxrpl/protocol/Rules.cpp.ai.md new file mode 100644 index 0000000000..272ad7a74d --- /dev/null +++ b/src/libxrpl/protocol/Rules.cpp.ai.md @@ -0,0 +1,37 @@ +# `src/libxrpl/protocol/Rules.cpp` + +## Role in the System + +`Rules.cpp` implements the machinery that makes XRPL's amendment system visible to transaction-processing code. Every transaction on the ledger must run under a consistent view of which protocol features are active. This file provides that view — a lightweight, immutable snapshot of enabled amendments — and also manages the thread/coroutine-local slot that holds the active ruleset so that any code in the processing path can query it without having to accept a `Rules` argument explicitly. + +## The `Rules` and `Rules::Impl` Classes + +`Rules` uses the pimpl idiom: the actual state lives in `Rules::Impl`, referenced through `std::shared_ptr`. The header comment calls this out explicitly — it makes `Rules` cheap to copy because all copies share the same `Impl`. Because ledger processing may propagate a `Rules` object through many call frames and data structures, this sharing matters in practice. + +`Impl` stores two things: a reference to `presets_` (an externally-owned `unordered_set` of features that are always on, such as genesis-era behavior or test overrides), and `set_`, a locally-owned `unordered_set` populated from the ledger's `sfAmendments` field. Notably, `presets_` uses `beast::uhash<>` while `set_` uses `hardened_hash<>`. The hardened hash guards against hash-flooding when inserting validator-supplied amendment IDs into the owned set, whereas presets are controlled by node operators and don't need the same protection. + +The `enabled()` method checks `presets_` first (a O(1) lookup into the externally-owned set), then `set_` (O(1) in the owned set). No locking is needed because both the `Impl` and the `set_` it owns are immutable after construction. + +### Construction Path and Encapsulation + +The constructor that accepts a ledger digest and an `STVector256` of amendments — the one that actually builds the full ruleset from on-ledger state — is `private`. Only two friend functions declared in `Rules.h` can invoke it: both overloads of `makeRulesGivenLedger`, defined in `src/libxrpl/ledger/ReadView.cpp`. There, `makeRulesGivenLedger` reads the `keylet::amendments()` state-map entry from a `DigestAwareReadView`, extracts the `sfAmendments` vector, and calls the private constructor. This design enforces that production `Rules` objects are always grounded in a real ledger view. The publicly accessible constructor that takes only `presets` exists specifically for genesis ledger semantics and unit tests. + +### Equality via Digest + +`Impl::operator==` deliberately avoids comparing the full amendment sets. Instead it compares `digest_` values — an `optional` hash of the ledger object containing the amendment list. If both `Impl` objects have no digest (both are genesis/preset-only rules), they're considered equal. If exactly one has a digest, they're unequal. If both have digests, it compares the digests and also asserts that `presets_` match: two `Rules` constructed from different preset configurations but the same ledger amendments would produce different behavior, making a digest-equal comparison incorrect. The outer `Rules::operator==` short-circuits on pointer identity before delegating to `Impl`, so comparing a `Rules` object with itself is O(1) pointer comparison. + +## Thread/Coroutine-Local Current Rules + +Transaction processing code needs access to the active `Rules` without threading the object through every call site. `setCurrentTransactionRules` and `getCurrentTransactionRules` manage a `LocalValue>` — a type from `xrpl/basics/LocalValue.h` that provides per-coroutine or per-thread storage. When XRPL's job system runs code on a coroutine, `LocalValue` stores a per-coroutine instance; when running on a plain thread, it falls back to thread-local semantics via `boost::thread_specific_ptr`. This dual-mode storage is critical because XRPL's application layer uses coroutines heavily, and true thread-locals would cause coroutines sharing a thread to stomp each other's rule context. + +The static `LocalValue` is wrapped inside a function (`getCurrentTransactionRulesRef`) rather than declared at namespace scope. This sidesteps C++ static initialization order issues — the `LocalValue` is constructed on first call, not at program startup. + +The header also provides `CurrentTransactionRulesGuard`, a RAII wrapper that saves the current rules on construction and restores them on destruction, enabling safe rule overrides in nested processing contexts and test harnesses. + +## Arithmetic Precision Side Effect + +`setCurrentTransactionRules` carries a non-obvious side effect: it calls `Number::setMantissaScale` immediately after storing the new rules. The `Number` type used throughout XRPL's financial arithmetic supports two mantissa ranges — `small` (standard XRP precision) and `large` (extended range for DeFi features). When `featureSingleAssetVault` or `featureLendingProtocol` is enabled, the large range is required to avoid overflow in AMM and lending calculations. Rather than having `Number` query the current rules on every arithmetic operation (which would be called millions of times per ledger), the precision mode is pushed once when rules change. If `r` is `nullopt` (no rules set), large numbers are allowed as a safe default. This push-rather-than-pull architecture is called out explicitly in a code comment. + +## The `isFeatureEnabled` Free Function + +`isFeatureEnabled` is the primary API for feature checks scattered throughout transaction processing. It fetches the thread/coroutine-local `Rules`, returns `false` if no rules are set, and otherwise delegates to `Rules::enabled`. The safe-default behavior (returning `false` when rules are absent) prevents accidental feature activation during startup or in code paths that haven't set up a rule context. \ No newline at end of file diff --git a/src/libxrpl/protocol/SField.cpp.ai.json b/src/libxrpl/protocol/SField.cpp.ai.json new file mode 100644 index 0000000000..0a239a1b42 --- /dev/null +++ b/src/libxrpl/protocol/SField.cpp.ai.json @@ -0,0 +1,327 @@ +{ + "args": [ + { + "lineno": 22, + "name": "private_access_tag_t pat" + }, + { + "lineno": 22, + "name": "Args&&... args" + }, + { + "lineno": 67, + "name": "private_access_tag_t" + }, + { + "lineno": 67, + "name": "SerializedTypeID tid" + }, + { + "lineno": 67, + "name": "int fv" + }, + { + "lineno": 67, + "name": "char const* fn" + }, + { + "lineno": 67, + "name": "int meta" + }, + { + "lineno": 67, + "name": "IsSigning signing" + }, + { + "lineno": 87, + "name": "private_access_tag_t" + }, + { + "lineno": 87, + "name": "int fc" + }, + { + "lineno": 87, + "name": "char const* fn" + }, + { + "lineno": 106, + "name": "int code" + }, + { + "lineno": 117, + "name": "SField const& f1" + }, + { + "lineno": 117, + "name": "SField const& f2" + }, + { + "lineno": 132, + "name": "std::string const& fieldName" + } + ], + "classes": [ + { + "args": [], + "lineno": 16, + "name": "SField::private_access_tag_t" + } + ], + "code_paths": [ + { + "call_chain": [ + "SField::SField", + "XRPL_ASSERT (fieldCode uniqueness)", + "XRPL_ASSERT (fieldName uniqueness)", + "knownCodeToField[fieldCode] = this", + "knownNameToField[fieldName] = this" + ], + "entry_point": "SField::SField (constructor)", + "purpose": "Constructs an SField, validates uniqueness of fieldCode and fieldName, and registers the field in global maps.", + "validation_points": [ + "XRPL_ASSERT(!knownCodeToField.contains(fieldCode))", + "XRPL_ASSERT(!knownNameToField.contains(fieldName))" + ] + }, + { + "call_chain": [ + "TypedField::TypedField", + "SField::SField" + ], + "entry_point": "TypedField::TypedField", + "purpose": "Constructs a typed SField, forwarding to SField constructor, thus triggering the same validation.", + "validation_points": [ + "XRPL_ASSERT(!knownCodeToField.contains(fieldCode))", + "XRPL_ASSERT(!knownNameToField.contains(fieldName))" + ] + }, + { + "call_chain": [ + "SField::getField", + "knownCodeToField.find(code)", + "return *(it->second) or sfInvalid" + ], + "entry_point": "SField::getField(int code)", + "purpose": "Retrieves an SField by code, returning sfInvalid if not found. No validation, but relies on prior registration/validation.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "fieldCode", + "flow": [ + "SField::SField", + "XRPL_ASSERT(!knownCodeToField.contains(fieldCode))", + "knownCodeToField[fieldCode] = this", + "SField::getField(int code) lookup" + ], + "origin": "SField::SField constructor (computed via field_code(tid, fv) or passed directly)", + "transformations": [ + "Computed from SerializedTypeID and fieldValue (or passed directly)", + "Checked for uniqueness", + "Registered in global map" + ], + "validated_at": "SField::SField (XRPL_ASSERT)" + }, + { + "field": "fieldName", + "flow": [ + "SField::SField", + "XRPL_ASSERT(!knownNameToField.contains(fieldName))", + "knownNameToField[fieldName] = this" + ], + "origin": "SField::SField constructor (passed as fn)", + "transformations": [ + "Passed as string", + "Checked for uniqueness", + "Registered in global map" + ], + "validated_at": "SField::SField (XRPL_ASSERT)" + }, + { + "field": "SField instance", + "flow": [ + "SField::SField", + "Registered in knownCodeToField and knownNameToField", + "Accessible via SField::getField" + ], + "origin": "Constructed via SField::SField or TypedField::TypedField", + "transformations": [ + "Constructed with validated fields", + "Stored in global maps" + ], + "validated_at": "SField::SField (XRPL_ASSERT)" + } + ], + "description": "Defines and manages SField objects, which represent fields in the XRPL protocol, including registration, lookup, and construction logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "fieldCode", + "validation", + "missing", + "check" + ], + "evidence": "Field fieldCode validated by XRPL_ASSERT (custom assertion macro)", + "issue_pattern": "Missing validation for fieldCode", + "why_false_positive": "XRPL_ASSERT (custom assertion macro) validates fieldCode automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "fieldName", + "validation", + "missing", + "check" + ], + "evidence": "Field fieldName validated by XRPL_ASSERT (custom assertion macro)", + "issue_pattern": "Missing validation for fieldName", + "why_false_positive": "XRPL_ASSERT (custom assertion macro) validates fieldName automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fieldCode", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SField::SField (constructor)", + "issue_pattern": "Missing empty string validation for fieldCode", + "why_false_positive": "XRPL_ASSERT validates fieldCode for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fieldName", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SField::SField (constructor)", + "issue_pattern": "Missing empty string validation for fieldName", + "why_false_positive": "XRPL_ASSERT validates fieldName for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/SField.cpp", + "functions": [ + { + "args": [ + "private_access_tag_t pat", + "Args&&... args" + ], + "lineno": 22, + "name": "TypedField::TypedField" + }, + { + "args": [ + "private_access_tag_t", + "SerializedTypeID tid", + "int fv", + "char const* fn", + "int meta", + "IsSigning signing" + ], + "lineno": 67, + "name": "SField::SField" + }, + { + "args": [ + "private_access_tag_t", + "int fc", + "char const* fn" + ], + "lineno": 87, + "name": "SField::SField" + }, + { + "args": [ + "int code" + ], + "lineno": 106, + "name": "SField::getField" + }, + { + "args": [ + "SField const& f1", + "SField const& f2" + ], + "lineno": 117, + "name": "SField::compare" + }, + { + "args": [ + "std::string const& fieldName" + ], + "lineno": 132, + "name": "SField::getField" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic (uniqueness of fieldCode and fieldName) is enforced at construction time via XRPL_ASSERT. Typical test coverage would be in protocol or SField-specific unit tests, likely in files such as 'test/protocol/SField_test.cpp' or similar. However, the code comments note that some SFields are constructed for historical/test reasons, implying that test coverage exists for edge cases. Gaps: There is no runtime validation for dynamic field registration (all fields are registered at static initialization), and no explicit tests for duplicate fieldCode/fieldName at runtime (since XRPL_ASSERT may abort or throw). If XRPL_ASSERT is compiled out, uniqueness is not enforced. There is also no test coverage for malformed or intentionally conflicting SField construction at runtime.", + "validation_architecture": { + "auto_validated_fields": [ + "fieldCode", + "fieldName" + ], + "framework": "XRPL_ASSERT (custom assertion macro)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "fieldCode", + "location": "SField::SField (constructor)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the fieldCode is unique among all SFields", + "Checks that knownCodeToField does not already contain this fieldCode" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "fieldName", + "location": "SField::SField (constructor)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the fieldName is unique among all SFields", + "Checks that knownNameToField does not already contain this fieldName" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/SField.cpp.ai.md b/src/libxrpl/protocol/SField.cpp.ai.md new file mode 100644 index 0000000000..dd407cd5e9 --- /dev/null +++ b/src/libxrpl/protocol/SField.cpp.ai.md @@ -0,0 +1,72 @@ +# `SField.cpp` — Protocol Field Registry + +## Role in the System + +`SField.cpp` is the single authoritative source of truth for every named field in the XRPL binary protocol. Serialized types — transactions, ledger entries, validation messages, metadata — are all composed of typed, named fields. Every field that can appear in any XRPL binary object must be registered here before the process starts. This file performs that mass registration at static initialization time, building two global lookup tables that the rest of the codebase queries at runtime. + +## The X-Macro Field Definition Pattern + +The most architecturally significant choice in this file is its use of the X-macro technique via ``. That macro file contains the master list of all XRPL protocol fields (roughly 300+), written once using `TYPED_SFIELD` and `UNTYPED_SFIELD` invocations. The same file is included in two very different contexts: + +- In `SField.h`, the macros expand to `extern` variable declarations, so every translation unit that includes the header can reference any field by name. +- In `SField.cpp`, the macros expand to actual `const` variable definitions, constructing each field object exactly once. + +This design eliminates the need to maintain two synchronized lists. Any developer adding a new field only touches `sfields.macro`. The naming convention is enforced mechanically: the `sfName` token in each macro call becomes both the C++ variable name and the human-readable field name. The field name string is derived by stripping the `sf` prefix from the variable name via `std::string_view(#sfName).substr(2)`. + +## Construction Access Control + +`SField` objects are immutable value-typed descriptors that must exist for the lifetime of the process. Allowing arbitrary construction of new `SField` objects at runtime would break the registry invariants. To prevent this, the file defines `SField::private_access_tag_t` — a struct whose definition is forward-declared as `public` in the class but is only actually defined in `SField.cpp`. A file-local `static` instance named `access` is the only token that satisfies the constructor's first parameter requirement: + +```cpp +static SField::private_access_tag_t access; +``` + +Every constructor call (`SField(access, ...)` or the macro-expanded `TypedField(access, ...)`) must pass this token. Since `private_access_tag_t` is only constructible from within this translation unit, it is effectively a compile-time access control mechanism. No external code can create an `SField` — they can only look them up. + +## Field Codes and the Dual Registry + +Each field is identified by a 32-bit `fieldCode` computed as `(SerializedTypeID << 16) | fieldValue`. This packs the type family (e.g., `STI_UINT32 = 2`) and the per-type index into a single integer, making code comparisons O(1) and canonically ordered. Fields with the same index in different type families are entirely distinct. + +The two static `unordered_map` members, `knownCodeToField` and `knownNameToField`, are populated in each constructor: + +```cpp +knownCodeToField[fieldCode] = this; +knownNameToField[fieldName] = this; +``` + +Both maps hold raw `const` pointers. This is safe because every `SField` is a `const` global with program lifetime — there is no deallocation. The lookup functions `getField(int code)` and `getField(std::string const& fieldName)` simply query these maps, returning `sfInvalid` on a miss rather than throwing or returning null. + +## Uniqueness Enforcement via `XRPL_ASSERT` + +Both constructors check that neither the `fieldCode` nor the `fieldName` already appear in the maps before inserting: + +```cpp +XRPL_ASSERT(!knownCodeToField.contains(fieldCode), "... : fieldCode is unique"); +XRPL_ASSERT(!knownNameToField.contains(fieldName), "... : fieldName is unique"); +``` + +`XRPL_ASSERT` expands to `ALWAYS_OR_UNREACHABLE`, which in turn expands to `assert()`. In a release build (`NDEBUG` defined), these checks compile away entirely, meaning a duplicate field in `sfields.macro` would silently shadow the earlier registration. When running under the Antithesis fuzzing instrumentation, failed assertions allow execution to continue rather than aborting. The practical implication is that duplicate-field detection is a debug-build-only safety net, not a runtime guarantee. + +## Historical Outliers and Discardable Fields + +Four fields bypass the macros and are constructed directly: + +- `sfInvalid` (code `-1`) and `sfGeneric` (code `0`) use the two-argument constructor that accepts a raw `fieldCode` integer rather than separate type/value components. `sfInvalid` serves as the "not found" sentinel returned by lookup misses; `sfGeneric` is a catch-all for untyped contexts. +- `sfHash` and `sfIndex` are `STI_UINT256` fields with `fieldValue` of 257 and 258 respectively — values deliberately above 256. The `isDiscardable()` predicate returns `true` when `fieldValue > 256`, and `shouldInclude()` gates binary serialization on `fieldValue < 256`. These two fields therefore exist only in JSON representations of ledger state, carrying computed values (the hash of an object, its ledger key) that cannot be embedded in the binary encoding of the object itself. + +## `TypedField` and Compile-Time Type Safety + +`TypedField` is a thin template wrapper over `SField` that tags each field with the C++ type of its serialized payload (e.g., `SF_UINT32` is `TypedField>`). The constructor simply forwards all arguments to `SField`: + +```cpp +template +template +TypedField::TypedField(private_access_tag_t pat, Args&&... args) + : SField(pat, std::forward(args)...) +``` + +The type parameter is never stored at runtime; it only matters to the template machinery in `STObject` and `STField` accessors that need to enforce that you can't read a `sfFlags` field as an `STAmount`. The `OptionaledField` wrapper and the `operator~` overload provide syntactic sugar for expressing optional field access. + +## `compare()` and Canonical Ordering + +`SField::compare()` returns `-1`, `0`, or `1` in the style of a comparator, but uses `0` as a sentinel for "illegal combination" when either field has a non-positive code. This covers `sfInvalid` and `sfGeneric`. The ordering is purely by `fieldCode`, which means fields are sorted first by type family, then by their index within that family — matching the canonical binary serialization order defined by the XRPL protocol specification. \ No newline at end of file diff --git a/src/libxrpl/protocol/SOTemplate.cpp.ai.json b/src/libxrpl/protocol/SOTemplate.cpp.ai.json new file mode 100644 index 0000000000..69980e6f9b --- /dev/null +++ b/src/libxrpl/protocol/SOTemplate.cpp.ai.json @@ -0,0 +1,263 @@ +{ + "args": [ + { + "lineno": 13, + "name": "uniqueFields" + }, + { + "lineno": 13, + "name": "commonFields" + }, + { + "lineno": 18, + "name": "uniqueFields" + }, + { + "lineno": 18, + "name": "commonFields" + }, + { + "lineno": 45, + "name": "sField" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SOTemplate::SOTemplate(std::initializer_list, std::initializer_list)", + "SOTemplate::SOTemplate(std::vector, std::vector)" + ], + "entry_point": "SOTemplate::SOTemplate(std::initializer_list, std::initializer_list)", + "purpose": "Constructs a SOTemplate from initializer lists by converting them to vectors and delegating to the main constructor.", + "validation_points": [ + "SOTemplate::SOTemplate(std::vector, std::vector)" + ] + }, + { + "call_chain": [ + "SOTemplate::SOTemplate(std::vector, std::vector)" + ], + "entry_point": "SOTemplate::SOTemplate(std::vector, std::vector)", + "purpose": "Main constructor: merges unique and common fields, validates them, and builds index mapping.", + "validation_points": [ + "SOTemplate::SOTemplate(std::vector, std::vector) (field index range and duplicate check)" + ] + }, + { + "call_chain": [ + "SOTemplate::getIndex(SField const&)" + ], + "entry_point": "SOTemplate::getIndex(SField const&)", + "purpose": "Returns the index of a field in the template, validating the field index range.", + "validation_points": [ + "SOTemplate::getIndex(SField const&) (field index range check)" + ] + } + ], + "data_flows": [ + { + "field": "SField.getNum()", + "flow": [ + "SOElement.sField()", + "SField.getNum()", + "Validation in SOTemplate constructor (range and duplicate check)", + "indices_ mapping" + ], + "origin": "SField instance from SOElement in uniqueFields/commonFields", + "transformations": [ + "Extracted from SOElement", + "Checked for valid range (must be >0 and , std::vector)" + }, + { + "field": "SField.getNum() (in getIndex)", + "flow": [ + "SField.getNum()", + "Validation in getIndex() (range check)", + "indices_ lookup" + ], + "origin": "SField instance passed to getIndex()", + "transformations": [ + "Checked for valid range (must be >0 and , std::vector)", + "issue_pattern": "Missing empty string validation for SField.getNum()", + "why_false_positive": "manual if-check validates SField.getNum() for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "SField.getNum()", + "range", + "bounds", + "validation" + ], + "evidence": "manual if-check at SOTemplate::SOTemplate(std::vector, std::vector)", + "issue_pattern": "Missing range validation for SField.getNum()", + "why_false_positive": "manual if-check validates SField.getNum() range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "SField.getNum() (duplicate check)", + "empty", + "string", + "validation" + ], + "evidence": "manual if-check using getIndex() at SOTemplate::SOTemplate(std::vector, std::vector)", + "issue_pattern": "Missing empty string validation for SField.getNum() (duplicate check)", + "why_false_positive": "manual if-check using getIndex() validates SField.getNum() (duplicate check) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "SField.getNum()", + "empty", + "string", + "validation" + ], + "evidence": "manual if-check at SOTemplate::getIndex(SField const&)", + "issue_pattern": "Missing empty string validation for SField.getNum()", + "why_false_positive": "manual if-check validates SField.getNum() for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "SField.getNum()", + "range", + "bounds", + "validation" + ], + "evidence": "manual if-check at SOTemplate::getIndex(SField const&)", + "issue_pattern": "Missing range validation for SField.getNum()", + "why_false_positive": "manual if-check validates SField.getNum() range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/SOTemplate.cpp", + "functions": [ + { + "args": [ + "std::initializer_list uniqueFields", + "std::initializer_list commonFields" + ], + "lineno": 12, + "name": "SOTemplate" + }, + { + "args": [ + "std::vector uniqueFields", + "std::vector commonFields" + ], + "lineno": 17, + "name": "SOTemplate" + }, + { + "args": [ + "SField const& sField" + ], + "lineno": 44, + "name": "getIndex" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code relies on runtime exceptions for validation failures (invalid or duplicate field indices). Typical test coverage would be in unit tests for SOTemplate, likely in files such as SOTemplate_test.cpp or protocol/SOTemplate_test.cpp. Tests should cover: (1) construction with valid fields, (2) construction with out-of-range field indices, (3) construction with duplicate fields, (4) getIndex with valid and invalid indices. Gaps may exist if tests do not explicitly check exception throwing for invalid/duplicate fields or do not test edge cases (e.g., boundary values for field indices).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "manual validation (no external framework)", + "validation_layer": "business_logic (constructor and method level)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "SField.getNum()", + "location": "SOTemplate::SOTemplate(std::vector, std::vector)", + "validated_by": "manual if-check", + "validates": [ + "Ensures SField index is > 0", + "Ensures SField index is < indices_.size() (i.e., within valid range)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "SField.getNum() (duplicate check)", + "location": "SOTemplate::SOTemplate(std::vector, std::vector)", + "validated_by": "manual if-check using getIndex()", + "validates": [ + "Ensures no duplicate SField indices in the template" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "SField.getNum()", + "location": "SOTemplate::getIndex(SField const&)", + "validated_by": "manual if-check", + "validates": [ + "Ensures SField index is > 0", + "Ensures SField index is < indices_.size() (i.e., within valid range)" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/SOTemplate.cpp.ai.md b/src/libxrpl/protocol/SOTemplate.cpp.ai.md new file mode 100644 index 0000000000..303a451c1b --- /dev/null +++ b/src/libxrpl/protocol/SOTemplate.cpp.ai.md @@ -0,0 +1,45 @@ +# `SOTemplate.cpp` — Serialized Object Schema Registry + +## Role in the System + +The XRPL ledger serializes every transaction and ledger object as a typed collection of named fields. `SOTemplate` is the compile-time-initialized schema that answers the question "which fields are legal here, and how are they required?" for each known object type. Every transaction format (`ttPAYMENT`, `ttOFFER_CREATE`, etc.) and every ledger entry format (`ltACCOUNT_ROOT`, `ltOFFER`, etc.) has exactly one `SOTemplate` that describes its field schema. The implementation in this file is short — 62 lines — but the two invariants it enforces at construction time are load-bearing for the entire serialization layer. + +## Key Types + +`SOTemplate` operates on two supporting types defined in `SOTemplate.h`: + +- **`SField`** — a globally registered descriptor for a single serialized field (e.g., `sfAmount`, `sfDestination`). Each `SField` carries a permanent numeric identifier `fieldNum` assigned at registration time. `SField::getNumFields()` returns the total count of all registered fields. + +- **`SOElement`** — a thin wrapper pairing an `SField` reference with a presence style (`soeREQUIRED`, `soeOPTIONAL`, `soeDEFAULT`) and an optional MPT-support flag for amount and issue fields. Its constructor calls `isUseful()` on the field, which rejects fields with a non-positive `fieldCode` — `sfInvalid` and similar sentinel fields cannot appear in a template. + +The `SOEStyle` enum encodes the three presence rules that `STObject` enforces during serialization: `soeREQUIRED` means the field must always be present, `soeOPTIONAL` means it may appear with a default value, and `soeDEFAULT` means it may appear but must not carry the type's default value (and inner objects of this kind must be constructed through `STObject::makeInnerObject()`). + +## Construction and the Two-List Design + +The constructor accepts two separate field lists: `uniqueFields` and `commonFields`. The split is semantically meaningful at the `KnownFormats` layer — every transaction type has its own unique fields, but all transactions share a set of common fields (e.g., `sfFee`, `sfSequence`, `sfSignature`). By accepting them separately, callers can maintain that common set once and pass it to every format without duplicating the field descriptors. Inside the constructor the two lists are merged into a single `elements_` vector (unique fields first) and the distinction is discarded — from `SOTemplate`'s perspective the merged sequence is the authoritative ordered list. + +There are two constructor overloads: one accepting `std::initializer_list` pairs (for the typical in-source literal syntax) and one accepting `std::vector` pairs (for programmatic construction). The initializer-list overload simply converts to vectors and delegates to the vector overload, so all real logic lives in one place. + +## The Index Lookup Table + +The core data structure is `indices_`, a `std::vector` pre-sized to `SField::getNumFields() + 1` and filled with `-1` (meaning "not in this template"). After merging the element lists, the constructor iterates over `elements_` and writes the element's position `i` into `indices_[sField.getNum()]`. The result is a direct-address table: given any `SField`, `getIndex()` returns its position in `elements_` in O(1) with a single vector subscript — no hashing, no comparisons, no cache misses from pointer chasing. + +This design is correct because `SField::fieldNum` values are dense integers assigned at static initialization time. Using `fieldNum` as a vector index gives O(1) without hash-map overhead, which matters because `getIndex()` is called on every field access during serialization and deserialization of every transaction or ledger object. + +## Invariants Enforced at Construction + +Two correctness checks are applied in the constructor loop: + +**Range check.** Every field's `getNum()` must satisfy `0 < fieldNum < indices_.size()`. The lower bound guards against sentinel/invalid fields that carry non-positive field numbers. The upper bound guards against fields registered after this `SOTemplate` was constructed (which would produce an out-of-bounds write). Because `SField::getNumFields()` is called at construction time, the size is snapshotted — if additional fields were registered dynamically afterward, they could not be added to this template (which is intentional; templates are immutable after construction). The same range check is repeated in `getIndex()`, making lookups defensively safe even if a caller passes an unusual field. + +**Duplicate check.** Before recording `indices_[sField.getNum()] = i`, the constructor calls `getIndex(sField)` and throws if the returned value is not `-1`. This catches programmer errors where the same `SField` appears twice in either list or once in each list. Since a duplicate would silently overwrite the earlier entry in `indices_`, breaking the field's position mapping, the check is necessary to catch mistakes that would otherwise produce silent data corruption. + +Both checks throw `std::runtime_error` via `Throw<>`, the XRPL macro that ensures the exception propagates correctly even across ABI boundaries. Because `SOTemplate` objects are constructed at program startup (as part of `TxFormats::getInstance()`, `LedgerFormats::getInstance()`, etc.), any violation is a fatal programming error caught immediately during initialization — not during live transaction processing. + +## Move-Only Semantics + +The header explicitly declares `SOTemplate` as move-only. The comment acknowledges that copying both `elements_` and `indices_` vectors is expensive, and since templates are created once and queried many times, there is no legitimate use case for copying. `KnownFormats::Item` stores `SOTemplate` by value inside a `std::forward_list` (so item addresses never change), and it passes field vectors by move into the constructor, keeping allocation costs at construction time only. + +## Relationship to `KnownFormats` and `STObject` + +`KnownFormats` (the base class for `TxFormats`, `LedgerFormats`, and `InnerObjectFormats`) stores one `KnownFormats::Item` per registered format, and each `Item` owns an `SOTemplate`. When the deserialization layer processes a serialized blob, it retrieves the appropriate `Item` by transaction or ledger type, extracts its `SOTemplate`, and uses `getIndex()` to validate that each field in the blob is permitted and to read the associated `SOEStyle`. The template's iteration interface (`begin()`/`end()`) allows callers to enumerate all expected fields in definition order, which is used when constructing default-initialized objects and when verifying that all required fields are present. \ No newline at end of file diff --git a/src/libxrpl/protocol/STAccount.cpp.ai.json b/src/libxrpl/protocol/STAccount.cpp.ai.json new file mode 100644 index 0000000000..3682a037f5 --- /dev/null +++ b/src/libxrpl/protocol/STAccount.cpp.ai.json @@ -0,0 +1,333 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "STAccount" + } + ], + "code_paths": [ + { + "call_chain": [ + "STAccount::STAccount(SField const& n, Buffer const& v)", + "if (v.size() != uint160::bytes) Throw(...)" + ], + "entry_point": "STAccount::STAccount(SField const& n, Buffer const& v)", + "purpose": "Constructs an STAccount from a Buffer (account id bytes) and validates the size of the Buffer.", + "validation_points": [ + "Explicit size check: if (v.size() != uint160::bytes) Throw(...)" + ] + }, + { + "call_chain": [ + "STAccount::add(Serializer& s) const", + "XRPL_ASSERT(getFName().isBinary(), ...)", + "XRPL_ASSERT(getFName().fieldType == STI_ACCOUNT, ...)", + "s.addVL(value_.data(), size)" + ], + "entry_point": "STAccount::add(Serializer& s) const", + "purpose": "Serializes the STAccount, validating that the field is binary and of the correct type before serialization.", + "validation_points": [ + "XRPL_ASSERT(getFName().isBinary(), ...)", + "XRPL_ASSERT(getFName().fieldType == STI_ACCOUNT, ...)" + ] + }, + { + "call_chain": [ + "STAccount::STAccount(SerialIter& sit, SField const& name)", + "STAccount::STAccount(SField const& n, Buffer const& v)", + "if (v.size() != uint160::bytes) Throw(...)" + ], + "entry_point": "STAccount::STAccount(SerialIter& sit, SField const& name)", + "purpose": "Constructs an STAccount from serialized data, validating the Buffer size.", + "validation_points": [ + "Explicit size check in STAccount(SField const& n, Buffer const& v)" + ] + } + ], + "data_flows": [ + { + "field": "value_ (AccountID)", + "flow": [ + "Buffer v (input to constructor)", + "Validated for size in STAccount(SField const& n, Buffer const& v)", + "memcpy to value_", + "Used in serialization (add), comparison (isEquivalent), and text conversion (getText)" + ], + "origin": "Buffer v (from serialized data or direct input)", + "transformations": [ + "Buffer is checked for correct size", + "Copied into value_ (AccountID, uint160)", + "Serialized as VL or converted to Base58" + ], + "validated_at": "STAccount(SField const& n, Buffer const& v) constructor" + }, + { + "field": "n (SField)", + "flow": [ + "SField n (input to constructor)", + "Stored in STBase", + "Accessed via getFName() in add()" + ], + "origin": "Passed to constructor by caller", + "transformations": [ + "Checked for isBinary and fieldType in add()" + ], + "validated_at": "STAccount::add (via XRPL_ASSERT)" + }, + { + "field": "default_ (bool)", + "flow": [ + "Set to true if Buffer is empty or value is beast::zero", + "Used in add() to determine serialization size", + "Used in isEquivalent and getText" + ], + "origin": "Set in constructors based on input", + "transformations": [ + "Set based on input Buffer or AccountID" + ], + "validated_at": "Implicitly validated by logic in constructors" + } + ], + "description": "Implements the STAccount class for representing and serializing XRPL account IDs, including constructors, serialization, and comparison logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Buffer v (account id bytes)", + "empty", + "string", + "validation" + ], + "evidence": "explicit size check in constructor at STAccount::STAccount(SField const& n, Buffer const& v)", + "issue_pattern": "Missing empty string validation for Buffer v (account id bytes)", + "why_false_positive": "explicit size check in constructor validates Buffer v (account id bytes) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Buffer v (account id bytes)", + "format", + "validation", + "invalid" + ], + "evidence": "explicit size check in constructor at STAccount::STAccount(SField const& n, Buffer const& v)", + "issue_pattern": "Missing format validation for Buffer v (account id bytes)", + "why_false_positive": "explicit size check in constructor validates Buffer v (account id bytes) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "SField n (field type and binary property)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at STAccount::add(Serializer& s) const", + "issue_pattern": "Missing empty string validation for SField n (field type and binary property)", + "why_false_positive": "XRPL_ASSERT macro validates SField n (field type and binary property) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "SField n (field type and binary property)", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT macro at STAccount::add(Serializer& s) const", + "issue_pattern": "Missing type validation for SField n (field type and binary property)", + "why_false_positive": "XRPL_ASSERT macro validates SField n (field type and binary property) type" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STAccount.cpp", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "STAccount" + }, + { + "args": [ + "SField const& n" + ], + "lineno": 17, + "name": "STAccount" + }, + { + "args": [ + "SField const& n", + "Buffer const& v" + ], + "lineno": 21, + "name": "STAccount" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 35, + "name": "STAccount" + }, + { + "args": [ + "SField const& n", + "AccountID const& v" + ], + "lineno": 39, + "name": "STAccount" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 43, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 48, + "name": "move" + }, + { + "args": [], + "lineno": 53, + "name": "getSType" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 58, + "name": "add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 68, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 74, + "name": "isDefault" + }, + { + "args": [], + "lineno": 78, + "name": "getText" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "STAccount is a core serialization/field type in XRPL. Typical test coverage would be in protocol/ or serialization test suites, e.g., STAccount_test.cpp, STObject_test.cpp, or broader transaction/serialization tests. Tests should cover: construction from valid/invalid Buffers (size mismatch), serialization (add), and field property assertions. Gaps may exist if: (1) Buffer size errors are not tested (invalid input), (2) SField property assertions are not tested (e.g., non-binary or wrong type), (3) edge cases for default_ logic are not covered. Review of test files is needed to confirm coverage of these validation paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (Throw<>, XRPL_ASSERT), no external validation framework", + "validation_layer": "business_logic (constructor and serialization logic)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw(\"Invalid STAccount size\"))", + "field": "Buffer v (account id bytes)", + "location": "STAccount::STAccount(SField const& n, Buffer const& v)", + "validated_by": "explicit size check in constructor", + "validates": [ + "Checks that if Buffer v is not empty, its size must be exactly uint160::bytes (20 bytes)", + "Prevents invalid-length account IDs from being accepted" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "assertion failure (likely contract violation, may throw or abort depending on XRPL_ASSERT implementation)", + "field": "SField n (field type and binary property)", + "location": "STAccount::add(Serializer& s) const", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that the field is binary (getFName().isBinary())", + "Checks that the field type is STI_ACCOUNT (getFName().fieldType == STI_ACCOUNT)" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STAccount.cpp.ai.md b/src/libxrpl/protocol/STAccount.cpp.ai.md new file mode 100644 index 0000000000..118440144c --- /dev/null +++ b/src/libxrpl/protocol/STAccount.cpp.ai.md @@ -0,0 +1,52 @@ +# `STAccount.cpp` — Serialized Account Field Implementation + +## Role in the System + +`STAccount` is the XRPL protocol's typed container for account identifiers as they appear in ledger objects and transactions. Every field in a transaction or ledger entry that holds an account address — sender, destination, issuer — is represented as an `STAccount` instance within the serialized type (ST) framework. The file implements the concrete methods of the `STAccount` class declared in the corresponding header, wiring up construction, binary serialization, equivalence, and text conversion. + +## Inheritance and Design + +`STAccount` inherits from both `STBase` and `CountedObject`. `STBase` is the polymorphic root of all serialized field types in the XRPL codebase; it binds a value to an `SField` descriptor that encodes the field's name, type code, and encoding properties. `CountedObject` is a lightweight CRTP mixin that tracks live instance counts for diagnostic purposes. + +The class stores the account ID as an `AccountID` — a typedef for `base_uint<160, AccountIDTag>` — a strongly-typed 160-bit integer. A notable comment in the header explains that the original implementation kept the value in an `STBlob` (a variable-length byte buffer), but since account IDs are *always* exactly 160 bits, a fixed-size `uint160` is more efficient. Crucially, the wire format was deliberately kept identical to `STBlob`: values are written as VL-encoded (variable-length prefixed) blobs via `addVL()`. This means the optimization is purely internal; on the wire and in serialized ledger data, the encoding is unchanged. + +## The `default_` Flag and the Zero State + +A `bool default_` member tracks whether the field has been explicitly assigned a value. Both the default constructor and the `SField`-only constructor initialize `value_` to `beast::zero` and set `default_ = true`. This mirrors `STBlob`'s notion of a zero-size default blob. + +The flag drives two behaviors. In `add()`, when `default_` is true, the field serializes as a zero-length VL blob — an empty byte sequence — rather than 20 zero bytes. In `getText()`, a default field returns an empty string rather than the base58 encoding of the all-zeros pseudo-account. This matters because the all-zeros value carries special meaning in the XRPL protocol (it is used as the XRP issuer sentinel), and serializing it as an empty blob cleanly distinguishes "field not set" from "field set to the zero account." + +## Construction Paths + +There are four meaningful constructors, each serving a different call site: + +- **`STAccount(SField const& n)`** creates a named but unset field, used when building new ledger objects before populating fields. +- **`STAccount(SField const& n, AccountID const& v)`** sets the value directly from a typed `AccountID`, clearing `default_`. This is the typical path in application code. +- **`STAccount(SField const& n, Buffer const& v)`** accepts raw bytes, as returned from parsing VL blobs. An empty buffer is accepted and leaves the field in the default state (this is the round-trip representation of a default field). A non-empty buffer must be exactly `uint160::bytes` (20 bytes); any other size causes a `Throw("Invalid STAccount size")`. The comment acknowledges the historical question of whether throwing from a constructor is safe here, but notes that the calling context (`STVar::STVar(SerialIter&, SField const&)`) already throws, so propagation is expected. +- **`STAccount(SerialIter& sit, SField const& name)`** is the deserialization entry point. It calls `sit.getVLBuffer()` to extract the variable-length blob and delegates directly to the Buffer constructor, inheriting its size validation. + +## Serialization: `add()` + +```cpp +void STAccount::add(Serializer& s) const +{ + XRPL_ASSERT(getFName().isBinary(), ...); + XRPL_ASSERT(getFName().fieldType == STI_ACCOUNT, ...); + int const size = isDefault() ? 0 : uint160::bytes; + s.addVL(value_.data(), size); +} +``` + +Two `XRPL_ASSERT` calls guard that the associated `SField` is a binary field of type `STI_ACCOUNT`. These are programmer-error checks — they fire in debug builds if `add()` is called on a field that was improperly constructed with a mismatched field descriptor. In release builds, depending on the macro's implementation, they may be no-ops. + +The actual serialization writes either zero bytes (for a default account) or the 20-byte raw value, both wrapped in VL encoding by `addVL()`. This preserves full round-trip fidelity with the blob-based format. + +## Equivalence vs. Comparison + +`isEquivalent()` is the polymorphic comparison required by `STBase`. It first `dynamic_cast`s the argument to `STAccount const*` (returning false on type mismatch), then checks that both `default_` and `value_` agree. Two `STAccount` fields are semantically equivalent only if they share the same default state and the same 160-bit value. + +The header also defines non-member `operator==` and `operator<` in several overloads. These compare only `value()`, ignoring `default_`. This is a deliberate design choice: when account IDs are used as keys (e.g., in sorted ledger entries or offer books), only the actual address matters, not whether the field was "explicitly set." + +## Placement-New Support + +The `copy()` and `move()` private methods delegate to `STBase::emplace()`, a helper that performs either placement-new into a caller-supplied buffer (if the object fits) or heap allocation. This is the mechanism by which `detail::STVar` — the type-erased variant that stores typed serialized fields — manages `STAccount` instances efficiently without forcing heap allocation for every field. \ No newline at end of file diff --git a/src/libxrpl/protocol/STAmount.cpp.ai.json b/src/libxrpl/protocol/STAmount.cpp.ai.json new file mode 100644 index 0000000000..5a26551ad8 --- /dev/null +++ b/src/libxrpl/protocol/STAmount.cpp.ai.json @@ -0,0 +1,978 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Number::rounding_mode mode" + ], + "lineno": 951, + "name": "DontAffectNumberRoundMode" + } + ], + "code_paths": [ + { + "call_chain": [ + "getSNValue", + "getInt64Value" + ], + "entry_point": "getSNValue", + "purpose": "Extracts a native (XRP) amount as int64, validating type and value.", + "validation_points": [ + "getInt64Value: Validates amount.native() is true (else throws)", + "getInt64Value: XRPL_ASSERT on amount.exponent() == 0", + "getInt64Value: XRPL_ASSERT on mantissa roundtrip" + ] + }, + { + "call_chain": [ + "getMPTValue", + "getInt64Value" + ], + "entry_point": "getMPTValue", + "purpose": "Extracts an MPT amount as int64, validating type and value.", + "validation_points": [ + "getInt64Value: Validates amount.holds() is true (else throws)", + "getInt64Value: XRPL_ASSERT on amount.exponent() == 0", + "getInt64Value: XRPL_ASSERT on mantissa roundtrip" + ] + }, + { + "call_chain": [ + "areComparable", + "std::visit (lambda with type traits)" + ], + "entry_point": "areComparable", + "purpose": "Checks if two STAmount instances are comparable (same type/currency/issue).", + "validation_points": [ + "areComparable: Type and issue comparability checked via type traits and value comparison" + ] + }, + { + "call_chain": [ + "STAmount::STAmount (SerialIter, SField)", + "parsing/bitmask logic" + ], + "entry_point": "STAmount::STAmount (SerialIter& sit, SField const& name)", + "purpose": "Deserializes an STAmount from serialized data, determines type, sets fields.", + "validation_points": [ + "Bitmask checks for native/MPT/XRP type", + "Field assignments based on type" + ] + } + ], + "data_flows": [ + { + "field": "amount.native()", + "flow": [ + "STAmount constructed (from SerialIter or direct)", + "amount.native() called in getSNValue", + "Passed to getInt64Value for validation" + ], + "origin": "STAmount instance (possibly from deserialization or construction)", + "transformations": [ + "Bitmask logic in constructor sets native flag" + ], + "validated_at": "getInt64Value (via getSNValue)" + }, + { + "field": "amount.holds()", + "flow": [ + "STAmount constructed (from SerialIter or direct)", + "amount.holds() called in getMPTValue", + "Passed to getInt64Value for validation" + ], + "origin": "STAmount instance (from deserialization or construction)", + "transformations": [ + "Bitmask logic in constructor sets MPT flag" + ], + "validated_at": "getInt64Value (via getMPTValue)" + }, + { + "field": "amount.exponent()", + "flow": [ + "STAmount constructed", + "amount.exponent() called in getInt64Value" + ], + "origin": "STAmount instance (from deserialization or construction)", + "transformations": [ + "Set during construction based on serialized value" + ], + "validated_at": "getInt64Value (XRPL_ASSERT)" + }, + { + "field": "amount.mantissa()", + "flow": [ + "STAmount constructed", + "amount.mantissa() called in getInt64Value" + ], + "origin": "STAmount instance (from deserialization or construction)", + "transformations": [ + "Set during construction based on serialized value" + ], + "validated_at": "getInt64Value (XRPL_ASSERT)" + }, + { + "field": "issue comparability", + "flow": [ + "STAmount constructed", + "areComparable called with two STAmount instances", + "std::visit lambda compares issues/currencies/types" + ], + "origin": "STAmount::asset().value()", + "transformations": [ + "Type traits and value comparison" + ], + "validated_at": "areComparable" + } + ], + "description": "Implements the STAmount class and related arithmetic, serialization, and conversion logic for representing and manipulating XRP, IOU, and MPT token amounts in the XRPL protocol.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "amount.native()", + "validation", + "missing", + "check" + ], + "evidence": "Field amount.native() validated by XRPL custom (Throw, XRPL_ASSERT, type traits, std::visit)", + "issue_pattern": "Missing validation for amount.native()", + "why_false_positive": "XRPL custom (Throw, XRPL_ASSERT, type traits, std::visit) validates amount.native() automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "amount.holds()", + "validation", + "missing", + "check" + ], + "evidence": "Field amount.holds() validated by XRPL custom (Throw, XRPL_ASSERT, type traits, std::visit)", + "issue_pattern": "Missing validation for amount.holds()", + "why_false_positive": "XRPL custom (Throw, XRPL_ASSERT, type traits, std::visit) validates amount.holds() automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "amount.exponent()", + "validation", + "missing", + "check" + ], + "evidence": "Field amount.exponent() validated by XRPL custom (Throw, XRPL_ASSERT, type traits, std::visit)", + "issue_pattern": "Missing validation for amount.exponent()", + "why_false_positive": "XRPL custom (Throw, XRPL_ASSERT, type traits, std::visit) validates amount.exponent() automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "amount.mantissa()", + "validation", + "missing", + "check" + ], + "evidence": "Field amount.mantissa() validated by XRPL custom (Throw, XRPL_ASSERT, type traits, std::visit)", + "issue_pattern": "Missing validation for amount.mantissa()", + "why_false_positive": "XRPL custom (Throw, XRPL_ASSERT, type traits, std::visit) validates amount.mantissa() automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount validity (native or MPT)", + "empty", + "string", + "validation" + ], + "evidence": "getInt64Value (called by getSNValue, getMPTValue) at getInt64Value", + "issue_pattern": "Missing empty string validation for amount validity (native or MPT)", + "why_false_positive": "getInt64Value (called by getSNValue, getMPTValue) validates amount validity (native or MPT) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount.exponent", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at getInt64Value", + "issue_pattern": "Missing empty string validation for amount.exponent", + "why_false_positive": "XRPL_ASSERT macro validates amount.exponent for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount.mantissa", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at getInt64Value", + "issue_pattern": "Missing empty string validation for amount.mantissa", + "why_false_positive": "XRPL_ASSERT macro validates amount.mantissa for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "amount.mantissa", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT macro at getInt64Value", + "issue_pattern": "Missing type validation for amount.mantissa", + "why_false_positive": "XRPL_ASSERT macro validates amount.mantissa type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount type (native or MPT)", + "empty", + "string", + "validation" + ], + "evidence": "getSNValue, getMPTValue at getSNValue, getMPTValue", + "issue_pattern": "Missing empty string validation for amount type (native or MPT)", + "why_false_positive": "getSNValue, getMPTValue validates amount type (native or MPT) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "amount type (native or MPT)", + "type", + "validation", + "check" + ], + "evidence": "getSNValue, getMPTValue at getSNValue, getMPTValue", + "issue_pattern": "Missing type validation for amount type (native or MPT)", + "why_false_positive": "getSNValue, getMPTValue validates amount type (native or MPT) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "issue comparability", + "empty", + "string", + "validation" + ], + "evidence": "areComparable (std::visit with type traits) at areComparable", + "issue_pattern": "Missing empty string validation for issue comparability", + "why_false_positive": "areComparable (std::visit with type traits) validates issue comparability for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STAmount.cpp", + "functions": [ + { + "args": [ + "amount", + "valid", + "error" + ], + "lineno": 23, + "name": "getInt64Value" + }, + { + "args": [ + "amount" + ], + "lineno": 36, + "name": "getSNValue" + }, + { + "args": [ + "amount" + ], + "lineno": 41, + "name": "getMPTValue" + }, + { + "args": [ + "v1", + "v2" + ], + "lineno": 46, + "name": "areComparable" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 61, + "name": "STAmount::STAmount" + }, + { + "args": [ + "SField const& name", + "std::int64_t mantissa" + ], + "lineno": 99, + "name": "STAmount::STAmount" + }, + { + "args": [ + "SField const& name", + "std::uint64_t mantissa", + "bool negative" + ], + "lineno": 105, + "name": "STAmount::STAmount" + }, + { + "args": [ + "SField const& name", + "STAmount const& from" + ], + "lineno": 111, + "name": "STAmount::STAmount" + }, + { + "args": [ + "std::uint64_t mantissa", + "bool negative" + ], + "lineno": 122, + "name": "STAmount::STAmount" + }, + { + "args": [ + "XRPAmount const& amount" + ], + "lineno": 128, + "name": "STAmount::STAmount" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 139, + "name": "STAmount::construct" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 143, + "name": "STAmount::copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 148, + "name": "STAmount::move" + }, + { + "args": [], + "lineno": 158, + "name": "STAmount::xrp" + }, + { + "args": [], + "lineno": 172, + "name": "STAmount::iou" + }, + { + "args": [], + "lineno": 185, + "name": "STAmount::mpt" + }, + { + "args": [ + "IOUAmount const& iou" + ], + "lineno": 198, + "name": "STAmount::operator=" + }, + { + "args": [ + "Number const& number" + ], + "lineno": 212, + "name": "STAmount::operator=" + }, + { + "args": [ + "STAmount const& a" + ], + "lineno": 229, + "name": "STAmount::operator+=" + }, + { + "args": [ + "STAmount const& a" + ], + "lineno": 234, + "name": "STAmount::operator-=" + }, + { + "args": [ + "STAmount const& v1", + "STAmount const& v2" + ], + "lineno": 239, + "name": "operator+" + }, + { + "args": [ + "STAmount const& v1", + "STAmount const& v2" + ], + "lineno": 277, + "name": "operator-" + }, + { + "args": [ + "Asset const& asset" + ], + "lineno": 287, + "name": "STAmount::setIssue" + }, + { + "args": [ + "STAmount const& offerOut", + "STAmount const& offerIn" + ], + "lineno": 297, + "name": "getRate" + }, + { + "args": [ + "STAmount const& a", + "STAmount const& b" + ], + "lineno": 332, + "name": "canAdd" + }, + { + "args": [ + "STAmount const& a", + "STAmount const& b" + ], + "lineno": 393, + "name": "canSubtract" + }, + { + "args": [ + "Json::Value& elem" + ], + "lineno": 453, + "name": "STAmount::setJson" + }, + { + "args": [], + "lineno": 471, + "name": "STAmount::getSType" + }, + { + "args": [], + "lineno": 475, + "name": "STAmount::getFullText" + }, + { + "args": [], + "lineno": 482, + "name": "STAmount::getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 527, + "name": "STAmount::getJson" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 532, + "name": "STAmount::add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 563, + "name": "STAmount::isEquivalent" + }, + { + "args": [], + "lineno": 568, + "name": "STAmount::isDefault" + }, + { + "args": [], + "lineno": 577, + "name": "STAmount::canonicalize" + }, + { + "args": [ + "std::int64_t v" + ], + "lineno": 642, + "name": "STAmount::set" + }, + { + "args": [ + "std::uint64_t rate" + ], + "lineno": 654, + "name": "amountFromQuality" + }, + { + "args": [ + "Asset const& asset", + "std::string const& amount" + ], + "lineno": 664, + "name": "amountFromString" + }, + { + "args": [ + "SField const& name", + "Json::Value const& v" + ], + "lineno": 671, + "name": "amountFromJson" + }, + { + "args": [ + "STAmount& result", + "Json::Value const& jvSource" + ], + "lineno": 735, + "name": "amountFromJsonNoThrow" + }, + { + "args": [ + "STAmount const& lhs", + "STAmount const& rhs" + ], + "lineno": 749, + "name": "operator==" + }, + { + "args": [ + "STAmount const& lhs", + "STAmount const& rhs" + ], + "lineno": 754, + "name": "operator<" + }, + { + "args": [ + "STAmount const& value" + ], + "lineno": 779, + "name": "operator-" + }, + { + "args": [ + "std::uint64_t multiplier", + "std::uint64_t multiplicand", + "std::uint64_t divisor" + ], + "lineno": 792, + "name": "muldiv" + }, + { + "args": [ + "std::uint64_t multiplier", + "std::uint64_t multiplicand", + "std::uint64_t divisor", + "std::uint64_t rounding" + ], + "lineno": 803, + "name": "muldiv_round" + }, + { + "args": [ + "STAmount const& num", + "STAmount const& den", + "Asset const& asset" + ], + "lineno": 816, + "name": "divide" + }, + { + "args": [ + "STAmount const& v1", + "STAmount const& v2", + "Asset const& asset" + ], + "lineno": 844, + "name": "multiply" + }, + { + "args": [ + "bool integral", + "std::uint64_t& value", + "int& offset", + "bool" + ], + "lineno": 889, + "name": "canonicalizeRound" + }, + { + "args": [ + "bool integral", + "std::uint64_t& value", + "int& offset", + "bool roundUp" + ], + "lineno": 914, + "name": "canonicalizeRoundStrict" + }, + { + "args": [ + "STAmount const& value", + "std::int32_t scale", + "Number::rounding_mode rounding" + ], + "lineno": 940, + "name": "roundToScale" + }, + { + "args": [ + "STAmount const& v1", + "STAmount const& v2", + "Asset const& asset", + "bool roundUp" + ], + "lineno": 963, + "name": "mulRoundImpl" + }, + { + "args": [ + "STAmount const& v1", + "STAmount const& v2", + "Asset const& asset", + "bool roundUp" + ], + "lineno": 1022, + "name": "mulRound" + }, + { + "args": [ + "STAmount const& v1", + "STAmount const& v2", + "Asset const& asset", + "bool roundUp" + ], + "lineno": 1027, + "name": "mulRoundStrict" + }, + { + "args": [ + "STAmount const& num", + "STAmount const& den", + "Asset const& asset", + "bool roundUp" + ], + "lineno": 1033, + "name": "divRoundImpl" + }, + { + "args": [ + "STAmount const& num", + "STAmount const& den", + "Asset const& asset", + "bool roundUp" + ], + "lineno": 1082, + "name": "divRound" + }, + { + "args": [ + "STAmount const& num", + "STAmount const& den", + "Asset const& asset", + "bool roundUp" + ], + "lineno": 1087, + "name": "divRoundStrict" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::logic_error for error reporting", + "exception_type": "std::logic_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 21, + "name": "xrpl" + } + ], + "test_coverage_notes": "STAmount and related validation logic are typically covered by unit tests in files like 'test/protocol/STAmount_test.cpp', 'test/protocol/Amount_test.cpp', and possibly integration tests involving transaction serialization/deserialization. These tests likely cover native, IOU, and MPT amounts, including edge cases for exponent/mantissa. However, coverage gaps may exist for: (1) malformed serialized data (bitmask edge cases), (2) negative/overflow mantissa/exponent values, (3) areComparable with mixed/invalid types, and (4) exception paths in getInt64Value. Fuzzing or property-based tests may be needed for full coverage of deserialization and validation error paths.", + "validation_architecture": { + "auto_validated_fields": [ + "amount.native()", + "amount.holds()", + "amount.exponent()", + "amount.mantissa()" + ], + "framework": "XRPL custom (Throw, XRPL_ASSERT, type traits, std::visit)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "amount validity (native or MPT)", + "location": "getInt64Value", + "validated_by": "getInt64Value (called by getSNValue, getMPTValue)", + "validates": [ + "Checks if the amount is valid for the requested type (native or MPT)", + "Throws if not valid" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or logs)", + "field": "amount.exponent", + "location": "getInt64Value", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that exponent is zero for int64 conversion" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or logs)", + "field": "amount.mantissa", + "location": "getInt64Value", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that mantissa fits in int64_t (roundtrip check)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "amount type (native or MPT)", + "location": "getSNValue, getMPTValue", + "validated_by": "getSNValue, getMPTValue", + "validates": [ + "Checks that amount is native (getSNValue) or MPT (getMPTValue)" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (returns bool)", + "field": "issue comparability", + "location": "areComparable", + "validated_by": "areComparable (std::visit with type traits)", + "validates": [ + "Checks that two STAmount objects are comparable (same type, currency, etc.)" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STAmount.cpp.ai.md b/src/libxrpl/protocol/STAmount.cpp.ai.md new file mode 100644 index 0000000000..8404c1ade7 --- /dev/null +++ b/src/libxrpl/protocol/STAmount.cpp.ai.md @@ -0,0 +1,126 @@ +# `src/libxrpl/protocol/STAmount.cpp` + +## Role and Purpose + +`STAmount` is the universal value type for the XRP Ledger. Every amount on the network — whether XRP drops, IOU tokens from a gateway, or Multi-Purpose Tokens (MPT) — is represented, serialized, compared, and arithmetically manipulated through this class and its companion free functions. The file is the implementation counterpart to `include/xrpl/protocol/STAmount.h`, providing everything from constructors and wire-format serialization to multiply-with-rounding and JSON parsing. + +The class inherits from `STBase`, which makes it a first-class XRPL serialized type (type tag `STI_AMOUNT`), and from `CountedObject` for memory diagnostics. + +--- + +## Internal Representation + +Every `STAmount` stores four fields: + +| Field | Type | Meaning | +|---|---|---| +| `mAsset` | `Asset` | A `std::variant` holding either an `Issue` (currency + issuer) or an `MPTIssue` (192-bit MPTID) | +| `mValue` | `uint64_t` | Unsigned mantissa | +| `mOffset` | `int` | Base-10 exponent | +| `mIsNegative` | `bool` | Sign | + +The *canonical* internal form differs by asset type: + +- **XRP** (`native()` true): `mOffset == 0`, `mValue` is the raw drop count (≤ `cMaxNativeN = 10^17`). Negative zero is illegal. +- **IOU** (issued currency): `mValue` ∈ [`10^15`, `10^16 − 1`] and `mOffset` ∈ [`−96`, `+80`], representing `value × 10^offset`. Zero is the special case `mValue = 0, mOffset = −100`. +- **MPT**: Same as XRP — integer, `mOffset == 0`, no fractional part. + +The header documents three static constants (`cMinValue`, `cMaxValue`, `cMinOffset`, `cMaxOffset`) that define the canonical IOU window. These are not just range guards — the window must be exactly [10^15, 10^16) for the multiplication and division algorithms to maintain precision without overflow. + +--- + +## Wire Format and Deserialization + +The serialization format packs type, sign, exponent, and mantissa into 64 bits (for the header word), then appends currency/issuer data as needed: + +- **Bit 63** (`cIssuedCurrency`, `0x8000000000000000`): 0 means native-or-MPT, 1 means IOU. +- **Bit 62** (`cPositive`, `0x4000000000000000`): 1 means positive, 0 means negative. +- **Bit 61** (`cMPToken`, `0x2000000000000000`): 1 means MPT; only meaningful when bit 63 is 0. + +For an **IOU**, the top 10 bits carry `offset + 97` (biased to stay non-negative); the low 54 bits carry the mantissa. Currency (160 bits) and issuer (160 bits) follow inline. For **MPT**, the header word carries the 56-bit value; a 192-bit MPTID (`get192()`) follows. For **XRP**, the 62-bit value sits in the lower bits of the header word — no additional bytes are needed. + +The `STAmount(SerialIter&, SField const&)` constructor does the decoding. It rejects "negative zero" XRP, invalid IOU currencies (those colliding with the XRP currency code), and mantissa/exponent values outside the canonical range. The inverse, `add(Serializer&)`, reassembles the same bit pattern. Together they establish the invariant that any round-tripped amount is canonical. + +--- + +## `canonicalize()` — The Core Normalizer + +`canonicalize()` is called by most constructors after setting `mValue`, `mOffset`, and `mIsNegative`. Its job is to bring the amount into its canonical form: + +For **integral types** (XRP and MPT), it repeatedly divides or multiplies `mValue` by 10 while adjusting `mOffset`, until `mOffset == 0`. It checks `cMaxNativeN` and `maxMPTokenAmount` overflow bounds before each multiply. If `getSTNumberSwitchover()` is enabled — a feature flag that activates a newer arithmetic engine — it delegates to `XRPAmount` or `MPTAmount` conversion via `Number`, which handles the normalization internally. + +For **IOU**, it nudges the mantissa up (multiply by 10, decrement offset) while `mValue < cMinValue`, and down (divide by 10, increment offset) while `mValue > cMaxValue`. If the result would underflow the minimum offset it collapses to canonical zero (`mValue = 0, mOffset = −100`). Overflow throws `std::runtime_error("value overflow")`. This normalization enforces the 16-significant-digit precision that the XRPL floating-point format guarantees. + +When `getSTNumberSwitchover()` is enabled, IOU canonicalization delegates to `iou()`, which converts to `IOUAmount` and back, using that class's normalizer. + +--- + +## Arithmetic: Addition and Subtraction + +`operator+(v1, v2)` begins by calling `areComparable(v1, v2)`. This uses a `std::visit` over the `Asset` variant with compile-time type-trait branches (`is_issue_v`, `is_mptissue_v`). Two amounts are comparable only if they share the same asset type and the same currency/issuer identity. Adding incompatible amounts throws immediately. + +For **XRP**: the function extracts `int64_t` drop counts via `getSNValue()` (which validates `native()` is true and that the exponent is zero), adds them, and builds a new `STAmount`. + +For **MPT**: same pattern via `getMPTValue()` and the `MPTAmount` value type. + +For **IOU** (legacy path): the function aligns exponents by dividing the mantissa of the lower-offset operand, losing the least significant digits. The comment explicitly acknowledges that "this addition cannot overflow an `int64_t`" — though the resulting `STAmount` can overflow after canonicalization. Amounts within `[−10, +10]` after alignment are rounded to zero and returned as the zero of the v1 currency. The switchover path delegates to `IOUAmount::operator+`, which uses the `Number` engine. + +Subtraction is simply `v1 + (−v2)`, and the unary negation operator flips `mIsNegative` (guarding against negating zero). + +--- + +## Multiply and Divide + +### Core Helpers: `muldiv` and `muldiv_round` + +Both use `boost::multiprecision::uint128_t` to compute `(a × b) / c` without overflow. These are private static helpers — the XRPL ledger cannot use 128-bit native integers portably, so Boost provides the intermediate precision. `muldiv_round` adds a rounding bias before the division. + +### `multiply()` + +For XRP × XRP, the function guards against overflow using the observation that if `min(a,b) > sqrt(cMaxNative)` or `((max >> 32) * min) > cMaxNative / 2^32`, the product must overflow. This avoids a 128-bit multiply for the common all-native case. + +For IOU × IOU (or mixed IOU), both mantissas are normalized to the canonical [10^15, 10^16) window (integral amounts are scaled up), then `muldiv(v1_mantissa, v2_mantissa, 10^14)` produces a result in the [10^16, 10^18) range. Adding 7 before division implements banker's-style rounding in the legacy path. The combined offset is `offset1 + offset2 + 14`. Canonicalization then squeezes the mantissa back into the canonical window. + +### `divide()` + +Same structure but the formula is `muldiv(num, 10^17, den)`, producing a result in [10^15, 10^17), with offset `numOffset − denOffset − 17`. The `+5` rounding bias handles the remainder in the legacy path. + +### Rounding Variants + +A key design complexity is the existence of **two rounding modes** for multiply and divide: + +- **`mulRound` / `divRound`** (legacy): use `canonicalizeRound()`, which rounds up only when the fractional part is ≥ 0.1. This surprising behavior is preserved for backward compatibility because cross-currency AMM-style calculations have it baked in. +- **`mulRoundStrict` / `divRoundStrict`**: use `canonicalizeRoundStrict()`, which tracks the actual remainder through all intermediate division steps and rounds correctly. These also propagate the rounding mode to `Number` via `NumberRoundModeGuard`, so that the `canonicalize()` call on the new `STAmount` uses consistent rounding. + +The shared template `mulRoundImpl` captures this duality: both the round function and the RAII guard type are template parameters. `DontAffectNumberRoundMode` is a no-op guard used in the non-strict variants to avoid disturbing the thread-local `Number` rounding mode. + +--- + +## Offer Rate Encoding: `getRate()` + +`getRate(offerOut, offerIn)` converts an order book offer into a 64-bit sort key. It divides `offerIn` by `offerOut` using the `noIssue()` pseudo-asset, then packs the result as `(exponent + 100) << 56 | mantissa`. Because the canonical mantissa window is [10^15, 10^16), the mantissa fits in 56 bits. Lower sort values represent better rates for takers. Offers that overflow or divide to zero return 0 (treated as worthless). The constant `uRateOne` is initialized at static time via `getRate(STAmount(1), STAmount(1))` and represents a 1:1 rate. + +--- + +## JSON Parsing: `amountFromJson()` + +This function handles XRP Ledger's flexible JSON amount encoding across four formats: JSON object (`{value, currency, issuer}` or `{value, mpt_issuance_id}`), JSON array, delimiter-split string, and bare numeric. Asset type is inferred from which fields are present. Fractional specifications are rejected for XRP and MPT since those are integer-only types. `amountFromJsonNoThrow()` wraps this with a catch-all and returns `false` on failure, logging the error. + +--- + +## Safety Predicates: `canAdd` and `canSubtract` + +These functions answer "would this arithmetic operation be safe?" without performing it. They are used by the AMM and vault subsystems to validate operations before executing them. + +For XRP and MPT, the checks are straightforward integer overflow/underflow bounds. For IOU, `canAdd` uses a round-trip relative error test: it checks whether `(a − b) + b ≈ a` and `(b − a) + a ≈ b` within a `10^−4` tolerance. This catches cases where exponent differences would lose too many significant digits. IOU subtraction is always considered safe since negative IOU balances are valid. + +--- + +## Feature-Gated Behavior + +Several code paths branch on runtime feature flags: + +- `getSTNumberSwitchover()`: a thread-local flag (used in test contexts and via amendment) that switches arithmetic from the legacy mantissa-shifting code to the `Number` / `IOUAmount` engine. +- `featureSingleAssetVault` and `featureLendingProtocol`: checked in `operator=(Number const&)` to decide whether to use the new `fromNumber()` path or the older direct mantissa assignment. + +This layering means the same source file simultaneously supports the historical ledger semantics (for replay) and the newer, more numerically sound code paths (for new transaction types). \ No newline at end of file diff --git a/src/libxrpl/protocol/STArray.cpp.ai.json b/src/libxrpl/protocol/STArray.cpp.ai.json new file mode 100644 index 0000000000..ad4bdaa0dc --- /dev/null +++ b/src/libxrpl/protocol/STArray.cpp.ai.json @@ -0,0 +1,497 @@ +{ + "args": [ + { + "lineno": 13, + "name": "other" + }, + { + "lineno": 17, + "name": "other" + }, + { + "lineno": 23, + "name": "n" + }, + { + "lineno": 27, + "name": "f" + }, + { + "lineno": 31, + "name": "f" + }, + { + "lineno": 31, + "name": "n" + }, + { + "lineno": 35, + "name": "sit" + }, + { + "lineno": 35, + "name": "f" + }, + { + "lineno": 35, + "name": "depth" + }, + { + "lineno": 65, + "name": "n" + }, + { + "lineno": 65, + "name": "buf" + }, + { + "lineno": 70, + "name": "n" + }, + { + "lineno": 70, + "name": "buf" + }, + { + "lineno": 107, + "name": "p" + }, + { + "lineno": 120, + "name": "s" + }, + { + "lineno": 133, + "name": "t" + }, + { + "lineno": 143, + "name": "compare" + } + ], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "STArray" + } + ], + "code_paths": [ + { + "call_chain": [ + "STArray::STArray(SerialIter&, SField const&, int)", + "sit.getFieldID(type, field)", + "SField::getField(type, field)", + "fn.isInvalid()", + "v_.emplace_back(sit, fn, depth + 1)", + "v_.back().applyTemplateFromSField(fn)" + ], + "entry_point": "STArray::STArray(SerialIter&, SField const&, int)", + "purpose": "Deserializes an array from a serialized stream, validating each element's type and field before constructing STObject instances.", + "validation_points": [ + "if ((type == STI_ARRAY) && (field == 1)) break;", + "if ((type == STI_OBJECT) && (field == 1)) Throw", + "if (fn.isInvalid()) Throw", + "if (fn.fieldType != STI_OBJECT) Throw" + ] + }, + { + "call_chain": [ + "STArray::add(Serializer&)", + "object.addFieldID(s)", + "object.add(s)", + "s.addFieldID(STI_OBJECT, 1)" + ], + "entry_point": "STArray::add(Serializer&)", + "purpose": "Serializes the array and its objects back into a Serializer stream.", + "validation_points": [] + }, + { + "call_chain": [ + "STArray::getJson(JsonOptions)", + "object.getSType()", + "object.getFName().getJsonName()", + "object.getJson(p)" + ], + "entry_point": "STArray::getJson(JsonOptions)", + "purpose": "Converts the array and its objects into a JSON representation.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "type/field (from SerialIter)", + "flow": [ + "SerialIter::getFieldID(type, field)", + "manual checks for STI_ARRAY/STI_OBJECT terminators", + "SField::getField(type, field)", + "fn.isInvalid()", + "fn.fieldType != STI_OBJECT", + "v_.emplace_back(sit, fn, depth + 1)" + ], + "origin": "SerialIter::getFieldID(type, field)", + "transformations": [ + "type/field extracted from serialized data", + "checked for array/object terminators", + "looked up as SField", + "validated for known/valid field", + "validated for correct type (must be STI_OBJECT)", + "used to construct STObject" + ], + "validated_at": "STArray::STArray(SerialIter&, SField const&, int) (multiple points)" + }, + { + "field": "fn (SField)", + "flow": [ + "SField::getField(type, field)", + "fn.isInvalid()", + "fn.fieldType != STI_OBJECT", + "v_.emplace_back(sit, fn, depth + 1)", + "v_.back().applyTemplateFromSField(fn)" + ], + "origin": "SField::getField(type, field)", + "transformations": [ + "SField instance created from type/field", + "checked for validity", + "checked for correct type", + "used to construct and template STObject" + ], + "validated_at": "Immediately after SField::getField" + }, + { + "field": "v_ (vector)", + "flow": [ + "v_.emplace_back(sit, fn, depth + 1)", + "v_.back().applyTemplateFromSField(fn)", + "used in getFullText/getText/getJson/add" + ], + "origin": "Constructed in STArray::STArray(SerialIter&, SField const&, int)", + "transformations": [ + "STObject constructed from validated input", + "template applied", + "used for serialization, JSON, text output" + ], + "validated_at": "Before emplace_back (all validation must pass)" + } + ], + "description": "Implements the STArray class for the XRPL protocol, representing an array of STObject instances with serialization, deserialization, JSON conversion, and utility methods.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type/field (from SerialIter)", + "empty", + "string", + "validation" + ], + "evidence": "manual check (if ((type == STI_ARRAY) && (field == 1))) at STArray(SerialIter& sit, SField const& f, int depth) constructor", + "issue_pattern": "Missing empty string validation for type/field (from SerialIter)", + "why_false_positive": "manual check (if ((type == STI_ARRAY) && (field == 1))) validates type/field (from SerialIter) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type/field (from SerialIter)", + "empty", + "string", + "validation" + ], + "evidence": "manual check (if ((type == STI_OBJECT) && (field == 1))) at STArray(SerialIter& sit, SField const& f, int depth) constructor", + "issue_pattern": "Missing empty string validation for type/field (from SerialIter)", + "why_false_positive": "manual check (if ((type == STI_OBJECT) && (field == 1))) validates type/field (from SerialIter) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type/field (from SerialIter)", + "empty", + "string", + "validation" + ], + "evidence": "SField::getField(type, field) + fn.isInvalid() at STArray(SerialIter& sit, SField const& f, int depth) constructor", + "issue_pattern": "Missing empty string validation for type/field (from SerialIter)", + "why_false_positive": "SField::getField(type, field) + fn.isInvalid() validates type/field (from SerialIter) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "type/field (from SerialIter)", + "type", + "validation", + "check" + ], + "evidence": "SField::getField(type, field) + fn.isInvalid() at STArray(SerialIter& sit, SField const& f, int depth) constructor", + "issue_pattern": "Missing type validation for type/field (from SerialIter)", + "why_false_positive": "SField::getField(type, field) + fn.isInvalid() validates type/field (from SerialIter) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fn.fieldType", + "empty", + "string", + "validation" + ], + "evidence": "if (fn.fieldType != STI_OBJECT) at STArray(SerialIter& sit, SField const& f, int depth) constructor", + "issue_pattern": "Missing empty string validation for fn.fieldType", + "why_false_positive": "if (fn.fieldType != STI_OBJECT) validates fn.fieldType for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "fn.fieldType", + "type", + "validation", + "check" + ], + "evidence": "if (fn.fieldType != STI_OBJECT) at STArray(SerialIter& sit, SField const& f, int depth) constructor", + "issue_pattern": "Missing type validation for fn.fieldType", + "why_false_positive": "if (fn.fieldType != STI_OBJECT) validates fn.fieldType type" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STArray.cpp", + "functions": [ + { + "args": [ + "STArray&& other" + ], + "lineno": 13, + "name": "STArray" + }, + { + "args": [ + "STArray&& other" + ], + "lineno": 17, + "name": "operator=" + }, + { + "args": [ + "int n" + ], + "lineno": 23, + "name": "STArray" + }, + { + "args": [ + "SField const& f" + ], + "lineno": 27, + "name": "STArray" + }, + { + "args": [ + "SField const& f", + "std::size_t n" + ], + "lineno": 31, + "name": "STArray" + }, + { + "args": [ + "SerialIter& sit", + "SField const& f", + "int depth" + ], + "lineno": 35, + "name": "STArray" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 65, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 70, + "name": "move" + }, + { + "args": [], + "lineno": 75, + "name": "getFullText" + }, + { + "args": [], + "lineno": 91, + "name": "getText" + }, + { + "args": [ + "JsonOptions p" + ], + "lineno": 107, + "name": "getJson" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 120, + "name": "add" + }, + { + "args": [], + "lineno": 129, + "name": "getSType" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 133, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 139, + "name": "isDefault" + }, + { + "args": [ + "bool (*compare)(STObject const&, STObject const&)" + ], + "lineno": 143, + "name": "sort" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "STArray deserialization and validation logic is typically tested in protocol-level or serialization/deserialization unit tests. Look for test files such as 'test/protocol/STArray_test.cpp', 'test/protocol/STObject_test.cpp', or integration tests that exercise transaction or ledger serialization. Gaps may exist if tests do not cover malformed input (e.g., arrays with non-object fields, unknown fields, or illegal terminators). Exception paths (Throw) should be explicitly tested for coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Manual validation, SField class", + "validation_layer": "business_logic (constructor parsing input stream)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (breaks loop)", + "field": "type/field (from SerialIter)", + "location": "STArray(SerialIter& sit, SField const& f, int depth) constructor", + "validated_by": "manual check (if ((type == STI_ARRAY) && (field == 1)))", + "validates": [ + "Checks for end-of-array marker (STI_ARRAY, field 1) to terminate parsing" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Illegal terminator in array\")", + "field": "type/field (from SerialIter)", + "location": "STArray(SerialIter& sit, SField const& f, int depth) constructor", + "validated_by": "manual check (if ((type == STI_OBJECT) && (field == 1)))", + "validates": [ + "Checks for illegal end-of-object marker inside array" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Unknown field\")", + "field": "type/field (from SerialIter)", + "location": "STArray(SerialIter& sit, SField const& f, int depth) constructor", + "validated_by": "SField::getField(type, field) + fn.isInvalid()", + "validates": [ + "Checks that the field type/field pair is a known SField" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Non-object in array\")", + "field": "fn.fieldType", + "location": "STArray(SerialIter& sit, SField const& f, int depth) constructor", + "validated_by": "if (fn.fieldType != STI_OBJECT)", + "validates": [ + "Checks that each array element is an object (STI_OBJECT)" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STArray.cpp.ai.md b/src/libxrpl/protocol/STArray.cpp.ai.md new file mode 100644 index 0000000000..10dfc20f51 --- /dev/null +++ b/src/libxrpl/protocol/STArray.cpp.ai.md @@ -0,0 +1,77 @@ +# `STArray.cpp` — Serialized Array of Inner Objects + +`STArray` is the XRPL protocol's typed container for sequences of `STObject` instances. It sits at the core of the ledger's binary encoding: any transaction field that holds a list of sub-objects — `sfMemos`, `sfSigners`, `sfNFTokens`, and others — is represented as an `STArray` on the wire and in memory. This file provides the non-trivial method bodies; the inline accessors and iterator plumbing live in the header. + +## Class Design + +`STArray` inherits from `STBase`, the polymorphic root of all serialized types, and from `CountedObject` for diagnostic object-count tracking. Its sole data member is `list_type v_`, a `std::vector`. Because `STBase` carries a field-name pointer (`SField const* fName`) separately from the data, the move constructor and move assignment operator must explicitly call `setFName(other.getFName())` before moving `v_`. If these were left to the compiler, the field name association would be lost, causing downstream field-ID mismatches during serialization. + +The copy constructor and copy assignment are compiler-defaulted, inheriting `STBase`'s shallow-copy semantics for the field name and `std::vector`'s deep copy for the elements. This is fine for `STArray` because `STObject` is itself copyable. + +## Deserialization + +The most architecturally significant code is the `SerialIter`-based constructor: + +```cpp +STArray::STArray(SerialIter& sit, SField const& f, int depth) : STBase(f) +``` + +XRPL's binary format encodes an `STArray` as a sentinel-terminated sequence. The parser loops over field-ID tokens until it reads a token with `type == STI_ARRAY && field == 1`, which is the canonical end-of-array marker. This design means the array length is not encoded up front; the decoder must process tokens linearly, which is the same approach used for `STObject` (whose terminator is `STI_OBJECT, field == 1`). + +Four distinct validation checks guard the loop body: + +1. **End-of-array marker** (`STI_ARRAY, field 1`) — break cleanly. +2. **Misplaced end-of-object marker** (`STI_OBJECT, field 1`) — if this appears where an array terminator is expected, the stream is structurally corrupt. A `std::runtime_error("Illegal terminator in array")` is thrown. +3. **Unknown field** — `SField::getField(type, field)` returns a sentinel invalid `SField` for unrecognized `(type, field)` pairs. `fn.isInvalid()` catches this and throws `"Unknown field"`. +4. **Non-object element** — every element in an `STArray` must be an `STObject` (`STI_OBJECT`). If the field's `fieldType` is anything else, `"Non-object in array"` is thrown. + +This fail-hard approach is intentional: a ledger object with a malformed or unexpected array element must never be silently accepted, since doing so would break consensus-level equivalence checks across nodes. + +After each element passes validation, it is constructed directly into the vector with `v_.emplace_back(sit, fn, depth + 1)`. The `depth + 1` increment threads a recursion counter into each child `STObject`'s own deserialization constructor, which enforces a maximum nesting depth of 10. This is a defense against crafted payloads that could overflow the call stack through deeply nested object hierarchies. + +Immediately after construction, `v_.back().applyTemplateFromSField(fn)` is called. The `fn` parameter — the field wrapping this inner object (e.g., `sfMemo`, `sfSigner`) — carries an `SOTemplate` registered in `InnerObjectFormats`. Applying the template validates the just-deserialized `STObject` against the known schema for that field type: unknown fields are rejected, required fields are checked, and fields carrying default values are flagged. This call can throw (`// May throw` is the explicit acknowledgment in the comment), and if it does, the partially constructed `STArray` is unwound by the exception. There is no partial-recovery logic — this is correct behavior because a partially valid ledger object is an invalid ledger object. + +## Binary Serialization + +The `add(Serializer& s)` method mirrors the deserialization loop structure. For each `STObject` element: + +```cpp +object.addFieldID(s); // write the element's typed field ID +object.add(s); // write the element's content +s.addFieldID(STI_OBJECT, 1); // write the per-element object terminator +``` + +Notably, `add()` does not write the outer array's own field ID — that responsibility belongs to the caller (usually `STObject::add()`), which calls `addFieldID()` on this `STArray` before calling `add()`. The outer array's end-of-array terminator (`STI_ARRAY, 1`) is similarly written by the parent, not here. This split of responsibility is consistent across all `STBase` subclasses. + +## JSON Representation + +`getJson()` emits a JSON array where each present element becomes a JSON object with a single key — the element's field name (from `getFName().getJsonName()`) — mapping to the element's own JSON representation: + +```json +[ + { "Memo": { "MemoData": "..." } }, + { "Memo": { "MemoData": "..." } } +] +``` + +This wrapping in an outer object keyed by field name is critical for round-trip fidelity with the XRPL JSON API: it preserves the named-field context of each inner object, which would otherwise be lost in a flat JSON array. Elements with type `STI_NOTPRESENT` are skipped — these are placeholder entries that represent absent optional fields in a template-bound context. + +`getText()` and `getFullText()` produce bracket-delimited comma-separated lists for human-readable debugging, with `getFullText()` delegating to `STObject::getFullText()` (which includes field names) while `getText()` uses the value-only form. + +## Buffer-Placement Copy and Move + +The `copy()` and `move()` overrides implement the in-place construction pattern inherited from `STBase`: + +```cpp +STBase* STArray::copy(std::size_t n, void* buf) const { + return emplace(n, buf, *this); +} +``` + +`STBase::emplace()` places the object into `buf` if it fits within `n` bytes; otherwise it heap-allocates. This allows the `detail::STVar` variant type — which holds heterogeneous `STBase` subtype values in a small fixed-size buffer — to avoid heap allocation for small objects while still handling arbitrarily large ones correctly. + +## Equivalence and Default State + +`isEquivalent()` uses `dynamic_cast` to confirm the compared object is also an `STArray`, then delegates to vector equality (`v_ == v->v_`), which cascades through `STObject::operator==`. `isDefault()` is simply `v_.empty()`, reflecting that an empty array is a no-op value that need not be encoded on the wire. + +The `sort()` method accepts a raw function pointer (not a `std::function`) for ordering elements by caller-supplied criteria. This is used primarily when signing multi-signer transactions, where `sfSigners` must be canonically ordered by account ID to ensure deterministic serialization across all signing parties. \ No newline at end of file diff --git a/src/libxrpl/protocol/STBase.cpp.ai.json b/src/libxrpl/protocol/STBase.cpp.ai.json new file mode 100644 index 0000000000..ec62e397ab --- /dev/null +++ b/src/libxrpl/protocol/STBase.cpp.ai.json @@ -0,0 +1,523 @@ +{ + "args": [ + { + "lineno": 12, + "name": "n" + }, + { + "lineno": 16, + "name": "t" + }, + { + "lineno": 26, + "name": "t" + }, + { + "lineno": 31, + "name": "t" + }, + { + "lineno": 36, + "name": "n" + }, + { + "lineno": 36, + "name": "buf" + }, + { + "lineno": 41, + "name": "n" + }, + { + "lineno": 41, + "name": "buf" + }, + { + "lineno": 69, + "name": "options" + }, + { + "lineno": 74, + "name": "s" + }, + { + "lineno": 81, + "name": "t" + }, + { + "lineno": 91, + "name": "n" + }, + { + "lineno": 101, + "name": "s" + }, + { + "lineno": 108, + "name": "out" + }, + { + "lineno": 108, + "name": "t" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "STBase" + } + ], + "code_paths": [ + { + "call_chain": [ + "STBase::STBase(SField const& n)" + ], + "entry_point": "STBase::STBase(SField const& n)", + "purpose": "Constructs an STBase object with a specific SField; ensures fName is set.", + "validation_points": [ + "XRPL_ASSERT(fName, ...) in constructor" + ] + }, + { + "call_chain": [ + "STBase::operator=(STBase const& t)" + ], + "entry_point": "STBase::operator=(STBase const& t)", + "purpose": "Assignment operator; copies fName if current fName is not useful.", + "validation_points": [ + "if (!fName->isUseful()) ..." + ] + }, + { + "call_chain": [ + "STBase::isEquivalent(STBase const& t) const" + ], + "entry_point": "STBase::isEquivalent(STBase const& t) const", + "purpose": "Checks if two STBase objects are equivalent; asserts type is STI_NOTPRESENT.", + "validation_points": [ + "XRPL_ASSERT(getSType() == STI_NOTPRESENT, ...)" + ] + }, + { + "call_chain": [ + "STBase::setFName(SField const& n)" + ], + "entry_point": "STBase::setFName(SField const& n)", + "purpose": "Sets the fName pointer to a new SField and validates it.", + "validation_points": [ + "XRPL_ASSERT(fName, ...) in setFName" + ] + }, + { + "call_chain": [ + "STBase::addFieldID(Serializer& s) const", + "Serializer::addFieldID(...)" + ], + "entry_point": "STBase::addFieldID(Serializer& s) const", + "purpose": "Adds the field ID to a Serializer; validates that fName is binary.", + "validation_points": [ + "XRPL_ASSERT(fName->isBinary(), ...)" + ] + } + ], + "data_flows": [ + { + "field": "fName (pointer to SField)", + "flow": [ + "STBase::STBase() or STBase::STBase(SField const& n) or setFName()", + "fName assigned", + "Used in operator=, addFieldID, getFullText, etc." + ], + "origin": "STBase::STBase() or STBase::STBase(SField const& n) or setFName()", + "transformations": [ + "Assigned from &sfGeneric (default) or &n (parameter)", + "Possibly reassigned in operator= or setFName" + ], + "validated_at": "XRPL_ASSERT in constructors and setFName" + }, + { + "field": "fName->isUseful()", + "flow": [ + "operator= checks fName->isUseful()", + "If not useful, fName is reassigned" + ], + "origin": "SField object pointed to by fName", + "transformations": [ + "Boolean check to determine if fName should be replaced" + ], + "validated_at": "if (!fName->isUseful()) in operator=" + }, + { + "field": "getSType()", + "flow": [ + "Used in operator==, operator!=, getFullText, isEquivalent" + ], + "origin": "STBase::getSType() (always returns STI_NOTPRESENT in this base class)", + "transformations": [ + "Compared to other STBase's getSType()", + "Used in XRPL_ASSERT in isEquivalent" + ], + "validated_at": "XRPL_ASSERT(getSType() == STI_NOTPRESENT, ...) in isEquivalent" + }, + { + "field": "fName->isBinary()", + "flow": [ + "addFieldID checks fName->isBinary()", + "If true, calls Serializer::addFieldID" + ], + "origin": "SField object pointed to by fName", + "transformations": [ + "Boolean check to ensure field is binary before serialization" + ], + "validated_at": "XRPL_ASSERT(fName->isBinary(), ...) in addFieldID" + } + ], + "description": "Implements the STBase class, which serves as a base class for serialized types in the XRPL protocol, providing basic serialization, comparison, and field management functionality.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "fName (pointer validity, binary property, usefulness)", + "validation", + "missing", + "check" + ], + "evidence": "Field fName (pointer validity, binary property, usefulness) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for fName (pointer validity, binary property, usefulness)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates fName (pointer validity, binary property, usefulness) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "getSType() (type presence)", + "validation", + "missing", + "check" + ], + "evidence": "Field getSType() (type presence) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for getSType() (type presence)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates getSType() (type presence) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fName (pointer to SField)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at STBase(SField const& n) constructor", + "issue_pattern": "Missing empty string validation for fName (pointer to SField)", + "why_false_positive": "XRPL_ASSERT validates fName (pointer to SField) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "fName (pointer to SField)", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT at STBase(SField const& n) constructor", + "issue_pattern": "Missing type validation for fName (pointer to SField)", + "why_false_positive": "XRPL_ASSERT validates fName (pointer to SField) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fName (pointer to SField)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at STBase::setFName", + "issue_pattern": "Missing empty string validation for fName (pointer to SField)", + "why_false_positive": "XRPL_ASSERT validates fName (pointer to SField) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "fName (pointer to SField)", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT at STBase::setFName", + "issue_pattern": "Missing type validation for fName (pointer to SField)", + "why_false_positive": "XRPL_ASSERT validates fName (pointer to SField) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "fName->isUseful()", + "empty", + "string", + "validation" + ], + "evidence": "if statement at STBase::operator=", + "issue_pattern": "Missing empty string validation for fName->isUseful()", + "why_false_positive": "if statement validates fName->isUseful() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "getSType() == STI_NOTPRESENT", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at STBase::isEquivalent", + "issue_pattern": "Missing empty string validation for getSType() == STI_NOTPRESENT", + "why_false_positive": "XRPL_ASSERT validates getSType() == STI_NOTPRESENT for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fName->isBinary()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at STBase::addFieldID", + "issue_pattern": "Missing empty string validation for fName->isBinary()", + "why_false_positive": "XRPL_ASSERT validates fName->isBinary() for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STBase.cpp", + "functions": [ + { + "args": [], + "lineno": 8, + "name": "STBase" + }, + { + "args": [ + "SField const& n" + ], + "lineno": 12, + "name": "STBase" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 16, + "name": "operator=" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 26, + "name": "operator==" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 31, + "name": "operator!=" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 36, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 41, + "name": "move" + }, + { + "args": [], + "lineno": 46, + "name": "getSType" + }, + { + "args": [], + "lineno": 51, + "name": "getFullText" + }, + { + "args": [], + "lineno": 65, + "name": "getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 69, + "name": "getJson" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 74, + "name": "add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 81, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 87, + "name": "isDefault" + }, + { + "args": [ + "SField const& n" + ], + "lineno": 91, + "name": "setFName" + }, + { + "args": [], + "lineno": 97, + "name": "getFName" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 101, + "name": "addFieldID" + }, + { + "args": [ + "std::ostream& out", + "STBase const& t" + ], + "lineno": 108, + "name": "operator<<" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file is a base class with minimal logic and many functions are stubs or default implementations. The main validation points are XRPL_ASSERTs and a few if statements. Test coverage is likely indirect, via derived classes or higher-level serialization/deserialization tests. There may be unit tests for STBase construction, assignment, and field setting in files like test/protocol/STBase_test.cpp or test/protocol/SField_test.cpp, but coverage for error/assertion paths (e.g., invalid fName, non-binary fields) is likely limited or only checked in debug builds. The add() function is marked as unreachable and is not covered by tests (LCOV_EXCL_START).", + "validation_architecture": { + "auto_validated_fields": [ + "fName (pointer validity, binary property, usefulness)", + "getSType() (type presence)" + ], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws in debug)", + "field": "fName (pointer to SField)", + "location": "STBase(SField const& n) constructor", + "validated_by": "XRPL_ASSERT", + "validates": [ + "fName is not null after assignment" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws in debug)", + "field": "fName (pointer to SField)", + "location": "STBase::setFName", + "validated_by": "XRPL_ASSERT", + "validates": [ + "fName is not null after assignment" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "None (conditional logic only)", + "field": "fName->isUseful()", + "location": "STBase::operator=", + "validated_by": "if statement", + "validates": [ + "fName is only overwritten if not useful" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws in debug)", + "field": "getSType() == STI_NOTPRESENT", + "location": "STBase::isEquivalent", + "validated_by": "XRPL_ASSERT", + "validates": [ + "STBase is only equivalent if type is STI_NOTPRESENT" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws in debug)", + "field": "fName->isBinary()", + "location": "STBase::addFieldID", + "validated_by": "XRPL_ASSERT", + "validates": [ + "fName must be a binary field before adding field ID" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STBase.cpp.ai.md b/src/libxrpl/protocol/STBase.cpp.ai.md new file mode 100644 index 0000000000..e7e57a805a --- /dev/null +++ b/src/libxrpl/protocol/STBase.cpp.ai.md @@ -0,0 +1,54 @@ +# `STBase.cpp` — Root of the XRPL Serialized-Type Hierarchy + +`STBase` is the abstract root of every "serialized type" (ST) in the XRPL protocol. Every field that can appear in a transaction, ledger entry, validation, or metadata — integers, amounts, account IDs, path sets, nested objects, arrays — inherits from this class. The `.cpp` file implements the base-class layer: field-name management, comparison dispatch, textual and JSON rendering, binary serialization plumbing, and the small-object allocation interface used by `STVar`. + +## The Field-Name Pointer + +The single data member is `SField const* fName`. `SField` instances are compile-time singletons: they live for the entire program lifetime, are non-copyable and non-movable, and encode a `(fieldType, fieldValue)` pair that serves as the canonical identifier of a field in the XRPL binary format. Every `STBase` is always a named field; the default constructor ties it to `sfGeneric` — the generic sentinel — rather than leaving `fName` null. + +`isUseful()` on `SField` returns `true` when `fieldCode > 0`, meaning the field has a real protocol-assigned identity. Fields with `fieldCode <= 0` (like `sfGeneric`) are placeholders with no wire representation. + +## Assignment Operator Semantics + +The copy-assignment operator has a deliberately non-obvious rule: it only copies `fName` from the right-hand side when `this->fName` is *not useful*. This is called out in a large warning comment in the header: + +> Do not create a vector of any object derived from STBase. The copy assignment operator has semantics that will cause contained types to change their names when an object is deleted because copy assignment is used to "slide down" the remaining types and this will not copy the field name. + +When `STObject` or `STArray` removes an element by sliding remaining elements down via copy assignment, it intentionally preserves the slot's existing (meaningful) field name rather than adopting the source element's name. Conversely, when a freshly-constructed object (holding only `sfGeneric`) is assigned a value, it inherits the name from the source. This dual-purpose assignment is a deliberate trade-off to avoid a separate "copy value, preserve name" API. + +## Comparison + +`operator==` and `operator!=` compose two checks: `getSType()` must match (same runtime type) and `isEquivalent()` must return `true`. The base `isEquivalent()` asserts — via `XRPL_ASSERT` — that it is only ever called on an instance whose type is literally `STI_NOTPRESENT`, i.e., a raw `STBase` object that was never overridden. All concrete subclasses override `isEquivalent()` to perform value comparison. + +## Serialization Interface + +`add(Serializer& s)` is the hook for binary serialization. The base implementation calls `UNREACHABLE()` and is marked `LCOV_EXCL` — it must never execute; every concrete type overrides it. `addFieldID()` handles the universal prefix step: encoding the field's type and index into the byte stream (required before the field's data), guarded by an assertion that the field is "binary" (`fieldValue < 256`), meaning it has a valid protocol wire encoding. + +## Small-Object Placement via `copy()` / `move()` + +The virtual `copy()` and `move()` methods delegate to the protected `emplace()` template: + +```cpp +template +static STBase* emplace(std::size_t n, void* buf, T&& val) +{ + using U = std::decay_t; + if (sizeof(U) > n) + return new U(std::forward(val)); + return new (buf) U(std::forward(val)); +} +``` + +This is the small-object optimization interface consumed by `detail::STVar`. `STVar` maintains a 72-byte aligned inline buffer; if the concrete derived type fits within those bytes it is placement-new'd there, avoiding a heap allocation. If it is larger, a regular `new` is used. All `STBase` subclasses participate automatically by overriding `copy()` and `move()` to call `emplace()` with `*this`. The size threshold and the buffer are owned entirely by `STVar` — `STBase` only provides the mechanism for constructing into an externally supplied buffer. + +## Text and JSON Rendering + +`getText()` returns an empty string at the base level. `getFullText()` checks `getSType() != STI_NOTPRESENT` before emitting anything; if the field has a name (`hasName()` → `fieldCode > 0`), it prepends `"fieldName = "` to the result of `getText()`. `getJson()` simply delegates to `getText()`. Derived classes override `getText()` and optionally `getJson()` for richer output. The free `operator<<` streams `getFullText()`. + +## `downcast()` Safety + +The header defines `downcast()` using `dynamic_cast`, throwing `std::bad_cast` on failure rather than returning null. This makes type-incorrect field access a hard error rather than a silent null dereference — consistent with the XRPL codebase's philosophy of asserting invariants aggressively. + +## Invariants and Assertions + +All `XRPL_ASSERT` calls in this file are business-logic invariants, not input validation. They fire only in debug builds (or assertion-enabled configurations). The four asserted invariants are: (1) `fName` is non-null after construction or `setFName()`; (2) `isEquivalent()` is never reached on a typed subclass without override; (3) `addFieldID()` is only called on fields with a wire representation. These assertions serve as documentation of the protocol contract rather than user-input guards. \ No newline at end of file diff --git a/src/libxrpl/protocol/STBlob.cpp.ai.json b/src/libxrpl/protocol/STBlob.cpp.ai.json new file mode 100644 index 0000000000..209bef89ef --- /dev/null +++ b/src/libxrpl/protocol/STBlob.cpp.ai.json @@ -0,0 +1,317 @@ +{ + "args": [ + { + "lineno": 10, + "name": "st" + }, + { + "lineno": 10, + "name": "name" + }, + { + "lineno": 15, + "name": "n" + }, + { + "lineno": 15, + "name": "buf" + }, + { + "lineno": 35, + "name": "s" + }, + { + "lineno": 44, + "name": "t" + } + ], + "classes": [ + { + "args": [ + "SerialIter& st", + "SField const& name" + ], + "lineno": 9, + "name": "STBlob" + } + ], + "code_paths": [ + { + "call_chain": [ + "STBlob::add" + ], + "entry_point": "STBlob::add", + "purpose": "Serializes the blob value into a Serializer, after validating the field type and binary status.", + "validation_points": [ + "XRPL_ASSERT(getFName().isBinary(), ...)", + "XRPL_ASSERT(getFName().fieldType == STI_VL || getFName().fieldType == STI_ACCOUNT, ...)" + ] + }, + { + "call_chain": [ + "STBlob::STBlob" + ], + "entry_point": "STBlob::STBlob (constructor)", + "purpose": "Constructs an STBlob from a SerialIter and SField, extracting the value from the iterator.", + "validation_points": [] + }, + { + "call_chain": [ + "STBlob::copy", + "STBlob::emplace" + ], + "entry_point": "STBlob::copy / STBlob::move", + "purpose": "Creates a copy or moved instance of STBlob.", + "validation_points": [] + }, + { + "call_chain": [ + "STBlob::isEquivalent" + ], + "entry_point": "STBlob::isEquivalent", + "purpose": "Checks if another STBase is an equivalent STBlob (same value).", + "validation_points": [] + }, + { + "call_chain": [ + "STBlob::isDefault" + ], + "entry_point": "STBlob::isDefault", + "purpose": "Checks if the blob is empty.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "value_", + "flow": [ + "SerialIter (input)", + "STBlob::STBlob (extracts value_)", + "STBlob::add (serializes value_)", + "Serializer::addVL (writes value_)" + ], + "origin": "SerialIter::getVLBuffer() in STBlob::STBlob constructor", + "transformations": [ + "Extracted as a buffer from SerialIter", + "Stored as value_ in STBlob", + "Serialized as variable-length data" + ], + "validated_at": "STBlob::add (field type and binary status validated before serialization)" + }, + { + "field": "getFName()", + "flow": [ + "SField (input to STBlob)", + "STBase (base class stores SField)", + "STBlob::add (calls getFName() for validation)" + ], + "origin": "SField passed to STBlob constructor", + "transformations": [ + "None (passed through as reference)" + ], + "validated_at": "STBlob::add (isBinary() and fieldType checked via XRPL_ASSERT)" + } + ], + "description": "Implements the STBlob class for handling variable-length binary data fields in the XRPL protocol, including serialization, copying, comparison, and textual representation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "getFName().isBinary()", + "validation", + "missing", + "check" + ], + "evidence": "Field getFName().isBinary() validated by XRPL_ASSERT macro (custom assertion, not a full validation framework)", + "issue_pattern": "Missing validation for getFName().isBinary()", + "why_false_positive": "XRPL_ASSERT macro (custom assertion, not a full validation framework) validates getFName().isBinary() automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "getFName().fieldType", + "validation", + "missing", + "check" + ], + "evidence": "Field getFName().fieldType validated by XRPL_ASSERT macro (custom assertion, not a full validation framework)", + "issue_pattern": "Missing validation for getFName().fieldType", + "why_false_positive": "XRPL_ASSERT macro (custom assertion, not a full validation framework) validates getFName().fieldType automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "getFName().isBinary()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at STBlob::add", + "issue_pattern": "Missing empty string validation for getFName().isBinary()", + "why_false_positive": "XRPL_ASSERT macro validates getFName().isBinary() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "getFName().isBinary()", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT macro at STBlob::add", + "issue_pattern": "Missing type validation for getFName().isBinary()", + "why_false_positive": "XRPL_ASSERT macro validates getFName().isBinary() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "getFName().fieldType", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at STBlob::add", + "issue_pattern": "Missing empty string validation for getFName().fieldType", + "why_false_positive": "XRPL_ASSERT macro validates getFName().fieldType for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "getFName().fieldType", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT macro at STBlob::add", + "issue_pattern": "Missing type validation for getFName().fieldType", + "why_false_positive": "XRPL_ASSERT macro validates getFName().fieldType type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STBlob.cpp", + "functions": [ + { + "args": [ + "SerialIter& st", + "SField const& name" + ], + "lineno": 10, + "name": "STBlob" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 15, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 20, + "name": "move" + }, + { + "args": [], + "lineno": 25, + "name": "getSType" + }, + { + "args": [], + "lineno": 30, + "name": "getText" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 35, + "name": "add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 44, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 50, + "name": "isDefault" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "STBlob is a low-level serialization class. Typical test coverage would be in protocol serialization/deserialization tests, likely in files such as 'test/protocol/STBlob_test.cpp', 'test/protocol/Serializer_test.cpp', or integration tests involving transaction or ledger serialization. The validation code (XRPL_ASSERT) is only triggered in STBlob::add, so tests must exercise serialization of STBlob fields with both valid and invalid SField configurations. Gaps may exist if tests do not cover non-binary fields or fields with incorrect fieldType (not STI_VL or STI_ACCOUNT), as these would trigger assertions. There is no evidence in this file of direct test hooks or coverage annotations.", + "validation_architecture": { + "auto_validated_fields": [ + "getFName().isBinary()", + "getFName().fieldType" + ], + "framework": "XRPL_ASSERT macro (custom assertion, not a full validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "getFName().isBinary()", + "location": "STBlob::add", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that the field name is of binary type" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "getFName().fieldType", + "location": "STBlob::add", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that the field type is either STI_VL (variable length) or STI_ACCOUNT" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STBlob.cpp.ai.md b/src/libxrpl/protocol/STBlob.cpp.ai.md new file mode 100644 index 0000000000..12d6b85adb --- /dev/null +++ b/src/libxrpl/protocol/STBlob.cpp.ai.md @@ -0,0 +1,36 @@ +# STBlob.cpp — Variable-Length Binary Field Implementation + +## Role in the System + +`STBlob` is the XRPL serialized-type class that holds variable-length binary data (`STI_VL`) and account identifiers (`STI_ACCOUNT`). It sits at the bottom of the protocol's type hierarchy — `STBase` is the abstract root, and `STBlob` is one of the concrete leaf types that knows how to read itself from a byte stream, write itself back, and compare its contents. In ledger objects and transactions, fields like `SigningPubKey`, `TxnSignature`, and `Account` are all represented as `STBlob` instances at the serialization layer. + +## Class Hierarchy and `emplace` Pattern + +`STBlob` inherits from both `STBase` and `CountedObject`. The `CountedObject` mixin provides live-object counting for diagnostics, while `STBase` supplies the field name (`SField`) and the virtual dispatch interface common to all serialized types. + +The two private methods `copy()` and `move()` exist solely to support `detail::STVar`, a type-erased container for polymorphic ST objects used inside `STObject`. Rather than owning ST values through heap-allocated `unique_ptr`s, `STVar` maintains a small fixed-size inline buffer and uses placement new when the object fits. The `STBase::emplace()` template captures this: if `sizeof(STBlob) <= n`, it constructs the value in `buf` via placement new and returns a pointer to it; otherwise it falls back to a regular `new` heap allocation. `copy()` and `move()` simply forward `*this` (by copy or `std::move`) into this helper, enabling the `STVar` machinery to clone or relocate blob fields without knowing their concrete type at the call site. + +## Serialization and Deserialization + +Deserialization is handled in the `SerialIter` constructor, which extracts the blob's payload by calling `st.getVLBuffer()`. This reads a variable-length-prefixed byte sequence from the stream and returns a `Buffer` — an owning heap-allocated byte array. The length prefix is decoded by `SerialIter` before this point, so the `STBlob` constructor receives already-delimited data and does nothing but store it. + +Serialization goes through `add(Serializer& s)`, which calls `s.addVL(value_.data(), value_.size())`. The `addVL` family encodes the length prefix before the raw bytes, exactly the inverse of `getVLBuffer`. Before writing anything, `add()` enforces two `XRPL_ASSERT` invariants: + +1. `getFName().isBinary()` — checks that the `SField`'s numeric field value is less than 256, which in the XRPL field numbering system distinguishes binary fields from non-binary ones. +2. `getFName().fieldType == STI_VL || getFName().fieldType == STI_ACCOUNT` — guards that only the two known wire types that use VL-encoding pass through this serialization path. + +These assertions defend against a programmer mistake — constructing an `STBlob` around an `SField` of the wrong type, which would produce a malformed wire encoding. With the statically declared `SField` constants used everywhere in the codebase this should never fire in production, but the guards catch misuse early during development. + +## The Dual-Type Design (`STI_VL` and `STI_ACCOUNT`) + +Both `STI_VL` and `STI_ACCOUNT` fields are represented by the same `STBlob` class with the same wire encoding: a VL-prefixed byte string. An `STI_ACCOUNT` field is just a 20-byte blob (the account's `AccountID`). The distinction in `fieldType` carries semantic meaning — it controls how the JSON layer formats the value and how parsers validate it — but at the binary serialization level handled by this file, both cases go through the identical `addVL` path. This unification avoids duplicating the serialization logic while still preserving the type tag in the `SField` for higher-level interpretation. + +## Supporting Methods + +`getSType()` unconditionally returns `STI_VL`, which is the type code used by the protocol's field-ID encoding. When an `STBlob` is added to a containing `STObject` and written to the wire, the field-ID byte encodes `STI_VL` regardless of whether the field is semantically an account address or a raw blob. + +`getText()` converts the raw bytes to uppercase hex via `strHex()`, which is used by `getFullText()` in `STBase` and ultimately surfaces in JSON output and log messages. + +`isEquivalent()` performs a `dynamic_cast` to confirm the other `STBase` is actually an `STBlob`, then compares the underlying `Buffer` values. This byte-level equality check is used by `STBase::operator==` and ultimately drives transaction de-duplication and ledger comparison logic. + +`isDefault()` returns `true` when `value_` is empty. The `STObject` serialization machinery uses this to skip optional fields that have not been set, keeping wire-format representations compact. \ No newline at end of file diff --git a/src/libxrpl/protocol/STCurrency.cpp.ai.json b/src/libxrpl/protocol/STCurrency.cpp.ai.json new file mode 100644 index 0000000000..611cc42968 --- /dev/null +++ b/src/libxrpl/protocol/STCurrency.cpp.ai.json @@ -0,0 +1,316 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "SField const& name" + ], + "lineno": 11, + "name": "STCurrency" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 15, + "name": "STCurrency" + }, + { + "args": [ + "SField const& name", + "Currency const& currency" + ], + "lineno": 19, + "name": "STCurrency" + } + ], + "code_paths": [ + { + "call_chain": [ + "currencyFromJson" + ], + "entry_point": "currencyFromJson", + "purpose": "Parses a JSON value into an STCurrency object, validating the input.", + "validation_points": [ + "currencyFromJson: v.isString() - Ensures input is a string.", + "currencyFromJson: currency == badCurrency() || currency == noCurrency() - Ensures parsed currency is valid." + ] + }, + { + "call_chain": [ + "STCurrency::construct", + "STCurrency(SerialIter&, SField const&)" + ], + "entry_point": "STCurrency::construct", + "purpose": "Constructs an STCurrency from serialized data.", + "validation_points": [] + }, + { + "call_chain": [ + "STCurrency::getJson" + ], + "entry_point": "STCurrency::getJson", + "purpose": "Serializes the currency field to JSON.", + "validation_points": [] + }, + { + "call_chain": [ + "STCurrency::add" + ], + "entry_point": "STCurrency::add", + "purpose": "Serializes the currency field to a Serializer.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "currency (in STCurrency)", + "flow": [ + "JSON input (v)", + "currencyFromJson: v.asString()", + "to_currency(v.asString())", + "STCurrency{name, currency}" + ], + "origin": "JSON input (Json::Value v) to currencyFromJson", + "transformations": [ + "JSON string extracted", + "Converted to Currency type via to_currency", + "Validated against badCurrency() and noCurrency()" + ], + "validated_at": "currencyFromJson (isString() and currency validity check)" + }, + { + "field": "currency_ (member of STCurrency)", + "flow": [ + "SerialIter", + "sit.get160()", + "currency_" + ], + "origin": "SerialIter in STCurrency(SerialIter&, SField const&)", + "transformations": [ + "160-bit value extracted from serialized data" + ], + "validated_at": "Not explicitly validated in this constructor" + } + ], + "description": "Implements the STCurrency class for representing and serializing currency types in the XRPL protocol, including construction, serialization, JSON conversion, and comparison.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "v (JSON input for currency)", + "empty", + "string", + "validation" + ], + "evidence": "v.isString() at currencyFromJson", + "issue_pattern": "Missing empty string validation for v (JSON input for currency)", + "why_false_positive": "v.isString() validates v (JSON input for currency) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "v (JSON input for currency)", + "type", + "validation", + "check" + ], + "evidence": "v.isString() at currencyFromJson", + "issue_pattern": "Missing type validation for v (JSON input for currency)", + "why_false_positive": "v.isString() validates v (JSON input for currency) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "currency (parsed from JSON string)", + "empty", + "string", + "validation" + ], + "evidence": "currency == badCurrency() || currency == noCurrency() at currencyFromJson", + "issue_pattern": "Missing empty string validation for currency (parsed from JSON string)", + "why_false_positive": "currency == badCurrency() || currency == noCurrency() validates currency (parsed from JSON string) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STCurrency.cpp", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "STCurrency::getSType" + }, + { + "args": [], + "lineno": 27, + "name": "STCurrency::getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 32, + "name": "STCurrency::getJson" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 37, + "name": "STCurrency::add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 42, + "name": "STCurrency::isEquivalent" + }, + { + "args": [], + "lineno": 48, + "name": "STCurrency::isDefault" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 52, + "name": "STCurrency::construct" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 57, + "name": "STCurrency::copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 62, + "name": "STCurrency::move" + }, + { + "args": [ + "SField const& name", + "Json::Value const& v" + ], + "lineno": 67, + "name": "currencyFromJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is in currencyFromJson, which expects a JSON string and checks for valid currency values. Typical test coverage would be in unit tests for JSON parsing and currency validation, likely in files such as STCurrency_test.cpp, STObject_test.cpp, or broader transaction/serialization tests. Gaps: No explicit validation for serialized input (SerialIter path); only JSON input is validated. Edge cases for malformed or boundary currency values from serialization may not be covered unless tested elsewhere.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (Throw<>, to_currency, isString from Json::Value)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "v (JSON input for currency)", + "location": "currencyFromJson", + "validated_by": "v.isString()", + "validates": [ + "Checks that the JSON value for currency is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "currency (parsed from JSON string)", + "location": "currencyFromJson", + "validated_by": "currency == badCurrency() || currency == noCurrency()", + "validates": [ + "Checks that the parsed currency is not an invalid or reserved value (badCurrency or noCurrency)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STCurrency.cpp.ai.md b/src/libxrpl/protocol/STCurrency.cpp.ai.md new file mode 100644 index 0000000000..cb7008fca5 --- /dev/null +++ b/src/libxrpl/protocol/STCurrency.cpp.ai.md @@ -0,0 +1,50 @@ +# STCurrency.cpp — Serialized Currency Field + +## Role in the System + +`STCurrency` is the serialized-type wrapper for XRPL currency identifiers. It slots into the `STBase` polymorphic hierarchy that underpins every field inside a ledger object or transaction. The "ST" prefix is protocol-wide shorthand for "Serialized Type" — every wire-format field type (`STAmount`, `STObject`, `STArray`, and so on) inherits from `STBase` and overrides the same small set of virtual methods. `STCurrency` is the leaf node in that hierarchy responsible for holding and round-tripping a single 160-bit currency code. + +## The `Currency` Type + +At the heart of the class is a single private member `currency_` of type `Currency`, which is `base_uint<160, detail::CurrencyTag>`. The tag parameter is a phantom type that makes `Currency`, `NodeID`, and other 160-bit values mutually incompatible without any runtime cost — an example of the policy the XRPL codebase applies throughout `UintTypes.h`. XRP itself is represented as the all-zeroes value; `isXRP()` simply tests for `beast::zero`. This zero-means-XRP convention drives the `isDefault()` override, which returns `true` when the stored currency is XRP. In `STBase`'s semantics, "default" values are omitted during canonical serialization, so a naked XRP amount field need not carry an explicit currency code on the wire. + +## Construction Paths + +Three constructors cover the three entry routes: + +1. **Name-only** (`STCurrency(SField const&)`) — produces a default-constructed (XRP) currency. Used when an `STObject` allocates a placeholder for a field that has not yet been populated. +2. **Deserialization** (`STCurrency(SerialIter&, SField const&)`) — reads exactly 160 bits from the stream via `sit.get160()`. No validation is performed here: the binary wire format is assumed correct, and the cost of parsing the ledger database would be prohibitive if every field value were re-checked on ingestion. +3. **Direct value** (`STCurrency(SField const&, Currency const&)`) — used programmatically when the `Currency` value is already known. + +The static factory `construct()` is a thin wrapper around constructor #2, kept `private` and declared `friend` of `detail::STVar`. `STVar` is the type-erased variant the `STObject` container uses internally; it discovers `construct` through a compile-time registration mechanism that maps `STI_CURRENCY` to this factory. + +## Small-Buffer Copy and Move + +The `copy()` and `move()` overrides delegate to `STBase::emplace()`: + +```cpp +STBase* copy(std::size_t n, void* buf) const override { return emplace(n, buf, *this); } +STBase* move(std::size_t n, void* buf) override { return emplace(n, buf, std::move(*this)); } +``` + +`emplace()` checks whether `sizeof(STCurrency)` fits within the caller-supplied `n`-byte `buf`. If it does, it uses placement-new into that buffer; otherwise it falls back to a heap allocation. This is the small-buffer optimization baked into `STVar`: container elements that are small enough live inline inside the parent object, avoiding a separate allocation and pointer chase for the common case. `STCurrency` is a compact type (a 160-bit integer plus a pointer-sized `fName`), so it will almost always land in the inline buffer. + +## Serialization and JSON + +`add(Serializer&)` emits the 160-bit value verbatim via `s.addBitString(currency_)`. `getText()` and `getJson()` both delegate to `to_string(currency_)`, which returns `""` for zero (XRP), `"XRP"` as a displayable alias, or the three-character ISO-4217-style ticker for well-known tokens, falling back to a hex string for opaque custom currencies. + +`isEquivalent()` performs a `dynamic_cast` to verify the other `STBase` is actually an `STCurrency` before comparing values. This is the standard pattern across all `STBase` subclasses — they must handle polymorphic comparison without a common strongly-typed overload. + +## JSON Deserialization and Validation + +The free function `currencyFromJson(SField const&, Json::Value const&)` is the only place in this file where untrusted external input is handled: + +```cpp +if (!v.isString()) Throw(...); +auto const currency = to_currency(v.asString()); +if (currency == badCurrency() || currency == noCurrency()) Throw(...); +``` + +Two validation gates are required rather than one because of a legacy quirk in `to_currency()`: it can return `badCurrency()` on a successful parse (the three-letter string `"XRP"` used as a token identifier, which the network deliberately prohibits to prevent confusion with native XRP). The docstring in `UintTypes.h` explicitly flags this as unfortunate legacy behavior that would be risky to change. `currencyFromJson` therefore rejects both sentinel values explicitly, giving callers a clean guarantee: if it doesn't throw, the returned `STCurrency` holds a well-formed, non-reserved currency. + +The asymmetry between the two deserialization paths — binary (`SerialIter`, no validation) and JSON (strict validation) — is intentional. Binary ledger data originates from a consensus-validated stream; JSON arrives from API consumers whose input cannot be trusted. \ No newline at end of file diff --git a/src/libxrpl/protocol/STInteger.cpp.ai.json b/src/libxrpl/protocol/STInteger.cpp.ai.json new file mode 100644 index 0000000000..629567b9ae --- /dev/null +++ b/src/libxrpl/protocol/STInteger.cpp.ai.json @@ -0,0 +1,400 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "STUInt8::getText or getJson", + "transResultInfo(TER::fromInt(value_), ...)" + ], + "entry_point": "STUInt8::getText / STUInt8::getJson", + "purpose": "Converts an 8-bit integer field (sfTransactionResult) to a human-readable string or JSON token, validating that the value is a known transaction result code.", + "validation_points": [ + "transResultInfo(TER::fromInt(value_), ...): Validates value_ as a known TER code" + ] + }, + { + "call_chain": [ + "STUInt16::getText or getJson", + "LedgerFormats::getInstance().findByType(safe_cast(value_))", + "TxFormats::getInstance().findByType(safe_cast(value_))" + ], + "entry_point": "STUInt16::getText / STUInt16::getJson", + "purpose": "Converts a 16-bit integer field (sfLedgerEntryType or sfTransactionType) to a human-readable string or JSON token, validating that the value is a known ledger entry or transaction type.", + "validation_points": [ + "LedgerFormats::getInstance().findByType(...): Validates value_ as a known LedgerEntryType", + "TxFormats::getInstance().findByType(...): Validates value_ as a known TxType" + ] + }, + { + "call_chain": [ + "STUInt32::getText or getJson", + "Permission::getInstance().getPermissionName(value_)" + ], + "entry_point": "STUInt32::getText / STUInt32::getJson", + "purpose": "Converts a 32-bit integer field (sfPermissionValue) to a human-readable permission name if possible.", + "validation_points": [ + "Permission::getInstance().getPermissionName(value_): Checks if value_ is a known permission" + ] + } + ], + "data_flows": [ + { + "field": "value_ (for sfTransactionResult)", + "flow": [ + "SerialIter.get8()", + "STInteger::STInteger", + "STUInt8::getText / getJson", + "transResultInfo(TER::fromInt(value_), ...)" + ], + "origin": "Deserialized from SerialIter via STInteger::STInteger", + "transformations": [ + "Raw byte read from serialized data", + "Wrapped in STInteger", + "Converted to TER via TER::fromInt", + "Validated and mapped to string/token" + ], + "validated_at": "transResultInfo(TER::fromInt(value_), ...)" + }, + { + "field": "value_ (for sfLedgerEntryType)", + "flow": [ + "SerialIter.get16()", + "STInteger::STInteger", + "STUInt16::getText / getJson", + "LedgerFormats::getInstance().findByType(safe_cast(value_))" + ], + "origin": "Deserialized from SerialIter via STInteger::STInteger", + "transformations": [ + "16-bit value read from serialized data", + "Wrapped in STInteger", + "Safe cast to LedgerEntryType", + "Validated and mapped to string" + ], + "validated_at": "LedgerFormats::getInstance().findByType(safe_cast(value_))" + }, + { + "field": "value_ (for sfTransactionType)", + "flow": [ + "SerialIter.get16()", + "STInteger::STInteger", + "STUInt16::getText / getJson", + "TxFormats::getInstance().findByType(safe_cast(value_))" + ], + "origin": "Deserialized from SerialIter via STInteger::STInteger", + "transformations": [ + "16-bit value read from serialized data", + "Wrapped in STInteger", + "Safe cast to TxType", + "Validated and mapped to string" + ], + "validated_at": "TxFormats::getInstance().findByType(safe_cast(value_))" + } + ], + "description": "Implements template specializations for serialization and JSON/text conversion of various integer types (STUInt8, STUInt16, STUInt32, STUInt64, STInt32) used in the XRPL protocol, including logic for interpreting field values as human-readable strings or tokens based on field type.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTransactionResult", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTransactionResult validated by Custom logic (LedgerFormats, TxFormats, transResultInfo)", + "issue_pattern": "Missing validation for sfTransactionResult", + "why_false_positive": "Custom logic (LedgerFormats, TxFormats, transResultInfo) validates sfTransactionResult automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLedgerEntryType", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLedgerEntryType validated by Custom logic (LedgerFormats, TxFormats, transResultInfo)", + "issue_pattern": "Missing validation for sfLedgerEntryType", + "why_false_positive": "Custom logic (LedgerFormats, TxFormats, transResultInfo) validates sfLedgerEntryType automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTransactionType", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTransactionType validated by Custom logic (LedgerFormats, TxFormats, transResultInfo)", + "issue_pattern": "Missing validation for sfTransactionType", + "why_false_positive": "Custom logic (LedgerFormats, TxFormats, transResultInfo) validates sfTransactionType automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "value_ (for sfTransactionResult)", + "empty", + "string", + "validation" + ], + "evidence": "transResultInfo(TER::fromInt(value_), ...) at STUInt8::getText and STUInt8::getJson", + "issue_pattern": "Missing empty string validation for value_ (for sfTransactionResult)", + "why_false_positive": "transResultInfo(TER::fromInt(value_), ...) validates value_ (for sfTransactionResult) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "value_ (for sfLedgerEntryType)", + "empty", + "string", + "validation" + ], + "evidence": "LedgerFormats::getInstance().findByType(safe_cast(value_)) at STUInt16::getText and STUInt16::getJson", + "issue_pattern": "Missing empty string validation for value_ (for sfLedgerEntryType)", + "why_false_positive": "LedgerFormats::getInstance().findByType(safe_cast(value_)) validates value_ (for sfLedgerEntryType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "value_ (for sfTransactionType)", + "empty", + "string", + "validation" + ], + "evidence": "TxFormats::getInstance().findByType(safe_cast(value_)) at STUInt16::getText and STUInt16::getJson", + "issue_pattern": "Missing empty string validation for value_ (for sfTransactionType)", + "why_false_positive": "TxFormats::getInstance().findByType(safe_cast(value_)) validates value_ (for sfTransactionType) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STInteger.cpp", + "functions": [ + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 13, + "name": "STInteger::STInteger" + }, + { + "args": [ + "const" + ], + "lineno": 18, + "name": "STUInt8::getSType" + }, + { + "args": [ + "const" + ], + "lineno": 23, + "name": "STUInt8::getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 41, + "name": "STUInt8::getJson" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 61, + "name": "STInteger::STInteger" + }, + { + "args": [ + "const" + ], + "lineno": 66, + "name": "STUInt16::getSType" + }, + { + "args": [ + "const" + ], + "lineno": 71, + "name": "STUInt16::getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 87, + "name": "STUInt16::getJson" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 107, + "name": "STInteger::STInteger" + }, + { + "args": [ + "const" + ], + "lineno": 112, + "name": "STUInt32::getSType" + }, + { + "args": [ + "const" + ], + "lineno": 117, + "name": "STUInt32::getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 127, + "name": "STUInt32::getJson" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 142, + "name": "STInteger::STInteger" + }, + { + "args": [ + "const" + ], + "lineno": 147, + "name": "STUInt64::getSType" + }, + { + "args": [ + "const" + ], + "lineno": 152, + "name": "STUInt64::getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 157, + "name": "STUInt64::getJson" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 181, + "name": "STInteger::STInteger" + }, + { + "args": [ + "const" + ], + "lineno": 186, + "name": "STInt32::getSType" + }, + { + "args": [ + "const" + ], + "lineno": 191, + "name": "STInt32::getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 196, + "name": "STInt32::getJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation code paths are typically exercised by higher-level transaction, ledger, and serialization/deserialization tests. Likely test files include: 'test/protocol/STInteger_test.cpp', 'test/protocol/TER_test.cpp', 'test/protocol/LedgerFormats_test.cpp', and 'test/protocol/TxFormats_test.cpp'. However, the error logging branches (e.g., unknown result code, unknown ledger/tx type) are marked with LCOV_EXCL_START/STOP, indicating they are not covered by tests. Thus, negative/invalid value paths are not tested. Most tests focus on valid, known values.", + "validation_architecture": { + "auto_validated_fields": [ + "sfTransactionResult", + "sfLedgerEntryType", + "sfTransactionType" + ], + "framework": "Custom logic (LedgerFormats, TxFormats, transResultInfo)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "Logs error via JLOG, no exception thrown", + "field": "value_ (for sfTransactionResult)", + "location": "STUInt8::getText and STUInt8::getJson", + "validated_by": "transResultInfo(TER::fromInt(value_), ...)", + "validates": [ + "Checks if value_ is a known transaction result code" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "No exception, returns value_ as string if not found", + "field": "value_ (for sfLedgerEntryType)", + "location": "STUInt16::getText and STUInt16::getJson", + "validated_by": "LedgerFormats::getInstance().findByType(safe_cast(value_))", + "validates": [ + "Checks if value_ is a known LedgerEntryType" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "No exception, returns value_ as string if not found", + "field": "value_ (for sfTransactionType)", + "location": "STUInt16::getText and STUInt16::getJson", + "validated_by": "TxFormats::getInstance().findByType(safe_cast(value_))", + "validates": [ + "Checks if value_ is a known TxType" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STInteger.cpp.ai.md b/src/libxrpl/protocol/STInteger.cpp.ai.md new file mode 100644 index 0000000000..509ea62ce0 --- /dev/null +++ b/src/libxrpl/protocol/STInteger.cpp.ai.md @@ -0,0 +1,33 @@ +# `STInteger.cpp` — Field-Aware Integer Serialization Specializations + +`STInteger.cpp` provides the explicit template specializations for the `STInteger` class template, which is the XRPL serialized type for all integer fields in the protocol. The file exists specifically because the generic template — fully defined in `STInteger.h` — cannot express per-instantiation behavior for deserialization, type identification, and human-readable output. Every method that needs to differ between `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t`, and `int32_t` lives here. + +## Why Template Specializations in a Separate File + +The header provides inline implementations for `add()`, `isDefault()`, `isEquivalent()`, `operator=`, and the copy/move plumbing — all of which behave identically regardless of the underlying integer type. But four virtual methods require type-specific logic: the `SerialIter` constructor (which must call `get8()`, `get16()`, `get32()`, or `get64()`), `getSType()` (which must return the matching `STI_UINT8` / `STI_UINT16` / etc. constant), `getText()`, and `getJson()`. Putting those in a `.cpp` file prevents duplicate symbol problems and avoids implicitly instantiating all specializations in every translation unit that includes the header. + +## Field-Identity-Aware Formatting + +The architecturally significant pattern here is that `getText()` and `getJson()` are not naive integer-to-string converters. They inspect `getFName()` — the field's compile-time identity — and produce semantic output for well-known protocol fields: + +**`STUInt8` / `sfTransactionResult`**: The 8-bit transaction result code is converted to a `TER` via `TER::fromInt()` and passed to `transResultInfo()`, which resolves it to a human-readable description (`getText()`) or a short token string like `"tesSUCCESS"` (`getJson()`). This field appears in transaction metadata, so these two output formats serve different consumers: human debugging versus API clients parsing JSON. If the code is unrecognized — which is expected to be impossible under correct operation — an error is logged and the raw integer falls through. The `LCOV_EXCL_START/STOP` markers around those branches acknowledge they are untestable by design. + +**`STUInt16` / `sfLedgerEntryType` and `sfTransactionType`**: The 16-bit type codes are converted to their respective enum types via `safe_cast()` and `safe_cast()`, then looked up in the singleton registries `LedgerFormats::getInstance()` and `TxFormats::getInstance()`. Using `safe_cast<>` here rather than a C-style cast is defensive: it ensures the conversion is intentional and auditable, even though both fields hold raw integers on the wire. The result is that JSON output for a ledger entry shows `"Offer"` rather than `7`, and for a transaction shows `"Payment"` rather than `0`. + +**`STUInt32` / `sfPermissionValue`**: Permission values are delegated to `Permission::getInstance().getPermissionName()`, which first attempts granular permission lookup, then falls back to transaction-type-based permission name resolution. This makes `sfPermissionValue` fields readable in API output while keeping the on-wire representation compact. + +## `STUInt64::getJson()` — Hex vs. Decimal, Always a String + +The 64-bit specialization is the most nuanced. JSON's `number` type is an IEEE 754 double, which cannot represent arbitrary `uint64_t` values without precision loss. `getJson()` therefore always returns a `Json::Value` constructed from a `std::string`, never from a raw numeric type. The conversion is done with `std::to_chars` — locale-independent, no-allocation, and guaranteed to produce the exact decimal or hexadecimal representation. + +The choice between base 10 and base 16 is driven by `SField::sMD_BaseTen`, a metadata flag stored on the `SField` instance. Fields explicitly annotated with `sMD_BaseTen` (such as sequence-like counters) render in decimal; all others render in hex, which is the natural representation for opaque 64-bit identifiers like quality values or rate denominators. This field-level metadata eliminates ad-hoc conditionals: the formatting decision was made when the field was registered, not at output time. + +`STUInt64::getText()` is notably simpler — it just calls `std::to_string()` — because the text representation is used in log output and diagnostic contexts where decimal is universally preferred regardless of field identity. + +## Deserialization Path + +Each specialization's `SerialIter` constructor simply delegates to the value constructor with the appropriately-sized read: `sit.get8()`, `sit.get16()`, `sit.get32()`, or `sit.get64()`. Note that `STInt32` (signed) reads via `sit.get32()` — the same method as `STUInt32` — and relies on the implicit bit-pattern reinterpretation that occurs when the unsigned result is stored in a signed integer. This is standard behavior in the XRPL serialization model, which treats the wire as type-agnostic bytes. + +## Relationship to `STParsedJSON.cpp` + +This file and `STParsedJSON.cpp` form a symmetric pair for the human-readable fields. `STParsedJSON.cpp` parses string values like `"tesSUCCESS"` or `"Payment"` back into their integer representations during JSON ingestion. `STInteger.cpp` converts stored integers back to those strings during JSON emission. The round-trip is exact for all registered types; unrecognized values degrade gracefully to raw integers rather than failing. \ No newline at end of file diff --git a/src/libxrpl/protocol/STIssue.cpp.ai.json b/src/libxrpl/protocol/STIssue.cpp.ai.json new file mode 100644 index 0000000000..2de954c65a --- /dev/null +++ b/src/libxrpl/protocol/STIssue.cpp.ai.json @@ -0,0 +1,275 @@ +{ + "args": [ + { + "lineno": 15, + "name": "name" + }, + { + "lineno": 18, + "name": "sit" + }, + { + "lineno": 66, + "name": "s" + }, + { + "lineno": 83, + "name": "t" + }, + { + "lineno": 96, + "name": "n" + }, + { + "lineno": 96, + "name": "buf" + }, + { + "lineno": 106, + "name": "v" + } + ], + "classes": [ + { + "args": [ + "SField const& name" + ], + "lineno": 15, + "name": "STIssue" + } + ], + "code_paths": [ + { + "call_chain": [ + "STIssue::STIssue(SerialIter&, SField const&)", + "sit.get160()", + "isXRP(static_cast(currencyOrAccount))", + "sit.get160() (again, if not XRP)", + "noAccount() == account", + "sit.get32() (if MPT)", + "isConsistent(issue) (if not MPT)", + "Throw(...) (if validation fails)" + ], + "entry_point": "STIssue::STIssue(SerialIter&, SField const&)", + "purpose": "Deserializes an Issue or MPTIssue from serialized data, validates Issue consistency.", + "validation_points": [ + "isConsistent(issue) (validates currency/account native mismatch)" + ] + }, + { + "call_chain": [ + "STIssue::add(Serializer&)", + "asset_.visit(...)", + "s.addBitString(issue.currency)", + "s.addBitString(issue.account) (if not XRP)", + "s.addBitString(issue.getIssuer()) (if MPTIssue)", + "s.addBitString(noAccount())", + "s.add32(sequence)" + ], + "entry_point": "STIssue::add(Serializer&)", + "purpose": "Serializes the Issue or MPTIssue to a Serializer.", + "validation_points": [] + }, + { + "call_chain": [ + "issueFromJson(SField const&, Json::Value const&)", + "assetFromJson(v)", + "STIssue{name, assetFromJson(v)}" + ], + "entry_point": "issueFromJson(SField const&, Json::Value const&)", + "purpose": "Constructs an STIssue from JSON input.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "currency", + "flow": [ + "sit.get160()", + "static_cast(currencyOrAccount)", + "isXRP() check", + "if not XRP: assigned to issue.currency", + "isConsistent(issue) validation", + "asset_ = issue" + ], + "origin": "sit.get160() in STIssue::STIssue(SerialIter&, SField const&)", + "transformations": [ + "Deserialized from serialized data", + "Type-cast to Currency", + "Checked for XRP special case" + ], + "validated_at": "isConsistent(issue)" + }, + { + "field": "account", + "flow": [ + "sit.get160()", + "static_cast(...)", + "if noAccount() == account: MPT path", + "else: assigned to issue.account", + "isConsistent(issue) validation", + "asset_ = issue" + ], + "origin": "sit.get160() (second call, if not XRP) in STIssue::STIssue(SerialIter&, SField const&)", + "transformations": [ + "Deserialized from serialized data", + "Type-cast to AccountID", + "Checked for MPT special case" + ], + "validated_at": "isConsistent(issue)" + }, + { + "field": "MPTID", + "flow": [ + "sit.get32() (sequence)", + "memcpy to mptID.data()", + "memcpy currencyOrAccount to mptID.data() + sizeof(sequence)", + "MPTIssue const issue{mptID}", + "asset_ = issue" + ], + "origin": "sit.get32() and currencyOrAccount in STIssue::STIssue(SerialIter&, SField const&)", + "transformations": [ + "Constructed from sequence and currencyOrAccount", + "No explicit validation" + ], + "validated_at": "N/A (no explicit validation)" + } + ], + "description": "Implements the STIssue class for serializing, deserializing, and handling Issue and MPTIssue types in the XRPL protocol, including JSON and binary serialization, equivalence checks, and default value checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "issue (currency and account fields)", + "empty", + "string", + "validation" + ], + "evidence": "isConsistent(issue) at STIssue::STIssue(SerialIter& sit, SField const& name) constructor", + "issue_pattern": "Missing empty string validation for issue (currency and account fields)", + "why_false_positive": "isConsistent(issue) validates issue (currency and account fields) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STIssue.cpp", + "functions": [ + { + "args": [ + "SField const& name" + ], + "lineno": 15, + "name": "STIssue" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 18, + "name": "STIssue" + }, + { + "args": [], + "lineno": 49, + "name": "getSType" + }, + { + "args": [], + "lineno": 54, + "name": "getText" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 59, + "name": "getJson" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 66, + "name": "add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 83, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 89, + "name": "isDefault" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 96, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 101, + "name": "move" + }, + { + "args": [ + "SField const& name", + "Json::Value const& v" + ], + "lineno": 106, + "name": "issueFromJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 14, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation (isConsistent(issue)) is only triggered during deserialization from SerialIter (binary input). There is no validation when constructing from JSON (issueFromJson) or when copying/moving. Test coverage should include tests for deserialization of both valid and invalid Issues (currency/account mismatch), including XRP, non-XRP, and MPT cases. Tests should also cover exception throwing on invalid input. Gaps: No validation on JSON input path, so malformed or inconsistent Issues could be constructed via JSON without error. Test files likely to cover this: protocol/STIssue_test.cpp, protocol/Issue_test.cpp, and possibly integration/serialization tests. Gaps may exist if tests do not explicitly check for exception throwing on invalid binary input.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (xrpl protocol, isConsistent, Throw)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw(...))", + "field": "issue (currency and account fields)", + "location": "STIssue::STIssue(SerialIter& sit, SField const& name) constructor", + "validated_by": "isConsistent(issue)", + "validates": [ + "Checks that the currency and account fields of the Issue are consistent (e.g., native currency must have no issuer, non-native must have issuer)", + "Prevents native currency from being paired with a non-null account, and vice versa" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STIssue.cpp.ai.md b/src/libxrpl/protocol/STIssue.cpp.ai.md new file mode 100644 index 0000000000..8ce4d67573 --- /dev/null +++ b/src/libxrpl/protocol/STIssue.cpp.ai.md @@ -0,0 +1,43 @@ +# `STIssue.cpp` — Serialized Asset Type for IOU, MPT, and XRP + +`STIssue` is the protocol-layer wrapper that allows an `Asset` — the domain abstraction for XRP, IOU, or Multi-Purpose Token (MPT) issuances — to live inside an XRPL serialized transaction or ledger object. It subclasses `STBase`, integrating with the broader `STObject` hierarchy that represents the canonical XRPL binary and JSON formats. + +## Role in the Serialization System + +The XRPL protocol represents typed fields using the `STBase` family of classes. `STIssue` plays the same role for asset identifiers that `STAmount` plays for amounts: it gives an `Asset` a field name, a type ID (`STI_ISSUE`), and the serialize/deserialize methods needed to traverse ledger objects generically. Without this wrapper, raw `Issue` and `MPTIssue` values would not be embeddable as named fields in `STObject`. + +Internally, `STIssue` holds a single `Asset asset_` initialized to `xrpIssue()`. Because `Asset` is `std::variant`, `STIssue` transparently handles all three asset flavors (XRP, IOU, MPT) through the variant's active member. + +## Wire Format and the Deserializing Constructor + +The most complex logic lives in `STIssue(SerialIter& sit, SField const& name)`. The serialized representation is a fixed-width field with type-multiplexing: + +- **XRP**: a single 160-bit all-zeros value (the currency sentinel detected by `isXRP()`). +- **IOU**: 160-bit currency code followed by 160-bit issuer `AccountID`. +- **MPT**: 160-bit issuer `AccountID` (in the currency slot), 160-bit `noAccount()` sentinel, then a 32-bit sequence number. + +The `noAccount()` sentinel in the second 160-bit slot is the discriminator between IOU and MPT — a design choice that maintains backward compatibility with the original two-field layout while encoding the MPT case without a separate type prefix byte. Distinguishing IOU from MPT thus requires reading the second 160-bit word and checking against the reserved sentinel. + +For MPT, the assembly of `MPTID` from the wire bytes is subtle: `MPTID` is a 192-bit blob laid out as `[sequence (32 bits) | accountID (160 bits)]`, but the wire format transmits the issuer account in the first slot and the sequence last. Two `memcpy` calls reverse this order when populating the `mptID` buffer — sequence lands at offset 0, the raw account bytes at offset 4. + +After building an `Issue` from the deserialized currency and account fields, `isConsistent(issue)` validates that native currency (XRP) is not paired with a non-null issuer and vice versa. A mismatch throws `std::runtime_error`. No such validation occurs for MPT (the variant is inherently consistent by construction), and the static assertion `MPTID::size() == sizeof(sequence) + sizeof(currencyOrAccount)` guards the layout assumption at compile time. + +## Serialization: `add()` + +Serialization inverts deserialization using `asset_.visit()` with two lambdas dispatched over the variant. For `Issue`, XRP writes only the currency (the account is implicit); non-XRP IOUs write both currency and account — preserving the compact encoding XRP has had since the ledger's inception. For `MPTIssue`, the method writes `getIssuer()` first (into the currency slot), then `noAccount()` as the sentinel, then the 32-bit sequence extracted from the front of the `MPTID` blob via `memcpy`. This round-trips cleanly against the deserializing constructor. + +## `isDefault()` and Default Value Semantics + +`isDefault()` returns `true` only when the asset is XRP — matching the member initializer `asset_{xrpIssue()}`. MPT issues are never considered default. This convention means an `STIssue` field omitted from a ledger object is implicitly XRP, which aligns with the ledger's historical treatment of the native currency as the base case. + +## Validation Asymmetry + +There is a deliberate asymmetry in where consistency validation occurs. In the deserializing constructor, `isConsistent()` is called explicitly only for `Issue`-variant paths, because untrusted binary data from the network may carry malformed fields. By contrast, the `issueFromJson()` free function simply delegates to `assetFromJson()` and constructs without re-checking consistency — the assumption being that JSON input has already been validated upstream or is generated by trusted code. The template constructor in `STIssue.h` does perform the consistency check when constructing from a concrete `Issue` or `MPTIssue` at compile-resolved call sites, providing a safety net for in-process object construction. + +## `copy()` / `move()` and the `STVar` Pattern + +Both override the low-level placement-construction hooks used by `detail::STVar`, the type-erased storage mechanism inside `STObject`. They delegate to `emplace(n, buf, ...)`, which performs in-place construction inside a caller-supplied buffer. This allows `STObject` to store heterogeneous `STBase` subclasses contiguously without heap allocation per field. + +## Relationship to `Asset` and the Broader Type Hierarchy + +`STIssue` is deliberately thin: it holds one `Asset` and defers all type-specific behavior to it via `visit()`. `getText()` and `getJson()` simply forward to `asset_.getText()` and `asset_.setJson()`, so display logic is centralized in `Asset` and its constituents. Comparison operators (`==`, `<=>`) delegating to `asset_` give `STIssue` full ordering, making it usable in sorted containers. The `holds()` and `get()` accessors expose the variant's active type for callers that need to discriminate between `Issue` and `MPTIssue` without going through the `visit()` pattern. \ No newline at end of file diff --git a/src/libxrpl/protocol/STLedgerEntry.cpp.ai.json b/src/libxrpl/protocol/STLedgerEntry.cpp.ai.json new file mode 100644 index 0000000000..cd32c259ea --- /dev/null +++ b/src/libxrpl/protocol/STLedgerEntry.cpp.ai.json @@ -0,0 +1,554 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 17, + "name": "STLedgerEntry" + } + ], + "code_paths": [ + { + "call_chain": [ + "STLedgerEntry::STLedgerEntry(Keylet const& k)", + "LedgerFormats::getInstance().findByType(type_)", + "set(format->getSOTemplate())" + ], + "entry_point": "STLedgerEntry::STLedgerEntry(Keylet const& k)", + "purpose": "Constructs a STLedgerEntry from a Keylet, validates the type, and applies the template.", + "validation_points": [ + "LedgerFormats::getInstance().findByType(type_) (validates LedgerEntryType from Keylet.type)", + "set(format->getSOTemplate()) (template validation)" + ] + }, + { + "call_chain": [ + "STLedgerEntry::STLedgerEntry(SerialIter&, uint256 const&)", + "set(sit)", + "setSLEType()", + "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType)))", + "applyTemplate(format->getSOTemplate())" + ], + "entry_point": "STLedgerEntry::STLedgerEntry(SerialIter& sit, uint256 const& index)", + "purpose": "Constructs a STLedgerEntry from serialized data, sets fields, validates type, and applies template.", + "validation_points": [ + "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType))) (validates LedgerEntryType from serialized data)", + "applyTemplate(format->getSOTemplate()) (template validation)" + ] + }, + { + "call_chain": [ + "STLedgerEntry::STLedgerEntry(STObject const&, uint256 const&)", + "setSLEType()", + "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType)))", + "applyTemplate(format->getSOTemplate())" + ], + "entry_point": "STLedgerEntry::STLedgerEntry(STObject const& object, uint256 const& index)", + "purpose": "Constructs a STLedgerEntry from an STObject, validates type, and applies template.", + "validation_points": [ + "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType))) (validates LedgerEntryType from serialized data)", + "applyTemplate(format->getSOTemplate()) (template validation)" + ] + }, + { + "call_chain": [ + "setSLEType()", + "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType)))", + "applyTemplate(format->getSOTemplate())" + ], + "entry_point": "STLedgerEntry::setSLEType()", + "purpose": "Validates the ledger entry type from serialized data and applies the template.", + "validation_points": [ + "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType)))", + "applyTemplate(format->getSOTemplate())" + ] + }, + { + "call_chain": [ + "getFullText()", + "LedgerFormats::getInstance().findByType(type_)" + ], + "entry_point": "STLedgerEntry::getFullText()", + "purpose": "Returns a string representation of the ledger entry, validating the type.", + "validation_points": [ + "LedgerFormats::getInstance().findByType(type_) (validates LedgerEntryType from type_ member)" + ] + } + ], + "data_flows": [ + { + "field": "type_ (LedgerEntryType)", + "flow": [ + "Keylet.type or getFieldU16(sfLedgerEntryType)", + "LedgerFormats::getInstance().findByType(...)", + "type_ member variable", + "used in set(), applyTemplate(), getFullText(), etc." + ], + "origin": "Keylet.type (in Keylet constructor) or from serialized data (sfLedgerEntryType)", + "transformations": [ + "safe_cast if from serialized data", + "validated via LedgerFormats::findByType", + "assigned to type_" + ], + "validated_at": "LedgerFormats::getInstance().findByType(...)" + }, + { + "field": "key_ (uint256)", + "flow": [ + "Keylet.key or index", + "assigned to key_", + "used in getFullText(), getText(), getJson()" + ], + "origin": "Keylet.key, index parameter, or from serialized data", + "transformations": [ + "None (direct assignment)" + ], + "validated_at": "Not explicitly validated in this file" + }, + { + "field": "sfLedgerEntryType", + "flow": [ + "getFieldU16(sfLedgerEntryType) or setFieldU16(sfLedgerEntryType, ...)", + "safe_cast", + "LedgerFormats::getInstance().findByType(...)" + ], + "origin": "Serialized data or set in constructor", + "transformations": [ + "safe_cast to LedgerEntryType" + ], + "validated_at": "LedgerFormats::getInstance().findByType(...)" + }, + { + "field": "SOTemplate", + "flow": [ + "getSOTemplate()", + "set() or applyTemplate()" + ], + "origin": "LedgerFormats::getInstance().findByType(...)->getSOTemplate()", + "transformations": [ + "Template applied to STObject" + ], + "validated_at": "set() or applyTemplate()" + } + ], + "description": "Implements the STLedgerEntry class, representing a ledger entry in the XRPL protocol, including construction, serialization, JSON conversion, and threading logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLedgerEntryType (type field, checked against known types)", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLedgerEntryType (type field, checked against known types) validated by LedgerFormats/SOTemplate (template-based validation), Throw<> (exception-based error handling), jss:: (JSON field naming, not direct validation here)", + "issue_pattern": "Missing validation for sfLedgerEntryType (type field, checked against known types)", + "why_false_positive": "LedgerFormats/SOTemplate (template-based validation), Throw<> (exception-based error handling), jss:: (JSON field naming, not direct validation here) validates sfLedgerEntryType (type field, checked against known types) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "All fields required by SOTemplate for the given ledger entry type (enforced via set/applyTemplate)", + "validation", + "missing", + "check" + ], + "evidence": "Field All fields required by SOTemplate for the given ledger entry type (enforced via set/applyTemplate) validated by LedgerFormats/SOTemplate (template-based validation), Throw<> (exception-based error handling), jss:: (JSON field naming, not direct validation here)", + "issue_pattern": "Missing validation for All fields required by SOTemplate for the given ledger entry type (enforced via set/applyTemplate)", + "why_false_positive": "LedgerFormats/SOTemplate (template-based validation), Throw<> (exception-based error handling), jss:: (JSON field naming, not direct validation here) validates All fields required by SOTemplate for the given ledger entry type (enforced via set/applyTemplate) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "LedgerEntryType (from Keylet.type)", + "empty", + "string", + "validation" + ], + "evidence": "LedgerFormats::getInstance().findByType(type_) at STLedgerEntry::STLedgerEntry(Keylet const& k) constructor", + "issue_pattern": "Missing empty string validation for LedgerEntryType (from Keylet.type)", + "why_false_positive": "LedgerFormats::getInstance().findByType(type_) validates LedgerEntryType (from Keylet.type) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "LedgerEntryType (from Keylet.type)", + "type", + "validation", + "check" + ], + "evidence": "LedgerFormats::getInstance().findByType(type_) at STLedgerEntry::STLedgerEntry(Keylet const& k) constructor", + "issue_pattern": "Missing type validation for LedgerEntryType (from Keylet.type)", + "why_false_positive": "LedgerFormats::getInstance().findByType(type_) validates LedgerEntryType (from Keylet.type) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "LedgerEntryType (from serialized data)", + "empty", + "string", + "validation" + ], + "evidence": "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType))) at STLedgerEntry::setSLEType()", + "issue_pattern": "Missing empty string validation for LedgerEntryType (from serialized data)", + "why_false_positive": "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType))) validates LedgerEntryType (from serialized data) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "LedgerEntryType (from serialized data)", + "type", + "validation", + "check" + ], + "evidence": "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType))) at STLedgerEntry::setSLEType()", + "issue_pattern": "Missing type validation for LedgerEntryType (from serialized data)", + "why_false_positive": "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType))) validates LedgerEntryType (from serialized data) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "LedgerEntryType (from type_ member)", + "empty", + "string", + "validation" + ], + "evidence": "LedgerFormats::getInstance().findByType(type_) at STLedgerEntry::getFullText()", + "issue_pattern": "Missing empty string validation for LedgerEntryType (from type_ member)", + "why_false_positive": "LedgerFormats::getInstance().findByType(type_) validates LedgerEntryType (from type_ member) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "LedgerEntryType (from type_ member)", + "type", + "validation", + "check" + ], + "evidence": "LedgerFormats::getInstance().findByType(type_) at STLedgerEntry::getFullText()", + "issue_pattern": "Missing type validation for LedgerEntryType (from type_ member)", + "why_false_positive": "LedgerFormats::getInstance().findByType(type_) validates LedgerEntryType (from type_ member) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "LedgerEntryType (from Keylet.type)", + "empty", + "string", + "validation" + ], + "evidence": "set(format->getSOTemplate()) at STLedgerEntry::STLedgerEntry(Keylet const& k) constructor", + "issue_pattern": "Missing empty string validation for LedgerEntryType (from Keylet.type)", + "why_false_positive": "set(format->getSOTemplate()) validates LedgerEntryType (from Keylet.type) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "LedgerEntryType (from serialized data)", + "empty", + "string", + "validation" + ], + "evidence": "applyTemplate(format->getSOTemplate()) at STLedgerEntry::setSLEType()", + "issue_pattern": "Missing empty string validation for LedgerEntryType (from serialized data)", + "why_false_positive": "applyTemplate(format->getSOTemplate()) validates LedgerEntryType (from serialized data) for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STLedgerEntry.cpp", + "functions": [ + { + "args": [ + "Keylet const& k" + ], + "lineno": 19, + "name": "STLedgerEntry" + }, + { + "args": [ + "SerialIter& sit", + "uint256 const& index" + ], + "lineno": 29, + "name": "STLedgerEntry" + }, + { + "args": [ + "STObject const& object", + "uint256 const& index" + ], + "lineno": 34, + "name": "STLedgerEntry" + }, + { + "args": [], + "lineno": 39, + "name": "setSLEType" + }, + { + "args": [], + "lineno": 51, + "name": "getFullText" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 65, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 70, + "name": "move" + }, + { + "args": [], + "lineno": 75, + "name": "getSType" + }, + { + "args": [], + "lineno": 80, + "name": "getText" + }, + { + "args": [ + "JsonOptions options" + ], + "lineno": 85, + "name": "getJson" + }, + { + "args": [ + "Rules const& rules" + ], + "lineno": 97, + "name": "isThreadedType" + }, + { + "args": [ + "uint256 const& txID", + "std::uint32_t ledgerSeq", + "uint256& prevTxID", + "std::uint32_t& prevLedgerID" + ], + "lineno": 109, + "name": "thread" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic for LedgerEntryType and template application is critical and likely covered by unit tests in the rippled codebase, especially in files such as 'test/ledger/...' (e.g., LedgerEntry_test.cpp, LedgerFormats_test.cpp, STLedgerEntry_test.cpp). However, direct negative tests for invalid LedgerEntryType (e.g., unknown types, corrupted serialized data) may not be fully covered. Exception paths (Throw) should be explicitly tested for robustness, but coverage may be incomplete for all error branches. Template validation (set/applyTemplate) is likely tested indirectly via object construction and serialization/deserialization tests, but edge cases (e.g., missing fields, malformed templates) may not be exhaustively tested.", + "validation_architecture": { + "auto_validated_fields": [ + "sfLedgerEntryType (type field, checked against known types)", + "All fields required by SOTemplate for the given ledger entry type (enforced via set/applyTemplate)" + ], + "framework": "LedgerFormats/SOTemplate (template-based validation), Throw<> (exception-based error handling), jss:: (JSON field naming, not direct validation here)", + "validation_layer": "business_logic (constructor and type-setting functions)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "LedgerEntryType (from Keylet.type)", + "location": "STLedgerEntry::STLedgerEntry(Keylet const& k) constructor", + "validated_by": "LedgerFormats::getInstance().findByType(type_)", + "validates": [ + "Checks that the ledger entry type provided by Keylet is known/registered", + "Prevents creation of SLE with unknown type" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "LedgerEntryType (from serialized data)", + "location": "STLedgerEntry::setSLEType()", + "validated_by": "LedgerFormats::getInstance().findByType(safe_cast(getFieldU16(sfLedgerEntryType)))", + "validates": [ + "Checks that the ledger entry type field in serialized data is known/registered", + "Prevents instantiation of SLE with invalid type from serialized input" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "LedgerEntryType (from type_ member)", + "location": "STLedgerEntry::getFullText()", + "validated_by": "LedgerFormats::getInstance().findByType(type_)", + "validates": [ + "Checks that the current type_ is valid before generating full text representation" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "May throw (not explicit in this snippet, but set() can throw if template is invalid)", + "field": "LedgerEntryType (from Keylet.type)", + "location": "STLedgerEntry::STLedgerEntry(Keylet const& k) constructor", + "validated_by": "set(format->getSOTemplate())", + "validates": [ + "Applies the SOTemplate for the given ledger entry type, which may enforce required fields and structure" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "May throw (not explicit in this snippet, but applyTemplate() can throw if template is invalid)", + "field": "LedgerEntryType (from serialized data)", + "location": "STLedgerEntry::setSLEType()", + "validated_by": "applyTemplate(format->getSOTemplate())", + "validates": [ + "Applies the SOTemplate for the given ledger entry type, enforcing required fields and structure" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STLedgerEntry.cpp.ai.md b/src/libxrpl/protocol/STLedgerEntry.cpp.ai.md new file mode 100644 index 0000000000..845a2588ea --- /dev/null +++ b/src/libxrpl/protocol/STLedgerEntry.cpp.ai.md @@ -0,0 +1,45 @@ +# `STLedgerEntry.cpp` — Typed Ledger Entry Implementation + +`STLedgerEntry` is the concrete, typed representation of a single object in the XRPL ledger state. The ledger itself is a key-value store (backed by a `SHAMap`) where every entry is keyed by a `uint256` hash and has a declared type. This file implements the lifecycle of those entries: creation, deserialization, serialization-to-JSON, and the threading mechanism that links each modified entry to the transaction that last changed it. + +## Inheritance and Identity + +`STLedgerEntry` inherits from `STObject`, the general-purpose serialized field container. The subclass adds two private members: `key_` (the `uint256` SHAMap key, also called the "index") and `type_` (the `LedgerEntryType` enum value). Every function in this file either establishes, validates, or exposes those two pieces of typed identity on top of what `STObject` already provides. + +The alias `using SLE = STLedgerEntry` is defined in the header, and `SLE::pointer` / `SLE::ref` aliases to `shared_ptr` variants are the standard way the rest of the codebase holds and passes ledger entries. + +## Three Paths to Construction + +The class has three constructors, each representing a distinct usage scenario. + +**Creating a new entry** — `STLedgerEntry(Keylet const& k)` builds an empty, properly initialized object. A `Keylet` bundles a `LedgerEntryType` with its deterministically computed `uint256` key. The constructor looks up the format in the singleton `LedgerFormats` registry and throws `std::runtime_error` if the type is unknown, making it impossible to create an entry of an unrecognized type. It then calls `set(format->getSOTemplate())`, which populates the `STObject` with all fields declared in the format's `SOTemplate` at their default values and marks their optionality/requirement. Finally it writes `sfLedgerEntryType` into the object so the wire encoding is self-describing. + +**Deserializing from wire** — `STLedgerEntry(SerialIter&, uint256 const& index)` first calls `STObject::set(sit)` to consume raw bytes and populate all fields, then calls the private `setSLEType()`. At deserialization time the type is embedded as the `sfLedgerEntryType` field inside the byte stream itself, so the constructor cannot know the type until after the fields are read. `setSLEType()` reads that field back out, looks it up in `LedgerFormats`, and calls `applyTemplate()` — the post-hoc counterpart to `set()`: rather than initializing an empty object, it validates and conforms an already-populated one to the required template, throwing if required fields are missing or the type is unrecognized. + +**Wrapping an `STObject`** — `STLedgerEntry(STObject const& object, uint256 const& index)` handles the case where fields have already been parsed into a generic `STObject` and just need to be re-interpreted as a typed ledger entry. It delegates to `setSLEType()` for the same validation path as the deserialization constructor. + +The split between `set()` (used when creating fresh) and `applyTemplate()` (used when validating deserialized data) reflects a real semantic difference: one is initialization, the other is conformance checking. + +## Representation Methods + +`getText()` produces a compact diagnostic string with the hex key and field contents via Boost.Format. + +`getFullText()` is the heavier debug representation: it re-validates the format (throwing on corruption) to obtain the human-readable type name and includes it in the output alongside the key and all field values. The redundant format lookup in `getFullText()` is a deliberate defensive check — by the time this method is called, `type_` should always be valid, but the lookup enforces that invariant before emitting output. + +`getJson()` is the most interesting representation method. It delegates to `STObject::getJson()`, then injects `jss::index` (the hex-encoded SHAMap key) since the key is stored in `key_` rather than in any serialized field. There is one special case: for `ltMPTOKEN_ISSUANCE` objects, `getJson()` also computes and injects `mpt_issuance_id` by calling `makeMptID(sfSequence, sfIssuer)`. This derived identifier is not stored as a field in the ledger object — it is recomputed on read. The design avoids redundancy in consensus-critical storage and ensures the derived ID is always consistent with the fields that define it. + +## Transaction Threading + +The ledger maintains an audit trail by threading each ledger entry through the transactions that modified it. The mechanism uses two fields, `sfPreviousTxnID` and `sfPreviousTxnLgrSeq`, which are updated every time a transaction touches an entry. + +`isThreadedType(Rules const& rules)` gates which objects participate in threading. Not all object types have always carried those fields. Five types — `ltDIR_NODE`, `ltAMENDMENTS`, `ltFEE_SETTINGS`, `ltNEGATIVE_UNL`, and `ltAMM` — only gained `PreviousTxnID` support when the `fixPreviousTxnID` amendment activated. Before that amendment, `isThreadedType()` returns `false` for those types even if the field is declared in the template. This guard prevents premature threading on objects that historical validator code would not expect to carry those fields. The function performs a linear scan of a five-element `constexpr` array, an acceptable cost given this is called during transaction application, not in a hot inner loop. + +`thread()` performs the actual update: it reads the current `sfPreviousTxnID`, checks whether the same transaction has already threaded this entry (returning `false` with an assertion if so, guarding against double-application), captures the old values into the output parameters, and writes the new transaction ID and ledger sequence. The output parameters allow callers to reconstruct the modification chain — each transaction records what the previous transaction was, forming a linked list through ledger history. + +## Copy and Move Semantics + +`copy()` and `move()` delegate to the `emplace()` helper from `STBase`. These methods support placement-new construction into caller-provided buffers, which is used by `detail::STVar` — a small-buffer variant that avoids heap allocation for serialized type objects. `STVar` is friended in the header, confirming this is an internal protocol-layer mechanism rather than a general-purpose interface. + +## Validation Architecture + +All three constructors fail loudly via `Throw` if the type is unrecognized. There is no silent fallback to a generic representation: an `STLedgerEntry` with an unknown type cannot be constructed. This is correct for a consensus system where permitting unrecognized object types to propagate would create non-deterministic behavior across validators. Template conformance via `applyTemplate()` enforces the same guarantee for deserialized data, ensuring that on-ledger objects cannot have field sets that diverge from their declared format. \ No newline at end of file diff --git a/src/libxrpl/protocol/STNumber.cpp.ai.json b/src/libxrpl/protocol/STNumber.cpp.ai.json new file mode 100644 index 0000000000..6fab4cc0f1 --- /dev/null +++ b/src/libxrpl/protocol/STNumber.cpp.ai.json @@ -0,0 +1,434 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "SField const& field, Number const& value", + "SerialIter& sit, SField const& field" + ], + "lineno": 18, + "name": "STNumber" + } + ], + "code_paths": [ + { + "call_chain": [ + "STNumber::associateAsset", + "STTakesAsset::associateAsset", + "XRPL_ASSERT_PARTS", + "roundToAsset" + ], + "entry_point": "STNumber::associateAsset", + "purpose": "Associates an Asset with the STNumber and ensures the field requires an asset, then rounds the value to the asset's precision.", + "validation_points": [ + "XRPL_ASSERT_PARTS(getFName().shouldMeta(SField::sMD_NeedsAsset), ...)" + ] + }, + { + "call_chain": [ + "STNumber::add", + "XRPL_ASSERT(getFName().isBinary(), ...)", + "XRPL_ASSERT(getFName().fieldType == getSType(), ...)", + "if (field.shouldMeta(SField::sMD_NeedsAsset)) { ... }", + "if (asset_) { roundToAsset(*asset_, value); XRPL_ASSERT_PARTS(value_ == value, ...) }", + "#if !NDEBUG XRPL_ASSERT_PARTS(Number::getMantissaScale() == MantissaRange::large, ...) #endif", + "XRPL_ASSERT_PARTS(mantissa in valid range, ...)", + "s.add64(mantissa)", + "s.add32(exponent)" + ], + "entry_point": "STNumber::add", + "purpose": "Serializes the STNumber, validating field type, binary status, asset requirements, rounding, and mantissa range.", + "validation_points": [ + "XRPL_ASSERT(getFName().isBinary(), ...)", + "XRPL_ASSERT(getFName().fieldType == getSType(), ...)", + "XRPL_ASSERT_PARTS(value_ == value, ...)", + "XRPL_ASSERT_PARTS(Number::getMantissaScale() == MantissaRange::large, ...)", + "XRPL_ASSERT_PARTS(mantissa in valid range, ...)" + ] + }, + { + "call_chain": [ + "STNumber::isEquivalent", + "XRPL_ASSERT(t.getSType() == this->getSType(), ...)", + "dynamic_cast(t)", + "value_ == v" + ], + "entry_point": "STNumber::isEquivalent", + "purpose": "Checks if another STBase is equivalent to this STNumber, validating type match.", + "validation_points": [ + "XRPL_ASSERT(t.getSType() == this->getSType(), ...)" + ] + } + ], + "data_flows": [ + { + "field": "value_", + "flow": [ + "constructor (value_ = value or value_ = Number{mantissa, exponent})", + "possibly transformed in associateAsset (roundToAsset)", + "used in add (copied to local 'value', possibly rounded again)", + "serialized via s.add64(mantissa), s.add32(exponent)" + ], + "origin": "STNumber constructor (from Number or deserialized from SerialIter)", + "transformations": [ + "May be rounded to asset precision in associateAsset or add", + "Compared for equality in add and isEquivalent" + ], + "validated_at": "add (XRPL_ASSERT_PARTS(value_ == value, ...)), isEquivalent (XRPL_ASSERT)" + }, + { + "field": "getFName()", + "flow": [ + "constructor", + "used in associateAsset (shouldMeta)", + "used in add (isBinary, fieldType, shouldMeta)" + ], + "origin": "STTakesAsset base class (field passed to constructor)", + "transformations": [ + "Checked for metadata flags, binary status, and type" + ], + "validated_at": "associateAsset (XRPL_ASSERT_PARTS), add (XRPL_ASSERT)" + }, + { + "field": "asset_", + "flow": [ + "set in associateAsset", + "checked in add (if asset_)", + "used in roundToAsset" + ], + "origin": "STTakesAsset base class, set via associateAsset", + "transformations": [ + "Used to round value_" + ], + "validated_at": "add (XRPL_ASSERT_PARTS(value_ == value, ...))" + }, + { + "field": "mantissa", + "flow": [ + "extracted in add", + "validated for range", + "serialized" + ], + "origin": "value_.mantissa()", + "transformations": [ + "None (just checked and serialized)" + ], + "validated_at": "add (XRPL_ASSERT_PARTS(mantissa in valid range, ...))" + } + ], + "description": "Implements the STNumber class for handling serialized numbers in the XRPL protocol, including parsing, serialization, and JSON conversion.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "SField::sMD_NeedsAsset", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT_PARTS at STNumber::associateAsset", + "issue_pattern": "Missing empty string validation for SField::sMD_NeedsAsset", + "why_false_positive": "XRPL_ASSERT_PARTS validates SField::sMD_NeedsAsset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "getFName().isBinary()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at STNumber::add", + "issue_pattern": "Missing empty string validation for getFName().isBinary()", + "why_false_positive": "XRPL_ASSERT validates getFName().isBinary() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "getFName().fieldType == getSType()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at STNumber::add", + "issue_pattern": "Missing empty string validation for getFName().fieldType == getSType()", + "why_false_positive": "XRPL_ASSERT validates getFName().fieldType == getSType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "getFName().fieldType == getSType()", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT at STNumber::add", + "issue_pattern": "Missing type validation for getFName().fieldType == getSType()", + "why_false_positive": "XRPL_ASSERT validates getFName().fieldType == getSType() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "value_ == value", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT_PARTS at STNumber::add", + "issue_pattern": "Missing empty string validation for value_ == value", + "why_false_positive": "XRPL_ASSERT_PARTS validates value_ == value for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Number::getMantissaScale() == MantissaRange::large", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT_PARTS at STNumber::add (inside #if !NDEBUG)", + "issue_pattern": "Missing empty string validation for Number::getMantissaScale() == MantissaRange::large", + "why_false_positive": "XRPL_ASSERT_PARTS validates Number::getMantissaScale() == MantissaRange::large for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mantissa", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT_PARTS at STNumber::add", + "issue_pattern": "Missing empty string validation for mantissa", + "why_false_positive": "XRPL_ASSERT_PARTS validates mantissa for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "mantissa", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT_PARTS at STNumber::add", + "issue_pattern": "Missing range validation for mantissa", + "why_false_positive": "XRPL_ASSERT_PARTS validates mantissa range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STNumber.cpp", + "functions": [ + { + "args": [], + "lineno": 27, + "name": "STNumber::getSType" + }, + { + "args": [], + "lineno": 32, + "name": "STNumber::getText" + }, + { + "args": [ + "a" + ], + "lineno": 37, + "name": "STNumber::associateAsset" + }, + { + "args": [ + "s" + ], + "lineno": 52, + "name": "STNumber::add" + }, + { + "args": [], + "lineno": 91, + "name": "STNumber::value" + }, + { + "args": [ + "v" + ], + "lineno": 96, + "name": "STNumber::setValue" + }, + { + "args": [ + "n", + "buf" + ], + "lineno": 101, + "name": "STNumber::copy" + }, + { + "args": [ + "n", + "buf" + ], + "lineno": 106, + "name": "STNumber::move" + }, + { + "args": [ + "t" + ], + "lineno": 111, + "name": "STNumber::isEquivalent" + }, + { + "args": [], + "lineno": 119, + "name": "STNumber::isDefault" + }, + { + "args": [ + "out", + "rhs" + ], + "lineno": 124, + "name": "operator<<" + }, + { + "args": [ + "number" + ], + "lineno": 129, + "name": "partsFromString" + }, + { + "args": [ + "field", + "value" + ], + "lineno": 181, + "name": "numberFromJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by unit tests for serialization/deserialization, asset association, and equivalence checks. Tests should exist in files like test/protocol/STNumber_test.cpp or test/protocol/Serializer_test.cpp. However, edge cases such as missing asset when required, mantissa out of range, or type mismatches may not be fully covered. The debug-only assertion for MantissaRange::large may not be tested in release builds. There is no explicit test coverage for exception paths or assertion failures.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT, XRPL_ASSERT_PARTS (custom assertion macros)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT_PARTS)", + "field": "SField::sMD_NeedsAsset", + "location": "STNumber::associateAsset", + "validated_by": "XRPL_ASSERT_PARTS", + "validates": [ + "Checks that the field requires an asset (shouldMeta(SField::sMD_NeedsAsset)) before associating asset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "getFName().isBinary()", + "location": "STNumber::add", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that the field is binary before serialization" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "getFName().fieldType == getSType()", + "location": "STNumber::add", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that the field type matches the serialized type" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT_PARTS)", + "field": "value_ == value", + "location": "STNumber::add", + "validated_by": "XRPL_ASSERT_PARTS", + "validates": [ + "Checks that the value is already rounded to the asset's precision" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "AssertionError (via XRPL_ASSERT_PARTS)", + "field": "Number::getMantissaScale() == MantissaRange::large", + "location": "STNumber::add (inside #if !NDEBUG)", + "validated_by": "XRPL_ASSERT_PARTS", + "validates": [ + "Checks that STNumber is only used with large mantissa scale when asset is not assigned" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT_PARTS)", + "field": "mantissa", + "location": "STNumber::add", + "validated_by": "XRPL_ASSERT_PARTS", + "validates": [ + "Checks that mantissa is within std::int64_t range" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STNumber.cpp.ai.md b/src/libxrpl/protocol/STNumber.cpp.ai.md new file mode 100644 index 0000000000..c0f5b4248b --- /dev/null +++ b/src/libxrpl/protocol/STNumber.cpp.ai.md @@ -0,0 +1,50 @@ +# `STNumber.cpp` — Serializable Precision Number for XRPL Fields + +## Role in the System + +`STNumber` fills a gap in the XRPL serialization type hierarchy. The existing `STAmount` bundles a numeric value together with its `Asset` (currency/issuer or MPT ID), which means every ledger field that stores an amount must also redundantly store asset information. For ledger objects like Vault, LoanBroker, and Loan — where many numeric fields all refer to the same vault asset — that duplication is wasteful. `STNumber` solves this by storing only the numeric value in a `Number` (mantissa + exponent) form, deferring asset binding to runtime. The comment in the header is precise: it is "effectively an `STAmount` sans `Asset`." + +The class is part of `libxrpl`'s protocol layer and participates in the same serialization framework as all other `STBase`-derived types, with type tag `STI_NUMBER`. + +## Class Hierarchy and the `STTakesAsset` Mixin + +Rather than inheriting directly from `STBase`, `STNumber` inherits through `STTakesAsset`, an intermediate mixin that adds an `std::optional asset_` member. This design separates concerns cleanly: `STTakesAsset` knows how to store an asset association without caring what the derived class does with it, while `STNumber` overrides `associateAsset()` to trigger precision rounding. The `asset_` field is explicitly runtime-only — it is never serialized to the ledger. + +## Serialization Wire Format and Sequencing + +On the wire, an `STNumber` is 12 bytes: a 64-bit signed mantissa followed by a 32-bit exponent, written by `add()` via `s.add64()` + `s.add32()`. Deserialization in the `SerialIter` constructor reverses this exactly: + +```cpp +auto mantissa = sit.geti64(); +auto exponent = sit.geti32(); +value_ = Number{mantissa, exponent}; +``` + +The comment — "We must call these methods in separate statements to guarantee their order of execution" — guards against the classic C++ evaluation order pitfall. Within a single function-call argument list, the order of argument evaluation is unspecified; forcing the calls into separate `auto` statements makes sequencing well-defined. + +## Asset Association and the Two-Phase Rounding Contract + +The precision of a `Number` depends on the asset it represents: XRP and MPT values must fit within integer constraints, while IOU values carry 15 significant decimal digits. The `roundToAsset()` call in `associateAsset()` performs this alignment by constructing a temporary `STAmount` from the `Asset` and `Number` pair, which drives the rounding through `STAmount`'s normalization logic. + +This creates a two-phase contract enforced by assertions in `add()`: + +1. **Phase 1 (`associateAsset`)**: the value is rounded to the asset's precision and stored back into `value_`. +2. **Phase 2 (`add`)**: if an `asset_` is present, `roundToAsset()` is called again on a local copy and compared against `value_` via `XRPL_ASSERT_PARTS`. The assertion verifies idempotency — that the stored value was already rounded. Any mismatch indicates that `setValue()` was called after `associateAsset()` without re-associating, a programming error that would produce incorrect ledger state. + +When `asset_` is absent at serialization time, the code relaxes into a debug-only check that the global `MantissaRange` is set to `large`. The "large" scale (mantissa in `[10^18, 10^19 - 1]`, amendment-gated by SingleAssetVault / LendingProtocol) is required for correctly representing XRP and MPT integer values that exceed the 15-digit "small" IOU range. Serializing an `STNumber` without an asset and without large-scale mode active would silently truncate precision, so this guard catches configuration errors in debug builds. + +## Mantissa Range Check at Serialization + +Before writing bytes, `add()` asserts that the `int64_t` mantissa extracted from `Number::mantissa()` falls within `[INT64_MIN, INT64_MAX]`. This appears redundant — `Number::mantissa()` already returns `std::int64_t` — but it is necessary because the internal representation uses an *unsigned* 64-bit mantissa extended to 19 digits when `MantissaRange::large` is active. The external `mantissa()` accessor divides by 10 when the internal value exceeds the signed 63-bit maximum, so the assertion documents and enforces the precondition that the wire-format representation must always fit in a signed 64-bit field. + +## JSON Parsing + +`partsFromString()` uses a compiled `boost::regex` (flagged `optimize`) to parse decimal notation including optional sign, integer part, fractional part, and exponent. The regex is `static` to pay the compile cost once. Fractions are absorbed into the mantissa by concatenating the integer and fractional digit strings, then setting the exponent to the negative of the fractional digit count before applying any explicit exponent. + +`numberFromJson()` dispatches on the JSON value type: integer JSON values are handled directly by reading `asInt()` / `asUInt()`, while string values go through `partsFromString()`. A notable guard in the string path asserts `!getCurrentTransactionRules()`, meaning string-based JSON parsing of `STNumber` fields is only permitted outside of transaction processing. During transaction processing, fields should arrive pre-parsed or as numeric JSON; accepting string-formatted numbers in that context would allow user-supplied text to drive parsing inside a transactor, which the XRPL protocol intentionally prevents. + +## Equivalence and Default State + +`isEquivalent()` uses `dynamic_cast` against the already-verified same-type target, then delegates to `Number::operator==`. `isDefault()` returns `true` for a default-constructed `Number()`, which represents zero in `Number`'s internal encoding. Both methods satisfy the virtual contract from `STBase` required by the serialization framework's field diffing and canonical-form checks. + +The `copy()` and `move()` overrides call the `emplace()` helper from `STBase`, enabling placement-new into pre-allocated buffers — a performance pattern throughout the ST-type family that avoids heap allocation for short-lived serialized field copies. \ No newline at end of file diff --git a/src/libxrpl/protocol/STObject.cpp.ai.json b/src/libxrpl/protocol/STObject.cpp.ai.json new file mode 100644 index 0000000000..562c71aebb --- /dev/null +++ b/src/libxrpl/protocol/STObject.cpp.ai.json @@ -0,0 +1,975 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 32, + "name": "STObject" + } + ], + "code_paths": [ + { + "call_chain": [ + "STObject(SerialIter&, SField const&, int)", + "set(sit, depth)", + "STObject(SerialIter&, SField const&, int) (recursive for nested objects)" + ], + "entry_point": "STObject(SerialIter& sit, SField const& name, int depth)", + "purpose": "Deserializes an STObject from a serialized stream, handling nested objects.", + "validation_points": [ + "STObject(SerialIter&, SField const&, int): if (depth > 10) Throw<...> (nesting depth validation)" + ] + }, + { + "call_chain": [ + "STObject(SOTemplate const&, SerialIter&, SField const&, int)", + "set(sit)", + "applyTemplate(type)" + ], + "entry_point": "STObject(SOTemplate const& type, SerialIter& sit, SField const& name)", + "purpose": "Constructs an STObject from a template and serialized data, ensuring template conformance.", + "validation_points": [ + "applyTemplate(type): Validates that the object matches the template (throws on mismatch)" + ] + }, + { + "call_chain": [ + "makeInnerObject(name)", + "getCurrentTransactionRules()", + "InnerObjectFormats::getInstance().findSOTemplateBySField(name)", + "set(*elements)" + ], + "entry_point": "STObject::makeInnerObject(SField const& name)", + "purpose": "Creates an inner STObject, applying the correct template based on business rules and feature flags.", + "validation_points": [ + "Business logic: Rules and feature flags determine if/which template is applied" + ] + } + ], + "data_flows": [ + { + "field": "depth", + "flow": [ + "STObject(SerialIter&, SField const&, int)", + "if (depth > 10) Throw<...>", + "set(sit, depth)", + "Potential recursive call for nested objects" + ], + "origin": "Passed as argument to STObject(SerialIter&, SField const&, int)", + "transformations": [ + "Incremented for each nested object" + ], + "validated_at": "STObject(SerialIter&, SField const&, int): if (depth > 10)" + }, + { + "field": "type (SOTemplate)", + "flow": [ + "STObject(SOTemplate const&, ...)", + "set(type)", + "applyTemplate(type)" + ], + "origin": "Passed to STObject(SOTemplate const&, ...)", + "transformations": [ + "Used to initialize v_ with default/nonPresent objects", + "Used to validate conformance in applyTemplate" + ], + "validated_at": "applyTemplate(type)" + }, + { + "field": "name (SField)", + "flow": [ + "STObject or makeInnerObject", + "Used to look up SOTemplate via InnerObjectFormats", + "Used in Rules logic to determine template applicability" + ], + "origin": "Passed to STObject constructors and makeInnerObject", + "transformations": [ + "Determines which template (if any) is applied" + ], + "validated_at": "makeInnerObject: Business logic with Rules and InnerObjectFormats" + }, + { + "field": "v_ (vector of fields)", + "flow": [ + "set(type): fills v_ with default/nonPresent objects", + "set(sit): fills v_ from serialized data", + "applyTemplate(type): checks v_ against template" + ], + "origin": "Initialized in set(type) or set(sit)", + "transformations": [ + "Populated from template or serialized data", + "Validated for template conformance" + ], + "validated_at": "applyTemplate(type)" + } + ], + "description": "Implements the STObject class for the XRPL protocol, providing serialization, deserialization, field access, and manipulation for structured objects in the XRP Ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "nesting depth", + "validation", + "missing", + "check" + ], + "evidence": "Field nesting depth validated by Custom validation via SOTemplate, Rules, and explicit checks", + "issue_pattern": "Missing validation for nesting depth", + "why_false_positive": "Custom validation via SOTemplate, Rules, and explicit checks validates nesting depth automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "inner object template applicability", + "validation", + "missing", + "check" + ], + "evidence": "Field inner object template applicability validated by Custom validation via SOTemplate, Rules, and explicit checks", + "issue_pattern": "Missing validation for inner object template applicability", + "why_false_positive": "Custom validation via SOTemplate, Rules, and explicit checks validates inner object template applicability automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "template conformance", + "validation", + "missing", + "check" + ], + "evidence": "Field template conformance validated by Custom validation via SOTemplate, Rules, and explicit checks", + "issue_pattern": "Missing validation for template conformance", + "why_false_positive": "Custom validation via SOTemplate, Rules, and explicit checks validates template conformance automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "nesting depth", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (depth > 10)) at STObject(SerialIter& sit, SField const& name, int depth) constructor", + "issue_pattern": "Missing empty string validation for nesting depth", + "why_false_positive": "explicit check (if (depth > 10)) validates nesting depth for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "nesting depth", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (if (depth > 10)) at STObject(SerialIter& sit, SField const& name, int depth) constructor", + "issue_pattern": "Missing range validation for nesting depth", + "why_false_positive": "explicit check (if (depth > 10)) validates nesting depth range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "inner object template applicability", + "empty", + "string", + "validation" + ], + "evidence": "business logic (Rules, SOTemplate, InnerObjectFormats) at STObject::makeInnerObject", + "issue_pattern": "Missing empty string validation for inner object template applicability", + "why_false_positive": "business logic (Rules, SOTemplate, InnerObjectFormats) validates inner object template applicability for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "template conformance", + "empty", + "string", + "validation" + ], + "evidence": "applyTemplate(type) at STObject(SOTemplate const& type, SerialIter& sit, SField const& name) constructor", + "issue_pattern": "Missing empty string validation for template conformance", + "why_false_positive": "applyTemplate(type) validates template conformance for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STObject.cpp", + "functions": [ + { + "args": [ + "STObject&& other" + ], + "lineno": 33, + "name": "STObject" + }, + { + "args": [ + "SField const& name" + ], + "lineno": 38, + "name": "STObject" + }, + { + "args": [ + "SOTemplate const& type", + "SField const& name" + ], + "lineno": 42, + "name": "STObject" + }, + { + "args": [ + "SOTemplate const& type", + "SerialIter& sit", + "SField const& name" + ], + "lineno": 46, + "name": "STObject" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name", + "int depth" + ], + "lineno": 51, + "name": "STObject" + }, + { + "args": [ + "SField const& name" + ], + "lineno": 58, + "name": "makeInnerObject" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 80, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 85, + "name": "move" + }, + { + "args": [], + "lineno": 90, + "name": "getSType" + }, + { + "args": [], + "lineno": 95, + "name": "isDefault" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 100, + "name": "add" + }, + { + "args": [ + "STObject&& other" + ], + "lineno": 105, + "name": "operator=" + }, + { + "args": [ + "SOTemplate const& type" + ], + "lineno": 112, + "name": "set" + }, + { + "args": [ + "SOTemplate const& type" + ], + "lineno": 124, + "name": "applyTemplate" + }, + { + "args": [ + "SField const& sField" + ], + "lineno": 163, + "name": "applyTemplateFromSField" + }, + { + "args": [ + "SerialIter& sit", + "int depth" + ], + "lineno": 170, + "name": "set" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 217, + "name": "hasMatchingEntry" + }, + { + "args": [], + "lineno": 225, + "name": "getFullText" + }, + { + "args": [], + "lineno": 247, + "name": "getText" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 262, + "name": "isEquivalent" + }, + { + "args": [ + "HashPrefix prefix" + ], + "lineno": 285, + "name": "getHash" + }, + { + "args": [ + "HashPrefix prefix" + ], + "lineno": 292, + "name": "getSigningHash" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 299, + "name": "getFieldIndex" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 313, + "name": "peekAtField" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 321, + "name": "getField" + }, + { + "args": [ + "int index" + ], + "lineno": 329, + "name": "getFieldSType" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 334, + "name": "peekAtPField" + }, + { + "args": [ + "SField const& field", + "bool createOkay" + ], + "lineno": 341, + "name": "getPField" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 353, + "name": "isFieldPresent" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 361, + "name": "peekFieldObject" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 366, + "name": "peekFieldArray" + }, + { + "args": [ + "std::uint32_t f" + ], + "lineno": 371, + "name": "setFlag" + }, + { + "args": [ + "std::uint32_t f" + ], + "lineno": 380, + "name": "clearFlag" + }, + { + "args": [ + "std::uint32_t f" + ], + "lineno": 389, + "name": "isFlag" + }, + { + "args": [], + "lineno": 394, + "name": "getFlags" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 403, + "name": "makeFieldPresent" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 419, + "name": "makeFieldAbsent" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 429, + "name": "delField" + }, + { + "args": [ + "int index" + ], + "lineno": 436, + "name": "delField" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 440, + "name": "getStyle" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 445, + "name": "getFieldU8" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 450, + "name": "getFieldU16" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 455, + "name": "getFieldU32" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 460, + "name": "getFieldU64" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 465, + "name": "getFieldH128" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 470, + "name": "getFieldH160" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 475, + "name": "getFieldH192" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 480, + "name": "getFieldH256" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 485, + "name": "getFieldI32" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 490, + "name": "getAccountID" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 495, + "name": "getFieldVL" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 502, + "name": "getFieldAmount" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 507, + "name": "getFieldPathSet" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 512, + "name": "getFieldV256" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 517, + "name": "getFieldObject" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 525, + "name": "getFieldArray" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 530, + "name": "getFieldCurrency" + }, + { + "args": [ + "SField const& field" + ], + "lineno": 535, + "name": "getFieldNumber" + }, + { + "args": [ + "std::unique_ptr v" + ], + "lineno": 540, + "name": "set" + }, + { + "args": [ + "STBase&& v" + ], + "lineno": 545, + "name": "set" + }, + { + "args": [ + "SField const& field", + "unsigned char v" + ], + "lineno": 555, + "name": "setFieldU8" + }, + { + "args": [ + "SField const& field", + "std::uint16_t v" + ], + "lineno": 560, + "name": "setFieldU16" + }, + { + "args": [ + "SField const& field", + "std::uint32_t v" + ], + "lineno": 565, + "name": "setFieldU32" + }, + { + "args": [ + "SField const& field", + "std::uint64_t v" + ], + "lineno": 570, + "name": "setFieldU64" + }, + { + "args": [ + "SField const& field", + "uint128 const& v" + ], + "lineno": 575, + "name": "setFieldH128" + }, + { + "args": [ + "SField const& field", + "uint192 const& v" + ], + "lineno": 580, + "name": "setFieldH192" + }, + { + "args": [ + "SField const& field", + "uint256 const& v" + ], + "lineno": 585, + "name": "setFieldH256" + }, + { + "args": [ + "SField const& field", + "std::int32_t v" + ], + "lineno": 590, + "name": "setFieldI32" + }, + { + "args": [ + "SField const& field", + "STVector256 const& v" + ], + "lineno": 595, + "name": "setFieldV256" + }, + { + "args": [ + "SField const& field", + "AccountID const& v" + ], + "lineno": 600, + "name": "setAccountID" + }, + { + "args": [ + "SField const& field", + "Blob const& v" + ], + "lineno": 605, + "name": "setFieldVL" + }, + { + "args": [ + "SField const& field", + "Slice const& s" + ], + "lineno": 610, + "name": "setFieldVL" + }, + { + "args": [ + "SField const& field", + "STAmount const& v" + ], + "lineno": 615, + "name": "setFieldAmount" + }, + { + "args": [ + "SField const& field", + "STCurrency const& v" + ], + "lineno": 620, + "name": "setFieldCurrency" + }, + { + "args": [ + "SField const& field", + "STIssue const& v" + ], + "lineno": 625, + "name": "setFieldIssue" + }, + { + "args": [ + "SField const& field", + "STNumber const& v" + ], + "lineno": 630, + "name": "setFieldNumber" + }, + { + "args": [ + "SField const& field", + "STPathSet const& v" + ], + "lineno": 635, + "name": "setFieldPathSet" + }, + { + "args": [ + "SField const& field", + "STArray const& v" + ], + "lineno": 640, + "name": "setFieldArray" + }, + { + "args": [ + "SField const& field", + "STObject const& v" + ], + "lineno": 645, + "name": "setFieldObject" + }, + { + "args": [ + "JsonOptions options" + ], + "lineno": 650, + "name": "getJson" + }, + { + "args": [ + "STObject const& obj" + ], + "lineno": 659, + "name": "operator==" + }, + { + "args": [ + "Serializer& s", + "WhichFields whichFields" + ], + "lineno": 687, + "name": "add" + }, + { + "args": [ + "STObject const& objToSort", + "WhichFields whichFields" + ], + "lineno": 705, + "name": "getSortedFields" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 30, + "name": "xrpl" + } + ], + "test_coverage_notes": "STObject and its validation logic are typically tested in protocol-level and serialization/deserialization tests. Likely test files: 'test/protocol/STObject_test.cpp', 'test/protocol/InnerObjectFormats_test.cpp', 'test/protocol/Serializer_test.cpp', and possibly transaction or AMM-related tests for Rules/feature flag logic. Gaps: Edge cases for maximum nesting depth, business rule transitions (feature flag changes), and malformed input may not be exhaustively tested. Template conformance errors may not be fully covered for all InnerObject types.", + "validation_architecture": { + "auto_validated_fields": [ + "nesting depth", + "inner object template applicability", + "template conformance" + ], + "framework": "Custom validation via SOTemplate, Rules, and explicit checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "nesting depth", + "location": "STObject(SerialIter& sit, SField const& name, int depth) constructor", + "validated_by": "explicit check (if (depth > 10))", + "validates": [ + "Ensures that the nesting depth of STObject does not exceed 10" + ], + "validation_type": "range" + }, + { + "confidence": 0.9, + "error_thrown": "none (template not applied if not valid)", + "field": "inner object template applicability", + "location": "STObject::makeInnerObject", + "validated_by": "business logic (Rules, SOTemplate, InnerObjectFormats)", + "validates": [ + "Checks if the correct SOTemplate should be applied to the inner object based on Rules and SField", + "Validates that only allowed templates are applied to specific inner objects" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "may throw (type not specified, but likely std::runtime_error or contract violation)", + "field": "template conformance", + "location": "STObject(SOTemplate const& type, SerialIter& sit, SField const& name) constructor", + "validated_by": "applyTemplate(type)", + "validates": [ + "Ensures that the deserialized object conforms to the provided SOTemplate" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STObject.cpp.ai.md b/src/libxrpl/protocol/STObject.cpp.ai.md new file mode 100644 index 0000000000..3115143d0c --- /dev/null +++ b/src/libxrpl/protocol/STObject.cpp.ai.md @@ -0,0 +1,69 @@ +# `STObject.cpp` — Serialized Object Implementation + +## Role and Context + +`STObject` is the fundamental container type in the XRPL protocol layer. Every ledger entry, every transaction, and every inner object embedded within them is an `STObject` at its core. This file implements the lifecycle of those objects: construction from templates or wire bytes, field access and mutation, binary serialization, hashing, and JSON rendering. + +`STObject` inherits from `STBase` (the abstract root of all serialized types) and acts as a typed heterogeneous map from `SField` keys to `STBase`-derived values. It sits one level above the raw serialization machinery (`Serializer`, `SerialIter`) and one level below transaction- or ledger-specific logic that relies on field accessors. + +## Internal Storage: `v_` and `STVar` + +Fields are stored in `v_`, a `std::vector`. `STVar` is a custom variant with a critical performance property: objects up to 72 bytes are stored inline in a stack-local `aligned_storage` buffer, avoiding heap allocation for the majority of small `ST*` types. Larger types fall back to `new`. The `STObject::copy()` and `STObject::move()` overrides delegate back into this mechanism, allowing `STVar` to reconstruct an `STObject` inside its own buffer when it fits. + +## Templated vs. Free Mode + +The most consequential design distinction in `STObject` is the two-mode lifecycle, controlled by `mType` (an `SOTemplate const*`): + +- **Free mode** (`mType == nullptr`): Fields are stored in insertion order. `getFieldIndex()` performs a linear scan. The object accepts any field via `set()`, `getPField(field, createOkay=true)`, and `emplace_back()`. Free objects are how `STObject` is used during transaction building. + +- **Templated mode** (`mType != nullptr`): `v_` is laid out in template order, with every slot pre-populated — `soeREQUIRED` fields carry a type-correct default, `soeOPTIONAL` and `soeDEFAULT` fields carry the `STI_NOTPRESENT` sentinel. `getFieldIndex()` delegates to `mType->getIndex()`, which is O(1) via an index array in `SOTemplate`. In templated mode, `set(STBase&&)` rejects unknown fields by throwing rather than appending. + +The pre-population strategy means optional fields always have a slot — `isFieldPresent()` checks the sentinel type rather than the field's existence. `makeFieldPresent()` replaces a not-present slot with a default-value instance; `makeFieldAbsent()` does the reverse. `delField()` physically erases from `v_`, which is only semantically safe in free mode since templated slots are positionally indexed. + +## Deserialization and Depth Guard + +`set(SerialIter& sit, int depth)` drives wire deserialization. It reads `(type, field)` ID pairs, looks up the corresponding `SField`, and constructs each child via `STVar(sit, fn, depth+1)`. When the child field is itself an `STObject`, `applyTemplateFromSField()` is called immediately to bind the inner object to its known template if one exists. + +Termination markers (`STI_OBJECT / field==1`) signal the end of the object. Encountering an `STI_ARRAY / field==1` inside an object is an error — cross-contaminated markers indicate malformed data. After all fields are read, the code checks for duplicates using `getSortedFields()` followed by `std::adjacent_find`, enforcing a fundamental ledger invariant: no `STObject` may contain two fields with the same `SField`. + +The depth guard — `if (depth > 10) Throw(...)` — is checked at the start of the `SerialIter` constructor. An attacker-controlled binary stream could otherwise force unbounded recursion through nested `STObject`/`STArray` structures; capping at depth 10 makes the worst-case stack consumption predictable. + +## Template Application + +`applyTemplate(SOTemplate const& type)` is used when an object was first deserialized in free mode and must then be validated against a known schema. It rebuilds `v_` from scratch in template order: + +1. For each template slot, it searches the existing `v_` for a matching field. If found and the slot is `soeDEFAULT`, it rejects a field whose value equals the type default — explicitly encoding the default is prohibited by ledger rules. +2. Missing required fields (`soeREQUIRED`) throw `FieldErr`. +3. Any fields remaining in `v_` after template processing must be `isDiscardable()`. Non-discardable unknown fields throw. + +The final `v_.swap(v)` atomically replaces the unordered deserialized data with the template-ordered, validated layout. `applyTemplateFromSField()` is a convenience wrapper that looks up the template from the global `InnerObjectFormats` singleton. + +## `makeInnerObject` and Feature-Flag Logic + +`makeInnerObject()` is a factory for inner objects that must carry template metadata. Its logic encodes a two-phase protocol amendment history: + +- `fixInnerObjTemplate`: Added templates specifically to AMM inner objects (`sfAuctionSlot`, `sfVoteEntry`). +- `fixInnerObjTemplate2`: Extended templates to all remaining inner objects. +- If no `Rules` are available (pre-consensus or unit test context), templates are always applied. + +This layered condition preserves backward compatibility: historical ledger entries serialized before these amendments lack the template structure, and replaying them requires not rejecting old data. The `getCurrentTransactionRules()` accessor reads the ambient transaction-processing context, making `makeInnerObject` implicitly context-sensitive. + +## Serialization and Hashing + +`add(Serializer& s, WhichFields whichFields)` is the canonical serialization path. It calls `getSortedFields()` to obtain a `fieldCode`-sorted view of present fields, then iterates through them, writing each field's type/ID header (`addFieldID`), its content (`field->add(s)`), and for nested objects/arrays, a termination marker (`s.addFieldID(sType, 1)`). The sort is mandatory — XRPL's binary format is canonically ordered by field code, so producing the same bytes for the same logical content requires deterministic ordering regardless of insertion order. + +`getHash()` prepends a `HashPrefix` (a domain-separation tag) and hashes all fields. `getSigningHash()` passes `omitSigningFields`, which causes `getSortedFields()` to exclude fields like `TxnSignature` — the hash the private key signs over must be independent of the signature itself. + +## Equality and Equivalence + +Two equality predicates exist with different semantics: + +- `operator==` compares only fields that return `isBinary() == true`. Non-binary fields (metadata, computed fields) are excluded. The implementation is O(n²) by design — the comment acknowledges this — because it compares two unordered sets by matching each element in one against all elements in the other. + +- `isEquivalent()` takes a shortcut when both objects share the same `mType` pointer, comparing field-by-field positionally. When templates differ, it falls back to comparing sorted field lists element-by-element. This fast path is sound because two objects referencing the same `SOTemplate` instance are guaranteed to have `v_` in the same order with the same slots. + +## Field Accessor Patterns + +The typed getters (`getFieldU32()`, `getAccountID()`, `getFieldAmount()`, etc.) are thin delegations to two template helpers: `getFieldByValue()` for value types and `getFieldByConstRef()` for reference types. The const-ref variants return references to function-local `static` empty values when the field is absent, rather than throwing — making them safe to call on optional fields without prior presence checks. `getFieldObject()` is exceptional: it calls `applyTemplateFromSField()` on the returned copy, so the caller receives a template-bound object even when the source was free. + +Flag manipulation (`setFlag`, `clearFlag`, `isFlag`) operates on the `sfFlags` field — a `STUInt32` present in most ledger objects. `setFlag` uses `getPField(sfFlags, createOkay=true)` to auto-create the field in free objects if absent, while `clearFlag` uses the non-creating variant and returns false if flags are not present. \ No newline at end of file diff --git a/src/libxrpl/protocol/STParsedJSON.cpp.ai.json b/src/libxrpl/protocol/STParsedJSON.cpp.ai.json new file mode 100644 index 0000000000..f513753fbb --- /dev/null +++ b/src/libxrpl/protocol/STParsedJSON.cpp.ai.json @@ -0,0 +1,389 @@ +{ + "args": [ + { + "lineno": 813, + "name": "name" + }, + { + "lineno": 813, + "name": "json" + } + ], + "classes": [ + { + "args": [ + "name", + "json" + ], + "lineno": 813, + "name": "STParsedJSONObject" + } + ], + "code_paths": [ + { + "call_chain": [ + "STParsedJSON::STParsedJSON", + "STParsedJSONDetail::to_unsigned", + "STParsedJSONDetail::make_name", + "STParsedJSONDetail::not_an_object / not_an_array / unknown_field / out_of_range / bad_type / invalid_data" + ], + "entry_point": "STParsedJSON (constructor or parse function, not shown in snippet)", + "purpose": "Parses and validates JSON input into strongly-typed protocol objects (e.g., STObject, STAmount, etc.), ensuring fields are present, of correct type, and within valid ranges.", + "validation_points": [ + "STParsedJSONDetail::to_unsigned (range validation)", + "STParsedJSONDetail::not_an_object (type validation)", + "STParsedJSONDetail::not_an_array (type validation)", + "STParsedJSONDetail::unknown_field (field existence validation)", + "STParsedJSONDetail::out_of_range (range validation)", + "STParsedJSONDetail::bad_type (type validation)", + "STParsedJSONDetail::invalid_data (semantic validation)" + ] + } + ], + "data_flows": [ + { + "field": "JSON field (generic, e.g., 'Amount', 'Account', etc.)", + "flow": [ + "JSON input", + "STParsedJSON::STParsedJSON (or parse function)", + "Field-specific validation (e.g., to_unsigned, type checks)", + "Error reporting via RPC::make_error or exception", + "Construction of protocol object (e.g., STAmount, STAccount)" + ], + "origin": "JSON input (likely from RPC or transaction submission)", + "transformations": [ + "Type conversion (e.g., string to uint, int to unsigned)", + "Range checking (to_unsigned)", + "Type checking (is object/array/string)", + "Semantic validation (e.g., valid AccountID, valid Amount format)" + ], + "validated_at": "STParsedJSONDetail::* validation helpers (to_unsigned, not_an_object, etc.)" + } + ], + "description": "This file provides functions for parsing JSON into XRPL protocol objects, handling various field types, error reporting, and recursive parsing of nested objects and arrays. It is used to convert JSON representations into strongly-typed protocol objects for the XRPL (XRP Ledger) system.", + "false_positive_patterns": [ + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STParsedJSON.cpp", + "functions": [ + { + "args": [ + "value" + ], + "lineno": 18, + "name": "to_unsigned" + }, + { + "args": [ + "object", + "field" + ], + "lineno": 34, + "name": "make_name" + }, + { + "args": [ + "object", + "field" + ], + "lineno": 40, + "name": "not_an_object" + }, + { + "args": [ + "object" + ], + "lineno": 46, + "name": "not_an_object" + }, + { + "args": [ + "object" + ], + "lineno": 51, + "name": "not_an_array" + }, + { + "args": [ + "object", + "field" + ], + "lineno": 56, + "name": "unknown_field" + }, + { + "args": [ + "object", + "field" + ], + "lineno": 62, + "name": "out_of_range" + }, + { + "args": [ + "object", + "field" + ], + "lineno": 68, + "name": "bad_type" + }, + { + "args": [ + "object", + "field" + ], + "lineno": 74, + "name": "invalid_data" + }, + { + "args": [ + "object" + ], + "lineno": 80, + "name": "invalid_data" + }, + { + "args": [ + "object", + "field" + ], + "lineno": 85, + "name": "array_expected" + }, + { + "args": [ + "object", + "field" + ], + "lineno": 91, + "name": "string_expected" + }, + { + "args": [ + "object" + ], + "lineno": 97, + "name": "too_deep" + }, + { + "args": [ + "object", + "index" + ], + "lineno": 103, + "name": "singleton_expected" + }, + { + "args": [ + "sField" + ], + "lineno": 110, + "name": "template_mismatch" + }, + { + "args": [ + "item", + "index" + ], + "lineno": 117, + "name": "non_object_in_array" + }, + { + "args": [ + "field", + "json_name", + "fieldName", + "name", + "value", + "error" + ], + "lineno": 126, + "name": "parseUnsigned" + }, + { + "args": [ + "field", + "json_name", + "fieldName", + "name", + "value", + "error" + ], + "lineno": 154, + "name": "parseUint16" + }, + { + "args": [ + "field", + "json_name", + "fieldName", + "name", + "value", + "error" + ], + "lineno": 202, + "name": "parseUint32" + }, + { + "args": [ + "json_name", + "fieldName", + "name", + "value", + "error" + ], + "lineno": 249, + "name": "parseLeaf" + }, + { + "args": [ + "json_name", + "json", + "inName", + "depth", + "error" + ], + "lineno": 670, + "name": "parseArray" + }, + { + "args": [ + "json_name", + "json", + "inName", + "depth", + "error" + ], + "lineno": 677, + "name": "parseObject" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + }, + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + }, + { + "lineno": 17, + "name": "STParsedJSONDetail" + } + ], + "test_coverage_notes": "Validation helpers are typically tested indirectly via higher-level tests (e.g., transaction submission, RPC command tests). Look for test files in 'src/test/protocol', 'src/test/rpc', or 'src/test/app' that submit malformed or boundary-case JSON to the system. Direct unit tests for STParsedJSONDetail helpers are unlikely; coverage may be incomplete for edge cases (e.g., very large numbers, deeply nested objects, unknown fields). LCOV_EXCL_START suggests some helpers are not covered by tests. Gaps may exist for rare or malformed input cases.", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STParsedJSON.cpp.ai.md b/src/libxrpl/protocol/STParsedJSON.cpp.ai.md new file mode 100644 index 0000000000..80b0f7a8d7 --- /dev/null +++ b/src/libxrpl/protocol/STParsedJSON.cpp.ai.md @@ -0,0 +1,51 @@ +# `src/libxrpl/protocol/STParsedJSON.cpp` + +## Role in the System + +This file is the JSON-to-protocol-object deserializer for the XRPL ledger. Every RPC call that submits a transaction, queries ledger state, or manipulates protocol data passes through this code before any validation or execution. Its job is to convert untyped `Json::Value` trees into the strongly-typed Serialized Type (`ST*`) object graph the rest of the system operates on. The public interface is deliberately simple: construct an `STParsedJSONObject` with a name and JSON, then check `object` (populated on success) or `error` (populated on failure). No exceptions escape; all errors surface as a `Json::Value` carrying RPC error codes. + +## Architecture: Three Parsing Layers + +All implementation lives in the anonymous `STParsedJSONDetail` namespace. The parsing logic is split into three mutually recursive functions: + +**`parseLeaf()`** handles every primitive (non-container) type. It does a `switch` on `field.fieldType`, covering the full `SerializedTypeID` enum: `STI_UINT8` through `STI_UINT256`, `STI_INT32`, `STI_VL` (variable-length blobs), `STI_AMOUNT`, `STI_NUMBER`, `STI_ACCOUNT`, `STI_ISSUE`, `STI_CURRENCY`, `STI_XCHAIN_BRIDGE`, `STI_VECTOR256`, and `STI_PATHSET`. Each case returns a `std::optional`, where `nullopt` signals failure and `error` (an output parameter) carries the human-readable RPC error. + +**`parseObject()`** iterates over a JSON object's member names, looks each name up in the global `SField` registry, and dispatches to `parseLeaf()` for leaf types, recursing into itself for `STI_OBJECT`/`STI_TRANSACTION`/`STI_LEDGERENTRY`/`STI_VALIDATION` children, and into `parseArray()` for `STI_ARRAY` children. After all fields are parsed, it calls `data.applyTemplateFromSField(inName)`, which retroactively enforces the `SOTemplate` associated with the field — rejecting unknown fields, catching missing required fields, and verifying that default-valued optional fields were not explicitly set. A mismatch throws `STObject::FieldErr`, caught and translated into a `template_mismatch` RPC error. + +**`parseArray()`** handles `STArray` values. A critical design constraint here: each element in a JSON array encoding an `STArray` must be a JSON object with exactly one key. This enforces the XRPL canonical convention where array elements are tagged with their field name, e.g., `[{"Memo": {...}}, {"Memo": {...}}]`. Null or multi-keyed elements are rejected as `singleton_expected`. Each element's inner object is parsed via `parseObject()`, and after parsing, the result must have a field type of `STI_OBJECT`; any other type is rejected as `non_object_in_array`. + +## Non-Obvious Type-Specific Behaviors + +Several leaf cases contain special-casing that encodes XRPL protocol knowledge: + +`parseUint16` (called for `STI_UINT16`) recognizes `sfTransactionType` and `sfLedgerEntryType` fields and accepts human-readable names like `"Payment"` or `"Offer"` in addition to numeric values. These names are resolved through the `TxFormats` and `LedgerFormats` singleton registries. Critically, when parsing at the top level with `sfGeneric` as the template sentinel, this function also upgrades `name` to `sfTransaction` or `sfLedgerEntry` so that the subsequent `applyTemplateFromSField()` call enforces the correct field schema for the specific transaction or ledger entry type. + +`parseUint32` (called for `STI_UINT32`) recognizes `sfPermissionValue` and translates granular permission names or transaction-type names into their numeric representations via `Permission::getInstance()`. + +`STI_UINT8` contains special handling for `sfTransactionResult`, accepting TER result code strings (e.g. `"tesSUCCESS"`, `"tecPATH_DRY"`) by calling `transCode()` and then `TERtoInt()`, including a range check to ensure the code fits in a `uint8_t`. + +`STI_UINT64` uses `std::from_chars` for hexadecimal parsing by default, but checks `field.shouldMeta(SField::sMD_BaseTen)` to switch to base-10 parsing for fields whose metadata marks them as decimal amounts. + +`STI_PATHSET` contains the most complex leaf logic. It handles both traditional IOU paths (with `account`, `currency`, `issuer`) and MPT (Multi-Purpose Token) paths (with `mpt_issuance_id`). When both `currency` and `mpt_issuance_id` are present simultaneously, the parse is rejected. For MPT paths, it also validates that the issuer field, if present, matches the issuer embedded in the MPTID itself. + +## Safety and Defensive Coding + +A `maxDepth = 64` constant guards both `parseObject()` and `parseArray()`. Any JSON structure nesting deeper than 64 levels is rejected as `too_deep`, preventing runaway recursion from crafted inputs. This is the JSON counterpart to the binary deserialization depth guard in `STArray` (which uses a maximum of 10 at the binary level). + +The `to_unsigned` and `to_unsigned` template pair in `STParsedJSONDetail` are SFINAE-constrained to signed→unsigned and unsigned→unsigned conversions respectively. Each variant throws `std::runtime_error` on range violation. This eliminates silent truncation: if JSON delivers `-1` for a `uint32_t` field, the parse fails with an explicit `invalid_data` error rather than wrapping to `UINT32_MAX`. + +The `static_assert(std::is_same_v)` inside the `STI_INT32` case is a forward-compatibility guard: if the upstream JSON library ever widens its `asInt()` return type, the compiler will signal that the bounds checking logic needs revisiting. + +## Error Reporting Strategy + +Error handling uses a hybrid model. Internally, parsing functions throw standard exceptions on individual conversion failures (via `beast::lexicalCastThrow`, `Throw`, etc.), and each caller wraps the failure in a `catch (std::exception const&)` block that records a specific RPC error in the `error` output parameter and returns `nullopt`. This keeps the control flow clean — an exception in a deeply nested helper propagates to the nearest catch, which converts it to a structured error without needing to thread error codes through every intermediate return. + +Error messages are path-qualified using `make_name()`, which concatenates `object.field` strings. As parsing recurses, each level prepends its own JSON path component, so a deeply nested error like `"Field 'tx_json.Memos.[0].Memo.MemoData' has bad type"` is reported with the full path to the offending field. + +## Integration with `detail::STVar` + +`parseLeaf()` returns `std::optional`, not `std::optional`. `STVar` is a type-erased "variant" container with a small-object optimization: types of 72 bytes or fewer are stored inline in aligned storage, avoiding heap allocation for common small types like `STUInt32` or `STAccount`. Each successful leaf parse calls `detail::make_stvar(...)` to construct the appropriately typed `ST*` object inside the `STVar`. The parent `STObject` accumulates these via `emplace_back`, which takes ownership of the `STVar`. + +## Usage Context + +`STParsedJSONObject` is constructed in `TransactionSign.cpp` whenever an RPC call provides a `tx_json` field to be signed or submitted. The caller checks `parsed.object.has_value()` and if true promotes the result to `STTx` for further processing; if false, `parsed.error` is forwarded directly to the RPC response. The same pattern appears in the `Simulate` RPC handler and in the `jtx` testing framework, making `STParsedJSONObject` the universal gateway between untyped JSON and the typed protocol object graph. \ No newline at end of file diff --git a/src/libxrpl/protocol/STPathSet.cpp.ai.json b/src/libxrpl/protocol/STPathSet.cpp.ai.json new file mode 100644 index 0000000000..54fb1c3c0a --- /dev/null +++ b/src/libxrpl/protocol/STPathSet.cpp.ai.json @@ -0,0 +1,367 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "STPathSet::STPathSet", + "SerialIter::get8", + "STPathElement construction", + "push_back(path)" + ], + "entry_point": "STPathSet::STPathSet(SerialIter&, SField const&)", + "purpose": "Deserializes a path set from serialized data, validating path element types and structure.", + "validation_points": [ + "STPathSet::STPathSet: if (path.empty()) ... Throw(...)", + "STPathSet::STPathSet: if ((iType & ~STPathElement::typeAll) != 0) ... Throw(...)", + "STPathSet::STPathSet: XRPL_ASSERT(!(hasCurrency && hasMPT), ...)" + ] + }, + { + "call_chain": [ + "STPathSet::assembleAdd", + "value.push_back(base)", + "newPath.push_back(tail)", + "while (++it != value.rend()) { if (*it == newPath) ... }" + ], + "entry_point": "STPathSet::assembleAdd(STPath const&, STPathElement const&)", + "purpose": "Adds a new path (base + tail) to the set if not a duplicate.", + "validation_points": [ + "No explicit validation, but relies on STPathElement and STPath invariants." + ] + }, + { + "call_chain": [ + "STPathElement::get_hash", + "element.getAccountID()", + "element.getPathAsset().visit(...)", + "element.getIssuerID()" + ], + "entry_point": "STPathElement::get_hash", + "purpose": "Computes a hash for a path element for deduplication or lookup.", + "validation_points": [ + "No explicit validation, assumes element is well-formed." + ] + } + ], + "data_flows": [ + { + "field": "iType (path element type byte)", + "flow": [ + "SerialIter::get8()", + "if (iType == typeNone || iType == typeBoundary) ...", + "else if ((iType & ~typeAll) != 0) ...", + "else { ... }" + ], + "origin": "SerialIter::get8() (deserialization input)", + "transformations": [ + "Bitmask checks to determine which fields are present in the path element" + ], + "validated_at": "STPathSet::STPathSet: (iType & ~STPathElement::typeAll) != 0" + }, + { + "field": "path (vector)", + "flow": [ + "path is built by appending STPathElement objects", + "if (iType == typeNone || typeBoundary) triggers validation", + "push_back(path) adds to STPathSet" + ], + "origin": "Local variable in STPathSet::STPathSet, built from deserialized elements", + "transformations": [ + "Cleared after each push_back", + "Constructed from deserialized fields" + ], + "validated_at": "STPathSet::STPathSet: if (path.empty())" + }, + { + "field": "hasCurrency && hasMPT (flags)", + "flow": [ + "iType bitmask", + "bool hasCurrency = (iType & typeCurrency) != 0", + "bool hasMPT = (iType & typeMPT) != 0", + "XRPL_ASSERT(!(hasCurrency && hasMPT), ...)" + ], + "origin": "Derived from iType bitmask in STPathSet::STPathSet", + "transformations": [ + "Logical AND to check mutual exclusivity" + ], + "validated_at": "STPathSet::STPathSet: XRPL_ASSERT(!(hasCurrency && hasMPT), ...)" + }, + { + "field": "AccountID, Currency, MPTID, Issuer", + "flow": [ + "Deserialized from sit", + "Used to construct STPathElement", + "Appended to path" + ], + "origin": "Deserialized from SerialIter (sit.get160(), sit.get192())", + "transformations": [ + "Type conversion (e.g., static_cast(sit.get160()))" + ], + "validated_at": "Implicitly validated by iType bitmask and XRPL_ASSERT" + } + ], + "description": "Implements serialization, deserialization, hashing, and JSON conversion for XRPL path sets and path elements, which are used in payment pathfinding and transaction routing.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "path (vector of STPathElement)", + "empty", + "string", + "validation" + ], + "evidence": "manual check (if path.empty()) at STPathSet::STPathSet (constructor)", + "issue_pattern": "Missing empty string validation for path (vector of STPathElement)", + "why_false_positive": "manual check (if path.empty()) validates path (vector of STPathElement) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "iType (path element type byte)", + "empty", + "string", + "validation" + ], + "evidence": "manual bitmask check ((iType & ~STPathElement::typeAll) != 0) at STPathSet::STPathSet (constructor)", + "issue_pattern": "Missing empty string validation for iType (path element type byte)", + "why_false_positive": "manual bitmask check ((iType & ~STPathElement::typeAll) != 0) validates iType (path element type byte) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "iType (path element type byte)", + "format", + "validation", + "invalid" + ], + "evidence": "manual bitmask check ((iType & ~STPathElement::typeAll) != 0) at STPathSet::STPathSet (constructor)", + "issue_pattern": "Missing format validation for iType (path element type byte)", + "why_false_positive": "manual bitmask check ((iType & ~STPathElement::typeAll) != 0) validates iType (path element type byte) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hasCurrency && hasMPT (mutually exclusive flags)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(!(hasCurrency && hasMPT), ...) at STPathSet::STPathSet (constructor)", + "issue_pattern": "Missing empty string validation for hasCurrency && hasMPT (mutually exclusive flags)", + "why_false_positive": "XRPL_ASSERT(!(hasCurrency && hasMPT), ...) validates hasCurrency && hasMPT (mutually exclusive flags) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STPathSet.cpp", + "functions": [ + { + "args": [ + "element" + ], + "lineno": 10, + "name": "STPathElement::get_hash" + }, + { + "args": [ + "sit", + "name" + ], + "lineno": 34, + "name": "STPathSet::STPathSet" + }, + { + "args": [ + "n", + "buf" + ], + "lineno": 81, + "name": "STPathSet::copy" + }, + { + "args": [ + "n", + "buf" + ], + "lineno": 86, + "name": "STPathSet::move" + }, + { + "args": [ + "base", + "tail" + ], + "lineno": 91, + "name": "STPathSet::assembleAdd" + }, + { + "args": [ + "t" + ], + "lineno": 107, + "name": "STPathSet::isEquivalent" + }, + { + "args": [], + "lineno": 113, + "name": "STPathSet::isDefault" + }, + { + "args": [ + "account", + "asset", + "issuer" + ], + "lineno": 117, + "name": "STPath::hasSeen" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 127, + "name": "STPath::getJson" + }, + { + "args": [ + "options" + ], + "lineno": 153, + "name": "STPathSet::getJson" + }, + { + "args": [], + "lineno": 160, + "name": "STPathSet::getSType" + }, + { + "args": [ + "s" + ], + "lineno": 164, + "name": "STPathSet::add" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is in STPathSet::STPathSet, which is exercised whenever a path set is deserialized from binary data. Typical test coverage would be in unit tests for STPathSet and STPathElement, likely found in files such as 'STPathSet_test.cpp', 'STPath_test.cpp', or protocol serialization/deserialization tests. Edge cases such as empty paths, invalid iType values, and mutually exclusive flags (hasCurrency && hasMPT) should be tested. Gaps may exist if tests do not cover all possible invalid iType values, or if malformed serialized data is not tested. Exception paths (Throw) and assertion failures (XRPL_ASSERT) should be explicitly tested for robust coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation, XRPL_ASSERT macro, logging via JLOG, exception handling via Throw<> template", + "validation_layer": "business_logic (constructor-level validation on deserialization)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"empty path\")", + "field": "path (vector of STPathElement)", + "location": "STPathSet::STPathSet (constructor)", + "validated_by": "manual check (if path.empty())", + "validates": [ + "Ensures that a path is not empty before being added to the pathset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"bad path element\")", + "field": "iType (path element type byte)", + "location": "STPathSet::STPathSet (constructor)", + "validated_by": "manual bitmask check ((iType & ~STPathElement::typeAll) != 0)", + "validates": [ + "Ensures that the path element type byte only contains valid bits" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely contract violation, may abort)", + "field": "hasCurrency && hasMPT (mutually exclusive flags)", + "location": "STPathSet::STPathSet (constructor)", + "validated_by": "XRPL_ASSERT(!(hasCurrency && hasMPT), ...)", + "validates": [ + "Ensures that a path element cannot have both currency and MPT flags set" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STPathSet.cpp.ai.md b/src/libxrpl/protocol/STPathSet.cpp.ai.md new file mode 100644 index 0000000000..2dce8584f5 --- /dev/null +++ b/src/libxrpl/protocol/STPathSet.cpp.ai.md @@ -0,0 +1,43 @@ +# `STPathSet.cpp` — Payment Path Serialization and Management + +## Role in the System + +Cross-currency payments on the XRP Ledger require a pathfinding step: a client or server discovers one or more routes through the network of offers and liquidity providers that can convert the source asset into the destination asset. These candidate routes are encoded in a `Paths` field of a `Payment` transaction as an `STPathSet` — a serialized type that carries an ordered collection of alternative payment paths. `STPathSet.cpp` implements the binary serialization, deserialization, hashing, cycle-detection, and JSON rendering for this type and the two nested types it depends on: `STPathElement` (a single node or hop descriptor) and `STPath` (an ordered sequence of such nodes representing one candidate route). + +## Type Hierarchy + +`STPathElement` stores the minimal description of a single hop: it holds a bitmask `mType` indicating which of its three optional components — an `AccountID`, an asset (`PathAsset`), and an issuer `AccountID` — are actually present, plus a pre-computed hash for fast equality. An element with an account set is a *rippling node* (payment flows through that account's trust lines); an element without an account is an *offer node* (a class of order book offers). The `is_offer_` flag captures this distinction at construction time. + +`PathAsset` holds a `std::variant`, reflecting the XRP Ledger's extension to support Multi-Purpose Tokens (MPT) alongside traditional IOU currencies. A path element can carry exactly one: setting both `typeCurrency` and `typeMPT` bits is a protocol invariant violation enforced by `XRPL_ASSERT` during deserialization. + +`STPath` is a plain `std::vector` wrapper with a vector-like interface plus `hasSeen()` for cycle detection. `STPathSet` is a `std::vector` wrapped inside `STBase`, making it a first-class serialized type (`STI_PATHSET`) that can appear in transaction fields. + +## Wire Format + +The binary encoding packs paths end-to-end with structural markers: + +- For each hop in a path, a one-byte type field (`iType`) is emitted first, followed by the present components. The type byte is a bitmask: `typeAccount = 0x01`, `typeCurrency = 0x10`, `typeIssuer = 0x20`, `typeMPT = 0x40`. Accounts and issuers are 20-byte `AccountID` values; currencies are 20-byte `Currency` values; MPT IDs are 24-byte `MPTID` values read with `sit.get192()`. +- Between consecutive paths, a `typeBoundary` byte (`0xFF`) is written as a separator. +- The entire path set is terminated by a `typeNone` byte (`0x00`). + +The `add()` serialization method emits this format exactly, while the `STPathSet(SerialIter&, SField const&)` constructor reverses it. The constructor loops indefinitely, peeling off one type byte per iteration. When it encounters `typeBoundary` or `typeNone`, it flushes the current `path` vector to the set — but first asserts the path is non-empty and throws `std::runtime_error("empty path")` if it is, since an empty path between separators would indicate corrupt input. Any unknown bit in the type byte (bits outside `typeAll = 0x71`) cause an immediate `std::runtime_error("bad path element")`. These two checks are the sole validation gate for untrusted binary data entering the pathset, making them critical for resilience. + +## Hashing Strategy + +`STPathElement::get_hash()` computes a non-cryptographic hash over the element's three fields using the pattern `hash = hash * multiplier ^ byte`. The three multipliers — 257, 509, 911 — are small primes chosen to produce good dispersion without collisions for the expected input domain. The comment explicitly notes this need not be secure. The hash is eagerly stored in `hash_value_` at every construction point, so equality comparisons (which check `hash_value_` before the full field comparison) short-circuit quickly. This matters because `assembleAdd()` must scan backward through accumulated paths to detect duplicates. + +Notably, `get_hash()` dispatches through `getPathAsset().visit()` rather than branching on `mType` directly. This is intentional: a comment in the source notes that `mType` may carry `typeAccount` while the asset slot is still set (e.g. from `Pathfinder::addLink()`), so the hash must reflect actual content rather than the type flags. + +## Duplicate Suppression During Pathfinding + +`assembleAdd(STPath const& base, STPathElement const& tail)` is a helper used by the pathfinder when constructing candidate paths incrementally. It appends `base` to the internal vector and then appends `tail` to form a candidate. It then scans all *other* paths in reverse to check for an exact duplicate. If one is found, the newly appended path is popped and the function returns `false`; otherwise it returns `true`. The reverse iteration (via `rbegin()`) is a micro-optimization: duplicates are most likely to be the most recently added paths, so scanning backward catches them earliest. + +The reason this dedup is done at add time rather than post-hoc is to bound the size of the path set during pathfinding. Allowing duplicate paths would waste computation in the payment engine, which must simulate flow through each path independently. + +## `copy()` and `move()` Placement Semantics + +`STBase` subclasses implement `copy()` and `move()` to support placement into an external buffer owned by `detail::STVar`, the type-erased storage used by `STObject` for its fields. Both simply delegate to `emplace()`, which calls placement-new. This is the standard pattern across all serialized types in `libxrpl/protocol` and keeps field storage compact without heap allocation per field. + +## JSON Rendering + +`STPath::getJson()` emits an array of objects, one per hop. Each object always includes the numeric `type` field so consumers can distinguish hop kinds without re-parsing the optional keys. Account-typed elements render their 20-byte ID as a base58-encoded string via `to_string()`; MPT elements emit `mpt_issuance_id`; IOU currency elements emit `currency`. The assertion that `typeCurrency` and `typeMPT` cannot both be set is repeated here in the JSON path, ensuring JSON output is always coherent even if an element reaches rendering through an unexpected code path. \ No newline at end of file diff --git a/src/libxrpl/protocol/STTakesAsset.cpp.ai.json b/src/libxrpl/protocol/STTakesAsset.cpp.ai.json new file mode 100644 index 0000000000..4d41ba4c5a --- /dev/null +++ b/src/libxrpl/protocol/STTakesAsset.cpp.ai.json @@ -0,0 +1,260 @@ +{ + "args": [ + { + "lineno": 7, + "name": "sle" + }, + { + "lineno": 7, + "name": "asset" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "associateAsset" + ], + "entry_point": "associateAsset", + "purpose": "Associates an Asset with all SLE fields that require asset association, performing type and value validations.", + "validation_points": [ + "entry.getSType() == STI_NOTPRESENT (type check, skip if not present)", + "entry.downcast() (C++ downcast, type safety)", + "sle.getStyle(ta.getFName()) != soeINVALID (asserts valid style)", + "style != soeDEFAULT || !ta.isDefault() (asserts non-default value if style is default)" + ] + } + ], + "data_flows": [ + { + "field": "entry (STBase&)", + "flow": [ + "SLE::getIndex(i)", + "entry.getFName()", + "entry.getSType()", + "entry.downcast()", + "ta.associateAsset(asset)", + "sle.makeFieldAbsent(field) (if needed)" + ], + "origin": "SLE::getIndex(i)", + "transformations": [ + "Type checked (STI_NOTPRESENT)", + "Downcast to STTakesAsset", + "Asset associated via ta.associateAsset(asset)", + "Possibly removed from SLE if default" + ], + "validated_at": [ + "entry.getSType() == STI_NOTPRESENT", + "entry.downcast()", + "sle.getStyle(ta.getFName()) != soeINVALID", + "style != soeDEFAULT || !ta.isDefault()" + ] + }, + { + "field": "asset (Asset const&)", + "flow": [ + "associateAsset parameter", + "ta.associateAsset(asset)" + ], + "origin": "associateAsset parameter", + "transformations": [ + "Passed to ta.associateAsset to update field value" + ], + "validated_at": "No direct validation, but only used after entry passes all checks" + }, + { + "field": "style (SLE::getStyle result)", + "flow": [ + "sle.getStyle(ta.getFName())", + "XRPL_ASSERT_PARTS(style != soeINVALID, ...)", + "XRPL_ASSERT_PARTS(style != soeDEFAULT || !ta.isDefault(), ...)", + "if (style == soeDEFAULT && ta.isDefault()) sle.makeFieldAbsent(field)" + ], + "origin": "sle.getStyle(ta.getFName())", + "transformations": [ + "Checked for validity (not soeINVALID)", + "Checked for default-ness" + ], + "validated_at": [ + "XRPL_ASSERT_PARTS(style != soeINVALID, ...)", + "XRPL_ASSERT_PARTS(style != soeDEFAULT || !ta.isDefault(), ...)" + ] + } + ], + "description": "Provides the associateAsset function to associate an Asset with fields in a SLE (STLedgerEntry) that require asset metadata, handling field presence, type, and default value logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "entry.getSType()", + "empty", + "string", + "validation" + ], + "evidence": "type check (STI_NOTPRESENT) at associateAsset", + "issue_pattern": "Missing empty string validation for entry.getSType()", + "why_false_positive": "type check (STI_NOTPRESENT) validates entry.getSType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "entry.getSType()", + "type", + "validation", + "check" + ], + "evidence": "type check (STI_NOTPRESENT) at associateAsset", + "issue_pattern": "Missing type validation for entry.getSType()", + "why_false_positive": "type check (STI_NOTPRESENT) validates entry.getSType() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "entry.downcast()", + "empty", + "string", + "validation" + ], + "evidence": "C++ downcast at associateAsset", + "issue_pattern": "Missing empty string validation for entry.downcast()", + "why_false_positive": "C++ downcast validates entry.downcast() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "entry.downcast()", + "type", + "validation", + "check" + ], + "evidence": "C++ downcast at associateAsset", + "issue_pattern": "Missing type validation for entry.downcast()", + "why_false_positive": "C++ downcast validates entry.downcast() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle.getStyle(ta.getFName())", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT_PARTS(style != soeINVALID, ...) at associateAsset", + "issue_pattern": "Missing empty string validation for sle.getStyle(ta.getFName())", + "why_false_positive": "XRPL_ASSERT_PARTS(style != soeINVALID, ...) validates sle.getStyle(ta.getFName()) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "style == soeDEFAULT && ta.isDefault()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT_PARTS(style != soeDEFAULT || !ta.isDefault(), ...) at associateAsset", + "issue_pattern": "Missing empty string validation for style == soeDEFAULT && ta.isDefault()", + "why_false_positive": "XRPL_ASSERT_PARTS(style != soeDEFAULT || !ta.isDefault(), ...) validates style == soeDEFAULT && ta.isDefault() for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STTakesAsset.cpp", + "functions": [ + { + "args": [ + "sle", + "asset" + ], + "lineno": 6, + "name": "associateAsset" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "The function associateAsset is likely tested indirectly via higher-level SLE or Asset manipulation tests. Look for tests in files such as 'test/ledger/SLE_test.cpp', 'test/protocol/STTakesAsset_test.cpp', or integration tests involving asset association. Direct unit tests for associateAsset may not exist if it is considered an internal helper. Validation paths (type checks, downcasts, style asserts) may not be explicitly tested unless there are tests for malformed SLEs or fields with incorrect types/styles. Gaps may exist in testing error/assertion cases, especially for invalid style or type scenarios.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT_PARTS (custom assertion macro), C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (skips iteration)", + "field": "entry.getSType()", + "location": "associateAsset", + "validated_by": "type check (STI_NOTPRESENT)", + "validates": [ + "skips fields that are not present (type STI_NOTPRESENT)" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "undefined (likely assertion or crash if type is wrong, but not explicit here)", + "field": "entry.downcast()", + "location": "associateAsset", + "validated_by": "C++ downcast", + "validates": [ + "ensures entry is of type STTakesAsset" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT_PARTS (likely assertion failure/exception)", + "field": "sle.getStyle(ta.getFName())", + "location": "associateAsset", + "validated_by": "XRPL_ASSERT_PARTS(style != soeINVALID, ...)", + "validates": [ + "template element style must not be soeINVALID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT_PARTS (likely assertion failure/exception)", + "field": "style == soeDEFAULT && ta.isDefault()", + "location": "associateAsset", + "validated_by": "XRPL_ASSERT_PARTS(style != soeDEFAULT || !ta.isDefault(), ...)", + "validates": [ + "if style is soeDEFAULT, value must not be default" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STTakesAsset.cpp.ai.md b/src/libxrpl/protocol/STTakesAsset.cpp.ai.md new file mode 100644 index 0000000000..75298f44dc --- /dev/null +++ b/src/libxrpl/protocol/STTakesAsset.cpp.ai.md @@ -0,0 +1,37 @@ +# `STTakesAsset.cpp` — Asset Association for Precision-Sensitive Ledger Fields + +## Role in the System + +This file provides the single free function `associateAsset(SLE&, Asset const&)`, which drives the runtime mechanism by which asset-type information is injected into ledger-entry fields that need it for correct numeric precision. The problem it solves arises from a design asymmetry in the XRPL serialized type system: `STNumber` fields store high-precision floating-point values that must be rounded to the precision appropriate for their underlying asset (e.g., XRP integer satoshis vs. IOU decimal places), but the asset type is not encoded inside the field itself — it lives separately in the ledger entry. `associateAsset()` bridges that gap, pushing the asset context into each relevant field so `STNumber` can round correctly during serialization. + +## Include Order Discipline + +The file opens with an unusual two-line comment: `STTakesAsset.h` must be included before `STLedgerEntry.h`. This ordering is structurally required because `STTakesAsset.h` contains only a forward declaration of `STLedgerEntry` (to declare the free function signature without a circular dependency), while the function body in this `.cpp` file requires the complete definition of `STLedgerEntry` (to call `getIndex()`, `getStyle()`, and `makeFieldAbsent()`). The comment makes the constraint explicit so future maintainers cannot accidentally reorder or merge the includes. + +## How `associateAsset()` Works + +The function iterates over every field in the `SLE` using integer offsets rather than by name or value. This offset-based loop (`getIndex(i)`) is the only path in `STObject` that yields a mutable `STBase&` reference; iterator-based traversal returns const views. For each field, the function checks whether the field's `SField` metadata includes the `sMD_NeedsAsset` flag (bit `0x80`, defined in `SField.h` and documented as "intended for `STNumber`"). Fields that lack this flag are skipped entirely. + +For flagged fields, the function applies a three-step process: + +**1. Presence guard.** If the field's serialized type ID is `STI_NOTPRESENT`, the field is absent from this ledger entry and the loop skips it. Optional fields may not always be populated. + +**2. Type-safe downcast.** `entry.downcast()` performs a `dynamic_cast` from `STBase&`. If the cast fails (returns null), `downcast()` throws — a deliberate fail-fast invariant. The contract is that any `SField` carrying `sMD_NeedsAsset` must be backed by a type derived from `STTakesAsset`. A mismatch indicates a programming error in the field schema, not a recoverable runtime condition, and asserting loudly is the correct response. + +**3. Association and default cleanup.** After calling `ta.associateAsset(asset)` — which, in the derived `STNumber` class, rounds the stored `Number` value to the asset's precision — the function checks whether the field's element style is `soeDEFAULT` and whether the field value has now become the default (typically zero). If so, it calls `sle.makeFieldAbsent(field)` to remove the field from the ledger entry. This cleanup step is subtle but necessary: asset-precision rounding can reduce a small non-zero value to exactly zero, and a zero-valued `soeDEFAULT` field must not be persisted in the ledger. Leaving it would cause unnecessary storage consumption and could affect equality checks elsewhere. + +Two `XRPL_ASSERT_PARTS` calls bracket the association: one confirms the element style is not `soeINVALID` (which would indicate the field isn't part of this SLE's template at all), and a second confirms that a `soeDEFAULT` field is not already at its default value before association. This second assertion captures the invariant that `soeDEFAULT` fields are removed from the SLE when they hit their default — so if one exists and is set, it must not already be zero before association runs. + +## Relationship to `STTakesAsset` and `STNumber` + +The `STTakesAsset` class (defined in the header) is a thin intermediate base class that sits between `STBase` and concrete field types like `STNumber`. It holds an `std::optional asset_` and exposes a virtual `associateAsset()` method whose base implementation simply calls `asset_.emplace(a)`. `STNumber` overrides this method to additionally apply precision rounding (`roundToAsset`) to its stored `Number` value at the moment of association. + +The fields that carry `sMD_NeedsAsset` are defined in `sfields.macro`: `sfAssetsAvailable`, `sfAssetsMaximum`, `sfAssetsTotal`, `sfLossUnrealized`, `sfDebtTotal`, `sfDebtMaximum`, `sfCoverAvailable`, `sfPrincipalOutstanding`, `sfTotalValueOutstanding`, and `sfManagementFeeOutstanding` — all `NUMBER`-typed fields used in the `SingleAssetVault` and `LendingProtocol` feature domains. These fields also carry `SField::sMD_Default`, meaning they are stored only when non-zero, which is exactly why the default-cleanup logic in `associateAsset()` matters. + +## Calling Convention + +The header's documentation specifies that `associateAsset()` should be called near the end of `doApply()` in any `Transactor` subclass, after all modifications to the SLE have been completed. In practice, vault transactors (`VaultSet`, `VaultDeposit`, `VaultWithdraw`, `VaultClawback`) each call it as a final bookkeeping step, passing the vault's SLE and its `vaultAsset`. The late placement is intentional: `STNumber` rounding reflects the final field values, so association must not precede any computation that alters those values. Associating early and then modifying the field afterward would leave the field in an un-rounded or incorrectly-rounded state before serialization. + +## Error Handling and Invariant Enforcement + +The function has no return value and throws no user-visible errors. The only error paths are the `downcast` throw (a programming error in field type registration) and the `XRPL_ASSERT_PARTS` calls (debug-mode assertions protecting structural invariants). This design reflects the expectation that by the time `associateAsset()` is called, the SLE and its schema are already validated; the function's job is bookkeeping, not validation. \ No newline at end of file diff --git a/src/libxrpl/protocol/STTx.cpp.ai.json b/src/libxrpl/protocol/STTx.cpp.ai.json new file mode 100644 index 0000000000..17cb18d95d --- /dev/null +++ b/src/libxrpl/protocol/STTx.cpp.ai.json @@ -0,0 +1,802 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "STTx::STTx(SerialIter&)", + "set(sit)", + "getFieldU16(sfTransactionType)", + "safe_cast", + "getTxFormat", + "TxFormats::findByType", + "applyTemplate" + ], + "entry_point": "STTx::STTx(SerialIter& sit)", + "purpose": "Deserializes a transaction from serialized bytes, validates structure, type, and template.", + "validation_points": [ + "Explicit check: Transaction length (length < txMinSizeBytes || length > txMaxSizeBytes)", + "set(sit): Checks for object terminator", + "getFieldU16(sfTransactionType) + safe_cast: Validates TransactionType field", + "getTxFormat: Validates TransactionType against known formats", + "applyTemplate: Enforces transaction field template" + ] + }, + { + "call_chain": [ + "STTx::STTx(STObject&&)", + "getFieldU16(sfTransactionType)", + "safe_cast", + "getTxFormat", + "TxFormats::findByType", + "applyTemplate" + ], + "entry_point": "STTx::STTx(STObject&& object)", + "purpose": "Constructs a transaction from an existing STObject, validates type and template.", + "validation_points": [ + "getFieldU16(sfTransactionType) + safe_cast: Validates TransactionType field", + "getTxFormat: Validates TransactionType against known formats", + "applyTemplate: Enforces transaction field template" + ] + }, + { + "call_chain": [ + "STTx::STTx(TxType, assembler)", + "getTxFormat", + "TxFormats::findByType", + "set(format->getSOTemplate())", + "setFieldU16(sfTransactionType, format->getType())", + "assembler(*this)", + "getFieldU16(sfTransactionType)", + "safe_cast", + "LogicError if type mutated" + ], + "entry_point": "STTx::STTx(TxType type, std::function assembler)", + "purpose": "Constructs a transaction by assembling fields, validates type before and after assembly.", + "validation_points": [ + "getTxFormat: Validates TransactionType against known formats", + "set(format->getSOTemplate()): Enforces template", + "getFieldU16(sfTransactionType) + safe_cast: Validates TransactionType after assembly", + "LogicError: Ensures assembler did not mutate type" + ] + } + ], + "data_flows": [ + { + "field": "sfTransactionType", + "flow": [ + "SerialIter or assembler", + "getFieldU16(sfTransactionType)", + "safe_cast", + "tx_type_ member", + "getTxFormat(tx_type_)" + ], + "origin": "Deserialized from SerialIter or set by assembler", + "transformations": [ + "Deserialized as uint16", + "Casted to TxType enum" + ], + "validated_at": "getTxFormat (throws if invalid type)" + }, + { + "field": "Serialized Transaction Bytes", + "flow": [ + "SerialIter", + "sit.getBytesLeft()", + "length check", + "set(sit)" + ], + "origin": "SerialIter input", + "transformations": [ + "Checked for min/max length", + "Parsed into STObject fields" + ], + "validated_at": "length check in STTx(SerialIter&)" + }, + { + "field": "Transaction Fields (template)", + "flow": [ + "Deserialization or assembler", + "applyTemplate(getTxFormat(tx_type_)->getSOTemplate())" + ], + "origin": "Deserialized or set by assembler", + "transformations": [ + "Checked against SOTemplate for required/optional fields" + ], + "validated_at": "applyTemplate" + }, + { + "field": "Object Terminator", + "flow": [ + "SerialIter", + "set(sit)", + "return value checked" + ], + "origin": "SerialIter input", + "transformations": [ + "If set(sit) returns true, indicates object terminator present" + ], + "validated_at": "Immediately after set(sit)" + } + ], + "description": "Implements the STTx class and related transaction logic for the XRP Ledger, including transaction construction, signing, signature verification, batch transaction handling, and local validation checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "All fields required by the SOTemplate for the transaction type", + "validation", + "missing", + "check" + ], + "evidence": "Field All fields required by the SOTemplate for the transaction type validated by SOTemplate (template-based validation), TxFormats, Throw<> exception handling", + "issue_pattern": "Missing validation for All fields required by the SOTemplate for the transaction type", + "why_false_positive": "SOTemplate (template-based validation), TxFormats, Throw<> exception handling validates All fields required by the SOTemplate for the transaction type automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "TransactionType", + "validation", + "missing", + "check" + ], + "evidence": "Field TransactionType validated by SOTemplate (template-based validation), TxFormats, Throw<> exception handling", + "issue_pattern": "Missing validation for TransactionType", + "why_false_positive": "SOTemplate (template-based validation), TxFormats, Throw<> exception handling validates TransactionType automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Serialized transaction length", + "validation", + "missing", + "check" + ], + "evidence": "Field Serialized transaction length validated by SOTemplate (template-based validation), TxFormats, Throw<> exception handling", + "issue_pattern": "Missing validation for Serialized transaction length", + "why_false_positive": "SOTemplate (template-based validation), TxFormats, Throw<> exception handling validates Serialized transaction length automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "TransactionType", + "empty", + "string", + "validation" + ], + "evidence": "getTxFormat (TxFormats::findByType) at getTxFormat (used in STTx constructors)", + "issue_pattern": "Missing empty string validation for TransactionType", + "why_false_positive": "getTxFormat (TxFormats::findByType) validates TransactionType for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Serialized Transaction Length", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in STTx(SerialIter&) at STTx(SerialIter& sit) constructor", + "issue_pattern": "Missing empty string validation for Serialized Transaction Length", + "why_false_positive": "explicit check in STTx(SerialIter&) validates Serialized Transaction Length for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "Serialized Transaction Length", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check in STTx(SerialIter&) at STTx(SerialIter& sit) constructor", + "issue_pattern": "Missing range validation for Serialized Transaction Length", + "why_false_positive": "explicit check in STTx(SerialIter&) validates Serialized Transaction Length range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Object Terminator", + "empty", + "string", + "validation" + ], + "evidence": "set(sit) return value at STTx(SerialIter& sit) constructor", + "issue_pattern": "Missing empty string validation for Object Terminator", + "why_false_positive": "set(sit) return value validates Object Terminator for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Object Terminator", + "format", + "validation", + "invalid" + ], + "evidence": "set(sit) return value at STTx(SerialIter& sit) constructor", + "issue_pattern": "Missing format validation for Object Terminator", + "why_false_positive": "set(sit) return value validates Object Terminator format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Transaction Fields (template enforcement)", + "empty", + "string", + "validation" + ], + "evidence": "applyTemplate (SOTemplate enforcement) at STTx(STObject&&), STTx(SerialIter&), STTx(TxType, assembler)", + "issue_pattern": "Missing empty string validation for Transaction Fields (template enforcement)", + "why_false_positive": "applyTemplate (SOTemplate enforcement) validates Transaction Fields (template enforcement) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "TransactionType (again, after assembler)", + "empty", + "string", + "validation" + ], + "evidence": "safe_cast(getFieldU16(sfTransactionType)) at STTx(TxType, assembler)", + "issue_pattern": "Missing empty string validation for TransactionType (again, after assembler)", + "why_false_positive": "safe_cast(getFieldU16(sfTransactionType)) validates TransactionType (again, after assembler) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "TransactionType (again, after assembler)", + "type", + "validation", + "check" + ], + "evidence": "safe_cast(getFieldU16(sfTransactionType)) at STTx(TxType, assembler)", + "issue_pattern": "Missing type validation for TransactionType (again, after assembler)", + "why_false_positive": "safe_cast(getFieldU16(sfTransactionType)) validates TransactionType (again, after assembler) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STTx.cpp", + "functions": [ + { + "args": [ + "type" + ], + "lineno": 27, + "name": "getTxFormat" + }, + { + "args": [ + "object" + ], + "lineno": 36, + "name": "STTx::STTx" + }, + { + "args": [ + "sit" + ], + "lineno": 44, + "name": "STTx::STTx" + }, + { + "args": [ + "type", + "assembler" + ], + "lineno": 56, + "name": "STTx::STTx" + }, + { + "args": [ + "n", + "buf" + ], + "lineno": 70, + "name": "STTx::copy" + }, + { + "args": [ + "n", + "buf" + ], + "lineno": 76, + "name": "STTx::move" + }, + { + "args": [], + "lineno": 82, + "name": "STTx::getSType" + }, + { + "args": [], + "lineno": 87, + "name": "STTx::getFullText" + }, + { + "args": [], + "lineno": 96, + "name": "STTx::getMentionedAccounts" + }, + { + "args": [ + "that" + ], + "lineno": 117, + "name": "getSigningData" + }, + { + "args": [], + "lineno": 124, + "name": "STTx::getSigningHash" + }, + { + "args": [ + "sigObject" + ], + "lineno": 128, + "name": "STTx::getSignature" + }, + { + "args": [], + "lineno": 137, + "name": "STTx::getSeqProxy" + }, + { + "args": [], + "lineno": 153, + "name": "STTx::getSeqValue" + }, + { + "args": [], + "lineno": 157, + "name": "STTx::getFeePayer" + }, + { + "args": [ + "publicKey", + "secretKey", + "signatureTarget" + ], + "lineno": 168, + "name": "STTx::sign" + }, + { + "args": [ + "rules", + "sigObject" + ], + "lineno": 182, + "name": "STTx::checkSign" + }, + { + "args": [ + "rules" + ], + "lineno": 196, + "name": "STTx::checkSign" + }, + { + "args": [ + "rules" + ], + "lineno": 210, + "name": "STTx::checkBatchSign" + }, + { + "args": [ + "options" + ], + "lineno": 234, + "name": "STTx::getJson" + }, + { + "args": [ + "options", + "binary" + ], + "lineno": 241, + "name": "STTx::getJson" + }, + { + "args": [], + "lineno": 262, + "name": "STTx::getMetaSQLInsertReplaceHeader" + }, + { + "args": [ + "inLedger", + "escapedMetaData" + ], + "lineno": 272, + "name": "STTx::getMetaSQL" + }, + { + "args": [ + "rawTxn", + "inLedger", + "status", + "escapedMetaData" + ], + "lineno": 278, + "name": "STTx::getMetaSQL" + }, + { + "args": [ + "sigObject", + "data" + ], + "lineno": 293, + "name": "singleSignHelper" + }, + { + "args": [ + "sigObject" + ], + "lineno": 312, + "name": "STTx::checkSingleSign" + }, + { + "args": [ + "batchSigner" + ], + "lineno": 317, + "name": "STTx::checkBatchSingleSign" + }, + { + "args": [ + "sigObject", + "txnAccountID", + "makeMsg", + "rules" + ], + "lineno": 323, + "name": "multiSignHelper" + }, + { + "args": [ + "batchSigner", + "rules" + ], + "lineno": 370, + "name": "STTx::checkBatchMultiSign" + }, + { + "args": [ + "rules", + "sigObject" + ], + "lineno": 382, + "name": "STTx::checkMultiSign" + }, + { + "args": [], + "lineno": 401, + "name": "STTx::getBatchTransactionIDs" + }, + { + "args": [ + "st", + "reason" + ], + "lineno": 434, + "name": "isMemoOkay" + }, + { + "args": [ + "st" + ], + "lineno": 484, + "name": "isAccountFieldOkay" + }, + { + "args": [ + "tx" + ], + "lineno": 494, + "name": "invalidMPTAmountInTx" + }, + { + "args": [ + "st", + "reason" + ], + "lineno": 517, + "name": "isRawTransactionOkay" + }, + { + "args": [ + "st", + "reason" + ], + "lineno": 547, + "name": "passesLocalChecks" + }, + { + "args": [ + "stx" + ], + "lineno": 567, + "name": "sterilize" + }, + { + "args": [ + "tx" + ], + "lineno": 574, + "name": "isPseudoTx" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 25, + "name": "xrpl" + } + ], + "test_coverage_notes": "Core transaction validation is likely covered by unit tests in files such as 'test/tx/Transaction_test.cpp', 'test/protocol/STTx_test.cpp', and integration tests in 'test/app/tx/'. These tests typically cover valid/invalid transaction types, field templates, and serialization/deserialization. However, edge cases such as mutated transaction types during assembly, explicit object terminator handling, and boundary transaction lengths may not be exhaustively tested. There may be limited coverage for error/exception paths (e.g., LogicError, runtime_error throws).", + "validation_architecture": { + "auto_validated_fields": [ + "All fields required by the SOTemplate for the transaction type", + "TransactionType", + "Serialized transaction length" + ], + "framework": "SOTemplate (template-based validation), TxFormats, Throw<> exception handling", + "validation_layer": "entry_point (constructors), business_logic (template enforcement)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "TransactionType", + "location": "getTxFormat (used in STTx constructors)", + "validated_by": "getTxFormat (TxFormats::findByType)", + "validates": [ + "TransactionType is a known/valid type (exists in TxFormats)" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "Serialized Transaction Length", + "location": "STTx(SerialIter& sit) constructor", + "validated_by": "explicit check in STTx(SerialIter&)", + "validates": [ + "Transaction length >= txMinSizeBytes", + "Transaction length <= txMaxSizeBytes" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "Object Terminator", + "location": "STTx(SerialIter& sit) constructor", + "validated_by": "set(sit) return value", + "validates": [ + "Transaction does not contain an object terminator" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "various (may throw on missing/invalid fields)", + "field": "Transaction Fields (template enforcement)", + "location": "STTx(STObject&&), STTx(SerialIter&), STTx(TxType, assembler)", + "validated_by": "applyTemplate (SOTemplate enforcement)", + "validates": [ + "Fields required by transaction type are present", + "Field types and formats match template" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "undefined (safe_cast may assert/throw on invalid cast)", + "field": "TransactionType (again, after assembler)", + "location": "STTx(TxType, assembler)", + "validated_by": "safe_cast(getFieldU16(sfTransactionType))", + "validates": [ + "TransactionType field is a valid enum value" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STTx.cpp.ai.md b/src/libxrpl/protocol/STTx.cpp.ai.md new file mode 100644 index 0000000000..c03dc86cfd --- /dev/null +++ b/src/libxrpl/protocol/STTx.cpp.ai.md @@ -0,0 +1,55 @@ +# `STTx.cpp` — XRPL Transaction Core: Construction, Signing, and Validation + +`STTx` is the canonical representation of an XRP Ledger transaction in the C++ implementation. It inherits from `STObject` but enforces a set of invariants at construction time that a plain `STObject` does not: the transaction type must be registered in `TxFormats`, the field layout must conform to the type's `SOTemplate`, and the 256-bit transaction ID (`tid_`) is computed once from the content and then cached. This file contains all three constructors, the complete signing and signature-verification machinery, local pre-submission validity checks, and SQL persistence helpers. + +## Three Construction Paths, One Set of Invariants + +Every `STTx` enters one of three constructors and all three must terminate with `tid_ = getHash(HashPrefix::transactionID)`. The cached hash is not a convenience — transaction IDs are the primary lookup key across the entire node, so computing them eagerly once at construction eliminates repeated hashing. + +**Wire deserialization** (`STTx(SerialIter&)`) is the hottest path at runtime: every inbound transaction and every ledger transaction loaded from disk passes through it. Before parsing any fields the constructor checks the raw byte count against `txMinSizeBytes` (32 bytes) and `txMaxSizeBytes` (1 MB). Enforcing these bounds before invoking `set(sit)` prevents pathological input from reaching the field-parsing loop. After parsing, `set(sit)` returning `true` means an object terminator was found inside the byte stream, which is structurally invalid for a top-level transaction, so the constructor throws. + +**Object promotion** (`STTx(STObject&&)`) is used when an `STObject` has already been parsed — for example when reconstructing a transaction from JSON. No size check is needed since the object is already in memory; the same `applyTemplate` call enforces field conformance. + +**Programmatic construction** (`STTx(TxType, assembler)`) is used by tests and transaction-building code. The constructor installs the `SOTemplate` first so the object has the correct field scaffolding, then calls the caller-supplied `assembler` lambda, and finally reads back `sfTransactionType` to verify it wasn't mutated. If the type changed during assembly, `LogicError` fires rather than `std::runtime_error` — the distinction matters: `LogicError` signals a programming mistake, not a data error. + +## Signing Architecture + +XRPL supports four distinct signing modes and `STTx` handles all of them. + +**Single signing** is detected by a non-empty `sfSigningPubKey`. The signing payload is the transaction content serialized without the signature fields, prefixed with `HashPrefix::txSign`. The static helper `getSigningData()` produces this payload. Crucially, both single-sign and multi-sign must be mutually exclusive on the same object: `singleSignHelper()` rejects a transaction that has both `sfSigningPubKey` populated and `sfSigners` present, preventing a transaction from being simultaneously signed two ways. + +**Multi-signing** is detected by an empty `sfSigningPubKey` paired with a non-empty `sfSigners` array. The `multiSignHelper()` function processes each signer entry with three enforcements: the transaction owner may not appear as one of their own multi-signers (the `txnAccountID` parameter carries this check), signers must appear in strictly ascending `AccountID` order (no duplicates), and signers must be within the 1–32 range (`STTx::minMultiSigners` / `STTx::maxMultiSigners`). Each signer's actual verification message is constructed by taking the shared prefix (`startMultiSigningData`) and appending the signer's `AccountID` via `finishMultiSigningData`. This per-signer suffix prevents a valid multi-signature from being replayed by a different account in the same or another transaction. + +**Batch signing** (`checkBatchSign()`, `checkBatchSingleSign()`, `checkBatchMultiSign()`) uses a completely different message format. The signed data is produced by `serializeBatch()` — a hash prefix specific to batches, the outer transaction flags, and the IDs of all inner transactions. Batch signers are authorizing a specific set of inner transactions, not the outer envelope fields. This means a batch re-signed with the same inner transactions but different outer flags would not verify against an existing batch signature. + +**Counterparty signing** (`sfCounterpartySignature`, currently used by `LoanSet`) allows a second party to sign the same transaction. The public `checkSign(Rules const&)` overload checks the primary signature and then, if `sfCounterpartySignature` is present, checks it using the same single/multi-sign dispatch. Errors from the counterparty check are prefixed with `"Counterparty: "` so callers can distinguish which signer failed. The `sign()` method accepts an optional `signatureTarget` reference so it can write the counterparty signature into the sub-object rather than the main `sfTxnSignature` field. + +## Fee Delegation + +`getFeePayer()` returns `sfDelegate` if that field is present, otherwise `sfAccount`. The comment in the implementation is architecturally important: the *authorization* for a delegate to act on behalf of the account is enforced separately in `Transactor::checkPermission`, while the *cryptographic validity* of the delegate's signature is enforced in `Transactor::checkSign`. `getFeePayer()` itself does no authorization — it only resolves which account's balance pays the fee. + +## Sequence and Ticket Unification + +`getSeqProxy()` returns a `SeqProxy` that unifies the classic `sfSequence` field with the newer `sfTicketSequence` field. When `sfSequence` is zero and `sfTicketSequence` is present, the transaction uses a ticket. The `SeqProxy` comparison operators guarantee that sequence-type values always sort before ticket-type values, which ensures that transactions that create tickets (sequence-based) sort ahead of transactions that consume them (ticket-based) in processing order. + +## Local Pre-Submission Checks + +`passesLocalChecks()` is a free function rather than an `STTx` method because it operates on any `STObject` — it runs before the object is necessarily promoted to a full `STTx`. It gates local transaction relay and submission: + +- `isMemoOkay()` enforces a 1024-byte total memo size (measured after serialization to catch any encoding overhead) and validates that `MemoType` and `MemoFormat` fields decode from hex and contain only RFC 3986 URL-safe characters. The character whitelist is a `constexpr`-initialized 256-element lookup table, computed once at program start, giving O(1) per-character validation without runtime branching. +- `isAccountFieldOkay()` walks the object's fields looking for any `STAccount` holding the default (zero) value, which would represent an uninitialized account ID. +- `isPseudoTx()` blocks submission of amendment, fee, and UNL-modify transaction types. These are system-generated by the ledger itself and must never arrive from external clients. +- `invalidMPTAmountInTx()` consults the `SOTemplate` for each field's MPT support flag (`soeMPTSupported` vs. `soeMPTNone`). If an `STAmount` or `STIssue` field holds an `MPTIssue` but the template does not declare that field as MPT-capable, the check fails. +- `isRawTransactionOkay()` validates the `sfRawTransactions` array for batch transactions: the array is capped at `maxBatchTxCount` (8), nested `ttBATCH` transactions inside the array are forbidden (no batch-of-batches), and each inner transaction's type must pass `applyTemplate()`. + +## Batch Transaction ID Caching + +`getBatchTransactionIDs()` uses a `mutable std::vector batchTxnIds_` for lazy initialization. The IDs are computed on first call by hashing each entry in `sfRawTransactions` and are never recomputed. An assertion on subsequent calls verifies that the cached vector size still matches the `sfRawTransactions` array size, enforcing the invariant that inner transactions cannot be modified after the IDs have been observed. + +## Sterilization + +`sterilize()` performs a serialize-then-deserialize round-trip: it serializes the `STTx` to bytes via `add()`, then constructs a new `STTx` from those bytes via `SerialIter`. The result is a canonical-form transaction where all equivalent representations collapse to the same byte sequence. This is used when a transaction arrives in a non-canonical in-memory form (for instance, constructed via JSON) and needs to be stored or compared against wire-format transactions. + +## SQL Persistence + +`getMetaSQL()` and `getMetaSQLInsertReplaceHeader()` produce a parameterized SQL row for the `Transactions` database table. The row includes the transaction ID, type name, source account, sequence number, ledger sequence, a single-character status code (`TxnSql` enum), the raw serialized transaction blob, and pre-escaped metadata. The comment marking this as a potential free function elsewhere (`// VFALCO This could be a free function elsewhere`) signals it's considered an architectural oddity — persistence concerns sitting directly on the domain object — but it remains here for historical reasons. \ No newline at end of file diff --git a/src/libxrpl/protocol/STValidation.cpp.ai.json b/src/libxrpl/protocol/STValidation.cpp.ai.json new file mode 100644 index 0000000000..8d0a79ad4c --- /dev/null +++ b/src/libxrpl/protocol/STValidation.cpp.ai.json @@ -0,0 +1,618 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "STValidation::isValid", + "publicKeyType(getSignerPublic())", + "verifyDigest(getSignerPublic(), getSigningHash(), makeSlice(getFieldVL(sfSignature)), ...)" + ], + "entry_point": "STValidation::isValid", + "purpose": "Checks if the validation object is cryptographically valid: correct key type and signature.", + "validation_points": [ + "publicKeyType(getSignerPublic()) == KeyType::secp256k1", + "verifyDigest(...)" + ] + }, + { + "call_chain": [ + "STValidation::validationFormat", + "SOTemplate::SOTemplate" + ], + "entry_point": "STValidation::validationFormat", + "purpose": "Defines and enforces the required/optional fields for a validation object.", + "validation_points": [ + "SOTemplate (field presence/type validation)" + ] + }, + { + "call_chain": [ + "STValidation::STValidation (or deserialization)", + "STObject::STObject", + "SOTemplate::validate" + ], + "entry_point": "STValidation (constructor, deserialization, or add)", + "purpose": "When a validation object is constructed or deserialized, its fields are checked against the template.", + "validation_points": [ + "SOTemplate::validate" + ] + } + ], + "data_flows": [ + { + "field": "sfSigningPubKey", + "flow": [ + "Deserialization (STValidation/STObject)", + "getSignerPublic()", + "publicKeyType(getSignerPublic())", + "verifyDigest(getSignerPublic(), ...)" + ], + "origin": "Input data (serialized validation object)", + "transformations": [ + "Extracted from serialized data", + "Checked for type (secp256k1)", + "Used to verify signature" + ], + "validated_at": "STValidation::isValid (publicKeyType check and verifyDigest)" + }, + { + "field": "sfSignature", + "flow": [ + "Deserialization (STValidation/STObject)", + "getFieldVL(sfSignature)", + "makeSlice(getFieldVL(sfSignature))", + "verifyDigest(..., makeSlice(getFieldVL(sfSignature)), ...)" + ], + "origin": "Input data (serialized validation object)", + "transformations": [ + "Extracted from serialized data", + "Converted to slice for signature verification" + ], + "validated_at": "STValidation::isValid (verifyDigest)" + }, + { + "field": "sfLedgerHash", + "flow": [ + "Deserialization (STValidation/STObject)", + "getFieldH256(sfLedgerHash)", + "getLedgerHash()", + "Used in consensus/ledger logic" + ], + "origin": "Input data (serialized validation object)", + "transformations": [ + "Extracted from serialized data" + ], + "validated_at": "SOTemplate (required field validation)" + }, + { + "field": "All fields in SOTemplate", + "flow": [ + "Deserialization (STValidation/STObject)", + "SOTemplate::validate" + ], + "origin": "Input data (serialized validation object)", + "transformations": [ + "Checked for presence, type, and constraints" + ], + "validated_at": "SOTemplate (via validationFormat)" + } + ], + "description": "Implements methods for the STValidation class in the XRPL protocol, handling validation object serialization, signature verification, and access to validation fields such as hashes, times, and flags.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfFlags", + "validation", + "missing", + "check" + ], + "evidence": "Field sfFlags validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfFlags", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfFlags automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLedgerHash", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLedgerHash validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfLedgerHash", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfLedgerHash automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLedgerSequence", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLedgerSequence validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfLedgerSequence", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfLedgerSequence automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSigningTime", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSigningTime validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfSigningTime", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfSigningTime automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSigningPubKey", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSigningPubKey validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfSigningPubKey", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfSigningPubKey automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSignature", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSignature validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfSignature", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfSignature automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCloseTime", + "validation", + "missing", + "check" + ], + "evidence": "Field sfCloseTime validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfCloseTime", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfCloseTime automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLoadFee", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLoadFee validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfLoadFee", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfLoadFee automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmendments", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAmendments validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfAmendments", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfAmendments automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfBaseFee", + "validation", + "missing", + "check" + ], + "evidence": "Field sfBaseFee validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfBaseFee", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfBaseFee automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfReserveBase", + "validation", + "missing", + "check" + ], + "evidence": "Field sfReserveBase validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfReserveBase", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfReserveBase automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfReserveIncrement", + "validation", + "missing", + "check" + ], + "evidence": "Field sfReserveIncrement validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfReserveIncrement", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfReserveIncrement automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfConsensusHash", + "validation", + "missing", + "check" + ], + "evidence": "Field sfConsensusHash validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfConsensusHash", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfConsensusHash automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCookie", + "validation", + "missing", + "check" + ], + "evidence": "Field sfCookie validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfCookie", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfCookie automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfValidatedHash", + "validation", + "missing", + "check" + ], + "evidence": "Field sfValidatedHash validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfValidatedHash", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfValidatedHash automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfServerVersion", + "validation", + "missing", + "check" + ], + "evidence": "Field sfServerVersion validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfServerVersion", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfServerVersion automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfBaseFeeDrops", + "validation", + "missing", + "check" + ], + "evidence": "Field sfBaseFeeDrops validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfBaseFeeDrops", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfBaseFeeDrops automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfReserveBaseDrops", + "validation", + "missing", + "check" + ], + "evidence": "Field sfReserveBaseDrops validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfReserveBaseDrops", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfReserveBaseDrops automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfReserveIncrementDrops", + "validation", + "missing", + "check" + ], + "evidence": "Field sfReserveIncrementDrops validated by SOTemplate (field presence/format), custom cryptographic checks", + "issue_pattern": "Missing validation for sfReserveIncrementDrops", + "why_false_positive": "SOTemplate (field presence/format), custom cryptographic checks validates sfReserveIncrementDrops automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "STValidation fields (see SOTemplate)", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate (validationFormat) at STValidation::validationFormat", + "issue_pattern": "Missing empty string validation for STValidation fields (see SOTemplate)", + "why_false_positive": "SOTemplate (validationFormat) validates STValidation fields (see SOTemplate) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "signer public key type", + "empty", + "string", + "validation" + ], + "evidence": "publicKeyType(getSignerPublic()) == KeyType::secp256k1 at STValidation::isValid", + "issue_pattern": "Missing empty string validation for signer public key type", + "why_false_positive": "publicKeyType(getSignerPublic()) == KeyType::secp256k1 validates signer public key type for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "signer public key type", + "type", + "validation", + "check" + ], + "evidence": "publicKeyType(getSignerPublic()) == KeyType::secp256k1 at STValidation::isValid", + "issue_pattern": "Missing type validation for signer public key type", + "why_false_positive": "publicKeyType(getSignerPublic()) == KeyType::secp256k1 validates signer public key type type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "signature validity", + "empty", + "string", + "validation" + ], + "evidence": "verifyDigest at STValidation::isValid", + "issue_pattern": "Missing empty string validation for signature validity", + "why_false_positive": "verifyDigest validates signature validity for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STValidation.cpp", + "functions": [ + { + "args": [ + "n", + "buf" + ], + "lineno": 13, + "name": "STValidation::copy" + }, + { + "args": [ + "n", + "buf" + ], + "lineno": 18, + "name": "STValidation::move" + }, + { + "args": [], + "lineno": 23, + "name": "STValidation::validationFormat" + }, + { + "args": [], + "lineno": 48, + "name": "STValidation::getSigningHash" + }, + { + "args": [], + "lineno": 53, + "name": "STValidation::getLedgerHash" + }, + { + "args": [], + "lineno": 58, + "name": "STValidation::getConsensusHash" + }, + { + "args": [], + "lineno": 63, + "name": "STValidation::getSignTime" + }, + { + "args": [], + "lineno": 68, + "name": "STValidation::getSeenTime" + }, + { + "args": [], + "lineno": 73, + "name": "STValidation::isValid" + }, + { + "args": [], + "lineno": 89, + "name": "STValidation::isFull" + }, + { + "args": [], + "lineno": 94, + "name": "STValidation::getSignature" + }, + { + "args": [], + "lineno": 99, + "name": "STValidation::getSerialized" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "STValidation is a core protocol object, so it is likely tested in unit tests under 'src/test/protocol/Validation_test.cpp', 'src/test/protocol/STValidation_test.cpp', or similar. Tests should cover: (1) field presence/absence (SOTemplate validation), (2) signature verification (isValid), (3) key type enforcement, (4) serialization/deserialization. Gaps may exist in edge cases: malformed signatures, non-secp256k1 keys, or missing optional fields. Integration tests may also exercise validation via consensus or ledger acceptance tests.", + "validation_architecture": { + "auto_validated_fields": [ + "sfFlags", + "sfLedgerHash", + "sfLedgerSequence", + "sfSigningTime", + "sfSigningPubKey", + "sfSignature", + "sfCloseTime", + "sfLoadFee", + "sfAmendments", + "sfBaseFee", + "sfReserveBase", + "sfReserveIncrement", + "sfConsensusHash", + "sfCookie", + "sfValidatedHash", + "sfServerVersion", + "sfBaseFeeDrops", + "sfReserveBaseDrops", + "sfReserveIncrementDrops" + ], + "framework": "SOTemplate (field presence/format), custom cryptographic checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Not explicit in this file; likely assertion or error on construction elsewhere", + "field": "STValidation fields (see SOTemplate)", + "location": "STValidation::validationFormat", + "validated_by": "SOTemplate (validationFormat)", + "validates": [ + "Ensures required fields are present (e.g., sfFlags, sfLedgerHash, sfLedgerSequence, sfSigningTime, sfSigningPubKey, sfSignature)", + "Optional/default fields are handled accordingly" + ], + "validation_type": "presence (required/optional/default)" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely aborts or logs error)", + "field": "signer public key type", + "location": "STValidation::isValid", + "validated_by": "publicKeyType(getSignerPublic()) == KeyType::secp256k1", + "validates": [ + "Checks that the public key used for signing is of type secp256k1" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "No explicit exception; returns false if invalid", + "field": "signature validity", + "location": "STValidation::isValid", + "validated_by": "verifyDigest", + "validates": [ + "Verifies that the signature over the signing hash is valid for the given public key", + "Checks for fully canonical signature if vfFullyCanonicalSig flag is set" + ], + "validation_type": "business_logic (cryptographic signature verification)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STValidation.cpp.ai.md b/src/libxrpl/protocol/STValidation.cpp.ai.md new file mode 100644 index 0000000000..3306c64044 --- /dev/null +++ b/src/libxrpl/protocol/STValidation.cpp.ai.md @@ -0,0 +1,47 @@ +# `STValidation.cpp` — Consensus Validation Object Implementation + +`STValidation` represents a single validator's signed assertion that it has agreed to close a specific ledger during the XRPL consensus process. This `.cpp` file implements the non-template, non-inline methods of that class; the constructors and several small accessor inlines live in `STValidation.h` because they are templated or hot-path enough to warrant inlining. + +## Role in the Consensus Pipeline + +Every participating XRPL validator broadcasts `STValidation` messages after each consensus round. A validation carries the hash of the ledger the validator agrees on, when it was signed, optional fee and amendment data the validator wants to advertise, and a cryptographic signature over all of that. The consensus engine accumulates validations from a quorum of trusted validators before it considers a ledger final. Because these objects flow across the network from untrusted peers, they must be verified before being acted on — and verified cheaply, because thousands may arrive per second. + +## Field Schema via `validationFormat()` + +`validationFormat()` returns a `SOTemplate` that declares every field a validation may contain, with its presence rule (`soeREQUIRED`, `soeOPTIONAL`, or `soeDEFAULT`). The template is a function-local static rather than a namespace-scope global. The comment explains why: the `SField` objects it references are themselves globals, and C++ gives no inter-translation-unit initialization order guarantee. A function-local static is initialized on first call, by which point all `SField` singletons are already alive. + +The required fields — `sfFlags`, `sfLedgerHash`, `sfLedgerSequence`, `sfSigningTime`, `sfSigningPubKey`, `sfSignature` — form the non-negotiable nucleus. Optional fields such as `sfLoadFee`, `sfAmendments`, `sfBaseFee`, `sfReserveBase`, and `sfReserveIncrement` carry advisory network-state data validators may publish. Three fields tagged `// featureXRPFees` — `sfBaseFeeDrops`, `sfReserveBaseDrops`, `sfReserveIncrementDrops` — are newer `soeOPTIONAL` entries added by the XRPFees amendment; they coexist with the legacy fee fields to allow gradual adoption without breaking older validators. + +`sfCookie` is declared `soeDEFAULT`, meaning it is always included in the serialized output but takes a default (zero) value if not explicitly set. This prevents fingerprinting validators that omit it while keeping the field unconditionally present for parsing. + +## Lazy, Cached Signature Verification in `isValid()` + +```cpp +mutable std::optional valid_; +``` + +The `valid_` member is unseated (`std::nullopt`) until the first call to `isValid()`. On that call the method verifies the signature and caches the result. Subsequent calls short-circuit to the cached value. This design is deliberate: constructing from a peer stream (`SerialIter`-based constructor) may optionally skip the signature check via the `checkSignature` flag, deferring the cost to the consumer. When the node constructs its own validation (the signing constructor), it sets `valid_ = true` immediately after calling `signDigest`, skipping a redundant round-trip through verification. + +Inside `isValid()`, an `XRPL_ASSERT` first checks that the public key type is `secp256k1`. This is a guard against the key escaping the construction-time check in an unusual code path — if the assert fires, the caller has introduced a logic error. The actual cryptographic check is `verifyDigest()` from `PublicKey.h`, which operates on the 256-bit pre-hashed digest rather than the raw message, because `getSigningHash()` has already applied SHA-512-Half. The `vfFullyCanonicalSig` flag is passed to require strict low-S canonicality, preventing the ECDSA malleability vector. + +## Two Notions of Time + +`getSignTime()` reconstructs a `NetClock::time_point` from the serialized `sfSigningTime` field — this is the time at which the validator signed the message and is part of the validated data. `getSeenTime()` returns `seenTime_`, which is set locally by the receiving node when the validation arrives and is never serialized. This distinction matters for replay-protection and for measuring network latency without trusting the sender's clock. + +## Full vs. Partial Validations + +`isFull()` checks `vfFullValidation` in `sfFlags`. A *full* validation is the definitive affirmation that the validator has observed a complete, validated ledger. A *partial* (or *tentative*) validation may be broadcast during an in-progress consensus round to signal early support for a candidate ledger. The consensus engine treats these differently: only full validations count toward quorum for ledger finality. + +## Signing Hash Namespace + +`getSigningHash()` delegates to `STObject::getSigningHash(HashPrefix::validation)`. The `HashPrefix::validation` constant is the 4-byte big-endian encoding of `'V','A','L',0x00`, prepended to the serialized object before hashing. This domain-separation technique ensures that a byte sequence that is a valid transaction serialization cannot produce the same digest as a validation, and vice versa — a critical defense against cross-type signature reuse attacks. + +## Polymorphic Copy/Move via `emplace()` + +`copy()` and `move()` override pure virtuals from `STBase`. They delegate to the inherited `emplace()` helper, which placement-constructs the object into a caller-supplied buffer of `n` bytes. This pattern supports `STObject`'s value-semantic storage of heterogeneous `ST*` variants without heap allocation per element — a performance-sensitive concern when thousands of serialized objects are being parsed in the critical path. + +## Construction Invariants (from the Header) + +The peer-deserialization constructor (`SerialIter&`) builds the `STObject` from the template, then extracts and validates `sfSigningPubKey` in the initializer list. If the key type is not `secp256k1`, it throws immediately. This key-type check in the initializer list — before the body runs — ensures `signingPubKey_` is never in an invalid state and prevents any code from even reaching `isValid()` with a non-ECDSA key. Similarly, `nodeID_` is initialized with a `lookupNodeID` callable that resolves the ephemeral signing key to a stable master-key-derived NodeID (the manifest system). + +The signing constructor closes the loop: after calling the user-provided fill callback `f(*this)`, it sets the `vfFullyCanonicalSig` flag, computes and stores the signature, marks itself trusted, and iterates over the `SOTemplate` to assert all required fields are present — a construction-time completeness check that catches omissions from the fill callback before the object leaves the constructor. \ No newline at end of file diff --git a/src/libxrpl/protocol/STVar.cpp.ai.json b/src/libxrpl/protocol/STVar.cpp.ai.json new file mode 100644 index 0000000000..3406effaf5 --- /dev/null +++ b/src/libxrpl/protocol/STVar.cpp.ai.json @@ -0,0 +1,409 @@ +{ + "args": [ + { + "lineno": 20, + "name": "other" + }, + { + "lineno": 25, + "name": "other" + }, + { + "lineno": 35, + "name": "rhs" + }, + { + "lineno": 49, + "name": "rhs" + }, + { + "lineno": 63, + "name": "name" + }, + { + "lineno": 66, + "name": "name" + }, + { + "lineno": 69, + "name": "sit" + }, + { + "lineno": 69, + "name": "name" + }, + { + "lineno": 69, + "name": "depth" + }, + { + "lineno": 76, + "name": "id" + }, + { + "lineno": 76, + "name": "name" + }, + { + "lineno": 92, + "name": "id" + }, + { + "lineno": 92, + "name": "depth" + }, + { + "lineno": 92, + "name": "args" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "STVar::STVar(SerialIter&, SField const&, int)", + "if (depth > 10) Throw", + "constructST(name.fieldType, depth, sit, name)" + ], + "entry_point": "STVar::STVar(SerialIter& sit, SField const& name, int depth)", + "purpose": "Constructs an STVar from serialized data, validating nesting depth.", + "validation_points": [ + "depth > 10 check (throws runtime_error)" + ] + }, + { + "call_chain": [ + "STVar::STVar(SerializedTypeID, SField const&)", + "XRPL_ASSERT((id == STI_NOTPRESENT) || (id == name.fieldType))", + "constructST(id, 0, name)" + ], + "entry_point": "STVar::STVar(SerializedTypeID id, SField const& name)", + "purpose": "Constructs an STVar from a type ID and SField, validating type consistency.", + "validation_points": [ + "XRPL_ASSERT macro (id vs name.fieldType)" + ] + }, + { + "call_chain": [ + "STVar::STVar(defaultObject_t, SField const&)", + "STVar::STVar(name.fieldType, name)", + "XRPL_ASSERT((id == STI_NOTPRESENT) || (id == name.fieldType))" + ], + "entry_point": "STVar::STVar(defaultObject_t, SField const& name)", + "purpose": "Constructs a default STVar object, type-checked.", + "validation_points": [ + "XRPL_ASSERT macro (id vs name.fieldType)" + ] + }, + { + "call_chain": [ + "STVar::STVar(nonPresentObject_t, SField const&)", + "STVar::STVar(STI_NOTPRESENT, name)", + "XRPL_ASSERT((id == STI_NOTPRESENT) || (id == name.fieldType))" + ], + "entry_point": "STVar::STVar(nonPresentObject_t, SField const& name)", + "purpose": "Constructs a non-present STVar object, type-checked.", + "validation_points": [ + "XRPL_ASSERT macro (id vs name.fieldType)" + ] + } + ], + "data_flows": [ + { + "field": "depth", + "flow": [ + "constructor parameter", + "depth > 10 check", + "passed to constructST(name.fieldType, depth, sit, name)", + "used in constructST to construct appropriate ST* type" + ], + "origin": "STVar::STVar(SerialIter& sit, SField const& name, int depth) parameter", + "transformations": [ + "Checked for maximum allowed value (10)", + "Passed through to constructST and possibly to ST* constructors" + ], + "validated_at": "Immediately in STVar::STVar(SerialIter&, SField const&, int)" + }, + { + "field": "id (SerializedTypeID)", + "flow": [ + "constructor parameter", + "XRPL_ASSERT((id == STI_NOTPRESENT) || (id == name.fieldType))", + "passed to constructST(id, 0, name)", + "used in switch to select ST* type" + ], + "origin": "STVar::STVar(SerializedTypeID id, SField const& name) parameter", + "transformations": [ + "Type-checked against name.fieldType", + "Used to select which ST* type to construct" + ], + "validated_at": "Immediately in STVar::STVar(SerializedTypeID, SField const&)" + }, + { + "field": "name.fieldType", + "flow": [ + "constructor parameter", + "used in XRPL_ASSERT (id == name.fieldType)", + "passed to constructST", + "used in switch to select ST* type" + ], + "origin": "SField const& name parameter", + "transformations": [ + "Compared to id for type consistency" + ], + "validated_at": "In XRPL_ASSERT in STVar::STVar(SerializedTypeID, SField const&)" + }, + { + "field": "sit (SerialIter)", + "flow": [ + "constructor parameter", + "passed to constructST(name.fieldType, depth, sit, name)", + "used in construction of ST* type" + ], + "origin": "STVar::STVar(SerialIter& sit, SField const& name, int depth) parameter", + "transformations": [ + "Used to deserialize data into ST* type" + ], + "validated_at": "Indirectly, as part of constructST and ST* constructors" + } + ], + "description": "Implements the STVar class and related logic for constructing, copying, moving, and destroying various XRPL serialized types using type-erased storage and runtime type dispatch.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "depth", + "validation", + "missing", + "check" + ], + "evidence": "Field depth validated by Custom (Throw, XRPL_ASSERT)", + "issue_pattern": "Missing validation for depth", + "why_false_positive": "Custom (Throw, XRPL_ASSERT) validates depth automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "SerializedTypeID vs SField::fieldType", + "validation", + "missing", + "check" + ], + "evidence": "Field SerializedTypeID vs SField::fieldType validated by Custom (Throw, XRPL_ASSERT)", + "issue_pattern": "Missing validation for SerializedTypeID vs SField::fieldType", + "why_false_positive": "Custom (Throw, XRPL_ASSERT) validates SerializedTypeID vs SField::fieldType automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "depth", + "empty", + "string", + "validation" + ], + "evidence": "explicit check at STVar::STVar(SerialIter& sit, SField const& name, int depth)", + "issue_pattern": "Missing empty string validation for depth", + "why_false_positive": "explicit check validates depth for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "depth", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check at STVar::STVar(SerialIter& sit, SField const& name, int depth)", + "issue_pattern": "Missing range validation for depth", + "why_false_positive": "explicit check validates depth range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "id (SerializedTypeID) vs name.fieldType", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at STVar::STVar(SerializedTypeID id, SField const& name)", + "issue_pattern": "Missing empty string validation for id (SerializedTypeID) vs name.fieldType", + "why_false_positive": "XRPL_ASSERT macro validates id (SerializedTypeID) vs name.fieldType for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "id (SerializedTypeID) vs name.fieldType", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT macro at STVar::STVar(SerializedTypeID id, SField const& name)", + "issue_pattern": "Missing type validation for id (SerializedTypeID) vs name.fieldType", + "why_false_positive": "XRPL_ASSERT macro validates id (SerializedTypeID) vs name.fieldType type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STVar.cpp", + "functions": [ + { + "args": [], + "lineno": 16, + "name": "STVar::~STVar" + }, + { + "args": [ + "STVar const& other" + ], + "lineno": 19, + "name": "STVar::STVar" + }, + { + "args": [ + "STVar&& other" + ], + "lineno": 24, + "name": "STVar::STVar" + }, + { + "args": [ + "STVar const& rhs" + ], + "lineno": 34, + "name": "STVar::operator=" + }, + { + "args": [ + "STVar&& rhs" + ], + "lineno": 48, + "name": "STVar::operator=" + }, + { + "args": [ + "defaultObject_t", + "SField const& name" + ], + "lineno": 62, + "name": "STVar::STVar" + }, + { + "args": [ + "nonPresentObject_t", + "SField const& name" + ], + "lineno": 65, + "name": "STVar::STVar" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name", + "int depth" + ], + "lineno": 68, + "name": "STVar::STVar" + }, + { + "args": [ + "SerializedTypeID id", + "SField const& name" + ], + "lineno": 75, + "name": "STVar::STVar" + }, + { + "args": [], + "lineno": 83, + "name": "STVar::destroy" + }, + { + "args": [ + "SerializedTypeID id", + "int depth", + "Args&&... args" + ], + "lineno": 91, + "name": "STVar::constructST" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 14, + "name": "detail" + } + ], + "test_coverage_notes": "The validation logic (depth check, XRPL_ASSERT) is critical for preventing malformed or malicious data from causing issues. Typical test coverage would be in protocol serialization/deserialization tests, such as those in 'test/protocol/STObject_test.cpp', 'test/protocol/STVar_test.cpp', or similar. However, if these tests do not explicitly test for depth overflow or mismatched type IDs, those paths may not be fully covered. There is a risk that edge cases (e.g., depth == 11, id != name.fieldType) are not directly tested unless negative/exceptional cases are included in the test suite. Review of test files is needed to confirm coverage of these validation paths.", + "validation_architecture": { + "auto_validated_fields": [ + "depth", + "SerializedTypeID vs SField::fieldType" + ], + "framework": "Custom (Throw, XRPL_ASSERT)", + "validation_layer": "constructor (entry_point for STVar)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "depth", + "location": "STVar::STVar(SerialIter& sit, SField const& name, int depth)", + "validated_by": "explicit check", + "validates": [ + "depth must not exceed 10" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely std::logic_error or abort)", + "field": "id (SerializedTypeID) vs name.fieldType", + "location": "STVar::STVar(SerializedTypeID id, SField const& name)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "id must be STI_NOTPRESENT or match name.fieldType" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STVar.cpp.ai.md b/src/libxrpl/protocol/STVar.cpp.ai.md new file mode 100644 index 0000000000..6530fef260 --- /dev/null +++ b/src/libxrpl/protocol/STVar.cpp.ai.md @@ -0,0 +1,60 @@ +# `STVar.cpp` — Type-Erased Variant Storage for XRPL Serialized Types + +`STVar` is the runtime polymorphic container at the heart of XRPL's serialization system. It acts as a type-erased "variant" that can hold any concrete `STBase`-derived type — `STUInt32`, `STAmount`, `STObject`, `STArray`, and roughly twenty others — without exposing the concrete type to callers. This file provides the out-of-line definitions for `STVar`'s constructors, assignment operators, destructor, and the central dispatch function `constructST`. + +## Small-Object Optimization + +The class stores objects using a two-tier strategy declared in the header: a 72-byte aligned stack buffer (`d_`) and a raw pointer `p_`. The `construct()` template checks `sizeof(T)` at compile time: if the object fits within 72 bytes it is placement-new'd into `d_`; otherwise it is heap-allocated. The `on_heap()` predicate simply compares `p_` against the address of `d_` — if they differ, the object lives on the heap. This avoids a separate boolean flag entirely. + +This optimization matters because `STVar` values appear inside `STObject` and `STArray`, which are themselves stored in vectors. Keeping small serialized types (integers, hashes, short blobs) in the stack buffer eliminates millions of heap allocations during transaction processing and ledger deserialization. + +## Destruction: Two Paths + +`destroy()` encodes the dual storage strategy directly: + +```cpp +if (on_heap()) + delete p_; +else + p_->~STBase(); +``` + +Placement-new requires an explicit destructor call, not `delete` — which would wrongly attempt to free memory from the stack buffer. `delete` is reserved for the heap path. Both paths reset `p_` to `nullptr` to leave the object in a valid empty state. + +## Copy and Move Semantics + +Copy construction delegates to `other.p_->copy(max_size, &d_)` — a virtual method on `STBase`. Each derived type implements `copy()` and `move()` via the shared `emplace()` helper in `STBase`, which itself re-applies the same size check: place into the provided buffer if it fits, otherwise heap-allocate. This means the small-object decision is made independently on each copy, which is correct since the destination buffer is always a fresh `STVar::d_`. + +Move construction handles the two cases differently. If the source object is on the heap, ownership is transferred by pointer swap (`p_ = other.p_; other.p_ = nullptr`) — a zero-copy O(1) move. If it lives in the source's stack buffer, a move-construction into the destination's buffer is necessary via `p_->move(max_size, &d_)`, since buffer addresses are non-transferable. + +## Construction Entry Points and Tag Dispatch + +Four public construction paths exist: + +- **`STVar(SerialIter&, SField const&, int depth)`** — deserialization from a wire-format byte stream. This is the only path that receives and enforces a `depth` limit. +- **`STVar(SerializedTypeID, SField const&)`** — construct a default-valued object of a given type. Used internally. +- **`STVar(defaultObject_t, SField const&)`** — a named-tag wrapper that delegates to the `SerializedTypeID` path using `name.fieldType`. +- **`STVar(nonPresentObject_t, SField const&)`** — constructs a bare `STBase` with `STI_NOTPRESENT`, representing a field that exists in the schema but is absent from a specific object. + +The `defaultObject_t` and `nonPresentObject_t` tag structs — defined as empty types with explicit constructors — exist solely to disambiguate these construction semantics at call sites without relying on overloading by type ID value. + +## `constructST`: The Type Dispatch Core + +`constructST` is a variadic template constrained by the `ValidConstructSTArgs` concept, which restricts the argument pack to exactly one of two forms: `(SField)` for default construction, or `(SerialIter, SField)` for deserialization. This compile-time narrowing ensures that downstream `construct(args...)` calls will always resolve to valid `ST*` constructors. + +The function dispatches on `SerializedTypeID` via a `switch`, calling `construct` for the matching concrete type. The vast majority of cases forward args directly. `STI_OBJECT` and `STI_ARRAY` are the exception: because `STObject` and `STArray` are themselves containers of `STVar` values, their deserialization constructors recursively create more `STVar` objects. To prevent unbounded recursion on maliciously crafted or corrupt data, these two types receive `depth` as an additional constructor argument. The `constructWithDepth` lambda uses `if constexpr` to select between the `(SField)` and `(SerialIter, SField, depth)` calling conventions at compile time. + +## Nesting Depth Guard + +The deserialization constructor enforces a hard limit of 10 levels of nesting: + +```cpp +if (depth > 10) + Throw("Maximum nesting depth of STVar exceeded"); +``` + +This is a security boundary. Without it, a crafted ledger object with deeply nested `STObject` or `STArray` fields could cause stack exhaustion. The depth counter is incremented by each recursive `STObject`/`STArray` constructor call and passed down through `constructST`. + +## Relationship to Surrounding Code + +`STVar` lives in `xrpl::detail` — it is an implementation detail, not part of the public API surface. Callers inside `STObject` and `STArray` use it as their element type, and `make_stvar(args...)` (defined inline in the header) is the friend-function factory for creating `STVar` instances of a known concrete type without going through the type-ID dispatch. The virtual `copy()` and `move()` methods on `STBase` form the contract that each derived serialized type must fulfil to participate in `STVar`'s ownership model. \ No newline at end of file diff --git a/src/libxrpl/protocol/STVector256.cpp.ai.json b/src/libxrpl/protocol/STVector256.cpp.ai.json new file mode 100644 index 0000000000..f8d88683c7 --- /dev/null +++ b/src/libxrpl/protocol/STVector256.cpp.ai.json @@ -0,0 +1,296 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 13, + "name": "STVector256" + } + ], + "code_paths": [ + { + "call_chain": [ + "STVector256::STVector256", + "sit.getVLDataLength", + "sit.getSlice", + "Throw (if invalid)", + "mValue.emplace_back" + ], + "entry_point": "STVector256(SerialIter& sit, SField const& name)", + "purpose": "Constructs an STVector256 from serialized data, validates VL data length, and populates mValue.", + "validation_points": [ + "STVector256::STVector256: Checks if slice.size() % uint256::size() != 0, throws if invalid" + ] + }, + { + "call_chain": [ + "STVector256::add", + "XRPL_ASSERT(getFName().isBinary())", + "XRPL_ASSERT(getFName().fieldType == STI_VECTOR256)", + "s.addVL" + ], + "entry_point": "STVector256::add(Serializer& s) const", + "purpose": "Serializes the STVector256, validating field type and binary property before serialization.", + "validation_points": [ + "STVector256::add: XRPL_ASSERT(getFName().isBinary())", + "STVector256::add: XRPL_ASSERT(getFName().fieldType == STI_VECTOR256)" + ] + } + ], + "data_flows": [ + { + "field": "VL data (slice)", + "flow": [ + "SerialIter (input buffer)", + "getVLDataLength() (gets length)", + "getSlice() (extracts data)", + "Validation: slice.size() % uint256::size()", + "mValue.emplace_back (stores as uint256)" + ], + "origin": "SerialIter::getSlice(sit.getVLDataLength())", + "transformations": [ + "Extracted from serialized buffer", + "Checked for correct length (multiple of 32 bytes)", + "Split into 32-byte chunks and stored as uint256" + ], + "validated_at": "STVector256::STVector256 (constructor)" + }, + { + "field": "Field type and binary property", + "flow": [ + "getFName()", + "isBinary()", + "fieldType", + "XRPL_ASSERT checks in add()" + ], + "origin": "getFName() (SField)", + "transformations": [ + "Checked for binary property", + "Checked for correct field type (STI_VECTOR256)" + ], + "validated_at": "STVector256::add" + } + ], + "description": "Implements the STVector256 class for handling arrays of 256-bit values (uint256) in the XRPL protocol, including serialization, deserialization, JSON conversion, and comparison.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "VL data length (slice.size())", + "empty", + "string", + "validation" + ], + "evidence": "manual check with exception at STVector256(SerialIter& sit, SField const& name) constructor", + "issue_pattern": "Missing empty string validation for VL data length (slice.size())", + "why_false_positive": "manual check with exception validates VL data length (slice.size()) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "VL data length (slice.size())", + "format", + "validation", + "invalid" + ], + "evidence": "manual check with exception at STVector256(SerialIter& sit, SField const& name) constructor", + "issue_pattern": "Missing format validation for VL data length (slice.size())", + "why_false_positive": "manual check with exception validates VL data length (slice.size()) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "field is binary", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at STVector256::add", + "issue_pattern": "Missing empty string validation for field is binary", + "why_false_positive": "XRPL_ASSERT macro validates field is binary for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "field is binary", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT macro at STVector256::add", + "issue_pattern": "Missing type validation for field is binary", + "why_false_positive": "XRPL_ASSERT macro validates field is binary type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "field type is STI_VECTOR256", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at STVector256::add", + "issue_pattern": "Missing empty string validation for field type is STI_VECTOR256", + "why_false_positive": "XRPL_ASSERT macro validates field type is STI_VECTOR256 for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "field type is STI_VECTOR256", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT macro at STVector256::add", + "issue_pattern": "Missing type validation for field type is STI_VECTOR256", + "why_false_positive": "XRPL_ASSERT macro validates field type is STI_VECTOR256 type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STVector256.cpp", + "functions": [ + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 13, + "name": "STVector256" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 29, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 34, + "name": "move" + }, + { + "args": [], + "lineno": 39, + "name": "getSType" + }, + { + "args": [], + "lineno": 44, + "name": "isDefault" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 49, + "name": "add" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 56, + "name": "isEquivalent" + }, + { + "args": [ + "JsonOptions" + ], + "lineno": 62, + "name": "getJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage is not shown in this file. Typically, STVector256 is tested in protocol serialization/deserialization and transaction processing tests. Likely test files: 'test/protocol/STVector256_test.cpp', 'test/protocol/SerializedType_test.cpp', or integration tests involving ledger objects with vector256 fields. Gaps: Direct negative tests for invalid VL length (not multiple of 32) and field type/binary property assertions may be missing if not explicitly tested for exceptions/assertions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Manual validation, XRPL_ASSERT macro, Throw<> template for exceptions", + "validation_layer": "constructor (input/format validation), business_logic (add method type checks)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<> template)", + "field": "VL data length (slice.size())", + "location": "STVector256(SerialIter& sit, SField const& name) constructor", + "validated_by": "manual check with exception", + "validates": [ + "Checks that the serialized data length is a multiple of uint256::size() (32 bytes)", + "Ensures the input data can be parsed into an integral number of 256-bit values" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "assertion failure (likely contract violation, may throw or abort)", + "field": "field is binary", + "location": "STVector256::add", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that the SField associated with this object is marked as binary" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "assertion failure (likely contract violation, may throw or abort)", + "field": "field type is STI_VECTOR256", + "location": "STVector256::add", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that the SField type matches STI_VECTOR256" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STVector256.cpp.ai.md b/src/libxrpl/protocol/STVector256.cpp.ai.md new file mode 100644 index 0000000000..f526b036be --- /dev/null +++ b/src/libxrpl/protocol/STVector256.cpp.ai.md @@ -0,0 +1,61 @@ +# `STVector256.cpp` — Serialized Array of 256-bit Hash Values + +`STVector256` is the XRPL serialized type for ordered lists of `uint256` values. On the wire it carries type identifier `STI_VECTOR256` (code 19) and appears throughout the ledger wherever a field must hold multiple 256-bit hashes — most visibly in `sfAmendments` (active feature flags), `sfIndexes` (directory-node page entries), and `sfHashes` (ledger history lists). The `.cpp` file implements the non-trivial methods; all simple accessors and mutations are inlined in the accompanying header. + +## Class Design + +`STVector256` inherits from two bases: `STBase` provides the named-field identity, `Serializer`/`SerialIter` integration, and the placement-new polymorphism contract; `CountedObject` hooks into the diagnostics subsystem so live instance counts can be monitored at runtime. The only data member is `mValue`, a `std::vector`, deliberately private with the full `std::vector` API surfaced as thin inline forwarding wrappers. This layering lets callers treat the object as a first-class container while keeping the `STBase` invariant (only `STVar` and friend classes touch `copy`/`move`) intact. + +## Deserialization Constructor + +The most complex method in the `.cpp` is the `SerialIter`-based constructor: + +```cpp +STVector256::STVector256(SerialIter& sit, SField const& name) : STBase(name) +{ + auto const slice = sit.getSlice(sit.getVLDataLength()); + if (slice.size() % uint256::size() != 0) + Throw(...); + ... +} +``` + +The binary wire format stores the entire array as a single variable-length (VL-prefixed) blob — a length-prefix encoding shared by all variable-size fields. `getVLDataLength()` reads and decodes the prefix, then `getSlice()` returns a `Slice` into that many bytes of the stream. The guard that follows is the critical validation: if the blob's size is not an exact multiple of 32, the data is corrupt or truncated, and the constructor throws `std::runtime_error` rather than silently producing a partial array. On success, the constructor reserves capacity once and fills `mValue` in a single forward pass by constructing each `uint256` from a 32-byte sub-slice. + +The choice to `Throw` here (versus an assertion) is deliberate: this path is exercised on untrusted network data during ledger deserialization. A mismatched size is a protocol violation that must be surfaced as an exception so the calling peer-management code can close the offending connection rather than crash. + +## Serialization — `add()` + +```cpp +void STVector256::add(Serializer& s) const +{ + XRPL_ASSERT(getFName().isBinary(), ...); + XRPL_ASSERT(getFName().fieldType == STI_VECTOR256, ...); + s.addVL(mValue.begin(), mValue.end(), mValue.size() * (256 / 8)); +} +``` + +`addVL` writes the total byte count as a VL prefix followed by the raw bytes produced by iterating over each `uint256`. The two `XRPL_ASSERT` guards enforce that the `SField` this object was constructed with is actually a binary, `VECTOR256`-typed field. These catch programmer errors — such as accidentally assigning a `STVector256` value to an `STBlob` field — early in debug builds, before bad data reaches the wire or the canonical hash computation. + +## Copy/Move Placement Protocol + +`copy()` and `move()` forward to `STBase::emplace()`, the template that implements the placement-new-or-heap pattern used by `STVar`: + +```cpp +STBase* STVector256::copy(std::size_t n, void* buf) const { return emplace(n, buf, *this); } +STBase* STVector256::move(std::size_t n, void* buf) { return emplace(n, buf, std::move(*this)); } +``` + +When the caller has pre-allocated a buffer large enough for an `STVector256`, `emplace` constructs in-place to avoid a heap allocation. When the buffer is too small it falls back to `new`. This matters because `STVar` — the type-erased wrapper used inside `STObject` to hold heterogeneous ST fields — relies on this protocol to keep small scalar fields on the stack while allowing larger or dynamically-sized types like `STVector256` to heap-allocate transparently. + +## Default Value Semantics + +`isDefault()` returns `true` when `mValue` is empty. In XRPL's canonical serialization rules, default-valued fields are omitted from the wire encoding, so an empty `STVector256` contributes nothing to a transaction hash. This is consistent with how all ST types behave — the absence of a field and an explicitly empty field have the same canonical representation. + +## JSON Output + +`getJson()` produces a `Json::arrayValue` where each element is the hex string of its `uint256`, via `to_string()`. The `JsonOptions` parameter is accepted but not consumed; `STVector256` has no output variants that depend on API version or date flags. The resulting JSON array is what appears in RPC responses for fields like `sfAmendments`. + +## Relationship to Sibling ST Types + +Within the `libxrpl/protocol` module, `STVector256` is one of the narrower concrete `STBase` specializations. Unlike `STBlob`, which holds arbitrary byte sequences, `STVector256` is exclusively typed for hash-sized values — this specificity is what justifies a distinct wire type rather than reusing `STBlob`. Fields that need a single hash use `STBitString<256>` (`uint256` directly); `STVector256` exists precisely for the multi-hash case. The `SF_VECTOR256` typedef in `SField.h` provides the typed-field wrapper that lets `STObject::getFieldV256()` and `setFieldV256()` give compile-time type safety at field-access sites. \ No newline at end of file diff --git a/src/libxrpl/protocol/STXChainBridge.cpp.ai.json b/src/libxrpl/protocol/STXChainBridge.cpp.ai.json new file mode 100644 index 0000000000..0427dfac53 --- /dev/null +++ b/src/libxrpl/protocol/STXChainBridge.cpp.ai.json @@ -0,0 +1,669 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "STXChainBridge" + } + ], + "code_paths": [ + { + "call_chain": [ + "STXChainBridge(Json::Value const& v)", + "STXChainBridge(SField const&, Json::Value const& v)" + ], + "entry_point": "STXChainBridge(Json::Value const& v)", + "purpose": "Constructs an STXChainBridge object from a JSON value, performing validation on the input.", + "validation_points": [ + "v.isObject()", + "checkExtra(v) (lambda: checks for extra/unexpected fields)", + "lockingChainDoorStr.isString()", + "issuingChainDoorStr.isString()", + "parseBase58(lockingChainDoorStr.asString())", + "parseBase58(issuingChainDoorStr.asString())" + ] + }, + { + "call_chain": [ + "STXChainBridge(STObject const& o)" + ], + "entry_point": "STXChainBridge(STObject const& o)", + "purpose": "Constructs an STXChainBridge from an STObject (likely from binary or trusted source).", + "validation_points": [] + }, + { + "call_chain": [ + "STXChainBridge(SerialIter& sit, SField const& name)" + ], + "entry_point": "STXChainBridge(SerialIter& sit, SField const& name)", + "purpose": "Constructs an STXChainBridge from serialized binary data.", + "validation_points": [] + }, + { + "call_chain": [ + "STXChainBridge(AccountID const&, Issue const&, AccountID const&, Issue const&)" + ], + "entry_point": "STXChainBridge(AccountID const&, Issue const&, AccountID const&, Issue const&)", + "purpose": "Constructs an STXChainBridge from explicit field values (likely internal use).", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "lockingChainDoor", + "flow": [ + "JSON input", + "lockingChainDoorStr = v[jss::LockingChainDoor]", + "parseBase58(lockingChainDoorStr.asString())", + "lockingChainDoor_ = STAccount{sfLockingChainDoor, *lockingChainDoor}" + ], + "origin": "Json::Value v[jss::LockingChainDoor]", + "transformations": [ + "JSON string \u2192 AccountID (via parseBase58)", + "AccountID \u2192 STAccount" + ], + "validated_at": "lockingChainDoorStr.isString(), parseBase58" + }, + { + "field": "lockingChainIssue", + "flow": [ + "JSON input", + "lockingChainIssue = v[jss::LockingChainIssue]", + "issueFromJson(lockingChainIssue)", + "lockingChainIssue_ = STIssue{sfLockingChainIssue, ...}" + ], + "origin": "Json::Value v[jss::LockingChainIssue]", + "transformations": [ + "JSON \u2192 Issue (via issueFromJson)", + "Issue \u2192 STIssue" + ], + "validated_at": "issueFromJson (not shown in this file, assumed to validate)" + }, + { + "field": "issuingChainDoor", + "flow": [ + "JSON input", + "issuingChainDoorStr = v[jss::IssuingChainDoor]", + "parseBase58(issuingChainDoorStr.asString())", + "issuingChainDoor_ = STAccount{sfIssuingChainDoor, *issuingChainDoor}" + ], + "origin": "Json::Value v[jss::IssuingChainDoor]", + "transformations": [ + "JSON string \u2192 AccountID (via parseBase58)", + "AccountID \u2192 STAccount" + ], + "validated_at": "issuingChainDoorStr.isString(), parseBase58" + }, + { + "field": "issuingChainIssue", + "flow": [ + "JSON input", + "issuingChainIssue = v[jss::IssuingChainIssue]", + "issueFromJson(issuingChainIssue)", + "issuingChainIssue_ = STIssue{sfIssuingChainIssue, ...}" + ], + "origin": "Json::Value v[jss::IssuingChainIssue]", + "transformations": [ + "JSON \u2192 Issue (via issueFromJson)", + "Issue \u2192 STIssue" + ], + "validated_at": "issueFromJson (not shown in this file, assumed to validate)" + } + ], + "description": "Implements the STXChainBridge class, which represents a cross-chain bridge structure in the XRPL protocol, providing serialization, deserialization, JSON conversion, and utility methods.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "LockingChainDoor", + "validation", + "missing", + "check" + ], + "evidence": "Field LockingChainDoor validated by xrpl::jss (JSON field names), parseBase58, Throw<> (exception handling)", + "issue_pattern": "Missing validation for LockingChainDoor", + "why_false_positive": "xrpl::jss (JSON field names), parseBase58, Throw<> (exception handling) validates LockingChainDoor automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "IssuingChainDoor", + "validation", + "missing", + "check" + ], + "evidence": "Field IssuingChainDoor validated by xrpl::jss (JSON field names), parseBase58, Throw<> (exception handling)", + "issue_pattern": "Missing validation for IssuingChainDoor", + "why_false_positive": "xrpl::jss (JSON field names), parseBase58, Throw<> (exception handling) validates IssuingChainDoor automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "LockingChainIssue", + "validation", + "missing", + "check" + ], + "evidence": "Field LockingChainIssue validated by xrpl::jss (JSON field names), parseBase58, Throw<> (exception handling)", + "issue_pattern": "Missing validation for LockingChainIssue", + "why_false_positive": "xrpl::jss (JSON field names), parseBase58, Throw<> (exception handling) validates LockingChainIssue automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "IssuingChainIssue", + "validation", + "missing", + "check" + ], + "evidence": "Field IssuingChainIssue validated by xrpl::jss (JSON field names), parseBase58, Throw<> (exception handling)", + "issue_pattern": "Missing validation for IssuingChainIssue", + "why_false_positive": "xrpl::jss (JSON field names), parseBase58, Throw<> (exception handling) validates IssuingChainIssue automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Json::Value v (constructor input)", + "empty", + "string", + "validation" + ], + "evidence": "v.isObject() at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing empty string validation for Json::Value v (constructor input)", + "why_false_positive": "v.isObject() validates Json::Value v (constructor input) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "Json::Value v (constructor input)", + "type", + "validation", + "check" + ], + "evidence": "v.isObject() at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing type validation for Json::Value v (constructor input)", + "why_false_positive": "v.isObject() validates Json::Value v (constructor input) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Json::Value v (constructor input)", + "empty", + "string", + "validation" + ], + "evidence": "checkExtra lambda (compares v's keys to bridgeJson template) at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing empty string validation for Json::Value v (constructor input)", + "why_false_positive": "checkExtra lambda (compares v's keys to bridgeJson template) validates Json::Value v (constructor input) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "LockingChainDoor", + "empty", + "string", + "validation" + ], + "evidence": "lockingChainDoorStr.isString() at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing empty string validation for LockingChainDoor", + "why_false_positive": "lockingChainDoorStr.isString() validates LockingChainDoor for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "LockingChainDoor", + "type", + "validation", + "check" + ], + "evidence": "lockingChainDoorStr.isString() at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing type validation for LockingChainDoor", + "why_false_positive": "lockingChainDoorStr.isString() validates LockingChainDoor type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "IssuingChainDoor", + "empty", + "string", + "validation" + ], + "evidence": "issuingChainDoorStr.isString() at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing empty string validation for IssuingChainDoor", + "why_false_positive": "issuingChainDoorStr.isString() validates IssuingChainDoor for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "IssuingChainDoor", + "type", + "validation", + "check" + ], + "evidence": "issuingChainDoorStr.isString() at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing type validation for IssuingChainDoor", + "why_false_positive": "issuingChainDoorStr.isString() validates IssuingChainDoor type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "LockingChainDoor (string value)", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing empty string validation for LockingChainDoor (string value)", + "why_false_positive": "parseBase58 validates LockingChainDoor (string value) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "LockingChainDoor (string value)", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing format validation for LockingChainDoor (string value)", + "why_false_positive": "parseBase58 validates LockingChainDoor (string value) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "IssuingChainDoor (string value)", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing empty string validation for IssuingChainDoor (string value)", + "why_false_positive": "parseBase58 validates IssuingChainDoor (string value) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "IssuingChainDoor (string value)", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at STXChainBridge(SField const& name, Json::Value const& v)", + "issue_pattern": "Missing format validation for IssuingChainDoor (string value)", + "why_false_positive": "parseBase58 validates IssuingChainDoor (string value) format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/STXChainBridge.cpp", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "STXChainBridge" + }, + { + "args": [ + "SField const& name" + ], + "lineno": 17, + "name": "STXChainBridge" + }, + { + "args": [ + "AccountID const& srcChainDoor", + "Issue const& srcChainIssue", + "AccountID const& dstChainDoor", + "Issue const& dstChainIssue" + ], + "lineno": 21, + "name": "STXChainBridge" + }, + { + "args": [ + "STObject const& o" + ], + "lineno": 30, + "name": "STXChainBridge" + }, + { + "args": [ + "Json::Value const& v" + ], + "lineno": 38, + "name": "STXChainBridge" + }, + { + "args": [ + "SField const& name", + "Json::Value const& v" + ], + "lineno": 41, + "name": "STXChainBridge" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 77, + "name": "STXChainBridge" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 85, + "name": "add" + }, + { + "args": [ + "JsonOptions jo" + ], + "lineno": 92, + "name": "getJson" + }, + { + "args": [], + "lineno": 101, + "name": "getText" + }, + { + "args": [], + "lineno": 108, + "name": "toSTObject" + }, + { + "args": [], + "lineno": 116, + "name": "getSType" + }, + { + "args": [ + "STBase const& t" + ], + "lineno": 120, + "name": "isEquivalent" + }, + { + "args": [], + "lineno": 126, + "name": "isDefault" + }, + { + "args": [ + "SerialIter& sit", + "SField const& name" + ], + "lineno": 131, + "name": "construct" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 136, + "name": "copy" + }, + { + "args": [ + "std::size_t n", + "void* buf" + ], + "lineno": 141, + "name": "move" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is in the constructor STXChainBridge(Json::Value const& v), which checks JSON structure, field presence, type, and value validity. Test coverage should exist in unit tests for bridge construction from JSON, including negative tests for invalid types, missing fields, extra fields, and invalid account strings. Look for tests in files like test/protocol/STXChainBridge_test.cpp or similar. Gaps may exist if issueFromJson is not thoroughly tested, or if tests do not cover all invalid input permutations (e.g., extra fields, wrong types, invalid base58). No explicit test references are present in this file.", + "validation_architecture": { + "auto_validated_fields": [ + "LockingChainDoor", + "IssuingChainDoor", + "LockingChainIssue", + "IssuingChainIssue" + ], + "framework": "xrpl::jss (JSON field names), parseBase58, Throw<> (exception handling)", + "validation_layer": "entry_point (constructor input validation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "Json::Value v (constructor input)", + "location": "STXChainBridge(SField const& name, Json::Value const& v)", + "validated_by": "v.isObject()", + "validates": [ + "Checks that the input JSON value is an object" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "Json::Value v (constructor input)", + "location": "STXChainBridge(SField const& name, Json::Value const& v)", + "validated_by": "checkExtra lambda (compares v's keys to bridgeJson template)", + "validates": [ + "Checks that no extra/unexpected fields are present in the input JSON object" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "LockingChainDoor", + "location": "STXChainBridge(SField const& name, Json::Value const& v)", + "validated_by": "lockingChainDoorStr.isString()", + "validates": [ + "Checks that LockingChainDoor field in JSON is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "IssuingChainDoor", + "location": "STXChainBridge(SField const& name, Json::Value const& v)", + "validated_by": "issuingChainDoorStr.isString()", + "validates": [ + "Checks that IssuingChainDoor field in JSON is a string" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "parseBase58 throws on invalid format (likely std::runtime_error or similar)", + "field": "LockingChainDoor (string value)", + "location": "STXChainBridge(SField const& name, Json::Value const& v)", + "validated_by": "parseBase58", + "validates": [ + "Checks that LockingChainDoor string is a valid base58-encoded AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "parseBase58 throws on invalid format (likely std::runtime_error or similar)", + "field": "IssuingChainDoor (string value)", + "location": "STXChainBridge(SField const& name, Json::Value const& v)", + "validated_by": "parseBase58", + "validates": [ + "Checks that IssuingChainDoor string is a valid base58-encoded AccountID" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/STXChainBridge.cpp.ai.md b/src/libxrpl/protocol/STXChainBridge.cpp.ai.md new file mode 100644 index 0000000000..d9c0e8c3ff --- /dev/null +++ b/src/libxrpl/protocol/STXChainBridge.cpp.ai.md @@ -0,0 +1,54 @@ +# STXChainBridge.cpp + +## Role and Purpose + +`STXChainBridge` is the serialized protocol type that encodes a cross-chain bridge specification in the XRPL. A bridge is defined by exactly four pieces of information: the *door account* on the locking chain, the *asset* locked on that chain, the *door account* on the issuing chain, and the *wrapped asset* minted on that chain. This file implements the construction, serialization, deserialization, and JSON conversion of that four-tuple as a first-class `STBase`-derived field — meaning it participates in the same wire-format and field-tag infrastructure as every other serialized type in the ledger (amounts, accounts, blobs, etc.). + +## Class Hierarchy and Field Architecture + +`STXChainBridge` inherits from `STBase`, the root of all "Serialized Type" objects. `STBase` is a polymorphic type identified by an `SField` tag and participating in the ledger's binary serialization protocol. It also inherits from `CountedObject` for diagnostic instance tracking. + +The four member fields are themselves `STBase`-derived types: + +- `lockingChainDoor_` and `issuingChainDoor_` — typed `STAccount`, wrapping an `AccountID` +- `lockingChainIssue_` and `issuingChainIssue_` — typed `STIssue`, wrapping an `Issue` (currency + issuer pair) + +Each field is associated with a named `SField` constant (`sfLockingChainDoor`, `sfIssuingChainDoor`, etc.) rather than a positional index. This design reflects the XRPL's general approach: every serialized field carries an identity tag that appears in the wire format, making the binary representation self-describing and robust to field reordering. + +## Construction Paths and Validation Tiers + +The class exposes six construction paths representing three trust levels: + +**Trusted / internal construction**: the `(AccountID, Issue, AccountID, Issue)` overload and the `STObject const&` overload perform no validation beyond member initialization. These are used when data is known valid — for example, when materializing a bridge object from already-validated ledger state. + +**Binary deserialization**: `STXChainBridge(SerialIter& sit, SField const& name)` reads the four fields sequentially from a binary stream, delegating validation to the underlying `STAccount` and `STIssue` deserialization. The `static construct()` factory wrapping this constructor is the hook called by the `STVar` dispatch table in `STVar.cpp`, which maps `STI_XCHAIN_BRIDGE` to this type during wire-format decoding. + +**JSON construction**: `STXChainBridge(SField const&, Json::Value const&)` is the most defensive path and the entry point for RPC/API input. It applies a layered validation strategy: +1. Checks that the JSON value is an object (`v.isObject()`). +2. Runs a `checkExtra` lambda that constructs a default-initialized bridge, serializes it to JSON, and compares the expected key set against the input — any unknown field name triggers an exception. This prevents silent discard of typo'd or future fields. +3. Validates that both door fields are JSON strings, then decodes them with `parseBase58`, returning `std::nullopt` for any malformed address rather than throwing internally. +4. Delegates issue parsing to `issueFromJson()`. + +All failures throw `std::runtime_error` via the `Throw<>` wrapper (from `contract.h`), which consistently signals protocol-layer parse errors. + +## Serialization: `add()` and `toSTObject()` + +`add(Serializer&)` serializes the four fields in declaration order — locking door, locking issue, issuing door, issuing issue. Because `STXChainBridge` is itself a field (not a container like `STObject`), no length prefix or inner field-type tags are written here; the outer framing is already in place from `addFieldID()` called by the enclosing `STObject`. + +`toSTObject()` exists as a bridge (pun aside) between this type and the generic `STObject` container. It constructs a fresh `STObject` tagged `sfXChainBridge` and copies the four sub-fields into it. This is needed wherever the ledger's generic object processing pipeline requires a flat `STObject` rather than the strongly-typed `STXChainBridge`. + +## Memory Management: `copy()` and `move()` + +The two private overrides satisfy the `STBase` small-buffer-optimization interface. `STVar` — the type-erased container used inside `STObject` and `STArray` — maintains an aligned internal buffer. If the concrete type fits within `max_size` bytes, it is placement-constructed into that buffer; otherwise it heap-allocates. The `emplace()` helper in `STBase` encapsulates this decision: given a size hint and a raw buffer, it either placement-news or heap-news the concrete object. `copy()` forwards to `emplace(n, buf, *this)` (copy-constructs) and `move()` forwards to `emplace(n, buf, std::move(*this))` (move-constructs). This pattern avoids virtual dispatch overhead for the common case where types are small enough to fit in-place. + +## The `ChainType` Enum + +The header defines `ChainType { locking, issuing }` and three static helpers — `otherChain()`, `srcChain(bool)`, `dstChain(bool)` — as inline functions. These allow callers dealing with cross-chain transaction processing to write direction-agnostic code by parameterizing on chain role rather than always naming locking or issuing explicitly. The `door(ChainType)` and `issue(ChainType)` accessors extend this polymorphism to field access. + +## Equivalence and Default Detection + +`isEquivalent()` uses `dynamic_cast` to ensure type identity before invoking the field-tuple comparison defined by `operator==` in the header. The `operator==` itself compares all four sub-fields via `std::tie`, which delegates to each field's own equality. `isDefault()` returns true only when all four members are in their field-default state — this is the hook used by serialization to skip optional fields that were never populated. + +## Integration in the Type Registry + +`STVar.cpp` registers `STXChainBridge` in the central `constructST()` switch via `case STI_XCHAIN_BRIDGE`. This means the general-purpose binary deserializer can construct a bridge value from a `SerialIter` by type ID alone, treating it identically to `STAmount`, `STAccount`, or any other primitive type. The `getSType()` override returning `STI_XCHAIN_BRIDGE` provides the runtime type tag that drives this dispatch. \ No newline at end of file diff --git a/src/libxrpl/protocol/SecretKey.cpp.ai.json b/src/libxrpl/protocol/SecretKey.cpp.ai.json new file mode 100644 index 0000000000..b3a3ffd261 --- /dev/null +++ b/src/libxrpl/protocol/SecretKey.cpp.ai.json @@ -0,0 +1,362 @@ +{ + "args": [ + { + "lineno": 22, + "name": "key" + }, + { + "lineno": 27, + "name": "slice" + }, + { + "lineno": 41, + "name": "out" + }, + { + "lineno": 41, + "name": "v" + }, + { + "lineno": 48, + "name": "seed" + }, + { + "lineno": 81, + "name": "seq" + }, + { + "lineno": 121, + "name": "ordinal" + }, + { + "lineno": 139, + "name": "pk" + }, + { + "lineno": 139, + "name": "sk" + }, + { + "lineno": 139, + "name": "digest" + }, + { + "lineno": 157, + "name": "m" + }, + { + "lineno": 200, + "name": "type" + }, + { + "lineno": 262, + "name": "s" + } + ], + "classes": [ + { + "args": [ + "Seed const& seed" + ], + "lineno": 67, + "name": "Generator" + } + ], + "code_paths": [ + { + "call_chain": [ + "SecretKey::SecretKey(Slice const& slice)" + ], + "entry_point": "SecretKey::SecretKey(Slice const& slice)", + "purpose": "Constructs a SecretKey from a Slice, validating the input size.", + "validation_points": [ + "if (slice.size() != sizeof(buf_)) LogicError(...)" + ] + }, + { + "call_chain": [ + "detail::deriveDeterministicRootKey(Seed const& seed)", + "sha512Half(buf)", + "secp256k1_ec_seckey_verify(secp256k1Context(), ret.data())" + ], + "entry_point": "detail::deriveDeterministicRootKey(Seed const& seed)", + "purpose": "Derives a deterministic secp256k1 root key from a seed, validating the derived key.", + "validation_points": [ + "secp256k1_ec_seckey_verify(secp256k1Context(), ret.data())" + ] + } + ], + "data_flows": [ + { + "field": "slice (input to SecretKey::SecretKey)", + "flow": [ + "Caller", + "SecretKey::SecretKey(Slice const& slice)", + "Validation: size check", + "std::memcpy(buf_, slice.data(), sizeof(buf_))", + "buf_ (internal storage)" + ], + "origin": "Caller provides a Slice (likely from serialized key material)", + "transformations": [ + "Size checked against expected key size (32 bytes)", + "Copied into internal buffer" + ], + "validated_at": "SecretKey::SecretKey(Slice const& slice) (size check)" + }, + { + "field": "seed (input to deriveDeterministicRootKey)", + "flow": [ + "Caller", + "detail::deriveDeterministicRootKey(Seed const& seed)", + "Copied into buf (first 16 bytes)", + "Counter appended (copy_uint32)", + "sha512Half(buf) \u2192 ret", + "secp256k1_ec_seckey_verify(secp256k1Context(), ret.data())", + "If valid, ret returned as root key" + ], + "origin": "Caller provides a Seed (likely from user input or wallet)", + "transformations": [ + "Seed copied into buffer", + "Counter appended", + "SHA-512/256 hash computed", + "Result validated as secp256k1 secret key" + ], + "validated_at": "secp256k1_ec_seckey_verify(secp256k1Context(), ret.data())" + } + ], + "description": "Implements cryptographic key generation, signing, and derivation for the XRP Ledger, supporting secp256k1 and ed25519 key types, including deterministic key derivation from seeds and random key generation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "slice.size()", + "empty", + "string", + "validation" + ], + "evidence": "explicit size check and LogicError exception at SecretKey::SecretKey(Slice const& slice) constructor", + "issue_pattern": "Missing empty string validation for slice.size()", + "why_false_positive": "explicit size check and LogicError exception validates slice.size() for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "slice.size()", + "format", + "validation", + "invalid" + ], + "evidence": "explicit size check and LogicError exception at SecretKey::SecretKey(Slice const& slice) constructor", + "issue_pattern": "Missing format validation for slice.size()", + "why_false_positive": "explicit size check and LogicError exception validates slice.size() format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "derived secp256k1 secret key (ret)", + "empty", + "string", + "validation" + ], + "evidence": "secp256k1_ec_seckey_verify at detail::deriveDeterministicRootKey", + "issue_pattern": "Missing empty string validation for derived secp256k1 secret key (ret)", + "why_false_positive": "secp256k1_ec_seckey_verify validates derived secp256k1 secret key (ret) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/SecretKey.cpp", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "SecretKey::~SecretKey" + }, + { + "args": [ + "std::array const& key" + ], + "lineno": 22, + "name": "SecretKey::SecretKey" + }, + { + "args": [ + "Slice const& slice" + ], + "lineno": 27, + "name": "SecretKey::SecretKey" + }, + { + "args": [], + "lineno": 33, + "name": "SecretKey::to_string" + }, + { + "args": [ + "std::uint8_t* out", + "std::uint32_t v" + ], + "lineno": 41, + "name": "copy_uint32" + }, + { + "args": [ + "Seed const& seed" + ], + "lineno": 48, + "name": "deriveDeterministicRootKey" + }, + { + "args": [ + "std::uint32_t seq" + ], + "lineno": 81, + "name": "Generator::calculateTweak" + }, + { + "args": [ + "Seed const& seed" + ], + "lineno": 104, + "name": "Generator::Generator" + }, + { + "args": [], + "lineno": 116, + "name": "Generator::~Generator" + }, + { + "args": [ + "std::size_t ordinal" + ], + "lineno": 121, + "name": "Generator::operator()" + }, + { + "args": [ + "PublicKey const& pk", + "SecretKey const& sk", + "uint256 const& digest" + ], + "lineno": 139, + "name": "signDigest" + }, + { + "args": [ + "PublicKey const& pk", + "SecretKey const& sk", + "Slice const& m" + ], + "lineno": 157, + "name": "sign" + }, + { + "args": [], + "lineno": 191, + "name": "randomSecretKey" + }, + { + "args": [ + "KeyType type", + "Seed const& seed" + ], + "lineno": 200, + "name": "generateSecretKey" + }, + { + "args": [ + "KeyType type", + "SecretKey const& sk" + ], + "lineno": 218, + "name": "derivePublicKey" + }, + { + "args": [ + "KeyType type", + "Seed const& seed" + ], + "lineno": 244, + "name": "generateKeyPair" + }, + { + "args": [ + "KeyType type" + ], + "lineno": 257, + "name": "randomKeyPair" + }, + { + "args": [ + "TokenType type", + "std::string const& s" + ], + "lineno": 262, + "name": "parseBase58" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + }, + { + "lineno": 39, + "name": "detail" + } + ], + "test_coverage_notes": "The code is low-level and likely tested indirectly via higher-level key management and wallet tests. Direct unit tests for SecretKey and deriveDeterministicRootKey may exist in files like test/SecretKey_test.cpp, test/Seed_test.cpp, or test/KeyDerivation_test.cpp. However, explicit tests for validation failures (e.g., invalid slice size, invalid derived key) may be missing or limited. Exception paths (LogicError, runtime_error) should be tested to ensure robust error handling, but coverage of these paths is often incomplete in practice.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LogicError", + "field": "slice.size()", + "location": "SecretKey::SecretKey(Slice const& slice) constructor", + "validated_by": "explicit size check and LogicError exception", + "validates": [ + "Checks that the input Slice is exactly 32 bytes (the size of buf_)." + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "derived secp256k1 secret key (ret)", + "location": "detail::deriveDeterministicRootKey", + "validated_by": "secp256k1_ec_seckey_verify", + "validates": [ + "Checks that the derived key is a valid secp256k1 secret key (nonzero, less than curve order)." + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/SecretKey.cpp.ai.md b/src/libxrpl/protocol/SecretKey.cpp.ai.md new file mode 100644 index 0000000000..ecb47ea488 --- /dev/null +++ b/src/libxrpl/protocol/SecretKey.cpp.ai.md @@ -0,0 +1,43 @@ +# `SecretKey.cpp` — Cryptographic Key Generation, Derivation, and Signing + +## Role in the System + +This file is the core cryptographic engine for the XRP Ledger's key management. It implements everything needed to go from a raw seed or randomness to usable signing keys: secret key construction, deterministic key derivation for both `secp256k1` and `ed25519`, public key derivation, message signing, and Base58-encoded key parsing. It sits at the lowest layer of the `libxrpl` protocol stack, and most higher-level wallet, transaction-submission, and account-management code ultimately calls into this file. + +## `SecretKey`: Lifecycle and Intentional Restrictions + +`SecretKey` stores exactly 32 bytes in a plain `uint8_t buf_[]`. Two design choices stand out immediately. First, the destructor unconditionally calls `secure_erase(buf_, sizeof(buf_))`, which attempts to overwrite key material before the memory is released. This guards against secret bytes lingering in freed pages or stack frames and is applied consistently to all intermediate key-material buffers throughout the file. + +Second, `operator==`, `operator!=`, and `operator<<` are explicitly deleted. The equality deletions prevent callers from comparing secret keys in ways that might leak timing information. The absence of `operator<<` closes off accidental logging — a developer cannot inadvertently stream a `SecretKey` to a logger or output stream. `to_string()` exists as an intentional, explicit escape hatch, named in a way that signals conscious intent. + +## XRPL's Custom secp256k1 Key Derivation + +The XRPL predates BIP-32 and uses its own deterministic derivation algorithm, implemented through the `detail::Generator` class and the `detail::deriveDeterministicRootKey` helper. + +`deriveDeterministicRootKey` accepts a 128-bit `Seed` and computes a valid secp256k1 scalar by hashing the seed concatenated with a big-endian 32-bit counter: `sha512Half(seed[0..15] || seq[0..3])`. The result is validated via `secp256k1_ec_seckey_verify`, which checks that it is nonzero and less than the curve's group order. Failure (statistically negligible — fewer than one in `2^128` seeds) causes a retry up to 128 times, after which a `std::runtime_error` is thrown. The intermediate buffer is always `secure_erase`d regardless of outcome. + +`Generator` builds on this root key to produce a whole *family* of key pairs. During construction, it derives the compressed 33-byte root public key. For each ordinal, `calculateTweak` hashes `(rootPublicKey || ordinal || subseq)` to produce a scalar tweak. The final secret key is computed as `root + tweak (mod n)` using `secp256k1_ec_seckey_tweak_add`. This mirrors a simplified form of BIP-32 child key derivation but is XRPL-specific and incompatible with standard HD wallets. The comment in the source explicitly warns implementers: third-party tools do not need to replicate this derivation, but should support it if they need to import existing XRPL accounts. + +`generateKeyPair` uses `Generator` for secp256k1, always requesting ordinal 0, which is the single-key case used almost universally. The generator pattern exists to support the older "family" concept where multiple addresses could be derived from one seed. + +## Ed25519 Key Derivation + +Ed25519 derivation is dramatically simpler. `generateSecretKey` for `KeyType::ed25519` simply computes `sha512Half_s(seed)` — the `_s` suffix indicates the variant that uses a `Slice` directly. There is no counter loop, no curve-order validation (Ed25519's scalar space is much larger relative to the hash output), and no multi-level structure. The public key is computed by `ed25519_publickey(sk.data(), &buf[1])` and prefixed with the byte `0xED` at position zero. This one-byte prefix is how the XRPL distinguishes compressed secp256k1 public keys (33 bytes, starting with `0x02` or `0x03`) from Ed25519 keys (also 33 bytes on the wire, but starting with `0xED`). + +## Signing + +The `sign` function dispatches on `KeyType`. For `ed25519`, it calls `ed25519_sign` directly on the raw message bytes — by design, Ed25519 hashes the message internally, and its security properties depend on that specific hash. Bypassing the internal hash (as `signDigest` does for secp256k1) is not supported for Ed25519. The header comment for `signDigest` makes this constraint explicit. + +For secp256k1, `sign` applies `SHA-512/Half` to the message before calling into libsecp256k1, producing a `uint256` digest that is then passed to `secp256k1_ecdsa_sign`. Both `sign` and `signDigest` use `secp256k1_nonce_function_rfc6979` as the nonce function. RFC 6979 deterministic nonce generation is a critical choice: it eliminates the risk of nonce reuse (which would catastrophically expose the private key) and makes signatures reproducible, which simplifies testing and auditing. The resulting DER-encoded signature is returned in a `Buffer` of up to 72 bytes. + +## The secp256k1 Context + +`secp256k1Context()` is a function template in `detail/secp256k1.h` that returns a pointer to a `static`-local `secp256k1_context*` initialized with both `SECP256K1_CONTEXT_VERIFY` and `SECP256K1_CONTEXT_SIGN` flags. In C++11 and later, static-local initialization is thread-safe, so this effectively provides a lazily initialized, process-global secp256k1 context with no locking overhead after first use. This is appropriate for a library where the context never changes after startup. + +## Random Key Generation + +`randomSecretKey` fills a 32-byte stack buffer with output from `crypto_prng()` (the CSPRNG) via `beast::rngfill`, wraps it in a `SecretKey`, and then `secure_erase`s the stack buffer. The CSPRNG itself is a separate concern handled by `xrpl/crypto/csprng.h`. `randomKeyPair` simply combines `randomSecretKey` with `derivePublicKey` — the random path does not use the deterministic `Generator` at all, making it suitable for one-off key generation where wallet recovery from a seed is not needed. + +## Base58 Parsing + +`parseBase58` is a template specialization that decodes a Base58-encoded token (typically a `TokenType::FamilySeed` or similar) and validates that the decoded payload is exactly 32 bytes. This is the entry point for loading persisted or user-supplied secret keys from their wire representation. \ No newline at end of file diff --git a/src/libxrpl/protocol/Seed.cpp.ai.json b/src/libxrpl/protocol/Seed.cpp.ai.json new file mode 100644 index 0000000000..6d8603c825 --- /dev/null +++ b/src/libxrpl/protocol/Seed.cpp.ai.json @@ -0,0 +1,650 @@ +{ + "args": [ + { + "lineno": 21, + "name": "slice" + }, + { + "lineno": 28, + "name": "seed" + }, + { + "lineno": 45, + "name": "passPhrase" + }, + { + "lineno": 52, + "name": "s" + }, + { + "lineno": 60, + "name": "str" + }, + { + "lineno": 60, + "name": "rfc1751" + }, + { + "lineno": 89, + "name": "seed" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "parseGenericSeed", + "parseBase58", + "parseBase58", + "parseBase58", + "seed.parseHex", + "parseBase58", + "RFC1751::getKeyFromEnglish", + "generateSeed" + ], + "entry_point": "parseGenericSeed", + "purpose": "Attempts to parse a generic seed string into a Seed object, trying multiple formats and validation steps.", + "validation_points": [ + "if (str.empty()) return std::nullopt;", + "parseBase58: if (result.empty()) return std::nullopt;", + "parseBase58: if (result.size() != 16) return std::nullopt;", + "Seed::Seed(Slice): if (slice.size() != buf_.size()) LogicError(...)", + "Seed::Seed(uint128): if (seed.size() != buf_.size()) LogicError(...)" + ] + }, + { + "call_chain": [ + "parseBase58", + "decodeBase58Token", + "Seed::Seed(Slice)" + ], + "entry_point": "parseBase58", + "purpose": "Parses a base58-encoded string as a Seed, validating size and content.", + "validation_points": [ + "if (result.empty()) return std::nullopt;", + "if (result.size() != 16) return std::nullopt;", + "Seed::Seed(Slice): if (slice.size() != buf_.size()) LogicError(...)" + ] + }, + { + "call_chain": [ + "Seed::Seed(Slice)" + ], + "entry_point": "Seed::Seed(Slice)", + "purpose": "Constructs a Seed from a Slice, validating the size.", + "validation_points": [ + "if (slice.size() != buf_.size()) LogicError(...)" + ] + }, + { + "call_chain": [ + "Seed::Seed(uint128)" + ], + "entry_point": "Seed::Seed(uint128)", + "purpose": "Constructs a Seed from a uint128, validating the size.", + "validation_points": [ + "if (seed.size() != buf_.size()) LogicError(...)" + ] + }, + { + "call_chain": [ + "generateSeed", + "sha512_half_hasher_s", + "Seed::Seed(Slice)" + ], + "entry_point": "generateSeed", + "purpose": "Generates a Seed from a passphrase using SHA-512/256 hashing.", + "validation_points": [ + "Seed::Seed(Slice): if (slice.size() != buf_.size()) LogicError(...)" + ] + }, + { + "call_chain": [ + "randomSeed", + "beast::rngfill", + "Seed::Seed(Slice)" + ], + "entry_point": "randomSeed", + "purpose": "Generates a random Seed using a cryptographically secure PRNG.", + "validation_points": [ + "Seed::Seed(Slice): if (slice.size() != buf_.size()) LogicError(...)" + ] + } + ], + "data_flows": [ + { + "field": "str (input string to parseGenericSeed)", + "flow": [ + "parseGenericSeed(str, rfc1751)", + "validation: if (str.empty()) return std::nullopt;", + "parseBase58(str), parseBase58(...), parseBase58(...)", + "seed.parseHex(str)", + "parseBase58(str)", + "RFC1751::getKeyFromEnglish(key, str)", + "generateSeed(str)" + ], + "origin": "User input or caller of parseGenericSeed", + "transformations": [ + "Checked for emptiness", + "Attempted base58 decoding", + "Attempted hex parsing", + "Attempted RFC1751 decoding", + "Hashed if all else fails" + ], + "validated_at": "parseGenericSeed (emptiness), parseBase58 (size/emptiness), Seed::Seed (size)" + }, + { + "field": "result (from decodeBase58Token)", + "flow": [ + "parseBase58(s)", + "result = decodeBase58Token(s, TokenType::FamilySeed)", + "if (result.empty()) return std::nullopt;", + "if (result.size() != 16) return std::nullopt;", + "Seed(makeSlice(result))" + ], + "origin": "decodeBase58Token(s, TokenType::FamilySeed)", + "transformations": [ + "Base58 decoding", + "Checked for emptiness", + "Checked for correct size" + ], + "validated_at": "parseBase58 (emptiness, size)" + }, + { + "field": "slice (input to Seed::Seed(Slice))", + "flow": [ + "Seed::Seed(Slice)", + "if (slice.size() != buf_.size()) LogicError(...)", + "std::memcpy(buf_.data(), slice.data(), buf_.size())" + ], + "origin": "makeSlice(result) or Slice(seed.data(), seed.size())", + "transformations": [ + "Checked for correct size", + "Copied into internal buffer" + ], + "validated_at": "Seed::Seed(Slice) (size)" + }, + { + "field": "seed (input to Seed::Seed(uint128))", + "flow": [ + "Seed::Seed(uint128)", + "if (seed.size() != buf_.size()) LogicError(...)", + "std::memcpy(buf_.data(), seed.data(), buf_.size())" + ], + "origin": "uint128 parsed from hex or constructed from RFC1751 blob", + "transformations": [ + "Checked for correct size", + "Copied into internal buffer" + ], + "validated_at": "Seed::Seed(uint128) (size)" + } + ], + "description": "Implements the Seed class and related functions for generating, parsing, and handling cryptographic seeds in the XRPL protocol.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "slice.size()", + "empty", + "string", + "validation" + ], + "evidence": "if (slice.size() != buf_.size()) LogicError(...) at Seed::Seed(Slice const& slice) constructor", + "issue_pattern": "Missing empty string validation for slice.size()", + "why_false_positive": "if (slice.size() != buf_.size()) LogicError(...) validates slice.size() for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "slice.size()", + "format", + "validation", + "invalid" + ], + "evidence": "if (slice.size() != buf_.size()) LogicError(...) at Seed::Seed(Slice const& slice) constructor", + "issue_pattern": "Missing format validation for slice.size()", + "why_false_positive": "if (slice.size() != buf_.size()) LogicError(...) validates slice.size() format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "seed.size()", + "empty", + "string", + "validation" + ], + "evidence": "if (seed.size() != buf_.size()) LogicError(...) at Seed::Seed(uint128 const& seed) constructor", + "issue_pattern": "Missing empty string validation for seed.size()", + "why_false_positive": "if (seed.size() != buf_.size()) LogicError(...) validates seed.size() for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "seed.size()", + "format", + "validation", + "invalid" + ], + "evidence": "if (seed.size() != buf_.size()) LogicError(...) at Seed::Seed(uint128 const& seed) constructor", + "issue_pattern": "Missing format validation for seed.size()", + "why_false_positive": "if (seed.size() != buf_.size()) LogicError(...) validates seed.size() format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result.size()", + "empty", + "string", + "validation" + ], + "evidence": "if (result.size() != 16) return std::nullopt; at parseBase58>", + "issue_pattern": "Missing empty string validation for result.size()", + "why_false_positive": "if (result.size() != 16) return std::nullopt; validates result.size() for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "result.size()", + "format", + "validation", + "invalid" + ], + "evidence": "if (result.size() != 16) return std::nullopt; at parseBase58>", + "issue_pattern": "Missing format validation for result.size()", + "why_false_positive": "if (result.size() != 16) return std::nullopt; validates result.size() format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result.empty()", + "empty", + "string", + "validation" + ], + "evidence": "if (result.empty()) return std::nullopt; at parseBase58>", + "issue_pattern": "Missing empty string validation for result.empty()", + "why_false_positive": "if (result.empty()) return std::nullopt; validates result.empty() for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "result.empty()", + "format", + "validation", + "invalid" + ], + "evidence": "if (result.empty()) return std::nullopt; at parseBase58>", + "issue_pattern": "Missing format validation for result.empty()", + "why_false_positive": "if (result.empty()) return std::nullopt; validates result.empty() format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "str.empty()", + "empty", + "string", + "validation" + ], + "evidence": "if (str.empty()) return std::nullopt; at parseGenericSeed", + "issue_pattern": "Missing empty string validation for str.empty()", + "why_false_positive": "if (str.empty()) return std::nullopt; validates str.empty() for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "str.empty()", + "format", + "validation", + "invalid" + ], + "evidence": "if (str.empty()) return std::nullopt; at parseGenericSeed", + "issue_pattern": "Missing format validation for str.empty()", + "why_false_positive": "if (str.empty()) return std::nullopt; validates str.empty() format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "str (AccountID/PublicKey/SecretKey parse)", + "empty", + "string", + "validation" + ], + "evidence": "if (parseBase58(str) || ... ) return std::nullopt; at parseGenericSeed", + "issue_pattern": "Missing empty string validation for str (AccountID/PublicKey/SecretKey parse)", + "why_false_positive": "if (parseBase58(str) || ... ) return std::nullopt; validates str (AccountID/PublicKey/SecretKey parse) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "str (AccountID/PublicKey/SecretKey parse)", + "format", + "validation", + "invalid" + ], + "evidence": "if (parseBase58(str) || ... ) return std::nullopt; at parseGenericSeed", + "issue_pattern": "Missing format validation for str (AccountID/PublicKey/SecretKey parse)", + "why_false_positive": "if (parseBase58(str) || ... ) return std::nullopt; validates str (AccountID/PublicKey/SecretKey parse) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "str (hex seed)", + "empty", + "string", + "validation" + ], + "evidence": "if (seed.parseHex(str)) return Seed{Slice(seed.data(), seed.size())}; at parseGenericSeed", + "issue_pattern": "Missing empty string validation for str (hex seed)", + "why_false_positive": "if (seed.parseHex(str)) return Seed{Slice(seed.data(), seed.size())}; validates str (hex seed) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "str (hex seed)", + "format", + "validation", + "invalid" + ], + "evidence": "if (seed.parseHex(str)) return Seed{Slice(seed.data(), seed.size())}; at parseGenericSeed", + "issue_pattern": "Missing format validation for str (hex seed)", + "why_false_positive": "if (seed.parseHex(str)) return Seed{Slice(seed.data(), seed.size())}; validates str (hex seed) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "str (base58 seed)", + "empty", + "string", + "validation" + ], + "evidence": "if (auto seed = parseBase58(str)) return seed; at parseGenericSeed", + "issue_pattern": "Missing empty string validation for str (base58 seed)", + "why_false_positive": "if (auto seed = parseBase58(str)) return seed; validates str (base58 seed) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "str (base58 seed)", + "format", + "validation", + "invalid" + ], + "evidence": "if (auto seed = parseBase58(str)) return seed; at parseGenericSeed", + "issue_pattern": "Missing format validation for str (base58 seed)", + "why_false_positive": "if (auto seed = parseBase58(str)) return seed; validates str (base58 seed) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "str (RFC1751 English phrase)", + "empty", + "string", + "validation" + ], + "evidence": "if (RFC1751::getKeyFromEnglish(key, str) == 1) at parseGenericSeed", + "issue_pattern": "Missing empty string validation for str (RFC1751 English phrase)", + "why_false_positive": "if (RFC1751::getKeyFromEnglish(key, str) == 1) validates str (RFC1751 English phrase) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "str (RFC1751 English phrase)", + "format", + "validation", + "invalid" + ], + "evidence": "if (RFC1751::getKeyFromEnglish(key, str) == 1) at parseGenericSeed", + "issue_pattern": "Missing format validation for str (RFC1751 English phrase)", + "why_false_positive": "if (RFC1751::getKeyFromEnglish(key, str) == 1) validates str (RFC1751 English phrase) format" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Seed.cpp", + "functions": [ + { + "args": [], + "lineno": 17, + "name": "Seed::~Seed" + }, + { + "args": [ + "Slice const& slice" + ], + "lineno": 21, + "name": "Seed::Seed" + }, + { + "args": [ + "uint128 const& seed" + ], + "lineno": 28, + "name": "Seed::Seed" + }, + { + "args": [], + "lineno": 36, + "name": "randomSeed" + }, + { + "args": [ + "std::string const& passPhrase" + ], + "lineno": 45, + "name": "generateSeed" + }, + { + "args": [ + "std::string const& s" + ], + "lineno": 52, + "name": "parseBase58>" + }, + { + "args": [ + "std::string const& str", + "bool rfc1751" + ], + "lineno": 60, + "name": "parseGenericSeed" + }, + { + "args": [ + "Seed const& seed" + ], + "lineno": 89, + "name": "seedAs1751" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested in unit tests for Seed parsing and generation, typically found in files like Seed_test.cpp, Protocol_test.cpp, or tests under workflow/XRPLF-rippled-develop/test/unit/protocol/ or similar. The validation paths (size checks, emptiness checks) are critical for security and correctness, so they are likely covered by tests for valid and invalid seed strings, base58 decoding, and RFC1751 parsing. However, edge cases such as oversized/undersized input, empty strings, and malformed base58/hex/RFC1751 inputs should be explicitly tested. Gaps may exist if tests do not cover all error branches (e.g., LogicError throws, all std::nullopt returns), or if fuzzing is not performed on all input paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation logic, no external validation framework detected", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LogicError exception", + "field": "slice.size()", + "location": "Seed::Seed(Slice const& slice) constructor", + "validated_by": "if (slice.size() != buf_.size()) LogicError(...)", + "validates": [ + "Checks that the input Slice is exactly 16 bytes (seed size)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "LogicError exception", + "field": "seed.size()", + "location": "Seed::Seed(uint128 const& seed) constructor", + "validated_by": "if (seed.size() != buf_.size()) LogicError(...)", + "validates": [ + "Checks that the input uint128 is exactly 16 bytes (seed size)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "Returns std::nullopt (no exception)", + "field": "result.size()", + "location": "parseBase58>", + "validated_by": "if (result.size() != 16) return std::nullopt;", + "validates": [ + "Checks that the decoded base58 result is exactly 16 bytes" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "Returns std::nullopt (no exception)", + "field": "result.empty()", + "location": "parseBase58>", + "validated_by": "if (result.empty()) return std::nullopt;", + "validates": [ + "Checks that the decoded base58 result is not empty" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "Returns std::nullopt (no exception)", + "field": "str.empty()", + "location": "parseGenericSeed", + "validated_by": "if (str.empty()) return std::nullopt;", + "validates": [ + "Checks that the input string is not empty" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "Returns std::nullopt (no exception)", + "field": "str (AccountID/PublicKey/SecretKey parse)", + "location": "parseGenericSeed", + "validated_by": "if (parseBase58(str) || ... ) return std::nullopt;", + "validates": [ + "Checks that the input string is not a valid AccountID, NodePublic, AccountPublic, NodePrivate, or AccountSecret base58 encoding" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns Seed if valid, continues otherwise)", + "field": "str (hex seed)", + "location": "parseGenericSeed", + "validated_by": "if (seed.parseHex(str)) return Seed{Slice(seed.data(), seed.size())};", + "validates": [ + "Checks if the input string is a valid hex-encoded 128-bit value" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns std::nullopt if invalid)", + "field": "str (base58 seed)", + "location": "parseGenericSeed", + "validated_by": "if (auto seed = parseBase58(str)) return seed;", + "validates": [ + "Checks if the input string is a valid base58-encoded seed" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns Seed if valid, continues otherwise)", + "field": "str (RFC1751 English phrase)", + "location": "parseGenericSeed", + "validated_by": "if (RFC1751::getKeyFromEnglish(key, str) == 1)", + "validates": [ + "Checks if the input string is a valid RFC1751 English phrase" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Seed.cpp.ai.md b/src/libxrpl/protocol/Seed.cpp.ai.md new file mode 100644 index 0000000000..06b96bb0cf --- /dev/null +++ b/src/libxrpl/protocol/Seed.cpp.ai.md @@ -0,0 +1,41 @@ +# `src/libxrpl/protocol/Seed.cpp` + +## Role and Purpose + +This file implements the `Seed` class and the suite of factory and parsing functions that feed into XRPL's deterministic key-derivation pipeline. A seed is the 128-bit root secret from which both secp256k1 and ed25519 key pairs are derived via `generateSecretKey` / `generateKeyPair`. Every XRPL account or validator node traces back to one of these 16-byte values, making its construction and parsing the most security-sensitive path in the key management subsystem. + +## The `Seed` Class + +`Seed` is deliberately simple: a fixed-size `std::array` with no default constructor, no mutable accessors, and a destructor that calls `secure_erase`. The absence of a default constructor is a meaningful invariant — it prevents any code path from holding an uninitialized seed. The copy constructor and copy-assignment operator are explicitly defaulted, which is correct because copies of keys are sometimes intentional (e.g., passing through function boundaries), but the destructor guarantees every copy clears its buffer independently on destruction. + +The two explicit constructors accept either a `Slice` (a non-owning span) or a `uint128`. Both validate size with `LogicError` rather than returning an error code or `std::optional`. This is intentional: callers constructing a `Seed` from an already-typed value (a decoded blob or a parsed integer) must have already verified the size before calling the constructor. A mismatched size is a programming error, not a user input error, so a hard abort through `LogicError` is appropriate. + +## Secure Erasure + +The destructor calls `secure_erase(buf_.data(), buf_.size())`, which in turn wraps OpenSSL's `OPENSSL_cleanse`. This is necessary because a naive `memset` or zeroing loop can be optimized away by the compiler when the buffer goes out of scope — `OPENSSL_cleanse` uses strategies specifically designed to resist dead-store elimination. In `generateSeed`, the same pattern is applied to the temporary stack buffer before returning: `randomSeed` fills a local `std::array`, constructs the `Seed` from it, and then calls `secure_erase` on the local array before returning the `Seed` by value. This prevents the raw entropy from lingering in the stack frame after the call returns. + +`generateSeed` similarly uses `sha512_half_hasher_s` rather than the non-secure variant `sha512_half_hasher`. The `_s` variant is a template specialization of `basic_sha512_half_hasher` that zeroes its internal SHA-512 state in its destructor. Without this, the passphrase's hash state would remain in stack memory after the hasher goes out of scope. + +## Seed Generation + +`randomSeed()` fills 16 bytes from `crypto_prng()`, XRPL's cryptographically secure pseudo-random number generator. `generateSeed(passPhrase)` computes the SHA-512 half (first 256 bits of SHA-512) of the passphrase and takes only the first 16 bytes of the 32-byte digest as the seed. This XRPL-specific algorithm is documented in `Seed.h`: the passphrase bytes are hashed without any normalization, null terminator, or length prefix. This deterministic derivation is intentional for usability — the well-known passphrase `"masterpassphrase"` always produces `snoPBrXtMeMyMHUVTgbuqAfg1SUTb`, as the test suite verifies. The obvious risk — weak passphrases map to weak seeds — is a known and accepted design tradeoff. + +## Parsing: `parseBase58` and `parseGenericSeed` + +`parseBase58` is a template specialization that decodes a Base58Check string tagged with `TokenType::FamilySeed`. The token type acts as a version byte in XRPL's Base58 encoding, so a string that decodes successfully but yields a size other than 16 bytes is rejected with `std::nullopt` rather than `LogicError`. This is correct: the caller supplied an externally-sourced string, so a mismatch is an input error rather than a logic violation. + +`parseGenericSeed` is the most architecturally interesting function. It implements a cascading format-detection strategy across five possible representations: + +1. **Rejection guard**: If the string decodes as a valid `AccountID`, `NodePublic`, `AccountPublic`, `NodePrivate`, or `AccountSecret`, it immediately returns `std::nullopt`. This is a critical safety check — without it, a valid node public key string could be silently reparsed as a seed via the passphrase hash fallback at the end of the function. This guard prevents key-type confusion attacks and accidental misuse in RPC calls. +2. **Hex**: Attempts to parse the string as a 128-bit hexadecimal value via `uint128::parseHex`. +3. **Base58**: Attempts `parseBase58` for the standard `sXXXX` format. +4. **RFC1751** (optional, gated by the `rfc1751` parameter): Decodes a mnemonic English word sequence using `RFC1751::getKeyFromEnglish`. A subtlety here is byte-order: the RFC1751-decoded key string is constructed with reversed bytes (`key.rbegin(), key.rend()`), matching the reversal performed by `seedAs1751` when encoding. The `rfc1751` parameter defaults to `true` but is marked deprecated in the header comment, reflecting the format's age. +5. **Passphrase fallback**: Any string that passes none of the above is treated as a passphrase and hashed via `generateSeed`. This fallback is a significant compatibility concern — it means `parseGenericSeed` never returns `std::nullopt` for a non-empty string that isn't another key type. Callers who want strict parsing should use `parseBase58` directly. + +## `seedAs1751` and Byte-Order + +`seedAs1751` encodes a seed as an RFC1751 mnemonic by first reversing the 16 seed bytes into a `std::string` via `std::reverse_copy`, then passing them to `RFC1751::getEnglishFromKey`. The reversal is paired with the corresponding reversal in `parseGenericSeed`'s RFC1751 decode path, forming a symmetric encode/decode pair. This byte reversal is not documented inline and is easy to miss — it appears to be a historical artifact of how XRPL originally adopted RFC1751, where the endianness convention differed from the RFC's standard interpretation. + +## Error Handling Philosophy + +The file uses two distinct error modes that map cleanly to their contexts. Constructors use `LogicError` (a hard abort) because size invariants on already-typed values are programmer errors. All parsing functions return `std::optional` (soft failure) because they consume unvalidated external strings. This boundary is precise and consistent throughout the implementation. \ No newline at end of file diff --git a/src/libxrpl/protocol/Serializer.cpp.ai.json b/src/libxrpl/protocol/Serializer.cpp.ai.json new file mode 100644 index 0000000000..3eb1213c42 --- /dev/null +++ b/src/libxrpl/protocol/Serializer.cpp.ai.json @@ -0,0 +1,654 @@ +{ + "args": [ + { + "lineno": 12, + "name": "i" + }, + { + "lineno": 19, + "name": "p" + }, + { + "lineno": 27, + "name": "i" + }, + { + "lineno": 31, + "name": "i" + }, + { + "lineno": 35, + "name": "i" + }, + { + "lineno": 39, + "name": "i" + }, + { + "lineno": 43, + "name": "i" + }, + { + "lineno": 47, + "name": "vector" + }, + { + "lineno": 53, + "name": "slice" + }, + { + "lineno": 59, + "name": "s" + }, + { + "lineno": 65, + "name": "ptr" + }, + { + "lineno": 65, + "name": "len" + }, + { + "lineno": 71, + "name": "type" + }, + { + "lineno": 71, + "name": "name" + }, + { + "lineno": 97, + "name": "byte" + }, + { + "lineno": 102, + "name": "byte" + }, + { + "lineno": 102, + "name": "offset" + }, + { + "lineno": 109, + "name": "bytes" + }, + { + "lineno": 120, + "name": "vector" + }, + { + "lineno": 128, + "name": "slice" + }, + { + "lineno": 135, + "name": "ptr" + }, + { + "lineno": 135, + "name": "len" + }, + { + "lineno": 142, + "name": "length" + }, + { + "lineno": 164, + "name": "length" + }, + { + "lineno": 177, + "name": "b1" + }, + { + "lineno": 188, + "name": "b1" + }, + { + "lineno": 195, + "name": "b1" + }, + { + "lineno": 195, + "name": "b2" + }, + { + "lineno": 204, + "name": "b1" + }, + { + "lineno": 204, + "name": "b2" + }, + { + "lineno": 204, + "name": "b3" + }, + { + "lineno": 214, + "name": "data" + }, + { + "lineno": 214, + "name": "size" + }, + { + "lineno": 224, + "name": "length" + }, + { + "lineno": 288, + "name": "type" + }, + { + "lineno": 288, + "name": "name" + }, + { + "lineno": 307, + "name": "size" + }, + { + "lineno": 344, + "name": "bytes" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Serializer::addFieldID" + ], + "entry_point": "Serializer::addFieldID", + "purpose": "Encodes a field identifier (type, name) into the serializer buffer.", + "validation_points": [ + "XRPL_ASSERT((type > 0) && (type < 256) && (name > 0) && (name < 256), ...)" + ] + }, + { + "call_chain": [ + "Serializer::add32(HashPrefix)", + "static_assert", + "Serializer::add32(std::uint32_t)" + ], + "entry_point": "Serializer::add32(HashPrefix)", + "purpose": "Adds a 32-bit hash prefix to the serializer buffer, ensuring type safety.", + "validation_points": [ + "static_assert(std::is_same_v>)" + ] + }, + { + "call_chain": [ + "Serializer::addVL(Blob const&)", + "addEncoded", + "addRaw", + "XRPL_ASSERT(...)" + ], + "entry_point": "Serializer::addVL(Blob const&)", + "purpose": "Adds a variable-length encoded blob to the serializer buffer.", + "validation_points": [ + "XRPL_ASSERT(mData.size() == (ret + vector.size() + encodeLengthLength(vector.size())), ...)" + ] + } + ], + "data_flows": [ + { + "field": "type (addFieldID argument)", + "flow": [ + "Caller provides type", + "Serializer::addFieldID receives type", + "XRPL_ASSERT validates type in range (1..255)", + "type used to encode field ID into mData" + ], + "origin": "Caller of Serializer::addFieldID", + "transformations": [ + "Checked for range", + "Bit-shifted and combined with name for encoding" + ], + "validated_at": "XRPL_ASSERT in addFieldID" + }, + { + "field": "name (addFieldID argument)", + "flow": [ + "Caller provides name", + "Serializer::addFieldID receives name", + "XRPL_ASSERT validates name in range (1..255)", + "name used to encode field ID into mData" + ], + "origin": "Caller of Serializer::addFieldID", + "transformations": [ + "Checked for range", + "Bit-shifted and combined with type for encoding" + ], + "validated_at": "XRPL_ASSERT in addFieldID" + }, + { + "field": "p (HashPrefix argument to add32)", + "flow": [ + "Caller provides p", + "Serializer::add32(HashPrefix) receives p", + "static_assert validates type at compile time", + "p cast to uint32_t and passed to add32(uint32_t)", + "Value encoded into mData" + ], + "origin": "Caller of Serializer::add32(HashPrefix)", + "transformations": [ + "Compile-time type check", + "Safe cast to uint32_t" + ], + "validated_at": "static_assert in add32(HashPrefix)" + }, + { + "field": "vector (Blob const& in addVL/addRaw)", + "flow": [ + "Caller provides vector", + "addVL calls addEncoded(vector.size())", + "addVL calls addRaw(vector)", + "XRPL_ASSERT checks mData size after addition" + ], + "origin": "Caller of Serializer::addVL or addRaw", + "transformations": [ + "Length encoded", + "Raw bytes appended to mData" + ], + "validated_at": "XRPL_ASSERT in addVL" + } + ], + "description": "Implements serialization and deserialization utilities for the XRPL protocol, including the Serializer and SerialIter classes for encoding/decoding binary data, variable-length fields, and field IDs.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type, name (arguments to addFieldID)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at Serializer::addFieldID", + "issue_pattern": "Missing empty string validation for type, name (arguments to addFieldID)", + "why_false_positive": "XRPL_ASSERT macro validates type, name (arguments to addFieldID) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "type, name (arguments to addFieldID)", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT macro at Serializer::addFieldID", + "issue_pattern": "Missing range validation for type, name (arguments to addFieldID)", + "why_false_positive": "XRPL_ASSERT macro validates type, name (arguments to addFieldID) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "p (HashPrefix argument to add32)", + "empty", + "string", + "validation" + ], + "evidence": "static_assert at Serializer::add32(HashPrefix p)", + "issue_pattern": "Missing empty string validation for p (HashPrefix argument to add32)", + "why_false_positive": "static_assert validates p (HashPrefix argument to add32) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "p (HashPrefix argument to add32)", + "type", + "validation", + "check" + ], + "evidence": "static_assert at Serializer::add32(HashPrefix p)", + "issue_pattern": "Missing type validation for p (HashPrefix argument to add32)", + "why_false_positive": "static_assert validates p (HashPrefix argument to add32) type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Serializer.cpp", + "functions": [ + { + "args": [ + "i" + ], + "lineno": 12, + "name": "Serializer::add16" + }, + { + "args": [ + "p" + ], + "lineno": 19, + "name": "Serializer::add32" + }, + { + "args": [ + "i" + ], + "lineno": 27, + "name": "Serializer::addInteger" + }, + { + "args": [ + "i" + ], + "lineno": 31, + "name": "Serializer::addInteger" + }, + { + "args": [ + "i" + ], + "lineno": 35, + "name": "Serializer::addInteger" + }, + { + "args": [ + "i" + ], + "lineno": 39, + "name": "Serializer::addInteger" + }, + { + "args": [ + "i" + ], + "lineno": 43, + "name": "Serializer::addInteger" + }, + { + "args": [ + "vector" + ], + "lineno": 47, + "name": "Serializer::addRaw" + }, + { + "args": [ + "slice" + ], + "lineno": 53, + "name": "Serializer::addRaw" + }, + { + "args": [ + "s" + ], + "lineno": 59, + "name": "Serializer::addRaw" + }, + { + "args": [ + "ptr", + "len" + ], + "lineno": 65, + "name": "Serializer::addRaw" + }, + { + "args": [ + "type", + "name" + ], + "lineno": 71, + "name": "Serializer::addFieldID" + }, + { + "args": [ + "byte" + ], + "lineno": 97, + "name": "Serializer::add8" + }, + { + "args": [ + "byte", + "offset" + ], + "lineno": 102, + "name": "Serializer::get8" + }, + { + "args": [ + "bytes" + ], + "lineno": 109, + "name": "Serializer::chop" + }, + { + "args": [], + "lineno": 115, + "name": "Serializer::getSHA512Half" + }, + { + "args": [ + "vector" + ], + "lineno": 120, + "name": "Serializer::addVL" + }, + { + "args": [ + "slice" + ], + "lineno": 128, + "name": "Serializer::addVL" + }, + { + "args": [ + "ptr", + "len" + ], + "lineno": 135, + "name": "Serializer::addVL" + }, + { + "args": [ + "length" + ], + "lineno": 142, + "name": "Serializer::addEncoded" + }, + { + "args": [ + "length" + ], + "lineno": 164, + "name": "Serializer::encodeLengthLength" + }, + { + "args": [ + "b1" + ], + "lineno": 177, + "name": "Serializer::decodeLengthLength" + }, + { + "args": [ + "b1" + ], + "lineno": 188, + "name": "Serializer::decodeVLLength" + }, + { + "args": [ + "b1", + "b2" + ], + "lineno": 195, + "name": "Serializer::decodeVLLength" + }, + { + "args": [ + "b1", + "b2", + "b3" + ], + "lineno": 204, + "name": "Serializer::decodeVLLength" + }, + { + "args": [ + "data", + "size" + ], + "lineno": 214, + "name": "SerialIter::SerialIter" + }, + { + "args": [], + "lineno": 218, + "name": "SerialIter::reset" + }, + { + "args": [ + "length" + ], + "lineno": 224, + "name": "SerialIter::skip" + }, + { + "args": [], + "lineno": 232, + "name": "SerialIter::get8" + }, + { + "args": [], + "lineno": 240, + "name": "SerialIter::get16" + }, + { + "args": [], + "lineno": 249, + "name": "SerialIter::get32" + }, + { + "args": [], + "lineno": 259, + "name": "SerialIter::get64" + }, + { + "args": [], + "lineno": 270, + "name": "SerialIter::geti32" + }, + { + "args": [], + "lineno": 279, + "name": "SerialIter::geti64" + }, + { + "args": [ + "type", + "name" + ], + "lineno": 288, + "name": "SerialIter::getFieldID" + }, + { + "args": [ + "size" + ], + "lineno": 307, + "name": "SerialIter::getRawHelper" + }, + { + "args": [ + "size" + ], + "lineno": 324, + "name": "SerialIter::getRaw" + }, + { + "args": [], + "lineno": 329, + "name": "SerialIter::getVLDataLength" + }, + { + "args": [ + "bytes" + ], + "lineno": 344, + "name": "SerialIter::getSlice" + }, + { + "args": [], + "lineno": 355, + "name": "SerialIter::getVL" + }, + { + "args": [], + "lineno": 360, + "name": "SerialIter::getVLBuffer" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The Serializer class is a core utility and is likely tested indirectly via transaction serialization/deserialization and protocol-level tests. Direct unit tests for addFieldID, add32(HashPrefix), and addVL may exist in files like Serializer_test.cpp or protocol/Serializer.test.cpp. However, the XRPL_ASSERT macro may be compiled out in release builds, so runtime validation may not be covered in all configurations. The static_assert in add32(HashPrefix) is compile-time only and not testable at runtime. Edge cases for invalid type/name in addFieldID may not be fully covered if asserts are disabled. There may be gaps in negative testing for invalid arguments if asserts are not checked in tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro, static_assert (compile-time)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::logic_error (via XRPL_ASSERT)", + "field": "type, name (arguments to addFieldID)", + "location": "Serializer::addFieldID", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "type > 0", + "type < 256", + "name > 0", + "name < 256" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "compile-time error (static_assert)", + "field": "p (HashPrefix argument to add32)", + "location": "Serializer::add32(HashPrefix p)", + "validated_by": "static_assert", + "validates": [ + "HashPrefix underlying type is std::uint32_t" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Serializer.cpp.ai.md b/src/libxrpl/protocol/Serializer.cpp.ai.md new file mode 100644 index 0000000000..b3eb324e42 --- /dev/null +++ b/src/libxrpl/protocol/Serializer.cpp.ai.md @@ -0,0 +1,61 @@ +# `src/libxrpl/protocol/Serializer.cpp` + +This file implements the binary serialization backbone of the XRP Ledger protocol. Two complementary classes live here — `Serializer` for writing and `SerialIter` for reading — together encoding and decoding the canonical wire format that underpins every transaction, ledger object, and cryptographic hash in the system. + +## `Serializer`: the write side + +`Serializer` wraps a `Blob` (`std::vector`) and provides a family of typed append methods. Every `add*` method returns the **byte offset** at which the data was written, not a success/failure bool. This design choice is intentional: callers that need to patch a previously written field (for instance, filling in a length or a reserved slot after the fact) can use the returned offset as a seek position. The object grows on demand and can be pre-sized with a constructor hint to avoid early reallocations. + +Multi-byte integers are always encoded big-endian by explicit byte-by-byte shifts (`(i >> 24) & 0xff`, etc.), which is more portable than relying on `memcpy`-with-endian-conversion. The `add32` and `add64` template overloads are constrained to types whose unsigned counterpart is exactly 32 or 64 bits, so passing a `long` on a platform where it is 64-bit won't silently use the wrong overload. + +### HashPrefix safety + +The overload `add32(HashPrefix p)` exists because `HashPrefix` is an opaque `enum class : std::uint32_t` representing the domain-separation prefixes prepended to data before signing or hashing (e.g., `TXN` for transaction IDs, `STX` for signing). The function includes a `static_assert` verifying that the underlying type is exactly `std::uint32_t`. Because these prefix values are part of the protocol and encoded into the wire format, any future accidental change to the enum's underlying type would cause silent corruption. The assert turns that into a compile error. + +### Field ID encoding + +`addFieldID(int type, int name)` implements the compact TLV field-tag scheme used throughout `STObject` serialization. Both type and name are validated (via `XRPL_ASSERT`) to be in the range 1–255. The encoding packs them into 1, 2, or 3 bytes: + +- If both fit in 4 bits (< 16): a single byte `(type << 4) | name` — the common case for well-known fields. +- If only the type fits in 4 bits: two bytes, the first being `type << 4` with the name byte following. +- If only the name fits in 4 bits: two bytes, name then type (note the reversed order for the "uncommon type, common name" case). +- If neither fits in 4 bits: three bytes — a leading `0x00` sentinel, then type, then name. + +The leading zero is the signal to the decoder that the next two bytes are a two-byte type+name pair. This space-efficient packing is meaningful at scale: XRPL ledger objects contain dozens of fields and millions of objects are stored, so shaving bytes off common field tags accumulates. + +### Variable-length fields + +`addVL` writes a variable-length-prefixed blob using XRPL's custom three-tier length encoding. `addEncoded(length)` picks the encoding width: + +- 0–192: one byte (direct value). +- 193–12,480: two bytes, using a bias-193 offset formula. +- 12,481–918,744: three bytes, using a bias-12,481 offset formula. +- Above 918,744: throws `std::overflow_error`. + +The static helper `encodeLengthLength(length)` returns the number of header bytes for a given data length, used in `addVL`'s post-condition assertion to verify the buffer grew by exactly `data_size + header_size` bytes. The corresponding `decodeLengthLength(b1)` and the three `decodeVLLength` overloads are the inverse map, dispatched by inspecting the first byte's range. + +### `getSHA512Half` + +This method computes SHA-512 over the current buffer and returns the first 256 bits of the result as a `uint256`. This is XRPL's standard hashing primitive — used to derive transaction IDs, inner node hashes in the ledger SHAMap, and signing digests. It delegates to `sha512Half` from `digest.h`, keeping the crypto dependency one level removed from the serialization primitive. + +## `SerialIter`: the read side + +`SerialIter` is a non-owning cursor into an existing byte buffer. It holds three state values: `p_` (the current read position), `remain_` (bytes left), and `used_` (bytes consumed since construction or the last `reset()`). The class header marks it `// DEPRECATED`, signaling an ongoing migration toward zero-copy `Slice`-based interfaces. + +The three-variable design makes `reset()` cheap: `p_ -= used_; remain_ += used_; used_ = 0;`. Saving the original pointer and length separately would also work but would add two words of state; instead, `used_` serves as an offset to reconstruct the original position. This allows a caller to speculatively consume fields, check validity, and rewind without allocating anything. + +All `get*` methods throw `std::runtime_error` on underrun — reading past the end of the buffer. This is intentional: protocol data arriving over the network must be treated as potentially malformed, and exceptions propagate cleanly through the STObject decode path. + +### Signed vs. unsigned reads + +`get16`, `get32`, and `get64` decode unsigned integers using explicit bit-shift assembly. `geti32` and `geti64` instead use `boost::endian::load_big_s32/s64`. The asymmetry exists because manual shift patterns (`(uint64_t(t[0]) << 24) | ...`) do not perform sign extension — for signed types with a set high bit, the result would be wrong. Boost's endian functions handle two's complement big-endian loading correctly for signed types. + +### `getRawHelper` and the null-pointer guard + +The private template `getRawHelper` is the implementation behind both `getRaw` (returning a `Blob` copy) and `getVLBuffer` (returning a `Buffer` copy). When `size == 0`, the code skips the `memcpy` call entirely. The comment explicitly cites C99 §7.21.1/2: while a zero-byte `memcpy` is nominally defined, empty `Blob` and `Buffer` objects may have a null `data()` pointer, and passing null to `memcpy` even with a zero count is undefined behavior in C++. The guard is precise and purposeful. + +`getVLDataLength()` reads the length prefix by first calling `get8()` on the leading byte, then dispatching through `Serializer::decodeLengthLength` to read 0, 1, or 2 additional bytes before assembling the final length via the appropriate `decodeVLLength` overload. The decoder is defined as `static` on `Serializer` because both the read and write paths share the same length encoding tables and it avoids duplicating the arithmetic. + +## Relationship to the broader serialization stack + +`STObject` and the rest of the `ST*` type hierarchy write themselves into a `Serializer` and read themselves back via `SerialIter`. The `addFieldID`/`getFieldID` pair forms the backbone of the tag-dispatch loop inside `STObject::makeFieldPresent` and the deserializer. `HashPrefix`-tagged hashes computed with `getSHA512Half` flow into the SHAMap node-hash chain and the signing pipeline in `Sign.cpp`. The `Slice`-returning `getSlice` is the preferred modern accessor since it avoids allocation; `getRaw` and `getVL` are retained for backward compatibility with older call sites that have not yet migrated. \ No newline at end of file diff --git a/src/libxrpl/protocol/Sign.cpp.ai.json b/src/libxrpl/protocol/Sign.cpp.ai.json new file mode 100644 index 0000000000..8f92d5dec3 --- /dev/null +++ b/src/libxrpl/protocol/Sign.cpp.ai.json @@ -0,0 +1,229 @@ +{ + "args": [ + { + "lineno": 12, + "name": "st" + }, + { + "lineno": 13, + "name": "prefix" + }, + { + "lineno": 14, + "name": "type" + }, + { + "lineno": 15, + "name": "sk" + }, + { + "lineno": 16, + "name": "sigField" + }, + { + "lineno": 23, + "name": "st" + }, + { + "lineno": 23, + "name": "prefix" + }, + { + "lineno": 23, + "name": "pk" + }, + { + "lineno": 23, + "name": "sigField" + }, + { + "lineno": 55, + "name": "obj" + }, + { + "lineno": 55, + "name": "signingID" + }, + { + "lineno": 62, + "name": "obj" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "verify (STObject, HashPrefix, PublicKey, SF_VL)", + "get(st, sigField)", + "verify(pk, Slice, Slice)" + ], + "entry_point": "verify", + "purpose": "Validates the signature in an STObject against a public key and prefix.", + "validation_points": [ + "get(st, sigField): Checks if the signature field exists and retrieves it.", + "verify(pk, Slice, Slice): Cryptographically verifies the signature." + ] + }, + { + "call_chain": [ + "sign (STObject&, HashPrefix, KeyType, SecretKey, SF_VL)", + "Serializer::add32", + "STObject::addWithoutSigningFields", + "sign(type, sk, ss.slice())", + "set(st, sigField, ...)" + ], + "entry_point": "sign", + "purpose": "Signs an STObject and sets the signature field.", + "validation_points": [ + "sign(type, sk, ss.slice()): Performs cryptographic signing (not validation, but signature creation)." + ] + }, + { + "call_chain": [ + "buildMultiSigningData", + "startMultiSigningData", + "finishMultiSigningData" + ], + "entry_point": "buildMultiSigningData", + "purpose": "Builds the data blob to be multi-signed, including relevant account fields.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "sigField (signature field in STObject)", + "flow": [ + "sign() creates signature", + "set(st, sigField, signature)", + "STObject now contains sigField", + "verify() retrieves sigField via get(st, sigField)", + "verify() uses sigField in cryptographic verification" + ], + "origin": "Set by sign() via set(st, sigField, ...)", + "transformations": [ + "Signature is generated from serialized data and secret key", + "Signature is stored in STObject under sigField", + "Signature is retrieved and passed as a Slice to verify()" + ], + "validated_at": "verify(): get(st, sigField) checks presence and validity of signature field" + }, + { + "field": "STObject (transaction or message object)", + "flow": [ + "Input to sign() or verify()", + "addWithoutSigningFields() serializes object (excluding signature fields)", + "Used as basis for signature creation or verification" + ], + "origin": "Input to sign() or verify()", + "transformations": [ + "Serialization (excluding signing fields)", + "Used as input to cryptographic functions" + ], + "validated_at": "Indirectly validated when signature is checked in verify()" + }, + { + "field": "signingID (AccountID for multi-signing)", + "flow": [ + "Input to buildMultiSigningData()", + "Passed to finishMultiSigningData()", + "Appended to serialized data blob for multi-signature" + ], + "origin": "Input to buildMultiSigningData()", + "transformations": [ + "Appended to serialized data for multi-signature context" + ], + "validated_at": "Not directly validated in this code; assumed correct by caller" + } + ], + "description": "This file provides functions for signing and verifying XRPL protocol objects, as well as building multi-signing data for transactions. It handles serialization and cryptographic signing/verification for XRPL transactions, including support for multi-signature workflows.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sigField (signature field in STObject)", + "empty", + "string", + "validation" + ], + "evidence": "get(st, sigField) at verify", + "issue_pattern": "Missing empty string validation for sigField (signature field in STObject)", + "why_false_positive": "get(st, sigField) validates sigField (signature field in STObject) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/Sign.cpp", + "functions": [ + { + "args": [ + "st", + "prefix", + "type", + "sk", + "sigField" + ], + "lineno": 11, + "name": "sign" + }, + { + "args": [ + "st", + "prefix", + "pk", + "sigField" + ], + "lineno": 22, + "name": "verify" + }, + { + "args": [ + "obj", + "signingID" + ], + "lineno": 54, + "name": "buildMultiSigningData" + }, + { + "args": [ + "obj" + ], + "lineno": 61, + "name": "startMultiSigningData" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code in Sign.cpp is core to signature creation and verification. Typical test coverage would be in unit tests for transaction signing and verification, likely in files such as 'test/protocol/Sign_test.cpp', 'test/protocol/STObject_test.cpp', or integration tests for transaction submission and validation. Gaps may exist in edge cases: malformed STObjects, missing sigField, or invalid key types. Multi-signing data construction may not be directly tested unless there are explicit multi-signature transaction tests. No explicit test references are present in this file, so coverage depends on broader protocol and transaction tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected (custom code, no explicit validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "sigField (signature field in STObject)", + "location": "verify", + "validated_by": "get(st, sigField)", + "validates": [ + "checks if signature field exists in STObject" + ], + "validation_type": "presence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/Sign.cpp.ai.md b/src/libxrpl/protocol/Sign.cpp.ai.md new file mode 100644 index 0000000000..0808ba8daf --- /dev/null +++ b/src/libxrpl/protocol/Sign.cpp.ai.md @@ -0,0 +1,33 @@ +# Sign.cpp — Protocol-Level Signing and Verification for XRPL Objects + +`Sign.cpp` is the thin but critical bridge between XRPL's raw cryptographic primitives (`SecretKey`/`PublicKey` signing) and the serialized ledger object layer (`STObject`). It answers the question: *given an arbitrary XRPL protocol object, how do you produce and verify a canonical signature over it?* The file contains four short functions whose design choices carry significant protocol-security weight. + +## Serialization before Signing + +Both `sign()` and `verify()` follow the same three-step pattern: build a `Serializer`, add the `HashPrefix`, then call `STObject::addWithoutSigningFields()`. The `addWithoutSigningFields()` call is the key detail: it serializes every field of the object *except* the signature fields themselves. This breaks the obvious circularity — you cannot include a signature in the data you sign — and ensures that the signing serialization is deterministic and unambiguous. + +The `HashPrefix` prepended to the serializer is a 4-byte domain-separation tag defined as a three-character ASCII string with a trailing null byte (e.g. `"STX\0"` for `txSign`, `"SMT\0"` for `txMultiSign`). Every distinct use of a signature in the protocol gets its own prefix. This makes it impossible to replay a valid signature from one context (say, a transaction authorization) as a valid signature in another (say, a ledger validation). The `HashPrefix` enum spans at least ten such domains — transactions, inner nodes, validations, proposals, manifests, payment channel claims, and more. + +## `sign()` and `verify()` + +The `sign()` overload is four lines: serialize the object, call the lower-level `sign(type, sk, slice)` from `SecretKey.h`, then `set()` the result into `sigField`. The default `sigField` is `sfSignature`, the standard transaction signature field, but the parameter allows the same logic to serve multi-signer contexts where the signature lives in a nested `Signer` entry. + +The `verify()` overload first calls `get(st, sigField)` and returns `false` immediately if the field is absent — a clean, no-exception guard. It then rebuilds the identical serialized blob and delegates to the cryptographic `verify(pk, message, signature)`. There is an important asymmetry: `sign()` takes a `KeyType` alongside the `SecretKey` (needed to distinguish secp256k1 from ed25519 at signing time), while `verify()` only needs the `PublicKey`, because the key type is encoded in the public key's first byte in the XRPL wire format. + +## Multi-Signature Data Construction + +The multi-signing functions reveal a deliberate performance trade-off. `buildMultiSigningData()` is the straightforward form: serialize the full object under `HashPrefix::txMultiSign`, then append the `signingID`. But the header also exposes a two-part split via `startMultiSigningData()` and `finishMultiSigningData()`. + +The rationale is batch verification efficiency. When a transaction carries multiple signers, the object serialization (the large part) is identical for all of them. Rather than reserializing the entire object once per signer, a validator calls `startMultiSigningData()` once, then calls `finishMultiSigningData(signerID, s)` for each signer in sequence, replacing only the small signer-specific tail. `finishMultiSigningData()` is an inline function in the header that appends the `AccountID` bit-string to the shared serializer. + +## Why the Signing Account ID Must Be Included + +The code contains an unusually detailed comment — attributed to David Schwartz — explaining why the signer's `AccountID` is appended to the multi-signing blob. Without it, an attacker who controls an entry in a `SignerList` could substitute *any other* signer who also holds a RegularKey pointing to the same third-party key. This kind of shared-RegularKey scenario is realistic for custodial services and exchange operators. Including the `AccountID` in the signing data makes each signer's authorization cryptographically specific to that account: you cannot transfer a signature from one signer slot to another. + +The comment also foreshadows future protocol evolution: if XRPL ever supports *nested* multi-signing (Carol signs for Bob who signs for Alice), the intermediate "signing-for" account IDs would also need to be incorporated into the data blob. The current two-level design (signer account + transaction account) is already in place, with the transaction's `Account` field naturally present in the serialized `STObject`, and the signer's identity added by `finishMultiSigningData()`. + +## Relationship to Surrounding Code + +This file sits at the intersection of three layers. Below it: `SecretKey.h` provides the raw `sign(KeyType, SecretKey, Slice)` and `verify(PublicKey, Slice, Slice)` functions that do actual elliptic-curve or EdDSA operations. Above it: transaction processing code calls these `sign()`/`verify()` wrappers directly on `STTx` (a subclass of `STObject`) without needing to know anything about serialization details. Alongside it: `STObject::addWithoutSigningFields()` implements the field-exclusion logic that makes the serialization canonical, and `HashPrefix` ensures every signature domain remains cryptographically isolated. + +The design is tightly minimal — less than 90 lines total — because each concern is delegated to the right layer. The protocol glue here is just the composition: prefix + non-signing serialization + signer identity, with the cryptographic heavy lifting pushed entirely into `SecretKey.cpp`. \ No newline at end of file diff --git a/src/libxrpl/protocol/TER.cpp.ai.json b/src/libxrpl/protocol/TER.cpp.ai.json new file mode 100644 index 0000000000..28f012830e --- /dev/null +++ b/src/libxrpl/protocol/TER.cpp.ai.json @@ -0,0 +1,153 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "transResults" + ], + "entry_point": "transResults", + "purpose": "Provides a mapping from TER (Transaction Engine Result) codes to their string tokens and human-readable descriptions. Used for interpreting transaction results.", + "validation_points": [] + }, + { + "call_chain": [ + "transResultInfo", + "transResults" + ], + "entry_point": "transResultInfo", + "purpose": "Given a TER code, retrieves the associated string token and description. Used to validate and interpret transaction result codes.", + "validation_points": [ + "transResultInfo (checks if TER code exists in map)" + ] + }, + { + "call_chain": [ + "transToken", + "transResultInfo", + "transResults" + ], + "entry_point": "transToken", + "purpose": "Given a TER code, returns the string token (e.g., 'tecNO_DST'). Used for logging, error reporting, and validation feedback.", + "validation_points": [ + "transResultInfo (validates TER code)" + ] + }, + { + "call_chain": [ + "transHuman", + "transResultInfo", + "transResults" + ], + "entry_point": "transHuman", + "purpose": "Given a TER code, returns the human-readable description. Used for user-facing error messages and validation feedback.", + "validation_points": [ + "transResultInfo (validates TER code)" + ] + }, + { + "call_chain": [ + "transCode", + "transResults" + ], + "entry_point": "transCode", + "purpose": "Given a string token, returns the corresponding TER code. Used to validate string tokens and map them to internal codes.", + "validation_points": [ + "transCode (validates string token exists in map)" + ] + } + ], + "data_flows": [ + { + "field": "TER code (TERUnderlyingType)", + "flow": [ + "Transaction processing", + "TER code produced", + "Passed to transResultInfo/transToken/transHuman", + "Mapped to string token and description" + ], + "origin": "Produced by transaction processing logic elsewhere in the codebase (e.g., after transaction execution)", + "transformations": [ + "Looked up in transResults map", + "Converted to string token and description" + ], + "validated_at": "transResultInfo (checks if code exists in map)" + }, + { + "field": "String token (e.g., 'tecNO_DST')", + "flow": [ + "Input string token", + "Passed to transCode", + "Iterates transResults map to find matching token", + "Returns corresponding TER code" + ], + "origin": "Input to transCode (from user input, config, or test)", + "transformations": [ + "String comparison", + "Mapping to TERUnderlyingType" + ], + "validated_at": "transCode (checks if token exists in map)" + } + ], + "description": "Provides mappings and utility functions for transaction engine result codes (TER) in the XRPL protocol, including human-readable descriptions and token lookups.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/TER.cpp", + "functions": [ + { + "args": [], + "lineno": 10, + "name": "transResults" + }, + { + "args": [ + "code", + "token", + "text" + ], + "lineno": 143, + "name": "transResultInfo" + }, + { + "args": [ + "code" + ], + "lineno": 157, + "name": "transToken" + }, + { + "args": [ + "code" + ], + "lineno": 166, + "name": "transHuman" + }, + { + "args": [ + "token" + ], + "lineno": 175, + "name": "transCode" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file is a mapping utility and does not itself perform transaction validation, but is critical for interpreting validation results. Direct unit tests may exist in files like 'TER_test.cpp', 'protocol/TER_test.cpp', or higher-level transaction engine tests (e.g., 'TransactionEngine_test.cpp', 'Transactor_test.cpp'). Gaps may exist if: (1) not all TER codes are tested for mapping correctness, (2) string tokens/descriptions are not checked for accuracy, (3) invalid/unknown codes or tokens are not tested for error handling. Validation of actual transaction logic (producing TER codes) is outside this file and must be tested elsewhere.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/TER.cpp.ai.md b/src/libxrpl/protocol/TER.cpp.ai.md new file mode 100644 index 0000000000..acf97b0a24 --- /dev/null +++ b/src/libxrpl/protocol/TER.cpp.ai.md @@ -0,0 +1,69 @@ +# `src/libxrpl/protocol/TER.cpp` — Transaction Engine Result Registry + +This file is the single authoritative registry that maps every Transaction Engine Result (TER) code to its string token and human-readable description. It is a purely runtime-lookup companion to the compile-time enum definitions in `TER.h`, providing the string data that logging, RPC responses, and reverse-parsing tools need. + +## The TER Code System + +TER codes encode the outcome of every transaction attempted on the XRP Ledger. Rather than a flat enumeration, the codebase uses six distinct C-style enums, each occupying a non-overlapping numeric range. This range structure is not cosmetic — it carries semantic meaning: + +| Prefix | Range | Meaning | +|--------|-------|---------| +| `tel` | −399 .. −300 | Local node error; not forwarded, no fee check | +| `tem` | −299 .. −200 | Malformed transaction; can never succeed | +| `tef` | −199 .. −100 | Failure due to ledger state or internal error | +| `ter` | −99 .. −1 | Retry; may succeed after other transactions | +| `tes` | 0 | Success | +| `tec` | 100+ | Applied with fee claimed; stored in ledger metadata | + +The range-based predicates `isTelLocal()`, `isTemMalformed()`, `isTefFailure()`, `isTerRetry()`, `isTesSuccess()`, and `isTecClaim()` (all defined in `TER.h`) exploit this layout directly by comparing numeric ranges rather than matching against a list of known codes — enabling new codes within a category to be classified automatically without changing the predicates. + +The `tec` codes and `tesSUCCESS` have explicitly pinned integer values because they are serialized into ledger metadata for historical transactions. The `tel`, `tem`, `tef`, and `ter` codes rely on sequential enum assignment from anchor values (e.g., `telLOCAL_ERROR = -399`) and are stable in the sense that binary codec definitions in `ripple-binary-codec` also reference them by integer — the header comments warn about this explicitly. + +## Type-Safe Wrappers: `TER` and `NotTEC` + +`TER.h` wraps the raw integer in a template class `TERSubset`, where a trait controls which `TE*codes` enum types are accepted by the constructor and assignment operator. Two instantiations are exported: + +- `TER` — accepts all six code families including `TECcodes`. Used as the general return type from transaction application. +- `NotTEC` — excludes `TECcodes`. Used for preflight return values. This matters because preflight runs before signature verification; if a preflight function could return a `tec` code, a malicious submitter could trigger a fee charge without a valid signature. + +All comparison operators (`==`, `!=`, `<`, etc.) are templated with `enable_if` guards that restrict them to pairs where both sides support `TERtoInt()`. This prevents accidental comparisons between unrelated integer types while still allowing cross-family comparisons like `ter == tec` (which are meaningful when checking whether two `TER` values are equal). + +## `transResults()` — The Primary Registry + +The implementation's central feature is `transResults()`, which returns a reference to a function-local static `unordered_map>`. Every TER code maps to a pair: the symbolic token string and the English description. + +The `MAKE_ERROR` macro is the key insight: + +```cpp +#define MAKE_ERROR(code, desc) { code, { #code, desc } } +``` + +The preprocessor stringification operator `#code` captures the C++ identifier (e.g., `tecNO_DST`) as a string literal at compile time. This eliminates any possibility of the token string diverging from the actual enum name — they are literally the same identifier, one as a value and one as a string. The macro is `#undef`'d after the map initialization to prevent leakage. + +The static local initialization follows the Meyers singleton pattern: it is initialized on first call, is guaranteed thread-safe by the C++11 standard, and the `const` qualifier on both the map and its value strings ensures no mutation after construction. + +## Lookup Functions + +`transResultInfo()` is the base lookup: it calls `TERtoInt(code)` to extract the underlying integer, then does a hash-map lookup. On success it populates two out-parameters (the token and the text) and returns `true`; on failure it returns `false`. Both `transToken()` and `transHuman()` are thin wrappers that delegate to `transResultInfo()` and return `"-"` for unknown codes rather than throwing. + +`transCode()` provides the reverse direction: given a string like `"tecNO_DST"`, it returns the corresponding `TER` wrapped in `std::optional`. The reverse map is also built as a function-local static, but it is constructed lazily via a lambda that runs once: + +```cpp +static auto const results = [] { + auto& byTer = transResults(); + auto range = boost::make_iterator_range(byTer.begin(), byTer.end()); + auto tRange = boost::adaptors::transform( + range, [](auto const& r) { return std::make_pair(r.second.first, r.first); }); + std::unordered_map const byToken( + tRange.begin(), tRange.end()); + return byToken; +}(); +``` + +The Boost range adaptor `transformed` flips each `{integer → (token, text)}` entry into a `{token → integer}` entry, which is then used to construct a new `unordered_map`. This approach avoids manually maintaining a second map and guarantees the two maps stay in sync — the reverse map is derived entirely from the primary one. The final reconstruction calls `TER::fromInt()` to wrap the raw integer back into a `TER` value, bypassing the type-checking constructors since the integer comes from a previously validated source. + +## Relationship to the Rest of the Protocol + +`transToken()` and `transHuman()` are called throughout the codebase wherever transaction results need to appear in logs, JSON-RPC responses, or error messages. `transCode()` is used in test harnesses and RPC parsing to convert string tokens back to `TER` values. The registry in `transResults()` is also accessible directly for use cases that need to iterate all known codes, such as documentation generators or conformance checkers. + +Two codes in the registry are worth noting as sentinels: `temUNCERTAIN` and `temUNKNOWN` are described as internal intermediate results that "should never be returned" to callers — they exist to represent the state of a result before determination is complete, and their presence in the registry ensures they produce readable output if they leak into logs during debugging. \ No newline at end of file diff --git a/src/libxrpl/protocol/TxFormats.cpp.ai.json b/src/libxrpl/protocol/TxFormats.cpp.ai.json new file mode 100644 index 0000000000..7b213b2d7f --- /dev/null +++ b/src/libxrpl/protocol/TxFormats.cpp.ai.json @@ -0,0 +1,1011 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "STObject::set", + "TxFormats::getInstance", + "TxFormats::getCommonFields", + "SOTemplate/SOElement validation" + ], + "entry_point": "Deserialization of Transaction (e.g., STObject::set)", + "purpose": "When a transaction is parsed from wire/JSON, the deserialization code uses the transaction type to look up the expected fields and their requirements (REQUIRED/OPTIONAL) via TxFormats. The SOTemplate validates presence, type, and constraints of each field.", + "validation_points": [ + "SOTemplate/SOElement validation (template-based, checks required fields, types, etc.)" + ] + }, + { + "call_chain": [ + "Transaction::Transaction", + "TxFormats::getInstance", + "TxFormats::getCommonFields", + "SOTemplate/SOElement validation" + ], + "entry_point": "Transaction construction (e.g., Transaction::Transaction)", + "purpose": "When constructing a Transaction object, the code uses TxFormats to determine the required/optional fields and validates them using the SOTemplate.", + "validation_points": [ + "SOTemplate/SOElement validation" + ] + }, + { + "call_chain": [ + "doSubmit", + "Transaction::fromSerialized", + "TxFormats::getInstance", + "TxFormats::getCommonFields", + "SOTemplate/SOElement validation" + ], + "entry_point": "Transaction submission (e.g., doSubmit)", + "purpose": "When a transaction is submitted to the network, it is parsed and validated against the TxFormats templates before being accepted.", + "validation_points": [ + "SOTemplate/SOElement validation" + ] + } + ], + "data_flows": [ + { + "field": "sfTransactionType", + "flow": [ + "Input (JSON/binary)", + "Deserialization (STObject)", + "TxFormats::getCommonFields", + "SOTemplate/SOElement validation", + "Transaction object" + ], + "origin": "Incoming transaction JSON or binary", + "transformations": [ + "Checked for presence (REQUIRED)", + "Type checked" + ], + "validated_at": "SOTemplate/SOElement validation (template-based, via getCommonFields)" + }, + { + "field": "sfAccount", + "flow": [ + "Input (JSON/binary)", + "Deserialization (STObject)", + "TxFormats::getCommonFields", + "SOTemplate/SOElement validation", + "Transaction object" + ], + "origin": "Incoming transaction JSON or binary", + "transformations": [ + "Checked for presence (REQUIRED)", + "Type checked" + ], + "validated_at": "SOTemplate/SOElement validation" + }, + { + "field": "sfSequence", + "flow": [ + "Input (JSON/binary)", + "Deserialization (STObject)", + "TxFormats::getCommonFields", + "SOTemplate/SOElement validation", + "Transaction object" + ], + "origin": "Incoming transaction JSON or binary", + "transformations": [ + "Checked for presence (REQUIRED)", + "Type checked" + ], + "validated_at": "SOTemplate/SOElement validation" + }, + { + "field": "sfFee", + "flow": [ + "Input (JSON/binary)", + "Deserialization (STObject)", + "TxFormats::getCommonFields", + "SOTemplate/SOElement validation", + "Transaction object" + ], + "origin": "Incoming transaction JSON or binary", + "transformations": [ + "Checked for presence (REQUIRED)", + "Type checked" + ], + "validated_at": "SOTemplate/SOElement validation" + }, + { + "field": "sfSigningPubKey", + "flow": [ + "Input (JSON/binary)", + "Deserialization (STObject)", + "TxFormats::getCommonFields", + "SOTemplate/SOElement validation", + "Transaction object" + ], + "origin": "Incoming transaction JSON or binary", + "transformations": [ + "Checked for presence (REQUIRED)", + "Type checked" + ], + "validated_at": "SOTemplate/SOElement validation" + } + ], + "description": "Defines the TxFormats class for transaction format definitions in the XRPL protocol, including common transaction fields and initialization logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTransactionType", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTransactionType validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfTransactionType", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfTransactionType automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfFlags", + "validation", + "missing", + "check" + ], + "evidence": "Field sfFlags validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfFlags", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfFlags automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSourceTag", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSourceTag validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfSourceTag", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfSourceTag automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccount validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfAccount", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSequence", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSequence validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfSequence", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfSequence automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfPreviousTxnID", + "validation", + "missing", + "check" + ], + "evidence": "Field sfPreviousTxnID validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfPreviousTxnID", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfPreviousTxnID automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLastLedgerSequence", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLastLedgerSequence validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfLastLedgerSequence", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfLastLedgerSequence automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccountTxnID", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccountTxnID validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfAccountTxnID", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfAccountTxnID automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfFee", + "validation", + "missing", + "check" + ], + "evidence": "Field sfFee validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfFee", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfFee automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfOperationLimit", + "validation", + "missing", + "check" + ], + "evidence": "Field sfOperationLimit validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfOperationLimit", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfOperationLimit automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfMemos", + "validation", + "missing", + "check" + ], + "evidence": "Field sfMemos validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfMemos", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfMemos automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSigningPubKey", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSigningPubKey validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfSigningPubKey", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfSigningPubKey automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTicketSequence", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTicketSequence validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfTicketSequence", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfTicketSequence automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTxnSignature", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTxnSignature validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfTxnSignature", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfTxnSignature automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSigners", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSigners validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfSigners", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfSigners automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfNetworkID", + "validation", + "missing", + "check" + ], + "evidence": "Field sfNetworkID validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfNetworkID", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfNetworkID automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDelegate", + "validation", + "missing", + "check" + ], + "evidence": "Field sfDelegate validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for sfDelegate", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates sfDelegate automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "all transaction-specific fields from transactions.macro", + "validation", + "missing", + "check" + ], + "evidence": "Field all transaction-specific fields from transactions.macro validated by SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "issue_pattern": "Missing validation for all transaction-specific fields from transactions.macro", + "why_false_positive": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping) validates all transaction-specific fields from transactions.macro automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "sfTransactionType", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfTransactionType", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfTransactionType for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "sfSequence", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfSequence", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "sfFee", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfFee", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "sfSigningPubKey", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfSigningPubKey", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfSigningPubKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfFlags", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfFlags", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfFlags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSourceTag", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfSourceTag", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfSourceTag for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfPreviousTxnID", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfPreviousTxnID", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfPreviousTxnID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLastLedgerSequence", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfLastLedgerSequence", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfLastLedgerSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccountTxnID", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfAccountTxnID", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfAccountTxnID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfOperationLimit", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfOperationLimit", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfOperationLimit for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfMemos", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfMemos", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfMemos for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTicketSequence", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfTicketSequence", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfTicketSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTxnSignature", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfTxnSignature", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfTxnSignature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSigners", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfSigners", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfSigners for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfNetworkID", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfNetworkID", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfNetworkID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDelegate", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based) at TxFormats::getCommonFields", + "issue_pattern": "Missing empty string validation for sfDelegate", + "why_false_positive": "SOTemplate/SOElement (template-based) validates sfDelegate for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "transaction-specific fields (from transactions.macro)", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate/SOElement (template-based), macro expansion at TxFormats constructor (macro expansion)", + "issue_pattern": "Missing empty string validation for transaction-specific fields (from transactions.macro)", + "why_false_positive": "SOTemplate/SOElement (template-based), macro expansion validates transaction-specific fields (from transactions.macro) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/TxFormats.cpp", + "functions": [ + { + "args": [], + "lineno": 8, + "name": "getCommonFields" + }, + { + "args": [], + "lineno": 27, + "name": "TxFormats" + }, + { + "args": [], + "lineno": 49, + "name": "getInstance" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "Validation of transaction fields via SOTemplate is typically covered by unit tests in files such as 'test/tx/Transaction_test.cpp', 'test/protocol/TxFormats_test.cpp', and integration tests that submit malformed or incomplete transactions. However, direct tests of TxFormats::getCommonFields may be limited, as most tests exercise the validation indirectly via transaction parsing and submission. Edge cases (e.g., missing required fields, wrong types) are usually tested, but coverage for all optional fields and new amendments may be incomplete. There may be gaps in testing for template changes or for fields added via amendments.", + "validation_architecture": { + "auto_validated_fields": [ + "sfTransactionType", + "sfFlags", + "sfSourceTag", + "sfAccount", + "sfSequence", + "sfPreviousTxnID", + "sfLastLedgerSequence", + "sfAccountTxnID", + "sfFee", + "sfOperationLimit", + "sfMemos", + "sfSigningPubKey", + "sfTicketSequence", + "sfTxnSignature", + "sfSigners", + "sfNetworkID", + "sfDelegate", + "all transaction-specific fields from transactions.macro" + ], + "framework": "SOTemplate/SOElement (template-based validation), jss:: (JSON field name mapping)", + "validation_layer": "business_logic (transaction construction/registration)" + }, + "validations": [ + { + "confidence": 0.95, + "error_thrown": "likely throws on missing required field (template enforcement, e.g., std::runtime_error or custom exception)", + "field": "sfTransactionType", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field is present in transaction object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.95, + "error_thrown": "likely throws on missing required field (template enforcement, e.g., std::runtime_error or custom exception)", + "field": "sfAccount", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field is present in transaction object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.95, + "error_thrown": "likely throws on missing required field (template enforcement, e.g., std::runtime_error or custom exception)", + "field": "sfSequence", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field is present in transaction object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.95, + "error_thrown": "likely throws on missing required field (template enforcement, e.g., std::runtime_error or custom exception)", + "field": "sfFee", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field is present in transaction object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.95, + "error_thrown": "likely throws on missing required field (template enforcement, e.g., std::runtime_error or custom exception)", + "field": "sfSigningPubKey", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field is present in transaction object" + ], + "validation_type": "presence (required field)" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfFlags", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfSourceTag", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfPreviousTxnID", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfLastLedgerSequence", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfAccountTxnID", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfOperationLimit", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfMemos", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfTicketSequence", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfTxnSignature", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfSigners", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfNetworkID", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "none (optional field)", + "field": "sfDelegate", + "location": "TxFormats::getCommonFields", + "validated_by": "SOTemplate/SOElement (template-based)", + "validates": [ + "field may be present; if present, type/format checked elsewhere" + ], + "validation_type": "optional presence" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on missing required field or type mismatch (template enforcement, e.g., std::runtime_error or custom exception)", + "field": "transaction-specific fields (from transactions.macro)", + "location": "TxFormats constructor (macro expansion)", + "validated_by": "SOTemplate/SOElement (template-based), macro expansion", + "validates": [ + "field is present if required", + "field type matches SField definition" + ], + "validation_type": "presence (required/optional as per macro), type (via SField/SOElement)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/TxFormats.cpp.ai.md b/src/libxrpl/protocol/TxFormats.cpp.ai.md new file mode 100644 index 0000000000..3c1b354cef --- /dev/null +++ b/src/libxrpl/protocol/TxFormats.cpp.ai.md @@ -0,0 +1,51 @@ +# `TxFormats.cpp` — Transaction Format Registry + +`TxFormats.cpp` is the central registry for every transaction type the XRP Ledger protocol recognizes. Its job is to declare, once and definitively, what fields each transaction is permitted to carry, whether those fields are required or optional, and which fields are universal across all transactions. Every path that parses, constructs, or validates a transaction — wire deserialization via `STObject`, JSON submission via `doSubmit`, or programmatic construction via `Transaction` — traces back to the templates built here. + +## Inheritance and the Singleton Pattern + +`TxFormats` inherits from `KnownFormats`, a CRTP template defined in `KnownFormats.h`. `KnownFormats` maintains two `boost::container::flat_map` lookup tables — one keyed by the string name of the transaction type (`names_`) and one keyed by the integer `TxType` enum value (`types_`). Each entry in these maps points to a `KnownFormats::Item`, which bundles together the transaction's numeric type, its string name, and a fully-constructed `SOTemplate`. + +`getInstance()` returns the singleton via a function-local `static`, making initialization thread-safe under C++11's guarantee that local statics are initialized exactly once even under concurrent first-calls. The object is expensive to construct (it registers dozens of transaction types), so it must never be re-created; the Meyer's singleton is the right choice. + +## The Common Fields Contract + +`getCommonFields()` returns a static `vector` containing the 17 fields that every XRPL transaction must or may carry, regardless of type. These are intentionally separated from transaction-specific fields so that both categories can be merged into a single `SOTemplate` at registration time without duplication. + +The required fields — `sfTransactionType`, `sfAccount`, `sfSequence`, `sfFee`, and `sfSigningPubKey` — form the minimum viable transaction skeleton. The absence of any of them causes validation to fail immediately when the resulting `SOTemplate` is checked during `STObject` construction. + +Several optional fields tell a story about protocol evolution: +- `sfPreviousTxnID` carries the comment `// emulate027`, signaling backward compatibility with a historical wire format (pre-amendment 027). It is kept optional rather than removed to avoid breaking existing transaction blobs. +- `sfSigners` (annotated `// submit_multisigned`) is the container for multi-signature arrays. It coexists with `sfSigningPubKey` because single-sig and multi-sig are orthogonal modes at the format level. +- `sfTicketSequence` allows a transaction to consume a ticket instead of the account's current sequence number, enabling out-of-order transaction submission. +- `sfNetworkID` was added to let sidechain networks distinguish their transactions from mainnet ones at the wire level. +- `sfDelegate` is the newest addition, supporting the delegation feature that allows an account to authorize another to act on its behalf. + +## The Macro Expansion Technique + +The constructor body is the most architecturally significant part of the file. Rather than manually calling `add()` for each of the dozens of transaction types, it exploits a single `#include` of `transactions.macro` with a bespoke `TRANSACTION` macro definition: + +```cpp +#define TRANSACTION(tag, value, name, delegable, amendment, privileges, fields) \ + add(jss::name, tag, UNWRAP fields, getCommonFields()); +``` + +The `transactions.macro` file contains one `TRANSACTION(...)` invocation per transaction type. Each invocation carries: the `TxType` enum tag (e.g., `ttPAYMENT`), a numeric wire value (e.g., `0`), a C++ class name that maps to a `jss::` string constant, a `Delegation` enum, amendment prerequisites, a privilege bitfield used by `InvariantCheck`, and a parenthesized list of `SOElement` entries specific to that transaction. + +The `UNWRAP(...)` helper macro strips the extra parentheses from the fields argument, which are required because the fields list itself contains commas that would otherwise confuse the preprocessor's argument parsing. This is a standard idiom for passing brace-enclosed initializer lists through variadic macros. + +The `#pragma push_macro` / `#undef` / `#pragma pop_macro` sandwich guards any pre-existing `TRANSACTION` definition in the translation unit — defensive hygiene for a macro that has a common name and might appear in platform headers. + +The `add()` method (inherited from `KnownFormats`) checks for duplicate `TxType` values at construction time and calls `LogicError` if one is found, making type-id collisions a hard crash at startup rather than a silent bug. + +## The SOTemplate and Validation Flow + +When `add()` stores a format, it constructs a `SOTemplate` from the union of the transaction-specific fields and the common fields. `SOTemplate` internally builds a position index from `SField` number to element index, enabling O(1) lookup during serialization. + +At parse time, `STObject::set` calls `findByType` on the `TxFormats` singleton to retrieve the `SOTemplate` for the transaction's type code, then validates every field in the incoming byte stream or JSON object against it. Fields marked `soeREQUIRED` that are missing cause immediate rejection; unknown fields not present in the template are equally rejected. This template-driven validation is why there is no explicit per-field null-checking elsewhere in the codebase — the `SOTemplate` system enforces presence and type for every field declaratively. + +The `SOETxMPTIssue` annotation visible in `transactions.macro` entries (e.g., `{sfAmount, soeREQUIRED, soeMPTSupported}` on `ttPAYMENT`) extends this validation further: `SOElement` carries a flag indicating whether an amount field may carry an MPT (Multi-Purpose Token) amount rather than a classic XRP/IOU amount, enabling the template system to police MPT usage without bespoke code in each transactor. + +## Adding a New Transaction Type + +The design ensures that the only file that must change when a new transaction type is introduced is `transactions.macro`. The `TxFormats` constructor, the `TxType` enum in `TxFormats.h`, and all downstream validation machinery all derive their knowledge from that single macro file, minimizing the surface area for omission bugs. \ No newline at end of file diff --git a/src/libxrpl/protocol/TxMeta.cpp.ai.json b/src/libxrpl/protocol/TxMeta.cpp.ai.json new file mode 100644 index 0000000000..64a60cc897 --- /dev/null +++ b/src/libxrpl/protocol/TxMeta.cpp.ai.json @@ -0,0 +1,597 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "obj.getFieldArray(sfAffectedNodes)", + "obj.getFieldU8(sfTransactionResult)", + "obj.getFieldU32(sfTransactionIndex)", + "dynamic_cast(obj.peekAtPField(sfAffectedNodes))", + "XRPL_ASSERT(affectedNodes, ...)", + "setAdditionalFields(obj)" + ], + "entry_point": "TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "purpose": "Constructs TxMeta from an STObject, extracting and validating transaction metadata fields.", + "validation_points": [ + "dynamic_cast(obj.peekAtPField(sfAffectedNodes))", + "XRPL_ASSERT(affectedNodes, ...)", + "obj.getFieldU8(sfTransactionResult)", + "obj.getFieldU32(sfTransactionIndex)", + "obj.getFieldArray(sfAffectedNodes)" + ] + }, + { + "call_chain": [ + "TxMeta::TxMeta(uint256 const&, std::uint32_t, Blob const&)", + "SerialIter sit(makeSlice(vec))", + "STObject obj(sit, sfMetadata)", + "obj.getFieldU8(sfTransactionResult)", + "obj.getFieldU32(sfTransactionIndex)", + "obj.getFieldArray(sfAffectedNodes)", + "setAdditionalFields(obj)" + ], + "entry_point": "TxMeta::TxMeta(uint256 const&, std::uint32_t, Blob const&)", + "purpose": "Constructs TxMeta from a serialized Blob, deserializing and validating fields.", + "validation_points": [ + "obj.getFieldU8(sfTransactionResult)", + "obj.getFieldU32(sfTransactionIndex)", + "obj.getFieldArray(sfAffectedNodes)" + ] + }, + { + "call_chain": [ + "TxMeta::getAffectedAccounts()", + "for (auto const& node : nodes_)", + "node.getFieldIndex(...)", + "dynamic_cast(&node.peekAtIndex(index))", + "XRPL_ASSERT(inner, ...)", + "for (auto const& field : *inner)", + "dynamic_cast(&field)", + "XRPL_ASSERT(!sa->isDefault(), ...)", + "dynamic_cast(&field)", + "XRPL_ASSERT(lim, ...)" + ], + "entry_point": "TxMeta::getAffectedAccounts()", + "purpose": "Extracts all affected accounts from the transaction metadata, validating types and presence.", + "validation_points": [ + "dynamic_cast(&node.peekAtIndex(index))", + "XRPL_ASSERT(inner, ...)", + "dynamic_cast(&field)", + "XRPL_ASSERT(!sa->isDefault(), ...)", + "dynamic_cast(&field)", + "XRPL_ASSERT(lim, ...)" + ] + } + ], + "data_flows": [ + { + "field": "sfAffectedNodes", + "flow": [ + "obj (STObject)", + "obj.getFieldArray(sfAffectedNodes)", + "nodes_ (TxMeta member)", + "used in getAffectedAccounts(), setAffectedNode(), etc." + ], + "origin": "STObject input parameter (obj)", + "transformations": [ + "dynamic_cast for type validation", + "XRPL_ASSERT to ensure cast succeeded", + "Assignment to nodes_" + ], + "validated_at": "dynamic_cast(obj.peekAtPField(sfAffectedNodes)), XRPL_ASSERT" + }, + { + "field": "sfTransactionResult", + "flow": [ + "obj (STObject)", + "obj.getFieldU8(sfTransactionResult)", + "result_ (TxMeta member)" + ], + "origin": "STObject input parameter (obj)", + "transformations": [ + "getFieldU8 validates presence and type" + ], + "validated_at": "obj.getFieldU8(sfTransactionResult)" + }, + { + "field": "sfTransactionIndex", + "flow": [ + "obj (STObject)", + "obj.getFieldU32(sfTransactionIndex)", + "index_ (TxMeta member)" + ], + "origin": "STObject input parameter (obj)", + "transformations": [ + "getFieldU32 validates presence and type" + ], + "validated_at": "obj.getFieldU32(sfTransactionIndex)" + }, + { + "field": "nodes_", + "flow": [ + "obj.getFieldArray(sfAffectedNodes) or deserialized from Blob", + "assigned to nodes_", + "used in getAffectedAccounts(), setAffectedNode(), etc." + ], + "origin": "Constructed from sfAffectedNodes field of STObject or Blob", + "transformations": [ + "Type checked via dynamic_cast and XRPL_ASSERT", + "May be replaced by *affectedNodes if cast succeeds" + ], + "validated_at": "dynamic_cast(obj.peekAtPField(sfAffectedNodes)), XRPL_ASSERT" + } + ], + "description": "Implements the TxMeta class, which represents transaction metadata in the XRPL, including affected nodes, result codes, and utility methods for manipulating and serializing transaction metadata.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTransactionResult", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTransactionResult validated by XRPL_ASSERT, STObject field accessors (getFieldU8, getFieldU32, getFieldArray), dynamic_cast", + "issue_pattern": "Missing validation for sfTransactionResult", + "why_false_positive": "XRPL_ASSERT, STObject field accessors (getFieldU8, getFieldU32, getFieldArray), dynamic_cast validates sfTransactionResult automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTransactionIndex", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTransactionIndex validated by XRPL_ASSERT, STObject field accessors (getFieldU8, getFieldU32, getFieldArray), dynamic_cast", + "issue_pattern": "Missing validation for sfTransactionIndex", + "why_false_positive": "XRPL_ASSERT, STObject field accessors (getFieldU8, getFieldU32, getFieldArray), dynamic_cast validates sfTransactionIndex automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAffectedNodes", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAffectedNodes validated by XRPL_ASSERT, STObject field accessors (getFieldU8, getFieldU32, getFieldArray), dynamic_cast", + "issue_pattern": "Missing validation for sfAffectedNodes", + "why_false_positive": "XRPL_ASSERT, STObject field accessors (getFieldU8, getFieldU32, getFieldArray), dynamic_cast validates sfAffectedNodes automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAffectedNodes (STArray)", + "empty", + "string", + "validation" + ], + "evidence": "dynamic_cast at TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "issue_pattern": "Missing empty string validation for sfAffectedNodes (STArray)", + "why_false_positive": "dynamic_cast validates sfAffectedNodes (STArray) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAffectedNodes (STArray)", + "type", + "validation", + "check" + ], + "evidence": "dynamic_cast at TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "issue_pattern": "Missing type validation for sfAffectedNodes (STArray)", + "why_false_positive": "dynamic_cast validates sfAffectedNodes (STArray) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAffectedNodes (STArray)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(affectedNodes, ...) at TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "issue_pattern": "Missing empty string validation for sfAffectedNodes (STArray)", + "why_false_positive": "XRPL_ASSERT(affectedNodes, ...) validates sfAffectedNodes (STArray) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAffectedNodes (STArray)", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT(affectedNodes, ...) at TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "issue_pattern": "Missing type validation for sfAffectedNodes (STArray)", + "why_false_positive": "XRPL_ASSERT(affectedNodes, ...) validates sfAffectedNodes (STArray) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "obj.getFieldU8(sfTransactionResult)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldU8 at TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "issue_pattern": "Missing empty string validation for obj.getFieldU8(sfTransactionResult)", + "why_false_positive": "getFieldU8 validates obj.getFieldU8(sfTransactionResult) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "obj.getFieldU32(sfTransactionIndex)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldU32 at TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "issue_pattern": "Missing empty string validation for obj.getFieldU32(sfTransactionIndex)", + "why_false_positive": "getFieldU32 validates obj.getFieldU32(sfTransactionIndex) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "obj.getFieldArray(sfAffectedNodes)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldArray at TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "issue_pattern": "Missing empty string validation for obj.getFieldArray(sfAffectedNodes)", + "why_false_positive": "getFieldArray validates obj.getFieldArray(sfAffectedNodes) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "obj.getFieldArray(sfAffectedNodes)", + "type", + "validation", + "check" + ], + "evidence": "getFieldArray at TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "issue_pattern": "Missing type validation for obj.getFieldArray(sfAffectedNodes)", + "why_false_positive": "getFieldArray validates obj.getFieldArray(sfAffectedNodes) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "obj.getFieldU8(sfTransactionResult)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldU8 at TxMeta::TxMeta(uint256 const&, std::uint32_t, Blob const&)", + "issue_pattern": "Missing empty string validation for obj.getFieldU8(sfTransactionResult)", + "why_false_positive": "getFieldU8 validates obj.getFieldU8(sfTransactionResult) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "obj.getFieldU32(sfTransactionIndex)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldU32 at TxMeta::TxMeta(uint256 const&, std::uint32_t, Blob const&)", + "issue_pattern": "Missing empty string validation for obj.getFieldU32(sfTransactionIndex)", + "why_false_positive": "getFieldU32 validates obj.getFieldU32(sfTransactionIndex) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "obj.getFieldArray(sfAffectedNodes)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldArray at TxMeta::TxMeta(uint256 const&, std::uint32_t, Blob const&)", + "issue_pattern": "Missing empty string validation for obj.getFieldArray(sfAffectedNodes)", + "why_false_positive": "getFieldArray validates obj.getFieldArray(sfAffectedNodes) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "obj.getFieldArray(sfAffectedNodes)", + "type", + "validation", + "check" + ], + "evidence": "getFieldArray at TxMeta::TxMeta(uint256 const&, std::uint32_t, Blob const&)", + "issue_pattern": "Missing type validation for obj.getFieldArray(sfAffectedNodes)", + "why_false_positive": "getFieldArray validates obj.getFieldArray(sfAffectedNodes) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "obj.getFName() == type", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at TxMeta::setAffectedNode", + "issue_pattern": "Missing empty string validation for obj.getFName() == type", + "why_false_positive": "XRPL_ASSERT validates obj.getFName() == type for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/TxMeta.cpp", + "functions": [ + { + "args": [ + "uint256 const& txid", + "std::uint32_t ledger", + "STObject const& obj" + ], + "lineno": 13, + "name": "TxMeta" + }, + { + "args": [ + "uint256 const& txid", + "std::uint32_t ledger", + "Blob const& vec" + ], + "lineno": 25, + "name": "TxMeta" + }, + { + "args": [ + "uint256 const& transactionID", + "std::uint32_t ledger" + ], + "lineno": 36, + "name": "TxMeta" + }, + { + "args": [ + "uint256 const& node", + "SField const& type", + "std::uint16_t nodeType" + ], + "lineno": 45, + "name": "setAffectedNode" + }, + { + "args": [], + "lineno": 62, + "name": "getAffectedAccounts" + }, + { + "args": [ + "SLE::ref node", + "SField const& type" + ], + "lineno": 108, + "name": "getAffectedNode" + }, + { + "args": [ + "uint256 const& node" + ], + "lineno": 123, + "name": "getAffectedNode" + }, + { + "args": [], + "lineno": 137, + "name": "getAsObject" + }, + { + "args": [ + "Serializer& s", + "TER result", + "std::uint32_t index" + ], + "lineno": 151, + "name": "addRaw" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + } + ] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "TxMeta is a core protocol class, so it is likely tested in protocol-level and transaction-processing tests. Look for tests in files like 'test/TxMeta_test.cpp', 'test/Transaction_test.cpp', or integration tests that exercise transaction application and metadata. However, direct validation of error paths (e.g., failed dynamic_cast, missing fields) may not be fully covered unless there are explicit negative tests. Template-based validation and XRPL_ASSERTs may not be directly tested unless assertions are enabled in test builds. There may be gaps in coverage for malformed or incomplete STObject/Blob inputs.", + "validation_architecture": { + "auto_validated_fields": [ + "sfTransactionResult", + "sfTransactionIndex", + "sfAffectedNodes" + ], + "framework": "XRPL_ASSERT, STObject field accessors (getFieldU8, getFieldU32, getFieldArray), dynamic_cast", + "validation_layer": "constructor (input validation), business_logic (setAffectedNode)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely throws std::logic_error or aborts)", + "field": "sfAffectedNodes (STArray)", + "location": "TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "validated_by": "dynamic_cast", + "validates": [ + "Ensures sfAffectedNodes field is of type STArray" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely throws std::logic_error or aborts)", + "field": "sfAffectedNodes (STArray)", + "location": "TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "validated_by": "XRPL_ASSERT(affectedNodes, ...)", + "validates": [ + "Ensures affectedNodes pointer is not null after dynamic_cast" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "Throws if field missing or not uint8_t (likely std::runtime_error or custom)", + "field": "obj.getFieldU8(sfTransactionResult)", + "location": "TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "validated_by": "getFieldU8", + "validates": [ + "Ensures sfTransactionResult exists and is uint8_t" + ], + "validation_type": "type|range" + }, + { + "confidence": 0.9, + "error_thrown": "Throws if field missing or not uint32_t (likely std::runtime_error or custom)", + "field": "obj.getFieldU32(sfTransactionIndex)", + "location": "TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "validated_by": "getFieldU32", + "validates": [ + "Ensures sfTransactionIndex exists and is uint32_t" + ], + "validation_type": "type|range" + }, + { + "confidence": 0.9, + "error_thrown": "Throws if field missing or not array (likely std::runtime_error or custom)", + "field": "obj.getFieldArray(sfAffectedNodes)", + "location": "TxMeta::TxMeta(uint256 const&, std::uint32_t, STObject const&)", + "validated_by": "getFieldArray", + "validates": [ + "Ensures sfAffectedNodes exists and is an array" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "Throws if field missing or not uint8_t (likely std::runtime_error or custom)", + "field": "obj.getFieldU8(sfTransactionResult)", + "location": "TxMeta::TxMeta(uint256 const&, std::uint32_t, Blob const&)", + "validated_by": "getFieldU8", + "validates": [ + "Ensures sfTransactionResult exists and is uint8_t" + ], + "validation_type": "type|range" + }, + { + "confidence": 0.9, + "error_thrown": "Throws if field missing or not uint32_t (likely std::runtime_error or custom)", + "field": "obj.getFieldU32(sfTransactionIndex)", + "location": "TxMeta::TxMeta(uint256 const&, std::uint32_t, Blob const&)", + "validated_by": "getFieldU32", + "validates": [ + "Ensures sfTransactionIndex exists and is uint32_t" + ], + "validation_type": "type|range" + }, + { + "confidence": 0.9, + "error_thrown": "Throws if field missing or not array (likely std::runtime_error or custom)", + "field": "obj.getFieldArray(sfAffectedNodes)", + "location": "TxMeta::TxMeta(uint256 const&, std::uint32_t, Blob const&)", + "validated_by": "getFieldArray", + "validates": [ + "Ensures sfAffectedNodes exists and is an array" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (likely throws std::logic_error or aborts)", + "field": "obj.getFName() == type", + "location": "TxMeta::setAffectedNode", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures the field name of the object matches the expected type after insertion" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/TxMeta.cpp.ai.md b/src/libxrpl/protocol/TxMeta.cpp.ai.md new file mode 100644 index 0000000000..e03a498968 --- /dev/null +++ b/src/libxrpl/protocol/TxMeta.cpp.ai.md @@ -0,0 +1,45 @@ +# `TxMeta.cpp` — Transaction Metadata Implementation + +## Role and Purpose + +Every transaction applied to the XRP Ledger produces a metadata record that describes exactly which ledger entries were created, modified, or deleted and what their field values were before and after the transaction. `TxMeta` is the class that constructs, accumulates, and serializes this record. It is a pure protocol artifact: it does not execute any business logic itself, but rather serves as the structured container that `ApplyStateTable` fills in as it processes each ledger entry modification, and that the network then stores alongside the transaction in the closed ledger. + +The metadata has three core pieces: the `sfAffectedNodes` array (a list of `sfCreatedNode`, `sfModifiedNode`, and `sfDeletedNode` entries), a `sfTransactionResult` code (the `TER` value mapped to a `uint8_t`), and a `sfTransactionIndex` giving the transaction's position within its ledger. Two optional fields round it out: `sfDeliveredAmount` (for payment transactions that may deliver less than the requested amount) and `sfParentBatchID` (for inner transactions processed as part of a `Batch` transaction). + +## Construction: Three Entry Points + +The three constructors serve different lifecycle phases. + +The **two-argument constructor** (`transactionID`, `ledger`) builds an empty metadata object during transaction application. `result_` is initialized to 255 (a sentinel marking it as unset) and `index_` to `UINT32_MAX`. The `nodes_` array pre-reserves 32 slots, which is enough for all but the most complex transactions and avoids reallocation during node accumulation. + +The **`Blob` constructor** deserializes previously persisted metadata bytes off disk or over the wire. It constructs a `SerialIter` over the raw bytes, materializes a full `STObject` tagged `sfMetadata`, then extracts the three required fields. This path is used when loading closed-ledger transaction records for RPC queries or transaction history. + +The **`STObject` constructor** handles the case where metadata is already parsed as part of a broader ledger object (e.g., during replay). A subtle quirk: the member initializer list calls `obj.getFieldArray(sfAffectedNodes)` to initialize `nodes_` by value, but the constructor body immediately overwrites `nodes_` again via a `dynamic_cast(obj.peekAtPField(...))`. The redundant first extraction is a mild inefficiency; the real initialization is the body assignment. The `XRPL_ASSERT` guards the cast, ensuring the field's runtime type matches the expected `STArray` — a defense against corrupted or malformed ledger data. + +Both deserialization constructors delegate to `setAdditionalFields()` (defined inline in the header) which conditionally reads `sfDeliveredAmount` and `sfParentBatchID` if present — fields that are optional by protocol. + +## Node Accumulation: `setAffectedNode` and `getAffectedNode` + +The accumulation pattern in `ApplyStateTable` follows a two-phase approach. First, `setAffectedNode(ledgerIndex, sfType, nodeType)` is called to register that a ledger entry is affected by the transaction and to categorize it (created, modified, or deleted). If an entry with the same `sfLedgerIndex` already exists in `nodes_`, its type field is updated in place. Otherwise a new `STObject` is pushed. Second, `getAffectedNode(ledgerIndex)` is called to retrieve the same node object and attach `sfPreviousFields`, `sfFinalFields`, or `sfNewFields` sub-objects describing actual field deltas. + +Both methods do a linear scan over `nodes_`. This is a deliberate choice: the list of affected nodes per transaction is bounded and small (the reservation of 32 slots is rarely exceeded), so a hash map would add overhead without benefit. + +The `getAffectedNode(uint256)` overload — which takes a hash rather than an `SLE::ref` — is an internal variant that asserts the node must already exist. It is only called after `setAffectedNode` has guaranteed registration. If the node is absent anyway, `UNREACHABLE` fires (an instrumentation abort in debug builds) and a `std::runtime_error` is thrown as a last-resort safety net. The `LCOV_EXCL` markers around this path indicate it is intentionally excluded from coverage metrics because correct callers will never trigger it. + +## `getAffectedAccounts()` — Cross-Type Account Extraction + +This method assembles the set of `AccountID` values whose ledger state was touched by the transaction, returning a `boost::container::flat_set` (sorted, compact, cache-friendly). The comment explicitly notes it must match the behavior of the JavaScript `Meta#getAffectedAccounts` method, establishing a cross-platform invariant. + +The logic handles three distinct field shapes: direct `STAccount` fields (e.g., `sfAccount`, `sfDestination`), `STAmount` fields for trust-line limits and order-book amounts that embed issuer IDs (`sfLowLimit`, `sfHighLimit`, `sfTakerPays`, `sfTakerGets`), and `sfMPTokenIssuanceID` — a 192-bit bitstring encoding for the MPToken feature from which an `AccountID` issuer is recovered via `MPTIssue`. For `sfCreatedNode` entries the scan covers `sfNewFields`, while modified and deleted nodes use `sfFinalFields`. This asymmetry reflects the metadata schema: newly created nodes record their initial state in `sfNewFields`, while modified/deleted nodes record their last-known state in `sfFinalFields`. + +Each type-cast is paired with an `XRPL_ASSERT` to validate that the dynamic type matches expectations, followed by a null guard that suppresses any possible crash in production builds where assertions are no-ops. + +## Serialization: `addRaw` and `getAsObject` + +`addRaw()` is the terminal step of metadata construction. It stamps `result_` and `index_` with the actual `TER` outcome and ledger position, then **sorts `nodes_` by `sfLedgerIndex`** before serializing. This sort is critical for determinism: two validators applying the same transaction must produce byte-identical metadata. Without sorting, the order in which `ApplyStateTable` happens to visit modified entries (which is map-iteration order, not guaranteed stable across implementations) would produce non-deterministic blobs. + +`getAsObject()` assembles the `STObject` representation unconditionally. It asserts `result_ != 255` to catch any call made before `addRaw()` has finalized the result — emitting metadata with an uninitialized result would silently corrupt the ledger record. The optional `deliveredAmount_` and `parentBatchID_` fields are written only when present. + +## Relationship to `ApplyStateTable` + +`TxMeta` and `ApplyStateTable` are tightly coupled. The entire accumulation workflow lives in `ApplyStateTable::apply()`: a fresh `TxMeta` is constructed, optional fields are set, then for each pending ledger entry change the table calls `setAffectedNode` followed by `getAffectedNode` to attach field-level deltas. Once all entries are processed, `addRaw` serializes the metadata into a `Serializer`, and both the raw bytes and the `TxMeta` object itself are returned — the former stored in the ledger database, the latter available for diagnostics and RPC responses. \ No newline at end of file diff --git a/src/libxrpl/protocol/UintTypes.cpp.ai.json b/src/libxrpl/protocol/UintTypes.cpp.ai.json new file mode 100644 index 0000000000..d9c9ccfaed --- /dev/null +++ b/src/libxrpl/protocol/UintTypes.cpp.ai.json @@ -0,0 +1,374 @@ +{ + "args": [ + { + "lineno": 19, + "name": "currency" + }, + { + "lineno": 44, + "name": "currency" + }, + { + "lineno": 44, + "name": "code" + }, + { + "lineno": 67, + "name": "code" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "to_currency(std::string const& code)", + "to_currency(Currency& currency, std::string const& code)" + ], + "entry_point": "to_currency(std::string const& code)", + "purpose": "Converts a string currency code to a Currency object, validating the code format.", + "validation_points": [ + "to_currency(Currency&, std::string const&) - Validates code for emptiness, system currency, and allowed charset for 3-char codes" + ] + }, + { + "call_chain": [ + "to_currency(Currency& currency, std::string const& code)" + ], + "entry_point": "to_currency(Currency& currency, std::string const& code)", + "purpose": "Directly converts and validates a string code into a Currency object.", + "validation_points": [ + "Checks for empty/system code, 3-char ISO code charset, and falls back to parseHex" + ] + }, + { + "call_chain": [ + "to_string(Currency const& currency)" + ], + "entry_point": "to_string(Currency const& currency)", + "purpose": "Converts a Currency object to its string representation, validating internal ISO code if present.", + "validation_points": [ + "Checks for system currency, noCurrency, and validates extracted ISO code for allowed charset" + ] + } + ], + "data_flows": [ + { + "field": "code (currency code string)", + "flow": [ + "External input", + "to_currency(std::string const& code)", + "to_currency(Currency& currency, std::string const& code)", + "Currency object" + ], + "origin": "External input (user, config, network, etc.)", + "transformations": [ + "Checked for emptiness or system code", + "If 3-char: validated for allowed charset, copied into Currency at offset", + "Else: parsed as hex" + ], + "validated_at": "to_currency(Currency&, std::string const&)" + }, + { + "field": "currency (Currency object)", + "flow": [ + "Constructed Currency", + "to_string(Currency const& currency)", + "String output" + ], + "origin": "Constructed via to_currency or direct construction", + "transformations": [ + "Checked for beast::zero (system currency)", + "Checked for noCurrency()", + "If ISO bits: extract 3-char code, validate charset", + "Else: hex string" + ], + "validated_at": "to_string(Currency const&)" + }, + { + "field": "iso (3-char extracted from currency)", + "flow": [ + "Currency object", + "to_string(Currency const&)", + "Extracted iso string", + "Validated and possibly returned" + ], + "origin": "Extracted from Currency.data() at offset 12", + "transformations": [ + "Extracted as substring", + "Validated for allowed charset" + ], + "validated_at": "to_string(Currency const&)" + } + ], + "description": "Implements functions for converting between string representations and internal representations of XRPL currency codes, including handling ISO-4217 codes and special XRPL currency constants.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "code (currency code string)", + "empty", + "string", + "validation" + ], + "evidence": "to_currency (explicit code) at to_currency(Currency& currency, std::string const& code)", + "issue_pattern": "Missing empty string validation for code (currency code string)", + "why_false_positive": "to_currency (explicit code) validates code (currency code string) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "code (currency code string)", + "format", + "validation", + "invalid" + ], + "evidence": "to_currency (explicit code) at to_currency(Currency& currency, std::string const& code)", + "issue_pattern": "Missing format validation for code (currency code string)", + "why_false_positive": "to_currency (explicit code) validates code (currency code string) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "code (currency code string)", + "empty", + "string", + "validation" + ], + "evidence": "to_currency (explicit code) at to_currency(Currency& currency, std::string const& code)", + "issue_pattern": "Missing empty string validation for code (currency code string)", + "why_false_positive": "to_currency (explicit code) validates code (currency code string) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "code (currency code string)", + "format", + "validation", + "invalid" + ], + "evidence": "to_currency (explicit code) at to_currency(Currency& currency, std::string const& code)", + "issue_pattern": "Missing format validation for code (currency code string)", + "why_false_positive": "to_currency (explicit code) validates code (currency code string) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "code (currency code string)", + "empty", + "string", + "validation" + ], + "evidence": "to_currency (explicit code) at to_currency(Currency& currency, std::string const& code)", + "issue_pattern": "Missing empty string validation for code (currency code string)", + "why_false_positive": "to_currency (explicit code) validates code (currency code string) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "code (currency code string)", + "format", + "validation", + "invalid" + ], + "evidence": "to_currency (explicit code) at to_currency(Currency& currency, std::string const& code)", + "issue_pattern": "Missing format validation for code (currency code string)", + "why_false_positive": "to_currency (explicit code) validates code (currency code string) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "currency (Currency object)", + "empty", + "string", + "validation" + ], + "evidence": "to_string (explicit code) at to_string(Currency const& currency)", + "issue_pattern": "Missing empty string validation for currency (Currency object)", + "why_false_positive": "to_string (explicit code) validates currency (Currency object) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "iso (3-char extracted from currency)", + "empty", + "string", + "validation" + ], + "evidence": "to_string (explicit code) at to_string(Currency const& currency)", + "issue_pattern": "Missing empty string validation for iso (3-char extracted from currency)", + "why_false_positive": "to_string (explicit code) validates iso (3-char extracted from currency) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "iso (3-char extracted from currency)", + "format", + "validation", + "invalid" + ], + "evidence": "to_string (explicit code) at to_string(Currency const& currency)", + "issue_pattern": "Missing format validation for iso (3-char extracted from currency)", + "why_false_positive": "to_string (explicit code) validates iso (3-char extracted from currency) format" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/UintTypes.cpp", + "functions": [ + { + "args": [ + "currency" + ], + "lineno": 19, + "name": "to_string" + }, + { + "args": [ + "currency", + "code" + ], + "lineno": 44, + "name": "to_currency" + }, + { + "args": [ + "code" + ], + "lineno": 67, + "name": "to_currency" + }, + { + "args": [], + "lineno": 76, + "name": "xrpCurrency" + }, + { + "args": [], + "lineno": 82, + "name": "noCurrency" + }, + { + "args": [], + "lineno": 88, + "name": "badCurrency" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 13, + "name": "detail" + } + ], + "test_coverage_notes": "Typical test coverage would be in protocol or serialization unit tests, e.g., CurrencyTests.cpp, ProtocolTests.cpp, or similar. Tests should cover: valid/invalid 3-char codes, system currency, noCurrency, hex parsing, and to_string roundtrips. Gaps may exist for edge cases: invalid charset in 3-char codes, malformed hex strings, and boundary values for Currency. No test code is shown here, so actual coverage must be checked in the test/ or unittest/ directories.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual/explicit validation)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "code (currency code string)", + "location": "to_currency(Currency& currency, std::string const& code)", + "validated_by": "to_currency (explicit code)", + "validates": [ + "Checks if code is empty", + "Checks if code equals systemCurrencyCode()" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "code (currency code string)", + "location": "to_currency(Currency& currency, std::string const& code)", + "validated_by": "to_currency (explicit code)", + "validates": [ + "Checks if code contains only allowed characters (isoCharSet) when code.size() == 3" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "returns value of currency.parseHex(code) (no exception in this file)", + "field": "code (currency code string)", + "location": "to_currency(Currency& currency, std::string const& code)", + "validated_by": "to_currency (explicit code)", + "validates": [ + "If code is not empty and not 3 chars, attempts to parse as hex" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns systemCurrencyCode() or '1' (no exception)", + "field": "currency (Currency object)", + "location": "to_string(Currency const& currency)", + "validated_by": "to_string (explicit code)", + "validates": [ + "Checks if currency == beast::zero (system currency)", + "Checks if currency == noCurrency()" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns strHex(currency) (no exception)", + "field": "iso (3-char extracted from currency)", + "location": "to_string(Currency const& currency)", + "validated_by": "to_string (explicit code)", + "validates": [ + "Checks if iso != systemCurrencyCode()", + "Checks if iso contains only allowed characters (isoCharSet)" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/UintTypes.cpp.ai.md b/src/libxrpl/protocol/UintTypes.cpp.ai.md new file mode 100644 index 0000000000..eefc5801d9 --- /dev/null +++ b/src/libxrpl/protocol/UintTypes.cpp.ai.md @@ -0,0 +1,53 @@ +# `UintTypes.cpp` — Currency Code Serialization + +This file implements the string-to-`Currency` and `Currency`-to-string conversion functions for the XRPL protocol. It is the single authoritative place where the on-wire binary representation of a currency code is mapped to and from a human-readable string, and where the protocol's sentinel currency values are defined. + +## The `Currency` Type and Its Layout + +`Currency` is a `base_uint<160, detail::CurrencyTag>` — a 160-bit (20-byte) opaque value. The XRPL [serialization spec](https://xrpl.org/serialization.html#currency-codes) carves this 20-byte field into regions: bytes 0–11 and 15–19 must be zero for an ISO-style currency, while bytes 12–14 hold the three-character ASCII code. The constants in `detail::` encode this layout: + +```cpp +constexpr std::size_t isoCodeOffset = 12; // byte offset of 3-char code +constexpr std::size_t isoCodeLength = 3; +``` + +The bitmask `sIsoBits` (`FFFFFFFFFFFFFFFFFFFFFFFF000000FFFFFFFFFF`) has zeros only at those three bytes. Anding it against a currency value and testing for zero confirms that every bit outside the ISO region is unset — the necessary condition before treating the value as an ISO code. + +## Sentinel Currency Values + +Three singleton `Currency` values represent special protocol states: + +- **`xrpCurrency()`** — all zeros (`beast::zero`). This is XRP, the ledger's native asset. The zero value is the protocol's canonical representation; `isXRP()` checks this directly. +- **`noCurrency()`** — value `1`. A placeholder used internally when no currency is specified (e.g., in data structures that must hold *some* value). +- **`badCurrency()`** — value `0x5852500000000000`. A deliberately poisoned sentinel. The header comment explains the motivation: early developers would sometimes encode "XRP" as a three-letter ISO currency rather than using the all-zero canonical form. `badCurrency()` exists to be a distinct, recognizable error value that marks this misuse. Crucially, `to_currency()` may return it (via `parseHex`) and callers are warned by the header comments that this legacy behavior is preserved to avoid breaking existing call sites. + +All three are function-local statics, ensuring they are lazily initialized once and never destroyed — a common XRPL idiom for protocol constants that must survive the entire process lifetime. + +## `to_string()`: Decoding a 160-bit Value + +The conversion from `Currency` to string applies a priority-ordered decision tree: + +1. If the value is `beast::zero`, return `"XRP"` (via `systemCurrencyCode()`). +2. If the value is `noCurrency()`, return `"1"`. +3. Apply the `sIsoBits` mask. If all non-ISO bits are zero, extract the three bytes at offset 12 and validate them against `isoCharSet`. If valid *and* not equal to `"XRP"`, return the three-character string. +4. Fall through to `strHex(currency)` — a raw 40-character hex representation. + +Step 3's guard against returning `"XRP"` is significant: it prevents any currency value with "XRP" in the ISO position (but non-zero bytes elsewhere, or the zero padding in the right place) from being mistakenly printed as the native currency string. If such a value exists in the ledger, it surfaces as hex, making the anomaly visible. + +`isoCharSet` is deliberately broad — it includes uppercase and lowercase letters, digits, and a set of symbols (`<>(){}[]|?!@#$%^&*`). This is wider than strict ISO 4217 (which only allows `[A-Z]`), reflecting XRPL's extended custom-currency ecosystem. + +## `to_currency()`: Parsing a String + +The overload `to_currency(Currency&, std::string const&)` returns a `bool` and is the validating entry point: + +- An empty string or `"XRP"` sets the currency to `beast::zero` and returns `true`. +- A three-character string whose characters are all in `isoCharSet` is accepted as an ISO code: the currency is zeroed, then the three bytes are copied into position at `isoCodeOffset`. Reject otherwise. +- Any other string is forwarded to `currency.parseHex(code)`, accepting 40-character hex strings representing arbitrary 160-bit values. This path can return `badCurrency()` if the hex happens to encode it. + +The value-returning overload `to_currency(std::string const&)` wraps this, returning `noCurrency()` on failure rather than propagating a boolean. Callers that need to distinguish parse failure from "no currency" should use the reference-output overload and check the return value. + +## Design Observations + +The file contains no exceptions. All error conditions are signaled through return values (`bool` or sentinel values), consistent with the XRPL codebase's general preference for value-based error handling in protocol-layer code. The `detail::` namespace isolates the layout constants from the public API, keeping the header clean while making the encoding strategy self-documenting in the implementation file. + +The round-trip property (`to_string(to_currency(s)) == s`) holds for well-formed ISO codes and hex strings, but breaks for the edge cases by design: `"XRP"` round-trips to the zero currency which prints as `"XRP"`, while a currency whose ISO region contains `"XRP"` with zero surroundings round-trips to hex — both consistent with the protocol's intent to keep the native currency unambiguous. \ No newline at end of file diff --git a/src/libxrpl/protocol/XChainAttestations.cpp.ai.json b/src/libxrpl/protocol/XChainAttestations.cpp.ai.json new file mode 100644 index 0000000000..b07097a080 --- /dev/null +++ b/src/libxrpl/protocol/XChainAttestations.cpp.ai.json @@ -0,0 +1,1137 @@ +{ + "args": [ + { + "lineno": 18, + "name": "attestationSignerAccount_" + }, + { + "lineno": 19, + "name": "publicKey_" + }, + { + "lineno": 20, + "name": "signature_" + }, + { + "lineno": 21, + "name": "sendingAccount_" + }, + { + "lineno": 22, + "name": "sendingAmount_" + }, + { + "lineno": 23, + "name": "rewardAccount_" + }, + { + "lineno": 24, + "name": "wasLockingChainSend_" + }, + { + "lineno": 78, + "name": "claimID_" + }, + { + "lineno": 79, + "name": "dst_" + }, + { + "lineno": 90, + "name": "secretKey_" + }, + { + "lineno": 182, + "name": "createCount_" + }, + { + "lineno": 183, + "name": "toCreate_" + }, + { + "lineno": 180, + "name": "rewardAmount_" + }, + { + "lineno": 283, + "name": "keyAccount_" + }, + { + "lineno": 284, + "name": "amount_" + }, + { + "lineno": 356, + "name": "rewardAmount_" + } + ], + "classes": [ + { + "args": [ + "attestationSignerAccount_", + "publicKey_", + "signature_", + "sendingAccount_", + "sendingAmount_", + "rewardAccount_", + "wasLockingChainSend_" + ], + "lineno": 17, + "name": "AttestationBase" + }, + { + "args": [ + "attestationSignerAccount_", + "publicKey_", + "signature_", + "sendingAccount_", + "sendingAmount_", + "rewardAccount_", + "wasLockingChainSend_", + "claimID_", + "dst_" + ], + "lineno": 77, + "name": "AttestationClaim" + }, + { + "args": [ + "attestationSignerAccount_", + "publicKey_", + "signature_", + "sendingAccount_", + "sendingAmount_", + "rewardAmount_", + "rewardAccount_", + "wasLockingChainSend_", + "createCount_", + "toCreate_" + ], + "lineno": 181, + "name": "AttestationCreateAccount" + }, + { + "args": [ + "keyAccount_", + "publicKey_", + "amount_", + "rewardAccount_", + "wasLockingChainSend_", + "dst_" + ], + "lineno": 282, + "name": "XChainClaimAttestation" + }, + { + "args": [ + "keyAccount_", + "publicKey_", + "amount_", + "rewardAmount_", + "rewardAccount_", + "wasLockingChainSend_", + "dst_" + ], + "lineno": 355, + "name": "XChainCreateAccountAttestation" + }, + { + "args": [ + "AttCollection&& atts" + ], + "lineno": 414, + "name": "XChainAttestationsBase" + } + ], + "code_paths": [ + { + "call_chain": [ + "AttestationBase::AttestationBase(Json::Value const& v)", + "Json::getOrThrow(v, field)" + ], + "entry_point": "AttestationBase::AttestationBase(Json::Value const& v)", + "purpose": "Constructs an AttestationBase from JSON input, validating and extracting all required fields.", + "validation_points": [ + "Json::getOrThrow(v, sfAttestationSignerAccount)", + "Json::getOrThrow(v, sfPublicKey)", + "Json::getOrThrow(v, sfSignature)", + "Json::getOrThrow(v, sfAccount)", + "Json::getOrThrow(v, sfAmount)", + "Json::getOrThrow(v, sfAttestationRewardAccount)", + "Json::getOrThrow(v, sfWasLockingChainSend)" + ] + }, + { + "call_chain": [ + "AttestationBase::AttestationBase(STObject const& o)", + "o[sfField]" + ], + "entry_point": "AttestationBase::AttestationBase(STObject const& o)", + "purpose": "Constructs an AttestationBase from a serialized object (STObject), extracting fields directly (assumes prior validation).", + "validation_points": [] + }, + { + "call_chain": [ + "AttestationBase::verify", + "AttestationBase::message", + "xrpl::verify(publicKey, makeSlice(msg), signature)" + ], + "entry_point": "AttestationBase::verify(STXChainBridge const& bridge) const", + "purpose": "Verifies the attestation's signature using the public key and message.", + "validation_points": [ + "publicKey, signature validated at construction (see above)" + ] + } + ], + "data_flows": [ + { + "field": "attestationSignerAccount", + "flow": [ + "Json::Value v or STObject o", + "AttestationBase::AttestationBase", + "this->attestationSignerAccount", + "used in equalHelper, sameEventHelper, addHelper, verify" + ], + "origin": "Json::Value input or STObject input", + "transformations": [ + "Parsed and validated via Json::getOrThrow if from JSON" + ], + "validated_at": "AttestationBase::AttestationBase(Json::Value const& v)" + }, + { + "field": "publicKey", + "flow": [ + "Json::Value v or STObject o", + "AttestationBase::AttestationBase", + "this->publicKey", + "used in verify, equalHelper, addHelper" + ], + "origin": "Json::Value input or STObject input", + "transformations": [ + "Parsed and validated via Json::getOrThrow if from JSON" + ], + "validated_at": "AttestationBase::AttestationBase(Json::Value const& v)" + }, + { + "field": "signature", + "flow": [ + "Json::Value v or STObject o", + "AttestationBase::AttestationBase", + "this->signature", + "used in verify, equalHelper, addHelper" + ], + "origin": "Json::Value input or STObject input", + "transformations": [ + "Parsed and validated via Json::getOrThrow if from JSON" + ], + "validated_at": "AttestationBase::AttestationBase(Json::Value const& v)" + }, + { + "field": "sendingAccount", + "flow": [ + "Json::Value v or STObject o", + "AttestationBase::AttestationBase", + "this->sendingAccount", + "used in sameEventHelper, equalHelper, addHelper" + ], + "origin": "Json::Value input or STObject input", + "transformations": [ + "Parsed and validated via Json::getOrThrow if from JSON" + ], + "validated_at": "AttestationBase::AttestationBase(Json::Value const& v)" + }, + { + "field": "sendingAmount", + "flow": [ + "Json::Value v or STObject o", + "AttestationBase::AttestationBase", + "this->sendingAmount", + "used in sameEventHelper, equalHelper, addHelper" + ], + "origin": "Json::Value input or STObject input", + "transformations": [ + "Parsed and validated via Json::getOrThrow if from JSON" + ], + "validated_at": "AttestationBase::AttestationBase(Json::Value const& v)" + }, + { + "field": "rewardAccount", + "flow": [ + "Json::Value v or STObject o", + "AttestationBase::AttestationBase", + "this->rewardAccount", + "used in equalHelper, addHelper" + ], + "origin": "Json::Value input or STObject input", + "transformations": [ + "Parsed and validated via Json::getOrThrow if from JSON" + ], + "validated_at": "AttestationBase::AttestationBase(Json::Value const& v)" + }, + { + "field": "wasLockingChainSend", + "flow": [ + "Json::Value v or STObject o", + "AttestationBase::AttestationBase", + "this->wasLockingChainSend", + "used in sameEventHelper, equalHelper, addHelper" + ], + "origin": "Json::Value input or STObject input", + "transformations": [ + "Parsed and validated via Json::getOrThrow if from JSON" + ], + "validated_at": "AttestationBase::AttestationBase(Json::Value const& v)" + } + ], + "description": "Implements classes and logic for XRPL cross-chain attestations, including claim and account creation attestations, their serialization/deserialization, equality, and matching logic, as well as container classes for collections of attestations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "attestationSignerAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field attestationSignerAccount validated by xrpl::Json::getOrThrow, STObject field accessors", + "issue_pattern": "Missing validation for attestationSignerAccount", + "why_false_positive": "xrpl::Json::getOrThrow, STObject field accessors validates attestationSignerAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "publicKey", + "validation", + "missing", + "check" + ], + "evidence": "Field publicKey validated by xrpl::Json::getOrThrow, STObject field accessors", + "issue_pattern": "Missing validation for publicKey", + "why_false_positive": "xrpl::Json::getOrThrow, STObject field accessors validates publicKey automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "signature", + "validation", + "missing", + "check" + ], + "evidence": "Field signature validated by xrpl::Json::getOrThrow, STObject field accessors", + "issue_pattern": "Missing validation for signature", + "why_false_positive": "xrpl::Json::getOrThrow, STObject field accessors validates signature automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sendingAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field sendingAccount validated by xrpl::Json::getOrThrow, STObject field accessors", + "issue_pattern": "Missing validation for sendingAccount", + "why_false_positive": "xrpl::Json::getOrThrow, STObject field accessors validates sendingAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sendingAmount", + "validation", + "missing", + "check" + ], + "evidence": "Field sendingAmount validated by xrpl::Json::getOrThrow, STObject field accessors", + "issue_pattern": "Missing validation for sendingAmount", + "why_false_positive": "xrpl::Json::getOrThrow, STObject field accessors validates sendingAmount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "rewardAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field rewardAccount validated by xrpl::Json::getOrThrow, STObject field accessors", + "issue_pattern": "Missing validation for rewardAccount", + "why_false_positive": "xrpl::Json::getOrThrow, STObject field accessors validates rewardAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "wasLockingChainSend", + "validation", + "missing", + "check" + ], + "evidence": "Field wasLockingChainSend validated by xrpl::Json::getOrThrow, STObject field accessors", + "issue_pattern": "Missing validation for wasLockingChainSend", + "why_false_positive": "xrpl::Json::getOrThrow, STObject field accessors validates wasLockingChainSend automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "attestationSignerAccount", + "empty", + "string", + "validation" + ], + "evidence": "Json::getOrThrow at AttestationBase(Json::Value const& v) constructor", + "issue_pattern": "Missing empty string validation for attestationSignerAccount", + "why_false_positive": "Json::getOrThrow validates attestationSignerAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "publicKey", + "empty", + "string", + "validation" + ], + "evidence": "Json::getOrThrow at AttestationBase(Json::Value const& v) constructor", + "issue_pattern": "Missing empty string validation for publicKey", + "why_false_positive": "Json::getOrThrow validates publicKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "signature", + "empty", + "string", + "validation" + ], + "evidence": "Json::getOrThrow at AttestationBase(Json::Value const& v) constructor", + "issue_pattern": "Missing empty string validation for signature", + "why_false_positive": "Json::getOrThrow validates signature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sendingAccount", + "empty", + "string", + "validation" + ], + "evidence": "Json::getOrThrow at AttestationBase(Json::Value const& v) constructor", + "issue_pattern": "Missing empty string validation for sendingAccount", + "why_false_positive": "Json::getOrThrow validates sendingAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sendingAmount", + "empty", + "string", + "validation" + ], + "evidence": "Json::getOrThrow at AttestationBase(Json::Value const& v) constructor", + "issue_pattern": "Missing empty string validation for sendingAmount", + "why_false_positive": "Json::getOrThrow validates sendingAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rewardAccount", + "empty", + "string", + "validation" + ], + "evidence": "Json::getOrThrow at AttestationBase(Json::Value const& v) constructor", + "issue_pattern": "Missing empty string validation for rewardAccount", + "why_false_positive": "Json::getOrThrow validates rewardAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "wasLockingChainSend", + "empty", + "string", + "validation" + ], + "evidence": "Json::getOrThrow at AttestationBase(Json::Value const& v) constructor", + "issue_pattern": "Missing empty string validation for wasLockingChainSend", + "why_false_positive": "Json::getOrThrow validates wasLockingChainSend for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "wasLockingChainSend", + "type", + "validation", + "check" + ], + "evidence": "Json::getOrThrow at AttestationBase(Json::Value const& v) constructor", + "issue_pattern": "Missing type validation for wasLockingChainSend", + "why_false_positive": "Json::getOrThrow validates wasLockingChainSend type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "attestationSignerAccount", + "empty", + "string", + "validation" + ], + "evidence": "STObject field accessor (o[sfAttestationSignerAccount]) at AttestationBase(STObject const& o) constructor", + "issue_pattern": "Missing empty string validation for attestationSignerAccount", + "why_false_positive": "STObject field accessor (o[sfAttestationSignerAccount]) validates attestationSignerAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "publicKey", + "empty", + "string", + "validation" + ], + "evidence": "STObject field accessor (o[sfPublicKey]) at AttestationBase(STObject const& o) constructor", + "issue_pattern": "Missing empty string validation for publicKey", + "why_false_positive": "STObject field accessor (o[sfPublicKey]) validates publicKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "signature", + "empty", + "string", + "validation" + ], + "evidence": "STObject field accessor (o[sfSignature]) at AttestationBase(STObject const& o) constructor", + "issue_pattern": "Missing empty string validation for signature", + "why_false_positive": "STObject field accessor (o[sfSignature]) validates signature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sendingAccount", + "empty", + "string", + "validation" + ], + "evidence": "STObject field accessor (o[sfAccount]) at AttestationBase(STObject const& o) constructor", + "issue_pattern": "Missing empty string validation for sendingAccount", + "why_false_positive": "STObject field accessor (o[sfAccount]) validates sendingAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sendingAmount", + "empty", + "string", + "validation" + ], + "evidence": "STObject field accessor (o[sfAmount]) at AttestationBase(STObject const& o) constructor", + "issue_pattern": "Missing empty string validation for sendingAmount", + "why_false_positive": "STObject field accessor (o[sfAmount]) validates sendingAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "rewardAccount", + "empty", + "string", + "validation" + ], + "evidence": "STObject field accessor (o[sfAttestationRewardAccount]) at AttestationBase(STObject const& o) constructor", + "issue_pattern": "Missing empty string validation for rewardAccount", + "why_false_positive": "STObject field accessor (o[sfAttestationRewardAccount]) validates rewardAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "wasLockingChainSend", + "empty", + "string", + "validation" + ], + "evidence": "STObject field accessor (bool(o[sfWasLockingChainSend])) at AttestationBase(STObject const& o) constructor", + "issue_pattern": "Missing empty string validation for wasLockingChainSend", + "why_false_positive": "STObject field accessor (bool(o[sfWasLockingChainSend])) validates wasLockingChainSend for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "wasLockingChainSend", + "type", + "validation", + "check" + ], + "evidence": "STObject field accessor (bool(o[sfWasLockingChainSend])) at AttestationBase(STObject const& o) constructor", + "issue_pattern": "Missing type validation for wasLockingChainSend", + "why_false_positive": "STObject field accessor (bool(o[sfWasLockingChainSend])) validates wasLockingChainSend type" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/XChainAttestations.cpp", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 27, + "name": "AttestationBase::equalHelper" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 37, + "name": "AttestationBase::sameEventHelper" + }, + { + "args": [ + "bridge" + ], + "lineno": 43, + "name": "AttestationBase::verify" + }, + { + "args": [ + "o" + ], + "lineno": 67, + "name": "AttestationBase::addHelper" + }, + { + "args": [], + "lineno": 120, + "name": "AttestationClaim::toSTObject" + }, + { + "args": [ + "bridge", + "sendingAccount", + "sendingAmount", + "rewardAccount", + "wasLockingChainSend", + "claimID", + "dst" + ], + "lineno": 132, + "name": "AttestationClaim::message" + }, + { + "args": [ + "bridge" + ], + "lineno": 148, + "name": "AttestationClaim::message" + }, + { + "args": [], + "lineno": 152, + "name": "AttestationClaim::validAmounts" + }, + { + "args": [ + "rhs" + ], + "lineno": 156, + "name": "AttestationClaim::sameEvent" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 161, + "name": "operator==" + }, + { + "args": [], + "lineno": 210, + "name": "AttestationCreateAccount::toSTObject" + }, + { + "args": [ + "bridge", + "sendingAccount", + "sendingAmount", + "rewardAmount", + "rewardAccount", + "wasLockingChainSend", + "createCount", + "dst" + ], + "lineno": 223, + "name": "AttestationCreateAccount::message" + }, + { + "args": [ + "bridge" + ], + "lineno": 239, + "name": "AttestationCreateAccount::message" + }, + { + "args": [], + "lineno": 243, + "name": "AttestationCreateAccount::validAmounts" + }, + { + "args": [ + "rhs" + ], + "lineno": 247, + "name": "AttestationCreateAccount::sameEvent" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 252, + "name": "operator==" + }, + { + "args": [], + "lineno": 312, + "name": "XChainClaimAttestation::toSTObject" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 324, + "name": "operator==" + }, + { + "args": [ + "rhs" + ], + "lineno": 337, + "name": "XChainClaimAttestation::match" + }, + { + "args": [], + "lineno": 374, + "name": "XChainCreateAccountAttestation::toSTObject" + }, + { + "args": [ + "rhs" + ], + "lineno": 397, + "name": "XChainCreateAccountAttestation::match" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 404, + "name": "operator==" + }, + { + "args": [], + "lineno": 423, + "name": "XChainAttestationsBase::begin" + }, + { + "args": [], + "lineno": 428, + "name": "XChainAttestationsBase::end" + }, + { + "args": [], + "lineno": 433, + "name": "XChainAttestationsBase::begin" + }, + { + "args": [], + "lineno": 438, + "name": "XChainAttestationsBase::end" + }, + { + "args": [], + "lineno": 463, + "name": "XChainAttestationsBase::toSTArray" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + }, + { + "lineno": 16, + "name": "Attestations" + } + ], + "test_coverage_notes": "Test coverage for this code is likely found in test files related to cross-chain attestations, such as 'test_xchain_attestations.cpp', 'test_xchain_claim.cpp', or similar files in the 'test' or 'unittest' directories. These tests should cover construction from JSON (including invalid/missing fields), signature verification, and equality/event comparison. Gaps may exist if tests do not cover all possible invalid JSON inputs, malformed signatures, or edge cases in field values. There is no evidence in this file of explicit test hooks or coverage annotations.", + "validation_architecture": { + "auto_validated_fields": [ + "attestationSignerAccount", + "publicKey", + "signature", + "sendingAccount", + "sendingAmount", + "rewardAccount", + "wasLockingChainSend" + ], + "framework": "xrpl::Json::getOrThrow, STObject field accessors", + "validation_layer": "entry_point (constructor input validation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via getOrThrow)", + "field": "attestationSignerAccount", + "location": "AttestationBase(Json::Value const& v) constructor", + "validated_by": "Json::getOrThrow", + "validates": [ + "Checks that the JSON field exists", + "Checks that the field is a valid AccountID (correct type/format)" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via getOrThrow)", + "field": "publicKey", + "location": "AttestationBase(Json::Value const& v) constructor", + "validated_by": "Json::getOrThrow", + "validates": [ + "Checks that the JSON field exists", + "Checks that the field is a valid PublicKey (correct type/format)" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via getOrThrow)", + "field": "signature", + "location": "AttestationBase(Json::Value const& v) constructor", + "validated_by": "Json::getOrThrow", + "validates": [ + "Checks that the JSON field exists", + "Checks that the field is a valid Buffer (correct type/format)" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via getOrThrow)", + "field": "sendingAccount", + "location": "AttestationBase(Json::Value const& v) constructor", + "validated_by": "Json::getOrThrow", + "validates": [ + "Checks that the JSON field exists", + "Checks that the field is a valid AccountID (correct type/format)" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via getOrThrow)", + "field": "sendingAmount", + "location": "AttestationBase(Json::Value const& v) constructor", + "validated_by": "Json::getOrThrow", + "validates": [ + "Checks that the JSON field exists", + "Checks that the field is a valid STAmount (correct type/format)" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via getOrThrow)", + "field": "rewardAccount", + "location": "AttestationBase(Json::Value const& v) constructor", + "validated_by": "Json::getOrThrow", + "validates": [ + "Checks that the JSON field exists", + "Checks that the field is a valid AccountID (correct type/format)" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via getOrThrow)", + "field": "wasLockingChainSend", + "location": "AttestationBase(Json::Value const& v) constructor", + "validated_by": "Json::getOrThrow", + "validates": [ + "Checks that the JSON field exists", + "Checks that the field is a boolean" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws if field missing or wrong type (STObject accessor)", + "field": "attestationSignerAccount", + "location": "AttestationBase(STObject const& o) constructor", + "validated_by": "STObject field accessor (o[sfAttestationSignerAccount])", + "validates": [ + "Checks that the STObject field exists", + "Checks that the field is a valid AccountID" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws if field missing or wrong type (STObject accessor)", + "field": "publicKey", + "location": "AttestationBase(STObject const& o) constructor", + "validated_by": "STObject field accessor (o[sfPublicKey])", + "validates": [ + "Checks that the STObject field exists", + "Checks that the field is a valid PublicKey" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws if field missing or wrong type (STObject accessor)", + "field": "signature", + "location": "AttestationBase(STObject const& o) constructor", + "validated_by": "STObject field accessor (o[sfSignature])", + "validates": [ + "Checks that the STObject field exists", + "Checks that the field is a valid Buffer" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws if field missing or wrong type (STObject accessor)", + "field": "sendingAccount", + "location": "AttestationBase(STObject const& o) constructor", + "validated_by": "STObject field accessor (o[sfAccount])", + "validates": [ + "Checks that the STObject field exists", + "Checks that the field is a valid AccountID" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws if field missing or wrong type (STObject accessor)", + "field": "sendingAmount", + "location": "AttestationBase(STObject const& o) constructor", + "validated_by": "STObject field accessor (o[sfAmount])", + "validates": [ + "Checks that the STObject field exists", + "Checks that the field is a valid STAmount" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws if field missing or wrong type (STObject accessor)", + "field": "rewardAccount", + "location": "AttestationBase(STObject const& o) constructor", + "validated_by": "STObject field accessor (o[sfAttestationRewardAccount])", + "validates": [ + "Checks that the STObject field exists", + "Checks that the field is a valid AccountID" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws if field missing or wrong type (STObject accessor)", + "field": "wasLockingChainSend", + "location": "AttestationBase(STObject const& o) constructor", + "validated_by": "STObject field accessor (bool(o[sfWasLockingChainSend]))", + "validates": [ + "Checks that the STObject field exists", + "Checks that the field is convertible to bool" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/XChainAttestations.cpp.ai.md b/src/libxrpl/protocol/XChainAttestations.cpp.ai.md new file mode 100644 index 0000000000..7881410d5d --- /dev/null +++ b/src/libxrpl/protocol/XChainAttestations.cpp.ai.md @@ -0,0 +1,38 @@ +# XChainAttestations.cpp + +This file implements the attestation type system for XRPL's cross-chain bridge protocol. Bridges connect a "locking chain" and an "issuing chain," with a set of independent witness servers observing events on each chain and cryptographically attesting that specific transfers occurred. `XChainAttestations.cpp` defines the data structures that carry those signed proofs both as wire format (for transactions submitted by witnesses) and as ledger state (for aggregated proofs stored in claim ID objects). + +## Two Parallel Hierarchies + +The design deliberately splits attestation representation into two distinct namespaces and class families: + +**Signing-side types** live in `namespace Attestations` and represent a complete attestation as submitted by a witness server: they carry the raw `Buffer signature`, the signer's `AccountID`, a `PublicKey`, the source event details, and the reward routing information. `AttestationBase` holds all the fields common to both transfer types. `AttestationClaim` extends it with a `claimID` (the monotonic counter that prevents replay) and an optional `dst` (destination override). `AttestationCreateAccount` instead carries `createCount`, the mandatory `toCreate` account, and a `rewardAmount` — the additional field needed for the account-bootstrapping flow. + +**Ledger-storage types** (`XChainClaimAttestation`, `XChainCreateAccountAttestation`) are what actually persists in the ledger's claim ID entries. They strip out the raw signature, reducing stored data to only the `keyAccount`, `publicKey`, amounts, and routing fields. The `TSignedAttestation` typedef inside each ledger type explicitly names the corresponding signing-side type, making the semantic link clear without runtime coupling. The conversion constructors from `TSignedAttestation` project the signing-side representation into the ledger representation in a single step. + +## Message Serialization and Signing + +Each `Attestations::` subtype exposes both a static `message()` overload accepting all fields explicitly, and an instance overload that delegates to it. The static form lets the test harness (`attester.cpp`) sign attestations without constructing an object, while the instance form supports `verify()` via the abstract virtual `message()` on `AttestationBase`. + +The serialized message is built by populating an `STObject{sfGeneric}` and calling `Serializer::add()` on it — identical to how ledger objects are canonically serialized. A comment in both `AttestationClaim::message()` and `AttestationCreateAccount::message()` explains that fields are written in `SField` order to ease implementation of independent Python serializers, a cross-ecosystem compatibility concern. The resulting bytes are then signed with `xrpl::sign()` or verified with `xrpl::verify()`. + +`AttestationBase::verify()` calls the virtual `message()` to regenerate the canonical bytes from the stored fields and tests them against the `publicKey`/`signature` pair. This is called during transaction preflight in `XChainBridge.cpp` (`attestationPreflight`) as the first rejection gate — if the witness's signature doesn't check out, the transaction returns `temXCHAIN_BAD_PROOF` before any state is touched. + +## The AttestationMatch Three-State Result + +The `match()` method on the ledger-storage types returns `AttestationMatch`, an enum with three values: `match`, `matchExceptDst`, and `nonDstMismatch`. This nuance reflects an intentional semantic difference between two transaction types: + +- When a witness submits `XChainAddClaimAttestation`, the destination in the accumulated attestations must all agree (`match` required). +- When a user submits `XChainClaim` explicitly, they specify the destination themselves, so collected attestations with a different destination are still eligible; `matchExceptDst` is acceptable. + +The `claimHelper` function in `XChainBridge.cpp` passes a `CheckDst` flag and branches on this result, accepting `matchExceptDst` when the user is claiming explicitly. Without the three-state enum this logic would require two separate matching passes. + +## Container and Bounds Defense + +`XChainAttestationsBase` is a thin template wrapper around a `std::vector` that enforces a hard cap of `maxAttestations = 256` at construction time from both `STArray` and `Json::Value` inputs. The comment notes this is far above any realistic witness-set size; the limit exists purely to bound memory allocation and processing time against malformed or malicious input. The protected destructor prevents external code from slicing instances of the concrete final subclasses `XChainClaimAttestations` and `XChainCreateAccountAttestations`. + +The template is explicitly instantiated at the bottom of the `.cpp` for both concrete types. This keeps all the template method bodies in the `.cpp` translation unit rather than the header, which would otherwise require every translation unit including the header to see and compile the full implementation. + +## sameEvent vs. Equality + +`sameEvent()` on the `Attestations::` types checks whether two attestations witness the same cross-chain event — same `sendingAccount`, `sendingAmount`, `wasLockingChainSend`, and for claims the same `claimID` and `dst`. It deliberately ignores the signer identity fields (`attestationSignerAccount`, `publicKey`, `signature`). Full `operator==` requires all fields to match. The separation is used when processing incoming attestations: an existing attestation for the same event from a different witness should be counted toward quorum, not treated as a duplicate entry. \ No newline at end of file diff --git a/src/libxrpl/protocol/digest.cpp.ai.json b/src/libxrpl/protocol/digest.cpp.ai.json new file mode 100644 index 0000000000..90e583d207 --- /dev/null +++ b/src/libxrpl/protocol/digest.cpp.ai.json @@ -0,0 +1,413 @@ +{ + "args": [ + { + "lineno": 15, + "name": "data" + }, + { + "lineno": 15, + "name": "size" + }, + { + "lineno": 40, + "name": "data" + }, + { + "lineno": 40, + "name": "size" + }, + { + "lineno": 65, + "name": "data" + }, + { + "lineno": 65, + "name": "size" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "openssl_ripemd160_hasher" + }, + { + "args": [], + "lineno": 33, + "name": "openssl_sha512_hasher" + }, + { + "args": [], + "lineno": 58, + "name": "openssl_sha256_hasher" + } + ], + "code_paths": [ + { + "call_chain": [ + "openssl_ripemd160_hasher::openssl_ripemd160_hasher", + "static_assert (sizeof(ctx_) == sizeof(RIPEMD160_CTX))", + "RIPEMD160_Init(ctx_)" + ], + "entry_point": "openssl_ripemd160_hasher::openssl_ripemd160_hasher", + "purpose": "Constructs a RIPEMD160 hasher, validates internal buffer size, initializes context.", + "validation_points": [ + "static_assert (sizeof(ctx_) == sizeof(RIPEMD160_CTX))" + ] + }, + { + "call_chain": [ + "openssl_ripemd160_hasher::operator()", + "RIPEMD160_Update(ctx_, data, size)" + ], + "entry_point": "openssl_ripemd160_hasher::operator()", + "purpose": "Feeds data into the RIPEMD160 hash context.", + "validation_points": [] + }, + { + "call_chain": [ + "openssl_ripemd160_hasher::operator result_type", + "RIPEMD160_Final(digest, ctx_)" + ], + "entry_point": "openssl_ripemd160_hasher::operator result_type", + "purpose": "Finalizes the RIPEMD160 hash and returns the digest.", + "validation_points": [] + }, + { + "call_chain": [ + "openssl_sha512_hasher::openssl_sha512_hasher", + "static_assert (sizeof(ctx_) == sizeof(SHA512_CTX))", + "SHA512_Init(ctx_)" + ], + "entry_point": "openssl_sha512_hasher::openssl_sha512_hasher", + "purpose": "Constructs a SHA512 hasher, validates internal buffer size, initializes context.", + "validation_points": [ + "static_assert (sizeof(ctx_) == sizeof(SHA512_CTX))" + ] + }, + { + "call_chain": [ + "openssl_sha512_hasher::operator()", + "SHA512_Update(ctx_, data, size)" + ], + "entry_point": "openssl_sha512_hasher::operator()", + "purpose": "Feeds data into the SHA512 hash context.", + "validation_points": [] + }, + { + "call_chain": [ + "openssl_sha512_hasher::operator result_type", + "SHA512_Final(digest, ctx_)" + ], + "entry_point": "openssl_sha512_hasher::operator result_type", + "purpose": "Finalizes the SHA512 hash and returns the digest.", + "validation_points": [] + }, + { + "call_chain": [ + "openssl_sha256_hasher::openssl_sha256_hasher", + "static_assert (sizeof(ctx_) == sizeof(SHA256_CTX))", + "SHA256_Init(ctx_)" + ], + "entry_point": "openssl_sha256_hasher::openssl_sha256_hasher", + "purpose": "Constructs a SHA256 hasher, validates internal buffer size, initializes context.", + "validation_points": [ + "static_assert (sizeof(ctx_) == sizeof(SHA256_CTX))" + ] + }, + { + "call_chain": [ + "openssl_sha256_hasher::operator()", + "SHA256_Update(ctx_, data, size)" + ], + "entry_point": "openssl_sha256_hasher::operator()", + "purpose": "Feeds data into the SHA256 hash context.", + "validation_points": [] + }, + { + "call_chain": [ + "openssl_sha256_hasher::operator result_type", + "SHA256_Final(digest, ctx_)" + ], + "entry_point": "openssl_sha256_hasher::operator result_type", + "purpose": "Finalizes the SHA256 hash and returns the digest.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "ctx_ (internal context buffer)", + "flow": [ + "Declared as ctx_ in hasher class", + "Validated for size in constructor via static_assert", + "Reinterpreted as OpenSSL context pointer", + "Initialized by OpenSSL *_Init", + "Updated by OpenSSL *_Update with input data", + "Finalized by OpenSSL *_Final to produce digest" + ], + "origin": "Member variable of each hasher class (openssl_ripemd160_hasher, openssl_sha512_hasher, openssl_sha256_hasher)", + "transformations": [ + "Casted to OpenSSL context type", + "Initialized (zeroed/seeded)", + "Mutated by Update with input data", + "Read and finalized to produce digest" + ], + "validated_at": "Constructor (static_assert on size)" + }, + { + "field": "data (input to operator())", + "flow": [ + "Passed to operator()(void const* data, std::size_t size)", + "Fed into OpenSSL *_Update(ctx_, data, size)", + "Mutates ctx_" + ], + "origin": "Caller of operator() (user of hasher class)", + "transformations": [ + "No transformation in this code; passed directly to OpenSSL" + ], + "validated_at": "No explicit validation in this code" + }, + { + "field": "digest (result_type)", + "flow": [ + "Created in operator result_type()", + "Filled by OpenSSL *_Final(digest.data(), ctx_)", + "Returned to caller" + ], + "origin": "Local variable in operator result_type()", + "transformations": [ + "Filled with hash result" + ], + "validated_at": "No explicit validation in this code" + } + ], + "description": "This file implements cryptographic hashers (RIPEMD-160, SHA-256, SHA-512) using OpenSSL for the XRPL protocol, providing wrapper classes for hashing operations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ctx_ (internal context buffer for each hasher)", + "validation", + "missing", + "check" + ], + "evidence": "Field ctx_ (internal context buffer for each hasher) validated by C++ static_assert (compile-time type validation)", + "issue_pattern": "Missing validation for ctx_ (internal context buffer for each hasher)", + "why_false_positive": "C++ static_assert (compile-time type validation) validates ctx_ (internal context buffer for each hasher) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ctx_ (internal context buffer)", + "empty", + "string", + "validation" + ], + "evidence": "static_assert at openssl_ripemd160_hasher::openssl_ripemd160_hasher (constructor)", + "issue_pattern": "Missing empty string validation for ctx_ (internal context buffer)", + "why_false_positive": "static_assert validates ctx_ (internal context buffer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ctx_ (internal context buffer)", + "type", + "validation", + "check" + ], + "evidence": "static_assert at openssl_ripemd160_hasher::openssl_ripemd160_hasher (constructor)", + "issue_pattern": "Missing type validation for ctx_ (internal context buffer)", + "why_false_positive": "static_assert validates ctx_ (internal context buffer) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ctx_ (internal context buffer)", + "empty", + "string", + "validation" + ], + "evidence": "static_assert at openssl_sha512_hasher::openssl_sha512_hasher (constructor)", + "issue_pattern": "Missing empty string validation for ctx_ (internal context buffer)", + "why_false_positive": "static_assert validates ctx_ (internal context buffer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ctx_ (internal context buffer)", + "type", + "validation", + "check" + ], + "evidence": "static_assert at openssl_sha512_hasher::openssl_sha512_hasher (constructor)", + "issue_pattern": "Missing type validation for ctx_ (internal context buffer)", + "why_false_positive": "static_assert validates ctx_ (internal context buffer) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ctx_ (internal context buffer)", + "empty", + "string", + "validation" + ], + "evidence": "static_assert at openssl_sha256_hasher::openssl_sha256_hasher (constructor)", + "issue_pattern": "Missing empty string validation for ctx_ (internal context buffer)", + "why_false_positive": "static_assert validates ctx_ (internal context buffer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ctx_ (internal context buffer)", + "type", + "validation", + "check" + ], + "evidence": "static_assert at openssl_sha256_hasher::openssl_sha256_hasher (constructor)", + "issue_pattern": "Missing type validation for ctx_ (internal context buffer)", + "why_false_positive": "static_assert validates ctx_ (internal context buffer) type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/digest.cpp", + "functions": [ + { + "args": [], + "lineno": 8, + "name": "openssl_ripemd160_hasher::openssl_ripemd160_hasher" + }, + { + "args": [ + "data", + "size" + ], + "lineno": 15, + "name": "openssl_ripemd160_hasher::operator()" + }, + { + "args": [], + "lineno": 21, + "name": "openssl_ripemd160_hasher::operator result_type" + }, + { + "args": [], + "lineno": 33, + "name": "openssl_sha512_hasher::openssl_sha512_hasher" + }, + { + "args": [ + "data", + "size" + ], + "lineno": 40, + "name": "openssl_sha512_hasher::operator()" + }, + { + "args": [], + "lineno": 46, + "name": "openssl_sha512_hasher::operator result_type" + }, + { + "args": [], + "lineno": 58, + "name": "openssl_sha256_hasher::openssl_sha256_hasher" + }, + { + "args": [ + "data", + "size" + ], + "lineno": 65, + "name": "openssl_sha256_hasher::operator()" + }, + { + "args": [], + "lineno": 71, + "name": "openssl_sha256_hasher::operator result_type" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file contains only implementation, not tests. Typical test coverage would be in files like 'test/digest_test.cpp', 'test/hasher_test.cpp', or similar. Tests should verify: (1) correct hash output for known inputs, (2) that the hasher classes can be constructed and used, (3) that ctx_ is the correct size (indirectly via static_assert). Gaps: No runtime validation of input data (e.g., null pointers, size==0), no explicit error handling for OpenSSL failures, and static_asserts are only compile-time checks. If test files do not exist or do not cover edge cases (e.g., very large inputs, multiple updates, empty input), those would be gaps.", + "validation_architecture": { + "auto_validated_fields": [ + "ctx_ (internal context buffer for each hasher)" + ], + "framework": "C++ static_assert (compile-time type validation)", + "validation_layer": "constructor (class initialization)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Compilation error (static_assert failure)", + "field": "ctx_ (internal context buffer)", + "location": "openssl_ripemd160_hasher::openssl_ripemd160_hasher (constructor)", + "validated_by": "static_assert", + "validates": [ + "Ensures that the size of ctx_ matches the size of RIPEMD160_CTX" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "Compilation error (static_assert failure)", + "field": "ctx_ (internal context buffer)", + "location": "openssl_sha512_hasher::openssl_sha512_hasher (constructor)", + "validated_by": "static_assert", + "validates": [ + "Ensures that the size of ctx_ matches the size of SHA512_CTX" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "Compilation error (static_assert failure)", + "field": "ctx_ (internal context buffer)", + "location": "openssl_sha256_hasher::openssl_sha256_hasher (constructor)", + "validated_by": "static_assert", + "validates": [ + "Ensures that the size of ctx_ matches the size of SHA256_CTX" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/digest.cpp.ai.md b/src/libxrpl/protocol/digest.cpp.ai.md new file mode 100644 index 0000000000..e763723312 --- /dev/null +++ b/src/libxrpl/protocol/digest.cpp.ai.md @@ -0,0 +1,47 @@ +# `src/libxrpl/protocol/digest.cpp` + +## Role in the System + +This file provides the concrete implementations of XRPL's three foundational cryptographic hashers: RIPEMD-160, SHA-256, and SHA-512. These primitives underpin nearly every cryptographic operation in the ledger — from deriving account IDs to computing transaction hashes and validating signatures. The implementation lives in `libxrpl` rather than application code because these hashers are part of the protocol's public API surface, shared across both the server and any downstream SDK consumers. + +## The Opaque-Buffer Design + +The most architecturally significant detail here is not in the `.cpp` at all — it is the private member declaration in the header. Each struct stores its OpenSSL context in a raw `char ctx_[]` array (96 bytes for `RIPEMD160_CTX`, 216 bytes for `SHA512_CTX`, 112 bytes for `SHA256_CTX`) rather than directly declaring the OpenSSL type. This is a deliberate header-isolation technique: the public `digest.h` header does not need to `#include` any OpenSSL header, keeping OpenSSL a private dependency of the library rather than a transitive include for every file that computes a hash. + +The cost of this design is that the `.cpp` must recover the real OpenSSL type through `reinterpret_cast`: + +```cpp +auto const ctx = reinterpret_cast(ctx_); +``` + +The `reinterpret_cast` is only safe if `ctx_` is exactly the right size and alignment for the target type. Each constructor therefore opens with a `static_assert`: + +```cpp +static_assert(sizeof(decltype(openssl_ripemd160_hasher::ctx_)) == sizeof(RIPEMD160_CTX), ""); +``` + +This is a compile-time firewall: if an OpenSSL upgrade ever changes the size of `RIPEMD160_CTX`, `SHA512_CTX`, or `SHA256_CTX`, the build breaks loudly at the only point in the code where the cast is made, rather than silently corrupting memory at runtime. The sizes are hardcoded in the header (`char ctx_[96]{}`) and must be kept in sync with the OpenSSL ABI — the `static_assert` enforces that invariant without requiring OpenSSL headers in `digest.h`. + +## The Hasher Interface Contract + +The three classes follow the **N3980 `Hasher` concept** ("Types Don't Know #"), a C++ proposal for a uniform hashing interface. Each class exposes: + +- `operator()(void const* data, std::size_t size) noexcept` — feeds a chunk of bytes into the running digest. +- `explicit operator result_type() noexcept` — finalises the digest and returns it as a fixed-size `std::array`. +- A `static constexpr endian` field indicating byte-order expectations for the `hash_append` machinery. + +The `noexcept` on both operators is intentional: OpenSSL's low-level `*_Update` and `*_Final` functions do not throw, and propagating exceptions from a hashing call site would be both surprising and unnecessary. Callers relying on this interface in `beast::hash_append` chains can therefore hash safely inside destructors or other `noexcept` contexts. + +## Composed Hashers Defined in the Header + +The `.cpp` only covers the three primitives, but `digest.h` builds two higher-level hashers directly on top of them: + +**`ripesha_hasher`** chains SHA-256 into RIPEMD-160. Its `operator result_type()` calls `sha256_hasher`'s conversion operator to get the 32-byte intermediate digest, then feeds that into a fresh `ripemd160_hasher` and finalises it. This RIPEMD-160(SHA-256(*)) construction is exactly how XRPL derives the 160-bit account identifier from a public key — it applies regardless of whether the key is secp256k1 or Ed25519. + +**`basic_sha512_half_hasher`** wraps `sha512_hasher` and truncates its 64-byte output to the first 32 bytes, producing the `uint256` type that XRPL calls the *SHA-512 Half*. This is used pervasively for ledger object IDs, transaction hashes, and signing digests. The `Secure` template parameter controls whether the destructor calls `secure_erase` on the internal `sha512_hasher` state — the `sha512_half_hasher_s` alias selects the secure variant for key-material contexts where leaving hash state in memory is a security risk. + +The convenience function `sha512Half(args...)` (and its secure twin `sha512Half_s`) rounds out the API: it constructs a `sha512_half_hasher`, feeds all arguments through `beast::hash_append`, and returns the resulting `uint256`. + +## Error Handling and Resource Management + +There is no dynamic allocation and no explicit error handling for OpenSSL failures. The OpenSSL `*_Init`, `*_Update`, and `*_Final` functions operate entirely on stack-allocated context structs and return error codes that this wrapper ignores. In practice these calls are infallible for valid, correctly-sized context buffers — the only realistic failure mode (a corrupt context) is already precluded by the `static_assert` size check at construction. No RAII guard beyond the `static_assert` is needed, since `char ctx_[N]{}` is zero-initialised at construction and lives on the stack for the duration of the hash operation. \ No newline at end of file diff --git a/src/libxrpl/protocol/tokens.cpp.ai.json b/src/libxrpl/protocol/tokens.cpp.ai.json new file mode 100644 index 0000000000..8b9aa83729 --- /dev/null +++ b/src/libxrpl/protocol/tokens.cpp.ai.json @@ -0,0 +1,609 @@ +{ + "args": [ + { + "lineno": 61, + "name": "data" + }, + { + "lineno": 61, + "name": "size" + }, + { + "lineno": 68, + "name": "v" + }, + { + "lineno": 74, + "name": "args" + }, + { + "lineno": 86, + "name": "out" + }, + { + "lineno": 86, + "name": "message" + }, + { + "lineno": 86, + "name": "size" + }, + { + "lineno": 98, + "name": "type" + }, + { + "lineno": 98, + "name": "token" + }, + { + "lineno": 98, + "name": "size" + }, + { + "lineno": 107, + "name": "s" + }, + { + "lineno": 107, + "name": "type" + }, + { + "lineno": 122, + "name": "message" + }, + { + "lineno": 122, + "name": "size" + }, + { + "lineno": 122, + "name": "temp" + }, + { + "lineno": 122, + "name": "temp_size" + }, + { + "lineno": 151, + "name": "s" + }, + { + "lineno": 185, + "name": "type" + }, + { + "lineno": 185, + "name": "token" + }, + { + "lineno": 185, + "name": "size" + }, + { + "lineno": 206, + "name": "s" + }, + { + "lineno": 206, + "name": "type" + }, + { + "lineno": 241, + "name": "input" + }, + { + "lineno": 241, + "name": "out" + }, + { + "lineno": 326, + "name": "input" + }, + { + "lineno": 326, + "name": "out" + }, + { + "lineno": 429, + "name": "token_type" + }, + { + "lineno": 429, + "name": "input" + }, + { + "lineno": 429, + "name": "out" + }, + { + "lineno": 453, + "name": "type" + }, + { + "lineno": 453, + "name": "s" + }, + { + "lineno": 453, + "name": "outBuf" + }, + { + "lineno": 491, + "name": "type" + }, + { + "lineno": 491, + "name": "token" + }, + { + "lineno": 491, + "name": "size" + }, + { + "lineno": 510, + "name": "s" + }, + { + "lineno": 510, + "name": "type" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "decodeBase58Token", + "b58_ref::DecodeBase58", + "decodeBase58Token (internal validation)", + "safe_cast" + ], + "entry_point": "decodeBase58Token", + "purpose": "Decodes a Base58-encoded token string, validates its structure, type, and checksum, and returns the decoded payload and token type.", + "validation_points": [ + "b58_ref::DecodeBase58 (validates Base58 encoding)", + "decodeBase58Token (validates payload length, prefix, checksum)", + "safe_cast (validates token type enum)" + ] + }, + { + "call_chain": [ + "encodeBase58Token", + "encodeBase58" + ], + "entry_point": "encodeBase58Token", + "purpose": "Encodes a payload and token type into a Base58-encoded token string, including prefix and checksum.", + "validation_points": [ + "encodeBase58Token (may validate payload length/type before encoding)" + ] + } + ], + "data_flows": [ + { + "field": "input_string (Base58-encoded token)", + "flow": [ + "input_string", + "decodeBase58Token", + "b58_ref::DecodeBase58", + "decodeBase58Token (payload extraction, prefix/type/length/cksum validation)", + "safe_cast (token type enum validation)", + "decoded payload and type returned" + ], + "origin": "External input (e.g., user, network, API)", + "transformations": [ + "Base58 decoding", + "Payload extraction", + "Prefix/type/length/cksum validation", + "Type enum conversion" + ], + "validated_at": "b58_ref::DecodeBase58, decodeBase58Token, safe_cast" + }, + { + "field": "payload (binary data)", + "flow": [ + "decodeBase58Token", + "payload extracted from decoded bytes", + "payload validated (length, prefix, checksum)", + "payload returned or used" + ], + "origin": "Decoded from Base58 string", + "transformations": [ + "Extracted from decoded bytes", + "Validated for length, prefix, checksum" + ], + "validated_at": "decodeBase58Token" + }, + { + "field": "token_type (enum)", + "flow": [ + "decodeBase58Token", + "prefix byte extracted", + "safe_cast to enum", + "enum used in return value" + ], + "origin": "Prefix byte in decoded payload", + "transformations": [ + "Prefix byte to enum conversion" + ], + "validated_at": "decodeBase58Token (prefix check), safe_cast" + } + ], + "description": "This file implements base58 encoding and decoding routines for XRPL tokens, including fast and reference algorithms, checksum calculation, and token type validation. It provides functions to encode and decode tokens in base58 format, with support for both generic and platform-specific implementations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Base58-encoded input string", + "empty", + "string", + "validation" + ], + "evidence": "b58_ref::DecodeBase58 at decodeBase58Token / b58_ref::DecodeBase58", + "issue_pattern": "Missing empty string validation for Base58-encoded input string", + "why_false_positive": "b58_ref::DecodeBase58 validates Base58-encoded input string for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Base58-encoded input string", + "format", + "validation", + "invalid" + ], + "evidence": "b58_ref::DecodeBase58 at decodeBase58Token / b58_ref::DecodeBase58", + "issue_pattern": "Missing format validation for Base58-encoded input string", + "why_false_positive": "b58_ref::DecodeBase58 validates Base58-encoded input string format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Decoded payload length", + "empty", + "string", + "validation" + ], + "evidence": "decodeBase58Token at decodeBase58Token", + "issue_pattern": "Missing empty string validation for Decoded payload length", + "why_false_positive": "decodeBase58Token validates Decoded payload length for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "Decoded payload length", + "range", + "bounds", + "validation" + ], + "evidence": "decodeBase58Token at decodeBase58Token", + "issue_pattern": "Missing range validation for Decoded payload length", + "why_false_positive": "decodeBase58Token validates Decoded payload length range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Token type prefix", + "empty", + "string", + "validation" + ], + "evidence": "decodeBase58Token at decodeBase58Token", + "issue_pattern": "Missing empty string validation for Token type prefix", + "why_false_positive": "decodeBase58Token validates Token type prefix for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Checksum", + "empty", + "string", + "validation" + ], + "evidence": "decodeBase58Token at decodeBase58Token", + "issue_pattern": "Missing empty string validation for Checksum", + "why_false_positive": "decodeBase58Token validates Checksum for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Checksum", + "format", + "validation", + "invalid" + ], + "evidence": "decodeBase58Token at decodeBase58Token", + "issue_pattern": "Missing format validation for Checksum", + "why_false_positive": "decodeBase58Token validates Checksum format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Token type (enum)", + "empty", + "string", + "validation" + ], + "evidence": "safe_cast at decodeBase58Token", + "issue_pattern": "Missing empty string validation for Token type (enum)", + "why_false_positive": "safe_cast validates Token type (enum) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "Token type (enum)", + "type", + "validation", + "check" + ], + "evidence": "safe_cast at decodeBase58Token", + "issue_pattern": "Missing type validation for Token type (enum)", + "why_false_positive": "safe_cast validates Token type (enum) type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol/tokens.cpp", + "functions": [ + { + "args": [ + "data", + "size" + ], + "lineno": 61, + "name": "digest" + }, + { + "args": [ + "v" + ], + "lineno": 68, + "name": "digest" + }, + { + "args": [ + "args..." + ], + "lineno": 74, + "name": "digest2" + }, + { + "args": [ + "out", + "message", + "size" + ], + "lineno": 86, + "name": "checksum" + }, + { + "args": [ + "type", + "token", + "size" + ], + "lineno": 98, + "name": "encodeBase58Token" + }, + { + "args": [ + "s", + "type" + ], + "lineno": 107, + "name": "decodeBase58Token" + }, + { + "args": [ + "message", + "size", + "temp", + "temp_size" + ], + "lineno": 122, + "name": "encodeBase58" + }, + { + "args": [ + "s" + ], + "lineno": 151, + "name": "decodeBase58" + }, + { + "args": [ + "type", + "token", + "size" + ], + "lineno": 185, + "name": "encodeBase58Token" + }, + { + "args": [ + "s", + "type" + ], + "lineno": 206, + "name": "decodeBase58Token" + }, + { + "args": [ + "input", + "out" + ], + "lineno": 241, + "name": "b256_to_b58_be" + }, + { + "args": [ + "input", + "out" + ], + "lineno": 326, + "name": "b58_to_b256_be" + }, + { + "args": [ + "token_type", + "input", + "out" + ], + "lineno": 429, + "name": "encodeBase58Token" + }, + { + "args": [ + "type", + "s", + "outBuf" + ], + "lineno": 453, + "name": "decodeBase58Token" + }, + { + "args": [ + "type", + "token", + "size" + ], + "lineno": 491, + "name": "encodeBase58Token" + }, + { + "args": [ + "s", + "type" + ], + "lineno": 510, + "name": "decodeBase58Token" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 59, + "name": "xrpl" + }, + { + "lineno": 113, + "name": "b58_ref" + }, + { + "lineno": 115, + "name": "detail" + }, + { + "lineno": 238, + "name": "b58_fast" + }, + { + "lineno": 240, + "name": "detail" + } + ], + "test_coverage_notes": "The main validation logic is in decodeBase58Token and b58_ref::DecodeBase58. Typical test coverage would be in protocol/tokens_test.cpp or similar, testing valid/invalid Base58 strings, payload lengths, prefixes, checksums, and token type enums. Gaps may exist if edge cases (e.g., malformed Base58, wrong prefix, invalid checksum, or out-of-range enum values) are not exhaustively tested. Tests should also cover round-trip encode/decode. If fuzzing or property-based tests are absent, some malformed or boundary cases may be untested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (xrpl/protocol/detail/b58_utils.h, xrpl/protocol/detail/token_errors.h)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "TokenError::badBase58", + "field": "Base58-encoded input string", + "location": "decodeBase58Token / b58_ref::DecodeBase58", + "validated_by": "b58_ref::DecodeBase58", + "validates": [ + "Checks that input string only contains valid Base58 characters", + "Checks for leading/trailing whitespace", + "Checks for empty input" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "TokenError::badTokenLength", + "field": "Decoded payload length", + "location": "decodeBase58Token", + "validated_by": "decodeBase58Token", + "validates": [ + "Checks that decoded payload length matches expected length for token type" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "TokenError::badTokenType", + "field": "Token type prefix", + "location": "decodeBase58Token", + "validated_by": "decodeBase58Token", + "validates": [ + "Checks that the prefix bytes match the expected token type" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "TokenError::badTokenChecksum", + "field": "Checksum", + "location": "decodeBase58Token", + "validated_by": "decodeBase58Token", + "validates": [ + "Computes and verifies the 4-byte checksum at the end of the decoded payload" + ], + "validation_type": "format" + }, + { + "confidence": 0.8, + "error_thrown": "std::bad_cast or assertion", + "field": "Token type (enum)", + "location": "decodeBase58Token", + "validated_by": "safe_cast", + "validates": [ + "Casts prefix bytes to enum type, ensuring valid enum value" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/protocol/tokens.cpp.ai.md b/src/libxrpl/protocol/tokens.cpp.ai.md new file mode 100644 index 0000000000..cadf8339a4 --- /dev/null +++ b/src/libxrpl/protocol/tokens.cpp.ai.md @@ -0,0 +1,59 @@ +# `src/libxrpl/protocol/tokens.cpp` + +## Role in the System + +This file is the single source of truth for XRPL's Base58Check encoding and decoding — the scheme that turns raw binary account IDs, key pairs, and seeds into the human-readable strings that XRPL users interact with every day (`rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh`, `aKEusmsH9dJvjfeEg8XhDfpEgmhkK1VywK`, etc.). It is derived from Bitcoin Core's reference implementation but diverges in alphabet, version byte values, and — crucially — introduces a 10-15× faster encoding path that avoids the O(n²) big-integer arithmetic of the original. + +## Token Format + +Every encoded XRPL identifier follows the same wire layout before Base58 encoding: + +``` +[ type byte (1) ][ raw payload (N) ][ checksum (4) ] +``` + +The type byte is drawn from the `TokenType` enum: `AccountID = 0`, `NodePublic = 28`, `FamilySeed = 33`, etc. The checksum is the first four bytes of SHA-256(SHA-256(type || payload)) — identical to Bitcoin's checksum design. On decode, all three components are verified before the raw payload is returned. Returning an empty string (reference path) or a `TokenCodecErrc` error (fast path) signals any mismatch. + +## XRPL Alphabet + +The Base58 alphabet `"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz"` is deliberately crafted so that `AccountID = 0` encoded addresses begin with the letter `r`. This is not cosmetic — it lets users and validators instantly recognise a classic XRPL account address before any decoding work occurs. The `alphabetReverse` lookup table is built as a `constexpr` 256-element array at compile time, mapping each ASCII code to its base-58 digit value or `-1` for invalid characters. + +## Two Implementations, One Interface + +The file exposes a single top-level pair of functions (`encodeBase58Token` / `decodeBase58Token` in namespace `xrpl`) that dispatch at compile time between two internal namespaces: + +**`b58_ref`** — A portable implementation adapted from Bitcoin Core. Encoding iterates over each input byte and repeatedly multiplies a base-58 work buffer by 256, adding the new byte as carry. Decoding does the inverse: for each input character it multiplies a base-256 buffer by 58 and adds the digit. Both are O(input × work-buffer) in time, which is O(n²) in the number of bytes. + +**`b58_fast`** — Available on all non-MSVC compilers (guarded by `#ifndef _MSC_VER`) because it relies on GCC's `unsigned __int128`. This implementation achieves 10-15× speedup by staging the base conversion through an intermediate representation. + +## The Fast Algorithm's Key Insight + +The file's opening block comment explains the mathematical foundations in detail. The core idea is that converting directly from base 58 to base 256 requires handling an O(n) multi-precision number for every input character. The fast algorithm avoids this by adding an intermediate step: + +``` +base 58 → base 58^10 → base 2^64 → base 2^8 +``` + +`58^10 = 430804206899405824`, which is the largest power of 58 that fits in a 64-bit register. This choice is strategic: ten consecutive base-58 digits can be accumulated into a single `uint64_t` without overflow, converting the first "hop" from O(n) big-integer operations down to simple 64-bit arithmetic. The second hop (base 58^10 → base 2^64) still requires multi-precision work, but operates on far fewer, much larger coefficients — for a 38-byte payload (the maximum: 1 type + 33 public key + 4 checksum) only 5 `uint64_t` coefficients are needed. The final hop (base 2^64 → base 2^8) is just big-endian byte extraction. + +The multi-precision helpers in `b58_utils.h` — `carrying_mul`, `carrying_add`, `inplace_bigint_div_rem`, `inplace_bigint_mul`, `inplace_bigint_add` — use `unsigned __int128` to handle the carry out of each 64-bit word cleanly. The key insight in `carrying_mul` is that `uint64_t × uint64_t` can overflow a 64-bit register, but `uint128_t × uint128_t + carry` does not overflow 128 bits for reasonable inputs, making the carry extraction reliable. + +## Encoding Path (`b256_to_b58_be`) + +The fast encoder works in three stages: it interprets the input bytes as a big-endian number represented in 64-bit limbs, repeatedly divides by `58^10` using `inplace_bigint_div_rem` to extract base-58^10 coefficients, then converts each coefficient to 10 base-58 digits using `b58_10_to_b58_be`, and finally maps those digits through `alphabetForward`. Leading zero bytes are handled separately — each maps to the first alphabet character ('r') to preserve the encoding's bijectivity for inputs with significant leading zeros. + +## Decoding Path (`b58_to_b256_be`) + +The fast decoder reverses the process. It groups the input string into chunks of 10 characters (with a possible partial chunk at the start), accumulates each group into a `uint64_t` coefficient, then synthesises the full big-integer value by iterating through the base-58^10 coefficients and using `inplace_bigint_mul` + `inplace_bigint_add`. The resulting array of `uint64_t` limbs is then written out as big-endian bytes. + +After the base conversion, `decodeBase58Token` applies the three-layer validation: it checks the decoded length is at least 6 bytes, verifies the leading type byte matches the expected `TokenType`, and recomputes the 4-byte checksum to confirm it matches the trailing bytes. Only if all three checks pass is the interior payload returned. + +## Error Handling and API Design + +The fast implementation uses `B58Result>` — an alias for `Expected` backed by `TokenCodecErrc` — to communicate typed errors (`inputTooLarge`, `mismatchedTokenType`, `mismatchedChecksum`, `invalidEncodingChar`, etc.) without exceptions. The reference implementation signals failure by returning an empty `std::string`. + +The top-level `encodeBase58Token` / `decodeBase58Token` string-returning overloads in the `b58_fast` namespace bridge between the span-based zero-allocation API and the legacy `std::string` interface, pre-allocating 128 bytes for encode and 64 bytes for decode — both sized generously above the theoretical maximum (≈46 base-58 characters for a 33-byte key) to avoid re-allocation while keeping stack usage bounded. Both overloads simply return an empty string on any error, preserving backward compatibility with callers that tested for an empty result. + +## Relationship to Other Files + +`tokens.h` declares all public entry points and the `TokenType` enum. `b58_utils.h` provides the inline arithmetic primitives behind the fast path; keeping them header-inline allows the compiler to aggressively inline and optimise the inner loops, which is important given how frequently address encoding runs. `token_errors.h` defines `TokenCodecErrc` and registers it as a `std::error_code` category, enabling integration with standard error-propagation machinery. The checksum uses `sha256_hasher` from `digest.h`, computed as a double-SHA256 — the same algorithm Bitcoin uses, ensuring the design remains auditable against well-understood prior art. \ No newline at end of file diff --git a/src/libxrpl/protocol_autogen/placeholder.cpp.ai.json b/src/libxrpl/protocol_autogen/placeholder.cpp.ai.json new file mode 100644 index 0000000000..087290a987 --- /dev/null +++ b/src/libxrpl/protocol_autogen/placeholder.cpp.ai.json @@ -0,0 +1,41 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [], + "entry_point": "N/A (No functions defined in placeholder.cpp)", + "purpose": "This file is a placeholder and does not implement any logic or functions. It only includes header files to ensure the protocol_autogen module can be built.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "N/A", + "flow": [], + "origin": "No fields are defined or manipulated in this file.", + "transformations": [], + "validated_at": "N/A" + } + ], + "description": "This file is a placeholder to ensure the protocol_autogen module can be built and includes necessary header files.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/protocol_autogen/placeholder.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [], + "test_coverage_notes": "There is no logic, data, or validation in this placeholder file, so there are no code paths to test. Test coverage is not applicable. Any validation logic would exist in the included headers (LedgerEntryBase.h, LedgerEntryBuilderBase.h, TransactionBase.h, TransactionBuilderBase.h), and would be tested in their respective test files, not here.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/protocol_autogen/placeholder.cpp.ai.md b/src/libxrpl/protocol_autogen/placeholder.cpp.ai.md new file mode 100644 index 0000000000..14e23aac4e --- /dev/null +++ b/src/libxrpl/protocol_autogen/placeholder.cpp.ai.md @@ -0,0 +1,14 @@ +# `protocol_autogen/placeholder.cpp` + +This file is a build-system shim for the `protocol_autogen` module inside `libxrpl`. It contains no logic, no functions, and no data — its only purpose is to guarantee that the module has at least one translation unit so the build system can compile it into a linkable object or library. + +The `protocol_autogen` subsystem is an auto-generated layer that wraps the XRPL serialization types (`STTx`, `SLE`) in strongly-typed, read-only C++ classes. The four headers it pulls in are: + +- `LedgerEntryBase.h` — base class for all immutable ledger entry wrappers (in `xrpl::ledger_entries`) +- `LedgerEntryBuilderBase.h` — corresponding builder pattern base for constructing ledger entries +- `TransactionBase.h` — base class for all immutable transaction wrappers (in `xrpl::transactions`) +- `TransactionBuilderBase.h` — builder base for constructing typed transaction objects + +All four are pure header files. In C++, a module that exports only headers has no `.cpp` files to compile, which means `cmake` or any static-library target built from the directory would otherwise produce an empty archive — something many linkers and build systems treat as an error or silently ignore. By providing this placeholder, the `protocol_autogen` CMake target always has a concrete object to compile, keeping the build graph consistent regardless of how many (or how few) `.cpp` implementation files the generated code accumulates over time. + +The include directives also serve a secondary diagnostic function: if any of the four base headers fail to compile (due to a bad code-generation run, a missing dependency, or an API break in `xrpl/protocol`), the failure surfaces here as a build error on a known, stable file rather than deep inside a generated file that may be harder to locate. \ No newline at end of file diff --git a/src/libxrpl/rdb/DatabaseCon.cpp.ai.json b/src/libxrpl/rdb/DatabaseCon.cpp.ai.json new file mode 100644 index 0000000000..df251f0b45 --- /dev/null +++ b/src/libxrpl/rdb/DatabaseCon.cpp.ai.json @@ -0,0 +1,380 @@ +{ + "args": [ + { + "lineno": 18, + "name": "id" + }, + { + "lineno": 27, + "name": "id" + }, + { + "lineno": 32, + "name": "session" + }, + { + "lineno": 32, + "name": "jobQueue" + }, + { + "lineno": 32, + "name": "registry" + }, + { + "lineno": 46, + "name": "id" + }, + { + "lineno": 71, + "name": "q" + }, + { + "lineno": 71, + "name": "registry" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "CheckpointersCollection" + } + ], + "code_paths": [ + { + "call_chain": [ + "DatabaseCon::setupCheckpointing", + "checkpointers.create", + "makeCheckpointer" + ], + "entry_point": "DatabaseCon::setupCheckpointing", + "purpose": "Sets up a checkpointer for the database connection, ensuring a JobQueue is provided and creating a new Checkpointer instance.", + "validation_points": [ + "DatabaseCon::setupCheckpointing: explicit null check on JobQueue* q (if (q == nullptr) Throw(...))" + ] + }, + { + "call_chain": [ + "DatabaseCon::~DatabaseCon", + "checkpointers.erase", + "CheckpointersCollection::erase" + ], + "entry_point": "DatabaseCon::~DatabaseCon", + "purpose": "Cleans up the checkpointer when the DatabaseCon is destroyed, ensuring no lingering locks or references.", + "validation_points": [ + "Implicit: checkpointer_ is checked for null before erase" + ] + }, + { + "call_chain": [ + "checkpointerFromId", + "checkpointers.fromId" + ], + "entry_point": "checkpointerFromId", + "purpose": "Retrieves a Checkpointer by its unique id.", + "validation_points": [ + "CheckpointersCollection::fromId: checks if id exists in map, returns nullptr if not" + ] + } + ], + "data_flows": [ + { + "field": "JobQueue* q", + "flow": [ + "setupCheckpointing argument", + "explicit null check", + "dereferenced and passed to checkpointers.create" + ], + "origin": "Passed as argument to DatabaseCon::setupCheckpointing", + "transformations": [ + "Validated for non-null", + "Dereferenced to JobQueue&" + ], + "validated_at": "DatabaseCon::setupCheckpointing (null check)" + }, + { + "field": "checkpointer_ (DatabaseCon member)", + "flow": [ + "checkpointers.create returns shared_ptr", + "assigned to checkpointer_", + "used in DatabaseCon::~DatabaseCon for cleanup" + ], + "origin": "Set in DatabaseCon::setupCheckpointing via checkpointers.create", + "transformations": [ + "Created via makeCheckpointer", + "Stored in CheckpointersCollection and DatabaseCon" + ], + "validated_at": "Not explicitly validated after creation; assumed valid if setupCheckpointing succeeds" + }, + { + "field": "Checkpointer id", + "flow": [ + "created in CheckpointersCollection::create", + "used as key in checkpointers_ map", + "retrieved via fromId or erased via erase" + ], + "origin": "Generated in CheckpointersCollection::create (auto const id = nextId_++)", + "transformations": [ + "Incremented for each new Checkpointer", + "Used for lookup and deletion" + ], + "validated_at": "CheckpointersCollection::fromId (checks existence in map)" + } + ], + "description": "Implements a collection for managing Checkpointer objects associated with database connections in the XRPL codebase, handling their creation, lookup, and cleanup, especially during DatabaseCon destruction.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "JobQueue* q", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at DatabaseCon::setupCheckpointing", + "issue_pattern": "Missing empty string validation for JobQueue* q", + "why_false_positive": "explicit null check validates JobQueue* q for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/rdb/DatabaseCon.cpp", + "functions": [ + { + "args": [ + "id" + ], + "lineno": 18, + "name": "fromId" + }, + { + "args": [ + "id" + ], + "lineno": 27, + "name": "erase" + }, + { + "args": [ + "session", + "jobQueue", + "registry" + ], + "lineno": 32, + "name": "create" + }, + { + "args": [ + "id" + ], + "lineno": 46, + "name": "checkpointerFromId" + }, + { + "args": [], + "lineno": 51, + "name": "~DatabaseCon" + }, + { + "args": [ + "q", + "registry" + ], + "lineno": 71, + "name": "setupCheckpointing" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::logic_error for error reporting", + "exception_type": "std::logic_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, DatabaseCon and checkpointing logic would be tested in integration or unit tests for database management and recovery. Tests should cover: (1) setupCheckpointing with valid and null JobQueue (expect exception on null), (2) proper creation and destruction of checkpointers, (3) retrieval and erasure of checkpointers by id, (4) concurrency safety of CheckpointersCollection. Gaps: No explicit test hooks or coverage for error paths (e.g., exception thrown on null JobQueue), nor for the waiting logic in the destructor. Test files likely to cover this: tests in the 'rdb', 'database', or 'ledger' modules, possibly named DatabaseCon_test.cpp, Checkpointer_test.cpp, or similar. If these do not exist, error handling and concurrency edge cases may be untested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, uses Throw utility for exceptions)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::logic_error (via Throw)", + "field": "JobQueue* q", + "location": "DatabaseCon::setupCheckpointing", + "validated_by": "explicit null check", + "validates": [ + "q must not be nullptr" + ], + "validation_type": "type (null pointer check)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/rdb/DatabaseCon.cpp.ai.md b/src/libxrpl/rdb/DatabaseCon.cpp.ai.md new file mode 100644 index 0000000000..98545ccf44 --- /dev/null +++ b/src/libxrpl/rdb/DatabaseCon.cpp.ai.md @@ -0,0 +1,53 @@ +# `DatabaseCon.cpp` — SQLite WAL Checkpointer Lifecycle Management + +## Role in the System + +This file provides the runtime glue between `DatabaseCon` (the XRPL wrapper around a SOCI SQLite session) and the WAL (Write-Ahead Log) checkpointing subsystem. Its central problem is a tricky ownership and shutdown puzzle: SQLite's WAL hook is a raw C callback registered on the native connection, yet the checkpoint logic lives in a C++ object that may be executing asynchronously on a `JobQueue` thread at exactly the moment its owning connection is being destroyed. The code here solves that safely without requiring a global lock across the entire checkout/checkpoint cycle. + +## `CheckpointersCollection` — A Stable Bridge for C Callbacks + +The file-private class `CheckpointersCollection` is a mutex-protected registry that maps monotonically-incrementing integer IDs (`std::uintptr_t`) to live `shared_ptr` instances. It exposes three operations: `create`, `fromId`, and `erase`. + +The ID scheme is the key design insight. When a `WALCheckpointer` is created, its numeric ID is cast to a `void*` and registered with SQLite's `sqlite3_wal_hook`. When SQLite fires the hook (on the thread doing a write commit), the hook receives only that `void*`. It calls the free function `checkpointerFromId()`, which delegates to `CheckpointersCollection::fromId()`. If the ID maps to a live checkpointer, `schedule()` is called; if the map entry has already been erased (because the `DatabaseCon` was torn down), `fromId()` returns `nullptr` and the hook removes itself by calling `sqlite3_wal_hook(conn, nullptr, nullptr)`. + +This design avoids the alternative of embedding a raw `this` pointer in the hook, which would be immediately dangerous — there is no guarantee the `WALCheckpointer` object lives long enough for a pending hook invocation. The ID-based lookup through a guarded collection ensures the hook always either finds a valid, reference-counted object or gracefully deregisters itself. + +The global `checkpointers` instance is a plain namespace-scope variable (`CheckpointersCollection checkpointers;`), making it a process-wide singleton. All `DatabaseCon` instances share this one registry. + +## Session Ownership Design + +`DatabaseCon` holds `session_` as a `std::shared_ptr`. The `WALCheckpointer` (in `SociDB.cpp`) holds only a `std::weak_ptr`. This split is intentional and documented in the header: + +> The checkpointer may outlive the `DatabaseCon` when the checkpointer job queue callback locks a weak pointer and the `DatabaseCon` is then destroyed. + +If the checkpointer held a `shared_ptr` to the session, a `jtWAL` job in flight would keep the session alive indefinitely — which is fine for the session object itself, but the calling code that destroys `DatabaseCon` might immediately open a new connection to the same SQLite file, which would fail if the old session still holds the WAL lock. The `weak_ptr` approach means the session is destroyed when `DatabaseCon` is, and `WALCheckpointer::checkpoint()` detects the expired session via `session_.lock()` returning null and exits without touching the connection. + +## Destructor — Bounded Blocking Shutdown + +`DatabaseCon::~DatabaseCon()` contains the most architecturally significant logic: + +```cpp +checkpointers.erase(checkpointer_->id()); +std::weak_ptr const wk(checkpointer_); +checkpointer_.reset(); +while (wk.use_count() != 0) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); +``` + +The sequence matters precisely: + +1. **Erase from the collection** so that any future SQLite WAL hook invocation finds nothing and deregisters itself. +2. **Drop `DatabaseCon`'s own `shared_ptr`** so the only remaining references are inside any in-flight `JobQueue` lambdas. +3. **Poll the `weak_ptr` use count** until it reaches zero, meaning all job queue references have been released and the checkpoint job has finished. + +The 100 ms busy-poll is a deliberate trade-off: a condition variable would require more plumbing inside `WALCheckpointer`, and database teardown is rare enough that the polling cost is negligible. Without this wait, opening a new `DatabaseCon` to the same SQLite file immediately after destroying the old one could fail because the old WAL checkpoint job might still hold a lock on the database file. + +## `setupCheckpointing()` — Deferred Wiring + +`setupCheckpointing(JobQueue*, ServiceRegistry&)` is separated from the constructors so that checkpointing can be conditionally enabled. Constructors that accept a `CheckpointerSetup` struct delegate to the base constructor first (to open and initialize the database), then call `setupCheckpointing`. Passing a null `JobQueue*` is detected immediately and throws `std::logic_error` via the XRPL `Throw<>` utility — a programming error, not a runtime failure. + +The function creates a `WALCheckpointer` via `makeCheckpointer()`, assigns it to `checkpointer_`, and simultaneously stores it in the global `CheckpointersCollection`. Storing in the collection must happen before the checkpointer is returned, because the SQLite WAL hook is armed inside the `WALCheckpointer` constructor — hook invocations can begin arriving immediately. + +## Relationship to `SociDB.cpp` + +The actual checkpoint logic — the `WALCheckpointer` class, the `sqlite3_wal_hook` registration, the `SQLITE_CHECKPOINT_PASSIVE` call — lives entirely in `SociDB.cpp`. `DatabaseCon.cpp` knows nothing about SQLite directly. It only manages the `Checkpointer` abstract interface: creating instances via `makeCheckpointer`, registering them in the collection, and cleaning them up on destruction. This separation keeps the lifecycle management (this file) decoupled from the checkpoint implementation details. \ No newline at end of file diff --git a/src/libxrpl/rdb/SociDB.cpp.ai.json b/src/libxrpl/rdb/SociDB.cpp.ai.json new file mode 100644 index 0000000000..74d66b392f --- /dev/null +++ b/src/libxrpl/rdb/SociDB.cpp.ai.json @@ -0,0 +1,673 @@ +{ + "args": [ + { + "lineno": 17, + "name": "name" + }, + { + "lineno": 17, + "name": "dir" + }, + { + "lineno": 17, + "name": "ext" + }, + { + "lineno": 28, + "name": "config" + }, + { + "lineno": 28, + "name": "dbName" + }, + { + "lineno": 39, + "name": "dbPath" + }, + { + "lineno": 52, + "name": "s" + }, + { + "lineno": 62, + "name": "beName" + }, + { + "lineno": 62, + "name": "connectionString" + }, + { + "lineno": 98, + "name": "from" + }, + { + "lineno": 98, + "name": "to" + }, + { + "lineno": 186, + "name": "id" + }, + { + "lineno": 186, + "name": "session" + }, + { + "lineno": 186, + "name": "queue" + }, + { + "lineno": 186, + "name": "registry" + } + ], + "classes": [ + { + "args": [ + "id", + "session", + "q", + "registry" + ], + "lineno": 132, + "name": "WALCheckpointer" + } + ], + "code_paths": [ + { + "call_chain": [ + "DBConfig(BasicConfig const&, std::string const&)", + "detail::getSociInit", + "detail::getSociSqliteInit" + ], + "entry_point": "DBConfig(BasicConfig const&, std::string const&)", + "purpose": "Constructs a DBConfig object from config and dbName, resolving the backend and database file path.", + "validation_points": [ + "detail::getSociSqliteInit (validates name non-empty)", + "detail::getSociInit (validates backendName == 'sqlite')" + ] + }, + { + "call_chain": [ + "open(soci::session&, BasicConfig const&, std::string const&)", + "DBConfig(BasicConfig const&, std::string const&)", + "DBConfig::open(soci::session&)" + ], + "entry_point": "open(soci::session&, BasicConfig const&, std::string const&)", + "purpose": "Opens a soci session using config and dbName, validating backend and db name.", + "validation_points": [ + "detail::getSociSqliteInit (name validation)", + "detail::getSociInit (backendName validation)" + ] + }, + { + "call_chain": [ + "open(soci::session&, std::string const&, std::string const&)" + ], + "entry_point": "open(soci::session&, std::string const&, std::string const&)", + "purpose": "Opens a soci session with explicit backend and connection string.", + "validation_points": [ + "open (validates beName == 'sqlite')" + ] + }, + { + "call_chain": [ + "getConnection(soci::session&)" + ], + "entry_point": "getConnection(soci::session&)", + "purpose": "Retrieves the raw sqlite3 connection from a soci session.", + "validation_points": [ + "getConnection (dynamic_cast and nullptr check on backend pointer)" + ] + }, + { + "call_chain": [ + "getKBUsedAll(soci::session&)", + "getConnection(soci::session&)" + ], + "entry_point": "getKBUsedAll(soci::session&)", + "purpose": "Returns total SQLite memory usage, validating connection.", + "validation_points": [ + "getConnection (dynamic_cast and nullptr check)" + ] + }, + { + "call_chain": [ + "getKBUsedDB(soci::session&)", + "getConnection(soci::session&)" + ], + "entry_point": "getKBUsedDB(soci::session&)", + "purpose": "Returns per-DB SQLite cache usage, validating connection.", + "validation_points": [ + "getConnection (dynamic_cast and nullptr check)" + ] + } + ], + "data_flows": [ + { + "field": "name (database name)", + "flow": [ + "detail::getSociInit (dbName param)", + "detail::getSociSqliteInit (name param)", + "boost::filesystem::path file(dir)", + "file /= name + ext", + "file.string()" + ], + "origin": "Passed to detail::getSociSqliteInit from detail::getSociInit", + "transformations": [ + "Appended to directory and extension to form file path" + ], + "validated_at": "detail::getSociSqliteInit (if name.empty() Throw)" + }, + { + "field": "backendName (database backend type)", + "flow": [ + "detail::getSociInit", + "get(section, 'backend', 'sqlite')", + "if (backendName != 'sqlite') Throw" + ], + "origin": "Read from config.section('sqdb')", + "transformations": [ + "Checked for supported backend" + ], + "validated_at": "detail::getSociInit" + }, + { + "field": "beName (database backend type, explicit)", + "flow": [ + "open(soci::session&, std::string const&, std::string const&)", + "if (beName == 'sqlite') s.open(soci::sqlite3, connectionString)", + "else Throw" + ], + "origin": "Passed to open(soci::session&, std::string const&, std::string const&)", + "transformations": [ + "Checked for supported backend" + ], + "validated_at": "open(soci::session&, std::string const&, std::string const&)" + }, + { + "field": "s.get_backend() (soci session backend pointer)", + "flow": [ + "getConnection(soci::session&)", + "auto be = s.get_backend()", + "dynamic_cast(be)", + "if (result == nullptr) Throw" + ], + "origin": "soci::session object", + "transformations": [ + "Casted to sqlite3_session_backend, pointer extracted" + ], + "validated_at": "getConnection(soci::session&)" + }, + { + "field": "sqlite3 connection pointer", + "flow": [ + "getConnection(soci::session&)", + "used in getKBUsedAll/getKBUsedDB" + ], + "origin": "getConnection(soci::session&)", + "transformations": [ + "Checked for nullptr" + ], + "validated_at": "getConnection(soci::session&)" + } + ], + "description": "Implements database connection utilities and a write-ahead log (WAL) checkpointer for SQLite databases in the XRPL codebase, including conversion helpers and configuration logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "name (database name)", + "empty", + "string", + "validation" + ], + "evidence": "manual if-check at detail::getSociSqliteInit", + "issue_pattern": "Missing empty string validation for name (database name)", + "why_false_positive": "manual if-check validates name (database name) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "name (database name)", + "format", + "validation", + "invalid" + ], + "evidence": "manual if-check at detail::getSociSqliteInit", + "issue_pattern": "Missing format validation for name (database name)", + "why_false_positive": "manual if-check validates name (database name) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "backendName (database backend type)", + "empty", + "string", + "validation" + ], + "evidence": "manual if-check at detail::getSociInit", + "issue_pattern": "Missing empty string validation for backendName (database backend type)", + "why_false_positive": "manual if-check validates backendName (database backend type) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "beName (database backend type)", + "empty", + "string", + "validation" + ], + "evidence": "manual if-check at open(soci::session&, std::string const&, std::string const&)", + "issue_pattern": "Missing empty string validation for beName (database backend type)", + "why_false_positive": "manual if-check validates beName (database backend type) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "s.get_backend() (soci session backend pointer)", + "empty", + "string", + "validation" + ], + "evidence": "dynamic_cast and nullptr check at getConnection", + "issue_pattern": "Missing empty string validation for s.get_backend() (soci session backend pointer)", + "why_false_positive": "dynamic_cast and nullptr check validates s.get_backend() (soci session backend pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "s.get_backend() (soci session backend pointer)", + "type", + "validation", + "check" + ], + "evidence": "dynamic_cast and nullptr check at getConnection", + "issue_pattern": "Missing type validation for s.get_backend() (soci session backend pointer)", + "why_false_positive": "dynamic_cast and nullptr check validates s.get_backend() (soci session backend pointer) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "getConnection(s) (sqlite3 connection pointer)", + "empty", + "string", + "validation" + ], + "evidence": "nullptr check at getKBUsedAll", + "issue_pattern": "Missing empty string validation for getConnection(s) (sqlite3 connection pointer)", + "why_false_positive": "nullptr check validates getConnection(s) (sqlite3 connection pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "getConnection(s) (sqlite3 connection pointer)", + "type", + "validation", + "check" + ], + "evidence": "nullptr check at getKBUsedAll", + "issue_pattern": "Missing type validation for getConnection(s) (sqlite3 connection pointer)", + "why_false_positive": "nullptr check validates getConnection(s) (sqlite3 connection pointer) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/rdb/SociDB.cpp", + "functions": [ + { + "args": [ + "name", + "dir", + "ext" + ], + "lineno": 17, + "name": "getSociSqliteInit" + }, + { + "args": [ + "config", + "dbName" + ], + "lineno": 28, + "name": "getSociInit" + }, + { + "args": [ + "dbPath" + ], + "lineno": 39, + "name": "DBConfig" + }, + { + "args": [ + "config", + "dbName" + ], + "lineno": 42, + "name": "DBConfig" + }, + { + "args": [], + "lineno": 47, + "name": "connectionString" + }, + { + "args": [ + "s" + ], + "lineno": 52, + "name": "open" + }, + { + "args": [ + "s", + "config", + "dbName" + ], + "lineno": 57, + "name": "open" + }, + { + "args": [ + "s", + "beName", + "connectionString" + ], + "lineno": 62, + "name": "open" + }, + { + "args": [ + "s" + ], + "lineno": 71, + "name": "getConnection" + }, + { + "args": [ + "s" + ], + "lineno": 81, + "name": "getKBUsedAll" + }, + { + "args": [ + "s" + ], + "lineno": 87, + "name": "getKBUsedDB" + }, + { + "args": [ + "from", + "to" + ], + "lineno": 98, + "name": "convert" + }, + { + "args": [ + "from", + "to" + ], + "lineno": 105, + "name": "convert" + }, + { + "args": [ + "from", + "to" + ], + "lineno": 112, + "name": "convert" + }, + { + "args": [ + "from", + "to" + ], + "lineno": 120, + "name": "convert" + }, + { + "args": [ + "id", + "session", + "queue", + "registry" + ], + "lineno": 186, + "name": "makeCheckpointer" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::logic_error for error reporting", + "exception_type": "std::logic_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 15, + "name": "detail" + } + ], + "test_coverage_notes": "This file is low-level and likely tested indirectly via higher-level database and configuration tests. Direct unit tests for validation (e.g., empty name, unsupported backend, invalid session) are not visible here. Tests may exist in files like DatabaseCon_test.cpp, SociDB_test.cpp, or integration tests that exercise database opening and error handling. Gaps: No evidence of explicit tests for all validation branches (e.g., passing empty name, wrong backend, or invalid session). Exception paths may not be fully covered unless negative tests are written.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Manual validation (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "name (database name)", + "location": "detail::getSociSqliteInit", + "validated_by": "manual if-check", + "validates": [ + "Checks that the database name is not empty" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "backendName (database backend type)", + "location": "detail::getSociInit", + "validated_by": "manual if-check", + "validates": [ + "Checks that the backend is 'sqlite' (no other backends supported)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "beName (database backend type)", + "location": "open(soci::session&, std::string const&, std::string const&)", + "validated_by": "manual if-check", + "validates": [ + "Checks that the backend is 'sqlite' (no other backends supported)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::logic_error", + "field": "s.get_backend() (soci session backend pointer)", + "location": "getConnection", + "validated_by": "dynamic_cast and nullptr check", + "validates": [ + "Checks that the soci session backend is a sqlite3_session_backend and not null" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::logic_error", + "field": "getConnection(s) (sqlite3 connection pointer)", + "location": "getKBUsedAll", + "validated_by": "nullptr check", + "validates": [ + "Checks that a valid sqlite3 connection is present before querying memory usage" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/rdb/SociDB.cpp.ai.md b/src/libxrpl/rdb/SociDB.cpp.ai.md new file mode 100644 index 0000000000..e452f8db25 --- /dev/null +++ b/src/libxrpl/rdb/SociDB.cpp.ai.md @@ -0,0 +1,43 @@ +# `src/libxrpl/rdb/SociDB.cpp` + +## Role and Context + +This file implements the XRPL ledger's thin adapter layer between the [SOCI](http://soci.sourceforge.net/) database abstraction library and SQLite, the only supported database backend. It covers three distinct concerns: session lifecycle management, diagnostic memory reporting, blob data conversion, and — most significantly — automated Write-Ahead Log (WAL) checkpointing. Everything here exists to insulate the rest of the codebase from SOCI's C++ quirks and SQLite's internal API surface. + +The file opens with a Clang `#pragma clang diagnostic ignored "-Wdeprecated"` guard because SOCI's own headers use deprecated constructs. Rather than suppress this globally, the guard wraps just this translation unit. + +## Session Configuration and Opening + +`DBConfig` provides a two-phase construction idiom: parse the connection parameters once, open the session later. The non-public `DBConfig(std::string const& dbPath)` constructor is called by the public `DBConfig(BasicConfig const&, std::string const&)` form after the `detail::getSociInit` helper resolves the backend name and file path. + +`detail::getSociInit` reads the `[sqdb]` section of the node config, looking for a `backend` key (defaulting to `"sqlite"`). If anything other than `"sqlite"` is specified, it throws immediately — the code makes no pretense of supporting other backends. It also handles a legacy naming quirk: the `validators` and `peerfinder` databases use the `.sqlite` extension while all other databases use `.db`. This inconsistency is a historical artifact preserved by the explicit branch in `getSociInit`. + +`detail::getSociSqliteInit` constructs the final filesystem path. It throws `std::runtime_error` if the database name is empty, which would otherwise silently produce an unusable path. + +Two free-function `open()` overloads provide an eager alternative to `DBConfig` when the session should be opened at the call site. The config-based overload delegates through `DBConfig`; the explicit-string overload accepts a backend name for forward-compatibility but enforces the same "sqlite only" constraint. Both paths ultimately call `s.open(soci::sqlite3, connectionString)`. + +## Penetrating the SOCI Abstraction + +SOCI's session object hides its underlying database connection behind a polymorphic backend interface. The static `getConnection()` helper uses `s.get_backend()` followed by a `dynamic_cast` to recover the raw `sqlite3*` connection pointer from the `conn_` member. If the cast fails or the pointer is null, it throws `std::logic_error`. This is the only place in the file where the SOCI abstraction is deliberately broken — it is necessary to invoke SQLite-specific APIs that SOCI does not expose (WAL hooks, memory statistics). + +`getKBUsedAll()` calls `sqlite3_memory_used()`, a SQLite process-global metric returned in kilobytes. `getKBUsedDB()` calls `sqlite3_db_status(..., SQLITE_DBSTATUS_CACHE_USED, ...)` to report the page-cache footprint of a specific database connection. Both functions exist to feed the node's performance and diagnostic subsystems. + +## Blob Conversion Helpers + +Four overloaded `convert()` functions bridge SOCI's `blob` type to and from `std::vector` and `std::string`. The SOCI blob API operates through `read`/`write` methods on a character buffer, which is awkward to use directly. These helpers centralise the `reinterpret_cast` between `char*` and `uint8_t*` and handle the empty-input edge case: writing a zero-byte blob requires calling `blob.trim(0)` rather than `blob.write(...)` with a null pointer. + +## WAL Checkpointing + +The most architecturally interesting part of the file is `WALCheckpointer`, an anonymous-namespace class that derives from the public `Checkpointer` interface. + +SQLite in WAL mode accumulates writes in a separate log file. Without periodic checkpointing that log file grows without bound and old readers are blocked from being recycled. SQLite does run its own automatic checkpoint at 1000 pages, but that happens synchronously on whichever thread is writing. `WALCheckpointer` offloads this work to the node's `JobQueue` so database writers are never stalled. + +The checkpointer installs itself via `sqlite3_wal_hook`. SQLite calls this hook — on the writing thread — after every WAL write. The hook receives a `void*` cookie which `WALCheckpointer` sets to its own integer ID (`std::uintptr_t id_`), not a raw pointer. This is deliberate: a raw `this` pointer would be a use-after-free hazard if the `DatabaseCon` owning the session is destroyed while a WAL write is in progress. The ID is instead used to look up the checkpointer in the global `CheckpointersCollection` (defined in `DatabaseCon.cpp`), which is a process-wide thread-safe map from integer ID to `shared_ptr`. If `checkpointerFromId` returns null (because the `DatabaseCon` has been destroyed and its destructor called `checkpointers.erase()`), the hook defensively unregisters itself by calling `sqlite3_wal_hook(conn, nullptr, nullptr)`. + +`schedule()` uses a `running_` boolean under a mutex to ensure at most one checkpoint job is in-flight at a time. If the `JobQueue` itself rejects the job (e.g., during shutdown), the `running_` flag is reset so the next hook invocation can try again. The job captures a `weak_ptr` (not `this`) so that if the `DatabaseCon` is torn down between the job being enqueued and it executing, the lambda safely observes an expired weak pointer and exits without touching the session. + +`checkpoint()` calls `sqlite3_wal_checkpoint_v2` with `SQLITE_CHECKPOINT_PASSIVE`, meaning it checkpoints only WAL frames that are not currently being read. `SQLITE_LOCKED` results are logged at trace level (expected under contention); other errors are logged as warnings. After each checkpoint attempt, `running_` is reset under the mutex. + +The `WALCheckpointer` also holds a `weak_ptr` rather than a raw reference. `DatabaseCon::~DatabaseCon` (in `DatabaseCon.cpp`) erases the checkpointer from the global collection, drops its own `shared_ptr` to the checkpointer, then spins waiting for the checkpointer's use count to reach zero before returning. This ensures the session is not destroyed while a checkpoint job is mid-execution on a worker thread. + +The `static checkpointPageCount = 1000` module-level constant mirrors SQLite's default auto-checkpoint threshold, making the trigger condition explicit and easy to tune. The inline comment in the `WALCheckpointer` class acknowledges that SQLite already does this automatically, leaving open the question of whether the explicit hook buys anything — the answer is primarily that it routes the work onto the XRPL job queue rather than the caller's thread. \ No newline at end of file diff --git a/src/libxrpl/resource/Charge.cpp.ai.json b/src/libxrpl/resource/Charge.cpp.ai.json new file mode 100644 index 0000000000..36a38e0d88 --- /dev/null +++ b/src/libxrpl/resource/Charge.cpp.ai.json @@ -0,0 +1,205 @@ +{ + "args": [ + { + "lineno": 9, + "name": "cost" + }, + { + "lineno": 9, + "name": "label" + }, + { + "lineno": 31, + "name": "os" + }, + { + "lineno": 31, + "name": "v" + }, + { + "lineno": 38, + "name": "c" + }, + { + "lineno": 43, + "name": "c" + }, + { + "lineno": 48, + "name": "m" + } + ], + "classes": [ + { + "args": [ + "cost", + "label" + ], + "lineno": 9, + "name": "Charge" + } + ], + "code_paths": [ + { + "call_chain": [ + "Charge::Charge" + ], + "entry_point": "Charge::Charge(value_type cost, std::string const& label)", + "purpose": "Constructs a Charge object with a cost and label.", + "validation_points": [] + }, + { + "call_chain": [ + "Charge::operator==" + ], + "entry_point": "Charge::operator==", + "purpose": "Compares two Charge objects for equality based on cost.", + "validation_points": [] + }, + { + "call_chain": [ + "Charge::operator<=>" + ], + "entry_point": "Charge::operator<=>", + "purpose": "Compares two Charge objects for ordering based on cost.", + "validation_points": [] + }, + { + "call_chain": [ + "Charge::operator*", + "Charge::Charge" + ], + "entry_point": "Charge::operator*", + "purpose": "Multiplies the cost of a Charge by a value and returns a new Charge.", + "validation_points": [] + }, + { + "call_chain": [ + "operator<<", + "Charge::to_string" + ], + "entry_point": "operator<<", + "purpose": "Serializes a Charge object to an output stream.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "m_cost", + "flow": [ + "constructor parameter", + "m_cost", + "cost()", + "operator==/operator<=>/operator*/to_string" + ], + "origin": "Charge::Charge constructor parameter 'cost'", + "transformations": [ + "Assigned directly in constructor", + "Returned as-is in cost()", + "Compared in operator== and operator<=>", + "Multiplied in operator*", + "Formatted as string in to_string" + ], + "validated_at": "No validation occurs" + }, + { + "field": "m_label", + "flow": [ + "constructor parameter", + "m_label", + "label()", + "to_string" + ], + "origin": "Charge::Charge constructor parameter 'label'", + "transformations": [ + "Assigned directly in constructor", + "Returned as-is in label()", + "Formatted as string in to_string" + ], + "validated_at": "No validation occurs" + } + ], + "description": "Implements the Charge class in the xrpl::Resource namespace, representing a resource cost with a label and providing comparison, multiplication, and string conversion operations.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/resource/Charge.cpp", + "functions": [ + { + "args": [ + "cost", + "label" + ], + "lineno": 9, + "name": "Charge" + }, + { + "args": [], + "lineno": 14, + "name": "label" + }, + { + "args": [], + "lineno": 19, + "name": "cost" + }, + { + "args": [], + "lineno": 24, + "name": "to_string" + }, + { + "args": [ + "os", + "v" + ], + "lineno": 31, + "name": "operator<<" + }, + { + "args": [ + "c" + ], + "lineno": 38, + "name": "operator==" + }, + { + "args": [ + "c" + ], + "lineno": 43, + "name": "operator<=>" + }, + { + "args": [ + "m" + ], + "lineno": 48, + "name": "operator*" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "Resource" + } + ], + "test_coverage_notes": "There is no validation logic in this code (e.g., no range checks, no input sanitization). The code assumes that the inputs to the Charge constructor are valid. Test coverage would likely exist in unit tests for Charge (e.g., Charge_test.cpp or Resource_test.cpp), but these would only test construction, comparison, multiplication, and string formatting, not validation. There are no explicit validation code paths to test. Gaps: No tests for invalid or edge-case inputs (e.g., negative costs, empty labels), but such validation is not present in the code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "notes": "No explicit or implicit input/field validation is present in this code. All fields are assigned directly without checks. No framework or type/range/format/business logic validation is performed in constructors, setters, or methods.", + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/resource/Charge.cpp.ai.md b/src/libxrpl/resource/Charge.cpp.ai.md new file mode 100644 index 0000000000..ce7570a0bd --- /dev/null +++ b/src/libxrpl/resource/Charge.cpp.ai.md @@ -0,0 +1,45 @@ +# `Charge.cpp` — Resource Cost Value Type + +## Role in the System + +`Charge` is the fundamental unit of account in XRPL's resource management subsystem (`xrpl::Resource`). The resource manager exists to throttle and disconnect peers or RPC clients that impose excessive computational load on the server. Every costly operation — verifying an invalid signature, processing a heavy RPC call, receiving a malformed peer message — is tagged with a `Charge` that quantifies its cost in abstract internal units. Those charges accumulate on a per-connection `Entry` and are evaluated by `Logic` to decide whether to warn or disconnect the offending consumer. + +`Charge.cpp` provides the implementation for this value type. It is intentionally minimal: just construction, accessors, comparison, scaling, and string conversion. All policy (thresholds, decay rates, disposition decisions) lives elsewhere in `Logic`. + +## Design of the Value Type + +`Charge` pairs two pieces of data: an integer `m_cost` (aliased as `value_type = int`) and a human-readable `m_label`. The label is purely diagnostic — it appears in log output and the `to_string()` representation (`"heavy RPC ($3000)"`) to make it easy to reason about what triggered a resource charge in a log trace. + +The default constructor is explicitly deleted. This forces every charge to carry a meaningful cost at creation time, preventing zero-cost sentinel objects from silently floating through the codebase and understating load. + +## Comparison Semantics + +Both `operator==` and `operator<=>` compare charges solely by `m_cost`, ignoring the label entirely. This is a deliberate choice: the label is metadata for humans, not part of the charge's semantic identity. Two `Charge` objects with the same numeric cost are considered equal even if they carry different labels. The spaceship operator (`<=>`, C++20) returns `std::strong_ordering`, enabling the full set of relational operators via the standard rewrite rules — useful if charges are ever sorted or placed in ordered containers. + +## Scaling Operator + +`operator*(value_type m)` returns a new `Charge` with the cost multiplied by `m`, preserving the original label. This enables sites that impose burdens proportional to some runtime quantity (e.g., the number of items in a batch request) to express that as `feeReferenceRPC * batchSize` without constructing an entirely new named charge. The label inheritance is the right call here: diagnostic output still names the operation type even when the cost has been scaled. + +## The Fee Schedule + +`Charge.cpp` is tightly paired with `Fees.cpp`, which defines the catalog of named constants used throughout the server: + +| Constant | Cost | Description | +|---|---|---| +| `feeTrivialPeer` | 1 | Peer message requiring no reply | +| `feeReferenceRPC` | 20 | Baseline RPC load | +| `feeMalformedRPC` / `feeMalformedRequest` | 100–200 | Immediately detectable invalid inputs | +| `feeUselessData` | 150 | Data the node has no use for | +| `feeModerateBurdenPeer` | 250 | Peer work requiring some effort | +| `feeInvalidData` | 400 | Data requiring verification before rejection | +| `feeMediumBurdenRPC` | 400 | Moderately expensive RPC | +| `feeHeavyBurdenPeer` / `feeInvalidSignature` | 2000 | Expensive peer work or failed signature check | +| `feeHeavyBurdenRPC` | 3000 | Very expensive RPC operation | +| `feeWarning` | 4000 | Cost of receiving a warning from a peer | +| `feeDrop` | 6000 | Cost assessed when a connection is dropped | + +The range from 1 to 6000 gives the `Logic` layer enough granularity to distinguish between nuisance noise and deliberate abuse. The comment in `Fees.cpp` notes that `Logic::charge` uses log-level cutoffs keyed to these same values, so the numeric scale is semantically meaningful, not arbitrary. + +## Usage at the Call Site + +`Consumer::charge(Charge const& what, ...)` is the primary consumption path. It delegates to `Logic::charge()` with the `Entry` for the connection and the `Charge` value. `Consumer::disposition()` uses a zero-cost `Charge(0)` probe — checking the entry's accumulated balance without adding to it — to query current standing without imposing load. Both paths make `Charge`'s lightweight value semantics (copy-cheap `int` plus `std::string`) appropriate; there is no need for pointer or reference sharing. \ No newline at end of file diff --git a/src/libxrpl/resource/Consumer.cpp.ai.json b/src/libxrpl/resource/Consumer.cpp.ai.json new file mode 100644 index 0000000000..77a6bc0c64 --- /dev/null +++ b/src/libxrpl/resource/Consumer.cpp.ai.json @@ -0,0 +1,479 @@ +{ + "args": [ + { + "lineno": 13, + "name": "logic" + }, + { + "lineno": 13, + "name": "entry" + }, + { + "lineno": 21, + "name": "other" + }, + { + "lineno": 74, + "name": "what" + }, + { + "lineno": 74, + "name": "context" + }, + { + "lineno": 88, + "name": "j" + }, + { + "lineno": 107, + "name": "publicKey" + }, + { + "lineno": 111, + "name": "os" + }, + { + "lineno": 111, + "name": "v" + } + ], + "classes": [ + { + "args": [ + "Logic& logic, Entry& entry", + "", + "Consumer const& other" + ], + "lineno": 12, + "name": "Consumer" + } + ], + "code_paths": [ + { + "call_chain": [ + "Consumer::warn", + "XRPL_ASSERT(m_entry)", + "m_logic->warn(*m_entry)" + ], + "entry_point": "Consumer::warn", + "purpose": "Warns the logic about the consumer; asserts m_entry is valid.", + "validation_points": [ + "XRPL_ASSERT(m_entry)" + ] + }, + { + "call_chain": [ + "Consumer::disconnect", + "XRPL_ASSERT(m_entry)", + "m_logic->disconnect(*m_entry)", + "JLOG(j.debug()) << m_entry->to_string()" + ], + "entry_point": "Consumer::disconnect", + "purpose": "Disconnects the consumer; asserts m_entry is valid.", + "validation_points": [ + "XRPL_ASSERT(m_entry)" + ] + }, + { + "call_chain": [ + "Consumer::balance", + "XRPL_ASSERT(m_entry)", + "m_logic->balance(*m_entry)" + ], + "entry_point": "Consumer::balance", + "purpose": "Returns the consumer's balance; asserts m_entry is valid.", + "validation_points": [ + "XRPL_ASSERT(m_entry)" + ] + }, + { + "call_chain": [ + "Consumer::entry", + "XRPL_ASSERT(m_entry)", + "return *m_entry" + ], + "entry_point": "Consumer::entry", + "purpose": "Returns a reference to the entry; asserts m_entry is valid.", + "validation_points": [ + "XRPL_ASSERT(m_entry)" + ] + }, + { + "call_chain": [ + "Consumer::charge", + "if ((m_logic != nullptr) && (m_entry != nullptr) && !m_entry->isUnlimited())", + "m_logic->charge(*m_entry, what, context)" + ], + "entry_point": "Consumer::charge", + "purpose": "Charges the consumer; checks for null and unlimited before charging.", + "validation_points": [ + "explicit nullptr checks", + "m_entry->isUnlimited()" + ] + }, + { + "call_chain": [ + "Consumer::disposition", + "if ((m_logic != nullptr) && (m_entry != nullptr))", + "m_logic->charge(*m_entry, Charge(0))" + ], + "entry_point": "Consumer::disposition", + "purpose": "Gets the disposition of the consumer; checks for null before charging.", + "validation_points": [ + "explicit nullptr checks" + ] + }, + { + "call_chain": [ + "Consumer::isUnlimited", + "if (m_entry != nullptr)", + "m_entry->isUnlimited()" + ], + "entry_point": "Consumer::isUnlimited", + "purpose": "Checks if the consumer is unlimited.", + "validation_points": [ + "explicit nullptr check" + ] + }, + { + "call_chain": [ + "Consumer::to_string", + "if (m_logic == nullptr)", + "return m_entry->to_string()" + ], + "entry_point": "Consumer::to_string", + "purpose": "Returns string representation of the consumer.", + "validation_points": [ + "explicit nullptr check" + ] + } + ], + "data_flows": [ + { + "field": "m_entry", + "flow": [ + "Consumer::Consumer(Logic&, Entry&)", + "assigned to m_entry", + "used in all member functions (warn, disconnect, balance, entry, charge, etc.)" + ], + "origin": "Constructor parameter (Entry& entry) or copy from other Consumer", + "transformations": [ + "Acquired/released via m_logic->acquire/release in copy/move/assignment/destructor", + "Validated via XRPL_ASSERT or nullptr checks before use" + ], + "validated_at": "XRPL_ASSERT in warn, disconnect, balance, entry; nullptr checks in charge, disposition, isUnlimited, to_string" + }, + { + "field": "m_logic", + "flow": [ + "Consumer::Consumer(Logic&, Entry&)", + "assigned to m_logic", + "used in all member functions to operate on m_entry" + ], + "origin": "Constructor parameter (Logic& logic) or copy from other Consumer", + "transformations": [ + "Acquired/released via m_logic->acquire/release in copy/move/assignment/destructor", + "Validated via nullptr checks before use" + ], + "validated_at": "nullptr checks in all functions before dereferencing" + }, + { + "field": "publicKey (in m_entry)", + "flow": [ + "Consumer::setPublicKey", + "m_entry->publicKey = publicKey" + ], + "origin": "setPublicKey(PublicKey const& publicKey)", + "transformations": [ + "Direct assignment" + ], + "validated_at": "No explicit validation in setPublicKey" + } + ], + "description": "Implements the xrpl::Resource::Consumer class, which manages resource consumption tracking and control for clients, including reference counting, charging, warning, disconnecting, and balance management.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_entry", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at warn()", + "issue_pattern": "Missing empty string validation for m_entry", + "why_false_positive": "XRPL_ASSERT macro validates m_entry for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_entry", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at disconnect()", + "issue_pattern": "Missing empty string validation for m_entry", + "why_false_positive": "XRPL_ASSERT macro validates m_entry for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_entry", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at balance()", + "issue_pattern": "Missing empty string validation for m_entry", + "why_false_positive": "XRPL_ASSERT macro validates m_entry for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_entry", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at entry()", + "issue_pattern": "Missing empty string validation for m_entry", + "why_false_positive": "XRPL_ASSERT macro validates m_entry for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_logic, m_entry", + "empty", + "string", + "validation" + ], + "evidence": "explicit nullptr checks at Consumer::Consumer(Consumer const& other), Consumer::~Consumer(), Consumer::operator=, Consumer::to_string(), Consumer::isUnlimited(), Consumer::disposition(), Consumer::charge()", + "issue_pattern": "Missing empty string validation for m_logic, m_entry", + "why_false_positive": "explicit nullptr checks validates m_logic, m_entry for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_entry->isUnlimited()", + "empty", + "string", + "validation" + ], + "evidence": "explicit check at charge()", + "issue_pattern": "Missing empty string validation for m_entry->isUnlimited()", + "why_false_positive": "explicit check validates m_entry->isUnlimited() for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/resource/Consumer.cpp", + "functions": [ + { + "args": [ + "Logic& logic", + "Entry& entry" + ], + "lineno": 13, + "name": "Consumer" + }, + { + "args": [], + "lineno": 17, + "name": "Consumer" + }, + { + "args": [ + "Consumer const& other" + ], + "lineno": 21, + "name": "Consumer" + }, + { + "args": [], + "lineno": 30, + "name": "~Consumer" + }, + { + "args": [ + "Consumer const& other" + ], + "lineno": 36, + "name": "operator=" + }, + { + "args": [], + "lineno": 52, + "name": "to_string" + }, + { + "args": [], + "lineno": 59, + "name": "isUnlimited" + }, + { + "args": [], + "lineno": 66, + "name": "disposition" + }, + { + "args": [ + "Charge const& what", + "std::string const& context" + ], + "lineno": 74, + "name": "charge" + }, + { + "args": [], + "lineno": 83, + "name": "warn" + }, + { + "args": [ + "beast::Journal const& j" + ], + "lineno": 88, + "name": "disconnect" + }, + { + "args": [], + "lineno": 97, + "name": "balance" + }, + { + "args": [], + "lineno": 102, + "name": "entry" + }, + { + "args": [ + "PublicKey const& publicKey" + ], + "lineno": 107, + "name": "setPublicKey" + }, + { + "args": [ + "std::ostream& os", + "Consumer const& v" + ], + "lineno": 111, + "name": "operator<<" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "Resource" + } + ], + "test_coverage_notes": "The validation code paths (XRPL_ASSERT and nullptr checks) are critical for safety. Typical test files would be in the test/resource or test/unit/resource directories, possibly named Consumer_test.cpp or Resource_test.cpp. Tests should cover: construction, copy, assignment, destruction, all public methods (warn, disconnect, balance, entry, charge, disposition, isUnlimited, to_string, setPublicKey), and especially error cases (null m_entry, null m_logic). Gaps may exist if tests do not explicitly check assertion failures (XRPL_ASSERT), or if edge cases (e.g., double release, null logic/entry) are not tested. No test code is shown here, so actual coverage must be verified in the test suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro (custom assertion), explicit nullptr checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "m_entry", + "location": "warn()", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "m_entry is not nullptr" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "m_entry", + "location": "disconnect()", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "m_entry is not nullptr" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "m_entry", + "location": "balance()", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "m_entry is not nullptr" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "m_entry", + "location": "entry()", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "m_entry is not nullptr" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (skips logic if nullptr)", + "field": "m_logic, m_entry", + "location": "Consumer::Consumer(Consumer const& other), Consumer::~Consumer(), Consumer::operator=, Consumer::to_string(), Consumer::isUnlimited(), Consumer::disposition(), Consumer::charge()", + "validated_by": "explicit nullptr checks", + "validates": [ + "m_logic and/or m_entry are not nullptr before use" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (skips charge if unlimited)", + "field": "m_entry->isUnlimited()", + "location": "charge()", + "validated_by": "explicit check", + "validates": [ + "charge is only applied if not unlimited" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/resource/Consumer.cpp.ai.md b/src/libxrpl/resource/Consumer.cpp.ai.md new file mode 100644 index 0000000000..d97ae3c4a8 --- /dev/null +++ b/src/libxrpl/resource/Consumer.cpp.ai.md @@ -0,0 +1,37 @@ +# `Consumer.cpp` — Resource Consumption Handle + +`Consumer.cpp` implements the `xrpl::Resource::Consumer` class, the public-facing handle through which every network endpoint (inbound peer, outbound peer, or administrative connection) interacts with the XRPL resource management system. Its job is simple to describe but nuanced to get right: it is a reference-counted, copyable proxy to a shared `Entry` in the central `Logic` table, exposing an API for charging load costs, querying disposition, and triggering warnings or disconnections. + +## Role in the Resource Subsystem + +The resource subsystem protects the rippled node from abuse by tracking per-endpoint resource consumption over time using an exponentially decaying balance (`DecayingSample`). When consumption crosses a warning threshold, the server notifies the peer; when it crosses the drop threshold, the server disconnects it. `Consumer` is the object that sits on the network layer side of this boundary — every connection object holds one, and every RPC handler that applies a fee calls `Consumer::charge()`. + +The real work lives in `Logic`, a class that owns the authoritative `hash_map` table and enforces all thresholds. `Consumer` is deliberately thin: it delegates every meaningful operation to `Logic` and exists primarily to manage `Entry` lifetime through reference counting. + +## Reference Counting and Lifetime + +The most architecturally significant aspect of `Consumer` is its copy semantics. An `Entry` record in `Logic::table_` tracks a `refcount` representing how many `Consumer` objects are actively pointing to it. When the copy constructor or `operator=` fires, it calls `m_logic->acquire(*m_entry)`, incrementing the count under `Logic`'s `recursive_mutex`. When a `Consumer` is destroyed or reassigned away, it calls `m_logic->release(*m_entry)`, which decrements the count. If the count hits zero, `Logic::release()` moves the `Entry` from the appropriate active list (`inbound_`, `outbound_`, or `admin_`) to the `inactive_` list, sets an expiry timestamp, and leaves the actual hash map erasure for `Logic::periodicActivity()` to handle. + +This deferred erasure is a deliberate design choice: it means the node retains consumption history for recently-disconnected peers, preventing them from immediately reconnecting with a clean slate after a drop. The raw pointer `m_entry` is therefore always stable and non-dangling for the lifetime of any `Consumer` that holds a reference, because the entry is only erased from the map when no `Consumer` references it and enough time has elapsed. + +The private constructor `Consumer(Logic& logic, Entry& entry)` is only accessible to `Logic` (declared as `friend`), ensuring that `Consumer` objects can only originate from a valid, centrally-managed entry. Default-constructed `Consumer` objects (with both `m_logic` and `m_entry` as `nullptr`) act as an explicit null/empty state for cases where a consumer is not yet assigned. + +## Two Tiers of Null Safety + +The member functions deliberately split into two safety tiers: + +**Soft guards** (nullptr checks that return safe defaults): `to_string()`, `isUnlimited()`, `disposition()`, and `charge()` all perform explicit `nullptr` checks. A default-constructed or moved-from `Consumer` can survive calls to these functions without crashing. `charge()` adds a third guard: `!m_entry->isUnlimited()`. Unlimited (administrative) endpoints bypass the charge path entirely, making the cost of checking nearly zero for trusted connections. + +**Hard guards** (assertions): `warn()`, `disconnect()`, `balance()`, and `entry()` use `XRPL_ASSERT` to assert that `m_entry` is non-null. These methods are only called by connection-handling code that has already validated that the `Consumer` is bound to a real endpoint, so a null here represents a programming error rather than a runtime condition. + +## Querying Disposition Without Side Effects + +`disposition()` is subtly clever: it calls `m_logic->charge(*m_entry, Charge(0))` — a charge of zero cost. `Logic::charge()` adds the fee cost to the entry's `DecayingSample` and then evaluates the resulting balance against the warning and drop thresholds to return a `Disposition`. Passing zero doesn't alter the balance, so `disposition()` gets a fresh threshold evaluation with no side effects. This is cheaper and more correct than duplicating the threshold logic in `Consumer`, and it ensures that the same clock-relative balance calculation used by real charges is used for disposition queries. + +## `setPublicKey` and Identity + +`setPublicKey()` bypasses `Logic` entirely and writes directly to `m_entry->publicKey`. This is safe because `publicKey` is purely identification metadata used in `Entry::to_string()` (via `getFingerprint`). It is never consulted in resource calculations or threshold comparisons. The design reflects a pragmatic layering: endpoints don't always know their peer's public key at the moment a `Consumer` is created (it's learned during the handshake), so the key is patched in afterward. + +## Relationship to Logic and Entry + +`Logic` is the true owner and authority. `Consumer` is a vocabulary type that makes the resource API ergonomic for callers — they get a copyable value type that can be stored in connection state, passed around, and destroyed without manual reference management. The relationship mirrors `std::shared_ptr` semantics without using atomic operations: all reference count mutations go through `Logic`'s `recursive_mutex`, which also guards the table, the intrusive lists, and the balance calculations, making the whole system coherently thread-safe under concurrent connection activity. \ No newline at end of file diff --git a/src/libxrpl/resource/Fees.cpp.ai.json b/src/libxrpl/resource/Fees.cpp.ai.json new file mode 100644 index 0000000000..50d1fc1a1d --- /dev/null +++ b/src/libxrpl/resource/Fees.cpp.ai.json @@ -0,0 +1,94 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Resource::Logic::charge", + "Resource::Fees::", + "Resource::Charge" + ], + "entry_point": "Resource::Logic::charge", + "purpose": "Applies a resource charge (fee) to a client or peer based on the type of request or behavior.", + "validation_points": [ + "Resource::Logic::charge (validates request type, context, and applies appropriate fee)" + ] + }, + { + "call_chain": [ + "RPC/Peer handler", + "Resource::Logic::charge", + "Resource::Fees::", + "Resource::Charge" + ], + "entry_point": "RPC/Peer request handling (e.g., NetworkOps, RPC handlers)", + "purpose": "When a request is received, the system determines if it is malformed, invalid, or burdensome, and applies the corresponding fee.", + "validation_points": [ + "RPC/Peer handler (validates request structure/signature)", + "Resource::Logic::charge (applies fee based on validation result)" + ] + } + ], + "data_flows": [ + { + "field": "request", + "flow": [ + "External input", + "RPC/Peer handler", + "Resource::Logic::charge", + "Resource::Fees::", + "Resource::Charge" + ], + "origin": "External client/peer input", + "transformations": [ + "Parsed and validated for structure and content", + "Classified as malformed, invalid, heavy, etc.", + "Mapped to a specific fee constant" + ], + "validated_at": "RPC/Peer handler and Resource::Logic::charge" + }, + { + "field": "feeType (e.g., feeMalformedRequest, feeInvalidSignature)", + "flow": [ + "Resource::Fees::", + "Resource::Logic::charge", + "Resource::Consumer (or similar accounting object)" + ], + "origin": "Resource::Fees.cpp (constant definitions)", + "transformations": [ + "Selected based on request validation/classification", + "Passed to charge logic for accounting" + ], + "validated_at": "Resource::Logic::charge (ensures correct fee for context)" + } + ], + "description": "Defines a set of constant Charge objects representing various resource usage fees for different types of requests and actions in the XRPL resource management system.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/resource/Fees.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "Resource" + } + ], + "test_coverage_notes": "This file only defines fee constants and does not contain logic or validation itself. Validation and fee application logic reside in Resource::Logic::charge and related request handling code. Tests likely exist in files covering Resource::Logic, RPC handlers, and peer request handling (e.g., test cases for malformed requests, invalid signatures, heavy requests). Direct unit tests for Fees.cpp are unlikely, but indirect coverage occurs via integration and functional tests. Gaps may exist if new fee types are added but not exercised in tests, or if edge cases (e.g., unknown request types) are not covered.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/resource/Fees.cpp.ai.md b/src/libxrpl/resource/Fees.cpp.ai.md new file mode 100644 index 0000000000..cc1d3c4740 --- /dev/null +++ b/src/libxrpl/resource/Fees.cpp.ai.md @@ -0,0 +1,46 @@ +# `src/libxrpl/resource/Fees.cpp` + +## Role in the System + +This file is the authoritative definition of every quantified resource charge in the XRPL node's rate-limiting and abuse-prevention system. It serves as a single, auditable table of numeric weights that the rest of the codebase uses when deciding how much to penalize a connected client or peer for particular categories of bad, expensive, or abusive behavior. Nothing about anti-abuse policy is scattered across individual call sites — it is concentrated here. + +## The `Charge` Type + +Each entry is a `const Charge` object, which pairs an integer `value_type` cost with a human-readable label string. `Charge` objects are immutable after construction — there is no mutation API — and comparisons are made purely on numeric cost. The label exists solely for logging and diagnostics. The `operator*` on `Charge` allows callers to scale a base fee (e.g., for repeated offenses) without defining separate constants. + +## The Fee Schedule + +The constants fall into four conceptual tiers reflecting the origin and severity of the request: + +**General request problems** cover behavior detectable before heavy processing: +- `feeMalformedRequest` (200) — structurally invalid input the server can immediately reject. +- `feeRequestNoReply` (10) — a valid but unsatisfiable request (low penalty since this may be a benign race condition). +- `feeInvalidSignature` (2000) — cryptographic signature verification that failed; expensive to check, clearly abusive at scale. +- `feeUselessData` (150) — data received but not applicable. +- `feeInvalidData` (400) — data that required verification work before rejection. + +**RPC loads** reflect cost to the server's JSON-RPC interface: +- `feeReferenceRPC` (20) is the baseline for a cheap, well-formed call. +- `feeMalformedRPC` (100) and `feeExceptionRPC` (100) penalize ill-formed or crashing calls. +- `feeMediumBurdenRPC` (400) and `feeHeavyBurdenRPC` (3000) penalize queries that drive significant computation — ledger traversals, path-finding, etc. + +**Peer loads** cover activity on the overlay network between validators and relay nodes: +- `feeTrivialPeer` (1) for no-reply messages. +- `feeModerateBurdenPeer` (250) for messages requiring some local work. +- `feeHeavyBurdenPeer` (2000) for computationally intensive peer messages. + +**Administrative / enforcement** constants mark the system's own enforcement actions: +- `feeWarning` (4000) is charged when the system sends a warning to a consumer already near the limit — the act of warning is itself priced to keep the accumulator rising. +- `feeDrop` (6000) is charged at the moment of disconnection, which deliberately pushes the balance high enough that a reconnecting peer is still penalized for a time after it reconnects. + +## Integration with `Logic::charge` + +These constants flow into `Resource::Logic::charge()`, the only place where a fee is actually applied to a tracked endpoint. That method uses three internal log-level cutoffs (`feeLogAsDebug = 100`, `feeLogAsInfo = 1000`, `feeLogAsWarn = 3000`) to decide how loudly to log each charge — a design explicitly tied to the numeric scale established here. The comment in `Fees.cpp` ("See also Resource::Logic::charge for log level cutoff values") documents this intentional coupling: anyone who adjusts a fee value needs to consider whether it crosses a logging threshold. + +After applying the charge, `Logic::disposition()` compares the running balance against two thresholds: `warningThreshold` triggers a `Disposition::warn` and `dropThreshold` triggers `Disposition::drop`. The numeric spacing between fee values and those thresholds determines how many of each request type a client can issue before being warned or dropped. `feeInvalidSignature` (2000) alone can push a client toward the warning level in a single call, while `feeRequestNoReply` (10) would require hundreds. This calibration is the core policy decision embodied by this file. + +## Design Rationale + +Defining all charges as `const` globals in a single translation unit rather than as local magic numbers at each call site achieves two things. First, it makes the policy visible and comparable in one place — a reviewer can immediately see that a dropped connection (6000) costs three times a heavy RPC (3000) and thirty times a reference RPC (20). Second, it prevents inconsistent re-definitions: any code that needs to charge for an invalid signature imports the same `feeInvalidSignature` and cannot accidentally use a different value. + +The absence of an `enum` or strongly-typed tag for fee categories is deliberate: `Charge` is an open value type, not a closed enumeration, so callers can scale (`fee * n`), log, and compare charges uniformly without needing to enumerate every case in a switch. \ No newline at end of file diff --git a/src/libxrpl/resource/ResourceManager.cpp.ai.json b/src/libxrpl/resource/ResourceManager.cpp.ai.json new file mode 100644 index 0000000000..07e5293763 --- /dev/null +++ b/src/libxrpl/resource/ResourceManager.cpp.ai.json @@ -0,0 +1,419 @@ +{ + "args": [ + { + "lineno": 22, + "name": "collector" + }, + { + "lineno": 22, + "name": "journal" + }, + { + "lineno": 39, + "name": "address" + }, + { + "lineno": 44, + "name": "address" + }, + { + "lineno": 44, + "name": "proxy" + }, + { + "lineno": 44, + "name": "forwardedFor" + }, + { + "lineno": 61, + "name": "address" + }, + { + "lineno": 66, + "name": "address" + }, + { + "lineno": 76, + "name": "origin" + }, + { + "lineno": 76, + "name": "gossip" + }, + { + "lineno": 89, + "name": "threshold" + }, + { + "lineno": 96, + "name": "map" + } + ], + "classes": [ + { + "args": [ + "beast::insight::Collector::ptr const& collector", + "beast::Journal journal" + ], + "lineno": 14, + "name": "ManagerImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "ManagerImp::newInboundEndpoint(address, proxy, forwardedFor)", + "boost::asio::ip::make_address(forwardedFor, ec)", + "ManagerImp::newInboundEndpoint(address) OR ManagerImp::newInboundEndpoint(beast::IPAddressConversion::from_asio(proxiedIp))", + "logic_.newInboundEndpoint(address)" + ], + "entry_point": "ManagerImp::newInboundEndpoint(beast::IP::Endpoint const&, bool const, std::string_view)", + "purpose": "Handles creation of a new inbound resource Consumer, optionally using a proxied IP address (from X-Forwarded-For header).", + "validation_points": [ + "boost::asio::ip::make_address(forwardedFor, ec) // Validates forwardedFor as a valid IP address" + ] + }, + { + "call_chain": [ + "ManagerImp::newInboundEndpoint(address)", + "logic_.newInboundEndpoint(address)" + ], + "entry_point": "ManagerImp::newInboundEndpoint(beast::IP::Endpoint const&)", + "purpose": "Creates a new inbound Consumer for a direct (non-proxied) endpoint.", + "validation_points": [] + }, + { + "call_chain": [ + "ManagerImp::newOutboundEndpoint(address)", + "logic_.newOutboundEndpoint(address)" + ], + "entry_point": "ManagerImp::newOutboundEndpoint(beast::IP::Endpoint const&)", + "purpose": "Creates a new outbound Consumer.", + "validation_points": [] + }, + { + "call_chain": [ + "ManagerImp::newUnlimitedEndpoint(address)", + "logic_.newUnlimitedEndpoint(address)" + ], + "entry_point": "ManagerImp::newUnlimitedEndpoint(beast::IP::Endpoint const&)", + "purpose": "Creates a new unlimited Consumer.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "forwardedFor", + "flow": [ + "forwardedFor (input)", + "boost::asio::ip::make_address(forwardedFor, ec)", + "if valid: beast::IPAddressConversion::from_asio(proxiedIp)", + "ManagerImp::newInboundEndpoint(beast::IP::Endpoint)", + "logic_.newInboundEndpoint(address)" + ], + "origin": "Input parameter to ManagerImp::newInboundEndpoint (likely from HTTP X-Forwarded-For header)", + "transformations": [ + "String (forwardedFor) parsed to boost::asio::ip::address (proxiedIp)", + "Converted to beast::IP::Endpoint" + ], + "validated_at": "boost::asio::ip::make_address(forwardedFor, ec)" + }, + { + "field": "address", + "flow": [ + "address (input)", + "logic_.newInboundEndpoint/address", + "Consumer object" + ], + "origin": "Input parameter to ManagerImp::newInboundEndpoint, newOutboundEndpoint, newUnlimitedEndpoint", + "transformations": [ + "May be replaced by proxied IP if proxy=true and forwardedFor is valid" + ], + "validated_at": "If proxy=true, validated via forwardedFor; otherwise, no explicit validation here" + } + ], + "description": "Implements the Resource::Manager interface for managing resource consumption, including tracking consumers, handling inbound/outbound endpoints, exporting/importing gossip, and periodic resource logic in a background thread.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "forwardedFor", + "validation", + "missing", + "check" + ], + "evidence": "Field forwardedFor validated by boost::asio::ip::make_address", + "issue_pattern": "Missing validation for forwardedFor", + "why_false_positive": "boost::asio::ip::make_address validates forwardedFor automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "forwardedFor", + "empty", + "string", + "validation" + ], + "evidence": "boost::asio::ip::make_address at ManagerImp::newInboundEndpoint(beast::IP::Endpoint const&, bool const, std::string_view)", + "issue_pattern": "Missing empty string validation for forwardedFor", + "why_false_positive": "boost::asio::ip::make_address validates forwardedFor for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "forwardedFor", + "format", + "validation", + "invalid" + ], + "evidence": "boost::asio::ip::make_address at ManagerImp::newInboundEndpoint(beast::IP::Endpoint const&, bool const, std::string_view)", + "issue_pattern": "Missing format validation for forwardedFor", + "why_false_positive": "boost::asio::ip::make_address validates forwardedFor format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/resource/ResourceManager.cpp", + "functions": [ + { + "args": [ + "beast::insight::Collector::ptr const& collector", + "beast::Journal journal" + ], + "lineno": 22, + "name": "ManagerImp" + }, + { + "args": [], + "lineno": 32, + "name": "~ManagerImp" + }, + { + "args": [ + "beast::IP::Endpoint const& address" + ], + "lineno": 39, + "name": "newInboundEndpoint" + }, + { + "args": [ + "beast::IP::Endpoint const& address", + "bool const proxy", + "std::string_view forwardedFor" + ], + "lineno": 44, + "name": "newInboundEndpoint" + }, + { + "args": [ + "beast::IP::Endpoint const& address" + ], + "lineno": 61, + "name": "newOutboundEndpoint" + }, + { + "args": [ + "beast::IP::Endpoint const& address" + ], + "lineno": 66, + "name": "newUnlimitedEndpoint" + }, + { + "args": [], + "lineno": 71, + "name": "exportConsumers" + }, + { + "args": [ + "std::string const& origin", + "Gossip const& gossip" + ], + "lineno": 76, + "name": "importConsumers" + }, + { + "args": [], + "lineno": 84, + "name": "getJson" + }, + { + "args": [ + "int threshold" + ], + "lineno": 89, + "name": "getJson" + }, + { + "args": [ + "beast::PropertyStream::Map& map" + ], + "lineno": 96, + "name": "onWrite" + }, + { + "args": [], + "lineno": 103, + "name": "run" + }, + { + "args": [], + "lineno": 117, + "name": "Manager" + }, + { + "args": [], + "lineno": 120, + "name": "~Manager" + }, + { + "args": [ + "beast::insight::Collector::ptr const& collector", + "beast::Journal journal" + ], + "lineno": 125, + "name": "make_Manager" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "Resource" + } + ], + "test_coverage_notes": "The validation of 'forwardedFor' via boost::asio::ip::make_address is a critical path. Typical test coverage would be in unit tests for ResourceManager or integration tests for HTTP request handling (where X-Forwarded-For is set). Look for test files like ResourceManager_test.cpp, Logic_test.cpp, or HTTPServerHandler_test.cpp. Gaps: If tests do not cover malformed or malicious 'forwardedFor' values, or do not check logging of warnings on invalid input, this path may be under-tested. There is no explicit test code in this file; coverage depends on higher-level tests invoking these entry points with various inputs.", + "validation_architecture": { + "auto_validated_fields": [ + "forwardedFor" + ], + "framework": "boost::asio::ip::make_address", + "validation_layer": "entry_point" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (error_code set on failure)", + "field": "forwardedFor", + "location": "ManagerImp::newInboundEndpoint(beast::IP::Endpoint const&, bool const, std::string_view)", + "validated_by": "boost::asio::ip::make_address", + "validates": [ + "Checks if forwardedFor string is a valid IP address" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/resource/ResourceManager.cpp.ai.md b/src/libxrpl/resource/ResourceManager.cpp.ai.md new file mode 100644 index 0000000000..439826e09c --- /dev/null +++ b/src/libxrpl/resource/ResourceManager.cpp.ai.md @@ -0,0 +1,39 @@ +# `ResourceManager.cpp` — Resource Manager Implementation + +## Role in the System + +`ResourceManager.cpp` is the concrete implementation of the `Resource::Manager` interface, the entry point for XRPL's per-peer resource-consumption tracking subsystem. The resource system exists to protect a rippled node from overload: every inbound or outbound connection is assigned a `Consumer` handle that accumulates a load balance as it makes requests. When that balance exceeds defined thresholds, the node can warn or drop the offending peer. This file wires together the stateless `Logic` engine, a background maintenance thread, and the public factory function `make_Manager()` that the rest of the node calls at startup. + +## The `ManagerImp` Private Implementation + +The entire implementation is hidden inside `ManagerImp`, a class private to the translation unit that inherits `Manager`. Callers only ever see `std::unique_ptr` returned by `make_Manager()`; the concrete type is completely opaque. This is a deliberate pImpl-style design: the header declares a pure virtual interface and the implementation detail — including the `Logic` hash tables, the background thread, and the mutex — never leak into headers that consumers must compile against. + +`ManagerImp` owns two major resources: a `Logic` instance and a `std::thread`. The `Logic` object (defined in `detail/Logic.h`) holds the actual hash tables of `Entry` records, the intrusive lists of active inbound/outbound/admin entries, and the gossip import table. Every public method on `ManagerImp` is a thin pass-through to the corresponding `Logic` method; `ManagerImp`'s own responsibility is purely lifecycle and threading. + +## Background Thread and Shutdown + +Construction launches a dedicated thread named `"Resource::Mngr"` that runs `ManagerImp::run()`. The loop calls `logic_.periodicActivity()` — which decays load balances, evicts stale entries, and fires telemetry meters — then waits up to one second on a `std::condition_variable` before repeating. The one-second cadence is intentional: load balances are time-weighted, and a sub-second tick would distort the decay math while a multi-second tick would make the system sluggish to respond to traffic spikes. + +Shutdown is clean and deterministic. The destructor acquires `mutex_`, sets `stop_ = true`, and signals the condition variable before calling `thread_.join()`. Because the `cond_.wait_for()` in `run()` holds the same `mutex_` via `std::unique_lock`, the signal interrupts the sleep immediately regardless of where in the one-second window the destructor fires. This avoids any fixed-sleep teardown latency. The ordering — signal under lock, then join outside the lock — is correct: the lock is released before `wait_for` returns, so the destructor can re-acquire it without deadlock. + +## Consumer Creation and Proxy Awareness + +The `Manager` interface exposes three consumer flavors, mapped directly to the `Kind` enum in `detail/Kind.h`: + +- **Inbound** (`kindInbound`): connections arriving at the server port, subject to normal rate limits. +- **Outbound** (`kindOutbound`): connections the node initiates to peers, tracked separately. +- **Unlimited** (`kindUnlimited`): trusted or administrative connections that bypass rate limiting but can still be subject to administrative RPC restrictions. + +The overloaded `newInboundEndpoint(address, proxy, forwardedFor)` is the most interesting variant. When a reverse proxy sits in front of the node, the TCP source address is the proxy's IP rather than the actual client's. This overload accepts the raw `X-Forwarded-For` string value and uses `boost::asio::ip::make_address()` to parse it. If parsing succeeds, the proxied IP is converted to a `beast::IP::Endpoint` via `beast::IPAddressConversion::from_asio()` and used as the consumer key. If parsing fails — because the header is absent, malformed, or contains a non-IP token — the code logs a warning at journal warn level and falls back to keying on the proxy's own address. This fallback is correct: it prevents a malformed header from crashing the node or creating an untracked consumer, at the cost of grouping all traffic through a broken proxy under one entry. + +## Gossip Protocol Integration + +The resource system participates in a peer-to-peer gossip mechanism for sharing load information. `exportConsumers()` serializes the current inbound consumer table into a `Gossip` struct (a flat vector of `{balance, address}` items), which the network layer can broadcast to peers. `importConsumers(origin, gossip)` ingests gossip received from another node, keying imported data by the `origin` identifier. This allows a cluster of rippled nodes to share knowledge about misbehaving IPs even if the abusive peer is connected to only one node — a critical property for protecting the peer-to-peer mesh as a whole. + +## Observability + +`Manager` inherits `beast::PropertyStream::Source` and registers under the name `"resource"`, making all resource state accessible through the node's diagnostic property-stream tree. `onWrite(map)` delegates to `Logic::onWrite()`, which walks the live tables and emits entries into the stream. The two `getJson()` overloads provide JSON snapshots — one unfiltered, one filtered by a minimum balance threshold — suitable for RPC introspection commands. + +## Thread Safety + +All mutable state lives inside `Logic`, which guards it with a `std::recursive_mutex`. `ManagerImp` itself only owns the `stop_` flag (protected by its own `mutex_`) and the background thread handle. Because `Logic`'s lock is recursive, `periodicActivity()` can call helper methods that also acquire the lock without deadlocking, a pattern that matters because `Logic` methods are called from both the background thread and from network-layer threads creating consumers. \ No newline at end of file diff --git a/src/libxrpl/server/InfoSub.cpp.ai.json b/src/libxrpl/server/InfoSub.cpp.ai.json new file mode 100644 index 0000000000..9abc893c97 --- /dev/null +++ b/src/libxrpl/server/InfoSub.cpp.ai.json @@ -0,0 +1,482 @@ +{ + "args": [ + { + "lineno": 15, + "name": "source" + }, + { + "lineno": 19, + "name": "consumer" + }, + { + "lineno": 53, + "name": "account" + }, + { + "lineno": 53, + "name": "rt" + }, + { + "lineno": 65, + "name": "account" + }, + { + "lineno": 65, + "name": "rt" + }, + { + "lineno": 77, + "name": "account" + }, + { + "lineno": 83, + "name": "account" + }, + { + "lineno": 93, + "name": "req" + }, + { + "lineno": 103, + "name": "apiVersion" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "InfoSub::getApiVersion" + ], + "entry_point": "InfoSub::getApiVersion", + "purpose": "Returns the API version for this InfoSub instance, with validation.", + "validation_points": [ + "XRPL_ASSERT(apiVersion_ > 0) in getApiVersion" + ] + }, + { + "call_chain": [ + "InfoSub::setApiVersion" + ], + "entry_point": "InfoSub::setApiVersion", + "purpose": "Sets the API version for this InfoSub instance.", + "validation_points": [] + }, + { + "call_chain": [ + "InfoSub::insertSubAccountInfo" + ], + "entry_point": "InfoSub::insertSubAccountInfo", + "purpose": "Adds an account to either realTimeSubscriptions_ or normalSubscriptions_ based on 'rt' flag.", + "validation_points": [] + }, + { + "call_chain": [ + "InfoSub::deleteSubAccountInfo" + ], + "entry_point": "InfoSub::deleteSubAccountInfo", + "purpose": "Removes an account from either realTimeSubscriptions_ or normalSubscriptions_ based on 'rt' flag.", + "validation_points": [] + }, + { + "call_chain": [ + "InfoSub::insertSubAccountHistory" + ], + "entry_point": "InfoSub::insertSubAccountHistory", + "purpose": "Adds an account to accountHistorySubscriptions_.", + "validation_points": [] + }, + { + "call_chain": [ + "InfoSub::deleteSubAccountHistory" + ], + "entry_point": "InfoSub::deleteSubAccountHistory", + "purpose": "Removes an account from accountHistorySubscriptions_.", + "validation_points": [] + }, + { + "call_chain": [ + "InfoSub::~InfoSub", + "m_source.unsubTransactions", + "m_source.unsubRTTransactions", + "m_source.unsubLedger", + "m_source.unsubManifests", + "m_source.unsubServer", + "m_source.unsubValidations", + "m_source.unsubPeerStatus", + "m_source.unsubConsensus", + "m_source.unsubAccountInternal", + "m_source.unsubAccountHistoryInternal" + ], + "entry_point": "InfoSub::~InfoSub", + "purpose": "Destructor: Unsubscribes this InfoSub from all subscriptions.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "apiVersion_", + "flow": [ + "setApiVersion (external call, likely from RPC or server setup)", + "apiVersion_ field is set", + "getApiVersion (external/internal call)", + "XRPL_ASSERT(apiVersion_ > 0)", + "apiVersion_ returned" + ], + "origin": "Set via InfoSub::setApiVersion(unsigned int apiVersion)", + "transformations": [ + "Direct assignment in setApiVersion", + "Validation in getApiVersion" + ], + "validated_at": "getApiVersion (XRPL_ASSERT)" + }, + { + "field": "realTimeSubscriptions_ / normalSubscriptions_", + "flow": [ + "insertSubAccountInfo (external call, likely from subscription logic)", + "AccountID inserted into realTimeSubscriptions_ or normalSubscriptions_", + "deleteSubAccountInfo (external call)", + "AccountID erased from respective set", + "~InfoSub (destructor)", + "unsubAccountInternal called with current sets" + ], + "origin": "insertSubAccountInfo(AccountID, bool rt)", + "transformations": [ + "Insertion/removal in sets" + ], + "validated_at": "No explicit validation" + }, + { + "field": "accountHistorySubscriptions_", + "flow": [ + "insertSubAccountHistory (external call)", + "AccountID inserted into accountHistorySubscriptions_", + "deleteSubAccountHistory (external call)", + "AccountID erased from set", + "~InfoSub (destructor)", + "unsubAccountHistoryInternal called for each account" + ], + "origin": "insertSubAccountHistory(AccountID)", + "transformations": [ + "Insertion/removal in set" + ], + "validated_at": "No explicit validation" + }, + { + "field": "request_", + "flow": [ + "setRequest (external call)", + "request_ field set", + "getRequest (external/internal call)", + "request_ returned", + "clearRequest (external/internal call)", + "request_ reset" + ], + "origin": "setRequest(std::shared_ptr)", + "transformations": [ + "Assignment, reset" + ], + "validated_at": "No explicit validation" + }, + { + "field": "mSeq", + "flow": [ + "InfoSub constructor", + "mSeq assigned", + "Used in all subscription/unsubscription calls", + "getSeq returns mSeq" + ], + "origin": "Assigned in InfoSub constructor via assign_id()", + "transformations": [ + "Assignment from assign_id()" + ], + "validated_at": "No explicit validation" + } + ], + "description": "Implements the InfoSub class, which serves as the primary interface for client operations on the XRPL network, handling subscriptions to various network events and managing client requests.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "mSeq (type: std::uint64_t, assigned by assign_id())", + "validation", + "missing", + "check" + ], + "evidence": "Field mSeq (type: std::uint64_t, assigned by assign_id()) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for mSeq (type: std::uint64_t, assigned by assign_id())", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates mSeq (type: std::uint64_t, assigned by assign_id()) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "apiVersion_ (unsigned int, but only runtime checked in getApiVersion())", + "validation", + "missing", + "check" + ], + "evidence": "Field apiVersion_ (unsigned int, but only runtime checked in getApiVersion()) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for apiVersion_ (unsigned int, but only runtime checked in getApiVersion())", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates apiVersion_ (unsigned int, but only runtime checked in getApiVersion()) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "AccountID (type safety for account fields in insert/delete functions)", + "validation", + "missing", + "check" + ], + "evidence": "Field AccountID (type safety for account fields in insert/delete functions) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for AccountID (type safety for account fields in insert/delete functions)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates AccountID (type safety for account fields in insert/delete functions) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "apiVersion_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at getApiVersion()", + "issue_pattern": "Missing empty string validation for apiVersion_", + "why_false_positive": "XRPL_ASSERT macro validates apiVersion_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "apiVersion_", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT macro at getApiVersion()", + "issue_pattern": "Missing range validation for apiVersion_", + "why_false_positive": "XRPL_ASSERT macro validates apiVersion_ range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/server/InfoSub.cpp", + "functions": [ + { + "args": [ + "Source& source" + ], + "lineno": 15, + "name": "InfoSub" + }, + { + "args": [ + "Source& source", + "Consumer consumer" + ], + "lineno": 19, + "name": "InfoSub" + }, + { + "args": [], + "lineno": 24, + "name": "~InfoSub" + }, + { + "args": [], + "lineno": 39, + "name": "getConsumer" + }, + { + "args": [], + "lineno": 44, + "name": "getSeq" + }, + { + "args": [], + "lineno": 49, + "name": "onSendEmpty" + }, + { + "args": [ + "AccountID const& account", + "bool rt" + ], + "lineno": 53, + "name": "insertSubAccountInfo" + }, + { + "args": [ + "AccountID const& account", + "bool rt" + ], + "lineno": 65, + "name": "deleteSubAccountInfo" + }, + { + "args": [ + "AccountID const& account" + ], + "lineno": 77, + "name": "insertSubAccountHistory" + }, + { + "args": [ + "AccountID const& account" + ], + "lineno": 83, + "name": "deleteSubAccountHistory" + }, + { + "args": [], + "lineno": 89, + "name": "clearRequest" + }, + { + "args": [ + "std::shared_ptr const& req" + ], + "lineno": 93, + "name": "setRequest" + }, + { + "args": [], + "lineno": 98, + "name": "getRequest" + }, + { + "args": [ + "unsigned int apiVersion" + ], + "lineno": 103, + "name": "setApiVersion" + }, + { + "args": [], + "lineno": 108, + "name": "getApiVersion" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, InfoSub is tested indirectly via higher-level server or RPC tests (e.g., subscription/unsubscription, API versioning). The only explicit validation is XRPL_ASSERT in getApiVersion, which would only be tested if getApiVersion is called with an unset or zero apiVersion_. There is likely a gap in direct unit tests for InfoSub's validation logic, especially for apiVersion_ (e.g., negative or zero values). Subscription management is likely covered by integration tests, but explicit validation paths (like XRPL_ASSERT) may not be directly tested unless negative test cases are written.", + "validation_architecture": { + "auto_validated_fields": [ + "mSeq (type: std::uint64_t, assigned by assign_id())", + "apiVersion_ (unsigned int, but only runtime checked in getApiVersion())", + "AccountID (type safety for account fields in insert/delete functions)" + ], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "apiVersion_", + "location": "getApiVersion()", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "apiVersion_ must be > 0" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/server/InfoSub.cpp.ai.md b/src/libxrpl/server/InfoSub.cpp.ai.md new file mode 100644 index 0000000000..d3cc9ead32 --- /dev/null +++ b/src/libxrpl/server/InfoSub.cpp.ai.md @@ -0,0 +1,41 @@ +# `InfoSub.cpp` — Client Subscription State Manager + +`InfoSub` represents a single connected client's view of the XRPL event stream. Its job is to act as the durable record of everything that client has subscribed to — ledger events, transactions, validator manifests, consensus rounds, individual accounts — and to ensure every one of those subscriptions is cleanly torn down when the connection closes. The `.cpp` file is compact because most architectural weight lives in the header; the implementation is primarily about lifecycle management and thread-safe mutation of subscription sets. + +## The Subscription Identity Problem + +Every `InfoSub` instance receives an immutable, process-unique 64-bit sequence number via `assign_id()`, stored as `mSeq`. The implementation uses a function-local `std::atomic` that increments on each construction — simple, lock-free, and correct under concurrent connection setup. This number is the client's identity across all server-side subscription tables. When the server (the `Source`) needs to remove a subscriber it addresses it by this integer, never by pointer. That design isolates the server tables from the lifetime of the `InfoSub` object itself, which matters critically in the destructor. + +## The Destructor's Two-Phase Unsubscription + +The destructor is the most important method in the file, and its comment is the key to understanding it. For flat event streams — transactions, ledger, manifests, server status, validations, peer status, consensus — each `unsub*` call takes only the `mSeq` integer. These calls remove the entry from the server's lookup table and nothing else. + +Account subscriptions are different. The `hash_set` members `realTimeSubscriptions_` and `normalSubscriptions_` are a mirror of the per-account subscription maps held by the `Source`. Under normal operation, when user code calls `unsubAccount`, the server removes the `InfoSub` from its maps *and* calls back to `deleteSubAccountInfo` to remove the account from `InfoSub`'s own sets. During destruction that callback would modify a container inside an already-dying object — harmless in practice, but architecturally wrong and potentially unsafe if subclass destructors have already run. + +The solution is `unsubAccountInternal`, an overload on `Source` that takes the raw `mSeq` and the set by value. It only touches the server's data structures. The `InfoSub` destructor passes its own sets directly, skipping the callback entirely. The empty checks before calling `unsubAccountInternal` are a minor optimization: if no accounts were ever subscribed, the virtual dispatch and set iteration are avoided. Account history subscriptions use the same pattern via `unsubAccountHistoryInternal`, iterating one account at a time because each history subscription can be in a partially-completed state. + +## Account Subscription Flavors + +The class maintains three separate `hash_set` collections, each with a distinct semantic: + +`realTimeSubscriptions_` — accounts whose transactions are delivered as they enter the network, before ledger close confirmation. The original comments note the "rt" naming was a historical artifact for "real time" meaning "unconfirmed." + +`normalSubscriptions_` — confirmed transactions only, delivered after ledger close. + +`accountHistorySubscriptions_` — a more exotic subscription type where the client also receives past transactions replayed from history. `insertSubAccountHistory` intentionally returns `bool` (whether the account was newly inserted) so callers can avoid redundant historical replay if a client double-subscribes. + +All three mutation methods (`insertSubAccountInfo`, `deleteSubAccountInfo`, `insertSubAccountHistory`, `deleteSubAccountHistory`) take `mLock`, a `protected` `std::mutex`. Marking the lock `protected` rather than `private` lets concrete subclasses like `WSInfoSub` or `RPCSub` extend its protection scope if they need to make compound operations atomic. + +## The `Source` Interface and Inversion of Control + +`InfoSub::Source` is a pure abstract inner class defining the entire subscription API that the server must implement. This is a deliberate inversion: `InfoSub` holds a reference to its source, but the source (in practice, `NetworkOPs`) knows nothing about the concrete `InfoSub` subclass — it sees only `InfoSub::ref` (`shared_ptr const&`). This decouples protocol logic from transport. The two known concrete subclasses in the codebase are `WSInfoSub`, which streams JSON over a WebSocket session held by weak pointer, and `RPCSub`, which serializes and delivers events to a remote HTTP callback URL via the job queue. Neither affects how the `Source` tracks subscriptions. + +## Resource Tracking and API Versioning + +`m_consumer` holds a `Resource::Consumer` handle that integrates with the server's load-shedding and rate-limiting framework. A client that floods the server with requests accrues charges; the consumer tracks a credit balance and signals when warnings or disconnection are warranted. The two-constructor design — one with and one without a `Consumer` — reflects that not all connection types participate in resource accounting (e.g., internal pseudo-clients). + +`apiVersion_` starts at zero and `getApiVersion()` asserts it is positive before returning. This is a deliberate "fail fast" guard: if server-side setup code forgets to call `setApiVersion`, any downstream use of the version field will abort with a clear message rather than silently applying version-zero behavior. The `noexcept` on `getApiVersion` is notable — `XRPL_ASSERT` is expected to terminate the process rather than throw, so the `noexcept` contract holds even on assertion failure. + +## Relation to `InfoSubRequest` + +`InfoSub` optionally holds a `shared_ptr`, a separate abstract interface for "path find" style stateful requests that need to be notified of close or status queries. This is a narrow escape hatch for long-lived RPC requests that outlive a single handler invocation. `clearRequest()` / `setRequest()` / `getRequest()` form a simple optional-value pattern using `shared_ptr` null as the absent state. \ No newline at end of file diff --git a/src/libxrpl/server/JSONRPCUtil.cpp.ai.json b/src/libxrpl/server/JSONRPCUtil.cpp.ai.json new file mode 100644 index 0000000000..91301e19f8 --- /dev/null +++ b/src/libxrpl/server/JSONRPCUtil.cpp.ai.json @@ -0,0 +1,185 @@ +{ + "args": [ + { + "lineno": 22, + "name": "nStatus" + }, + { + "lineno": 22, + "name": "content" + }, + { + "lineno": 22, + "name": "output" + }, + { + "lineno": 22, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "HTTPReply" + ], + "entry_point": "HTTPReply", + "purpose": "Formats and outputs an HTTP response based on status and content, including validation of content and status.", + "validation_points": [ + "if (content.empty() && nStatus == 401) { ... } // Validates content and status for special 401 handling", + "switch (nStatus) { ... } // Validates nStatus against allowed HTTP codes" + ] + }, + { + "call_chain": [ + "getHTTPHeaderTimestamp" + ], + "entry_point": "getHTTPHeaderTimestamp", + "purpose": "Generates a formatted HTTP date header string.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "content", + "flow": [ + "HTTPReply argument", + "Validation: content.empty() check", + "Used in output (either as part of special 401 HTML or as JSON body)" + ], + "origin": "Passed as argument to HTTPReply (likely from upstream HTTP request handler or RPC handler)", + "transformations": [ + "Checked for emptiness", + "Size used for Content-Length calculation", + "Output directly as HTTP body" + ], + "validated_at": "if (content.empty() && nStatus == 401)" + }, + { + "field": "nStatus", + "flow": [ + "HTTPReply argument", + "Validation: content.empty() && nStatus == 401", + "switch (nStatus) for HTTP status line selection" + ], + "origin": "Passed as argument to HTTPReply (set by upstream logic based on request outcome)", + "transformations": [ + "Checked for specific value (401) in if statement", + "Matched in switch statement to select HTTP status line" + ], + "validated_at": [ + "if (content.empty() && nStatus == 401)", + "switch (nStatus)" + ] + }, + { + "field": "output", + "flow": [ + "HTTPReply argument", + "Used to emit all HTTP headers and body" + ], + "origin": "Passed as argument to HTTPReply (Json::Output, likely a functor or callback for writing response)", + "transformations": [ + "Receives formatted strings for output" + ], + "validated_at": "Not validated (assumed to be a valid callable)" + } + ], + "description": "Provides utility functions for generating HTTP headers and replies for the XRPL JSON-RPC server, including timestamped headers and error handling.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "content", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (content.empty()) at HTTPReply", + "issue_pattern": "Missing empty string validation for content", + "why_false_positive": "explicit check (content.empty()) validates content for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "nStatus", + "empty", + "string", + "validation" + ], + "evidence": "switch statement at HTTPReply", + "issue_pattern": "Missing empty string validation for nStatus", + "why_false_positive": "switch statement validates nStatus for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/server/JSONRPCUtil.cpp", + "functions": [ + { + "args": [], + "lineno": 9, + "name": "getHTTPHeaderTimestamp" + }, + { + "args": [ + "nStatus", + "content", + "output", + "j" + ], + "lineno": 22, + "name": "HTTPReply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is utility code for HTTP response formatting and is likely tested indirectly via higher-level server or RPC tests. Direct unit tests for HTTPReply and getHTTPHeaderTimestamp may not exist, but integration tests for HTTP endpoints (e.g., in test/server or test/rpc) would exercise these paths. Gaps: No explicit tests for malformed or edge-case content/status combinations, and no tests for header formatting edge cases. No validation of output functor/callback.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (conditional logic, not exception)", + "field": "content", + "location": "HTTPReply", + "validated_by": "explicit check (content.empty())", + "validates": [ + "Checks if content is empty when nStatus == 401 to determine special unauthorized response" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "None (default case not handled, falls through)", + "field": "nStatus", + "location": "HTTPReply", + "validated_by": "switch statement", + "validates": [ + "Checks if nStatus is one of the allowed HTTP status codes (200, 202, 400, 401, 403, 404, 405)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/server/JSONRPCUtil.cpp.ai.md b/src/libxrpl/server/JSONRPCUtil.cpp.ai.md new file mode 100644 index 0000000000..2b60bfeb15 --- /dev/null +++ b/src/libxrpl/server/JSONRPCUtil.cpp.ai.md @@ -0,0 +1,38 @@ +# `src/libxrpl/server/JSONRPCUtil.cpp` + +This file implements the two thin but critical utility functions that serialize raw HTTP responses for the XRPL JSON-RPC server. Every HTTP reply that leaves `ServerHandler` passes through `HTTPReply`, making this the single place where the wire format of the server's HTTP layer is defined. + +## Role in the Larger System + +The XRPL node exposes its RPC API over plain HTTP. `ServerHandler` in `src/xrpld/rpc/detail/ServerHandler.cpp` handles connection lifecycle and request parsing, but it delegates all response serialization here. The public contract is minimal: a single header at `include/xrpl/server/detail/JSONRPCUtil.h` exposes only `HTTPReply`, keeping `getHTTPHeaderTimestamp` as an internal helper. + +## `getHTTPHeaderTimestamp` + +This function produces a complete `Date:` header line in the HTTP/1.1 date format (`Date: Mon, 01 Jan 2024 00:00:00 +0000\r\n`). The implementation is straightforward POSIX: `time()` → `gmtime_r` (or `gmtime_s` on MSVC) → `strftime` into a 96-byte stack buffer. The cross-platform split uses a compile-time `#ifndef _MSC_VER` guard — the two functions have swapped argument order, which is a historical MSVC divergence, and the guard handles it cleanly without any runtime cost. + +A `CHECKME` comment acknowledges that this call is not free and that memoizing the result — since the timestamp only needs second-level precision — might be worthwhile at high request rates. In practice it is called twice per response (once for the 401 challenge path and once for every other response), but no caching has been added. + +## `HTTPReply` + +```cpp +void HTTPReply(int nStatus, std::string const& content, + Json::Output const& output, beast::Journal j); +``` + +`Json::Output` is defined in `include/xrpl/json/Output.h` as `std::function`. The callback-based design means HTTP headers and body are streamed to the underlying transport in small chunks without ever allocating a single buffer large enough to hold the entire response. This matters for large JSON payloads from ledger-dump commands. + +The function handles two distinct code paths: + +**Bare 401 challenge.** When `content.empty() && nStatus == 401`, `HTTPReply` emits an HTTP/1.0 `WWW-Authenticate: Basic` challenge with a hardcoded 296-byte HTML body. Two design wrinkles are called out in the source itself: (1) this branch uses `HTTP/1.0` while all other branches use `HTTP/1.1` — the comment marks this as potentially accidental, (2) the `Server:` header is built from `systemName() + "-json-rpc/v1"` with a literal `v1`, whereas the normal path uses `BuildInfo::getFullVersionString()`. The hardcoded `Content-Length: 296` comment warns that the constant must be updated if the HTML body changes — a classic maintenance trap. + +**All other responses.** A `switch` on `nStatus` emits the correct HTTP status line for the subset of codes the server actually uses: 200, 202, 400, 401, 403, 404, 405, 429, 500, 501, and 503. The switch has no `default` case — a `// NOLINTNEXTLINE` suppresses the linter. An unrecognized status code silently produces a response with `Date:`, `Connection: Keep-Alive`, `Content-Length`, and `Content-Type` headers but no status line, which would be malformed. In practice this cannot happen because all `HTTPReply` call sites in `ServerHandler.cpp` hard-code one of the enumerated codes. + +The `Content-Length` calculation is `content.size() + 2`. The `+2` accounts for the `\r\n` that `HTTPReply` unconditionally appends after the body. This means the length advertised to the client is always accurate without requiring the caller to pre-compute it. + +The `Server:` header for normal replies is assembled from `systemName()` (returning `"xrpld"` as a compile-time static string) concatenated with `BuildInfo::getFullVersionString()`, yielding something like `xrpld-json-rpc/xrpld-2.3.0`. This matches the identifier used in WebSocket handshakes via `BaseWSPeer`. + +## Call Sites and Error Handling + +`ServerHandler::onRequest` calls `HTTPReply(403, ...)` when the port does not have the HTTP protocol enabled, when authorization fails, and when a request is role-forbidden. It calls `HTTPReply(503, ...)` when the server rejects the coroutine (typically during shutdown or overload). The request-processing loop calls `HTTPReply(400, ...)` for every parse and validation failure. The final call at line 983 of `ServerHandler.cpp` sends the actual RPC result with the HTTP status code derived from the JSON response. + +Logging is handled by a single `JLOG(j.trace())` at the top of `HTTPReply`, recording the status code and the full content string. At trace level this produces verbose output; the journal guard ensures no string formatting occurs in production unless the trace sink is attached. \ No newline at end of file diff --git a/src/libxrpl/server/LoadFeeTrack.cpp.ai.json b/src/libxrpl/server/LoadFeeTrack.cpp.ai.json new file mode 100644 index 0000000000..915babdef1 --- /dev/null +++ b/src/libxrpl/server/LoadFeeTrack.cpp.ai.json @@ -0,0 +1,421 @@ +{ + "args": [ + { + "lineno": 49, + "name": "fee" + }, + { + "lineno": 49, + "name": "feeTrack" + }, + { + "lineno": 49, + "name": "fees" + }, + { + "lineno": 49, + "name": "bUnlimited" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoadFeeTrack::raiseLocalFee" + ], + "entry_point": "LoadFeeTrack::raiseLocalFee", + "purpose": "Raises the local transaction load fee if certain conditions are met, to manage server load.", + "validation_points": [ + "if (++raiseCount_ < 2) // Ensures fee is not raised too frequently", + "localTxnLoadFee_ = std::max(localTxnLoadFee_, remoteTxnLoadFee_) // Ensures local fee is at least remote fee", + "localTxnLoadFee_ = std::min(localTxnLoadFee_, lftFeeMax) // Caps the fee at a maximum" + ] + }, + { + "call_chain": [ + "LoadFeeTrack::lowerLocalFee" + ], + "entry_point": "LoadFeeTrack::lowerLocalFee", + "purpose": "Lowers the local transaction load fee, typically when load decreases.", + "validation_points": [ + "localTxnLoadFee_ -= (localTxnLoadFee_ / lftFeeDecFraction) // Reduces fee slowly", + "localTxnLoadFee_ = std::max(localTxnLoadFee_, lftNormalFee) // Ensures fee does not go below normal" + ] + }, + { + "call_chain": [ + "scaleFeeLoad" + ], + "entry_point": "scaleFeeLoad", + "purpose": "Scales a transaction fee based on current load and user privileges.", + "validation_points": [ + "if (fee == 0) return fee; // Validates input fee", + "feeTrack.getScalingFactors() // Gets current scaling factors (validated in LoadFeeTrack)", + "if (bUnlimited && (feeFactor > uRemFee) && (feeFactor < (4 * uRemFee))) feeFactor = uRemFee; // Privilege check", + "mulDiv(fee, feeFactor, safe_cast(feeTrack.getLoadBase())) // Validates against overflow" + ] + } + ], + "data_flows": [ + { + "field": "raiseCount_", + "flow": [ + "initialized to 0", + "incremented in raiseLocalFee", + "reset to 0 in lowerLocalFee" + ], + "origin": "LoadFeeTrack class member, initialized to 0", + "transformations": [ + "Incremented (++raiseCount_) in raiseLocalFee", + "Reset (raiseCount_ = 0) in lowerLocalFee" + ], + "validated_at": "if (++raiseCount_ < 2) in raiseLocalFee" + }, + { + "field": "localTxnLoadFee_", + "flow": [ + "initialized", + "compared and set to max(localTxnLoadFee_, remoteTxnLoadFee_) in raiseLocalFee", + "incremented by (localTxnLoadFee_ / lftFeeIncFraction) in raiseLocalFee", + "capped at lftFeeMax in raiseLocalFee", + "decremented by (localTxnLoadFee_ / lftFeeDecFraction) in lowerLocalFee", + "floored at lftNormalFee in lowerLocalFee" + ], + "origin": "LoadFeeTrack class member, set at construction or via config", + "transformations": [ + "max(localTxnLoadFee_, remoteTxnLoadFee_)", + "add (localTxnLoadFee_ / lftFeeIncFraction)", + "min(localTxnLoadFee_, lftFeeMax)", + "subtract (localTxnLoadFee_ / lftFeeDecFraction)", + "max(localTxnLoadFee_, lftNormalFee)" + ], + "validated_at": [ + "std::max(localTxnLoadFee_, remoteTxnLoadFee_) in raiseLocalFee", + "std::min(localTxnLoadFee_, lftFeeMax) in raiseLocalFee", + "std::max(localTxnLoadFee_, lftNormalFee) in lowerLocalFee" + ] + }, + { + "field": "remoteTxnLoadFee_", + "flow": [ + "set from network", + "used in std::max(localTxnLoadFee_, remoteTxnLoadFee_) in raiseLocalFee" + ], + "origin": "LoadFeeTrack class member, updated from network peers", + "transformations": [ + "Compared to localTxnLoadFee_" + ], + "validated_at": "std::max(localTxnLoadFee_, remoteTxnLoadFee_) in raiseLocalFee" + }, + { + "field": "fee (parameter to scaleFeeLoad)", + "flow": [ + "passed to scaleFeeLoad", + "checked for zero", + "multiplied by feeFactor", + "divided by loadBase" + ], + "origin": "Caller of scaleFeeLoad (transaction fee calculation)", + "transformations": [ + "mulDiv(fee, feeFactor, loadBase)" + ], + "validated_at": [ + "if (fee == 0)", + "mulDiv result checked for overflow" + ] + } + ], + "description": "Implements fee scaling and adjustment logic for transaction fees in the XRPL server, including methods to raise or lower local fees and to scale fees based on network load.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "raiseCount_", + "empty", + "string", + "validation" + ], + "evidence": "if (++raiseCount_ < 2) at raiseLocalFee", + "issue_pattern": "Missing empty string validation for raiseCount_", + "why_false_positive": "if (++raiseCount_ < 2) validates raiseCount_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "localTxnLoadFee_", + "empty", + "string", + "validation" + ], + "evidence": "localTxnLoadFee_ = std::max(localTxnLoadFee_, remoteTxnLoadFee_) at raiseLocalFee", + "issue_pattern": "Missing empty string validation for localTxnLoadFee_", + "why_false_positive": "localTxnLoadFee_ = std::max(localTxnLoadFee_, remoteTxnLoadFee_) validates localTxnLoadFee_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "localTxnLoadFee_", + "empty", + "string", + "validation" + ], + "evidence": "localTxnLoadFee_ = std::min(localTxnLoadFee_, lftFeeMax) at raiseLocalFee", + "issue_pattern": "Missing empty string validation for localTxnLoadFee_", + "why_false_positive": "localTxnLoadFee_ = std::min(localTxnLoadFee_, lftFeeMax) validates localTxnLoadFee_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "localTxnLoadFee_", + "range", + "bounds", + "validation" + ], + "evidence": "localTxnLoadFee_ = std::min(localTxnLoadFee_, lftFeeMax) at raiseLocalFee", + "issue_pattern": "Missing range validation for localTxnLoadFee_", + "why_false_positive": "localTxnLoadFee_ = std::min(localTxnLoadFee_, lftFeeMax) validates localTxnLoadFee_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "localTxnLoadFee_", + "empty", + "string", + "validation" + ], + "evidence": "localTxnLoadFee_ -= (localTxnLoadFee_ / lftFeeDecFraction) at lowerLocalFee", + "issue_pattern": "Missing empty string validation for localTxnLoadFee_", + "why_false_positive": "localTxnLoadFee_ -= (localTxnLoadFee_ / lftFeeDecFraction) validates localTxnLoadFee_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "localTxnLoadFee_", + "empty", + "string", + "validation" + ], + "evidence": "localTxnLoadFee_ = std::max(localTxnLoadFee_, lftNormalFee) at lowerLocalFee", + "issue_pattern": "Missing empty string validation for localTxnLoadFee_", + "why_false_positive": "localTxnLoadFee_ = std::max(localTxnLoadFee_, lftNormalFee) validates localTxnLoadFee_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "localTxnLoadFee_", + "range", + "bounds", + "validation" + ], + "evidence": "localTxnLoadFee_ = std::max(localTxnLoadFee_, lftNormalFee) at lowerLocalFee", + "issue_pattern": "Missing range validation for localTxnLoadFee_", + "why_false_positive": "localTxnLoadFee_ = std::max(localTxnLoadFee_, lftNormalFee) validates localTxnLoadFee_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fee", + "empty", + "string", + "validation" + ], + "evidence": "if (fee == 0) return fee; at scaleFeeLoad", + "issue_pattern": "Missing empty string validation for fee", + "why_false_positive": "if (fee == 0) return fee; validates fee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mulDiv result (fee, feeFactor, feeTrack.getLoadBase())", + "empty", + "string", + "validation" + ], + "evidence": "if (!result) Throw(...) at scaleFeeLoad", + "issue_pattern": "Missing empty string validation for mulDiv result (fee, feeFactor, feeTrack.getLoadBase())", + "why_false_positive": "if (!result) Throw(...) validates mulDiv result (fee, feeFactor, feeTrack.getLoadBase()) for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/server/LoadFeeTrack.cpp", + "functions": [ + { + "args": [], + "lineno": 9, + "name": "LoadFeeTrack::raiseLocalFee" + }, + { + "args": [], + "lineno": 29, + "name": "LoadFeeTrack::lowerLocalFee" + }, + { + "args": [ + "fee", + "feeTrack", + "fees", + "bUnlimited" + ], + "lineno": 48, + "name": "scaleFeeLoad" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The core logic is likely tested in unit tests under 'src/test/server/LoadFeeTrack_test.cpp' or similar files. These tests should cover raising and lowering fees, boundary conditions (max/min), and scaling logic. However, integration with network-updated remoteTxnLoadFee_ and privilege logic in scaleFeeLoad may not be fully covered. Edge cases such as overflow in mulDiv or concurrent access to LoadFeeTrack may not be exhaustively tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, C++ std::max/min, custom Throw)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "raiseCount_", + "location": "raiseLocalFee", + "validated_by": "if (++raiseCount_ < 2)", + "validates": [ + "raiseCount_ must be >= 2 to proceed with raising fee" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "localTxnLoadFee_", + "location": "raiseLocalFee", + "validated_by": "localTxnLoadFee_ = std::max(localTxnLoadFee_, remoteTxnLoadFee_)", + "validates": [ + "localTxnLoadFee_ is set to at least remoteTxnLoadFee_" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "localTxnLoadFee_", + "location": "raiseLocalFee", + "validated_by": "localTxnLoadFee_ = std::min(localTxnLoadFee_, lftFeeMax)", + "validates": [ + "localTxnLoadFee_ does not exceed lftFeeMax" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "localTxnLoadFee_", + "location": "lowerLocalFee", + "validated_by": "localTxnLoadFee_ -= (localTxnLoadFee_ / lftFeeDecFraction)", + "validates": [ + "localTxnLoadFee_ is reduced by a fraction" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "localTxnLoadFee_", + "location": "lowerLocalFee", + "validated_by": "localTxnLoadFee_ = std::max(localTxnLoadFee_, lftNormalFee)", + "validates": [ + "localTxnLoadFee_ does not go below lftNormalFee" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "fee", + "location": "scaleFeeLoad", + "validated_by": "if (fee == 0) return fee;", + "validates": [ + "fee of 0 is a no-op and returned immediately" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::overflow_error", + "field": "mulDiv result (fee, feeFactor, feeTrack.getLoadBase())", + "location": "scaleFeeLoad", + "validated_by": "if (!result) Throw(...)", + "validates": [ + "checks for overflow in fee calculation" + ], + "validation_type": "type|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/server/LoadFeeTrack.cpp.ai.md b/src/libxrpl/server/LoadFeeTrack.cpp.ai.md new file mode 100644 index 0000000000..386951b8a8 --- /dev/null +++ b/src/libxrpl/server/LoadFeeTrack.cpp.ai.md @@ -0,0 +1,39 @@ +# `LoadFeeTrack.cpp` — Dynamic Transaction Fee Scaling + +This file implements the two methods of `LoadFeeTrack` that mutate fee state, and the free function `scaleFeeLoad` that converts a raw fee amount into the load-adjusted amount that a transaction must actually pay. Together they form the core of the XRPL server's adaptive fee mechanism, which dynamically prices transaction submission proportional to how stressed the local node is. + +## Architecture and Purpose + +The XRPL protocol requires every transaction to include a minimum fee in drops. Under normal conditions, that minimum is just the "reference fee" — a tiny amount. But a validator or relay node under heavy load has every incentive to charge more: higher fees make senders prioritise their most important transactions, shedding low-value traffic as the node approaches capacity. `LoadFeeTrack` (header in `include/xrpl/server/LoadFeeTrack.h`) tracks three independent scale factors: + +- `localTxnLoadFee_` — this node's own load multiplier, adjusted each second +- `remoteTxnLoadFee_` — the highest multiplier seen from network peers (written externally via `setRemoteFee()`) +- `clusterTxnLoadFee_` — a multiplier from the trusted cluster (written via `setClusterFee()`) + +All three start at `lftNormalFee = 256`. This constant acts as the denominator of the scale ratio: a fee of 10,000 drops with a factor of 256 / 256 = 1.0 costs exactly 10,000 drops. A factor of 512 doubles it. + +## Fee Adjustment: `raiseLocalFee()` and `lowerLocalFee()` + +`LoadManager` owns a dedicated background thread that wakes every second, checks whether the application's job queue is overloaded, and calls exactly one of these two methods. If the queue is overloaded, it calls `raiseLocalFee()`; otherwise `lowerLocalFee()`. + +**`raiseLocalFee()`** has a deliberate two-sample guard: it pre-increments `raiseCount_` and returns `false` immediately if the new count is less than 2. Only on the second consecutive overloaded sample does it actually mutate the fee. This single-sample immunity prevents transient queue bursts from immediately penalising submitters — load must persist for at least two seconds before fees begin to climb. Once triggered, the fee is first snapped up to at least `remoteTxnLoadFee_` (ensuring the local rate never falls below the network's observed rate at the moment of raising), then increased by `1/4` of the current value — a compound, exponential escalation. An absolute ceiling of `lftFeeMax = lftNormalFee × 1,000,000` prevents the multiplier from growing unboundedly. + +**`lowerLocalFee()`** resets `raiseCount_` to zero immediately, so any future raise will again require two consecutive overloaded samples. It then subtracts `1/4` of the current fee, mirroring the raise step size, but floors at `lftNormalFee` so the local multiplier never drops below baseline. Both functions return `false` when the fee did not actually change — either because it was already at the boundary or because the guard short-circuited — allowing `LoadManager` to skip the `reportFeeChange()` notification in that case. + +The raise/lower fraction constants (`lftFeeIncFraction` and `lftFeeDecFraction`) are both 4, so the geometric step size is symmetric. However, because raising requires two consecutive triggers while lowering acts immediately, the fee decays faster than it escalates in practice — this is an asymmetric bias toward giving submitters relief, appropriate for a public network. + +All mutations and reads go through `std::mutex lock_` via `std::lock_guard`, providing RAII-safe locking. The mutex is `mutable` so that `const` query methods like `getLocalFee()` and `getScalingFactors()` can still acquire it safely. + +## Fee Computation: `scaleFeeLoad()` + +`scaleFeeLoad()` is the point where an incoming or outgoing transaction's raw base fee is converted into what the node actually requires. Its signature accepts an `XRPAmount` (in drops), the `LoadFeeTrack` instance, a `Fees` struct (not directly used in the current implementation body), and a `bUnlimited` flag that marks trusted/privileged clients. + +The function delegates to `getScalingFactors()`, which returns a pair: `(max(local, remote), max(remote, cluster))`. The first element — the effective local factor — is what drives scaling. The second — the "remote factor" — is used for the privilege exemption. + +**Privilege logic**: Nodes in the trusted cluster or with administrator-level access receive `bUnlimited = true`. They pay only the remote/cluster rate as long as the local factor is below four times the remote. Once local stress exceeds that threshold — the node is genuinely overwhelmed — even privileged callers pay the full local rate. The threshold of 4× represents a judgment that moderate local overload should not punish trusted submitters, but severe overload affects everyone. + +**Overflow-safe arithmetic**: The actual computation is `fee × feeFactor / lftNormalFee`, but since `feeFactor` can reach `lftFeeMax = 256,000,000` and `fee` can be a substantial XRP amount in drops, the intermediate product can overflow 64 bits. `mulDiv()` (from `include/xrpl/protocol/Units.h`) handles this by performing the multiplication in 128-bit via `boost::multiprecision::uint128_t` and returning `std::nullopt` on overflow. `scaleFeeLoad()` checks that optional and throws `std::overflow_error` via the XRPL `Throw<>` helper if it fires — a defensive hard failure rather than silently returning a wrong fee. + +## Integration + +`LoadManager` is the sole writer of local fee state; it runs the raise/lower loop in its dedicated thread. The transaction application path (`Transactor.cpp`) calls `scaleFeeLoad()` during fee checking, passing `tapUNLIMITED` flag through to `bUnlimited`. RPC signing code (`TransactionSign.cpp`) similarly calls `scaleFeeLoad()` to report what fee a transaction will require before submission. Changes to the fee track are broadcast via `app_.getOPs().reportFeeChange()`, which notifies the network layer to propagate updated fee information to peers. \ No newline at end of file diff --git a/src/libxrpl/server/Manifest.cpp.ai.json b/src/libxrpl/server/Manifest.cpp.ai.json new file mode 100644 index 0000000000..b7945f9123 --- /dev/null +++ b/src/libxrpl/server/Manifest.cpp.ai.json @@ -0,0 +1,744 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "deserializeManifest", + "STObject::applyTemplate(manifestFormat)", + "publicKeyType(makeSlice(pk))", + "isProperlyFormedTomlDomain(domain)", + "Manifest::revoked(seq)", + "Manifest constructor" + ], + "entry_point": "deserializeManifest", + "purpose": "Deserializes and validates a manifest from a binary slice, enforcing field presence, types, and business rules.", + "validation_points": [ + "s.empty() check at start of deserializeManifest", + "STObject::applyTemplate(manifestFormat) for field presence/type", + "st.isFieldPresent(sfVersion) && st.getFieldU16(sfVersion) != 0 for version validation", + "publicKeyType(makeSlice(pk)) for master public key type", + "isProperlyFormedTomlDomain(domain) for domain format", + "publicKeyType(makeSlice(spk)) for signing key type", + "signingKey != masterKey for key uniqueness", + "Revocation logic for ephemeral key/sig presence" + ] + }, + { + "call_chain": [ + "to_string", + "Manifest::revoked", + "toBase58" + ], + "entry_point": "to_string", + "purpose": "Formats a Manifest as a string, checks for revoked status and presence of signing key.", + "validation_points": [ + "Manifest::revoked (checks sequence number)", + "Throw if signingKey is missing" + ] + } + ], + "data_flows": [ + { + "field": "s (Slice input)", + "flow": [ + "deserializeManifest argument", + "SerialIter sit{s}", + "STObject st{sit, sfGeneric}", + "st.applyTemplate(manifestFormat)", + "Field extraction from st" + ], + "origin": "deserializeManifest argument", + "transformations": [ + "Checked for empty", + "Parsed into STObject", + "Validated against manifestFormat template" + ], + "validated_at": "s.empty() at start, st.applyTemplate(manifestFormat)" + }, + { + "field": "sfPublicKey", + "flow": [ + "st.getFieldVL(sfPublicKey)", + "makeSlice(pk)", + "publicKeyType(makeSlice(pk))", + "PublicKey(makeSlice(pk))", + "Manifest(masterKey=PublicKey)" + ], + "origin": "Extracted from STObject st", + "transformations": [ + "Checked for presence/type by template", + "Type validated by publicKeyType", + "Converted to PublicKey" + ], + "validated_at": "st.applyTemplate(manifestFormat), publicKeyType(makeSlice(pk))" + }, + { + "field": "sfVersion", + "flow": [ + "st.isFieldPresent(sfVersion)", + "st.getFieldU16(sfVersion)", + "if (value != 0) return std::nullopt" + ], + "origin": "Optional field in manifest", + "transformations": [ + "Checked for presence", + "Value checked for being 0" + ], + "validated_at": "if (st.isFieldPresent(sfVersion) && st.getFieldU16(sfVersion) != 0)" + }, + { + "field": "sfDomain", + "flow": [ + "st.isFieldPresent(sfDomain)", + "st.getFieldVL(sfDomain)", + "reinterpret_cast to string", + "isProperlyFormedTomlDomain(domain)" + ], + "origin": "Optional field in manifest", + "transformations": [ + "Checked for presence", + "Converted to string", + "Format validated" + ], + "validated_at": "isProperlyFormedTomlDomain(domain)" + }, + { + "field": "sfSigningPubKey", + "flow": [ + "st.isFieldPresent(sfSigningPubKey)", + "st.getFieldVL(sfSigningPubKey)", + "makeSlice(spk)", + "publicKeyType(makeSlice(spk))", + "signingKey.emplace(makeSlice(spk))" + ], + "origin": "Optional field in manifest", + "transformations": [ + "Checked for presence", + "Type validated", + "Converted to PublicKey" + ], + "validated_at": "publicKeyType(makeSlice(spk))" + }, + { + "field": "sfSignature", + "flow": [ + "st.isFieldPresent(sfSignature)", + "Used for presence check (not further processed here)" + ], + "origin": "Optional field in manifest", + "transformations": [ + "Checked for presence" + ], + "validated_at": "Presence checked in logic for revoked/non-revoked manifests" + }, + { + "field": "sequence (sfSequence)", + "flow": [ + "st.getFieldU32(sfSequence)", + "Manifest::revoked(seq)", + "Manifest(sequence=seq)" + ], + "origin": "Required field in manifest", + "transformations": [ + "Checked for presence/type by template", + "Used to determine revoked status" + ], + "validated_at": "st.applyTemplate(manifestFormat)" + } + ], + "description": "Implements logic for handling, verifying, serializing, deserializing, and caching XRPL validator manifests, including revocation and key management.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfPublicKey", + "validation", + "missing", + "check" + ], + "evidence": "Field sfPublicKey validated by SOTemplate/STObject (XRPL serialization/validation framework)", + "issue_pattern": "Missing validation for sfPublicKey", + "why_false_positive": "SOTemplate/STObject (XRPL serialization/validation framework) validates sfPublicKey automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfMasterSignature", + "validation", + "missing", + "check" + ], + "evidence": "Field sfMasterSignature validated by SOTemplate/STObject (XRPL serialization/validation framework)", + "issue_pattern": "Missing validation for sfMasterSignature", + "why_false_positive": "SOTemplate/STObject (XRPL serialization/validation framework) validates sfMasterSignature automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSequence", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSequence validated by SOTemplate/STObject (XRPL serialization/validation framework)", + "issue_pattern": "Missing validation for sfSequence", + "why_false_positive": "SOTemplate/STObject (XRPL serialization/validation framework) validates sfSequence automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfVersion", + "validation", + "missing", + "check" + ], + "evidence": "Field sfVersion validated by SOTemplate/STObject (XRPL serialization/validation framework)", + "issue_pattern": "Missing validation for sfVersion", + "why_false_positive": "SOTemplate/STObject (XRPL serialization/validation framework) validates sfVersion automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDomain", + "validation", + "missing", + "check" + ], + "evidence": "Field sfDomain validated by SOTemplate/STObject (XRPL serialization/validation framework)", + "issue_pattern": "Missing validation for sfDomain", + "why_false_positive": "SOTemplate/STObject (XRPL serialization/validation framework) validates sfDomain automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSigningPubKey", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSigningPubKey validated by SOTemplate/STObject (XRPL serialization/validation framework)", + "issue_pattern": "Missing validation for sfSigningPubKey", + "why_false_positive": "SOTemplate/STObject (XRPL serialization/validation framework) validates sfSigningPubKey automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSignature", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSignature validated by SOTemplate/STObject (XRPL serialization/validation framework)", + "issue_pattern": "Missing validation for sfSignature", + "why_false_positive": "SOTemplate/STObject (XRPL serialization/validation framework) validates sfSignature automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "s (input Slice)", + "empty", + "string", + "validation" + ], + "evidence": "s.empty() check at deserializeManifest", + "issue_pattern": "Missing empty string validation for s (input Slice)", + "why_false_positive": "s.empty() check validates s (input Slice) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "manifest fields (sfPublicKey, sfMasterSignature, sfSequence, sfVersion, sfDomain, sfSigningPubKey, sfSignature)", + "empty", + "string", + "validation" + ], + "evidence": "SOTemplate manifestFormat + st.applyTemplate(manifestFormat) at deserializeManifest", + "issue_pattern": "Missing empty string validation for manifest fields (sfPublicKey, sfMasterSignature, sfSequence, sfVersion, sfDomain, sfSigningPubKey, sfSignature)", + "why_false_positive": "SOTemplate manifestFormat + st.applyTemplate(manifestFormat) validates manifest fields (sfPublicKey, sfMasterSignature, sfSequence, sfVersion, sfDomain, sfSigningPubKey, sfSignature) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVersion", + "empty", + "string", + "validation" + ], + "evidence": "st.isFieldPresent(sfVersion) && st.getFieldU16(sfVersion) != 0 at deserializeManifest", + "issue_pattern": "Missing empty string validation for sfVersion", + "why_false_positive": "st.isFieldPresent(sfVersion) && st.getFieldU16(sfVersion) != 0 validates sfVersion for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPublicKey", + "empty", + "string", + "validation" + ], + "evidence": "publicKeyType(makeSlice(pk)) at deserializeManifest", + "issue_pattern": "Missing empty string validation for sfPublicKey", + "why_false_positive": "publicKeyType(makeSlice(pk)) validates sfPublicKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomain", + "empty", + "string", + "validation" + ], + "evidence": "isProperlyFormedTomlDomain(domain) at deserializeManifest", + "issue_pattern": "Missing empty string validation for sfDomain", + "why_false_positive": "isProperlyFormedTomlDomain(domain) validates sfDomain for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfDomain", + "format", + "validation", + "invalid" + ], + "evidence": "isProperlyFormedTomlDomain(domain) at deserializeManifest", + "issue_pattern": "Missing format validation for sfDomain", + "why_false_positive": "isProperlyFormedTomlDomain(domain) validates sfDomain format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "revocation manifest logic (sfSequence, sfSigningPubKey, sfSignature)", + "empty", + "string", + "validation" + ], + "evidence": "Manifest::revoked(seq) + if (hasEphemeralKey) / if (hasEphemeralSig) at deserializeManifest", + "issue_pattern": "Missing empty string validation for revocation manifest logic (sfSequence, sfSigningPubKey, sfSignature)", + "why_false_positive": "Manifest::revoked(seq) + if (hasEphemeralKey) / if (hasEphemeralSig) validates revocation manifest logic (sfSequence, sfSigningPubKey, sfSignature) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m.signingKey", + "empty", + "string", + "validation" + ], + "evidence": "if (!m.signingKey) Throw at to_string", + "issue_pattern": "Missing empty string validation for m.signingKey", + "why_false_positive": "if (!m.signingKey) Throw validates m.signingKey for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/server/Manifest.cpp", + "functions": [ + { + "args": [ + "Manifest const& m" + ], + "lineno": 11, + "name": "to_string" + }, + { + "args": [ + "Slice s", + "beast::Journal journal" + ], + "lineno": 20, + "name": "deserializeManifest" + }, + { + "args": [ + "Stream& s", + "std::string const& action", + "PublicKey const& pk", + "std::uint32_t seq" + ], + "lineno": 87, + "name": "logMftAct" + }, + { + "args": [ + "Stream& s", + "std::string const& action", + "PublicKey const& pk", + "std::uint32_t seq", + "std::uint32_t oldSeq" + ], + "lineno": 95, + "name": "logMftAct" + }, + { + "args": [ + "this" + ], + "lineno": 103, + "name": "Manifest::verify" + }, + { + "args": [ + "this" + ], + "lineno": 116, + "name": "Manifest::hash" + }, + { + "args": [ + "this" + ], + "lineno": 126, + "name": "Manifest::revoked" + }, + { + "args": [ + "std::uint32_t sequence" + ], + "lineno": 133, + "name": "Manifest::revoked" + }, + { + "args": [ + "this" + ], + "lineno": 140, + "name": "Manifest::getSignature" + }, + { + "args": [ + "this" + ], + "lineno": 150, + "name": "Manifest::getMasterSignature" + }, + { + "args": [ + "std::vector const& blob", + "beast::Journal journal" + ], + "lineno": 157, + "name": "loadValidatorToken" + }, + { + "args": [ + "PublicKey const& pk" + ], + "lineno": 191, + "name": "ManifestCache::getSigningKey" + }, + { + "args": [ + "PublicKey const& pk" + ], + "lineno": 202, + "name": "ManifestCache::getMasterKey" + }, + { + "args": [ + "PublicKey const& pk" + ], + "lineno": 211, + "name": "ManifestCache::getSequence" + }, + { + "args": [ + "PublicKey const& pk" + ], + "lineno": 220, + "name": "ManifestCache::getDomain" + }, + { + "args": [ + "PublicKey const& pk" + ], + "lineno": 229, + "name": "ManifestCache::getManifest" + }, + { + "args": [ + "PublicKey const& pk" + ], + "lineno": 238, + "name": "ManifestCache::revoked" + }, + { + "args": [ + "Manifest m" + ], + "lineno": 247, + "name": "ManifestCache::applyManifest" + }, + { + "args": [ + "DatabaseCon& dbCon", + "std::string const& dbTable" + ], + "lineno": 370, + "name": "ManifestCache::load" + }, + { + "args": [ + "DatabaseCon& dbCon", + "std::string const& dbTable", + "std::string const& configManifest", + "std::vector const& configRevocation" + ], + "lineno": 376, + "name": "ManifestCache::load" + }, + { + "args": [ + "DatabaseCon& dbCon", + "std::string const& dbTable", + "std::function const& isTrusted" + ], + "lineno": 414, + "name": "ManifestCache::save" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + }, + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + }, + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + }, + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is in deserializeManifest. Typical test coverage would be in files like Manifest_test.cpp or Server_test.cpp, testing valid/invalid manifests, revoked manifests, missing/invalid fields, and domain validation. Gaps may exist if edge cases (e.g., malformed domains, invalid public key types, version != 0, revoked manifests with extra fields) are not explicitly tested. Exception handling paths (e.g., malformed input causing exceptions) should also be tested. If tests do not cover all combinations of optional/required fields and error cases, those are coverage gaps.", + "validation_architecture": { + "auto_validated_fields": [ + "sfPublicKey", + "sfMasterSignature", + "sfSequence", + "sfVersion", + "sfDomain", + "sfSigningPubKey", + "sfSignature" + ], + "framework": "SOTemplate/STObject (XRPL serialization/validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt", + "field": "s (input Slice)", + "location": "deserializeManifest", + "validated_by": "s.empty() check", + "validates": [ + "input is not empty" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "throws if required fields missing or types wrong (via STObject)", + "field": "manifest fields (sfPublicKey, sfMasterSignature, sfSequence, sfVersion, sfDomain, sfSigningPubKey, sfSignature)", + "location": "deserializeManifest", + "validated_by": "SOTemplate manifestFormat + st.applyTemplate(manifestFormat)", + "validates": [ + "sfPublicKey is present", + "sfMasterSignature is present", + "sfSequence is present", + "sfVersion defaults to 0 if not present", + "sfDomain, sfSigningPubKey, sfSignature are optional" + ], + "validation_type": "template/structural" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt", + "field": "sfVersion", + "location": "deserializeManifest", + "validated_by": "st.isFieldPresent(sfVersion) && st.getFieldU16(sfVersion) != 0", + "validates": [ + "version must be 0 if present" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt", + "field": "sfPublicKey", + "location": "deserializeManifest", + "validated_by": "publicKeyType(makeSlice(pk))", + "validates": [ + "public key is a recognized type" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt", + "field": "sfDomain", + "location": "deserializeManifest", + "validated_by": "isProperlyFormedTomlDomain(domain)", + "validates": [ + "domain is a properly formed TOML domain" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt", + "field": "revocation manifest logic (sfSequence, sfSigningPubKey, sfSignature)", + "location": "deserializeManifest", + "validated_by": "Manifest::revoked(seq) + if (hasEphemeralKey) / if (hasEphemeralSig)", + "validates": [ + "revocation manifests must not specify a new signing key", + "revocation manifests must not specify a signing key signature" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "m.signingKey", + "location": "to_string", + "validated_by": "if (!m.signingKey) Throw", + "validates": [ + "signingKey must be present for non-revoked manifests" + ], + "validation_type": "business_logic|presence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/server/Manifest.cpp.ai.md b/src/libxrpl/server/Manifest.cpp.ai.md new file mode 100644 index 0000000000..55c63bb84e --- /dev/null +++ b/src/libxrpl/server/Manifest.cpp.ai.md @@ -0,0 +1,41 @@ +# `src/libxrpl/server/Manifest.cpp` + +## Role in the System + +This file implements the XRPL validator key manifest system — a security indirection layer that decouples a validator's long-lived identity (the "master" key) from the ephemeral signing key used day-to-day. The design solves a critical operational problem: if an ephemeral signing key is compromised, the validator operator can issue a new manifest signed by the master key (kept offline) to revoke the old ephemeral key and install a replacement. All peers that receive this manifest immediately stop accepting validations from the old key. If the master key itself is compromised, a revocation manifest with the maximum sequence number (`0xFFFFFFFF`) permanently silences the validator without any further configuration changes being needed across the network. + +The file implements `struct Manifest` (the data type), `ManifestCache` (the runtime store), and several free functions for parsing, verifying, and persisting manifests. + +## Manifest Structure and Serialization + +A `Manifest` carries five fields: a raw serialized byte string (`serialized`), the permanent `masterKey`, an optional `signingKey`, a monotonically increasing `sequence`, and an optional `domain`. The `signingKey` is `std::optional` because revocation manifests intentionally omit it — a revoked manifest intentionally has no valid signing key on record, so no validations can be accepted from that validator. + +`deserializeManifest()` is the front door for all manifest parsing, whether from the network or from config. It declares a local `static SOTemplate` called `manifestFormat` that encodes the schema: `sfPublicKey`, `sfMasterSignature`, and `sfSequence` are required; `sfVersion` defaults to 0; and `sfDomain`, `sfSigningPubKey`, and `sfSignature` are optional. The `STObject::applyTemplate()` call enforces this schema structurally before any business logic runs — required fields missing or type mismatches throw immediately and are caught at the function boundary, returning `std::nullopt`. This prevents any downstream code from dealing with partially-constructed manifests. + +After structural parsing, `deserializeManifest` enforces XRPL-specific business rules in a deliberate order. Version forward-compatibility is checked first: only version 0 manifests are understood, and unknown versions are silently rejected rather than treated as errors. The master public key is then validated via `publicKeyType()` to confirm it is a recognized cryptographic key type. Domain strings, if present, are validated by `isProperlyFormedTomlDomain()` to ensure they are well-formed for TOML lookups. Finally, the revocation distinction is enforced: revocation manifests (those with `sequence == 0xFFFFFFFF`) must have neither an ephemeral key nor an ephemeral signature — any such fields would constitute a malformed revocation. Conversely, non-revocation manifests must have both, and the signing key must differ from the master key. + +Crucially, **`deserializeManifest` does not verify signatures**. This is documented intentionally in the header. Signature verification is more expensive and is deferred to `Manifest::verify()`, called explicitly after the caller has decided the manifest is worth validating. + +## Signature Verification + +`Manifest::verify()` re-deserializes from `this->serialized` each call rather than caching a parsed form. This is a deliberate memory tradeoff: storing a pre-parsed `STObject` would duplicate data that is already in `serialized`, and `verify()` is called infrequently. The method verifies two signatures. For non-revocation manifests, it first verifies the ephemeral key's signature over the manifest body using `HashPrefix::manifest` (via `xrpl::verify()`), which zeros out the signature field before hashing so the object signs its own content sans-signature. It then unconditionally verifies the master key's `sfMasterSignature` the same way. A revocation manifest skips the ephemeral signature check (since no ephemeral key exists) and only validates the master signature. + +The static and instance `revoked()` overloads are architecturally clean: the static form `Manifest::revoked(uint32_t sequence)` expresses the pure predicate, and the instance form delegates to it. This allows callers to check revocation during deserialization (before a `Manifest` object even exists) and in `ManifestCache` query methods. + +## ManifestCache: Thread-Safe Storage with Double-Check Locking + +`ManifestCache` maintains two parallel maps: `map_` (master `PublicKey` → `Manifest`) and `signingToMasterKeys_` (ephemeral `PublicKey` → master `PublicKey`). These must be kept consistent — when a manifest is updated, the old ephemeral key is erased from `signingToMasterKeys_` and the new one is inserted. The reverse map enables `getMasterKey()` to answer the common question "given this signing key, who is the validator?" without scanning the entire manifest collection. + +`applyManifest()` is the most architecturally interesting function. It uses a deliberate two-phase locking pattern rather than `std::shared_mutex` lock upgrades. The comment in the code explicitly warns against upgradeable locks as a "recipe for deadlock." Instead, the function runs a `prewriteCheck` lambda under a shared read lock first, then re-acquires an exclusive write lock and runs `prewriteCheck` again. The re-run skips the signature check (already validated under the read lock, since cryptographic verification is the expensive step) but repeats all the cheaper consistency checks in case another writer modified the maps between lock acquisitions. + +The validation in `prewriteCheck` guards against two categories of security concern beyond simple staleness and signature validity. First, it checks that the manifest's master key is not already in use as an ephemeral key for some other validator — this would indicate a key collision or an attack. Second, it checks that the proposed ephemeral key is not already registered as anyone's master or ephemeral key. Both checks return a distinct `ManifestDisposition` value (`badMasterKey` or `badEphemeralKey`) so the caller can distinguish these failure modes. + +The `seq_` field is an `std::atomic` incremented (without a lock) whenever `map_` changes. External code (such as overlay gossip) polls this value to detect whether new manifests have arrived since the last check, without needing to acquire the mutex for a trivial staleness test. + +## Validator Token Loading + +`loadValidatorToken()` handles the `[validator_token]` configuration entry. These tokens are multi-line, base64-encoded JSON blobs containing two fields: `manifest` (a base64 manifest string) and `validation_secret_key` (hex-encoded 32-byte raw secret). The function reassembles the multi-line input by trimming whitespace and concatenating, then decodes and JSON-parses it. The `SecretKey` is derived from the raw 32 bytes. The returned `ValidatorToken` pairs the manifest text with the secret key so the caller can both register the manifest in the cache and retain the key for signing validations. + +## Persistence + +`ManifestCache::save()` holds only a shared read lock while iterating the cache, delegating the actual SQL writes to `saveManifests()` from `xrpl/rdb`. It accepts a caller-provided predicate `isTrusted(PublicKey)` so that only manifests for currently-trusted validators are persisted — untrusted validators' manifests are not worth keeping across restarts. The two-argument `load()` simply calls `xrpl::getManifests()` to populate from the database. The four-argument `load()` additionally processes the `configManifest` and `configRevocation` entries from the node's configuration file, applying them through the same `applyManifest()` code path so that all validation rules are enforced uniformly regardless of where the manifest originated. \ No newline at end of file diff --git a/src/libxrpl/server/Port.cpp.ai.json b/src/libxrpl/server/Port.cpp.ai.json new file mode 100644 index 0000000000..dbc85eab86 --- /dev/null +++ b/src/libxrpl/server/Port.cpp.ai.json @@ -0,0 +1,332 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "parse_Port", + "populate" + ], + "entry_point": "parse_Port", + "purpose": "Parses a configuration Section to populate a Port object, including validation of IP addresses and networks.", + "validation_points": [ + "populate: beast::IP::Endpoint::from_string_checked (validates IP string)", + "populate: is_unspecified(*addr) (checks for wildcard IPs)", + "populate: boost::asio::ip::make_network_v4/make_network_v6 (validates and parses network notation)" + ] + }, + { + "call_chain": [ + "operator<<" + ], + "entry_point": "operator<<", + "purpose": "Serializes a Port object for logging or display, including its validated IP/networks.", + "validation_points": [] + }, + { + "call_chain": [ + "Port::secure" + ], + "entry_point": "Port::secure", + "purpose": "Checks if the Port uses a secure protocol.", + "validation_points": [] + }, + { + "call_chain": [ + "Port::protocols" + ], + "entry_point": "Port::protocols", + "purpose": "Returns a comma-separated string of protocols for the Port.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "ip (from Section field)", + "flow": [ + "Section.get(field)", + "populate (reads and splits by ',')", + "beast::IP::Endpoint::from_string_checked (validates/creates endpoint)", + "is_unspecified(*addr) (checks for wildcard)", + "boost::asio::ip::make_network_v4/make_network_v6 (parses/validates network)", + "nets4/nets6 vectors in Port" + ], + "origin": "Section configuration (e.g., config file)", + "transformations": [ + "Trimmed", + "Parsed as IP or network", + "Converted to network notation if single IP" + ], + "validated_at": "populate (see validation_points above)" + }, + { + "field": "protocol", + "flow": [ + "Section.get('protocol')", + "parse_Port", + "Port.protocol set" + ], + "origin": "Section configuration", + "transformations": [ + "Split into set of strings" + ], + "validated_at": "No explicit validation, but checked for known values in Port::secure" + }, + { + "field": "admin_nets_v4/admin_nets_v6", + "flow": [ + "Section.get('admin_networks')", + "populate", + "Port.admin_nets_v4/admin_nets_v6" + ], + "origin": "Section configuration (admin_networks field)", + "transformations": [ + "Parsed as comma-separated list", + "Validated as above" + ], + "validated_at": "populate" + }, + { + "field": "secure_gateway_nets_v4/secure_gateway_nets_v6", + "flow": [ + "Section.get('secure_gateway_networks')", + "populate", + "Port.secure_gateway_nets_v4/secure_gateway_nets_v6" + ], + "origin": "Section configuration (secure_gateway_networks field)", + "transformations": [ + "Parsed as comma-separated list", + "Validated as above" + ], + "validated_at": "populate" + } + ], + "description": "This file provides utility functions for parsing and representing network port configuration in the XRPL server, including parsing IP addresses, protocols, admin and secure gateway networks, and websocket options from configuration sections.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ip (from Section field)", + "empty", + "string", + "validation" + ], + "evidence": "beast::IP::Endpoint::from_string_checked at populate", + "issue_pattern": "Missing empty string validation for ip (from Section field)", + "why_false_positive": "beast::IP::Endpoint::from_string_checked validates ip (from Section field) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "ip (from Section field)", + "format", + "validation", + "invalid" + ], + "evidence": "beast::IP::Endpoint::from_string_checked at populate", + "issue_pattern": "Missing format validation for ip (from Section field)", + "why_false_positive": "beast::IP::Endpoint::from_string_checked validates ip (from Section field) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ip (from Section field)", + "empty", + "string", + "validation" + ], + "evidence": "is_unspecified(*addr) at populate", + "issue_pattern": "Missing empty string validation for ip (from Section field)", + "why_false_positive": "is_unspecified(*addr) validates ip (from Section field) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ip (from Section field)", + "empty", + "string", + "validation" + ], + "evidence": "boost::asio::ip::make_network_v4 / make_network_v6 at populate", + "issue_pattern": "Missing empty string validation for ip (from Section field)", + "why_false_positive": "boost::asio::ip::make_network_v4 / make_network_v6 validates ip (from Section field) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "ip (from Section field)", + "format", + "validation", + "invalid" + ], + "evidence": "boost::asio::ip::make_network_v4 / make_network_v6 at populate", + "issue_pattern": "Missing format validation for ip (from Section field)", + "why_false_positive": "boost::asio::ip::make_network_v4 / make_network_v6 validates ip (from Section field) format" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/server/Port.cpp", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "Port::secure" + }, + { + "args": [], + "lineno": 20, + "name": "Port::protocols" + }, + { + "args": [ + "os", + "p" + ], + "lineno": 27, + "name": "operator<<" + }, + { + "args": [ + "section", + "field", + "log", + "nets4", + "nets6" + ], + "lineno": 56, + "name": "populate" + }, + { + "args": [ + "port", + "section", + "log" + ], + "lineno": 120, + "name": "parse_Port" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage for this code is typically found in integration/configuration tests, such as those in 'src/test/server/Port_test.cpp' or similar files. These tests likely cover valid and invalid IP/network configurations, wildcard handling, and protocol parsing. However, edge cases such as malformed IPs, invalid network masks, or duplicate/wildcard entries may not be exhaustively tested. There may be limited unit tests for exception handling paths (e.g., what happens if make_network_v4 throws). Manual review of test files is needed to confirm coverage of all validation branches.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "beast::IP, boost::asio::ip", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns std::optional, skips invalid)", + "field": "ip (from Section field)", + "location": "populate", + "validated_by": "beast::IP::Endpoint::from_string_checked", + "validates": [ + "Checks if the IP string is a valid IPv4 or IPv6 address" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "ip (from Section field)", + "location": "populate", + "validated_by": "is_unspecified(*addr)", + "validates": [ + "Checks if the IP address is unspecified (0.0.0.0 or ::)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "boost::system::system_error (on invalid network format)", + "field": "ip (from Section field)", + "location": "populate", + "validated_by": "boost::asio::ip::make_network_v4 / make_network_v6", + "validates": [ + "Checks if the IP string is a valid IPv4 or IPv6 network (CIDR notation)" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/server/Port.cpp.ai.md b/src/libxrpl/server/Port.cpp.ai.md new file mode 100644 index 0000000000..fe02d107d6 --- /dev/null +++ b/src/libxrpl/server/Port.cpp.ai.md @@ -0,0 +1,42 @@ +# `src/libxrpl/server/Port.cpp` + +## Role in the System + +This file implements the runtime logic for parsing and representing network port configuration for the XRPL server. It translates raw configuration text (loaded from `rippled.cfg`) into structured `Port` objects that govern how the server binds, which protocols it speaks, and which IP ranges are granted elevated access. The file is the sole implementation site for `parse_Port()`, the entry point called by `ServerHandler.cpp` during server startup. + +## Two-Struct Design: `ParsedPort` vs. `Port` + +The header defines two distinct structs. `ParsedPort` wraps `ip` and `port` as `std::optional<>`, while `Port` holds them as concrete values. This asymmetry is deliberate. The `ServerHandler.cpp` calling code first parses the top-level `[server]` section into a `ParsedPort common`, then iterates over each named port section, copies `common` into a fresh `ParsedPort`, and calls `parse_Port()` again to apply per-port overrides. Keeping IP and port optional allows the common section to supply defaults that individual port sections can leave absent without raising a parse error. Once all parsing completes, a `to_Port()` function in `ServerHandler.cpp` converts the `ParsedPort` into the final concrete `Port` used at runtime. + +## `parse_Port()`: Field-by-Field Validation + +`parse_Port()` works by sequentially extracting fields from the `Section` abstraction and validating each one before assigning it. The pattern throughout is: read with `section.get()`, validate inside a `try/catch`, log a human-readable error message referencing the field name and section, and call `Rethrow()` or `Throw()` to propagate failures. This pattern means a misconfigured port causes a clean error at startup rather than a silent default or undefined behaviour later. + +Notable validation rules baked in here: +- **Port 0 is forbidden for `[server]`**: the root server section uses port 0 as a sentinel for "not configured," so the code explicitly rejects it as a literal value. +- **`send_queue_limit` of 0 is rejected**: a WebSocket queue with zero capacity would immediately disconnect every client; the parser treats it as an error. The default of 100 is set here if the key is absent. +- **`limit`**: connection limits accept the string `"unlimited"` (case-insensitive via `boost::iequals`) and map it to the integer 0; any other value must be a valid `uint16_t`. + +Protocol names are split by RFC 2616 comma rules and inserted into a case-insensitive `std::set`. `Port::secure()` then checks this set for `"peer"`, `"https"`, `"wss"`, or `"wss2"` — all protocols that require TLS — without needing to worry about case normalisation. + +The permessage-deflate (WebSocket per-message compression) options are read with `value_or()` throughout, so all seven PMD parameters have sensible defaults (compression enabled, 15-bit window sizes, level 8, memory level 4) that only need to appear in config when overriding. + +## `populate()`: IP and Subnet Parsing + +The most complex logic in the file lives in the file-static `populate()` helper, called twice by `parse_Port()` — once for the `admin` field and once for `secure_gateway`. Both fields accept a comma-separated list of IPv4 addresses, IPv6 addresses, or CIDR subnets, and the helper parses each entry uniformly into dual `network_v4`/`network_v6` vectors. + +The parsing uses a deliberate two-pass approach. It first calls `beast::IP::Endpoint::from_string_checked()`, which returns an `optional` rather than throwing. If the result is valid, the input was a bare IP address (no prefix length). In that case: +- A **wildcard address** (`0.0.0.0` or `::`) triggers immediate expansion to `0.0.0.0/0` and `::/0` in both vectors, then breaks out of the loop entirely — there is no point processing further entries since every address is already covered. +- A **single concrete IP** is promoted to a host-route network by appending `/32` (IPv4) or `/128` (IPv6) before constructing the CIDR object. This unifies the data model so callers only ever deal with `network_v4`/`network_v6` objects, never raw addresses. + +If `from_string_checked()` returns empty, the input is assumed to be in CIDR subnet notation. The code tries `make_network_v4()` first; if that throws `boost::system::system_error`, it falls back to `make_network_v6()`. If that also throws, the outer catch block logs the entry and re-throws. + +A subtle but important correctness check follows subnet parsing: the constructed network is compared against its own `canonical()` form. `10.1.2.3/24` has a canonical form of `10.1.2.0/24` — the host bits must be zero for a network address. If the configured value is non-canonical, `populate()` logs a descriptive message identifying both the configured form and the correct canonical form, then throws. This prevents admins from accidentally granting access to a broader subnet than intended due to a typo in the host portion. + +## Access Tiers: `admin` and `secure_gateway` + +The two networks parsed by `populate()` serve distinct roles in the XRPL server's request-routing logic. Networks listed in `admin` grant callers full administrative API access, subject to IP-level verification at connection time. Networks listed in `secure_gateway` identify trusted proxy or gateway nodes whose HTTP headers (`X-Forwarded-For`, etc.) can be used for further identity assertions — this is how XRPL deployments behind load balancers can still propagate client identity to the node. + +## `operator<<` for Logging + +The stream operator outputs the port name, bound IP and port number, then iterates the admin and secure_gateway network vectors (both IPv4 and IPv6), and finally the protocol list via `protocols()`. The format is intentionally human-readable for startup logs and debugging; it does not attempt to reconstruct config syntax. \ No newline at end of file diff --git a/src/libxrpl/server/State.cpp.ai.json b/src/libxrpl/server/State.cpp.ai.json new file mode 100644 index 0000000000..cda5a4ce14 --- /dev/null +++ b/src/libxrpl/server/State.cpp.ai.json @@ -0,0 +1,246 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "initStateDB" + ], + "entry_point": "initStateDB", + "purpose": "Initializes the DbState and CanDelete tables if they do not exist, and ensures a row with Key=1 exists in each.", + "validation_points": [ + "Manual SQL query: SELECT COUNT(Key) FROM DbState WHERE Key = 1; (checks if row exists)", + "Manual SQL query: SELECT COUNT(Key) FROM CanDelete WHERE Key = 1; (checks if row exists)", + "Validation: If boost::optional is empty, throws runtime_error" + ] + }, + { + "call_chain": [ + "getCanDelete" + ], + "entry_point": "getCanDelete", + "purpose": "Fetches the CanDeleteSeq value from the CanDelete table.", + "validation_points": [] + }, + { + "call_chain": [ + "setCanDelete" + ], + "entry_point": "setCanDelete", + "purpose": "Updates the CanDeleteSeq value in the CanDelete table.", + "validation_points": [] + }, + { + "call_chain": [ + "getSavedState" + ], + "entry_point": "getSavedState", + "purpose": "Fetches WritableDb, ArchiveDb, and LastRotatedLedger from DbState.", + "validation_points": [] + }, + { + "call_chain": [ + "setSavedState" + ], + "entry_point": "setSavedState", + "purpose": "Updates WritableDb, ArchiveDb, and LastRotatedLedger in DbState.", + "validation_points": [] + }, + { + "call_chain": [ + "setLastRotated" + ], + "entry_point": "setLastRotated", + "purpose": "Updates LastRotatedLedger in DbState.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "DbState.Key", + "flow": [ + "initStateDB: SELECT COUNT(Key) FROM DbState WHERE Key = 1;", + "If count == 0, INSERT INTO DbState VALUES (1, '', '', 0);" + ], + "origin": "Database table DbState, column Key", + "transformations": [ + "Validation: Checks if row exists, inserts if not" + ], + "validated_at": "initStateDB (manual SQL + boost::optional check)" + }, + { + "field": "CanDelete.Key", + "flow": [ + "initStateDB: SELECT COUNT(Key) FROM CanDelete WHERE Key = 1;", + "If count == 0, INSERT INTO CanDelete VALUES (1, 0);" + ], + "origin": "Database table CanDelete, column Key", + "transformations": [ + "Validation: Checks if row exists, inserts if not" + ], + "validated_at": "initStateDB (manual SQL + boost::optional check)" + }, + { + "field": "CanDeleteSeq", + "flow": [ + "getCanDelete: SELECT CanDeleteSeq FROM CanDelete WHERE Key = 1;", + "setCanDelete: UPDATE CanDelete SET CanDeleteSeq = :canDelete WHERE Key = 1;" + ], + "origin": "Database table CanDelete, column CanDeleteSeq", + "transformations": [ + "Read and write via SQL" + ], + "validated_at": "No explicit validation beyond DB constraints" + }, + { + "field": "WritableDb, ArchiveDb, LastRotatedLedger", + "flow": [ + "getSavedState: SELECT ... FROM DbState WHERE Key = 1;", + "setSavedState: UPDATE DbState SET ... WHERE Key = 1;", + "setLastRotated: UPDATE DbState SET LastRotatedLedger = :seq WHERE Key = 1;" + ], + "origin": "Database table DbState, columns WritableDb, ArchiveDb, LastRotatedLedger", + "transformations": [ + "Read and write via SQL" + ], + "validated_at": "No explicit validation beyond DB constraints" + } + ], + "description": "This file provides functions to initialize and manage state-related tables in a database for the XRPL server, including functions to get/set deletion markers and saved state information.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Key count in DbState", + "empty", + "string", + "validation" + ], + "evidence": "manual SQL query + boost::optional at initStateDB", + "issue_pattern": "Missing empty string validation for Key count in DbState", + "why_false_positive": "manual SQL query + boost::optional validates Key count in DbState for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Key count in CanDelete", + "empty", + "string", + "validation" + ], + "evidence": "manual SQL query + boost::optional at initStateDB", + "issue_pattern": "Missing empty string validation for Key count in CanDelete", + "why_false_positive": "manual SQL query + boost::optional validates Key count in CanDelete for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/server/State.cpp", + "functions": [ + { + "args": [ + "session", + "config", + "dbName" + ], + "lineno": 5, + "name": "initStateDB" + }, + { + "args": [ + "session" + ], + "lineno": 44, + "name": "getCanDelete" + }, + { + "args": [ + "session", + "canDelete" + ], + "lineno": 51, + "name": "setCanDelete" + }, + { + "args": [ + "session" + ], + "lineno": 58, + "name": "getSavedState" + }, + { + "args": [ + "session", + "state" + ], + "lineno": 67, + "name": "setSavedState" + }, + { + "args": [ + "session", + "seq" + ], + "lineno": 76, + "name": "setLastRotated" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is low-level and likely tested indirectly via higher-level integration or database state tests. There is no evidence of direct unit tests for these functions in this file. Validation logic (row existence) is only checked in initStateDB; other functions assume the row exists. Gaps: No tests for error handling (e.g., missing rows after init), no tests for SQL injection or malformed input, no tests for exception paths. Test files that might cover this: database initialization/integration tests, possibly in test suites for ledger or state management (e.g., LedgerCleaner, Database tests in rippled).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation using boost::optional and SQL queries)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "Key count in DbState", + "location": "initStateDB", + "validated_by": "manual SQL query + boost::optional", + "validates": [ + "Checks if SELECT COUNT(Key) FROM DbState WHERE Key = 1 returns a value", + "Ensures the query result is not null (boost::optional is set)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "Key count in CanDelete", + "location": "initStateDB", + "validated_by": "manual SQL query + boost::optional", + "validates": [ + "Checks if SELECT COUNT(Key) FROM CanDelete WHERE Key = 1 returns a value", + "Ensures the query result is not null (boost::optional is set)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/server/State.cpp.ai.md b/src/libxrpl/server/State.cpp.ai.md new file mode 100644 index 0000000000..ac93469137 --- /dev/null +++ b/src/libxrpl/server/State.cpp.ai.md @@ -0,0 +1,31 @@ +# `src/libxrpl/server/State.cpp` — Online-Delete State Persistence + +This file implements the low-level SQLite persistence layer for the XRPL node's **online-delete** (ledger rotation) subsystem. When a rippled node is configured with online deletion enabled, it must survive restarts without losing track of which backing stores are active and how far deletion has already progressed. `State.cpp` encapsulates exactly that bookkeeping: two minimal tables, six thin SQL-wrapper functions, and carefully validated initialization logic. + +## The Two Tables + +`initStateDB` creates and seeds two tables in an SQLite database whose path and name come from the node configuration: + +**`DbState`** tracks which of the two rotating node-store shards is currently writable, which is the archive (read-only), and the ledger sequence number at which the last rotation completed (`LastRotatedLedger`). The pair of shard path names (`WritableDb`, `ArchiveDb`) are filenames resolved at runtime; on a rotation, the old writable shard becomes the archive and a fresh shard becomes writable. Persisting these names means the node can reattach to the correct backends after a crash or a planned restart. + +**`CanDelete`** stores a single ledger sequence — the high-water mark below which the deletion thread is permitted to discard ledger data. This is the persistence side of rippled's "advisory delete" feature: an operator or the `can_delete` RPC command (`CanDelete.cpp`) can advance this threshold, and the value must survive process restarts. + +Both tables follow a **singleton-row pattern**: every SELECT and UPDATE targets `WHERE Key = 1`, and `initStateDB` inserts that row with blank/zero defaults if it doesn't already exist. This simplicity is intentional — the tables have no business being multi-row; they are effectively two named fields that outlive the process. + +## Initialization and Failure Handling + +The most structurally interesting part of `initStateDB` is how it verifies that the seed row was written. Rather than relying on `INSERT`'s side effects, it first counts the existing row with `SELECT COUNT(Key) FROM DbState WHERE Key = 1` and, critically, captures the result via `boost::optional` rather than `std::optional`. A comment in the code explains this directly: SOCI's bind-into mechanism requires `boost::optional`, not the C++17 standard variant. If SOCI returns a null indicator (which should not happen for `COUNT(*)` but is theoretically possible if the session is in a bad state), the optional is empty and `Throw` aborts startup immediately. This pattern runs twice — once for `DbState`, once for `CanDelete` — before either `INSERT` is attempted. + +`PRAGMA synchronous=FULL` is applied during init. This is the strictest SQLite durability setting, forcing full fsync on every write. For a data file this small and this infrequently written, the performance cost is negligible; the benefit is that a power loss cannot corrupt the rotation bookmarks. Losing track of which shard is writable would corrupt the node store. + +## The API Surface + +The six free functions in the `xrpl` namespace are deliberately stateless: each one accepts a `soci::session&` and does exactly one SQL operation. There is no caching, no in-memory shadow, and no object lifetime to manage. This is the right trade-off because the sole consumer is `SHAMapStoreImp::SavedStateDB` (in `SHAMapStoreImp.h` / `SHAMapStoreImp.cpp`), which wraps every call site with a `std::mutex`. Thread safety is the caller's responsibility; the functions here are intentionally unaware of it. + +`setCanDelete` returns the value that was just stored — the same value passed in. The return is present to match the shape of the `SHAMapStore` public interface and to allow call sites to confirm what was persisted, but no caller currently inspects it. `setLastRotated` is a narrower sibling of `setSavedState`: it updates only `LastRotatedLedger` without touching the shard names, which is useful during a mid-rotation progress update where the names have not yet changed. + +## Role in the Broader System + +`SHAMapStoreImp::SavedStateDB` is the only direct user of these functions. It is a private inner class of `SHAMapStoreImp`, the component that manages the rotating dual-shard NodeStore. On startup, `SavedStateDB::init` calls `initStateDB` to open the database and ensure the schema exists. As the rotation thread advances, it calls `setState` (which calls `setSavedState`) to atomically record both the new shard names and the rotated ledger index, or `setLastRotated` to checkpoint progress within a rotation. The `can_delete` RPC handler (`CanDelete.cpp`) reaches `setCanDelete` through `SHAMapStore`'s public interface, giving operators runtime control over the deletion threshold without restarting the node. + +The `SavedState` struct — three fields, defined in `State.h` — is the only data type exchanged between this layer and its callers. Its simplicity reflects the limited scope of what needs to survive a restart: two path strings and one integer. Everything else (in-memory caches, rotation scheduling, health checks) lives in `SHAMapStoreImp` itself and is rebuilt from scratch on each startup using these three persisted values as the starting point. \ No newline at end of file diff --git a/src/libxrpl/server/Vacuum.cpp.ai.json b/src/libxrpl/server/Vacuum.cpp.ai.json new file mode 100644 index 0000000000..bcecd5c73c --- /dev/null +++ b/src/libxrpl/server/Vacuum.cpp.ai.json @@ -0,0 +1,188 @@ +{ + "args": [ + { + "lineno": 9, + "name": "setup" + }, + { + "lineno": 9, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doVacuumDB" + ], + "entry_point": "doVacuumDB", + "purpose": "Performs a SQLite VACUUM operation on the transaction database, ensuring sufficient disk space and valid configuration before proceeding.", + "validation_points": [ + "XRPL_ASSERT(dbSize != static_cast(-1), ...)", + "if (available < dbSize) { ... return false; }", + "XRPL_ASSERT(setup.globalPragma, ...)" + ] + } + ], + "data_flows": [ + { + "field": "dbSize", + "flow": [ + "file_size(dbPath)", + "dbSize variable", + "XRPL_ASSERT(dbSize != -1)", + "used in disk space check (available < dbSize)", + "used in error message if insufficient space" + ], + "origin": "file_size(dbPath)", + "transformations": [ + "Read from filesystem as uintmax_t" + ], + "validated_at": "XRPL_ASSERT(dbSize != static_cast(-1), ...)" + }, + { + "field": "available", + "flow": [ + "space(dbPath.parent_path()).available", + "compared to dbSize in if (available < dbSize)", + "used in error message if insufficient" + ], + "origin": "space(dbPath.parent_path()).available", + "transformations": [ + "Read from filesystem as uintmax_t" + ], + "validated_at": "if (available < dbSize) { ... return false; }" + }, + { + "field": "setup.globalPragma", + "flow": [ + "setup.globalPragma", + "XRPL_ASSERT(setup.globalPragma, ...)", + "dereferenced and iterated: for (auto const& p : *setup.globalPragma) session << p" + ], + "origin": "setup parameter (DatabaseCon::Setup)", + "transformations": [ + "Checked for non-null", + "Iterated and executed as SQL statements" + ], + "validated_at": "XRPL_ASSERT(setup.globalPragma, ...)" + } + ], + "description": "Provides a function to perform a VACUUM operation on the XRPL transaction database, ensuring sufficient disk space and reporting progress.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "dbSize (database file size)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at doVacuumDB", + "issue_pattern": "Missing empty string validation for dbSize (database file size)", + "why_false_positive": "XRPL_ASSERT validates dbSize (database file size) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "available disk space", + "empty", + "string", + "validation" + ], + "evidence": "explicit if statement at doVacuumDB", + "issue_pattern": "Missing empty string validation for available disk space", + "why_false_positive": "explicit if statement validates available disk space for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "setup.globalPragma", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at doVacuumDB", + "issue_pattern": "Missing empty string validation for setup.globalPragma", + "why_false_positive": "XRPL_ASSERT validates setup.globalPragma for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/server/Vacuum.cpp", + "functions": [ + { + "args": [ + "setup", + "j" + ], + "lineno": 7, + "name": "doVacuumDB" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage. Typically, doVacuumDB would be tested via integration or system tests that exercise database maintenance operations. Tests would need to cover: (1) insufficient disk space, (2) missing or invalid globalPragma, (3) file size errors, and (4) successful vacuum. Gaps likely exist in simulating disk space errors and file system failures, as these are hard to mock without specialized test harnesses. Test files might be found in workflow/XRPLF-rippled-develop/test or similar, but are not referenced here.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), explicit C++ logic", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or logs error)", + "field": "dbSize (database file size)", + "location": "doVacuumDB", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that file_size(dbPath) does not return (uintmax_t)-1, which would indicate an error in retrieving the file size" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prints error to std::cerr)", + "field": "available disk space", + "location": "doVacuumDB", + "validated_by": "explicit if statement", + "validates": [ + "Checks that available disk space in the database directory is at least as large as the database file size" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or logs error)", + "field": "setup.globalPragma", + "location": "doVacuumDB", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that setup.globalPragma is not null before dereferencing" + ], + "validation_type": "type/null check" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/server/Vacuum.cpp.ai.md b/src/libxrpl/server/Vacuum.cpp.ai.md new file mode 100644 index 0000000000..c7d839e46f --- /dev/null +++ b/src/libxrpl/server/Vacuum.cpp.ai.md @@ -0,0 +1,29 @@ +# `src/libxrpl/server/Vacuum.cpp` + +## Role in the System + +This file implements `doVacuumDB()`, a maintenance utility that performs a SQLite `VACUUM` operation on the XRPL transaction database (`transaction.db`). It exists as a purpose-built administrative path invoked exclusively from `Main.cpp` when the operator runs `rippled --vacuum` — never during normal server operation. The placement under `libxrpl/server/` rather than `libxrpl/rdb/` reflects its nature: it is a node administration concern, not a general database-layer concern. + +## What SQLite VACUUM Requires + +SQLite's `VACUUM` command rebuilds the entire database file from scratch into a new file, then replaces the original. This defragments storage, reclaims free pages left behind by deletions, and compacts the file. Because it writes a complete second copy of the database before swapping, it requires free disk space roughly equal to the current database size. The transaction database on production XRPL nodes can grow very large, making this precondition non-trivial. The function enforces it explicitly. + +## The `doVacuumDB()` Function + +The function's logic falls into three phases: pre-flight checks, the VACUUM itself, and post-VACUUM configuration restoration. + +**Pre-flight.** The function builds the path to `transaction.db` from `setup.dataDir` and queries its size via `boost::filesystem::file_size`. An `XRPL_ASSERT` confirms the call succeeded (a return value of `(uintmax_t)-1` signals failure in the Boost filesystem API). It then queries `boost::filesystem::space` on the parent directory. If available space is less than the database size, it prints a diagnostic to `std::cerr` and returns `false` — a graceful, user-facing failure rather than an assert, because this is an operator-correctable condition rather than a programming error. + +**Opening the database and forcing disk-backed temp storage.** The function constructs a `DatabaseCon` for `transaction.db` using `TxDBName`, `setup.txPragma`, and `TxDBInit` — the same pragmas and DDL schema used during normal node startup — and obtains a SOCI session. Before issuing `VACUUM`, it unconditionally forces `PRAGMA temp_store=file`. The comment explains why: SQLite's VACUUM generates substantial temporary data, and the typical hardware recommendation for XRPL nodes means this data will not fit in memory. Regardless of what the operator configured for `temp_store` in `rippled.cfg`, this path overrides it. Using in-memory temp storage during VACUUM on a multi-gigabyte database would risk OOM. + +**VACUUM and pragma restoration.** After logging the pre-VACUUM page size, the function issues `VACUUM;` and then re-applies every pragma in `setup.globalPragma`. This restoration step is necessary because SQLite resets certain pragmas when a VACUUM rebuilds the database file — notably journal mode and synchronous settings. Without re-applying them, the connection would operate with SQLite defaults rather than the node's configured settings. An `XRPL_ASSERT` guards the dereference of `setup.globalPragma`, which is expected to be non-null when `doVacuumDB` is called (the assert would fire in debug builds if `globalPragma` was never populated). The function then queries and logs the post-VACUUM page size as confirmation. + +## Error Handling Design + +Two distinct error handling strategies are used, reflecting two distinct failure categories. `XRPL_ASSERT` handles conditions that indicate a logic or environment error — a filesystem call returning an error sentinel, or a null `globalPragma` pointer — both of which should not occur in a correctly configured system. The `return false` path handles an operator-correctable condition (insufficient disk space) where a clean failure with a descriptive error message is more useful than an abort. Exceptions from `DatabaseCon` construction or SOCI operations propagate to the caller in `Main.cpp`, which catches `std::exception` and prints its message before returning `-1`. + +## Relationship to Surrounding Infrastructure + +`DatabaseCon::Setup` (from `xrpl/rdb/DatabaseCon.h`) carries `dataDir`, `txPragma`, and `globalPragma` — the shared, node-wide pragma strings built once from `rippled.cfg` during startup and stored as a `static std::unique_ptr const>`. `TxDBName`, `TxDBInit`, and `CommonDBPragmaTemp` come from `xrpl/rdb/DBInit.h`, which defines the schema and tuning constants for all three XRPL SQLite databases. + +The caller in `Main.cpp` additionally rejects the `--vacuum` flag if the node is in standalone mode, since standalone mode does not use the transaction database in the same way and vacuuming it makes no operational sense. This guard lives in the caller rather than `doVacuumDB` itself, keeping the function's scope strictly to the filesystem and database mechanics. \ No newline at end of file diff --git a/src/libxrpl/server/Wallet.cpp.ai.json b/src/libxrpl/server/Wallet.cpp.ai.json new file mode 100644 index 0000000000..0f84a9cb49 --- /dev/null +++ b/src/libxrpl/server/Wallet.cpp.ai.json @@ -0,0 +1,344 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "getManifests", + "deserializeManifest", + "Manifest::verify", + "mCache.applyManifest" + ], + "entry_point": "getManifests", + "purpose": "Loads manifests from the database, validates them, and applies them to the manifest cache.", + "validation_points": [ + "deserializeManifest (checks manifest structure/format)", + "Manifest::verify (cryptographic signature validation)" + ] + }, + { + "call_chain": [ + "saveManifests", + "isTrusted", + "Manifest::revoked", + "saveManifest" + ], + "entry_point": "saveManifests", + "purpose": "Saves manifests to the database, but only if they are trusted or revoked.", + "validation_points": [ + "isTrusted (checks if manifest is from a trusted validator)", + "Manifest::revoked (checks if manifest is a revocation)" + ] + }, + { + "call_chain": [ + "addValidatorManifest", + "saveManifest" + ], + "entry_point": "addValidatorManifest", + "purpose": "Adds a validator manifest to the ValidatorManifests table.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "manifest (serialized)", + "flow": [ + "Database", + "getManifests (loads as soci::blob)", + "convert (blob to std::string)", + "deserializeManifest (parses string to Manifest object)", + "Manifest::verify (validates signature)", + "mCache.applyManifest (stores in cache)" + ], + "origin": "Database (RawData column in manifest table)", + "transformations": [ + "Deserialization from blob to string", + "Parsing string to Manifest object" + ], + "validated_at": "deserializeManifest, Manifest::verify" + }, + { + "field": "manifest (trusted status)", + "flow": [ + "ManifestCache", + "saveManifests (iterates map)", + "isTrusted (checks trust status)", + "Manifest::revoked (checks revocation)", + "saveManifest (writes to DB if trusted or revoked)" + ], + "origin": "ManifestCache (in-memory map of manifests)", + "transformations": [ + "Trust check", + "Revocation check" + ], + "validated_at": "isTrusted, Manifest::revoked" + }, + { + "field": "NodeIdentity (PublicKey, PrivateKey)", + "flow": [ + "Database", + "getNodeIdentity (loads keys as strings)", + "parseBase58 (decodes base58 to key objects)", + "derivePublicKey (verifies key pair)", + "return to caller" + ], + "origin": "Database (NodeIdentity table)", + "transformations": [ + "Base58 decoding", + "Key pair verification" + ], + "validated_at": "derivePublicKey (checks key pair match)" + } + ], + "description": "This file provides utility functions for managing the Wallet database in the XRPL server, including creation, reading, writing, and manipulation of manifests, node identities, peer reservations, and feature votes using SOCI for database access.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "manifest (serialized manifest data from DB)", + "empty", + "string", + "validation" + ], + "evidence": "deserializeManifest() and Manifest::verify() at getManifests", + "issue_pattern": "Missing empty string validation for manifest (serialized manifest data from DB)", + "why_false_positive": "deserializeManifest() and Manifest::verify() validates manifest (serialized manifest data from DB) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "manifest (trusted status and revocation)", + "empty", + "string", + "validation" + ], + "evidence": "isTrusted() and Manifest::revoked() at saveManifests", + "issue_pattern": "Missing empty string validation for manifest (trusted status and revocation)", + "why_false_positive": "isTrusted() and Manifest::revoked() validates manifest (trusted status and revocation) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/server/Wallet.cpp", + "functions": [ + { + "args": [ + "setup", + "j" + ], + "lineno": 7, + "name": "makeWalletDB" + }, + { + "args": [ + "setup", + "dbname", + "j" + ], + "lineno": 13, + "name": "makeTestWalletDB" + }, + { + "args": [ + "session", + "dbTable", + "mCache", + "j" + ], + "lineno": 20, + "name": "getManifests" + }, + { + "args": [ + "session", + "dbTable", + "serialized" + ], + "lineno": 46, + "name": "saveManifest" + }, + { + "args": [ + "session", + "dbTable", + "isTrusted", + "map", + "j" + ], + "lineno": 56, + "name": "saveManifests" + }, + { + "args": [ + "session", + "serialized" + ], + "lineno": 75, + "name": "addValidatorManifest" + }, + { + "args": [ + "session" + ], + "lineno": 81, + "name": "clearNodeIdentity" + }, + { + "args": [ + "session" + ], + "lineno": 85, + "name": "getNodeIdentity" + }, + { + "args": [ + "session", + "j" + ], + "lineno": 110, + "name": "getPeerReservationTable" + }, + { + "args": [ + "session", + "nodeId", + "description" + ], + "lineno": 137, + "name": "insertPeerReservation" + }, + { + "args": [ + "session", + "nodeId" + ], + "lineno": 147, + "name": "deletePeerReservation" + }, + { + "args": [ + "session" + ], + "lineno": 154, + "name": "createFeatureVotes" + }, + { + "args": [ + "session", + "callback" + ], + "lineno": 171, + "name": "readAmendments" + }, + { + "args": [ + "session", + "amendment", + "name", + "vote" + ], + "lineno": 200, + "name": "voteAmendment" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code in Wallet.cpp is typically tested via integration and unit tests in the rippled codebase, especially in test files related to manifest handling, validator management, and wallet DB operations. Likely test files include Manifest_test.cpp, ManifestCache_test.cpp, and possibly integration tests for validator key management. However, direct tests for database error handling, malformed manifest data, and edge cases (e.g., untrusted manifests, revoked manifests) may be limited or absent. The cryptographic validation (Manifest::verify) is likely tested elsewhere, but the full DB-to-cache-to-validation path may not be exhaustively covered.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (logs warning, skips invalid entry)", + "field": "manifest (serialized manifest data from DB)", + "location": "getManifests", + "validated_by": "deserializeManifest() and Manifest::verify()", + "validates": [ + "Checks if manifest data can be deserialized (format validation)", + "Checks if deserialized manifest is cryptographically valid (business logic: verify signature)" + ], + "validation_type": "format, business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (logs info, skips untrusted non-revocation manifests)", + "field": "manifest (trusted status and revocation)", + "location": "saveManifests", + "validated_by": "isTrusted() and Manifest::revoked()", + "validates": [ + "Checks if manifest is revoked (business logic)", + "Checks if manifest is trusted (business logic)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/server/Wallet.cpp.ai.md b/src/libxrpl/server/Wallet.cpp.ai.md new file mode 100644 index 0000000000..e10ce74b91 --- /dev/null +++ b/src/libxrpl/server/Wallet.cpp.ai.md @@ -0,0 +1,43 @@ +# `src/libxrpl/server/Wallet.cpp` + +## Role in the System + +`Wallet.cpp` is the persistence layer for a running rippled node's local identity and configuration state. It implements all read and write operations against `wallet.db`, a SQLite database that survives node restarts and holds four categories of data: the node's stable cryptographic identity (its secp256k1 keypair), whitelisted peer reservations, validator and publisher key-rotation manifests, and per-node amendment vote preferences. The file is thin by design — it is purely a data-access layer, deferring all business logic and in-memory caching to higher-level abstractions like `ManifestCache`. + +The public surface of this file is declared in `include/xrpl/server/Wallet.h`. The schema it operates against is defined as compile-time string arrays in `include/xrpl/rdb/DBInit.h`: `WalletDBInit` initializes five tables (`NodeIdentity`, `PeerReservations`, `ValidatorManifests`, `PublisherManifests`, and optionally `FeatureVotes`), and `WalletDBName` provides the fixed filename `wallet.db`. + +## Database Construction + +`makeWalletDB()` and `makeTestWalletDB()` are factory functions that construct a `DatabaseCon` — the XRPL wrapper around a SOCI/SQLite connection — and apply the `WalletDBInit` DDL on first open. The test variant accepts an arbitrary `dbname`, allowing unit tests to use isolated databases without collision. Both return `std::unique_ptr`, transferring ownership to the caller; no manual cleanup is needed. + +## Node Identity (`getNodeIdentity`, `clearNodeIdentity`) + +`getNodeIdentity()` implements a load-or-generate pattern for the node's stable secp256k1 keypair. On each call it queries `NodeIdentity` and attempts to parse both columns as base58-encoded keys. Critically, it validates that the stored pair is internally consistent by calling `derivePublicKey(KeyType::secp256k1, *sk)` and comparing the result against the stored public key. This guards against a partially-written or corrupted row silently producing a broken identity. Only if the pair passes this check is it returned; otherwise, a fresh random keypair is generated via `randomKeyPair()` and inserted. This means the table may accumulate rows over time if corruption occurs, but the logic always prefers the first valid row found, making the operation idempotent from the caller's perspective. `clearNodeIdentity()` supports test teardown and re-keying scenarios by issuing a simple `DELETE FROM NodeIdentity`. + +## Manifest Persistence (`getManifests`, `saveManifests`, `addValidatorManifest`) + +The manifest system exists because XRPL validators use a master secret key — kept in cold storage — to sign ephemeral "signing key" certificates. These certificates (manifests) let the rest of the network verify validation signatures without exposing the master key. The wallet database persists manifests across restarts so the node doesn't need to re-gossip them from peers on every boot. + +`getManifests()` is the load path: it iterates all rows in the given table, converts each BLOB to a `std::string`, and passes it through `deserializeManifest()` followed by `Manifest::verify()` before calling `mCache.applyManifest()`. Invalid or unverifiable manifests are logged as warnings and skipped — the database is treated as an untrusted source that must be cryptographically re-validated on read. + +`saveManifests()` uses a full-replace strategy: it opens a transaction, deletes all existing rows, then re-inserts from the live `ManifestCache` map. This avoids the complexity of a diff. The trust filter is nuanced: untrusted non-revocation manifests are silently dropped (not persisted), but **revocation manifests are always saved regardless of trust status**. This asymmetry is intentional — a revocation is evidence of compromise; discarding it based on trust status would be a security regression. + +The low-level `saveManifest()` (file-scope static) creates a fresh `soci::blob` for each row. The comment explains a subtle SOCI quirk: reusing a blob object is unsafe when data lengths vary between inserts, because SOCI's blob write length is expected to be no shorter than the previous write. Since ECDSA signatures vary in length, a new blob per row is the only safe approach. + +`addValidatorManifest()` is a focused append — it wraps a single `saveManifest()` call targeting `ValidatorManifests` in its own transaction. It is called when a new validator manifest arrives at runtime (via gossip), rather than during bulk persistence. + +## Peer Reservations (`getPeerReservationTable`, `insertPeerReservation`, `deletePeerReservation`) + +Peer reservations allow an operator to designate specific nodes that are guaranteed connection slots regardless of the normal peer capacity limits. All three functions are straightforward, but `insertPeerReservation()` is noteworthy for using SQLite's `ON CONFLICT (PublicKey) DO UPDATE SET` upsert syntax — updating the description if the key already exists — rather than a conditional insert. This makes the operation idempotent, which is important since the same node may be re-registered with an updated description. `getPeerReservationTable()` returns an `std::unordered_set` keyed by `PeerReservation` using `beast::uhash`, consistent with the rest of the XRPL codebase's preference for open-addressing hash tables. + +## Amendment Voting (`createFeatureVotes`, `readAmendments`, `voteAmendment`) + +The `FeatureVotes` table records this node's preferences on XRPL amendments. It is conspicuously absent from `WalletDBInit` — it is created lazily by `createFeatureVotes()`, which checks `sqlite_master` and creates the table only if needed. The return value (`true` if it already existed) signals callers that migration may be required. + +The `AmendmentVote` enum has unintuitive integer mappings (up = 0, down = 1, obsolete = -1), explicitly acknowledged in the header comment as a historical artifact that cannot be changed without a migration. + +`voteAmendment()` uses an **append-only log** pattern: votes are always inserted, never updated in-place. `readAmendments()` recovers the current state using a window function — `RANK() OVER (PARTITION BY AmendmentHash ORDER BY ROWID DESC)` — selecting only the row with rank 1 (the most recently inserted) per amendment hash. This design preserves the full vote history and avoids `UPDATE` contention at the cost of table growth over time. + +## SOCI and `boost::optional` + +A recurring pattern throughout the file is the use of `boost::optional` rather than `std::optional` for SOCI output variables. This is a SOCI library requirement — it does not yet support the standard optional — and is called out explicitly in comments at every affected site, serving as documentation for future maintainers who might otherwise "modernize" the code and break it. \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMap.cpp.ai.json b/src/libxrpl/shamap/SHAMap.cpp.ai.json new file mode 100644 index 0000000000..af3ac505e5 --- /dev/null +++ b/src/libxrpl/shamap/SHAMap.cpp.ai.json @@ -0,0 +1,689 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "makeTypedLeaf" + ], + "entry_point": "makeTypedLeaf", + "purpose": "Creates a typed SHAMap leaf node based on the SHAMapNodeType provided.", + "validation_points": [ + "Manual check: if type is not recognized, triggers LogicError exception." + ] + }, + { + "call_chain": [ + "SHAMap::dirtyUp" + ], + "entry_point": "SHAMap::dirtyUp", + "purpose": "Propagates changes up the SHAMap tree, updating hashes and links from a modified child node up to the root.", + "validation_points": [ + "XRPL_ASSERT: Validates state_ is not Synching or Immutable.", + "XRPL_ASSERT: Validates child is non-null and has correct cowid_.", + "XRPL_ASSERT: Validates node is non-null after dynamic cast.", + "XRPL_ASSERT: Validates branch index is valid (>= 0)." + ] + }, + { + "call_chain": [ + "SHAMap::walkTowardsKey" + ], + "entry_point": "SHAMap::walkTowardsKey", + "purpose": "Traverses the SHAMap from the root towards a key, optionally recording the path in a stack.", + "validation_points": [ + "XRPL_ASSERT: Validates stack is null or empty at entry." + ] + }, + { + "call_chain": [ + "SHAMap::SHAMap", + "SHAMap::SHAMap (copy constructor)", + "SHAMap::snapShot" + ], + "entry_point": "SHAMap::SHAMap (constructors)", + "purpose": "Constructs a SHAMap in various states (Modifying, Synching, Immutable), optionally as a snapshot.", + "validation_points": [ + "State is set explicitly; no runtime validation, but state_ is validated in downstream functions like dirtyUp." + ] + } + ], + "data_flows": [ + { + "field": "type (SHAMapNodeType)", + "flow": [ + "makeTypedLeaf parameter", + "if/else chain", + "constructor for specific SHAMapLeafNode subclass or LogicError" + ], + "origin": "Parameter to makeTypedLeaf", + "transformations": [ + "Type is checked against known enum values; if not matched, triggers LogicError." + ], + "validated_at": "makeTypedLeaf (manual check, LogicError)" + }, + { + "field": "state_ (SHAMapState)", + "flow": [ + "SHAMap constructor", + "assigned to state_", + "used in dirtyUp and other methods" + ], + "origin": "Set in SHAMap constructors", + "transformations": [ + "Set to Modifying, Synching, or Immutable depending on constructor and isMutable flag." + ], + "validated_at": "dirtyUp (XRPL_ASSERT), other methods may also assert state_" + }, + { + "field": "child (SHAMapTreeNode pointer)", + "flow": [ + "dirtyUp parameter", + "XRPL_ASSERT for non-null and cowid_ match", + "used as child node in tree update" + ], + "origin": "Parameter to dirtyUp", + "transformations": [ + "Checked for validity, then moved up the tree as dirtyUp propagates." + ], + "validated_at": "dirtyUp (XRPL_ASSERT)" + }, + { + "field": "stack (SharedPtrNodeStack)", + "flow": [ + "walkTowardsKey parameter", + "XRPL_ASSERT for null or empty", + "populated during traversal", + "passed to dirtyUp" + ], + "origin": "Parameter to dirtyUp and walkTowardsKey", + "transformations": [ + "Stack is filled with path nodes during traversal, then consumed in dirtyUp." + ], + "validated_at": "walkTowardsKey (XRPL_ASSERT)" + } + ], + "description": "This file implements the core logic for the SHAMap (Shared Hash Map) data structure used in the XRPL (XRP Ledger) project. It provides methods for traversing, modifying, and synchronizing the SHAMap tree, including adding, deleting, and updating items, as well as handling node sharing, caching, and persistence.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (SHAMapNodeType)", + "empty", + "string", + "validation" + ], + "evidence": "LogicError (manual check) at makeTypedLeaf", + "issue_pattern": "Missing empty string validation for type (SHAMapNodeType)", + "why_false_positive": "LogicError (manual check) validates type (SHAMapNodeType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "state_ (SHAMapState)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at dirtyUp", + "issue_pattern": "Missing empty string validation for state_ (SHAMapState)", + "why_false_positive": "XRPL_ASSERT validates state_ (SHAMapState) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "child (SHAMapTreeNode pointer)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at dirtyUp", + "issue_pattern": "Missing empty string validation for child (SHAMapTreeNode pointer)", + "why_false_positive": "XRPL_ASSERT validates child (SHAMapTreeNode pointer) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/shamap/SHAMap.cpp", + "functions": [ + { + "args": [ + "type", + "item", + "owner" + ], + "lineno": 11, + "name": "makeTypedLeaf" + }, + { + "args": [ + "t", + "f" + ], + "lineno": 23, + "name": "SHAMap::SHAMap" + }, + { + "args": [ + "t", + "hash", + "f" + ], + "lineno": 31, + "name": "SHAMap::SHAMap" + }, + { + "args": [ + "other", + "isMutable" + ], + "lineno": 39, + "name": "SHAMap::SHAMap" + }, + { + "args": [ + "isMutable" + ], + "lineno": 54, + "name": "SHAMap::snapShot" + }, + { + "args": [ + "stack", + "target", + "child" + ], + "lineno": 59, + "name": "SHAMap::dirtyUp" + }, + { + "args": [ + "id", + "stack" + ], + "lineno": 85, + "name": "SHAMap::walkTowardsKey" + }, + { + "args": [ + "id" + ], + "lineno": 108, + "name": "SHAMap::findKey" + }, + { + "args": [ + "hash" + ], + "lineno": 117, + "name": "SHAMap::fetchNodeFromDB" + }, + { + "args": [ + "hash", + "object" + ], + "lineno": 123, + "name": "SHAMap::finishFetch" + }, + { + "args": [ + "hash", + "filter" + ], + "lineno": 146, + "name": "SHAMap::checkFilter" + }, + { + "args": [ + "hash", + "filter" + ], + "lineno": 167, + "name": "SHAMap::fetchNodeNT" + }, + { + "args": [ + "hash" + ], + "lineno": 183, + "name": "SHAMap::fetchNodeNT" + }, + { + "args": [ + "hash" + ], + "lineno": 192, + "name": "SHAMap::fetchNode" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 201, + "name": "SHAMap::descendThrow" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 209, + "name": "SHAMap::descendThrow" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 217, + "name": "SHAMap::descend" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 228, + "name": "SHAMap::descend" + }, + { + "args": [ + "parent", + "branch" + ], + "lineno": 239, + "name": "SHAMap::descendNoStore" + }, + { + "args": [ + "parent", + "parentID", + "branch", + "filter" + ], + "lineno": 247, + "name": "SHAMap::descend" + }, + { + "args": [ + "parent", + "branch", + "filter", + "pending", + "callback" + ], + "lineno": 267, + "name": "SHAMap::descendAsync" + }, + { + "args": [ + "node", + "nodeID" + ], + "lineno": 295, + "name": "SHAMap::unshareNode" + }, + { + "args": [ + "node", + "stack", + "branch", + "loopParams" + ], + "lineno": 312, + "name": "SHAMap::belowHelper" + }, + { + "args": [ + "node", + "stack", + "branch" + ], + "lineno": 344, + "name": "SHAMap::lastBelow" + }, + { + "args": [ + "node", + "stack", + "branch" + ], + "lineno": 352, + "name": "SHAMap::firstBelow" + }, + { + "args": [ + "node" + ], + "lineno": 360, + "name": "SHAMap::onlyBelow" + }, + { + "args": [ + "stack" + ], + "lineno": 389, + "name": "SHAMap::peekFirstItem" + }, + { + "args": [ + "id", + "stack" + ], + "lineno": 399, + "name": "SHAMap::peekNextItem" + }, + { + "args": [ + "id" + ], + "lineno": 419, + "name": "SHAMap::peekItem" + }, + { + "args": [ + "id", + "hash" + ], + "lineno": 427, + "name": "SHAMap::peekItem" + }, + { + "args": [ + "id" + ], + "lineno": 435, + "name": "SHAMap::upper_bound" + }, + { + "args": [ + "id" + ], + "lineno": 464, + "name": "SHAMap::lower_bound" + }, + { + "args": [ + "id" + ], + "lineno": 493, + "name": "SHAMap::hasItem" + }, + { + "args": [ + "id" + ], + "lineno": 498, + "name": "SHAMap::delItem" + }, + { + "args": [ + "type", + "item" + ], + "lineno": 563, + "name": "SHAMap::addGiveItem" + }, + { + "args": [ + "type", + "item" + ], + "lineno": 617, + "name": "SHAMap::addItem" + }, + { + "args": [], + "lineno": 622, + "name": "SHAMap::getHash" + }, + { + "args": [ + "type", + "item" + ], + "lineno": 631, + "name": "SHAMap::updateGiveItem" + }, + { + "args": [ + "hash", + "filter" + ], + "lineno": 663, + "name": "SHAMap::fetchRoot" + }, + { + "args": [ + "t", + "node" + ], + "lineno": 693, + "name": "SHAMap::writeNode" + }, + { + "args": [ + "node" + ], + "lineno": 713, + "name": "SHAMap::preFlushNode" + }, + { + "args": [], + "lineno": 726, + "name": "SHAMap::unshare" + }, + { + "args": [ + "t" + ], + "lineno": 731, + "name": "SHAMap::flushDirty" + }, + { + "args": [ + "doWrite", + "t" + ], + "lineno": 736, + "name": "SHAMap::walkSubTree" + }, + { + "args": [ + "hash" + ], + "lineno": 813, + "name": "SHAMap::dump" + }, + { + "args": [ + "hash" + ], + "lineno": 849, + "name": "SHAMap::cacheLookup" + }, + { + "args": [ + "hash", + "node" + ], + "lineno": 855, + "name": "SHAMap::canonicalize" + }, + { + "args": [], + "lineno": 862, + "name": "SHAMap::invariants" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The SHAMap code is core to XRPL and is typically tested in unit tests under 'src/test/shamap/' (e.g., SHAMap_test.cpp, SHAMapInner_test.cpp, SHAMapSync_test.cpp). These tests cover basic map operations, node insertion, deletion, and snapshotting. However, explicit tests for validation failures (e.g., passing invalid types to makeTypedLeaf, or invalid states/children to dirtyUp) may be limited or absent. Exception and assertion paths (LogicError, XRPL_ASSERT) are often not directly tested unless there are negative tests for error handling. Test coverage for normal data flows is likely high, but coverage for all validation error paths may be incomplete.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT, LogicError (custom contract/assertion macros/exceptions)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LogicError (exception)", + "field": "type (SHAMapNodeType)", + "location": "makeTypedLeaf", + "validated_by": "LogicError (manual check)", + "validates": [ + "Ensures 'type' is one of tnTRANSACTION_NM, tnTRANSACTION_MD, tnACCOUNT_STATE", + "Throws if 'type' is not a recognized SHAMapNodeType" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely contract violation exception)", + "field": "state_ (SHAMapState)", + "location": "dirtyUp", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures state_ is not Synching or Immutable before dirtyUp is called" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely contract violation exception)", + "field": "child (SHAMapTreeNode pointer)", + "location": "dirtyUp", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures child is not null", + "Ensures child->cowid() matches SHAMap::cowid_" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMap.cpp.ai.md b/src/libxrpl/shamap/SHAMap.cpp.ai.md new file mode 100644 index 0000000000..700c345898 --- /dev/null +++ b/src/libxrpl/shamap/SHAMap.cpp.ai.md @@ -0,0 +1,101 @@ +# SHAMap.cpp + +## Role in the System + +`SHAMap` is the foundational authenticated data structure of the XRP Ledger. Every ledger snapshot — whether it represents account state or a transaction set — is stored as a `SHAMap`. The structure is simultaneously a 16-way radix trie (fan-out of 16, depth 64) and a Merkle tree: inner nodes hash their children, and the root hash cryptographically commits to the entire content. This dual nature means that two nodes can prove they agree on ledger state simply by comparing root hashes, and any disagreement can be located in O(log N) steps. + +This file implements the full lifecycle of a `SHAMap`: construction, mutation (add, delete, update), read access and iteration, copy-on-write snapshotting, lazy node fetching from the database or peers, and flushing dirty nodes back to persistent storage. + +--- + +## State Machine and the `cowid_` Invariant + +Every `SHAMap` carries a `state_` (`SHAMapState`: `Modifying`, `Immutable`, `Synching`, `Invalid`) and an integer `cowid_` — a copy-on-write generation counter. + +The `cowid_` is the linchpin of the snapshotting system. Tree nodes carry their own `cowid()` indicating which map generation owns them. A node is "owned" by this map when its `cowid()` equals the map's `cowid_`; it is "shared" (canonicalized) when its `cowid()` is zero. `unshareNode()` enforces this: if a node's generation differs from the current map's, it clones the node before any mutation occurs. This is cheaper and more expressive than a single dirty-bit because multiple live snapshots can coexist with independent generation counters without any coordination. + +The copy constructor increments `cowid_` (`cowid_(other.cowid_ + 1)`) and then calls `unshare()` if either the original or the new copy is mutable — preventing concurrent maps from accidentally sharing nodes that either side might later mutate. An immutable copy of an immutable map can share all nodes safely without any clone step. + +--- + +## Constructors and Snapshotting + +Three constructors handle the three entry points: + +- `SHAMap(t, f)` — creates a fresh, empty map in `Modifying` state with a new empty root `SHAMapInnerNode`. +- `SHAMap(t, hash, f)` — creates a map in `Synching` state. The `hash` parameter is intentionally ignored at construction (only the root's hash matters once it is fetched), but is part of the API to signal caller intent clearly. +- `SHAMap(other, isMutable)` — used by `snapShot()`. Copies all metadata and the `root_` pointer, then conditionally calls `unshare()` to break sharing when mutability demands it. + +`snapShot()` is a thin wrapper returning a `std::shared_ptr` — necessary because callers often need to extend the map's lifetime beyond the current scope. + +--- + +## Tree Traversal + +`walkTowardsKey(id, stack)` is the core descent primitive. Starting from `root_`, it selects a 4-bit nibble of the 256-bit key at each level (via `selectBranch`), follows that branch, and optionally records each visited node in a `SharedPtrNodeStack`. The stack records the path from root to — but not including — the target node, enabling `dirtyUp()` to walk back up efficiently. + +`findKey()` calls `walkTowardsKey()` and performs an exact key comparison at the leaf, returning `nullptr` if the leaf found does not carry the requested key. This is necessary because the radix trie can terminate at a leaf whose prefix matches but whose stored key diverges. + +`firstBelow()` and `lastBelow()` delegate to `belowHelper()`, which accepts a tuple of lambdas `{init, cmp, incr}` to parameterize direction — avoiding code duplication. `peekFirstItem()` and `peekNextItem()` build on these to implement the map's forward iterator. `onlyBelow()` answers "is there exactly one leaf under this subtree?" and is used during deletion to collapse the trie. + +--- + +## Mutation: Add, Delete, Update + +All three mutation operations share a common pattern: walk towards the target key building a path stack, perform the local structural change, then call `dirtyUp()` to propagate hash invalidation upwards. + +**`addGiveItem()`** handles two cases. If the walk terminates at an inner node with an empty branch, it simply creates a typed leaf there. If it terminates at a leaf whose key collides in prefix with the new key, it must split: a loop descends additional levels creating new `SHAMapInnerNode` instances until the two keys diverge into separate branches. This respects the radix trie's merge property — inner nodes are only created when multiple items must coexist below them. + +**`delItem()`** removes the leaf and walks back up the stack, reducing each inner node's child count. If an inner node drops to zero children it is nulled out; if it drops to one child and `onlyBelow()` confirms a single item below it, the inner node collapses and the leaf is hoisted up — enforcing the merge property. The replacement leaf is recreated via `makeTypedLeaf()` using the deleted leaf's original type. + +**`updateGiveItem()`** locates the leaf, CoW-unshares it, swaps the payload, and — only if `setItem()` signals the hash changed — calls `dirtyUp()`, preventing spurious rehashing for no-op updates. + +**`dirtyUp(stack, target, child)`** consumes the path stack bottom-up, calling `unshareNode()` on each inner node and `setChild()` to link in the updated subtree, producing a chain of freshly CoW-owned inner nodes from the modified point back up to the root. + +--- + +## Node Fetching and Backed Maps + +`SHAMap` works both fully in-memory and lazily against a `NodeStore` database. The `backed_` flag distinguishes these modes. + +Node retrieval follows a tiered strategy in `fetchNodeNT()`: + +1. **In-process cache** (`cacheLookup()`) — queries `Family::getTreeNodeCache()`. +2. **Database** (`fetchNodeFromDB()`) — calls `f_.db().fetchNodeObject()`. +3. **Sync filter** (`checkFilter()`) — consults a `SHAMapSyncFilter`, supplying nodes received from peers during ledger acquisition. + +`fetchNodeNT()` returns `nullptr` on miss; `fetchNode()` throws `SHAMapMissingNode`. `descendThrow()` uses the throwing variant, ensuring that callers expecting a fully-available tree receive an exception rather than silent `nullptr` propagation. + +`finishFetch()` deserializes raw bytes into a `SHAMapTreeNode` via `makeFromPrefix()`, calls `canonicalize()` to register the node in the cache, and catches `std::runtime_error` to log and suppress deserialization failures rather than crash. + +`descendAsync()` offers a non-blocking path: if the node is absent from cache and filter, it posts an async I/O request via `f_.db().asyncFetch()` and sets `pending = true`. The callback invokes `finishFetch()` and fires a user-supplied callback. This is used during sync to maximize I/O concurrency across many concurrent node fetches. + +--- + +## Canonicalization and Caching + +`canonicalize(hash, node)` registers a node in the family-wide `TreeNodeCache`, or replaces the local pointer with an already-cached equivalent. The node must have `cowid == 0` before this call — only "unshared" nodes are safe to place in the shared cache. `cacheLookup()` asserts that returned nodes have `cowid == 0`, enforcing this invariant at both sides of the cache boundary. This design avoids duplicating node objects across multiple `SHAMap` instances rooted in the same `Family`. + +--- + +## Flushing and Persistence + +`walkSubTree(doWrite, t)` performs a post-order depth-first traversal using an explicit stack (safe on a potentially 64-level tree). For each node: `preFlushNode()` clones it if its `cowid` differs from the map's (protecting other maps sharing the node), then leaf and inner nodes compute updated hashes and call `unshare()` (sets `cowid` to 0). If `doWrite` is true, `writeNode()` serializes and persists to `f_.db()`. + +`flushDirty()` calls `walkSubTree(backed_, t)`, writing only for database-backed maps. `unshare()` calls `walkSubTree(false, ...)`, traversing to make all nodes shareable without writing. + +`getHash()` contains a deliberate `const_cast(*this).unshare()` when the root hash is zero. Computing the root hash requires traversing and updating inner node hashes — logically a read, but physically mutating. The `const_cast` is the acknowledged design compromise. + +--- + +## Leaf Type Dispatch + +`makeTypedLeaf()` maps `SHAMapNodeType` to one of three concrete leaf classes: `SHAMapTxLeafNode`, `SHAMapTxPlusMetaLeafNode`, `SHAMapAccountStateLeafNode`. Unrecognized types throw `LogicError` immediately — a programming error, not a recoverable condition. The three-way split exists because the XRPL serializes transaction maps and state maps differently, and type information must survive round-trips through the database. + +--- + +## Invariants and Defensive Patterns + +`invariants()` forces a full hash recompute, iterates every leaf via `peekFirstItem`/`peekNextItem`, and delegates to the root node's own `invariants()` check. Throughout the file, `XRPL_ASSERT` guards internal preconditions — state checks in `dirtyUp()`, `cowid` invariants in `unshareNode()` and `walkSubTree()`, non-null stack conditions in iteration — surfacing logic bugs in development without runtime overhead in production. Error conditions arising from external input use exceptions (`SHAMapMissingNode`, `LogicError`, `std::runtime_error`). + +The `full_` flag tracks whether the map is believed complete in the database. If `finishFetch()` finds a node absent, it clears `full_` and calls `f_.missingNodeAcquireBySeq()`, notifying the acquisition subsystem to re-fetch the ledger — integrating the map's lazy-loading mechanism directly with the ledger acquisition pipeline. \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapDelta.cpp.ai.json b/src/libxrpl/shamap/SHAMapDelta.cpp.ai.json new file mode 100644 index 0000000000..13e7c3e7ad --- /dev/null +++ b/src/libxrpl/shamap/SHAMapDelta.cpp.ai.json @@ -0,0 +1,434 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SHAMap::compare", + "SHAMap::walkBranch" + ], + "entry_point": "SHAMap::compare", + "purpose": "Compares two SHAMap trees, collecting differences up to a limit.", + "validation_points": [ + "SHAMap::compare: XRPL_ASSERT(isValid() && otherMap.isValid())", + "SHAMap::walkBranch: safe_downcast", + "SHAMap::walkBranch: safe_downcast", + "SHAMap::walkBranch: --maxCount <= 0", + "SHAMap::walkBranch: !otherMapItem" + ] + }, + { + "call_chain": [ + "SHAMap::walkBranch" + ], + "entry_point": "SHAMap::walkBranch", + "purpose": "Walks a branch of the SHAMap, comparing nodes/items to another map's item.", + "validation_points": [ + "safe_downcast", + "safe_downcast", + "--maxCount <= 0", + "!otherMapItem" + ] + } + ], + "data_flows": [ + { + "field": "node (SHAMapTreeNode*)", + "flow": [ + "function argument", + "pushed to nodeStack", + "popped and type-checked (isInner/isLeaf)", + "downcast via safe_downcast", + "used for traversal or item extraction" + ], + "origin": "Passed as argument to SHAMap::walkBranch", + "transformations": [ + "Type-checked (isInner/isLeaf)", + "Downcast to SHAMapInnerNode* or SHAMapLeafNode*" + ], + "validated_at": "safe_downcast or safe_downcast" + }, + { + "field": "otherMapItem (boost::intrusive_ptr)", + "flow": [ + "function argument", + "checked for null (!otherMapItem)", + "compared to item->key() and item->slice()" + ], + "origin": "Passed as argument to SHAMap::walkBranch", + "transformations": [ + "Null-checked", + "Compared for key and slice equality" + ], + "validated_at": "!otherMapItem" + }, + { + "field": "maxCount (int&)", + "flow": [ + "function argument", + "decremented (--maxCount) on each difference found", + "checked (--maxCount <= 0) to abort early" + ], + "origin": "Passed as argument to SHAMap::walkBranch (from SHAMap::compare)", + "transformations": [ + "Decremented on each difference", + "Used as early exit condition" + ], + "validated_at": "--maxCount <= 0" + }, + { + "field": "item (SHAMapItem::Ptr)", + "flow": [ + "peekItem() from SHAMapLeafNode", + "compared to otherMapItem (key and slice)", + "inserted into differences map as needed" + ], + "origin": "Extracted from SHAMapLeafNode via peekItem()", + "transformations": [ + "Compared for key and slice", + "Wrapped in DeltaRef for differences" + ], + "validated_at": "Indirectly via safe_downcast" + } + ], + "description": "Implements functions for comparing, traversing, and finding differences or missing nodes in SHAMap (a specialized Merkle tree used in XRPL). Includes both sequential and parallel traversal for missing node detection.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "node (via safe_downcast)", + "validation", + "missing", + "check" + ], + "evidence": "Field node (via safe_downcast) validated by C++ type system, safe_downcast, contract.h", + "issue_pattern": "Missing validation for node (via safe_downcast)", + "why_false_positive": "C++ type system, safe_downcast, contract.h validates node (via safe_downcast) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "maxCount (manual range check)", + "validation", + "missing", + "check" + ], + "evidence": "Field maxCount (manual range check) validated by C++ type system, safe_downcast, contract.h", + "issue_pattern": "Missing validation for maxCount (manual range check)", + "why_false_positive": "C++ type system, safe_downcast, contract.h validates maxCount (manual range check) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "otherMapItem (null check)", + "validation", + "missing", + "check" + ], + "evidence": "Field otherMapItem (null check) validated by C++ type system, safe_downcast, contract.h", + "issue_pattern": "Missing validation for otherMapItem (null check)", + "why_false_positive": "C++ type system, safe_downcast, contract.h validates otherMapItem (null check) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node", + "empty", + "string", + "validation" + ], + "evidence": "safe_downcast at SHAMap::walkBranch", + "issue_pattern": "Missing empty string validation for node", + "why_false_positive": "safe_downcast validates node for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "node", + "type", + "validation", + "check" + ], + "evidence": "safe_downcast at SHAMap::walkBranch", + "issue_pattern": "Missing type validation for node", + "why_false_positive": "safe_downcast validates node type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node", + "empty", + "string", + "validation" + ], + "evidence": "safe_downcast at SHAMap::walkBranch", + "issue_pattern": "Missing empty string validation for node", + "why_false_positive": "safe_downcast validates node for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "node", + "type", + "validation", + "check" + ], + "evidence": "safe_downcast at SHAMap::walkBranch", + "issue_pattern": "Missing type validation for node", + "why_false_positive": "safe_downcast validates node type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "maxCount", + "empty", + "string", + "validation" + ], + "evidence": "manual check (--maxCount <= 0) at SHAMap::walkBranch", + "issue_pattern": "Missing empty string validation for maxCount", + "why_false_positive": "manual check (--maxCount <= 0) validates maxCount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "maxCount", + "range", + "bounds", + "validation" + ], + "evidence": "manual check (--maxCount <= 0) at SHAMap::walkBranch", + "issue_pattern": "Missing range validation for maxCount", + "why_false_positive": "manual check (--maxCount <= 0) validates maxCount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "otherMapItem", + "empty", + "string", + "validation" + ], + "evidence": "null check (!otherMapItem) at SHAMap::walkBranch", + "issue_pattern": "Missing empty string validation for otherMapItem", + "why_false_positive": "null check (!otherMapItem) validates otherMapItem for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/shamap/SHAMapDelta.cpp", + "functions": [ + { + "args": [ + "node", + "otherMapItem", + "isFirstMap", + "differences", + "maxCount" + ], + "lineno": 15, + "name": "SHAMap::walkBranch" + }, + { + "args": [ + "otherMap", + "differences", + "maxCount" + ], + "lineno": 74, + "name": "SHAMap::compare" + }, + { + "args": [ + "missingNodes", + "maxMissing" + ], + "lineno": 168, + "name": "SHAMap::walkMap" + }, + { + "args": [ + "missingNodes", + "maxMissing" + ], + "lineno": 202, + "name": "SHAMap::walkMapParallel" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The core logic is likely tested in SHAMap unit tests, typically found in files like SHAMap_test.cpp or SHAMapDelta_test.cpp. These tests should cover tree comparison, difference detection, and edge cases (e.g., maxCount limits, empty branches, mismatched keys/slices). However, specific validation failures (e.g., invalid downcasts, corrupt trees, or missing nodes) may not be exhaustively tested, especially error paths involving exceptions or assertion failures. Tests for malformed input or intentional corruption may be limited or absent.", + "validation_architecture": { + "auto_validated_fields": [ + "node (via safe_downcast)", + "maxCount (manual range check)", + "otherMapItem (null check)" + ], + "framework": "C++ type system, safe_downcast, contract.h", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::bad_cast (via contract.h or safe_downcast)", + "field": "node", + "location": "SHAMap::walkBranch", + "validated_by": "safe_downcast", + "validates": [ + "Ensures node is SHAMapInnerNode* before treating as inner node" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "std::bad_cast (via contract.h or safe_downcast)", + "field": "node", + "location": "SHAMap::walkBranch", + "validated_by": "safe_downcast", + "validates": [ + "Ensures node is SHAMapLeafNode* before treating as leaf node" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "function returns false (not exception)", + "field": "maxCount", + "location": "SHAMap::walkBranch", + "validated_by": "manual check (--maxCount <= 0)", + "validates": [ + "Ensures maxCount does not go below zero, aborts early if limit reached" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (handled as empty branch)", + "field": "otherMapItem", + "location": "SHAMap::walkBranch", + "validated_by": "null check (!otherMapItem)", + "validates": [ + "Checks if otherMapItem is null to determine empty branch" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapDelta.cpp.ai.md b/src/libxrpl/shamap/SHAMapDelta.cpp.ai.md new file mode 100644 index 0000000000..279394cf75 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapDelta.cpp.ai.md @@ -0,0 +1,61 @@ +# SHAMapDelta.cpp + +This file implements the comparison and completeness-checking operations on `SHAMap`, the hex-radix Merkle trie that underlies every XRPL ledger's account-state and transaction maps. It is logically distinct from the main map mechanics (insert, fetch, hash) and focuses entirely on answering two questions: *how do two trees differ?* and *what nodes are missing from this tree?* + +## The Delta Comparison Subsystem + +`SHAMap::compare` and `SHAMap::walkBranch` together implement a diff of two maps. The result is a `Delta` — a `std::map` where each entry records a `DeltaItem = pair, intrusive_ptr>`. A `nullptr` first element means the item was absent from the first map (added), a `nullptr` second element means it was absent from the second map (deleted), and two non-null pointers at the same key mean the content changed between maps. + +### The Core Optimization + +The key insight is that `compare` short-circuits at the root hash before touching any node: + +```cpp +if (getHash() == otherMap.getHash()) + return true; +``` + +This is the whole point of a Merkle tree — matching subtrees need not be visited. When `compare` descends into two inner nodes, it iterates their 16 child-hash slots and only pushes a pair onto the work stack when the hashes for that branch differ. Matching branches are silently skipped, giving the algorithm O(d) complexity in the number of differences rather than O(n) in total items. + +### The Four Cases in `compare` + +At each step the algorithm pops a pair `(ourNode, otherNode)` and dispatches on their types: + +1. **Both leaves**: Direct key and slice comparison. Same key but different data → one modified entry; different keys → two unmatched entries (one deleted, one added). +2. **Inner vs. leaf**: The inner subtree must be walked fully against the single leaf item from the other side. Delegated to `walkBranch`. +3. **Leaf vs. inner**: Symmetric, but calls `walkBranch` on `otherMap` with `isFirstMap = false`. +4. **Both inner**: Iterate 16 child slots; push differing non-empty pairs for further traversal; call `walkBranch` when one side is empty. + +### `walkBranch` and the `isFirstMap` Flag + +`walkBranch` handles the asymmetric case: a full subtree on one side paired against either nothing (null `otherMapItem`) or a single leaf from the other side. It iterates the subtree breadth-first using an explicit stack, descending inner nodes and collecting all leaf items. For each leaf it finds three outcomes: + +- The other side has no item at all (`emptyBranch`): the leaf is recorded as unmatched. +- The leaf key matches `otherMapItem->key()` but slices differ: a modified-item entry is recorded and `emptyBranch` is set to true (the item has been "consumed"). +- Keys match exactly: a perfect match; `emptyBranch` is set true to suppress a trailing unmatched entry. + +After the walk, if `otherMapItem` was never matched (because its key didn't appear anywhere in the subtree), it is added as its own unmatched entry. + +The `isFirstMap` boolean determines which half of the `DeltaItem` pair carries the item and which carries `nullptr`, ensuring that the semantic meaning — (first-map version, second-map version) — is preserved regardless of which direction the asymmetry runs. + +### The `maxCount` Defense + +Both functions accept `maxCount` by reference and decrement a single shared counter on every insertion into `differences`. The caller receives `false` when the limit is reached, indicating a truncated diff. This guards against pathological or adversarial cases where a peer advertises a tree with thousands of fabricated differences — without a limit, even a short sync handshake could trigger O(n) traversal. The `compare` public interface takes `maxCount` by value, then passes it by reference into `walkBranch`, so the counter is shared across all recursive delegations. + +The two primary callers reveal how the limit is used: +- The ledger-diff RPC (`LedgerDiff.cpp`) passes `std::numeric_limits::max()`, treating it as unlimited. +- The ledger-sync handler (`Ledger.cpp` RPC) passes a small constant like 256, bounding exposure during synchronization. + +## Missing-Node Detection + +`walkMap` and `walkMapParallel` serve a different need: checking that the local node store contains every node of a given tree. During ledger loading, a node may have a hash for a child but not the child's data. `descendNoStore` (called from both functions) returns null for such gaps without throwing, so the caller can aggregate them into a `std::vector` and report or fetch them. + +`walkMap` is a straightforward iterative DFS over inner nodes: descend each non-empty branch, push inner nodes, and record nulls. It is used for transaction maps and as the fallback for state maps. + +### Parallel Walk Strategy + +`walkMapParallel` exists for performance during ledger loading and is called from `Ledger.cpp` when the parallel flag is set. It partitions the tree at depth 1 — the 16 children of the root inner node — and launches one `std::thread` per non-empty, non-leaf top-level child. Each thread owns its own `nodeStack` and traverses its subtree independently. Only `missingNodes` and `maxMissing` are shared between threads, protected by a single `std::mutex` via `std::lock_guard`. + +A subtle correctness concern: an unhandled exception in a `std::thread` calls `std::terminate`. The worker lambda therefore catches `SHAMapMissingNode` and records it into a separate `exceptions` vector (also mutex-guarded) rather than letting it escape. After joining all threads the function inspects this vector and logs each caught exception. The return value — `true` if no exceptions were thrown — is semantically distinct from whether missing nodes were found; missing nodes are always communicated through the output vector. + +The choice to parallelize at depth 1 rather than deeper is a pragmatic tradeoff: it gives up to 16-way parallelism with zero dynamic load balancing, trades the simplicity of one shared stack for 16 independent stacks, and avoids any coordination overhead during descent. \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapInnerNode.cpp.ai.json b/src/libxrpl/shamap/SHAMapInnerNode.cpp.ai.json new file mode 100644 index 0000000000..2d6d3aab79 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapInnerNode.cpp.ai.json @@ -0,0 +1,594 @@ +{ + "args": [ + { + "lineno": 9, + "name": "cowid" + }, + { + "lineno": 9, + "name": "numAllocatedChildren" + }, + { + "lineno": 35, + "name": "toAllocate" + }, + { + "lineno": 40, + "name": "i" + }, + { + "lineno": 81, + "name": "hash" + }, + { + "lineno": 81, + "name": "hashValid" + }, + { + "lineno": 81, + "name": "data" + }, + { + "lineno": 108, + "name": "data" + }, + { + "lineno": 167, + "name": "s" + }, + { + "lineno": 187, + "name": "s" + }, + { + "lineno": 194, + "name": "id" + }, + { + "lineno": 207, + "name": "m" + }, + { + "lineno": 207, + "name": "child" + }, + { + "lineno": 232, + "name": "m" + }, + { + "lineno": 232, + "name": "child" + }, + { + "lineno": 247, + "name": "branch" + }, + { + "lineno": 259, + "name": "branch" + }, + { + "lineno": 271, + "name": "m" + }, + { + "lineno": 280, + "name": "branch" + }, + { + "lineno": 280, + "name": "node" + }, + { + "lineno": 304, + "name": "is_root" + } + ], + "classes": [ + { + "args": [ + "std::uint32_t cowid", + "std::uint8_t numAllocatedChildren" + ], + "lineno": 9, + "name": "SHAMapInnerNode" + } + ], + "code_paths": [ + { + "call_chain": [ + "SHAMapInnerNode::SHAMapInnerNode" + ], + "entry_point": "SHAMapInnerNode::SHAMapInnerNode", + "purpose": "Constructs a SHAMapInnerNode with given cowid and numAllocatedChildren.", + "validation_points": [ + "numAllocatedChildren validated by parameter type (std::uint8_t)", + "cowid validated by parameter type (std::uint32_t)" + ] + }, + { + "call_chain": [ + "SHAMapInnerNode::resizeChildArrays", + "TaggedPointer::TaggedPointer" + ], + "entry_point": "SHAMapInnerNode::resizeChildArrays", + "purpose": "Resizes the child arrays to allocate space for a given number of children.", + "validation_points": [ + "toAllocate validated by parameter type (std::uint8_t)" + ] + }, + { + "call_chain": [ + "SHAMapInnerNode::makeFullInner", + "intr_ptr::make_shared", + "SHAMapInnerNode::SHAMapInnerNode", + "SHAMapInnerNode::resizeChildArrays" + ], + "entry_point": "SHAMapInnerNode::makeFullInner", + "purpose": "Creates a fully populated inner node from serialized data.", + "validation_points": [ + "data.size() checked against expected size; throws if invalid" + ] + }, + { + "call_chain": [ + "SHAMapInnerNode::clone", + "intr_ptr::make_shared", + "SHAMapInnerNode::SHAMapInnerNode" + ], + "entry_point": "SHAMapInnerNode::clone", + "purpose": "Creates a deep copy of the node, including children and hashes.", + "validation_points": [ + "branchCount validated by parameter type (std::uint8_t) in constructor" + ] + } + ], + "data_flows": [ + { + "field": "numAllocatedChildren", + "flow": [ + "SHAMapInnerNode::SHAMapInnerNode(numAllocatedChildren)", + "hashesAndChildren_ initialized with numAllocatedChildren", + "Used in child array allocations and resizing" + ], + "origin": "Constructor parameter to SHAMapInnerNode", + "transformations": [ + "Passed directly to hashesAndChildren_", + "Used to size arrays" + ], + "validated_at": "Constructor parameter type (std::uint8_t) ensures range" + }, + { + "field": "cowid", + "flow": [ + "SHAMapInnerNode::SHAMapInnerNode(cowid)", + "Passed to SHAMapTreeNode base class" + ], + "origin": "Constructor parameter to SHAMapInnerNode", + "transformations": [ + "No transformation; stored as-is" + ], + "validated_at": "Constructor parameter type (std::uint32_t)" + }, + { + "field": "toAllocate", + "flow": [ + "SHAMapInnerNode::resizeChildArrays(toAllocate)", + "Passed to TaggedPointer constructor", + "TaggedPointer resizes internal arrays" + ], + "origin": "Parameter to resizeChildArrays", + "transformations": [ + "Used to allocate new arrays" + ], + "validated_at": "Parameter type (std::uint8_t)" + }, + { + "field": "data (Slice)", + "flow": [ + "SHAMapInnerNode::makeFullInner(data, ...)", + "data.size() validated against branchFactor * uint256::bytes", + "SerialIter si(data)", + "Hashes extracted from data and stored in hashesAndChildren_" + ], + "origin": "Parameter to makeFullInner", + "transformations": [ + "Checked for correct size", + "Deserialized into hashes" + ], + "validated_at": "Explicit check: if (data.size() != branchFactor * uint256::bytes) Throw" + } + ], + "description": "Implements the SHAMapInnerNode class, which represents an inner node in a SHAMap (a specialized Merkle tree used in XRPL). Provides methods for node construction, cloning, serialization, hash updating, child management, and invariants checking.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "numAllocatedChildren", + "validation", + "missing", + "check" + ], + "evidence": "Field numAllocatedChildren validated by C++ type system (no explicit validation framework in this file)", + "issue_pattern": "Missing validation for numAllocatedChildren", + "why_false_positive": "C++ type system (no explicit validation framework in this file) validates numAllocatedChildren automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "cowid", + "validation", + "missing", + "check" + ], + "evidence": "Field cowid validated by C++ type system (no explicit validation framework in this file)", + "issue_pattern": "Missing validation for cowid", + "why_false_positive": "C++ type system (no explicit validation framework in this file) validates cowid automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "toAllocate", + "validation", + "missing", + "check" + ], + "evidence": "Field toAllocate validated by C++ type system (no explicit validation framework in this file)", + "issue_pattern": "Missing validation for toAllocate", + "why_false_positive": "C++ type system (no explicit validation framework in this file) validates toAllocate automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "numAllocatedChildren", + "empty", + "string", + "validation" + ], + "evidence": "implicit (constructor parameter type: std::uint8_t) at SHAMapInnerNode::SHAMapInnerNode (constructor)", + "issue_pattern": "Missing empty string validation for numAllocatedChildren", + "why_false_positive": "implicit (constructor parameter type: std::uint8_t) validates numAllocatedChildren for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "numAllocatedChildren", + "type", + "validation", + "check" + ], + "evidence": "implicit (constructor parameter type: std::uint8_t) at SHAMapInnerNode::SHAMapInnerNode (constructor)", + "issue_pattern": "Missing type validation for numAllocatedChildren", + "why_false_positive": "implicit (constructor parameter type: std::uint8_t) validates numAllocatedChildren type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "cowid", + "empty", + "string", + "validation" + ], + "evidence": "implicit (constructor parameter type: std::uint32_t) at SHAMapInnerNode::SHAMapInnerNode (constructor)", + "issue_pattern": "Missing empty string validation for cowid", + "why_false_positive": "implicit (constructor parameter type: std::uint32_t) validates cowid for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "cowid", + "type", + "validation", + "check" + ], + "evidence": "implicit (constructor parameter type: std::uint32_t) at SHAMapInnerNode::SHAMapInnerNode (constructor)", + "issue_pattern": "Missing type validation for cowid", + "why_false_positive": "implicit (constructor parameter type: std::uint32_t) validates cowid type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "toAllocate", + "empty", + "string", + "validation" + ], + "evidence": "implicit (parameter type: std::uint8_t) at SHAMapInnerNode::resizeChildArrays", + "issue_pattern": "Missing empty string validation for toAllocate", + "why_false_positive": "implicit (parameter type: std::uint8_t) validates toAllocate for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "toAllocate", + "type", + "validation", + "check" + ], + "evidence": "implicit (parameter type: std::uint8_t) at SHAMapInnerNode::resizeChildArrays", + "issue_pattern": "Missing type validation for toAllocate", + "why_false_positive": "implicit (parameter type: std::uint8_t) validates toAllocate type" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/shamap/SHAMapInnerNode.cpp", + "functions": [ + { + "args": [ + "std::uint32_t cowid", + "std::uint8_t numAllocatedChildren" + ], + "lineno": 9, + "name": "SHAMapInnerNode" + }, + { + "args": [], + "lineno": 13, + "name": "~SHAMapInnerNode" + }, + { + "args": [], + "lineno": 15, + "name": "partialDestructor" + }, + { + "args": [ + "F&& f" + ], + "lineno": 23, + "name": "iterChildren" + }, + { + "args": [ + "F&& f" + ], + "lineno": 29, + "name": "iterNonEmptyChildIndexes" + }, + { + "args": [ + "std::uint8_t toAllocate" + ], + "lineno": 35, + "name": "resizeChildArrays" + }, + { + "args": [ + "int i" + ], + "lineno": 40, + "name": "getChildIndex" + }, + { + "args": [ + "std::uint32_t cowid" + ], + "lineno": 45, + "name": "clone" + }, + { + "args": [ + "Slice data", + "SHAMapHash const& hash", + "bool hashValid" + ], + "lineno": 81, + "name": "makeFullInner" + }, + { + "args": [ + "Slice data" + ], + "lineno": 108, + "name": "makeCompressedInner" + }, + { + "args": [], + "lineno": 137, + "name": "updateHash" + }, + { + "args": [], + "lineno": 153, + "name": "updateHashDeep" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 167, + "name": "serializeForWire" + }, + { + "args": [ + "Serializer& s" + ], + "lineno": 187, + "name": "serializeWithPrefix" + }, + { + "args": [ + "SHAMapNodeID const& id" + ], + "lineno": 194, + "name": "getString" + }, + { + "args": [ + "int m", + "intr_ptr::SharedPtr child" + ], + "lineno": 207, + "name": "setChild" + }, + { + "args": [ + "int m", + "intr_ptr::SharedPtr const& child" + ], + "lineno": 232, + "name": "shareChild" + }, + { + "args": [ + "int branch" + ], + "lineno": 247, + "name": "getChildPointer" + }, + { + "args": [ + "int branch" + ], + "lineno": 259, + "name": "getChild" + }, + { + "args": [ + "int m" + ], + "lineno": 271, + "name": "getChildHash" + }, + { + "args": [ + "int branch", + "intr_ptr::SharedPtr node" + ], + "lineno": 280, + "name": "canonicalizeChild" + }, + { + "args": [ + "bool is_root" + ], + "lineno": 304, + "name": "invariants" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The core validation logic (constructor parameter types, explicit size check in makeFullInner) is likely covered by unit tests in the SHAMap or SHAMapInnerNode test suites. Typical test files would be in 'src/test/shamap/' or similar, e.g., SHAMap_test.cpp, SHAMapInnerNode_test.cpp. However, implicit validation via parameter types (e.g., std::uint8_t for numAllocatedChildren) may not be directly tested for out-of-range or overflow conditions. The explicit exception in makeFullInner is likely tested for invalid input data, but edge cases for maximum/minimum values of numAllocatedChildren, cowid, and toAllocate may not be thoroughly tested unless specifically targeted in the test suite.", + "validation_architecture": { + "auto_validated_fields": [ + "numAllocatedChildren", + "cowid", + "toAllocate" + ], + "framework": "C++ type system (no explicit validation framework in this file)", + "validation_layer": "entry_point (constructor parameter types)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "n/a (compile-time type enforcement)", + "field": "numAllocatedChildren", + "location": "SHAMapInnerNode::SHAMapInnerNode (constructor)", + "validated_by": "implicit (constructor parameter type: std::uint8_t)", + "validates": [ + "numAllocatedChildren must be an unsigned 8-bit integer" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (compile-time type enforcement)", + "field": "cowid", + "location": "SHAMapInnerNode::SHAMapInnerNode (constructor)", + "validated_by": "implicit (constructor parameter type: std::uint32_t)", + "validates": [ + "cowid must be an unsigned 32-bit integer" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (compile-time type enforcement)", + "field": "toAllocate", + "location": "SHAMapInnerNode::resizeChildArrays", + "validated_by": "implicit (parameter type: std::uint8_t)", + "validates": [ + "toAllocate must be an unsigned 8-bit integer" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapInnerNode.cpp.ai.md b/src/libxrpl/shamap/SHAMapInnerNode.cpp.ai.md new file mode 100644 index 0000000000..d943d5ac65 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapInnerNode.cpp.ai.md @@ -0,0 +1,51 @@ +# `SHAMapInnerNode.cpp` — Inner Routing Node of the SHAMap Merkle Trie + +## Role in the System + +The XRP Ledger's state and transaction data are authenticated through the `SHAMap`, a 16-way radix Merkle trie. `SHAMapInnerNode` is the non-leaf (routing) node of that trie. Every inner node branches on exactly 4 bits of a 256-bit key path, giving a `branchFactor` of 16. Its cryptographic hash is deterministically derived from the hashes of its children, and the root hash of the entire trie is the canonical fingerprint of ledger state. + +This file implements the logic needed to create, modify, clone, hash, serialize, and validate these inner nodes while satisfying the trie's concurrent-read, copy-on-write (CoW) modification model. + +## Memory Layout: `TaggedPointer` + +The most architecturally significant detail is `hashesAndChildren_`, a `TaggedPointer` that stores two parallel arrays — a `SHAMapHash[]` for known child hashes and an `intr_ptr::SharedPtr[]` for in-memory child pointers — in a *single* allocation, with the array capacity encoded in the lowest two bits of the pointer. `TaggedPointer` supports only four allocation sizes, and nodes typically start small (default `numAllocatedChildren = 2`). + +This sparsity is the primary RAM optimization. A fully populated inner node needs space for 16 hash+pointer pairs; a node with two children needs only two. According to the header comment, this reduces average inner-node memory to roughly 25% of a naive dense layout. The `isBranch_` field is a 16-bit bitmask that maps logical branch positions to physical array indices, allowing iteration over non-empty branches at O(population-count) cost rather than O(16). + +The dual-array layout is also the reason `iterChildren()` and `iterNonEmptyChildIndexes()` exist as separate primitives: hash computation always needs all 16 slots (zero-filled for absent children), while mutation and serialization only need non-empty slots. + +## Copy-on-Write Semantics + +`SHAMapTreeNode` carries a `cowid_` field: a non-zero value names the owning `SHAMap`; zero means the node is freely shared. All mutation methods assert `cowid_ != 0` (guarded by `XRPL_ASSERT`), making it a hard invariant that only the owning map modifies a node. When another map needs to modify a shared node, it calls `clone(cowid)`, which produces a deep copy with the new owner's id. + +The `clone()` implementation has a subtle correctness requirement: it copies hashes outside the lock (they are immutable once set) but copies child shared-pointer references under the per-child spinlock, because another thread might be racing to install a freshly-fetched child pointer at the same time. + +## Per-Child Spinlocks + +`lock_` is a `std::atomic` used as a compact spinlock array: one bit per branch, 16 bits total. The `packed_spinlock` wrapper acquires exactly one of those bits via `fetch_or`/`fetch_and` with acquire/release ordering. This means reads of *different* children can proceed concurrently without serializing on a single mutex. Only `getChild()`, `getChildPointer()`, and `canonicalizeChild()` acquire these locks; `setChild()` and `shareChild()` do not, since they operate only on CoW-owned (non-shared) nodes and thus have exclusive access by construction. + +The full-node `spinlock` (not `packed_spinlock`) is also present in `clone()`, where the entire children array needs to be snapshot consistently. + +## `setChild()` — Dynamic Resizing + +When a child is added to or removed from a node, the branch count changes and the `TaggedPointer` may need to be reallocated to a different capacity tier. `setChild()` computes `dstIsBranch`, derives the needed allocation count via `popcnt16`, and then constructs a new `TaggedPointer` in place, moving existing entries. This reallocation is handled entirely within the `TaggedPointer` constructors to avoid double-copying. After the resize, the child pointer is installed and `hash_` is zeroed, marking the node as dirty. + +## `canonicalizeChild()` — First-Writer Wins + +When a node is fetched from the database and installed into an inner node, multiple threads might simultaneously fetch the same child. `canonicalizeChild()` resolves the race under the per-child spinlock: if a child is already set, the incumbent wins and the caller's newly-constructed node is discarded. If the slot is empty, the caller's node is installed. This implements a lazy, lock-safe memoization pattern that ensures the entire tree converges on a single canonical object per node, avoiding duplicated in-memory representations. + +## Hash Computation + +`updateHash()` iterates all 16 branches via `iterChildren()` (including zero hashes for absent branches), prepends `HashPrefix::innerNode`, and computes the SHA-512/2 half-hash. Iterating all 16 branches regardless of sparsity is intentional: the hash must be identical whether the node is stored densely or sparsely. `updateHashDeep()` additionally pulls each child's current hash from the in-memory child pointer before calling `updateHash()`, which is used during trie construction or after batch mutations where child hashes may have been updated in memory but not yet propagated upward. + +## Serialization: Dense vs. Compressed Wire Format + +`serializeForWire()` applies a cutoff of 12 occupied branches. Nodes with fewer than 12 children use the *compressed inner* format: each non-empty branch is encoded as a 32-byte hash followed by a 1-byte branch index (33 bytes × n). Nodes with 12 or more branches use the *full inner* format: all 16 hashes in sequence (512 bytes). `makeFullInner()` and `makeCompressedInner()` are the corresponding deserialization factories, validated against exact size constraints and throwing `std::runtime_error` on malformed input. The compressed format is beneficial on the wire (typical inner nodes are sparse) but adds branch-index bookkeeping. + +## `partialDestructor()` and Intrusive Weak Pointers + +`SHAMapInnerNode` inherits from `IntrusiveRefCounts`, which supports weak pointers. When the strong reference count reaches zero but weak references are still live, `partialDestructor()` is called before the object is destroyed: it manually calls `.reset()` on every child `SharedPtr`, breaking reference cycles without releasing the object's memory. This is the canonical pattern for intrusive ref-count systems that need to separate resource release from deallocation. + +## `invariants()` + +The `invariants()` method is a debug-time coherence check. It verifies that the `isBranch_` bitmask is consistent with non-zero hashes, that non-root nodes have non-zero hashes, and that the `count` of non-empty branches matches hash zeroness. It also recurses into children. This is called during testing and debugging to catch corruption early, and it correctly handles both dense and sparse array layouts by branching on `numAllocated == branchFactor`. \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.json b/src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.json new file mode 100644 index 0000000000..167329de70 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.json @@ -0,0 +1,356 @@ +{ + "args": [ + { + "lineno": 4, + "name": "item" + }, + { + "lineno": 4, + "name": "cowid" + }, + { + "lineno": 13, + "name": "hash" + }, + { + "lineno": 34, + "name": "id" + } + ], + "classes": [ + { + "args": [ + "boost::intrusive_ptr item, std::uint32_t cowid", + "boost::intrusive_ptr item, std::uint32_t cowid, SHAMapHash const& hash" + ], + "lineno": 4, + "name": "SHAMapLeafNode" + } + ], + "code_paths": [ + { + "call_chain": [ + "SHAMapLeafNode::SHAMapLeafNode" + ], + "entry_point": "SHAMapLeafNode::SHAMapLeafNode(boost::intrusive_ptr item, std::uint32_t cowid)", + "purpose": "Constructs a SHAMapLeafNode with an item and cowid; validates item size.", + "validation_points": [ + "XRPL_ASSERT(item_->size() >= 12) in constructor" + ] + }, + { + "call_chain": [ + "SHAMapLeafNode::SHAMapLeafNode" + ], + "entry_point": "SHAMapLeafNode::SHAMapLeafNode(boost::intrusive_ptr item, std::uint32_t cowid, SHAMapHash const& hash)", + "purpose": "Constructs a SHAMapLeafNode with an item, cowid, and hash; validates item size.", + "validation_points": [ + "XRPL_ASSERT(item_->size() >= 12) in constructor" + ] + }, + { + "call_chain": [ + "SHAMapLeafNode::setItem" + ], + "entry_point": "SHAMapLeafNode::setItem(boost::intrusive_ptr item)", + "purpose": "Sets the item for the node, validates cowid, updates hash.", + "validation_points": [ + "XRPL_ASSERT(cowid_)" + ] + }, + { + "call_chain": [ + "SHAMapLeafNode::invariants" + ], + "entry_point": "SHAMapLeafNode::invariants(bool)", + "purpose": "Checks invariants for the node: hash is nonzero, item is non-null.", + "validation_points": [ + "XRPL_ASSERT(hash_.isNonZero())", + "XRPL_ASSERT(item_)" + ] + } + ], + "data_flows": [ + { + "field": "item_", + "flow": [ + "Constructor parameter", + "Assigned to item_", + "Used in setItem, getString, invariants" + ], + "origin": "Constructor parameter (item)", + "transformations": [ + "Moved into item_ in constructor", + "Replaced in setItem" + ], + "validated_at": "item_->size() validated in constructors; item_ validated in invariants" + }, + { + "field": "cowid_", + "flow": [ + "Constructor parameter", + "Passed to SHAMapTreeNode base", + "Used in setItem" + ], + "origin": "Constructor parameter (cowid)", + "transformations": [ + "None (direct assignment)" + ], + "validated_at": "XRPL_ASSERT(cowid_) in setItem" + }, + { + "field": "hash_", + "flow": [ + "Constructor parameter or computed", + "Assigned to hash_", + "Used in getString, invariants" + ], + "origin": "Constructor parameter (hash) or computed via updateHash()", + "transformations": [ + "Set in constructor or updated in setItem via updateHash()" + ], + "validated_at": "XRPL_ASSERT(hash_.isNonZero()) in invariants" + }, + { + "field": "item_->size()", + "flow": [ + "item_ (from constructor)", + "item_->size() checked in constructor", + "item_->size() used in getString" + ], + "origin": "SHAMapItem::size()", + "transformations": [ + "None (read-only)" + ], + "validated_at": "XRPL_ASSERT(item_->size() >= 12) in constructors" + } + ], + "description": "Implements the SHAMapLeafNode class, representing a leaf node in the SHAMap tree structure used in XRPL, including constructors, item access, mutation, string representation, and invariants checking.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "item_->size()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapLeafNode::SHAMapLeafNode (constructor, line 7 and 17)", + "issue_pattern": "Missing empty string validation for item_->size()", + "why_false_positive": "XRPL_ASSERT validates item_->size() for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "item_->size()", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapLeafNode::SHAMapLeafNode (constructor, line 7 and 17)", + "issue_pattern": "Missing range validation for item_->size()", + "why_false_positive": "XRPL_ASSERT validates item_->size() range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "cowid_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapLeafNode::setItem", + "issue_pattern": "Missing empty string validation for cowid_", + "why_false_positive": "XRPL_ASSERT validates cowid_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "cowid_", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapLeafNode::setItem", + "issue_pattern": "Missing range validation for cowid_", + "why_false_positive": "XRPL_ASSERT validates cowid_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hash_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapLeafNode::invariants", + "issue_pattern": "Missing empty string validation for hash_", + "why_false_positive": "XRPL_ASSERT validates hash_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "item_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapLeafNode::invariants", + "issue_pattern": "Missing empty string validation for item_", + "why_false_positive": "XRPL_ASSERT validates item_ for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "item_", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT at SHAMapLeafNode::invariants", + "issue_pattern": "Missing type validation for item_", + "why_false_positive": "XRPL_ASSERT validates item_ type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/shamap/SHAMapLeafNode.cpp", + "functions": [ + { + "args": [ + "boost::intrusive_ptr item", + "std::uint32_t cowid" + ], + "lineno": 4, + "name": "SHAMapLeafNode::SHAMapLeafNode" + }, + { + "args": [ + "boost::intrusive_ptr item", + "std::uint32_t cowid", + "SHAMapHash const& hash" + ], + "lineno": 11, + "name": "SHAMapLeafNode::SHAMapLeafNode" + }, + { + "args": [], + "lineno": 20, + "name": "SHAMapLeafNode::peekItem" + }, + { + "args": [ + "boost::intrusive_ptr item" + ], + "lineno": 25, + "name": "SHAMapLeafNode::setItem" + }, + { + "args": [ + "SHAMapNodeID const& id" + ], + "lineno": 34, + "name": "SHAMapLeafNode::getString" + }, + { + "args": [ + "bool" + ], + "lineno": 56, + "name": "SHAMapLeafNode::invariants" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is primarily in constructors, setItem, and invariants. Typical test files would be in the rippled codebase under tests/shamap/ or tests/shamap/SHAMapLeafNode_test.cpp, but coverage depends on whether tests explicitly construct SHAMapLeafNode with invalid item sizes, null items, zero cowid, or zero hash. Gaps may exist if tests do not cover edge cases (e.g., item size < 12, cowid_ == 0, hash_ == 0, item_ == nullptr). Invariants may only be checked in debug/test builds. Review of test files is needed to confirm coverage of all validation paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro/function)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "item_->size()", + "location": "SHAMapLeafNode::SHAMapLeafNode (constructor, line 7 and 17)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures item_->size() >= 12", + "Minimum input size for SHAMapItem" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "cowid_", + "location": "SHAMapLeafNode::setItem", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures cowid_ is nonzero before allowing setItem" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "hash_", + "location": "SHAMapLeafNode::invariants", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures hash_ is nonzero (hash_.isNonZero())" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "item_", + "location": "SHAMapLeafNode::invariants", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures item_ is not null" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.md b/src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.md new file mode 100644 index 0000000000..05e6e1f0e5 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapLeafNode.cpp.ai.md @@ -0,0 +1,57 @@ +# `SHAMapLeafNode.cpp` — Abstract Leaf Node Implementation + +## Role in the SHAMap Hierarchy + +The SHAMap data structure underlying the XRPL ledger is a Merkle Patricia trie. Its nodes split into two fundamental categories: inner nodes that branch the tree and leaf nodes that hold actual ledger data. `SHAMapLeafNode` occupies the middle tier of a three-level inheritance chain: + +``` +SHAMapTreeNode (base: CoW identity, hash, refcounts) + └── SHAMapLeafNode (this file: item ownership, mutation, shared behavior) + ├── SHAMapTxLeafNode + ├── SHAMapTxPlusMetaLeafNode + └── SHAMapAccountStateLeafNode +``` + +This file implements the behavior that is common to every leaf type — holding a `SHAMapItem`, providing read access, mutating with hash-change detection, debug formatting, and invariant checking — while leaving type-specific logic (hashing algorithm, wire serialization, node type identity) to the concrete subclasses. + +## Copy-on-Write Integration + +Both constructors are `protected`, not `public`. This is not an accident: `SHAMapLeafNode` is abstract in the design sense even though C++ does not mark it explicitly with a pure virtual function (those are pushed to the subclasses). Only the three concrete leaf classes can call these constructors from their own constructors. + +The `cowid_` field, inherited from `SHAMapTreeNode`, encodes ownership in the CoW scheme. A nonzero `cowid_` means the node is exclusively owned by a particular `SHAMap` instance and carries unflushed changes. Zero means the node has been released into a shared state — it may simultaneously be referenced by multiple `SHAMap` views of the ledger. The `setItem()` method enforces this invariant directly: + +```cpp +XRPL_ASSERT(cowid_, "xrpl::SHAMapLeafNode::setItem : nonzero cowid"); +``` + +Mutating a shared (unowned) node would corrupt every `SHAMap` that holds a pointer to it. This assert is the runtime tripwire that catches any code path which forgets to first `clone()` the node before modifying it. + +## Two Construction Paths + +Two constructor overloads exist for two distinct situations. The first takes only an item and a `cowid`, leaving `hash_` default-initialized. Concrete subclasses using this overload immediately call `updateHash()` in their own constructor body to compute the hash fresh from the item. This is the path used when constructing a new node for the first time. + +The second overload accepts a pre-computed `SHAMapHash` alongside the item and `cowid`. This is used during deserialization (`makeFromWire`, `makeFromPrefix`) and during `clone()` operations, where the hash is already known and recomputing it would be wasteful. Passing the hash directly into the base class sets `hash_` without triggering a SHA-512 half computation. + +Both constructors assert `item_->size() >= 12`. The `size_` on a `SHAMapItem` reflects its data payload length, not the size of the object itself. The 12-byte floor is a protocol-level sanity guard: any serialized ledger object meaningful enough to appear in a SHAMap must carry at least this many bytes of content. This prevents degenerate or truncated objects from entering the tree. + +## Item Mutation with Change Detection + +`setItem()` replaces the held `SHAMapItem` and recomputes the hash via the virtual `updateHash()` dispatch. It returns a `bool` indicating whether the hash actually changed: + +```cpp +auto const oldHash = hash_; +updateHash(); +return (oldHash != hash_); +``` + +The return value is an optimization signal. Propagating a hash change upward through the tree — recomputing every ancestor inner node's hash — is expensive. If `setItem()` returns `false`, the caller knows the tree's Merkle root is unaffected and can skip that work entirely. In practice this situation is rare (replacing an item with one that produces the same hash would be unusual), but the check costs almost nothing and makes the SHAMap's mutation path more resilient to unnecessary work. + +## `getString()` and Debug Formatting + +`getString()` calls `SHAMapTreeNode::getString(id)` for position-level context and then appends type and content information. The type-dispatch calls `getType()`, which is pure virtual and resolved to the concrete subclass at runtime. This means `SHAMapLeafNode` produces a human-readable diagnostic string without knowing whether the item is a transaction, a transaction-with-metadata, or an account state object. The output includes the item's key (the 256-bit tag) and the item's data size in bytes alongside the node hash. + +## Invariants and Finality + +`invariants()` is marked `final override` in `SHAMapLeafNode`, sealing it against further override in concrete subclasses. It asserts two conditions that must hold for any valid leaf: the hash must be non-zero and the item pointer must not be null. These conditions collectively mean the leaf has been properly initialized and has not had its contents silently cleared. Similarly, `isLeaf()` and `isInner()` are both `final override` at this level, returning `true` and `false` respectively — no concrete leaf subclass can accidentally break the `isLeaf()` contract by overriding it again. + +The `boost::intrusive_ptr` used for `item_` integrates with `SHAMapItem`'s custom `intrusive_ptr_add_ref` / `intrusive_ptr_release` hooks, which manage the object's lifetime through a slab allocator. This avoids the separate heap allocation that `std::shared_ptr`'s control block would require — relevant for an object type that may have millions of live instances during ledger processing. \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapNodeID.cpp.ai.json b/src/libxrpl/shamap/SHAMapNodeID.cpp.ai.json new file mode 100644 index 0000000000..b475230834 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapNodeID.cpp.ai.json @@ -0,0 +1,608 @@ +{ + "args": [ + { + "lineno": 7, + "name": "depth" + }, + { + "lineno": 38, + "name": "depth" + }, + { + "lineno": 38, + "name": "hash" + }, + { + "lineno": 44, + "name": "m" + }, + { + "lineno": 72, + "name": "data" + }, + { + "lineno": 72, + "name": "size" + }, + { + "lineno": 92, + "name": "id" + }, + { + "lineno": 92, + "name": "hash" + }, + { + "lineno": 106, + "name": "depth" + }, + { + "lineno": 106, + "name": "key" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SHAMapNodeID::SHAMapNodeID" + ], + "entry_point": "SHAMapNodeID::SHAMapNodeID", + "purpose": "Constructs a SHAMapNodeID from a depth and hash, canonicalizing the hash and validating inputs.", + "validation_points": [ + "XRPL_ASSERT: depth <= SHAMap::leafDepth", + "XRPL_ASSERT: id_ == (id_ & depthMask(depth))" + ] + }, + { + "call_chain": [ + "SHAMapNodeID::getChildNodeID", + "SHAMapNodeID::SHAMapNodeID" + ], + "entry_point": "SHAMapNodeID::getChildNodeID", + "purpose": "Creates a child node ID from the current node, validating branch index and depth.", + "validation_points": [ + "XRPL_ASSERT: m < SHAMap::branchFactor", + "XRPL_ASSERT: depth_ <= SHAMap::leafDepth", + "Throw: if depth_ >= SHAMap::leafDepth", + "Throw: if id_ != (id_ & depthMask(depth_))", + "SHAMapNodeID::SHAMapNodeID: re-validates depth and id" + ] + }, + { + "call_chain": [ + "deserializeSHAMapNodeID", + "SHAMapNodeID::SHAMapNodeID" + ], + "entry_point": "deserializeSHAMapNodeID", + "purpose": "Deserializes a SHAMapNodeID from raw data, validating depth and hash mask.", + "validation_points": [ + "if (depth <= SHAMap::leafDepth)", + "if (id == (id & depthMask(depth)))", + "SHAMapNodeID::SHAMapNodeID: re-validates depth and id" + ] + }, + { + "call_chain": [ + "selectBranch" + ], + "entry_point": "selectBranch", + "purpose": "Selects a branch index from a node ID and hash, validating the result.", + "validation_points": [ + "XRPL_ASSERT: branch < SHAMap::branchFactor" + ] + }, + { + "call_chain": [ + "SHAMapNodeID::createID", + "SHAMapNodeID::SHAMapNodeID" + ], + "entry_point": "SHAMapNodeID::createID", + "purpose": "Creates a canonical SHAMapNodeID from a depth and key, validating depth and masking key.", + "validation_points": [ + "XRPL_ASSERT: (depth >= 0) && (depth < 65)", + "SHAMapNodeID::SHAMapNodeID: re-validates depth and id" + ] + } + ], + "data_flows": [ + { + "field": "depth", + "flow": [ + "Input parameter", + "Validated (XRPL_ASSERT or if-check)", + "Stored in SHAMapNodeID::depth_", + "Used in masking, child node creation, serialization" + ], + "origin": "Input parameter to SHAMapNodeID::SHAMapNodeID, deserializeSHAMapNodeID, createID", + "transformations": [ + "Checked for bounds (<= SHAMap::leafDepth, >= 0)", + "Incremented in getChildNodeID" + ], + "validated_at": "SHAMapNodeID::SHAMapNodeID, deserializeSHAMapNodeID, createID" + }, + { + "field": "id (hash)", + "flow": [ + "Input parameter", + "Masked with depthMask in createID and SHAMapNodeID::SHAMapNodeID", + "Stored in SHAMapNodeID::id_", + "Used in getChildNodeID, getRawString, selectBranch" + ], + "origin": "Input parameter to SHAMapNodeID::SHAMapNodeID, deserializeSHAMapNodeID, createID", + "transformations": [ + "Masked with depthMask(depth)", + "Bitwise operations in getChildNodeID" + ], + "validated_at": "SHAMapNodeID::SHAMapNodeID, deserializeSHAMapNodeID" + }, + { + "field": "m (branch index)", + "flow": [ + "Input parameter", + "Validated (XRPL_ASSERT: m < SHAMap::branchFactor)", + "Used to set bits in id_ for child node" + ], + "origin": "Input parameter to SHAMapNodeID::getChildNodeID", + "transformations": [ + "Bitwise OR into id_ at correct nibble" + ], + "validated_at": "SHAMapNodeID::getChildNodeID" + }, + { + "field": "depth_ (current node depth)", + "flow": [ + "Set in constructor", + "Used in getChildNodeID, selectBranch, getRawString" + ], + "origin": "Stored in SHAMapNodeID instance", + "transformations": [ + "Incremented in getChildNodeID" + ], + "validated_at": "SHAMapNodeID::SHAMapNodeID, getChildNodeID" + } + ], + "description": "Implements SHAMapNodeID logic for the XRP Ledger, including node ID creation, serialization, deserialization, child node calculation, and branch selection for SHAMap nodes.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "depth", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapNodeID::SHAMapNodeID (constructor)", + "issue_pattern": "Missing empty string validation for depth", + "why_false_positive": "XRPL_ASSERT validates depth for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "depth", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapNodeID::SHAMapNodeID (constructor)", + "issue_pattern": "Missing range validation for depth", + "why_false_positive": "XRPL_ASSERT validates depth range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "id (hash)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapNodeID::SHAMapNodeID (constructor)", + "issue_pattern": "Missing empty string validation for id (hash)", + "why_false_positive": "XRPL_ASSERT validates id (hash) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m (branch index)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapNodeID::getChildNodeID", + "issue_pattern": "Missing empty string validation for m (branch index)", + "why_false_positive": "XRPL_ASSERT validates m (branch index) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "m (branch index)", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapNodeID::getChildNodeID", + "issue_pattern": "Missing range validation for m (branch index)", + "why_false_positive": "XRPL_ASSERT validates m (branch index) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "depth_ (current node depth)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapNodeID::getChildNodeID", + "issue_pattern": "Missing empty string validation for depth_ (current node depth)", + "why_false_positive": "XRPL_ASSERT validates depth_ (current node depth) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "depth_ (current node depth)", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at SHAMapNodeID::getChildNodeID", + "issue_pattern": "Missing range validation for depth_ (current node depth)", + "why_false_positive": "XRPL_ASSERT validates depth_ (current node depth) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "depth_ (current node depth)", + "empty", + "string", + "validation" + ], + "evidence": "Throw at SHAMapNodeID::getChildNodeID", + "issue_pattern": "Missing empty string validation for depth_ (current node depth)", + "why_false_positive": "Throw validates depth_ (current node depth) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "id_ (hash)", + "empty", + "string", + "validation" + ], + "evidence": "Throw at SHAMapNodeID::getChildNodeID", + "issue_pattern": "Missing empty string validation for id_ (hash)", + "why_false_positive": "Throw validates id_ (hash) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "size (input buffer size)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (size == 33)) at deserializeSHAMapNodeID", + "issue_pattern": "Missing empty string validation for size (input buffer size)", + "why_false_positive": "explicit check (if (size == 33)) validates size (input buffer size) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "size (input buffer size)", + "format", + "validation", + "invalid" + ], + "evidence": "explicit check (if (size == 33)) at deserializeSHAMapNodeID", + "issue_pattern": "Missing format validation for size (input buffer size)", + "why_false_positive": "explicit check (if (size == 33)) validates size (input buffer size) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "depth (from input buffer)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (depth <= SHAMap::leafDepth)) at deserializeSHAMapNodeID", + "issue_pattern": "Missing empty string validation for depth (from input buffer)", + "why_false_positive": "explicit check (if (depth <= SHAMap::leafDepth)) validates depth (from input buffer) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "depth (from input buffer)", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (if (depth <= SHAMap::leafDepth)) at deserializeSHAMapNodeID", + "issue_pattern": "Missing range validation for depth (from input buffer)", + "why_false_positive": "explicit check (if (depth <= SHAMap::leafDepth)) validates depth (from input buffer) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "id (from input buffer)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (id == (id & depthMask(depth)))) at deserializeSHAMapNodeID", + "issue_pattern": "Missing empty string validation for id (from input buffer)", + "why_false_positive": "explicit check (if (id == (id & depthMask(depth)))) validates id (from input buffer) for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/shamap/SHAMapNodeID.cpp", + "functions": [ + { + "args": [ + "depth" + ], + "lineno": 7, + "name": "depthMask" + }, + { + "args": [], + "lineno": 38, + "name": "SHAMapNodeID::getRawString" + }, + { + "args": [ + "m" + ], + "lineno": 44, + "name": "SHAMapNodeID::getChildNodeID" + }, + { + "args": [ + "data", + "size" + ], + "lineno": 72, + "name": "deserializeSHAMapNodeID" + }, + { + "args": [ + "id", + "hash" + ], + "lineno": 92, + "name": "selectBranch" + }, + { + "args": [ + "depth", + "key" + ], + "lineno": 106, + "name": "SHAMapNodeID::createID" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::logic_error for error reporting", + "exception_type": "std::logic_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this code is likely found in SHAMapNodeID unit tests, possibly in files like 'test/shamap/SHAMapNodeID_test.cpp' or 'test/shamap/SHAMap_test.cpp'. The validation logic is well-guarded by assertions and exceptions, but coverage gaps may exist for error paths (e.g., invalid depth, invalid mask, branch index out of range). Exception-throwing branches (Throw) may not be fully covered unless negative tests are present. Serialization/deserialization edge cases (e.g., malformed input) should be explicitly tested. If fuzz or property-based tests exist, they may cover more edge cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro, Throw, explicit checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely std::logic_error or abort in debug)", + "field": "depth", + "location": "SHAMapNodeID::SHAMapNodeID (constructor)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "depth must be <= SHAMap::leafDepth" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely std::logic_error or abort in debug)", + "field": "id (hash)", + "location": "SHAMapNodeID::SHAMapNodeID (constructor)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "id must match id & depthMask(depth)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely std::logic_error or abort in debug)", + "field": "m (branch index)", + "location": "SHAMapNodeID::getChildNodeID", + "validated_by": "XRPL_ASSERT", + "validates": [ + "m must be < SHAMap::branchFactor" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely std::logic_error or abort in debug)", + "field": "depth_ (current node depth)", + "location": "SHAMapNodeID::getChildNodeID", + "validated_by": "XRPL_ASSERT", + "validates": [ + "depth_ must be <= SHAMap::leafDepth" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::logic_error", + "field": "depth_ (current node depth)", + "location": "SHAMapNodeID::getChildNodeID", + "validated_by": "Throw", + "validates": [ + "Cannot get child node if depth_ >= SHAMap::leafDepth" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "std::logic_error", + "field": "id_ (hash)", + "location": "SHAMapNodeID::getChildNodeID", + "validated_by": "Throw", + "validates": [ + "id_ must match id_ & depthMask(depth_)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns std::nullopt)", + "field": "size (input buffer size)", + "location": "deserializeSHAMapNodeID", + "validated_by": "explicit check (if (size == 33))", + "validates": [ + "input buffer must be exactly 33 bytes" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns std::nullopt)", + "field": "depth (from input buffer)", + "location": "deserializeSHAMapNodeID", + "validated_by": "explicit check (if (depth <= SHAMap::leafDepth))", + "validates": [ + "depth must be <= SHAMap::leafDepth" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns std::nullopt)", + "field": "id (from input buffer)", + "location": "deserializeSHAMapNodeID", + "validated_by": "explicit check (if (id == (id & depthMask(depth))))", + "validates": [ + "id must match id & depthMask(depth)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapNodeID.cpp.ai.md b/src/libxrpl/shamap/SHAMapNodeID.cpp.ai.md new file mode 100644 index 0000000000..c380304a67 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapNodeID.cpp.ai.md @@ -0,0 +1,57 @@ +# SHAMapNodeID.cpp + +## Role in the System + +`SHAMapNodeID.cpp` implements the node-address type for the XRPL's `SHAMap` — a 16-ary Merkle patricia trie that underpins ledger state and transaction data. Every interior node and every leaf in the tree has an address consisting of two values: a `depth_` (how many levels below the root the node sits) and an `id_` (a `uint256` encoding the path taken to reach that node). This file provides all the logic for constructing, navigating, and serializing those addresses. + +## The SHAMap Tree Shape + +The tree has exactly 65 levels: a root at depth 0 and leaves at depth 64 (`SHAMap::leafDepth`). Each step down the tree consumes one 4-bit nibble of a 256-bit key, so 64 nibbles cover all 256 bits. Each inner node has exactly 16 possible children (`SHAMap::branchFactor = SHAMapInnerNode::branchFactor = 16`). This relationship between the tree geometry and the nibble-by-nibble key encoding is the load-bearing assumption of this entire file. + +## The `depthMask()` Function + +The pivotal private helper is `depthMask(unsigned int depth)`. It returns a `uint256` bitmask with exactly the high-order bits that are meaningful at that depth set to 1 and all lower bits set to 0. The mask table has 65 entries and is computed once at program startup inside a `static` local of a struct: + +- At depth 0 (root), the mask is all-zero because the root carries no path prefix. +- At depth 1, the high nibble of byte 0 (`0xF0`) is set. +- At depth 2, the full first byte (`0xFF`) is set. +- Each subsequent pair of depths adds one nibble more. +- At depth 64 (leaf), all 32 bytes are fully set. + +The loop advances two depths at a time, writing `0xF0` for odd depths and `0xFF` for even depths into the appropriate byte. This makes the masking exact: storing a node ID requires retaining only those bits that correspond to the path traversed so far, and zeroing everything else. Two nodes at the same depth on the same branch of the tree always have identical IDs regardless of which leaf key led to them — canonicality is enforced structurally. + +## Constructor and Invariant Enforcement + +The constructor `SHAMapNodeID(unsigned int depth, uint256 const& hash)` is strict: it asserts via `XRPL_ASSERT` both that `depth <= leafDepth` and that `hash == (hash & depthMask(depth))`. This means callers are expected to pass an already-masked value. The constructor does not silently mask for the caller; it treats a non-canonical hash as a programmer error. This is a deliberate safety choice — if the trie navigation produces a hash that doesn't conform, something upstream is broken, and catching it early is preferable to silently accepting corrupted state. + +The static factory `createID(int depth, uint256 const& key)` is the one entry point that *does* mask on the caller's behalf, applying `key & depthMask(depth)` before delegating to the constructor. This is intended for building a node ID from a leaf's key: the caller knows the depth but supplies the full key, and `createID` strips the irrelevant bits. The asymmetry — constructor rejects unmasked input, factory method accepts it — makes the API's intent explicit. + +## Descending the Tree: `getChildNodeID` + +`getChildNodeID(unsigned int m)` produces the ID of child branch `m` (where `m` is 0–15). The design here is notable for its dual-layered error handling: an `XRPL_ASSERT` checks `depth_ <= leafDepth` (debug builds only), while a `Throw` also fires at runtime if `depth_ >= leafDepth`. The assert catches programmer misuse in debug mode, but the throw survives into release builds because asking for the children of a leaf is a genuine logic error that could arise from corrupted data, not just from mistakes during development. + +A second throw guards against a corrupted `id_` that doesn't conform to `depthMask(depth_)`. This is a defensive check — in a well-formed system it should never trigger, but it provides a last-resort safeguard against a node that somehow reached an inconsistent state. + +The actual bit manipulation is compact: after copying the parent's depth and ID, it OR-writes nibble `m` into the correct nibble position of `id_`: + +```cpp +node.id_.begin()[depth_ / 2] |= ((depth_ & 1) != 0u) ? m : (m << 4); +``` + +When `depth_` is even the nibble to write occupies the high 4 bits of a byte (shift left 4), and when odd it occupies the low 4 bits. This mirrors how `depthMask` sets bits, keeping the two operations perfectly paired. + +## `selectBranch`: Reading a Nibble + +`selectBranch(SHAMapNodeID const& id, uint256 const& hash)` is the inverse of `getChildNodeID`: given a hash (a full leaf key), it extracts the nibble at the node's current depth to determine which of the 16 children to follow next. It reads the appropriate byte with `*(hash.begin() + (depth / 2))`, then shifts right by 4 or masks to 4 bits depending on whether the depth is even or odd. The result is always in [0, 15], confirmed by a final `XRPL_ASSERT`. + +## Serialization and Deserialization + +`getRawString()` serializes a node ID to 33 bytes: 32 bytes for the `uint256 id_` followed by 1 byte for `depth_`. This is the "wire format" documented in the header. + +`deserializeSHAMapNodeID(void const* data, std::size_t size)` is the safe deserializer that returns `std::optional` — returning `std::nullopt` on any validation failure rather than throwing. It checks exactly three things in order: the buffer must be 33 bytes, the depth byte must be within `[0, leafDepth]`, and the 256-bit prefix must satisfy the depth mask. Only when all three pass does it call the constructor. This three-layer check matches the constructor's own assertions but operates defensively because network-received data cannot be trusted. + +## Design Tradeoffs + +The static `masks_t` lookup table avoids recomputing masks on every tree traversal. Since the SHAMap is accessed on the hot path for every ledger state lookup, the single cache-friendly array read is preferable to recomputing masks via bit arithmetic. The `static const` inside the function guarantees thread-safe initialization in C++11 and later with no explicit synchronization needed. + +The choice to make `SHAMapNodeID` copyable and comparable (full set of relational operators via `std::tie(depth_, id_)`) reflects its use as a map key and cache key throughout the XRPL node implementation — these operations need to be cheap and correct, which they are given that both fields are value types. \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapSync.cpp.ai.json b/src/libxrpl/shamap/SHAMapSync.cpp.ai.json new file mode 100644 index 0000000000..63d56c1417 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapSync.cpp.ai.json @@ -0,0 +1,570 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SHAMap::visitLeaves", + "SHAMap::visitNodes" + ], + "entry_point": "SHAMap::visitLeaves", + "purpose": "Iterates over all leaf nodes in the SHAMap and applies a user-provided function to each.", + "validation_points": [ + "SHAMap::visitNodes: if (!root_) return;" + ] + }, + { + "call_chain": [ + "SHAMap::visitNodes" + ], + "entry_point": "SHAMap::visitNodes", + "purpose": "Traverses all nodes (inner and leaf) in the SHAMap, applying a user-provided function to each.", + "validation_points": [ + "if (!root_) return;" + ] + }, + { + "call_chain": [ + "SHAMap::visitDifferences" + ], + "entry_point": "SHAMap::visitDifferences", + "purpose": "Visits every node in this SHAMap that is not present in the specified 'have' SHAMap, applying a function to each differing node.", + "validation_points": [ + "if (!root_) return;", + "if (root_->getHash().isZero()) return;", + "if ((have != nullptr) && (root_->getHash() == have->root_->getHash())) return;", + "if (have == nullptr) || !have->hasLeafNode(...)", + "if ((have == nullptr) || !have->hasInnerNode(...))" + ] + } + ], + "data_flows": [ + { + "field": "root_", + "flow": [ + "SHAMap instance", + "SHAMap::visitNodes/visitLeaves/visitDifferences", + "checked for null", + "used for traversal" + ], + "origin": "SHAMap instance (private member, set at construction or via addRootNode/deserialize)", + "transformations": [ + "Checked for null before use", + "Casted to SHAMapInnerNode or SHAMapLeafNode as needed" + ], + "validated_at": "if (!root_) return; (in all traversal functions)" + }, + { + "field": "root_->getHash()", + "flow": [ + "root_", + "getHash()", + "compared to zero or to have->root_->getHash()" + ], + "origin": "Computed from root_ node", + "transformations": [ + "Compared to zero (isZero())", + "Compared for equality with another SHAMap's root hash" + ], + "validated_at": [ + "if (root_->getHash().isZero()) return;", + "if ((have != nullptr) && (root_->getHash() == have->root_->getHash())) return;" + ] + }, + { + "field": "have (SHAMap const* have)", + "flow": [ + "visitDifferences parameter", + "checked for null", + "used for hash comparison and node existence checks" + ], + "origin": "Input parameter to visitDifferences", + "transformations": [ + "Null-checked before dereference", + "Used to call hasLeafNode/hasInnerNode" + ], + "validated_at": [ + "if ((have != nullptr) && (root_->getHash() == have->root_->getHash())) return;", + "if (have == nullptr) || !have->hasLeafNode(...)", + "if ((have == nullptr) || !have->hasInnerNode(...))" + ] + }, + { + "field": "SHAMapTreeNode* (descendNoStore/descendThrow)", + "flow": [ + "descend from parent node", + "cast to SHAMapInnerNode or SHAMapLeafNode", + "used in traversal or validation" + ], + "origin": "Derived from traversal of SHAMapInnerNode branches", + "transformations": [ + "Casted to correct node type", + "Used for further traversal or validation" + ], + "validated_at": "Type-checked via isInner()/isLeaf() before casting" + } + ], + "description": "Implements various traversal, comparison, proof, and synchronization methods for the SHAMap data structure in the XRPL (XRP Ledger) codebase.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "root_ (SHAMap root node pointer)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at SHAMap::visitNodes", + "issue_pattern": "Missing empty string validation for root_ (SHAMap root node pointer)", + "why_false_positive": "explicit null check validates root_ (SHAMap root node pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "root_ (SHAMap root node pointer)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at SHAMap::visitDifferences", + "issue_pattern": "Missing empty string validation for root_ (SHAMap root node pointer)", + "why_false_positive": "explicit null check validates root_ (SHAMap root node pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "root_->getHash()", + "empty", + "string", + "validation" + ], + "evidence": "isZero() check at SHAMap::visitDifferences", + "issue_pattern": "Missing empty string validation for root_->getHash()", + "why_false_positive": "isZero() check validates root_->getHash() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "have (input SHAMap pointer)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at SHAMap::visitDifferences", + "issue_pattern": "Missing empty string validation for have (input SHAMap pointer)", + "why_false_positive": "explicit null check validates have (input SHAMap pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "root_->getHash() == have->root_->getHash()", + "empty", + "string", + "validation" + ], + "evidence": "equality check at SHAMap::visitDifferences", + "issue_pattern": "Missing empty string validation for root_->getHash() == have->root_->getHash()", + "why_false_positive": "equality check validates root_->getHash() == have->root_->getHash() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node.isInner()", + "empty", + "string", + "validation" + ], + "evidence": "isInner() method at SHAMap::visitLeaves (lambda in visitNodes)", + "issue_pattern": "Missing empty string validation for node.isInner()", + "why_false_positive": "isInner() method validates node.isInner() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "child->isLeaf()", + "empty", + "string", + "validation" + ], + "evidence": "isLeaf() method at SHAMap::visitNodes", + "issue_pattern": "Missing empty string validation for child->isLeaf()", + "why_false_positive": "isLeaf() method validates child->isLeaf() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node->isEmptyBranch(pos)", + "empty", + "string", + "validation" + ], + "evidence": "isEmptyBranch() method at SHAMap::visitNodes", + "issue_pattern": "Missing empty string validation for node->isEmptyBranch(pos)", + "why_false_positive": "isEmptyBranch() method validates node->isEmptyBranch(pos) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "safe_downcast(node)", + "empty", + "string", + "validation" + ], + "evidence": "safe_downcast (likely asserts/casts type) at SHAMap::visitLeaves", + "issue_pattern": "Missing empty string validation for safe_downcast(node)", + "why_false_positive": "safe_downcast (likely asserts/casts type) validates safe_downcast(node) for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/shamap/SHAMapSync.cpp", + "functions": [ + { + "args": [ + "leafFunction" + ], + "lineno": 8, + "name": "SHAMap::visitLeaves" + }, + { + "args": [ + "function" + ], + "lineno": 15, + "name": "SHAMap::visitNodes" + }, + { + "args": [ + "have", + "function" + ], + "lineno": 61, + "name": "SHAMap::visitDifferences" + }, + { + "args": [ + "mn", + "se" + ], + "lineno": 110, + "name": "SHAMap::gmn_ProcessNodes" + }, + { + "args": [ + "mn" + ], + "lineno": 181, + "name": "SHAMap::gmn_ProcessDeferredReads" + }, + { + "args": [ + "max", + "filter" + ], + "lineno": 222, + "name": "SHAMap::getMissingNodes" + }, + { + "args": [ + "wanted", + "data", + "fatLeaves", + "depth" + ], + "lineno": 314, + "name": "SHAMap::getNodeFat" + }, + { + "args": [ + "s" + ], + "lineno": 377, + "name": "SHAMap::serializeRoot" + }, + { + "args": [ + "hash", + "rootNode", + "filter" + ], + "lineno": 381, + "name": "SHAMap::addRootNode" + }, + { + "args": [ + "node", + "rawNode", + "filter" + ], + "lineno": 414, + "name": "SHAMap::addKnownNode" + }, + { + "args": [ + "other" + ], + "lineno": 491, + "name": "SHAMap::deepCompare" + }, + { + "args": [ + "targetNodeID", + "targetNodeHash" + ], + "lineno": 547, + "name": "SHAMap::hasInnerNode" + }, + { + "args": [ + "tag", + "targetNodeHash" + ], + "lineno": 565, + "name": "SHAMap::hasLeafNode" + }, + { + "args": [ + "key" + ], + "lineno": 591, + "name": "SHAMap::getProofPath" + }, + { + "args": [ + "rootHash", + "key", + "path" + ], + "lineno": 617, + "name": "SHAMap::verifyProofPath" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The SHAMap traversal and difference functions are typically tested in unit tests under the rippled/test or xrplf/rippled/test directories, such as SHAMap_test.cpp, SHAMapSync_test.cpp, or Ledger_test.cpp. These tests cover basic traversal, leaf/inner node handling, and difference detection. However, edge cases such as null root_, zero hashes, and mismatched trees may not be exhaustively tested. There may be limited coverage for error handling paths (e.g., exceptions thrown by descendThrow), and for cases where 'have' is null or partially matching. Fuzzing or property-based tests may be lacking for deep or malformed trees.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, some use of safe_downcast for type safety)", + "validation_layer": "business_logic (validations occur during tree traversal, not at entry point)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (function returns early)", + "field": "root_ (SHAMap root node pointer)", + "location": "SHAMap::visitNodes", + "validated_by": "explicit null check", + "validates": [ + "root_ must not be null before traversal" + ], + "validation_type": "type (null pointer check)" + }, + { + "confidence": 1.0, + "error_thrown": "none (function returns early)", + "field": "root_ (SHAMap root node pointer)", + "location": "SHAMap::visitDifferences", + "validated_by": "explicit null check", + "validates": [ + "root_ must not be null before traversal" + ], + "validation_type": "type (null pointer check)" + }, + { + "confidence": 1.0, + "error_thrown": "none (function returns early)", + "field": "root_->getHash()", + "location": "SHAMap::visitDifferences", + "validated_by": "isZero() check", + "validates": [ + "root_ hash must not be zero" + ], + "validation_type": "business_logic (hash must be non-zero)" + }, + { + "confidence": 1.0, + "error_thrown": "none (branching logic)", + "field": "have (input SHAMap pointer)", + "location": "SHAMap::visitDifferences", + "validated_by": "explicit null check", + "validates": [ + "have may be null, logic adapts accordingly" + ], + "validation_type": "type (null pointer check)" + }, + { + "confidence": 1.0, + "error_thrown": "none (function returns early)", + "field": "root_->getHash() == have->root_->getHash()", + "location": "SHAMap::visitDifferences", + "validated_by": "equality check", + "validates": [ + "if both roots have same hash, skip traversal" + ], + "validation_type": "business_logic (skip if trees are identical)" + }, + { + "confidence": 1.0, + "error_thrown": "none (branching logic)", + "field": "node.isInner()", + "location": "SHAMap::visitLeaves (lambda in visitNodes)", + "validated_by": "isInner() method", + "validates": [ + "only process leaf nodes" + ], + "validation_type": "type (node type check)" + }, + { + "confidence": 1.0, + "error_thrown": "none (branching logic)", + "field": "child->isLeaf()", + "location": "SHAMap::visitNodes", + "validated_by": "isLeaf() method", + "validates": [ + "distinguish between leaf and inner nodes" + ], + "validation_type": "type (node type check)" + }, + { + "confidence": 1.0, + "error_thrown": "none (branching logic)", + "field": "node->isEmptyBranch(pos)", + "location": "SHAMap::visitNodes", + "validated_by": "isEmptyBranch() method", + "validates": [ + "skip empty branches in tree traversal" + ], + "validation_type": "business_logic (branch existence)" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws on bad cast (std::bad_cast or assertion)", + "field": "safe_downcast(node)", + "location": "SHAMap::visitLeaves", + "validated_by": "safe_downcast (likely asserts/casts type)", + "validates": [ + "node must be SHAMapLeafNode if not inner" + ], + "validation_type": "type (runtime type check)" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapSync.cpp.ai.md b/src/libxrpl/shamap/SHAMapSync.cpp.ai.md new file mode 100644 index 0000000000..c73a45f1f7 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapSync.cpp.ai.md @@ -0,0 +1,45 @@ +# SHAMapSync.cpp + +This file implements the network synchronization engine for the XRPL `SHAMap` — the combined 16-way radix/Merkle tree that underpins ledger state and transaction sets. While other files in the `shamap` module handle in-memory mutations and local lookups, `SHAMapSync.cpp` answers the specific question that drives the consensus protocol: *"What does a peer need to send me, or what must I send a peer, to bring a SHAMap into agreement?"* It also provides general traversal utilities used by both sync code and application logic. + +## Tree Traversal + +`visitNodes` implements a depth-first traversal using an explicit `std::stack` rather than recursion, avoiding stack exhaustion on deep maps. It feeds every tree node, inner and leaf alike, through a caller-supplied `bool`-returning functor; returning `false` exits early. The traversal skips empty branches and uses a deliberate optimization: it avoids pushing an inner node onto the stack if that node has no remaining children to explore, saving pointless push/pop cycles. `visitLeaves` is a thin wrapper that delegates to `visitNodes` and filters to leaf nodes only. + +`visitDifferences` compares two maps efficiently by leveraging the Merkle property. It short-circuits immediately if the two root hashes match — making it O(1) when maps are identical — and then uses `hasInnerNode` to prune entire subtrees that are already in agreement, visiting only nodes not present in the reference map. The `have` pointer is nullable; a null value means "report everything in `this`," which covers the bootstrap case where the peer has nothing yet. + +## Missing Node Discovery + +`getMissingNodes` is the most complex function in the file. Its job is to traverse the local map, discover which nodes are referenced but unavailable locally, and return up to `max` such (nodeID, hash) pairs. This is called repeatedly during ledger synchronization until the result is empty. + +The complexity comes from two competing demands: correctness across async I/O and throughput for heavily distributed sync. The function uses the `MissingNodes` inner struct (defined in `SHAMap.h`) to bundle all traversal state — the work stack, the set of already-discovered missing hashes, the deferred-read queue, and a "resume" map. + +**Random traversal start.** `firstChild` in each `StackEntry` is initialized to `rand_int(255)`, so multiple threads or peers calling `getMissingNodes` concurrently on the same map will produce different request sets. This is intentional: sending different requests increases the probability of acquiring distinct missing nodes per round, rather than redundantly requesting the same nodes. + +**FullBelow cache.** The `Family` object holds a generational "full below" cache that remembers which subtree roots have been confirmed locally complete. Before descending into any child, the code checks this cache via `touch_if_exists`. After fully processing an inner node with no missing children, it writes the node's hash into the cache and marks it with the current generation via `setFullBelowGen`. The generation number lets the cache age out stale entries across ledger boundaries. + +**Async I/O.** `descendAsync` attempts to fetch a child node from storage without blocking. If the read is pending, the caller (via a lambda) will be notified asynchronously, and `mn.deferred_` is incremented. When the deferred limit (`maxDefer_`, hardcoded to 512) is reached, `gmn_ProcessDeferredReads` is called. This function blocks on a `std::condition_variable`, draining all in-flight reads before resuming traversal. Nodes that complete successfully are canonicalized into the tree and added to the `resumes_` map, so the parent is revisited once the async batch is done. + +The `MissingNodes::stack_` uses `std::deque` as its underlying container rather than the default `std::vector`. This is explicitly required: raw `SHAMapInnerNode*` pointers stored in stack entries must remain valid while new entries are pushed. `std::vector` can reallocate and invalidate those pointers; `std::deque` does not. + +Two private helpers separate concerns: `gmn_ProcessNodes` handles the iterative descent and bookkeeping for a single inner node, while `gmn_ProcessDeferredReads` handles I/O completion. Both mutate the same `MissingNodes` context object. + +## Serving Nodes to Peers + +`getNodeFat` serves a requested node plus nearby descendants in a single response. The "fat" protocol amortizes round-trip latency during sync: instead of exchanging one node per message, a server bundles a subtree up to a specified depth. The depth budget is decremented only when an inner node has more than one child; single-child chains ("compressed paths" in the radix sense) are traversed for free. This means a server doesn't waste depth budget traversing structurally forced paths that carry no branching information. If `fatLeaves` is true, leaf nodes adjacent to the traversal boundary are included; otherwise only inner nodes are bundled. + +## Ingesting Nodes During Sync + +`addRootNode` and `addKnownNode` are the receiving side of the sync protocol. Both return a `SHAMapAddNode` result — a tristate of `useful` (node successfully integrated), `duplicate` (node already present), or `invalid` (node corrupt or structurally inconsistent) — letting the caller accumulate statistics across a batch of received nodes. + +`addKnownNode` performs two levels of integrity validation. The first is hash verification: the deserialized node must hash to the expected value stored in its parent branch. The second is structural: for leaf nodes, the function reconstructs the expected `SHAMapNodeID` from the leaf's actual key and verifies it matches the `SHAMapNodeID` the caller claimed. A hash collision could in theory produce a blob that hashes correctly but belongs at a different tree position; this check closes that gap. A mismatched depth or position at `leafDepth` transitions the map to `SHAMapState::Invalid`, which is the appropriate response to provably corrupt data — not a crash, but a state that the consensus engine can detect and act on. The function also skips descending into FullBelow subtrees, consistent with how `getMissingNodes` tracks completeness. + +## Merkle Proof Generation and Verification + +`getProofPath` collects every node on the path from a given leaf to the root by calling `walkTowardsKey` with a stack, then serializing each node in leaf-to-root order. The reverse ordering (leaf first) is significant: the verifier processes root first and walks down toward the leaf, matching hashes along the way. + +`verifyProofPath` is a static method — it operates on raw serialized bytes and requires no live SHAMap. It deserializes each blob in root-to-leaf order, verifies the hash matches the expected value, then selects the child hash for the next step using `selectBranch`. The entire deserialization is wrapped in a try/catch because the path data originates from the network and may be malformed. The function also enforces a path length bound of 65, matching the maximum tree depth (64 inner levels plus one leaf). + +## Relationship to the Broader Module + +All traversal paths ultimately call `descendThrow` or `descendNoStore` — functions defined elsewhere in the module that handle cache lookups, database fetches, and the copy-on-write protocol. This file is deliberately free of those details; it focuses on tree-level logic and assumes the descent primitives provide correctly typed, non-null nodes or throw on failure. The `SHAMapSyncFilter` interface is the seam between this code and the application layer: it lets the caller inject an alternative node source (e.g., in-progress relay data) and receive notifications when nodes are integrated, without this file depending on any specific storage backend. \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.json b/src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.json new file mode 100644 index 0000000000..145a26f055 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.json @@ -0,0 +1,427 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SHAMapTreeNode::makeFromWire", + "SHAMapTreeNode::makeTransaction | makeAccountState | makeTransactionWithMeta | SHAMapInnerNode::makeFullInner | SHAMapInnerNode::makeCompressedInner" + ], + "entry_point": "SHAMapTreeNode::makeFromWire", + "purpose": "Deserializes a wire-format node and dispatches to the appropriate node constructor based on type byte.", + "validation_points": [ + "SHAMapTreeNode::makeTransactionWithMeta: size check, getBitString", + "SHAMapTreeNode::makeAccountState: size check, getBitString, tag.isZero" + ] + }, + { + "call_chain": [ + "SHAMapTreeNode::makeFromPrefix", + "SHAMapTreeNode::makeTransaction | makeAccountState | makeTransactionWithMeta | SHAMapInnerNode::makeFullInner | SHAMapInnerNode::makeCompressedInner" + ], + "entry_point": "SHAMapTreeNode::makeFromPrefix", + "purpose": "Deserializes a node with a 4-byte prefix and dispatches to the appropriate node constructor.", + "validation_points": [ + "SHAMapTreeNode::makeFromPrefix: rawNode.size() < 4", + "SHAMapTreeNode::makeTransactionWithMeta: size check, getBitString", + "SHAMapTreeNode::makeAccountState: size check, getBitString, tag.isZero" + ] + }, + { + "call_chain": [ + "SHAMapTreeNode::makeTransactionWithMeta" + ], + "entry_point": "SHAMapTreeNode::makeTransactionWithMeta", + "purpose": "Constructs a transaction-with-metadata leaf node from serialized data.", + "validation_points": [ + "size check (s.size() < tag.bytes)", + "getBitString (s.getBitString(tag, s.size() - tag.bytes))" + ] + }, + { + "call_chain": [ + "SHAMapTreeNode::makeAccountState" + ], + "entry_point": "SHAMapTreeNode::makeAccountState", + "purpose": "Constructs an account state leaf node from serialized data.", + "validation_points": [ + "size check (s.size() < tag.bytes)", + "getBitString (s.getBitString(tag, s.size() - tag.bytes))", + "tag.isZero()" + ] + } + ], + "data_flows": [ + { + "field": "data (Slice)", + "flow": [ + "Input to makeFromWire/makeFromPrefix", + "Passed to makeTransaction/makeTransactionWithMeta/makeAccountState", + "Wrapped in Serializer s", + "Used for size checks and getBitString", + "Chopped and passed to make_shamapitem", + "Used to construct SHAMap*LeafNode" + ], + "origin": "Input parameter to makeTransaction, makeTransactionWithMeta, makeAccountState, makeFromWire, makeFromPrefix", + "transformations": [ + "Wrapped in Serializer", + "Chopped (s.chop(tag.bytes))", + "Sliced (s.slice())" + ], + "validated_at": "size check (s.size() < tag.bytes), getBitString" + }, + { + "field": "tag (uint256)", + "flow": [ + "Declared", + "Filled by getBitString", + "Used as key in make_shamapitem" + ], + "origin": "Declared in makeTransactionWithMeta and makeAccountState", + "transformations": [ + "Filled by getBitString", + "Checked for isZero (in makeAccountState)" + ], + "validated_at": "getBitString, tag.isZero()" + }, + { + "field": "rawNode (Slice)", + "flow": [ + "Input", + "Type byte or prefix extracted", + "Suffix/prefix removed", + "Passed as data to makeTransaction/makeAccountState/makeTransactionWithMeta" + ], + "origin": "Input to makeFromWire/makeFromPrefix", + "transformations": [ + "remove_suffix(1) or remove_prefix(4)" + ], + "validated_at": "makeFromWire: rawNode.empty(), makeFromPrefix: rawNode.size() < 4" + } + ], + "description": "Implements factory methods for creating various types of SHAMapTreeNode objects from serialized data, handling different node types (transaction, account state, inner, etc.) for the XRPL SHAMap structure.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "data (Slice)", + "empty", + "string", + "validation" + ], + "evidence": "manual size check (s.size() < tag.bytes) at SHAMapTreeNode::makeTransactionWithMeta", + "issue_pattern": "Missing empty string validation for data (Slice)", + "why_false_positive": "manual size check (s.size() < tag.bytes) validates data (Slice) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "data (Slice)", + "range", + "bounds", + "validation" + ], + "evidence": "manual size check (s.size() < tag.bytes) at SHAMapTreeNode::makeTransactionWithMeta", + "issue_pattern": "Missing range validation for data (Slice)", + "why_false_positive": "manual size check (s.size() < tag.bytes) validates data (Slice) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "data (Slice)", + "empty", + "string", + "validation" + ], + "evidence": "s.getBitString(tag, s.size() - tag.bytes) at SHAMapTreeNode::makeTransactionWithMeta", + "issue_pattern": "Missing empty string validation for data (Slice)", + "why_false_positive": "s.getBitString(tag, s.size() - tag.bytes) validates data (Slice) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "data (Slice)", + "format", + "validation", + "invalid" + ], + "evidence": "s.getBitString(tag, s.size() - tag.bytes) at SHAMapTreeNode::makeTransactionWithMeta", + "issue_pattern": "Missing format validation for data (Slice)", + "why_false_positive": "s.getBitString(tag, s.size() - tag.bytes) validates data (Slice) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "data (Slice)", + "empty", + "string", + "validation" + ], + "evidence": "manual size check (s.size() < tag.bytes) at SHAMapTreeNode::makeAccountState", + "issue_pattern": "Missing empty string validation for data (Slice)", + "why_false_positive": "manual size check (s.size() < tag.bytes) validates data (Slice) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "data (Slice)", + "range", + "bounds", + "validation" + ], + "evidence": "manual size check (s.size() < tag.bytes) at SHAMapTreeNode::makeAccountState", + "issue_pattern": "Missing range validation for data (Slice)", + "why_false_positive": "manual size check (s.size() < tag.bytes) validates data (Slice) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "data (Slice)", + "empty", + "string", + "validation" + ], + "evidence": "s.getBitString(tag, s.size() - tag.bytes) at SHAMapTreeNode::makeAccountState", + "issue_pattern": "Missing empty string validation for data (Slice)", + "why_false_positive": "s.getBitString(tag, s.size() - tag.bytes) validates data (Slice) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "data (Slice)", + "format", + "validation", + "invalid" + ], + "evidence": "s.getBitString(tag, s.size() - tag.bytes) at SHAMapTreeNode::makeAccountState", + "issue_pattern": "Missing format validation for data (Slice)", + "why_false_positive": "s.getBitString(tag, s.size() - tag.bytes) validates data (Slice) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tag (uint256)", + "empty", + "string", + "validation" + ], + "evidence": "tag.isZero() at SHAMapTreeNode::makeAccountState", + "issue_pattern": "Missing empty string validation for tag (uint256)", + "why_false_positive": "tag.isZero() validates tag (uint256) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rawNode (Slice)", + "empty", + "string", + "validation" + ], + "evidence": "rawNode.empty() at SHAMapTreeNode::makeFromWire", + "issue_pattern": "Missing empty string validation for rawNode (Slice)", + "why_false_positive": "rawNode.empty() validates rawNode (Slice) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "rawNode (Slice)", + "format", + "validation", + "invalid" + ], + "evidence": "rawNode.empty() at SHAMapTreeNode::makeFromWire", + "issue_pattern": "Missing format validation for rawNode (Slice)", + "why_false_positive": "rawNode.empty() validates rawNode (Slice) format" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/shamap/SHAMapTreeNode.cpp", + "functions": [ + { + "args": [ + "Slice data", + "SHAMapHash const& hash", + "bool hashValid" + ], + "lineno": 11, + "name": "SHAMapTreeNode::makeTransaction" + }, + { + "args": [ + "Slice data", + "SHAMapHash const& hash", + "bool hashValid" + ], + "lineno": 20, + "name": "SHAMapTreeNode::makeTransactionWithMeta" + }, + { + "args": [ + "Slice data", + "SHAMapHash const& hash", + "bool hashValid" + ], + "lineno": 41, + "name": "SHAMapTreeNode::makeAccountState" + }, + { + "args": [ + "Slice rawNode" + ], + "lineno": 62, + "name": "SHAMapTreeNode::makeFromWire" + }, + { + "args": [ + "Slice rawNode", + "SHAMapHash const& hash" + ], + "lineno": 89, + "name": "SHAMapTreeNode::makeFromPrefix" + }, + { + "args": [ + "SHAMapNodeID const& id" + ], + "lineno": 115, + "name": "SHAMapTreeNode::getString" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by SHAMap and SHAMapTreeNode unit tests, typically found in files like SHAMap_test.cpp, SHAMapTreeNode_test.cpp, or SHAMapInnerNode_test.cpp. These tests would exercise deserialization, error handling, and node construction. However, edge cases such as very short input slices, invalid tag values (e.g., tag.isZero()), and malformed wire types may not be exhaustively tested unless explicitly covered. The FIXME comments suggest some validation is ad hoc and could be missed by tests. There is no evidence of fuzz or property-based testing for malformed input, which could leave gaps in coverage for validation failures.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "manual validation (no external framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "data (Slice)", + "location": "SHAMapTreeNode::makeTransactionWithMeta", + "validated_by": "manual size check (s.size() < tag.bytes)", + "validates": [ + "Ensures input data is at least as large as tag.bytes (32 bytes for uint256)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::out_of_range", + "field": "data (Slice)", + "location": "SHAMapTreeNode::makeTransactionWithMeta", + "validated_by": "s.getBitString(tag, s.size() - tag.bytes)", + "validates": [ + "Ensures getBitString can extract a uint256 from the data at the expected offset" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "data (Slice)", + "location": "SHAMapTreeNode::makeAccountState", + "validated_by": "manual size check (s.size() < tag.bytes)", + "validates": [ + "Ensures input data is at least as large as tag.bytes (32 bytes for uint256)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::out_of_range", + "field": "data (Slice)", + "location": "SHAMapTreeNode::makeAccountState", + "validated_by": "s.getBitString(tag, s.size() - tag.bytes)", + "validates": [ + "Ensures getBitString can extract a uint256 from the data at the expected offset" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "tag (uint256)", + "location": "SHAMapTreeNode::makeAccountState", + "validated_by": "tag.isZero()", + "validates": [ + "Ensures tag is not all zeroes (invalid account state node)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns empty shared_ptr (no exception)", + "field": "rawNode (Slice)", + "location": "SHAMapTreeNode::makeFromWire", + "validated_by": "rawNode.empty()", + "validates": [ + "Ensures input is not empty before parsing" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.md b/src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.md new file mode 100644 index 0000000000..0c8e762cb9 --- /dev/null +++ b/src/libxrpl/shamap/SHAMapTreeNode.cpp.ai.md @@ -0,0 +1,45 @@ +# `SHAMapTreeNode.cpp` — Node Deserialization Factory + +## Overview + +`SHAMapTreeNode.cpp` provides the implementation of the static factory methods declared on the abstract `SHAMapTreeNode` base class. Its sole job is to reconstruct the correct concrete node object from raw serialized bytes — whether arriving over the peer-to-peer network wire, or being loaded from storage with an already-verified hash. The file contains no data members, no virtual dispatch, and no tree-walking logic; it is purely a deserialization layer that bridges raw bytes and the node type hierarchy. + +## Node Type Hierarchy + +`SHAMapTreeNode` is the abstract root of a four-class hierarchy. Leaf variants — `SHAMapTxLeafNode`, `SHAMapTxPlusMetaLeafNode`, and `SHAMapAccountStateLeafNode` — each hold a `SHAMapItem` (a ref-counted, slab-allocated object storing a 256-bit key and an opaque payload). `SHAMapInnerNode` is the branching node with up to sixteen child slots. The factory methods in this file instantiate all four concrete types but declare `makeTransaction`, `makeAccountState`, and `makeTransactionWithMeta` as `private` on the header, keeping them as internal routing helpers called only from the two public entry points. + +## Two Serialization Formats + +The two public entry points correspond to two different on-wire representations: + +**`makeFromWire(Slice rawNode)`** handles the legacy wire format. The type discriminant is a single byte appended to the *end* of the buffer (`rawNode[rawNode.size() - 1]`), and the five possible values are the `wireType*` constants defined in `SHAMapTreeNode.h` (e.g., `wireTypeTransaction = 0`, `wireTypeAccountState = 1`). The slice is trimmed of that trailing byte with `remove_suffix(1)` before being passed downstream. Because the wire format carries no pre-computed hash, `makeFromWire` always passes `hashValid = false`, letting each concrete node's constructor call `updateHash()` to compute the hash from scratch. + +**`makeFromPrefix(Slice rawNode, SHAMapHash const& hash)`** handles the prefixed format used for hash verification. Here the first four bytes encode a `HashPrefix` enum value (the canonical domain-separation prefix used throughout the XRP Ledger's hashing scheme), extracted with explicit big-endian byte arithmetic and then stripped via `remove_prefix(4)`. Because the hash is supplied externally (already verified by the caller), `hashValid = true` is passed through, bypassing recomputation in the leaf constructors. This is the path taken when retrieving nodes from a trusted node store or receiving them from a sync partner that already attested to their hash. + +The `hashValid` flag flows all the way into the leaf constructors via overloaded forms: when `false`, the single-argument constructor calls `updateHash()` immediately; when `true`, the two-argument constructor takes the hash directly into `hash_` and skips recomputation. + +## Tag Extraction + +The three leaf factories differ in how they derive the `SHAMapItem` key (`tag`): + +- **`makeTransaction`** computes the key on-the-fly: `sha512Half(HashPrefix::transactionID, data)`. The raw transaction bytes carry no embedded key — the key *is* the hash of the content. This is consistent with `SHAMapTxLeafNode::updateHash()`, which uses the same formula. + +- **`makeTransactionWithMeta`** and **`makeAccountState`** extract a 32-byte `uint256` tag that is appended to the *tail* of the payload during serialization (see `serializeForWire()` in both leaf node headers, which calls `s.addBitString(item_->key())`). Both helpers use `s.getBitString(tag, s.size() - tag.bytes)` to read those last 32 bytes, then `s.chop(tag.bytes)` to remove them before creating the item. This separation is necessary because `SHAMapItem` stores the key and the payload separately, so the key must be peeled off before the `make_shamapitem` call. + +The `isZero()` guard in `makeAccountState` is a business-logic invariant: a zero `uint256` is not a valid ledger object identity and would represent a corrupted or malicious node. No equivalent guard exists for transaction nodes because the transaction key is computed, not loaded. + +## Validation and Error Handling + +Each path validates its input at the earliest possible point: + +- `makeFromWire` returns a null `intr_ptr::SharedPtr{}` on empty input rather than throwing — the caller is expected to treat null as "no node". Unknown type bytes throw `std::runtime_error`. +- `makeFromPrefix` requires at least 4 bytes for the prefix; shorter input throws immediately. +- Both `makeTransactionWithMeta` and `makeAccountState` perform an explicit size check before calling `getBitString`, throwing `std::runtime_error("Short TXN+MD node")` or `std::runtime_error("short AS node")` respectively. The subsequent `getBitString` call provides a second layer of validation (returning false on failure, which throws `std::out_of_range`). Both layers are noted with `// FIXME: improve this interface` — the manual pre-check exists because the `getBitString` API does not distinguish between a size-zero input and a successful zero read. + +## Copy-on-Write Semantics + +Every factory method constructs nodes with `cowid = 0`. In the SHAMap copy-on-write design, `cowid_ == 0` signals that a node is not dirty and not exclusively owned by any map, making it immediately eligible for sharing across multiple `SHAMap` instances. Newly deserialized nodes are inherently read-only — they haven't been modified by any map — so starting them as unowned is the correct default. A node becomes owned (and non-shareable) only when a map needs to mutate it, at which point it is cloned with a non-zero `cowid`. + +## `getString` + +The `getString(SHAMapNodeID const&)` override is a trivial delegation to `to_string(id)`, providing a human-readable positional description for debugging and logging. It is the only non-factory method implemented in this file and has no architectural significance. \ No newline at end of file diff --git a/src/libxrpl/tx/ApplyContext.cpp.ai.json b/src/libxrpl/tx/ApplyContext.cpp.ai.json new file mode 100644 index 0000000000..d1f0c5c0f7 --- /dev/null +++ b/src/libxrpl/tx/ApplyContext.cpp.ai.json @@ -0,0 +1,455 @@ +{ + "args": [ + { + "lineno": 11, + "name": "registry_" + }, + { + "lineno": 12, + "name": "base" + }, + { + "lineno": 13, + "name": "parentBatchId" + }, + { + "lineno": 14, + "name": "tx_" + }, + { + "lineno": 15, + "name": "preclaimResult_" + }, + { + "lineno": 16, + "name": "baseFee_" + }, + { + "lineno": 17, + "name": "flags" + }, + { + "lineno": 18, + "name": "journal_" + }, + { + "lineno": 34, + "name": "ter" + }, + { + "lineno": 45, + "name": "func" + }, + { + "lineno": 54, + "name": "result" + }, + { + "lineno": 68, + "name": "fee" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ApplyContext::ApplyContext" + ], + "entry_point": "ApplyContext::ApplyContext", + "purpose": "Constructor for ApplyContext, sets up context for transaction application, including validation of batch flag and parentBatchId.", + "validation_points": [ + "XRPL_ASSERT in constructor: Validates that parentBatchId is set if and only if tapBATCH flag is set." + ] + }, + { + "call_chain": [ + "ApplyContext::apply", + "ApplyViewImpl::apply" + ], + "entry_point": "ApplyContext::apply", + "purpose": "Applies the transaction to the ledger view, possibly as a dry run, and returns transaction metadata.", + "validation_points": [ + "Indirect: ApplyViewImpl::apply may perform further validation, but not shown in this file." + ] + }, + { + "call_chain": [ + "ApplyContext::checkInvariants", + "ApplyContext::checkInvariantsHelper", + "ApplyContext::visit", + "InvariantCheck::visitEntry (for each checker)", + "InvariantCheck::finalize (for each checker)" + ], + "entry_point": "ApplyContext::checkInvariants", + "purpose": "Checks all transaction invariants after application, logging and failing if any are violated.", + "validation_points": [ + "XRPL_ASSERT in checkInvariants: Ensures result is tesSUCCESS or tecCLAIM.", + "InvariantCheck::finalize: Each invariant check validates the transaction.", + "Exception handling: Any exception in invariant checks is caught and triggers failInvariantCheck." + ] + }, + { + "call_chain": [ + "ApplyContext::failInvariantCheck" + ], + "entry_point": "ApplyContext::failInvariantCheck", + "purpose": "Maps invariant check failures to the correct TER code.", + "validation_points": [ + "Direct: Maps tecINVARIANT_FAILED/tefINVARIANT_FAILED to tefINVARIANT_FAILED." + ] + } + ], + "data_flows": [ + { + "field": "parentBatchId_", + "flow": [ + "constructor argument", + "assigned to member parentBatchId_", + "used in XRPL_ASSERT for validation", + "passed to ApplyViewImpl::apply in ApplyContext::apply" + ], + "origin": "ApplyContext constructor argument", + "transformations": [ + "Validated for presence/absence against tapBATCH flag" + ], + "validated_at": "ApplyContext constructor (XRPL_ASSERT)" + }, + { + "field": "flags_", + "flow": [ + "constructor argument", + "assigned to member flags_", + "used in XRPL_ASSERT for validation", + "used to determine dry run in ApplyContext::apply", + "used to initialize view_" + ], + "origin": "ApplyContext constructor argument", + "transformations": [ + "Bitwise checked for tapBATCH and tapDRY_RUN" + ], + "validated_at": "ApplyContext constructor (XRPL_ASSERT)" + }, + { + "field": "tx", + "flow": [ + "constructor argument", + "assigned to member tx", + "passed to ApplyViewImpl::apply", + "passed to InvariantCheck::finalize", + "used in logging" + ], + "origin": "ApplyContext constructor argument (STTx const& tx_)", + "transformations": [ + "No transformation, but used as input to multiple downstream checks" + ], + "validated_at": "Not directly validated here; assumed validated elsewhere" + }, + { + "field": "result (TER)", + "flow": [ + "passed to checkInvariants", + "asserted to be tesSUCCESS or tecCLAIM", + "passed to checkInvariantsHelper", + "passed to InvariantCheck::finalize", + "possibly transformed by failInvariantCheck" + ], + "origin": "Argument to checkInvariants/checkInvariantsHelper", + "transformations": [ + "May be mapped to tefINVARIANT_FAILED by failInvariantCheck" + ], + "validated_at": "checkInvariants (XRPL_ASSERT)" + }, + { + "field": "fee (XRPAmount)", + "flow": [ + "passed to checkInvariants", + "passed to checkInvariantsHelper", + "passed to InvariantCheck::finalize" + ], + "origin": "Argument to checkInvariants/checkInvariantsHelper", + "transformations": [ + "No transformation in this file" + ], + "validated_at": "Not directly validated here" + } + ], + "description": "Implements the ApplyContext class for transaction application logic in the XRPL codebase, handling transaction application, invariant checks, and view management.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "tx (STTx const&): type-checked by C++", + "validation", + "missing", + "check" + ], + "evidence": "Field tx (STTx const&): type-checked by C++ validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for tx (STTx const&): type-checked by C++", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates tx (STTx const&): type-checked by C++ automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "base (OpenView&): type-checked by C++", + "validation", + "missing", + "check" + ], + "evidence": "Field base (OpenView&): type-checked by C++ validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for base (OpenView&): type-checked by C++", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates base (OpenView&): type-checked by C++ automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "preclaimResult_ (TER): type-checked by C++", + "validation", + "missing", + "check" + ], + "evidence": "Field preclaimResult_ (TER): type-checked by C++ validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for preclaimResult_ (TER): type-checked by C++", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates preclaimResult_ (TER): type-checked by C++ automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "baseFee_ (XRPAmount): type-checked by C++", + "validation", + "missing", + "check" + ], + "evidence": "Field baseFee_ (XRPAmount): type-checked by C++ validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for baseFee_ (XRPAmount): type-checked by C++", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates baseFee_ (XRPAmount): type-checked by C++ automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "flags (ApplyFlags): type-checked by C++", + "validation", + "missing", + "check" + ], + "evidence": "Field flags (ApplyFlags): type-checked by C++ validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for flags (ApplyFlags): type-checked by C++", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates flags (ApplyFlags): type-checked by C++ automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "journal (beast::Journal): type-checked by C++", + "validation", + "missing", + "check" + ], + "evidence": "Field journal (beast::Journal): type-checked by C++ validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for journal (beast::Journal): type-checked by C++", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates journal (beast::Journal): type-checked by C++ automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "parentBatchId and flags_ (tapBATCH)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at ApplyContext constructor", + "issue_pattern": "Missing empty string validation for parentBatchId and flags_ (tapBATCH)", + "why_false_positive": "XRPL_ASSERT macro validates parentBatchId and flags_ (tapBATCH) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/ApplyContext.cpp", + "functions": [ + { + "args": [ + "ServiceRegistry& registry_", + "OpenView& base", + "std::optional const& parentBatchId", + "STTx const& tx_", + "TER preclaimResult_", + "XRPAmount baseFee_", + "ApplyFlags flags", + "beast::Journal journal_" + ], + "lineno": 10, + "name": "ApplyContext::ApplyContext" + }, + { + "args": [], + "lineno": 28, + "name": "ApplyContext::discard" + }, + { + "args": [ + "TER ter" + ], + "lineno": 33, + "name": "ApplyContext::apply" + }, + { + "args": [], + "lineno": 39, + "name": "ApplyContext::size" + }, + { + "args": [ + "std::function const&, std::shared_ptr const&)> const& func" + ], + "lineno": 44, + "name": "ApplyContext::visit" + }, + { + "args": [ + "TER const result" + ], + "lineno": 53, + "name": "ApplyContext::failInvariantCheck" + }, + { + "args": [ + "TER const result", + "XRPAmount const fee", + "std::index_sequence" + ], + "lineno": 66, + "name": "ApplyContext::checkInvariantsHelper" + }, + { + "args": [ + "TER const result", + "XRPAmount const fee" + ], + "lineno": 104, + "name": "ApplyContext::checkInvariants" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic in ApplyContext (especially the XRPL_ASSERT for parentBatchId/flags_ and the invariant checks) is likely covered by integration and unit tests for transaction application and batch processing. Tests would exist in files such as 'test/tx/apply_test.cpp', 'test/tx/invariant_test.cpp', or similar. However, direct unit tests for the constructor's XRPL_ASSERT and for failInvariantCheck's mapping logic may be limited. Exception handling in checkInvariantsHelper may not be fully tested unless invariants are deliberately made to throw. There may be gaps in testing for edge cases where parentBatchId and tapBATCH flag mismatches occur, or for rare invariant check failures.", + "validation_architecture": { + "auto_validated_fields": [ + "tx (STTx const&): type-checked by C++", + "base (OpenView&): type-checked by C++", + "preclaimResult_ (TER): type-checked by C++", + "baseFee_ (XRPAmount): type-checked by C++", + "flags (ApplyFlags): type-checked by C++", + "journal (beast::Journal): type-checked by C++" + ], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system", + "validation_layer": "entry_point (constructor)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "parentBatchId and flags_ (tapBATCH)", + "location": "ApplyContext constructor", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "parentBatchId must be set if and only if tapBATCH flag is set in flags_" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/ApplyContext.cpp.ai.md b/src/libxrpl/tx/ApplyContext.cpp.ai.md new file mode 100644 index 0000000000..f4bcfb9df6 --- /dev/null +++ b/src/libxrpl/tx/ApplyContext.cpp.ai.md @@ -0,0 +1,67 @@ +## `ApplyContext.cpp` — Transaction Application Context and Invariant Enforcement + +### Role in the System + +`ApplyContext` sits at the heart of XRPL's transaction processing pipeline. It owns the sandboxed ledger view that a `Transactor` writes into during transaction execution, and it is the last line of defense before any corrupted state can reach a finalized ledger. When a transaction's own logic fails to prevent an illegal ledger mutation — whether due to a bug or an attempted exploit — `ApplyContext` catches it through a battery of invariant checks applied after every apply attempt. + +The class is constructed once per transaction application attempt by the apply layer (`apply.cpp`/`applySteps.cpp`), after preflight and preclaim have already passed. By the time `ApplyContext` exists, the transaction has been declared structurally sound and account-eligible; the remaining question is whether executing it produces a valid ledger state. + +### Construction and the Sandboxed View + +The constructor takes the live `OpenView` (`base_`), the pre-validated `STTx`, the preclaim result, the base fee, and apply flags. Its first meaningful act is to `emplace` an `ApplyViewImpl` (`view_`) on top of `base_`. This indirection is fundamental to the transaction safety model: the transactor reads and writes exclusively through `view_`, never directly against `base_`. No ledger mutation escapes the sandbox until the explicit `apply()` call merges the deltas. + +The constructor asserts that `parentBatchId` is populated if and only if `tapBATCH` is set in flags: + +```cpp +XRPL_ASSERT( + parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH), + "Parent Batch ID should be set if batch apply flag is set"); +``` + +This enforces a hard invariant around batch transaction processing — both pieces of batch context must either be present or absent together. The header also offers a convenience constructor for non-batch callers that omits `parentBatchId` entirely and asserts the flag is clear, removing any possibility of accidentally constructing a half-batch context. + +### `discard()` — Cheap State Rollback + +`discard()` simply re-emplaces a fresh `ApplyViewImpl` on top of `base_`, discarding all accumulated changes. Because the sandbox has never touched `base_`, this rollback costs only object construction. `Transactor` uses this mechanism when it needs to retry after an invariant failure: it attempts a full apply, detects a broken invariant, calls `discard()` to wipe the sandbox, then re-applies with fee-only logic before checking invariants a second time. + +### `apply()` — Committing the Sandbox + +When the transactor is satisfied with the transaction result, `apply()` delegates to `view_->apply()`, which merges the sandbox's ledger entry deltas into the live `base_`. The call forwards both `parentBatchId_` and the dry-run flag (`tapDRY_RUN`), so the commit step remains aware of batch context and simulation mode — keeping those concerns cleanly separated from the transactor itself. + +### Invariant Checking Architecture + +The invariant checking machinery in `checkInvariantsHelper` is the most architecturally significant part of this file. It uses compile-time polymorphism — a `std::tuple` of checker objects and an `std::index_sequence` — to iterate all registered invariant checkers without virtual dispatch or runtime polymorphism overhead. + +`getInvariantChecks()` (defined in `InvariantCheck.h`) returns a fresh `InvariantChecks` tuple containing 25 distinct checker types, ranging from `XRPNotCreated` and `AccountRootsNotDeleted` to `ValidVault` and `ValidLoan`. Each checker implements `visitEntry()` to accumulate per-entry state and `finalize()` to render a pass/fail verdict. `checkInvariantsHelper` drives both phases: + +First, it uses a fold expression to call `visitEntry` on every checker for every modified ledger entry via the `visit()` delegation: + +```cpp +visit([&checkers](...) { + (..., std::get(checkers).visitEntry(isDelete, before, after)); +}); +``` + +Then it finalizes all checkers into an array of booleans: + +```cpp +std::array const finalizers{ + {std::get(checkers).finalize(tx, result, fee, *view_, journal)...}}; +``` + +The critical design decision here is that the finalize step is **not** a `...&&` fold expression. A fold would short-circuit after the first failure, silencing subsequent ones. The array-then-`all_of` pattern ensures every failing invariant logs its own diagnostic message before a verdict is returned. In a production incident where multiple invariants fail simultaneously — which indicates a serious regression or exploit — having all failures visible is essential for diagnosis. + +The invariant checkers run even on failed (`tec*`) transactions. As the `InvariantChecker_PROTOTYPE` documentation explains, bugs or exploits could cause a failed transaction to mutate ledger state in unexpected ways; invariants must defend against that possibility regardless of the transaction result code. + +### The `failInvariantCheck()` Escalation Logic + +When invariants fail, the severity of the returned code reflects how far through the retry sequence the caller is: + +- A first-time failure from a normal result produces `tecINVARIANT_FAILED`, which **is** included in the ledger — the sender is still charged a fee for the invalid transaction attempt. +- If invariants fail again during a fee-only retry (recognized because the incoming result is already `tecINVARIANT_FAILED` or `tefINVARIANT_FAILED`), the function escalates to `tefINVARIANT_FAILED`, which does **not** get included in a ledger. + +The rationale is explicit in the code comments: if even the minimal fee-charge path breaks invariants, something is wrong enough that no ledger entry of any kind should be created. This two-tier approach balances the normal goal of fee recovery against the impossibility of safely applying anything when the transaction's behavior is deeply unpredictable. + +### Relationship to Sibling Files + +`ApplyContext` is consumed primarily by `Transactor.cpp`, which holds a reference to it throughout the apply phase and calls `checkInvariants()` after `doApply()` returns. The `apply.cpp` and `applySteps.cpp` files orchestrate the higher-level flow that constructs an `ApplyContext` and dispatches it to the correct transactor subclass. The invariant definitions themselves live in `src/libxrpl/tx/invariants/`, with `InvariantCheck.h` providing the `InvariantChecks` tuple type and the `getInvariantChecks()` factory used here at runtime. \ No newline at end of file diff --git a/src/libxrpl/tx/SignerEntries.cpp.ai.json b/src/libxrpl/tx/SignerEntries.cpp.ai.json new file mode 100644 index 0000000000..7dc0082059 --- /dev/null +++ b/src/libxrpl/tx/SignerEntries.cpp.ai.json @@ -0,0 +1,187 @@ +{ + "args": [ + { + "lineno": 9, + "name": "obj" + }, + { + "lineno": 9, + "name": "journal" + }, + { + "lineno": 9, + "name": "annotation" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SignerEntries::deserialize" + ], + "entry_point": "SignerEntries::deserialize", + "purpose": "Deserializes and validates the sfSignerEntries array from an STObject, extracting SignerEntry fields and returning a vector of SignerEntry objects or an error.", + "validation_points": [ + "if (!obj.isFieldPresent(sfSignerEntries))", + "if (sEntry.getFName() != sfSignerEntry)" + ] + } + ], + "data_flows": [ + { + "field": "sfSignerEntries", + "flow": [ + "obj (STObject)", + "obj.isFieldPresent(sfSignerEntries) (validation)", + "obj.getFieldArray(sfSignerEntries) (extract STArray)", + "sEntries (STArray of STObject)", + "for each sEntry in sEntries" + ], + "origin": "Input STObject parameter 'obj'", + "transformations": [ + "Checked for presence (validation)", + "Extracted as STArray" + ], + "validated_at": "if (!obj.isFieldPresent(sfSignerEntries))" + }, + { + "field": "sfSignerEntry", + "flow": [ + "sEntry (STObject in sEntries)", + "sEntry.getFName() (validation)" + ], + "origin": "Each STObject in sEntries (STArray)", + "transformations": [ + "Checked that sEntry is of type sfSignerEntry" + ], + "validated_at": "if (sEntry.getFName() != sfSignerEntry)" + }, + { + "field": "sfAccount", + "flow": [ + "sEntry.getAccountID(sfAccount)", + "account (AccountID)" + ], + "origin": "Each sEntry (STObject in sEntries)", + "transformations": [ + "Extracted as AccountID" + ], + "validated_at": "No explicit validation in this function" + }, + { + "field": "sfSignerWeight", + "flow": [ + "sEntry.getFieldU16(sfSignerWeight)", + "weight (uint16_t)" + ], + "origin": "Each sEntry (STObject in sEntries)", + "transformations": [ + "Extracted as uint16_t" + ], + "validated_at": "No explicit validation in this function" + }, + { + "field": "sfWalletLocator", + "flow": [ + "sEntry.at(~sfWalletLocator)", + "tag (optional)" + ], + "origin": "Each sEntry (STObject in sEntries)", + "transformations": [ + "Extracted as optional" + ], + "validated_at": "No explicit validation in this function" + } + ], + "description": "Implements deserialization and validation of SignerEntries from an STObject for XRPL transactions, ensuring correct structure and extracting relevant fields.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSignerEntries", + "empty", + "string", + "validation" + ], + "evidence": "obj.isFieldPresent(sfSignerEntries) at SignerEntries::deserialize", + "issue_pattern": "Missing empty string validation for sfSignerEntries", + "why_false_positive": "obj.isFieldPresent(sfSignerEntries) validates sfSignerEntries for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSignerEntry", + "empty", + "string", + "validation" + ], + "evidence": "sEntry.getFName() != sfSignerEntry at SignerEntries::deserialize (inside for loop over sEntries)", + "issue_pattern": "Missing empty string validation for sfSignerEntry", + "why_false_positive": "sEntry.getFName() != sfSignerEntry validates sfSignerEntry for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/SignerEntries.cpp", + "functions": [ + { + "args": [ + "STObject const& obj", + "beast::Journal journal", + "std::string_view annotation" + ], + "lineno": 9, + "name": "SignerEntries::deserialize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "This function is likely tested indirectly via transaction processing and multi-signature transaction tests. Look for tests in files such as 'test/tx/SignerEntries_test.cpp', 'test/tx/SetSignerList_test.cpp', or integration tests involving multi-signing. Direct unit tests for malformed or missing sfSignerEntries, or for malformed SignerEntry objects, should exist. Gaps may include: missing tests for edge cases (e.g., empty arrays, invalid field types, duplicate entries), and lack of explicit tests for the optional sfWalletLocator field. No evidence in this file of test hooks or test-specific code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation using xrpl protocol classes (STObject, STArray)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Unexpected(temMALFORMED)", + "field": "sfSignerEntries", + "location": "SignerEntries::deserialize", + "validated_by": "obj.isFieldPresent(sfSignerEntries)", + "validates": [ + "Checks if the SignerEntries array field is present in the input STObject" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "Unexpected(temMALFORMED)", + "field": "sfSignerEntry", + "location": "SignerEntries::deserialize (inside for loop over sEntries)", + "validated_by": "sEntry.getFName() != sfSignerEntry", + "validates": [ + "Checks that each entry in the SignerEntries array is of type SignerEntry" + ], + "validation_type": "type|format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/SignerEntries.cpp.ai.md b/src/libxrpl/tx/SignerEntries.cpp.ai.md new file mode 100644 index 0000000000..a7cbe91746 --- /dev/null +++ b/src/libxrpl/tx/SignerEntries.cpp.ai.md @@ -0,0 +1,40 @@ +# `SignerEntries.cpp` — Deserializing Multi-Signer Lists from Wire and Ledger Data + +This file provides the single implementation point for `SignerEntries::deserialize()`, a static factory method that extracts a validated signer list from an `STObject`. The code is deliberately small: the complex policy decisions about what a legal signer list looks like live in callers such as `SignerListSet` and `XChainBridge`; this layer is concerned only with structural correctness and data extraction. + +## What Problem This Solves + +XRPL's multi-signing feature allows an account to be controlled by a weighted quorum of up to 32 co-signers, each identified by their account address, a signing weight, and an optional routing tag (`sfWalletLocator`). This signer list appears in two different contexts: embedded in a `SignerListSet` transaction being submitted to the network, and stored directly in a ledger state entry (`SLE`) after the transaction is applied. `deserialize()` handles both cases identically, with the `annotation` parameter (`"transaction"` or `"ledger"`) injected into trace-level log messages so that a journal entry names its source. + +## The `SignerEntry` Struct and Its Comparison Semantics + +Defined in the header, `SignerEntry` bundles three fields: `AccountID account`, `std::uint16_t weight`, and `std::optional tag`. The `operator<` and `operator==` implementations compare **only `account`** and ignore weight and tag entirely. This is not an oversight — callers such as `SignerListSet` sort the resulting vector by `account` and then scan for adjacent duplicates using `std::adjacent_find`. The weight is irrelevant to identity; two `SignerEntry` objects are "the same signer" regardless of their weights. + +The `SignerEntries` class itself has an explicitly deleted default constructor, making it a pure namespace-style container. There is no object to create; the only entry point is the static `deserialize()` method. + +## The `deserialize()` Function + +```cpp +Expected, NotTEC> +SignerEntries::deserialize(STObject const& obj, beast::Journal journal, std::string_view annotation) +``` + +The return type is `Expected` — the XRPL codebase's pre-C++23 analogue of `std::expected`. `NotTEC` is a restricted TER subset that excludes `tec`-class codes, which matter here because this function can be called from `preflight` — before signature checking — where returning a `tec` would be a security problem (it would allow fee-burning without a valid signature). Returning `temMALFORMED` via `Unexpected(temMALFORMED)` correctly indicates a client error that should be rejected immediately. + +The function performs two structural guards before extracting data: + +1. **Presence check**: If `sfSignerEntries` is absent from the incoming `STObject`, the object is malformed. The journal logs the annotation text so developers know whether the bad object came from a transaction or from the ledger. + +2. **Type check per element**: The `sfSignerEntries` field is an `STArray` — an ordered list of `STObject` elements. Each element's field name (retrieved via `getFName()`) must equal `sfSignerEntry`. If something else appears in that array slot, the whole object is rejected. + +After passing those guards, each `STObject` in the array yields three fields via straightforward `STObject` accessors: `sfAccount` as an `AccountID`, `sfSignerWeight` as `uint16_t`, and `sfWalletLocator` as `std::optional` using the tilde-prefix optional-field accessor (`~sfWalletLocator`). No additional per-field validation is done here — weight-range checks, account-validity checks, and the prohibition on signing one's own account all happen in the callers. + +The vector is pre-allocated with `reserve(STTx::maxMultiSigners)`, where `maxMultiSigners` is the protocol constant 32. This avoids repeated heap allocations during iteration without over-allocating in the common case. + +## Call Sites and Their Error Mapping + +`SignerListSet.cpp` calls `deserialize()` with `(tx, j, "transaction")` during `preflight`, checks the `Expected` result, and propagates the `NotTEC` error directly if deserialization fails. It then sorts the returned vector and runs duplicate-account detection — work that `deserialize()` deliberately leaves to the caller. + +`XChainBridge.cpp` calls `deserialize()` with `(*sleS, j, "ledger")` when reading an account's signer list from a ledger state entry. Here a failure maps to `tecINTERNAL` rather than being propagated directly, because a corrupted on-ledger signer list represents an internal consistency problem rather than a client input error. + +The symmetric handling of both sources in a single function is the key architectural choice: it prevents the field-extraction logic from diverging between the two contexts over time. \ No newline at end of file diff --git a/src/libxrpl/tx/Transactor.cpp.ai.json b/src/libxrpl/tx/Transactor.cpp.ai.json new file mode 100644 index 0000000000..32ee73ddfc --- /dev/null +++ b/src/libxrpl/tx/Transactor.cpp.ai.json @@ -0,0 +1,621 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 199, + "name": "Transactor" + } + ], + "code_paths": [ + { + "call_chain": [ + "applyTransaction", + "Transactor::preflight", + "preflight0", + "preflight1", + "preflight2" + ], + "entry_point": "applyTransaction (external, not shown here)", + "purpose": "Performs layered transaction validation before execution.", + "validation_points": [ + "preflight0: tfInnerBatchTxn flag, sfNetworkID, Transaction ID, Transaction Flags", + "preflight1: sfSigningPubKey (via preflightCheckSigningKey)", + "preflight2: further semantic checks (not shown in this snippet)" + ] + }, + { + "call_chain": [ + "Transactor::preflight", + "preflight0", + "detail::preflightCheckSigningKey", + "detail::preflightCheckSimulateKeys" + ], + "entry_point": "Transactor::preflight", + "purpose": "Runs all preflight checks for a transaction.", + "validation_points": [ + "preflight0: flag and field checks", + "preflightCheckSigningKey: validates signing public key", + "preflightCheckSimulateKeys: checks simulation signatures" + ] + } + ], + "data_flows": [ + { + "field": "tfInnerBatchTxn (flag)", + "flow": [ + "ctx.tx", + "preflight0: ctx.tx.isFlag(tfInnerBatchTxn)", + "validation result" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence on pseudo transactions" + ], + "validated_at": "preflight0" + }, + { + "field": "sfNetworkID", + "flow": [ + "ctx.tx", + "preflight0: ctx.tx[~sfNetworkID]", + "compared to nodeNID" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Optional extraction, compared to node's network ID" + ], + "validated_at": "preflight0" + }, + { + "field": "Transaction ID", + "flow": [ + "ctx.tx", + "preflight0: ctx.tx.getTransactionID()", + "checked for zero" + ], + "origin": "ctx.tx.getTransactionID()", + "transformations": [ + "Direct extraction, checked for non-zero" + ], + "validated_at": "preflight0" + }, + { + "field": "Transaction Flags", + "flow": [ + "ctx.tx", + "preflight0: ctx.tx.getFlags() & flagMask", + "checked for invalid bits" + ], + "origin": "ctx.tx.getFlags()", + "transformations": [ + "Bitmask AND with allowed flags" + ], + "validated_at": "preflight0" + }, + { + "field": "sfSigningPubKey", + "flow": [ + "sigObject", + "preflightCheckSigningKey: sigObject.getFieldVL(sfSigningPubKey)", + "publicKeyType(makeSlice(spk))" + ], + "origin": "sigObject (usually ctx.tx)", + "transformations": [ + "Extracted as variable length, checked for valid key type" + ], + "validated_at": "preflightCheckSigningKey" + } + ], + "description": "Implements the core logic for transaction preflight checks, signature validation, fee calculation, sequence/ticket handling, and application of transactions in the XRPL ledger. Contains the Transactor class and related helper functions for transaction processing.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfInnerBatchTxn flag (on pseudo transactions)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight0", + "issue_pattern": "Missing empty string validation for tfInnerBatchTxn flag (on pseudo transactions)", + "why_false_positive": "explicit check in code validates tfInnerBatchTxn flag (on pseudo transactions) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNetworkID (NetworkID field)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight0", + "issue_pattern": "Missing empty string validation for sfNetworkID (NetworkID field)", + "why_false_positive": "explicit check in code validates sfNetworkID (NetworkID field) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Transaction ID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight0", + "issue_pattern": "Missing empty string validation for Transaction ID", + "why_false_positive": "explicit check in code validates Transaction ID for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Transaction ID", + "format", + "validation", + "invalid" + ], + "evidence": "explicit check in code at preflight0", + "issue_pattern": "Missing format validation for Transaction ID", + "why_false_positive": "explicit check in code validates Transaction ID format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Transaction Flags", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight0", + "issue_pattern": "Missing empty string validation for Transaction Flags", + "why_false_positive": "explicit check in code validates Transaction Flags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSigningPubKey (Signing Public Key)", + "empty", + "string", + "validation" + ], + "evidence": "preflightCheckSigningKey at preflightCheckSigningKey", + "issue_pattern": "Missing empty string validation for sfSigningPubKey (Signing Public Key)", + "why_false_positive": "preflightCheckSigningKey validates sfSigningPubKey (Signing Public Key) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/Transactor.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx", + "std::uint32_t flagMask" + ], + "lineno": 18, + "name": "preflight0" + }, + { + "args": [ + "STObject const& sigObject", + "beast::Journal j" + ], + "lineno": 61, + "name": "preflightCheckSigningKey" + }, + { + "args": [ + "ApplyFlags flags", + "STObject const& sigObject", + "beast::Journal j" + ], + "lineno": 72, + "name": "preflightCheckSimulateKeys" + }, + { + "args": [ + "PreflightContext const& ctx", + "std::uint32_t flagMask" + ], + "lineno": 110, + "name": "preflight1" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 168, + "name": "preflight2" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 200, + "name": "Transactor" + }, + { + "args": [ + "std::optional const& slice", + "std::size_t maxLength" + ], + "lineno": 207, + "name": "validDataLength" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 213, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 217, + "name": "preflightSigValidated" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 221, + "name": "checkPermission" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 233, + "name": "calculateBaseFee" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 247, + "name": "calculateOwnerReserveFee" + }, + { + "args": [ + "ServiceRegistry& registry", + "XRPAmount baseFee", + "Fees const& fees", + "ApplyFlags flags" + ], + "lineno": 266, + "name": "minimumFee" + }, + { + "args": [ + "PreclaimContext const& ctx", + "XRPAmount baseFee" + ], + "lineno": 271, + "name": "checkFee" + }, + { + "args": [], + "lineno": 308, + "name": "payFee" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx", + "beast::Journal j" + ], + "lineno": 326, + "name": "checkSeqProxy" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 370, + "name": "checkPriorTxAndLastLedger" + }, + { + "args": [ + "SLE::pointer const& sleAccount" + ], + "lineno": 397, + "name": "consumeSeqProxy" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& account", + "uint256 const& ticketIndex", + "beast::Journal j" + ], + "lineno": 409, + "name": "ticketDelete" + }, + { + "args": [], + "lineno": 448, + "name": "preCompute" + }, + { + "args": [], + "lineno": 453, + "name": "apply" + }, + { + "args": [ + "ReadView const& view", + "ApplyFlags flags", + "std::optional const& parentBatchId", + "AccountID const& idAccount", + "STObject const& sigObject", + "beast::Journal const j" + ], + "lineno": 478, + "name": "checkSign" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 522, + "name": "checkSign" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 528, + "name": "checkBatchSign" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& idSigner", + "AccountID const& idAccount", + "std::shared_ptr sleAccount", + "beast::Journal const j" + ], + "lineno": 557, + "name": "checkSingleSign" + }, + { + "args": [ + "ReadView const& view", + "ApplyFlags flags", + "AccountID const& id", + "STObject const& sigObject", + "beast::Journal const j" + ], + "lineno": 570, + "name": "checkMultiSign" + }, + { + "args": [ + "ApplyView& view", + "std::vector const& offers", + "beast::Journal viewJ" + ], + "lineno": 670, + "name": "removeUnfundedOffers" + }, + { + "args": [ + "ApplyView& view", + "std::vector const& offers", + "beast::Journal viewJ" + ], + "lineno": 683, + "name": "removeExpiredNFTokenOffers" + }, + { + "args": [ + "ApplyView& view", + "std::vector const& creds", + "beast::Journal viewJ" + ], + "lineno": 697, + "name": "removeExpiredCredentials" + }, + { + "args": [ + "ApplyView& view", + "std::vector const& trustLines", + "beast::Journal viewJ" + ], + "lineno": 705, + "name": "removeDeletedTrustLines" + }, + { + "args": [ + "ApplyView& view", + "std::vector const& mpts", + "beast::Journal viewJ" + ], + "lineno": 722, + "name": "removeDeletedMPTs" + }, + { + "args": [ + "XRPAmount fee" + ], + "lineno": 738, + "name": "reset" + }, + { + "args": [ + "uint256 txHash" + ], + "lineno": 771, + "name": "trapTransaction" + }, + { + "args": [], + "lineno": 777, + "name": "operator()" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::logic_error for error reporting", + "exception_type": "std::logic_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + }, + { + "lineno": 59, + "name": "detail" + } + ], + "test_coverage_notes": "The validation logic in preflight0 and preflightCheckSigningKey is critical and likely covered by unit and integration tests in the rippled codebase. Typical test files would be in 'src/test/app/tx/' or 'src/test/tx/' (e.g., Transactor_test.cpp, Preflight_test.cpp, or Transaction_test.cpp). However, some error branches (e.g., simulation signature checks marked as 'should never be hit') may not be covered, as indicated by LCOV_EXCL_LINE comments. There may be limited or no coverage for legacy network ID edge cases or malformed pseudo transaction flags. Test coverage for explicit flag and field validation is likely strong, but coverage for all possible malformed or edge-case transactions may be incomplete.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in C++ (no external validation framework)", + "validation_layer": "business_logic (preflight checks before transaction application)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "tfInnerBatchTxn flag (on pseudo transactions)", + "location": "preflight0", + "validated_by": "explicit check in code", + "validates": [ + "Pseudo transactions cannot contain the tfInnerBatchTxn flag" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "telNETWORK_ID_MAKES_TX_NON_CANONICAL, telREQUIRES_NETWORK_ID, telWRONG_NETWORK", + "field": "sfNetworkID (NetworkID field)", + "location": "preflight0", + "validated_by": "explicit check in code", + "validates": [ + "Legacy networks (id <= 1024) cannot specify NetworkID in txn", + "New networks (id > 1024) require NetworkID field to be present", + "NetworkID in txn must match node's NetworkID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "Transaction ID", + "location": "preflight0", + "validated_by": "explicit check in code", + "validates": [ + "Transaction ID may not be zero" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "Transaction Flags", + "location": "preflight0", + "validated_by": "explicit check in code", + "validates": [ + "Transaction flags must not have any bits set in flagMask" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_SIGNATURE", + "field": "sfSigningPubKey (Signing Public Key)", + "location": "preflightCheckSigningKey", + "validated_by": "preflightCheckSigningKey", + "validates": [ + "If signing public key is present and not empty, it must be a valid public key type" + ], + "validation_type": "format|type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/Transactor.cpp.ai.md b/src/libxrpl/tx/Transactor.cpp.ai.md new file mode 100644 index 0000000000..09c264de2d --- /dev/null +++ b/src/libxrpl/tx/Transactor.cpp.ai.md @@ -0,0 +1,121 @@ +Now I have enough context to write the documentation. Let me produce it: + +--- + +## `src/libxrpl/tx/Transactor.cpp` + +### Role in the System + +`Transactor` is the abstract base class for every transaction type in the XRP Ledger. Every `Payment`, `OfferCreate`, `AccountSet`, `NFTokenMint`, and so on ultimately inherits from it. This file implements the **shared infrastructure** that all transactions use: layered preflight validation, fee calculation and payment, sequence/ticket consumption, signature verification, and the final apply-or-reset decision loop. The concrete transaction type only has to supply `doApply()` and a few static hooks; everything else flows through the machinery here. + +--- + +### The Three-Phase Transaction Pipeline + +Transaction processing is split across three phases, each with its own context struct: + +1. **Preflight** (`PreflightContext`) — stateless, read-only, runs against the raw `STTx`. The ledger view is not consulted. Errors here produce `tem*` or `tel*` codes. +2. **Preclaim** (`PreclaimContext`) — read-only against a ledger view snapshot. Checks whether the account exists, the fee is sufficient, and the signature is valid. Errors here produce `tef*`/`ter*` codes. +3. **Apply** (`ApplyContext`) — mutable. The transaction actually changes ledger state. + +The gateway into phase 3 is `Transactor::operator()()`, which receives the preclaim result stored in `ctx_.preclaimResult` and proceeds to call `apply()` only when that result is `tesSUCCESS`. + +--- + +### Layered Preflight: `preflight0` → `preflight1` → `T::preflight()` → `preflight2` + +The template function `invokePreflight()` in the header orchestrates all preflight checks for a concrete transactor type `T`: + +```cpp +preflight1(ctx, T::getFlagsMask(ctx)) → T::preflight(ctx) → preflight2(ctx) → T::preflightSigValidated(ctx) +``` + +**`preflight0`** (free function, called from `preflight1`) is the most primitive gate. It enforces three invariants that apply to every transaction regardless of type: +- Pseudo-transactions cannot carry the `tfInnerBatchTxn` flag. +- The `sfNetworkID` field must be absent on legacy networks (ID ≤ 1024) and present and matching on newer ones. This prevents replay across networks. +- The transaction ID cannot be the zero hash. +- No bits outside the transaction's allowed flag mask may be set. + +**`preflight1`** adds account-level sanity: the `sfAccount` field must be a non-zero ID, the `sfFee` must be a non-negative native (XRP) amount, and the signing public key format must be valid. It also enforces that tickets and `sfAccountTxnID` are mutually exclusive — an important ordering-constraint invariant documented inline. + +**`preflight2`** handles simulation mode (`tapDRY_RUN`) and the cryptographic signature validity check via the hash router's cached result (`checkValidity`). For `tfInnerBatchTxn` transactions the signature check is skipped entirely here because the outer batch transaction already provides authorization. + +The design ensures a derived class's `preflight()` runs *between* the framework's own `preflight1` and `preflight2` checks, never bypassing them. The header comment is explicit: "Do not try to call preflight1 or preflight2 directly." + +--- + +### Fee Calculation and the `reset()` Safety Net + +`calculateBaseFee()` returns `baseFee * (1 + multiSigCount)` — each additional signer costs one extra base fee. `minimumFee()` then scales that by the current server load via `scaleFeeLoad`. + +`checkFee()` does a nuanced balance check: it uses a static `ReadView` snapshot, so multiple in-flight transactions from the same account may each independently pass the balance check. The code documents this optimism explicitly: + +> *"Because preclaim evaluates against a static readview, it does not reflect fee deductions from other transactions paid by the same account within the current ledger… The fee shortfall will be handled by the Transactor::reset mechanism."* + +`reset()` is the correction path. It discards all ledger mutations via `ctx_.discard()`, then re-deducts only the fee, clamping it to the actual remaining balance if necessary: + +```cpp +if (fee > balance) + fee = balance; +``` + +This ensures that a failing transaction can still claim its fee even when the account is over-committed — a core ledger invariant. + +--- + +### Sequence and Ticket Consumption + +`checkSeqProxy()` handles both classic sequence numbers and the newer Ticket mechanism via `SeqProxy`. For sequence-based transactions it enforces strict monotonic ordering. For ticket-based transactions it checks that the ticket's numeric value is below the current account sequence (to rule out tickets that haven't been created yet), and that the ticket SLE actually exists in the ledger. + +`consumeSeqProxy()` advances the account sequence for normal transactions, or calls `ticketDelete()` for ticket transactions. `ticketDelete()` performs three coordinated ledger mutations: removes the ticket SLE, removes it from the owner directory, decrements `sfTicketCount` (removing the field entirely when it reaches zero), and adjusts the owner reserve count. + +--- + +### Signature Verification + +`checkSign()` handles four distinct authorization paths: + +1. **Batch inner transactions**: asserts that no key, signature, or signer list is present — authorization was already checked on the outer batch. +2. **Simulation (`tapDRY_RUN`)**: if no signing key and no signers are present, validation is skipped entirely. +3. **Multi-signature** (`sfSigners` present): delegates to `checkMultiSign()`. +4. **Single signature**: derives the signing account from the public key and calls `checkSingleSign()`. + +`checkSingleSign()` applies three precedence rules: regular key first, then enabled master key, then `tefMASTER_DISABLED` for a disabled master key. + +`checkMultiSign()` performs a linear merge of the sorted `sfSigners` array against the account's sorted `SignerEntry` list, validating each entry against the phantom / master key / regular key rules documented inline. It terminates with `tefBAD_QUORUM` if the weight sum falls short of `sfSignerQuorum`. + +`checkBatchSign()` extends multi-sign to the outer batch transaction's `sfBatchSigners` array, permitting unsigned accounts to appear when their master key is the signer — allowing a batch to fund an account creation as part of the same batch. + +--- + +### The `operator()()` Apply Loop + +The entry point for phase 3 is `operator()()`. It: + +1. Installs RAII guards (`NumberSO`, `CurrentTransactionRulesGuard`) that adapt numeric arithmetic rules based on enabled amendments. +2. In debug builds, serializes and re-parses the transaction to detect serdes mismatches. +3. Calls `apply()` if preclaim succeeded; `apply()` runs `preCompute()`, captures `preFeeBalance_`, calls `consumeSeqProxy()`, calls `payFee()`, updates `sfAccountTxnID` if present, then calls the virtual `doApply()`. +4. After the call, enforces `tecOVERSIZE` if the metadata delta exceeds `oversizeMetaDataCap`. +5. For `tapFAIL_HARD` with a `tec` result, the context is discarded and no state changes occur. +6. For special `tec` codes (`tecOVERSIZE`, `tecKILLED`, `tecINCOMPLETE`, `tecEXPIRED`), it visits the context diff to collect deleted objects, calls `reset()` to discard the mutation, then replays targeted cleanup helpers: `removeUnfundedOffers`, `removeExpiredNFTokenOffers`, `removeDeletedTrustLines`, `removeDeletedMPTs`, `removeExpiredCredentials`. This mechanism lets certain failure codes still produce useful side-effects (offer cleanup) without applying the failed transaction's primary effects. +7. Passes applied results through `ctx_.checkInvariants()`. If invariant checking itself fails (`tecINVARIANT_FAILED`), the context is reset again and the invariants re-checked on the fee-only claim. If invariants still fail, the transaction is not applied at all. +8. For dry-run mode, sets `applied = false` unconditionally after all the above — the state changes are computed but never committed. +9. Returns `{result, applied, metadata}` as an `ApplyResult`. + +--- + +### Permission Delegation + +`checkPermission()` validates the optional `sfDelegate` field against a `DelegateObject` ledger entry (`keylet::delegate`). If the object is absent, `terNO_DELEGATE_PERMISSION` is returned. If present, `checkTxPermission()` from `DelegateHelpers` evaluates whether the delegate has the right to submit this specific transaction type. + +--- + +### Pseudo-Account Guard + +When the `featureLendingProtocol` amendment is enabled, `checkSign()` rejects any signing attempt from a pseudo-account with `tefBAD_AUTH`. Pseudo-accounts are protocol-internal constructs that must never be able to sign user-facing transactions; this guard is defensive against ledger corruption or future code paths that might inadvertently authorize them. + +--- + +### Debug and Diagnostics + +`trapTransaction()` exists solely to provide a named breakpoint location for replaying specific transactions during debugging. It logs the hash and returns, doing nothing else. The `LCOV_EXCL_LINE` and `LCOV_EXCL_START/STOP` markers scattered through the file indicate branches that are structurally unreachable in normal ledger operation — defensive guards against ledger corruption — and are excluded from coverage requirements accordingly. \ No newline at end of file diff --git a/src/libxrpl/tx/apply.cpp.ai.json b/src/libxrpl/tx/apply.cpp.ai.json new file mode 100644 index 0000000000..84d625b71f --- /dev/null +++ b/src/libxrpl/tx/apply.cpp.ai.json @@ -0,0 +1,362 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "apply(ServiceRegistry&, OpenView&, STTx const&, ApplyFlags, beast::Journal)", + "apply(ServiceRegistry&, OpenView&, PreflightChecks&&)", + "preclaim(preflightChecks(), registry, view)", + "doApply(preclaimResult, registry, view)" + ], + "entry_point": "apply(ServiceRegistry&, OpenView&, STTx const&, ApplyFlags, beast::Journal)", + "purpose": "Applies a transaction to the ledger, including validation and execution.", + "validation_points": [ + "preflightChecks() (calls checkValidity)", + "checkValidity (signature, local checks, batch checks)" + ] + }, + { + "call_chain": [ + "checkValidity(HashRouter&, STTx const&, Rules const&)" + ], + "entry_point": "checkValidity(HashRouter&, STTx const&, Rules const&)", + "purpose": "Performs all transaction validity checks: batch flags, signature, local fields.", + "validation_points": [ + "Explicit checks for tfInnerBatchTxn, sfTxnSignature, sfSigners, signingPubKey", + "tx.checkSign(rules) (signature validation)", + "passesLocalChecks(tx, reason) (local field validation)" + ] + }, + { + "call_chain": [ + "forceValidity(HashRouter&, uint256 const&, Validity)" + ], + "entry_point": "forceValidity(HashRouter&, uint256 const&, Validity)", + "purpose": "Forcibly sets the validity flags for a transaction in the HashRouter.", + "validation_points": [ + "Sets SF_LOCALGOOD, SF_SIGGOOD flags based on Validity" + ] + } + ], + "data_flows": [ + { + "field": "tfInnerBatchTxn", + "flow": [ + "STTx (input transaction)", + "checkValidity: tx.isFlag(tfInnerBatchTxn)", + "Batch-specific validation logic" + ], + "origin": "STTx::isFlag(tfInnerBatchTxn)", + "transformations": [ + "Checked for presence; triggers batch-specific validation" + ], + "validated_at": "checkValidity" + }, + { + "field": "sfTxnSignature", + "flow": [ + "STTx (input transaction)", + "checkValidity: tx.isFieldPresent(sfTxnSignature)" + ], + "origin": "STTx::isFieldPresent(sfTxnSignature)", + "transformations": [ + "Checked for presence; if present in inner batch, triggers error" + ], + "validated_at": "checkValidity" + }, + { + "field": "sfSigners", + "flow": [ + "STTx (input transaction)", + "checkValidity: tx.isFieldPresent(sfSigners)" + ], + "origin": "STTx::isFieldPresent(sfSigners)", + "transformations": [ + "Checked for presence; if present in inner batch, triggers error" + ], + "validated_at": "checkValidity" + }, + { + "field": "signingPubKey", + "flow": [ + "STTx (input transaction)", + "checkValidity: tx.getSigningPubKey().empty()" + ], + "origin": "STTx::getSigningPubKey()", + "transformations": [ + "Checked for emptiness; if not empty in inner batch, triggers error" + ], + "validated_at": "checkValidity" + }, + { + "field": "transaction signature", + "flow": [ + "STTx (input transaction)", + "checkValidity: tx.checkSign(rules)" + ], + "origin": "STTx (input transaction)", + "transformations": [ + "Signature cryptographically verified" + ], + "validated_at": "checkValidity" + }, + { + "field": "local transaction fields", + "flow": [ + "STTx (input transaction)", + "checkValidity: passesLocalChecks(tx, reason)" + ], + "origin": "STTx (input transaction)", + "transformations": [ + "Checked for local field validity (e.g., required fields, field ranges)" + ], + "validated_at": "checkValidity" + } + ], + "description": "Implements transaction application logic for the XRPL, including signature and local validity checks, batch transaction handling, and transaction application to ledger views.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Signature (via checkSign)", + "validation", + "missing", + "check" + ], + "evidence": "Field Signature (via checkSign) validated by Custom validation via explicit checks and helper functions (e.g., passesLocalChecks, checkSign)", + "issue_pattern": "Missing validation for Signature (via checkSign)", + "why_false_positive": "Custom validation via explicit checks and helper functions (e.g., passesLocalChecks, checkSign) validates Signature (via checkSign) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Batch transaction structure (via explicit field checks)", + "validation", + "missing", + "check" + ], + "evidence": "Field Batch transaction structure (via explicit field checks) validated by Custom validation via explicit checks and helper functions (e.g., passesLocalChecks, checkSign)", + "issue_pattern": "Missing validation for Batch transaction structure (via explicit field checks)", + "why_false_positive": "Custom validation via explicit checks and helper functions (e.g., passesLocalChecks, checkSign) validates Batch transaction structure (via explicit field checks) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Local transaction fields (via passesLocalChecks)", + "validation", + "missing", + "check" + ], + "evidence": "Field Local transaction fields (via passesLocalChecks) validated by Custom validation via explicit checks and helper functions (e.g., passesLocalChecks, checkSign)", + "issue_pattern": "Missing validation for Local transaction fields (via passesLocalChecks)", + "why_false_positive": "Custom validation via explicit checks and helper functions (e.g., passesLocalChecks, checkSign) validates Local transaction fields (via passesLocalChecks) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfInnerBatchTxn flag, sfTxnSignature, sfSigners, signingPubKey", + "empty", + "string", + "validation" + ], + "evidence": "explicit checks in checkValidity at checkValidity", + "issue_pattern": "Missing empty string validation for tfInnerBatchTxn flag, sfTxnSignature, sfSigners, signingPubKey", + "why_false_positive": "explicit checks in checkValidity validates tfInnerBatchTxn flag, sfTxnSignature, sfSigners, signingPubKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction signature", + "empty", + "string", + "validation" + ], + "evidence": "tx.checkSign(rules) at checkValidity", + "issue_pattern": "Missing empty string validation for transaction signature", + "why_false_positive": "tx.checkSign(rules) validates transaction signature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "local transaction fields", + "empty", + "string", + "validation" + ], + "evidence": "passesLocalChecks(tx, reason) at checkValidity", + "issue_pattern": "Missing empty string validation for local transaction fields", + "why_false_positive": "passesLocalChecks(tx, reason) validates local transaction fields for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/apply.cpp", + "functions": [ + { + "args": [ + "router", + "tx", + "rules" + ], + "lineno": 15, + "name": "checkValidity" + }, + { + "args": [ + "router", + "txid", + "validity" + ], + "lineno": 67, + "name": "forceValidity" + }, + { + "args": [ + "registry", + "view", + "preflightChecks" + ], + "lineno": 83, + "name": "apply" + }, + { + "args": [ + "registry", + "view", + "tx", + "flags", + "j" + ], + "lineno": 89, + "name": "apply" + }, + { + "args": [ + "registry", + "view", + "parentBatchId", + "tx", + "flags", + "j" + ], + "lineno": 96, + "name": "apply" + }, + { + "args": [ + "registry", + "batchView", + "batchTxn", + "j" + ], + "lineno": 104, + "name": "applyBatchTransactions" + }, + { + "args": [ + "registry", + "view", + "txn", + "retryAssured", + "flags", + "j" + ], + "lineno": 154, + "name": "applyTransaction" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "Validation logic in checkValidity is likely covered by unit tests in the rippled codebase, especially in files such as 'test/tx/Transaction_test.cpp', 'test/apply/Apply_test.cpp', and 'test/batch/Batch_test.cpp'. These tests should cover signature validation, batch transaction rules, and local field checks. However, edge cases for batch inner transactions, especially with new features (e.g., fixBatchInnerSigs), may not be fully covered. There may also be limited coverage for forced validity via forceValidity, and for error propagation via exceptions. Integration tests may not cover all combinations of flags and malformed transactions.", + "validation_architecture": { + "auto_validated_fields": [ + "Signature (via checkSign)", + "Batch transaction structure (via explicit field checks)", + "Local transaction fields (via passesLocalChecks)" + ], + "framework": "Custom validation via explicit checks and helper functions (e.g., passesLocalChecks, checkSign)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns {Validity::SigBad, \"Malformed: Invalid inner batch transaction.\"}", + "field": "tfInnerBatchTxn flag, sfTxnSignature, sfSigners, signingPubKey", + "location": "checkValidity", + "validated_by": "explicit checks in checkValidity", + "validates": [ + "If tfInnerBatchTxn is set and batch feature enabled, transaction must NOT have sfTxnSignature, signingPubKey, or sfSigners" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns {Validity::SigBad, sigVerify.error()}", + "field": "transaction signature", + "location": "checkValidity", + "validated_by": "tx.checkSign(rules)", + "validates": [ + "Signature is cryptographically valid for transaction and rules" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 0.9, + "error_thrown": "returns {Validity::SigGoodOnly, reason}", + "field": "local transaction fields", + "location": "checkValidity", + "validated_by": "passesLocalChecks(tx, reason)", + "validates": [ + "Transaction passes local field and business logic checks (details in passesLocalChecks)" + ], + "validation_type": "business_logic|format|type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/apply.cpp.ai.md b/src/libxrpl/tx/apply.cpp.ai.md new file mode 100644 index 0000000000..576e5b2aa6 --- /dev/null +++ b/src/libxrpl/tx/apply.cpp.ai.md @@ -0,0 +1,56 @@ +# `src/libxrpl/tx/apply.cpp` + +## Role in the System + +This file is the top-level transaction application coordinator for the XRPL. It bridges two orthogonal concerns: the network-level validity cache (tracked by `HashRouter`) and the stateless-then-stateful application pipeline (`preflight → preclaim → doApply`) defined in `applySteps.cpp`. Where `applySteps.cpp` contains the mechanics of each pipeline stage, `apply.cpp` decides *when* to run them, *how* to short-circuit redundant work, and *how* to handle the multi-transaction semantics of the Batch feature. + +## Validity Caching via `HashRouter` + +`checkValidity()` is the gateway that decides whether a transaction is safe to propagate over the P2P network and ultimately apply to a ledger. Cryptographic signature verification and local-field checks are intentionally expensive, so their results are memoized in the node's `HashRouter` — a time-bounded hash table that tracks metadata about every recently seen object by its 256-bit hash. + +The file claims four of the router's six private flag bits (`PRIVATE1`–`PRIVATE4`), aliased as `SF_SIGBAD`, `SF_SIGGOOD`, `SF_LOCALBAD`, and `SF_LOCALGOOD`. The naming makes the semantics self-documenting: a transaction moves through a linear validity state machine from unknown → `SigBad`/`SigGoodOnly`/`Valid`. The check logic is carefully ordered — bad signature short-circuits immediately without running local checks, because there is no point inspecting well-formedness of a transaction whose author cannot be authenticated. This avoids a class of CPU-exhaustion attacks where an attacker sends malformed-but-locally-valid transactions with invalid signatures. + +The returned `Validity` enum (`SigBad`, `SigGoodOnly`, `Valid`) maps directly to what the P2P layer can do with the transaction: a bad signature means the transaction should not be forwarded; a good signature with failed local checks means it may be relayed (the signature vouches for authenticity) but will never be applied; `Valid` means it can be both relayed and applied. + +## `forceValidity()` — Elevating Cached State + +`forceValidity()` lets callers forcibly promote a transaction's cached validity, bypassing the actual checks. It uses a `[[fallthrough]]` switch to compose flags: `Valid` sets both `SF_LOCALGOOD` and `SF_SIGGOOD`; `SigGoodOnly` sets only `SF_SIGGOOD`. Crucially, it can never set `SF_SIGBAD` — calling `forceValidity(router, txid, Validity::SigBad)` is a no-op. The design intentionally allows raising validity (e.g., for locally-submitted transactions where the node is authoritative) but never lowering it through this path. The header's comment warns "Use with extreme care," since bypassing signature verification in the cache means every subsequent `checkValidity` call on the same hash will skip the cryptographic check entirely until the cache entry expires. + +## The `apply()` Pipeline + +The private template `apply(ServiceRegistry&, OpenView&, PreflightChecks&&)` is the canonical implementation. It accepts any callable that produces a `PreflightResult`, runs `preclaim` on that result against the open view, then calls `doApply`. Wrapping the preflight step in a callable — rather than accepting a `PreflightResult` directly — is a key design choice: `preflight` results can be safely computed in advance and on a different thread (they contain no ledger references), but `preclaim` and `doApply` must run together against the same view. The template keeps this sequencing enforced while allowing both the standard case (plain `preflight`) and the batch-inner case (batch `preflight` with a `parentBatchId`) to share identical post-preflight logic. + +The `NumberSO` RAII guard is installed here, before `preclaim` runs. It configures the thread-local fixed-point math precision mode based on whether the `fixUniversalNumber` amendment is active in the current view's rules. This mirrors similar setup in `applySteps.cpp`'s `with_txn_type()`, which adds `fixUniversalNumber` to `Transactor::operator()` for the actual execution phase. + +## Batch Transaction Execution + +`applyBatchTransactions()` implements the Batch feature's inner-transaction execution loop. The outer `ttBATCH` transaction has already been applied by the time this function is called (via `applyTransaction()`), meaning the outer account's sequence number and fee have already been committed. The inner transactions are then executed against a layered view stack: + +1. **`wholeBatchView`** — an `OpenView` wrapping the outer ledger view, created with the `batch_view` tag. Changes here are all-or-nothing relative to the outer view. +2. **`perTxBatchView`** — an `OpenView` wrapping `wholeBatchView`, created fresh per inner transaction. If the inner transaction succeeds, its changes are promoted to `wholeBatchView` via `perTxBatchView.apply(batchView)`. If it fails, the sub-view is simply abandoned. + +This two-level nesting isolates each inner transaction's tentative state changes. Only after the entire batch loop completes successfully does `wholeBatchView.apply(view)` promote the aggregate changes to the actual ledger. + +The three Batch execution modes are enforced by examining the outer transaction's flags: +- **`tfAllOrNothing`** — any inner failure causes `applyBatchTransactions()` to return `false`, and none of the changes reach the outer view. +- **`tfUntilFailure`** — iteration stops at the first failure; all previously applied inner transactions are kept. +- **`tfOnlyOne`** — stops after the first *success*; subsequent transactions are not executed. + +The `applied` count guards the return value: even in `tfUntilFailure` mode, the function returns `false` if no inner transaction was ever applied. This ensures `wholeBatchView.apply(view)` is only called when there is something worth committing. + +## Batch Inner Signature Handling and `fixBatchInnerSigs` + +Inner batch transactions are signed as unsigned objects — they carry no `sfTxnSignature`, no `sfSigners`, and an empty `sfSigningPubKey`. Their authorization comes entirely from the outer transaction's signature. `checkValidity()` detects this via `tfInnerBatchTxn` and `featureBatch`, then applies a defensive check: if any of those signature fields are *present*, it returns `SigBad` with a malformation error. + +The `fixBatchInnerSigs` amendment addresses a subtle correctness bug in the original Batch implementation. Before the fix, when an inner transaction reached `checkValidity()`, the code would still run `passesLocalChecks()` and then record `SF_SIGGOOD` — implying a valid signature on a transaction that has none. The `fixBatchInnerSigs` block (`neverValid` path) corrects this: once the amendment is enabled, any inner-batch transaction is never assigned a good-signature cache entry; the code returns immediately after the defensive field check. The comment in the source explicitly notes this block "should probably have never been included in the original `Batch` implementation," making the amendment a targeted retroactive fix rather than a feature expansion. + +## `applyTransaction()` — The Ledger-Layer Interface + +`applyTransaction()` is the highest-level entry point, used by the ledger consensus and open-ledger building machinery. It adds `tapRETRY` to the flags when `retryAssured` is true (signaling to the `Transactor` that a `tec` result can be soft-failed and re-tried in the same ledger cycle). After a successful `apply()` call, it checks whether the applied transaction was a `ttBATCH` and, if so, runs `applyBatchTransactions()` in the same call frame. This placement is intentional: the batch's inner transactions must run immediately after the outer transaction is committed, within the same ledger view and under the same error-handling umbrella. + +The function's return type, `ApplyTransactionResult` (`Success`, `Fail`, `Retry`), collapses the full `TER` space into a three-way decision for callers that only care about scheduling: +- `tef`/`tem`/`tel` codes — hard failures, no retry. +- Applied transactions — always `Success`. +- Anything else — `Retry`, meaning the transaction remains a candidate for the open ledger. + +All execution is wrapped in a `try`/`catch(std::exception const&)` that converts exceptions into `Fail`, matching the guarantee documented in `apply.h` that `applyTransaction()` does not throw. \ No newline at end of file diff --git a/src/libxrpl/tx/applySteps.cpp.ai.json b/src/libxrpl/tx/applySteps.cpp.ai.json new file mode 100644 index 0000000000..cd8d653e16 --- /dev/null +++ b/src/libxrpl/tx/applySteps.cpp.ai.json @@ -0,0 +1,351 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "TxType t" + ], + "lineno": 15, + "name": "UnknownTxnType" + } + ], + "code_paths": [ + { + "call_chain": [ + "invoke_preflight", + "with_txn_type", + "Transactor::invokePreflight", + "consequences_helper" + ], + "entry_point": "invoke_preflight", + "purpose": "Performs preflight validation for a transaction, including type-based dispatch and consequence calculation.", + "validation_points": [ + "Transactor::invokePreflight (core validation logic)", + "with_txn_type (txnType switch validation)" + ] + }, + { + "call_chain": [ + "invoke_preclaim", + "with_txn_type", + "Transactor::invokePreclaim" + ], + "entry_point": "invoke_preclaim", + "purpose": "Performs preclaim validation for a transaction, type-dispatched.", + "validation_points": [ + "Transactor::invokePreclaim", + "with_txn_type (txnType switch validation)" + ] + }, + { + "call_chain": [ + "invoke_calculateBaseFee", + "with_txn_type", + "Transactor::calculateBaseFee" + ], + "entry_point": "invoke_calculateBaseFee", + "purpose": "Calculates the base fee for a transaction, type-dispatched.", + "validation_points": [ + "with_txn_type (txnType switch validation)" + ] + }, + { + "call_chain": [ + "invoke_apply", + "with_txn_type", + "Transactor::operator()" + ], + "entry_point": "invoke_apply", + "purpose": "Applies a transaction to the ledger, type-dispatched.", + "validation_points": [ + "with_txn_type (txnType switch validation)" + ] + } + ], + "data_flows": [ + { + "field": "txnType", + "flow": [ + "ctx.tx.getTxnType()", + "with_txn_type (switch statement)", + "template dispatch to T", + "Transactor::invokePreflight / invokePreclaim / etc." + ], + "origin": "ctx.tx.getTxnType() (from PreflightContext)", + "transformations": [ + "Used as a switch key to select transaction handler", + "If unknown, triggers UnknownTxnType exception" + ], + "validated_at": "with_txn_type (switch statement)" + }, + { + "field": "ctx (PreflightContext)", + "flow": [ + "invoke_preflight(ctx)", + "with_txn_type(..., f)", + "f.template operator()()", + "Transactor::invokePreflight(ctx)", + "consequences_helper(ctx)" + ], + "origin": "Passed as argument to invoke_preflight/invoke_preclaim/etc.", + "transformations": [ + "Passed unchanged through call chain", + "Used to extract tx, rules, etc." + ], + "validated_at": "Transactor::invokePreflight(ctx)" + }, + { + "field": "tx (STTx)", + "flow": [ + "ctx.tx", + "TxConsequences(ctx.tx)", + "consequences_helper(ctx)" + ], + "origin": "ctx.tx", + "transformations": [ + "Used to construct TxConsequences", + "May be passed to T::makeTxConsequences(ctx) for custom logic" + ], + "validated_at": "Transactor::invokePreflight(ctx) (indirectly)" + } + ], + "description": "Implements transaction type dispatch and core transaction processing steps (preflight, preclaim, apply, fee calculation) for the XRPL ledger, using type-based dispatch and compile-time polymorphism.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "txnType", + "validation", + "missing", + "check" + ], + "evidence": "Field txnType validated by Custom switch-based validation using macro expansion (transactions.macro)", + "issue_pattern": "Missing validation for txnType", + "why_false_positive": "Custom switch-based validation using macro expansion (transactions.macro) validates txnType automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "txnType", + "empty", + "string", + "validation" + ], + "evidence": "with_txn_type (switch statement) at with_txn_type", + "issue_pattern": "Missing empty string validation for txnType", + "why_false_positive": "with_txn_type (switch statement) validates txnType for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/applySteps.cpp", + "functions": [ + { + "args": [ + "rules", + "txnType", + "f" + ], + "lineno": 26, + "name": "with_txn_type" + }, + { + "args": [ + "ctx" + ], + "lineno": 74, + "name": "consequences_helper" + }, + { + "args": [ + "ctx" + ], + "lineno": 82, + "name": "consequences_helper" + }, + { + "args": [ + "ctx" + ], + "lineno": 90, + "name": "consequences_helper" + }, + { + "args": [ + "ctx" + ], + "lineno": 97, + "name": "invoke_preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 120, + "name": "invoke_preclaim" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 170, + "name": "invoke_calculateBaseFee" + }, + { + "args": [ + "ctx" + ], + "lineno": 217, + "name": "invoke_apply" + }, + { + "args": [ + "registry", + "rules", + "tx", + "flags", + "j" + ], + "lineno": 241, + "name": "preflight" + }, + { + "args": [ + "registry", + "rules", + "parentBatchId", + "tx", + "flags", + "j" + ], + "lineno": 253, + "name": "preflight" + }, + { + "args": [ + "preflightResult", + "registry", + "view" + ], + "lineno": 265, + "name": "preclaim" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 299, + "name": "calculateBaseFee" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 303, + "name": "calculateDefaultBaseFee" + }, + { + "args": [ + "preclaimResult", + "registry", + "view" + ], + "lineno": 307, + "name": "doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code in applySteps.cpp is primarily template dispatch and type-based routing, so direct unit tests are rare. Instead, coverage is provided by higher-level transaction engine and validation tests, typically found in files like 'test/tx/Transactor_test.cpp', 'test/app/tx/applySteps_test.cpp', or integration tests that exercise transaction submission and validation. Gaps may exist for unknown/invalid txnType handling (UnknownTxnType exception), and for feature-flagged code paths (e.g., SingleAssetVault, LendingProtocol) unless specifically tested. Custom consequences logic (T::makeTxConsequences) may also lack direct coverage if not all transaction types are exercised in tests.", + "validation_architecture": { + "auto_validated_fields": [ + "txnType" + ], + "framework": "Custom switch-based validation using macro expansion (transactions.macro)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "UnknownTxnType", + "field": "txnType", + "location": "with_txn_type", + "validated_by": "with_txn_type (switch statement)", + "validates": [ + "Checks if txnType matches a known transaction type defined in transactions.macro", + "Throws if txnType is not recognized" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/applySteps.cpp.ai.md b/src/libxrpl/tx/applySteps.cpp.ai.md new file mode 100644 index 0000000000..7c43adea26 --- /dev/null +++ b/src/libxrpl/tx/applySteps.cpp.ai.md @@ -0,0 +1,50 @@ +# `src/libxrpl/tx/applySteps.cpp` + +## Role in the System + +This file is the orchestration hub for the XRPL transaction processing pipeline. Every transaction submitted to the ledger passes through the four entry points defined here — `preflight`, `preclaim`, `calculateBaseFee`, and `doApply` — in that order. The file does not contain any transaction-specific business logic itself; instead, it provides the type-dispatch machinery that routes each transaction to the correct concrete `Transactor` subclass and assembles the results into the structured `PreflightResult`, `PreclaimResult`, and `ApplyResult` objects that higher layers consume. + +## The X-Macro Dispatch Engine + +The central mechanism is `with_txn_type()`, a function template that converts a runtime `TxType` enum value into a compile-time template parameter: + +```cpp +template +auto with_txn_type(Rules const& rules, TxType txnType, F&& f) +``` + +Inside, it `#include`s `transactions.macro` a second time with `TRANSACTION` redefined to emit one `case` label per known transaction type. Each case calls `f.template operator()()`, passing the concrete transactor class as a template argument. This X-macro technique generates a flat switch statement at compile time, covering every transaction type declared in `transactions.macro` without any virtual dispatch overhead and without requiring `applySteps.cpp` to directly `#include` any transactor headers (the comment at line 14 explicitly forbids it). The first `#include` of the macro near the top of the file is done with `TRANSACTION` defined as a no-op so that only the transactor headers referenced via `TRANSACTION_INCLUDE` guards in the macro get pulled in for include-only purposes. + +If `txnType` matches no case, `with_txn_type` throws the internal `UnknownTxnType` sentinel exception. All four public entry points catch this; the catch blocks are marked `LCOV_EXCL` because they represent conditions that should be unreachable in production — a transaction with an unknown type would have failed long before reaching here. + +## Numeric Precision Guards + +Before the switch dispatch, `with_txn_type` sets up thread-local RAII guards that control arithmetic precision for the entire processing step: + +- When `featureSingleAssetVault` or `featureLendingProtocol` is enabled, a `CurrentTransactionRulesGuard` installs the current `Rules` into a thread-local slot (making them accessible without passing them everywhere), and a `NumberSO` guard configures floating-point-style number arithmetic based on whether `fixUniversalNumber` is active. +- Without those features, a `NumberMantissaScaleGuard` forces the legacy small-mantissa behavior to preserve historical correctness. + +The comment here is self-critical: ideally these guards would have been applied to every processing phase from the start, but they were historically placed only in `doApply` (via `Transactor::operator()`). They were added to `with_txn_type` only once the new vault/lending features made it necessary for read-only phases like `preflight` and `preclaim` to also see the correct numeric rules. + +## The Three-Phase Pipeline + +**Preflight** (`invoke_preflight` / public `preflight`) validates a transaction purely from its static content — no ledger state required. It calls `Transactor::invokePreflight` and, on success, computes `TxConsequences` via the `consequences_helper` template family. Three overloads of `consequences_helper` are selected at compile time by C++20 `requires` clauses on `T::ConsequencesFactory`: +- `Normal` — constructs a standard `TxConsequences` from the raw `STTx`. +- `Blocker` — marks the transaction as a blocker (e.g., `SetRegularKey`), signaling to the transaction queue that this transaction affects the ability of subsequent transactions to claim fees. +- `Custom` — delegates to `T::makeTxConsequences(ctx)` for transaction-specific logic. + +There are two overloads of the public `preflight` function: one for ordinary transactions and one for transactions that are part of a batch (carrying a `parentBatchId`). Both produce a `PreflightResult` whose members are all `const`, by design, making it very difficult to construct a plausible-looking result without actually running the check. + +**Preclaim** (`invoke_preclaim` / public `preclaim`) validates against ledger state. Before calling `T::preclaim(ctx)`, `invoke_preclaim` runs a hardcoded sequence of static-method checks via name hiding (compile-time polymorphism without virtual functions): `checkSeqProxy`, `checkPriorTxAndLastLedger`, `checkPermission`, `checkSign`, and then `checkFee`. The code comments enforce a critical security invariant: every check up to and including `checkSign` **must** return `NotTEC` (not a `tec` code). A `tec` result would cause a fee to be charged even before the signature is verified, which could enable theft or fund destruction. Only after `checkSign` succeeds can `checkFee` return a full `TER`. + +The public `preclaim` also handles a race condition: if the `Rules` recorded in `preflightResult` no longer match the rules on the provided view (the ledger advanced between preflight and preclaim), it silently re-runs `preflight` with the new rules before constructing the `PreclaimContext`. The resulting `PreclaimResult` sets `likelyToClaimFee` based on whether the result is a `tes` success or a hard-fail `tec` (i.e., not a soft retry). + +**doApply** constructs an `ApplyContext` and calls `invoke_apply`, which dispatches through `with_txn_type` to instantiate `T p(ctx)` and call `p()`. It first guards against a caller logic error by checking that the ledger sequence of the `preclaimResult`'s view matches the target `view`. If `likelyToClaimFee` is false, the transaction is returned early without applying. + +## TxConsequences Constructors + +The `TxConsequences` constructors (implemented here, declared in the header) build a compact summary of what the transaction "costs" the account's queue position. The base constructor extracts the fee from `sfFee` only if it is native XRP and non-negative — a defensive guard against malformed or exotic fee fields. Derived constructors layer on `potentialSpend` (for transactions like Payment that may consume more XRP than just the fee), a `blocker` flag, or a custom `sequencesConsumed` count (for multi-sequence-consuming operations). This structure feeds the `TxQ` logic that decides which transactions from an account can be tentatively accepted. + +## Key Design Decisions + +The prohibition on including transactor headers directly in this file is architectural: it keeps compile-time dependencies minimal and forces all type-specific behavior through the macro-generated dispatch. The compile-time polymorphism pattern (static methods with name hiding rather than virtual functions) is used throughout the `Transactor` hierarchy specifically because it allows the dispatch to be resolved at compile time via template instantiation, with the switch table as the only runtime cost. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/AMMInvariant.cpp.ai.json b/src/libxrpl/tx/invariants/AMMInvariant.cpp.ai.json new file mode 100644 index 0000000000..dafe4dde26 --- /dev/null +++ b/src/libxrpl/tx/invariants/AMMInvariant.cpp.ai.json @@ -0,0 +1,546 @@ +{ + "args": [ + { + "lineno": 10, + "name": "isDelete" + }, + { + "lineno": 11, + "name": "before" + }, + { + "lineno": 12, + "name": "after" + }, + { + "lineno": 33, + "name": "amount" + }, + { + "lineno": 33, + "name": "amount2" + }, + { + "lineno": 33, + "name": "lptAMMBalance" + }, + { + "lineno": 34, + "name": "zeroAllowed" + }, + { + "lineno": 46, + "name": "enforce" + }, + { + "lineno": 46, + "name": "j" + }, + { + "lineno": 111, + "name": "res" + }, + { + "lineno": 82, + "name": "tx" + }, + { + "lineno": 82, + "name": "view" + }, + { + "lineno": 200, + "name": "result" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ValidAMM::visitEntry", + "ValidAMM::finalizeVote / finalizeBid / finalizeCreate / finalizeDelete / finalizeDEX / finalizeDeposit / finalizeWithdraw / generalInvariant" + ], + "entry_point": "ValidAMM::visitEntry", + "purpose": "Collects before/after SLE state for AMM-related objects during a transaction, then triggers invariant checks at transaction finalization.", + "validation_points": [ + "visitEntry: Validates SLE type and field presence (AMM, LPTokenBalance, AMM pool change)", + "finalizeVote: Validates AMM pool and LPTokenBalance invariance on vote", + "finalizeBid: Validates AMM pool invariance and LPTokenBalance decrease on bid", + "finalizeCreate: Validates AMM object creation, pool balances, and LPToken math" + ] + }, + { + "call_chain": [ + "ValidAMM::finalizeVote" + ], + "entry_point": "ValidAMM::finalizeVote", + "purpose": "Checks that AMM pool and LPTokenBalance did not change during a vote transaction.", + "validation_points": [ + "finalizeVote: Compares lptAMMBalanceBefore_ and lptAMMBalanceAfter_, and ammPoolChanged_" + ] + }, + { + "call_chain": [ + "ValidAMM::finalizeBid" + ], + "entry_point": "ValidAMM::finalizeBid", + "purpose": "Checks that AMM pool did not change and LPTokenBalance only decreased during a bid.", + "validation_points": [ + "finalizeBid: Checks ammPoolChanged_, and that lptAMMBalanceAfter_ <= lptAMMBalanceBefore_ and > 0" + ] + }, + { + "call_chain": [ + "ValidAMM::finalizeCreate", + "ammPoolHolds", + "validBalances", + "ammLPTokens" + ], + "entry_point": "ValidAMM::finalizeCreate", + "purpose": "Checks that AMM object was created, pool balances are positive, and LPToken math is correct.", + "validation_points": [ + "finalizeCreate: Checks ammAccount_ presence, validBalances, and LPToken math" + ] + } + ], + "data_flows": [ + { + "field": "AMM SLE (ltAMM)", + "flow": [ + "SLE (after)", + "visitEntry: after->getType() == ltAMM", + "visitEntry: after->getAccountID(sfAccount), after->getFieldAmount(sfLPTokenBalance)", + "Stored in ammAccount_, lptAMMBalanceAfter_" + ], + "origin": "Ledger entry (SLE) passed to visitEntry (before/after)", + "transformations": [ + "Type check", + "Field extraction" + ], + "validated_at": "visitEntry" + }, + { + "field": "AMM pool change (ltRIPPLE_STATE/ltACCOUNT_ROOT)", + "flow": [ + "SLE (after)", + "visitEntry: after->getType() == ltRIPPLE_STATE or ltACCOUNT_ROOT", + "visitEntry: after->getFlags() & lsfAMMNode or after->isFieldPresent(sfAMMID)", + "Sets ammPoolChanged_" + ], + "origin": "Ledger entry (SLE) passed to visitEntry (after)", + "transformations": [ + "Type and flag/field presence check" + ], + "validated_at": "visitEntry" + }, + { + "field": "LPTokenBalance (before/after)", + "flow": [ + "SLE (before/after)", + "visitEntry: getFieldAmount(sfLPTokenBalance)", + "Stored in lptAMMBalanceBefore_/lptAMMBalanceAfter_" + ], + "origin": "Ledger entry (SLE) passed to visitEntry (before/after)", + "transformations": [ + "Type check", + "Field extraction" + ], + "validated_at": "visitEntry" + }, + { + "field": "AMM pool balances (amount, amount2)", + "flow": [ + "finalizeCreate", + "ammPoolHolds(view, *ammAccount_, ...)", + "Returns amount, amount2" + ], + "origin": "ammPoolHolds (called in finalizeCreate)", + "transformations": [ + "Ledger read", + "Asset extraction" + ], + "validated_at": "validBalances (called in finalizeCreate)" + }, + { + "field": "LPToken math", + "flow": [ + "finalizeCreate", + "ammLPTokens(amount, amount2, lptAMMBalanceAfter_->get())", + "Compared to *lptAMMBalanceAfter_" + ], + "origin": "ammLPTokens (called in finalizeCreate)", + "transformations": [ + "Math: sqrt(amount * amount2)" + ], + "validated_at": "finalizeCreate" + } + ], + "description": "Implements invariants and validation logic for Automated Market Maker (AMM) transactions in the XRPL ledger, ensuring AMM-related state changes conform to protocol rules.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "AMM SLE type (ltAMM), AccountID (sfAccount), LPTokenBalance (sfLPTokenBalance)", + "empty", + "string", + "validation" + ], + "evidence": "type check and field presence (after->getType(), after->getAccountID, after->getFieldAmount) at visitEntry", + "issue_pattern": "Missing empty string validation for AMM SLE type (ltAMM), AccountID (sfAccount), LPTokenBalance (sfLPTokenBalance)", + "why_false_positive": "type check and field presence (after->getType(), after->getAccountID, after->getFieldAmount) validates AMM SLE type (ltAMM), AccountID (sfAccount), LPTokenBalance (sfLPTokenBalance) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "AMM pool change (ltRIPPLE_STATE with lsfAMMNode, ltACCOUNT_ROOT with sfAMMID)", + "empty", + "string", + "validation" + ], + "evidence": "type and flag/field presence check (after->getType(), after->getFlags(), after->isFieldPresent) at visitEntry", + "issue_pattern": "Missing empty string validation for AMM pool change (ltRIPPLE_STATE with lsfAMMNode, ltACCOUNT_ROOT with sfAMMID)", + "why_false_positive": "type and flag/field presence check (after->getType(), after->getFlags(), after->isFieldPresent) validates AMM pool change (ltRIPPLE_STATE with lsfAMMNode, ltACCOUNT_ROOT with sfAMMID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "LPTokenBalance (before)", + "empty", + "string", + "validation" + ], + "evidence": "type check and field extraction (before->getType(), before->getFieldAmount) at visitEntry", + "issue_pattern": "Missing empty string validation for LPTokenBalance (before)", + "why_false_positive": "type check and field extraction (before->getType(), before->getFieldAmount) validates LPTokenBalance (before) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM pool and LPTokenBalance invariance on vote", + "empty", + "string", + "validation" + ], + "evidence": "business logic (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) at finalizeVote", + "issue_pattern": "Missing empty string validation for AMM pool and LPTokenBalance invariance on vote", + "why_false_positive": "business logic (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_) validates AMM pool and LPTokenBalance invariance on vote for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM pool invariance on bid", + "empty", + "string", + "validation" + ], + "evidence": "business logic (ammPoolChanged_) at finalizeBid", + "issue_pattern": "Missing empty string validation for AMM pool invariance on bid", + "why_false_positive": "business logic (ammPoolChanged_) validates AMM pool invariance on bid for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "LPTokenBalance decrease on bid", + "empty", + "string", + "validation" + ], + "evidence": "business logic (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero) at finalizeBid", + "issue_pattern": "Missing empty string validation for LPTokenBalance decrease on bid", + "why_false_positive": "business logic (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero) validates LPTokenBalance decrease on bid for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM balances (amount, amount2, lptAMMBalance)", + "empty", + "string", + "validation" + ], + "evidence": "validBalances function (comparison with beast::zero) at validBalances", + "issue_pattern": "Missing empty string validation for AMM balances (amount, amount2, lptAMMBalance)", + "why_false_positive": "validBalances function (comparison with beast::zero) validates AMM balances (amount, amount2, lptAMMBalance) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/AMMInvariant.cpp", + "functions": [ + { + "args": [ + "isDelete", + "before", + "after" + ], + "lineno": 9, + "name": "ValidAMM::visitEntry" + }, + { + "args": [ + "amount", + "amount2", + "lptAMMBalance", + "zeroAllowed" + ], + "lineno": 32, + "name": "validBalances" + }, + { + "args": [ + "enforce", + "j" + ], + "lineno": 45, + "name": "ValidAMM::finalizeVote" + }, + { + "args": [ + "enforce", + "j" + ], + "lineno": 59, + "name": "ValidAMM::finalizeBid" + }, + { + "args": [ + "tx", + "view", + "enforce", + "j" + ], + "lineno": 81, + "name": "ValidAMM::finalizeCreate" + }, + { + "args": [ + "enforce", + "res", + "j" + ], + "lineno": 110, + "name": "ValidAMM::finalizeDelete" + }, + { + "args": [ + "enforce", + "j" + ], + "lineno": 126, + "name": "ValidAMM::finalizeDEX" + }, + { + "args": [ + "tx", + "view", + "zeroAllowed", + "j" + ], + "lineno": 140, + "name": "ValidAMM::generalInvariant" + }, + { + "args": [ + "tx", + "view", + "enforce", + "j" + ], + "lineno": 170, + "name": "ValidAMM::finalizeDeposit" + }, + { + "args": [ + "tx", + "view", + "enforce", + "j" + ], + "lineno": 185, + "name": "ValidAMM::finalizeWithdraw" + }, + { + "args": [ + "tx", + "result", + "", + "view", + "j" + ], + "lineno": 199, + "name": "ValidAMM::finalize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "AMM invariants are typically tested in integration/tx/AMM_invariant_test.cpp or similar files. These tests cover creation, bid, vote, and pool change scenarios. However, some error paths (e.g., LCOV_EXCL_START blocks) may not be directly tested, especially for enforcement=false or unreachable error states. Edge cases for field absence or malformed SLEs may not be fully covered. Unit tests for validBalances and LPToken math may exist but are not guaranteed.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic, no external validation framework", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "none (no exception, just sets member variables)", + "field": "AMM SLE type (ltAMM), AccountID (sfAccount), LPTokenBalance (sfLPTokenBalance)", + "location": "visitEntry", + "validated_by": "type check and field presence (after->getType(), after->getAccountID, after->getFieldAmount)", + "validates": [ + "Checks if the SLE is of type ltAMM", + "Extracts AccountID and LPTokenBalance if so" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (just sets ammPoolChanged_ flag)", + "field": "AMM pool change (ltRIPPLE_STATE with lsfAMMNode, ltACCOUNT_ROOT with sfAMMID)", + "location": "visitEntry", + "validated_by": "type and flag/field presence check (after->getType(), after->getFlags(), after->isFieldPresent)", + "validates": [ + "Checks if SLE is a pool-related entry by type and flag/field" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none", + "field": "LPTokenBalance (before)", + "location": "visitEntry", + "validated_by": "type check and field extraction (before->getType(), before->getFieldAmount)", + "validates": [ + "Checks if the previous SLE is ltAMM and extracts LPTokenBalance" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false if enforce is true (no exception, signals invariant failure)", + "field": "AMM pool and LPTokenBalance invariance on vote", + "location": "finalizeVote", + "validated_by": "business logic (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_)", + "validates": [ + "Ensures LPTokenBalance and pool do not change during a vote" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false if enforce is true (no exception, signals invariant failure)", + "field": "AMM pool invariance on bid", + "location": "finalizeBid", + "validated_by": "business logic (ammPoolChanged_)", + "validates": [ + "Ensures pool does not change during a bid" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false if enforce is true (no exception, signals invariant failure)", + "field": "LPTokenBalance decrease on bid", + "location": "finalizeBid", + "validated_by": "business logic (*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ || *lptAMMBalanceAfter_ <= beast::zero)", + "validates": [ + "Ensures LPTokenBalance only decreases and remains positive on bid" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception, used as boolean check)", + "field": "AMM balances (amount, amount2, lptAMMBalance)", + "location": "validBalances", + "validated_by": "validBalances function (comparison with beast::zero)", + "validates": [ + "Ensures all balances are positive, or all are zero if zeroAllowed" + ], + "validation_type": "range|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/AMMInvariant.cpp.ai.md b/src/libxrpl/tx/invariants/AMMInvariant.cpp.ai.md new file mode 100644 index 0000000000..3d9cf4fe7b --- /dev/null +++ b/src/libxrpl/tx/invariants/AMMInvariant.cpp.ai.md @@ -0,0 +1,57 @@ +# `AMMInvariant.cpp` — Post-transaction Invariant Checker for AMM State + +## Role in the System + +This file implements `ValidAMM`, one of the specialized invariant checkers that XRPL runs as a final safety net after every transaction is applied. It participates in the tuple-based `InvariantChecks` system defined in `InvariantCheck.h`, where every checker exposes two methods: `visitEntry`, called once per modified ledger entry, and `finalize`, called when the transaction is complete. + +The purpose of `ValidAMM` is to assert that AMM-related ledger state is mathematically consistent after any transaction that touches an AMM — whether that transaction is purpose-built (`ttAMM_CREATE`, `ttAMM_DEPOSIT`, etc.) or incidental (a `ttPAYMENT` or `ttOFFER_CREATE` that routed through AMM liquidity). Invariants in XRPL are the last line of defense: if a bug in the transaction processor allows illegal state to form, the invariant checker can reject the transaction entirely before it commits to the ledger. + +## State Accumulation in `visitEntry` + +`ValidAMM` uses a two-phase design that is common across all XRPL invariant checkers. During the first phase, `visitEntry` scans every SLE (serialized ledger entry) touched by the transaction — both before and after mutation — and records three pieces of state: + +- `ammAccount_`: the account ID of the AMM pseudo-account, populated from any `ltAMM` entry seen in `after`. +- `lptAMMBalanceAfter_` and `lptAMMBalanceBefore_`: the LP token supply recorded from the `after` and `before` snapshots of the `ltAMM` object, respectively. +- `ammPoolChanged_`: a boolean set to `true` if any trust-line entry bearing `lsfAMMNode`, or any account root carrying `sfAMMID`, was touched — meaning the on-pool reserves themselves changed. + +Deletion events are ignored entirely; the `isDelete` flag causes an early return, since object removal is handled through the presence or absence of `ammAccount_` and the LP balance fields after the fact. + +The pool-change detection deliberately unifies two SLE types: `ltRIPPLE_STATE` entries tagged `lsfAMMNode` hold the fungible-token reserves, while `ltACCOUNT_ROOT` entries tagged `sfAMMID` hold the XRP reserve. Either kind of mutation sets `ammPoolChanged_`. + +## The `finalize` Dispatch + +After all entries are visited, `finalize` is called with the full transaction and a `ReadView`. It immediately discards failed transactions — with the deliberate exception of `tecINCOMPLETE`, which arises during `ttAMM_DELETE` when there are too many trust lines to delete in a single pass. All other TER codes that are not `tesSUCCESS` skip invariant checks, since failed transactions must not modify AMM-relevant state in the first place (a separate invariant elsewhere would catch that). + +A single `bool enforce` flag is derived from `view.rules().enabled(fixAMMv1_3)`. This is the XRPL amendment-activation pattern: when the amendment is not yet active, violations are logged but do not cause the transaction to fail. Once the amendment activates on-network, violations become hard failures that reject the transaction. This lets the invariant ship before activation without disrupting the network. + +The dispatch then branches by transaction type, delegating to one of seven `finalize*` methods. + +## Per-transaction Invariants + +**`finalizeVote`**: An `AMMVote` transaction adjusts fee parameters through a weighted-vote mechanism. It must not change the LP token supply or the pool reserves at all. The check simply compares `lptAMMBalanceBefore_` and `lptAMMBalanceAfter_` for equality and asserts `ammPoolChanged_` is false. + +**`finalizeBid`**: An `AMMBid` transaction burns LP tokens to win the auction slot. The pool reserves are untouched (`ammPoolChanged_` must be false), and the LP token supply must strictly decrease and remain above zero. + +**`finalizeCreate`**: This is the most mathematically demanding case. After creation, `finalizeCreate` reads the live pool balances using `ammPoolHolds`, then calls `ammLPTokens` (which computes `sqrt(asset1 * asset2)`) and checks for exact equality with the recorded LP token balance. All three balances — both pool assets and the LP supply — must be strictly positive. There is no tolerance here: at creation the geometric-mean formula must hold exactly. + +**`finalizeDelete`**: On a successful `AMMDelete`, the AMM object must no longer exist, so `ammAccount_` must be empty. On `tecINCOMPLETE` (partial deletion), the object must also be unmodified from the invariant's perspective — `ammAccount_` must still be absent because the check relies on it not having been touched in a way that would be visible here. + +**`finalizeDEX`**: DEX transactions — `ttPAYMENT`, `ttOFFER_CREATE`, and `ttCHECK_CASH` — route through AMM pool swaps but must never modify the `ltAMM` ledger object itself (only the pool trust-lines change). If `ammAccount_` is populated it means the AMM object was incorrectly written, and the invariant fails. + +**`finalizeDeposit` / `finalizeWithdraw`**: Both delegate to `generalInvariant` after confirming the AMM object still exists for deposit (and handling the case of final withdrawal deleting it). + +## The General Pool Invariant + +`generalInvariant` encodes the core AMM mathematical property for deposits and withdrawals: + +``` +sqrt(poolAsset1 × poolAsset2) ≥ LPTokenBalance +``` + +This is the constant-product invariant expressed in geometric-mean form. The strong check is a direct `>=` comparison. Because floating-point rounding in `STAmount` arithmetic can produce a `poolProductMean` that is very slightly *less* than the LP balance even when the operation was mathematically valid, a weak fallback is also provided: `withinRelativeDistance` accepts a relative error up to `1e-11`. + +The `ZeroAllowed` enum handles the edge case cleanly: on withdrawal, if the last LP redeems all tokens, the pool drains to zero and the AMM object is deleted. `ZeroAllowed::Yes` allows the condition where all three quantities are simultaneously zero, which is the correct post-state for a final withdrawal or clawback. Deposits, by contrast, always require strict positivity (`ZeroAllowed::No`). + +## Enforcement and Defensive Patterns + +The `LCOV_EXCL_START/STOP` markers on most error branches reflect the expectation that these paths should be unreachable if transaction processing is correct. They exist as a belt-and-suspenders guard against hypothetical bugs, not as regular error paths. The `enforce` flag separates the diagnostic role (always log) from the enforcement role (only reject when the amendment is active), allowing safe deployment before full network activation of `fixAMMv1_3`. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/FreezeInvariant.cpp.ai.json b/src/libxrpl/tx/invariants/FreezeInvariant.cpp.ai.json new file mode 100644 index 0000000000..365992b751 --- /dev/null +++ b/src/libxrpl/tx/invariants/FreezeInvariant.cpp.ai.json @@ -0,0 +1,418 @@ +{ + "args": [ + { + "lineno": 13, + "name": "isDelete" + }, + { + "lineno": 14, + "name": "before" + }, + { + "lineno": 15, + "name": "after" + }, + { + "lineno": 39, + "name": "tx" + }, + { + "lineno": 40, + "name": "ter" + }, + { + "lineno": 41, + "name": "fee" + }, + { + "lineno": 42, + "name": "view" + }, + { + "lineno": 43, + "name": "j" + }, + { + "lineno": 158, + "name": "issuerID" + }, + { + "lineno": 166, + "name": "changes" + }, + { + "lineno": 169, + "name": "enforce" + }, + { + "lineno": 166, + "name": "issuer" + }, + { + "lineno": 195, + "name": "change" + }, + { + "lineno": 196, + "name": "high" + }, + { + "lineno": 199, + "name": "globalFreeze" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TransfersNotFrozen::visitEntry", + "TransfersNotFrozen::isValidEntry", + "TransfersNotFrozen::calculateBalanceChange", + "TransfersNotFrozen::recordBalanceChanges" + ], + "entry_point": "TransfersNotFrozen::visitEntry", + "purpose": "Processes each ledger entry change (before/after) during a transaction, validates if the entry is relevant, computes balance changes, and records them for later invariant checking.", + "validation_points": [ + "TransfersNotFrozen::isValidEntry (validates entry types and presence)", + "balanceChange.signum() == 0 (validates if there is a non-zero balance change)" + ] + }, + { + "call_chain": [ + "TransfersNotFrozen::finalize", + "TransfersNotFrozen::findIssuer", + "TransfersNotFrozen::validateIssuerChanges" + ], + "entry_point": "TransfersNotFrozen::finalize", + "purpose": "After all entries are visited, this function validates that no frozen assets were transferred by checking all recorded balance changes against issuer freeze state.", + "validation_points": [ + "TransfersNotFrozen::findIssuer (validates issuer existence)", + "TransfersNotFrozen::validateIssuerChanges (validates that issuer changes are allowed given freeze state)" + ] + } + ], + "data_flows": [ + { + "field": "before/after SLE entries", + "flow": [ + "visitEntry (input)", + "isValidEntry (type and presence validation)", + "calculateBalanceChange (computes delta)", + "recordBalanceChanges (stores for finalize)" + ], + "origin": "Ledger entry snapshots before and after transaction, passed to visitEntry", + "transformations": [ + "Type checked (isValidEntry)", + "Delta computed (calculateBalanceChange)" + ], + "validated_at": "isValidEntry" + }, + { + "field": "balanceChange", + "flow": [ + "calculateBalanceChange", + "visitEntry (checks signum)", + "recordBalanceChanges (stores if non-zero)" + ], + "origin": "Result of calculateBalanceChange(before, after, isDelete)", + "transformations": [ + "Computed as difference between before and after balances" + ], + "validated_at": "visitEntry (balanceChange.signum() == 0)" + }, + { + "field": "issuerSle", + "flow": [ + "finalize (for each balance change)", + "findIssuer (looks up issuer SLE in ledger view)", + "validateIssuerChanges (uses issuerSle for freeze checks)" + ], + "origin": "findIssuer(issue.account, view) in finalize", + "transformations": [ + "Looked up from ledger by account" + ], + "validated_at": "finalize (if (!issuerSle) ...)" + }, + { + "field": "issuer changes", + "flow": [ + "finalize (iterates over balanceChanges_)", + "validateIssuerChanges (checks if changes are allowed given freeze state)" + ], + "origin": "balanceChanges_ collected during visitEntry", + "transformations": [ + "Aggregated per issuer/issue" + ], + "validated_at": "validateIssuerChanges" + } + ], + "description": "Implements the TransfersNotFrozen invariant for the XRPL ledger, ensuring that asset transfers do not violate freeze restrictions on trust lines, including logic for tracking, validating, and enforcing freeze states during ledger modifications.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "before/after SLE entries", + "empty", + "string", + "validation" + ], + "evidence": "isValidEntry(before, after) at TransfersNotFrozen::visitEntry", + "issue_pattern": "Missing empty string validation for before/after SLE entries", + "why_false_positive": "isValidEntry(before, after) validates before/after SLE entries for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "balanceChange", + "empty", + "string", + "validation" + ], + "evidence": "balanceChange.signum() == 0 at TransfersNotFrozen::visitEntry", + "issue_pattern": "Missing empty string validation for balanceChange", + "why_false_positive": "balanceChange.signum() == 0 validates balanceChange for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "issuerSle existence", + "empty", + "string", + "validation" + ], + "evidence": "findIssuer(issue.account, view) at TransfersNotFrozen::finalize", + "issue_pattern": "Missing empty string validation for issuerSle existence", + "why_false_positive": "findIssuer(issue.account, view) validates issuerSle existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "issuer changes", + "empty", + "string", + "validation" + ], + "evidence": "validateIssuerChanges(issuerSle, changes, tx, j, enforce) at TransfersNotFrozen::finalize", + "issue_pattern": "Missing empty string validation for issuer changes", + "why_false_positive": "validateIssuerChanges(issuerSle, changes, tx, j, enforce) validates issuer changes for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/FreezeInvariant.cpp", + "functions": [ + { + "args": [ + "isDelete", + "before", + "after" + ], + "lineno": 12, + "name": "TransfersNotFrozen::visitEntry" + }, + { + "args": [ + "tx", + "ter", + "fee", + "view", + "j" + ], + "lineno": 38, + "name": "TransfersNotFrozen::finalize" + }, + { + "args": [ + "before", + "after" + ], + "lineno": 87, + "name": "TransfersNotFrozen::isValidEntry" + }, + { + "args": [ + "before", + "after", + "isDelete" + ], + "lineno": 104, + "name": "TransfersNotFrozen::calculateBalanceChange" + }, + { + "args": [ + "issue", + "change" + ], + "lineno": 132, + "name": "TransfersNotFrozen::recordBalance" + }, + { + "args": [ + "after", + "balanceChange" + ], + "lineno": 146, + "name": "TransfersNotFrozen::recordBalanceChanges" + }, + { + "args": [ + "issuerID", + "view" + ], + "lineno": 157, + "name": "TransfersNotFrozen::findIssuer" + }, + { + "args": [ + "issuer", + "changes", + "tx", + "j", + "enforce" + ], + "lineno": 165, + "name": "TransfersNotFrozen::validateIssuerChanges" + }, + { + "args": [ + "change", + "high", + "tx", + "j", + "enforce", + "globalFreeze" + ], + "lineno": 194, + "name": "TransfersNotFrozen::validateFrozenState" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "FreezeInvariant is typically tested via integration and invariant test suites, e.g., in rippled/test/invariant_check_test.cpp, rippled/test/Freeze_test.cpp, and possibly in broader transaction or trustline tests. These tests cover scenarios like trustline freeze, global freeze, and attempts to transfer frozen assets. Gaps may exist in edge cases: e.g., missing issuer SLE, complex multi-hop paths, or deep freeze amendment toggling. Direct unit tests for isValidEntry, calculateBalanceChange, and validateIssuerChanges may be limited; most coverage is likely via higher-level transaction tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "none (early return)", + "field": "before/after SLE entries", + "location": "TransfersNotFrozen::visitEntry", + "validated_by": "isValidEntry(before, after)", + "validates": [ + "Checks if the ledger entry pair (before, after) is valid for processing", + "Likely checks type, existence, and/or relevant fields" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (early return)", + "field": "balanceChange", + "location": "TransfersNotFrozen::visitEntry", + "validated_by": "balanceChange.signum() == 0", + "validates": [ + "Checks if the balance change is non-zero before recording" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (assertion failure if enforce is true), otherwise continue", + "field": "issuerSle existence", + "location": "TransfersNotFrozen::finalize", + "validated_by": "findIssuer(issue.account, view)", + "validates": [ + "Checks that the issuer's ledger entry exists in the current view" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.95, + "error_thrown": "returns false (invariant violation)", + "field": "issuer changes", + "location": "TransfersNotFrozen::finalize", + "validated_by": "validateIssuerChanges(issuerSle, changes, tx, j, enforce)", + "validates": [ + "Checks that the issuer's freeze state and the changes are valid according to business rules", + "Likely checks for illegal asset movement when frozen" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/FreezeInvariant.cpp.ai.md b/src/libxrpl/tx/invariants/FreezeInvariant.cpp.ai.md new file mode 100644 index 0000000000..574018d36d --- /dev/null +++ b/src/libxrpl/tx/invariants/FreezeInvariant.cpp.ai.md @@ -0,0 +1,62 @@ +# `FreezeInvariant.cpp` — Enforcing Frozen Asset Transfer Restrictions + +## Role in the System + +`FreezeInvariant.cpp` implements `TransfersNotFrozen`, one of the post-transaction invariant checkers that form the XRPL's last line of defense against consensus-breaking ledger mutations. After every transaction is applied — successful or not — the invariant framework streams every modified ledger entry through `visitEntry()` and then calls `finalize()` to render a pass/fail verdict. A `false` return from `finalize()` causes the entire transaction to be rolled back. + +This particular invariant answers one question: *did this transaction move token balances across a frozen trust line?* Freeze rules on XRPL are non-trivial to evaluate in isolation, which is why the checker accumulates state across the full set of affected trust lines before making any judgement. + +## Why Two-Phase Collection Is Necessary + +The comment in `visitEntry()` makes the key insight explicit: *"A trust line freeze state alone doesn't determine if a transfer is frozen."* A payment might touch multiple trust lines — the sender's, the receiver's, intermediate offer lines. Whether a particular balance change is legally frozen depends on directionality and which side of the line set the freeze flag. More importantly, whether a transfer is to or from the issuer determines whether freeze restrictions apply at all. + +This forces a collect-then-validate architecture. During `visitEntry()`, the checker accepts `ltRIPPLE_STATE` (trust line) entries and `ltACCOUNT_ROOT` entries. Account roots are silently catalogued in `possibleIssuers_` — a local cache so `finalize()` can resolve issuers without hitting the ledger view for every account that was already touched by the transaction. Trust line changes are decomposed into `BalanceChange` records and grouped into `balanceChanges_`, a `std::map` keyed by currency and issuer. + +The `IssuerChanges` struct separates balance decreases (`senders`) from increases (`receivers`). This split is what enables the critical issuer-transfer exemption: if `changes.receivers` is empty, all the outflow is going back to the issuer. If `changes.senders` is empty, the issuer is distributing tokens. Either direction is unconditionally allowed — freeze restrictions only apply to peer-to-peer transfers that bypass the issuer. + +## Trust Line Orientation and Balance Sign + +XRPL trust lines have a canonical orientation determined by account ID comparison. The "low" side has the numerically lower account ID. The `sfBalance` field is recorded from the low side's perspective, so a positive balance means the low account holds tokens. `recordBalanceChanges()` exploits this: it computes the balance delta, records it once for the high account as issuer (using the delta sign directly) and once for the low account as issuer (negating the sign), because the same raw balance movement looks opposite from each issuer's perspective. + +The regular freeze flags follow the same orientation: `lsfHighFreeze` means the *high* side froze the *low* side's outbound transfers, and vice versa. In `validateFrozenState()`, the `high` boolean indicates whether the issuer under scrutiny sits on the high side of this particular trust line, which determines which freeze flag to read: + +```cpp +bool const freeze = + change.balanceChangeSign < 0 && change.line->isFlag(high ? lsfLowFreeze : lsfHighFreeze); +``` + +Note that regular freeze is directional — it only applies when the frozen party is the *sender* (`balanceChangeSign < 0`). Deep freeze (`lsfLowDeepFreeze` / `lsfHighDeepFreeze`) is unconditional: it blocks all movement regardless of who initiated the transfer. + +## The Three Freeze Tiers + +`validateFrozenState()` aggregates three independent freeze conditions into a single `frozen` boolean: + +1. **Global freeze** (`lsfGlobalFreeze` on the issuer's account root): the issuer has halted all transfers of their currency on the entire network. +2. **Regular freeze** (`lsfLowFreeze` / `lsfHighFreeze`): the issuer has frozen one specific counterparty's outbound transfers. Direction-sensitive. +3. **Deep freeze** (`lsfLowDeepFreeze` / `lsfHighDeepFreeze`): the issuer has bilaterally frozen a specific trust line, blocking both inbound and outbound movement regardless of directionality. + +Any of the three being true is sufficient to block the transfer. + +## AMMClawback Exception + +The only transaction type that carries the `overrideFreeze` privilege (as declared in `transactions.macro`) is `AMMClawback`. When `hasPrivilege(tx, overrideFreeze)` returns true, the invariant relaxes freeze enforcement — but not unconditionally. AMM-pool trust lines (`lsfAMMNode`) can be clawed back through regular and deep freezes, because AMM clawback is specifically designed to recover tokens from frozen AMM positions. However, a global freeze cannot be overridden, and regular (non-AMM) trust lines cannot be clawed back even with the privilege. The condition: + +```cpp +if ((!isAMMLine || globalFreeze) && hasPrivilege(tx, overrideFreeze)) +``` + +reads as: "if this is an AMM line and not globally frozen, AMMClawback is permitted to proceed." The logic is inverted for readability — if the condition is *not* met, we fall through to the fatal log and failure return. + +## Amendment-Gated Enforcement and the `enforce` Pattern + +A notable piece of defensive engineering lives in `finalize()`. The `enforce` variable is set to `view.rules().enabled(featureDeepFreeze)`, tying hard enforcement to the DeepFreeze amendment. The detailed comment in the source explains the rationale: the invariant runs its detection logic regardless of amendment status so that operators monitoring fatal-level logs get early warning of exploits even before the amendment is live. If a freeze bypass were discovered, node operators would see fatal log output; the XRPL community could then expedite amendment activation or deploy a hotfix amendment, and only the single `enforce =` line would need to change. + +This pattern also interacts with `XRPL_ASSERT(enforce, ...)`. As documented in `InvariantCheckPrivilege.h`, `assert(enforce)` is intentionally counterintuitive: the assert fires when `enforce` is false (amendment disabled) *and* an invariant violation is detected. In debug builds this crashes the process, catching developer mistakes in tests that exercise the invariant without enabling the amendment. In release builds, if `enforce` is false, the invariant logs the violation but returns `true` (allowing the transaction through), acting as a monitoring-only probe. + +## Boundary Cases in Balance Calculation + +`calculateBalanceChange()` handles two subtle edge cases for dynamically created and deleted trust lines. When a trust line is created mid-transaction (by a Payment crossing offers, for example), `before` is null and the pre-existing balance is treated as zero — so the full post-transaction balance counts as the change. When `isDelete` is true, the post-transaction balance is also treated as zero, ensuring that deletion of a frozen trust line is still caught as a balance movement rather than silently exempted as "nothing remains." Both cases prevent a loophole where creating or deleting a trust line could bypass freeze checks by making the balance appear unchanged. + +## Relationship to Surrounding Files + +`FreezeInvariant.cpp` is one of roughly ten invariant implementations in `src/libxrpl/tx/invariants/`. The class is registered in the `InvariantChecks` tuple in `InvariantCheck.h` alongside checkers for XRP totals, account creation, NFTs, AMM pools, and vaults. All checkers share the same two-method contract (`visitEntry` / `finalize`) and the same privilege system via `InvariantCheckPrivilege.h`. The privilege system centralizes transaction-type-to-capability mapping in a single X-macro expansion over `transactions.macro`, so adding a new transaction type that needs freeze-override capability requires only a single change to that macro file — the invariant checker code never needs to be updated. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp.ai.json b/src/libxrpl/tx/invariants/InvariantCheck.cpp.ai.json new file mode 100644 index 0000000000..2994e3befe --- /dev/null +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp.ai.json @@ -0,0 +1,586 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ApplyTx", + "InvariantChecks::visitEntry (for each invariant)", + "InvariantChecks::finalize (for each invariant)" + ], + "entry_point": "ApplyTx (or similar transaction application function)", + "purpose": "Applies a transaction, running all registered invariants to validate ledger changes and transaction properties.", + "validation_points": [ + "TransactionFeeCheck::finalize", + "hasPrivilege" + ] + }, + { + "call_chain": [ + "TransactionFeeCheck::visitEntry", + "TransactionFeeCheck::finalize" + ], + "entry_point": "TransactionFeeCheck invariant", + "purpose": "Validates that the transaction fee is non-negative, below system maximum, and does not exceed the fee specified in the transaction.", + "validation_points": [ + "TransactionFeeCheck::finalize" + ] + }, + { + "call_chain": [ + "hasPrivilege" + ], + "entry_point": "Privilege check for transaction type", + "purpose": "Checks if a transaction type has the required privilege.", + "validation_points": [ + "hasPrivilege" + ] + } + ], + "data_flows": [ + { + "field": "fee (XRPAmount)", + "flow": [ + "ApplyTx", + "TransactionFeeCheck::finalize (fee argument)", + "Validation logic (fee.drops() < 0, fee >= INITIAL_XRP, fee > tx.sfFee)" + ], + "origin": "Computed during transaction application (ApplyTx), passed to invariants", + "transformations": [ + "Checked for negativity", + "Checked against system maximum (INITIAL_XRP)", + "Compared to tx.sfFee" + ], + "validated_at": "TransactionFeeCheck::finalize" + }, + { + "field": "tx.sfFee (STAmount)", + "flow": [ + "STTx", + "TransactionFeeCheck::finalize (tx.getFieldAmount(sfFee))" + ], + "origin": "Field in the transaction (STTx)", + "transformations": [ + "Extracted from transaction", + "Converted to XRPAmount via .xrp()" + ], + "validated_at": "TransactionFeeCheck::finalize" + }, + { + "field": "transaction type privilege", + "flow": [ + "STTx", + "hasPrivilege" + ], + "origin": "Transaction type (STTx::getTxnType())", + "transformations": [ + "Transaction type mapped to privilege mask via macro", + "Bitwise check against required privilege" + ], + "validated_at": "hasPrivilege" + } + ], + "description": "Implements various transaction and ledger invariants for the XRPL, ensuring ledger integrity by checking for invalid state changes after transaction application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fee (XRPAmount)", + "empty", + "string", + "validation" + ], + "evidence": "TransactionFeeCheck::finalize at TransactionFeeCheck::finalize", + "issue_pattern": "Missing empty string validation for fee (XRPAmount)", + "why_false_positive": "TransactionFeeCheck::finalize validates fee (XRPAmount) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "fee (XRPAmount)", + "range", + "bounds", + "validation" + ], + "evidence": "TransactionFeeCheck::finalize at TransactionFeeCheck::finalize", + "issue_pattern": "Missing range validation for fee (XRPAmount)", + "why_false_positive": "TransactionFeeCheck::finalize validates fee (XRPAmount) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fee (XRPAmount)", + "empty", + "string", + "validation" + ], + "evidence": "TransactionFeeCheck::finalize at TransactionFeeCheck::finalize", + "issue_pattern": "Missing empty string validation for fee (XRPAmount)", + "why_false_positive": "TransactionFeeCheck::finalize validates fee (XRPAmount) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "fee (XRPAmount)", + "range", + "bounds", + "validation" + ], + "evidence": "TransactionFeeCheck::finalize at TransactionFeeCheck::finalize", + "issue_pattern": "Missing range validation for fee (XRPAmount)", + "why_false_positive": "TransactionFeeCheck::finalize validates fee (XRPAmount) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fee (XRPAmount) vs. tx.sfFee", + "empty", + "string", + "validation" + ], + "evidence": "TransactionFeeCheck::finalize at TransactionFeeCheck::finalize", + "issue_pattern": "Missing empty string validation for fee (XRPAmount) vs. tx.sfFee", + "why_false_positive": "TransactionFeeCheck::finalize validates fee (XRPAmount) vs. tx.sfFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "transaction type privilege", + "empty", + "string", + "validation" + ], + "evidence": "hasPrivilege at hasPrivilege", + "issue_pattern": "Missing empty string validation for transaction type privilege", + "why_false_positive": "hasPrivilege validates transaction type privilege for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/InvariantCheck.cpp", + "functions": [ + { + "args": [ + "STTx const& tx", + "Privilege priv" + ], + "lineno": 27, + "name": "hasPrivilege" + }, + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const&" + ], + "lineno": 44, + "name": "TransactionFeeCheck::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const", + "XRPAmount const fee", + "ReadView const&", + "beast::Journal const& j" + ], + "lineno": 49, + "name": "TransactionFeeCheck::finalize" + }, + { + "args": [ + "bool isDelete", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 72, + "name": "XRPNotCreated::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const", + "XRPAmount const fee", + "ReadView const&", + "beast::Journal const& j" + ], + "lineno": 108, + "name": "XRPNotCreated::finalize" + }, + { + "args": [ + "bool", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 126, + "name": "XRPBalanceChecks::visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const& j" + ], + "lineno": 148, + "name": "XRPBalanceChecks::finalize" + }, + { + "args": [ + "bool isDelete", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 163, + "name": "NoBadOffers::visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const& j" + ], + "lineno": 181, + "name": "NoBadOffers::finalize" + }, + { + "args": [ + "bool isDelete", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 196, + "name": "NoZeroEscrow::visitEntry" + }, + { + "args": [ + "STTx const& txn", + "TER const", + "XRPAmount const", + "ReadView const& rv", + "beast::Journal const& j" + ], + "lineno": 246, + "name": "NoZeroEscrow::finalize" + }, + { + "args": [ + "bool isDelete", + "std::shared_ptr const& before", + "std::shared_ptr const&" + ], + "lineno": 263, + "name": "AccountRootsNotDeleted::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const result", + "XRPAmount const", + "ReadView const&", + "beast::Journal const& j" + ], + "lineno": 270, + "name": "AccountRootsNotDeleted::finalize" + }, + { + "args": [ + "bool isDelete", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 295, + "name": "AccountRootsDeletedClean::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const result", + "XRPAmount const", + "ReadView const& view", + "beast::Journal const& j" + ], + "lineno": 301, + "name": "AccountRootsDeletedClean::finalize" + }, + { + "args": [ + "bool", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 370, + "name": "LedgerEntryTypesMatch::visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const& j" + ], + "lineno": 399, + "name": "LedgerEntryTypesMatch::finalize" + }, + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const& after" + ], + "lineno": 418, + "name": "NoXRPTrustLines::visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const& j" + ], + "lineno": 429, + "name": "NoXRPTrustLines::finalize" + }, + { + "args": [ + "bool", + "std::shared_ptr const&", + "std::shared_ptr const& after" + ], + "lineno": 441, + "name": "NoDeepFreezeTrustLinesWithoutFreeze::visitEntry" + }, + { + "args": [ + "STTx const&", + "TER const", + "XRPAmount const", + "ReadView const&", + "beast::Journal const& j" + ], + "lineno": 454, + "name": "NoDeepFreezeTrustLinesWithoutFreeze::finalize" + }, + { + "args": [ + "bool", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 466, + "name": "ValidNewAccountRoot::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const result", + "XRPAmount const", + "ReadView const& view", + "beast::Journal const& j" + ], + "lineno": 478, + "name": "ValidNewAccountRoot::finalize" + }, + { + "args": [ + "bool", + "std::shared_ptr const& before", + "std::shared_ptr const&" + ], + "lineno": 517, + "name": "ValidClawback::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const result", + "XRPAmount const", + "ReadView const& view", + "beast::Journal const& j" + ], + "lineno": 523, + "name": "ValidClawback::finalize" + }, + { + "args": [ + "bool isDelete", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 561, + "name": "ValidPseudoAccounts::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const", + "XRPAmount const", + "ReadView const& view", + "beast::Journal const& j" + ], + "lineno": 594, + "name": "ValidPseudoAccounts::finalize" + }, + { + "args": [ + "bool isDelete", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 613, + "name": "NoModifiedUnmodifiableFields::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const", + "XRPAmount const", + "ReadView const& view", + "beast::Journal const& j" + ], + "lineno": 622, + "name": "NoModifiedUnmodifiableFields::finalize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 18, + "name": "xrpl" + } + ], + "test_coverage_notes": "The invariants are typically tested in integration and unit tests under the rippled/test/ or xrplf/rippled/test/ directories, such as InvariantCheck_test.cpp, Transaction_test.cpp, or ApplyTx_test.cpp. These tests cover cases like negative fees, excessive fees, and privilege checks. However, gaps may exist for edge cases (e.g., maximum fee boundary, privilege escalation attempts, malformed transactions). Direct unit tests for hasPrivilege may be limited, as it is often exercised indirectly via transaction application tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom C++ logic, no external validation framework", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Logs fatal error, returns false (no exception thrown)", + "field": "fee (XRPAmount)", + "location": "TransactionFeeCheck::finalize", + "validated_by": "TransactionFeeCheck::finalize", + "validates": [ + "fee must not be negative (fee.drops() < 0)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "Logs fatal error, returns false (no exception thrown)", + "field": "fee (XRPAmount)", + "location": "TransactionFeeCheck::finalize", + "validated_by": "TransactionFeeCheck::finalize", + "validates": [ + "fee must be less than INITIAL_XRP (fee >= INITIAL_XRP)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "Logs fatal error, returns false (no exception thrown)", + "field": "fee (XRPAmount) vs. tx.sfFee", + "location": "TransactionFeeCheck::finalize", + "validated_by": "TransactionFeeCheck::finalize", + "validates": [ + "fee must not exceed the fee specified in the transaction (fee > tx.getFieldAmount(sfFee).xrp())" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "Returns false (no exception thrown)", + "field": "transaction type privilege", + "location": "hasPrivilege", + "validated_by": "hasPrivilege", + "validates": [ + "Checks if transaction type has required privilege" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/InvariantCheck.cpp.ai.md b/src/libxrpl/tx/invariants/InvariantCheck.cpp.ai.md new file mode 100644 index 0000000000..d2ce26dbd2 --- /dev/null +++ b/src/libxrpl/tx/invariants/InvariantCheck.cpp.ai.md @@ -0,0 +1,60 @@ +# `src/libxrpl/tx/invariants/InvariantCheck.cpp` + +## Role in the System + +This file is the primary implementation of XRPL's post-transaction invariant checking machinery. Every time a transaction is applied to the ledger, a suite of structural and financial invariants is run against the resulting state changes. If any invariant fails, the transaction's result is overridden with `tecINVARIANT_FAILED` (or `tefINVARIANT_FAILED` if a re-application also fails), ensuring that a corrupt or exploited transaction can never be committed to a validated ledger. + +The invariants here are the last line of defense. They are not pre-validation checks — they run *after* the transaction engine has already applied its logic, operating on the diff between the pre- and post-transaction ledger state. They exist to catch bugs in transaction processors, novel exploits, or edge cases that pre-validation missed. + +## The Two-Phase Visitor Framework + +Every invariant checker in this file follows the same interface defined in `InvariantCheck.h`: + +- `visitEntry(isDelete, before, after)` — called once per modified `SLE` (Serialized Ledger Entry) in the transaction's sandbox. This is where checkers accumulate state: counters, flags, or lists of entries to examine. +- `finalize(tx, result, fee, view, journal)` — called once after all entries have been visited. This is where the checker renders a verdict, returning `true` to pass or `false` to fail. + +The dispatch loop lives in `ApplyContext::checkInvariantsHelper()`, which holds an `InvariantChecks` tuple (a `std::tuple` of every checker type, enumerated in `InvariantCheck.h`). A variadic fold expression fires every checker's `visitEntry` for each changed SLE, then evaluates all `finalize()` calls. One critical design note in that code: it deliberately avoids a short-circuit `&&` fold for the finalizers — every invariant is evaluated and logs independently, so the full set of failures appears in the journal in a single transaction failure. + +## `hasPrivilege()` and the X-Macro Dispatch + +The free function `hasPrivilege(STTx const& tx, Privilege priv)` maps transaction types to `Privilege` bitmasks via the `transactions.macro` X-macro expansion. Each entry in that macro carries a privilege bitfield; `hasPrivilege` performs a bitwise AND test against it. This avoids a hand-maintained switch statement and keeps privilege assignments co-located with transaction type definitions. + +The `Privilege` enum in `InvariantCheckPrivilege.h` captures semantically distinct capabilities: `createAcct`, `mustDeleteAcct`, `mayDeleteAcct`, `createPseudoAcct`, `overrideFreeze`, `changeNFTCounts`, `createMPTIssuance`, and more. Having separate `must` and `may` variants allows invariants to distinguish transactions that are required to produce side effects (AccountDelete *must* delete exactly one account root) from those that can optionally do so (AMMWithdraw *may* delete an account root when LP token supply hits zero). + +## Core Invariants + +**`TransactionFeeCheck`** validates three fee constraints: the fee must not be negative, must not equal or exceed the total XRP supply (`INITIAL_XRP`), and must not exceed the fee declared in the transaction's `sfFee` field. The third constraint permits fee discounts but not fee surcharges — an important economic invariant. + +**`XRPNotCreated`** tracks the net XRP drop change across all `ltACCOUNT_ROOT`, `ltPAYCHAN`, and `ltESCROW` entries touched by the transaction. Payment channels track uncommitted XRP as `sfAmount - sfBalance` (the unclaimed portion), so deletions of pay channels and escrows are excluded from the tally since those fields are not adjusted at deletion time. The finalize check requires that `drops_` be non-positive (no XRP creation) and that `-drops_` exactly equals the fee charged. This ties fee accounting to XRP conservation. + +**`XRPBalanceChecks`** verifies that every modified account root carries a balance denominated in native XRP between 0 and `INITIAL_XRP` inclusive. This is a belt-and-suspenders check against type confusion — the `STAmount::native()` call verifies the currency code, not just the magnitude. + +**`NoBadOffers`** checks that DEX order entries contain non-negative amounts and that neither side of the offer is XRP-for-XRP. The XRP-to-XRP check uses `pays.native() && gets.native()` rather than comparing currency codes, matching what the XRP native type represents internally. + +**`NoZeroEscrow`** has grown into a multi-asset invariant. It checks escrow amounts for XRP (must be strictly positive and below `INITIAL_XRP`), IOU (must be positive, must not use the `badCurrency()` sentinel), and MPT (must be positive and below `maxMPTokenAmount`). The same invariant also validates `ltMPTOKEN_ISSUANCE` entries: outstanding amount must be within range, and locked amount must not exceed outstanding. Similarly `ltMPTOKEN` entries are checked. This reuse of a semantically-named escrow invariant for MPT accounting is a pragmatic bundling choice rather than a conceptual one. + +## Account Deletion Integrity + +Two checkers cooperate to ensure account deletions are clean: + +**`AccountRootsNotDeleted`** uses the privilege system to enforce cardinality rules: a transaction with `mustDeleteAcct` (AccountDelete, AMMDelete) must delete exactly one account root on success. A transaction with `mayDeleteAcct` (AMMWithdraw, AMMClawback) may delete one. All other transactions must not delete any. The `accountsDeleted_` counter is incremented in `visitEntry` and checked in `finalize` against the transaction result and its privilege set. + +**`AccountRootsDeletedClean`** stores the `(before, after)` SLE pair for each deleted account root and performs a thorough post-deletion audit in `finalize`. It checks that the deleted account's balance is zero and its `sfOwnerCount` is zero. It then probes the ledger for objects that should have been cleaned up: the owner directory, signer lists, check entries, deposit pre-authorizations, and the full range of NFT pages (using `view.succ()` to scan between `nftpage_min` and `nftpage_max` for any intermediate pages). Pseudo-account linked objects (AMM, Vault, etc.) are also checked by reading the pseudo-account designator fields from the `before` SLE — `before` is used specifically because fields might be cleared during deletion, and the pre-deletion snapshot still has the object keys. + +This invariant uses a feature-gated enforcement pattern: `enforce = view.rules().enabled(featureInvariantsV1_1) || view.rules().enabled(featureSingleAssetVault) || ...`. Log messages and `XRPL_ASSERT` fire unconditionally; hard failures only occur when an enabling amendment is active. This allows invariant checks to be shipped and observed in logs on non-upgraded nodes before they become consensus-breaking. + +## Pseudo-Account Invariants + +**`ValidNewAccountRoot`** ensures that new accounts are created only by privileged transactions (those with `createAcct | createPseudoAcct`), that at most one account is created per transaction, and that the starting sequence is set correctly. For regular accounts, the sequence must equal the current ledger sequence. For pseudo-accounts (AMM, Vault, etc.), the sequence must be 0, and the flags must be exactly `lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth` — encoding the security constraints that prevent pseudo-accounts from acting as regular signers or initiating payments. + +**`ValidPseudoAccounts`** extends this for all modifications to pseudo-accounts during their lifetime. It verifies that exactly one pseudo-account designator field is present (`getPseudoAccountFields()` returns this list from the SField metadata), that the sequence never changes, that the required flags remain set, and that no `sfRegularKey` is ever assigned. Errors are accumulated in a `std::vector` and all are emitted to the journal before returning. + +## `NoModifiedUnmodifiableFields` + +This invariant enforces that structurally immutable fields are never altered on existing objects. For all ledger entry types, `sfLedgerEntryType` and `sfLedgerIndex` are immutable. For `ltLOAN_BROKER` and `ltLOAN` objects (lending protocol), an extended set of origination-time fields — rates, fees, schedules, parties, and configuration — are also immutable after creation. The checker collects all modified (not created or deleted) SLE pairs in a `std::set` during `visitEntry`, then compares fields using a local lambda `fieldChanged` that handles both presence and value differences. + +The invariant uses `enforce = view.rules().enabled(featureLendingProtocol)` as its gate. The comment in the code notes that this gate applies even to general `sfLedgerEntryType`/`sfLedgerIndex` checks, because that's when the invariant was introduced — a deliberate coupling of a universal check to a specific feature's amendment lifecycle. + +## Assertion Pattern for Developers + +The `InvariantCheckPrivilege.h` header documents the recurring `assert(enforce)` pattern explicitly. In debug builds, violating an invariant in a code path where the protecting amendment is not yet enabled is a fatal assertion. In release builds, the same violation logs at `fatal` level but does not fail the transaction. This creates a staged deployment contract: developers see hard failures early, but production nodes on pre-amendment ledgers get observability without breaking consensus. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp.ai.json b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp.ai.json new file mode 100644 index 0000000000..97629e7bf6 --- /dev/null +++ b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp.ai.json @@ -0,0 +1,429 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ValidLoanBroker::visitEntry" + ], + "entry_point": "ValidLoanBroker::visitEntry", + "purpose": "Collects and classifies ledger entries (SLEs) relevant to Loan Brokers, Accounts, Trust Lines, and MPTokens for later invariant validation.", + "validation_points": [ + "if (after->getType() == ltLOAN_BROKER)", + "else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID))", + "else if (after->getType() == ltRIPPLE_STATE)", + "else if (after->getType() == ltMPTOKEN)" + ] + }, + { + "call_chain": [ + "ValidLoanBroker::finalize", + "for (auto const& line : lines_)", + "for (auto const& field : {&sfLowLimit, &sfHighLimit})", + "view.read(keylet::account(line->at(*field).getIssuer()))", + "if (account && account->isFieldPresent(sfLoanBrokerID))", + "brokers_.emplace(loanBrokerID, BrokerInfo{})" + ], + "entry_point": "ValidLoanBroker::finalize", + "purpose": "Final validation pass after all entries have been visited; ensures that all relevant accounts and brokers are tracked and that invariants are enforced.", + "validation_points": [ + "if (account && account->isFieldPresent(sfLoanBrokerID))" + ] + }, + { + "call_chain": [ + "ValidLoanBroker::goodZeroDirectory" + ], + "entry_point": "ValidLoanBroker::goodZeroDirectory", + "purpose": "Validates that a Loan Broker directory with zero OwnerCount is well-formed (no extra pages, only valid entries).", + "validation_points": [ + "if ((prev && (*prev != 0u)) || (next && (*next != 0u)))", + "if (indexes.size() > 1)", + "if (indexes.size() == 1)", + "if (!sle)", + "if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)" + ] + } + ], + "data_flows": [ + { + "field": "after->getType()", + "flow": [ + "visitEntry parameter 'after'", + "after->getType()", + "type-based branching in visitEntry", + "classification into brokers_, lines_, mpts_" + ], + "origin": "Ledger entry (SLE) passed to visitEntry", + "transformations": [ + "Type is checked to determine how to classify the entry" + ], + "validated_at": "visitEntry: if (after->getType() == ...)" + }, + { + "field": "after->isFieldPresent(sfLoanBrokerID)", + "flow": [ + "visitEntry parameter 'after'", + "after->isFieldPresent(sfLoanBrokerID)", + "if true, after->at(sfLoanBrokerID) is used as key in brokers_" + ], + "origin": "AccountRoot SLE", + "transformations": [ + "Presence check; value extracted if present" + ], + "validated_at": "visitEntry: else if (after->getType() == ltACCOUNT_ROOT && after->isFieldPresent(sfLoanBrokerID))" + }, + { + "field": "dir->at(~sfIndexNext), dir->at(~sfIndexPrevious)", + "flow": [ + "goodZeroDirectory parameter 'dir'", + "dir->at(~sfIndexNext), dir->at(~sfIndexPrevious)", + "if ((prev && (*prev != 0u)) || (next && (*next != 0u)))" + ], + "origin": "Directory SLE passed to goodZeroDirectory", + "transformations": [ + "Dereferenced and compared to 0u" + ], + "validated_at": "goodZeroDirectory: if ((prev && (*prev != 0u)) || (next && (*next != 0u)))" + }, + { + "field": "dir->getFieldV256(sfIndexes)", + "flow": [ + "goodZeroDirectory parameter 'dir'", + "dir->getFieldV256(sfIndexes)", + "indexes.size() > 1 or == 1", + "if == 1, index used to fetch SLE from ledger" + ], + "origin": "Directory SLE", + "transformations": [ + "Size checked; if size==1, dereferenced to get index" + ], + "validated_at": "goodZeroDirectory: if (indexes.size() > 1), if (indexes.size() == 1)" + }, + { + "field": "line->at(*field).getIssuer()", + "flow": [ + "finalize: for (auto const& line : lines_)", + "for (auto const& field : {&sfLowLimit, &sfHighLimit})", + "line->at(*field).getIssuer()", + "view.read(keylet::account(...))" + ], + "origin": "Trust line SLE in lines_", + "transformations": [ + "Issuer extracted from trust line field" + ], + "validated_at": "finalize: if (account && account->isFieldPresent(sfLoanBrokerID))" + }, + { + "field": "mpt->at(sfAccount)", + "flow": [ + "finalize: for (auto const& mpt : mpts_)", + "mpt->at(sfAccount)", + "view.read(keylet::account(...))" + ], + "origin": "MPTOKEN SLE in mpts_", + "transformations": [ + "Account extracted from MPTOKEN" + ], + "validated_at": "finalize: if (account && account->isFieldPresent(sfLoanBrokerID))" + } + ], + "description": "Implements the ValidLoanBroker invariant check for the XRPL ledger, ensuring the integrity and correctness of Loan Broker objects and their related directory and account states.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "after->getType()", + "empty", + "string", + "validation" + ], + "evidence": "if (after->getType() == ...) at visitEntry", + "issue_pattern": "Missing empty string validation for after->getType()", + "why_false_positive": "if (after->getType() == ...) validates after->getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "after->getType()", + "type", + "validation", + "check" + ], + "evidence": "if (after->getType() == ...) at visitEntry", + "issue_pattern": "Missing type validation for after->getType()", + "why_false_positive": "if (after->getType() == ...) validates after->getType() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "after->isFieldPresent(sfLoanBrokerID)", + "empty", + "string", + "validation" + ], + "evidence": "after->isFieldPresent(sfLoanBrokerID) at visitEntry", + "issue_pattern": "Missing empty string validation for after->isFieldPresent(sfLoanBrokerID)", + "why_false_positive": "after->isFieldPresent(sfLoanBrokerID) validates after->isFieldPresent(sfLoanBrokerID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "dir->at(~sfIndexNext), dir->at(~sfIndexPrevious)", + "empty", + "string", + "validation" + ], + "evidence": "if ((prev && (*prev != 0u)) || (next && (*next != 0u))) at goodZeroDirectory", + "issue_pattern": "Missing empty string validation for dir->at(~sfIndexNext), dir->at(~sfIndexPrevious)", + "why_false_positive": "if ((prev && (*prev != 0u)) || (next && (*next != 0u))) validates dir->at(~sfIndexNext), dir->at(~sfIndexPrevious) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "dir->getFieldV256(sfIndexes)", + "empty", + "string", + "validation" + ], + "evidence": "if (indexes.size() > 1) at goodZeroDirectory", + "issue_pattern": "Missing empty string validation for dir->getFieldV256(sfIndexes)", + "why_false_positive": "if (indexes.size() > 1) validates dir->getFieldV256(sfIndexes) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "dir->getFieldV256(sfIndexes)", + "empty", + "string", + "validation" + ], + "evidence": "if (indexes.size() == 1) at goodZeroDirectory", + "issue_pattern": "Missing empty string validation for dir->getFieldV256(sfIndexes)", + "why_false_positive": "if (indexes.size() == 1) validates dir->getFieldV256(sfIndexes) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle->getType()", + "empty", + "string", + "validation" + ], + "evidence": "if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) at goodZeroDirectory", + "issue_pattern": "Missing empty string validation for sle->getType()", + "why_false_positive": "if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) validates sle->getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sle->getType()", + "type", + "validation", + "check" + ], + "evidence": "if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) at goodZeroDirectory", + "issue_pattern": "Missing type validation for sle->getType()", + "why_false_positive": "if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN) validates sle->getType() type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp", + "functions": [ + { + "args": [ + "isDelete", + "before", + "after" + ], + "lineno": 13, + "name": "ValidLoanBroker::visitEntry" + }, + { + "args": [ + "view", + "dir", + "j" + ], + "lineno": 36, + "name": "ValidLoanBroker::goodZeroDirectory" + }, + { + "args": [ + "tx", + "TER", + "XRPAmount", + "view", + "j" + ], + "lineno": 62, + "name": "ValidLoanBroker::finalize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is likely tested via invariant enforcement tests in the rippled codebase, particularly those that exercise ledger invariants during transaction application. Look for tests in files such as 'Invariant_test.cpp', 'LoanBrokerInvariant_test.cpp', or integration tests that enable the Lending Protocol amendment and perform operations involving Loan Brokers, Trust Lines, and MPTokens. Gaps may exist if there are no tests for edge cases like malformed directories, missing accounts, or unexpected SLE types in directories. Manual inspection of the test suite is needed to confirm coverage of all validation branches, especially error/failure paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom logic, SLE field accessors, no external validation framework", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (branches on type, no exception)", + "field": "after->getType()", + "location": "visitEntry", + "validated_by": "if (after->getType() == ...)", + "validates": [ + "Checks if SLE is of type ltLOAN_BROKER, ltACCOUNT_ROOT, ltRIPPLE_STATE, or ltMPTOKEN before processing" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (branches on presence, no exception)", + "field": "after->isFieldPresent(sfLoanBrokerID)", + "location": "visitEntry", + "validated_by": "after->isFieldPresent(sfLoanBrokerID)", + "validates": [ + "Checks if sfLoanBrokerID field is present in ACCOUNT_ROOT" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "JLOG(j.fatal()), returns false", + "field": "dir->at(~sfIndexNext), dir->at(~sfIndexPrevious)", + "location": "goodZeroDirectory", + "validated_by": "if ((prev && (*prev != 0u)) || (next && (*next != 0u)))", + "validates": [ + "Ensures Loan Broker with zero OwnerCount does not have multiple directory pages" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "JLOG(j.fatal()), returns false", + "field": "dir->getFieldV256(sfIndexes)", + "location": "goodZeroDirectory", + "validated_by": "if (indexes.size() > 1)", + "validates": [ + "Ensures Loan Broker with zero OwnerCount does not have multiple indexes in the Directory root" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "JLOG(j.fatal()), returns false (if sle not found or type mismatch)", + "field": "dir->getFieldV256(sfIndexes)", + "location": "goodZeroDirectory", + "validated_by": "if (indexes.size() == 1)", + "validates": [ + "If there is one index, checks that the referenced SLE exists and is of type ltRIPPLE_STATE or ltMPTOKEN" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "JLOG(j.fatal()), returns false", + "field": "sle->getType()", + "location": "goodZeroDirectory", + "validated_by": "if (sle->getType() != ltRIPPLE_STATE && sle->getType() != ltMPTOKEN)", + "validates": [ + "Ensures directory entry is of expected type" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp.ai.md b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp.ai.md new file mode 100644 index 0000000000..9038bc5669 --- /dev/null +++ b/src/libxrpl/tx/invariants/LoanBrokerInvariant.cpp.ai.md @@ -0,0 +1,54 @@ +# `LoanBrokerInvariant.cpp` + +## Role in the System + +This file implements `ValidLoanBroker`, one of the per-transaction invariant checkers that run after every transaction is applied to the XRPL ledger. It belongs to the invariant check framework defined alongside `InvariantCheck.cpp` and is specific to the Lending Protocol amendment (XLS-66). Its purpose is to verify that `LoanBroker` ledger objects and their associated state remain internally consistent after each transaction commits — catching bugs in transaction processing before they corrupt live ledger state. + +The Lending Protocol introduces a `LoanBroker` as a pseudo-account-backed object that coordinates collateralized loans from a `Vault`. A broker holds cover collateral in a pseudo-account and tracks its `sfDebtTotal` (outstanding loans) and `sfCoverAvailable` (collateral available for liquidation). The invariant exists to ensure these accounting fields can never drift out of sync with on-ledger balances. + +## Two-Phase Design + +Like every invariant in the framework, `ValidLoanBroker` operates in two phases. `visitEntry()` is called once per modified ledger entry (SLE) during transaction application, accumulating relevant objects into member collections. After all entries are visited, `finalize()` performs the actual validation across the accumulated state. This split is necessary because a single transaction can touch multiple objects that together reveal a constraint violation: no single SLE change is inherently wrong in isolation. + +## Entry Collection (`visitEntry`) + +The collector is deliberately broad. It classifies each post-transaction `after` SLE into one of four buckets: + +- **`ltLOAN_BROKER`** — added to `brokers_` with both before/after snapshots stored in a `BrokerInfo` struct. +- **`ltACCOUNT_ROOT` with `sfLoanBrokerID` present** — indicates a broker pseudo-account was touched. Its `sfLoanBrokerID` field is used as a key in `brokers_`, creating a placeholder `BrokerInfo{}` entry if none exists. This ensures the broker is checked even if the `ltLOAN_BROKER` object itself was not directly modified. +- **`ltRIPPLE_STATE`** — collected into `lines_` for deferred issuer lookup. +- **`ltMPTOKEN`** — collected into `mpts_` for deferred account lookup. + +The `isDelete` flag is deliberately not used here. The post-state `after` is what matters for all invariant checks except the sequence monotonicity check, which compares `before` against `after`. + +## Indirect Broker Discovery in `finalize` + +The most architecturally interesting aspect of this invariant is its approach to incomplete visibility. A transaction may modify trust lines or MPTokens held by a broker's pseudo-account without ever directly touching the `ltLOAN_BROKER` SLE itself. To catch such cases, `finalize` iterates through all collected `lines_` and `mpts_`, reads the account root for each issuer/holder, and if that account carries `sfLoanBrokerID`, adds the broker to the tracking map. + +This lazy discovery strategy avoids the `visitEntry` phase needing to speculatively read the full ledger for every modified trust line or MPToken. The `emplace` call uses the insert-if-absent semantics of `std::map::emplace`, so brokers already discovered directly are not overwritten. + +At the end of this discovery pass, `brokers_` may contain entries where `brokerBefore` and `brokerAfter` are both null — just the ID is known. In that case, `finalize` reads the broker SLE from the current view via `keylet::loanbroker(brokerID)`. If the object is missing from the ledger entirely, the invariant fails immediately with `"Loan Broker missing"`. + +## `goodZeroDirectory` + +This private static helper enforces a property stated in the XLS-66 spec: when a broker's `sfOwnerCount` is zero, its owner directory must be a single-page root containing at most one entry, and that entry may only be an `ltRIPPLE_STATE` or `ltMPTOKEN` object. The reasoning is that a broker with no outstanding loans or other obligations should hold essentially no owned objects — the only exception being the trust line or MPToken through which the cover collateral is held. + +The check examines `sfIndexNext` and `sfIndexPrevious` on the directory root using optional-field access (`dir->at(~sfIndexNext)`). If either is nonzero the directory has multiple pages, which is disallowed. It then inspects `sfIndexes` directly: more than one entry is a failure; exactly one entry must resolve to a valid SLE of the permitted types. + +## Core Invariants Checked in `finalize` + +For each tracked broker the following conditions must hold: + +**Sequence monotonicity.** `sfLoanSequence` must never decrease. Since this sequence is used to derive unique loan keys, a decrement would allow replay of previously-issued loan IDs. + +**Non-negative accounting.** Both `sfDebtTotal` and `sfCoverAvailable` must be ≥ 0. Because these are `STNumber` fields, negative values are representable and would indicate a bookkeeping bug. + +**Vault linkage.** The broker's `sfVaultID` must reference a `Vault` object that still exists in the ledger. A dangling reference would prevent later loan operations from functioning. + +**Cover/balance relationship.** `sfCoverAvailable` tracks how much of the pseudo-account's asset balance is committed as collateral cover. It must never exceed the pseudo-account's actual on-ledger balance (checked via `accountHolds` with freeze and auth handling both set to ignore, since pseudo-accounts are exempt from those controls). If `sfCoverAvailable > pseudoBalance` the broker is claiming more coverage than it actually holds. + +**Tighter equality under `fixSecurity3_1_3`.** A subsequent security fix amendment further tightens the upper bound: `sfCoverAvailable` must equal `pseudoBalance`, not merely be ≤ it. The single exception is `ttLOAN_BROKER_DELETE`, where the broker object is being removed and `sfCoverAvailable` is deliberately not zeroed before deletion — checking the field at that point would produce a false positive. + +## Amendment Gating + +Unlike some invariants that conditionally enforce based on whether a relevant amendment is active, `ValidLoanBroker::finalize` has no amendment gate. The comment explains the rationale: `ltLOAN_BROKER` objects cannot exist in the ledger unless the Lending Protocol amendment has already been enabled. If the code reaches the broker validation loop there must be live broker state, so the amendment is implicitly confirmed by the data's presence. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/LoanInvariant.cpp.ai.json b/src/libxrpl/tx/invariants/LoanInvariant.cpp.ai.json new file mode 100644 index 0000000000..9402c616d0 --- /dev/null +++ b/src/libxrpl/tx/invariants/LoanInvariant.cpp.ai.json @@ -0,0 +1,430 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "InvariantChecker::visitEntry (framework)", + "ValidLoan::visitEntry" + ], + "entry_point": "ValidLoan::visitEntry", + "purpose": "Collects before/after SLEs for Loan objects during a transaction application.", + "validation_points": [] + }, + { + "call_chain": [ + "InvariantChecker::finalize (framework)", + "ValidLoan::finalize" + ], + "entry_point": "ValidLoan::finalize", + "purpose": "Performs all loan invariants/validations after transaction application, before commit.", + "validation_points": [ + "ValidLoan::finalize (all validation logic is here)" + ] + } + ], + "data_flows": [ + { + "field": "Loan.PaymentRemaining", + "flow": [ + "Ledger SLE (after)", + "ValidLoan::visitEntry (collected)", + "ValidLoan::finalize (used in validation if/else)" + ], + "origin": "after->at(sfPaymentRemaining) (from SLE after state)", + "transformations": [ + "Read as integer", + "Compared to 0" + ], + "validated_at": "ValidLoan::finalize" + }, + { + "field": "Loan.TotalValueOutstanding", + "flow": [ + "Ledger SLE (after)", + "ValidLoan::visitEntry", + "ValidLoan::finalize" + ], + "origin": "after->at(sfTotalValueOutstanding)", + "transformations": [ + "Read as integer", + "Compared to 0" + ], + "validated_at": "ValidLoan::finalize" + }, + { + "field": "Loan.PrincipalOutstanding", + "flow": [ + "Ledger SLE (after)", + "ValidLoan::visitEntry", + "ValidLoan::finalize" + ], + "origin": "after->at(sfPrincipalOutstanding)", + "transformations": [ + "Read as integer", + "Compared to 0" + ], + "validated_at": "ValidLoan::finalize" + }, + { + "field": "Loan.ManagementFeeOutstanding", + "flow": [ + "Ledger SLE (after)", + "ValidLoan::visitEntry", + "ValidLoan::finalize" + ], + "origin": "after->at(sfManagementFeeOutstanding)", + "transformations": [ + "Read as integer", + "Compared to 0" + ], + "validated_at": "ValidLoan::finalize" + }, + { + "field": "LoanOverpayment flag", + "flow": [ + "Ledger SLE (before/after)", + "ValidLoan::visitEntry", + "ValidLoan::finalize" + ], + "origin": "before->isFlag(lsfLoanOverpayment), after->isFlag(lsfLoanOverpayment)", + "transformations": [ + "Boolean comparison" + ], + "validated_at": "ValidLoan::finalize" + }, + { + "field": "LoanServiceFee, LatePaymentFee, ClosePaymentFee, PrincipalOutstanding, TotalValueOutstanding, ManagementFeeOutstanding", + "flow": [ + "Ledger SLE (after)", + "ValidLoan::visitEntry", + "ValidLoan::finalize (for loop over fields)" + ], + "origin": "after->at(field)", + "transformations": [ + "Read as integer", + "Compared to 0 (must not be negative)" + ], + "validated_at": "ValidLoan::finalize" + }, + { + "field": "PeriodicPayment", + "flow": [ + "Ledger SLE (after)", + "ValidLoan::visitEntry", + "ValidLoan::finalize (for loop)" + ], + "origin": "after->at(sfPeriodicPayment)", + "transformations": [ + "Read as integer", + "Compared to 0 (must be positive)" + ], + "validated_at": "ValidLoan::finalize" + } + ], + "description": "Implements the ValidLoan invariant check for XRPL loans, ensuring loan ledger entries remain consistent and valid according to protocol rules.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Loan.PaymentRemaining, Loan.TotalValueOutstanding, Loan.PrincipalOutstanding, Loan.ManagementFeeOutstanding", + "empty", + "string", + "validation" + ], + "evidence": "manual logic (if/else) at ValidLoan::finalize", + "issue_pattern": "Missing empty string validation for Loan.PaymentRemaining, Loan.TotalValueOutstanding, Loan.PrincipalOutstanding, Loan.ManagementFeeOutstanding", + "why_false_positive": "manual logic (if/else) validates Loan.PaymentRemaining, Loan.TotalValueOutstanding, Loan.PrincipalOutstanding, Loan.ManagementFeeOutstanding for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Loan.PaymentRemaining, Loan.TotalValueOutstanding, Loan.PrincipalOutstanding, Loan.ManagementFeeOutstanding", + "empty", + "string", + "validation" + ], + "evidence": "manual logic (if/else) at ValidLoan::finalize", + "issue_pattern": "Missing empty string validation for Loan.PaymentRemaining, Loan.TotalValueOutstanding, Loan.PrincipalOutstanding, Loan.ManagementFeeOutstanding", + "why_false_positive": "manual logic (if/else) validates Loan.PaymentRemaining, Loan.TotalValueOutstanding, Loan.PrincipalOutstanding, Loan.ManagementFeeOutstanding for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "LoanOverpayment flag", + "empty", + "string", + "validation" + ], + "evidence": "manual logic (if/else) at ValidLoan::finalize", + "issue_pattern": "Missing empty string validation for LoanOverpayment flag", + "why_false_positive": "manual logic (if/else) validates LoanOverpayment flag for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "LoanServiceFee, LatePaymentFee, ClosePaymentFee, PrincipalOutstanding, TotalValueOutstanding, ManagementFeeOutstanding", + "empty", + "string", + "validation" + ], + "evidence": "manual logic (for loop + if) at ValidLoan::finalize", + "issue_pattern": "Missing empty string validation for LoanServiceFee, LatePaymentFee, ClosePaymentFee, PrincipalOutstanding, TotalValueOutstanding, ManagementFeeOutstanding", + "why_false_positive": "manual logic (for loop + if) validates LoanServiceFee, LatePaymentFee, ClosePaymentFee, PrincipalOutstanding, TotalValueOutstanding, ManagementFeeOutstanding for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "LoanServiceFee, LatePaymentFee, ClosePaymentFee, PrincipalOutstanding, TotalValueOutstanding, ManagementFeeOutstanding", + "range", + "bounds", + "validation" + ], + "evidence": "manual logic (for loop + if) at ValidLoan::finalize", + "issue_pattern": "Missing range validation for LoanServiceFee, LatePaymentFee, ClosePaymentFee, PrincipalOutstanding, TotalValueOutstanding, ManagementFeeOutstanding", + "why_false_positive": "manual logic (for loop + if) validates LoanServiceFee, LatePaymentFee, ClosePaymentFee, PrincipalOutstanding, TotalValueOutstanding, ManagementFeeOutstanding range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "PeriodicPayment", + "empty", + "string", + "validation" + ], + "evidence": "manual logic (for loop + if) at ValidLoan::finalize", + "issue_pattern": "Missing empty string validation for PeriodicPayment", + "why_false_positive": "manual logic (for loop + if) validates PeriodicPayment for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "PeriodicPayment", + "range", + "bounds", + "validation" + ], + "evidence": "manual logic (for loop + if) at ValidLoan::finalize", + "issue_pattern": "Missing range validation for PeriodicPayment", + "why_false_positive": "manual logic (for loop + if) validates PeriodicPayment range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Loan SLE type", + "empty", + "string", + "validation" + ], + "evidence": "type check (after->getType() == ltLOAN) at ValidLoan::visitEntry", + "issue_pattern": "Missing empty string validation for Loan SLE type", + "why_false_positive": "type check (after->getType() == ltLOAN) validates Loan SLE type for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "Loan SLE type", + "type", + "validation", + "check" + ], + "evidence": "type check (after->getType() == ltLOAN) at ValidLoan::visitEntry", + "issue_pattern": "Missing type validation for Loan SLE type", + "why_false_positive": "type check (after->getType() == ltLOAN) validates Loan SLE type type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/LoanInvariant.cpp", + "functions": [ + { + "args": [ + "isDelete", + "before", + "after" + ], + "lineno": 8, + "name": "ValidLoan::visitEntry" + }, + { + "args": [ + "tx", + "TER", + "XRPAmount", + "view", + "j" + ], + "lineno": 17, + "name": "ValidLoan::finalize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "Loan invariants are typically tested in integration or unit tests for the Lending Protocol amendment. Look for test files in the codebase such as 'test/tx/Loan_test.cpp', 'test/invariants/LoanInvariant_test.cpp', or similar. Tests should cover: (1) loans with zero PaymentRemaining but nonzero outstanding values, (2) loans with nonzero PaymentRemaining but zero outstanding values, (3) changes to LoanOverpayment flag, (4) negative values for any of the fee/outstanding fields, (5) zero or negative PeriodicPayment. Gaps may exist if there are no explicit tests for each invariant failure, or if edge cases (e.g., missing fields, type errors) are not covered. If the Lending Protocol is new, test coverage may be incomplete or only present in feature branches.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation (no external validation framework)", + "validation_layer": "business_logic (invariant enforcement after transaction application)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false, logs fatal error", + "field": "Loan.PaymentRemaining, Loan.TotalValueOutstanding, Loan.PrincipalOutstanding, Loan.ManagementFeeOutstanding", + "location": "ValidLoan::finalize", + "validated_by": "manual logic (if/else)", + "validates": [ + "If PaymentRemaining == 0, then all outstanding values must be zero (fully paid off)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false, logs fatal error", + "field": "Loan.PaymentRemaining, Loan.TotalValueOutstanding, Loan.PrincipalOutstanding, Loan.ManagementFeeOutstanding", + "location": "ValidLoan::finalize", + "validated_by": "manual logic (if/else)", + "validates": [ + "If PaymentRemaining != 0, then none of the outstanding values can be zero (not fully paid off)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false, logs fatal error", + "field": "LoanOverpayment flag", + "location": "ValidLoan::finalize", + "validated_by": "manual logic (if/else)", + "validates": [ + "LoanOverpayment flag must not change between before and after" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false, logs fatal error", + "field": "LoanServiceFee, LatePaymentFee, ClosePaymentFee, PrincipalOutstanding, TotalValueOutstanding, ManagementFeeOutstanding", + "location": "ValidLoan::finalize", + "validated_by": "manual logic (for loop + if)", + "validates": [ + "Each field must not be negative" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "returns false, logs fatal error", + "field": "PeriodicPayment", + "location": "ValidLoan::finalize", + "validated_by": "manual logic (for loop + if)", + "validates": [ + "PeriodicPayment must be positive (greater than zero)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (only processes if type matches)", + "field": "Loan SLE type", + "location": "ValidLoan::visitEntry", + "validated_by": "type check (after->getType() == ltLOAN)", + "validates": [ + "Only processes entries of type ltLOAN" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/LoanInvariant.cpp.ai.md b/src/libxrpl/tx/invariants/LoanInvariant.cpp.ai.md new file mode 100644 index 0000000000..182a77fa72 --- /dev/null +++ b/src/libxrpl/tx/invariants/LoanInvariant.cpp.ai.md @@ -0,0 +1,31 @@ +# `LoanInvariant.cpp` — ValidLoan Invariant Checker + +## Role in the System + +This file implements `ValidLoan`, one of several modular invariant-checker classes that plug into the XRPL `InvariantCheck` framework. Its purpose is to enforce a set of post-transaction consistency rules on `ltLOAN` ledger objects introduced by the XLS-66d Lending Protocol amendment. Invariant checkers serve as a last line of defense: they run after every transaction application — including failed ones — and can veto the commit entirely if ledger state has become incoherent. A `false` return from `finalize` causes the transaction to be rolled back and logged at `fatal` level, protecting the ledger from bugs or exploits that slip through higher-level validation. + +## Two-Phase Visitor Pattern + +Every invariant checker in the framework exposes exactly two methods. `visitEntry()` is called once per mutated SLE during transaction processing; `finalize()` is called after all entries have been visited to render the overall verdict. `ValidLoan` follows this contract precisely. + +`visitEntry()` filters on `after->getType() == ltLOAN` and only then appends the `(before, after)` pair to `loans_`. The `before` snapshot may be null for newly created objects; `after` is always present because deleted entries are not of interest here — a deleted loan is simply absent from `loans_` and therefore skipped. Collecting pairs rather than performing checks inline is intentional: some invariants require the complete picture of a transaction's effects (e.g., cross-SLE relationships), though `ValidLoan` in its current form checks each loan independently. + +## Invariants Enforced in `finalize()` + +All four checks operate on the `after` SLE — the state that would be committed — using the `before` SLE only where a change comparison is needed. The checks are: + +**Payment completion consistency (bidirectional).** If `sfPaymentRemaining == 0`, then `sfTotalValueOutstanding`, `sfPrincipalOutstanding`, and `sfManagementFeeOutstanding` must all be zero: a loan with no scheduled payments left must be fully settled. The reciprocal is equally enforced: if `sfPaymentRemaining != 0`, then all three outstanding fields must be non-zero — a loan cannot simultaneously have active payment obligations and a zeroed-out balance. These two checks are logically complementary and prevent any split state where the payment schedule and the outstanding balances diverge. The reference to the XLS-66d invariant specification (§3.2.2.3) is embedded in a comment, making it traceable to the protocol standard. + +**Overpayment flag immutability.** The `lsfLoanOverpayment` flag is checked via `before->isFlag(...)` vs `after->isFlag(...)`, but only when `before` is non-null (i.e., for modifications, not creations). This prevents any transaction from toggling the overpayment flag mid-lifecycle; the flag is evidently meant to be set once and preserved. + +**Non-negativity of fee and balance fields.** A range loop iterates over six `STNumber` fields — `sfLoanServiceFee`, `sfLatePaymentFee`, `sfClosePaymentFee`, `sfPrincipalOutstanding`, `sfTotalValueOutstanding`, `sfManagementFeeOutstanding` — verifying each is ≥ 0. Using `STNumber` (rather than `STAmount`) allows precise numeric comparisons against `beast::zero` and integer 0, which is appropriate for loan accounting fields that represent fixed-point decimals rather than XRP or IOU amounts. + +**Strict positivity of `sfPeriodicPayment`.** A second range loop — structured identically but checking `<= 0` — enforces that the periodic payment amount is always strictly positive. A zero or negative payment amount would be economically nonsensical and could trigger divide-by-zero or underflow in payment schedule logic elsewhere. + +## Relationship to the Broader Invariant Framework + +`ValidLoan` is declared in `LoanInvariant.h` and aggregated into the master `InvariantCheck.h` alongside peers like `ValidLoanBroker`, `ValidVault`, and `AMMCheck`. The framework drives all checkers via a `std::tuple` visitor — every checker's `visitEntry` and `finalize` are called in sequence for each transaction, with a single logical `AND` over all results. + +The companion `LoanBrokerInvariant.cpp` handles the `ltLOAN_BROKER` type and is substantially more complex: it traces directory page structure, validates that a broker's `sfCoverAvailable` is consistent with its pseudo-account's actual asset balance, checks vault references, and gated some checks behind the `fixSecurity3_1_3` amendment. `ValidLoan` by contrast is intentionally narrow — it validates each loan's internal numeric consistency without reference to the broker or vault objects that own it. This separation keeps each checker's scope well-bounded and independently testable. + +The comment in `finalize()` — "Loans will not exist on ledger if the Lending Protocol amendment is not enabled, so there's no need to check it" — explains why there is no `view.rules().enabled(featureLendingProtocol)` guard. Because `ltLOAN` objects can only be created through amendment-gated transactions, the `loans_` vector will simply be empty on pre-amendment ledgers, making the entire loop a no-op. This is the cleanest possible amendment gating: the invariant itself is amendment-agnostic. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp.ai.json b/src/libxrpl/tx/invariants/MPTInvariant.cpp.ai.json new file mode 100644 index 0000000000..2b42df1b3f --- /dev/null +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp.ai.json @@ -0,0 +1,569 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ApplyContext::checkInvariants", + "InvariantChecker::visitEntry", + "ValidMPTIssuance::visitEntry" + ], + "entry_point": "ValidMPTIssuance::visitEntry", + "purpose": "Called for each ledger entry modified by a transaction to track MPT issuance/token creation/deletion and validate invariants.", + "validation_points": [ + "ValidMPTIssuance::visitEntry: after->getType() == ltMPTOKEN_ISSUANCE", + "ValidMPTIssuance::visitEntry: after->getType() == ltMPTOKEN", + "ValidMPTIssuance::visitEntry: after->at(sfMPTokenIssuanceID) (field presence)", + "ValidMPTIssuance::visitEntry: after->at(sfAccount) (field presence)", + "ValidMPTIssuance::visitEntry: mptIssue.getIssuer() == after->at(sfAccount)" + ] + }, + { + "call_chain": [ + "ApplyContext::checkInvariants", + "InvariantChecker::finalize", + "ValidMPTIssuance::finalize" + ], + "entry_point": "ValidMPTIssuance::finalize", + "purpose": "Called after all ledger modifications for a transaction to enforce that only valid MPT issuance/token changes occurred.", + "validation_points": [ + "ValidMPTIssuance::finalize: mptCreatedByIssuer_ (business logic check)", + "ValidMPTIssuance::finalize: mptIssuancesCreated_ == 1 && mptIssuancesDeleted_ == 0 (for createMPTIssuance)", + "ValidMPTIssuance::finalize: mptIssuancesCreated_ == 0 && mptIssuancesDeleted_ == 1 (for destroyMPTIssuance)" + ] + } + ], + "data_flows": [ + { + "field": "after->getType()", + "flow": [ + "SLE::getType()", + "ValidMPTIssuance::visitEntry", + "Type checked for ltMPTOKEN_ISSUANCE or ltMPTOKEN" + ], + "origin": "Ledger entry (SLE) after modification", + "transformations": [ + "Type is compared to constants to determine what kind of ledger entry is being processed" + ], + "validated_at": "ValidMPTIssuance::visitEntry" + }, + { + "field": "after->at(sfMPTokenIssuanceID)", + "flow": [ + "SLE::at(sfMPTokenIssuanceID)", + "MPTIssue constructor", + "mptIssue.getIssuer()" + ], + "origin": "Ledger entry (SLE) after modification", + "transformations": [ + "Field is extracted and used to construct an MPTIssue object" + ], + "validated_at": "ValidMPTIssuance::visitEntry (field presence and extraction)" + }, + { + "field": "after->at(sfAccount)", + "flow": [ + "SLE::at(sfAccount)", + "Compared to mptIssue.getIssuer()" + ], + "origin": "Ledger entry (SLE) after modification", + "transformations": [ + "Field is extracted and compared to issuer" + ], + "validated_at": "ValidMPTIssuance::visitEntry (field presence and business logic)" + }, + { + "field": "mptCreatedByIssuer_", + "flow": [ + "Set in visitEntry", + "Checked in finalize" + ], + "origin": "Set in ValidMPTIssuance::visitEntry if mptIssue.getIssuer() == after->at(sfAccount)", + "transformations": [ + "Boolean flag set based on business logic" + ], + "validated_at": "ValidMPTIssuance::finalize" + }, + { + "field": "mptIssuancesCreated_, mptIssuancesDeleted_, mptokensCreated_, mptokensDeleted_", + "flow": [ + "Incremented in visitEntry", + "Checked in finalize" + ], + "origin": "Counters incremented in visitEntry based on ledger entry type and operation", + "transformations": [ + "Simple integer counters" + ], + "validated_at": "ValidMPTIssuance::finalize" + } + ], + "description": "Implements invariant checks for Multi-Purpose Token (MPT) issuance and payment transactions in the XRPL ledger, ensuring correct creation, deletion, and balance updates of MPT-related ledger entries.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Ledger entry type (via getType())", + "validation", + "missing", + "check" + ], + "evidence": "Field Ledger entry type (via getType()) validated by Custom business logic, XRPL assertion macros (XRPL_ASSERT_PARTS), field accessors (at()), type checks", + "issue_pattern": "Missing validation for Ledger entry type (via getType())", + "why_false_positive": "Custom business logic, XRPL assertion macros (XRPL_ASSERT_PARTS), field accessors (at()), type checks validates Ledger entry type (via getType()) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Presence of sfMPTokenIssuanceID and sfAccount (via at())", + "validation", + "missing", + "check" + ], + "evidence": "Field Presence of sfMPTokenIssuanceID and sfAccount (via at()) validated by Custom business logic, XRPL assertion macros (XRPL_ASSERT_PARTS), field accessors (at()), type checks", + "issue_pattern": "Missing validation for Presence of sfMPTokenIssuanceID and sfAccount (via at())", + "why_false_positive": "Custom business logic, XRPL assertion macros (XRPL_ASSERT_PARTS), field accessors (at()), type checks validates Presence of sfMPTokenIssuanceID and sfAccount (via at()) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Transaction result code (via isTesSuccess, tecINCOMPLETE)", + "validation", + "missing", + "check" + ], + "evidence": "Field Transaction result code (via isTesSuccess, tecINCOMPLETE) validated by Custom business logic, XRPL assertion macros (XRPL_ASSERT_PARTS), field accessors (at()), type checks", + "issue_pattern": "Missing validation for Transaction result code (via isTesSuccess, tecINCOMPLETE)", + "why_false_positive": "Custom business logic, XRPL assertion macros (XRPL_ASSERT_PARTS), field accessors (at()), type checks validates Transaction result code (via isTesSuccess, tecINCOMPLETE) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "after->getType()", + "empty", + "string", + "validation" + ], + "evidence": "explicit type check (== ltMPTOKEN_ISSUANCE) at visitEntry", + "issue_pattern": "Missing empty string validation for after->getType()", + "why_false_positive": "explicit type check (== ltMPTOKEN_ISSUANCE) validates after->getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "after->getType()", + "type", + "validation", + "check" + ], + "evidence": "explicit type check (== ltMPTOKEN_ISSUANCE) at visitEntry", + "issue_pattern": "Missing type validation for after->getType()", + "why_false_positive": "explicit type check (== ltMPTOKEN_ISSUANCE) validates after->getType() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "after->getType()", + "empty", + "string", + "validation" + ], + "evidence": "explicit type check (== ltMPTOKEN) at visitEntry", + "issue_pattern": "Missing empty string validation for after->getType()", + "why_false_positive": "explicit type check (== ltMPTOKEN) validates after->getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "after->getType()", + "type", + "validation", + "check" + ], + "evidence": "explicit type check (== ltMPTOKEN) at visitEntry", + "issue_pattern": "Missing type validation for after->getType()", + "why_false_positive": "explicit type check (== ltMPTOKEN) validates after->getType() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "after->at(sfMPTokenIssuanceID)", + "empty", + "string", + "validation" + ], + "evidence": "field presence and extraction (MPTIssue constructor) at visitEntry", + "issue_pattern": "Missing empty string validation for after->at(sfMPTokenIssuanceID)", + "why_false_positive": "field presence and extraction (MPTIssue constructor) validates after->at(sfMPTokenIssuanceID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "after->at(sfAccount)", + "empty", + "string", + "validation" + ], + "evidence": "field presence and extraction at visitEntry", + "issue_pattern": "Missing empty string validation for after->at(sfAccount)", + "why_false_positive": "field presence and extraction validates after->at(sfAccount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mptCreatedByIssuer_", + "empty", + "string", + "validation" + ], + "evidence": "business logic (comparison: mptIssue.getIssuer() == after->at(sfAccount)) at visitEntry", + "issue_pattern": "Missing empty string validation for mptCreatedByIssuer_", + "why_false_positive": "business logic (comparison: mptIssue.getIssuer() == after->at(sfAccount)) validates mptCreatedByIssuer_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mptCreatedByIssuer_", + "empty", + "string", + "validation" + ], + "evidence": "business logic (flag check and assertion) at finalize", + "issue_pattern": "Missing empty string validation for mptCreatedByIssuer_", + "why_false_positive": "business logic (flag check and assertion) validates mptCreatedByIssuer_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mptIssuancesCreated_", + "empty", + "string", + "validation" + ], + "evidence": "business logic (counter check) at finalize", + "issue_pattern": "Missing empty string validation for mptIssuancesCreated_", + "why_false_positive": "business logic (counter check) validates mptIssuancesCreated_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mptIssuancesDeleted_", + "empty", + "string", + "validation" + ], + "evidence": "business logic (counter check) at finalize", + "issue_pattern": "Missing empty string validation for mptIssuancesDeleted_", + "why_false_positive": "business logic (counter check) validates mptIssuancesDeleted_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mptIssuancesCreated_", + "empty", + "string", + "validation" + ], + "evidence": "business logic (counter check) at finalize", + "issue_pattern": "Missing empty string validation for mptIssuancesCreated_", + "why_false_positive": "business logic (counter check) validates mptIssuancesCreated_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result", + "empty", + "string", + "validation" + ], + "evidence": "isTesSuccess(result) or (mptV2Enabled && result == tecINCOMPLETE) at finalize", + "issue_pattern": "Missing empty string validation for result", + "why_false_positive": "isTesSuccess(result) or (mptV2Enabled && result == tecINCOMPLETE) validates result for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/MPTInvariant.cpp", + "functions": [ + { + "args": [ + "isDelete", + "before", + "after" + ], + "lineno": 10, + "name": "ValidMPTIssuance::visitEntry" + }, + { + "args": [ + "tx", + "result", + "_fee", + "view", + "j" + ], + "lineno": 44, + "name": "ValidMPTIssuance::finalize" + }, + { + "args": [ + "", + "before", + "after" + ], + "lineno": 232, + "name": "ValidMPTPayment::visitEntry" + }, + { + "args": [ + "tx", + "result", + "", + "view", + "j" + ], + "lineno": 292, + "name": "ValidMPTPayment::finalize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this code is likely found in integration and invariant test suites, such as 'invariant_test.cpp', 'MPTInvariant_test.cpp', or broader transaction/ledger tests in the rippled codebase. These would test that invalid MPT issuance/token creation/deletion is caught and that valid operations succeed. However, edge cases (e.g., multiple issuances in one tx, issuer/account mismatches, privilege checks) may not be exhaustively tested. There may be gaps in testing for all combinations of feature flags (featureMPTokensV2, featureSingleAssetVault, featureLendingProtocol) and for all possible transaction types that could interact with these invariants.", + "validation_architecture": { + "auto_validated_fields": [ + "Ledger entry type (via getType())", + "Presence of sfMPTokenIssuanceID and sfAccount (via at())", + "Transaction result code (via isTesSuccess, tecINCOMPLETE)" + ], + "framework": "Custom business logic, XRPL assertion macros (XRPL_ASSERT_PARTS), field accessors (at()), type checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (increments counters only)", + "field": "after->getType()", + "location": "visitEntry", + "validated_by": "explicit type check (== ltMPTOKEN_ISSUANCE)", + "validates": [ + "Checks if the ledger entry is of type MPTOKEN_ISSUANCE before counting creation/deletion" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (increments counters only)", + "field": "after->getType()", + "location": "visitEntry", + "validated_by": "explicit type check (== ltMPTOKEN)", + "validates": [ + "Checks if the ledger entry is of type MPTOKEN before counting creation/deletion" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "exception if field missing (likely assertion or throw in at())", + "field": "after->at(sfMPTokenIssuanceID)", + "location": "visitEntry", + "validated_by": "field presence and extraction (MPTIssue constructor)", + "validates": [ + "Ensures MPToken has an associated IssuanceID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "exception if field missing (likely assertion or throw in at())", + "field": "after->at(sfAccount)", + "location": "visitEntry", + "validated_by": "field presence and extraction", + "validates": [ + "Ensures MPToken has an associated Account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets flag)", + "field": "mptCreatedByIssuer_", + "location": "visitEntry", + "validated_by": "business logic (comparison: mptIssue.getIssuer() == after->at(sfAccount))", + "validates": [ + "Checks if the MPToken is created by its own issuer (which is forbidden)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT_PARTS (assertion failure if enforceCreatedByIssuer is true)", + "field": "mptCreatedByIssuer_", + "location": "finalize", + "validated_by": "business logic (flag check and assertion)", + "validates": [ + "Ensures no MPToken is created for its own issuer if certain features are enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (logs fatal error, returns false)", + "field": "mptIssuancesCreated_", + "location": "finalize", + "validated_by": "business logic (counter check)", + "validates": [ + "Ensures at least one MPT issuance is created if transaction has createMPTIssuance privilege" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (logs fatal error, returns false)", + "field": "mptIssuancesDeleted_", + "location": "finalize", + "validated_by": "business logic (counter check)", + "validates": [ + "Ensures no MPT issuance is deleted if transaction has createMPTIssuance privilege" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (logs fatal error, returns false)", + "field": "mptIssuancesCreated_", + "location": "finalize", + "validated_by": "business logic (counter check)", + "validates": [ + "Ensures only one MPT issuance is created per transaction with createMPTIssuance privilege" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (controls whether further validation is performed)", + "field": "result", + "location": "finalize", + "validated_by": "isTesSuccess(result) or (mptV2Enabled && result == tecINCOMPLETE)", + "validates": [ + "Ensures validations only run on successful or specific incomplete results" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/MPTInvariant.cpp.ai.md b/src/libxrpl/tx/invariants/MPTInvariant.cpp.ai.md new file mode 100644 index 0000000000..c950e55381 --- /dev/null +++ b/src/libxrpl/tx/invariants/MPTInvariant.cpp.ai.md @@ -0,0 +1,49 @@ +# `MPTInvariant.cpp` — MPT Issuance and Payment Invariant Checks + +This file implements two invariant checker classes — `ValidMPTIssuance` and `ValidMPTPayment` — that form part of the XRPL ledger's post-apply safety net for Multi-Purpose Tokens (MPTs). Every successful transaction passes through the invariant checking framework before its changes are committed; if any checker's `finalize()` returns `false`, the transaction is rolled back regardless of how it completed. These classes enforce that MPT-related ledger state can only change in ways the protocol explicitly authorizes. + +## The `visitEntry` / `finalize` Interface + +Both classes conform to the standard invariant checker contract: `visitEntry()` is called once per modified ledger entry (`SLE`) with `before` and `after` snapshots, accumulating summary state in member variables. `finalize()` is then called once after all entries have been visited, receiving the completed transaction, its result code, the fee charged, and the read-only post-apply view. This two-phase structure exists because no single ledger entry carries enough context to validate the operation in isolation; consistency can only be judged globally once all changes are known. + +## `ValidMPTIssuance`: Structural Constraints on Issuances and Tokens + +`ValidMPTIssuance` tracks four unsigned integer counters (`mptIssuancesCreated_`, `mptIssuancesDeleted_`, `mptokensCreated_`, `mptokensDeleted_`) and one boolean flag (`mptCreatedByIssuer_`). In `visitEntry()`, it counts `ltMPTOKEN_ISSUANCE` and `ltMPTOKEN` SLE changes, and additionally sets `mptCreatedByIssuer_` when a newly-created `ltMPTOKEN` entry's `sfMPTokenIssuanceID` resolves to the same account as the token holder — something that should never happen because an issuer does not hold their own tokens. + +`finalize()` dispatches on the transaction's *privilege mask* rather than its type. The `hasPrivilege()` helper (defined in `InvariantCheck.cpp` via an X-macro over `transactions.macro`) maps each `ttXXX` transaction type to a bitmask of `Privilege` enum values. This indirection is deliberate: it decouples invariant logic from the explosive enumeration of transaction types. Adding a new transaction type that creates MPT issuances only requires setting the `createMPTIssuance` privilege flag in the macro table; the invariant logic doesn't change. + +The privilege dispatch follows this hierarchy: + +- **`createMPTIssuance`**: Exactly one `ltMPTOKEN_ISSUANCE` must be created, none deleted. +- **`destroyMPTIssuance`**: Exactly one deleted, none created. +- **`mustAuthorizeMPT | mayAuthorizeMPT`**: No issuance changes allowed. Token changes are tightly constrained: when a holder submits (no `sfHolder` field in tx), exactly one MPToken must be created or deleted; when the issuer submits, none. AMM-specific transactions (`ttAMM_WITHDRAW`, `ttAMM_CLAWBACK`) are carved out to allow at most one MPToken creation and at most two deletions — reflecting that AMM pool dissolution can remove both token sides simultaneously. +- **`mayCreateMPT`**: No issuances and no deletions; creation is allowed for non-issuers. `ttAMM_CREATE` may create up to two MPTokens (one per asset side), and `ttCHECK_CASH` up to one for the receiver. This is the path that covers payment and offer-crossing flows where MPTokens are auto-created for recipients who don't yet hold the token. +- **`mayDeleteMPT`**: Only deletions, no creations; at most two for `ttAMM_DELETE`. + +If none of these privilege branches match and the transaction result is successful (or `tecINCOMPLETE` with `featureMPTokensV2` enabled), any non-zero counter constitutes a violation and `finalize()` returns `false`. + +### The Issuer-Token Detection and Graduated Enforcement + +The `mptCreatedByIssuer_` flag catches a subtle invariant: an MPToken for the issuer's own issuance must never be created, because the issuer account implicitly "holds" unlimited amounts. The handling here uses the `assert(enforce)` pattern documented in `InvariantCheckPrivilege.h`: when the invariant fires, it logs a fatal message unconditionally, then checks whether the amendment `featureSingleAssetVault` or `featureLendingProtocol` is active. If so, `enforceCreatedByIssuer` is `true`, the `XRPL_ASSERT_PARTS` fires in debug builds, and `finalize()` returns `false`. If neither amendment is active, the assert still fires in debug/test builds (catching developer mistakes early), but the function continues rather than rolling back — a deliberate backward-compatibility concession for the older amendment environment. + +## `ValidMPTPayment`: Accounting Conservation + +`ValidMPTPayment` enforces the conservation invariant on `OutstandingAmount` for every MPT touched by a transaction: + +``` +OutstandingAmount_after == OutstandingAmount_before + Σ(MPTAmount_after − MPTAmount_before) +``` + +The `MPTData` struct, keyed on `MPTID` in a `hash_map`, tracks the before/after `sfOutstandingAmount` from `ltMPTOKEN_ISSUANCE` entries and accumulates a signed net delta (`mptAmount`) from all `ltMPTOKEN` entries. Each MPToken's contribution is `sfMPTAmount + sfLockedAmount`, since locked amounts remain part of the outstanding supply. + +Because `sfMPTAmount` is a 64-bit quantity and `maxMPTokenAmount` is `0x7FFF'FFFF'FFFF'FFFF` (the maximum representable signed 63-bit value), overflow is a real concern. `visitEntry()` guards against it in two ways: it checks each individual token value against `maxMPTokenAmount`, and it checks whether the token's combined amount (`mptAmt + lockedAmt`) would overflow. If either condition fires, `overflow_` is set and all further processing is skipped for that transaction. + +`finalize()` then performs a second-level overflow check on the signed delta arithmetic itself before comparing `outstanding[After]` to `outstanding[Before] + mptAmount`. This double-check is necessary because the accumulated delta is a signed 64-bit integer and can itself wrap. Overflow failures return `!enforce` — soft-failing (returning `true`) when `featureMPTokensV2` is not yet active, hard-failing when it is. + +## Design Observations + +The file is deliberately conservative about what it checks at the `visitEntry()` phase. The per-entry visitor accumulates only primitive counts and deltas; no conclusions are drawn. This is important for correctness: ledger entries may appear in any order within a transaction's change set, and committing to a judgment mid-stream would produce false positives. + +The `tecINCOMPLETE` result code exemption in `ValidMPTIssuance::finalize()` reflects a protocol extension: with `featureMPTokensV2`, certain operations can succeed partially (returning `tecINCOMPLETE` rather than `tesSUCCESS`) while still making valid ledger changes that must pass invariant checks. + +The privilege mask system pays dividends when reading the code: the `finalize()` logic never needs a switch on transaction type inside each privilege branch, keeping the combinatorial complexity under control even as the set of MPT-capable transactions grows. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/NFTInvariant.cpp.ai.json b/src/libxrpl/tx/invariants/NFTInvariant.cpp.ai.json new file mode 100644 index 0000000000..2b26733e2a --- /dev/null +++ b/src/libxrpl/tx/invariants/NFTInvariant.cpp.ai.json @@ -0,0 +1,529 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "InvariantCheck::visitEntry (base class, called by transaction application logic)", + "ValidNFTokenPage::visitEntry" + ], + "entry_point": "ValidNFTokenPage::visitEntry", + "purpose": "Validates the integrity and ordering of NFTokenPage ledger entries during transaction application (create, modify, delete).", + "validation_points": [ + "ValidNFTokenPage::visitEntry (main validation logic in lambda 'check')" + ] + }, + { + "call_chain": [ + "InvariantCheck::finalize (base class, called after all entries visited)", + "ValidNFTokenPage::finalize" + ], + "entry_point": "ValidNFTokenPage::finalize", + "purpose": "Finalizes the invariant check, typically reporting or acting on any validation failures detected during visitEntry.", + "validation_points": [ + "ValidNFTokenPage::finalize (acts on flags set during visitEntry)" + ] + }, + { + "call_chain": [ + "InvariantCheck::visitEntry", + "NFTokenCountTracking::visitEntry" + ], + "entry_point": "NFTokenCountTracking::visitEntry", + "purpose": "Tracks and validates the count of NFTs per account across ledger changes.", + "validation_points": [ + "NFTokenCountTracking::visitEntry" + ] + }, + { + "call_chain": [ + "InvariantCheck::finalize", + "NFTokenCountTracking::finalize" + ], + "entry_point": "NFTokenCountTracking::finalize", + "purpose": "Finalizes NFT count tracking, ensuring no overflows or underflows.", + "validation_points": [ + "NFTokenCountTracking::finalize" + ] + } + ], + "data_flows": [ + { + "field": "SLE type (ltNFTOKEN_PAGE)", + "flow": [ + "SLE object", + "visitEntry (before/after)", + "getType() check" + ], + "origin": "Ledger entry (SLE) passed as 'before' or 'after' to visitEntry", + "transformations": [ + "Explicit type check: only process if type == ltNFTOKEN_PAGE" + ], + "validated_at": "ValidNFTokenPage::visitEntry (early return if not NFTOKEN_PAGE)" + }, + { + "field": "sfPreviousPageMin / sfNextPageMin (page links)", + "flow": [ + "SLE object", + "visitEntry", + "lambda 'check'", + "(*sle)[~sfPreviousPageMin] / (*sle)[~sfNextPageMin]" + ], + "origin": "Fields in SLE (NFTokenPage)", + "transformations": [ + "Bitmask applied: accountBits and pageBits extracted", + "Compared to current page's accountBits/pageBits" + ], + "validated_at": "ValidNFTokenPage::visitEntry::check (bitmask comparisons for account and ordering)" + }, + { + "field": "sfNFTokens (array of NFTs on page)", + "flow": [ + "SLE object", + "visitEntry", + "lambda 'check'", + "sle->getFieldArray(sfNFTokens)" + ], + "origin": "Field in SLE (NFTokenPage)", + "transformations": [ + "Counted for size validation", + "Iterated for ordering and page-bits validation" + ], + "validated_at": "ValidNFTokenPage::visitEntry::check (size, ordering, and page-bits checks)" + }, + { + "field": "sfNFTokenID (NFT ID in page)", + "flow": [ + "sfNFTokens array", + "for loop in check", + "obj[sfNFTokenID]" + ], + "origin": "Element in sfNFTokens array", + "transformations": [ + "Compared for ordering (nft::compareTokens)", + "Bitmasked for pageBits validation" + ], + "validated_at": "ValidNFTokenPage::visitEntry::check (ordering and page-bits checks)" + }, + { + "field": "sfURI (NFT URI)", + "flow": [ + "sfNFTokens array", + "for loop in check", + "obj[~sfURI]" + ], + "origin": "Element in sfNFTokens array", + "transformations": [ + "Checked for presence and non-empty" + ], + "validated_at": "ValidNFTokenPage::visitEntry::check (empty URI check)" + } + ], + "description": "Implements invariants for NFT-related ledger objects in the XRPL, including checks for NFTokenPage structure, linking, sorting, and NFT mint/burn count tracking.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "SLE type (ltNFTOKEN_PAGE)", + "empty", + "string", + "validation" + ], + "evidence": "explicit type check (getType()) at ValidNFTokenPage::visitEntry", + "issue_pattern": "Missing empty string validation for SLE type (ltNFTOKEN_PAGE)", + "why_false_positive": "explicit type check (getType()) validates SLE type (ltNFTOKEN_PAGE) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "SLE type (ltNFTOKEN_PAGE)", + "type", + "validation", + "check" + ], + "evidence": "explicit type check (getType()) at ValidNFTokenPage::visitEntry", + "issue_pattern": "Missing type validation for SLE type (ltNFTOKEN_PAGE)", + "why_false_positive": "explicit type check (getType()) validates SLE type (ltNFTOKEN_PAGE) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPreviousPageMin link (account bits)", + "empty", + "string", + "validation" + ], + "evidence": "bitmask comparison at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing empty string validation for sfPreviousPageMin link (account bits)", + "why_false_positive": "bitmask comparison validates sfPreviousPageMin link (account bits) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPreviousPageMin link (ordering)", + "empty", + "string", + "validation" + ], + "evidence": "bitmask comparison at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing empty string validation for sfPreviousPageMin link (ordering)", + "why_false_positive": "bitmask comparison validates sfPreviousPageMin link (ordering) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNextPageMin link (account bits)", + "empty", + "string", + "validation" + ], + "evidence": "bitmask comparison at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing empty string validation for sfNextPageMin link (account bits)", + "why_false_positive": "bitmask comparison validates sfNextPageMin link (account bits) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNextPageMin link (ordering)", + "empty", + "string", + "validation" + ], + "evidence": "bitmask comparison at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing empty string validation for sfNextPageMin link (ordering)", + "why_false_positive": "bitmask comparison validates sfNextPageMin link (ordering) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokens array size", + "empty", + "string", + "validation" + ], + "evidence": "array size check at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing empty string validation for sfNFTokens array size", + "why_false_positive": "array size check validates sfNFTokens array size for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfNFTokens array size", + "range", + "bounds", + "validation" + ], + "evidence": "array size check at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing range validation for sfNFTokens array size", + "why_false_positive": "array size check validates sfNFTokens array size range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokens array (sorted order)", + "empty", + "string", + "validation" + ], + "evidence": "nft::compareTokens at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing empty string validation for sfNFTokens array (sorted order)", + "why_false_positive": "nft::compareTokens validates sfNFTokens array (sorted order) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokens array (token page bits in range)", + "empty", + "string", + "validation" + ], + "evidence": "bitmask and range check at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing empty string validation for sfNFTokens array (token page bits in range)", + "why_false_positive": "bitmask and range check validates sfNFTokens array (token page bits in range) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfURI (inside sfNFTokens array)", + "empty", + "string", + "validation" + ], + "evidence": "optional field presence and empty check at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing empty string validation for sfURI (inside sfNFTokens array)", + "why_false_positive": "optional field presence and empty check validates sfURI (inside sfNFTokens array) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfURI (inside sfNFTokens array)", + "format", + "validation", + "invalid" + ], + "evidence": "optional field presence and empty check at ValidNFTokenPage::visitEntry::check (lambda)", + "issue_pattern": "Missing format validation for sfURI (inside sfNFTokens array)", + "why_false_positive": "optional field presence and empty check validates sfURI (inside sfNFTokens array) format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/NFTInvariant.cpp", + "functions": [ + { + "args": [ + "isDelete", + "before", + "after" + ], + "lineno": 11, + "name": "ValidNFTokenPage::visitEntry" + }, + { + "args": [ + "tx", + "result", + "XRPAmount", + "view", + "j" + ], + "lineno": 97, + "name": "ValidNFTokenPage::finalize" + }, + { + "args": [ + "", + "before", + "after" + ], + "lineno": 143, + "name": "NFTokenCountTracking::visitEntry" + }, + { + "args": [ + "tx", + "result", + "XRPAmount", + "view", + "j" + ], + "lineno": 155, + "name": "NFTokenCountTracking::finalize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The invariant checks are typically tested in the rippled codebase under unit tests for invariants and NFT logic. Look for test files such as 'test/invariant/NFTInvariant_test.cpp', 'test/app/tx/NFToken_test.cpp', or similar. These should cover cases like invalid page links, empty or oversized pages, unsorted NFT IDs, and invalid URIs. Gaps may exist in edge cases (e.g., malformed links, simultaneous multiple errors, or rare boundary conditions like deleting the final page). Integration tests may also exercise these invariants indirectly via transaction application. Direct fuzzing or property-based tests for all bitmask edge cases may be limited.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom logic (no external validation framework)", + "validation_layer": "business_logic (invariant check after transaction application)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (function returns early)", + "field": "SLE type (ltNFTOKEN_PAGE)", + "location": "ValidNFTokenPage::visitEntry", + "validated_by": "explicit type check (getType())", + "validates": [ + "Ensures only NFTOKEN_PAGE entries are processed" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets badLink_ = true)", + "field": "sfPreviousPageMin link (account bits)", + "location": "ValidNFTokenPage::visitEntry::check (lambda)", + "validated_by": "bitmask comparison", + "validates": [ + "Previous page link must match owning account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets badLink_ = true)", + "field": "sfPreviousPageMin link (ordering)", + "location": "ValidNFTokenPage::visitEntry::check (lambda)", + "validated_by": "bitmask comparison", + "validates": [ + "Previous page link must be strictly less than current page (ordering)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets badLink_ = true)", + "field": "sfNextPageMin link (account bits)", + "location": "ValidNFTokenPage::visitEntry::check (lambda)", + "validated_by": "bitmask comparison", + "validates": [ + "Next page link must match owning account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets badLink_ = true)", + "field": "sfNextPageMin link (ordering)", + "location": "ValidNFTokenPage::visitEntry::check (lambda)", + "validated_by": "bitmask comparison", + "validates": [ + "Next page link must be strictly greater than current page (ordering)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets invalidSize_ = true)", + "field": "sfNFTokens array size", + "location": "ValidNFTokenPage::visitEntry::check (lambda)", + "validated_by": "array size check", + "validates": [ + "NFTokenPage must not be empty (unless being deleted)", + "NFTokenPage must not exceed dirMaxTokensPerPage" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets badSort_ = true)", + "field": "sfNFTokens array (sorted order)", + "location": "ValidNFTokenPage::visitEntry::check (lambda)", + "validated_by": "nft::compareTokens", + "validates": [ + "NFTokenIDs in the page must be sorted" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets badEntry_ = true)", + "field": "sfNFTokens array (token page bits in range)", + "location": "ValidNFTokenPage::visitEntry::check (lambda)", + "validated_by": "bitmask and range check", + "validates": [ + "Each NFTokenID must belong to the current page (tokenPageBits in [loLimit, hiLimit))" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets badURI_ = true)", + "field": "sfURI (inside sfNFTokens array)", + "location": "ValidNFTokenPage::visitEntry::check (lambda)", + "validated_by": "optional field presence and empty check", + "validates": [ + "If sfURI is present, it must not be empty" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/NFTInvariant.cpp.ai.md b/src/libxrpl/tx/invariants/NFTInvariant.cpp.ai.md new file mode 100644 index 0000000000..07d28d163d --- /dev/null +++ b/src/libxrpl/tx/invariants/NFTInvariant.cpp.ai.md @@ -0,0 +1,56 @@ +# NFTInvariant.cpp — NFT Ledger Invariant Checkers + +`NFTInvariant.cpp` implements two post-transaction invariant checkers for the XRPL NFT subsystem: `ValidNFTokenPage` and `NFTokenCountTracking`. Both follow the ledger's two-phase invariant protocol — `visitEntry` is called for every ledger entry touched by a transaction (with `before` and `after` snapshots), then `finalize` is called once to render a verdict. If `finalize` returns `false`, the transaction is rejected with `tecINVARIANT_FAILED` and no state change is committed. + +These classes are not called by application code directly; they are instantiated and driven by the invariant-check harness inside the transaction application pipeline, which maintains a heterogeneous list of invariant checker objects and dispatches to each one. + +## NFToken Page Structure + +Understanding the checks requires understanding how NFToken pages are addressed. Each `ltNFTOKEN_PAGE` ledger entry has a 256-bit key that simultaneously encodes two things: the high 160 bits (`accountBits = ~pageMask`) identify the owning account, and the low 96 bits (`pageBits = nft::pageMask`) represent the page's upper-bound discriminator — the highest NFT ID (in page-bits terms) that may reside on this page. The mask is defined in `nftPageMask.h` as 96 consecutive 1-bits in the low word: + +``` +pageMask = 0x0000000000000000000000000000000000000000ffffffffffffffffffffffff +``` + +This design means every page's address is self-describing: from the key alone you can derive both who owns it and where it sits in the sorted NFT ID space. + +## ValidNFTokenPage + +`ValidNFTokenPage` enforces the structural integrity of `ltNFTOKEN_PAGE` entries. Its `visitEntry` method ignores any SLE that isn't an NFT page (early-return type guard), then applies a `check` lambda to both the `before` and `after` states. + +**Link integrity** is verified by inspecting `sfPreviousPageMin` and `sfNextPageMin` fields. For each link that is present, two conditions must hold: the high 160 bits of the linked key must match the current page's account bits (enforcing ownership), and the linked page's low 96 bits must be strictly less than (for previous) or strictly greater than (for next) the current page's discriminator. A violation sets `badLink_`. + +**Size constraints** are checked against `dirMaxTokensPerPage` (32 tokens). A page being actively used must contain at least one and at most 32 tokens; an empty page is only permitted when it is in the process of being deleted (`isDelete == true`). A violation sets `invalidSize_`. + +**Sorting** is enforced by iterating the `sfNFTokens` array and calling `nft::compareTokens` on each consecutive pair, starting from a lower bound derived from the previous page link. Each token's ID must be strictly greater than the last. A violation sets `badSort_`. + +**Page membership** is verified by extracting the page-bits of each token ID and confirming it falls in `[loLimit, hiLimit)`. A token with page-bits below the lower limit belongs on an earlier page; one at or above the upper limit belongs on a later one. A violation sets `badEntry_`. + +**URI validity**: if a token carries an `sfURI` field, it must be non-empty. Storing an empty URI is protocol-invalid — the field should simply be absent. A violation sets `badURI_`. + +Two additional checks appear in `visitEntry` outside the `check` lambda: + +- **Deleted final page**: if the page being deleted has all 96 page-bits set to `1` (i.e., it is the highest-addressed page in the directory) but still has a `sfPreviousPageMin` link, deleting it would orphan the rest of the directory. This sets `deletedFinalPage_`. + +- **Lost forward link**: for a non-final page transitioning from `before` to `after`, if `sfNextPageMin` was present in `before` but absent in `after`, the chain has been broken. This sets `deletedLink_`. + +Both of these last two checks are gated in `finalize` behind `view.rules().enabled(fixNFTokenPageLinks)`. The amendment gating is deliberate: it allows the checks to be added to the invariant framework while historical ledger replay (which predates the fix) remains unaffected. Before the amendment activates, link corruption is logged but not enforced; after it activates, `finalize` returns `false`. + +## NFTokenCountTracking + +`NFTokenCountTracking` guards the global NFT mint and burn tallies stored on `ltACCOUNT_ROOT` entries as `sfMintedNFTokens` and `sfBurnedNFTokens`. The checker accumulates pre- and post-transaction totals across all account roots touched, using `value_or(0)` to handle accounts that have never minted or burned any NFTs. + +The `finalize` logic branches on whether the transaction holds the `changeNFTCounts` privilege (a bitmask flag declared in `InvariantCheckPrivilege.h` and populated from a macro-driven transaction table). For transactions that lack this privilege, neither count may change — this catches any bug where a non-NFT transaction accidentally writes to these fields. + +For privileged transactions, the rules are asymmetric and strict: + +- A successful `ttNFTOKEN_MINT` must strictly increase `sfMintedNFTokens` (not merely equal — it must go up). The burned total must be unchanged. +- A failed `ttNFTOKEN_MINT` must leave both counts identical to their pre-transaction values. +- A successful `ttNFTOKEN_BURN` must strictly increase `sfBurnedNFTokens`. The minted total must be unchanged. +- A failed `ttNFTOKEN_BURN` must leave both counts identical. + +The strict inequality (`>=` rather than `!=`) for successful mint/burn is important: it catches the case where the count wraps around or the field is incorrectly written back, not just the case where nothing changed. The choice to track global totals across all touched account roots rather than per-account makes the check simpler and still sound, because an NFT operation legitimately affects exactly one account's counters. + +## Interaction with the Invariant Framework + +Both classes accumulate state in boolean flags or integer accumulators that are set to failure-indicating values during `visitEntry`. All fatal log messages are emitted from `finalize`, which sees the final `ReadView` and the transaction's `TER` result code. This two-phase structure means every page touched by the transaction is checked before any failure is reported, giving the logs a complete picture even when multiple invariants are violated in a single transaction. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp.ai.json b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp.ai.json new file mode 100644 index 0000000000..d5aab3f4c9 --- /dev/null +++ b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp.ai.json @@ -0,0 +1,499 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "InvariantChecker::visitEntry (framework)", + "ValidPermissionedDEX::visitEntry" + ], + "entry_point": "ValidPermissionedDEX::visitEntry", + "purpose": "Called for each ledger entry modified by a transaction. Collects domain IDs and flags malformed hybrid offers or regular offers.", + "validation_points": [ + "after->isFieldPresent(sfDomainID)", + "after->isFlag(lsfHybrid)", + "after->isFieldPresent(sfAdditionalBooks)", + "after->getFieldArray(sfAdditionalBooks).size() > 1" + ] + }, + { + "call_chain": [ + "InvariantChecker::finalize (framework)", + "ValidPermissionedDEX::finalize" + ], + "entry_point": "ValidPermissionedDEX::finalize", + "purpose": "Called after all entries are visited for a transaction. Validates transaction type, result, domain existence, domain consistency, and offer regularity.", + "validation_points": [ + "tx.getTxnType()", + "isTesSuccess(result)", + "badHybrids_", + "tx.isFieldPresent(sfDomainID)", + "view.exists(keylet::permissionedDomain(domain))", + "d != domain (for all domains_)", + "regularOffers_" + ] + } + ], + "data_flows": [ + { + "field": "sfDomainID (on DIR_NODE)", + "flow": [ + "after->isFieldPresent(sfDomainID)", + "after->getFieldH256(sfDomainID)", + "domains_.insert(...)", + "domains_ checked in finalize" + ], + "origin": "Ledger entry (after) of type ltDIR_NODE", + "transformations": [ + "Checked for presence", + "Extracted as H256", + "Inserted into set" + ], + "validated_at": "visitEntry (presence), finalize (consistency with tx domain)" + }, + { + "field": "sfDomainID (on OFFER)", + "flow": [ + "after->isFieldPresent(sfDomainID)", + "after->getFieldH256(sfDomainID)", + "domains_.insert(...)", + "domains_ checked in finalize" + ], + "origin": "Ledger entry (after) of type ltOFFER", + "transformations": [ + "Checked for presence", + "Extracted as H256", + "Inserted into set" + ], + "validated_at": "visitEntry (presence), finalize (consistency with tx domain)" + }, + { + "field": "lsfHybrid (flag on OFFER)", + "flow": [ + "after->isFlag(lsfHybrid)", + "if true, check sfDomainID and sfAdditionalBooks" + ], + "origin": "Ledger entry (after) of type ltOFFER", + "transformations": [ + "Boolean check" + ], + "validated_at": "visitEntry (hybrid offer validation)" + }, + { + "field": "sfAdditionalBooks (on OFFER)", + "flow": [ + "after->isFieldPresent(sfAdditionalBooks)", + "after->getFieldArray(sfAdditionalBooks).size() > 1" + ], + "origin": "Ledger entry (after) of type ltOFFER", + "transformations": [ + "Checked for presence", + "Checked array size" + ], + "validated_at": "visitEntry (hybrid offer validation)" + }, + { + "field": "Transaction Type", + "flow": [ + "tx.getTxnType()", + "checked in finalize" + ], + "origin": "STTx (tx)", + "transformations": [ + "Compared to ttPAYMENT and ttOFFER_CREATE" + ], + "validated_at": "finalize" + }, + { + "field": "Transaction Result Code", + "flow": [ + "isTesSuccess(result)" + ], + "origin": "TER result", + "transformations": [ + "Boolean check" + ], + "validated_at": "finalize" + }, + { + "field": "sfDomainID (on transaction)", + "flow": [ + "tx.isFieldPresent(sfDomainID)", + "tx.getFieldH256(sfDomainID)", + "view.exists(keylet::permissionedDomain(domain))" + ], + "origin": "STTx (tx)", + "transformations": [ + "Checked for presence", + "Extracted as H256", + "Used to look up domain existence" + ], + "validated_at": "finalize" + } + ], + "description": "Implements the ValidPermissionedDEX invariant check for XRPL transactions, ensuring that permissioned DEX offers and domains are handled correctly and that malformed or unauthorized domain interactions are detected and prevented.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID (on DIR_NODE and OFFER entries)", + "empty", + "string", + "validation" + ], + "evidence": "isFieldPresent(sfDomainID) at visitEntry", + "issue_pattern": "Missing empty string validation for sfDomainID (on DIR_NODE and OFFER entries)", + "why_false_positive": "isFieldPresent(sfDomainID) validates sfDomainID (on DIR_NODE and OFFER entries) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID (on OFFER entries)", + "empty", + "string", + "validation" + ], + "evidence": "isFieldPresent(sfDomainID) at visitEntry", + "issue_pattern": "Missing empty string validation for sfDomainID (on OFFER entries)", + "why_false_positive": "isFieldPresent(sfDomainID) validates sfDomainID (on OFFER entries) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfHybrid flag, sfDomainID, sfAdditionalBooks (on OFFER entries)", + "empty", + "string", + "validation" + ], + "evidence": "isFlag(lsfHybrid), isFieldPresent(sfDomainID), isFieldPresent(sfAdditionalBooks), getFieldArray(sfAdditionalBooks).size() > 1 at visitEntry", + "issue_pattern": "Missing empty string validation for lsfHybrid flag, sfDomainID, sfAdditionalBooks (on OFFER entries)", + "why_false_positive": "isFlag(lsfHybrid), isFieldPresent(sfDomainID), isFieldPresent(sfAdditionalBooks), getFieldArray(sfAdditionalBooks).size() > 1 validates lsfHybrid flag, sfDomainID, sfAdditionalBooks (on OFFER entries) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction type (ttPAYMENT, ttOFFER_CREATE)", + "empty", + "string", + "validation" + ], + "evidence": "tx.getTxnType() at finalize", + "issue_pattern": "Missing empty string validation for transaction type (ttPAYMENT, ttOFFER_CREATE)", + "why_false_positive": "tx.getTxnType() validates transaction type (ttPAYMENT, ttOFFER_CREATE) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction result code", + "empty", + "string", + "validation" + ], + "evidence": "isTesSuccess(result) at finalize", + "issue_pattern": "Missing empty string validation for transaction result code", + "why_false_positive": "isTesSuccess(result) validates transaction result code for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "badHybrids_ (hybrid offer validity)", + "empty", + "string", + "validation" + ], + "evidence": "badHybrids_ flag at finalize", + "issue_pattern": "Missing empty string validation for badHybrids_ (hybrid offer validity)", + "why_false_positive": "badHybrids_ flag validates badHybrids_ (hybrid offer validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID (on transaction)", + "empty", + "string", + "validation" + ], + "evidence": "tx.isFieldPresent(sfDomainID) at finalize", + "issue_pattern": "Missing empty string validation for sfDomainID (on transaction)", + "why_false_positive": "tx.isFieldPresent(sfDomainID) validates sfDomainID (on transaction) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID (existence in ledger)", + "empty", + "string", + "validation" + ], + "evidence": "view.exists(keylet::permissionedDomain(domain)) at finalize", + "issue_pattern": "Missing empty string validation for sfDomainID (existence in ledger)", + "why_false_positive": "view.exists(keylet::permissionedDomain(domain)) validates sfDomainID (existence in ledger) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "domains_ set (consumed domains)", + "empty", + "string", + "validation" + ], + "evidence": "for (auto const& d : domains_) at finalize", + "issue_pattern": "Missing empty string validation for domains_ set (consumed domains)", + "why_false_positive": "for (auto const& d : domains_) validates domains_ set (consumed domains) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "regularOffers_ flag", + "empty", + "string", + "validation" + ], + "evidence": "regularOffers_ at finalize", + "issue_pattern": "Missing empty string validation for regularOffers_ flag", + "why_false_positive": "regularOffers_ validates regularOffers_ flag for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp", + "functions": [ + { + "args": [ + "bool", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 10, + "name": "ValidPermissionedDEX::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const result", + "XRPAmount const", + "ReadView const& view", + "beast::Journal const& j" + ], + "lineno": 38, + "name": "ValidPermissionedDEX::finalize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "This invariant is likely tested in unit/integration tests for invariants, especially those covering OfferCreate and Payment transactions with/without sfDomainID, hybrid offers, and malformed offers. Look for test files such as 'Invariant_test.cpp', 'PermissionedDEXInvariant_test.cpp', or similar in the test/unit_test or test/integration directories. Gaps may exist for edge cases: multiple domains in domains_, malformed hybrid offers (missing fields or extra books), and regular offers affected by domain transactions. Ensure tests cover both success and all failure paths (badHybrids_, missing domain, domain mismatch, regularOffers_).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic (no external validation framework)", + "validation_layer": "business_logic (Invariant enforcement after transaction application)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (just tracks domains_ set)", + "field": "sfDomainID (on DIR_NODE and OFFER entries)", + "location": "visitEntry", + "validated_by": "isFieldPresent(sfDomainID)", + "validates": [ + "Checks if sfDomainID is present on DIR_NODE or OFFER entries and collects domain IDs" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets regularOffers_ = true if missing)", + "field": "sfDomainID (on OFFER entries)", + "location": "visitEntry", + "validated_by": "isFieldPresent(sfDomainID)", + "validates": [ + "Checks if OFFER entry is a regular offer (no sfDomainID)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets badHybrids_ = true if invalid)", + "field": "lsfHybrid flag, sfDomainID, sfAdditionalBooks (on OFFER entries)", + "location": "visitEntry", + "validated_by": "isFlag(lsfHybrid), isFieldPresent(sfDomainID), isFieldPresent(sfAdditionalBooks), getFieldArray(sfAdditionalBooks).size() > 1", + "validates": [ + "If lsfHybrid is set, OFFER must have sfDomainID and sfAdditionalBooks present, and sfAdditionalBooks must have at most 1 entry" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns true if not relevant type)", + "field": "transaction type (ttPAYMENT, ttOFFER_CREATE)", + "location": "finalize", + "validated_by": "tx.getTxnType()", + "validates": [ + "Only validates for ttPAYMENT and ttOFFER_CREATE transactions" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns true if not tesSUCCESS)", + "field": "transaction result code", + "location": "finalize", + "validated_by": "isTesSuccess(result)", + "validates": [ + "Only validates if transaction result is tesSUCCESS" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false, logs fatal error", + "field": "badHybrids_ (hybrid offer validity)", + "location": "finalize", + "validated_by": "badHybrids_ flag", + "validates": [ + "If any malformed hybrid offers were found, transaction is invalid" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns true if not present)", + "field": "sfDomainID (on transaction)", + "location": "finalize", + "validated_by": "tx.isFieldPresent(sfDomainID)", + "validates": [ + "If transaction does not specify a domain, skip further domain checks" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false, logs fatal error", + "field": "sfDomainID (existence in ledger)", + "location": "finalize", + "validated_by": "view.exists(keylet::permissionedDomain(domain))", + "validates": [ + "Checks that the domain specified in the transaction exists in the ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false, logs fatal error", + "field": "domains_ set (consumed domains)", + "location": "finalize", + "validated_by": "for (auto const& d : domains_)", + "validates": [ + "Ensures all affected domains match the domain specified in the transaction" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false, logs fatal error", + "field": "regularOffers_ flag", + "location": "finalize", + "validated_by": "regularOffers_", + "validates": [ + "Ensures that domain transactions do not affect regular offers" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp.ai.md b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp.ai.md new file mode 100644 index 0000000000..4142e31f37 --- /dev/null +++ b/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp.ai.md @@ -0,0 +1,39 @@ +# `PermissionedDEXInvariant.cpp` + +## Role in the System + +This file implements `ValidPermissionedDEX`, one of roughly twenty invariant checkers that form the last line of defense in XRPL's transaction processing pipeline. The invariant checker framework (assembled as the `InvariantChecks` tuple in `InvariantCheck.h`) runs every checker against every successfully-applied transaction before its ledger mutations become permanent. If any checker returns `false`, the transaction is rejected even if transaction processing logic declared it valid — preventing ledger corruption that a consensus bug or exploit might otherwise cause. + +`ValidPermissionedDEX` specifically enforces the integrity of the Permissioned DEX amendment: a feature that allows currency exchanges to be scoped to a specific domain, where participation is gated by credentials. Offers, order-book directory nodes, and payments can carry a `sfDomainID` field (a 256-bit hash) that ties them to a particular permissioned domain. This invariant ensures that the ledger never ends up in a state where a domain-scoped transaction has silently crossed domain boundaries or left malformed objects behind. + +## Two-Phase Design + +The class follows the `visitEntry` / `finalize` protocol required of all XRPL invariants. The split is architecturally important: ledger entries are streamed past the checker one at a time (there is no random access during iteration), so `visitEntry` accumulates local state across all modified entries, and `finalize` evaluates the complete picture once all entries have been seen. + +`visitEntry` inspects every modified `ltDIR_NODE` and `ltOFFER` ledger entry in the `after` snapshot (the post-transaction state). It collects any `sfDomainID` values it encounters into `domains_`, a `hash_set`. If it encounters an offer _without_ a `sfDomainID`, it sets the `regularOffers_` flag. For offers marked with `lsfHybrid`, it performs a structural integrity check: a hybrid offer must have both a domain ID and an `sfAdditionalBooks` array of exactly one entry. Failing that constraint sets `badHybrids_`. No errors are raised during this phase — it is purely a data collection pass. + +`finalize` then evaluates the accumulated flags and set against the transaction itself. The first thing it does is scope itself: only `ttPAYMENT` and `ttOFFER_CREATE` transactions that succeeded (`isTesSuccess`) are checked. All others return `true` immediately, since those transaction types cannot legitimately produce domain-scoped DEX state. + +## What the Invariant Guarantees + +Four concrete properties are enforced in `finalize`: + +**Hybrid offer integrity.** For an `OfferCreate`, if `visitEntry` flagged any hybrid offer as structurally malformed, the invariant fails with a fatal log entry. A hybrid offer is one that participates simultaneously in both the standard DEX and a permissioned domain's DEX. Its `sfAdditionalBooks` array (with exactly one entry) records its secondary order-book placement. An `sfAdditionalBooks` array of size greater than one — or a hybrid offer missing either `sfDomainID` or `sfAdditionalBooks` — indicates that transaction logic produced an incoherent offer structure. + +**Domain existence.** If the transaction itself carries an `sfDomainID`, the invariant verifies via `keylet::permissionedDomain(domain)` that the corresponding `ltPERMISSIONED_DOMAIN` object actually exists in the ledger. A domain-scoped transaction referencing a non-existent domain is categorically invalid. + +**Domain isolation.** Every `ltDIR_NODE` and `ltOFFER` touched by the transaction must carry the _same_ domain ID as the transaction itself. If the `domains_` set collected any ID that does not match the transaction's declared domain, the invariant fails. This prevents a domain-scoped payment or order from silently consuming liquidity from a different permissioned domain. + +**No contamination of regular offers.** If the transaction specifies a domain, it must not have touched any regular (non-domain) offers. Setting `regularOffers_ = true` during `visitEntry` for any offer lacking `sfDomainID` causes an invariant failure here. This ensures the permissioned and open markets remain strictly segregated at the ledger level. + +## Design Tradeoffs and Notable Choices + +The decision to skip checking for non-successful transactions is deliberate for domain-isolation logic, but it means the `badHybrids_` check is also silently bypassed for failed `OfferCreate` transactions. The reasoning is that a failed transaction should not have produced any offer objects at all; if it did, that would be caught by other invariants (such as `NoBadOffers`) or by the structural checks during offer-book maintenance. The Permissioned DEX invariant is focused on semantic correctness of the permissioned boundary, not general offer validity. + +The use of a `hash_set` for `domains_` rather than a single optional value is a deliberate defensive choice. A single domain ID field would fail silently if, for example, two different domain IDs appeared across modified entries. Using a set allows the invariant to detect any case where the transaction touched entries from _more than one_ domain, a condition that would be invisible to a simple equality check against the first observed value. + +The `before` parameter in `visitEntry` is unused — the invariant only inspects post-transaction state. This is consistent with the invariant's purpose: it validates that what was written is correct, not what was overwritten. + +## Relationship to Sibling Files + +`ValidPermissionedDEX` sits alongside `ValidPermissionedDomain` (in `PermissionedDomainInvariant.cpp`) in the invariant suite. The two are complementary: `ValidPermissionedDomain` validates the well-formedness of `ltPERMISSIONED_DOMAIN` objects (credential arrays are sorted, unique, and within size limits), while `ValidPermissionedDEX` validates the integrity of the _usage_ of those domains in trading activity. Together they provide defense-in-depth for the Permissioned DEX feature at the protocol level. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp.ai.json b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp.ai.json new file mode 100644 index 0000000000..93898bb830 --- /dev/null +++ b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp.ai.json @@ -0,0 +1,231 @@ +{ + "args": [ + { + "lineno": 11, + "name": "isDel" + }, + { + "lineno": 12, + "name": "before" + }, + { + "lineno": 13, + "name": "after" + }, + { + "lineno": 43, + "name": "tx" + }, + { + "lineno": 44, + "name": "result" + }, + { + "lineno": 46, + "name": "view" + }, + { + "lineno": 47, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "InvariantCheck::visitEntry (base class, called by ApplyContext)", + "ValidPermissionedDomain::visitEntry" + ], + "entry_point": "ValidPermissionedDomain::visitEntry", + "purpose": "Collects and validates the state of sfAcceptedCredentials in SLEs of type ltPERMISSIONED_DOMAIN as they are modified by a transaction.", + "validation_points": [ + "Lambda 'check' inside visitEntry validates uniqueness and sortedness of sfAcceptedCredentials" + ] + }, + { + "call_chain": [ + "InvariantCheck::finalize (base class, called by ApplyContext after tx apply)", + "ValidPermissionedDomain::finalize" + ], + "entry_point": "ValidPermissionedDomain::finalize", + "purpose": "Performs final validation after transaction application, ensuring all permissioned domain invariants are enforced.", + "validation_points": [ + "Lambda 'check' inside finalize validates size, uniqueness, and sortedness of sfAcceptedCredentials", + "Additional logic checks number of affected entries, deletion, and transaction type" + ] + } + ], + "data_flows": [ + { + "field": "sfAcceptedCredentials", + "flow": [ + "SLE (before/after) passed to visitEntry", + "visitEntry extracts sfAcceptedCredentials via sle->getFieldArray(sfAcceptedCredentials)", + "credentials::makeSorted creates a sorted vector of (issuer, credentialType) pairs", + "Lambda 'check' in visitEntry compares sorted array to original for uniqueness and sortedness", + "SleStatus struct records validation results and is stored in sleStatus_", + "finalize iterates sleStatus_ and applies further validation" + ], + "origin": "SLE (ledger entry) of type ltPERMISSIONED_DOMAIN, field sfAcceptedCredentials (STArray)", + "transformations": [ + "Extraction from SLE as STArray", + "Sorting and duplicate detection via credentials::makeSorted", + "Comparison of sorted vs original for order and uniqueness" + ], + "validated_at": "Lambda 'check' in visitEntry (uniqueness, sortedness), Lambda 'check' in finalize (size, uniqueness, sortedness)" + } + ], + "description": "Implements the ValidPermissionedDomain invariant check for XRPL transactions, ensuring that permissioned domain ledger entries are only modified in valid ways, with credential arrays being unique, sorted, and within allowed size limits.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAcceptedCredentials (array field in SLE of type ltPERMISSIONED_DOMAIN)", + "empty", + "string", + "validation" + ], + "evidence": "lambda check in visitEntry at visitEntry", + "issue_pattern": "Missing empty string validation for sfAcceptedCredentials (array field in SLE of type ltPERMISSIONED_DOMAIN)", + "why_false_positive": "lambda check in visitEntry validates sfAcceptedCredentials (array field in SLE of type ltPERMISSIONED_DOMAIN) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAcceptedCredentials (array field in SLE of type ltPERMISSIONED_DOMAIN)", + "empty", + "string", + "validation" + ], + "evidence": "lambda check in finalize at finalize", + "issue_pattern": "Missing empty string validation for sfAcceptedCredentials (array field in SLE of type ltPERMISSIONED_DOMAIN)", + "why_false_positive": "lambda check in finalize validates sfAcceptedCredentials (array field in SLE of type ltPERMISSIONED_DOMAIN) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp", + "functions": [ + { + "args": [ + "isDel", + "before", + "after" + ], + "lineno": 9, + "name": "ValidPermissionedDomain::visitEntry" + }, + { + "args": [ + "tx", + "result", + "", + "view", + "j" + ], + "lineno": 41, + "name": "ValidPermissionedDomain::finalize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage likely exists in unit/integration tests for PermissionedDomainSet transactions and invariants, possibly in files like test/tx/PermissionedDomain_test.cpp or test/invariants/PermissionedDomainInvariant_test.cpp. However, gaps may exist for edge cases: (1) transactions affecting multiple permissioned domains, (2) deletion scenarios, (3) malformed or empty sfAcceptedCredentials arrays, (4) transactions with feature flag fixPermissionedDomainInvariant disabled. Manual review of test files is needed to confirm coverage of all invariant failure paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom invariant logic (no external validation framework)", + "validation_layer": "business_logic (Invariant enforcement in transaction processing)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (status is recorded in SleStatus, not thrown immediately)", + "field": "sfAcceptedCredentials (array field in SLE of type ltPERMISSIONED_DOMAIN)", + "location": "visitEntry", + "validated_by": "lambda check in visitEntry", + "validates": [ + "Checks if credentials array is unique (no duplicates)", + "Checks if credentials array is sorted by (sfIssuer, sfCredentialType)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns false, triggers invariant failure, logs fatal error)", + "field": "sfAcceptedCredentials (array field in SLE of type ltPERMISSIONED_DOMAIN)", + "location": "finalize", + "validated_by": "lambda check in finalize", + "validates": [ + "Checks that credentials array is not empty", + "Checks that credentials array size does not exceed maxPermissionedDomainCredentialsArraySize", + "Checks that credentials array is unique (no duplicates)", + "Checks that credentials array is sorted by (sfIssuer, sfCredentialType)" + ], + "validation_type": "business_logic|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp.ai.md b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp.ai.md new file mode 100644 index 0000000000..6082fcfcc0 --- /dev/null +++ b/src/libxrpl/tx/invariants/PermissionedDomainInvariant.cpp.ai.md @@ -0,0 +1,41 @@ +# `PermissionedDomainInvariant.cpp` + +## Role in the System + +This file implements `ValidPermissionedDomain`, one of the post-transaction invariant checks that the XRPL applies to every transaction before finalizing its effects on the ledger. The invariant specifically guards `ltPERMISSIONED_DOMAIN` ledger entries, which represent on-ledger access-control constructs whose validity depends entirely on the structural correctness of their `sfAcceptedCredentials` array. If that array is empty, oversized, contains duplicates, or is out of canonical order, downstream authorization logic would either fail silently or produce non-deterministic results across validator nodes — hence the need for a hard post-apply check. + +## Two-Phase Invariant Pattern + +Like all invariant checks in the XRPL engine, `ValidPermissionedDomain` follows the framework's two-phase contract. During transaction application, `ApplyContext` calls `visitEntry` once per modified ledger entry. After the transaction has been fully applied, `ApplyContext` calls `finalize` with the completed transaction and its result code. The class accumulates state between these calls in `sleStatus_`, a `std::vector` with one element per modified `ltPERMISSIONED_DOMAIN` entry. + +## `visitEntry`: Observation and Pre-Analysis + +`visitEntry` filters immediately on ledger entry type — entries that aren't `ltPERMISSIONED_DOMAIN` are silently ignored, keeping the check narrowly scoped. The method only examines the `after` state, not `before`, because the invariant's goal is to ensure the resulting ledger is valid; the pre-transaction state is immaterial for this purpose. + +For each qualifying entry the inner `check` lambda calls `credentials::makeSorted` on the raw `sfAcceptedCredentials` array. The behavior of `makeSorted` is central to how both uniqueness and sort order are verified simultaneously: it attempts to insert each `(sfIssuer, sfCredentialType)` pair into a `std::set`, and if any insertion fails due to an existing equal element it immediately returns an empty set. This means the `isUnique_` flag in `SleStatus` is set from `!sorted.empty()` — a non-obvious convention where "empty result" signals "duplicates found." + +Sort order is then checked only when `isUnique_` is true (duplicates invalidate the sorted comparison anyway). The code walks both the canonical `std::set` and the original `STArray` in lockstep via an index counter, comparing `sfIssuer` and `sfCredentialType` at each position. This avoids re-sorting the full credential objects; it reuses the already-computed `makeSorted` output. + +The resulting `SleStatus` records four values: the raw credential count, whether the array is sorted, whether it is unique, and whether the entry is being deleted (`isDel`). + +## `finalize`: Conditional Validation Under Feature Flag + +`finalize` contains the most architecturally interesting logic: a hard branch on `view.rules().enabled(fixPermissionedDomainInvariant)` that selects between an older, narrower invariant and a newer, comprehensive one. This pattern is common in XRPL for amendments that tighten protocol rules without retroactively altering historical ledger state. + +**Pre-amendment path** (feature not enabled): The check only runs when the transaction type is `ttPERMISSIONED_DOMAIN_SET` and the result was successful. This was the original invariant scope, verifying the credential array is valid after a set operation. It ignores delete transactions entirely. + +**Post-amendment path** (feature enabled, `fixPermissionedDomainInvariant`): This is significantly stricter. First, if the transaction failed (`!isTesSuccess(result)`), `sleStatus_` must be empty — a failed transaction must not have touched any permissioned domain entry at all. Second, a single transaction may affect at most one domain entry; if `sleStatus_.size() > 1` the invariant fails. Third, the check branches on transaction type: + +- `ttPERMISSIONED_DOMAIN_SET`: must have affected exactly one entry, that entry must not be a deletion, and the credentials must pass all four structural checks (non-empty, ≤ `maxPermissionedDomainCredentialsArraySize` which is 10, unique, sorted). +- `ttPERMISSIONED_DOMAIN_DELETE`: must have affected exactly one entry, and the entry must be flagged as deleted. No credential checks are needed for a deletion. +- Any other transaction type: must not have affected any domain entries at all. An unexpected modification by an unrelated transaction type is an invariant violation. + +The inner `check` lambda in `finalize` independently re-validates the same structural properties (non-empty, within size limit, unique, sorted) using the `SleStatus` data captured during `visitEntry`. The redundancy is intentional: `visitEntry` captures facts about each entry independently, while `finalize` applies the policy that ties those facts to the transaction type and its outcome. + +## Error Handling + +All invariant failures are reported via `JLOG(j.fatal())` and return `false`. A `false` return from any invariant's `finalize` causes `ApplyContext` to roll back the transaction entirely, protecting the ledger from entering an invalid state. There is no exception path — the invariant contract is enforced through return values, consistent with the broader XRPL error-handling philosophy. + +## Relationship to Sibling Files + +`PermissionedDomainInvariant.h` defines the `ValidPermissionedDomain` class with its private `SleStatus` struct, keeping the public interface to just two methods. `credentials::makeSorted` in `CredentialHelpers.cpp` is the shared utility that both this invariant and the `PermissionedDomainSet` transactor rely on for canonical credential ordering, ensuring the same normalization contract is applied both at write time and at invariant-check time. The `maxPermissionedDomainCredentialsArraySize = 10` constant from `Protocol.h` is the shared cap enforced both by `preflight` validation in the transactor and again here as a post-application safety net. \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp.ai.json b/src/libxrpl/tx/invariants/VaultInvariant.cpp.ai.json new file mode 100644 index 0000000000..34eb7ac02a --- /dev/null +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp.ai.json @@ -0,0 +1,352 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ValidVault::visitEntry", + "ValidVault::Vault::make / ValidVault::Shares::make" + ], + "entry_point": "ValidVault::visitEntry", + "purpose": "Processes ledger entry changes (creation, modification, deletion) for Vault and related objects, collecting state for invariant validation.", + "validation_points": [ + "XRPL_ASSERT in visitEntry (after != nullptr && (before != nullptr || !isDelete))", + "XRPL_ASSERT in Vault::make (from.getType() == ltVAULT)", + "XRPL_ASSERT in Shares::make (from.getType() == ltMPTOKEN_ISSUANCE)" + ] + }, + { + "call_chain": [ + "ValidVault::Vault::make" + ], + "entry_point": "ValidVault::Vault::make", + "purpose": "Constructs a Vault struct from a SLE, validating type and extracting fields.", + "validation_points": [ + "XRPL_ASSERT (from.getType() == ltVAULT)" + ] + }, + { + "call_chain": [ + "ValidVault::Shares::make" + ], + "entry_point": "ValidVault::Shares::make", + "purpose": "Constructs a Shares struct from a SLE, validating type and extracting fields.", + "validation_points": [ + "XRPL_ASSERT (from.getType() == ltMPTOKEN_ISSUANCE)" + ] + } + ], + "data_flows": [ + { + "field": "from.getType()", + "flow": [ + "SLE::getType()", + "XRPL_ASSERT in Vault::make or Shares::make", + "Field extraction if type matches" + ], + "origin": "SLE object passed to Vault::make or Shares::make", + "transformations": [ + "Type checked against expected value (ltVAULT or ltMPTOKEN_ISSUANCE)" + ], + "validated_at": "XRPL_ASSERT in Vault::make or Shares::make" + }, + { + "field": "sfAsset, sfOwner, sfShareMPTID, sfAssetsTotal, sfAssetsAvailable, sfAssetsMaximum, sfLossUnrealized", + "flow": [ + "SLE", + "Vault::make extracts fields after type validation", + "Stored in ValidVault::Vault struct" + ], + "origin": "Fields in SLE passed to Vault::make", + "transformations": [ + "Direct extraction via SLE::at or SLE::getAccountID" + ], + "validated_at": "After XRPL_ASSERT (type check) in Vault::make" + }, + { + "field": "sfOutstandingAmount, sfMaximumAmount, sfSequence, sfIssuer", + "flow": [ + "SLE", + "Shares::make extracts fields after type validation", + "Stored in ValidVault::Shares struct" + ], + "origin": "Fields in SLE passed to Shares::make", + "transformations": [ + "Direct extraction; sharesMaximum uses value_or fallback" + ], + "validated_at": "After XRPL_ASSERT (type check) in Shares::make" + }, + { + "field": "before, after, isDelete", + "flow": [ + "visitEntry", + "XRPL_ASSERT (after != nullptr && (before != nullptr || !isDelete))", + "Switch on before/after->getType() to extract and process fields" + ], + "origin": "Arguments to visitEntry (from invariant framework)", + "transformations": [ + "Checked for nullness and logical consistency" + ], + "validated_at": "XRPL_ASSERT at start of visitEntry" + } + ], + "description": "Implements the ValidVault invariant check for XRPL transactions, ensuring that all operations involving Vault objects and their associated shares comply with protocol rules and invariants during ledger state transitions.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "SLE type for Vault and Shares creation", + "validation", + "missing", + "check" + ], + "evidence": "Field SLE type for Vault and Shares creation validated by XRPL_ASSERT (custom assertion macro)", + "issue_pattern": "Missing validation for SLE type for Vault and Shares creation", + "why_false_positive": "XRPL_ASSERT (custom assertion macro) validates SLE type for Vault and Shares creation automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Presence and logical consistency of before/after/isDelete in visitEntry", + "validation", + "missing", + "check" + ], + "evidence": "Field Presence and logical consistency of before/after/isDelete in visitEntry validated by XRPL_ASSERT (custom assertion macro)", + "issue_pattern": "Missing validation for Presence and logical consistency of before/after/isDelete in visitEntry", + "why_false_positive": "XRPL_ASSERT (custom assertion macro) validates Presence and logical consistency of before/after/isDelete in visitEntry automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "from.getType()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ValidVault::Vault::make", + "issue_pattern": "Missing empty string validation for from.getType()", + "why_false_positive": "XRPL_ASSERT validates from.getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "from.getType()", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT at ValidVault::Vault::make", + "issue_pattern": "Missing type validation for from.getType()", + "why_false_positive": "XRPL_ASSERT validates from.getType() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "from.getType()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ValidVault::Shares::make", + "issue_pattern": "Missing empty string validation for from.getType()", + "why_false_positive": "XRPL_ASSERT validates from.getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "from.getType()", + "type", + "validation", + "check" + ], + "evidence": "XRPL_ASSERT at ValidVault::Shares::make", + "issue_pattern": "Missing type validation for from.getType()", + "why_false_positive": "XRPL_ASSERT validates from.getType() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "after, before, isDelete", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ValidVault::visitEntry", + "issue_pattern": "Missing empty string validation for after, before, isDelete", + "why_false_positive": "XRPL_ASSERT validates after, before, isDelete for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/invariants/VaultInvariant.cpp", + "functions": [ + { + "args": [ + "SLE const& from" + ], + "lineno": 13, + "name": "ValidVault::Vault::make" + }, + { + "args": [ + "SLE const& from" + ], + "lineno": 27, + "name": "ValidVault::Shares::make" + }, + { + "args": [ + "bool isDelete", + "std::shared_ptr const& before", + "std::shared_ptr const& after" + ], + "lineno": 41, + "name": "ValidVault::visitEntry" + }, + { + "args": [ + "STTx const& tx", + "TER const ret", + "XRPAmount const fee", + "ReadView const& view", + "beast::Journal const& j" + ], + "lineno": 99, + "name": "ValidVault::finalize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is part of the XRPL invariant framework, typically tested via integration and unit tests for transaction invariants. Likely test files: 'Invariant_test.cpp', 'VaultInvariant_test.cpp', or similar in the tx/invariants or workflow/test directories. Tests should cover creation, modification, and deletion of Vault and MPTokenIssuance objects, including type mismatches and field extraction. Gaps may exist if edge cases (e.g., malformed SLEs, missing fields, or unexpected types) are not explicitly tested. Direct unit tests for Vault::make and Shares::make are recommended to ensure type validation and field extraction are robust.", + "validation_architecture": { + "auto_validated_fields": [ + "SLE type for Vault and Shares creation", + "Presence and logical consistency of before/after/isDelete in visitEntry" + ], + "framework": "XRPL_ASSERT (custom assertion macro)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "from.getType()", + "location": "ValidVault::Vault::make", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures the SLE object passed is of type ltVAULT" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "from.getType()", + "location": "ValidVault::Shares::make", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures the SLE object passed is of type ltMPTOKEN_ISSUANCE" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "after, before, isDelete", + "location": "ValidVault::visitEntry", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that if before is empty, isDelete must be false", + "Ensures that after is not null", + "Ensures that at least one object is available for processing" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/invariants/VaultInvariant.cpp.ai.md b/src/libxrpl/tx/invariants/VaultInvariant.cpp.ai.md new file mode 100644 index 0000000000..d9f693697a --- /dev/null +++ b/src/libxrpl/tx/invariants/VaultInvariant.cpp.ai.md @@ -0,0 +1,61 @@ +# `VaultInvariant.cpp` — Post-Transaction Invariant Checker for Vault Objects + +## Role in the System + +`VaultInvariant.cpp` implements `ValidVault`, one of the specialized invariant checkers in XRPL's post-transaction safety net. It is registered as the 24th entry in the `InvariantChecks` tuple defined in `InvariantCheck.h`, where all invariant checkers are composed and run together after every successful or failed transaction application. + +The invariant exists because the Vault feature (`featureSingleAssetVault`) introduces a complex web of mutually-consistent ledger objects: a `ltVAULT` object tracking assets and shares, an `ltMPTOKEN_ISSUANCE` for the share token, individual `ltMPTOKEN` entries per depositor, and an `ltACCOUNT_ROOT` for the vault's pseudo-account. Any bug in the transaction handlers could corrupt this web in ways that would be catastrophic — e.g., crediting assets without minting shares, or allowing over-withdrawal. The invariant checker is the last line of defense. + +## Two-Phase Pattern: `visitEntry` → `finalize` + +Like every invariant in the framework, `ValidVault` works in two phases. + +**Phase 1 — `visitEntry`**: called once per touched ledger entry. The checker does not know the transaction type yet, nor does it know which `ltMPTOKEN_ISSUANCE` objects are vault shares versus unrelated MPT issuances. So it takes a defensive approach: it snapshots every `ltVAULT` object it sees into `beforeVault_` / `afterVault_`, and snapshots all `ltMPTOKEN_ISSUANCE` entries into `beforeMPTs_` / `afterMPTs_` for later reconciliation against the vault's `sfShareMPTID`. + +The key mechanism in `visitEntry` is the `deltas_` map — a `uint256 → Number` table that captures the net balance change for every interesting ledger entry. The sign convention is: for entries where higher balance means more assets flowing *into* the vault (account roots, trust lines, MPToken holdings), the sign is `-1` (balance decreased means assets went to vault). For `ltMPTOKEN_ISSUANCE`, outstanding amount increases as shares are minted, so sign is `+1`. The delta is computed as `(balanceBefore - balanceAfter) * sign`, which yields a positive number when assets/shares flow into the relevant account. + +Notably, a delta entry is stored even when the balance hasn't changed (the comment at line 130 explains this): a transaction may update an account root for other reasons (e.g., sequence number) while having a zero net balance delta, and using a non-zero `sign` as the filter ensures the entry is captured for accounting completeness rather than relying on a zero-comparison that could mask subtle accounting bugs. + +**Phase 2 — `finalize`**: called once after all entries have been visited. This is where the actual invariants are evaluated. + +## Enforcement vs. Assertion: The `enforce` Pattern + +A subtle design decision runs throughout `finalize`: every fatal invariant failure is accompanied by `XRPL_ASSERT(enforce, ...)` before returning `!enforce`. The `enforce` boolean is `true` when the `featureSingleAssetVault` amendment is active. + +This two-tier system is explained in `InvariantCheckPrivilege.h`: `XRPL_ASSERT` fires (crashing the process) only in debug/test builds. In a production build with the amendment active, `enforce` is `true`, so `!enforce` is `false`, meaning the transaction is rejected hard. In a test or developer build where the amendment is *not* yet enabled, `!enforce` is `true` — the violation is logged but the transaction is allowed through. This is intentionally painful for developers, designed to surface invariant violations while building vault-adjacent features before the amendment goes live. + +## `finalize` Logic Flow + +The `finalize` function first dispatches on whether any vault was touched at all, using `hasPrivilege` to cross-check the transaction type's declared capabilities against what actually happened. For example, a transaction with `mustModifyVault` privilege that produced no vault change is a protocol bug; a non-vault transaction that somehow mutated a vault is equally suspicious. The privilege system is a bitmask enumerated in `InvariantCheckPrivilege.h`. + +Deletion (`ttVAULT_DELETE`) is handled early as a special case, because it's the only vault-modifying transaction with no "after" vault state. The invariant verifies the deleted vault had zero shares outstanding, zero `assetsTotal`, and zero `assetsAvailable`, and that the corresponding `ltMPTOKEN_ISSUANCE` was co-deleted in the same transaction. + +For all other transaction types, `finalize` matches the vault's `sfShareMPTID` against the accumulated MPT issuance snapshots and falls back to reading from the current `ReadView` if the issuance was not itself modified in the transaction. This handles the common case where a deposit touches `sfOutstandingAmount` but the `ltMPTOKEN_ISSUANCE` was not otherwise part of the modified set. + +Universal checks applied to every non-delete vault operation include: +- Immutable fields (`sfAsset`, `sfAccount`/pseudo-ID, `sfShareMPTID`) must not change between before and after states. +- `assetsAvailable` ≤ `assetsTotal` ≥ 0; `assetsMaximum` ≥ 0. +- `lossUnrealized` ≤ `assetsTotal - assetsAvailable`. +- `lossUnrealized` must not change except in loan transactions (`ttLOAN_MANAGE`, `ttLOAN_PAY`), since loss tracking is a loan-layer concern. + +## Per-Transaction Invariants + +**`ttVAULT_CREATE`**: Vault must have been newly created (no "before" state), must be empty (all fields zero), the shares issuer must be a pseudo-account, and the pseudo-account's `sfVaultID` must point back to this vault — ensuring the bidirectional link is always set up atomically. + +**`ttVAULT_SET`**: Metadata-only update. The invariant requires that the vault's asset balance at the pseudo-account did not change, `assetsTotal` and `assetsAvailable` are unchanged, and shares outstanding are unchanged. The delta-map lookup for the pseudo-account's key is the mechanism used to detect any accidental asset movement. + +**`ttVAULT_DEPOSIT`**: The vault's asset balance must increase, the depositor's asset balance must decrease by the same magnitude, and the depositor's share holdings must increase. The `assetsTotal` and `assetsAvailable` fields must each increase by exactly `vaultDeltaAssets`. One nuance: if the depositor is the asset's IOU issuer, their balance does not change (issuer payments create supply rather than moving existing funds), so the account-side delta check is bypassed with `issuerDeposit`. + +**`ttVAULT_WITHDRAW`**: Symmetric to deposit. The vault's asset balance decreases, the destination's balance increases by the same amount. The destination may differ from `sfAccount` if `sfDestination` is set (directed withdrawal). The invariant enforces exactly one destination account changes balance — not both `sfAccount` and `sfDestination`. Issuer withdrawals are similarly exempted from the balance-equality check. + +**`ttVAULT_CLAWBACK`**: The asset issuer may forcibly withdraw assets and burn shares. The invariant first validates the caller is either the asset's issuer or — as a special case — the vault *owner* performing an emergency share-burn of an empty vault (e.g., to recover from a state where shares exist but assets do not). The holder's shares must decrease, and the corresponding issuance outstanding must decrease by the same amount. + +**Loan transactions** (`ttLOAN_SET`, `ttLOAN_MANAGE`, `ttLOAN_PAY`): Currently checked as TBD placeholders returning `true`, indicating the loan invariant logic is delegated to the separate `ValidLoan` and `ValidLoanBroker` checkers. + +## Asset Type Polymorphism + +The `deltaAssets` lambda handles all three XRPL asset types through `std::visit` on the `Asset` variant. For XRP it looks up the `AccountRoot` key; for IOU trust lines it looks up the `RippleState` key and applies a sign flip based on which side of the trust line the account is on (`id > issue.getIssuer() ? -1 : 1`, reflecting the asymmetric balance storage in `ltRIPPLE_STATE`); for MPT assets it looks up the `MPToken` key for the specific holder. This polymorphism allows the same delta-accounting logic to work correctly regardless of what asset a vault holds. + +## Fee Compensation for XRP Vaults + +For XRP-denominated vault deposits and withdrawals, the transaction fee must be added back to the sender's observed balance delta before comparing against the vault's delta. Without this correction, a depositor who also pays a fee would appear to have sent more XRP than the vault received. The compensation is skipped for delegated transactions (where `sfDelegate` differs from `sfAccount`), because in that case the delegate — not the sender — pays the fee and the delegate's balance is tracked separately. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.json b/src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.json new file mode 100644 index 0000000000..6f6bd24506 --- /dev/null +++ b/src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.json @@ -0,0 +1,361 @@ +{ + "args": [ + { + "lineno": 8, + "name": "view" + }, + { + "lineno": 9, + "name": "ammAccountID" + }, + { + "lineno": 10, + "name": "tradingFee" + }, + { + "lineno": 11, + "name": "in" + }, + { + "lineno": 12, + "name": "out" + }, + { + "lineno": 13, + "name": "ammContext" + }, + { + "lineno": 14, + "name": "j" + }, + { + "lineno": 35, + "name": "balances" + }, + { + "lineno": 82, + "name": "out" + }, + { + "lineno": 82, + "name": "asset" + }, + { + "lineno": 89, + "name": "rules" + }, + { + "lineno": 109, + "name": "clobQuality" + } + ], + "classes": [ + { + "args": [ + "view", + "ammAccountID", + "tradingFee", + "in", + "out", + "ammContext", + "j" + ], + "lineno": 7, + "name": "AMMLiquidity" + } + ], + "code_paths": [ + { + "call_chain": [ + "AMMLiquidity::AMMLiquidity", + "AMMLiquidity::fetchBalances" + ], + "entry_point": "AMMLiquidity::AMMLiquidity (constructor)", + "purpose": "Initializes AMMLiquidity object and fetches/validates AMM account balances for assetIn and assetOut.", + "validation_points": [ + "AMMLiquidity::fetchBalances: Validates amountIn and amountOut >= 0, throws std::runtime_error if invalid" + ] + }, + { + "call_chain": [ + "AMMLiquidity::generateFibSeqOffer" + ], + "entry_point": "AMMLiquidity::generateFibSeqOffer", + "purpose": "Generates an AMM offer using a Fibonacci sequence scaling, validates output amount and iteration limits.", + "validation_points": [ + "AMMLiquidity::generateFibSeqOffer: XRPL_ASSERT on !ammContext_.maxItersReached()", + "AMMLiquidity::generateFibSeqOffer: Throws std::overflow_error if cur.out >= balances.out" + ] + }, + { + "call_chain": [ + "AMMLiquidity::maxOffer", + "maxOut", + "toAmount" + ], + "entry_point": "AMMLiquidity::maxOffer", + "purpose": "Calculates the maximum offer the AMM can provide, with validation on output amount.", + "validation_points": [ + "AMMLiquidity::maxOffer: Checks out <= 0 or out >= balances.out, returns std::nullopt if invalid" + ] + } + ], + "data_flows": [ + { + "field": "amountIn", + "flow": [ + "fetchBalances", + "TAmounts{get(amountIn), ...}", + "initialBalances_ (constructor)", + "used in generateFibSeqOffer, maxOffer, etc." + ], + "origin": "ammAccountHolds(view, ammAccountID_, assetIn_) in fetchBalances", + "transformations": [ + "Converted to TIn via get()", + "Used in swapAssetIn, toAmount, etc." + ], + "validated_at": "fetchBalances: if (amountIn < beast::zero) Throw" + }, + { + "field": "amountOut", + "flow": [ + "fetchBalances", + "TAmounts{..., get(amountOut)}", + "initialBalances_ (constructor)", + "used in generateFibSeqOffer, maxOffer, etc." + ], + "origin": "ammAccountHolds(view, ammAccountID_, assetOut_) in fetchBalances", + "transformations": [ + "Converted to TOut via get()", + "Used in swapAssetOut, toAmount, etc." + ], + "validated_at": "fetchBalances: if (amountOut < beast::zero) Throw" + }, + { + "field": "cur.out", + "flow": [ + "generateFibSeqOffer: cur.out assigned", + "cur.out scaled by Fibonacci factor", + "cur.out validated against balances.out" + ], + "origin": "swapAssetIn(initialBalances_, cur.in, tradingFee_) in generateFibSeqOffer", + "transformations": [ + "Scaled by Fibonacci sequence", + "Converted to TOut via toAmount" + ], + "validated_at": "generateFibSeqOffer: if (cur.out >= balances.out) Throw" + }, + { + "field": "ammContext_.curIters()", + "flow": [ + "generateFibSeqOffer: checked for 0", + "used as index into Fibonacci array", + "XRPL_ASSERT(!ammContext_.maxItersReached())" + ], + "origin": "AMMContext object passed to constructor", + "transformations": [ + "Used as loop/iteration control" + ], + "validated_at": "generateFibSeqOffer: XRPL_ASSERT(!ammContext_.maxItersReached())" + } + ], + "description": "Implements the AMMLiquidity class template for XRPL, providing methods to fetch balances, generate offers, and interact with AMM pools for different asset types.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amountIn, amountOut (AMM account balances for assetIn and assetOut)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check and Throw at fetchBalances", + "issue_pattern": "Missing empty string validation for amountIn, amountOut (AMM account balances for assetIn and assetOut)", + "why_false_positive": "explicit check and Throw validates amountIn, amountOut (AMM account balances for assetIn and assetOut) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "amountIn, amountOut (AMM account balances for assetIn and assetOut)", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check and Throw at fetchBalances", + "issue_pattern": "Missing range validation for amountIn, amountOut (AMM account balances for assetIn and assetOut)", + "why_false_positive": "explicit check and Throw validates amountIn, amountOut (AMM account balances for assetIn and assetOut) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "cur.out (output amount for offer)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check and Throw at generateFibSeqOffer", + "issue_pattern": "Missing empty string validation for cur.out (output amount for offer)", + "why_false_positive": "explicit check and Throw validates cur.out (output amount for offer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ammContext_.maxItersReached()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at generateFibSeqOffer", + "issue_pattern": "Missing empty string validation for ammContext_.maxItersReached()", + "why_false_positive": "XRPL_ASSERT macro validates ammContext_.maxItersReached() for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/AMMLiquidity.cpp", + "functions": [ + { + "args": [ + "view", + "ammAccountID", + "tradingFee", + "in", + "out", + "ammContext", + "j" + ], + "lineno": 7, + "name": "AMMLiquidity" + }, + { + "args": [ + "view" + ], + "lineno": 20, + "name": "fetchBalances" + }, + { + "args": [ + "balances" + ], + "lineno": 34, + "name": "generateFibSeqOffer" + }, + { + "args": [], + "lineno": 67, + "name": "maxAmount" + }, + { + "args": [ + "out", + "asset" + ], + "lineno": 81, + "name": "maxOut" + }, + { + "args": [ + "balances", + "rules" + ], + "lineno": 88, + "name": "maxOffer" + }, + { + "args": [ + "view", + "clobQuality" + ], + "lineno": 108, + "name": "getOffer" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this code is likely found in unit/integration tests for AMM pathfinding and offer generation, such as 'test/tx/AMMOffer_test.cpp', 'test/tx/AMMLiquidity_test.cpp', or similar. These should cover normal and edge cases for balance validation, offer generation, and overflow/iteration errors. However, explicit negative tests for the Throw and Throw cases (e.g., negative balances, exceeding output) may be missing or incomplete. Tests for XRPL_ASSERT (maxItersReached) may be harder to trigger in normal test flows unless specifically targeted. Review of test files is needed to confirm coverage of all validation branches.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (explicit checks, XRPL_ASSERT macro, Throw utility)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"AMMLiquidity: invalid balances\")", + "field": "amountIn, amountOut (AMM account balances for assetIn and assetOut)", + "location": "fetchBalances", + "validated_by": "explicit check and Throw", + "validates": [ + "amountIn >= 0", + "amountOut >= 0" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::overflow_error (\"AMMLiquidity: generateFibSeqOffer exceeds the balance\")", + "field": "cur.out (output amount for offer)", + "location": "generateFibSeqOffer", + "validated_by": "explicit check and Throw", + "validates": [ + "cur.out < balances.out" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "ammContext_.maxItersReached()", + "location": "generateFibSeqOffer", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "maximum iterations not reached" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.md b/src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.md new file mode 100644 index 0000000000..79ad8efe17 --- /dev/null +++ b/src/libxrpl/tx/paths/AMMLiquidity.cpp.ai.md @@ -0,0 +1,40 @@ +# AMMLiquidity.cpp + +`AMMLiquidity` is the adapter between an on-ledger Automated Market Maker pool and the XRPL payment engine's offer-book traversal layer (`BookStep`). It generates synthetic offers from live AMM pool state so that `BookStep` can treat AMM liquidity identically to limit-order-book (CLOB) offers during payment path execution. + +## Role in the Payment Engine + +Payment execution in the XRPL walks through book steps, consuming offers one at a time. CLOB offers are finite discrete records; an AMM pool is a continuous curve. `AMMLiquidity` solves this mismatch by producing `AMMOffer` objects — value types that mirror the `TOffer` interface — sized according to the current interaction context. A single `AMMLiquidity` instance lives for the duration of one payment's book-step execution; it owns the `initialBalances_` snapshot taken at construction while calling `fetchBalances()` freshly each time `getOffer()` is invoked so it always prices against the pool's current state after prior swap activity. + +## Two Distinct Offer Generation Modes + +The class bifurcates its strategy entirely on whether `AMMContext::multiPath()` is true. + +**Single-path mode** (one payment path, competing with CLOB): `getOffer()` calls `changeSpotPriceQuality()` to solve for swap amounts that, if consumed, would move the AMM's spot-price quality exactly to the CLOB offer quality passed in as `clobQuality`. This is the mathematically correct approach: it computes the exact input/output pair satisfying the constant-product invariant and the quality constraint, dispatching to `getAMMOfferStartWithTakerGets` or `getAMMOfferStartWithTakerPays` depending on which asset is XRP (to round XRP first and maximise quality). If `changeSpotPriceQuality` cannot produce a valid offer (e.g., pool too small or fee too high), the `fixAMMv1_2` amendment allows falling back to `maxOffer` provided that offer's quality still beats the CLOB's. + +When `clobQuality` is entirely absent — meaning there is no competing CLOB and the path is unconstrained — `getOffer()` returns `maxOffer()`, which deliberately over-sizes the offer to the theoretical maximum. The actual consumed amount is trimmed later by `BookStep` according to send-max, deliver limits, or available funds. + +**Multi-path mode** (multiple payment paths): calling `changeSpotPriceQuality` independently on each path would cause each path to consume the whole quality differential, leading to double-counting. Instead, `generateFibSeqOffer` produces a sequence of exponentially growing synthetic offers keyed to `AMMContext::curIters()`, which counts how many payment-engine iterations have already consumed an AMM offer. The starting offer is a tiny fraction of the pool (`InitialFibSeqPct = 5/20000 = 0.025%` of the in-asset balance) and the output is then scaled by the Fibonacci multiplier for the current iteration: + +``` +fib[] = {1, 2, 3, 5, 8, 13, 21, …, 1346269} // 30 entries +cur.out = (base_out) × fib[curIters - 1] +``` + +At iteration 0 the base offer is returned directly; from iteration 1 onward the prior output is scaled by successive Fibonacci numbers. The sequence grows to cover the full pool asymptotically while guaranteeing each individual offer is physically valid (`cur.out < balances.out`; violation throws `std::overflow_error`). The iteration counter is capped at `AMMContext::MaxIterations = 30` — enforced by both an early exit in `getOffer()` and an `XRPL_ASSERT` inside `generateFibSeqOffer`. This cap prevents AMM offers from indefinitely dominating the offer-counter budget that `BookStep` does not otherwise apply to AMM. + +## The maxOffer and Its Amendment Duality + +`maxOffer()` exists in two behaviours controlled by the `fixAMMOverflowOffer` amendment. Pre-amendment, it constructs an offer with `takerPays = maxAmount()` (the protocol ceiling for the type) and lets `swapAssetIn` compute the corresponding output. This is safe mathematically but can produce astronomically large offers that overflow intermediate calculations, hence it was the source of the overflow exception previously swallowed inside `getOffer`. Post-amendment, the function instead sizes output at 99% of the current pool balance (`out = 0.99 × balances.out`) via the file-local `maxOut()` helper, then back-calculates input via `swapAssetOut`. This is bounded and cannot overflow. If the capped output would be zero or would equal the full balance (corner case for tiny pools), `maxOffer()` returns `std::nullopt`. + +## Quality Threshold Guard + +Before any offer is generated, `getOffer()` computes the pool's current spot-price quality and bails out if it is no better than the CLOB quality, or if it is within a relative distance of 1×10⁻⁷ of it. This threshold is intentional: after a partial swap the new spot price may approach but never quite reach the target due to floating-point rounding, and without the threshold the loop could iterate fruitlessly for many rounds. + +## Template Instantiation and Type System + +The class is parameterised on `` constrained by `StepAmount`. The translation unit closes with eight explicit instantiations covering all legal asset-type pairings: `IOUAmount`×`IOUAmount`, `XRPAmount`×`IOUAmount`, `IOUAmount`×`XRPAmount`, and the five `MPTAmount` combinations. Each pairing has distinct rounding semantics inside `swapAssetIn` / `swapAssetOut`, which always round in the AMM's favour — outputs are rounded down, inputs are rounded up — to preserve the constant-product invariant under integer representation constraints. + +## Error Handling Architecture + +`getOffer()` wraps all offer computation in a try/catch block. An `std::overflow_error` — thrown by `generateFibSeqOffer` when the Fibonacci-scaled output exceeds the pool — is caught here. Pre-`fixAMMOverflowOffer`, the fallback is to attempt `maxOffer`; post-amendment it returns `std::nullopt` instead, letting the payment engine continue with whatever CLOB offers remain. Any other `std::exception` is logged and suppressed, returning `std::nullopt`. This layered approach means a problematic AMM pool cannot abort an entire payment transaction; it simply ceases to contribute liquidity for that path. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/AMMOffer.cpp.ai.json b/src/libxrpl/tx/paths/AMMOffer.cpp.ai.json new file mode 100644 index 0000000000..bd92b9b88a --- /dev/null +++ b/src/libxrpl/tx/paths/AMMOffer.cpp.ai.json @@ -0,0 +1,246 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "AMMLiquidity const& ammLiquidity", + "TAmounts const& amounts", + "TAmounts const& balances", + "Quality const& quality" + ], + "lineno": 8, + "name": "AMMOffer" + } + ], + "code_paths": [ + { + "call_chain": [ + "BookStep::consumeOffer (external, not shown)", + "AMMOffer::consume" + ], + "entry_point": "AMMOffer::consume", + "purpose": "Consumes (partially or fully) an AMM offer, updating state and marking as used.", + "validation_points": [ + "AMMOffer::consume: Checks if consumed.in > amounts_.in || consumed.out > amounts_.out, throws if invalid." + ] + }, + { + "call_chain": [ + "BookStep::consumeOffer (external, not shown)", + "AMMOffer::checkInvariant" + ], + "entry_point": "AMMOffer::checkInvariant", + "purpose": "Validates that the consumed amounts do not exceed the original offer amounts (invariant check).", + "validation_points": [ + "AMMOffer::checkInvariant: Checks if consumed.in > amounts_.in || consumed.out > amounts_.out, logs error if invalid." + ] + }, + { + "call_chain": [ + "BookStep::limitOffer (external, not shown)", + "AMMOffer::limitOut" + ], + "entry_point": "AMMOffer::limitOut", + "purpose": "Calculates the maximum output amount for an offer, possibly rounding and applying quality/fees.", + "validation_points": [ + "No explicit validation, but uses quality().ceil_out_strict or swapAssetOut, which may have internal checks." + ] + }, + { + "call_chain": [ + "BookStep::limitOffer (external, not shown)", + "AMMOffer::limitIn" + ], + "entry_point": "AMMOffer::limitIn", + "purpose": "Calculates the maximum input amount for an offer, possibly rounding and applying quality/fees.", + "validation_points": [ + "No explicit validation, but uses quality().ceil_in_strict or swapAssetIn, which may have internal checks." + ] + } + ], + "data_flows": [ + { + "field": "amounts_", + "flow": [ + "AMMOffer::AMMOffer (constructor)", + "Stored in amounts_", + "Read in AMMOffer::amount, AMMOffer::consume, AMMOffer::checkInvariant" + ], + "origin": "Constructor parameter (TAmounts const& amounts)", + "transformations": [ + "None (read-only after construction)" + ], + "validated_at": "AMMOffer::consume, AMMOffer::checkInvariant (compared against consumed)" + }, + { + "field": "consumed", + "flow": [ + "Passed from BookStep::consumeOffer (external)", + "Validated in AMMOffer::consume or AMMOffer::checkInvariant", + "If valid, triggers state update (consumed_ = true, context().setAMMUsed())" + ], + "origin": "Parameter to AMMOffer::consume and AMMOffer::checkInvariant", + "transformations": [ + "None (used for validation and state update)" + ], + "validated_at": "AMMOffer::consume, AMMOffer::checkInvariant" + }, + { + "field": "balances_", + "flow": [ + "AMMOffer::AMMOffer (constructor)", + "Stored in balances_", + "Used in limitOut, limitIn, getQualityFunc" + ], + "origin": "Constructor parameter (TAmounts const& balances)", + "transformations": [ + "Passed to swapAssetOut, swapAssetIn, QualityFunction" + ], + "validated_at": "Not directly validated in this file" + }, + { + "field": "quality_", + "flow": [ + "AMMOffer::AMMOffer (constructor)", + "Stored in quality_", + "Used in limitOut, limitIn, getQualityFunc" + ], + "origin": "Constructor parameter (Quality const& quality)", + "transformations": [ + "Passed to quality().ceil_out_strict, quality().ceil_in_strict, etc." + ], + "validated_at": "Not directly validated in this file" + } + ], + "description": "Implements the AMMOffer template class for Automated Market Maker (AMM) offers in the XRPL, providing methods for offer management, quality calculation, and invariant checking.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "consumed (TAmounts const& consumed)", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at AMMOffer::consume", + "issue_pattern": "Missing empty string validation for consumed (TAmounts const& consumed)", + "why_false_positive": "explicit if-check validates consumed (TAmounts const& consumed) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/AMMOffer.cpp", + "functions": [ + { + "args": [ + "AMMLiquidity const& ammLiquidity", + "TAmounts const& amounts", + "TAmounts const& balances", + "Quality const& quality" + ], + "lineno": 8, + "name": "AMMOffer" + }, + { + "args": [], + "lineno": 17, + "name": "assetIn" + }, + { + "args": [], + "lineno": 23, + "name": "assetOut" + }, + { + "args": [], + "lineno": 29, + "name": "owner" + }, + { + "args": [], + "lineno": 35, + "name": "amount" + }, + { + "args": [ + "ApplyView& view", + "TAmounts const& consumed" + ], + "lineno": 41, + "name": "consume" + }, + { + "args": [ + "TAmounts const& offerAmount", + "TOut const& limit", + "bool roundUp" + ], + "lineno": 56, + "name": "limitOut" + }, + { + "args": [ + "TAmounts const& offerAmount", + "TIn const& limit", + "bool roundUp" + ], + "lineno": 75, + "name": "limitIn" + }, + { + "args": [], + "lineno": 91, + "name": "getQualityFunc" + }, + { + "args": [ + "TAmounts const& consumed", + "beast::Journal j" + ], + "lineno": 98, + "name": "checkInvariant" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::logic_error for error reporting", + "exception_type": "std::logic_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "The core validation (consumed <= amounts_) is critical and likely tested in integration/flow tests for AMM offers and BookStep logic. However, there is no evidence in this file of direct unit tests for AMMOffer. Tests likely exist in files such as 'test/AMMOffer_test.cpp', 'test/BookStep_test.cpp', or broader transaction/AMM integration tests. Gaps may exist in edge cases (e.g., boundary values for consumed amounts, multi-path logic, or error handling for exceptions). Direct negative tests for the exception path in consume and error logging in checkInvariant should be verified.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, custom Throw<> macro)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::logic_error (via Throw)", + "field": "consumed (TAmounts const& consumed)", + "location": "AMMOffer::consume", + "validated_by": "explicit if-check", + "validates": [ + "consumed.in must be <= amounts_.in", + "consumed.out must be <= amounts_.out" + ], + "validation_type": "business_logic|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/AMMOffer.cpp.ai.md b/src/libxrpl/tx/paths/AMMOffer.cpp.ai.md new file mode 100644 index 0000000000..452dec08f4 --- /dev/null +++ b/src/libxrpl/tx/paths/AMMOffer.cpp.ai.md @@ -0,0 +1,50 @@ +# `AMMOffer.cpp` — Synthetic AMM Offer Adapter for XRPL's Payment Engine + +## Role in the System + +The XRPL payment engine routes payments through a sequence of `BookStep` objects, each of which consumes offers from an order book. Prior to AMM integration, every offer was a `TOffer` backed by a real ledger entry. `AMMOffer` is the bridge that allows an Automated Market Maker pool to participate in that same flow without being a real order-book entry. + +The class is intentionally designed as a **structural mirror** of `TOffer`: it exposes the same methods (`assetIn`, `assetOut`, `owner`, `amount`, `consume`, `limitOut`, `limitIn`, `fully_consumed`, `isFunded`, `adjustRates`, `send`, `checkInvariant`) so that `BookStep`'s generic, template-based inner loop handles AMM and CLOB liquidity through the same code paths. The design is duck-typed at the C++ template level — `BookStep` is parameterized on an offer type and calls those methods without knowing which kind of offer it holds. + +## Template Parameterization + +`AMMOffer` is constrained to types satisfying the `StepAmount` concept, and the file ends with eight explicit instantiations covering every legal pairing of `XRPAmount`, `IOUAmount`, and `MPTAmount`. This approach keeps the bulk of the implementation in `.cpp` rather than headers while still allowing all token-type combinations the ledger supports. + +## Core State + +The constructor receives four immutable pieces of state: + +- **`ammLiquidity_`** — a reference to the `AMMLiquidity` manager that vends offers and holds the pool's `AMMContext`. This reference is the lifeline back to the pool and to the transaction's execution context. +- **`amounts_`** — the initial offer size as seen by `BookStep`. In single-path mode this is set to either a quality-matched size or a pool-draining maximum; in multi-path mode it is a Fibonacci-sequence-scaled amount. After construction, `amounts_` is read-only. +- **`balances_`** — a snapshot of the live pool token balances at the moment the offer was generated. These are used in single-path mode to compute exact swap amounts via the constant-product formula. +- **`quality_`** — either the spot-price quality (when `balances_ != amounts_`) or the amounts quality. In multi-path mode this becomes the fixed proportional rate used when the offer is partially consumed. + +## The Single-Path / Multi-Path Duality + +The most architecturally significant design choice in this file is the bifurcation inside `limitOut`, `limitIn`, and `getQualityFunc` based on `ammLiquidity_.multiPath()`. + +**Single-path mode** (one payment path, no competing paths): the offer can be resized according to the AMM's own conservation function. `limitOut` calls `swapAssetOut(balances_, limit, tradingFee())` and `limitIn` calls `swapAssetIn(balances_, limit, tradingFee())`, which apply the constant-product formula `(x + Δx)(y − Δy) = xy` (adjusted for the trading fee) to compute the exact input or output the pool would require. Because there is only one path, changing the offer's effective quality does not disturb any ordering among strands. + +**Multi-path mode** (multiple strands, Fibonacci-sized offers): the AMM offer is deliberately made to behave like a CLOB offer. Resizing is done proportionally to the original quality using `Quality::ceil_out_strict` or `Quality::ceil_in_strict`, which means the taker pays slightly more than the AMM formula alone would demand. The comment in the source explains why: this overshooting causes the post-trade pool product `(poolPays − assetOut)(poolGets + assetIn)` to exceed the original `poolPays × poolGets`, preserving the constant-product invariant even when rounding introduces small errors. If the offer quality were instead recomputed from the live formula at each limiting step, it could shift the relative quality ordering of strands and corrupt the path optimization. + +`getQualityFunc` mirrors the same split: for multi-path it returns a constant `QualityFunction` (slope = 0, intercept = quality), just like a CLOB offer; for single-path it returns a proper AMM quality function with a negative slope derived from the pool depth, which the path optimizer uses to find the output amount that meets the payment's requested quality limit. + +The `fixReducedOffersV2` amendment gate inside `limitIn` is a precision refinement: when active, it uses the stricter `ceil_in_strict` variant (which removes a small rounding slop) instead of `ceil_in`. The older code path is preserved for ledger replay of historical transactions. + +## `consume()` — Intentionally Thin + +`consume(view, consumed)` validates that the consumed pair does not exceed the initial offer size, sets the `consumed_` flag, and calls `ammLiquidity_.context().setAMMUsed()` to inform the outer execution context that AMM liquidity was touched in this iteration. Critically, it does **not** modify the pool balances itself. The comment in the source is explicit: actual pool updates are performed in `BookStep::consumeOffer()`, which calls `accountSend` on the AMM account. This keeps the ledger mutation in one place and avoids double-application. The `ApplyView&` parameter is accepted for interface compatibility with `TOffer::consume` but is not used. + +The `key()` method returns `std::nullopt` because AMM offers have no ledger object key — they are ephemeral, synthesized per payment iteration. + +## The `checkInvariant()` Constant-Product Guard + +After each offer execution, `BookStep` calls `checkInvariant`. The method recomputes the pre-trade pool product `k = balances_.in * balances_.out` and the post-trade product `k' = (balances_.in + consumed.in) * (balances_.out − consumed.out)`. The invariant requires `k' >= k`, which should hold exactly for a constant-product AMM. However, finite-precision arithmetic can cause `k'` to fall just below `k`, so the check also passes if the relative deviation is within `1e-7` (`withinRelativeDistance(product, newProduct, Number{1, -7})`). Violations are logged at error level with full balance and product details, enabling post-mortem analysis without aborting a ledger. + +## Fee and Transfer-Rate Handling + +`adjustRates()` always returns the output transfer rate as `QUALITY_ONE` (no transfer fee), because AMM accounts are exempt from transfer fees on payment transactions. The static `send()` method wraps `accountSend` with `WaiveTransferFee::Yes` and `AllowMPTOverflow::Yes`, reflecting the same policy: AMM funds move at face value regardless of issuer-specified transfer rates, and MPT balance overflow is permitted because the pool is expected to hold enough reserves. + +## Relationship to Surrounding Files + +`AMMOffer` is constructed exclusively by `AMMLiquidity::getOffer`, which decides the sizing strategy (Fibonacci sequence or quality-matched) and passes the resulting amounts and current pool balances. `BookStep` holds an `std::optional>>` as its tip and decides at each payment engine iteration whether AMM or CLOB liquidity is better quality. `QualityFunction` encodes the AMM's average quality as a linear function of output, enabling the path optimizer to solve for the optimal output amount in closed form rather than iterating. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/BookStep.cpp.ai.json b/src/libxrpl/tx/paths/BookStep.cpp.ai.json new file mode 100644 index 0000000000..e101b4a738 --- /dev/null +++ b/src/libxrpl/tx/paths/BookStep.cpp.ai.json @@ -0,0 +1,573 @@ +{ + "args": [ + { + "lineno": 312, + "name": "offer" + }, + { + "lineno": 312, + "name": "ofrAmt" + }, + { + "lineno": 312, + "name": "stpAmt" + }, + { + "lineno": 312, + "name": "ownerGives" + }, + { + "lineno": 312, + "name": "transferRateIn" + }, + { + "lineno": 312, + "name": "transferRateOut" + }, + { + "lineno": 312, + "name": "limit" + }, + { + "lineno": 627, + "name": "col" + }, + { + "lineno": 1012, + "name": "step" + }, + { + "lineno": 1012, + "name": "book" + }, + { + "lineno": 1042, + "name": "ctx" + }, + { + "lineno": 1042, + "name": "in" + }, + { + "lineno": 1042, + "name": "out" + } + ], + "classes": [ + { + "args": [ + "StrandContext const& ctx, Asset const& in, Asset const& out" + ], + "lineno": 18, + "name": "BookStep" + }, + { + "args": [ + "StrandContext const& ctx, Asset const& in, Asset const& out" + ], + "lineno": 202, + "name": "BookPaymentStep" + }, + { + "args": [ + "StrandContext const& ctx, Asset const& in, Asset const& out" + ], + "lineno": 246, + "name": "BookOfferCrossingStep" + } + ], + "code_paths": [ + { + "call_chain": [ + "BookStep::BookStep", + "ctx.view.read(keylet::amm(in, out))", + "ammSle->getFieldAmount(sfLPTokenBalance) != beast::zero", + "AMMLiquidity::AMMLiquidity (emplace)" + ], + "entry_point": "BookStep::BookStep (constructor)", + "purpose": "Initializes a BookStep, checks if an AMM exists for the book, and if so, validates that the AMM has a nonzero LP token balance before constructing AMMLiquidity.", + "validation_points": [ + "if (auto const ammSle = ctx.view.read(keylet::amm(in, out)); ...)", + "ammSle->getFieldAmount(sfLPTokenBalance) != beast::zero" + ] + }, + { + "call_chain": [ + "make_BookStepHelper/make_BookStepII/make_BookStepIX/make_BookStepXI/make_BookStepMM", + "BookStep::BookStep" + ], + "entry_point": "make_BookStep* functions", + "purpose": "Factory functions that create BookStep instances, triggering the constructor and thus the validation logic.", + "validation_points": [ + "BookStep::BookStep (see above)" + ] + } + ], + "data_flows": [ + { + "field": "ammSle", + "flow": [ + "ctx.view.read(keylet::amm(in, out))", + "if (ammSle && ...)", + "used to access sfLPTokenBalance and sfAccount" + ], + "origin": "ctx.view.read(keylet::amm(in, out))", + "transformations": [ + "Read from ledger as SLE::pointer", + "Checked for existence (non-null)", + "Used to extract fields" + ], + "validated_at": "BookStep::BookStep (if (auto const ammSle = ...))" + }, + { + "field": "sfLPTokenBalance", + "flow": [ + "ammSle->getFieldAmount(sfLPTokenBalance)", + "compared to beast::zero", + "if nonzero, used to construct AMMLiquidity" + ], + "origin": "ammSle->getFieldAmount(sfLPTokenBalance)", + "transformations": [ + "Extracted as Amount from SLE", + "Compared to zero" + ], + "validated_at": "BookStep::BookStep (ammSle->getFieldAmount(sfLPTokenBalance) != beast::zero)" + }, + { + "field": "AMMLiquidity", + "flow": [ + "BookStep::BookStep", + "AMMLiquidity::AMMLiquidity", + "Stored in BookStep::ammLiquidity_" + ], + "origin": "Constructed in BookStep::BookStep if AMM exists and LPTokenBalance > 0", + "transformations": [ + "Constructed with context, account, trading fee, in/out assets, etc." + ], + "validated_at": "BookStep::BookStep (after both AMM existence and LPTokenBalance > 0 checks)" + } + ], + "description": "Implements the BookStep logic for XRPL payments and offer crossing, handling order book and AMM offers, transfer fees, quality calculations, and step execution in payment/offer crossing strands.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "book_ (Book constructed from in, out, ctx.domainID)", + "validation", + "missing", + "check" + ], + "evidence": "Field book_ (Book constructed from in, out, ctx.domainID) validated by None explicit; uses C++ type system and business logic checks", + "issue_pattern": "Missing validation for book_ (Book constructed from in, out, ctx.domainID)", + "why_false_positive": "None explicit; uses C++ type system and business logic checks validates book_ (Book constructed from in, out, ctx.domainID) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "strandSrc_ (AccountID from ctx)", + "validation", + "missing", + "check" + ], + "evidence": "Field strandSrc_ (AccountID from ctx) validated by None explicit; uses C++ type system and business logic checks", + "issue_pattern": "Missing validation for strandSrc_ (AccountID from ctx)", + "why_false_positive": "None explicit; uses C++ type system and business logic checks validates strandSrc_ (AccountID from ctx) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "strandDst_ (AccountID from ctx)", + "validation", + "missing", + "check" + ], + "evidence": "Field strandDst_ (AccountID from ctx) validated by None explicit; uses C++ type system and business logic checks", + "issue_pattern": "Missing validation for strandDst_ (AccountID from ctx)", + "why_false_positive": "None explicit; uses C++ type system and business logic checks validates strandDst_ (AccountID from ctx) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "prevStep_ (pointer from ctx)", + "validation", + "missing", + "check" + ], + "evidence": "Field prevStep_ (pointer from ctx) validated by None explicit; uses C++ type system and business logic checks", + "issue_pattern": "Missing validation for prevStep_ (pointer from ctx)", + "why_false_positive": "None explicit; uses C++ type system and business logic checks validates prevStep_ (pointer from ctx) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ownerPaysTransferFee_ (bool from ctx)", + "validation", + "missing", + "check" + ], + "evidence": "Field ownerPaysTransferFee_ (bool from ctx) validated by None explicit; uses C++ type system and business logic checks", + "issue_pattern": "Missing validation for ownerPaysTransferFee_ (bool from ctx)", + "why_false_positive": "None explicit; uses C++ type system and business logic checks validates ownerPaysTransferFee_ (bool from ctx) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "j_ (Journal from ctx)", + "validation", + "missing", + "check" + ], + "evidence": "Field j_ (Journal from ctx) validated by None explicit; uses C++ type system and business logic checks", + "issue_pattern": "Missing validation for j_ (Journal from ctx)", + "why_false_positive": "None explicit; uses C++ type system and business logic checks validates j_ (Journal from ctx) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "strandDeliver_ (Asset from ctx)", + "validation", + "missing", + "check" + ], + "evidence": "Field strandDeliver_ (Asset from ctx) validated by None explicit; uses C++ type system and business logic checks", + "issue_pattern": "Missing validation for strandDeliver_ (Asset from ctx)", + "why_false_positive": "None explicit; uses C++ type system and business logic checks validates strandDeliver_ (Asset from ctx) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ammSle->getFieldAmount(sfLPTokenBalance)", + "empty", + "string", + "validation" + ], + "evidence": "comparison with beast::zero at BookStep constructor", + "issue_pattern": "Missing empty string validation for ammSle->getFieldAmount(sfLPTokenBalance)", + "why_false_positive": "comparison with beast::zero validates ammSle->getFieldAmount(sfLPTokenBalance) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ammSle", + "empty", + "string", + "validation" + ], + "evidence": "if (auto const ammSle = ctx.view.read(keylet::amm(in, out)); ...) at BookStep constructor", + "issue_pattern": "Missing empty string validation for ammSle", + "why_false_positive": "if (auto const ammSle = ctx.view.read(keylet::amm(in, out)); ...) validates ammSle for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/BookStep.cpp", + "functions": [ + { + "args": [ + "offer", + "ofrAmt", + "stpAmt", + "ownerGives", + "transferRateIn", + "transferRateOut", + "limit" + ], + "lineno": 312, + "name": "limitStepIn" + }, + { + "args": [ + "offer", + "ofrAmt", + "stpAmt", + "ownerGives", + "transferRateIn", + "transferRateOut", + "limit" + ], + "lineno": 332, + "name": "limitStepOut" + }, + { + "args": [ + "col" + ], + "lineno": 627, + "name": "sum" + }, + { + "args": [ + "step", + "book" + ], + "lineno": 1012, + "name": "equalHelper" + }, + { + "args": [ + "step", + "book" + ], + "lineno": 1018, + "name": "bookStepEqual" + }, + { + "args": [ + "ctx", + "in", + "out" + ], + "lineno": 1042, + "name": "make_BookStepHelper" + }, + { + "args": [ + "ctx", + "in", + "out" + ], + "lineno": 1057, + "name": "make_BookStepII" + }, + { + "args": [ + "ctx", + "in" + ], + "lineno": 1062, + "name": "make_BookStepIX" + }, + { + "args": [ + "ctx", + "out" + ], + "lineno": 1067, + "name": "make_BookStepXI" + }, + { + "args": [ + "ctx", + "in", + "out" + ], + "lineno": 1072, + "name": "make_BookStepMM" + }, + { + "args": [ + "ctx", + "in", + "out" + ], + "lineno": 1077, + "name": "make_BookStepMI" + }, + { + "args": [ + "ctx", + "in", + "out" + ], + "lineno": 1082, + "name": "make_BookStepIM" + }, + { + "args": [ + "ctx", + "in" + ], + "lineno": 1087, + "name": "make_BookStepMX" + }, + { + "args": [ + "ctx", + "out" + ], + "lineno": 1092, + "name": "make_BookStepXM" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + }, + { + "lineno": 1009, + "name": "test" + } + ], + "test_coverage_notes": "BookStep and its validation logic are likely tested indirectly via pathfinding, AMM, and payment integration tests. Look for tests in files such as 'test/AMM_test.cpp', 'test/Path_test.cpp', 'test/BookStep_test.cpp', or similar. Direct unit tests for BookStep constructor validation (AMM existence and LPTokenBalance > 0) may be missing or only covered via higher-level integration tests. Edge cases such as AMM present but LPTokenBalance == 0, or AMM not present, should be explicitly tested but may not be. There may be a gap in direct negative-path unit tests for these validation branches.", + "validation_architecture": { + "auto_validated_fields": [ + "book_ (Book constructed from in, out, ctx.domainID)", + "strandSrc_ (AccountID from ctx)", + "strandDst_ (AccountID from ctx)", + "prevStep_ (pointer from ctx)", + "ownerPaysTransferFee_ (bool from ctx)", + "j_ (Journal from ctx)", + "strandDeliver_ (Asset from ctx)" + ], + "framework": "None explicit; uses C++ type system and business logic checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "No exception; AMM liquidity not emplaced if validation fails", + "field": "ammSle->getFieldAmount(sfLPTokenBalance)", + "location": "BookStep constructor", + "validated_by": "comparison with beast::zero", + "validates": [ + "Checks if AMM ledger entry exists and has nonzero LP token balance before constructing AMMLiquidity" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "No exception; AMM liquidity not emplaced if validation fails", + "field": "ammSle", + "location": "BookStep constructor", + "validated_by": "if (auto const ammSle = ctx.view.read(keylet::amm(in, out)); ...)", + "validates": [ + "Checks if AMM ledger entry exists for the given in/out assets" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/BookStep.cpp.ai.md b/src/libxrpl/tx/paths/BookStep.cpp.ai.md new file mode 100644 index 0000000000..8afa07af7d --- /dev/null +++ b/src/libxrpl/tx/paths/BookStep.cpp.ai.md @@ -0,0 +1,49 @@ +## `BookStep.cpp` — Order Book and AMM Exchange Step in the Payment Engine + +### Role and Context + +`BookStep.cpp` implements the exchange step in XRPL's multi-step payment and offer-crossing engine. When a payment strand needs to convert one asset to another — IOU to XRP, MPT to IOU, or any other combination — a `BookStep` handles that conversion by consuming offers from either the Central Limit Order Book (CLOB) or from an Automated Market Maker (AMM) pool. It sits between endpoint steps (`XRPEndpointStep`, `MPTEndpointStep`, `DirectStep`) that move value in and out of accounts, and is the component responsible for actual price discovery and execution at the ledger level. + +### Template Architecture: CRTP for Zero-Cost Polymorphism + +The core class is `BookStep`, which uses the Curiously Recurring Template Pattern. `TIn` and `TOut` encode the amount types (e.g., `XRPAmount`, `IOUAmount`, `MPTAmount`), eliminating virtual dispatch and allowing compile-time specialization of the hot path. `TDerived` provides the policy hook: two concrete subclasses exist. + +`BookPaymentStep` handles regular payments. It consults offers of any quality (no threshold), always charges transfer fees by the book owner's rates, and never restricts self-crossing. `BookOfferCrossingStep` handles the offer crossing scenario (when a new offer is placed that can immediately match existing offers). It enforces a `qualityThreshold_` to stop once the market moves out of range, implements nuanced self-cross deletion logic, and adjusts transfer fee waivers for the case where the sender and receiver are the same account. + +The CRTP dispatch occurs in methods like `adjustQualityWithFees()`, `limitSelfCrossQuality()`, `checkQualityThreshold()`, `getOfrInRate()`, and `getOfrOutRate()`, each of which has a different implementation in `BookPaymentStep` vs `BookOfferCrossingStep`. All are called via `static_cast(this)->method()`, avoiding virtual call overhead in the innermost iteration loop. + +### AMM Integration + +At construction, `BookStep` checks the ledger for an AMM pool whose in/out assets match the book. If one exists **and** its `sfLPTokenBalance` is nonzero (ruling out newly bootstrapped but unfunded pools), an `AMMLiquidity` object is emplaced into `ammLiquidity_`. This optional field drives all AMM-aware paths downstream. + +The `tip()` method determines which source currently offers the best price: it peeks at the CLOB book tip via a lightweight `BookTip` read-only probe, then compares against an AMM offer generated by `getAMMOffer()`. The return type `std::optional>>` captures either a CLOB quality or a fully materialized AMM offer. This unified representation lets `qualityUpperBound()` and `getQualityFunc()` present a single quality signal upward to the strand solver regardless of the underlying liquidity source. + +AMM offers are never processed before CLOB offers at the same quality level; the AMM's `tryAMM` lambda inside `forEachOffer` runs once per iteration only when the AMM has better quality than the CLOB tip, or when there are no CLOB offers at all. AMM does not currently support domain-partitioned books (`book_.domain` check guards this). The `fixAMMv1_1` amendment gates certain quality adjustments for single-path AMM scenarios, preventing the crossing from becoming blocked by a CLOB offer at a lower quality than the AMM can actually beat. + +### Offer Iteration: `forEachOffer` + +`forEachOffer` is the central engine. It creates a `FlowOfferStream` with a `StepCounter` capped at `MaxOffersToConsume = 1000`. If that limit is reached, the step marks itself `inactive_`, signaling the strand solver to abandon this strand rather than spin indefinitely — a critical DoS protection. + +Each offer in the callback (`execOffer`) undergoes several checks before execution: +- **Authorization**: For MPT assets, the offer owner must have a valid `MPToken` and pass `requireAuth`. Offers that fail are permanently removed from the ledger even if no crossing happens (`permRmOffer`). +- **MPT DEX rules**: `checkMPTDEX()` verifies tradability and transfer constraints specific to `MPTIssue`, including the `CanTransfer` flag and frozen/locked token state. These checks vary depending on whether the previous step was another `BookStep` or an `MPTEndpointStep`. +- **Self-cross handling** (offer crossing only): `BookOfferCrossingStep::limitSelfCrossQuality` detects when alice's new offer would cross alice's own old offer. Rather than executing the cross or skipping the offer (both problematic), it deletes the old offer from the tip so subsequent offers become accessible. This is the only mechanism that allows continued crossing past a self-blocking offer. +- **Quality threshold**: Offer crossing can stop iteration early if the current offer quality falls below the crossing threshold. + +Transfer rates are computed once per `forEachOffer` invocation. The in-rate (`trIn`) is parity unless the previous step redeemed (paid into the book owner's trust line), in which case the issuer's transfer rate applies. The out-rate (`trOut`) applies when `ownerPaysTransferFee_` is set, meaning the offer owner — not the payment sender — absorbs the transfer cost. + +### Reverse and Forward Passes + +XRPL's payment engine works in two phases. `revImp` runs a backward simulation starting from the desired output, consuming offers and accumulating amounts into `savedIns`/`savedOuts` flat multisets, then summing via the `sum()` helper (which avoids floating-point rounding by accumulating typed amounts individually before summing). The result is stored in `cache_`. + +`fwdImp` re-runs forward from the actual available input. It asserts `cache_` is set (since reverse must always precede forward) and replays offer consumption. A subtle adjustment handles IOU normalization: when two IOU amounts differ by fewer than 10 in the mantissa, subtraction yields zero, making it appear an offer was fully consumed when it wasn't. The forward pass detects this and compares against the reverse cache to reconcile. + +`validFwd` provides an additional correctness check by running `fwdImp` a second time and using `checkNear` to verify the result matches what the reverse pass cached. If the amounts diverge, the strand is rejected. This defense handles cases where ledger state changes between the reverse and forward passes in pathfinding simulation. + +### `check()`: Strand Validation + +Before a strand is committed, `check()` validates structural invariants: the book cannot have the same asset on both sides, issuers must exist in the ledger, and the book's output asset must not have appeared in any previous step's output (loop detection via `seenBookOuts`). For the case where the preceding step is a `DirectStep` into a trust line, it also verifies that the trust line exists and has not set the `NoRipple` flag, and for MPT assets, that trading is permitted via `canTrade`. + +### Factory Functions and Type Dispatch + +The public API consists of nine `make_BookStep*` functions that encode the type combination in their name: `II` (IOU→IOU), `IX` (IOU→XRP), `XI`, `MM` (MPT→MPT), `MI`, `IM`, `MX`, `XM`. Each delegates to the internal `make_BookStepHelper`, which instantiates either `BookPaymentStep` or `BookOfferCrossingStep` based on `ctx.offerCrossing`, calls `check()`, and returns `{TER, std::unique_ptr}`. The absence of `XX` (XRP→XRP) is enforced by the caller — this combination has no order book. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/BookTip.cpp.ai.json b/src/libxrpl/tx/paths/BookTip.cpp.ai.json new file mode 100644 index 0000000000..8d7b030341 --- /dev/null +++ b/src/libxrpl/tx/paths/BookTip.cpp.ai.json @@ -0,0 +1,358 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "BookTip::step", + "view_.succ", + "dirFirst", + "view_.peek", + "offerDelete" + ], + "entry_point": "BookTip::step", + "purpose": "Iterates through offers in a book directory, validates and deletes offers as needed, and advances to the next offer.", + "validation_points": [ + "if (m_valid)", + "if (m_entry)", + "auto const first_page = view_.succ(m_book, m_end)", + "if (dirFirst(view_, *first_page, dir, di, m_index))", + "m_entry = view_.peek(keylet::offer(m_index))" + ] + }, + { + "call_chain": [ + "BookTip::BookTip", + "getBookBase", + "getQualityNext" + ], + "entry_point": "BookTip::BookTip", + "purpose": "Initializes BookTip with the base book index and the next quality boundary.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "m_book", + "flow": [ + "BookTip::BookTip", + "BookTip::step (used as argument to view_.succ)", + "updated in step: m_book = *first_page or --m_book" + ], + "origin": "BookTip::BookTip (getBookBase(book))", + "transformations": [ + "Initialized from getBookBase(book)", + "Updated to *first_page after directory traversal", + "Decremented (--m_book) to set up for next query" + ], + "validated_at": "Indirectly validated by view_.succ(m_book, m_end)" + }, + { + "field": "first_page", + "flow": [ + "BookTip::step", + "if (!first_page) return false", + "used as *first_page in dirFirst" + ], + "origin": "view_.succ(m_book, m_end)", + "transformations": [ + "Result of ledger directory traversal" + ], + "validated_at": "if (!first_page) return false" + }, + { + "field": "m_index", + "flow": [ + "dirFirst(view_, *first_page, dir, di, m_index)", + "used as keylet::offer(m_index) in view_.peek" + ], + "origin": "Set by dirFirst", + "transformations": [ + "Set to the index of the first offer in the directory" + ], + "validated_at": "dirFirst returns true if entry exists" + }, + { + "field": "m_entry", + "flow": [ + "Set in BookTip::step after dirFirst", + "Checked by if (m_entry)", + "Passed to offerDelete if m_valid and m_entry", + "Set to nullptr after deletion" + ], + "origin": "view_.peek(keylet::offer(m_index))", + "transformations": [ + "Set to pointer to offer SLE", + "Set to nullptr after deletion" + ], + "validated_at": "if (m_entry)" + }, + { + "field": "m_valid", + "flow": [ + "Checked at start of BookTip::step", + "Set to true after dirFirst" + ], + "origin": "Set to true after successful dirFirst", + "transformations": [ + "Boolean flag for internal state" + ], + "validated_at": "if (m_valid)" + }, + { + "field": "dir", + "flow": [ + "dirFirst(view_, *first_page, dir, di, m_index)", + "m_dir = dir->key()" + ], + "origin": "Set by dirFirst", + "transformations": [ + "Pointer to directory SLE" + ], + "validated_at": "dirFirst returns true if directory entry exists" + } + ], + "description": "Implements the BookTip class, which iterates through offers in a given order book in the XRPL ledger, advancing to the next available offer by quality.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "first_page (directory index)", + "empty", + "string", + "validation" + ], + "evidence": "view_.succ(m_book, m_end) at BookTip::step", + "issue_pattern": "Missing empty string validation for first_page (directory index)", + "why_false_positive": "view_.succ(m_book, m_end) validates first_page (directory index) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "directory entry existence", + "empty", + "string", + "validation" + ], + "evidence": "dirFirst(view_, *first_page, dir, di, m_index) at BookTip::step", + "issue_pattern": "Missing empty string validation for directory entry existence", + "why_false_positive": "dirFirst(view_, *first_page, dir, di, m_index) validates directory entry existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "m_entry (offer existence)", + "empty", + "string", + "validation" + ], + "evidence": "view_.peek(keylet::offer(m_index)) at BookTip::step", + "issue_pattern": "Missing empty string validation for m_entry (offer existence)", + "why_false_positive": "view_.peek(keylet::offer(m_index)) validates m_entry (offer existence) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "m_valid (internal state)", + "empty", + "string", + "validation" + ], + "evidence": "if (m_valid) at BookTip::step", + "issue_pattern": "Missing empty string validation for m_valid (internal state)", + "why_false_positive": "if (m_valid) validates m_valid (internal state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "m_entry (pointer validity)", + "empty", + "string", + "validation" + ], + "evidence": "if (m_entry) at BookTip::step", + "issue_pattern": "Missing empty string validation for m_entry (pointer validity)", + "why_false_positive": "if (m_entry) validates m_entry (pointer validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "m_entry (pointer validity)", + "type", + "validation", + "check" + ], + "evidence": "if (m_entry) at BookTip::step", + "issue_pattern": "Missing type validation for m_entry (pointer validity)", + "why_false_positive": "if (m_entry) validates m_entry (pointer validity) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/BookTip.cpp", + "functions": [ + { + "args": [ + "ApplyView& view", + "Book const& book" + ], + "lineno": 6, + "name": "BookTip::BookTip" + }, + { + "args": [ + "beast::Journal j" + ], + "lineno": 11, + "name": "BookTip::step" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "BookTip is a core component of XRPL pathfinding and offer traversal. Typical test coverage would be in integration and unit tests for pathfinding, offer crossing, and order book traversal. Likely test files: 'Path_test.cpp', 'BookTip_test.cpp', 'OfferStream_test.cpp', or similar in the rippled/test or xrpl/test directories. However, the code defensively handles empty directories (which 'should never happen'), and this edge case may not be explicitly tested. Direct validation of m_valid, m_entry, and directory traversal is likely covered indirectly via higher-level pathfinding and offer crossing tests, but explicit unit tests for BookTip edge cases (empty directory, invalid offers, etc.) may be missing.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom ledger view and helper functions (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "first_page (directory index)", + "location": "BookTip::step", + "validated_by": "view_.succ(m_book, m_end)", + "validates": [ + "Checks if there is a next directory page (first_page) at or worse than current quality", + "If not found, step() returns false" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (continues loop)", + "field": "directory entry existence", + "location": "BookTip::step", + "validated_by": "dirFirst(view_, *first_page, dir, di, m_index)", + "validates": [ + "Checks if the directory (first_page) contains at least one offer entry", + "If not, advances to next directory" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (m_entry may be nullptr)", + "field": "m_entry (offer existence)", + "location": "BookTip::step", + "validated_by": "view_.peek(keylet::offer(m_index))", + "validates": [ + "Checks if the offer entry exists in the ledger for the given index" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (conditional logic)", + "field": "m_valid (internal state)", + "location": "BookTip::step", + "validated_by": "if (m_valid)", + "validates": [ + "Checks if the current BookTip state is valid before attempting to delete an offer" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (conditional logic)", + "field": "m_entry (pointer validity)", + "location": "BookTip::step", + "validated_by": "if (m_entry)", + "validates": [ + "Checks if m_entry is not nullptr before calling offerDelete" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/BookTip.cpp.ai.md b/src/libxrpl/tx/paths/BookTip.cpp.ai.md new file mode 100644 index 0000000000..464b0484a3 --- /dev/null +++ b/src/libxrpl/tx/paths/BookTip.cpp.ai.md @@ -0,0 +1,46 @@ +# BookTip.cpp — Raw Order Book Cursor + +`BookTip` is a low-level, destructive cursor over a single order book in the XRPL ledger. Its sole job is to present offers one at a time, in decreasing quality order (best exchange rate first), while physically removing each offer from the ledger as iteration advances. It lives at the innermost layer of the offer-crossing pipeline and is consumed directly by `TOfferStreamBase` / `FlowOfferStream`, which sit above it and handle validity filtering, expiry, and funding checks. + +## The Ledger's Order Book Layout + +XRPL stores order book offers in directory nodes whose 256-bit keys encode the book identity in the lower bits and the exchange rate (quality) in the upper 64 bits. `getBookBase(book)` produces the minimum-quality key for a given currency pair — numerically the lowest key in the book. `getQualityNext(m_book)` produces the first key that lies outside this book's quality range, acting as a sentinel for the upper bound. + +Quality on XRPL is expressed as TakerPays/TakerGets. A *lower* numeric quality value means the taker pays less per unit — which is *better* for the taker. As a result, the book's directory keys increase numerically as quality decreases. `BookTip` traverses these keys from `getBookBase` upward toward `getQualityNext`, meaning it walks from best quality to worst quality, as documented in the header. + +## Constructor + +```cpp +BookTip::BookTip(ApplyView& view, Book const& book) + : view_(view), m_book(getBookBase(book)), m_end(getQualityNext(m_book)) +``` + +The constructor establishes two cursor sentinels: `m_book` (current scan position, initialized to the book's base) and `m_end` (exclusive upper bound). No ledger access happens here; the cursor starts in an invalid state (`m_valid = false`) until the first call to `step()`. + +## The `step()` Protocol: Delete-then-Advance + +`step()` is a "delete the previous, fetch the next" operation, which is the central design decision in this file. When called: + +1. If the cursor holds a valid, non-null `m_entry` from the previous call, it calls `offerDelete(view_, m_entry, j)` to permanently remove that offer from the ledger, its owner directory, and the book directory. The `m_valid` guard on this block ensures no deletion happens on the very first call when there is no "previous" offer. + +2. The function then enters an infinite loop probing `view_.succ(m_book, m_end)` to find the next directory page key strictly greater than `m_book` and less than `m_end`. If no such page exists, the book is exhausted and the method returns `false`. + +3. On finding a candidate page, `dirFirst()` reads the page's first entry (index `[0]` in the `sfIndexes` array). If that succeeds, the cursor captures the directory key, the offer index, the offer SLE via `view_.peek`, and the quality from `getQuality(*first_page)`. + +4. The positional update is the subtle part: `m_book` is first set to `*first_page`, then decremented by one (`--m_book`). On the *next* call to `step()`, `view_.succ(m_book, m_end)` with `m_book = *first_page - 1` will return `*first_page` again — the same directory — as long as it still has entries. After `offerDelete` removes the just-served offer from the directory, `dirFirst` will find the new head. This re-visitation continues until the directory is drained, at which point `dirFirst` fails, the code sets `m_book = *first_page` and loops around, advancing `succ` past the now-empty page to the next quality tier. + +This design cleanly handles all offers at the same quality level without needing an inner loop or a separate "next-within-tier" cursor. The single position variable `m_book` drives both re-visitation and advancement. + +## Defensive Handling of Empty Directories + +The comment inside the loop says "there should never be an empty directory but just in case." If `dirFirst` returns false for a page returned by `succ`, the code sets `m_book = *first_page` and continues the loop without touching `m_book - 1`. This causes `succ` to seek past the empty page, cleanly handling what should be an impossible state without crashing or corrupting the cursor. + +## What `BookTip` Does Not Do + +The header is explicit: `BookTip` returns all offers, including those with missing ledger entries, unfunded balances, or invalid fields. It performs no validation beyond confirming that the ledger objects exist via `view_.peek`. The surrounding `TOfferStreamBase::step()` is responsible for filtering out stale and invalid offers, and it calls `BookTip::step()` in a loop until a usable offer is found or the book is exhausted. + +Because `m_entry` is a `std::shared_ptr`, the SLE object remains alive while the caller inspects it, even though it has been removed from the ledger in a prior `step()` call. The `entry()`, `index()`, `dir()`, and `quality()` accessors on the public interface expose a snapshot of the most recently advanced-to offer, valid only between consecutive calls to `step()`. + +## Relationship to `OfferStream` + +`TOfferStreamBase` holds a `BookTip tip_` member directly. Its own `step()` calls `tip_.step()`, which advances the raw cursor and deletes the previous raw offer. The stream layer then validates the newly exposed raw offer and, if invalid, calls `tip_.step()` again via the outer stream loop. Because `BookTip::step()` always deletes `m_entry` on entry, any offer the stream layer decides to skip is cleaned up automatically on the next advancement — the stream layer does not need to call a separate delete. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/DirectStep.cpp.ai.json b/src/libxrpl/tx/paths/DirectStep.cpp.ai.json new file mode 100644 index 0000000000..0dd6f78ec4 --- /dev/null +++ b/src/libxrpl/tx/paths/DirectStep.cpp.ai.json @@ -0,0 +1,750 @@ +{ + "args": [ + { + "lineno": 16, + "name": "src_" + }, + { + "lineno": 17, + "name": "dst_" + }, + { + "lineno": 18, + "name": "currency_" + }, + { + "lineno": 21, + "name": "prevStep_" + }, + { + "lineno": 22, + "name": "isLast_" + }, + { + "lineno": 23, + "name": "j_" + } + ], + "classes": [ + { + "args": [ + "StrandContext const& ctx", + "AccountID const& src", + "AccountID const& dst", + "Currency const& c" + ], + "lineno": 13, + "name": "DirectStepI" + }, + { + "args": [ + "StrandContext const& ctx", + "AccountID const& src", + "AccountID const& dst", + "Currency const& c" + ], + "lineno": 120, + "name": "DirectIPaymentStep" + }, + { + "args": [ + "StrandContext const& ctx", + "AccountID const& src", + "AccountID const& dst", + "Currency const& c" + ], + "lineno": 153, + "name": "DirectIOfferCrossingStep" + } + ], + "code_paths": [ + { + "call_chain": [ + "DirectStepI::DirectStepI", + "StepFactory::makeStep (likely)", + "Strand construction" + ], + "entry_point": "DirectStepI constructor", + "purpose": "Constructs a DirectStepI object as part of a payment path strand, initializing src, dst, currency, prevStep, isLast, and journal.", + "validation_points": [ + "DirectStepI::DirectStepI (validates src, dst, currency, ctx.prevStep, ctx.isLast, ctx.j via type system)" + ] + }, + { + "call_chain": [ + "DirectIPaymentStep::check or DirectIOfferCrossingStep::check", + "DirectStepI::check (inherited)", + "StepChecks::check (possibly)", + "Validation logic" + ], + "entry_point": "DirectIPaymentStep::check / DirectIOfferCrossingStep::check", + "purpose": "Checks the validity of the step (e.g., account existence, trust lines, etc.) before using it in a payment path.", + "validation_points": [ + "DirectStepI::check" + ] + }, + { + "call_chain": [ + "DirectIPaymentStep::maxFlow or DirectIOfferCrossingStep::maxFlow", + "DirectStepI::maxPaymentFlow" + ], + "entry_point": "DirectIPaymentStep::maxFlow / DirectIOfferCrossingStep::maxFlow", + "purpose": "Calculates the maximum amount that can flow through this step, considering trust lines and balances.", + "validation_points": [ + "DirectStepI::maxPaymentFlow (uses validated fields)" + ] + }, + { + "call_chain": [ + "DirectStepI::revImp or fwdImp", + "DirectStepI::qualities / qualitiesSrcRedeems / qualitiesSrcIssues", + "DirectStepI::debtDirection" + ], + "entry_point": "DirectStepI::revImp / fwdImp", + "purpose": "Implements reverse and forward pass for payment pathfinding, computing actual amounts and qualities.", + "validation_points": [ + "DirectStepI::qualities* (uses validated fields)" + ] + }, + { + "call_chain": [ + "DirectStepI::validFwd", + "DirectStepI::fwdImp" + ], + "entry_point": "DirectStepI::validFwd", + "purpose": "Checks if a forward step is valid for a given input amount.", + "validation_points": [ + "DirectStepI::fwdImp" + ] + } + ], + "data_flows": [ + { + "field": "src_", + "flow": [ + "constructor argument", + "DirectStepI::src_", + "used in qualities, maxPaymentFlow, debtDirection, etc." + ], + "origin": "DirectStepI constructor (from argument src)", + "transformations": [ + "None (passed through as AccountID)" + ], + "validated_at": "DirectStepI constructor (type system)" + }, + { + "field": "dst_", + "flow": [ + "constructor argument", + "DirectStepI::dst_", + "used in qualities, maxPaymentFlow, debtDirection, etc." + ], + "origin": "DirectStepI constructor (from argument dst)", + "transformations": [ + "None (passed through as AccountID)" + ], + "validated_at": "DirectStepI constructor (type system)" + }, + { + "field": "currency_", + "flow": [ + "constructor argument", + "DirectStepI::currency_", + "used in qualities, maxPaymentFlow, etc." + ], + "origin": "DirectStepI constructor (from argument c)", + "transformations": [ + "None (passed through as Currency)" + ], + "validated_at": "DirectStepI constructor (type system)" + }, + { + "field": "prevStep_", + "flow": [ + "StrandContext", + "DirectStepI::prevStep_", + "used in qualitiesSrcIssues, etc." + ], + "origin": "StrandContext (ctx.prevStep)", + "transformations": [ + "None" + ], + "validated_at": "DirectStepI constructor (type system)" + }, + { + "field": "isLast_", + "flow": [ + "StrandContext", + "DirectStepI::isLast_", + "used in logic to determine step behavior" + ], + "origin": "StrandContext (ctx.isLast)", + "transformations": [ + "None" + ], + "validated_at": "DirectStepI constructor (type system)" + }, + { + "field": "cache_", + "flow": [ + "revImp/fwdImp compute values", + "cache_ set to Cache{in, srcToDst, out, srcDebtDir}", + "cachedIn/cachedOut access cache_" + ], + "origin": "DirectStepI::Cache struct, set in revImp/fwdImp", + "transformations": [ + "Set to std::optional after computation" + ], + "validated_at": "std::optional enforces presence/absence" + }, + { + "field": "IOUAmount in/out/srcToDst", + "flow": [ + "revImp/fwdImp", + "Cache struct", + "cachedIn/cachedOut" + ], + "origin": "Computed in revImp/fwdImp", + "transformations": [ + "Amounts are computed based on ledger state, qualities, and direction" + ], + "validated_at": "revImp/fwdImp (indirectly, via logic and type system)" + } + ], + "description": "Implements direct IOU transfer steps for XRPL payment and offer crossing paths, including logic for quality, trust line checks, liquidity calculation, and step validation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "src", + "validation", + "missing", + "check" + ], + "evidence": "Field src validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for src", + "why_false_positive": "C++ type system, std::optional validates src automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "dst", + "validation", + "missing", + "check" + ], + "evidence": "Field dst validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for dst", + "why_false_positive": "C++ type system, std::optional validates dst automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "currency", + "validation", + "missing", + "check" + ], + "evidence": "Field currency validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for currency", + "why_false_positive": "C++ type system, std::optional validates currency automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "prevStep_", + "validation", + "missing", + "check" + ], + "evidence": "Field prevStep_ validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for prevStep_", + "why_false_positive": "C++ type system, std::optional validates prevStep_ automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "isLast_", + "validation", + "missing", + "check" + ], + "evidence": "Field isLast_ validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for isLast_", + "why_false_positive": "C++ type system, std::optional validates isLast_ automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "j_", + "validation", + "missing", + "check" + ], + "evidence": "Field j_ validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for j_", + "why_false_positive": "C++ type system, std::optional validates j_ automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "cache_", + "validation", + "missing", + "check" + ], + "evidence": "Field cache_ validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for cache_", + "why_false_positive": "C++ type system, std::optional validates cache_ automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "src, dst, currency", + "empty", + "string", + "validation" + ], + "evidence": "DirectStepI constructor (type system) at DirectStepI::DirectStepI", + "issue_pattern": "Missing empty string validation for src, dst, currency", + "why_false_positive": "DirectStepI constructor (type system) validates src, dst, currency for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "src, dst, currency", + "type", + "validation", + "check" + ], + "evidence": "DirectStepI constructor (type system) at DirectStepI::DirectStepI", + "issue_pattern": "Missing type validation for src, dst, currency", + "why_false_positive": "DirectStepI constructor (type system) validates src, dst, currency type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ctx.prevStep, ctx.isLast, ctx.j", + "empty", + "string", + "validation" + ], + "evidence": "DirectStepI constructor (type system) at DirectStepI::DirectStepI", + "issue_pattern": "Missing empty string validation for ctx.prevStep, ctx.isLast, ctx.j", + "why_false_positive": "DirectStepI constructor (type system) validates ctx.prevStep, ctx.isLast, ctx.j for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ctx.prevStep, ctx.isLast, ctx.j", + "type", + "validation", + "check" + ], + "evidence": "DirectStepI constructor (type system) at DirectStepI::DirectStepI", + "issue_pattern": "Missing type validation for ctx.prevStep, ctx.isLast, ctx.j", + "why_false_positive": "DirectStepI constructor (type system) validates ctx.prevStep, ctx.isLast, ctx.j type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "cache_", + "empty", + "string", + "validation" + ], + "evidence": "std::optional (type system) at cache_ member and usage in cachedIn/cachedOut", + "issue_pattern": "Missing empty string validation for cache_", + "why_false_positive": "std::optional (type system) validates cache_ for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/DirectStep.cpp", + "functions": [ + { + "args": [ + "ReadView const& sb", + "QualityDirection qDir" + ], + "lineno": 181, + "name": "DirectIPaymentStep::quality" + }, + { + "args": [ + "ReadView const&", + "QualityDirection qDir" + ], + "lineno": 210, + "name": "DirectIOfferCrossingStep::quality" + }, + { + "args": [ + "ReadView const& sb", + "IOUAmount const&" + ], + "lineno": 215, + "name": "DirectIPaymentStep::maxFlow" + }, + { + "args": [ + "ReadView const& sb", + "IOUAmount const& desired" + ], + "lineno": 220, + "name": "DirectIOfferCrossingStep::maxFlow" + }, + { + "args": [ + "StrandContext const& ctx", + "std::shared_ptr const& sleSrc" + ], + "lineno": 246, + "name": "DirectIPaymentStep::check" + }, + { + "args": [ + "StrandContext const&", + "std::shared_ptr const&" + ], + "lineno": 292, + "name": "DirectIOfferCrossingStep::check" + }, + { + "args": [ + "ReadView const& sb" + ], + "lineno": 304, + "name": "DirectStepI::maxPaymentFlow" + }, + { + "args": [ + "ReadView const& sb", + "StrandDirection dir" + ], + "lineno": 314, + "name": "DirectStepI::debtDirection" + }, + { + "args": [ + "PaymentSandbox& sb", + "ApplyView& /*afView*/", + "boost::container::flat_set& /*ofrsToRm*/", + "IOUAmount const& out" + ], + "lineno": 322, + "name": "DirectStepI::revImp" + }, + { + "args": [ + "IOUAmount const& fwdIn", + "IOUAmount const& fwdSrcToDst", + "IOUAmount const& fwdOut", + "DebtDirection srcDebtDir" + ], + "lineno": 370, + "name": "DirectStepI::setCacheLimiting" + }, + { + "args": [ + "PaymentSandbox& sb", + "ApplyView& /*afView*/", + "boost::container::flat_set& /*ofrsToRm*/", + "IOUAmount const& in" + ], + "lineno": 399, + "name": "DirectStepI::fwdImp" + }, + { + "args": [ + "PaymentSandbox& sb", + "ApplyView& afView", + "EitherAmount const& in" + ], + "lineno": 438, + "name": "DirectStepI::validFwd" + }, + { + "args": [ + "ReadView const& sb" + ], + "lineno": 474, + "name": "DirectStepI::qualitiesSrcRedeems" + }, + { + "args": [ + "ReadView const& sb", + "DebtDirection prevStepDebtDirection" + ], + "lineno": 486, + "name": "DirectStepI::qualitiesSrcIssues" + }, + { + "args": [ + "ReadView const& sb", + "DebtDirection srcDebtDir", + "StrandDirection strandDir" + ], + "lineno": 504, + "name": "DirectStepI::qualities" + }, + { + "args": [ + "ReadView const& v" + ], + "lineno": 515, + "name": "DirectStepI::lineQualityIn" + }, + { + "args": [ + "ReadView const& v", + "DebtDirection prevStepDir" + ], + "lineno": 520, + "name": "DirectStepI::qualityUpperBound" + }, + { + "args": [ + "StrandContext const& ctx" + ], + "lineno": 535, + "name": "DirectStepI::check" + }, + { + "args": [ + "Step const& step", + "AccountID const& src", + "AccountID const& dst", + "Currency const& currency" + ], + "lineno": 601, + "name": "test::directStepEqual" + }, + { + "args": [ + "StrandContext const& ctx", + "AccountID const& src", + "AccountID const& dst", + "Currency const& c" + ], + "lineno": 613, + "name": "make_DirectStepI" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 599, + "name": "test" + } + ], + "test_coverage_notes": "DirectStepI is a template base for DirectIPaymentStep and DirectIOfferCrossingStep. Tests for these are typically found in 'src/test/app/paths/' such as 'DirectStep_test.cpp', 'Strand_test.cpp', 'PathCursor_test.cpp', and possibly 'OfferCrossing_test.cpp'. These tests cover construction, validation, and flow logic. However, coverage gaps may exist for edge cases (e.g., malformed input, rare trust line states, or exception paths). The constructor's type-based validation is not directly tested for invalid types (as C++ type system enforces this at compile time), but runtime validation (e.g., for account existence or trust line state) is tested in the derived classes' tests. There may be limited or no tests for error handling via exceptions in this specific file.", + "validation_architecture": { + "auto_validated_fields": [ + "src", + "dst", + "currency", + "prevStep_", + "isLast_", + "j_", + "cache_" + ], + "framework": "C++ type system, std::optional", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "n/a (compile-time type enforcement)", + "field": "src, dst, currency", + "location": "DirectStepI::DirectStepI", + "validated_by": "DirectStepI constructor (type system)", + "validates": [ + "src is AccountID", + "dst is AccountID", + "currency is Currency" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (compile-time type enforcement)", + "field": "ctx.prevStep, ctx.isLast, ctx.j", + "location": "DirectStepI::DirectStepI", + "validated_by": "DirectStepI constructor (type system)", + "validates": [ + "prevStep_ is Step const*", + "isLast_ is bool", + "j_ is beast::Journal const" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (optional checked at runtime, no exception thrown)", + "field": "cache_", + "location": "cache_ member and usage in cachedIn/cachedOut", + "validated_by": "std::optional (type system)", + "validates": [ + "cache_ is present before access" + ], + "validation_type": "type/presence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/DirectStep.cpp.ai.md b/src/libxrpl/tx/paths/DirectStep.cpp.ai.md new file mode 100644 index 0000000000..c126c683bd --- /dev/null +++ b/src/libxrpl/tx/paths/DirectStep.cpp.ai.md @@ -0,0 +1,59 @@ +# `DirectStep.cpp` — IOU-to-IOU Direct Transfer Step + +## Role in the System + +The XRPL payment engine decomposes a payment into a *strand* — an ordered sequence of `Step` objects, each representing one segment of value flow. `DirectStep.cpp` implements the **direct IOU transfer step**: the case where two accounts that share a trust line exchange the same-currency IOU without routing through an offer book. This is the fundamental rippling operation of the XRPL ledger. + +The file lives alongside `BookStep.cpp`, `XRPEndpointStep.cpp`, and `MPTEndpointStep.cpp` in the paths engine. Together these cover every possible segment type in a payment or offer-crossing strand. `DirectStep.cpp` is specifically the IOU↔IOU path between gateway accounts. + +## Class Hierarchy + +The design applies **CRTP** (Curiously Recurring Template Pattern) to share the bulk of the logic while allowing two divergent behaviours — one for ordinary payments and one for offer crossing — to override specific policy points without virtual dispatch overhead. + +`DirectStepI` extends `StepImp>`, which is itself an adapter that bridges the type-erased `Step` interface (accepting and returning `EitherAmount` unions) to typed `revImp` / `fwdImp` methods operating on `IOUAmount` directly. The two concrete subtypes are: + +- **`DirectIPaymentStep`**: used for payment transactions. It reads trust-line quality fields, requires a pre-existing trust line, and enforces authorization and dry-path limits. +- **`DirectIOfferCrossingStep`**: used when an offer crosses. It ignores trust-line quality fields entirely ("a long-standing tradition"), does not require a pre-existing trust line, and can exceed the trust-line limit on the final step of the strand. + +## Debt Direction: The Core Bookkeeping Concept + +A central concept throughout is `DebtDirection`. When account A holds a positive balance on a trust line with account B, A **redeems** — it holds IOUs issued by B and is sending value back toward the issuer. When A holds a negative balance (owes B), A **issues** — it is creating new IOU obligations. + +This matters for transfer fees: a transfer fee is charged when the issuer sends their own currency (issues). It is *not* charged when a holder returns IOUs to the issuer (redeems). `debtDirection()` determines this by calling `accountHolds()`: a positive signum means `redeems`, otherwise `issues`. The forward pass can read the direction from the cache when available, saving the ledger lookup. + +## Reverse and Forward Passes + +The pathfinding engine runs two passes per candidate strand: a **reverse pass** (working backward from the desired output) and a **forward pass** (working forward from the available input). Each step implements `revImp` and `fwdImp` accordingly. + +**`revImp`** receives a requested `out` amount and must compute how much `in` is required. It calls the CRTP-derived `maxFlow()` to find the maximum that can flow given current ledger state, then calls `qualities()` to get `srcQOut` (the outgoing quality multiplier on the source side) and `dstQIn` (the incoming quality multiplier on the destination side). The intermediate value `srcToDst` is the amount that actually moves on the trust line — it can differ from `out` if `dstQIn` applies a discount. The result is stored in `cache_`. + +**`fwdImp`** receives an actual `in` amount and recomputes the flow, consulting `cache_->srcToDst` as the reference for `maxFlow()`. The twist is that rounding in fixed-point arithmetic can cause the forward pass to derive *slightly larger* amounts than the reverse pass established. The `setCacheLimiting()` function reconciles the two: it takes the minimum of the forward-computed values and the cached values, preserving the invariant that the forward pass never delivers more liquidity than the reverse pass authorised. If the discrepancy exceeds a 1% mantissa ratio threshold, the function logs a warning and accepts the forward values rather than silently clamping them — a defensive choice that prioritises visibility of unexpected behaviour over silent correction. + +## Quality Computation + +The `qualities()` dispatch method routes to one of two implementations depending on debt direction: + +- **`qualitiesSrcRedeems()`**: When the source redeems, `srcQOut` is the max of the trust-line quality-out and the previous step's `lineQualityIn`. This handles the case where the prior step's inbound quality is worse than what the current step's trust line advertises, taking the more conservative figure. `dstQIn` is always `QUALITY_ONE` here. +- **`qualitiesSrcIssues()`**: When the source issues, `srcQOut` becomes the **transfer rate** of the source account if the previous step redeemed (meaning value is transitioning from a redeem step to an issue step, which triggers the transfer fee). `dstQIn` is the trust-line quality-in of the destination, capped at `QUALITY_ONE` on the last step to avoid over-charging the final recipient. + +For `DirectIOfferCrossingStep`, the `quality()` override returns `QUALITY_ONE` unconditionally. This is a protocol-level decision: offer quality fields on trust lines are irrelevant during offer crossing. + +## Validation and the Two-Phase Check + +`check()` operates in two layers. The base `DirectStepI::check()` handles common constraints applicable to both payments and offer crossing: + +- Both accounts must be non-null and distinct. +- The source account must exist in the ledger. +- Freeze constraints are checked (with a special carve-out: a single-hop path that is both `isFirst` and `isLast` cannot be frozen, since it represents pure self-issue/redemption). +- When the previous step was also a `DirectStep`, `checkNoRipple` is invoked to enforce the NoRipple flag. An account that sets NoRipple on both sides of a trust line cannot be traversed as an intermediate node. +- Loop detection uses `seenDirectAssets`, a two-element array of flat sets (one for src issues, one for dst issues). The two slots permit the same account to appear as both the source in one step and the destination in another — legitimate in a two-hop path — but not more. + +The CRTP-dispatched `check(ctx, sleSrc)` then adds context-specific checks. For payments, `DirectIPaymentStep::check()` verifies the trust line exists (`terNO_LINE`), checks `lsfRequireAuth` against the trust-line auth flag, enforces the NoRipple flag when the previous step was a book step, and confirms the path isn't dry (destination balance at limit). For offer crossing, the override is a no-op: none of these constraints apply. + +## Offer Crossing: Intentional Relaxation of Rules + +`DirectIOfferCrossingStep::maxFlow()` with `isLast_` set returns `{desired, DebtDirection::issues}` unconditionally, entirely bypassing `maxPaymentFlow()`. This allows an offer crossing to deliver the full desired amount even if it exceeds the trust-line limit — the assumption being that creating an offer signals willingness to receive IOUs at any balance. The `verifyPrevStepDebtDirection()` assert documents an important structural invariant: when a direct step follows a book step during offer crossing, the book step always *issues* (never redeems), and the assert flags any future deviation in that behaviour. + +## Factory and Test Interface + +`make_DirectStepI()` is the factory entry point called by the strand builder (`toStrand()`). It reads `ctx.offerCrossing` to select the correct concrete type, constructs the step, runs `check()`, and returns the step polymorphically as `std::unique_ptr`. The `test::directStepEqual()` function in the `test` namespace exposes an introspection hook that downcasts to `DirectStepI` for unit test assertions — a deliberate test-only seam that avoids adding virtual methods to the production interface. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/Flow.cpp.ai.json b/src/libxrpl/tx/paths/Flow.cpp.ai.json new file mode 100644 index 0000000000..ae800cce29 --- /dev/null +++ b/src/libxrpl/tx/paths/Flow.cpp.ai.json @@ -0,0 +1,301 @@ +{ + "args": [ + { + "lineno": 12, + "name": "sb" + }, + { + "lineno": 12, + "name": "srcAsset" + }, + { + "lineno": 12, + "name": "dstAsset" + }, + { + "lineno": 12, + "name": "f" + }, + { + "lineno": 30, + "name": "sb" + }, + { + "lineno": 31, + "name": "deliver" + }, + { + "lineno": 32, + "name": "src" + }, + { + "lineno": 33, + "name": "dst" + }, + { + "lineno": 34, + "name": "paths" + }, + { + "lineno": 35, + "name": "defaultPaths" + }, + { + "lineno": 36, + "name": "partialPayment" + }, + { + "lineno": 37, + "name": "ownerPaysTransferFee" + }, + { + "lineno": 38, + "name": "offerCrossing" + }, + { + "lineno": 39, + "name": "limitQuality" + }, + { + "lineno": 40, + "name": "sendMax" + }, + { + "lineno": 41, + "name": "domainID" + }, + { + "lineno": 42, + "name": "j" + }, + { + "lineno": 43, + "name": "flowDebugInfo" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "flow", + "toStrands", + "isTesSuccess (on toStrandsTer)", + "finishFlow" + ], + "entry_point": "flow", + "purpose": "Processes a payment path, validates the path construction, and executes the payment flow if valid.", + "validation_points": [ + "isTesSuccess(toStrandsTer) - Validates if path construction succeeded before proceeding" + ] + }, + { + "call_chain": [ + "flow", + "sendMax (std::optional check)" + ], + "entry_point": "flow", + "purpose": "Determines the source asset for the payment, validating if sendMax is provided.", + "validation_points": [ + "if (sendMax) - Validates presence of sendMax before using" + ] + }, + { + "call_chain": [ + "flow", + "std::visit", + "finishFlow" + ], + "entry_point": "flow", + "purpose": "Dispatches to the correct flow template based on asset types, then finalizes the flow result.", + "validation_points": [ + "isTesSuccess(f.ter) in finishFlow - Validates transaction result before applying changes" + ] + } + ], + "data_flows": [ + { + "field": "toStrandsTer", + "flow": [ + "toStrands() returns (toStrandsTer, strands)", + "isTesSuccess(toStrandsTer) checked", + "If not success, result.setResult(toStrandsTer) and return" + ], + "origin": "Result of toStrands() in flow", + "transformations": [ + "Directly checked for success/failure" + ], + "validated_at": "Immediately after toStrands() call in flow" + }, + { + "field": "sendMax", + "flow": [ + "flow parameter", + "if (sendMax) check", + "sendMax->asset() used for srcAsset and sendMaxAsset" + ], + "origin": "Input parameter to flow (std::optional)", + "transformations": [ + "Checked for presence (optional)", + "Converted to Asset via sendMax->asset()" + ], + "validated_at": "if (sendMax) check in flow" + }, + { + "field": "strands", + "flow": [ + "toStrands() returns strands", + "Used for AMMContext.setMultiPath(strands.size() > 1)", + "Passed to flow()" + ], + "origin": "Result of toStrands()", + "transformations": [ + "Counted for multi-path check", + "Passed as-is to flow template" + ], + "validated_at": "Indirectly validated by isTesSuccess(toStrandsTer)" + }, + { + "field": "f.ter", + "flow": [ + "Returned from flow()", + "Checked by isTesSuccess(f.ter) in finishFlow", + "Used to set result.setResult(f.ter)" + ], + "origin": "Result field from flow()", + "transformations": [ + "Checked for success/failure", + "Copied to output" + ], + "validated_at": "isTesSuccess(f.ter) in finishFlow" + } + ], + "description": "Implements the main flow logic for XRPL payments, including pathfinding, strand construction, and payment execution, supporting multiple asset types and AMM context.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "toStrandsTer (result of toStrands)", + "empty", + "string", + "validation" + ], + "evidence": "isTesSuccess at flow", + "issue_pattern": "Missing empty string validation for toStrandsTer (result of toStrands)", + "why_false_positive": "isTesSuccess validates toStrandsTer (result of toStrands) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sendMax", + "empty", + "string", + "validation" + ], + "evidence": "std::optional check at flow", + "issue_pattern": "Missing empty string validation for sendMax", + "why_false_positive": "std::optional check validates sendMax for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sendMax", + "type", + "validation", + "check" + ], + "evidence": "std::optional check at flow", + "issue_pattern": "Missing type validation for sendMax", + "why_false_positive": "std::optional check validates sendMax type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/Flow.cpp", + "functions": [ + { + "args": [ + "PaymentSandbox& sb", + "Asset const& srcAsset", + "Asset const& dstAsset", + "FlowResult&& f" + ], + "lineno": 11, + "name": "finishFlow" + }, + { + "args": [ + "PaymentSandbox& sb", + "STAmount const& deliver", + "AccountID const& src", + "AccountID const& dst", + "STPathSet const& paths", + "bool defaultPaths", + "bool partialPayment", + "bool ownerPaysTransferFee", + "OfferCrossing offerCrossing", + "std::optional const& limitQuality", + "std::optional const& sendMax", + "std::optional const& domainID", + "beast::Journal j", + "path::detail::FlowDebugInfo* flowDebugInfo" + ], + "lineno": 29, + "name": "flow" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The core validation paths (toStrandsTer via isTesSuccess, sendMax optional check) are likely covered by integration and unit tests for payment pathfinding and payment execution. Typical test files would be in the rippled repo under 'src/test/app/Flow_test.cpp', 'src/test/app/Payment_test.cpp', or 'src/test/paths/Path_test.cpp'. However, edge cases such as malformed paths, missing sendMax, or unusual asset types may not be exhaustively tested. There is no evidence in this file of explicit unit tests for the validation logic itself; coverage depends on higher-level payment and path tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (xrpl business logic, no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "path::RippleCalc::Output with error ter", + "field": "toStrandsTer (result of toStrands)", + "location": "flow", + "validated_by": "isTesSuccess", + "validates": [ + "Checks if the result of toStrands is a tesSUCCESS code", + "If not, returns early with error" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "N/A (optional, so no error)", + "field": "sendMax", + "location": "flow", + "validated_by": "std::optional check", + "validates": [ + "Checks if sendMax is present before using" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/Flow.cpp.ai.md b/src/libxrpl/tx/paths/Flow.cpp.ai.md new file mode 100644 index 0000000000..1d8a2b4433 --- /dev/null +++ b/src/libxrpl/tx/paths/Flow.cpp.ai.md @@ -0,0 +1,38 @@ +# `src/libxrpl/tx/paths/Flow.cpp` + +## Role in the System + +`Flow.cpp` is the public entry point for XRPL's payment execution engine. It bridges the high-level transaction processor — which speaks in terms of `STAmount`, `AccountID`, and `STPathSet` — into the fully type-parameterized, template-driven machinery in `StrandFlow.h`. The file is intentionally small: its sole job is to resolve runtime asset types into compile-time template parameters and hand off to the inner engine. All the iterative liquidity-seeking, offer consumption, and sandbox management lives elsewhere. + +## The Two Functions + +### `finishFlow` (file-local template) + +`finishFlow` is a generic cleanup helper that translates a `FlowResult` into the public `path::RippleCalc::Output` type. The critical decision it encodes is the sandbox commit policy: if the inner execution succeeded (`isTesSuccess(f.ter)`), the speculative `PaymentSandbox` is applied to the caller's sandbox, making the ledger mutations permanent in the transaction scope. On failure, the sandbox is simply abandoned — the changes evaporate — but any `removableOffers` discovered during the attempt are still passed back to the caller. This matters for offer crossing, where expired or unfunded offers discovered during a failed payment still need to be cleaned from the ledger. The amounts `in` and `out` are also materialized back to `STAmount` via `toSTAmount()` regardless of success or failure. + +### `flow` (the public function) + +The public `flow()` function is declared in `Flow.h` and is called by `RippleCalc::rippleCalculate()`. It orchestrates the full payment lifecycle through four well-defined phases: + +**Phase 1 — Source asset resolution.** The source asset cannot always be inferred from the delivery asset alone. If `sendMax` is present, the source currency is whatever the sender is spending, which may differ from the delivery currency (cross-currency payment). If `sendMax` is absent, the source asset is derived from `deliver`: XRP stays XRP, IOU becomes an `Issue` with `src` as the issuer (defaulting to the sender's own trust line), and MPT assets pass through unchanged. This logic handles the asymmetry that XRP has no issuer, while IOU issuance must be anchored to a specific account. + +**Phase 2 — Strand construction.** `toStrands()` converts the user-supplied `STPathSet` (and the optional default path) into a `std::vector`, where each `Strand` is a `std::vector>`. Steps are polymorphic objects representing one hop in the payment path — account-to-account rippling (`DirectStepI`), order book crossings (`BookStepII`, `BookStepIX`, `BookStepXI`), or XRP/MPT endpoints. If strand construction fails — invalid path, missing trust lines, etc. — `flow` returns immediately with the error TER and zero amounts. No computation is wasted. + +**Phase 3 — AMM context initialization.** An `AMMContext` is constructed here and will be passed all the way down to `AMMLiquidity` during order book execution. The context is initialized to `multiPath=false`, then updated with `ammContext.setMultiPath(strands.size() > 1)` once the strand count is known. This matters because the AMM quality function optimization (which can solve for the exact output amount that achieves a target quality) is only valid for single-path payments; multi-path payments disable it. The `AMMContext` also caps total AMM iterations at 30 (`MaxIterations`) to prevent unbounded computation, since AMM offers are not counted in the standard offer counter. + +**Phase 4 — Template dispatch via `std::visit`.** The inner `flow` function in `StrandFlow.h` is a template parameterized on amount types (`XRPAmount`, `IOUAmount`, `MPTAmount`). This design avoids virtual dispatch and tagged-union overhead on every step of the inner execution loop. However, the public API operates on runtime `STAmount`/`Asset` values. The bridge is `std::visit` on the variant returned by `srcAsset.getAmountType()` and `dstAsset.getAmountType()`. The lambda extracts `TIn_` and `TOut_` from the variant's active type, then calls the typed overload. After execution, `finishFlow` converts the typed result back to the `RippleCalc::Output` struct. + +## Design Rationale + +The file-level separation of concerns is intentional: `Flow.cpp` handles the type-erased public contract and one-time setup; `StrandFlow.h` owns the iterative algorithm. This means the payment loop in `StrandFlow.h` can be fully optimized by the compiler for each combination of `(TIn, TOut)` — six combinations for the three asset types — without any runtime branching inside the hot loop. + +The `sendMax` optionality is handled carefully. It must be converted to the same numeric type as `TInAmt` before being passed to the inner engine, and the conversion is guarded against negative values (a zero or negative `sendMax` leaves `remainingIn` as `nullopt`, effectively unconstrained). The `std::optional domainID` parameter threads through to `toStrands` to support permissioned payment domains, where liquidity is restricted to offers within a specific domain. + +## Relationship to `StrandFlow.h` + +`StrandFlow.h` contains the two templated overloads of `flow` that `Flow.cpp` calls indirectly: + +- A **per-strand** `flow(baseView, strand, maxIn, out, j)` that executes a single payment strand using reverse-then-forward passes: first walking backward through the strand to find the required input, detecting the limiting step, then walking forward from the limiter to finalize amounts. +- A **multi-strand** `flow(baseView, strands, outReq, ...)` that manages `ActiveStrands` — a priority queue sorted by theoretical quality upper bound — iterating until `remainingOut` is satisfied or all strands are exhausted. This outer loop caps at 1000 iterations and 1500 offers considered to bound worst-case execution time. + +`Flow.cpp` is the façade that makes this template machinery accessible through a single, unambiguous non-template function signature. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp.ai.json b/src/libxrpl/tx/paths/MPTEndpointStep.cpp.ai.json new file mode 100644 index 0000000000..9714fe841d --- /dev/null +++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp.ai.json @@ -0,0 +1,378 @@ +{ + "args": [ + { + "lineno": 15, + "name": "ctx" + }, + { + "lineno": 16, + "name": "src" + }, + { + "lineno": 17, + "name": "dst" + }, + { + "lineno": 18, + "name": "mpt" + }, + { + "lineno": 32, + "name": "in_" + }, + { + "lineno": 33, + "name": "srcToDst_" + }, + { + "lineno": 34, + "name": "out_" + }, + { + "lineno": 35, + "name": "srcDebtDir_" + } + ], + "classes": [ + { + "args": [ + "StrandContext const& ctx", + "AccountID const& src", + "AccountID const& dst", + "MPTID const& mpt" + ], + "lineno": 13, + "name": "MPTEndpointStep" + }, + { + "args": [ + "StrandContext const& ctx", + "AccountID const& src", + "AccountID const& dst", + "MPTID const& mpt" + ], + "lineno": 168, + "name": "MPTEndpointPaymentStep" + }, + { + "args": [ + "StrandContext const& ctx", + "AccountID const& src", + "AccountID const& dst", + "MPTID const& mpt" + ], + "lineno": 198, + "name": "MPTEndpointOfferCrossingStep" + } + ], + "code_paths": [ + { + "call_chain": [ + "make_MPTEndpointStep", + "MPTEndpointStep::MPTEndpointStep (constructor)", + "XRPL_ASSERT (src_ and dst_ validation)" + ], + "entry_point": "make_MPTEndpointStep", + "purpose": "Creates an MPTEndpointStep instance, initializing source, destination, and MPT issue. Validates that either src or dst is the issuer.", + "validation_points": [ + "MPTEndpointStep::MPTEndpointStep (constructor): XRPL_ASSERT(src_ == mptIssue_.getIssuer() || dst_ == mptIssue_.getIssuer())" + ] + }, + { + "call_chain": [ + "MPTEndpointPaymentStep::check", + "MPTEndpointStep::src() / dst() / mptID() / other accessors" + ], + "entry_point": "MPTEndpointPaymentStep::check", + "purpose": "Checks the validity of the payment step, likely using the validated fields.", + "validation_points": [ + "Relies on constructor validation (no additional validation in check itself)" + ] + }, + { + "call_chain": [ + "MPTEndpointOfferCrossingStep::check", + "MPTEndpointStep::src() / dst() / mptID() / other accessors" + ], + "entry_point": "MPTEndpointOfferCrossingStep::check", + "purpose": "Checks the validity of the offer crossing step, using the validated fields.", + "validation_points": [ + "Relies on constructor validation (no additional validation in check itself)" + ] + }, + { + "call_chain": [ + "MPTEndpointOfferCrossingStep::checkCreateMPT", + "MPTEndpointStep::src() / dst() / mptID() / other accessors" + ], + "entry_point": "MPTEndpointOfferCrossingStep::checkCreateMPT", + "purpose": "Checks and possibly creates an MPT endpoint during offer crossing.", + "validation_points": [ + "Relies on constructor validation (no additional validation in check itself)" + ] + } + ], + "data_flows": [ + { + "field": "src_", + "flow": [ + "make_MPTEndpointStep (or similar)", + "MPTEndpointStep::MPTEndpointStep (constructor)", + "stored in src_", + "used in accessors (src(), directStepSrcAcct(), directStepAccts())", + "used in logic (e.g., isDirectBetweenHolders_, validation, etc.)" + ], + "origin": "Passed as 'src' argument to MPTEndpointStep constructor (from make_MPTEndpointStep or similar factory)", + "transformations": [ + "None (direct assignment and usage)" + ], + "validated_at": "MPTEndpointStep::MPTEndpointStep (constructor) via XRPL_ASSERT" + }, + { + "field": "dst_", + "flow": [ + "make_MPTEndpointStep (or similar)", + "MPTEndpointStep::MPTEndpointStep (constructor)", + "stored in dst_", + "used in accessors (dst(), directStepAccts())", + "used in logic (e.g., isDirectBetweenHolders_, validation, etc.)" + ], + "origin": "Passed as 'dst' argument to MPTEndpointStep constructor (from make_MPTEndpointStep or similar factory)", + "transformations": [ + "None (direct assignment and usage)" + ], + "validated_at": "MPTEndpointStep::MPTEndpointStep (constructor) via XRPL_ASSERT" + }, + { + "field": "mptIssue_", + "flow": [ + "make_MPTEndpointStep (or similar)", + "MPTEndpointStep::MPTEndpointStep (constructor)", + "stored in mptIssue_", + "used in isDirectBetweenHolders_ logic", + "used in accessors (mptID())" + ], + "origin": "Constructed from 'mpt' argument in MPTEndpointStep constructor", + "transformations": [ + "None (direct assignment and usage)" + ], + "validated_at": "Indirectly validated as part of XRPL_ASSERT (since src_ or dst_ must match mptIssue_.getIssuer())" + }, + { + "field": "cache_", + "flow": [ + "Initially std::nullopt", + "Set by internal logic (e.g., after calculation)", + "Read by cachedIn(), cachedOut()" + ], + "origin": "Set internally by MPTEndpointStep methods (not shown in provided code)", + "transformations": [ + "Set to Cache struct with calculated values" + ], + "validated_at": "Not directly validated; assumed correct by internal logic" + } + ], + "description": "Implements MPTEndpointStep and its derivatives for handling Multi-Party Token (MPT) endpoints in payment and offer crossing flows in the XRPL transaction pathfinding logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "src_ and dst_ (source and destination AccountID)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at MPTEndpointStep constructor", + "issue_pattern": "Missing empty string validation for src_ and dst_ (source and destination AccountID)", + "why_false_positive": "XRPL_ASSERT macro validates src_ and dst_ (source and destination AccountID) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/MPTEndpointStep.cpp", + "functions": [ + { + "args": [ + "StrandContext const& ctx", + "std::shared_ptr const& sleSrc" + ], + "lineno": 222, + "name": "MPTEndpointPaymentStep::check" + }, + { + "args": [ + "StrandContext const& ctx", + "std::shared_ptr const&" + ], + "lineno": 273, + "name": "MPTEndpointOfferCrossingStep::check" + }, + { + "args": [ + "ApplyView& view", + "xrpl::DebtDirection srcDebtDir" + ], + "lineno": 277, + "name": "MPTEndpointOfferCrossingStep::checkCreateMPT" + }, + { + "args": [ + "StrandContext const& ctx", + "AccountID const& src", + "AccountID const& dst", + "MPTID const& mpt" + ], + "lineno": 563, + "name": "make_MPTEndpointStep" + }, + { + "args": [ + "Step const& step", + "AccountID const& src", + "AccountID const& dst", + "MPTID const& mptid" + ], + "lineno": 587, + "name": "test::mptEndpointStepEqual" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + }, + { + "lineno": 585, + "name": "test" + } + ], + "test_coverage_notes": "The code provides a test utility function test::mptEndpointStepEqual, suggesting some unit test coverage. However, the actual test files are not shown. The critical validation (XRPL_ASSERT in the constructor) is only triggered when an MPTEndpointStep is constructed, so tests must cover construction with both valid and invalid src/dst/issuer combinations. If tests do not explicitly construct MPTEndpointStep with invalid src/dst, this validation path may not be fully tested. There is no evidence in this file of negative tests (i.e., tests that expect the assertion to fail). Test coverage for data flow through accessors and cache_ is likely, but coverage for error/exception paths and assertion failures is unclear.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro (custom assertion, not a third-party framework)", + "notes": "No explicit input validation framework (like jss:: or template engine) is used in this file. Validation is performed via assertions in the constructor, enforcing business logic constraints. No type, range, or format validation is present in the visible code. Exception handling is via assertion failure (likely throws or aborts).", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "src_ and dst_ (source and destination AccountID)", + "location": "MPTEndpointStep constructor", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Either src_ or dst_ must be the issuer of the MPTIssue (mptIssue_.getIssuer())" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/MPTEndpointStep.cpp.ai.md b/src/libxrpl/tx/paths/MPTEndpointStep.cpp.ai.md new file mode 100644 index 0000000000..281f9f04a0 --- /dev/null +++ b/src/libxrpl/tx/paths/MPTEndpointStep.cpp.ai.md @@ -0,0 +1,39 @@ +# `MPTEndpointStep.cpp` — MPT Endpoint Step for Payment Paths + +## Role in the System + +This file implements the payment-path step that handles the source or destination account when the asset in motion is a Multi-Party Token (MPT). In the XRPL payment engine, every path through the ledger is decomposed into a chain of `Step` objects. `MPTEndpointStep` is the MPT counterpart to `XRPEndpointStep` and `DirectStepI`, and it represents the edge of a strand where MPT balances are actually moved between an issuer and a holder (or between two holders in a direct payment). It is the last concrete piece needed to make MPT a first-class citizen in cross-currency and direct-payment flows, including DEX offer crossing. + +## Class Hierarchy and CRTP Design + +The file defines a single CRTP base class `MPTEndpointStep` that inherits from `StepImp>`. Two concrete subclasses are defined in the same translation unit: `MPTEndpointPaymentStep` and `MPTEndpointOfferCrossingStep`. The CRTP pattern — rather than a virtual dispatch on the payment-vs-crossing distinction — is deliberate: it mirrors the identical structure used by `DirectStepI` for IOU steps, and it allows the hot-path methods `revImp`, `fwdImp`, and `qualitiesSrcIssues` to call back into the concrete type's `maxPaymentFlow`, `checkCreateMPT`, and `verifyPrevStepDebtDirection` without any virtual-function overhead. The distinction is resolved once at construction time by `make_MPTEndpointStep`, which dispatches on `ctx.offerCrossing`. + +## Construction and Invariant Enforcement + +The private constructor captures the `StrandContext` at strand-build time and pre-computes a handful of flags that would otherwise require repeated lookups. Notably, `isDirectBetweenHolders_` is set when the strand delivers this MPT issue, neither strand endpoint is the issuer, and the step is either the first in the strand or its predecessor is not a book step. This flag is later used in `check()` to apply holder-specific frozen/transfer rules. The constructor ends with an `XRPL_ASSERT` that one of `src_` or `dst_` must equal `mptIssue_.getIssuer()` — an invariant that rules out two non-issuer accounts ever appearing directly adjacent in an MPT step, which would be structurally incoherent for a token defined by a single issuer account. + +## Two-Phase Validation in `check()` + +The base-class `check()` performs structural validation common to both payments and offer crossing: it rejects zero/equal accounts, verifies the source account exists on the ledger, enforces that MPT can only appear as the first or last step in a strand (never a middle hop), checks frozen status for the relevant account depending on position, and detects path loops using `seenBookOuts` and `seenDirectAssets`. After all structural checks pass, it delegates to `static_cast(this)->check(ctx, sleSrc)` for context-specific rules. + +`MPTEndpointPaymentStep::check()` then applies the full MPT authorization, frozen, and `canTransfer`/`canTrade` rules. For direct holder-to-holder payments it checks `canTransfer` between the active holder and the strand's final destination. For cross-token paths through the DEX it calls `canTrade` instead. It also short-circuits the path with `tecPATH_DRY` if there is no available source balance and no previous step that could create balance first. `MPTEndpointOfferCrossingStep::check()` unconditionally returns `tesSUCCESS` — offer crossing imposes no up-front MPT auth/frozen checks because the book step that precedes it always issues and those checks are deferred to `checkCreateMPT`. + +## Reverse and Forward Passes + +The payment engine uses a two-pass approach: first a reverse pass (`revImp`) to determine how much input is needed to produce a desired output, then a forward pass (`fwdImp`) to actually execute the flow. Both passes start by calling `maxPaymentFlow()` to determine the maximum transferable amount and the `DebtDirection` (whether the source is issuing or redeeming w.r.t. the issuer). They then call `qualities()` to retrieve `srcQOut` (the effective transfer-rate quality applied to the source's output). Since MPT lacks per-trustline quality fields, `dstQIn` is always `QUALITY_ONE`; only `srcQOut` can differ from unity, and only when the issuer is sourcing tokens and the previous step was redeeming (triggering the MPT transfer rate). + +In `revImp`, if the requested output exceeds `maxSrcToDst`, the step becomes the *limiting node* and delivers `maxSrcToDst` instead. In either case the computed `(in, srcToDst, out, srcDebtDir)` tuple is written to `cache_`. The actual ledger mutation is performed by `directSendNoFee()` using `PaymentSandbox`, which journals the transfer without committing until the full path succeeds. + +## Cache and Forward-Pass Rounding Guard + +Because rounding can cause the forward pass to compute slightly more liquidity than the reverse pass determined was available, `setCacheLimiting()` caps forward-pass results against the cached reverse-pass values. The cap has a deliberate tolerance policy: a difference of exactly one unit is silently corrected; a difference larger than one unit but within 1% is clamped to the cached value; a difference larger than 1% logs a warning at `warn` level and accepts the forward-pass value, allowing the anomaly to be investigated without blocking the transaction. This three-tier approach prevents obscure rounding edge cases from being silently swallowed while also avoiding false panics on normal integer rounding. + +`validFwd()` provides a post-execution consistency check: it saves the cache, re-executes `fwdImp` with the same input, and then verifies that the new cache values are "near" (within `checkNear`) the saved values. A `FlowException` during that re-execution is treated as a validation failure. This check is the payment engine's defense against strands that somehow behave differently on repeated execution. + +## Offer Crossing: `checkCreateMPT` + +The `MPTEndpointOfferCrossingStep::checkCreateMPT()` method handles a detail specific to offer crossing: the offer owner may not have an `MPToken` ledger object for the purchased asset, since the purchase happens atomically during crossing. If the step is `isLast_` (i.e., it represents TakerPays in offer-crossing terms), the method calls `xrpl::checkCreateMPT()` to create the object if necessary. The comment in the code explicitly notes that the reserve check is intentionally waived here because the offer never goes on the books when it crosses — `CreateOffer::applyGuts()` handles reserve separately. The payment variant, `MPTEndpointPaymentStep::checkCreateMPT()`, is a static no-op returning `tesSUCCESS`. + +## Factory and Test Utility + +`make_MPTEndpointStep()` is the external entry point. It instantiates either `MPTEndpointPaymentStep` or `MPTEndpointOfferCrossingStep` based on `ctx.offerCrossing`, immediately calls `check()`, and returns a `{TER, std::unique_ptr}` pair. If `check()` fails, a null pointer is returned. The `test::mptEndpointStepEqual()` function in the nested `test` namespace provides structural equality testing by downcasting through the `MPTEndpointPaymentStep` template specialization — only the payment variant is matched, which is sufficient since tests construct payment steps when verifying strand composition. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/OfferStream.cpp.ai.json b/src/libxrpl/tx/paths/OfferStream.cpp.ai.json new file mode 100644 index 0000000000..14e530ae67 --- /dev/null +++ b/src/libxrpl/tx/paths/OfferStream.cpp.ai.json @@ -0,0 +1,427 @@ +{ + "args": [ + { + "lineno": 10, + "name": "view" + }, + { + "lineno": 10, + "name": "book" + }, + { + "lineno": 12, + "name": "asset" + }, + { + "lineno": 12, + "name": "view" + }, + { + "lineno": 22, + "name": "view" + }, + { + "lineno": 22, + "name": "cancelView" + }, + { + "lineno": 22, + "name": "book" + }, + { + "lineno": 22, + "name": "when" + }, + { + "lineno": 22, + "name": "counter" + }, + { + "lineno": 22, + "name": "journal" + }, + { + "lineno": 39, + "name": "view" + }, + { + "lineno": 65, + "name": "view" + }, + { + "lineno": 65, + "name": "id" + }, + { + "lineno": 65, + "name": "amtDefault" + }, + { + "lineno": 65, + "name": "asset" + }, + { + "lineno": 65, + "name": "freezeHandling" + }, + { + "lineno": 65, + "name": "authHandling" + }, + { + "lineno": 65, + "name": "j" + }, + { + "lineno": 232, + "name": "offerIndex" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TOfferStreamBase::TOfferStreamBase", + "checkIssuers", + "issuerExists" + ], + "entry_point": "TOfferStreamBase::TOfferStreamBase", + "purpose": "Constructor for offer stream, validates that both book.in and book.out issuers exist before proceeding.", + "validation_points": [ + "checkIssuers: Validates book.in.getIssuer() and book.out.getIssuer() via issuerExists" + ] + }, + { + "call_chain": [ + "TOfferStreamBase::erase", + "view.peek(keylet::page(tip_.dir()))", + "std::find(v.begin(), v.end(), tip_.index())" + ], + "entry_point": "TOfferStreamBase::erase", + "purpose": "Removes an offer from a directory if the offer is missing its ledger entry, cleaning up dangling references.", + "validation_points": [ + "Checks directory existence: view.peek(keylet::page(tip_.dir()))", + "Checks offer existence in directory: std::find(v.begin(), v.end(), tip_.index())" + ] + }, + { + "call_chain": [ + "accountFundsHelper", + "id == asset.getIssuer()", + "issuerFundsToSelfIssue (if MPTAmount)", + "accountHolds" + ], + "entry_point": "accountFundsHelper", + "purpose": "Determines the available funds for an account for a given asset, with special handling if the account is the issuer.", + "validation_points": [ + "Checks if account is issuer: if (id == asset.getIssuer())" + ] + } + ], + "data_flows": [ + { + "field": "book.in.getIssuer() / book.out.getIssuer()", + "flow": [ + "Book (constructor argument)", + "checkIssuers (constructor)", + "issuerExists (lambda in checkIssuers)", + "view.exists(keylet::account(issuer))" + ], + "origin": "Book object passed to TOfferStreamBase constructor", + "transformations": [ + "Checked if issuer is XRP (isXRP)", + "Checked if issuer account exists in ledger" + ], + "validated_at": "checkIssuers" + }, + { + "field": "tip_.dir()", + "flow": [ + "tip_ (TOfferStreamBase member)", + "tip_.dir()", + "view.peek(keylet::page(tip_.dir()))" + ], + "origin": "tip_ member (initialized in TOfferStreamBase constructor)", + "transformations": [ + "Used to look up directory page in ledger" + ], + "validated_at": "TOfferStreamBase::erase" + }, + { + "field": "tip_.index()", + "flow": [ + "tip_ (TOfferStreamBase member)", + "tip_.index()", + "std::find(v.begin(), v.end(), tip_.index())" + ], + "origin": "tip_ member (initialized in TOfferStreamBase constructor)", + "transformations": [ + "Used to check if offer index is present in directory" + ], + "validated_at": "TOfferStreamBase::erase" + }, + { + "field": "id == asset.getIssuer()", + "flow": [ + "accountFundsHelper (function parameter)", + "if (id == asset.getIssuer())", + "return amtDefault or issuerFundsToSelfIssue", + "else accountHolds" + ], + "origin": "accountFundsHelper parameters", + "transformations": [ + "Short-circuits to default/issuer funds if account is issuer" + ], + "validated_at": "accountFundsHelper" + } + ], + "description": "Implements the core logic for streaming and processing offers in the XRPL decentralized exchange order book, including offer validation, removal, and fund checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "book.in.getIssuer() and book.out.getIssuer()", + "empty", + "string", + "validation" + ], + "evidence": "checkIssuers (local function) at TOfferStreamBase::TOfferStreamBase (constructor)", + "issue_pattern": "Missing empty string validation for book.in.getIssuer() and book.out.getIssuer()", + "why_false_positive": "checkIssuers (local function) validates book.in.getIssuer() and book.out.getIssuer() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "directory existence (tip_.dir())", + "empty", + "string", + "validation" + ], + "evidence": "view.peek(keylet::page(tip_.dir())) at TOfferStreamBase::erase", + "issue_pattern": "Missing empty string validation for directory existence (tip_.dir())", + "why_false_positive": "view.peek(keylet::page(tip_.dir())) validates directory existence (tip_.dir()) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "offer existence in directory (tip_.index())", + "empty", + "string", + "validation" + ], + "evidence": "std::find(v.begin(), v.end(), tip_.index()) at TOfferStreamBase::erase", + "issue_pattern": "Missing empty string validation for offer existence in directory (tip_.index())", + "why_false_positive": "std::find(v.begin(), v.end(), tip_.index()) validates offer existence in directory (tip_.index()) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "account is issuer (id == asset.getIssuer())", + "empty", + "string", + "validation" + ], + "evidence": "if (id == asset.getIssuer()) at accountFundsHelper (template function)", + "issue_pattern": "Missing empty string validation for account is issuer (id == asset.getIssuer())", + "why_false_positive": "if (id == asset.getIssuer()) validates account is issuer (id == asset.getIssuer()) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/OfferStream.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "Book const& book" + ], + "lineno": 10, + "name": "checkIssuers" + }, + { + "args": [ + "ReadView const& view", + "Asset const& asset" + ], + "lineno": 12, + "name": "issuerExists" + }, + { + "args": [ + "ApplyView& view", + "ApplyView& cancelView", + "Book const& book", + "NetClock::time_point when", + "StepCounter& counter", + "beast::Journal journal" + ], + "lineno": 22, + "name": "TOfferStreamBase" + }, + { + "args": [ + "ApplyView& view" + ], + "lineno": 39, + "name": "erase" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& id", + "T const& amtDefault", + "Asset const& asset", + "FreezeHandling freezeHandling", + "AuthHandling authHandling", + "beast::Journal j" + ], + "lineno": 65, + "name": "accountFundsHelper" + }, + { + "args": [], + "lineno": 89, + "name": "shouldRmSmallIncreasedQOffer" + }, + { + "args": [], + "lineno": 134, + "name": "step" + }, + { + "args": [ + "uint256 const& offerIndex" + ], + "lineno": 232, + "name": "permRmOffer" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is core to offer streaming and order book traversal in XRPL. Typical test coverage would be in integration and unit tests for order book crossing, offer creation, and offer removal. Likely test files: OfferStream_test.cpp, BookStep_test.cpp, Path_test.cpp, and integration tests for DEX/offer crossing. Gaps: Direct validation of missing directory/offer cleanup (erase) may not be explicitly tested; edge cases for issuer existence (e.g., deleted issuer accounts) may be under-tested. Tests for accountFundsHelper's issuer short-circuit logic may be present in IOU/issuer-related tests but should be verified.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (assertion failure, likely aborts)", + "field": "book.in.getIssuer() and book.out.getIssuer()", + "location": "TOfferStreamBase::TOfferStreamBase (constructor)", + "validated_by": "checkIssuers (local function)", + "validates": [ + "Checks that the issuer for both the input and output asset of the Book exists in the ledger (or is XRP, which is always valid).", + "Uses view.exists(keylet::account(issuer)) to check existence." + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Logs error via JLOG, returns early (no exception thrown)", + "field": "directory existence (tip_.dir())", + "location": "TOfferStreamBase::erase", + "validated_by": "view.peek(keylet::page(tip_.dir()))", + "validates": [ + "Checks that the directory page for the offer exists before attempting to erase an offer from it." + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Logs error via JLOG, returns early (no exception thrown)", + "field": "offer existence in directory (tip_.index())", + "location": "TOfferStreamBase::erase", + "validated_by": "std::find(v.begin(), v.end(), tip_.index())", + "validates": [ + "Checks that the offer index exists in the directory's sfIndexes field before attempting to erase it." + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "None (returns amtDefault or result of issuerFundsToSelfIssue)", + "field": "account is issuer (id == asset.getIssuer())", + "location": "accountFundsHelper (template function)", + "validated_by": "if (id == asset.getIssuer())", + "validates": [ + "Checks if the account is the issuer of the asset, which affects how funds are calculated." + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/OfferStream.cpp.ai.md b/src/libxrpl/tx/paths/OfferStream.cpp.ai.md new file mode 100644 index 0000000000..bcf7fa768b --- /dev/null +++ b/src/libxrpl/tx/paths/OfferStream.cpp.ai.md @@ -0,0 +1,52 @@ +# `src/libxrpl/tx/paths/OfferStream.cpp` + +## Role in the System + +`OfferStream.cpp` implements the order-book cursor used during payment processing on the XRPL decentralized exchange. When the payment engine (flow calculation) needs to cross the book for a given asset pair, it instantiates a `FlowOfferStream` and calls `step()` repeatedly to walk from the best-quality offer down to lower-quality ones, skipping or permanently removing any that are stale, expired, frozen, or effectively unfunded. The result of each successful `step()` is a typed `TOffer` representing a live, crossable offer at the tip. + +The file is entirely template-driven. Both `TOfferStreamBase` and `FlowOfferStream` are parameterised on `TIn` and `TOut`, which can each be `XRPAmount`, `IOUAmount`, or `MPTAmount`. All eight combinations used in practice are explicitly instantiated at the bottom of the file, enforcing that the translation unit emits concrete machine code for every valid asset pairing including the newer `MPTAmount` pairings introduced alongside Multi-Purpose Tokens. + +## The Two Views + +The constructor takes two `ApplyView` references: `view_` and `cancelView_`. `view_` is the working ledger state where all side effects during the current transaction accumulate. `cancelView_` is a pristine snapshot of the ledger before the transaction began. + +This two-view design is fundamental to one of the trickiest distinctions `step()` must make: whether an offer is *found unfunded* or *became unfunded*. + +- **Found unfunded**: The owner's balance was already zero in `cancelView_`. This offer is genuinely bad and should be permanently removed from the ledger regardless of whether the current payment strand is committed. +- **Became unfunded**: The owner's balance dropped to zero only inside `view_`, because an earlier step in the same transaction consumed the funds. This offer is still valid in the original ledger and should only be skipped—not deleted—since the current payment attempt might not ultimately succeed. + +`FlowOfferStream::permRmOffer()` records offers that qualify for permanent removal into a `boost::container::flat_set`. The flat set is chosen for its cache-friendly sorted-array layout, which is efficient for small sets of removals typical in a single transaction. + +## The `step()` Loop + +`step()` is the main workhorse and carries the explicit comment: *"Modifying the order or logic of these operations causes a protocol breaking change."* Every early-exit path in the loop must maintain consensus compatibility. The checks proceed in this order: + +1. **Missing ledger entry**: `BookTip::step()` deletes the current offer from the view before advancing. If the resulting `entry` pointer is null, `erase()` cleans up the dangling directory reference and the loop continues. +2. **Expiry**: If `sfExpiration` is present and the offer's deadline is at or before the current ledger close time, it is permanently removed. +3. **Zero amounts**: An offer with either `TakerPays` or `TakerGets` at zero is malformed and permanently removed. +4. **Deep freeze**: If the offer's input asset trust line is deep-frozen for the owner (`isDeepFrozen`), the offer is removed. Deep freeze is a stricter state than regular freeze, preventing even outbound transfers. +5. **Permissioned DEX domain**: If the offer carries an `sfDomainID` field and `permissioned_dex::offerInDomain` returns false (the owner or counterparty no longer meets domain criteria), the offer is removed. +6. **Owner funds check**: `accountFundsHelper` computes how much of `assetOut` the owner actually holds. A zero or negative balance triggers the found-unfunded vs. became-unfunded distinction described above. +7. **Small increased-quality check**: Even if the owner has some funds, the *effective* amounts after clamping to those funds may be so small that the offer's quality degrades below its stated quality. Such offers block the order book without providing meaningful liquidity and are removed. + +## The `accountFundsHelper` Template + +This file-local function unifies fund lookup across all three amount types via `if constexpr` branches: + +- For `IOUAmount`, if the account is the issuer, the function returns `amtDefault` immediately—an issuer's balance with themselves is effectively unlimited for IOU purposes (they are self-funded). +- For `MPTAmount`, issuers are not unlimited; instead `issuerFundsToSelfIssue` computes the actual issuable headroom from the MPT's supply limits. +- For all other cases, `accountHolds` with `fhZERO_IF_FROZEN` and `ahZERO_IF_UNAUTHORIZED` ensures frozen trust lines and unauthorized accounts surface as zero funds, making those offers appear unfunded. + +## `shouldRmSmallIncreasedQOffer` + +This method guards against a subtle order-book-blocking scenario. When an owner's real funds fall below `offer_.amount().out`, the effective exchange amounts shrink proportionally via `quality().ceil_out_strict(...)` with `roundUp = false`. The `false` rounding is intentional: rounding up would prevent the blocking but could also raise the effective quality, which would itself distort the book ordering. The check then asks: is the effective input (`TTakerPays`) so small (at or below `minPositiveAmount()`) that rounding effects cause the effective quality to drop below the original stated quality? If so, the offer is removed. + +The check is skipped entirely when `TakerGets` is XRP: because XRP is indivisible integer drops, the worst-case adjusted quality for a one-drop output is still astronomically good for any realistic IOU, so this protection is unnecessary. + +## The `erase()` Repair Path + +When a directory entry exists but points to a missing SLE, `erase()` manually removes the orphaned index from `sfIndexes` of the directory page. The code comment honestly acknowledges that this should use `ApplyView::dirRemove` (which would also clean up empty directories), but that change would alter how empty directories are handled consensus-wide and is therefore locked out as a protocol-breaking change. The current implementation leaves empty directories in place rather than risk breaking consensus. + +## Explicit Template Instantiations + +The eight `template class` lines at the bottom serve as the translation unit's public contract. They force the compiler to emit code for every combination—`IOUAmount×IOUAmount`, `XRPAmount×IOUAmount`, `IOUAmount×XRPAmount`, and all five `MPTAmount`-involved pairings—in this single compilation unit, keeping link times predictable and avoiding duplicate symbols across translation units that include the header. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/PaySteps.cpp.ai.json b/src/libxrpl/tx/paths/PaySteps.cpp.ai.json new file mode 100644 index 0000000000..54f3c9802d --- /dev/null +++ b/src/libxrpl/tx/paths/PaySteps.cpp.ai.json @@ -0,0 +1,344 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "view_", + "strand_", + "strandSrc_", + "strandDst_", + "strandDeliver_", + "limitQuality_", + "isLast_", + "ownerPaysTransferFee_", + "offerCrossing_", + "isDefaultPath_", + "seenDirectAssets_", + "seenBookOuts_", + "ammContext_", + "domainID_", + "j_" + ], + "lineno": 470, + "name": "StrandContext" + } + ], + "code_paths": [ + { + "call_chain": [ + "toStrand(s) -> toStep -> checkNear / isXRPAccount" + ], + "entry_point": "toStrand / toStrands", + "purpose": "Builds a payment path (strand) from a sequence of STPathElements, validating each step and constructing the appropriate Step objects.", + "validation_points": [ + "toStep: Validates STPathElement types and asset/currency/issuer relationships.", + "isXRPAccount: Validates if a path element is an XRP account.", + "checkNear: Used (elsewhere, not directly in toStep) to validate IOUAmount values for near-equality." + ] + }, + { + "call_chain": [ + "toStep -> isXRPAccount", + "toStep -> checkNear (indirect, via steps that use IOUAmount validation)" + ], + "entry_point": "toStep", + "purpose": "Converts two adjacent STPathElements and an Asset into a Step, validating the types and relationships between elements.", + "validation_points": [ + "isXRPAccount: Validates if an account is an XRP account.", + "XRPL_ASSERT: Validates node types and asset/issuer relationships.", + "checkNear: Used in downstream steps for IOUAmount validation." + ] + }, + { + "call_chain": [ + "checkNear" + ], + "entry_point": "checkNear", + "purpose": "Validates that two IOUAmount values are nearly equal, within a tolerance.", + "validation_points": [ + "checkNear: Directly validates IOUAmount values." + ] + }, + { + "call_chain": [ + "isXRPAccount" + ], + "entry_point": "isXRPAccount", + "purpose": "Checks if a given STPathElement represents an XRP account.", + "validation_points": [ + "isXRPAccount: Validates node type and account type." + ] + } + ], + "data_flows": [ + { + "field": "IOUAmount (expected, actual)", + "flow": [ + "Payment calculation logic", + "checkNear", + "Result used to determine if amounts are acceptably close" + ], + "origin": "Passed as arguments to checkNear (likely from payment calculation logic)", + "transformations": [ + "Exponent and mantissa compared, tolerance applied" + ], + "validated_at": "checkNear" + }, + { + "field": "STPathElement (pe, e1, e2)", + "flow": [ + "User-supplied path (via transaction)", + "toStrand(s)", + "toStep (e1, e2)", + "isXRPAccount (pe or *e1)", + "Step construction" + ], + "origin": "Input to toStep (from toStrand(s)), or to isXRPAccount", + "transformations": [ + "Node type checked", + "AccountID, Asset, Issuer extracted" + ], + "validated_at": "isXRPAccount, toStep (XRPL_ASSERTs)" + }, + { + "field": "Asset (curAsset)", + "flow": [ + "Transaction context", + "toStep", + "Used to determine step type and asset relationships" + ], + "origin": "Derived from path or transaction context", + "transformations": [ + "visit() pattern to dispatch on asset type" + ], + "validated_at": "toStep (via asset type checks and XRPL_ASSERTs)" + } + ], + "description": "This file implements the logic for constructing and validating payment paths (strands) in the XRPL ledger, including normalization, validation, and conversion of path elements into executable steps for payment processing. It handles both XRP and issued assets, including Multi-Party Tokens (MPT), and ensures path correctness and compliance with protocol rules.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "IOUAmount (expected, actual)", + "empty", + "string", + "validation" + ], + "evidence": "checkNear at checkNear function", + "issue_pattern": "Missing empty string validation for IOUAmount (expected, actual)", + "why_false_positive": "checkNear validates IOUAmount (expected, actual) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "STPathElement (pe)", + "empty", + "string", + "validation" + ], + "evidence": "isXRPAccount at isXRPAccount function", + "issue_pattern": "Missing empty string validation for STPathElement (pe)", + "why_false_positive": "isXRPAccount validates STPathElement (pe) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "STPathElement (e1, e2)", + "empty", + "string", + "validation" + ], + "evidence": "toStep at toStep function", + "issue_pattern": "Missing empty string validation for STPathElement (e1, e2)", + "why_false_positive": "toStep validates STPathElement (e1, e2) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/PaySteps.cpp", + "functions": [ + { + "args": [ + "expected", + "actual" + ], + "lineno": 9, + "name": "checkNear" + }, + { + "args": [ + "pe" + ], + "lineno": 27, + "name": "isXRPAccount" + }, + { + "args": [ + "ctx", + "e1", + "e2", + "curAsset" + ], + "lineno": 33, + "name": "toStep" + }, + { + "args": [ + "view", + "src", + "dst", + "deliver", + "limitQuality", + "sendMaxAsset", + "path", + "ownerPaysTransferFee", + "offerCrossing", + "ammContext", + "domainID", + "j" + ], + "lineno": 109, + "name": "toStrand" + }, + { + "args": [ + "view", + "src", + "dst", + "deliver", + "limitQuality", + "sendMax", + "paths", + "addDefaultPath", + "ownerPaysTransferFee", + "offerCrossing", + "ammContext", + "domainID", + "j" + ], + "lineno": 370, + "name": "toStrands" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "Core validation logic (checkNear, isXRPAccount, toStep) is likely covered by pathfinding and payment engine tests, typically found in test files such as 'Path_test.cpp', 'Strand_test.cpp', or 'PaymentSandbox_test.cpp'. However, some error branches (e.g., offer/account payment step, xrp/xrp offer payment step) are marked as unreachable or excluded from coverage (LCOV_EXCL_START), indicating that these are not directly tested. Edge cases involving malformed paths or asset types may not be fully covered unless explicitly tested in negative test cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom C++ logic (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "IOUAmount (expected, actual)", + "location": "checkNear function", + "validated_by": "checkNear", + "validates": [ + "Exponent difference is at most 1", + "If actual.exponent() < -20, always true", + "Mantissa values are close within a relative tolerance of 0.001" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "STPathElement (pe)", + "location": "isXRPAccount function", + "validated_by": "isXRPAccount", + "validates": [ + "pe.getNodeType() == STPathElement::typeAccount", + "pe.getAccountID() is XRP" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns error code (TER) or unique_ptr", + "field": "STPathElement (e1, e2)", + "location": "toStep function", + "validated_by": "toStep", + "validates": [ + "e1->isAccount()", + "e2->isAccount()", + "e1->getNodeType() & STPathElement::typeCurrency", + "e1->getPathAsset().isXRP()", + "isXRPAccount(*e1)", + "e2->isAccount()", + "e1->isOffer() && e2->isAccount()" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/PaySteps.cpp.ai.md b/src/libxrpl/tx/paths/PaySteps.cpp.ai.md new file mode 100644 index 0000000000..d83d0782bb --- /dev/null +++ b/src/libxrpl/tx/paths/PaySteps.cpp.ai.md @@ -0,0 +1,61 @@ +# `src/libxrpl/tx/paths/PaySteps.cpp` + +## Role in the System + +`PaySteps.cpp` is the strand construction layer of XRPL's payment engine. It bridges the gap between the raw payment path data embedded in a transaction — a user-supplied `STPathSet` of account and offer-book waypoints — and the executable `Strand` representation that the inner flow engine consumes. A `Strand` is a `std::vector>`, where each `Step` is a polymorphic object encoding one hop's worth of liquidity: an account-to-account IOU transfer, an order book crossing, or an XRP/MPT endpoint. + +The file's three public interfaces (`toStep`, `toStrand`, `toStrands`) form the construction pipeline. `Flow.cpp` calls `toStrands()` during payment setup; that function calls `toStrand()` per path; which calls the file-local `toStep()` per element pair. Everything below `toStrands()` is internal to this pipeline. + +## Path Element Pairs and the `toStep()` Dispatch + +`toStep()` is the fundamental factory dispatcher. It receives two adjacent normalized path elements (`e1`, `e2`) and the asset currently flowing through the strand (`curAsset`), and returns a newly allocated `Step`. The dispatch logic encodes every valid hop topology in the XRPL protocol: + +- **XRP endpoint**: `e1` is an account with an XRP currency flag and `ctx.isFirst` is set → `make_XRPEndpointStep`. Similarly, when `e1` is the XRP bridge account and `e2` is a regular account on the last hop → another XRP endpoint. These handle the source-side and destination-side XRP terminals. +- **MPT endpoint**: When both `e1` and `e2` are accounts and `curAsset` holds an `MPTIssue` → `make_MPTEndpointStep`. The comments document three sub-cases: direct issuer↔holder payment, inter-holder payment (two steps: holder→issuer→holder1), and cross-token payments where the MPT endpoint appears as the first or last step only. +- **IOU direct step**: When both elements are accounts and `curAsset` holds an `Issue` → `make_DirectStepI`. This is the rippling primitive — two accounts sharing a trust line. +- **Book steps**: When `e2` is an offer node, `toStep` inspects the `curAsset`/`outAsset` combination and dispatches one of eight book step factories: `make_BookStepII`, `make_BookStepIX`, `make_BookStepXI`, `make_BookStepXM`, `make_BookStepMX`, `make_BookStepMI`, `make_BookStepIM`, or `make_BookStepMM`. The naming convention encodes the input/output pair: `I` for IOU, `X` for XRP, `M` for MPT. + +One path topology is explicitly rejected: an XRP→XRP offer book hop. That combination — `isXRP(curAsset) && outAsset.isXRP()` — has no valid interpretation and returns `temBAD_PATH`. + +## Path Normalization in `toStrand()` + +`toStrand()` is the core of this file. Its first task is upfront validation of every element in the raw `STPath`: element types must be valid bit-flag combinations, account nodes cannot carry issuer/currency flags simultaneously, XRP cannot appear as an explicit issuer, and MPT nodes cannot appear next to account-only nodes (MPT has no rippling semantics). Violating any of these returns `temBAD_PATH` immediately. + +After validation, the raw path is expanded into `normPath` — a fully explicit sequence of path elements with all implied nodes inserted: + +1. **Source endpoint**: Always the first element, typed with `typeAccount | typeIssuer | typeCurrency` (or `typeMPT`). For IOU, the issuer in this implied element is set to `src` itself, representing the sender's own trust line. +2. **Implied SendMax issuer**: If `sendMaxAsset` is specified and its issuer differs from `src`, and the path doesn't begin with an account that *is* that issuer, an extra account node for the SendMax issuer is inserted. This is the "push the IOU to its issuer before crossing" step. +3. **User-supplied path elements**: Copied verbatim into `normPath`. +4. **Implied deliver asset node**: If the last asset in `normPath` doesn't match `deliver` (or, during offer crossing, the issuer differs), an offer node specifying `deliver` is appended. This ensures the path ends with the correct asset type. +5. **Implied deliver issuer**: If the deliver issuer is neither already the last node's account nor the destination itself, an account node for the deliver issuer is inserted. This represents the "pull the IOU from its issuer" step. +6. **Destination endpoint**: Appended unconditionally if the last node isn't already `dst`. + +This normalization is the reason `toStep()` never has to reason about missing intermediate accounts — by the time elements reach it, the path is fully explicit. + +## Iterating Pairs and Tracking `curAsset` + +The main loop processes `normPath` as overlapping pairs `(normPath[i], normPath[i+1])`. A critical subtlety governs offer nodes: when `cur` is an offer and `next` is an account, `continue` skips creating a new step because the step was already created for the offer at the *previous* iteration — offer steps are created when `e2` is an offer, not when `e1` is one. + +`curAsset` tracks the asset flowing through the strand as the loop advances. For `Issue` assets, the `.account` field updates at each account node to reflect the current holder (essential for determining whose trust line is in play). For MPT assets, the account embedded in the `MPTID` is immutable, so only the MPTID itself is tracked. The transition from MPT to IOU is handled explicitly: when a `cur` node carries a currency flag and `curAsset` holds an `MPTIssue`, `curAsset` resets to a bare `Issue{}` before the currency is applied. + +Two blocks in the loop — handling implied accounts before offers and between pairs of account nodes — are noted in comments as dead code: because `curAsset` always tracks the current account via the normalization above, the issuer is never mismatched at this point. The blocks are retained as defensive fallbacks. + +## Duplicate Detection via `StrandContext` + +Each call to `toStep()` receives a freshly constructed `StrandContext` that carries mutable references to two accumulating sets: `seenDirectAssets` (a two-element flat_set array, indexed 0 for source-side, 1 for destination-side) and `seenBookOuts` (a flat_set of output assets). These are passed into the step factory constructors where each step can detect and reject loops — an account appearing twice in the same currency on the same side of a direct step, or an offer book producing the same output asset twice in the same strand. The two-slot design for direct assets allows the same account to appear once as a source and once as a destination (a legitimate two-hop pattern) without triggering the loop guard. + +`StrandContext` is also the carrier for strand position metadata: `isFirst` (derived as `strand_.empty()`) and `isLast` (passed by the caller), which step factories use to decide endpoint behaviour and fee applicability. + +## Post-Construction Verification + +After the strand is fully assembled, `checkStrand()` performs an invariant walk. It traces account continuity — the output account of step N must equal the input account of step N+1 — and verifies that the final asset matches `deliver`. For book steps, it validates that the incoming asset matches the book's input side and advances `curAsset` to the book's output. The entire function is wrapped in an `UNREACHABLE` guard: if it fires, there is a logic error in the normalization or dispatch code above. All branches that reach it are excluded from coverage measurement (`LCOV_EXCL_START`). + +## `toStrands()` and Error Policy + +`toStrands()` wraps `toStrand()` across an entire `STPathSet`. Its error handling has a deliberate asymmetry: `temBAD_PATH` (a malformed transaction error) aborts the entire operation immediately regardless of other paths, because it indicates a protocol-level mistake the sender must fix. A non-`tem` failure on an individual path (e.g., a liquidity or trust-line error) is recorded as `lastFailTer` but does not abort — the remaining paths are still attempted. If at least one path succeeds, `toStrands()` returns `tesSUCCESS` with whatever strands were built. Only when *all* paths fail does the last failure code propagate. + +Duplicate strand deduplication is done with a linear scan (`std::find` with `operator==`). Since path count is bounded by the protocol (eight paths maximum), this is acceptable despite the O(n²) nature; strand equality uses the per-step `equal()` virtual, which compares accounts, assets, and book keys. + +## `checkNear()` and Floating-Point Tolerance + +`checkNear()` answers whether two `IOUAmount` values should be considered equal. IOU amounts use a mantissa+exponent representation capable of expressing values across many orders of magnitude. The function permits a relative tolerance of 0.1% (`ratTol = 0.001`) and requires that exponents differ by at most 1. When the actual amount's exponent is below −20, any expected value is accepted — rounding at extreme scale makes exact equality meaningless. XRP and MPT counterparts (inlined in `Steps.h`) use exact integer equality, since those amount types are fixed-point without the floating-point accumulation behaviour of IOUs. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/RippleCalc.cpp.ai.json b/src/libxrpl/tx/paths/RippleCalc.cpp.ai.json new file mode 100644 index 0000000000..5165c9426a --- /dev/null +++ b/src/libxrpl/tx/paths/RippleCalc.cpp.ai.json @@ -0,0 +1,355 @@ +{ + "args": [ + { + "lineno": 12, + "name": "view" + }, + { + "lineno": 19, + "name": "saMaxAmountReq" + }, + { + "lineno": 27, + "name": "saDstAmountReq" + }, + { + "lineno": 31, + "name": "uDstAccountID" + }, + { + "lineno": 32, + "name": "uSrcAccountID" + }, + { + "lineno": 36, + "name": "spsPaths" + }, + { + "lineno": 38, + "name": "domainID" + }, + { + "lineno": 39, + "name": "registry" + }, + { + "lineno": 40, + "name": "pInputs" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "xrpl::path::RippleCalc::rippleCalculate", + "flow (xrpl::path::flow)" + ], + "entry_point": "xrpl::path::RippleCalc::rippleCalculate", + "purpose": "Calculates the result of a payment path, including liquidity and quality, using the provided ledger view and transaction parameters.", + "validation_points": [ + "Inline lambda for saMaxAmountReq (limitQuality)", + "Inline lambda for saMaxAmountReq (sendMax)", + "Pointer check for pInputs", + "flow() parameter validation (external to this file)" + ] + } + ], + "data_flows": [ + { + "field": "saMaxAmountReq", + "flow": [ + "Function parameter", + "Used in inline lambda for limitQuality", + "Used in inline lambda for sendMax", + "Passed as sendMax to flow()" + ], + "origin": "Function parameter (caller of rippleCalculate)", + "transformations": [ + "Checked for > beast::zero in limitQuality lambda", + "Checked for >= beast::zero, asset equality, and issuer match in sendMax lambda", + "Converted to Quality or std::optional" + ], + "validated_at": "Inline lambdas in rippleCalculate" + }, + { + "field": "pInputs", + "flow": [ + "Function parameter", + "Pointer checked for nullptr", + "Dereferenced for defaultPathsAllowed and partialPaymentAllowed" + ], + "origin": "Function parameter (caller of rippleCalculate)", + "transformations": [ + "If nullptr, defaultPaths = true, partialPayment = false", + "If not nullptr, values taken from struct" + ], + "validated_at": "Pointer check at start of function" + }, + { + "field": "saDstAmountReq", + "flow": [ + "Function parameter", + "Used in limitQuality lambda", + "Passed directly to flow()" + ], + "origin": "Function parameter (caller of rippleCalculate)", + "transformations": [ + "Combined with saMaxAmountReq to create Quality in limitQuality lambda" + ], + "validated_at": "Indirectly in limitQuality lambda" + }, + { + "field": "defaultPaths", + "flow": [ + "Set from pInputs or default", + "Passed to flow()" + ], + "origin": "Derived from pInputs or defaulted", + "transformations": [ + "Boolean logic based on pInputs" + ], + "validated_at": "At assignment from pInputs" + }, + { + "field": "partialPayment", + "flow": [ + "Set from pInputs or default", + "Passed to flow()" + ], + "origin": "Derived from pInputs or defaulted", + "transformations": [ + "Boolean logic based on pInputs" + ], + "validated_at": "At assignment from pInputs" + }, + { + "field": "limitQuality", + "flow": [ + "Computed in lambda", + "Passed to flow()" + ], + "origin": "Derived from saMaxAmountReq, saDstAmountReq, and pInputs", + "transformations": [ + "std::optional based on checks" + ], + "validated_at": "In lambda" + }, + { + "field": "sendMax", + "flow": [ + "Computed in lambda", + "Passed to flow()" + ], + "origin": "Derived from saMaxAmountReq, saDstAmountReq, uSrcAccountID", + "transformations": [ + "std::optional based on checks" + ], + "validated_at": "In lambda" + } + ], + "description": "Implements the rippleCalculate function for pathfinding and liquidity calculation in XRPL payments, using the Flow algorithm and handling payment sandboxing, path exploration, and error handling.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "pInputs (pointer null check)", + "validation", + "missing", + "check" + ], + "evidence": "Field pInputs (pointer null check) validated by None explicit (custom logic, some validation likely in flow())", + "issue_pattern": "Missing validation for pInputs (pointer null check)", + "why_false_positive": "None explicit (custom logic, some validation likely in flow()) validates pInputs (pointer null check) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "saMaxAmountReq (range and asset/issuer checks before use)", + "validation", + "missing", + "check" + ], + "evidence": "Field saMaxAmountReq (range and asset/issuer checks before use) validated by None explicit (custom logic, some validation likely in flow())", + "issue_pattern": "Missing validation for saMaxAmountReq (range and asset/issuer checks before use)", + "why_false_positive": "None explicit (custom logic, some validation likely in flow()) validates saMaxAmountReq (range and asset/issuer checks before use) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "saMaxAmountReq", + "empty", + "string", + "validation" + ], + "evidence": "inline lambda in rippleCalculate at RippleCalc::rippleCalculate (limitQuality lambda)", + "issue_pattern": "Missing empty string validation for saMaxAmountReq", + "why_false_positive": "inline lambda in rippleCalculate validates saMaxAmountReq for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "saMaxAmountReq", + "empty", + "string", + "validation" + ], + "evidence": "inline lambda in rippleCalculate at RippleCalc::rippleCalculate (sendMax lambda)", + "issue_pattern": "Missing empty string validation for saMaxAmountReq", + "why_false_positive": "inline lambda in rippleCalculate validates saMaxAmountReq for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "pInputs", + "empty", + "string", + "validation" + ], + "evidence": "pointer check at RippleCalc::rippleCalculate (defaultPaths/partialPayment assignment)", + "issue_pattern": "Missing empty string validation for pInputs", + "why_false_positive": "pointer check validates pInputs for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "flow() call parameters", + "empty", + "string", + "validation" + ], + "evidence": "flow() function (external) at RippleCalc::rippleCalculate (flow() call)", + "issue_pattern": "Missing empty string validation for flow() call parameters", + "why_false_positive": "flow() function (external) validates flow() call parameters for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/RippleCalc.cpp", + "functions": [ + { + "args": [ + "PaymentSandbox& view", + "STAmount const& saMaxAmountReq", + "STAmount const& saDstAmountReq", + "AccountID const& uDstAccountID", + "AccountID const& uSrcAccountID", + "STPathSet const& spsPaths", + "std::optional const& domainID", + "ServiceRegistry& registry", + "Input const* const pInputs" + ], + "lineno": 10, + "name": "xrpl::path::RippleCalc::rippleCalculate" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "xrpl::path" + } + ], + "test_coverage_notes": "The core logic of rippleCalculate is likely covered by integration and unit tests for payment pathfinding and payment execution. Typical test files would be in the rippled codebase under 'src/test/app/paths/' or similar, such as 'Path_test.cpp', 'RippleCalc_test.cpp', or 'Flow_test.cpp'. However, the inline validation lambdas for saMaxAmountReq and pInputs are only as well-tested as the test cases that exercise edge cases (e.g., null pInputs, negative/zero saMaxAmountReq, asset mismatches). There may be gaps in coverage for unusual or malformed input combinations, especially if tests do not explicitly check for validation failures or exception handling in flow().", + "validation_architecture": { + "auto_validated_fields": [ + "pInputs (pointer null check)", + "saMaxAmountReq (range and asset/issuer checks before use)" + ], + "framework": "None explicit (custom logic, some validation likely in flow())", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "none (conditional logic)", + "field": "saMaxAmountReq", + "location": "RippleCalc::rippleCalculate (limitQuality lambda)", + "validated_by": "inline lambda in rippleCalculate", + "validates": [ + "Checks if saMaxAmountReq > beast::zero before using for limitQuality" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (conditional logic)", + "field": "saMaxAmountReq", + "location": "RippleCalc::rippleCalculate (sendMax lambda)", + "validated_by": "inline lambda in rippleCalculate", + "validates": [ + "Checks if saMaxAmountReq >= beast::zero", + "Checks if saMaxAmountReq.asset() != saDstAmountReq.asset()", + "Checks if saMaxAmountReq.getIssuer() != uSrcAccountID" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logic)", + "field": "pInputs", + "location": "RippleCalc::rippleCalculate (defaultPaths/partialPayment assignment)", + "validated_by": "pointer check", + "validates": [ + "Checks if pInputs is nullptr before accessing members" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "std::exception (caught)", + "field": "flow() call parameters", + "location": "RippleCalc::rippleCalculate (flow() call)", + "validated_by": "flow() function (external)", + "validates": [ + "All parameters to flow() are assumed to be validated inside flow()", + "Any exception thrown by flow() is caught and handled" + ], + "validation_type": "business_logic|type|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/RippleCalc.cpp.ai.md b/src/libxrpl/tx/paths/RippleCalc.cpp.ai.md new file mode 100644 index 0000000000..d879c482bc --- /dev/null +++ b/src/libxrpl/tx/paths/RippleCalc.cpp.ai.md @@ -0,0 +1,47 @@ +# `RippleCalc.cpp` — Payment Path Calculation Entry Point + +## Role in the System + +`RippleCalc.cpp` is a thin but architecturally significant adapter that sits between the XRPL transaction engine and the lower-level `flow()` payment algorithm. It implements the single static method `RippleCalc::rippleCalculate()`, which is the canonical public entry point for executing a payment across a set of paths. The actual multi-path traversal, liquidity aggregation, and quality optimization all live in `Flow.cpp`; this file's job is to translate high-level transaction parameters into the form `flow()` expects, manage sandbox isolation, and ensure the ledger is never left in a partially-mutated state when something goes wrong. + +## The Double-Sandbox Pattern + +The most architecturally important design decision in this file is the creation of a *nested* `PaymentSandbox`: + +```cpp +PaymentSandbox flowSB(&view); +``` + +The caller already supplies a `PaymentSandbox& view`, which itself is a copy-on-write overlay over the real ledger state. Rather than letting `flow()` write directly into the caller's view, `rippleCalculate()` wraps it in a second sandbox (`flowSB`) and passes that to `flow()`. Only after `flow()` returns does the code call `flowSB.apply(view)`, promoting any changes to the caller's view. + +The reason is exception safety: if `flow()` throws, `flowSB` is destroyed with its mutations intact but unapplied, leaving `view` completely unmodified. Without this intermediate layer, any partial state that `flow()` had already written before throwing would corrupt the caller's sandbox. The `apply()` call sits unconditionally after the try-catch block; `flow()` itself applies its own internal sandbox to `flowSB` only on success (see `finishFlow()` in `Flow.cpp`), so on failure `flowSB` holds no significant mutations and the apply is effectively a no-op. + +## Parameter Preprocessing via Lambdas + +Before delegating to `flow()`, the function derives three computed values inline: + +**`defaultPaths`** and **`partialPayment`** are straightforward null-guard extractions from the optional `pInputs` pointer. When `pInputs` is `nullptr`, the function applies sensible defaults: default paths are enabled, partial payments are not. + +**`limitQuality`** is only non-null if the caller has set `pInputs->limitQuality` *and* `saMaxAmountReq > beast::zero`. When both conditions hold, a `Quality` object is constructed from the ratio `Amounts(saMaxAmountReq, saDstAmountReq)` — this ratio represents the maximum acceptable exchange rate (input per unit of output). Paths offering worse quality than this threshold will be skipped by the flow engine. + +**`sendMax`** encodes a subtle semantic: when `saMaxAmountReq` is negative (the sentinel value `-1` meaning "no limit"), or when the source and destination assets differ, or when the issuer of the source amount is not the sending account, then `sendMax` is set to the raw `saMaxAmountReq`. In the remaining case — sending the same IOU that the destination receives, with the sender as issuer — `sendMax` is `std::nullopt`. This signals to `flow()` that no separate spending cap needs to be enforced beyond what the delivery target already implies. + +## Exception Handling and `tecINTERNAL` + +```cpp +catch (std::exception& e) +{ + JLOG(j.error()) << "Exception from flow: " << e.what(); + path::RippleCalc::Output exceptResult; + exceptResult.setResult(tecINTERNAL); + return exceptResult; +} +``` + +Returning `tecINTERNAL` rather than rethrowing is a deliberate choice rooted in how XRPL ledger result codes work. `tec`-class codes cause the transaction to be included in the ledger and the fee to be charged. If instead the function threw or returned a `tem`/`ter`/`tef` code, the transaction might not be stored, creating a discrepancy between nodes that handled the exception and those that didn't. Converting any unexpected exception to `tecINTERNAL` provides a safe, deterministic fallback that every validator will agree on. + +## Relationship to `flow()` and `RippleCalc::Output` + +The `Output` struct (defined in `RippleCalc.h`) carries the actual amounts moved (`actualAmountIn`, `actualAmountOut`), the final `TER` result code, and a set of `removableOffers` — unfunded or expired offers discovered during path traversal that could not be cleaned up because the payment failed. `rippleCalculate()` returns this struct unmodified from whatever `flow()` produced, but logs the key fields at debug level before returning, which aids in tracing payment behavior in logs. + +The `hardcoded false` passed to `flow()` for `ownerPaysTransferFee` and `OfferCrossing::no` for the crossing mode indicate that `rippleCalculate()` is always invoked in the pure-payment context (not offer crossing). Offer crossing has its own dedicated path through `flow()` with different semantics, called directly without going through `RippleCalc`. \ No newline at end of file diff --git a/src/libxrpl/tx/paths/XRPEndpointStep.cpp.ai.json b/src/libxrpl/tx/paths/XRPEndpointStep.cpp.ai.json new file mode 100644 index 0000000000..b2e1215c48 --- /dev/null +++ b/src/libxrpl/tx/paths/XRPEndpointStep.cpp.ai.json @@ -0,0 +1,471 @@ +{ + "args": [ + { + "lineno": 19, + "name": "ctx" + }, + { + "lineno": 19, + "name": "acc" + }, + { + "lineno": 133, + "name": "sb" + }, + { + "lineno": 133, + "name": "afView" + }, + { + "lineno": 133, + "name": "ofrsToRm" + }, + { + "lineno": 133, + "name": "out" + }, + { + "lineno": 147, + "name": "in" + }, + { + "lineno": 127, + "name": "v" + }, + { + "lineno": 127, + "name": "prevStepDir" + }, + { + "lineno": 223, + "name": "step" + }, + { + "lineno": 223, + "name": "acc" + } + ], + "classes": [ + { + "args": [ + "StrandContext const& ctx, AccountID const& acc" + ], + "lineno": 11, + "name": "XRPEndpointStep" + }, + { + "args": [ + "StrandContext const& ctx, AccountID const& acc" + ], + "lineno": 74, + "name": "XRPEndpointPaymentStep" + }, + { + "args": [ + "StrandContext const& ctx, AccountID const& acc" + ], + "lineno": 91, + "name": "XRPEndpointOfferCrossingStep" + } + ], + "code_paths": [ + { + "call_chain": [ + "make_XRPEndpointStep (not shown, but implied)", + "XRPEndpointStep::XRPEndpointStep(StrandContext const&, AccountID const&)" + ], + "entry_point": "make_XRPEndpointStep / XRPEndpointStep constructor", + "purpose": "Constructs an XRPEndpointStep, initializing account, isLast, and journal fields.", + "validation_points": [ + "StrandContext::isLast is implicitly validated by type (bool)", + "acc (AccountID) is implicitly validated by type" + ] + }, + { + "call_chain": [ + "XRPEndpointStep::check(StrandContext const&)" + ], + "entry_point": "XRPEndpointStep::check", + "purpose": "Performs error and constraint checks on the step (e.g., frozen constraints).", + "validation_points": [ + "Explicit validation logic inside check (details not shown in snippet)" + ] + }, + { + "call_chain": [ + "XRPEndpointStep::validFwd(PaymentSandbox&, ApplyView&, EitherAmount const&)" + ], + "entry_point": "XRPEndpointStep::validFwd", + "purpose": "Checks if a forward step is valid given the input amount.", + "validation_points": [ + "cache_ (std::optional) is checked for presence before use" + ] + }, + { + "call_chain": [ + "XRPEndpointStep::revImp(PaymentSandbox&, ApplyView&, flat_set&, XRPAmount const&)", + "XRPEndpointStep::fwdImp(PaymentSandbox&, ApplyView&, flat_set&, XRPAmount const&)" + ], + "entry_point": "XRPEndpointStep::revImp / fwdImp", + "purpose": "Implements reverse and forward liquidity calculations for the step.", + "validation_points": [ + "cache_ is checked for presence before use" + ] + } + ], + "data_flows": [ + { + "field": "acc_ (AccountID)", + "flow": [ + "Constructor parameter", + "Stored in acc_", + "Accessed via acc()", + "Used in directStepAccts(), xrpLiquidImpl(), logStringImpl()" + ], + "origin": "Constructor argument (AccountID const& acc)", + "transformations": [ + "No transformation; direct assignment and usage" + ], + "validated_at": "Implicitly validated by AccountID type at construction" + }, + { + "field": "isLast_ (bool)", + "flow": [ + "ctx.isLast", + "Stored in isLast_", + "Used in directStepAccts() to determine account pair order" + ], + "origin": "Constructor argument (StrandContext const& ctx)", + "transformations": [ + "No transformation; direct assignment and usage" + ], + "validated_at": "Implicitly validated by bool type at construction" + }, + { + "field": "cache_ (std::optional)", + "flow": [ + "Set internally (e.g., in revImp/fwdImp)", + "Accessed via cached(), cachedIn(), cachedOut()", + "Used in validFwd()" + ], + "origin": "Set internally during liquidity calculations (not shown in snippet)", + "transformations": [ + "Wrapped/unwrapped in std::optional", + "Converted to EitherAmount in cached()" + ], + "validated_at": "Checked for presence (has_value) before dereferencing" + } + ], + "description": "Implements XRPEndpointStep and its derivatives for handling XRP endpoints in payment and offer crossing strands in the XRPL transaction pathfinding logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "acc (AccountID)", + "validation", + "missing", + "check" + ], + "evidence": "Field acc (AccountID) validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for acc (AccountID)", + "why_false_positive": "C++ type system, std::optional validates acc (AccountID) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "isLast (bool)", + "validation", + "missing", + "check" + ], + "evidence": "Field isLast (bool) validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for isLast (bool)", + "why_false_positive": "C++ type system, std::optional validates isLast (bool) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "cache_ (std::optional)", + "validation", + "missing", + "check" + ], + "evidence": "Field cache_ (std::optional) validated by C++ type system, std::optional", + "issue_pattern": "Missing validation for cache_ (std::optional)", + "why_false_positive": "C++ type system, std::optional validates cache_ (std::optional) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "StrandContext::isLast", + "empty", + "string", + "validation" + ], + "evidence": "implicit type checking (bool) at XRPEndpointStep constructor", + "issue_pattern": "Missing empty string validation for StrandContext::isLast", + "why_false_positive": "implicit type checking (bool) validates StrandContext::isLast for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "StrandContext::isLast", + "type", + "validation", + "check" + ], + "evidence": "implicit type checking (bool) at XRPEndpointStep constructor", + "issue_pattern": "Missing type validation for StrandContext::isLast", + "why_false_positive": "implicit type checking (bool) validates StrandContext::isLast type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "acc (AccountID)", + "empty", + "string", + "validation" + ], + "evidence": "implicit type checking (AccountID) at XRPEndpointStep constructor", + "issue_pattern": "Missing empty string validation for acc (AccountID)", + "why_false_positive": "implicit type checking (AccountID) validates acc (AccountID) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "acc (AccountID)", + "type", + "validation", + "check" + ], + "evidence": "implicit type checking (AccountID) at XRPEndpointStep constructor", + "issue_pattern": "Missing type validation for acc (AccountID)", + "why_false_positive": "implicit type checking (AccountID) validates acc (AccountID) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "cache_ (XRPAmount)", + "empty", + "string", + "validation" + ], + "evidence": "std::optional at cached(), cachedIn(), cachedOut()", + "issue_pattern": "Missing empty string validation for cache_ (XRPAmount)", + "why_false_positive": "std::optional validates cache_ (XRPAmount) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/paths/XRPEndpointStep.cpp", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 120, + "name": "operator==" + }, + { + "args": [ + "v", + "prevStepDir" + ], + "lineno": 126, + "name": "qualityUpperBound" + }, + { + "args": [ + "sb", + "afView", + "ofrsToRm", + "out" + ], + "lineno": 132, + "name": "revImp" + }, + { + "args": [ + "sb", + "afView", + "ofrsToRm", + "in" + ], + "lineno": 146, + "name": "fwdImp" + }, + { + "args": [ + "sb", + "afView", + "in" + ], + "lineno": 160, + "name": "validFwd" + }, + { + "args": [ + "ctx" + ], + "lineno": 184, + "name": "check" + }, + { + "args": [ + "step", + "acc" + ], + "lineno": 222, + "name": "xrpEndpointStepEqual" + }, + { + "args": [ + "ctx", + "acc" + ], + "lineno": 232, + "name": "make_XRPEndpointStep" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + }, + { + "lineno": 221, + "name": "test" + } + ], + "test_coverage_notes": "XRPEndpointStep is a core path step implementation, so it is likely tested indirectly via payment and pathfinding integration tests (e.g., in test/paths/ or test/app/paths/). Direct unit tests for XRPEndpointStep itself may be limited or absent, as much of its logic is exercised through higher-level transaction tests. Validation of acc_ and isLast_ is type-based and not explicitly tested. The cache_ logic is likely tested via liquidity/path tests, but edge cases (e.g., unset cache_) may not be directly covered. The check() method's error/frozen constraint logic should be covered by tests for frozen accounts or payment failures, but coverage depends on the thoroughness of those tests.", + "validation_architecture": { + "auto_validated_fields": [ + "acc (AccountID)", + "isLast (bool)", + "cache_ (std::optional)" + ], + "framework": "C++ type system, std::optional", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (compile-time type safety)", + "field": "StrandContext::isLast", + "location": "XRPEndpointStep constructor", + "validated_by": "implicit type checking (bool)", + "validates": [ + "isLast is a boolean, only true/false accepted" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (compile-time type safety)", + "field": "acc (AccountID)", + "location": "XRPEndpointStep constructor", + "validated_by": "implicit type checking (AccountID)", + "validates": [ + "acc must be a valid AccountID type" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (optional checked at runtime)", + "field": "cache_ (XRPAmount)", + "location": "cached(), cachedIn(), cachedOut()", + "validated_by": "std::optional", + "validates": [ + "cache_ is present before use" + ], + "validation_type": "type/presence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/paths/XRPEndpointStep.cpp.ai.md b/src/libxrpl/tx/paths/XRPEndpointStep.cpp.ai.md new file mode 100644 index 0000000000..3a1ad74a96 --- /dev/null +++ b/src/libxrpl/tx/paths/XRPEndpointStep.cpp.ai.md @@ -0,0 +1,73 @@ +# `XRPEndpointStep.cpp` — XRP Source/Destination in the Payment Path Engine + +## Role in the System + +Every payment through the XRPL pathfinding engine is represented as a **strand**: an ordered sequence of `Step` objects that transform one currency into another until the destination receives the intended asset. When XRP is either the currency being sent or the currency being received, the strand begins or ends with an `XRPEndpointStep`. This file implements that step — the bookend that connects a real account's XRP balance to the abstract flow graph used by the path engine (`Flow.cpp`, `StrandFlow.h`). + +Unlike `DirectStep` (which handles IOU-to-IOU trust-line transfers) or `BookStep` (which traverses an order book), `XRPEndpointStep` does no currency conversion. Its sole job is to debit or credit XRP from a concrete account and hand the amount to (or receive it from) the virtual `xrpAccount()` sentinel that represents XRP as a fungible in-flight quantity. + +## CRTP-Based Policy Split: Payments vs. Offer Crossing + +The file defines one CRTP base class template, `XRPEndpointStep`, and two concrete subclasses that supply a single behavioral variation: `xrpLiquid()`. + +``` +XRPEndpointStep (CRTP base, all logic lives here) + ├─ XRPEndpointPaymentStep (payments — full reserve applies) + └─ XRPEndpointOfferCrossingStep (offer crossing — reduced reserve) +``` + +The reason for the split is a long-standing ledger rule: when an offer crosses and the buyer doesn't yet hold a trust line (or MPT holding) for the delivered asset, the system knows a new ledger object will be created, consuming one reserve increment. So the buyer is allowed to spend one reserve unit more of XRP than normal. `XRPEndpointOfferCrossingStep` captures this by computing a `reserveReduction_` at construction time — calling `computeReserveReduction()` — and passing it into `xrpLiquidImpl()` as a negative reserve offset. The payment variant always passes `0`. + +Rather than embedding this policy in a runtime `if`/`else`, the CRTP pattern makes the choice a compile-time dispatch through `static_cast(this)->xrpLiquid(sb)`. Both `revImp()` and `fwdImp()` call `xrpLiquid()` this way, so there is zero virtual overhead on the hot path. + +## The Single-Cache Design + +Most `Step` subclasses maintain separate `cachedIn` and `cachedOut` fields because their forward and reverse amounts differ (an order book step consumes offers at different rates depending on direction). For `XRPEndpointStep`, this is unnecessary: XRP moves 1:1, so input equals output always. The comment at the field declaration makes this explicit: + +> *Since this step will always be an endpoint in a strand (either the first or last step) the same cache is used for cachedIn and cachedOut and only one will ever be used.* + +`cache_` is a single `std::optional`. Both `cachedIn()` and `cachedOut()` return the same `cached()` helper, which wraps it in an `EitherAmount`. This is legitimate because a step that is `isFirst` only ever executes as a sender (only `cachedOut` is consumed by the next step), while a step that is `isLast` only ever executes as a receiver (only `cachedIn` is consumed by the previous step). + +## Reverse and Forward Execution + +The path engine first executes a **reverse pass** (`revImp`) — walking the strand from destination to source to find how much input is needed — and then a **forward pass** (`fwdImp`) that commits the actual transfer. The cache bridges these two passes. + +In `revImp`, the direction of transfer is encoded directly in `isLast_`: +- If this is the **last step** (XRP receiver), it accepts the full requested `out` unconditionally — the engine has already established that the amount is valid, and a receiving account can always accept XRP. +- If this is the **first step** (XRP sender), it caps at `std::min(balance, out)` where `balance = xrpLiquid(sb)`. This is the spendable balance: total XRP minus the base reserve, minus the owner reserve for each ledger object. + +`fwdImp` mirrors this structure. It asserts that `cache_` is populated (guaranteeing `revImp` ran first), then applies the same balance-capping logic before calling `accountSend()`. If `accountSend()` fails, both methods return `{zero, zero}` to signal the strand is dry. + +The sender/receiver pair passed to `accountSend()` is selected at runtime using `isLast_`: + +```cpp +auto& sender = isLast_ ? xrpAccount() : acc_; +auto& receiver = isLast_ ? acc_ : xrpAccount(); +``` + +`xrpAccount()` is the canonical sentinel representing the XRP network itself — passing it to `accountSend()` denotes a "burn" or "mint" in the abstract payment sandbox rather than a real account debit. + +## Validation in `check()` + +`check()` runs once at strand construction time (not during execution). It enforces four invariants: + +1. **Account existence** — the `acc_` must resolve to a live ledger account object. Absent accounts return `terNO_ACCOUNT`. +2. **Endpoint-only constraint** — the step must be either first or last in the strand (`!ctx.isFirst && !ctx.isLast` returns `temBAD_PATH`). XRP cannot be an intermediate currency in a multi-hop path. +3. **Freeze check** — `checkFreeze()` inspects the source-to-destination direction for global account freeze, per-trustline directional freeze, and deep-freeze flags. An XRP transfer may still be blocked if the destination account itself is globally frozen. +4. **Loop detection** — `ctx.seenDirectAssets` is a two-element array tracking which currencies have already appeared on each side of the strand. Inserting `xrpIssue()` into the appropriate slot and failing if the insert returns `false` prevents a malformed path from looping through XRP twice. + +## `qualityUpperBound()` and `debtDirection()` + +Quality in the path engine is the ratio of output to input. Since `XRPEndpointStep` is 1:1, it always returns `Quality{STAmount::uRateOne}` (rate = 1.0). This is used by the engine to pre-filter strands with a worse overall quality than `limitQuality`. + +`debtDirection()` always returns `DebtDirection::issues`. The debt direction concept (issues vs. redeems) is relevant to IOU trust lines where one party may be the issuer. XRP has no issuer, so it is always in the "issues" state — it never redeems toward a counterparty. + +## `validFwd()` — Pre-Commit Sanity Check + +Between the reverse and forward passes, the engine calls `validFwd()` to verify the incoming amount against the cached expectation. For a first-step sender, it re-checks that `balance >= xrpIn`, warning if balance has shifted since the reverse pass (which could happen if another strand modified the sandbox). If the amounts diverge from the cache, a warning is logged but the step still returns `true` — the discrepancy is noted but not fatal, deferring error handling to `fwdImp`. + +## Factory and Test Helper + +`make_XRPEndpointStep()` is the sole public entry point. It reads `ctx.offerCrossing` and instantiates the appropriate subclass, runs `check()`, and returns a `std::unique_ptr` on success or `{ter, nullptr}` on failure. This keeps the two-phase construction (allocate, then validate) predictable for callers. + +The `test::xrpEndpointStepEqual()` function in the nested `test` namespace exists solely so unit tests can downcast a `Step*` to `XRPEndpointPaymentStep` and verify the account identity without exposing the private type hierarchy to test code. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp.ai.json b/src/libxrpl/tx/transactors/account/AccountDelete.cpp.ai.json new file mode 100644 index 0000000000..55bdc54ee9 --- /dev/null +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp.ai.json @@ -0,0 +1,635 @@ +{ + "args": [ + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 22, + "name": "ctx" + }, + { + "lineno": 33, + "name": "view" + }, + { + "lineno": 33, + "name": "tx" + }, + { + "lineno": 46, + "name": "registry" + }, + { + "lineno": 46, + "name": "view" + }, + { + "lineno": 46, + "name": "account" + }, + { + "lineno": 46, + "name": "delIndex" + }, + { + "lineno": 46, + "name": "sleDel" + }, + { + "lineno": 46, + "name": "j" + }, + { + "lineno": 53, + "name": "registry" + }, + { + "lineno": 53, + "name": "view" + }, + { + "lineno": 53, + "name": "account" + }, + { + "lineno": 53, + "name": "delIndex" + }, + { + "lineno": 53, + "name": "sleDel" + }, + { + "lineno": 53, + "name": "j" + }, + { + "lineno": 60, + "name": "view" + }, + { + "lineno": 60, + "name": "account" + }, + { + "lineno": 60, + "name": "delIndex" + }, + { + "lineno": 60, + "name": "j" + }, + { + "lineno": 67, + "name": "view" + }, + { + "lineno": 67, + "name": "delIndex" + }, + { + "lineno": 67, + "name": "j" + }, + { + "lineno": 74, + "name": "view" + }, + { + "lineno": 74, + "name": "account" + }, + { + "lineno": 74, + "name": "delIndex" + }, + { + "lineno": 74, + "name": "sleDel" + }, + { + "lineno": 83, + "name": "view" + }, + { + "lineno": 83, + "name": "account" + }, + { + "lineno": 83, + "name": "delIndex" + }, + { + "lineno": 83, + "name": "sleDel" + }, + { + "lineno": 83, + "name": "j" + }, + { + "lineno": 90, + "name": "view" + }, + { + "lineno": 90, + "name": "account" + }, + { + "lineno": 90, + "name": "sleDel" + }, + { + "lineno": 90, + "name": "j" + }, + { + "lineno": 97, + "name": "view" + }, + { + "lineno": 97, + "name": "sleDel" + }, + { + "lineno": 97, + "name": "j" + }, + { + "lineno": 104, + "name": "view" + }, + { + "lineno": 104, + "name": "account" + }, + { + "lineno": 104, + "name": "delIndex" + }, + { + "lineno": 104, + "name": "sleDel" + }, + { + "lineno": 104, + "name": "j" + }, + { + "lineno": 112, + "name": "t" + }, + { + "lineno": 132, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AccountDelete::preflight", + "credentials::checkFields" + ], + "entry_point": "AccountDelete::preflight", + "purpose": "Performs preflight validation for AccountDelete transactions, ensuring fields are correct and business rules are enforced before execution.", + "validation_points": [ + "AccountDelete::preflight: Checks sfAccount != sfDestination", + "AccountDelete::preflight: Calls credentials::checkFields for credential field validation" + ] + }, + { + "call_chain": [ + "AccountDelete::checkExtraFeatures" + ], + "entry_point": "AccountDelete::checkExtraFeatures", + "purpose": "Checks if extra features (like credentials) are allowed by the current rules and transaction fields.", + "validation_points": [ + "AccountDelete::checkExtraFeatures: Validates presence of sfCredentialIDs and featureCredentials" + ] + } + ], + "data_flows": [ + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "AccountDelete::preflight" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared to ctx.tx[sfDestination] for equality" + ], + "validated_at": "AccountDelete::preflight" + }, + { + "field": "sfDestination", + "flow": [ + "Transaction input", + "AccountDelete::preflight" + ], + "origin": "ctx.tx[sfDestination] (transaction input)", + "transformations": [ + "Compared to ctx.tx[sfAccount] for equality" + ], + "validated_at": "AccountDelete::preflight" + }, + { + "field": "sfCredentialIDs", + "flow": [ + "Transaction input", + "AccountDelete::checkExtraFeatures", + "AccountDelete::preflight", + "credentials::checkFields" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence with ctx.tx.isFieldPresent(sfCredentialIDs)", + "Validated with credentials::checkFields" + ], + "validated_at": "AccountDelete::checkExtraFeatures, AccountDelete::preflight (via credentials::checkFields)" + } + ], + "description": "Implements the logic for the AccountDelete transaction in the XRPL codebase, including preflight, preclaim, fee calculation, and ledger cleanup for account deletion.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccount validated by xrpl transaction preflight validation (custom, not external framework)", + "issue_pattern": "Missing validation for sfAccount", + "why_false_positive": "xrpl transaction preflight validation (custom, not external framework) validates sfAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDestination", + "validation", + "missing", + "check" + ], + "evidence": "Field sfDestination validated by xrpl transaction preflight validation (custom, not external framework)", + "issue_pattern": "Missing validation for sfDestination", + "why_false_positive": "xrpl transaction preflight validation (custom, not external framework) validates sfDestination automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCredentialIDs", + "validation", + "missing", + "check" + ], + "evidence": "Field sfCredentialIDs validated by xrpl transaction preflight validation (custom, not external framework)", + "issue_pattern": "Missing validation for sfCredentialIDs", + "why_false_positive": "xrpl transaction preflight validation (custom, not external framework) validates sfCredentialIDs automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "other credential fields (via credentials::checkFields)", + "validation", + "missing", + "check" + ], + "evidence": "Field other credential fields (via credentials::checkFields) validated by xrpl transaction preflight validation (custom, not external framework)", + "issue_pattern": "Missing validation for other credential fields (via credentials::checkFields)", + "why_false_positive": "xrpl transaction preflight validation (custom, not external framework) validates other credential fields (via credentials::checkFields) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount, sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "explicit comparison at AccountDelete::preflight", + "issue_pattern": "Missing empty string validation for sfAccount, sfDestination", + "why_false_positive": "explicit comparison validates sfAccount, sfDestination for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Credential fields (e.g., sfCredentialIDs)", + "empty", + "string", + "validation" + ], + "evidence": "credentials::checkFields at AccountDelete::preflight", + "issue_pattern": "Missing empty string validation for Credential fields (e.g., sfCredentialIDs)", + "why_false_positive": "credentials::checkFields validates Credential fields (e.g., sfCredentialIDs) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfCredentialIDs", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.isFieldPresent(sfCredentialIDs) && ctx.rules.enabled(featureCredentials) at AccountDelete::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfCredentialIDs", + "why_false_positive": "ctx.tx.isFieldPresent(sfCredentialIDs) && ctx.rules.enabled(featureCredentials) validates sfCredentialIDs for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/account/AccountDelete.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "AccountDelete::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 22, + "name": "AccountDelete::preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 33, + "name": "AccountDelete::calculateBaseFee" + }, + { + "args": [ + "ServiceRegistry&", + "ApplyView& view", + "AccountID const& account", + "uint256 const& delIndex", + "std::shared_ptr const& sleDel", + "beast::Journal j" + ], + "lineno": 46, + "name": "offerDelete" + }, + { + "args": [ + "ServiceRegistry& registry", + "ApplyView& view", + "AccountID const& account", + "uint256 const& delIndex", + "std::shared_ptr const& sleDel", + "beast::Journal j" + ], + "lineno": 53, + "name": "removeSignersFromLedger" + }, + { + "args": [ + "ServiceRegistry&", + "ApplyView& view", + "AccountID const& account", + "uint256 const& delIndex", + "std::shared_ptr const&", + "beast::Journal j" + ], + "lineno": 60, + "name": "removeTicketFromLedger" + }, + { + "args": [ + "ServiceRegistry&", + "ApplyView& view", + "AccountID const&", + "uint256 const& delIndex", + "std::shared_ptr const&", + "beast::Journal j" + ], + "lineno": 67, + "name": "removeDepositPreauthFromLedger" + }, + { + "args": [ + "ServiceRegistry&", + "ApplyView& view", + "AccountID const& account", + "uint256 const& delIndex", + "std::shared_ptr const& sleDel", + "beast::Journal" + ], + "lineno": 74, + "name": "removeNFTokenOfferFromLedger" + }, + { + "args": [ + "ServiceRegistry&", + "ApplyView& view", + "AccountID const& account", + "uint256 const& delIndex", + "std::shared_ptr const& sleDel", + "beast::Journal j" + ], + "lineno": 83, + "name": "removeDIDFromLedger" + }, + { + "args": [ + "ServiceRegistry&", + "ApplyView& view", + "AccountID const& account", + "uint256 const&", + "std::shared_ptr const& sleDel", + "beast::Journal j" + ], + "lineno": 90, + "name": "removeOracleFromLedger" + }, + { + "args": [ + "ServiceRegistry&", + "ApplyView& view", + "AccountID const&", + "uint256 const&", + "std::shared_ptr const& sleDel", + "beast::Journal j" + ], + "lineno": 97, + "name": "removeCredentialFromLedger" + }, + { + "args": [ + "ServiceRegistry&", + "ApplyView& view", + "AccountID const& account", + "uint256 const& delIndex", + "std::shared_ptr const& sleDel", + "beast::Journal j" + ], + "lineno": 104, + "name": "removeDelegateFromLedger" + }, + { + "args": [ + "LedgerEntryType t" + ], + "lineno": 112, + "name": "nonObligationDeleter" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 132, + "name": "AccountDelete::preclaim" + }, + { + "args": [], + "lineno": 217, + "name": "AccountDelete::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + } + ], + "test_coverage_notes": "AccountDelete transaction logic is typically tested in unit/integration test files such as AccountDelete_test.cpp or Transaction_test.cpp. Validation of sfAccount != sfDestination and credential field checks should be covered, but edge cases (e.g., malformed credential fields, feature flag toggling) may not be fully tested. Tests for featureCredentials gating and credential field presence should be verified. Gaps may exist if new credential-related features are added without corresponding tests.", + "validation_architecture": { + "auto_validated_fields": [ + "sfAccount", + "sfDestination", + "sfCredentialIDs", + "other credential fields (via credentials::checkFields)" + ], + "framework": "xrpl transaction preflight validation (custom, not external framework)", + "validation_layer": "business_logic (preflight phase of transaction processing)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temDST_IS_SRC", + "field": "sfAccount, sfDestination", + "location": "AccountDelete::preflight", + "validated_by": "explicit comparison", + "validates": [ + "Checks that the account being deleted is not the same as the destination account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "various NotTEC error codes (see credentials::checkFields)", + "field": "Credential fields (e.g., sfCredentialIDs)", + "location": "AccountDelete::preflight", + "validated_by": "credentials::checkFields", + "validates": [ + "Checks presence, format, and validity of credential-related fields in the transaction" + ], + "validation_type": "business_logic|format|type" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (returns bool, used for gating feature usage)", + "field": "sfCredentialIDs", + "location": "AccountDelete::checkExtraFeatures", + "validated_by": "ctx.tx.isFieldPresent(sfCredentialIDs) && ctx.rules.enabled(featureCredentials)", + "validates": [ + "Checks that credential features are only used if the feature is enabled" + ], + "validation_type": "business_logic|feature_flag" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/account/AccountDelete.cpp.ai.md b/src/libxrpl/tx/transactors/account/AccountDelete.cpp.ai.md new file mode 100644 index 0000000000..e129bad09f --- /dev/null +++ b/src/libxrpl/tx/transactors/account/AccountDelete.cpp.ai.md @@ -0,0 +1,51 @@ +# AccountDelete.cpp + +`AccountDelete.cpp` implements the XRPL `AccountDelete` transaction, the only mechanism by which an account can be permanently erased from the ledger. Executing this transaction removes the source account's SLE, cleans up every deletable object it owns, and sweeps its remaining XRP balance to a specified destination address. The implementation follows the standard four-phase transactor lifecycle: `checkExtraFeatures` → `preflight` → `preclaim` → `doApply`. + +## Fee Design + +`calculateBaseFee()` departs from the standard base-fee formula by charging one full owner reserve increment instead. This is intentional: account deletion is a privileged, irreversible operation that must be economically significant enough to prevent spam while remaining cheaper than simply holding reserves indefinitely. The reserve-sized fee is computed by the shared `calculateOwnerReserveFee()` helper inherited from `Transactor`. + +## Preflight and Feature Gating + +`checkExtraFeatures()` enforces that `sfCredentialIDs` may only appear in the transaction when the `featureCredentials` amendment is active. This is the correct place for feature-flag gating because it happens before ledger state is consulted. + +`preflight()` performs two fast, stateless checks: it rejects `temDST_IS_SRC` when source and destination are the same account (since there is nobody left to receive the XRP), and delegates credential field format validation to `credentials::checkFields`. + +## Preclaim: Blocking Conditions + +`preclaim()` is the most consequential method. It must return `tesSUCCESS` only when every condition required for a safe deletion is met, because an account that fails here causes no ledger mutation and no fee burn. + +**Destination checks.** The destination account must exist (`tecNO_DST`). If it has `lsfRequireDestTag` set, a destination tag must be present. If it has `lsfDepositAuth` set and no `sfCredentialIDs` is supplied, the source must have a `DepositPreauth` object authorising it. The deposit-auth check is intentionally skipped here when credentials are present — expired-credential removal requires a mutable `ApplyView`, which is only available in `doApply`. + +**NFToken obligations.** XRPL treats minted NFTs as ledger obligations even after transfer. If `sfMintedNFTokens != sfBurnedNFTokens`, the account is still an issuer of outstanding tokens and cannot be deleted (`tecHAS_OBLIGATIONS`). Separately, if the account holds any NFTokens in its NFToken pages, deletion is also blocked. A second NFT-related check guards against duplicate token IDs after account resurrection: `FirstNFTokenSequence + MintedNFTokens + 255` must not exceed the current ledger sequence, because authorized minting can create NFTs without advancing the issuer's account sequence. + +**Sequence freshness check.** An account's sequence number must be at least 256 below the current ledger index (`tecTOO_SOON`). Without this guard, a resurrected account could replay transactions that were valid before deletion — a critical security property given XRPL's replay-prevention model. + +**Owner directory scan.** The code iterates the account's owner directory and calls `nonObligationDeleter()` on each entry's type. If any entry returns `nullptr` from that function — meaning it is a genuine obligation (trust lines with balances, escrows, checks, payment channels) — `preclaim` fails with `tecHAS_OBLIGATIONS`. If the directory contains more than `maxDeletableDirEntries` (1000) deletable items, it returns `tefTOO_BIG` to prevent a single transaction from consuming excessive execution time. + +## The Deleter Dispatch Table + +The anonymous namespace defines a uniform function pointer type `DeleterFuncPtr` and a set of thin adapter functions, one per deletable ledger entry type. The adapter signatures all match `DeleterFuncPtr` exactly, even though the underlying deletion functions have varied signatures — the adapters simply drop unused parameters: + +```cpp +using DeleterFuncPtr = TER (*)( + ServiceRegistry&, ApplyView&, AccountID const&, + uint256 const&, std::shared_ptr const&, beast::Journal); +``` + +`nonObligationDeleter()` is a `switch` over `LedgerEntryType` that maps each deletable type to its adapter, and returns `nullptr` for any type that represents a blocking obligation. This design is used identically in both `preclaim` (to detect blockers) and `doApply` (to actually invoke deletions), ensuring the two phases stay in sync without duplicating the type classification logic. + +The supported deletable types are: `ltOFFER`, `ltSIGNER_LIST`, `ltTICKET`, `ltDEPOSIT_PREAUTH`, `ltNFTOKEN_OFFER`, `ltDID`, `ltORACLE`, `ltCREDENTIAL`, and `ltDELEGATE`. Each delegates to its own transactor's static deletion helper (e.g., `DIDDelete::deleteSLE`, `OracleDelete::deleteOracle`), keeping cleanup logic co-located with the feature that created the object. + +## doApply: Execution + +`doApply()` first re-checks deposit authorisation using `verifyDepositPreauth()` when `sfCredentialIDs` is present. Unlike the `preclaim` call to `credentials::valid()`, this call operates on the mutable `ApplyView` so it can remove any expired credentials it discovers — a deliberate two-phase split to handle credential expiry correctly. + +The account's owner directory is then walked via `cleanupOnAccountDelete()`, which invokes the deleter callback for each entry. The `nonObligationDeleter` dispatch is called again here; if it somehow returns `nullptr` (which `preclaim` should have prevented), the code hits an `UNREACHABLE` macro and logs an error — a defensive invariant assertion rather than silent data corruption. + +After the directory is empty, the remaining XRP balance is transferred atomically: it is added to the destination's `sfBalance` and subtracted from the source's, with `ctx_.deliver()` recording the amount for the metadata. An `XRPL_ASSERT` then verifies the source balance is exactly zero before erasure. Finally, the source account SLE is removed from the ledger view with `view().erase(src)`. As a minor housekeeping detail, if the destination account had `lsfPasswordSpent` set and the incoming XRP is nonzero, that flag is cleared, re-arming the free password-change allowance. + +## Invariants and Failure Modes + +The sequence-number and NFT-sequence guards are the primary replay-prevention mechanism for deleted accounts. The `tefTOO_BIG` limit prevents a DoS attack where a user accumulates thousands of deletable objects to force a very expensive transaction. The fatal log in `preclaim` and the `UNREACHABLE` guard in `doApply` together ensure that any corruption of the owner directory is surfaced loudly rather than silently propagated. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/account/AccountSet.cpp.ai.json b/src/libxrpl/tx/transactors/account/AccountSet.cpp.ai.json new file mode 100644 index 0000000000..0720be1188 --- /dev/null +++ b/src/libxrpl/tx/transactors/account/AccountSet.cpp.ai.json @@ -0,0 +1,315 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Transactor::apply", + "AccountSet::preflight", + "AccountSet::preclaim", + "AccountSet::doApply" + ], + "entry_point": "Transactor::apply (via AccountSet)", + "purpose": "Processes an AccountSet transaction, validating and applying changes to an account.", + "validation_points": [ + "AccountSet::preflight" + ] + }, + { + "call_chain": [ + "AccountSet::makeTxConsequences" + ], + "entry_point": "AccountSet::makeTxConsequences", + "purpose": "Determines the consequences category of the transaction (e.g., blocker or normal) based on flags.", + "validation_points": [ + "AccountSet::makeTxConsequences (flag checks)" + ] + } + ], + "data_flows": [ + { + "field": "sfSetFlag", + "flow": [ + "STTx (tx)", + "AccountSet::preflight (tx.getFieldU32(sfSetFlag))", + "Validation logic (compared to sfClearFlag, asfRequireAuth, asfRequireDest, asfDisallowXRP, etc.)", + "Used to determine which account flags to set" + ], + "origin": "STTx (transaction input)", + "transformations": [ + "Read as uint32_t", + "Compared for equality/contradiction with sfClearFlag", + "Mapped to specific account flags" + ], + "validated_at": "AccountSet::preflight" + }, + { + "field": "sfClearFlag", + "flow": [ + "STTx (tx)", + "AccountSet::preflight (tx.getFieldU32(sfClearFlag))", + "Validation logic (compared to sfSetFlag, asfRequireAuth, asfRequireDest, asfDisallowXRP, etc.)", + "Used to determine which account flags to clear" + ], + "origin": "STTx (transaction input)", + "transformations": [ + "Read as uint32_t", + "Compared for equality/contradiction with sfSetFlag", + "Mapped to specific account flags" + ], + "validated_at": "AccountSet::preflight" + }, + { + "field": "tfRequireAuth / tfOptionalAuth / asfRequireAuth", + "flow": [ + "STTx (tx)", + "AccountSet::preflight (tx.getFlags(), tx.getFieldU32(sfSetFlag), tx.getFieldU32(sfClearFlag))", + "Validation logic (checks for contradictory set/clear)", + "Used to set/clear RequireAuth flag on account" + ], + "origin": "STTx flags and fields", + "transformations": [ + "Bitwise checks", + "Boolean logic for contradiction" + ], + "validated_at": "AccountSet::preflight" + }, + { + "field": "tfRequireDestTag / tfOptionalDestTag / asfRequireDest", + "flow": [ + "STTx (tx)", + "AccountSet::preflight (tx.getFlags(), tx.getFieldU32(sfSetFlag), tx.getFieldU32(sfClearFlag))", + "Validation logic (checks for contradictory set/clear)", + "Used to set/clear RequireDestTag flag on account" + ], + "origin": "STTx flags and fields", + "transformations": [ + "Bitwise checks", + "Boolean logic for contradiction" + ], + "validated_at": "AccountSet::preflight" + }, + { + "field": "tfDisallowXRP / tfAllowXRP / asfDisallowXRP", + "flow": [ + "STTx (tx)", + "AccountSet::preflight (tx.getFlags(), tx.getFieldU32(sfSetFlag), tx.getFieldU32(sfClearFlag))", + "Validation logic (checks for contradictory set/clear)", + "Used to set/clear DisallowXRP flag on account" + ], + "origin": "STTx flags and fields", + "transformations": [ + "Bitwise checks", + "Boolean logic for contradiction" + ], + "validated_at": "AccountSet::preflight" + }, + { + "field": "sfTransferRate", + "flow": [ + "STTx (tx)", + "AccountSet::preflight (tx.isFieldPresent(sfTransferRate), tx.getFieldU32(sfTransferRate))", + "Validation logic (range checks: != 0, >= QUALITY_ONE, <= 2*QUALITY_ONE)", + "Used to set TransferRate on account" + ], + "origin": "STTx (transaction input)", + "transformations": [ + "Read as uint32_t", + "Range checked" + ], + "validated_at": "AccountSet::preflight" + }, + { + "field": "sfTickSize", + "flow": [ + "STTx (tx)", + "AccountSet::preflight (tx.isFieldPresent(sfTickSize), tx[sfTickSize])", + "Validation logic (range checks: == 0 or minTickSize <= value <= maxTickSize)", + "Used to set TickSize on account" + ], + "origin": "STTx (transaction input)", + "transformations": [ + "Read as uint32_t", + "Range checked" + ], + "validated_at": "AccountSet::preflight" + } + ], + "description": "Implements the AccountSet transaction logic for the XRPL ledger, including preflight checks, permission checks, preclaim logic, and application of account settings and flags.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSetFlag and sfClearFlag", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for sfSetFlag and sfClearFlag", + "why_false_positive": "explicit check in code validates sfSetFlag and sfClearFlag for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfRequireAuth, tfOptionalAuth, asfRequireAuth", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for tfRequireAuth, tfOptionalAuth, asfRequireAuth", + "why_false_positive": "explicit check in code validates tfRequireAuth, tfOptionalAuth, asfRequireAuth for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfRequireDestTag, tfOptionalDestTag, asfRequireDest", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for tfRequireDestTag, tfOptionalDestTag, asfRequireDest", + "why_false_positive": "explicit check in code validates tfRequireDestTag, tfOptionalDestTag, asfRequireDest for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "tfDisallowXRP, tfAllowXRP, asfDisallowXRP", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code (partial, code cut off) at preflight", + "issue_pattern": "Missing empty string validation for tfDisallowXRP, tfAllowXRP, asfDisallowXRP", + "why_false_positive": "explicit check in code (partial, code cut off) validates tfDisallowXRP, tfAllowXRP, asfDisallowXRP for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/account/AccountSet.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 12, + "name": "AccountSet::makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 32, + "name": "AccountSet::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 36, + "name": "AccountSet::preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 109, + "name": "AccountSet::checkPermission" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 146, + "name": "AccountSet::preclaim" + }, + { + "args": [], + "lineno": 191, + "name": "AccountSet::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "AccountSet transaction validation is typically covered in unit/integration tests under the rippled repository, especially in files like 'AccountSet_test.cpp', 'Transactor_test.cpp', or 'Tx_test.cpp'. These tests cover valid/invalid flag combinations, contradictory flags, and field value ranges (e.g., TransferRate, TickSize). Gaps may exist for edge cases involving simultaneous set/clear of the same flag, or for new flags/features not yet covered by tests. Fuzzing or property-based tests may be limited for complex flag interactions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in preflight function (no external framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "sfSetFlag and sfClearFlag", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "SetFlag and ClearFlag are not both set to the same value (cannot set and clear the same flag in one transaction)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "tfRequireAuth, tfOptionalAuth, asfRequireAuth", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "RequireAuth is not both set and cleared in the same transaction (contradictory flags not allowed)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "tfRequireDestTag, tfOptionalDestTag, asfRequireDest", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "RequireDestTag is not both set and cleared in the same transaction (contradictory flags not allowed)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "temINVALID_FLAG (presumed, based on pattern)", + "field": "tfDisallowXRP, tfAllowXRP, asfDisallowXRP", + "location": "preflight", + "validated_by": "explicit check in code (partial, code cut off)", + "validates": [ + "DisallowXRP is not both set and cleared in the same transaction (contradictory flags not allowed)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/account/AccountSet.cpp.ai.md b/src/libxrpl/tx/transactors/account/AccountSet.cpp.ai.md new file mode 100644 index 0000000000..286c36cd55 --- /dev/null +++ b/src/libxrpl/tx/transactors/account/AccountSet.cpp.ai.md @@ -0,0 +1,41 @@ +# AccountSet.cpp + +`AccountSet.cpp` implements the `AccountSet` transactor — the primary mechanism by which XRPL account holders configure the behavioural properties of their accounts. It governs a wide range of settings: access controls (`RequireAuth`, `DepositAuth`), XRP reception preferences (`DisallowXRP`), freezing authority (`NoFreeze`, `GlobalFreeze`), trust-line policy (`DefaultRipple`, `AllowTrustLineClawback`, `AllowTrustLineLocking`), transaction tracking (`AccountTxnID`), and metadata fields such as domain, email hash, message key, and NFT minting delegation. + +## Transactor Lifecycle + +`AccountSet` extends the `Transactor` base class and follows the four-phase execution model used by all XRPL transaction types: `makeTxConsequences` → `preflight` → `preclaim` → `doApply`. Each phase has distinct access rights — `preflight` sees only the transaction itself with no ledger access, `preclaim` has read-only ledger access, and `doApply` has full mutable ledger access via the apply view. + +## Dual Flag Interface and Legacy Complexity + +One of the most structurally unusual aspects of `AccountSet` is that it exposes account flags through *two* parallel mechanisms: legacy transaction-level bitflags (e.g., `tfRequireAuth`, `tfOptionalAuth`, `tfRequireDestTag`) set in the transaction's `Flags` field, and the more modern `sfSetFlag`/`sfClearFlag` integer fields carrying `asf*` constants. Both paths must produce the same logical effect. In `preflight` and `doApply`, the code computes booleans like `bSetRequireAuth` and `bClearRequireAuth` by ORing both signal paths together. This makes the code verbose but is unavoidable: the legacy flag interface predates the `SetFlag`/`ClearFlag` fields and remains supported for protocol compatibility. + +`preflight` enforces that neither path allows contradictory intent within a single transaction. If both `bSetRequireAuth` and `bClearRequireAuth` resolve to true — via any combination of the two input mechanisms — the transaction is rejected with `temINVALID_FLAG`. The same guard applies to `RequireDestTag` and `DisallowXRP`. An additional sanity check prevents `sfSetFlag` and `sfClearFlag` from carrying the same value simultaneously. + +## Consequence Classification + +`makeTxConsequences` uses a custom factory (`ConsequencesFactory{Custom}`) to classify transactions as either `blocker` or `normal`. Transactions that set or clear `asfRequireAuth`, `asfDisableMaster`, or `asfAccountTxnID` — and equivalently, the legacy `tfRequireAuth`/`tfOptionalAuth` bitflags — are classified as blockers. This matters within the batch transaction mechanism: a blocker prevents later transactions from the same account in the same batch from being reordered past it, since these flags fundamentally change how subsequent transactions are processed or signed. + +## Delegation and Granular Permissions + +`checkPermission` enforces a nuanced access-control policy for delegated transactions. The overall `AccountSet` transaction type is not delegable at the transaction level — a delegate with only a transaction-type permission for `ttACCOUNT_SET` cannot execute it. However, a more granular delegation layer (`GranularPermissionType`) permits specific field-level operations. The code checks individual fields: `sfEmailHash`, `sfMessageKey`, `sfDomain`, `sfTransferRate`, and `sfTickSize` each require their own `AccountEmailHashSet`, `AccountMessageKeySet`, `AccountDomainSet`, `AccountTransferRateSet`, or `AccountTickSizeSet` granular permission respectively. Flag changes (any non-zero `sfSetFlag`, `sfClearFlag`, or transaction-level flags) are explicitly prohibited — there is no granular permission pathway for toggling account flags. `sfWalletLocator` and `sfNFTokenMinter` are also always denied to delegates. + +## Stateful Constraints in preclaim + +`preclaim` performs two checks that require ledger state. First, setting `RequireAuth` on an account that currently lacks it is only valid if the account's owner directory is empty — no existing trust lines can be retroactively subjected to an authorization requirement. If the retry flag `tapRETRY` is set, the error code is softened from `tecOWNERS` to `terOWNERS`, signalling that the transaction could succeed later if the owner directory empties. + +Second, when the `featureClawback` amendment is enabled, `asfAllowTrustLineClawback` and `asfNoFreeze` are mutually exclusive and each requires an empty owner directory when being set. Setting clawback while `NoFreeze` is active returns `tecNO_PERMISSION`; setting `NoFreeze` while clawback is active does likewise. The empty-directory requirement for clawback prevents an issuer from silently acquiring clawback rights over existing trust lines. + +## Critical Irreversibility Guards in doApply + +Several flag operations in `doApply` are guarded by master-key authentication. The `sigWithMaster` lambda inspects the transaction's signing public key and verifies it derives to the account's own address — meaning the master private key was used to sign. Both `asfDisableMaster` and `asfNoFreeze` require this. For `asfDisableMaster` specifically, the code additionally verifies that a `sfRegularKey` field is present on the account SLE or that a signer list object exists at `keylet::signers(account_)` before disabling the master key — otherwise the account would be locked out permanently, returning `tecNO_ALTERNATIVE_KEY`. + +The `NoFreeze`/`GlobalFreeze` interaction is also carefully guarded: once `NoFreeze` is active, `GlobalFreeze` can still be *set* (to protect all counterparties at once) but can never be *cleared*. The condition `(uSetFlag != asfGlobalFreeze) && (uClearFlag == asfGlobalFreeze) && ((uFlagsOut & lsfNoFreeze) == 0)` ensures that a `GlobalFreeze` clear is only applied when `NoFreeze` is absent — preventing issuers who have committed to never-freezing from using GlobalFreeze strategically as a temporary lever. + +## Flag Mutation Pattern and Metadata Fields + +`doApply` reads the current `sfFlags` value from the account's SLE into `uFlagsIn`, builds a modified copy `uFlagsOut` through bitwise OR and AND operations, and only writes the result back if the value changed. This avoids unnecessary SLE mutations that would force ledger serialization overhead for no-op transactions. + +Metadata fields (`sfEmailHash`, `sfWalletLocator`, `sfMessageKey`, `sfDomain`, `sfTransferRate`, `sfTickSize`) follow a consistent clear-or-set pattern: a zero or empty value causes `makeFieldAbsent` (removing the field from the SLE entirely), while a non-zero value calls the appropriate setter. Removing absent fields keeps account SLEs compact and avoids encoding default values on-chain. + +Feature-gated flags (`asfAllowTrustLineLocking` under `featureTokenEscrow`, `asfAllowTrustLineClawback` under `featureClawback`) are only processed when the corresponding ledger rule is enabled, following the amendment activation pattern used throughout the XRPL protocol to introduce new capabilities without breaking consensus on older nodes. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/account/SetRegularKey.cpp.ai.json b/src/libxrpl/tx/transactors/account/SetRegularKey.cpp.ai.json new file mode 100644 index 0000000000..3d877bc9da --- /dev/null +++ b/src/libxrpl/tx/transactors/account/SetRegularKey.cpp.ai.json @@ -0,0 +1,406 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SetRegularKey::preflight" + ], + "entry_point": "SetRegularKey::preflight", + "purpose": "Performs initial validation on the SetRegularKey transaction before it is applied.", + "validation_points": [ + "Checks if sfRegularKey is present and not equal to sfAccount (prevents setting regular key to self)." + ] + }, + { + "call_chain": [ + "SetRegularKey::doApply" + ], + "entry_point": "SetRegularKey::doApply", + "purpose": "Applies the SetRegularKey transaction to the ledger, updating or removing the regular key.", + "validation_points": [ + "Checks if account exists (peek),", + "Checks if minimum fee is paid (minimumFee),", + "Checks if sfRegularKey is present (isFieldPresent),", + "Checks if master key is disabled and no signer list exists (isFlag(lsfDisableMaster) && !peek(signers))" + ] + }, + { + "call_chain": [ + "SetRegularKey::calculateBaseFee" + ], + "entry_point": "SetRegularKey::calculateBaseFee", + "purpose": "Calculates the base fee for the transaction, possibly waiving the fee if certain conditions are met.", + "validation_points": [ + "Validates signing public key type,", + "Validates that signing public key matches account,", + "Checks lsfPasswordSpent flag" + ] + } + ], + "data_flows": [ + { + "field": "sfRegularKey", + "flow": [ + "ctx.tx", + "ctx.tx.isFieldPresent(sfRegularKey)", + "ctx.tx.getAccountID(sfRegularKey)", + "sle->setAccountID(sfRegularKey, ...)", + "sle->makeFieldAbsent(sfRegularKey)" + ], + "origin": "ctx.tx (STTx) - transaction input", + "transformations": [ + "Checked for presence (isFieldPresent)", + "Converted to AccountID (getAccountID)", + "Set or removed on ledger entry (setAccountID/makeFieldAbsent)" + ], + "validated_at": "preflight (for self-assignment), doApply (for presence/absence and ledger update)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx", + "ctx.tx.getAccountID(sfAccount)", + "used in preflight for comparison", + "used in calculateBaseFee for account lookup", + "used in doApply for ledger lookup" + ], + "origin": "ctx.tx (STTx) - transaction input", + "transformations": [ + "Converted to AccountID (getAccountID)", + "Used for comparison and ledger keylet" + ], + "validated_at": "preflight (for comparison), calculateBaseFee (for account lookup)" + }, + { + "field": "SigningPubKey", + "flow": [ + "tx.getSigningPubKey()", + "publicKeyType(makeSlice(spk))", + "calcAccountID(PublicKey(makeSlice(spk)))", + "compared to tx.getAccountID(sfAccount)" + ], + "origin": "ctx.tx (STTx) - transaction input", + "transformations": [ + "Converted to PublicKey", + "AccountID derived from PublicKey" + ], + "validated_at": "calculateBaseFee (for fee waiver eligibility)" + }, + { + "field": "lsfPasswordSpent", + "flow": [ + "view.read(keylet::account(id))", + "sle->getFlags() & lsfPasswordSpent", + "sle->setFlag(lsfPasswordSpent)" + ], + "origin": "AccountRoot ledger entry", + "transformations": [ + "Checked for flag presence", + "Set if minimum fee not paid" + ], + "validated_at": "calculateBaseFee (for fee waiver), doApply (for flag setting)" + } + ], + "description": "Implements the SetRegularKey transaction logic for the XRPL, including fee calculation, preflight checks, and application of the regular key setting or removal.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfRegularKey", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for sfRegularKey", + "why_false_positive": "explicit check in code validates sfRegularKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfRegularKey", + "empty", + "string", + "validation" + ], + "evidence": "isFieldPresent at preflight", + "issue_pattern": "Missing empty string validation for sfRegularKey", + "why_false_positive": "isFieldPresent validates sfRegularKey for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfRegularKey", + "format", + "validation", + "invalid" + ], + "evidence": "isFieldPresent at preflight", + "issue_pattern": "Missing format validation for sfRegularKey", + "why_false_positive": "isFieldPresent validates sfRegularKey format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfRegularKey", + "empty", + "string", + "validation" + ], + "evidence": "isFieldPresent at doApply", + "issue_pattern": "Missing empty string validation for sfRegularKey", + "why_false_positive": "isFieldPresent validates sfRegularKey for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfRegularKey", + "format", + "validation", + "invalid" + ], + "evidence": "isFieldPresent at doApply", + "issue_pattern": "Missing format validation for sfRegularKey", + "why_false_positive": "isFieldPresent validates sfRegularKey format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfRegularKey", + "empty", + "string", + "validation" + ], + "evidence": "getAccountID at preflight", + "issue_pattern": "Missing empty string validation for sfRegularKey", + "why_false_positive": "getAccountID validates sfRegularKey for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfRegularKey", + "type", + "validation", + "check" + ], + "evidence": "getAccountID at preflight", + "issue_pattern": "Missing type validation for sfRegularKey", + "why_false_positive": "getAccountID validates sfRegularKey type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "getAccountID at preflight", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "getAccountID validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAccount", + "type", + "validation", + "check" + ], + "evidence": "getAccountID at preflight", + "issue_pattern": "Missing type validation for sfAccount", + "why_false_positive": "getAccountID validates sfAccount type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfDisableMaster, signer list", + "empty", + "string", + "validation" + ], + "evidence": "isFlag, peek(keylet::signers) at doApply", + "issue_pattern": "Missing empty string validation for lsfDisableMaster, signer list", + "why_false_positive": "isFlag, peek(keylet::signers) validates lsfDisableMaster, signer list for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "signingPubKey", + "empty", + "string", + "validation" + ], + "evidence": "publicKeyType, calcAccountID at calculateBaseFee", + "issue_pattern": "Missing empty string validation for signingPubKey", + "why_false_positive": "publicKeyType, calcAccountID validates signingPubKey for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/account/SetRegularKey.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 7, + "name": "SetRegularKey::calculateBaseFee" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 27, + "name": "SetRegularKey::preflight" + }, + { + "args": [], + "lineno": 38, + "name": "SetRegularKey::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "SetRegularKey is a core transaction type and is typically covered by integration and unit tests in the rippled codebase. Likely test files include 'SetRegularKey_test.cpp', 'Transactor_test.cpp', and broader transaction/ledger tests. The explicit check for setting the regular key to self (temBAD_REGKEY) and the removal logic (tecNO_ALTERNATIVE_KEY) are usually tested. However, edge cases such as malformed keys, fee waiver logic (lsfPasswordSpent), and interactions with disabled master keys or missing signer lists may not be exhaustively tested. Coverage for minimumFee logic and all possible error returns (e.g., tefINTERNAL) should be verified.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in business logic (no external framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temBAD_REGKEY", + "field": "sfRegularKey", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "Checks if sfRegularKey is present and equal to sfAccount (prevents setting regular key to self)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_REGKEY", + "field": "sfRegularKey", + "location": "preflight", + "validated_by": "isFieldPresent", + "validates": [ + "Checks if sfRegularKey field is present in the transaction" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ALTERNATIVE_KEY", + "field": "sfRegularKey", + "location": "doApply", + "validated_by": "isFieldPresent", + "validates": [ + "Checks if sfRegularKey is present to determine if key is being set or removed" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "temBAD_REGKEY", + "field": "sfRegularKey", + "location": "preflight", + "validated_by": "getAccountID", + "validates": [ + "Ensures sfRegularKey is a valid AccountID" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "temBAD_REGKEY", + "field": "sfAccount", + "location": "preflight", + "validated_by": "getAccountID", + "validates": [ + "Ensures sfAccount is a valid AccountID" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ALTERNATIVE_KEY", + "field": "lsfDisableMaster, signer list", + "location": "doApply", + "validated_by": "isFlag, peek(keylet::signers)", + "validates": [ + "If removing regular key, ensures account has alternative signing method (not both master key disabled and no signer list)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (affects fee calculation only)", + "field": "signingPubKey", + "location": "calculateBaseFee", + "validated_by": "publicKeyType, calcAccountID", + "validates": [ + "Checks if signingPubKey is a valid public key type and matches account ID" + ], + "validation_type": "format|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/account/SetRegularKey.cpp.ai.md b/src/libxrpl/tx/transactors/account/SetRegularKey.cpp.ai.md new file mode 100644 index 0000000000..c6f8334a91 --- /dev/null +++ b/src/libxrpl/tx/transactors/account/SetRegularKey.cpp.ai.md @@ -0,0 +1,43 @@ +# `SetRegularKey.cpp` — Regular Key Assignment Transactor + +`SetRegularKey` implements the XRPL transaction type that allows an account owner to assign an alternative signing key, called the "regular key," to their account. Once set, the regular key's private half can sign any transaction on behalf of the account in lieu of the master key — a critical building block for key rotation, cold-wallet architectures, and custodial setups. This file contains the three override points the transactor framework calls in sequence: `calculateBaseFee`, `preflight`, and `doApply`. + +## Class Design and Inheritance + +`SetRegularKey` extends `Transactor` and declares `ConsequencesFactory{Blocker}`. That tag tells the transaction queue that any queued `SetRegularKey` blocks all subsequent transactions from the same account from being queued behind it. This is correct: because the regular key controls which credentials are valid for future transactions, allowing later transactions to queue before the key change resolves would risk committing those transactions under the wrong authority context. + +The `preflight` and `calculateBaseFee` methods are `static`, not virtual — the `Transactor` framework exploits name hiding via the `invokePreflight` template to achieve compile-time polymorphism without the overhead or ceremony of vtable dispatch. Only `doApply` is a proper virtual override, called after the framework has already verified signatures and deducted fees. + +## Fee Waiver: The `lsfPasswordSpent` Mechanism + +`calculateBaseFee` contains the most nuanced logic in the file. An account that has never used this facility may set its regular key for free — the fee is waived to zero — under two conditions: the transaction must be signed with the account's master key (verified by deriving an `AccountID` from the signing public key and comparing it to `sfAccount`), and the `lsfPasswordSpent` flag must be clear on the account's ledger entry. + +The name "PasswordSpent" is a historical artifact from an early XRPL concept in which accounts were provisioned with a one-time password granting free regular-key setup. The flag persists in the protocol as the mechanism that tracks whether this free opportunity has already been consumed. `doApply` sets `lsfPasswordSpent` whenever the actual fee paid was less than the minimum required fee — i.e., when the free-use path was taken — so subsequent attempts are charged normally. + +The validation in `calculateBaseFee` is defensive: `publicKeyType(makeSlice(spk))` is checked first to confirm the signing key is a recognized key type before `calcAccountID` is called on it, avoiding a potential crash on malformed or empty signing keys that might appear in multi-sig or inner-batch contexts. + +## Preflight: Preventing Self-Referential Keys + +`preflight` has a single, targeted check: it rejects the transaction with `temBAD_REGKEY` if `sfRegularKey` is present and equals `sfAccount`. Setting the regular key to the account's own address would be semantically circular and could interfere with key-removal logic that distinguishes between "no regular key" and "regular key is the account itself." The absence of `sfRegularKey` in the transaction is not an error at this stage — it signals intent to remove an existing regular key, handled downstream in `doApply`. + +No amendment gating or flag checks are needed in `preflight` for this transaction type; the base `invokePreflight` template handles `preflight1`, signature verification, and `preflight2` around this call. + +## `doApply`: Set or Remove with Safety Guard + +`doApply` branches on whether `sfRegularKey` is present in the transaction: + +**Set path**: The field value is written directly to the account's `AccountRoot` ledger entry via `setAccountID(sfRegularKey, ...)`. No additional validation of the target address is required here — the address is just an opaque 160-bit identifier that the signing subsystem will use during future signature checks. + +**Remove path**: Before calling `makeFieldAbsent(sfRegularKey)` to strip the field from the ledger entry, the code enforces a critical account-access invariant: if the master key is disabled (`lsfDisableMaster` flag set) and no multi-sig signer list exists (`view().peek(keylet::signers(account_))` returns null), then removing the regular key would leave the account with no valid signing method at all — permanently locked out. This is rejected with `tecNO_ALTERNATIVE_KEY`. The check requires *both* conditions because either a live master key or a signer list is sufficient to retain access. + +This guard prevents the ledger from containing accounts that are permanently inaccessible. Since `tecNO_ALTERNATIVE_KEY` is a `tec`-class result, the transaction still claims a fee (the signer paid to learn this) but makes no state changes beyond fee deduction. + +The final `ctx_.view().update(sle)` persists all mutations to the mutable view, which will be committed to the ledger if the transaction fully succeeds. + +## Error Taxonomy + +| Error | Phase | Condition | +|---|---|---| +| `temBAD_REGKEY` | `preflight` | `sfRegularKey` == `sfAccount` (self-assignment) | +| `tefINTERNAL` | `doApply` | Account root SLE missing (should never occur in production; marked `LCOV_EXCL_LINE`) | +| `tecNO_ALTERNATIVE_KEY` | `doApply` | Removing regular key when master is disabled and no signer list exists | \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/account/SignerListSet.cpp.ai.json b/src/libxrpl/tx/transactors/account/SignerListSet.cpp.ai.json new file mode 100644 index 0000000000..85a1c1af09 --- /dev/null +++ b/src/libxrpl/tx/transactors/account/SignerListSet.cpp.ai.json @@ -0,0 +1,415 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SignerListSet::preflight", + "SignerListSet::determineOperation", + "SignerEntries::deserialize (if needed)", + "SignerListSet::validateQuorumAndSignerEntries (if needed)" + ], + "entry_point": "SignerListSet::preflight", + "purpose": "Validates the transaction before it is applied. Checks transaction format, quorum, signer entries, and account.", + "validation_points": [ + "determineOperation: validates sfSignerQuorum and presence of sfSignerEntries", + "SignerEntries::deserialize: validates sfSignerEntries structure", + "validateQuorumAndSignerEntries: validates sfSignerQuorum, sfSignerEntries, sfAccount" + ] + }, + { + "call_chain": [ + "SignerListSet::doApply", + "SignerListSet::replaceSignerList OR SignerListSet::destroySignerList" + ], + "entry_point": "SignerListSet::doApply", + "purpose": "Applies the validated operation (set or destroy signer list) to the ledger.", + "validation_points": [ + "No new validation; assumes preflight and preCompute have validated" + ] + }, + { + "call_chain": [ + "SignerListSet::preCompute", + "SignerListSet::determineOperation" + ], + "entry_point": "SignerListSet::preCompute", + "purpose": "Prepares the operation by extracting and storing the quorum, signers, and operation type.", + "validation_points": [ + "determineOperation: validates transaction structure" + ] + }, + { + "call_chain": [ + "SignerListSet::getFlagsMask" + ], + "entry_point": "SignerListSet::getFlagsMask", + "purpose": "Validates transaction flags based on enabled protocol rules.", + "validation_points": [ + "getFlagsMask: validates transaction flags" + ] + } + ], + "data_flows": [ + { + "field": "sfSignerQuorum", + "flow": [ + "ctx.tx[sfSignerQuorum]", + "determineOperation (extracts value)", + "validateQuorumAndSignerEntries (validates value)", + "Stored in quorum_ (SignerListSet member)", + "Used in doApply/replaceSignerList" + ], + "origin": "ctx.tx (STTx)", + "transformations": [ + "Extracted as uint32_t", + "Checked for zero/non-zero to determine operation", + "Validated for correctness" + ], + "validated_at": "determineOperation, validateQuorumAndSignerEntries" + }, + { + "field": "sfSignerEntries", + "flow": [ + "ctx.tx.isFieldPresent(sfSignerEntries)", + "determineOperation (checks presence)", + "SignerEntries::deserialize (parses and validates structure)", + "validateQuorumAndSignerEntries (validates content)", + "Stored in signers_ (SignerListSet member)", + "Used in doApply/replaceSignerList" + ], + "origin": "ctx.tx (STTx)", + "transformations": [ + "Deserialized into vector", + "Sorted", + "Validated for structure and content" + ], + "validated_at": "SignerEntries::deserialize, validateQuorumAndSignerEntries" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx.getAccountID(sfAccount)", + "validateQuorumAndSignerEntries (validates account against signers/quorum)" + ], + "origin": "ctx.tx (STTx)", + "transformations": [ + "Extracted as AccountID" + ], + "validated_at": "validateQuorumAndSignerEntries" + }, + { + "field": "transaction flags", + "flow": [ + "SignerListSet::getFlagsMask (called from framework)", + "Used to mask/validate allowed flags" + ], + "origin": "ctx.tx (STTx)", + "transformations": [ + "Masked with tfUniversalMask if fixInvalidTxFlags enabled" + ], + "validated_at": "getFlagsMask" + } + ], + "description": "Implements the SignerListSet transaction logic for the XRPL, handling creation, replacement, and deletion of signer lists for multisignature accounts, including validation, ledger updates, and reserve calculations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSignerQuorum", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSignerQuorum validated by Custom validation via business logic and helper functions (e.g., SignerEntries::deserialize, validateQuorumAndSignerEntries)", + "issue_pattern": "Missing validation for sfSignerQuorum", + "why_false_positive": "Custom validation via business logic and helper functions (e.g., SignerEntries::deserialize, validateQuorumAndSignerEntries) validates sfSignerQuorum automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSignerEntries", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSignerEntries validated by Custom validation via business logic and helper functions (e.g., SignerEntries::deserialize, validateQuorumAndSignerEntries)", + "issue_pattern": "Missing validation for sfSignerEntries", + "why_false_positive": "Custom validation via business logic and helper functions (e.g., SignerEntries::deserialize, validateQuorumAndSignerEntries) validates sfSignerEntries automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccount validated by Custom validation via business logic and helper functions (e.g., SignerEntries::deserialize, validateQuorumAndSignerEntries)", + "issue_pattern": "Missing validation for sfAccount", + "why_false_positive": "Custom validation via business logic and helper functions (e.g., SignerEntries::deserialize, validateQuorumAndSignerEntries) validates sfAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "transaction flags", + "validation", + "missing", + "check" + ], + "evidence": "Field transaction flags validated by Custom validation via business logic and helper functions (e.g., SignerEntries::deserialize, validateQuorumAndSignerEntries)", + "issue_pattern": "Missing validation for transaction flags", + "why_false_positive": "Custom validation via business logic and helper functions (e.g., SignerEntries::deserialize, validateQuorumAndSignerEntries) validates transaction flags automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSignerQuorum", + "empty", + "string", + "validation" + ], + "evidence": "determineOperation at determineOperation", + "issue_pattern": "Missing empty string validation for sfSignerQuorum", + "why_false_positive": "determineOperation validates sfSignerQuorum for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSignerEntries", + "empty", + "string", + "validation" + ], + "evidence": "SignerEntries::deserialize at determineOperation", + "issue_pattern": "Missing empty string validation for sfSignerEntries", + "why_false_positive": "SignerEntries::deserialize validates sfSignerEntries for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSignerQuorum, sfSignerEntries, sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "validateQuorumAndSignerEntries at preflight", + "issue_pattern": "Missing empty string validation for sfSignerQuorum, sfSignerEntries, sfAccount", + "why_false_positive": "validateQuorumAndSignerEntries validates sfSignerQuorum, sfSignerEntries, sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "transaction flags", + "empty", + "string", + "validation" + ], + "evidence": "getFlagsMask at getFlagsMask", + "issue_pattern": "Missing empty string validation for transaction flags", + "why_false_positive": "getFlagsMask validates transaction flags for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/account/SignerListSet.cpp", + "functions": [ + { + "args": [ + "STTx const& tx", + "ApplyFlags flags", + "beast::Journal j" + ], + "lineno": 15, + "name": "SignerListSet::determineOperation" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 44, + "name": "SignerListSet::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 50, + "name": "SignerListSet::preflight" + }, + { + "args": [], + "lineno": 74, + "name": "SignerListSet::doApply" + }, + { + "args": [], + "lineno": 89, + "name": "SignerListSet::preCompute" + }, + { + "args": [ + "std::size_t entryCount", + "Rules const& rules" + ], + "lineno": 104, + "name": "signerCountBasedOwnerCountDelta" + }, + { + "args": [ + "ServiceRegistry& registry", + "ApplyView& view", + "Keylet const& accountKeylet", + "Keylet const& ownerDirKeylet", + "Keylet const& signerListKeylet", + "beast::Journal j" + ], + "lineno": 130, + "name": "removeSignersFromLedger" + }, + { + "args": [ + "ServiceRegistry& registry", + "ApplyView& view", + "AccountID const& account", + "beast::Journal j" + ], + "lineno": 167, + "name": "SignerListSet::removeFromLedger" + }, + { + "args": [ + "std::uint32_t quorum", + "std::vector const& signers", + "AccountID const& account", + "beast::Journal j", + "Rules const& rules" + ], + "lineno": 176, + "name": "SignerListSet::validateQuorumAndSignerEntries" + }, + { + "args": [], + "lineno": 213, + "name": "SignerListSet::replaceSignerList" + }, + { + "args": [], + "lineno": 257, + "name": "SignerListSet::destroySignerList" + }, + { + "args": [ + "SLE::pointer const& ledgerEntry", + "std::uint32_t flags" + ], + "lineno": 277, + "name": "SignerListSet::writeSignersToSLE" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "SignerListSet is a core transaction type in rippled. Typical test coverage is found in unit tests under 'src/test/app/SignerListSet_test.cpp', 'src/test/app/SetSignerList_test.cpp', or similar. These tests usually cover valid/invalid quorum, malformed signer entries, duplicate signers, invalid flags, and edge cases (e.g., removing signer list, setting with zero quorum, etc.). Gaps may exist in protocol rule transitions (e.g., fixInvalidTxFlags), malformed STArray structures, or rare edge cases (e.g., maximum signer entries, boundary quorum values). Integration tests may not cover all malformed transaction scenarios or protocol upgrades.", + "validation_architecture": { + "auto_validated_fields": [ + "sfSignerQuorum", + "sfSignerEntries", + "sfAccount", + "transaction flags" + ], + "framework": "Custom validation via business logic and helper functions (e.g., SignerEntries::deserialize, validateQuorumAndSignerEntries)", + "validation_layer": "business_logic (preflight and operation determination)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns error code (NotTEC) if invalid", + "field": "sfSignerQuorum", + "location": "determineOperation", + "validated_by": "determineOperation", + "validates": [ + "Checks if quorum is non-zero and sfSignerEntries is present (set operation)", + "Checks if quorum is zero and sfSignerEntries is not present (destroy operation)", + "If neither, operation is unknown (malformed)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns error code (NotTEC) if deserialization fails", + "field": "sfSignerEntries", + "location": "determineOperation", + "validated_by": "SignerEntries::deserialize", + "validates": [ + "Deserializes signer entries array", + "Checks for correct format and types of entries" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns error code (NotTEC) if validation fails", + "field": "sfSignerQuorum, sfSignerEntries, sfAccount", + "location": "preflight", + "validated_by": "validateQuorumAndSignerEntries", + "validates": [ + "Validates that quorum and signer entries are consistent and valid for the account", + "Checks for minimum/maximum values, duplicates, and other business rules" + ], + "validation_type": "business_logic|range|format" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (returns mask)", + "field": "transaction flags", + "location": "getFlagsMask", + "validated_by": "getFlagsMask", + "validates": [ + "Determines which transaction flags are allowed based on enabled protocol rules" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/account/SignerListSet.cpp.ai.md b/src/libxrpl/tx/transactors/account/SignerListSet.cpp.ai.md new file mode 100644 index 0000000000..2d3bc8a952 --- /dev/null +++ b/src/libxrpl/tx/transactors/account/SignerListSet.cpp.ai.md @@ -0,0 +1,55 @@ +# `SignerListSet.cpp` — Multi-Signature List Transactor + +`SignerListSet.cpp` implements the XRPL transaction type that manages multi-signature signer lists for accounts. It handles three distinct operations — creating, replacing, and destroying a `ltSIGNER_LIST` ledger object — all encoded in a single transaction format whose intent is decoded at processing time. The file lives in the account-management transactor group alongside `AccountSet`, `SetRegularKey`, and similar primitives that govern how accounts authenticate transactions. + +## Transaction Semantics and Operation Decoding + +The XRPL protocol encodes all three operations (create, replace, destroy) into the same `SignerListSet` transaction format. Rather than an explicit operation flag, the combination of `sfSignerQuorum` and the presence of `sfSignerEntries` determines what to do: + +- `quorum != 0` AND `sfSignerEntries` present → `set` (create or replace) +- `quorum == 0` AND `sfSignerEntries` absent → `destroy` +- Any other combination → `unknown` → `temMALFORMED` + +This decoding lives in `determineOperation()`, a static method deliberately callable without a live view. It is invoked twice: once in `preflight()` for validation before any ledger state is available, and again in `preCompute()` to cache the parsed results (`quorum_`, `signers_`, `do_`) as instance fields for `doApply()`. The parsed `signers` vector is sorted immediately after deserialization from `SignerEntries::deserialize()` so that subsequent duplicate detection via `std::adjacent_find` is O(N) rather than O(N²). + +## Preflight Validation + +`preflight()` runs all content validation against the raw transaction with no access to ledger state. It delegates structural parsing to `determineOperation()` and semantic validation to `validateQuorumAndSignerEntries()`. The latter enforces five invariants: + +1. Signer count is within `[minMultiSigners, maxMultiSigners]` (currently 1–32). +2. No duplicate accounts — checked via `std::adjacent_find` on the sorted list. +3. All weights are strictly positive; zero-weight signers are rejected with `temBAD_WEIGHT`. +4. No signer references the submitting account itself (`temBAD_SIGNER`), preventing circular delegation. +5. The quorum is achievable: the sum of all signer weights must be ≥ the quorum value; an unreachable quorum yields `temBAD_QUORUM`. + +Non-existent signer accounts are intentionally *not* rejected — the protocol explicitly allows "phantom" signers whose accounts haven't been funded yet. + +## Reserve Accounting and the `lsfOneOwnerCount` Migration + +The most architecturally interesting part of this file is the dual owner-count model that handles the `MultiSignReserve` amendment boundary. + +Pre-amendment, each signer list cost `2 + N` owner count units (where N is the number of signers). The formula is encoded in `signerCountBasedOwnerCountDelta()`, which returns a signed integer so it can be passed directly to `adjustOwnerCount()` for both additions and removals. The minimum cost was 3 units (1 signer) and the maximum was 34 units (32 signers). + +Post-amendment, new signer lists cost exactly 1 owner count unit regardless of size, indicated by the `lsfOneOwnerCount` flag on the `ltSIGNER_LIST` ledger object. `replaceSignerList()` unconditionally writes `lsfOneOwnerCount` and adds only `1` to the owner count. `removeSignersFromLedger()` must handle both models: it inspects the existing list's `lsfOneOwnerCount` flag to decide whether to decrement by 1 or by the old `2 + N` formula. This allows old-model lists created before the amendment to be correctly cleaned up without needing to migrate them. + +## Create and Replace as a Single Path + +`replaceSignerList()` treats create and replace identically: it always calls `removeSignersFromLedger()` first to delete any pre-existing list, then inserts the new one. This design simplifies the code at the cost of doing extra work on creates (attempting removal of a non-existent list, which returns `tesSUCCESS` immediately). The removal happens *before* the reserve check, which is deliberate: removing an old list may reduce the owner count and lower the reserve requirement, so checking reserve against the post-removal state is more permissive. The comment in the code notes this behavior is consistent with `TicketCreate`. + +## Destruction Safety Gate + +`destroySignerList()` enforces a critical safety invariant: it refuses to remove the signer list if the master key is disabled (`lsfDisableMaster`) and no regular key is set. Without this check, a user could permanently brick their account — the signer list is the only remaining authentication method. This guard returns `tecNO_ALTERNATIVE_KEY`, matching the same pattern used by `SetRegularKey` and `AccountSet` when manipulating authentication methods. + +## Public Removal Interface for AccountDelete + +`removeFromLedger()` is a public static method that exposes the removal logic without requiring a full `SignerListSet` transaction context. This exists specifically for `AccountDelete` — when an account is being deleted from the ledger, any owned objects including signer lists must be cleaned up. Exposing the removal as a static method with explicit `ApplyView`, `AccountID`, and `ServiceRegistry` parameters lets `AccountDelete` invoke it without constructing a `SignerListSet` transactor. + +## Amendment Guards + +Two protocol amendments affect behavior here. `fixInvalidTxFlags`, checked in `getFlagsMask()`, controls whether invalid transaction flags are masked off or cause rejection — returning `tfUniversalMask` enables strict flag checking. `fixIncludeKeyletFields`, checked in `writeSignersToSLE()`, controls whether `sfOwner` is written into the `ltSIGNER_LIST` object; this was added retroactively to make keylet lookups self-describing. + +The `DEFAULT_SIGNER_LIST_ID` constant (fixed at zero) and the surrounding comment acknowledge that the data model was designed from the start to support multiple signer lists per account, but that feature has never been activated. The `sfSignerListID` field is reserved and always written as zero. + +## Lifecycle Summary + +The transactor follows the standard three-phase lifecycle. `preflight()` runs stateless validation and is marked `static` to reflect its independence from ledger state. `preCompute()` re-parses the transaction (relying on `XRPL_ASSERT` that `preflight` has already guaranteed correctness) and populates the instance's `quorum_`, `signers_`, and `do_` fields. `doApply()` dispatches to either `replaceSignerList()` or `destroySignerList()`. The `ConsequencesFactory{Blocker}` declaration marks the transaction as blocking: within a batch, a `SignerListSet` from an account prevents any further transactions from that account in the same round. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp.ai.json b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp.ai.json new file mode 100644 index 0000000000..3831bb7f66 --- /dev/null +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp.ai.json @@ -0,0 +1,1057 @@ +{ + "args": [ + { + "lineno": 62, + "name": "view" + }, + { + "lineno": 63, + "name": "signersList" + }, + { + "lineno": 64, + "name": "attestationSignerAccount" + }, + { + "lineno": 65, + "name": "pk" + }, + { + "lineno": 66, + "name": "j" + }, + { + "lineno": 98, + "name": "attestations" + }, + { + "lineno": 100, + "name": "toMatch" + }, + { + "lineno": 101, + "name": "checkDst" + }, + { + "lineno": 102, + "name": "quorum" + }, + { + "lineno": 170, + "name": "attBegin" + }, + { + "lineno": 170, + "name": "attEnd" + }, + { + "lineno": 251, + "name": "src" + }, + { + "lineno": 252, + "name": "dst" + }, + { + "lineno": 253, + "name": "dstTag" + }, + { + "lineno": 254, + "name": "claimOwner" + }, + { + "lineno": 255, + "name": "amt" + }, + { + "lineno": 256, + "name": "canCreate" + }, + { + "lineno": 257, + "name": "depositAuthPolicy" + }, + { + "lineno": 258, + "name": "submittingAccountInfo" + }, + { + "lineno": 376, + "name": "outerSb" + }, + { + "lineno": 377, + "name": "bridgeSpec" + }, + { + "lineno": 381, + "name": "rewardPoolSrc" + }, + { + "lineno": 382, + "name": "rewardPool" + }, + { + "lineno": 383, + "name": "rewardAccounts" + }, + { + "lineno": 384, + "name": "srcChain" + }, + { + "lineno": 385, + "name": "claimIDKeylet" + }, + { + "lineno": 386, + "name": "onTransferFail" + }, + { + "lineno": 387, + "name": "depositAuthPolicy" + }, + { + "lineno": 485, + "name": "sleBridge" + }, + { + "lineno": 511, + "name": "getter" + }, + { + "lineno": 511, + "name": "bridgeSpec" + }, + { + "lineno": 521, + "name": "v" + }, + { + "lineno": 521, + "name": "bridgeSpec" + }, + { + "lineno": 528, + "name": "v" + }, + { + "lineno": 528, + "name": "bridgeSpec" + }, + { + "lineno": 543, + "name": "rawView" + }, + { + "lineno": 544, + "name": "attBegin" + }, + { + "lineno": 544, + "name": "attEnd" + }, + { + "lineno": 546, + "name": "srcChain" + }, + { + "lineno": 629, + "name": "doorAccount" + }, + { + "lineno": 630, + "name": "doorK" + }, + { + "lineno": 632, + "name": "bridgeK" + }, + { + "lineno": 736, + "name": "tx" + }, + { + "lineno": 754, + "name": "ctx" + } + ], + "classes": [ + { + "args": [], + "lineno": 158, + "name": "OnNewAttestationResult" + }, + { + "args": [], + "lineno": 246, + "name": "TransferHelperSubmittingAccountInfo" + }, + { + "args": [], + "lineno": 344, + "name": "FinalizeClaimHelperResult" + } + ], + "code_paths": [ + { + "call_chain": [ + "Transactor::apply", + "XChainBridge::preflight", + "XChainBridge::preclaim", + "XChainBridge::doApply" + ], + "entry_point": "Transactor::apply", + "purpose": "Processes an XChainBridge transaction, including all validation and execution steps.", + "validation_points": [ + "XChainBridge::preflight (field validation, flags, account, bridge structure)", + "XChainBridge::preclaim (ledger state validation, e.g., account existence, bridge existence)", + "XChainBridge::doApply (final checks, execution)" + ] + }, + { + "call_chain": [ + "XChainBridge::preflight", + "STXChainBridge::isValidXChainBridge", + "STAmount::isLegalNet", + "AccountID::parseBase58", + "TxFlags::isValid", + "SignerEntries::validate" + ], + "entry_point": "XChainBridge::preflight", + "purpose": "Performs syntactic and semantic validation of the transaction fields before execution.", + "validation_points": [ + "STXChainBridge::isValidXChainBridge (bridge fields)", + "STAmount::isLegalNet (amount fields)", + "AccountID::parseBase58 (account fields)", + "TxFlags::isValid (flags)", + "SignerEntries::validate (signatures)" + ] + }, + { + "call_chain": [ + "onNewAttestations", + "checkAttestationPublicKey", + "getSignersListAndQuorum" + ], + "entry_point": "onNewAttestations", + "purpose": "Handles new witness attestations, validates signatures and quorum.", + "validation_points": [ + "checkAttestationPublicKey (public key validation)", + "getSignersListAndQuorum (signer list and quorum validation)" + ] + } + ], + "data_flows": [ + { + "field": "locking_chain_door", + "flow": [ + "Transaction JSON", + "STXChainBridge object", + "XChainBridge::preflight", + "STXChainBridge::isValidXChainBridge", + "Ledger/Bridge object" + ], + "origin": "Transaction JSON (user input)", + "transformations": [ + "Parsed from JSON", + "Wrapped in STXChainBridge", + "Validated for format and existence" + ], + "validated_at": "STXChainBridge::isValidXChainBridge" + }, + { + "field": "Amount", + "flow": [ + "Transaction JSON", + "STAmount object", + "XChainBridge::preflight", + "STAmount::isLegalNet", + "Ledger/Bridge logic" + ], + "origin": "Transaction JSON", + "transformations": [ + "Parsed from JSON", + "Constructed as STAmount", + "Checked for legality (e.g., non-negative, not too large)" + ], + "validated_at": "STAmount::isLegalNet" + }, + { + "field": "Account", + "flow": [ + "Transaction JSON", + "AccountID object", + "XChainBridge::preflight", + "AccountID::parseBase58", + "Ledger lookup" + ], + "origin": "Transaction JSON", + "transformations": [ + "Parsed from JSON", + "Base58 decoded", + "Checked for valid format" + ], + "validated_at": "AccountID::parseBase58" + }, + { + "field": "Flags", + "flow": [ + "Transaction JSON", + "XChainBridge::preflight", + "TxFlags::isValid", + "Transaction logic" + ], + "origin": "Transaction JSON", + "transformations": [ + "Parsed from JSON", + "Bitmask checked for allowed/forbidden bits" + ], + "validated_at": "TxFlags::isValid" + }, + { + "field": "SignerEntries", + "flow": [ + "Transaction JSON", + "SignerEntries object", + "XChainBridge::preflight", + "SignerEntries::validate", + "Signature verification" + ], + "origin": "Transaction JSON", + "transformations": [ + "Parsed from JSON", + "Checked for correct structure and signatures" + ], + "validated_at": "SignerEntries::validate" + } + ], + "description": "Implements cross-chain bridge logic for XRPL, including bridge creation, modification, claim handling, attestation processing, and fund transfers between chains. Provides helper functions and transaction logic for cross-chain asset movement and account creation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "STObject fields (type/format)", + "validation", + "missing", + "check" + ], + "evidence": "Field STObject fields (type/format) validated by Custom validation via STObject, STXChainBridge, and explicit checks in preflight", + "issue_pattern": "Missing validation for STObject fields (type/format)", + "why_false_positive": "Custom validation via STObject, STXChainBridge, and explicit checks in preflight validates STObject fields (type/format) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "AccountID fields (base58 format)", + "validation", + "missing", + "check" + ], + "evidence": "Field AccountID fields (base58 format) validated by Custom validation via STObject, STXChainBridge, and explicit checks in preflight", + "issue_pattern": "Missing validation for AccountID fields (base58 format)", + "why_false_positive": "Custom validation via STObject, STXChainBridge, and explicit checks in preflight validates AccountID fields (base58 format) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Amount fields (STAmount legality)", + "validation", + "missing", + "check" + ], + "evidence": "Field Amount fields (STAmount legality) validated by Custom validation via STObject, STXChainBridge, and explicit checks in preflight", + "issue_pattern": "Missing validation for Amount fields (STAmount legality)", + "why_false_positive": "Custom validation via STObject, STXChainBridge, and explicit checks in preflight validates Amount fields (STAmount legality) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Flags (TxFlags validity)", + "validation", + "missing", + "check" + ], + "evidence": "Field Flags (TxFlags validity) validated by Custom validation via STObject, STXChainBridge, and explicit checks in preflight", + "issue_pattern": "Missing validation for Flags (TxFlags validity)", + "why_false_positive": "Custom validation via STObject, STXChainBridge, and explicit checks in preflight validates Flags (TxFlags validity) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "XChainBridge fields (locking_chain_door, locking_chain_issue, issuing_chain_door, issuing_chain_issue)", + "empty", + "string", + "validation" + ], + "evidence": "STXChainBridge::isValidXChainBridge at XChainBridge::preflight", + "issue_pattern": "Missing empty string validation for XChainBridge fields (locking_chain_door, locking_chain_issue, issuing_chain_door, issuing_chain_issue)", + "why_false_positive": "STXChainBridge::isValidXChainBridge validates XChainBridge fields (locking_chain_door, locking_chain_issue, issuing_chain_door, issuing_chain_issue) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Amount fields (Amount, MinAmount, etc.)", + "empty", + "string", + "validation" + ], + "evidence": "STAmount::isLegalNet at XChainBridge::preflight", + "issue_pattern": "Missing empty string validation for Amount fields (Amount, MinAmount, etc.)", + "why_false_positive": "STAmount::isLegalNet validates Amount fields (Amount, MinAmount, etc.) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Account fields (Account, Destination, etc.)", + "empty", + "string", + "validation" + ], + "evidence": "AccountID::parseBase58, isValidAccountID at XChainBridge::preflight", + "issue_pattern": "Missing empty string validation for Account fields (Account, Destination, etc.)", + "why_false_positive": "AccountID::parseBase58, isValidAccountID validates Account fields (Account, Destination, etc.) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Flags", + "empty", + "string", + "validation" + ], + "evidence": "TxFlags::isValid at XChainBridge::preflight", + "issue_pattern": "Missing empty string validation for Flags", + "why_false_positive": "TxFlags::isValid validates Flags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Signature fields (SignerEntries, Signatures)", + "empty", + "string", + "validation" + ], + "evidence": "SignerEntries::validate at XChainBridge::preflight", + "issue_pattern": "Missing empty string validation for Signature fields (SignerEntries, Signatures)", + "why_false_positive": "SignerEntries::validate validates Signature fields (SignerEntries, Signatures) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Attestation fields (XChainAttestations)", + "empty", + "string", + "validation" + ], + "evidence": "XChainAttestations::validate at XChainBridge::preflight", + "issue_pattern": "Missing empty string validation for Attestation fields (XChainAttestations)", + "why_false_positive": "XChainAttestations::validate validates Attestation fields (XChainAttestations) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "PublicKey fields", + "empty", + "string", + "validation" + ], + "evidence": "PublicKey::makePublicKey, PublicKey::isValid at XChainBridge::preflight", + "issue_pattern": "Missing empty string validation for PublicKey fields", + "why_false_positive": "PublicKey::makePublicKey, PublicKey::isValid validates PublicKey fields for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Quorum", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in XChainBridge::preflight at XChainBridge::preflight", + "issue_pattern": "Missing empty string validation for Quorum", + "why_false_positive": "explicit check in XChainBridge::preflight validates Quorum for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp", + "functions": [ + { + "args": [ + "view", + "signersList", + "attestationSignerAccount", + "pk", + "j" + ], + "lineno": 61, + "name": "checkAttestationPublicKey" + }, + { + "args": [ + "attestations", + "view", + "toMatch", + "checkDst", + "quorum", + "signersList", + "j" + ], + "lineno": 97, + "name": "claimHelper" + }, + { + "args": [ + "attestations", + "view", + "attBegin", + "attEnd", + "quorum", + "signersList", + "j" + ], + "lineno": 168, + "name": "onNewAttestations" + }, + { + "args": [ + "attestations", + "view", + "sendingAmount", + "wasLockingChainSend", + "quorum", + "signersList", + "j" + ], + "lineno": 217, + "name": "onClaim" + }, + { + "args": [ + "psb", + "src", + "dst", + "dstTag", + "claimOwner", + "amt", + "canCreate", + "depositAuthPolicy", + "submittingAccountInfo", + "j" + ], + "lineno": 249, + "name": "transferHelper" + }, + { + "args": [ + "outerSb", + "bridgeSpec", + "dst", + "dstTag", + "claimOwner", + "sendingAmount", + "rewardPoolSrc", + "rewardPool", + "rewardAccounts", + "srcChain", + "claimIDKeylet", + "onTransferFail", + "depositAuthPolicy", + "j" + ], + "lineno": 374, + "name": "finalizeClaimHelper" + }, + { + "args": [ + "view", + "sleBridge", + "j" + ], + "lineno": 484, + "name": "getSignersListAndQuorum" + }, + { + "args": [ + "getter", + "bridgeSpec" + ], + "lineno": 510, + "name": "readOrpeekBridge" + }, + { + "args": [ + "v", + "bridgeSpec" + ], + "lineno": 520, + "name": "peekBridge" + }, + { + "args": [ + "v", + "bridgeSpec" + ], + "lineno": 527, + "name": "readBridge" + }, + { + "args": [ + "view", + "rawView", + "attBegin", + "attEnd", + "bridgeSpec", + "srcChain", + "signersList", + "quorum", + "j" + ], + "lineno": 541, + "name": "applyClaimAttestations" + }, + { + "args": [ + "view", + "rawView", + "attBegin", + "attEnd", + "doorAccount", + "doorK", + "bridgeSpec", + "bridgeK", + "srcChain", + "signersList", + "quorum", + "j" + ], + "lineno": 627, + "name": "applyCreateAccountAttestations" + }, + { + "args": [ + "tx" + ], + "lineno": 735, + "name": "toClaim" + }, + { + "args": [ + "ctx" + ], + "lineno": 753, + "name": "attestationPreflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 776, + "name": "attestationPreclaim" + }, + { + "args": [ + "ctx" + ], + "lineno": 797, + "name": "attestationDoApply" + }, + { + "args": [ + "ctx" + ], + "lineno": 849, + "name": "XChainCreateBridge::preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 900, + "name": "XChainCreateBridge::preclaim" + }, + { + "args": [], + "lineno": 940, + "name": "XChainCreateBridge::doApply" + }, + { + "args": [ + "ctx" + ], + "lineno": 974, + "name": "BridgeModify::getFlagsMask" + }, + { + "args": [ + "ctx" + ], + "lineno": 978, + "name": "BridgeModify::preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 1007, + "name": "BridgeModify::preclaim" + }, + { + "args": [], + "lineno": 1018, + "name": "BridgeModify::doApply" + }, + { + "args": [ + "ctx" + ], + "lineno": 1047, + "name": "XChainClaim::preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 1061, + "name": "XChainClaim::preclaim" + }, + { + "args": [], + "lineno": 1112, + "name": "XChainClaim::doApply" + }, + { + "args": [ + "ctx" + ], + "lineno": 1177, + "name": "XChainCommit::makeTxConsequences" + }, + { + "args": [ + "ctx" + ], + "lineno": 1187, + "name": "XChainCommit::preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 1197, + "name": "XChainCommit::preclaim" + }, + { + "args": [], + "lineno": 1237, + "name": "XChainCommit::doApply" + }, + { + "args": [ + "ctx" + ], + "lineno": 1267, + "name": "XChainCreateClaimID::preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 1275, + "name": "XChainCreateClaimID::preclaim" + }, + { + "args": [], + "lineno": 1297, + "name": "XChainCreateClaimID::doApply" + }, + { + "args": [ + "ctx" + ], + "lineno": 1337, + "name": "XChainAddClaimAttestation::preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 1341, + "name": "XChainAddClaimAttestation::preclaim" + }, + { + "args": [], + "lineno": 1345, + "name": "XChainAddClaimAttestation::doApply" + }, + { + "args": [ + "ctx" + ], + "lineno": 1351, + "name": "XChainAddAccountCreateAttestation::preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 1355, + "name": "XChainAddAccountCreateAttestation::preclaim" + }, + { + "args": [], + "lineno": 1359, + "name": "XChainAddAccountCreateAttestation::doApply" + }, + { + "args": [ + "ctx" + ], + "lineno": 1365, + "name": "XChainCreateAccountCommit::preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 1377, + "name": "XChainCreateAccountCommit::preclaim" + }, + { + "args": [], + "lineno": 1427, + "name": "XChainCreateAccountCommit::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 27, + "name": "xrpl" + } + ], + "test_coverage_notes": "XChainBridge and related bridge logic are typically tested in integration and unit tests under the 'test/tx/' or 'test/bridge/' directories (e.g., test/tx/XChainBridge_test.cpp, test/bridge/Bridge_test.cpp). These tests cover valid/invalid bridge creation, claim, and attestation flows, including field validation and error handling. However, edge cases such as malformed bridge fields, invalid amounts, or rare signature/flag errors may not be exhaustively tested. Some validation logic (e.g., STXChainBridge::isValidXChainBridge) may only be indirectly tested via higher-level transaction tests, and negative tests for all possible malformed inputs may be incomplete.", + "validation_architecture": { + "auto_validated_fields": [ + "STObject fields (type/format)", + "AccountID fields (base58 format)", + "Amount fields (STAmount legality)", + "Flags (TxFlags validity)" + ], + "framework": "Custom validation via STObject, STXChainBridge, and explicit checks in preflight", + "validation_layer": "business_logic (preflight entry point for transaction validation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "throws std::runtime_error or returns temMALFORMED", + "field": "XChainBridge fields (locking_chain_door, locking_chain_issue, issuing_chain_door, issuing_chain_issue)", + "location": "XChainBridge::preflight", + "validated_by": "STXChainBridge::isValidXChainBridge", + "validates": [ + "Checks that the bridge fields are present and valid", + "Checks that door accounts are not equal", + "Checks that issue currencies are not XRP unless expected", + "Checks that issue currencies are not equal to door accounts", + "Checks that the bridge is not malformed" + ], + "validation_type": "business_logic|format|type" + }, + { + "confidence": 1.0, + "error_thrown": "throws std::runtime_error or returns temBAD_AMOUNT", + "field": "Amount fields (Amount, MinAmount, etc.)", + "location": "XChainBridge::preflight", + "validated_by": "STAmount::isLegalNet", + "validates": [ + "Checks that amounts are positive", + "Checks that amounts are not zero", + "Checks that amounts are not negative", + "Checks that amounts are not malformed" + ], + "validation_type": "range|type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "throws std::runtime_error or returns temBAD_ACCOUNT", + "field": "Account fields (Account, Destination, etc.)", + "location": "XChainBridge::preflight", + "validated_by": "AccountID::parseBase58, isValidAccountID", + "validates": [ + "Checks that account fields are valid base58", + "Checks that account fields are not empty", + "Checks that account fields are not malformed" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "throws std::runtime_error or returns temINVALID_FLAG", + "field": "Flags", + "location": "XChainBridge::preflight", + "validated_by": "TxFlags::isValid", + "validates": [ + "Checks that transaction flags are valid for this transaction type", + "Checks that no invalid flags are set" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 0.9, + "error_thrown": "throws std::runtime_error or returns temMALFORMED", + "field": "Signature fields (SignerEntries, Signatures)", + "location": "XChainBridge::preflight", + "validated_by": "SignerEntries::validate", + "validates": [ + "Checks that signer entries are valid", + "Checks that signatures are present and valid" + ], + "validation_type": "business_logic|format|type" + }, + { + "confidence": 0.8, + "error_thrown": "throws std::runtime_error or returns temMALFORMED", + "field": "Attestation fields (XChainAttestations)", + "location": "XChainBridge::preflight", + "validated_by": "XChainAttestations::validate", + "validates": [ + "Checks that attestations are valid", + "Checks that attestations are not duplicated", + "Checks that attestations are properly formatted" + ], + "validation_type": "business_logic|format|type" + }, + { + "confidence": 0.8, + "error_thrown": "throws std::runtime_error or returns temBAD_PUBLIC_KEY", + "field": "PublicKey fields", + "location": "XChainBridge::preflight", + "validated_by": "PublicKey::makePublicKey, PublicKey::isValid", + "validates": [ + "Checks that public keys are valid and not malformed" + ], + "validation_type": "format|type" + }, + { + "confidence": 0.9, + "error_thrown": "throws std::runtime_error or returns temBAD_QUORUM", + "field": "Quorum", + "location": "XChainBridge::preflight", + "validated_by": "explicit check in XChainBridge::preflight", + "validates": [ + "Checks that quorum is not zero", + "Checks that quorum is not greater than number of signers" + ], + "validation_type": "range|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp.ai.md b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp.ai.md new file mode 100644 index 0000000000..06386f50d5 --- /dev/null +++ b/src/libxrpl/tx/transactors/bridge/XChainBridge.cpp.ai.md @@ -0,0 +1,57 @@ +# XChainBridge.cpp + +## Role in the System + +This file is the monolithic implementation hub for all cross-chain bridge transaction types on the XRP Ledger. It provides the `preflight`, `preclaim`, and `doApply` methods for eight transactor classes — `XChainCreateBridge`, `BridgeModify`, `XChainClaim`, `XChainCommit`, `XChainCreateClaimID`, `XChainAddClaimAttestation`, `XChainAddAccountCreateAttestation`, and `XChainCreateAccountCommit` — plus a suite of internal helper functions that implement the shared quorum, attestation, and fund-transfer mechanics those transactors all rely on. + +The bridge protocol connects two independent ledgers (a *locking chain* and an *issuing chain*) without an exchange rate. Committing an asset into the locking chain's door account generates an equivalent wrapped asset on the issuing chain, and vice versa. The door account is a regular XRPL multi-sig account controlled by witness servers. A file-level block comment explains this "box" mental model and is worth reading before diving into the code. + +--- + +## The Seven-Transaction Lifecycle + +A normal cross-chain transfer follows a defined sequence: + +1. **`XChainCreateClaimID`** — The recipient on the destination chain reserves a monotonically increasing claim ID. This must happen *before* the source-side commit, locking in the source account identity and the reward the submitter is willing to pay witness servers. + +2. **`XChainCommit`** — The sender on the source chain locks funds into the door account via `transferHelper`, and deliberately supports fee-dipping (spending down to the reserve) via `TransferHelperSubmittingAccountInfo`. + +3. **`XChainAddClaimAttestation`** — Each witness server submits a signed attestation that the commit event occurred. When a quorum's worth of weight is reached *and* the attestation includes a destination, funds are settled automatically. + +4. **`XChainClaim`** (optional fallback) — If no destination was embedded in the commit, or if automatic settlement failed, the claim ID owner can explicitly trigger settlement. + +For account bootstrap, the protocol substitutes `XChainCreateAccountCommit` (locks both the creation amount and the witness reward) and `XChainAddAccountCreateAttestation` (witness attestations enforcing strict commit order). + +--- + +## Internal Helper Architecture + +### `transferHelper` + +The central payment primitive handling both XRP (direct balance manipulation) and IOU (via the `flow` engine). Key behaviors: self-transfers short-circuit immediately; deposit authorization is enforced unless the destination is the claim owner bypassing their own auth; XRP account creation is permitted only when the amount meets the base reserve; and the `submittingAccountInfo` parameter supports fee-dipping for commit transactions. + +### `finalizeClaimHelper` + +Orchestrates two-step settlement using a nested `PaymentSandbox` so the main transfer can be rolled back if reward distribution fails. The `FinalizeClaimHelperResult` struct reports three independent TER outcomes with a priority-ordered `ter()` accessor. The `OnTransferFail` enum is critical: regular claims use `keepClaim` (failed transfer preserves the claim ID for retry), while account-create claims use `removeClaim` (failed transfer still destroys the claim ID to unblock subsequent ordered creates). + +### `claimHelper` and `onNewAttestations` + +`claimHelper` is a template shared between regular and account-create quorum checks. Before counting weight, it strips attestations whose signer's key is no longer valid (master key disabled, regular key rotated). `onNewAttestations` adds or replaces an attestation from a signer, then immediately checks quorum. The `CheckDst::check` vs `CheckDst::ignore` flag distinguishes automatic settlement (destination must match the attested value) from user-triggered `XChainClaim` (any matching quorum suffices). + +### `applyClaimAttestations` and `applyCreateAccountAttestations` + +Both use a lambda scope to read and modify the claim ID SLE, then explicitly drop all references before calling `finalizeClaimHelper` — because the payment sandbox infrastructure requires no live SLE references from a parent view overlap with a child sandbox's mutations. The code comments this pattern as "ugly — admittedly." `applyCreateAccountAttestations` enforces the strict ordering invariant (`createCount == claimCount + 1`) and advances the counter past failed claims to prevent one stalled create from blocking all later ones. + +--- + +## Validation Design + +`XChainCreateBridge::preflight` enforces structural invariants: distinct door accounts (replay prevention), matching XRP/IOU types on both sides, and critically — for XRP bridges the issuing door must be the genesis root account; for IOU bridges it must be the currency issuer. This guarantees the issuing side can never exhaust its supply of wrapped tokens. + +`checkAttestationPublicKey` handles three cases: non-existent account (public key must derive to the signer account via `calcAccountID`), existing account using master key (master must not be disabled), and existing account using regular key (regular key field must match). This runs at both `preclaim` time and again inside `onNewAttestations` — explicitly redundant as a defensive guard against future refactoring. + +--- + +## Concurrency and State Management + +All state mutations go through `PaymentSandbox`, which buffers changes and applies atomically only on explicit `.apply()`. If any phase encounters `tecINTERNAL` or `tefBAD_LEDGER`, the entire sandbox is discarded. The `readOrpeekBridge` helper resolves bridge ambiguity by trying the locking-chain keylet first, then the issuing-chain keylet. `XChainClaim` and the attestation transactors are marked `ConsequencesFactory{Blocker}` — because they may trigger fund movements of indeterminate size, the transaction queue cannot pre-compute their consequences. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/check/CheckCancel.cpp.ai.json b/src/libxrpl/tx/transactors/check/CheckCancel.cpp.ai.json new file mode 100644 index 0000000000..ba8937b371 --- /dev/null +++ b/src/libxrpl/tx/transactors/check/CheckCancel.cpp.ai.json @@ -0,0 +1,320 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 15, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "CheckCancel::preflight" + ], + "entry_point": "CheckCancel::preflight", + "purpose": "Initial transaction preflight checks (syntax, basic validity). In this implementation, it always returns tesSUCCESS (no validation).", + "validation_points": [] + }, + { + "call_chain": [ + "CheckCancel::preclaim" + ], + "entry_point": "CheckCancel::preclaim", + "purpose": "Performs semantic validation before applying the transaction. Checks existence of the Check, expiration, and permissions.", + "validation_points": [ + "ctx.view.read(keylet::check(ctx.tx[sfCheckID])): Validates sfCheckID exists.", + "hasExpired(ctx.view, (*sleCheck)[~sfExpiration]): Validates expiration.", + "acctId != (*sleCheck)[sfAccount] && acctId != (*sleCheck)[sfDestination]: Validates sender is source or destination if not expired." + ] + }, + { + "call_chain": [ + "CheckCancel::doApply" + ], + "entry_point": "CheckCancel::doApply", + "purpose": "Applies the transaction to the ledger. Removes the Check from owner and destination directories, updates owner count, erases Check.", + "validation_points": [ + "view().peek(keylet::check(ctx_.tx[sfCheckID])): Validates sfCheckID exists (should be redundant after preclaim).", + "view().dirRemove(keylet::ownerDir(dstId), page, sleCheck->key(), true): Validates/removes Check from destination directory.", + "view().dirRemove(keylet::ownerDir(srcId), page, sleCheck->key(), true): Validates/removes Check from owner directory." + ] + } + ], + "data_flows": [ + { + "field": "sfCheckID", + "flow": [ + "ctx.tx[sfCheckID]", + "keylet::check(ctx.tx[sfCheckID])", + "ctx.view.read(...) in preclaim", + "view().peek(...) in doApply" + ], + "origin": "ctx.tx[sfCheckID] (transaction input)", + "transformations": [ + "Used to look up Check ledger entry" + ], + "validated_at": "preclaim (existence), doApply (existence, redundant)" + }, + { + "field": "sfExpiration", + "flow": [ + "(*sleCheck)[~sfExpiration]", + "hasExpired(ctx.view, ...)" + ], + "origin": "(*sleCheck)[~sfExpiration] (optional field in Check ledger entry)", + "transformations": [ + "Checked for presence and compared to ledger close time" + ], + "validated_at": "preclaim (expiration check)" + }, + { + "field": "sfAccount (transaction sender)", + "flow": [ + "AccountID const acctId{ctx.tx[sfAccount]}", + "Compared to (*sleCheck)[sfAccount] and (*sleCheck)[sfDestination]" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Converted to AccountID, compared for permission" + ], + "validated_at": "preclaim (permission check)" + }, + { + "field": "sfDestinationNode", + "flow": [ + "(*sleCheck)[sfDestinationNode]", + "view().dirRemove(keylet::ownerDir(dstId), page, sleCheck->key(), true)" + ], + "origin": "(*sleCheck)[sfDestinationNode] (Check ledger entry)", + "transformations": [ + "Used as page index for directory removal" + ], + "validated_at": "doApply (directory removal, error if fails)" + }, + { + "field": "sfOwnerNode", + "flow": [ + "(*sleCheck)[sfOwnerNode]", + "view().dirRemove(keylet::ownerDir(srcId), page, sleCheck->key(), true)" + ], + "origin": "(*sleCheck)[sfOwnerNode] (Check ledger entry)", + "transformations": [ + "Used as page index for directory removal" + ], + "validated_at": "doApply (directory removal, error if fails)" + } + ], + "description": "Implements the CheckCancel transaction logic for the XRPL ledger, including preflight, preclaim, and apply steps for canceling checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCheckID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::check(ctx.tx[sfCheckID])) at CheckCancel::preclaim", + "issue_pattern": "Missing empty string validation for sfCheckID", + "why_false_positive": "ctx.view.read(keylet::check(ctx.tx[sfCheckID])) validates sfCheckID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfExpiration", + "empty", + "string", + "validation" + ], + "evidence": "hasExpired(ctx.view, (*sleCheck)[~sfExpiration]) at CheckCancel::preclaim", + "issue_pattern": "Missing empty string validation for sfExpiration", + "why_false_positive": "hasExpired(ctx.view, (*sleCheck)[~sfExpiration]) validates sfExpiration for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount (transaction sender)", + "empty", + "string", + "validation" + ], + "evidence": "acctId != (*sleCheck)[sfAccount] && acctId != (*sleCheck)[sfDestination] at CheckCancel::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount (transaction sender)", + "why_false_positive": "acctId != (*sleCheck)[sfAccount] && acctId != (*sleCheck)[sfDestination] validates sfAccount (transaction sender) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCheckID", + "empty", + "string", + "validation" + ], + "evidence": "view().peek(keylet::check(ctx_.tx[sfCheckID])) at CheckCancel::doApply", + "issue_pattern": "Missing empty string validation for sfCheckID", + "why_false_positive": "view().peek(keylet::check(ctx_.tx[sfCheckID])) validates sfCheckID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDestinationNode", + "empty", + "string", + "validation" + ], + "evidence": "view().dirRemove(keylet::ownerDir(dstId), page, sleCheck->key(), true) at CheckCancel::doApply", + "issue_pattern": "Missing empty string validation for sfDestinationNode", + "why_false_positive": "view().dirRemove(keylet::ownerDir(dstId), page, sleCheck->key(), true) validates sfDestinationNode for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwnerNode", + "empty", + "string", + "validation" + ], + "evidence": "view().dirRemove(keylet::ownerDir(srcId), page, sleCheck->key(), true) at CheckCancel::doApply", + "issue_pattern": "Missing empty string validation for sfOwnerNode", + "why_false_positive": "view().dirRemove(keylet::ownerDir(srcId), page, sleCheck->key(), true) validates sfOwnerNode for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/check/CheckCancel.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 10, + "name": "CheckCancel::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 15, + "name": "CheckCancel::preclaim" + }, + { + "args": [], + "lineno": 38, + "name": "CheckCancel::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for CheckCancel would be in unit/integration tests for transaction processing, e.g., 'CheckCancel_test.cpp', 'Transactor_test.cpp', or broader transaction suite files. Tests should cover: non-existent Check (tecNO_ENTRY), expired Check (anyone can cancel), not expired (only source/destination can cancel), directory removal failures (tefBAD_LEDGER, usually unreachable), and owner count adjustment. Gaps: No validation in preflight (could be tested for completeness), and error paths in doApply are marked LCOV_EXCL (excluded from coverage, likely not tested).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl ledger view and keylet accessors", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfCheckID", + "location": "CheckCancel::preclaim", + "validated_by": "ctx.view.read(keylet::check(ctx.tx[sfCheckID]))", + "validates": [ + "Check existence in ledger by CheckID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION (if not expired and not authorized)", + "field": "sfExpiration", + "location": "CheckCancel::preclaim", + "validated_by": "hasExpired(ctx.view, (*sleCheck)[~sfExpiration])", + "validates": [ + "Check expiration against ledger close time" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfAccount (transaction sender)", + "location": "CheckCancel::preclaim", + "validated_by": "acctId != (*sleCheck)[sfAccount] && acctId != (*sleCheck)[sfDestination]", + "validates": [ + "If check is not expired, only creator or destination can cancel" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfCheckID", + "location": "CheckCancel::doApply", + "validated_by": "view().peek(keylet::check(ctx_.tx[sfCheckID]))", + "validates": [ + "Check existence in ledger by CheckID (should be caught in preclaim)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfDestinationNode", + "location": "CheckCancel::doApply", + "validated_by": "view().dirRemove(keylet::ownerDir(dstId), page, sleCheck->key(), true)", + "validates": [ + "Check can be removed from destination's owner directory" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfOwnerNode", + "location": "CheckCancel::doApply", + "validated_by": "view().dirRemove(keylet::ownerDir(srcId), page, sleCheck->key(), true)", + "validates": [ + "Check can be removed from owner's directory" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/check/CheckCancel.cpp.ai.md b/src/libxrpl/tx/transactors/check/CheckCancel.cpp.ai.md new file mode 100644 index 0000000000..ba19b329c3 --- /dev/null +++ b/src/libxrpl/tx/transactors/check/CheckCancel.cpp.ai.md @@ -0,0 +1,45 @@ +# `CheckCancel.cpp` — Canceling Checks on the XRPL + +## Role in the System + +This file implements the `CheckCancel` transactor, one of three components (alongside `CheckCreate` and `CheckCash`) that together manage the XRPL's deferred payment mechanism. A Check ledger object represents a standing authorization to transfer value — the sender has not yet moved funds, but has declared intent and locked a reserve. `CheckCancel` is the teardown path: it removes the check from the ledger, releases the owner reserve, and cleans up both directory entries that reference the object. It serves two distinct use cases — voluntary abortion by either party before expiration, and post-expiration sweeping by any account. + +## Class Structure + +`CheckCancel` inherits from `Transactor` and declares `ConsequencesFactory` as `Normal`, meaning the consensus engine treats it as a standard fee-paying transaction with no special fee escalation behavior. The `preflight` and `preclaim` methods are `static`, operating only on their context parameters without any instance state, while `doApply` is a virtual override that accesses the mutable ledger view through the base class. + +## The Three-Phase Execution Model + +**`preflight`** is intentionally empty — it returns `tesSUCCESS` unconditionally. There is nothing about the cancel transaction that can be validated without ledger state: whether the check exists, whether it's expired, and whether the submitter is authorized all depend on current ledger contents. Contrast this with `CheckCreate::preflight`, which validates self-sends, amount sanity, and expiration format — purely structural properties of the transaction itself. The blank `CheckCancel::preflight` is architecturally correct, not a gap. + +**`preclaim`** handles all semantic validation against a read-only view. It encodes two distinct authorization regimes: + +1. If the check has **expired** (its `sfExpiration` has passed the parent ledger's close time), any account may cancel it. Expired checks are stale obligations that anyone can sweep from the ledger. + +2. If the check has **not yet expired**, only the original creator (`sfAccount` on the check SLE) or the designated destination (`sfDestination`) may cancel. A third party cannot unilaterally void a valid, outstanding check. + +The expiration comparison deliberately uses the **parent ledger's close time**, not the ledger currently being constructed. This is a determinism constraint: the closing time of the in-progress ledger is not finalized at transaction application time, so using it would produce different results across validators. The parent ledger's close time is consensus-agreed and immutable, making the expiration check fully deterministic. + +Unauthorized cancellation returns `tecNO_PERMISSION` — a `tec`-class error meaning the transaction still claims the transaction fee even though it fails, consistent with how the XRPL handles fee-charging for ledger-state-level rejections. + +## Ledger Mutation in `doApply` + +The apply phase executes three sequential operations that mirror in reverse what `CheckCreate::doApply` set up. + +**Directory cleanup** is the most structurally significant operation. When a check is created, it is inserted into the owner directories of both the source account and the destination account. The check SLE caches the directory page numbers for both insertions as `sfOwnerNode` and `sfDestinationNode`. `CheckCancel` reads these cached page numbers and calls `view().dirRemove()` with them directly, enabling O(1) removal without scanning directory pages. The guard `if (srcId != dstId)` skips the destination directory removal for self-checks — a case that `CheckCreate` explicitly rejects with `temREDUNDANT`, so this guard is a defensive measure for ledger consistency rather than a live code path. + +**Owner reserve release** via `adjustOwnerCount(view(), sleSrc, -1, viewJ)` decrements the source account's `sfOwnerCount` by one. This releases the XRP reserve increment that was allocated when the check was created. Only the source account's count is adjusted because only the source "owns" the check object for reserve purposes — the destination has a directory reference but bears no reserve obligation. + +**SLE deletion** with `view().erase(sleCheck)` removes the check object itself from the ledger state. + +## Defensive Coding Patterns + +The `view().dirRemove()` failure branches are wrapped in `// LCOV_EXCL_START` / `// LCOV_EXCL_STOP` markers, indicating they are excluded from coverage requirements and considered unreachable in a well-formed ledger. If directory entries are inconsistent with the SLE, it signals ledger corruption rather than a user error, hence the `tefBAD_LEDGER` return code and `fatal`-level log. The `tef` prefix means the transaction is not applied and the fee is not charged. + +The existence check on `sleCheck` inside `doApply` using `view().peek()` is redundant given that `preclaim` already verified the check exists. This is a deliberate defensive guard against any theoretical divergence between the read-only `preclaim` view and the mutable `doApply` view — if somehow the check disappeared between phases (which the framework prevents in practice), the fallback returns `tecNO_ENTRY` rather than crashing. + +## Relationship to Sibling Files + +`CheckCreate.cpp` is the direct inverse of `CheckCancel`: where `CheckCreate::doApply` calls `dirInsert` twice and `adjustOwnerCount(..., +1, ...)`, `CheckCancel::doApply` calls `dirRemove` twice and `adjustOwnerCount(..., -1, ...)`. The `sfOwnerNode` and `sfDestinationNode` fields written by `CheckCreate` are the exact page indices consumed by `CheckCancel`. + +`CheckCash.cpp` shares the cleanup responsibility — it must also remove the check SLE, its directory entries, and adjust the owner count, but it additionally handles value delivery (XRP or IOU/MPT), trust line interactions, and partial payment semantics. `CheckCancel` is structurally simpler because it destroys the promise without ever honoring it. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp.ai.json b/src/libxrpl/tx/transactors/check/CheckCash.cpp.ai.json new file mode 100644 index 0000000000..6a9b88c3a0 --- /dev/null +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp.ai.json @@ -0,0 +1,450 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 19, + "name": "ctx" + }, + { + "lineno": 46, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "CheckCash::preflight" + ], + "entry_point": "CheckCash::preflight", + "purpose": "Performs initial stateless validation of the CheckCash transaction. Ensures only one of Amount or DeliverMin is present, validates amount, and checks for bad asset/currency.", + "validation_points": [ + "CheckCash::preflight: Exactly one of Amount or DeliverMin must be present.", + "CheckCash::preflight: Amount must be legal and positive.", + "CheckCash::preflight: Asset must not be bad." + ] + }, + { + "call_chain": [ + "CheckCash::preclaim" + ], + "entry_point": "CheckCash::preclaim", + "purpose": "Performs contextual validation using the current ledger state. Checks that the referenced Check exists, the destination matches, accounts exist, destination tag requirements, and expiration.", + "validation_points": [ + "CheckCash::preclaim: Check exists in ledger.", + "CheckCash::preclaim: Transaction account matches Check destination.", + "CheckCash::preclaim: Source and destination accounts exist.", + "CheckCash::preclaim: DestinationTag required if lsfRequireDestTag is set.", + "CheckCash::preclaim: Check has not expired." + ] + }, + { + "call_chain": [ + "CheckCash::doApply" + ], + "entry_point": "CheckCash::doApply", + "purpose": "Executes the transaction after all validations pass. (Not shown in full context, but standard for XRPL transactors.)", + "validation_points": [ + "Relies on preflight and preclaim having validated all inputs." + ] + }, + { + "call_chain": [ + "CheckCash::checkExtraFeatures" + ], + "entry_point": "CheckCash::checkExtraFeatures", + "purpose": "Checks if the transaction uses features (like MPTokensV2) that are enabled or not.", + "validation_points": [ + "CheckCash::checkExtraFeatures: Validates feature flags and token types." + ] + } + ], + "data_flows": [ + { + "field": "sfAmount / sfDeliverMin", + "flow": [ + "Transaction input", + "CheckCash::preflight (optAmount/optDeliverMin extracted)", + "STAmount value constructed", + "isLegalNet(value), value.signum(), badAsset() checks", + "Passed to CheckCash::preclaim as part of ctx.tx" + ], + "origin": "ctx.tx[~sfAmount] / ctx.tx[~sfDeliverMin] (transaction input)", + "transformations": [ + "Optional extraction (may be absent)", + "STAmount construction (type conversion)", + "Checked for legality, positivity, and asset validity" + ], + "validated_at": "CheckCash::preflight" + }, + { + "field": "sfCheckID", + "flow": [ + "Transaction input", + "CheckCash::preclaim (used to look up Check in ledger: ctx.view.read(keylet::check(ctx.tx[sfCheckID])))" + ], + "origin": "ctx.tx[sfCheckID] (transaction input)", + "transformations": [ + "Used as key to fetch Check object from ledger" + ], + "validated_at": "CheckCash::preclaim" + }, + { + "field": "sfAccount (transaction) vs sfDestination (check)", + "flow": [ + "Transaction input and ledger Check object", + "CheckCash::preclaim (compared for equality)" + ], + "origin": "ctx.tx[sfAccount] (transaction input), sleCheck->at(sfDestination) (ledger)", + "transformations": [ + "Direct comparison" + ], + "validated_at": "CheckCash::preclaim" + }, + { + "field": "sfDestinationTag", + "flow": [ + "Ledger Check object", + "CheckCash::preclaim (checked if required by destination account flags)" + ], + "origin": "sleCheck->isFieldPresent(sfDestinationTag) (ledger Check object)", + "transformations": [ + "Presence check" + ], + "validated_at": "CheckCash::preclaim" + }, + { + "field": "sfExpiration", + "flow": [ + "Ledger Check object", + "CheckCash::preclaim (hasExpired(ctx.view, ...))" + ], + "origin": "sleCheck->at(~sfExpiration) (ledger Check object)", + "transformations": [ + "Expiration time compared to current ledger time" + ], + "validated_at": "CheckCash::preclaim" + } + ], + "description": "Implements the CheckCash transaction logic for the XRPL ledger, including preflight, preclaim, and application of check cashing, handling both XRP and IOU/MPT tokens, trust lines, reserves, and ledger updates.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount / sfDeliverMin", + "empty", + "string", + "validation" + ], + "evidence": "explicit logic (static_cast(optAmount) == static_cast(optDeliverMin)) at CheckCash::preflight", + "issue_pattern": "Missing empty string validation for sfAmount / sfDeliverMin", + "why_false_positive": "explicit logic (static_cast(optAmount) == static_cast(optDeliverMin)) validates sfAmount / sfDeliverMin for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount / sfDeliverMin", + "empty", + "string", + "validation" + ], + "evidence": "isLegalNet(value), value.signum() > 0 at CheckCash::preflight", + "issue_pattern": "Missing empty string validation for sfAmount / sfDeliverMin", + "why_false_positive": "isLegalNet(value), value.signum() > 0 validates sfAmount / sfDeliverMin for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount / sfDeliverMin (asset)", + "empty", + "string", + "validation" + ], + "evidence": "badAsset() == value.asset() at CheckCash::preflight", + "issue_pattern": "Missing empty string validation for sfAmount / sfDeliverMin (asset)", + "why_false_positive": "badAsset() == value.asset() validates sfAmount / sfDeliverMin (asset) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCheckID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::check(ctx.tx[sfCheckID])) at CheckCash::preclaim", + "issue_pattern": "Missing empty string validation for sfCheckID", + "why_false_positive": "ctx.view.read(keylet::check(ctx.tx[sfCheckID])) validates sfCheckID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount (transaction) vs sfDestination (check)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx[sfAccount] != dstId at CheckCash::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount (transaction) vs sfDestination (check)", + "why_false_positive": "ctx.tx[sfAccount] != dstId validates sfAccount (transaction) vs sfDestination (check) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount (check) == sfDestination (check)", + "empty", + "string", + "validation" + ], + "evidence": "srcId == dstId at CheckCash::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount (check) == sfDestination (check)", + "why_false_positive": "srcId == dstId validates sfAccount (check) == sfDestination (check) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount (check), sfDestination (check)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::account(srcId)), ctx.view.read(keylet::account(dstId)) at CheckCash::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount (check), sfDestination (check)", + "why_false_positive": "ctx.view.read(keylet::account(srcId)), ctx.view.read(keylet::account(dstId)) validates sfAccount (check), sfDestination (check) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount / sfDeliverMin (MPTIssue type)", + "empty", + "string", + "validation" + ], + "evidence": "optAmount->holds(), optDeliverMin->holds() at CheckCash::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfAmount / sfDeliverMin (MPTIssue type)", + "why_false_positive": "optAmount->holds(), optDeliverMin->holds() validates sfAmount / sfDeliverMin (MPTIssue type) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/check/CheckCash.cpp", + "functions": [ + { + "args": [ + "xrpl::PreflightContext const& ctx" + ], + "lineno": 11, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 19, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 46, + "name": "preclaim" + }, + { + "args": [], + "lineno": 168, + "name": "doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "CheckCash validation is typically covered by unit/integration tests in the rippled codebase, especially in files like 'test/tx/Check_test.cpp' or similar. These tests should cover malformed transactions (missing/extra fields), bad amounts, bad currencies, missing/expired checks, wrong destination, missing destination tags, and feature flag gating. Gaps may exist for edge cases like self-checks (srcId == dstId), which are rare and marked as 'should be caught when the check is created'. Some error paths (e.g., missing accounts when check exists) are marked as 'should never occur' and may not be directly tested. Feature flag logic (MPTokensV2) may require dedicated tests to ensure correct gating.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl protocol, custom logic, ledger view", + "validation_layer": "business_logic (preflight/preclaim methods)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAmount / sfDeliverMin", + "location": "CheckCash::preflight", + "validated_by": "explicit logic (static_cast(optAmount) == static_cast(optDeliverMin))", + "validates": [ + "Exactly one of Amount or DeliverMin must be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount / sfDeliverMin", + "location": "CheckCash::preflight", + "validated_by": "isLegalNet(value), value.signum() > 0", + "validates": [ + "Amount is legal for network", + "Amount is positive" + ], + "validation_type": "format, range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_CURRENCY", + "field": "sfAmount / sfDeliverMin (asset)", + "location": "CheckCash::preflight", + "validated_by": "badAsset() == value.asset()", + "validates": [ + "Asset is not a bad/currency" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfCheckID", + "location": "CheckCash::preclaim", + "validated_by": "ctx.view.read(keylet::check(ctx.tx[sfCheckID]))", + "validates": [ + "Check exists in ledger" + ], + "validation_type": "business_logic, existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfAccount (transaction) vs sfDestination (check)", + "location": "CheckCash::preclaim", + "validated_by": "ctx.tx[sfAccount] != dstId", + "validates": [ + "Only destination account can cash the check" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "sfAccount (check) == sfDestination (check)", + "location": "CheckCash::preclaim", + "validated_by": "srcId == dstId", + "validates": [ + "Check is not written to self" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "not shown (incomplete code), but logs warning", + "field": "sfAccount (check), sfDestination (check)", + "location": "CheckCash::preclaim", + "validated_by": "ctx.view.read(keylet::account(srcId)), ctx.view.read(keylet::account(dstId))", + "validates": [ + "Source and destination accounts exist" + ], + "validation_type": "business_logic, existence" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (feature not enabled)", + "field": "sfAmount / sfDeliverMin (MPTIssue type)", + "location": "CheckCash::checkExtraFeatures", + "validated_by": "optAmount->holds(), optDeliverMin->holds()", + "validates": [ + "MPTIssue type only allowed if featureMPTokensV2 enabled" + ], + "validation_type": "type, business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/check/CheckCash.cpp.ai.md b/src/libxrpl/tx/transactors/check/CheckCash.cpp.ai.md new file mode 100644 index 0000000000..e1941c5299 --- /dev/null +++ b/src/libxrpl/tx/transactors/check/CheckCash.cpp.ai.md @@ -0,0 +1,62 @@ +# `CheckCash.cpp` — Check Redemption Transactor + +## Role in the System + +`CheckCash.cpp` implements the XRPL `CheckCash` transaction: the act of the designated recipient redeeming a check that was previously created via `CheckCreate`. The check mechanism is analogous to a paper bank check — the sender creates it with a maximum amount (`sfSendMax`) and a designated payee; only that payee can cash it, and they may ask for any amount up to the `sfSendMax`. This file contains the three-phase transactor lifecycle (`preflight` → `preclaim` → `doApply`) plus the feature-gating hook `checkExtraFeatures`. + +`CheckCash` inherits from `Transactor` and declares `ConsequencesFactory{Normal}`, meaning it consumes the account's sequence number but does not require special fee escalation treatment. + +## Feature Gating + +`checkExtraFeatures()` exists to gate Multi-Purpose Token (MPT) usage behind the `featureMPTokensV2` amendment. The check fields `sfAmount` and `sfDeliverMin` can carry either a classic `Issue` or an `MPTIssue`. Without the amendment enabled, any `CheckCash` transaction specifying an MPT asset in either field is rejected before any further processing. This pattern — a static pre-screen run before `preflight` — lets the ledger cleanly introduce new transaction capabilities without forking validation logic across amendment conditions throughout the rest of the code. + +## Preflight: Stateless Structural Validation + +`preflight()` enforces three invariants without consulting ledger state: + +1. **Mutual exclusivity**: Exactly one of `sfAmount` or `sfDeliverMin` must be present. `sfAmount` means "deliver exactly this amount or fail"; `sfDeliverMin` means "deliver as much as possible, but at least this much." The check `static_cast(optAmount) == static_cast(optDeliverMin)` cleanly handles both the "both absent" and "both present" error cases in one expression. + +2. **Amount validity**: The unified value (whichever field is present) must pass `isLegalNet()` and be strictly positive. + +3. **Asset validity**: The asset must not be the sentinel `badAsset()`. + +## Preclaim: Stateful Contextual Validation + +`preclaim()` consults the ledger view to validate the transaction against current state. It walks through a sequence of guards: + +- The referenced check (`sfCheckID`) must exist on the ledger. +- Only the check's `sfDestination` may cash it (`tecNO_PERMISSION` otherwise). A self-check guard (`srcId == dstId`) is also present though marked `LCOV_EXCL_START` — it should be impossible if `CheckCreate` validated correctly, but is defended against as belt-and-suspenders. +- The destination account's `lsfRequireDestTag` flag is honored: if set, the check must have included `sfDestinationTag`. +- The check must not have expired. + +Then the amount validation proceeds against the check's own `sfSendMax`. The request currency and issuer must match `sfSendMax` exactly. The requested value must not exceed `sfSendMax`. There is also a liquidity check against the source account's available funds (`accountFunds()` with `fhZERO_IF_FROZEN`). A notable subtlety here: when the amount is XRP, one reserve increment (`ctx.view.fees().increment`) is added to the reported available funds. This is because the check itself occupies one owner reserve slot; cashing it will free that slot, so the source effectively has one additional reserve's worth of XRP available to transfer. + +For IOU assets where the destination is not the issuer itself, `preclaim()` validates the destination's eligibility to receive the asset. For classic IOUs this means verifying the issuer exists, checking `lsfRequireAuth` and the trust line's authorization flags using the canonical high/low account ordering, and checking that the destination's trust line is not frozen. For MPTs it uses `requireAuth()` with `AuthType::WeakAuth`, checks MPT-level freeze, and calls `canTrade()` to confirm the MPT is permitted on the DEX. + +## `doApply()`: Execution + +All mutations are staged in a `PaymentSandbox` (`psb`) wrapping the apply-context view. This is not optional — `flow()` is designed to operate on a `PaymentSandbox` because of its deferred-credit accounting, which prevents in-flight liquidity from being double-counted across path steps. Only at the very end does `psb.apply(ctx_.rawView())` commit changes; if anything fails partway, the sandbox is silently discarded. + +### XRP Path + +The payment engine's `flow()` does not handle native XRP transfers, so XRP cashing is handled directly. `xrpLiquid(psb, srcId, -1, viewJ)` computes how much XRP the source can actually move, passing `-1` as an owner-count adjustment to credit the reserve that will be freed when the check is deleted. + +When `sfDeliverMin` is used, the actual delivery amount is `max(DeliverMin, min(sendMax, srcLiquid))`: deliver as much as possible up to the check's cap, so long as the minimum floor is met. For `sfAmount`, the requested amount is used verbatim. The transfer is executed via `transferXRP`. + +### IOU/MPT Path + +The IOU/MPT path delegates to `flow()` but requires careful setup: + +**Trust line auto-creation.** Unlike a plain `Payment`, `CheckCash` automatically creates a trust line (for IOU) or `MPToken` entry (for MPT) if one does not yet exist. The logic reasoning is sound: the destination signed the `CheckCash` transaction, which is cryptographic proof they consent to receiving these funds. A reserve check via the `checkReserve` lambda confirms the destination can afford the new ledger entry before it is created. The trust line is created with zero balance and zero limit via `trustCreate()`; the transaction machinery will automatically clean it up if the subsequent payment fails. + +**Trust line limit elevation.** Even with a trust line present, the destination may have set a limit that would be exceeded by the incoming funds. Since the destination explicitly consented, `doApply()` temporarily raises the trust line limit to `STAmount{cMaxValue, cMaxOffset}` — the maximum representable value — before calling `flow()`. A `scope_exit` guard restores the original limit unconditionally when the scope exits, regardless of whether `flow()` succeeded or failed. This is the central design choice of the IOU cashing path: RAII ensures the temporary limit mutation never escapes the function. + +**DeliverMin capping.** When `sfDeliverMin` is specified, `flow()` is invoked with a target amount of `STAmount::cMaxValue / 2` (or `maxMPTokenAmount / 2` for MPTs) and `partial_payment = true`. The cap at half the maximum prevents overflow when gateway transfer rates (which can be up to 200%) are applied internally. After `flow()` returns, the `actualAmountOut` is compared to `*optDeliverMin`; if it falls short, `tecPATH_PARTIAL` is returned. In both `sfAmount` and `sfDeliverMin` cases, `ctx_.deliver(result.actualAmountOut)` records the delivered-amount metadata. + +### Cleanup + +After a successful transfer, `doApply()` removes the check from both the destination's and the source's owner directories, decrements the source's owner count (freeing the reserve), erases the check ledger entry, and finally calls `psb.apply()`. The directory removal failure paths are `LCOV_EXCL_START`-marked because a corrupt directory at this point would indicate a pre-existing ledger inconsistency, not a recoverable error. + +## Relationship to Sibling Files + +`CheckCancel.cpp` in the same directory follows the same structural pattern but is considerably simpler — it only removes the check from both directories and decrements the owner count, with no fund movement. `CheckCreate.cpp` establishes the check ledger entry that `CheckCash` and `CheckCancel` both operate on. The separation keeps each phase of the check lifecycle isolated and independently testable. The `flow()` function from `xrpl/tx/paths/Flow.h` carries the bulk of the IOU/MPT payment logic and is reused across `Payment`, `OfferCreate`, and check cashing, which is why `CheckCash` must wrap its mutations in a `PaymentSandbox`. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/check/CheckCreate.cpp.ai.json b/src/libxrpl/tx/transactors/check/CheckCreate.cpp.ai.json new file mode 100644 index 0000000000..0283a03e91 --- /dev/null +++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp.ai.json @@ -0,0 +1,531 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "CheckCreate::preflight" + ], + "entry_point": "CheckCreate::preflight", + "purpose": "Performs stateless validation of the CheckCreate transaction before it is accepted for further processing.", + "validation_points": [ + "Check for sfAccount == sfDestination (self-check)", + "Check sendMax is legal and positive", + "Check sendMax asset is not badAsset", + "Check sfExpiration is present and nonzero" + ] + }, + { + "call_chain": [ + "CheckCreate::preclaim" + ], + "entry_point": "CheckCreate::preclaim", + "purpose": "Performs contextual validation (requires ledger state) before the transaction is applied.", + "validation_points": [ + "Check destination account exists", + "Check destination account flags (lsfDisallowIncomingCheck, lsfRequireDestTag)", + "Check destination is not a pseudo-account", + "Check sendMax is not globally frozen", + "Check trustline is not frozen (if exists)" + ] + }, + { + "call_chain": [ + "CheckCreate::doApply" + ], + "entry_point": "CheckCreate::doApply", + "purpose": "Applies the transaction to the ledger (not shown in provided code, but standard for XRPL transactors).", + "validation_points": [ + "Assumes preflight and preclaim have passed; may have additional checks" + ] + }, + { + "call_chain": [ + "CheckCreate::checkExtraFeatures" + ], + "entry_point": "CheckCreate::checkExtraFeatures", + "purpose": "Checks for feature flags and transaction field compatibility (e.g., MPTokens).", + "validation_points": [ + "Checks if featureMPTokensV2 is enabled or if sendMax does not hold MPTIssue" + ] + } + ], + "data_flows": [ + { + "field": "sfAccount", + "flow": [ + "Transaction JSON", + "ctx.tx[sfAccount]", + "Compared to sfDestination in preflight", + "Used as srcId in preclaim" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Direct comparison", + "Assignment to srcId" + ], + "validated_at": "preflight (self-check), preclaim (used for trustline checks)" + }, + { + "field": "sfDestination", + "flow": [ + "Transaction JSON", + "ctx.tx[sfDestination]", + "Compared to sfAccount in preflight", + "Used as dstId in preclaim", + "Used to look up destination account in ledger" + ], + "origin": "ctx.tx[sfDestination] (transaction input)", + "transformations": [ + "Direct comparison", + "Assignment to dstId", + "Ledger lookup" + ], + "validated_at": "preflight (self-check), preclaim (account existence, flags, pseudo-account, dest tag)" + }, + { + "field": "sfSendMax", + "flow": [ + "Transaction JSON", + "ctx.tx.getFieldAmount(sfSendMax)", + "STAmount sendMax in preflight and preclaim" + ], + "origin": "ctx.tx.getFieldAmount(sfSendMax) (transaction input)", + "transformations": [ + "isLegalNet(sendMax)", + "sendMax.signum()", + "sendMax.asset()", + "sendMax.native()", + "sendMax.getIssuer()" + ], + "validated_at": "preflight (isLegalNet, signum, badAsset), preclaim (frozen checks, trustline checks)" + }, + { + "field": "sfExpiration", + "flow": [ + "Transaction JSON (optional)", + "ctx.tx[~sfExpiration]", + "optExpiry in preflight" + ], + "origin": "ctx.tx[~sfExpiration] (optional transaction input)", + "transformations": [ + "Optional presence check", + "Value check (*optExpiry == 0)" + ], + "validated_at": "preflight (nonzero check)" + }, + { + "field": "sfDestinationTag", + "flow": [ + "Transaction JSON (optional)", + "ctx.tx.isFieldPresent(sfDestinationTag)", + "Checked in preclaim if lsfRequireDestTag is set" + ], + "origin": "ctx.tx[sfDestinationTag] (optional transaction input)", + "transformations": [ + "Presence check" + ], + "validated_at": "preclaim (required if lsfRequireDestTag is set on destination)" + } + ], + "description": "Implements the CheckCreate transaction logic for the XRPL, including preflight validation, preclaim checks, and ledger application for creating a Check object.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccount validated by Custom validation in transaction preflight/preclaim methods", + "issue_pattern": "Missing validation for sfAccount", + "why_false_positive": "Custom validation in transaction preflight/preclaim methods validates sfAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDestination", + "validation", + "missing", + "check" + ], + "evidence": "Field sfDestination validated by Custom validation in transaction preflight/preclaim methods", + "issue_pattern": "Missing validation for sfDestination", + "why_false_positive": "Custom validation in transaction preflight/preclaim methods validates sfDestination automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSendMax", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSendMax validated by Custom validation in transaction preflight/preclaim methods", + "issue_pattern": "Missing validation for sfSendMax", + "why_false_positive": "Custom validation in transaction preflight/preclaim methods validates sfSendMax automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfExpiration", + "validation", + "missing", + "check" + ], + "evidence": "Field sfExpiration validated by Custom validation in transaction preflight/preclaim methods", + "issue_pattern": "Missing validation for sfExpiration", + "why_false_positive": "Custom validation in transaction preflight/preclaim methods validates sfExpiration automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDestinationTag", + "validation", + "missing", + "check" + ], + "evidence": "Field sfDestinationTag validated by Custom validation in transaction preflight/preclaim methods", + "issue_pattern": "Missing validation for sfDestinationTag", + "why_false_positive": "Custom validation in transaction preflight/preclaim methods validates sfDestinationTag automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount, sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "explicit comparison at CheckCreate::preflight", + "issue_pattern": "Missing empty string validation for sfAccount, sfDestination", + "why_false_positive": "explicit comparison validates sfAccount, sfDestination for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSendMax", + "empty", + "string", + "validation" + ], + "evidence": "isLegalNet(sendMax), sendMax.signum() > 0 at CheckCreate::preflight", + "issue_pattern": "Missing empty string validation for sfSendMax", + "why_false_positive": "isLegalNet(sendMax), sendMax.signum() > 0 validates sfSendMax for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSendMax.asset()", + "empty", + "string", + "validation" + ], + "evidence": "badAsset() == sendMax.asset() at CheckCreate::preflight", + "issue_pattern": "Missing empty string validation for sfSendMax.asset()", + "why_false_positive": "badAsset() == sendMax.asset() validates sfSendMax.asset() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfExpiration", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx[~sfExpiration], *optExpiry == 0 at CheckCreate::preflight", + "issue_pattern": "Missing empty string validation for sfExpiration", + "why_false_positive": "ctx.tx[~sfExpiration], *optExpiry == 0 validates sfExpiration for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfExpiration", + "range", + "bounds", + "validation" + ], + "evidence": "ctx.tx[~sfExpiration], *optExpiry == 0 at CheckCreate::preflight", + "issue_pattern": "Missing range validation for sfExpiration", + "why_false_positive": "ctx.tx[~sfExpiration], *optExpiry == 0 validates sfExpiration range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::account(dstId)) at CheckCreate::preclaim", + "issue_pattern": "Missing empty string validation for sfDestination", + "why_false_positive": "ctx.view.read(keylet::account(dstId)) validates sfDestination for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfDisallowIncomingCheck (destination account flags)", + "empty", + "string", + "validation" + ], + "evidence": "flags & lsfDisallowIncomingCheck at CheckCreate::preclaim", + "issue_pattern": "Missing empty string validation for lsfDisallowIncomingCheck (destination account flags)", + "why_false_positive": "flags & lsfDisallowIncomingCheck validates lsfDisallowIncomingCheck (destination account flags) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "pseudo-account discriminator (destination)", + "empty", + "string", + "validation" + ], + "evidence": "isPseudoAccount(sleDst) at CheckCreate::preclaim", + "issue_pattern": "Missing empty string validation for pseudo-account discriminator (destination)", + "why_false_positive": "isPseudoAccount(sleDst) validates pseudo-account discriminator (destination) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDestinationTag", + "empty", + "string", + "validation" + ], + "evidence": "flags & lsfRequireDestTag, ctx.tx.isFieldPresent(sfDestinationTag) at CheckCreate::preclaim", + "issue_pattern": "Missing empty string validation for sfDestinationTag", + "why_false_positive": "flags & lsfRequireDestTag, ctx.tx.isFieldPresent(sfDestinationTag) validates sfDestinationTag for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSendMax (MPTIssue)", + "empty", + "string", + "validation" + ], + "evidence": "!ctx.tx[sfSendMax].holds() at CheckCreate::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfSendMax (MPTIssue)", + "why_false_positive": "!ctx.tx[sfSendMax].holds() validates sfSendMax (MPTIssue) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/check/CheckCreate.cpp", + "functions": [ + { + "args": [ + "xrpl::PreflightContext const& ctx" + ], + "lineno": 11, + "name": "CheckCreate::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "CheckCreate::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 44, + "name": "CheckCreate::preclaim" + }, + { + "args": [], + "lineno": 120, + "name": "CheckCreate::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "CheckCreate is a core XRPL transaction type. Typical test coverage is found in unit/integration test files such as 'test/tx/Check_test.cpp', 'test/tx/CheckCreate_test.cpp', or similar. These tests usually cover: self-checks, bad sendMax, bad asset, missing/zero expiration, destination account existence, disallow flags, pseudo-account, dest tag requirements, and frozen asset/trustline scenarios. Gaps may exist for edge cases involving new features (e.g., MPTokens), pseudo-accounts, or complex trustline/freeze interactions. Tests for feature flag gating (featureMPTokensV2) may be missing or incomplete if the feature is new.", + "validation_architecture": { + "auto_validated_fields": [ + "sfAccount", + "sfDestination", + "sfSendMax", + "sfExpiration", + "sfDestinationTag" + ], + "framework": "Custom validation in transaction preflight/preclaim methods", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temREDUNDANT", + "field": "sfAccount, sfDestination", + "location": "CheckCreate::preflight", + "validated_by": "explicit comparison", + "validates": [ + "Check that sender and destination are not the same account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfSendMax", + "location": "CheckCreate::preflight", + "validated_by": "isLegalNet(sendMax), sendMax.signum() > 0", + "validates": [ + "Check that sendMax is a legal network amount", + "Check that sendMax is positive" + ], + "validation_type": "format, range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_CURRENCY", + "field": "sfSendMax.asset()", + "location": "CheckCreate::preflight", + "validated_by": "badAsset() == sendMax.asset()", + "validates": [ + "Check that sendMax asset is not a bad/currency" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_EXPIRATION", + "field": "sfExpiration", + "location": "CheckCreate::preflight", + "validated_by": "ctx.tx[~sfExpiration], *optExpiry == 0", + "validates": [ + "Check that expiration, if present, is not zero" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_DST", + "field": "sfDestination", + "location": "CheckCreate::preclaim", + "validated_by": "ctx.view.read(keylet::account(dstId))", + "validates": [ + "Check that destination account exists" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "lsfDisallowIncomingCheck (destination account flags)", + "location": "CheckCreate::preclaim", + "validated_by": "flags & lsfDisallowIncomingCheck", + "validates": [ + "Check that destination does not disallow incoming checks" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "pseudo-account discriminator (destination)", + "location": "CheckCreate::preclaim", + "validated_by": "isPseudoAccount(sleDst)", + "validates": [ + "Check that destination is not a pseudo-account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecDST_TAG_NEEDED", + "field": "sfDestinationTag", + "location": "CheckCreate::preclaim", + "validated_by": "flags & lsfRequireDestTag, ctx.tx.isFieldPresent(sfDestinationTag)", + "validates": [ + "Check that destination tag is present if required by destination account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "N/A (returns bool, used for feature gating)", + "field": "sfSendMax (MPTIssue)", + "location": "CheckCreate::checkExtraFeatures", + "validated_by": "!ctx.tx[sfSendMax].holds()", + "validates": [ + "Check that sendMax is not an MPTIssue unless featureMPTokensV2 is enabled" + ], + "validation_type": "type, business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/check/CheckCreate.cpp.ai.md b/src/libxrpl/tx/transactors/check/CheckCreate.cpp.ai.md new file mode 100644 index 0000000000..60d02138a5 --- /dev/null +++ b/src/libxrpl/tx/transactors/check/CheckCreate.cpp.ai.md @@ -0,0 +1,51 @@ +# `CheckCreate.cpp` — Check Ledger Object Creation + +## Role in the System + +`CheckCreate.cpp` implements the first leg of the XRPL check lifecycle: writing a `Check` ledger object that authorizes a designated recipient to pull funds from the sender's account at a later time. The analogy to a paper bank check is intentional — the sender (`sfAccount`) specifies a maximum drawable amount (`sfSendMax`) denominated in XRP, an IOU, or (with the `featureMPTokensV2` amendment) an MPT. The named destination can cash it via `CheckCash` for any amount up to that cap, or either party can destroy it via `CheckCancel`. This file contains all three phases of the XRPL transactor lifecycle for that creation step. + +The `CheckCreate` class inherits from `Transactor` and declares `ConsequencesFactory{Normal}`, meaning it consumes the sender's sequence number under the standard fee model with no special escalation. + +## Feature Gating + +`checkExtraFeatures()` is a static pre-screen that runs before `preflight`. Its single responsibility is to block MPT-denominated checks unless the `featureMPTokensV2` amendment is active. The expression `ctx.rules.enabled(featureMPTokensV2) || !ctx.tx[sfSendMax].holds()` is a short-circuit gate: if the amendment is enabled, any asset is allowed through; if not, an `MPTIssue` in `sfSendMax` causes immediate rejection. This pattern isolates feature-flag logic at the boundary, keeping the rest of the validation code free of amendment conditions. + +## `preflight`: Stateless Structural Validation + +`preflight()` validates the transaction fields without consulting ledger state. Three invariants are enforced: + +1. **Self-check rejection.** If `sfAccount == sfDestination`, the transaction returns `temREDUNDANT`. A check written to oneself has no economic purpose and the check mechanism cannot serve it. + +2. **`sfSendMax` integrity.** The amount must pass `isLegalNet()` (confirming it is representable on the network) and `signum() > 0` (no zero or negative amounts). Additionally, the asset must not equal `badAsset()`, guarding against a malformed currency code. These are `tem`-class errors because they reflect malformed transactions that should never have been submitted. + +3. **Expiration sanity.** `sfExpiration` is optional, but if present it must not be zero. An expiration of zero would immediately make the check unspendable, which is almost certainly a client error. + +## `preclaim`: Stateful Contextual Validation + +`preclaim()` consults the current ledger state to determine if creating the check is permissible. Validations proceed in a deliberate order, each guarded by an early `return`: + +**Destination account existence and permissions.** The destination account must exist (`tecNO_DST` if not). Its account flags are then checked: `lsfDisallowIncomingCheck` lets an account opt out of receiving checks entirely, returning `tecNO_PERMISSION`. Pseudo-accounts are also blocked with `tecNO_PERMISSION` via `isPseudoAccount(sleDst)`. The comment explains why this check is not amendment-gated: the discriminator fields that mark an account as a pseudo-account are themselves behind amendments, so the behavior automatically tracks whatever amendments are active. + +**Destination tag requirement.** If the destination has `lsfRequireDestTag` set, the transaction must include `sfDestinationTag`. This is a common convention for hosted wallets that use the tag field to route funds internally — `tecDST_TAG_NEEDED` tells the sender to retry with the tag. + +**Freeze checks for non-native assets.** When `sfSendMax` is not XRP, the code distinguishes between IOU (`Issue`) and MPT (`MPTIssue`) assets using a `visit` lambda dispatch. For IOUs, global freeze is checked first (`tecFROZEN`), then the individual trustline between the sender and the issuer, and finally the trustline between the issuer and the destination. The high/low account ordering used by `lsfHighFreeze`/`lsfLowFreeze` flags is respected explicitly. For MPTs, the parallel `isFrozen()` helper checks individual account-level freeze, returning `tecLOCKED`. Both branches only validate sender and destination when they are not the issuer themselves — the issuer cannot freeze their own side of a line for self-directed operations. + +**Pre-creation expiry.** `hasExpired()` ensures the check would not be immediately expired upon creation, returning `tecEXPIRED`. This prevents creating dead entries that consume reserve with no utility. + +**Trade capability.** The final call to `canTrade(ctx.view, ctx.tx[sfSendMax].asset())` confirms the asset supports trading at all — a catch-all relevant primarily to MPTs that may have trading disabled at the MPT level. + +## `doApply`: Ledger Mutation + +`doApply()` runs after both validation phases have passed and makes the following mutations atomically: + +**Reserve enforcement.** The check uses `preFeeBalance_` (the sender's balance before the transaction fee was deducted) against the reserve requirement for `ownerCount + 1`. Using the pre-fee balance is deliberate — it allows an account that is near the reserve floor to still pay the fee and create the check, rather than being locked out of transacting entirely. + +**SLE construction.** The `Check` ledger entry is keyed by `keylet::check(account_, seq)` where `seq` is `ctx_.tx.getSeqValue()`. Using the transaction's sequence (or ticket) value as the check's ledger key is not accidental: it makes the key deterministically computable from information the recipient already knows, without requiring a ledger lookup. All required fields (`sfAccount`, `sfDestination`, `sfSequence`, `sfSendMax`) are written unconditionally. The four optional fields — `sfSourceTag`, `sfDestinationTag`, `sfInvoiceID`, and `sfExpiration` — are written only if present in the transaction, using the `ctx_.tx[~sfField]` optional accessor pattern. + +**Dual directory insertion.** The check is inserted into both the destination's owner directory and the source's owner directory. The resulting page numbers are stored back onto the SLE as `sfDestinationNode` and `sfOwnerNode`. This is the standard XRPL pattern for objects that need to be removed during later operations: `CheckCash` and `CheckCancel` both use these stored page numbers to call `dirRemove()` in O(1) without traversing the directory tree. The `tecDIR_FULL` paths after each insertion are marked `LCOV_EXCL_LINE` because a full owner directory represents a pre-existing ledger anomaly rather than an expected validation failure. + +**Owner count adjustment.** `adjustOwnerCount(view(), sle, 1, viewJ)` increments the sender's owner count, which increases the reserve requirement for future transactions. This links back to the `doApply()` reserve check: the sender must have had enough headroom before the fee was applied to absorb this increment. + +## Relationship to Sibling Files + +`CheckCancel.cpp` and `CheckCash.cpp` in the same directory both operate on the `Check` ledger entry that `CheckCreate.cpp` produces. `CheckCancel` is the simplest of the three — it merely removes the entry from both directories and decrements the owner count. `CheckCash` is the most complex, involving the payment engine, trust line manipulation, and `PaymentSandbox`. The structural data written by `doApply()` — particularly `sfOwnerNode`, `sfDestinationNode`, and the check keylet — is what enables those sibling transactors to locate and clean up the entry without additional state. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp.ai.json b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp.ai.json new file mode 100644 index 0000000000..09311f6902 --- /dev/null +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp.ai.json @@ -0,0 +1,402 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "CredentialAccept::preflight" + ], + "entry_point": "CredentialAccept::preflight", + "purpose": "Initial stateless validation of the transaction fields before any ledger access.", + "validation_points": [ + "Checks if sfIssuer is present and nonzero.", + "Checks if sfCredentialType is non-empty and within allowed length." + ] + }, + { + "call_chain": [ + "CredentialAccept::preclaim" + ], + "entry_point": "CredentialAccept::preclaim", + "purpose": "Stateful validation: checks ledger state for issuer, credential existence, and acceptance status.", + "validation_points": [ + "Checks if issuer account exists in ledger.", + "Checks if credential (subject, issuer, credType) exists in ledger.", + "Checks if credential is already accepted (flags)." + ] + }, + { + "call_chain": [ + "CredentialAccept::doApply" + ], + "entry_point": "CredentialAccept::doApply", + "purpose": "Applies the transaction: updates ledger state, sets credential as accepted, adjusts owner counts.", + "validation_points": [ + "Checks for expired credential (calls checkExpired).", + "Checks subject and issuer account objects exist (should always pass if preclaim succeeded).", + "Checks subject has sufficient reserve." + ] + } + ], + "data_flows": [ + { + "field": "sfIssuer", + "flow": [ + "Transaction input", + "preflight: checked for presence", + "preclaim: extracted as AccountID, checked for existence in ledger", + "doApply: used to fetch issuer account SLE" + ], + "origin": "ctx.tx[sfIssuer] (transaction JSON input)", + "transformations": [ + "Parsed from transaction JSON", + "Converted to AccountID" + ], + "validated_at": "preflight (presence), preclaim (ledger existence)" + }, + { + "field": "sfCredentialType", + "flow": [ + "Transaction input", + "preflight: checked for non-empty and max length", + "preclaim: used as part of credential key", + "doApply: used as part of credential key" + ], + "origin": "ctx.tx[sfCredentialType] (transaction JSON input)", + "transformations": [ + "Parsed from transaction JSON", + "Checked for size" + ], + "validated_at": "preflight (size/empty check)" + }, + { + "field": "credential (subject, issuer, credType)", + "flow": [ + "Transaction input", + "preclaim: used to construct credential keylet", + "preclaim: checked for existence in ledger", + "doApply: used to fetch and update credential SLE" + ], + "origin": "ctx.tx[sfAccount], ctx.tx[sfIssuer], ctx.tx[sfCredentialType]", + "transformations": [ + "Combined into keylet", + "Used to fetch SLE" + ], + "validated_at": "preclaim (ledger existence)" + }, + { + "field": "credential.flags", + "flow": [ + "preclaim: read from credential SLE", + "preclaim: checked for lsfAccepted flag", + "doApply: set lsfAccepted flag" + ], + "origin": "Ledger credential SLE (sleCred->getFieldU32(sfFlags))", + "transformations": [ + "Bitwise check for lsfAccepted", + "Set lsfAccepted" + ], + "validated_at": "preclaim (duplicate check)" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "preclaim: used as subject in credential key", + "doApply: used as account_ (subject) for owner count adjustment" + ], + "origin": "ctx.tx[sfAccount] (transaction JSON input)", + "transformations": [ + "Parsed from transaction JSON" + ], + "validated_at": "Implicitly validated by presence in transaction and credential existence" + } + ], + "description": "Implements the CredentialAccept transaction logic for the XRPL, including preflight, preclaim, and apply steps for accepting credentials between accounts.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfIssuer", + "empty", + "string", + "validation" + ], + "evidence": "explicit check: if (!ctx.tx[sfIssuer]) at CredentialAccept::preflight", + "issue_pattern": "Missing empty string validation for sfIssuer", + "why_false_positive": "explicit check: if (!ctx.tx[sfIssuer]) validates sfIssuer for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfIssuer", + "format", + "validation", + "invalid" + ], + "evidence": "explicit check: if (!ctx.tx[sfIssuer]) at CredentialAccept::preflight", + "issue_pattern": "Missing format validation for sfIssuer", + "why_false_positive": "explicit check: if (!ctx.tx[sfIssuer]) validates sfIssuer format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCredentialType", + "empty", + "string", + "validation" + ], + "evidence": "explicit check: credType.empty() || (credType.size() > maxCredentialTypeLength) at CredentialAccept::preflight", + "issue_pattern": "Missing empty string validation for sfCredentialType", + "why_false_positive": "explicit check: credType.empty() || (credType.size() > maxCredentialTypeLength) validates sfCredentialType for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfIssuer", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.exists(keylet::account(issuer)) at CredentialAccept::preclaim", + "issue_pattern": "Missing empty string validation for sfIssuer", + "why_false_positive": "ctx.view.exists(keylet::account(issuer)) validates sfIssuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credential (subject, issuer, credType)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::credential(subject, issuer, credType)) at CredentialAccept::preclaim", + "issue_pattern": "Missing empty string validation for credential (subject, issuer, credType)", + "why_false_positive": "ctx.view.read(keylet::credential(subject, issuer, credType)) validates credential (subject, issuer, credType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credential.flags", + "empty", + "string", + "validation" + ], + "evidence": "(sleCred->getFieldU32(sfFlags) & lsfAccepted) != 0u at CredentialAccept::preclaim", + "issue_pattern": "Missing empty string validation for credential.flags", + "why_false_positive": "(sleCred->getFieldU32(sfFlags) & lsfAccepted) != 0u validates credential.flags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfIssuer, sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "view().peek(keylet::account(account_)), view().peek(keylet::account(issuer)) at CredentialAccept::doApply", + "issue_pattern": "Missing empty string validation for sfIssuer, sfAccount", + "why_false_positive": "view().peek(keylet::account(account_)), view().peek(keylet::account(issuer)) validates sfIssuer, sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount reserve", + "empty", + "string", + "validation" + ], + "evidence": "view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1); if (preFeeBalance_ < reserve) at CredentialAccept::doApply", + "issue_pattern": "Missing empty string validation for sfAccount reserve", + "why_false_positive": "view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1); if (preFeeBalance_ < reserve) validates sfAccount reserve for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "credential expiration", + "empty", + "string", + "validation" + ], + "evidence": "checkExpired(sleCred, view().header().parentCloseTime) at CredentialAccept::doApply", + "issue_pattern": "Missing empty string validation for credential expiration", + "why_false_positive": "checkExpired(sleCred, view().header().parentCloseTime) validates credential expiration for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 9, + "name": "CredentialAccept::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "CredentialAccept::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 29, + "name": "CredentialAccept::preclaim" + }, + { + "args": [], + "lineno": 54, + "name": "CredentialAccept::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for CredentialAccept is likely found in unit/integration test files such as 'test/tx/Credential_test.cpp', 'test/tx/CredentialAccept_test.cpp', or similar. These should cover: missing/invalid sfIssuer, invalid sfCredentialType size, non-existent issuer, non-existent credential, already accepted credential, expired credential, and insufficient reserve. Gaps may include: edge cases for credential type length, malformed credential SLEs, and rare ledger state races. Coverage for doApply's error paths (e.g., tefINTERNAL) may be limited, as these are hard to trigger in normal operation.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl transaction processing (custom, not external framework)", + "validation_layer": "entry_point (preflight), middleware (preclaim), business_logic (doApply)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID_ACCOUNT_ID", + "field": "sfIssuer", + "location": "CredentialAccept::preflight", + "validated_by": "explicit check: if (!ctx.tx[sfIssuer])", + "validates": [ + "Presence of Issuer field (non-zero)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfCredentialType", + "location": "CredentialAccept::preflight", + "validated_by": "explicit check: credType.empty() || (credType.size() > maxCredentialTypeLength)", + "validates": [ + "CredentialType field is not empty", + "CredentialType field does not exceed maxCredentialTypeLength" + ], + "validation_type": "format|range" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ISSUER", + "field": "sfIssuer", + "location": "CredentialAccept::preclaim", + "validated_by": "ctx.view.exists(keylet::account(issuer))", + "validates": [ + "Issuer account exists in ledger" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "credential (subject, issuer, credType)", + "location": "CredentialAccept::preclaim", + "validated_by": "ctx.view.read(keylet::credential(subject, issuer, credType))", + "validates": [ + "Credential entry exists in ledger" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecDUPLICATE", + "field": "credential.flags", + "location": "CredentialAccept::preclaim", + "validated_by": "(sleCred->getFieldU32(sfFlags) & lsfAccepted) != 0u", + "validates": [ + "Credential has not already been accepted" + ], + "validation_type": "business_logic|state" + }, + { + "confidence": 0.9, + "error_thrown": "tefINTERNAL", + "field": "sfIssuer, sfAccount", + "location": "CredentialAccept::doApply", + "validated_by": "view().peek(keylet::account(account_)), view().peek(keylet::account(issuer))", + "validates": [ + "Subject and Issuer accounts exist (should always pass if preclaim succeeded)" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecINSUFFICIENT_RESERVE", + "field": "sfAccount reserve", + "location": "CredentialAccept::doApply", + "validated_by": "view().fees().accountReserve(sleSubject->getFieldU32(sfOwnerCount) + 1); if (preFeeBalance_ < reserve)", + "validates": [ + "Subject account has sufficient reserve for new object" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 0.9, + "error_thrown": "not shown in snippet, but likely error or deletion", + "field": "credential expiration", + "location": "CredentialAccept::doApply", + "validated_by": "checkExpired(sleCred, view().header().parentCloseTime)", + "validates": [ + "Credential is not expired" + ], + "validation_type": "business_logic|state" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp.ai.md b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp.ai.md new file mode 100644 index 0000000000..fb57876224 --- /dev/null +++ b/src/libxrpl/tx/transactors/credentials/CredentialAccept.cpp.ai.md @@ -0,0 +1,40 @@ +# CredentialAccept.cpp + +## Role in the System + +`CredentialAccept` implements the `CredentialAccept` transaction for the XRPL's W3C-aligned Verifiable Credentials feature. It sits at the end of the two-step credential issuance handshake: a credential begins its life when an issuer submits `CredentialCreate`, which places the credential object in both the issuer's and subject's owner directories but counts the reserve cost against the issuer alone. The subject must then submit a `CredentialAccept` transaction to signal consent, at which point ownership — and the associated XRP reserve burden — transfers from issuer to subject. Until that acceptance, the credential exists in the ledger but carries the `lsfAccepted` flag cleared, making it invisible to any authorization checks downstream. + +## Transaction Phases + +Like every XRPL transactor, the logic is split across three static/virtual hooks executed sequentially by the engine. + +**`getFlagsMask`** gates which transaction flags are meaningful. When the `fixInvalidTxFlags` amendment is active, it returns `tfUniversalMask`, causing the framework to reject any unknown flags set by the sender. Before the amendment, returning `0` suppressed this check entirely — a legacy behavior preserved for replay compatibility. + +**`preflight`** performs pure field-level validation with no ledger access. It checks that `sfIssuer` is non-zero (an all-zero account ID is never a valid address) and that `sfCredentialType` is both non-empty and no longer than `maxCredentialTypeLength`. These two invariants are sufficient to reject obviously malformed transactions early, before any state reads are attempted. + +**`preclaim`** performs read-only ledger validation. It verifies that the issuer's account root actually exists (`tecNO_ISSUER`), that a credential object keyed on `(subject, issuer, credentialType)` is present in the ledger (`tecNO_ENTRY`), and that the credential has not already been accepted (`tecDUPLICATE`). The order matters: checking the credential's existence before checking the flag avoids a null-dereference on the SLE pointer. + +**`doApply`** is where the state mutations occur. + +## doApply: Design Decisions + +**Reserve check deferred to doApply.** The reserve is checked against `preFeeBalance_` — the subject's balance snapshot taken *before* the transaction fee is deducted — rather than checking in `preclaim`. Doing so in `preclaim` would use the pre-fee balance too, but only `doApply` has access to `preFeeBalance_` as a member of the `Transactor` base class. The check requires the owner count incremented by one to compute the required reserve for the incoming credential object. + +**Expiry check with active cleanup.** Even though `preclaim` validates that the credential exists, its expiry is not checked there. Instead, `checkExpired` is called in `doApply` against `view().header().parentCloseTime`. The reason: the XRPL design philosophy intentionally delays cleanup of expired objects until a transaction tries to use them. When an expiry is detected here, `credentials::deleteSLE` removes the credential from the ledger and its owner directories entirely, even though the transaction itself returns `tecEXPIRED`. This is a non-trivial detail: `tec`-class errors in XRPL *do* commit state changes (the fee is charged and any side effects — including this deletion — are persisted), which means the ledger cleans itself up opportunistically at the cost of an acceptance attempt. + +**Ownership transfer via `adjustOwnerCount`.** The core semantic work is two lines: + +```cpp +adjustOwnerCount(view(), sleIssuer, -1, j_); +adjustOwnerCount(view(), sleSubject, 1, j_); +``` + +`CredentialCreate` placed the credential in both directories but only increased the issuer's owner count. `CredentialAccept` completes the transfer by decrementing the issuer's count and incrementing the subject's, shifting the reserve obligation. The credential's directory membership itself is unchanged — both entries were written at creation time. + +**Invariant guard for `tefINTERNAL`.** After `preclaim` establishes that the credential exists — which implicitly means both account roots exist — `doApply` still re-fetches both account SLEs with `view().peek()` and returns `tefINTERNAL` if either is missing. This is marked `LCOV_EXCL_LINE` because the code path should be unreachable in correct operation: it guards against theoretical inconsistency in the ledger state that preclaim cannot prevent between validation and application phases. + +## Relationship to Sibling Transactors + +`CredentialCreate` establishes the ledger object and increments the issuer's reserve. `CredentialAccept` (this file) acknowledges it and transfers the reserve. `CredentialDelete` can be submitted by either party to remove the credential entirely, releasing the reserve back to whoever currently holds ownership. Together they form a complete lifecycle that maps to the W3C Verifiable Credentials model: issue → accept → optionally revoke. + +The `credentials::deleteSLE` helper called on expiry in `doApply` is the same routine used by `CredentialDelete`, ensuring consistent directory cleanup regardless of which removal path is taken. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp.ai.json b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp.ai.json new file mode 100644 index 0000000000..4137e9e84f --- /dev/null +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp.ai.json @@ -0,0 +1,329 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "CredentialCreate::preflight" + ], + "entry_point": "CredentialCreate::preflight", + "purpose": "Performs initial stateless validation of the transaction fields before any ledger access.", + "validation_points": [ + "CredentialCreate::preflight" + ] + }, + { + "call_chain": [ + "CredentialCreate::preclaim" + ], + "entry_point": "CredentialCreate::preclaim", + "purpose": "Performs stateful validation, checking ledger state for existence of subject and duplicate credentials.", + "validation_points": [ + "CredentialCreate::preclaim" + ] + }, + { + "call_chain": [ + "CredentialCreate::doApply" + ], + "entry_point": "CredentialCreate::doApply", + "purpose": "Applies the transaction to the ledger, assuming all previous validations passed.", + "validation_points": [ + "CredentialCreate::doApply (expiration check, reserve check)" + ] + } + ], + "data_flows": [ + { + "field": "sfSubject", + "flow": [ + "ctx.tx[sfSubject]", + "CredentialCreate::preflight (checked for presence)", + "CredentialCreate::preclaim (used for account existence check)", + "CredentialCreate::doApply (used to create credential SLE)" + ], + "origin": "ctx.tx[sfSubject] (transaction input)", + "transformations": [ + "Checked for presence (not null)", + "Used as keylet::account(subject) for existence", + "Used as keylet::credential(subject, account, credType)" + ], + "validated_at": "preflight (presence), preclaim (account existence)" + }, + { + "field": "sfURI", + "flow": [ + "ctx.tx[~sfURI]", + "CredentialCreate::preflight (checked for size/emptiness)", + "CredentialCreate::doApply (set on SLE if present)" + ], + "origin": "ctx.tx[~sfURI] (optional transaction input)", + "transformations": [ + "Checked for empty or oversized", + "Set as fieldVL on SLE if present" + ], + "validated_at": "preflight" + }, + { + "field": "sfCredentialType", + "flow": [ + "ctx.tx[sfCredentialType]", + "CredentialCreate::preflight (checked for size/emptiness)", + "CredentialCreate::preclaim (used for duplicate check)", + "CredentialCreate::doApply (set on SLE)" + ], + "origin": "ctx.tx[sfCredentialType] (transaction input)", + "transformations": [ + "Checked for empty or oversized", + "Used as part of credential keylet for duplicate check", + "Set as fieldVL on SLE" + ], + "validated_at": "preflight (size/emptiness), preclaim (duplicate check)" + }, + { + "field": "sfExpiration", + "flow": [ + "ctx.tx[~sfExpiration]", + "CredentialCreate::doApply (checked if present, compared to closeTime, set on SLE)" + ], + "origin": "ctx.tx[~sfExpiration] (optional transaction input)", + "transformations": [ + "If present, compared to closeTime for expiration", + "Set as fieldU32 on SLE if valid" + ], + "validated_at": "doApply (expiration check)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "CredentialCreate::preclaim (used as issuer in keylet::credential)", + "CredentialCreate::doApply (used as issuer, owner directory, SLE field)" + ], + "origin": "ctx.tx[sfAccount] (transaction input, issuer)", + "transformations": [ + "Used as part of credential keylet", + "Set as sfIssuer on SLE" + ], + "validated_at": "preclaim (duplicate check), doApply (reserve check)" + } + ], + "description": "Implements the CredentialCreate transaction logic for creating verifiable credentials (VCs) on the XRPL, including preflight, preclaim, and apply steps, handling validation, existence checks, and ledger updates.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSubject", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (!tx[sfSubject])) at preflight", + "issue_pattern": "Missing empty string validation for sfSubject", + "why_false_positive": "explicit check (if (!tx[sfSubject])) validates sfSubject for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfURI", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (uri && (uri->empty() || (uri->size() > maxCredentialURILength))) at preflight", + "issue_pattern": "Missing empty string validation for sfURI", + "why_false_positive": "explicit check (uri && (uri->empty() || (uri->size() > maxCredentialURILength))) validates sfURI for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCredentialType", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (credType.empty() || (credType.size() > maxCredentialTypeLength)) at preflight", + "issue_pattern": "Missing empty string validation for sfCredentialType", + "why_false_positive": "explicit check (credType.empty() || (credType.size() > maxCredentialTypeLength)) validates sfCredentialType for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSubject", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.exists(keylet::account(subject)) at preclaim", + "issue_pattern": "Missing empty string validation for sfSubject", + "why_false_positive": "ctx.view.exists(keylet::account(subject)) validates sfSubject for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credential (composite: subject, account, credType)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType)) at preclaim", + "issue_pattern": "Missing empty string validation for credential (composite: subject, account, credType)", + "why_false_positive": "ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType)) validates credential (composite: subject, account, credType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfExpiration", + "empty", + "string", + "validation" + ], + "evidence": "if (closeTime > *optExp) at doApply", + "issue_pattern": "Missing empty string validation for sfExpiration", + "why_false_positive": "if (closeTime > *optExp) validates sfExpiration for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 27, + "name": "CredentialCreate::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 32, + "name": "CredentialCreate::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 52, + "name": "CredentialCreate::preclaim" + }, + { + "args": [], + "lineno": 67, + "name": "CredentialCreate::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ], + "test_coverage_notes": "Tests for CredentialCreate are likely found in unit/integration test files such as 'test/tx/Credential_test.cpp', 'test/tx/Transactor_test.cpp', or similar. These should cover: missing/invalid sfSubject, oversized/empty sfURI, oversized/empty sfCredentialType, non-existent subject account, duplicate credential, expired credential, and insufficient reserve. Gaps may exist for edge cases like boundary sizes for URI/type, malformed SLE creation, or rare internal errors (tefINTERNAL). No explicit test references are present in this file, so coverage should be verified in the test suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in transaction preflight/preclaim/apply pattern (xrpld transaction engine)", + "validation_layer": "business_logic (preflight, preclaim, doApply)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfSubject", + "location": "preflight", + "validated_by": "explicit check (if (!tx[sfSubject]))", + "validates": [ + "field must be present" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfURI", + "location": "preflight", + "validated_by": "explicit check (uri && (uri->empty() || (uri->size() > maxCredentialURILength)))", + "validates": [ + "if present, must not be empty", + "if present, must not exceed maxCredentialURILength" + ], + "validation_type": "format|range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfCredentialType", + "location": "preflight", + "validated_by": "explicit check (credType.empty() || (credType.size() > maxCredentialTypeLength))", + "validates": [ + "must not be empty", + "must not exceed maxCredentialTypeLength" + ], + "validation_type": "format|range" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_TARGET", + "field": "sfSubject", + "location": "preclaim", + "validated_by": "ctx.view.exists(keylet::account(subject))", + "validates": [ + "subject account must exist in ledger" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecDUPLICATE", + "field": "credential (composite: subject, account, credType)", + "location": "preclaim", + "validated_by": "ctx.view.exists(keylet::credential(subject, ctx.tx[sfAccount], credType))", + "validates": [ + "credential must not already exist for (subject, account, credType)" + ], + "validation_type": "business_logic|uniqueness" + }, + { + "confidence": 0.8, + "error_thrown": "not shown (incomplete code, but likely error or rejection)", + "field": "sfExpiration", + "location": "doApply", + "validated_by": "if (closeTime > *optExp)", + "validates": [ + "expiration must not be in the past relative to parentCloseTime" + ], + "validation_type": "range|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp.ai.md b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp.ai.md new file mode 100644 index 0000000000..1568afc306 --- /dev/null +++ b/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp.ai.md @@ -0,0 +1,57 @@ +# `CredentialCreate.cpp` — XRPL Verifiable Credential Issuance + +## Purpose and Context + +This file implements the `CredentialCreate` transaction, which allows an account (the issuer) to issue a W3C-style [Verifiable Credential](https://www.w3.org/TR/vc-data-model-2.0/) on the XRP Ledger. The credential binds a subject account, an issuer account, and an arbitrary `CredentialType` blob into an on-ledger object (`ltCREDENTIAL`) that third parties can query without contacting the issuer. Together with `CredentialAccept` and `CredentialDelete` (sibling files in the same directory), this forms a three-transaction lifecycle for managing credentials. + +The transactor follows the standard XRPL three-phase pattern — `preflight`, `preclaim`, and `doApply` — with each phase having well-defined responsibilities and error semantics. + +## Three-Phase Lifecycle + +### `getFlagsMask` and `preflight` + +`getFlagsMask` returns `tfUniversalMask` only when the `fixInvalidTxFlags` amendment is active; otherwise it returns 0 (allowing any flags). This gating prevents protocol inconsistencies on networks where the amendment has not yet deployed. + +`preflight` is purely stateless — it never touches the ledger view — so its results can be cached and reused across retry batches. It validates three fields: + +- **`sfSubject`** must be present (non-zero `AccountID`). A missing subject yields `temMALFORMED`. +- **`sfURI`** is optional, but if present it must not be empty and must not exceed `maxCredentialURILength` (256 bytes, from `Protocol.h`). An empty URI is explicitly rejected because it would be indistinguishable from a missing one at retrieval time. +- **`sfCredentialType`** is required, must not be empty, and must not exceed `maxCredentialTypeLength` (64 bytes). This field is part of the composite ledger key, so size bounds are enforced here rather than at insert time. + +All `preflight` failures return `tem…` codes, which mark the transaction as permanently malformed; no fee is consumed. + +### `preclaim` + +`preclaim` performs two ledger-state checks that are cheap to evaluate without producing side effects: + +1. The subject account must exist (`keylet::account(subject)`). Issuing a credential against a non-existent account would create a dangling reference, returned as `tecNO_TARGET`. +2. A credential with the same `(subject, issuer, credentialType)` composite key must not already exist on the ledger. The uniqueness check uses `keylet::credential(subject, ctx.tx[sfAccount], credType)` — note that the transaction's `sfAccount` field is the issuer. Duplicates return `tecDUPLICATE`. + +The reason the reserve check does not appear here (but does in `doApply`) is subtle: reserve requirements can change between `preclaim` and `doApply` if a fee change amendment activates mid-stream. Moving it to `doApply` is the conservative, correct choice. + +### `doApply` + +`doApply` is where the ledger is actually mutated. Several non-obvious decisions are worth noting: + +**Expiration is validated here, not in `preflight`.** Because `sfExpiration` is a ledger close-time offset and the definitive `parentCloseTime` is only available inside the apply view's header, comparing the expiration against `preflight`'s notional "now" would be unreliable. The comparison `closeTime > *optExp` uses the actual close time from the ledger header, returning `tecEXPIRED` if the credential would be born already-expired. + +**Reserve is checked against `preFeeBalance_`**, the issuer's balance *before* the transaction fee was deducted. This matches the general XRPL convention that reserve enforcement uses the pre-fee balance so the fee itself cannot push an account below its reserve in an inconsistent way. + +**Self-issuance takes a shortcut.** When `subject == account_` (the issuer is also the subject), the credential SLE has `lsfAccepted` set immediately and is inserted only into the issuer/subject's single owner directory. A second `CredentialAccept` transaction would be redundant for this case. This is an important UX shortcut for self-attested claims. + +**Third-party issuance creates a two-directory entry.** When the issuer and subject are distinct, the credential is inserted into *both* the issuer's and the subject's owner directories, with `sfIssuerNode` and `sfSubjectNode` recording the respective page numbers for O(1) deletion later. However, only the issuer's `ownerCount` is incremented — the subject bears no reserve cost until they call `CredentialAccept`, which transfers ownership (increments subject's count, decrements issuer's). If the subject never accepts, the issuer remains responsible for the reserve and can call `CredentialDelete` to reclaim it. This ownership model is the central invariant connecting all three sibling transactors. + +**`tefINTERNAL` guards** on `sleCred` creation failure and on a missing issuer SLE are marked `LCOV_EXCL_LINE`, indicating they represent impossible runtime states given correct ledger integrity — but are left in for defensive completeness. + +## Relationships to Sibling Files + +- `CredentialAccept.cpp` consumes the pending credential created here: it checks `lsfAccepted == 0`, verifies the credential isn't expired, then sets `lsfAccepted`, decrements the issuer's owner count, and increments the subject's — completing the reserve transfer. +- `CredentialDelete.cpp` handles teardown: it removes the SLE and cleans up both directory entries, decrementing only one owner count (whichever party currently holds ownership). +- `CredentialHelpers.h` / `CredentialHelpers.cpp` provides shared utilities like `checkExpired`, `deleteSLE`, and `valid` used across all three transactors — none are called from `CredentialCreate` itself since it produces a new object rather than consuming an existing one. + +## Key Invariants + +- The composite key `(subject, issuer, credentialType)` is globally unique on the ledger; `preclaim` enforces this. +- Before acceptance, the issuer holds the reserve burden. After `CredentialAccept`, the subject holds it. +- Self-issued credentials are born accepted; third-party credentials start unaccepted. +- An expired-at-creation credential is rejected at `doApply` rather than silently stored and immediately expired, preventing zombie entries that would waste storage and confuse downstream authorization checks. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp.ai.json b/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp.ai.json new file mode 100644 index 0000000000..7db865c496 --- /dev/null +++ b/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp.ai.json @@ -0,0 +1,328 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "CredentialDelete::preflight" + ], + "entry_point": "CredentialDelete::preflight", + "purpose": "Performs initial transaction validation: checks presence and validity of sfSubject, sfIssuer, and sfCredentialType fields.", + "validation_points": [ + "Checks if neither sfSubject nor sfIssuer is present (temMALFORMED)", + "Checks if either sfSubject or sfIssuer is zero (temINVALID_ACCOUNT_ID)", + "Checks if sfCredentialType is empty or too long (temMALFORMED)" + ] + }, + { + "call_chain": [ + "CredentialDelete::preclaim" + ], + "entry_point": "CredentialDelete::preclaim", + "purpose": "Checks if the credential to be deleted actually exists in the ledger.", + "validation_points": [ + "Checks existence of credential in ledger (tecNO_ENTRY)" + ] + }, + { + "call_chain": [ + "CredentialDelete::doApply" + ], + "entry_point": "CredentialDelete::doApply", + "purpose": "Performs the actual deletion, with additional checks for permissions and credential expiration.", + "validation_points": [ + "Checks existence of credential in ledger (tefINTERNAL)", + "Checks if the credential is expired or owned by the transaction account (tecNO_PERMISSION)" + ] + } + ], + "data_flows": [ + { + "field": "sfSubject", + "flow": [ + "Transaction input", + "preflight: checked for presence and zero value", + "preclaim: value_or(account) used to default to sfAccount if missing", + "doApply: value_or(account_) used again" + ], + "origin": "ctx.tx[~sfSubject] (transaction input)", + "transformations": [ + "Optional field; defaults to sfAccount if not present in preclaim/doApply" + ], + "validated_at": "preflight (presence and zero check)" + }, + { + "field": "sfIssuer", + "flow": [ + "Transaction input", + "preflight: checked for presence and zero value", + "preclaim: value_or(account) used to default to sfAccount if missing", + "doApply: value_or(account_) used again" + ], + "origin": "ctx.tx[~sfIssuer] (transaction input)", + "transformations": [ + "Optional field; defaults to sfAccount if not present in preclaim/doApply" + ], + "validated_at": "preflight (presence and zero check)" + }, + { + "field": "sfCredentialType", + "flow": [ + "Transaction input", + "preflight: checked for empty or too long", + "preclaim: used as part of credential key", + "doApply: used as part of credential key" + ], + "origin": "ctx.tx[sfCredentialType] (transaction input)", + "transformations": [ + "No transformation; used directly" + ], + "validated_at": "preflight (empty/length check)" + }, + { + "field": "credential existence (subject, issuer, credType)", + "flow": [ + "preclaim: ctx.view.exists(keylet::credential(subject, issuer, credType))", + "doApply: view().peek(keylet::credential(subject, issuer, credType))" + ], + "origin": "Ledger state (keylet::credential)", + "transformations": [ + "None; just checked for existence" + ], + "validated_at": "preclaim (existence check), doApply (existence check)" + } + ], + "description": "Implements the CredentialDelete transaction logic for deleting credentials in the XRPL ledger, including preflight, preclaim, and apply steps.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSubject and sfIssuer", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (!subject && !issuer)) at CredentialDelete::preflight", + "issue_pattern": "Missing empty string validation for sfSubject and sfIssuer", + "why_false_positive": "explicit check (if (!subject && !issuer)) validates sfSubject and sfIssuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSubject and sfIssuer", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if ((subject && subject->isZero()) || (issuer && issuer->isZero()))) at CredentialDelete::preflight", + "issue_pattern": "Missing empty string validation for sfSubject and sfIssuer", + "why_false_positive": "explicit check (if ((subject && subject->isZero()) || (issuer && issuer->isZero()))) validates sfSubject and sfIssuer for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfSubject and sfIssuer", + "format", + "validation", + "invalid" + ], + "evidence": "explicit check (if ((subject && subject->isZero()) || (issuer && issuer->isZero()))) at CredentialDelete::preflight", + "issue_pattern": "Missing format validation for sfSubject and sfIssuer", + "why_false_positive": "explicit check (if ((subject && subject->isZero()) || (issuer && issuer->isZero()))) validates sfSubject and sfIssuer format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCredentialType", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (credType.empty() || (credType.size() > maxCredentialTypeLength))) at CredentialDelete::preflight", + "issue_pattern": "Missing empty string validation for sfCredentialType", + "why_false_positive": "explicit check (if (credType.empty() || (credType.size() > maxCredentialTypeLength))) validates sfCredentialType for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credential existence (subject, issuer, credType)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.exists(keylet::credential(subject, issuer, credType)) at CredentialDelete::preclaim", + "issue_pattern": "Missing empty string validation for credential existence (subject, issuer, credType)", + "why_false_positive": "ctx.view.exists(keylet::credential(subject, issuer, credType)) validates credential existence (subject, issuer, credType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credential existence (subject, issuer, credType)", + "empty", + "string", + "validation" + ], + "evidence": "view().peek(keylet::credential(subject, issuer, credType)) at CredentialDelete::doApply", + "issue_pattern": "Missing empty string validation for credential existence (subject, issuer, credType)", + "why_false_positive": "view().peek(keylet::credential(subject, issuer, credType)) validates credential existence (subject, issuer, credType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credential expiration and permission", + "empty", + "string", + "validation" + ], + "evidence": "checkExpired(sleCred, ctx_.view().header().parentCloseTime) at CredentialDelete::doApply", + "issue_pattern": "Missing empty string validation for credential expiration and permission", + "why_false_positive": "checkExpired(sleCred, ctx_.view().header().parentCloseTime) validates credential expiration and permission for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "CredentialDelete::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "CredentialDelete::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 41, + "name": "CredentialDelete::preclaim" + }, + { + "args": [], + "lineno": 52, + "name": "CredentialDelete::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this code would be in unit/integration tests for credential transactions, likely in files such as test/tx/CredentialDelete_test.cpp or similar. Tests should cover: missing fields (sfSubject/sfIssuer), zeroed fields, invalid credential type length, non-existent credential, permission checks (deleting non-expired credential not owned by account), and successful deletion. Gaps may exist if tests do not cover all malformed/invalid input combinations, or edge cases like both sfSubject and sfIssuer present, or credential expiration edge cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in transactor pattern (xrpl core, no external framework)", + "validation_layer": "business_logic (preflight, preclaim, doApply)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfSubject and sfIssuer", + "location": "CredentialDelete::preflight", + "validated_by": "explicit check (if (!subject && !issuer))", + "validates": [ + "At least one of sfSubject or sfIssuer must be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_ACCOUNT_ID", + "field": "sfSubject and sfIssuer", + "location": "CredentialDelete::preflight", + "validated_by": "explicit check (if ((subject && subject->isZero()) || (issuer && issuer->isZero())))", + "validates": [ + "sfSubject and sfIssuer must not be zeroed AccountIDs" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfCredentialType", + "location": "CredentialDelete::preflight", + "validated_by": "explicit check (if (credType.empty() || (credType.size() > maxCredentialTypeLength)))", + "validates": [ + "sfCredentialType must not be empty", + "sfCredentialType must not exceed maxCredentialTypeLength" + ], + "validation_type": "format|range" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "credential existence (subject, issuer, credType)", + "location": "CredentialDelete::preclaim", + "validated_by": "ctx.view.exists(keylet::credential(subject, issuer, credType))", + "validates": [ + "Credential entry must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "credential existence (subject, issuer, credType)", + "location": "CredentialDelete::doApply", + "validated_by": "view().peek(keylet::credential(subject, issuer, credType))", + "validates": [ + "Credential entry must exist in ledger (double check)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "credential expiration and permission", + "location": "CredentialDelete::doApply", + "validated_by": "checkExpired(sleCred, ctx_.view().header().parentCloseTime)", + "validates": [ + "If neither subject nor issuer is the transaction account, credential must be expired to delete" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp.ai.md b/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp.ai.md new file mode 100644 index 0000000000..7128c33ceb --- /dev/null +++ b/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp.ai.md @@ -0,0 +1,56 @@ +# `CredentialDelete.cpp` — Credential Deletion Transactor + +## Role in the System + +`CredentialDelete` implements the transaction handler for removing on-ledger verifiable credential objects from the XRP Ledger. Credentials, modeled after the W3C Verifiable Credentials specification, are issued by one account (`sfIssuer`) and addressed to a subject account (`sfSubject`). Once created they persist in ledger state indefinitely unless explicitly deleted or, in some flows, cleaned up as a side-effect of other transactions. This transactor provides the explicit deletion path. + +Like all transactors in the XRPL codebase, `CredentialDelete` follows the three-phase lifecycle: `preflight` for stateless format checks, `preclaim` for read-only ledger validation, and `doApply` for the actual state mutation. + +## Transaction Field Semantics + +A notable design choice is that both `sfSubject` and `sfIssuer` are optional in the transaction, but at least one must be present. When either field is absent, `preclaim` and `doApply` silently substitute the transaction's signing account (`account_`) via `.value_or(account)`. This means: + +- An issuer deleting their own credential only needs to provide `sfSubject`. +- A subject deleting a credential they hold only needs to provide `sfIssuer`. +- The issuer and subject can be the same account (self-issued credential), in which case neither field is technically necessary beyond the check that at least one must appear. + +The `preflight` rejects zeroed `AccountID` values for either field, and validates `sfCredentialType` is non-empty and within `maxCredentialTypeLength`. These are formatting invariants the ledger enforces uniformly across all credential transactions. + +## Permission Model in `doApply` + +The most architecturally significant logic lives in the permission check in `doApply`: + +```cpp +if ((subject != account_) && (issuer != account_) && + !checkExpired(sleCred, ctx_.view().header().parentCloseTime)) +{ + return tecNO_PERMISSION; +} +``` + +This enforces three principals who may delete a credential: +1. **The subject** (credential holder) — may delete at any time. +2. **The issuer** — may delete at any time (issuers can revoke credentials they've created). +3. **Any third party** — may only delete if the credential has already expired. + +The third-party path exists for ledger hygiene. Once a credential's expiration has passed it becomes inert but continues to consume reserve space. Allowing anyone to sweep expired credentials incentivizes garbage collection without compromising active credential integrity. Expiry is checked against `parentCloseTime` — the close time of the *parent* ledger rather than the current one — which is a consensus-deterministic value available at apply time without ambiguity. + +## Existence Checks Across Phases + +`preclaim` verifies the credential exists using `ctx.view.exists(keylet::credential(...))`, returning `tecNO_ENTRY` if not found. `doApply` then re-fetches the SLE via `view().peek()`. If `peek` returns null after `preclaim` confirmed existence, the code returns `tefINTERNAL` (marked `LCOV_EXCL_LINE` — unreachable in practice). This double-check is defensive: `tef` errors signal ledger corruption rather than user error, and they abort transaction application rather than charging a fee, which distinguishes them semantically from `tec` errors. + +## Actual Deletion via `deleteSLE` + +`doApply` delegates the low-level removal to `credentials::deleteSLE()` defined in `CredentialHelpers.cpp`. That function handles the non-trivial aspects of credential lifecycle: + +- A credential is tracked in *two* owner directories: one for the issuer (`sfIssuerNode`) and, once accepted, one for the subject (`sfSubjectNode`). +- Owner counts are maintained per-account for reserve calculation. Before acceptance (`lsfAccepted` flag unset), the reserve is charged to the issuer; after acceptance it transfers to the subject. `deleteSLE` inspects the `lsfAccepted` flag to correctly decrement the right account's owner count. +- Both directory entries and the SLE itself are removed, releasing the reserve held by whichever account owned the object. + +## `getFlagsMask` and Amendment Gating + +`getFlagsMask` returns `tfUniversalMask` when the `fixInvalidTxFlags` amendment is active, or `0` (allow any flags) when it is not. The value `0` is a sentinel in the XRPL transactor framework meaning "accept all flag bits without complaint," preserving backward compatibility for transactions submitted before the amendment enabled stricter flag enforcement. + +## Relationship to Sibling Transactors + +`CredentialCreate` creates the SLE, inserts it into the issuer's owner directory, and optionally the subject's directory. `CredentialAccept` transfers ownership from issuer to subject by setting `lsfAccepted`. `CredentialDelete` is the terminal operation, and its permission model mirrors the lifecycle: either party may close out the relationship, and the ledger will also accept deletion from any party once the credential's utility has expired. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp.ai.json b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp.ai.json new file mode 100644 index 0000000000..fa085b4beb --- /dev/null +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp.ai.json @@ -0,0 +1,562 @@ +{ + "args": [ + { + "lineno": 10, + "name": "ctx" + }, + { + "lineno": 32, + "name": "ctx" + }, + { + "lineno": 91, + "name": "view" + }, + { + "lineno": 91, + "name": "sle" + }, + { + "lineno": 91, + "name": "account" + }, + { + "lineno": 91, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "DelegateSet::preflight" + ], + "entry_point": "DelegateSet::preflight", + "purpose": "Performs stateless validation of the DelegateSet transaction before it is accepted for further processing.", + "validation_points": [ + "permissions.size() > permissionMaxSize", + "ctx.tx[sfAccount] == ctx.tx[sfAuthorize]", + "!permissionSet.insert(permission[sfPermissionValue]).second", + "!Permission::getInstance().isDelegable(permission[sfPermissionValue], ctx.rules)" + ] + }, + { + "call_chain": [ + "DelegateSet::preclaim" + ], + "entry_point": "DelegateSet::preclaim", + "purpose": "Performs stateful validation, checking ledger state for existence of accounts and delegate objects.", + "validation_points": [ + "!ctx.view.exists(keylet::account(ctx.tx[sfAccount]))", + "!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize]))", + "ctx.tx.getFieldArray(sfPermissions).empty() && !ctx.view.exists(keylet::delegate(ctx.tx[sfAccount], ctx.tx[sfAuthorize]))" + ] + }, + { + "call_chain": [ + "DelegateSet::doApply", + "DelegateSet::deleteDelegate (conditionally called)" + ], + "entry_point": "DelegateSet::doApply", + "purpose": "Applies the transaction to the ledger, creating, updating, or deleting delegate objects as appropriate.", + "validation_points": [ + "!sleOwner", + "permissions.empty() (triggers deleteDelegate)", + "preFeeBalance_ < reserve", + "!page" + ] + }, + { + "call_chain": [ + "DelegateSet::deleteDelegate" + ], + "entry_point": "DelegateSet::deleteDelegate", + "purpose": "Removes a delegate object from the ledger and updates owner count.", + "validation_points": [ + "!sle", + "!view.dirRemove(...)", + "!sleOwner" + ] + } + ], + "data_flows": [ + { + "field": "sfPermissions", + "flow": [ + "Transaction input", + "preflight: checked for size, uniqueness, delegability", + "preclaim: checked for emptiness (delete intent)", + "doApply: used to update or create delegate SLE" + ], + "origin": "ctx.tx.getFieldArray(sfPermissions) (transaction input)", + "transformations": [ + "Checked for array size limit", + "Checked for duplicate permission values", + "Checked for delegability of each permission", + "Written to ledger SLE if valid" + ], + "validated_at": "preflight (size, uniqueness, delegability), preclaim (emptiness for delete)" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "preflight: checked for self-authorization", + "preclaim: checked for existence in ledger", + "doApply: used to look up and update account SLE" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared to sfAuthorize for self-authorization check", + "Used to generate keylets for account and delegate objects" + ], + "validated_at": "preflight (self-authorization), preclaim (account existence)" + }, + { + "field": "sfAuthorize", + "flow": [ + "Transaction input", + "preflight: checked for self-authorization", + "preclaim: checked for existence in ledger", + "doApply: used to look up delegate SLE" + ], + "origin": "ctx.tx[sfAuthorize] (transaction input)", + "transformations": [ + "Compared to sfAccount for self-authorization check", + "Used to generate keylets for delegate objects" + ], + "validated_at": "preflight (self-authorization), preclaim (account existence)" + }, + { + "field": "sfPermissionValue", + "flow": [ + "Transaction input (array element)", + "preflight: checked for uniqueness and delegability", + "doApply: written to ledger SLE if valid" + ], + "origin": "permission[sfPermissionValue] (from each element of sfPermissions array)", + "transformations": [ + "Inserted into permissionSet for uniqueness check", + "Checked via Permission::getInstance().isDelegable" + ], + "validated_at": "preflight (uniqueness, delegability)" + } + ], + "description": "Implements the DelegateSet transaction logic for the XRPL, including preflight, preclaim, apply, and deletion of delegate objects with permission checks and ledger updates.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPermissions (array)", + "empty", + "string", + "validation" + ], + "evidence": "permissions.size() > permissionMaxSize at DelegateSet::preflight", + "issue_pattern": "Missing empty string validation for sfPermissions (array)", + "why_false_positive": "permissions.size() > permissionMaxSize validates sfPermissions (array) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfPermissions (array)", + "range", + "bounds", + "validation" + ], + "evidence": "permissions.size() > permissionMaxSize at DelegateSet::preflight", + "issue_pattern": "Missing range validation for sfPermissions (array)", + "why_false_positive": "permissions.size() > permissionMaxSize validates sfPermissions (array) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount and sfAuthorize", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx[sfAccount] == ctx.tx[sfAuthorize] at DelegateSet::preflight", + "issue_pattern": "Missing empty string validation for sfAccount and sfAuthorize", + "why_false_positive": "ctx.tx[sfAccount] == ctx.tx[sfAuthorize] validates sfAccount and sfAuthorize for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPermissionValue (within sfPermissions array)", + "empty", + "string", + "validation" + ], + "evidence": "!permissionSet.insert(permission[sfPermissionValue]).second at DelegateSet::preflight", + "issue_pattern": "Missing empty string validation for sfPermissionValue (within sfPermissions array)", + "why_false_positive": "!permissionSet.insert(permission[sfPermissionValue]).second validates sfPermissionValue (within sfPermissions array) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPermissionValue (within sfPermissions array)", + "empty", + "string", + "validation" + ], + "evidence": "!Permission::getInstance().isDelegable(permission[sfPermissionValue], ctx.rules) at DelegateSet::preflight", + "issue_pattern": "Missing empty string validation for sfPermissionValue (within sfPermissions array)", + "why_false_positive": "!Permission::getInstance().isDelegable(permission[sfPermissionValue], ctx.rules) validates sfPermissionValue (within sfPermissions array) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "!ctx.view.exists(keylet::account(ctx.tx[sfAccount])) at DelegateSet::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "!ctx.view.exists(keylet::account(ctx.tx[sfAccount])) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAuthorize", + "empty", + "string", + "validation" + ], + "evidence": "!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize])) at DelegateSet::preclaim", + "issue_pattern": "Missing empty string validation for sfAuthorize", + "why_false_positive": "!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize])) validates sfAuthorize for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPermissions (array) and delegate object existence", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.getFieldArray(sfPermissions).empty() && !ctx.view.exists(keylet::delegate(ctx.tx[sfAccount], ctx.tx[sfAuthorize])) at DelegateSet::preclaim", + "issue_pattern": "Missing empty string validation for sfPermissions (array) and delegate object existence", + "why_false_positive": "ctx.tx.getFieldArray(sfPermissions).empty() && !ctx.view.exists(keylet::delegate(ctx.tx[sfAccount], ctx.tx[sfAuthorize])) validates sfPermissions (array) and delegate object existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount (owner account)", + "empty", + "string", + "validation" + ], + "evidence": "!sleOwner at DelegateSet::doApply", + "issue_pattern": "Missing empty string validation for sfAccount (owner account)", + "why_false_positive": "!sleOwner validates sfAccount (owner account) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPermissions (array)", + "empty", + "string", + "validation" + ], + "evidence": "permissions.empty() at DelegateSet::doApply (when creating new delegate object)", + "issue_pattern": "Missing empty string validation for sfPermissions (array)", + "why_false_positive": "permissions.empty() validates sfPermissions (array) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account reserve (balance check)", + "empty", + "string", + "validation" + ], + "evidence": "preFeeBalance_ < reserve at DelegateSet::doApply", + "issue_pattern": "Missing empty string validation for account reserve (balance check)", + "why_false_positive": "preFeeBalance_ < reserve validates account reserve (balance check) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "owner directory capacity", + "empty", + "string", + "validation" + ], + "evidence": "!page at DelegateSet::doApply", + "issue_pattern": "Missing empty string validation for owner directory capacity", + "why_false_positive": "!page validates owner directory capacity for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 10, + "name": "DelegateSet::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 32, + "name": "DelegateSet::preclaim" + }, + { + "args": [], + "lineno": 51, + "name": "DelegateSet::doApply" + }, + { + "args": [ + "ApplyView& view", + "std::shared_ptr const& sle", + "AccountID const& account", + "beast::Journal j" + ], + "lineno": 91, + "name": "DelegateSet::deleteDelegate" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "DelegateSet is a new or advanced feature, so test coverage may be limited to integration and unit tests for transaction processing. Likely test files: 'test/tx/DelegateSet_test.cpp', 'test/tx/Transactor_test.cpp', or similar. Coverage should include: oversized permissions array, self-authorization, duplicate permissions, non-delegable permissions, missing accounts, deleting non-existent delegate, insufficient reserve, and successful create/update/delete flows. Gaps may exist for edge cases (e.g., directory full, internal errors, owner count mismatches). LCOV_EXCL_LINE and LCOV_EXCL_START/STOP indicate some error paths are not covered by tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in business logic (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temARRAY_TOO_LARGE", + "field": "sfPermissions (array)", + "location": "DelegateSet::preflight", + "validated_by": "permissions.size() > permissionMaxSize", + "validates": [ + "Ensures the permissions array does not exceed maximum allowed size" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAccount and sfAuthorize", + "location": "DelegateSet::preflight", + "validated_by": "ctx.tx[sfAccount] == ctx.tx[sfAuthorize]", + "validates": [ + "Prevents an account from authorizing itself as a delegate" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfPermissionValue (within sfPermissions array)", + "location": "DelegateSet::preflight", + "validated_by": "!permissionSet.insert(permission[sfPermissionValue]).second", + "validates": [ + "Ensures no duplicate permission values in the permissions array" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfPermissionValue (within sfPermissions array)", + "location": "DelegateSet::preflight", + "validated_by": "!Permission::getInstance().isDelegable(permission[sfPermissionValue], ctx.rules)", + "validates": [ + "Checks if each permission value is delegable under current rules" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_ACCOUNT", + "field": "sfAccount", + "location": "DelegateSet::preclaim", + "validated_by": "!ctx.view.exists(keylet::account(ctx.tx[sfAccount]))", + "validates": [ + "Ensures the source account exists in the ledger" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_TARGET", + "field": "sfAuthorize", + "location": "DelegateSet::preclaim", + "validated_by": "!ctx.view.exists(keylet::account(ctx.tx[sfAuthorize]))", + "validates": [ + "Ensures the target (authorized) account exists in the ledger" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfPermissions (array) and delegate object existence", + "location": "DelegateSet::preclaim", + "validated_by": "ctx.tx.getFieldArray(sfPermissions).empty() && !ctx.view.exists(keylet::delegate(ctx.tx[sfAccount], ctx.tx[sfAuthorize]))", + "validates": [ + "Prevents deleting a delegate object that does not exist" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sfAccount (owner account)", + "location": "DelegateSet::doApply", + "validated_by": "!sleOwner", + "validates": [ + "Ensures the owner account exists before applying changes" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "sfPermissions (array)", + "location": "DelegateSet::doApply (when creating new delegate object)", + "validated_by": "permissions.empty()", + "validates": [ + "Prevents creating a delegate object with empty permissions" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINSUFFICIENT_RESERVE", + "field": "account reserve (balance check)", + "location": "DelegateSet::doApply", + "validated_by": "preFeeBalance_ < reserve", + "validates": [ + "Ensures the account has enough reserve to create a new ledger object" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecDIR_FULL", + "field": "owner directory capacity", + "location": "DelegateSet::doApply", + "validated_by": "!page", + "validates": [ + "Ensures the owner's directory is not full before inserting delegate object" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp.ai.md b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp.ai.md new file mode 100644 index 0000000000..f2d9350fa0 --- /dev/null +++ b/src/libxrpl/tx/transactors/delegate/DelegateSet.cpp.ai.md @@ -0,0 +1,57 @@ +# `DelegateSet.cpp` — DelegateSet Transaction Transactor + +## Role in the System + +`DelegateSet.cpp` implements the `DelegateSet` transaction type, which allows an XRPL account to authorize a second account to submit certain transactions on its behalf. The resulting on-ledger object — a `Delegate` — records the granting account (`sfAccount`), the authorized account (`sfAuthorize`), and the set of `sfPermissions` that have been delegated. The transaction can create a new delegation, overwrite an existing one with a different permission set, or delete the delegation entirely by submitting an empty permissions array. + +This file is one file in a small cluster under `src/libxrpl/tx/transactors/delegate/`: `DelegateUtils.cpp` provides the runtime check helpers (`checkTxPermission`, `loadGranularPermission`) that use the stored `Delegate` SLE at execution time, while `DelegateSet.cpp` is solely concerned with lifecycle management of that SLE. + +## Validation Pipeline + +The transactor follows the standard three-phase XRPL pattern: stateless preflight, stateful preclaim, and ledger-mutating apply. + +### `preflight` — stateless checks + +Four invariants are enforced before the ledger state is consulted: + +1. **Array size bound**: the `sfPermissions` array must not exceed `permissionMaxSize` (10, defined in `Protocol.h`). Exceeding it returns `temARRAY_TOO_LARGE`. +2. **No self-delegation**: `sfAccount == sfAuthorize` is rejected with `temMALFORMED`. An account cannot grant itself a permission it already inherently has. +3. **No duplicate permissions**: an `std::unordered_set` accumulates each `sfPermissionValue` as the array is iterated; a failed `insert` (meaning the value was already present) returns `temMALFORMED`. +4. **Delegability check**: each permission value is validated against `Permission::getInstance().isDelegable(value, rules)`. The singleton `Permission` registry knows, per-value and per-enabled-amendments, whether a transaction type or granular permission is legally delegable. Non-delegable values (e.g., transactions that manipulate signing authority themselves) return `temMALFORMED`. + +The `Permission` singleton distinguishes two namespaces of permission values: standard transaction-type permissions (where `permissionValue == TxType + 1`, fitting in 16 bits) and granular permissions (values above `UINT16_MAX`, enumerated in `permissions.macro`). This encoding is why the uniqueness check works cleanly on raw `uint32_t` — both namespaces fit in the same integer space without collision. + +### `preclaim` — stateful checks + +Two existence checks run against the read-only ledger view: + +- The submitting account must exist (`terNO_ACCOUNT` if not — marked `LCOV_EXCL_LINE` because the transactor framework normally guarantees this). +- The target account (`sfAuthorize`) must exist (`tecNO_TARGET`). A delegation to a non-existent account is meaningless and could create dangling state. + +A third check handles the delete path: if the permissions array is empty (signaling delete intent) but no `Delegate` object for `(account, authorizeAccount)` exists, `tecNO_ENTRY` is returned. This prevents a no-op delete from succeeding silently and consuming fees unexpectedly. + +## `doApply` — Ledger Mutation + +The apply logic branches on whether the delegate SLE already exists: + +**Update path**: if the SLE exists, the permissions array in it is atomically replaced with the new one from the transaction. If the new array is empty, `deleteDelegate` is called instead. + +**Delete path** (via `deleteDelegate`): the SLE is removed from the owner's directory (`dirRemove`), the owner count is decremented by one via `adjustOwnerCount`, and the SLE itself is erased from the view. `deleteDelegate` is a `static` method exposed in the header specifically so `AccountDelete` can call it when cleaning up all objects owned by an account being deleted. + +**Create path**: a reserve check ensures the account holds enough XRP to absorb the new owner-count increment (`preFeeBalance_ < reserve` → `tecINSUFFICIENT_RESERVE`). The new SLE is keyed by `keylet::delegate(account, authorizedAccount)` — a composite key that makes lookups for a specific delegation O(1). The SLE is inserted into the owner directory (`dirInsert`), and `sfOwnerNode` records the directory page for efficient future removal. A `tecDIR_FULL` guard covers the case where the directory has no room (marked `LCOV_EXCL_LINE` as it is extremely rare in practice). + +A notable defensive check appears at the start of the create branch: if the SLE does not exist yet but `permissions` is empty, the code returns `tecINTERNAL`. In theory this state is unreachable because `preclaim` already rejects it with `tecNO_ENTRY`, but the guard is retained as a belt-and-suspenders invariant for the apply phase. + +## Resource and Ownership Model + +Each `Delegate` SLE is owned by the granting account: it appears in that account's owner directory and increments its `sfOwnerCount`. The owner-count-based reserve requirement (`accountReserve(ownerCount + 1)`) ensures the ledger cannot be cheaply filled with delegation objects. When the delegation is deleted — either explicitly via `DelegateSet` or implicitly via `AccountDelete` — the count is decremented and the reserve is released. + +The `deleteDelegate` static interface is the primary seam for `AccountDelete` integration. The `AccountDelete` transactor calls it passing the `ApplyView`, the SLE pointer, and the account ID, without needing to know anything about how the directory or count management works. + +## Error-Code Taxonomy + +The file is careful to use the right TER class for each failure: +- `tem*` codes from `preflight` indicate malformed transactions that are rejected before any ledger state is read. +- `ter*` codes from `preclaim` indicate transient or existence failures — the transaction might succeed if retried later. +- `tec*` codes from `doApply` are consensus-applied failures that consume the fee. +- `tef*`/`tefINTERNAL` marks logic faults that should never occur in a correctly operating node (all `LCOV_EXCL_LINE` annotated, confirming they are untested and treated as unreachable by design). \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp.ai.json b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp.ai.json new file mode 100644 index 0000000000..3a0d5dc744 --- /dev/null +++ b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp.ai.json @@ -0,0 +1,420 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "checkTxPermission" + ], + "entry_point": "checkTxPermission", + "purpose": "Checks if a delegate has permission for a given transaction type.", + "validation_points": [ + "Explicit null check on delegate at function start", + "Implicit validation of permissionArray by iterating over it", + "Implicit validation of permission[sfPermissionValue] by field access" + ] + }, + { + "call_chain": [ + "loadGranularPermission" + ], + "entry_point": "loadGranularPermission", + "purpose": "Loads granular permissions for a delegate and transaction type into a set.", + "validation_points": [ + "Explicit null check on delegate at function start", + "Implicit validation of permissionArray by iterating over it", + "Implicit validation of permission[sfPermissionValue] by field access" + ] + } + ], + "data_flows": [ + { + "field": "delegate (std::shared_ptr)", + "flow": [ + "Input argument", + "Explicit null check", + "Used to call getFieldArray(sfPermissions)" + ], + "origin": "Passed as argument to both functions (likely from ledger lookup elsewhere)", + "transformations": [ + "Checked for null", + "Used to extract permissionArray" + ], + "validated_at": "Function entry (explicit null check)" + }, + { + "field": "permissionArray (STArray)", + "flow": [ + "Extracted from delegate", + "Iterated over in for loop" + ], + "origin": "delegate->getFieldArray(sfPermissions)", + "transformations": [ + "None (assumed valid if present)" + ], + "validated_at": "Implicitly validated by iteration (no explicit check for array existence or type)" + }, + { + "field": "permission[sfPermissionValue]", + "flow": [ + "Accessed in for loop", + "Compared to txPermission (in checkTxPermission)", + "Cast to GranularPermissionType and compared to txType (in loadGranularPermission)" + ], + "origin": "Each element of permissionArray", + "transformations": [ + "Read as integer", + "Cast to enum (GranularPermissionType) in loadGranularPermission" + ], + "validated_at": "Implicitly validated by field access (no explicit check for field existence)" + }, + { + "field": "txPermission", + "flow": [ + "Computed from transaction", + "Compared to permissionValue" + ], + "origin": "tx.getTxnType() + 1", + "transformations": [ + "Incremented by 1" + ], + "validated_at": "Not explicitly validated (assumes tx.getTxnType() returns valid value)" + }, + { + "field": "granularPermissions (std::unordered_set&)", + "flow": [ + "Initially empty", + "granularValue inserted if type matches txType" + ], + "origin": "Output parameter to loadGranularPermission", + "transformations": [ + "granularValue cast and inserted" + ], + "validated_at": "No explicit validation (assumes set is valid)" + } + ], + "description": "This file provides helper functions for checking and loading delegate permissions related to transaction types in the XRPL ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "delegate (std::shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at checkTxPermission", + "issue_pattern": "Missing empty string validation for delegate (std::shared_ptr)", + "why_false_positive": "explicit null check validates delegate (std::shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "permissionArray (STArray from delegate->getFieldArray(sfPermissions))", + "empty", + "string", + "validation" + ], + "evidence": "implicit: iterated over, assumed valid if present at checkTxPermission", + "issue_pattern": "Missing empty string validation for permissionArray (STArray from delegate->getFieldArray(sfPermissions))", + "why_false_positive": "implicit: iterated over, assumed valid if present validates permissionArray (STArray from delegate->getFieldArray(sfPermissions)) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "permission[sfPermissionValue]", + "empty", + "string", + "validation" + ], + "evidence": "implicit: field access, assumes field exists in each permission at checkTxPermission", + "issue_pattern": "Missing empty string validation for permission[sfPermissionValue]", + "why_false_positive": "implicit: field access, assumes field exists in each permission validates permission[sfPermissionValue] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "delegate (std::shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at loadGranularPermission", + "issue_pattern": "Missing empty string validation for delegate (std::shared_ptr)", + "why_false_positive": "explicit null check validates delegate (std::shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "permissionArray (STArray from delegate->getFieldArray(sfPermissions))", + "empty", + "string", + "validation" + ], + "evidence": "implicit: iterated over, assumed valid if present at loadGranularPermission", + "issue_pattern": "Missing empty string validation for permissionArray (STArray from delegate->getFieldArray(sfPermissions))", + "why_false_positive": "implicit: iterated over, assumed valid if present validates permissionArray (STArray from delegate->getFieldArray(sfPermissions)) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "permission[sfPermissionValue]", + "empty", + "string", + "validation" + ], + "evidence": "implicit: field access, assumes field exists in each permission at loadGranularPermission", + "issue_pattern": "Missing empty string validation for permission[sfPermissionValue]", + "why_false_positive": "implicit: field access, assumes field exists in each permission validates permission[sfPermissionValue] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.6, + "detection_keywords": [ + "granularValue (cast from permissionValue)", + "empty", + "string", + "validation" + ], + "evidence": "static_cast at loadGranularPermission", + "issue_pattern": "Missing empty string validation for granularValue (cast from permissionValue)", + "why_false_positive": "static_cast validates granularValue (cast from permissionValue) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "granularValue (cast from permissionValue)", + "type", + "validation", + "check" + ], + "evidence": "static_cast at loadGranularPermission", + "issue_pattern": "Missing type validation for granularValue (cast from permissionValue)", + "why_false_positive": "static_cast validates granularValue (cast from permissionValue) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "type (Permission::getInstance().getGranularTxType(granularValue))", + "empty", + "string", + "validation" + ], + "evidence": "pointer check (if type && *type == txType) at loadGranularPermission", + "issue_pattern": "Missing empty string validation for type (Permission::getInstance().getGranularTxType(granularValue))", + "why_false_positive": "pointer check (if type && *type == txType) validates type (Permission::getInstance().getGranularTxType(granularValue)) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp", + "functions": [ + { + "args": [ + "delegate", + "tx" + ], + "lineno": 5, + "name": "checkTxPermission" + }, + { + "args": [ + "delegate", + "txType", + "granularPermissions" + ], + "lineno": 20, + "name": "loadGranularPermission" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "These utility functions are likely tested indirectly via higher-level transaction processing or permission-checking tests. Direct unit tests for checkTxPermission and loadGranularPermission may exist in files such as 'test/tx/Delegate_test.cpp', 'test/ledger/DelegateHelpers_test.cpp', or similar. However, the code relies on implicit validation for permissionArray and permission fields, which may not be robustly tested for malformed or missing data. There is no explicit test coverage for cases where permissionArray is missing or contains malformed entries. Null delegate handling is explicitly tested by the code, but test coverage for edge cases (e.g., empty permissionArray, missing sfPermissionValue) should be verified.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, some use of xrpl/protocol types)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "terNO_DELEGATE_PERMISSION (error code, not exception)", + "field": "delegate (std::shared_ptr)", + "location": "checkTxPermission", + "validated_by": "explicit null check", + "validates": [ + "delegate pointer is not null" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (empty array just results in error code)", + "field": "permissionArray (STArray from delegate->getFieldArray(sfPermissions))", + "location": "checkTxPermission", + "validated_by": "implicit: iterated over, assumed valid if present", + "validates": [ + "permission array exists and is iterable" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (if field missing, likely throws or returns default)", + "field": "permission[sfPermissionValue]", + "location": "checkTxPermission", + "validated_by": "implicit: field access, assumes field exists in each permission", + "validates": [ + "permission entry contains sfPermissionValue" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (function returns early)", + "field": "delegate (std::shared_ptr)", + "location": "loadGranularPermission", + "validated_by": "explicit null check", + "validates": [ + "delegate pointer is not null" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (empty array just results in no permissions loaded)", + "field": "permissionArray (STArray from delegate->getFieldArray(sfPermissions))", + "location": "loadGranularPermission", + "validated_by": "implicit: iterated over, assumed valid if present", + "validates": [ + "permission array exists and is iterable" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (if field missing, likely throws or returns default)", + "field": "permission[sfPermissionValue]", + "location": "loadGranularPermission", + "validated_by": "implicit: field access, assumes field exists in each permission", + "validates": [ + "permission entry contains sfPermissionValue" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.6, + "error_thrown": "none (C++ static_cast, may cause UB if out of range)", + "field": "granularValue (cast from permissionValue)", + "location": "loadGranularPermission", + "validated_by": "static_cast", + "validates": [ + "permissionValue is convertible to GranularPermissionType" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "none (skips if not matching)", + "field": "type (Permission::getInstance().getGranularTxType(granularValue))", + "location": "loadGranularPermission", + "validated_by": "pointer check (if type && *type == txType)", + "validates": [ + "granular permission type matches txType" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp.ai.md b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp.ai.md new file mode 100644 index 0000000000..9271ce7944 --- /dev/null +++ b/src/libxrpl/tx/transactors/delegate/DelegateUtils.cpp.ai.md @@ -0,0 +1,29 @@ +# `DelegateUtils.cpp` — Delegate Permission Enforcement Utilities + +This file is the execution-time core of XRPL's account delegation permission system. It implements two free functions that transactors call when processing a transaction submitted by a delegate (an account acting on behalf of another account). The broader delegation system allows account owners to grant either broad (per-transaction-type) or narrow (operation-specific) authority; these two functions are responsible for querying the on-ledger permission grant and determining whether a specific action is authorized. + +## Encoding Convention: Why `TxType + 1` + +The `sfPermissions` array stored on a delegate's `SLE` (Serialized Ledger Entry) uses a single `sfPermissionValue` integer field to encode both coarse-grained and granular permissions in the same space. Coarse permissions are encoded as `TxType + 1` — the `+1` shift is deliberate: `TxType` values start at zero (e.g., `ttPAYMENT == 0`), but a stored value of `0` would be ambiguous or falsy in some contexts. Adding one maps every valid transaction type to a positive non-zero integer occupying the range `[1, 65535]`. Granular permissions, by design, are assigned values above `UINT16_MAX` (≥ 65536), so the two namespaces never overlap. The `Permission` singleton asserts this separation at construction time. + +## `checkTxPermission()` + +This function answers the binary question: *does this delegate have authorization to submit this transaction type at all?* It takes the delegate's `SLE` (already fetched by the caller) and the pending `STTx`. It guards immediately against a null delegate, returning `terNO_DELEGATE_PERMISSION` — a `TER` (Transaction Error Result) code that signals a retriable protocol-level failure, distinct from a hard `TEC` error. + +The check is a linear scan of the `sfPermissions` array. For each entry it reads `sfPermissionValue` and compares against `tx.getTxnType() + 1`. An empty array, a revoked delegate, or a permission list that simply does not include the required transaction type all produce the same outcome: `terNO_DELEGATE_PERMISSION`. The absence of short-circuit logic beyond the first match is intentional given that permission lists are expected to be small (bounded by ledger object size constraints). + +## `loadGranularPermission()` + +Where `checkTxPermission` handles the coarse gate, `loadGranularPermission` populates the set of *fine-grained* capabilities the delegate holds for a specific transaction type. Granular permissions represent sub-operations within a transaction — for example, a delegate might be granted `PaymentMint` (allowing only IOU issuance payments) without receiving authority over the full `Payment` transaction type. + +The function iterates the same `sfPermissions` array but applies a different test. Each stored `permissionValue` is cast via `static_cast` and then passed to `Permission::getInstance().getGranularTxType()`, which consults the singleton's internal reverse map (`granularTxTypeMap_`) to find which `TxType` a given granular permission belongs to. Only entries whose parent `TxType` matches the requested `txType` are inserted into the output `granularPermissions` set. This design means individual transactors (e.g., `Payment`, `TrustSet`, `AccountSet`) can call `loadGranularPermission` with their own type and receive only the relevant sub-permissions, without needing to know the encoding details of unrelated permission namespaces. + +The `static_cast` without a range check is a known tradeoff: the ledger's own validator rejects `sfPermissionValue` integers that do not correspond to registered entries at submission time, so by the time these functions execute the values are considered trusted. + +## Null-Safety Pattern + +Both functions follow the same null-guard idiom at their respective entry points. A null `delegate` pointer indicates the ledger lookup failed to find a delegate object (the account has not been granted delegation), which is semantically identical to having no permissions. `checkTxPermission` returns `terNO_DELEGATE_PERMISSION` while `loadGranularPermission` returns silently, leaving the output set empty. The callers can then handle both cases uniformly without needing to differentiate between "no delegate exists" and "delegate exists but has no relevant permission." + +## Relationship to `DelegateHelpers.h` and the `Permission` Singleton + +The file includes only `DelegateHelpers.h` and `STArray.h`. `DelegateHelpers.h` provides the function declarations and brings in `GranularPermissionType`, `Permission`, `TxType`, and the relevant `sfField` descriptors. The `Permission` singleton, initialized lazily on first access, owns the authoritative maps between granular permission codes and their parent transaction types — `loadGranularPermission` delegates all knowledge of that mapping back to the singleton rather than encoding it locally. This separation keeps the utility functions stateless and the permission registry centralized. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMBid.cpp.ai.json b/src/libxrpl/tx/transactors/dex/AMMBid.cpp.ai.json new file mode 100644 index 0000000000..70a4aae50f --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMBid.cpp.ai.json @@ -0,0 +1,571 @@ +{ + "args": [ + { + "lineno": 9, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 54, + "name": "ctx" + }, + { + "lineno": 97, + "name": "ctx_" + }, + { + "lineno": 97, + "name": "sb" + }, + { + "lineno": 97, + "name": "account_" + }, + { + "lineno": 97, + "name": "j_" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AMMBid::doApply", + "AMMBid::preflight", + "AMMBid::preclaim", + "applyBid" + ], + "entry_point": "AMMBid::doApply", + "purpose": "Processes an AMM Bid transaction, validating and applying it to the ledger.", + "validation_points": [ + "AMMBid::preflight", + "AMMBid::preclaim" + ] + }, + { + "call_chain": [ + "AMMBid::preflight" + ], + "entry_point": "AMMBid::preflight", + "purpose": "Performs stateless validation of the transaction fields before any ledger access.", + "validation_points": [ + "AMMBid::preflight" + ] + }, + { + "call_chain": [ + "AMMBid::preclaim" + ], + "entry_point": "AMMBid::preclaim", + "purpose": "Performs contextual validation using the current ledger state.", + "validation_points": [ + "AMMBid::preclaim" + ] + }, + { + "call_chain": [ + "AMMBid::checkExtraFeatures" + ], + "entry_point": "AMMBid::checkExtraFeatures", + "purpose": "Checks feature flags and asset type compatibility before further processing.", + "validation_points": [ + "AMMBid::checkExtraFeatures" + ] + } + ], + "data_flows": [ + { + "field": "sfAsset / sfAsset2", + "flow": [ + "ctx.tx[sfAsset], ctx.tx[sfAsset2]", + "invalidAMMAssetPair(ctx.tx[sfAsset], ctx.tx[sfAsset2])", + "keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2])", + "ctx.view.read(keylet::amm(...))" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for validity as an asset pair", + "Used to look up AMM ledger entry" + ], + "validated_at": "AMMBid::preflight (invalidAMMAssetPair), AMMBid::preclaim (AMM existence)" + }, + { + "field": "sfBidMin / sfBidMax", + "flow": [ + "ctx.tx[~sfBidMin], ctx.tx[~sfBidMax]", + "invalidAMMAmount(*bidMin), invalidAMMAmount(*bidMax)", + "Compared to lpTokens and lpTokensBalance" + ], + "origin": "ctx.tx (transaction input, optional fields)", + "transformations": [ + "Type and value checked for AMM compatibility", + "Compared against account's LP token balance" + ], + "validated_at": "AMMBid::preflight (invalidAMMAmount), AMMBid::preclaim (asset match, balance check)" + }, + { + "field": "sfAuthAccounts", + "flow": [ + "ctx.tx.isFieldPresent(sfAuthAccounts)", + "ctx.tx.getFieldArray(sfAuthAccounts)", + "Loop over accounts for uniqueness and existence" + ], + "origin": "ctx.tx (transaction input, optional)", + "transformations": [ + "Checked for max size", + "Checked for uniqueness (if fixAMMv1_3 enabled)", + "Checked for existence in ledger" + ], + "validated_at": "AMMBid::preflight (size, uniqueness), AMMBid::preclaim (existence)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "Used for uniqueness check in sfAuthAccounts", + "Used to check LP token holdings" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Compared to auth accounts for uniqueness", + "Used to look up LP token balance" + ], + "validated_at": "AMMBid::preflight (uniqueness), AMMBid::preclaim (LP token check)" + }, + { + "field": "feature flags (ammEnabled, featureMPTokensV2, fixAMMv1_3)", + "flow": [ + "ctx.rules.enabled(feature)", + "Used to gate logic and validation" + ], + "origin": "ctx.rules", + "transformations": [ + "Enable/disable code paths and validation logic" + ], + "validated_at": "AMMBid::checkExtraFeatures, AMMBid::preflight" + } + ], + "description": "Implements the logic for handling AMM (Automated Market Maker) Bid transactions in the XRPL ledger, including preflight checks, preclaim validation, and applying bids to the ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAsset", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAsset validated by Custom validation via helper functions and protocol rules", + "issue_pattern": "Missing validation for sfAsset", + "why_false_positive": "Custom validation via helper functions and protocol rules validates sfAsset automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAsset2", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAsset2 validated by Custom validation via helper functions and protocol rules", + "issue_pattern": "Missing validation for sfAsset2", + "why_false_positive": "Custom validation via helper functions and protocol rules validates sfAsset2 automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfBidMin", + "validation", + "missing", + "check" + ], + "evidence": "Field sfBidMin validated by Custom validation via helper functions and protocol rules", + "issue_pattern": "Missing validation for sfBidMin", + "why_false_positive": "Custom validation via helper functions and protocol rules validates sfBidMin automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfBidMax", + "validation", + "missing", + "check" + ], + "evidence": "Field sfBidMax validated by Custom validation via helper functions and protocol rules", + "issue_pattern": "Missing validation for sfBidMax", + "why_false_positive": "Custom validation via helper functions and protocol rules validates sfBidMax automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAuthAccounts", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAuthAccounts validated by Custom validation via helper functions and protocol rules", + "issue_pattern": "Missing validation for sfAuthAccounts", + "why_false_positive": "Custom validation via helper functions and protocol rules validates sfAuthAccounts automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM feature enabled", + "empty", + "string", + "validation" + ], + "evidence": "ammEnabled(ctx.rules) at AMMBid::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for AMM feature enabled", + "why_false_positive": "ammEnabled(ctx.rules) validates AMM feature enabled for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "MPTokensV2 feature and asset types", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled(featureMPTokensV2) && holds() at AMMBid::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for MPTokensV2 feature and asset types", + "why_false_positive": "ctx.rules.enabled(featureMPTokensV2) && holds() validates MPTokensV2 feature and asset types for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAsset and sfAsset2 (asset pair)", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAssetPair(ctx.tx[sfAsset], ctx.tx[sfAsset2]) at AMMBid::preflight", + "issue_pattern": "Missing empty string validation for sfAsset and sfAsset2 (asset pair)", + "why_false_positive": "invalidAMMAssetPair(ctx.tx[sfAsset], ctx.tx[sfAsset2]) validates sfAsset and sfAsset2 (asset pair) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBidMin", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAmount(*bidMin) at AMMBid::preflight", + "issue_pattern": "Missing empty string validation for sfBidMin", + "why_false_positive": "invalidAMMAmount(*bidMin) validates sfBidMin for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBidMax", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAmount(*bidMax) at AMMBid::preflight", + "issue_pattern": "Missing empty string validation for sfBidMax", + "why_false_positive": "invalidAMMAmount(*bidMax) validates sfBidMax for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAuthAccounts (array size)", + "empty", + "string", + "validation" + ], + "evidence": "authAccounts.size() > AUCTION_SLOT_MAX_AUTH_ACCOUNTS at AMMBid::preflight", + "issue_pattern": "Missing empty string validation for sfAuthAccounts (array size)", + "why_false_positive": "authAccounts.size() > AUCTION_SLOT_MAX_AUTH_ACCOUNTS validates sfAuthAccounts (array size) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAuthAccounts (uniqueness and not self)", + "empty", + "string", + "validation" + ], + "evidence": "unique.contains(authAccount) || authAccount == account at AMMBid::preflight (if fixAMMv1_3 enabled)", + "issue_pattern": "Missing empty string validation for sfAuthAccounts (uniqueness and not self)", + "why_false_positive": "unique.contains(authAccount) || authAccount == account validates sfAuthAccounts (uniqueness and not self) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM existence (asset pair)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2])) at AMMBid::preclaim", + "issue_pattern": "Missing empty string validation for AMM existence (asset pair)", + "why_false_positive": "ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2])) validates AMM existence (asset pair) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM LP token balance", + "empty", + "string", + "validation" + ], + "evidence": "(*ammSle)[sfLPTokenBalance] == beast::zero at AMMBid::preclaim", + "issue_pattern": "Missing empty string validation for AMM LP token balance", + "why_false_positive": "(*ammSle)[sfLPTokenBalance] == beast::zero validates AMM LP token balance for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAuthAccounts (account existence)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::account(account[sfAccount])) at AMMBid::preclaim", + "issue_pattern": "Missing empty string validation for sfAuthAccounts (account existence)", + "why_false_positive": "ctx.view.read(keylet::account(account[sfAccount])) validates sfAuthAccounts (account existence) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/dex/AMMBid.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 9, + "name": "AMMBid::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "AMMBid::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 54, + "name": "AMMBid::preclaim" + }, + { + "args": [ + "ApplyContext& ctx_", + "Sandbox& sb", + "AccountID const& account_", + "beast::Journal j_" + ], + "lineno": 97, + "name": "applyBid" + }, + { + "args": [], + "lineno": 246, + "name": "AMMBid::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "AMMBid validation is likely covered by integration and unit tests in files such as 'test/tx/AMMBid_test.cpp', 'test/AMM_test.cpp', or similar. These tests should cover valid/invalid asset pairs, bid min/max logic, auth accounts, and feature flag gating. However, edge cases such as malformed auth accounts, feature flag transitions, and rare asset types (e.g., MPTIssue) may not be exhaustively tested. There may be gaps in negative testing for all combinations of optional fields and feature flags.", + "validation_architecture": { + "auto_validated_fields": [ + "sfAsset", + "sfAsset2", + "sfBidMin", + "sfBidMax", + "sfAuthAccounts" + ], + "framework": "Custom validation via helper functions and protocol rules", + "validation_layer": "business_logic (preflight/preclaim transaction validation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": "AMM feature enabled", + "location": "AMMBid::checkExtraFeatures", + "validated_by": "ammEnabled(ctx.rules)", + "validates": [ + "Checks if AMM feature is enabled in protocol rules" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": "MPTokensV2 feature and asset types", + "location": "AMMBid::checkExtraFeatures", + "validated_by": "ctx.rules.enabled(featureMPTokensV2) && holds()", + "validates": [ + "If MPTokensV2 is not enabled, ensures sfAsset and sfAsset2 do not hold MPTIssue" + ], + "validation_type": "business_logic|type" + }, + { + "confidence": 1.0, + "error_thrown": "returns error code from invalidAMMAssetPair", + "field": "sfAsset and sfAsset2 (asset pair)", + "location": "AMMBid::preflight", + "validated_by": "invalidAMMAssetPair(ctx.tx[sfAsset], ctx.tx[sfAsset2])", + "validates": [ + "Checks that the asset pair is valid for AMM" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 1.0, + "error_thrown": "returns error code from invalidAMMAmount", + "field": "sfBidMin", + "location": "AMMBid::preflight", + "validated_by": "invalidAMMAmount(*bidMin)", + "validates": [ + "If present, checks that sfBidMin is a valid AMM amount" + ], + "validation_type": "format|range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns error code from invalidAMMAmount", + "field": "sfBidMax", + "location": "AMMBid::preflight", + "validated_by": "invalidAMMAmount(*bidMax)", + "validates": [ + "If present, checks that sfBidMax is a valid AMM amount" + ], + "validation_type": "format|range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAuthAccounts (array size)", + "location": "AMMBid::preflight", + "validated_by": "authAccounts.size() > AUCTION_SLOT_MAX_AUTH_ACCOUNTS", + "validates": [ + "Checks that the number of AuthAccounts does not exceed maximum allowed" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAuthAccounts (uniqueness and not self)", + "location": "AMMBid::preflight (if fixAMMv1_3 enabled)", + "validated_by": "unique.contains(authAccount) || authAccount == account", + "validates": [ + "Checks that no AuthAccount is duplicated and none is the submitting account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_AMM", + "field": "AMM existence (asset pair)", + "location": "AMMBid::preclaim", + "validated_by": "ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]))", + "validates": [ + "Checks that an AMM exists for the given asset pair" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecAMM_EMPTY", + "field": "AMM LP token balance", + "location": "AMMBid::preclaim", + "validated_by": "(*ammSle)[sfLPTokenBalance] == beast::zero", + "validates": [ + "Checks that the AMM has a non-zero LP token balance" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_ACCOUNT", + "field": "sfAuthAccounts (account existence)", + "location": "AMMBid::preclaim", + "validated_by": "ctx.view.read(keylet::account(account[sfAccount]))", + "validates": [ + "Checks that each AuthAccount exists in the ledger" + ], + "validation_type": "business_logic|existence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMBid.cpp.ai.md b/src/libxrpl/tx/transactors/dex/AMMBid.cpp.ai.md new file mode 100644 index 0000000000..8969d336cc --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMBid.cpp.ai.md @@ -0,0 +1,55 @@ +# `AMMBid.cpp` — AMM Auction Slot Bidding Transactor + +## Role in the System + +This file implements the `AMMBid` transaction type, one of several AMM-specific transactors living under `src/libxrpl/tx/transactors/dex/`. It allows liquidity providers (LPs) to compete for the AMM's single **auction slot** — a 24-hour exclusive grant that entitles the holder and up to four authorized accounts to trade at a deeply discounted fee. Every AMM instance has exactly one auction slot, and the bidding mechanism is designed so that a competitive market in slot ownership benefits all LPs via LP token burns. + +The sibling transactors (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMVote`, `AMMDelete`) handle other AMM lifecycle operations; `AMMBid` is specific to the fee-discount auction mechanic. + +## Auction Slot Economics + +The slot's pricing is governed by constants from `AMMCore.h`: + +- **Slot duration**: 24 hours, divided into 20 equal intervals of 72 minutes each (`AUCTION_SLOT_TIME_INTERVALS = 20`) +- **Discounted fee**: the AMM's trading fee divided by 10 (`AUCTION_SLOT_DISCOUNTED_FEE_FRACTION = 10`) +- **Minimum slot price**: `lptAMMBalance × tradingFee / 25`, computed freshly at apply time against the current total LP token supply + +Bid amounts are always denominated in LP tokens, not XRP or the AMM's underlying assets. This design ensures that the cost of holding a discount slot scales with the AMM's size. + +When an active holder is outbid, the new bidder pays a computed price that includes a 5% premium (`p1_05 = 1.05`) over the original purchase price, adjusted downward as the slot ages. The formula applies `1 - fractionUsed^60` as a decay multiplier (where `fractionUsed = (timeSlot + 1) / 20`), making it progressively cheaper to outbid a slot holder the further into the 24-hour window they are. In the first interval (slot 0) the decay term is omitted and the full 5% premium applies. The previous holder is refunded `(1 - fractionUsed) × pricePurchased` in LP tokens, representing the unused fraction of their slot. The bid amount minus this refund is **permanently burned** by calling `redeemIOU` to destroy the tokens and decrementing `sfLPTokenBalance` on the AMM ledger entry. This deflationary burn benefits all remaining LPs. + +The **tailing slot** (slot 19) is treated specially: `validOwner` uses `< tailingSlot` rather than `<=`. At the last interval the holder gets no refund, pays minimum price, and the cheapest rational action is to simply let the slot expire. + +## Transaction Processing Pipeline + +### `checkExtraFeatures` + +Called before `preflight` to test feature flag compatibility. It rejects the transaction if the `ammEnabled` guard fails (the core AMM amendment), or if the transaction references MPT-typed assets while `featureMPTokensV2` is not yet active. This keeps the AMM from processing asset types that the ledger rules don't yet support. + +### `preflight` (stateless validation) + +Validates the transaction fields without touching ledger state. It rejects malformed asset pairs via `invalidAMMAssetPair()`, validates the optional `sfBidMin`/`sfBidMax` amounts via `invalidAMMAmount()`, and caps the `sfAuthAccounts` array at `AUCTION_SLOT_MAX_AUTH_ACCOUNTS = 4`. Under the `fixAMMv1_3` amendment, it additionally enforces that no auth account appears more than once and that the submitter's own account is not in the list — a deduplication check that was absent in the initial deployment. + +### `preclaim` (stateful validation) + +Reads the AMM ledger entry and performs live checks. It rejects if the AMM doesn't exist (`terNO_AMM`), if the pool is empty (`tecAMM_EMPTY`), or if any listed auth account lacks a ledger entry (`terNO_ACCOUNT`). Critically it verifies that the submitter is actually an LP (`ammLPHolds()` returning non-zero) and that any `sfBidMin`/`sfBidMax` values do not exceed the submitter's own holdings and are consistent with the correct LP token asset type. A `bidMin > bidMax` cross-check catches inverted ranges. + +### `doApply` and `applyBid` + +`doApply` wraps execution in a `Sandbox` — all mutations are staged against a copy of the view and committed to `ctx_.rawView()` only if `applyBid` succeeds. This is the standard XRPL transactor pattern for atomic application. + +`applyBid` is a file-scope free function (not a class method), which is architecturally deliberate: it takes explicit parameters and has no hidden access to `AMMBid` member state, making its preconditions clear and testable. It contains two nested lambdas: + +- **`getPayPrice`**: Given a computed market price, constrains it to the `[sfBidMin, sfBidMax]` range. If `sfBidMax` is present and the market price exceeds it, the bid fails with `tecAMM_FAILED`. If only `sfBidMin` is present, the bidder pays the maximum of their stated minimum and the market price. This lets callers set a price floor (ensuring they get the slot if they want it) or a ceiling (capping their exposure). + +- **`updateSlot`**: Atomically rewrites all `sfAuctionSlot` fields — owner account, expiration, discounted fee, slot price, and auth accounts — then burns the token amount by calling `redeemIOU` on the sandbox and updating `sfLPTokenBalance`. Using `adjustLPTokens(..., IsDeposit::No)` corrects for the 16-digit precision loss inherent in IOU arithmetic when subtracting from the running LP balance. + +### Feature Flag Handling in `applyBid` + +The `fixInnerObjTemplate` amendment changes how the `sfAuctionSlot` inner object is managed. Before the fix, the code would lazily add the field if absent via `makeFieldPresent`. After the fix, the slot must already be present (initialized during AMM creation), and the code asserts this with `XRPL_ASSERT`, returning `tecINTERNAL` if violated. This shift from lazy initialization to eager initialization prevents a class of inconsistency bugs where the object might be partially constructed. + +## Error Handling and Invariants + +Two error paths are annotated `// LCOV_EXCL_START` — the case where the LP token burn amount would equal or exceed the total AMM balance, and the case where the computed refund exceeds the pay price. Both are mathematically impossible given valid inputs, but the code still guards them to catch any future numerical regression. They return `tecINTERNAL` rather than silently producing corrupted state. + +The entire apply path is guarded by the `Sandbox` pattern: no ledger change is visible outside the transaction unless `applyBid` returns a success code. Any intermediate failure (failed `accountSend` for the refund, failed `redeemIOU` for the burn) rolls back cleanly without partial writes. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp.ai.json b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp.ai.json new file mode 100644 index 0000000000..dcd852c1c9 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp.ai.json @@ -0,0 +1,673 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AMMClawback::preflight" + ], + "entry_point": "AMMClawback::preflight", + "purpose": "Performs initial stateless validation of the AMMClawback transaction. Checks for malformed fields, flag correctness, and asset/account relationships.", + "validation_points": [ + "issuer == holder check (malformed)", + "isXRP(asset) check (malformed)", + "tfClawTwoAssets flag and issuer match (invalid flag)", + "asset.getIssuer() == issuer (malformed)", + "clawAmount.asset() == asset (bad amount)", + "clawAmount > 0 (bad amount)" + ] + }, + { + "call_chain": [ + "AMMClawback::preclaim" + ], + "entry_point": "AMMClawback::preclaim", + "purpose": "Performs contextual validation using ledger state. Checks for existence of accounts, AMM pool, and issuer permissions.", + "validation_points": [ + "issuer account exists", + "holder account exists", + "AMM pool exists", + "issuer flags (lsfAllowTrustLineClawback, lsfNoFreeze)", + "MPTIssue: sleIssuance flags and issuer match" + ] + }, + { + "call_chain": [ + "AMMClawback::checkExtraFeatures" + ], + "entry_point": "AMMClawback::checkExtraFeatures", + "purpose": "Checks if the required protocol features are enabled and validates asset types for MPTokens.", + "validation_points": [ + "featureAMMClawback enabled", + "featureMPTokensV2 enabled or asset types not MPTIssue" + ] + } + ], + "data_flows": [ + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "issuer variable in preflight/preclaim", + "used for issuer/holder comparison, asset issuer match, and permission checks" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared to sfHolder", + "Compared to asset.getIssuer()", + "Used to fetch account SLE from ledger" + ], + "validated_at": "preflight (issuer == holder, asset.getIssuer() == issuer), preclaim (account exists, permission checks)" + }, + { + "field": "sfHolder", + "flow": [ + "ctx.tx[sfHolder]", + "holder variable in preflight/preclaim", + "used for issuer/holder comparison and account existence check" + ], + "origin": "ctx.tx[sfHolder] (transaction input)", + "transformations": [ + "Compared to sfAccount" + ], + "validated_at": "preflight (issuer == holder), preclaim (account exists)" + }, + { + "field": "sfAmount", + "flow": [ + "ctx.tx[~sfAmount]", + "clawAmount variable in preflight/checkExtraFeatures", + "used for asset match and value checks" + ], + "origin": "ctx.tx[~sfAmount] (optional transaction input)", + "transformations": [ + "Checked for presence", + "Compared to asset", + "Checked for > 0" + ], + "validated_at": "preflight (asset match, > 0), checkExtraFeatures (MPTIssue check)" + }, + { + "field": "sfAsset", + "flow": [ + "ctx.tx[sfAsset]", + "asset variable in preflight/preclaim/checkExtraFeatures", + "used for isXRP check, issuer match, AMM pool lookup, and MPTIssue checks" + ], + "origin": "ctx.tx[sfAsset] (transaction input)", + "transformations": [ + "Checked for isXRP", + "Compared to asset2.getIssuer()", + "Compared to asset.getIssuer()", + "Used in keylet::amm lookup" + ], + "validated_at": "preflight (isXRP, issuer match), preclaim (AMM pool exists, permission checks), checkExtraFeatures (MPTIssue check)" + }, + { + "field": "sfAsset2", + "flow": [ + "ctx.tx[sfAsset2]", + "asset2 variable in preflight/preclaim/checkExtraFeatures", + "used for issuer match, AMM pool lookup, and MPTIssue checks" + ], + "origin": "ctx.tx[sfAsset2] (transaction input)", + "transformations": [ + "Compared to asset.getIssuer()", + "Used in keylet::amm lookup" + ], + "validated_at": "preflight (issuer match for tfClawTwoAssets), preclaim (AMM pool exists), checkExtraFeatures (MPTIssue check)" + }, + { + "field": "flags", + "flow": [ + "ctx.tx.getFlags()", + "flags variable in preflight", + "used for tfClawTwoAssets logic" + ], + "origin": "ctx.tx.getFlags() (transaction input)", + "transformations": [ + "Bitmask checked for tfClawTwoAssets" + ], + "validated_at": "preflight (tfClawTwoAssets logic)" + }, + { + "field": "issuerFlagsIn", + "flow": [ + "sleIssuer->getFieldU32(sfFlags)", + "issuerFlagsIn variable in preclaim", + "used for permission checks" + ], + "origin": "sleIssuer->getFieldU32(sfFlags) (ledger state)", + "transformations": [ + "Bitmask checked for lsfAllowTrustLineClawback, lsfNoFreeze" + ], + "validated_at": "preclaim (permission checks)" + } + ], + "description": "Implements the AMMClawback transaction logic for the XRPL decentralized exchange, handling preflight checks, permissions, and the application of clawback operations on AMM pools.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccount validated by xrpl transaction preflight/preclaim validation", + "issue_pattern": "Missing validation for sfAccount", + "why_false_positive": "xrpl transaction preflight/preclaim validation validates sfAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfHolder", + "validation", + "missing", + "check" + ], + "evidence": "Field sfHolder validated by xrpl transaction preflight/preclaim validation", + "issue_pattern": "Missing validation for sfHolder", + "why_false_positive": "xrpl transaction preflight/preclaim validation validates sfHolder automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAsset", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAsset validated by xrpl transaction preflight/preclaim validation", + "issue_pattern": "Missing validation for sfAsset", + "why_false_positive": "xrpl transaction preflight/preclaim validation validates sfAsset automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAsset2", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAsset2 validated by xrpl transaction preflight/preclaim validation", + "issue_pattern": "Missing validation for sfAsset2", + "why_false_positive": "xrpl transaction preflight/preclaim validation validates sfAsset2 automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAmount validated by xrpl transaction preflight/preclaim validation", + "issue_pattern": "Missing validation for sfAmount", + "why_false_positive": "xrpl transaction preflight/preclaim validation validates sfAmount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "flags", + "validation", + "missing", + "check" + ], + "evidence": "Field flags validated by xrpl transaction preflight/preclaim validation", + "issue_pattern": "Missing validation for flags", + "why_false_positive": "xrpl transaction preflight/preclaim validation validates flags automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "featureAMMClawback", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled at checkExtraFeatures", + "issue_pattern": "Missing empty string validation for featureAMMClawback", + "why_false_positive": "ctx.rules.enabled validates featureAMMClawback for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "featureMPTokensV2", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled at checkExtraFeatures", + "issue_pattern": "Missing empty string validation for featureMPTokensV2", + "why_false_positive": "ctx.rules.enabled validates featureMPTokensV2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount (optional)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx[~sfAmount] at checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfAmount (optional)", + "why_false_positive": "ctx.tx[~sfAmount] validates sfAmount (optional) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount vs sfHolder", + "empty", + "string", + "validation" + ], + "evidence": "issuer == holder at preflight", + "issue_pattern": "Missing empty string validation for sfAccount vs sfHolder", + "why_false_positive": "issuer == holder validates sfAccount vs sfHolder for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAsset", + "empty", + "string", + "validation" + ], + "evidence": "isXRP(asset) at preflight", + "issue_pattern": "Missing empty string validation for sfAsset", + "why_false_positive": "isXRP(asset) validates sfAsset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfClawTwoAssets flag and asset issuers", + "empty", + "string", + "validation" + ], + "evidence": "flags & tfClawTwoAssets, asset.getIssuer() != asset2.getIssuer() at preflight", + "issue_pattern": "Missing empty string validation for tfClawTwoAssets flag and asset issuers", + "why_false_positive": "flags & tfClawTwoAssets, asset.getIssuer() != asset2.getIssuer() validates tfClawTwoAssets flag and asset issuers for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAsset.getIssuer() vs sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "asset.getIssuer() != issuer at preflight", + "issue_pattern": "Missing empty string validation for sfAsset.getIssuer() vs sfAccount", + "why_false_positive": "asset.getIssuer() != issuer validates sfAsset.getIssuer() vs sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount.asset() vs sfAsset", + "empty", + "string", + "validation" + ], + "evidence": "clawAmount && clawAmount->asset() != asset at preflight", + "issue_pattern": "Missing empty string validation for sfAmount.asset() vs sfAsset", + "why_false_positive": "clawAmount && clawAmount->asset() != asset validates sfAmount.asset() vs sfAsset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "clawAmount && *clawAmount <= beast::zero at preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "clawAmount && *clawAmount <= beast::zero validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "range", + "bounds", + "validation" + ], + "evidence": "clawAmount && *clawAmount <= beast::zero at preflight", + "issue_pattern": "Missing range validation for sfAmount", + "why_false_positive": "clawAmount && *clawAmount <= beast::zero validates sfAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount (issuer)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::account(ctx.tx[sfAccount])) at preclaim", + "issue_pattern": "Missing empty string validation for sfAccount (issuer)", + "why_false_positive": "ctx.view.read(keylet::account(ctx.tx[sfAccount])) validates sfAccount (issuer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfHolder", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::account(ctx.tx[sfHolder])) at preclaim", + "issue_pattern": "Missing empty string validation for sfHolder", + "why_false_positive": "ctx.view.read(keylet::account(ctx.tx[sfHolder])) validates sfHolder for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM pool (asset, asset2)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::amm(asset, asset2)) at preclaim", + "issue_pattern": "Missing empty string validation for AMM pool (asset, asset2)", + "why_false_positive": "ctx.view.read(keylet::amm(asset, asset2)) validates AMM pool (asset, asset2) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/dex/AMMClawback.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 10, + "name": "AMMClawback::getFlagsMask" + }, + { + "args": [ + "xrpl::PreflightContext const& ctx" + ], + "lineno": 15, + "name": "AMMClawback::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 25, + "name": "AMMClawback::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 54, + "name": "AMMClawback::preclaim" + }, + { + "args": [], + "lineno": 99, + "name": "AMMClawback::doApply" + }, + { + "args": [ + "Sandbox& sb" + ], + "lineno": 108, + "name": "AMMClawback::applyGuts" + }, + { + "args": [ + "Sandbox& sb", + "SLE const& ammSle", + "AccountID const& holder", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& holdLPtokens", + "STAmount const& amount" + ], + "lineno": 196, + "name": "AMMClawback::equalWithdrawMatchingOneAmount" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "AMMClawback is a new transaction type, so coverage is likely in integration/functional tests for AMM and clawback features. Look for test files such as 'AMMClawback_test.cpp', 'AMM_test.cpp', or 'AMMClawback.feature' in the test suite. Key validation paths (issuer/holder mismatch, asset checks, flag logic, permission checks) should be covered, but edge cases (e.g., MPTIssue handling, feature flag combinations, missing accounts, malformed assets) may lack exhaustive tests. Coverage for negative cases (malformed, permission denied, missing AMM) should be verified. No explicit test references in this file; review test suite for gaps in MPTokens, feature flag, and multi-asset scenarios.", + "validation_architecture": { + "auto_validated_fields": [ + "sfAccount", + "sfHolder", + "sfAsset", + "sfAsset2", + "sfAmount", + "flags" + ], + "framework": "xrpl transaction preflight/preclaim validation", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (prevents operation)", + "field": "featureAMMClawback", + "location": "checkExtraFeatures", + "validated_by": "ctx.rules.enabled", + "validates": [ + "Checks if the AMMClawback feature is enabled in the rules" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents operation)", + "field": "featureMPTokensV2", + "location": "checkExtraFeatures", + "validated_by": "ctx.rules.enabled", + "validates": [ + "Checks if the MPTokensV2 feature is enabled in the rules" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents operation)", + "field": "sfAmount (optional)", + "location": "checkExtraFeatures", + "validated_by": "ctx.tx[~sfAmount]", + "validates": [ + "If MPTokensV2 is not enabled, checks that sfAmount, sfAsset, and sfAsset2 do not hold MPTIssue" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAccount vs sfHolder", + "location": "preflight", + "validated_by": "issuer == holder", + "validates": [ + "Ensures the issuer and holder are not the same account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAsset", + "location": "preflight", + "validated_by": "isXRP(asset)", + "validates": [ + "Ensures the asset is not XRP" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "tfClawTwoAssets flag and asset issuers", + "location": "preflight", + "validated_by": "flags & tfClawTwoAssets, asset.getIssuer() != asset2.getIssuer()", + "validates": [ + "If tfClawTwoAssets is set, both assets must have the same issuer" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAsset.getIssuer() vs sfAccount", + "location": "preflight", + "validated_by": "asset.getIssuer() != issuer", + "validates": [ + "Ensures the asset's issuer matches the Account field" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount.asset() vs sfAsset", + "location": "preflight", + "validated_by": "clawAmount && clawAmount->asset() != asset", + "validates": [ + "If sfAmount is present, its asset subfield must match sfAsset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflight", + "validated_by": "clawAmount && *clawAmount <= beast::zero", + "validates": [ + "If sfAmount is present, it must be greater than zero" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_ACCOUNT", + "field": "sfAccount (issuer)", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::account(ctx.tx[sfAccount]))", + "validates": [ + "Ensures the issuer account exists in the ledger" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_ACCOUNT", + "field": "sfHolder", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::account(ctx.tx[sfHolder]))", + "validates": [ + "Ensures the holder account exists in the ledger" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_AMM", + "field": "AMM pool (asset, asset2)", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::amm(asset, asset2))", + "validates": [ + "Ensures the AMM pool for the asset pair exists" + ], + "validation_type": "existence|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMClawback.cpp.ai.md b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp.ai.md new file mode 100644 index 0000000000..25ebafd4c1 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMClawback.cpp.ai.md @@ -0,0 +1,41 @@ +# AMMClawback.cpp + +## Role in the System + +`AMMClawback.cpp` implements the `ttAMM_CLAWBACK` transaction, which lets a token issuer reclaim their own assets from an AMM liquidity pool that a particular account (the *holder*) has a position in. This transaction fills a regulatory gap that existed when the original AMM feature launched: issuers with clawback authority over their tokens had no mechanism to recover those assets once they had been deposited into an AMM pool, because the pool's LP tokens aren't the underlying token and the normal `Clawback` transaction only operates on trust lines. The `featureAMMClawback` amendment introduced this transactor to close that gap by layering a forced withdrawal on top of the existing AMM infrastructure. + +The file lives in the `dex/` group alongside `AMMWithdraw`, `AMMDeposit`, and the other AMM transactors. It is a heavy consumer of `AMMWithdraw`'s static helpers rather than reimplementing withdrawal math itself. + +## Transaction Fields and Flags + +The transaction identifies the operation through five fields: `sfAccount` (the issuer initiating the claw), `sfHolder` (the LP whose position will be unwound), `sfAsset` (the issuer's own token — always required), `sfAsset2` (the pool's paired asset), and an optional `sfAmount` (a cap on how much of `sfAsset` to recover). One flag is defined: `tfClawTwoAssets`, which extends the claw to include `sfAsset2` in addition to `sfAsset`. + +## Validation Pipeline + +`checkExtraFeatures` acts as a feature gate. It returns `false` — blocking the transaction entirely — if `featureAMMClawback` is not enabled. It also restricts MPT-denominated assets in any of the three amount fields to ledgers where `featureMPTokensV2` is active, preventing the newer token type from being used before the full MPT feature set has rolled out. + +`preflight` performs all stateless structural checks. The issuer cannot equal the holder (a self-clawback makes no sense). `sfAsset` cannot be XRP since XRP has no issuer and cannot be subject to clawback. When `tfClawTwoAssets` is set, both `sfAsset` and `sfAsset2` must share the same issuer — the flag is only useful when the issuer controls both sides of the pool, and the framework enforces this rather than silently ignoring the flag for the uncontrolled asset. The optional `sfAmount`, if present, must refer to the same asset as `sfAsset` and must be positive. + +`preclaim` performs ledger-state checks. Beyond verifying that both the issuer and holder accounts exist and that the AMM pool for the asset pair is present, it enforces permission requirements. The permission logic has a non-obvious branch: when `featureMPTokensV2` is **not** enabled and the issuer lacks `lsfAllowTrustLineClawback` or has set `lsfNoFreeze`, `preclaim` returns `tesSUCCESS` rather than an error code. This is an intentional soft-fail — the transaction is accepted by the network but becomes a no-op in `applyGuts` because no withdrawal path would be reached. This backward-compatible design avoids penalizing an issuer who submits the transaction on a network where the amendment has passed but whose account doesn't yet have the right flags set. When `featureMPTokensV2` is enabled, the per-asset `checkClawAsset` lambda takes over: for IOU assets it re-checks `lsfAllowTrustLineClawback` / `lsfNoFreeze` on the account; for MPT assets it checks the issuance-level `lsfMPTCanClawback` flag and verifies the issuance's `sfIssuer` matches the transaction submitter. The lambda cleanly handles the polymorphic `Asset` type via `visit()`. + +## Application Logic + +`doApply()` follows the standard XRPL sandbox pattern: it creates a `Sandbox` over the mutable view, delegates to `applyGuts()`, and only commits the sandbox to the raw view on success. This ensures that any partial failure leaves the ledger untouched. + +`applyGuts()` begins, when the `fixAMMClawbackRounding` amendment is active, by reading the holder's LP token balance and passing it through `verifyAndAdjustLPTokenBalance`. This corrects accumulated floating-point rounding drift in the AMM's `LPTokenBalance` field — a known issue when a holder has been the sole LP for a long time — before the withdrawal math begins. The balance is then re-read after the adjustment because the helper may have modified the ledger entry. + +The core withdrawal takes one of two paths depending on whether `sfAmount` is present: + +- **No `sfAmount`**: The full position is liquidated. `AMMWithdraw::equalWithdrawTokens` is called with the holder's entire LP token balance, burning all their tokens and returning both assets proportionally. The trading fee is explicitly passed as `0` because this is not a voluntary withdrawal — there is no fee discount to model. + +- **With `sfAmount`**: `equalWithdrawMatchingOneAmount` calculates the fraction of the pool that corresponds to the requested `sfAsset` amount. If the implied LP token withdrawal would exceed the holder's actual balance, it falls back to a full-position liquidation via `equalWithdrawTokens`. Otherwise it calls `AMMWithdraw::withdraw` with the computed token count and proportionally scaled `sfAsset2` amount. Here, `fixAMMClawbackRounding` adds another rounding pass: `getRoundedLPTokens` snaps the LP token count to a representable value, `adjustFracByTokens` re-derives the exact fraction from the snapped token count, and `getRoundedAsset` aligns both asset amounts. This prevents sub-dust residuals from stranding value in the pool. + +After withdrawal, `AMMWithdraw::deleteAMMAccountIfEmpty` checks whether the new LP token balance is zero; if so, it tears down the AMM object and its associated account, keeping the ledger free of empty AMM entries. + +The final transfer step uses `directSendNoFee` to move `amountWithdraw` (the recovered `sfAsset` amount) from the holder to the issuer. The second asset is only transferred if `tfClawTwoAssets` is set; absent that flag, `amount2Withdraw` is computed during withdrawal but simply left with the holder, preserving minimal disruption to the counter-party's position in cases where the issuer does not control `sfAsset2`. + +## Design Observations + +The decision to reuse `AMMWithdraw`'s static helpers — `equalWithdrawTokens`, `withdraw`, `deleteAMMAccountIfEmpty` — rather than duplicating the withdrawal mathematics is deliberate. The withdrawal invariants (constant-product formula, LP token burn, pool balance update) are complex and must be identical for all withdrawal paths. `AMMClawback` is architecturally a *policy layer* that determines *how much* to withdraw and *where the assets go* after withdrawal; the actual AMM state mutation is fully delegated. + +The unconditional `tfee = 0` throughout `applyGuts` and `equalWithdrawMatchingOneAmount` reflects that the AMM trading fee was designed to compensate LPs for impermanent loss during voluntary swaps. A clawback is not a voluntary trade and the fee would be economically incoherent — it would partially shield the holder from a regulatory action by making recovery more expensive for the issuer. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp.ai.json b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp.ai.json new file mode 100644 index 0000000000..d3f6e46d50 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp.ai.json @@ -0,0 +1,490 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AMMCreate::preflight" + ], + "entry_point": "AMMCreate::preflight", + "purpose": "Performs stateless validation of the AMMCreate transaction before it is processed.", + "validation_points": [ + "AMMCreate::preflight: asset uniqueness (amount.asset() == amount2.asset())", + "AMMCreate::preflight: invalidAMMAmount(amount)", + "AMMCreate::preflight: invalidAMMAmount(amount2)", + "AMMCreate::preflight: trading fee threshold (ctx.tx[sfTradingFee] > TRADING_FEE_THRESHOLD)" + ] + }, + { + "call_chain": [ + "AMMCreate::preclaim", + "keylet::amm", + "requireAuth", + "isFrozen", + "noDefaultRipple", + "xrpLiquid" + ], + "entry_point": "AMMCreate::preclaim", + "purpose": "Performs stateful validation, checking ledger state and account/asset conditions before applying the transaction.", + "validation_points": [ + "AMMCreate::preclaim: AMM instance existence (ctx.view.read(ammKeylet))", + "AMMCreate::preclaim: requireAuth for both assets", + "AMMCreate::preclaim: isFrozen for both assets", + "AMMCreate::preclaim: noDefaultRipple for both assets", + "AMMCreate::preclaim: xrpLiquid (reserve check)" + ] + }, + { + "call_chain": [ + "AMMCreate::doApply", + "applyCreate" + ], + "entry_point": "AMMCreate::doApply", + "purpose": "Applies the transaction after all validations pass.", + "validation_points": [ + "Relies on preflight and preclaim having already validated inputs" + ] + }, + { + "call_chain": [ + "AMMCreate::checkExtraFeatures" + ], + "entry_point": "AMMCreate::checkExtraFeatures", + "purpose": "Checks feature flags and token type compatibility before transaction processing.", + "validation_points": [ + "AMMCreate::checkExtraFeatures: ammEnabled(ctx.rules)", + "AMMCreate::checkExtraFeatures: featureMPTokensV2 and MPTIssue checks" + ] + } + ], + "data_flows": [ + { + "field": "sfAmount", + "flow": [ + "ctx.tx[sfAmount]", + "amount = ctx.tx[sfAmount]", + "used in preflight, preclaim, checkExtraFeatures" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Checked for asset uniqueness (asset() comparison)", + "Validated by invalidAMMAmount(amount)", + "Checked for MPTIssue type" + ], + "validated_at": "AMMCreate::preflight, AMMCreate::checkExtraFeatures" + }, + { + "field": "sfAmount2", + "flow": [ + "ctx.tx[sfAmount2]", + "amount2 = ctx.tx[sfAmount2]", + "used in preflight, preclaim, checkExtraFeatures" + ], + "origin": "ctx.tx[sfAmount2] (transaction input)", + "transformations": [ + "Checked for asset uniqueness (asset() comparison)", + "Validated by invalidAMMAmount(amount2)", + "Checked for MPTIssue type" + ], + "validated_at": "AMMCreate::preflight, AMMCreate::checkExtraFeatures" + }, + { + "field": "sfTradingFee", + "flow": [ + "ctx.tx[sfTradingFee]", + "used in preflight" + ], + "origin": "ctx.tx[sfTradingFee] (transaction input)", + "transformations": [ + "Compared to TRADING_FEE_THRESHOLD" + ], + "validated_at": "AMMCreate::preflight" + }, + { + "field": "AMM feature enablement", + "flow": [ + "ctx.rules", + "ammEnabled(ctx.rules)", + "used in checkExtraFeatures" + ], + "origin": "ctx.rules (network rules/feature flags)", + "transformations": [ + "Boolean check for AMM feature" + ], + "validated_at": "AMMCreate::checkExtraFeatures" + }, + { + "field": "Account authorization", + "flow": [ + "ctx.tx[sfAccount]", + "accountID = ctx.tx[sfAccount]", + "used in preclaim for requireAuth, isFrozen, xrpLiquid" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Checked for authorization to hold assets", + "Checked for frozen status", + "Checked for sufficient XRP reserve" + ], + "validated_at": "AMMCreate::preclaim" + } + ], + "description": "Implements the logic for creating an Automated Market Maker (AMM) instance on the XRPL, including preflight checks, authorization, reserve and trustline management, and ledger updates.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount, sfAmount2", + "empty", + "string", + "validation" + ], + "evidence": "asset() comparison at AMMCreate::preflight", + "issue_pattern": "Missing empty string validation for sfAmount, sfAmount2", + "why_false_positive": "asset() comparison validates sfAmount, sfAmount2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAmount(amount) at AMMCreate::preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "invalidAMMAmount(amount) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount2", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAmount(amount2) at AMMCreate::preflight", + "issue_pattern": "Missing empty string validation for sfAmount2", + "why_false_positive": "invalidAMMAmount(amount2) validates sfAmount2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTradingFee", + "empty", + "string", + "validation" + ], + "evidence": "comparison with TRADING_FEE_THRESHOLD at AMMCreate::preflight", + "issue_pattern": "Missing empty string validation for sfTradingFee", + "why_false_positive": "comparison with TRADING_FEE_THRESHOLD validates sfTradingFee for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfTradingFee", + "range", + "bounds", + "validation" + ], + "evidence": "comparison with TRADING_FEE_THRESHOLD at AMMCreate::preflight", + "issue_pattern": "Missing range validation for sfTradingFee", + "why_false_positive": "comparison with TRADING_FEE_THRESHOLD validates sfTradingFee range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM feature enablement", + "empty", + "string", + "validation" + ], + "evidence": "ammEnabled(ctx.rules) at AMMCreate::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for AMM feature enablement", + "why_false_positive": "ammEnabled(ctx.rules) validates AMM feature enablement for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "MPTokensV2 feature and MPTIssue in sfAmount/sfAmount2", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled(featureMPTokensV2) and holds() at AMMCreate::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for MPTokensV2 feature and MPTIssue in sfAmount/sfAmount2", + "why_false_positive": "ctx.rules.enabled(featureMPTokensV2) and holds() validates MPTokensV2 feature and MPTIssue in sfAmount/sfAmount2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM existence for token pair", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(ammKeylet) at AMMCreate::preclaim", + "issue_pattern": "Missing empty string validation for AMM existence for token pair", + "why_false_positive": "ctx.view.read(ammKeylet) validates AMM existence for token pair for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Authorization for asset1", + "empty", + "string", + "validation" + ], + "evidence": "requireAuth(ctx.view, amount.asset(), accountID) at AMMCreate::preclaim", + "issue_pattern": "Missing empty string validation for Authorization for asset1", + "why_false_positive": "requireAuth(ctx.view, amount.asset(), accountID) validates Authorization for asset1 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Authorization for asset2", + "empty", + "string", + "validation" + ], + "evidence": "requireAuth(ctx.view, amount2.asset(), accountID) at AMMCreate::preclaim", + "issue_pattern": "Missing empty string validation for Authorization for asset2", + "why_false_positive": "requireAuth(ctx.view, amount2.asset(), accountID) validates Authorization for asset2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Frozen status for asset1 and asset2", + "empty", + "string", + "validation" + ], + "evidence": "isFrozen(ctx.view, accountID, amount.asset()) || isFrozen(ctx.view, accountID, amount2.asset()) at AMMCreate::preclaim", + "issue_pattern": "Missing empty string validation for Frozen status for asset1 and asset2", + "why_false_positive": "isFrozen(ctx.view, accountID, amount.asset()) || isFrozen(ctx.view, accountID, amount2.asset()) validates Frozen status for asset1 and asset2 for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/dex/AMMCreate.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "AMMCreate::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 22, + "name": "AMMCreate::preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 48, + "name": "AMMCreate::calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 54, + "name": "AMMCreate::preclaim" + }, + { + "args": [ + "ApplyContext& ctx_", + "Sandbox& sb", + "AccountID const& account_", + "beast::Journal j_" + ], + "lineno": 143, + "name": "applyCreate" + }, + { + "args": [], + "lineno": 266, + "name": "AMMCreate::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "Tests for AMMCreate are likely found in integration/tx/AMMCreate_test.cpp, possibly also in unit tests for AMMHelpers or protocol/AMMCore. Typical tests cover valid/invalid asset pairs, invalid amounts, trading fee limits, feature flag enablement, duplicate AMM creation, authorization, frozen assets, and reserve checks. Gaps may exist for edge cases in feature flag combinations (e.g., MPTokensV2 off with MPTIssue), and for negative test cases involving complex asset/freeze/authorization scenarios. Some validation logic (e.g., noDefaultRipple, requireAuth) may rely on helper functions that are not directly unit tested in the context of AMMCreate.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation via business logic and helper functions (no external validation framework detected)", + "validation_layer": "business_logic (preflight and preclaim transaction hooks)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temBAD_AMM_TOKENS", + "field": "sfAmount, sfAmount2", + "location": "AMMCreate::preflight", + "validated_by": "asset() comparison", + "validates": [ + "Ensures the two assets are not the same" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "error code returned by invalidAMMAmount", + "field": "sfAmount", + "location": "AMMCreate::preflight", + "validated_by": "invalidAMMAmount(amount)", + "validates": [ + "Checks if amount is a valid AMM amount (format, value, etc.)" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 1.0, + "error_thrown": "error code returned by invalidAMMAmount", + "field": "sfAmount2", + "location": "AMMCreate::preflight", + "validated_by": "invalidAMMAmount(amount2)", + "validates": [ + "Checks if amount2 is a valid AMM amount (format, value, etc.)" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_FEE", + "field": "sfTradingFee", + "location": "AMMCreate::preflight", + "validated_by": "comparison with TRADING_FEE_THRESHOLD", + "validates": [ + "Ensures trading fee does not exceed threshold" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents transaction)", + "field": "AMM feature enablement", + "location": "AMMCreate::checkExtraFeatures", + "validated_by": "ammEnabled(ctx.rules)", + "validates": [ + "Checks if AMM feature is enabled in rules" + ], + "validation_type": "business_logic|feature_flag" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents transaction)", + "field": "MPTokensV2 feature and MPTIssue in sfAmount/sfAmount2", + "location": "AMMCreate::checkExtraFeatures", + "validated_by": "ctx.rules.enabled(featureMPTokensV2) and holds()", + "validates": [ + "Prevents use of MPTIssue unless featureMPTokensV2 is enabled" + ], + "validation_type": "business_logic|feature_flag|type" + }, + { + "confidence": 1.0, + "error_thrown": "tecDUPLICATE", + "field": "AMM existence for token pair", + "location": "AMMCreate::preclaim", + "validated_by": "ctx.view.read(ammKeylet)", + "validates": [ + "Ensures no existing AMM for the token pair" + ], + "validation_type": "business_logic|uniqueness" + }, + { + "confidence": 1.0, + "error_thrown": "error code returned by requireAuth", + "field": "Authorization for asset1", + "location": "AMMCreate::preclaim", + "validated_by": "requireAuth(ctx.view, amount.asset(), accountID)", + "validates": [ + "Checks if account is authorized to use asset1" + ], + "validation_type": "business_logic|authorization" + }, + { + "confidence": 1.0, + "error_thrown": "error code returned by requireAuth", + "field": "Authorization for asset2", + "location": "AMMCreate::preclaim", + "validated_by": "requireAuth(ctx.view, amount2.asset(), accountID)", + "validates": [ + "Checks if account is authorized to use asset2" + ], + "validation_type": "business_logic|authorization" + }, + { + "confidence": 1.0, + "error_thrown": "tecFROZEN", + "field": "Frozen status for asset1 and asset2", + "location": "AMMCreate::preclaim", + "validated_by": "isFrozen(ctx.view, accountID, amount.asset()) || isFrozen(ctx.view, accountID, amount2.asset())", + "validates": [ + "Ensures neither asset is globally or individually frozen" + ], + "validation_type": "business_logic|status" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMCreate.cpp.ai.md b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp.ai.md new file mode 100644 index 0000000000..d90e1555ba --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMCreate.cpp.ai.md @@ -0,0 +1,57 @@ +# `AMMCreate.cpp` — AMM Pool Bootstrapping Transactor + +## Role in the System + +This file implements the `AMMCreate` transactor, which handles the `ttAMM_CREATE` transaction type on the XRP Ledger. Its purpose is to bootstrap an Automated Market Maker (AMM) liquidity pool from scratch: creating the ledger objects that represent the pool, minting LP tokens for the pool's first liquidity provider, and registering the new token pair with the payment engine's order book. Every other AMM transaction (`AMMDeposit`, `AMMWithdraw`, `AMMBid`, `AMMVote`, `AMMDelete`) depends on the three-object structure that `AMMCreate` establishes. + +## The Transactor Lifecycle + +`AMMCreate` inherits from `Transactor` and participates in the standard four-phase processing model: `checkExtraFeatures` → `preflight` → `preclaim` → `doApply`. The fee is computed separately via `calculateBaseFee`, which charges one owner reserve — the cost of the new `ltAMM` ledger entry. + +## Validation Layers + +### `checkExtraFeatures` (feature gating) + +The first gate refuses the transaction entirely if the `ammEnabled` rules check fails, preventing AMM transactions on networks or ledger versions that predate XLS-30. It also blocks Multi-Purpose Token (MPT) assets unless `featureMPTokensV2` is active, enforcing a clean feature boundary between the two amendments. + +### `preflight` (stateless checks) + +`preflight` operates without ledger access and enforces three invariants: the two assets must be distinct (same asset on both sides makes no economic sense), each amount must pass `invalidAMMAmount` (which validates format, positivity, and type constraints), and the trading fee must not exceed `TRADING_FEE_THRESHOLD` (1000 basis points = 1%). + +### `preclaim` (stateful checks) + +`preclaim` is the heaviest validation phase. Its checks, in execution order: + +**Uniqueness**: `keylet::amm(amount.asset(), amount2.asset())` computes the deterministic AMM object key and checks for prior existence. The key is an order-independent hash of both assets' currency/issuer fields — the same key is reached regardless of how the creator ordered the assets in the transaction. + +**Authorization and freeze**: Both assets must be reachable by the creator account (`requireAuth`) and must not be frozen at the global or per-account level (`isFrozen`). + +**DefaultRipple**: For IOU (non-XRP, non-MPT) assets, the `noDefaultRipple` lambda verifies the issuer account has the `lsfDefaultRipple` flag set. Without it, the AMM's trust lines cannot participate in rippling, which would make the pool unreachable in payment paths. XRP and MPT assets skip this check. + +**Reserve and balance**: `xrpLiquid` calculates the creator's spendable XRP after accounting for one extra reserve (for the LP token trust line the creator will receive). The same XRP liquid balance is also compared against the deposit amount if one of the assets is XRP. For IOU/MPT assets, `accountFunds` is used with frozen-zero and unauthorized-zero semantics to prevent AMM creation from bypassing freezes. + +**Anti-nesting**: The `isLPToken` lambda prevents using LP tokens from another AMM pool as an asset in the new pool. It detects LP token issuers by checking if the issuer's `AccountRoot` carries the `sfAMMID` field (a marker added to all AMM pseudo-accounts). + +**Address collision**: When `featureSingleAssetVault` is enabled, `pseudoAccountAddress` checks that the would-be pseudo-account address doesn't already exist in the ledger before committing to it. + +**MPT allowance**: `checkMPTTxAllowed` validates that the MPT issuance permits `ttAMM_CREATE` operations for the account. + +**Clawback guard**: When `featureAMMClawback` is disabled, AMM creation is rejected if either asset's issuer has clawback enabled (`lsfAllowTrustLineClawback` for IOUs, `lsfMPTCanClawback` for MPTs). An issuer with clawback could drain the pool unilaterally, so this blocks the creation until the `featureAMMClawback` amendment — which handles clawback in a controlled manner — is live. + +## Ledger Mutation: `applyCreate` + +The static `applyCreate` function, called from `doApply` inside a `Sandbox`, performs all actual ledger changes. The sandbox pattern ensures the entire operation is atomic: if any step returns an error, the sandbox is simply discarded rather than partially committed. + +**Pseudo-account creation**: `createPseudoAccount(sb, ammKeylet.key, sfAMMID)` derives an `AccountRoot` from the AMM keylet's hash. The account has no master key and is flagged with `sfAMMID`, marking it as a non-user pseudo-account. This account will hold XRP (if one asset is XRP) and serve as the LP token issuer. + +**LP token issuance**: `ammLPTIssue` derives the LP token currency from the asset pair and the pseudo-account ID. `ammLPTokens` computes the initial supply as `sqrt(asset1 * asset2)` (the geometric mean), which is the standard constant-product AMM seeding formula. The LP tokens are created with a zero credit-limit trust line — a deliberate design choice called out in an inline comment: this prevents anyone from receiving LP tokens without affirmative action (a deposit, trust line creation, or offer crossing). The tokens are then sent from the pseudo-account to the creator. + +**`ltAMM` object construction**: The AMM ledger entry is built with both assets stored in canonical order via `std::minmax`, ensuring the object's content always has a predictable low/high ordering regardless of transaction input order. `initializeFeeAuctionVote` populates the initial fee, auction slot, and voting state, with the creator automatically receiving the first auction slot and vote. + +**Asset transfer via `sendAndInitTrustOrMPT`**: Each asset is transferred from the creator to the pseudo-account. For IOU assets, the standard trust line created by `accountSend` is then retrieved and marked with `lsfAMMNode`, distinguishing AMM-held trust lines from normal LP trust lines. For MPT assets, `createMPToken` establishes the pseudo-account's MPT holding with the `lsfMPTAMM` flag; if the MPT requires authorization and the pseudo-account hasn't been pre-authorized, `lsfMPTAuthorized` is also set. In both cases, `WaiveTransferFee::Yes` waives the transfer fee for the seeding transfer. + +**Order book registration**: Both swap directions (asset1→asset2 and asset2→asset1) are registered with `OrderBookDB` at their respective initial exchange rates. This makes the new pool immediately visible to the payment and offer-crossing engines as a liquidity source. + +## Relationship to Sibling Files + +The dex transactor directory contains `AMMDeposit.cpp`, `AMMWithdraw.cpp`, `AMMBid.cpp`, `AMMVote.cpp`, `AMMDelete.cpp`, and `AMMClawback.cpp`. All of them locate their target pool via `keylet::amm()` — the same deterministic key established here. `AMMCreate` is thus the genesis point of a state machine whose lifecycle is terminated only by `AMMDelete` (when the pool is emptied). The `featureAMMClawback` guard in `preclaim` is mirrored by `AMMClawback.cpp`, which provides the controlled clawback path that makes the guard safe to lift. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMDelete.cpp.ai.json b/src/libxrpl/tx/transactors/dex/AMMDelete.cpp.ai.json new file mode 100644 index 0000000000..757bfa1188 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMDelete.cpp.ai.json @@ -0,0 +1,251 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AMMDelete::preflight" + ], + "entry_point": "AMMDelete::preflight", + "purpose": "Initial transaction preflight checks (syntax, basic feature gating).", + "validation_points": [ + "AMMDelete::checkExtraFeatures (called externally, not from preflight in this code)" + ] + }, + { + "call_chain": [ + "AMMDelete::preclaim" + ], + "entry_point": "AMMDelete::preclaim", + "purpose": "Checks ledger state for AMM existence and LP token balance before transaction is allowed to proceed.", + "validation_points": [ + "AMM ledger entry existence (ctx.view.read(keylet::amm(...)))", + "AMM LP Token Balance ((*ammSle)[sfLPTokenBalance] != beast::zero)" + ] + }, + { + "call_chain": [ + "AMMDelete::doApply", + "deleteAMMAccount" + ], + "entry_point": "AMMDelete::doApply", + "purpose": "Applies the AMM deletion to the ledger if all validations pass.", + "validation_points": [ + "Assumes preclaim has validated; doApply itself does not re-validate" + ] + }, + { + "call_chain": [ + "AMMDelete::checkExtraFeatures" + ], + "entry_point": "AMMDelete::checkExtraFeatures", + "purpose": "Checks if AMM and multi-protocol token features are enabled and if asset fields are valid.", + "validation_points": [ + "AMM feature enabled (ammEnabled(ctx.rules))", + "featureMPTokensV2 or MPTIssue in sfAsset/sfAsset2" + ] + } + ], + "data_flows": [ + { + "field": "ctx.rules", + "flow": [ + "ctx.rules", + "ammEnabled(ctx.rules) or ctx.rules.enabled(featureMPTokensV2)" + ], + "origin": "PreflightContext or PreclaimContext (transaction context)", + "transformations": [ + "Checked for feature enablement" + ], + "validated_at": "AMMDelete::checkExtraFeatures" + }, + { + "field": "ctx.tx[sfAsset], ctx.tx[sfAsset2]", + "flow": [ + "ctx.tx[sfAsset], ctx.tx[sfAsset2]", + "keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2])", + "ctx.view.read(keylet::amm(...))" + ], + "origin": "Transaction fields (sfAsset, sfAsset2)", + "transformations": [ + "Used to construct AMM ledger key", + "Checked for MPTIssue type" + ], + "validated_at": "AMMDelete::checkExtraFeatures (MPTIssue check), AMMDelete::preclaim (AMM existence)" + }, + { + "field": "ammSle[sfLPTokenBalance]", + "flow": [ + "ctx.view.read(keylet::amm(...))", + "(*ammSle)[sfLPTokenBalance]" + ], + "origin": "AMM ledger entry (SLE) read from ledger", + "transformations": [ + "Checked for zero (empty pool)" + ], + "validated_at": "AMMDelete::preclaim" + } + ], + "description": "Implements the logic for deleting an Automated Market Maker (AMM) instance in the XRPL, including preflight checks, preclaim validation, and application of the deletion to the ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM feature enabled (via rules)", + "empty", + "string", + "validation" + ], + "evidence": "ammEnabled(ctx.rules) at AMMDelete::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for AMM feature enabled (via rules)", + "why_false_positive": "ammEnabled(ctx.rules) validates AMM feature enabled (via rules) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "featureMPTokensV2 or MPTIssue in sfAsset/sfAsset2", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled(featureMPTokensV2) || (!ctx.tx[sfAsset].holds() && !ctx.tx[sfAsset2].holds()) at AMMDelete::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for featureMPTokensV2 or MPTIssue in sfAsset/sfAsset2", + "why_false_positive": "ctx.rules.enabled(featureMPTokensV2) || (!ctx.tx[sfAsset].holds() && !ctx.tx[sfAsset2].holds()) validates featureMPTokensV2 or MPTIssue in sfAsset/sfAsset2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM ledger entry existence (asset pair validity)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2])) at AMMDelete::preclaim", + "issue_pattern": "Missing empty string validation for AMM ledger entry existence (asset pair validity)", + "why_false_positive": "ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2])) validates AMM ledger entry existence (asset pair validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM LP Token Balance", + "empty", + "string", + "validation" + ], + "evidence": "(*ammSle)[sfLPTokenBalance] != beast::zero at AMMDelete::preclaim", + "issue_pattern": "Missing empty string validation for AMM LP Token Balance", + "why_false_positive": "(*ammSle)[sfLPTokenBalance] != beast::zero validates AMM LP Token Balance for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/dex/AMMDelete.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "AMMDelete::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "AMMDelete::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "AMMDelete::preclaim" + }, + { + "args": [], + "lineno": 36, + "name": "AMMDelete::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "AMMDelete is likely tested in integration/functional tests for AMM operations, such as 'AMMDelete_test.cpp', 'AMM_test.cpp', or broader DEX/AMM test suites. The specific validation paths (feature enablement, asset type checks, AMM existence, LP token balance) should be covered by tests that attempt to delete AMMs under various conditions (feature disabled, invalid asset types, non-existent AMMs, non-empty pools). Gaps may exist if tests do not explicitly cover all combinations of feature flags and asset types, or if negative cases (e.g., MPTIssue present, AMM not found) are not tested. No unit tests are shown in this file; coverage depends on external test files.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": "AMM feature enabled (via rules)", + "location": "AMMDelete::checkExtraFeatures", + "validated_by": "ammEnabled(ctx.rules)", + "validates": [ + "Checks if AMM feature is enabled in the current ruleset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": "featureMPTokensV2 or MPTIssue in sfAsset/sfAsset2", + "location": "AMMDelete::checkExtraFeatures", + "validated_by": "ctx.rules.enabled(featureMPTokensV2) || (!ctx.tx[sfAsset].holds() && !ctx.tx[sfAsset2].holds())", + "validates": [ + "If featureMPTokensV2 is not enabled, ensures sfAsset and sfAsset2 do not hold MPTIssue" + ], + "validation_type": "business_logic|type" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_AMM", + "field": "AMM ledger entry existence (asset pair validity)", + "location": "AMMDelete::preclaim", + "validated_by": "ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2]))", + "validates": [ + "Checks that the AMM ledger entry for the asset pair exists" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecAMM_NOT_EMPTY", + "field": "AMM LP Token Balance", + "location": "AMMDelete::preclaim", + "validated_by": "(*ammSle)[sfLPTokenBalance] != beast::zero", + "validates": [ + "Ensures the AMM's LP token balance is zero before allowing deletion" + ], + "validation_type": "business_logic|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMDelete.cpp.ai.md b/src/libxrpl/tx/transactors/dex/AMMDelete.cpp.ai.md new file mode 100644 index 0000000000..f4e16c9228 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMDelete.cpp.ai.md @@ -0,0 +1,44 @@ +# `AMMDelete.cpp` — AMM Account Cleanup Transactor + +## Role in the System + +`AMMDelete.cpp` implements the `AMMDelete` transactor, which handles the on-ledger transaction that permanently removes an Automated Market Maker pool from the XRP Ledger. An AMM pool consists of a synthetic account object (`ltAMM` SLE), a dedicated root account entry, zero or more LP-token trustlines held by former liquidity providers, and optionally MPToken objects for Multi-Purpose Token pools. All of these ledger entries must be reclaimed once a pool has been drained to zero. This transactor is the mechanism that performs that reclamation. + +The file is compact — just four methods, each occupying its designated phase in the XRPL transactor lifecycle — but it orchestrates a non-trivial multi-transaction deletion protocol through its delegation to `deleteAMMAccount()` in `AMMHelpers`. + +## Transactor Lifecycle + +XRPL transactors decompose processing into distinct phases, each evaluated under progressively stronger ledger access. + +**`checkExtraFeatures`** is a static feature-gate, called before `preflight`, that decides whether this transaction type is even permissible under the current amendment ruleset. Two conditions must both hold: the core AMM amendment must be active (`ammEnabled(ctx.rules)`), and — crucially — if the `featureMPTokensV2` amendment is not yet live, neither `sfAsset` nor `sfAsset2` may carry an `MPTIssue`. This prevents MPT-backed AMM pools from being submitted before the network is ready to reason about them, guarding against nodes that support the transaction format but not the full MPT semantics. + +**`preflight`** is intentionally empty, returning `tesSUCCESS` unconditionally. All syntactic validation for the transaction fields (`sfAsset`, `sfAsset2`) is handled by the framework-level field validators and by `checkExtraFeatures`; there is nothing left to check in a fee-free, ledger-agnostic context. + +**`preclaim`** reads the ledger before any state changes to enforce two hard prerequisites. First, it resolves the AMM ledger entry via `keylet::amm(sfAsset, sfAsset2)`. If no such entry exists the transaction fails immediately with `terNO_AMM` — a retriable error code signalling an invalid precondition rather than a malformed transaction. Second, and more importantly, it reads `sfLPTokenBalance` off the AMM SLE and rejects with `tecAMM_NOT_EMPTY` if any LP tokens remain outstanding. This is the fundamental invariant: deletion is only permitted once all liquidity providers have withdrawn their shares and the pool holds no obligations. There is no way to force-close a pool that still has external holders. + +**`doApply`** performs the actual ledger mutations, working against a `Sandbox` — a copy-on-write view layered on top of the current ledger state. The sandbox ensures that partial work can be discarded if anything goes wrong, and that changes are only committed atomically via `sb.apply(ctx_.rawView())`. + +## Chunked Deletion and `tecINCOMPLETE` + +The most architecturally significant design decision in this file is how `doApply` handles the return value from `deleteAMMAccount`: + +```cpp +auto const ter = deleteAMMAccount(sb, ctx_.tx[sfAsset], ctx_.tx[sfAsset2], j_); +if (isTesSuccess(ter) || ter == tecINCOMPLETE) + sb.apply(ctx_.rawView()); +return ter; +``` + +`deleteAMMAccount` (in `AMMHelpers.cpp`) deletes up to `maxDeletableAMMTrustLines` (512) trustlines per call. A popular AMM pool may accumulate many thousands of LP-token trustlines from holders who exited their positions without explicitly closing the trustline. If more than 512 remain when deletion is attempted, `deleteAMMAccount` returns `tecINCOMPLETE` rather than blocking the transaction entirely. + +The key design insight is that `tecINCOMPLETE` is treated as a *partial commit*: `doApply` calls `sb.apply` even when it receives `tecINCOMPLETE`. Up to 512 trustlines are deleted, those changes are written to the ledger, and the transaction succeeds in a partial sense — the submitter pays the transaction fee. Because `preclaim` already confirmed zero LP token balance, the AMM is guaranteed to be draining toward zero without the possibility of new liquidity entering. Anyone can resubmit the `AMMDelete` transaction repeatedly until all trustlines are gone and the AMM SLE and root account are finally erased, at which point `deleteAMMAccount` returns `tesSUCCESS`. + +This incremental approach is necessary because XRPL transactions must complete within bounded ledger-close time; a single transaction cannot iterate over an unbounded number of ledger objects without risking consensus timeouts or excessive resource consumption. + +## Relationship to `AMMHelpers` + +The actual deletion sequence — iterating the AMM account's owner directory, deleting trustlines, then deleting MPToken objects, then removing the directory and erasing the AMM SLE and account SLE — all lives in `deleteAMMAccount` and its helpers (`deleteAMMTrustLines`, `deleteAMMMPTokens`). `AMMDelete.cpp` is intentionally thin: it owns only the three-phase validation logic and the `Sandbox`/commit pattern. This separation means the same deletion logic can be reused by other transactors (e.g., `AMMWithdraw` can trigger cleanup when the pool reaches zero) without duplicating the ledger-mutation code. + +## Error Handling Notes + +`deleteAMMAccount` internally guards against impossible states — missing AMM account, non-zero trustline balances, unexpected SLE types — by returning `tecINTERNAL`, which are marked `LCOV_EXCL` as unreachable under normal operation. Those paths represent invariant violations that should never occur given correct preceding validation in `preclaim` and correct operation of the withdrawal transactors that zero-out balances before trustlines are deleted. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp.ai.json b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp.ai.json new file mode 100644 index 0000000000..5af1305d83 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp.ai.json @@ -0,0 +1,613 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AMMDeposit::doApply", + "AMMDeposit::preflight", + "AMMDeposit::preclaim", + "AMMDeposit::applyGuts", + "AMMDeposit::deposit" + ], + "entry_point": "AMMDeposit::doApply", + "purpose": "Processes an AMMDeposit transaction, validating input and applying changes to the ledger.", + "validation_points": [ + "AMMDeposit::preflight", + "AMMDeposit::preclaim" + ] + }, + { + "call_chain": [ + "AMMDeposit::preflight" + ], + "entry_point": "AMMDeposit::preflight", + "purpose": "Performs stateless validation of the AMMDeposit transaction fields and flags.", + "validation_points": [ + "AMMDeposit::preflight" + ] + }, + { + "call_chain": [ + "AMMDeposit::checkExtraFeatures" + ], + "entry_point": "AMMDeposit::checkExtraFeatures", + "purpose": "Checks if extra features (like MPTokens) are enabled and if the transaction is compatible.", + "validation_points": [ + "AMMDeposit::checkExtraFeatures" + ] + } + ], + "data_flows": [ + { + "field": "flags", + "flow": [ + "Transaction input (ctx.tx)", + "AMMDeposit::preflight (flags = ctx.tx.getFlags())", + "Bitmask checks for tfDepositSubTx and specific flag types", + "Determines which validation branch to take" + ], + "origin": "ctx.tx.getFlags()", + "transformations": [ + "Bitmasking with tfDepositSubTx and other flag constants", + "std::popcount to ensure only one sub-transaction flag is set" + ], + "validated_at": "AMMDeposit::preflight" + }, + { + "field": "amount", + "flow": [ + "Transaction input (ctx.tx)", + "AMMDeposit::preflight (amount = ctx.tx[~sfAmount])", + "Branch-specific validation (presence, type, asset match)", + "Passed to invalidAMMAmount for further validation" + ], + "origin": "ctx.tx[~sfAmount]", + "transformations": [ + "Optional extraction", + "Type/asset checks", + "Passed to invalidAMMAmount" + ], + "validated_at": "AMMDeposit::preflight" + }, + { + "field": "amount2", + "flow": [ + "Transaction input (ctx.tx)", + "AMMDeposit::preflight (amount2 = ctx.tx[~sfAmount2])", + "Branch-specific validation (presence, type, asset match)", + "Passed to invalidAMMAmount for further validation" + ], + "origin": "ctx.tx[~sfAmount2]", + "transformations": [ + "Optional extraction", + "Type/asset checks", + "Passed to invalidAMMAmount" + ], + "validated_at": "AMMDeposit::preflight" + }, + { + "field": "lpTokens", + "flow": [ + "Transaction input (ctx.tx)", + "AMMDeposit::preflight (lpTokens = ctx.tx[~sfLPTokenOut])", + "Branch-specific validation (presence, value > 0)", + "Used in deposit logic" + ], + "origin": "ctx.tx[~sfLPTokenOut]", + "transformations": [ + "Optional extraction", + "Value check (*lpTokens <= beast::zero)" + ], + "validated_at": "AMMDeposit::preflight" + }, + { + "field": "ePrice", + "flow": [ + "Transaction input (ctx.tx)", + "AMMDeposit::preflight (ePrice = ctx.tx[~sfEPrice])", + "Branch-specific validation (presence/absence depending on flags)", + "Used in deposit logic" + ], + "origin": "ctx.tx[~sfEPrice]", + "transformations": [ + "Optional extraction" + ], + "validated_at": "AMMDeposit::preflight" + }, + { + "field": "tradingFee", + "flow": [ + "Transaction input (ctx.tx)", + "AMMDeposit::preflight (tradingFee = ctx.tx[~sfTradingFee])", + "Branch-specific validation (should not be present except for tfTwoAssetIfEmpty)", + "Used in deposit logic" + ], + "origin": "ctx.tx[~sfTradingFee]", + "transformations": [ + "Optional extraction" + ], + "validated_at": "AMMDeposit::preflight" + }, + { + "field": "asset / asset2", + "flow": [ + "Transaction input (ctx.tx)", + "AMMDeposit::preflight (asset = ctx.tx[sfAsset], asset2 = ctx.tx[sfAsset2])", + "invalidAMMAssetPair(asset, asset2) validation", + "Used for further amount validation" + ], + "origin": "ctx.tx[sfAsset], ctx.tx[sfAsset2]", + "transformations": [ + "Type checks", + "invalidAMMAssetPair" + ], + "validated_at": "AMMDeposit::preflight" + } + ], + "description": "Implements the AMMDeposit transaction logic for the XRPL decentralized exchange, including preflight checks, preclaim checks, and the application of various deposit strategies into an Automated Market Maker (AMM) pool.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "flags (tfDepositSubTx)", + "empty", + "string", + "validation" + ], + "evidence": "std::popcount(flags & tfDepositSubTx) != 1 at AMMDeposit::preflight", + "issue_pattern": "Missing empty string validation for flags (tfDepositSubTx)", + "why_false_positive": "std::popcount(flags & tfDepositSubTx) != 1 validates flags (tfDepositSubTx) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfLPToken, lpTokens, ePrice, amount, amount2, tradingFee", + "empty", + "string", + "validation" + ], + "evidence": "if ((flags & tfLPToken) != 0u) { ... } at AMMDeposit::preflight", + "issue_pattern": "Missing empty string validation for tfLPToken, lpTokens, ePrice, amount, amount2, tradingFee", + "why_false_positive": "if ((flags & tfLPToken) != 0u) { ... } validates tfLPToken, lpTokens, ePrice, amount, amount2, tradingFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfSingleAsset, amount, amount2, lpTokens, ePrice, tradingFee", + "empty", + "string", + "validation" + ], + "evidence": "else if ((flags & tfSingleAsset) != 0u) { ... } at AMMDeposit::preflight", + "issue_pattern": "Missing empty string validation for tfSingleAsset, amount, amount2, lpTokens, ePrice, tradingFee", + "why_false_positive": "else if ((flags & tfSingleAsset) != 0u) { ... } validates tfSingleAsset, amount, amount2, lpTokens, ePrice, tradingFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfTwoAsset, amount, amount2, lpTokens, ePrice, tradingFee", + "empty", + "string", + "validation" + ], + "evidence": "else if ((flags & tfTwoAsset) != 0u) { ... } at AMMDeposit::preflight", + "issue_pattern": "Missing empty string validation for tfTwoAsset, amount, amount2, lpTokens, ePrice, tradingFee", + "why_false_positive": "else if ((flags & tfTwoAsset) != 0u) { ... } validates tfTwoAsset, amount, amount2, lpTokens, ePrice, tradingFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfOneAssetLPToken, amount, lpTokens, amount2, ePrice, tradingFee", + "empty", + "string", + "validation" + ], + "evidence": "else if ((flags & tfOneAssetLPToken) != 0u) { ... } at AMMDeposit::preflight", + "issue_pattern": "Missing empty string validation for tfOneAssetLPToken, amount, lpTokens, amount2, ePrice, tradingFee", + "why_false_positive": "else if ((flags & tfOneAssetLPToken) != 0u) { ... } validates tfOneAssetLPToken, amount, lpTokens, amount2, ePrice, tradingFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfLimitLPToken, amount, ePrice, lpTokens, amount2, tradingFee", + "empty", + "string", + "validation" + ], + "evidence": "else if ((flags & tfLimitLPToken) != 0u) { ... } at AMMDeposit::preflight", + "issue_pattern": "Missing empty string validation for tfLimitLPToken, amount, ePrice, lpTokens, amount2, tradingFee", + "why_false_positive": "else if ((flags & tfLimitLPToken) != 0u) { ... } validates tfLimitLPToken, amount, ePrice, lpTokens, amount2, tradingFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfTwoAssetIfEmpty, amount, amount2, ePrice, lpTokens", + "empty", + "string", + "validation" + ], + "evidence": "else if ((flags & tfTwoAssetIfEmpty) != 0u) { ... } at AMMDeposit::preflight", + "issue_pattern": "Missing empty string validation for tfTwoAssetIfEmpty, amount, amount2, ePrice, lpTokens", + "why_false_positive": "else if ((flags & tfTwoAssetIfEmpty) != 0u) { ... } validates tfTwoAssetIfEmpty, amount, amount2, ePrice, lpTokens for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "asset, asset2", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAssetPair(asset, asset2) at AMMDeposit::preflight", + "issue_pattern": "Missing empty string validation for asset, asset2", + "why_false_positive": "invalidAMMAssetPair(asset, asset2) validates asset, asset2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "AMM feature enabled", + "empty", + "string", + "validation" + ], + "evidence": "ammEnabled(ctx.rules) at AMMDeposit::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for AMM feature enabled", + "why_false_positive": "ammEnabled(ctx.rules) validates AMM feature enabled for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "MPTokens V2 feature and asset/amount types", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled(featureMPTokensV2) || ... at AMMDeposit::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for MPTokens V2 feature and asset/amount types", + "why_false_positive": "ctx.rules.enabled(featureMPTokensV2) || ... validates MPTokens V2 feature and asset/amount types for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 11, + "name": "AMMDeposit::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 22, + "name": "AMMDeposit::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 26, + "name": "AMMDeposit::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 99, + "name": "AMMDeposit::preclaim" + }, + { + "args": [ + "Sandbox& sb" + ], + "lineno": 210, + "name": "AMMDeposit::applyGuts" + }, + { + "args": [], + "lineno": 270, + "name": "AMMDeposit::doApply" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amountDeposit", + "std::optional const& amount2Deposit", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokensDeposit", + "std::optional const& depositMin", + "std::optional const& deposit2Min", + "std::optional const& lpTokensDepositMin", + "std::uint16_t tfee" + ], + "lineno": 292, + "name": "AMMDeposit::deposit" + }, + { + "args": [ + "Rules const& rules", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokensDeposit" + ], + "lineno": 347, + "name": "adjustLPTokensOut" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokensDeposit", + "std::optional const& depositMin", + "std::optional const& deposit2Min", + "std::uint16_t tfee" + ], + "lineno": 359, + "name": "AMMDeposit::equalDepositTokens" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "STAmount const& amount2", + "std::optional const& lpTokensDepositMin", + "std::uint16_t tfee" + ], + "lineno": 399, + "name": "AMMDeposit::equalDepositLimit" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "std::optional const& lpTokensDepositMin", + "std::uint16_t tfee" + ], + "lineno": 453, + "name": "AMMDeposit::singleDeposit" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokensDeposit", + "std::uint16_t tfee" + ], + "lineno": 484, + "name": "AMMDeposit::singleDepositTokens" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount", + "STAmount const& lptAMMBalance", + "STAmount const& ePrice", + "std::uint16_t tfee" + ], + "lineno": 510, + "name": "AMMDeposit::singleDepositEPrice" + }, + { + "args": [ + "Sandbox& view", + "AccountID const& ammAccount", + "STAmount const& amount", + "STAmount const& amount2", + "Asset const& lptIssue", + "std::uint16_t tfee" + ], + "lineno": 589, + "name": "AMMDeposit::equalDepositInEmptyState" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "AMMDeposit validation is typically covered in unit/integration tests under the transaction processing suite. Look for files like 'AMMDeposit_test.cpp', 'AMM_test.cpp', or 'Transactor_test.cpp' in the rippled or xrpl repo. These should test flag combinations, malformed transactions, and edge cases (e.g., missing/extra fields, invalid asset pairs, negative/zero amounts). Gaps may exist for rare flag combinations, feature toggles (e.g., MPTokens), or error propagation paths. Ensure tests cover all branches in the flag validation logic and all error returns in preflight.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in business logic (no external framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "flags (tfDepositSubTx)", + "location": "AMMDeposit::preflight", + "validated_by": "std::popcount(flags & tfDepositSubTx) != 1", + "validates": [ + "Exactly one tfDepositSubTx flag must be set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "tfLPToken, lpTokens, ePrice, amount, amount2, tradingFee", + "location": "AMMDeposit::preflight", + "validated_by": "if ((flags & tfLPToken) != 0u) { ... }", + "validates": [ + "If tfLPToken is set, lpTokens must be present", + "ePrice must not be present", + "If amount is present, amount2 must also be present (and vice versa)", + "tradingFee must not be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "tfSingleAsset, amount, amount2, lpTokens, ePrice, tradingFee", + "location": "AMMDeposit::preflight", + "validated_by": "else if ((flags & tfSingleAsset) != 0u) { ... }", + "validates": [ + "If tfSingleAsset is set, amount must be present", + "amount2, ePrice, tradingFee must not be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "tfTwoAsset, amount, amount2, lpTokens, ePrice, tradingFee", + "location": "AMMDeposit::preflight", + "validated_by": "else if ((flags & tfTwoAsset) != 0u) { ... }", + "validates": [ + "If tfTwoAsset is set, amount and amount2 must be present", + "ePrice, tradingFee must not be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "tfOneAssetLPToken, amount, lpTokens, amount2, ePrice, tradingFee", + "location": "AMMDeposit::preflight", + "validated_by": "else if ((flags & tfOneAssetLPToken) != 0u) { ... }", + "validates": [ + "If tfOneAssetLPToken is set, amount and lpTokens must be present", + "amount2, ePrice, tradingFee must not be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "tfLimitLPToken, amount, ePrice, lpTokens, amount2, tradingFee", + "location": "AMMDeposit::preflight", + "validated_by": "else if ((flags & tfLimitLPToken) != 0u) { ... }", + "validates": [ + "If tfLimitLPToken is set, amount and ePrice must be present", + "lpTokens, amount2, tradingFee must not be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "tfTwoAssetIfEmpty, amount, amount2, ePrice, lpTokens", + "location": "AMMDeposit::preflight", + "validated_by": "else if ((flags & tfTwoAssetIfEmpty) != 0u) { ... }", + "validates": [ + "If tfTwoAssetIfEmpty is set, amount and amount2 must be present", + "ePrice, lpTokens must not be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "result of invalidAMMAssetPair (likely temMALFORMED or similar)", + "field": "asset, asset2", + "location": "AMMDeposit::preflight", + "validated_by": "invalidAMMAssetPair(asset, asset2)", + "validates": [ + "Asset pair must be valid for AMM" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "returns false (caller may throw or reject)", + "field": "AMM feature enabled", + "location": "AMMDeposit::checkExtraFeatures", + "validated_by": "ammEnabled(ctx.rules)", + "validates": [ + "AMM feature must be enabled in rules" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "returns false (caller may throw or reject)", + "field": "MPTokens V2 feature and asset/amount types", + "location": "AMMDeposit::checkExtraFeatures", + "validated_by": "ctx.rules.enabled(featureMPTokensV2) || ...", + "validates": [ + "If MPTokensV2 is not enabled, asset, asset2, amount, amount2 must not hold MPTIssue" + ], + "validation_type": "business_logic|type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp.ai.md b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp.ai.md new file mode 100644 index 0000000000..a31861febd --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp.ai.md @@ -0,0 +1,58 @@ +# AMMDeposit.cpp + +`AMMDeposit` implements the `AMMDeposit` transaction for the XRP Ledger's on-chain Automated Market Maker (AMM), specified in [XLS-30d](https://github.com/XRPLF/XRPL-Standards/discussions/78). Its purpose is to allow a liquidity provider (LP) to deposit one or both assets into an existing AMM pool and receive LP tokens representing their proportional ownership of the pool. The file lives alongside the other DEX transactors (`AMMWithdraw`, `AMMBid`, `AMMVote`, etc.) under `src/libxrpl/tx/transactors/dex/`. + +## Deposit Modes and Flag Dispatch + +The transaction carries exactly one sub-transaction flag drawn from `tfDepositSubTx`. The `preflight` function enforces this with `std::popcount(flags & tfDepositSubTx) != 1`, returning `temMALFORMED` for anything other than exactly one bit set. Each flag defines a distinct deposit strategy with a strictly prescribed set of required and forbidden fields: + +| Flag | Required | Forbidden | +|---|---|---| +| `tfLPToken` | `sfLPTokenOut`; `sfAmount`/`sfAmount2` must appear together or not at all | `sfEPrice`, `sfTradingFee` | +| `tfSingleAsset` | `sfAmount` | `sfAmount2`, `sfEPrice`, `sfTradingFee` | +| `tfTwoAsset` | `sfAmount`, `sfAmount2` | `sfEPrice`, `sfTradingFee` | +| `tfOneAssetLPToken` | `sfAmount`, `sfLPTokenOut` | `sfAmount2`, `sfEPrice`, `sfTradingFee` | +| `tfLimitLPToken` | `sfAmount`, `sfEPrice` | `sfLPTokenOut`, `sfAmount2`, `sfTradingFee` | +| `tfTwoAssetIfEmpty` | `sfAmount`, `sfAmount2` | `sfEPrice`, `sfLPTokenOut` | + +The `tfTwoAssetIfEmpty` flag is the only mode that allows `sfTradingFee` (optional) and is the only path that seeds a brand-new, empty pool. Every other mode requires a live pool (`lptAMMBalance > 0`), and `preclaim` enforces the asymmetry: `tfTwoAssetIfEmpty` demands `lptAMMBalance == 0`, while all others fail with `tecAMM_EMPTY` if the pool holds no LP tokens. + +## Validation Pipeline + +`checkExtraFeatures` is the earliest gate. It checks that the AMM amendment is enabled via `ammEnabled(ctx.rules)` and that if `featureMPTokensV2` is not live, neither the asset pair descriptors (`sfAsset`, `sfAsset2`) nor the deposit amounts hold an `MPTIssue`. This prevents MPT-denominated pool participation until the feature is activated, with no impact on IOU/XRP pools. + +`preflight` is fully stateless and performs three additional checks after the flag/field consistency tests: `invalidAMMAssetPair` ensures the two pool assets differ and form a valid pair; amounts whose `asset()` are identical are rejected with `temBAD_AMM_TOKENS`; and `lpTokens`, if present, must be strictly positive. The `sfEPrice` field receives a special treatment under `featureMPTokensV2` — the asset pair constraint on effective-price is relaxed (set to `std::nullopt`) to allow MPT-valued effective prices. + +`preclaim` is where ledger state is first consulted. It reads the AMM `SLE` and calls `ammHolds` to retrieve current pool balances `(amountBalance, amount2Balance, lptAMMBalance)`. The balance checks in this phase are advisory: for `tfLPToken` mode (where the actual deposit amounts are derived from the token quantity, not stated up front), only authorization and freeze checks run against the current pool balances, not against specific deposit amounts. For every other mode, a concrete balance check fires. The comment explicitly notes that these checks must be repeated inside `deposit()` because amounts may shift during calculation. + +The `featureAMMClawback` amendment adds an additional layer in `preclaim`: a `WeakAuth` `requireAuth` check on both pool assets and a full freeze check (`isFrozen`) against the LP account. The weak variant is used because the LP account may not hold an MPT object yet; the actual MPT object existence is deferred to the send operation. + +## Application: Sandbox and applyGuts + +`doApply` follows the canonical transactor pattern for state-mutating operations. It creates a `Sandbox` — a copy-on-write view layered over the committed ledger — and delegates to `applyGuts`. Only if `applyGuts` signals success does the sandbox commit its changes with `sb.apply(ctx_.rawView())`. This ensures partial-failure atomicity: a deposit that fails balance checks after amount calculation leaves the ledger unchanged. + +Inside `applyGuts`, the trading fee is resolved differently depending on pool state. For an empty pool (`lptAMMBalance == beast::zero`), the fee comes from `sfTradingFee` in the transaction itself; otherwise it is read from the AMM object via `getTradingFee`, which accounts for any vote-adjusted fee. After a successful deposit, the AMM `SLE`'s `sfLPTokenBalance` is updated in place. When seeding an empty pool, `initializeFeeAuctionVote` also initializes the auction slot and voting records. + +## Deposit Strategies + +Each mode delegates to a private calculation method, then calls the shared `deposit()` primitive. + +**`equalDepositTokens`** (`tfLPToken`): The LP specifies how many LP tokens they want. Both asset deposits are derived as `amount = balance × (tokensAdj / lptAMMBalance)`. The `sfAmount`/`sfAmount2` fields, if present, act as minimum thresholds rather than deposit amounts. No trading fee is charged because the deposit is perfectly proportional. + +**`equalDepositLimit`** (`tfTwoAsset`): The LP specifies maximums for both assets. The algorithm computes the required asset-2 deposit from the asset-1 limit; if that fits within the asset-2 maximum the transaction proceeds. If not, it inverts the calculation — computing required asset-1 from the asset-2 limit and checking against the asset-1 maximum. If neither direction satisfies both constraints simultaneously, the transaction fails with `tecAMM_FAILED`. No trading fee applies here either. + +**`singleDeposit`** (`tfSingleAsset`): Deposits one asset only; the pool absorbs the imbalance internally. LP tokens are calculated using `lpTokensOut`, which applies equation (3) from the XLS-30 spec. The trading fee is charged because single-asset deposit is mathematically equivalent to a swap followed by a proportional deposit. + +**`singleDepositTokens`** (`tfOneAssetLPToken`): The LP specifies how many LP tokens they want, backed by a single asset. `ammAssetIn` inverts the single-deposit formula (equation 4 from XLS-30) to derive the required asset deposit. If the computed deposit exceeds the LP's stated maximum (`sfAmount`), the transaction fails. + +**`singleDepositEPrice`** (`tfLimitLPToken`): The most mathematically complex path. An effective price cap `ePrice = assetIn / lpTokensOut` is enforced. If the natural trade at the stated `sfAmount` already satisfies the effective-price bound, that path is taken. Otherwise, a quadratic equation derived from the AMM invariant and effective-price definition is solved via `solveQuadraticEq` to find the exact asset input that hits the price limit exactly, and a second call to `getRoundedLPTokens` computes the resulting token output. + +**`equalDepositInEmptyState`** (`tfTwoAssetIfEmpty`): Seeds a new pool. LP tokens are minted as `sqrt(asset1 × asset2)` via `ammLPTokens`, with both asset amounts treated as both the deposit and the initial "balance" (since the pool is empty). The trading fee from `sfTradingFee` is stored on the AMM object. + +## The `deposit()` Primitive and LP Token Rounding + +All six strategies ultimately call the private `deposit()` method, which applies `adjustAmountsByLPTokens` to account for integer rounding of LP tokens. This adjustment is critical: because LP tokens are stored with finite precision, the actual issuable token count may be slightly less than computed, and the corresponding asset deposits must be scaled down proportionally. After adjustment, minimum threshold checks run against all three quantities (`depositMin`, `deposit2Min`, `lpTokensDepositMin`), failing with `tecAMM_FAILED` if any threshold is not met. + +The `fixAMMv1_3` amendment sharpens the rounding path via the file-local `adjustLPTokensOut` helper. Without the fix, a zero-token result after adjustment returned `tecAMM_FAILED`; with it, the more precise `tecAMM_INVALID_TOKENS` is returned so callers can distinguish rounding collapse from a genuine fee/price failure. + +Fund movement in `deposit()` is sequential: asset-1 is sent from the LP account to the AMM account first, then asset-2 if present, and finally LP tokens are issued from the AMM account to the LP. Transfer fees are waived (`WaiveTransferFee::Yes`) for both asset transfers because the AMM is a ledger-native construct, not a user-to-user payment. The XRP liquidity check accounts for the LP-token trustline reserve — a fresh depositor who does not yet hold an LP-token trustline needs an additional owner reserve, and `xrpLiquid` is called with `!sle` (the trustline flag) to factor that in. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMVote.cpp.ai.json b/src/libxrpl/tx/transactors/dex/AMMVote.cpp.ai.json new file mode 100644 index 0000000000..d8fc10e841 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMVote.cpp.ai.json @@ -0,0 +1,509 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AMMVote::doApply", + "AMMVote::preflight", + "AMMVote::preclaim", + "applyVote" + ], + "entry_point": "AMMVote::doApply", + "purpose": "Processes an AMM vote transaction, validating input and applying the vote if valid.", + "validation_points": [ + "AMMVote::preflight", + "AMMVote::preclaim" + ] + }, + { + "call_chain": [ + "AMMVote::preflight" + ], + "entry_point": "AMMVote::preflight", + "purpose": "Performs stateless validation of the transaction fields before execution.", + "validation_points": [ + "AMMVote::preflight" + ] + }, + { + "call_chain": [ + "AMMVote::preclaim" + ], + "entry_point": "AMMVote::preclaim", + "purpose": "Performs contextual validation using ledger state before transaction execution.", + "validation_points": [ + "AMMVote::preclaim" + ] + }, + { + "call_chain": [ + "AMMVote::checkExtraFeatures" + ], + "entry_point": "AMMVote::checkExtraFeatures", + "purpose": "Checks feature flags and asset types for AMM voting eligibility.", + "validation_points": [ + "AMMVote::checkExtraFeatures" + ] + } + ], + "data_flows": [ + { + "field": "sfAsset", + "flow": [ + "ctx.tx[sfAsset]", + "invalidAMMAssetPair", + "keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2])", + "ctx.view.read(...)" + ], + "origin": "ctx.tx[sfAsset] (transaction input)", + "transformations": [ + "Checked for validity as an asset pair", + "Used to locate AMM ledger entry" + ], + "validated_at": "AMMVote::preflight (invalidAMMAssetPair), AMMVote::preclaim (ledger lookup)" + }, + { + "field": "sfAsset2", + "flow": [ + "ctx.tx[sfAsset2]", + "invalidAMMAssetPair", + "keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2])", + "ctx.view.read(...)" + ], + "origin": "ctx.tx[sfAsset2] (transaction input)", + "transformations": [ + "Checked for validity as an asset pair", + "Used to locate AMM ledger entry" + ], + "validated_at": "AMMVote::preflight (invalidAMMAssetPair), AMMVote::preclaim (ledger lookup)" + }, + { + "field": "sfTradingFee", + "flow": [ + "ctx.tx[sfTradingFee]", + "AMMVote::preflight (comparison to TRADING_FEE_THRESHOLD)", + "applyVote (used as feeNew)" + ], + "origin": "ctx.tx[sfTradingFee] (transaction input)", + "transformations": [ + "Compared to TRADING_FEE_THRESHOLD for validity", + "Used to update vote entry if valid" + ], + "validated_at": "AMMVote::preflight" + }, + { + "field": "sfLPTokenBalance", + "flow": [ + "ammSle->getFieldAmount(sfLPTokenBalance)", + "AMMVote::preclaim (compared to beast::zero)" + ], + "origin": "ammSle->getFieldAmount(sfLPTokenBalance) (ledger state)", + "transformations": [ + "Checked for zero balance to ensure AMM is not empty" + ], + "validated_at": "AMMVote::preclaim" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j)", + "applyVote (used as account_)" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Checked for LP token holdings", + "Used to update or create vote entry" + ], + "validated_at": "AMMVote::preclaim (ammLPHolds)" + } + ], + "description": "Implements the AMMVote transaction logic for voting on trading fees in an Automated Market Maker (AMM) on the XRPL, including preflight, preclaim, and application of votes.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAsset", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAsset validated by Custom validation via business logic and protocol helpers (no external validation framework)", + "issue_pattern": "Missing validation for sfAsset", + "why_false_positive": "Custom validation via business logic and protocol helpers (no external validation framework) validates sfAsset automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAsset2", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAsset2 validated by Custom validation via business logic and protocol helpers (no external validation framework)", + "issue_pattern": "Missing validation for sfAsset2", + "why_false_positive": "Custom validation via business logic and protocol helpers (no external validation framework) validates sfAsset2 automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTradingFee", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTradingFee validated by Custom validation via business logic and protocol helpers (no external validation framework)", + "issue_pattern": "Missing validation for sfTradingFee", + "why_false_positive": "Custom validation via business logic and protocol helpers (no external validation framework) validates sfTradingFee automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccount validated by Custom validation via business logic and protocol helpers (no external validation framework)", + "issue_pattern": "Missing validation for sfAccount", + "why_false_positive": "Custom validation via business logic and protocol helpers (no external validation framework) validates sfAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLPTokenBalance", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLPTokenBalance validated by Custom validation via business logic and protocol helpers (no external validation framework)", + "issue_pattern": "Missing validation for sfLPTokenBalance", + "why_false_positive": "Custom validation via business logic and protocol helpers (no external validation framework) validates sfLPTokenBalance automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "sfAsset", + "sfAsset2" + ], + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAssetPair at AMMVote::preflight", + "issue_pattern": "Missing empty string validation for ['sfAsset', 'sfAsset2']", + "why_false_positive": "invalidAMMAssetPair validates ['sfAsset', 'sfAsset2'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTradingFee", + "empty", + "string", + "validation" + ], + "evidence": "comparison (ctx.tx[sfTradingFee] > TRADING_FEE_THRESHOLD) at AMMVote::preflight", + "issue_pattern": "Missing empty string validation for sfTradingFee", + "why_false_positive": "comparison (ctx.tx[sfTradingFee] > TRADING_FEE_THRESHOLD) validates sfTradingFee for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfTradingFee", + "range", + "bounds", + "validation" + ], + "evidence": "comparison (ctx.tx[sfTradingFee] > TRADING_FEE_THRESHOLD) at AMMVote::preflight", + "issue_pattern": "Missing range validation for sfTradingFee", + "why_false_positive": "comparison (ctx.tx[sfTradingFee] > TRADING_FEE_THRESHOLD) validates sfTradingFee range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "sfAsset", + "sfAsset2" + ], + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::amm(...)) at AMMVote::preclaim", + "issue_pattern": "Missing empty string validation for ['sfAsset', 'sfAsset2']", + "why_false_positive": "ctx.view.read(keylet::amm(...)) validates ['sfAsset', 'sfAsset2'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLPTokenBalance", + "empty", + "string", + "validation" + ], + "evidence": "ammSle->getFieldAmount(sfLPTokenBalance) == beast::zero at AMMVote::preclaim", + "issue_pattern": "Missing empty string validation for sfLPTokenBalance", + "why_false_positive": "ammSle->getFieldAmount(sfLPTokenBalance) == beast::zero validates sfLPTokenBalance for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "sfAccount", + "sfAsset", + "sfAsset2" + ], + "empty", + "string", + "validation" + ], + "evidence": "ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j) at AMMVote::preclaim", + "issue_pattern": "Missing empty string validation for ['sfAccount', 'sfAsset', 'sfAsset2']", + "why_false_positive": "ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j) validates ['sfAccount', 'sfAsset', 'sfAsset2'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "sfAsset", + "sfAsset2" + ], + "empty", + "string", + "validation" + ], + "evidence": "ammEnabled(ctx.rules) at AMMVote::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for ['sfAsset', 'sfAsset2']", + "why_false_positive": "ammEnabled(ctx.rules) validates ['sfAsset', 'sfAsset2'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "sfAsset", + "sfAsset2" + ], + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled(featureMPTokensV2) || (!ctx.tx[sfAsset].holds() && !ctx.tx[sfAsset2].holds()) at AMMVote::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for ['sfAsset', 'sfAsset2']", + "why_false_positive": "ctx.rules.enabled(featureMPTokensV2) || (!ctx.tx[sfAsset].holds() && !ctx.tx[sfAsset2].holds()) validates ['sfAsset', 'sfAsset2'] for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/dex/AMMVote.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 9, + "name": "AMMVote::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "AMMVote::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 32, + "name": "AMMVote::preclaim" + }, + { + "args": [ + "ApplyContext& ctx_", + "Sandbox& sb", + "AccountID const& account_", + "beast::Journal j_" + ], + "lineno": 48, + "name": "applyVote" + }, + { + "args": [], + "lineno": 143, + "name": "AMMVote::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "AMMVote logic is typically tested in integration/functional tests for AMM voting transactions. Look for test files such as 'AMMVote_test.cpp', 'AMM_test.cpp', or broader transaction/AMM suites in the rippled codebase. Tests should cover: invalid asset pairs, trading fee limits, empty AMM pools, non-LP accounts, and successful voting. Gaps may exist in edge cases (e.g., feature flag combinations, rare asset types, or malformed vote arrays). Direct unit tests for 'applyVote' may be limited, as it is often exercised via higher-level transaction tests.", + "validation_architecture": { + "auto_validated_fields": [ + "sfAsset", + "sfAsset2", + "sfTradingFee", + "sfAccount", + "sfLPTokenBalance" + ], + "framework": "Custom validation via business logic and protocol helpers (no external validation framework)", + "validation_layer": "business_logic (preflight/preclaim methods before transaction application)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "NotTEC (custom error code)", + "field": [ + "sfAsset", + "sfAsset2" + ], + "location": "AMMVote::preflight", + "validated_by": "invalidAMMAssetPair", + "validates": [ + "Checks if the asset pair is valid for AMM operations" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_FEE", + "field": "sfTradingFee", + "location": "AMMVote::preflight", + "validated_by": "comparison (ctx.tx[sfTradingFee] > TRADING_FEE_THRESHOLD)", + "validates": [ + "Trading fee must not exceed TRADING_FEE_THRESHOLD" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_AMM", + "field": [ + "sfAsset", + "sfAsset2" + ], + "location": "AMMVote::preclaim", + "validated_by": "ctx.view.read(keylet::amm(...))", + "validates": [ + "Checks if the AMM exists for the given asset pair" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecAMM_EMPTY", + "field": "sfLPTokenBalance", + "location": "AMMVote::preclaim", + "validated_by": "ammSle->getFieldAmount(sfLPTokenBalance) == beast::zero", + "validates": [ + "Checks if the AMM pool is empty (no LP tokens)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecAMM_INVALID_TOKENS", + "field": [ + "sfAccount", + "sfAsset", + "sfAsset2" + ], + "location": "AMMVote::preclaim", + "validated_by": "ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j)", + "validates": [ + "Checks if the account holds any LP tokens for the AMM" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": [ + "sfAsset", + "sfAsset2" + ], + "location": "AMMVote::checkExtraFeatures", + "validated_by": "ammEnabled(ctx.rules)", + "validates": [ + "Checks if AMM feature is enabled in the current ruleset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": [ + "sfAsset", + "sfAsset2" + ], + "location": "AMMVote::checkExtraFeatures", + "validated_by": "ctx.rules.enabled(featureMPTokensV2) || (!ctx.tx[sfAsset].holds() && !ctx.tx[sfAsset2].holds())", + "validates": [ + "If featureMPTokensV2 is not enabled, ensures neither asset is an MPTIssue" + ], + "validation_type": "business_logic|type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMVote.cpp.ai.md b/src/libxrpl/tx/transactors/dex/AMMVote.cpp.ai.md new file mode 100644 index 0000000000..58f91043b9 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMVote.cpp.ai.md @@ -0,0 +1,49 @@ +# `AMMVote.cpp` — AMM Trading Fee Governance Transactor + +This file implements the `AMMVote` transactor, which allows liquidity providers (LPs) to participate in on-chain governance of an AMM pool's trading fee. It is one of the DEX-specific transactors in `src/libxrpl/tx/transactors/dex/` and follows the standard XRPL three-phase transaction pipeline: feature-gate check, stateless preflight validation, stateful preclaim validation, and mutation application. + +## Transaction Pipeline + +**`checkExtraFeatures()`** is the first gate. It enforces that the base AMM amendment is active via `ammEnabled()`, and also blocks `AMMVote` transactions on MPT-backed pools unless `featureMPTokensV2` is separately enabled. This two-layer gating pattern appears across AMM transactors: the base feature enables the DEX, while newer token type support requires its own amendment. + +**`preflight()`** performs stateless validation before any ledger reads. It calls `invalidAMMAssetPair()` to reject malformed or identical asset pairs, and enforces the `TRADING_FEE_THRESHOLD` ceiling of 1000 basis points (1%). A fee of exactly zero is legal — an LP can vote for a free pool. + +**`preclaim()`** performs three ledger-state checks in sequence: the AMM must exist for the specified asset pair (`terNO_AMM`), the pool's total `sfLPTokenBalance` must not be zero (`tecAMM_EMPTY`), and the submitting account must hold a non-zero balance of LP tokens (`tecAMM_INVALID_TOKENS`). The third check is critical to the governance model: only current LPs may vote, preventing external actors from influencing fee dynamics. + +## Vote Application Logic + +The real work lives in the file-scope static function `applyVote()`. Isolating it from the `Transactor` class hierarchy is deliberate — it keeps the logic testable independently and avoids bloating the virtual dispatch surface. + +### Vote Slot Maintenance + +The AMM ledger entry (`sfVoteSlots`) holds up to `VOTE_MAX_SLOTS` (8) vote entries. Each entry records the voter's `AccountID`, their proposed `sfTradingFee`, and a computed `sfVoteWeight`. On every vote execution, the function iterates all existing entries and re-evaluates each voter's current LP balance via `ammLPHolds()`. Entries where the account no longer holds LP tokens are silently dropped rather than explicitly removed — they are simply not pushed into `updatedVoteSlots`. This passive eviction keeps the slot array clean without requiring a separate cleanup transaction. + +For the submitting account, if an existing entry is found (`foundAccount = true`), its fee value is updated in place to `feeNew` and its token balance is refreshed to `lpTokensNew`. The iteration simultaneously accumulates running numerator (`num`) and denominator (`den`) sums for the weighted-average fee calculation. + +### Slot Eviction Policy + +When the submitting account is new (no existing entry) and fewer than eight slots are occupied, a new entry is appended unconditionally. When all slots are full, the function must decide whether to evict the weakest current voter. The eviction criterion is: the newcomer must hold **more** LP tokens than the least-token holder, or hold equal tokens and propose a **higher** fee. If neither condition holds, no eviction occurs and the voter is not recorded — but the transaction still succeeds. This design choice is significant: it treats the fee recalculation (which still happens over stale balances) as valuable even when no new vote can be inserted. + +The minimum-slot detection is made deterministic through a three-level comparison: fewest LP tokens first, then lowest fee, then lowest `AccountID`. This lexicographic ordering ensures all validators reach the same eviction decision regardless of iteration order differences. + +### Weighted Fee Recalculation + +After building `updatedVoteSlots`, the effective fee is computed as `num / den` using the XRPL `Number` type (arbitrary precision rational arithmetic). The result is cast to `std::int64_t` to truncate to an integer basis-point value. If the result is non-zero, it is written to `sfTradingFee` on the AMM SLE. If it rounds to zero, the field is explicitly made absent with `makeFieldAbsent()` rather than being set to zero. This matters because absent fields serialize differently than present-but-zero fields in the XRPL's canonical binary format. + +### Auction Slot Side Effect + +Each fee update has a cascading effect on the AMM's auction slot. The `sfDiscountedFee` field inside `sfAuctionSlot` is recalculated as `fee / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION` (dividing by 10, so the discounted rate is one-tenth of the new trading fee). If the division yields zero, `sfDiscountedFee` is cleared. This tight coupling means every successful vote touches three conceptually distinct areas of the AMM SLE: the vote array, the pool-wide trading fee, and the auction slot's discount rate. + +An `XRPL_ASSERT` guards the structural invariant that `sfAuctionSlot` is present when the `fixInnerObjTemplate` amendment is active. This acts as a consensus-critical sanity check: if the AMM's SLE is ever malformed, the assert fires loudly during validation rather than silently corrupting the auction fee. + +## Sandbox Isolation + +`doApply()` wraps all mutations in a `Sandbox` constructed over the current apply view. Only on `tesSUCCESS` (the second element of `applyVote()`'s return pair) does it call `sb.apply()` to flush changes into the real ledger view. This two-phase commit pattern is standard across XRPL transactors and ensures that any failure path inside `applyVote()` — including the `tecINTERNAL` guard on the AMM SLE peek — leaves the ledger completely unmodified. + +## Key Constants + +From `AMMCore.h`, the constants governing this transactor are: +- `VOTE_MAX_SLOTS = 8` — maximum concurrent vote entries per AMM +- `VOTE_WEIGHT_SCALE_FACTOR = 100000` — vote weight is expressed in units of 1/100,000 of total LP supply +- `TRADING_FEE_THRESHOLD = 1000` — maximum votable fee (1%, since fees are in units of 1/100,000) +- `AUCTION_SLOT_DISCOUNTED_FEE_FRACTION = 10` — auction slot discount is always one-tenth of the current trading fee \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp.ai.json b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp.ai.json new file mode 100644 index 0000000000..df1c37b634 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp.ai.json @@ -0,0 +1,769 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AMMWithdraw::doApply", + "AMMWithdraw::applyGuts", + "AMMWithdraw::withdraw", + "tokensWithdraw" + ], + "entry_point": "AMMWithdraw::doApply", + "purpose": "Executes the AMMWithdraw transaction, applying business logic after validation.", + "validation_points": [ + "AMMWithdraw::preflight", + "AMMWithdraw::preclaim" + ] + }, + { + "call_chain": [ + "AMMWithdraw::preflight" + ], + "entry_point": "AMMWithdraw::preflight", + "purpose": "Performs all static and syntactic validation on the transaction before execution.", + "validation_points": [ + "AMMWithdraw::preflight" + ] + }, + { + "call_chain": [ + "AMMWithdraw::preclaim" + ], + "entry_point": "AMMWithdraw::preclaim", + "purpose": "Performs contextual validation (e.g., ledger state) before transaction application.", + "validation_points": [ + "AMMWithdraw::preclaim" + ] + } + ], + "data_flows": [ + { + "field": "flags", + "flow": [ + "Transaction input (ctx.tx)", + "AMMWithdraw::preflight", + "Bitmask checks for flag combinations", + "Controls which validation branch is taken" + ], + "origin": "ctx.tx.getFlags()", + "transformations": [ + "Bitmasking with tfWithdrawSubTx and other flags", + "std::popcount to ensure only one sub-transaction flag is set" + ], + "validated_at": "AMMWithdraw::preflight" + }, + { + "field": "lpTokens", + "flow": [ + "Transaction input (ctx.tx)", + "AMMWithdraw::preflight", + "Checked for presence/absence depending on flags", + "Used in downstream logic if validation passes" + ], + "origin": "ctx.tx[~sfLPTokenIn]", + "transformations": [ + "Optional extraction", + "Compared to beast::zero for validity" + ], + "validated_at": "AMMWithdraw::preflight" + }, + { + "field": "amount", + "flow": [ + "Transaction input (ctx.tx)", + "AMMWithdraw::preflight", + "Checked for presence/absence depending on flags", + "Passed to invalidAMMAmount for further validation" + ], + "origin": "ctx.tx[~sfAmount]", + "transformations": [ + "Optional extraction", + "Type/issue checks", + "Passed to invalidAMMAmount" + ], + "validated_at": "AMMWithdraw::preflight" + }, + { + "field": "amount2", + "flow": [ + "Transaction input (ctx.tx)", + "AMMWithdraw::preflight", + "Checked for presence/absence depending on flags", + "Passed to invalidAMMAmount for further validation" + ], + "origin": "ctx.tx[~sfAmount2]", + "transformations": [ + "Optional extraction", + "Type/issue checks", + "Passed to invalidAMMAmount" + ], + "validated_at": "AMMWithdraw::preflight" + }, + { + "field": "ePrice", + "flow": [ + "Transaction input (ctx.tx)", + "AMMWithdraw::preflight", + "Checked for presence/absence depending on flags", + "Passed to invalidAMMAmount for further validation" + ], + "origin": "ctx.tx[~sfEPrice]", + "transformations": [ + "Optional extraction", + "Type/issue checks", + "Passed to invalidAMMAmount" + ], + "validated_at": "AMMWithdraw::preflight" + }, + { + "field": "asset", + "flow": [ + "Transaction input (ctx.tx)", + "AMMWithdraw::preflight", + "Passed to invalidAMMAssetPair for validation" + ], + "origin": "ctx.tx[sfAsset]", + "transformations": [ + "Type/issue checks" + ], + "validated_at": "AMMWithdraw::preflight" + }, + { + "field": "asset2", + "flow": [ + "Transaction input (ctx.tx)", + "AMMWithdraw::preflight", + "Passed to invalidAMMAssetPair for validation" + ], + "origin": "ctx.tx[sfAsset2]", + "transformations": [ + "Type/issue checks" + ], + "validated_at": "AMMWithdraw::preflight" + } + ], + "description": "Implements the logic for Automated Market Maker (AMM) Withdraw transactions in the XRPL, including preflight checks, withdrawal calculations, and ledger updates for various withdrawal scenarios.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "flags (tfWithdrawSubTx)", + "empty", + "string", + "validation" + ], + "evidence": "std::popcount(flags & tfWithdrawSubTx) != 1 at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for flags (tfWithdrawSubTx)", + "why_false_positive": "std::popcount(flags & tfWithdrawSubTx) != 1 validates flags (tfWithdrawSubTx) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lpTokens, amount, amount2, ePrice (when tfLPToken set)", + "empty", + "string", + "validation" + ], + "evidence": "if (!lpTokens || amount || amount2 || ePrice) at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for lpTokens, amount, amount2, ePrice (when tfLPToken set)", + "why_false_positive": "if (!lpTokens || amount || amount2 || ePrice) validates lpTokens, amount, amount2, ePrice (when tfLPToken set) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lpTokens, amount, amount2, ePrice (when tfWithdrawAll set)", + "empty", + "string", + "validation" + ], + "evidence": "if (lpTokens || amount || amount2 || ePrice) at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for lpTokens, amount, amount2, ePrice (when tfWithdrawAll set)", + "why_false_positive": "if (lpTokens || amount || amount2 || ePrice) validates lpTokens, amount, amount2, ePrice (when tfWithdrawAll set) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount, lpTokens, amount2, ePrice (when tfOneAssetWithdrawAll set)", + "empty", + "string", + "validation" + ], + "evidence": "if (!amount || lpTokens || amount2 || ePrice) at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for amount, lpTokens, amount2, ePrice (when tfOneAssetWithdrawAll set)", + "why_false_positive": "if (!amount || lpTokens || amount2 || ePrice) validates amount, lpTokens, amount2, ePrice (when tfOneAssetWithdrawAll set) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount, lpTokens, amount2, ePrice (when tfSingleAsset set)", + "empty", + "string", + "validation" + ], + "evidence": "if (!amount || lpTokens || amount2 || ePrice) at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for amount, lpTokens, amount2, ePrice (when tfSingleAsset set)", + "why_false_positive": "if (!amount || lpTokens || amount2 || ePrice) validates amount, lpTokens, amount2, ePrice (when tfSingleAsset set) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount, amount2, lpTokens, ePrice (when tfTwoAsset set)", + "empty", + "string", + "validation" + ], + "evidence": "if (!amount || !amount2 || lpTokens || ePrice) at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for amount, amount2, lpTokens, ePrice (when tfTwoAsset set)", + "why_false_positive": "if (!amount || !amount2 || lpTokens || ePrice) validates amount, amount2, lpTokens, ePrice (when tfTwoAsset set) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount, lpTokens, amount2, ePrice (when tfOneAssetLPToken set)", + "empty", + "string", + "validation" + ], + "evidence": "if (!amount || !lpTokens || amount2 || ePrice) at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for amount, lpTokens, amount2, ePrice (when tfOneAssetLPToken set)", + "why_false_positive": "if (!amount || !lpTokens || amount2 || ePrice) validates amount, lpTokens, amount2, ePrice (when tfOneAssetLPToken set) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount, ePrice, lpTokens, amount2 (when tfLimitLPToken set)", + "empty", + "string", + "validation" + ], + "evidence": "if (!amount || !ePrice || lpTokens || amount2) at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for amount, ePrice, lpTokens, amount2 (when tfLimitLPToken set)", + "why_false_positive": "if (!amount || !ePrice || lpTokens || amount2) validates amount, ePrice, lpTokens, amount2 (when tfLimitLPToken set) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "asset, asset2", + "empty", + "string", + "validation" + ], + "evidence": "invalidAMMAssetPair(asset, asset2) at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for asset, asset2", + "why_false_positive": "invalidAMMAssetPair(asset, asset2) validates asset, asset2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount, amount2 (must not be same asset)", + "empty", + "string", + "validation" + ], + "evidence": "if (amount && amount2 && amount->asset() == amount2->asset()) at AMMWithdraw::preflight", + "issue_pattern": "Missing empty string validation for amount, amount2 (must not be same asset)", + "why_false_positive": "if (amount && amount2 && amount->asset() == amount2->asset()) validates amount, amount2 (must not be same asset) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "AMM feature enabled", + "empty", + "string", + "validation" + ], + "evidence": "ammEnabled(ctx.rules) at AMMWithdraw::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for AMM feature enabled", + "why_false_positive": "ammEnabled(ctx.rules) validates AMM feature enabled for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "MPTokens V2 feature or asset types", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled(featureMPTokensV2) || ... at AMMWithdraw::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for MPTokens V2 feature or asset types", + "why_false_positive": "ctx.rules.enabled(featureMPTokensV2) || ... validates MPTokens V2 feature or asset types for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 9, + "name": "AMMWithdraw::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 22, + "name": "AMMWithdraw::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 26, + "name": "AMMWithdraw::preflight" + }, + { + "args": [ + "STAmount const& lpTokens", + "std::optional const& tokensIn", + "std::uint32_t flags" + ], + "lineno": 74, + "name": "tokensWithdraw" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 82, + "name": "AMMWithdraw::preclaim" + }, + { + "args": [ + "Sandbox& sb" + ], + "lineno": 148, + "name": "AMMWithdraw::applyGuts" + }, + { + "args": [], + "lineno": 210, + "name": "AMMWithdraw::doApply" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amountWithdraw", + "std::optional const& amount2Withdraw", + "STAmount const& lpTokensAMMBalance", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee" + ], + "lineno": 225, + "name": "AMMWithdraw::withdraw" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "AccountID const& account", + "STAmount const& amountBalance", + "STAmount const& amountWithdraw", + "std::optional const& amount2Withdraw", + "STAmount const& lpTokensAMMBalance", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee", + "FreezeHandling freezeHandling", + "AuthHandling authHandling", + "WithdrawAll withdrawAll", + "XRPAmount const& priorBalance", + "beast::Journal const& journal" + ], + "lineno": 239, + "name": "AMMWithdraw::withdraw" + }, + { + "args": [ + "Rules const& rules", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokensWithdraw", + "WithdrawAll withdrawAll" + ], + "lineno": 357, + "name": "adjustLPTokensIn" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokens", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee" + ], + "lineno": 370, + "name": "AMMWithdraw::equalWithdrawTokens" + }, + { + "args": [ + "Sandbox& sb", + "std::shared_ptr const ammSle", + "STAmount const& lpTokenBalance", + "Asset const& asset1", + "Asset const& asset2", + "beast::Journal const& journal" + ], + "lineno": 384, + "name": "AMMWithdraw::deleteAMMAccountIfEmpty" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const account", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& lpTokens", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee", + "FreezeHandling freezeHandling", + "AuthHandling authHandling", + "WithdrawAll withdrawAll", + "XRPAmount const& priorBalance", + "beast::Journal const& journal" + ], + "lineno": 403, + "name": "AMMWithdraw::equalWithdrawTokens" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& amount2Balance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "STAmount const& amount2", + "std::uint16_t tfee" + ], + "lineno": 453, + "name": "AMMWithdraw::equalWithdrawLimit" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "std::uint16_t tfee" + ], + "lineno": 502, + "name": "AMMWithdraw::singleWithdraw" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "STAmount const& lpTokensWithdraw", + "std::uint16_t tfee" + ], + "lineno": 534, + "name": "AMMWithdraw::singleWithdrawTokens" + }, + { + "args": [ + "Sandbox& view", + "SLE const& ammSle", + "AccountID const& ammAccount", + "STAmount const& amountBalance", + "STAmount const& lptAMMBalance", + "STAmount const& amount", + "STAmount const& ePrice", + "std::uint16_t tfee" + ], + "lineno": 561, + "name": "AMMWithdraw::singleWithdrawEPrice" + }, + { + "args": [ + "STTx const& tx" + ], + "lineno": 609, + "name": "AMMWithdraw::isWithdrawAll" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "AMMWithdraw validation is typically covered by unit and integration tests in the rippled codebase, especially in files like 'test/AMM_test.cpp', 'test/AMMWithdraw_test.cpp', or similar. These tests should cover valid and invalid flag combinations, presence/absence of required fields, and edge cases for amounts and tokens. However, gaps may exist for rare flag combinations, malformed asset pairs, or negative/zero token values. Additional fuzz or property-based tests could help ensure all validation branches are exercised.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in business logic (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "flags (tfWithdrawSubTx)", + "location": "AMMWithdraw::preflight", + "validated_by": "std::popcount(flags & tfWithdrawSubTx) != 1", + "validates": [ + "Exactly one withdraw sub-transaction flag must be set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "lpTokens, amount, amount2, ePrice (when tfLPToken set)", + "location": "AMMWithdraw::preflight", + "validated_by": "if (!lpTokens || amount || amount2 || ePrice)", + "validates": [ + "lpTokens must be present", + "amount, amount2, ePrice must NOT be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "lpTokens, amount, amount2, ePrice (when tfWithdrawAll set)", + "location": "AMMWithdraw::preflight", + "validated_by": "if (lpTokens || amount || amount2 || ePrice)", + "validates": [ + "lpTokens, amount, amount2, ePrice must NOT be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "amount, lpTokens, amount2, ePrice (when tfOneAssetWithdrawAll set)", + "location": "AMMWithdraw::preflight", + "validated_by": "if (!amount || lpTokens || amount2 || ePrice)", + "validates": [ + "amount must be present", + "lpTokens, amount2, ePrice must NOT be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "amount, lpTokens, amount2, ePrice (when tfSingleAsset set)", + "location": "AMMWithdraw::preflight", + "validated_by": "if (!amount || lpTokens || amount2 || ePrice)", + "validates": [ + "amount must be present", + "lpTokens, amount2, ePrice must NOT be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "amount, amount2, lpTokens, ePrice (when tfTwoAsset set)", + "location": "AMMWithdraw::preflight", + "validated_by": "if (!amount || !amount2 || lpTokens || ePrice)", + "validates": [ + "amount and amount2 must be present", + "lpTokens, ePrice must NOT be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "amount, lpTokens, amount2, ePrice (when tfOneAssetLPToken set)", + "location": "AMMWithdraw::preflight", + "validated_by": "if (!amount || !lpTokens || amount2 || ePrice)", + "validates": [ + "amount and lpTokens must be present", + "amount2, ePrice must NOT be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "amount, ePrice, lpTokens, amount2 (when tfLimitLPToken set)", + "location": "AMMWithdraw::preflight", + "validated_by": "if (!amount || !ePrice || lpTokens || amount2)", + "validates": [ + "amount and ePrice must be present", + "lpTokens, amount2 must NOT be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "result of invalidAMMAssetPair (NotTEC error code)", + "field": "asset, asset2", + "location": "AMMWithdraw::preflight", + "validated_by": "invalidAMMAssetPair(asset, asset2)", + "validates": [ + "asset and asset2 must form a valid AMM asset pair" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "amount, amount2 (must not be same asset)", + "location": "AMMWithdraw::preflight", + "validated_by": "if (amount && amount2 && amount->asset() == amount2->asset())", + "validates": [ + "amount and amount2 must not refer to the same asset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": "AMM feature enabled", + "location": "AMMWithdraw::checkExtraFeatures", + "validated_by": "ammEnabled(ctx.rules)", + "validates": [ + "AMM feature must be enabled in rules" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": "MPTokens V2 feature or asset types", + "location": "AMMWithdraw::checkExtraFeatures", + "validated_by": "ctx.rules.enabled(featureMPTokensV2) || ...", + "validates": [ + "If MPTokensV2 not enabled, asset, asset2, amount, amount2 must not be MPTIssue" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp.ai.md b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp.ai.md new file mode 100644 index 0000000000..8be6bc9f24 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp.ai.md @@ -0,0 +1,73 @@ +# `AMMWithdraw.cpp` — AMM Liquidity Withdrawal Transactor + +## Role in the System + +`AMMWithdraw.cpp` implements the XRPL AMM Withdraw transaction, one of the core DEX operations defined by [XLS-30d](https://github.com/XRPLF/XRPL-Standards/discussions/78). Its purpose is to redeem LP tokens held by a liquidity provider in exchange for a proportional share of the AMM pool's reserves. The file lives in `src/libxrpl/tx/transactors/dex/` alongside `AMMDeposit.cpp`, `AMMBid.cpp`, `AMMVote.cpp`, and `AMMDelete.cpp` — each a self-contained transactor. `AMMWithdraw` is the mirror of `AMMDeposit`; where the deposit mints LP tokens, the withdrawal burns them. + +## Withdrawal Modes + +The transaction specification defines seven distinct withdrawal sub-types, each controlled by an exclusive flag bit from `tfWithdrawSubTx`. `preflight()` enforces exclusivity with `std::popcount(flags & tfWithdrawSubTx) != 1`, guaranteeing exactly one mode is selected. Each mode has a strict combination of required and forbidden fields: + +| Flag | Required fields | Forbidden fields | Fee charged? | +|---|---|---|---| +| `tfLPToken` | `sfLPTokenIn` | amount, amount2, ePrice | No | +| `tfWithdrawAll` | — (none) | all optional fields | No | +| `tfOneAssetWithdrawAll` | `sfAmount` (asset spec only) | lpTokens, amount2, ePrice | Yes | +| `tfSingleAsset` | `sfAmount` | lpTokens, amount2, ePrice | Yes | +| `tfTwoAsset` | `sfAmount`, `sfAmount2` | lpTokens, ePrice | No | +| `tfOneAssetLPToken` | `sfAmount`, `sfLPTokenIn` | amount2, ePrice | Yes | +| `tfLimitLPToken` | `sfAmount`, `sfEPrice` | lpTokens, amount2 | Yes | + +The flag-to-field matrix in `preflight()` is enforced with explicit boolean checks — the asymmetric use of `!field` vs `field` mirrors the "must have" vs "must not have" semantics clearly and cheaply. + +## Three-Phase Transaction Flow + +`AMMWithdraw` follows the standard XRPL transactor lifecycle: + +**`preflight()`** performs purely static validation against the transaction's own fields — flag consistency, asset pair validity via `invalidAMMAssetPair()`, same-asset check (`amount->asset() == amount2->asset()` returns `temBAD_AMM_TOKENS`), and per-field sanity via `invalidAMMAmount()`. It also runs `checkExtraFeatures()`, which gates the entire transaction on `ammEnabled(ctx.rules)` and blocks MPT-bearing fields unless `featureMPTokensV2` is active. No ledger reads occur here. + +**`preclaim()`** reads ledger state to validate the request against live pool composition. It fetches the AMM SLE via `keylet::amm()`, calls `ammHolds()` to get current balances (with freeze and auth handling both set to `fhIGNORE_FREEZE`/`ahIGNORE_AUTH` because this is a read-only check), verifies the pool is non-empty (`lptAMMBalance != beast::zero`), confirms the LP actually holds tokens, checks against over-withdrawal, and validates authorization and freeze status per-asset. The `checkAmount` lambda centralizes the per-asset checks for `sfAmount` and `sfAmount2` without duplicating the freeze/auth/MPT logic. + +**`doApply()`** wraps everything in a `Sandbox` — a copy-on-write overlay of the ledger view. All mutations happen inside `applyGuts()` against `sb`; if that returns a success result, `sb.apply(ctx_.rawView())` atomically commits the changes. If anything fails mid-withdrawal, the sandbox is simply discarded. + +## `applyGuts()` — Dispatch and AMM Lifecycle + +`applyGuts()` re-reads pool state from the sandbox (now with `fhZERO_IF_FROZEN`/`ahZERO_IF_UNAUTHORIZED` so frozen balances are treated as zero for calculation purposes), then dispatches to the appropriate calculation function based on `subTxType`. After calculation, it calls `deleteAMMAccountIfEmpty()` — if the new LP token balance is exactly zero, the entire AMM account is deleted from the ledger. This lifecycle management is critical: the AMM object must not persist with zero liquidity, as doing so would leave stale state and waste ledger object slots. + +The `fixAMMv1_1` amendment guard near the top of `applyGuts()` calls `verifyAndAdjustLPTokenBalance()` to handle a known rounding drift between the last LP's trustline balance and the AMM object's recorded `LPTokenBalance`. Without this correction, the final withdrawal could fail because the two don't match exactly. + +## Calculation Functions + +### Equal withdrawal (`equalWithdrawTokens`) + +The proportional mode — `tfLPToken` and `tfWithdrawAll` — computes asset amounts as `frac = tokensWithdrawn / totalLPTokens` and applies it to both pool balances: `amountWithdraw = amountBalance * frac`. The helper `adjustLPTokensIn()` applies `fixAMMv1_3`-gated rounding to the LP token count before computing the fraction. A critical guard fires if this rounding rounds either asset amount to zero: `return {tecAMM_FAILED, ...}`. This protects against the degenerate case where the withdrawal is too small to produce a non-zero asset amount due to fixed-point truncation, telling the user to increase the token amount rather than silently producing a one-sided withdrawal. + +When `lpTokensWithdraw == lptAMMBalance` (the last LP is withdrawing everything), the function short-circuits to `withdraw(..., WithdrawAll::Yes, ...)`, bypassing the rounding adjustments and using the raw pool balances. This is the correct behavior: the last LP must receive exactly what remains. + +### Dual-asset bounded withdrawal (`equalWithdrawLimit`) + +`tfTwoAsset` accepts maximums for both assets and uses the constant-product relationship to determine the actual amounts. It calculates `t` (LP tokens) from `amount / amountBalance`, derives the implied `amount2`, and checks if that fits within the user's stated maximum. If not, it pivots to use `amount2` as the binding constraint. The algorithm is documented inline with the formulae from the spec (equations 5 and 6). + +### Single-asset withdrawals (`singleWithdraw`, `singleWithdrawTokens`, `singleWithdrawEPrice`) + +All three charge a trading fee. `singleWithdraw` solves equation 7 for LP tokens given a desired asset output: `t = T * (c - sqrt(c² - 4R)) / 2` via the `lpTokensIn()` helper from `AMMHelpers`. `singleWithdrawTokens` is the inverse — given LP tokens, it computes asset output via `ammAssetOut()`. `singleWithdrawEPrice` solves the two-constraint problem algebraically, deriving the unique LP token amount where the effective price equals `ePrice`, with the derivation shown step-by-step in the comments. + +## Core `withdraw()` — Atomic State Mutation + +The static `withdraw()` overload is the only place actual ledger state changes. Its logic: + +1. **Rounding adjustment**: If `WithdrawAll::No`, calls `adjustAmountsByLPTokens()` to recompute actual withdrawal amounts from the rounded LP token count, ensuring the constant-product invariant holds after rounding. +2. **Invariant guards**: Checks that LP tokens don't exceed the caller's own balance, that the withdrawal doesn't drain exactly one side of the pool (which would violate `k = x * y`), and that consuming all LP tokens only happens when both pool balances are being fully drained. +3. **MPT pool state validity** (under `featureMPTokensV2`): after subtracting the withdrawn amounts, all three post-state values — balance1, balance2, LP tokens — must be simultaneously zero or simultaneously non-zero. +4. **Reserve check**: the `sufficientReserve` lambda (gated on `fixAMMv1_2`) verifies the receiving account has enough XRP to cover a new trustline or MPToken object before creating it, using `priorBalance` (the pre-fee XRP balance) to avoid penalizing accounts that are paying XRP fees. +5. **Transfers**: `accountSend()` moves asset1 (and optionally asset2) from the AMM account to the LP account with `WaiveTransferFee::Yes`, then `redeemIOU()` burns the LP tokens against the AMM's token issuance. + +The private instance-method `withdraw()` overload is a thin adapter that fills in instance-specific parameters (`account_`, `preFeeBalance_`, freeze handling) and discards the per-asset return values, returning only the new LP token balance. + +## Design Decisions Worth Noting + +The `WithdrawAll` scoped enum (not just a `bool`) makes the "drain everything" intent explicit across function signatures, preventing accidental pass-by-position mistakes when the state variable propagates through several layers. + +The `tokensWithdraw()` free function normalizes the "which tokens to redeem" question: for `tfWithdrawAll` and `tfOneAssetWithdrawAll`, the answer is always "all the LP tokens the account holds," regardless of what `sfLPTokenIn` says — because those flags don't even accept `sfLPTokenIn`. This normalization happens once in both `preclaim()` and `applyGuts()`, keeping the downstream functions free of flag-checking logic. + +The amendment guards (`fixAMMv1_1`, `fixAMMv1_2`, `fixAMMv1_3`) are not merely feature flags — each corrects a specific class of bugs in the original AMM math. `v1_1` addresses LP token trustline drift, `v1_2` adds reserve checking for the receiving account, and `v1_3` tightens rounding handling so zero-token results are `tecAMM_INVALID_TOKENS` rather than silent failures or `tecAMM_FAILED`. The old `tecAMM_FAILED` branches remain guarded under `!enabled(fixAMMv1_3)` to preserve behavior on pre-amendment ledgers. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/OfferCancel.cpp.ai.json b/src/libxrpl/tx/transactors/dex/OfferCancel.cpp.ai.json new file mode 100644 index 0000000000..a88030d856 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/OfferCancel.cpp.ai.json @@ -0,0 +1,237 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "OfferCancel::preflight" + ], + "entry_point": "OfferCancel::preflight", + "purpose": "Initial stateless validation of the OfferCancel transaction. Checks for required fields and basic constraints.", + "validation_points": [ + "ctx.tx[sfOfferSequence] == 0u (rejects if missing or zero)" + ] + }, + { + "call_chain": [ + "OfferCancel::preclaim" + ], + "entry_point": "OfferCancel::preclaim", + "purpose": "Stateful validation: checks that the account exists and that the offer sequence is valid relative to the account's sequence.", + "validation_points": [ + "ctx.view.read(keylet::account(id)) (validates sfAccount exists)", + "(*sle)[sfSequence] <= offerSequence (validates sfOfferSequence is less than account's sequence)" + ] + }, + { + "call_chain": [ + "OfferCancel::doApply", + "view().read(keylet::account(account_))", + "view().peek(keylet::offer(account_, offerSequence))", + "offerDelete(view(), sleOffer, ...)" + ], + "entry_point": "OfferCancel::doApply", + "purpose": "Applies the transaction: attempts to find and delete the offer if it exists.", + "validation_points": [ + "view().read(keylet::account(account_)) (checks account exists, but should always pass if preclaim succeeded)" + ] + } + ], + "data_flows": [ + { + "field": "sfOfferSequence", + "flow": [ + "ctx.tx[sfOfferSequence] in preflight", + "ctx.tx[sfOfferSequence] in preclaim", + "ctx_.tx[sfOfferSequence] in doApply" + ], + "origin": "ctx.tx[sfOfferSequence] (transaction input)", + "transformations": [ + "Checked for zero in preflight", + "Compared to account's sfSequence in preclaim", + "Used as offer key in doApply" + ], + "validated_at": "preflight (nonzero), preclaim (must be less than account's sfSequence)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount] in preclaim", + "keylet::account(id) in preclaim", + "view().read(keylet::account(account_)) in doApply" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Used to look up account in ledger" + ], + "validated_at": "preclaim (account must exist), doApply (should always exist if preclaim passed)" + }, + { + "field": "AccountRoot::sfSequence", + "flow": [ + "read from ledger in preclaim", + "compared to offerSequence" + ], + "origin": "(*sle)[sfSequence] (from ledger account object)", + "transformations": [ + "Compared to sfOfferSequence to ensure offerSequence is less than account's sequence" + ], + "validated_at": "preclaim" + } + ], + "description": "Implements the OfferCancel transaction logic for the XRPL DEX, including preflight, preclaim, and apply steps to cancel an offer based on a given sequence.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOfferSequence", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfOfferSequence] == 0u) at OfferCancel::preflight", + "issue_pattern": "Missing empty string validation for sfOfferSequence", + "why_false_positive": "explicit check (ctx.tx[sfOfferSequence] == 0u) validates sfOfferSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "ledger lookup (ctx.view.read(keylet::account(id))) at OfferCancel::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "ledger lookup (ctx.view.read(keylet::account(id))) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOfferSequence", + "empty", + "string", + "validation" + ], + "evidence": "comparison ((*sle)[sfSequence] <= offerSequence) at OfferCancel::preclaim", + "issue_pattern": "Missing empty string validation for sfOfferSequence", + "why_false_positive": "comparison ((*sle)[sfSequence] <= offerSequence) validates sfOfferSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "ledger lookup (view().read(keylet::account(account_))) at OfferCancel::doApply", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "ledger lookup (view().read(keylet::account(account_))) validates sfAccount for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/dex/OfferCancel.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 6, + "name": "OfferCancel::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 19, + "name": "OfferCancel::preclaim" + }, + { + "args": [], + "lineno": 38, + "name": "OfferCancel::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for OfferCancel would be in transaction-level integration tests, likely in files such as 'OfferCancel_test.cpp', 'Transactor_test.cpp', or broader transaction suite files. Tests should cover: missing sfOfferSequence, non-existent account, invalid offer sequence (too high), successful cancel, and cancel of non-existent offer. Gaps may exist if there are no explicit tests for malformed transactions (e.g., sfOfferSequence == 0), or for edge cases where the account is deleted between preclaim and doApply (should be unreachable). Code coverage tools (e.g., LCOV) indicate the tefINTERNAL branch is not covered, suggesting this error path is not tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in transaction processing (xrpl transaction engine)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temBAD_SEQUENCE", + "field": "sfOfferSequence", + "location": "OfferCancel::preflight", + "validated_by": "explicit check (ctx.tx[sfOfferSequence] == 0u)", + "validates": [ + "Checks that sfOfferSequence is not zero (must be a valid sequence number)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_ACCOUNT", + "field": "sfAccount", + "location": "OfferCancel::preclaim", + "validated_by": "ledger lookup (ctx.view.read(keylet::account(id)))", + "validates": [ + "Checks that the account exists in the ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_SEQUENCE", + "field": "sfOfferSequence", + "location": "OfferCancel::preclaim", + "validated_by": "comparison ((*sle)[sfSequence] <= offerSequence)", + "validates": [ + "Checks that the account's current sequence is greater than the offer sequence being cancelled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sfAccount", + "location": "OfferCancel::doApply", + "validated_by": "ledger lookup (view().read(keylet::account(account_)))", + "validates": [ + "Checks that the account exists in the ledger before applying the transaction" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/OfferCancel.cpp.ai.md b/src/libxrpl/tx/transactors/dex/OfferCancel.cpp.ai.md new file mode 100644 index 0000000000..8b2047eb82 --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/OfferCancel.cpp.ai.md @@ -0,0 +1,39 @@ +# `OfferCancel.cpp` — DEX Offer Cancellation Transactor + +## Role and Purpose + +`OfferCancel.cpp` implements the `OfferCancel` transaction type for the XRPL decentralized exchange (DEX). Its sole job is to remove an existing limit order (offer) that the submitting account previously placed on the order book. The file is deliberately minimal — fewer than 65 lines of logic — because the complexity of tearing down an offer is entirely encapsulated in the shared `offerDelete` helper. + +The class lives in the `dex/` subdirectory alongside `OfferCreate.cpp` and the AMM transactors, forming the full lifecycle of DEX participation. `OfferCancel` is the simplest of these; it reads one field from the transaction, finds the ledger object, and delegates destruction. + +## Three-Phase Transactor Pattern + +`OfferCancel` inherits from `Transactor` and participates in the standard three-phase validation/application pipeline that the XRPL transaction engine uses for every transaction type. + +**`preflight`** is a static, read-only, ledger-free check. It only verifies that `sfOfferSequence` is non-zero. A zero value is semantically meaningless (sequence numbers on XRPL start at 1), so returning `temBAD_SEQUENCE` here stops the transaction before any network consensus work is done. This check costs almost nothing and prevents a class of trivially malformed transactions. + +**`preclaim`** has access to a `ReadView` of the current ledger but must not modify it. It performs two checks: first, that the submitting account actually exists (`terNO_ACCOUNT` if not); second, that `sfOfferSequence` is strictly less than the account's current `sfSequence`. This second check is a forward-reference guard — an offer can only exist if the account has already submitted the transaction that created it, and each transaction increments the sequence. If `offerSequence >= account.sfSequence`, the offer could not have been created yet, making the cancel request either a mistake or an attempted attack, and `temBAD_SEQUENCE` is returned. The ledger is not searched for the specific offer here; that lookup is deferred to `doApply`. + +**`doApply`** performs the actual mutation. It re-reads the account (a defensive `tefINTERNAL` guard — marked `LCOV_EXCL_LINE` because it should be unreachable after `preclaim` — acknowledges the logical impossibility without skipping the null check entirely). It then calls `view().peek()` to get a mutable handle to the offer SLE via `keylet::offer(account_, offerSequence)`. If the offer is not found, the method returns `tesSUCCESS` without error. + +## Idempotent Success on Missing Offers + +The most deliberate non-obvious design choice is that canceling a non-existent offer is **not an error**. `doApply` logs a debug message and returns `tesSUCCESS` regardless. This matters for real-world reliability: an offer may have been consumed by a crossing trade between when a client submitted the cancel and when consensus executes it. Returning success in this case prevents the client from needing to distinguish between "cancel succeeded" and "offer was already gone," and avoids forcing a fee charge for an operation that became a no-op through normal trading activity. + +## `offerDelete` — The Bookkeeping Delegate + +The actual removal work is done by `offerDelete()` from `xrpl/ledger/helpers/OfferHelpers.h`. Reading its implementation reveals why it's a shared helper rather than inline logic: canceling an offer is structurally identical to expiring an offer during payment processing or deleting an offer during account deletion. The helper removes the offer SLE from three places: + +1. The account's owner directory (tracking objects toward the reserve requirement). +2. The primary order book directory identified by `sfBookDirectory`. +3. Any additional book directories if `sfAdditionalBooks` is present, which applies to hybrid domain offers (flagged `lsfHybrid`) that participate in multiple order books simultaneously. + +It then decrements the account's owner count via `adjustOwnerCount`, releasing the XRP reserve held against the offer. The fact that `offerDelete` gracefully handles a null SLE by returning success allows callers to be loose about whether the object exists before calling it — though `OfferCancel::doApply` already guards against this with its `peek` check. + +## `peek` vs `read` and the Mutability Contract + +Within `doApply`, the account is fetched with `view().read()` (returning a `shared_ptr`) because it is only inspected. The offer is fetched with `view().peek()` (returning a mutable `shared_ptr`) because `offerDelete` must erase it. This distinction is enforced by the `ApplyView` interface: passing a `const` SLE to `offerDelete` would fail to compile, making the mutability intent explicit at the call site. + +## `ConsequencesFactory` and Transaction Consequences + +`OfferCancel` declares `ConsequencesFactory{Normal}`, which means the engine treats it as a regular transaction: it may fail with a fee charged, but it does not block other transactions in a batch from executing. This contrasts with `Blocker`-typed transactions that would abort a batch if they fail. The Normal classification is correct since a failed cancel has no side effects on other account operations. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp.ai.json b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp.ai.json new file mode 100644 index 0000000000..a64b87cc3f --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp.ai.json @@ -0,0 +1,576 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "OfferCreate::preflight" + ], + "entry_point": "OfferCreate::preflight", + "purpose": "Performs all stateless validation on an OfferCreate transaction before it is accepted for further processing.", + "validation_points": [ + "OfferCreate::preflight" + ] + }, + { + "call_chain": [ + "Transactor::preflight", + "OfferCreate::preflight" + ], + "entry_point": "Transactor::preflight (base class, not shown here)", + "purpose": "The transaction engine calls the base Transactor::preflight, which then dispatches to the derived OfferCreate::preflight for OfferCreate transactions.", + "validation_points": [ + "OfferCreate::preflight" + ] + }, + { + "call_chain": [ + "OfferCreate::preclaim" + ], + "entry_point": "OfferCreate::preclaim", + "purpose": "Performs stateful validation (not shown in code snippet), but typically checks ledger state after stateless checks pass.", + "validation_points": [ + "OfferCreate::preclaim" + ] + }, + { + "call_chain": [ + "OfferCreate::checkExtraFeatures" + ], + "entry_point": "OfferCreate::checkExtraFeatures", + "purpose": "Checks for feature flags and field compatibility (e.g., sfDomainID with featurePermissionedDEX).", + "validation_points": [ + "OfferCreate::checkExtraFeatures" + ] + } + ], + "data_flows": [ + { + "field": "sfHybrid (tfHybrid flag)", + "flow": [ + "ctx.tx", + "tx.isFlag(tfHybrid)", + "if-check in preflight" + ], + "origin": "ctx.tx (STTx) input transaction", + "transformations": [ + "Checked for presence", + "Validated that sfDomainID is present if tfHybrid is set" + ], + "validated_at": "OfferCreate::preflight" + }, + { + "field": "sfDomainID", + "flow": [ + "ctx.tx", + "tx.isFieldPresent(sfDomainID)", + "if-check in preflight and checkExtraFeatures" + ], + "origin": "ctx.tx (STTx) input transaction", + "transformations": [ + "Checked for presence", + "Validated only allowed if featurePermissionedDEX is enabled" + ], + "validated_at": "OfferCreate::preflight, OfferCreate::checkExtraFeatures" + }, + { + "field": "tfImmediateOrCancel, tfFillOrKill", + "flow": [ + "ctx.tx", + "tx.getFlags()", + "bitmask checks in preflight" + ], + "origin": "ctx.tx.getFlags()", + "transformations": [ + "Bitmask extraction", + "Checked for mutual exclusivity" + ], + "validated_at": "OfferCreate::preflight" + }, + { + "field": "sfExpiration", + "flow": [ + "ctx.tx", + "tx.isFieldPresent(sfExpiration)", + "tx.getFieldU32(sfExpiration)", + "if-check in preflight" + ], + "origin": "ctx.tx (STTx) input transaction", + "transformations": [ + "Checked for presence", + "Checked for nonzero value" + ], + "validated_at": "OfferCreate::preflight" + }, + { + "field": "sfOfferSequence", + "flow": [ + "ctx.tx", + "tx[~sfOfferSequence]", + "if-check in preflight" + ], + "origin": "ctx.tx (STTx) input transaction", + "transformations": [ + "Checked for presence", + "Checked for nonzero value" + ], + "validated_at": "OfferCreate::preflight" + }, + { + "field": "sfTakerPays, sfTakerGets", + "flow": [ + "ctx.tx", + "tx[sfTakerPays], tx[sfTakerGets]", + "isLegalNet(saTakerPays), isLegalNet(saTakerGets)", + "further checks for native/IOU, amount > 0, issuer, asset" + ], + "origin": "ctx.tx (STTx) input transaction", + "transformations": [ + "Type-checked (native/IOU)", + "Amount checked (positive, nonzero)", + "Issuer and asset checked for validity", + "Redundancy checks (XRP for XRP, IOU for IOU)" + ], + "validated_at": "OfferCreate::preflight" + } + ], + "description": "Implements the OfferCreate transaction logic for the XRPL decentralized exchange, including preflight checks, offer crossing, hybrid/domain offers, and ledger state updates.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfHybrid flag and sfDomainID field", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at OfferCreate::preflight", + "issue_pattern": "Missing empty string validation for tfHybrid flag and sfDomainID field", + "why_false_positive": "explicit if-check validates tfHybrid flag and sfDomainID field for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfImmediateOrCancel and tfFillOrKill flags", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at OfferCreate::preflight", + "issue_pattern": "Missing empty string validation for tfImmediateOrCancel and tfFillOrKill flags", + "why_false_positive": "explicit if-check validates tfImmediateOrCancel and tfFillOrKill flags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfExpiration", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at OfferCreate::preflight", + "issue_pattern": "Missing empty string validation for sfExpiration", + "why_false_positive": "explicit if-check validates sfExpiration for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfExpiration", + "range", + "bounds", + "validation" + ], + "evidence": "explicit if-check at OfferCreate::preflight", + "issue_pattern": "Missing range validation for sfExpiration", + "why_false_positive": "explicit if-check validates sfExpiration range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOfferSequence", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at OfferCreate::preflight", + "issue_pattern": "Missing empty string validation for sfOfferSequence", + "why_false_positive": "explicit if-check validates sfOfferSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfOfferSequence", + "range", + "bounds", + "validation" + ], + "evidence": "explicit if-check at OfferCreate::preflight", + "issue_pattern": "Missing range validation for sfOfferSequence", + "why_false_positive": "explicit if-check validates sfOfferSequence range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTakerPays and sfTakerGets", + "empty", + "string", + "validation" + ], + "evidence": "isLegalNet function at OfferCreate::preflight", + "issue_pattern": "Missing empty string validation for sfTakerPays and sfTakerGets", + "why_false_positive": "isLegalNet function validates sfTakerPays and sfTakerGets for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfTakerPays and sfTakerGets (both native)", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at OfferCreate::preflight", + "issue_pattern": "Missing empty string validation for sfTakerPays and sfTakerGets (both native)", + "why_false_positive": "explicit if-check validates sfTakerPays and sfTakerGets (both native) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID and featurePermissionedDEX", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at OfferCreate::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfDomainID and featurePermissionedDEX", + "why_false_positive": "explicit if-check validates sfDomainID and featurePermissionedDEX for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTakerPays and sfTakerGets (MPTIssue type)", + "empty", + "string", + "validation" + ], + "evidence": "holds() type check at OfferCreate::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfTakerPays and sfTakerGets (MPTIssue type)", + "why_false_positive": "holds() type check validates sfTakerPays and sfTakerGets (MPTIssue type) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfTakerPays and sfTakerGets (MPTIssue type)", + "type", + "validation", + "check" + ], + "evidence": "holds() type check at OfferCreate::checkExtraFeatures", + "issue_pattern": "Missing type validation for sfTakerPays and sfTakerGets (MPTIssue type)", + "why_false_positive": "holds() type check validates sfTakerPays and sfTakerGets (MPTIssue type) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/dex/OfferCreate.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "OfferCreate::makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 20, + "name": "OfferCreate::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 28, + "name": "OfferCreate::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 37, + "name": "OfferCreate::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 97, + "name": "OfferCreate::preclaim" + }, + { + "args": [ + "ReadView const& view", + "ApplyFlags const flags", + "AccountID const id", + "beast::Journal const j", + "Asset const& asset" + ], + "lineno": 154, + "name": "OfferCreate::checkAcceptAsset" + }, + { + "args": [ + "PaymentSandbox& psb", + "PaymentSandbox& psbCancel", + "Amounts const& takerAmount", + "std::optional const& domainID" + ], + "lineno": 210, + "name": "OfferCreate::flowCross" + }, + { + "args": [ + "STAmount const& amount" + ], + "lineno": 340, + "name": "OfferCreate::format_amount" + }, + { + "args": [ + "Sandbox& sb", + "std::shared_ptr sleOffer", + "Keylet const& offerKey", + "STAmount const& saTakerPays", + "STAmount const& saTakerGets", + "std::function)> const& setDir" + ], + "lineno": 349, + "name": "OfferCreate::applyHybrid" + }, + { + "args": [ + "Sandbox& sb", + "Sandbox& sbCancel" + ], + "lineno": 374, + "name": "OfferCreate::applyGuts" + }, + { + "args": [], + "lineno": 590, + "name": "OfferCreate::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "OfferCreate validation is typically covered by unit/integration tests in the rippled codebase, especially in files like test/tx/Offer_test.cpp, test/tx/OfferCreate_test.cpp, and possibly test/tx/HybridDEX_test.cpp or test/tx/PermissionedDEX_test.cpp. These tests cover flag combinations, malformed offers, bad amounts, and feature-flag interactions. However, new fields like sfDomainID and tfHybrid may not be fully covered if recently added; tests for featurePermissionedDEX and hybrid offers should be checked for completeness. Edge cases for issuer/asset validation and bad currency codes should also be verified in test coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in OfferCreate::preflight and OfferCreate::checkExtraFeatures", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "tfHybrid flag and sfDomainID field", + "location": "OfferCreate::preflight", + "validated_by": "explicit if-check", + "validates": [ + "If tfHybrid flag is set, sfDomainID must be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "tfImmediateOrCancel and tfFillOrKill flags", + "location": "OfferCreate::preflight", + "validated_by": "explicit if-check", + "validates": [ + "Both tfImmediateOrCancel and tfFillOrKill cannot be set at the same time" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_EXPIRATION", + "field": "sfExpiration", + "location": "OfferCreate::preflight", + "validated_by": "explicit if-check", + "validates": [ + "If sfExpiration is present, it must not be zero" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_SEQUENCE", + "field": "sfOfferSequence", + "location": "OfferCreate::preflight", + "validated_by": "explicit if-check", + "validates": [ + "If sfOfferSequence is present, it must not be zero" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfTakerPays and sfTakerGets", + "location": "OfferCreate::preflight", + "validated_by": "isLegalNet function", + "validates": [ + "Both sfTakerPays and sfTakerGets must be legal network amounts" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "not shown in snippet, but likely temBAD_AMOUNT or similar", + "field": "sfTakerPays and sfTakerGets (both native)", + "location": "OfferCreate::preflight", + "validated_by": "explicit if-check", + "validates": [ + "Both sfTakerPays and sfTakerGets cannot be native (XRP) at the same time" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (likely leads to error elsewhere)", + "field": "sfDomainID and featurePermissionedDEX", + "location": "OfferCreate::checkExtraFeatures", + "validated_by": "explicit if-check", + "validates": [ + "If sfDomainID is present, featurePermissionedDEX must be enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (likely leads to error elsewhere)", + "field": "sfTakerPays and sfTakerGets (MPTIssue type)", + "location": "OfferCreate::checkExtraFeatures", + "validated_by": "holds() type check", + "validates": [ + "If featureMPTokensV2 is not enabled, neither sfTakerPays nor sfTakerGets can hold MPTIssue" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/dex/OfferCreate.cpp.ai.md b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp.ai.md new file mode 100644 index 0000000000..564f6f17ff --- /dev/null +++ b/src/libxrpl/tx/transactors/dex/OfferCreate.cpp.ai.md @@ -0,0 +1,59 @@ +# `OfferCreate.cpp` — DEX Offer Creation Transactor + +## Role and Context + +This file implements the `OfferCreate` transactor, the core engine behind XRPL's decentralized exchange. When an account submits an `OfferCreate` transaction it is trying to exchange one asset for another at a stated price. This code handles everything from stateless input validation through order-book crossing to placing any residual offer onto the ledger. It sits in the `dex/` transactor sub-directory alongside the AMM family of transactors and `OfferCancel`, and it inherits from `Transactor` — the CRTP-adjacent base that supplies `account_`, `ctx_`, `j_`, and `preFeeBalance_`. + +## Three-Phase Execution Pipeline + +The XRPL transaction engine drives all transactors through three phases, each a distinct static or virtual method. + +**`preflight`** performs stateless, read-free validation. All checks here are cheap and can be done before the ledger view is locked. The checks enforce: `tfHybrid` implies `sfDomainID` must be present (else `temINVALID_FLAG`); `tfImmediateOrCancel` and `tfFillOrKill` are mutually exclusive; `sfExpiration`, if present, must be non-zero; `sfOfferSequence`, if present (for concurrent cancel), must be non-zero; both `sfTakerPays` and `sfTakerGets` must pass `isLegalNet`; neither can be zero or negative; they cannot both be XRP (no native-to-native offer); they cannot reference the same asset (no redundant IOU-for-IOU); and neither can use the reserved bad-currency sentinel. This exhaustive upfront validation avoids touching any state for clearly malformed inputs. + +**`checkExtraFeatures`** is called by the base class to gate optional fields behind their governing feature flags: `sfDomainID` is rejected unless `featurePermissionedDEX` is enabled; either amount holding an `MPTIssue` type is rejected unless `featureMPTokensV2` is enabled. The dynamic flags mask returned by `getFlagsMask` similarly adds `tfHybrid` to the prohibited-flag set when `featurePermissionedDEX` is off — preventing hybrid offers before the feature is live. + +**`preclaim`** reads ledger state to catch conditions that require an account or object lookup. It verifies: the submitting account exists; neither asset is globally frozen; the account has sufficient funds to partially cover `sfTakerGets` (with a special carve-out for MPT issuers whose `OutstandingAmount ≥ MaximumAmount`); the cancellation sequence, if present, is less than the account's current sequence (preventing cancellation of not-yet-issued offers); the transaction itself has not already expired; if `sfTakerPays` is non-native, `checkAcceptAsset` confirms the submitter is authorized to receive that asset; and if `sfDomainID` is present, the submitter must already be a member of that permissioned domain. + +## `checkAcceptAsset` — Authorization and Freeze Gating + +This static helper determines whether account `id` may legally hold what it would receive. The logic branches on asset type. For an `Issue`, if the issuer has `lsfRequireAuth` set, it reads the trust line and checks the appropriate authorization bit (using canonical `id > issuer` ordering to pick the correct `lsfLowAuth`/`lsfHighAuth` bit). It also checks for `lsfLowDeepFreeze | lsfHighDeepFreeze` — a newer deep-freeze mechanic where either side of a trust line can prohibit token movement. For `MPTIssue`, it calls `requireAuth` in `WeakAuth` mode, which intentionally skips requiring a pre-existing `MPToken` object because one will be lazily created if the account has the right authorization. + +The `tapRETRY` flag governs whether failures return soft `ter` codes (retryable) or hard `tec` codes (fee-consuming). This distinction matters for validation-retry paths in the server. + +## `doApply` and the Dual-Sandbox Pattern + +`doApply` is the only virtual method — it materialises ledger changes. It creates two `Sandbox` objects wrapping the live ledger view: `sb` accumulates all changes including the crossed amounts and any new offer placed on books; `sbCancel` accumulates only fee payments and the removal of expired/unfunded offers encountered during crossing. The pair is threaded through `applyGuts` and then through `flowCross`. If `applyGuts` returns `result.second == true`, `sb` is committed; otherwise (for `tfFillOrKill` orders that weren't satisfied) `sbCancel` is committed instead, ensuring that stale offers found during the failed attempt are still cleaned up, while no trades and no new order are recorded. + +## `applyGuts` — The Main State Machine + +`applyGuts` orchestrates the full apply lifecycle: + +1. **Cancel prior offer.** If `sfOfferSequence` is present, `offerDelete` removes the named offer from both sandboxes immediately, before any crossing. + +2. **Expiry check.** The offer's own `sfExpiration` is re-checked against the current ledger close time. An already-expired offer returns `{tecEXPIRED, true}`, committing the `sb` that includes any cancellation work already done. + +3. **Tick-size rounding.** Both the payer's and getter's issuers are checked for `sfTickSize`. The tightest tick size governs. For a sell offer, `saTakerPays` is rounded; for a buy offer, `saTakerGets` is rounded. Either rounding to zero short-circuits with success (order "rounded away"). This step runs before crossing so that the quality stored in order books is the rounded quality. + +4. **Offer crossing via `flowCross`.** The amounts are inverted — the offer placer is treated as a taker to leverage the payment engine — and fed to `flowCross`. The `PaymentSandbox` child views created here are applied back into `sb` and `sbCancel` afterward. + +5. **Post-cross remainder adjustment.** If the cross was partial, the unfilled amounts are recomputed. For sell mode, the unfilled input is reduced by `actualAmountIn` (net of gateway transfer rate), and output is derived from the preserved `Quality`. For buy mode, the unfilled output is reduced and input is scaled up to maintain quality. Either side going negative triggers a zero clamp with an assertion — the assert is deliberately kept alongside the clamp because this condition should be impossible, but a rounding edge case demands defensive handling. + +6. **FillOrKill / ImmediateOrCancel short-circuits.** After crossing, if `tfFillOrKill` is set and the offer is unfilled, `{tecKILLED, false}` commits only `sbCancel`. For `tfImmediateOrCancel`, an unfilled result returns `{tecKILLED, false}`; a partially or fully filled result returns `{tesSUCCESS, true}`. + +7. **Reserve check.** The account's pre-fee balance (`preFeeBalance_` from the base class) is compared against the reserve for `ownerCount + 1`. If insufficient, the transaction succeeds only if some crossing actually occurred (the `crossed` flag) — otherwise it returns `tecINSUF_RESERVE_OFFER`. This design allows an offer to cross even if the owner cannot afford to store any remainder. + +8. **Book placement.** The offer object is written into its owner directory and order book. The book key is `keylet::quality(keylet::book(book), uRate)` where `uRate` is the original pre-crossing rate — intentionally preserving the submitted price for queue ordering even when partial crossing changed the remaining amounts. If the book did not exist before, `OrderBookDB` is notified. + +## `flowCross` — Delegating to the Payment Engine + +Rather than maintaining a separate matching loop, `OfferCreate` delegates crossing entirely to `flow()` from the payment paths engine. This unifies the quality-matching, transfer-rate accounting, and multi-hop XRP bridging logic with the code path used by `Payment` transactions. + +Key details: if neither leg is XRP, `flowCross` injects an additional path through XRP as an intermediate, enabling two non-XRP assets to cross through a shared XRP order book. For `tfSell` mode, the deliver limit is set to `cMaxNative` or `cMaxValue/2` (IOU) or `maxMPTokenAmount/2` (MPT) — capped at half maximum to accommodate potential 200% gateway transfer rates. The gateway transfer rate is computed upfront and folded into `sendMax` so the payment engine's threshold comparison is accurate. Stale or expired offers surfaced during the crossing (`result.removableOffers`) are deleted from both sandboxes. A `try/catch` wraps the entire call; any uncaught exception logs and returns `tecINTERNAL` rather than propagating. + +## Hybrid Offers — Dual Book Placement + +A hybrid offer (`tfHybrid`) lives in both a permissioned domain order book and the open order book simultaneously. `applyHybrid` handles the second placement: it asserts `sfDomainID` is present, sets `lsfHybrid` on the offer SLE, computes an open-book `Book` (no domain ID), and calls `sb.dirAppend` with a `setBookDir` callback that deliberately passes `std::nullopt` for the domain so the open book's directory object has no `sfDomainID` field. The two book entries are linked by storing `sfBookDirectory` and `sfBookNode` of the open directory inside an `sfAdditionalBooks` array on the offer SLE. This allows the crossing engine to find the offer from either side of the market. + +## `makeTxConsequences` — Fee Reserve Estimation + +The custom `TxConsequences` factory declares the maximum possible XRP spend as the `sfTakerGets` amount when it is native, or zero otherwise. This tells the transaction queue the worst-case XRP impact of the offer, enabling safe parallel scheduling across queued transactions. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/did/DIDDelete.cpp.ai.json b/src/libxrpl/tx/transactors/did/DIDDelete.cpp.ai.json new file mode 100644 index 0000000000..f555a358fb --- /dev/null +++ b/src/libxrpl/tx/transactors/did/DIDDelete.cpp.ai.json @@ -0,0 +1,300 @@ +{ + "args": [ + { + "lineno": 8, + "name": "ctx" + }, + { + "lineno": 13, + "name": "ctx" + }, + { + "lineno": 13, + "name": "sleKeylet" + }, + { + "lineno": 13, + "name": "owner" + }, + { + "lineno": 21, + "name": "view" + }, + { + "lineno": 21, + "name": "sle" + }, + { + "lineno": 21, + "name": "owner" + }, + { + "lineno": 21, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "DIDDelete::doApply", + "DIDDelete::deleteSLE(ApplyContext&, Keylet, AccountID)", + "DIDDelete::deleteSLE(ApplyView&, std::shared_ptr, AccountID, beast::Journal)" + ], + "entry_point": "DIDDelete::doApply", + "purpose": "Deletes a DID ledger entry for a given account, ensuring all references and ownerships are properly removed.", + "validation_points": [ + "DIDDelete::deleteSLE(ApplyContext&, Keylet, AccountID): Validates existence of SLE via ctx.view().peek(sleKeylet)", + "DIDDelete::deleteSLE(ApplyView&, std::shared_ptr, AccountID, beast::Journal): Validates owner directory entry via view.dirRemove(...)", + "DIDDelete::deleteSLE(ApplyView&, std::shared_ptr, AccountID, beast::Journal): Validates owner account root via view.peek(keylet::account(owner))" + ] + }, + { + "call_chain": [ + "DIDDelete::preflight" + ], + "entry_point": "DIDDelete::preflight", + "purpose": "Preflight check for the transaction. Currently always returns success (no validation).", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "sleKeylet", + "flow": [ + "DIDDelete::doApply (calls keylet::did(account_))", + "DIDDelete::deleteSLE(ApplyContext&, Keylet, AccountID) (receives sleKeylet)", + "ctx.view().peek(sleKeylet) (fetches SLE)" + ], + "origin": "Constructed from keylet::did(account_) in doApply", + "transformations": [ + "Used as a key to fetch the SLE from the ledger view" + ], + "validated_at": "DIDDelete::deleteSLE(ApplyContext&, Keylet, AccountID): if (!sle) return tecNO_ENTRY" + }, + { + "field": "owner directory entry", + "flow": [ + "DIDDelete::deleteSLE(ApplyView&, std::shared_ptr, AccountID, beast::Journal)", + "view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true)" + ], + "origin": "keylet::ownerDir(owner)", + "transformations": [ + "Attempts to remove the SLE from the owner's directory" + ], + "validated_at": "DIDDelete::deleteSLE(ApplyView&, ...): if (!view.dirRemove(...)) return tefBAD_LEDGER" + }, + { + "field": "owner account root", + "flow": [ + "DIDDelete::deleteSLE(ApplyView&, ...)", + "view.peek(keylet::account(owner))" + ], + "origin": "keylet::account(owner)", + "transformations": [ + "Fetched from ledger to adjust owner count" + ], + "validated_at": "DIDDelete::deleteSLE(ApplyView&, ...): if (!sleOwner) return tecINTERNAL" + }, + { + "field": "owner count", + "flow": [ + "DIDDelete::deleteSLE(ApplyView&, ...)", + "adjustOwnerCount(view, sleOwner, -1, j)", + "view.update(sleOwner)" + ], + "origin": "(*sleOwner)[sfOwnerCount]", + "transformations": [ + "Decremented by 1 to reflect removal of DID" + ], + "validated_at": "Implicitly validated by presence of sleOwner" + } + ], + "description": "Implements the logic for deleting a Decentralized Identifier (DID) object from the XRPL ledger, including preflight checks and ledger entry removal.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sleKeylet (ledger entry key)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view().peek(sleKeylet) at DIDDelete::deleteSLE(ApplyContext&, Keylet, AccountID const)", + "issue_pattern": "Missing empty string validation for sleKeylet (ledger entry key)", + "why_false_positive": "ctx.view().peek(sleKeylet) validates sleKeylet (ledger entry key) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "owner directory entry", + "empty", + "string", + "validation" + ], + "evidence": "view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true) at DIDDelete::deleteSLE(ApplyView&, std::shared_ptr, AccountID const, beast::Journal)", + "issue_pattern": "Missing empty string validation for owner directory entry", + "why_false_positive": "view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true) validates owner directory entry for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "owner account root", + "empty", + "string", + "validation" + ], + "evidence": "view.peek(keylet::account(owner)) at DIDDelete::deleteSLE(ApplyView&, std::shared_ptr, AccountID const, beast::Journal)", + "issue_pattern": "Missing empty string validation for owner account root", + "why_false_positive": "view.peek(keylet::account(owner)) validates owner account root for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/did/DIDDelete.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "DIDDelete::preflight" + }, + { + "args": [ + "ApplyContext& ctx", + "Keylet sleKeylet", + "AccountID const owner" + ], + "lineno": 13, + "name": "DIDDelete::deleteSLE" + }, + { + "args": [ + "ApplyView& view", + "std::shared_ptr sle", + "AccountID const owner", + "beast::Journal j" + ], + "lineno": 21, + "name": "DIDDelete::deleteSLE" + }, + { + "args": [], + "lineno": 44, + "name": "DIDDelete::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage (no test code or comments). Typical test files would be in the unit test suite for the DID feature, likely under 'test/tx/did' or similar. The code has LCOV_EXCL annotations, indicating some error paths are not covered by tests (e.g., fatal errors in dirRemove, missing owner account root). The preflight function is a stub and not tested for validation logic. Main validation paths (missing SLE, failed dirRemove, missing owner root) may not be fully covered by tests, especially for rare/failure cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom logic (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sleKeylet (ledger entry key)", + "location": "DIDDelete::deleteSLE(ApplyContext&, Keylet, AccountID const)", + "validated_by": "ctx.view().peek(sleKeylet)", + "validates": [ + "Checks if the ledger entry for the given Keylet exists before attempting deletion" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "owner directory entry", + "location": "DIDDelete::deleteSLE(ApplyView&, std::shared_ptr, AccountID const, beast::Journal)", + "validated_by": "view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true)", + "validates": [ + "Checks if the DID object can be removed from the owner's directory" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "owner account root", + "location": "DIDDelete::deleteSLE(ApplyView&, std::shared_ptr, AccountID const, beast::Journal)", + "validated_by": "view.peek(keylet::account(owner))", + "validates": [ + "Checks if the owner's AccountRoot exists before adjusting owner count" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/did/DIDDelete.cpp.ai.md b/src/libxrpl/tx/transactors/did/DIDDelete.cpp.ai.md new file mode 100644 index 0000000000..10c86cde10 --- /dev/null +++ b/src/libxrpl/tx/transactors/did/DIDDelete.cpp.ai.md @@ -0,0 +1,41 @@ +# `DIDDelete.cpp` — DID Ledger Entry Removal Transactor + +## Role in the System + +`DIDDelete.cpp` implements the `DIDDelete` transaction type, which removes a Decentralized Identifier (DID) object from the XRP Ledger. DIDs are W3C-standardized self-sovereign identity constructs, and the XRPL stores them as first-class ledger entries (`ltDID`) associated with an account. When an account no longer wants a DID, this transactor handles the coordinated teardown: unlinking the entry from the owner directory, adjusting the reserve counter, and erasing the SLE itself. + +## Class Structure and Inheritance + +`DIDDelete` inherits from `Transactor`, the standard base class for all XRPL transaction types. The constructor takes an `ApplyContext&` and simply forwards it, while `ConsequencesFactory` is set to `Normal`, indicating no special fee or sequence-number consequences beyond the baseline. The three public entry points the framework calls are `preflight`, and the virtual `doApply`. + +## The No-Op `preflight` + +`preflight` unconditionally returns `tesSUCCESS`. This is intentional: a `DIDDelete` transaction carries no payload fields that require field-level validation at preflight time. The only thing being deleted is the DID SLE identified by the submitting account's key. Any error conditions (the DID not existing, the owner directory being corrupted) can only be checked against the current ledger state in `doApply`, not statically in preflight. + +This contrasts sharply with `DIDSet::preflight`, which validates that at least one of `sfURI`, `sfDIDDocument`, or `sfData` is present and within length bounds. The asymmetry reflects the inherent difference between a creation/update transaction (where the submitted fields must be well-formed) and a pure deletion (where there is nothing to validate ahead of time). + +## The Two-Tier `deleteSLE` Design + +The most architecturally significant choice in this file is exposing `deleteSLE` as a pair of `static` overloads rather than baking deletion logic directly into `doApply`. + +The first overload accepts `ApplyContext& ctx`, a `Keylet`, and an `AccountID`. It is a thin adapter: it resolves the keylet to a concrete `SLE` via `ctx.view().peek()`, returns `tecNO_ENTRY` if the entry does not exist, then delegates to the second overload. + +The second overload accepts `ApplyView& view`, the resolved `std::shared_ptr`, the owner's `AccountID`, and a `beast::Journal`. This is where the actual mutation logic lives. By accepting a raw `ApplyView&` instead of the full `ApplyContext&`, this overload can be called from any context that has a writable ledger view — including other transactors that may need to delete a DID as a side-effect of some larger operation, without going through the full transactor machinery. + +`doApply` itself is three lines: construct the DID keylet via `keylet::did(account_)`, then call the first `deleteSLE` overload with that keylet and the submitting account's ID. + +## The Deletion Sequence + +Inside the lower-level `deleteSLE(ApplyView&, ...)`, deletion follows a strict three-step sequence that mirrors the inverse of how `DIDSet`'s `addSLE` helper creates entries: + +1. **Owner directory removal**: `view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true)` unlinks the DID from the account's owner directory page. The `sfOwnerNode` field stored on the SLE itself contains the exact directory page number, making this a direct O(1) lookup rather than a scan. If `dirRemove` fails, the code returns `tefBAD_LEDGER` — a fatal-class error indicating ledger state corruption, not a user error. This path is annotated `LCOV_EXCL` because it should be unreachable under correct operation. + +2. **Owner count adjustment**: `adjustOwnerCount(view, sleOwner, -1, j)` decrements the `sfOwnerCount` field on the account root by one. Owner count governs the XRP reserve requirement; failing to decrement it would permanently inflate the account's reserve even after the DID is gone. The account root is fetched fresh via `view.peek(keylet::account(owner))`; a missing account root returns `tecINTERNAL` (also `LCOV_EXCL` — an account that submitted a transaction cannot have a missing root). After adjustment, `view.update(sleOwner)` marks the account root dirty for inclusion in the ledger diff. + +3. **SLE erasure**: `view.erase(sle)` removes the DID entry from the ledger state entirely. + +This ordering — directory first, then count, then erase — ensures that even if an intermediate step were to fail, the ledger would not be left with a dangling SLE that is unreachable via the owner directory. + +## Error Handling Philosophy + +The error taxonomy used here reflects the XRPL's distinction between user errors and invariant violations. `tecNO_ENTRY` (returned when the DID SLE doesn't exist) is a `tec`-class error: the transaction is applied to the ledger, the fee is consumed, but the requested operation fails because of a valid but unsatisfied precondition. `tefBAD_LEDGER` and `tecINTERNAL` are defensive guards on conditions that should be structurally impossible given a valid ledger — their `LCOV_EXCL` annotations signal that test coverage is neither expected nor required for these paths. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/did/DIDSet.cpp.ai.json b/src/libxrpl/tx/transactors/did/DIDSet.cpp.ai.json new file mode 100644 index 0000000000..8695dfe303 --- /dev/null +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp.ai.json @@ -0,0 +1,428 @@ +{ + "args": [ + { + "lineno": 23, + "name": "ctx" + }, + { + "lineno": 44, + "name": "ctx" + }, + { + "lineno": 44, + "name": "sle" + }, + { + "lineno": 44, + "name": "owner" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "DIDSet::preflight" + ], + "entry_point": "DIDSet::preflight", + "purpose": "Performs stateless validation of the DIDSet transaction before it is applied to the ledger.", + "validation_points": [ + "Checks if at least one of sfURI, sfDIDDocument, sfData is present.", + "Checks if all present fields are empty.", + "Checks if any field exceeds its maximum allowed length." + ] + }, + { + "call_chain": [ + "DIDSet::doApply", + "addSLE (conditionally, for new objects)" + ], + "entry_point": "DIDSet::doApply", + "purpose": "Applies the DIDSet transaction to the ledger, updating or creating the DID object.", + "validation_points": [ + "Re-checks for empty fields after update (returns tecEMPTY_DID if all are absent)." + ] + }, + { + "call_chain": [ + "addSLE" + ], + "entry_point": "addSLE", + "purpose": "Handles insertion of a new SLE (ledger object) for a DID, including reserve checks and directory insertion.", + "validation_points": [ + "Checks account reserve before allowing object creation." + ] + } + ], + "data_flows": [ + { + "field": "sfURI", + "flow": [ + "ctx.tx[sfURI] (input)", + "DIDSet::preflight (presence, emptiness, length validation)", + "DIDSet::doApply (update or set on SLE)", + "SLE (ledger object, persisted)" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence and emptiness", + "Checked for length (isTooLong)", + "Set or removed from SLE depending on value" + ], + "validated_at": "DIDSet::preflight" + }, + { + "field": "sfDIDDocument", + "flow": [ + "ctx.tx[sfDIDDocument] (input)", + "DIDSet::preflight (presence, emptiness, length validation)", + "DIDSet::doApply (update or set on SLE)", + "SLE (ledger object, persisted)" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence and emptiness", + "Checked for length (isTooLong)", + "Set or removed from SLE depending on value" + ], + "validated_at": "DIDSet::preflight" + }, + { + "field": "sfData", + "flow": [ + "ctx.tx[sfData] (input)", + "DIDSet::preflight (presence, emptiness, length validation)", + "DIDSet::doApply (update or set on SLE)", + "SLE (ledger object, persisted)" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence and emptiness", + "Checked for length (isTooLong)", + "Set or removed from SLE depending on value" + ], + "validated_at": "DIDSet::preflight" + } + ], + "description": "Implements the DIDSet transaction for managing Decentralized Identifiers (DIDs) on the XRPL ledger, including validation, creation, and updating of DID ledger objects.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "sfURI", + "sfDIDDocument", + "sfData" + ], + "empty", + "string", + "validation" + ], + "evidence": "explicit field presence check at DIDSet::preflight", + "issue_pattern": "Missing empty string validation for ['sfURI', 'sfDIDDocument', 'sfData']", + "why_false_positive": "explicit field presence check validates ['sfURI', 'sfDIDDocument', 'sfData'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "sfURI", + "sfDIDDocument", + "sfData" + ], + "empty", + "string", + "validation" + ], + "evidence": "explicit field emptiness check at DIDSet::preflight", + "issue_pattern": "Missing empty string validation for ['sfURI', 'sfDIDDocument', 'sfData']", + "why_false_positive": "explicit field emptiness check validates ['sfURI', 'sfDIDDocument', 'sfData'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfURI", + "empty", + "string", + "validation" + ], + "evidence": "isTooLong lambda (length check) at DIDSet::preflight", + "issue_pattern": "Missing empty string validation for sfURI", + "why_false_positive": "isTooLong lambda (length check) validates sfURI for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfURI", + "range", + "bounds", + "validation" + ], + "evidence": "isTooLong lambda (length check) at DIDSet::preflight", + "issue_pattern": "Missing range validation for sfURI", + "why_false_positive": "isTooLong lambda (length check) validates sfURI range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDIDDocument", + "empty", + "string", + "validation" + ], + "evidence": "isTooLong lambda (length check) at DIDSet::preflight", + "issue_pattern": "Missing empty string validation for sfDIDDocument", + "why_false_positive": "isTooLong lambda (length check) validates sfDIDDocument for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfDIDDocument", + "range", + "bounds", + "validation" + ], + "evidence": "isTooLong lambda (length check) at DIDSet::preflight", + "issue_pattern": "Missing range validation for sfDIDDocument", + "why_false_positive": "isTooLong lambda (length check) validates sfDIDDocument range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfData", + "empty", + "string", + "validation" + ], + "evidence": "isTooLong lambda (length check) at DIDSet::preflight", + "issue_pattern": "Missing empty string validation for sfData", + "why_false_positive": "isTooLong lambda (length check) validates sfData for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfData", + "range", + "bounds", + "validation" + ], + "evidence": "isTooLong lambda (length check) at DIDSet::preflight", + "issue_pattern": "Missing range validation for sfData", + "why_false_positive": "isTooLong lambda (length check) validates sfData range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account reserve (owner's XRP balance)", + "empty", + "string", + "validation" + ], + "evidence": "explicit balance and reserve check at addSLE", + "issue_pattern": "Missing empty string validation for account reserve (owner's XRP balance)", + "why_false_positive": "explicit balance and reserve check validates account reserve (owner's XRP balance) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/did/DIDSet.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 23, + "name": "DIDSet::preflight" + }, + { + "args": [ + "ApplyContext& ctx", + "std::shared_ptr const& sle", + "AccountID const& owner" + ], + "lineno": 44, + "name": "addSLE" + }, + { + "args": [], + "lineno": 74, + "name": "DIDSet::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this code would be in unit/integration tests for the DIDSet transaction. Look for files like 'DIDSet_test.cpp', 'Transactor_test.cpp', or generic transaction/ledger tests in the rippled codebase. Tests should cover: (1) missing all fields, (2) all fields empty, (3) fields exceeding max length, (4) valid field updates, (5) object creation with/without sufficient reserve, (6) object deletion when all fields are removed. Gaps may exist if edge cases (e.g., boundary lengths, simultaneous field updates, or reserve exhaustion) are not explicitly tested. Code coverage tools or manual inspection of test files are needed to confirm full coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in C++ (no external validation framework)", + "validation_layer": "business_logic (preflight and apply phase of transaction processing)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temEMPTY_DID", + "field": [ + "sfURI", + "sfDIDDocument", + "sfData" + ], + "location": "DIDSet::preflight", + "validated_by": "explicit field presence check", + "validates": [ + "At least one of sfURI, sfDIDDocument, or sfData must be present in the transaction" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temEMPTY_DID", + "field": [ + "sfURI", + "sfDIDDocument", + "sfData" + ], + "location": "DIDSet::preflight", + "validated_by": "explicit field emptiness check", + "validates": [ + "If a field is present, it must not be empty (all present fields must have non-empty values)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfURI", + "location": "DIDSet::preflight", + "validated_by": "isTooLong lambda (length check)", + "validates": [ + "sfURI must not exceed maxDIDURILength" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfDIDDocument", + "location": "DIDSet::preflight", + "validated_by": "isTooLong lambda (length check)", + "validates": [ + "sfDIDDocument must not exceed maxDIDDocumentLength" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfData", + "location": "DIDSet::preflight", + "validated_by": "isTooLong lambda (length check)", + "validates": [ + "sfData must not exceed maxDIDDataLength" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "tecINSUFFICIENT_RESERVE", + "field": "account reserve (owner's XRP balance)", + "location": "addSLE", + "validated_by": "explicit balance and reserve check", + "validates": [ + "Owner must have sufficient XRP balance to meet reserve requirements for new object creation" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/did/DIDSet.cpp.ai.md b/src/libxrpl/tx/transactors/did/DIDSet.cpp.ai.md new file mode 100644 index 0000000000..8b387d8346 --- /dev/null +++ b/src/libxrpl/tx/transactors/did/DIDSet.cpp.ai.md @@ -0,0 +1,39 @@ +# `DIDSet.cpp` — DID Creation and Update Transactor + +## Role and Context + +`DIDSet.cpp` implements the `DIDSet` transaction type, which creates or updates a Decentralized Identifier (DID) object owned by an XRPL account. The implementation conforms to the W3C DID v1.0 specification, mapping the spec's three core payload concepts onto three blob fields stored in a `ltDID` ledger entry: `sfURI` (the DID document URI), `sfDIDDocument` (the raw document body), and `sfData` (arbitrary associated data). Each field is independently optional but at least one must carry meaningful content — an entirely empty DID is rejected as meaningless. All three are capped at 256 bytes each (`maxDIDURILength`, `maxDIDDocumentLength`, `maxDIDDataLength`, all defined in `Protocol.h`). + +`DIDSet` inherits from `Transactor` and plugs into the standard two-phase apply pipeline: a stateless `preflight` that runs before any ledger state is touched, followed by `doApply` that commits changes to the current ledger view. + +## Validation in `preflight` + +`preflight` enforces two distinct emptiness invariants before any ledger access occurs. + +The first check rejects a transaction where none of the three fields is even present — a `DIDSet` carrying no payload is malformed (`temEMPTY_DID`). The second check is subtler: it rejects a transaction where all three fields *are* present but every one of them is an empty byte string. This matters because a client could send `URI=""`, `DIDDocument=""`, `Data=""` as a way to wipe an existing DID clean rather than using `DIDDelete`. The protocol blocks that by treating "all-present but all-empty" as equivalent to "no fields at all." + +The length check uses a local `isTooLong` lambda that dereferences the optional field via `ctx.tx[~sField]` — the tilde operator returns `std::optional` — and only evaluates the length if the field is actually present. A single `temMALFORMED` covers any field that exceeds its per-field limit. + +## Apply Logic in `doApply` + +`doApply` is a clean upsert. It computes the canonical DID keylet for the submitting account via `keylet::did(account_)` — each account can hold at most one DID object, so the keylet is deterministic and needs no disambiguation. It then peeks at the ledger to check existence. + +**Update path**: If the DID object already exists, a local `update` lambda is applied to each of the three fields. The lambda's behavior is intentionally asymmetric: if the transaction includes a field and it is non-empty, the SLE field is overwritten; if the transaction includes a field but it is *empty*, the field is actively removed from the SLE via `makeFieldAbsent`. This allows callers to surgically clear one field while leaving others intact. After all three updates, the code re-checks whether all fields are now absent — it is possible to arrive at an empty DID via update (e.g., clearing the last remaining field), and this is rejected with `tecEMPTY_DID`, which is a `tec`-class error that still claims the fee rather than being silently dropped. + +**Create path**: If no DID object exists yet, the code constructs a fresh `SLE` of type `ltDID`, sets `sfAccount` to the submitting account, and calls the file-local `addSLE` helper. Only non-empty fields are populated in the new object — the `set` lambda skips absent or empty fields entirely. There is a guard here behind the `fixEmptyDID` amendment: without that fix enabled, the create path would allow an SLE where all three payload fields ended up absent (because the transaction passed `preflight`'s loose check but all provided values were empty strings). The amendment adds the same post-creation empty check that the update path already performs, returning `tecEMPTY_DID` before the object is ever inserted. + +## The `addSLE` Helper + +`addSLE` is a file-local static function that handles the bookkeeping required when inserting any new owner-tracked ledger object. It follows a standard three-step pattern: reserve check, object insertion, and directory linkage. + +The reserve check reads the account's current `sfBalance` and computes the XRP reserve for `ownerCount + 1` objects. If the balance would fall below the new reserve threshold, it returns `tecINSUFFICIENT_RESERVE` before touching anything. This is the correct place for the check — it must happen *before* the object is inserted, not after. + +After inserting the SLE into the ledger view, `addSLE` calls `dirInsert` to add the object's key into the account's owner directory, capturing the returned page index into `sfOwnerNode` on the SLE itself. That stored page index is essential for O(1) removal later — `DIDDelete` uses it to call `dirRemove` without scanning the entire directory. Finally, `adjustOwnerCount` increments `sfOwnerCount` on the account root and the updated account SLE is written back. + +## Relationship to `DIDDelete` + +`DIDDelete` is the symmetric counterpart. Its `deleteSLE` mirrors `addSLE` exactly in reverse: it calls `dirRemove` using the stored `sfOwnerNode`, decrements `sfOwnerCount`, and erases the SLE. The two files together form the complete DID lifecycle on the ledger; neither contains domain logic that belongs in the other. + +## Error-Code Taxonomy + +The file uses error codes at two different severity levels deliberately. `tem`-class codes from `preflight` cause the transaction to be dropped entirely (no fee claimed, not included in a ledger). `tec`-class codes from `doApply` — `tecEMPTY_DID`, `tecINSUFFICIENT_RESERVE`, `tecDIR_FULL` — still consume the transaction fee and are recorded in the ledger. The re-check for an empty DID inside `doApply` therefore correctly uses `tecEMPTY_DID` rather than `temEMPTY_DID`, because by that point the transaction has already passed preflight and a fee should be charged. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/escrow/Escrow.cpp.ai.json b/src/libxrpl/tx/transactors/escrow/Escrow.cpp.ai.json new file mode 100644 index 0000000000..108a70bb51 --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/Escrow.cpp.ai.json @@ -0,0 +1,601 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "EscrowCreate::preflight", + "amountFromValue", + "STAmount::isLegalNet", + "accountFromStringStrict", + "sfCancelAfter field check", + "sfFinishAfter field check", + "STObject::isFieldPresent (sfCondition)" + ], + "entry_point": "EscrowCreate::preflight", + "purpose": "Performs static validation of EscrowCreate transaction fields before execution.", + "validation_points": [ + "amountFromValue (validates Amount)", + "STAmount::isLegalNet (validates Amount legality)", + "accountFromStringStrict (validates Destination)", + "sfCancelAfter field check (validates CancelAfter)", + "sfFinishAfter field check (validates FinishAfter)", + "STObject::isFieldPresent (validates Condition presence/format)" + ] + }, + { + "call_chain": [ + "EscrowFinish::preflight", + "STObject::isFieldPresent (sfCondition)", + "STObject::isFieldPresent (sfFulfillment)" + ], + "entry_point": "EscrowFinish::preflight", + "purpose": "Validates EscrowFinish transaction fields, especially cryptographic conditions.", + "validation_points": [ + "STObject::isFieldPresent (validates Condition and Fulfillment presence/format)" + ] + }, + { + "call_chain": [ + "EscrowCancel::preflight" + ], + "entry_point": "EscrowCancel::preflight", + "purpose": "Validates EscrowCancel transaction fields (minimal validation).", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "Amount", + "flow": [ + "Transaction JSON", + "EscrowCreate::preflight", + "amountFromValue", + "STAmount::isLegalNet", + "EscrowCreate::doApply" + ], + "origin": "Transaction JSON input", + "transformations": [ + "Parsed from JSON", + "Converted to STAmount", + "Legality checked (no negative, no XRP drops, etc.)" + ], + "validated_at": "amountFromValue / STAmount::isLegalNet" + }, + { + "field": "Destination", + "flow": [ + "Transaction JSON", + "EscrowCreate::preflight", + "accountFromStringStrict", + "EscrowCreate::doApply" + ], + "origin": "Transaction JSON input", + "transformations": [ + "Parsed from JSON", + "Validated as strict account ID" + ], + "validated_at": "accountFromStringStrict" + }, + { + "field": "CancelAfter", + "flow": [ + "Transaction JSON", + "EscrowCreate::preflight", + "sfCancelAfter field check", + "EscrowCreate::doApply" + ], + "origin": "Transaction JSON input", + "transformations": [ + "Parsed from JSON", + "Checked for presence and correct type (uint32)" + ], + "validated_at": "sfCancelAfter field check" + }, + { + "field": "FinishAfter", + "flow": [ + "Transaction JSON", + "EscrowCreate::preflight", + "sfFinishAfter field check", + "EscrowCreate::doApply" + ], + "origin": "Transaction JSON input", + "transformations": [ + "Parsed from JSON", + "Checked for presence and correct type (uint32)" + ], + "validated_at": "sfFinishAfter field check" + }, + { + "field": "Condition", + "flow": [ + "Transaction JSON", + "EscrowCreate::preflight", + "STObject::isFieldPresent (sfCondition)", + "EscrowCreate::doApply" + ], + "origin": "Transaction JSON input", + "transformations": [ + "Parsed from JSON", + "Checked for presence and correct format (cryptographic condition)" + ], + "validated_at": "STObject::isFieldPresent (sfCondition)" + } + ], + "description": "", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Amount", + "validation", + "missing", + "check" + ], + "evidence": "Field Amount validated by Custom validation in preflight functions, STObject field checks, crypto-conditions library", + "issue_pattern": "Missing validation for Amount", + "why_false_positive": "Custom validation in preflight functions, STObject field checks, crypto-conditions library validates Amount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Destination", + "validation", + "missing", + "check" + ], + "evidence": "Field Destination validated by Custom validation in preflight functions, STObject field checks, crypto-conditions library", + "issue_pattern": "Missing validation for Destination", + "why_false_positive": "Custom validation in preflight functions, STObject field checks, crypto-conditions library validates Destination automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Owner", + "validation", + "missing", + "check" + ], + "evidence": "Field Owner validated by Custom validation in preflight functions, STObject field checks, crypto-conditions library", + "issue_pattern": "Missing validation for Owner", + "why_false_positive": "Custom validation in preflight functions, STObject field checks, crypto-conditions library validates Owner automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "EscrowID", + "validation", + "missing", + "check" + ], + "evidence": "Field EscrowID validated by Custom validation in preflight functions, STObject field checks, crypto-conditions library", + "issue_pattern": "Missing validation for EscrowID", + "why_false_positive": "Custom validation in preflight functions, STObject field checks, crypto-conditions library validates EscrowID automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "CancelAfter", + "validation", + "missing", + "check" + ], + "evidence": "Field CancelAfter validated by Custom validation in preflight functions, STObject field checks, crypto-conditions library", + "issue_pattern": "Missing validation for CancelAfter", + "why_false_positive": "Custom validation in preflight functions, STObject field checks, crypto-conditions library validates CancelAfter automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "FinishAfter", + "validation", + "missing", + "check" + ], + "evidence": "Field FinishAfter validated by Custom validation in preflight functions, STObject field checks, crypto-conditions library", + "issue_pattern": "Missing validation for FinishAfter", + "why_false_positive": "Custom validation in preflight functions, STObject field checks, crypto-conditions library validates FinishAfter automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Condition", + "validation", + "missing", + "check" + ], + "evidence": "Field Condition validated by Custom validation in preflight functions, STObject field checks, crypto-conditions library", + "issue_pattern": "Missing validation for Condition", + "why_false_positive": "Custom validation in preflight functions, STObject field checks, crypto-conditions library validates Condition automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Fulfillment", + "validation", + "missing", + "check" + ], + "evidence": "Field Fulfillment validated by Custom validation in preflight functions, STObject field checks, crypto-conditions library", + "issue_pattern": "Missing validation for Fulfillment", + "why_false_positive": "Custom validation in preflight functions, STObject field checks, crypto-conditions library validates Fulfillment automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Amount", + "empty", + "string", + "validation" + ], + "evidence": "amountFromValue / STAmount::isLegalNet at EscrowCreate::preflight", + "issue_pattern": "Missing empty string validation for Amount", + "why_false_positive": "amountFromValue / STAmount::isLegalNet validates Amount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Destination", + "empty", + "string", + "validation" + ], + "evidence": "accountFromStringStrict at EscrowCreate::preflight", + "issue_pattern": "Missing empty string validation for Destination", + "why_false_positive": "accountFromStringStrict validates Destination for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "CancelAfter", + "empty", + "string", + "validation" + ], + "evidence": "sfCancelAfter field check at EscrowCreate::preflight", + "issue_pattern": "Missing empty string validation for CancelAfter", + "why_false_positive": "sfCancelAfter field check validates CancelAfter for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "FinishAfter", + "empty", + "string", + "validation" + ], + "evidence": "sfFinishAfter field check at EscrowCreate::preflight", + "issue_pattern": "Missing empty string validation for FinishAfter", + "why_false_positive": "sfFinishAfter field check validates FinishAfter for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Condition", + "empty", + "string", + "validation" + ], + "evidence": "STObject::isFieldPresent / condition format check at EscrowCreate::preflight", + "issue_pattern": "Missing empty string validation for Condition", + "why_false_positive": "STObject::isFieldPresent / condition format check validates Condition for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Condition", + "format", + "validation", + "invalid" + ], + "evidence": "STObject::isFieldPresent / condition format check at EscrowCreate::preflight", + "issue_pattern": "Missing format validation for Condition", + "why_false_positive": "STObject::isFieldPresent / condition format check validates Condition format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Owner", + "empty", + "string", + "validation" + ], + "evidence": "accountFromStringStrict at EscrowFinish::preflight", + "issue_pattern": "Missing empty string validation for Owner", + "why_false_positive": "accountFromStringStrict validates Owner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "EscrowID", + "empty", + "string", + "validation" + ], + "evidence": "uint256::parseHex at EscrowFinish::preflight", + "issue_pattern": "Missing empty string validation for EscrowID", + "why_false_positive": "uint256::parseHex validates EscrowID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Condition", + "empty", + "string", + "validation" + ], + "evidence": "crypto-condition check at EscrowFinish::preflight", + "issue_pattern": "Missing empty string validation for Condition", + "why_false_positive": "crypto-condition check validates Condition for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Fulfillment", + "empty", + "string", + "validation" + ], + "evidence": "crypto-condition fulfillment check at EscrowFinish::preflight", + "issue_pattern": "Missing empty string validation for Fulfillment", + "why_false_positive": "crypto-condition fulfillment check validates Fulfillment for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Owner", + "empty", + "string", + "validation" + ], + "evidence": "accountFromStringStrict at EscrowCancel::preflight", + "issue_pattern": "Missing empty string validation for Owner", + "why_false_positive": "accountFromStringStrict validates Owner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "EscrowID", + "empty", + "string", + "validation" + ], + "evidence": "uint256::parseHex at EscrowCancel::preflight", + "issue_pattern": "Missing empty string validation for EscrowID", + "why_false_positive": "uint256::parseHex validates EscrowID for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/escrow/Escrow.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [], + "test_coverage_notes": "Test coverage for Escrow validation is typically found in unit/integration tests such as 'test/tx/Escrow_test.cpp', 'test/tx/Escrow_test.py', or similar files. These tests cover valid and invalid Amounts, Destinations, CancelAfter/FinishAfter logic, and Condition/Fulfillment checks. Gaps may exist in edge cases for malformed fields, boundary timestamps, or rare cryptographic condition formats. Some negative tests (e.g., illegal Amounts, missing required fields) are present, but exhaustive fuzzing or malformed binary field tests may not be fully covered.", + "validation_architecture": { + "auto_validated_fields": [ + "Amount", + "Destination", + "Owner", + "EscrowID", + "CancelAfter", + "FinishAfter", + "Condition", + "Fulfillment" + ], + "framework": "Custom validation in preflight functions, STObject field checks, crypto-conditions library", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "Amount", + "location": "EscrowCreate::preflight", + "validated_by": "amountFromValue / STAmount::isLegalNet", + "validates": [ + "Amount must be a legal XRP amount", + "Amount must be positive", + "Amount must not be zero" + ], + "validation_type": "type|range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_DST_ACCOUNT", + "field": "Destination", + "location": "EscrowCreate::preflight", + "validated_by": "accountFromStringStrict", + "validates": [ + "Destination must be a valid account address" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_EXPIRATION", + "field": "CancelAfter", + "location": "EscrowCreate::preflight", + "validated_by": "sfCancelAfter field check", + "validates": [ + "CancelAfter must be after current close time", + "CancelAfter must be greater than FinishAfter if both are present" + ], + "validation_type": "type|range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_EXPIRATION", + "field": "FinishAfter", + "location": "EscrowCreate::preflight", + "validated_by": "sfFinishAfter field check", + "validates": [ + "FinishAfter must be after current close time" + ], + "validation_type": "type|range|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "temMALFORMED", + "field": "Condition", + "location": "EscrowCreate::preflight", + "validated_by": "STObject::isFieldPresent / condition format check", + "validates": [ + "Condition must be a valid crypto-condition" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_OWNER", + "field": "Owner", + "location": "EscrowFinish::preflight", + "validated_by": "accountFromStringStrict", + "validates": [ + "Owner must be a valid account address" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_ESCROW", + "field": "EscrowID", + "location": "EscrowFinish::preflight", + "validated_by": "uint256::parseHex", + "validates": [ + "EscrowID must be a valid 256-bit hex" + ], + "validation_type": "format|type" + }, + { + "confidence": 0.9, + "error_thrown": "temMALFORMED", + "field": "Condition", + "location": "EscrowFinish::preflight", + "validated_by": "crypto-condition check", + "validates": [ + "Condition must match escrow's stored condition" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "temMALFORMED", + "field": "Fulfillment", + "location": "EscrowFinish::preflight", + "validated_by": "crypto-condition fulfillment check", + "validates": [ + "Fulfillment must be valid for the escrow's condition" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_OWNER", + "field": "Owner", + "location": "EscrowCancel::preflight", + "validated_by": "accountFromStringStrict", + "validates": [ + "Owner must be a valid account address" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_ESCROW", + "field": "EscrowID", + "location": "EscrowCancel::preflight", + "validated_by": "uint256::parseHex", + "validates": [ + "EscrowID must be a valid 256-bit hex" + ], + "validation_type": "format|type" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/escrow/Escrow.cpp.ai.md b/src/libxrpl/tx/transactors/escrow/Escrow.cpp.ai.md new file mode 100644 index 0000000000..d8e1ea3317 --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/Escrow.cpp.ai.md @@ -0,0 +1,31 @@ +# Escrow.cpp — Module Placeholder for the XRPL Escrow Transactors + +`Escrow.cpp` is an empty file — it contains zero bytes. Its significance is structural rather than functional: it nominally anchors the escrow transactor module within the build system and signals the conceptual boundary of a feature whose implementation is entirely distributed across three sibling files in the same directory. + +## Module Context + +The escrow subsystem spans four files: this empty placeholder and the three active implementation units `EscrowCreate.cpp`, `EscrowFinish.cpp`, and `EscrowCancel.cpp`. Together they implement XRPL's conditional payment mechanism, which locks XRP or tokens into a ledger object that can only be released to a destination under specific conditions — a time-based unlock, a cryptographic condition/fulfillment pair, or both. The canonical module-level comment describing all three transaction types lives in `EscrowCreate.cpp` rather than here, which reinforces that this file is vestigial. + +The empty file is most likely a build system artifact: either CMake required it when the escrow logic was originally consolidated in one file, or a refactor split a formerly monolithic `Escrow.cpp` into three specialized transactors without removing the now-empty source. + +## The Three Transactors + +**`EscrowCreate`** locks funds and writes a new `SLE` (Serialized Ledger Entry) into the ledger. The keylet is derived from the creator's account and their current sequence value (`keylet::escrow(account_, ctx_.tx.getSeqValue())`), making each escrow globally addressable with no additional state. The `doApply()` phase inserts the escrow into up to three owner directories: the sender's, the recipient's (when different from the sender), and — for IOU escrows only, not MPT — the issuer's. The issuer directory entry exists so the issuer can track total locked balance against their outstanding obligations. MPT does not need this because locked balance is tracked directly in the `MPTokenIssuance` object. + +A non-obvious design choice is the transfer rate snapshot: when `featureTokenEscrow` is active and the amount carries a non-parity transfer rate, the current rate is stored in the escrow SLE as `sfTransferRate`. This is a deliberate protection against issuer rate manipulation — the rate is frozen at creation and compared against the current rate at finish time; whichever is lower wins. Without this mechanism, an issuer could raise their transfer fee after an escrow is created, retroactively penalizing the escrow recipient. + +The `preflight` function enforces a subtle invariant: at least one of `sfFinishAfter` or `sfCondition` must be present. Without either, an escrow could theoretically be finished immediately and unconditionally — legal but confusing enough to be disallowed. + +**`EscrowFinish`** is the most complex of the three. It carries the burden of cryptographic condition verification, time window enforcement, and — for token escrows — potential trust line or MPToken account creation on delivery. The fulfillment size feeds directly into `calculateBaseFee()`: the fee surcharge is `base * (32 + size / 16)`, penalizing large fulfillments to prevent computational DoS attacks. + +Crypto-condition validation (`checkCondition()`) is expensive to run, and a single transaction can be replayed multiple times during consensus processing. To avoid redundant work, the result is cached in the `HashRouter` using two private flag bits (`SF_CF_INVALID` / `SF_CF_VALID`). The `preflightSigValidated()` phase populates these bits; `doApply()` reads them and falls back to recomputing only if the flags have aged out of the router. The escrow SLE's own `sfCondition` field is then compared byte-for-byte against the transaction's `sfCondition` to prevent finish attempts that supply a valid but different condition than the one locked at creation. + +For IOU token escrows, `doApply()` delegates to `escrowUnlockApplyHelper` (defined in `EscrowHelpers.h`), which may create a trust line for the receiver on the fly if one doesn't exist — provided the receiver has enough XRP reserve to cover it. This mirrors behavior in ordinary payments but must be handled explicitly here because escrow finish is not routed through the payment path. + +**`EscrowCancel`** is intentionally minimal. Its `preflight` unconditionally returns `tesSUCCESS` — all meaningful checking happens in `doApply()` against live ledger state. The key guard is time: cancellation is only permitted after `sfCancelAfter` has passed. Escrows with no cancel time cannot be cancelled at all. On cancel, tokens are returned at `parityRate` (no transfer fee), because the funds are simply being restored to their origin rather than being transferred to a third party. + +## Validation Architecture + +All three transactors follow XRPL's three-phase transaction pipeline: `preflight` (static, no ledger access) → `preclaim` (read-only ledger checks) → `doApply` (mutating). Field-level validation — empty strings, type checks, format validity — is handled automatically by `STObject` field deserialization and the `amountFromValue` / `accountFromStringStrict` helpers, so manual null checks do not appear in the business logic code. Cryptographic condition format is validated by deserializing with `Condition::deserialize()` in preflight and failing with `temMALFORMED` on parse error. + +For token escrows, `preclaim` in both `EscrowCreate` and `EscrowCancel` dispatches via `std::visit` over `amount.asset().value()`, branching on `Issue` vs `MPTIssue` at compile time through the `ValidIssueType` concept. This pattern avoids a runtime `if`/`else` on asset type and ensures that new asset types added in the future will produce a compile-time error rather than a silent skip. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp.ai.json b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp.ai.json new file mode 100644 index 0000000000..3ab83e9565 --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp.ai.json @@ -0,0 +1,365 @@ +{ + "args": [ + { + "lineno": 12, + "name": "ctx" + }, + { + "lineno": 25, + "name": "ctx" + }, + { + "lineno": 25, + "name": "account" + }, + { + "lineno": 25, + "name": "amount" + }, + { + "lineno": 41, + "name": "ctx" + }, + { + "lineno": 41, + "name": "account" + }, + { + "lineno": 41, + "name": "amount" + }, + { + "lineno": 62, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "EscrowCancel::preflight" + ], + "entry_point": "EscrowCancel::preflight", + "purpose": "Initial preflight check for EscrowCancel transaction. Currently, this is a stub that always returns success.", + "validation_points": [] + }, + { + "call_chain": [ + "EscrowCancel::preclaim", + "escrowCancelPreclaimHelper or escrowCancelPreclaimHelper" + ], + "entry_point": "EscrowCancel::preclaim", + "purpose": "Performs preclaim validation for EscrowCancel, including existence and authorization checks for non-XRP escrows.", + "validation_points": [ + "escrowCancelPreclaimHelper: issuer == account check, requireAuth check", + "escrowCancelPreclaimHelper: issuer == account check, existence of MPT issuance object, requireAuth (WeakAuth) check" + ] + }, + { + "call_chain": [ + "EscrowCancel::doApply" + ], + "entry_point": "EscrowCancel::doApply", + "purpose": "Applies the EscrowCancel transaction, including final checks (escrow existence, cancel time, directory removal) and state mutation.", + "validation_points": [ + "Escrow existence check (peek)", + "CancelAfter field presence and time check" + ] + } + ], + "data_flows": [ + { + "field": "sfOwner", + "flow": [ + "ctx.tx[sfOwner]", + "keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence])", + "ctx.view.read(k) or ctx_.view().peek(k)" + ], + "origin": "ctx.tx[sfOwner] (transaction input)", + "transformations": [ + "Used to construct escrow keylet" + ], + "validated_at": "Escrow existence check in preclaim and doApply" + }, + { + "field": "sfOfferSequence", + "flow": [ + "ctx.tx[sfOfferSequence]", + "keylet::escrow(ctx.tx[sfOwner], ctx.tx[sfOfferSequence])", + "ctx.view.read(k) or ctx_.view().peek(k)" + ], + "origin": "ctx.tx[sfOfferSequence] (transaction input)", + "transformations": [ + "Used to construct escrow keylet" + ], + "validated_at": "Escrow existence check in preclaim and doApply" + }, + { + "field": "sfAmount", + "flow": [ + "(*slep)[sfAmount]", + "amount", + "if (!isXRP(amount)) { ... }", + "escrowCancelPreclaimHelper(ctx, account, amount)" + ], + "origin": "(*slep)[sfAmount] (from escrow ledger entry)", + "transformations": [ + "Type-checked for XRP vs. IOU/MPT", + "Passed to helper for further validation" + ], + "validated_at": "escrowCancelPreclaimHelper or escrowCancelPreclaimHelper" + }, + { + "field": "issuer (of amount)", + "flow": [ + "amount.getIssuer()", + "escrowCancelPreclaimHelper", + "issuer == account check", + "requireAuth check" + ], + "origin": "amount.getIssuer()", + "transformations": [ + "Compared to escrow account", + "Used for authorization check" + ], + "validated_at": "escrowCancelPreclaimHelper or escrowCancelPreclaimHelper" + }, + { + "field": "MPT issuance object", + "flow": [ + "amount.get().getMptID()", + "keylet::mptIssuance(...)", + "ctx.view.read(issuanceKey)" + ], + "origin": "amount.get().getMptID()", + "transformations": [ + "Used to look up MPT issuance ledger entry" + ], + "validated_at": "escrowCancelPreclaimHelper (existence check)" + }, + { + "field": "requireAuth flag", + "flow": [ + "requireAuth(ctx.view, ...)", + "Checks authorization for account" + ], + "origin": "Issuer account or MPT issuance object", + "transformations": [ + "Checked for both Issue and MPTIssue (with AuthType::WeakAuth for MPT)" + ], + "validated_at": "escrowCancelPreclaimHelper or escrowCancelPreclaimHelper" + }, + { + "field": "sfCancelAfter", + "flow": [ + "(*slep)[~sfCancelAfter]", + "Checked for presence and compared to now" + ], + "origin": "(*slep)[~sfCancelAfter] (from escrow ledger entry)", + "transformations": [ + "Optional field, checked for existence and time" + ], + "validated_at": "EscrowCancel::doApply" + } + ], + "description": "Implements the logic for the EscrowCancel transaction in the XRPL codebase, including preflight, preclaim, and apply logic for canceling escrows, supporting both XRP and token escrows.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "issuer (of amount)", + "empty", + "string", + "validation" + ], + "evidence": "manual check (issuer == account) at escrowCancelPreclaimHelper and escrowCancelPreclaimHelper", + "issue_pattern": "Missing empty string validation for issuer (of amount)", + "why_false_positive": "manual check (issuer == account) validates issuer (of amount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "authorization (requireAuth flag on issuer)", + "empty", + "string", + "validation" + ], + "evidence": "requireAuth function at escrowCancelPreclaimHelper", + "issue_pattern": "Missing empty string validation for authorization (requireAuth flag on issuer)", + "why_false_positive": "requireAuth function validates authorization (requireAuth flag on issuer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "existence of MPT issuance object", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(issuanceKey) at escrowCancelPreclaimHelper", + "issue_pattern": "Missing empty string validation for existence of MPT issuance object", + "why_false_positive": "ctx.view.read(issuanceKey) validates existence of MPT issuance object for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "authorization (requireAuth flag on MPT issuer)", + "empty", + "string", + "validation" + ], + "evidence": "requireAuth function (with AuthType::WeakAuth) at escrowCancelPreclaimHelper", + "issue_pattern": "Missing empty string validation for authorization (requireAuth flag on MPT issuer)", + "why_false_positive": "requireAuth function (with AuthType::WeakAuth) validates authorization (requireAuth flag on MPT issuer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "existence of escrow object", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(k) at EscrowCancel::preclaim", + "issue_pattern": "Missing empty string validation for existence of escrow object", + "why_false_positive": "ctx.view.read(k) validates existence of escrow object for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 12, + "name": "EscrowCancel::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx", + "AccountID const& account", + "STAmount const& amount" + ], + "lineno": 25, + "name": "escrowCancelPreclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx", + "AccountID const& account", + "STAmount const& amount" + ], + "lineno": 41, + "name": "escrowCancelPreclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 62, + "name": "EscrowCancel::preclaim" + }, + { + "args": [], + "lineno": 87, + "name": "EscrowCancel::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is in escrowCancelPreclaimHelper and escrowCancelPreclaimHelper, as well as in EscrowCancel::doApply. Typical test files would be in the rippled codebase under 'src/test/app/Escrow_test.cpp', 'src/test/app/TokenEscrow_test.cpp', or similar. Coverage for XRP escrows is likely high, but MPT/IOU paths (especially requireAuth and MPT issuance existence) may have less coverage, especially for edge cases (issuer == account, missing MPT issuance, WeakAuth). The preflight function is a stub and likely not tested. Directory removal failure in doApply is likely not covered (LCOV_EXCL).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation, XRPL transaction engine", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "issuer (of amount)", + "location": "escrowCancelPreclaimHelper and escrowCancelPreclaimHelper", + "validated_by": "manual check (issuer == account)", + "validates": [ + "issuer must not be the same as the account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "result of requireAuth (various TER codes)", + "field": "authorization (requireAuth flag on issuer)", + "location": "escrowCancelPreclaimHelper", + "validated_by": "requireAuth function", + "validates": [ + "if issuer has requireAuth, account must be authorized" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND", + "field": "existence of MPT issuance object", + "location": "escrowCancelPreclaimHelper", + "validated_by": "ctx.view.read(issuanceKey)", + "validates": [ + "MPT issuance object must exist for the given MPT ID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "result of requireAuth (various TER codes)", + "field": "authorization (requireAuth flag on MPT issuer)", + "location": "escrowCancelPreclaimHelper", + "validated_by": "requireAuth function (with AuthType::WeakAuth)", + "validates": [ + "if MPT issuer has requireAuth, account must be authorized (weak auth)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_TARGET", + "field": "existence of escrow object", + "location": "EscrowCancel::preclaim", + "validated_by": "ctx.view.read(k)", + "validates": [ + "escrow object must exist for given owner and offer sequence" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp.ai.md b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp.ai.md new file mode 100644 index 0000000000..d3afc1cf31 --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowCancel.cpp.ai.md @@ -0,0 +1,41 @@ +# `EscrowCancel.cpp` — Escrow Cancellation Transactor + +## Role in the System + +`EscrowCancel.cpp` implements the `EscrowCancel` transaction type in the XRPL transaction engine. Its sole job is to unwind an escrow whose conditions were never met by the time its `CancelAfter` deadline passed, returning the locked funds to the original owner. This is the "expiry refund" path in the escrow lifecycle: if neither party triggers a successful `EscrowFinish` before the deadline, any party can submit an `EscrowCancel` to reclaim the funds once the ledger's parent close time has advanced past `sfCancelAfter`. + +The file participates in the standard three-phase transactor pipeline defined by the `Transactor` base class: `preflight` (stateless format checks), `preclaim` (read-only ledger validation), and `doApply` (mutating ledger state). + +## Phase 1: `preflight` — Intentional Stub + +`EscrowCancel::preflight` returns `tesSUCCESS` unconditionally. This is deliberate: an `EscrowCancel` transaction carries only `sfOwner` and `sfOfferSequence` to identify the escrow object; there are no variable-length cryptographic fields or format invariants to validate at the syntax level. The original XRP-only escrow design had nothing to check here, and the token escrow extension (guarded by `featureTokenEscrow`) adds no new transaction fields that need syntactic validation either. + +## Phase 2: `preclaim` — Read-Only Authorization Checks + +`EscrowCancel::preclaim` is entirely gated on the `featureTokenEscrow` amendment. For classic XRP escrows the method returns `tesSUCCESS` immediately, deferring the existence check to `doApply`. This apparent redundancy (the existence check happens again in `doApply`) is a consequence of the amendment structure: the XRP escrow code predates the token escrow feature, and the additional preclaim logic was bolted on alongside it. + +When `featureTokenEscrow` is active, `preclaim` reads the escrow SLE identified by `keylet::escrow(sfOwner, sfOfferSequence)` and, for non-XRP amounts, dispatches to one of two template specializations via a `std::visit` over the asset's `value()` variant: + +**`escrowCancelPreclaimHelper`** validates IOU escrows. It guards against the degenerate `issuer == account` case (which should be impossible by construction but returns `tecINTERNAL` defensively), then calls `requireAuth` to confirm the escrow owner is still authorized by the issuing account. Authorization must remain valid at cancel time, not just at creation time. + +**`escrowCancelPreclaimHelper`** handles Multi-Purpose Token escrows. It adds an extra step not needed for IOUs: it explicitly looks up the `MPTIssuance` object via `keylet::mptIssuance` and returns `tecOBJECT_NOT_FOUND` if it has been deleted since the escrow was created. It then checks authorization using `AuthType::WeakAuth` rather than the strict auth used for IOUs. This distinction matters: MPT authorization can be "weak" (the holder is allowed to participate without issuer per-transaction approval), reflecting the different trust model MPTs operate under. + +## Phase 3: `doApply` — State Mutation + +`doApply` performs the actual ledger surgery and is where all the interesting invariant enforcement happens. + +**Escrow lookup:** The escrow is located via `ctx_.view().peek()` (mutable access) using the same key as `preclaim`. If the object is missing and `featureTokenEscrow` is enabled, this returns `tecINTERNAL` — a missing object at apply time after a successful preclaim means the ledger is in an inconsistent state. For legacy XRP escrows (where preclaim performed no existence check), the missing-object case returns `tecNO_TARGET` instead. + +**Time guard:** Two checks enforce the cancellation window. If the escrow has no `sfCancelAfter` field at all, cancellation is permanently forbidden — this covers escrows that use only a `FinishAfter` condition with no expiry. If `sfCancelAfter` is present but the current ledger's `parentCloseTime` has not yet passed it, the attempt is also rejected with `tecNO_PERMISSION`. The `after()` helper abstracts the comparison of ledger timestamps. + +**Directory cleanup:** An escrow object can be linked into up to three owner directories. The primary entry is always in the `ownerDir` of the escrow's `sfAccount` (the original creator) under `sfOwnerNode`. An optional entry may exist in the recipient's `ownerDir` under `sfDestinationNode`, recorded at escrow creation time if a destination was specified. For non-XRP token escrows, there may be a third entry in the issuer's `ownerDir` under `sfIssuerNode`. Each removal uses `ctx_.view().dirRemove()` with a hard failure path (`tefBAD_LEDGER`) if the directory entry cannot be found — directory inconsistency is treated as fatal ledger corruption. + +**Fund return:** For XRP, the funds are returned by directly incrementing the owner's `sfBalance`. This is a direct balance manipulation rather than a payment, bypassing payment routing entirely. For non-XRP assets, `escrowUnlockApplyHelper` (defined in `EscrowHelpers.h`) handles the IOU trust-line or MPT balance transfer. A critical detail in the cancel path is that both `sender` and `receiver` are set to `account` — the escrow owner — because the cancel operation refunds the funds back to their source. The `createAsset` flag is `account == account_`, where `account_` is the transaction submitter; this is `true` when the escrow owner is canceling their own escrow, permitting the helper to auto-create a trust line or MPToken holding if one was somehow absent, and `false` otherwise. `parityRate` is passed as the `lockedRate`, meaning no transfer fee is applied on the refund path (unlike `EscrowFinish`, which may apply a fee if the rate was locked at creation and has since changed). + +**Owner count:** `adjustOwnerCount` decrements the escrow owner's reserve count by one, releasing the reserve that was held against the escrow object since its creation. + +## Design Tradeoffs + +The split between `preclaim` and `doApply` for the existence check is a legacy artifact. XRP escrows perform the check only in `doApply` because the original code predated the separation of read-only and mutating phases. Token escrow added `preclaim` checks to surface authorization failures earlier (before consuming compute in `doApply`), but rather than refactoring the XRP path, the new checks are entirely behind the amendment flag. + +The `createAsset = (account == account_)` heuristic for whether to auto-create a trust line or MPToken holding during a cancel is slightly subtle. Since the funds go back to the escrow creator (`account`), the question is whether to create missing ledger objects for that account. Allowing creation only when the submitter is the owner prevents a third-party canceler from unilaterally creating objects (and reserve obligations) on another account's behalf during a cancel operation. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp.ai.json b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp.ai.json new file mode 100644 index 0000000000..6a20d86847 --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp.ai.json @@ -0,0 +1,454 @@ +{ + "args": [ + { + "lineno": 44, + "name": "ctx" + }, + { + "lineno": 54, + "name": "ctx" + }, + { + "lineno": 63, + "name": "ctx" + }, + { + "lineno": 72, + "name": "ctx" + }, + { + "lineno": 110, + "name": "ctx" + }, + { + "lineno": 110, + "name": "account" + }, + { + "lineno": 110, + "name": "dest" + }, + { + "lineno": 110, + "name": "amount" + }, + { + "lineno": 157, + "name": "ctx" + }, + { + "lineno": 157, + "name": "account" + }, + { + "lineno": 157, + "name": "dest" + }, + { + "lineno": 157, + "name": "amount" + }, + { + "lineno": 200, + "name": "ctx" + }, + { + "lineno": 236, + "name": "view" + }, + { + "lineno": 236, + "name": "issuer" + }, + { + "lineno": 236, + "name": "sender" + }, + { + "lineno": 236, + "name": "amount" + }, + { + "lineno": 236, + "name": "journal" + }, + { + "lineno": 247, + "name": "view" + }, + { + "lineno": 247, + "name": "issuer" + }, + { + "lineno": 247, + "name": "sender" + }, + { + "lineno": 247, + "name": "amount" + }, + { + "lineno": 247, + "name": "journal" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "EscrowCreate::preflight", + "escrowCreatePreflightHelper or escrowCreatePreflightHelper (via std::visit)", + "isXRP", + "amount.native()", + "amount <= beast::zero", + "badCurrency() == amount.get().currency", + "ctx.rules.enabled(featureMPTokensV1)", + "amount.mpt() > MPTAmount{maxMPTokenAmount}" + ], + "entry_point": "EscrowCreate::preflight", + "purpose": "Validates the EscrowCreate transaction before it is processed. Ensures the amount, currency, and feature flags are correct, and that required fields are present and logically consistent.", + "validation_points": [ + "EscrowCreate::preflight: Checks isXRP(amount), featureTokenEscrow, amount <= beast::zero, presence and order of sfCancelAfter/sfFinishAfter", + "escrowCreatePreflightHelper: Checks amount.native(), amount <= beast::zero, badCurrency()", + "escrowCreatePreflightHelper: Checks featureMPTokensV1, amount.native(), amount.mpt() > maxMPTokenAmount, amount <= beast::zero" + ] + }, + { + "call_chain": [ + "EscrowCreate::doApply", + "escrowLockApplyHelper or escrowLockApplyHelper (not shown in snippet, but implied by naming and pattern)" + ], + "entry_point": "EscrowCreate::doApply", + "purpose": "Applies the escrow creation to the ledger after preflight/preclaim checks pass.", + "validation_points": [ + "Relies on preflight/preclaim having validated the transaction; doApply itself is not shown to do further validation in this snippet." + ] + } + ], + "data_flows": [ + { + "field": "sfAmount", + "flow": [ + "ctx.tx[sfAmount]", + "amount (local variable in preflight and helpers)", + "isXRP(amount) or amount.asset().value()", + "escrowCreatePreflightHelper(ctx)", + "Validation checks (amount.native(), amount <= beast::zero, badCurrency(), amount.mpt() > maxMPTokenAmount)", + "Used in makeTxConsequences, doApply" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Casted to STAmount", + "Checked for native/XRP or MPT/Token", + "Compared to zero", + "Currency extracted and compared" + ], + "validated_at": "EscrowCreate::preflight, escrowCreatePreflightHelper, escrowCreatePreflightHelper" + }, + { + "field": "sfCancelAfter / sfFinishAfter", + "flow": [ + "ctx.tx[~sfCancelAfter], ctx.tx[~sfFinishAfter]", + "Checked for presence in preflight", + "Compared for logical order (cancel > finish)" + ], + "origin": "ctx.tx[~sfCancelAfter], ctx.tx[~sfFinishAfter] (transaction input, optional fields)", + "transformations": [ + "Optional field extraction", + "Comparison for logical consistency" + ], + "validated_at": "EscrowCreate::preflight" + }, + { + "field": "sfAmount.currency", + "flow": [ + "ctx.tx[sfAmount]", + "amount.get().currency", + "Compared to badCurrency()" + ], + "origin": "amount.get().currency", + "transformations": [ + "Extraction from STAmount", + "Comparison" + ], + "validated_at": "escrowCreatePreflightHelper" + }, + { + "field": "feature flags (featureTokenEscrow, featureMPTokensV1)", + "flow": [ + "ctx.rules.enabled(featureTokenEscrow)", + "ctx.rules.enabled(featureMPTokensV1)" + ], + "origin": "ctx.rules.enabled(feature...)", + "transformations": [ + "Boolean gating of code paths" + ], + "validated_at": "EscrowCreate::preflight, escrowCreatePreflightHelper" + } + ], + "description": "Implements the logic for the EscrowCreate transaction in the XRP Ledger, including preflight checks, preclaim checks, and application of the transaction to the ledger. Handles both XRP and token escrows, with validation for conditions, timeouts, and account permissions.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "isXRP(amount) at EscrowCreate::makeTxConsequences", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "isXRP(amount) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "type", + "validation", + "check" + ], + "evidence": "isXRP(amount) at EscrowCreate::makeTxConsequences", + "issue_pattern": "Missing type validation for sfAmount", + "why_false_positive": "isXRP(amount) validates sfAmount type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "amount.native() || amount <= beast::zero at escrowCreatePreflightHelper", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "amount.native() || amount <= beast::zero validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount.currency", + "empty", + "string", + "validation" + ], + "evidence": "badCurrency() == amount.get().currency at escrowCreatePreflightHelper", + "issue_pattern": "Missing empty string validation for sfAmount.currency", + "why_false_positive": "badCurrency() == amount.get().currency validates sfAmount.currency for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "!ctx.rules.enabled(featureMPTokensV1) at escrowCreatePreflightHelper", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "!ctx.rules.enabled(featureMPTokensV1) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || amount <= beast::zero at escrowCreatePreflightHelper", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || amount <= beast::zero validates sfAmount for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 44, + "name": "EscrowCreate::makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 54, + "name": "escrowCreatePreflightHelper" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 63, + "name": "escrowCreatePreflightHelper" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 72, + "name": "EscrowCreate::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx", + "AccountID const& account", + "AccountID const& dest", + "STAmount const& amount" + ], + "lineno": 110, + "name": "escrowCreatePreclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx", + "AccountID const& account", + "AccountID const& dest", + "STAmount const& amount" + ], + "lineno": 157, + "name": "escrowCreatePreclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 200, + "name": "EscrowCreate::preclaim" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& issuer", + "AccountID const& sender", + "STAmount const& amount", + "beast::Journal journal" + ], + "lineno": 236, + "name": "escrowLockApplyHelper" + }, + { + "args": [ + "ApplyView& view", + "AccountID const& issuer", + "AccountID const& sender", + "STAmount const& amount", + "beast::Journal journal" + ], + "lineno": 247, + "name": "escrowLockApplyHelper" + }, + { + "args": [], + "lineno": 257, + "name": "EscrowCreate::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 24, + "name": "xrpl" + } + ], + "test_coverage_notes": "EscrowCreate transaction validation is typically covered in unit/integration tests under the rippled codebase, often in files like 'Escrow_test.cpp', 'TxEscrow_test.cpp', or similar. These tests usually cover valid and invalid amounts, missing/invalid fields, feature flag gating, and logical errors in time fields. However, coverage gaps may exist for edge cases involving new token types (MPTIssue), feature flag transitions, and maximum token amount boundaries. Tests for badCurrency() and negative/zero amounts are likely present, but tests for all combinations of optional fields (sfCancelAfter/sfFinishAfter) and feature flag permutations should be reviewed for completeness.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in C++ (xrpl codebase)", + "validation_layer": "business_logic (preflight checks before transaction application)" + }, + "validations": [ + { + "confidence": 0.7, + "error_thrown": "none (used for consequences, not erroring)", + "field": "sfAmount", + "location": "EscrowCreate::makeTxConsequences", + "validated_by": "isXRP(amount)", + "validates": [ + "Checks if the amount is XRP (native) or not" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "escrowCreatePreflightHelper", + "validated_by": "amount.native() || amount <= beast::zero", + "validates": [ + "Checks that amount is not native (not XRP)", + "Checks that amount is greater than zero" + ], + "validation_type": "type|range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_CURRENCY", + "field": "sfAmount.currency", + "location": "escrowCreatePreflightHelper", + "validated_by": "badCurrency() == amount.get().currency", + "validates": [ + "Checks that the currency is not a bad currency (e.g., illegal or disallowed currency code)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temDISABLED", + "field": "sfAmount", + "location": "escrowCreatePreflightHelper", + "validated_by": "!ctx.rules.enabled(featureMPTokensV1)", + "validates": [ + "Checks that the MPTokensV1 feature is enabled before allowing MPTIssue" + ], + "validation_type": "business_logic|feature_flag" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "escrowCreatePreflightHelper", + "validated_by": "amount.native() || amount.mpt() > MPTAmount{maxMPTokenAmount} || amount <= beast::zero", + "validates": [ + "Checks that amount is not native (not XRP)", + "Checks that MPT amount does not exceed maxMPTokenAmount", + "Checks that amount is greater than zero" + ], + "validation_type": "type|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp.ai.md b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp.ai.md new file mode 100644 index 0000000000..9482f1dcb0 --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp.ai.md @@ -0,0 +1,60 @@ +# `EscrowCreate.cpp` — Escrow Creation Transactor + +## Role in the System + +`EscrowCreate.cpp` implements the ledger transaction that locks funds in a conditional escrow object. It is one of three escrow transactors (`EscrowCreate`, `EscrowFinish`, `EscrowCancel`) and is responsible for the full three-phase lifecycle of creating an escrow: stateless validation (`preflight`), state-dependent validation (`preclaim`), and ledger mutation (`doApply`). The file originally handled only XRP escrows; it has since been extended under the `featureTokenEscrow` amendment to support IOU trustline amounts and MPT (Multi-Purpose Token) amounts, each with distinct locking semantics. + +## Asset Type Dispatch Pattern + +The file uses explicit template specializations for `Issue` (IOU/trustline) and `MPTIssue` (MPT) types rather than if/else branching. Three helper families follow this pattern: `escrowCreatePreflightHelper`, `escrowCreatePreclaimHelper`, and `escrowLockApplyHelper`. Each is declared as a template and then specialized for each asset type. The caller dispatches via `std::visit` over the `STAmount::asset().value()` variant, so the compiler selects the correct specialization at the visit site. This avoids virtual dispatch overhead and keeps each asset type's validation logic self-contained without mixing them in a shared branch tree. + +## Preflight: Stateless Validation + +`EscrowCreate::preflight` validates the transaction without touching the ledger. For XRP amounts, it simply requires a positive value. For non-XRP amounts, it first checks that the `featureTokenEscrow` amendment is enabled — without it the field is rejected outright — then dispatches to the appropriate template specialization. + +The `Issue` specialization (`escrowCreatePreflightHelper`) rejects native amounts (the `amount.native()` guard prevents XRP slipping through), zero or negative values, and the bad-currency sentinel. The `MPTIssue` specialization additionally requires `featureMPTokensV1` to be active and enforces the `maxMPTokenAmount` ceiling on the raw MPT balance. + +Three temporal invariants are enforced regardless of asset type: + +- At least one of `sfCancelAfter` or `sfFinishAfter` must be present — an escrow with neither has no defined lifecycle. +- When both are present, `sfCancelAfter` must be strictly after `sfFinishAfter`, preventing a logically backwards escrow that could never be finished. +- When `sfFinishAfter` is absent, a crypto-condition (`sfCondition`) must be present. Without this guard, an escrow with only a cancel time could be finished immediately upon creation, which is likely a user mistake. + +When `sfCondition` is present, its bytes are deserialized and validated with `Condition::deserialize`, returning `temMALFORMED` on any parse error. This early rejection avoids persisting an escrow whose condition can never be evaluated. + +## Preclaim: State-Dependent Validation + +`EscrowCreate::preclaim` reads ledger state to validate against it. The first check — independent of asset type — is that the destination account exists and is not a pseudo-account. The pseudo-account guard is explicitly noted as not needing its own amendment gate, because the conditions that would make a pseudo-account a valid destination are themselves gated. + +For IOU escrows, `escrowCreatePreclaimHelper` enforces several constraints: + +- The issuer must not be the same as the sender. An issuer escrowing their own tokens makes no sense economically and would create a bookkeeping anomaly. +- The issuer must have the `lsfAllowTrustLineLocking` flag set. Token escrow is opt-in for issuers. +- The sender must hold a trust line to the issuer, and the trust line balance polarity must match the address ordering convention (`balance > 0` implies `issuer > account`, `balance < 0` implies `issuer < account`). This mirrors the XRP Ledger's internal trust line representation. +- Both the sender and destination must be authorized if the issuer requires authorization. +- Neither account can be frozen. +- The sender's spendable balance (computed with `accountHolds` using `fhIGNORE_FREEZE` to get the actual balance) must cover the requested amount. +- A `canAdd` precision check guards against precision loss that could occur when the amount is added back during finish. + +For MPT escrows, `escrowCreatePreclaimHelper` follows the same structure but with MPT-specific rules: the `MPTokenIssuance` object must exist and carry the `lsfMPTCanEscrow` flag, the sender must hold an `MPToken` object for this issuance, and freeze violations return `tecLOCKED` rather than `tecFROZEN` — a deliberate distinction in error codes between IOU and MPT freeze states. + +## `doApply`: Ledger Mutation + +`doApply` begins by re-checking both `sfCancelAfter` and `sfFinishAfter` against the current `parentCloseTime`. This guard is not redundant: a transaction that passed preflight may arrive in a ledger close where the expiry has already passed, and it must be rejected rather than creating an immediately-expired escrow. + +Reserve and balance checks come next. The sender's XRP balance must cover the reserve for one additional ledger object, and for XRP amounts must also cover the escrowed value itself. + +The escrow SLE is constructed with fields copied directly from the transaction. Two conditional fields are notable: + +- Under `fixIncludeKeyletFields`, the `sfSequence` field is written to the escrow SLE. This allows other transactions (notably `EscrowFinish` and `EscrowCancel`) to reconstruct the escrow's keylet directly from the SLE without needing external input, and is part of a broader cross-ledger object navigation improvement. +- Under `featureTokenEscrow` for non-XRP amounts, the transfer rate is captured at creation time into `sfTransferRate` if it differs from parity. Snapshotting the rate at creation is deliberate: IOU transfer rates can change after the escrow is created, but the sender committed to the escrowed amount under the rate in effect at that moment. The finish operation later reads this stored rate to calculate the correct delivery amount. + +### Owner Directory Tracking + +The escrow keylet is inserted into the sender's owner directory unconditionally, and into the destination's owner directory if the sender and destination differ. For IOU escrows (not MPT), the keylet is also inserted into the issuer's owner directory. The comment explains the asymmetry: locked IOU funds are moved to the issuer during creation (via `directSendNoFee`), so the issuer holds a liability that needs tracking. MPT escrows do not require this because the `MPTokenIssuance` object directly tracks the total locked supply in its own fields, making an additional directory entry unnecessary. + +### Locking the Funds + +XRP is locked by subtracting directly from `sfBalance` on the sender's account root. IOU tokens are locked by calling `directSendNoFee` to transfer the amount from the sender back to the issuer — conceptually, the tokens are retired from circulation and will be re-issued when the escrow finishes. MPT tokens are locked via `lockEscrowMPT`, which moves the balance into a dedicated locked-amount field on the sender's `MPToken` object rather than transferring ownership, which is why no issuer directory entry is needed. + +Finally, `adjustOwnerCount` increments the sender's owner count by one, raising the XRP reserve requirement, and the modified account root SLE is committed with `ctx_.view().update(sle)`. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp.ai.json b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp.ai.json new file mode 100644 index 0000000000..30f7fedca5 --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp.ai.json @@ -0,0 +1,383 @@ +{ + "args": [ + { + "lineno": 19, + "name": "f" + }, + { + "lineno": 19, + "name": "c" + }, + { + "lineno": 32, + "name": "ctx" + }, + { + "lineno": 67, + "name": "view" + }, + { + "lineno": 67, + "name": "tx" + }, + { + "lineno": 81, + "name": "dest" + }, + { + "lineno": 81, + "name": "amount" + }, + { + "lineno": 81, + "name": "account" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "EscrowFinish::preflight" + ], + "entry_point": "EscrowFinish::preflight", + "purpose": "Initial transaction shape validation: checks that if sfCondition is present, sfFulfillment is also present (and vice versa).", + "validation_points": [ + "EscrowFinish::preflight: Validates presence/absence of sfCondition and sfFulfillment." + ] + }, + { + "call_chain": [ + "EscrowFinish::preflightSigValidated", + "checkCondition (if both sfCondition and sfFulfillment present)", + "credentials::checkFields" + ], + "entry_point": "EscrowFinish::preflightSigValidated", + "purpose": "Performs deeper validation after signature checks: validates condition/fulfillment pair, and credential fields.", + "validation_points": [ + "checkCondition: Validates that fulfillment matches condition (calls Condition::deserialize, Fulfillment::deserialize, validate).", + "credentials::checkFields: Validates credential fields." + ] + }, + { + "call_chain": [ + "EscrowFinish::doApply", + "escrowFinishPreclaimHelper or escrowFinishPreclaimHelper" + ], + "entry_point": "EscrowFinish::doApply", + "purpose": "Applies the transaction to the ledger, after all preflight/preclaim checks.", + "validation_points": [ + "escrowFinishPreclaimHelper: Validates destination authorization and frozen status (not directly related to sfCondition/sfFulfillment, but part of overall validation)." + ] + }, + { + "call_chain": [ + "EscrowFinish::checkExtraFeatures" + ], + "entry_point": "EscrowFinish::checkExtraFeatures", + "purpose": "Checks if credential features are enabled if sfCredentialIDs is present.", + "validation_points": [ + "EscrowFinish::checkExtraFeatures: Validates feature flag for credentials." + ] + } + ], + "data_flows": [ + { + "field": "sfCondition", + "flow": [ + "Transaction input", + "EscrowFinish::preflight (checked for presence)", + "EscrowFinish::preflightSigValidated (passed to checkCondition)", + "checkCondition (deserialized and validated)" + ], + "origin": "ctx.tx[~sfCondition] (transaction input)", + "transformations": [ + "Deserialized to Condition object in checkCondition" + ], + "validated_at": "EscrowFinish::preflight (presence), checkCondition (validity)" + }, + { + "field": "sfFulfillment", + "flow": [ + "Transaction input", + "EscrowFinish::preflight (checked for presence)", + "EscrowFinish::preflightSigValidated (passed to checkCondition)", + "checkCondition (deserialized and validated)" + ], + "origin": "ctx.tx[~sfFulfillment] (transaction input)", + "transformations": [ + "Deserialized to Fulfillment object in checkCondition" + ], + "validated_at": "EscrowFinish::preflight (presence), checkCondition (validity)" + }, + { + "field": "sfCredentialIDs", + "flow": [ + "Transaction input", + "EscrowFinish::checkExtraFeatures (checked for presence and feature flag)", + "EscrowFinish::preflightSigValidated (passed to credentials::checkFields)" + ], + "origin": "ctx.tx[~sfCredentialIDs] (transaction input)", + "transformations": [ + "Checked for presence, then validated by credentials::checkFields" + ], + "validated_at": "EscrowFinish::checkExtraFeatures, credentials::checkFields" + } + ], + "description": "Implements the EscrowFinish transaction logic for the XRPL, including preflight checks, condition/fulfillment validation, fee calculation, and ledger application for both XRP and token escrows.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCondition", + "validation", + "missing", + "check" + ], + "evidence": "Field sfCondition validated by Custom validation logic, cryptographic condition/fulfillment library, credentials helper", + "issue_pattern": "Missing validation for sfCondition", + "why_false_positive": "Custom validation logic, cryptographic condition/fulfillment library, credentials helper validates sfCondition automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfFulfillment", + "validation", + "missing", + "check" + ], + "evidence": "Field sfFulfillment validated by Custom validation logic, cryptographic condition/fulfillment library, credentials helper", + "issue_pattern": "Missing validation for sfFulfillment", + "why_false_positive": "Custom validation logic, cryptographic condition/fulfillment library, credentials helper validates sfFulfillment automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCredentialIDs", + "validation", + "missing", + "check" + ], + "evidence": "Field sfCredentialIDs validated by Custom validation logic, cryptographic condition/fulfillment library, credentials helper", + "issue_pattern": "Missing validation for sfCredentialIDs", + "why_false_positive": "Custom validation logic, cryptographic condition/fulfillment library, credentials helper validates sfCredentialIDs automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCondition and sfFulfillment", + "empty", + "string", + "validation" + ], + "evidence": "explicit logic at EscrowFinish::preflight", + "issue_pattern": "Missing empty string validation for sfCondition and sfFulfillment", + "why_false_positive": "explicit logic validates sfCondition and sfFulfillment for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCondition and sfFulfillment", + "empty", + "string", + "validation" + ], + "evidence": "checkCondition (calls Condition::deserialize, Fulfillment::deserialize, validate) at EscrowFinish::preflightSigValidated (via checkCondition)", + "issue_pattern": "Missing empty string validation for sfCondition and sfFulfillment", + "why_false_positive": "checkCondition (calls Condition::deserialize, Fulfillment::deserialize, validate) validates sfCondition and sfFulfillment for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Credential fields (e.g., sfCredentialIDs)", + "empty", + "string", + "validation" + ], + "evidence": "credentials::checkFields at EscrowFinish::preflightSigValidated", + "issue_pattern": "Missing empty string validation for Credential fields (e.g., sfCredentialIDs)", + "why_false_positive": "credentials::checkFields validates Credential fields (e.g., sfCredentialIDs) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCredentialIDs", + "empty", + "string", + "validation" + ], + "evidence": "explicit logic at EscrowFinish::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfCredentialIDs", + "why_false_positive": "explicit logic validates sfCredentialIDs for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp", + "functions": [ + { + "args": [ + "Slice f", + "Slice c" + ], + "lineno": 19, + "name": "checkCondition" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 32, + "name": "EscrowFinish::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 36, + "name": "EscrowFinish::preflight" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 46, + "name": "EscrowFinish::preflightSigValidated" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 67, + "name": "EscrowFinish::calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx", + "AccountID const& dest", + "STAmount const& amount" + ], + "lineno": 81, + "name": "escrowFinishPreclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx", + "AccountID const& dest", + "STAmount const& amount" + ], + "lineno": 98, + "name": "escrowFinishPreclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 117, + "name": "EscrowFinish::preclaim" + }, + { + "args": [], + "lineno": 143, + "name": "EscrowFinish::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + } + ], + "test_coverage_notes": "EscrowFinish transaction logic is typically tested in unit/integration tests under the transaction suite, e.g., 'test/tx/Escrow_test.cpp', 'test/tx/EscrowFinish_test.cpp', or similar. These tests usually cover: (1) presence/absence of sfCondition/sfFulfillment, (2) valid/invalid condition/fulfillment pairs, (3) credential field validation, (4) feature flag gating. Gaps may exist in edge cases for malformed credential fields, rare cryptographic condition types, or feature-flag transitions. Direct tests for credential feature gating may be limited if featureCredentials is new or experimental.", + "validation_architecture": { + "auto_validated_fields": [ + "sfCondition", + "sfFulfillment", + "sfCredentialIDs" + ], + "framework": "Custom validation logic, cryptographic condition/fulfillment library, credentials helper", + "validation_layer": "business_logic (preflight/preflightSigValidated functions)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfCondition and sfFulfillment", + "location": "EscrowFinish::preflight", + "validated_by": "explicit logic", + "validates": [ + "If one of sfCondition or sfFulfillment is present, the other must also be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none in preflightSigValidated (just sets router flags); deserialization may fail internally", + "field": "sfCondition and sfFulfillment", + "location": "EscrowFinish::preflightSigValidated (via checkCondition)", + "validated_by": "checkCondition (calls Condition::deserialize, Fulfillment::deserialize, validate)", + "validates": [ + "sfCondition is a valid serialized Condition", + "sfFulfillment is a valid serialized Fulfillment", + "Fulfillment matches Condition" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "error code returned (not tesSUCCESS)", + "field": "Credential fields (e.g., sfCredentialIDs)", + "location": "EscrowFinish::preflightSigValidated", + "validated_by": "credentials::checkFields", + "validates": [ + "Credential fields are present and valid according to credential rules" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 0.9, + "error_thrown": "none (returns bool)", + "field": "sfCredentialIDs", + "location": "EscrowFinish::checkExtraFeatures", + "validated_by": "explicit logic", + "validates": [ + "If sfCredentialIDs is present, featureCredentials must be enabled" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp.ai.md b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp.ai.md new file mode 100644 index 0000000000..bcb6b9858b --- /dev/null +++ b/src/libxrpl/tx/transactors/escrow/EscrowFinish.cpp.ai.md @@ -0,0 +1,41 @@ +# EscrowFinish.cpp + +`EscrowFinish.cpp` implements the transaction handler that releases escrowed funds to their intended destination on the XRP Ledger. It is the counterpart to `EscrowCreate` — where `EscrowCreate` locks value into a ledger object with attached time and/or crypto-condition constraints, `EscrowFinish` enforces those constraints and, when satisfied, transfers the locked value to the destination account and removes the escrow object from the ledger. + +## Transaction Lifecycle and Validation Phases + +`EscrowFinish` inherits from `Transactor` and participates in the standard four-phase processing pipeline: `preflight` → `preflightSigValidated` → `preclaim` → `doApply`. The split across these phases is architecturally deliberate. + +**`preflight`** performs only structural validation — if `sfCondition` is present without `sfFulfillment` or vice versa, the transaction is immediately rejected as `temMALFORMED`. Crypto-condition verification is intentionally deferred because it is computationally expensive and should only be performed after the transaction's signatures have been validated (which happens between `preflight` and `preflightSigValidated`). + +**`preflightSigValidated`** is where the actual crypto-condition check runs, via the static helper `checkCondition(Slice f, Slice c)`. This function deserializes both the `sfFulfillment` and `sfCondition` fields using the `cryptoconditions` library and verifies that the fulfillment satisfies the condition. Importantly, the result is cached in the `HashRouter` under two private flag bits — `SF_CF_VALID` (mapped to `PRIVATE6`) and `SF_CF_INVALID` (mapped to `PRIVATE5`). This means that if the same transaction is processed multiple times by different code paths during consensus, the expensive cryptographic check is only performed once; subsequent passes find the flags already set and skip straight to the conclusion. + +**`preclaim`** handles read-only ledger state checks that don't modify anything. For token escrows (gated on `featureTokenEscrow`), this phase fetches the escrow SLE and calls template-specialized helpers to verify that the destination is authorized to hold the asset and is not frozen or deep-frozen. The two specializations — `escrowFinishPreclaimHelper` for IOU/trust-line assets and `escrowFinishPreclaimHelper` for Multi-Purpose Tokens — handle the different authorization models of each asset type. MPTIssue uses `AuthType::WeakAuth`, reflecting a less strict authorization requirement, while IOU issues use the standard `requireAuth` check and also test for deep-freezing. + +## Fee Calculation as a DoS Defense + +`calculateBaseFee` charges extra for fulfillments according to the formula `base * (32 + size/16)`. This is a deliberate anti-abuse measure: crypto-condition validation is O(fulfillment size), so larger fulfillments translate directly into more computational work per validator. By making the fee scale with fulfillment size, the protocol discourages arbitrarily large fulfillment payloads. + +## The Apply Phase + +`doApply` assembles all the checks and performs the actual ledger mutations. Its logic proceeds in a carefully ordered sequence: + +1. **Time window enforcement** — The escrow's `sfFinishAfter` and `sfCancelAfter` times are checked against `parentCloseTime` of the current ledger header. If the finish time hasn't passed yet, or the cancel time has already passed, the transaction fails with `tecNO_PERMISSION`. Using `parentCloseTime` (rather than the current ledger's close time) is a standard XRPL pattern that ensures determinism across validators. + +2. **Crypto-condition re-check** — The code reads the `SF_CF_VALID`/`SF_CF_INVALID` flags from the `HashRouter`. The comment acknowledges the theoretical edge case: if the router's aged cache evicts the flags before `doApply` runs, the condition is re-evaluated inline. This path is marked `LCOV_EXCL_START` as it is essentially untestable in practice. After that, the condition stored *in the escrow SLE* (`sfCondition` on the `slep` object) is compared against the condition provided *in the transaction*. These must match exactly — the condition in the transaction is not trusted on its own; it must be the identical condition that was locked in at escrow creation time. + +3. **Deposit authorization** — `verifyDepositPreauth` checks whether the destination account requires deposit preauthorization and whether the transaction submitter (`account_`) is preauthorized. This prevents escrow payouts from bypassing deposit preauth settings that the destination may have set after the escrow was created. + +4. **Directory cleanup** — The escrow SLE is removed from the owner's directory, from the recipient's directory (if `sfDestinationNode` is present on the SLE — it may not be for older escrows created before the `DestinationTag` feature), and for token escrows, also from the issuer's directory. Failing to remove from any directory is a fatal ledger corruption (`tefBAD_LEDGER`). + +5. **Asset transfer** — For XRP, this is a direct balance increment on the destination account SLE. For non-XRP amounts, the call is dispatched via `std::visit` to `escrowUnlockApplyHelper` (defined in `EscrowHelpers.h`), which handles trust-line creation if needed, transfer rate calculation, and the actual credit. + +## Transfer Rate Semantics for Token Escrows + +Token escrows introduce a nuanced transfer-rate mechanic. When an escrow is created, the current transfer rate of the IOU issuer is recorded as `sfTransferRate` in the escrow SLE. At finish time, the *lower* of the locked rate and the current rate is applied. This asymmetry — always taking the more favorable rate from the recipient's perspective — protects against issuers raising their transfer rate after an escrow is committed. The deduction is also taken from the escrowed amount rather than added on top, which differs from normal payment semantics and is explicitly noted in `EscrowHelpers.h`. + +The `createAsset` boolean in `escrowUnlockApplyHelper` signals whether the account submitting the finish transaction is itself the destination. When `destID == account_`, the helper is permitted to auto-create trust lines or MPToken entries on behalf of that account, funded by the recipient's XRP reserve. When a third party finishes the escrow (e.g., a bot or the original sender claiming back after expiry), auto-creation is blocked to prevent the finisher from modifying another account's trust line state. + +## Feature Gating + +The file makes clean use of `featureTokenEscrow` and `featureCredentials` guards throughout. XRP escrows work unconditionally; token escrow paths are entirely off unless `featureTokenEscrow` is enabled. Similarly, `sfCredentialIDs` support (validated via `credentials::checkFields` and `credentials::valid`) is guarded by `featureCredentials`. `checkExtraFeatures` enforces this gate at the preflight level — returning false causes the transaction to be rejected before any deeper processing if the required amendment is not yet active. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LendingHelpers.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LendingHelpers.cpp.ai.json new file mode 100644 index 0000000000..3905d560d9 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LendingHelpers.cpp.ai.json @@ -0,0 +1,1143 @@ +{ + "args": [ + { + "lineno": 6, + "name": "ctx" + }, + { + "lineno": 11, + "name": "other" + }, + { + "lineno": 39, + "name": "interestRate" + }, + { + "lineno": 39, + "name": "paymentInterval" + }, + { + "lineno": 51, + "name": "asset" + }, + { + "lineno": 51, + "name": "value" + }, + { + "lineno": 51, + "name": "scale" + }, + { + "lineno": 72, + "name": "periodicRate" + }, + { + "lineno": 72, + "name": "paymentsRemaining" + }, + { + "lineno": 129, + "name": "interest" + }, + { + "lineno": 129, + "name": "managementFeeRate" + }, + { + "lineno": 129, + "name": "loanScale" + }, + { + "lineno": 143, + "name": "principalOutstanding" + }, + { + "lineno": 143, + "name": "lateInterestRate" + }, + { + "lineno": 143, + "name": "parentCloseTime" + }, + { + "lineno": 143, + "name": "nextPaymentDueDate" + }, + { + "lineno": 167, + "name": "startDate" + }, + { + "lineno": 167, + "name": "prevPaymentDate" + }, + { + "lineno": 191, + "name": "NumberProxy" + }, + { + "lineno": 191, + "name": "UInt32Proxy" + }, + { + "lineno": 191, + "name": "UInt32OptionalProxy" + }, + { + "lineno": 266, + "name": "overpaymentComponents" + }, + { + "lineno": 266, + "name": "roundedOldState" + }, + { + "lineno": 266, + "name": "periodicPayment" + }, + { + "lineno": 266, + "name": "periodicRate" + }, + { + "lineno": 266, + "name": "paymentRemaining" + }, + { + "lineno": 266, + "name": "j" + }, + { + "lineno": 340, + "name": "totalValueOutstandingProxy" + }, + { + "lineno": 340, + "name": "principalOutstandingProxy" + }, + { + "lineno": 340, + "name": "managementFeeOutstandingProxy" + }, + { + "lineno": 340, + "name": "periodicPaymentProxy" + }, + { + "lineno": 340, + "name": "paymentRemainingProxy" + }, + { + "lineno": 393, + "name": "view" + }, + { + "lineno": 393, + "name": "nextDueDate" + }, + { + "lineno": 393, + "name": "periodic" + }, + { + "lineno": 393, + "name": "latePaymentFee" + }, + { + "lineno": 393, + "name": "amount" + }, + { + "lineno": 453, + "name": "closeInterestRate" + }, + { + "lineno": 453, + "name": "totalInterestOutstanding" + }, + { + "lineno": 453, + "name": "closePaymentFee" + }, + { + "lineno": 517, + "name": "PaymentComponents" + }, + { + "lineno": 526, + "name": "totalValueOutstanding" + }, + { + "lineno": 526, + "name": "principalOutstanding" + }, + { + "lineno": 526, + "name": "managementFeeOutstanding" + }, + { + "lineno": 526, + "name": "periodicPayment" + }, + { + "lineno": 526, + "name": "periodicRate" + }, + { + "lineno": 526, + "name": "paymentRemaining" + }, + { + "lineno": 526, + "name": "managementFeeRate" + }, + { + "lineno": 617, + "name": "overpayment" + }, + { + "lineno": 617, + "name": "overpaymentInterestRate" + }, + { + "lineno": 617, + "name": "overpaymentFeeRate" + }, + { + "lineno": 661, + "name": "lhs" + }, + { + "lineno": 661, + "name": "rhs" + }, + { + "lineno": 687, + "name": "vaultAsset" + }, + { + "lineno": 687, + "name": "principalRequested" + }, + { + "lineno": 687, + "name": "expectInterest" + }, + { + "lineno": 687, + "name": "paymentTotal" + }, + { + "lineno": 687, + "name": "properties" + }, + { + "lineno": 735, + "name": "theoreticalPrincipalOutstanding" + }, + { + "lineno": 735, + "name": "periodicRate" + }, + { + "lineno": 735, + "name": "parentCloseTime" + }, + { + "lineno": 735, + "name": "paymentInterval" + }, + { + "lineno": 735, + "name": "prevPaymentDate" + }, + { + "lineno": 735, + "name": "startDate" + }, + { + "lineno": 735, + "name": "closeInterestRate" + }, + { + "lineno": 759, + "name": "periodicPayment" + }, + { + "lineno": 759, + "name": "periodicRate" + }, + { + "lineno": 759, + "name": "paymentRemaining" + }, + { + "lineno": 759, + "name": "managementFeeRate" + }, + { + "lineno": 792, + "name": "totalValueOutstanding" + }, + { + "lineno": 792, + "name": "principalOutstanding" + }, + { + "lineno": 792, + "name": "managementFeeOutstanding" + }, + { + "lineno": 805, + "name": "loan" + }, + { + "lineno": 818, + "name": "value" + }, + { + "lineno": 818, + "name": "managementFeeRate" + }, + { + "lineno": 818, + "name": "scale" + }, + { + "lineno": 829, + "name": "interestRate" + }, + { + "lineno": 829, + "name": "paymentInterval" + }, + { + "lineno": 829, + "name": "paymentsRemaining" + }, + { + "lineno": 829, + "name": "managementFeeRate" + }, + { + "lineno": 829, + "name": "minimumScale" + }, + { + "lineno": 849, + "name": "principalOutstanding" + }, + { + "lineno": 849, + "name": "periodicRate" + }, + { + "lineno": 849, + "name": "paymentsRemaining" + }, + { + "lineno": 849, + "name": "managementFeeRate" + }, + { + "lineno": 849, + "name": "minimumScale" + }, + { + "lineno": 900, + "name": "asset" + }, + { + "lineno": 900, + "name": "view" + }, + { + "lineno": 900, + "name": "loan" + }, + { + "lineno": 900, + "name": "brokerSle" + }, + { + "lineno": 900, + "name": "amount" + }, + { + "lineno": 900, + "name": "paymentType" + }, + { + "lineno": 900, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "checkLendingProtocolDependencies", + "ctx.rules.enabled(featureSingleAssetVault)", + "VaultCreate::checkExtraFeatures(ctx)" + ], + "entry_point": "checkLendingProtocolDependencies", + "purpose": "Checks if lending protocol dependencies are enabled before proceeding with lending operations.", + "validation_points": [ + "ctx.rules.enabled(featureSingleAssetVault)", + "VaultCreate::checkExtraFeatures(ctx)" + ] + }, + { + "call_chain": [ + "LoanPaymentParts::operator+=", + "XRPL_ASSERT (other.principalPaid >= beast::zero)", + "XRPL_ASSERT (other.interestPaid >= beast::zero)", + "XRPL_ASSERT (other.feePaid >= beast::zero)" + ], + "entry_point": "LoanPaymentParts::operator+=", + "purpose": "Adds payment parts together, ensuring all components are non-negative before addition.", + "validation_points": [ + "XRPL_ASSERT (other.principalPaid >= beast::zero)", + "XRPL_ASSERT (other.interestPaid >= beast::zero)", + "XRPL_ASSERT (other.feePaid >= beast::zero)" + ] + }, + { + "call_chain": [ + "LoanStateDeltas::nonNegative", + "if (principal < beast::zero) principal = numZero", + "if (interest < beast::zero) interest = numZero", + "if (managementFee < beast::zero) managementFee = numZero" + ], + "entry_point": "LoanStateDeltas::nonNegative", + "purpose": "Ensures that all loan state delta fields are non-negative by clamping negatives to zero.", + "validation_points": [ + "principal < beast::zero", + "interest < beast::zero", + "managementFee < beast::zero" + ] + } + ], + "data_flows": [ + { + "field": "other.principalPaid", + "flow": [ + "LoanPaymentParts (input param)", + "XRPL_ASSERT (>= beast::zero)", + "principalPaid += other.principalPaid" + ], + "origin": "LoanPaymentParts input to operator+=", + "transformations": [ + "Validated as non-negative", + "Added to this.principalPaid" + ], + "validated_at": "XRPL_ASSERT in LoanPaymentParts::operator+=" + }, + { + "field": "other.interestPaid", + "flow": [ + "LoanPaymentParts (input param)", + "XRPL_ASSERT (>= beast::zero)", + "interestPaid += other.interestPaid" + ], + "origin": "LoanPaymentParts input to operator+=", + "transformations": [ + "Validated as non-negative", + "Added to this.interestPaid" + ], + "validated_at": "XRPL_ASSERT in LoanPaymentParts::operator+=" + }, + { + "field": "other.feePaid", + "flow": [ + "LoanPaymentParts (input param)", + "XRPL_ASSERT (>= beast::zero)", + "feePaid += other.feePaid" + ], + "origin": "LoanPaymentParts input to operator+=", + "transformations": [ + "Validated as non-negative", + "Added to this.feePaid" + ], + "validated_at": "XRPL_ASSERT in LoanPaymentParts::operator+=" + }, + { + "field": "ctx.rules", + "flow": [ + "PreflightContext", + "ctx.rules.enabled(featureSingleAssetVault)", + "VaultCreate::checkExtraFeatures(ctx)" + ], + "origin": "PreflightContext passed to checkLendingProtocolDependencies", + "transformations": [ + "Checked for feature enablement", + "Passed to VaultCreate for further validation" + ], + "validated_at": "checkLendingProtocolDependencies" + }, + { + "field": "LoanStateDeltas.principal/interest/managementFee", + "flow": [ + "LoanStateDeltas fields", + "if < beast::zero, set to numZero" + ], + "origin": "LoanStateDeltas object", + "transformations": [ + "Clamped to zero if negative" + ], + "validated_at": "LoanStateDeltas::nonNegative" + } + ], + "description": "This file implements core lending and loan amortization logic for the XRPL (XRP Ledger) lending protocol, including payment processing, overpayment handling, late/full payment calculations, loan state management, and related mathematical helpers. It closely follows the XLS-66 specification for loan amortization and payment breakdowns.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "other.principalPaid", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at LoanPaymentParts::operator+=", + "issue_pattern": "Missing empty string validation for other.principalPaid", + "why_false_positive": "XRPL_ASSERT validates other.principalPaid for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "other.principalPaid", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at LoanPaymentParts::operator+=", + "issue_pattern": "Missing range validation for other.principalPaid", + "why_false_positive": "XRPL_ASSERT validates other.principalPaid range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "other.interestPaid", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at LoanPaymentParts::operator+=", + "issue_pattern": "Missing empty string validation for other.interestPaid", + "why_false_positive": "XRPL_ASSERT validates other.interestPaid for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "other.interestPaid", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at LoanPaymentParts::operator+=", + "issue_pattern": "Missing range validation for other.interestPaid", + "why_false_positive": "XRPL_ASSERT validates other.interestPaid range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "other.feePaid", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at LoanPaymentParts::operator+=", + "issue_pattern": "Missing empty string validation for other.feePaid", + "why_false_positive": "XRPL_ASSERT validates other.feePaid for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "other.feePaid", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at LoanPaymentParts::operator+=", + "issue_pattern": "Missing range validation for other.feePaid", + "why_false_positive": "XRPL_ASSERT validates other.feePaid range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ctx.rules.enabled(featureSingleAssetVault)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled at checkLendingProtocolDependencies", + "issue_pattern": "Missing empty string validation for ctx.rules.enabled(featureSingleAssetVault)", + "why_false_positive": "ctx.rules.enabled validates ctx.rules.enabled(featureSingleAssetVault) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "VaultCreate::checkExtraFeatures(ctx)", + "empty", + "string", + "validation" + ], + "evidence": "VaultCreate::checkExtraFeatures at checkLendingProtocolDependencies", + "issue_pattern": "Missing empty string validation for VaultCreate::checkExtraFeatures(ctx)", + "why_false_positive": "VaultCreate::checkExtraFeatures validates VaultCreate::checkExtraFeatures(ctx) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "principal, interest, managementFee", + "empty", + "string", + "validation" + ], + "evidence": "if (field < beast::zero) at LoanStateDeltas::nonNegative", + "issue_pattern": "Missing empty string validation for principal, interest, managementFee", + "why_false_positive": "if (field < beast::zero) validates principal, interest, managementFee for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "principal, interest, managementFee", + "range", + "bounds", + "validation" + ], + "evidence": "if (field < beast::zero) at LoanStateDeltas::nonNegative", + "issue_pattern": "Missing range validation for principal, interest, managementFee", + "why_false_positive": "if (field < beast::zero) validates principal, interest, managementFee range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "value (rounded to scale)", + "empty", + "string", + "validation" + ], + "evidence": "isRounded (roundToAsset) at isRounded", + "issue_pattern": "Missing empty string validation for value (rounded to scale)", + "why_false_positive": "isRounded (roundToAsset) validates value (rounded to scale) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "value (rounded to scale)", + "format", + "validation", + "invalid" + ], + "evidence": "isRounded (roundToAsset) at isRounded", + "issue_pattern": "Missing format validation for value (rounded to scale)", + "why_false_positive": "isRounded (roundToAsset) validates value (rounded to scale) format" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LendingHelpers.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 6, + "name": "checkLendingProtocolDependencies" + }, + { + "args": [ + "LoanPaymentParts const& other" + ], + "lineno": 11, + "name": "operator+=" + }, + { + "args": [ + "LoanPaymentParts const& other" + ], + "lineno": 27, + "name": "operator==" + }, + { + "args": [ + "TenthBips32 interestRate", + "std::uint32_t paymentInterval" + ], + "lineno": 39, + "name": "loanPeriodicRate" + }, + { + "args": [ + "Asset const& asset", + "Number const& value", + "std::int32_t scale" + ], + "lineno": 51, + "name": "isRounded" + }, + { + "args": [], + "lineno": 61, + "name": "LoanStateDeltas::nonNegative" + }, + { + "args": [ + "Number const& periodicRate", + "std::uint32_t paymentsRemaining" + ], + "lineno": 72, + "name": "computeRaisedRate" + }, + { + "args": [ + "Number const& periodicRate", + "std::uint32_t paymentsRemaining" + ], + "lineno": 82, + "name": "computePaymentFactor" + }, + { + "args": [ + "Number const& principalOutstanding", + "Number const& periodicRate", + "std::uint32_t paymentsRemaining" + ], + "lineno": 99, + "name": "loanPeriodicPayment" + }, + { + "args": [ + "Number const& periodicPayment", + "Number const& periodicRate", + "std::uint32_t paymentsRemaining" + ], + "lineno": 114, + "name": "loanPrincipalFromPeriodicPayment" + }, + { + "args": [ + "Asset const& asset", + "Number const& interest", + "TenthBips16 managementFeeRate", + "std::int32_t loanScale" + ], + "lineno": 129, + "name": "computeInterestAndFeeParts" + }, + { + "args": [ + "Number const& principalOutstanding", + "TenthBips32 lateInterestRate", + "NetClock::time_point parentCloseTime", + "std::uint32_t nextPaymentDueDate" + ], + "lineno": 143, + "name": "loanLatePaymentInterest" + }, + { + "args": [ + "Number const& principalOutstanding", + "Number const& periodicRate", + "NetClock::time_point parentCloseTime", + "std::uint32_t startDate", + "std::uint32_t prevPaymentDate", + "std::uint32_t paymentInterval" + ], + "lineno": 167, + "name": "loanAccruedInterest" + }, + { + "args": [ + "ExtendedPaymentComponents const& payment", + "NumberProxy& totalValueOutstandingProxy", + "NumberProxy& principalOutstandingProxy", + "NumberProxy& managementFeeOutstandingProxy", + "UInt32Proxy& paymentRemainingProxy", + "UInt32Proxy& prevPaymentDateProxy", + "UInt32OptionalProxy& nextDueDateProxy", + "std::uint32_t paymentInterval" + ], + "lineno": 191, + "name": "doPayment" + }, + { + "args": [ + "Asset const& asset", + "std::int32_t loanScale", + "ExtendedPaymentComponents const& overpaymentComponents", + "LoanState const& roundedOldState", + "Number const& periodicPayment", + "Number const& periodicRate", + "std::uint32_t paymentRemaining", + "TenthBips16 const managementFeeRate", + "beast::Journal j" + ], + "lineno": 266, + "name": "tryOverpayment" + }, + { + "args": [ + "Asset const& asset", + "std::int32_t loanScale", + "ExtendedPaymentComponents const& overpaymentComponents", + "NumberProxy& totalValueOutstandingProxy", + "NumberProxy& principalOutstandingProxy", + "NumberProxy& managementFeeOutstandingProxy", + "NumberProxy& periodicPaymentProxy", + "Number const& periodicRate", + "std::uint32_t const paymentRemaining", + "TenthBips16 const managementFeeRate", + "beast::Journal j" + ], + "lineno": 340, + "name": "doOverpayment" + }, + { + "args": [ + "Asset const& asset", + "ApplyView const& view", + "Number const& principalOutstanding", + "std::int32_t nextDueDate", + "ExtendedPaymentComponents const& periodic", + "TenthBips32 lateInterestRate", + "std::int32_t loanScale", + "Number const& latePaymentFee", + "STAmount const& amount", + "TenthBips16 managementFeeRate", + "beast::Journal j" + ], + "lineno": 393, + "name": "computeLatePayment" + }, + { + "args": [ + "Asset const& asset", + "ApplyView& view", + "Number const& principalOutstanding", + "Number const& managementFeeOutstanding", + "Number const& periodicPayment", + "std::uint32_t paymentRemaining", + "std::uint32_t prevPaymentDate", + "std::uint32_t const startDate", + "std::uint32_t const paymentInterval", + "TenthBips32 const closeInterestRate", + "std::int32_t loanScale", + "Number const& totalInterestOutstanding", + "Number const& periodicRate", + "Number const& closePaymentFee", + "STAmount const& amount", + "TenthBips16 managementFeeRate", + "beast::Journal j" + ], + "lineno": 453, + "name": "computeFullPayment" + }, + { + "args": [], + "lineno": 517, + "name": "PaymentComponents::trackedInterestPart" + }, + { + "args": [ + "Asset const& asset", + "std::int32_t scale", + "Number const& totalValueOutstanding", + "Number const& principalOutstanding", + "Number const& managementFeeOutstanding", + "Number const& periodicPayment", + "Number const& periodicRate", + "std::uint32_t paymentRemaining", + "TenthBips16 managementFeeRate" + ], + "lineno": 526, + "name": "computePaymentComponents" + }, + { + "args": [ + "Asset const& asset", + "int32_t const loanScale", + "Number const& overpayment", + "TenthBips32 const overpaymentInterestRate", + "TenthBips32 const overpaymentFeeRate", + "TenthBips16 const managementFeeRate" + ], + "lineno": 617, + "name": "computeOverpaymentComponents" + }, + { + "args": [ + "LoanState const& lhs", + "LoanState const& rhs" + ], + "lineno": 661, + "name": "operator-" + }, + { + "args": [ + "LoanState const& lhs", + "detail::LoanStateDeltas const& rhs" + ], + "lineno": 669, + "name": "operator-" + }, + { + "args": [ + "LoanState const& lhs", + "detail::LoanStateDeltas const& rhs" + ], + "lineno": 678, + "name": "operator+" + }, + { + "args": [ + "Asset const& vaultAsset", + "Number const& principalRequested", + "bool expectInterest", + "std::uint32_t paymentTotal", + "LoanProperties const& properties", + "beast::Journal j" + ], + "lineno": 687, + "name": "checkLoanGuards" + }, + { + "args": [ + "Number const& theoreticalPrincipalOutstanding", + "Number const& periodicRate", + "NetClock::time_point parentCloseTime", + "std::uint32_t paymentInterval", + "std::uint32_t prevPaymentDate", + "std::uint32_t startDate", + "TenthBips32 closeInterestRate" + ], + "lineno": 735, + "name": "computeFullPaymentInterest" + }, + { + "args": [ + "Number const& periodicPayment", + "Number const& periodicRate", + "std::uint32_t const paymentRemaining", + "TenthBips32 const managementFeeRate" + ], + "lineno": 759, + "name": "computeTheoreticalLoanState" + }, + { + "args": [ + "Number const& totalValueOutstanding", + "Number const& principalOutstanding", + "Number const& managementFeeOutstanding" + ], + "lineno": 792, + "name": "constructLoanState" + }, + { + "args": [ + "SLE::const_ref loan" + ], + "lineno": 805, + "name": "constructRoundedLoanState" + }, + { + "args": [ + "Asset const& asset", + "Number const& value", + "TenthBips32 managementFeeRate", + "std::int32_t scale" + ], + "lineno": 818, + "name": "computeManagementFee" + }, + { + "args": [ + "Asset const& asset", + "Number const& principalOutstanding", + "TenthBips32 interestRate", + "std::uint32_t paymentInterval", + "std::uint32_t paymentsRemaining", + "TenthBips32 managementFeeRate", + "std::int32_t minimumScale" + ], + "lineno": 829, + "name": "computeLoanProperties" + }, + { + "args": [ + "Asset const& asset", + "Number const& principalOutstanding", + "Number const& periodicRate", + "std::uint32_t paymentsRemaining", + "TenthBips32 managementFeeRate", + "std::int32_t minimumScale" + ], + "lineno": 849, + "name": "computeLoanProperties" + }, + { + "args": [ + "Asset const& asset", + "ApplyView& view", + "SLE::ref loan", + "SLE::const_ref brokerSle", + "STAmount const& amount", + "LoanPaymentType const paymentType", + "beast::Journal j" + ], + "lineno": 900, + "name": "loanMakePayment" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 59, + "name": "detail" + } + ], + "test_coverage_notes": "The code is likely covered by unit tests for lending and vault creation logic, typically found in files like LendingHelpers_test.cpp, VaultCreate_test.cpp, or broader transaction preflight tests. However, direct validation of negative values for principalPaid, interestPaid, and feePaid in LoanPaymentParts::operator+= may not be exhaustively tested, especially for edge cases (e.g., zero values, large values, or type overflows). The dependency check (featureSingleAssetVault and VaultCreate::checkExtraFeatures) is likely tested in integration or feature-flag tests, but coverage for all rule combinations and error paths should be verified. There is no explicit mention of test files in the provided code, so test coverage for all validation branches should be confirmed.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT, custom logic, protocol rule checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "other.principalPaid", + "location": "LoanPaymentParts::operator+=", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that principalPaid is non-negative (>= 0)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "other.interestPaid", + "location": "LoanPaymentParts::operator+=", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that interestPaid is non-negative (>= 0)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "other.feePaid", + "location": "LoanPaymentParts::operator+=", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that feePaid is non-negative (>= 0)" + ], + "validation_type": "range" + }, + { + "confidence": 0.9, + "error_thrown": "Returns false (no exception)", + "field": "ctx.rules.enabled(featureSingleAssetVault)", + "location": "checkLendingProtocolDependencies", + "validated_by": "ctx.rules.enabled", + "validates": [ + "Checks if featureSingleAssetVault is enabled in protocol rules" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "Returns false (no exception)", + "field": "VaultCreate::checkExtraFeatures(ctx)", + "location": "checkLendingProtocolDependencies", + "validated_by": "VaultCreate::checkExtraFeatures", + "validates": [ + "Checks extra feature dependencies for VaultCreate" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Field is set to zero (no exception)", + "field": "principal, interest, managementFee", + "location": "LoanStateDeltas::nonNegative", + "validated_by": "if (field < beast::zero)", + "validates": [ + "Ensures principal, interest, and managementFee are not negative; sets to zero if negative" + ], + "validation_type": "range" + }, + { + "confidence": 0.9, + "error_thrown": "Returns boolean (no exception)", + "field": "value (rounded to scale)", + "location": "isRounded", + "validated_by": "isRounded (roundToAsset)", + "validates": [ + "Checks if value is already rounded to the specified scale for the asset" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LendingHelpers.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LendingHelpers.cpp.ai.md new file mode 100644 index 0000000000..5efad090a9 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LendingHelpers.cpp.ai.md @@ -0,0 +1,66 @@ +# `LendingHelpers.cpp` — XRPL Lending Protocol Amortization Engine + +This file is the numerical core of the XRPL lending protocol (XLS-66 specification). It implements every mathematical operation involved in a loan's life cycle: computing amortized periodic payments, splitting each payment into principal, interest, and management-fee components, handling late payments, early-closure ("full") payments, and overpayments that trigger re-amortization. The top-level entry point `loanMakePayment()` ties these together into the `make_payment` function defined in XLS-66 §3.2.4.4. + +## Amortization Foundations + +The file begins with a group of low-level formulas that map directly to numbered equations in the XLS-66 Equation Glossary (Section A-2): + +- `loanPeriodicRate()` (Eq. 1) converts an annualized rate in tenth-of-a-basis-point units (`TenthBips32`) to a per-payment-interval rate by prorating against `secondsInYear`. +- `computeRaisedRate()` (Eq. 5) computes `(1 + r)^n` and `computePaymentFactor()` (Eq. 6) derives the standard amortization factor `r(1+r)^n / ((1+r)^n - 1)`, handling the zero-interest special case cleanly. +- `loanPeriodicPayment()` (Eq. 7) applies the factor to produce the fixed installment amount; `loanPrincipalFromPeriodicPayment()` (Eq. 10) is the inverse, recovering what the principal should be given a periodic payment. + +The zero-interest path is handled explicitly throughout: when `periodicRate == 0`, equal principal slices replace the exponential formula, avoiding division-by-zero while keeping the same code path. + +## State Representation and the Theoretical/Rounded Split + +Two key design concerns permeate the entire file: _what the loan state should be_ at full mathematical precision (the **theoretical** state), and _what it actually is_ after rounding to the asset's representable scale (the **rounded/ledger** state). + +`LoanState` holds four fields — `valueOutstanding`, `principalOutstanding`, `interestDue`, `managementFeeDue` — with `interestDue` always derived from the others to guarantee consistency. `LoanProperties` bundles these with the periodic payment, the loan's rounding scale, and the first-payment principal (a canary value used to detect precision loss). + +`computeTheoreticalLoanState()` (XLS-66 §3.2.4.4, Eqs. 30–33) computes the loan state purely from the amortization schedule at full precision. `constructLoanState()` and `constructRoundedLoanState()` build `LoanState` from actual ledger field values. The difference between these two representations is the accumulated rounding error — a small but important number that must be carried forward whenever the loan is re-amortized (e.g., after an overpayment). + +## Payment Components: Tracked vs. Untracked + +`detail::PaymentComponents` represents the ledger-visible deltas: what will be subtracted from `sfTotalValueOutstanding`, `sfPrincipalOutstanding`, and `sfManagementFeeOutstanding` in the Loan ledger object. `detail::ExtendedPaymentComponents` extends this with two untracked amounts — `untrackedInterest` (late penalty interest paid directly to the vault) and `untrackedManagementFee` (service fees, late fees paid directly to the broker). The `totalDue` field is computed in the constructor as the sum, ensuring the borrower's check amount is always against a single authoritative figure. + +The outer `LoanPaymentParts` struct is what gets returned all the way up to `LoanPay`, summarising what the caller should actually move between accounts: `principalPaid` (to vault), `interestPaid` (to vault), `feePaid` (to broker), and `valueChange` (the sign indicating whether the loan's tracked value increased or decreased beyond normal schedule). + +## Computing Regular Payment Components + +`computePaymentComponents()` is responsible for splitting a single scheduled installment. Its algorithm avoids recomputing the formula from scratch; instead it asks "what should the loan state be after this payment?" by calling `computeTheoreticalLoanState(paymentRemaining - 1)` and taking the delta between the current ledger state and that target. This naturally absorbs accumulated rounding errors without explicit error-tracking. After computing deltas the function applies a series of caps (`std::min`) and the `addressExcess` lambda to ensure no component exceeds the available balance or the periodic payment. When `paymentRemaining == 1` or the total outstanding ≤ the payment, a final-payment path zeroes every tracked field, guaranteeing clean loan closure. + +## The Template Proxy Pattern + +`doPayment()` is templated on `NumberProxy`, `UInt32Proxy`, and `UInt32OptionalProxy`. This allows the same function to run against `ValueProxy` and `ValueProxy` objects (which write through to the actual Loan SLE) and against plain `Number`/`uint32_t` values used in simulation. The `XRPL_ASSERT_PARTS` calls then fire in both contexts, giving consistent validation whether running from the real transaction engine or a unit test. + +## Late Payments + +`computeLatePayment()` first verifies the due date has passed via `hasExpired()`; if not, it returns `tecTOO_SOON`. The penalty interest is computed by `loanLatePaymentInterest()` (Eq. 16), which calculates how many seconds the payment is overdue and calls `loanPeriodicRate()` with that interval. The penalty is then split into vault interest and broker fee using `computeInterestAndFeeParts()`, both parts added as untracked amounts on top of the regular periodic payment. The borrower's provided `amount` must cover `late.totalDue` or the transaction fails with `tecINSUFFICIENT_PAYMENT`. + +## Full (Early-Closure) Payments + +`computeFullPayment()` handles voluntarily paying off the loan before the final scheduled installment. It is explicitly disallowed when only one payment remains (which should follow the normal path). The function reverse-calculates the theoretical principal from `loanPrincipalFromPeriodicPayment()`, then computes two cost components via `computeFullPaymentInterest()`: accrued interest since the last payment (Eq. 27) and a prepayment penalty (Eq. 28). The `untrackedInterest` field in the resulting `ExtendedPaymentComponents` is set to `roundedFullInterest - totalInterestOutstanding`; this can be negative (early payoff saves more interest than the penalty costs) or positive (penalty exceeds the discounted interest), and the sign drives the `valueChange` field in the returned `LoanPaymentParts`. + +## Overpayments and Re-Amortization + +Overpayments are the most algorithmically complex case. When a borrower pays more than the periodic amount, the surplus reduces principal immediately, but because the remaining payment schedule was computed assuming a higher principal, the periodic payment must be recalculated — a process called re-amortization. + +`tryOverpayment()` handles this in a sandbox: it captures the accumulated rounding error (`roundedOldState - theoreticalState`), reduces the theoretical principal by the overpayment, calls `computeLoanProperties()` for the new schedule, adds the rounding errors back (`newTheoreticalState + errors`), then rounds all three components back to `loanScale` using conservative clamping. It validates the result with `checkLoanGuards()` and rejects if any invariant is violated — returning `Unexpected(tesSUCCESS)` (a deliberate non-error signal meaning "ignore the overpayment silently"). The principal must strictly decrease; if it doesn't, the overpayment is rejected. + +`doOverpayment()` wraps `tryOverpayment()` and, only after all validations pass, writes the new values through the proxy objects to the actual ledger. + +## Loan Guards and Precision Loss Detection + +`checkLoanGuards()` enforces four invariants at loan-creation and re-amortization time: + +1. If an interest rate is set, total interest must be a measurable positive number — otherwise the amortization table is meaningless. +2. The first-payment principal (pre-computed in `computeLoanProperties()`) must be positive at full precision. This prevents loans where the principal component rounds to zero every period, meaning principal would never actually be paid down. +3. The rounded periodic payment must not be zero. +4. The total value divided by the rounded payment (with upward rounding) must equal the scheduled payment count, ensuring the loan will actually complete in the specified number of installments. + +All four failures return `tecPRECISION_LOSS`, surfacing the precision constraint as a transaction error rather than a silent arithmetic drift. + +## `loanMakePayment()` Dispatch + +The top-level function reads all relevant loan fields via `ValueProxy` objects — which lazily back-propagate writes to the SLE — and branches on `LoanPaymentType` (regular, late, full, overpayment). The regular and overpayment paths share a `while` loop that processes up to `loanMaximumPaymentsPerTransaction` installments, accumulating results in `LoanPaymentParts`. The overpayment phase appends only if the remaining `amount - totalPaid` is positive, the loan flag `lsfLoanOverpayment` is set, and `trackedPrincipalDelta > 0` after fee deduction. Final `XRPL_ASSERT` calls at the exit verify that all returned amounts are rounded and non-negative, providing a last-resort consistency check across all code paths. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp.ai.json new file mode 100644 index 0000000000..8e1b64065b --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp.ai.json @@ -0,0 +1,536 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoanBrokerCoverClawback::preflight" + ], + "entry_point": "LoanBrokerCoverClawback::preflight", + "purpose": "Performs initial stateless validation of the transaction fields before further processing.", + "validation_points": [ + "LoanBrokerCoverClawback::preflight (validates sfLoanBrokerID, sfAmount, amount->native(), *amount < beast::zero, isLegalNet(*amount), amount->holds(), amount->getIssuer())" + ] + }, + { + "call_chain": [ + "LoanBrokerCoverClawback::preclaim", + "preclaimHelper or preclaimHelper" + ], + "entry_point": "LoanBrokerCoverClawback::preclaim", + "purpose": "Performs contextual validation after preflight, before applying the transaction.", + "validation_points": [ + "preclaimHelper or preclaimHelper (not shown in provided code, but likely further checks on fields)" + ] + }, + { + "call_chain": [ + "LoanBrokerCoverClawback::doApply", + "determineBrokerID", + "determineAsset", + "determineClawAmount" + ], + "entry_point": "LoanBrokerCoverClawback::doApply", + "purpose": "Applies the transaction, using validated and possibly transformed data.", + "validation_points": [ + "determineBrokerID (validates presence and correctness of brokerID, checks for pseudo-account and LoanBrokerID)", + "determineAsset (validates asset type and issuer, partial code shown)" + ] + }, + { + "call_chain": [ + "LoanBrokerCoverClawback::checkExtraFeatures", + "checkLendingProtocolDependencies" + ], + "entry_point": "LoanBrokerCoverClawback::checkExtraFeatures", + "purpose": "Checks for protocol feature dependencies before transaction processing.", + "validation_points": [ + "checkLendingProtocolDependencies (not shown, but likely validates protocol state)" + ] + } + ], + "data_flows": [ + { + "field": "sfLoanBrokerID", + "flow": [ + "ctx.tx[~sfLoanBrokerID]", + "LoanBrokerCoverClawback::preflight (checked for presence, zero value)", + "determineBrokerID (used if present, otherwise inferred from amount issuer)", + "doApply (used for transaction logic)" + ], + "origin": "ctx.tx[~sfLoanBrokerID] (transaction input)", + "transformations": [ + "Checked for presence", + "Checked for zero value", + "If absent, inferred from amount issuer via determineBrokerID" + ], + "validated_at": "LoanBrokerCoverClawback::preflight, determineBrokerID" + }, + { + "field": "sfAmount", + "flow": [ + "ctx.tx[~sfAmount]", + "LoanBrokerCoverClawback::preflight (checked for presence, type, value, legality)", + "determineBrokerID (used to infer brokerID if needed)", + "determineAsset (used to determine asset type and issuer)", + "doApply (used for transaction logic)" + ], + "origin": "ctx.tx[~sfAmount] (transaction input)", + "transformations": [ + "Checked for presence", + "Checked for native() (XRP) type", + "Checked for negative value", + "Checked for isLegalNet", + "Checked for holds", + "Issuer extracted for further logic" + ], + "validated_at": "LoanBrokerCoverClawback::preflight" + }, + { + "field": "amount->getIssuer()", + "flow": [ + "amount->getIssuer()", + "LoanBrokerCoverClawback::preflight (compared to sfAccount and beast::zero)", + "determineBrokerID (used to look up pseudo-account and LoanBrokerID)", + "determineAsset (used to determine asset type and holder)" + ], + "origin": "Extracted from sfAmount", + "transformations": [ + "Issuer extracted from amount", + "Used to lookup account in ledger", + "Used to infer brokerID if not provided" + ], + "validated_at": "LoanBrokerCoverClawback::preflight, determineBrokerID" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "LoanBrokerCoverClawback::preflight (compared to amount->getIssuer())", + "determineAsset (used to check asset ownership)" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Used for comparison with issuer" + ], + "validated_at": "LoanBrokerCoverClawback::preflight" + } + ], + "description": "Implements the LoanBrokerCoverClawback transaction logic for the XRPL lending protocol, including preflight, preclaim, and application logic for clawing back cover funds from a loan broker's pseudo-account.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID, sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at LoanBrokerCoverClawback::preflight", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID, sfAmount", + "why_false_positive": "explicit if-check validates sfLoanBrokerID, sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at LoanBrokerCoverClawback::preflight", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "explicit if-check validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "amount->native() at LoanBrokerCoverClawback::preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "amount->native() validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "*amount < beast::zero at LoanBrokerCoverClawback::preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "*amount < beast::zero validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "range", + "bounds", + "validation" + ], + "evidence": "*amount < beast::zero at LoanBrokerCoverClawback::preflight", + "issue_pattern": "Missing range validation for sfAmount", + "why_false_positive": "*amount < beast::zero validates sfAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "isLegalNet(*amount) at LoanBrokerCoverClawback::preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "isLegalNet(*amount) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount (when sfLoanBrokerID is not present)", + "empty", + "string", + "validation" + ], + "evidence": "amount->holds() at LoanBrokerCoverClawback::preflight", + "issue_pattern": "Missing empty string validation for sfAmount (when sfLoanBrokerID is not present)", + "why_false_positive": "amount->holds() validates sfAmount (when sfLoanBrokerID is not present) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount (when sfLoanBrokerID is not present)", + "type", + "validation", + "check" + ], + "evidence": "amount->holds() at LoanBrokerCoverClawback::preflight", + "issue_pattern": "Missing type validation for sfAmount (when sfLoanBrokerID is not present)", + "why_false_positive": "amount->holds() validates sfAmount (when sfLoanBrokerID is not present) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount.getIssuer()", + "empty", + "string", + "validation" + ], + "evidence": "holder == account || holder == beast::zero at LoanBrokerCoverClawback::preflight", + "issue_pattern": "Missing empty string validation for sfAmount.getIssuer()", + "why_false_positive": "holder == account || holder == beast::zero validates sfAmount.getIssuer() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Lending protocol dependencies", + "empty", + "string", + "validation" + ], + "evidence": "checkLendingProtocolDependencies(ctx) at LoanBrokerCoverClawback::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for Lending protocol dependencies", + "why_false_positive": "checkLendingProtocolDependencies(ctx) validates Lending protocol dependencies for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount (in determineBrokerID)", + "empty", + "string", + "validation" + ], + "evidence": "!dstAmount || !dstAmount->holds() at determineBrokerID", + "issue_pattern": "Missing empty string validation for sfAmount (in determineBrokerID)", + "why_false_positive": "!dstAmount || !dstAmount->holds() validates sfAmount (in determineBrokerID) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount (in determineBrokerID)", + "type", + "validation", + "check" + ], + "evidence": "!dstAmount || !dstAmount->holds() at determineBrokerID", + "issue_pattern": "Missing type validation for sfAmount (in determineBrokerID)", + "why_false_positive": "!dstAmount || !dstAmount->holds() validates sfAmount (in determineBrokerID) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "maybePseudo (issuer account existence)", + "empty", + "string", + "validation" + ], + "evidence": "!sle at determineBrokerID", + "issue_pattern": "Missing empty string validation for maybePseudo (issuer account existence)", + "why_false_positive": "!sle validates maybePseudo (issuer account existence) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "LoanBrokerCoverClawback::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 12, + "name": "LoanBrokerCoverClawback::preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 41, + "name": "determineBrokerID" + }, + { + "args": [ + "ReadView const& view", + "AccountID const& account", + "AccountID const& brokerPseudoAccountID", + "STAmount const& amount" + ], + "lineno": 67, + "name": "determineAsset" + }, + { + "args": [ + "SLE const& sleBroker", + "Asset const& vaultAsset", + "std::optional const& amount" + ], + "lineno": 89, + "name": "determineClawAmount" + }, + { + "args": [ + "PreclaimContext const& ctx", + "SLE const& sleIssuer", + "STAmount const& clawAmount" + ], + "lineno": 112, + "name": "preclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx", + "SLE const& sleIssuer", + "STAmount const& clawAmount" + ], + "lineno": 120, + "name": "preclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 134, + "name": "LoanBrokerCoverClawback::preclaim" + }, + { + "args": [], + "lineno": 196, + "name": "LoanBrokerCoverClawback::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic in preflight is likely covered by unit/integration tests for transaction preflight checks, especially for malformed or invalid transactions (e.g., missing fields, invalid amounts, wrong issuers). Test files may include those under 'test/tx/lending/' or similar directories, such as 'LoanBrokerCoverClawback_test.cpp' or generic transaction validation suites. However, some code paths (e.g., LCOV_EXCL_LINE for isLegalNet failure, tecINTERNAL in determineBrokerID) are marked as unreachable or redundant, suggesting they may not be directly tested. There may be gaps in coverage for edge cases where brokerID is inferred from the issuer, or for pseudo-accounts without LoanBrokerID. Full coverage would require tests for all validation branches, including negative and boundary cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation via explicit checks and helper functions (e.g., isLegalNet, checkLendingProtocolDependencies)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfLoanBrokerID, sfAmount", + "location": "LoanBrokerCoverClawback::preflight", + "validated_by": "explicit if-check", + "validates": [ + "At least one of sfLoanBrokerID or sfAmount must be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfLoanBrokerID", + "location": "LoanBrokerCoverClawback::preflight", + "validated_by": "explicit if-check", + "validates": [ + "sfLoanBrokerID must not be beast::zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "LoanBrokerCoverClawback::preflight", + "validated_by": "amount->native()", + "validates": [ + "sfAmount must not be native (XRP) for clawback" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "LoanBrokerCoverClawback::preflight", + "validated_by": "*amount < beast::zero", + "validates": [ + "sfAmount must not be negative" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "LoanBrokerCoverClawback::preflight", + "validated_by": "isLegalNet(*amount)", + "validates": [ + "sfAmount must be a legal net amount" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfAmount (when sfLoanBrokerID is not present)", + "location": "LoanBrokerCoverClawback::preflight", + "validated_by": "amount->holds()", + "validates": [ + "sfAmount must not hold MPTIssue if sfLoanBrokerID is not present" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfAmount.getIssuer()", + "location": "LoanBrokerCoverClawback::preflight", + "validated_by": "holder == account || holder == beast::zero", + "validates": [ + "Issuer of sfAmount must not be the same as sfAccount or beast::zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "unknown (depends on checkLendingProtocolDependencies)", + "field": "Lending protocol dependencies", + "location": "LoanBrokerCoverClawback::checkExtraFeatures", + "validated_by": "checkLendingProtocolDependencies(ctx)", + "validates": [ + "Lending protocol dependencies are satisfied" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "sfAmount (in determineBrokerID)", + "location": "determineBrokerID", + "validated_by": "!dstAmount || !dstAmount->holds()", + "validates": [ + "sfAmount must be present and must hold Issue type" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "Unexpect (incomplete in snippet)", + "field": "maybePseudo (issuer account existence)", + "location": "determineBrokerID", + "validated_by": "!sle", + "validates": [ + "Issuer account for sfAmount must exist in ledger" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp.ai.md new file mode 100644 index 0000000000..5de5b79d86 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverClawback.cpp.ai.md @@ -0,0 +1,54 @@ +# `LoanBrokerCoverClawback.cpp` + +## Role in the Lending Protocol + +The XRPL lending protocol (XLS-66) introduces a `LoanBroker` ledger object that acts as an intermediary between a single-asset vault and individual loans. Each broker maintains a "cover" pool — assets deposited into the broker's pseudo-account — that serves as a first-loss buffer absorbing borrower defaults before vault depositors are affected. `LoanBrokerCoverClawback.cpp` implements the transaction that lets the original asset issuer reclaim a portion of these cover funds, subject to a configurable minimum cover-to-debt ratio. + +This is the inverse of `LoanBrokerCoverDeposit`. Comparing the two files shows their structural symmetry: deposit increments `sfCoverAvailable` and sends funds *to* the pseudo-account; clawback decrements `sfCoverAvailable` and sends funds *from* the pseudo-account, using `WaiveTransferFee::Yes` in both directions. + +## Dual Identification Modes + +One of the more unusual design choices here is that the transaction has two ways to identify the target broker: + +1. **Explicit**: provide `sfLoanBrokerID` directly. +2. **Implicit**: provide `sfAmount` as an IOU where the `issuer` field encodes the broker's pseudo-account. The ledger-side `determineBrokerID()` helper resolves this by reading the pseudo-account SLE and extracting its `sfLoanBrokerID` field. + +This dual mode exists because IOU trust lines are bidirectional — both endpoints are simultaneously "issuer" and "holder" from the protocol's perspective. A user might naturally express "claw back 100 USD from the broker's pseudo-account" by constructing an IOU amount with the pseudo-account as the issuer, mimicking normal IOU clawback syntax. The implicit path accommodates that convention. `preflight` rejects the combination of implicit-mode with `MPTIssue`, since MPTs lack the trust-line issuer encoding that makes implicit resolution possible. + +The `determineAsset()` helper normalizes the resulting asset representation: whether the submitter specifies the IOU from their own account's perspective or from the pseudo-account's perspective, the function always returns an `Issue` with the submitting account as the issuer, matching the canonical vault asset representation used in comparisons against `sfAsset`. + +## Minimum Cover Enforcement + +`determineClawAmount()` is the financial heart of the file. It computes the maximum permissible withdrawal as: + +``` +minRequiredCover = tenthBipsOfValue(sfDebtTotal, sfCoverRateMinimum) [rounded UP] +maxClawAmount = sfCoverAvailable - minRequiredCover [rounded DOWN] +``` + +The deliberate asymmetric rounding — ceiling for the minimum required, floor for the remainder — ensures the ledger never allows cover to fall below the minimum ratio. `NumberRoundModeGuard` scopes the rounding mode changes, restoring the previous mode on exit. If the broker's cover is already at or below its minimum, `determineClawAmount` returns `tecINSUFFICIENT_FUNDS`. + +When `sfAmount` is absent or zero, the convention is "take all you can" — the function caps to `maxClawAmount`. When a specific amount is requested, it is silently capped to `maxClawAmount` rather than rejected; the submitter receives at most what the minimum-ratio floor allows, without having to know the exact number in advance. + +## Asset-Type-Specific Permission Checks + +`preclaim` uses a `std::visit` dispatch to call one of two template specialisations of `preclaimHelper`: + +- `preclaimHelper` enforces that the issuer account has `lsfAllowTrustLineClawback` set and does *not* have `lsfNoFreeze` set. This mirrors the standard IOU clawback permission model. +- `preclaimHelper` looks up the `MPTIssuance` SLE and checks `lsfMPTCanClawback`. It also asserts that the issuance's recorded `sfIssuer` matches the submitting account — a redundant internal consistency check marked `LCOV_EXCL_LINE`. + +XRP is explicitly blocked at both `preflight` (via `amount->native()`) and `preclaim` (via `vaultAsset.native()`), because native assets have no counterparty and therefore nobody can exercise a trust-line clawback. + +Only the vault asset's issuer is authorized to clawback. The check `vaultAsset.getIssuer() != account` in `preclaim` ensures this, short-circuiting before any asset-type dispatch. + +## Balance Invariant Check + +A notable defensive check in `preclaim` explicitly verifies that the broker pseudo-account's actual trust-line or MPT balance (via `accountHolds`) is at least as large as the computed `clawAmount`. Ordinarily `sfCoverAvailable` and the on-ledger balance should be perfectly synchronized by the deposit and withdraw paths. The check exists to detect any ledger corruption that might cause them to diverge. If they do, `tecINTERNAL` is returned rather than allowing the transaction to proceed on potentially inconsistent state. + +## `doApply` Execution + +After `preclaim` has validated everything, `doApply` is deliberately minimal. It re-runs `determineBrokerID` and `determineClawAmount` (since they operate on the live mutable view rather than the read-only preclaim view), decrements `sfCoverAvailable` on the broker SLE, calls `view().update()` to commit that change, invokes `associateAsset` to maintain the broker's asset-type metadata index, then calls `accountSend` to transfer funds from the pseudo-account to the submitting account with the transfer fee waived. Every `tecINTERNAL` path in `doApply` is marked `LCOV_EXCL_LINE` — they are structurally unreachable because `preclaim` already established the necessary preconditions on the same ledger state. + +## Relationship to Sibling Files + +This file is one of three that manage the cover pool lifecycle: `LoanBrokerCoverDeposit.cpp` (owner adds cover), `LoanBrokerCoverWithdraw.cpp` (owner removes cover), and this file (asset issuer claws back cover). The deposit and withdraw paths are restricted to the broker's `sfOwner`; only this clawback path is exercised by the third-party asset issuer, reflecting the regulatory clawback rights attached to IOU/MPT issuance on XRPL. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp.ai.json new file mode 100644 index 0000000000..08d30a980f --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp.ai.json @@ -0,0 +1,545 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoanBrokerCoverDeposit::preflight" + ], + "entry_point": "LoanBrokerCoverDeposit::preflight", + "purpose": "Initial stateless validation of transaction fields before ledger access.", + "validation_points": [ + "ctx.tx[sfLoanBrokerID] == beast::zero (invalid broker ID)", + "dstAmount <= beast::zero (invalid amount)", + "!isLegalNet(dstAmount) (illegal amount format)" + ] + }, + { + "call_chain": [ + "LoanBrokerCoverDeposit::preclaim" + ], + "entry_point": "LoanBrokerCoverDeposit::preclaim", + "purpose": "Stateful validation: checks ledger state, permissions, asset properties, and account balances.", + "validation_points": [ + "ctx.view.read(keylet::loanbroker(brokerID)) (broker existence)", + "account != sleBroker->at(sfOwner) (ownership check)", + "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))) (vault existence)", + "amount.asset() != vaultAsset (asset match)", + "canTransfer(ctx.view, vaultAsset, account, pseudoAccountID) (asset transferability)", + "checkFrozen(ctx.view, account, vaultAsset) (asset frozen check)", + "checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset) (deep frozen check)", + "requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth) (authorization)", + "accountHolds(...) < amount (sufficient funds)" + ] + }, + { + "call_chain": [ + "LoanBrokerCoverDeposit::doApply" + ], + "entry_point": "LoanBrokerCoverDeposit::doApply", + "purpose": "Applies the transaction: moves funds, updates broker state, associates asset.", + "validation_points": [ + "view().peek(keylet::loanbroker(brokerID)) (broker existence, again)", + "view().read(keylet::vault(broker->at(sfVaultID))) (vault existence, again)", + "accountSend(view(), account_, brokerPseudoID, amount, j_, WaiveTransferFee::Yes) (may fail if transfer not possible)" + ] + }, + { + "call_chain": [ + "LoanBrokerCoverDeposit::checkExtraFeatures", + "checkLendingProtocolDependencies" + ], + "entry_point": "LoanBrokerCoverDeposit::checkExtraFeatures", + "purpose": "Checks protocol feature dependencies (feature flags, protocol version, etc).", + "validation_points": [ + "checkLendingProtocolDependencies" + ] + } + ], + "data_flows": [ + { + "field": "sfLoanBrokerID", + "flow": [ + "ctx.tx[sfLoanBrokerID]", + "preflight: checked for zero", + "preclaim: used to look up broker in ledger", + "doApply: used to look up broker in ledger" + ], + "origin": "ctx.tx[sfLoanBrokerID] (transaction input)", + "transformations": [ + "Checked for zero (invalid)", + "Used as key for ledger lookup" + ], + "validated_at": "preflight (zero check), preclaim (existence check)" + }, + { + "field": "sfAmount", + "flow": [ + "ctx.tx[sfAmount]", + "preflight: checked for <= 0, isLegalNet", + "preclaim: used as 'amount', checked for asset match, transferability, frozen, auth, sufficient funds", + "doApply: used for transfer and updating broker" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Checked for <= 0", + "Checked for legal format", + "Compared to vault asset", + "Used in transfer", + "Added to broker's CoverAvailable" + ], + "validated_at": "preflight (amount checks), preclaim (asset match, transferability, frozen, auth, balance)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "preclaim: compared to sleBroker->at(sfOwner) for ownership", + "used as source in canTransfer, checkFrozen, requireAuth, accountHolds", + "doApply: used as source in accountSend" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Checked for ownership", + "Used in asset transfer and permission checks" + ], + "validated_at": "preclaim (ownership, transferability, frozen, auth, balance)" + }, + { + "field": "sfVaultID", + "flow": [ + "preclaim: used to look up vault", + "doApply: used to look up vault" + ], + "origin": "sleBroker->at(sfVaultID) (from broker ledger entry)", + "transformations": [ + "Used as key for vault lookup" + ], + "validated_at": "preclaim (vault existence), doApply (vault existence)" + }, + { + "field": "sfAsset", + "flow": [ + "preclaim: compared to amount.asset()", + "used in canTransfer, checkFrozen, checkDeepFrozen, requireAuth, accountHolds", + "doApply: used in associateAsset" + ], + "origin": "vault->at(sfAsset) (from vault ledger entry)", + "transformations": [ + "Compared to transaction asset", + "Used in transfer and permission checks" + ], + "validated_at": "preclaim (asset match, transferability, frozen, auth, balance)" + } + ], + "description": "Implements the LoanBrokerCoverDeposit transaction logic for the XRPL lending protocol, including preflight, preclaim, and apply steps for depositing cover into a loan broker's vault.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfLoanBrokerID] == beast::zero) at preflight", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "explicit check (ctx.tx[sfLoanBrokerID] == beast::zero) validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (dstAmount <= beast::zero) at preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "explicit check (dstAmount <= beast::zero) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (dstAmount <= beast::zero) at preflight", + "issue_pattern": "Missing range validation for sfAmount", + "why_false_positive": "explicit check (dstAmount <= beast::zero) validates sfAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "isLegalNet(dstAmount) at preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "isLegalNet(dstAmount) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::loanbroker(brokerID)) at preclaim", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "ctx.view.read(keylet::loanbroker(brokerID)) validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "account != sleBroker->at(sfOwner) at preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "account != sleBroker->at(sfOwner) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID (indirect via sleBroker)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))) at preclaim", + "issue_pattern": "Missing empty string validation for sfVaultID (indirect via sleBroker)", + "why_false_positive": "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))) validates sfVaultID (indirect via sleBroker) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount.asset()", + "empty", + "string", + "validation" + ], + "evidence": "amount.asset() != vaultAsset at preclaim", + "issue_pattern": "Missing empty string validation for sfAmount.asset()", + "why_false_positive": "amount.asset() != vaultAsset validates sfAmount.asset() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vaultAsset", + "empty", + "string", + "validation" + ], + "evidence": "canTransfer(ctx.view, vaultAsset, account, pseudoAccountID) at preclaim", + "issue_pattern": "Missing empty string validation for vaultAsset", + "why_false_positive": "canTransfer(ctx.view, vaultAsset, account, pseudoAccountID) validates vaultAsset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vaultAsset", + "empty", + "string", + "validation" + ], + "evidence": "checkFrozen(ctx.view, account, vaultAsset) at preclaim", + "issue_pattern": "Missing empty string validation for vaultAsset", + "why_false_positive": "checkFrozen(ctx.view, account, vaultAsset) validates vaultAsset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vaultAsset", + "empty", + "string", + "validation" + ], + "evidence": "checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset) at preclaim", + "issue_pattern": "Missing empty string validation for vaultAsset", + "why_false_positive": "checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset) validates vaultAsset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vaultAsset", + "empty", + "string", + "validation" + ], + "evidence": "requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth) at preclaim", + "issue_pattern": "Missing empty string validation for vaultAsset", + "why_false_positive": "requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth) validates vaultAsset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account balance of vaultAsset", + "empty", + "string", + "validation" + ], + "evidence": "accountHolds(...) < amount at preclaim", + "issue_pattern": "Missing empty string validation for account balance of vaultAsset", + "why_false_positive": "accountHolds(...) < amount validates account balance of vaultAsset for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "account balance of vaultAsset", + "range", + "bounds", + "validation" + ], + "evidence": "accountHolds(...) < amount at preclaim", + "issue_pattern": "Missing range validation for account balance of vaultAsset", + "why_false_positive": "accountHolds(...) < amount validates account balance of vaultAsset range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "LoanBrokerCoverDeposit::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "LoanBrokerCoverDeposit::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 27, + "name": "LoanBrokerCoverDeposit::preclaim" + }, + { + "args": [], + "lineno": 74, + "name": "LoanBrokerCoverDeposit::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is likely tested in integration/functional tests for lending/LoanBrokerCoverDeposit transactions. Typical test files would be in the rippled repo under 'test/tx/lending' or similar, e.g., 'LoanBrokerCoverDeposit_test.cpp'. The code has explicit error returns for all validation failures, which should be covered by negative tests (invalid broker ID, bad amount, wrong asset, insufficient funds, unauthorized, frozen, etc). However, some error paths (e.g., missing vault in doApply, marked as LCOV_EXCL_LINE) may not be covered by tests, as they are considered impossible in normal operation. There is no evidence of unit tests for internal helpers (e.g., canTransfer, checkFrozen), so coverage may depend on higher-level transaction tests. Deep protocol feature checks (checkExtraFeatures) may not be directly tested unless protocol upgrade tests exist.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation logic, no external validation framework detected", + "validation_layer": "business_logic (preflight and preclaim are business logic validation layers before state mutation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfLoanBrokerID", + "location": "preflight", + "validated_by": "explicit check (ctx.tx[sfLoanBrokerID] == beast::zero)", + "validates": [ + "LoanBrokerID must not be zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflight", + "validated_by": "explicit check (dstAmount <= beast::zero)", + "validates": [ + "Amount must be greater than zero" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflight", + "validated_by": "isLegalNet(dstAmount)", + "validates": [ + "Amount must be a legal net amount (custom business logic)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfLoanBrokerID", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::loanbroker(brokerID))", + "validates": [ + "LoanBroker must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfAccount", + "location": "preclaim", + "validated_by": "account != sleBroker->at(sfOwner)", + "validates": [ + "Account must be owner of LoanBroker" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfVaultID (indirect via sleBroker)", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)))", + "validates": [ + "Vault must exist for the broker" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecWRONG_ASSET", + "field": "sfAmount.asset()", + "location": "preclaim", + "validated_by": "amount.asset() != vaultAsset", + "validates": [ + "Amount asset must match vault asset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "return value of canTransfer (various TER codes)", + "field": "vaultAsset", + "location": "preclaim", + "validated_by": "canTransfer(ctx.view, vaultAsset, account, pseudoAccountID)", + "validates": [ + "Asset must be transferable from account to pseudoAccountID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "return value of checkFrozen (various TER codes)", + "field": "vaultAsset", + "location": "preclaim", + "validated_by": "checkFrozen(ctx.view, account, vaultAsset)", + "validates": [ + "Asset must not be frozen for account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "return value of checkDeepFrozen (various TER codes)", + "field": "vaultAsset", + "location": "preclaim", + "validated_by": "checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset)", + "validates": [ + "Asset must not be deep frozen for pseudoAccountID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "return value of requireAuth (various TER codes)", + "field": "vaultAsset", + "location": "preclaim", + "validated_by": "requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth)", + "validates": [ + "Account must be authorized for asset (strong auth)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINSUFFICIENT_FUNDS", + "field": "account balance of vaultAsset", + "location": "preclaim", + "validated_by": "accountHolds(...) < amount", + "validates": [ + "Account must have sufficient spendable balance of asset" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp.ai.md new file mode 100644 index 0000000000..bc2bce4f06 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverDeposit.cpp.ai.md @@ -0,0 +1,38 @@ +# `LoanBrokerCoverDeposit.cpp` + +## Role in the System + +This file implements the `LoanBrokerCoverDeposit` transaction, one of three symmetrical cover-management operations in the XRPL lending protocol (alongside `LoanBrokerCoverWithdraw` and `LoanBrokerCoverClawback`). Its specific job is to allow the **owner** of a `LoanBroker` ledger object to deposit collateral into the broker's pseudo-account, increasing the broker's `sfCoverAvailable` balance. Cover serves as a loss-absorption buffer: the lending protocol requires a broker to maintain a minimum ratio of cover relative to its outstanding loan debt (`sfDebtTotal`), preventing it from taking on more loan exposure than its capitalization can support. + +The file is compact — 126 lines — but encodes a layered validation strategy characteristic of all XRPL transactors. It sits under `src/libxrpl/tx/transactors/lending/` alongside the other lending transaction implementations, and depends on `LendingHelpers` for shared protocol utilities. + +## Transaction Lifecycle + +`LoanBrokerCoverDeposit` inherits from `Transactor` and exposes the four standard static/virtual hooks the framework calls in sequence. + +**`checkExtraFeatures`** simply delegates to `checkLendingProtocolDependencies`, which verifies that both `featureSingleAssetVault` and the vault feature's own dependency chain are enabled in the current ledger rules. This gate prevents the transaction from even reaching preflight on ledgers that do not yet support the lending protocol. + +**`preflight`** performs cheap, stateless field validation before any ledger reads occur. It rejects a zero `sfLoanBrokerID` (not a valid object reference), a non-positive `sfAmount`, and any amount that fails `isLegalNet` (which catches malformed IOU representations with illegal precision). These checks are intentionally minimal — anything that requires ledger state belongs in `preclaim`. + +**`preclaim`** is the substantive validation phase. Its checks fall into a clear hierarchy: + +1. *Object existence*: the broker keyed by `sfLoanBrokerID` must exist in the ledger; if missing, `tecNO_ENTRY` is returned. +2. *Ownership*: `sfAccount` on the transaction must equal `sfOwner` on the broker. Only the broker's owner may inject cover; returns `tecNO_PERMISSION` otherwise. +3. *Vault integrity*: the broker's associated vault is looked up via `sfVaultID`. A missing vault is treated as a fatal ledger inconsistency (`tefBAD_LEDGER`, guarded by `LCOV_EXCL_START`), reflecting that this state can only occur through data corruption — the broker creation path guarantees the vault exists. +4. *Asset match*: the deposited amount's asset must equal the vault's `sfAsset`. Returns `tecWRONG_ASSET` if they differ, preventing mixed-asset cover pools. +5. *Transfer compliance* (four sequential checks): `canTransfer` ensures the asset is transferable between the two accounts; `checkFrozen` ensures the asset is not frozen on the depositor's side; `checkDeepFrozen` ensures the broker's pseudo-account can receive (important because deep-freeze prevents receiving even if the sender is unaffected); `requireAuth` with `AuthType::StrongAuth` ensures the depositor has an established trust line or MPToken for the asset. This strong-auth requirement is notable — it means a depositor cannot accidentally fund a broker with an asset they are not explicitly authorized to hold. +6. *Balance*: `accountHolds` with `SpendableHandling::shFULL_BALANCE` verifies the depositor holds enough of the asset, using `fhZERO_IF_FROZEN` and `ahZERO_IF_UNAUTHORIZED` so frozen or unauthorized balances cannot be spent. + +**`doApply`** performs the mutation once all preclaim checks pass. It re-reads the broker and vault (failures here are `tecINTERNAL`, again `LCOV_EXCL_LINE` — the ledger is immutable between preclaim and apply within the same transaction batch). The three operations are: + +1. `accountSend(view(), account_, brokerPseudoID, amount, j_, WaiveTransferFee::Yes)` — moves funds from the depositor to the broker's pseudo-account. Transfer fees are explicitly waived because this is a protocol-internal capital movement, not a user-to-user IOU transfer; charging a fee here would erode cover capitalization in a way the protocol spec does not intend. +2. `broker->at(sfCoverAvailable) += amount` followed by `view().update(broker)` — increments the tracked cover balance. The `sfCoverAvailable` field is the source of truth the protocol uses for minimum-cover ratio checks during loan issuance and cover withdrawal; it must stay in sync with the pseudo-account's actual token balance. +3. `associateAsset(*broker, vaultAsset)` — a bookkeeping call that updates asset-tracking metadata on the broker SLE. This pattern appears uniformly across all lending transactors (deposit, withdraw, clawback, loan lifecycle events), ensuring protocol-level asset enumeration and cleanup routines can correctly identify which asset each broker object is associated with. + +## Design Decisions Worth Noting + +The ownership check in `preclaim` uses `sfOwner` from the broker object itself rather than trusting any owner field in the transaction, which prevents an attacker from crafting a transaction that claims ownership via a manipulated field. The broker object in the ledger is the authoritative source. + +Compared to `LoanBrokerCoverWithdraw`, which must additionally enforce minimum cover ratios (the withdrawal reduces cover and could violate the required buffer), the deposit path has no equivalent floor check — deposits can only increase cover, so there is no minimum to enforce from the depositor's perspective. + +The `LCOV_EXCL_LINE` annotations on the `doApply` null-checks signal that test coverage tooling should not penalize these unreachable guards. They exist as paranoia, not as functional error paths: `preclaim` already verified both the broker and vault exist, and the XRPL apply-phase runs against the same ledger snapshot. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp.ai.json new file mode 100644 index 0000000000..d04790aceb --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp.ai.json @@ -0,0 +1,496 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoanBrokerCoverWithdraw::preflight" + ], + "entry_point": "LoanBrokerCoverWithdraw::preflight", + "purpose": "Performs initial stateless validation of the transaction fields before any ledger access.", + "validation_points": [ + "sfLoanBrokerID checked for nonzero", + "sfAmount checked for > 0", + "sfAmount checked by isLegalNet", + "sfDestination (if present) checked for nonzero" + ] + }, + { + "call_chain": [ + "LoanBrokerCoverWithdraw::preclaim", + "canTransfer", + "canWithdraw (if destination != account)", + "requireAuth", + "checkFrozen", + "checkDeepFrozen" + ], + "entry_point": "LoanBrokerCoverWithdraw::preclaim", + "purpose": "Performs stateful validation using ledger data, including permissions, asset checks, and destination checks.", + "validation_points": [ + "isPseudoAccount(ctx.view, dstAcct)", + "LoanBroker existence (ctx.view.read(keylet::loanbroker))", + "Account is owner of LoanBroker", + "Vault existence (ctx.view.read(keylet::vault))", + "Asset matches vault asset", + "canTransfer", + "canWithdraw (if destination != account)", + "requireAuth", + "checkFrozen (unless sending to issuer)", + "checkDeepFrozen (unless sending to issuer)" + ] + }, + { + "call_chain": [ + "LoanBrokerCoverWithdraw::doApply" + ], + "entry_point": "LoanBrokerCoverWithdraw::doApply", + "purpose": "Executes the transaction after all validations pass (not shown in provided code, but standard in XRPL transactors).", + "validation_points": [ + "Assumes all validation already performed in preflight and preclaim" + ] + }, + { + "call_chain": [ + "LoanBrokerCoverWithdraw::checkExtraFeatures", + "checkLendingProtocolDependencies" + ], + "entry_point": "LoanBrokerCoverWithdraw::checkExtraFeatures", + "purpose": "Checks for protocol feature dependencies before transaction is allowed.", + "validation_points": [ + "checkLendingProtocolDependencies" + ] + } + ], + "data_flows": [ + { + "field": "sfLoanBrokerID", + "flow": [ + "ctx.tx[sfLoanBrokerID]", + "preflight: checked for nonzero", + "preclaim: used to look up LoanBroker SLE" + ], + "origin": "ctx.tx[sfLoanBrokerID] (transaction input)", + "transformations": [ + "None (direct usage)" + ], + "validated_at": "preflight (nonzero), preclaim (LoanBroker existence)" + }, + { + "field": "sfAmount", + "flow": [ + "ctx.tx[sfAmount]", + "preflight: checked for > 0 and isLegalNet", + "preclaim: compared to vault asset, used in further checks" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Checked for legality and positivity" + ], + "validated_at": "preflight (<= 0, isLegalNet), preclaim (asset match)" + }, + { + "field": "sfDestination", + "flow": [ + "ctx.tx[~sfDestination]", + "preflight: checked for nonzero if present", + "preclaim: defaulted to account if not present, checked for pseudo-account, used in asset transfer checks" + ], + "origin": "ctx.tx[~sfDestination] (optional transaction input)", + "transformations": [ + "Optional, defaulted to sfAccount if not present" + ], + "validated_at": "preflight (nonzero), preclaim (isPseudoAccount, canWithdraw, requireAuth, checkFrozen, checkDeepFrozen)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "preclaim: used as default destination, checked as owner of LoanBroker" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "None" + ], + "validated_at": "preclaim (ownership check)" + }, + { + "field": "vaultAsset", + "flow": [ + "vault->at(sfAsset)", + "preclaim: compared to amount.asset(), used in canTransfer, requireAuth, checkFrozen, checkDeepFrozen" + ], + "origin": "vault->at(sfAsset) (ledger state)", + "transformations": [ + "None" + ], + "validated_at": "preclaim (asset match, transferability, freezes)" + } + ], + "description": "Implements the LoanBrokerCoverWithdraw transaction logic for the XRPL lending protocol, including preflight, preclaim, and apply steps for withdrawing cover from a loan broker's vault.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (== beast::zero) at preflight", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "explicit check (== beast::zero) validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (<= beast::zero) at preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "explicit check (<= beast::zero) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (<= beast::zero) at preflight", + "issue_pattern": "Missing range validation for sfAmount", + "why_false_positive": "explicit check (<= beast::zero) validates sfAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "isLegalNet(dstAmount) at preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "isLegalNet(dstAmount) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (*destination == beast::zero) at preflight", + "issue_pattern": "Missing empty string validation for sfDestination", + "why_false_positive": "explicit check (*destination == beast::zero) validates sfDestination for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "isPseudoAccount(ctx.view, dstAcct) at preclaim", + "issue_pattern": "Missing empty string validation for sfDestination", + "why_false_positive": "isPseudoAccount(ctx.view, dstAcct) validates sfDestination for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::loanbroker(brokerID)) at preclaim", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "ctx.view.read(keylet::loanbroker(brokerID)) validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "account != sleBroker->at(sfOwner) at preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "account != sleBroker->at(sfOwner) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID (from sleBroker)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))) at preclaim", + "issue_pattern": "Missing empty string validation for sfVaultID (from sleBroker)", + "why_false_positive": "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))) validates sfVaultID (from sleBroker) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount.asset()", + "empty", + "string", + "validation" + ], + "evidence": "amount.asset() != vaultAsset at preclaim", + "issue_pattern": "Missing empty string validation for sfAmount.asset()", + "why_false_positive": "amount.asset() != vaultAsset validates sfAmount.asset() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vaultAsset, pseudoAccountID, dstAcct", + "empty", + "string", + "validation" + ], + "evidence": "canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct) at preclaim", + "issue_pattern": "Missing empty string validation for vaultAsset, pseudoAccountID, dstAcct", + "why_false_positive": "canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct) validates vaultAsset, pseudoAccountID, dstAcct for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount, sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "canWithdraw(ctx.view, tx) at preclaim (if account != dstAcct)", + "issue_pattern": "Missing empty string validation for sfAccount, sfDestination", + "why_false_positive": "canWithdraw(ctx.view, tx) validates sfAccount, sfDestination for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 9, + "name": "LoanBrokerCoverWithdraw::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "LoanBrokerCoverWithdraw::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 32, + "name": "LoanBrokerCoverWithdraw::preclaim" + }, + { + "args": [], + "lineno": 99, + "name": "LoanBrokerCoverWithdraw::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test files for this code would be in the unit test suite for lending transactions, e.g., LoanBrokerCoverWithdraw_test.cpp or LendingTransactor_test.cpp. Tests should cover: invalid LoanBrokerID, zero/negative/illegal amounts, missing or pseudo destination, non-owner attempts, wrong asset, frozen assets, and destination consent. Gaps may exist if there are no tests for edge cases like deep frozen assets, missing vaults (which is marked LCOV_EXCL_START), or protocol feature dependencies. Coverage for all validation branches should be confirmed in the test suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation logic, Ripple/XRPL transaction processing", + "validation_layer": "business_logic (preflight and preclaim transaction hooks)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfLoanBrokerID", + "location": "preflight", + "validated_by": "explicit check (== beast::zero)", + "validates": [ + "LoanBrokerID must not be zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflight", + "validated_by": "explicit check (<= beast::zero)", + "validates": [ + "Amount must be greater than zero" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflight", + "validated_by": "isLegalNet(dstAmount)", + "validates": [ + "Amount must be a legal net amount (asset/format check)" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfDestination", + "location": "preflight", + "validated_by": "explicit check (*destination == beast::zero)", + "validates": [ + "Destination, if present, must not be zero" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 1.0, + "error_thrown": "tecPSEUDO_ACCOUNT", + "field": "sfDestination", + "location": "preclaim", + "validated_by": "isPseudoAccount(ctx.view, dstAcct)", + "validates": [ + "Destination account must not be a pseudo-account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfLoanBrokerID", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::loanbroker(brokerID))", + "validates": [ + "LoanBroker must exist in ledger" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfAccount", + "location": "preclaim", + "validated_by": "account != sleBroker->at(sfOwner)", + "validates": [ + "Transaction account must be owner of LoanBroker" + ], + "validation_type": "business_logic|ownership" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfVaultID (from sleBroker)", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)))", + "validates": [ + "Vault referenced by LoanBroker must exist" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecWRONG_ASSET", + "field": "sfAmount.asset()", + "location": "preclaim", + "validated_by": "amount.asset() != vaultAsset", + "validates": [ + "Withdrawal asset must match vault asset" + ], + "validation_type": "business_logic|type" + }, + { + "confidence": 1.0, + "error_thrown": "return value of canTransfer (various TERs)", + "field": "vaultAsset, pseudoAccountID, dstAcct", + "location": "preclaim", + "validated_by": "canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct)", + "validates": [ + "Asset must be transferable from pseudo-account to destination" + ], + "validation_type": "business_logic|transferability" + }, + { + "confidence": 1.0, + "error_thrown": "return value of canWithdraw (various TERs)", + "field": "sfAccount, sfDestination", + "location": "preclaim (if account != dstAcct)", + "validated_by": "canWithdraw(ctx.view, tx)", + "validates": [ + "Withdrawal to third party must pass withdrawal checks" + ], + "validation_type": "business_logic|authorization" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp.ai.md new file mode 100644 index 0000000000..a7ff8f0230 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.cpp.ai.md @@ -0,0 +1,60 @@ +# `LoanBrokerCoverWithdraw.cpp` + +## Role in the System + +This file implements the `LoanBrokerCoverWithdraw` transactor, the mechanism by which the owner of a `LoanBroker` ledger object recoups idle cover capital previously deposited into the broker's pseudo-account. It is the withdrawal leg of the `LoanBrokerCoverDeposit` / `LoanBrokerCoverWithdraw` pair that governs a broker's collateral pool in the XRPL lending protocol (XLS-66). + +A `LoanBroker` intermediates between a lending vault and individual borrowers. Cover capital sits in a pseudo-account controlled by the broker and backstops outstanding loans. Because that capital is locked inside a pseudo-account, no ordinary payment transaction can reach it; this transactor is the only sanctioned path out. + +## Transaction Flow + +The code follows the standard XRPL three-phase transactor pattern: `checkExtraFeatures` → `preflight` → `preclaim` → `doApply`. + +**`checkExtraFeatures`** does nothing beyond delegating to `checkLendingProtocolDependencies`, which gates the entire lending feature set on protocol amendments. If the required amendments are not enabled on the ledger, the transaction is rejected before any field parsing. + +**`preflight`** performs cheap, stateless sanity checks. It rejects a zero `sfLoanBrokerID`, a non-positive or otherwise illegal `sfAmount`, and a zero `sfDestination` if one is supplied. The destination is optional — when absent the submitter withdraws to themselves. + +**`preclaim`** is where the substantive enforcement happens. Its logic breaks into several layers: + +1. **Object lookups and ownership.** The preclaim resolves the `LoanBroker` SLE by `keylet::loanbroker(brokerID)` and confirms the submitting account is its `sfOwner`. It then follows the broker's `sfVaultID` reference to the vault to obtain the canonical `sfAsset`. A missing vault is treated as `tefBAD_LEDGER` and marked `LCOV_EXCL_START` because in a well-formed ledger, the broker cannot outlive its vault. + +2. **Asset compatibility.** The requested withdrawal amount's asset must match the vault's asset. A mismatch returns `tecWRONG_ASSET`, preventing type confusion. + +3. **Transfer-path checks.** The source of funds is the broker's `sfAccount` pseudo-account, not the submitting account. `canTransfer` checks whether the asset is transferable from that pseudo-account to the destination. If the destination differs from the submitter, the transactor escalates to `StrongAuth` and calls `canWithdraw(ctx.view, tx)`, which enforces that the destination account exists, that it has not set a destination tag requirement that would block the transfer, that deposit authorization is satisfied, and that the destination will not exceed trustline or MPToken limits. The `AuthType` discrimination is intentional: withdrawing to yourself is a balance adjustment, but withdrawing to a third party is functionally equivalent to a transfer, warranting full transfer-path scrutiny. + +4. **Freeze checks.** Unless the destination is the asset issuer (who is exempt from their own freeze restrictions), the broker's pseudo-account is checked for source-side freezes and the destination is checked for deep-freeze status. This mirrors the same pattern used in vault and payment transactors. + +5. **Minimum cover enforcement.** This is the defining constraint unique to this transactor. After computing how much cover is currently available (`sfCoverAvailable`), preclaim calculates the minimum cover that must remain: + + ```cpp + NumberRoundModeGuard const mg(Number::upward); + minimumCover = roundToAsset( + vaultAsset, + tenthBipsOfValue(currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))), + currentDebtTotal.exponent()); + ``` + + `sfCoverRateMinimum` is expressed in 1/10 basis-point units, so `tenthBipsOfValue` computes a fractional portion of the total outstanding debt. The `NumberRoundModeGuard` forces upward rounding for both the bips multiplication and the `roundToAsset` call, deliberately overstating the required minimum to prevent rounding exploitation. If the proposed withdrawal would leave `coverAvail - amount < minimumCover`, the transaction returns `tecINSUFFICIENT_FUNDS`. A separate check ensures the withdrawal does not exceed total cover available at all. + +6. **Actual balance.** Even if the ledger's accounting says cover is available, the actual pseudo-account balance is verified with `accountHolds`. This guards against ledger inconsistencies where `sfCoverAvailable` and the real balance drift apart. + +**`doApply`** executes with all checks satisfied: + +```cpp +broker->at(sfCoverAvailable) -= amount; +view().update(broker); +associateAsset(*broker, vaultAsset); +return doWithdraw(view(), tx, account_, dstAcct, brokerPseudoID, preFeeBalance_, amount, j_); +``` + +`sfCoverAvailable` is decremented first, then `associateAsset` is called on the broker SLE. This is required because numeric fields on SLEs that hold asset-denominated `STNumber` values must be told which asset they represent before the ledger serializes them; failing to call it would corrupt the broker's stored numbers. The actual asset transfer is performed by the shared `doWithdraw` helper from `View.h`, which handles the specifics of moving funds from a pseudo-account (`brokerPseudoID`) to the destination, applying waived transfer fees and other settlement logic common to all vault-adjacent withdrawals. + +## Design Decisions + +**Pseudo-account as source, not submitter.** The submitting account (`account_`) authorizes the transaction but is never the source of funds — `brokerPseudoID` is. This cleanly separates custody from authorization and prevents the broker owner from "spending" cover directly from their own account. + +**Conditional `StrongAuth` vs. `WeakAuth`.** Withdrawing to a third party demands `StrongAuth` (the destination must have a RippleState or MPToken already created), while withdrawing to self requires only `WeakAuth`. This avoids forcing broker owners to pre-establish trust relationships with themselves just to reclaim their own capital, while still enforcing full consent checks for novel recipients. + +**Upward rounding for minimum cover.** The minimum cover calculation is explicitly wrapped in `NumberRoundModeGuard(Number::upward)`. This is a conservative choice: when the ledger rounds the minimum required cover up, withdrawals are blocked marginally sooner than they might be with neutral rounding, giving loans extra protection against edge-case under-collateralization from accumulated rounding dust. + +**Mirror symmetry with `LoanBrokerCoverDeposit`.** The two transactors are intentional inverses. Deposit increments `sfCoverAvailable` after calling `accountSend` from the submitter to the pseudo-account; withdraw decrements it before calling `doWithdraw` from the pseudo-account to the destination. Both call `associateAsset` to keep STNumber fields correctly typed throughout. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp.ai.json new file mode 100644 index 0000000000..afa42e6928 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp.ai.json @@ -0,0 +1,406 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoanBrokerDelete::checkExtraFeatures", + "checkLendingProtocolDependencies" + ], + "entry_point": "LoanBrokerDelete::checkExtraFeatures", + "purpose": "Checks for protocol-level feature dependencies before transaction processing.", + "validation_points": [ + "checkLendingProtocolDependencies" + ] + }, + { + "call_chain": [ + "LoanBrokerDelete::preflight" + ], + "entry_point": "LoanBrokerDelete::preflight", + "purpose": "Performs initial stateless validation of the transaction.", + "validation_points": [ + "ctx.tx[sfLoanBrokerID] == beast::zero" + ] + }, + { + "call_chain": [ + "LoanBrokerDelete::preclaim" + ], + "entry_point": "LoanBrokerDelete::preclaim", + "purpose": "Performs stateful validation, checking ledger state and permissions.", + "validation_points": [ + "ctx.view.read(keylet::loanbroker(brokerID))", + "account != brokerOwner", + "sleBroker->at(sfOwnerCount) != 0", + "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)))", + "sleBroker->at(sfDebtTotal) != beast::zero", + "checkDeepFrozen(ctx.view, brokerOwner, asset)" + ] + }, + { + "call_chain": [ + "LoanBrokerDelete::doApply" + ], + "entry_point": "LoanBrokerDelete::doApply", + "purpose": "Applies the transaction, deleting the broker and transferring assets.", + "validation_points": [ + "view().peek(keylet::loanbroker(brokerID))", + "view().read(keylet::vault(vaultID))", + "view().dirRemove(...)" + ] + } + ], + "data_flows": [ + { + "field": "sfLoanBrokerID", + "flow": [ + "ctx.tx[sfLoanBrokerID]", + "preflight: checked for beast::zero", + "preclaim: used as brokerID to look up broker", + "doApply: used to look up and delete broker" + ], + "origin": "ctx.tx[sfLoanBrokerID] (transaction input)", + "transformations": [ + "Checked for zero value", + "Used as key for ledger lookup" + ], + "validated_at": "preflight, preclaim" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "preclaim: compared to brokerOwner", + "doApply: used as account_ for asset transfer" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared for equality" + ], + "validated_at": "preclaim" + }, + { + "field": "sfOwnerCount", + "flow": [ + "ctx.view.read(keylet::loanbroker(brokerID))", + "preclaim: checked for nonzero" + ], + "origin": "sleBroker->at(sfOwnerCount) (ledger state)", + "transformations": [ + "Checked for nonzero (must be zero to proceed)" + ], + "validated_at": "preclaim" + }, + { + "field": "sfVaultID", + "flow": [ + "ctx.view.read(keylet::loanbroker(brokerID))", + "preclaim: used to look up vault", + "doApply: used to look up vault" + ], + "origin": "sleBroker->at(sfVaultID) (ledger state)", + "transformations": [ + "Used as key for ledger lookup" + ], + "validated_at": "preclaim, doApply" + }, + { + "field": "sfDebtTotal", + "flow": [ + "ctx.view.read(keylet::loanbroker(brokerID))", + "preclaim: checked for nonzero, rounded, compared to zero" + ], + "origin": "sleBroker->at(sfDebtTotal) (ledger state)", + "transformations": [ + "Rounded to asset scale", + "Checked for nonzero" + ], + "validated_at": "preclaim" + }, + { + "field": "sfCoverAvailable", + "flow": [ + "ctx.view.read(keylet::loanbroker(brokerID))", + "preclaim: checked if > 0, triggers deep freeze check", + "doApply: used to transfer assets" + ], + "origin": "sleBroker->at(sfCoverAvailable) (ledger state)", + "transformations": [ + "Converted to STAmount", + "Used in asset transfer" + ], + "validated_at": "preclaim" + } + ], + "description": "Implements the LoanBrokerDelete transaction logic for deleting a LoanBroker object in the XRPL lending protocol, including preflight, preclaim, and apply steps with all necessary ledger and permission checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfLoanBrokerID] == beast::zero) at preflight", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "explicit check (ctx.tx[sfLoanBrokerID] == beast::zero) validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::loanbroker(brokerID)) at preclaim", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "ctx.view.read(keylet::loanbroker(brokerID)) validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "account != brokerOwner at preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "account != brokerOwner validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwnerCount", + "empty", + "string", + "validation" + ], + "evidence": "sleBroker->at(sfOwnerCount); ownerCount != 0 at preclaim", + "issue_pattern": "Missing empty string validation for sfOwnerCount", + "why_false_positive": "sleBroker->at(sfOwnerCount); ownerCount != 0 validates sfOwnerCount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))) at preclaim", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID))) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDebtTotal", + "empty", + "string", + "validation" + ], + "evidence": "sleBroker->at(sfDebtTotal); debtTotal != beast::zero at preclaim", + "issue_pattern": "Missing empty string validation for sfDebtTotal", + "why_false_positive": "sleBroker->at(sfDebtTotal); debtTotal != beast::zero validates sfDebtTotal for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCoverAvailable", + "empty", + "string", + "validation" + ], + "evidence": "coverAvailable > beast::zero; checkDeepFrozen(ctx.view, brokerOwner, asset) at preclaim", + "issue_pattern": "Missing empty string validation for sfCoverAvailable", + "why_false_positive": "coverAvailable > beast::zero; checkDeepFrozen(ctx.view, brokerOwner, asset) validates sfCoverAvailable for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Lending Protocol Dependencies", + "empty", + "string", + "validation" + ], + "evidence": "checkLendingProtocolDependencies(ctx) at checkExtraFeatures", + "issue_pattern": "Missing empty string validation for Lending Protocol Dependencies", + "why_false_positive": "checkLendingProtocolDependencies(ctx) validates Lending Protocol Dependencies for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "LoanBrokerDelete::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "LoanBrokerDelete::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 21, + "name": "LoanBrokerDelete::preclaim" + }, + { + "args": [], + "lineno": 74, + "name": "LoanBrokerDelete::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by integration and unit tests for LoanBrokerDelete transactions, typically found in files like 'LoanBrokerDelete_test.cpp', 'LendingTransactor_test.cpp', or more general transaction/ledger tests. The LCOV_EXCL_START/STOP and LCOV_EXCL_LINE comments indicate some error paths (e.g., missing vault, failed dirRemove) are not covered by tests. Defensive checks (e.g., debt rounding, missing vault) may not be directly tested. Permission, existence, and field validation logic should be covered by positive/negative test cases, but edge cases (e.g., deep freeze, rounding errors, ledger corruption) may lack explicit tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation logic (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfLoanBrokerID", + "location": "preflight", + "validated_by": "explicit check (ctx.tx[sfLoanBrokerID] == beast::zero)", + "validates": [ + "LoanBrokerID must not be zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfLoanBrokerID", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::loanbroker(brokerID))", + "validates": [ + "LoanBroker must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfAccount", + "location": "preclaim", + "validated_by": "account != brokerOwner", + "validates": [ + "Account must be owner of LoanBroker" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "sfOwnerCount", + "location": "preclaim", + "validated_by": "sleBroker->at(sfOwnerCount); ownerCount != 0", + "validates": [ + "LoanBroker must have zero owner count (no obligations)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfVaultID", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::vault(sleBroker->at(sfVaultID)))", + "validates": [ + "Vault referenced by LoanBroker must exist" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "sfDebtTotal", + "location": "preclaim", + "validated_by": "sleBroker->at(sfDebtTotal); debtTotal != beast::zero", + "validates": [ + "LoanBroker must have zero total debt (after rounding to asset scale)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "return value of checkDeepFrozen (varies)", + "field": "sfCoverAvailable", + "location": "preclaim", + "validated_by": "coverAvailable > beast::zero; checkDeepFrozen(ctx.view, brokerOwner, asset)", + "validates": [ + "If cover assets exist, broker owner must not be deep frozen for that asset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "return value of checkLendingProtocolDependencies (varies)", + "field": "Lending Protocol Dependencies", + "location": "checkExtraFeatures", + "validated_by": "checkLendingProtocolDependencies(ctx)", + "validates": [ + "Lending protocol dependencies must be satisfied" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp.ai.md new file mode 100644 index 0000000000..079c2ebb95 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerDelete.cpp.ai.md @@ -0,0 +1,57 @@ +# `LoanBrokerDelete.cpp` — Tear-down of a LoanBroker and Its Pseudo-Account + +## Role in the System + +This file implements the `LoanBrokerDelete` transactor, which removes a `LoanBroker` ledger object from the XRPL ledger. A `LoanBroker` is created by `LoanBrokerSet` on behalf of the vault owner; it manages the terms under which loans are issued against a vault, holds a cover-capital buffer in a dedicated pseudo-account, and tracks aggregate debt. Because this object and its associated pseudo-account both consume owner reserves and hold real assets, deletion must go through a careful multi-phase process rather than a simple erase. + +`LoanBrokerDelete` is one of the lower-level teardown transactors in the lending protocol — alongside `LoanDelete` — and mirrors the construction logic in `LoanBrokerSet`. Both use the standard XRPL transactor pipeline: `checkExtraFeatures` → `preflight` → `preclaim` → `doApply`. + +## Validation Phases + +### `checkExtraFeatures` + +Delegates immediately to `checkLendingProtocolDependencies`, which verifies that the required feature flags for the lending protocol are all enabled. This gate is shared across every lending transactor and prevents these transactions from being processed on network builds where the protocol is not fully active. + +### `preflight` + +The only stateless check here is confirming that `sfLoanBrokerID` is not zero. The field identifies the target broker object by its on-ledger key; a zero value means the transaction was malformed before it even reached the network, so `temINVALID` is returned immediately. The thinness of this phase is intentional — most semantic constraints require reading ledger state and belong in `preclaim`. + +### `preclaim` + +This phase reads the current ledger (without mutating it) and enforces six ordered invariants: + +1. **Existence check** — The broker keyed by `sfLoanBrokerID` must exist (`tecNO_ENTRY` otherwise). Subsequent steps rely on reading the broker's fields. + +2. **Ownership check** — `sfAccount` on the transaction must match `sfOwner` on the broker SLE (`tecNO_PERMISSION` otherwise). Only the entity that created the broker may destroy it. + +3. **Zero owner count** — `sleBroker->at(sfOwnerCount)` must be zero. Non-zero means there are active `Loan` objects backed by this broker; those must be fully repaid and deleted via `LoanDelete` before the broker itself can go (`tecHAS_OBLIGATIONS`). + +4. **Vault consistency** — The vault referenced by `sfVaultID` on the broker SLE must still exist in the ledger. If it has disappeared, the ledger is corrupt and `tefBAD_LEDGER` is returned. This path is marked `LCOV_EXCL_START` because a healthy ledger cannot reach it — the vault and broker are created together and the vault cannot be deleted while a broker references it. + +5. **Zero debt total (with rounding)** — `sfDebtTotal` on the broker is checked and, if non-zero, rounded toward zero using `roundToAsset` with `Number::towards_zero` at the vault's asset scale. Only if the rounded value is still non-zero does preclaim fail with `tecHAS_OBLIGATIONS`. The code comment explicitly labels this "purely defensive" — the last `LoanDelete` for a broker's final loan should have zeroed `sfDebtTotal`, but floating-point accounting can leave sub-precision dust that rounds to zero at the asset's scale. The rounding direction (toward zero rather than away) is deliberate: it gives dust the benefit of the doubt and allows deletion to proceed when the residual is within one unit of the asset's smallest denomination. This path is also excluded from coverage because it should be unreachable in practice. + +6. **Deep-freeze check** — If `sfCoverAvailable` is non-zero, the remaining cover capital will be returned to the broker owner upon deletion. If the owner's account is deep-frozen for the vault's asset type (meaning their ability to receive that asset has been administratively revoked), the transfer cannot happen, so preclaim returns whatever error `checkDeepFrozen` produces. This check is intentionally skipped when `coverAvailable` is zero to avoid blocking no-op refunds. + +## `doApply` — Mutation Sequence + +The apply phase performs a strict sequence of mutations. Any `tefBAD_LEDGER` returns inside it are guarded with `LCOV_EXCL_LINE` because the preclaim phase already verified the preconditions, and reaching them in apply would indicate a fundamental ledger consistency failure. + +1. **Directory removal** — The broker SLE is removed from two owner directories: the broker owner's account directory (keyed by `sfOwnerNode`) and the vault pseudo-account's directory (keyed by `sfVaultNode`). This bidirectional cleanup mirrors the two-directory insertion done in `LoanBrokerSet::doApply`. + +2. **Cover refund** — Any `sfCoverAvailable` balance is transferred from the broker's pseudo-account back to `account_` (the transaction submitter / broker owner) via `accountSend` with `WaiveTransferFee::Yes`. Fees are waived because this is a protocol-internal cleanup transfer, not a user-initiated payment. + +3. **Empty holding removal** — `removeEmptyHolding` is called on the broker pseudo-account for the vault asset. This removes the trust line or MPToken object that was holding the now-zero cover balance, recovering the owner reserve that was charged when the holding was created. + +4. **Pseudo-account sanity checks** — Three defensive assertions confirm the pseudo-account is fully empty before erasure: its `sfBalance` is zero, its `sfOwnerCount` is zero, and it has no owner directory. All three are marked `LCOV_EXCL_LINE` — the preceding steps should have cleared all these obligations, and failure here would indicate a protocol-level bug. + +5. **Object erasure** — The broker pseudo-account SLE is erased first, then the broker SLE itself. Order matters here: erasing the pseudo-account's SLE while the broker SLE is still readable means the erasure can reference broker metadata if needed. The reverse order could leave a dangling reference. + +6. **Owner count adjustment** — `adjustOwnerCount` is called with `-2` on the broker owner's account SLE. The two-unit decrement balances the two-unit increment in `LoanBrokerSet`: one for the `LoanBroker` object itself, one for the pseudo-account. + +7. **Asset association** — `associateAsset(*broker, vaultAsset)` is called after the broker SLE has been erased. This iterates over all `sMD_NeedsAsset` fields on the (now-erased, but still in-scope) SLE and records the asset, enabling downstream components such as fee calculations or reserve tracking to identify which asset was involved. The pattern of calling this after mutation — rather than before — is consistent across all lending transactors. + +## Design Notes + +The pseudo-account pattern (a synthetic `AccountRoot` SLE that has no signing keys) is used to give the broker a first-class ledger presence as the custodian of cover capital. This allows standard ledger accounting — owner directories, balance checks, trust lines — to apply uniformly rather than requiring custom asset-holding logic inside the `LoanBroker` SLE itself. The cost is the two-object bookkeeping burden (broker object + pseudo-account), which is why the owner count is decremented by exactly two on deletion. + +The debt-rounding check at preclaim reflects a broader pattern in the lending protocol: `STNumber` fields that store financial amounts are subject to accumulated imprecision from periodic payment calculations, and the protocol chooses a consistent rounding strategy (`towards_zero` for "does anything remain?") to avoid spurious failures at cleanup time. The explicit comment marking it "purely defensive" signals to future maintainers that if this check ever fires in production, it represents either a bug in `LoanDelete` or an unanticipated path through the loan lifecycle. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp.ai.json new file mode 100644 index 0000000000..b3f468ba4a --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp.ai.json @@ -0,0 +1,606 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoanBrokerSet::preflight" + ], + "entry_point": "LoanBrokerSet::preflight", + "purpose": "Performs stateless validation of transaction fields before further processing.", + "validation_points": [ + "LoanBrokerSet::preflight" + ] + }, + { + "call_chain": [ + "LoanBrokerSet::preclaim" + ], + "entry_point": "LoanBrokerSet::preclaim", + "purpose": "Performs contextual validation (ledger state, permissions, object existence) before transaction application.", + "validation_points": [ + "LoanBrokerSet::preclaim" + ] + }, + { + "call_chain": [ + "LoanBrokerSet::doApply" + ], + "entry_point": "LoanBrokerSet::doApply", + "purpose": "Applies the transaction after preflight and preclaim have succeeded.", + "validation_points": [ + "LoanBrokerSet::preflight", + "LoanBrokerSet::preclaim" + ] + }, + { + "call_chain": [ + "LoanBrokerSet::checkExtraFeatures", + "checkLendingProtocolDependencies" + ], + "entry_point": "LoanBrokerSet::checkExtraFeatures", + "purpose": "Checks for protocol feature dependencies before transaction processing.", + "validation_points": [ + "checkLendingProtocolDependencies" + ] + } + ], + "data_flows": [ + { + "field": "sfData", + "flow": [ + "Transaction input", + "LoanBrokerSet::preflight", + "validDataLength", + "Validation result" + ], + "origin": "ctx.tx[~sfData] (transaction input)", + "transformations": [ + "Checked for presence and non-emptiness", + "Validated for maximum allowed length" + ], + "validated_at": "LoanBrokerSet::preflight" + }, + { + "field": "sfManagementFeeRate", + "flow": [ + "Transaction input", + "LoanBrokerSet::preflight", + "validNumericRange", + "Validation result" + ], + "origin": "ctx.tx[~sfManagementFeeRate] (transaction input)", + "transformations": [ + "Validated for numeric range (maxManagementFeeRate)" + ], + "validated_at": "LoanBrokerSet::preflight" + }, + { + "field": "sfCoverRateMinimum", + "flow": [ + "Transaction input", + "LoanBrokerSet::preflight", + "validNumericRange", + "Validation result" + ], + "origin": "ctx.tx[~sfCoverRateMinimum] (transaction input)", + "transformations": [ + "Validated for numeric range (maxCoverRate)" + ], + "validated_at": "LoanBrokerSet::preflight" + }, + { + "field": "sfCoverRateLiquidation", + "flow": [ + "Transaction input", + "LoanBrokerSet::preflight", + "validNumericRange", + "Validation result" + ], + "origin": "ctx.tx[~sfCoverRateLiquidation] (transaction input)", + "transformations": [ + "Validated for numeric range (maxCoverRate)" + ], + "validated_at": "LoanBrokerSet::preflight" + }, + { + "field": "sfDebtMaximum", + "flow": [ + "Transaction input", + "LoanBrokerSet::preflight", + "validNumericRange", + "LoanBrokerSet::preclaim (if present)", + "Compared to current DebtTotal (if updating existing broker)" + ], + "origin": "ctx.tx[~sfDebtMaximum] (transaction input)", + "transformations": [ + "Validated for numeric range (maxMPTokenAmount, 0)", + "Compared to current DebtTotal to prevent lowering below current" + ], + "validated_at": "LoanBrokerSet::preflight, LoanBrokerSet::preclaim" + }, + { + "field": "sfLoanBrokerID", + "flow": [ + "Transaction input", + "LoanBrokerSet::preflight", + "Checked for zero value", + "LoanBrokerSet::preclaim", + "Used to look up existing LoanBroker SLE" + ], + "origin": "ctx.tx[~sfLoanBrokerID] (transaction input)", + "transformations": [ + "Checked for presence and non-zero value", + "Used to fetch and validate existing broker" + ], + "validated_at": "LoanBrokerSet::preflight, LoanBrokerSet::preclaim" + }, + { + "field": "sfVaultID", + "flow": [ + "Transaction input", + "LoanBrokerSet::preflight", + "Checked for zero value", + "LoanBrokerSet::preclaim", + "Used to look up Vault SLE" + ], + "origin": "ctx.tx[~sfVaultID] (transaction input)", + "transformations": [ + "Checked for presence and non-zero value", + "Used to fetch and validate vault" + ], + "validated_at": "LoanBrokerSet::preflight, LoanBrokerSet::preclaim" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "LoanBrokerSet::preclaim", + "Compared to sfOwner of Vault and LoanBroker" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Checked for ownership of Vault and LoanBroker" + ], + "validated_at": "LoanBrokerSet::preclaim" + } + ], + "description": "Implements the LoanBrokerSet transaction logic for creating or modifying LoanBroker objects in the XRPL lending protocol, including preflight, preclaim, and apply logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfData", + "empty", + "string", + "validation" + ], + "evidence": "validDataLength at LoanBrokerSet::preflight", + "issue_pattern": "Missing empty string validation for sfData", + "why_false_positive": "validDataLength validates sfData for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfData", + "range", + "bounds", + "validation" + ], + "evidence": "validDataLength at LoanBrokerSet::preflight", + "issue_pattern": "Missing range validation for sfData", + "why_false_positive": "validDataLength validates sfData range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfManagementFeeRate", + "empty", + "string", + "validation" + ], + "evidence": "validNumericRange at LoanBrokerSet::preflight", + "issue_pattern": "Missing empty string validation for sfManagementFeeRate", + "why_false_positive": "validNumericRange validates sfManagementFeeRate for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfManagementFeeRate", + "range", + "bounds", + "validation" + ], + "evidence": "validNumericRange at LoanBrokerSet::preflight", + "issue_pattern": "Missing range validation for sfManagementFeeRate", + "why_false_positive": "validNumericRange validates sfManagementFeeRate range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCoverRateMinimum", + "empty", + "string", + "validation" + ], + "evidence": "validNumericRange at LoanBrokerSet::preflight", + "issue_pattern": "Missing empty string validation for sfCoverRateMinimum", + "why_false_positive": "validNumericRange validates sfCoverRateMinimum for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfCoverRateMinimum", + "range", + "bounds", + "validation" + ], + "evidence": "validNumericRange at LoanBrokerSet::preflight", + "issue_pattern": "Missing range validation for sfCoverRateMinimum", + "why_false_positive": "validNumericRange validates sfCoverRateMinimum range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCoverRateLiquidation", + "empty", + "string", + "validation" + ], + "evidence": "validNumericRange at LoanBrokerSet::preflight", + "issue_pattern": "Missing empty string validation for sfCoverRateLiquidation", + "why_false_positive": "validNumericRange validates sfCoverRateLiquidation for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfCoverRateLiquidation", + "range", + "bounds", + "validation" + ], + "evidence": "validNumericRange at LoanBrokerSet::preflight", + "issue_pattern": "Missing range validation for sfCoverRateLiquidation", + "why_false_positive": "validNumericRange validates sfCoverRateLiquidation range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDebtMaximum", + "empty", + "string", + "validation" + ], + "evidence": "validNumericRange at LoanBrokerSet::preflight", + "issue_pattern": "Missing empty string validation for sfDebtMaximum", + "why_false_positive": "validNumericRange validates sfDebtMaximum for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfDebtMaximum", + "range", + "bounds", + "validation" + ], + "evidence": "validNumericRange at LoanBrokerSet::preflight", + "issue_pattern": "Missing range validation for sfDebtMaximum", + "why_false_positive": "validNumericRange validates sfDebtMaximum range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (isFieldPresent, == beast::zero) at LoanBrokerSet::preflight", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "explicit check (isFieldPresent, == beast::zero) validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (== beast::zero) at LoanBrokerSet::preflight", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "explicit check (== beast::zero) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCoverRateMinimum & sfCoverRateLiquidation", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (value_or(0) == 0) at LoanBrokerSet::preflight", + "issue_pattern": "Missing empty string validation for sfCoverRateMinimum & sfCoverRateLiquidation", + "why_false_positive": "explicit check (value_or(0) == 0) validates sfCoverRateMinimum & sfCoverRateLiquidation for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::vault(vaultID)) at LoanBrokerSet::preclaim", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "ctx.view.read(keylet::vault(vaultID)) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "account == sleVault->at(sfOwner) at LoanBrokerSet::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "account == sleVault->at(sfOwner) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::loanbroker(*brokerID)) at LoanBrokerSet::preclaim", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "ctx.view.read(keylet::loanbroker(*brokerID)) validates sfLoanBrokerID for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "LoanBrokerSet::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "LoanBrokerSet::preflight" + }, + { + "args": [], + "lineno": 56, + "name": "LoanBrokerSet::getValueFields" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 64, + "name": "LoanBrokerSet::preclaim" + }, + { + "args": [], + "lineno": 120, + "name": "LoanBrokerSet::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "LoanBrokerSet is a new feature in the lending protocol. Typical test files would be in the unit test suite under 'test/tx/Lending' or similar, e.g., 'test/tx/Lending_test.cpp', 'test/tx/LoanBrokerSet_test.cpp', or integration tests for lending transactions. Tests should cover: field presence/absence, invalid values (out of range, zero where not allowed), permission checks (ownership), object existence (vault, broker), and business logic (debt maximum not below current). Gaps may exist if edge cases (e.g., both cover rates zero/non-zero, data payload length, updating existing brokers) are not explicitly tested. If no dedicated LoanBrokerSet tests exist, coverage is likely incomplete.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation functions (validNumericRange, validDataLength, explicit checks), XRPL transaction framework", + "validation_layer": "business_logic (preflight, preclaim)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfData", + "location": "LoanBrokerSet::preflight", + "validated_by": "validDataLength", + "validates": [ + "Checks if sfData is present and not empty", + "Checks if sfData length is within maxDataPayloadLength" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfManagementFeeRate", + "location": "LoanBrokerSet::preflight", + "validated_by": "validNumericRange", + "validates": [ + "Checks if sfManagementFeeRate is within maxManagementFeeRate" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfCoverRateMinimum", + "location": "LoanBrokerSet::preflight", + "validated_by": "validNumericRange", + "validates": [ + "Checks if sfCoverRateMinimum is within maxCoverRate" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfCoverRateLiquidation", + "location": "LoanBrokerSet::preflight", + "validated_by": "validNumericRange", + "validates": [ + "Checks if sfCoverRateLiquidation is within maxCoverRate" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfDebtMaximum", + "location": "LoanBrokerSet::preflight", + "validated_by": "validNumericRange", + "validates": [ + "Checks if sfDebtMaximum is within [0, maxMPTokenAmount]" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfLoanBrokerID", + "location": "LoanBrokerSet::preflight", + "validated_by": "explicit check (isFieldPresent, == beast::zero)", + "validates": [ + "If modifying existing LoanBroker, fixed fields (sfManagementFeeRate, sfCoverRateMinimum, sfCoverRateLiquidation) must not be present", + "sfLoanBrokerID must not be beast::zero" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfVaultID", + "location": "LoanBrokerSet::preflight", + "validated_by": "explicit check (== beast::zero)", + "validates": [ + "sfVaultID must not be beast::zero" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfCoverRateMinimum & sfCoverRateLiquidation", + "location": "LoanBrokerSet::preflight", + "validated_by": "explicit check (value_or(0) == 0)", + "validates": [ + "Both sfCoverRateMinimum and sfCoverRateLiquidation must be zero or both non-zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfVaultID", + "location": "LoanBrokerSet::preclaim", + "validated_by": "ctx.view.read(keylet::vault(vaultID))", + "validates": [ + "Vault with sfVaultID must exist" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfAccount", + "location": "LoanBrokerSet::preclaim", + "validated_by": "account == sleVault->at(sfOwner)", + "validates": [ + "sfAccount must be the owner of the Vault" + ], + "validation_type": "business_logic|ownership" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfLoanBrokerID", + "location": "LoanBrokerSet::preclaim", + "validated_by": "ctx.view.read(keylet::loanbroker(*brokerID))", + "validates": [ + "LoanBroker with sfLoanBrokerID must exist if updating" + ], + "validation_type": "business_logic|existence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp.ai.md new file mode 100644 index 0000000000..a583ac972e --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanBrokerSet.cpp.ai.md @@ -0,0 +1,57 @@ +# `LoanBrokerSet.cpp` — LoanBroker Create/Update Transactor + +## Role in the System + +`LoanBrokerSet.cpp` implements the `LoanBrokerSet` transaction type, which is the entry point for managing `LoanBroker` ledger objects in the XRPL lending protocol (XLS-66). A `LoanBroker` sits between a `Vault` (the pooled liquidity provider) and individual `Loan` objects (borrowers), acting as the economic policy controller for a lending program: it defines the management fee structure, the collateral cover thresholds, and a total debt ceiling. + +The transaction is dual-purpose. The same type handles both creation and modification, distinguished entirely by the presence of `sfLoanBrokerID` in the transaction fields. When absent, a new broker is created and linked to an existing vault. When present, only the mutable subset of fields may be updated. This design avoids introducing a separate `LoanBrokerCreate` / `LoanBrokerModify` pair, keeping the protocol surface compact at the cost of some conditional logic in every phase. + +## Transaction Pipeline + +Like all XRPL transactors, `LoanBrokerSet` participates in a three-phase pipeline: + +**`checkExtraFeatures`** delegates directly to `checkLendingProtocolDependencies`, ensuring the relevant amendment gates are active before any field parsing begins. + +**`preflight`** performs stateless validation of all transaction fields in order of increasing complexity. It validates numeric ranges for `sfManagementFeeRate`, `sfCoverRateMinimum`, `sfCoverRateLiquidation`, and `sfDebtMaximum` using the `Lending::validNumericRange` helper, and checks `sfData` size against `maxDataPayloadLength`. Two business-logic invariants are enforced here: + +1. If `sfLoanBrokerID` is present (update mode), the immutable fields `sfManagementFeeRate`, `sfCoverRateMinimum`, and `sfCoverRateLiquidation` must not appear. These parameters define the economic terms seen by all borrowers under this broker and may not be retroactively changed — enforcing this at the stateless layer means no ledger access is needed to reject such attempts. + +2. `sfCoverRateMinimum` and `sfCoverRateLiquidation` must either both be zero or both be non-zero. The two rates form a meaningful pair representing the minimum collateral ratio and the liquidation threshold respectively; having one without the other would leave the cover system in an undefined configuration. + +**`preclaim`** performs ledger-state validation. It confirms the target vault exists, that the submitting account owns it, and — in update mode — that the referenced broker exists, belongs to the same vault, and is also owned by the submitter. The cross-checking against `sfVaultID` on the existing broker is the immutability guard for the vault association: a broker is permanently bound to the vault it was created for. + +A notable business rule in `preclaim`: when updating `sfDebtMaximum`, the new value must not be below the broker's current `sfDebtTotal`, unless the new value is zero. A zero `sfDebtMaximum` means "unlimited", so reducing an existing cap to unlimited is always allowed. This prevents an owner from stranding active loans in a state where the outstanding debt already exceeds the new ceiling. + +`preclaim` also calls `canAddHolding` and `checkFrozen` on the vault's pseudo-account in creation mode. These checks confirm the asset type (IOU/MPToken/XRP) can accept a new holding entry and that the vault's pseudo-account is not frozen — preconditions for the `addEmptyHolding` call that follows in `doApply`. + +## Asset Precision Validation via `getValueFields()` + +`getValueFields()` returns a static list containing only `sfDebtMaximum`. During `preclaim`, each field in this list is checked by constructing an `STAmount{asset, *value}` round-trip: if the value cannot be exactly represented as the vault's asset type, the transaction fails with `tecPRECISION_LOSS`. This matters primarily for MPToken and XRP amounts, which are integers — a `debtMaximum` that cannot be expressed in whole tokens would silently lose meaning after rounding. + +## `doApply` — Object Creation + +When no `sfLoanBrokerID` is present, `doApply` constructs the full broker object and its on-chain infrastructure: + +1. A new `SLE` is allocated via `keylet::loanbroker(account_, sequence)`, where `sequence` is the transaction's sequence value. This makes the key deterministic and collision-resistant. + +2. `dirLink` is called twice: once to insert the broker into the owner account's directory (using the default `sfOwnerNode`), and once to insert it into the vault's pseudo-account directory using `sfVaultNode`. The two-way directory linkage allows ledger traversal tools and deletion logic to efficiently enumerate all brokers associated with either an owner or a vault. + +3. `adjustOwnerCount` increments the owner's reserve count by two — one slot for the broker SLE and one for the pseudo-account that will be created next. The reserve sufficiency check (`preFeeBalance_ < view.fees().accountReserve(ownerCount)`) occurs *after* the increment, so the test reflects the final post-creation reserve requirement. + +4. `createPseudoAccount` creates a synthetic `AccountRoot` SLE keyed from `broker->key()`, with `sfLoanBrokerID` as the back-reference field type. This gives the broker an on-chain identity capable of holding assets, particularly for cover deposits made by lenders or third parties. + +5. `addEmptyHolding` initializes the pseudo-account's trust relationship with the vault's asset type (a trust line for IOUs, an MPToken slot for MPTokens, or nothing for XRP). This is necessary before any asset can flow into or out of the broker's pseudo-account. + +6. The broker's `sfLoanSequence` is initialized to 1. Each loan subsequently created under this broker will consume a sequence number, providing loan objects with a stable, broker-scoped identifier. + +## `doApply` — Object Update + +When `sfLoanBrokerID` is present, only `sfData` and `sfDebtMaximum` may be patched — the code uses `~sfField` (optional-field accessors) so absent fields are simply ignored. After mutation, `view.update(broker)` commits the change. The vault SLE is read to retrieve `vaultAsset`, which is passed to `associateAsset`. + +## `associateAsset` Convention + +Both the create and update paths end with `associateAsset(*broker, vaultAsset)`. This function iterates the broker SLE's `STTakesAsset` and `STNumber` fields and calls their virtual `associateAsset` method, which re-rounds stored numeric values to the precision implied by the asset type. The XRPL convention documented in `STTakesAsset.h` is that this call must happen at the very end of `doApply`, after all writes are complete, to avoid cumulative rounding errors. + +## Defensive Coding Patterns + +Several paths in `doApply` include `LCOV_EXCL_START`/`LCOV_EXCL_STOP` guards around `tefBAD_LEDGER` returns. These represent states that `preclaim` has already ruled out: a broker not existing after `preclaim` confirmed its existence, or a vault disappearing between phases. The guards acknowledge that the code is unreachable in correct operation but exists as a safety net against future refactoring that could accidentally break the `preclaim`/`doApply` sequencing guarantee. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp.ai.json new file mode 100644 index 0000000000..9e0ce174b0 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp.ai.json @@ -0,0 +1,481 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoanDelete::checkExtraFeatures", + "checkLendingProtocolDependencies" + ], + "entry_point": "LoanDelete::checkExtraFeatures", + "purpose": "Checks that all protocol dependencies for lending are satisfied before proceeding.", + "validation_points": [ + "checkLendingProtocolDependencies" + ] + }, + { + "call_chain": [ + "LoanDelete::preflight" + ], + "entry_point": "LoanDelete::preflight", + "purpose": "Performs initial stateless validation of the transaction, such as field presence and basic checks.", + "validation_points": [ + "ctx.tx[sfLoanID] == beast::zero" + ] + }, + { + "call_chain": [ + "LoanDelete::preclaim" + ], + "entry_point": "LoanDelete::preclaim", + "purpose": "Performs stateful validation, checking ledger state for existence and permissions.", + "validation_points": [ + "ctx.view.read(keylet::loan(loanID))", + "loanSle->at(sfPaymentRemaining) > 0", + "ctx.view.read(keylet::loanbroker(loanBrokerID))", + "loanBrokerSle->at(sfOwner) != account && loanSle->at(sfBorrower) != account" + ] + }, + { + "call_chain": [ + "LoanDelete::doApply" + ], + "entry_point": "LoanDelete::doApply", + "purpose": "Applies the transaction to the ledger, assuming all validations have passed.", + "validation_points": [ + "loanSle, borrowerSle, brokerSle, vaultSle existence checks (defensive, not user-facing validation)" + ] + } + ], + "data_flows": [ + { + "field": "sfLoanID", + "flow": [ + "ctx.tx[sfLoanID]", + "LoanDelete::preflight (checked for zero)", + "LoanDelete::preclaim (used to read loanSle)", + "LoanDelete::doApply (used to peek loanSle)" + ], + "origin": "ctx.tx[sfLoanID] (transaction input)", + "transformations": [ + "Checked for zero (invalid)", + "Used as key to fetch loan object" + ], + "validated_at": "LoanDelete::preflight, LoanDelete::preclaim" + }, + { + "field": "sfPaymentRemaining", + "flow": [ + "loanSle->at(sfPaymentRemaining)", + "LoanDelete::preclaim (checked > 0)" + ], + "origin": "loanSle->at(sfPaymentRemaining) (from ledger)", + "transformations": [ + "Checked if > 0 to prevent deletion of active loans" + ], + "validated_at": "LoanDelete::preclaim" + }, + { + "field": "sfLoanBrokerID", + "flow": [ + "loanSle->at(sfLoanBrokerID)", + "ctx.view.read(keylet::loanbroker(loanBrokerID)) (preclaim)", + "loanSle->at(sfLoanBrokerID) (doApply)" + ], + "origin": "loanSle->at(sfLoanBrokerID) (from loan object)", + "transformations": [ + "Used as key to fetch loan broker object" + ], + "validated_at": "LoanDelete::preclaim" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "LoanDelete::preclaim (used for permission check)" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared to loanBrokerSle->at(sfOwner) and loanSle->at(sfBorrower)" + ], + "validated_at": "LoanDelete::preclaim" + }, + { + "field": "sfOwner", + "flow": [ + "loanBrokerSle->at(sfOwner)", + "LoanDelete::preclaim (permission check)" + ], + "origin": "loanBrokerSle->at(sfOwner) (from loan broker object)", + "transformations": [ + "Compared to sfAccount" + ], + "validated_at": "LoanDelete::preclaim" + }, + { + "field": "sfBorrower", + "flow": [ + "loanSle->at(sfBorrower)", + "LoanDelete::preclaim (permission check)", + "LoanDelete::doApply (used to fetch borrowerSle)" + ], + "origin": "loanSle->at(sfBorrower) (from loan object)", + "transformations": [ + "Compared to sfAccount", + "Used as key to fetch borrower account" + ], + "validated_at": "LoanDelete::preclaim" + } + ], + "description": "Implements the LoanDelete transaction logic for deleting a loan in the XRPL lending protocol, including preflight, preclaim, and apply logic with permission and state checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfLoanID] == beast::zero) at preflight", + "issue_pattern": "Missing empty string validation for sfLoanID", + "why_false_positive": "explicit check (ctx.tx[sfLoanID] == beast::zero) validates sfLoanID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::loan(loanID)) at preclaim", + "issue_pattern": "Missing empty string validation for sfLoanID", + "why_false_positive": "ctx.view.read(keylet::loan(loanID)) validates sfLoanID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPaymentRemaining", + "empty", + "string", + "validation" + ], + "evidence": "loanSle->at(sfPaymentRemaining) > 0 at preclaim", + "issue_pattern": "Missing empty string validation for sfPaymentRemaining", + "why_false_positive": "loanSle->at(sfPaymentRemaining) > 0 validates sfPaymentRemaining for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::loanbroker(loanBrokerID)) at preclaim", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "ctx.view.read(keylet::loanbroker(loanBrokerID)) validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount, sfOwner, sfBorrower", + "empty", + "string", + "validation" + ], + "evidence": "loanBrokerSle->at(sfOwner) != account && loanSle->at(sfBorrower) != account at preclaim", + "issue_pattern": "Missing empty string validation for sfAccount, sfOwner, sfBorrower", + "why_false_positive": "loanBrokerSle->at(sfOwner) != account && loanSle->at(sfBorrower) != account validates sfAccount, sfOwner, sfBorrower for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanID", + "empty", + "string", + "validation" + ], + "evidence": "view.peek(keylet::loan(loanID)) at doApply", + "issue_pattern": "Missing empty string validation for sfLoanID", + "why_false_positive": "view.peek(keylet::loan(loanID)) validates sfLoanID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBorrower", + "empty", + "string", + "validation" + ], + "evidence": "view.peek(keylet::account(borrower)) at doApply", + "issue_pattern": "Missing empty string validation for sfBorrower", + "why_false_positive": "view.peek(keylet::account(borrower)) validates sfBorrower for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "view.peek(keylet::loanbroker(brokerID)) at doApply", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "view.peek(keylet::loanbroker(brokerID)) validates sfLoanBrokerID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "view.peek(keylet::vault(brokerSle->at(sfVaultID))) at doApply", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "view.peek(keylet::vault(brokerSle->at(sfVaultID))) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "LoanID in directories", + "empty", + "string", + "validation" + ], + "evidence": "view.dirRemove(...) at doApply", + "issue_pattern": "Missing empty string validation for LoanID in directories", + "why_false_positive": "view.dirRemove(...) validates LoanID in directories for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Lending protocol dependencies", + "empty", + "string", + "validation" + ], + "evidence": "checkLendingProtocolDependencies(ctx) at checkExtraFeatures", + "issue_pattern": "Missing empty string validation for Lending protocol dependencies", + "why_false_positive": "checkLendingProtocolDependencies(ctx) validates Lending protocol dependencies for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LoanDelete.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "LoanDelete::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "LoanDelete::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 20, + "name": "LoanDelete::preclaim" + }, + { + "args": [], + "lineno": 49, + "name": "LoanDelete::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "LoanDelete is a transaction type, so it is likely tested in integration/functional tests for lending. Look for test files such as 'test/tx/Lending_test.cpp', 'test/tx/LoanDelete_test.cpp', or similar. The main validation paths (loan existence, payment remaining, broker/borrower permissions) are likely covered, but edge cases such as missing broker, missing borrower, or vault errors may not be directly tested unless negative/defensive tests exist. The explicit check for sfLoanID == beast::zero is a stateless validation and should be covered by malformed transaction tests. The permission checks (account must be broker owner or borrower) should be tested for both positive and negative cases. Defensive ledger checks in doApply (tefBAD_LEDGER) may not be directly tested unless there are tests for ledger corruption or manual ledger manipulation.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in transaction processing (xrpl/rippled)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfLoanID", + "location": "preflight", + "validated_by": "explicit check (ctx.tx[sfLoanID] == beast::zero)", + "validates": [ + "LoanID must not be zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfLoanID", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::loan(loanID))", + "validates": [ + "Loan must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "sfPaymentRemaining", + "location": "preclaim", + "validated_by": "loanSle->at(sfPaymentRemaining) > 0", + "validates": [ + "Loan must have no remaining payment obligations" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "sfLoanBrokerID", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::loanbroker(loanBrokerID))", + "validates": [ + "LoanBroker must exist for given LoanBrokerID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfAccount, sfOwner, sfBorrower", + "location": "preclaim", + "validated_by": "loanBrokerSle->at(sfOwner) != account && loanSle->at(sfBorrower) != account", + "validates": [ + "Account must be either Loan Broker Owner or Loan Borrower" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfLoanID", + "location": "doApply", + "validated_by": "view.peek(keylet::loan(loanID))", + "validates": [ + "Loan must exist in ledger at apply time" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfBorrower", + "location": "doApply", + "validated_by": "view.peek(keylet::account(borrower))", + "validates": [ + "Borrower account must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfLoanBrokerID", + "location": "doApply", + "validated_by": "view.peek(keylet::loanbroker(brokerID))", + "validates": [ + "LoanBroker must exist in ledger at apply time" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfVaultID", + "location": "doApply", + "validated_by": "view.peek(keylet::vault(brokerSle->at(sfVaultID)))", + "validates": [ + "Vault must exist for the LoanBroker" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "LoanID in directories", + "location": "doApply", + "validated_by": "view.dirRemove(...)", + "validates": [ + "LoanID must be removable from owner directories" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "Not specified (returns bool, likely handled upstream)", + "field": "Lending protocol dependencies", + "location": "checkExtraFeatures", + "validated_by": "checkLendingProtocolDependencies(ctx)", + "validates": [ + "Lending protocol dependencies must be satisfied" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanDelete.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp.ai.md new file mode 100644 index 0000000000..187c529860 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanDelete.cpp.ai.md @@ -0,0 +1,61 @@ +# `LoanDelete.cpp` — Loan Teardown Transactor + +## Purpose and System Context + +`LoanDelete.cpp` implements the `LoanDelete` transaction type for the XRPL lending protocol (XLS-66). Its job is straightforward on the surface — remove a fully-repaid `Loan` ledger object — but the surrounding invariants, permission model, and debt-forgiveness edge case make it architecturally significant. + +The lending protocol maintains a three-tier object graph: a `Vault` holds pooled assets, a `LoanBroker` intermediates between the vault and individual borrowers, and `Loan` objects represent individual debt agreements. `LoanDelete` sits at the bottom of this hierarchy, cleaning up leaf nodes once borrowers have satisfied their obligations. + +`LoanDelete` inherits from `Transactor` and follows the standard XRPL three-phase processing pipeline: `preflight` (stateless), `preclaim` (stateful read-only), and `doApply` (ledger mutation). The additional `checkExtraFeatures` override simply delegates to `checkLendingProtocolDependencies`, ensuring the required protocol amendments are active before any lending transaction is processed. + +## Validation Phases + +**`preflight`** is intentionally minimal — it only checks that `sfLoanID` is not zero (`temINVALID`). Since a zero ID indicates a structurally malformed transaction, this is a cheap early exit that saves a ledger lookup. + +**`preclaim`** does the real gating work, executing four ordered checks: + +1. **Existence**: The `Loan` object must exist for the given `sfLoanID`, returning `tecNO_ENTRY` if not. +2. **Outstanding balance**: `sfPaymentRemaining > 0` returns `tecHAS_OBLIGATIONS`. This is the core business rule — active loans cannot be deleted. +3. **Broker consistency**: The loan's `sfLoanBrokerID` must resolve to an existing `LoanBroker` object. If not, `tecINTERNAL` is returned, annotated `LCOV_EXCL_LINE` — the ledger would have to be corrupted for this to happen, since the broker ID is embedded at loan creation. +4. **Authorization**: The submitting account must be either the `LoanBroker`'s owner (`sfOwner`) or the loan's borrower (`sfBorrower`). Both parties have symmetric authority to clean up a settled loan — neither needs the other's consent to release the ledger objects. + +## Apply Logic and the Dual Owner Count System + +`doApply` begins with defensive `peek` lookups on the loan, borrower account, broker, and vault SLEs. All four `tefBAD_LEDGER` returns in this phase are `LCOV_EXCL_LINE`-annotated because `preclaim` already confirmed their existence; these guards are structural contracts rather than user-facing validation paths. + +The core teardown proceeds in order: + +1. **Directory removal**: The loan's ID is removed from two owner directories — the `LoanBroker`'s pseudo-account directory (`sfLoanBrokerNode`) and the borrower's account directory (`sfOwnerNode`). Both must succeed or the entire transaction rolls back with `tefBAD_LEDGER`. +2. **SLE erasure**: The `Loan` object is deleted from the ledger. +3. **Broker owner count decrement**: `adjustOwnerCount` decrements `sfOwnerCount` on the broker SLE itself (not on the broker's pseudo-account). This count tracks the number of outstanding loans brokered — it is distinct from the pseudo-account's owner count, which governs XRP reserve requirements for objects the pseudo-account directly owns. `LoanBrokerDelete` decrements the pseudo-account count by two (for both the broker object and the pseudo-account); `LoanDelete` only touches the broker-level count. + +## The Last-Loan Debt Forgiveness Invariant + +After the owner count decrement, there is a critical edge case: if `sfOwnerCount` has reached zero — meaning this was the last outstanding loan — any residual value in `sfDebtTotal` on the broker is wiped to zero: + +```cpp +if (brokerSle->at(sfOwnerCount) == 0) +{ + auto debtTotalProxy = brokerSle->at(sfDebtTotal); + if (*debtTotalProxy != beast::zero) + { + XRPL_ASSERT_PARTS( + roundToAsset(..., debtTotalProxy, ..., Number::towards_zero) == beast::zero, + ... + "last loan, remaining debt rounds to zero"); + debtTotalProxy = 0; + } +} +``` + +The design rationale is that `sfDebtTotal` accumulates rounding dust over the lifetime of many loans. Once no loans remain, there is no mechanism to recover that dust — no more payments can reduce it. Rather than leaving the `LoanBroker` permanently stranded with unreclaimable residue (which would block `LoanBrokerDelete`, which independently checks that debt rounds to zero), the last `LoanDelete` forgives it. The `XRPL_ASSERT_PARTS` immediately before the zero assignment acts as an invariant check: the debt must already round to zero, so the forgiveness is purely cleaning up floating-point noise, not writing off real value. + +## Borrower Owner Count and Asset Association + +After the broker count is handled, `adjustOwnerCount` decrements the borrower's own account owner count — the `Loan` object was registered as a borrower-owned object at creation, so deletion releases that reserve slot. + +Finally, `associateAsset` is called on the loan, broker, and vault SLEs with the vault's asset type. The code comment is candid: "These associations shouldn't do anything, but do them just to be safe." This is a protocol convention: `STNumber` and `STTakesAsset`-derived fields may carry asset-precision metadata that needs to be kept consistent. Calling `associateAsset` at the end of `doApply` is idiomatic in the lending transactors, and the defensive call here ensures the convention is respected even across deletion paths where no write-back of these fields is expected. + +## Relationship to Sibling Transactors + +`LoanDelete` is intentionally simpler than `LoanBrokerDelete`. The broker deletion is more complex because it must handle cover-asset refunds, pseudo-account teardown, and ensuring no remaining obligations exist at the broker level. `LoanDelete` is a prerequisite: `LoanBrokerDelete` requires `sfOwnerCount == 0` on the broker, which can only happen once all loans have been deleted. The two transactors form an ordered teardown sequence mandated by the protocol. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LoanManage.cpp.ai.json new file mode 100644 index 0000000000..ba9d8a725a --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp.ai.json @@ -0,0 +1,466 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoanManage::doApply", + "LoanManage::preflight", + "LoanManage::preclaim", + "LoanManage::defaultLoan / impairLoan / unimpairLoan" + ], + "entry_point": "LoanManage::doApply", + "purpose": "Processes a LoanManage transaction, validating input and applying state changes to a loan.", + "validation_points": [ + "LoanManage::preflight", + "LoanManage::preclaim" + ] + }, + { + "call_chain": [ + "LoanManage::preflight" + ], + "entry_point": "LoanManage::preflight", + "purpose": "Performs stateless validation of the transaction (field presence, flag exclusivity).", + "validation_points": [ + "LoanManage::preflight" + ] + }, + { + "call_chain": [ + "LoanManage::preclaim" + ], + "entry_point": "LoanManage::preclaim", + "purpose": "Performs contextual validation (loan existence, state transitions, permissions).", + "validation_points": [ + "LoanManage::preclaim" + ] + } + ], + "data_flows": [ + { + "field": "sfLoanID", + "flow": [ + "ctx.tx[sfLoanID]", + "LoanManage::preflight (checked for zero)", + "LoanManage::preclaim (used to look up loanSle)", + "loanSle (used for further validation and state changes)" + ], + "origin": "ctx.tx[sfLoanID] (transaction input)", + "transformations": [ + "Checked for zero (invalid)", + "Used as key to fetch loan ledger entry" + ], + "validated_at": "LoanManage::preflight, LoanManage::preclaim" + }, + { + "field": "sfFlags", + "flow": [ + "ctx.tx[~sfFlags]", + "LoanManage::preflight (checked for mutual exclusivity)" + ], + "origin": "ctx.tx[~sfFlags] (transaction input, optional)", + "transformations": [ + "Bitmask applied (tfUniversalMask)", + "Checked that only one flag is set" + ], + "validated_at": "LoanManage::preflight" + }, + { + "field": "loan state (lsfLoanDefault, lsfLoanImpaired)", + "flow": [ + "loanSle (from ctx.view.read(keylet::loan(loanID)))", + "LoanManage::preclaim (checked for allowed state transitions)" + ], + "origin": "loanSle->isFlag(lsfLoanDefault/lsfLoanImpaired) (ledger entry)", + "transformations": [ + "Boolean checks for state", + "Used to restrict allowed transitions" + ], + "validated_at": "LoanManage::preclaim" + }, + { + "field": "sfPaymentRemaining", + "flow": [ + "loanSle", + "LoanManage::preclaim (checked for zero)" + ], + "origin": "loanSle->at(sfPaymentRemaining) (ledger entry)", + "transformations": [ + "Compared to zero to block modification if fully paid" + ], + "validated_at": "LoanManage::preclaim" + }, + { + "field": "sfNextPaymentDueDate, sfGracePeriod", + "flow": [ + "loanSle", + "LoanManage::preclaim (used to check if loan can be defaulted)" + ], + "origin": "loanSle->at(sfNextPaymentDueDate), loanSle->at(sfGracePeriod) (ledger entry)", + "transformations": [ + "Summed and passed to hasExpired()" + ], + "validated_at": "LoanManage::preclaim" + }, + { + "field": "sfLoanBrokerID, sfOwner", + "flow": [ + "loanSle", + "loanBrokerSle (from ctx.view.read(keylet::loanbroker(loanBrokerID)))", + "LoanManage::preclaim (checked for ownership)" + ], + "origin": "loanSle->at(sfLoanBrokerID), loanBrokerSle->at(sfOwner) (ledger entries)", + "transformations": [ + "Used to fetch broker ledger entry", + "Compared to tx[sfAccount]" + ], + "validated_at": "LoanManage::preclaim" + } + ], + "description": "Implements the LoanManage transaction logic for managing loan state transitions (default, impair, unimpair) in the XRPL lending protocol, including preflight checks, permission validation, and ledger updates for loans, brokers, and vaults.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfLoanID] == beast::zero) at LoanManage::preflight", + "issue_pattern": "Missing empty string validation for sfLoanID", + "why_false_positive": "explicit check (ctx.tx[sfLoanID] == beast::zero) validates sfLoanID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfFlags", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (flags mutually exclusive) at LoanManage::preflight", + "issue_pattern": "Missing empty string validation for sfFlags", + "why_false_positive": "explicit check (flags mutually exclusive) validates sfFlags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::loan(loanID)) at LoanManage::preclaim", + "issue_pattern": "Missing empty string validation for sfLoanID", + "why_false_positive": "ctx.view.read(keylet::loan(loanID)) validates sfLoanID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "loan state (lsfLoanDefault)", + "empty", + "string", + "validation" + ], + "evidence": "loanSle->isFlag(lsfLoanDefault) at LoanManage::preclaim", + "issue_pattern": "Missing empty string validation for loan state (lsfLoanDefault)", + "why_false_positive": "loanSle->isFlag(lsfLoanDefault) validates loan state (lsfLoanDefault) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "loan state (lsfLoanImpaired) and tx flag (tfLoanImpair)", + "empty", + "string", + "validation" + ], + "evidence": "loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair) at LoanManage::preclaim", + "issue_pattern": "Missing empty string validation for loan state (lsfLoanImpaired) and tx flag (tfLoanImpair)", + "why_false_positive": "loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair) validates loan state (lsfLoanImpaired) and tx flag (tfLoanImpair) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "loan state (not impaired/default) and tx flag (tfLoanUnimpair)", + "empty", + "string", + "validation" + ], + "evidence": "!(loanSle->isFlag(lsfLoanImpaired) || loanSle->isFlag(lsfLoanDefault)) && (tx.isFlag(tfLoanUnimpair)) at LoanManage::preclaim", + "issue_pattern": "Missing empty string validation for loan state (not impaired/default) and tx flag (tfLoanUnimpair)", + "why_false_positive": "!(loanSle->isFlag(lsfLoanImpaired) || loanSle->isFlag(lsfLoanDefault)) && (tx.isFlag(tfLoanUnimpair)) validates loan state (not impaired/default) and tx flag (tfLoanUnimpair) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPaymentRemaining", + "empty", + "string", + "validation" + ], + "evidence": "loanSle->at(sfPaymentRemaining) == 0 at LoanManage::preclaim", + "issue_pattern": "Missing empty string validation for sfPaymentRemaining", + "why_false_positive": "loanSle->at(sfPaymentRemaining) == 0 validates sfPaymentRemaining for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tfLoanDefault flag and payment due date", + "empty", + "string", + "validation" + ], + "evidence": "tx.isFlag(tfLoanDefault) && !hasExpired(ctx.view, loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod)) at LoanManage::preclaim", + "issue_pattern": "Missing empty string validation for tfLoanDefault flag and payment due date", + "why_false_positive": "tx.isFlag(tfLoanDefault) && !hasExpired(ctx.view, loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod)) validates tfLoanDefault flag and payment due date for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfLoanBrokerID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::loanbroker(loanBrokerID)) at LoanManage::preclaim (partial, code cut off)", + "issue_pattern": "Missing empty string validation for sfLoanBrokerID", + "why_false_positive": "ctx.view.read(keylet::loanbroker(loanBrokerID)) validates sfLoanBrokerID for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LoanManage.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "LoanManage::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "LoanManage::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 18, + "name": "LoanManage::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 36, + "name": "LoanManage::preclaim" + }, + { + "args": [ + "SLE::ref loanSle" + ], + "lineno": 74, + "name": "owedToVault" + }, + { + "args": [ + "ApplyView& view", + "SLE::ref loanSle", + "SLE::ref brokerSle", + "SLE::ref vaultSle", + "Asset const& vaultAsset", + "beast::Journal j" + ], + "lineno": 89, + "name": "LoanManage::defaultLoan" + }, + { + "args": [ + "ApplyView& view", + "SLE::ref loanSle", + "SLE::ref vaultSle", + "Asset const& vaultAsset", + "beast::Journal j" + ], + "lineno": 197, + "name": "LoanManage::impairLoan" + }, + { + "args": [ + "ApplyView& view", + "SLE::ref loanSle", + "SLE::ref vaultSle", + "Asset const& vaultAsset", + "beast::Journal j" + ], + "lineno": 232, + "name": "LoanManage::unimpairLoan" + }, + { + "args": [], + "lineno": 266, + "name": "LoanManage::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "LoanManage is a new feature, so test coverage may be limited. Typical test files would be in 'workflow/XRPLF-rippled-develop/test/src/tx/lending/' or similar, e.g., LoanManage_test.cpp or Lending_test.cpp. Tests should cover: missing/zero sfLoanID, mutually exclusive flags, non-existent loan, invalid state transitions (double impair, default after default, unimpair when unimpaired), fully paid loans, defaulting before due date, and broker ownership. Gaps may exist for edge cases (e.g., malformed flags, race conditions, or internal errors). No explicit test references are present in the code, so coverage should be verified in the test suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in C++ (Ripple/XRPL transaction engine)", + "validation_layer": "business_logic (preflight and preclaim transaction hooks)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfLoanID", + "location": "LoanManage::preflight", + "validated_by": "explicit check (ctx.tx[sfLoanID] == beast::zero)", + "validates": [ + "LoanID must not be zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "sfFlags", + "location": "LoanManage::preflight", + "validated_by": "explicit check (flags mutually exclusive)", + "validates": [ + "Only one of tfLoanDefault, tfLoanImpair, or tfLoanUnimpair can be set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfLoanID", + "location": "LoanManage::preclaim", + "validated_by": "ctx.view.read(keylet::loan(loanID))", + "validates": [ + "Loan must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "loan state (lsfLoanDefault)", + "location": "LoanManage::preclaim", + "validated_by": "loanSle->isFlag(lsfLoanDefault)", + "validates": [ + "Loan in default cannot be modified" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "loan state (lsfLoanImpaired) and tx flag (tfLoanImpair)", + "location": "LoanManage::preclaim", + "validated_by": "loanSle->isFlag(lsfLoanImpaired) && tx.isFlag(tfLoanImpair)", + "validates": [ + "Loan cannot be impaired twice" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "loan state (not impaired/default) and tx flag (tfLoanUnimpair)", + "location": "LoanManage::preclaim", + "validated_by": "!(loanSle->isFlag(lsfLoanImpaired) || loanSle->isFlag(lsfLoanDefault)) && (tx.isFlag(tfLoanUnimpair))", + "validates": [ + "Unimpaired loan cannot be unimpaired again" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfPaymentRemaining", + "location": "LoanManage::preclaim", + "validated_by": "loanSle->at(sfPaymentRemaining) == 0", + "validates": [ + "Loan cannot be modified after fully paid" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecTOO_SOON", + "field": "tfLoanDefault flag and payment due date", + "location": "LoanManage::preclaim", + "validated_by": "tx.isFlag(tfLoanDefault) && !hasExpired(ctx.view, loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod))", + "validates": [ + "Loan cannot be defaulted before next payment due date + grace period" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "not shown, but likely tecNO_ENTRY or similar", + "field": "sfLoanBrokerID", + "location": "LoanManage::preclaim (partial, code cut off)", + "validated_by": "ctx.view.read(keylet::loanbroker(loanBrokerID))", + "validates": [ + "Loan broker must exist" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanManage.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LoanManage.cpp.ai.md new file mode 100644 index 0000000000..3a2aef2814 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanManage.cpp.ai.md @@ -0,0 +1,68 @@ +# `LoanManage.cpp` — Loan Lifecycle State Transitions + +`LoanManage.cpp` implements the `LoanManage` transactor, which manages the credit-quality lifecycle of a loan object in the XRPL lending protocol (XLS-66). While `LoanSet` creates loans and `LoanPay` processes payments, `LoanManage` handles the three adversarial state transitions a loan broker needs when a borrower misses payments: marking a loan as impaired, clearing that impairment, or triggering a formal default. Because these transitions involve multi-object accounting across the `Loan`, `LoanBroker`, and `Vault` ledger entries, the transactor exists as a dedicated type rather than being folded into `LoanPay`. + +## Transaction Flags as a State Machine + +A `LoanManage` transaction carries exactly one of three optional flags — `tfLoanDefault`, `tfLoanImpair`, `tfLoanUnimpair` — or no flag at all (a deliberate no-op). The `preflight` method enforces mutual exclusivity with a standard power-of-two bitmask trick: if more than one bit in `*flagField & tfUniversalMask` is set, `(flags & (flags - 1)) != 0` is true and the transaction is rejected with `temINVALID_FLAG`. + +The underlying state machine is directional and is enforced in `preclaim`: + +- **Normal → Impaired** (`tfLoanImpair`): records a paper loss on the vault. +- **Normal → Default** (`tfLoanDefault`): finalizes the loss and triggers First-Loss Capital recovery. +- **Impaired → Normal** (`tfLoanUnimpair`): reverses the paper loss and resets the payment schedule. +- **Impaired → Default** (`tfLoanDefault`): same final settlement path as above. +- **Default → anything**: permanently blocked; a defaulted loan is a terminal state. +- **Re-impair an already-impaired loan**: blocked. +- **Unimpair a normal loan**: blocked. + +`preclaim` also enforces two other preconditions: a fully-paid loan (`sfPaymentRemaining == 0`) cannot be modified, and a default cannot be triggered before `sfNextPaymentDueDate + sfGracePeriod` has elapsed, returning `tecTOO_SOON` if attempted too early. Authorization is strictly gated: the transaction must be submitted by the account that owns the `LoanBroker` associated with the loan — not the borrower, not the vault owner. + +## `owedToVault`: A Key Accounting Identity + +The file-local helper `owedToVault()` encodes the spec formula from XLS-66 §3.2.3.2. A vault is only owed principal and its portion of interest; management fees accrue to the broker. Since `sfInterestOutstanding` is not stored directly (it would be redundant), the identity is: + +``` +owedToVault = sfTotalValueOutstanding - sfManagementFeeOutstanding +``` + +This value drives both the impairment "paper loss" and the default settlement amount, making it a load-bearing calculation used in all three operation paths. + +## `impairLoan`: Marking a Paper Loss + +When a loan is impaired, the vault records an unrealized loss equal to `owedToVault(loanSle)`. The `sfLossUnrealized` field on the vault SLE is incremented via `adjustImpreciseNumber`, which rounds the result to the vault's asset scale and clamps to zero to prevent accumulated floating-point dust from going negative. A guard checks that the resulting unrealized loss does not exceed the vault's unavailable assets (`sfAssetsTotal - sfAssetsAvailable`); exceeding that would leave the vault in an arithmetically inconsistent state and returns `tecLIMIT_EXCEEDED`. + +One subtlety: if the loan's next payment due date hasn't passed yet, `impairLoan` accelerates it to the current ledger close time. This signals that payment is immediately expected; the grace period clock starts from now rather than from the original schedule. + +The impaired flag `lsfLoanImpaired` is set on the loan object, but no funds move. This is a pure accounting mark. + +## `unimpairLoan`: Reversing the Paper Loss + +`unimpairLoan` is the mirror operation. It decrements `sfLossUnrealized` by the same `owedToVault` amount and clears `lsfLoanImpaired`. Restoring the next payment due date requires a policy decision: if the originally scheduled due date is still in the future, restore it; otherwise, set it to `now + paymentInterval`. This prevents a restored loan from being immediately overdue simply because the impairment period consumed the original window. + +## `defaultLoan`: First-Loss Capital Settlement + +`defaultLoan` is the most financially complex operation. It settles the outstanding balance by drawing on First-Loss Capital held by the `LoanBroker` pseudo-account before writing the remainder off against the vault. + +The First-Loss Capital coverage amount is computed in two rounds using `NumberRoundModeGuard` set to `Number::upward` to protect vault depositors: + +1. `minimumCover = coverRateMinimum × sfDebtTotal` (the broker's total outstanding debt scaled by the minimum coverage rate). +2. `covered = min(minimumCover × coverRateLiquidation, totalDefaultAmount)` then clamped to `sfCoverAvailable`. + +The rates are `TenthBips32` values — one ten-thousandth of a basis point — allowing fine-grained coverage ratios. The vault absorbs `totalDefaultAmount - defaultCovered` as a realized loss, rounded downward (`Number::downward`) to the vault's own scale to again favor depositors. + +The code includes a deliberate dust-handling edge case (lines 192–208): because the loan and vault may operate at different decimal scales, subtracting `vaultDefaultRounded` from total assets and simultaneously adding back `defaultCovered` can leave `sfAssetsAvailable` fractionally above `sfAssetsTotal` by a rounding artifact. The guard checks whether the exponent difference between the two values exceeds 13 orders of magnitude; if so, the difference is classified as dust and the total is bumped up to match the available amount. This prevents a fatal consistency violation from a legitimate precision mismatch. + +After accounting, the loan is zeroed out (`sfTotalValueOutstanding`, `sfPrincipalOutstanding`, `sfManagementFeeOutstanding`, `sfPaymentRemaining`, `sfNextPaymentDueDate` all set to 0), `lsfLoanDefault` is set, and `accountSend` moves the covered amount from the broker's pseudo-account to the vault's pseudo-account with `WaiveTransferFee::Yes`. This is necessary because the broker pseudo-account holds the First-Loss Capital as an on-chain balance. + +If the loan was already impaired before defaulting, `sfLossUnrealized` on the vault is also decremented by `totalDefaultAmount`, converting the paper loss into the realized loss that was just recorded. A consistency check (`vaultLossUnrealizedProxy < totalDefaultAmount → tefBAD_LEDGER`) guards this path, though it is marked `LCOV_EXCL_LINE` as theoretically unreachable in a valid ledger. + +## `doApply` and Amendment Gating + +`doApply` resolves the chain `Loan → LoanBroker → Vault` by following `sfLoanBrokerID` on the loan and `sfVaultID` on the broker, then dispatches to the appropriate operation based on the transaction flag. Missing any SLE at this stage returns `tefBAD_LEDGER` rather than a `tec` code, because these objects must have been present in `preclaim` — any absence here indicates an internal ledger inconsistency. + +After a successful operation, when the `fixSecurity3_1_3` amendment is active, `associateAsset` is called on all three SLEs. Pre-amendment, this call only happened on the no-op path. The amendment corrects this oversight so that asset index associations are maintained consistently across all `LoanManage` outcomes. + +## Defensive Patterns + +Every "impossible" state (broker SLE missing, vault SLE missing, cover available less than covered amount, unrealized loss less than default amount) is guarded with a `JLOG(j.warn())`/`JLOG(j.fatal())` followed by `tefBAD_LEDGER` or `tecINTERNAL`, all tagged `LCOV_EXCL_LINE`. This is the standard XRPL pattern: guard corrupted-ledger states with non-crashing error returns rather than assertions, so a validator encountering an unexpected ledger state fails the transaction cleanly rather than crashing the node. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LoanPay.cpp.ai.json new file mode 100644 index 0000000000..13470ae2ca --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp.ai.json @@ -0,0 +1,425 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoanPay::preflight" + ], + "entry_point": "LoanPay::preflight", + "purpose": "Performs initial stateless validation of the LoanPay transaction fields and flags.", + "validation_points": [ + "ctx.tx[sfLoanID] == beast::zero (invalid loan ID)", + "ctx.tx[sfAmount] <= beast::zero (invalid amount)", + "std::popcount(flagsSet) > 1 (mutually exclusive flags check)" + ] + }, + { + "call_chain": [ + "LoanPay::calculateBaseFee", + "Transactor::calculateBaseFee (base fee calculation)" + ], + "entry_point": "LoanPay::calculateBaseFee", + "purpose": "Calculates the transaction fee, with logic depending on loan state and payment type.", + "validation_points": [ + "loanSle = view.read(keylet::loan(loanID)) (loan existence)", + "brokerSle = view.read(keylet::loanbroker(loanSle->at(sfLoanBrokerID))) (broker existence)", + "vaultSle = view.read(keylet::vault(brokerSle->at(sfVaultID))) (vault existence)", + "asset != amount.asset() (asset match check)" + ] + }, + { + "call_chain": [ + "LoanPay::preclaim" + ], + "entry_point": "LoanPay::preclaim", + "purpose": "Performs stateful validation (ledger-dependent checks) before transaction application.", + "validation_points": [ + "Likely checks for existence and correctness of loan, broker, vault, and payment state (not fully shown in provided code)." + ] + }, + { + "call_chain": [ + "LoanPay::doApply" + ], + "entry_point": "LoanPay::doApply", + "purpose": "Applies the transaction to the ledger after all validations pass.", + "validation_points": [ + "Assumes all previous validations have passed; may contain additional runtime checks." + ] + } + ], + "data_flows": [ + { + "field": "sfLoanID", + "flow": [ + "ctx.tx[sfLoanID]", + "LoanPay::preflight (checked for zero)", + "LoanPay::calculateBaseFee (used to look up loanSle)", + "LoanPay::preclaim (likely used for further checks)", + "LoanPay::doApply (used for ledger mutation)" + ], + "origin": "ctx.tx[sfLoanID] (transaction input)", + "transformations": [ + "Checked for zero (invalid)", + "Used as key for ledger lookup" + ], + "validated_at": "LoanPay::preflight" + }, + { + "field": "sfAmount", + "flow": [ + "ctx.tx[sfAmount]", + "LoanPay::preflight (checked for <= 0)", + "LoanPay::calculateBaseFee (used for fee estimation and payment calculations)", + "LoanPay::doApply (used for actual payment logic)" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Checked for <= 0 (invalid)", + "Used in arithmetic for payment estimation" + ], + "validated_at": "LoanPay::preflight" + }, + { + "field": "flags (tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment)", + "flow": [ + "ctx.tx.getFlags()", + "LoanPay::preflight (checked for mutual exclusivity)", + "LoanPay::calculateBaseFee (affects fee logic)", + "LoanPay::doApply (affects payment logic)" + ], + "origin": "ctx.tx.getFlags() (transaction input)", + "transformations": [ + "Bitmask and popcount to ensure only one flag is set" + ], + "validated_at": "LoanPay::preflight" + }, + { + "field": "sfLoanBrokerID", + "flow": [ + "loanSle->at(sfLoanBrokerID)", + "view.read(keylet::loanbroker(...)) (lookup brokerSle)", + "brokerSle used for further validation and fee calculation" + ], + "origin": "loanSle->at(sfLoanBrokerID) (from ledger)", + "transformations": [ + "Used as key for ledger lookup" + ], + "validated_at": "LoanPay::calculateBaseFee" + }, + { + "field": "sfAsset", + "flow": [ + "vaultSle->at(sfAsset)", + "compared to amount.asset() in calculateBaseFee" + ], + "origin": "vaultSle->at(sfAsset) (from ledger)", + "transformations": [ + "Compared for equality" + ], + "validated_at": "LoanPay::calculateBaseFee" + } + ], + "description": "Implements the LoanPay transaction logic for the XRPL lending protocol, including preflight checks, fee calculation, preclaim validation, and application of loan payments, updating loan, broker, and vault state accordingly.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLoanID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfLoanID] == beast::zero) at LoanPay::preflight", + "issue_pattern": "Missing empty string validation for sfLoanID", + "why_false_positive": "explicit check (ctx.tx[sfLoanID] == beast::zero) validates sfLoanID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfAmount] <= beast::zero) at LoanPay::preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "explicit check (ctx.tx[sfAmount] <= beast::zero) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfAmount] <= beast::zero) at LoanPay::preflight", + "issue_pattern": "Missing range validation for sfAmount", + "why_false_positive": "explicit check (ctx.tx[sfAmount] <= beast::zero) validates sfAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "LoanPay flags (tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (std::popcount(flagsSet) > 1) at LoanPay::preflight", + "issue_pattern": "Missing empty string validation for LoanPay flags (tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment)", + "why_false_positive": "explicit check (std::popcount(flagsSet) > 1) validates LoanPay flags (tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Loan existence (sfLoanID)", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::loan(loanID)) at LoanPay::calculateBaseFee", + "issue_pattern": "Missing empty string validation for Loan existence (sfLoanID)", + "why_false_positive": "view.read(keylet::loan(loanID)) validates Loan existence (sfLoanID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "LoanBroker existence (sfLoanBrokerID)", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::loanbroker(loanSle->at(sfLoanBrokerID))) at LoanPay::calculateBaseFee", + "issue_pattern": "Missing empty string validation for LoanBroker existence (sfLoanBrokerID)", + "why_false_positive": "view.read(keylet::loanbroker(loanSle->at(sfLoanBrokerID))) validates LoanBroker existence (sfLoanBrokerID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Vault existence (sfVaultID)", + "empty", + "string", + "validation" + ], + "evidence": "view.read(keylet::vault(brokerSle->at(sfVaultID))) at LoanPay::calculateBaseFee", + "issue_pattern": "Missing empty string validation for Vault existence (sfVaultID)", + "why_false_positive": "view.read(keylet::vault(brokerSle->at(sfVaultID))) validates Vault existence (sfVaultID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Loan payment remaining (sfPaymentRemaining)", + "empty", + "string", + "validation" + ], + "evidence": "loanSle->at(sfPaymentRemaining) <= loanPaymentsPerFeeIncrement at LoanPay::calculateBaseFee", + "issue_pattern": "Missing empty string validation for Loan payment remaining (sfPaymentRemaining)", + "why_false_positive": "loanSle->at(sfPaymentRemaining) <= loanPaymentsPerFeeIncrement validates Loan payment remaining (sfPaymentRemaining) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Loan payment due date (sfNextPaymentDueDate)", + "empty", + "string", + "validation" + ], + "evidence": "hasExpired(view, loanSle->at(sfNextPaymentDueDate)) at LoanPay::calculateBaseFee", + "issue_pattern": "Missing empty string validation for Loan payment due date (sfNextPaymentDueDate)", + "why_false_positive": "hasExpired(view, loanSle->at(sfNextPaymentDueDate)) validates Loan payment due date (sfNextPaymentDueDate) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LoanPay.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 11, + "name": "LoanPay::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 16, + "name": "LoanPay::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "LoanPay::preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 44, + "name": "LoanPay::calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 109, + "name": "LoanPay::preclaim" + }, + { + "args": [], + "lineno": 170, + "name": "LoanPay::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "LoanPay is a new feature in the lending protocol. Typical test files would be in 'workflow/XRPLF-rippled-develop/test/src/tx/lending/' or similar, e.g., LoanPay_test.cpp, LendingHelpers_test.cpp, or integration tests for lending transactions. Tests should cover: (1) invalid/missing sfLoanID, (2) invalid/missing sfAmount, (3) multiple flags set, (4) non-existent loan/broker/vault, (5) asset mismatch, (6) correct fee calculation for various payment types. Gaps may exist if edge cases (e.g., boundary values for payments, expired loans, or malformed flags) are not explicitly tested. If no such test files exist, coverage is lacking for these validation paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in C++ (no external validation framework detected)", + "validation_layer": "business_logic (preflight and fee calculation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfLoanID", + "location": "LoanPay::preflight", + "validated_by": "explicit check (ctx.tx[sfLoanID] == beast::zero)", + "validates": [ + "sfLoanID must not be zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "LoanPay::preflight", + "validated_by": "explicit check (ctx.tx[sfAmount] <= beast::zero)", + "validates": [ + "sfAmount must be greater than zero" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "LoanPay flags (tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment)", + "location": "LoanPay::preflight", + "validated_by": "explicit check (std::popcount(flagsSet) > 1)", + "validates": [ + "Only one LoanPay flag can be set per transaction" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (returns normalCost, error handled in preclaim)", + "field": "Loan existence (sfLoanID)", + "location": "LoanPay::calculateBaseFee", + "validated_by": "view.read(keylet::loan(loanID))", + "validates": [ + "Loan with sfLoanID must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (returns normalCost, error handled in preclaim)", + "field": "LoanBroker existence (sfLoanBrokerID)", + "location": "LoanPay::calculateBaseFee", + "validated_by": "view.read(keylet::loanbroker(loanSle->at(sfLoanBrokerID)))", + "validates": [ + "LoanBroker with sfLoanBrokerID must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (returns normalCost, error handled in preclaim)", + "field": "Vault existence (sfVaultID)", + "location": "LoanPay::calculateBaseFee", + "validated_by": "view.read(keylet::vault(brokerSle->at(sfVaultID)))", + "validates": [ + "Vault with sfVaultID must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (returns normalCost)", + "field": "Loan payment remaining (sfPaymentRemaining)", + "location": "LoanPay::calculateBaseFee", + "validated_by": "loanSle->at(sfPaymentRemaining) <= loanPaymentsPerFeeIncrement", + "validates": [ + "If fewer than loanPaymentsPerFeeIncrement payments remain, skip extra fee computation" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (returns normalCost)", + "field": "Loan payment due date (sfNextPaymentDueDate)", + "location": "LoanPay::calculateBaseFee", + "validated_by": "hasExpired(view, loanSle->at(sfNextPaymentDueDate))", + "validates": [ + "If payment is late and late payment flag is not set, transaction will fail" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanPay.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LoanPay.cpp.ai.md new file mode 100644 index 0000000000..63bf466c19 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanPay.cpp.ai.md @@ -0,0 +1,41 @@ +# `LoanPay.cpp` — XRPL Lending Protocol: Loan Payment Transactor + +`LoanPay` implements the transaction that allows a borrower to make scheduled or special-case payments on an outstanding loan in the XRPL lending protocol (XLS-66). It sits in the lending transactor subdirectory alongside `LoanManage`, and follows the standard XRPL transactor lifecycle: `preflight` → `calculateBaseFee` → `preclaim` → `doApply`. The class inherits from `Transactor` and overrides only the parts of that pipeline that require lending-specific logic. + +## Payment Type Taxonomy + +Every `LoanPay` transaction carries at most one of three optional flags: `tfLoanLatePayment`, `tfLoanFullPayment`, or `tfLoanOverpayment`. These are enforced as mutually exclusive in `preflight` using `std::popcount` on the isolated flag bits — a compact and future-proof guard. If no flag is set, the payment is treated as a regular on-time periodic payment. The `preflight` uses a `static_assert` to verify at compile time that the bitmask constants are consistent with the mask field, catching stale definitions before they reach production. + +## Dynamic Fee Scaling in `calculateBaseFee` + +Most XRPL transactors defer entirely to `Transactor::calculateBaseFee`. `LoanPay` is unusual in that it can process multiple amortization periods in a single transaction when the borrower overpays. To prevent attackers from submitting a very large overpayment that forces the network to compute hundreds of payment periods for the price of a single base fee, `calculateBaseFee` estimates the number of payments that will be made and charges one base fee per `loanPaymentsPerFeeIncrement` payments (rounding up). + +The fee estimation walks the ledger hierarchy — loan → loanbroker → vault — to retrieve the asset, periodic payment, and service fee. If any leg of this chain is absent or mismatched, `calculateBaseFee` returns the normal cost rather than failing; the error is deferred to `preclaim`. The rounding mode for the payment count estimate switches between `upward` (for overpayments, which get counted as full payments) and `downward` (for regular cases), via `NumberRoundModeGuard`. Late payment and full payment transactions are capped at the single base fee because they perform a fixed, bounded amount of work regardless of the transaction amount. + +## Preclaim: Stateful Validation + +`preclaim` confirms ownership (`sfBorrower == account`), that the loan is not already paid off, and that the asset of the payment amount matches the vault's configured asset. It then runs a series of compliance checks using helpers from `TokenHelpers`: `checkFrozen`, `checkDeepFrozen`, and `requireAuth`. The vault's pseudo-account must be able to receive funds (`checkDeepFrozen`), because that is where principal and interest will ultimately flow. + +The balance check uses `accountHolds` with `SpendableHandling::shFULL_BALANCE` and deliberately rejects partial payments: if the account cannot cover the full `sfAmount` specified in the transaction, the transaction fails even if the actual loan obligation is smaller. This makes the semantics explicit — a borrower is stating they will pay exactly `sfAmount`, and the system holds them to it. + +Overpayment permission is also enforced here: if the transaction sets `tfLoanOverpayment` but the loan's `lsfLoanOverpayment` flag is not set, the transaction is refused. Post-amendment (`fixSecurity3_1_3`) this returns `tecNO_PERMISSION`; pre-amendment it returns `temINVALID_FLAG`. The conditional error code selection via `ctx.view.rules().enabled(fixSecurity3_1_3)` is a pattern used throughout the XRPL codebase to fix incorrect error codes without breaking replayed historical ledgers. + +## `doApply`: Three-Party Funds Flow + +Application proceeds in three logical phases: state mutation, then balance updates, then fund transfer. + +**Impairment reversal.** If the loan carries `lsfLoanImpaired`, `doApply` calls `LoanManage::unimpairLoan` before processing the payment. An impaired loan represents a paper loss recorded against the vault's `sfLossUnrealized` field. When the borrower actually pays, the paper loss is reversed and the loan's next-due-date is recalculated. This must happen before `loanMakePayment` is called because `unimpairLoan` may adjust `sfNextPaymentDueDate`, which affects how the payment itself is classified. + +**Payment computation.** `loanMakePayment` (from `LendingHelpers`) does the heavy amortization arithmetic, returning a `LoanPaymentParts` struct that breaks the payment into `principalPaid`, `interestPaid`, `feePaid`, and a `valueChange` component. The `valueChange` is non-zero for late, full, and overpayments: a positive value change means penalty interest has increased the loan's outstanding value; a negative value change means an overpayment or discounted early payoff has reduced it. On success, `loanMakePayment` has already modified `loanSle` in-place, so `doApply` calls `view.update(loanSle)` immediately after. + +**Broker fee routing.** The broker's management fee can be directed to one of two places: the broker owner's account (the normal case) or the broker's pseudo-account (the first-loss capital pool). The decision is made with a lambda that conservatively checks whether `coverAvailable` meets the minimum threshold (`tenthBipsOfValue(debtTotal, coverRateMinimum)`, rounded upward), and that the broker owner is neither deep-frozen nor unauthorized. If the owner cannot receive funds, the fee accumulates in the cover pool rather than blocking the payment. Only if both the owner path and the pseudo-account path are unavailable (both deep-frozen) does `doApply` return an error. + +**Scale reconciliation.** The vault and loan may operate at different numeric scales. Amounts credited to the vault (`totalPaidToVaultRounded`) are rounded downward to the vault's scale, preventing the vault from being credited more than the actual funds transferred. Amounts used to reduce the broker's `sfDebtTotal` use `adjustImpreciseNumber`, which re-rounds to the vault scale after adjustment and clamps to zero, preventing small accumulated rounding errors across multiple loans from ever producing a negative debt balance. + +**Atomic transfer.** The actual funds move via a single `accountSendMulti` call — borrower → vault pseudo-account for `totalPaidToVaultRounded`, and borrower → broker payee for `totalPaidToBroker`. Transfer fees are waived (`WaiveTransferFee::Yes`) because this is a structured obligation repayment, not a free-market transfer. Before the multi-send, if the broker payee is the broker's own account (i.e., the same as the transaction submitter), `addEmptyHolding` may recreate a token trust line the broker may have previously deleted. + +**Debug-only invariant verification.** Two `#if !NDEBUG` blocks sandwich the fund transfer. The first verifies that `sfAssetsAvailable` in the vault ledger entry agrees with the actual balance of the vault's pseudo-account before the payment. The second, after the transfer, verifies fund conservation (sum of borrower + vault + broker balances is unchanged), that no account balance went negative, and that vault and broker balances did not decrease. These checks are structurally important for testing but are too expensive for production; the `XRPL_ASSERT_PARTS` macros used in both debug and release code serve as lightweight postcondition guards for the release build. + +## Object Graph Updated + +Every successful `doApply` mutates three ledger objects: `loanSle` (outstanding balances and payment count), `brokerSle` (`sfDebtTotal`, and optionally `sfCoverAvailable`), and `vaultSle` (`sfAssetsAvailable` and `sfAssetsTotal`). The `valueChange` from the payment computation threads through the vault update: it adjusts `sfAssetsTotal` while the rounded payment amount adjusts `sfAssetsAvailable`, preserving the invariant that available ≤ total. The invariant is asserted both before and after rounding adjustments, with a fatal log and `tecINTERNAL` as a last-resort guard if the math somehow violates it despite all precautions. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp.ai.json b/src/libxrpl/tx/transactors/lending/LoanSet.cpp.ai.json new file mode 100644 index 0000000000..dd3e3618cd --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp.ai.json @@ -0,0 +1,227 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoanSet::preflight", + "xrpl::detail::preflightCheckSigningKey (optional)", + "xrpl::detail::preflightCheckSimulateKeys (optional)" + ], + "entry_point": "LoanSet::preflight", + "purpose": "Performs initial validation of a LoanSet transaction before it is processed. Checks flags, required fields, numeric ranges, and signatures.", + "validation_points": [ + "LoanSet::preflight: Checks for tfInnerBatchTxn, presence of sfCounterparty, and CounterpartySignature.", + "LoanSet::preflight: Validates data length, numeric minimums, and numeric ranges for various fields.", + "LoanSet::preflight: Calls preflightCheckSigningKey and preflightCheckSimulateKeys for signature validation." + ] + }, + { + "call_chain": [ + "LoanSet::checkExtraFeatures", + "checkLendingProtocolDependencies" + ], + "entry_point": "LoanSet::checkExtraFeatures", + "purpose": "Checks if lending protocol dependencies are met for the transaction.", + "validation_points": [ + "checkLendingProtocolDependencies" + ] + }, + { + "call_chain": [ + "LoanSet::checkSign", + "Transactor::checkSign" + ], + "entry_point": "LoanSet::checkSign", + "purpose": "Validates the transaction's signature(s) after preflight.", + "validation_points": [ + "Transactor::checkSign" + ] + } + ], + "data_flows": [ + { + "field": "sfCounterpartySignature", + "flow": [ + "ctx.tx", + "LoanSet::preflight: tx.isFieldPresent(sfCounterpartySignature)", + "LoanSet::preflight: tx.getFieldObject(sfCounterpartySignature)", + "xrpl::detail::preflightCheckSigningKey (if present)", + "xrpl::detail::preflightCheckSimulateKeys (if present)" + ], + "origin": "ctx.tx (the transaction object)", + "transformations": [ + "Checked for presence", + "Extracted as STObject", + "Passed to signature validation functions" + ], + "validated_at": "LoanSet::preflight" + }, + { + "field": "sfPrincipalRequested", + "flow": [ + "ctx.tx", + "LoanSet::preflight: auto const p = tx[sfPrincipalRequested]", + "LoanSet::preflight: if (p <= 0) return temINVALID" + ], + "origin": "ctx.tx", + "transformations": [ + "Extracted as numeric value", + "Checked for > 0" + ], + "validated_at": "LoanSet::preflight" + }, + { + "field": "sfLoanOriginationFee", + "flow": [ + "ctx.tx", + "LoanSet::preflight: tx[~sfLoanOriginationFee]", + "LoanSet::preflight: validNumericRange(tx[~sfLoanOriginationFee], p)" + ], + "origin": "ctx.tx", + "transformations": [ + "Extracted as optional numeric value", + "Checked for valid numeric range (<= principal requested)" + ], + "validated_at": "LoanSet::preflight" + }, + { + "field": "sfData", + "flow": [ + "ctx.tx", + "LoanSet::preflight: tx[~sfData]", + "LoanSet::preflight: validDataLength(tx[~sfData], maxDataPayloadLength)" + ], + "origin": "ctx.tx", + "transformations": [ + "Extracted as optional blob", + "Checked for non-empty and valid length" + ], + "validated_at": "LoanSet::preflight" + }, + { + "field": "sfPaymentInterval", + "flow": [ + "ctx.tx", + "LoanSet::preflight: tx[~sfPaymentInterval]", + "LoanSet::preflight: validNumericMinimum(paymentInterval, LoanSet::minPaymentInterval)" + ], + "origin": "ctx.tx", + "transformations": [ + "Extracted as optional numeric value", + "Checked for minimum value" + ], + "validated_at": "LoanSet::preflight" + }, + { + "field": "sfGracePeriod", + "flow": [ + "ctx.tx", + "LoanSet::preflight: tx[~sfGracePeriod]", + "LoanSet::preflight: validNumericRange(gracePeriod, paymentInterval.value_or(default), defaultGracePeriod)" + ], + "origin": "ctx.tx", + "transformations": [ + "Extracted as optional numeric value", + "Checked for valid range relative to payment interval" + ], + "validated_at": "LoanSet::preflight" + }, + { + "field": "sfLoanBrokerID", + "flow": [ + "ctx.tx", + "LoanSet::preflight: ctx.tx[~sfLoanBrokerID]", + "LoanSet::preflight: if (brokerID && *brokerID == beast::zero) return temINVALID" + ], + "origin": "ctx.tx", + "transformations": [ + "Extracted as optional ID", + "Checked for non-zero" + ], + "validated_at": "LoanSet::preflight" + } + ], + "description": "Implements the LoanSet transaction logic for the XRPL lending protocol, including preflight, preclaim, signature checks, fee calculation, and application of a new loan to the ledger.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/lending/LoanSet.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 9, + "name": "LoanSet::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 14, + "name": "LoanSet::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 19, + "name": "LoanSet::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 87, + "name": "LoanSet::checkSign" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 110, + "name": "LoanSet::calculateBaseFee" + }, + { + "args": [], + "lineno": 134, + "name": "LoanSet::getValueFields" + }, + { + "args": [ + "ReadView const& view" + ], + "lineno": 144, + "name": "getStartDate" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 148, + "name": "LoanSet::preclaim" + }, + { + "args": [], + "lineno": 232, + "name": "LoanSet::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "LoanSet transaction validation is likely covered by unit/integration tests in the rippled codebase, especially in files such as 'test/tx/LoanSet_test.cpp', 'test/lending/Lending_test.cpp', or similar. These tests should cover valid and invalid transactions, missing required fields, invalid numeric ranges, and signature checks. However, edge cases such as malformed CounterpartySignature, maximum/minimum numeric values, and batch transaction scenarios may not be exhaustively tested unless explicitly present in the test suite. There may also be gaps in testing for protocol feature toggles (e.g., featureBatch) and for all possible combinations of optional fields.", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/lending/LoanSet.cpp.ai.md b/src/libxrpl/tx/transactors/lending/LoanSet.cpp.ai.md new file mode 100644 index 0000000000..4d9fd898c3 --- /dev/null +++ b/src/libxrpl/tx/transactors/lending/LoanSet.cpp.ai.md @@ -0,0 +1,64 @@ +# `LoanSet.cpp` — Loan Creation Transactor + +## Role in the System + +`LoanSet.cpp` implements the `LoanSet` transaction, which is the entry point for creating a new loan in XRPL's on-ledger lending protocol (XLS-66). When applied, the transaction disburses principal from a vault's pool of assets to a borrower and records the full amortization schedule as a `Loan` ledger object. It sits at the intersection of the vault, loan broker, and borrower account subsystems, and must reconcile the financial properties of a structured loan with the protocol's ledger data model. + +The lending protocol depends on the `SingleAssetVault` feature and its own feature flag, verified via `checkLendingProtocolDependencies()` in `checkExtraFeatures`. + +## Dual-Signature Architecture + +The most distinctive aspect of `LoanSet` is that it requires two parties to agree: the borrower (the transaction submitter, `sfAccount`) and the lender-side representative (the `sfCounterparty`, which defaults to the `LoanBroker`'s owner if omitted). The counterparty's consent is expressed through a `sfCounterpartySignature` sub-object embedded in the transaction. This sub-object can hold either a single signature (`sfTxnSignature`) or a multisignature list (`sfSigners`). + +`calculateBaseFee` accounts for this by adding one extra `baseFee` per counterparty signer, mirroring the way the base transactor prices standard multisignatures. This prevents cheap DoS via bloated counterparty signature arrays. Notably, the base class's per-signer cost is *not* applied to the `CounterpartySignature` — `LoanSet` computes the count directly from the sub-object to avoid double-counting. + +The batch inner transaction path is a deliberate carve-out: when `tfInnerBatchTxn` is set, `sfCounterparty` may be absent and `sfCounterpartySignature` is skipped entirely in `preflight`. This allows batch-orchestrated lending flows where co-signing is handled at a higher protocol layer. + +## Validation Pipeline + +`preflight` is purely stateless. It validates field formats and numeric ranges without touching the ledger: + +- `sfPrincipalRequested` must be strictly positive. +- All rate fields (`sfInterestRate`, `sfLateInterestRate`, `sfCloseInterestRate`, `sfOverpaymentInterestRate`, `sfOverpaymentFee`) are checked against protocol-defined maximums using `validNumericRange`. +- Fee fields (`sfLoanServiceFee`, `sfLatePaymentFee`, `sfClosePaymentFee`) are checked for a non-negative minimum. +- `sfLoanOriginationFee` is validated against `sfPrincipalRequested` as an upper bound — it cannot exceed what is borrowed. +- `sfPaymentInterval` must be at least `minPaymentInterval` (60 seconds). +- `sfGracePeriod` is bounded above by the payment interval itself. A grace period longer than the interval would mean the late window for one payment overlaps the due date of the next, which the protocol prohibits. + +`checkSign` resolves the counterparty identity (from `sfCounterparty` or from the broker's `sfOwner` field) and delegates to `Transactor::checkSign` once for the primary signer and again for the counterparty signature sub-object. + +## Time Overflow Guard in `preclaim` + +Before touching any ledger objects, `preclaim` performs a dedicated arithmetic overflow check. The last moment at which a payment can be due is `startDate + (paymentInterval × paymentTotal) + gracePeriod`. The `sfNextPaymentDueDate` field is `std::uint32_t`, so the protocol's time horizon is capped at `4,294,967,295` seconds (roughly year 2106 in POSIX time). If any combination of user-supplied intervals and totals would push the final grace period past that limit, the transaction returns `tecKILLED`. + +This check is done using only the current ledger close time and the transaction fields, before acquiring any SLE locks, because it is cheap and catches a class of inputs that would otherwise require very large payment totals to detect. + +## Business Rule Checks in `preclaim` + +`preclaim` then loads the `LoanBroker`, `Vault`, and `Borrower` account objects and enforces: + +- The broker must exist; if a `sfCounterparty` is specified explicitly but the broker is gone, `tecNO_ENTRY` is returned. +- Either `sfAccount` or `sfCounterparty` must be the broker's owner. This enforces that one party in every loan is the controlling lender. +- The vault must not have exceeded `sfAssetsMaximum` (if set). +- A two-stage precision check ensures every fee and principal value can be represented in the vault's asset type. For MPTs, which have integer-only precision, even a fractional fee would cause silent truncation; `tecPRECISION_LOSS` prevents this before any funds move. +- Frozen account checks cascade through all parties: the vault pseudo-account and borrower cannot be frozen (they are sending/receiving assets), the broker pseudo-account and broker owner cannot be *deep* frozen (they receive fee payments). + +## `doApply` — Ledger Mutation + +`doApply` materializes the loan. Its logic follows this sequence: + +**1. Financial viability checks.** The vault's `sfAssetsAvailable` must cover `sfPrincipalRequested`. The broker's `sfDebtTotal + newDebtDelta` must not exceed `sfDebtMaximum`. The broker's first-loss capital (`sfCoverAvailable`) must satisfy the cover rate minimum against the new total debt. The cover check deliberately rounds the required cover *upward* via `NumberRoundModeGuard(Number::upward)`, making the solvency test conservative. + +**2. Loan property computation.** `computeLoanProperties()` derives the full amortization structure: the periodic payment amount, the loan scale (the number of decimal places needed to represent periodic payments without rounding errors), and the initial `LoanState` — breaking down total outstanding value into principal, interest due, and management fee. A second precision check runs here against the computed loan scale; this is distinct from the `preclaim` check because the scale is not known until the amortization formula runs (relevant for IOU assets). + +**3. Fund disbursement.** A single `accountSendMulti` call transfers two amounts simultaneously from the vault pseudo-account: `principalRequested - originationFee` to the borrower, and `originationFee` to the broker owner. The atomicity of this call means the origination fee can never be credited without also delivering the net principal. Trust lines or MPT holdings are created on-demand for the borrower and broker owner if they do not already exist. + +**4. Loan SLE creation.** The `Loan` object is keyed by `keylet::loan(brokerID, loanSequence)`, giving each loan a stable, globally unique identity. A lambda `setLoanField` copies optional transaction fields onto the loan directly, correctly handling absent optional fields by applying their default values. The first `sfNextPaymentDueDate` is set to `startDate + paymentInterval`. + +**5. Vault and broker state updates.** The vault's `sfAssetsAvailable` decrements by `principalRequested`, while `sfAssetsTotal` increments by the interest component (`state.interestDue`). This correctly reflects that the vault now holds a larger claim (principal + interest) but has less liquid cash available. The broker's `sfDebtTotal` is updated with `adjustImpreciseNumber`, which re-rounds to the vault scale and clamps to zero to absorb accumulated floating-point dust. The broker's `sfLoanSequence` is incremented; wrapping to zero returns `tecMAX_SEQUENCE_REACHED`. + +**6. Directory linking.** The loan is linked into both the broker pseudo-account's directory (`sfLoanBrokerNode`) and the borrower's owner directory (`sfOwnerNode`). The borrower's owner count is incremented first and checked against their XRP reserve *before* funds move, ensuring the borrower can always maintain the reserve obligation that the new loan object creates. + +## Relationship to Sibling Files + +`LoanPay.cpp` handles subsequent payments against loans created here. `LoanDelete.cpp` handles cleanup when a loan reaches zero balance. `LoanBrokerSet.cpp` establishes the broker object that `LoanSet` requires. `LendingHelpers.cpp` provides the shared amortization mathematics (`computeLoanProperties`, `constructLoanState`, `checkLoanGuards`) that `LoanSet` delegates all financial calculation to, keeping the transactor itself focused on ledger mutation rather than financial modeling. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp.ai.json b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp.ai.json new file mode 100644 index 0000000000..219f0f232c --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp.ai.json @@ -0,0 +1,604 @@ +{ + "args": [ + { + "lineno": 9, + "name": "ctx" + }, + { + "lineno": 28, + "name": "ctx" + }, + { + "lineno": 210, + "name": "from" + }, + { + "lineno": 210, + "name": "to" + }, + { + "lineno": 210, + "name": "amount" + }, + { + "lineno": 232, + "name": "buyer" + }, + { + "lineno": 232, + "name": "seller" + }, + { + "lineno": 232, + "name": "nftokenID" + }, + { + "lineno": 273, + "name": "offer" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "NFTokenAcceptOffer::doApply", + "NFTokenAcceptOffer::acceptOffer", + "NFTokenAcceptOffer::pay", + "NFTokenAcceptOffer::transferNFToken" + ], + "entry_point": "NFTokenAcceptOffer::doApply", + "purpose": "Executes the main logic for accepting an NFT offer, including payment and token transfer.", + "validation_points": [ + "NFTokenAcceptOffer::preflight", + "NFTokenAcceptOffer::preclaim" + ] + }, + { + "call_chain": [ + "NFTokenAcceptOffer::preflight" + ], + "entry_point": "NFTokenAcceptOffer::preflight", + "purpose": "Performs initial stateless validation of the transaction fields before any ledger access.", + "validation_points": [ + "NFTokenAcceptOffer::preflight" + ] + }, + { + "call_chain": [ + "NFTokenAcceptOffer::preclaim" + ], + "entry_point": "NFTokenAcceptOffer::preclaim", + "purpose": "Performs stateful validation, checking existence and correctness of referenced offers.", + "validation_points": [ + "NFTokenAcceptOffer::preclaim" + ] + } + ], + "data_flows": [ + { + "field": "sfNFTokenBuyOffer", + "flow": [ + "Transaction input", + "NFTokenAcceptOffer::preflight (checked for presence)", + "NFTokenAcceptOffer::preclaim (passed to checkOffer lambda)", + "checkOffer: offer existence, zero check, expiration, amount sign", + "Used in doApply for offer processing" + ], + "origin": "ctx.tx[~sfNFTokenBuyOffer] (transaction input)", + "transformations": [ + "Optional presence check", + "Offer SLE lookup", + "Validation of offer fields" + ], + "validated_at": "preflight, preclaim" + }, + { + "field": "sfNFTokenSellOffer", + "flow": [ + "Transaction input", + "NFTokenAcceptOffer::preflight (checked for presence)", + "NFTokenAcceptOffer::preclaim (passed to checkOffer lambda)", + "checkOffer: offer existence, zero check, expiration, amount sign", + "Used in doApply for offer processing" + ], + "origin": "ctx.tx[~sfNFTokenSellOffer] (transaction input)", + "transformations": [ + "Optional presence check", + "Offer SLE lookup", + "Validation of offer fields" + ], + "validated_at": "preflight, preclaim" + }, + { + "field": "sfNFTokenBrokerFee", + "flow": [ + "Transaction input", + "NFTokenAcceptOffer::preflight (checked for presence, value > 0, only in brokered mode)", + "Used in doApply for broker fee calculation" + ], + "origin": "ctx.tx[~sfNFTokenBrokerFee] (transaction input)", + "transformations": [ + "Optional presence check", + "Value comparison (must be > 0)", + "Checked only if both buy and sell offers are present" + ], + "validated_at": "preflight" + }, + { + "field": "sfAmount (in offer SLE)", + "flow": [ + "Offer SLE (ledger)", + "checkOffer: negative check", + "preclaim: asset match, payment sufficiency", + "doApply: payment logic" + ], + "origin": "Offer SLE (from ledger, via checkOffer)", + "transformations": [ + "Negative value check", + "Asset comparison", + "Payment amount comparison" + ], + "validated_at": "preclaim" + }, + { + "field": "sfNFTokenID", + "flow": [ + "Offer SLE (ledger)", + "preclaim: buy/sell offer token ID match" + ], + "origin": "Offer SLE (from ledger, via checkOffer)", + "transformations": [ + "Equality check between buy and sell offers" + ], + "validated_at": "preclaim" + }, + { + "field": "sfOwner", + "flow": [ + "Offer SLE (ledger)", + "preclaim: check for offer loop (owner equality)" + ], + "origin": "Offer SLE (from ledger, via checkOffer)", + "transformations": [ + "Equality check between buy and sell offer owners" + ], + "validated_at": "preclaim" + }, + { + "field": "sfDestination", + "flow": [ + "Offer SLE (ledger)", + "preclaim: if present, must match submitting account" + ], + "origin": "Offer SLE (from ledger, via checkOffer)", + "transformations": [ + "Optional presence check", + "Equality check with submitting account" + ], + "validated_at": "preclaim" + }, + { + "field": "sfExpiration", + "flow": [ + "Offer SLE (ledger)", + "checkOffer: hasExpired check", + "preclaim: may return tecEXPIRED or allow to proceed to doApply" + ], + "origin": "Offer SLE (from ledger, via checkOffer)", + "transformations": [ + "Expiration check (current time vs. expiration field)" + ], + "validated_at": "preclaim" + } + ], + "description": "Implements the logic for accepting NFT offers on the XRPL ledger, including preflight, preclaim, payment, NFT transfer, and offer acceptance, with checks for offer validity, permissions, trustlines, and reserve requirements.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenBuyOffer, sfNFTokenSellOffer", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (!bo && !so)) at NFTokenAcceptOffer::preflight", + "issue_pattern": "Missing empty string validation for sfNFTokenBuyOffer, sfNFTokenSellOffer", + "why_false_positive": "explicit check (if (!bo && !so)) validates sfNFTokenBuyOffer, sfNFTokenSellOffer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenBrokerFee", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (auto const bf = ctx.tx[~sfNFTokenBrokerFee])) at NFTokenAcceptOffer::preflight", + "issue_pattern": "Missing empty string validation for sfNFTokenBrokerFee", + "why_false_positive": "explicit check (if (auto const bf = ctx.tx[~sfNFTokenBrokerFee])) validates sfNFTokenBrokerFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenBrokerFee", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (*bf <= beast::zero)) at NFTokenAcceptOffer::preflight", + "issue_pattern": "Missing empty string validation for sfNFTokenBrokerFee", + "why_false_positive": "explicit check (if (*bf <= beast::zero)) validates sfNFTokenBrokerFee for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfNFTokenBrokerFee", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (if (*bf <= beast::zero)) at NFTokenAcceptOffer::preflight", + "issue_pattern": "Missing range validation for sfNFTokenBrokerFee", + "why_false_positive": "explicit check (if (*bf <= beast::zero)) validates sfNFTokenBrokerFee range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenBuyOffer, sfNFTokenSellOffer", + "empty", + "string", + "validation" + ], + "evidence": "checkOffer lambda (if (id->isZero())) at NFTokenAcceptOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenBuyOffer, sfNFTokenSellOffer", + "why_false_positive": "checkOffer lambda (if (id->isZero())) validates sfNFTokenBuyOffer, sfNFTokenSellOffer for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfNFTokenBuyOffer, sfNFTokenSellOffer", + "format", + "validation", + "invalid" + ], + "evidence": "checkOffer lambda (if (id->isZero())) at NFTokenAcceptOffer::preclaim", + "issue_pattern": "Missing format validation for sfNFTokenBuyOffer, sfNFTokenSellOffer", + "why_false_positive": "checkOffer lambda (if (id->isZero())) validates sfNFTokenBuyOffer, sfNFTokenSellOffer format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenBuyOffer, sfNFTokenSellOffer", + "empty", + "string", + "validation" + ], + "evidence": "checkOffer lambda (if (!offerSLE)) at NFTokenAcceptOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenBuyOffer, sfNFTokenSellOffer", + "why_false_positive": "checkOffer lambda (if (!offerSLE)) validates sfNFTokenBuyOffer, sfNFTokenSellOffer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfExpiration", + "empty", + "string", + "validation" + ], + "evidence": "hasExpired(ctx.view, (*offerSLE)[~sfExpiration]) at NFTokenAcceptOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfExpiration", + "why_false_positive": "hasExpired(ctx.view, (*offerSLE)[~sfExpiration]) validates sfExpiration for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "(*offerSLE)[sfAmount].negative() at NFTokenAcceptOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "(*offerSLE)[sfAmount].negative() validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "range", + "bounds", + "validation" + ], + "evidence": "(*offerSLE)[sfAmount].negative() at NFTokenAcceptOffer::preclaim", + "issue_pattern": "Missing range validation for sfAmount", + "why_false_positive": "(*offerSLE)[sfAmount].negative() validates sfAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenID (of bo and so)", + "empty", + "string", + "validation" + ], + "evidence": "if ((*bo)[sfNFTokenID] != (*so)[sfNFTokenID]) at NFTokenAcceptOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenID (of bo and so)", + "why_false_positive": "if ((*bo)[sfNFTokenID] != (*so)[sfNFTokenID]) validates sfNFTokenID (of bo and so) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount.asset() (of bo and so)", + "empty", + "string", + "validation" + ], + "evidence": "if ((*bo)[sfAmount].asset() != (*so)[sfAmount].asset()) at NFTokenAcceptOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfAmount.asset() (of bo and so)", + "why_false_positive": "if ((*bo)[sfAmount].asset() != (*so)[sfAmount].asset()) validates sfAmount.asset() (of bo and so) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 9, + "name": "NFTokenAcceptOffer::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 28, + "name": "NFTokenAcceptOffer::preclaim" + }, + { + "args": [ + "AccountID const& from", + "AccountID const& to", + "STAmount const& amount" + ], + "lineno": 210, + "name": "NFTokenAcceptOffer::pay" + }, + { + "args": [ + "AccountID const& buyer", + "AccountID const& seller", + "uint256 const& nftokenID" + ], + "lineno": 232, + "name": "NFTokenAcceptOffer::transferNFToken" + }, + { + "args": [ + "std::shared_ptr const& offer" + ], + "lineno": 273, + "name": "NFTokenAcceptOffer::acceptOffer" + }, + { + "args": [], + "lineno": 299, + "name": "NFTokenAcceptOffer::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is typically covered by unit/integration tests in the rippled codebase, especially in files like 'test/tx/NFTokenAcceptOffer_test.cpp' or similar. These tests likely cover: missing/invalid buy/sell offer IDs, broker fee presence/amount, offer existence, expiration, negative amounts, asset/token ID mismatches, and permission checks. However, edge cases such as malformed broker fee values, expired offers with/without the fixExpiredNFTokenOfferRemoval amendment, and destination mismatches may not be exhaustively tested. Tests for all possible error codes (temMALFORMED, tecOBJECT_NOT_FOUND, tecEXPIRED, temBAD_OFFER, tecNFTOKEN_BUY_SELL_MISMATCH, tecCANT_ACCEPT_OWN_NFTOKEN_OFFER, tecINSUFFICIENT_PAYMENT, tecNO_PERMISSION) should be verified for coverage. Gaps may exist for amendment-specific behaviors and rare malformed input combinations.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in C++ (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfNFTokenBuyOffer, sfNFTokenSellOffer", + "location": "NFTokenAcceptOffer::preflight", + "validated_by": "explicit check (if (!bo && !so))", + "validates": [ + "At least one of sfNFTokenBuyOffer or sfNFTokenSellOffer must be specified" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfNFTokenBrokerFee", + "location": "NFTokenAcceptOffer::preflight", + "validated_by": "explicit check (if (auto const bf = ctx.tx[~sfNFTokenBrokerFee]))", + "validates": [ + "BrokerFee must not be present in direct mode (must have both bo and so if present)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfNFTokenBrokerFee", + "location": "NFTokenAcceptOffer::preflight", + "validated_by": "explicit check (if (*bf <= beast::zero))", + "validates": [ + "BrokerFee must be greater than zero if present" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND", + "field": "sfNFTokenBuyOffer, sfNFTokenSellOffer", + "location": "NFTokenAcceptOffer::preclaim", + "validated_by": "checkOffer lambda (if (id->isZero()))", + "validates": [ + "Offer ID must not be zero" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND", + "field": "sfNFTokenBuyOffer, sfNFTokenSellOffer", + "location": "NFTokenAcceptOffer::preclaim", + "validated_by": "checkOffer lambda (if (!offerSLE))", + "validates": [ + "Offer must exist in the ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecEXPIRED (if amendment not enabled)", + "field": "sfExpiration", + "location": "NFTokenAcceptOffer::preclaim", + "validated_by": "hasExpired(ctx.view, (*offerSLE)[~sfExpiration])", + "validates": [ + "Offer must not be expired (unless fixExpiredNFTokenOfferRemoval is enabled)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_OFFER", + "field": "sfAmount", + "location": "NFTokenAcceptOffer::preclaim", + "validated_by": "(*offerSLE)[sfAmount].negative()", + "validates": [ + "Offer amount must not be negative" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "tecNFTOKEN_BUY_SELL_MISMATCH", + "field": "sfNFTokenID (of bo and so)", + "location": "NFTokenAcceptOffer::preclaim", + "validated_by": "if ((*bo)[sfNFTokenID] != (*so)[sfNFTokenID])", + "validates": [ + "Buy and sell offers must be for the same NFToken" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNFTOKEN_BUY_SELL_MISMATCH", + "field": "sfAmount.asset() (of bo and so)", + "location": "NFTokenAcceptOffer::preclaim", + "validated_by": "if ((*bo)[sfAmount].asset() != (*so)[sfAmount].asset())", + "validates": [ + "Buy and sell offers must be for the same asset" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp.ai.md b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp.ai.md new file mode 100644 index 0000000000..3658f1d9d0 --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenAcceptOffer.cpp.ai.md @@ -0,0 +1,47 @@ +# `NFTokenAcceptOffer.cpp` — NFT Offer Settlement Implementation + +This file implements the `NFTokenAcceptOffer` transactor, the most financially complex transaction in the XRPL NFT subsystem. While other NFT transactors (`NFTokenMint`, `NFTokenBurn`, `NFTokenCreateOffer`, `NFTokenCancelOffer`) each interact with a single account and a single object, this transactor must coordinate up to four parties — buyer, seller, NFT issuer collecting a royalty, and an optional broker — across multiple atomic ledger writes. + +## Operation Modes + +The transaction operates in one of two modes determined by which offer field(s) appear in the transaction: + +**Direct mode** provides exactly one of `sfNFTokenBuyOffer` or `sfNFTokenSellOffer`. The submitter either owns the NFT and is accepting a buyer's bid, or is a buyer accepting the seller's listed price. The `acceptOffer()` helper routes this path. + +**Brokered mode** provides both offer IDs simultaneously. The submitting account is a third-party broker who has matched a standing buy offer against a standing sell offer. The broker does not own the token, never receives it, and may collect a fee via `sfNFTokenBrokerFee`. This path is handled inline within `doApply()` rather than through `acceptOffer()` because the payment sequencing differs structurally. + +## Preflight: Stateless Field Validation + +`preflight()` enforces two invariants without touching the ledger. First, at least one offer ID must be present — providing neither is `temMALFORMED`. Second, `sfNFTokenBrokerFee` is only legal in brokered mode (both offer IDs present) and must be strictly positive; a broker fee in direct mode, or a zero fee, returns `temMALFORMED`. These are caught before any I/O because they reflect purely malformed transaction construction. + +## Preclaim: Stateful Offer Validation + +`preclaim()` performs the bulk of offer-level validation using a local `checkOffer` lambda that takes an optional offer ID and returns the loaded `SLE` alongside a `TER`. For each offer it confirms: the ID is non-zero, the ledger object exists, the stored amount is not negative, and the offer hasn't expired. + +Expiration handling reveals a deliberate amendment-driven behavior change. Before `fixExpiredNFTokenOfferRemoval`, expired offers immediately returned `tecEXPIRED` from `preclaim`, permanently stranding the ledger object since the apply phase was never reached. After the amendment, `checkOffer` allows expired offers through `preclaim` and returns the SLE; `doApply` then deletes the object before returning `tecEXPIRED`. This amendment fixes a ledger garbage-collection bug without changing the externally visible error code. + +For brokered mode, `preclaim` verifies that both offers reference the same `sfNFTokenID` and the same payment asset (`tecNFTOKEN_BUY_SELL_MISMATCH` otherwise), that the buyer's bid is at least as large as the seller's ask, that the broker fee doesn't exceed the bid, and that after subtracting the broker fee the remaining amount still satisfies the seller's ask. The anti-loop check — rejecting the case where both offers share the same `sfOwner` — prevents a degenerate scenario where a broker tries to facilitate a self-trade. + +Trust-line hygiene is enforced across two graduated amendments. `fixEnforceNFTokenTrustline` prevents the NFT issuer from being granted an unintended trust line when a transfer fee is owed but no line already exists. `fixEnforceNFTokenTrustlineV2` extends coverage to every IOU payment recipient: seller, buyer, broker, and issuer are each validated via `nft::checkTrustlineAuthorized` and `nft::checkTrustlineDeepFrozen` as appropriate to the mode. The checks are gated at the level of each participant's involvement — for example, in brokered mode the submitter's (broker's) trustline for the sell-side asset is not checked, because the broker in that mode is not the party receiving the IOU payment. + +## `pay()`: Atomic Payment with Balance Assertions + +Rather than calling `accountSend` directly, all monetary transfers within this transactor route through the private `pay()` helper. After a successful `accountSend`, `pay()` re-evaluates both the sender's and receiver's effective balance using `accountFunds` with `fhZERO_IF_FROZEN`. A negative result from either check returns `tecINSUFFICIENT_FUNDS` immediately. + +This extra step exists because IOU transfer fees can produce counterintuitive outcomes: a payer with exactly enough balance may end up with a small negative after the fee is credited to the issuer, or a receiver holding the issuing account's own currency may end up with a positive IOU balance from the issuer's perspective. The post-payment assertion catches these edge cases before the ledger write is finalised, converting what would otherwise be a corrupt ledger state into an orderly transaction failure. + +## `transferNFToken()`: Ownership Move with Reserve Check + +Ownership transfer is a page-management operation. `nft::findTokenAndPage` locates the token object and its containing `NFTokenPage` in the seller's directory. `nft::removeToken` removes it from that page, potentially collapsing pages and decrementing the seller's owner count. `nft::insertToken` places the token into the buyer's directory, potentially allocating a new page and incrementing the buyer's owner count. + +After insertion, if `fixNFTokenReserve` is enabled and the buyer's owner count increased, the method checks whether the buyer's current `sfBalance` meets the reserve for the new count. Critically, this uses the buyer's current balance — which has already been reduced by the NFT purchase price and the transaction fee — rather than `preFeeBalance_`. Using the pre-fee balance would overstate available reserve and allow a buyer to acquire an NFT without sufficient backing, which was the original bug this amendment addresses. The comment notes the small caveat that the transaction fee has already been deducted, making the effective reserve requirement a few drops higher than the pure owner-count-based calculation would suggest. + +## `acceptOffer()` and `doApply()`: Direct vs. Brokered Payment Sequencing + +`acceptOffer()` handles direct mode. It reads the offer's `sfAmount`, computes the issuer's royalty cut using `nft::getTransferFee` and `nft::transferFeeAsRate`, pays the issuer (if the cut is non-zero and neither buyer nor seller is the issuer), pays the seller the remainder, then calls `transferNFToken`. Skipping the royalty when the buyer or seller is the issuer prevents nonsensical self-payments. + +`doApply()` handles the full dispatch. First, with `fixExpiredNFTokenOfferRemoval` active, it attempts to delete any expired offer via `nft::deleteTokenOffer` and returns `tecEXPIRED` if any were found — ensuring expired objects don't persist. Then it unconditionally deletes the valid offers being accepted (both buy and sell objects are always removed from the ledger regardless of mode). For brokered mode, payments are sequenced strictly: broker cut first, then the issuer's royalty is calculated on what remains, then the seller receives the final remainder. The comment in the code makes the ordering invariant explicit — computing the issuer's royalty before removing the broker's fee would allow total disbursements to exceed what the buyer originally authorised. + +## Error Handling and Defensive Patterns + +Several `tecINTERNAL` returns are marked with `// LCOV_EXCL_LINE` annotations, indicating paths that are logically unreachable given the invariants established in `preclaim` but are included as defensive guards. Fatal-level log messages accompany the two cases where `nft::deleteTokenOffer` unexpectedly fails, which would indicate a ledger consistency problem rather than a user input issue. The overall design avoids throwing exceptions, relying entirely on `TER` return codes propagated up the call chain. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp.ai.json b/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp.ai.json new file mode 100644 index 0000000000..9365093697 --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp.ai.json @@ -0,0 +1,361 @@ +{ + "args": [ + { + "lineno": 8, + "name": "ctx" + }, + { + "lineno": 13, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "NFTokenBurn::preflight" + ], + "entry_point": "NFTokenBurn::preflight", + "purpose": "Initial lightweight validation of the transaction. In this implementation, it always returns success.", + "validation_points": [] + }, + { + "call_chain": [ + "NFTokenBurn::preclaim" + ], + "entry_point": "NFTokenBurn::preclaim", + "purpose": "Performs main validation: checks token existence, ownership, permissions, and burnability.", + "validation_points": [ + "ctx.tx.isFieldPresent(sfOwner)", + "nft::findToken(ctx.view, owner, ctx.tx[sfNFTokenID])", + "nft::getFlags(ctx.tx[sfNFTokenID]) & nft::flagBurnable", + "nft::getIssuer(ctx.tx[sfNFTokenID])" + ] + }, + { + "call_chain": [ + "NFTokenBurn::doApply", + "nft::removeToken", + "nft::removeTokenOffersWithLimit" + ], + "entry_point": "NFTokenBurn::doApply", + "purpose": "Executes the burn: removes the token, updates issuer's burned count, deletes offers.", + "validation_points": [ + "Relies on preclaim for validation; asserts token removal success." + ] + } + ], + "data_flows": [ + { + "field": "sfOwner", + "flow": [ + "ctx.tx", + "ctx.tx.isFieldPresent(sfOwner) (preclaim)", + "ctx.tx.getAccountID(sfOwner) (preclaim)", + "used as 'owner' in findToken and removeToken" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence", + "Converted to AccountID" + ], + "validated_at": "ctx.tx.isFieldPresent(sfOwner) in preclaim" + }, + { + "field": "sfNFTokenID", + "flow": [ + "ctx.tx", + "ctx.tx[sfNFTokenID] (preclaim)", + "nft::findToken(ctx.view, owner, ctx.tx[sfNFTokenID])", + "nft::getFlags(ctx.tx[sfNFTokenID])", + "nft::getIssuer(ctx.tx[sfNFTokenID])", + "nft::removeToken(view, owner, ctx.tx[sfNFTokenID])", + "keylet::nft_sells(ctx.tx[sfNFTokenID])", + "keylet::nft_buys(ctx.tx[sfNFTokenID])" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Used as lookup key", + "Flags extracted", + "Issuer extracted" + ], + "validated_at": "nft::findToken(ctx.view, owner, ctx.tx[sfNFTokenID]) in preclaim" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx", + "ctx.tx[sfAccount] (preclaim)", + "used as fallback for owner", + "used for permission checks" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Used as AccountID" + ], + "validated_at": "Implicitly validated by presence in transaction; used in permission logic in preclaim" + }, + { + "field": "nft::getFlags(ctx.tx[sfNFTokenID])", + "flow": [ + "ctx.tx[sfNFTokenID]", + "nft::getFlags(ctx.tx[sfNFTokenID])", + "checked for nft::flagBurnable" + ], + "origin": "Derived from sfNFTokenID", + "transformations": [ + "Bitmask check for burnable flag" + ], + "validated_at": "if ((nft::getFlags(ctx.tx[sfNFTokenID]) & nft::flagBurnable) == 0) in preclaim" + }, + { + "field": "nft::getIssuer(ctx.tx[sfNFTokenID])", + "flow": [ + "ctx.tx[sfNFTokenID]", + "nft::getIssuer(ctx.tx[sfNFTokenID])", + "used for permission checks and issuer account lookup" + ], + "origin": "Derived from sfNFTokenID", + "transformations": [ + "Extracted as AccountID" + ], + "validated_at": "if (auto const issuer = nft::getIssuer(ctx.tx[sfNFTokenID]); issuer != account) in preclaim" + } + ], + "description": "Implements the logic for burning (destroying) an NFT (NFToken) on the XRPL, including permission checks and cleanup of related offers.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwner", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.isFieldPresent(sfOwner) at NFTokenBurn::preclaim", + "issue_pattern": "Missing empty string validation for sfOwner", + "why_false_positive": "ctx.tx.isFieldPresent(sfOwner) validates sfOwner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenID", + "empty", + "string", + "validation" + ], + "evidence": "nft::findToken(ctx.view, owner, ctx.tx[sfNFTokenID]) at NFTokenBurn::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenID", + "why_false_positive": "nft::findToken(ctx.view, owner, ctx.tx[sfNFTokenID]) validates sfNFTokenID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx[sfAccount] at NFTokenBurn::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "ctx.tx[sfAccount] validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenID", + "empty", + "string", + "validation" + ], + "evidence": "nft::getFlags(ctx.tx[sfNFTokenID]) & nft::flagBurnable at NFTokenBurn::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenID", + "why_false_positive": "nft::getFlags(ctx.tx[sfNFTokenID]) & nft::flagBurnable validates sfNFTokenID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenID", + "empty", + "string", + "validation" + ], + "evidence": "nft::getIssuer(ctx.tx[sfNFTokenID]) at NFTokenBurn::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenID", + "why_false_positive": "nft::getIssuer(ctx.tx[sfNFTokenID]) validates sfNFTokenID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenMinter", + "empty", + "string", + "validation" + ], + "evidence": "(*sle)[~sfNFTokenMinter] at NFTokenBurn::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenMinter", + "why_false_positive": "(*sle)[~sfNFTokenMinter] validates sfNFTokenMinter for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenID", + "empty", + "string", + "validation" + ], + "evidence": "nft::removeToken at NFTokenBurn::doApply", + "issue_pattern": "Missing empty string validation for sfNFTokenID", + "why_false_positive": "nft::removeToken validates sfNFTokenID for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 7, + "name": "NFTokenBurn::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 12, + "name": "NFTokenBurn::preclaim" + }, + { + "args": [], + "lineno": 38, + "name": "NFTokenBurn::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "NFTokenBurn is typically tested in integration and unit tests for NFT operations. Likely test files: 'test/tx/NFTokenBurn_test.cpp', 'test/tx/NFToken_test.cpp', or similar. Tests should cover: burning by owner, burning by issuer (with/without burnable flag), burning non-existent token, permission errors, and offer cleanup. Gaps may exist in edge cases: minter permissions, issuer account not found, or concurrent offer deletion limits. No explicit test references in this file; coverage depends on broader test suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic (xrpl, nft namespace)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "N/A (used to determine owner, not error)", + "field": "sfOwner", + "location": "NFTokenBurn::preclaim", + "validated_by": "ctx.tx.isFieldPresent(sfOwner)", + "validates": [ + "Checks if sfOwner field is present to determine owner of NFT" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfNFTokenID", + "location": "NFTokenBurn::preclaim", + "validated_by": "nft::findToken(ctx.view, owner, ctx.tx[sfNFTokenID])", + "validates": [ + "Checks if the NFT with sfNFTokenID exists for the owner" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "N/A (used to determine acting account, not error)", + "field": "sfAccount", + "location": "NFTokenBurn::preclaim", + "validated_by": "ctx.tx[sfAccount]", + "validates": [ + "Used to determine if the transaction sender is the owner or issuer" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfNFTokenID", + "location": "NFTokenBurn::preclaim", + "validated_by": "nft::getFlags(ctx.tx[sfNFTokenID]) & nft::flagBurnable", + "validates": [ + "Checks if the NFT is burnable when issuer is burning" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfNFTokenID", + "location": "NFTokenBurn::preclaim", + "validated_by": "nft::getIssuer(ctx.tx[sfNFTokenID])", + "validates": [ + "Checks if the issuer is the transaction sender or minter" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfNFTokenMinter", + "location": "NFTokenBurn::preclaim", + "validated_by": "(*sle)[~sfNFTokenMinter]", + "validates": [ + "Checks if the minter is the transaction sender" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "ret (TER error code, e.g., tecNO_ENTRY)", + "field": "sfNFTokenID", + "location": "NFTokenBurn::doApply", + "validated_by": "nft::removeToken", + "validates": [ + "Checks again that the NFT exists before burning (should not fail due to preclaim)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp.ai.md b/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp.ai.md new file mode 100644 index 0000000000..5c49d920cd --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenBurn.cpp.ai.md @@ -0,0 +1,41 @@ +# `NFTokenBurn.cpp` — NFT Burn Transactor + +## Role in the System + +`NFTokenBurn.cpp` implements the three-phase `NFTokenBurn` transaction handler for destroying an NFT on the XRP Ledger. Burning permanently removes the token from the ledger, cleans up all associated buy and sell offers, and increments a counter on the issuer's account. It sits in the NFT transactor family alongside `NFTokenMint`, `NFTokenCreateOffer`, `NFTokenAcceptOffer`, `NFTokenCancelOffer`, and `NFTokenModify`, all sharing the `nft::` helper namespace for common ledger manipulation. + +## Three-Phase Execution Model + +Like all XRPL transactors, `NFTokenBurn` follows the framework's canonical three-phase pipeline: + +**`preflight`** is intentionally empty — it returns `tesSUCCESS` unconditionally. The framework already validates transaction structure (field presence, account signature, fee) before reaching this hook; there are no additional lightweight checks specific to a burn that can be done without ledger access. + +**`preclaim`** is where all meaningful validation happens. It runs against a read-only ledger view before any state is mutated, making it safe to call in parallel across transaction batches. The logic resolves the token owner: if the transaction carries `sfOwner`, that account is taken as the holder; otherwise the submitting account (`sfAccount`) is used as both submitter and holder. This dual-path is what allows an issuer or minter to burn a token they don't currently hold. + +**`doApply`** executes the mutation unconditionally, trusting `preclaim` to have already validated all preconditions. + +## Permission Architecture + +The permission model in `preclaim` is the most nuanced part of the file. Token ownership is always sufficient to burn — no flags or role checks needed. The complexity only enters when a non-owner submits the transaction: + +1. The token's `flagBurnable` bit (bit 0 of the 16-bit flags stored in the high bytes of the 256-bit token ID) must be set, or the non-owner receives `tecNO_PERMISSION`. This is the issuer's design-time decision to allow forced recall. +2. Even with `flagBurnable` set, only the **issuer** or the issuer's **designated minter** may burn. The issuer is embedded directly in the `sfNFTokenID` via `nft::getIssuer()`, which reads bytes from the token ID's fixed layout. If the submitting account is not the issuer, the code reads the issuer's account SLE and checks the optional `sfNFTokenMinter` field — a delegated-minting relationship set via `AccountSet`. Only if the submitter matches either the issuer or the current minter is the burn permitted. + +This three-tier hierarchy (owner → issuer → minter) is a deliberate design choice. Encoding the issuer directly in the token ID means the permission check never needs to look up historical ledger state or a separate registry; the token ID is self-describing. + +## State Mutations in `doApply` + +`doApply` performs three distinct mutations: + +**Token removal** via `nft::removeToken()` strips the token from the owner's `NFTokenPage` directory. The return value is checked and propagated, but the comment acknowledges this guard "should never happen" because `preclaim` already confirmed existence — it is purely defensive. + +**Issuer burn counter** — after removal, the code peeks the issuer's account SLE and increments `sfBurnedNFTokens` using `value_or(0)` because the field is optional and absent until the first burn. This counter is append-only and provides on-chain accounting for how many tokens of that issuance have been destroyed, without requiring a full scan of the token namespace. + +**Offer cleanup** deletes all open offers for the burned token up to a hard cap of `maxDeletableTokenOfferEntries` (500, from `Protocol.h`). The allocation between sell and buy offers is non-trivial: sell offers are processed first, and only the remaining budget (`500 - deletedSellOffers`) is applied to buy offers. The rationale in the comment is that sell offers are typically fewer, so attacking them first maximises the chance of completely clearing the sell-offer directory within a single transaction. Any offers that exceed the 500-item budget are simply left orphaned on the ledger — an existing `notTooManyOffers()` preclaim guard in sibling code prevents a token from becoming permanently un-burnable by capping total live offers before this path is reached. + +## Key Relationships + +- **`NFTokenHelpers.h` / `NFTokenHelpers.cpp`** — provides `findToken()`, `removeToken()`, `removeTokenOffersWithLimit()`, and `notTooManyOffers()`. The burn transactor is essentially a thin orchestration layer over these helpers. +- **`protocol/nft.h`** — `nft::getFlags()` and `nft::getIssuer()` extract permission metadata by reading directly from the packed binary layout of the 256-bit token ID using `memcpy` + big-endian conversion. No ledger lookup is needed. +- **`Transactor` base class** — supplies `view()`, `ctx_`, and the `doApply()` override contract. `ConsequencesFactory{Normal}` tells the transaction queue that this transaction has standard fee consequences. +- **`Protocol.h`** — `maxDeletableTokenOfferEntries = 500` bounds the offer deletion loop, a per-transaction work cap that prevents this operation from being used to consume unbounded compute. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp.ai.json b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp.ai.json new file mode 100644 index 0000000000..38164a7516 --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp.ai.json @@ -0,0 +1,338 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "NFTokenCancelOffer::preflight" + ], + "entry_point": "NFTokenCancelOffer::preflight", + "purpose": "Initial stateless validation of the NFTokenCancelOffer transaction. Checks for malformed input, such as empty or oversized offer lists and duplicate offer IDs.", + "validation_points": [ + "Check if sfNFTokenOffers is empty or exceeds maxTokenOfferCancelCount", + "Check for duplicate IDs in sfNFTokenOffers using std::sort + std::adjacent_find" + ] + }, + { + "call_chain": [ + "NFTokenCancelOffer::preclaim" + ], + "entry_point": "NFTokenCancelOffer::preclaim", + "purpose": "Stateful validation: checks ledger state to ensure the account has permission to cancel each offer.", + "validation_points": [ + "For each id in sfNFTokenOffers: check if offer exists in ledger", + "Check if offer is of type ltNFTOKEN_OFFER", + "Check if offer is expired (hasExpired)", + "Check if account is owner or destination" + ] + }, + { + "call_chain": [ + "NFTokenCancelOffer::doApply" + ], + "entry_point": "NFTokenCancelOffer::doApply", + "purpose": "Applies the transaction: attempts to delete each offer from the ledger.", + "validation_points": [ + "No new validation; assumes preflight and preclaim have passed" + ] + } + ], + "data_flows": [ + { + "field": "sfNFTokenOffers", + "flow": [ + "Transaction input", + "Read in preflight for size and duplicate checks", + "Read in preclaim for per-offer permission checks", + "Used in doApply to delete offers" + ], + "origin": "ctx.tx[sfNFTokenOffers] (transaction input)", + "transformations": [ + "In preflight: copied to STVector256, sorted, checked for duplicates", + "In preclaim: iterated, each id used to lookup ledger entry", + "In doApply: iterated, each id used to delete offer" + ], + "validated_at": "preflight (size, duplicates), preclaim (existence, type, permissions)" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "Read in preclaim for permission checks" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "None" + ], + "validated_at": "preclaim (used to check ownership/permission)" + }, + { + "field": "Offer Ledger Entry", + "flow": [ + "Looked up in preclaim for each id", + "Checked for type, expiration, owner, destination" + ], + "origin": "ctx.view.read(keylet::child(id))", + "transformations": [ + "Checked for existence", + "Type checked (ltNFTOKEN_OFFER)", + "Fields extracted: sfOwner, ~sfExpiration, ~sfDestination" + ], + "validated_at": "preclaim (existence, type, expiration, owner, destination)" + } + ], + "description": "Implements the logic for the NFTokenCancelOffer transaction, including preflight, preclaim, and apply steps for cancelling NFT offers on the XRPL ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenOffers", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ids.empty() || ids.size() > maxTokenOfferCancelCount) at NFTokenCancelOffer::preflight", + "issue_pattern": "Missing empty string validation for sfNFTokenOffers", + "why_false_positive": "explicit check (ids.empty() || ids.size() > maxTokenOfferCancelCount) validates sfNFTokenOffers for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfNFTokenOffers", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (ids.empty() || ids.size() > maxTokenOfferCancelCount) at NFTokenCancelOffer::preflight", + "issue_pattern": "Missing range validation for sfNFTokenOffers", + "why_false_positive": "explicit check (ids.empty() || ids.size() > maxTokenOfferCancelCount) validates sfNFTokenOffers range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenOffers", + "empty", + "string", + "validation" + ], + "evidence": "std::sort + std::adjacent_find at NFTokenCancelOffer::preflight", + "issue_pattern": "Missing empty string validation for sfNFTokenOffers", + "why_false_positive": "std::sort + std::adjacent_find validates sfNFTokenOffers for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenOffers (each id)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::child(id)) at NFTokenCancelOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenOffers (each id)", + "why_false_positive": "ctx.view.read(keylet::child(id)) validates sfNFTokenOffers (each id) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenOffers (each id)", + "empty", + "string", + "validation" + ], + "evidence": "hasExpired(ctx.view, (*offer)[~sfExpiration]) at NFTokenCancelOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenOffers (each id)", + "why_false_positive": "hasExpired(ctx.view, (*offer)[~sfExpiration]) validates sfNFTokenOffers (each id) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenOffers (each id)", + "empty", + "string", + "validation" + ], + "evidence": "(*offer)[sfOwner] == account at NFTokenCancelOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenOffers (each id)", + "why_false_positive": "(*offer)[sfOwner] == account validates sfNFTokenOffers (each id) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenOffers (each id)", + "empty", + "string", + "validation" + ], + "evidence": "(*offer)[~sfDestination] == account at NFTokenCancelOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenOffers (each id)", + "why_false_positive": "(*offer)[~sfDestination] == account validates sfNFTokenOffers (each id) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenOffers (each id)", + "empty", + "string", + "validation" + ], + "evidence": "offer && !nft::deleteTokenOffer(view(), offer) at NFTokenCancelOffer::doApply", + "issue_pattern": "Missing empty string validation for sfNFTokenOffers (each id)", + "why_false_positive": "offer && !nft::deleteTokenOffer(view(), offer) validates sfNFTokenOffers (each id) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 10, + "name": "NFTokenCancelOffer::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 27, + "name": "NFTokenCancelOffer::preclaim" + }, + { + "args": [], + "lineno": 61, + "name": "NFTokenCancelOffer::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "NFTokenCancelOffer is a core transaction type and is likely covered by integration and unit tests in the rippled codebase, especially in files like 'test/tx/NFTokenCancelOffer_test.cpp' or similar. Tests should cover: empty offers, too many offers, duplicate offers, non-existent offers, offers not owned by the account, expired offers, and successful cancellation. Gaps may exist if edge cases (e.g., malformed offers, offers with missing fields, or concurrent offer consumption) are not explicitly tested. Code coverage tools (LCOV) are referenced in doApply, suggesting some error paths may not be fully covered by tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in transaction preflight/preclaim/apply pattern (Rippled transaction engine)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfNFTokenOffers", + "location": "NFTokenCancelOffer::preflight", + "validated_by": "explicit check (ids.empty() || ids.size() > maxTokenOfferCancelCount)", + "validates": [ + "Ensures the sfNFTokenOffers field is not empty", + "Ensures the sfNFTokenOffers field does not exceed maxTokenOfferCancelCount" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfNFTokenOffers", + "location": "NFTokenCancelOffer::preflight", + "validated_by": "std::sort + std::adjacent_find", + "validates": [ + "Ensures there are no duplicate offer IDs in sfNFTokenOffers" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfNFTokenOffers (each id)", + "location": "NFTokenCancelOffer::preclaim", + "validated_by": "ctx.view.read(keylet::child(id))", + "validates": [ + "Checks if the offer exists in the ledger (if not, it's assumed consumed and skipped)", + "Checks if the offer is of type ltNFTOKEN_OFFER (if not, permission denied)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfNFTokenOffers (each id)", + "location": "NFTokenCancelOffer::preclaim", + "validated_by": "hasExpired(ctx.view, (*offer)[~sfExpiration])", + "validates": [ + "Allows anyone to cancel if the offer is expired" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfNFTokenOffers (each id)", + "location": "NFTokenCancelOffer::preclaim", + "validated_by": "(*offer)[sfOwner] == account", + "validates": [ + "Allows the owner of the offer to cancel" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfNFTokenOffers (each id)", + "location": "NFTokenCancelOffer::preclaim", + "validated_by": "(*offer)[~sfDestination] == account", + "validates": [ + "Allows the destination (recipient) of the offer to cancel" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfNFTokenOffers (each id)", + "location": "NFTokenCancelOffer::doApply", + "validated_by": "offer && !nft::deleteTokenOffer(view(), offer)", + "validates": [ + "Checks that the offer can be deleted from the ledger" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp.ai.md b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp.ai.md new file mode 100644 index 0000000000..bfdb9c2723 --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenCancelOffer.cpp.ai.md @@ -0,0 +1,44 @@ +# `NFTokenCancelOffer.cpp` — NFT Offer Cancellation Transactor + +This file implements the `NFTokenCancelOffer` transaction type, which allows accounts to remove one or more outstanding NFT buy or sell offers from the XRPL ledger. It follows the three-phase transactor pattern shared by all XRPL transaction types: `preflight` (stateless input validation), `preclaim` (stateful permission checks against a read-only ledger view), and `doApply` (ledger mutation). + +## Three-Phase Transactor Pattern + +`NFTokenCancelOffer` inherits from `Transactor`. The framework calls the three phases in sequence, aborting at the first failure. `preflight` and `preclaim` are static methods that operate on context structs rather than on the object itself; `doApply` is a virtual override with access to `ctx_` and the mutable `view()`. + +## `preflight` — Stateless Input Validation + +Two invariants are enforced before any ledger access occurs. + +**Bounds check.** The `sfNFTokenOffers` vector must be non-empty and must not exceed `maxTokenOfferCancelCount` (defined as 500 in `Protocol.h`). An empty list serves no purpose and is rejected as `temMALFORMED`. The upper bound guards against oversized transactions: each offer ID is a 256-bit hash, and allowing an unbounded list would make the transaction a denial-of-service vector both in terms of transaction size and server-side processing. + +**Duplicate detection.** Rather than an O(n²) pairwise comparison, the IDs are copied into a local `STVector256`, sorted, and then checked with `std::adjacent_find`. This detects duplicates in O(n log n) time and keeps the code concise. Duplicates are rejected because they would inflate transaction size without doing any additional work and could obscure intent. + +Note the subtle field-access pattern: the first read uses the direct accessor `ctx.tx[sfNFTokenOffers]` to get a const reference for the size check, then a second call to `getFieldV256` creates a mutable copy to sort. This avoids mutating the transaction's own field storage. + +## `preclaim` — Per-Offer Permission Model + +After stateless validation passes, `preclaim` checks whether the submitting account actually has the right to cancel each named offer. The logic uses `std::find_if` with a predicate that returns `true` (meaning "deny") for any offer the account may not cancel, and `false` (meaning "allow") otherwise. If any offer returns deny, `preclaim` returns `tecNO_PERMISSION`. + +Three categories of callers may cancel an offer: + +1. **The offer owner** (`sfOwner == account`). The account that placed the offer may always retract it. +2. **The designated destination** (`sfDestination == account`). If the offer was restricted to a specific counterparty, that counterparty may decline it by cancelling. +3. **Anyone, if the offer has expired** (`hasExpired(ctx.view, (*offer)[~sfExpiration])`). Once an offer's expiration time has passed relative to the ledger's close time, it has no economic value and any account is permitted to clean it up. This is a useful garbage-collection mechanism. + +Two additional edge cases are handled gracefully: + +- **Missing offer.** If `ctx.view.read(keylet::child(id))` returns null, the offer no longer exists — it was presumably consumed by `NFTokenAcceptOffer` between transaction submission and this validation pass. The predicate returns `false` (allow), effectively treating a missing offer as silently skipped rather than an error. This prevents a race condition where a batch cancel fails entirely because one offer was accepted just before the cancel landed. +- **Wrong ledger entry type.** If the 256-bit ID resolves to a ledger entry that is *not* of type `ltNFTOKEN_OFFER`, the account has no permission to delete it. Returning `tecNO_PERMISSION` here prevents a class of spoofing attacks where an attacker tricks a victim into submitting a cancel transaction that targets an unrelated ledger object. + +## `doApply` — Ledger Mutation + +After the permission gauntlet, `doApply` iterates the offer IDs and calls `nft::deleteTokenOffer(view(), offer)` for each one that still exists in the mutable view. The `deleteTokenOffer` helper (declared in `NFTokenHelpers.h`) handles the two-location bookkeeping that every NFT offer deletion requires: removing the offer from the token's buy or sell directory and from the offer owner's account directory, and releasing the one-offer-per-entry reserve held against the owner's account. + +If `deleteTokenOffer` returns `false` despite the offer having been found in the view, `doApply` logs a fatal message and returns `tefBAD_LEDGER`. This path is marked `LCOV_EXCL_START / LCOV_EXCL_STOP`, signalling that it is a defensive catch for an internal consistency failure that should never be triggered in a well-formed ledger. Because `preclaim` already verified that all targeted entries are valid `ltNFTOKEN_OFFER` objects, reaching this path would imply ledger corruption rather than user error. + +## Design Observations + +The permission model deliberately separates *who placed the offer* from *who should be allowed to retract it*, giving both the creator (owner) and the intended recipient (destination) independent veto rights. This symmetry means neither party can lock the other into an unwanted trade. The expiration escape hatch completes the design by ensuring that offers do not accumulate indefinitely when participants become inactive. + +The silent skip for already-consumed offers in both `preclaim` and `doApply` is an intentional idempotency concession: a cancel transaction submitted alongside an accept transaction should not fail simply because the accept arrived first. This is consistent with XRPL's broader design philosophy of avoiding unnecessary transaction failures for economically harmless conditions. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp.ai.json b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp.ai.json new file mode 100644 index 0000000000..4cc0dae8a6 --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp.ai.json @@ -0,0 +1,590 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "NFTokenCreateOffer::preflight", + "nft::getFlags", + "nft::tokenOfferCreatePreflight" + ], + "entry_point": "NFTokenCreateOffer::preflight", + "purpose": "Performs initial transaction validation (syntax, field presence, basic checks) before ledger access.", + "validation_points": [ + "nft::getFlags (validates sfNFTokenID)", + "nft::tokenOfferCreatePreflight (validates sfAccount, sfAmount, sfDestination, sfExpiration)" + ] + }, + { + "call_chain": [ + "NFTokenCreateOffer::preclaim", + "hasExpired", + "nft::findToken", + "nft::tokenOfferCreatePreclaim" + ], + "entry_point": "NFTokenCreateOffer::preclaim", + "purpose": "Performs contextual validation (ledger state, token existence, expiration) before applying transaction.", + "validation_points": [ + "hasExpired (validates sfExpiration)", + "nft::findToken (validates existence of NFT by sfNFTokenID and owner)", + "nft::tokenOfferCreatePreclaim (further contextual checks)" + ] + }, + { + "call_chain": [ + "NFTokenCreateOffer::doApply", + "nft::tokenOfferCreateApply" + ], + "entry_point": "NFTokenCreateOffer::doApply", + "purpose": "Applies the transaction to the ledger after all validations pass.", + "validation_points": [ + "Assumes all prior validations passed; does not perform new validation here." + ] + } + ], + "data_flows": [ + { + "field": "sfNFTokenID", + "flow": [ + "ctx.tx[sfNFTokenID]", + "nft::getFlags", + "nft::findToken", + "nft::getIssuer", + "nft::getTransferFee", + "nft::tokenOfferCreatePreflight", + "nft::tokenOfferCreatePreclaim", + "nft::tokenOfferCreateApply" + ], + "origin": "ctx.tx[sfNFTokenID] (transaction input)", + "transformations": [ + "Parsed for flags, issuer, transfer fee; checked for existence in ledger" + ], + "validated_at": "nft::getFlags (preflight), nft::findToken (preclaim)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "nft::tokenOfferCreatePreflight", + "nft::tokenOfferCreatePreclaim", + "nft::tokenOfferCreateApply" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Checked for validity, permissions, and used as offer creator" + ], + "validated_at": "nft::tokenOfferCreatePreflight" + }, + { + "field": "sfAmount", + "flow": [ + "ctx.tx[sfAmount]", + "nft::tokenOfferCreatePreflight", + "nft::tokenOfferCreatePreclaim", + "nft::tokenOfferCreateApply" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Checked for valid amount, type, and limits" + ], + "validated_at": "nft::tokenOfferCreatePreflight" + }, + { + "field": "sfDestination", + "flow": [ + "ctx.tx[~sfDestination]", + "nft::tokenOfferCreatePreflight", + "nft::tokenOfferCreatePreclaim", + "nft::tokenOfferCreateApply" + ], + "origin": "ctx.tx[~sfDestination] (optional transaction input)", + "transformations": [ + "Optional; checked for validity if present" + ], + "validated_at": "nft::tokenOfferCreatePreflight" + }, + { + "field": "sfExpiration", + "flow": [ + "ctx.tx[~sfExpiration]", + "nft::tokenOfferCreatePreflight", + "hasExpired", + "nft::tokenOfferCreatePreclaim", + "nft::tokenOfferCreateApply" + ], + "origin": "ctx.tx[~sfExpiration] (optional transaction input)", + "transformations": [ + "Optional; checked for validity and expiration" + ], + "validated_at": "nft::tokenOfferCreatePreflight, hasExpired (preclaim)" + }, + { + "field": "sfOwner", + "flow": [ + "ctx.tx[~sfOwner]", + "nft::tokenOfferCreatePreflight", + "nft::tokenOfferCreatePreclaim", + "nft::tokenOfferCreateApply" + ], + "origin": "ctx.tx[~sfOwner] (optional transaction input)", + "transformations": [ + "Optional; used to determine offer type (buy/sell)" + ], + "validated_at": "nft::tokenOfferCreatePreflight" + } + ], + "description": "Implements the NFTokenCreateOffer transaction logic for creating NFT offers on the XRPL, including preflight, preclaim, and apply steps.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccount validated by xrpl core validation (custom, not external framework)", + "issue_pattern": "Missing validation for sfAccount", + "why_false_positive": "xrpl core validation (custom, not external framework) validates sfAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAmount validated by xrpl core validation (custom, not external framework)", + "issue_pattern": "Missing validation for sfAmount", + "why_false_positive": "xrpl core validation (custom, not external framework) validates sfAmount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfNFTokenID", + "validation", + "missing", + "check" + ], + "evidence": "Field sfNFTokenID validated by xrpl core validation (custom, not external framework)", + "issue_pattern": "Missing validation for sfNFTokenID", + "why_false_positive": "xrpl core validation (custom, not external framework) validates sfNFTokenID automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDestination", + "validation", + "missing", + "check" + ], + "evidence": "Field sfDestination validated by xrpl core validation (custom, not external framework)", + "issue_pattern": "Missing validation for sfDestination", + "why_false_positive": "xrpl core validation (custom, not external framework) validates sfDestination automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfExpiration", + "validation", + "missing", + "check" + ], + "evidence": "Field sfExpiration validated by xrpl core validation (custom, not external framework)", + "issue_pattern": "Missing validation for sfExpiration", + "why_false_positive": "xrpl core validation (custom, not external framework) validates sfExpiration automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfOwner", + "validation", + "missing", + "check" + ], + "evidence": "Field sfOwner validated by xrpl core validation (custom, not external framework)", + "issue_pattern": "Missing validation for sfOwner", + "why_false_positive": "xrpl core validation (custom, not external framework) validates sfOwner automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "txFlags", + "validation", + "missing", + "check" + ], + "evidence": "Field txFlags validated by xrpl core validation (custom, not external framework)", + "issue_pattern": "Missing validation for txFlags", + "why_false_positive": "xrpl core validation (custom, not external framework) validates txFlags automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfNFTokenID", + "empty", + "string", + "validation" + ], + "evidence": "nft::getFlags at NFTokenCreateOffer::preflight", + "issue_pattern": "Missing empty string validation for sfNFTokenID", + "why_false_positive": "nft::getFlags validates sfNFTokenID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "nft::tokenOfferCreatePreflight at NFTokenCreateOffer::preflight", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "nft::tokenOfferCreatePreflight validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "nft::tokenOfferCreatePreflight at NFTokenCreateOffer::preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "nft::tokenOfferCreatePreflight validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfDestination (optional)", + "empty", + "string", + "validation" + ], + "evidence": "nft::tokenOfferCreatePreflight at NFTokenCreateOffer::preflight", + "issue_pattern": "Missing empty string validation for sfDestination (optional)", + "why_false_positive": "nft::tokenOfferCreatePreflight validates sfDestination (optional) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfExpiration (optional)", + "empty", + "string", + "validation" + ], + "evidence": "nft::tokenOfferCreatePreflight at NFTokenCreateOffer::preflight", + "issue_pattern": "Missing empty string validation for sfExpiration (optional)", + "why_false_positive": "nft::tokenOfferCreatePreflight validates sfExpiration (optional) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfOwner (optional)", + "empty", + "string", + "validation" + ], + "evidence": "nft::tokenOfferCreatePreflight at NFTokenCreateOffer::preflight", + "issue_pattern": "Missing empty string validation for sfOwner (optional)", + "why_false_positive": "nft::tokenOfferCreatePreflight validates sfOwner (optional) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "txFlags", + "empty", + "string", + "validation" + ], + "evidence": "nft::tokenOfferCreatePreflight at NFTokenCreateOffer::preflight", + "issue_pattern": "Missing empty string validation for txFlags", + "why_false_positive": "nft::tokenOfferCreatePreflight validates txFlags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfExpiration (optional)", + "empty", + "string", + "validation" + ], + "evidence": "hasExpired at NFTokenCreateOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfExpiration (optional)", + "why_false_positive": "hasExpired validates sfExpiration (optional) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenID", + "empty", + "string", + "validation" + ], + "evidence": "nft::findToken at NFTokenCreateOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenID", + "why_false_positive": "nft::findToken validates sfNFTokenID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfAccount, sfAmount, sfDestination, sfOwner, txFlags, nftokenID, issuer, transferFee", + "empty", + "string", + "validation" + ], + "evidence": "nft::tokenOfferCreatePreclaim at NFTokenCreateOffer::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount, sfAmount, sfDestination, sfOwner, txFlags, nftokenID, issuer, transferFee", + "why_false_positive": "nft::tokenOfferCreatePreclaim validates sfAccount, sfAmount, sfDestination, sfOwner, txFlags, nftokenID, issuer, transferFee for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "NFTokenCreateOffer::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "NFTokenCreateOffer::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 33, + "name": "NFTokenCreateOffer::preclaim" + }, + { + "args": [], + "lineno": 56, + "name": "NFTokenCreateOffer::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "NFTokenCreateOffer is typically tested in integration/functional test suites for NFT offer creation. Likely test files: 'test/tx/NFTokenCreateOffer_test.cpp', 'test/tx/NFToken_test.cpp', or similar. These tests should cover valid/invalid field combinations, missing/invalid sfNFTokenID, sfAccount, sfAmount, expiration logic, and offer creation flows. Gaps may exist in edge cases for optional fields (sfDestination, sfOwner), malformed sfAmount, or rare flag combinations. Direct unit tests for internal helpers (nft::tokenOfferCreatePreflight, nft::getFlags) may be limited; most validation is exercised via transaction-level tests.", + "validation_architecture": { + "auto_validated_fields": [ + "sfAccount", + "sfAmount", + "sfNFTokenID", + "sfDestination", + "sfExpiration", + "sfOwner", + "txFlags" + ], + "framework": "xrpl core validation (custom, not external framework)", + "validation_layer": "business_logic (preflight/preclaim/apply)" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "NotTEC (via tokenOfferCreatePreflight)", + "field": "sfNFTokenID", + "location": "NFTokenCreateOffer::preflight", + "validated_by": "nft::getFlags", + "validates": [ + "Checks that NFTokenID is present and valid", + "Extracts flags from NFTokenID", + "May check format and existence" + ], + "validation_type": "type|format|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "NotTEC (via tokenOfferCreatePreflight)", + "field": "sfAccount", + "location": "NFTokenCreateOffer::preflight", + "validated_by": "nft::tokenOfferCreatePreflight", + "validates": [ + "Checks that Account is present and valid", + "Checks permissions/ownership" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "NotTEC (via tokenOfferCreatePreflight)", + "field": "sfAmount", + "location": "NFTokenCreateOffer::preflight", + "validated_by": "nft::tokenOfferCreatePreflight", + "validates": [ + "Checks that Amount is present and valid", + "Checks amount is positive and within allowed range" + ], + "validation_type": "type|range|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "NotTEC (via tokenOfferCreatePreflight)", + "field": "sfDestination (optional)", + "location": "NFTokenCreateOffer::preflight", + "validated_by": "nft::tokenOfferCreatePreflight", + "validates": [ + "Checks that Destination, if present, is valid", + "Checks address format" + ], + "validation_type": "type|format|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "NotTEC (via tokenOfferCreatePreflight)", + "field": "sfExpiration (optional)", + "location": "NFTokenCreateOffer::preflight", + "validated_by": "nft::tokenOfferCreatePreflight", + "validates": [ + "Checks that Expiration, if present, is valid", + "Checks expiration is in the future" + ], + "validation_type": "type|range|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "NotTEC (via tokenOfferCreatePreflight)", + "field": "sfOwner (optional)", + "location": "NFTokenCreateOffer::preflight", + "validated_by": "nft::tokenOfferCreatePreflight", + "validates": [ + "Checks that Owner, if present, is valid", + "Checks ownership rules" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "NotTEC (via tokenOfferCreatePreflight)", + "field": "txFlags", + "location": "NFTokenCreateOffer::preflight", + "validated_by": "nft::tokenOfferCreatePreflight", + "validates": [ + "Checks that transaction flags are valid for this operation" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecEXPIRED", + "field": "sfExpiration (optional)", + "location": "NFTokenCreateOffer::preclaim", + "validated_by": "hasExpired", + "validates": [ + "Checks if the offer has already expired" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfNFTokenID", + "location": "NFTokenCreateOffer::preclaim", + "validated_by": "nft::findToken", + "validates": [ + "Checks that the referenced NFToken exists in the ledger for the correct owner" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 0.8, + "error_thrown": "TER (various error codes)", + "field": "sfAccount, sfAmount, sfDestination, sfOwner, txFlags, nftokenID, issuer, transferFee", + "location": "NFTokenCreateOffer::preclaim", + "validated_by": "nft::tokenOfferCreatePreclaim", + "validates": [ + "Checks business logic for offer creation", + "Checks account and ownership rules", + "Checks amount and transfer fee validity", + "Checks destination and owner fields" + ], + "validation_type": "business_logic|type|range|format" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp.ai.md b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp.ai.md new file mode 100644 index 0000000000..34856d3d0d --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenCreateOffer.cpp.ai.md @@ -0,0 +1,40 @@ +# `NFTokenCreateOffer.cpp` — NFT Offer Creation Transactor + +## Role in the System + +This file implements the `NFTokenCreateOffer` transactor, which handles the XRPL transaction that places an offer to buy or sell an NFT. It sits within the `src/libxrpl/tx/transactors/nft/` directory alongside the other NFT transaction types (`NFTokenMint`, `NFTokenBurn`, `NFTokenAcceptOffer`, etc.). As a transactor, it is responsible for validating and applying one specific transaction type to the ledger, following the standard XRPL three-phase lifecycle: `preflight` → `preclaim` → `doApply`. + +The file itself is intentionally thin — barely 80 lines. Nearly all substantive logic lives in shared helper functions declared in `xrpl/ledger/helpers/NFTokenHelpers.h` and also used by `NFTokenMint`. This is the defining architectural choice of this file. + +## The Three-Phase Transactor Pattern + +`NFTokenCreateOffer` inherits from `Transactor` and overrides three static methods plus `doApply()`: + +**`getFlagsMask`** returns `tfNFTokenCreateOfferMask`, which is generated automatically from the `TxFlags.h` X-macro system. The `TRANSACTION(NFTokenCreateOffer, TF_FLAG(tfSellNFToken, 0x00000001), MASK_ADJ(0))` entry creates this mask, which encompasses `tfSellNFToken` plus the universal flags. The framework uses this mask in `preflight1()` to reject any transaction whose flags include bits not recognized for this type — a forward-compatibility guard against clients setting bits that might acquire meaning under future amendments. + +**`preflight`** runs stateless, no-ledger-access validation. It first extracts the token's embedded flags from the `sfNFTokenID` field (NFT flags like `lsfBurnable`, `lsfOnlyXRP`, and `lsfTransferable` are encoded directly into the token ID), then delegates all parameter validation to `nft::tokenOfferCreatePreflight()`. That shared function validates the offer amount, optional destination and expiration fields, ownership rules, and transaction flags. + +**`preclaim`** performs read-only ledger checks. It does two things the shared code cannot: first, it calls `hasExpired()` to reject an already-expired offer before any other work — this is a ledger-level time check that needs the current ledger's close time. Second, it resolves which account's NFT directory to search based on the `tfSellNFToken` flag: + +```cpp +nft::findToken(ctx.view, + ctx.tx[((txFlags & tfSellNFToken) != 0u) ? sfAccount : sfOwner], + nftokenID) +``` + +For a **sell offer**, the token must be in `sfAccount`'s own directory (the submitter is offering to sell their own token). For a **buy offer**, `sfOwner` names the token's current holder, and the token must be in that account's directory. If the token is not found, `tecNO_ENTRY` is returned immediately, avoiding unnecessary downstream processing. After this check, `nft::tokenOfferCreatePreclaim()` validates business-logic constraints like transfer fee eligibility, destination account existence, and whether the NFT's `lsfTransferable` flag permits third-party trading. + +**`doApply`** simply calls `nft::tokenOfferCreateApply()`, forwarding all transaction fields plus `preFeeBalance_` (the submitter's XRP balance before the transaction fee was deducted). The apply function is responsible for creating the `NFTokenOffer` ledger object, inserting it into the token's buy or sell directory, and adding it to the offer creator's owner directory (which consumes one reserve increment). + +## Code Sharing with `NFTokenMint` + +The most notable design decision is the extraction of all three phases into free functions (`tokenOfferCreatePreflight`, `tokenOfferCreatePreclaim`, `tokenOfferCreateApply`) in `NFTokenHelpers`. This is because `NFTokenMint` optionally creates a sell offer simultaneously with minting — if `sfAmount` is present in a mint transaction, the same validation and apply logic must run. Rather than duplicating rules, the shared functions accept `txFlags` as a parameter with a default of `tfSellNFToken`, since a mint-embedded offer is always a sell offer. + +The `NFTokenCreateOffer` transactor passes `ctx.tx.getFlags()` explicitly, while `NFTokenMint` passes `tfSellNFToken` as a compile-time constant. This distinction keeps the shared helpers generic while encoding the semantic constraint that minting can only produce sell offers, not buy offers. + +## Invariants and Failure Modes + +- `tecEXPIRED` is returned in `preclaim` before `findToken` is even called, since an expired offer cannot be created regardless of token state. +- `tecNO_ENTRY` signals that the referenced NFT does not exist in the expected owner's directory. This check is asymmetric: the owner lookup switches on `tfSellNFToken`, so a sell offer where the submitter does not own the token will always fail here. +- The `ConsequencesFactory{Normal}` declaration means the transaction framework calculates standard fee consequences (deducting the transaction fee and incrementing the account sequence), with no special handling like `Blocker` or custom balance impacts. +- Optional fields (`sfDestination`, `sfExpiration`, `sfOwner`) are accessed via the `~sfField` idiom, which returns `std::optional` rather than throwing on absence — a consistent XRPL pattern for fields that may be omitted without error. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp.ai.json b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp.ai.json new file mode 100644 index 0000000000..e922cd60e0 --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp.ai.json @@ -0,0 +1,538 @@ +{ + "args": [ + { + "lineno": 9, + "name": "txFlags" + }, + { + "lineno": 14, + "name": "ctx" + }, + { + "lineno": 91, + "name": "flags" + }, + { + "lineno": 91, + "name": "fee" + }, + { + "lineno": 91, + "name": "issuer" + }, + { + "lineno": 91, + "name": "taxon" + }, + { + "lineno": 91, + "name": "tokenSeq" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "NFTokenMint::preflight", + "hasOfferFields", + "nft::tokenOfferCreatePreflight" + ], + "entry_point": "NFTokenMint::preflight", + "purpose": "Performs all preflight validation for NFTokenMint transactions, including field presence, value ranges, and offer field validation.", + "validation_points": [ + "NFTokenMint::preflight: sfTransferFee, tfTransferable, sfIssuer, sfURI, offer fields", + "nft::tokenOfferCreatePreflight: offer field validation" + ] + }, + { + "call_chain": [ + "NFTokenMint::preclaim" + ], + "entry_point": "NFTokenMint::preclaim", + "purpose": "Performs additional checks after preflight but before applying the transaction (not shown in provided code, but standard in XRPL transactors).", + "validation_points": [ + "NFTokenMint::preclaim: (not shown, but typically checks ledger state)" + ] + }, + { + "call_chain": [ + "NFTokenMint::doApply", + "NFTokenMint::createNFTokenID" + ], + "entry_point": "NFTokenMint::doApply", + "purpose": "Applies the transaction to the ledger, creating the NFToken after all validations pass.", + "validation_points": [ + "NFTokenMint::doApply: assumes preflight/preclaim passed" + ] + }, + { + "call_chain": [ + "NFTokenMint::checkExtraFeatures", + "hasOfferFields" + ], + "entry_point": "NFTokenMint::checkExtraFeatures", + "purpose": "Checks if extra features (like mint offers) are allowed by network rules.", + "validation_points": [ + "NFTokenMint::checkExtraFeatures: feature flag and offer field presence" + ] + } + ], + "data_flows": [ + { + "field": "sfTransferFee", + "flow": [ + "transaction input", + "NFTokenMint::preflight", + "checked for maxTransferFee and tfTransferable" + ], + "origin": "ctx.tx[~sfTransferFee] (transaction input)", + "transformations": [ + "Checked for presence", + "Compared to maxTransferFee", + "If >0, tfTransferable flag required" + ], + "validated_at": "NFTokenMint::preflight" + }, + { + "field": "tfTransferable (flag)", + "flow": [ + "transaction input", + "NFTokenMint::preflight", + "checked if sfTransferFee > 0" + ], + "origin": "ctx.tx.isFlag(tfTransferable)", + "transformations": [ + "Boolean check" + ], + "validated_at": "NFTokenMint::preflight" + }, + { + "field": "sfIssuer", + "flow": [ + "transaction input", + "NFTokenMint::preflight", + "compared to ctx.tx[sfAccount]" + ], + "origin": "ctx.tx[~sfIssuer]", + "transformations": [ + "Checked for presence", + "Compared to transaction account" + ], + "validated_at": "NFTokenMint::preflight" + }, + { + "field": "sfURI", + "flow": [ + "transaction input", + "NFTokenMint::preflight", + "checked for empty or length > maxTokenURILength" + ], + "origin": "ctx.tx[~sfURI]", + "transformations": [ + "Checked for presence", + "Checked for empty string", + "Checked for length" + ], + "validated_at": "NFTokenMint::preflight" + }, + { + "field": "Offer fields (sfAmount, sfDestination, sfExpiration)", + "flow": [ + "transaction input", + "hasOfferFields", + "NFTokenMint::preflight", + "nft::tokenOfferCreatePreflight" + ], + "origin": "ctx.tx.isFieldPresent(sfAmount/sfDestination/sfExpiration)", + "transformations": [ + "Presence checked", + "If present, further validated by tokenOfferCreatePreflight" + ], + "validated_at": "hasOfferFields, NFTokenMint::preflight, nft::tokenOfferCreatePreflight" + }, + { + "field": "Transaction Flags", + "flow": [ + "transaction input", + "extractNFTokenFlagsFromTxFlags", + "used in offer validation" + ], + "origin": "ctx.tx.getFlags()", + "transformations": [ + "Masked to 16 bits" + ], + "validated_at": "nft::tokenOfferCreatePreflight" + } + ], + "description": "Implements the logic for the NFTokenMint transaction in the XRPL, including preflight checks, preclaim checks, minting logic, and creation of NFT IDs.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTransferFee", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTransferFee validated by Custom explicit validation (no external framework); uses PreflightContext and feature flags", + "issue_pattern": "Missing validation for sfTransferFee", + "why_false_positive": "Custom explicit validation (no external framework); uses PreflightContext and feature flags validates sfTransferFee automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfIssuer", + "validation", + "missing", + "check" + ], + "evidence": "Field sfIssuer validated by Custom explicit validation (no external framework); uses PreflightContext and feature flags", + "issue_pattern": "Missing validation for sfIssuer", + "why_false_positive": "Custom explicit validation (no external framework); uses PreflightContext and feature flags validates sfIssuer automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfURI", + "validation", + "missing", + "check" + ], + "evidence": "Field sfURI validated by Custom explicit validation (no external framework); uses PreflightContext and feature flags", + "issue_pattern": "Missing validation for sfURI", + "why_false_positive": "Custom explicit validation (no external framework); uses PreflightContext and feature flags validates sfURI automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "txFlags", + "validation", + "missing", + "check" + ], + "evidence": "Field txFlags validated by Custom explicit validation (no external framework); uses PreflightContext and feature flags", + "issue_pattern": "Missing validation for txFlags", + "why_false_positive": "Custom explicit validation (no external framework); uses PreflightContext and feature flags validates txFlags automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "offer fields (sfAmount, sfDestination, sfExpiration)", + "validation", + "missing", + "check" + ], + "evidence": "Field offer fields (sfAmount, sfDestination, sfExpiration) validated by Custom explicit validation (no external framework); uses PreflightContext and feature flags", + "issue_pattern": "Missing validation for offer fields (sfAmount, sfDestination, sfExpiration)", + "why_false_positive": "Custom explicit validation (no external framework); uses PreflightContext and feature flags validates offer fields (sfAmount, sfDestination, sfExpiration) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTransferFee", + "empty", + "string", + "validation" + ], + "evidence": "explicit code at NFTokenMint::preflight", + "issue_pattern": "Missing empty string validation for sfTransferFee", + "why_false_positive": "explicit code validates sfTransferFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTransferFee + tfTransferable", + "empty", + "string", + "validation" + ], + "evidence": "explicit code at NFTokenMint::preflight", + "issue_pattern": "Missing empty string validation for sfTransferFee + tfTransferable", + "why_false_positive": "explicit code validates sfTransferFee + tfTransferable for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfIssuer", + "empty", + "string", + "validation" + ], + "evidence": "explicit code at NFTokenMint::preflight", + "issue_pattern": "Missing empty string validation for sfIssuer", + "why_false_positive": "explicit code validates sfIssuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfURI", + "empty", + "string", + "validation" + ], + "evidence": "explicit code at NFTokenMint::preflight", + "issue_pattern": "Missing empty string validation for sfURI", + "why_false_positive": "explicit code validates sfURI for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "offer fields (sfAmount, sfDestination, sfExpiration)", + "empty", + "string", + "validation" + ], + "evidence": "hasOfferFields() helper at NFTokenMint::checkExtraFeatures (used in preflight)", + "issue_pattern": "Missing empty string validation for offer fields (sfAmount, sfDestination, sfExpiration)", + "why_false_positive": "hasOfferFields() helper validates offer fields (sfAmount, sfDestination, sfExpiration) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "txFlags", + "empty", + "string", + "validation" + ], + "evidence": "extractNFTokenFlagsFromTxFlags() at extractNFTokenFlagsFromTxFlags (used in business logic)", + "issue_pattern": "Missing empty string validation for txFlags", + "why_false_positive": "extractNFTokenFlagsFromTxFlags() validates txFlags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "txFlags", + "empty", + "string", + "validation" + ], + "evidence": "getFlagsMask() at NFTokenMint::getFlagsMask", + "issue_pattern": "Missing empty string validation for txFlags", + "why_false_positive": "getFlagsMask() validates txFlags for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp", + "functions": [ + { + "args": [ + "txFlags" + ], + "lineno": 9, + "name": "extractNFTokenFlagsFromTxFlags" + }, + { + "args": [ + "ctx" + ], + "lineno": 14, + "name": "hasOfferFields" + }, + { + "args": [ + "ctx" + ], + "lineno": 20, + "name": "checkExtraFeatures" + }, + { + "args": [ + "ctx" + ], + "lineno": 25, + "name": "getFlagsMask" + }, + { + "args": [ + "ctx" + ], + "lineno": 54, + "name": "preflight" + }, + { + "args": [ + "flags", + "fee", + "issuer", + "taxon", + "tokenSeq" + ], + "lineno": 91, + "name": "createNFTokenID" + }, + { + "args": [ + "ctx" + ], + "lineno": 126, + "name": "preclaim" + }, + { + "args": [], + "lineno": 154, + "name": "doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "NFTokenMint validation is typically covered by unit/integration tests in the rippled codebase, especially in files like 'test/tx/NFTokenMint_test.cpp', 'test/tx/NFToken_test.cpp', or similar. These tests cover valid/invalid transfer fees, issuer/account mismatches, URI length, and offer field combinations. However, edge cases for new amendments (e.g., featureNFTokenMintOffer, featureDynamicNFT, fixRemoveNFTokenAutoTrustLine) may not be fully covered if tests are not updated for new features. Also, negative tests for malformed combinations (e.g., missing sfAmount when offer fields present) should be checked for completeness.", + "validation_architecture": { + "auto_validated_fields": [ + "sfTransferFee", + "sfIssuer", + "sfURI", + "txFlags", + "offer fields (sfAmount, sfDestination, sfExpiration)" + ], + "framework": "Custom explicit validation (no external framework); uses PreflightContext and feature flags", + "validation_layer": "business_logic (preflight phase of transaction processing)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temBAD_NFTOKEN_TRANSFER_FEE", + "field": "sfTransferFee", + "location": "NFTokenMint::preflight", + "validated_by": "explicit code", + "validates": [ + "Checks if sfTransferFee is present", + "Ensures sfTransferFee does not exceed maxTransferFee" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfTransferFee + tfTransferable", + "location": "NFTokenMint::preflight", + "validated_by": "explicit code", + "validates": [ + "If sfTransferFee is non-zero, tfTransferable flag must be set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfIssuer", + "location": "NFTokenMint::preflight", + "validated_by": "explicit code", + "validates": [ + "Issuer must only be set if tx is not executed by the minter (sfIssuer != sfAccount)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfURI", + "location": "NFTokenMint::preflight", + "validated_by": "explicit code", + "validates": [ + "If sfURI is present, it must not be empty and must not exceed maxTokenURILength" + ], + "validation_type": "format|range" + }, + { + "confidence": 0.9, + "error_thrown": "implicit (prevents tx if feature not enabled)", + "field": "offer fields (sfAmount, sfDestination, sfExpiration)", + "location": "NFTokenMint::checkExtraFeatures (used in preflight)", + "validated_by": "hasOfferFields() helper", + "validates": [ + "If any offer fields are present, featureNFTokenMintOffer must be enabled" + ], + "validation_type": "business_logic|feature_flag" + }, + { + "confidence": 0.8, + "error_thrown": "none (used for further validation)", + "field": "txFlags", + "location": "extractNFTokenFlagsFromTxFlags (used in business logic)", + "validated_by": "extractNFTokenFlagsFromTxFlags()", + "validates": [ + "Extracts lower 16 bits of txFlags for NFT flag validation" + ], + "validation_type": "type|bitmask" + }, + { + "confidence": 0.8, + "error_thrown": "none (used for further validation)", + "field": "txFlags", + "location": "NFTokenMint::getFlagsMask", + "validated_by": "getFlagsMask()", + "validates": [ + "Determines allowed flag mask based on enabled amendments/features" + ], + "validation_type": "business_logic|feature_flag" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp.ai.md b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp.ai.md new file mode 100644 index 0000000000..ab4d9cf65e --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenMint.cpp.ai.md @@ -0,0 +1,63 @@ +# `NFTokenMint.cpp` — NFT Minting Transactor + +## Role and Purpose + +This file implements the `NFTokenMint` transactor, which handles the XRPL transaction that creates a new Non-Fungible Token (NFT) on the ledger. It is one of six NFT-specific transactors in the `src/libxrpl/tx/transactors/nft/` module and is responsible for the full lifecycle of token creation: validating inputs before any ledger state is touched, verifying preconditions against live ledger state, and then materializing the token in the owner's NFToken page directory. The file also implements the canonical algorithm for constructing a globally unique 256-bit NFToken ID — a compact encoding that eliminates the need for a separate ID registry. + +## The Three-Phase Transaction Model + +XRPL transactions follow a pipeline: `preflight` (stateless validation), `preclaim` (read-only ledger checks), and `doApply` (state mutations). `NFTokenMint` implements all three phases as static methods on the `NFTokenMint` class. + +**`preflight`** rejects nonsensical inputs before any ledger I/O occurs. Its checks are deliberately sequenced from cheapest to most expensive: + +- A `sfTransferFee` exceeding `maxTransferFee` (50,000 basis points / 50%) returns `temBAD_NFTOKEN_TRANSFER_FEE`. A non-zero fee without the `tfTransferable` flag is logically contradictory — you cannot collect royalties on a non-transferable token — so it returns `temMALFORMED`. +- Setting `sfIssuer` equal to `sfAccount` is meaningless (the account is already the issuer by default) and is rejected as `temMALFORMED`. +- A present `sfURI` must be non-empty and no longer than `maxTokenURILength` (256 bytes). The empty-string case is rejected because it is ambiguous with the absent-field signal; callers who want no URI simply omit the field. +- If any of `sfAmount`, `sfDestination`, or `sfExpiration` are present the code delegates to `nft::tokenOfferCreatePreflight`, the shared offer-validation routine also used by `NFTokenCreateOffer`. This reuse enforces consistent offer semantics across both transaction types. + +**`preclaim`** performs read-only ledger queries to confirm the issuer exists and the minter has permission. When `sfIssuer` is set, it reads the issuer's `AccountRoot` and verifies that the `sfNFTokenMinter` field on that account matches the transaction sender. If it does not, `tecNO_PERMISSION` is returned. When offer fields are present it also checks expiration and delegates to `nft::tokenOfferCreatePreclaim`. + +**`doApply`** applies state changes: it increments the issuer's mint counter, constructs the NFToken ID, inserts the token into the owner's page directory, optionally creates an immediately-attached sell offer, and then checks the reserve. + +## Constructing the NFToken ID + +`createNFTokenID` produces a deterministic, compact 256-bit identifier by packing five fields into a fixed-layout big-endian byte array: + +``` +[0–1] flags (2 bytes) +[2–3] transfer fee (2 bytes) +[4–23] issuer AccountID (20 bytes) +[24–27] ciphered taxon (4 bytes) +[28–31] token sequence (4 bytes) +``` + +This layout is not arbitrary: the accessor functions in `include/xrpl/protocol/nft.h` (e.g., `getFlags`, `getTransferFee`, `getIssuer`, `getTaxon`, `getSerial`) hard-code these byte offsets to extract fields from a live token ID without deserializing anything. The method is `public` and marked as supporting unit tests to allow direct exercising of the ID construction logic independent of ledger machinery. + +### The Ciphered Taxon + +Before packing, the taxon is passed through `nft::cipheredTaxon(tokenSeq, taxon)`, which applies a linear congruential transformation: `taxon XOR ((384160001 * tokenSeq) + 2459)`. This permutation is chosen because of the Hull-Dobell theorem guarantees: with an appropriate multiplier and increment over a power-of-two modulus it produces a bijection on `[0, 2^32)`. The purpose is ledger performance: if many NFTs shared the same taxon, their IDs would cluster together in the B-tree page directory, forming oversized pages. By mixing the taxon with the sequence number (which is outside the issuer's direct control), the distribution is spread across many pages without adding any storage overhead. Crucially, the transformation is marked as a breaking change: altering these constants would require an amendment because it would change the IDs of tokens that would otherwise have been identical. + +## Amendment-Aware Flag Masking + +`getFlagsMask` returns the set of legal transaction flags using a nested conditional on two amendments: + +- **`fixRemoveNFTokenAutoTrustLine`**: Before this amendment, issuers could set the `tfTrustLine` flag, which allowed an NFT transfer to implicitly create a TrustLine on the issuer's account. This was exploited as a reserve-inflation attack — two accounts could trade the token back and forth indefinitely, adding TrustLines (and thus reserve requirements) to the issuer without their consent. Once the amendment is active, `tfTrustLine` is removed from the valid mask entirely, making it illegal to mint with that flag. +- **`featureDynamicNFT`**: Introduced the `tfMutable` flag, which allows the token URI to be modified after minting via `NFTokenModify`. When this amendment is enabled, `tfMutable` is added to the valid mask. + +This cascading logic means the flag mask can be one of four values depending on which amendments have activated, all computed at the `PreflightContext` level before any ledger access. + +## The Mint Counter and FirstNFTokenSequence Bootstrap + +The `doApply` method manages a subtle bootstrapping problem. Each issuer's `AccountRoot` stores a cumulative `sfMintedNFTokens` counter, and the token's sequence number is derived as `sfFirstNFTokenSequence + sfMintedNFTokens`. The `sfFirstNFTokenSequence` is set only once — on the first mint — and its initial value must match the issuer's account sequence at the time of that first mint. + +The complication is that by the time `doApply` runs, the account sequence has already been pre-incremented for normal (non-Ticket) transactions. The code therefore subtracts one for self-minting with a direct sequence, but uses the sequence as-is for Ticket-based transactions (which do not increment the account sequence) and for authorized-minter transactions (where the issuer's sequence is completely untouched). Getting this wrong would cause the first token's ID to encode an incorrect sequence, permanently invalidating the uniqueness guarantee. + +Overflow is also defended: `sfMintedNFTokens` wraps at 32 bits, so after incrementing, `doApply` checks for zero (wraparound) and also verifies that `tokenSeq` did not overflow back below the `sfFirstNFTokenSequence` offset. Either condition returns `tecMAX_SEQUENCE_REACHED`. These checks use the `Expected` return type from the lambda, propagating errors without exceptions. + +## Composite Mint-and-Offer + +When `sfAmount` is present in the transaction, `doApply` calls `nft::tokenOfferCreateApply` immediately after inserting the token. This creates a sell offer atomically with the mint itself — a common pattern for marketplaces that want to list tokens for sale as part of a single transaction. This feature is gated by `featureNFTokenMintOffer`, checked in `checkExtraFeatures`: if the feature is not active and offer fields are present, the transaction is rejected before preflight. The shared `nft::tokenOfferCreate*` routines enforce that only sell offers (not buy offers) can be created this way, by passing `tfSellNFToken` as the implicit transaction flags. + +## Reserve Checking + +The reserve check at the end of `doApply` is deliberately conditional on whether the owner count actually increased. Inserting an NFToken into an existing page that has spare capacity does not create a new ledger object and therefore should not require an incremental reserve. The reserve check is only enforced when a new page must be allocated or when a new sell offer is created — both of which increment `sfOwnerCount`. This is an intentional performance and usability tradeoff: issuers with many tokens in a given taxon range can add tokens without repeatedly satisfying reserve requirements, as long as the page isn't full. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp.ai.json b/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp.ai.json new file mode 100644 index 0000000000..97248a24ef --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp.ai.json @@ -0,0 +1,300 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "NFTokenModify::preflight" + ], + "entry_point": "NFTokenModify::preflight", + "purpose": "Initial stateless validation of the transaction fields before any ledger access.", + "validation_points": [ + "sfOwner validated by explicit comparison (owner == sfAccount)", + "sfURI validated by explicit length and emptiness check" + ] + }, + { + "call_chain": [ + "NFTokenModify::preclaim" + ], + "entry_point": "NFTokenModify::preclaim", + "purpose": "Stateful validation: checks ledger for NFT existence, mutability, and permissions.", + "validation_points": [ + "sfNFTokenID validated by nft::findToken", + "sfNFTokenID validated by nft::getFlags & nft::flagMutable", + "sfNFTokenID, sfAccount validated by nft::getIssuer, account comparison, sfNFTokenMinter" + ] + }, + { + "call_chain": [ + "NFTokenModify::doApply", + "nft::changeTokenURI" + ], + "entry_point": "NFTokenModify::doApply", + "purpose": "Applies the transaction to the ledger, updating the NFT's URI.", + "validation_points": [ + "No new validation; assumes preflight and preclaim have succeeded" + ] + } + ], + "data_flows": [ + { + "field": "sfOwner", + "flow": [ + "Transaction input", + "NFTokenModify::preflight (compared to sfAccount)", + "NFTokenModify::preclaim (used to determine owner)", + "NFTokenModify::doApply (used as owner argument)" + ], + "origin": "ctx.tx[~sfOwner] (optional field in transaction)", + "transformations": [ + "Optional extraction (may default to sfAccount if not present)", + "Compared for equality to sfAccount" + ], + "validated_at": "NFTokenModify::preflight (owner != sfAccount)" + }, + { + "field": "sfURI", + "flow": [ + "Transaction input", + "NFTokenModify::preflight (checked for emptiness and length)", + "NFTokenModify::doApply (passed to nft::changeTokenURI)" + ], + "origin": "ctx.tx[~sfURI] (optional field in transaction)", + "transformations": [ + "Optional extraction", + "Checked for empty or too long" + ], + "validated_at": "NFTokenModify::preflight" + }, + { + "field": "sfNFTokenID", + "flow": [ + "Transaction input", + "NFTokenModify::preclaim (used in nft::findToken, nft::getFlags, nft::getIssuer)", + "NFTokenModify::doApply (passed to nft::changeTokenURI)" + ], + "origin": "ctx.tx[sfNFTokenID] (required field in transaction)", + "transformations": [ + "Used to look up NFT in ledger", + "Used to extract flags and issuer" + ], + "validated_at": "NFTokenModify::preclaim (existence, mutability, issuer/minter permissions)" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "NFTokenModify::preflight (compared to sfOwner)", + "NFTokenModify::preclaim (used as account, compared to issuer/minter)", + "NFTokenModify::doApply (used as fallback owner)" + ], + "origin": "ctx.tx[sfAccount] (required field in transaction)", + "transformations": [ + "Direct comparison", + "Used for permission checks" + ], + "validated_at": "NFTokenModify::preflight, NFTokenModify::preclaim" + }, + { + "field": "sfNFTokenMinter", + "flow": [ + "NFTokenModify::preclaim (read from issuer's account root if issuer != account)", + "Used to check if minter matches account" + ], + "origin": "Account root in ledger (optional field)", + "transformations": [ + "Optional extraction from ledger", + "Compared to account" + ], + "validated_at": "NFTokenModify::preclaim" + } + ], + "description": "Implements the NFTokenModify transaction logic for modifying NFT metadata (such as URI) on the XRPL, including preflight, preclaim, and apply steps with permission and validity checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwner", + "empty", + "string", + "validation" + ], + "evidence": "explicit comparison at NFTokenModify::preflight", + "issue_pattern": "Missing empty string validation for sfOwner", + "why_false_positive": "explicit comparison validates sfOwner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfURI", + "empty", + "string", + "validation" + ], + "evidence": "explicit length and emptiness check at NFTokenModify::preflight", + "issue_pattern": "Missing empty string validation for sfURI", + "why_false_positive": "explicit length and emptiness check validates sfURI for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenID", + "empty", + "string", + "validation" + ], + "evidence": "nft::findToken at NFTokenModify::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenID", + "why_false_positive": "nft::findToken validates sfNFTokenID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenID", + "empty", + "string", + "validation" + ], + "evidence": "nft::getFlags & nft::flagMutable at NFTokenModify::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenID", + "why_false_positive": "nft::getFlags & nft::flagMutable validates sfNFTokenID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfNFTokenID, sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "nft::getIssuer, account comparison, sfNFTokenMinter at NFTokenModify::preclaim", + "issue_pattern": "Missing empty string validation for sfNFTokenID, sfAccount", + "why_false_positive": "nft::getIssuer, account comparison, sfNFTokenMinter validates sfNFTokenID, sfAccount for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 7, + "name": "NFTokenModify::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 18, + "name": "NFTokenModify::preclaim" + }, + { + "args": [], + "lineno": 39, + "name": "NFTokenModify::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this code would typically be found in unit/integration tests for NFT modification transactions, likely in files such as 'test/tx/NFTokenModify_test.cpp', 'test/tx/NFToken_test.cpp', or similar. Tests should cover: (1) owner/account mismatch, (2) URI length/emptiness, (3) non-existent NFT, (4) immutable NFT, (5) issuer/minter permissions, (6) successful modification. Gaps may exist if tests do not cover all combinations of optional fields (e.g., missing sfOwner, missing sfURI), or edge cases like issuer account missing in ledger. Coverage for error codes (temMALFORMED, tecNO_ENTRY, tecNO_PERMISSION, tecINTERNAL) should be verified.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in C++ (xrpl transaction processing)", + "validation_layer": "business_logic (preflight and preclaim phases of transaction processing)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfOwner", + "location": "NFTokenModify::preflight", + "validated_by": "explicit comparison", + "validates": [ + "Checks if sfOwner (if present) is the same as sfAccount", + "Disallows transaction if owner is the same as account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfURI", + "location": "NFTokenModify::preflight", + "validated_by": "explicit length and emptiness check", + "validates": [ + "Checks if sfURI is present", + "Checks if sfURI is not empty", + "Checks if sfURI length does not exceed maxTokenURILength" + ], + "validation_type": "format|range" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfNFTokenID", + "location": "NFTokenModify::preclaim", + "validated_by": "nft::findToken", + "validates": [ + "Checks if the NFT with sfNFTokenID exists for the owner" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfNFTokenID", + "location": "NFTokenModify::preclaim", + "validated_by": "nft::getFlags & nft::flagMutable", + "validates": [ + "Checks if the NFT is mutable (flagMutable set)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL or tecNO_PERMISSION", + "field": "sfNFTokenID, sfAccount", + "location": "NFTokenModify::preclaim", + "validated_by": "nft::getIssuer, account comparison, sfNFTokenMinter", + "validates": [ + "Checks if the transaction sender is the issuer or minter of the NFT", + "If not, checks if the issuer's account exists", + "If not, checks if the minter matches the sender" + ], + "validation_type": "business_logic|authorization" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp.ai.md b/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp.ai.md new file mode 100644 index 0000000000..9f0f1395a3 --- /dev/null +++ b/src/libxrpl/tx/transactors/nft/NFTokenModify.cpp.ai.md @@ -0,0 +1,47 @@ +# `NFTokenModify.cpp` — NFT Metadata Modification Transactor + +`NFTokenModify.cpp` implements the `NFTokenModify` transaction, which allows an authorized party to update the metadata URI attached to a mutable NFT on the XRP Ledger. It follows the standard three-phase transactor pattern—`preflight`, `preclaim`, `doApply`—and is one of the smallest transactors in the NFT suite, because nearly all heavy lifting is delegated to `nft::changeTokenURI` in `NFTokenHelpers.cpp`. + +## Why This Transactor Exists + +NFTs minted on the XRPL carry an immutable `NFTokenID` that encodes their flags, issuer, taxon, and serial number at the bit level (as defined in `protocol/nft.h`). Mutability of the token itself is opt-in: only tokens whose `flagMutable` bit (`0x0010`) was set at mint time can ever have their URI changed. This transaction exists precisely to serve that case — issuers who need to update off-chain metadata (artwork pointers, license files, version hashes) without burning and re-minting the token, at the cost of accepting the trust implications of mutable state. + +## `preflight` — Stateless Field Validation + +`preflight` runs before any ledger reads, so it only validates the raw transaction fields. + +The first check prevents a logical contradiction: if the optional `sfOwner` field is present and equals `sfAccount`, the transaction is malformed. `sfOwner` is only meaningful when a third party (e.g., a marketplace wallet) currently holds the token; submitting it with your own account is redundant and signals a construction error. + +The second check guards the optional `sfURI` field. If supplied, the URI must be non-empty and must not exceed `maxTokenURILength`. The design choice to make `sfURI` optional rather than required is deliberate: an issuer who wants to strip the URI entirely (reverting to a bare token with no metadata pointer) submits the transaction without the field. `changeTokenURI` treats `std::nullopt` as "remove the field," and `preflight` never rejects the absent case. + +## `preclaim` — Stateful Authorization Checks + +`preclaim` has ledger read access and performs three sequential checks against the live ledger state. + +**Existence check.** `nft::findToken` scans the owner's `NFTokenPage` directory for the given `NFTokenID`. Failure returns `tecNO_ENTRY`. The owner is resolved here with the same `sfOwner`-or-`sfAccount` fallback used in `doApply`. + +**Mutability check.** `nft::getFlags(nftokenID)` extracts the 16-bit flag field packed into the first two bytes of the 256-bit token ID (big-endian, as `nft.h` shows). If `flagMutable` is clear, the token was minted as permanently immutable and no modification is ever permitted — `tecNO_PERMISSION`. + +**Issuer/minter authorization check.** The token ID also embeds the issuer's `AccountID` (bytes 4–24, via `nft::getIssuer`). If the transaction sender is not the issuer, the code reads the issuer's account root SLE and checks its optional `sfNFTokenMinter` field. This mirrors the delegation model used by `NFTokenMint`: an issuer can designate a single minter account that acts on their behalf. If the sender matches neither the issuer nor the designated minter, `tecNO_PERMISSION` is returned. + +The `tecINTERNAL` path on the issuer account lookup (`// LCOV_EXCL_LINE`) guards against a theoretically impossible state: the issuer address is encoded in the token ID itself and must have existed at mint time, so its account root cannot legitimately be absent. The comment and exclusion from coverage reflect this. + +## `doApply` — Ledger Mutation + +With all validation passed, `doApply` is minimal: + +```cpp +return nft::changeTokenURI(view(), owner, nftokenID, ctx_.tx[~sfURI]); +``` + +`nft::changeTokenURI` locates the `NFTokenPage` SLE for the owner, finds the specific `STObject` entry in the page's `sfNFTokens` array that matches the token ID, then either sets `sfURI` (if the optional slice is present) or calls `makeFieldAbsent` to remove the field entirely. The page SLE is then marked dirty via `view.update(page)`. If the page or token cannot be found at this stage, `tecINTERNAL` is returned — a redundant guard since `preclaim` already confirmed existence, but present to handle any hypothetical inconsistency introduced by conflicting transactions in the same ledger batch. + +## Permission Model and the `sfOwner` Field + +The ownership/authorization separation is a recurring pattern in NFT transactors. A token minted by issuer A and later transferred to wallet B is still controlled by A for modification purposes — B owns the token economically but has no authority to change its metadata. When A (or A's designated minter) wants to modify that token, they must include `sfOwner: B` in the transaction to tell the ledger where to find it. Omitting `sfOwner` means "the token is in my own wallet." This is why `preflight` rejects `sfOwner == sfAccount`: the correct way to modify a self-owned token is simply to leave `sfOwner` absent. + +## Relationship to Sibling Files + +- **`protocol/nft.h`**: Defines `flagMutable`, `getFlags()`, and `getIssuer()` as inline functions operating directly on the binary layout of the `uint256` token ID. `NFTokenModify` uses these to avoid deserializing the full token object just for a permission check. +- **`ledger/helpers/NFTokenHelpers.h`**: Declares `findToken` and `changeTokenURI`. The former is used in `preclaim` (read-only `ReadView`), while the latter requires a writable `ApplyView` and is called only in `doApply`. +- **`NFTokenMint`**: The minter delegation model checked in `preclaim` is the same pattern used during minting — `sfNFTokenMinter` on the issuer's account root grants one account the ability to act as proxy issuer for both creating and modifying tokens. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp.ai.json b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp.ai.json new file mode 100644 index 0000000000..7305f042f9 --- /dev/null +++ b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp.ai.json @@ -0,0 +1,462 @@ +{ + "args": [ + { + "lineno": 8, + "name": "ctx" + }, + { + "lineno": 13, + "name": "ctx" + }, + { + "lineno": 32, + "name": "view" + }, + { + "lineno": 32, + "name": "sle" + }, + { + "lineno": 32, + "name": "account" + }, + { + "lineno": 32, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "OracleDelete::preflight" + ], + "entry_point": "OracleDelete::preflight", + "purpose": "Initial preflight checks for transaction validity (syntax, basic rules).", + "validation_points": [] + }, + { + "call_chain": [ + "OracleDelete::preclaim" + ], + "entry_point": "OracleDelete::preclaim", + "purpose": "Performs ledger state validation before transaction is applied.", + "validation_points": [ + "ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfAccount)))", + "ctx.view.read(keylet::oracle(ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID]))", + "ctx.tx.getAccountID(sfAccount) != sle->getAccountID(sfOwner)" + ] + }, + { + "call_chain": [ + "OracleDelete::doApply", + "OracleDelete::deleteOracle" + ], + "entry_point": "OracleDelete::doApply", + "purpose": "Applies the OracleDelete transaction to the ledger.", + "validation_points": [ + "if (auto sle = ctx_.view().peek(keylet::oracle(account_, ctx_.tx[sfOracleDocumentID])))", + "OracleDelete::deleteOracle: if (!sle)", + "OracleDelete::deleteOracle: if (!view.dirRemove(...))", + "OracleDelete::deleteOracle: if (!sleOwner)" + ] + } + ], + "data_flows": [ + { + "field": "sfAccount", + "flow": [ + "ctx.tx.getAccountID(sfAccount)", + "keylet::account(ctx.tx.getAccountID(sfAccount))", + "ctx.view.exists(...)", + "keylet::oracle(ctx.tx.getAccountID(sfAccount), ...)", + "ctx.view.read(...)" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Extracted as AccountID from transaction", + "Used to construct keylets for account and oracle objects" + ], + "validated_at": "OracleDelete::preclaim (account existence, ownership)" + }, + { + "field": "sfOracleDocumentID", + "flow": [ + "ctx.tx[sfOracleDocumentID]", + "keylet::oracle(ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID])", + "ctx.view.read(...)" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Used as key to locate Oracle SLE" + ], + "validated_at": "OracleDelete::preclaim (oracle existence)" + }, + { + "field": "sle (Oracle SLE pointer)", + "flow": [ + "sle", + "sle->getAccountID(sfOwner)", + "deleteOracle(sle, ...)", + "view.dirRemove(..., sle->key(), ...)", + "view.erase(sle)" + ], + "origin": "ctx.view.read(keylet::oracle(...))", + "transformations": [ + "Checked for existence", + "Used to verify ownership", + "Used for directory removal and erasure" + ], + "validated_at": "OracleDelete::preclaim (existence, ownership), OracleDelete::deleteOracle (existence)" + }, + { + "field": "sfOwnerNode", + "flow": [ + "view.dirRemove(keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), true)" + ], + "origin": "(*sle)[sfOwnerNode]", + "transformations": [ + "Used as directory node index for removal" + ], + "validated_at": "OracleDelete::deleteOracle (dirRemove success)" + }, + { + "field": "sfPriceDataSeries", + "flow": [ + "sle->getFieldArray(sfPriceDataSeries).size()", + "count = (size > 5 ? -2 : -1)", + "adjustOwnerCount(view, sleOwner, count, j)" + ], + "origin": "sle->getFieldArray(sfPriceDataSeries)", + "transformations": [ + "Array size checked to determine decrement value for owner count" + ], + "validated_at": "No explicit validation, but used in logic" + } + ], + "description": "Implements the OracleDelete transaction logic for deleting oracle entries from the XRPL ledger, including preflight, preclaim, and apply steps.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfAccount))) at preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfAccount))) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOracleDocumentID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::oracle(ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID])) at preclaim", + "issue_pattern": "Missing empty string validation for sfOracleDocumentID", + "why_false_positive": "ctx.view.read(keylet::oracle(ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID])) validates sfOracleDocumentID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount vs sfOwner", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.getAccountID(sfAccount) != sle->getAccountID(sfOwner) at preclaim", + "issue_pattern": "Missing empty string validation for sfAccount vs sfOwner", + "why_false_positive": "ctx.tx.getAccountID(sfAccount) != sle->getAccountID(sfOwner) validates sfAccount vs sfOwner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle (Oracle SLE pointer)", + "empty", + "string", + "validation" + ], + "evidence": "!sle at deleteOracle", + "issue_pattern": "Missing empty string validation for sle (Oracle SLE pointer)", + "why_false_positive": "!sle validates sle (Oracle SLE pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sle (Oracle SLE pointer)", + "type", + "validation", + "check" + ], + "evidence": "!sle at deleteOracle", + "issue_pattern": "Missing type validation for sle (Oracle SLE pointer)", + "why_false_positive": "!sle validates sle (Oracle SLE pointer) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "dirRemove success", + "empty", + "string", + "validation" + ], + "evidence": "!view.dirRemove(...) at deleteOracle", + "issue_pattern": "Missing empty string validation for dirRemove success", + "why_false_positive": "!view.dirRemove(...) validates dirRemove success for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sleOwner (Account SLE pointer)", + "empty", + "string", + "validation" + ], + "evidence": "!sleOwner at deleteOracle", + "issue_pattern": "Missing empty string validation for sleOwner (Account SLE pointer)", + "why_false_positive": "!sleOwner validates sleOwner (Account SLE pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sleOwner (Account SLE pointer)", + "type", + "validation", + "check" + ], + "evidence": "!sleOwner at deleteOracle", + "issue_pattern": "Missing type validation for sleOwner (Account SLE pointer)", + "why_false_positive": "!sleOwner validates sleOwner (Account SLE pointer) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Oracle SLE existence", + "empty", + "string", + "validation" + ], + "evidence": "ctx_.view().peek(keylet::oracle(account_, ctx_.tx[sfOracleDocumentID])) at doApply", + "issue_pattern": "Missing empty string validation for Oracle SLE existence", + "why_false_positive": "ctx_.view().peek(keylet::oracle(account_, ctx_.tx[sfOracleDocumentID])) validates Oracle SLE existence for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "OracleDelete::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 13, + "name": "OracleDelete::preclaim" + }, + { + "args": [ + "ApplyView& view", + "std::shared_ptr const& sle", + "AccountID const& account", + "beast::Journal j" + ], + "lineno": 32, + "name": "OracleDelete::deleteOracle" + }, + { + "args": [], + "lineno": 54, + "name": "OracleDelete::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code contains LCOV_EXCL_LINE and LCOV_EXCL_START/STOP comments, indicating some error paths are excluded from coverage (e.g., internal errors, impossible states). Main validation paths (account existence, oracle existence, ownership) are likely covered by standard transaction tests. However, error branches for internal failures (e.g., dirRemove failure, missing sleOwner) are not tested. Test files likely to cover this logic include transaction/OracleDelete and ledger/Oracle tests, but explicit negative tests for internal errors may be missing.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl ledger view and SLE accessors", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "terNO_ACCOUNT", + "field": "sfAccount", + "location": "preclaim", + "validated_by": "ctx.view.exists(keylet::account(ctx.tx.getAccountID(sfAccount)))", + "validates": [ + "Checks that the account specified by sfAccount exists in the ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfOracleDocumentID", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::oracle(ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID]))", + "validates": [ + "Checks that the Oracle entry for the given account and OracleDocumentID exists" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "sfAccount vs sfOwner", + "location": "preclaim", + "validated_by": "ctx.tx.getAccountID(sfAccount) != sle->getAccountID(sfOwner)", + "validates": [ + "Checks that the account requesting the delete is the owner of the Oracle entry" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "sle (Oracle SLE pointer)", + "location": "deleteOracle", + "validated_by": "!sle", + "validates": [ + "Checks that the Oracle SLE pointer is not null before proceeding" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "dirRemove success", + "location": "deleteOracle", + "validated_by": "!view.dirRemove(...)", + "validates": [ + "Checks that the Oracle entry is successfully removed from the owner's directory" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "sleOwner (Account SLE pointer)", + "location": "deleteOracle", + "validated_by": "!sleOwner", + "validates": [ + "Checks that the owner's Account SLE pointer is not null before adjusting owner count" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "Oracle SLE existence", + "location": "doApply", + "validated_by": "ctx_.view().peek(keylet::oracle(account_, ctx_.tx[sfOracleDocumentID]))", + "validates": [ + "Checks that the Oracle SLE exists before attempting to delete" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp.ai.md b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp.ai.md new file mode 100644 index 0000000000..a6ff2f4834 --- /dev/null +++ b/src/libxrpl/tx/transactors/oracle/OracleDelete.cpp.ai.md @@ -0,0 +1,42 @@ +# `OracleDelete.cpp` — Price Oracle Deletion Transactor + +## Purpose and Context + +This file implements the `OracleDelete` transaction type, which removes a Price Oracle ledger object (`ltORACLE`) from the XRP Ledger. Price Oracles, specified in XLS-47d, serve as on-chain bridges for external price data used by decentralized applications. `OracleDelete` is the counterpart to `OracleSet`: where `OracleSet` creates or updates oracle entries, this transactor tears them down, cleaning up the ledger state and returning the owner reserve. + +The file sits in the `xrpl::` namespace alongside `OracleSet.cpp`, and the `OracleDelete` class inherits from `Transactor` via the standard three-phase transaction framework: `preflight` → `preclaim` → `doApply`. + +## Three-Phase Transaction Flow + +**`preflight`** is intentionally trivial — it returns `tesSUCCESS` unconditionally. This is a deliberate design choice: a delete operation has no stateless properties to validate (no field ranges, no array size constraints), so all meaningful checks are deferred to the phase that can inspect ledger state. + +**`preclaim`** does the real validation against a read-only ledger snapshot. It checks two things: that the submitting account exists (`terNO_ACCOUNT` if not), and that an oracle object exists at `keylet::oracle(account, sfOracleDocumentID)` (`tecNO_ENTRY` if not). A third check compares `sfAccount` from the transaction against `sfOwner` stored in the oracle SLE, but the code comments this as unreachable — because the oracle keylet is derived from the account ID, successfully reading the oracle at that key is sufficient proof of ownership. The ownership comparison is retained as a defensive invariant and is excluded from coverage metrics (`LCOV_EXCL_START/STOP`). + +**`doApply`** re-fetches the oracle SLE using `peek()` (which returns a mutable reference into the apply-view sandbox) and delegates immediately to the static `deleteOracle()` helper. + +## `deleteOracle` — the Core Deletion Logic + +The static `deleteOracle()` method is the architectural heart of this file, and its exposure as a `public static` is significant: other transactors (or future extensions) can reuse oracle teardown without creating an `OracleDelete` transactor instance. + +The deletion sequence follows a strict order that reflects ledger integrity requirements: + +1. **Directory removal first** — `view.dirRemove(keylet::ownerDir(account), (*sle)[sfOwnerNode], sle->key(), true)` removes the oracle from its account's owner directory. This operation reads `sfOwnerNode` from the oracle SLE, which stores the directory page index populated during creation in `OracleSet::doApply`. The SLE must still be alive for this step; erasing it first would lose the page pointer. Failure here returns `tefBAD_LEDGER`, marking internal ledger corruption. + +2. **Owner count adjustment** — the owner reserve is decremented by either `-1` or `-2` depending on whether the oracle held more than 5 price data series entries: + ```cpp + auto const count = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1; + adjustOwnerCount(view, sleOwner, count, j); + ``` + This mirrors the creation path in `OracleSet::doApply`, where oracles with up to 5 entries consume one owner reserve unit and those with more consume two. The asymmetry in reserve cost reflects the ledger storage cost of large price series arrays. By reading the actual current size from the SLE rather than accepting it from the transaction, the logic is immune to a caller passing a mismatched count. + +3. **SLE erasure** — `view.erase(sle)` removes the oracle object from the ledger state. This is the final step; after it, the SLE pointer is invalid and no further reads from it are safe. + +## Error Handling and Defensive Patterns + +Several error paths are explicitly marked `LCOV_EXCL_LINE` or bracketed in `LCOV_EXCL_START/STOP`, signaling that they guard against conditions the framework guarantees cannot occur in correct operation: a null SLE in `deleteOracle` after `doApply` already confirmed existence; a missing account SLE after `preclaim` confirmed the account exists; and the ownership mismatch discussed above. These are treated as internal fault detection rather than expected application logic. + +The `tefBAD_LEDGER` return from a failed `dirRemove` is the one truly unrecoverable error path: it would mean the owner directory and the oracle SLE have diverged in state, indicating a ledger consistency bug rather than a user error. + +## Relationship to `OracleSet` + +Reading `OracleSet.cpp` in context clarifies the reserve accounting symmetry. On creation, `OracleSet::doApply` calls `dirInsert`, sets `sfOwnerNode` on the new SLE, and increments the owner count by 1 or 2 depending on series size. `OracleDelete::deleteOracle` exactly reverses these three operations. The `> 5` threshold used for the decrement here matches the `> 5` threshold used for the increment there, ensuring the reserve returned equals the reserve originally taken. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp.ai.json b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp.ai.json new file mode 100644 index 0000000000..6d2a1aa4e4 --- /dev/null +++ b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp.ai.json @@ -0,0 +1,543 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "OracleSet::preflight" + ], + "entry_point": "OracleSet::preflight", + "purpose": "Initial stateless validation of OracleSet transaction fields before further processing.", + "validation_points": [ + "Checks if sfPriceDataSeries is empty (temARRAY_EMPTY)", + "Checks if sfPriceDataSeries size exceeds maxOracleDataSeries (temARRAY_TOO_LARGE)", + "Checks if sfProvider, sfURI, sfAssetClass have invalid lengths (temMALFORMED)" + ] + }, + { + "call_chain": [ + "OracleSet::preclaim" + ], + "entry_point": "OracleSet::preclaim", + "purpose": "Stateful validation, including account existence, time checks, and per-entry validation in sfPriceDataSeries.", + "validation_points": [ + "Checks if account exists (terNO_ACCOUNT)", + "Checks if sfLastUpdateTime is within allowed range (tecINVALID_UPDATE_TIME, tecINTERNAL)", + "Iterates sfPriceDataSeries: validates base/quote asset difference, uniqueness, scale, presence of price (temMALFORMED)", + "Checks if update time is more recent than previous (tecINVALID_UPDATE_TIME)", + "Checks consistency of sfProvider and sfAssetClass (temMALFORMED)" + ] + }, + { + "call_chain": [ + "OracleSet::doApply" + ], + "entry_point": "OracleSet::doApply", + "purpose": "Applies the transaction after preflight and preclaim have succeeded. Not shown in full, but would use validated data.", + "validation_points": [ + "Relies on previous validations in preflight and preclaim" + ] + } + ], + "data_flows": [ + { + "field": "sfPriceDataSeries", + "flow": [ + "ctx.tx.getFieldArray(sfPriceDataSeries)", + "OracleSet::preflight: checked for empty/size", + "OracleSet::preclaim: iterated for per-entry validation", + "Used in doApply to update ledger" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for emptiness and size", + "Each entry checked for base/quote asset, uniqueness, scale, price presence" + ], + "validated_at": "OracleSet::preflight, OracleSet::preclaim" + }, + { + "field": "sfProvider", + "flow": [ + "ctx.tx[sfProvider]", + "OracleSet::preflight: checked for length", + "OracleSet::preclaim: checked for consistency with ledger" + ], + "origin": "ctx.tx", + "transformations": [ + "Length checked against maxOracleProvider", + "Compared to on-ledger value if present" + ], + "validated_at": "OracleSet::preflight, OracleSet::preclaim" + }, + { + "field": "sfURI", + "flow": [ + "ctx.tx[sfURI]", + "OracleSet::preflight: checked for length" + ], + "origin": "ctx.tx", + "transformations": [ + "Length checked against maxOracleURI" + ], + "validated_at": "OracleSet::preflight" + }, + { + "field": "sfAssetClass", + "flow": [ + "ctx.tx[sfAssetClass]", + "OracleSet::preflight: checked for length", + "OracleSet::preclaim: checked for consistency with ledger" + ], + "origin": "ctx.tx", + "transformations": [ + "Length checked against maxOracleSymbolClass", + "Compared to on-ledger value if present" + ], + "validated_at": "OracleSet::preflight, OracleSet::preclaim" + }, + { + "field": "sfLastUpdateTime", + "flow": [ + "ctx.tx[sfLastUpdateTime]", + "OracleSet::preclaim: checked for epoch offset, range, and recency" + ], + "origin": "ctx.tx", + "transformations": [ + "Checked for being after epoch_offset", + "Checked for being within maxLastUpdateTimeDelta of closeTime", + "Compared to previous on-ledger value" + ], + "validated_at": "OracleSet::preclaim" + }, + { + "field": "sfBaseAsset / sfQuoteAsset", + "flow": [ + "entry[sfBaseAsset], entry[sfQuoteAsset]", + "OracleSet::preclaim: checked for equality (must differ), used to form tokenPairKey" + ], + "origin": "Each entry in sfPriceDataSeries", + "transformations": [ + "Compared for equality", + "Used as key for uniqueness" + ], + "validated_at": "OracleSet::preclaim" + }, + { + "field": "sfScale", + "flow": [ + "entry[~sfScale]", + "OracleSet::preclaim: checked for exceeding maxPriceScale" + ], + "origin": "Each entry in sfPriceDataSeries", + "transformations": [ + "Checked for value > maxPriceScale" + ], + "validated_at": "OracleSet::preclaim" + }, + { + "field": "sfAssetPrice", + "flow": [ + "entry.isFieldPresent(sfAssetPrice)", + "OracleSet::preclaim: determines if pair is added/updated or deleted" + ], + "origin": "Each entry in sfPriceDataSeries", + "transformations": [ + "Presence determines add/update/delete logic" + ], + "validated_at": "OracleSet::preclaim" + } + ], + "description": "Implements the OracleSet transaction logic for creating and updating oracle price data objects in the XRPL ledger, including preflight, preclaim, and apply logic for validation and ledger mutation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPriceDataSeries", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (dataSeries.empty()) at OracleSet::preflight", + "issue_pattern": "Missing empty string validation for sfPriceDataSeries", + "why_false_positive": "explicit check (dataSeries.empty()) validates sfPriceDataSeries for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPriceDataSeries", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (dataSeries.size() > maxOracleDataSeries) at OracleSet::preflight", + "issue_pattern": "Missing empty string validation for sfPriceDataSeries", + "why_false_positive": "explicit check (dataSeries.size() > maxOracleDataSeries) validates sfPriceDataSeries for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfPriceDataSeries", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (dataSeries.size() > maxOracleDataSeries) at OracleSet::preflight", + "issue_pattern": "Missing range validation for sfPriceDataSeries", + "why_false_positive": "explicit check (dataSeries.size() > maxOracleDataSeries) validates sfPriceDataSeries range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfProvider", + "empty", + "string", + "validation" + ], + "evidence": "isInvalidLength lambda at OracleSet::preflight", + "issue_pattern": "Missing empty string validation for sfProvider", + "why_false_positive": "isInvalidLength lambda validates sfProvider for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfProvider", + "range", + "bounds", + "validation" + ], + "evidence": "isInvalidLength lambda at OracleSet::preflight", + "issue_pattern": "Missing range validation for sfProvider", + "why_false_positive": "isInvalidLength lambda validates sfProvider range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfURI", + "empty", + "string", + "validation" + ], + "evidence": "isInvalidLength lambda at OracleSet::preflight", + "issue_pattern": "Missing empty string validation for sfURI", + "why_false_positive": "isInvalidLength lambda validates sfURI for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfURI", + "range", + "bounds", + "validation" + ], + "evidence": "isInvalidLength lambda at OracleSet::preflight", + "issue_pattern": "Missing range validation for sfURI", + "why_false_positive": "isInvalidLength lambda validates sfURI range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAssetClass", + "empty", + "string", + "validation" + ], + "evidence": "isInvalidLength lambda at OracleSet::preflight", + "issue_pattern": "Missing empty string validation for sfAssetClass", + "why_false_positive": "isInvalidLength lambda validates sfAssetClass for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAssetClass", + "range", + "bounds", + "validation" + ], + "evidence": "isInvalidLength lambda at OracleSet::preflight", + "issue_pattern": "Missing range validation for sfAssetClass", + "why_false_positive": "isInvalidLength lambda validates sfAssetClass range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::account(...)) at OracleSet::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "ctx.view.read(keylet::account(...)) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLastUpdateTime", + "empty", + "string", + "validation" + ], + "evidence": "explicit range checks at OracleSet::preclaim", + "issue_pattern": "Missing empty string validation for sfLastUpdateTime", + "why_false_positive": "explicit range checks validates sfLastUpdateTime for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfLastUpdateTime", + "range", + "bounds", + "validation" + ], + "evidence": "explicit range checks at OracleSet::preclaim", + "issue_pattern": "Missing range validation for sfLastUpdateTime", + "why_false_positive": "explicit range checks validates sfLastUpdateTime range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBaseAsset and sfQuoteAsset (in sfPriceDataSeries)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (entry[sfBaseAsset] == entry[sfQuoteAsset]) at OracleSet::preclaim (inside for loop over sfPriceDataSeries)", + "issue_pattern": "Missing empty string validation for sfBaseAsset and sfQuoteAsset (in sfPriceDataSeries)", + "why_false_positive": "explicit check (entry[sfBaseAsset] == entry[sfQuoteAsset]) validates sfBaseAsset and sfQuoteAsset (in sfPriceDataSeries) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "token pair uniqueness in sfPriceDataSeries", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (pairs.contains(key) || pairsDel.contains(key)) at OracleSet::preclaim (inside for loop over sfPriceDataSeries)", + "issue_pattern": "Missing empty string validation for token pair uniqueness in sfPriceDataSeries", + "why_false_positive": "explicit check (pairs.contains(key) || pairsDel.contains(key)) validates token pair uniqueness in sfPriceDataSeries for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/oracle/OracleSet.cpp", + "functions": [ + { + "args": [ + "pair" + ], + "lineno": 9, + "name": "tokenPairKey" + }, + { + "args": [ + "ctx" + ], + "lineno": 15, + "name": "OracleSet::preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 32, + "name": "OracleSet::preclaim" + }, + { + "args": [ + "ctx", + "count" + ], + "lineno": 109, + "name": "adjustOwnerCount" + }, + { + "args": [ + "obj" + ], + "lineno": 119, + "name": "setPriceDataInnerObjTemplate" + }, + { + "args": [], + "lineno": 126, + "name": "OracleSet::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "OracleSet transaction logic is likely tested in integration/functional test files such as 'test/tx/OracleSet_test.cpp', 'test/tx/Oracle_test.cpp', or similar. Unit tests may exist for malformed transactions, field length violations, time window violations, duplicate/invalid pairs, and missing required fields. Gaps may include edge cases for maximum allowed values, simultaneous field errors, and rare ledger state transitions. Some error codes (e.g., tecINTERNAL, LCOV_EXCL_LINE) may not be directly tested. Coverage for all validation branches should be confirmed in the test suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in C++ (no external validation framework)", + "validation_layer": "business_logic (preflight and preclaim transaction hooks)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temARRAY_EMPTY", + "field": "sfPriceDataSeries", + "location": "OracleSet::preflight", + "validated_by": "explicit check (dataSeries.empty())", + "validates": [ + "array must not be empty" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temARRAY_TOO_LARGE", + "field": "sfPriceDataSeries", + "location": "OracleSet::preflight", + "validated_by": "explicit check (dataSeries.size() > maxOracleDataSeries)", + "validates": [ + "array must not exceed maxOracleDataSeries" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfProvider", + "location": "OracleSet::preflight", + "validated_by": "isInvalidLength lambda", + "validates": [ + "length must be > 0 and <= maxOracleProvider if present" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfURI", + "location": "OracleSet::preflight", + "validated_by": "isInvalidLength lambda", + "validates": [ + "length must be > 0 and <= maxOracleURI if present" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAssetClass", + "location": "OracleSet::preflight", + "validated_by": "isInvalidLength lambda", + "validates": [ + "length must be > 0 and <= maxOracleSymbolClass if present" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_ACCOUNT", + "field": "sfAccount", + "location": "OracleSet::preclaim", + "validated_by": "ctx.view.read(keylet::account(...))", + "validates": [ + "account must exist" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINVALID_UPDATE_TIME or tecINTERNAL", + "field": "sfLastUpdateTime", + "location": "OracleSet::preclaim", + "validated_by": "explicit range checks", + "validates": [ + "must be >= epoch_offset", + "must be within maxLastUpdateTimeDelta seconds of closeTime" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfBaseAsset and sfQuoteAsset (in sfPriceDataSeries)", + "location": "OracleSet::preclaim (inside for loop over sfPriceDataSeries)", + "validated_by": "explicit check (entry[sfBaseAsset] == entry[sfQuoteAsset])", + "validates": [ + "base asset and quote asset must not be the same" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "token pair uniqueness in sfPriceDataSeries", + "location": "OracleSet::preclaim (inside for loop over sfPriceDataSeries)", + "validated_by": "explicit check (pairs.contains(key) || pairsDel.contains(key))", + "validates": [ + "no duplicate token pairs in add/update/delete sets" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/oracle/OracleSet.cpp.ai.md b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp.ai.md new file mode 100644 index 0000000000..af52a26b7c --- /dev/null +++ b/src/libxrpl/tx/transactors/oracle/OracleSet.cpp.ai.md @@ -0,0 +1,44 @@ +# OracleSet.cpp — Price Oracle Create/Update Transactor + +`OracleSet.cpp` implements the `OracleSet` transaction for the XRPL Price Oracle feature (XLS-47d). Its purpose is to create or update an on-ledger `ltORACLE` object that stores off-chain price data — base/quote currency pairs, their prices, and an optional scaling factor — bridging external market data into decentralized applications built on the ledger. It follows the standard three-phase transactor lifecycle: `preflight` (stateless validation), `preclaim` (stateful validation), and `doApply` (ledger mutation). + +## Transaction Phases + +### `preflight` — Stateless Validation + +The first gate operates entirely on the raw transaction bytes before any ledger state is consulted. It rejects transactions with an empty `sfPriceDataSeries` array (`temARRAY_EMPTY`) or one exceeding `maxOracleDataSeries` (10 entries, `temARRAY_TOO_LARGE`). Variable-length string fields are validated through the `isInvalidLength` lambda: `sfProvider` and `sfURI` are bounded at 256 bytes, and `sfAssetClass` at 16 bytes. Empty strings are also rejected, since the lambda checks for zero length in addition to overflow. These checks use `tem`-class codes, meaning they fail the transaction before it can affect any account's sequence number or fee balance. + +### `preclaim` — Stateful Validation + +The second phase reads ledger state and enforces business rules. It handles two fundamentally different scenarios — creation and update — distinguished by whether a `keylet::oracle(account, documentID)` object already exists on the ledger. + +**Time window enforcement.** The `sfLastUpdateTime` field carries time in XRPL's own epoch (seconds since January 1, 2000, i.e. `epoch_offset = 946684800`). The code subtracts this offset to obtain a Unix timestamp, then compares it against the ledger's `closeTime`. The timestamp must fall within `maxLastUpdateTimeDelta` (300 seconds) of close time in both directions. This tight window ensures oracle data is fresh and prevents backdating or future-dating of price feeds. The `epoch_offset` subtraction is not just a conversion detail — it's also a safety guard: any `sfLastUpdateTime` value smaller than `epoch_offset.count()` signals a malformed submission and returns `tecINVALID_UPDATE_TIME` immediately. + +**Token pair classification.** The code walks the `sfPriceDataSeries` array, classifying each entry into two `std::set` collections: `pairs` (entries that include `sfAssetPrice` and should be created or updated) and `pairsDel` (entries without a price, which signal deletion). The `tokenPairKey()` helper extracts the `(Currency, Currency)` pair as a `std::pair` map key. Entries with identical base and quote assets (`entry[sfBaseAsset] == entry[sfQuoteAsset]`) are rejected as `temMALFORMED`, as are duplicate keys — the uniqueness check using `pairs.contains(key) || pairsDel.contains(key)` detects the case where two entries in the same transaction would target the same trading pair. The `sfScale` field is optional but capped at `maxPriceScale` (20) when present. + +**Create vs. update divergence.** For a new oracle, `sfProvider` and `sfAssetClass` must both be present; they are required metadata that cannot change after creation. For an update, the `isConsistent` lambda verifies that any provided `sfProvider` or `sfAssetClass` matches the existing on-ledger value — these fields are effectively immutable after creation. Additionally, the update path enforces time monotonicity: the new `sfLastUpdateTime` must be strictly greater than the previous one, preventing clock rollback attacks. The update path also reconciles the `pairsDel` set against existing pairs: after merging existing pairs into `pairs` and removing any that match `pairsDel`, any non-empty `pairsDel` indicates a deletion of a pair that doesn't exist on the ledger, returning `tecTOKEN_PAIR_NOT_FOUND`. + +**Reserve accounting.** Oracle objects consume either 1 or 2 owner reserve units depending on whether the total number of tracked pairs exceeds 5. This step function (`count > 5 ? 2 : 1`) reflects the larger serialized size of bigger oracle objects. The `adjustReserve` variable captures the delta between old and new reserve counts so the reserve check uses a projected post-transaction balance. A balance below the projected reserve returns `tecINSUFFICIENT_RESERVE`. + +### `doApply` — Ledger Mutation + +**Update path.** The update path builds a `std::map, STObject>` from the existing SLE's `sfPriceDataSeries` to allow efficient keyed lookup. Each entry in the transaction then either deletes the pair (by erasing from the map), updates its price and optional scale (by mutating the existing map value), or adds a new pair. The `populatePriceData` lambda handles construction of new `STObject` entries with their inner object template applied. After the merge, the map is converted back to an `STArray` and written into the SLE. URI may be updated at this time; `sfProvider` and `sfAssetClass` cannot be changed (enforced in `preclaim`). The `sfLastUpdateTime` is always refreshed. + +A notable repair: if the existing SLE is missing `sfOracleDocumentID` and the `fixIncludeKeyletFields` amendment is active, the field is backfilled. This is a forward-compatibility fixup — older oracle objects created before the amendment lack this field, and the update is the only safe opportunity to embed it. + +**Create path.** A new SLE is allocated and populated with owner, provider, optional URI, asset class, update time, and the price data series. Under the `fixPriceOracleOrder` amendment, the transaction's `sfPriceDataSeries` entries are first inserted into the same sorted `std::map` before being serialized into the SLE, guaranteeing canonical on-ledger ordering regardless of submission order. Without the amendment, the raw transaction array order is preserved — this is the pre-fix behavior retained for ledger history consistency. The object is then inserted into the owner's directory via `dirInsert`, and the owner count is incremented using the same 1-or-2 reserve step function. + +## Helper Functions + +`tokenPairKey()` provides a consistent `(Currency, Currency)` comparison key used in both `preclaim` and `doApply`. Factoring it out avoids subtle bugs where the key extraction logic diverges between validation and application. + +`setPriceDataInnerObjTemplate()` looks up the `SOTemplate` for `sfPriceData` inner objects from `InnerObjectFormats` and applies it to a freshly constructed `STObject`. This is necessary because XRPL's serialization layer requires each inner object to declare its canonical field set before fields can be set on it — without the template, field operations would behave unpredictably. + +The file-scope `adjustOwnerCount()` overload is a thin wrapper that peeks the account SLE and delegates to the ledger helper, returning `false` only in the dead-code path where the account has disappeared between `preclaim` and `doApply` (hence the `LCOV_EXCL_LINE` annotation, since this is guarded by `preclaim`'s `terNO_ACCOUNT` check). + +## Amendment-Gated Behavior Summary + +Two amendments modify the behavior of `OracleSet`: + +- **`fixPriceOracleOrder`**: On creation, sorts `sfPriceDataSeries` entries into a canonical (lexicographic by currency code) order. Without this fix, insertion order was non-deterministic across validator nodes if clients submitted entries in varying orders. +- **`fixIncludeKeyletFields`**: Embeds `sfOracleDocumentID` directly into the SLE so the object is self-describing. Before this fix, callers reconstructing the keylet had to supply the document ID from external context; with it, the field can be read directly from the SLE during traversal or RPC lookups. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp.ai.json b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp.ai.json new file mode 100644 index 0000000000..e7ffd12598 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp.ai.json @@ -0,0 +1,332 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 18, + "name": "ctx" + }, + { + "lineno": 61, + "name": "ctx" + }, + { + "lineno": 181, + "name": "view" + }, + { + "lineno": 181, + "name": "preauthIndex" + }, + { + "lineno": 181, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "DepositPreauth::preflight", + "credentials::checkArray (if credentials fields present)", + "DepositPreauth::checkExtraFeatures (may be called elsewhere, e.g., feature gating)" + ], + "entry_point": "DepositPreauth::preflight", + "purpose": "Performs initial transaction validation: checks field presence, mutual exclusivity, account validity, and credentials array structure.", + "validation_points": [ + "preflight: Checks that exactly one of sfAuthorize, sfUnauthorize, sfAuthorizeCredentials, sfUnauthorizeCredentials is present.", + "preflight: Validates account IDs (not zero, not self-preauth).", + "preflight: Calls credentials::checkArray for credentials arrays.", + "checkExtraFeatures: Ensures credentials fields are only allowed if featureCredentials is enabled." + ] + }, + { + "call_chain": [ + "DepositPreauth::preclaim" + ], + "entry_point": "DepositPreauth::preclaim", + "purpose": "Performs ledger state validation: checks existence/non-existence of accounts and preauth entries.", + "validation_points": [ + "preclaim: Checks that target account exists for sfAuthorize.", + "preclaim: Checks for duplicate or missing preauth entries.", + "preclaim: For credentials arrays, checks issuer existence and uniqueness." + ] + }, + { + "call_chain": [ + "DepositPreauth::doApply", + "removeFromLedger (if unauthorizing)" + ], + "entry_point": "DepositPreauth::doApply", + "purpose": "Applies the transaction to the ledger, after all validations.", + "validation_points": [ + "doApply: Assumes preflight and preclaim have already validated input." + ] + } + ], + "data_flows": [ + { + "field": "sfAuthorize", + "flow": [ + "ctx.tx[~sfAuthorize] (optional field extraction in preflight)", + "Checked for presence and value in preflight", + "Used in preclaim to check ledger state", + "Used in doApply to update ledger" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Checked for presence (has_value)", + "Compared to sfAccount to prevent self-preauth" + ], + "validated_at": "preflight (presence, value, not self), preclaim (account exists, not duplicate)" + }, + { + "field": "sfUnauthorize", + "flow": [ + "ctx.tx[~sfUnauthorize] (optional field extraction in preflight)", + "Checked for presence and value in preflight", + "Used in preclaim to check ledger state", + "Used in doApply to update ledger" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Checked for presence (has_value)" + ], + "validated_at": "preflight (presence, value), preclaim (entry exists)" + }, + { + "field": "sfAuthorizeCredentials", + "flow": [ + "ctx.tx.isFieldPresent(sfAuthorizeCredentials) (preflight, checkExtraFeatures)", + "ctx.tx.getFieldArray(sfAuthorizeCredentials) (preflight, preclaim)", + "credentials::checkArray (preflight)", + "DepositPreauth::checkExtraFeatures (feature gating)", + "Used in preclaim for issuer checks" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Checked for presence", + "Validated as array (size, structure)", + "Each element's issuer checked for existence" + ], + "validated_at": "preflight (array structure), checkExtraFeatures (feature gating), preclaim (issuer existence, uniqueness)" + }, + { + "field": "sfUnauthorizeCredentials", + "flow": [ + "ctx.tx.isFieldPresent(sfUnauthorizeCredentials) (preflight, checkExtraFeatures)", + "ctx.tx.getFieldArray(sfUnauthorizeCredentials) (preflight, preclaim)", + "credentials::checkArray (preflight)", + "DepositPreauth::checkExtraFeatures (feature gating)", + "Used in preclaim for issuer checks" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Checked for presence", + "Validated as array (size, structure)", + "Each element's issuer checked for existence" + ], + "validated_at": "preflight (array structure), checkExtraFeatures (feature gating), preclaim (issuer existence, uniqueness)" + } + ], + "description": "Implements the DepositPreauth transaction logic for the XRPL ledger, including preflight, preclaim, application, and removal of DepositPreauth entries.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAuthorize, sfUnauthorize, sfAuthorizeCredentials, sfUnauthorizeCredentials", + "empty", + "string", + "validation" + ], + "evidence": "explicit logic (field presence check) at DepositPreauth::preflight", + "issue_pattern": "Missing empty string validation for sfAuthorize, sfUnauthorize, sfAuthorizeCredentials, sfUnauthorizeCredentials", + "why_false_positive": "explicit logic (field presence check) validates sfAuthorize, sfUnauthorize, sfAuthorizeCredentials, sfUnauthorizeCredentials for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAuthorize, sfUnauthorize", + "empty", + "string", + "validation" + ], + "evidence": "explicit logic (has_value check) at DepositPreauth::preflight", + "issue_pattern": "Missing empty string validation for sfAuthorize, sfUnauthorize", + "why_false_positive": "explicit logic (has_value check) validates sfAuthorize, sfUnauthorize for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAuthorize", + "empty", + "string", + "validation" + ], + "evidence": "explicit logic (equality check) at DepositPreauth::preflight", + "issue_pattern": "Missing empty string validation for sfAuthorize", + "why_false_positive": "explicit logic (equality check) validates sfAuthorize for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "sfAuthorizeCredentials, sfUnauthorizeCredentials", + "empty", + "string", + "validation" + ], + "evidence": "credentials::checkArray at DepositPreauth::preflight", + "issue_pattern": "Missing empty string validation for sfAuthorizeCredentials, sfUnauthorizeCredentials", + "why_false_positive": "credentials::checkArray validates sfAuthorizeCredentials, sfUnauthorizeCredentials for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "sfAuthorizeCredentials, sfUnauthorizeCredentials", + "empty", + "string", + "validation" + ], + "evidence": "DepositPreauth::checkExtraFeatures at DepositPreauth::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfAuthorizeCredentials, sfUnauthorizeCredentials", + "why_false_positive": "DepositPreauth::checkExtraFeatures validates sfAuthorizeCredentials, sfUnauthorizeCredentials for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp", + "functions": [ + { + "args": [ + "ctx" + ], + "lineno": 11, + "name": "checkExtraFeatures" + }, + { + "args": [ + "ctx" + ], + "lineno": 18, + "name": "preflight" + }, + { + "args": [ + "ctx" + ], + "lineno": 61, + "name": "preclaim" + }, + { + "args": [], + "lineno": 104, + "name": "doApply" + }, + { + "args": [ + "view", + "preauthIndex", + "j" + ], + "lineno": 181, + "name": "removeFromLedger" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "DepositPreauth is typically tested in unit/integration tests under the transaction processing suite, e.g., 'test/tx/DepositPreauth_test.cpp' or similar. Tests likely cover: valid/invalid field combinations, self-preauth, zero account, credentials array size/structure, feature gating, and ledger state checks (duplicate, missing entries). Gaps may exist for edge cases in credentials array validation (e.g., duplicate issuers, malformed elements), and for feature gating if featureCredentials is toggled. Tests for checkExtraFeatures may be indirect unless feature toggling is explicitly tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl custom validation (no external framework detected)", + "validation_layer": "business_logic (preflight/preclaim methods in transactor)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAuthorize, sfUnauthorize, sfAuthorizeCredentials, sfUnauthorizeCredentials", + "location": "DepositPreauth::preflight", + "validated_by": "explicit logic (field presence check)", + "validates": [ + "Exactly one of the four fields (sfAuthorize, sfUnauthorize, sfAuthorizeCredentials, sfUnauthorizeCredentials) must be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_ACCOUNT_ID", + "field": "sfAuthorize, sfUnauthorize", + "location": "DepositPreauth::preflight", + "validated_by": "explicit logic (has_value check)", + "validates": [ + "If sfAuthorize or sfUnauthorize is present, the value must be a valid (non-zero) AccountID" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temCANNOT_PREAUTH_SELF", + "field": "sfAuthorize", + "location": "DepositPreauth::preflight", + "validated_by": "explicit logic (equality check)", + "validates": [ + "An account cannot preauthorize itself (sfAuthorize must not equal sfAccount)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.95, + "error_thrown": "various (depends on credentials::checkArray, but not tesSUCCESS)", + "field": "sfAuthorizeCredentials, sfUnauthorizeCredentials", + "location": "DepositPreauth::preflight", + "validated_by": "credentials::checkArray", + "validates": [ + "Credential array must pass credentials::checkArray (likely checks size, format, and content of array)" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 0.95, + "error_thrown": "implicit (feature not enabled, transaction not allowed)", + "field": "sfAuthorizeCredentials, sfUnauthorizeCredentials", + "location": "DepositPreauth::checkExtraFeatures", + "validated_by": "DepositPreauth::checkExtraFeatures", + "validates": [ + "If credential arrays are present, featureCredentials must be enabled" + ], + "validation_type": "business_logic|feature_flag" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp.ai.md b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp.ai.md new file mode 100644 index 0000000000..30b6c6e3db --- /dev/null +++ b/src/libxrpl/tx/transactors/payment/DepositPreauth.cpp.ai.md @@ -0,0 +1,48 @@ +# `DepositPreauth.cpp` — Transactor for Deposit Pre-Authorization + +## Purpose and Context + +XRPL accounts can enable the *Deposit Authorization* flag, which causes the ledger to reject incoming payments from unrecognized senders. `DepositPreauth.cpp` implements the `DepositPreauth` transaction type, whose sole job is to manage the whitelist entries that let other accounts (or holders of specific credential types) bypass that gate. Without this transactor, there would be no way to build or dismantle the allow-list that Deposit Authorization depends on. + +The file is one of two transactors in the `payment/` subdirectory (alongside `Payment.cpp`) and inherits from `Transactor`, following the standard three-stage XRPL processing pipeline: `preflight` → `preclaim` → `doApply`. + +## Four Modes in One Transaction Type + +A single `DepositPreauth` transaction can do exactly one of four things, selected by which field is present in the transaction: + +| Field | Effect | +|---|---| +| `sfAuthorize` | Create a preauth entry granting a specific `AccountID` | +| `sfUnauthorize` | Remove a previously granted `AccountID` preauth entry | +| `sfAuthorizeCredentials` | Create a preauth entry for a credential-type set | +| `sfUnauthorizeCredentials` | Remove a credential-type set preauth entry | + +The credentials-based path (`sfAuthorizeCredentials` / `sfUnauthorizeCredentials`) is an amendment-gated extension: `checkExtraFeatures()` returns `false` (causing the transaction to be rejected before `preflight` is even called) if either credentials field is present but the `featureCredentials` amendment is not enabled. This is the standard XRPL mechanism for safely introducing new transaction fields in a backward-compatible way. + +## Validation Pipeline + +**`preflight`** is a static, ledger-free check that validates structure and field semantics. Its first and most important check enforces the "exactly one of four" constraint: it counts how many of the four allowed fields are present and returns `temMALFORMED` if that count is not exactly one. This approach — converting boolean presence flags to integers and summing them — is compact and explicit. + +For the account-based path, `preflight` further validates that the target `AccountID` is non-zero (`temINVALID_ACCOUNT_ID`) and that an account is not pre-authorizing itself (`temCANNOT_PREAUTH_SELF`). The self-authorization check only applies to `sfAuthorize`; `sfUnauthorize` does not need it because a self-authorizing entry could never have been created in the first place. + +For the credentials path, `preflight` delegates to `credentials::checkArray()`, which validates array size against `maxCredentialsArraySize` and verifies the structure of each credential element. + +**`preclaim`** runs against a read-only ledger snapshot and checks runtime state: whether target accounts exist (`tecNO_TARGET`, `tecNO_ISSUER`), whether an entry being created is a duplicate (`tecDUPLICATE`), and whether an entry being removed actually exists (`tecNO_ENTRY`). The credential-based duplicate check requires building a `std::set>` sorted by `(issuer, credentialType)`, which serves double duty: it catches duplicates within the array itself (returning `tefINTERNAL` if `emplace` fails to insert, though that path is guarded as `LCOV_EXCL_LINE`) and produces the canonical ordering needed to derive the deterministic ledger key. + +## Apply Logic and Reserve Accounting + +`doApply()` handles all four branches. The authorization branches (both account and credential) share a common pattern: first, they check that the account's *pre-fee balance* (`preFeeBalance_`, captured before fee deduction) covers the reserve required for one additional owned object. Using the pre-fee balance here is deliberate — it allows an account to dip into its reserve to pay the transaction fee while still being able to create the preauth entry, matching the behavior of other object-creating transactions throughout the ledger. + +After the reserve check, the new `SLE` is populated and inserted into the ledger. The entry is also added to the account's owner directory via `view().dirInsert(keylet::ownerDir(account_), ...)`, and `sfOwnerNode` is written back onto the `SLE` so that removal can locate the directory page in O(1) without a linear scan. `adjustOwnerCount` is called to increment the owner's reserve obligation. + +For the credentials path, the raw `STArray` from the transaction is re-sorted into a canonical `std::set` by `credentials::makeSorted()`, then reconstructed as an `STArray` for storage on the ledger entry. This ensures the ledger object's `sfAuthorizeCredentials` array always has a deterministic order regardless of how the submitter ordered the elements, which is required for the keylet derivation to be consistent. + +## `removeFromLedger` — Shared Removal Utility + +The `removeFromLedger(ApplyView&, uint256 const&, beast::Journal)` static method is deliberately not scoped to a particular transaction branch. Both the `sfUnauthorize` path and the `sfUnauthorizeCredentials` path call it with a pre-computed ledger key. More importantly, it is also called by the `AccountDelete` transactor when cleaning up owned objects before deleting an account — that's why the comment notes "Existence already checked in preclaim and AccountDelete." The method removes the entry from the owner directory, decrements `sfOwnerCount`, and erases the `SLE`. Failure to find the entry logs at `warn` and returns `tecNO_ENTRY`; an inconsistent directory removal (which should never happen on a healthy ledger) logs at `fatal` and returns `tefBAD_LEDGER`. + +## Design Observations + +The separation of `removeFromLedger` as a public static method rather than an internal helper reflects a real dependency: `AccountDelete` must be able to clean up preauth objects without going through the full transaction pipeline. This kind of cross-transactor utility is a recognized pattern in the XRPL codebase for objects that can be deleted both explicitly and as part of account cleanup. + +The credentials sorting strategy — building a sorted set at both `preclaim` time (for the keylet lookup) and `doApply` time (for the stored array) — trades a small amount of repeated work for correctness: the ledger entry's key is derived from the sorted representation, so any mismatch between what was checked in `preclaim` and what is stored in `doApply` would silently create a bad entry. By using `credentials::makeSorted()` consistently in both phases, the code avoids that class of bug. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp.ai.json b/src/libxrpl/tx/transactors/payment/Payment.cpp.ai.json new file mode 100644 index 0000000000..b6579f8ce9 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp.ai.json @@ -0,0 +1,676 @@ +{ + "args": [ + { + "lineno": 14, + "name": "ctx" + }, + { + "lineno": 15, + "name": "tx" + }, + { + "lineno": 23, + "name": "account" + }, + { + "lineno": 23, + "name": "dstAmount" + }, + { + "lineno": 23, + "name": "sendMax" + }, + { + "lineno": 154, + "name": "view" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Payment::preflight", + "getMaxSourceAmount", + "STAmount::holds", + "ctx.rules.enabled", + "tx.isFieldPresent", + "tx.getFieldAmount", + "tx.getAccountID" + ], + "entry_point": "Payment::preflight", + "purpose": "Performs preflight validation on Payment transactions, checking feature flags, field presence, type correctness, and consistency between fields.", + "validation_points": [ + "Payment::preflight (feature flag checks, type checks, field presence checks, consistency checks)", + "getMaxSourceAmount (type and presence checks)" + ] + }, + { + "call_chain": [ + "Payment::checkExtraFeatures", + "ctx.tx.isFieldPresent", + "ctx.rules.enabled" + ], + "entry_point": "Payment::checkExtraFeatures", + "purpose": "Validates that extra fields (sfCredentialIDs, sfDomainID) are only present if the corresponding feature is enabled.", + "validation_points": [ + "Payment::checkExtraFeatures (feature flag checks for extra fields)" + ] + }, + { + "call_chain": [ + "Payment::doApply", + "Payment::preflight", + "Payment::preclaim", + "Payment::checkPermission" + ], + "entry_point": "Payment::doApply", + "purpose": "Applies the Payment transaction, but only after passing preflight, preclaim, and permission checks.", + "validation_points": [ + "Payment::preflight", + "Payment::preclaim", + "Payment::checkPermission" + ] + } + ], + "data_flows": [ + { + "field": "sfAmount", + "flow": [ + "ctx.tx (input)", + "tx.getFieldAmount(sfAmount)", + "STAmount dstAmount", + "dstAmount.holds()", + "getMaxSourceAmount", + "Validation in preflight" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Type checked via holds()", + "Used to construct STAmount", + "Compared for asset consistency" + ], + "validated_at": "Payment::preflight (type and consistency checks)" + }, + { + "field": "sfSendMax", + "flow": [ + "ctx.tx (input)", + "tx.isFieldPresent(sfSendMax)", + "tx[~sfSendMax]", + "getMaxSourceAmount" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Presence checked", + "Used as max source amount if present" + ], + "validated_at": "Payment::preflight (presence and consistency checks)" + }, + { + "field": "sfCredentialIDs", + "flow": [ + "ctx.tx (input)", + "tx.isFieldPresent(sfCredentialIDs)", + "Payment::checkExtraFeatures" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Presence checked", + "Feature flag checked" + ], + "validated_at": "Payment::checkExtraFeatures" + }, + { + "field": "sfDomainID", + "flow": [ + "ctx.tx (input)", + "tx.isFieldPresent(sfDomainID)", + "Payment::checkExtraFeatures" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Presence checked", + "Feature flag checked" + ], + "validated_at": "Payment::checkExtraFeatures" + }, + { + "field": "sfPaths", + "flow": [ + "ctx.tx (input)", + "tx.isFieldPresent(sfPaths)", + "Payment::preflight" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Presence checked", + "Used to determine if path-based payment" + ], + "validated_at": "Payment::preflight (malformed if present with MPTIssue and not MPTokensV2)" + } + ], + "description": "Implements the Payment transaction logic for the XRPL ledger, including preflight checks, permission checks, preclaim logic, and application of payments, supporting both XRP and issued tokens, as well as advanced features like multi-path payments, delegated payments, and permissioned DEX.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAmount validated by xrpl protocol validation (jss::, STAmount, Issue, feature flags)", + "issue_pattern": "Missing validation for sfAmount", + "why_false_positive": "xrpl protocol validation (jss::, STAmount, Issue, feature flags) validates sfAmount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSendMax", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSendMax validated by xrpl protocol validation (jss::, STAmount, Issue, feature flags)", + "issue_pattern": "Missing validation for sfSendMax", + "why_false_positive": "xrpl protocol validation (jss::, STAmount, Issue, feature flags) validates sfSendMax automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCredentialIDs", + "validation", + "missing", + "check" + ], + "evidence": "Field sfCredentialIDs validated by xrpl protocol validation (jss::, STAmount, Issue, feature flags)", + "issue_pattern": "Missing validation for sfCredentialIDs", + "why_false_positive": "xrpl protocol validation (jss::, STAmount, Issue, feature flags) validates sfCredentialIDs automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfDomainID", + "validation", + "missing", + "check" + ], + "evidence": "Field sfDomainID validated by xrpl protocol validation (jss::, STAmount, Issue, feature flags)", + "issue_pattern": "Missing validation for sfDomainID", + "why_false_positive": "xrpl protocol validation (jss::, STAmount, Issue, feature flags) validates sfDomainID automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfPaths", + "validation", + "missing", + "check" + ], + "evidence": "Field sfPaths validated by xrpl protocol validation (jss::, STAmount, Issue, feature flags)", + "issue_pattern": "Missing validation for sfPaths", + "why_false_positive": "xrpl protocol validation (jss::, STAmount, Issue, feature flags) validates sfPaths automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCredentialIDs", + "empty", + "string", + "validation" + ], + "evidence": "feature flag check at Payment::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfCredentialIDs", + "why_false_positive": "feature flag check validates sfCredentialIDs for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "feature flag check at Payment::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "feature flag check validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount (destination amount)", + "empty", + "string", + "validation" + ], + "evidence": "type check (holds) at Payment::preflight", + "issue_pattern": "Missing empty string validation for sfAmount (destination amount)", + "why_false_positive": "type check (holds) validates sfAmount (destination amount) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount (destination amount)", + "type", + "validation", + "check" + ], + "evidence": "type check (holds) at Payment::preflight", + "issue_pattern": "Missing type validation for sfAmount (destination amount)", + "why_false_positive": "type check (holds) validates sfAmount (destination amount) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfSendMax", + "empty", + "string", + "validation" + ], + "evidence": "field presence check at Payment::makeTxConsequences", + "issue_pattern": "Missing empty string validation for sfSendMax", + "why_false_positive": "field presence check validates sfSendMax for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfAmount (native/XRP check)", + "empty", + "string", + "validation" + ], + "evidence": "native() method at Payment::makeTxConsequences", + "issue_pattern": "Missing empty string validation for sfAmount (native/XRP check)", + "why_false_positive": "native() method validates sfAmount (native/XRP check) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount (native/XRP check)", + "type", + "validation", + "check" + ], + "evidence": "native() method at Payment::makeTxConsequences", + "issue_pattern": "Missing type validation for sfAmount (native/XRP check)", + "why_false_positive": "native() method validates sfAmount (native/XRP check) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfAmount (destination amount)", + "empty", + "string", + "validation" + ], + "evidence": "holds at Payment::getFlagsMask, Payment::preflight", + "issue_pattern": "Missing empty string validation for sfAmount (destination amount)", + "why_false_positive": "holds validates sfAmount (destination amount) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount (destination amount)", + "type", + "validation", + "check" + ], + "evidence": "holds at Payment::getFlagsMask, Payment::preflight", + "issue_pattern": "Missing type validation for sfAmount (destination amount)", + "why_false_positive": "holds validates sfAmount (destination amount) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfSendMax", + "empty", + "string", + "validation" + ], + "evidence": "optional presence at getMaxSourceAmount", + "issue_pattern": "Missing empty string validation for sfSendMax", + "why_false_positive": "optional presence validates sfSendMax for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfAmount (Issue type)", + "empty", + "string", + "validation" + ], + "evidence": "native() method, Issue construction at getMaxSourceAmount", + "issue_pattern": "Missing empty string validation for sfAmount (Issue type)", + "why_false_positive": "native() method, Issue construction validates sfAmount (Issue type) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount (Issue type)", + "type", + "validation", + "check" + ], + "evidence": "native() method, Issue construction at getMaxSourceAmount", + "issue_pattern": "Missing type validation for sfAmount (Issue type)", + "why_false_positive": "native() method, Issue construction validates sfAmount (Issue type) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "sfPaths", + "empty", + "string", + "validation" + ], + "evidence": "isFieldPresent at Payment::preflight", + "issue_pattern": "Missing empty string validation for sfPaths", + "why_false_positive": "isFieldPresent validates sfPaths for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/payment/Payment.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 14, + "name": "Payment::makeTxConsequences" + }, + { + "args": [ + "AccountID const& account", + "STAmount const& dstAmount", + "std::optional const& sendMax" + ], + "lineno": 23, + "name": "getMaxSourceAmount" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 38, + "name": "Payment::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 46, + "name": "Payment::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 57, + "name": "Payment::preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 154, + "name": "Payment::checkPermission" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 191, + "name": "Payment::preclaim" + }, + { + "args": [], + "lineno": 259, + "name": "Payment::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "Payment transaction validation is typically covered in unit/integration tests under 'test/tx/Payment_test.cpp', 'test/tx/PaymentFlow_test.cpp', and possibly 'test/tx/MPTokens_test.cpp'. These tests cover standard, edge, and malformed cases, including feature flag gating, field presence, and type checks. However, new fields like sfCredentialIDs and sfDomainID may lack exhaustive negative tests for feature flag gating, and MPTokensV2-specific paths may not be fully covered if recently added. Tests for malformed combinations (e.g., inconsistent asset types, forbidden field combinations) should be reviewed for completeness.", + "validation_architecture": { + "auto_validated_fields": [ + "sfAmount", + "sfSendMax", + "sfCredentialIDs", + "sfDomainID", + "sfPaths" + ], + "framework": "xrpl protocol validation (jss::, STAmount, Issue, feature flags)", + "validation_layer": "business_logic (preflight, feature gating, type checks)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (causes temDISABLED or similar error upstream)", + "field": "sfCredentialIDs", + "location": "Payment::checkExtraFeatures", + "validated_by": "feature flag check", + "validates": [ + "Presence of sfCredentialIDs requires featureCredentials to be enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (causes temDISABLED or similar error upstream)", + "field": "sfDomainID", + "location": "Payment::checkExtraFeatures", + "validated_by": "feature flag check", + "validates": [ + "Presence of sfDomainID requires featurePermissionedDEX to be enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temDISABLED", + "field": "sfAmount (destination amount)", + "location": "Payment::preflight", + "validated_by": "type check (holds)", + "validates": [ + "If destination amount is MPTIssue, featureMPTokensV1 must be enabled" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (affects calculation, not error)", + "field": "sfSendMax", + "location": "Payment::makeTxConsequences", + "validated_by": "field presence check", + "validates": [ + "If sfSendMax is present, use it for max spend calculation; else use sfAmount" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (affects calculation, not error)", + "field": "sfAmount (native/XRP check)", + "location": "Payment::makeTxConsequences", + "validated_by": "native() method", + "validates": [ + "If amount is not native (XRP), transaction does not spend XRP" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (affects mask/logic, not error)", + "field": "sfAmount (destination amount)", + "location": "Payment::getFlagsMask, Payment::preflight", + "validated_by": "holds", + "validates": [ + "Checks if destination amount is MPTIssue for flag mask and feature gating" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (affects calculation, not error)", + "field": "sfSendMax", + "location": "getMaxSourceAmount", + "validated_by": "optional presence", + "validates": [ + "If sendMax is present, use it as max source amount; else use dstAmount" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (affects calculation, not error)", + "field": "sfAmount (Issue type)", + "location": "getMaxSourceAmount", + "validated_by": "native() method, Issue construction", + "validates": [ + "If Issue is native, use dstAmount; else construct STAmount with Issue and account" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "N/A (code incomplete, but likely error if present with MPTokensV1 off)", + "field": "sfPaths", + "location": "Payment::preflight", + "validated_by": "isFieldPresent", + "validates": [ + "If MPTokensV2 is not enabled and destination is MPT, sfPaths presence is checked" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment/Payment.cpp.ai.md b/src/libxrpl/tx/transactors/payment/Payment.cpp.ai.md new file mode 100644 index 0000000000..da0deae7b7 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment/Payment.cpp.ai.md @@ -0,0 +1,75 @@ +# `Payment.cpp` — XRP Ledger Payment Transaction Transactor + +## Role in the System + +`Payment.cpp` implements the `Payment` transaction type, the most fundamental and feature-rich transaction on the XRP Ledger. It handles three structurally distinct cases under one umbrella: direct XRP-to-XRP transfers, multi-hop IOU/token payments routed through the DEX-like path engine, and direct Multi-Purpose Token (MPT) transfers. Because of this breadth the file is the largest single transactor in the codebase, and it contains the most elaborate feature-gating logic. + +The class inherits from `Transactor`, which enforces a strict multi-phase pipeline. Each phase has a different view of the world and a different contract about what it may read or write. + +## Transaction Pipeline + +**`makeTxConsequences()`** executes before ledger state is read. Its only job is to compute the worst-case XRP spend for fee-reservation purposes: if `sfSendMax` is present and native, that amount is the ceiling; otherwise `sfAmount` is used. Non-XRP payments return `beast::zero`. + +**`checkExtraFeatures()`** gates two optional fields behind amendments: `sfCredentialIDs` requires `featureCredentials`, and `sfDomainID` requires `featurePermissionedDEX`. This is the canonical place to add similar guards for any future optional field. + +**`getFlagsMask()`** is context-sensitive. Under `MPTokensV1` without `MPTokensV2`, direct MPT payments are limited to `tfUniversal | tfPartialPayment`; all other payment modes get the full `tfPaymentMask`. This avoids exposing flags that have no meaning yet for token types that don't support path-based routing. + +**`preflight()`** performs entirely stateless structural validation. A notable design is `getMaxSourceAmount()`, a free helper that reconstructs the sender's source ceiling. When no `sfSendMax` is present and the destination asset is an IOU, the helper builds a new `STAmount` with the *sender's* account as issuer rather than the destination's issuer. This matters because IOU amounts are trust-line-scoped: the "same" asset from two different issuers is not fungible. + +`preflight()` then enforces a matrix of incompatible flag/field combinations: XRP-to-XRP payments may not carry `sfSendMax`, `sfPaths`, `tfPartialPayment`, `tfLimitQuality`, or `tfNoRippleDirect`, because those features only make sense when routing through intermediaries. MPT direct payments (pre-V2) are similarly forbidden from carrying `sfPaths`. Self-payments are rejected unless `sfPaths` is present — which would indicate an attempted arbitrage cycle, which is permitted. + +The `sfDeliverMin` field is validated to be positive, have the same asset as `sfAmount`, not exceed `sfAmount`, and only appear when `tfPartialPayment` is set. This combination forms the "at least this much must arrive" guarantee. + +**`checkPermission()`** handles delegated transactions — where a separate delegate account authorises the transaction on behalf of the source account. It first attempts coarse-grained permission via `checkTxPermission()`. If that fails it loads granular permissions and checks two specific types: `PaymentMint` (the source is the token issuer — sending tokens into existence) and `PaymentBurn` (the destination is the issuer — sending tokens back to be destroyed). Critically, granular permissions are only valid for *direct* payments: if `sfPaths` is present or `sfSendMax` names a different asset than `sfAmount`, the check immediately returns `terNO_DELEGATE_PERMISSION`. This prevents a delegate from routing funds through currency conversions it was never authorised to perform. + +**`preclaim()`** reads ledger state without modifying it. It validates: whether the destination account exists (non-native payments fail hard with `tecNO_DST` if it doesn't, while XRP payments may create the account if the amount exceeds the base reserve); whether the destination requires a `DestinationTag` and one is missing; whether the path set exceeds the hard limits (`MaxPathSize = 6` paths, `MaxPathLength = 8` hops); whether `sfCredentialIDs` entries are valid on-ledger; and, for `sfDomainID` payments, whether both source and destination are members of the specified permissioned DEX domain. + +The comment `// TODO: de-dupe` at line 323 flags a known maintenance debt — the reserve-insufficiency check for destination creation is handled slightly differently from the analogous check in `doApply()`. + +## `doApply()` — Three Execution Paths + +The routing decision that drives `doApply()` is computed on a single boolean: + +```cpp +bool const ripple = (hasPaths || sendMax || !dstAmount.native()) && (!isDstMPT || MPTokensV2); +``` + +This selects the path engine only when the payment isn't a plain XRP transfer *and* isn't a pre-V2 MPT transfer. + +### IOU / Path-Based Payments + +When `ripple` is true, `doApply()` verifies deposit pre-authorisation, constructs a `path::RippleCalc::Input` struct, then calls `RippleCalc::rippleCalculate()` inside a `PaymentSandbox`. The sandbox is a copy-on-write overlay over the current ledger view; `pv.apply(ctx_.rawView())` propagates mutations only if processing reaches that line — meaning a mid-payment failure leaves the ledger untouched. + +After `RippleCalc` returns, if the actual delivery `rc.actualAmountOut` differs from `dstAmount`, there are two cases: if `sfDeliverMin` is set and the actual amount falls short, the result is `tecPATH_PARTIAL`; otherwise `ctx_.deliver()` records the actual delivered amount for the transaction metadata. The "delivered amount" metadata field is crucial for partial payment detection by downstream consumers. + +A subtle but important policy: if `RippleCalc` returns any `ter*` retry code, it is converted to `tecPATH_DRY`. This charges the fee instead of granting a free retry. The comment explains the rationale — the overhead of running the path engine has already been incurred, and charging a fee discourages users from submitting poorly-constructed path specs. + +### Direct MPT Payments (pre-V2) + +When the destination is an `MPTIssue` and `featureMPTokensV2` is not enabled, the file handles MPT payments directly without the path engine. The logic: + +1. Validates that both accounts hold or are authorised for the MPT issuance via `requireAuth()`. +2. Calls `canTransfer()` to check any transfer restrictions on the issuance. +3. Checks deposit pre-authorisation. +4. Computes the transfer rate from `transferRate()` and multiplies by `dstAmount` to get `requiredMaxSourceAmount`. The comment acknowledges that the rounding here will change once MPT is integrated into the DEX. +5. If `tfPartialPayment` is set and the sender can't cover the full source amount, it scales down `amountDeliver` proportionally. +6. If either `requiredMaxSourceAmount > maxSourceAmount` or the scaled delivery is below `sfDeliverMin`, it returns `tecPATH_PARTIAL`. +7. Executes via `accountSend()` inside a `PaymentSandbox`. + +The freeze check (`isAnyFrozen()`) is deliberately skipped when one party is the issuer — issuers can always send to holders and holders can always return to issuers, even when frozen. + +The `fixMPTDeliveredAmount` amendment gates the `ctx_.deliver()` call for MPT partial payments. This matches the pattern used for IOU payments, where delivered amount tracking was also introduced retroactively. + +### Direct XRP Payments + +The simplest path. It checks source balance against `dstAmount + reserve`, accounting for whether the source account is also the fee payer (in delegated transactions the fee comes from the delegate, not the source). The deposit authorisation check has a special bypass (Rule 3): if the destination's balance is at or below the base reserve *and* the payment amount is also at or below the base reserve, the check is waived. This prevents an account from entering an unrecoverable state where it has set `lsfDepositAuth`, spent all its XRP, and can no longer receive funds to pay for transactions to unset the flag. + +The code guards against payments to pseudo-accounts (AMMs, vaults, etc.) via `isPseudoAccount(sleDst)`. This guard is not amendment-gated because pseudo-account status is determined by discriminator fields that are themselves amendment-gated, so the behaviour is implicitly correct across all amendment combinations. + +After the balance arithmetic, `lsfPasswordSpent` is cleared on the destination if it was set — a legacy mechanism from the original password-based account creation flow. + +## Design Observations + +The three-way dispatch inside `doApply()` rather than three separate transactor subclasses is a deliberate choice: the XRPL transaction format uses a single `ttPAYMENT` type identifier, so all payment semantics must live in one transactor. The cost is complexity in `preflight()` and `doApply()`; the benefit is that adding new payment variants (e.g., enabling MPTs in the path engine under V2) requires only local changes to the routing logic rather than a new transaction type. + +The `PaymentSandbox` pattern applied to both path-based and MPT payments — but not to direct XRP — reflects their relative complexity: XRP arithmetic is trivially reversible (two balance field writes), while path-based and MPT payments can touch many ledger objects and must appear atomic. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp.ai.json b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp.ai.json new file mode 100644 index 0000000000..12d6011fc4 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp.ai.json @@ -0,0 +1,619 @@ +{ + "args": [ + { + "lineno": 11, + "name": "ctx" + }, + { + "lineno": 16, + "name": "ctx" + }, + { + "lineno": 21, + "name": "ctx" + }, + { + "lineno": 67, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "PaymentChannelClaim::preflight" + ], + "entry_point": "PaymentChannelClaim::preflight", + "purpose": "Performs stateless validation of PaymentChannelClaim transaction fields before ledger access.", + "validation_points": [ + "sfBalance: isXRP, > 0", + "sfAmount: isXRP, > 0", + "sfBalance <= sfAmount", + "Flags: tfClose and tfRenew not both set", + "sfSignature: present with sfPublicKey and sfBalance, signature verification", + "Credential fields: credentials::checkFields" + ] + }, + { + "call_chain": [ + "PaymentChannelClaim::preclaim", + "Transactor::preclaim (if featureCredentials not enabled)", + "credentials::valid (if featureCredentials enabled)" + ], + "entry_point": "PaymentChannelClaim::preclaim", + "purpose": "Performs contextual validation (ledger state, credentials) before applying transaction.", + "validation_points": [ + "credentials::valid: credential field validation" + ] + }, + { + "call_chain": [ + "PaymentChannelClaim::doApply", + "closeChannel (if expired)", + "Field checks and signature checks" + ], + "entry_point": "PaymentChannelClaim::doApply", + "purpose": "Applies the PaymentChannelClaim transaction to the ledger, enforcing business rules and updating state.", + "validation_points": [ + "Channel existence", + "Expiration/cancelAfter checks", + "Account permissions (src/dst)", + "Signature presence and correctness", + "Balance/funds checks" + ] + } + ], + "data_flows": [ + { + "field": "sfBalance", + "flow": [ + "Transaction JSON", + "preflight: checked for isXRP, > 0", + "preflight: compared to sfAmount", + "preflight: used in signature verification", + "doApply: compared to channel balance/funds" + ], + "origin": "ctx.tx[~sfBalance] (transaction JSON)", + "transformations": [ + "Converted to XRPAmount", + "Compared to zero", + "Compared to sfAmount", + "Used in signature message" + ], + "validated_at": "preflight" + }, + { + "field": "sfAmount", + "flow": [ + "Transaction JSON", + "preflight: checked for isXRP, > 0", + "preflight: compared to sfBalance", + "preflight: used in signature verification" + ], + "origin": "ctx.tx[~sfAmount] (transaction JSON)", + "transformations": [ + "Converted to XRPAmount", + "Compared to zero", + "Compared to sfBalance" + ], + "validated_at": "preflight" + }, + { + "field": "sfSignature", + "flow": [ + "Transaction JSON", + "preflight: checked for presence", + "preflight: used in signature verification", + "doApply: checked for presence if dst" + ], + "origin": "ctx.tx[~sfSignature] (transaction JSON)", + "transformations": [ + "Used as input to verify()" + ], + "validated_at": "preflight, doApply" + }, + { + "field": "sfPublicKey", + "flow": [ + "Transaction JSON", + "preflight: checked for presence if sfSignature present", + "preflight: checked for type", + "preflight: used to construct PublicKey", + "doApply: compared to channel's public key" + ], + "origin": "ctx.tx[~sfPublicKey] (transaction JSON)", + "transformations": [ + "Type checked", + "Constructed as PublicKey" + ], + "validated_at": "preflight, doApply" + }, + { + "field": "Flags (tfClose, tfRenew)", + "flow": [ + "Transaction JSON", + "preflight: checked for both tfClose and tfRenew set" + ], + "origin": "ctx.tx.getFlags() (transaction JSON)", + "transformations": [ + "Bitmask checked" + ], + "validated_at": "preflight" + }, + { + "field": "sfCredentialIDs", + "flow": [ + "Transaction JSON", + "checkExtraFeatures: checked for featureCredentials enabled" + ], + "origin": "ctx.tx.isFieldPresent(sfCredentialIDs) (transaction JSON)", + "transformations": [ + "Presence checked" + ], + "validated_at": "checkExtraFeatures" + } + ], + "description": "Implements the PaymentChannelClaim transaction logic for the XRPL, including preflight, preclaim, and apply steps for payment channel claims.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfBalance", + "validation", + "missing", + "check" + ], + "evidence": "Field sfBalance validated by xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification", + "issue_pattern": "Missing validation for sfBalance", + "why_false_positive": "xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification validates sfBalance automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAmount validated by xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification", + "issue_pattern": "Missing validation for sfAmount", + "why_false_positive": "xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification validates sfAmount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSignature", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSignature validated by xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification", + "issue_pattern": "Missing validation for sfSignature", + "why_false_positive": "xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification validates sfSignature automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfPublicKey", + "validation", + "missing", + "check" + ], + "evidence": "Field sfPublicKey validated by xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification", + "issue_pattern": "Missing validation for sfPublicKey", + "why_false_positive": "xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification validates sfPublicKey automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCredentialIDs", + "validation", + "missing", + "check" + ], + "evidence": "Field sfCredentialIDs validated by xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification", + "issue_pattern": "Missing validation for sfCredentialIDs", + "why_false_positive": "xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification validates sfCredentialIDs automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "flags", + "validation", + "missing", + "check" + ], + "evidence": "Field flags validated by xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification", + "issue_pattern": "Missing validation for flags", + "why_false_positive": "xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification validates flags automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBalance", + "empty", + "string", + "validation" + ], + "evidence": "isXRP, *bal <= beast::zero at preflight", + "issue_pattern": "Missing empty string validation for sfBalance", + "why_false_positive": "isXRP, *bal <= beast::zero validates sfBalance for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "isXRP, *amt <= beast::zero at preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "isXRP, *amt <= beast::zero validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBalance, sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "*bal > *amt at preflight", + "issue_pattern": "Missing empty string validation for sfBalance, sfAmount", + "why_false_positive": "*bal > *amt validates sfBalance, sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Flags (tfClose, tfRenew)", + "empty", + "string", + "validation" + ], + "evidence": "flags & tfClose, flags & tfRenew at preflight", + "issue_pattern": "Missing empty string validation for Flags (tfClose, tfRenew)", + "why_false_positive": "flags & tfClose, flags & tfRenew validates Flags (tfClose, tfRenew) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSignature", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx[~sfSignature] at preflight", + "issue_pattern": "Missing empty string validation for sfSignature", + "why_false_positive": "ctx.tx[~sfSignature] validates sfSignature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPublicKey", + "empty", + "string", + "validation" + ], + "evidence": "publicKeyType(ctx.tx[sfPublicKey]) at preflight", + "issue_pattern": "Missing empty string validation for sfPublicKey", + "why_false_positive": "publicKeyType(ctx.tx[sfPublicKey]) validates sfPublicKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSignature", + "empty", + "string", + "validation" + ], + "evidence": "verify(pk, msg.slice(), *sig) at preflight", + "issue_pattern": "Missing empty string validation for sfSignature", + "why_false_positive": "verify(pk, msg.slice(), *sig) validates sfSignature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBalance, sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "reqBalance > authAmt at preflight", + "issue_pattern": "Missing empty string validation for sfBalance, sfAmount", + "why_false_positive": "reqBalance > authAmt validates sfBalance, sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfCredentialIDs", + "empty", + "string", + "validation" + ], + "evidence": "!ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials) at checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfCredentialIDs", + "why_false_positive": "!ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials) validates sfCredentialIDs for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "credentials fields", + "empty", + "string", + "validation" + ], + "evidence": "credentials::checkFields(ctx.tx, ctx.j) at preflight", + "issue_pattern": "Missing empty string validation for credentials fields", + "why_false_positive": "credentials::checkFields(ctx.tx, ctx.j) validates credentials fields for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "credentials validity", + "empty", + "string", + "validation" + ], + "evidence": "credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j) at preclaim", + "issue_pattern": "Missing empty string validation for credentials validity", + "why_false_positive": "credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j) validates credentials validity for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 11, + "name": "PaymentChannelClaim::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const&" + ], + "lineno": 16, + "name": "PaymentChannelClaim::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 21, + "name": "PaymentChannelClaim::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 67, + "name": "PaymentChannelClaim::preclaim" + }, + { + "args": [], + "lineno": 80, + "name": "PaymentChannelClaim::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "PaymentChannelClaim is typically tested in integration and unit tests under the rippled codebase, especially in files like 'test/tx/PaymentChannel_test.cpp', 'test/tx/PaymentChannelClaim_test.cpp', and possibly 'test/tx/Transactor_test.cpp'. These tests cover valid/invalid claims, signature checks, balance/amount edge cases, and flag combinations. However, gaps may exist for edge cases involving credential fields (sfCredentialIDs), feature flag transitions (featureCredentials), and malformed public keys or signatures. Some negative paths (e.g., both tfClose and tfRenew set, or missing required fields when signature is present) may not be exhaustively tested.", + "validation_architecture": { + "auto_validated_fields": [ + "sfBalance", + "sfAmount", + "sfSignature", + "sfPublicKey", + "sfCredentialIDs", + "flags" + ], + "framework": "xrpl protocol custom validation, credentials::checkFields/valid, cryptographic signature verification", + "validation_layer": "business_logic (preflight/preclaim), some feature gating" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfBalance", + "location": "preflight", + "validated_by": "isXRP, *bal <= beast::zero", + "validates": [ + "must be XRP", + "must be > 0" + ], + "validation_type": "type|range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflight", + "validated_by": "isXRP, *amt <= beast::zero", + "validates": [ + "must be XRP", + "must be > 0" + ], + "validation_type": "type|range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfBalance, sfAmount", + "location": "preflight", + "validated_by": "*bal > *amt", + "validates": [ + "balance cannot exceed amount" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "Flags (tfClose, tfRenew)", + "location": "preflight", + "validated_by": "flags & tfClose, flags & tfRenew", + "validates": [ + "tfClose and tfRenew cannot both be set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfSignature", + "location": "preflight", + "validated_by": "ctx.tx[~sfSignature]", + "validates": [ + "if signature is present, public key and balance must be present" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfPublicKey", + "location": "preflight", + "validated_by": "publicKeyType(ctx.tx[sfPublicKey])", + "validates": [ + "public key must be valid type" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_SIGNATURE", + "field": "sfSignature", + "location": "preflight", + "validated_by": "verify(pk, msg.slice(), *sig)", + "validates": [ + "signature must be valid for message" + ], + "validation_type": "cryptographic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfBalance, sfAmount", + "location": "preflight", + "validated_by": "reqBalance > authAmt", + "validates": [ + "requested balance cannot exceed authorized amount" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "N/A (returns false, used upstream)", + "field": "sfCredentialIDs", + "location": "checkExtraFeatures", + "validated_by": "!ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials)", + "validates": [ + "credential IDs only allowed if feature enabled" + ], + "validation_type": "business_logic|feature_flag" + }, + { + "confidence": 0.9, + "error_thrown": "various (depends on credentials::checkFields)", + "field": "credentials fields", + "location": "preflight", + "validated_by": "credentials::checkFields(ctx.tx, ctx.j)", + "validates": [ + "credential fields must be valid" + ], + "validation_type": "business_logic|custom" + }, + { + "confidence": 0.9, + "error_thrown": "various (depends on credentials::valid)", + "field": "credentials validity", + "location": "preclaim", + "validated_by": "credentials::valid(ctx.tx, ctx.view, ctx.tx[sfAccount], ctx.j)", + "validates": [ + "credential must be valid in context" + ], + "validation_type": "business_logic|custom" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp.ai.md b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp.ai.md new file mode 100644 index 0000000000..e4d672125b --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelClaim.cpp.ai.md @@ -0,0 +1,55 @@ +# `PaymentChannelClaim.cpp` — Settling Off-Chain XRP Payments On-Chain + +## Role in the System + +Payment channels on the XRP Ledger implement a micropayment pattern where two parties can exchange XRP off-chain at high frequency without touching the ledger for every transfer. The channel is opened once (via `PaymentChannelCreate`) with a fixed capacity, and then the sender issues incrementally larger off-chain authorizations — cryptographically signed messages telling the receiver "you may now claim up to N drops from this channel." When the receiver is ready to settle, they submit a `PaymentChannelClaim` transaction to move the net balance on-chain. This file is the implementation of that settlement step. + +`PaymentChannelClaim` handles every interacting case: the destination claiming with a signed authorization, the source reclaiming their own channel, cooperative or unilateral close requests, channel expiry enforcement, and optional credential-based deposit authorization. + +## Cumulative Balance Model + +The most important design decision to understand here is that `sfBalance` is a **cumulative total**, not a per-transaction payment amount. A `PayChan` ledger object tracks two key fields: `sfAmount` (the channel's total funded capacity) and `sfBalance` (how much has been claimed so far, monotonically increasing). When a sender authorizes a claim, they sign a message encoding the channel ID and the new cumulative total — "the receiver may now have claimed 300 XRP total from this channel." The receiver submits this signature along with the desired `sfBalance`. + +In `doApply()`, the actual XRP moved is `reqDelta = reqBalance - chanBalance`: only the incremental difference between the requested cumulative balance and the previously settled balance actually transfers. This is why the code returns `tecUNFUNDED_PAYMENT` when `reqBalance <= chanBalance` — there is nothing new to transfer, not a payment failure per se. The cumulative model also prevents replay: a previously submitted claim cannot be resubmitted for double payment because the channel balance only moves forward. + +## Validation Pipeline: Three Phases + +`PaymentChannelClaim` participates in the standard `Transactor` three-phase pipeline. + +**`checkExtraFeatures()`** is called by the Transactor framework before `preflight` as a feature-gate. It prevents `sfCredentialIDs` from appearing in the transaction if the `featureCredentials` amendment is not yet active. This is critical for forward-compatibility: new optional fields must be invisible to nodes running under older rule sets. + +**`preflight()`** performs stateless validation with no access to ledger state. The checks here are deliberately ordered and interconnected: +- Both `sfBalance` and `sfAmount`, when present, must be positive XRP (not IOU amounts) and `sfBalance` must not exceed `sfAmount`. +- `tfClose` and `tfRenew` are mutually exclusive flags — a transaction cannot simultaneously request both channel closure and expiry removal. +- If `sfSignature` is present, `sfPublicKey` and `sfBalance` must also be present; a bare signature without the other fields is malformed. +- Signature verification happens entirely in preflight using `serializePayChanAuthorization()`, which serializes the channel keylet and authorized drop amount into a canonical byte string (prefixed with `HashPrefix::paymentChannelClaim`). This is verified against the transaction-supplied public key — but **not** yet against the channel's stored key, because preflight cannot read the ledger. + +Credential field structure is validated via `credentials::checkFields()` at the end of `preflight`. + +**`preclaim()`** does one thing: if `featureCredentials` is enabled, it validates the credential objects in the current ledger view via `credentials::valid()`. This is deliberately deferred from `preflight` because reading ledger objects to check credential expiration requires state access. Per the comment in `CredentialHelpers.h`, `credentials::valid()` should only be called in `preclaim` (not `doApply`) because it does not remove expired credentials — that cleanup is delegated to `verifyDepositPreauth()` in `doApply`. + +## Apply Logic and State Transitions + +`doApply()` first locates the `ltPAYCHAN` ledger object by the channel ID in `sfChannel`. If the channel cannot be found, `tecNO_TARGET` is returned. + +**Expiry takes priority over everything else.** Before checking permissions or balances, the code checks whether the channel has reached either its absolute `sfCancelAfter` deadline or its owner-settable `sfExpiration`. If the current ledger's `parentCloseTime` has passed either threshold, `closeChannel()` is called unconditionally. This means a claim transaction can serve as the mechanism that triggers channel closure even when it would otherwise do nothing — a holder of an expired channel just needs someone to submit a transaction referencing it. + +The permission check (`txAccount != src && txAccount != dst`) enforces that only the two parties involved in the channel can interact with it. + +**Balance settlement**: When `sfBalance` is in the transaction: +- The destination must supply a signature — the check `txAccount == dst && !ctx_.tx[~sfSignature]` enforces this. A source can claim their own funds without a signature, but the destination cannot. +- If a signature is present, `ctx_.tx[sfPublicKey]` must match the `sfPublicKey` stored in the channel object. This is the doApply-side complement of the preflight signature check: preflight verified mathematical validity; doApply verifies the key belongs to this specific channel. +- After amount checks, the destination account balance is incremented by `reqDelta` and the channel's `sfBalance` is updated to the new cumulative total. +- `verifyDepositPreauth()` is called before the transfer to honor any `DepositPreauth` restrictions the destination account may have set, including cleaning up any expired credentials in the process. + +**`tfRenew` flag**: Only the source (`src == txAccount`) may remove the channel expiry. Clearing `sfExpiration` (set to `std::nullopt`) gives the source a way to retract a previously-issued close request, effectively restarting the channel's active period. + +**`tfClose` flag** implements an asymmetric close protocol: +- If the **destination** requests close, or if the channel is fully drained (`sfBalance == sfAmount`), `closeChannel()` runs immediately. The destination has no need to wait — they have already accepted all funds. +- If the **source** requests close, rather than closing immediately (which would let the source steal unclaimed XRP by locking out the destination), a `settleExpiration` is computed as `now + sfSettleDelay`. If no prior expiration exists, or if the current expiration is further in the future than `settleExpiration`, the closer expiration wins. This gives the destination a guaranteed window to submit their highest-authorized claim before the channel can be reclaimed. + +**`closeChannel()` teardown** removes the `ltPAYCHAN` object from both the source's and destination's owner directories, returns unclaimed XRP (`sfAmount - sfBalance`) to the source, decrements the source's owner count, and erases the channel from the ledger. + +## Relationship to Sibling Transactors + +`PaymentChannelCreate` opens the channel and escrows the capacity. `PaymentChannelFund` adds XRP to an existing channel's `sfAmount` and optionally extends its expiry. `PaymentChannelClaim` is the only one that transfers XRP *out* to the destination — the other two only move XRP into or within the channel. All three share the same channel-close logic via `closeChannel()` from `PaymentChannelHelpers`, and all three check expiry at the start of `doApply` before doing anything else. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp.ai.json b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp.ai.json new file mode 100644 index 0000000000..7ea8ea28d9 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp.ai.json @@ -0,0 +1,450 @@ +{ + "args": [ + { + "lineno": 32, + "name": "ctx" + }, + { + "lineno": 37, + "name": "ctx" + }, + { + "lineno": 49, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "PaymentChannelCreate::preflight" + ], + "entry_point": "PaymentChannelCreate::preflight", + "purpose": "Performs stateless validation of the PaymentChannelCreate transaction. Checks for basic field correctness and malformed transactions.", + "validation_points": [ + "isXRP(ctx.tx[sfAmount])", + "ctx.tx[sfAmount] <= beast::zero", + "ctx.tx[sfAccount] == ctx.tx[sfDestination]", + "!publicKeyType(ctx.tx[sfPublicKey])" + ] + }, + { + "call_chain": [ + "PaymentChannelCreate::preclaim" + ], + "entry_point": "PaymentChannelCreate::preclaim", + "purpose": "Performs contextual validation, checking ledger state and account conditions before transaction application.", + "validation_points": [ + "ctx.view.read(keylet::account(account))", + "balance < reserve", + "balance < reserve + ctx.tx[sfAmount]", + "ctx.view.read(keylet::account(dst))", + "(flags & lsfDisallowIncomingPayChan) != 0u", + "((flags & lsfRequireDestTag) != 0u) && !ctx.tx[~sfDestinationTag]", + "isPseudoAccount(sled)" + ] + }, + { + "call_chain": [ + "PaymentChannelCreate::doApply" + ], + "entry_point": "PaymentChannelCreate::doApply", + "purpose": "Applies the transaction to the ledger after all validations pass. Assumes all validation is already complete.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "sfAmount", + "flow": [ + "Transaction JSON", + "ctx.tx[sfAmount]", + "PaymentChannelCreate::preflight", + "PaymentChannelCreate::preclaim", + "PaymentChannelCreate::doApply" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Checked for isXRP type", + "Compared to beast::zero (must be > 0)", + "Used in reserve/funding checks" + ], + "validated_at": "preflight (type, >0), preclaim (funds available)" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction JSON", + "ctx.tx[sfAccount]", + "PaymentChannelCreate::preflight", + "PaymentChannelCreate::preclaim", + "PaymentChannelCreate::doApply" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared to sfDestination (must not be same)", + "Used to look up account root in ledger" + ], + "validated_at": "preflight (not same as destination), preclaim (account exists)" + }, + { + "field": "sfDestination", + "flow": [ + "Transaction JSON", + "ctx.tx[sfDestination]", + "PaymentChannelCreate::preflight", + "PaymentChannelCreate::preclaim" + ], + "origin": "ctx.tx[sfDestination] (transaction input)", + "transformations": [ + "Compared to sfAccount (must not be same)", + "Used to look up destination account in ledger" + ], + "validated_at": "preflight (not same as account), preclaim (destination exists, flags checked)" + }, + { + "field": "sfPublicKey", + "flow": [ + "Transaction JSON", + "ctx.tx[sfPublicKey]", + "PaymentChannelCreate::preflight" + ], + "origin": "ctx.tx[sfPublicKey] (transaction input)", + "transformations": [ + "Checked for valid public key type" + ], + "validated_at": "preflight" + }, + { + "field": "sfDestinationTag", + "flow": [ + "Transaction JSON", + "ctx.tx[~sfDestinationTag]", + "PaymentChannelCreate::preclaim" + ], + "origin": "ctx.tx[~sfDestinationTag] (optional transaction input)", + "transformations": [ + "Checked for presence if destination requires it" + ], + "validated_at": "preclaim (if destination account has lsfRequireDestTag)" + } + ], + "description": "Implements the PaymentChannelCreate transaction logic for creating XRP payment channels, including preflight checks, preclaim checks, and ledger application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "isXRP(ctx.tx[sfAmount]) at preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "isXRP(ctx.tx[sfAmount]) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "type", + "validation", + "check" + ], + "evidence": "isXRP(ctx.tx[sfAmount]) at preflight", + "issue_pattern": "Missing type validation for sfAmount", + "why_false_positive": "isXRP(ctx.tx[sfAmount]) validates sfAmount type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "(ctx.tx[sfAmount] <= beast::zero) at preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "(ctx.tx[sfAmount] <= beast::zero) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "range", + "bounds", + "validation" + ], + "evidence": "(ctx.tx[sfAmount] <= beast::zero) at preflight", + "issue_pattern": "Missing range validation for sfAmount", + "why_false_positive": "(ctx.tx[sfAmount] <= beast::zero) validates sfAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount, sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "(ctx.tx[sfAccount] == ctx.tx[sfDestination]) at preflight", + "issue_pattern": "Missing empty string validation for sfAccount, sfDestination", + "why_false_positive": "(ctx.tx[sfAccount] == ctx.tx[sfDestination]) validates sfAccount, sfDestination for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfPublicKey", + "empty", + "string", + "validation" + ], + "evidence": "!publicKeyType(ctx.tx[sfPublicKey]) at preflight", + "issue_pattern": "Missing empty string validation for sfPublicKey", + "why_false_positive": "!publicKeyType(ctx.tx[sfPublicKey]) validates sfPublicKey for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfPublicKey", + "format", + "validation", + "invalid" + ], + "evidence": "!publicKeyType(ctx.tx[sfPublicKey]) at preflight", + "issue_pattern": "Missing format validation for sfPublicKey", + "why_false_positive": "!publicKeyType(ctx.tx[sfPublicKey]) validates sfPublicKey format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::account(account)) at preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "ctx.view.read(keylet::account(account)) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBalance, sfOwnerCount, sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "balance < reserve at preclaim", + "issue_pattern": "Missing empty string validation for sfBalance, sfOwnerCount, sfAmount", + "why_false_positive": "balance < reserve validates sfBalance, sfOwnerCount, sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBalance, sfOwnerCount, sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "balance < reserve + ctx.tx[sfAmount] at preclaim", + "issue_pattern": "Missing empty string validation for sfBalance, sfOwnerCount, sfAmount", + "why_false_positive": "balance < reserve + ctx.tx[sfAmount] validates sfBalance, sfOwnerCount, sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::account(dst)) at preclaim", + "issue_pattern": "Missing empty string validation for sfDestination", + "why_false_positive": "ctx.view.read(keylet::account(dst)) validates sfDestination for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 32, + "name": "PaymentChannelCreate::makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 37, + "name": "PaymentChannelCreate::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 49, + "name": "PaymentChannelCreate::preclaim" + }, + { + "args": [], + "lineno": 87, + "name": "PaymentChannelCreate::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "PaymentChannelCreate is a core transaction type and is typically covered by unit/integration tests in the rippled codebase. Look for tests in files like 'test/tx/PaymentChannel_test.cpp', 'test/PaymentChannel_test.cpp', or similar. These tests should cover valid/invalid amounts, account existence, destination tag requirements, public key validation, and reserve/funding checks. Gaps may exist for edge cases (e.g., pseudo-accounts, rare flag combinations, or malformed public keys). Ensure tests cover all validation branches, especially error returns (temBAD_AMOUNT, temDST_IS_SRC, temMALFORMED, terNO_ACCOUNT, tecINSUFFICIENT_RESERVE, tecUNFUNDED, tecNO_DST, tecNO_PERMISSION, tecDST_TAG_NEEDED).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl core transaction processing (custom, not external framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflight", + "validated_by": "isXRP(ctx.tx[sfAmount])", + "validates": [ + "Checks that the Amount field is an XRP amount (not IOU or other type)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflight", + "validated_by": "(ctx.tx[sfAmount] <= beast::zero)", + "validates": [ + "Checks that the Amount is greater than zero" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temDST_IS_SRC", + "field": "sfAccount, sfDestination", + "location": "preflight", + "validated_by": "(ctx.tx[sfAccount] == ctx.tx[sfDestination])", + "validates": [ + "Checks that the source and destination accounts are not the same" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfPublicKey", + "location": "preflight", + "validated_by": "!publicKeyType(ctx.tx[sfPublicKey])", + "validates": [ + "Checks that the public key is a valid type/format" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_ACCOUNT", + "field": "sfAccount", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::account(account))", + "validates": [ + "Checks that the source account exists in the ledger" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecINSUFFICIENT_RESERVE", + "field": "sfBalance, sfOwnerCount, sfAmount", + "location": "preclaim", + "validated_by": "balance < reserve", + "validates": [ + "Checks that the account has enough balance to meet the reserve requirement for owning an additional object" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecUNFUNDED", + "field": "sfBalance, sfOwnerCount, sfAmount", + "location": "preclaim", + "validated_by": "balance < reserve + ctx.tx[sfAmount]", + "validates": [ + "Checks that the account has enough balance to fund the channel after accounting for reserve" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_DST", + "field": "sfDestination", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::account(dst))", + "validates": [ + "Checks that the destination account exists in the ledger" + ], + "validation_type": "existence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp.ai.md b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp.ai.md new file mode 100644 index 0000000000..a6f719314f --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelCreate.cpp.ai.md @@ -0,0 +1,46 @@ +# `PaymentChannelCreate.cpp` + +## Role in the System + +This file implements the `PaymentChannelCreate` transactor — the on-ledger entry point for opening an XRP payment channel. Payment channels are XRPL's primary primitive for streaming micropayments: the channel owner locks XRP into a dedicated ledger object, then issues cryptographically-signed claims off-ledger. The recipient can present a claim at any time to settle on-chain, while the owner can continuously authorize higher amounts without touching the ledger. Only opening the channel, adding funds (`PaymentChannelFund`), claiming (`PaymentChannelClaim`), and closing require ledger transactions — the high-frequency payment flow happens entirely off-chain. + +`PaymentChannelCreate.cpp` is responsible only for the construction phase. It sits alongside `PaymentChannelClaim.cpp` and `PaymentChannelFund.cpp` in the same directory, forming the complete lifecycle of a payment channel. + +## Three-Phase Transaction Model + +Like all transactors, `PaymentChannelCreate` is split across four methods following XRPL's layered validation architecture. + +**`makeTxConsequences`** declares the worst-case XRP consumed by this transaction — `sfAmount` plus the transaction fee. This is used by the network before applying the transaction to determine whether a sender can afford it. + +**`preflight`** runs with no ledger access and performs purely structural checks: +- `sfAmount` must be XRP (not an IOU) and strictly positive. Using `isXRP()` and comparing against `beast::zero` rejects both wrong-currency amounts and zero-value channels that would have no purpose. +- The source account cannot be its own destination (`temDST_IS_SRC`). Allowing this would create a nonsensical self-paying channel that serves no off-ledger settlement purpose. +- `sfPublicKey` must pass `publicKeyType()` — only known cryptographic key formats (secp256k1 or Ed25519) are accepted. The key is what the owner will later use to sign off-ledger claims; an unrecognized format would make every claim unverifiable. + +**`preclaim`** runs against a read-only snapshot of the current ledger and validates state-dependent conditions: +- The source account must exist. +- Two separate reserve checks ensure the account can both cover the incremental reserve for owning one more ledger object (`balance < reserve`) and additionally fund the channel itself (`balance < reserve + amount`). The two-step distinction matters: `tecINSUFFICIENT_RESERVE` means the account cannot afford the reserve at all, while `tecUNFUNDED` means it could afford the reserve but not the requested channel amount. +- The destination must exist (`tecNO_DST`). There is a deliberate design choice not to create channels to non-existent accounts, even though XRP would be held in escrow — this avoids reserving funds to an address that may never be activated. +- If the destination has set `lsfDisallowIncomingPayChan`, the creation is rejected with `tecNO_PERMISSION`. This lets accounts opt out of receiving unsolicited channels. +- If the destination has set `lsfRequireDestTag`, a `sfDestinationTag` must be present — otherwise `tecDST_TAG_NEEDED` is returned. +- `isPseudoAccount(sled)` prevents channels from being directed at pseudo-accounts (synthetic account objects that back ledger features rather than real users). This check is intentionally not amendment-gated: because pseudo-account discriminator fields are themselves only written under amendment guards, the check naturally behaves correctly across all amendment states. + +**`doApply`** mutates the ledger, assuming all validations passed. It performs two amendment-gated checks and then constructs the channel SLE. + +## Key Design Decisions + +**Channel key derivation.** `keylet::payChan(account, dst, ctx_.tx.getSeqValue())` hashes the source account, destination account, and transaction sequence (or ticket) number into the channel's ledger key. This ensures that two channels between the same pair of accounts are always addressable distinctly. Using `getSeqValue()` rather than `getSeq()` transparently handles both regular sequence numbers and ticket-based transactions — a detail acknowledged by a code comment referencing `SeqProxy.h`. + +**`sfCancelAfter` expiry check in `doApply`, not `preclaim`.** The `fixPayChanCancelAfter` amendment adds a guard that rejects channel creation if the optional `sfCancelAfter` timestamp is already past the ledger's `parentCloseTime`. This check lives in `doApply` rather than `preclaim` because the canonical close time of the ledger under construction is only fully determined at apply time. Running it during `preclaim` against an earlier view could allow a transaction through that immediately expires. + +**`sfBalance` initialized via `zeroed()`.** The newly created SLE sets `sfBalance` to `ctx_.tx[sfAmount].zeroed()` — not a literal zero, but a zero of the same `XRPAmount` type. This makes the channel's "amount paid so far" field type-consistent with its "total funded" field from the start. + +**`fixIncludeKeyletFields` amendment.** When active, `sfSequence` is written directly into the channel SLE. This allows off-ledger clients and tools to reconstruct the channel's `keylet` purely from the object itself, without needing the originating transaction data. This was a bug-fix amendment: channels created before it lack the sequence field, requiring callers to retrieve the transaction to compute the key. + +**Dual directory insertion.** The channel is inserted into both the owner's directory and the destination's directory, with the resulting page numbers stored as `sfOwnerNode` and `sfDestinationNode` respectively. This is the standard XRPL pattern for objects that two parties have claims to: both accounts can enumerate the channel through their own account's linked object list, and removal is efficiently reversible during claim/close operations. + +**Owner count and balance mutation.** At the end of `doApply`, the source account's `sfBalance` is decremented by `sfAmount` (the XRP moves into the channel SLE), and `adjustOwnerCount` increments the owner count by one. The owner count increase raises the account's required reserve, reinforcing that the channel occupies a real slot in the ledger's object hierarchy. + +## Relationship to Sibling Transactors + +`PaymentChannelFund.cpp` adds XRP to an existing channel: it updates `sfAmount` on the channel SLE directly and debits the owner, without creating any new SLE or touching directories. `PaymentChannelClaim.cpp` handles the claim flow — verifying the off-ledger signature, updating `sfBalance`, optionally closing the channel, and returning any remaining funds to the owner. `PaymentChannelCreate` is the only transactor that allocates the SLE and both directory entries; all others assume those structures already exist. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp.ai.json b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp.ai.json new file mode 100644 index 0000000000..9b8d66d7c8 --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp.ai.json @@ -0,0 +1,407 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "PaymentChannelFund::preflight" + ], + "entry_point": "PaymentChannelFund::preflight", + "purpose": "Performs initial stateless validation of the PaymentChannelFund transaction before it is applied to the ledger.", + "validation_points": [ + "Checks that sfAmount is XRP and > 0" + ] + }, + { + "call_chain": [ + "PaymentChannelFund::doApply" + ], + "entry_point": "PaymentChannelFund::doApply", + "purpose": "Performs all stateful validation and applies the PaymentChannelFund transaction to the ledger if valid.", + "validation_points": [ + "Checks that payment channel exists (sfChannel)", + "Checks channel not expired (sfCancelAfter, sfExpiration)", + "Checks only owner can fund (sfAccount)", + "Checks expiration extension is valid (sfExpiration)", + "Checks source account exists (sfAccount)", + "Checks reserve and funds (sfBalance, sfOwnerCount, sfAmount)", + "Checks destination account exists (sfDestination)" + ] + } + ], + "data_flows": [ + { + "field": "sfAmount", + "flow": [ + "ctx.tx[sfAmount]", + "PaymentChannelFund::preflight (isXRP, > 0)", + "PaymentChannelFund::doApply (used for funding, balance checks, and updating channel)" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Validated as XRP and > 0 in preflight", + "Added to channel's sfAmount", + "Subtracted from source account's sfBalance" + ], + "validated_at": "preflight (isXRP, > 0), doApply (funds check)" + }, + { + "field": "sfChannel", + "flow": [ + "ctx.tx[sfChannel]", + "Keylet k constructed", + "ctx_.view().peek(k) to fetch channel SLE" + ], + "origin": "ctx.tx[sfChannel] (transaction input)", + "transformations": [ + "Used to locate the payment channel in the ledger" + ], + "validated_at": "doApply (channel existence check)" + }, + { + "field": "sfAccount (source)", + "flow": [ + "ctx.tx[sfAccount]", + "compared to channel's sfAccount (src)", + "used to fetch source account SLE" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Checked for permission (must match channel owner)", + "Used to fetch account SLE" + ], + "validated_at": "doApply (permission check, account existence check)" + }, + { + "field": "sfExpiration", + "flow": [ + "ctx.tx[~sfExpiration]", + "if present, compared to minExpiration", + "if valid, updates channel's sfExpiration" + ], + "origin": "ctx.tx[~sfExpiration] (optional transaction input)", + "transformations": [ + "Validated against minExpiration", + "Updates channel SLE if valid" + ], + "validated_at": "doApply (expiration extension check)" + }, + { + "field": "sfDestination", + "flow": [ + "(*slep)[sfDestination]", + "ctx_.view().read(keylet::account(dst))" + ], + "origin": "(*slep)[sfDestination] (from channel SLE)", + "transformations": [ + "Checked for existence in ledger" + ], + "validated_at": "doApply (destination existence check)" + }, + { + "field": "sfBalance", + "flow": [ + "(*sle)[sfBalance]", + "checked against reserve and funding amount", + "decremented by ctx_.tx[sfAmount] if funding succeeds" + ], + "origin": "(*sle)[sfBalance] (from source account SLE)", + "transformations": [ + "Checked for sufficient funds", + "Decremented on successful fund" + ], + "validated_at": "doApply (funds/reserve check)" + } + ], + "description": "Implements the logic for the PaymentChannelFund transaction in the XRPL, handling the addition of funds to an existing payment channel, including validation, permission checks, expiration extension, and ledger updates.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "isXRP() and (ctx.tx[sfAmount] <= beast::zero) at preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "isXRP() and (ctx.tx[sfAmount] <= beast::zero) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfChannel", + "empty", + "string", + "validation" + ], + "evidence": "ctx_.view().peek(k) at doApply", + "issue_pattern": "Missing empty string validation for sfChannel", + "why_false_positive": "ctx_.view().peek(k) validates sfChannel for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "src != txAccount at doApply", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "src != txAccount validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfExpiration", + "empty", + "string", + "validation" + ], + "evidence": "(*slep)[~sfExpiration] and (*extend < minExpiration) at doApply", + "issue_pattern": "Missing empty string validation for sfExpiration", + "why_false_positive": "(*slep)[~sfExpiration] and (*extend < minExpiration) validates sfExpiration for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount (source account)", + "empty", + "string", + "validation" + ], + "evidence": "ctx_.view().peek(keylet::account(txAccount)) at doApply", + "issue_pattern": "Missing empty string validation for sfAccount (source account)", + "why_false_positive": "ctx_.view().peek(keylet::account(txAccount)) validates sfAccount (source account) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBalance and sfOwnerCount", + "empty", + "string", + "validation" + ], + "evidence": "balance < reserve at doApply", + "issue_pattern": "Missing empty string validation for sfBalance and sfOwnerCount", + "why_false_positive": "balance < reserve validates sfBalance and sfOwnerCount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBalance and sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "balance < reserve + ctx_.tx[sfAmount] at doApply", + "issue_pattern": "Missing empty string validation for sfBalance and sfAmount", + "why_false_positive": "balance < reserve + ctx_.tx[sfAmount] validates sfBalance and sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDestination", + "empty", + "string", + "validation" + ], + "evidence": "ctx_.view().read(keylet::account(dst)) at doApply", + "issue_pattern": "Missing empty string validation for sfDestination", + "why_false_positive": "ctx_.view().read(keylet::account(dst)) validates sfDestination for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfCancelAfter and sfExpiration", + "empty", + "string", + "validation" + ], + "evidence": "(cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration) at doApply", + "issue_pattern": "Missing empty string validation for sfCancelAfter and sfExpiration", + "why_false_positive": "(cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration) validates sfCancelAfter and sfExpiration for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "PaymentChannelFund::makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "PaymentChannelFund::preflight" + }, + { + "args": [], + "lineno": 20, + "name": "PaymentChannelFund::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "PaymentChannelFund is typically tested in integration and unit tests for payment channels. Likely test files include PaymentChannel_test.cpp, PaymentChannelFund_test.cpp, or broader transaction engine tests. The preflight and doApply validations (amount, channel existence, permissions, expiration, reserve, destination) are usually covered. Gaps may exist for rare edge cases (e.g., internal errors, simultaneous expiration/cancel, malformed SLEs). LCOV_EXCL_LINE indicates some error paths may not be directly tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in business logic (no external framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflight", + "validated_by": "isXRP() and (ctx.tx[sfAmount] <= beast::zero)", + "validates": [ + "Checks that sfAmount is an XRP amount (not IOU)", + "Checks that sfAmount is greater than zero" + ], + "validation_type": "type|range" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfChannel", + "location": "doApply", + "validated_by": "ctx_.view().peek(k)", + "validates": [ + "Checks that the payment channel exists" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfAccount", + "location": "doApply", + "validated_by": "src != txAccount", + "validates": [ + "Checks that the transaction is submitted by the channel owner" + ], + "validation_type": "business_logic|ownership" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_EXPIRATION", + "field": "sfExpiration", + "location": "doApply", + "validated_by": "(*slep)[~sfExpiration] and (*extend < minExpiration)", + "validates": [ + "Checks that the new expiration is not before the minimum allowed expiration" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sfAccount (source account)", + "location": "doApply", + "validated_by": "ctx_.view().peek(keylet::account(txAccount))", + "validates": [ + "Checks that the source account exists" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecINSUFFICIENT_RESERVE", + "field": "sfBalance and sfOwnerCount", + "location": "doApply", + "validated_by": "balance < reserve", + "validates": [ + "Checks that the account has enough balance to meet reserve requirements" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 1.0, + "error_thrown": "tecUNFUNDED", + "field": "sfBalance and sfAmount", + "location": "doApply", + "validated_by": "balance < reserve + ctx_.tx[sfAmount]", + "validates": [ + "Checks that the account has enough balance to fund the channel after reserve" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_DST", + "field": "sfDestination", + "location": "doApply", + "validated_by": "ctx_.view().read(keylet::account(dst))", + "validates": [ + "Checks that the destination account exists" + ], + "validation_type": "existence" + }, + { + "confidence": 0.9, + "error_thrown": "closeChannel() (returns error code)", + "field": "sfCancelAfter and sfExpiration", + "location": "doApply", + "validated_by": "(cancelAfter && closeTime >= *cancelAfter) || (expiration && closeTime >= *expiration)", + "validates": [ + "Checks if the channel should be closed due to expiration or cancelAfter" + ], + "validation_type": "business_logic|range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp.ai.md b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp.ai.md new file mode 100644 index 0000000000..00b0014bbb --- /dev/null +++ b/src/libxrpl/tx/transactors/payment_channel/PaymentChannelFund.cpp.ai.md @@ -0,0 +1,33 @@ +# `PaymentChannelFund.cpp` — Adding Funds to an Existing Payment Channel + +## Role in the System + +`PaymentChannelFund` implements the `PaymentChannelFund` transaction type, which lets a payment channel's owner top up the channel's XRP balance or extend its expiration time. It is one of three payment-channel transactors alongside `PaymentChannelCreate` and `PaymentChannelClaim`, and together they implement XRPL's off-ledger micropayment primitive: the channel sequesters XRP on-ledger while the parties exchange signed claim messages off-ledger, settling periodically. The fund transactor is the "refill" step — once a channel is running low, the owner can inject more XRP without closing and reopening. + +## Class Design + +`PaymentChannelFund` inherits from `Transactor` and follows the standard XRPL transactor pattern: a static `preflight` for stateless validation, and a `doApply` virtual method for stateful application. The `ConsequencesFactory` is set to `Custom`, which forces `makeTxConsequences` to be called. That method marks the full `sfAmount` (not just the fee) as XRP consumed from the account — this is critical so the transaction engine correctly computes account resource consumption when calculating whether conflicting transactions in the same ledger can coexist. + +## `preflight` — Stateless Validation + +The preflight check is intentionally minimal: it only verifies that `sfAmount` is a positive XRP value (not IOU). More complex checks that need ledger state (channel existence, permissions, reserve) are deferred to `doApply`. The `isXRP()` guard is necessary because the `sfAmount` field is polymorphic in the broader protocol and could carry non-XRP amounts from a malformed transaction. + +## `doApply` — State Transitions and Their Order + +The stateful apply phase performs a carefully ordered sequence of checks and mutations. The ordering is intentional and has protocol-level significance. + +**Expiry check precedes permission check.** The channel is fetched first, then both `sfCancelAfter` (immutable hard deadline set at creation) and `sfExpiration` (mutable soft deadline) are checked against `parentCloseTime`. If either has passed, `closeChannel()` is called immediately and the transactor returns — *before* checking whether the submitter owns the channel. This design means any transaction touching an expired channel, regardless of who submitted it, will trigger cleanup. The XRPL relies on this: expired channels get garbage-collected by whoever touches them next, not just their owner. + +**Only the owner can fund.** After the expiry check, the transactor enforces that `ctx_.tx[sfAccount]` equals the `sfAccount` stored in the channel SLE. Only the original creator-owner may add funds or extend expiration; the recipient has no such right. + +**Expiration extension logic.** If the transaction includes an optional `sfExpiration` field, the code computes a `minExpiration` as `parentCloseTime + sfSettleDelay`. It then tightens this floor: if the channel already has an expiration set that is *earlier* than `minExpiration`, `minExpiration` is reduced to match the existing expiration. The effect is that the owner can never set a new expiration *earlier* than the current one, and can never set an expiration that bypasses the settle delay window. This prevents a subtle attack where an owner could sneak in a very short expiration to deprive the recipient of their settle window. + +**Reserve and balance checks.** The transactor fetches the owner's account SLE and checks two conditions separately. First it checks `balance < reserve` (`tecINSUFFICIENT_RESERVE`) — the account's current balance doesn't even cover its base reserve, independent of the funding amount. Then it checks `balance < reserve + sfAmount` (`tecUNFUNDED`) — the account can't afford to send the requested amount on top of its reserve. This two-step structure gives callers distinct error codes for "account is already underwater" versus "amount too large." + +**Destination existence guard.** Immediately before the ledger write, the code checks that the channel's destination account still exists. This handles a race condition: the destination account could have been deleted after channel creation. Adding more funds to an orphaned channel would be wasteful (those XRP would be unclaimable), so `tecNO_DST` prevents it. + +**Ledger update — double-entry invariant.** The actual mutation increments `(*slep)[sfAmount]` (the total funded capacity of the channel) and decrements `(*sle)[sfBalance]` (the owner's account balance) by the same `sfAmount`. Both SLEs are then passed to `ctx_.view().update()`. Note that the channel's `sfAmount` tracks *total capacity*, not *available balance* — `sfBalance` on the channel tracks cumulative payments already claimed. The owner's account `sfBalance` is the XRP actually leaving the owner's pocket. + +## Failure Modes and Defensive Checks + +The `tefINTERNAL` returned when the source account SLE cannot be fetched is annotated `LCOV_EXCL_LINE`, indicating it is considered unreachable in practice — if a signed transaction has been accepted into the network, the submitting account must exist. The annotation signals that branch is purely defensive. All other error codes are reachable and tested: `tecNO_ENTRY` (channel doesn't exist), `tecNO_PERMISSION` (non-owner), `temBAD_EXPIRATION` (illegal expiration extension), `tecINSUFFICIENT_RESERVE`, `tecUNFUNDED`, and `tecNO_DST`. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp.ai.json b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp.ai.json new file mode 100644 index 0000000000..c141e79930 --- /dev/null +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp.ai.json @@ -0,0 +1,383 @@ +{ + "args": [ + { + "lineno": 8, + "name": "ctx" + }, + { + "lineno": 16, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "PermissionedDomainDelete::preflight" + ], + "entry_point": "PermissionedDomainDelete::preflight", + "purpose": "Initial transaction validation: checks for malformed or missing fields before further processing.", + "validation_points": [ + "Checks if sfDomainID is present and not zero (ctx.tx.getFieldH256(sfDomainID) == beast::zero)" + ] + }, + { + "call_chain": [ + "PermissionedDomainDelete::preclaim" + ], + "entry_point": "PermissionedDomainDelete::preclaim", + "purpose": "Checks ledger state and permissions before transaction is allowed to proceed.", + "validation_points": [ + "Checks if permissioned domain exists (ctx.view.read(keylet::permissionedDomain(domain)))", + "Checks if sfOwner and sfAccount fields are present (sleDomain->isFieldPresent(sfOwner) && ctx.tx.isFieldPresent(sfAccount))", + "Checks if sfOwner matches sfAccount (sleDomain->getAccountID(sfOwner) != ctx.tx.getAccountID(sfAccount))" + ] + }, + { + "call_chain": [ + "PermissionedDomainDelete::doApply" + ], + "entry_point": "PermissionedDomainDelete::doApply", + "purpose": "Applies the transaction to the ledger, deleting the permissioned domain if all validations pass.", + "validation_points": [ + "Checks if sfDomainID is present (ctx_.tx.isFieldPresent(sfDomainID))", + "Asserts owner count is nonzero before decrementing" + ] + } + ], + "data_flows": [ + { + "field": "sfDomainID", + "flow": [ + "ctx.tx.getFieldH256(sfDomainID) in preflight", + "ctx.tx.getFieldH256(sfDomainID) in preclaim", + "ctx_.tx.at(sfDomainID) in doApply" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for zero value in preflight", + "Used as key to lookup permissioned domain in ledger in preclaim and doApply" + ], + "validated_at": "preflight (not zero), preclaim (exists in ledger), doApply (present in tx)" + }, + { + "field": "sfOwner", + "flow": [ + "sleDomain->isFieldPresent(sfOwner) in preclaim", + "sleDomain->getAccountID(sfOwner) in preclaim", + "(*slePd)[sfOwnerNode] in doApply" + ], + "origin": "sleDomain (ledger entry for permissioned domain)", + "transformations": [ + "Checked for presence in preclaim", + "Compared to sfAccount in preclaim", + "Used to find owner directory node in doApply" + ], + "validated_at": "preclaim (presence and match with sfAccount)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx.isFieldPresent(sfAccount) in preclaim", + "ctx.tx.getAccountID(sfAccount) in preclaim" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence in preclaim", + "Compared to sfOwner in preclaim" + ], + "validated_at": "preclaim (presence and match with sfOwner)" + }, + { + "field": "sfOwnerNode", + "flow": [ + "(*slePd)[sfOwnerNode] in doApply" + ], + "origin": "slePd (permissioned domain ledger entry)", + "transformations": [ + "Used to locate the directory node for removal" + ], + "validated_at": "Not directly validated, but used after prior validations" + }, + { + "field": "sfOwnerCount", + "flow": [ + "ownerSle->getFieldU32(sfOwnerCount) in doApply" + ], + "origin": "ownerSle (account root ledger entry)", + "transformations": [ + "Asserted to be > 0 before decrement", + "Decremented by adjustOwnerCount" + ], + "validated_at": "doApply (asserted > 0)" + } + ], + "description": "Implements the PermissionedDomainDelete transaction logic for deleting a permissioned domain in the XRPL ledger, including preflight, preclaim, and apply steps.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.getFieldH256(sfDomainID) == beast::zero at PermissionedDomainDelete::preflight", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "ctx.tx.getFieldH256(sfDomainID) == beast::zero validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfDomainID", + "format", + "validation", + "invalid" + ], + "evidence": "ctx.tx.getFieldH256(sfDomainID) == beast::zero at PermissionedDomainDelete::preflight", + "issue_pattern": "Missing format validation for sfDomainID", + "why_false_positive": "ctx.tx.getFieldH256(sfDomainID) == beast::zero validates sfDomainID format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::permissionedDomain(domain)) at PermissionedDomainDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "ctx.view.read(keylet::permissionedDomain(domain)) validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwner, sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "sleDomain->isFieldPresent(sfOwner) && ctx.tx.isFieldPresent(sfAccount) at PermissionedDomainDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfOwner, sfAccount", + "why_false_positive": "sleDomain->isFieldPresent(sfOwner) && ctx.tx.isFieldPresent(sfAccount) validates sfOwner, sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwner, sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "sleDomain->getAccountID(sfOwner) != ctx.tx.getAccountID(sfAccount) at PermissionedDomainDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfOwner, sfAccount", + "why_false_positive": "sleDomain->getAccountID(sfOwner) != ctx.tx.getAccountID(sfAccount) validates sfOwner, sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "ctx_.tx.isFieldPresent(sfDomainID) at PermissionedDomainDelete::doApply", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "ctx_.tx.isFieldPresent(sfDomainID) validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "owner directory entry", + "empty", + "string", + "validation" + ], + "evidence": "!view().dirRemove(...) at PermissionedDomainDelete::doApply", + "issue_pattern": "Missing empty string validation for owner directory entry", + "why_false_positive": "!view().dirRemove(...) validates owner directory entry for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwnerCount", + "empty", + "string", + "validation" + ], + "evidence": "ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0 at PermissionedDomainDelete::doApply", + "issue_pattern": "Missing empty string validation for sfOwnerCount", + "why_false_positive": "ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0 validates sfOwnerCount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfOwnerCount", + "range", + "bounds", + "validation" + ], + "evidence": "ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0 at PermissionedDomainDelete::doApply", + "issue_pattern": "Missing range validation for sfOwnerCount", + "why_false_positive": "ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0 validates sfOwnerCount range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 7, + "name": "PermissionedDomainDelete::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 15, + "name": "PermissionedDomainDelete::preclaim" + }, + { + "args": [], + "lineno": 32, + "name": "PermissionedDomainDelete::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by unit/integration tests for PermissionedDomainDelete transactions, typically found in files like 'PermissionedDomain_test.cpp', 'Transactor_test.cpp', or similar in the rippled codebase. The validation paths (malformed domain, missing fields, permission errors, successful deletion) are straightforward and should be tested. However, edge cases such as ledger corruption (e.g., missing owner directory node, owner count underflow) may not be fully covered unless explicitly tested. The LCOV_EXCL_START/STOP block suggests that the error path for directory removal failure is not covered by tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL custom transaction processing (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfDomainID", + "location": "PermissionedDomainDelete::preflight", + "validated_by": "ctx.tx.getFieldH256(sfDomainID) == beast::zero", + "validates": [ + "Checks that the DomainID field is present and not zero (invalid/empty hash)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfDomainID", + "location": "PermissionedDomainDelete::preclaim", + "validated_by": "ctx.view.read(keylet::permissionedDomain(domain))", + "validates": [ + "Checks that the Permissioned Domain entry exists in the ledger for the given DomainID" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (internal assertion, likely aborts or logs error)", + "field": "sfOwner, sfAccount", + "location": "PermissionedDomainDelete::preclaim", + "validated_by": "sleDomain->isFieldPresent(sfOwner) && ctx.tx.isFieldPresent(sfAccount)", + "validates": [ + "Checks that the Permissioned Domain entry has an owner and the transaction specifies an account" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfOwner, sfAccount", + "location": "PermissionedDomainDelete::preclaim", + "validated_by": "sleDomain->getAccountID(sfOwner) != ctx.tx.getAccountID(sfAccount)", + "validates": [ + "Checks that the transaction's account matches the owner of the Permissioned Domain" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (internal assertion, likely aborts or logs error)", + "field": "sfDomainID", + "location": "PermissionedDomainDelete::doApply", + "validated_by": "ctx_.tx.isFieldPresent(sfDomainID)", + "validates": [ + "Checks that the DomainID field is present in the transaction" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "owner directory entry", + "location": "PermissionedDomainDelete::doApply", + "validated_by": "!view().dirRemove(...)", + "validates": [ + "Checks that the directory entry for the permissioned domain can be removed" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT (internal assertion, likely aborts or logs error)", + "field": "sfOwnerCount", + "location": "PermissionedDomainDelete::doApply", + "validated_by": "ownerSle && ownerSle->getFieldU32(sfOwnerCount) > 0", + "validates": [ + "Checks that the owner's OwnerCount is present and greater than zero before decrementing" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp.ai.md b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp.ai.md new file mode 100644 index 0000000000..1c900f35ea --- /dev/null +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainDelete.cpp.ai.md @@ -0,0 +1,29 @@ +# PermissionedDomainDelete.cpp + +## Role in the System + +This file implements the `PermissionedDomainDelete` transactor, responsible for removing a permissioned domain object from the XRPL ledger. Permissioned domains are on-ledger constructs that associate a domain owner with a set of accepted credential types; the Delete transaction is the counterpart to `PermissionedDomainSet`, which creates and mutates those objects. The transactor follows the standard three-phase XRPL pipeline — `preflight`, `preclaim`, `doApply` — ensuring that progressively deeper checks gate each stage. + +## The Three-Phase Pipeline + +**`preflight`** operates on the raw transaction before any ledger state is consulted. Its only duty here is to reject a zero-valued `sfDomainID`, the 256-bit hash identifying the domain. A zero hash is structurally invalid (analogous to a null pointer), so the transaction is immediately rejected with `temMALFORMED`. This check is deliberately minimal: `preflight` is meant to be cheap and stateless, and the structural validity of the domain ID is the only thing that can be verified without hitting the ledger. + +**`preclaim`** bridges the transaction against live ledger state while still being read-only. It resolves the `sfDomainID` to an actual ledger entry via `keylet::permissionedDomain(domain)`. If no such entry exists, `tecNO_ENTRY` is returned — the domain the sender wants to delete simply isn't there. If it does exist, an `XRPL_ASSERT` confirms that both `sfOwner` on the domain object and `sfAccount` on the transaction are present (these are invariants of well-formed objects; failure here indicates ledger or protocol corruption rather than a user error). The critical authorization check then follows: the domain's `sfOwner` must equal the transaction's `sfAccount`. Only the account that created the domain can delete it; any mismatch returns `tecNO_PERMISSION`. + +**`doApply`** performs the actual mutation. It has three ordered responsibilities: + +1. **Directory removal** — Every owner-controlled ledger object is tracked in the account's owner directory. The domain entry's `sfOwnerNode` field records which page of that directory holds its back-reference. `view().dirRemove()` removes that reference, passing `true` to also clean up the directory page if it becomes empty. Failure here cannot be triggered by a well-formed transaction — it indicates ledger structural corruption — so the block is annotated `LCOV_EXCL_START`/`LCOV_EXCL_STOP` to exclude it from coverage metrics, and it returns `tefBAD_LEDGER` if somehow reached. + +2. **Owner count adjustment** — The ledger enforces a reserve requirement proportional to the number of objects an account owns (its `sfOwnerCount`). Creating a domain increments this count (as seen in `PermissionedDomainSet::doApply`), so deletion must decrement it via `adjustOwnerCount(..., -1, ...)`. An assertion guards against underflow: `sfOwnerCount` must be greater than zero before decrementing. + +3. **Object erasure** — `view().erase(slePd)` removes the `SLE` (Serialized Ledger Entry) for the permissioned domain from the ledger view. This is the final and irreversible step. + +## Design Observations + +The separation between `preclaim` (read-only authorization) and `doApply` (mutation) is a deliberate architectural pattern in XRPL's transactor framework. It allows the engine to speculatively run preclaim across multiple transactions without committing side effects, and only advance to `doApply` once the full transaction set has been validated. + +The ownership check in `preclaim` is intentionally strict: there is no admin override, no co-ownership, and no delegated authority modeled here. The domain owner is the sole account authorized to delete the domain, which keeps the authorization model simple and auditable. + +The `ConsequencesFactory = Normal` setting on the class means this transaction consumes a standard fee and produces normal (non-blocking) consequences in the transaction queue — it will not hold up unrelated transactions from the same account. + +Compared to `PermissionedDomainSet`, deletion is much simpler: there is no reserve check (reserves are freed, not consumed), no credential array validation, and no directory insertion. The only bookkeeping asymmetry is that deletion calls `dirRemove` and decrements the owner count, whereas creation calls `dirInsert` and increments it. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp.ai.json b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp.ai.json new file mode 100644 index 0000000000..ca0846be88 --- /dev/null +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp.ai.json @@ -0,0 +1,321 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "PermissionedDomainSet::preflight" + ], + "entry_point": "PermissionedDomainSet::preflight", + "purpose": "Initial stateless validation of transaction fields before further processing.", + "validation_points": [ + "credentials::checkArray (validates sfAcceptedCredentials array size/format)", + "Manual check: domain && *domain == beast::zero (validates sfDomainID is not zero)" + ] + }, + { + "call_chain": [ + "PermissionedDomainSet::preclaim" + ], + "entry_point": "PermissionedDomainSet::preclaim", + "purpose": "Stateful validation: checks ledger state for referenced accounts and domain existence/ownership.", + "validation_points": [ + "ctx.view.exists(keylet::account(account)) (validates sfAccount exists)", + "ctx.view.exists(keylet::account(credential.getAccountID(sfIssuer))) (validates each sfIssuer exists)", + "ctx.view.read(keylet::permissionedDomain(ctx.tx.getFieldH256(sfDomainID))) (validates sfDomainID exists)", + "sleDomain->getAccountID(sfOwner) != account (validates ownership of domain)" + ] + }, + { + "call_chain": [ + "PermissionedDomainSet::doApply" + ], + "entry_point": "PermissionedDomainSet::doApply", + "purpose": "Applies the transaction: creates or modifies the Permissioned Domain object in the ledger.", + "validation_points": [ + "view().peek(keylet::account(account_)) (re-checks account exists)", + "view().peek(keylet::permissionedDomain(...)) (re-checks domain exists if modifying)", + "balance < reserve (checks sufficient reserve for new object)" + ] + } + ], + "data_flows": [ + { + "field": "sfAcceptedCredentials", + "flow": [ + "Transaction input", + "preflight: credentials::checkArray (validates array)", + "preclaim: iterated for issuer existence", + "doApply: sorted and written to ledger" + ], + "origin": "ctx.tx.getFieldArray(sfAcceptedCredentials) (transaction input)", + "transformations": [ + "Validated for size/format", + "Each credential's sfIssuer checked for existence", + "Sorted (credentials::makeSorted)", + "Transformed into STArray for ledger storage" + ], + "validated_at": "preflight (array structure), preclaim (issuer existence)" + }, + { + "field": "sfDomainID", + "flow": [ + "Transaction input", + "preflight: checked for zero value", + "preclaim: checked for existence in ledger", + "preclaim: checked for ownership", + "doApply: used to fetch or create domain ledger entry" + ], + "origin": "ctx.tx.at(~sfDomainID) or ctx.tx.getFieldH256(sfDomainID) (transaction input)", + "transformations": [ + "Checked for zero value", + "Used as key to fetch ledger entry", + "Ownership checked" + ], + "validated_at": "preflight (zero check), preclaim (existence/ownership)" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "preclaim: checked for existence in ledger", + "doApply: used as owner for new domain" + ], + "origin": "ctx.tx.getAccountID(sfAccount) (transaction input)", + "transformations": [ + "Checked for existence", + "Used as owner in ledger entry" + ], + "validated_at": "preclaim" + }, + { + "field": "sfIssuer (inside sfAcceptedCredentials)", + "flow": [ + "Extracted from each credential in sfAcceptedCredentials", + "preclaim: checked for existence in ledger" + ], + "origin": "credential.getAccountID(sfIssuer) (from each credential in sfAcceptedCredentials)", + "transformations": [ + "Checked for existence" + ], + "validated_at": "preclaim" + } + ], + "description": "Implements the PermissionedDomainSet transactor for the XRPL, handling creation and modification of permissioned domains, including credential validation and ledger updates.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAcceptedCredentials", + "empty", + "string", + "validation" + ], + "evidence": "credentials::checkArray at PermissionedDomainSet::preflight", + "issue_pattern": "Missing empty string validation for sfAcceptedCredentials", + "why_false_positive": "credentials::checkArray validates sfAcceptedCredentials for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "manual check (domain && *domain == beast::zero) at PermissionedDomainSet::preflight", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "manual check (domain && *domain == beast::zero) validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.exists(keylet::account(account)) at PermissionedDomainSet::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "ctx.view.exists(keylet::account(account)) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAcceptedCredentials[].sfIssuer", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.exists(keylet::account(credential.getAccountID(sfIssuer))) at PermissionedDomainSet::preclaim", + "issue_pattern": "Missing empty string validation for sfAcceptedCredentials[].sfIssuer", + "why_false_positive": "ctx.view.exists(keylet::account(credential.getAccountID(sfIssuer))) validates sfAcceptedCredentials[].sfIssuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::permissionedDomain(ctx.tx.getFieldH256(sfDomainID))) at PermissionedDomainSet::preclaim", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "ctx.view.read(keylet::permissionedDomain(ctx.tx.getFieldH256(sfDomainID))) validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID ownership", + "empty", + "string", + "validation" + ], + "evidence": "sleDomain->getAccountID(sfOwner) != account at PermissionedDomainSet::preclaim", + "issue_pattern": "Missing empty string validation for sfDomainID ownership", + "why_false_positive": "sleDomain->getAccountID(sfOwner) != account validates sfDomainID ownership for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 9, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 14, + "name": "preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 28, + "name": "preclaim" + }, + { + "args": [], + "lineno": 49, + "name": "doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is in preflight and preclaim. These are typically tested via transaction unit/integration tests in the rippled codebase, likely in files such as test/tx/PermissionedDomainSet_test.cpp or similar. Coverage gaps may exist for error branches marked LCOV_EXCL_LINE (e.g., tefINTERNAL, tecDIR_FULL), which are hard to trigger in normal operation. Edge cases such as malformed sfAcceptedCredentials, non-existent issuers, insufficient reserve, and domain ownership mismatches should be explicitly tested. If no PermissionedDomainSet-specific test file exists, coverage may be incomplete for these validation paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl custom validation (ledger view, helper functions, error codes)", + "validation_layer": "business_logic (preflight, preclaim, doApply)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "NotTEC error code (non-tesSUCCESS)", + "field": "sfAcceptedCredentials", + "location": "PermissionedDomainSet::preflight", + "validated_by": "credentials::checkArray", + "validates": [ + "Array size does not exceed maxPermissionedDomainCredentialsArraySize", + "Array structure/format is correct (as per checkArray implementation)" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfDomainID", + "location": "PermissionedDomainSet::preflight", + "validated_by": "manual check (domain && *domain == beast::zero)", + "validates": [ + "Domain ID is not zero (beast::zero)" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sfAccount", + "location": "PermissionedDomainSet::preclaim", + "validated_by": "ctx.view.exists(keylet::account(account))", + "validates": [ + "Account exists in ledger" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ISSUER", + "field": "sfAcceptedCredentials[].sfIssuer", + "location": "PermissionedDomainSet::preclaim", + "validated_by": "ctx.view.exists(keylet::account(credential.getAccountID(sfIssuer)))", + "validates": [ + "Each credential issuer account exists in ledger" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfDomainID", + "location": "PermissionedDomainSet::preclaim", + "validated_by": "ctx.view.read(keylet::permissionedDomain(ctx.tx.getFieldH256(sfDomainID)))", + "validates": [ + "Permissioned domain entry exists for given DomainID" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfDomainID ownership", + "location": "PermissionedDomainSet::preclaim", + "validated_by": "sleDomain->getAccountID(sfOwner) != account", + "validates": [ + "Account is owner of the permissioned domain" + ], + "validation_type": "business_logic|ownership" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp.ai.md b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp.ai.md new file mode 100644 index 0000000000..e00f5baddd --- /dev/null +++ b/src/libxrpl/tx/transactors/permissioned_domain/PermissionedDomainSet.cpp.ai.md @@ -0,0 +1,37 @@ +# PermissionedDomainSet.cpp + +## Role and Purpose + +`PermissionedDomainSet` is the transactor responsible for both creating new Permissioned Domain objects and modifying existing ones on the XRP Ledger. A Permissioned Domain is a named access-control construct that specifies a list of accepted credential types — each identified by an issuer account and a `CredentialType` blob — that define which credentials grant access to the domain. This file implements the full lifecycle of the "set" operation: feature gating, stateless field validation, stateful ledger consistency checks, and final ledger mutation. + +## Transaction Duality: Create vs. Modify + +The most architecturally significant aspect of this transactor is that a single transaction type handles both creation and update. The discriminator is the optional `sfDomainID` field. When it is absent, `doApply()` creates a new domain object; when it is present, the existing domain is fetched and its `sfAcceptedCredentials` array is replaced wholesale. There is no partial update — callers must always submit the complete desired credential list. + +This design choice avoids a separate `PermissionedDomainUpdate` transaction type at the cost of slightly more complex validation, since `sfDomainID` is optional and its presence or absence must be handled consistently across `preflight`, `preclaim`, and `doApply`. + +## Validation Pipeline + +`checkExtraFeatures` acts as an early gate, returning false unless the `featureCredentials` amendment is active. The XRPL transactor framework calls this before anything else, preventing the transaction from even reaching preflight on networks that haven't enabled the feature. + +`preflight` is purely stateless and validates the transaction's fields in isolation. It delegates the bulk of credential array validation to `credentials::checkArray`, which enforces that the array is non-empty, does not exceed `maxPermissionedDomainCredentialsArraySize` (10 entries), each entry has a valid non-empty `sfCredentialType` within the allowed length, and there are no duplicate `(issuer, credentialType)` pairs (detected via a hash-set on `sha512Half(issuer, credentialType)`). The only additional check `preflight` adds is rejecting a `sfDomainID` of `beast::zero` — a structurally valid but semantically nonsensical identifier. + +`preclaim` performs all ledger-state-dependent checks. It verifies each credential issuer account actually exists in the current ledger view (returning `tecNO_ISSUER` if not), and when `sfDomainID` is present, it confirms both that the domain object exists (`tecNO_ENTRY`) and that the submitting account owns it (`tecNO_PERMISSION`). The account existence check for `sfAccount` itself returns `tefINTERNAL` — a code indicating an impossible condition that should have been caught upstream, hence the `LCOV_EXCL_LINE` annotations. + +## Applying the Transaction + +`doApply()` begins by sorting the submitted credentials through `credentials::makeSorted`, which returns a `std::set>`. Using an ordered set provides canonical, deterministic ordering for the `sfAcceptedCredentials` array stored in the ledger, making the ledger entry independent of submission order. The sorted pairs are then serialized into an `STArray` of `sfCredential` inner objects. + +For **modifications**, the flow is minimal: peek the existing `SLE`, replace its `sfAcceptedCredentials` field in-place, and call `view().update()`. No reserve check is needed because the object already exists and is already counted against the owner's reserve. + +For **creation**, the flow is richer. A reserve check ensures the account's XRP balance covers `fees().accountReserve(ownerCount + 1)` before any state is mutated, returning `tecINSUFFICIENT_RESERVE` if not. The domain's `Keylet` is derived from `keylet::permissionedDomain(account, sequence)` — using the transaction's `sfSequence` as the unique disambiguator, which ensures each domain gets a globally unique, collision-resistant identifier tied to the submitter and their transaction sequence. The new `SLE` is populated with the owner account, sequence number, and sorted credentials, inserted into the account's owner directory via `view().dirInsert()`, and finally `adjustOwnerCount()` increments the owner's object count to reflect the new reserve obligation. + +## Error Handling and Invariants + +The `tefINTERNAL` and `tecDIR_FULL` branches are all marked `LCOV_EXCL_LINE` because they represent conditions the framework guarantees won't occur under normal operation (the account was verified to exist in `preclaim`, and owner directories are extremely rarely truly full). This is a deliberate defensiveness pattern: keep the invariant checks in `doApply` to catch any future code path that bypasses `preclaim`, but don't burden test coverage requirements with untriggerable branches. + +The separation between `tecNO_ISSUER` (checked at claim time with ledger access) and the `credentials::checkArray` structural checks (done at preflight without ledger access) follows the general XRPL transactor principle: fail cheap and stateless first, then fail with full ledger context. + +## Relationship to Sibling Files + +`PermissionedDomainDelete.cpp` in the same directory is the counterpart transactor. It handles the teardown: removes the domain from the owner directory, calls `adjustOwnerCount(..., -1, ...)` to release the reserve slot, and erases the `SLE`. The asymmetry worth noting is that `PermissionedDomainSet` is responsible for both creation and update, while delete is isolated in its own transactor — a typical XRPL convention where "set" covers the full write lifecycle and "delete" is a separate, explicit act. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp.ai.json b/src/libxrpl/tx/transactors/system/Batch.cpp.ai.json new file mode 100644 index 0000000000..637e534ce2 --- /dev/null +++ b/src/libxrpl/tx/transactors/system/Batch.cpp.ai.json @@ -0,0 +1,218 @@ +{ + "args": [ + { + "lineno": 32, + "name": "view" + }, + { + "lineno": 32, + "name": "tx" + }, + { + "lineno": 120, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Batch::preflight", + "Batch::calculateBaseFee", + "xrpl::calculateBaseFee (for inner txns)", + "Transactor::calculateBaseFee" + ], + "entry_point": "Batch::preflight", + "purpose": "Performs initial stateless validation of a Batch transaction, including structure, field presence, and fee calculation.", + "validation_points": [ + "Batch::preflight (checks for required fields, structure, and calls calculateBaseFee for fee validation)", + "Batch::calculateBaseFee (validates batch structure, inner txns, signers, and fee overflows)" + ] + }, + { + "call_chain": [ + "Batch::preflightSigValidated", + "Batch::checkSign" + ], + "entry_point": "Batch::preflightSigValidated", + "purpose": "Performs signature validation after stateless checks.", + "validation_points": [ + "Batch::checkSign (validates signatures for batch and inner transactions)" + ] + }, + { + "call_chain": [ + "Batch::doApply", + "xrpl::apply (for each inner transaction)" + ], + "entry_point": "Batch::doApply", + "purpose": "Applies the batch transaction and all inner transactions to the ledger.", + "validation_points": [ + "Assumes previous validation; may re-check invariants before applying." + ] + } + ], + "data_flows": [ + { + "field": "sfRawTransactions", + "flow": [ + "STTx (outer batch)", + "Batch::calculateBaseFee (tx.getFieldArray(sfRawTransactions))", + "Loop over inner txns: STTx{std::move(txn)}", + "xrpl::calculateBaseFee (for each inner STTx)" + ], + "origin": "STTx (outer batch transaction)", + "transformations": [ + "Extracted as array", + "Each element converted to STTx", + "Each inner STTx validated for type and fee" + ], + "validated_at": "Batch::calculateBaseFee (checks for maxBatchTxCount, inner batch type, fee overflow)" + }, + { + "field": "sfBatchSigners", + "flow": [ + "STTx (outer batch)", + "Batch::calculateBaseFee (tx.getFieldArray(sfBatchSigners))", + "Loop over signers: count signatures" + ], + "origin": "STTx (outer batch transaction)", + "transformations": [ + "Extracted as array", + "Each element checked for sfTxnSignature or sfSigners", + "Signer count incremented" + ], + "validated_at": "Batch::calculateBaseFee (checks for maxBatchTxCount, fee overflow for signers)" + }, + { + "field": "sfTxnSignature / sfSigners (in BatchSigners)", + "flow": [ + "STObject (signer)", + "Batch::calculateBaseFee (checks presence of sfTxnSignature or sfSigners)", + "Signer count incremented accordingly" + ], + "origin": "Each STObject in sfBatchSigners array", + "transformations": [ + "Counted for fee calculation" + ], + "validated_at": "Batch::calculateBaseFee (signer count overflow check)" + }, + { + "field": "fee", + "flow": [ + "view.fees().base", + "Transactor::calculateBaseFee (for batch)", + "Batch::calculateBaseFee (for batch, inner txns, signers)", + "Summed to total batch fee" + ], + "origin": "Ledger fees (view.fees().base), STTx fee fields", + "transformations": [ + "Summed for batch, inner txns, and signers", + "Overflow checked at each step" + ], + "validated_at": "Batch::calculateBaseFee (overflow checks, max limits)" + } + ], + "description": "Implements logic for batch transaction processing in the XRPL, including fee calculation, preflight validation, signature checks, and application of batch transactions.", + "false_positive_patterns": [ + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/system/Batch.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 32, + "name": "Batch::calculateBaseFee" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 120, + "name": "Batch::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 134, + "name": "Batch::preflight" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 282, + "name": "Batch::preflightSigValidated" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 349, + "name": "Batch::checkSign" + }, + { + "args": [], + "lineno": 372, + "name": "Batch::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "Batch transaction logic is likely tested in integration and unit tests under the transaction and batch processing test suites. Look for files like 'Batch_test.cpp', 'Transactor_test.cpp', or 'ApplyTx_test.cpp' in the test directory. The code contains LCOV_EXCL_START/STOP blocks around error/overflow conditions, indicating these are not covered by standard tests (e.g., overflow, inner batch detection, excessive array sizes). Thus, edge cases such as fee overflows, inner batch transactions, and excessive signers/transactions may not be fully covered by automated tests.", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/system/Batch.cpp.ai.md b/src/libxrpl/tx/transactors/system/Batch.cpp.ai.md new file mode 100644 index 0000000000..b351a4ea23 --- /dev/null +++ b/src/libxrpl/tx/transactors/system/Batch.cpp.ai.md @@ -0,0 +1,52 @@ +# `Batch.cpp` — Batch Transaction Transactor + +`Batch.cpp` implements the transactor for `ttBATCH`, XRPL's mechanism for bundling multiple inner transactions into a single outer transaction with a defined execution policy. The file lives in the "system" transactor subdirectory alongside `Change`, `LedgerStateFix`, and `TicketCreate` — transactors that operate at a protocol-infrastructure level rather than against ordinary user accounts. + +## Why Batches Exist + +Without batches, submitting several related transactions requires coordination across ledger closes, each carrying independent fees and each risking partial execution. A batch lets a group of accounts (or one account with many transactions) express "apply all of these, or none" (or other policies) as a single atomic unit with a single combined fee. + +## Execution Policies and the `doApply` Split + +`Batch::doApply()` returns `tesSUCCESS` unconditionally and does nothing else. This is intentional: the outer batch transaction only needs to land in the ledger and consume its fee. The actual execution of inner transactions is deferred to `applyBatchTransactions()` in `apply.cpp`, which is called by `applyTransaction()` after `doApply` completes successfully. + +`applyBatchTransactions` creates a nested `OpenView` called `wholeBatchView`. Each inner transaction runs inside its own further-nested `perTxBatchView`, and successful changes are promoted to `wholeBatchView` one at a time. Only if at least one inner transaction succeeds is `wholeBatchView` merged back into the primary ledger view. The four execution policies — whose flag bits are checked with `std::popcount` in `preflight` to guarantee exactly one is active — map directly to control flow in that loop: + +- **`tfAllOrNothing`**: returns `false` (discards `wholeBatchView`) on the first inner failure. +- **`tfUntilFailure`**: breaks on the first failure but keeps all prior successes. +- **`tfOnlyOne`**: breaks immediately after the first success, applying only that transaction. +- **`tfIndependent`**: runs every inner transaction regardless of individual outcomes; all successes commit. + +## Fee Architecture in `calculateBaseFee` + +The fee formula is: `batchBase + Σ(inner tx fees) + signerCount × view.fees().base`, where `batchBase` itself equals `view.fees().base + Transactor::calculateBaseFee(view, tx)`. The extra `view.fees().base` per batch signer reflects the cost of verifying each multi-party batch signature — one base unit per key (whether the signer used a single signature or multi-sign, the count is expanded accordingly by examining `sfSigners` sub-arrays). + +Every intermediate addition in `calculateBaseFee` is preceded by an explicit overflow check against `std::numeric_limits::max()`. These guards are wrapped in `LCOV_EXCL_START/STOP` because they are unreachable under any valid transaction structure — `preflight` already enforces that array counts and structures are within limits before `calculateBaseFee` would ever be called with malformed data. They exist purely as defense in depth. + +## `preflight`: Structural Validation + +`preflight` is a dense structural validator that runs before any state-aware checks. Its key decisions: + +**Inner transaction authentication model**: Inner transactions must have an empty `sfSigningPubKey`, no `sfTxnSignature`, and no `sfSigners`. They do not self-authenticate; the outer account signs the outer transaction, and batch signers (other accounts whose inner transactions are included) sign a separate batch payload. `preflight` enforces this by rejecting any inner transaction that carries conventional signature fields. The optional `sfCounterpartySignature` field is treated the same way — if present, it must not contain any signature material. + +**Zero-fee requirement**: Every inner transaction must carry a fee of exactly zero XRP. The batch's combined fee (computed by `calculateBaseFee`) covers the cost of all inner transactions. + +**Duplicate and nesting prevention**: A `std::unordered_set` tracks inner transaction hashes; duplicates return `temREDUNDANT`. Nested `ttBATCH` inner transactions are explicitly rejected with `temINVALID`. A compile-time `disabledTxTypes` array in the header blocks vault (`ttVAULT_*`) and loan (`ttLOAN_*`) transaction types entirely — these have multi-step state machines (deposit, withdraw, clawback) whose invariants would be difficult to reason about under batch atomicity. + +**Sequence integrity**: Every inner transaction must carry exactly one of `sfSequence` (nonzero) or `sfTicketSequence` — both present or both absent is invalid. For `tfAllOrNothing` and `tfUntilFailure` modes, duplicate sequence or ticket values across inner transactions from the same account are detected and rejected at this phase. The `tfIndependent` and `tfOnlyOne` modes relax this constraint because partial success is acceptable — two inner transactions from the same account consuming the same sequence slot can coexist when only one might actually execute. + +**Inner preflight recursion**: Each inner transaction has `xrpl::preflight` called on it with `tapBATCH` and the outer batch's transaction ID as `parentBatchId`. This propagates the full normal preflight pipeline (feature checks, flag validation, field sanity) to every inner transaction before the outer batch is accepted. + +## `preflightSigValidated`: Signer Authorization + +This runs after the outer transaction's own signature is verified by the framework (the `invokePreflight` template in `Transactor.h` calls `preflight2` for outer signature verification before calling `preflightSigValidated`). The method builds `requiredSigners` — the set of all inner transaction account IDs that differ from the outer account, plus counterparty accounts — and then reconciles that set against the `sfBatchSigners` array. + +The reconciliation is bidirectional: each batch signer is removed from `requiredSigners` as it appears; any signer not in `requiredSigners` is an extra unknown signer (`temBAD_SIGNER`). After the loop, `requiredSigners` must be empty — all inner accounts must be accounted for. The outer account is explicitly excluded from `sfBatchSigners` (returning `temBAD_SIGNER` if found there) because its authorization is already captured by the outer transaction signature. Finally, `ctx.tx.checkBatchSign(ctx.rules)` cryptographically verifies the batch signature payload produced by `serializeBatch()` in `protocol/Batch.h`, which serializes the batch flags and the ordered list of inner transaction IDs. + +## `checkSign` + +Chains `Transactor::checkSign` (validates the outer transaction's own signature or multi-sign set) followed by `Transactor::checkBatchSign` (validates signatures from the `sfBatchSigners` array against the ledger-stored account public keys). Both must pass. + +## Logging Convention + +All log messages use the `BatchTrace[]` prefix, making it straightforward to correlate outer batch events with inner transaction preflight failures in production logs by filtering on a single transaction ID. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/system/Change.cpp.ai.json b/src/libxrpl/tx/transactors/system/Change.cpp.ai.json new file mode 100644 index 0000000000..35ba9f0a22 --- /dev/null +++ b/src/libxrpl/tx/transactors/system/Change.cpp.ai.json @@ -0,0 +1,420 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Transactor::invokePreflight", + "preflight0", + "ctx.tx.getAccountID(sfAccount)", + "ctx.tx.getFieldAmount(sfFee)", + "ctx.tx.getSigningPubKey() / getSignature() / isFieldPresent(sfSigners)", + "ctx.tx.getFieldU32(sfSequence) / isFieldPresent(sfPreviousTxnID)" + ], + "entry_point": "Transactor::invokePreflight", + "purpose": "Performs preflight validation for Change transactions, ensuring all required fields are present and valid before further processing.", + "validation_points": [ + "preflight0 (flags validation)", + "ctx.tx.getAccountID(sfAccount) (account validation)", + "ctx.tx.getFieldAmount(sfFee) (fee validation)", + "ctx.tx.getSigningPubKey(), getSignature(), isFieldPresent(sfSigners) (signature validation)", + "ctx.tx.getFieldU32(sfSequence), isFieldPresent(sfPreviousTxnID) (sequence validation)" + ] + }, + { + "call_chain": [ + "Change::preclaim", + "ctx.view.open()", + "ctx.tx.getTxnType()", + "ctx.tx.isFieldPresent(sfBaseFeeDrops / sfReserveBaseDrops / sfReserveIncrementDrops)", + "ctx.tx.isFieldPresent(sfBaseFee / sfReferenceFeeUnits / sfReserveBase / sfReserveIncrement)" + ], + "entry_point": "Change::preclaim", + "purpose": "Performs further validation after preflight, specifically for ttFEE transactions, checking field presence and feature flags.", + "validation_points": [ + "ctx.view.open() (ledger state validation)", + "ctx.tx.isFieldPresent(...) (field presence validation based on feature flags)" + ] + } + ], + "data_flows": [ + { + "field": "flags", + "flow": [ + "ctx.tx (input)", + "preflight0 (validation)", + "Transactor::invokePreflight (returns error if invalid)" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Checked against tfEnableAmendmentMask if featureLendingProtocol enabled" + ], + "validated_at": "preflight0" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx (input)", + "ctx.tx.getAccountID(sfAccount) (retrieval and validation)", + "Transactor::invokePreflight (returns error if not zero)" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Compared to beast::zero" + ], + "validated_at": "Transactor::invokePreflight" + }, + { + "field": "sfFee", + "flow": [ + "ctx.tx (input)", + "ctx.tx.getFieldAmount(sfFee) (retrieval and validation)", + "Transactor::invokePreflight (returns error if not native or not zero)" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Checked for native() and == beast::zero" + ], + "validated_at": "Transactor::invokePreflight" + }, + { + "field": "sfSigningPubKey / sfSignature / sfSigners", + "flow": [ + "ctx.tx (input)", + "ctx.tx.getSigningPubKey(), getSignature(), isFieldPresent(sfSigners) (retrieval and validation)", + "Transactor::invokePreflight (returns error if any present)" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Checked for presence (should be empty/not present)" + ], + "validated_at": "Transactor::invokePreflight" + }, + { + "field": "sfSequence / sfPreviousTxnID", + "flow": [ + "ctx.tx (input)", + "ctx.tx.getFieldU32(sfSequence), isFieldPresent(sfPreviousTxnID) (retrieval and validation)", + "Transactor::invokePreflight (returns error if sequence != 0 or previous txn present)" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Checked for value == 0 and absence" + ], + "validated_at": "Transactor::invokePreflight" + }, + { + "field": "sfBaseFeeDrops / sfReserveBaseDrops / sfReserveIncrementDrops", + "flow": [ + "ctx.tx (input)", + "ctx.tx.isFieldPresent(...) (checked in Change::preclaim)", + "Change::preclaim (returns error if missing or forbidden depending on featureXRPFees)" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Presence required or forbidden depending on feature flag" + ], + "validated_at": "Change::preclaim" + }, + { + "field": "sfBaseFee / sfReferenceFeeUnits / sfReserveBase / sfReserveIncrement", + "flow": [ + "ctx.tx (input)", + "ctx.tx.isFieldPresent(...) (checked in Change::preclaim)", + "Change::preclaim (returns error if missing or forbidden depending on featureXRPFees)" + ], + "origin": "Transaction input (ctx.tx)", + "transformations": [ + "Presence required or forbidden depending on feature flag" + ], + "validated_at": "Change::preclaim" + } + ], + "description": "Implements the Change transactor for the XRPL system, handling special system transactions such as amendments, fee changes, and UNL modifications. Contains logic for preflight, preclaim, and application of these system-level transactions.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "flags", + "empty", + "string", + "validation" + ], + "evidence": "preflight0 at Transactor::invokePreflight", + "issue_pattern": "Missing empty string validation for flags", + "why_false_positive": "preflight0 validates flags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.getAccountID(sfAccount) at Transactor::invokePreflight", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "ctx.tx.getAccountID(sfAccount) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfFee", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.getFieldAmount(sfFee) at Transactor::invokePreflight", + "issue_pattern": "Missing empty string validation for sfFee", + "why_false_positive": "ctx.tx.getFieldAmount(sfFee) validates sfFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSigningPubKey, sfSignature, sfSigners", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.getSigningPubKey(), ctx.tx.getSignature(), ctx.tx.isFieldPresent(sfSigners) at Transactor::invokePreflight", + "issue_pattern": "Missing empty string validation for sfSigningPubKey, sfSignature, sfSigners", + "why_false_positive": "ctx.tx.getSigningPubKey(), ctx.tx.getSignature(), ctx.tx.isFieldPresent(sfSigners) validates sfSigningPubKey, sfSignature, sfSigners for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfSequence, sfPreviousTxnID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.getFieldU32(sfSequence), ctx.tx.isFieldPresent(sfPreviousTxnID) at Transactor::invokePreflight", + "issue_pattern": "Missing empty string validation for sfSequence, sfPreviousTxnID", + "why_false_positive": "ctx.tx.getFieldU32(sfSequence), ctx.tx.isFieldPresent(sfPreviousTxnID) validates sfSequence, sfPreviousTxnID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger state (open/closed)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.open() at Change::preclaim", + "issue_pattern": "Missing empty string validation for ledger state (open/closed)", + "why_false_positive": "ctx.view.open() validates ledger state (open/closed) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBaseFeeDrops, sfReserveBaseDrops, sfReserveIncrementDrops", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.isFieldPresent at Change::preclaim (ttFEE case, featureXRPFees enabled)", + "issue_pattern": "Missing empty string validation for sfBaseFeeDrops, sfReserveBaseDrops, sfReserveIncrementDrops", + "why_false_positive": "ctx.tx.isFieldPresent validates sfBaseFeeDrops, sfReserveBaseDrops, sfReserveIncrementDrops for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBaseFee, sfReferenceFeeUnits, sfReserveBase, sfReserveIncrement", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx.isFieldPresent at Change::preclaim (ttFEE case, featureXRPFees enabled)", + "issue_pattern": "Missing empty string validation for sfBaseFee, sfReferenceFeeUnits, sfReserveBase, sfReserveIncrement", + "why_false_positive": "ctx.tx.isFieldPresent validates sfBaseFee, sfReferenceFeeUnits, sfReserveBase, sfReserveIncrement for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/system/Change.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 11, + "name": "Transactor::invokePreflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 41, + "name": "Change::preclaim" + }, + { + "args": [], + "lineno": 91, + "name": "Change::doApply" + }, + { + "args": [], + "lineno": 109, + "name": "Change::preCompute" + }, + { + "args": [], + "lineno": 115, + "name": "Change::applyAmendment" + }, + { + "args": [], + "lineno": 186, + "name": "Change::applyFee" + }, + { + "args": [], + "lineno": 221, + "name": "Change::applyUNLModify" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "Validation logic for Change transactions is typically covered in unit/integration tests for system transactions in the rippled codebase. Look for test files such as 'Change_test.cpp', 'Transactor_test.cpp', or 'SystemTransactions_test.cpp' in the 'src/test' or 'src/test/tx' directories. These should cover valid/invalid Change transactions, field presence/absence, and feature flag toggling. However, coverage gaps may exist for edge cases involving feature flag transitions (e.g., enabling/disabling featureXRPFees or featureLendingProtocol), malformed field values, or rare combinations of forbidden/required fields. Manual review of test assertions is recommended to ensure all validation branches are exercised.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl transaction processing (custom, not external framework)", + "validation_layer": "business_logic (preflight and preclaim transaction validation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "return value from preflight0 (NotTEC error code)", + "field": "flags", + "location": "Transactor::invokePreflight", + "validated_by": "preflight0", + "validates": [ + "Checks if transaction flags are allowed (0 means allow any flags, otherwise tfEnableAmendmentMask if featureLendingProtocol is enabled)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_SRC_ACCOUNT", + "field": "sfAccount", + "location": "Transactor::invokePreflight", + "validated_by": "ctx.tx.getAccountID(sfAccount)", + "validates": [ + "Checks that the source account is zero (must be a system transaction)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_FEE", + "field": "sfFee", + "location": "Transactor::invokePreflight", + "validated_by": "ctx.tx.getFieldAmount(sfFee)", + "validates": [ + "Checks that the fee is native (XRP)", + "Checks that the fee is exactly zero" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_SIGNATURE", + "field": "sfSigningPubKey, sfSignature, sfSigners", + "location": "Transactor::invokePreflight", + "validated_by": "ctx.tx.getSigningPubKey(), ctx.tx.getSignature(), ctx.tx.isFieldPresent(sfSigners)", + "validates": [ + "Checks that no signing public key is present", + "Checks that no signature is present", + "Checks that no signers field is present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_SEQUENCE", + "field": "sfSequence, sfPreviousTxnID", + "location": "Transactor::invokePreflight", + "validated_by": "ctx.tx.getFieldU32(sfSequence), ctx.tx.isFieldPresent(sfPreviousTxnID)", + "validates": [ + "Checks that sequence is zero", + "Checks that previous transaction ID is not present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "ledger state (open/closed)", + "location": "Change::preclaim", + "validated_by": "ctx.view.open()", + "validates": [ + "Checks that the transaction is not applied to an open ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfBaseFeeDrops, sfReserveBaseDrops, sfReserveIncrementDrops", + "location": "Change::preclaim (ttFEE case, featureXRPFees enabled)", + "validated_by": "ctx.tx.isFieldPresent", + "validates": [ + "Checks that these fields are present when featureXRPFees is enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfBaseFee, sfReferenceFeeUnits, sfReserveBase, sfReserveIncrement", + "location": "Change::preclaim (ttFEE case, featureXRPFees enabled)", + "validated_by": "ctx.tx.isFieldPresent", + "validates": [ + "Checks that these fields are NOT present when featureXRPFees is enabled" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/system/Change.cpp.ai.md b/src/libxrpl/tx/transactors/system/Change.cpp.ai.md new file mode 100644 index 0000000000..e7e989ce25 --- /dev/null +++ b/src/libxrpl/tx/transactors/system/Change.cpp.ai.md @@ -0,0 +1,51 @@ +# `Change.cpp` — System Pseudo-Transaction Transactor + +## Role in the System + +`Change.cpp` implements the `Change` transactor, which processes three types of *pseudo-transactions* that XRPL validators inject into ledgers during consensus: amendment activations (`ttAMENDMENT`), fee schedule updates (`ttFEE`), and Negative UNL modifications (`ttUNL_MODIFY`). Pseudo-transactions are never submitted by users — they are constructed programmatically by the consensus machinery and embedded directly into ledger proposals. As a result, they carry a fundamentally different identity from normal transactions: zero account ID, zero fee, no signature, and sequence number zero. + +The type aliases at the bottom of `Change.h` — `EnableAmendment`, `SetFee`, and `UNLModify` — all resolve to the same `Change` class, which is the idiomatic way the codebase documents which logical operation each transaction type performs without proliferating separate classes for closely related concerns. + +## Validation Architecture + +### Preflight: Template Specialization for Pseudo-Transactions + +`invokePreflight` is a full template specialization of the generic `Transactor::invokePreflight`. The specialization exists because pseudo-transactions violate nearly every validation rule that applies to normal user transactions: they must have no cryptographic signature, no signing public key, no multisig signers list, a zero fee, a zero sequence, and a source account ID of `beast::zero`. Injecting these checks into a specialization lets the generic `invokePreflight` continue enforcing normal user-transaction rules without conditionals scattered throughout. + +The flag validation in `invokePreflight` is notably pragmatic: flag mask enforcement via `tfEnableAmendmentMask` is only active when `featureLendingProtocol` is enabled. The inline comment explains this directly — adding a dedicated amendment purely to gate a flag mask check would be protocol complexity for no meaningful gain, so `featureLendingProtocol` serves as a proxy. + +### Preclaim: Ledger-State and Transaction-Type Checks + +`preclaim` rejects any `Change` transaction applied against an open ledger. Pseudo-transactions belong to closed ledgers only, and this guard enforces that boundary. The `ttFEE` case in `preclaim` also handles a field format transition driven by the `featureXRPFees` amendment. Before that feature is enabled, the fee object uses legacy integer fields (`sfBaseFee`, `sfReferenceFeeUnits`, `sfReserveBase`, `sfReserveIncrement`). After it is enabled, those are replaced by XRP-amount fields (`sfBaseFeeDrops`, `sfReserveBaseDrops`, `sfReserveIncrementDrops`). `preclaim` enforces that both sets of fields are mutually exclusive depending on which era the ledger is in, returning `temMALFORMED` or `temDISABLED` accordingly. This prevents a stale fee transaction format from silently mixing old and new fields after a feature upgrade. + +## Amendment State Machine + +`applyAmendment()` drives a three-state lifecycle recorded in the `sfAmendments` ledger object: + +1. **Gaining majority** (`tfGotMajority`): The amendment's hash is recorded in `sfMajorities` alongside the current ledger close time. The close time is the anchor used to measure the two-week majority window enforced by the `AmendmentTable`. If the amendment is not supported by this server, a warning is logged but the change proceeds — the server remains functional at this stage. + +2. **Losing majority** (`tfLostMajority`): The entry is removed from `sfMajorities`. The amendment returns to a "known but not voted in" state. + +3. **Enabling** (no flags): The hash is moved from `sfMajorities` into the `sfAmendments` vector and `AmendmentTable::enable()` is called to update the in-memory feature table. If the enabled amendment is one this server does not implement, `NetworkOPs::setAmendmentBlocked()` is invoked, which puts the server into a degraded amendment-blocked state — the server stops validating and warns operators to upgrade. + +The idempotency guards (`tefALREADY`) are carefully placed: a `tfGotMajority` on an amendment already in majority returns `tefALREADY`; a `tfLostMajority` on one that was never in majority returns `tefALREADY`; applying an amendment already in the enabled set returns `tefALREADY`. This prevents consensus replays from corrupting ledger state. + +## Fee Schedule Updates + +`applyFee()` overwrites the singleton `FeeSettings` ledger object (accessed via `keylet::fees()`). The implementation mirrors the field-era split from `preclaim`: under `featureXRPFees`, it writes the new `Drops`-suffixed fields and explicitly calls `makeFieldAbsent` to remove any legacy fields that might exist from earlier ledgers. This cleanup step ensures that the ledger object is canonical for the current era rather than carrying stale fields that could confuse fee-reading code. + +## Negative UNL Modifications + +`applyUNLModify()` implements staged changes to the Negative UNL (N-UNL), a feature allowing the network to continue reaching consensus even when a significant fraction of validators is offline. Modifications are recorded in the `NegativeUNL` ledger object and are only permitted on *flag ledgers* (every 256th ledger, enforced by `isFlagLedger`). This restriction matches the consensus protocol's amendment voting cycle. + +The operation has two modes controlled by `sfUNLModifyDisabling`: + +- **Disabling** (marking a validator for removal): recorded in `sfValidatorToDisable`. Only one validator can be staged for disabling at a time, the candidate must not already be in the negative UNL, and it cannot be the same validator currently staged for re-enabling. + +- **Re-enabling** (returning a validator to the active UNL): recorded in `sfValidatorToReEnable`. The validator must already be present in `sfDisabledValidators`, and the same uniqueness/conflict checks apply symmetrically. + +These "pending" fields (`sfValidatorToDisable`, `sfValidatorToReEnable`) function as a single-slot staging area: the transaction marks intent, and the full promotion/demotion into `sfDisabledValidators` is processed separately by ledger-closing logic. The asymmetric preconditions — disabling requires absence from N-UNL, re-enabling requires presence — enforce the logical invariant that you cannot remove what isn't there or restore what was never disabled. All validation failures in `applyUNLModify` return `tefFAILURE`, indicating server-level rejection without network-level blame. + +## Design Note: `preCompute` + +`preCompute()` asserts that `account_` is `beast::zero`. This is a defensive invariant check that fires during the apply phase to confirm the pseudo-transaction's source account was never overwritten after `preflight` validated it. It catches any future refactor that might accidentally hydrate a real account ID into a `Change` transactor context. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp.ai.json b/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp.ai.json new file mode 100644 index 0000000000..f1f7865e45 --- /dev/null +++ b/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp.ai.json @@ -0,0 +1,231 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LedgerStateFix::preflight" + ], + "entry_point": "LedgerStateFix::preflight", + "purpose": "Initial validation of transaction fields before further processing.", + "validation_points": [ + "Validates sfLedgerFixType via switch statement.", + "If FixType::nfTokenPageLink, validates presence of sfOwner via isFieldPresent." + ] + }, + { + "call_chain": [ + "LedgerStateFix::preclaim" + ], + "entry_point": "LedgerStateFix::preclaim", + "purpose": "Performs further validation after preflight, ensuring referenced objects exist.", + "validation_points": [ + "If FixType::nfTokenPageLink, validates that account for sfOwner exists via view.read(keylet::account(owner))." + ] + }, + { + "call_chain": [ + "LedgerStateFix::doApply" + ], + "entry_point": "LedgerStateFix::doApply", + "purpose": "Executes the fix operation, performing the actual ledger state change.", + "validation_points": [ + "If FixType::nfTokenPageLink, validates/repairs NFT directory links for sfOwner via nft::repairNFTokenDirectoryLinks." + ] + } + ], + "data_flows": [ + { + "field": "sfLedgerFixType", + "flow": [ + "ctx.tx[sfLedgerFixType] in preflight", + "ctx.tx[sfLedgerFixType] in preclaim", + "ctx_.tx[sfLedgerFixType] in doApply" + ], + "origin": "ctx.tx (STTx) - transaction input", + "transformations": [ + "Read as enum FixType", + "Used in switch/case and if statements to select code path" + ], + "validated_at": "preflight (switch statement)" + }, + { + "field": "sfOwner", + "flow": [ + "ctx.tx.isFieldPresent(sfOwner) in preflight", + "ctx.tx[sfOwner] in preclaim", + "ctx_.tx[sfOwner] in doApply" + ], + "origin": "ctx.tx (STTx) - transaction input", + "transformations": [ + "Checked for presence (isFieldPresent)", + "Extracted as AccountID", + "Used as argument to keylet::account and nft::repairNFTokenDirectoryLinks" + ], + "validated_at": "preflight (isFieldPresent), preclaim (view.read(keylet::account(owner))), doApply (nft::repairNFTokenDirectoryLinks)" + } + ], + "description": "Implements the LedgerStateFix transactor, which applies specific ledger state fixes (such as repairing NFToken directory links) based on the transaction's FixType.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLedgerFixType", + "empty", + "string", + "validation" + ], + "evidence": "explicit switch statement at LedgerStateFix::preflight", + "issue_pattern": "Missing empty string validation for sfLedgerFixType", + "why_false_positive": "explicit switch statement validates sfLedgerFixType for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwner", + "empty", + "string", + "validation" + ], + "evidence": "isFieldPresent method at LedgerStateFix::preflight", + "issue_pattern": "Missing empty string validation for sfOwner", + "why_false_positive": "isFieldPresent method validates sfOwner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwner", + "empty", + "string", + "validation" + ], + "evidence": "keylet::account(owner) and view.read at LedgerStateFix::preclaim", + "issue_pattern": "Missing empty string validation for sfOwner", + "why_false_positive": "keylet::account(owner) and view.read validates sfOwner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfOwner", + "empty", + "string", + "validation" + ], + "evidence": "nft::repairNFTokenDirectoryLinks at LedgerStateFix::doApply", + "issue_pattern": "Missing empty string validation for sfOwner", + "why_false_positive": "nft::repairNFTokenDirectoryLinks validates sfOwner for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 7, + "name": "LedgerStateFix::preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 22, + "name": "LedgerStateFix::calculateBaseFee" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 31, + "name": "LedgerStateFix::preclaim" + }, + { + "args": [], + "lineno": 44, + "name": "LedgerStateFix::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by unit/integration tests for LedgerStateFix transactions, especially for FixType::nfTokenPageLink. Tests should cover: (1) missing/invalid sfLedgerFixType, (2) missing sfOwner, (3) non-existent sfOwner account, (4) successful and failed repairNFTokenDirectoryLinks. However, coverage gaps may exist for: (a) unhandled FixTypes (default case), (b) error paths marked LCOV_EXCL_LINE (tecINTERNAL), (c) edge cases in nft::repairNFTokenDirectoryLinks. Test files would likely be found in the rippled/test or xrplf-rippled/test directories, possibly named LedgerStateFix_test.cpp or similar. No explicit test references are present in this file.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in transaction processing (no external framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "tefINVALID_LEDGER_FIX_TYPE", + "field": "sfLedgerFixType", + "location": "LedgerStateFix::preflight", + "validated_by": "explicit switch statement", + "validates": [ + "Checks that sfLedgerFixType is a recognized FixType (currently only FixType::nfTokenPageLink is allowed)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID", + "field": "sfOwner", + "location": "LedgerStateFix::preflight", + "validated_by": "isFieldPresent method", + "validates": [ + "Checks that sfOwner field is present in the transaction when FixType is nfTokenPageLink" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND", + "field": "sfOwner", + "location": "LedgerStateFix::preclaim", + "validated_by": "keylet::account(owner) and view.read", + "validates": [ + "Checks that the account specified by sfOwner exists in the ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "tecFAILED_PROCESSING", + "field": "sfOwner", + "location": "LedgerStateFix::doApply", + "validated_by": "nft::repairNFTokenDirectoryLinks", + "validates": [ + "Checks that the repair operation on the NFToken directory links for the owner succeeds" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp.ai.md b/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp.ai.md new file mode 100644 index 0000000000..fe224162eb --- /dev/null +++ b/src/libxrpl/tx/transactors/system/LedgerStateFix.cpp.ai.md @@ -0,0 +1,53 @@ +# `LedgerStateFix.cpp` — Surgical Ledger State Repair Transactor + +`LedgerStateFix` is a privileged maintenance transactor introduced under the `fixNFTokenPageLinks` amendment (transaction type `ttLEDGER_STATE_FIX`, code 53). Its purpose is narrow but important: it provides a sanctioned on-chain mechanism for correcting corrupted or inconsistent ledger state that cannot be self-healed through normal transaction execution. Rather than introducing a bespoke transaction type for each class of corruption, the design embeds a `sfLedgerFixType` discriminant in the transaction, making the transactor an extensible dispatch table for future repair operations. + +## Transaction Structure and Dispatch + +The transactor's class hierarchy inherits from `Transactor` in the standard way, with the `FixType` enum defined directly on the class: + +```cpp +enum FixType : std::uint16_t { + nfTokenPageLink = 1, +}; +``` + +Currently only one variant exists. Every method — `preflight`, `preclaim`, and `doApply` — switches or branches on `sfLedgerFixType`. This architecture means adding a new fix type requires only a new `FixType` constant and a matching case in each phase; no structural changes to the transactor are needed. + +## Three-Phase Validation and the LCOV Guards + +The standard XRPL transactor lifecycle runs `preflight` (pure validation against the transaction itself), `preclaim` (read-only ledger inspection), then `doApply` (ledger mutation). `LedgerStateFix` uses all three phases defensively: + +**`preflight`** validates the `sfLedgerFixType` via a `switch` statement. An unrecognized type returns `tefINVALID_LEDGER_FIX_TYPE` immediately — the `tef` prefix means the transaction is rejected before even entering the engine queue. For `nfTokenPageLink`, it additionally confirms that `sfOwner` is present via `isFieldPresent`; without a target account the repair has no meaning. + +**`preclaim`** uses the read-only `view` to confirm that the account identified by `sfOwner` actually exists (`keylet::account(owner)`). If the account is absent from the ledger, it returns `tecOBJECT_NOT_FOUND` and the transaction fails without applying. + +**`doApply`** delegates to `nft::repairNFTokenDirectoryLinks`, interpreting a `false` return as `tecFAILED_PROCESSING`. + +Both `preclaim` and `doApply` include unreachable `return tecINTERNAL` paths annotated `// LCOV_EXCL_LINE`. These exist because the compiler cannot see that `preflight` guarantees only valid `FixType` values reach these methods. The annotation signals to coverage tooling that these lines are intentionally excluded from metrics — they are defensive code, not dead logic. + +## Fee Model: Owner Reserve, Not Base Fee + +`calculateBaseFee()` does not return the network's reference fee. Instead, it forwards to `calculateOwnerReserveFee()`: + +```cpp +return calculateOwnerReserveFee(view, tx); +``` + +This is the same pricing strategy used by `AccountDelete` and `AMMCreate`. The owner reserve fee (one reserve increment) is orders of magnitude larger than the standard base fee. The design rationale is economic deterrence: a repair transaction that finds nothing to fix still costs the submitter a full reserve increment, so there is no incentive to probe the ledger speculatively. The fee is non-refundable regardless of whether `repairNFTokenDirectoryLinks` makes any changes. This cost also signals intentionality — operators submit this transaction only when they have strong reason to believe an account's NFToken directory is corrupt. + +## What `repairNFTokenDirectoryLinks` Actually Fixes + +The underlying repair function in `NFTokenHelpers.cpp` traverses the doubly-linked list of `NFTokenPage` ledger objects for the given account. NFToken pages are keyed by a canonical range marker derived from the highest token ID they contain; a known bug could produce pages whose `sfPreviousPageMin` or `sfNextPageMin` links were incorrect or whose final page carried the wrong key. + +The repair walks from the first page to the last, fixing three categories of corruption: + +1. **Stale back-pointer on the first page** — the head of the list must not have a `sfPreviousPageMin`; if it does, that field is removed. +2. **Broken forward/backward links between adjacent pages** — each pair of consecutive pages has their `sfNextPageMin` and `sfPreviousPageMin` set to point at each other, correcting any mismatch. +3. **Miskeyed final page** — if the actual last page in the directory does not carry the canonical `nftpage_max(owner)` key (the maximum possible key for the account's range), the function allocates a new SLE at the correct key, copies the token array and prev-link into it, patches the preceding page's forward pointer, erases the old page, and inserts the new one. + +The function returns `true` if any repair was performed and `false` otherwise. A `false` return does not indicate an error from the function's perspective, but `doApply` maps it to `tecFAILED_PROCESSING` — meaning the submitter pays the owner reserve fee while achieving no change to the ledger. This reflects the fact that submitting a fix for an account whose directory is already consistent is not an internal error, but is nonetheless a failed operation from the transaction's point of view. + +## Relationship to the Amendment System + +The auto-generated `protocol_autogen/transactions/LedgerStateFix.h` records that the transaction is gated on the `fixNFTokenPageLinks` amendment. Until that amendment activates on a network, nodes will reject the transaction type entirely at the protocol layer, before `preflight` is even reached. This means the transactor implementation itself contains no amendment guard — that responsibility is handled upstream by the transaction routing infrastructure. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp.ai.json b/src/libxrpl/tx/transactors/system/TicketCreate.cpp.ai.json new file mode 100644 index 0000000000..8edb183117 --- /dev/null +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp.ai.json @@ -0,0 +1,392 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TicketCreate::preflight", + "TicketCreate::preclaim", + "TicketCreate::doApply" + ], + "entry_point": "TicketCreate::operator() (via Transactor::operator())", + "purpose": "Processes a TicketCreate transaction, validating and applying it to the ledger.", + "validation_points": [ + "TicketCreate::preflight (sfTicketCount range check)", + "TicketCreate::preclaim (sfAccount existence, ticket count business logic)", + "TicketCreate::doApply (sfAccount existence, reserve calculation)" + ] + } + ], + "data_flows": [ + { + "field": "sfTicketCount", + "flow": [ + "Transaction JSON input", + "ctx.tx[sfTicketCount] in preflight", + "ctx.tx[sfTicketCount] in preclaim", + "ctx_.tx[sfTicketCount] in doApply" + ], + "origin": "ctx.tx[sfTicketCount] (transaction input)", + "transformations": [ + "Range checked in preflight", + "Used in business logic check in preclaim (addedTickets)", + "Used in reserve calculation in doApply", + "Used to determine number of tickets created" + ], + "validated_at": "preflight (range), preclaim (business logic), doApply (reserve)" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction JSON input", + "ctx.tx[sfAccount] in preclaim", + "keylet::account(id) in preclaim", + "view().peek(keylet::account(account_)) in doApply" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Checked for existence in preclaim", + "Used to fetch account root SLE", + "Used as owner of created tickets" + ], + "validated_at": "preclaim (existence), doApply (existence)" + }, + { + "field": "sfOwnerCount", + "flow": [ + "sleAccountRoot->getFieldU32(sfOwnerCount) in doApply", + "Used in reserve calculation" + ], + "origin": "AccountRoot SLE", + "transformations": [ + "Added to ticketCount to compute new reserve requirement" + ], + "validated_at": "doApply (reserve calculation)" + }, + { + "field": "preFeeBalance_", + "flow": [ + "preFeeBalance_ in doApply", + "Compared to required reserve" + ], + "origin": "Context (pre-transaction account balance)", + "transformations": [ + "Compared against computed reserve to validate sufficient funds" + ], + "validated_at": "doApply (reserve calculation)" + }, + { + "field": "sfSequence", + "flow": [ + "Used in doApply to sanity check sequence increment", + "Used to determine firstTicketSeq" + ], + "origin": "ctx_.tx[sfSequence] (transaction input), sleAccountRoot[sfSequence]", + "transformations": [ + "Checked for correct increment (txSeq == firstTicketSeq - 1)" + ], + "validated_at": "doApply (sanity check)" + } + ], + "description": "Implements the TicketCreate transaction logic for the XRPL, including preflight checks, preclaim validation, and application of the transaction to the ledger, handling ticket creation and reserve requirements.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTicketCount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTicketCount validated by Custom validation in transaction processing (xrpl core logic)", + "issue_pattern": "Missing validation for sfTicketCount", + "why_false_positive": "Custom validation in transaction processing (xrpl core logic) validates sfTicketCount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAccount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfAccount validated by Custom validation in transaction processing (xrpl core logic)", + "issue_pattern": "Missing validation for sfAccount", + "why_false_positive": "Custom validation in transaction processing (xrpl core logic) validates sfAccount automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfSequence", + "validation", + "missing", + "check" + ], + "evidence": "Field sfSequence validated by Custom validation in transaction processing (xrpl core logic)", + "issue_pattern": "Missing validation for sfSequence", + "why_false_positive": "Custom validation in transaction processing (xrpl core logic) validates sfSequence automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfOwnerCount", + "validation", + "missing", + "check" + ], + "evidence": "Field sfOwnerCount validated by Custom validation in transaction processing (xrpl core logic)", + "issue_pattern": "Missing validation for sfOwnerCount", + "why_false_positive": "Custom validation in transaction processing (xrpl core logic) validates sfOwnerCount automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTicketCount", + "empty", + "string", + "validation" + ], + "evidence": "explicit range check at preflight", + "issue_pattern": "Missing empty string validation for sfTicketCount", + "why_false_positive": "explicit range check validates sfTicketCount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfTicketCount", + "range", + "bounds", + "validation" + ], + "evidence": "explicit range check at preflight", + "issue_pattern": "Missing range validation for sfTicketCount", + "why_false_positive": "explicit range check validates sfTicketCount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "account existence check at preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "account existence check validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTicketCount, sfTicketCount (current), consumedTickets", + "empty", + "string", + "validation" + ], + "evidence": "business logic check at preclaim", + "issue_pattern": "Missing empty string validation for sfTicketCount, sfTicketCount (current), consumedTickets", + "why_false_positive": "business logic check validates sfTicketCount, sfTicketCount (current), consumedTickets for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "account existence check at doApply", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "account existence check validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTicketCount, sfOwnerCount, preFeeBalance_", + "empty", + "string", + "validation" + ], + "evidence": "reserve calculation at doApply", + "issue_pattern": "Missing empty string validation for sfTicketCount, sfOwnerCount, preFeeBalance_", + "why_false_positive": "reserve calculation validates sfTicketCount, sfOwnerCount, preFeeBalance_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfSequence", + "empty", + "string", + "validation" + ], + "evidence": "sanity check at doApply", + "issue_pattern": "Missing empty string validation for sfSequence", + "why_false_positive": "sanity check validates sfSequence for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/system/TicketCreate.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "TicketCreate::makeTxConsequences" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 14, + "name": "TicketCreate::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 22, + "name": "TicketCreate::preclaim" + }, + { + "args": [], + "lineno": 44, + "name": "TicketCreate::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "TicketCreate is typically tested in unit/integration tests under the transaction processing suite, e.g., 'Ticket_test.cpp', 'Transactor_test.cpp', or 'TicketCreate_test.cpp' (if present). These tests cover valid/invalid ticket counts, account existence, reserve checks, and directory full conditions. However, some internal error paths (e.g., tefINTERNAL, LCOV_EXCL_LINE) may not be directly tested. Edge cases like unsigned underflow are commented as impossible, so may not be covered. Test coverage is generally good for business logic and validation, but some rare internal errors may lack explicit tests.", + "validation_architecture": { + "auto_validated_fields": [ + "sfTicketCount", + "sfAccount", + "sfSequence", + "sfOwnerCount" + ], + "framework": "Custom validation in transaction processing (xrpl core logic)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID_COUNT", + "field": "sfTicketCount", + "location": "preflight", + "validated_by": "explicit range check", + "validates": [ + "Ensures sfTicketCount is >= minValidCount and <= maxValidCount" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "terNO_ACCOUNT", + "field": "sfAccount", + "location": "preclaim", + "validated_by": "account existence check", + "validates": [ + "Ensures the account referenced by sfAccount exists in the ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecDIR_FULL", + "field": "sfTicketCount, sfTicketCount (current), consumedTickets", + "location": "preclaim", + "validated_by": "business logic check", + "validates": [ + "Ensures the account does not exceed maxTicketThreshold after ticket creation" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sfAccount", + "location": "doApply", + "validated_by": "account existence check", + "validates": [ + "Ensures the account exists before applying the transaction" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINSUFFICIENT_RESERVE", + "field": "sfTicketCount, sfOwnerCount, preFeeBalance_", + "location": "doApply", + "validated_by": "reserve calculation", + "validates": [ + "Ensures the account has enough XRP to cover the reserve for the new tickets" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "not shown (incomplete code, but would return error)", + "field": "sfSequence", + "location": "doApply", + "validated_by": "sanity check", + "validates": [ + "Ensures the transaction sequence matches the expected account sequence" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/system/TicketCreate.cpp.ai.md b/src/libxrpl/tx/transactors/system/TicketCreate.cpp.ai.md new file mode 100644 index 0000000000..036a5e6b8e --- /dev/null +++ b/src/libxrpl/tx/transactors/system/TicketCreate.cpp.ai.md @@ -0,0 +1,67 @@ +# `TicketCreate.cpp` — Transactor for Batch Sequence-Number Reservation + +## Role in the System + +`TicketCreate` implements the XRP Ledger transaction that pre-reserves one or more sequence numbers by creating `ltTICKET` ledger objects. Tickets exist so accounts can submit transactions out of normal sequence order: a future transaction may reference a ticket rather than the account's current `sfSequence`, enabling parallel or advance-prepared signing workflows. This file contains the three-phase transactor logic — `preflight`, `preclaim`, and `doApply` — plus the custom `TxConsequences` factory that communicates the true sequence consumption to the transaction queue. + +## The Three-Phase Transactor Pipeline + +The XRPL transactor framework separates validation into distinct phases of increasing cost. `TicketCreate` overrides all three. + +**`makeTxConsequences`** uses the `Custom` factory variant and passes `ctx.tx[sfTicketCount]` as the `sequencesConsumed` argument to `TxConsequences`. The default for a normal transaction is one consumed sequence, but a `TicketCreate` burns `ticketCount` sequence numbers in a single shot. Reporting this accurately to the `TxQ` prevents the queue from misjudging how much sequence space a pending transaction consumes, which would break ordering guarantees for subsequent transactions submitted by the same account. + +**`preflight`** performs a stateless range check: `sfTicketCount` must be at least `minValidCount` (1) and at most `maxValidCount` (250). The 250 ceiling is deliberately empirical: the header's comment records that on a 2018 MacBook Pro in release mode, creating 250 tickets averaged 1.21 ms, matching the 1.25 ms average of a compute-intensive three-path Payment. The cap is therefore a CPU budget decision, not an arbitrary ledger policy. + +**`preclaim`** enforces the per-account ticket ceiling before any ledger mutations occur. The logic is: + +```cpp +curTicketCount + addedTickets - consumedTickets > maxTicketThreshold → tecDIR_FULL +``` + +The `consumedTickets` term equals 1 when the `TicketCreate` transaction itself is submitted via a ticket (the `SeqProxy` is a ticket rather than a sequence number). This one-ticket subtraction is not accidental: if an account at the 250-ticket limit submits a `TicketCreate` via a ticket to add one more, the net change is zero and the transaction should succeed. Without this correction the check would falsely reject it. The comment in the code notes that unsigned underflow cannot happen because `addedTickets >= 1` and `consumedTickets <= 1`. + +## `doApply` — Ledger Mutations + +**Reserve check.** The function first computes the reserve required after the new tickets are added: + +```cpp +XRPAmount const reserve = + view().fees().accountReserve(sleAccountRoot->getFieldU32(sfOwnerCount) + ticketCount); +if (preFeeBalance_ < reserve) + return tecINSUFFICIENT_RESERVE; +``` + +`preFeeBalance_` (from the `Transactor` base class) holds the account balance *before* the transaction fee is deducted. Using the pre-fee balance is a conscious design choice documented in a comment: the account is allowed to consume its reserve in order to pay the fee, but the reserve XRP for newly created tickets must actually exist prior to fee payment. Checking against the post-fee balance would produce incorrect results in that edge case. + +**Sequence anchoring.** Each ticket's `sfTicketSequence` field is set to a contiguous block starting at `firstTicketSeq = (*sleAccountRoot)[sfSequence]` — the account's sequence *after* the framework has already incremented it for this transaction. A sanity guard confirms this: + +```cpp +if (std::uint32_t const txSeq = ctx_.tx[sfSequence]; + txSeq != 0 && txSeq != (firstTicketSeq - 1)) + return tefINTERNAL; +``` + +The `txSeq != 0` condition handles the case where the `TicketCreate` was itself submitted via a ticket (sequence field is 0 in that case). This guard is marked `LCOV_EXCL_LINE` because it is a defensive invariant the framework guarantees will never fire in production — it would only trip from a bug in the transaction machinery. + +**Ticket creation loop.** For each ticket, the function allocates an `SLE` of type `ltTICKET`, sets `sfAccount` and `sfTicketSequence`, inserts it into the ledger, and registers it in the account's owner directory via `dirInsert`. The returned page number is stored in `sfOwnerNode` on the ticket SLE, enabling efficient deletion later without a full directory scan. The `tecDIR_FULL` path inside the loop is likewise excluded from coverage analysis since reaching it would require an abnormally enormous owner directory. + +**Post-loop state updates.** After all tickets are created the function: + +1. Increments `sfTicketCount` on the account root by `ticketCount`. This field mirrors the count of live tickets owned by the account and is what `preclaim` reads on future `TicketCreate` calls. +2. Calls `adjustOwnerCount(view(), sleAccountRoot, ticketCount, viewJ)` to raise `sfOwnerCount` by `ticketCount`, increasing the account's XRP reserve obligation. +3. Advances `sfSequence` to `firstTicketSeq + ticketCount`. The code explicitly notes (October 2018) that **`TicketCreate` is the only transaction in the XRPL protocol that can advance an account's `sfSequence` by more than one in a single transaction**. This is the mechanism by which the protocol "burns" those sequence numbers into reserved tickets rather than leaving gaps. + +## Failure Modes and Error Codes + +| Error | Phase | Condition | +|---|---|---| +| `temINVALID_COUNT` | preflight | `sfTicketCount` outside [1, 250] | +| `terNO_ACCOUNT` | preclaim | submitting account does not exist | +| `tecDIR_FULL` | preclaim | account would exceed 250 total tickets | +| `tecINSUFFICIENT_RESERVE` | doApply | pre-fee balance cannot cover the new reserve | +| `tecDIR_FULL` | doApply | owner directory insert fails (practically unreachable) | +| `tefINTERNAL` | doApply | account SLE missing or sequence invariant violated (both unreachable in practice) | + +## Relationship to Other Components + +`TicketCreate` depends on `keylet::ticket(account, seq)` from `Indexes.h` to derive the canonical ledger key for each ticket object, and on `keylet::ownerDir(account)` plus `dirInsert` from `DirectoryHelpers` to link tickets into the account's ownership chain. The `adjustOwnerCount` helper in `AccountRootHelpers` manages the `sfOwnerCount` field symmetrically with every other owned object type in the ledger. Ticket deletion mirrors this in reverse via `Transactor::ticketDelete`, a static helper exposed for use by `AccountDelete` when it cleans up an account's owned objects. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/Clawback.cpp.ai.json b/src/libxrpl/tx/transactors/token/Clawback.cpp.ai.json new file mode 100644 index 0000000000..05c04fec09 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/Clawback.cpp.ai.json @@ -0,0 +1,596 @@ +{ + "args": [ + { + "lineno": 13, + "name": "ctx" + }, + { + "lineno": 28, + "name": "ctx" + }, + { + "lineno": 45, + "name": "ctx" + }, + { + "lineno": 61, + "name": "ctx" + }, + { + "lineno": 61, + "name": "sleIssuer" + }, + { + "lineno": 61, + "name": "issuer" + }, + { + "lineno": 61, + "name": "holder" + }, + { + "lineno": 61, + "name": "clawAmount" + }, + { + "lineno": 93, + "name": "ctx" + }, + { + "lineno": 93, + "name": "sleIssuer" + }, + { + "lineno": 93, + "name": "issuer" + }, + { + "lineno": 93, + "name": "holder" + }, + { + "lineno": 93, + "name": "clawAmount" + }, + { + "lineno": 112, + "name": "ctx" + }, + { + "lineno": 139, + "name": "ctx" + }, + { + "lineno": 157, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Clawback::preflight", + "preflightHelper or preflightHelper" + ], + "entry_point": "Clawback::preflight", + "purpose": "Performs stateless validation of Clawback transactions before they are accepted for further processing.", + "validation_points": [ + "preflightHelper: Validates sfHolder absence, sfAmount (isXRP, <= 0), issuer != holder", + "preflightHelper: Validates featureMPTokensV1 enabled, sfHolder present, issuer != holder, amount <= max, amount > 0" + ] + }, + { + "call_chain": [ + "Clawback::preclaim", + "preclaimHelper or preclaimHelper" + ], + "entry_point": "Clawback::preclaim", + "purpose": "Performs stateful validation (ledger-dependent) to check permissions, trustlines, and balances before applying the Clawback.", + "validation_points": [ + "preclaimHelper: Checks issuer flags, trustline existence, trustline balance sign, accountHolds > 0", + "preclaimHelper: (Partial in provided code, but would check MPT issuance existence, permissions, etc.)" + ] + }, + { + "call_chain": [ + "Clawback::doApply", + "applyHelper or applyHelper" + ], + "entry_point": "Clawback::doApply", + "purpose": "Applies the Clawback operation after all validations pass.", + "validation_points": [ + "Assumes preflight and preclaim have already validated inputs; doApply focuses on state mutation." + ] + } + ], + "data_flows": [ + { + "field": "sfHolder", + "flow": [ + "ctx.tx", + "preflightHelper: ctx.tx.isFieldPresent(sfHolder)", + "preflightHelper: ctx.tx[~sfHolder]", + "Used as 'holder' in further validation" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence/absence depending on Issue/MPTIssue", + "Extracted as AccountID" + ], + "validated_at": "preflightHelper (must be absent), preflightHelper (must be present)" + }, + { + "field": "sfAmount", + "flow": [ + "ctx.tx", + "preflightHelper: ctx.tx[sfAmount]", + "preflightHelper: ctx.tx[sfAmount]", + "Used as 'clawAmount' in further validation" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for isXRP, <= 0 (Issue)", + "Checked for <= maxMPTokenAmount, <= 0 (MPTIssue)", + "Issuer extracted from amount" + ], + "validated_at": "preflightHelper, preflightHelper" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx", + "preflightHelper: ctx.tx[sfAccount]", + "preflightHelper: ctx.tx[sfAccount]", + "Used as 'issuer' in further validation" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Compared to holder for != check" + ], + "validated_at": "preflightHelper, preflightHelper" + }, + { + "field": "featureMPTokensV1", + "flow": [ + "ctx.rules", + "preflightHelper: ctx.rules.enabled(featureMPTokensV1)" + ], + "origin": "ctx.rules (network rules/feature flags)", + "transformations": [ + "Boolean check for feature enablement" + ], + "validated_at": "preflightHelper" + }, + { + "field": "issuer flags (lsfAllowTrustLineClawback, lsfNoFreeze)", + "flow": [ + "ctx.view.read(sleIssuer)", + "preclaimHelper: sleIssuer.getFieldU32(sfFlags)" + ], + "origin": "SLE issuer account (ledger state)", + "transformations": [ + "Bitmask checks for permissions" + ], + "validated_at": "preclaimHelper" + }, + { + "field": "trustline (RippleState)", + "flow": [ + "ctx.view.read(keylet::line(holder, issuer, currency))", + "preclaimHelper: sleRippleState" + ], + "origin": "Ledger (ctx.view)", + "transformations": [ + "Existence check, balance extraction" + ], + "validated_at": "preclaimHelper" + }, + { + "field": "accountHolds", + "flow": [ + "accountHolds(ctx.view, holder, currency, issuer, fhIGNORE_FREEZE, ctx.j)" + ], + "origin": "Ledger (ctx.view)", + "transformations": [ + "Returns available balance for holder" + ], + "validated_at": "preclaimHelper" + } + ], + "description": "Implements the Clawback transaction logic for XRPL tokens, including preflight, preclaim, and apply steps for both Issue and MPTIssue token types.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfHolder", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx.isFieldPresent(sfHolder)) at preflightHelper", + "issue_pattern": "Missing empty string validation for sfHolder", + "why_false_positive": "explicit check (ctx.tx.isFieldPresent(sfHolder)) validates sfHolder for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "explicit checks (isXRP, <= beast::zero) at preflightHelper", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "explicit checks (isXRP, <= beast::zero) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount vs sfAmount.issuer", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (issuer == holder) at preflightHelper", + "issue_pattern": "Missing empty string validation for sfAccount vs sfAmount.issuer", + "why_false_positive": "explicit check (issuer == holder) validates sfAccount vs sfAmount.issuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "featureMPTokensV1", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled(featureMPTokensV1) at preflightHelper", + "issue_pattern": "Missing empty string validation for featureMPTokensV1", + "why_false_positive": "ctx.rules.enabled(featureMPTokensV1) validates featureMPTokensV1 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfHolder (optional)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (auto const mptHolder = ctx.tx[~sfHolder]; if (!mptHolder)) at preflightHelper", + "issue_pattern": "Missing empty string validation for sfHolder (optional)", + "why_false_positive": "explicit check (auto const mptHolder = ctx.tx[~sfHolder]; if (!mptHolder)) validates sfHolder (optional) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount vs sfHolder", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfAccount] == *mptHolder) at preflightHelper", + "issue_pattern": "Missing empty string validation for sfAccount vs sfHolder", + "why_false_positive": "explicit check (ctx.tx[sfAccount] == *mptHolder) validates sfAccount vs sfHolder for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount (MPT)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (clawAmount.mpt() > MPTAmount{maxMPTokenAmount} || clawAmount <= beast::zero) at preflightHelper", + "issue_pattern": "Missing empty string validation for sfAmount (MPT)", + "why_false_positive": "explicit check (clawAmount.mpt() > MPTAmount{maxMPTokenAmount} || clawAmount <= beast::zero) validates sfAmount (MPT) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfAmount.asset().value()", + "empty", + "string", + "validation" + ], + "evidence": "std::visit with preflightHelper at Clawback::preflight", + "issue_pattern": "Missing empty string validation for sfAmount.asset().value()", + "why_false_positive": "std::visit with preflightHelper validates sfAmount.asset().value() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfAllowTrustLineClawback, lsfNoFreeze (issuer flags)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (issuerFlagsIn & lsfAllowTrustLineClawback, issuerFlagsIn & lsfNoFreeze) at preclaimHelper", + "issue_pattern": "Missing empty string validation for lsfAllowTrustLineClawback, lsfNoFreeze (issuer flags)", + "why_false_positive": "explicit check (issuerFlagsIn & lsfAllowTrustLineClawback, issuerFlagsIn & lsfNoFreeze) validates lsfAllowTrustLineClawback, lsfNoFreeze (issuer flags) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "trust line existence", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::line(...)) at preclaimHelper", + "issue_pattern": "Missing empty string validation for trust line existence", + "why_false_positive": "ctx.view.read(keylet::line(...)) validates trust line existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfBalance (trust line balance sign)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (balance > beast::zero && issuer < holder, balance < beast::zero && issuer > holder) at preclaimHelper", + "issue_pattern": "Missing empty string validation for sfBalance (trust line balance sign)", + "why_false_positive": "explicit check (balance > beast::zero && issuer < holder, balance < beast::zero && issuer > holder) validates sfBalance (trust line balance sign) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/token/Clawback.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "preflightHelper" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 28, + "name": "preflightHelper" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 45, + "name": "Clawback::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx", + "SLE const& sleIssuer", + "AccountID const& issuer", + "AccountID const& holder", + "STAmount const& clawAmount" + ], + "lineno": 61, + "name": "preclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx", + "SLE const& sleIssuer", + "AccountID const& issuer", + "AccountID const& holder", + "STAmount const& clawAmount" + ], + "lineno": 93, + "name": "preclaimHelper" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 112, + "name": "Clawback::preclaim" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 139, + "name": "applyHelper" + }, + { + "args": [ + "ApplyContext& ctx" + ], + "lineno": 157, + "name": "applyHelper" + }, + { + "args": [], + "lineno": 174, + "name": "Clawback::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "Clawback transaction logic is typically tested in unit/integration tests under the transaction or token test suites. Look for files like 'test/tx/Clawback_test.cpp', 'test/tx/MPTokens_test.cpp', or similar. Tests should cover: (1) malformed transactions (missing/extra fields), (2) permission errors (issuer flags), (3) trustline existence, (4) insufficient funds, (5) feature flag enablement. Gaps may exist for edge cases in MPTIssue (since code is partial), and for negative test cases involving complex trustline states or feature flag transitions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation via explicit checks and helper functions (no external validation framework)", + "validation_layer": "business_logic (preflight/preclaim functions in transaction processing)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfHolder", + "location": "preflightHelper", + "validated_by": "explicit check (ctx.tx.isFieldPresent(sfHolder))", + "validates": [ + "Ensures sfHolder is not present for Issue type" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "preflightHelper", + "validated_by": "explicit checks (isXRP, <= beast::zero)", + "validates": [ + "Ensures amount is not XRP", + "Ensures amount is greater than zero" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAccount vs sfAmount.issuer", + "location": "preflightHelper", + "validated_by": "explicit check (issuer == holder)", + "validates": [ + "Ensures issuer and holder are not the same" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temDISABLED", + "field": "featureMPTokensV1", + "location": "preflightHelper", + "validated_by": "ctx.rules.enabled(featureMPTokensV1)", + "validates": [ + "Ensures MPTokensV1 feature is enabled" + ], + "validation_type": "business_logic|feature_flag" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfHolder (optional)", + "location": "preflightHelper", + "validated_by": "explicit check (auto const mptHolder = ctx.tx[~sfHolder]; if (!mptHolder))", + "validates": [ + "Ensures sfHolder is present for MPTIssue" + ], + "validation_type": "business_logic|presence" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAccount vs sfHolder", + "location": "preflightHelper", + "validated_by": "explicit check (ctx.tx[sfAccount] == *mptHolder)", + "validates": [ + "Ensures issuer and holder are not the same for MPTIssue" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount (MPT)", + "location": "preflightHelper", + "validated_by": "explicit check (clawAmount.mpt() > MPTAmount{maxMPTokenAmount} || clawAmount <= beast::zero)", + "validates": [ + "Ensures MPT amount does not exceed maxMPTokenAmount", + "Ensures MPT amount is greater than zero" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "varies (temBAD_AMOUNT, temMALFORMED, temDISABLED, etc.)", + "field": "sfAmount.asset().value()", + "location": "Clawback::preflight", + "validated_by": "std::visit with preflightHelper", + "validates": [ + "Dispatches to correct preflightHelper based on asset type" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "lsfAllowTrustLineClawback, lsfNoFreeze (issuer flags)", + "location": "preclaimHelper", + "validated_by": "explicit check (issuerFlagsIn & lsfAllowTrustLineClawback, issuerFlagsIn & lsfNoFreeze)", + "validates": [ + "Ensures issuer has permission to clawback (AllowTrustLineClawback set, NoFreeze not set)" + ], + "validation_type": "business_logic|permission" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_LINE", + "field": "trust line existence", + "location": "preclaimHelper", + "validated_by": "ctx.view.read(keylet::line(...))", + "validates": [ + "Ensures trust line exists between holder and issuer" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfBalance (trust line balance sign)", + "location": "preclaimHelper", + "validated_by": "explicit check (balance > beast::zero && issuer < holder, balance < beast::zero && issuer > holder)", + "validates": [ + "Ensures correct address ordering for positive/negative balances" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/Clawback.cpp.ai.md b/src/libxrpl/tx/transactors/token/Clawback.cpp.ai.md new file mode 100644 index 0000000000..bbc8158d00 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/Clawback.cpp.ai.md @@ -0,0 +1,35 @@ +# `Clawback.cpp` — Clawback Transaction Transactor + +This file implements the `Clawback` transaction type for the XRP Ledger, which allows a token issuer to forcibly reclaim tokens held by another account. The feature is opt-in at the issuer level and is the primary mechanism for regulatory compliance use-cases (e.g., AML/KYC remediation) where an issuer must be able to recover issued tokens. The transactor handles two fundamentally different token models—classic IOU trust-line tokens (`Issue`) and the newer Multi-Purpose Token standard (`MPTIssue`)—within a unified three-phase execution pipeline. + +## Transaction Lifecycle and Architecture + +`Clawback` extends `Transactor` and participates in the standard three-phase lifecycle: `preflight` (stateless validation), `preclaim` (stateful, read-only validation), and `doApply` (state mutation). Each phase is forwarded to a type-specific template helper via `std::visit` on `sfAmount.asset().value()`, which is a `std::variant`. This means the central dispatcher functions (`preflight`, `preclaim`, `doApply`) are thin routers that extract the token type at runtime and call the correct specialization of `preflightHelper`, `preclaimHelper`, or `applyHelper` via explicit template specialization. + +The design cleanly separates the two token models without inheritance or virtual dispatch on the helpers themselves. Each specialization is a file-local `static` function, invisible outside the translation unit. + +## IOU Clawback: `Issue` Specializations + +**An unusual encoding:** for IOU clawback, the transaction does not use `sfHolder` to identify the token holder. Instead, the holder's account ID is packed into the `issuer` sub-field of the `sfAmount` value (the `STAmount`'s embedded `Issue::account`). This is a deliberate overloading of the field for encoding reasons in the original wire protocol. `preflightHelper` explicitly rejects any transaction that also includes `sfHolder`, enforcing mutual exclusivity. At apply time, `applyHelper` then corrects this by replacing `clawAmount.get().account` with the actual issuer's account before invoking the transfer. + +**Permission model for IOUs:** `preclaimHelper` enforces two flag conditions on the issuer's account entry: `lsfAllowTrustLineClawback` must be set, and `lsfNoFreeze` must not be set. The `NoFreeze` exclusion is architecturally significant — an account that has permanently waived freeze authority also loses clawback authority. The two flags are mutually exclusive by design, since clawback is an even stronger power than freezing and should not be available to issuers that have made the no-freeze commitment. + +**Trust-line balance sign convention:** XRPL trust-line balances are stored with a sign convention tied to account address ordering. A positive raw `sfBalance` means the account with the lexicographically higher address is the net holder. `preclaimHelper` explicitly validates this invariant: if the raw balance is positive, the issuer must have the higher address; if negative, the lower address. This check prevents an attacker from constructing a transaction that appears to be clawing back from the correct holder but actually targets the wrong side of the trust line. + +**Why `accountHolds` instead of reading the balance directly:** the code comments explain this choice. `accountHolds` accounts for additional constraints on spendable balance (e.g., offers, XLS-34 style lock-ups), whereas the raw `sfBalance` on the trust-line SLE reflects the nominal balance. The preclaim uses `accountHolds` with `fhIGNORE_FREEZE` to verify that the available balance is actually non-zero, and `applyHelper` calls it again at apply time to get the current spendable amount, applying `std::min(spendableAmount, clawAmount)` to ensure the transaction never overdrafts even if the ledger state changed slightly between preclaim and apply. + +## MPT Clawback: `MPTIssue` Specializations + +MPT clawback uses a separate `sfHolder` field in the transaction (the `sfAmount` issuer sub-field is meaningless for MPTs). `preflightHelper` requires `featureMPTokensV1` to be enabled and mandates `sfHolder` to be present and distinct from `sfAccount`. + +`preclaimHelper` checks the MPT issuance object for the `lsfMPTCanClawback` flag, which is set at issuance creation time and cannot be changed later. It also verifies the issuance's `sfIssuer` field matches the transaction submitter, preventing a scenario where someone constructs a transaction against an issuance they don't own. The `MPToken` holder object must exist (`keylet::mptoken`), and `accountHolds` with both `fhIGNORE_FREEZE` and `ahIGNORE_AUTH` is used to determine spendable balance — authorization status is irrelevant for a forced reclaim by the issuer. + +The `applyHelper` passes `/*checkIssuer*/ false` to `directSendNoFee`, which differs from the IOU path's `true`. This reflects a structural difference: for IOUs, `directSendNoFee` needs to verify issuer involvement in the trust-line accounting; for MPTs the issuance object's ownership is already authoritative. + +## Pseudo-Account and AMM Guards + +`Clawback::preclaim` includes two protective checks before delegating to the type-specific helpers. First, when `featureSingleAssetVault` is active, `isPseudoAccount` is checked and returns `tecPSEUDO_ACCOUNT` if the holder is a pseudo-account (a vault-managed internal account). Second, regardless of amendment status, accounts with `sfAMMID` present (AMM pool accounts) are blocked with `tecAMM_ACCOUNT`. These guards prevent clawback from being used against protocol-internal accounts that follow different ownership semantics. + +## Error Code Summary + +The validation logic uses `tem*` codes (stateless malformation) at preflight and `tec*` codes (ledger-state errors) at preclaim, following XRPL conventions. Notable codes: `temBAD_AMOUNT` for zero, XRP, or out-of-range amounts; `temMALFORMED` for missing/unexpected fields; `tecNO_PERMISSION` for missing flags or wrong address ordering; `tecNO_LINE` / `tecOBJECT_NOT_FOUND` for missing ledger objects; `tecINSUFFICIENT_FUNDS` when the spendable balance is zero. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp.ai.json b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp.ai.json new file mode 100644 index 0000000000..ed858f71fd --- /dev/null +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp.ai.json @@ -0,0 +1,449 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "MPTokenAuthorize::preflight" + ], + "entry_point": "MPTokenAuthorize::preflight", + "purpose": "Initial lightweight validation of transaction fields before deeper checks or ledger access.", + "validation_points": [ + "Checks if sfAccount == ~sfHolder (malformed if true)" + ] + }, + { + "call_chain": [ + "MPTokenAuthorize::preclaim" + ], + "entry_point": "MPTokenAuthorize::preclaim", + "purpose": "Performs contextual validation using ledger state, including existence and permission checks.", + "validation_points": [ + "Checks presence of ~sfHolder", + "Validates MPToken existence (ctx.view.read)", + "Checks for unauthorize flag and validates MPT state", + "Validates sfMPTAmount and ~sfLockedAmount", + "Checks issuer permissions and MPT issuance flags" + ] + }, + { + "call_chain": [ + "MPTokenAuthorize::doApply" + ], + "entry_point": "MPTokenAuthorize::doApply", + "purpose": "Executes the transaction after all validations pass (not shown in provided code, but standard in XRPL transactors).", + "validation_points": [ + "Assumes all validation already performed in preflight/preclaim" + ] + } + ], + "data_flows": [ + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "Read in preflight/preclaim", + "Compared to ~sfHolder and used as accountID" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Direct comparison to ~sfHolder", + "Used as key for ledger lookups" + ], + "validated_at": "preflight (compared to ~sfHolder), preclaim (used for permission checks)" + }, + { + "field": "~sfHolder", + "flow": [ + "Transaction input", + "Read in preflight/preclaim", + "Presence checked", + "Used as holderID" + ], + "origin": "ctx.tx[~sfHolder] (transaction input, optional)", + "transformations": [ + "Presence check (!holderID)", + "Used as key for ledger lookups" + ], + "validated_at": "preflight (compared to sfAccount), preclaim (presence check, used for account lookup)" + }, + { + "field": "sfMPTokenIssuanceID", + "flow": [ + "Transaction input", + "Used to construct keylet for mptoken and mptIssuance ledger objects" + ], + "origin": "ctx.tx[sfMPTokenIssuanceID] (transaction input)", + "transformations": [ + "Used as argument to keylet::mptoken and keylet::mptIssuance" + ], + "validated_at": "preclaim (used for ledger existence checks)" + }, + { + "field": "sfMPTAmount", + "flow": [ + "Read from mptoken ledger object", + "Checked for nonzero value" + ], + "origin": "(*sleMpt)[sfMPTAmount] (ledger object field)", + "transformations": [ + "Compared to 0" + ], + "validated_at": "preclaim (if nonzero, triggers tecHAS_OBLIGATIONS)" + }, + { + "field": "~sfLockedAmount", + "flow": [ + "Read from mptoken ledger object", + "Checked for nonzero value (value_or(0))" + ], + "origin": "(*sleMpt)[~sfLockedAmount] (ledger object field, optional)", + "transformations": [ + "value_or(0) to handle optionality", + "Compared to 0" + ], + "validated_at": "preclaim (if nonzero, triggers tecHAS_OBLIGATIONS)" + }, + { + "field": "sfIssuer", + "flow": [ + "Read from mptIssuance ledger object", + "Compared to accountID" + ], + "origin": "(*sleMptIssuance)[sfIssuer] (ledger object field)", + "transformations": [ + "Direct comparison" + ], + "validated_at": "preclaim (permission check: only issuer can perform certain actions)" + }, + { + "field": "sfFlags", + "flow": [ + "Read from mptIssuance ledger object", + "Bitmask checked for lsfMPTRequireAuth" + ], + "origin": "(*sleMptIssuance)[sfFlags] (ledger object field)", + "transformations": [ + "Bitmask operation" + ], + "validated_at": "preclaim (authorization requirement check)" + } + ], + "description": "Implements the MPTokenAuthorize transaction logic for authorizing or unauthorizing Multi-Party Tokens (MPT) in the XRPL ledger, including preflight, preclaim, and apply logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount and ~sfHolder", + "empty", + "string", + "validation" + ], + "evidence": "explicit comparison at MPTokenAuthorize::preflight", + "issue_pattern": "Missing empty string validation for sfAccount and ~sfHolder", + "why_false_positive": "explicit comparison validates sfAccount and ~sfHolder for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "~sfHolder", + "empty", + "string", + "validation" + ], + "evidence": "presence check (if (!holderID)) at MPTokenAuthorize::preclaim", + "issue_pattern": "Missing empty string validation for ~sfHolder", + "why_false_positive": "presence check (if (!holderID)) validates ~sfHolder for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "MPToken existence (keylet::mptoken)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read at MPTokenAuthorize::preclaim", + "issue_pattern": "Missing empty string validation for MPToken existence (keylet::mptoken)", + "why_false_positive": "ctx.view.read validates MPToken existence (keylet::mptoken) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfMPTAmount", + "empty", + "string", + "validation" + ], + "evidence": "(*sleMpt)[sfMPTAmount] != 0 at MPTokenAuthorize::preclaim", + "issue_pattern": "Missing empty string validation for sfMPTAmount", + "why_false_positive": "(*sleMpt)[sfMPTAmount] != 0 validates sfMPTAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "~sfLockedAmount", + "empty", + "string", + "validation" + ], + "evidence": "(*sleMpt)[~sfLockedAmount].value_or(0) != 0 at MPTokenAuthorize::preclaim", + "issue_pattern": "Missing empty string validation for ~sfLockedAmount", + "why_false_positive": "(*sleMpt)[~sfLockedAmount].value_or(0) != 0 validates ~sfLockedAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "MPTIssuance existence (keylet::mptIssuance)", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read at MPTokenAuthorize::preclaim", + "issue_pattern": "Missing empty string validation for MPTIssuance existence (keylet::mptIssuance)", + "why_false_positive": "ctx.view.read validates MPTIssuance existence (keylet::mptIssuance) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "lsfMPTLocked flag", + "empty", + "string", + "validation" + ], + "evidence": "sleMpt->isFlag(lsfMPTLocked) at MPTokenAuthorize::preclaim", + "issue_pattern": "Missing empty string validation for lsfMPTLocked flag", + "why_false_positive": "sleMpt->isFlag(lsfMPTLocked) validates lsfMPTLocked flag for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfIssuer vs. sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "accountID == (*sleMptIssuance)[sfIssuer] at MPTokenAuthorize::preclaim", + "issue_pattern": "Missing empty string validation for sfIssuer vs. sfAccount", + "why_false_positive": "accountID == (*sleMptIssuance)[sfIssuer] validates sfIssuer vs. sfAccount for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "MPTokenAuthorize::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "MPTokenAuthorize::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 20, + "name": "MPTokenAuthorize::preclaim" + }, + { + "args": [], + "lineno": 97, + "name": "MPTokenAuthorize::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic in preflight and preclaim is likely covered by unit/integration tests for MPTokenAuthorize transactions. Tests should exist for malformed transactions (sfAccount == ~sfHolder), missing or invalid ~sfHolder, non-existent mptoken/mptIssuance objects, permission errors (issuer vs. holder), and obligations (nonzero sfMPTAmount/~sfLockedAmount). Gaps may exist for edge cases such as featureSingleAssetVault flag, internal errors (tefINTERNAL), and rare ledger states (e.g., deleted MPTs with zero balance). Test files may be found in the rippled/test or xrplf/xrpl.js/test directories, especially those named with 'MPToken', 'Authorize', or 'Transactor'.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in business logic (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAccount and ~sfHolder", + "location": "MPTokenAuthorize::preflight", + "validated_by": "explicit comparison", + "validates": [ + "Ensures that the transaction's sfAccount is not the same as ~sfHolder" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none directly (branches logic)", + "field": "~sfHolder", + "location": "MPTokenAuthorize::preclaim", + "validated_by": "presence check (if (!holderID))", + "validates": [ + "Checks if ~sfHolder is present to determine transaction intent (delete/unauthorize vs. create/authorize)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND", + "field": "MPToken existence (keylet::mptoken)", + "location": "MPTokenAuthorize::preclaim", + "validated_by": "ctx.view.read", + "validates": [ + "Checks if the Multi-Party Token (MPT) exists before unauthorizing/deleting" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "sfMPTAmount", + "location": "MPTokenAuthorize::preclaim", + "validated_by": "(*sleMpt)[sfMPTAmount] != 0", + "validates": [ + "Ensures MPT balance is zero before allowing unauthorization/deletion" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "~sfLockedAmount", + "location": "MPTokenAuthorize::preclaim", + "validated_by": "(*sleMpt)[~sfLockedAmount].value_or(0) != 0", + "validates": [ + "Ensures locked amount is zero before allowing unauthorization/deletion" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND or tefINTERNAL", + "field": "MPTIssuance existence (keylet::mptIssuance)", + "location": "MPTokenAuthorize::preclaim", + "validated_by": "ctx.view.read", + "validates": [ + "Checks if the MPTIssuance object exists before proceeding" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "lsfMPTLocked flag", + "location": "MPTokenAuthorize::preclaim", + "validated_by": "sleMpt->isFlag(lsfMPTLocked)", + "validates": [ + "Prevents unauthorization if the MPT is locked and the feature is enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfIssuer vs. sfAccount", + "location": "MPTokenAuthorize::preclaim", + "validated_by": "accountID == (*sleMptIssuance)[sfIssuer]", + "validates": [ + "Prevents issuer from authorizing themselves as a holder" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp.ai.md b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp.ai.md new file mode 100644 index 0000000000..6f1797d505 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp.ai.md @@ -0,0 +1,41 @@ +# `MPTokenAuthorize.cpp` — MPT Holder Opt-In and Issuer Allowlist Transactor + +This file implements the `MPTokenAuthorize` transactor, which handles all authorization state transitions for Multi-Purpose Tokens (MPTs) on the XRP Ledger. It covers four distinct operations in a single transaction type: a holder opting in to receive a token (creating an `MPToken` SLE), a holder opting out and deleting their `MPToken` SLE, an issuer granting explicit allowlist access to a holder, and an issuer revoking that access. The presence or absence of the optional `sfHolder` field in the transaction is the sole signal that determines which of these two roles the submitter is playing. + +## The Dual-Role Architecture + +The most architecturally significant choice in this file is that a single transaction type serves both holders and issuers. The `sfHolder` field acts as the pivot: when absent, `ctx.tx[~sfHolder]` evaluates to a falsy optional and the submitter is treated as the holder acting on their own behalf. When present, the submitter must be the token's issuer and is managing the allowlist for the named account. This avoids introducing two separate transaction types that would share almost all their validation logic, but it does mean `preclaim` branches on this early and the two paths share virtually no code below that branch point. + +The `preflight` guard is minimal but essential: it rejects the case where `sfAccount` equals `~sfHolder`. This blocks an issuer from mistakenly naming themselves as the holder they want to authorize, which would be undefined behavior in the preclaim logic that assumes account and holderID are always distinct. + +## `preclaim`: Diverging Validation Paths + +### Holder Path (no `sfHolder`) + +When a holder is the submitter, the first thing `preclaim` checks is whether the transaction carries the `tfMPTUnauthorize` flag, because there is an important ordering constraint: a holder may delete their `MPToken` object even after the parent `MPTokenIssuance` has been destroyed. This edge case arises when all balances reach zero before the issuer destroys the issuance, and then the outstanding zero-balance `MPToken` objects need to be cleaned up afterwards. By checking the unauthorize flag before attempting to read the issuance, the code avoids a spurious `tecOBJECT_NOT_FOUND` in that cleanup scenario. + +Deleting a holder's `MPToken` requires two separate zero-balance checks — `sfMPTAmount` must be zero and the optional `~sfLockedAmount` must also be zero (or absent). The double check guards against tokens that have a zero net balance but still carry locked amounts from active escrows or vault operations. Returning `tecHAS_OBLIGATIONS` from both is consistent, but note the asymmetry: if the `MPToken` doesn't exist at all during an unauthorize attempt, the error is `tecOBJECT_NOT_FOUND`, not `tecHAS_OBLIGATIONS`. There is also a `featureSingleAssetVault`-gated check against the `lsfMPTLocked` flag on the `MPToken` SLE itself; a holder cannot remove an MPT that is currently locked by a vault. + +When the holder wants to create their `MPToken` (opt in), the issuance must exist, the submitter cannot be the issuer themselves, and the `MPToken` must not already exist (guards against `tecDUPLICATE`). + +### Issuer Path (with `sfHolder`) + +When `sfHolder` is present, the submitter is asserting they are the issuer. `preclaim` validates this in sequence: the named holder account must exist on the ledger, the `MPTokenIssuance` must exist, and the submitter's account must actually match the `sfIssuer` field on the issuance. If the issuance doesn't have `lsfMPTRequireAuth` set, issuer-side authorization is meaningless and `tecNO_AUTH` is returned — the issuer can only manage an allowlist if the token type was configured at creation to require one. + +A subtle guard prevents pseudo-accounts (vault pseudo-accounts and loan-broker pseudo-accounts, identified by the presence of `sfVaultID` or `sfLoanBrokerID` on the account root) from being unauthorized by an issuer. Pseudo-accounts are implicitly always authorized because they exist as protocol-controlled entities, not user-controlled accounts. The comment explicitly notes that no amendment gate is needed here because such accounts can only exist if the `featureSingleAssetVault` amendment is enabled anyway. + +## `doApply`: Thin Delegation to `authorizeMPToken` + +`doApply()` is a single delegating call to `authorizeMPToken()` in `MPTokenHelpers.cpp`, passing through the transaction flags, the `preFeeBalance_` (the pre-fee XRP balance captured by the base `Transactor` class before the fee is consumed), and the optional `sfHolder`. All actual ledger mutations happen in that helper, which is intentionally shared: `addEmptyHolding()` and `removeEmptyHolding()` in the same helpers file both call `authorizeMPToken()` to reuse the create and delete paths. + +What `authorizeMPToken` does for each operation: +- **Holder create**: Computes the reserve requirement (with a two-item exemption that mirrors trust line behavior — the first two owned items do not increase the reserve), creates the `MPToken` SLE with zero amounts and cleared flags, links it into the holder's owner directory via `dirLink`, and increments the owner count. +- **Holder delete**: Removes the `MPToken` from the owner directory with `dirRemove`, decrements the owner count, and erases the SLE from the ledger view. +- **Issuer authorize**: Reads the holder's `MPToken` SLE and sets the `lsfMPTAuthorized` flag. +- **Issuer unauthorize**: Reads the holder's `MPToken` SLE and clears the `lsfMPTAuthorized` flag. + +By the time `doApply` runs, all invariants have been established in `preclaim`, so `authorizeMPToken` treats any precondition failures (missing SLE after passing preclaim) as `tecINTERNAL` or `UNREACHABLE`, annotated with `LCOV_EXCL_LINE` since they cannot be reached through valid transaction processing. + +## Relationship to the Broader MPT System + +`MPTokenAuthorize` sits alongside `MPTokenIssuanceCreate`, `MPTokenIssuanceDestroy`, `MPTokenIssuanceSet`, and `Clawback` in the token transactor directory. Together they manage the full lifecycle of MPTs. The authorization state managed here — the `lsfMPTAuthorized` flag and the very existence of the `MPToken` SLE — is later consulted by `requireAuth()` in `MPTokenHelpers` during payment processing to ensure holders have opted in and, when required, have been explicitly allowlisted by the issuer before any transfer can occur. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp.ai.json b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp.ai.json new file mode 100644 index 0000000000..d3c61b132f --- /dev/null +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp.ai.json @@ -0,0 +1,505 @@ +{ + "args": [ + { + "lineno": 8, + "name": "ctx" + }, + { + "lineno": 19, + "name": "ctx" + }, + { + "lineno": 25, + "name": "ctx" + }, + { + "lineno": 56, + "name": "view" + }, + { + "lineno": 56, + "name": "journal" + }, + { + "lineno": 56, + "name": "args" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doApply", + "preflight", + "checkExtraFeatures", + "create" + ], + "entry_point": "doApply", + "purpose": "Processes an MPTokenIssuanceCreate transaction: validates input, checks feature flags, and creates the token issuance ledger entry.", + "validation_points": [ + "preflight", + "checkExtraFeatures" + ] + }, + { + "call_chain": [ + "preflight" + ], + "entry_point": "preflight", + "purpose": "Performs all field-level and logical validations on the transaction before it is applied.", + "validation_points": [ + "preflight" + ] + }, + { + "call_chain": [ + "checkExtraFeatures" + ], + "entry_point": "checkExtraFeatures", + "purpose": "Validates that certain fields are only present if the corresponding protocol features are enabled.", + "validation_points": [ + "checkExtraFeatures" + ] + } + ], + "data_flows": [ + { + "field": "sfMutableFlags", + "flow": [ + "transaction input", + "preflight (validation: must be present and nonzero, must match mask)", + "checkExtraFeatures (validation: featureDynamicMPT must be enabled if present)", + "create (written to ledger entry if present)" + ], + "origin": "ctx.tx[~sfMutableFlags] (transaction input)", + "transformations": [ + "Checked for presence and value", + "Masked with tmfMPTokenIssuanceCreateMutableMask" + ], + "validated_at": "preflight, checkExtraFeatures" + }, + { + "field": "sfTransferFee", + "flow": [ + "transaction input", + "preflight (validation: must not exceed maxTransferFee, if >0 tfMPTCanTransfer must be set)", + "create (written to ledger entry if present)" + ], + "origin": "ctx.tx[~sfTransferFee] (transaction input)", + "transformations": [ + "Checked for value and flag dependency" + ], + "validated_at": "preflight" + }, + { + "field": "sfDomainID", + "flow": [ + "transaction input", + "preflight (validation: must not be zero, tfMPTRequireAuth must be set if present)", + "checkExtraFeatures (validation: featurePermissionedDomains and featureSingleAssetVault must be enabled if present)", + "create (written to ledger entry if present)" + ], + "origin": "ctx.tx[~sfDomainID] (transaction input)", + "transformations": [ + "Checked for zero value", + "Checked for flag dependency", + "Checked for feature enablement" + ], + "validated_at": "preflight, checkExtraFeatures" + }, + { + "field": "sfMPTokenMetadata", + "flow": [ + "transaction input", + "preflight (validation: must not be empty, must not exceed maxMPTokenMetadataLength)", + "create (written to ledger entry if present)" + ], + "origin": "ctx.tx[~sfMPTokenMetadata] (transaction input)", + "transformations": [ + "Checked for length" + ], + "validated_at": "preflight" + }, + { + "field": "sfMaximumAmount", + "flow": [ + "transaction input", + "preflight (validation: must be >0, must not exceed maxMPTokenAmount, must fit in unsigned 63 bits)", + "create (written to ledger entry if present)" + ], + "origin": "ctx.tx[~sfMaximumAmount] (transaction input)", + "transformations": [ + "Checked for range" + ], + "validated_at": "preflight" + } + ], + "description": "Implements the MPTokenIssuanceCreate transaction logic for creating multi-party token issuances on the XRPL, including preflight validation, feature checks, and ledger entry creation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfMutableFlags", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for sfMutableFlags", + "why_false_positive": "explicit check in code validates sfMutableFlags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTransferFee", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for sfTransferFee", + "why_false_positive": "explicit check in code validates sfTransferFee for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfTransferFee", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing range validation for sfTransferFee", + "why_false_positive": "explicit check in code validates sfTransferFee range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTransferFee", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for sfTransferFee", + "why_false_positive": "explicit check in code validates sfTransferFee for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "explicit check in code validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "explicit check in code validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfMPTokenMetadata", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for sfMPTokenMetadata", + "why_false_positive": "explicit check in code validates sfMPTokenMetadata for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfMaximumAmount", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing empty string validation for sfMaximumAmount", + "why_false_positive": "explicit check in code validates sfMaximumAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfMaximumAmount", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check in code at preflight", + "issue_pattern": "Missing range validation for sfMaximumAmount", + "why_false_positive": "explicit check in code validates sfMaximumAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID, sfMutableFlags", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfDomainID, sfMutableFlags", + "why_false_positive": "explicit check in code validates sfDomainID, sfMutableFlags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "args.priorBalance", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at create", + "issue_pattern": "Missing empty string validation for args.priorBalance", + "why_false_positive": "explicit check in code validates args.priorBalance for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "args.account", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at create", + "issue_pattern": "Missing empty string validation for args.account", + "why_false_positive": "explicit check in code validates args.account for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 19, + "name": "getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 25, + "name": "preflight" + }, + { + "args": [ + "ApplyView& view", + "beast::Journal journal", + "MPTCreateArgs const& args" + ], + "lineno": 56, + "name": "create" + }, + { + "args": [], + "lineno": 104, + "name": "doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is in preflight and checkExtraFeatures. Typical test coverage would be in unit/integration tests for MPTokenIssuanceCreate, likely found in files such as 'test/tx/MPTokenIssuanceCreate_test.cpp' or similar. These should test all validation branches: invalid/missing flags, excessive transfer fee, missing required flags, invalid domain ID, metadata length, and maximum amount. Gaps may exist if there are no tests for feature flag gating (checkExtraFeatures), or for edge cases like zero/overflow values. Coverage for create is less critical for validation, but should be tested for correct ledger entry creation. If no tests exist for feature flag combinations or for all error codes returned by preflight, those would be coverage gaps.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in C++ (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "sfMutableFlags", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "If sfMutableFlags is present, it must not be zero and must have at least one valid flag set (tmfMPTokenIssuanceCreateMutableMask)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_TRANSFER_FEE", + "field": "sfTransferFee", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "sfTransferFee must not exceed maxTransferFee" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfTransferFee", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "If sfTransferFee is non-zero, tfMPTCanTransfer flag must be set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfDomainID", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "sfDomainID must not be beast::zero" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfDomainID", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "If sfDomainID is present, tfMPTRequireAuth flag must be set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfMPTokenMetadata", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "sfMPTokenMetadata must not be empty", + "sfMPTokenMetadata length must not exceed maxMPTokenMetadataLength" + ], + "validation_type": "format|range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfMaximumAmount", + "location": "preflight", + "validated_by": "explicit check in code", + "validates": [ + "sfMaximumAmount must not be zero", + "sfMaximumAmount must not exceed maxMPTokenAmount" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents transaction)", + "field": "sfDomainID, sfMutableFlags", + "location": "checkExtraFeatures", + "validated_by": "explicit check in code", + "validates": [ + "If sfDomainID is present, featurePermissionedDomains and featureSingleAssetVault must be enabled", + "If sfMutableFlags is present, featureDynamicMPT must be enabled" + ], + "validation_type": "business_logic|feature_flag" + }, + { + "confidence": 1.0, + "error_thrown": "tecINSUFFICIENT_RESERVE", + "field": "args.priorBalance", + "location": "create", + "validated_by": "explicit check in code", + "validates": [ + "args.priorBalance must be >= account reserve for (OwnerCount + 1)" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "args.account", + "location": "create", + "validated_by": "explicit check in code", + "validates": [ + "Account must exist in ledger" + ], + "validation_type": "type|existence" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp.ai.md b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp.ai.md new file mode 100644 index 0000000000..8a880679c9 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp.ai.md @@ -0,0 +1,62 @@ +# `MPTokenIssuanceCreate.cpp` + +## Role in the System + +This file implements the `MPTokenIssuanceCreate` transactor — the entry point for creating a new Multi-Purpose Token (MPT) issuance on the XRP Ledger. An MPT issuance is the on-ledger configuration object that defines a fungible token's parameters: its supply cap, transfer fee, authorization policy, permissioned-domain membership, and optional metadata. Every MPT in circulation traces back to an issuance record created by this transactor. + +The file fits within the standard XRPL transactor architecture: `MPTokenIssuanceCreate` inherits from `Transactor` and implements the three hooks the framework calls during transaction processing — `checkExtraFeatures`, `preflight`, and `doApply`. A fourth method, the static `create()` factory, is architecturally notable because it decouples ledger-entry construction from the transaction context, allowing it to be reused by other transactors. + +## Validation in Two Stages + +XRPL validation is split between stateless preflight (no ledger access, called potentially multiple times) and stateful apply (has write access to the view). `MPTokenIssuanceCreate` applies an additional pre-preflight hook, `checkExtraFeatures`, that gates certain fields on the presence of protocol amendments: + +- `sfDomainID` requires both `featurePermissionedDomains` **and** `featureSingleAssetVault` to be active. +- `sfMutableFlags` requires `featureDynamicMPT`. + +Returning `false` from `checkExtraFeatures` prevents the transaction from proceeding at all; this is the correct place for amendment gating because it runs before the main flag-mask check. + +`getFlagsMask()` returns `tfMPTokenIssuanceCreateMask`, which the framework passes to `preflight1()` to reject any unknown transaction flags before the transactor's own `preflight` is reached. + +`preflight` then enforces field-level business rules: + +1. **`sfMutableFlags`**: If present, the value must be nonzero and must not have bits set outside `tmfMPTokenIssuanceCreateMutableMask`. The check is inverted (`(*mutableFlags & mask) != 0u` means bits outside the mask are set), which is a common defensive pattern in this codebase to catch callers who set reserved bits. + +2. **`sfTransferFee`**: Capped at `maxTransferFee` (50,000 basis points = 50%). A non-zero fee is only meaningful when the token is transferable, so a non-zero fee without `tfMPTCanTransfer` returns `temMALFORMED` — the combination is incoherent by protocol design. + +3. **`sfDomainID`**: Must not be `beast::zero` (an all-zeros sentinel) and mandates `tfMPTRequireAuth`. The rationale is that a domain-scoped issuance restricts which holders are eligible, which only makes sense when the issuer controls authorization. + +4. **`sfMPTokenMetadata`**: Non-empty, bounded to `maxMPTokenMetadataLength` (1024 bytes). + +5. **`sfMaximumAmount`**: Must be positive and within `maxMPTokenAmount` (0x7FFF_FFFF_FFFF_FFFF — a signed 63-bit maximum). This ceiling exists to ensure MPT amounts fit within the XRPL `Number` type's representable range. + +## The `create()` Factory + +The static `create(ApplyView&, beast::Journal, MPTCreateArgs const&)` method is the unit of reuse. Its return type, `Expected`, follows the value-or-error pattern used across newer XRPL code: on success it carries the freshly minted `MPTID`; on failure it wraps a `TER` in `Unexpected`. This avoids out-parameters and makes it impossible for a caller to use the ID without checking for an error first. + +`MPTCreateArgs` bundles all optional fields into a single aggregate. `priorBalance` is `std::optional` — when present, the reserve check is performed; when `std::nullopt`, the check is skipped. This opt-out is intentional: `VaultCreate` calls `create()` on a pseudo-account that is freshly created within the same transaction and has no meaningful pre-fee balance, so it passes `std::nullopt` to bypass the reserve gate. + +Inside `create()`: + +1. The issuer's account SLE is peeked (write-access) for the owner-count update. A missing account returns `tecINTERNAL`, which is marked `LCOV_EXCL_LINE` because a valid transaction reaching apply phase always has its account in the ledger. + +2. If `priorBalance` is set, the reserve for `ownerCount + 1` is checked before any ledger mutation, returning `tecINSUFFICIENT_RESERVE` early. + +3. The `MPTID` is computed via `makeMptID(args.sequence, args.account)` — a deterministic 192-bit identifier derived from the issuer's account ID and the transaction sequence number. Because sequence numbers are monotonically increasing per account, collisions are impossible under normal ledger operation. + +4. The new `SLE` is built under `keylet::mptIssuance(mptId)`. Flags are stored as `args.flags & ~tfUniversal`, stripping the universal transaction flags that are not meaningful as ledger-entry flags. + +5. `sfOutstandingAmount` is initialized to 0 — the canonical starting point for a newly created issuance. + +6. All optional fields (`sfMaximumAmount`, `sfAssetScale`, `sfTransferFee`, `sfMPTokenMetadata`, `sfDomainID`, `sfMutableFlags`) are written only when present in `args`, keeping the SLE sparse. + +7. The issuance is inserted into the owner's directory (`keylet::ownerDir`). A full directory returns `tecDIR_FULL`, also excluded from coverage as a theoretical edge case. + +8. `adjustOwnerCount` increments the account's `sfOwnerCount` by 1, which both raises the reserve threshold and protects the issuance from being garbage-collected until explicitly destroyed. + +## Reuse by `VaultCreate` + +`VaultCreate` calls `MPTokenIssuanceCreate::create()` directly to mint the share token that vault depositors receive. It issues the MPT from a newly-created pseudo-account at sequence 1, with `priorBalance = std::nullopt` (no reserve check), and maps vault-level flags (`tfVaultShareNonTransferable`, `tfVaultPrivate`) to the corresponding MPT flags. This reuse is the primary reason `create()` is a free static method rather than implemented inline in `doApply`. + +## Symmetry with `MPTokenIssuanceDestroy` + +`MPTokenIssuanceDestroy` is the mirror image: it validates that no outstanding or locked balance exists, removes the SLE from the owner directory, erases it, and decrements `sfOwnerCount`. The `sfOutstandingAmount` initialized to 0 in `create()` is exactly the condition `MPTokenIssuanceDestroy::preclaim` checks — the ledger enforces that a token with circulating supply cannot be destroyed. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp.ai.json b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp.ai.json new file mode 100644 index 0000000000..443e5a8604 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp.ai.json @@ -0,0 +1,320 @@ +{ + "args": [ + { + "lineno": 7, + "name": "ctx" + }, + { + "lineno": 11, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "MPTokenIssuanceDestroy::preflight" + ], + "entry_point": "MPTokenIssuanceDestroy::preflight", + "purpose": "Initial transaction checks (syntax, basic validity). In this implementation, it always returns success.", + "validation_points": [] + }, + { + "call_chain": [ + "MPTokenIssuanceDestroy::preclaim" + ], + "entry_point": "MPTokenIssuanceDestroy::preclaim", + "purpose": "Performs semantic validation: checks that the MPTokenIssuance exists, is owned by the sender, and has no outstanding or locked amounts.", + "validation_points": [ + "sleMPT existence (OBJECT_NOT_FOUND)", + "sfIssuer matches sfAccount (NO_PERMISSION)", + "sfOutstandingAmount == 0 (HAS_OBLIGATIONS)", + "sfLockedAmount == 0 (HAS_OBLIGATIONS)" + ] + }, + { + "call_chain": [ + "MPTokenIssuanceDestroy::doApply" + ], + "entry_point": "MPTokenIssuanceDestroy::doApply", + "purpose": "Applies the transaction: removes the MPTokenIssuance ledger entry, updates owner count, and cleans up.", + "validation_points": [ + "account_ matches mpt->getAccountID(sfIssuer) (INTERNAL)" + ] + } + ], + "data_flows": [ + { + "field": "sfMPTokenIssuanceID", + "flow": [ + "Transaction input", + "Used as key in keylet::mptIssuance", + "Used to read/peek ledger entry (sleMPT/mpt)" + ], + "origin": "ctx.tx[sfMPTokenIssuanceID] (transaction input)", + "transformations": [ + "Used as a key to fetch the MPTokenIssuance ledger object" + ], + "validated_at": "preclaim (existence check), doApply (used again)" + }, + { + "field": "sfIssuer", + "flow": [ + "Read from ledger entry sleMPT", + "Compared to ctx.tx[sfAccount] in preclaim", + "Compared to account_ in doApply" + ], + "origin": "(*sleMPT)[sfIssuer] (from ledger entry)", + "transformations": [ + "Direct comparison for equality" + ], + "validated_at": "preclaim (must match sfAccount), doApply (must match account_)" + }, + { + "field": "sfOutstandingAmount", + "flow": [ + "Read from ledger entry sleMPT", + "Compared to 0 in preclaim" + ], + "origin": "(*sleMPT)[sfOutstandingAmount] (from ledger entry)", + "transformations": [ + "Direct comparison for zero" + ], + "validated_at": "preclaim (must be zero)" + }, + { + "field": "sfLockedAmount", + "flow": [ + "Read from ledger entry sleMPT (optional)", + "value_or(0) to default to zero if missing", + "Compared to 0 in preclaim" + ], + "origin": "(*sleMPT)[~sfLockedAmount] (from ledger entry, optional field)", + "transformations": [ + "Optional field, defaults to zero if not present" + ], + "validated_at": "preclaim (must be zero)" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "Compared to (*sleMPT)[sfIssuer] in preclaim" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Direct comparison for equality" + ], + "validated_at": "preclaim" + } + ], + "description": "Implements the logic for destroying a Multi-Party Token (MPT) issuance on the XRPL, including preflight checks, preclaim validation, and ledger application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfMPTokenIssuanceID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])) at preclaim", + "issue_pattern": "Missing empty string validation for sfMPTokenIssuanceID", + "why_false_positive": "ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])) validates sfMPTokenIssuanceID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfIssuer", + "empty", + "string", + "validation" + ], + "evidence": "(*sleMPT)[sfIssuer] != ctx.tx[sfAccount] at preclaim", + "issue_pattern": "Missing empty string validation for sfIssuer", + "why_false_positive": "(*sleMPT)[sfIssuer] != ctx.tx[sfAccount] validates sfIssuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOutstandingAmount", + "empty", + "string", + "validation" + ], + "evidence": "(*sleMPT)[sfOutstandingAmount] != 0 at preclaim", + "issue_pattern": "Missing empty string validation for sfOutstandingAmount", + "why_false_positive": "(*sleMPT)[sfOutstandingAmount] != 0 validates sfOutstandingAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLockedAmount", + "empty", + "string", + "validation" + ], + "evidence": "(*sleMPT)[~sfLockedAmount].value_or(0) != 0 at preclaim", + "issue_pattern": "Missing empty string validation for sfLockedAmount", + "why_false_positive": "(*sleMPT)[~sfLockedAmount].value_or(0) != 0 validates sfLockedAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfIssuer (again, in doApply)", + "empty", + "string", + "validation" + ], + "evidence": "account_ != mpt->getAccountID(sfIssuer) at doApply", + "issue_pattern": "Missing empty string validation for sfIssuer (again, in doApply)", + "why_false_positive": "account_ != mpt->getAccountID(sfIssuer) validates sfIssuer (again, in doApply) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Owner Directory removal", + "empty", + "string", + "validation" + ], + "evidence": "!view().dirRemove(...) at doApply", + "issue_pattern": "Missing empty string validation for Owner Directory removal", + "why_false_positive": "!view().dirRemove(...) validates Owner Directory removal for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 7, + "name": "MPTokenIssuanceDestroy::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 11, + "name": "MPTokenIssuanceDestroy::preclaim" + }, + { + "args": [], + "lineno": 28, + "name": "MPTokenIssuanceDestroy::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is structured for unit/integration testing via transaction submission. Typical test files would be in the rippled repo under 'test/tx/MPTokenIssuanceDestroy_test.cpp' or similar. The LCOV_EXCL_LINE comments indicate some error paths (e.g., locked amount nonzero, internal errors) are not covered by tests. The preflight function is trivial and likely not tested directly. The main validation logic (existence, ownership, outstanding/locked amounts) should be covered by positive and negative tests, but error branches in doApply (e.g., dirRemove failure, account_ mismatch) may not be tested. There may be gaps in testing for rare or internal error conditions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic in transaction transactor (xrpl core, no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND", + "field": "sfMPTokenIssuanceID", + "location": "preclaim", + "validated_by": "ctx.view.read(keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID]))", + "validates": [ + "Checks that the referenced MPTokenIssuance object exists in the ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfIssuer", + "location": "preclaim", + "validated_by": "(*sleMPT)[sfIssuer] != ctx.tx[sfAccount]", + "validates": [ + "Ensures the transaction submitter is the issuer of the MPTokenIssuance" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "sfOutstandingAmount", + "location": "preclaim", + "validated_by": "(*sleMPT)[sfOutstandingAmount] != 0", + "validates": [ + "Ensures there are no outstanding balances for the issuance" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "sfLockedAmount", + "location": "preclaim", + "validated_by": "(*sleMPT)[~sfLockedAmount].value_or(0) != 0", + "validates": [ + "Ensures there are no locked amounts for the issuance" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecINTERNAL", + "field": "sfIssuer (again, in doApply)", + "location": "doApply", + "validated_by": "account_ != mpt->getAccountID(sfIssuer)", + "validates": [ + "Ensures the account performing the operation matches the issuer in the object" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "tefBAD_LEDGER", + "field": "Owner Directory removal", + "location": "doApply", + "validated_by": "!view().dirRemove(...)", + "validates": [ + "Ensures the owner directory entry can be removed (ledger integrity check)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp.ai.md b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp.ai.md new file mode 100644 index 0000000000..73faa296d7 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp.ai.md @@ -0,0 +1,45 @@ +# `MPTokenIssuanceDestroy.cpp` — Transactor for Destroying an MPT Issuance + +## Role and Purpose + +This file implements the `MPTokenIssuanceDestroy` transactor, which handles the lifecycle event of permanently removing a Multi-Party Token (MPT) issuance object from the XRPL ledger. It is one of a family of MPT-related transactors — alongside `MPTokenIssuanceCreate`, `MPTokenIssuanceSet`, and `MPTokenAuthorize` — that together manage the full lifecycle of fungible token types on the ledger. + +The transactor follows the standard XRPL three-phase execution model defined in the `Transactor` base class: `preflight` (stateless syntax/rule checks), `preclaim` (read-only ledger validation), and `doApply` (state mutation). This separation is architecturally important: `preflight` and `preclaim` are static methods, allowing the engine to cheaply screen transactions before committing ledger access, while `doApply` is an instance method that mutates the ledger view. + +## Phase Analysis + +### `preflight` + +The `preflight` implementation is intentionally trivial — it simply returns `tesSUCCESS`. This is by design: a destroy transaction carries only an `sfMPTokenIssuanceID` field and no flags or complex parameters that require offline validation. All meaningful constraints are ledger-state-dependent and therefore deferred to `preclaim`. + +### `preclaim` + +This phase performs all substantive validation against a read-only snapshot of the ledger: + +1. **Existence check** — The issuance is looked up via `keylet::mptIssuance(ctx.tx[sfMPTokenIssuanceID])`. If the ledger entry is absent, `tecOBJECT_NOT_FOUND` is returned. This subsumes any concern about whether the ID itself is well-formed, since the keylet lookup implicitly validates its format. + +2. **Ownership check** — The `sfIssuer` field on the ledger object must match `sfAccount` from the transaction. Any other account — even one with administrative authority — cannot destroy someone else's issuance. The error is `tecNO_PERMISSION`. + +3. **Outstanding balance check** — `sfOutstandingAmount` must be exactly zero. This enforces a critical economic invariant: you cannot destroy a token type while holders still have positive balances. The destroy operation would otherwise eliminate the accounting record for real token holdings, corrupting the ledger state. + +4. **Locked amount check** — `sfLockedAmount` is an optional field (accessed via the `~` optional-field operator and defaulted with `value_or(0)`); if present and non-zero, the operation is also blocked with `tecHAS_OBLIGATIONS`. Locked amounts represent tokens held in escrow or protocol-controlled positions that cannot be unilaterally freed by the issuer. The `LCOV_EXCL_LINE` annotation on this branch indicates it is unreachable under current test coverage — a known gap in coverage for this defensive check. + +The separation of outstanding vs. locked amount checks is deliberate: they represent distinct accounting concepts (circulating supply vs. protocol-held collateral), and future protocol changes may handle them differently. + +### `doApply` + +The application phase performs three ledger mutations, all of which must succeed atomically as part of the transaction: + +1. **Issuer re-verification** — The first statement re-checks that `account_` (the transactor's cached account ID) matches the issuance's `sfIssuer`. This appears redundant with `preclaim` but is a defensive invariant: `doApply` runs against a mutable view that could, in theory, diverge from the read-only view used during `preclaim`. The `tecINTERNAL` error here (also `LCOV_EXCL_LINE`) would indicate a framework-level bug rather than user error. + +2. **Owner directory removal** — `view().dirRemove(keylet::ownerDir(account_), (*mpt)[sfOwnerNode], mpt->key(), false)` removes the issuance's entry from the issuer's owner directory. The `sfOwnerNode` field stored on the ledger object is a back-pointer into the directory, making this O(1) removal rather than a linear search. If the directory entry is not found, `tefBAD_LEDGER` is returned, signalling ledger structural corruption. The final `false` argument indicates the directory itself should not be deleted even if it becomes empty. + +3. **Object erasure and owner count adjustment** — `view().erase(mpt)` removes the `MPTokenIssuance` SLE from the ledger, and `adjustOwnerCount(..., -1, j_)` decrements the issuer's reserve-tracked owner count. These two operations are the symmetric inverse of what `MPTokenIssuanceCreate::create()` does: that function inserts the SLE, inserts the directory entry, and increments the owner count by 1. The destroy operation cleanly unwinds this state. + +## Design Tradeoffs and Invariants + +The strict "zero outstanding balance" requirement before destruction is the central economic safety guarantee. An alternative design might allow destruction with non-zero supply (implicitly burning remaining tokens), but XRPL's ledger model tracks holder `MPToken` objects separately — simply deleting the issuance record while those objects exist would leave orphaned entries. The `tecHAS_OBLIGATIONS` error forces the issuer to coordinate with all holders to redeem/burn their tokens before the issuance type itself can be removed. + +The `sfOwnerNode` back-pointer pattern is pervasive across XRPL object types: by storing the directory page index at object-creation time, removal never needs to scan the owner directory. This file's `doApply` relies on that invariant having been correctly established by `MPTokenIssuanceCreate`. + +There are no concurrency concerns specific to this file — XRPL ledger application is single-threaded per ledger close. The `view()` mutable accessor and `ctx_.tx` access patterns follow the standard transactor contract throughout. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp.ai.json b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp.ai.json new file mode 100644 index 0000000000..ac30cc74f4 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp.ai.json @@ -0,0 +1,530 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 21, + "name": "MPTMutabilityFlags" + } + ], + "code_paths": [ + { + "call_chain": [ + "Transactor::preflight", + "MPTokenIssuanceSet::preflight" + ], + "entry_point": "MPTokenIssuanceSet::preflight", + "purpose": "Performs stateless validation of the MPTokenIssuanceSet transaction before it is accepted for further processing.", + "validation_points": [ + "MPTokenIssuanceSet::preflight" + ] + }, + { + "call_chain": [ + "Transactor::preclaim", + "MPTokenIssuanceSet::preclaim" + ], + "entry_point": "MPTokenIssuanceSet::preclaim", + "purpose": "Performs contextual validation (e.g., ledger state) after preflight passes.", + "validation_points": [ + "MPTokenIssuanceSet::preclaim" + ] + }, + { + "call_chain": [ + "Transactor::apply", + "MPTokenIssuanceSet::doApply" + ], + "entry_point": "MPTokenIssuanceSet::doApply", + "purpose": "Applies the transaction to the ledger after all validations pass.", + "validation_points": [ + "MPTokenIssuanceSet::preflight", + "MPTokenIssuanceSet::preclaim" + ] + }, + { + "call_chain": [ + "MPTokenIssuanceSet::preflight", + "MPTokenIssuanceSet::checkExtraFeatures" + ], + "entry_point": "MPTokenIssuanceSet::checkExtraFeatures", + "purpose": "Checks if extra features are enabled when sfDomainID is present.", + "validation_points": [ + "MPTokenIssuanceSet::checkExtraFeatures" + ] + } + ], + "data_flows": [ + { + "field": "sfDomainID", + "flow": [ + "ctx.tx", + "ctx.tx.isFieldPresent(sfDomainID)", + "MPTokenIssuanceSet::preflight", + "MPTokenIssuanceSet::checkExtraFeatures" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence", + "Used in conditional logic to validate feature flags" + ], + "validated_at": "MPTokenIssuanceSet::preflight, MPTokenIssuanceSet::checkExtraFeatures" + }, + { + "field": "sfHolder", + "flow": [ + "ctx.tx", + "ctx.tx.isFieldPresent(sfHolder)", + "ctx.tx[~sfHolder]", + "MPTokenIssuanceSet::preflight" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for presence", + "Compared to sfAccount for equality" + ], + "validated_at": "MPTokenIssuanceSet::preflight" + }, + { + "field": "transaction flags (tfMPTLock, tfMPTUnlock)", + "flow": [ + "ctx.tx", + "ctx.tx.getFlags()", + "MPTokenIssuanceSet::preflight" + ], + "origin": "ctx.tx.getFlags()", + "transformations": [ + "Bitmask check for mutual exclusivity" + ], + "validated_at": "MPTokenIssuanceSet::preflight" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx", + "ctx.tx[sfAccount]", + "MPTokenIssuanceSet::preflight" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Compared to sfHolder for equality" + ], + "validated_at": "MPTokenIssuanceSet::preflight" + }, + { + "field": "sfMutableFlags", + "flow": [ + "ctx.tx", + "ctx.tx[~sfMutableFlags]", + "MPTokenIssuanceSet::preflight" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Bitmask checks for valid/invalid flag combinations", + "Checked for zero or invalid mask" + ], + "validated_at": "MPTokenIssuanceSet::preflight" + }, + { + "field": "sfMPTokenMetadata", + "flow": [ + "ctx.tx", + "ctx.tx[~sfMPTokenMetadata]", + "MPTokenIssuanceSet::preflight" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for length against maxMPTokenMetadataLength" + ], + "validated_at": "MPTokenIssuanceSet::preflight" + }, + { + "field": "sfTransferFee", + "flow": [ + "ctx.tx", + "ctx.tx[~sfTransferFee]", + "MPTokenIssuanceSet::preflight" + ], + "origin": "ctx.tx (transaction input)", + "transformations": [ + "Checked for value against maxTransferFee" + ], + "validated_at": "MPTokenIssuanceSet::preflight" + } + ], + "description": "Implements the MPTokenIssuanceSet transaction logic for the XRPL, including preflight validation, permission checks, preclaim checks, and application of changes to the ledger for multi-party token issuance settings.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "manual check (isFieldPresent) at MPTokenIssuanceSet::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "manual check (isFieldPresent) validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID and sfHolder", + "empty", + "string", + "validation" + ], + "evidence": "manual check (isFieldPresent) at MPTokenIssuanceSet::preflight", + "issue_pattern": "Missing empty string validation for sfDomainID and sfHolder", + "why_false_positive": "manual check (isFieldPresent) validates sfDomainID and sfHolder for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction flags (tfMPTLock, tfMPTUnlock)", + "empty", + "string", + "validation" + ], + "evidence": "manual bitmask check at MPTokenIssuanceSet::preflight", + "issue_pattern": "Missing empty string validation for transaction flags (tfMPTLock, tfMPTUnlock)", + "why_false_positive": "manual bitmask check validates transaction flags (tfMPTLock, tfMPTUnlock) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount and sfHolder", + "empty", + "string", + "validation" + ], + "evidence": "manual check (==) at MPTokenIssuanceSet::preflight", + "issue_pattern": "Missing empty string validation for sfAccount and sfHolder", + "why_false_positive": "manual check (==) validates sfAccount and sfHolder for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction is a no-op", + "empty", + "string", + "validation" + ], + "evidence": "manual check (flags, fields, isMutate) at MPTokenIssuanceSet::preflight", + "issue_pattern": "Missing empty string validation for transaction is a no-op", + "why_false_positive": "manual check (flags, fields, isMutate) validates transaction is a no-op for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "featureDynamicMPT", + "empty", + "string", + "validation" + ], + "evidence": "manual check (ctx.rules.enabled) at MPTokenIssuanceSet::preflight", + "issue_pattern": "Missing empty string validation for featureDynamicMPT", + "why_false_positive": "manual check (ctx.rules.enabled) validates featureDynamicMPT for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfHolder (when mutating)", + "empty", + "string", + "validation" + ], + "evidence": "manual check (isMutate && holderID) at MPTokenIssuanceSet::preflight", + "issue_pattern": "Missing empty string validation for sfHolder (when mutating)", + "why_false_positive": "manual check (isMutate && holderID) validates sfHolder (when mutating) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction flags (tfUniversalMask) when mutating", + "empty", + "string", + "validation" + ], + "evidence": "manual bitmask check at MPTokenIssuanceSet::preflight", + "issue_pattern": "Missing empty string validation for transaction flags (tfUniversalMask) when mutating", + "why_false_positive": "manual bitmask check validates transaction flags (tfUniversalMask) when mutating for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfTransferFee", + "empty", + "string", + "validation" + ], + "evidence": "manual range check at MPTokenIssuanceSet::preflight", + "issue_pattern": "Missing empty string validation for sfTransferFee", + "why_false_positive": "manual range check validates sfTransferFee for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfTransferFee", + "range", + "bounds", + "validation" + ], + "evidence": "manual range check at MPTokenIssuanceSet::preflight", + "issue_pattern": "Missing range validation for sfTransferFee", + "why_false_positive": "manual range check validates sfTransferFee range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 8, + "name": "MPTokenIssuanceSet::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 14, + "name": "MPTokenIssuanceSet::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 38, + "name": "MPTokenIssuanceSet::preflight" + }, + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 110, + "name": "MPTokenIssuanceSet::checkPermission" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 143, + "name": "MPTokenIssuanceSet::preclaim" + }, + { + "args": [], + "lineno": 217, + "name": "MPTokenIssuanceSet::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage for MPTokenIssuanceSet is likely found in unit/integration tests under the transaction or token test suites, such as 'test/tx/MPTokenIssuanceSet_test.cpp' or similar files. These tests should cover: field presence/absence, flag combinations, feature enablement, and malformed transactions. However, gaps may exist in edge cases for feature flag combinations, maximum metadata length, and simultaneous field/flag mutations. Manual or fuzz testing may be needed for full coverage of all validation branches, especially for new or experimental features (e.g., featureDynamicMPT).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation (no external validation framework)", + "validation_layer": "business_logic (preflight transaction validation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (preflight will fail)", + "field": "sfDomainID", + "location": "MPTokenIssuanceSet::checkExtraFeatures", + "validated_by": "manual check (isFieldPresent)", + "validates": [ + "If sfDomainID is present, featurePermissionedDomains and featureSingleAssetVault must be enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfDomainID and sfHolder", + "location": "MPTokenIssuanceSet::preflight", + "validated_by": "manual check (isFieldPresent)", + "validates": [ + "Both sfDomainID and sfHolder cannot be present in the same transaction" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "transaction flags (tfMPTLock, tfMPTUnlock)", + "location": "MPTokenIssuanceSet::preflight", + "validated_by": "manual bitmask check", + "validates": [ + "tfMPTLock and tfMPTUnlock cannot both be set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAccount and sfHolder", + "location": "MPTokenIssuanceSet::preflight", + "validated_by": "manual check (==)", + "validates": [ + "sfAccount and sfHolder cannot be the same" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "transaction is a no-op", + "location": "MPTokenIssuanceSet::preflight", + "validated_by": "manual check (flags, fields, isMutate)", + "validates": [ + "Transaction must change something (flags, sfDomainID, or mutate fields)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temDISABLED", + "field": "featureDynamicMPT", + "location": "MPTokenIssuanceSet::preflight", + "validated_by": "manual check (ctx.rules.enabled)", + "validates": [ + "Mutation not allowed unless featureDynamicMPT is enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfHolder (when mutating)", + "location": "MPTokenIssuanceSet::preflight", + "validated_by": "manual check (isMutate && holderID)", + "validates": [ + "sfHolder not allowed when mutating MPTokenIssuance" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "transaction flags (tfUniversalMask) when mutating", + "location": "MPTokenIssuanceSet::preflight", + "validated_by": "manual bitmask check", + "validates": [ + "Cannot set universal flags when mutating MPTokenIssuance" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_TRANSFER_FEE", + "field": "sfTransferFee", + "location": "MPTokenIssuanceSet::preflight", + "validated_by": "manual range check", + "validates": [ + "sfTransferFee must not exceed maxTransferFee" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp.ai.md b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp.ai.md new file mode 100644 index 0000000000..a15508044c --- /dev/null +++ b/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp.ai.md @@ -0,0 +1,58 @@ +# `MPTokenIssuanceSet.cpp` — MPToken Issuance State Mutation Transactor + +This file implements the `MPTokenIssuanceSet` transaction type for the XRPL's Multi-Purpose Token (MPT) system. The transaction serves two distinct but related purposes: it allows an issuer to lock or unlock an MPToken issuance (or an individual holder's MPToken), and — under the `featureDynamicMPT` amendment — to mutate fields on an existing `MPTokenIssuance` ledger object that were originally designed to be set at creation time: behavioral flags, metadata, and the transfer fee. + +## Class Structure + +`MPTokenIssuanceSet` inherits from `Transactor` and follows the standard XRPL transactor pipeline. The class exposes the standard static hooks (`checkExtraFeatures`, `getFlagsMask`, `preflight`, `checkPermission`, `preclaim`) plus the virtual `doApply()`. All methods except `doApply` are static because they operate on context-carrying argument objects rather than `this`; `doApply` is the only method that needs mutable ledger access through the inherited `view()`. + +## The Two-Level Flag System + +A critical design pattern running throughout the file is the distinction between two classes of flags on an `MPTokenIssuance` ledger object: + +- **Operational flags** (`lsfMPT*`): describe the current state of the issuance — whether it is locked, whether it requires authorization, whether clawback is enabled, etc. +- **Mutability flags** (`lsmfMPTCanMutate*`): stored in the `sfMutableFlags` field, these act as meta-flags that declare _which_ operational flags and fields the issuer is allowed to change post-creation. + +This two-level design enforces post-issuance governance promises to holders: if an issuer creates an MPToken issuance without `lsmfMPTCanMutateCanTransfer`, the transfer policy is locked at creation. The `MPTokenIssuanceSet` transaction can never change it, regardless of what the issuer subsequently submits. + +## The `mptMutabilityFlags` Table + +The file-local `MPTMutabilityFlags` struct and the `mptMutabilityFlags` constexpr array are the architectural backbone for mutation handling: + +```cpp +static constexpr std::array mptMutabilityFlags = { + {{tmfMPTSetCanLock, tmfMPTClearCanLock, lsmfMPTCanMutateCanLock}, + ...}}; +``` + +Each entry maps the set-bit and clear-bit in the transaction's `sfMutableFlags` field to the corresponding mutability gate on the issuance SLE. This table-driven approach is used in three places: `preflight` checks for set-and-clear-same-flag conflicts, `preclaim` checks whether the issuer has permission to change each flag, and `doApply` iterates the table to apply changes. The design ensures the three phases always reason about the same six properties with no possibility of a mismatch between which flags are validated and which are applied. + +## `preflight` — Stateless Validation + +`preflight` has two modes depending on which fields are present. If `sfMutableFlags`, `sfMPTokenMetadata`, or `sfTransferFee` is present (`isMutate`), the transaction is in mutation mode and `featureDynamicMPT` must be enabled. This gates the entire new mutation feature behind an amendment, preserving backward compatibility. + +The `sfDomainID` and `sfHolder` mutual exclusion reflects the semantics of the two operations: domain assignment targets the issuance object itself, while `sfHolder` targets an individual holder's `MPToken` object — they cannot both be present in a single transaction. + +The no-op check (under `featureSingleAssetVault` or `featureDynamicMPT`) rejects transactions that neither set any flags nor include any mutation fields, preventing fee-burning no-op submissions. + +When in mutation mode, the validator enforces two important interaction rules: you cannot name a `sfHolder` (because mutation operates on the issuance, not on a holder's token), and you cannot set transaction-level flags alongside mutation fields (they would apply to different objects). The prohibition on setting a non-zero `sfTransferFee` while simultaneously clearing `tmfMPTClearCanTransfer` prevents a contradictory single-transaction state where a fee is set on an issuance whose transfer capability is then immediately removed. + +## `checkPermission` — Granular Delegation + +This static method supports the XRPL delegate feature. If the transaction has no `sfDelegate` field, the issuer signed directly and is unconditionally permitted. When a delegate is present, the method first tries a broad per-transaction-type permission check via `checkTxPermission`. If that fails, it falls back to examining granular permissions: `MPTokenIssuanceLock` and `MPTokenIssuanceUnlock` are checked individually against the lock/unlock flags. The dead-code comment (`// LCOV_EXCL_LINE`) on the broad-mask guard is honest self-documentation — currently no other `MPTokenIssuanceSet`-specific transaction flags exist, so the branch cannot be reached, but it is retained as defensive forward-compatibility infrastructure. + +## `preclaim` — Ledger State Checks + +`preclaim` performs checks that require reading ledger state. The `lsfMPTCanLock` check has deliberate asymmetric logic: if either `featureSingleAssetVault` or `featureDynamicMPT` is enabled and the transaction is not actually locking or unlocking, the absence of `lsfMPTCanLock` is not fatal. This allows mutation operations (metadata, fees, flags) on issuances that were not created with locking capability, without accidentally blocking non-locking mutations just because the issuance lacks lock permission. + +The transfer fee interaction is the subtlest preclaim check. A non-zero `sfTransferFee` requires `lsfMPTCanTransfer` to be already set _on the ledger before this transaction_. Enabling `tmfMPTSetCanTransfer` in `sfMutableFlags` within the same transaction does not satisfy this requirement. The `preflight` layer already blocks the reverse case (non-zero fee while clearing transfer), but `preclaim` is needed here because it's the first point where the current ledger state of `lsfMPTCanTransfer` is visible. + +The domain assignment check requires `lsfMPTRequireAuth` on the issuance — binding a permissioned domain to an issuance that doesn't already use authorization would be meaningless. + +## `doApply` — Ledger Mutation + +`doApply` begins by selecting the correct SLE: if `sfHolder` is present it peeks the holder's `MPToken` object, otherwise it peeks the `MPTokenIssuance`. Lock/unlock operations flip `lsfMPTLocked` directly on whatever SLE was selected, which is how both issuance-wide and per-holder locking are handled by the same transaction. + +For mutation operations, the method iterates `mptMutabilityFlags` setting or clearing `lsmfMPTCanMutate*` flags in `sfFlags`. Clearing `tmfMPTClearCanTransfer` also removes the `sfTransferFee` field atomically — you cannot have a fee-bearing issuance with transfer disabled. + +Both `sfTransferFee` and `sfMPTokenMetadata` use "absent means default" semantics: a zero fee or empty metadata string removes the field entirely rather than storing a zero/empty value. For `sfDomainID`, `beast::zero` serves as a sentinel to clear an existing domain, mirroring how similar sentinel-clear patterns appear elsewhere in the XRPL protocol. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp.ai.json b/src/libxrpl/tx/transactors/token/TrustSet.cpp.ai.json new file mode 100644 index 0000000000..a7ea4ecb24 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp.ai.json @@ -0,0 +1,428 @@ +{ + "args": [ + { + "lineno": 8, + "name": "uFlags" + }, + { + "lineno": 9, + "name": "bHigh" + }, + { + "lineno": 10, + "name": "bNoFreeze" + }, + { + "lineno": 11, + "name": "bSetFreeze" + }, + { + "lineno": 12, + "name": "bClearFreeze" + }, + { + "lineno": 13, + "name": "bSetDeepFreeze" + }, + { + "lineno": 14, + "name": "bClearDeepFreeze" + }, + { + "lineno": 33, + "name": "ctx" + }, + { + "lineno": 71, + "name": "view" + }, + { + "lineno": 71, + "name": "tx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Transactor::preflight (framework)", + "TrustSet::preflight" + ], + "entry_point": "TrustSet::preflight", + "purpose": "Performs stateless validation of TrustSet transaction fields before further processing.", + "validation_points": [ + "Manual bitmask check of transaction flags (uTxFlags)", + "isLegalNet(saLimitAmount)", + "saLimitAmount.native()", + "badCurrency() == saLimitAmount.get().currency", + "saLimitAmount < beast::zero", + "issuer == noAccount()" + ] + }, + { + "call_chain": [ + "Transactor::checkPermission (framework)", + "TrustSet::checkPermission" + ], + "entry_point": "TrustSet::checkPermission", + "purpose": "Checks if the transaction has permission to perform TrustSet, especially with delegate fields.", + "validation_points": [ + "Delegate existence and permission checks", + "Granular permission flags validation", + "Trustline existence for granular permissions" + ] + }, + { + "call_chain": [ + "Transactor::preclaim (framework)", + "TrustSet::preclaim" + ], + "entry_point": "TrustSet::preclaim", + "purpose": "Performs contextual validation (not shown in provided code, but standard in XRPL transactor pattern).", + "validation_points": [ + "Likely checks for account existence, trustline state, etc. (not shown in snippet)" + ] + }, + { + "call_chain": [ + "Transactor::doApply (framework)", + "TrustSet::doApply" + ], + "entry_point": "TrustSet::doApply", + "purpose": "Applies the TrustSet transaction to the ledger after all validations pass.", + "validation_points": [ + "Assumes all previous validations have passed" + ] + } + ], + "data_flows": [ + { + "field": "uTxFlags", + "flow": [ + "Transaction input", + "TrustSet::preflight", + "Manual bitmask check", + "Used to determine allowed/forbidden flags" + ], + "origin": "tx.getFlags() (from transaction input)", + "transformations": [ + "Bitmask AND/OR with tfTrustSetMask, tfSetDeepFreeze, tfClearDeepFreeze" + ], + "validated_at": "TrustSet::preflight" + }, + { + "field": "sfLimitAmount", + "flow": [ + "Transaction input", + "TrustSet::preflight", + "saLimitAmount variable", + "Multiple validation checks" + ], + "origin": "tx.getFieldAmount(sfLimitAmount) (from transaction input)", + "transformations": [ + "Checked with isLegalNet()", + "Checked for native() (XRP)", + "Checked for badCurrency()", + "Compared to beast::zero (negative check)", + "Issuer extracted for further validation" + ], + "validated_at": "TrustSet::preflight" + }, + { + "field": "issuer (from sfLimitAmount)", + "flow": [ + "Extracted from saLimitAmount", + "Checked for null or noAccount()" + ], + "origin": "saLimitAmount.getIssuer()", + "transformations": [ + "None (direct extraction and comparison)" + ], + "validated_at": "TrustSet::preflight" + }, + { + "field": "delegate (sfDelegate)", + "flow": [ + "Transaction input", + "TrustSet::checkPermission", + "If present, used to look up delegateKey and SLE" + ], + "origin": "tx[~sfDelegate] (optional field in transaction)", + "transformations": [ + "Used to construct keylet::delegate", + "Used to read SLE from ledger" + ], + "validated_at": "TrustSet::checkPermission" + }, + { + "field": "TrustSet permission flags", + "flow": [ + "Transaction input", + "TrustSet::checkPermission", + "Bitmask check for tfTrustSetPermissionMask" + ], + "origin": "tx.getFlags()", + "transformations": [ + "Bitmask AND" + ], + "validated_at": "TrustSet::checkPermission" + } + ], + "description": "Implements the TrustSet transaction logic for the XRPL, including preflight checks, permission checks, preclaim validation, and application of trust line changes (creation, modification, deletion), with support for granular permissions, freeze/deep freeze, and reserve management.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Transaction Flags (uTxFlags)", + "empty", + "string", + "validation" + ], + "evidence": "manual bitmask check at TrustSet::preflight", + "issue_pattern": "Missing empty string validation for Transaction Flags (uTxFlags)", + "why_false_positive": "manual bitmask check validates Transaction Flags (uTxFlags) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLimitAmount", + "empty", + "string", + "validation" + ], + "evidence": "isLegalNet(saLimitAmount) at TrustSet::preflight", + "issue_pattern": "Missing empty string validation for sfLimitAmount", + "why_false_positive": "isLegalNet(saLimitAmount) validates sfLimitAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLimitAmount (native check)", + "empty", + "string", + "validation" + ], + "evidence": "saLimitAmount.native() at TrustSet::preflight", + "issue_pattern": "Missing empty string validation for sfLimitAmount (native check)", + "why_false_positive": "saLimitAmount.native() validates sfLimitAmount (native check) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLimitAmount.currency", + "empty", + "string", + "validation" + ], + "evidence": "badCurrency() == saLimitAmount.get().currency at TrustSet::preflight", + "issue_pattern": "Missing empty string validation for sfLimitAmount.currency", + "why_false_positive": "badCurrency() == saLimitAmount.get().currency validates sfLimitAmount.currency for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLimitAmount (negative check)", + "empty", + "string", + "validation" + ], + "evidence": "saLimitAmount < beast::zero at TrustSet::preflight", + "issue_pattern": "Missing empty string validation for sfLimitAmount (negative check)", + "why_false_positive": "saLimitAmount < beast::zero validates sfLimitAmount (negative check) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfLimitAmount (negative check)", + "range", + "bounds", + "validation" + ], + "evidence": "saLimitAmount < beast::zero at TrustSet::preflight", + "issue_pattern": "Missing range validation for sfLimitAmount (negative check)", + "why_false_positive": "saLimitAmount < beast::zero validates sfLimitAmount (negative check) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLimitAmount.issuer", + "empty", + "string", + "validation" + ], + "evidence": "!issuer || issuer == noAccount() at TrustSet::preflight", + "issue_pattern": "Missing empty string validation for sfLimitAmount.issuer", + "why_false_positive": "!issuer || issuer == noAccount() validates sfLimitAmount.issuer for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/token/TrustSet.cpp", + "functions": [ + { + "args": [ + "uFlags", + "bHigh", + "bNoFreeze", + "bSetFreeze", + "bClearFreeze", + "bSetDeepFreeze", + "bClearDeepFreeze" + ], + "lineno": 7, + "name": "computeFreezeFlags" + }, + { + "args": [ + "ctx" + ], + "lineno": 32, + "name": "TrustSet::getFlagsMask" + }, + { + "args": [ + "ctx" + ], + "lineno": 36, + "name": "TrustSet::preflight" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 70, + "name": "TrustSet::checkPermission" + }, + { + "args": [ + "ctx" + ], + "lineno": 120, + "name": "TrustSet::preclaim" + }, + { + "args": [], + "lineno": 232, + "name": "TrustSet::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 1, + "name": "" + }, + { + "lineno": 30, + "name": "xrpl" + } + ], + "test_coverage_notes": "TrustSet transaction validation is typically covered in unit/integration tests under the rippled codebase, especially in files like 'test/tx/TrustSet_test.cpp', 'test/tx/SetTrust_test.cpp', or similar. These tests usually cover flag validation, amount validation, currency checks, and permission checks. However, coverage gaps may exist for edge cases such as deep freeze flags when the amendment is disabled, negative or malformed sfLimitAmount, and granular delegate permission scenarios. Tests for new features (like DeepFreeze) may be missing if the feature is recent or not yet widely tested. Manual review of test files is recommended to confirm coverage of all validation branches.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation (no external validation framework detected)", + "validation_layer": "business_logic (preflight transaction validation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temINVALID_FLAG", + "field": "Transaction Flags (uTxFlags)", + "location": "TrustSet::preflight", + "validated_by": "manual bitmask check", + "validates": [ + "If featureDeepFreeze is not enabled, tfSetDeepFreeze and tfClearDeepFreeze flags must not be set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfLimitAmount", + "location": "TrustSet::preflight", + "validated_by": "isLegalNet(saLimitAmount)", + "validates": [ + "Checks if the amount is a legal network amount (not negative, not too large, etc.)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_LIMIT", + "field": "sfLimitAmount (native check)", + "location": "TrustSet::preflight", + "validated_by": "saLimitAmount.native()", + "validates": [ + "Checks that the limit amount is not in the native currency (XRP)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_CURRENCY", + "field": "sfLimitAmount.currency", + "location": "TrustSet::preflight", + "validated_by": "badCurrency() == saLimitAmount.get().currency", + "validates": [ + "Checks that the currency is not XRP (XRP as IOU is not allowed)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_LIMIT", + "field": "sfLimitAmount (negative check)", + "location": "TrustSet::preflight", + "validated_by": "saLimitAmount < beast::zero", + "validates": [ + "Checks that the limit amount is not negative" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temDST_NEEDED", + "field": "sfLimitAmount.issuer", + "location": "TrustSet::preflight", + "validated_by": "!issuer || issuer == noAccount()", + "validates": [ + "Checks that the issuer field is present and not the 'no account' value" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/token/TrustSet.cpp.ai.md b/src/libxrpl/tx/transactors/token/TrustSet.cpp.ai.md new file mode 100644 index 0000000000..307909f4f0 --- /dev/null +++ b/src/libxrpl/tx/transactors/token/TrustSet.cpp.ai.md @@ -0,0 +1,44 @@ +# `TrustSet.cpp` — TrustSet Transaction Transactor + +`TrustSet.cpp` implements the `TrustSet` transaction type for the XRP Ledger, the mechanism by which accounts establish, modify, and implicitly destroy bilateral trust lines (`RippleState` ledger objects). A trust line tracks the credit limit, current balance, quality adjustments, noRipple preference, authorization state, and freeze state for a specific IOU currency between exactly two accounts. Without this transactor, non-XRP tokenized value could not flow through the ledger. + +## Transactor Pipeline + +`TrustSet` inherits from the `Transactor` base class and implements four static-or-virtual entry points that the framework dispatches in order: `getFlagsMask`, `preflight`, `checkPermission`, `preclaim`, and `doApply`. Each phase has a distinct contract. + +**`preflight`** runs without any ledger state access, performing pure structural validation on the submitted transaction fields. It rejects `sfLimitAmount` values that are native (XRP), use the `badCurrency()` sentinel, are negative, or carry a missing/`noAccount()` issuer. It also checks that the `tfSetDeepFreeze` and `tfClearDeepFreeze` flags are only present when the `featureDeepFreeze` amendment has been activated — these flags are defined in `tfTrustSetMask` but their use is amendment-gated here rather than in the flag mask itself, which keeps the mask stable across upgrades. + +**`checkPermission`** enforces the delegate authorization model. When the transaction carries an optional `sfDelegate` field, the transactor looks up the `Delegate` ledger entry for `(account, delegate)` and checks it against `checkTxPermission`. If the delegate only holds granular permissions (a subset defined in `Permissions.h`), the method restricts the transaction to operations that map to the three defined granular TrustSet capabilities: `TrustlineAuthorize` (maps to `tfSetfAuth`), `TrustlineFreeze` (maps to `tfSetFreeze`), and `TrustlineUnfreeze` (maps to `tfClearFreeze`). Any other flags in `tfTrustSetPermissionMask` — the complement of those three operations and universal flags — result in `terNO_DELEGATE_PERMISSION`. Crucially, delegates operating on granular permissions also cannot create new trust lines (they are blocked if the line does not yet exist) and cannot change the credit limit. + +**`preclaim`** performs context-dependent validation that requires ledger state reads but makes no writes. Notable checks include: + +- `tfSetfAuth` is only meaningful if the submitting account has `lsfRequireAuth` set; otherwise it returns `tefNO_AUTH_REQUIRED`. +- The `lsfDisallowIncomingTrustline` flag on the destination is honoured, but `fixDisallowIncomingV1` softens the original overly broad check by allowing the transaction to proceed if a trust line already exists. This was a targeted bug fix: the original implementation blocked issuers from modifying limits on already-live lines when the holder had enabled `DisallowIncoming`. +- Pseudo-accounts (AMM pools, single-asset vaults, loan brokers) are handled with explicit allow-listing. For AMM accounts, a new trust line is permitted only if the currency matches the pool's LP token and the AMM holds a non-zero balance. For vault and loan broker pseudo-accounts, only modifications to an existing line pass; new line creation is denied. All other pseudo-accounts return `tecPSEUDO_ACCOUNT`. +- The deep-freeze invariant is verified in `preclaim` by simulating what the trust line flags would look like after applying the transaction, using the shared `computeFreezeFlags()` helper. The invariant is: `deepFrozen → frozen` (you cannot deep-freeze a line that is not already normally frozen, and you cannot clear normal freeze while deep freeze remains). Trying to both set and clear a freeze flag in the same transaction also fails here. + +## The `bHigh` Convention + +`RippleState` ledger objects store both sides of a trust relationship in a single shared ledger entry. The low and high sides are determined solely by AccountID lexicographic order: the account whose ID bytes compare lower is the "low" side. Every per-side field — `sfLowLimit`/`sfHighLimit`, `sfLowQualityIn`/`sfHighQualityIn`, `lsfLowFreeze`/`lsfHighFreeze`, `lsfLowReserve`/`lsfHighReserve`, and so on — comes in symmetric pairs. The `bool bHigh = account_ > uDstAccountID` predicate threads through all of `doApply` to select the correct side of every field without duplicating logic. This design means a TrustSet operation by account A and a TrustSet operation by account B both write to the same ledger object but modify disjoint fields. + +## Reserve Accounting + +Trust lines consume one unit of the owner reserve for each side that holds non-default state. `doApply` computes `bLowReserveSet` and `bHighReserveSet` by inspecting whether any side-specific state deviates from defaults: a non-zero quality, a non-zero credit limit, a frozen flag, a positive balance, or a noRipple setting that differs from the account's `lsfDefaultRipple` flag. When the computed need for a reserve differs from the `lsfLowReserve`/`lsfHighReserve` flags currently stored in the trust line, `adjustOwnerCount` is called to increment or decrement the appropriate account's owner count. + +A notable concession to onboarding ergonomics: the reserve requirement is waived when an account owns fewer than two total objects (line 352). This allows gateways to fund new user accounts and establish trust lines without forcing users to hold extra XRP beyond the bare account reserve. + +## Auto-deletion of Trust Lines + +When `bLowReserveClear && bHighReserveClear` (the `bDefault` flag) becomes true — meaning both sides have zeroed out all non-default state and the balance is zero — `doApply` calls `trustDelete` to remove the `RippleState` entry from the ledger and both accounts' owner directories. This prevents stale zero-balance objects from accumulating. The deletion is unconditional; there is no special user-visible "delete trust line" transaction type. + +## `computeFreezeFlags()` Helper + +The file-scope helper `computeFreezeFlags()` encapsulates the four-way conditional logic for applying normal and deep-freeze flags to a flag word. It is called identically from both `preclaim` (to simulate the post-transaction state for validation) and `doApply` (to compute the value to write). Sharing this helper guarantees the two phases agree on what the resulting flag state will be, preventing a class of subtle inconsistency bugs where validation passes based on one interpretation and the write produces another. + +## Quality Normalization + +Quality values encode the exchange rate as a fixed-point number scaled so that `QUALITY_ONE` (1,000,000,000) represents a 1:1 exchange ratio. `doApply` normalizes any quality set to exactly `QUALITY_ONE` back to zero before writing. Zero is the compact representation of "no custom quality" (the field is made absent via `makeFieldAbsent`), so this prevents a user from explicitly writing the default value and wasting 4 bytes of ledger storage per side. The `uQualityOut` normalization happens at line 357–358 during field parsing; the `uQualityIn`/`uQualityOut` values read back from the existing line are similarly normalized at lines 447–451 and 514–517 before the reserve decision is made. + +## Interaction with `RippleStateHelpers` + +`doApply` delegates the heavy lifting of ledger object construction to `trustCreate` and `trustDelete` from `RippleStateHelpers.cpp`. `trustCreate` inserts the new `ltRIPPLE_STATE` object, adds entries to both accounts' owner directories, initializes all flags from the parameters passed by `doApply`, and calls `adjustOwnerCount` for the creating account. `trustDelete` removes the line from both directories and erases the SLE. This keeps the mutation logic centralized and reusable by other transactors (such as `issueIOU`, which may implicitly create a trust line when issuing tokens to an account that has none). \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp.ai.json b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp.ai.json new file mode 100644 index 0000000000..8cfaf3968d --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp.ai.json @@ -0,0 +1,471 @@ +{ + "args": [ + { + "lineno": 13, + "name": "ctx" + }, + { + "lineno": 31, + "name": "vault" + }, + { + "lineno": 31, + "name": "maybeAmount" + }, + { + "lineno": 31, + "name": "account" + }, + { + "lineno": 46, + "name": "ctx" + }, + { + "lineno": 133, + "name": "vault" + }, + { + "lineno": 133, + "name": "sleShareIssuance" + }, + { + "lineno": 133, + "name": "holder" + }, + { + "lineno": 133, + "name": "clawbackAmount" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "VaultClawback::preflight" + ], + "entry_point": "VaultClawback::preflight", + "purpose": "Initial stateless validation of the VaultClawback transaction before it is accepted for further processing.", + "validation_points": [ + "ctx.tx[sfVaultID] == beast::zero (rejects zero/empty vault ID)", + "*amount < beast::zero (rejects negative amounts)", + "isXRP(amount->asset()) (rejects XRP clawback)" + ] + }, + { + "call_chain": [ + "VaultClawback::preclaim", + "clawbackAmount" + ], + "entry_point": "VaultClawback::preclaim", + "purpose": "Performs contextual validation using ledger state, including vault existence, asset/issuer checks, and share clawback rules.", + "validation_points": [ + "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) (vault must exist)", + "ctx.view.read(keylet::mptIssuance(mptIssuanceID)) (share issuance must exist)", + "!maybeAmount && !vaultAsset.native() && vaultAsset.getIssuer() == vault->at(sfOwner) (must specify amount if issuer is owner)", + "account != vault->at(sfOwner) (only owner can clawback shares)", + "sharesTotal == 0 || (assetsTotal != 0 || assetsAvailable != 0) (owner can only clawback shares if no assets remain)" + ] + }, + { + "call_chain": [ + "VaultClawback::doApply", + "VaultClawback::preflight", + "VaultClawback::preclaim", + "clawbackAmount" + ], + "entry_point": "VaultClawback::doApply", + "purpose": "Executes the actual clawback operation after all validations pass.", + "validation_points": [ + "Relies on preflight and preclaim for validation; may have additional runtime checks." + ] + } + ], + "data_flows": [ + { + "field": "sfVaultID", + "flow": [ + "ctx.tx[sfVaultID]", + "VaultClawback::preflight (checked for zero)", + "VaultClawback::preclaim (used to look up vault SLE)", + "VaultClawback::doApply (used for operation)" + ], + "origin": "ctx.tx[sfVaultID] (transaction input)", + "transformations": [ + "Checked for zero value", + "Used as key to fetch vault ledger entry" + ], + "validated_at": "preflight (zero check), preclaim (vault existence check)" + }, + { + "field": "sfAmount", + "flow": [ + "ctx.tx[~sfAmount]", + "VaultClawback::preflight (checked for negative, XRP)", + "VaultClawback::preclaim (used for asset/issuer logic)", + "clawbackAmount (used to determine actual amount to clawback)", + "VaultClawback::doApply (used for operation)" + ], + "origin": "ctx.tx[~sfAmount] (optional transaction input)", + "transformations": [ + "Checked for negative value", + "Checked for XRP asset", + "If not present, computed based on vault state" + ], + "validated_at": "preflight (negative/XRP check), preclaim (issuer/owner logic)" + }, + { + "field": "sfShareMPTID", + "flow": [ + "vault->at(sfShareMPTID)", + "VaultClawback::preclaim (used to look up share issuance SLE)", + "ctx.view.read(keylet::mptIssuance(mptIssuanceID))" + ], + "origin": "vault->at(sfShareMPTID) (from vault SLE)", + "transformations": [ + "Used as key to fetch share issuance ledger entry" + ], + "validated_at": "preclaim (share issuance existence check)" + }, + { + "field": "sfAsset", + "flow": [ + "vault->at(sfAsset)", + "VaultClawback::preclaim (used for asset/issuer logic)", + "clawbackAmount (used to determine amount to clawback)" + ], + "origin": "vault->at(sfAsset) (from vault SLE)", + "transformations": [ + "Checked for native/non-native", + "Issuer compared to owner" + ], + "validated_at": "preclaim (issuer/owner logic)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "VaultClawback::preclaim (used for permission checks)", + "clawbackAmount (used to determine amount to clawback)" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared to vault owner for permission" + ], + "validated_at": "preclaim (permission checks)" + } + ], + "description": "Implements the VaultClawback transaction logic for the XRPL ledger, including preflight, preclaim, asset calculation, and application of clawback operations on vault assets and shares.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfVaultID] == beast::zero) at VaultClawback::preflight", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "explicit check (ctx.tx[sfVaultID] == beast::zero) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfVaultID", + "format", + "validation", + "invalid" + ], + "evidence": "explicit check (ctx.tx[sfVaultID] == beast::zero) at VaultClawback::preflight", + "issue_pattern": "Missing format validation for sfVaultID", + "why_false_positive": "explicit check (ctx.tx[sfVaultID] == beast::zero) validates sfVaultID format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (amount < beast::zero) at VaultClawback::preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "explicit check (amount < beast::zero) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (amount < beast::zero) at VaultClawback::preflight", + "issue_pattern": "Missing range validation for sfAmount", + "why_false_positive": "explicit check (amount < beast::zero) validates sfAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "isXRP(amount->asset()) at VaultClawback::preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "isXRP(amount->asset()) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) at VaultClawback::preclaim", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfShareMPTID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::mptIssuance(mptIssuanceID)) at VaultClawback::preclaim", + "issue_pattern": "Missing empty string validation for sfShareMPTID", + "why_false_positive": "ctx.view.read(keylet::mptIssuance(mptIssuanceID)) validates sfShareMPTID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "(!maybeAmount && !vaultAsset.native() && vaultAsset.getIssuer() == vault->at(sfOwner)) at VaultClawback::preclaim", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "(!maybeAmount && !vaultAsset.native() && vaultAsset.getIssuer() == vault->at(sfOwner)) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/vault/VaultClawback.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "VaultClawback::preflight" + }, + { + "args": [ + "std::shared_ptr const& vault", + "std::optional const& maybeAmount", + "AccountID const& account" + ], + "lineno": 31, + "name": "clawbackAmount" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 46, + "name": "VaultClawback::preclaim" + }, + { + "args": [ + "std::shared_ptr const& vault", + "std::shared_ptr const& sleShareIssuance", + "AccountID const& holder", + "STAmount const& clawbackAmount" + ], + "lineno": 133, + "name": "VaultClawback::assetsToClawback" + }, + { + "args": [], + "lineno": 210, + "name": "VaultClawback::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by unit/integration tests in the rippled codebase, especially in files such as 'test/tx/VaultClawback_test.cpp', 'test/tx/Vault_test.cpp', or similar. The explicit validation branches (zero vault ID, negative amount, XRP asset, missing vault/share issuance, issuer/owner logic, permission checks) should be tested. However, some error branches (e.g., missing share issuance, internal errors) may not be fully covered, as indicated by LCOV_EXCL_START/STOP comments. Edge cases involving ambiguous issuer/owner relationships and share burning logic may require additional targeted tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in business logic (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfVaultID", + "location": "VaultClawback::preflight", + "validated_by": "explicit check (ctx.tx[sfVaultID] == beast::zero)", + "validates": [ + "Ensures VaultID is not zero/empty" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "VaultClawback::preflight", + "validated_by": "explicit check (amount < beast::zero)", + "validates": [ + "Ensures amount is not negative" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAmount", + "location": "VaultClawback::preflight", + "validated_by": "isXRP(amount->asset())", + "validates": [ + "Ensures clawback is not attempted on XRP" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfVaultID", + "location": "VaultClawback::preclaim", + "validated_by": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID]))", + "validates": [ + "Ensures referenced vault exists in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sfShareMPTID", + "location": "VaultClawback::preclaim", + "validated_by": "ctx.view.read(keylet::mptIssuance(mptIssuanceID))", + "validates": [ + "Ensures share issuance entry exists for vault" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecWRONG_ASSET", + "field": "sfAmount", + "location": "VaultClawback::preclaim", + "validated_by": "(!maybeAmount && !vaultAsset.native() && vaultAsset.getIssuer() == vault->at(sfOwner))", + "validates": [ + "Ensures amount is specified when issuer is owner and asset is not native" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultClawback.cpp.ai.md b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp.ai.md new file mode 100644 index 0000000000..96785da369 --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultClawback.cpp.ai.md @@ -0,0 +1,37 @@ +# VaultClawback.cpp + +`VaultClawback` implements the transactor for the `VaultClawback` transaction type in the XRPL vault system. A vault is a pooled on-ledger financial primitive where depositors exchange underlying assets for MPT-based share tokens representing their proportional claim. Clawback gives two distinct principals a way to reclaim value from a vault: the underlying asset's issuer can reclaim assets held in the vault (taking the depositor's proportional shares in exchange), and the vault owner can burn orphaned share tokens when a vault has no remaining assets. Both use cases share one transaction type because they follow the same fundamental ledger mechanics — destroy shares, optionally recover assets — but they differ substantially in who is permitted and under what conditions. + +## The Three-Phase Transactor Pipeline + +Like all XRPL transactors, `VaultClawback` separates validation into a stateless phase (`preflight`), a ledger-read phase (`preclaim`), and the mutation phase (`doApply`). + +`preflight` handles the checks that require only the transaction fields themselves. A zero `sfVaultID` is rejected immediately as `temMALFORMED`. The optional `sfAmount` field, if present, must be non-negative and must not name XRP — vaults holding XRP cannot be subject to clawback, which is consistent with XRP's nature as the native ledger currency with no issuer. + +`preclaim` loads the vault and share issuance SLEs and branches on the asset being named by the `sfAmount` field. A free helper, `clawbackAmount`, resolves which asset and quantity is implied when `sfAmount` is absent: if the transaction submitter is the vault owner, the implied zero-amount refers to shares (`sfShareMPTID`); otherwise it refers to the vault's underlying asset. This disambiguation is why `clawbackAmount` takes the submitter's `AccountID` — a zero amount has different semantics depending on who is asking. + +One subtle edge case is explicitly guarded: when the vault owner is also the issuer of the vault's underlying asset, omitting `sfAmount` would be ambiguous between the share-burn path and the asset-clawback path. Rather than silently guess, `preclaim` rejects with `tecWRONG_ASSET`, forcing the caller to be explicit. + +## Two Clawback Modes + +**Asset issuer clawback** is the main use case. The issuer of the vault's underlying asset (IOU or MPT) can reclaim assets held inside the vault from a specific `sfHolder` account's position. The preclaim checks enforce that the asset is non-XRP, that the submitter is actually the asset's issuer (not just any participant), that the asset's issuance flags permit clawback (`lsfMPTCanClawback` for MPTs, or `lsfAllowTrustLineClawback` without `lsfNoFreeze` for IOUs), and that the issuer is not attempting to claw back from themselves. + +The conversion arithmetic is isolated in `assetsToClawback`. This method converts the requested asset quantity into shares to destroy, then converts those shares back into assets to confirm the recoverable amount. When the caller specifies zero (meaning "all of holder's shares"), `sharesDestroyed` is determined from the holder's current balance via `accountHolds`. When a specific asset amount is given, the round-trip goes through `assetsToSharesWithdraw` first and then `sharesToAssetsWithdraw` — the double conversion exists because shares are integer-valued MPTs, so rounding from assets to shares and back yields the actual net asset recovery. + +A critical safety clamp follows both paths: if the computed `assetsRecovered` exceeds `sfAssetsAvailable`, it is clamped to the available amount. Shares are then recomputed using `TruncateShares::yes` — deliberate truncation rather than rounding — to ensure that the corresponding assets derived from the truncated share count do not re-exceed the cap. The code then re-derives `assetsRecovered` from the truncated `sharesDestroyed` and double-checks the bound, treating any breach as an internal error rather than silently committing an over-recovery. + +**Vault owner share burn** is a cleanup path. If a vault has outstanding share tokens but zero `sfAssetsTotal` and zero `sfAssetsAvailable`, the vault owner may use `VaultClawback` with a shares-denominated amount to burn a specific holder's shares. The check is strict: the vault must have no assets at all, and the amount (if non-zero) must equal exactly the holder's entire balance. This prevents partial burns, which would leave the share supply in an ambiguous state when the vault has no backing assets. + +## Security Fix: `fixSecurity3_1_3` + +The `assetsToClawback` method contains explicit branching on the `fixSecurity3_1_3` amendment. Before the amendment, a zero-amount clawback would convert the holder's full share balance to assets and return that amount directly — without clamping to `sfAssetsAvailable`. This allowed recovery of more assets than the vault actually had liquid, effectively bypassing any outstanding loans. The fix gate at line 233 preserves the old code path for ledger replay on historical ledgers, while all new transactions execute through the clamped path. The comment is explicit that the pre-fix behavior is retained "for ledger replay compatibility." + +## `doApply` Execution Order + +After validating with the pre-stored `sfAmount` and re-resolving `clawbackAmount`, `doApply` asserts that `sfLossUnrealized ≤ sfAssetsTotal − sfAssetsAvailable`, which is a structural invariant of the vault's accounting. The actual mutations proceed in a carefully ordered sequence: decrement `sfAssetsTotal` and `sfAssetsAvailable` on the vault SLE, call `view().update(vault)`, then `accountSend` to move shares from the holder to the vault pseudo-account (waiving transfer fees), then optionally `removeEmptyHolding` to clean up the holder's empty MPToken entry, and finally `accountSend` again to move the recovered assets from the vault pseudo-account to the issuer. + +`removeEmptyHolding` is called only when the holder is not the vault owner. The vault owner's MPToken for shares is deliberately preserved because it anchors the share issuance — removing it would leave the MPTokenIssuance without an owner holding, which would be structurally inconsistent. + +A negative-balance sanity check follows the asset transfer: `accountHolds` is called on the vault pseudo-account to confirm the vault's asset balance did not go negative. This is belt-and-suspenders validation against arithmetic bugs that would otherwise silently corrupt ledger state. + +The method closes with `associateAsset(*vault, vaultAsset)`, the standard XRPL pattern for flushing `STNumber` rounding against the now-finalized vault SLE. Per the `STTakesAsset` contract, this must come after all mutations are complete, so that scaled numeric fields are rounded exactly once against their final committed values. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp.ai.json b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp.ai.json new file mode 100644 index 0000000000..b8a3d37ce7 --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp.ai.json @@ -0,0 +1,524 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "VaultCreate::preflight" + ], + "entry_point": "VaultCreate::preflight", + "purpose": "Performs stateless transaction validation before execution.", + "validation_points": [ + "validDataLength(ctx.tx[~sfData], maxDataPayloadLength)", + "ctx.tx[~sfWithdrawalPolicy] direct comparison", + "ctx.tx[~sfDomainID] direct comparison and flag check", + "ctx.tx[~sfAssetsMaximum] direct comparison", + "ctx.tx[~sfMPTokenMetadata] empty() and length() checks", + "ctx.tx[~sfScale] type/limit checks" + ] + }, + { + "call_chain": [ + "VaultCreate::preclaim" + ], + "entry_point": "VaultCreate::preclaim", + "purpose": "Performs contextual validation using ledger state before transaction application.", + "validation_points": [ + "canAddHolding(ctx.view, vaultAsset)", + "isPseudoAccount(ctx.view, vaultAsset.getIssuer())", + "isFrozen(ctx.view, account, vaultAsset)", + "ctx.tx[~sfDomainID] existence check in ledger", + "pseudoAccountAddress(ctx.view, keylet::vault(account, sequence).key)" + ] + }, + { + "call_chain": [ + "VaultCreate::doApply" + ], + "entry_point": "VaultCreate::doApply", + "purpose": "Applies the transaction to the ledger after all validations pass.", + "validation_points": [ + "Relies on preflight and preclaim having already validated inputs" + ] + }, + { + "call_chain": [ + "Transactor::apply", + "VaultCreate::preflight", + "VaultCreate::preclaim", + "VaultCreate::doApply" + ], + "entry_point": "Transactor::apply", + "purpose": "Top-level transaction application entry point; orchestrates validation and application.", + "validation_points": [ + "preflight", + "preclaim" + ] + } + ], + "data_flows": [ + { + "field": "sfData", + "flow": [ + "Transaction JSON", + "ctx.tx[~sfData]", + "validDataLength() in preflight" + ], + "origin": "ctx.tx[~sfData] (transaction input)", + "transformations": [ + "Length checked against maxDataPayloadLength" + ], + "validated_at": "VaultCreate::preflight" + }, + { + "field": "sfWithdrawalPolicy", + "flow": [ + "Transaction JSON", + "ctx.tx[~sfWithdrawalPolicy]", + "Direct comparison in preflight" + ], + "origin": "ctx.tx[~sfWithdrawalPolicy] (transaction input)", + "transformations": [ + "Compared to vaultStrategyFirstComeFirstServe" + ], + "validated_at": "VaultCreate::preflight" + }, + { + "field": "sfDomainID", + "flow": [ + "Transaction JSON", + "ctx.tx[~sfDomainID]", + "Zero check and flag check in preflight", + "Ledger existence check in preclaim" + ], + "origin": "ctx.tx[~sfDomainID] (transaction input)", + "transformations": [ + "Checked for zero value", + "Checked that tfVaultPrivate flag is set", + "Checked for existence in ledger" + ], + "validated_at": "VaultCreate::preflight, VaultCreate::preclaim" + }, + { + "field": "sfAssetsMaximum", + "flow": [ + "Transaction JSON", + "ctx.tx[~sfAssetsMaximum]", + "Direct comparison in preflight" + ], + "origin": "ctx.tx[~sfAssetsMaximum] (transaction input)", + "transformations": [ + "Checked for non-negative value" + ], + "validated_at": "VaultCreate::preflight" + }, + { + "field": "sfMPTokenMetadata", + "flow": [ + "Transaction JSON", + "ctx.tx[~sfMPTokenMetadata]", + "empty() and length() checks in preflight" + ], + "origin": "ctx.tx[~sfMPTokenMetadata] (transaction input)", + "transformations": [ + "Checked for non-empty and within maxMPTokenMetadataLength" + ], + "validated_at": "VaultCreate::preflight" + }, + { + "field": "sfScale", + "flow": [ + "Transaction JSON", + "ctx.tx[~sfScale]", + "Type/limit checks in preflight" + ], + "origin": "ctx.tx[~sfScale] (transaction input)", + "transformations": [ + "Checked for asset type compatibility", + "Checked against vaultMaximumIOUScale" + ], + "validated_at": "VaultCreate::preflight" + }, + { + "field": "sfAsset", + "flow": [ + "Transaction JSON", + "ctx.tx[sfAsset]", + "Used in preflight (type checks), preclaim (canAddHolding, isPseudoAccount, isFrozen)" + ], + "origin": "ctx.tx[sfAsset] (transaction input)", + "transformations": [ + "Checked for type (MPTIssue, native)", + "Checked for pseudo-account issuer", + "Checked for frozen status" + ], + "validated_at": "VaultCreate::preflight, VaultCreate::preclaim" + } + ], + "description": "Implements the VaultCreate transaction logic for the XRPL, including preflight, preclaim, and application of vault creation, with checks for asset validity, permissions, and creation of associated pseudo-accounts and tokens.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfData", + "empty", + "string", + "validation" + ], + "evidence": "validDataLength at VaultCreate::preflight", + "issue_pattern": "Missing empty string validation for sfData", + "why_false_positive": "validDataLength validates sfData for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfData", + "range", + "bounds", + "validation" + ], + "evidence": "validDataLength at VaultCreate::preflight", + "issue_pattern": "Missing range validation for sfData", + "why_false_positive": "validDataLength validates sfData range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfWithdrawalPolicy", + "empty", + "string", + "validation" + ], + "evidence": "direct comparison at VaultCreate::preflight", + "issue_pattern": "Missing empty string validation for sfWithdrawalPolicy", + "why_false_positive": "direct comparison validates sfWithdrawalPolicy for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "direct comparison and flag check at VaultCreate::preflight", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "direct comparison and flag check validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAssetsMaximum", + "empty", + "string", + "validation" + ], + "evidence": "direct comparison at VaultCreate::preflight", + "issue_pattern": "Missing empty string validation for sfAssetsMaximum", + "why_false_positive": "direct comparison validates sfAssetsMaximum for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAssetsMaximum", + "range", + "bounds", + "validation" + ], + "evidence": "direct comparison at VaultCreate::preflight", + "issue_pattern": "Missing range validation for sfAssetsMaximum", + "why_false_positive": "direct comparison validates sfAssetsMaximum range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfMPTokenMetadata", + "empty", + "string", + "validation" + ], + "evidence": "empty() and length() checks at VaultCreate::preflight", + "issue_pattern": "Missing empty string validation for sfMPTokenMetadata", + "why_false_positive": "empty() and length() checks validates sfMPTokenMetadata for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfScale", + "empty", + "string", + "validation" + ], + "evidence": "direct comparison and type check at VaultCreate::preflight", + "issue_pattern": "Missing empty string validation for sfScale", + "why_false_positive": "direct comparison and type check validates sfScale for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "featureMPTokensV1", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled at VaultCreate::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for featureMPTokensV1", + "why_false_positive": "ctx.rules.enabled validates featureMPTokensV1 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.rules.enabled at VaultCreate::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "ctx.rules.enabled validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAsset", + "empty", + "string", + "validation" + ], + "evidence": "canAddHolding at VaultCreate::preclaim", + "issue_pattern": "Missing empty string validation for sfAsset", + "why_false_positive": "canAddHolding validates sfAsset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAsset.issuer", + "empty", + "string", + "validation" + ], + "evidence": "isPseudoAccount at VaultCreate::preclaim", + "issue_pattern": "Missing empty string validation for sfAsset.issuer", + "why_false_positive": "isPseudoAccount validates sfAsset.issuer for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/vault/VaultCreate.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "VaultCreate::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 22, + "name": "VaultCreate::getFlagsMask" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 27, + "name": "VaultCreate::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 61, + "name": "VaultCreate::preclaim" + }, + { + "args": [], + "lineno": 97, + "name": "VaultCreate::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "Tests for VaultCreate are likely found in files such as 'vault_create_test.cpp', 'VaultTransactor_test.cpp', or similar in the test/ or unit_test/ directories. These should cover valid/invalid field values, flag combinations, and ledger state conditions. However, edge cases such as maximum metadata length, domain ID with/without tfVaultPrivate, and pseudo-account issuer checks may not be exhaustively tested. Tests for negative asset maximum, zero domain ID, and scale limits should be verified for coverage. Integration tests may be required to fully exercise preclaim ledger state checks.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation via explicit checks and helper functions (e.g., validDataLength, canAddHolding, isPseudoAccount)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfData", + "location": "VaultCreate::preflight", + "validated_by": "validDataLength", + "validates": [ + "Checks that the data field (sfData) does not exceed maxDataPayloadLength" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfWithdrawalPolicy", + "location": "VaultCreate::preflight", + "validated_by": "direct comparison", + "validates": [ + "Ensures withdrawal policy is vaultStrategyFirstComeFirstServe if present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfDomainID", + "location": "VaultCreate::preflight", + "validated_by": "direct comparison and flag check", + "validates": [ + "Ensures DomainID is not zero", + "Ensures DomainID is only present if tfVaultPrivate flag is set" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAssetsMaximum", + "location": "VaultCreate::preflight", + "validated_by": "direct comparison", + "validates": [ + "Ensures AssetsMaximum is not negative" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfMPTokenMetadata", + "location": "VaultCreate::preflight", + "validated_by": "empty() and length() checks", + "validates": [ + "Ensures MPTokenMetadata is not empty", + "Ensures MPTokenMetadata does not exceed maxMPTokenMetadataLength" + ], + "validation_type": "format|range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfScale", + "location": "VaultCreate::preflight", + "validated_by": "direct comparison and type check", + "validates": [ + "Ensures scale is not present for native or MPTIssue assets", + "Ensures scale does not exceed vaultMaximumIOUScale" + ], + "validation_type": "type|range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": "featureMPTokensV1", + "location": "VaultCreate::checkExtraFeatures", + "validated_by": "ctx.rules.enabled", + "validates": [ + "Ensures featureMPTokensV1 is enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (prevents further processing)", + "field": "sfDomainID", + "location": "VaultCreate::checkExtraFeatures", + "validated_by": "ctx.rules.enabled", + "validates": [ + "Ensures featurePermissionedDomains is enabled if DomainID is present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns ter (transaction error code)", + "field": "sfAsset", + "location": "VaultCreate::preclaim", + "validated_by": "canAddHolding", + "validates": [ + "Checks if the asset can be added to the vault (business logic, e.g., asset type, limits)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tec (transaction error code)", + "field": "sfAsset.issuer", + "location": "VaultCreate::preclaim", + "validated_by": "isPseudoAccount", + "validates": [ + "Ensures the asset issuer is not a pseudo-account (prevents holding assets that can't be clawed back)" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultCreate.cpp.ai.md b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp.ai.md new file mode 100644 index 0000000000..16af19a8e5 --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultCreate.cpp.ai.md @@ -0,0 +1,60 @@ +# `VaultCreate.cpp` — Vault Creation Transactor + +## Role and Context + +`VaultCreate.cpp` implements the `VaultCreate` transaction type, which allows an account on the XRP Ledger to instantiate a new on-chain vault. Vaults are yield-bearing, pooled asset structures that accept deposits from one or more holders, track their proportional ownership via MPT (Multi-Purpose Token) shares, and support optional permissioning through domains. This file is one of six vault transactors (alongside `VaultDeposit`, `VaultWithdraw`, `VaultSet`, `VaultDelete`, and `VaultClawback`) in the `src/libxrpl/tx/transactors/vault/` directory; `VaultCreate` handles only the initial construction of the vault ledger object and all of its supporting infrastructure. + +## Three-Phase Validation Architecture + +Like all XRPL transactors, `VaultCreate` separates validation into three stages: `checkExtraFeatures` (amendment gate), `preflight` (stateless field validation), and `preclaim` (stateful ledger checks), with `doApply` performing the actual mutations only if all three pass. + +### `checkExtraFeatures` + +This is a pure amendment gate. The vault feature set itself requires `featureMPTokensV1` — if that amendment is not active, the entire transaction type is rejected. A secondary gate protects the `sfDomainID` field: using it requires the `featurePermissionedDomains` amendment, allowing both features to activate independently without cross-coupling. + +### `preflight` — Stateless Validation + +`preflight` validates all transaction fields that can be checked without touching the ledger. Several decisions here are worth noting: + +- **`sfWithdrawalPolicy`**: The only currently accepted value is `vaultStrategyFirstComeFirstServe` (constant `1`). This is future-proofing in design — the field exists in the protocol, but alternative strategies are not yet enabled; anything other than the one known value is rejected as `temMALFORMED`. + +- **`sfDomainID` coupling**: A domain ID must be non-zero, and critically, it is only valid when the `tfVaultPrivate` flag is also set. Associating a permissioned domain with a public vault would be semantically incoherent — public vaults admit all holders — so this constraint is enforced at the earliest possible stage. + +- **`sfScale` type restriction**: The scale factor (controlling IOU-to-share decimal precision) is meaningless for native XRP or MPT assets, which have fixed integer representations. Scale is rejected with `temMALFORMED` when the asset is `native()` or holds an `MPTIssue`, and is also bounded above by `vaultMaximumIOUScale` (18), chosen because 10^19 exceeds the maximum MPT amount (2^63 − 1 ≈ 10^18.9), ensuring that even a single IOU unit can always be converted to shares. + +### `preclaim` — Stateful Ledger Checks + +Three substantive checks occur here that require ledger access: + +**`canAddHolding`**: Delegates to the asset-type-appropriate helper to verify that the vault's pseudo-account will be able to hold the given asset — checking MPT issuance limits and similar constraints. + +**Pseudo-account issuer check**: Vaults whose asset is itself issued by a pseudo-account (e.g., shares of another vault, or AMM LP tokens) are rejected with `tecWRONG_ASSET`. The comment explains the reasoning precisely: pseudo-account-issued assets cannot be clawed back through the normal mechanism because the issuer has no private key and no direct authority path. Allowing such a vault to hold irrecoverable assets would be a permanent liability if the vault needed emergency intervention. + +**Address collision check**: `pseudoAccountAddress(ctx.view, keylet::vault(account, sequence).key)` pre-computes the deterministic address that will be used for the vault's pseudo-account. If the result is `beast::zero`, the derived address collides with an existing account; the transaction returns `terADDRESS_COLLISION` before any state change occurs. + +## `doApply` — Ledger Construction + +The application phase creates four distinct on-chain objects and links them together. The ordering of operations is architecturally significant. + +**1. Directory link and owner count**: `dirLink` inserts the new vault SLE into the owner's directory. Immediately after, `adjustOwnerCount` increments the owner's count by **2** — one for the vault `SLE` and one for the pseudo-account that will be created. This pre-increment happens before the reserve check so that the reserve calculation reflects the true post-creation state. The check `preFeeBalance_ < view().fees().accountReserve(ownerCount)` then uses the pre-fee XRP balance to confirm the owner can afford both new ledger objects simultaneously. + +**2. Pseudo-account creation**: `createPseudoAccount(view(), vault->key(), sfVaultID)` creates a new `ACCOUNT_ROOT` SLE at the deterministically-derived address, with the vault's key stored in its `sfVaultID` field. This pseudo-account acts as the on-chain identity for the vault — it holds the vault's asset balance, issues vault shares, and is the entity counterparties interact with during deposits and withdrawals. It has no private key; it is controlled solely by the transactor logic. + +**3. Empty holding for the asset**: `addEmptyHolding` establishes either an `MPToken` (for MPT assets) or a trust line / `RippleState` (for IOU assets) between the pseudo-account and the vault's underlying asset issuer. This zero-balance holding is necessary to initialize the account's relationship with the asset before any deposits arrive. XRP vaults do not need this call to create a holding, but the dispatch is handled uniformly. + +**4. MPT share issuance creation**: `MPTokenIssuanceCreate::create` is called from the pseudo-account's perspective (with `sequence = 1`, since the pseudo-account was just created and has no prior issuances). This creates the `MPTokenIssuance` SLE that represents the vault's share tokens. The flags passed to the issuance are derived from the transaction's own flags: + +- If `tfVaultShareNonTransferable` is **not** set, the shares receive `lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer`, making them freely tradeable on the DEX and via payment channels. +- If `tfVaultPrivate` is set, `lsfMPTRequireAuth` is added, restricting share transfers to explicitly authorized accounts. + +Note the explicit comment: this is the issuance for *shares*, not for the asset itself — the comment exists because the call is structurally similar to the `addEmptyHolding` call above, and the distinction is easy to miss. + +**5. Vault SLE population**: After all dependent objects exist, the vault's fields are populated: `sfAsset`, `sfOwner`, `sfAccount` (pseudo-account ID), `sfAssetsTotal`/`sfAssetsAvailable`/`sfLossUnrealized` (all starting at zero), optional `sfAssetsMaximum`, `sfShareMPTID` (the newly created issuance), `sfData`, and `sfWithdrawalPolicy` (defaulting to `vaultStrategyFirstComeFirstServe`). The private flag from the transaction is persisted directly into the vault's `sfFlags`. + +**6. Owner MPToken authorization**: `authorizeMPToken` is called for the vault creator's account unconditionally, creating an `MPToken` SLE so the owner can hold vault shares from the moment the vault is created. For private vaults, a second `authorizeMPToken` call authorizes the pseudo-account itself — this is required so the pseudo-account can participate in share issuance mechanics, with the vault owner acting as the authorizing party. + +**7. `associateAsset`**: The final call propagates the vault's asset type through all `sMD_NeedsAsset` fields in the vault SLE (primarily `STNumber` fields like `sfAssetsTotal`). This ties the asset's decimal scale to the number representation, ensuring serialization rounds correctly according to the asset's precision. + +## Key Invariants + +The reserve check is the only guard against the ledger being modified without sufficient XRP. If it fails after `adjustOwnerCount` has already incremented the owner count, the transaction is aborted as `tecINSUFFICIENT_RESERVE` and the ledger changes are rolled back by the framework — `doApply` returning a `tec` error code signals the framework to discard the apply view. The `// LCOV_EXCL_LINE` annotations on the internal error paths (`tefINTERNAL`, pseudo-account creation failure, share issuance failure) document that these are invariant violations that `preclaim` should have already made impossible under correct operation, serving as defensive assertions rather than reachable logic. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp.ai.json b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp.ai.json new file mode 100644 index 0000000000..86c99b6e61 --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp.ai.json @@ -0,0 +1,484 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "VaultDelete::preflight" + ], + "entry_point": "VaultDelete::preflight", + "purpose": "Initial stateless validation of the VaultDelete transaction. Checks for malformed or missing fields.", + "validation_points": [ + "ctx.tx[sfVaultID] == beast::zero" + ] + }, + { + "call_chain": [ + "VaultDelete::preclaim" + ], + "entry_point": "VaultDelete::preclaim", + "purpose": "Stateful validation: checks ledger state for existence, ownership, and obligations before allowing deletion.", + "validation_points": [ + "ctx.view.read(keylet::vault(ctx.tx[sfVaultID]))", + "vault->at(sfOwner) != ctx.tx[sfAccount]", + "vault->at(sfAssetsAvailable) != 0", + "vault->at(sfAssetsTotal) != 0", + "ctx.view.read(keylet::mptIssuance(vault->at(sfShareMPTID)))", + "sleMPT->at(sfIssuer) != vault->getAccountID(sfAccount)", + "sleMPT->at(sfOutstandingAmount) != 0" + ] + }, + { + "call_chain": [ + "VaultDelete::doApply", + "removeEmptyHolding", + "view().peek(keylet::account(...))", + "view().peek(keylet::mptIssuance(...))", + "removeEmptyHolding (for MPToken)", + "view().dirRemove(...)" + ], + "entry_point": "VaultDelete::doApply", + "purpose": "Applies the transaction: removes vault, asset holdings, and share issuance after all validations pass.", + "validation_points": [ + "Redundant checks for existence of vault, pseudo-account, mptIssuance, and MPToken" + ] + } + ], + "data_flows": [ + { + "field": "sfVaultID", + "flow": [ + "ctx.tx[sfVaultID]", + "preflight: checked for zero", + "preclaim: used to look up vault in ledger", + "doApply: used to look up vault in ledger" + ], + "origin": "ctx.tx[sfVaultID] (transaction input)", + "transformations": [ + "Checked for zero value", + "Used as key for ledger lookup" + ], + "validated_at": "preflight, preclaim" + }, + { + "field": "sfOwner", + "flow": [ + "vault->at(sfOwner)", + "preclaim: compared to ctx.tx[sfAccount]" + ], + "origin": "vault->at(sfOwner) (ledger vault entry)", + "transformations": [ + "Compared for equality" + ], + "validated_at": "preclaim" + }, + { + "field": "sfAssetsAvailable", + "flow": [ + "vault->at(sfAssetsAvailable)", + "preclaim: checked for nonzero" + ], + "origin": "vault->at(sfAssetsAvailable) (ledger vault entry)", + "transformations": [ + "Checked for nonzero" + ], + "validated_at": "preclaim" + }, + { + "field": "sfAssetsTotal", + "flow": [ + "vault->at(sfAssetsTotal)", + "preclaim: checked for nonzero" + ], + "origin": "vault->at(sfAssetsTotal) (ledger vault entry)", + "transformations": [ + "Checked for nonzero" + ], + "validated_at": "preclaim" + }, + { + "field": "sfShareMPTID", + "flow": [ + "vault->at(sfShareMPTID)", + "preclaim: used to look up mptIssuance", + "doApply: used to look up mptIssuance and mptoken" + ], + "origin": "vault->at(sfShareMPTID) (ledger vault entry)", + "transformations": [ + "Used as key for ledger lookup" + ], + "validated_at": "preclaim, doApply" + }, + { + "field": "sfIssuer", + "flow": [ + "sleMPT->at(sfIssuer)", + "preclaim: compared to vault->getAccountID(sfAccount)" + ], + "origin": "sleMPT->at(sfIssuer) (ledger mptIssuance entry)", + "transformations": [ + "Compared for equality" + ], + "validated_at": "preclaim" + }, + { + "field": "sfOutstandingAmount", + "flow": [ + "sleMPT->at(sfOutstandingAmount)", + "preclaim: checked for nonzero" + ], + "origin": "sleMPT->at(sfOutstandingAmount) (ledger mptIssuance entry)", + "transformations": [ + "Checked for nonzero" + ], + "validated_at": "preclaim" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "preclaim: compared to vault->at(sfOwner)", + "doApply: used as account_ for MPToken removal" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared for equality", + "Used as key for ledger lookup" + ], + "validated_at": "preclaim" + } + ], + "description": "Implements the VaultDelete transaction logic for deleting a vault in the XRPL ledger, including preflight, preclaim, and apply steps with all necessary checks and ledger modifications.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfVaultID] == beast::zero) at VaultDelete::preflight", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "explicit check (ctx.tx[sfVaultID] == beast::zero) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfVaultID", + "format", + "validation", + "invalid" + ], + "evidence": "explicit check (ctx.tx[sfVaultID] == beast::zero) at VaultDelete::preflight", + "issue_pattern": "Missing format validation for sfVaultID", + "why_false_positive": "explicit check (ctx.tx[sfVaultID] == beast::zero) validates sfVaultID format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) at VaultDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOwner", + "empty", + "string", + "validation" + ], + "evidence": "vault->at(sfOwner) != ctx.tx[sfAccount] at VaultDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfOwner", + "why_false_positive": "vault->at(sfOwner) != ctx.tx[sfAccount] validates sfOwner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAssetsAvailable", + "empty", + "string", + "validation" + ], + "evidence": "vault->at(sfAssetsAvailable) != 0 at VaultDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfAssetsAvailable", + "why_false_positive": "vault->at(sfAssetsAvailable) != 0 validates sfAssetsAvailable for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAssetsTotal", + "empty", + "string", + "validation" + ], + "evidence": "vault->at(sfAssetsTotal) != 0 at VaultDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfAssetsTotal", + "why_false_positive": "vault->at(sfAssetsTotal) != 0 validates sfAssetsTotal for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfShareMPTID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::mptIssuance(vault->at(sfShareMPTID))) at VaultDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfShareMPTID", + "why_false_positive": "ctx.view.read(keylet::mptIssuance(vault->at(sfShareMPTID))) validates sfShareMPTID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfIssuer", + "empty", + "string", + "validation" + ], + "evidence": "sleMPT->at(sfIssuer) != vault->getAccountID(sfAccount) at VaultDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfIssuer", + "why_false_positive": "sleMPT->at(sfIssuer) != vault->getAccountID(sfAccount) validates sfIssuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfOutstandingAmount", + "empty", + "string", + "validation" + ], + "evidence": "sleMPT->at(sfOutstandingAmount) != 0 at VaultDelete::preclaim", + "issue_pattern": "Missing empty string validation for sfOutstandingAmount", + "why_false_positive": "sleMPT->at(sfOutstandingAmount) != 0 validates sfOutstandingAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "view().peek(keylet::vault(ctx_.tx[sfVaultID])) at VaultDelete::doApply", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "view().peek(keylet::vault(ctx_.tx[sfVaultID])) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "view().peek(keylet::account(pseudoID)) at VaultDelete::doApply", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "view().peek(keylet::account(pseudoID)) validates sfAccount for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/vault/VaultDelete.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "VaultDelete::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 23, + "name": "VaultDelete::preclaim" + }, + { + "args": [], + "lineno": 61, + "name": "VaultDelete::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is likely covered by unit/integration tests for VaultDelete transactions. Typical test files would be in the rippled repo under `src/test/tx/` or `src/test/vault/` (e.g., VaultDelete_test.cpp, VaultTransactor_test.cpp). Tests should cover: zero/empty vault ID, non-existent vault, wrong owner, nonzero assets, missing or mismatched mptIssuance, nonzero outstanding shares, and successful deletion. However, some error branches (marked LCOV_EXCL_START/STOP) are likely not covered by tests, such as internal errors or missing pseudo-accounts, as these are considered unreachable in normal operation. Test coverage for these rare branches is likely missing.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in transaction preflight/preclaim/apply pattern (xrpld transaction engine)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfVaultID", + "location": "VaultDelete::preflight", + "validated_by": "explicit check (ctx.tx[sfVaultID] == beast::zero)", + "validates": [ + "vault ID must not be zero/empty" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfVaultID", + "location": "VaultDelete::preclaim", + "validated_by": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID]))", + "validates": [ + "vault must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfOwner", + "location": "VaultDelete::preclaim", + "validated_by": "vault->at(sfOwner) != ctx.tx[sfAccount]", + "validates": [ + "transaction account must be vault owner" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "sfAssetsAvailable", + "location": "VaultDelete::preclaim", + "validated_by": "vault->at(sfAssetsAvailable) != 0", + "validates": [ + "vault must have zero assets available" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "sfAssetsTotal", + "location": "VaultDelete::preclaim", + "validated_by": "vault->at(sfAssetsTotal) != 0", + "validates": [ + "vault must have zero assets total" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND", + "field": "sfShareMPTID", + "location": "VaultDelete::preclaim", + "validated_by": "ctx.view.read(keylet::mptIssuance(vault->at(sfShareMPTID)))", + "validates": [ + "MPTokenIssuance object for vault shares must exist" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfIssuer", + "location": "VaultDelete::preclaim", + "validated_by": "sleMPT->at(sfIssuer) != vault->getAccountID(sfAccount)", + "validates": [ + "vault share issuer must match vault account" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecHAS_OBLIGATIONS", + "field": "sfOutstandingAmount", + "location": "VaultDelete::preclaim", + "validated_by": "sleMPT->at(sfOutstandingAmount) != 0", + "validates": [ + "vault shares must have zero outstanding amount" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sfVaultID", + "location": "VaultDelete::doApply", + "validated_by": "view().peek(keylet::vault(ctx_.tx[sfVaultID]))", + "validates": [ + "vault must exist in ledger (again, for safety)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefBAD_LEDGER", + "field": "sfAccount", + "location": "VaultDelete::doApply", + "validated_by": "view().peek(keylet::account(pseudoID))", + "validates": [ + "vault pseudo-account must exist" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultDelete.cpp.ai.md b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp.ai.md new file mode 100644 index 0000000000..88be3692b7 --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultDelete.cpp.ai.md @@ -0,0 +1,45 @@ +# `VaultDelete.cpp` — Vault Teardown Transaction + +`VaultDelete.cpp` implements the three-phase transactor lifecycle — `preflight`, `preclaim`, and `doApply` — for the `VaultDelete` transaction type. Its purpose is to completely dismantle a vault from the XRPL ledger, reclaiming all associated objects and adjusting owner reserves accordingly. + +## What a Vault Is + +To understand why deletion is non-trivial, consider what `VaultCreate` assembles. A vault is not a single ledger entry; it is a cluster of objects: + +- A **vault SLE** (`keylet::vault(...)`) that records ownership, asset type, share MPT identifier, and running asset totals. +- A **pseudo-account SLE** (`keylet::account(pseudoID)`) — a synthetic account address that actually holds the vault's underlying assets. This pseudo-account owns no private key; it exists solely to hold trust lines or MPT balances on behalf of depositors. +- An **`MPTokenIssuance` SLE** recording the vault's share token, issued by the pseudo-account at sequence 1. This is what depositors receive when they deposit assets. +- Optionally, an **`MPToken` SLE** on the vault owner's account if they themselves hold shares. + +When `VaultCreate` runs, it calls `adjustOwnerCount(view(), owner, 2, j_)` — one count for the vault, one for the pseudo-account, both charged to the real owner's reserve. `VaultDelete` must invert all of this. + +## Validation Phases + +`preflight` is deliberately minimal: it only checks that `sfVaultID` is not `beast::zero`. This is the only stateless check possible — the vault's actual state lives in the ledger. + +`preclaim` performs the substantive gate-keeping against the read-only ledger view. It enforces several invariants, all of which return `tecHAS_OBLIGATIONS` if violated: + +- **`sfAssetsAvailable == 0`** and **`sfAssetsTotal == 0`** — these two fields are checked separately because a vault can carry unrealized losses where total differs from available. Both must be zero before deletion is allowed; any assets must be fully withdrawn first. +- **`sfOutstandingAmount == 0`** on the `MPTokenIssuance` — no depositor may still hold vault shares. This is the share-ledger counterpart to the asset checks: even if assets are zero, an outstanding share balance would indicate an unresolved obligation. + +The ownership check (`vault->at(sfOwner) != ctx.tx[sfAccount]`) ensures only the vault creator can trigger deletion. The two `MPTokenIssuance` checks (existence and issuer match) are guarded by `LCOV_EXCL_START` because they guard against ledger invariant violations that cannot arise through normal transaction flow — they are reachable only if prior transactions corrupted ledger state. + +## Teardown Order in `doApply` + +The destruction sequence in `doApply` follows a strict dependency order: + +**Step 1 — Remove the asset holding.** `removeEmptyHolding(view(), vault->at(sfAccount), asset, j_)` removes whatever the pseudo-account used to hold the underlying asset — a trust line (`RippleState`), or an `MPToken`. The holding must be empty (zero balance) by this point; `preclaim` has already verified `sfAssetsTotal == 0`. This call also removes the object from the pseudo-account's owner directory, decrementing its owner count. + +**Step 2 — Remove the vault owner's MPToken for shares.** If the vault creator holds an `MPToken` for the share issuance (`keylet::mptoken(shareMPTID, account_)`), it is cleaned up via a second `removeEmptyHolding` call. The vault owner's share balance must already be zero — `preclaim` verified `sfOutstandingAmount == 0` across all holders. + +**Step 3 — Remove the `MPTokenIssuance`.** Rather than delegating to the `MPTokenIssuanceDestroy` transaction (which carries fee logic and extra checks irrelevant here), `doApply` directly removes the issuance from the pseudo-account's owner directory via `view().dirRemove(...)`, calls `adjustOwnerCount(view(), pseudoAcct, -1, j_)`, then erases the SLE. The comment explicitly notes this bypass: *"Do not use MPTokenIssuanceDestroy for this, no special logic needed."* + +**Step 4 — Verify the pseudo-account is clean.** After the above removals, the pseudo-account's owner directory should be empty. The code explicitly checks `view().peek(keylet::ownerDir(pseudoID))` and returns `tecHAS_OBLIGATIONS` if the directory still exists — this is a defensive invariant check. It also verifies that `sfBalance` is zero and `sfOwnerCount` is zero before erasing the pseudo-account. These checks are all `LCOV_EXCL_LINE`-guarded because they should be unreachable in valid ledger state, but their presence prevents silent corruption if they are somehow reached. + +**Step 5 — Remove the vault from the real owner's directory and erase all remaining SLEs.** The vault itself is removed from the real owner's `ownerDir`, then `adjustOwnerCount(view(), owner, -2, j_)` fires — the single `-2` adjustment is the exact inverse of `VaultCreate`'s `+2`, accounting for both the vault SLE and the pseudo-account that was just destroyed. Finally, the vault SLE is erased. + +## Error Code Semantics + +The distinction between `tec*` and `tef*` codes is meaningful here. Errors in `preclaim` that a submitter could reasonably encounter — wrong owner, nonzero assets, outstanding shares — return `tec` codes (transaction engine codes), which consume the transaction fee. Errors in `doApply` that indicate impossible ledger states — missing pseudo-account, balance on the pseudo-account, nonzero owner count — return `tef` (transaction engine failure) or `tefBAD_LEDGER`, signalling an internal inconsistency rather than a user-correctable condition. + +The mid-apply check `view().peek(keylet::ownerDir(pseudoID))` returning `tecHAS_OBLIGATIONS` is notable: it is the one `tec` code in `doApply`, which the comment marks as `LCOV_EXCL_LINE`. It exists because a future ledger feature could attach additional objects to the pseudo-account's directory; the check is a forward-safety valve to prevent destroying a pseudo-account that still owns unhandled objects. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp.ai.json b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp.ai.json new file mode 100644 index 0000000000..9738955b8d --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp.ai.json @@ -0,0 +1,513 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "VaultDeposit::preflight" + ], + "entry_point": "VaultDeposit::preflight", + "purpose": "Initial stateless validation of transaction fields before any ledger access.", + "validation_points": [ + "ctx.tx[sfVaultID] == beast::zero (rejects empty vault ID)", + "ctx.tx[sfAmount] <= beast::zero (rejects zero or negative amount)" + ] + }, + { + "call_chain": [ + "VaultDeposit::preclaim" + ], + "entry_point": "VaultDeposit::preclaim", + "purpose": "Stateful validation using ledger data, ensuring vault and asset correctness, transferability, and authorization.", + "validation_points": [ + "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) (vault must exist)", + "assets.asset() != vaultAsset (asset must match vault's asset)", + "canTransfer(ctx.view, vaultAsset, account, vaultAccount) (asset must be transferable)", + "vaultShare == assets.asset() (vault shares and assets must differ)", + "ctx.view.read(keylet::mptIssuance(mptIssuanceID)) (issuance must exist)", + "sleIssuance->isFlag(lsfMPTLocked) (issuance must not be locked)", + "isFrozen(ctx.view, account, vaultAsset) (asset must not be frozen for depositor)", + "isFrozen(ctx.view, account, vaultShare) (vault shares must not be frozen)", + "credentials::validDomain(ctx.view, *maybeDomainID, account) (authorization for private vaults)", + "requireAuth(ctx.view, vaultAsset, account) (source MPToken must exist if asset is MPT)" + ] + }, + { + "call_chain": [ + "VaultDeposit::doApply" + ], + "entry_point": "VaultDeposit::doApply", + "purpose": "Executes the actual deposit after all validations pass. Not shown in provided code, but would use validated data.", + "validation_points": [ + "Assumes all validations in preflight and preclaim have passed" + ] + } + ], + "data_flows": [ + { + "field": "sfVaultID", + "flow": [ + "ctx.tx[sfVaultID]", + "preflight: checked for zero", + "preclaim: used to read vault from ledger (ctx.view.read(keylet::vault(...)))" + ], + "origin": "ctx.tx[sfVaultID] (transaction input)", + "transformations": [ + "Checked for zero value", + "Used as key to fetch vault ledger entry" + ], + "validated_at": "preflight (zero check), preclaim (vault existence check)" + }, + { + "field": "sfAmount", + "flow": [ + "ctx.tx[sfAmount]", + "preflight: checked for <= 0", + "preclaim: extracted as 'assets', asset() compared to vaultAsset" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Checked for non-positive value", + "asset() extracted for comparison" + ], + "validated_at": "preflight (<= 0 check), preclaim (asset match check)" + }, + { + "field": "sfAsset (from vault)", + "flow": [ + "vault->at(sfAsset)", + "preclaim: compared to assets.asset()", + "used in canTransfer, isFrozen, requireAuth" + ], + "origin": "vault->at(sfAsset) (ledger entry)", + "transformations": [ + "Compared for equality", + "Passed to helper functions for further validation" + ], + "validated_at": "preclaim (asset match, transferability, frozen, auth checks)" + }, + { + "field": "sfAccount", + "flow": [ + "ctx.tx[sfAccount]", + "preclaim: used as 'account', compared to vault->at(sfOwner), passed to canTransfer, isFrozen, requireAuth" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared for ownership", + "Passed to helper functions" + ], + "validated_at": "preclaim (ownership, transferability, frozen, auth checks)" + }, + { + "field": "sfShareMPTID", + "flow": [ + "vault->at(sfShareMPTID)", + "preclaim: used to construct vaultShare (MPTIssue)", + "used to read sleIssuance" + ], + "origin": "vault->at(sfShareMPTID) (ledger entry)", + "transformations": [ + "Used to construct MPTIssue", + "Used as key to fetch issuance ledger entry" + ], + "validated_at": "preclaim (vault shares vs asset, issuance existence, locked check)" + }, + { + "field": "sfDomainID (optional)", + "flow": [ + "sleIssuance->at(~sfDomainID)", + "preclaim: if present, used for credentials::validDomain" + ], + "origin": "sleIssuance->at(~sfDomainID) (ledger entry, optional)", + "transformations": [ + "Optional presence check", + "Passed to domain validation" + ], + "validated_at": "preclaim (authorization for private vaults)" + } + ], + "description": "Implements the VaultDeposit transaction logic for depositing assets into a vault on the XRPL, including preflight, preclaim, and application logic with authorization, asset checks, and share issuance.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfVaultID] == beast::zero) at VaultDeposit::preflight", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "explicit check (ctx.tx[sfVaultID] == beast::zero) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfVaultID", + "format", + "validation", + "invalid" + ], + "evidence": "explicit check (ctx.tx[sfVaultID] == beast::zero) at VaultDeposit::preflight", + "issue_pattern": "Missing format validation for sfVaultID", + "why_false_positive": "explicit check (ctx.tx[sfVaultID] == beast::zero) validates sfVaultID format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfAmount] <= beast::zero) at VaultDeposit::preflight", + "issue_pattern": "Missing empty string validation for sfAmount", + "why_false_positive": "explicit check (ctx.tx[sfAmount] <= beast::zero) validates sfAmount for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAmount", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfAmount] <= beast::zero) at VaultDeposit::preflight", + "issue_pattern": "Missing range validation for sfAmount", + "why_false_positive": "explicit check (ctx.tx[sfAmount] <= beast::zero) validates sfAmount range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) at VaultDeposit::preclaim", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount.asset() vs vault->at(sfAsset)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (assets.asset() != vaultAsset) at VaultDeposit::preclaim", + "issue_pattern": "Missing empty string validation for sfAmount.asset() vs vault->at(sfAsset)", + "why_false_positive": "explicit check (assets.asset() != vaultAsset) validates sfAmount.asset() vs vault->at(sfAsset) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "canTransfer (vaultAsset, account, vaultAccount)", + "empty", + "string", + "validation" + ], + "evidence": "canTransfer function at VaultDeposit::preclaim", + "issue_pattern": "Missing empty string validation for canTransfer (vaultAsset, account, vaultAccount)", + "why_false_positive": "canTransfer function validates canTransfer (vaultAsset, account, vaultAccount) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vaultShare == assets.asset()", + "empty", + "string", + "validation" + ], + "evidence": "explicit check at VaultDeposit::preclaim", + "issue_pattern": "Missing empty string validation for vaultShare == assets.asset()", + "why_false_positive": "explicit check validates vaultShare == assets.asset() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sleIssuance existence", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::mptIssuance(mptIssuanceID)) at VaultDeposit::preclaim", + "issue_pattern": "Missing empty string validation for sleIssuance existence", + "why_false_positive": "ctx.view.read(keylet::mptIssuance(mptIssuanceID)) validates sleIssuance existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sleIssuance->isFlag(lsfMPTLocked)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check at VaultDeposit::preclaim", + "issue_pattern": "Missing empty string validation for sleIssuance->isFlag(lsfMPTLocked)", + "why_false_positive": "explicit check validates sleIssuance->isFlag(lsfMPTLocked) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "isFrozen(ctx.view, account, vaultAsset)", + "empty", + "string", + "validation" + ], + "evidence": "isFrozen function at VaultDeposit::preclaim", + "issue_pattern": "Missing empty string validation for isFrozen(ctx.view, account, vaultAsset)", + "why_false_positive": "isFrozen function validates isFrozen(ctx.view, account, vaultAsset) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "isFrozen(ctx.view, account, vaultShare)", + "empty", + "string", + "validation" + ], + "evidence": "isFrozen function at VaultDeposit::preclaim", + "issue_pattern": "Missing empty string validation for isFrozen(ctx.view, account, vaultShare)", + "why_false_positive": "isFrozen function validates isFrozen(ctx.view, account, vaultShare) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check at VaultDeposit::preclaim", + "issue_pattern": "Missing empty string validation for vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner)", + "why_false_positive": "explicit check validates vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "VaultDeposit::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 25, + "name": "VaultDeposit::preclaim" + }, + { + "args": [], + "lineno": 81, + "name": "VaultDeposit::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is likely covered by unit/integration tests for VaultDeposit transactions. Look for test files such as 'VaultDeposit_test.cpp', 'VaultTransactor_test.cpp', or generic transaction validation suites in the test/ or src/test/ directories. Edge cases like zero/empty vault ID, negative/zero amount, non-existent vault, asset mismatch, non-transferable/frozen assets, locked issuance, and private vault authorization are explicitly checked and should be tested. However, some internal errors (e.g., vault shares == asset, missing/locked issuance) are marked with LCOV_EXCL_START/STOP, indicating they may not be covered by standard tests. Tests for expired credentials and domain authorization may also be limited. Gaps may exist for rare or internal error paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in business logic (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfVaultID", + "location": "VaultDeposit::preflight", + "validated_by": "explicit check (ctx.tx[sfVaultID] == beast::zero)", + "validates": [ + "VaultID must not be zero/empty" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "temBAD_AMOUNT", + "field": "sfAmount", + "location": "VaultDeposit::preflight", + "validated_by": "explicit check (ctx.tx[sfAmount] <= beast::zero)", + "validates": [ + "Amount must be greater than zero" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfVaultID", + "location": "VaultDeposit::preclaim", + "validated_by": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID]))", + "validates": [ + "Vault must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecWRONG_ASSET", + "field": "sfAmount.asset() vs vault->at(sfAsset)", + "location": "VaultDeposit::preclaim", + "validated_by": "explicit check (assets.asset() != vaultAsset)", + "validates": [ + "Asset being deposited must match vault asset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "various TER codes (if not isTesSuccess)", + "field": "canTransfer (vaultAsset, account, vaultAccount)", + "location": "VaultDeposit::preclaim", + "validated_by": "canTransfer function", + "validates": [ + "Vault asset must be transferable from depositor to vault" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "vaultShare == assets.asset()", + "location": "VaultDeposit::preclaim", + "validated_by": "explicit check", + "validates": [ + "Vault shares and assets must not be the same" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sleIssuance existence", + "location": "VaultDeposit::preclaim", + "validated_by": "ctx.view.read(keylet::mptIssuance(mptIssuanceID))", + "validates": [ + "Issuance of vault shares must exist" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sleIssuance->isFlag(lsfMPTLocked)", + "location": "VaultDeposit::preclaim", + "validated_by": "explicit check", + "validates": [ + "Issuance of vault shares must not be locked" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecFROZEN or tecLOCKED (depending on asset type)", + "field": "isFrozen(ctx.view, account, vaultAsset)", + "location": "VaultDeposit::preclaim", + "validated_by": "isFrozen function", + "validates": [ + "Depositor's asset must not be frozen" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecLOCKED", + "field": "isFrozen(ctx.view, account, vaultShare)", + "location": "VaultDeposit::preclaim", + "validated_by": "isFrozen function", + "validates": [ + "Vault shares must not be frozen for depositor" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "not shown in snippet, but likely access control error", + "field": "vault->isFlag(lsfVaultPrivate) && account != vault->at(sfOwner)", + "location": "VaultDeposit::preclaim", + "validated_by": "explicit check", + "validates": [ + "If vault is private, only owner or authorized can deposit" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp.ai.md b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp.ai.md new file mode 100644 index 0000000000..8f8e588fa6 --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultDeposit.cpp.ai.md @@ -0,0 +1,53 @@ +# `VaultDeposit.cpp` — Vault Deposit Transactor + +`VaultDeposit.cpp` implements the three-phase XRPL transaction logic for depositing assets into a vault ledger object. The vault primitive is a pooled-asset container: a depositor sends a fungible asset (XRP, IOU, or MPT) to the vault's pseudo-account and receives back vault *shares*, which are themselves an MPT issuance unique to that vault. This file contains the complete lifecycle — validation, state checks, and ledger mutation — for that exchange. + +## Transaction Lifecycle + +XRPL transactors divide their work across three static phases: + +**`preflight`** runs before ledger access and validates only the raw transaction fields. Two checks apply: `sfVaultID` must not be `beast::zero` (an uninitialised key), and `sfAmount` must be strictly positive. These are syntactic guards; business logic is deferred. + +**`preclaim`** performs read-only stateful validation against the ledger view. It resolves the vault SLE by `sfVaultID`, then enforces a cascade of invariants: the deposited asset must match `vault->at(sfAsset)`, the asset must be transferable from the depositor to the vault pseudo-account via `canTransfer`, neither the asset nor the vault shares can be frozen for the depositor, and the share MPT issuance must exist and not bear the `lsfMPTLocked` flag. The sanity check that vault shares and vault assets are different types is classified as an internal error (`tefINTERNAL`) and marked `LCOV_EXCL`, acknowledging it should be impossible given correct vault creation. + +Private vault authorization deserves special attention. Because the vault's pseudo-account cannot sign transactions, it cannot use the standard MPT issuer-authorisation flow. Instead, private vault admission is governed by a `sfDomainID` on the share issuance, validated through `credentials::validDomain`. Critically, `preclaim` suppresses `tecEXPIRED` and only hard-fails other errors. This is intentional: expired credential cleanup is a side-effect that modifies ledger state, which is only permitted in `doApply`. If the domain check passes (or returns `tecEXPIRED`), the transaction proceeds; `doApply` later calls `enforceMPTokenAuthorization`, which handles expiry deletion. If no `sfDomainID` is present on a private vault, the call returns `tecNO_AUTH` — no fallback to direct issuer grants. + +**`doApply`** is where all ledger mutations occur. + +## MPToken Provisioning in `doApply` + +Before computing the exchange, `doApply` must ensure the depositor holds an `MPToken` entry for the vault shares. The branching here reflects two modes: + +- **Private vault, non-owner**: `enforceMPTokenAuthorization` is called. This checks (and potentially deletes) expired credentials and then creates or validates the `MPToken`. +- **Public vault, or vault owner**: `authorizeMPToken` is called unconditionally if the MPToken does not yet exist. Additionally, when the vault owner deposits into a *private* vault, a second `authorizeMPToken` call provisions an `MPToken` on the vault's pseudo-account (`sleIssuance->at(sfIssuer)`), authorised for the owner (`holderID = account_`). This is necessary because the vault's accounting infrastructure must itself be able to hold shares during certain operations, and the pseudo-account cannot self-authorise. + +## Share/Asset Exchange Arithmetic + +The exchange computation in `doApply` has a specific two-step structure that protects depositors from overpayment: + +1. Call `assetsToSharesDeposit(vault, sleIssuance, amount)` — this converts the offered assets into shares, **truncating** (floor) the result since shares are integral MPT values. On a fresh vault with `sfAssetsTotal == 0`, the initial shares are seeded using the vault's `sfScale` field: `shares = floor(assets.mantissa * 10^(assets.exponent + scale))`. This seeds the exchange rate at creation time. + +2. Call `sharesToAssetsDeposit(vault, sleIssuance, sharesCreated)` — this inverts the truncated share count back into an asset amount. The result `assetsDeposited` is guaranteed to be ≤ `amount` (an internal error fires if it is not), so the depositor is charged only what those exact shares correspond to — any fractional asset remainder stays with the depositor. + +If `sharesCreated` rounds down to zero, the transaction returns `tecPRECISION_LOSS`, signalling that the deposit is too small relative to the current share price. Arithmetic overflow from `Number` with large `sfScale` values returns `tecPATH_DRY` with a debug-level log message rather than an error log, since this is a user-triggerable path. + +## Ledger Mutations and Invariant Ordering + +The ordering of state changes in `doApply` is deliberate: + +``` +vault->at(sfAssetsTotal) += assetsDeposited; +vault->at(sfAssetsAvailable) += assetsDeposited; +view().update(vault); +// Limit check against sfAssetsMaximum — BEFORE any transfer +accountSend(depositor → vaultAccount, assetsDeposited); // assets in +// Negative balance sanity check +accountSend(vaultAccount → depositor, sharesCreated); // shares out +associateAsset(*vault, vaultAsset); +``` + +The vault's totals are updated first so the `sfAssetsMaximum` cap can be enforced before any actual asset movement. If the deposit would exceed the maximum, the transaction fails with `tecLIMIT_EXCEEDED` at no cost to the depositor (the `view()` is rolled back by the framework). Both `accountSend` calls pass `WaiveTransferFee::Yes`, ensuring no transfer fees are levied on vault participants regardless of the asset's fee configuration. The negative-balance sanity check after the first `accountSend` is a last-resort internal guard against ledger corruption; it is also `LCOV_EXCL` because it should be unreachable under correct logic. + +## Relationship to Sibling Transactors + +`VaultWithdraw.cpp` is the mirror image of this file. Where `VaultDeposit` mints shares by calling `assetsToSharesDeposit` then verifies with `sharesToAssetsDeposit`, `VaultWithdraw` burns shares using `assetsToSharesWithdraw` and `sharesToAssetsWithdraw`, which additionally subtract `sfLossUnrealized` from `sfAssetsTotal` to account for off-chain losses reported by the vault operator. Notably, `VaultWithdraw` does not re-check `lsfVaultPrivate` — once you hold shares, you are considered permanently authorised to redeem them. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultSet.cpp.ai.json b/src/libxrpl/tx/transactors/vault/VaultSet.cpp.ai.json new file mode 100644 index 0000000000..5037fa24e7 --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultSet.cpp.ai.json @@ -0,0 +1,507 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "VaultSet::preflight" + ], + "entry_point": "VaultSet::preflight", + "purpose": "Performs initial stateless validation of the VaultSet transaction fields before any ledger access.", + "validation_points": [ + "sfVaultID checked for zero/empty", + "sfData checked for size constraints", + "sfAssetsMaximum checked for negative values", + "At least one of sfDomainID, sfAssetsMaximum, sfData must be present" + ] + }, + { + "call_chain": [ + "VaultSet::preclaim" + ], + "entry_point": "VaultSet::preclaim", + "purpose": "Performs stateful validation, checking ledger state and permissions before applying the transaction.", + "validation_points": [ + "Vault existence (keylet::vault)", + "Submitter is owner (sfAccount == vault->at(sfOwner))", + "Issuance object existence (keylet::mptIssuance)", + "If sfDomainID present: vault is private, domain exists, issuance is private" + ] + }, + { + "call_chain": [ + "VaultSet::doApply" + ], + "entry_point": "VaultSet::doApply", + "purpose": "Applies the transaction to the ledger, updating fields if all validations pass.", + "validation_points": [ + "Vault existence (peek)", + "Issuance object existence (peek)" + ] + }, + { + "call_chain": [ + "VaultSet::checkExtraFeatures" + ], + "entry_point": "VaultSet::checkExtraFeatures", + "purpose": "Checks if extra features (like permissioned domains) are enabled if sfDomainID is present.", + "validation_points": [ + "sfDomainID present requires featurePermissionedDomains enabled" + ] + } + ], + "data_flows": [ + { + "field": "sfVaultID", + "flow": [ + "Transaction input", + "preflight: checked for zero", + "preclaim: used to read vault from ledger", + "doApply: used to peek vault from ledger" + ], + "origin": "ctx.tx[sfVaultID] (transaction input)", + "transformations": [ + "Checked for zero value", + "Used as key to lookup vault object" + ], + "validated_at": "preflight (zero check), preclaim (vault existence), doApply (vault existence)" + }, + { + "field": "sfData", + "flow": [ + "Transaction input", + "preflight: checked for presence, size constraints", + "doApply: if present, updates vault->at(sfData)" + ], + "origin": "ctx.tx[~sfData] (optional transaction input)", + "transformations": [ + "Checked for empty or oversized payload", + "Written to vault object if present" + ], + "validated_at": "preflight (size check)" + }, + { + "field": "sfAssetsMaximum", + "flow": [ + "Transaction input", + "preflight: checked for negative value", + "doApply: (not shown in snippet, but would be updated if present)" + ], + "origin": "ctx.tx[~sfAssetsMaximum] (optional transaction input)", + "transformations": [ + "Checked for negative value" + ], + "validated_at": "preflight (negative check)" + }, + { + "field": "sfDomainID", + "flow": [ + "Transaction input", + "preflight: checked for presence (at least one field must be present)", + "preclaim: if present, checks vault is private, domain exists, issuance is private" + ], + "origin": "ctx.tx[~sfDomainID] (optional transaction input)", + "transformations": [ + "Checked for presence", + "If present, triggers further permission checks" + ], + "validated_at": "preflight (presence), preclaim (permission and existence checks)" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "preclaim: compared to vault->at(sfOwner) to check ownership" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Compared for equality" + ], + "validated_at": "preclaim (ownership check)" + } + ], + "description": "Implements the VaultSet transaction logic for updating mutable fields of a vault in the XRPL ledger, including preflight, preclaim, and apply steps.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[sfVaultID] == beast::zero) at VaultSet::preflight", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "explicit check (ctx.tx[sfVaultID] == beast::zero) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfData", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[~sfData]) at VaultSet::preflight", + "issue_pattern": "Missing empty string validation for sfData", + "why_false_positive": "explicit check (ctx.tx[~sfData]) validates sfData for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfData", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (ctx.tx[~sfData]) at VaultSet::preflight", + "issue_pattern": "Missing range validation for sfData", + "why_false_positive": "explicit check (ctx.tx[~sfData]) validates sfData range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAssetsMaximum", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (ctx.tx[~sfAssetsMaximum]) at VaultSet::preflight", + "issue_pattern": "Missing empty string validation for sfAssetsMaximum", + "why_false_positive": "explicit check (ctx.tx[~sfAssetsMaximum]) validates sfAssetsMaximum for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfAssetsMaximum", + "range", + "bounds", + "validation" + ], + "evidence": "explicit check (ctx.tx[~sfAssetsMaximum]) at VaultSet::preflight", + "issue_pattern": "Missing range validation for sfAssetsMaximum", + "why_false_positive": "explicit check (ctx.tx[~sfAssetsMaximum]) validates sfAssetsMaximum range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID, sfAssetsMaximum, sfData", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (!ctx.tx.isFieldPresent(sfDomainID) && ...) at VaultSet::preflight", + "issue_pattern": "Missing empty string validation for sfDomainID, sfAssetsMaximum, sfData", + "why_false_positive": "explicit check (!ctx.tx.isFieldPresent(sfDomainID) && ...) validates sfDomainID, sfAssetsMaximum, sfData for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfVaultID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) at VaultSet::preclaim", + "issue_pattern": "Missing empty string validation for sfVaultID", + "why_false_positive": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID])) validates sfVaultID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx[sfAccount] != vault->at(sfOwner) at VaultSet::preclaim", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "ctx.tx[sfAccount] != vault->at(sfOwner) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfShareMPTID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::mptIssuance(mptIssuanceID)) at VaultSet::preclaim", + "issue_pattern": "Missing empty string validation for sfShareMPTID", + "why_false_positive": "ctx.view.read(keylet::mptIssuance(mptIssuanceID)) validates sfShareMPTID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.tx[~sfDomainID] at VaultSet::preclaim", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "ctx.tx[~sfDomainID] validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "ctx.view.read(keylet::permissionedDomain(*domain)) at VaultSet::preclaim", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "ctx.view.read(keylet::permissionedDomain(*domain)) validates sfDomainID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfShareMPTID/lsfMPTRequireAuth", + "empty", + "string", + "validation" + ], + "evidence": "(sleIssuance->getFlags() & lsfMPTRequireAuth) == 0 at VaultSet::preclaim", + "issue_pattern": "Missing empty string validation for sfShareMPTID/lsfMPTRequireAuth", + "why_false_positive": "(sleIssuance->getFlags() & lsfMPTRequireAuth) == 0 validates sfShareMPTID/lsfMPTRequireAuth for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfDomainID", + "empty", + "string", + "validation" + ], + "evidence": "VaultSet::checkExtraFeatures at VaultSet::checkExtraFeatures", + "issue_pattern": "Missing empty string validation for sfDomainID", + "why_false_positive": "VaultSet::checkExtraFeatures validates sfDomainID for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/vault/VaultSet.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 10, + "name": "VaultSet::checkExtraFeatures" + }, + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 15, + "name": "VaultSet::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 41, + "name": "VaultSet::preclaim" + }, + { + "args": [], + "lineno": 81, + "name": "VaultSet::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is split between preflight (stateless checks), preclaim (stateful/permission checks), and doApply (final application). Typical test files would be in the unit/integration test directories, e.g., 'test/tx/VaultSet_test.cpp' or similar. Tests should cover: zero/empty sfVaultID, missing/invalid sfData, negative sfAssetsMaximum, missing all updatable fields, non-existent vault, non-owner submitter, missing issuance, domain permission checks, and successful updates. Gaps may exist if there are no tests for edge cases like oversized sfData, non-existent domains, or internal errors (tefINTERNAL). LCOV_EXCL_START/STOP comments indicate some error paths may not be covered by tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation in business logic (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfVaultID", + "location": "VaultSet::preflight", + "validated_by": "explicit check (ctx.tx[sfVaultID] == beast::zero)", + "validates": [ + "Vault ID must not be zero/empty" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfData", + "location": "VaultSet::preflight", + "validated_by": "explicit check (ctx.tx[~sfData])", + "validates": [ + "If sfData is present, it must not be empty", + "If sfData is present, its length must not exceed maxDataPayloadLength" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfAssetsMaximum", + "location": "VaultSet::preflight", + "validated_by": "explicit check (ctx.tx[~sfAssetsMaximum])", + "validates": [ + "If sfAssetsMaximum is present, it must not be negative" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "temMALFORMED", + "field": "sfDomainID, sfAssetsMaximum, sfData", + "location": "VaultSet::preflight", + "validated_by": "explicit check (!ctx.tx.isFieldPresent(sfDomainID) && ...)", + "validates": [ + "At least one of sfDomainID, sfAssetsMaximum, or sfData must be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_ENTRY", + "field": "sfVaultID", + "location": "VaultSet::preclaim", + "validated_by": "ctx.view.read(keylet::vault(ctx.tx[sfVaultID]))", + "validates": [ + "Vault with given sfVaultID must exist" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfAccount", + "location": "VaultSet::preclaim", + "validated_by": "ctx.tx[sfAccount] != vault->at(sfOwner)", + "validates": [ + "Transaction submitter must be the owner of the vault" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sfShareMPTID", + "location": "VaultSet::preclaim", + "validated_by": "ctx.view.read(keylet::mptIssuance(mptIssuanceID))", + "validates": [ + "Issuance entry for vault shares must exist" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecNO_PERMISSION", + "field": "sfDomainID", + "location": "VaultSet::preclaim", + "validated_by": "ctx.tx[~sfDomainID]", + "validates": [ + "Can only set domain if vault is private (lsfVaultPrivate flag set)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tecOBJECT_NOT_FOUND", + "field": "sfDomainID", + "location": "VaultSet::preclaim", + "validated_by": "ctx.view.read(keylet::permissionedDomain(*domain))", + "validates": [ + "If sfDomainID is nonzero, the referenced domain must exist" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "tefINTERNAL", + "field": "sfShareMPTID/lsfMPTRequireAuth", + "location": "VaultSet::preclaim", + "validated_by": "(sleIssuance->getFlags() & lsfMPTRequireAuth) == 0", + "validates": [ + "Issuance of vault shares must be private (lsfMPTRequireAuth flag set)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "N/A (returns bool, used as a feature check)", + "field": "sfDomainID", + "location": "VaultSet::checkExtraFeatures", + "validated_by": "VaultSet::checkExtraFeatures", + "validates": [ + "If sfDomainID is present, featurePermissionedDomains must be enabled" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultSet.cpp.ai.md b/src/libxrpl/tx/transactors/vault/VaultSet.cpp.ai.md new file mode 100644 index 0000000000..517e7ef270 --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultSet.cpp.ai.md @@ -0,0 +1,35 @@ +# VaultSet.cpp — Vault Metadata and Access Policy Update Transactor + +`VaultSet.cpp` implements the `VaultSet` transactor, which allows the owner of an on-ledger vault to update a limited set of mutable vault properties without affecting the vault's core structure or asset balances. It lives in the `vault` transactor group alongside `VaultCreate`, `VaultDeposit`, `VaultWithdraw`, `VaultDelete`, and `VaultClawback`. + +## Role in the Vault Subsystem + +A vault in XRPL is a ledger object (`SLE` keyed by `keylet::vault`) that acts as a pooled asset container. Vaults are backed by a pseudo-account that holds the actual assets, and share ownership is represented via an `MPTokenIssuance` object whose ID is stored as `sfShareMPTID` on the vault. `VaultCreate` establishes all the immutable structure of a vault; `VaultSet` then handles post-creation configuration. Only three fields are intentionally mutable after creation: the data payload (`sfData`), the asset cap (`sfAssetsMaximum`), and the permissioned domain (`sfDomainID`). + +## Validation Pipeline + +The class follows the standard three-phase XRPL transactor model. + +`checkExtraFeatures()` is the earliest gate: if the transaction carries an `sfDomainID`, it verifies that the `featurePermissionedDomains` amendment is enabled on the network, returning `false` (reject) otherwise. This allows the feature to be deployed independently of vault support. + +`preflight()` performs stateless field-level validation before any ledger access. It enforces that `sfVaultID` is not the zero hash, that `sfData` (if present) is neither empty nor exceeds `maxDataPayloadLength`, and that `sfAssetsMaximum` (if present) is non-negative. Crucially, it also requires that at least one of the three mutable fields is present — a no-op transaction is rejected as `temMALFORMED` rather than silently accepted. + +`preclaim()` reads the ledger to enforce ownership and consistency. It verifies the vault exists (`tecNO_ENTRY`), confirms the submitting account matches `sfOwner` stored on the vault (`tecNO_PERMISSION`), and checks that the `MPTokenIssuance` for vault shares is present. The issuance check uses `tefINTERNAL` (marked `LCOV_EXCL_START`) because the missing-issuance path represents an invariant violation — a vault should never exist without its issuance. Domain-related checks only trigger when `sfDomainID` is present in the transaction: the vault must have been created with `lsfVaultPrivate`, the referenced domain must exist in the ledger (`tecOBJECT_NOT_FOUND` if not, unless zero), and the issuance must carry `lsfMPTRequireAuth` (another `tefINTERNAL` sanity guard, enforced at `VaultCreate` time). + +## Application Logic in doApply() + +`doApply()` performs the actual mutations. It re-fetches the vault and issuance with `view().peek()` (mutable references) and applies each field if present in the transaction. + +**AssetsMaximum update** has a runtime semantic check that `preflight` cannot perform: if the new maximum is non-zero and is less than the current `sfAssetsTotal`, the transaction fails with `tecLIMIT_EXCEEDED`. Zero is the sentinel meaning "no limit," so setting it to zero always succeeds and removes any existing cap. + +**DomainID update** is architecturally interesting: the domain is not stored on the vault SLE itself — it lives on the underlying `MPTokenIssuance` object. `VaultSet` writes the domain directly to `sleIssuance` via `setFieldH256`, then calls `view().update(sleIssuance)`. Setting `sfDomainID` to the zero hash clears the field entirely (`makeFieldAbsent`) rather than storing zero, preserving compactness on the ledger. Clearing the domain does not remove `lsfVaultPrivate` from the vault — once a vault is private, it stays private. A private vault with no domain falls back to rejecting all non-owner depositors with `tecNO_AUTH`, as checked by `VaultDeposit::preclaim`. + +A subtle comment in the code explains why `view().update(vault)` is always called, even when only the issuance changed: the vault invariant checker needs to see the vault as modified so it can validate the operation. Skipping the vault update would make invariant verification unreliable. + +**`associateAsset()` call** at the end is a precision-binding step. It iterates all fields in the vault SLE that carry the `sMD_NeedsAsset` metadata flag (fields derived from `STTakesAsset`, such as `STNumber`) and calls their virtual `associateAsset()` method with the vault's underlying asset. This ties the `STNumber` precision to the asset type — for example, so asset totals stored as numbers are rounded to the correct decimal scale for the vault's currency. The same call appears in every vault-modifying transactor. + +## Design Observations + +The decision to store `sfDomainID` on the `MPTokenIssuance` rather than on the vault SLE itself is architecturally driven: the MPT authorization machinery (`lsfMPTRequireAuth`, domain-based credential validation) operates on issuances, not vaults. By keeping the domain on the issuance, the depositor authorization path in `VaultDeposit` can use the standard `credentials::validDomain` check without special-casing the vault layer. + +The `lsfVaultPrivate` flag is intentionally made immutable by design. The comment in `doApply()` explicitly notes that making a private vault public is not currently supported. This one-way privacy boundary simplifies the trust model: depositors in a private vault can rely on its access control never being retroactively removed. \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp.ai.json b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp.ai.json new file mode 100644 index 0000000000..0d198589cf --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp.ai.json @@ -0,0 +1,193 @@ +{ + "args": [ + { + "lineno": 13, + "name": "ctx" + }, + { + "lineno": 27, + "name": "ctx" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "VaultWithdraw::preflight" + ], + "entry_point": "VaultWithdraw::preflight", + "purpose": "Initial stateless validation of the VaultWithdraw transaction. Checks for malformed fields and basic constraints before further processing.", + "validation_points": [ + "VaultWithdraw::preflight" + ] + }, + { + "call_chain": [ + "VaultWithdraw::preclaim", + "canTransfer", + "canWithdraw", + "sharesToAssetsWithdraw" + ], + "entry_point": "VaultWithdraw::preclaim", + "purpose": "Performs contextual validation using ledger state. Checks vault existence, asset correctness, withdrawal policy, and withdrawal limits.", + "validation_points": [ + "VaultWithdraw::preclaim", + "canTransfer", + "canWithdraw", + "sharesToAssetsWithdraw" + ] + }, + { + "call_chain": [ + "VaultWithdraw::doApply" + ], + "entry_point": "VaultWithdraw::doApply", + "purpose": "Executes the transaction after all validations pass. Not shown in code, but typically applies state changes.", + "validation_points": [ + "VaultWithdraw::preflight", + "VaultWithdraw::preclaim" + ] + } + ], + "data_flows": [ + { + "field": "sfVaultID", + "flow": [ + "Transaction input", + "VaultWithdraw::preflight (checked for zero/empty)", + "VaultWithdraw::preclaim (used to look up vault in ledger)" + ], + "origin": "ctx.tx[sfVaultID] (transaction input)", + "transformations": [ + "Checked for zero/empty", + "Used as key to fetch vault ledger entry" + ], + "validated_at": "VaultWithdraw::preflight, VaultWithdraw::preclaim" + }, + { + "field": "sfAmount", + "flow": [ + "Transaction input", + "VaultWithdraw::preflight (checked for <= 0)", + "VaultWithdraw::preclaim (used for asset checks, withdrawal limits)" + ], + "origin": "ctx.tx[sfAmount] (transaction input)", + "transformations": [ + "Checked for <= 0", + "Compared against vault asset/share", + "May be converted from shares to assets" + ], + "validated_at": "VaultWithdraw::preflight, VaultWithdraw::preclaim" + }, + { + "field": "sfDestination", + "flow": [ + "Transaction input", + "VaultWithdraw::preflight (checked for zero/empty if present)", + "VaultWithdraw::preclaim (used as destination account, fallback to sfAccount)" + ], + "origin": "ctx.tx[~sfDestination] (optional transaction input)", + "transformations": [ + "Checked for zero/empty", + "Defaulted to sfAccount if not present" + ], + "validated_at": "VaultWithdraw::preflight, VaultWithdraw::preclaim" + }, + { + "field": "sfAccount", + "flow": [ + "Transaction input", + "VaultWithdraw::preclaim (used as source account, fallback for destination)" + ], + "origin": "ctx.tx[sfAccount] (transaction input)", + "transformations": [ + "Used as fallback for destination" + ], + "validated_at": "VaultWithdraw::preclaim" + }, + { + "field": "sfAsset / sfShareMPTID", + "flow": [ + "Ledger vault entry", + "VaultWithdraw::preclaim (compared to amount.asset())" + ], + "origin": "vault->at(sfAsset), vault->at(sfShareMPTID) (ledger vault entry)", + "transformations": [ + "Compared to transaction asset" + ], + "validated_at": "VaultWithdraw::preclaim" + }, + { + "field": "sfWithdrawalPolicy", + "flow": [ + "Ledger vault entry", + "VaultWithdraw::preclaim (compared to vaultStrategyFirstComeFirstServe)" + ], + "origin": "vault->at(sfWithdrawalPolicy) (ledger vault entry)", + "transformations": [ + "Checked for allowed withdrawal policy" + ], + "validated_at": "VaultWithdraw::preclaim" + }, + { + "field": "sfShareMPTID (shares)", + "flow": [ + "Ledger vault entry", + "VaultWithdraw::preclaim (if amount.asset() == vaultShare, triggers share-to-asset conversion)" + ], + "origin": "vault->at(sfShareMPTID) (ledger vault entry)", + "transformations": [ + "Converted to asset amount via sharesToAssetsWithdraw" + ], + "validated_at": "VaultWithdraw::preclaim (post-fixSecurity3_1_3)" + } + ], + "description": "Implements the VaultWithdraw transaction logic for the XRPL, including preflight, preclaim, and application of vault asset withdrawals, with checks for asset validity, withdrawal policy, authorization, and asset conversion.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp", + "functions": [ + { + "args": [ + "PreflightContext const& ctx" + ], + "lineno": 13, + "name": "VaultWithdraw::preflight" + }, + { + "args": [ + "PreclaimContext const& ctx" + ], + "lineno": 27, + "name": "VaultWithdraw::preclaim" + }, + { + "args": [], + "lineno": 97, + "name": "VaultWithdraw::doApply" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for transaction validation includes malformed transactions (missing/zero fields), invalid amounts, wrong assets, unauthorized withdrawals, and policy enforcement. Likely test files: 'test/tx/VaultWithdraw_test.cpp', 'test/ledger/Vault_test.cpp', or similar. Gaps: Exception paths (e.g., overflow in sharesToAssetsWithdraw), internal errors (tefINTERNAL), and rare policy configurations may not be fully covered. LCOV_EXCL annotations suggest some error branches are not covered by tests.", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp.ai.md b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp.ai.md new file mode 100644 index 0000000000..0aca9924cc --- /dev/null +++ b/src/libxrpl/tx/transactors/vault/VaultWithdraw.cpp.ai.md @@ -0,0 +1,53 @@ +# `VaultWithdraw.cpp` — Vault Withdrawal Transactor + +## Role in the System + +`VaultWithdraw` is the XRPL transactor that implements the `VaultWithdraw` transaction type, allowing vault participants to redeem shares and receive underlying assets in return. A vault on the XRPL is a pool that holds a single asset type and issues MPT-based shares proportionally. This file is the structural mirror of `VaultDeposit.cpp`: where deposit converts assets into shares, withdrawal converts shares back into assets. It follows the standard three-phase transactor lifecycle — `preflight`, `preclaim`, and `doApply` — with complexity concentrated in the application phase where the share-to-asset exchange rate is computed. + +## Preflight: Stateless Sanity Checks + +`preflight` performs pure-transaction validation with no ledger access. It rejects a zero `sfVaultID` (a null key is always a programming error), a non-positive `sfAmount` (withdrawing zero or negative amounts is semantically meaningless), and a zero `sfDestination` if present (the optional destination field, when supplied, must be a real account). These checks are cheap and deterministic — they run before any ledger state is examined. + +## Preclaim: Contextual Validation + +`preclaim` validates the transaction against live ledger state. It fetches the vault SLE by ID and fails with `tecNO_ENTRY` if it doesn't exist. It then verifies that the requested amount is denominated in either the vault's underlying asset (`sfAsset`) or the vault's share MPT (`sfShareMPTID`), returning `tecWRONG_ASSET` if neither matches. The vault's `sfWithdrawalPolicy` must be `vaultStrategyFirstComeFirstServe` — the only currently supported policy; other values hit `tefINTERNAL` guarded by `LCOV_EXCL` markers, indicating unreachable code under normal conditions. + +### Amendment: `fixSecurity3_1_3` and the Share-Limit Gap + +An important security fix is embedded in `preclaim`. The `canWithdraw` function checks whether the destination account would exceed its asset-holding limit (IOU trust line maximum or MPT `MaximumAmount`). Before the `fixSecurity3_1_3` amendment, if a user specified the withdrawal amount in shares rather than assets, this limit check was silently skipped — the code path went directly to the simpler `canWithdraw(ctx.view, ctx.tx)` overload. Post-amendment, when the amount is share-denominated, the code first calls `sharesToAssetsWithdraw` to compute the equivalent asset amount, then feeds that result into the full `canWithdraw(view, from, to, amount, hasDestinationTag)` overload. The explicit `overflow_error` catch around this conversion returns `tecPATH_DRY` rather than crashing — a deliberate choice given that large `sfScale` values make overflow arithmetically trivial. + +### Authorization and Freeze Checks + +`preclaim` enforces a two-tier authorization model. If the transaction sends assets to the submitting account's own wallet, `WeakAuth` is used for `requireAuth` — the system will create an MPToken or trust line on the submitter's behalf in `doApply`. If `sfDestination` specifies a third-party account, `StrongAuth` is required, meaning the token holding must already exist. Two separate `checkFrozen` calls follow: one on the vault's asset for the destination account (you cannot receive a frozen IOU or locked MPT), and one on the vault's share MPT for the submitting account (you cannot surrender shares that are frozen/locked on your side). + +## `doApply`: The Exchange Rate Logic + +The application phase peeks the vault SLE for mutation, resolves the share MPT issuance, and then determines how many shares to burn and how many assets to release. Two exchange modes exist: + +**Asset-denominated mode** (`amount.asset() == vaultAsset`): The user specifies a fixed asset quantity to receive. The code calls `assetsToSharesWithdraw` to determine how many integer shares must be redeemed to cover that amount, then immediately calls `sharesToAssetsWithdraw` on the resulting integer share count to get the actual asset amount to disburse. This double-conversion is not redundant — it is a deliberate rounding correction. Because shares are MPT (integer-only), converting assets-to-shares truncates fractional parts. Re-converting shares-to-assets computes the precise asset amount the vault will actually release for those integer shares. If the first conversion produces zero shares, `tecPRECISION_LOSS` is returned; the deposit was too small to represent even one share unit. + +**Share-denominated mode** (`amount.asset() == share`): The user specifies an exact share count to burn. `sharesToAssetsWithdraw` directly computes the corresponding asset payout. No rounding step is needed here since the share count is already integral. + +Both modes catch `std::overflow_error` and return `tecPATH_DRY` with a debug log that includes the vault's `sfScale`, `sfAssetsTotal`, and the issuance's `sfOutstandingAmount`. These fields are logged at `debug` rather than `error` level precisely because large-scale vaults make overflow easily triggerable by legitimate users. + +### Liquidity Gating via `sfAssetsAvailable` + +After computing the exchange, `doApply` checks `sfAssetsAvailable` rather than the vault pseudo-account's raw balance. The distinction matters: a vault may have pledged assets to lending brokers, reducing what can actually be paid out on demand. `sfAssetsAvailable` tracks only the assets currently liquid in the vault, not the total it is entitled to. If available assets are insufficient, the withdrawal returns `tecINSUFFICIENT_FUNDS`. Upon success, both `sfAssetsTotal` and `sfAssetsAvailable` are decremented by `assetsWithdrawn`. + +### Share Redemption and MPToken Cleanup + +Shares flow from the submitter back to the vault pseudo-account via `accountSend(..., WaiveTransferFee::Yes)`. Transfer fees are waived on both legs of vault share movement, consistent with the vault's role as a protocol-level construct rather than an end-user token. + +After redeeming shares, if the submitter's share balance drops to zero and they are not the vault owner, the code attempts to remove the now-empty MPToken holding with `removeEmptyHolding`. Vault owners are explicitly excluded from this cleanup because they may need the MPToken alive for future share issuances. A result of `tecHAS_OBLIGATIONS` is silently ignored — the MPToken has non-zero associated obligations and must persist. + +### Private Vaults and Indefinite Authorization + +A notable design choice is the explicit comment that `doApply` does not check `lsfVaultPrivate` on the vault. Possession of shares is treated as proof of prior authorization: if you ever deposited into a private vault, you must have been authorized at that time. This means access to a private vault is effectively irrevocable once granted via share ownership. Withdrawal privileges are tied to share possession, not to ongoing credential validity. + +### Completion + +`doApply` concludes by calling `associateAsset(*vault, vaultAsset)` — a per-SLE hook that triggers any `STNumber` field rounding to the correct decimal scale for the vault's asset — before delegating the actual asset credit to `doWithdraw` from `View.h`. `doWithdraw` handles trust line or MPToken creation for the destination account (when withdrawing to self under `WeakAuth`), enforces deposit authorization checks for third-party recipients, and executes the final `accountSend` from the vault pseudo-account to the destination. + +## Key Relationships + +`VaultWithdraw.cpp` depends heavily on `VaultHelpers.h` for the four directional conversion functions (`assetsToSharesWithdraw`, `sharesToAssetsWithdraw`) which encapsulate the vault's exchange rate math using `sfAssetsTotal`, `sfOutstandingAmount`, and `sfScale`. `TokenHelpers.h` provides `accountHolds`, `requireAuth`, `checkFrozen`, `accountSend`, and `removeEmptyHolding`. `View.h` provides `canWithdraw` and `doWithdraw` as the shared withdrawal authorization and execution primitives used across all vault-adjacent transactors. The symmetric counterpart `VaultDeposit.cpp` follows identical structural conventions and should be read alongside this file for a complete picture of the vault share lifecycle. \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCensorshipDetector.h.ai.json b/src/xrpld/app/consensus/RCLCensorshipDetector.h.ai.json new file mode 100644 index 0000000000..d067e0a18a --- /dev/null +++ b/src/xrpld/app/consensus/RCLCensorshipDetector.h.ai.json @@ -0,0 +1,110 @@ +{ + "args": [ + { + "lineno": 8, + "name": "TxID" + }, + { + "lineno": 8, + "name": "Sequence" + }, + { + "lineno": 14, + "name": "txid_" + }, + { + "lineno": 14, + "name": "seq_" + }, + { + "lineno": 44, + "name": "proposed" + }, + { + "lineno": 65, + "name": "accepted" + }, + { + "lineno": 65, + "name": "pred" + }, + { + "lineno": 19, + "name": "lhs" + }, + { + "lineno": 19, + "name": "rhs" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "RCLCensorshipDetector" + }, + { + "args": [ + "txid_", + "seq_" + ], + "lineno": 11, + "name": "TxIDSeq" + } + ], + "description": "Implements a censorship detector for XRPL consensus rounds, tracking proposed transactions and detecting which have not been accepted, with utilities to propose, check, and reset tracked transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/consensus/RCLCensorshipDetector.h", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 19, + "name": "operator<" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 26, + "name": "operator<" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 31, + "name": "operator<" + }, + { + "args": [ + "proposed" + ], + "lineno": 44, + "name": "propose" + }, + { + "args": [ + "accepted", + "pred" + ], + "lineno": 65, + "name": "check" + }, + { + "args": [], + "lineno": 89, + "name": "reset" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCensorshipDetector.h.ai.md b/src/xrpld/app/consensus/RCLCensorshipDetector.h.ai.md new file mode 100644 index 0000000000..02b68cc92e --- /dev/null +++ b/src/xrpld/app/consensus/RCLCensorshipDetector.h.ai.md @@ -0,0 +1,49 @@ +# RCLCensorshipDetector.h + +## Purpose + +`RCLCensorshipDetector` tracks transactions that the local validating node has proposed across multiple consensus rounds, looking for transactions that the network consistently refuses to include in a ledger. Such systematic exclusion — where a transaction is repeatedly eligible but never confirmed — can signal censorship by a bloc of validators. The class provides the data structure and merge logic; the policy for what constitutes a warning and how to surface it lives in the caller (`RCLConsensus`). + +## Template Design + +The class is templated on `TxID` and `Sequence`, keeping it generic. In practice it is always instantiated as `RCLCensorshipDetector`, where `LedgerIndex` records which ledger sequence the transaction was *first* proposed in — not the current round. This distinction is essential: by freezing the sequence number at first proposal, the detector can later compute `current_ledger - first_proposed_ledger` to determine exactly how many rounds a transaction has been waiting. + +## Internal State + +The single data member `tracker_` is a `std::vector` kept in sorted order. `TxIDSeq` pairs a transaction ID with its first-seen ledger sequence. The three `operator<` overloads defined as friends cover all comparison combinations needed by the sorted-range algorithms: `TxIDSeq` vs. `TxIDSeq`, `TxIDSeq` vs. raw `TxID`, and `TxID` vs. `TxIDSeq`. This heterogeneous ordering is what lets `remove_if_intersect_or_match` (which receives the `accepted` set as a plain `vector`) operate in a single linear pass against `tracker_` without converting types. + +Choosing a sorted vector over a hash map is a deliberate performance tradeoff. Both `propose()` and `check()` call custom merge algorithms from `xrpl/basics/algorithm.h` that exploit sorted order to run in O(n + m) time. A hash map would need individual lookups with worse cache behavior for these bulk operations. + +## The propose() Method + +`propose()` is called at the beginning of each consensus round with the transactions the node is putting forward. Its job is to reconcile the incoming proposal with whatever was tracked from the previous round: + +1. Transactions present in the previous round *and* still being proposed retain their original `seq` (the round they were first seen). The `generalized_set_intersection` call performs this in-place update: for each transaction in the intersection of the new proposal and the old tracker, it copies the stored `seq` from the old entry into the new one. +2. Transactions that were tracked before but dropped from this round's proposal are discarded. +3. Brand-new transactions are added with the current ledger sequence as their `seq`. + +The result is that `tracker_` always holds exactly the set of transactions the node is *currently* proposing, with `seq` set to the earliest ledger in which each was first proposed. + +## The check() Method + +`check()` is called after consensus completes, once the accepted transaction set is known. It removes entries from `tracker_` in two categories: + +- **Accepted transactions**: any tracked transaction that made it into the ledger is removed unconditionally. +- **Predicate-matched transactions**: for each tracked transaction still pending, the supplied predicate `pred(TxID, Sequence)` is consulted. If it returns `true`, the entry is removed. + +The predicate in `RCLConsensus::buildLCL()` does two things. First, it silently removes transactions that *failed* (bad fee, wrong account state, etc.) — these should not trigger a warning because their exclusion is legitimate, not malicious. Second, for transactions that have legitimately been waiting, it checks whether `(current_seq - first_proposed_seq) % censorshipWarnInternal == 0`, where `censorshipWarnInternal` is 15. This fires a `JLOG(j.warn())` message every 15 ledgers for each persistently unconfirmed transaction, providing periodic escalating visibility without log flooding. + +The predicate returns `false` for transactions that should continue to be tracked, keeping them alive in `tracker_` for future rounds. + +## The reset() Method + +`reset()` clears all state by emptying `tracker_`. It is called from `RCLConsensus::consensusModeChange()` whenever the node transitions away from proposing mode — for example, when it loses sync with the network, reconnects after an outage, or switches from proposing to observing. Without this guard, transactions queued before a disconnect would generate spurious censorship warnings when the node rejoins, since they would appear to have been waiting through the entire outage period. + +## Integration in RCLConsensus + +`RCLConsensus::Imp` holds a single `censorshipDetector_` member. The two-phase call pattern maps naturally to the consensus lifecycle: + +- `propose()` is called inside `RCLConsensus::onConsensusReached()` when the node builds its initial transaction set from the `SHAMap`, visited leaf by leaf. +- `check()` is called inside `RCLConsensus::buildLCL()` after `applyTransactions()` has determined which transactions were accepted and which were retriable or failed. + +The class is not thread-safe — it lives entirely within `RCLConsensus::Imp`, which is accessed under the application's consensus lock. \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp.ai.json b/src/xrpld/app/consensus/RCLConsensus.cpp.ai.json new file mode 100644 index 0000000000..f6941a1075 --- /dev/null +++ b/src/xrpld/app/consensus/RCLConsensus.cpp.ai.json @@ -0,0 +1,766 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 17, + "name": "RCLConsensus" + }, + { + "args": [], + "lineno": 32, + "name": "RCLConsensus::Adaptor" + }, + { + "args": [ + "char const* label", + "bool const validating", + "beast::Journal j" + ], + "lineno": 706, + "name": "RclConsensusLogger" + } + ], + "code_paths": [ + { + "call_chain": [ + "RCLConsensus::RCLConsensus", + "RCLConsensus::Adaptor::Adaptor" + ], + "entry_point": "RCLConsensus::RCLConsensus", + "purpose": "Constructs the consensus engine and its adaptor, initializing validator keys and validation cookie.", + "validation_points": [ + "RCLConsensus::Adaptor::Adaptor: XRPL_ASSERT(valCookie_, ...)", + "RCLConsensus::Adaptor::Adaptor: validatorKeys_.nodeID != beast::zero", + "RCLConsensus::Adaptor::Adaptor: validatorKeys_.keys pointer check" + ] + }, + { + "call_chain": [ + "RCLConsensus::Adaptor::acquireLedger", + "ledgerMaster_.getLedgerByHash", + "app_.getJobQueue().addJob" + ], + "entry_point": "RCLConsensus::Adaptor::acquireLedger", + "purpose": "Attempts to acquire a ledger by hash, triggers async acquisition if not present.", + "validation_points": [ + "XRPL_ASSERT(!built->open() && built->isImmutable(), ...)" + ] + } + ], + "data_flows": [ + { + "field": "valCookie_", + "flow": [ + "Adaptor constructor", + "Assigned to valCookie_", + "Validated by XRPL_ASSERT", + "Used for logging and possibly for consensus identity" + ], + "origin": "RCLConsensus::Adaptor::Adaptor constructor (randomly generated)", + "transformations": [ + "Random value assigned at construction" + ], + "validated_at": "RCLConsensus::Adaptor::Adaptor (XRPL_ASSERT)" + }, + { + "field": "validatorKeys_.nodeID", + "flow": [ + "ValidatorKeys input", + "Assigned to member validatorKeys_", + "Checked for non-zero in Adaptor constructor", + "Used for logging and nUnlVote_ initialization" + ], + "origin": "Passed into RCLConsensus::Adaptor::Adaptor as ValidatorKeys const&", + "transformations": [ + "None (direct assignment and check)" + ], + "validated_at": "RCLConsensus::Adaptor::Adaptor (if (validatorKeys_.nodeID != beast::zero))" + }, + { + "field": "validatorKeys_.keys", + "flow": [ + "ValidatorKeys input", + "Assigned to member validatorKeys_", + "Pointer checked in Adaptor constructor", + "Used for logging public keys" + ], + "origin": "Passed into RCLConsensus::Adaptor::Adaptor as ValidatorKeys const&", + "transformations": [ + "Pointer check (null or not)" + ], + "validated_at": "RCLConsensus::Adaptor::Adaptor (if (validatorKeys_.keys))" + } + ], + "description": "Implements the RCLConsensus class and its Adaptor for managing consensus rounds, ledger building, transaction sharing, validation, and related operations in the XRPL (Ripple) distributed ledger system.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "valCookie_ (range via assertion)", + "validation", + "missing", + "check" + ], + "evidence": "Field valCookie_ (range via assertion) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for valCookie_ (range via assertion)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates valCookie_ (range via assertion) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "validatorKeys_.nodeID (business logic via conditional)", + "validation", + "missing", + "check" + ], + "evidence": "Field validatorKeys_.nodeID (business logic via conditional) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for validatorKeys_.nodeID (business logic via conditional)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates validatorKeys_.nodeID (business logic via conditional) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "validatorKeys_.keys (pointer validity via conditional)", + "validation", + "missing", + "check" + ], + "evidence": "Field validatorKeys_.keys (pointer validity via conditional) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for validatorKeys_.keys (pointer validity via conditional)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates validatorKeys_.keys (pointer validity via conditional) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "valCookie_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at RCLConsensus::Adaptor::Adaptor (constructor)", + "issue_pattern": "Missing empty string validation for valCookie_", + "why_false_positive": "XRPL_ASSERT validates valCookie_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "valCookie_", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT at RCLConsensus::Adaptor::Adaptor (constructor)", + "issue_pattern": "Missing range validation for valCookie_", + "why_false_positive": "XRPL_ASSERT validates valCookie_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "validatorKeys_.nodeID", + "empty", + "string", + "validation" + ], + "evidence": "comparison (validatorKeys_.nodeID != beast::zero) at RCLConsensus::Adaptor::Adaptor (constructor)", + "issue_pattern": "Missing empty string validation for validatorKeys_.nodeID", + "why_false_positive": "comparison (validatorKeys_.nodeID != beast::zero) validates validatorKeys_.nodeID for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "validatorKeys_.keys", + "empty", + "string", + "validation" + ], + "evidence": "pointer check (validatorKeys_.keys) at RCLConsensus::Adaptor::Adaptor (constructor)", + "issue_pattern": "Missing empty string validation for validatorKeys_.keys", + "why_false_positive": "pointer check (validatorKeys_.keys) validates validatorKeys_.keys for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "validatorKeys_.keys", + "type", + "validation", + "check" + ], + "evidence": "pointer check (validatorKeys_.keys) at RCLConsensus::Adaptor::Adaptor (constructor)", + "issue_pattern": "Missing type validation for validatorKeys_.keys", + "why_false_positive": "pointer check (validatorKeys_.keys) validates validatorKeys_.keys type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/consensus/RCLConsensus.cpp", + "functions": [ + { + "args": [ + "Application& app", + "std::unique_ptr&& feeVote", + "LedgerMaster& ledgerMaster", + "LocalTxs& localTxs", + "InboundTransactions& inboundTransactions", + "Consensus::clock_type const& clock", + "ValidatorKeys const& validatorKeys", + "beast::Journal journal" + ], + "lineno": 19, + "name": "RCLConsensus::RCLConsensus" + }, + { + "args": [ + "Application& app", + "std::unique_ptr&& feeVote", + "LedgerMaster& ledgerMaster", + "LocalTxs& localTxs", + "InboundTransactions& inboundTransactions", + "ValidatorKeys const& validatorKeys", + "beast::Journal journal" + ], + "lineno": 34, + "name": "RCLConsensus::Adaptor::Adaptor" + }, + { + "args": [ + "LedgerHash const& hash" + ], + "lineno": 56, + "name": "RCLConsensus::Adaptor::acquireLedger" + }, + { + "args": [ + "RCLCxPeerPos const& peerPos" + ], + "lineno": 81, + "name": "RCLConsensus::Adaptor::share" + }, + { + "args": [ + "RCLCxTx const& tx" + ], + "lineno": 101, + "name": "RCLConsensus::Adaptor::share" + }, + { + "args": [ + "RCLCxPeerPos::Proposal const& proposal" + ], + "lineno": 116, + "name": "RCLConsensus::Adaptor::propose" + }, + { + "args": [ + "RCLTxSet const& txns" + ], + "lineno": 146, + "name": "RCLConsensus::Adaptor::share" + }, + { + "args": [ + "RCLTxSet::ID const& setId" + ], + "lineno": 150, + "name": "RCLConsensus::Adaptor::acquireTxSet" + }, + { + "args": [], + "lineno": 158, + "name": "RCLConsensus::Adaptor::hasOpenTransactions" + }, + { + "args": [ + "LedgerHash const& h" + ], + "lineno": 162, + "name": "RCLConsensus::Adaptor::proposersValidated" + }, + { + "args": [ + "RCLCxLedger const& ledger", + "LedgerHash const& h" + ], + "lineno": 167, + "name": "RCLConsensus::Adaptor::proposersFinished" + }, + { + "args": [ + "uint256 ledgerID", + "RCLCxLedger const& ledger", + "ConsensusMode mode" + ], + "lineno": 174, + "name": "RCLConsensus::Adaptor::getPrevLedger" + }, + { + "args": [ + "RCLCxLedger const& ledger", + "NetClock::time_point const& closeTime", + "ConsensusMode mode" + ], + "lineno": 188, + "name": "RCLConsensus::Adaptor::onClose" + }, + { + "args": [ + "Result const& result", + "RCLCxLedger const& prevLedger", + "NetClock::duration const& closeResolution", + "ConsensusCloseTimes const& rawCloseTimes", + "ConsensusMode const& mode", + "Json::Value&& consensusJson" + ], + "lineno": 265, + "name": "RCLConsensus::Adaptor::onForceAccept" + }, + { + "args": [ + "Result const& result", + "RCLCxLedger const& prevLedger", + "NetClock::duration const& closeResolution", + "ConsensusCloseTimes const& rawCloseTimes", + "ConsensusMode const& mode", + "Json::Value&& consensusJson", + "bool const validating" + ], + "lineno": 274, + "name": "RCLConsensus::Adaptor::onAccept" + }, + { + "args": [ + "Result const& result", + "RCLCxLedger const& prevLedger", + "NetClock::duration closeResolution", + "ConsensusCloseTimes const& rawCloseTimes", + "ConsensusMode const& mode", + "Json::Value&& consensusJson" + ], + "lineno": 292, + "name": "RCLConsensus::Adaptor::doAccept" + }, + { + "args": [ + "protocol::NodeEvent ne", + "RCLCxLedger const& ledger", + "bool haveCorrectLCL" + ], + "lineno": 464, + "name": "RCLConsensus::Adaptor::notify" + }, + { + "args": [ + "RCLCxLedger const& previousLedger", + "CanonicalTXSet& retriableTxs", + "NetClock::time_point closeTime", + "bool closeTimeCorrect", + "NetClock::duration closeResolution", + "std::chrono::milliseconds roundTime", + "std::set& failedTxs" + ], + "lineno": 491, + "name": "RCLConsensus::Adaptor::buildLCL" + }, + { + "args": [ + "RCLCxLedger const& ledger", + "RCLTxSet const& txns", + "bool proposing" + ], + "lineno": 520, + "name": "RCLConsensus::Adaptor::validate" + }, + { + "args": [ + "ConsensusMode before", + "ConsensusMode after" + ], + "lineno": 573, + "name": "RCLConsensus::Adaptor::onModeChange" + }, + { + "args": [ + "bool full" + ], + "lineno": 585, + "name": "RCLConsensus::getJson" + }, + { + "args": [ + "NetClock::time_point const& now", + "std::unique_ptr const& clog" + ], + "lineno": 597, + "name": "RCLConsensus::timerEntry" + }, + { + "args": [ + "NetClock::time_point const& now", + "RCLTxSet const& txSet" + ], + "lineno": 613, + "name": "RCLConsensus::gotTxSet" + }, + { + "args": [ + "NetClock::time_point const& now", + "std::optional consensusDelay" + ], + "lineno": 626, + "name": "RCLConsensus::simulate" + }, + { + "args": [ + "NetClock::time_point const& now", + "RCLCxPeerPos const& newProposal" + ], + "lineno": 632, + "name": "RCLConsensus::peerProposal" + }, + { + "args": [ + "RCLCxLedger const& prevLgr", + "hash_set const& nowTrusted" + ], + "lineno": 638, + "name": "RCLConsensus::Adaptor::preStartRound" + }, + { + "args": [], + "lineno": 670, + "name": "RCLConsensus::Adaptor::haveValidated" + }, + { + "args": [], + "lineno": 674, + "name": "RCLConsensus::Adaptor::getValidLedgerIndex" + }, + { + "args": [], + "lineno": 678, + "name": "RCLConsensus::Adaptor::getQuorumKeys" + }, + { + "args": [ + "Ledger_t::Seq const seq", + "hash_set& trustedKeys" + ], + "lineno": 683, + "name": "RCLConsensus::Adaptor::laggards" + }, + { + "args": [], + "lineno": 688, + "name": "RCLConsensus::Adaptor::validator" + }, + { + "args": [ + "std::size_t const positions" + ], + "lineno": 692, + "name": "RCLConsensus::Adaptor::updateOperatingMode" + }, + { + "args": [ + "NetClock::time_point const& now", + "RCLCxLedger::ID const& prevLgrId", + "RCLCxLedger const& prevLgr", + "hash_set const& nowUntrusted", + "hash_set const& nowTrusted", + "std::unique_ptr const& clog" + ], + "lineno": 697, + "name": "RCLConsensus::startRound" + }, + { + "args": [ + "char const* label", + "bool const validating", + "beast::Journal j" + ], + "lineno": 707, + "name": "RclConsensusLogger::RclConsensusLogger" + }, + { + "args": [], + "lineno": 715, + "name": "RclConsensusLogger::~RclConsensusLogger" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is primarily in constructors and is likely tested indirectly via consensus engine startup and validator identity tests. Look for test files in the codebase such as 'test/consensus/Consensus_test.cpp', 'test/consensus/RCLConsensus_test.cpp', or integration tests that start the application with various validator key configurations. Direct unit tests for constructor validation (e.g., valCookie_ nonzero, nodeID nonzero, keys pointer) may be missing or only covered by integration tests. There may be a gap in explicit negative tests for invalid ValidatorKeys or zero valCookie_ (since XRPL_ASSERT may abort).", + "validation_architecture": { + "auto_validated_fields": [ + "valCookie_ (range via assertion)", + "validatorKeys_.nodeID (business logic via conditional)", + "validatorKeys_.keys (pointer validity via conditional)" + ], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system", + "validation_layer": "constructor (object initialization)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "valCookie_", + "location": "RCLConsensus::Adaptor::Adaptor (constructor)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "valCookie_ must be nonzero" + ], + "validation_type": "range" + }, + { + "confidence": 0.9, + "error_thrown": "None (used as conditional, not error)", + "field": "validatorKeys_.nodeID", + "location": "RCLConsensus::Adaptor::Adaptor (constructor)", + "validated_by": "comparison (validatorKeys_.nodeID != beast::zero)", + "validates": [ + "Checks if nodeID is set (not zero) before using validator keys" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "None (used as conditional, not error)", + "field": "validatorKeys_.keys", + "location": "RCLConsensus::Adaptor::Adaptor (constructor)", + "validated_by": "pointer check (validatorKeys_.keys)", + "validates": [ + "Checks if validatorKeys_.keys is not null before dereferencing" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLConsensus.cpp.ai.md b/src/xrpld/app/consensus/RCLConsensus.cpp.ai.md new file mode 100644 index 0000000000..f72d1c92ea --- /dev/null +++ b/src/xrpld/app/consensus/RCLConsensus.cpp.ai.md @@ -0,0 +1,59 @@ +# `RCLConsensus.cpp` — XRPL Consensus Engine Implementation + +## Role and Context + +`RCLConsensus.cpp` is the implementation file for the XRP Ledger's consensus engine integration layer. It sits at the junction between the protocol-agnostic `Consensus` template (defined in `xrpld/consensus/`) and all of the concrete XRPL infrastructure — ledger management, transaction sets, overlay networking, fee voting, amendment tables, and validation broadcasting. The file defines two closely related classes: `RCLConsensus` (the externally-facing wrapper) and its inner `RCLConsensus::Adaptor` (which implements the template interface the generic engine requires), plus the helper `RclConsensusLogger`. + +## The Adaptor Pattern + +The generic `Consensus` template is algorithm-only; it delegates all protocol interactions through the `Adaptor` type via static dispatch. `RCLConsensus::Adaptor` is that concrete adaptor for the Ripple Consensus Ledger. Rather than exposing the adaptor directly, `RCLConsensus` owns both `adaptor_` and `consensus_` as member fields and provides a mutex-guarded public API that routes every inbound event into the engine under `mutex_`. This separation is intentional: the generic `Consensus` object is single-threaded and stateful, so all entries from external threads (timer, peer proposals, new tx sets) acquire `mutex_` first, while the adaptor callbacks invoked from _within_ the engine's methods already hold that lock implicitly by contract. + +The adaptor's observable state — `validating_`, `prevProposers_`, `prevRoundTime_`, `mode_` — is declared `std::atomic` so callers can query them without holding the consensus lock. + +## Construction and Identity + +The `Adaptor` constructor randomly generates `valCookie_`, a nonzero 64-bit value used to tag every validation this node emits. Tagging validations with a session cookie lets peers detect stale messages from a previous server instance that has since restarted. The value is computed as `1 + rand_int(prng, max-1)`, guaranteeing it is never zero, and this is defended by `XRPL_ASSERT`. If the node holds validator keys, the constructor logs the master public key and, when key rotation is active (master key ≠ ephemeral signing key), also logs the ephemeral key with its rotation sequence number. + +## Ledger Acquisition + +`acquireLedger` implements a "lazy single-trigger" pattern. If the required ledger isn't already available in `LedgerMaster`, it records the target hash in `acquiringLedger_` to prevent duplicate jobs, then submits a `jtADVANCE` job to `InboundLedgers::acquireAsync`. On the next timer tick, if the ledger has arrived, the flow continues; otherwise the engine remains blocked. Once a ledger is found, two assertions verify it is closed (not `open()`) and immutable before wrapping it in `RCLCxLedger`. + +## Ledger Close — `onClose` + +When the current ledger closes, `onClose` builds the initial transaction set from the open ledger's snapshot. Critically, pseudo-transactions are injected at this stage, not by generic consensus: fee-vote and amendment-vote pseudo-txs are added when the previous ledger was a flag ledger (only if proposing and synced), and negative-UNL vote pseudo-txs are added when the previous ledger was a voting ledger. After snapshotting the `SHAMap`, if the node has correct LCL, all proposed transaction IDs and their sequence numbers are registered with `censorshipDetector_` for later tracking. + +## Accept Path — `onAccept` vs `onForceAccept` + +When consensus reaches agreement, `onAccept` **defers** the expensive work: it schedules a `jtACCEPT` job on the application's job queue so the consensus engine's timer thread is not held. The comment in the code explains why this is safe: the generic `Consensus` engine guarantees that the `result` reference and the fields captured from `prevLedger` will remain valid and unchanged until `startRound` is called (which only happens from `endConsensus`, triggered at the end of the accept job). `onForceAccept` is the bypass path for simulations and ledger-skip scenarios; it calls `doAccept` directly without scheduling. + +## The Heart — `doAccept` + +`doAccept` is where a consensus result becomes a committed ledger. It proceeds through several phases: + +**Transaction deserialization**: The consensus tx set (`SHAMap`) is walked to build a `CanonicalTXSet`, which sorts transactions by the hash of their containing set — giving deterministic but unpredictable ordering across validators. Any transaction that fails deserialization is recorded in `failed` and excluded. + +**Ledger construction**: `buildLCL` is called, which either replays a captured ledger (for testing/catch-up) or calls `buildLedger` to apply the canonical tx set on top of the previous ledger. `buildLCL` also calls `TxQ::processClosedLedger` to update fee escalation state based on whether the round was long (>5 seconds). + +**Censorship detection**: After acceptance, `censorshipDetector_.check` compares what was accepted against what was proposed in `onClose`. Any transaction that has been proposed but not accepted for `censorshipWarnInternal` (15) ledger intervals triggers a warning log. Transactions that actually failed application are exempted from warnings. + +**Validation**: If the node is an active validator and consensus did not fail (`ConsensusState::Yes`), `validate` constructs an `STValidation` signed with the ephemeral key. The validation embeds: the ledger hash, the consensus hash (the agreed tx set ID), the ledger sequence, the `valCookie_`, the most recent fully-validated ledger hash, server version (on voting ledgers), load fee, fee vote preferences, and amendment votes. The validation is first added to the local validation store via `handleNewValidation`, then broadcast over the overlay, then published to RPC subscribers. + +**Open ledger transition**: Disputed transactions that this node voted NO on (and that are not pseudo-txs) are re-inserted into `retriableTxs` so they get a second chance in the next open ledger. Then `OpenLedger::accept` advances the open ledger under a double-lock on `masterMutex` and `LedgerMaster::peekMutex`. After that, `LedgerMaster::switchLCL` finalizes the closed ledger. + +**Clock adjustment**: If the round ended without consensus failure, the node computes a weighted average of peers' close-time votes to estimate its own clock offset and calls `TimeKeeper::adjustCloseTime`. This is the mechanism by which the distributed network converges on a consistent `NetClock`. + +## Pre-round Setup — `preStartRound` + +Before each round, the adaptor recalculates `validating_`. The guard conditions are: validator keys must be configured, the ledger sequence must be at or past the `maxDisallowedLedger` threshold (protecting against signing stale validations after a restart), the node must not be amendment-blocked, and in live network mode the validator list must not have expired. If any condition fails, the node silently drops to observer mode. The function also notifies `NegativeUNLVote` about any newly trusted validators. It returns `true` (entering as proposer) only when `validating_ && synced`. + +## Censorship Detector Reset + +`onModeChange` resets `censorshipDetector_` whenever consensus mode transitions away from `proposing` or `observing`. This prevents false-positive censorship warnings that would otherwise fire when the node loses sync and starts operating on a divergent view of the ledger. + +## `RclConsensusLogger` + +A lightweight RAII timer. It only allocates a `stringstream` when the node is validating or journal info is enabled; otherwise construction is effectively free. On destruction it writes the elapsed wall-clock duration plus any accumulated diagnostic text using `writeAlways` — bypassing the journal's normal severity filter to ensure acceptance timing always appears in logs for operator diagnostics. + +## Key Invariants and Failure Modes + +The code assumes the generic `Consensus` engine's single-threaded contract: callbacks fire with `mutex_` logically held (the lock is owned by the public entry methods), so `doAccept` and other adaptor callbacks never need to re-acquire it. Violating this contract by calling adaptor methods from outside would cause data races on the internal consensus state. The ledger acquired in `acquireLedger` is asserted to be both immutable and closed before use — an open or mutable ledger reaching this path indicates a serious internal inconsistency. Validation signing silently no-ops (`return` after a warning log) if `validatorKeys_.keys` is null, rather than throwing, to allow non-validator nodes to participate in consensus rounds as observers without special-casing at the call sites. \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLConsensus.h.ai.json b/src/xrpld/app/consensus/RCLConsensus.h.ai.json new file mode 100644 index 0000000000..4b1ca2b2cc --- /dev/null +++ b/src/xrpld/app/consensus/RCLConsensus.h.ai.json @@ -0,0 +1,130 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "app", + "feeVote", + "ledgerMaster", + "localTxs", + "inboundTransactions", + "clock", + "validatorKeys", + "journal" + ], + "lineno": 27, + "name": "RCLConsensus" + }, + { + "args": [ + "label", + "validating", + "j" + ], + "lineno": 205, + "name": "RclConsensusLogger" + } + ], + "description": "Defines the RCLConsensus class, which manages the consensus algorithm for the Ripple Consensus Ledger (RCL), including its adaptor for the generic consensus engine, and logging utilities for consensus activities.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/consensus/RCLConsensus.h", + "functions": [ + { + "args": [], + "lineno": 120, + "name": "validating" + }, + { + "args": [], + "lineno": 126, + "name": "prevProposers" + }, + { + "args": [], + "lineno": 134, + "name": "prevRoundTime" + }, + { + "args": [], + "lineno": 142, + "name": "mode" + }, + { + "args": [], + "lineno": 147, + "name": "phase" + }, + { + "args": [ + "full" + ], + "lineno": 152, + "name": "getJson" + }, + { + "args": [ + "now", + "prevLgrId", + "prevLgr", + "nowUntrusted", + "nowTrusted", + "clog" + ], + "lineno": 158, + "name": "startRound" + }, + { + "args": [ + "now", + "clog" + ], + "lineno": 167, + "name": "timerEntry" + }, + { + "args": [ + "now", + "txSet" + ], + "lineno": 172, + "name": "gotTxSet" + }, + { + "args": [], + "lineno": 177, + "name": "prevLedgerID" + }, + { + "args": [ + "now", + "consensusDelay" + ], + "lineno": 184, + "name": "simulate" + }, + { + "args": [ + "now", + "newProposal" + ], + "lineno": 190, + "name": "peerProposal" + }, + { + "args": [], + "lineno": 195, + "name": "parms" + }, + { + "args": [], + "lineno": 217, + "name": "ss" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 18, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLConsensus.h.ai.md b/src/xrpld/app/consensus/RCLConsensus.h.ai.md new file mode 100644 index 0000000000..2be968f10c --- /dev/null +++ b/src/xrpld/app/consensus/RCLConsensus.h.ai.md @@ -0,0 +1,56 @@ +# `RCLConsensus.h` — XRP Ledger Consensus Orchestrator + +## Role in the System + +`RCLConsensus` is the bridge between the abstract, ledger-agnostic consensus algorithm (`Consensus`) and the concrete XRP Ledger application. The generic engine in `src/xrpld/consensus/Consensus.h` is a policy-based template — it knows nothing about XRPL ledger format, peer networking, or the SHAMap transaction set representation. `RCLConsensus` supplies all of that context through a nested `Adaptor` class that satisfies the `Consensus<>` template requirements, while also providing a clean, mutex-protected public interface that the rest of `xrpld` interacts with. + +## Two-Layer Architecture + +The file declares two cooperating classes within the `xrpl` namespace. + +**`RCLConsensus`** is the externally visible class. It holds a `recursive_mutex` that guards all access to the internal `Consensus` object. Every public method — `startRound()`, `timerEntry()`, `gotTxSet()`, `peerProposal()`, `simulate()` — acquires this lock before delegating to the inner `consensus_` member. The `prevLedgerID()` method is a textbook example: it takes the lock and immediately reads a value, ensuring callers from multiple threads see a consistent result. The recursive nature of the mutex is deliberate: some code paths reenter the lock from within the same thread during complex state transitions. + +**`RCLConsensus::Adaptor`** is the private implementation class. It holds all the application-level services (`Application&`, `LedgerMaster&`, `LocalTxs&`, `InboundTransactions&`) and implements the callbacks that `Consensus` calls at each phase transition. Crucially, the code comment in the header is explicit: these private callback methods are *only* ever called by `Consensus` (via friend declaration), which means they execute while the outer mutex is already held — except for the `onAccept` dispatched job (see below). This single-writer invariant is what makes the callback implementations safe without needing to re-acquire locks of their own. + +## Adaptor Type Bindings + +`Adaptor` defines the associated type aliases that parameterize the generic algorithm: + +- `Ledger_t = RCLCxLedger` — thin shared-ptr wrapper over `Ledger const` +- `TxSet_t = RCLTxSet` — backed by a `SHAMap` snapshot +- `PeerPosition_t = RCLCxPeerPos` — a signed proposal from a peer +- `NodeID_t = NodeID`, `NodeKey_t = PublicKey` + +These bindings mean the generic `Consensus<>` state machine never sees raw XRPL types — it works through these adaptor types, which allows the algorithm to be tested in isolation with lightweight stubs. + +## State-Accessible Atomics + +Four `Adaptor` members are `std::atomic`: `validating_`, `prevProposers_`, `prevRoundTime_`, and `mode_`. These have corresponding public getters (`validating()`, `prevProposers()`, `prevRoundTime()`, `mode()`) on both `Adaptor` and `RCLConsensus`. The atomic storage is intentional: callers like RPC handlers and monitoring code need to read consensus status without contending for the main consensus mutex. The rest of the application can sample the current consensus mode or round statistics at any time with no lock overhead. + +## Lifecycle Callbacks + +The consensus cycle flows through three primary `Adaptor` callbacks: + +**`onClose()`** fires when the open ledger closes. It snapshots the open ledger's transactions into an immutable `SHAMap`, then conditionally injects pseudo-transactions for fee voting, amendment voting, and negative-UNL voting based on whether the current ledger is a flag ledger or voting ledger. It also calls `censorshipDetector_.propose()` with the initial transaction set, beginning censorship tracking for this round. + +**`onAccept()`** is the most architecturally interesting callback. Rather than directly processing the agreed ledger, it dispatches a `jtACCEPT` job onto the application's `JobQueue`. The comment in the implementation explicitly explains why no lock is held in that job: the generic `Consensus<>` guarantees that once `onAccept` is called, the consensus result state won't change until `startRound` is called by `endConsensus()`. This deferred, lock-free dispatch avoids blocking the consensus timer while the expensive ledger-building and validation I/O happens on a worker thread. + +**`onForceAccept()`** handles simulation and forced-accept scenarios — it calls `doAccept()` directly, synchronously, without the job queue indirection. + +**`doAccept()`** is the shared implementation backing both paths. It builds a `CanonicalTXSet` from the agreed transaction set (using the SHAMap hash as a seed for deterministic-but-unpredictable ordering), calls `buildLCL()` to apply transactions and produce the new closed ledger, runs censorship detection, optionally calls `validate()` to sign and broadcast a validation, builds a new open ledger from disputed and retried transactions, and finally updates the time-keeper using a weighted average of peer close-time reports. + +## Censorship Detection + +`RCLCensorshipDetector` tracks transactions the local node has proposed that haven't made it into a consensus ledger. The `censorshipWarnInternal` constant of 15 means a warning is emitted every 15 ledgers that a proposed transaction remains excluded. The detector is reset via `onModeChange()` whenever the node leaves the proposing or observing modes, preventing stale warnings after network reconnects. + +## Validation Cookie + +During `Adaptor` construction, `valCookie_` is assigned a randomly chosen non-zero `uint64_t`. This cookie is embedded in outgoing `STValidation` messages to let recipients distinguish fresh validations from replayed ones, providing a lightweight replay-protection mechanism that doesn't require persistent state across restarts. + +## `preStartRound()` — The Gatekeeper + +Before each new consensus round, `preStartRound()` determines whether the node should actively propose. It checks that validator keys are configured, that the previous ledger sequence exceeds `getMaxDisallowedLedger()` (preventing validation of ledgers before the server fully synced), that the node is not amendment-blocked, and that the UNL has not expired. If all checks pass and the node is in `OperatingMode::FULL`, it returns `true`, meaning the node will enter the round as a proposer. Otherwise, it participates only as an observer. + +## `RclConsensusLogger` + +The companion `RclConsensusLogger` class is an RAII timing helper. Constructed with a label and a `validating` flag, it allocates a `stringstream` only when logging is warranted (always for validators, otherwise at `INFO` level). On destruction it appends the elapsed time in seconds and flushes the accumulated log content at the appropriate level. It exists because consensus heartbeat processing spans multiple function calls — the logger correlates all of them into a single timestamped trace entry rather than emitting disconnected log lines. \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCxLedger.h.ai.json b/src/xrpld/app/consensus/RCLCxLedger.h.ai.json new file mode 100644 index 0000000000..9c2dd24b2a --- /dev/null +++ b/src/xrpld/app/consensus/RCLCxLedger.h.ai.json @@ -0,0 +1,80 @@ +{ + "args": [ + { + "lineno": 29, + "name": "l" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const& l" + ], + "lineno": 13, + "name": "RCLCxLedger" + } + ], + "description": "Defines the RCLCxLedger class, a thin wrapper over a shared pointer to a Ledger, providing accessors and metadata for use in RCLConsensus.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/consensus/RCLCxLedger.h", + "functions": [ + { + "args": [], + "lineno": 20, + "name": "RCLCxLedger" + }, + { + "args": [ + "std::shared_ptr const& l" + ], + "lineno": 28, + "name": "RCLCxLedger" + }, + { + "args": [], + "lineno": 35, + "name": "seq" + }, + { + "args": [], + "lineno": 41, + "name": "id" + }, + { + "args": [], + "lineno": 47, + "name": "parentID" + }, + { + "args": [], + "lineno": 53, + "name": "closeTimeResolution" + }, + { + "args": [], + "lineno": 59, + "name": "closeAgree" + }, + { + "args": [], + "lineno": 65, + "name": "closeTime" + }, + { + "args": [], + "lineno": 71, + "name": "parentCloseTime" + }, + { + "args": [], + "lineno": 77, + "name": "getJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCxLedger.h.ai.md b/src/xrpld/app/consensus/RCLCxLedger.h.ai.md new file mode 100644 index 0000000000..248c36e3a8 --- /dev/null +++ b/src/xrpld/app/consensus/RCLCxLedger.h.ai.md @@ -0,0 +1,55 @@ +# RCLCxLedger.h + +## Role in the System + +`RCLCxLedger` is the ledger-type adapter that bridges the XRPL's concrete `Ledger` class into the generic, policy-based `Consensus` engine. The consensus engine in `src/xrpld/consensus/Consensus.h` is intentionally ledger-agnostic — it operates against a user-supplied `Adaptor` type, which in turn declares a `Ledger_t` alias. For the live XRP network (the RCL — Ripple Consensus Ledger), that alias is `RCLCxLedger`: + +```cpp +// In RCLConsensus::Adaptor: +using Ledger_t = RCLCxLedger; +``` + +The file's sole job is to present just enough of `Ledger const`'s surface area to satisfy the interface contract the generic engine expects, while keeping the full-fledged ledger object reachable through the public `ledger_` member. + +## Design: Thin Wrapper over Shared Ownership + +`RCLCxLedger` owns a `std::shared_ptr` rather than a value or a raw pointer. This is consistent with the rest of the XRPL codebase, where ledgers are large, immutable objects shared across many subsystems simultaneously. The wrapper adds no heap allocation of its own — copying an `RCLCxLedger` merely increments the reference count on the underlying ledger. + +The public `ledger_` member is an explicit design choice (and acknowledged departure from ideal encapsulation). When `RCLConsensus::Adaptor` needs to build a *new* ledger by applying a transaction set to the previous one, it must pass the concrete `Ledger const*` to the application layer. Exposing `ledger_` directly avoids a proliferation of accessor overloads while keeping the wrapper simple. The accompanying TODO note flags the long-term intent to replace this with `shared_ptr` — which would enforce read-only access at the type level — but that would require a mechanism to construct a new ledger from a `ReadView`, which doesn't yet exist. + +## Interface Contract Satisfied + +The generic `Consensus` template calls the following methods on its `Ledger_t`: + +- `id()` → `LedgerHash` — used as the primary key when tracking which ledger a round is building on. +- `seq()` → `LedgerIndex` — used to compute the next sequence number (`previousLedger_.seq() + Seq{1}`). +- `parentID()` → `LedgerHash` — for tracing ledger ancestry. +- `closeTimeResolution()` → `NetClock::duration` — the consensus engine uses this to determine acceptable close-time windows for a new ledger; validators only agree if their proposed close times fall within the same resolution bucket. +- `closeAgree()` → `bool` — delegates to `xrpl::getCloseAgree(header)`, which decodes a flag in the ledger header indicating whether consensus validators agreed on the close time or were forced to accept a default. +- `parentCloseTime()` → `NetClock::time_point` — used in timing calculations for whether to close the current open ledger. + +These accessors are all one-liners that forward directly into `ledger_->header()` — there is no caching or computation here, by design. The `Ledger` object itself caches its parsed header, so repeated calls are cheap. + +## Sibling Types + +`RCLCxLedger` is one of three RCL-specific consensus wrappers defined in this directory: + +| Wrapper | Wraps | Adaptor alias | +|---|---|---| +| `RCLCxLedger` | `shared_ptr` | `Ledger_t` | +| `RCLCxTx` / `RCLTxSet` | `SHAMapItem` / `SHAMap` | `TxSet_t` | +| `RCLCxPeerPos` | Peer proposal message | `PeerPosition_t` | + +Together they form the complete set of concrete types that parameterize `Consensus`. The pattern is uniform: each wrapper defines an `ID` type alias and an `id()` accessor, giving the generic engine a consistent way to identify and compare ledgers, transaction sets, and peer proposals without knowing their internal structure. + +## Known Design Debt + +Two TODO comments in the file call out intentional shortcuts: + +1. **Default constructor**: `RCLCxLedger() = default` leaves `ledger_` as a null `shared_ptr`. The comment notes this should be eliminated by guaranteeing `RCLConsensus` is always initialized with a valid ledger, but no callers yet enforce that invariant, and the adaptor methods do not null-check before dereferencing. + +2. **`ledger_` type**: Ideally this would be `shared_ptr` to express that consensus never modifies a ledger after creation. The concrete `Ledger` type is needed only when building the *next* ledger; using `ReadView` everywhere else would make the immutability contract explicit at the type system level. + +## `getJson()` + +The `getJson()` method delegates to `xrpl::getJson({*ledger_, {}})` (from `LedgerToJson.h`), producing a full JSON representation of the ledger. This is used by the consensus engine for diagnostic logging when a node detects it is working on the wrong ledger (`ConsensusMode::wrongLedger`), making it straightforward to dump the complete state of the ledger in question to the journal or RPC output without any extra serialization logic in `RCLCxLedger` itself. \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCxPeerPos.cpp.ai.json b/src/xrpld/app/consensus/RCLCxPeerPos.cpp.ai.json new file mode 100644 index 0000000000..60a61c737a --- /dev/null +++ b/src/xrpld/app/consensus/RCLCxPeerPos.cpp.ai.json @@ -0,0 +1,416 @@ +{ + "args": [ + { + "lineno": 8, + "name": "publicKey" + }, + { + "lineno": 9, + "name": "signature" + }, + { + "lineno": 10, + "name": "suppression" + }, + { + "lineno": 11, + "name": "proposal" + }, + { + "lineno": 39, + "name": "proposeHash" + }, + { + "lineno": 40, + "name": "previousLedger" + }, + { + "lineno": 41, + "name": "proposeSeq" + }, + { + "lineno": 42, + "name": "closeTime" + }, + { + "lineno": 43, + "name": "publicKey" + }, + { + "lineno": 44, + "name": "signature" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RCLCxPeerPos::RCLCxPeerPos" + ], + "entry_point": "RCLCxPeerPos::RCLCxPeerPos", + "purpose": "Constructs a peer proposal object from received data, performs initial signature size validation, and stores fields.", + "validation_points": [ + "XRPL_ASSERT macro: Validates signature size (not empty, <= capacity)", + "if statement: Re-validates signature size before assignment" + ] + }, + { + "call_chain": [ + "RCLCxPeerPos::checkSign", + "verifyDigest" + ], + "entry_point": "RCLCxPeerPos::checkSign", + "purpose": "Checks cryptographic validity of the signature against the proposal's signing hash and public key.", + "validation_points": [ + "verifyDigest: Validates cryptographic signature" + ] + }, + { + "call_chain": [ + "RCLCxPeerPos::getJson", + "proposal().getJson()", + "toBase58" + ], + "entry_point": "RCLCxPeerPos::getJson", + "purpose": "Serializes the proposal to JSON, including peer_id if publicKey is non-empty.", + "validation_points": [ + "if (publicKey().size() != 0u): Validates publicKey is non-empty before encoding" + ] + }, + { + "call_chain": [ + "proposalUniqueId", + "Serializer::addBitString/add32/addVL", + "Serializer::getSHA512Half" + ], + "entry_point": "proposalUniqueId", + "purpose": "Computes a unique identifier for a proposal based on its fields, public key, and signature.", + "validation_points": [ + "No explicit validation in this function; assumes inputs are already validated" + ] + } + ], + "data_flows": [ + { + "field": "signature", + "flow": [ + "Constructor argument", + "XRPL_ASSERT and if statement validate size", + "signature_.assign(signature.begin(), signature.end())", + "Stored in signature_ member", + "Used in checkSign() and proposalUniqueId()" + ], + "origin": "Constructor argument (Slice const& signature)", + "transformations": [ + "Size checked (<= capacity)", + "Copied into signature_ vector" + ], + "validated_at": "RCLCxPeerPos::RCLCxPeerPos (XRPL_ASSERT and if statement)" + }, + { + "field": "publicKey", + "flow": [ + "Constructor argument", + "Stored in publicKey_ member", + "Used in checkSign(), getJson(), proposalUniqueId()" + ], + "origin": "Constructor argument (PublicKey const& publicKey)", + "transformations": [ + "None in constructor", + "Size checked in getJson()" + ], + "validated_at": "RCLCxPeerPos::getJson (size check before toBase58)" + }, + { + "field": "signature_", + "flow": [ + "Assigned in constructor after validation", + "Used in checkSign()", + "Used in proposalUniqueId()" + ], + "origin": "Assigned from signature argument in constructor", + "transformations": [ + "Copied from input Slice" + ], + "validated_at": "Constructor (size check before assignment)" + }, + { + "field": "proposal_", + "flow": [ + "Constructor argument", + "Stored in proposal_ member", + "Used in checkSign() (proposal_.signingHash())", + "Used in getJson()" + ], + "origin": "Constructor argument (Proposal const& proposal)", + "transformations": [ + "None" + ], + "validated_at": "Not directly validated here" + } + ], + "description": "Implements the RCLCxPeerPos class for handling peer proposals in XRPL consensus, including construction, signature verification, JSON serialization, and unique proposal ID calculation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "signature", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at RCLCxPeerPos constructor", + "issue_pattern": "Missing empty string validation for signature", + "why_false_positive": "XRPL_ASSERT macro validates signature for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "signature", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT macro at RCLCxPeerPos constructor", + "issue_pattern": "Missing range validation for signature", + "why_false_positive": "XRPL_ASSERT macro validates signature range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "signature", + "empty", + "string", + "validation" + ], + "evidence": "if statement at RCLCxPeerPos constructor", + "issue_pattern": "Missing empty string validation for signature", + "why_false_positive": "if statement validates signature for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "signature", + "range", + "bounds", + "validation" + ], + "evidence": "if statement at RCLCxPeerPos constructor", + "issue_pattern": "Missing range validation for signature", + "why_false_positive": "if statement validates signature range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "signature (cryptographic validity)", + "empty", + "string", + "validation" + ], + "evidence": "verifyDigest function at RCLCxPeerPos::checkSign", + "issue_pattern": "Missing empty string validation for signature (cryptographic validity)", + "why_false_positive": "verifyDigest function validates signature (cryptographic validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "publicKey", + "empty", + "string", + "validation" + ], + "evidence": "size check at RCLCxPeerPos::getJson", + "issue_pattern": "Missing empty string validation for publicKey", + "why_false_positive": "size check validates publicKey for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "publicKey", + "format", + "validation", + "invalid" + ], + "evidence": "size check at RCLCxPeerPos::getJson", + "issue_pattern": "Missing format validation for publicKey", + "why_false_positive": "size check validates publicKey format" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/consensus/RCLCxPeerPos.cpp", + "functions": [ + { + "args": [ + "publicKey", + "signature", + "suppression", + "proposal" + ], + "lineno": 7, + "name": "RCLCxPeerPos::RCLCxPeerPos" + }, + { + "args": [], + "lineno": 22, + "name": "RCLCxPeerPos::checkSign" + }, + { + "args": [], + "lineno": 27, + "name": "RCLCxPeerPos::getJson" + }, + { + "args": [ + "proposeHash", + "previousLedger", + "proposeSeq", + "closeTime", + "publicKey", + "signature" + ], + "lineno": 38, + "name": "proposalUniqueId" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by consensus and proposal validation tests in the XRPLF/rippled codebase, such as tests in 'src/test/consensus', 'src/test/app/consensus', or 'src/test/protocol'. Specifically, tests for proposal signature validation, malformed proposals, and peer position handling would exercise these paths. However, direct unit tests for RCLCxPeerPos constructor's signature size checks and checkSign() may be limited; fuzzing or malformed input tests may not cover all edge cases (e.g., oversized signatures, empty public keys). There may be gaps in testing for boundary conditions (e.g., signature size exactly at capacity, empty signature, invalid public key sizes).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL custom (XRPL_ASSERT, verifyDigest, jss:: for JSON keys)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "signature", + "location": "RCLCxPeerPos constructor", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "signature is not empty", + "signature size does not exceed signature_ capacity (max 72 bytes)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (assignment skipped if invalid)", + "field": "signature", + "location": "RCLCxPeerPos constructor", + "validated_by": "if statement", + "validates": [ + "signature is not empty", + "signature size does not exceed signature_ capacity" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "signature (cryptographic validity)", + "location": "RCLCxPeerPos::checkSign", + "validated_by": "verifyDigest function", + "validates": [ + "signature matches publicKey and proposal_.signingHash" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (field omitted if invalid)", + "field": "publicKey", + "location": "RCLCxPeerPos::getJson", + "validated_by": "size check", + "validates": [ + "publicKey is not empty before encoding to Base58" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCxPeerPos.cpp.ai.md b/src/xrpld/app/consensus/RCLCxPeerPos.cpp.ai.md new file mode 100644 index 0000000000..9d7062dc35 --- /dev/null +++ b/src/xrpld/app/consensus/RCLCxPeerPos.cpp.ai.md @@ -0,0 +1,55 @@ +# `RCLCxPeerPos.cpp` — Signed Peer Proposal for RCL Consensus + +This file implements `RCLCxPeerPos`, the XRPL network's concrete representation of a validator's signed position during a consensus round. It is the glue layer between the generic consensus engine (`ConsensusProposal`) and the network transport layer — wrapping a proposal with its cryptographic proof of origin and its hash-router suppression identity. + +## Role in the Consensus Pipeline + +XRPL's consensus protocol is built around a generic `ConsensusProposal` template. That template knows nothing about signatures, public keys, or network deduplication. `RCLCxPeerPos` layers exactly those concerns on top: it owns a `PublicKey`, a signature, a pre-computed suppression hash (`suppression_`), and the concrete proposal instantiated as `ConsensusProposal`. This separation lets the consensus algorithm remain topology-agnostic while the network layer retains full control over trust and replay suppression. + +## Constructor and Signature Storage + +The constructor is called exclusively for *received* proposals — when a peer's `TMProposeSet` message is unpacked by `PeerImp`. The signature arrives as a raw `Slice` and must be stored durably. The design choice here is deliberate: `signature_` is declared as `boost::container::static_vector`. This is a fixed-capacity container allocated entirely on the stack/inline with no heap allocation, whose capacity exactly matches the maximum DER-encoded ECDSA signature size (72 bytes). + +The constructor enforces this limit with two independent checks: + +```cpp +XRPL_ASSERT( + !signature.empty() && signature.size() <= signature_.capacity(), + "xrpl::RCLCxPeerPos::RCLCxPeerPos : valid signature size"); + +if (!signature.empty() && signature.size() <= signature_.capacity()) + signature_.assign(signature.begin(), signature.end()); +``` + +The `XRPL_ASSERT` fires in debug builds (typically aborting), catching protocol violations during development. The `if` guard runs in all builds — if an oversized signature somehow slips through validation upstream, the assignment is silently skipped rather than overflowing the static buffer. This dual pattern is common in the XRPL codebase: assert to detect bugs early, guard to preserve invariants in production. + +## Signature Verification: `checkSign()` + +`checkSign()` delegates entirely to `verifyDigest()`, passing the stored public key, the proposal's signing hash (computed by `ConsensusProposal`), and the stored signature. It returns a `bool` rather than throwing; callers in `PeerImp` discard proposals that fail this check. This function is the point at which a received proposal transitions from "structurally valid" to "cryptographically authenticated." + +## `proposalUniqueId()` — The Suppression Hash + +The free function `proposalUniqueId()` is called in two places: `RCLConsensus::propose()` when the local validator broadcasts its own position, and `PeerImp` when a peer proposal first arrives. In both cases the result seeds the hash router to deduplicate proposal flooding. + +```cpp +Serializer s(512); +s.addBitString(proposeHash); +s.addBitString(previousLedger); +s.add32(proposeSeq); +s.add32(closeTime.time_since_epoch().count()); +s.addVL(publicKey); +s.addVL(signature); +return s.getSHA512Half(); +``` + +The function commits every semantically distinguishing field — position hash, previous ledger, sequence number, close time, public key, and the signature itself — into a single `SHA512Half` digest. Including the signature in this ID is a considered choice: it makes the suppression ID unique not just per *proposal content* but per *signed emission*. Two validators could in theory agree on identical proposal fields, yet their differing signatures produce distinct suppression IDs, so their broadcasts are tracked independently by the relay mechanism. It also guarantees that a verbatim replayed message (same bytes, same signature) produces the same suppression ID and is silently dropped, even if it arrives via a different peer. + +In `PeerImp`, immediately after calling `proposalUniqueId()`, the result is passed to `app_.getHashRouter().addSuppressionPeerWithStatus()`. If that returns `!added`, the proposal is a duplicate and is dropped before any cryptographic verification occurs — ensuring expensive `checkSign()` calls are never made on redundant traffic. + +## JSON Rendering + +`getJson()` delegates to `proposal().getJson()` for the proposal fields and then conditionally appends `peer_id` as a Base58-encoded node public key. The size guard on `publicKey()` before calling `toBase58()` handles the edge case where a locally-originated proposal (which has no remote peer key) is serialized — in that path the `peer_id` field is simply omitted. + +## Relationship to Sibling Files + +Within `src/xrpld/app/consensus/`, `RCLCxPeerPos` is the peer-proposal counterpart to `RCLCxLedger` (ledger wrapper) and `RCLCxTx` (transaction wrapper) — all three adapt generic consensus types to the concrete XRPL protocol. `RCLConsensus.cpp` is the primary consumer, constructing `RCLCxPeerPos` objects from inbound wire messages and passing them upward into the generic consensus algorithm. \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCxPeerPos.h.ai.json b/src/xrpld/app/consensus/RCLCxPeerPos.h.ai.json new file mode 100644 index 0000000000..7b06f8180a --- /dev/null +++ b/src/xrpld/app/consensus/RCLCxPeerPos.h.ai.json @@ -0,0 +1,121 @@ +{ + "args": [ + { + "lineno": 28, + "name": "publicKey" + }, + { + "lineno": 29, + "name": "signature" + }, + { + "lineno": 30, + "name": "suppress" + }, + { + "lineno": 31, + "name": "proposal" + }, + { + "lineno": 104, + "name": "proposeHash" + }, + { + "lineno": 105, + "name": "previousLedger" + }, + { + "lineno": 106, + "name": "proposeSeq" + }, + { + "lineno": 107, + "name": "closeTime" + }, + { + "lineno": 108, + "name": "publicKey" + }, + { + "lineno": 109, + "name": "signature" + } + ], + "classes": [ + { + "args": [ + "PublicKey const& publicKey", + "Slice const& signature", + "uint256 const& suppress", + "Proposal const& proposal" + ], + "lineno": 15, + "name": "RCLCxPeerPos" + } + ], + "description": "Defines the RCLCxPeerPos class, representing a peer's signed consensus proposal in the XRPL consensus process, including signature verification and unique proposal identification.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/consensus/RCLCxPeerPos.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "checkSign" + }, + { + "args": [], + "lineno": 48, + "name": "signature" + }, + { + "args": [], + "lineno": 54, + "name": "publicKey" + }, + { + "args": [], + "lineno": 60, + "name": "suppressionID" + }, + { + "args": [], + "lineno": 66, + "name": "proposal" + }, + { + "args": [], + "lineno": 71, + "name": "getJson" + }, + { + "args": [], + "lineno": 74, + "name": "render" + }, + { + "args": [ + "Hasher& h" + ], + "lineno": 80, + "name": "hash_append" + }, + { + "args": [ + "uint256 const& proposeHash", + "uint256 const& previousLedger", + "std::uint32_t proposeSeq", + "NetClock::time_point closeTime", + "Slice const& publicKey", + "Slice const& signature" + ], + "lineno": 97, + "name": "proposalUniqueId" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCxPeerPos.h.ai.md b/src/xrpld/app/consensus/RCLCxPeerPos.h.ai.md new file mode 100644 index 0000000000..a0bc6cc40d --- /dev/null +++ b/src/xrpld/app/consensus/RCLCxPeerPos.h.ai.md @@ -0,0 +1,49 @@ +# RCLCxPeerPos.h — Signed Peer Proposal for RCL Consensus + +`RCLCxPeerPos` is the network-layer wrapper for a consensus proposal in the XRP Ledger. It takes the generic `ConsensusProposal` template from the protocol-layer consensus engine and binds it to XRPL's concrete types, then augments it with the cryptographic material — public key, signature, and suppression ID — required for secure peer-to-peer propagation. + +## Role in the Consensus Stack + +The XRPL consensus process is split into two layers. The generic `Consensus<>` engine in `src/xrpld/consensus/` operates on abstract types. The `RCL`-prefixed types in `src/xrpld/app/consensus/` are the concrete instantiations that talk to the live network. `RCLCxPeerPos` sits at the boundary: it is the object passed into `consensus_.peerProposal()` and the object serialized onto the wire by `RCLConsensus::Adaptor::share()`. Every proposal that a validator broadcasts or receives is an instance of this class. + +The `Proposal` type alias concretizes the template parameters: + +```cpp +using Proposal = ConsensusProposal; +``` + +`NodeID` identifies the originating validator node, the first `uint256` is the hash of the prior ledger the proposal builds upon, and the second `uint256` is the hash of the candidate transaction set being proposed. + +## Signature Storage Design + +The signature is stored as `boost::container::static_vector` rather than a `std::vector`. This is a deliberate performance choice: DER-encoded ECDSA/secp256k1 signatures have a well-known maximum size of 72 bytes. The `static_vector` lives entirely on the stack within the object itself, eliminating a heap allocation for every received proposal. The constructor guards this with both an `XRPL_ASSERT` and a runtime check before calling `assign`, ensuring a malformed oversized signature never causes undefined behaviour, even if the assertion is compiled out. + +## Signing Hash and Verification + +`checkSign()` delegates to `verifyDigest(publicKey(), proposal_.signingHash(), signature(), false)`. The `signingHash()` is computed lazily and cached inside `ConsensusProposal` via a `mutable std::optional`. The hash is a `sha512Half` over the `HashPrefix::proposal` discriminator (`"PRP\0"`), the sequence number, close time, previous ledger hash, and the proposed transaction-set hash. Importantly, `changePosition()` and `bowOut()` reset `signingHash_` when fields change, keeping it consistent. + +The private `hash_append()` method inside `RCLCxPeerPos` mirrors exactly those same fields. This makes the class participate in generic hashing algorithms using the `beast::hash_append` protocol, but it is separate from and should not be confused with the signing hash itself. + +## Suppression ID and Duplicate Filtering + +The `suppressionID()` is a `uint256` computed by the free function `proposalUniqueId()` at the point where the local node originally receives or creates a proposal. The function serializes all proposal fields — position, previous ledger, sequence, close time — together with the raw public key and signature bytes, then returns the SHA512-half of the concatenation: + +```cpp +uint256 proposalUniqueId( + uint256 const& proposeHash, + uint256 const& previousLedger, + std::uint32_t proposeSeq, + NetClock::time_point closeTime, + Slice const& publicKey, + Slice const& signature); +``` + +This ID is passed directly to the hash router (`app_.getOverlay().relay(prop, peerPos.suppressionID(), ...)`) so that a proposal echoed back from a peer is recognized and dropped without re-processing. Incorporating the signature into the suppression ID means that even if two proposals had identical logical content, a corrupted or distinct signature would still produce a unique ID and not be incorrectly suppressed. + +## The "Omitted Previous Ledger" Edge Case + +The comment on `proposalUniqueId` documents an important protocol subtlety: the `previousLedger` field may legitimately be absent when a proposal is first transmitted. The signer computes `signingHash()` as if the field is present (using all-zeroes if absent), and recipients inject the actual last-closed-ledger value they know about before calling `checkSign()`. This tolerates the case where a node sends a proposal before it has confirmed which ledger is closing, while still allowing recipients to verify authorship once they have that context. + +## Relationship to `RCLConsensus` + +In `RCLConsensus::Adaptor::propose()`, when the local node crafts its own outbound proposal, it calls `proposalUniqueId()` to build the suppression ID before constructing the `RCLCxPeerPos`. In `RCLConsensus::peerProposal()`, inbound proposals arrive already wrapped as `RCLCxPeerPos` objects (constructed upstream in the overlay/peer message handler) and are forwarded straight into the consensus engine. `getJson()` adds a `peer_id` field (the Base58-encoded node public key) on top of `ConsensusProposal::getJson()`, which is used for RPC introspection and logging. \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCxTx.h.ai.json b/src/xrpld/app/consensus/RCLCxTx.h.ai.json new file mode 100644 index 0000000000..3dadc0d2b3 --- /dev/null +++ b/src/xrpld/app/consensus/RCLCxTx.h.ai.json @@ -0,0 +1,147 @@ +{ + "args": [ + { + "lineno": 17, + "name": "txn" + }, + { + "lineno": 44, + "name": "src" + }, + { + "lineno": 51, + "name": "t" + }, + { + "lineno": 59, + "name": "entry" + }, + { + "lineno": 71, + "name": "m" + }, + { + "lineno": 78, + "name": "m" + }, + { + "lineno": 86, + "name": "entry" + }, + { + "lineno": 97, + "name": "entry" + }, + { + "lineno": 117, + "name": "j" + } + ], + "classes": [ + { + "args": [ + "txn" + ], + "lineno": 13, + "name": "RCLCxTx" + }, + { + "args": [ + "m" + ], + "lineno": 34, + "name": "RCLTxSet" + }, + { + "args": [ + "src" + ], + "lineno": 41, + "name": "MutableTxSet" + } + ], + "description": "Defines RCLCxTx and RCLTxSet, thin wrappers over SHAMapItem and SHAMap, representing transactions and transaction sets in RCLConsensus for XRPL.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/consensus/RCLCxTx.h", + "functions": [ + { + "args": [ + "txn" + ], + "lineno": 17, + "name": "RCLCxTx" + }, + { + "args": [], + "lineno": 23, + "name": "id" + }, + { + "args": [ + "src" + ], + "lineno": 44, + "name": "MutableTxSet" + }, + { + "args": [ + "t" + ], + "lineno": 51, + "name": "insert" + }, + { + "args": [ + "entry" + ], + "lineno": 59, + "name": "erase" + }, + { + "args": [ + "m" + ], + "lineno": 71, + "name": "RCLTxSet" + }, + { + "args": [ + "m" + ], + "lineno": 78, + "name": "RCLTxSet" + }, + { + "args": [ + "entry" + ], + "lineno": 86, + "name": "exists" + }, + { + "args": [ + "entry" + ], + "lineno": 97, + "name": "find" + }, + { + "args": [], + "lineno": 110, + "name": "id" + }, + { + "args": [ + "j" + ], + "lineno": 117, + "name": "compare" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLCxTx.h.ai.md b/src/xrpld/app/consensus/RCLCxTx.h.ai.md new file mode 100644 index 0000000000..36ce3763b4 --- /dev/null +++ b/src/xrpld/app/consensus/RCLCxTx.h.ai.md @@ -0,0 +1,33 @@ +# `RCLCxTx.h` — Transaction Adapter Types for RCL Consensus + +This header defines the two adapter types — `RCLCxTx` and `RCLTxSet` — that bridge the XRP Ledger's `SHAMap`-based transaction storage to the generic `Consensus` template engine. It is part of the glue layer in `src/xrpld/app/consensus/` whose sole purpose, as its README states, is "connecting the generic consensus algorithm to the xrpld-specific instance of consensus." + +## The Adapter Contract + +The generic `Consensus` template (found in `src/xrpld/consensus/Consensus.h`) is policy-based: it drives the consensus round but knows nothing about ledgers, nodes, or serialization. It derives its transaction set type as `Adaptor::TxSet_t` and then requires that type to expose a specific structural contract — a nested `Tx` type, a nested `MutableTxSet` class, an `ID` alias, and the methods `exists()`, `find()`, `id()`, and `compare()`. `RCLCxTx` and `RCLTxSet` together satisfy that contract for the live XRP Ledger Consensus (RCL), with `RCLConsensus::Adaptor` declaring `using TxSet_t = RCLTxSet`. + +## `RCLCxTx`: A Transaction Handle + +`RCLCxTx` is a minimal value type wrapping a `boost::intrusive_ptr`. It adds nothing except the `ID` type alias (`uint256`) and the `id()` accessor that delegates to `SHAMapItem::key()`. The intrusive pointer's reference counting is baked into `SHAMapItem` itself, making copies cheap compared to `shared_ptr`. This class is what `DisputedTx` wraps during intra-round disagreements over which transactions belong in the next ledger. + +## `RCLTxSet`: An Immutable Transaction Set View + +`RCLTxSet` wraps a `std::shared_ptr` and treats it as an immutable, content-addressed snapshot of the proposed transaction set. Its `id()` method returns `map_->getHash().as_uint256()`, which is the Merkle root of all included transactions — the value that validators exchange as their position during consensus voting. + +### `find()` Returns a Pointer, Not a `Tx` + +The most deliberate API quirk is `find()`: it returns `boost::intrusive_ptr` rather than an `RCLCxTx`. The in-code comment explains why — `RCLCxTx` cannot represent absence (it holds a non-nullable intrusive pointer), so returning a nullable pointer lets the generic consensus layer distinguish "found" from "not found" using standard pointer semantics before constructing a `Tx` value. This is a consequence of designing `RCLCxTx` as a non-optional handle rather than forcing optional semantics into the core type. + +### Bounded `compare()` for Safety + +`compare()` computes the symmetric difference between two transaction sets by delegating to `SHAMap::compare()` and translating the resulting `Delta` map into a `std::map`, where `true` means the transaction is present in `this` set. The call caps the comparison at 65,536 differences — a hard limit chosen to bound the CPU work a malicious or corrupted validator set could force on a node. The `XRPL_ASSERT` inside the loop enforces that any entry in the delta is exclusive to exactly one side, which is guaranteed by `SHAMap::compare()` semantics. + +## `MutableTxSet`: Copy-on-Write Mutation + +`MutableTxSet` is a nested friend class that provides the transient mutable phase when the consensus engine needs to adjust a transaction set (inserting or erasing individual transactions during dispute resolution). Its constructor calls `src.map_->snapShot(true)` — SHAMap's copy-on-write mechanism — cloning only the tree path nodes that change. Converting back to an immutable `RCLTxSet` via `RCLTxSet(MutableTxSet const&)` calls `snapShot(false)`, producing a new immutable view with a freshly computed Merkle root. This pattern means mutation never touches the original set, preserving referential integrity for other parts of the system that hold references to the prior snapshot. + +The `insert()` method uses `SHAMapNodeType::tnTRANSACTION_NM` (non-metadata transaction node type), which is the correct leaf type for unsigned transaction entries in the SHAMap tree. + +## Relationship to the Broader Consensus Layer + +`RCLCxTx.h` is intentionally thin — it contains no application logic, no logging, and no network I/O. All of that lives in `RCLConsensus.cpp`, which uses these types to acquire transaction sets from peers (`acquireTxSet`), to apply the agreed set to the open ledger, and to share individual transactions (`share(RCLCxTx const&)`). The separation keeps the generic consensus algorithm's data model clean and testable independently of the full application stack. \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLValidations.cpp.ai.json b/src/xrpld/app/consensus/RCLValidations.cpp.ai.json new file mode 100644 index 0000000000..7867d9e727 --- /dev/null +++ b/src/xrpld/app/consensus/RCLValidations.cpp.ai.json @@ -0,0 +1,327 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RCLValidatedLedger::RCLValidatedLedger", + "ledger->read(keylet::skip())", + "XRPL_ASSERT(hashIndex->getFieldU32(sfLastLedgerSequence) == (seq() - 1))" + ], + "entry_point": "RCLValidatedLedger::RCLValidatedLedger(std::shared_ptr const&, beast::Journal)", + "purpose": "Constructs a validated ledger object from a Ledger, extracting ancestor hashes and validating last ledger sequence.", + "validation_points": [ + "XRPL_ASSERT on sfLastLedgerSequence" + ] + }, + { + "call_chain": [ + "RCLValidatedLedger::operator[]", + "if (s >= minSeq() && s <= seq())", + "return ancestors_[...] or ledgerID_" + ], + "entry_point": "RCLValidatedLedger::operator[]", + "purpose": "Retrieves the hash for a given ancestor sequence, validating that the sequence is in range.", + "validation_points": [ + "if (s >= minSeq() && s <= seq())" + ] + }, + { + "call_chain": [ + "RCLValidationsAdaptor::acquire", + "app_.getLedgerMaster().getLedgerByHash(hash)", + "XRPL_ASSERT(!ledger->open() && ledger->isImmutable())", + "XRPL_ASSERT(ledger->header().hash == hash)" + ], + "entry_point": "RCLValidationsAdaptor::acquire", + "purpose": "Acquires a validated ledger by hash, ensuring it is closed, immutable, and matches the requested hash.", + "validation_points": [ + "XRPL_ASSERT on ledger state", + "XRPL_ASSERT on hash match" + ] + } + ], + "data_flows": [ + { + "field": "sfLastLedgerSequence", + "flow": [ + "Ledger::read(keylet::skip())", + "hashIndex->getFieldU32(sfLastLedgerSequence)", + "XRPL_ASSERT(hashIndex->getFieldU32(sfLastLedgerSequence) == (seq() - 1))" + ], + "origin": "hashIndex->getFieldU32(sfLastLedgerSequence)", + "transformations": [ + "Extracted from hashIndex", + "Compared to (seq() - 1)" + ], + "validated_at": "RCLValidatedLedger::RCLValidatedLedger (XRPL_ASSERT)" + }, + { + "field": "ancestor sequence (s)", + "flow": [ + "operator[] parameter", + "if (s >= minSeq() && s <= seq())", + "return ancestors_[...] or ledgerID_" + ], + "origin": "Input to RCLValidatedLedger::operator[]", + "transformations": [ + "Checked for range validity", + "Used as index into ancestors_ or returns ledgerID_" + ], + "validated_at": "RCLValidatedLedger::operator[] (if condition)" + }, + { + "field": "ledger", + "flow": [ + "getLedgerByHash(hash)", + "XRPL_ASSERT(!ledger->open() && ledger->isImmutable())", + "XRPL_ASSERT(ledger->header().hash == hash)", + "return RCLValidatedLedger(ledger, j_)" + ], + "origin": "app_.getLedgerMaster().getLedgerByHash(hash)", + "transformations": [ + "Checked for open/immutable state", + "Checked for hash match" + ], + "validated_at": "RCLValidationsAdaptor::acquire (XRPL_ASSERTs)" + }, + { + "field": "ancestors_", + "flow": [ + "Ledger::read(keylet::skip())", + "hashIndex->getFieldV256(sfHashes).value()", + "ancestors_ = ...", + "Used in operator[] and mismatch" + ], + "origin": "hashIndex->getFieldV256(sfHashes).value()", + "transformations": [ + "Extracted from ledger", + "Stored in RCLValidatedLedger", + "Indexed for ancestor hash lookup" + ], + "validated_at": "Indirectly via sfLastLedgerSequence validation" + } + ], + "description": "Implements logic for handling validated ledgers and validations in the XRPL consensus process, including ledger ancestry tracking, validation acquisition, and validation handling.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLastLedgerSequence", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at RCLValidatedLedger::RCLValidatedLedger(shared_ptr const&, beast::Journal)", + "issue_pattern": "Missing empty string validation for sfLastLedgerSequence", + "why_false_positive": "XRPL_ASSERT macro validates sfLastLedgerSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ancestor sequence range", + "empty", + "string", + "validation" + ], + "evidence": "if (s >= minSeq() && s <= seq()) at RCLValidatedLedger::operator[](Seq const& s) const", + "issue_pattern": "Missing empty string validation for ancestor sequence range", + "why_false_positive": "if (s >= minSeq() && s <= seq()) validates ancestor sequence range for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "ancestor sequence range", + "range", + "bounds", + "validation" + ], + "evidence": "if (s >= minSeq() && s <= seq()) at RCLValidatedLedger::operator[](Seq const& s) const", + "issue_pattern": "Missing range validation for ancestor sequence range", + "why_false_positive": "if (s >= minSeq() && s <= seq()) validates ancestor sequence range range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/consensus/RCLValidations.cpp", + "functions": [ + { + "args": [ + "MakeGenesis" + ], + "lineno": 10, + "name": "RCLValidatedLedger::RCLValidatedLedger" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "beast::Journal j" + ], + "lineno": 15, + "name": "RCLValidatedLedger::RCLValidatedLedger" + }, + { + "args": [], + "lineno": 32, + "name": "RCLValidatedLedger::minSeq" + }, + { + "args": [], + "lineno": 37, + "name": "RCLValidatedLedger::seq" + }, + { + "args": [], + "lineno": 41, + "name": "RCLValidatedLedger::id" + }, + { + "args": [ + "Seq const& s" + ], + "lineno": 45, + "name": "RCLValidatedLedger::operator[]" + }, + { + "args": [ + "RCLValidatedLedger const& a", + "RCLValidatedLedger const& b" + ], + "lineno": 61, + "name": "mismatch" + }, + { + "args": [ + "Application& app", + "beast::Journal j" + ], + "lineno": 80, + "name": "RCLValidationsAdaptor::RCLValidationsAdaptor" + }, + { + "args": [], + "lineno": 84, + "name": "RCLValidationsAdaptor::now" + }, + { + "args": [ + "LedgerHash const& hash" + ], + "lineno": 89, + "name": "RCLValidationsAdaptor::acquire" + }, + { + "args": [ + "Application& app", + "std::shared_ptr const& val", + "std::string const& source", + "BypassAccept const bypassAccept", + "std::optional j" + ], + "lineno": 117, + "name": "handleNewValidation" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is core consensus/validation logic. Direct unit tests for RCLValidatedLedger and RCLValidationsAdaptor are likely in test files such as 'test/consensus/Validations_test.cpp', 'test/ledger/Ledger_test.cpp', or similar. However, the XRPL_ASSERT on sfLastLedgerSequence and the ancestor sequence range check may not be directly tested for all edge cases (e.g., missing or malformed skip list, out-of-range ancestor queries). Integration tests may cover normal flows, but explicit negative/edge case coverage (e.g., missing ancestors, bad ledger state) should be reviewed. The async job path in acquire (when ledger is missing) is likely only covered in integration or network tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro, manual range checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "sfLastLedgerSequence", + "location": "RCLValidatedLedger::RCLValidatedLedger(shared_ptr const&, beast::Journal)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that hashIndex->getFieldU32(sfLastLedgerSequence) == (seq() - 1)", + "Ensures the last ledger sequence in the skip list matches the expected previous sequence" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (logs warning, returns default ID{0})", + "field": "ancestor sequence range", + "location": "RCLValidatedLedger::operator[](Seq const& s) const", + "validated_by": "if (s >= minSeq() && s <= seq())", + "validates": [ + "Checks that requested ancestor sequence is within available range" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLValidations.cpp.ai.md b/src/xrpld/app/consensus/RCLValidations.cpp.ai.md new file mode 100644 index 0000000000..e17169e034 --- /dev/null +++ b/src/xrpld/app/consensus/RCLValidations.cpp.ai.md @@ -0,0 +1,35 @@ +# RCLValidations.cpp + +This file provides the XRPL-specific (RCL — Ripple Consensus Ledger) concrete implementations that wire the generic, protocol-agnostic `Validations<>` template into the running `xrpld` application. It defines three interlocking components: a ledger ancestry abstraction (`RCLValidatedLedger`), the adaptor bridging generic validation machinery to XRPL's application layer (`RCLValidationsAdaptor`), and the central intake function for incoming validations (`handleNewValidation`). + +## RCLValidatedLedger: Ancestry Without Loading History + +The generic `LedgerTrie` used by `Validations<>` needs to answer one question for any two candidate ledgers: do they share a common ancestor, and if so, at what sequence did they diverge? `RCLValidatedLedger` answers this by exploiting the skip list embedded in every closed XRPL ledger — the `sfHashes` field under `keylet::skip()`, which stores up to 256 recent ancestor hashes in sequence order. + +On construction from a real `Ledger`, `RCLValidatedLedger` reads this skip list and stashes it in `ancestors_`. The `operator[](Seq s)` method then provides O(1) lookup of any ancestor hash within the available window (`minSeq()` to `seq()`). If the requested sequence falls outside the window — meaning more than 256 ledgers separate the query point from the known history — the method returns `ID{0}`, a sentinel that the trie interprets as "unknown/distinct." This is a deliberate design choice: rather than attempting expensive ledger fetches just to determine ancestry, the system conservatively treats widely separated ledgers as unrelated. + +The `MakeGenesis` constructor tag produces a zero-sequence, zero-hash genesis placeholder used to root the trie without any real ledger data. + +The `mismatch()` free function (a required interface for the generic trie) finds the earliest sequence at which two ledger chains diverge. It computes the overlap window between the two ledgers' known ancestor ranges, then walks backward from the overlap ceiling until it finds a sequence where both report the same hash. If the entire window disagrees, it falls back to `Seq{1}` — one after genesis — reflecting the worst-case assumption that the chains forked as early as possible. + +## RCLValidationsAdaptor: Bridging Generic and Concrete + +`RCLValidationsAdaptor` satisfies the adaptor concept required by `Validations`. It specifies `std::mutex` for thread safety, `RCLValidation` as the validation type, and `RCLValidatedLedger` as the ledger type. `RCLValidations` is then just a type alias for `Validations`. + +The `acquire()` method handles the case where the generic validation machinery needs to actually load a ledger (e.g., when building the trie after receiving validations for a ledger the node doesn't have locally). It first tries `LedgerMaster::getLedgerByHash()` wrapped in a `perf::measureDurationAndLog` call to surface unexpectedly slow lookups. If the ledger isn't found, rather than blocking or failing, `acquire()` fires an async job via `JobQueue` with type `jtADVANCE` that calls `InboundLedgers::acquireAsync()` — the normal mechanism for fetching missing ledgers from peers. The function returns `std::nullopt` immediately, and the validation machinery will retry the lookup when the ledger eventually arrives. This design keeps the critical consensus path non-blocking: the adaptor is a thin shim, and all real I/O is deferred to the job queue. + +The `now()` method delegates to `TimeKeeper::closeTime()`, returning the network-adjusted clock time used to determine whether validations are current or stale. + +## handleNewValidation: Trust Resolution and Intake + +`handleNewValidation` is the single entry point for all validations, whether locally produced or received from peers. It performs three distinct operations in sequence. + +**Trust resolution.** The validation arrives carrying only a signing key. The function queries `ValidatorList::getTrustedKey()` to see whether that signing key maps to a trusted master key. If so, it marks the `STValidation` as trusted. This step exists because validators may rotate their ephemeral signing keys; what matters for trust is the master identity, not the ephemeral key used to sign a particular message. If the signing key isn't trusted, the function also checks `getListedKey()` — unlisted validators still get tracked in the `Validations<>` state, which is important for monitoring. The `calcNodeID(masterKey.value_or(signingKey))` call normalizes both cases into a stable `NodeID` before passing the validation to `Validations<>::add()`. + +**Acceptance triggering.** When `add()` returns `ValStatus::current` and the validation is trusted, the function calls `LedgerMaster::checkAccept()` with the validated ledger's hash and sequence. This is the mechanism by which accumulating trusted validations can push the node to declare a ledger fully validated. The `BypassAccept::yes` path skips this call and is used when replaying validations during startup catch-up to avoid spurious or redundant accept checks. + +**Byzantine behavior logging.** Validations that fail with `ValStatus::conflicting` (same validator, same sequence, different ledger hash — or different sign times) or `ValStatus::multiple` (same validator, same sequence and hash, but different cookies) are logged with a "Byzantine Behavior Detector" prefix. Trusted-validator violations are logged at ERROR; unlisted-validator violations at INFO. The code explicitly chooses *not* to suppress forwarding of these problematic validations. As the comment notes, this seems counterintuitive but is intentional: peers need to independently observe Byzantine validators so their operators can react. Suppressing the evidence would undermine the network's ability to detect and respond to misbehavior. + +## Invariants and Error Handling + +The `XRPL_ASSERT` in the `RCLValidatedLedger` constructor checks that `sfLastLedgerSequence` in the skip list equals `seq() - 1`, catching any corruption in the ledger's own metadata. The assert in `RCLValidationsAdaptor::acquire()` verifies that a successfully retrieved ledger is both closed (not `open()`) and immutable — a double check against receiving a mutable working ledger by mistake. Out-of-range ancestor lookups in `operator[]` degrade gracefully with a warning log and return `ID{0}`, never accessing the `ancestors_` vector out of bounds. \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLValidations.h.ai.json b/src/xrpld/app/consensus/RCLValidations.h.ai.json new file mode 100644 index 0000000000..519b981ad6 --- /dev/null +++ b/src/xrpld/app/consensus/RCLValidations.h.ai.json @@ -0,0 +1,96 @@ +{ + "args": [ + { + "lineno": 31, + "name": "v" + }, + { + "lineno": 87, + "name": "ledger" + }, + { + "lineno": 87, + "name": "j" + }, + { + "lineno": 113, + "name": "app" + }, + { + "lineno": 113, + "name": "j" + }, + { + "lineno": 124, + "name": "id" + }, + { + "lineno": 139, + "name": "app" + }, + { + "lineno": 140, + "name": "val" + }, + { + "lineno": 141, + "name": "source" + }, + { + "lineno": 142, + "name": "bypassAccept" + }, + { + "lineno": 143, + "name": "j" + } + ], + "classes": [ + { + "args": [ + "v" + ], + "lineno": 19, + "name": "RCLValidation" + }, + { + "args": [ + "MakeGenesis", + "ledger", + "j" + ], + "lineno": 74, + "name": "RCLValidatedLedger" + }, + { + "args": [ + "app", + "j" + ], + "lineno": 110, + "name": "RCLValidationsAdaptor" + } + ], + "description": "This file provides wrapper classes and adaptors for handling validations in the XRPL consensus process, including RCLValidation (a wrapper over STValidation), RCLValidatedLedger (a wrapper for ledgers for use in validation trie structures), and RCLValidationsAdaptor (an adaptor for managing validations and ledger acquisition). It also defines utility functions and types for managing and processing validations.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/consensus/RCLValidations.h", + "functions": [ + { + "args": [ + "app", + "val", + "source", + "bypassAccept", + "j" + ], + "lineno": 137, + "name": "handleNewValidation" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/consensus/RCLValidations.h.ai.md b/src/xrpld/app/consensus/RCLValidations.h.ai.md new file mode 100644 index 0000000000..fb4be8c390 --- /dev/null +++ b/src/xrpld/app/consensus/RCLValidations.h.ai.md @@ -0,0 +1,53 @@ +# RCLValidations.h — XRP Ledger Consensus Validation Adaptor Layer + +This file is the glue between two separate abstraction levels in rippled's consensus architecture: the protocol-level `STValidation` serialized object and the generic, policy-parameterized `Validations<>` template engine. Rather than coupling the generic template directly to XRPL types, the design uses a classic adaptor pattern: three lightweight types defined here satisfy the concept interface expected by the template, and a `using` alias collapses the whole thing into `RCLValidations`. + +## The Adapter Pattern in Context + +The generic `Validations` template in `src/xrpld/consensus/Validations.h` is deliberately XRPL-agnostic. It accepts any adaptor that provides `Mutex`, `Validation`, and `Ledger` type members plus a handful of methods. This allows simulation and testing code to swap in lightweight fakes. The three classes in this file are the production concrete types for the XRP Ledger Consensus (RCL) layer. + +## `RCLValidation` — Protocol Message Wrapper + +`RCLValidation` wraps a `std::shared_ptr` and exposes the uniform interface that `Validations<>` expects. `STValidation` is a full serialized protocol object — it understands signing, serialization, and field access via the STObject machinery. `RCLValidation` hides all of that behind simple accessors: `ledgerID()`, `seq()`, `signTime()`, `seenTime()`, `key()`, `nodeID()`, `trusted()`, `full()`, and `loadFee()`. + +The distinction between `key()` (the ephemeral signing key) and `nodeID()` (derived from the validator's master key via the manifest system) is significant for validators that rotate their signing keys. The generic validation code uses `NodeID` to track the same logical validator across key rotations. + +`loadFee()` returning `std::optional` signals that not every validation carries a fee vote — partial validations in particular may omit it. The `cookie()` accessor provides a per-restart entropy value used to detect duplicate validations from a restarted validator without requiring persistent state. + +## `RCLValidatedLedger` — Ancestor-Aware Ledger View + +`RCLValidatedLedger` wraps an immutable `Ledger` for use in the `LedgerTrie`. The trie models ledger history as a sequence-to-hash mapping: any two ledgers sharing the same hash at a given sequence share the same complete ancestry up to that point. The trie uses this to determine which ledger branch the network is converging on. + +The XRPL ledger structure stores up to 256 prior ancestor hashes in the skip list (`keylet::skip()`, field `sfHashes`). `RCLValidatedLedger` copies these into a `std::vector` at construction time, enabling the `operator[](Seq)` lookup. When the requested ancestor falls outside the `[minSeq(), seq()]` window — meaning it's more than 256 ledgers back — the operator returns `ID{0}`, which the trie treats as a distinct leaf, effectively forcing the two ledgers to be considered unrelated. This is a deliberate conservative choice: the code cannot prove a common ancestor, so it assumes divergence. + +The `MakeGenesis` constructor tag creates a null ledger at sequence 0, used as the universal root of the trie where all chains eventually converge. + +The `mismatch()` free function (declared as a friend to make the symmetry between the two operands clear) walks the overlapping sequence interval backward until it finds the first matching ancestor hash. If the entire overlapping interval mismatches, it returns sequence 1 — the earliest possible divergence point beyond the genesis ledger. This is used by the trie to insert ledgers at the correct branch point. + +## `RCLValidationsAdaptor` — XRPL-Specific Policy + +`RCLValidationsAdaptor` satisfies the adaptor concept for `Validations<>` by providing the `now()` time source (from `Application::getTimeKeeper().closeTime()`) and the `acquire()` method. These are the only two points where XRPL-specific infrastructure bleeds into the otherwise generic validation machinery. + +`acquire()` first checks whether the `LedgerMaster` already has the ledger cached. If it does, it returns an `RCLValidatedLedger` immediately. If it doesn't, it posts a `jtADVANCE` job to the `JobQueue` that calls `InboundLedgers::acquireAsync()` — a non-blocking peer fetch. Critically, `acquire()` returns `std::nullopt` in the miss case rather than blocking. The generic `Validations<>` template handles missing ledgers gracefully by deferring trie insertions until the ledger becomes available. + +The performance instrumentation on `getLedgerByHash` (via `perf::measureDurationAndLog` with a 10ms threshold) reflects the operational concern that slow ledger lookups during validation processing could stall consensus; slow calls are logged for operational visibility. + +## `handleNewValidation()` — Entry Point and Byzantine Detection + +`handleNewValidation()` is the single function called by the network layer when a new validation arrives. It performs three distinct jobs. + +First, it determines trust status by consulting the `ValidatorList`. If the signing key maps to a currently trusted master key, the validation is marked trusted before being added. If the key isn't trusted but is listed (known but not currently on the UNL), the master key is still resolved for the purpose of identity continuity — ensuring the trie correctly attributes validations across key rotations. + +Second, it calls `validations.add()` using the resolved `NodeID`, which either admits the validation as current or rejects it with a `ValStatus` indicating why. Only `ValStatus::current` validations from trusted validators proceed to `LedgerMaster::checkAccept()`, which re-evaluates whether the node can advance its validated ledger given the new vote tally. + +Third, and architecturally notable, validations with `ValStatus::conflicting` or `ValStatus::multiple` are logged via the "Byzantine Behavior Detector" path — error-level for trusted validators, info-level for untrusted — but are **not suppressed from relay**. The comment in the source explains the reasoning: the node should forward these anomalous validations precisely because peers need to independently observe the same misbehavior to alert their operators. Silencing them would undermine the distributed detection mechanism. + +The `BypassAccept` parameter allows callers to skip the `checkAccept` call on current trusted validations. This is used in scenarios like historical replay, where advancing the validated ledger would be incorrect even though the validation itself is valid. + +## Type Alias + +```cpp +using RCLValidations = Validations; +``` + +This single line is the payoff of the entire file. All of the `Validations<>` template machinery — the `LedgerTrie`, the `SeqEnforcer` per-validator monotonicity check, the aged expiry containers, the trust accounting — is instantiated against these three concrete XRPL types. The rest of the application uses `RCLValidations` directly through `Application::getValidations()`. \ No newline at end of file diff --git a/src/xrpld/app/ledger/AbstractFetchPackContainer.h.ai.json b/src/xrpld/app/ledger/AbstractFetchPackContainer.h.ai.json new file mode 100644 index 0000000000..ee9f183005 --- /dev/null +++ b/src/xrpld/app/ledger/AbstractFetchPackContainer.h.ai.json @@ -0,0 +1,33 @@ +{ + "args": [ + { + "lineno": 19, + "name": "nodeHash" + } + ], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "AbstractFetchPackContainer" + } + ], + "description": "Defines an abstract interface for retrieving fetch packs (partial ledger data) by hash, without requiring an application or ledgermaster object, in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/AbstractFetchPackContainer.h", + "functions": [ + { + "args": [ + "nodeHash" + ], + "lineno": 18, + "name": "getFetchPack" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/AbstractFetchPackContainer.h.ai.md b/src/xrpld/app/ledger/AbstractFetchPackContainer.h.ai.md new file mode 100644 index 0000000000..480aa11786 --- /dev/null +++ b/src/xrpld/app/ledger/AbstractFetchPackContainer.h.ai.md @@ -0,0 +1,27 @@ +# `AbstractFetchPackContainer.h` + +## Role + +`AbstractFetchPackContainer` is a narrow dependency-inversion interface that exposes a single capability: looking up cached fetch-pack blobs by their `uint256` hash. Its stated purpose — "without an application or ledgermaster object" — explains exactly why it exists. Without this interface, `AccountStateSF` and `TransactionStateSF` would need to hold a reference to the heavyweight `LedgerMaster` (or the full `Application`) just to perform one cache lookup during ledger sync. Instead, they depend only on this minimal abstract type. + +## What Fetch Packs Are + +During historical ledger acquisition, a catching-up node sends peers a `TMGetObjectByHash` request of type `otFETCH_PACK`. Peers respond with batches of SHAMap nodes needed to reconstruct the missing ledger. `LedgerMaster` stores these blobs in a tagged cache (`fetch_packs_`) keyed by the node's `uint256` content hash. The single method `getFetchPack()` is the only externally visible access point to that cache. + +## Interface Design + +The interface contains one pure virtual method: + +```cpp +virtual std::optional getFetchPack(uint256 const& nodeHash) = 0; +``` + +It returns `std::nullopt` on a cache miss and the raw node data on a hit. The implementation in `LedgerMaster::getFetchPack()` does more than a plain cache lookup — it removes the entry after retrieval (fetch packs are consumed, not reread) and validates data integrity by recomputing `sha512Half` over the blob, discarding and returning `nullopt` if the hash does not match. + +## Consumers + +Both `AccountStateSF` and `TransactionStateSF` — the `SHAMapSyncFilter` implementations for account-state and transaction trees respectively — accept an `AbstractFetchPackContainer&` at construction and delegate their `getNode()` callbacks directly to it. `SHAMapSyncFilter::getNode()` is called by the SHAMap sync machinery whenever it needs a node that is not already in memory; checking the fetch-pack cache first avoids an unnecessary network round-trip since peers may have pre-populated it. `InboundLedger` bypasses this interface and calls `getLedgerMaster().getFetchPack()` directly, because it already holds a reference to the full application context. + +## Why This Abstraction + +The separation keeps sync-filter classes testable in isolation: a test double implementing `AbstractFetchPackContainer` can inject arbitrary node data without constructing a live `LedgerMaster`. It also enforces a clear ownership boundary — sync filters are concerned only with *reading* cached blobs, never with how those blobs were fetched or stored. \ No newline at end of file diff --git a/src/xrpld/app/ledger/AcceptedLedger.cpp.ai.json b/src/xrpld/app/ledger/AcceptedLedger.cpp.ai.json new file mode 100644 index 0000000000..577c18b792 --- /dev/null +++ b/src/xrpld/app/ledger/AcceptedLedger.cpp.ai.json @@ -0,0 +1,149 @@ +{ + "args": [ + { + "lineno": 6, + "name": "ledger" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const& ledger" + ], + "lineno": 5, + "name": "AcceptedLedger" + } + ], + "code_paths": [ + { + "call_chain": [ + "AcceptedLedger::AcceptedLedger", + "insertAll (lambda)", + "AcceptedLedgerTx::AcceptedLedgerTx" + ], + "entry_point": "AcceptedLedger::AcceptedLedger", + "purpose": "Constructs an AcceptedLedger object from a ReadView ledger, extracting all transactions and wrapping them as AcceptedLedgerTx objects.", + "validation_points": [ + "AcceptedLedgerTx::AcceptedLedgerTx (potential validation of transaction data)", + "ReadView::txs (if ReadView validates transaction structure)" + ] + } + ], + "data_flows": [ + { + "field": "ledger", + "flow": [ + "AcceptedLedger::AcceptedLedger parameter", + "insertAll lambda captures ledger", + "AcceptedLedgerTx constructed with ledger" + ], + "origin": "AcceptedLedger constructor parameter (std::shared_ptr const& ledger)", + "transformations": [ + "Passed as shared_ptr", + "Used to access ledger->txs" + ], + "validated_at": "No explicit validation in this code; possible validation in ReadView or AcceptedLedgerTx" + }, + { + "field": "transactions_", + "flow": [ + "Created empty in AcceptedLedger", + "Populated by insertAll with AcceptedLedgerTx objects", + "Sorted by getTxnSeq" + ], + "origin": "AcceptedLedger member variable (std::vector>)", + "transformations": [ + "Reserve space", + "Emplace_back AcceptedLedgerTx", + "Sort by transaction sequence" + ], + "validated_at": "No explicit validation in this code; relies on correctness of ledger->txs and AcceptedLedgerTx" + }, + { + "field": "ledger->txs", + "flow": [ + "Accessed in insertAll", + "Each item passed to AcceptedLedgerTx" + ], + "origin": "ReadView::txs (assumed to be a map or iterable of transaction data)", + "transformations": [ + "Iterated over", + "Each (key, value) pair used to construct AcceptedLedgerTx" + ], + "validated_at": "No explicit validation in this code; possible validation in ReadView or AcceptedLedgerTx" + } + ], + "description": "Implements the AcceptedLedger class, which constructs a list of AcceptedLedgerTx objects from the transactions in a given ledger and sorts them by transaction sequence.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/AcceptedLedger.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is a constructor and does not contain explicit validation logic. Validation (e.g., of transaction data) likely occurs in AcceptedLedgerTx or ReadView. Test coverage would depend on tests for AcceptedLedger, AcceptedLedgerTx, and ReadView. Typical test files might include 'AcceptedLedger_test.cpp', 'AcceptedLedgerTx_test.cpp', or integration tests for ledger acceptance. Gaps: No direct validation or error handling is present in this code; if ledger->txs contains invalid data, it is not checked here.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None", + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/AcceptedLedger.cpp.ai.md b/src/xrpld/app/ledger/AcceptedLedger.cpp.ai.md new file mode 100644 index 0000000000..2b864b560e --- /dev/null +++ b/src/xrpld/app/ledger/AcceptedLedger.cpp.ai.md @@ -0,0 +1,36 @@ +# `AcceptedLedger.cpp` — Post-Consensus Transaction Index for a Closed Ledger + +## Role in the System + +`AcceptedLedger` bridges the raw, immutable ledger state (a `ReadView`) and the downstream consumers that need to iterate transactions in a predictable order. Two subsystems depend on it directly: `NetworkOPs`, which fans out transaction events to WebSocket subscribers after validation, and the relational database backend (`Node.cpp`), which persists transaction records to SQLite. Both follow the same cache-or-construct pattern, checking `app.getAcceptedLedgerCache()` (a `TaggedCache` keyed by ledger hash) before constructing a new instance. + +## What the Constructor Does + +The constructor's job is entirely one-shot transformation: take every transaction from `ledger->txs`, wrap each pair of `(STTx, STObject metadata)` in an `AcceptedLedgerTx`, and sort the resulting vector by transaction sequence. There is no deferred or lazy loading — the entire ledger's transaction set is materialized at construction time and owned for the lifetime of the `AcceptedLedger` object. + +The `transactions_.reserve(256)` call appears twice in the source (lines 9 and 18), which is a harmless redundancy — the second call is a no-op because the vector was already reserved. The 256-slot reservation is a heuristic to avoid reallocation for typical ledger sizes. + +## `AcceptedLedgerTx` — The Wrapped Transaction + +Each element in `transactions_` is an `AcceptedLedgerTx`, defined in `include/xrpl/ledger/AcceptedLedgerTx.h` and implemented in `src/libxrpl/ledger/AcceptedLedgerTx.cpp`. Its constructor eagerly builds several representations from the raw transaction and metadata: + +- A `TxMeta` object derived from the `STObject` metadata blob, capturing the transaction result code, affected nodes, and the transaction's index within the ledger. +- A `flat_set` of all accounts touched by the transaction, derived from `TxMeta::getAffectedAccounts()`, used by `InfoSub` to route events to subscribed clients. +- Pre-serialized JSON combining the transaction, metadata, hex-encoded raw metadata, human-readable result string, and — for `ttOFFER_CREATE` transactions that are not self-funded — the owner's current balance of the `TakerGets` asset (`owner_funds`). +- A raw binary serialization of the metadata blob (`mRawMeta`) for database storage, accessed via `getEscMeta()`. + +The constructor asserts `!ledger->open()`, enforcing that `AcceptedLedgerTx` objects are only created from closed ledger views. This is a correctness invariant: transaction metadata is only meaningful once the ledger is finalized. + +## Sorting by Transaction Sequence + +After all transactions are wrapped, the constructor sorts by `getTxnSeq()`, which returns `mMeta.getIndex()` — the transaction's ordinal position within the closed ledger, not the account-level sequence number on the `STTx`. This ordering guarantees that downstream consumers iterate transactions in the same deterministic order they were applied during consensus, which matters for database writes and for publishing events in a consistent sequence to WebSocket clients. + +## Caching and Ownership Model + +`AcceptedLedger` is owned exclusively via `shared_ptr` and cached by ledger hash. The `AcceptedLedger` itself holds a `shared_ptr` to keep the underlying ledger view alive for the duration of any reference to its transactions. Each `AcceptedLedgerTx` is owned via `unique_ptr` inside the `transactions_` vector, so the vector has sole ownership of the wrapped objects. Callers iterate with `begin()`/`end()` and receive raw `unique_ptr` references — there is no need to copy individual transactions out of the container. + +The `TaggedCache` handles cache eviction automatically based on age and memory pressure, which prevents stale `AcceptedLedger` objects from accumulating for every ledger ever validated. + +## Naming Clarification + +The header includes a `VFALCO TODO` comment that flags a terminology ambiguity in the XRPL codebase: "closed" means the ledger's close time has passed; "accepted" nominally refers to a ledger that passed the consensus round but has not yet accumulated sufficient validations; and "validated" refers to a ledger with full validation quorum. In practice, both `NetworkOPs` and the database backend construct `AcceptedLedger` objects from ledgers that have already been validated, meaning the class name slightly understates the certainty of the ledger state it represents. \ No newline at end of file diff --git a/src/xrpld/app/ledger/AcceptedLedger.h.ai.json b/src/xrpld/app/ledger/AcceptedLedger.h.ai.json new file mode 100644 index 0000000000..1937815181 --- /dev/null +++ b/src/xrpld/app/ledger/AcceptedLedger.h.ai.json @@ -0,0 +1,55 @@ +{ + "args": [ + { + "lineno": 27, + "name": "ledger" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const& ledger" + ], + "lineno": 22, + "name": "AcceptedLedger" + } + ], + "description": "Defines the AcceptedLedger class, representing a ledger that has become irrevocable after passing a validation threshold, and provides access to its transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/AcceptedLedger.h", + "functions": [ + { + "args": [ + "std::shared_ptr const& ledger" + ], + "lineno": 27, + "name": "AcceptedLedger" + }, + { + "args": [], + "lineno": 29, + "name": "getLedger" + }, + { + "args": [], + "lineno": 34, + "name": "size" + }, + { + "args": [], + "lineno": 39, + "name": "begin" + }, + { + "args": [], + "lineno": 44, + "name": "end" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/AcceptedLedger.h.ai.md b/src/xrpld/app/ledger/AcceptedLedger.h.ai.md new file mode 100644 index 0000000000..121e408d2b --- /dev/null +++ b/src/xrpld/app/ledger/AcceptedLedger.h.ai.md @@ -0,0 +1,31 @@ +# `AcceptedLedger.h` — Post-Consensus Ledger Transaction View + +## Role in the System + +`AcceptedLedger` is a lightweight materialization layer sitting between a raw validated ledger and the subsystems that need to iterate its transactions — specifically the network subscription publisher (`NetworkOPs`) and the relational database backend (`Node.cpp`). Its single job is to take a `ReadView const` and produce a sorted, eagerly-constructed sequence of `AcceptedLedgerTx` objects that callers can range-iterate without touching the underlying SHAMap again. + +The header carries an important terminology note. In XRPL parlance the word "accepted" is used loosely: a ledger that is "closed" has passed its close time and admits no new transactions; one that is "accepted" has gone through the consensus process and is believed to be correct, but may not yet have crossed the full validation threshold from a quorum of validators; once it does, it becomes "validated." The comment in the file acknowledges this ambiguity and flags it for cleanup. In practice the class is constructed and cached only for ledgers that have already accumulated sufficient validations, so the distinction is mostly historical. + +## Construction and Transaction Ordering + +The constructor in `AcceptedLedger.cpp` iterates `ledger->txs` — the ordered transaction/metadata pairs stored in the `ReadView` — and wraps each pair in a `std::unique_ptr`. After collecting all transactions it sorts them by `getTxnSeq()`, which maps to the transaction's position index recorded in its `TxMeta`. This sort step is the key reason `AcceptedLedger` exists as a separate object: the underlying `ReadView` stores transactions in SHAMap order (keyed by transaction hash), not in execution order. Downstream consumers — especially those writing to SQL databases or delivering ordered event streams to clients — need a stable, sequence-ordered view. By sorting once at construction time, all subsequent consumers get O(1) random access with no repeated sorting. + +The constructor pre-reserves 256 slots before populating the vector, which amortizes reallocation cost for a typical block of transactions. The reserve call appears twice in the implementation, which is a harmless but redundant duplication. + +## Ownership and Caching + +`AcceptedLedger` holds a `std::shared_ptr` to keep the underlying ledger alive for as long as the accepted view exists. The `transactions_` vector holds `std::unique_ptr`, giving exclusive ownership of the enriched transaction wrappers. This two-tier ownership model means the class itself is always managed via `std::shared_ptr` by callers — and that is exactly the type stored in the application-wide `TaggedCache`. + +Both primary call sites in `NetworkOPs.cpp` and `Node.cpp` follow the same pattern: attempt a cache lookup by ledger hash, and only construct a new `AcceptedLedger` on a miss, then insert it into the cache with `canonicalize_replace_client`. This ensures that when multiple consumers process the same validated ledger (e.g., both publishing to WebSocket subscribers and writing to the database), the sort and allocation work happens at most once per ledger. Hit rate and cache size are exposed through the `get_counts` admin RPC as `AL_hit_rate` and `AL_size`, giving operators visibility into cache efficiency. + +## `AcceptedLedgerTx` — The Enriched Transaction Wrapper + +Each element in the `transactions_` vector is an `AcceptedLedgerTx` (defined in `include/xrpl/ledger/AcceptedLedgerTx.h`), which bundles together the raw `STTx`, the associated `TxMeta`, a `flat_set` of affected accounts, a serialized metadata blob, and a pre-built `Json::Value`. This per-transaction enrichment is what `InfoSub` needs to dispatch targeted account notifications to WebSocket subscribers — rather than re-parsing metadata on every delivery, the affected-accounts set is computed once at `AcceptedLedgerTx` construction time. + +## Diagnostic Instrumentation via `CountedObject` + +`AcceptedLedger` inherits from `CountedObject`, which uses a static `CountedObjects::Counter` with a lock-free linked-list registration to track the number of live instances. The counter increments on construction and decrements on destruction. This adds zero overhead to normal operation — the counter is an `std::atomic` — while enabling the `get_counts` handler to report how many `AcceptedLedger` objects are resident in memory at any moment, which complements the cache size metric. + +## Interface Design + +The public interface is intentionally minimal: `getLedger()` returns the underlying `ReadView`, `size()` returns the transaction count, and `begin()`/`end()` return iterators into the sorted `transactions_` vector. There is no random-access by index or by transaction ID; consumers are expected to iterate the full sequence. This matches the two real use cases — sequential SQL writes and sequential event fan-out — and avoids the complexity of building secondary indices that are never needed. \ No newline at end of file diff --git a/src/xrpld/app/ledger/AccountStateSF.cpp.ai.json b/src/xrpld/app/ledger/AccountStateSF.cpp.ai.json new file mode 100644 index 0000000000..1e34dc6663 --- /dev/null +++ b/src/xrpld/app/ledger/AccountStateSF.cpp.ai.json @@ -0,0 +1,114 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SHAMap::visitNodes / SHAMap::fetchNode / SHAMap::getMissingNodes", + "AccountStateSF::gotNode" + ], + "entry_point": "AccountStateSF::gotNode", + "purpose": "Handles receipt of a node's data during SHAMap traversal or fetch, storing it in the node database.", + "validation_points": [ + "Upstream: SHAMap logic may validate node data before calling gotNode, but gotNode itself does not perform validation." + ] + }, + { + "call_chain": [ + "SHAMap::getNode / SHAMap::fetchNode / SHAMap::getMissingNodes", + "AccountStateSF::getNode" + ], + "entry_point": "AccountStateSF::getNode", + "purpose": "Retrieves node data from the fetch pack for SHAMap reconstruction or validation.", + "validation_points": [ + "Upstream: SHAMap may validate the returned data, but getNode itself does not perform validation." + ] + } + ], + "data_flows": [ + { + "field": "nodeData (Blob)", + "flow": [ + "SHAMap fetches node data (from network or disk)", + "nodeData passed to AccountStateSF::gotNode", + "db_.store(hotACCOUNT_NODE, std::move(nodeData), nodeHash.as_uint256(), ledgerSeq)" + ], + "origin": "Passed into AccountStateSF::gotNode by SHAMap traversal/fetch logic", + "transformations": [ + "nodeData is moved (std::move) into the database store method; no validation or transformation in gotNode" + ], + "validated_at": "Not validated in gotNode; assumed validated upstream in SHAMap or network layer" + }, + { + "field": "nodeHash (SHAMapHash)", + "flow": [ + "SHAMap computes or receives nodeHash", + "nodeHash passed to AccountStateSF::gotNode or getNode", + "Converted to uint256 via as_uint256()", + "Used as key in db_.store or fp_.getFetchPack" + ], + "origin": "Passed into AccountStateSF::gotNode and getNode by SHAMap logic", + "transformations": [ + "Type conversion from SHAMapHash to uint256" + ], + "validated_at": "Not validated in AccountStateSF; assumed valid from SHAMap context" + }, + { + "field": "ledgerSeq (std::uint32_t)", + "flow": [ + "SHAMap determines ledger sequence", + "ledgerSeq passed to AccountStateSF::gotNode", + "Used as argument in db_.store" + ], + "origin": "Passed into AccountStateSF::gotNode by SHAMap logic", + "transformations": [ + "No transformation" + ], + "validated_at": "Not validated in AccountStateSF" + } + ], + "description": "Implements methods for AccountStateSF to handle storing and retrieving account state nodes in the ledger, interacting with a database and fetch pack.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/AccountStateSF.cpp", + "functions": [ + { + "args": [ + "bool", + "SHAMapHash const& nodeHash", + "std::uint32_t ledgerSeq", + "Blob&& nodeData", + "SHAMapNodeType" + ], + "lineno": 5, + "name": "AccountStateSF::gotNode" + }, + { + "args": [ + "SHAMapHash const& nodeHash" + ], + "lineno": 13, + "name": "AccountStateSF::getNode" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "Direct unit tests for AccountStateSF are unlikely; coverage is typically indirect via SHAMap and ledger synchronization tests. Likely test files: SHAMap_test.cpp, LedgerReplay_test.cpp, LedgerMaster_test.cpp, FetchPack_test.cpp. Gaps: No direct validation or error handling in AccountStateSF methods; relies on upstream validation. Edge cases (e.g., corrupt nodeData, invalid nodeHash) are not handled or tested at this level.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/AccountStateSF.cpp.ai.md b/src/xrpld/app/ledger/AccountStateSF.cpp.ai.md new file mode 100644 index 0000000000..6325e58364 --- /dev/null +++ b/src/xrpld/app/ledger/AccountStateSF.cpp.ai.md @@ -0,0 +1,49 @@ +# `AccountStateSF` — Account State SHAMap Sync Filter + +## Role and Purpose + +`AccountStateSF` is the concrete `SHAMapSyncFilter` implementation that connects the low-level SHAMap synchronization machinery to the ledger's persistence and peer-data layers for the **account state tree**. When a node is fetching a historical ledger from the network, the SHAMap traversal code needs two capabilities: somewhere to cache nodes it has just received, and somewhere to look for nodes it might already have cached locally from prior peer messages. `AccountStateSF` fills both roles — it writes newly received nodes straight into the `NodeStore::Database`, and it reads speculatively available nodes out of a fetch-pack cache supplied by `AbstractFetchPackContainer`. + +The class holds references to both collaborators: `db_` is the persistent node store shared across all ledger trees, and `fp_` is an abstract interface for fetch-pack retrieval, deliberately narrow so that neither `Application` nor `LedgerMaster` need to be visible at construction time. + +## The `SHAMapSyncFilter` Contract + +The base class `SHAMapSyncFilter` (in `include/xrpl/shamap/SHAMapSyncFilter.h`) defines only two pure virtual methods and is non-copyable: + +- `gotNode()` — called by the SHAMap engine after a tree node has been obtained (from the network, a peer response, or a disk fetch). The implementation is responsible for persisting or caching the node so it survives beyond the current sync pass. +- `getNode()` — called before the SHAMap engine goes to the network for a missing node. If the filter can supply the data from a local cache, the network round-trip is avoided. + +The `bool fromFilter` first parameter to `gotNode()` distinguishes whether the node came from `getNode()` itself (i.e., was already in the local fetch pack) versus arrived fresh from the network. `AccountStateSF` ignores this flag — it unconditionally stores every node into the database. This is intentional: the persistent node store is the canonical destination, and writing the same content-addressed blob twice is harmless (the store will deduplicate by `uint256` hash). There is no scenario where the account state filter would want to silently discard a node it just learned about. + +## `gotNode` — Persisting Received Nodes + +```cpp +void AccountStateSF::gotNode(bool, SHAMapHash const& nodeHash, + std::uint32_t ledgerSeq, Blob&& nodeData, SHAMapNodeType) const +{ + db_.store(hotACCOUNT_NODE, std::move(nodeData), nodeHash.as_uint256(), ledgerSeq); +} +``` + +The two ignored parameters (`bool` and `SHAMapNodeType`) narrow the implementation's concern: `AccountStateSF` only ever handles account state nodes, so the type tag from the SHAMap layer is redundant — the `hotACCOUNT_NODE` tag is hardcoded when writing to the database. This tag ends up serialized into the stored blob's encoding and distinguishes account state nodes from ledger-header blobs (`hotLEDGER`) and transaction nodes (`hotTRANSACTION_NODE`) in the node store. The `nodeData` blob is moved rather than copied, avoiding an allocation for what could be a substantial serialized SHAMap inner or leaf node. + +## `getNode` — Looking Up Cached Nodes + +```cpp +std::optional AccountStateSF::getNode(SHAMapHash const& nodeHash) const +{ + return fp_.getFetchPack(nodeHash.as_uint256()); +} +``` + +The fetch pack is a short-lived cache of partial ledger data collected from peers during the ledger-acquisition protocol. Consulting it before requesting nodes from the network avoids redundant peer messages when multiple missing nodes from the same ledger are being fetched in parallel. `AbstractFetchPackContainer` returns `std::nullopt` when the hash is not present, which signals to the SHAMap engine to proceed with a network request. + +## Relationship to `ConsensusTransSetSF` + +The sibling class `ConsensusTransSetSF` serves the same role for transaction sets built during consensus. The architectural difference is deliberate: `ConsensusTransSetSF` uses an in-memory `TaggedCache` because consensus transaction sets are ephemeral — they only need to live long enough for consensus to complete. `AccountStateSF` routes everything through the persistent `NodeStore::Database` because account state nodes must survive process restarts and be available for future ledger queries. The choice of backing store is the entire semantic difference between the two filters. + +## Usage in `InboundLedger` + +`InboundLedger` constructs `AccountStateSF` instances at multiple points during the ledger-fetch lifecycle — when checking for a locally-cached state root, when computing missing node lists, and when processing incoming peer responses containing account state subtrees. The filter is always constructed on the stack with `mLedger->stateMap().family().db()` and `app_.getLedgerMaster()` (which implements `AbstractFetchPackContainer`), keeping its lifetime scoped to the operation at hand and avoiding any heap allocation or shared ownership. + +The `SHAMapSyncFilter` base class deletes its copy constructor and copy-assignment operator. `AccountStateSF` inherits this non-copyability, reinforcing that it is a thin, stack-scoped adapter rather than an ownable resource. \ No newline at end of file diff --git a/src/xrpld/app/ledger/AccountStateSF.h.ai.json b/src/xrpld/app/ledger/AccountStateSF.h.ai.json new file mode 100644 index 0000000000..42a71874bd --- /dev/null +++ b/src/xrpld/app/ledger/AccountStateSF.h.ai.json @@ -0,0 +1,71 @@ +{ + "args": [ + { + "lineno": 11, + "name": "db" + }, + { + "lineno": 11, + "name": "fp" + }, + { + "lineno": 16, + "name": "fromFilter" + }, + { + "lineno": 17, + "name": "nodeHash" + }, + { + "lineno": 18, + "name": "ledgerSeq" + }, + { + "lineno": 19, + "name": "nodeData" + }, + { + "lineno": 20, + "name": "type" + } + ], + "classes": [ + { + "args": [ + "db", + "fp" + ], + "lineno": 8, + "name": "AccountStateSF" + } + ], + "description": "Defines the AccountStateSF class, a sync filter for account state nodes used during ledger synchronization in the XRPL, facilitating node data storage and retrieval via a database and fetch pack container.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/AccountStateSF.h", + "functions": [ + { + "args": [ + "fromFilter", + "nodeHash", + "ledgerSeq", + "nodeData", + "type" + ], + "lineno": 16, + "name": "AccountStateSF::gotNode" + }, + { + "args": [ + "nodeHash" + ], + "lineno": 22, + "name": "AccountStateSF::getNode" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/AccountStateSF.h.ai.md b/src/xrpld/app/ledger/AccountStateSF.h.ai.md new file mode 100644 index 0000000000..0d25aaab41 --- /dev/null +++ b/src/xrpld/app/ledger/AccountStateSF.h.ai.md @@ -0,0 +1,23 @@ +# `AccountStateSF` — Account State SHAMap Sync Filter + +`AccountStateSF` is a concrete implementation of `SHAMapSyncFilter` dedicated to the **account state tree** during ledger synchronization. Its entire purpose is to answer two questions that the generic SHAMap sync machinery cannot: "what do I do when I receive a new node?" and "where do I look for a node I don't have yet?" The class lives at the seam between the SHAMap layer (which is deliberately kept storage-agnostic) and the application-level infrastructure that owns persistent node storage and ephemeral peer-supplied data. + +## Role in Ledger Acquisition + +When an XRPL node is catching up to the network, it reconstructs missing ledgers by fetching their constituent SHAMap nodes from peers. The `InboundLedger` subsystem drives this process and constructs `AccountStateSF` instances on the stack — for example, when calling `fetchRoot()` or `neededStateHashes()` on the account state map. The sync filter is short-lived: it exists only for the duration of a single acquisition step and is discarded once the call returns. This usage pattern explains why the constructor takes references rather than shared ownership: the caller — `InboundLedger` — is responsible for ensuring that both the `NodeStore::Database` and the `AbstractFetchPackContainer` outlive the filter. + +## The Two Filter Callbacks + +The `SHAMapSyncFilter` base class defines exactly two pure virtual methods, and `AccountStateSF` provides the account-state-specific semantics for each. + +`gotNode()` is called by SHAMap sync code when it has successfully received a node from a peer. `AccountStateSF`'s implementation unconditionally forwards it to `db_.store()`, tagging it as `hotACCOUNT_NODE`. This tag is significant: it tells the node store that this blob is an account state trie node, enabling type-specific storage and eviction policies. The `fromFilter` boolean (indicating whether the node came from the filter's own cache rather than a remote peer) and the `SHAMapNodeType` are both ignored — the implementation stores regardless of origin, since durability is always the right behavior here. The node data is moved in, reflecting a zero-copy handoff to the database. + +`getNode()` is called when sync code needs a specific node that isn't yet in the local store. Rather than attempting a database read (which would be slow and is unlikely to help during an in-progress acquisition), it consults the **fetch pack** via `fp_.getFetchPack()`. Fetch packs are short-lived, peer-assembled blobs of ledger data: a peer bundles up the nodes needed to complete a ledger acquisition and sends them ahead of explicit requests. The fetch pack container holds these in memory temporarily, making them available through this call. Returning `std::nullopt` signals that the node isn't available locally and must be explicitly requested from peers. + +## Dependency Design + +The constructor takes `AbstractFetchPackContainer&` rather than a direct reference to `LedgerMaster`. This is a deliberate narrowing of the dependency. `AccountStateSF` only needs one thing from `LedgerMaster`: the ability to look up fetch pack blobs by hash. Binding it to the full `LedgerMaster` would introduce a broad coupling between the sync filter and the application's central ledger management object. The `AbstractFetchPackContainer` interface — a single-method pure virtual — expresses the minimum required contract. `LedgerMaster` happens to implement it, so the usage in `InboundLedger.cpp` passes `app_.getLedgerMaster()` directly, but the filter itself knows nothing of that. + +## Relationship to Sibling Filters + +`AccountStateSF` has a structural twin: `TransactionStateSF`, which fulfills the same role for the **transaction tree** of a ledger. Together they cover the two SHAMap trees that compose a complete XRPL ledger. Both implement `gotNode()` and `getNode()` with identical strategies but different `hotType` storage tags. A third related class, `ConsensusTransSetSF`, handles transaction sets during active consensus — a distinct use case where nodes go into a transient cache rather than the persistent node store. The existence of these three classes reflects the principle that storage policy is per-tree and per-phase, and the sync filter interface provides the customization point without burdening the generic SHAMap code with those distinctions. \ No newline at end of file diff --git a/src/xrpld/app/ledger/BuildLedger.h.ai.json b/src/xrpld/app/ledger/BuildLedger.h.ai.json new file mode 100644 index 0000000000..b863b9c502 --- /dev/null +++ b/src/xrpld/app/ledger/BuildLedger.h.ai.json @@ -0,0 +1,39 @@ +{ + "args": [], + "classes": [], + "description": "This file declares functions for building a new ledger in the XRPL system, either by applying consensus transactions or by replaying transactions from a prior ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/BuildLedger.h", + "functions": [ + { + "args": [ + "parent", + "closeTime", + "closeTimeCorrect", + "closeResolution", + "app", + "txns", + "failedTxs", + "j" + ], + "lineno": 23, + "name": "buildLedger" + }, + { + "args": [ + "replayData", + "applyFlags", + "app", + "j" + ], + "lineno": 46, + "name": "buildLedger" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/BuildLedger.h.ai.md b/src/xrpld/app/ledger/BuildLedger.h.ai.md new file mode 100644 index 0000000000..d1176694b1 --- /dev/null +++ b/src/xrpld/app/ledger/BuildLedger.h.ai.md @@ -0,0 +1,70 @@ +# `BuildLedger.h` — Ledger Construction Entry Points + +This header declares the two public entry points used to construct a finalized `Ledger` object — the immutable, hash-committed record of one round of consensus. It lives at the boundary between the consensus engine and the ledger state machine: once consensus has agreed on a set of transactions, `buildLedger` is what turns that agreement into a canonical ledger. + +## Two Construction Paths + +The file exposes two overloads of `buildLedger`, both returning `std::shared_ptr`. + +**Consensus path** — the primary production path: + +```cpp +std::shared_ptr buildLedger( + std::shared_ptr const& parent, + NetClock::time_point closeTime, + bool const closeTimeCorrect, + NetClock::duration closeResolution, + Application& app, + CanonicalTXSet& txns, + std::set& failedTxs, + beast::Journal j); +``` + +This is called after a consensus round completes. The `txns` parameter is a `CanonicalTXSet` — a sorted, salted map that groups an account's transactions by sequence number to prevent ordering attacks. Critically, `txns` is both an input and an output: transactions that neither succeeded nor definitively failed are left in the set on return, signaling to the caller that they must be re-queued for the next round. `failedTxs` accumulates the IDs of transactions that failed irrecoverably in this round. + +**Replay path** — used during ledger replay (e.g., catchup or history validation): + +```cpp +std::shared_ptr buildLedger( + LedgerReplay const& replayData, + ApplyFlags applyFlags, + Application& app, + beast::Journal j); +``` + +`LedgerReplay` bundles a parent ledger, the ledger being replicated, and its transactions pre-sorted into apply order via `orderedTxns()` (keyed by `uint32_t` sequence position, not account sequence). This path passes caller-controlled `ApplyFlags`, which in practice includes flags like `tapNO_CHECK_SIGN` to skip re-verifying cryptographic signatures on already-validated transactions — an important performance optimization for replay at scale. + +## Shared Skeleton: `buildLedgerImpl` + +Both overloads delegate to an internal template function `buildLedgerImpl` defined in `detail/BuildLedger.cpp`. The template parameter `ApplyTxs` is a callable with signature `void(OpenView&, std::shared_ptr const&)`, letting the two paths inject their transaction-application logic while sharing the surrounding ledger lifecycle: + +1. A new mutable `Ledger` is constructed from the parent and close time. +2. If this is a flag ledger (every 256th ledger), `updateNegativeUNL()` is called to finalize UNL adjustments — a consensus mechanism for disabling offline validators. +3. An `OpenView` accumulator wraps the mutable ledger. All transaction state changes are applied here without immediately committing. +4. `accum.apply(*built)` flushes the accumulated delta back to the ledger's `SHAMap`. +5. The ledger's skip list is updated (used for efficient ledger-hash lookups at arbitrary sequence numbers). +6. Both the account state and transaction `SHAMap` trees are flushed to the node store (`flushDirty`), persisting the new LCL. +7. `unshare()` ensures the ledger's internal state maps are not aliased with any other objects. +8. `setAccepted()` locks in the close time and resolution, marking the ledger as final. + +## Multi-Pass Transaction Application (`applyTransactions`) + +The consensus path's `ApplyTxs` lambda calls `applyTransactions`, which implements a retry loop bounded by two constants from `OpenLedger.h`: `LEDGER_TOTAL_PASSES = 3` and `LEDGER_RETRY_PASSES = 1`. + +The rationale for multiple passes is inter-transaction dependency: an `OfferCreate` from account A may only succeed once a preceding `Payment` to account A (from a different account) is applied first, but the `CanonicalTXSet` ordering might process them in the wrong relative order within one pass. Retry passes allow such dependent transactions to succeed after their prerequisites have been resolved. + +The pass structure: +- Passes 0 through `LEDGER_RETRY_PASSES` are "retry" passes — transactions returning `Retry` stay in the set. +- Once `changes == 0` or `pass >= LEDGER_RETRY_PASSES`, the retry flag switches off. +- At least one non-retry "final" pass is guaranteed before the loop exits. +- An `XRPL_ASSERT` enforces the invariant: if `txns` is non-empty at exit, the retry flag must be `false` (confirming the final pass ran). + +On pass 0, any transaction already present in the built ledger's transaction map (a duplicate from a previous round) is immediately dropped. This is the deduplication guard — without it, a retry transaction that was already committed could be applied twice. + +Exception safety is handled per-transaction: any `std::exception` thrown during `applyTransaction` moves the offending transaction into `failedTxs` and removes it from `txns`, preventing a single bad transaction from aborting the entire ledger build. + +## Design Rationale + +The overloaded-function design (rather than a strategy object or enum dispatch) is intentional: the two paths have fundamentally different input types (`CanonicalTXSet` vs. `LedgerReplay`) with incompatible interfaces, so overload resolution cleanly separates them without a runtime branch inside the implementation. The `buildLedgerImpl` template avoids code duplication for the ledger lifecycle while remaining zero-cost — the lambda is inlined by the compiler, leaving no virtual dispatch overhead in a hot path. + +The `OpenView` accumulation pattern (apply to a view, then flush) is consistent with how `OpenLedger` manages the live open ledger, ensuring that the same apply semantics are used for both speculative (open) and final (closed) ledger construction. \ No newline at end of file diff --git a/src/xrpld/app/ledger/ConsensusTransSetSF.cpp.ai.json b/src/xrpld/app/ledger/ConsensusTransSetSF.cpp.ai.json new file mode 100644 index 0000000000..62a90f93cf --- /dev/null +++ b/src/xrpld/app/ledger/ConsensusTransSetSF.cpp.ai.json @@ -0,0 +1,265 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app", + "NodeCache& nodeCache" + ], + "lineno": 9, + "name": "ConsensusTransSetSF" + } + ], + "code_paths": [ + { + "call_chain": [ + "ConsensusTransSetSF::gotNode", + "std::make_shared(std::ref(sit))", + "stx->getTransactionID()", + "XRPL_ASSERT" + ], + "entry_point": "ConsensusTransSetSF::gotNode", + "purpose": "Handles a node received during consensus transaction set acquisition. If the node is a transaction, validates its hash and submits it for processing.", + "validation_points": [ + "XRPL_ASSERT(stx->getTransactionID() == nodeHash.as_uint256(), ...)" + ] + }, + { + "call_chain": [ + "ConsensusTransSetSF::getNode", + "app_.getMasterTransaction().fetch_from_cache", + "txn->getSTransaction()->add(s)", + "sha512Half(s.slice())", + "XRPL_ASSERT" + ], + "entry_point": "ConsensusTransSetSF::getNode", + "purpose": "Retrieves a node (transaction) from cache or transaction master, serializes it, and validates its hash before returning the data.", + "validation_points": [ + "XRPL_ASSERT(sha512Half(s.slice()) == nodeHash.as_uint256(), ...)" + ] + } + ], + "data_flows": [ + { + "field": "nodeData", + "flow": [ + "gotNode parameter", + "m_nodeCache.insert(nodeHash, nodeData)", + "Serializer s(nodeData.data() + 4, nodeData.size() - 4)", + "SerialIter sit(s.slice())", + "STTx constructed from sit", + "stx->getTransactionID() validated" + ], + "origin": "Input parameter to ConsensusTransSetSF::gotNode", + "transformations": [ + "Skipped first 4 bytes (prefix)", + "Deserialized into STTx" + ], + "validated_at": "XRPL_ASSERT(stx->getTransactionID() == nodeHash.as_uint256(), ...)" + }, + { + "field": "nodeHash", + "flow": [ + "gotNode/getNode parameter", + "Used as key for m_nodeCache", + "Used as hash to validate transaction" + ], + "origin": "Input parameter to gotNode/getNode", + "transformations": [ + "Converted to uint256 for comparison" + ], + "validated_at": "XRPL_ASSERT in both gotNode and getNode" + }, + { + "field": "txn (Transaction::pointer)", + "flow": [ + "Fetched from cache", + "txn->getSTransaction()->add(s)", + "Serializer s", + "sha512Half(s.slice())", + "XRPL_ASSERT" + ], + "origin": "app_.getMasterTransaction().fetch_from_cache(nodeHash.as_uint256())", + "transformations": [ + "Serialized with prefix", + "Hashed with sha512Half" + ], + "validated_at": "XRPL_ASSERT(sha512Half(s.slice()) == nodeHash.as_uint256(), ...)" + }, + { + "field": "nodeData.size()", + "flow": [ + "gotNode parameter", + "if (nodeData.size() > 16)" + ], + "origin": "Input parameter to gotNode", + "transformations": [ + "Checked for minimum size to be considered a transaction" + ], + "validated_at": "Explicit size check before deserialization" + } + ], + "description": "Implements the ConsensusTransSetSF class, which acts as a sync filter for transaction sets during consensus in the XRPL, handling node acquisition and transaction submission.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction hash (stx->getTransactionID())", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ConsensusTransSetSF::gotNode", + "issue_pattern": "Missing empty string validation for transaction hash (stx->getTransactionID())", + "why_false_positive": "XRPL_ASSERT validates transaction hash (stx->getTransactionID()) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction hash (sha512Half(s.slice()))", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at ConsensusTransSetSF::getNode", + "issue_pattern": "Missing empty string validation for transaction hash (sha512Half(s.slice()))", + "why_false_positive": "XRPL_ASSERT validates transaction hash (sha512Half(s.slice())) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "nodeData size", + "empty", + "string", + "validation" + ], + "evidence": "explicit size check (nodeData.size() > 16) at ConsensusTransSetSF::gotNode", + "issue_pattern": "Missing empty string validation for nodeData size", + "why_false_positive": "explicit size check (nodeData.size() > 16) validates nodeData size for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "nodeData size", + "range", + "bounds", + "validation" + ], + "evidence": "explicit size check (nodeData.size() > 16) at ConsensusTransSetSF::gotNode", + "issue_pattern": "Missing range validation for nodeData size", + "why_false_positive": "explicit size check (nodeData.size() > 16) validates nodeData size range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/ConsensusTransSetSF.cpp", + "functions": [ + { + "args": [ + "Application& app", + "NodeCache& nodeCache" + ], + "lineno": 10, + "name": "ConsensusTransSetSF::ConsensusTransSetSF" + }, + { + "args": [ + "bool fromFilter", + "SHAMapHash const& nodeHash", + "std::uint32_t", + "Blob&& nodeData", + "SHAMapNodeType type" + ], + "lineno": 14, + "name": "ConsensusTransSetSF::gotNode" + }, + { + "args": [ + "SHAMapHash const& nodeHash" + ], + "lineno": 48, + "name": "ConsensusTransSetSF::getNode" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "ConsensusTransSetSF is a core consensus utility. Typical test coverage would be in integration/consensus tests, possibly in files like test/ledger/SHAMap_test.cpp, test/app/ledger/ConsensusTransSetSF_test.cpp, or test/app/consensus/Consensus_test.cpp. Direct unit tests for validation (hash checks, size checks) may be limited; most coverage likely comes from higher-level consensus and transaction set acquisition tests. Gaps: Exception handling for invalid transactions may not be directly tested; edge cases for malformed nodeData or hash mismatches may lack explicit unit tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), explicit C++ checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely std::runtime_error or abort, depending on XRPL_ASSERT implementation)", + "field": "transaction hash (stx->getTransactionID())", + "location": "ConsensusTransSetSF::gotNode", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that the transaction ID of the deserialized transaction matches the nodeHash provided" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely std::runtime_error or abort, depending on XRPL_ASSERT implementation)", + "field": "transaction hash (sha512Half(s.slice()))", + "location": "ConsensusTransSetSF::getNode", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that the hash of the serialized transaction matches the nodeHash provided" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logic, not an exception)", + "field": "nodeData size", + "location": "ConsensusTransSetSF::gotNode", + "validated_by": "explicit size check (nodeData.size() > 16)", + "validates": [ + "Ensures nodeData is large enough to be a transaction (greater than 16 bytes) before attempting to deserialize" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/ConsensusTransSetSF.cpp.ai.md b/src/xrpld/app/ledger/ConsensusTransSetSF.cpp.ai.md new file mode 100644 index 0000000000..5df254858a --- /dev/null +++ b/src/xrpld/app/ledger/ConsensusTransSetSF.cpp.ai.md @@ -0,0 +1,31 @@ +# `ConsensusTransSetSF` — Transaction Set Sync Filter for Consensus + +## Role in the System + +During XRPL consensus, every validator must converge on the same transaction set before building the next ledger. When a node doesn't have the full transaction set proposed by its peers, it acquires it incrementally over the peer-to-peer network using `TransactionAcquire`, which internally drives a `SHAMap` synchronization process. `ConsensusTransSetSF` sits at the boundary between that low-level `SHAMap` sync machinery and the higher-level application layer — acting as the bridge that lets the sync engine consult and populate application caches, and that opportunistically injects discovered transactions into the processing pipeline. + +The file implements `SHAMapSyncFilter`, a two-method pure-virtual interface defined in `include/xrpl/shamap/SHAMapSyncFilter.h`. The two methods represent opposite data directions: `getNode` supplies data *to* the sync engine (look here first before fetching from the network), and `gotNode` receives notifications *from* the sync engine (a new node just arrived). + +## Design Rationale: Two-Direction Filtering + +The `SHAMapSyncFilter` abstraction cleanly separates what the SHAMap knows (tree structure, hashes) from what the application knows (transaction caches, job queues, database). This lets the sync algorithm remain agnostic about higher-level storage. `ConsensusTransSetSF` is explicitly noted in its header as being "needed on both add and check functions," distinguishing it from `AccountStateSF`, which only participates in add operations. This distinction reflects that during consensus transaction set acquisition, the node both needs to contribute locally cached data (to avoid redundant network fetches) and needs to act on newly received data (to pipeline discovered transactions). + +## `getNode`: Serving Local Data to the Sync Engine + +`getNode(nodeHash)` is called by the `SHAMap` sync code when it needs the raw bytes for a particular hash before deciding to request it from a peer. The lookup proceeds in two stages. First it checks `m_nodeCache`, a `TaggedCache` for intermediate SHAMap tree nodes. If that misses, it falls back to `app_.getMasterTransaction().fetch_from_cache()`, which queries an in-memory `TaggedCache` for leaf transaction nodes. + +When a transaction is found via `TransactionMaster`, it must be re-serialized into wire format before returning. This serialization prepends `HashPrefix::transactionID` (4 bytes) before the transaction body — the same prefix used when the transaction was originally hashed. An `XRPL_ASSERT` then verifies that `sha512Half` of the resulting blob matches the requested hash. This invariant enforcement ensures no hash/content mismatch can propagate silently through the sync process. + +## `gotNode`: Receiving and Forwarding Newly Acquired Nodes + +`gotNode` is invoked whenever the sync engine successfully integrates a node into the `SHAMap`. The `fromFilter` flag signals whether the data originated from the filter itself (i.e., was returned by a prior `getNode` call). If true, the node was already known locally and there is nothing to do — the method returns immediately, avoiding redundant cache writes and duplicate transaction submissions. + +For genuinely new nodes, the raw bytes are first inserted into `m_nodeCache`. If the node type is `SHAMapNodeType::tnTRANSACTION_NM` (a leaf transaction node without metadata) and the data is longer than 16 bytes (a guard against malformed stubs), the method proceeds to deserialize it. It skips the 4-byte `HashPrefix` prefix and constructs an `STTx` from the remaining bytes via `SerialIter`. An `XRPL_ASSERT` checks that the deserialized transaction's ID matches the node hash, catching any corruption before the transaction enters the application. + +The transaction is then submitted asynchronously via `app_.getJobQueue().addJob(jtTRANSACTION, ...)`, which calls `NetworkOPs::submitTransaction`. This is the critical side-effect: transactions discovered while assembling a consensus set are opportunistically forwarded into the node's own transaction processing pipeline, so the node can validate and hold them locally even if it hadn't seen them before. The job queue dispatch avoids blocking the sync callback thread on transaction validation work. + +Deserialization failures are caught as `std::exception` and logged as warnings rather than propagating. A malformed transaction in a proposed set is treated as a non-fatal event — the SHAMap node is still cached, but the invalid transaction is not submitted. + +## Integration with `TransactionAcquire` + +`ConsensusTransSetSF` is instantiated in `TransactionAcquire::trigger()` and `TransactionAcquire::takeNodes()`, always using `app_.getTempNodeCache()` as the shared `NodeCache`. In `trigger()`, it is passed to `SHAMap::getMissingNodes(&sf)`, allowing the sync engine to fill gaps from local caches before computing which hashes to request from peers. In `takeNodes()`, it is passed to `SHAMap::addKnownNode(..., &sf)`, so each newly received peer node flows through `gotNode` for caching and potential transaction injection. The filter object is stack-allocated and short-lived — created per call rather than shared across the lifetime of the acquisition — which is safe because `Application` and `NodeCache` are both externally owned and longer-lived. \ No newline at end of file diff --git a/src/xrpld/app/ledger/ConsensusTransSetSF.h.ai.json b/src/xrpld/app/ledger/ConsensusTransSetSF.h.ai.json new file mode 100644 index 0000000000..bd61f22d72 --- /dev/null +++ b/src/xrpld/app/ledger/ConsensusTransSetSF.h.ai.json @@ -0,0 +1,50 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app", + "NodeCache& nodeCache" + ], + "lineno": 13, + "name": "ConsensusTransSetSF" + } + ], + "description": "Defines the ConsensusTransSetSF class, a sync filter for transaction sets used during consensus building in XRPL, allowing SHAMapSync code to interact with higher-level structures such as caches and transaction stores.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/ConsensusTransSetSF.h", + "functions": [ + { + "args": [ + "Application& app", + "NodeCache& nodeCache" + ], + "lineno": 17, + "name": "ConsensusTransSetSF" + }, + { + "args": [ + "bool fromFilter", + "SHAMapHash const& nodeHash", + "std::uint32_t ledgerSeq", + "Blob&& nodeData", + "SHAMapNodeType type" + ], + "lineno": 21, + "name": "gotNode" + }, + { + "args": [ + "SHAMapHash const& nodeHash" + ], + "lineno": 28, + "name": "getNode" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/ConsensusTransSetSF.h.ai.md b/src/xrpld/app/ledger/ConsensusTransSetSF.h.ai.md new file mode 100644 index 0000000000..60b739044d --- /dev/null +++ b/src/xrpld/app/ledger/ConsensusTransSetSF.h.ai.md @@ -0,0 +1,25 @@ +# ConsensusTransSetSF.h — Sync Filter for Consensus Transaction Sets + +`ConsensusTransSetSF` is a concrete implementation of the `SHAMapSyncFilter` interface, purpose-built for the transaction-set acquisition phase of XRPL consensus. It bridges the low-level `SHAMap` tree-synchronization machinery with two higher-level application structures: an in-memory node cache and the node's own transaction store. + +## Why a Sync Filter Exists + +`SHAMap` is a content-addressed Merkle tree used to represent both ledger state and transaction sets. When a validator needs to reconcile a proposed transaction set from its peers, the `SHAMapSync` code walks the tree and identifies missing nodes. But the sync code itself has no knowledge of higher-level caches, the transaction pool, or the job queue — it just knows how to hash nodes and ask for missing ones. The `SHAMapSyncFilter` interface is the designated extension point that lets the application layer hook into this process. `ConsensusTransSetSF` is the filter instance created for every transaction-set acquisition. + +## The Two Filter Methods + +`getNode()` is called by the `SHAMap` when it needs a node during `getMissingNodes()` traversal. The filter can satisfy the request from local data, avoiding a network round-trip. `ConsensusTransSetSF` tries two sources in priority order: first the `NodeCache` (a `TaggedCache` of recently seen raw node bytes), then the `TransactionMaster`'s transaction cache. If the `TransactionMaster` has the transaction, the method serializes it on the fly with a `HashPrefix::transactionID` prefix, validates the resulting hash against the requested hash via `XRPL_ASSERT`, and returns the serialized bytes. This two-stage lookup is deliberate: the node cache is a general blob store covering any SHAMap node type, while the transaction master provides a typed lookup only for known transactions. + +`gotNode()` is called when the SHAMap successfully acquires a node — either from the filter itself (`fromFilter == true`) or from the peer network. When `fromFilter` is true, the method returns immediately; there is nothing new to learn since the data came from local state. When the node arrived from the network (`fromFilter == false`), two things happen. First, the raw bytes are inserted into the `NodeCache` so subsequent `getNode()` lookups and other map operations can be served from memory. Second, if the node type is `tnTRANSACTION_NM` (a non-metadata transaction leaf) and the data is large enough to be meaningful, the method deserializes the transaction from the node data (skipping the 4-byte `HashPrefix`) and submits it to the network via `app_.getJobQueue().addJob(jtTRANSACTION, ...)`. The job calls `NetworkOPs::submitTransaction()`, which routes the transaction into the node's own transaction pool. + +## The Side-Effect Design Decision + +Submitting newly discovered transactions to the local pool from within `gotNode()` is a notable design choice. During consensus, a validator must apply every transaction in the agreed-upon transaction set to compute the correct next ledger state. If a transaction is in the consensus set but was never previously seen by this node — perhaps it was submitted to a different peer first — the node might fail to apply it. By opportunistically submitting each newly seen transaction during tree acquisition, `ConsensusTransSetSF` ensures the transaction enters the node's local processing pipeline before consensus closes. The submission happens asynchronously via the job queue rather than inline in the callback, which keeps the SHAMap sync loop from blocking on transaction validation. + +## Usage in TransactionAcquire + +`ConsensusTransSetSF` is instantiated only in `TransactionAcquire`, the class responsible for fetching a specific transaction set identified by a `uint256` hash. It is created on the stack in two places: `trigger()`, where it is passed to `mMap->getMissingNodes()` so locally cached transactions can satisfy tree gaps without peer requests, and `takeNodes()`, where it is passed to the `SHAMap` as incoming peer data is integrated into the tree. Both call sites supply `app_.getTempNodeCache()` as the node cache, which is the application's shared temporary node cache. The filter's journal is "TransactionAcquire", matching the broader acquisition context. + +## Invariant Enforcement + +Both `getNode()` and `gotNode()` include `XRPL_ASSERT` checks that the SHA-512 half-hash of the serialized transaction bytes matches the `nodeHash` used as the lookup key. This defends against cache corruption, incorrect serialization, or malicious data from untrusted peers. If the assertion fires, it signals that somewhere a transaction was stored or transmitted with an incorrect hash relationship — an integrity violation that should never occur under correct operation. \ No newline at end of file diff --git a/src/xrpld/app/ledger/InboundLedger.h.ai.json b/src/xrpld/app/ledger/InboundLedger.h.ai.json new file mode 100644 index 0000000000..da28602ac6 --- /dev/null +++ b/src/xrpld/app/ledger/InboundLedger.h.ai.json @@ -0,0 +1,233 @@ +{ + "args": [ + { + "lineno": 23, + "name": "app" + }, + { + "lineno": 24, + "name": "hash" + }, + { + "lineno": 25, + "name": "seq" + }, + { + "lineno": 26, + "name": "reason" + }, + { + "lineno": 27, + "name": "clock_type" + }, + { + "lineno": 28, + "name": "peerSet" + } + ], + "classes": [ + { + "args": [ + "app", + "hash", + "seq", + "reason", + "clock_type", + "peerSet" + ], + "lineno": 11, + "name": "InboundLedger" + } + ], + "description": "Defines the InboundLedger class, which manages the acquisition of a ledger from peers in the XRPL network, handling data reception, peer management, and completion/failure states.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/InboundLedger.h", + "functions": [ + { + "args": [ + "seq" + ], + "lineno": 32, + "name": "update" + }, + { + "args": [], + "lineno": 36, + "name": "isComplete" + }, + { + "args": [], + "lineno": 41, + "name": "isFailed" + }, + { + "args": [], + "lineno": 46, + "name": "getLedger" + }, + { + "args": [], + "lineno": 51, + "name": "getSeq" + }, + { + "args": [], + "lineno": 56, + "name": "checkLocal" + }, + { + "args": [ + "collectionLock" + ], + "lineno": 57, + "name": "init" + }, + { + "args": [ + "Peer", + "TMLedgerData" + ], + "lineno": 59, + "name": "gotData" + }, + { + "args": [ + "int" + ], + "lineno": 65, + "name": "getJson" + }, + { + "args": [], + "lineno": 68, + "name": "runData" + }, + { + "args": [], + "lineno": 70, + "name": "touch" + }, + { + "args": [], + "lineno": 75, + "name": "getLastAction" + }, + { + "args": [ + "nodes", + "reason" + ], + "lineno": 82, + "name": "filterNodes" + }, + { + "args": [ + "Peer", + "TriggerReason" + ], + "lineno": 85, + "name": "trigger" + }, + { + "args": [], + "lineno": 87, + "name": "getNeededHashes" + }, + { + "args": [], + "lineno": 89, + "name": "addPeers" + }, + { + "args": [ + "srcDB" + ], + "lineno": 91, + "name": "tryDB" + }, + { + "args": [], + "lineno": 93, + "name": "done" + }, + { + "args": [ + "progress", + "peerSetLock" + ], + "lineno": 95, + "name": "onTimer" + }, + { + "args": [], + "lineno": 98, + "name": "getPeerCount" + }, + { + "args": [], + "lineno": 100, + "name": "pmDowncast" + }, + { + "args": [ + "peer", + "data" + ], + "lineno": 102, + "name": "processData" + }, + { + "args": [ + "data" + ], + "lineno": 104, + "name": "takeHeader" + }, + { + "args": [ + "packet", + "SHAMapAddNode" + ], + "lineno": 106, + "name": "receiveNode" + }, + { + "args": [ + "data", + "SHAMapAddNode" + ], + "lineno": 108, + "name": "takeTxRootNode" + }, + { + "args": [ + "data", + "SHAMapAddNode" + ], + "lineno": 110, + "name": "takeAsRootNode" + }, + { + "args": [ + "max", + "filter" + ], + "lineno": 112, + "name": "neededTxHashes" + }, + { + "args": [ + "max", + "filter" + ], + "lineno": 114, + "name": "neededStateHashes" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/InboundLedger.h.ai.md b/src/xrpld/app/ledger/InboundLedger.h.ai.md new file mode 100644 index 0000000000..4ee481d767 --- /dev/null +++ b/src/xrpld/app/ledger/InboundLedger.h.ai.md @@ -0,0 +1,69 @@ +# `InboundLedger.h` — Peer-driven Ledger Acquisition State Machine + +`InboundLedger` represents a single in-flight attempt to obtain a complete ledger from the XRPL peer network. It exists because a rippled node frequently needs ledger data it does not yet hold locally — during catch-up after a network gap, during consensus validation, or while building historical archives. Each instance encapsulates the full acquisition lifecycle: checking the local node store, requesting data from peers, receiving and processing responses, and finally signaling completion or failure to the rest of the system. + +## Inheritance and Object Model + +The class inherits from three bases simultaneously: + +- **`TimeoutCounter`** supplies the timed retry loop. It owns a `boost::asio` timer set to fire every `ledgerAcquireTimeout` (3 seconds). On each expiry it calls the virtual `onTimer()` hook, which `InboundLedger` overrides to escalate the query strategy. After `ledgerTimeoutRetriesMax` (6) unproductive timeouts the acquisition marks itself failed. `TimeoutCounter` also provides the `complete_` / `failed_` pair and the `recursive_mutex mtx_` used as the primary state-machine lock. + +- **`enable_shared_from_this`** is required because the object is owned through shared pointers and callbacks — timer closures, job queue lambdas, and peer callbacks — all capture `shared_from_this()`. Without this, the object might be destroyed while an outstanding callback still references it. + +- **`CountedObject`** provides a global instance counter for diagnostics. + +## The Three-Flag State Machine + +A ledger has three independently fetchable components, tracked by `mHaveHeader`, `mHaveTransactions`, and `mHaveState`: + +1. **Header** (`mHaveHeader`) — the 128-byte record containing the transaction root hash and account state root hash. Nothing else can be fetched without it, because those root hashes are the entry points into the two SHAMaps. + +2. **Transaction map** (`mHaveTransactions`) — the complete `SHAMap` of all transactions in the ledger. + +3. **Account state map** (`mHaveState`) — the complete `SHAMap` of all ledger state objects. + +When both maps are complete, `complete_` is set and `done()` is called. The acquisition is considered finished as soon as all three flags are true; partial success is not possible. Notice the code in `trigger()` fetches state data before transaction data, with an explicit comment: *"it's the most likely to be useful if we wind up abandoning this fetch."* Ledger state is bulkier and more broadly applicable, so it gets priority when network time is limited. + +## Acquisition Reason and Its Effect + +The `Reason` enum is more than metadata — it actively changes behavior at several key points: + +- **`HISTORY`**: Expects a fetch pack to arrive shortly, so `addPeers()` does not immediately call `trigger()` on newly-added peers; `onTimer()` waits for the pack and triggers only after if still needed. On completion, `done()` calls `InboundLedgers::onLedgerFetched()` for rate tracking, but does not attempt to advance the validated ledger. + +- **`CONSENSUS`**: This ledger is needed to close or validate the current round. On completion `done()` calls both `LedgerMaster::checkAccept()` and `LedgerMaster::tryAdvance()` via a dispatched job. + +- **`GENERIC`**: Behaves like `CONSENSUS` for post-completion processing. + +## Dual-Lock Concurrency Design + +`InboundLedger` uses two distinct mutexes with carefully separated responsibilities: + +- **`mtx_`** (inherited `recursive_mutex`): Guards the entire acquisition state — header flags, SHAMap progress, peer queries, and the `complete_`/`failed_` state. This lock may be held for significant durations, e.g., during `getMissingNodes()` on a large state map — though even that is released temporarily inside `trigger()` to avoid monopolizing it. + +- **`mReceivedDataLock`** (`std::mutex`): Guards only `mReceivedData` and `mReceiveDispatched`. This is a plain non-recursive mutex because `gotData()` is called on network I/O threads and must return quickly without blocking on the heavy state-machine lock. + +The `mReceiveDispatched` flag is the key to the batching strategy: `gotData()` queues data and returns `true` the first time (telling the caller to dispatch a job to run `runData()`). Subsequent calls return `false` — the already-dispatched job will drain the queue. `runData()` loops until the queue is empty, then clears `mReceiveDispatched`. + +## Progressive Query Escalation in `onTimer()` + +When no progress is detected (`wasProgress == false`): +1. `checkLocal()` is called first — the node store may have received relevant data since the last check. +2. `mByHash = true` is re-armed, allowing the by-hash request path. +3. Peers are expanded (`addPeers()` adds `peerCountAdd` = 3 more) and existing peers are re-triggered. +4. After `ledgerBecomeAggressiveThreshold` (4) timeouts, `trigger()` switches from SHAMap-node requests to `TMGetObjectByHash` bulk requests, asking all known peers simultaneously for specific missing hashes. + +## `runData()` — Batched Response Processing + +When peer responses arrive via `gotData()`, they are batched into `mReceivedData`. `runData()` drains this queue in a loop: it swaps the live queue into a local vector (minimizing lock hold time), then calls `processData()` for each entry. After the loop, `dataCounts.prune()` and `dataCounts.sampleN(6, ...)` select up to six of the most productive peers — those that supplied the most useful nodes — and call `trigger()` on each with `TriggerReason::reply`. This directs follow-up requests toward peers that have proven useful, rather than broadcasting to everyone. + +## Initialization and Local Lookup + +`init()` is called while holding the `InboundLedgers` collection lock, which it releases early via `collectionLock.unlock()` before doing any real work. This pattern limits contention: the collection only needs to be locked long enough to insert the new entry, not during the potentially slow local DB search. `tryDB()` checks the node store and fetch packs for the header, then both SHAMap roots, working entirely from local storage before any peer contact. + +## Destructor Salvage + +The destructor examines `mReceivedData` for any unprocessed account-state nodes (`liAS_NODE`) and routes them to `InboundLedgers::gotStaleData()`. Account state nodes are generic — they may be valid for other ledger acquisitions — so discarding them would waste data already paid for in network bandwidth. + +## Relationship to `InboundLedgers` + +`InboundLedgers` is the owning registry (a map from hash to `shared_ptr`). It handles deduplication — if the same hash is requested a second time while an acquisition is already in flight, `update()` is called on the existing instance to refresh its sequence number and `mLastAction` timestamp. The `touch()` / `mLastAction` mechanism enables `InboundLedgers::sweep()` to evict stale completed or failed acquisitions without disturbing active ones. \ No newline at end of file diff --git a/src/xrpld/app/ledger/InboundLedgers.h.ai.json b/src/xrpld/app/ledger/InboundLedgers.h.ai.json new file mode 100644 index 0000000000..5ef042cc8a --- /dev/null +++ b/src/xrpld/app/ledger/InboundLedgers.h.ai.json @@ -0,0 +1,43 @@ +{ + "args": [ + { + "lineno": 62, + "name": "app" + }, + { + "lineno": 63, + "name": "clock" + }, + { + "lineno": 64, + "name": "collector" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "InboundLedgers" + } + ], + "description": "Defines the InboundLedgers interface for managing the lifetime and acquisition of inbound ledgers in the XRPL system.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/InboundLedgers.h", + "functions": [ + { + "args": [ + "app", + "clock", + "collector" + ], + "lineno": 61, + "name": "make_InboundLedgers" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/InboundLedgers.h.ai.md b/src/xrpld/app/ledger/InboundLedgers.h.ai.md new file mode 100644 index 0000000000..21a11c0d87 --- /dev/null +++ b/src/xrpld/app/ledger/InboundLedgers.h.ai.md @@ -0,0 +1,41 @@ +# `InboundLedgers.h` — Ledger Acquisition Manager Interface + +This header defines the abstract interface governing how a rippled node fetches ledgers it does not yet hold locally. Whenever a node is catching up from behind, handling a consensus round for which it lacks the prior ledger, or backfilling historical data, it must request and reassemble ledger data from network peers. `InboundLedgers` is the central coordinator for that process: it owns the map of in-flight `InboundLedger` acquisitions, tracks recently-failed attempts, and exposes the lifecycle hooks that the rest of the application uses to interact with active fetches. + +## Two Acquisition Entry Points + +The interface deliberately offers two paths for requesting a ledger: `acquire()` and `acquireAsync()`. + +`acquire()` is designed for callers that may need an immediate answer. It looks up or creates an `InboundLedger` entry and — if that ledger happens to already be complete — returns a `shared_ptr` right away. If acquisition is still in progress or has failed, it returns `nullptr`. The implementation wraps the entire operation with `perf::measureDurationAndLog`, flagging calls that take longer than 500ms as slow. + +`acquireAsync()` is explicitly for callers already executing on the Job Queue. It adds the hash to a `pendingAcquires_` set under its own mutex, then calls through to `acquire()`. The set acts as a deduplication guard: if the same hash is already pending, it returns without doing anything. This matters because Job Queue tasks can be dispatched concurrently, and without this gate they could create redundant `InboundLedger` objects racing to initialize. The header comment notes that it may eventually be possible to migrate all callers to this path — an acknowledgment that the two-path design is partly historical and partly a pragmatic performance split. + +## Find vs. Acquire + +`find()` is separate from `acquire()` by design: it looks up an existing in-flight acquisition without triggering a new one. This prevents callers that merely want to check progress — such as routing incoming peer data to the right `InboundLedger` — from accidentally spawning new network requests. + +## Routing Incoming Peer Data + +`gotLedgerData()` is called by the networking layer when a peer delivers a `TMLedgerData` message. The implementation uses `find()` to locate the matching in-flight acquisition and calls `InboundLedger::gotData()` on it. If `gotData()` returns true (indicating the data is fresh and processing hasn't been dispatched yet), a `jtLEDGER_DATA` job is enqueued to run `InboundLedger::runData()` on the Job Queue. + +The more interesting case is when `find()` returns nothing — the acquisition has already completed or been swept. Rather than discarding the late data entirely, `gotLedgerData()` checks whether the packet carries state node data (`liAS_NODE`). If so, it schedules a call to `gotStaleData()`, which parses each `SHAMapTreeNode` from the wire format, re-serializes it in prefix format, and stashes it in the `LedgerMaster`'s fetch pack cache. The reasoning: the node already paid the bandwidth cost to receive the data, so it might as well be cached in case a future acquisition or historical backfill can use it. The existing `VFALCO TODO` in the header flags the direct `Peer` dependency in `gotLedgerData()` as a design smell — ideally this layer would operate only on protocol messages, not peer handles. + +## Failure Tracking + +`logFailure()`, `isFailure()`, and `clearFailures()` exist because the network cannot always deliver every requested ledger. Without failure bookkeeping the system would repeatedly retry undeliverable hashes. Internally, `InboundLedgersImp` uses a `beast::aged_map` named `mRecentFailures`. Entries expire after `kReacquireInterval` (five minutes), so a failed hash will eventually become eligible for retry. `isFailure()` calls `beast::expire()` on the map before doing its lookup, meaning stale failures are pruned lazily on each check rather than requiring a separate timer-driven cleanup pass. + +`clearFailures()` also clears `mLedgers` entirely — a notable side effect that makes it a heavier operation than its name suggests, used during shutdown or when a fundamental state reset is needed. + +## Fetch Rate Tracking + +`fetchRate()` returns the rate of completed historical ledger fetches in fetches-per-minute. The underlying measurement is a `DecayWindow<30>` (a 30-second exponentially-decaying sample), and the value is scaled by 60 to express it per minute. The `onLedgerFetched()` method increments this counter; its comment notes it should only be called for `InboundLedger::Reason::HISTORY`, keeping the metric focused on catch-up throughput rather than consensus-driven fetches. + +## Housekeeping + +`sweep()` evicts acquisitions that have been idle for more than one minute, collecting them into a local vector before erasing from the map so that their destructors run outside the lock. Entries that were last active in the future (a clock-skew guard) have their timestamps touched forward. The method also calls `beast::expire()` on `mRecentFailures` to prune stale failure records. `stop()` sets a `stopping_` flag that causes all subsequent `acquire()` calls to return immediately, then clears both the ledger map and failure map. + +`gotFetchPack()` is called when new fetch pack data becomes available in the `LedgerMaster`. It snapshot-copies the current `mLedgers` map under the lock, then calls `checkLocal()` on every in-flight `InboundLedger` outside the lock, giving each acquisition a chance to satisfy itself from the newly-arrived data without holding the global mutex. + +## Factory and Interface Design + +`make_InboundLedgers()` is the sole public construction path, returning a `unique_ptr` to the concrete `InboundLedgersImp`. This follows the standard XRPL pattern of pairing an abstract interface with a hidden implementation, enabling test injection via mock implementations and shielding consumers from internal data structure choices. The factory accepts an `Application&` for access to shared subsystems (Job Queue, LedgerMaster, NetworkOPs), a `beast::abstract_clock` for injectable time (enabling deterministic tests), and a `beast::insight::Collector` for emitting the `ledger_fetches` counter to the configured metrics backend. \ No newline at end of file diff --git a/src/xrpld/app/ledger/InboundTransactions.h.ai.json b/src/xrpld/app/ledger/InboundTransactions.h.ai.json new file mode 100644 index 0000000000..19788fc054 --- /dev/null +++ b/src/xrpld/app/ledger/InboundTransactions.h.ai.json @@ -0,0 +1,67 @@ +{ + "args": [ + { + "lineno": 27, + "name": "setHash" + }, + { + "lineno": 27, + "name": "acquire" + }, + { + "lineno": 36, + "name": "setHash" + }, + { + "lineno": 37, + "name": "peer" + }, + { + "lineno": 38, + "name": "message" + }, + { + "lineno": 46, + "name": "setHash" + }, + { + "lineno": 46, + "name": "set" + }, + { + "lineno": 46, + "name": "acquired" + }, + { + "lineno": 53, + "name": "seq" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "InboundTransactions" + } + ], + "description": "Defines the InboundTransactions interface for managing the acquisition and lifetime of transaction sets in the XRPL system, including methods for retrieving, adding, and managing transaction sets, as well as a factory function for creating instances.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/InboundTransactions.h", + "functions": [ + { + "args": [ + "app", + "collector", + "gotSet" + ], + "lineno": 61, + "name": "make_InboundTransactions" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/InboundTransactions.h.ai.md b/src/xrpld/app/ledger/InboundTransactions.h.ai.md new file mode 100644 index 0000000000..fa053515ec --- /dev/null +++ b/src/xrpld/app/ledger/InboundTransactions.h.ai.md @@ -0,0 +1,41 @@ +# `InboundTransactions.h` — Transaction Set Acquisition Interface + +## Role in the System + +`InboundTransactions` is the abstract interface that manages the acquisition and lifetime of *transaction sets* — the `SHAMap`-backed collections of transactions that the XRPL consensus algorithm exchanges between validators. During consensus, a validator may reference a transaction set by its hash that it has not yet seen locally. This class is the mechanism by which a node fetches those sets from peers and holds them long enough for the consensus round to complete. + +The header defines a non-copyable abstract base class and the `make_InboundTransactions` factory function that constructs the concrete implementation in `detail/InboundTransactions.cpp`. This pattern keeps the compilation boundary clean: callers depend only on the interface, not on `TransactionAcquire`, `PeerSet`, or the internal `hash_map` bookkeeping. + +## Interface Design + +The three core virtual methods map directly onto the lifecycle events of a transaction set during consensus: + +**`getSet(setHash, acquire)`** is the main read path. When `acquire` is true and the set is unknown, the implementation immediately creates a `TransactionAcquire` object and begins asking peers for the missing SHAMap nodes, then returns `nullptr` to the caller. The caller must call `getSet` again (or wait for the `gotSet` callback) rather than blocking. This non-blocking design is deliberate: consensus is event-driven, and blocking here would stall the entire consensus state machine. + +**`gotData(setHash, peer, message)`** feeds inbound `TMLedgerData` network messages into the appropriate `TransactionAcquire` session. It validates each SHAMap node in the message and charges the sending peer via `Resource::feeInvalidData` / `feeUselessData` / `feeMalformedRequest` if the data is bad or redundant. This peer-charging logic acts as a lightweight DoS defense at the point where untrusted data enters the acquisition pipeline. + +**`giveSet(setHash, set, acquired)`** is the write path, used for both externally acquired sets (routed back by `TransactionAcquire::done()`) and locally constructed ones (e.g., a validator's own proposal). The `acquired` flag is forwarded to the `gotSet` callback so the consensus engine can distinguish whether a set arrived from the network or was built locally. If the map entry already holds a set (duplicate delivery), `giveSet` silently discards the duplicate and does not fire the callback again — deduplication is structural rather than explicit. + +**`newRound(seq)`** is the eviction hook. The implementation stores a sequence number alongside each cached set and, on each new consensus round, discards any entry whose sequence is more than `setKeepRounds = 3` rounds away from the current sequence. This sliding window prevents unbounded memory growth while keeping sets available for validators running slightly behind or ahead. The zero-hash set (the canonical empty transaction set) receives special protection: `newRound` always refreshes its sequence to prevent it from expiring, since consensus may reference it legally at any time. + +## Internal Architecture (from `detail/InboundTransactions.cpp`) + +The concrete class `InboundTransactionsImp` owns a `hash_map` protected by a `std::recursive_mutex`. The `InboundTransactionSet` struct holds three things: the ledger sequence at which this entry was last touched (`mSeq`), a completed `std::shared_ptr` (`mSet`), and an optional `TransactionAcquire::pointer` (`mAcquire`) that represents a pending network fetch. Once `mSet` is populated, `mAcquire` is reset — the two states are mutually exclusive in practice. + +The `gotSet` callback injected at construction time (type `std::function const&, bool)>`) is how `InboundTransactions` notifies the consensus engine (specifically `RCLConsensus::Adaptor`) that a newly-complete set is available. The boolean parameter signals whether the set came from a peer acquisition. In the consensus adapter, `acquireTxSet()` calls `getSet(id, true)` and expects to receive either an immediately-available set or `nullptr` while the async fetch is in progress; the `gotSet` callback re-enters the consensus state machine when the fetch completes. + +## Relationship to `RCLConsensus::Adaptor` + +The consensus layer interacts with this interface in three places: + +- `Adaptor::acquireTxSet()` calls `getSet()` — the primary lookup path. +- `Adaptor::share()` calls `giveSet(txns.id(), txns.map_, false)` — publishing a locally-built proposal to the cache so peers can request it. +- `startRound()` and `acquireLedger()` call `newRound(seq)` — advancing the cache's expiry window when consensus moves to a new ledger sequence. + +## Stop Semantics + +`stop()` sets an internal `stopping_` flag and clears the entire map under the lock. Once stopped, `getSet(..., true)` will not initiate new acquisitions even if `acquire` is true. This prevents new peer requests from being issued during teardown, when the `Application` and its peer subsystem may be partially destroyed. + +## Design Tradeoffs + +The use of `std::recursive_mutex` (rather than `std::mutex`) signals that some call paths — particularly through `TransactionAcquire`'s completion callback back into `giveSet` — were anticipated to re-enter the lock from the same thread. The abstract base plus factory pattern allows unit tests to substitute a mock without dragging in the full network acquisition machinery. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerCleaner.h.ai.json b/src/xrpld/app/ledger/LedgerCleaner.h.ai.json new file mode 100644 index 0000000000..ae1b42a966 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerCleaner.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [ + { + "lineno": 32, + "name": "parameters" + }, + { + "lineno": 44, + "name": "app" + }, + { + "lineno": 44, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "LedgerCleaner" + } + ], + "description": "Defines the LedgerCleaner interface for checking and cleaning ledger/transaction database continuity, including an abstract class and a factory function.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerCleaner.h", + "functions": [ + { + "args": [ + "app", + "journal" + ], + "lineno": 44, + "name": "make_LedgerCleaner" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerCleaner.h.ai.md b/src/xrpld/app/ledger/LedgerCleaner.h.ai.md new file mode 100644 index 0000000000..968be42020 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerCleaner.h.ai.md @@ -0,0 +1,41 @@ +# `LedgerCleaner.h` — Interface for Ledger/Transaction Database Continuity Maintenance + +## Role in the System + +The XRP Ledger node maintains two distinct persistence layers: a key-value node store (SHAMap nodes) and SQLite-backed relational databases for account and transaction history. Over time — through crashes, incomplete syncs, or bugs in older software versions — these stores can develop inconsistencies: missing SHAMap nodes, SQL rows referencing wrong ledger hashes, or gaps in ledger history. `LedgerCleaner.h` defines the abstract interface for a dedicated background service that detects and repairs these inconsistencies without blocking normal ledger operation. + +## Interface Design and Inheritance + +`LedgerCleaner` inherits from `beast::PropertyStream::Source`, registered under the fixed name `"ledgercleaner"`. This is a recurring XRPL pattern: subsystems that need operational visibility inherit from `PropertyStream::Source` so they can expose runtime state through the server's diagnostic introspection system — the `get_counts` RPC command and similar tools — with no additional wiring at the call site. The `onWrite()` implementation in `LedgerCleanerImp` publishes current `status`, `min_ledger`, `max_ledger`, `check_nodes`, `fix_txns`, and a `fail_counts` counter under the lock, giving operators a live view of cleaning progress. + +The three pure virtual methods — `start()`, `stop()`, and `clean()` — model an explicit lifecycle. The concrete `LedgerCleanerImp` creates a single dedicated `std::thread` named `"LedgerCleaner"` in `start()`, blocks it on a `std::condition_variable`, and tears it down gracefully in `stop()` by setting `shouldExit_` and joining the thread. The destructor asserts that `stop()` was called before destruction, catching misuse at runtime via `LogicError`. + +## Non-blocking `clean()` and JSON Parameters + +`clean()` is documented as non-blocking: it configures the cleaning task and signals the worker thread via `wakeup_.notify_one()` but returns immediately. This is essential because cleaning can involve scanning hundreds of thousands of ledger sequences with I/O-intensive SHAMap walks and database comparisons — invoking that work synchronously on any consensus or RPC thread would be catastrophic. + +The method accepts a `Json::Value` rather than a typed parameter struct. This reflects the standard XRPL pattern for admin commands: the JSON arrives from the RPC layer with minimal transformation, and the implementation interprets well-known keys (`ledger`, `min_ledger`, `max_ledger`, `full`, `fix_txns`, `check_nodes`, `stop`) directly from the payload. A single-ledger shortcut exists: providing just `"ledger"` forces both `fixTxns_` and `checkNodes_` true, since a single-ledger repair is likely being triggered precisely because that ledger is known to be broken. The `"stop"` key sets both range bounds to zero, causing the worker loop to return after its current unit of work. + +The RPC handler in `rpc/handlers/admin/data/LedgerCleaner.cpp` is a trivial one-liner: it calls `context.app.getLedgerCleaner().clean(context.params)` and returns a status string. The interface absorbs the full JSON without further parsing at the handler layer. + +## The Worker Loop and Concurrency Model + +The worker thread in `doLedgerCleaner()` iterates from `maxRange_` down to `minRange_`, processing one ledger per iteration. Before each ledger it checks `app_.getFeeTrack().isLoadedLocal()` — if the server is under high load, the cleaner backs off with a five-second sleep. On each successful ledger, it pauses 100 ms to reduce I/O pressure. On failure (hash not found, or `doLedger()` returns false) it sleeps two seconds to allow `InboundLedgers` to make progress before retrying the same ledger. This adaptive pacing prevents the cleaner from competing with consensus or network I/O during busy periods. + +All shared state (`minRange_`, `maxRange_`, `checkNodes_`, `fixTxns_`, `failures_`, `state_`, `shouldExit_`) is guarded by `mutex_`. The worker acquires the lock to snapshot parameters at the start of each iteration, then releases it before doing any I/O work, keeping the critical section narrow. + +## What `doLedger()` Actually Checks + +The per-ledger cleaning logic in `doLedger()` performs three distinct validations: + +1. **SQL database consistency**: It loads the ledger from the SQL index via `loadByIndex()` and compares the resulting header hash and parent hash against the authoritative node-store version. Any mismatch sets `doTxns = true`, triggering a rewrite via `pendSaveValidated()`. + +2. **History index integrity**: `LedgerMaster::fixIndex()` verifies the ledger's entry in the history table and corrects it if wrong. + +3. **SHAMap node completeness** (when `checkNodes_` is set): `nodeLedger->walkLedger()` traverses every node in the ledger's state and transaction trees. If any node is missing, the ledger is evicted from the master cache via `clearLedger()` and re-acquired via `InboundLedgers::acquire()`. + +To resolve the hash of a target ledger, `getHash()` walks backward through the reference ledger's skip list using `hashOfSeq()`. If the target is too far back for the current validated ledger's skip list to reach directly, it uses `getCandidateLedger()` to find an intermediate ledger that holds the needed hash, acquires that ledger first, then looks up the target. This handles arbitrarily deep history while minimizing node-store lookups. + +## Factory and Ownership + +`make_LedgerCleaner()` is the sole construction path. It takes `Application&` — the top-level context providing access to `LedgerMaster`, `InboundLedgers`, `FeeTrack`, and the journal system — and returns a `std::unique_ptr`. This factory pattern keeps the concrete `LedgerCleanerImp` class entirely invisible to callers and confines the heavy include surface (`LedgerMaster.h`, `InboundLedgers.h`, `LedgerPersistence.h`) to the implementation translation unit. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerHistory.cpp.ai.json b/src/xrpld/app/ledger/LedgerHistory.cpp.ai.json new file mode 100644 index 0000000000..4829429de2 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerHistory.cpp.ai.json @@ -0,0 +1,411 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LedgerHistory::insert" + ], + "entry_point": "LedgerHistory::insert", + "purpose": "Insert a ledger into the cache, optionally marking it as validated.", + "validation_points": [ + "ledger->isImmutable() (LogicError if false)", + "ledger->stateMap().getHash().isNonZero() (XRPL_ASSERT)" + ] + }, + { + "call_chain": [ + "LedgerHistory::getLedgerBySeq", + "loadByIndex (if not found in cache)" + ], + "entry_point": "LedgerHistory::getLedgerBySeq", + "purpose": "Retrieve a ledger by its sequence number, loading from storage if not cached.", + "validation_points": [ + "ret->header().seq == index (XRPL_ASSERT)", + "ret->isImmutable() (XRPL_ASSERT)" + ] + }, + { + "call_chain": [ + "LedgerHistory::getLedgerByHash", + "loadByHash (if not found in cache)" + ], + "entry_point": "LedgerHistory::getLedgerByHash", + "purpose": "Retrieve a ledger by its hash, loading from storage if not cached.", + "validation_points": [ + "ret->isImmutable() (XRPL_ASSERT, both after fetch and after load)", + "ret->header().hash == hash (XRPL_ASSERT, both after fetch and after load)" + ] + } + ], + "data_flows": [ + { + "field": "ledger (std::shared_ptr)", + "flow": [ + "insert argument", + "isImmutable() check", + "stateMap().getHash().isNonZero() check", + "m_ledgers_by_hash.canonicalize_replace_cache", + "mLedgersByIndex[ledger->header().seq] = ledger->header().hash (if validated)" + ], + "origin": "Passed as argument to LedgerHistory::insert", + "transformations": [ + "Checked for immutability", + "Checked for nonzero state map hash", + "Inserted into cache and index" + ], + "validated_at": "LedgerHistory::insert" + }, + { + "field": "ret (std::shared_ptr)", + "flow": [ + "loadByIndex/loadByHash", + "ret->header().seq == index (getLedgerBySeq)", + "ret->isImmutable() (getLedgerBySeq/getLedgerByHash)", + "m_ledgers_by_hash.canonicalize_replace_client", + "mLedgersByIndex[ret->header().seq] = ret->header().hash" + ], + "origin": "Result of loadByIndex or loadByHash", + "transformations": [ + "Loaded from storage", + "Validated for correct sequence/hash and immutability", + "Inserted into cache and index" + ], + "validated_at": "LedgerHistory::getLedgerBySeq, LedgerHistory::getLedgerByHash" + }, + { + "field": "mLedgersByIndex", + "flow": [ + "insert/getLedgerBySeq/getLedgerByHash", + "mLedgersByIndex[seq] = hash", + "Used in getLedgerHash and getLedgerBySeq" + ], + "origin": "LedgerHistory member, updated in insert/getLedgerBySeq/getLedgerByHash", + "transformations": [ + "Mapping from sequence to hash is updated" + ], + "validated_at": "Indirectly validated via ledger immutability and hash/seq checks" + } + ], + "description": "Implements the LedgerHistory class for managing, tracking, and analyzing ledgers by hash and sequence in the XRPL server, including mismatch detection and logging.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger (immutability)", + "empty", + "string", + "validation" + ], + "evidence": "isImmutable() method + LogicError exception at LedgerHistory::insert", + "issue_pattern": "Missing empty string validation for ledger (immutability)", + "why_false_positive": "isImmutable() method + LogicError exception validates ledger (immutability) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger->stateMap().getHash()", + "empty", + "string", + "validation" + ], + "evidence": "isNonZero() method + XRPL_ASSERT macro at LedgerHistory::insert", + "issue_pattern": "Missing empty string validation for ledger->stateMap().getHash()", + "why_false_positive": "isNonZero() method + XRPL_ASSERT macro validates ledger->stateMap().getHash() for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger->stateMap().getHash()", + "format", + "validation", + "invalid" + ], + "evidence": "isNonZero() method + XRPL_ASSERT macro at LedgerHistory::insert", + "issue_pattern": "Missing format validation for ledger->stateMap().getHash()", + "why_false_positive": "isNonZero() method + XRPL_ASSERT macro validates ledger->stateMap().getHash() format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ret->header().seq == index", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at LedgerHistory::getLedgerBySeq", + "issue_pattern": "Missing empty string validation for ret->header().seq == index", + "why_false_positive": "XRPL_ASSERT macro validates ret->header().seq == index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ret->isImmutable()", + "empty", + "string", + "validation" + ], + "evidence": "isImmutable() method + XRPL_ASSERT macro at LedgerHistory::getLedgerBySeq", + "issue_pattern": "Missing empty string validation for ret->isImmutable()", + "why_false_positive": "isImmutable() method + XRPL_ASSERT macro validates ret->isImmutable() for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerHistory.cpp", + "functions": [ + { + "args": [ + "ReadView const& ledger", + "uint256 const& tx", + "char const* msg", + "beast::Journal& j" + ], + "lineno": 109, + "name": "log_one" + }, + { + "args": [ + "ReadView const& builtLedger", + "ReadView const& validLedger", + "uint256 const& tx", + "beast::Journal j" + ], + "lineno": 122, + "name": "log_metadata_difference" + }, + { + "args": [ + "SHAMap const& sm" + ], + "lineno": 202, + "name": "leaves" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "bool validated" + ], + "lineno": 38, + "name": "LedgerHistory::insert" + }, + { + "args": [ + "LedgerIndex index" + ], + "lineno": 54, + "name": "LedgerHistory::getLedgerHash" + }, + { + "args": [ + "LedgerIndex index" + ], + "lineno": 61, + "name": "LedgerHistory::getLedgerBySeq" + }, + { + "args": [ + "LedgerHash const& hash" + ], + "lineno": 92, + "name": "LedgerHistory::getLedgerByHash" + }, + { + "args": [ + "LedgerHash const& built", + "LedgerHash const& valid", + "std::optional const& builtConsensusHash", + "std::optional const& validatedConsensusHash", + "Json::Value const& consensus" + ], + "lineno": 217, + "name": "LedgerHistory::handleMismatch" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "uint256 const& consensusHash", + "Json::Value consensus" + ], + "lineno": 312, + "name": "LedgerHistory::builtLedger" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "std::optional const& consensusHash" + ], + "lineno": 340, + "name": "LedgerHistory::validatedLedger" + }, + { + "args": [ + "LedgerIndex ledgerIndex", + "LedgerHash const& ledgerHash" + ], + "lineno": 368, + "name": "LedgerHistory::fixIndex" + }, + { + "args": [ + "LedgerIndex seq" + ], + "lineno": 382, + "name": "LedgerHistory::clearLedgerCachePrior" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "LedgerHistory is a core component, so it is likely tested in integration and unit tests in the rippled codebase. Typical test files would be in 'src/test/ledger/' such as LedgerHistory_test.cpp, Ledger_test.cpp, or LedgerCache_test.cpp. However, the specific validation logic (immutability, nonzero hash, sequence/hash match) may not be directly unit tested for all error paths (e.g., mutable ledgers, zero hashes, mismatched seq/hash). There may be gaps in negative-path testing (ensuring exceptions/asserts fire as expected).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro, LogicError exception, isImmutable() method", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LogicError", + "field": "ledger (immutability)", + "location": "LedgerHistory::insert", + "validated_by": "isImmutable() method + LogicError exception", + "validates": [ + "Ensures the ledger passed to insert is immutable" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "ledger->stateMap().getHash()", + "location": "LedgerHistory::insert", + "validated_by": "isNonZero() method + XRPL_ASSERT macro", + "validates": [ + "Ensures the state map hash of the ledger is non-zero" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "ret->header().seq == index", + "location": "LedgerHistory::getLedgerBySeq", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures the loaded ledger's sequence matches the requested index" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "ret->isImmutable()", + "location": "LedgerHistory::getLedgerBySeq", + "validated_by": "isImmutable() method + XRPL_ASSERT macro", + "validates": [ + "Ensures the loaded ledger is immutable before caching" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerHistory.cpp.ai.md b/src/xrpld/app/ledger/LedgerHistory.cpp.ai.md new file mode 100644 index 0000000000..bbcbbbc12e --- /dev/null +++ b/src/xrpld/app/ledger/LedgerHistory.cpp.ai.md @@ -0,0 +1,47 @@ +# `LedgerHistory.cpp` — Ledger Cache and Consensus Mismatch Detector + +`LedgerHistory` sits at the intersection of two concerns that happen to share data: serving historical ledgers quickly via an in-memory cache, and detecting when this node's locally-built ledger disagrees with the one the network has validated. The class is consumed primarily by `LedgerMaster` and the consensus subsystem; operators see its effects through the `ledger.history.mismatch` metric and the diagnostic log output it produces on divergence events. + +## Cache Architecture + +The class maintains two `TaggedCache` instances and one unbounded `std::map`. + +`m_ledgers_by_hash` is the primary cache, mapping `LedgerHash → Ledger const`. Its capacity and TTL are drawn from application configuration (`SizedItem::ledgerSize` and `SizedItem::ledgerAge`), so they scale with the node's resource profile. Lookups first check the cache; on a miss, `loadByHash` or `loadByIndex` pull the ledger from the database and populate the cache for future access. + +`mLedgersByIndex` is a plain `std::map` that records only validated ledgers — it is the index-to-hash mapping needed to answer "what hash does sequence N map to?" without touching the database. This map is not protected by its own mutex; instead it shares `m_ledgers_by_hash.peekMutex()`. Co-locating them under one lock prevents the sequence-to-hash mapping from diverging from the hash-to-ledger cache in concurrent scenarios. The acknowledged technical debt (`// FIXME: Need to clean up ledgers by index at some point`) is that `mLedgersByIndex` grows without bound — `clearLedgerCachePrior` prunes the hash cache but does not touch the index map. + +`m_consensus_validated` is a smaller, second `TaggedCache` (64 entries, 5-minute TTL) keyed on `LedgerIndex` and holding `cv_entry` structs. Each `cv_entry` stores the optional hash of the locally-built ledger, the optional hash of the validated ledger, both consensus transaction set hashes, and the consensus JSON snapshot for that round. This cache is the core data store for the mismatch detection machinery. + +## The Immutability Invariant + +Every ledger entering the history is required to be immutable. `insert()` enforces this with a hard `LogicError` (not a recoverable exception — this is a programming contract violation). `getLedgerBySeq()` and `getLedgerByHash()` assert immutability on ledgers loaded from the database before caching them. The reason is structural: `TaggedCache` permits shared ownership across multiple callers; if any caller could mutate a cached ledger, all concurrent readers would observe inconsistent state. By requiring immutability at the boundary, the class can safely distribute `shared_ptr` without further synchronization. + +## `insert()` — Cache Population + +`insert()` is the entry point when a new ledger arrives. It calls `canonicalize_replace_cache` on `m_ledgers_by_hash`, which means: if an object for this hash already exists in the cache, replace the cached copy with the incoming one (the caller's version "wins"). This is the right policy here because the insert caller typically holds a fresher, just-built ledger. If `validated` is true, the sequence-to-hash mapping in `mLedgersByIndex` is also recorded. The method returns `true` if the hash was already present, letting callers detect duplicate insertions. + +`getLedgerBySeq()` and `getLedgerByHash()` use `canonicalize_replace_client` on cache population after a database load: if the cache already holds a canonical instance, the caller's newly-loaded pointer is replaced with the cached one. This ensures that all code paths end up sharing a single object per unique ledger, avoiding redundant memory consumption. + +## Mismatch Detection: `builtLedger()` and `validatedLedger()` + +These two methods implement a rendezvous pattern. Each records its event in `m_consensus_validated` and checks whether the other event for the same sequence has already arrived. + +`builtLedger()` is called when local transaction processing for a round completes. It creates (or retrieves) a `cv_entry` for the sequence number, and if `validated` is already populated but `built` is not, it compares the two hashes. If they differ, `handleMismatch()` is triggered. The consensus hash and JSON snapshot of the round are stored in the entry regardless, so `validatedLedger()` can access them later. + +`validatedLedger()` mirrors this logic for the network-validation path. Because either event can arrive first — the node might validate before finishing local construction, or finish building before receiving the validation — both methods defensively check only the "other side is present and I am absent" condition before comparing. + +## `handleMismatch()` — Fault Triage + +When the built and validated hashes differ, `handleMismatch()` performs a structured triage to classify the failure mode: + +1. **Parent hash mismatch**: the two ledgers have different parents, indicating this node built on the wrong prior ledger (a sync or fork issue, not a determinism problem). +2. **Close time mismatch**: Byzantine agreement failure — validators chose a different close time, meaning the disagreement is at the consensus protocol level, not the execution engine. +3. **Consensus transaction set mismatch** (if both hashes are available): the transaction sets that were applied actually differ. If the transaction sets match, the error is more serious: same inputs produced different outputs. + +After classification, the method calls `leaves()` to extract the transaction maps of both ledgers as sorted vectors of `SHAMapItem` pointers, then walks the two sorted lists in parallel — a classic set-difference merge. Transactions present in one but not the other are logged via `log_one()`; transactions present in both but with different metadata (same tx, different outcome) are passed to `log_metadata_difference()`, which compares result code (`TER`), index, and state-change nodes independently to isolate exactly which field diverged. The mismatch counter `mismatch_counter_` is incremented every time the method is entered, making the event visible to external monitoring infrastructure through `beast::insight`. + +## `fixIndex()` and `clearLedgerCachePrior()` + +`fixIndex()` corrects a stale or wrong sequence-to-hash mapping, returning `false` to signal that a repair was actually performed. This is used when a more authoritative hash for a given ledger index arrives after the mapping was initially set — for example, when processing validation messages from peers. + +`clearLedgerCachePrior()` is a bulk eviction operation. It iterates all keys in `m_ledgers_by_hash`, loads each ledger by hash, and removes any whose sequence number falls below the given threshold. This is used when the node needs to shed old cache entries ahead of a targeted cleanup, complementing the TTL-based automatic sweeping done by `sweep()`. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerHistory.h.ai.json b/src/xrpld/app/ledger/LedgerHistory.h.ai.json new file mode 100644 index 0000000000..c842367a54 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerHistory.h.ai.json @@ -0,0 +1,155 @@ +{ + "args": [ + { + "lineno": 13, + "name": "collector" + }, + { + "lineno": 13, + "name": "app" + }, + { + "lineno": 20, + "name": "ledger" + }, + { + "lineno": 20, + "name": "validated" + }, + { + "lineno": 32, + "name": "ledgerIndex" + }, + { + "lineno": 36, + "name": "ledgerHash" + }, + { + "lineno": 72, + "name": "seq" + }, + { + "lineno": 78, + "name": "built" + }, + { + "lineno": 79, + "name": "valid" + }, + { + "lineno": 80, + "name": "builtConsensusHash" + }, + { + "lineno": 81, + "name": "validatedConsensusHash" + }, + { + "lineno": 82, + "name": "consensus" + } + ], + "classes": [ + { + "args": [ + "collector", + "app" + ], + "lineno": 12, + "name": "LedgerHistory" + } + ], + "description": "Defines the LedgerHistory class, which retains and manages historical ledgers, providing methods for tracking, retrieving, and maintaining ledger data by hash and sequence number.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerHistory.h", + "functions": [ + { + "args": [ + "ledger", + "validated" + ], + "lineno": 19, + "name": "insert" + }, + { + "args": [], + "lineno": 25, + "name": "getCacheHitRate" + }, + { + "args": [ + "ledgerIndex" + ], + "lineno": 31, + "name": "getLedgerBySeq" + }, + { + "args": [ + "ledgerHash" + ], + "lineno": 35, + "name": "getLedgerByHash" + }, + { + "args": [ + "ledgerIndex" + ], + "lineno": 40, + "name": "getLedgerHash" + }, + { + "args": [], + "lineno": 47, + "name": "sweep" + }, + { + "args": [ + "", + "consensusHash", + "" + ], + "lineno": 54, + "name": "builtLedger" + }, + { + "args": [ + "", + "consensusHash" + ], + "lineno": 58, + "name": "validatedLedger" + }, + { + "args": [ + "ledgerIndex", + "ledgerHash" + ], + "lineno": 66, + "name": "fixIndex" + }, + { + "args": [ + "seq" + ], + "lineno": 71, + "name": "clearLedgerCachePrior" + }, + { + "args": [ + "built", + "valid", + "builtConsensusHash", + "validatedConsensusHash", + "consensus" + ], + "lineno": 77, + "name": "handleMismatch" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerHistory.h.ai.md b/src/xrpld/app/ledger/LedgerHistory.h.ai.md new file mode 100644 index 0000000000..5a4ebb81d7 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerHistory.h.ai.md @@ -0,0 +1,42 @@ +# `LedgerHistory.h` — Historical Ledger Cache and Mismatch Detector + +`LedgerHistory` is the in-memory ledger registry for `LedgerMaster`. It serves two distinct purposes that, while related, operate largely independently: fast retrieval of historical ledgers by hash or sequence number, and a per-sequence consensus comparison mechanism that can detect Byzantine failures or determinism bugs in transaction processing. + +## Storage Architecture + +The class maintains three separate data structures with different lifetimes and purposes. + +`m_ledgers_by_hash` is a `TaggedCache` — the primary object store. Its capacity and TTL are drawn from `SizedItem::ledgerSize` and `SizedItem::ledgerAge` in the application config, making it scale with node memory settings. `TaggedCache` tracks objects both by strong (cached) and weak (tracked) pointers, so a ledger remains findable as long as any code holds a `shared_ptr` to it, even after it ages out of the bounded cache window. + +`mLedgersByIndex` is a plain `std::map` serving as a sequence-to-hash index for validated ledgers. Unlike the two caches, it is unbounded and never automatically pruned — a known issue called out with a `FIXME` comment in the `.cpp` file. This map is protected by reusing `m_ledgers_by_hash.peekMutex()` rather than maintaining its own mutex, tying its locking lifetime to the main cache. + +`m_consensus_validated` is a second `TaggedCache` capped at 64 entries with a 5-minute TTL. Each `cv_entry` stores optional hashes for the locally built ledger, the network-validated ledger, and the corresponding consensus transaction set hashes, plus the consensus metadata JSON. This cache exists solely for the mismatch-detection path. + +## Retrieval and Disk Fallback + +`getLedgerByHash()` first tries `m_ledgers_by_hash.fetch()`, then falls back to `loadByHash()` from `LedgerPersistence`, inserting the result back into cache on success. + +`getLedgerBySeq()` has a more careful locking sequence: it holds `m_ledgers_by_hash.peekMutex()` to look up the hash from `mLedgersByIndex`, then explicitly releases the lock before calling `getLedgerByHash()`. This avoids a re-entrant lock acquisition because `getLedgerByHash()` also acquires the same mutex internally. When neither cache has the answer, it calls `loadByIndex()` and registers the result in both the hash cache and the index map. + +Both retrieval paths enforce immutability with `XRPL_ASSERT(ret->isImmutable(), ...)`. The `insert()` path goes further — it raises a `LogicError` if a mutable ledger is presented. Only immutable ledgers are stored because `TaggedCache` explicitly prohibits modification of cached objects without external synchronization. + +## Consensus Mismatch Detection + +The `builtLedger()` and `validatedLedger()` methods record the outcomes of two separate events — the local node constructing a ledger from the consensus transaction set, and the network declaring a ledger fully validated. These events can arrive in either order, which the `cv_entry` struct accommodates with separate `built` and `validated` fields. + +When the second event arrives for a given sequence, `builtLedger()` or `validatedLedger()` compares the stored hash from the first event against the incoming hash. A match logs a `MATCH` debug entry and is the expected case. A divergence increments `mismatch_counter_` (a `beast::insight::Counter` visible to monitoring systems) and triggers `handleMismatch()`. + +`handleMismatch()` performs structured forensics to characterize the root cause: + +1. **Different parent hashes** — the built ledger started from a different prior ledger; this is a sync/acquisition issue. +2. **Different close times** — the two ledgers agreed on transactions but disagree on when the ledger closed; this indicates a Byzantine actor misreporting timestamps. +3. **Different consensus transaction sets** — the hash of the agreed-upon transaction set differs, meaning the node processed different transactions than the network majority. +4. **Same transaction set, different outcome** — both ledgers contain identical transactions but produce different state. This is the most serious case: a determinism bug. `handleMismatch()` performs a sorted symmetric merge of the two ledgers' `txMap` leaves to identify transactions present in one but not the other, and for transactions present in both but with differing data, `log_metadata_difference()` drills into `TxMeta` to compare result codes, ledger entry indices, and SHAMap node lists. + +The `leaves()` helper function extracts and sorts `SHAMapItem` pointers from a transaction map so the two-pointer merge loop can run in O(n) time over both sets simultaneously. + +## Integration with LedgerMaster + +`LedgerHistory` is a non-public data member of `LedgerMaster` and is not exposed as an interface on its own. All calls in `LedgerMaster.cpp` go through thin delegation methods (`addLedger()`, `getLedgerBySeq()`, etc.) that simply forward to the corresponding `LedgerHistory` methods. The `sweep()` method is called periodically from `LedgerMaster`'s sweep logic and delegates to both `TaggedCache` instances. `clearLedgerCachePrior()` removes entries from `m_ledgers_by_hash` for ledgers older than a given sequence — notably it does not clean `mLedgersByIndex`, which is the source of the lingering FIXME. + +The `fixIndex()` method corrects a stale index mapping: if `mLedgersByIndex[ledgerIndex]` holds the wrong hash, it is updated in-place and `false` is returned to signal that a repair was needed. This is used during ledger acquisition when a previously cached mapping turns out to have been for an uncommitted fork. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerHolder.h.ai.json b/src/xrpld/app/ledger/LedgerHolder.h.ai.json new file mode 100644 index 0000000000..01a1993ab0 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerHolder.h.ai.json @@ -0,0 +1,38 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 16, + "name": "LedgerHolder" + } + ], + "description": "Provides a thread-safe holder for an immutable ledger using a mutex for synchronization. Ensures only immutable ledgers are held and offers set/get/empty operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerHolder.h", + "functions": [ + { + "args": [ + "ledger" + ], + "lineno": 20, + "name": "set" + }, + { + "args": [], + "lineno": 31, + "name": "get" + }, + { + "args": [], + "lineno": 37, + "name": "empty" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerHolder.h.ai.md b/src/xrpld/app/ledger/LedgerHolder.h.ai.md new file mode 100644 index 0000000000..d7f72bd877 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerHolder.h.ai.md @@ -0,0 +1,37 @@ +# `LedgerHolder.h` — Thread-Safe Immutable Ledger Slot + +`LedgerHolder` is a small synchronization wrapper used by `LedgerMaster` to safely share ledger snapshots across threads. Its entire job is to guarantee that one canonical `shared_ptr` — always immutable, never null once set — can be written by one thread and read by many others without data races. + +## Context: Why This Class Exists + +`LedgerMaster` maintains two of these holders as private members: + +```cpp +LedgerHolder mClosedLedger; // the most recently closed ledger +LedgerHolder mValidLedger; // the highest fully-validated ledger +``` + +These ledgers are produced by the consensus engine and then consumed concurrently by path-finding, RPC handlers, peer-fetching logic, and the validation pipeline. Rather than exposing a raw `shared_ptr` protected by `LedgerMaster`'s own `std::recursive_mutex`, the design isolates the locking concern inside `LedgerHolder`. Each holder carries its own dedicated `std::mutex`, which prevents contention on the coarser `m_mutex` for what is ultimately a pointer copy. + +`get()` returns a full copy of the `shared_ptr`, not a reference, so the caller holds an independent owning handle. This is critical: if the holder is updated (i.e., `set()` is called from the consensus thread) while another thread is in the middle of processing the old ledger, the old ledger object remains alive until all callers release their copies. There are no dangling references. + +## Invariants Enforced at Write Time + +`set()` enforces two hard preconditions and terminates via `LogicError` — a fatal, unrecoverable error — if either is violated: + +1. **Non-null**: passing a null `shared_ptr` is a programming error; `LedgerHolder` is not designed to represent "no ledger" after the first assignment. +2. **Immutable**: only ledgers on which `isImmutable()` returns true may be stored. The XRPL codebase uses mutability as a lifecycle marker: a ledger being built is mutable; once closed and hashed, it is sealed. Storing a mutable ledger would allow concurrent modification through the returned pointer, breaking thread safety without any lock. + +This pattern mirrors `LedgerHistory` and `RCLValidations`, both of which enforce the same immutability contract with equal severity. + +## Lock Strategy and a Noted Future Path + +All three public methods — `set()`, `get()`, and `empty()` — take a `std::lock_guard` on the internal `m_lock` for the duration of the operation. The file's own comment acknowledges that `std::atomic>` (available since C++20) could make this class lock-free entirely. As of the current codebase the mutex remains, presumably for compatibility or because the simpler approach was "good enough" given the low contention expected on what are effectively rare write operations (ledger close is an infrequent event). + +## Diagnostics via `CountedObject` + +`LedgerHolder` inherits from `CountedObject`, which uses a lock-free linked list of static `Counter` instances to track how many `LedgerHolder` objects are alive at any point. This costs nothing at runtime beyond two atomic increments over the object's lifetime and enables the `getCounts()` diagnostic path to report live instance counts, useful for detecting leaks in testing. + +## Design Summary + +`LedgerHolder` is intentionally minimal — three methods, one mutex, one `shared_ptr`. Its value lies not in complexity but in colocation: it bundles the immutability contract, the null guard, and the mutex into a single reusable unit, preventing callers from forgetting any of the three. The alternative — inline `std::mutex` members and ad hoc checks in `LedgerMaster` — would scatter identical boilerplate across each ledger slot, making future refactoring (e.g., switching to `std::atomic`) harder to apply consistently. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerMaster.h.ai.json b/src/xrpld/app/ledger/LedgerMaster.h.ai.json new file mode 100644 index 0000000000..59cabafe5a --- /dev/null +++ b/src/xrpld/app/ledger/LedgerMaster.h.ai.json @@ -0,0 +1,507 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application&", + "Stopwatch&", + "beast::insight::Collector::ptr const&", + "beast::Journal" + ], + "lineno": 25, + "name": "LedgerMaster" + }, + { + "args": [ + "Handler const&", + "beast::insight::Collector::ptr const&" + ], + "lineno": 241, + "name": "Stats" + } + ], + "description": "Defines the LedgerMaster class, which manages the current, closed, and validated ledgers, tracks ledger history, handles held transactions, and manages fetch packs in the XRPL server.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerMaster.h", + "functions": [ + { + "args": [], + "lineno": 32, + "name": "getCurrentLedgerIndex" + }, + { + "args": [], + "lineno": 34, + "name": "getValidLedgerIndex" + }, + { + "args": [ + "ReadView const&", + "beast::Journal::Stream", + "char const*" + ], + "lineno": 36, + "name": "isCompatible" + }, + { + "args": [], + "lineno": 38, + "name": "peekMutex" + }, + { + "args": [], + "lineno": 41, + "name": "getCurrentLedger" + }, + { + "args": [], + "lineno": 45, + "name": "getClosedLedger" + }, + { + "args": [], + "lineno": 50, + "name": "getValidatedLedger" + }, + { + "args": [], + "lineno": 53, + "name": "getValidatedRules" + }, + { + "args": [], + "lineno": 58, + "name": "getPublishedLedger" + }, + { + "args": [], + "lineno": 61, + "name": "getPublishedLedgerAge" + }, + { + "args": [], + "lineno": 62, + "name": "getValidatedLedgerAge" + }, + { + "args": [ + "std::string&" + ], + "lineno": 63, + "name": "isCaughtUp" + }, + { + "args": [], + "lineno": 66, + "name": "getEarliestFetch" + }, + { + "args": [ + "std::shared_ptr" + ], + "lineno": 68, + "name": "storeLedger" + }, + { + "args": [ + "std::shared_ptr const&", + "bool", + "bool" + ], + "lineno": 71, + "name": "setFullLedger" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 77, + "name": "canBeCurrent" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 81, + "name": "switchLCL" + }, + { + "args": [ + "std::uint32_t", + "uint256 const&" + ], + "lineno": 84, + "name": "failedSave" + }, + { + "args": [], + "lineno": 87, + "name": "getCompleteLedgers" + }, + { + "args": [], + "lineno": 92, + "name": "applyHeldTransactions" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 99, + "name": "popAcctTransaction" + }, + { + "args": [ + "std::uint32_t" + ], + "lineno": 104, + "name": "getHashBySeq" + }, + { + "args": [ + "std::uint32_t", + "InboundLedger::Reason" + ], + "lineno": 108, + "name": "walkHashBySeq" + }, + { + "args": [ + "std::uint32_t", + "std::shared_ptr const&", + "InboundLedger::Reason" + ], + "lineno": 115, + "name": "walkHashBySeq" + }, + { + "args": [ + "std::uint32_t" + ], + "lineno": 123, + "name": "getLedgerBySeq" + }, + { + "args": [ + "uint256 const&" + ], + "lineno": 126, + "name": "getLedgerByHash" + }, + { + "args": [ + "std::uint32_t", + "std::uint32_t" + ], + "lineno": 129, + "name": "setLedgerRangePresent" + }, + { + "args": [ + "LedgerIndex" + ], + "lineno": 132, + "name": "getCloseTimeBySeq" + }, + { + "args": [ + "LedgerHash const&", + "LedgerIndex" + ], + "lineno": 135, + "name": "getCloseTimeByHash" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 138, + "name": "addHeldTransaction" + }, + { + "args": [ + "ReadView const&" + ], + "lineno": 139, + "name": "fixMismatch" + }, + { + "args": [ + "std::uint32_t" + ], + "lineno": 141, + "name": "haveLedger" + }, + { + "args": [ + "std::uint32_t" + ], + "lineno": 142, + "name": "clearLedger" + }, + { + "args": [ + "ReadView const&" + ], + "lineno": 143, + "name": "isValidated" + }, + { + "args": [ + "std::uint32_t&", + "std::uint32_t&" + ], + "lineno": 144, + "name": "getValidatedRange" + }, + { + "args": [ + "std::uint32_t&", + "std::uint32_t&" + ], + "lineno": 145, + "name": "getFullValidatedRange" + }, + { + "args": [], + "lineno": 147, + "name": "sweep" + }, + { + "args": [], + "lineno": 148, + "name": "getCacheHitRate" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 150, + "name": "checkAccept" + }, + { + "args": [ + "uint256 const&", + "std::uint32_t" + ], + "lineno": 151, + "name": "checkAccept" + }, + { + "args": [ + "std::shared_ptr const&", + "uint256 const&", + "Json::Value" + ], + "lineno": 153, + "name": "consensusBuilt" + }, + { + "args": [ + "LedgerIndex" + ], + "lineno": 157, + "name": "setBuildingLedger" + }, + { + "args": [], + "lineno": 159, + "name": "tryAdvance" + }, + { + "args": [], + "lineno": 160, + "name": "newPathRequest" + }, + { + "args": [], + "lineno": 161, + "name": "isNewPathRequest" + }, + { + "args": [], + "lineno": 162, + "name": "newOrderBookDB" + }, + { + "args": [ + "LedgerIndex", + "LedgerHash const&" + ], + "lineno": 164, + "name": "fixIndex" + }, + { + "args": [ + "LedgerIndex" + ], + "lineno": 166, + "name": "clearPriorLedgers" + }, + { + "args": [ + "LedgerIndex" + ], + "lineno": 168, + "name": "clearLedgerCachePrior" + }, + { + "args": [ + "std::unique_ptr" + ], + "lineno": 171, + "name": "takeReplay" + }, + { + "args": [], + "lineno": 172, + "name": "releaseReplay" + }, + { + "args": [ + "bool", + "std::uint32_t" + ], + "lineno": 175, + "name": "gotFetchPack" + }, + { + "args": [ + "uint256 const&", + "std::shared_ptr" + ], + "lineno": 177, + "name": "addFetchPack" + }, + { + "args": [ + "uint256 const&" + ], + "lineno": 180, + "name": "getFetchPack" + }, + { + "args": [ + "std::weak_ptr const&", + "std::shared_ptr const&", + "uint256", + "UptimeClock::time_point" + ], + "lineno": 183, + "name": "makeFetchPack" + }, + { + "args": [], + "lineno": 189, + "name": "getFetchPackCacheSize" + }, + { + "args": [], + "lineno": 192, + "name": "haveValidated" + }, + { + "args": [], + "lineno": 197, + "name": "minSqlSeq" + }, + { + "args": [ + "uint32_t", + "uint32_t" + ], + "lineno": 200, + "name": "txnIdFromIndex" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 205, + "name": "setValidLedger" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 206, + "name": "setPubLedger" + }, + { + "args": [ + "std::shared_ptr" + ], + "lineno": 208, + "name": "tryFill" + }, + { + "args": [ + "LedgerIndex", + "InboundLedger::Reason" + ], + "lineno": 210, + "name": "getFetchPack" + }, + { + "args": [ + "LedgerIndex", + "InboundLedger::Reason" + ], + "lineno": 212, + "name": "getLedgerHashForHistory" + }, + { + "args": [], + "lineno": 214, + "name": "getNeededValidations" + }, + { + "args": [ + "std::uint32_t", + "bool&", + "InboundLedger::Reason", + "std::unique_lock&" + ], + "lineno": 215, + "name": "fetchForHistory" + }, + { + "args": [ + "std::unique_lock&" + ], + "lineno": 219, + "name": "doAdvance" + }, + { + "args": [ + "std::unique_lock&" + ], + "lineno": 222, + "name": "findNewLedgersToPublish" + }, + { + "args": [], + "lineno": 225, + "name": "updatePaths" + }, + { + "args": [ + "char const*", + "std::unique_lock&" + ], + "lineno": 228, + "name": "newPFWork" + }, + { + "args": [], + "lineno": 257, + "name": "collect_metrics" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 18, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerMaster.h.ai.md b/src/xrpld/app/ledger/LedgerMaster.h.ai.md new file mode 100644 index 0000000000..a007068634 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerMaster.h.ai.md @@ -0,0 +1,62 @@ +# `LedgerMaster.h` — Central Ledger State Manager + +`LedgerMaster` is the single authoritative coordinator for all ledger state inside a running `rippled` node. It tracks four distinct ledger views simultaneously, manages the pipeline that advances a locally-built ledger to fully-validated-and-published status, buffers transactions that arrive between ledger closes, and serves the fetch-pack protocol that lets peers fill historical gaps. Nearly every other subsystem that needs to know "what ledger are we on?" queries `LedgerMaster`. + +## Four Concurrent Views of Ledger State + +The class maintains four named ledger pointers with carefully differentiated semantics: + +| Pointer | Meaning | +|---|---| +| `mClosedLedger` | The most recent last-closed ledger (LCL): consensus has finished but quorum validation has not yet been confirmed. | +| `mValidLedger` | The highest-sequence ledger for which the local node has collected a quorum of trusted validations. | +| `mPubLedger` | The last ledger published to clients (via `NetworkOPs`); can lag `mValidLedger` while `doAdvance` fills any gap. | +| `mPathLedger` / `mHistLedger` | Internal bookmarks used for pathfinding and history-fill work respectively. | + +The split between "closed" and "validated" is fundamental to XRPL's consensus model: a node can close a ledger locally well before the network has produced enough validations. `getClosedLedger()` returns `mClosedLedger` directly (the holder is internally synchronized). `getValidatedLedger()` goes through the `LedgerHolder` wrapper which holds its own `std::mutex`. For performance-critical reads that only need the index or sign-time, the class exposes `mValidLedgerSeq`, `mValidLedgerSign`, `mPubLedgerSeq`, and `mPubLedgerClose` as `std::atomic` values so callers never take a lock just to check whether the node is caught up. + +## `LedgerHolder` — Immutable-Only Thread-Safe Wrapper + +The two "hot" ledgers (`mClosedLedger`, `mValidLedger`) use `LedgerHolder` rather than a raw `shared_ptr`. `LedgerHolder::set()` enforces two invariants at call time: the pointer must be non-null, and `ledger->isImmutable()` must be true. This prevents a mutable working ledger from accidentally escaping into the holder, which would allow races on the ledger's state tree. The holder's internal `std::mutex` is separate from `LedgerMaster`'s own `m_mutex` — so reading the current validated ledger is never blocked by the broader master lock. + +## The Advance Pipeline + +When consensus finishes, it calls `switchLCL()` (on a networked node) which stores the new LCL and then calls `checkAccept()`. `checkAccept()` queries the validations subsystem for the number of trusted validations for that ledger's hash. If the count meets quorum, `setValidLedger()` is called, which also updates `mValidLedgerSeq`, notifies the amendment table, triggers `SHAMapStore::onLedgerClosed()`, and checks for unsupported enabled amendments that would put the node into amendment-blocked mode. + +After a new validated ledger is committed, `tryAdvance()` schedules a job to run `doAdvance()`, which works under `m_mutex`. `doAdvance()` calls `findNewLedgersToPublish()` to collect any contiguous validated ledgers that haven't been published yet, then publishes them in sequence order. If there are gaps in `mCompleteLedgers`, it calls `fetchForHistory()` to acquire missing ledgers from the network, bounded by `MAX_LEDGER_GAP` (100 ledgers), ledger age (`MAX_LEDGER_AGE_ACQUIRE` = 1 minute), and write-load (`MAX_WRITE_LOAD_ACQUIRE`). The private `doAdvance()` and `fetchForHistory()` signatures take a `std::unique_lock&` by reference — a documentation pattern making it clear at every call site that the caller must already hold `m_mutex`. + +## `canBeCurrent()` — Defense Against Ledger Injection + +Before any ledger is accepted as a new "current" reference, `canBeCurrent()` applies three independent sanity checks: + +1. The candidate sequence must be ≥ the last validated sequence — never jump backward. +2. The candidate's `parentCloseTime` must be within 5 minutes of the node's wall clock (with a grace period for early startup before ledger 10). +3. The candidate sequence must not exceed `validLedger.seq + 10 + (elapsed_seconds / 2)` — preventing a malicious or diverged majority from bumping the sequence far into the future. + +This is a layered defense that makes it expensive to force a node onto a wrong chain even if an attacker controls a significant fraction of the network's validators. + +## Held Transactions and `CanonicalTXSet` + +Transactions that arrive while the node is closing a ledger or during a gap are placed in `mHeldTransactions`, a `CanonicalTXSet`. At the start of `applyHeldTransactions()`, the set is swapped out under the lock and then passed to `NetworkOPs::processTransactionSet()` outside the lock — keeping the critical section minimal. `popAcctTransaction()` supports per-account transaction chaining: when a transaction for an account succeeds, the caller can immediately pull the next queued transaction for the same account without waiting for the next close cycle. + +## Fetch Pack Subsystem + +`LedgerMaster` inherits from `AbstractFetchPackContainer`, a narrow interface that exposes only `getFetchPack(hash)`. This abstraction lets peer-layer code retrieve partial ledger data blobs without a direct dependency on `LedgerMaster` itself. Internally, `fetch_packs_` is a `TaggedCache` (65 536-entry capacity, 45-second TTL) that stores raw SHAMap node data. `makeFetchPack()` assembles a response pack for a requesting peer, walking backward from `haveLedgerHash` to collect up to `ledger_fetch_size_` nodes. The `mGotFetchPackThread` atomic flag prevents more than one concurrent gotFetchPack job from being dispatched to the job queue. + +## Concurrency Design + +The class employs three synchronization primitives at different granularities: + +- `m_mutex` (`std::recursive_mutex`) — the main lock covering most mutable state including `mClosedLedger`, `mHeldTransactions`, advance-thread flags, and pathfinding state. Recursive because `tryAdvance()` can be called from within code that already holds the lock. +- `mCompleteLock` (`std::recursive_mutex`) — a separate lock solely for `mCompleteLedgers` (a `RangeSet`), preventing the expensive range scan from blocking the main lock. +- Atomics (`mValidLedgerSeq`, `mValidLedgerSign`, `mPubLedgerSeq`, `mPubLedgerClose`, `mBuildingLedgerSeq`) — allow lightweight reads of frequently-polled values from any thread without contention. + +## Metrics and Network Guard + +The nested `Stats` struct wires two `beast::insight::Gauge` meters (`Validated_Ledger_Age`, `Published_Ledger_Age`) through a hook that calls `collect_metrics()` on each reporting cycle. This surfaces latency between the validated and published ledgers to whatever stats backend is configured. + +The constant `max_ledger_difference_` (one million sequences) guards against a validator accidentally switching between the test and production networks. If a node's stored `mLastValidLedger` is more than one million sequences ahead of the first ledger being validated after startup, an assertion fires rather than silently accepting the cross-network state. + +## Relationship to Sibling Files + +`LedgerHistory` stores a `TaggedCache` of recent ledgers by hash and a separate mapping by index, and also tracks the "built vs. validated" divergence per ledger sequence for consensus debugging. `LedgerMaster` delegates all historical cache lookups to it. `LedgerReplay` carries an ordered map of transactions plus parent/replay ledger pointers. `LedgerMaster` holds at most one replay at a time under the main mutex, transferring ownership in and out via `takeReplay()`/`releaseReplay()` — a move-only protocol that prevents the replay state from being shared across threads. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerPersistence.h.ai.json b/src/xrpld/app/ledger/LedgerPersistence.h.ai.json new file mode 100644 index 0000000000..3f2ef4d4fd --- /dev/null +++ b/src/xrpld/app/ledger/LedgerPersistence.h.ai.json @@ -0,0 +1,67 @@ +{ + "args": [], + "classes": [], + "description": "This file declares utility functions for saving and loading fully-validated ledgers in the XRPL system, including loading by index or hash, and fetching the latest ledger from the database.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerPersistence.h", + "functions": [ + { + "args": [ + "registry", + "ledger", + "isSynchronous", + "isCurrent" + ], + "lineno": 13, + "name": "pendSaveValidated" + }, + { + "args": [ + "info", + "rules", + "fees", + "registry", + "acquire" + ], + "lineno": 29, + "name": "loadLedgerHelper" + }, + { + "args": [ + "ledgerIndex", + "rules", + "fees", + "registry", + "acquire" + ], + "lineno": 44, + "name": "loadByIndex" + }, + { + "args": [ + "ledgerHash", + "rules", + "fees", + "registry", + "acquire" + ], + "lineno": 59, + "name": "loadByHash" + }, + { + "args": [ + "rules", + "fees", + "registry" + ], + "lineno": 74, + "name": "getLatestLedger" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerPersistence.h.ai.md b/src/xrpld/app/ledger/LedgerPersistence.h.ai.md new file mode 100644 index 0000000000..93af95ea28 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerPersistence.h.ai.md @@ -0,0 +1,37 @@ +# `LedgerPersistence.h` — Ledger Save and Load Interface + +This header declares the five free functions that form the persistence boundary between the in-memory `Ledger` representation and the relational (SQLite) database in the XRPL node. It is intentionally thin — pure declarations in the `xrpl` namespace — with all logic in `detail/LedgerPersistence.cpp`. Callers that need to save a validated ledger or reconstruct one from stored header information use only this interface. + +## Why This Exists as a Separate File + +The ledger subsystem separates construction, validation, persistence, and history into distinct modules. `LedgerPersistence.h` specifically owns the handoff point where a fully-validated, immutable `Ledger` transitions to durable storage and the reverse path where a stored `LedgerHeader` is promoted back to a live in-memory ledger. Keeping this in its own translation unit means the heavy relational-database and job-queue dependencies stay out of the headers that only need to work with in-memory ledgers. + +## Saving: `pendSaveValidated` and its Internal Pipeline + +`pendSaveValidated` is the most architecturally significant function in the file. It accepts a `shared_ptr`, meaning the ledger must already be immutable before any save begins — an assertion in the implementation enforces this invariant. + +The function implements a three-layer duplicate-suppression scheme before any work touches the database: + +1. **Hash-router deduplication.** `HashRouter::setFlags(hash, SAVED)` is called first. The hash router is normally used for P2P message deduplication, but here it serves as a lightweight, per-hash guard. If the flag was already present, either the save completed or is in progress; the function can return early unless the caller explicitly needs synchronous completion. + +2. **`PendingSaves` coordination.** `PendingSaves` tracks in-flight saves by sequence number with a mutex-protected map of `LedgerIndex → bool` (false = scheduled, true = in progress). `shouldWork()` either registers the sequence and returns true, detects that another thread already dispatched it, or — if the caller specified `isSynchronous` and the work is actively running — blocks on a condition variable until the other thread calls `finishWork()`. This ensures that a synchronous caller never returns until the database write actually completes. + +3. **Job queue dispatch.** If the caller does not need synchronous completion and the job queue accepts the work, the save is dispatched as either `jtPUBLEDGER` (current validated ledger, higher latency limits) or `jtPUBOLDLEDGER` (historical, with a 2-thread concurrency cap and 10-second latency budget). The distinction matters because current-ledger publishing drives the live RPC feed and must not be throttled as aggressively as background catchup work. If the job queue refuses (e.g., it is shutting down), the call falls back to an immediate synchronous save in the caller's thread. + +The internal static `saveValidatedLedger` function brackets the actual `RelationalDatabase::saveValidatedLedger()` call with `PendingSaves::startWork()` / `finishWork()`. `startWork` returns false if another thread raced in and started work first, allowing a clean short-circuit before any database I/O. + +## Loading: `loadLedgerHelper`, `loadByIndex`, `loadByHash` + +`loadLedgerHelper` is the common primitive. It constructs a `Ledger` from a `LedgerHeader` struct obtained from the database and a `NodeFamily` for the underlying node store. The `acquire` flag controls whether the `Ledger` constructor should attempt to fetch missing SHAMap nodes from peers if they are not in local storage. If the Ledger constructor sets the `loaded` boolean to false — indicating the state map could not be retrieved — the returned `shared_ptr` is reset to nullptr. All callers must check for null returns. + +`loadByIndex` and `loadByHash` follow the same two-step pattern: query the relational database for the `LedgerHeader`, delegate to `loadLedgerHelper`, then call the internal `finishLoadByIndexOrHash`. That finisher validates a fee-entry invariant (the `fees` keylet must exist for any ledger at or above the `XRP_LEDGER_EARLIEST_FEES` sequence), then calls `setImmutable()` and `setFull()` on the ledger, making it safe to hand to callers. `loadByHash` adds a debug assertion that the loaded ledger's hash matches what was requested — a sanity check against database inconsistency. + +The `rules` and `fees` parameters passed to every load function serve as bootstrap defaults fed into the `Ledger` constructor. The actual values may be overwritten when the ledger's state map is read, but during startup or in edge cases where the state is inaccessible, these defaults prevent an uninitialized object. In practice, callers derive them from the node config: `Rules{config_->features}` and `config_->FEES.toFees()`. + +## `getLatestLedger` — Startup Restart Point + +`getLatestLedger` returns a three-element tuple `(ledger, seq, hash)` representing the highest-sequence ledger stored in the database. If the database is empty, all three elements are default-initialized (nullptr, 0, zero hash). The primary caller is `Application.cpp` during startup, where it determines where to resume processing after a restart. Unlike the index/hash loaders it does not call `finishLoadByIndexOrHash`, delegating that finalization to the `loadLedgerHelper` call itself, and the ledger is returned still allowing further mutation at the startup path. + +## Dependency Injection via `ServiceRegistry` + +All five functions take `ServiceRegistry&` rather than accepting individual service references. The `ServiceRegistry` virtual interface exposes `getRelationalDatabase()`, `getPendingSaves()`, `getHashRouter()`, `getJobQueue()`, and `getNodeFamily()`. This keeps the persistence functions decoupled from concrete application types and makes the functions testable by injecting a mock registry. The `Application` class implements `ServiceRegistry`, so production call sites simply pass `app_` or `*this`. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerReplay.h.ai.json b/src/xrpld/app/ledger/LedgerReplay.h.ai.json new file mode 100644 index 0000000000..dd027b5cb9 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerReplay.h.ai.json @@ -0,0 +1,39 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr parent, std::shared_ptr replay", + "std::shared_ptr parent, std::shared_ptr replay, std::map>&& orderedTxns" + ], + "lineno": 9, + "name": "LedgerReplay" + } + ], + "description": "Defines the LedgerReplay class, which encapsulates the logic for replaying a ledger by storing references to a parent ledger, a replay ledger, and an ordered set of transactions to be replayed.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerReplay.h", + "functions": [ + { + "args": [], + "lineno": 27, + "name": "parent" + }, + { + "args": [], + "lineno": 33, + "name": "replay" + }, + { + "args": [], + "lineno": 39, + "name": "orderedTxns" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerReplay.h.ai.md b/src/xrpld/app/ledger/LedgerReplay.h.ai.md new file mode 100644 index 0000000000..4398956905 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerReplay.h.ai.md @@ -0,0 +1,33 @@ +# `LedgerReplay.h` — Replay Data Bundle for Ledger Reconstruction + +`LedgerReplay` is a focused, immutable value object that packages everything needed to deterministically re-execute a previously closed ledger: the parent ledger, the target ledger being replayed, and the set of transactions in their canonical application order. It is the single handoff point between the components that *acquire* ledger data and the `buildLedger` function that *applies* it. + +## Role in the System + +When the XRPL node is missing a historical ledger, the `LedgerReplayer` subsystem can reconstruct it from peers rather than downloading the full SHAMap state. The workflow is: + +1. `LedgerDeltaAcquire` fetches the header and ordered transactions from a peer over the `TMReplayDeltaRequest` protocol message. +2. A `LedgerReplay` object is assembled from the parent ledger and the fetched data. +3. `buildLedger(LedgerReplay const&, ...)` in `BuildLedger.cpp` iterates `orderedTxns()` and calls `applyTransaction` for each, recreating the ledger state from scratch. The resulting ledger hash must match the expected hash or the attempt fails. + +There is also a secondary usage path through `LedgerMaster::takeReplay` / `releaseReplay`, which holds a `unique_ptr` for manual replay triggering during ledger publication. + +## Design of the Two Constructors + +The two-constructor design reflects two distinct provenance paths for the transaction set. + +The **single-argument constructor** (taking only `parent` and `replay`) auto-derives the ordered transaction map directly from the replay ledger's own `txMap()`. It iterates `replay_->txMap()`, calls `txRead(item.key())` to fetch both the `STTx` and its metadata, then reads `sfTransactionIndex` from the metadata to determine canonical position. Using the metadata's `sfTransactionIndex` field is important: it is the authoritative record of the order in which transactions were applied when the ledger was originally closed, not merely the order in which they arrived or were sorted. This constructor is used when a fully validated `Ledger` object is already available locally. + +The **move constructor** accepts a pre-built `std::map>&&` and simply moves it in. This path is used by `LedgerDeltaAcquire::tryBuild`, where the ordered transaction list arrives over the network inside a `TMReplayDeltaResponse` message and has already been assembled by the message handler before `LedgerReplay` is constructed. The move avoids an unnecessary copy of what could be a large map. + +## Ordering Invariant + +The map key type `std::uint32_t` represents `sfTransactionIndex`, a zero-based position field embedded in each transaction's metadata object at the time the ledger was first built. Because `std::map` iterates in ascending key order, any range-for over `orderedTxns()` visits transactions in exactly the sequence they were applied originally. This ordering invariant is load-bearing: applying transactions to an `OpenView` is not commutative — earlier transactions can consume sequence numbers or reserve funds that affect whether later transactions succeed — so any permutation would produce a different `txMap` root hash and a different ledger hash. + +## Immutability and Ownership + +All three data members are `private`, and all three accessors return `const&`. Once constructed, the object cannot be mutated. Ownership of the underlying `Ledger` objects is shared via `shared_ptr`; `LedgerReplay` is a co-owner, keeping both ledgers alive for the duration of the replay operation. The object itself is typically stack-allocated at the call site of `buildLedger` and destroyed immediately after the new ledger is built. + +## `CountedObject` Instrumentation + +Inheriting from `CountedObject` registers the type with the global `CountedObjects` registry at zero cost to normal operation. This allows the server's diagnostic layer to report how many `LedgerReplay` instances exist at any moment — useful for detecting leaks or unexpected accumulation of in-flight replay tasks during debugging or monitoring. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerReplayTask.h.ai.json b/src/xrpld/app/ledger/LedgerReplayTask.h.ai.json new file mode 100644 index 0000000000..c99298da6d --- /dev/null +++ b/src/xrpld/app/ledger/LedgerReplayTask.h.ai.json @@ -0,0 +1,211 @@ +{ + "args": [ + { + "lineno": 36, + "name": "r" + }, + { + "lineno": 37, + "name": "finishLedgerHash" + }, + { + "lineno": 38, + "name": "totalNumLedgers" + }, + { + "lineno": 46, + "name": "hash" + }, + { + "lineno": 46, + "name": "seq" + }, + { + "lineno": 46, + "name": "sList" + }, + { + "lineno": 55, + "name": "existingTask" + }, + { + "lineno": 65, + "name": "app" + }, + { + "lineno": 66, + "name": "inboundLedgers" + }, + { + "lineno": 67, + "name": "replayer" + }, + { + "lineno": 68, + "name": "skipListAcquirer" + }, + { + "lineno": 69, + "name": "parameter" + }, + { + "lineno": 79, + "name": "delta" + }, + { + "lineno": 93, + "name": "progress" + }, + { + "lineno": 93, + "name": "sl" + }, + { + "lineno": 109, + "name": "deltaHash" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "LedgerReplayer& replayer", + "std::shared_ptr& skipListAcquirer", + "TaskParameter const& parameter" + ], + "lineno": 18, + "name": "LedgerReplayTask" + }, + { + "args": [ + "InboundLedger::Reason r", + "uint256 const& finishLedgerHash", + "std::uint32_t totalNumLedgers" + ], + "lineno": 20, + "name": "LedgerReplayTask::TaskParameter" + } + ], + "description": "Defines the LedgerReplayTask class, which manages the replay of a range of ledgers by coordinating subtasks for acquiring ledger deltas and skip lists, used in the XRPL ledger synchronization process.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerReplayTask.h", + "functions": [ + { + "args": [ + "InboundLedger::Reason r", + "uint256 const& finishLedgerHash", + "std::uint32_t totalNumLedgers" + ], + "lineno": 32, + "name": "TaskParameter::TaskParameter" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t seq", + "std::vector const& sList" + ], + "lineno": 44, + "name": "TaskParameter::update" + }, + { + "args": [ + "TaskParameter const& existingTask" + ], + "lineno": 54, + "name": "TaskParameter::canMergeInto" + }, + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "LedgerReplayer& replayer", + "std::shared_ptr& skipListAcquirer", + "TaskParameter const& parameter" + ], + "lineno": 63, + "name": "LedgerReplayTask::LedgerReplayTask" + }, + { + "args": [], + "lineno": 70, + "name": "LedgerReplayTask::~LedgerReplayTask" + }, + { + "args": [], + "lineno": 73, + "name": "LedgerReplayTask::init" + }, + { + "args": [ + "std::shared_ptr const& delta" + ], + "lineno": 78, + "name": "LedgerReplayTask::addDelta" + }, + { + "args": [], + "lineno": 82, + "name": "LedgerReplayTask::getTaskParameter" + }, + { + "args": [], + "lineno": 88, + "name": "LedgerReplayTask::finished" + }, + { + "args": [ + "bool progress", + "ScopedLockType& sl" + ], + "lineno": 92, + "name": "LedgerReplayTask::onTimer" + }, + { + "args": [], + "lineno": 95, + "name": "LedgerReplayTask::pmDowncast" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t seq", + "std::vector const& sList" + ], + "lineno": 101, + "name": "LedgerReplayTask::updateSkipList" + }, + { + "args": [ + "uint256 const& deltaHash" + ], + "lineno": 108, + "name": "LedgerReplayTask::deltaReady" + }, + { + "args": [ + "ScopedLockType& sl" + ], + "lineno": 114, + "name": "LedgerReplayTask::trigger" + }, + { + "args": [ + "ScopedLockType& sl" + ], + "lineno": 120, + "name": "LedgerReplayTask::tryAdvance" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "xrpl::test" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerReplayTask.h.ai.md b/src/xrpld/app/ledger/LedgerReplayTask.h.ai.md new file mode 100644 index 0000000000..8d60234da0 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerReplayTask.h.ai.md @@ -0,0 +1,43 @@ +# LedgerReplayTask.h + +`LedgerReplayTask` is the top-level coordinator for replaying a contiguous range of historical ledgers during XRPL network synchronization. When a node needs to catch up by re-executing a known sequence of ledgers — rather than acquiring the full state tree — this class orchestrates the two-phase network acquisition process and drives incremental ledger construction. + +## Role in the Ledger Replay Subsystem + +The replay subsystem provides an alternative to full state-tree synchronization. Instead of downloading entire ledger state maps, a node can ask peers for the compact "delta" representation of each ledger (its header plus ordered transactions), then re-apply those transactions against the parent ledger. `LedgerReplayTask` sits at the top of this hierarchy, with `SkipListAcquire` and `LedgerDeltaAcquire` (both also `TimeoutCounter` subclasses) as subordinate network-acquisition tasks. `LedgerReplayer` acts as the factory and registry, while `LedgerReplayTask` focuses solely on the lifecycle of one range. + +## TaskParameter: Deferred Initialization by Design + +The nested `TaskParameter` struct captures the two-stage nature of the problem. At construction only three things are known: the replay `reason_`, the hash of the last ledger in the range (`finishHash_`), and how many ledgers to cover (`totalLedgers_`). Everything else — the sequence numbers, the full skip list, and the start hash — can only be known once the skip list embedded in the finish ledger is retrieved from the network. + +The `full_` boolean gates all downstream work. `trigger()` and `tryAdvance()` both check `parameter_.full_` first and return immediately if it is false. This prevents any attempt to locate the start ledger or build deltas before the range boundaries are established. + +The `update()` method populates the deferred fields when verified skip list data arrives. It validates that the provided hash matches `finishHash_`, that the skip list is long enough, appends `finishHash_` to the skip list, then computes `startHash_` by indexing from the end of the list. This is the only place where `full_` is set to `true`. + +`canMergeInto()` enables deduplication at the `LedgerReplayer` level. A new task can be silently absorbed into an existing one if the two share the same `reason_` and the existing task's range fully covers the new request — either because they share the same finish hash with the existing range being at least as large, or (once the existing task is `full_`) because the new task's finish hash appears within the existing skip list at an offset where the existing task's total count subsumes the new task's count plus the remaining distance. This avoids redundant network traffic for overlapping catch-up requests. + +## Asynchronous Execution Model + +`LedgerReplayTask` inherits from `TimeoutCounter`, which provides an asynchronous loop: a repeating timer (`TASK_TIMEOUT = 500ms`) fires a job into the job queue, which calls `onTimer`. While the timer loop runs in the background, a separate callback-based flow makes forward progress. + +On `init()`, the task registers a `weak_ptr`-captured lambda with its `SkipListAcquire` subtask. This callback fires when the skip list either succeeds or fails. On success, `updateSkipList()` is called, which fills `parameter_`, then calls `replayer_.createDeltas()` (outside the task lock, to avoid deadlock with `LedgerReplayer`'s own mutex), then re-acquires the lock and calls `trigger()`. + +When each `LedgerDeltaAcquire` subtask completes, it invokes its own callback into `deltaReady()`, which calls `tryAdvance()` under the task lock. + +## Sequential Chain Building in tryAdvance + +`tryAdvance()` implements the core ledger construction loop. It only proceeds when three conditions are simultaneously true: the parent ledger is available (`parent_` is non-null), the parameter is `full_`, and all expected deltas have been added (`totalLedgers_ - 1 == deltas_.size()`). This last condition is important: deltas are created by `LedgerReplayer::createDeltas` after the skip list is known, so the task must wait for both the skip list and all delta subtasks to be registered before it can start building. + +The loop walks `deltas_` from `deltaToBuild_` forward, calling `tryBuild(parent)` on each `LedgerDeltaAcquire`. Each successfully built ledger replaces `parent_`, becoming the base for the next delta — a strict sequential dependency that `XRPL_ASSERT` enforces by requiring consecutive sequence numbers. If any delta is not yet ready, the loop returns early; it will be re-entered when the next `deltaReady()` callback fires. + +On completion of all deltas, `complete_ = true` is set. Any `std::runtime_error` from `tryBuild` (data corruption or replay failure) sets `failed_ = true` instead, stopping the task without retrying. + +## Timeout Budget + +The maximum number of timeouts before failure is computed as `max(TASK_MAX_TIMEOUTS_MINIMUM, totalLedgers_ * TASK_MAX_TIMEOUTS_MULTIPLIER)` — i.e., `max(10, n * 2)`. Longer ranges get proportionally more time before being abandoned. On each `onTimer()` call, `trigger()` is re-entered to retry acquiring the start ledger or re-driving `tryAdvance` for any newly ready deltas. If the timeout budget is exhausted, `failed_` is set and the `TimeoutCounter` loop exits. + +## Lifetime and Concurrency + +All three mutable internal pointers — `parent_`, `skipListAcquirer_`, and `deltas_` — are protected by the `recursive_mutex` inherited from `TimeoutCounter`. The `skipListAcquirer_` is held as a `shared_ptr` (not `weak_ptr`) to guarantee the subtask is not destroyed while the callback is still pending. Subtask callbacks capture `weak_ptr` so that a destroyed task does not produce dangling calls. The public `finished()` method acquires the lock to safely read `isDone()`. + +`test::LedgerReplayClient` is friended to allow test code to inspect internal state — a deliberate white-box testing escape hatch rather than a production coupling. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerReplayer.h.ai.json b/src/xrpld/app/ledger/LedgerReplayer.h.ai.json new file mode 100644 index 0000000000..df29e2bc8a --- /dev/null +++ b/src/xrpld/app/ledger/LedgerReplayer.h.ai.json @@ -0,0 +1,103 @@ +{ + "args": [ + { + "lineno": 40, + "name": "app" + }, + { + "lineno": 41, + "name": "inboundLedgers" + }, + { + "lineno": 42, + "name": "peerSetBuilder" + } + ], + "classes": [ + { + "args": [ + "app", + "inboundLedgers", + "peerSetBuilder" + ], + "lineno": 38, + "name": "LedgerReplayer" + } + ], + "description": "Manages the lifetime and coordination of ledger replay tasks in the XRPL system, including replaying ranges of ledgers, handling subtasks, and managing related resources and timeouts.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerReplayer.h", + "functions": [ + { + "args": [ + "r", + "finishLedgerHash", + "totalNumLedgers" + ], + "lineno": 49, + "name": "replay" + }, + { + "args": [ + "task" + ], + "lineno": 56, + "name": "createDeltas" + }, + { + "args": [ + "info", + "data" + ], + "lineno": 63, + "name": "gotSkipList" + }, + { + "args": [ + "info", + "txns" + ], + "lineno": 71, + "name": "gotReplayDelta" + }, + { + "args": [], + "lineno": 79, + "name": "sweep" + }, + { + "args": [], + "lineno": 82, + "name": "stop" + }, + { + "args": [], + "lineno": 85, + "name": "tasksSize" + }, + { + "args": [], + "lineno": 92, + "name": "deltasSize" + }, + { + "args": [], + "lineno": 99, + "name": "skipListsSize" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "test" + }, + { + "lineno": 15, + "name": "LedgerReplayParameters" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerReplayer.h.ai.md b/src/xrpld/app/ledger/LedgerReplayer.h.ai.md new file mode 100644 index 0000000000..c68c20c895 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerReplayer.h.ai.md @@ -0,0 +1,38 @@ +# `LedgerReplayer.h` — Top-Level Orchestrator for Ledger Replay + +## Role in the System + +Ledger replay is the mechanism by which an XRPL node fetches a contiguous range of historical ledgers from its peers by downloading each ledger's header and transactions (the "delta") and re-applying them against the prior state. `LedgerReplayer` is the single top-level manager for this subsystem — it creates, tracks, deduplicates, and tears down all replay work. Callers start a replay by calling `replay()` with a terminal ledger hash and a count; everything below that API is handled internally across a three-tier task hierarchy. + +## The `LedgerReplayParameters` Namespace + +All hard-coded tuning values live in `LedgerReplayParameters` rather than being scattered as magic numbers. The hierarchy has two timeout tiers: top-level `LedgerReplayTask` objects fire on `TASK_TIMEOUT = 500ms`, while subtasks (`SkipListAcquire` and `LedgerDeltaAcquire`) fire on the shorter `SUB_TASK_TIMEOUT = 250ms`. The asymmetry reflects their different scopes — a task orchestrates many subtasks and warrants more patience, while a subtask does exactly one network fetch. + +The `TASK_MAX_TIMEOUTS_MULTIPLIER` formula — `max(10, N * 2)` — scales allowed timeouts with range size, preventing large replays from being cancelled too aggressively while bounding the wait for small ones. `MAX_TASKS = 10` and `MAX_TASK_SIZE = 256` are hard caps that prevent unbounded queue growth. A fallback path exists when peers don't support the replay feature: after `MAX_NO_FEATURE_PEER_COUNT = 2` non-supporting peers are encountered, subtasks switch to `SUB_TASK_FALLBACK_TIMEOUT = 1000ms` and retry via the legacy acquisition path. + +## Task Hierarchy + +`LedgerReplayer` owns the top level. Each `LedgerReplayTask` represents a request to replay a range of up to 256 ledgers and holds two kinds of subtask: + +- `SkipListAcquire` — fetches the ancestor-hash list embedded in the finish ledger, which provides the exact hashes of all ledgers in the range. +- `LedgerDeltaAcquire` — fetches one ledger's header and transactions from peers. Created by `createDeltas()` after the skip list is known. + +All three types extend `TimeoutCounter`, which drives an asynchronous retry loop via Boost.Asio timers and a `JobQueue`, calling `onTimer()` on each expiry. + +## Ownership and Deduplication via `weak_ptr` + +The most architecturally notable design in `LedgerReplayer` is how it holds subtasks. `tasks_` holds strong `shared_ptr` objects, but `skipLists_` and `deltas_` are `hash_map>`. Each subtask is owned by the task (or tasks) that depend on it; `LedgerReplayer` merely tracks them for routing incoming network data. + +This serves two purposes simultaneously. First, deduplication: `replay()` checks the `skipLists_` map before creating a new `SkipListAcquire`, and `createDeltas()` does the same for each `LedgerDeltaAcquire`. If another task already has an in-flight request for the same hash, the new task reuses the existing subtask — two tasks covering overlapping ledger ranges share the same delta fetches automatically. Second, automatic cleanup: when all tasks referencing a subtask are destroyed, the subtask destructs without `LedgerReplayer` needing to explicitly track lifetimes. `sweep()` later purges the dead `weak_ptr` entries from the maps. + +The `replay()` method also checks whether a new request can be merged entirely into an existing `LedgerReplayTask` via `canMergeInto()`. If the new range is a sub-range of an existing task's range, the new request is silently dropped, preventing duplicate top-level tasks. + +## Locking Strategy and Deadlock Prevention + +`LedgerReplayer` uses a single `std::mutex mtx_` to protect `tasks_`, `skipLists_`, and `deltas_`. Subtasks each have their own internal `recursive_mutex` (from `TimeoutCounter`). The protocol in `gotSkipList()` and `gotReplayDelta()` is deliberate: acquire `mtx_`, look up the subtask, promote the `weak_ptr` to a `shared_ptr`, then **release** `mtx_` before calling `processData()` on the subtask. This lock-before-copy-then-release pattern ensures the outer lock is never held when the inner subtask lock is acquired, preventing inversion deadlocks between the two mutex levels. + +## Lifecycle Methods + +`stop()` cancels all live tasks and subtasks by calling `cancel()` on each, then clears all three collections. `cancel()` on a `TimeoutCounter` marks it done without forcing immediate cancellation of queued timer callbacks — those callbacks simply exit early when they see `isDone()`. `sweep()` is called periodically (from an external maintenance path) to remove finished tasks and dangling `weak_ptr` entries; it measures and logs the time spent under `mtx_` to detect contention. + +The `test::LedgerReplayClient` friend class declared in both `LedgerReplayer` and its subtask classes grants white-box access to internal state for unit tests, keeping the production API clean without requiring extra accessors. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerToJson.h.ai.json b/src/xrpld/app/ledger/LedgerToJson.h.ai.json new file mode 100644 index 0000000000..1b9a30d597 --- /dev/null +++ b/src/xrpld/app/ledger/LedgerToJson.h.ai.json @@ -0,0 +1,55 @@ +{ + "args": [ + { + "lineno": 13, + "name": "l" + }, + { + "lineno": 14, + "name": "ctx" + }, + { + "lineno": 15, + "name": "o" + }, + { + "lineno": 16, + "name": "q" + } + ], + "classes": [], + "description": "Defines the LedgerFill struct and related functions for serializing ledger data to JSON in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LedgerToJson.h", + "functions": [ + { + "args": [ + "Json::Value&", + "LedgerFill const&" + ], + "lineno": 38, + "name": "addJson" + }, + { + "args": [ + "LedgerFill const&" + ], + "lineno": 42, + "name": "getJson" + }, + { + "args": [ + "Json::Value&", + "Json::Value const&" + ], + "lineno": 46, + "name": "copyFrom" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LedgerToJson.h.ai.md b/src/xrpld/app/ledger/LedgerToJson.h.ai.md new file mode 100644 index 0000000000..95917fc2cf --- /dev/null +++ b/src/xrpld/app/ledger/LedgerToJson.h.ai.md @@ -0,0 +1,60 @@ +# `LedgerToJson.h` — Ledger Serialization Interface + +This header defines the public interface for serializing XRP Ledger data into JSON, a capability exercised by nearly every ledger-related RPC endpoint. It lives in `src/xrpld/app/ledger/` and is the single include callers need to convert a `ReadView` into the JSON structure returned to API clients. The implementation is in `detail/LedgerToJson.cpp`. + +## The `LedgerFill` Parameter Object + +Rather than a long argument list, the header introduces `LedgerFill` as a self-contained descriptor that bundles the ledger, serialization options, and contextual services. This is deliberate: `addJson()` and `getJson()` need several independent pieces of information, and packing them here keeps the call sites readable — notably in `LedgerHandler::writeResult()`: + +```cpp +addJson(value, {*ledger_, &context_, options_, queueTxs_}); +``` + +`LedgerFill` holds the ledger as a `ReadView const&` (not a pointer or shared_ptr), so the caller's lifetime guarantee is structural. The `txQueue` is taken by value with `std::move`, reflecting that the queue snapshot is a one-time capture from `TxQ::getTxs()` whose ownership should transfer into the fill descriptor. + +The most subtle field is the `RPC::Context const*`. It is nullable by design — `getJson()` is sometimes called without a live RPC context (for example in `doLedgerData` when generating the base ledger header). When non-null, `context` contributes three things: the API version that governs output shape, the `LedgerMaster` reference needed to resolve validation status, and the journal for error logging. + +The constructor resolves `closeTime` eagerly: + +```cpp +if (context) + closeTime = context->ledgerMaster.getCloseTimeBySeq(ledger.seq()); +``` + +This is a deliberate optimization. `getCloseTimeBySeq()` may involve a database or in-memory lookup that is expensive to repeat per-transaction. By caching it in `std::optional` at construction, the value is computed once and then stamped onto every expanded transaction's `close_time_iso` field in the implementation. + +## The Options Bitmask + +The nested `Options` enum provides seven orthogonal flags combined via bitwise OR at call sites: + +| Flag | Value | Effect | +|------|-------|--------| +| `dumpTxrp` | 1 | Include the transactions array | +| `dumpState` | 2 | Include the account state trie | +| `expand` | 4 | Serialize objects as full JSON instead of hash strings | +| `full` | 8 | Implies both `dumpTxrp` and `dumpState`; also forces expand | +| `binary` | 16 | Hex-encode serialized blobs instead of JSON fields | +| `ownerFunds` | 32 | Annotate `OfferCreate` transactions with the creator's available balance | +| `dumpQueue` | 64 | Append pending transaction queue contents | + +The `full` flag is a convenience superset: `isExpanded()` in the implementation returns true when `full` is set even if `expand` is not, avoiding redundancy in call sites. Callers that set `full` or `dumpState` are required by `LedgerHandler` to hold unlimited permissions and pass a load check before constructing the fill — the serialization layer itself has no access control, keeping concerns separated. + +## `addJson()` vs `getJson()` + +These two functions expose the same underlying serialization pipeline with different placement semantics. + +`addJson(json, fill)` nests the ledger under `json["ledger"]` — the JSON envelope shape expected by the `ledger` RPC method. Queue data, if requested via `dumpQueue`, is placed *outside* that nested object, directly in `json["queue_data"]`. This asymmetry is intentional: the transaction queue is metadata *about* the open ledger rather than data encoded in the ledger itself, so it sits at the response's top level. + +`getJson(fill)` returns a fresh `Json::Value` containing only the ledger's own fields, without the `"ledger"` wrapper. This is used in `doLedgerData` when returning the ledger header on the first page of a paginated state-node query — there the caller places the result under `jvResult[jss::ledger]` itself. + +## API Version Awareness + +The `context->apiVersion` value, passed through `LedgerFill`, controls several output differences in the implementation. In API v1, `ledger_index` is a string; in v2 it is a native integer. Expanded transactions in v2 move transaction fields under a `tx_json` sub-object and use `meta_blob` rather than `meta` for binary metadata. The hash field is added explicitly in v2. Without a context, the implementation defaults to `apiMaximumSupportedVersion`, so callers that omit context receive the current canonical format. + +## `copyFrom()` + +This utility function merges key-value pairs from one `Json::Value` object into another. Its short-circuit — assigning directly when the destination is null — handles the common case where a field is being set for the first time. When the destination already has content, it iterates `getMemberNames()` and copies each key individually (a shallow merge, not a recursive deep merge). An assertion guards that the source is an object or null; callers must not pass arrays or scalar values. The TODO comment in the implementation acknowledges that the deep-copy semantics of the fallback path may deserve reconsideration if JSON values ever contain nested shared references. + +## Exception Safety + +The transaction-iteration path in the implementation is wrapped in a try/catch. If any transaction in the ledger's storage is undeserializable — for instance due to a corrupt or incompatible encoding — the exception is caught, logged to the context's journal at error level, and the response is returned with whatever transactions were successfully serialized before the failure. This prevents a single bad ledger entry from producing an empty or server-error response. \ No newline at end of file diff --git a/src/xrpld/app/ledger/LocalTxs.h.ai.json b/src/xrpld/app/ledger/LocalTxs.h.ai.json new file mode 100644 index 0000000000..700e4a9394 --- /dev/null +++ b/src/xrpld/app/ledger/LocalTxs.h.ai.json @@ -0,0 +1,51 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "LocalTxs" + } + ], + "description": "Defines the LocalTxs class for tracking and managing local transactions to ensure they are applied to open ledgers and held until seen in a fully-validated ledger.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/LocalTxs.h", + "functions": [ + { + "args": [ + "LedgerIndex index", + "std::shared_ptr const& txn" + ], + "lineno": 22, + "name": "push_back" + }, + { + "args": [], + "lineno": 26, + "name": "getTxSet" + }, + { + "args": [ + "ReadView const& view" + ], + "lineno": 30, + "name": "sweep" + }, + { + "args": [], + "lineno": 34, + "name": "size" + }, + { + "args": [], + "lineno": 38, + "name": "make_LocalTxs" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/LocalTxs.h.ai.md b/src/xrpld/app/ledger/LocalTxs.h.ai.md new file mode 100644 index 0000000000..2322d4c837 --- /dev/null +++ b/src/xrpld/app/ledger/LocalTxs.h.ai.md @@ -0,0 +1,46 @@ +# `LocalTxs.h` — Local Transaction Retention Interface + +## Role in the System + +`LocalTxs.h` defines the abstract interface that guards against a subtle consensus-divergence hazard: a transaction submitted by a local client can be silently dropped from the node's open ledger when that node's consensus view diverges from the network majority. Without intervention, the client's second transaction would receive `terPRE_SEQ` (because the node no longer believes the first transaction was applied), and the node would not relay it further. + +The interface contracts a small buffer — by design holding pending local transactions for up to five ledgers — and re-applies them to every new open ledger until a fully-validated ledger confirms they were accepted, expired, or became impossible. The `holdLedgers = 5` constant is intentionally generous: five ledger closes is ample time for a transaction to propagate through consensus under normal network conditions, while the `sfLastLedgerSequence` field on the transaction itself can tighten the window if the sender requires faster expiry. + +## Interface Design + +`LocalTxs` is a pure virtual base with four operations and a factory function: + +``` +virtual void push_back(LedgerIndex index, shared_ptr const& txn) = 0; +virtual CanonicalTXSet getTxSet() = 0; +virtual void sweep(ReadView const& view) = 0; +virtual size_t size() = 0; + +unique_ptr make_LocalTxs(); +``` + +Hiding the implementation behind a pure interface (with `make_LocalTxs()` as its factory) keeps the concrete class, its mutex, and its internal `LocalTx` wrapper type entirely out of the header. Callers — notably `NetworkOPsImp` and `RCLConsensus::Adaptor` — depend only on this narrow contract, which simplifies testing and prevents accidental coupling to implementation details. + +## Lifecycle and Callers + +`NetworkOPsImp` owns the single live instance as a `std::unique_ptr m_localTX`, constructed via `make_LocalTxs()` at startup. + +**Enrollment (`push_back`):** When the node's transaction-processing path applies a transaction flagged as `local`, and the transaction is not forced-hard-failed, `NetworkOPsImp` calls `push_back` with the current open ledger index and the signed transaction. This enrolls the transaction in the retention buffer so it survives any subsequent consensus rollback. + +**Re-application (`getTxSet`):** Both `NetworkOPsImp::doAdvance` and `RCLConsensus::Adaptor` call `getTxSet` when constructing a new open ledger. The method packages all currently held transactions into a `CanonicalTXSet` (initialized with a zero salt) so they are applied in a deterministic per-account sequence order. Using `CanonicalTXSet` here ensures transactions from the same account are ordered by `SeqProxy`, preventing the engine from rejecting them for out-of-order application. + +**Pruning (`sweep`):** `LedgerMaster::setValidLedger` calls `app_.getOPs().updateLocalTx(*l)`, which delegates to `m_localTX->sweep(view)`, each time a new fully-validated ledger arrives. The concrete `sweep` implementation runs `std::list::remove_if` with a lambda that queries the validated ledger's `ReadView` to determine which held transactions are no longer needed. Three pruning conditions apply in order: + +1. **Expiry by ledger index.** If the current validated ledger sequence exceeds the transaction's expiration (the minimum of the enrollment ledger plus `holdLedgers` and `sfLastLedgerSequence + 1`), the entry is dropped unconditionally. +2. **Confirmed inclusion.** If `view.txExists(id)` returns true, the validated ledger already contains the transaction; it is removed. +3. **Account-sequence analysis.** For sequence-based transactions (`seqProx.isSeq()`), if the account's on-ledger sequence has advanced past the transaction's sequence number, the transaction would produce `tefPAST_SEQ` and is discarded. For ticket-based transactions, the code keeps entries whose ticket has not yet been created (the account sequence hasn't reached the ticket number yet), but removes them once the ticket should exist and `keylet::ticket` can be confirmed absent from the ledger. + +This three-stage sweep avoids prematurely evicting ticket transactions that are genuinely waiting for their ticket to be created, while still bounding their lifetime by the `holdLedgers` ceiling. + +## Concurrency + +The concrete implementation (`LocalTxsImp`) stores entries in a `std::list` protected by a `std::mutex`. All four public methods acquire the lock under a `std::lock_guard`. Transactions can be submitted concurrently from client-facing threads while consensus and ledger-advance threads call `getTxSet` and `sweep`, making the lock mandatory. The `std::list` is chosen deliberately: `remove_if` invalidates only erased iterators, and the list never moves elements, so no iterator re-validation is needed after incremental removal. + +## Key Invariant + +The interface enforces the invariant that no local transaction remains in the buffer for more than `holdLedgers` validated ledger closes, regardless of network conditions, ticket state, or whether the transaction was ultimately applied. This bounds memory consumption and prevents stale transactions from accumulating during extended network partitions. \ No newline at end of file diff --git a/src/xrpld/app/ledger/OpenLedger.h.ai.json b/src/xrpld/app/ledger/OpenLedger.h.ai.json new file mode 100644 index 0000000000..172ad8fd34 --- /dev/null +++ b/src/xrpld/app/ledger/OpenLedger.h.ai.json @@ -0,0 +1,118 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr const& ledger", + "CachedSLEs& cache", + "beast::Journal journal" + ], + "lineno": 27, + "name": "OpenLedger" + } + ], + "description": "Defines the OpenLedger class, which represents and manages the open ledger in the XRPL system, including transaction application, modification, and acceptance logic with thread safety and retry mechanisms.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/OpenLedger.h", + "functions": [ + { + "args": [], + "lineno": 54, + "name": "empty" + }, + { + "args": [], + "lineno": 68, + "name": "current" + }, + { + "args": [ + "modify_type const& f" + ], + "lineno": 83, + "name": "modify" + }, + { + "args": [ + "Application& app", + "Rules const& rules", + "std::shared_ptr const& ledger", + "OrderedTxs const& locals", + "bool retriesFirst", + "OrderedTxs& retries", + "ApplyFlags flags", + "std::string const& suffix", + "modify_type const& f" + ], + "lineno": 98, + "name": "accept" + }, + { + "args": [ + "Application& app", + "OpenView& view", + "ReadView const& check", + "FwdRange const& txs", + "OrderedTxs& retries", + "ApplyFlags flags", + "beast::Journal j" + ], + "lineno": 130, + "name": "apply" + }, + { + "args": [ + "Rules const& rules", + "std::shared_ptr const& ledger" + ], + "lineno": 170, + "name": "create" + }, + { + "args": [ + "Application& app", + "OpenView& view", + "std::shared_ptr const& tx", + "bool retry", + "ApplyFlags flags", + "beast::Journal j" + ], + "lineno": 173, + "name": "apply_one" + }, + { + "args": [ + "std::shared_ptr const& tx" + ], + "lineno": 210, + "name": "debugTxstr" + }, + { + "args": [ + "OrderedTxs const& set" + ], + "lineno": 212, + "name": "debugTostr" + }, + { + "args": [ + "SHAMap const& set" + ], + "lineno": 214, + "name": "debugTostr" + }, + { + "args": [ + "std::shared_ptr const& view" + ], + "lineno": 216, + "name": "debugTostr" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/OpenLedger.h.ai.md b/src/xrpld/app/ledger/OpenLedger.h.ai.md new file mode 100644 index 0000000000..0023dfc4ca --- /dev/null +++ b/src/xrpld/app/ledger/OpenLedger.h.ai.md @@ -0,0 +1,60 @@ +# `OpenLedger.h` — The Pending Ledger State Machine + +## Role in the System + +Every ledger in XRPL passes through two phases: open (pending) and closed (validated). `OpenLedger` is the authoritative owner of the open phase — the accumulating ledger where new transactions land before a consensus round seals them into a closed, immutable ledger. It acts as the live transaction staging area that the rest of the application reads and writes concurrently during normal operation, and that the consensus engine replaces atomically when a new ledger is accepted. + +The class is not copyable or default-constructible, reflecting its role as a unique, shared resource within `Application`. It is constructed from the most recently closed ledger and holds its state as an `OpenView const` shared pointer — a read-only snapshot that any thread can safely hold. + +## Core Data Model + +The internal state is deceptively simple: a single `std::shared_ptr` named `current_` holds the entire open ledger at any moment. `OpenView` (from `include/xrpl/ledger/OpenView.h`) is a writable accumulator that layers state changes over a read-only base — in this case a `CachedLedger` wrapping the last closed ledger. Reads are cheap because they snapshot the pointer and share the immutable object; writes always produce a new `OpenView` copy and swap it in. + +`CachedSLEs&` is held by reference and passed into `create()` when constructing new `CachedLedger` wrappers. It prevents repeated deserialization of the same ledger state entries across view copies, which is critical for throughput given how frequently modifications occur. + +`OrderedTxs` is a type alias for `CanonicalTXSet`, a deterministically sorted map keyed on `(salted-account, seqProxy, txId)`. The salt comes from the parent ledger's hash, which prevents adversaries from mining account IDs to manipulate the sort order. This ordering is what makes transaction replay deterministic across nodes during consensus. + +## Two-Mutex Concurrency Design + +The class maintains two mutexes with a strict acquisition order: + +- `modify_mutex_` serializes all mutation operations — both `modify()` and the write phase of `accept()`. +- `current_mutex_` protects only the pointer swap into `current_`. + +This split exists because `current()` must be as cheap as possible — it's called from many read paths (RPC handlers, signing utilities, TxQ checks). Under this design, `current()` grabs only `current_mutex_`, loads the `shared_ptr`, and returns. The heavy work of building a new view happens under `modify_mutex_` alone, and the final pointer publish is a minimal critical section under `current_mutex_`. + +`modify()` illustrates the pattern: it acquires `modify_mutex_`, copy-constructs a new `OpenView` from `*current_`, calls the user-supplied functor on it, and if the functor returns `true`, acquires `current_mutex_` to swap in the new view. The functor never touches the lock, and the outer `modify_mutex_` prevents two concurrent modifications from racing on their copies. + +## The `accept()` Transition + +`accept()` is the most complex operation — it drives the ledger close sequence. Its logic, after acquiring `modify_mutex_` to block concurrent `modify()` calls, proceeds in layers: + +1. **Retries first (optional):** If `retriesFirst` is true, the previously-collected set of retriable transactions is re-applied to the new open view *before* acquiring `modify_mutex_`. This handles disputed transactions that need an early shot at the fresh ledger without blocking new transaction ingestion. + +2. **Current open transactions:** All transactions in the outgoing `current_` view are applied to the new view via the `apply()` template. Any that fail transiently go into the `retries` output set for the caller to manage. + +3. **Modify callback:** The optional `modify_type` functor `f` is called (still under `modify_mutex_`) to perform additional modifications — in practice this is where `TxQ` injects queued transactions. + +4. **Local transactions:** The `locals` set is fed through `app.getTxQ().apply()` one by one. + +5. **Relay recovered transactions:** For every transaction that made it into the new open view, `accept()` consults `HashRouter::shouldRelay()` and, if appropriate, serializes the transaction and broadcasts it via the overlay. Inner batch transactions (flagged `tfInnerBatchTxn`) are skipped here since they should not be independently relayed. + +6. **Atomic publish:** `current_mutex_` is acquired and `current_` is replaced with the new view. + +The careful ordering — retries outside the lock, then `modify_mutex_` to block new submissions, then `current_mutex_` only for the pointer swap — ensures that no transaction submitted concurrently via `modify()` is silently lost during ledger close. + +## Transaction Application and Retry Logic + +The `apply()` function template (defined inline in the header) implements a multi-pass retry algorithm. The constants `LEDGER_TOTAL_PASSES = 3` and `LEDGER_RETRY_PASSES = 1` govern its behavior: + +- In the first pass, every candidate transaction is attempted with `retry = true`. Transactions returning `Result::retry` (transient failures such as sequence gaps or insufficient reserves that might resolve once other transactions apply) go into the `retries` set. +- Subsequent passes re-attempt retries. A pass switches out of retry mode once it stops making progress (`changes == 0`) or after `LEDGER_RETRY_PASSES` additional retry-enabled passes. +- A final non-retry pass is always guaranteed, ensuring that no transaction sits in the set without at least one definitive attempt. + +`apply_one()` maps `xrpl::apply()` results to the three-valued `Result` enum. Applied transactions and those sent to the queue (`terQUEUED`) are `success`. Transactions with `tef*`, `tem*`, or `tel*` codes are `failure` (discarded permanently). Everything else is `retry`. + +The template accepts any forward range via `FwdRange`, and the comment "Dereferencing the iterator can throw since it may be transformed" explains why each iteration is wrapped in a `try/catch`: the `boost::adaptors::transform` range used in `accept()` lazily extracts the transaction pointer from the `txs` pair, and that transformation can throw. + +## Debug Utilities + +The four `debugTostr()` / `debugTxstr()` free functions provide abbreviated transaction-set representations for log output. Each truncates the transaction ID to four hex characters — enough to visually distinguish transactions in trace logs without the noise of full hashes. These are intentionally not part of the `OpenLedger` class since they operate on `OrderedTxs`, `SHAMap`, `ReadView`, and `STTx` types directly. \ No newline at end of file diff --git a/src/xrpld/app/ledger/OrderBookDB.h.ai.json b/src/xrpld/app/ledger/OrderBookDB.h.ai.json new file mode 100644 index 0000000000..1d7c2e1c20 --- /dev/null +++ b/src/xrpld/app/ledger/OrderBookDB.h.ai.json @@ -0,0 +1,37 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "beast::insight::Collector::ptr const& collector", + "Application& app" + ], + "lineno": 11, + "name": "LedgerHistory" + } + ], + "description": "This file defines the LedgerHistory class, which is responsible for retaining and tracking historical ledgers in the XRPL system. It provides mechanisms to track, retrieve, and manage ledgers by their hash or sequence number.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/OrderBookDB.h", + "functions": [ + { + "args": [ + "beast::insight::Collector::ptr const& collector", + "Application& app" + ], + "lineno": 15, + "name": "LedgerHistory" + }, + { + "args": [], + "lineno": 20, + "name": "trackLedger" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/OrderBookDB.h.ai.md b/src/xrpld/app/ledger/OrderBookDB.h.ai.md new file mode 100644 index 0000000000..1ae6d9e820 --- /dev/null +++ b/src/xrpld/app/ledger/OrderBookDB.h.ai.md @@ -0,0 +1,39 @@ +# `OrderBookDB.h` — Order Book Index Interface + +## Role in the System + +`OrderBookDB` is the pure abstract interface for the component that maintains an in-memory index of every active order book in the XRP Ledger. An "order book" in XRPL terms is a directional market pair: a `Book` specifies an `in` asset (what the taker pays) and an `out` asset (what the taker receives), plus an optional `domain` for permissioned DEX environments. The `OrderBookDB` exists to answer two different client needs efficiently: pathfinding queries ("what currencies can I reach from this asset?") and WebSocket subscriptions ("notify me when a trade happens in this market"). + +The file at `src/xrpld/app/ledger/OrderBookDB.h` is a stub pointing into the public include tree; the authoritative interface lives at `include/xrpl/ledger/OrderBookDB.h`. The concrete implementation is split across `OrderBookDBImpl.h` and `OrderBookDBImpl.cpp` in the same `src/xrpld/app/ledger/` directory. + +## Interface Design + +The `OrderBookDB` abstract class exposes six virtual methods that split cleanly into two concerns. + +**Index maintenance:** `setup()` and `addOrderBook()` keep the in-memory book map current. `setup()` is the periodic full rebuild — it is called when a new ledger is accepted and triggers a full scan of all ledger objects to reconstruct the book index from scratch. `addOrderBook()` is the incremental path, used when a new offer entry becomes known outside the full scan cycle. + +**Query and subscription:** `getBooksByTakerPays()` returns all books where the taker pays a given asset — the core primitive for pathfinding. `getBookSize()` returns the count without materializing the full vector. `isBookToXRP()` is a fast predicate checking whether any book converts a given asset into XRP, which pathfinding uses as a special hop. `getBookListeners()` and `makeBookListeners()` expose the `BookListeners` objects that dispatch transaction notifications to subscribed WebSocket clients. + +The `domain` parameter appearing on all three query methods reflects XRPL's support for permissioned DEX pools. Books can be scoped to a `Domain` (a `uint256` identifier), and the index maintains entirely separate structures for domain-scoped books versus global books, preventing domain lookup from bleeding into general pathfinding. + +## Implementation: Lazy Full Scans with Throttling + +`OrderBookDBImpl::setup()` deliberately avoids rescanning the ledger on every close. It compares the incoming ledger's sequence against the last-updated sequence (`seq_`, an `std::atomic`). If the new ledger is within 25,600 sequences ahead, or within 16 sequences behind, the update is skipped. This asymmetric tolerance handles both the normal forward progression of the ledger and scenarios like brief reorganizations. The atomic compare-exchange pattern (`seq_.exchange`) also ensures that if two threads race to trigger a full update, only one proceeds. + +In networked mode, the full scan is dispatched as a `jtUPDATE_PF` job to the `JobQueue`, keeping the validation and consensus threads unblocked while the potentially expensive full ledger traversal runs in the background. In standalone mode it runs synchronously since there are no competing threads to starve. + +## Update Strategy: Build-Then-Swap + +`OrderBookDBImpl::update()` builds completely fresh copies of all four internal maps — `allBooks_`, `xrpBooks_`, `domainBooks_`, and `xrpDomainBooks_` — into local variables, walking every `ltDIR_NODE` that carries an `sfExchangeRate` (the marker for an order book directory) and every `ltAMM` entry. Only after the full scan completes does it acquire `mLock` and swap the locals into the member fields atomically. This minimises lock-hold time: readers are only blocked during the pointer swap, not during the multi-second traversal. If a `SHAMapMissingNode` exception is thrown mid-scan (a common occurrence during ledger acquisition), `seq_` is reset to zero to force a re-attempt on the next call. + +## Concurrency Model + +All public methods that touch the in-memory maps take `mLock`, a `std::recursive_mutex`. The recursive variant is necessary because `makeBookListeners()` calls `getBookListeners()` while already holding the lock — both methods are `public` and `getBookListeners()` also acquires the same mutex. The `seq_` atomic is used independently of `mLock` for the setup/update throttling logic, since it only guards a single integer and the check-then-act pattern uses `exchange` rather than load/store. + +## Transaction Notification Path + +`processTxn()` is called for every transaction applied to an accepted ledger. It walks the transaction metadata looking for `ltOFFER` nodes (created, modified, or deleted offers) and resolves the corresponding `BookListeners` for each affected book. The key design detail is `havePublished`, a `hash_set` of subscriber IDs built locally per transaction. If a single transaction touches multiple offers in the same book, or if a client is subscribed to multiple books affected by one transaction, the set prevents duplicate delivery. This deduplication lives at the transaction level rather than inside `BookListeners::publish()`, keeping the listener logic simple. + +## Data Model + +`Book` holds two `Asset` fields (`in`, `out`) plus `std::optional domain`. `Asset` is a variant over `Issue` (currency + issuer account) and `MPTIssue` (a multi-purpose token identifier), which means the same `OrderBookDB` infrastructure covers both classical IOU markets and the newer MPT-based markets without requiring separate indexes. The internal maps use `hardened_hash_map` — a hash-flooding-resistant variant — because the keys are derived from ledger data and therefore attacker-influenced; a standard `unordered_map` would be vulnerable to deliberate hash collisions that degrade lookups to O(n). \ No newline at end of file diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.cpp.ai.json b/src/xrpld/app/ledger/OrderBookDBImpl.cpp.ai.json new file mode 100644 index 0000000000..37d9aec78f --- /dev/null +++ b/src/xrpld/app/ledger/OrderBookDBImpl.cpp.ai.json @@ -0,0 +1,579 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ServiceRegistry& registry", + "OrderBookDBConfig const& config" + ], + "lineno": 8, + "name": "OrderBookDBImpl" + } + ], + "code_paths": [ + { + "call_chain": [ + "make_OrderBookDB", + "OrderBookDBImpl::OrderBookDBImpl" + ], + "entry_point": "make_OrderBookDB", + "purpose": "Factory function to create an OrderBookDBImpl instance.", + "validation_points": [] + }, + { + "call_chain": [ + "OrderBookDBImpl::setup", + "OrderBookDBImpl::update" + ], + "entry_point": "OrderBookDBImpl::setup", + "purpose": "Triggers a full order book update if conditions are met. Schedules update job if not in standalone mode.", + "validation_points": [ + "if (!standalone_ && registry_.get().getOPs().isNeedNetworkLedger())", + "if ((ledger->seq() > seq) && ((ledger->seq() - seq) < 25600))", + "if ((ledger->seq() <= seq) && ((seq - ledger->seq()) < 16))", + "if (seq_.exchange(ledger->seq()) != seq)", + "if (pathSearchMax_ != 0)", + "if (pathSearchMax_ == 0) (in update)" + ] + }, + { + "call_chain": [ + "OrderBookDBImpl::update" + ], + "entry_point": "OrderBookDBImpl::update", + "purpose": "Performs the actual update of the order book database from the ledger.", + "validation_points": [ + "if (pathSearchMax_ == 0)", + "if (auto const seq = seq_.load(); seq > ledger->seq())" + ] + } + ], + "data_flows": [ + { + "field": "ledger (std::shared_ptr)", + "flow": [ + "setup() argument", + "used for validation (ledger->seq())", + "passed to update()", + "used to iterate ledger->sles" + ], + "origin": "Passed as argument to setup() and update()", + "transformations": [ + "ledger->seq() extracted for validation", + "ledger->sles iterated for order book entries" + ], + "validated_at": [ + "setup: if (!standalone_ && registry_.get().getOPs().isNeedNetworkLedger())", + "setup: if ((ledger->seq() > seq) && ((ledger->seq() - seq) < 25600))", + "setup: if ((ledger->seq() <= seq) && ((seq - ledger->seq()) < 16))", + "setup: if (seq_.exchange(ledger->seq()) != seq)", + "update: if (auto const seq = seq_.load(); seq > ledger->seq())" + ] + }, + { + "field": "ledger->seq() (ledger sequence number)", + "flow": [ + "setup: ledger->seq()", + "used in validation logic", + "passed to update()", + "used in update for further validation" + ], + "origin": "Read from ledger object", + "transformations": [ + "Compared to seq_ (atomic sequence number)", + "Used in job name for JobQueue" + ], + "validated_at": [ + "setup: if ((ledger->seq() > seq) && ((ledger->seq() - seq) < 25600))", + "setup: if ((ledger->seq() <= seq) && ((seq - ledger->seq()) < 16))", + "setup: if (seq_.exchange(ledger->seq()) != seq)", + "update: if (auto const seq = seq_.load(); seq > ledger->seq())" + ] + }, + { + "field": "pathSearchMax_", + "flow": [ + "OrderBookDBImpl::OrderBookDBImpl", + "setup: if (pathSearchMax_ != 0)", + "update: if (pathSearchMax_ == 0)" + ], + "origin": "OrderBookDBConfig::pathSearchMax (constructor argument)", + "transformations": [], + "validated_at": [ + "setup: if (pathSearchMax_ != 0)", + "update: if (pathSearchMax_ == 0)" + ] + }, + { + "field": "seq_ (atomic)", + "flow": [ + "setup: seq = seq_.load()", + "setup: if (seq_.exchange(ledger->seq()) != seq)", + "update: auto const seq = seq_.load()" + ], + "origin": "OrderBookDBImpl member, initialized to 0", + "transformations": [ + "Atomic load and exchange for concurrency control" + ], + "validated_at": [ + "setup: if (seq_.exchange(ledger->seq()) != seq)", + "update: if (auto const seq = seq_.load(); seq > ledger->seq())" + ] + } + ], + "description": "Implements the OrderBookDBImpl class for managing and updating the order book database in the XRPL server, including order book updates, lookups, and listener notifications based on ledger changes.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger (std::shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "if (!standalone_ && registry_.get().getOPs().isNeedNetworkLedger()) at OrderBookDBImpl::setup", + "issue_pattern": "Missing empty string validation for ledger (std::shared_ptr)", + "why_false_positive": "if (!standalone_ && registry_.get().getOPs().isNeedNetworkLedger()) validates ledger (std::shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger->seq() (ledger sequence number)", + "empty", + "string", + "validation" + ], + "evidence": "if ((ledger->seq() > seq) && ((ledger->seq() - seq) < 25600)) ... if ((ledger->seq() <= seq) && ((seq - ledger->seq()) < 16)) at OrderBookDBImpl::setup", + "issue_pattern": "Missing empty string validation for ledger->seq() (ledger sequence number)", + "why_false_positive": "if ((ledger->seq() > seq) && ((ledger->seq() - seq) < 25600)) ... if ((ledger->seq() <= seq) && ((seq - ledger->seq()) < 16)) validates ledger->seq() (ledger sequence number) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger->seq() (ledger sequence number)", + "range", + "bounds", + "validation" + ], + "evidence": "if ((ledger->seq() > seq) && ((ledger->seq() - seq) < 25600)) ... if ((ledger->seq() <= seq) && ((seq - ledger->seq()) < 16)) at OrderBookDBImpl::setup", + "issue_pattern": "Missing range validation for ledger->seq() (ledger sequence number)", + "why_false_positive": "if ((ledger->seq() > seq) && ((ledger->seq() - seq) < 25600)) ... if ((ledger->seq() <= seq) && ((seq - ledger->seq()) < 16)) validates ledger->seq() (ledger sequence number) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger->seq() (ledger sequence number)", + "empty", + "string", + "validation" + ], + "evidence": "if (seq_.exchange(ledger->seq()) != seq) at OrderBookDBImpl::setup", + "issue_pattern": "Missing empty string validation for ledger->seq() (ledger sequence number)", + "why_false_positive": "if (seq_.exchange(ledger->seq()) != seq) validates ledger->seq() (ledger sequence number) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "pathSearchMax_", + "empty", + "string", + "validation" + ], + "evidence": "if (pathSearchMax_ != 0) at OrderBookDBImpl::setup", + "issue_pattern": "Missing empty string validation for pathSearchMax_", + "why_false_positive": "if (pathSearchMax_ != 0) validates pathSearchMax_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "pathSearchMax_", + "empty", + "string", + "validation" + ], + "evidence": "if (pathSearchMax_ == 0) at OrderBookDBImpl::update", + "issue_pattern": "Missing empty string validation for pathSearchMax_", + "why_false_positive": "if (pathSearchMax_ == 0) validates pathSearchMax_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger->seq() (ledger sequence number)", + "empty", + "string", + "validation" + ], + "evidence": "if (auto const seq = seq_.load(); seq > ledger->seq()) at OrderBookDBImpl::update", + "issue_pattern": "Missing empty string validation for ledger->seq() (ledger sequence number)", + "why_false_positive": "if (auto const seq = seq_.load(); seq > ledger->seq()) validates ledger->seq() (ledger sequence number) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger->seq() (ledger sequence number)", + "range", + "bounds", + "validation" + ], + "evidence": "if (auto const seq = seq_.load(); seq > ledger->seq()) at OrderBookDBImpl::update", + "issue_pattern": "Missing range validation for ledger->seq() (ledger sequence number)", + "why_false_positive": "if (auto const seq = seq_.load(); seq > ledger->seq()) validates ledger->seq() (ledger sequence number) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "registry_.get().isStopping()", + "empty", + "string", + "validation" + ], + "evidence": "if (registry_.get().isStopping()) at OrderBookDBImpl::update (inside ledger walk loop)", + "issue_pattern": "Missing empty string validation for registry_.get().isStopping()", + "why_false_positive": "if (registry_.get().isStopping()) validates registry_.get().isStopping() for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/OrderBookDBImpl.cpp", + "functions": [ + { + "args": [ + "ServiceRegistry& registry", + "OrderBookDBConfig const& config" + ], + "lineno": 15, + "name": "make_OrderBookDB" + }, + { + "args": [ + "std::shared_ptr const& ledger" + ], + "lineno": 20, + "name": "OrderBookDBImpl::setup" + }, + { + "args": [ + "std::shared_ptr const& ledger" + ], + "lineno": 44, + "name": "OrderBookDBImpl::update" + }, + { + "args": [ + "Book const& book" + ], + "lineno": 120, + "name": "OrderBookDBImpl::addOrderBook" + }, + { + "args": [ + "Asset const& asset", + "std::optional const& domain" + ], + "lineno": 139, + "name": "OrderBookDBImpl::getBooksByTakerPays" + }, + { + "args": [ + "Asset const& asset", + "std::optional const& domain" + ], + "lineno": 163, + "name": "OrderBookDBImpl::getBookSize" + }, + { + "args": [ + "Asset const& asset", + "std::optional const& domain" + ], + "lineno": 179, + "name": "OrderBookDBImpl::isBookToXRP" + }, + { + "args": [ + "Book const& book" + ], + "lineno": 187, + "name": "OrderBookDBImpl::makeBookListeners" + }, + { + "args": [ + "Book const& book" + ], + "lineno": 202, + "name": "OrderBookDBImpl::getBookListeners" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "AcceptedLedgerTx const& alTx", + "MultiApiJson const& jvObj" + ], + "lineno": 215, + "name": "OrderBookDBImpl::processTxn" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "OrderBookDBImpl is a core ledger component. Typical test coverage would be in integration and unit tests for order book and pathfinding logic. Look for test files such as 'OrderBookDB_test.cpp', 'Path_find_test.cpp', or integration tests in 'ledger' or 'app' test suites. However, the specific validation branches (e.g., sequence number edge cases, pathSearchMax_ toggling, standalone mode) may not be exhaustively tested unless there are explicit tests for ledger sequence rollovers, network/standalone toggling, and pathfinding enable/disable. Error handling via exceptions may not be directly tested unless tests inject malformed ledgers or simulate stopping conditions. Gaps likely exist in concurrency edge cases and rare sequence number transitions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, business logic checks)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (early return, logs warning)", + "field": "ledger (std::shared_ptr)", + "location": "OrderBookDBImpl::setup", + "validated_by": "if (!standalone_ && registry_.get().getOPs().isNeedNetworkLedger())", + "validates": [ + "Checks if not in standalone mode and network ledger is needed", + "If so, skips order book update" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return)", + "field": "ledger->seq() (ledger sequence number)", + "location": "OrderBookDBImpl::setup", + "validated_by": "if ((ledger->seq() > seq) && ((ledger->seq() - seq) < 25600)) ... if ((ledger->seq() <= seq) && ((seq - ledger->seq()) < 16))", + "validates": [ + "Checks if ledger sequence is within a certain range of previous sequence", + "Prevents redundant or too-frequent updates" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return)", + "field": "ledger->seq() (ledger sequence number)", + "location": "OrderBookDBImpl::setup", + "validated_by": "if (seq_.exchange(ledger->seq()) != seq)", + "validates": [ + "Ensures only one update per sequence", + "Prevents race conditions" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return)", + "field": "pathSearchMax_", + "location": "OrderBookDBImpl::setup", + "validated_by": "if (pathSearchMax_ != 0)", + "validates": [ + "Checks if pathfinding is enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return)", + "field": "pathSearchMax_", + "location": "OrderBookDBImpl::update", + "validated_by": "if (pathSearchMax_ == 0)", + "validates": [ + "Checks if pathfinding is enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return, logs debug)", + "field": "ledger->seq() (ledger sequence number)", + "location": "OrderBookDBImpl::update", + "validated_by": "if (auto const seq = seq_.load(); seq > ledger->seq())", + "validates": [ + "Skips update if a newer update is pending" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return, logs info)", + "field": "registry_.get().isStopping()", + "location": "OrderBookDBImpl::update (inside ledger walk loop)", + "validated_by": "if (registry_.get().isStopping())", + "validates": [ + "Halts update if the process is stopping" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.cpp.ai.md b/src/xrpld/app/ledger/OrderBookDBImpl.cpp.ai.md new file mode 100644 index 0000000000..0e6112350f --- /dev/null +++ b/src/xrpld/app/ledger/OrderBookDBImpl.cpp.ai.md @@ -0,0 +1,50 @@ +# `OrderBookDBImpl.cpp` — In-Memory Order Book Index for Pathfinding and Subscriptions + +`OrderBookDBImpl` is the concrete implementation of the `OrderBookDB` interface. It maintains an in-memory index of every order book and AMM pool present in the ledger, serving two distinct consumers: the pathfinding engine (which needs to know which books exist without scanning the ledger each time) and the WebSocket subscription system (which needs to fan out trade notifications to clients watching specific books). + +## Data Model + +The internal index is organized around the "taker pays" direction — that is, given an asset that a market participant is willing to pay, what are the possible assets they can receive? Four separate data structures cover the full address space: + +- `allBooks_` — a `hardened_hash_map>` mapping each paying asset to the set of receivable assets for global (non-domain) books. +- `domainBooks_` — the same structure keyed by `pair`, scoping books to a specific permissioned DEX domain. +- `xrpBooks_` / `xrpDomainBooks_` — fast-path sets recording which assets have at least one book leading to XRP, used by the pathfinder's XRP-termination heuristic without needing to iterate the full book set. + +`Asset` is a variant type covering both traditional `Issue` (currency + issuer) and the newer `MPTIssue` (Multi-Purpose Token ID), so the index handles both asset kinds uniformly. + +## Full Rebuild: `setup()` and `update()` + +`setup()` is the entry point called each time a ledger is accepted. Its primary job is rate-limiting: a full ledger walk to rebuild the index is expensive, so the method suppresses redundant calls through two sequence number checks. If the new ledger is more than zero but fewer than 25,600 sequences ahead of the last full update, the call is silently skipped — this covers normal chain advance where the order book topology changes slowly. Conversely, if the new ledger is within 16 sequences *behind* the last update (possible during network catch-up or short-range reorg), it is also skipped to avoid regressing state. The threshold asymmetry (25,600 vs. 16) reflects that forward-advancing ledgers are far more common. + +The compare-and-swap via `seq_.exchange()` prevents duplicate jobs from being enqueued if two threads call `setup()` concurrently with the same ledger. If the atomic exchange reveals that another caller already claimed this ledger sequence, the second caller returns immediately. + +When an update is warranted, `update()` is dispatched to the `JobQueue` under job type `jtUPDATE_PF` in networked mode, or called inline in standalone mode. This async dispatch matters for performance: the ledger walk happens on a dedicated thread pool thread rather than blocking whatever called `setup()`. + +`update()` performs a linear scan of all `sle` (serialized ledger entries) in the ledger snapshot. It looks for two entry types: + +- **`ltDIR_NODE` with `sfExchangeRate`** at the root index: these are order book root directories. The entry carries `sfTakerPaysCurrency`/`sfTakerPaysIssuer` or `sfTakerPaysMPT` (and corresponding Gets fields), from which the full `Book` — including an optional `sfDomainID` — is reconstructed. +- **`ltAMM`**: AMM pools are synthetic two-sided books; both `(asset1 → asset2)` and `(asset2 → asset1)` directions are registered, since an AMM pool services swaps in either direction. + +The update builds its new maps entirely into *local* variables, then acquires `mLock` only for the final `swap()` calls. This copy-and-swap pattern is intentional: the ledger walk may take milliseconds on a large ledger, and queries to `getBooksByTakerPays()` must not be blocked for the entire duration. + +If `isStopping()` is detected mid-walk, the update resets `seq_` to `0` and returns, ensuring the next `setup()` call will trigger a fresh attempt rather than assuming the old data is still current. The same reset happens when a `SHAMapMissingNode` exception is caught, which occurs if the node store is incomplete — the incomplete result is discarded rather than partially replacing good data. + +After a successful swap, `ledgerMaster.newOrderBookDB()` is called to wake any components that depend on order book availability. + +## Incremental Updates: `addOrderBook()` + +`addOrderBook()` offers a fast path for adding a single book without a full rebuild. This is used when a `jtOFFER_CREATE` transaction creates a new book that did not previously exist — it keeps the index current between full `setup()` cycles without the overhead of a ledger scan. + +## WebSocket Subscription Pipeline + +`processTxn()` handles the subscription fan-out side. When a transaction is accepted into a ledger, its metadata contains `sfModifiedNode`, `sfCreatedNode`, and `sfDeletedNode` entries. For each `ltOFFER` node, the method extracts the book identity from `sfTakerGets`/`sfTakerPays` in the appropriate field (`sfPreviousFields` for modifications, `sfNewFields` for creations, `sfFinalFields` for deletions) and calls `publish()` on the corresponding `BookListeners` instance. + +The `havePublished` set (a `hash_set`) is maintained per-transaction to avoid duplicate delivery. A single transaction can touch dozens of offer entries in the same book (as one big order crosses multiple resting offers), and a client might also subscribe to multiple books touched by one transaction. Without deduplication, a client would receive the same transaction JSON multiple times. + +`BookListeners` itself stores weak references (`std::weak_ptr`) to subscriber sessions. Expired sessions are pruned lazily during `publish()` when the weak pointer can no longer be promoted. + +`makeBookListeners()` and `getBookListeners()` form an idempotent registry pattern: the former creates a `BookListeners` if one doesn't exist for a book, the latter is a read-only lookup. Both hold `mLock`; `makeBookListeners()` calls `getBookListeners()` internally, which requires `mLock` to be `std::recursive_mutex` rather than a plain mutex. + +## Concurrency Summary + +`seq_` is the only field accessed without `mLock`; it uses `std::atomic` to coordinate the race between concurrent `setup()` callers and between `setup()` and the async `update()` job. Everything else — all four book maps, the listeners map — is guarded by `mLock`. The copy-and-swap in `update()` minimizes the critical section for the expensive rebuild path, while `processTxn()` and query methods hold the lock for the duration of their work since those operations are much shorter. \ No newline at end of file diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.h.ai.json b/src/xrpld/app/ledger/OrderBookDBImpl.h.ai.json new file mode 100644 index 0000000000..3d02ec306b --- /dev/null +++ b/src/xrpld/app/ledger/OrderBookDBImpl.h.ai.json @@ -0,0 +1,46 @@ +{ + "args": [ + { + "lineno": 22, + "name": "registry" + }, + { + "lineno": 22, + "name": "config" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "OrderBookDBConfig" + }, + { + "args": [ + "registry", + "config" + ], + "lineno": 28, + "name": "OrderBookDBImpl" + } + ], + "description": "Defines the OrderBookDBImpl class, which implements the OrderBookDB interface for managing order books in the XRPL ledger, including configuration, book management, and listener support.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/OrderBookDBImpl.h", + "functions": [ + { + "args": [ + "registry", + "config" + ], + "lineno": 22, + "name": "make_OrderBookDB" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/OrderBookDBImpl.h.ai.md b/src/xrpld/app/ledger/OrderBookDBImpl.h.ai.md new file mode 100644 index 0000000000..c1334671d2 --- /dev/null +++ b/src/xrpld/app/ledger/OrderBookDBImpl.h.ai.md @@ -0,0 +1,53 @@ +# `OrderBookDBImpl.h` — Order Book Index and Subscription Dispatcher + +## Role in the System + +`OrderBookDBImpl` is the concrete implementation of the `OrderBookDB` interface, which maintains a live in-memory index of all order books present in the XRP Ledger. Its two primary responsibilities are distinct but coupled: first, it provides fast lookup structures that pathfinding and other subsystems query to discover which currency pairs have active markets; second, it acts as a subscription fanout engine, routing accepted-ledger transactions to WebSocket clients who have subscribed to specific books. + +The header declares the `OrderBookDBConfig` plain struct that carries the two configuration knobs — `pathSearchMax` and `standalone` — as well as the `make_OrderBookDB` factory function that follows the XRPL convention of hiding the concrete type behind a factory that returns `unique_ptr`. + +## Data Structures and Their Design Choices + +The private state reveals careful attention to both performance and security: + +``` +hardened_hash_map> allBooks_; +hardened_hash_map, hardened_hash_set> domainBooks_; +hash_set xrpBooks_; +hash_set> xrpDomainBooks_; +``` + +The main book maps use `hardened_hash_map` — a hash map whose seed is randomized at construction time using `std::random_device` — rather than a plain `hash_map`. This is a DoS-hardening measure: `Asset` values are derived directly from untrusted ledger data, and a malicious actor could submit transactions containing crafted currency/issuer combinations that all collide in a predictable hash table, degrading lookup from O(1) to O(n). By randomizing the hash seed per process instance, the collision structure becomes unpredictable to the attacker. + +The `xrpBooks_` and `xrpDomainBooks_` sets are `hash_set` (standard, non-hardened) and exist purely as fast existence tests. The asymmetry reflects that these are consulted millions of times during pathfinding but have a bounded size (one entry per currency that has an XRP book), making them a legitimate performance optimization. + +The `mListeners` map uses a plain `hash_map` because its keys are locally trusted (the node registers them) and not subject to adversarial injection. + +`seq_` is `std::atomic` and doubles as a version stamp and a compare-and-exchange gate that prevents redundant full scans. + +## The `setup()` / `update()` Split + +`setup()` is called on every accepted ledger but is intentionally cheap for common cases. It applies two skip conditions before doing any work: + +- If the ledger is between 1 and 25599 sequences ahead of the last full update, skip — the in-memory index is still accurate enough. +- If the ledger is within 15 sequences behind the stored sequence (a minor reorg), skip. + +If neither condition fires, `setup()` does a `seq_.exchange(ledger->seq())` and checks whether another thread already claimed this ledger; only one caller wins the race. The winner either calls `update()` directly (standalone mode) or enqueues it as a `jtUPDATE_PF` job on the JobQueue (networked mode), keeping the ledger-application hot path non-blocking. + +`update()` performs the full ledger walk. It iterates every state ledger entry (`ledger->sles`), identifying `ltDIR_NODE` entries that carry an `sfExchangeRate` field with `sfRootIndex == key()` — those are the canonical roots of offer directories, i.e., a single price level in an order book. It reconstructs each `Book` from `sfTakerPaysCurrency`/`sfTakerPaysIssuer` or the newer `sfTakerPaysMPT` field (for Multi-Purpose Tokens), handles optional `sfDomainID` for domain-restricted books, and separately handles `ltAMM` entries which expose two implicit reverse books for their asset pair. All of this populates local copies of the four maps; once the walk completes, the maps are swapped under `mLock` in a single critical section, making the update atomic from the perspective of readers. If the process is stopping or a `SHAMapMissingNode` exception is thrown mid-scan, `seq_` is reset to 0 so the next `setup()` call will trigger a fresh full rebuild rather than skipping. + +## Transaction Subscription Dispatch via `processTxn()` + +When a ledger is accepted and applied, `processTxn()` is called for each transaction. It iterates the transaction metadata nodes, looking for affected `ltOFFER` entries. For each modified, created, or deleted offer node it extracts the `TakerPays`/`TakerGets` amounts to reconstruct the book identity, then looks up the corresponding `BookListeners` and calls `publish()`. + +The `hash_set havePublished` local variable is a deduplication guard: a single transaction may touch dozens of offer nodes spread across many price levels of the same book, or touch multiple books to which a single client has subscribed. Without this guard, the same client could receive the same transaction notification many times. `BookListeners::publish()` records each subscriber's ID in `havePublished` before sending, and skips it on subsequent calls within the same transaction. + +## Concurrency Model + +All mutable state is protected by `mLock`, declared as `std::recursive_mutex`. The recursive variant is necessary because `makeBookListeners()` calls `getBookListeners()` while already holding the lock, and `processTxn()` also holds the lock while calling `getBookListeners()`. This is a deliberate convenience rather than an oversight — the critical sections are short and `recursive_mutex` avoids introducing a separate unlocked internal helper. + +The only lockless coordination is between `setup()` calls via `seq_`, which uses `std::atomic` compare-and-exchange to ensure that only one `update()` job is enqueued per ledger sequence, even under concurrent invocations from different threads. + +## Relationship to Pathfinding Configuration + +`pathSearchMax_` is the primary gate for whether pathfinding is active at all. Both `setup()` and `update()` return immediately when it is zero, meaning the entire order book index remains empty — a deliberate operational mode that allows a node operator to disable pathfinding to reduce memory and CPU cost. The `standalone_` flag affects only the threading model for `update()`, not its correctness. \ No newline at end of file diff --git a/src/xrpld/app/ledger/TransactionMaster.h.ai.json b/src/xrpld/app/ledger/TransactionMaster.h.ai.json new file mode 100644 index 0000000000..af039a5a8a --- /dev/null +++ b/src/xrpld/app/ledger/TransactionMaster.h.ai.json @@ -0,0 +1,90 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app" + ], + "lineno": 12, + "name": "TransactionMaster" + } + ], + "description": "Defines the TransactionMaster class, which tracks all transactions in memory, providing methods to fetch, cache, and manage transactions for the XRPL application.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/TransactionMaster.h", + "functions": [ + { + "args": [ + "Application& app" + ], + "lineno": 15, + "name": "TransactionMaster" + }, + { + "args": [ + "uint256 const&" + ], + "lineno": 20, + "name": "fetch_from_cache" + }, + { + "args": [ + "uint256 const&", + "error_code_i& ec" + ], + "lineno": 22, + "name": "fetch" + }, + { + "args": [ + "uint256 const&", + "ClosedInterval const& range", + "error_code_i& ec" + ], + "lineno": 32, + "name": "fetch" + }, + { + "args": [ + "boost::intrusive_ptr const& item", + "SHAMapNodeType type", + "std::uint32_t uCommitLedger" + ], + "lineno": 39, + "name": "fetch" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t ledger", + "std::optional tseq", + "std::optional netID" + ], + "lineno": 44, + "name": "inLedger" + }, + { + "args": [ + "std::shared_ptr* pTransaction" + ], + "lineno": 51, + "name": "canonicalize" + }, + { + "args": [], + "lineno": 54, + "name": "sweep" + }, + { + "args": [], + "lineno": 57, + "name": "getCache" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/TransactionMaster.h.ai.md b/src/xrpld/app/ledger/TransactionMaster.h.ai.md new file mode 100644 index 0000000000..2f8e829927 --- /dev/null +++ b/src/xrpld/app/ledger/TransactionMaster.h.ai.md @@ -0,0 +1,29 @@ +# `TransactionMaster` — In-Memory Transaction Cache and Lookup Coordinator + +`TransactionMaster` is the single point of authority for `Transaction` objects that are live in process memory. Every subsystem that needs to look up, intern, or update the status of a transaction goes through this class rather than bypassing directly to the database or re-deserializing from a `SHAMapItem`. Its existence eliminates duplicate object allocation: two callers asking for the same hash receive the same `shared_ptr`, and status mutations (e.g., marking a transaction `COMMITTED`) performed by one caller are immediately visible to all others holding a reference. + +## Core Storage + +The backing store is a `TaggedCache` named `"TransactionCache"`, constructed in `Application` with a capacity ceiling of 65,536 entries and a 30-minute expiry. `TaggedCache` is a hybrid map-plus-LRU-cache: while a strong reference is held by the cache itself, the entry stays alive and can be retrieved by key. After eviction, the map retains a weak reference for as long as any outside caller holds a `shared_ptr`; those callers can therefore keep an entry alive past the TTL without holding a separate lock. The mutex discipline is entirely internal to `TaggedCache` (it uses a `std::recursive_mutex`), so callers of `TransactionMaster` do not need to take a separate lock. + +## The `fetch` Family + +Three overloads of `fetch()` serve different call sites: + +**`fetch(hash, ec)`** is the RPC-layer workhorse, used by `Tx.cpp` when the `tx` command has no ledger range constraint. It first calls `fetch_from_cache(hash)`: if the result is present and already validated (i.e., `mLedgerIndex != 0`), the cache-only read would still miss its metadata, so the code deliberately skips that path and falls through to `Transaction::load()`. Conversely, if the cache hit is *un*validated, the transaction is returned immediately with a null `TxMeta` pointer — the caller asked about a transaction that hasn't landed in a closed ledger yet, so there is no metadata to return. After a database hit, `canonicalize_replace_client` inserts the freshly loaded object into the cache (or, if another thread raced and inserted first, replaces the caller's local pointer with the already-cached instance). + +**`fetch(hash, range, ec)`** mirrors the above but threads a `ClosedInterval` through to `Transaction::load()`. This range tells the database layer which ledger sequence window to search, enabling the return value's `TxSearched` variant to distinguish `TxSearched::All` (every ledger in the range was present), `TxSearched::Some` (some ledgers were missing), and `TxSearched::Unknown`. RPC clients use this to surface a correct response indicating whether a "not found" result is definitive or provisional. + +**`fetch(SHAMapItem, type, uCommitLedger)`** serves consensus and ledger-application paths that already have the raw serialized bytes from the SHAMap. It first checks the cache by the item's key (hash). On a miss, it directly deserializes an `STTx` from the item's slice — handling both `tnTRANSACTION_NM` (transaction bytes only) and `tnTRANSACTION_MD` (transaction-plus-metadata, requiring an extra VL-length decode) node types. On a cache hit, if `uCommitLedger` is non-zero, the cached `Transaction` is updated to `COMMITTED` status before the `STTx` is returned. Notice that this overload returns a bare `shared_ptr`, not a `Transaction` wrapper — the SHAMap-based callers only need the immutable protocol object, not the lifecycle metadata. + +## `canonicalize` — Object Deduplication + +`canonicalize(std::shared_ptr*)` is a pointer-rewrite operation. The caller passes the address of its own `shared_ptr`; if the cache already holds an entry for that hash, `TaggedCache::canonicalize_replace_client` atomically replaces the caller's pointer with the cached instance. The net effect is that, after the call, the caller's variable points to whichever object the cache considers canonical. This matters when the same transaction arrives through multiple ingestion paths (peer messages, local submission, database replay) roughly simultaneously — rather than N separate heap objects, all callers converge on one. `NetworkOPs` calls this after locally submitting a transaction; `PeerImp` calls it when a transaction arrives over the network. + +## `inLedger` — Status Promotion Without Insertion + +`inLedger(hash, ledger, tseq, netID)` has a subtle contract: it returns `false` and does nothing if the transaction is *not already in the cache*. It intentionally does not fetch from the database or insert a new entry. Its sole purpose is to promote a transaction that is already cached — presumably from an earlier broadcast phase — to `COMMITTED` status now that the database layer has confirmed which ledger it landed in. This is called from `Node.cpp` during the SQLite-backed transaction loading path. The asymmetry (update-only, never insert) prevents stale database entries from populating the in-memory cache during bulk historical scans. + +## Lifecycle + +`TransactionMaster` is owned as a value member of `ApplicationImp`, constructed before the application starts processing and destroyed with it. `sweep()` is called periodically from the application's sweep loop (along with other caches) to evict expired entries and reclaim memory. `getCache()` exposes a direct reference to the `TaggedCache` for metrics collection and the sweep callback that `SHAMapStoreImp` registers to correlate the transaction cache with node-store rotation events. \ No newline at end of file diff --git a/src/xrpld/app/ledger/TransactionStateSF.cpp.ai.json b/src/xrpld/app/ledger/TransactionStateSF.cpp.ai.json new file mode 100644 index 0000000000..5b286c417f --- /dev/null +++ b/src/xrpld/app/ledger/TransactionStateSF.cpp.ai.json @@ -0,0 +1,151 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SHAMap::visitNodes / SHAMap::fetchNode / SHAMap::addKnownNode (likely callers, not shown in this file)", + "TransactionStateSF::gotNode" + ], + "entry_point": "TransactionStateSF::gotNode", + "purpose": "Handles a node received from the SHAMap, validates its type, and stores it in the database.", + "validation_points": [ + "TransactionStateSF::gotNode: XRPL_ASSERT(type != SHAMapNodeType::tnTRANSACTION_NM)" + ] + }, + { + "call_chain": [ + "SHAMap::getMissingNodes / SHAMap::getNodeFat / SHAMap::getFetchPack (likely callers, not shown in this file)", + "TransactionStateSF::getNode" + ], + "entry_point": "TransactionStateSF::getNode", + "purpose": "Fetches a node's data from the fetch pack using its hash.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "type (SHAMapNodeType)", + "flow": [ + "Caller provides type", + "TransactionStateSF::gotNode receives type", + "XRPL_ASSERT checks type != tnTRANSACTION_NM", + "If valid, proceeds to store node" + ], + "origin": "Caller of TransactionStateSF::gotNode (e.g., SHAMap traversal code)", + "transformations": [ + "Validation: XRPL_ASSERT ensures type is not tnTRANSACTION_NM" + ], + "validated_at": "TransactionStateSF::gotNode" + }, + { + "field": "nodeData (Blob)", + "flow": [ + "Caller provides nodeData", + "TransactionStateSF::gotNode receives nodeData", + "nodeData is moved into db_.store" + ], + "origin": "Caller of TransactionStateSF::gotNode (node data from SHAMap)", + "transformations": [ + "std::move to transfer ownership" + ], + "validated_at": "No explicit validation" + }, + { + "field": "nodeHash (SHAMapHash)", + "flow": [ + "Caller provides nodeHash", + "TransactionStateSF::gotNode: nodeHash.as_uint256() used as key in db_.store", + "TransactionStateSF::getNode: nodeHash.as_uint256() used as key in fp_.getFetchPack" + ], + "origin": "Caller of TransactionStateSF::gotNode or getNode", + "transformations": [ + "Conversion: nodeHash.as_uint256()" + ], + "validated_at": "No explicit validation" + }, + { + "field": "ledgerSeq (std::uint32_t)", + "flow": [ + "Caller provides ledgerSeq", + "TransactionStateSF::gotNode passes ledgerSeq to db_.store" + ], + "origin": "Caller of TransactionStateSF::gotNode", + "transformations": [], + "validated_at": "No explicit validation" + } + ], + "description": "Implements the TransactionStateSF class methods for handling transaction state nodes during ledger synchronization, including storing and fetching node data.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (SHAMapNodeType type)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at TransactionStateSF::gotNode", + "issue_pattern": "Missing empty string validation for type (SHAMapNodeType type)", + "why_false_positive": "XRPL_ASSERT macro validates type (SHAMapNodeType type) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/TransactionStateSF.cpp", + "functions": [ + { + "args": [ + "bool", + "SHAMapHash const& nodeHash", + "std::uint32_t ledgerSeq", + "Blob&& nodeData", + "SHAMapNodeType type" + ], + "lineno": 6, + "name": "TransactionStateSF::gotNode" + }, + { + "args": [ + "SHAMapHash const& nodeHash" + ], + "lineno": 18, + "name": "TransactionStateSF::getNode" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "Direct unit tests for TransactionStateSF are not shown in this file. Likely, this code is tested indirectly via SHAMap or ledger synchronization tests, which exercise fetch pack and node storage logic. The validation (XRPL_ASSERT) for node type may not be directly tested unless there are tests that intentionally pass tnTRANSACTION_NM to gotNode. Gaps: No explicit tests for assertion failure paths or for invalid node types; no direct tests for data corruption or malformed input.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro (custom assertion, not a general validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "type (SHAMapNodeType type)", + "location": "TransactionStateSF::gotNode", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures that the input 'type' is not equal to SHAMapNodeType::tnTRANSACTION_NM" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/TransactionStateSF.cpp.ai.md b/src/xrpld/app/ledger/TransactionStateSF.cpp.ai.md new file mode 100644 index 0000000000..d15681b3af --- /dev/null +++ b/src/xrpld/app/ledger/TransactionStateSF.cpp.ai.md @@ -0,0 +1,53 @@ +# `TransactionStateSF.cpp` — Transaction Tree Sync Filter + +## Role in the System + +`TransactionStateSF` is a thin but semantically important class that bridges the SHAMap synchronization machinery and the node store during ledger acquisition. When a rippled node is catching up and pulling a ledger's transaction tree from peers, the `SHAMap` sync engine needs a two-way hook: a place to *store* nodes that arrive from the network, and a place to *look up* nodes that a peer may have pre-packaged in a fetch pack. `TransactionStateSF` provides both of those hooks, scoped specifically to the transaction tree (`txMap`) as opposed to the account-state tree. + +## Relationship to `SHAMapSyncFilter` + +The class inherits from `SHAMapSyncFilter`, a non-copyable abstract interface with two pure virtuals: `gotNode()` and `getNode()`. The filter is passed into `SHAMap` add/sync operations so the map can notify the application layer when new nodes are encountered or request cached data before going to the database. `TransactionStateSF` and `AccountStateSF` are the two concrete implementations used during ledger sync; they differ only in the node type tag they write to the database and in one assertion. + +## `gotNode()` — Persisting Incoming Nodes + +When the SHAMap sync engine receives a tree node from a peer and verifies its hash, it calls `gotNode()` so the application layer can durably store it: + +```cpp +db_.store(hotTRANSACTION_NODE, std::move(nodeData), nodeHash.as_uint256(), ledgerSeq); +``` + +The `hotTRANSACTION_NODE` tag (value `4` in the `NodeObjectType` enum) distinguishes transaction-tree nodes from account-state nodes (`hotACCOUNT_NODE = 3`) and ledger headers (`hotLEDGER = 1`) within the shared node store. This tag is preserved through serialisation and is used by the `DecodedBlob` layer on read-back to reconstitute the correct object type. + +Node data is moved — not copied — into `db_.store`, transferring ownership in a single step and avoiding an unnecessary heap allocation. + +The `fromFilter` boolean is intentionally ignored (unnamed parameter). It tells the callee whether the data originated from the filter's own `getNode()` call rather than from a peer message, but `TransactionStateSF` doesn't need to distinguish these origins: both paths are equally trustworthy by the time `gotNode()` is invoked. + +## The `tnTRANSACTION_NM` Assertion + +The single guard in this file asserts that the arriving node's type is *not* `SHAMapNodeType::tnTRANSACTION_NM` (transaction without metadata): + +```cpp +XRPL_ASSERT( + type != SHAMapNodeType::tnTRANSACTION_NM, + "xrpl::TransactionStateSF::gotNode : valid input"); +``` + +This is not redundant pedantry. In the XRPL data model there are two kinds of transaction leaf nodes: `tnTRANSACTION_NM` (no metadata, used for proposed/standalone transactions) and `tnTRANSACTION_MD` (with metadata, the form committed into closed ledgers). A `TransactionStateSF` instance is only ever constructed for a ledger's *committed* transaction tree. Receiving a `tnTRANSACTION_NM` leaf in that context signals a logic error — likely a misrouted node from the consensus transaction set, which is handled by the separate `ConsensusTransSetSF` filter. The assertion enforces this invariant at the boundary between sync machinery and persistent storage. The companion `AccountStateSF::gotNode()` carries no such assertion because the account-state tree has only one leaf type. + +## `getNode()` — Fetch Pack Lookup + +```cpp +return fp_.getFetchPack(nodeHash.as_uint256()); +``` + +Before the SHAMap asks the database or the network for a missing node, it calls `getNode()` to check the *fetch pack* — a temporary, peer-supplied cache of ledger nodes for efficient bulk sync. `AbstractFetchPackContainer` is a minimal interface (`getFetchPack(uint256)`) that decouples `TransactionStateSF` from the full `LedgerMaster` object, even though `LedgerMaster` is what actually implements it in production (as seen in `InboundLedger.cpp`). The fetch pack is a speculative optimisation: if the peer predicted which nodes you'd need and packed them in advance, `getNode()` returns them without a round trip. On cache miss it returns `std::nullopt` and the sync engine falls back to requesting the node from the network. + +## Usage Context + +`TransactionStateSF` is constructed stack-locally in `InboundLedger.cpp` at several call sites, each pairing the transaction map's family database with `app_.getLedgerMaster()` as the fetch pack container: + +```cpp +TransactionStateSF filter(mLedger->txMap().family().db(), app_.getLedgerMaster()); +``` + +This scoping is deliberate: the filter lives only for the duration of a single sync pass, holding non-owning references to the database and ledger master, both of which outlive any individual sync operation. \ No newline at end of file diff --git a/src/xrpld/app/ledger/TransactionStateSF.h.ai.json b/src/xrpld/app/ledger/TransactionStateSF.h.ai.json new file mode 100644 index 0000000000..de2a13f857 --- /dev/null +++ b/src/xrpld/app/ledger/TransactionStateSF.h.ai.json @@ -0,0 +1,79 @@ +{ + "args": [ + { + "lineno": 11, + "name": "db" + }, + { + "lineno": 11, + "name": "fp" + }, + { + "lineno": 15, + "name": "fromFilter" + }, + { + "lineno": 16, + "name": "nodeHash" + }, + { + "lineno": 17, + "name": "ledgerSeq" + }, + { + "lineno": 18, + "name": "nodeData" + }, + { + "lineno": 19, + "name": "type" + } + ], + "classes": [ + { + "args": [ + "NodeStore::Database& db", + "AbstractFetchPackContainer& fp" + ], + "lineno": 8, + "name": "TransactionStateSF" + } + ], + "description": "Defines the TransactionStateSF class, a sync filter for transaction tree nodes during ledger synchronization in the XRPL, providing methods to handle and fetch transaction state nodes.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/TransactionStateSF.h", + "functions": [ + { + "args": [ + "NodeStore::Database& db", + "AbstractFetchPackContainer& fp" + ], + "lineno": 11, + "name": "TransactionStateSF" + }, + { + "args": [ + "bool fromFilter", + "SHAMapHash const& nodeHash", + "std::uint32_t ledgerSeq", + "Blob&& nodeData", + "SHAMapNodeType type" + ], + "lineno": 15, + "name": "gotNode" + }, + { + "args": [ + "SHAMapHash const& nodeHash" + ], + "lineno": 22, + "name": "getNode" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/TransactionStateSF.h.ai.md b/src/xrpld/app/ledger/TransactionStateSF.h.ai.md new file mode 100644 index 0000000000..c128a2de80 --- /dev/null +++ b/src/xrpld/app/ledger/TransactionStateSF.h.ai.md @@ -0,0 +1,57 @@ +# `TransactionStateSF` — Transaction Tree Sync Filter + +## Role in the System + +`TransactionStateSF` is a narrow adapter class that bridges the SHAMap synchronization machinery with the persistent node store and the fetch-pack cache during ledger acquisition. Every validated XRPL ledger contains two Merkle trees: an account-state tree and a transaction tree. When a node is catching up and has to fetch a ledger it does not yet have locally, it must reconstruct both trees node-by-node from peers. `TransactionStateSF` is the callback filter attached to the transaction tree during that process, while its structurally identical sibling `AccountStateSF` covers the account-state tree. + +The in-file comment — *"this class is only needed on add functions"* — captures the design intent precisely: the filter is not consulted during read-only traversal of a fully-present map. It only activates when nodes are being inserted into a SHAMap that is being rebuilt, making it a pure synchronization concern. + +## Inheritance and Interface + +The class inherits from `SHAMapSyncFilter`, a non-copyable abstract callback defined in `include/xrpl/shamap/SHAMapSyncFilter.h`. That interface declares exactly two pure virtual methods: + +- `gotNode()` — called by the SHAMap engine after a node has been successfully received and decoded, giving the filter a chance to persist it. +- `getNode()` — called when the SHAMap engine needs a node that it does not have in memory; the filter may return it from a local cache. + +The non-copyable constraint on `SHAMapSyncFilter` (deleted copy constructor and assignment) is inherited by `TransactionStateSF`, which is appropriate because both held members are references — copying would silently alias the same resources without updating the reference targets. + +## Constructor and Dependencies + +The constructor takes two references: + +```cpp +TransactionStateSF(NodeStore::Database& db, AbstractFetchPackContainer& fp) +``` + +`NodeStore::Database` is the persistent key-value store where all ledger objects (ledger headers, account nodes, transaction nodes) ultimately live. `AbstractFetchPackContainer` is a thin interface that decouples the filter from `LedgerMaster` and `Application`; it exposes a single `getFetchPack(uint256)` method that probes a short-lived, peer-sourced cache of raw blob data called a *fetch pack*. This abstraction exists specifically to avoid pulling the heavyweight `Application` object into a context where only fetch-pack access is needed. + +In practice, as seen in `InboundLedger.cpp`, callers pass `mLedger->txMap().family().db()` as the database and `app_.getLedgerMaster()` (which implements `AbstractFetchPackContainer`) as the fetch pack container. + +## `gotNode()` — Persisting Received Nodes + +```cpp +void gotNode(bool, SHAMapHash const& nodeHash, std::uint32_t ledgerSeq, + Blob&& nodeData, SHAMapNodeType type) const override; +``` + +The implementation stores the arriving node into the database with the `hotTRANSACTION_NODE` type tag. The `bool fromFilter` parameter is ignored here — it signals whether the node originated from the filter's own `getNode()` call or arrived directly from a peer, but the persistence action is identical either way. + +A notable defensive detail is the `XRPL_ASSERT` that rejects `SHAMapNodeType::tnTRANSACTION_NM` (transaction without metadata). After a ledger closes, its transaction tree holds `tnTRANSACTION_MD` entries (transactions with attached metadata); bare non-metadata transaction nodes should not appear in this context. The assertion catches any mismatch at development time without incurring a runtime check in production. + +The `nodeData` parameter is taken by rvalue reference and moved directly into `db_.store(...)`, avoiding any unnecessary copy of what may be a large blob. + +## `getNode()` — Serving Nodes from the Fetch-Pack Cache + +```cpp +std::optional getNode(SHAMapHash const& nodeHash) const override; +``` + +The implementation delegates entirely to `fp_.getFetchPack(nodeHash.as_uint256())`. Fetch packs are peer-provided bundles of ledger object data, distributed by nodes that have the full ledger to nodes that are catching up. If the hash is present in the cache, the blob is returned; otherwise `std::nullopt` signals to the SHAMap engine that it must request the node from a peer directly. + +## Relationship to `AccountStateSF` + +`AccountStateSF` is structurally identical — same constructor signature, same two-reference layout, same delegation pattern — but its `gotNode()` stores nodes as `hotACCOUNT_NODE` rather than `hotTRANSACTION_NODE`. The two classes exist as separate types so that the SHAMap engine can receive a single typed filter pointer and the correct storage tag is applied automatically, without any runtime branching. The duplication is intentional: each filter is permanently bound to one of the two tree roles a ledger has. + +## Usage in `InboundLedger` + +`InboundLedger.cpp` creates `TransactionStateSF` instances on the stack in four locations: when fetching the transaction tree root, when finding missing nodes, when adding a received root node, and when enumerating still-needed hashes. Each instance is short-lived and stack-allocated, reflecting that the filter holds no ownership — it is a view over the database and the fetch-pack container that exist elsewhere for the ledger's lifetime. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp.ai.json b/src/xrpld/app/ledger/detail/BuildLedger.cpp.ai.json new file mode 100644 index 0000000000..3399dbf98a --- /dev/null +++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp.ai.json @@ -0,0 +1,414 @@ +{ + "args": [ + { + "lineno": 17, + "name": "parent" + }, + { + "lineno": 18, + "name": "closeTime" + }, + { + "lineno": 19, + "name": "closeTimeCorrect" + }, + { + "lineno": 20, + "name": "closeResolution" + }, + { + "lineno": 21, + "name": "app" + }, + { + "lineno": 22, + "name": "j" + }, + { + "lineno": 23, + "name": "applyTxs" + }, + { + "lineno": 62, + "name": "built" + }, + { + "lineno": 63, + "name": "txns" + }, + { + "lineno": 64, + "name": "failed" + }, + { + "lineno": 65, + "name": "view" + }, + { + "lineno": 127, + "name": "failedTxns" + }, + { + "lineno": 148, + "name": "replayData" + }, + { + "lineno": 149, + "name": "applyFlags" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "buildLedgerImpl", + "OpenView accum(&*built)", + "XRPL_ASSERT(!accum.open(), ...)", + "applyTxs(accum, built)", + "accum.apply(*built)", + "built->updateSkipList()", + "built->stateMap().flushDirty()", + "built->txMap().flushDirty()", + "built->unshare()", + "XRPL_ASSERT(built->header().seq < XRP_LEDGER_EARLIEST_FEES || built->read(keylet::fees()), ...)", + "built->setAccepted()" + ], + "entry_point": "buildLedgerImpl", + "purpose": "Builds a new ledger from a parent, applies transactions, validates ledger state and fees, and finalizes the ledger.", + "validation_points": [ + "XRPL_ASSERT(!accum.open(), ...)", + "XRPL_ASSERT(built->header().seq < XRP_LEDGER_EARLIEST_FEES || built->read(keylet::fees()), ...)" + ] + }, + { + "call_chain": [ + "applyTransactions", + "for each tx in txns", + "applyTransaction(app, view, *it->second, ...)", + "switch (ApplyTransactionResult)", + "txns.erase(it) or failed.insert(txid)" + ], + "entry_point": "applyTransactions", + "purpose": "Applies a set of transactions to a ledger view, handling success, failure, and retry cases.", + "validation_points": [ + "applyTransaction (not shown in this file, but likely contains transaction-level validation)" + ] + } + ], + "data_flows": [ + { + "field": "accum.open()", + "flow": [ + "OpenView accum(&*built)", + "XRPL_ASSERT(!accum.open(), ...)", + "applyTxs(accum, built)", + "accum.apply(*built)" + ], + "origin": "OpenView accum(&*built)", + "transformations": [ + "accum is constructed from built ledger", + "checked for open state (should be closed)", + "transactions applied to accum", + "accum state applied back to built ledger" + ], + "validated_at": "XRPL_ASSERT(!accum.open(), ...)" + }, + { + "field": "built->header().seq and built->read(keylet::fees())", + "flow": [ + "built->header().seq", + "built->read(keylet::fees())", + "XRPL_ASSERT(built->header().seq < XRP_LEDGER_EARLIEST_FEES || built->read(keylet::fees()), ...)", + "built->setAccepted()" + ], + "origin": "built ledger (std::make_shared(*parent, closeTime))", + "transformations": [ + "header().seq is checked for ledger sequence number", + "read(keylet::fees()) checks for presence of fees entry", + "assertion ensures fees are present for ledgers after a certain sequence" + ], + "validated_at": "XRPL_ASSERT(built->header().seq < XRP_LEDGER_EARLIEST_FEES || built->read(keylet::fees()), ...)" + }, + { + "field": "transactions (txns)", + "flow": [ + "applyTransactions receives txns", + "for each tx in txns", + "applyTransaction(app, view, *it->second, ...)", + "switch (ApplyTransactionResult)", + "txns.erase(it) or failed.insert(txid)" + ], + "origin": "CanonicalTXSet& txns (input to applyTransactions)", + "transformations": [ + "transactions are iterated and applied", + "on success/failure/retry, txns is mutated", + "failed transactions are collected" + ], + "validated_at": "applyTransaction (not shown here, but likely validates each transaction)" + } + ], + "description": "Implements functions to build and apply transactions to XRPL ledgers, supporting both consensus and replayed ledgers.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "OpenView state (open/closed)", + "validation", + "missing", + "check" + ], + "evidence": "Field OpenView state (open/closed) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for OpenView state (open/closed)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates OpenView state (open/closed) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Ledger fee structure presence (fees object existence for new ledgers)", + "validation", + "missing", + "check" + ], + "evidence": "Field Ledger fee structure presence (fees object existence for new ledgers) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for Ledger fee structure presence (fees object existence for new ledgers)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates Ledger fee structure presence (fees object existence for new ledgers) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "accum.open()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at buildLedgerImpl", + "issue_pattern": "Missing empty string validation for accum.open()", + "why_false_positive": "XRPL_ASSERT validates accum.open() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "built->header().seq < XRP_LEDGER_EARLIEST_FEES || built->read(keylet::fees())", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at buildLedgerImpl", + "issue_pattern": "Missing empty string validation for built->header().seq < XRP_LEDGER_EARLIEST_FEES || built->read(keylet::fees())", + "why_false_positive": "XRPL_ASSERT validates built->header().seq < XRP_LEDGER_EARLIEST_FEES || built->read(keylet::fees()) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/BuildLedger.cpp", + "functions": [ + { + "args": [ + "parent", + "closeTime", + "closeTimeCorrect", + "closeResolution", + "app", + "j", + "applyTxs" + ], + "lineno": 16, + "name": "buildLedgerImpl" + }, + { + "args": [ + "app", + "built", + "txns", + "failed", + "view", + "j" + ], + "lineno": 61, + "name": "applyTransactions" + }, + { + "args": [ + "parent", + "closeTime", + "closeTimeCorrect", + "closeResolution", + "app", + "txns", + "failedTxns", + "j" + ], + "lineno": 120, + "name": "buildLedger" + }, + { + "args": [ + "replayData", + "applyFlags", + "app", + "j" + ], + "lineno": 147, + "name": "buildLedger" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The core validation logic (XRPL_ASSERTs) is in buildLedgerImpl. These are likely tested indirectly via higher-level ledger-building and consensus tests, such as those in test/ledger/BuildLedger_test.cpp, test/app/ledger/Ledger_test.cpp, or integration tests that exercise ledger creation and transaction application. Direct unit tests for assertion failures or edge cases (e.g., missing fees entry, accum.open() true) may be limited or absent. Transaction-level validation is likely covered in transaction application tests, but the specific assertion paths here may not be directly tested for negative cases.", + "validation_architecture": { + "auto_validated_fields": [ + "OpenView state (open/closed)", + "Ledger fee structure presence (fees object existence for new ledgers)" + ], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "accum.open()", + "location": "buildLedgerImpl", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures the OpenView 'accum' is not open before applying transactions", + "Checks ledger state is valid for building" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "built->header().seq < XRP_LEDGER_EARLIEST_FEES || built->read(keylet::fees())", + "location": "buildLedgerImpl", + "validated_by": "XRPL_ASSERT", + "validates": [ + "If ledger sequence is after XRP_LEDGER_EARLIEST_FEES, ensures the fees object exists in the ledger", + "Prevents accepting a ledger without required fee structure" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp.ai.md b/src/xrpld/app/ledger/detail/BuildLedger.cpp.ai.md new file mode 100644 index 0000000000..cea169f426 --- /dev/null +++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp.ai.md @@ -0,0 +1,45 @@ +# `BuildLedger.cpp` — Ledger Construction from Consensus and Replay + +This file implements the mechanics of constructing a new closed ledger on the XRP Ledger. It provides the two public entry points declared in `BuildLedger.h` — one for the consensus path (building a ledger from a fresh set of agreed-upon transactions) and one for replay (deterministically re-deriving a previously-validated ledger). Both paths share a single private implementation template, `buildLedgerImpl`, which handles the invariant bookkeeping surrounding transaction application. + +## Role in the Ledger Pipeline + +After the consensus protocol agrees on a transaction set, the node must turn that agreement into a concrete `Ledger` object whose state tree hash can be compared with peers. That is the job of the consensus `buildLedger` overload. The replay overload serves ledger acquisition: when a node downloads a historical ledger to fill a gap, it must re-execute its transactions against the known parent to verify the resulting hash rather than trust the network blindly. The two paths demand different application strategies, but the surrounding ceremony — creating the child ledger, staging changes in a view, flushing the SHAMap to storage, and finalizing — is identical, which is why `buildLedgerImpl` exists as a template. + +## `buildLedgerImpl` — The Shared Scaffolding + +The template accepts an `ApplyTxs` callable with the signature `void(OpenView&, std::shared_ptr const&)`. This design separates the "what transactions to apply and how" from the "how to set up and finalize the ledger," letting the two callers supply only the logic that differs between them. + +The sequence inside `buildLedgerImpl` is carefully ordered: + +1. **Child ledger creation**: `std::make_shared(*parent, closeTime)` copies the parent's state tree header but does not duplicate the underlying SHAMap nodes — they are shared copy-on-write. + +2. **Flag ledger handling**: If the new ledger falls on a flag ledger boundary (sequence divisible by 256), `updateNegativeUNL()` is called. This is the mechanism for updating the set of validators that are currently offline and should be excluded from quorum calculations. + +3. **Accumulator pattern**: An `OpenView accum(&*built)` is constructed. `OpenView` is an in-memory staging layer that buffers state changes without writing them to the underlying ledger. The `XRPL_ASSERT(!accum.open(), ...)` immediately after construction confirms the view represents a *closing* (not open-for-new-transactions) ledger — a subtle but important invariant distinguishing in-progress consensus rounds from ledger-building. The `applyTxs` callable receives this accumulator; only after all transactions are processed does `accum.apply(*built)` commit the changes atomically to the ledger. + +4. **SHAMap persistence**: `built->stateMap().flushDirty(hotACCOUNT_NODE)` and `built->txMap().flushDirty(hotTRANSACTION_NODE)` write all modified SHAMap nodes to the node store. The heat hints (`hotACCOUNT_NODE`, `hotTRANSACTION_NODE`) influence caching priority in the node store. This step must follow the apply so that only the final, committed state is persisted. + +5. **Finalization**: `built->unshare()` breaks sharing with the parent's SHAMap to ensure the new ledger owns its own copy. The fee structure assertion — `built->header().seq < XRP_LEDGER_EARLIEST_FEES || built->read(keylet::fees())` — then guards that every ledger after the early genesis period carries a `FeeSettings` object. Finally, `setAccepted()` stamps the ledger with its close time and resolution, transitioning it to the accepted state. + +## `applyTransactions` — The Consensus Retry Loop + +The consensus path requires more sophistication than simple ordered application because transactions in a `CanonicalTXSet` can have inter-dependencies. A payment that fails because the sender's sequence number hasn't been advanced yet by a prior transaction should be retried, not discarded. + +`CanonicalTXSet` sorts transactions by `(salted_account_key, seqProxy, txid)`, keeping each account's transactions in sequence order while randomizing the relative ordering between accounts (using the parent ledger hash as a salt, preventing adversarial transaction ordering attacks). The retry loop runs at most `LEDGER_TOTAL_PASSES` (3) times, with `LEDGER_RETRY_PASSES` (1) "certain retry" passes and then non-retry "final" passes. These constants are defined as macros in `OpenLedger.h` and mirror the identical logic in `OpenLedger`'s `apply()` function for the open-ledger path. + +On each pass, `applyTransaction` is called for each remaining transaction. The result drives three outcomes: `Success` removes the transaction from the set and increments the change counter; `Fail` moves it to the `failed` set and removes it from the working set; `Retry` leaves it in place for the next pass. Once a pass completes with zero changes, the loop either exits (if already in non-retry mode) or switches off `certainRetry`. The final `XRPL_ASSERT` enforces that if any transactions remain after the loop, at least one non-retry pass has occurred — a guarantee that no transaction was simply skipped due to premature loop exit. + +Pass 0 has a special short-circuit: if a transaction already exists in the parent ledger (detectable via `built->txExists(txid)`), it is silently dropped. This prevents applying the same transaction twice when building from an ancestor that already captured it. + +Exception handling wraps each `applyTransaction` call. If a transaction throws a `std::exception`, it is logged, added to `failed`, and the loop continues. This ensures one malformed transaction cannot abort the construction of an entire ledger. + +## Replay Path — Deterministic Re-execution + +The replay `buildLedger` overload passes a simpler lambda to `buildLedgerImpl`. It iterates `replayData.orderedTxns()` — a `std::map>` keyed by the transaction's original position in the ledger — and calls `applyTransaction` with `certainRetry = false`. There is no retry loop, no failure set, and no deduplication check. A previously-validated ledger's transaction set is already known to be applicable in order; replaying it is a mechanical repetition, not a consensus negotiation. + +The close time correctness flag is derived from the original ledger's close flags: if `sLCF_NoConsensusTime` is *not* set, consensus agreed on the close time and `closeTimeCorrect` is `true`. This bit-level reading of the stored close flags preserves the original ledger's semantics during replay. + +## Relationship to Sibling Files + +`OpenLedger.h` defines the `LEDGER_TOTAL_PASSES` and `LEDGER_RETRY_PASSES` constants and contains a near-identical retry loop used for the open ledger (transactions arriving during consensus). The two loops are deliberate parallels: the open-ledger path retries newly submitted transactions against the current open view, while `applyTransactions` here retries them against the view being built for the next closed ledger. `LedgerReplay` is a simple value type holding `parent_`, `replay_`, and `orderedTxns_` — its only role is carrying the data the replay lambda needs. `CanonicalTXSet` provides the sorted-by-account transaction container that makes the multi-pass retry strategy effective for the consensus path. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp.ai.json b/src/xrpld/app/ledger/detail/InboundLedger.cpp.ai.json new file mode 100644 index 0000000000..dc33410ffc --- /dev/null +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp.ai.json @@ -0,0 +1,648 @@ +{ + "args": [ + { + "lineno": 44, + "name": "app" + }, + { + "lineno": 45, + "name": "hash" + }, + { + "lineno": 46, + "name": "seq" + }, + { + "lineno": 47, + "name": "reason" + }, + { + "lineno": 48, + "name": "clock" + }, + { + "lineno": 49, + "name": "peerSet" + } + ], + "classes": [ + { + "args": [ + "app", + "hash", + "seq", + "reason", + "clock", + "peerSet" + ], + "lineno": 44, + "name": "InboundLedger" + }, + { + "args": [], + "lineno": 771, + "name": "PeerDataCounts" + } + ], + "code_paths": [ + { + "call_chain": [ + "InboundLedger::init", + "tryDB", + "addPeers / queueJob / done", + "XRPL_ASSERT (validation)" + ], + "entry_point": "InboundLedger::init", + "purpose": "Initializes the InboundLedger, attempts to find the ledger in the local DB, and if found, validates its header and fees.", + "validation_points": [ + "XRPL_ASSERT in InboundLedger::init (validates mLedger->header().seq and mLedger->read(keylet::fees()))" + ] + }, + { + "call_chain": [ + "InboundLedger::checkLocal", + "tryDB", + "done" + ], + "entry_point": "InboundLedger::checkLocal", + "purpose": "Checks if the ledger is already present locally and marks as done if found.", + "validation_points": [ + "Indirect: If ledger is found, InboundLedger::init's validation applies" + ] + }, + { + "call_chain": [ + "InboundLedger::InboundLedger", + "touch" + ], + "entry_point": "InboundLedger (constructor)", + "purpose": "Constructs the InboundLedger object and sets up initial state.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "mLedger", + "flow": [ + "tryDB (sets mLedger if found)", + "InboundLedger::init (uses mLedger)", + "XRPL_ASSERT (validates mLedger->header().seq and mLedger->read(keylet::fees()))", + "mLedger->setImmutable()", + "app_.getLedgerMaster().storeLedger(mLedger)" + ], + "origin": "Set in tryDB if ledger is found in DB, or later via network fetch", + "transformations": [ + "Loaded from DB or network", + "Validated for header.seq and fees", + "Set to immutable", + "Stored in LedgerMaster" + ], + "validated_at": "InboundLedger::init (XRPL_ASSERT)" + }, + { + "field": "mSeq", + "flow": [ + "InboundLedger::InboundLedger (sets mSeq)", + "InboundLedger::update (may update mSeq if previously 0)", + "Used in validation: mLedger->header().seq" + ], + "origin": "Constructor argument or updated in update()", + "transformations": [ + "Set from constructor or update()", + "Compared in XRPL_ASSERT" + ], + "validated_at": "InboundLedger::init (XRPL_ASSERT)" + }, + { + "field": "fees (from mLedger->read(keylet::fees()))", + "flow": [ + "tryDB (loads ledger)", + "InboundLedger::init (reads fees from mLedger)", + "XRPL_ASSERT (validates fees presence)" + ], + "origin": "Ledger DB or network fetch", + "transformations": [ + "Read from ledger state", + "Checked for existence" + ], + "validated_at": "InboundLedger::init (XRPL_ASSERT)" + } + ], + "description": "Implements the InboundLedger class, which manages the acquisition and synchronization of ledgers from peers in the XRPL network, handling requests, responses, timeouts, and local storage checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "mLedger (type safety via smart pointers)", + "validation", + "missing", + "check" + ], + "evidence": "Field mLedger (type safety via smart pointers) validated by XRPL_ASSERT macro, C++ type system, possible jss:: for JSON validation (not shown in this snippet)", + "issue_pattern": "Missing validation for mLedger (type safety via smart pointers)", + "why_false_positive": "XRPL_ASSERT macro, C++ type system, possible jss:: for JSON validation (not shown in this snippet) validates mLedger (type safety via smart pointers) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "hash_ (uint256 type)", + "validation", + "missing", + "check" + ], + "evidence": "Field hash_ (uint256 type) validated by XRPL_ASSERT macro, C++ type system, possible jss:: for JSON validation (not shown in this snippet)", + "issue_pattern": "Missing validation for hash_ (uint256 type)", + "why_false_positive": "XRPL_ASSERT macro, C++ type system, possible jss:: for JSON validation (not shown in this snippet) validates hash_ (uint256 type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "mSeq (std::uint32_t type)", + "validation", + "missing", + "check" + ], + "evidence": "Field mSeq (std::uint32_t type) validated by XRPL_ASSERT macro, C++ type system, possible jss:: for JSON validation (not shown in this snippet)", + "issue_pattern": "Missing validation for mSeq (std::uint32_t type)", + "why_false_positive": "XRPL_ASSERT macro, C++ type system, possible jss:: for JSON validation (not shown in this snippet) validates mSeq (std::uint32_t type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "mReason (enum Reason type)", + "validation", + "missing", + "check" + ], + "evidence": "Field mReason (enum Reason type) validated by XRPL_ASSERT macro, C++ type system, possible jss:: for JSON validation (not shown in this snippet)", + "issue_pattern": "Missing validation for mReason (enum Reason type)", + "why_false_positive": "XRPL_ASSERT macro, C++ type system, possible jss:: for JSON validation (not shown in this snippet) validates mReason (enum Reason type) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mLedger->header().seq and mLedger->read(keylet::fees())", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at InboundLedger::init", + "issue_pattern": "Missing empty string validation for mLedger->header().seq and mLedger->read(keylet::fees())", + "why_false_positive": "XRPL_ASSERT macro validates mLedger->header().seq and mLedger->read(keylet::fees()) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/InboundLedger.cpp", + "functions": [ + { + "args": [ + "root", + "map", + "max", + "filter" + ], + "lineno": 97, + "name": "neededHashes" + }, + { + "args": [ + "max", + "filter" + ], + "lineno": 116, + "name": "InboundLedger::neededTxHashes" + }, + { + "args": [ + "max", + "filter" + ], + "lineno": 121, + "name": "InboundLedger::neededStateHashes" + }, + { + "args": [ + "srcDB" + ], + "lineno": 128, + "name": "InboundLedger::tryDB" + }, + { + "args": [ + "wasProgress", + "collectionLock" + ], + "lineno": 217, + "name": "InboundLedger::onTimer" + }, + { + "args": [], + "lineno": 259, + "name": "InboundLedger::addPeers" + }, + { + "args": [], + "lineno": 267, + "name": "InboundLedger::pmDowncast" + }, + { + "args": [], + "lineno": 271, + "name": "InboundLedger::done" + }, + { + "args": [ + "peer", + "reason" + ], + "lineno": 304, + "name": "InboundLedger::trigger" + }, + { + "args": [ + "nodes", + "reason" + ], + "lineno": 464, + "name": "InboundLedger::filterNodes" + }, + { + "args": [ + "data" + ], + "lineno": 500, + "name": "InboundLedger::takeHeader" + }, + { + "args": [ + "packet", + "san" + ], + "lineno": 527, + "name": "InboundLedger::receiveNode" + }, + { + "args": [ + "data", + "san" + ], + "lineno": 589, + "name": "InboundLedger::takeAsRootNode" + }, + { + "args": [ + "data", + "san" + ], + "lineno": 610, + "name": "InboundLedger::takeTxRootNode" + }, + { + "args": [], + "lineno": 631, + "name": "InboundLedger::getNeededHashes" + }, + { + "args": [ + "peer", + "data" + ], + "lineno": 655, + "name": "InboundLedger::gotData" + }, + { + "args": [ + "peer", + "packet" + ], + "lineno": 678, + "name": "InboundLedger::processData" + }, + { + "args": [], + "lineno": 803, + "name": "InboundLedger::runData" + }, + { + "args": [ + "int" + ], + "lineno": 846, + "name": "InboundLedger::getJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + }, + { + "lineno": 769, + "name": "detail" + } + ], + "test_coverage_notes": "Testing for InboundLedger is likely found in unit/integration tests for ledger acquisition and validation, such as 'test_inbound_ledger.cpp', 'test_ledger_acquisition.cpp', or similar files in the test/ or src/test/ledger/ directories. The XRPL_ASSERT macro is typically tested via scenarios where ledgers are missing fees or have invalid headers. However, edge cases such as malformed ledgers, missing fees, or sequence mismatches may not be fully covered unless explicitly tested. There may be gaps in testing error handling paths (e.g., exceptions in tryDB, incomplete ledgers, or aggressive timeouts).", + "validation_architecture": { + "auto_validated_fields": [ + "mLedger (type safety via smart pointers)", + "hash_ (uint256 type)", + "mSeq (std::uint32_t type)", + "mReason (enum Reason type)" + ], + "framework": "XRPL_ASSERT macro, C++ type system, possible jss:: for JSON validation (not shown in this snippet)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely std::logic_error or abort)", + "field": "mLedger->header().seq and mLedger->read(keylet::fees())", + "location": "InboundLedger::init", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "If ledger sequence is before XRP_LEDGER_EARLIEST_FEES, skip fee check", + "If ledger sequence is after, ensure fees entry exists in ledger" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/InboundLedger.cpp.ai.md b/src/xrpld/app/ledger/detail/InboundLedger.cpp.ai.md new file mode 100644 index 0000000000..d9b7c322fc --- /dev/null +++ b/src/xrpld/app/ledger/detail/InboundLedger.cpp.ai.md @@ -0,0 +1,56 @@ +# `InboundLedger.cpp` — Peer-Sourced Ledger Acquisition Engine + +`InboundLedger` is the core state machine responsible for fetching a complete ledger from the peer network. A single validated XRPL ledger consists of three independently verifiable pieces: the ledger header (which commits to the roots of both tries), the transaction SHAMap, and the account-state SHAMap. `InboundLedger` tracks which pieces have arrived, orchestrates requests to peers, and drives retry logic until all three parts are assembled or the attempt is abandoned. + +## Inheritance and Lifecycle + +`InboundLedger` inherits from `TimeoutCounter`, which provides the asynchronous heartbeat loop: a recurring 3-second timer (`ledgerAcquireTimeout`) fires `onTimer` via the job queue. The subclass uses this to escalate its request strategy. The class also inherits `enable_shared_from_this`, which is essential — it shares itself into closures dispatched to the job queue, preventing premature destruction while work is in flight. + +Construction is deliberately cheap: the object is initialized, a `touch()` sets the last-activity timestamp, and nothing else happens synchronously. `init()` — called separately under the `InboundLedgers` collection lock — does the real setup work. The caller holds the collection lock on entry; `init()` immediately acquires the internal `mtx_` and then releases the collection lock, establishing a clear ordering and minimizing the time the collection is locked. + +## Local Store First: `tryDB` + +Before sending any network request, `tryDB` attempts to satisfy the acquisition entirely from local data. It checks both the `NodeStore` database and the `LedgerMaster`'s fetch pack cache (compressed batches of node data propagated during consensus). The rationale for checking both is that a ledger's nodes may have arrived by different paths: the header might be in the main node store while its SHAMap nodes arrived in a fetch pack. + +A subtle cross-database case is handled: if the ledger header is found in a source DB that differs from the ledger's own family DB (which happens when shard databases are in play), the header blob is copied to the correct destination before proceeding. This avoids later cache misses during state map traversal. + +`tryDB` sets `mHaveHeader`, `mHaveTransactions`, and `mHaveState` individually. For the SHAMaps, it first fetches the root node via `fetchRoot`, then probes with `neededTxHashes`/`neededStateHashes` asking for a single missing hash — if the answer is empty, the entire map is locally available without fetching every node one by one. A ledger with an empty transaction hash (`txHash.isZero()`) correctly short-circuits to `mHaveTransactions = true`. An empty account hash, however, is treated as a fatal error and sets `failed_`, since every valid ledger must have a non-empty account state. + +## Peer Selection and Request Triggering + +`addPeers` delegates to `mPeerSet`, passing two lambdas: a predicate that filters peers to those claiming to have the target ledger (`peer->hasLedger(hash_, mSeq)`), and a callback that triggers requests immediately — except for `Reason::HISTORY` acquisitions where a fetch pack is likely incoming and premature requests waste bandwidth. + +`trigger` is the central request dispatcher. It chooses what to request based on which pieces are still missing, in priority order: header first (nothing else can proceed without the root hashes), then account state (the largest map, prioritized because it's the most valuable if the acquisition is abandoned partway), then transaction data. Query depth is adapted to the trigger reason: `TriggerReason::reply` uses depth 1 (or depth 2 for high-latency peers, since the round trip is already being paid), while `added` and `timeout` use depth 0 — querying blindly into an unknown tree is wasteful. + +## Escalation: Aggressive Mode and `filterNodes` + +Two escalation mechanisms kick in as timeouts accumulate. First, `mRecentNodes` tracks SHAMap node hashes that were recently requested. `filterNodes` uses `std::stable_partition` to move duplicate nodes to the back, then erases them — preventing the same node from flooding the same peer on successive calls. On a `TriggerReason::timeout` trigger, even duplicate nodes are sent, ensuring stalled acquisitions retry everything. `mRecentNodes` is cleared at the start of each timer tick. + +Second, after `ledgerBecomeAggressiveThreshold = 4` timeouts without progress, the code switches protocols entirely: instead of using the SHAMap tree-walk protocol (`TMGetLedger`), it gathers the specific hashes of all missing nodes via `getNeededHashes` and sends a `TMGetObjectByHash` request directly asking for those content-addressed objects. This bypasses the tree traversal when normal traversal is stalling, effectively asking the network "give me these exact bytes" rather than "walk this tree with me." + +The `timeouts_` counter after `ledgerTimeoutRetriesMax = 6` terminates the acquisition unconditionally by setting `failed_` and calling `done()`. + +## Two-Lock Receive Pipeline + +Incoming data takes a path specifically designed to avoid blocking the overlay receive thread. `gotData` is called from the networking layer under a lightweight `mReceivedDataLock` (a plain non-recursive `std::mutex`); it appends to `mReceivedData` and returns `true` exactly once via the `mReceiveDispatched` flag. That single `true` return value signals that the caller needs to dispatch `runData` to the job queue. Subsequent packets accumulate in the queue without dispatching a new job. + +`runData` drains the entire pending queue in a loop, processing each queued `TMLedgerData` via `processData`. It uses the `detail::PeerDataCounts` helper — defined in a local `detail` namespace within this file — to track how many useful SHAMap nodes each peer contributed. After processing, `PeerDataCounts::prune()` eliminates peers that returned less than half the maximum useful-node count. `sampleN` then randomly selects at most 6 survivors to receive followup `trigger` calls. This approach rewards productive peers without creating a deterministic ordering that could be gamed, and avoids hammering every peer in the set after every response. + +`processData` validates each incoming packet — checking for non-empty node lists, verifying that each node has both an ID and data payload, and charging `Resource::feeMalformedRequest` or `Resource::feeInvalidData` against misbehaving peers — before delegating to `receiveNode`, `takeHeader`, `takeAsRootNode`, or `takeTxRootNode`. + +## Completion and `done()` + +`done()` uses `mSignaled` to ensure exactly-once semantics — it can be called from multiple code paths (the timer, `trigger`, `receiveNode`). A completed, non-failed ledger is made immutable before being routed by `mReason`: + +- `Reason::HISTORY`: notifies `InboundLedgers::onLedgerFetched()` to update the historical fetch rate; does not store via `LedgerMaster` since history backfill has its own pipeline. +- All other reasons: calls `LedgerMaster::storeLedger`. + +Crucially, `done()` dispatches an `AcqDone` job rather than calling `checkAccept` and `tryAdvance` inline. This is because `done()` may be called while holding the internal `mtx_` lock, and those `LedgerMaster` operations could attempt to re-enter structures that would deadlock or simply be too expensive for a hot path. + +## Fee Invariant + +An `XRPL_ASSERT` appears at every completion boundary — in `tryDB`, `init`, and `done` — verifying that any ledger at or after `XRP_LEDGER_EARLIEST_FEES` has a fee settings entry in its state map. This invariant reflects a protocol-level guarantee: after a certain ledger sequence, the fee object must exist. Asserting it at acquisition time catches corruption or bugs in the SHAMap assembly before the ledger propagates into consensus state. + +## Destructor and Stale Data Recycling + +The destructor forwards any account-state (`liAS_NODE`) packets that were received but not yet processed to `InboundLedgers::gotStaleData`. Account-state nodes are ledger-agnostic in the sense that many of them appear across consecutive ledgers; discarding them on cancellation would waste bandwidth. `gotStaleData` allows the subsystem to apply them to other in-progress acquisitions. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp.ai.json b/src/xrpld/app/ledger/detail/InboundLedgers.cpp.ai.json new file mode 100644 index 0000000000..4032927526 --- /dev/null +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp.ai.json @@ -0,0 +1,748 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "app", + "clock", + "collector", + "peerSetBuilder" + ], + "lineno": 10, + "name": "InboundLedgersImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "acquire", + "doAcquire (lambda inside acquire)", + "XRPL_ASSERT (hash validation)", + "if (reason validation)", + "ScopedLockType sl(mLock)", + "if (stopping_ validation)", + "mLedgers.find(hash)", + "if (inbound->isFailed() validation)", + "if (!inbound->isComplete() validation)", + "inbound->getLedger()" + ], + "entry_point": "acquire", + "purpose": "Attempts to acquire a ledger by hash and sequence, validating input and internal state before returning the ledger or null.", + "validation_points": [ + "XRPL_ASSERT (hash)", + "if (reason)", + "if (stopping_)", + "if (inbound->isFailed())", + "if (!inbound->isComplete())" + ] + }, + { + "call_chain": [ + "acquireAsync", + "pendingAcquires_.contains(hash)", + "pendingAcquires_.insert(hash)", + "acquire (calls above chain)", + "pendingAcquires_.erase(hash)" + ], + "entry_point": "acquireAsync", + "purpose": "Asynchronously attempts to acquire a ledger, ensuring only one concurrent acquire per hash, and handles exceptions.", + "validation_points": [ + "acquire (see above for validations)" + ] + } + ], + "data_flows": [ + { + "field": "hash", + "flow": [ + "acquire/acquireAsync parameter", + "doAcquire lambda", + "XRPL_ASSERT(hash.isNonZero())", + "mLedgers.find(hash)", + "inbound ledger construction or lookup", + "inbound->getLedger()" + ], + "origin": "Parameter to acquire/acquireAsync", + "transformations": [ + "Checked for nonzero", + "Used as key in mLedgers", + "Passed to InboundLedger constructor" + ], + "validated_at": "XRPL_ASSERT(hash.isNonZero())" + }, + { + "field": "reason", + "flow": [ + "acquire/acquireAsync parameter", + "doAcquire lambda", + "if (reason != GENERIC && reason != CONSENSUS)", + "potential early return" + ], + "origin": "Parameter to acquire/acquireAsync", + "transformations": [ + "Compared to enum values" + ], + "validated_at": "if (reason != GENERIC && reason != CONSENSUS)" + }, + { + "field": "stopping_", + "flow": [ + "doAcquire lambda", + "ScopedLockType sl(mLock)", + "if (stopping_)", + "early return if true" + ], + "origin": "Member variable of InboundLedgersImp", + "transformations": [ + "Checked for shutdown state" + ], + "validated_at": "if (stopping_)" + }, + { + "field": "inbound", + "flow": [ + "mLedgers.find(hash)", + "if not found, create new InboundLedger", + "inbound->isFailed()", + "inbound->update(seq) if not new", + "inbound->isComplete()", + "inbound->getLedger()" + ], + "origin": "Lookup or creation in mLedgers", + "transformations": [ + "May be updated with new seq", + "May be initialized if new" + ], + "validated_at": [ + "inbound->isFailed()", + "inbound->isComplete()" + ] + } + ], + "description": "Implements the InboundLedgers subsystem for managing the acquisition, caching, and failure tracking of ledgers received from the network in the XRPL node. Handles asynchronous acquisition, failure logging, fetch pack processing, and periodic sweeping of stale entries.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "hash (via XRPL_ASSERT)", + "validation", + "missing", + "check" + ], + "evidence": "Field hash (via XRPL_ASSERT) validated by XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet)", + "issue_pattern": "Missing validation for hash (via XRPL_ASSERT)", + "why_false_positive": "XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet) validates hash (via XRPL_ASSERT) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "reason (via business logic)", + "validation", + "missing", + "check" + ], + "evidence": "Field reason (via business logic) validated by XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet)", + "issue_pattern": "Missing validation for reason (via business logic)", + "why_false_positive": "XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet) validates reason (via business logic) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "stopping_ (via business logic)", + "validation", + "missing", + "check" + ], + "evidence": "Field stopping_ (via business logic) validated by XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet)", + "issue_pattern": "Missing validation for stopping_ (via business logic)", + "why_false_positive": "XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet) validates stopping_ (via business logic) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "inbound->isFailed() (via business logic)", + "validation", + "missing", + "check" + ], + "evidence": "Field inbound->isFailed() (via business logic) validated by XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet)", + "issue_pattern": "Missing validation for inbound->isFailed() (via business logic)", + "why_false_positive": "XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet) validates inbound->isFailed() (via business logic) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "inbound->isComplete() (via business logic)", + "validation", + "missing", + "check" + ], + "evidence": "Field inbound->isComplete() (via business logic) validated by XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet)", + "issue_pattern": "Missing validation for inbound->isComplete() (via business logic)", + "why_false_positive": "XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet) validates inbound->isComplete() (via business logic) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hash", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at acquire (doAcquire lambda)", + "issue_pattern": "Missing empty string validation for hash", + "why_false_positive": "XRPL_ASSERT validates hash for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "reason", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-statement at acquire (doAcquire lambda)", + "issue_pattern": "Missing empty string validation for reason", + "why_false_positive": "explicit if-statement validates reason for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "stopping_", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-statement at acquire (doAcquire lambda, inside lock)", + "issue_pattern": "Missing empty string validation for stopping_", + "why_false_positive": "explicit if-statement validates stopping_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "inbound->isFailed()", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-statement at acquire (doAcquire lambda)", + "issue_pattern": "Missing empty string validation for inbound->isFailed()", + "why_false_positive": "explicit if-statement validates inbound->isFailed() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "inbound->isComplete()", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-statement at acquire (doAcquire lambda)", + "issue_pattern": "Missing empty string validation for inbound->isComplete()", + "why_false_positive": "explicit if-statement validates inbound->isComplete() for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/InboundLedgers.cpp", + "functions": [ + { + "args": [ + "hash", + "seq", + "reason" + ], + "lineno": 38, + "name": "acquire" + }, + { + "args": [ + "hash", + "seq", + "reason" + ], + "lineno": 81, + "name": "acquireAsync" + }, + { + "args": [ + "hash" + ], + "lineno": 104, + "name": "find" + }, + { + "args": [ + "hash", + "peer", + "packet" + ], + "lineno": 126, + "name": "gotLedgerData" + }, + { + "args": [ + "h", + "seq" + ], + "lineno": 157, + "name": "logFailure" + }, + { + "args": [ + "h" + ], + "lineno": 163, + "name": "isFailure" + }, + { + "args": [ + "packet_ptr" + ], + "lineno": 176, + "name": "gotStaleData" + }, + { + "args": [], + "lineno": 200, + "name": "clearFailures" + }, + { + "args": [], + "lineno": 208, + "name": "fetchRate" + }, + { + "args": [], + "lineno": 215, + "name": "onLedgerFetched" + }, + { + "args": [], + "lineno": 221, + "name": "getInfo" + }, + { + "args": [], + "lineno": 255, + "name": "gotFetchPack" + }, + { + "args": [], + "lineno": 272, + "name": "sweep" + }, + { + "args": [], + "lineno": 312, + "name": "stop" + }, + { + "args": [], + "lineno": 320, + "name": "cacheSize" + }, + { + "args": [ + "app", + "clock", + "collector" + ], + "lineno": 332, + "name": "make_InboundLedgers" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested by integration and unit tests for ledger acquisition and network operations. Typical test files would be in the test/ or unit_test/ directories, such as InboundLedgers_test.cpp or LedgerMaster_test.cpp. However, the specific validation branches (e.g., hash zero, reason mismatch, stopping_ true, inbound->isFailed(), inbound->isComplete() false) may not all be directly tested unless there are explicit negative/edge case tests. Exception handling in acquireAsync may also lack direct test coverage. Template-based validation and XRPL_ASSERT may be tested indirectly via assertion failures in test runs.", + "validation_architecture": { + "auto_validated_fields": [ + "hash (via XRPL_ASSERT)", + "reason (via business logic)", + "stopping_ (via business logic)", + "inbound->isFailed() (via business logic)", + "inbound->isComplete() (via business logic)" + ], + "framework": "XRPL_ASSERT, explicit C++ logic, some jss:: for JSON elsewhere (not in this snippet)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely std::logic_error or abort)", + "field": "hash", + "location": "acquire (doAcquire lambda)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "hash must be nonzero", + "calls hash.isNonZero()" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns empty shared_ptr (no exception)", + "field": "reason", + "location": "acquire (doAcquire lambda)", + "validated_by": "explicit if-statement", + "validates": [ + "if app_.getOPs().isNeedNetworkLedger() is true, reason must be GENERIC or CONSENSUS", + "otherwise, returns {} (empty)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "returns empty shared_ptr (no exception)", + "field": "stopping_", + "location": "acquire (doAcquire lambda, inside lock)", + "validated_by": "explicit if-statement", + "validates": [ + "if stopping_ is true, aborts acquisition" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "returns empty shared_ptr (no exception)", + "field": "inbound->isFailed()", + "location": "acquire (doAcquire lambda)", + "validated_by": "explicit if-statement", + "validates": [ + "if inbound ledger is marked failed, aborts acquisition" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "returns empty shared_ptr (no exception)", + "field": "inbound->isComplete()", + "location": "acquire (doAcquire lambda)", + "validated_by": "explicit if-statement", + "validates": [ + "if inbound ledger is not complete, aborts acquisition" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/InboundLedgers.cpp.ai.md b/src/xrpld/app/ledger/detail/InboundLedgers.cpp.ai.md new file mode 100644 index 0000000000..cc7afdb057 --- /dev/null +++ b/src/xrpld/app/ledger/detail/InboundLedgers.cpp.ai.md @@ -0,0 +1,41 @@ +# `InboundLedgers.cpp` — Ledger Acquisition Manager + +This file contains the sole concrete implementation of the `InboundLedgers` interface, hidden behind a factory function. Its job is to manage the *collection* of in-flight ledger acquisitions: creating them, deduplicating requests, routing incoming peer data to the right acquisition, and eventually evicting stale ones. Individual acquisition mechanics live in `InboundLedger`; this file is the registry and coordinator that sits above it. + +## Architecture: Factory-Hidden Implementation + +`InboundLedgersImp` is defined entirely within this `.cpp` file — it is never exposed in a header. Only `make_InboundLedgers()` is visible to the rest of the application, returning a `std::unique_ptr`. This is a deliberate encapsulation: callers depend only on the abstract interface, and the entire implementation can change without touching consumers. The same pattern appears throughout the XRPL codebase (e.g., `PeerSetBuilderImpl`). + +## Acquiring a Ledger: Synchronous vs. Async + +`acquire()` is the workhorse. It wraps its logic in a `doAcquire` lambda measured by `perf::measureDurationAndLog` — any call exceeding 500 ms is logged as a warning, since ledger acquisition is expected to be fast (it either finds an existing entry or schedules network work and returns immediately). The real work inside the lock is minimal: look up the hash in `mLedgers`, and if missing, construct a new `InboundLedger` and call `init()` on it while still holding the lock. The return value is non-null only if the ledger is already `isComplete()`, so in practice `acquire()` usually returns `{}` and the actual ledger arrives later through async callbacks. + +`acquireAsync()` adds an important layer: a `pendingAcquires_` set guarded by its own `acquiresMutex_`. When called from a job queue thread, this prevents two concurrent jobs from both entering `acquire()` for the same hash simultaneously. The pattern is careful: the outer lock is taken, the hash is inserted, then `scope_unlock` temporarily releases `acquiresMutex_` for the actual `acquire()` call, and finally the hash is erased unconditionally after — even if `acquire()` throws an exception. This prevents the `pendingAcquires_` set from leaking hash entries when exceptions propagate. + +The two locks — `acquiresMutex_` (non-recursive `std::mutex`) and `mLock` (recursive `std::recursive_mutex`) — guard orthogonal state and are never held simultaneously in a way that creates deadlock risk. `acquiresMutex_` exists solely to deduplicate async calls and is released before `mLock` is ever taken. + +## Reason Filtering at Bootstrap + +When `app_.getOPs().isNeedNetworkLedger()` is true, the node is still bootstrapping and only allows acquisitions with `Reason::GENERIC` or `Reason::CONSENSUS`. History backfill (`Reason::HISTORY`) is silently suppressed. This prevents a newly-joined node from flooding peers with history requests before it has established its current position in the network — a bandwidth protection heuristic noted in the source with the candid comment "probably not the right rule." + +## Routing Incoming Peer Data + +`gotLedgerData()` is called when a `TMLedgerData` protocol message arrives from a peer. It looks up the ledger hash in `mLedgers` via `find()`. If found, it hands the data to `InboundLedger::gotData()`, and if that call returns `true` (meaning processing hasn't already been dispatched), it enqueues a `jtLEDGER_DATA` job to call `InboundLedger::runData()`. This decoupling matters: peer message handlers run on network threads and must return quickly; the actual node processing happens on the job queue. + +If the ledger is no longer tracked (acquisition was completed or swept), and the incoming packet contains account-state nodes (`liAS_NODE`), the method still dispatches a job for `gotStaleData()`. This is a bandwidth-recovery optimization: the node already paid network cost to receive these nodes, so they are deserialized from wire format, re-serialized with the canonical prefix format, and stored in `LedgerMaster`'s fetch pack for future use. Transaction nodes are discarded because they are less reusable across ledger boundaries. + +## Failure Tracking and Reacquisition Throttling + +`mRecentFailures` is a `beast::aged_map` — a time-aware associative container. Failed acquisition hashes are recorded with `logFailure()` and expire after `kReacquireInterval` (5 minutes). Before any `isFailure()` check, `beast::expire()` sweeps stale entries, making the cache self-maintaining without a dedicated cleanup job. The same expiry call appears at the end of `sweep()` to keep failures pruned in sync with the ledger map. + +## Fetch Rate Measurement + +The `fetchRate_` member is a `DecayWindow<30, clock_type>` — a 30-second half-life exponential decay tracker. `onLedgerFetched()` increments it, and `fetchRate()` returns its current rate scaled to per-minute by multiplying by 60 (`60 * value / 30`). This rate is only meaningful for history ledger fetches; the comment in the header makes clear that callers should only invoke `onLedgerFetched()` for `Reason::HISTORY` acquisitions. The `fetchRateMutex_` is a separate non-recursive mutex dedicated solely to `fetchRate_`, avoiding contention with the main `mLock` during the rate query path. + +## Sweeping Stale Acquisitions + +`sweep()` runs periodically to remove acquisitions that have been idle for more than one minute. The design is deliberate about lock scope: it holds `mLock` only long enough to collect stale `InboundLedger` shared pointers into a local `stuffToSweep` vector, then drops the lock before the vector goes out of scope. This means `InboundLedger` destructors run without `mLock` held, avoiding potential deadlocks if those destructors attempt any re-entrant operations. The comment `// shouldn't cause the actual final delete` notes that the `stuffToSweep` vector maintains a reference, preventing premature destruction during the erase. + +## Shutdown Safety + +`stop()` acquires `mLock`, sets `stopping_ = true`, and clears both `mLedgers` and `mRecentFailures`. Any `acquire()` call racing with `stop()` will see `stopping_` inside the same lock scope and return `{}`. This provides a clean shutdown fence: once `stop()` returns, no new `InboundLedger` objects will be created through this manager. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/InboundTransactions.cpp.ai.json b/src/xrpld/app/ledger/detail/InboundTransactions.cpp.ai.json new file mode 100644 index 0000000000..f22e4aa5c2 --- /dev/null +++ b/src/xrpld/app/ledger/detail/InboundTransactions.cpp.ai.json @@ -0,0 +1,613 @@ +{ + "args": [ + { + "lineno": 19, + "name": "seq" + }, + { + "lineno": 19, + "name": "set" + } + ], + "classes": [ + { + "args": [ + "std::uint32_t seq", + "std::shared_ptr const& set" + ], + "lineno": 15, + "name": "InboundTransactionSet" + }, + { + "args": [ + "Application& app", + "beast::insight::Collector::ptr const& collector", + "std::function const&, bool)> gotSet", + "std::unique_ptr peerSetBuilder" + ], + "lineno": 33, + "name": "InboundTransactionsImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "getSet" + ], + "entry_point": "getSet", + "purpose": "Retrieves or acquires a transaction set (SHAMap) for a given hash. Optionally starts acquisition if not present.", + "validation_points": [ + "m_map.find(hash) - checks if hash exists in map", + "if (!acquire || stopping_) - validates acquire flag and stopping state" + ] + }, + { + "call_chain": [ + "gotData", + "getAcquire" + ], + "entry_point": "gotData", + "purpose": "Handles incoming ledger data from a peer, validates and processes it for transaction acquisition.", + "validation_points": [ + "getAcquire(hash) - checks if acquisition object exists for hash", + "if (ta == nullptr) - validates acquisition presence", + "if (!node.has_nodeid() || !node.has_nodedata()) - validates node fields in packet", + "if (!id) - validates deserialization of node id" + ] + }, + { + "call_chain": [ + "InboundTransactionsImp" + ], + "entry_point": "InboundTransactionsImp (constructor)", + "purpose": "Initializes the InboundTransactions implementation, sets up zero set and peer set builder.", + "validation_points": [ + "std::move(peerSetBuilder) - validates/moves builder", + "std::make_shared(...) - validates SHAMap creation" + ] + }, + { + "call_chain": [ + "getAcquire" + ], + "entry_point": "getAcquire", + "purpose": "Retrieves the TransactionAcquire pointer for a given hash if it exists.", + "validation_points": [ + "m_map.find(hash) - validates existence of hash" + ] + } + ], + "data_flows": [ + { + "field": "hash (uint256)", + "flow": [ + "Function argument", + "m_map.find(hash) - lookup", + "If not found and acquire==true, create TransactionAcquire and insert into m_map[hash]", + "Used to retrieve or create SHAMap/TransactionAcquire" + ], + "origin": "Function argument (getSet, getAcquire, gotData)", + "transformations": [ + "Used as key for map lookup", + "Triggers creation of TransactionAcquire if not present" + ], + "validated_at": "m_map.find(hash)" + }, + { + "field": "acquire (bool)", + "flow": [ + "Function argument", + "if (!acquire || stopping_) - early return if false", + "If true, may create TransactionAcquire" + ], + "origin": "Function argument (getSet)", + "transformations": [ + "Controls whether to start acquisition" + ], + "validated_at": "if (!acquire || stopping_)" + }, + { + "field": "peerSetBuilder", + "flow": [ + "Constructor argument", + "std::move(peerSetBuilder) - stored in m_peerSetBuilder", + "Used in TransactionAcquire creation" + ], + "origin": "Constructor argument (InboundTransactionsImp)", + "transformations": [ + "Moved into member variable" + ], + "validated_at": "std::move(peerSetBuilder)" + }, + { + "field": "set (std::shared_ptr)", + "flow": [ + "Created via std::make_shared", + "Assigned to m_zeroSet.mSet or obj.mSet", + "Returned by getSet" + ], + "origin": "Created in InboundTransactionsImp constructor and getSet", + "transformations": [ + "Constructed with SHAMapType and uint256", + "setUnbacked() called for zero set" + ], + "validated_at": "std::make_shared(...)" + }, + { + "field": "ta (TransactionAcquire::pointer)", + "flow": [ + "Created via std::make_shared", + "Assigned to obj.mAcquire in m_map[hash]", + "init(startPeers) called", + "Returned by getAcquire" + ], + "origin": "Created in getSet", + "transformations": [ + "Constructed with app_, hash, and peer set", + "init() called to start acquisition" + ], + "validated_at": "std::make_shared(...)" + }, + { + "field": "packet.nodes()", + "flow": [ + "Iterated in gotData", + "Each node validated for has_nodeid() and has_nodedata()", + "nodeid deserialized and validated" + ], + "origin": "gotData argument (protocol::TMLedgerData)", + "transformations": [ + "Deserialization of nodeid", + "Validation of presence" + ], + "validated_at": "if (!node.has_nodeid() || !node.has_nodedata()), if (!id)" + } + ], + "description": "Implements the InboundTransactions subsystem for acquiring, tracking, and managing transaction sets (SHAMaps) from peers in the XRPL network, including logic for acquiring transaction sets, handling incoming data, and managing transaction set lifetimes.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "unique_ptr/shared_ptr types (RAII)", + "validation", + "missing", + "check" + ], + "evidence": "Field unique_ptr/shared_ptr types (RAII) validated by C++ STL, custom business logic", + "issue_pattern": "Missing validation for unique_ptr/shared_ptr types (RAII)", + "why_false_positive": "C++ STL, custom business logic validates unique_ptr/shared_ptr types (RAII) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "map key existence (m_map.find)", + "validation", + "missing", + "check" + ], + "evidence": "Field map key existence (m_map.find) validated by C++ STL, custom business logic", + "issue_pattern": "Missing validation for map key existence (m_map.find)", + "why_false_positive": "C++ STL, custom business logic validates map key existence (m_map.find) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "bool flags (acquire, stopping_)", + "validation", + "missing", + "check" + ], + "evidence": "Field bool flags (acquire, stopping_) validated by C++ STL, custom business logic", + "issue_pattern": "Missing validation for bool flags (acquire, stopping_)", + "why_false_positive": "C++ STL, custom business logic validates bool flags (acquire, stopping_) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "hash (uint256)", + "empty", + "string", + "validation" + ], + "evidence": "m_map.find(hash); checks for existence at getAcquire, getSet", + "issue_pattern": "Missing empty string validation for hash (uint256)", + "why_false_positive": "m_map.find(hash); checks for existence validates hash (uint256) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "acquire (bool)", + "empty", + "string", + "validation" + ], + "evidence": "if (!acquire || stopping_) at getSet", + "issue_pattern": "Missing empty string validation for acquire (bool)", + "why_false_positive": "if (!acquire || stopping_) validates acquire (bool) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "peerSetBuilder", + "empty", + "string", + "validation" + ], + "evidence": "std::move(peerSetBuilder) in constructor at InboundTransactionsImp constructor", + "issue_pattern": "Missing empty string validation for peerSetBuilder", + "why_false_positive": "std::move(peerSetBuilder) in constructor validates peerSetBuilder for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "peerSetBuilder", + "type", + "validation", + "check" + ], + "evidence": "std::move(peerSetBuilder) in constructor at InboundTransactionsImp constructor", + "issue_pattern": "Missing type validation for peerSetBuilder", + "why_false_positive": "std::move(peerSetBuilder) in constructor validates peerSetBuilder type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "set (std::shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "std::make_shared(...) at InboundTransactionsImp constructor", + "issue_pattern": "Missing empty string validation for set (std::shared_ptr)", + "why_false_positive": "std::make_shared(...) validates set (std::shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "set (std::shared_ptr)", + "type", + "validation", + "check" + ], + "evidence": "std::make_shared(...) at InboundTransactionsImp constructor", + "issue_pattern": "Missing type validation for set (std::shared_ptr)", + "why_false_positive": "std::make_shared(...) validates set (std::shared_ptr) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "ta (TransactionAcquire::pointer)", + "empty", + "string", + "validation" + ], + "evidence": "std::make_shared(...) at getSet", + "issue_pattern": "Missing empty string validation for ta (TransactionAcquire::pointer)", + "why_false_positive": "std::make_shared(...) validates ta (TransactionAcquire::pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ta (TransactionAcquire::pointer)", + "type", + "validation", + "check" + ], + "evidence": "std::make_shared(...) at getSet", + "issue_pattern": "Missing type validation for ta (TransactionAcquire::pointer)", + "why_false_positive": "std::make_shared(...) validates ta (TransactionAcquire::pointer) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/InboundTransactions.cpp", + "functions": [ + { + "args": [ + "Application& app", + "beast::insight::Collector::ptr const& collector", + "std::function const&, bool)> gotSet", + "std::unique_ptr peerSetBuilder" + ], + "lineno": 34, + "name": "InboundTransactionsImp" + }, + { + "args": [ + "uint256 const& hash" + ], + "lineno": 49, + "name": "getAcquire" + }, + { + "args": [ + "uint256 const& hash", + "bool acquire" + ], + "lineno": 59, + "name": "getSet" + }, + { + "args": [ + "LedgerHash const& hash", + "std::shared_ptr peer", + "std::shared_ptr packet_ptr" + ], + "lineno": 89, + "name": "gotData" + }, + { + "args": [ + "uint256 const& hash", + "std::shared_ptr const& set", + "bool fromAcquire" + ], + "lineno": 123, + "name": "giveSet" + }, + { + "args": [ + "std::uint32_t seq" + ], + "lineno": 143, + "name": "newRound" + }, + { + "args": [], + "lineno": 167, + "name": "stop" + }, + { + "args": [], + "lineno": 184, + "name": "~InboundTransactions" + }, + { + "args": [ + "Application& app", + "beast::insight::Collector::ptr const& collector", + "std::function const&, bool)> gotSet" + ], + "lineno": 186, + "name": "make_InboundTransactions" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested by integration and unit tests for transaction set acquisition and peer data handling. Look for test files in the xrpld/test or xrpld/app/ledger/test directories, such as InboundTransactions_test.cpp, TransactionAcquire_test.cpp, or SHAMap_test.cpp. However, specific validation branches (e.g., malformed node data, missing hash in map, stopping_ flag) may not be exhaustively tested unless negative/edge-case tests are present. Test coverage for peer resource charging and error handling may be limited.", + "validation_architecture": { + "auto_validated_fields": [ + "unique_ptr/shared_ptr types (RAII)", + "map key existence (m_map.find)", + "bool flags (acquire, stopping_)" + ], + "framework": "C++ STL, custom business logic", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "none (returns empty pointer or nullptr)", + "field": "hash (uint256)", + "location": "getAcquire, getSet", + "validated_by": "m_map.find(hash); checks for existence", + "validates": [ + "hash must exist in m_map to retrieve TransactionAcquire or SHAMap" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (returns nullptr)", + "field": "acquire (bool)", + "location": "getSet", + "validated_by": "if (!acquire || stopping_)", + "validates": [ + "acquire must be true and not stopping_ to proceed with acquisition" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (assumes unique_ptr is valid)", + "field": "peerSetBuilder", + "location": "InboundTransactionsImp constructor", + "validated_by": "std::move(peerSetBuilder) in constructor", + "validates": [ + "peerSetBuilder must be a valid unique_ptr" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (relies on std::make_shared to throw if allocation fails)", + "field": "set (std::shared_ptr)", + "location": "InboundTransactionsImp constructor", + "validated_by": "std::make_shared(...)", + "validates": [ + "set must be a valid SHAMap pointer" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "std::bad_alloc if allocation fails", + "field": "ta (TransactionAcquire::pointer)", + "location": "getSet", + "validated_by": "std::make_shared(...)", + "validates": [ + "ta must be a valid TransactionAcquire pointer" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/InboundTransactions.cpp.ai.md b/src/xrpld/app/ledger/detail/InboundTransactions.cpp.ai.md new file mode 100644 index 0000000000..6afee17f9c --- /dev/null +++ b/src/xrpld/app/ledger/detail/InboundTransactions.cpp.ai.md @@ -0,0 +1,35 @@ +# `InboundTransactions.cpp` — Transaction Set Acquisition and Lifecycle Manager + +This file implements the `InboundTransactions` subsystem, which manages the acquisition, caching, and lifetime of transaction sets (`SHAMap` instances of type `TRANSACTION`) that the consensus engine needs during each ledger-building round. During XRPL consensus, every validator must converge on the same set of transactions; this component is what bridges the gap between "I know the hash of the transaction set I need" and "I actually have its contents." + +## Internal Structure + +The file defines two private classes and a factory function. `InboundTransactionSet` is a trivial value holder bundling a round sequence number (`mSeq`), an optional in-progress `TransactionAcquire` object, and the final `SHAMap` once acquisition completes. `InboundTransactionsImp` is the concrete implementation, hidden behind the `InboundTransactions` abstract interface declared in the header. Consumers interact only with the interface and the `make_InboundTransactions()` factory, keeping the acquisition machinery fully encapsulated. + +The central data structure is `m_map`, a `hash_map` keyed by transaction set hash. A single `std::recursive_mutex` (`mLock`) serializes all access to this map. + +## The Zero Set + +The constructor immediately seeds `m_map` with a special entry for `uint256()` (the all-zeros hash), holding a pre-built empty, unbacked `SHAMap`. This "zero set" represents the empty transaction set, which is a valid consensus outcome. A reference `m_zeroSet` is bound to this map entry at construction time; this is safe because `hash_map` (an open-addressing or node-based hash map) does not move elements on insert, so the reference remains stable. `newRound()` explicitly bumps `m_zeroSet.mSeq` on every round to prevent it from being swept out by the TTL logic. + +## Acquisition Flow (`getSet`) + +`getSet(hash, acquire)` is the primary read path. When the requested set is already in `m_map`, it returns the `SHAMap` immediately. If acquisition is in progress and `acquire` is true, it also calls `stillNeed()` on the `TransactionAcquire` object, which resets the peer retry counter back to the normal threshold — a defensive measure that prevents the timeout from giving up on a set the caller just confirmed it still needs. + +When the set is unknown and `acquire` is true, a new `TransactionAcquire` is created and inserted into `m_map` under the lock, then `init(startPeers)` is called **outside** the lock. This lock-release-before-init pattern is deliberate: `init()` acquires `TransactionAcquire`'s own internal mutex and immediately contacts peers via `addPeers`. When acquisition later completes, `TransactionAcquire::done()` posts a job to call `giveSet()`, which in turn re-acquires `mLock`. Holding `mLock` across `init()` would create a potential deadlock path. After initiating acquisition, `getSet` returns `nullptr`; callers are expected to retry via the `gotSet` callback rather than blocking. + +## Incoming Data Pipeline (`gotData`) + +`gotData()` is called from the network layer when a peer delivers a `TMLedgerData` protobuf message. The implementation first fetches the corresponding `TransactionAcquire` under lock — releasing the lock before any further work. If no acquisition is active for the given hash, the peer is immediately charged `Resource::feeUselessData`. Otherwise, each node in the packet is validated for the presence of both `nodeid` and `nodedata` fields, and `deserializeSHAMapNodeID` is called on the raw bytes; any deserialization failure triggers `feeInvalidData`. The validated nodes are then handed to `TransactionAcquire::takeNodes()`, which returns a `SHAMapAddNode` status; a non-useful result triggers a `feeUselessData` charge on the peer. This multi-layered charging is an inbound DoS defense: it differentiates malformed, unsolicited, and redundant data to accurately penalize misbehaving peers. + +## Completion and Deduplication (`giveSet`) + +Once `TransactionAcquire` assembles all nodes, it marks the `SHAMap` immutable and posts a job to call `giveSet(hash, map, true)`. Locally-constructed sets (built by consensus itself) arrive via `giveSet(hash, map, false)`. Inside `giveSet`, the implementation checks whether the entry already has a complete set; if so, the `m_gotSet` callback is suppressed. This deduplication prevents the consensus layer from processing the same transaction set twice — which could happen if, say, a local set construction races with a peer delivering the same set. In either case the `mAcquire` pointer is cleared, releasing the `TransactionAcquire` object and its peer connections. + +## Round-Based TTL (`newRound`) + +Each time consensus starts a new round, `newRound(seq)` is called. The implementation keeps only entries whose `mSeq` falls within `[seq - setKeepRounds, seq + setKeepRounds]` where `setKeepRounds = 3`. The forward window (allowing entries newer than the current round) is important because validators may receive transaction sets for the upcoming round before the current one closes. Sets older than three rounds are presumed irrelevant and evicted. The zero set is always pinned to the current `seq` to avoid eviction. + +## Shutdown Safety + +`stop()` sets a `stopping_` flag under the lock and clears `m_map`, dropping all shared_ptr references to in-flight acquisitions. The `getSet` fast-path checks `stopping_` before creating any new `TransactionAcquire`, ensuring no new acquisitions begin after the application begins shutting down. Since `TransactionAcquire::done()` posts the `giveSet` call as a job rather than calling it directly, jobs queued before `stop()` may still invoke `giveSet` after the map is cleared; `giveSet`'s `m_map[hash]` operator will re-insert a fresh entry, but `m_gotSet` will still fire for it. This is noted as acceptable in the `TransactionAcquire` source: the consensus structures need not be updated during shutdown. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerCleaner.cpp.ai.json b/src/xrpld/app/ledger/detail/LedgerCleaner.cpp.ai.json new file mode 100644 index 0000000000..afc33d9a6e --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerCleaner.cpp.ai.json @@ -0,0 +1,509 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app, beast::Journal journal" + ], + "lineno": 27, + "name": "LedgerCleanerImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "clean", + "LedgerMaster::getFullValidatedRange", + "LedgerCleanerImp::clean (field assignments)", + "run (via thread_)" + ], + "entry_point": "clean", + "purpose": "Initiates a ledger cleaning operation, sets up parameters, and triggers the background cleaning thread.", + "validation_points": [ + "LedgerMaster::getFullValidatedRange (validates ledger range)", + "LedgerCleanerImp::clean (validates and assigns minRange_, maxRange_, checkNodes_, fixTxns_, failures_)" + ] + }, + { + "call_chain": [ + "start", + "std::thread{&LedgerCleanerImp::run, this}", + "run" + ], + "entry_point": "start", + "purpose": "Starts the background cleaning thread.", + "validation_points": [ + "RAII validation of thread_ (checked in destructor and stop)" + ] + }, + { + "call_chain": [ + "stop", + "shouldExit_ = true", + "wakeup_.notify_one()", + "thread_.join()" + ], + "entry_point": "stop", + "purpose": "Stops the background cleaning thread safely.", + "validation_points": [ + "RAII validation of thread_ (checked in destructor and stop)" + ] + }, + { + "call_chain": [ + "onWrite", + "PropertyStream::Map population" + ], + "entry_point": "onWrite", + "purpose": "Reports the current status and parameters of the cleaner.", + "validation_points": [ + "Explicit checks on maxRange_, failures_, checkNodes_, fixTxns_" + ] + } + ], + "data_flows": [ + { + "field": "thread_", + "flow": [ + "constructor", + "start (assigned new std::thread)", + "run (executed in thread)", + "stop (joined)", + "destructor (checked for joinable)" + ], + "origin": "LedgerCleanerImp constructor (default-initialized)", + "transformations": [ + "Created, started, joined, checked for proper shutdown" + ], + "validated_at": "Destructor (RAII), stop()" + }, + { + "field": "minRange_ / maxRange_", + "flow": [ + "clean (retrieved from LedgerMaster)", + "assigned to minRange_, maxRange_", + "used in run/onWrite/doLedgerCleaner" + ], + "origin": "LedgerMaster::getFullValidatedRange (called in clean)", + "transformations": [ + "Set from validated ledger range, used to control cleaning scope" + ], + "validated_at": "Type system (LedgerIndex), explicit checks in onWrite" + }, + { + "field": "checkNodes_", + "flow": [ + "clean (set)", + "used in run/doLedgerCleaner/onWrite" + ], + "origin": "clean (set from JSON params or default)", + "transformations": [ + "Boolean flag, set from input, controls node checking" + ], + "validated_at": "Type system, explicit check in onWrite" + }, + { + "field": "fixTxns_", + "flow": [ + "clean (set)", + "used in run/doLedgerCleaner/onWrite" + ], + "origin": "clean (set from JSON params or default)", + "transformations": [ + "Boolean flag, set from input, controls SQL rewrite" + ], + "validated_at": "Type system, explicit check in onWrite" + }, + { + "field": "failures_", + "flow": [ + "clean (reset)", + "incremented on error", + "reported in onWrite" + ], + "origin": "clean (reset to 0), incremented in run/doLedgerCleaner", + "transformations": [ + "Integer counter, reset and incremented" + ], + "validated_at": "Type system, explicit check in onWrite" + } + ], + "description": "Implements the LedgerCleaner, which cleans up and repairs inconsistencies in the ledger databases, checks for missing nodes, and can reprocess transactions or stop cleaning on request.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "thread_ (lifetime and joinability via RAII)", + "validation", + "missing", + "check" + ], + "evidence": "Field thread_ (lifetime and joinability via RAII) validated by C++ type system, RAII, custom exception (LogicError)", + "issue_pattern": "Missing validation for thread_ (lifetime and joinability via RAII)", + "why_false_positive": "C++ type system, RAII, custom exception (LogicError) validates thread_ (lifetime and joinability via RAII) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "minRange_, maxRange_, checkNodes_, fixTxns_, failures_ (type-checked by compiler)", + "validation", + "missing", + "check" + ], + "evidence": "Field minRange_, maxRange_, checkNodes_, fixTxns_, failures_ (type-checked by compiler) validated by C++ type system, RAII, custom exception (LogicError)", + "issue_pattern": "Missing validation for minRange_, maxRange_, checkNodes_, fixTxns_, failures_ (type-checked by compiler)", + "why_false_positive": "C++ type system, RAII, custom exception (LogicError) validates minRange_, maxRange_, checkNodes_, fixTxns_, failures_ (type-checked by compiler) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "thread_ (std::thread)", + "empty", + "string", + "validation" + ], + "evidence": "RAII pattern, destructor check at LedgerCleanerImp::~LedgerCleanerImp()", + "issue_pattern": "Missing empty string validation for thread_ (std::thread)", + "why_false_positive": "RAII pattern, destructor check validates thread_ (std::thread) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "status fields (minRange_, maxRange_, checkNodes_, fixTxns_, failures_)", + "empty", + "string", + "validation" + ], + "evidence": "type system, explicit checks at onWrite()", + "issue_pattern": "Missing empty string validation for status fields (minRange_, maxRange_, checkNodes_, fixTxns_, failures_)", + "why_false_positive": "type system, explicit checks validates status fields (minRange_, maxRange_, checkNodes_, fixTxns_, failures_) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerCleaner.cpp", + "functions": [ + { + "args": [], + "lineno": 49, + "name": "start" + }, + { + "args": [], + "lineno": 54, + "name": "stop" + }, + { + "args": [ + "map" + ], + "lineno": 67, + "name": "onWrite" + }, + { + "args": [ + "params" + ], + "lineno": 89, + "name": "clean" + }, + { + "args": [], + "lineno": 164, + "name": "run" + }, + { + "args": [ + "ledger", + "index" + ], + "lineno": 185, + "name": "getLedgerHash" + }, + { + "args": [ + "ledgerIndex", + "ledgerHash", + "doNodes", + "doTxns" + ], + "lineno": 205, + "name": "doLedger" + }, + { + "args": [ + "ledgerIndex", + "referenceLedger" + ], + "lineno": 246, + "name": "getHash" + }, + { + "args": [], + "lineno": 288, + "name": "doLedgerCleaner" + }, + { + "args": [ + "app", + "journal" + ], + "lineno": 355, + "name": "make_LedgerCleaner" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "LedgerCleaner is a background maintenance component. Typical test coverage would be in integration/system tests, not unit tests. Look for tests in files like 'LedgerCleaner_test.cpp', 'LedgerMaster_test.cpp', or higher-level ledger/consensus tests. Validation of thread lifecycle (start/stop/destructor) is hard to unit test; may rely on manual or integration testing. JSON parameter parsing and field validation may be tested via RPC/command tests. Gaps: Direct unit tests for thread RAII, error handling, and edge cases (e.g., double start/stop, invalid ledger ranges) may be missing.", + "validation_architecture": { + "auto_validated_fields": [ + "thread_ (lifetime and joinability via RAII)", + "minRange_, maxRange_, checkNodes_, fixTxns_, failures_ (type-checked by compiler)" + ], + "framework": "C++ type system, RAII, custom exception (LogicError)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LogicError(\"LedgerCleanerImp::stop not called.\")", + "field": "thread_ (std::thread)", + "location": "LedgerCleanerImp::~LedgerCleanerImp()", + "validated_by": "RAII pattern, destructor check", + "validates": [ + "Ensures thread_ is not joinable (i.e., stop() was called before destruction)", + "Prevents resource leak or undefined behavior" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (fields are only reported, not set)", + "field": "status fields (minRange_, maxRange_, checkNodes_, fixTxns_, failures_)", + "location": "onWrite()", + "validated_by": "type system, explicit checks", + "validates": [ + "Checks if maxRange_ == 0 to determine idle/running status", + "Ensures only valid status is reported" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerCleaner.cpp.ai.md b/src/xrpld/app/ledger/detail/LedgerCleaner.cpp.ai.md new file mode 100644 index 0000000000..b27e633703 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerCleaner.cpp.ai.md @@ -0,0 +1,43 @@ +# LedgerCleaner.cpp + +`LedgerCleaner.cpp` implements `LedgerCleanerImp`, a background maintenance service that detects and repairs two classes of database corruption that can accumulate in a long-running rippled node: inconsistencies between the node store (key-value SHAMap storage) and the SQLite account/transaction databases, and gaps or missing nodes in ledger history. It was originally motivated by bugs in older versions of rippled that could leave the SQL databases in a state that diverged from the node store. The implementation lives behind the `LedgerCleaner` abstract interface declared in `LedgerCleaner.h`, which the rest of the application holds as a `std::unique_ptr`. The concrete class is created exclusively through `make_LedgerCleaner()`. + +## Threading Model + +The cleaner owns a dedicated `std::thread` (`thread_`) that it starts via `start()` and tears down via `stop()`. The destructor enforces a strict lifecycle invariant: if `thread_` is still joinable at destruction time, `LogicError` is thrown. This is intentional — it makes improper shutdown a loud hard fault rather than a silent resource leak or undefined behavior from a detached thread accessing a destroyed object. + +Shared state between the calling thread (which invokes `clean()`, `stop()`, and `onWrite()`) and the cleaning thread is protected by a single `std::mutex` combined with a `std::condition_variable` (`wakeup_`). The `run()` loop blocks on `wakeup_` until either the `State::cleaning` flag is set or `shouldExit_` becomes true, then calls `doLedgerCleaner()`. After `doLedgerCleaner()` returns, the state flips back to `notCleaning` and the loop waits again. This means re-invoking `clean()` while a previous run is still in progress simply updates the parameters atomically — the next iteration of `doLedgerCleaner()`'s inner loop will pick them up naturally. + +## Parameter Handling + +The `clean()` method accepts a `Json::Value` with several optional fields: `ledger` (single-ledger shortcut that also implies both `fix_txns` and `check_nodes`), `min_ledger`/`max_ledger` for range targeting, `full` as a shorthand that enables both modes simultaneously, `fix_txns` to rewrite SQL rows for transactions, `check_nodes` to verify node store completeness, and `stop` to cancel ongoing work by zeroing the range. When no explicit range is given, the cleaner defaults to the current fully-validated ledger range obtained from `LedgerMaster::getFullValidatedRange()`. Setting `stop` to true cleverly zeroes both `minRange_` and `maxRange_`, which the main loop detects as a terminal condition without needing a separate cancel flag. + +## The Cleaning Loop + +`doLedgerCleaner()` iterates from `maxRange_` downward toward `minRange_`, processing one ledger per iteration. Before each ledger it checks `getFeeTrack().isLoadedLocal()` — if the server is under elevated local load, the cleaner backs off entirely for five seconds. This makes it a genuinely low-priority background task that yields to foreground consensus and RPC work. + +For each ledger, the loop calls `getHash()` to resolve the correct ledger hash, then `doLedger()` to actually validate and repair. After a successful pass, `maxRange_` is decremented (and `minRange_` is incremented if we reach the floor), then the loop sleeps 100 ms to throttle I/O pressure. On failure, `failures_` is incremented and the loop sleeps two seconds — long enough for `InboundLedgers` to make progress acquiring the missing data before the next attempt. + +## Hash Resolution: `getHash()` and `getLedgerHash()` + +Finding the authoritative hash for a target ledger is non-trivial when operating over potentially deep history. `getHash()` maintains a `referenceLedger` — a known-good subsequent ledger whose skip list can be traversed backward. It calls `hashOfSeq()` on the reference ledger to look up the target index. If `hashOfSeq()` throws `SHAMapMissingNode`, `getLedgerHash()` catches the exception and immediately triggers a new inbound ledger acquisition, returning `beast::zero` (all-bits-zero) as a sentinel. This lazy-repair pattern means transient gaps in the node store self-heal as fetches complete. + +If the validated ledger's skip list doesn't reach the target directly (the hash comes back zero without an exception), `getHash()` calls `getCandidateLedger()` to compute the sequence number of an intermediate ledger whose skip list is guaranteed to contain the target. That intermediate ledger is acquired via `InboundLedgers::acquire()`, and if successful, the lookup is retried through it. This two-hop approach handles arbitrarily deep history without needing to walk ledger-by-ledger all the way back. + +## Per-Ledger Repair: `doLedger()` + +Given a hash, `doLedger()` performs up to four checks: + +1. **Node store availability**: `InboundLedgers::acquire()` returns the live ledger object. If it's not yet available, the index is cleared from `LedgerMaster`'s cache and a fresh acquisition is triggered — a probe-and-kick pattern. + +2. **SQL database consistency**: `loadByIndex()` loads the ledger from SQLite and compares the hash and parent hash against the node-store version. Any mismatch (including the common case from legacy bugs where the SQL row exists but contains wrong data) sets `doTxns = true` to force a rewrite. + +3. **History index consistency**: `LedgerMaster::fixIndex()` verifies and corrects the ledger's entry in the history table. A mismatch here also forces `doTxns = true`. + +4. **Node completeness** (when `doNodes` is true): `walkLedger()` traverses the entire SHAMap tree. If any node is missing, the ledger is cleared and re-acquired. This is the most expensive check and is only triggered by explicit user request via `check_nodes` or `full`. + +If `doTxns` ended up true (either by request or by detecting a mismatch), `pendSaveValidated()` is called with `isSynchronous = true` to rewrite the SQL rows for this ledger synchronously before the cleaner advances to the next. + +## Observability + +`onWrite()` exposes the current state to `beast::PropertyStream`, which the application uses for status reporting (e.g., via the `server_info` RPC). When `maxRange_` is zero the cleaner reports `"idle"`; otherwise it reports the current range, both mode flags, and the failure count if nonzero. This gives operators a live view of cleaning progress without needing a separate monitoring thread. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp.ai.json b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp.ai.json new file mode 100644 index 0000000000..720a2fea00 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp.ai.json @@ -0,0 +1,635 @@ +{ + "args": [ + { + "lineno": 11, + "name": "app" + }, + { + "lineno": 12, + "name": "inboundLedgers" + }, + { + "lineno": 13, + "name": "ledgerHash" + }, + { + "lineno": 14, + "name": "ledgerSeq" + }, + { + "lineno": 15, + "name": "peerSet" + }, + { + "lineno": 26, + "name": "numPeers" + }, + { + "lineno": 34, + "name": "limit" + }, + { + "lineno": 34, + "name": "sl" + }, + { + "lineno": 74, + "name": "progress" + }, + { + "lineno": 91, + "name": "info" + }, + { + "lineno": 91, + "name": "orderedTxns" + }, + { + "lineno": 112, + "name": "reason" + }, + { + "lineno": 112, + "name": "cb" + }, + { + "lineno": 127, + "name": "parent" + }, + { + "lineno": 154, + "name": "reason" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "uint256 const& ledgerHash", + "std::uint32_t ledgerSeq", + "std::unique_ptr peerSet" + ], + "lineno": 10, + "name": "LedgerDeltaAcquire" + } + ], + "code_paths": [ + { + "call_chain": [ + "LedgerDeltaAcquire::init", + "LedgerDeltaAcquire::trigger", + "peerSet_->addPeers (with two lambdas)", + "peer->supportsFeature", + "peer->hasLedger", + "app_.getLedgerMaster().getLedgerByHash" + ], + "entry_point": "LedgerDeltaAcquire::init", + "purpose": "Initializes the delta acquire process, triggers peer selection and validation, and attempts to acquire the ledger delta from peers or fallback.", + "validation_points": [ + "LedgerDeltaAcquire::trigger: app_.getLedgerMaster().getLedgerByHash(hash_) (validates ledger existence)", + "peerSet_->addPeers: peer->supportsFeature(ProtocolFeature::LedgerReplay) (validates peer feature support)", + "peerSet_->addPeers: peer->hasLedger(hash_, ledgerSeq_) (validates peer has requested ledger)", + "peerSet_->addPeers: ++noFeaturePeerCount >= MAX_NO_FEATURE_PEER_COUNT (validates peer count for fallback)" + ] + }, + { + "call_chain": [ + "LedgerDeltaAcquire::onTimer", + "LedgerDeltaAcquire::trigger" + ], + "entry_point": "LedgerDeltaAcquire::onTimer", + "purpose": "Handles timeouts, re-triggers peer selection or marks as failed if too many timeouts.", + "validation_points": [ + "LedgerDeltaAcquire::onTimer: timeouts_ > SUB_TASK_MAX_TIMEOUTS (validates timeout threshold)", + "LedgerDeltaAcquire::trigger: (see above for validations)" + ] + }, + { + "call_chain": [ + "LedgerDeltaAcquire::processData" + ], + "entry_point": "LedgerDeltaAcquire::processData", + "purpose": "Processes received ledger data, validates sequence, and prepares for replay.", + "validation_points": [ + "LedgerDeltaAcquire::processData: info.seq == ledgerSeq_ (validates correct ledger sequence)" + ] + } + ], + "data_flows": [ + { + "field": "fullLedger_", + "flow": [ + "LedgerDeltaAcquire::trigger", + "fullLedger_ assignment", + "if (fullLedger_) { complete_ = true; notify(sl); }" + ], + "origin": "app_.getLedgerMaster().getLedgerByHash(hash_)", + "transformations": [ + "Assigned from getLedgerByHash", + "Checked for existence" + ], + "validated_at": "LedgerDeltaAcquire::trigger" + }, + { + "field": "peer->supportsFeature(ProtocolFeature::LedgerReplay)", + "flow": [ + "peerSet_->addPeers (lambda filter)", + "if (peer->supportsFeature(...)) { ... } else { ... }" + ], + "origin": "peer object in peerSet_->addPeers", + "transformations": [ + "Boolean check for feature support" + ], + "validated_at": "peerSet_->addPeers (lambda filter and action)" + }, + { + "field": "peer->hasLedger(hash_, ledgerSeq_)", + "flow": [ + "peerSet_->addPeers (lambda filter)" + ], + "origin": "peer object in peerSet_->addPeers", + "transformations": [ + "Boolean check for ledger presence" + ], + "validated_at": "peerSet_->addPeers (lambda filter)" + }, + { + "field": "noFeaturePeerCount", + "flow": [ + "peerSet_->addPeers (lambda action)", + "if (++noFeaturePeerCount >= MAX_NO_FEATURE_PEER_COUNT) { fallBack_ = true; }" + ], + "origin": "Class member, incremented in peerSet_->addPeers", + "transformations": [ + "Incremented per peer without feature", + "Compared to threshold" + ], + "validated_at": "peerSet_->addPeers (lambda action)" + }, + { + "field": "timeouts_", + "flow": [ + "LedgerDeltaAcquire::onTimer", + "if (timeouts_ > SUB_TASK_MAX_TIMEOUTS) { failed_ = true; notify(sl); }" + ], + "origin": "Class member, incremented on timeout", + "transformations": [ + "Incremented on each timeout", + "Compared to threshold" + ], + "validated_at": "LedgerDeltaAcquire::onTimer" + }, + { + "field": "orderedTxns_", + "flow": [ + "LedgerDeltaAcquire::processData", + "if (info.seq == ledgerSeq_) { orderedTxns_ = std::move(orderedTxns); }" + ], + "origin": "processData argument", + "transformations": [ + "Moved from argument to member" + ], + "validated_at": "LedgerDeltaAcquire::processData (info.seq == ledgerSeq_)" + } + ], + "description": "Implements the LedgerDeltaAcquire class, which manages the acquisition and replay of ledger deltas in the XRPL node, handling peer communication, data processing, and ledger building for ledger replay operations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fullLedger_ (ledger existence by hash)", + "empty", + "string", + "validation" + ], + "evidence": "app_.getLedgerMaster().getLedgerByHash(hash_) at LedgerDeltaAcquire::trigger", + "issue_pattern": "Missing empty string validation for fullLedger_ (ledger existence by hash)", + "why_false_positive": "app_.getLedgerMaster().getLedgerByHash(hash_) validates fullLedger_ (ledger existence by hash) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "peer supports LedgerReplay feature", + "empty", + "string", + "validation" + ], + "evidence": "peer->supportsFeature(ProtocolFeature::LedgerReplay) at LedgerDeltaAcquire::trigger (lambda in peerSet_->addPeers)", + "issue_pattern": "Missing empty string validation for peer supports LedgerReplay feature", + "why_false_positive": "peer->supportsFeature(ProtocolFeature::LedgerReplay) validates peer supports LedgerReplay feature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "peer has requested ledger", + "empty", + "string", + "validation" + ], + "evidence": "peer->hasLedger(hash_, ledgerSeq_) at LedgerDeltaAcquire::trigger (lambda in peerSet_->addPeers)", + "issue_pattern": "Missing empty string validation for peer has requested ledger", + "why_false_positive": "peer->hasLedger(hash_, ledgerSeq_) validates peer has requested ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "noFeaturePeerCount (number of peers without LedgerReplay support)", + "empty", + "string", + "validation" + ], + "evidence": "++noFeaturePeerCount >= LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT at LedgerDeltaAcquire::trigger (lambda in peerSet_->addPeers)", + "issue_pattern": "Missing empty string validation for noFeaturePeerCount (number of peers without LedgerReplay support)", + "why_false_positive": "++noFeaturePeerCount >= LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT validates noFeaturePeerCount (number of peers without LedgerReplay support) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "noFeaturePeerCount (number of peers without LedgerReplay support)", + "range", + "bounds", + "validation" + ], + "evidence": "++noFeaturePeerCount >= LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT at LedgerDeltaAcquire::trigger (lambda in peerSet_->addPeers)", + "issue_pattern": "Missing range validation for noFeaturePeerCount (number of peers without LedgerReplay support)", + "why_false_positive": "++noFeaturePeerCount >= LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT validates noFeaturePeerCount (number of peers without LedgerReplay support) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "timeouts_ (number of timeouts)", + "empty", + "string", + "validation" + ], + "evidence": "timeouts_ > LedgerReplayParameters::SUB_TASK_MAX_TIMEOUTS at LedgerDeltaAcquire::onTimer", + "issue_pattern": "Missing empty string validation for timeouts_ (number of timeouts)", + "why_false_positive": "timeouts_ > LedgerReplayParameters::SUB_TASK_MAX_TIMEOUTS validates timeouts_ (number of timeouts) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "timeouts_ (number of timeouts)", + "range", + "bounds", + "validation" + ], + "evidence": "timeouts_ > LedgerReplayParameters::SUB_TASK_MAX_TIMEOUTS at LedgerDeltaAcquire::onTimer", + "issue_pattern": "Missing range validation for timeouts_ (number of timeouts)", + "why_false_positive": "timeouts_ > LedgerReplayParameters::SUB_TASK_MAX_TIMEOUTS validates timeouts_ (number of timeouts) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "isDone() (completion state)", + "empty", + "string", + "validation" + ], + "evidence": "isDone() at LedgerDeltaAcquire::init", + "issue_pattern": "Missing empty string validation for isDone() (completion state)", + "why_false_positive": "isDone() validates isDone() (completion state) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp", + "functions": [ + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "uint256 const& ledgerHash", + "std::uint32_t ledgerSeq", + "std::unique_ptr peerSet" + ], + "lineno": 10, + "name": "LedgerDeltaAcquire" + }, + { + "args": [], + "lineno": 22, + "name": "~LedgerDeltaAcquire" + }, + { + "args": [ + "int numPeers" + ], + "lineno": 26, + "name": "init" + }, + { + "args": [ + "std::size_t limit", + "ScopedLockType& sl" + ], + "lineno": 34, + "name": "trigger" + }, + { + "args": [ + "bool progress", + "ScopedLockType& sl" + ], + "lineno": 74, + "name": "onTimer" + }, + { + "args": [], + "lineno": 86, + "name": "pmDowncast" + }, + { + "args": [ + "LedgerHeader const& info", + "std::map>&& orderedTxns" + ], + "lineno": 91, + "name": "processData" + }, + { + "args": [ + "InboundLedger::Reason reason", + "OnDeltaDataCB&& cb" + ], + "lineno": 112, + "name": "addDataCallback" + }, + { + "args": [ + "std::shared_ptr const& parent" + ], + "lineno": 127, + "name": "tryBuild" + }, + { + "args": [ + "ScopedLockType& sl", + "std::optional reason" + ], + "lineno": 154, + "name": "onLedgerBuilt" + }, + { + "args": [ + "ScopedLockType& sl" + ], + "lineno": 181, + "name": "notify" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is likely tested indirectly via integration tests for ledger replay and acquisition, such as in test files under `src/test/ledger/`, `src/test/app/`, or `src/test/overlay/`. However, the specific validation branches (e.g., fallback on too many non-feature peers, timeout handling, and correct peer selection) may not be fully covered by unit tests, especially edge cases like exceeding MAX_NO_FEATURE_PEER_COUNT or SUB_TASK_MAX_TIMEOUTS. Direct unit tests for LedgerDeltaAcquire are likely missing or minimal, as much of the logic depends on peer/network simulation and application state.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic, PeerSet, LedgerMaster", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (early return, sets complete_ = true)", + "field": "fullLedger_ (ledger existence by hash)", + "location": "LedgerDeltaAcquire::trigger", + "validated_by": "app_.getLedgerMaster().getLedgerByHash(hash_)", + "validates": [ + "Checks if the requested ledger already exists in local storage by hash" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (peer is skipped if not supported, fallback logic triggered if too many)", + "field": "peer supports LedgerReplay feature", + "location": "LedgerDeltaAcquire::trigger (lambda in peerSet_->addPeers)", + "validated_by": "peer->supportsFeature(ProtocolFeature::LedgerReplay)", + "validates": [ + "Checks if peer supports the LedgerReplay protocol feature" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (peer is skipped if not present)", + "field": "peer has requested ledger", + "location": "LedgerDeltaAcquire::trigger (lambda in peerSet_->addPeers)", + "validated_by": "peer->hasLedger(hash_, ledgerSeq_)", + "validates": [ + "Checks if peer has the requested ledger by hash and sequence" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets fallBack_ = true, changes timerInterval_)", + "field": "noFeaturePeerCount (number of peers without LedgerReplay support)", + "location": "LedgerDeltaAcquire::trigger (lambda in peerSet_->addPeers)", + "validated_by": "++noFeaturePeerCount >= LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT", + "validates": [ + "Checks if the number of peers without LedgerReplay support exceeds a maximum threshold" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets failed_ = true, likely triggers failure handling elsewhere)", + "field": "timeouts_ (number of timeouts)", + "location": "LedgerDeltaAcquire::onTimer", + "validated_by": "timeouts_ > LedgerReplayParameters::SUB_TASK_MAX_TIMEOUTS", + "validates": [ + "Checks if the number of timeouts exceeds a maximum allowed value" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (early return if already done)", + "field": "isDone() (completion state)", + "location": "LedgerDeltaAcquire::init", + "validated_by": "isDone()", + "validates": [ + "Checks if the acquisition process is already complete before triggering" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp.ai.md b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp.ai.md new file mode 100644 index 0000000000..e6934eebed --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.cpp.ai.md @@ -0,0 +1,37 @@ +# `LedgerDeltaAcquire.cpp` — Single-Ledger Delta Fetcher for Ledger Replay + +## Role in the System + +XRPL nodes that fall behind the network can catch up by *replaying* a range of historical ledgers rather than downloading each full state trie. `LedgerDeltaAcquire` is the subtask responsible for acquiring the delta of a single ledger in that range: the ledger header (`LedgerHeader`) and its ordered transaction set (`STTx`). It is one of two subtask types in the ledger replay subsystem; the other is `SkipListAcquire`, which retrieves the skip list needed to enumerate the ledgers to replay. Both are orchestrated by `LedgerReplayTask`, which is in turn managed by `LedgerReplayer`. + +## Inheritance and the Timer Loop + +`LedgerDeltaAcquire` extends `TimeoutCounter`, a base class that implements an active-object retry loop backed by a Boost.Asio steady-clock timer and the application's `JobQueue`. The base class handles the mechanics: set a timer → fire a job → call `onTimer` → if still not done, re-arm. Subclasses override `onTimer()` and `pmDowncast()` (a `weak_ptr` downcast required so timer callbacks can safely check whether the object is still alive without extending its lifetime). The constructor registers `jtREPLAY_TASK` jobs capped at `MAX_QUEUED_TASKS` to bound the ledger-replay system's impact on the shared job queue. + +## Initialization and Peer Selection (`init`, `trigger`) + +`init()` acquires the object's mutex and calls `trigger()` followed by `setTimer()`. This two-step is intentional: `trigger()` may complete the task immediately (if the ledger is already in local storage), in which case the timer is still set but will see `isDone()` on its first fire and exit without doing work. The symmetry keeps every code path using the same timer lifecycle. + +Inside `trigger()`, the first check is `getLedgerByHash(hash_)`. If the local node already has this ledger (e.g. it was stored by a parallel acquisition), the task completes immediately by marking `complete_ = true` and calling `notify()`. This short-circuit avoids unnecessary network requests and is always checked before any peer communication. + +The normal path sends `TMReplayDeltaRequest` protocol messages to up to `limit` peers. The `addPeers` call uses two lambdas: a *filter* that requires the peer to both support `ProtocolFeature::LedgerReplay` and have the specific ledger, and an *action* that actually sends the request. Only peers supporting the replay feature receive the message; peers that lack it increment `noFeaturePeerCount`. When that counter reaches `MAX_NO_FEATURE_PEER_COUNT` (2), the task switches to *fallback mode*: it calls `InboundLedgers::acquire()` to download the full ledger by the traditional `GENERIC` path and extends `timerInterval_` from 250ms to 1000ms. This graceful degradation ensures the node can still catch up even if it is surrounded by peers running an older protocol version. + +## Receiving Data and Two-Phase Building + +When the overlay layer receives a `TMReplayDeltaResponse`, it routes the parsed, hash-verified data to `LedgerReplayer::gotReplayDelta()`, which forwards it to the relevant `LedgerDeltaAcquire` via `processData()`. At this point only a *lightweight* ledger is constructed — `replayTemp_` is a `Ledger` initialized from the header info alone, without a full SHAMap state. This object is not a valid, standalone ledger; it exists solely to carry the header metadata into `LedgerReplay` later. + +The actual ledger build happens in `tryBuild()`, called by `LedgerReplayTask` when it has both a parent ledger and a completed `LedgerDeltaAcquire` subtask. `tryBuild()` assembles a `LedgerReplay` value from the parent, `replayTemp_`, and `orderedTxns_`, then calls `buildLedger()` which re-executes each transaction against the parent state and constructs the resulting SHAMap. The critical correctness check is the final hash comparison: `fullLedger_->header().hash == hash_`. If the replayed ledger's hash does not match what was requested, the task throws `std::runtime_error` — a hard failure because it indicates data corruption or a logic error, not a transient network condition. + +Two `XRPL_ASSERT` calls enforce the precondition that the parent sequence is exactly one less than the target and that the parent hash matches the target's `parentHash` field. These are invariants that `LedgerReplayTask` is responsible for guaranteeing before calling `tryBuild()`. + +## Callback Registration and Notification + +Multiple consumers can register interest in the same delta via `addDataCallback()`. Each call adds an `OnDeltaDataCB` to `dataReadyCallbacks_` and registers a `Reason` (e.g., `GENERIC`). The `reasons_` set is used later in `onLedgerBuilt()` to decide what to do with the finished ledger — currently only `GENERIC` triggers `LedgerMaster::storeLedger()`; other reasons have placeholder `TODO` branches for future use. + +`notify()` drains the callback vector by swapping it into a local before releasing the lock, then fires each callback, then reacquires the lock. This unlock-callback-relock pattern is essential: callbacks invoke `LedgerReplayTask::deltaReady()`, which takes its own lock and may call back into `LedgerDeltaAcquire`'s public interface. Without the unlock, this would deadlock on the recursive mutex. `XRPL_ASSERT(isDone())` at the top of `notify()` enforces the contract that callbacks are never dispatched while the acquisition is still in progress. + +`onLedgerBuilt()` posts a `jtREPLAY_TASK` job rather than calling `LedgerMaster` inline. This keeps mutex hold times short and avoids priority inversions on a hot path that may be exercised for many sequential ledgers during a catchup. + +## Timeout and Failure Handling + +`onTimer()` uses a straightforward threshold policy: if `timeouts_` exceeds `SUB_TASK_MAX_TIMEOUTS` (10), the task is marked failed and `notify()` is called to propagate failure up to `LedgerReplayTask`. Otherwise it calls `trigger(1, sl)` to try adding one more peer. This means the task is willing to retry up to 10 times before giving up, giving the network roughly 10 × 250ms = 2.5 seconds (or 10 × 1000ms = 10 seconds in fallback mode) to respond. Once `LedgerReplayTask` sees a failed delta, it marks its own task as failed and propagates the failure to the node's ledger acquisition logic. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h.ai.json b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h.ai.json new file mode 100644 index 0000000000..03efedb3e6 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h.ai.json @@ -0,0 +1,162 @@ +{ + "args": [ + { + "lineno": 31, + "name": "app" + }, + { + "lineno": 32, + "name": "inboundLedgers" + }, + { + "lineno": 33, + "name": "ledgerHash" + }, + { + "lineno": 34, + "name": "ledgerSeq" + }, + { + "lineno": 35, + "name": "peerSet" + }, + { + "lineno": 39, + "name": "numPeers" + }, + { + "lineno": 47, + "name": "info" + }, + { + "lineno": 48, + "name": "orderedTxns" + }, + { + "lineno": 57, + "name": "parent" + }, + { + "lineno": 68, + "name": "reason" + }, + { + "lineno": 68, + "name": "cb" + }, + { + "lineno": 75, + "name": "progress" + }, + { + "lineno": 75, + "name": "peerSetLock" + }, + { + "lineno": 85, + "name": "limit" + }, + { + "lineno": 85, + "name": "sl" + }, + { + "lineno": 93, + "name": "reason" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "uint256 const& ledgerHash", + "std::uint32_t ledgerSeq", + "std::unique_ptr peerSet" + ], + "lineno": 18, + "name": "LedgerDeltaAcquire" + } + ], + "description": "Defines the LedgerDeltaAcquire class, which manages the retrieval and assembly of a ledger delta (header and transactions) from the network in the XRPL system, including callback management and peer coordination.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h", + "functions": [ + { + "args": [ + "numPeers" + ], + "lineno": 38, + "name": "init" + }, + { + "args": [ + "info", + "orderedTxns" + ], + "lineno": 46, + "name": "processData" + }, + { + "args": [ + "parent" + ], + "lineno": 56, + "name": "tryBuild" + }, + { + "args": [ + "reason", + "cb" + ], + "lineno": 67, + "name": "addDataCallback" + }, + { + "args": [ + "progress", + "peerSetLock" + ], + "lineno": 74, + "name": "onTimer" + }, + { + "args": [], + "lineno": 78, + "name": "pmDowncast" + }, + { + "args": [ + "limit", + "sl" + ], + "lineno": 84, + "name": "trigger" + }, + { + "args": [ + "sl", + "reason" + ], + "lineno": 92, + "name": "onLedgerBuilt" + }, + { + "args": [ + "sl" + ], + "lineno": 101, + "name": "notify" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "test" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h.ai.md b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h.ai.md new file mode 100644 index 0000000000..8f0a39add1 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerDeltaAcquire.h.ai.md @@ -0,0 +1,41 @@ +# `LedgerDeltaAcquire` — Ledger Delta Network Acquisition + +`LedgerDeltaAcquire` is a subtask in the XRPL ledger-replay pipeline, responsible for fetching a single ledger's **delta** — its header (`LedgerHeader`) and ordered transaction set — from the peer network so the ledger can be rebuilt deterministically from a known parent. It sits alongside `SkipListAcquire` as one of two concrete subtask types managed by `LedgerReplayTask` and, at a higher level, by `LedgerReplayer`. + +## Position in the Replay Architecture + +The replay subsystem exists to let a node reconstruct historical ledgers without downloading their full SHAMap state. Instead of fetching every account object, the node fetches only the transactions that changed the ledger, replays them against an already-validated parent, and derives the child. `LedgerDeltaAcquire` handles precisely the network-fetch phase: it collects and stores the raw ingredients (header + transactions) and then later, when `tryBuild()` is called with the parent ledger, drives the actual replay through `buildLedger()`. + +## Lifecycle + +The object moves through a clear two-phase sequence. First, `init(numPeers)` starts the timer loop (inherited from `TimeoutCounter`) and calls `trigger()` to dispatch requests. `trigger()` always checks `LedgerMaster::getLedgerByHash()` first — if the ledger is already present locally the task marks itself complete immediately and notifies without touching the network. + +When data arrives from a peer (validated externally before being passed in), `processData()` builds a header-only `replayTemp_` ledger — a lightweight `Ledger` object constructed from just the `LedgerHeader` and the current `Rules`. Storing transactions in `orderedTxns_` (keyed by transaction index) alongside this skeleton is sufficient because `buildLedger()` needs the parent state and a replay descriptor, not a pre-existing SHAMap. + +The second phase is `tryBuild()`, called by `LedgerReplayTask` once the parent ledger is in hand. It asserts that `parent->seq() + 1 == replayTemp_->seq()` and that the parent hash matches `replayTemp_->header().parentHash`, then constructs a `LedgerReplay` object and calls `buildLedger()`. If the resulting ledger's hash matches `hash_`, the task succeeds and `onLedgerBuilt()` is triggered; otherwise the task is marked failed and throws `std::runtime_error`, signalling a data corruption or malicious peer. + +## Peer Selection and Fallback + +The primary fetch strategy uses `peerSet_->addPeers()` to select peers that both support `ProtocolFeature::LedgerReplay` and claim to hold the target ledger, then sends a `TMReplayDeltaRequest`. If the `PeerSet` repeatedly returns peers that lack the feature, `noFeaturePeerCount` increments and once it reaches `MAX_NO_FEATURE_PEER_COUNT` (2), `fallBack_` is set. In fallback mode `trigger()` abandons the replay-specific protocol and calls `inboundLedgers_.acquire()` with `InboundLedger::Reason::GENERIC`, falling back to the traditional full-ledger acquisition path. The timer interval is simultaneously widened from `SUB_TASK_TIMEOUT` (250 ms) to `SUB_TASK_FALLBACK_TIMEOUT` (1000 ms) to give the heavier acquisition time to complete. This graceful degradation means a node that cannot find replay-capable peers can still fill gaps in its history, just less efficiently. + +## Timeout Handling + +`onTimer()` is called by `TimeoutCounter`'s async timer loop on each expiry. If `timeouts_` exceeds `SUB_TASK_MAX_TIMEOUTS` (10) the task is marked failed and `notify()` is called; otherwise `trigger(1, sl)` attempts a fresh single-peer request. The timeout count thus acts as a retry budget: the task will keep probing the network roughly every 250 ms for up to about 2.5 seconds before giving up. + +## Callback and Notification Pattern + +Callers (in practice, `LedgerReplayTask` instances) register interest via `addDataCallback(reason, cb)`. The `reason` parameter feeds into `reasons_` — a `std::set` that controls post-build ledger processing. The callback itself is pushed into `dataReadyCallbacks_`. Because a `LedgerDeltaAcquire` may serve multiple `LedgerReplayTask` instances that were created for different purposes (e.g., once for `GENERIC` catch-up and once for a second task added later), `addDataCallback()` correctly handles the already-done case: if the task is already complete when a new callback is registered, `notify()` is called immediately. + +`notify()` uses a swap-then-unlock pattern: it swaps `dataReadyCallbacks_` into a local vector, releases the mutex, then invokes each callback, then relocks. This avoids calling arbitrary external code while holding the lock — a classical deadlock-avoidance technique. + +## Post-Build Processing + +`onLedgerBuilt()` enqueues a `jtREPLAY_TASK` job that iterates the collected reasons. For `InboundLedger::Reason::GENERIC`, it calls `LedgerMaster::storeLedger()`. Only on the **first** build (i.e., not when a new reason is added to an already-built ledger) does it also call `LedgerMaster::tryAdvance()`, triggering the ledger advancement machinery. This distinction prevents duplicate advancement attempts when the same delta is reused for a new reason. + +## Thread Safety + +All mutable state is guarded by `TimeoutCounter::mtx_`, a `std::recursive_mutex`. Every public method acquires `ScopedLockType sl(mtx_)` before touching state. Private methods that participate in a call chain take the lock by reference (`ScopedLockType& sl`) as a convention that communicates "caller already holds the lock." The `recursive_mutex` permits `trigger()` to be called both from `init()` and from `onTimer()` — both of which already hold the lock — without deadlocking. + +## Design Notes + +The two-stage representation (`replayTemp_` for the header-skeleton, `fullLedger_` for the fully built ledger) separates concerns cleanly: data arrival and ledger construction are decoupled so that the parent ledger does not need to be available at the moment the delta data arrives from peers. The friend declarations for `LedgerReplayTask` (asserts only) and `test::LedgerReplayClient` are deliberately narrow — the former needs internal visibility only for debug invariant checks, and the test client needs direct state inspection without going through the public API. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp.ai.json b/src/xrpld/app/ledger/detail/LedgerMaster.cpp.ai.json new file mode 100644 index 0000000000..09358a21b0 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp.ai.json @@ -0,0 +1,858 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 755, + "name": "valSeq" + } + ], + "code_paths": [ + { + "call_chain": [ + "LedgerMaster::getValidLedgerIndex" + ], + "entry_point": "LedgerMaster::getValidLedgerIndex", + "purpose": "Returns the sequence number of the last validated ledger.", + "validation_points": [] + }, + { + "call_chain": [ + "LedgerMaster::isCompatible", + "LedgerMaster::getValidatedLedger", + "areCompatible" + ], + "entry_point": "LedgerMaster::isCompatible", + "purpose": "Checks if a given ledger view is compatible with the last validated ledger.", + "validation_points": [ + "areCompatible (compares hashes and sequences to validate compatibility)" + ] + }, + { + "call_chain": [ + "LedgerMaster::setValidLedger", + "LedgerMaster::getValidatedLedger", + "LedgerMaster::isCompatible" + ], + "entry_point": "LedgerMaster::setValidLedger", + "purpose": "Sets the current valid ledger and checks compatibility.", + "validation_points": [ + "LedgerMaster::isCompatible (calls areCompatible for validation)" + ] + }, + { + "call_chain": [ + "LedgerMaster::setPubLedger", + "LedgerMaster::getValidatedLedger", + "LedgerMaster::isCompatible" + ], + "entry_point": "LedgerMaster::setPubLedger", + "purpose": "Sets the published ledger and checks compatibility.", + "validation_points": [ + "LedgerMaster::isCompatible (calls areCompatible for validation)" + ] + }, + { + "call_chain": [ + "shouldAcquire" + ], + "entry_point": "shouldAcquire", + "purpose": "Determines if a candidate ledger should be fetched from the network based on current state and configuration.", + "validation_points": [ + "shouldAcquire (validates candidateLedger against currentLedger, ledgerHistory, minimumOnline)" + ] + } + ], + "data_flows": [ + { + "field": "mValidLedgerSeq", + "flow": [ + "LedgerMaster::setValidLedger", + "LedgerMaster::mValidLedgerSeq", + "LedgerMaster::getValidLedgerIndex" + ], + "origin": "Set internally by LedgerMaster when a ledger is validated", + "transformations": [ + "Set to the sequence number of the validated ledger" + ], + "validated_at": "LedgerMaster::setValidLedger (calls isCompatible for validation before setting)" + }, + { + "field": "validLedger", + "flow": [ + "LedgerMaster::getValidatedLedger", + "LedgerMaster::isCompatible", + "areCompatible" + ], + "origin": "LedgerMaster::getValidatedLedger", + "transformations": [ + "Fetched from internal state, passed to areCompatible for validation" + ], + "validated_at": "areCompatible" + }, + { + "field": "candidateLedger", + "flow": [ + "shouldAcquire (input)", + "shouldAcquire (logic)", + "Return value (bool: should acquire or not)" + ], + "origin": "Input parameter to shouldAcquire", + "transformations": [ + "Compared against currentLedger, ledgerHistory, minimumOnline" + ], + "validated_at": "shouldAcquire (internal logic)" + }, + { + "field": "view (ReadView const&)", + "flow": [ + "Caller", + "LedgerMaster::isCompatible", + "areCompatible" + ], + "origin": "Passed to LedgerMaster::isCompatible by caller (could be OpenLedger, etc.)", + "transformations": [ + "Used for hash and sequence comparison" + ], + "validated_at": "areCompatible" + } + ], + "description": "Implements the LedgerMaster class and related logic for managing, validating, publishing, and synchronizing ledgers in the XRPL server. Handles ledger history, fetch packs, pathfinding, and validation advancement.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerMaster.cpp", + "functions": [ + { + "args": [ + "currentLedger", + "ledgerHistory", + "minimumOnline", + "candidateLedger", + "j" + ], + "lineno": 27, + "name": "shouldAcquire" + }, + { + "args": [ + "app", + "stopwatch", + "collector", + "journal" + ], + "lineno": 44, + "name": "LedgerMaster" + }, + { + "args": [], + "lineno": 61, + "name": "getCurrentLedgerIndex" + }, + { + "args": [], + "lineno": 66, + "name": "getValidLedgerIndex" + }, + { + "args": [ + "view", + "s", + "reason" + ], + "lineno": 71, + "name": "isCompatible" + }, + { + "args": [], + "lineno": 90, + "name": "getPublishedLedgerAge" + }, + { + "args": [], + "lineno": 110, + "name": "getValidatedLedgerAge" + }, + { + "args": [ + "reason" + ], + "lineno": 130, + "name": "isCaughtUp" + }, + { + "args": [ + "l" + ], + "lineno": 148, + "name": "setValidLedger" + }, + { + "args": [ + "l" + ], + "lineno": 202, + "name": "setPubLedger" + }, + { + "args": [ + "transaction" + ], + "lineno": 208, + "name": "addHeldTransaction" + }, + { + "args": [ + "ledger" + ], + "lineno": 217, + "name": "canBeCurrent" + }, + { + "args": [ + "lastClosed" + ], + "lineno": 265, + "name": "switchLCL" + }, + { + "args": [ + "ledgerIndex", + "ledgerHash" + ], + "lineno": 285, + "name": "fixIndex" + }, + { + "args": [ + "ledger" + ], + "lineno": 289, + "name": "storeLedger" + }, + { + "args": [], + "lineno": 297, + "name": "applyHeldTransactions" + }, + { + "args": [ + "tx" + ], + "lineno": 312, + "name": "popAcctTransaction" + }, + { + "args": [ + "i" + ], + "lineno": 318, + "name": "setBuildingLedger" + }, + { + "args": [ + "seq" + ], + "lineno": 323, + "name": "haveLedger" + }, + { + "args": [ + "seq" + ], + "lineno": 328, + "name": "clearLedger" + }, + { + "args": [ + "ledger" + ], + "lineno": 333, + "name": "isValidated" + }, + { + "args": [ + "minVal", + "maxVal" + ], + "lineno": 362, + "name": "getFullValidatedRange" + }, + { + "args": [ + "minVal", + "maxVal" + ], + "lineno": 387, + "name": "getValidatedRange" + }, + { + "args": [], + "lineno": 419, + "name": "getEarliestFetch" + }, + { + "args": [ + "ledger" + ], + "lineno": 432, + "name": "tryFill" + }, + { + "args": [ + "missing", + "reason" + ], + "lineno": 474, + "name": "getFetchPack" + }, + { + "args": [ + "ledger" + ], + "lineno": 511, + "name": "fixMismatch" + }, + { + "args": [ + "ledger", + "isSynchronous", + "isCurrent" + ], + "lineno": 545, + "name": "setFullLedger" + }, + { + "args": [ + "seq", + "hash" + ], + "lineno": 589, + "name": "failedSave" + }, + { + "args": [ + "hash", + "seq" + ], + "lineno": 594, + "name": "checkAccept" + }, + { + "args": [], + "lineno": 634, + "name": "getNeededValidations" + }, + { + "args": [ + "ledger" + ], + "lineno": 639, + "name": "checkAccept" + }, + { + "args": [ + "ledger", + "consensusHash", + "consensus" + ], + "lineno": 726, + "name": "consensusBuilt" + }, + { + "args": [ + "index", + "reason" + ], + "lineno": 803, + "name": "getLedgerHashForHistory" + }, + { + "args": [ + "sl" + ], + "lineno": 813, + "name": "findNewLedgersToPublish" + }, + { + "args": [], + "lineno": 900, + "name": "tryAdvance" + }, + { + "args": [], + "lineno": 917, + "name": "updatePaths" + }, + { + "args": [], + "lineno": 1002, + "name": "newPathRequest" + }, + { + "args": [], + "lineno": 1007, + "name": "isNewPathRequest" + }, + { + "args": [], + "lineno": 1015, + "name": "newOrderBookDB" + }, + { + "args": [ + "name", + "sl" + ], + "lineno": 1023, + "name": "newPFWork" + }, + { + "args": [], + "lineno": 1034, + "name": "peekMutex" + }, + { + "args": [], + "lineno": 1039, + "name": "getCurrentLedger" + }, + { + "args": [], + "lineno": 1044, + "name": "getValidatedLedger" + }, + { + "args": [], + "lineno": 1049, + "name": "getValidatedRules" + }, + { + "args": [], + "lineno": 1059, + "name": "getPublishedLedger" + }, + { + "args": [], + "lineno": 1064, + "name": "getCompleteLedgers" + }, + { + "args": [ + "ledgerIndex" + ], + "lineno": 1069, + "name": "getCloseTimeBySeq" + }, + { + "args": [ + "ledgerHash", + "index" + ], + "lineno": 1075, + "name": "getCloseTimeByHash" + }, + { + "args": [ + "index" + ], + "lineno": 1087, + "name": "getHashBySeq" + }, + { + "args": [ + "index", + "reason" + ], + "lineno": 1095, + "name": "walkHashBySeq" + }, + { + "args": [ + "index", + "referenceLedger", + "reason" + ], + "lineno": 1102, + "name": "walkHashBySeq" + }, + { + "args": [ + "index" + ], + "lineno": 1127, + "name": "getLedgerBySeq" + }, + { + "args": [ + "hash" + ], + "lineno": 1152, + "name": "getLedgerByHash" + }, + { + "args": [ + "minV", + "maxV" + ], + "lineno": 1160, + "name": "setLedgerRangePresent" + }, + { + "args": [], + "lineno": 1165, + "name": "sweep" + }, + { + "args": [], + "lineno": 1170, + "name": "getCacheHitRate" + }, + { + "args": [ + "seq" + ], + "lineno": 1175, + "name": "clearPriorLedgers" + }, + { + "args": [ + "seq" + ], + "lineno": 1181, + "name": "clearLedgerCachePrior" + }, + { + "args": [ + "replay" + ], + "lineno": 1186, + "name": "takeReplay" + }, + { + "args": [], + "lineno": 1191, + "name": "releaseReplay" + }, + { + "args": [ + "missing", + "progress", + "reason", + "sl" + ], + "lineno": 1196, + "name": "fetchForHistory" + }, + { + "args": [ + "sl" + ], + "lineno": 1277, + "name": "doAdvance" + }, + { + "args": [ + "hash", + "data" + ], + "lineno": 1357, + "name": "addFetchPack" + }, + { + "args": [ + "hash" + ], + "lineno": 1362, + "name": "getFetchPack" + }, + { + "args": [ + "progress", + "seq" + ], + "lineno": 1372, + "name": "gotFetchPack" + }, + { + "args": [ + "want", + "have", + "cnt", + "into", + "seq", + "withLeaves" + ], + "lineno": 1387, + "name": "populateFetchPack" + }, + { + "args": [ + "wPeer", + "request", + "haveLedgerHash", + "uptime" + ], + "lineno": 1417, + "name": "makeFetchPack" + }, + { + "args": [], + "lineno": 1497, + "name": "getFetchPackCacheSize" + }, + { + "args": [], + "lineno": 1502, + "name": "minSqlSeq" + }, + { + "args": [ + "ledgerSeq", + "txnIndex" + ], + "lineno": 1507, + "name": "txnIdFromIndex" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 24, + "name": "xrpl" + } + ], + "test_coverage_notes": "The LedgerMaster validation logic is typically tested in integration and unit tests under the 'ledger', 'consensus', and 'app' test suites. Likely test files include LedgerMaster_test.cpp, LedgerHistory_test.cpp, and possibly integration tests in NetworkOPs_test.cpp. However, the specific validation paths (e.g., isCompatible, areCompatible) may not be exhaustively tested for all edge cases, such as rare hash mismatches or sequence number edge conditions. The shouldAcquire logic may be tested in history acquisition tests, but coverage for all branches (especially minimumOnline logic) should be verified. There may be gaps in tests for error handling and for scenarios with unusual ledger gaps or configuration values.", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerMaster.cpp.ai.md b/src/xrpld/app/ledger/detail/LedgerMaster.cpp.ai.md new file mode 100644 index 0000000000..1c6f3f70dd --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerMaster.cpp.ai.md @@ -0,0 +1,106 @@ +# `src/xrpld/app/ledger/detail/LedgerMaster.cpp` + +## Role in the System + +`LedgerMaster` is the single most central bookkeeping object in a running rippled node. It owns the authoritative answers to several concurrent questions: *Which ledger should new transactions enter?* *What is the last ledger we believe the network validated?* *What is the last ledger we have published to clients?* *What history do we hold, and what are we still acquiring?* Everything downstream — consensus, path-finding, the RPC layer, the overlay — defers to `LedgerMaster` for these answers. + +The implementation file is ~2170 lines. At that scale it is better understood as a state machine with five interlocking concerns: ledger state management, validation advancement, ledger publication, history backfill, and fetch-pack serving. Each is described below. + +--- + +## Ledger State Hierarchy + +The class maintains four distinct views of ledger state: + +| Pointer | Meaning | +|---|---| +| `app_.getOpenLedger().current()` | Mutable ledger accepting new transactions | +| `mClosedLedger` | Most recently consensus-closed ledger (not yet validated) | +| `mValidLedger` | Highest-sequence ledger with a quorum of trusted validations | +| `mPubLedger` | Last ledger announced to clients; may lag `mValidLedger` | + +`mClosedLedger` and `mValidLedger` are `LedgerHolder` objects — thin wrappers that enforce immutability (via `isImmutable()` check on set) and provide mutex-guarded reads. The publication sequence number `mPubLedgerSeq` and the validation timestamp `mValidLedgerSign` are `std::atomic` so that `getPublishedLedgerAge()` and `getValidatedLedgerAge()` can be called from any thread without taking `m_mutex`. + +A non-obvious invariant: `mValidLedgerSeq` can advance ahead of `mPubLedgerSeq` whenever the node receives enough validations but cannot yet acquire all intervening ledgers. `findNewLedgersToPublish` bridges that gap; if the gap exceeds `MAX_LEDGER_GAP` (100), the system skips forward directly to the validated ledger rather than trying to reconstruct every intermediate ledger. + +--- + +## Validation Advancement + +The critical path runs: **`consensusBuilt` → `checkAccept` → `setValidLedger` → `tryAdvance`**. + +`consensusBuilt()` is called by the consensus engine when a locally-built ledger is finalized. It hands the ledger to `checkAccept(ledger)`, which applies three filters before promoting the ledger to validated status: + +1. **`canBeCurrent`** enforces Byzantine defense: it rejects any ledger whose sequence precedes the last validated ledger, whose parent close time differs from local clock time by more than five minutes, or whose sequence is unreasonably far ahead of the last validated ledger. The maximum future sequence is computed as `validSeq + 10 + elapsed_seconds / 2` — a rough model that assumes at most one ledger per two seconds, with 10 ledgers of slack for clock drift. + +2. **Quorum check**: the call to `app_.getValidations().getTrustedForLedger()` fetches trusted validations for the ledger hash, then passes them through `app_.getValidators().negativeUNLFilter()` which excludes validators temporarily suppressed by the negative-UNL mechanism. If the count falls below `getNeededValidations()` (the configured quorum, 0 in standalone mode), `checkAccept` returns early. + +3. `setValidLedger()` finalises promotion. It calculates a *median signature time* across validator sign times (not the ledger close time) and stores it in `mValidLedgerSign`. This median is used to detect whether the node's own clock is consistent with the network. It then updates `SHAMapStore`, the amendment table, and `NetworkOPs`. Critically, it checks `app_.getAmendmentTable().hasUnsupportedEnabled()`: if any amendment the current binary does not understand has activated, the node calls `setAmendmentBlocked()`, rendering it inoperative for new transactions. + +When consensus succeeds but the resulting ledger has not yet received a quorum (a situation that can happen during network partitions), `consensusBuilt` also scans all *current* trusted validations for any separately-validated ledger that might have crossed the quorum threshold. The inner `valSeq` class (defined locally) is used as a lightweight accumulator to find the highest-sequence ledger with enough votes without constructing separate collections. + +--- + +## Ledger Publication and the Advance Loop + +`tryAdvance()` is a throttled dispatcher: it sets `mAdvanceWork = true` and, if no advance thread is already running, submits a `jtADVANCE` job. The actual work happens in `doAdvance()`, which is always called with `m_mutex` held (the `std::unique_lock` parameter is a compiler-enforced reminder of this invariant). + +`doAdvance()` runs a `do…while(mAdvanceWork)` loop so that work discovered mid-execution reruns immediately. On each iteration it calls `findNewLedgersToPublish()`: + +- If new ledgers are ready, it calls `setFullLedger` and `app_.getOPs().pubLedger()` for each, then triggers path-finding work via `newPFWork`. +- If the publish and valid sequences are equal (we are caught up), it looks for missing history using `prevMissing(mCompleteLedgers, pubSeq, earliestSeq)` and calls `fetchForHistory` when `shouldAcquire` permits it. `shouldAcquire` respects three throttles: whether the candidate is within the configured `ledger_history_` window, whether it exceeds the SHAMapStore's `minimumOnline` requirement, and whether it might be the current ledger. + +`findNewLedgersToPublish` releases `m_mutex` (via `scope_unlock`) while doing I/O. That requires the function and its caller to be aware that state may change while the lock is dropped; the function therefore re-reads `mValidLedger` at entry and does not rely on stale copies. + +When `LEDGER_REPLAY` mode is enabled, `findNewLedgersToPublish` walks backward from the validated ledger through `mLedgerHistory` to narrow the replay gap, then hands the work to `app_.getLedgerReplayer()`. + +--- + +## History Backfill + +`fetchForHistory()` resolves a missing sequence number by: + +1. Calling `getLedgerHashForHistory()` which looks up the hash via the skip list embedded in the validated ledger's state map, using `mHistLedger` as an alternative reference when the primary validated ledger does not span the target index. +2. Attempting to get the ledger from the in-memory history cache. +3. If not cached, checking whether a prior inbound-ledger acquire for that hash has already permanently failed before dispatching a new `InboundLedgers::acquire`. +4. If the ledger is still unavailable, requesting a *fetch pack* from the best-scoring peer that claims to hold the missing range. + +Once a historical ledger is acquired, `fetchForHistory` calls `setFullLedger` and may spawn a `TryFill` job, which walks backward through the relational database (in batches of 500 sequences) to mark an entire contiguous range in `mCompleteLedgers` without re-fetching each ledger individually. + +--- + +## Fetch Packs + +A fetch pack is a batch of SHAMap tree nodes bundled together so a peer can acquire a historical ledger in fewer round trips. The two sides of the protocol are both here: + +**Requesting** (`getFetchPack`, called from `fetchForHistory`): The node selects the active peer with the highest randomized-but-latency-weighted score that advertises the needed range, then sends a `TMGetObjectByHash` of type `otFETCH_PACK` pointing at the *next* ledger's hash (so the peer can compute which nodes are missing). + +**Serving** (`makeFetchPack`): A peer-serving path that builds a response. It respects several rate-limiting gates: request age > 1 second (stale, discard), local load too high, or validated ledger too old. The core loop serializes the ledger header, then calls the file-static `populateFetchPack()` to walk the SHAMap differences between what the requester already has (`have->stateMap()`) and the ledger being packed (`want->stateMap()`). Transaction maps are packed unconditionally with `nullptr` as the "have" map because remote peers are unlikely to have historical transactions. The loop continues to older ledgers until 512 objects have been added or one second of uptime has elapsed. + +Received fetch pack data is cached in `fetch_packs_`, a `TaggedCache` with a 45-second TTL and 65536 entries. Retrieval validates the blob's hash before returning it, so a corrupted or substituted entry is silently discarded. + +--- + +## Concurrency Design + +Two locks serve distinct purposes: +- `m_mutex` (recursive) guards nearly all mutable state. It is recursive because several public methods call private methods that also lock it. +- `mCompleteLock` (non-recursive) guards only `mCompleteLedgers` (a boost interval set). Using a separate lock prevents `mCompleteLedgers` operations from blocking unrelated state reads. + +Atomic members (`mPubLedgerClose`, `mPubLedgerSeq`, `mValidLedgerSign`, `mValidLedgerSeq`, `mBuildingLedgerSeq`) allow hot-path reads without locking. The single `mGotFetchPackThread` atomic flag prevents more than one `GotFetchPack` job from being queued simultaneously. + +The `scope_unlock` RAII guard appears throughout `doAdvance` and `findNewLedgersToPublish` to release `m_mutex` around expensive operations (I/O, network sends) while ensuring it is re-acquired when the scope ends, preventing long stalls in the job queue. + +--- + +## Fee Mediation and Upgrade Detection + +`checkAccept(ledger)` also serves two secondary roles. First, it collects fee load factors from validations for the ledger and its parent, takes the median, and calls `app_.getFeeTrack().setRemoteFee(fee)` — a mechanism by which the node tracks whether the broader network is under higher fee pressure than local state would indicate. + +Second, every 256 ledgers (`ledger->seq() % 256 == 0`), it inspects the `sfServerVersion` field in validator messages to detect whether 60% or more of xrpld validators are running a newer binary. If so, it emits an upgrade warning to stderr and the error log. This check is throttled to at most once per week by `upgradeWarningPrevTime_`. + +--- + +## Metrics + +The nested `Stats` struct registers two Graphite-style gauges — `LedgerMaster/Validated_Ledger_Age` and `LedgerMaster/Published_Ledger_Age` — through the beast insight collector. These are polled via the `hook` callback connected to `collect_metrics()`, providing continuous observability into how stale the node's ledger views are. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerPersistence.cpp.ai.json b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp.ai.json new file mode 100644 index 0000000000..81e7d8a398 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp.ai.json @@ -0,0 +1,425 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "pendSaveValidated", + "saveValidatedLedger" + ], + "entry_point": "pendSaveValidated", + "purpose": "Initiates the process to persist a validated ledger, either synchronously or via the job queue.", + "validation_points": [ + "pendSaveValidated: registry.getHashRouter().setFlags(ledger->header().hash, HashRouterFlags::SAVED)", + "pendSaveValidated: XRPL_ASSERT(ledger->isImmutable(), ...)", + "pendSaveValidated: registry.getPendingSaves().shouldWork(ledger->header().seq, isSynchronous)", + "saveValidatedLedger: registry.getPendingSaves().startWork(seq)" + ] + }, + { + "call_chain": [ + "saveValidatedLedger" + ], + "entry_point": "saveValidatedLedger", + "purpose": "Actually persists the ledger to the database if not already being saved.", + "validation_points": [ + "saveValidatedLedger: registry.getPendingSaves().startWork(seq)" + ] + }, + { + "call_chain": [ + "loadLedgerHelper" + ], + "entry_point": "loadLedgerHelper", + "purpose": "Constructs a Ledger object from header and other parameters, sets loaded flag.", + "validation_points": [ + "loadLedgerHelper: loaded flag after Ledger constructor" + ] + }, + { + "call_chain": [ + "getLatestLedger", + "loadLedgerHelper" + ], + "entry_point": "getLatestLedger", + "purpose": "Fetches the latest ledger from the database and constructs a Ledger object.", + "validation_points": [ + "loadLedgerHelper: loaded flag after Ledger constructor" + ] + }, + { + "call_chain": [ + "loadByIndex", + "loadLedgerHelper" + ], + "entry_point": "loadByIndex", + "purpose": "Loads a ledger by its index from the database.", + "validation_points": [ + "loadLedgerHelper: loaded flag after Ledger constructor" + ] + }, + { + "call_chain": [ + "loadByHash", + "loadLedgerHelper" + ], + "entry_point": "loadByHash", + "purpose": "Loads a ledger by its hash from the database.", + "validation_points": [ + "loadLedgerHelper: loaded flag after Ledger constructor" + ] + } + ], + "data_flows": [ + { + "field": "ledger->header().seq", + "flow": [ + "LedgerHeader", + "ledger->header().seq", + "pendSaveValidated", + "registry.getPendingSaves().shouldWork / startWork / pending", + "saveValidatedLedger", + "registry.getPendingSaves().finishWork" + ], + "origin": "LedgerHeader (from database or constructed)", + "transformations": [ + "Checked for pending/shouldWork/startWork to prevent duplicate saves" + ], + "validated_at": "pendSaveValidated (shouldWork), saveValidatedLedger (startWork)" + }, + { + "field": "ledger->header().hash", + "flow": [ + "LedgerHeader", + "ledger->header().hash", + "pendSaveValidated", + "registry.getHashRouter().setFlags" + ], + "origin": "LedgerHeader (from database or constructed)", + "transformations": [ + "Flagged as SAVED in HashRouter to prevent duplicate saves" + ], + "validated_at": "pendSaveValidated (setFlags)" + }, + { + "field": "ledger (object)", + "flow": [ + "loadLedgerHelper", + "Ledger constructor", + "loaded flag set", + "used in pendSaveValidated/saveValidatedLedger" + ], + "origin": "Constructed via loadLedgerHelper or passed in", + "transformations": [ + "Constructed, checked for loaded, possibly reset to nullptr" + ], + "validated_at": "loadLedgerHelper (loaded flag), pendSaveValidated (XRPL_ASSERT(ledger->isImmutable()))" + }, + { + "field": "ledger->isImmutable()", + "flow": [ + "Ledger object", + "pendSaveValidated", + "XRPL_ASSERT(ledger->isImmutable())" + ], + "origin": "Ledger object", + "transformations": [ + "Checked for immutability before saving" + ], + "validated_at": "pendSaveValidated" + }, + { + "field": "fees (Ledger fees)", + "flow": [ + "LedgerHeader", + "loadLedgerHelper", + "finishLoadByIndexOrHash", + "ledger->read(keylet::fees())" + ], + "origin": "LedgerHeader or ledger->read(keylet::fees())", + "transformations": [ + "Checked for presence if ledger seq >= XRP_LEDGER_EARLIEST_FEES" + ], + "validated_at": "finishLoadByIndexOrHash" + } + ], + "description": "Implements functions for saving and loading validated ledgers to and from persistent storage, using a service registry abstraction. Handles synchronous and asynchronous saving, and provides helpers for loading ledgers by index or hash.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger->header().seq", + "empty", + "string", + "validation" + ], + "evidence": "registry.getPendingSaves().startWork(seq) at saveValidatedLedger", + "issue_pattern": "Missing empty string validation for ledger->header().seq", + "why_false_positive": "registry.getPendingSaves().startWork(seq) validates ledger->header().seq for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger->header().hash", + "empty", + "string", + "validation" + ], + "evidence": "registry.getHashRouter().setFlags(ledger->header().hash, HashRouterFlags::SAVED) at pendSaveValidated", + "issue_pattern": "Missing empty string validation for ledger->header().hash", + "why_false_positive": "registry.getHashRouter().setFlags(ledger->header().hash, HashRouterFlags::SAVED) validates ledger->header().hash for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(ledger->isImmutable(), ...) at pendSaveValidated", + "issue_pattern": "Missing empty string validation for ledger", + "why_false_positive": "XRPL_ASSERT(ledger->isImmutable(), ...) validates ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger->header().seq", + "empty", + "string", + "validation" + ], + "evidence": "registry.getPendingSaves().shouldWork(ledger->header().seq, isSynchronous) at pendSaveValidated", + "issue_pattern": "Missing empty string validation for ledger->header().seq", + "why_false_positive": "registry.getPendingSaves().shouldWork(ledger->header().seq, isSynchronous) validates ledger->header().seq for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "ledger", + "empty", + "string", + "validation" + ], + "evidence": "loaded flag after Ledger constructor at loadLedgerHelper", + "issue_pattern": "Missing empty string validation for ledger", + "why_false_positive": "loaded flag after Ledger constructor validates ledger for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerPersistence.cpp", + "functions": [ + { + "args": [ + "registry", + "ledger", + "current" + ], + "lineno": 8, + "name": "saveValidatedLedger" + }, + { + "args": [ + "registry", + "ledger", + "isSynchronous", + "isCurrent" + ], + "lineno": 27, + "name": "pendSaveValidated" + }, + { + "args": [ + "info", + "rules", + "fees", + "registry", + "acquire" + ], + "lineno": 62, + "name": "loadLedgerHelper" + }, + { + "args": [ + "ledger", + "j" + ], + "lineno": 78, + "name": "finishLoadByIndexOrHash" + }, + { + "args": [ + "rules", + "fees", + "registry" + ], + "lineno": 91, + "name": "getLatestLedger" + }, + { + "args": [ + "ledgerIndex", + "rules", + "fees", + "registry", + "acquire" + ], + "lineno": 99, + "name": "loadByIndex" + }, + { + "args": [ + "ledgerHash", + "rules", + "fees", + "registry", + "acquire" + ], + "lineno": 113, + "name": "loadByHash" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The functions in LedgerPersistence.cpp are typically tested indirectly via higher-level ledger and persistence tests. Likely test files include LedgerPersistence_test.cpp, Ledger_test.cpp, and integration tests in the ledger or app/ledger test suites. Direct validation of the PendingSaves, HashRouter, and immutability checks may not be fully covered unless there are explicit unit tests for pendSaveValidated and saveValidatedLedger. Edge cases such as double saves, job queue failures, and immutability assertion failures may not be exhaustively tested. Test coverage for error paths (e.g., loaded flag false, database save failures) should be reviewed for completeness.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "None (returns true early, logs 'Save aborted')", + "field": "ledger->header().seq", + "location": "saveValidatedLedger", + "validated_by": "registry.getPendingSaves().startWork(seq)", + "validates": [ + "Checks if work for this ledger sequence has already started or completed" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "None (returns true early, logs 'Double pend save')", + "field": "ledger->header().hash", + "location": "pendSaveValidated", + "validated_by": "registry.getHashRouter().setFlags(ledger->header().hash, HashRouterFlags::SAVED)", + "validates": [ + "Checks if this ledger hash has already been flagged as SAVED" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts process or throws)", + "field": "ledger", + "location": "pendSaveValidated", + "validated_by": "XRPL_ASSERT(ledger->isImmutable(), ...)", + "validates": [ + "Checks that the ledger is immutable before saving" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "None (returns true early, logs 'Pend save with seq in pending saves')", + "field": "ledger->header().seq", + "location": "pendSaveValidated", + "validated_by": "registry.getPendingSaves().shouldWork(ledger->header().seq, isSynchronous)", + "validates": [ + "Checks if work should be started for this ledger sequence (prevents duplicate work)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "None (resets pointer to null if not loaded)", + "field": "ledger", + "location": "loadLedgerHelper", + "validated_by": "loaded flag after Ledger constructor", + "validates": [ + "Checks if ledger was successfully loaded (via loaded flag), resets pointer if not" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerPersistence.cpp.ai.md b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp.ai.md new file mode 100644 index 0000000000..7b675a1a66 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerPersistence.cpp.ai.md @@ -0,0 +1,36 @@ +# `LedgerPersistence.cpp` — Validated Ledger Save and Load + +This file implements the persistence bridge between the in-memory `Ledger` object and the node's relational database. It covers two symmetrical concerns: persisting a newly-validated ledger to durable storage, and reconstructing a `Ledger` from stored header records when the node needs historical data. All services are reached through the `ServiceRegistry` abstract interface rather than the concrete `Application` class, which keeps the code testable and decoupled from lifecycle management. + +## Save Path: `pendSaveValidated` → `saveValidatedLedger` + +The public entry point `pendSaveValidated()` orchestrates the save of a fully-validated ledger. Because ledger validation can produce the same ledger from multiple code paths (consensus, ledger acquisition, replay), it uses two independent deduplication layers before touching the database. + +**Layer 1 — hash-level dedup via `HashRouter`**: The first gate calls `registry.getHashRouter().setFlags(ledger->header().hash, HashRouterFlags::SAVED)`. `HashRouter` is an aged unordered map used primarily for P2P message routing; marking a hash as `SAVED` here acts as a lightweight, time-bounded "have I recently handled this exact ledger?" check. If the flag was already set, the function short-circuits with a "Double pend save" log. The nuance is that if the caller requested a synchronous save *and* a save is actually still in flight, it skips this early return and continues — the need for guaranteed completion overrides the dedup. + +**Layer 2 — sequence-level concurrency via `PendingSaves`**: After the hash check, `PendingSaves::shouldWork(seq, isSynchronous)` is called. This mutex-protected map tracks every sequence currently being persisted. If no entry exists yet, the method inserts the sequence and returns `true` (proceed). If an entry exists and the caller is asynchronous, it returns `false` (already dispatched, nothing to do). Crucially, if the entry exists but the save is already in progress (`it->second == true`) and the caller is synchronous, `shouldWork` blocks on a `std::condition_variable` until `finishWork` wakes it — this is how a synchronous caller waits for an in-flight background save to complete before returning. + +Once both gates pass, `pendSaveValidated` asserts that the ledger is immutable (`XRPL_ASSERT(ledger->isImmutable())`). A mutable ledger being persisted would be a serious protocol error, so this is a hard assertion rather than a soft error return. + +The function then prefers to enqueue the actual I/O via the `JobQueue`. Current-ledger saves use `jtPUBLEDGER` and historical backfills use `jtPUBOLDLEDGER` — different job types allow the scheduler to prioritize live consensus work over historical filling. If the `JobQueue` rejects the job (e.g. the queue is at capacity or the system is shutting down), execution falls through to a direct synchronous call. + +**Inside `saveValidatedLedger`**: This static function performs the actual write. It calls `PendingSaves::startWork(seq)` as a final concurrency guard at the point of actual database access: `startWork` atomically checks if work is underway and marks it so, returning `false` if another thread already claimed it. This is distinct from `shouldWork`: `shouldWork` manages the *decision* to proceed, while `startWork` manages the *claim* to do the work. After `db.saveValidatedLedger()` completes, `PendingSaves::finishWork(seq)` removes the entry and broadcasts on the condition variable, waking any threads blocked in `shouldWork`. + +## Load Path: `loadByIndex`, `loadByHash`, `getLatestLedger` + +The three public load functions share a common two-step pattern: + +1. Query `RelationalDatabase` for a `LedgerHeader` record using the requested key (sequence number, hash, or newest available). +2. Delegate to `loadLedgerHelper`, then seal the result via `finishLoadByIndexOrHash`. + +`loadLedgerHelper` constructs a `Ledger` object by passing the `LedgerHeader` along with `rules`, `fees`, a `NodeFamily` (for the underlying SHAMap storage), and a journal. The `Ledger` constructor signals success or failure through a by-reference `bool loaded` flag — if the object could not be fully constructed from local node storage, `loaded` is left false and the helper immediately resets the `shared_ptr` to null. The `acquire` parameter controls whether the `Ledger` constructor should attempt to fetch missing SHAMap nodes from peers. + +`finishLoadByIndexOrHash` finalises a successfully loaded ledger in three ways. It calls `setImmutable()` to prevent further modification (consistent with the save-path assertion), then `setFull()` to mark the object as completely loaded. Between these it also asserts a protocol invariant: any ledger at or beyond `XRP_LEDGER_EARLIEST_FEES` must contain a fee-settings object (`keylet::fees()`). This guards against corrupt or truncated database records slipping into the in-memory ledger cache as apparently-valid objects. + +`loadByHash` adds one more assertion after loading: the reconstructed ledger's hash must match the requested hash. This catches any database inconsistency where a stored record is indexed under the wrong hash. + +## Design Observations + +The separation of `shouldWork` and `startWork` in `PendingSaves` reflects a deliberate two-phase concurrency model: the outer function (`pendSaveValidated`) decides *whether* to start, and the inner function (`saveValidatedLedger`) atomically claims the work slot. This prevents a TOCTOU race where two asynchronous jobs both pass `shouldWork` but only one should do the actual write. + +The `ServiceRegistry` abstraction is used consistently — no raw `Application&` references appear here. This is part of an ongoing migration in the rippled codebase away from passing `Application` everywhere; `LedgerPersistence.cpp` is already fully migrated to the service-locator model, which makes the persistence logic independently mockable and testable. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplay.cpp.ai.json b/src/xrpld/app/ledger/detail/LedgerReplay.cpp.ai.json new file mode 100644 index 0000000000..b28e5b857b --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplay.cpp.ai.json @@ -0,0 +1,141 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr parent, std::shared_ptr replay", + "std::shared_ptr parent, std::shared_ptr replay, std::map>&& orderedTxns" + ], + "lineno": 6, + "name": "LedgerReplay" + } + ], + "code_paths": [ + { + "call_chain": [ + "LedgerReplay::LedgerReplay", + "replay_->txMap()", + "replay_->txRead(item.key())" + ], + "entry_point": "LedgerReplay::LedgerReplay (constructor)", + "purpose": "Constructs a LedgerReplay object, extracting and ordering transactions from a replay ledger.", + "validation_points": [ + "No explicit validation in this code; assumes replay_ is a valid Ledger and txRead returns valid data." + ] + } + ], + "data_flows": [ + { + "field": "parent_", + "flow": [ + "Constructor argument", + "Assigned to member variable parent_" + ], + "origin": "Constructor argument (std::shared_ptr parent)", + "transformations": [ + "std::move into member" + ], + "validated_at": "No explicit validation in this code" + }, + { + "field": "replay_", + "flow": [ + "Constructor argument", + "Assigned to member variable replay_" + ], + "origin": "Constructor argument (std::shared_ptr replay)", + "transformations": [ + "std::move into member" + ], + "validated_at": "No explicit validation in this code" + }, + { + "field": "orderedTxns_", + "flow": [ + "replay_->txMap() provides keys", + "For each key, replay_->txRead(key) returns (tx, meta)", + "meta[sfTransactionIndex] is used as the map key", + "orderedTxns_ is populated with (txIndex, tx)" + ], + "origin": "Built from replay_->txMap() and replay_->txRead()", + "transformations": [ + "Extraction of transaction index from metadata", + "Move of transaction pointer into orderedTxns_" + ], + "validated_at": "No explicit validation; assumes txRead returns valid (tx, meta) pairs and meta contains sfTransactionIndex" + } + ], + "description": "Implements the LedgerReplay class, which manages replaying transactions from a ledger for the XRPL system.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerReplay.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is a low-level utility for replaying ledgers and ordering transactions. There is no explicit validation or error handling in this snippet. Test coverage would likely be indirect, via higher-level tests that exercise ledger replay or transaction ordering (e.g., integration tests for ledger replay, transaction application, or consensus replay). Look for tests in files like LedgerReplay_test.cpp, Ledger_test.cpp, or integration tests involving ledger replay. Gaps: No unit tests for invalid or malformed input (e.g., missing sfTransactionIndex, null pointers, or invalid txMap/txRead behavior) are evident from this code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None", + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplay.cpp.ai.md b/src/xrpld/app/ledger/detail/LedgerReplay.cpp.ai.md new file mode 100644 index 0000000000..837c6a47d1 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplay.cpp.ai.md @@ -0,0 +1,25 @@ +# `LedgerReplay.cpp` — Transaction Ordering for Ledger Reconstruction + +`LedgerReplay.cpp` provides the constructor implementations for the `LedgerReplay` value object — a lightweight data bundle that pairs two ledger snapshots with their transactions sorted into deterministic application order. It is the handoff type between the network acquisition layer and the `buildLedger()` function that re-executes a historical ledger from scratch. + +## Why This Class Exists + +When an XRPL node is missing a historical ledger, it has two ways to reconstruct it: download the full SHAMap state (expensive), or replay the transactions against the known parent ledger (efficient). The replay path requires two things: the parent ledger to start from, and the exact sequence of transactions to apply. `LedgerReplay` holds both, along with a reference to the target ledger whose metadata drives the reconstruction parameters (close time, close flags, close resolution). + +`buildLedger(LedgerReplay const&, ...)` in `BuildLedger.cpp` consumes the object directly — it iterates `orderedTxns()` in map order and calls `applyTransaction()` for each, then finalises the resulting ledger using the replay ledger's own header metadata. + +## The Two Constructors + +The class offers two construction paths that differ in how `orderedTxns_` is populated. + +The **primary constructor** takes only `parent` and `replay` ledgers and builds the ordered transaction map itself. It walks `replay_->txMap()` to enumerate all transaction keys, then for each key calls `replay_->txRead(item.key())`, which returns a `tx_type` — a `std::pair, std::shared_ptr>`. The second element is the transaction metadata `STObject`, which for a closed ledger carries an `sfTransactionIndex` field recording the position at which the transaction was applied. The constructor extracts that index and uses it as the map key, inserting the transaction (moved, not copied, to avoid a shared-pointer refcount bump) into `orderedTxns_`. Because `std::map` orders by key, the resulting container is automatically sorted by application sequence — exactly what `buildLedger()` needs to deterministically reproduce the ledger. + +The **secondary constructor** accepts a pre-built `std::map>&&` and moves it directly into `orderedTxns_`. This path is used when `LedgerDeltaAcquire` has already assembled the ordered transaction set from peer-fetched data; constructing it inline avoids re-iterating the txMap a second time. + +## Design Observations + +There is no validation in either constructor — no null checks on `parent_` or `replay_`, no assertion that the metadata contains `sfTransactionIndex`. This is intentional: callers (specifically `LedgerDeltaAcquire` and `LedgerReplayer`) are responsible for verifying that the ledger data is structurally sound before creating a `LedgerReplay`. The class trusts its inputs and acts as a pure data carrier. + +`LedgerReplay` inherits from `CountedObject`, which adds lightweight live-instance tracking used for diagnostic reporting — a common pattern across XRPL protocol objects, adding zero per-instance overhead beyond the counter. + +All three stored fields are `shared_ptr` types, so the object can be cheaply copied or passed through multiple subsystem layers without duplicating ledger data. The `orderedTxns_` map holding `shared_ptr` nodes similarly allows the same transaction objects to be referenced from both the replay ledger's own txMap and the replay context without duplication. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp.ai.json b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp.ai.json new file mode 100644 index 0000000000..e0b757787a --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp.ai.json @@ -0,0 +1,463 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app", + "LedgerReplayer& replayer" + ], + "lineno": 9, + "name": "LedgerReplayMsgHandler" + } + ], + "code_paths": [ + { + "call_chain": [ + "LedgerReplayMsgHandler::processProofPathRequest" + ], + "entry_point": "LedgerReplayMsgHandler::processProofPathRequest", + "purpose": "Handles incoming proof path requests, validates input, fetches ledger, computes proof path, and constructs response.", + "validation_points": [ + "Immediately at function start: checks for presence and size of key, ledgerhash, type, and type validity" + ] + }, + { + "call_chain": [ + "LedgerReplayMsgHandler::processProofPathResponse" + ], + "entry_point": "LedgerReplayMsgHandler::processProofPathResponse", + "purpose": "Handles incoming proof path responses, validates reply, deserializes header, checks hash, and verifies key.", + "validation_points": [ + "Immediately at function start: checks for error, presence of key, ledgerhash, type, ledgerheader, and non-empty path", + "Checks type is lmACCOUNT_STATE", + "Checks ledger hash matches calculated hash", + "Checks key is skip list key" + ] + } + ], + "data_flows": [ + { + "field": "key", + "flow": [ + "packet.key() (input)", + "validated for presence and size", + "assigned to reply.set_key()", + "converted to uint256 for ledger lookup and path computation" + ], + "origin": "protocol::TMProofPathRequest (network message)", + "transformations": [ + "Checked for presence (has_key)", + "Checked for correct size", + "Converted to uint256" + ], + "validated_at": "processProofPathRequest: if (!packet.has_key() || packet.key().size() != uint256::size())" + }, + { + "field": "ledgerhash", + "flow": [ + "packet.ledgerhash() (input)", + "validated for presence and size", + "assigned to reply.set_ledgerhash()", + "converted to uint256 for ledger lookup" + ], + "origin": "protocol::TMProofPathRequest (network message)", + "transformations": [ + "Checked for presence (has_ledgerhash)", + "Checked for correct size", + "Converted to uint256" + ], + "validated_at": "processProofPathRequest: if (!packet.has_ledgerhash() || packet.ledgerhash().size() != uint256::size())" + }, + { + "field": "type", + "flow": [ + "packet.type() (input)", + "validated for presence and protocol::TMLedgerMapType_IsValid", + "assigned to reply.set_type()", + "used in switch to select map" + ], + "origin": "protocol::TMProofPathRequest (network message)", + "transformations": [ + "Checked for presence (has_type)", + "Checked for validity (TMLedgerMapType_IsValid)" + ], + "validated_at": "processProofPathRequest: if (!packet.has_type() || !protocol::TMLedgerMapType_IsValid(packet.type()))" + }, + { + "field": "ledgerheader", + "flow": [ + "serialized with addRaw()", + "set in reply.set_ledgerheader()", + "in response, deserialized with deserializeHeader()" + ], + "origin": "ledger->header() (from fetched ledger)", + "transformations": [ + "Serialized to bytes", + "Deserialized back to LedgerHeader" + ], + "validated_at": "processProofPathResponse: checked for presence (has_ledgerheader)" + }, + { + "field": "path", + "flow": [ + "computed based on type and key", + "added to reply via reply.add_path()", + "in response, checked for non-empty (path_size() > 0)" + ], + "origin": "ledger->stateMap().getProofPath(key) or ledger->txMap().getProofPath(key)", + "transformations": [ + "Vector of Blobs", + "Serialized into repeated field" + ], + "validated_at": "processProofPathResponse: checked for non-empty (path_size() == 0)" + } + ], + "description": "Implements the LedgerReplayMsgHandler class, which processes and handles network messages related to ledger replay, proof path requests/responses, and replay delta requests/responses in the XRPL ledger synchronization and replay system.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "key", + "empty", + "string", + "validation" + ], + "evidence": "packet.has_key() at LedgerReplayMsgHandler::processProofPathRequest", + "issue_pattern": "Missing empty string validation for key", + "why_false_positive": "packet.has_key() validates key for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledgerhash", + "empty", + "string", + "validation" + ], + "evidence": "packet.has_ledgerhash() at LedgerReplayMsgHandler::processProofPathRequest", + "issue_pattern": "Missing empty string validation for ledgerhash", + "why_false_positive": "packet.has_ledgerhash() validates ledgerhash for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type", + "empty", + "string", + "validation" + ], + "evidence": "packet.has_type() at LedgerReplayMsgHandler::processProofPathRequest", + "issue_pattern": "Missing empty string validation for type", + "why_false_positive": "packet.has_type() validates type for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledgerhash", + "empty", + "string", + "validation" + ], + "evidence": "packet.ledgerhash().size() != uint256::size() at LedgerReplayMsgHandler::processProofPathRequest", + "issue_pattern": "Missing empty string validation for ledgerhash", + "why_false_positive": "packet.ledgerhash().size() != uint256::size() validates ledgerhash for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "key", + "empty", + "string", + "validation" + ], + "evidence": "packet.key().size() != uint256::size() at LedgerReplayMsgHandler::processProofPathRequest", + "issue_pattern": "Missing empty string validation for key", + "why_false_positive": "packet.key().size() != uint256::size() validates key for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type", + "empty", + "string", + "validation" + ], + "evidence": "protocol::TMLedgerMapType_IsValid(packet.type()) at LedgerReplayMsgHandler::processProofPathRequest", + "issue_pattern": "Missing empty string validation for type", + "why_false_positive": "protocol::TMLedgerMapType_IsValid(packet.type()) validates type for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledgerhash", + "empty", + "string", + "validation" + ], + "evidence": "app_.getLedgerMaster().getLedgerByHash(ledgerHash) at LedgerReplayMsgHandler::processProofPathRequest", + "issue_pattern": "Missing empty string validation for ledgerhash", + "why_false_positive": "app_.getLedgerMaster().getLedgerByHash(ledgerHash) validates ledgerhash for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "key (in ledger map)", + "empty", + "string", + "validation" + ], + "evidence": "ledger->stateMap().getProofPath(key) / ledger->txMap().getProofPath(key) at LedgerReplayMsgHandler::processProofPathRequest", + "issue_pattern": "Missing empty string validation for key (in ledger map)", + "why_false_positive": "ledger->stateMap().getProofPath(key) / ledger->txMap().getProofPath(key) validates key (in ledger map) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp", + "functions": [ + { + "args": [ + "Application& app", + "LedgerReplayer& replayer" + ], + "lineno": 10, + "name": "LedgerReplayMsgHandler::LedgerReplayMsgHandler" + }, + { + "args": [ + "std::shared_ptr const& msg" + ], + "lineno": 15, + "name": "LedgerReplayMsgHandler::processProofPathRequest" + }, + { + "args": [ + "std::shared_ptr const& msg" + ], + "lineno": 61, + "name": "LedgerReplayMsgHandler::processProofPathResponse" + }, + { + "args": [ + "std::shared_ptr const& msg" + ], + "lineno": 104, + "name": "LedgerReplayMsgHandler::processReplayDeltaRequest" + }, + { + "args": [ + "std::shared_ptr const& msg" + ], + "lineno": 137, + "name": "LedgerReplayMsgHandler::processReplayDeltaResponse" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Fields validated before use", + "error_behavior": "Throws exception if invalid", + "false_positive_risk": "Missing validation check", + "template_type": "STObject", + "type": "template_constructor", + "validates": "Input validated on construction" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested by integration or unit tests that exercise ledger replay and proof path request/response flows. Look for test files such as LedgerReplayMsgHandler_test.cpp, LedgerReplayer_test.cpp, or broader ledger replay/consensus tests. However, the specific validation error paths (e.g., malformed requests, invalid sizes, invalid types) may not be exhaustively tested unless there are explicit negative tests for malformed protocol messages. Gaps may exist in testing all error branches, especially for malformed or malicious input.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Manual validation, protocol buffer field presence, enum validation via protocol::TMLedgerMapType_IsValid", + "validation_layer": "business_logic (inside handler function, not at entry point or middleware)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "reply.set_error(protocol::TMReplyError::reBAD_REQUEST)", + "field": "key", + "location": "LedgerReplayMsgHandler::processProofPathRequest", + "validated_by": "packet.has_key()", + "validates": [ + "Checks if 'key' field is present in the request" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "reply.set_error(protocol::TMReplyError::reBAD_REQUEST)", + "field": "ledgerhash", + "location": "LedgerReplayMsgHandler::processProofPathRequest", + "validated_by": "packet.has_ledgerhash()", + "validates": [ + "Checks if 'ledgerhash' field is present in the request" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "reply.set_error(protocol::TMReplyError::reBAD_REQUEST)", + "field": "type", + "location": "LedgerReplayMsgHandler::processProofPathRequest", + "validated_by": "packet.has_type()", + "validates": [ + "Checks if 'type' field is present in the request" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "reply.set_error(protocol::TMReplyError::reBAD_REQUEST)", + "field": "ledgerhash", + "location": "LedgerReplayMsgHandler::processProofPathRequest", + "validated_by": "packet.ledgerhash().size() != uint256::size()", + "validates": [ + "Checks if 'ledgerhash' is exactly 32 bytes (uint256 size)" + ], + "validation_type": "format|length" + }, + { + "confidence": 1.0, + "error_thrown": "reply.set_error(protocol::TMReplyError::reBAD_REQUEST)", + "field": "key", + "location": "LedgerReplayMsgHandler::processProofPathRequest", + "validated_by": "packet.key().size() != uint256::size()", + "validates": [ + "Checks if 'key' is exactly 32 bytes (uint256 size)" + ], + "validation_type": "format|length" + }, + { + "confidence": 1.0, + "error_thrown": "reply.set_error(protocol::TMReplyError::reBAD_REQUEST)", + "field": "type", + "location": "LedgerReplayMsgHandler::processProofPathRequest", + "validated_by": "protocol::TMLedgerMapType_IsValid(packet.type())", + "validates": [ + "Checks if 'type' is a valid enum value for TMLedgerMapType" + ], + "validation_type": "enum|range" + }, + { + "confidence": 1.0, + "error_thrown": "reply.set_error(protocol::TMReplyError::reNO_LEDGER)", + "field": "ledgerhash", + "location": "LedgerReplayMsgHandler::processProofPathRequest", + "validated_by": "app_.getLedgerMaster().getLedgerByHash(ledgerHash)", + "validates": [ + "Checks if ledger with given hash exists in the system" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "reply.set_error(protocol::TMReplyError::reNO_NODE)", + "field": "key (in ledger map)", + "location": "LedgerReplayMsgHandler::processProofPathRequest", + "validated_by": "ledger->stateMap().getProofPath(key) / ledger->txMap().getProofPath(key)", + "validates": [ + "Checks if the node with the given key exists in the ledger map" + ], + "validation_type": "business_logic|existence" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp.ai.md b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp.ai.md new file mode 100644 index 0000000000..459e4025c1 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.cpp.ai.md @@ -0,0 +1,31 @@ +# LedgerReplayMsgHandler.cpp + +## Role in the System + +`LedgerReplayMsgHandler` is the network protocol boundary for the ledger replay subsystem. It translates raw Protobuf messages into verified, deserialized data structures before any replay logic runs. The class operates in two symmetric roles simultaneously: as a **server** responding to peers requesting data from this node, and as a **client** consuming responses that arrive from peers this node queried. Its central responsibility is ensuring that nothing untrusted crosses into `LedgerReplayer` — all cryptographic verification happens here. + +Ledger replay is how a node catches up to the network without downloading full ledger state: it acquires a ledger's header and its transaction set (the "delta"), then re-applies the transactions locally to reconstruct the ledger from a known ancestor. To know *which* ledgers to acquire, a node first fetches the skip list — a special ledger entry that records the hashes of older ledgers at exponentially spaced intervals. Both data types have dedicated P2P messages with corresponding request/response pairs. + +## Request Handlers: Serving Peers + +`processProofPathRequest()` answers `TMProofPathRequest` messages from peers that want a cryptographic proof that a particular key exists in a ledger's `stateMap` or `txMap`. The handler validates all required fields up front — presence, byte length (both `key` and `ledgerhash` must be exactly `uint256::size()` = 32 bytes), and enum validity of the `type` field — returning a `reBAD_REQUEST` error response immediately if anything is wrong. It then looks up the ledger locally; if absent, the response carries `reNO_LEDGER`. On success, it calls `getProofPath(key)` on the appropriate SHAMap, serializes the ledger header with `addRaw()`, and packs each proof node as a repeated field in the response. If the key isn't in the map, `reNO_NODE` is returned. + +`processReplayDeltaRequest()` answers `TMReplayDeltaRequest` by packaging a ledger's full transaction set. The key correctness guard here is `ledger->isImmutable()` — the handler refuses to serve a ledger that isn't fully finalized. All leaf nodes of the ledger's `txMap` are visited via `visitLeaves()` and appended verbatim as repeated `transaction` fields alongside the serialized header. + +Both request handlers return the response object by value; the caller is responsible for transmitting it to the requesting peer. Error conditions are encoded inside the returned message rather than thrown, which keeps the caller's dispatch loop simple. + +## Response Handlers: Consuming Peer Data + +`processProofPathResponse()` handles incoming `TMProofPathResponse` messages. Structural checks come first: required fields must be present, the path must be non-empty, and currently only `lmACCOUNT_STATE` responses are accepted (transaction-map proofs are served but not yet consumed on the receiving side). The handler then deserializes the embedded ledger header and recomputes the ledger hash using `calculateLedgerHash(info)`, comparing it against the `ledgerhash` field in the message — a critical defence against a peer serving a tampered header. The proof path is then passed to `SHAMap::verifyProofPath()` which validates the Merkle inclusion proof against `info.accountHash`. Only if this cryptographic check passes does the code proceed to deserialize the leaf node. An additional content check enforces that only the skip list key (`keylet::skip().key`) is accepted, reflecting the current scoped implementation. The verified `SHAMapItem` is then handed to `replayer_.gotSkipList()`. + +`processReplayDeltaResponse()` is the most complex handler. After header deserialization and hash verification, it reconstructs the transaction set from the serialized blobs. Each blob encodes a `TxShaMapItem` — a VL-prefixed pair of the raw transaction followed by its metadata. The handler reads both parts via `SerialIter` and `getVLDataLength()`, deserializes an `STTx` and an `STObject` with `sfMetadata`, and orders the transactions by their `sfTransactionIndex` metadata field into a `std::map>`. Simultaneously, it re-adds each item to a fresh local `SHAMap` to reconstruct the transaction map. The final verification compares `txMap.getHash().as_uint256()` against `info.txHash` from the validated header. The entire deserialization is wrapped in a `try/catch` for `std::exception` since `STTx` and `STObject` constructors throw on malformed input. Only after the hash check passes does `replayer_.gotReplayDelta()` receive the data. + +## Design Decisions + +The asymmetry between request and response return types is intentional. Request handlers return a response message (value type) because the call site sends it directly back over the wire. Response handlers return `bool` because data delivery goes via `LedgerReplayer` — the caller only needs to know whether the message was valid enough to process. + +The decision to verify everything before calling into `LedgerReplayer` establishes a clean trust boundary. `gotSkipList()` and `gotReplayDelta()` can be documented as accepting only pre-verified data, simplifying the replayer's logic considerably. This mirrors the pattern used elsewhere in XRPL's inbound acquisition subsystems, where the P2P layer is responsible for proof verification and the acquisition layer only handles state machine transitions. + +The current partial implementation — proof paths can be served for both state and transaction maps, but only state-map (skip list) responses are consumed — is acknowledged explicitly in a log message ("we only support the state ShaMap for now"). This is a safe asymmetry: unimplemented paths fall through to `false` returns rather than silently dropping data or asserting. + +The `processReplayDeltaResponse` avoids any trust in the ordering of transactions as sent by the peer. By using `sfTransactionIndex` from the transaction metadata, it enforces canonical execution order regardless of the peer's wire order. Rebuilding the full `SHAMap` locally and comparing its root hash to the header's `txHash` ensures neither a reordering attack nor a transaction substitution can succeed. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h.ai.json b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h.ai.json new file mode 100644 index 0000000000..bad194a039 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h.ai.json @@ -0,0 +1,65 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app", + "LedgerReplayer& replayer" + ], + "lineno": 8, + "name": "LedgerReplayMsgHandler" + } + ], + "description": "Defines the LedgerReplayMsgHandler class, which handles processing of ledger replay and proof path protocol messages in the XRPL application.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h", + "functions": [ + { + "args": [ + "Application& app", + "LedgerReplayer& replayer" + ], + "lineno": 10, + "name": "LedgerReplayMsgHandler" + }, + { + "args": [], + "lineno": 11, + "name": "~LedgerReplayMsgHandler" + }, + { + "args": [ + "std::shared_ptr const& msg" + ], + "lineno": 17, + "name": "processProofPathRequest" + }, + { + "args": [ + "std::shared_ptr const& msg" + ], + "lineno": 23, + "name": "processProofPathResponse" + }, + { + "args": [ + "std::shared_ptr const& msg" + ], + "lineno": 30, + "name": "processReplayDeltaRequest" + }, + { + "args": [ + "std::shared_ptr const& msg" + ], + "lineno": 36, + "name": "processReplayDeltaResponse" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h.ai.md b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h.ai.md new file mode 100644 index 0000000000..9e0f3a48b6 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplayMsgHandler.h.ai.md @@ -0,0 +1,29 @@ +# `LedgerReplayMsgHandler` + +`LedgerReplayMsgHandler` is the message-processing bridge between individual peer connections and the central `LedgerReplayer` subsystem. It handles exactly four protocol message types that drive the ledger replay feature: `TMProofPathRequest`, `TMProofPathResponse`, `TMReplayDeltaRequest`, and `TMReplayDeltaResponse`. One instance lives as a direct member of `PeerImp`, meaning every active peer connection owns its own handler that shares a reference to the single application-wide `LedgerReplayer`. + +## Role in the Replay Pipeline + +Ledger replay allows a node to reconstruct historical ledgers by fetching the minimum necessary data from peers: a cryptographic proof that a ledger's skip list entry is authentic (via a SHAMap proof path), and the set of transactions that were applied to produce the target ledger (the "delta"). `LedgerReplayMsgHandler` straddles two roles simultaneously — it acts as a server when this node already possesses a ledger a peer wants, and as a client-side verifier when this node receives data it requested from a peer. + +## Request Handling (Server Side) + +`processProofPathRequest()` and `processReplayDeltaRequest()` are called when a remote peer wants data from the local node. Both follow the same defensive pattern: validate the inbound protobuf fields first (checking field presence, `uint256`-sized byte strings, valid enum values), set `reBAD_REQUEST` on the response proto and return immediately if anything is off. If the requested ledger cannot be found via `LedgerMaster::getLedgerByHash()`, the response carries `reNO_LEDGER`. A further `reNO_NODE` error is possible if the requested SHAMap key doesn't exist in the ledger's state map. + +For proof path requests, the handler dispatches on the `TMLedgerMapType` enum to call `getProofPath()` on either the account state map or the transaction map, then serializes the ledger header and the raw proof path bytes into the response. For replay delta requests, it iterates over all transaction leaf nodes via `txMap().visitLeaves()` and packs each raw item into the response's `transaction` repeated field. + +The return type for both request handlers is the response protobuf by value. The caller in `PeerImp::onMessage` then inspects `has_error()` and decides whether to send the response or charge the requesting peer a resource fee. This design keeps `LedgerReplayMsgHandler` free of networking concerns — it only validates and serializes. + +Notably, `PeerImp` offloads request handling to the job queue (`jtREPLAY_REQ`) rather than processing inline on the network strand. This prevents a slow ledger lookup from blocking message dispatch for the peer. Response handling, by contrast, is processed directly in the network message path. + +## Response Handling (Client Side) + +`processProofPathResponse()` and `processReplayDeltaResponse()` are called when a previously-requested response arrives. Both return `false` on any validation failure, which `PeerImp` translates into a `feeInvalidData` resource charge against the peer — a lightweight form of abuse prevention. + +Every response goes through cryptographic verification before touching the `LedgerReplayer`. For proof path responses, the handler deserializes the ledger header, recomputes its hash via `calculateLedgerHash()`, and compares against the hash the peer echoed back. It then calls `SHAMap::verifyProofPath()` using `info.accountHash` as the root to confirm the Merkle path is internally consistent. At the time of writing, the response handler explicitly limits itself to `lmACCOUNT_STATE` and to the skip list key (`keylet::skip()`), with a comment indicating transaction-map proof support is deferred. Only after verification does it deserialize the leaf node and call `replayer_.gotSkipList()`. + +For replay delta responses, the same header hash check applies. The handler then reconstructs a local `SHAMap` of type `TRANSACTION` by deserializing each transaction-metadata item from the response. Each encoded item is a VL-prefixed pair: the raw transaction bytes followed by the metadata. The handler extracts `sfTransactionIndex` from the metadata to insert transactions into an `orderedTxns` map by their execution order, and inserts the paired SHAMap item for hash verification. Only when the rebuilt `txMap.getHash()` matches `info.txHash` from the deserialized header — proving the peer sent a complete and unmodified transaction set — does the handler invoke `replayer_.gotReplayDelta()`. All deserialization is wrapped in a broad `std::exception` catch that returns `false` on any malformed input. + +## Design Choices + +The symmetric split between request and response handlers within one class is intentional: both sides of the same protocol message exchange belong together, avoiding scatter across unrelated files while keeping `PeerImp` free of SHAMap and replay internals. Returning the response by value (rather than passing a mutable reference or a callback) makes the request path straightforward to test and reason about, with the networking concern cleanly owned by the caller. The conservative cryptographic checking on every response — even though the data ultimately originates from a peer that the node chose to contact — reflects a defense-in-depth posture consistent throughout the XRPL codebase. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp.ai.json b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp.ai.json new file mode 100644 index 0000000000..d90faf49aa --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp.ai.json @@ -0,0 +1,441 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LedgerReplayTask::TaskParameter::TaskParameter" + ], + "entry_point": "LedgerReplayTask::TaskParameter::TaskParameter", + "purpose": "Constructs a TaskParameter object with finishLedgerHash and totalNumLedgers, validates inputs.", + "validation_points": [ + "XRPL_ASSERT on finishLedgerHash.isNonZero() && totalNumLedgers > 0" + ] + }, + { + "call_chain": [ + "LedgerReplayTask::TaskParameter::update" + ], + "entry_point": "LedgerReplayTask::TaskParameter::update", + "purpose": "Updates TaskParameter with new hash, sequence, and skip list; validates hash, skip list size, and full_ flag.", + "validation_points": [ + "if (finishHash_ != hash || sList.size() + 1 < totalLedgers_ || full_)", + "XRPL_ASSERT on startHash_.isNonZero()" + ] + }, + { + "call_chain": [ + "LedgerReplayTask::init", + "skipListAcquirer_->addDataCallback", + "LedgerReplayTask::updateSkipList", + "LedgerReplayTask::parameter_.update" + ], + "entry_point": "LedgerReplayTask::init", + "purpose": "Initializes the replay task, sets up callback for skip list data, triggers update and validation.", + "validation_points": [ + "LedgerReplayTask::TaskParameter::update (see above)" + ] + }, + { + "call_chain": [ + "LedgerReplayTask::trigger" + ], + "entry_point": "LedgerReplayTask::trigger", + "purpose": "Triggers the replay task if parameter_.full_ is true; relies on prior validation.", + "validation_points": [ + "if (!parameter_.full_) return;" + ] + } + ], + "data_flows": [ + { + "field": "finishLedgerHash", + "flow": [ + "LedgerReplayTask::TaskParameter::TaskParameter (input)", + "TaskParameter::finishHash_ (member assignment)", + "TaskParameter::update (compared to hash argument)", + "Used in canMergeInto, update, and elsewhere" + ], + "origin": "Constructor argument to TaskParameter", + "transformations": [ + "Assigned directly; compared for equality" + ], + "validated_at": "LedgerReplayTask::TaskParameter::TaskParameter (XRPL_ASSERT)" + }, + { + "field": "totalNumLedgers", + "flow": [ + "LedgerReplayTask::TaskParameter::TaskParameter (input)", + "TaskParameter::totalLedgers_ (member assignment)", + "TaskParameter::update (compared to sList.size() + 1)", + "Used in canMergeInto, update, and elsewhere" + ], + "origin": "Constructor argument to TaskParameter", + "transformations": [ + "Assigned directly; compared for size" + ], + "validated_at": "LedgerReplayTask::TaskParameter::TaskParameter (XRPL_ASSERT)" + }, + { + "field": "hash (update argument)", + "flow": [ + "skipListAcquirer_->addDataCallback (callback arg)", + "LedgerReplayTask::updateSkipList", + "TaskParameter::update (hash argument)" + ], + "origin": "Argument to TaskParameter::update (from skipListAcquirer callback)", + "transformations": [ + "Compared to finishHash_" + ], + "validated_at": "TaskParameter::update (if statement)" + }, + { + "field": "sList (update argument)", + "flow": [ + "skipListAcquirer_->addDataCallback", + "LedgerReplayTask::updateSkipList", + "TaskParameter::update (sList argument)" + ], + "origin": "skipListAcquirer_->getData()->skipList", + "transformations": [ + "Compared for size; copied to skipList_; finishHash_ appended" + ], + "validated_at": "TaskParameter::update (if statement)" + }, + { + "field": "full_", + "flow": [ + "TaskParameter::update (checked in if, set to true at end)", + "TaskParameter::canMergeInto (checked)", + "LedgerReplayTask::trigger (checked)" + ], + "origin": "TaskParameter member, default false", + "transformations": [ + "Set to true after successful update" + ], + "validated_at": "TaskParameter::update (if statement)" + }, + { + "field": "startHash_", + "flow": [ + "TaskParameter::update (set from skipList_)", + "XRPL_ASSERT on isNonZero()", + "Used in LedgerReplayTask::trigger (to acquire parent ledger)" + ], + "origin": "Set in TaskParameter::update", + "transformations": [ + "Extracted from skipList_" + ], + "validated_at": "TaskParameter::update (XRPL_ASSERT)" + } + ], + "description": "Implements the LedgerReplayTask class, which manages the process of replaying a sequence of ledgers in XRPL by acquiring necessary ledger deltas and handling task progress, timeouts, and completion.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "finishLedgerHash, totalNumLedgers", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at LedgerReplayTask::TaskParameter::TaskParameter (constructor)", + "issue_pattern": "Missing empty string validation for finishLedgerHash, totalNumLedgers", + "why_false_positive": "XRPL_ASSERT macro validates finishLedgerHash, totalNumLedgers for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hash, sList, full_", + "empty", + "string", + "validation" + ], + "evidence": "explicit if statement at LedgerReplayTask::TaskParameter::update", + "issue_pattern": "Missing empty string validation for hash, sList, full_", + "why_false_positive": "explicit if statement validates hash, sList, full_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "startHash_", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at LedgerReplayTask::TaskParameter::update", + "issue_pattern": "Missing empty string validation for startHash_", + "why_false_positive": "XRPL_ASSERT macro validates startHash_ for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp", + "functions": [ + { + "args": [ + "InboundLedger::Reason r", + "uint256 const& finishLedgerHash", + "std::uint32_t totalNumLedgers" + ], + "lineno": 8, + "name": "LedgerReplayTask::TaskParameter::TaskParameter" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t seq", + "std::vector const& sList" + ], + "lineno": 17, + "name": "LedgerReplayTask::TaskParameter::update" + }, + { + "args": [ + "TaskParameter const& existingTask" + ], + "lineno": 34, + "name": "LedgerReplayTask::TaskParameter::canMergeInto" + }, + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "LedgerReplayer& replayer", + "std::shared_ptr& skipListAcquirer", + "TaskParameter const& parameter" + ], + "lineno": 51, + "name": "LedgerReplayTask::LedgerReplayTask" + }, + { + "args": [], + "lineno": 70, + "name": "LedgerReplayTask::~LedgerReplayTask" + }, + { + "args": [], + "lineno": 75, + "name": "LedgerReplayTask::init" + }, + { + "args": [ + "ScopedLockType& sl" + ], + "lineno": 92, + "name": "LedgerReplayTask::trigger" + }, + { + "args": [ + "uint256 const& deltaHash" + ], + "lineno": 117, + "name": "LedgerReplayTask::deltaReady" + }, + { + "args": [ + "ScopedLockType& sl" + ], + "lineno": 124, + "name": "LedgerReplayTask::tryAdvance" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t seq", + "std::vector const& sList" + ], + "lineno": 157, + "name": "LedgerReplayTask::updateSkipList" + }, + { + "args": [ + "bool progress", + "ScopedLockType& sl" + ], + "lineno": 177, + "name": "LedgerReplayTask::onTimer" + }, + { + "args": [], + "lineno": 191, + "name": "LedgerReplayTask::pmDowncast" + }, + { + "args": [ + "std::shared_ptr const& delta" + ], + "lineno": 196, + "name": "LedgerReplayTask::addDelta" + }, + { + "args": [], + "lineno": 217, + "name": "LedgerReplayTask::finished" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested indirectly via integration tests for ledger replay and inbound ledger acquisition. Direct unit tests for TaskParameter validation logic may exist in test files such as LedgerReplayTask_test.cpp, LedgerReplayer_test.cpp, or InboundLedgers_test.cpp. However, explicit tests for all validation branches (e.g., invalid finishLedgerHash, too-short skip list, full_ already set) may not be present or may be incomplete. Exception/error handling paths may not be fully covered in tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro (custom assertion), explicit C++ logic", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "finishLedgerHash, totalNumLedgers", + "location": "LedgerReplayTask::TaskParameter::TaskParameter (constructor)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "finishLedgerHash.isNonZero()", + "totalNumLedgers > 0" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "hash, sList, full_", + "location": "LedgerReplayTask::TaskParameter::update", + "validated_by": "explicit if statement", + "validates": [ + "finishHash_ == hash", + "sList.size() + 1 >= totalLedgers_", + "full_ == false" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "startHash_", + "location": "LedgerReplayTask::TaskParameter::update", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "startHash_.isNonZero()" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp.ai.md b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp.ai.md new file mode 100644 index 0000000000..7054d6ff13 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplayTask.cpp.ai.md @@ -0,0 +1,48 @@ +# `LedgerReplayTask.cpp` — Orchestrating Sequential Ledger Range Replay + +## Role in the System + +`LedgerReplayTask` is the top-level orchestrator for replaying a contiguous range of historical ledgers from the network. When the XRPL node needs to catch up on a sequence of ledgers it never validated — for example during a gap in consensus — it instantiates a `LedgerReplayTask` to coordinate the work. The task is not itself a peer-communication primitive; rather, it delegates to two lower-level subtask types: `SkipListAcquire` (to determine the exact ledger hashes in the range) and `LedgerDeltaAcquire` (to fetch each individual ledger's header and transactions from peers). This file implements both the `TaskParameter` nested class and the `LedgerReplayTask` lifecycle. + +## Two-Phase Parameter Resolution + +The nested `TaskParameter` class captures a fundamental asymmetry in what the caller knows at request time versus what the network must supply. At construction, the caller provides only three things: an `InboundLedger::Reason` categorizing why this replay is needed, a `finishHash_` identifying the last ledger in the desired range, and a `totalLedgers_` count. The starting ledger hash — and the exact hashes of all intermediate ledgers — are unknown until the skip list arrives from the network. + +`TaskParameter::update()` completes the picture once the skip list is available. It appends `finishHash_` to the skip list (making it fully inclusive), then derives `startHash_` by indexing backward exactly `totalLedgers_` positions from the end. The `full_` flag gates all downstream work; nothing can progress until this flag is set. The `XRPL_ASSERT` on `startHash_.isNonZero()` catches a scenario where the skip list is too short to cover the requested range — a data integrity check rather than graceful recovery. + +The `update()` method is also deliberately idempotent-preventing: if `full_` is already true, it immediately returns false. Once a task's range is resolved, it cannot be re-resolved. + +## Task Deduplication via `canMergeInto()` + +Before creating a new `LedgerReplayTask`, `LedgerReplayer` checks whether an incoming replay request can be satisfied by an existing task. `canMergeInto()` handles two cases: + +1. **Same finish hash, smaller or equal range**: the new request's ledgers are a strict suffix of what the existing task is already fetching. +2. **Finish hash falls inside an existing task's skip list**: once the existing task's `full_` is set, the new request's `finishHash_` can be located in the already-known skip list, and if the existing task covers at least as many ledgers from that point forward, the new request is redundant. + +The second case requires careful arithmetic: `existingTask.totalLedgers_ >= totalLedgers_ + (exList.end() - i) - 1`, where `(exList.end() - i) - 1` is the number of ledgers in the existing task that come after the new request's finish point. This is non-obvious but ensures the existing task's range genuinely supersedes the new one. + +## Lifecycle and the Init/Trigger Pattern + +`LedgerReplayTask` inherits from `TimeoutCounter`, which provides a timer-driven retry loop running on the job queue. The separation of constructor and `init()` is intentional: construction sets up state, while `init()` registers the callback with `skipListAcquirer_` and starts the timer. This separation allows the task to be created and stored before it begins network activity. + +`init()` registers a lambda on `skipListAcquirer_` using a `std::weak_ptr` capture — a deliberate lifetime safety measure. If the task is cancelled or destroyed before the skip list arrives, `wptr.lock()` will return null and the callback silently discards its result. This prevents use-after-free and avoids the need for explicit cancellation signals flowing from parent to child. + +## The Sequential Build Loop + +Once the skip list resolves the full parameter set, `replayer_.createDeltas()` is called to spawn one `LedgerDeltaAcquire` per ledger in the range (excluding the start ledger itself, which is the parent). Each delta is registered via `addDelta()`, which attaches a callback and appends to the `deltas_` vector. The assertion in `addDelta()` that `deltas_.back()->ledgerSeq_ + 1 == delta->ledgerSeq_` enforces strict ordering — deltas must be added in sequence because `tryAdvance()` depends on this to maintain a valid `parent_` chain. + +The core of the task is `tryAdvance()`. It advances a cursor `deltaToBuild_` through the `deltas_` vector, calling `delta->tryBuild(parent)` on each. `tryBuild` returns null if the delta's network data hasn't arrived yet, causing `tryAdvance()` to suspend at that index. When any delta later signals readiness via `deltaReady()`, `tryAdvance()` resumes from where it stopped — not from the beginning. This means each ledger in the range is applied at most once, keeping replay efficient even when deltas arrive out-of-order relative to when they're actually needed. + +The loop only starts when three conditions are all true: a parent ledger is available, `parameter_.full_` is set, and all `totalLedgers_ - 1` deltas have been registered. This prevents partial builds from beginning before the full work set is known. + +## Error Handling and Timeout Policy + +`tryAdvance()` wraps the build loop in a `try/catch` for `std::runtime_error`. `LedgerDeltaAcquire::tryBuild()` may throw this if the replayed ledger's resulting hash doesn't match the expected hash — indicating corrupt or malicious data from a peer. The task responds by setting `failed_` rather than propagating the exception, keeping the error contained within the task. + +The `onTimer()` override scales the allowed timeout budget with the size of the replay range: `maxTimeouts_ = max(TASK_MAX_TIMEOUTS_MINIMUM, totalLedgers_ * TASK_MAX_TIMEOUTS_MULTIPLIER)`. This is important because a 200-ledger replay genuinely requires more clock time than a 2-ledger replay, and a fixed timeout budget would produce spurious failures for large ranges. The constants from `LedgerReplayParameters` place a floor of 10 timeouts at 500ms each, guaranteeing at least 5 seconds before a task is abandoned regardless of size. + +## Concurrency and Lock Discipline + +The most subtle locking pattern appears in `updateSkipList()`. It acquires the mutex to call `parameter_.update()`, then explicitly releases the lock before calling `replayer_.createDeltas()`. This is necessary because `LedgerReplayer` holds its own mutex, and calling into it while holding the task's mutex would risk deadlock if the replayer simultaneously tries to interact with this task. After `createDeltas()` returns, the lock is re-acquired to call `trigger()`. This deliberate lock-release-reacquire pattern is a form of lock ordering enforcement by design. + +The base class `TimeoutCounter` uses a `recursive_mutex`, which permits `trigger()` and `tryAdvance()` to be called from `onTimer()` (which holds the lock) without deadlocking on reentrance. Functions annotated with `ScopedLockType&` parameters document the convention that the caller must hold the lock — a pattern used consistently across the ledger replay subsystem. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplayer.cpp.ai.json b/src/xrpld/app/ledger/detail/LedgerReplayer.cpp.ai.json new file mode 100644 index 0000000000..0c5d262faa --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplayer.cpp.ai.json @@ -0,0 +1,435 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "std::unique_ptr peerSetBuilder" + ], + "lineno": 6, + "name": "LedgerReplayer" + } + ], + "code_paths": [ + { + "call_chain": [ + "LedgerReplayer::replay", + "LedgerReplayTask::TaskParameter ctor", + "LedgerReplayTask ctor", + "LedgerReplayTask::init", + "LedgerReplayer::createDeltas (indirect, via task->init)" + ], + "entry_point": "LedgerReplayer::replay", + "purpose": "Initiates a ledger replay task, validates input, manages task and skip list creation, and starts the replay process.", + "validation_points": [ + "LedgerReplayer::replay: XRPL_ASSERT on finishLedgerHash and totalNumLedgers", + "LedgerReplayer::replay: if (app_.isStopping())", + "LedgerReplayer::replay: if (tasks_.size() >= MAX_TASKS)", + "LedgerReplayer::replay: if (parameter.canMergeInto(t->getTaskParameter()))" + ] + }, + { + "call_chain": [ + "LedgerReplayer::~LedgerReplayer", + "tasks_.clear()" + ], + "entry_point": "LedgerReplayer::~LedgerReplayer", + "purpose": "Destructor cleans up all replay tasks.", + "validation_points": [] + }, + { + "call_chain": [ + "LedgerReplayer::createDeltas" + ], + "entry_point": "LedgerReplayer::createDeltas", + "purpose": "Creates ledger deltas for a replay task, checks skip list consistency.", + "validation_points": [ + "LedgerReplayer::createDeltas: checks on skip list items and parameter consistency" + ] + } + ], + "data_flows": [ + { + "field": "finishLedgerHash", + "flow": [ + "LedgerReplayer::replay parameter", + "XRPL_ASSERT validation", + "LedgerReplayTask::TaskParameter ctor", + "Used as key in skipLists_", + "Passed to SkipListAcquire", + "Used in logging and task creation" + ], + "origin": "Parameter to LedgerReplayer::replay", + "transformations": [ + "Checked for non-zero", + "Used as map key", + "Passed to constructors" + ], + "validated_at": "LedgerReplayer::replay (XRPL_ASSERT)" + }, + { + "field": "totalNumLedgers", + "flow": [ + "LedgerReplayer::replay parameter", + "XRPL_ASSERT validation", + "LedgerReplayTask::TaskParameter ctor", + "Used in logging and task creation" + ], + "origin": "Parameter to LedgerReplayer::replay", + "transformations": [ + "Checked for >0 and <= MAX_TASK_SIZE" + ], + "validated_at": "LedgerReplayer::replay (XRPL_ASSERT)" + }, + { + "field": "tasks_", + "flow": [ + "LedgerReplayer::replay", + "Checked for size >= MAX_TASKS", + "Used to check for mergeable tasks", + "New task pushed if validations pass" + ], + "origin": "LedgerReplayer member", + "transformations": [ + "Checked for size", + "Iterated for merge check", + "Appended to" + ], + "validated_at": "LedgerReplayer::replay (if (tasks_.size() >= MAX_TASKS))" + }, + { + "field": "app_.isStopping()", + "flow": [ + "LedgerReplayer::replay", + "Checked before proceeding with task creation" + ], + "origin": "Application member function", + "transformations": [ + "Boolean check" + ], + "validated_at": "LedgerReplayer::replay (if (app_.isStopping()))" + }, + { + "field": "parameter.canMergeInto(t->getTaskParameter())", + "flow": [ + "LedgerReplayer::replay", + "Iterates over tasks_ to check if new task can be merged" + ], + "origin": "LedgerReplayTask::TaskParameter method", + "transformations": [ + "Comparison logic" + ], + "validated_at": "LedgerReplayer::replay (if (parameter.canMergeInto(...)))" + } + ], + "description": "Implements the LedgerReplayer class, which manages replaying ledgers by coordinating tasks, skip lists, and deltas for ledger synchronization and replay in the XRPL system.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "finishLedgerHash, totalNumLedgers", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at LedgerReplayer::replay", + "issue_pattern": "Missing empty string validation for finishLedgerHash, totalNumLedgers", + "why_false_positive": "XRPL_ASSERT macro validates finishLedgerHash, totalNumLedgers for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "app_.isStopping()", + "empty", + "string", + "validation" + ], + "evidence": "if statement at LedgerReplayer::replay", + "issue_pattern": "Missing empty string validation for app_.isStopping()", + "why_false_positive": "if statement validates app_.isStopping() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tasks_.size()", + "empty", + "string", + "validation" + ], + "evidence": "if statement at LedgerReplayer::replay", + "issue_pattern": "Missing empty string validation for tasks_.size()", + "why_false_positive": "if statement validates tasks_.size() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "parameter.canMergeInto(t->getTaskParameter())", + "empty", + "string", + "validation" + ], + "evidence": "if statement at LedgerReplayer::replay", + "issue_pattern": "Missing empty string validation for parameter.canMergeInto(t->getTaskParameter())", + "why_false_positive": "if statement validates parameter.canMergeInto(t->getTaskParameter()) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerReplayer.cpp", + "functions": [ + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "std::unique_ptr peerSetBuilder" + ], + "lineno": 6, + "name": "LedgerReplayer" + }, + { + "args": [], + "lineno": 15, + "name": "~LedgerReplayer" + }, + { + "args": [ + "InboundLedger::Reason r", + "uint256 const& finishLedgerHash", + "std::uint32_t totalNumLedgers" + ], + "lineno": 20, + "name": "replay" + }, + { + "args": [ + "std::shared_ptr task" + ], + "lineno": 70, + "name": "createDeltas" + }, + { + "args": [ + "LedgerHeader const& info", + "boost::intrusive_ptr const& item" + ], + "lineno": 110, + "name": "gotSkipList" + }, + { + "args": [ + "LedgerHeader const& info", + "std::map>&& txns" + ], + "lineno": 128, + "name": "gotReplayDelta" + }, + { + "args": [], + "lineno": 146, + "name": "sweep" + }, + { + "args": [], + "lineno": 181, + "name": "stop" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic in LedgerReplayer::replay is critical for input safety and resource management. Typical test coverage would be in unit tests for LedgerReplayer, possibly in files like LedgerReplayer_test.cpp or LedgerReplayTask_test.cpp. Tests should cover: (1) invalid finishLedgerHash/totalNumLedgers (assertion failure), (2) app_.isStopping() returns true (task not created), (3) tasks_ at capacity (task not created), (4) mergeable task exists (task not created), (5) normal task creation. Gaps may exist if there are no tests for assertion failures (as these may terminate the process), or for concurrency/race conditions in task management. Integration tests may also be needed to cover full replay flows.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro, manual C++ checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely throws or aborts)", + "field": "finishLedgerHash, totalNumLedgers", + "location": "LedgerReplayer::replay", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "finishLedgerHash.isNonZero()", + "totalNumLedgers > 0", + "totalNumLedgers <= LedgerReplayParameters::MAX_TASK_SIZE" + ], + "validation_type": "format, range, business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "early return (no exception)", + "field": "app_.isStopping()", + "location": "LedgerReplayer::replay", + "validated_by": "if statement", + "validates": [ + "prevents replay if application is stopping" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "early return (no exception), logs info", + "field": "tasks_.size()", + "location": "LedgerReplayer::replay", + "validated_by": "if statement", + "validates": [ + "tasks_.size() < LedgerReplayParameters::MAX_TASKS" + ], + "validation_type": "range, business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "early return (no exception), logs info", + "field": "parameter.canMergeInto(t->getTaskParameter())", + "location": "LedgerReplayer::replay", + "validated_by": "if statement", + "validates": [ + "prevents duplicate/mergeable replay tasks" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerReplayer.cpp.ai.md b/src/xrpld/app/ledger/detail/LedgerReplayer.cpp.ai.md new file mode 100644 index 0000000000..ca03d55606 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerReplayer.cpp.ai.md @@ -0,0 +1,57 @@ +# `LedgerReplayer.cpp` — Ledger Replay Coordinator + +`LedgerReplayer` is the top-level manager for reconstructing historical ledgers from the network. When an XRPL node needs a contiguous range of ledgers it cannot derive locally — for example during consensus recovery, validation, or full-history catch-up — it calls `replay()` here. The class orchestrates a two-phase network acquisition process and owns the lifetime of all sub-tasks involved. + +## System Architecture + +The replay subsystem has three layers. `LedgerReplayer` sits at the top and coordinates zero or more `LedgerReplayTask` objects, each of which represents a request to rebuild a range of up to 256 consecutive ledgers. Each task in turn depends on two kinds of network sub-tasks: + +- **`SkipListAcquire`** — fetches the skip list entry embedded in a ledger's state tree. The skip list is a compact data structure that lets a node walk backwards through ledger history without downloading every header; it names the ancestors of the target ledger that the replayer must acquire in sequence. +- **`LedgerDeltaAcquire`** — fetches the header and the ordered transaction set for one specific ledger so the ledger can be reconstructed by applying those transactions to its parent. + +`LedgerReplayer` owns the authoritative collections for both sub-task types via `hash_map>` maps keyed on ledger hash, plus a `std::vector>` for the active tasks. + +## `replay()` — Entry Point and Deduplication + +When a caller asks to replay `totalNumLedgers` ledgers ending at `finishLedgerHash`, `replay()` performs several guard checks before creating anything: + +1. An `XRPL_ASSERT` enforces that the hash is non-zero and that the count is in `(0, MAX_TASK_SIZE]` (currently 256). This is a hard precondition for callers. +2. If the application is shutting down, the request is silently dropped. +3. If the active task count has hit `MAX_TASKS` (10), the request is dropped with a log message. This prevents runaway memory growth during pathological network conditions. +4. The new request's `TaskParameter` is tested against every existing task via `canMergeInto()`. If the requested range is a subset of an already-running task, the new request is discarded — the existing task will cover it. + +Only after all four checks pass is a new `LedgerReplayTask` created and appended to `tasks_`. + +## Shared `SkipListAcquire` Ownership + +Before creating the `SkipListAcquire` for the finish ledger, `replay()` consults `skipLists_` using `weak_ptr::lock()`. If a live `SkipListAcquire` for the same finish hash already exists (because another task is sharing it), the new task reuses that object. Only if the weak pointer is null or expired is a fresh `SkipListAcquire` constructed and the map updated. + +This sharing pattern avoids redundant network requests: two concurrent tasks with the same finish ledger hash issue only one round of peer queries for that skip list. The same pattern is applied to `LedgerDeltaAcquire` objects in `createDeltas()`. + +The reason for weak pointers rather than shared pointers in the maps is that the maps must not keep sub-tasks alive after all referencing tasks are done. The actual lifetime is held by the `LedgerReplayTask` objects; the maps are a lookup index, not an ownership register. + +Initialization ordering is deliberate. `skipList->init(1)` is called before `task->init()`, because if the skip list is already satisfied locally the callback chain fires immediately. Starting the task first could produce a brief window where `createDeltas()` is called before the skip list object is ready. + +## `createDeltas()` — Building the Delta Chain + +After a `LedgerReplayTask` resolves its skip list, it calls back into `LedgerReplayer::createDeltas()`. This method walks the skip list between `startSeq_` and `finishSeq_`, creating one `LedgerDeltaAcquire` per intermediate ledger. The iterator walk over `skipList_` provides the hashes; `seq` is incremented in lockstep to supply the sequence number to each `LedgerDeltaAcquire` constructor. + +A guard checks that the `startHash_` is found in the skip list and that there is at least one entry after it (the first delta). If either condition fails, the method logs an error and returns without creating any deltas — the task will eventually time out rather than produce garbage. + +Each `LedgerDeltaAcquire` follows the same weak-pointer reuse pattern as `SkipListAcquire`. Because multiple tasks can overlap in their ledger ranges, two different `LedgerReplayTask` instances may share individual `LedgerDeltaAcquire` sub-tasks. A stub comment in the method notes a future optimization: if the local node already has a validated ancestor within the range, the task could be narrowed to skip acquiring those known ledgers. + +## Inbound Data Dispatch: `gotSkipList()` and `gotReplayDelta()` + +These two methods are called by the network message handler layer when a peer responds with a `TMProofPathResponse` or `TMReplayDeltaResponse`. Both follow the same pattern: take the ledger hash from the inbound header, look it up in the appropriate weak-pointer map, attempt to lock, erase stale entries on failure, and forward the payload to the live object via `processData()`. + +The lock is dropped before calling `processData()`. This avoids holding `mtx_` during the potentially expensive `processData()` callbacks, which may trigger ledger rebuilds or further network requests. + +## Concurrency Model + +All mutable state — `tasks_`, `skipLists_`, and `deltas_` — is protected by a single `std::mutex mtx_`. The locking discipline is consistent: acquire the lock, manipulate the collections, release it, then call methods on the objects retrieved while the lock was held. This means no lock re-entry is possible from within `processData()` calls back into `LedgerReplayer`. + +The destructor acquires `mtx_` before clearing `tasks_`, ensuring that any concurrent `replay()` call that is mid-execution will complete or observe the cleared state. However, `stop()` is more thorough: it explicitly calls `cancel()` on every task and sub-task before clearing the maps, propagating a shutdown signal down the hierarchy so that pending timer callbacks are suppressed. + +## `sweep()` — Periodic Cleanup + +`sweep()` is called periodically (presumably by the application's maintenance timer). It removes finished tasks from `tasks_` using `std::remove_if`, and prunes stale entries from `skipLists_` and `deltas_` via a small lambda that attempts to lock each weak pointer. Entries that can no longer be locked — because all referencing tasks have released them — are erased. Lock-hold time is measured and logged at debug level, since `sweep()` runs under the same global mutex and a slow sweep could delay inbound data dispatch on other threads. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp.ai.json b/src/xrpld/app/ledger/detail/LedgerToJson.cpp.ai.json new file mode 100644 index 0000000000..d107a7b066 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp.ai.json @@ -0,0 +1,597 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "fillJson" + ], + "entry_point": "fillJson", + "purpose": "Fills a Json::Value with ledger header information, including validation of closeTime and apiVersion.", + "validation_points": [ + "apiVersion > 1 (ledger_index formatting)", + "info.closeTime != NetClock::time_point{} (close_time_human, close_time_iso, close_time_estimated)" + ] + }, + { + "call_chain": [ + "fillJsonBinary" + ], + "entry_point": "fillJsonBinary", + "purpose": "Fills a Json::Value with binary ledger header data if ledger is closed.", + "validation_points": [ + "closed (if/else check)" + ] + }, + { + "call_chain": [ + "fillJsonTx" + ], + "entry_point": "fillJsonTx", + "purpose": "Serializes a transaction and its metadata to JSON or binary, with validation on expansion, binary, apiVersion, and pointer checks.", + "validation_points": [ + "!bExpanded (early return)", + "bBinary (branch for binary serialization)", + "fill.context->apiVersion > 1 (hash/meta_blob fields)", + "stMeta (pointer null check before use)" + ] + }, + { + "call_chain": [ + "isFull", + "isExpanded", + "isBinary" + ], + "entry_point": "isFull / isExpanded / isBinary", + "purpose": "Bitmask checks on fill.options to determine output verbosity and format.", + "validation_points": [ + "fill.options & LedgerFill::full/expand/binary (bitmask validation)" + ] + } + ], + "data_flows": [ + { + "field": "fill.options", + "flow": [ + "LedgerFill.options", + "isFull/isExpanded/isBinary", + "controls branching in fillJson/fillJsonTx" + ], + "origin": "LedgerFill struct, typically constructed by caller (e.g., RPC handler)", + "transformations": [ + "Bitmask checked for full/expand/binary options" + ], + "validated_at": "isFull/isExpanded/isBinary" + }, + { + "field": "info.closeTime", + "flow": [ + "LedgerHeader.closeTime", + "fillJson", + "if (info.closeTime != NetClock::time_point{})", + "json fields: close_time_human, close_time_iso, close_time_estimated" + ], + "origin": "LedgerHeader struct, populated from ledger data", + "transformations": [ + "Compared to NetClock::time_point{} to check for validity", + "Converted to string/ISO format" + ], + "validated_at": "fillJson (if (info.closeTime != NetClock::time_point{}))" + }, + { + "field": "stMeta", + "flow": [ + "fillJsonTx parameter", + "if (stMeta)", + "serializeHex(*stMeta) or stMeta->getJson()" + ], + "origin": "std::shared_ptr, passed to fillJsonTx", + "transformations": [ + "Pointer null check before dereference", + "Serialized to hex or JSON" + ], + "validated_at": "fillJsonTx (if (stMeta))" + }, + { + "field": "txn", + "flow": [ + "fillJsonTx parameter", + "txn->getTransactionID(), txn->getTxnType(), txn->getJson()" + ], + "origin": "std::shared_ptr, passed to fillJsonTx", + "transformations": [ + "Assumed non-null (no explicit check in snippet)", + "Used for serialization and field extraction" + ], + "validated_at": "Implied by usage, not explicitly checked" + }, + { + "field": "fill.context->apiVersion", + "flow": [ + "fill.context->apiVersion", + "fillJson (ledger_index formatting)", + "fillJsonTx (hash/meta_blob fields, disables legacy JSON)" + ], + "origin": "RPC Context, set by API handler", + "transformations": [ + "Compared to 1 to select output format/fields" + ], + "validated_at": "fillJson, fillJsonTx (if (apiVersion > 1))" + } + ], + "description": "This file provides functions to serialize ledger data, transactions, and state into JSON format for RPC responses in the XRPL (XRP Ledger) server. It handles various options for binary, expanded, and full output, and supports different API versions.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "LedgerFill.options (bitmask flags)", + "validation", + "missing", + "check" + ], + "evidence": "Field LedgerFill.options (bitmask flags) validated by jss:: (JSON field name mapping), C++ type system, bitmask flag checks", + "issue_pattern": "Missing validation for LedgerFill.options (bitmask flags)", + "why_false_positive": "jss:: (JSON field name mapping), C++ type system, bitmask flag checks validates LedgerFill.options (bitmask flags) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "LedgerHeader fields (type-checked by C++ struct)", + "validation", + "missing", + "check" + ], + "evidence": "Field LedgerHeader fields (type-checked by C++ struct) validated by jss:: (JSON field name mapping), C++ type system, bitmask flag checks", + "issue_pattern": "Missing validation for LedgerHeader fields (type-checked by C++ struct)", + "why_false_positive": "jss:: (JSON field name mapping), C++ type system, bitmask flag checks validates LedgerHeader fields (type-checked by C++ struct) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "apiVersion (unsigned int, range checked in logic)", + "validation", + "missing", + "check" + ], + "evidence": "Field apiVersion (unsigned int, range checked in logic) validated by jss:: (JSON field name mapping), C++ type system, bitmask flag checks", + "issue_pattern": "Missing validation for apiVersion (unsigned int, range checked in logic)", + "why_false_positive": "jss:: (JSON field name mapping), C++ type system, bitmask flag checks validates apiVersion (unsigned int, range checked in logic) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fill.options", + "empty", + "string", + "validation" + ], + "evidence": "bitmask checks (isFull, isExpanded, isBinary) at isFull, isExpanded, isBinary", + "issue_pattern": "Missing empty string validation for fill.options", + "why_false_positive": "bitmask checks (isFull, isExpanded, isBinary) validates fill.options for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "info.closeTime", + "empty", + "string", + "validation" + ], + "evidence": "comparison to NetClock::time_point{} at fillJson", + "issue_pattern": "Missing empty string validation for info.closeTime", + "why_false_positive": "comparison to NetClock::time_point{} validates info.closeTime for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "stMeta", + "empty", + "string", + "validation" + ], + "evidence": "pointer null check at fillJsonTx", + "issue_pattern": "Missing empty string validation for stMeta", + "why_false_positive": "pointer null check validates stMeta for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "stMeta", + "type", + "validation", + "check" + ], + "evidence": "pointer null check at fillJsonTx", + "issue_pattern": "Missing type validation for stMeta", + "why_false_positive": "pointer null check validates stMeta type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "txn", + "empty", + "string", + "validation" + ], + "evidence": "pointer null check (implied, not explicit in snippet) at fillJsonTx", + "issue_pattern": "Missing empty string validation for txn", + "why_false_positive": "pointer null check (implied, not explicit in snippet) validates txn for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "txn", + "type", + "validation", + "check" + ], + "evidence": "pointer null check (implied, not explicit in snippet) at fillJsonTx", + "issue_pattern": "Missing type validation for txn", + "why_false_positive": "pointer null check (implied, not explicit in snippet) validates txn type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fill.context->apiVersion", + "empty", + "string", + "validation" + ], + "evidence": "comparison (apiVersion > 1) at fillJson, fillJsonTx", + "issue_pattern": "Missing empty string validation for fill.context->apiVersion", + "why_false_positive": "comparison (apiVersion > 1) validates fill.context->apiVersion for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "fill.context->apiVersion", + "range", + "bounds", + "validation" + ], + "evidence": "comparison (apiVersion > 1) at fillJson, fillJsonTx", + "issue_pattern": "Missing range validation for fill.context->apiVersion", + "why_false_positive": "comparison (apiVersion > 1) validates fill.context->apiVersion range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LedgerToJson.cpp", + "functions": [ + { + "args": [ + "fill" + ], + "lineno": 13, + "name": "isFull" + }, + { + "args": [ + "fill" + ], + "lineno": 18, + "name": "isExpanded" + }, + { + "args": [ + "fill" + ], + "lineno": 23, + "name": "isBinary" + }, + { + "args": [ + "json", + "closed", + "info", + "bFull", + "apiVersion" + ], + "lineno": 28, + "name": "fillJson" + }, + { + "args": [ + "json", + "closed", + "info" + ], + "lineno": 52, + "name": "fillJsonBinary" + }, + { + "args": [ + "fill", + "bBinary", + "bExpanded", + "txn", + "stMeta" + ], + "lineno": 67, + "name": "fillJsonTx" + }, + { + "args": [ + "json", + "fill" + ], + "lineno": 143, + "name": "fillJsonTx" + }, + { + "args": [ + "json", + "fill" + ], + "lineno": 170, + "name": "fillJsonState" + }, + { + "args": [ + "json", + "fill" + ], + "lineno": 194, + "name": "fillJsonQueue" + }, + { + "args": [ + "json", + "fill" + ], + "lineno": 224, + "name": "fillJson" + }, + { + "args": [ + "json", + "fill" + ], + "lineno": 244, + "name": "addJson" + }, + { + "args": [ + "fill" + ], + "lineno": 252, + "name": "getJson" + }, + { + "args": [ + "to", + "from" + ], + "lineno": 258, + "name": "copyFrom" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by integration and unit tests in the XRPLF/rippled repository, especially in test files under src/test/app/ledger, src/test/rpc, and src/test/ledger. Tests would exercise ledger serialization via RPC calls (ledger, ledger_data, tx, etc.), covering different fill.options, apiVersion, and ledger states. However, pointer null checks (stMeta, txn) may not be explicitly tested for null pointer dereference, and edge cases for malformed or missing fields may not be fully covered. There is no evidence of explicit negative tests for all validation branches (e.g., apiVersion boundary, empty closeTime, null stMeta).", + "validation_architecture": { + "auto_validated_fields": [ + "LedgerFill.options (bitmask flags)", + "LedgerHeader fields (type-checked by C++ struct)", + "apiVersion (unsigned int, range checked in logic)" + ], + "framework": "jss:: (JSON field name mapping), C++ type system, bitmask flag checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "fill.options", + "location": "isFull, isExpanded, isBinary", + "validated_by": "bitmask checks (isFull, isExpanded, isBinary)", + "validates": [ + "checks if options bitmask contains required flags (full, expand, binary)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logic)", + "field": "info.closeTime", + "location": "fillJson", + "validated_by": "comparison to NetClock::time_point{}", + "validates": [ + "checks if closeTime is set (not default/zero)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logic)", + "field": "stMeta", + "location": "fillJsonTx", + "validated_by": "pointer null check", + "validates": [ + "checks if stMeta pointer is not null before accessing" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "none (assumed, but dereferenced without explicit check)", + "field": "txn", + "location": "fillJsonTx", + "validated_by": "pointer null check (implied, not explicit in snippet)", + "validates": [ + "assumes txn is valid shared_ptr before dereferencing" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional logic)", + "field": "fill.context->apiVersion", + "location": "fillJson, fillJsonTx", + "validated_by": "comparison (apiVersion > 1)", + "validates": [ + "checks API version to determine output format/fields" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LedgerToJson.cpp.ai.md b/src/xrpld/app/ledger/detail/LedgerToJson.cpp.ai.md new file mode 100644 index 0000000000..c5525cb1c4 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LedgerToJson.cpp.ai.md @@ -0,0 +1,59 @@ +# `LedgerToJson.cpp` — Ledger Serialization for RPC Responses + +## Purpose and Context + +This file implements the conversion pipeline from an in-memory XRPL ledger to the `Json::Value` structures returned by the `ledger`, `ledger_data`, and related JSON/WebSocket RPC methods. It is the single place in the server where ledger headers, transaction sets, account state, and the transaction queue are marshalled into the wire format clients receive. + +The file is small but carries significant complexity because it must handle a combinatorial space of output options: whether the caller wants a full dump or just hashes, whether the encoding should be binary (hex-serialized) or human-readable JSON, and which API version the client negotiated. All of those decisions are encapsulated in the `LedgerFill` value-type defined in the companion header. + +## The `LedgerFill` Configuration Object + +`LedgerFill` (declared in `LedgerToJson.h`) is a plain aggregate that bundles everything the serialization functions need. It carries a `ReadView const&` for the ledger, a bitmask of `Options` flags, an optional pre-fetched queue snapshot, and a nullable `RPC::Context` pointer. The `options` bitmask has seven independent bits: + +| Flag | Effect | +|---|---| +| `dumpTxrp` | Include the transaction set | +| `dumpState` | Include all account-state entries | +| `expand` | Expand transactions/state to full JSON instead of hashes | +| `full` | Implies `expand` **and** forces both `dumpTxrp` and `dumpState` | +| `binary` | Encode expansions as raw hex instead of JSON | +| `ownerFunds` | Inject live `owner_funds` balance into offer-create entries | +| `dumpQueue` | Append the open-ledger transaction queue | + +Three small file-private helpers—`isFull`, `isExpanded`, and `isBinary`—evaluate these bits. `isExpanded` deliberately implies `isFull`, encoding the hierarchy: binary or expanded means full per-object detail; collapsed means hash-only output. + +The `RPC::Context` pointer is allowed to be `nullptr`. Several callsites (including diagnostic log paths in `LedgerHistory.cpp`) construct `LedgerFill` without an RPC context. Every access to `fill.context` is guarded, and the API version defaults to `RPC::apiMaximumSupportedVersion` when the context is absent. + +## Ledger Header Serialization + +`fillJson` (the header-only overload) handles the open-vs-closed distinction carefully. For an open ledger that does not request a full dump, the function writes only `parent_hash`, `ledger_index`, and `closed: false`, then returns early—omitting the hash and close-time fields that are undefined for an in-progress ledger. This early-return pattern is intentional: a client that asks for the open ledger should not receive stale or zero-valued close data. + +The `ledger_index` field changes type across API versions: pre-v2 clients receive a string (for backward compatibility with code that parses it as text), while v2+ clients receive a JSON integer. The `close_time_human` and `close_time_iso` convenience fields are guarded by a comparison against `NetClock::time_point{}` (the epoch), avoiding emission of an "epoch" timestamp for ledgers whose close time has not been set. + +`fillJsonBinary` is the binary-mode header path. A closed ledger is serialized via `addRaw(info, s)` and emitted as a hex string under `ledger_data`; an open ledger emits only `closed: false`. + +## Transaction Serialization + +`fillJsonTx` (the single-transaction overload) is the most complex function in the file and has three distinct output modes controlled by the binary flag and API version: + +**Binary mode** (`bBinary == true`): The transaction is serialized to hex under `tx_blob`. For v2+ clients the hash is also included under `hash` and metadata under `meta_blob`; for v1 clients metadata goes under `meta`. This asymmetry exists because v2 made the hash a first-class field independent of blob decoding. + +**API v2+ JSON mode**: The transaction is rendered with `JsonOptions::disable_API_prior_V2`, which suppresses legacy field aliases. It is nested under `tx_json` and receives its hash explicitly. The function calls `RPC::insertDeliverMax` to copy the `Amount` field to `DeliverMax` (and remove `Amount` for v2+ clients), reflecting the v2 API rename. For `ttPAYMENT` and `ttCHECK_CASH` transactions, `RPC::insertDeliveredAmount` injects `delivered_amount` into the metadata block—a post-processing step required because the on-ledger metadata does not always contain the actual delivered value. For `MPTokenIssuanceCreate` transactions, `RPC::insertMPTokenIssuanceID` injects the computed `mpt_issuance_id`. + +**API v1 JSON mode**: The transaction is inlined directly at the top of `txJson` (no `tx_json` wrapper), metadata appears under `metaData` (capital D), and the same `delivered_amount` and MPT ID injections are applied. The different nesting reflects how the v1 `ledger` RPC historically embedded transactions. + +The `ownerFunds` injection after all three branches applies specifically to `ttOFFER_CREATE`: it queries `accountFunds` live from the ledger to include the offer creator's current balance of the asset they are offering. This is useful for order-book consumers who want to filter out underfunded offers without issuing separate account lookups. Notably, self-funded offers (where the issuer is also the account) are excluded because cross-currency self-offers have no funding risk. + +The array-level `fillJsonTx` overload iterates `fill.ledger.txs` and appends results. The entire loop body is wrapped in a `try`/`catch(std::exception const&)` with an error log—this mirrors a pattern found in the gRPC `doLedgerGrpc` handler for the same reason: a deserialization failure in one transaction should not abort serialization of the remaining transactions. + +## State and Queue Serialization + +`fillJsonState` iterates the ledger's SLE (Serialized Ledger Entry) set. In binary mode, each entry is emitted as an object with `hash` and `tx_blob`. In expanded mode, the full `getJson()` representation is appended. In collapsed mode, only the 256-bit key hash is appended. These three variants map to the same `expand`/`binary`/neither dichotomy used throughout the file. + +`fillJsonQueue` serializes entries from the transaction queue snapshot stored in `fill.txQueue`. Each entry exposes fee-level metadata (`fee_level`, `fee`, `max_spend_drops`, `auth_change`), retry tracking (`retries_remaining`, `preflight_result`, `last_result`), and the transaction itself via the same `fillJsonTx` path. The v2 API flattens queue transaction fields directly into the queue entry object; v1 nests them under a `tx` key. + +## Public Entry Points and `copyFrom` + +`addJson` is the standard entry point: it writes a `ledger` key into the supplied JSON object, fills it, and then conditionally appends `queue_data` at the top level. `getJson` is a convenience wrapper that allocates a fresh `Json::Value` and delegates to the private `fillJson`. + +`copyFrom` is a utility exposed from the header because it is used by `LedgerHandler::writeResult` to merge pre-validated result fields into the final response. Its fast path handles the common case where the destination is unset (direct assignment avoids member iteration). When the destination already exists, it iterates `from`'s members and copies them one by one. The `XRPL_ASSERT` verifying that `from` is an object-or-null guards against silent data corruption from accidentally merging a non-object value into an existing object—a form of defensive programming against misuse of the API rather than against malformed ledger data. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LocalTxs.cpp.ai.json b/src/xrpld/app/ledger/detail/LocalTxs.cpp.ai.json new file mode 100644 index 0000000000..a6a9fc9d30 --- /dev/null +++ b/src/xrpld/app/ledger/detail/LocalTxs.cpp.ai.json @@ -0,0 +1,486 @@ +{ + "args": [ + { + "lineno": 23, + "name": "index" + }, + { + "lineno": 23, + "name": "txn" + }, + { + "lineno": 46, + "name": "i" + }, + { + "lineno": 89, + "name": "view" + } + ], + "classes": [ + { + "args": [ + "LedgerIndex index", + "std::shared_ptr const& txn" + ], + "lineno": 16, + "name": "LocalTx" + }, + { + "args": [], + "lineno": 64, + "name": "LocalTxsImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "LocalTxsImp::push_back", + "LocalTx::LocalTx (constructor)", + "LocalTx::getAccount", + "LocalTx::getSeqProxy" + ], + "entry_point": "LocalTxsImp::push_back", + "purpose": "Adds a new local transaction to the local transaction set, wrapping it in a LocalTx object which performs validation and caching.", + "validation_points": [ + "LocalTx::LocalTx (validates sfLastLedgerSequence, validates txn type, validates sfAccount)" + ] + }, + { + "call_chain": [ + "LocalTxsImp::sweep", + "LocalTx::isExpired", + "LocalTx::getID", + "LocalTx::getAccount", + "LocalTx::getSeqProxy" + ], + "entry_point": "LocalTxsImp::sweep", + "purpose": "Removes expired or accepted transactions from the local transaction set, validating expiration and existence in the ledger.", + "validation_points": [ + "LocalTx::isExpired (validates expiration against ledger index)", + "view.txExists (checks if transaction is in ledger)", + "AccountID/account existence and sequence validation" + ] + }, + { + "call_chain": [ + "LocalTxsImp::getTxSet", + "LocalTx::getTX" + ], + "entry_point": "LocalTxsImp::getTxSet", + "purpose": "Retrieves all local transactions as a canonical set for application to the open ledger.", + "validation_points": [ + "No explicit validation here; relies on prior validation in LocalTx construction" + ] + } + ], + "data_flows": [ + { + "field": "sfLastLedgerSequence", + "flow": [ + "STTx::getFieldU32(sfLastLedgerSequence)", + "LocalTx::LocalTx (constructor)", + "m_expire (expiration ledger index)" + ], + "origin": "STTx (transaction) field", + "transformations": [ + "If present, m_expire is set to min(default_expire, sfLastLedgerSequence+1)" + ], + "validated_at": "LocalTx::LocalTx (constructor)" + }, + { + "field": "txn (STTx pointer)", + "flow": [ + "LocalTxsImp::push_back", + "LocalTx::LocalTx (constructor)", + "m_txn" + ], + "origin": "Input to LocalTxsImp::push_back", + "transformations": [ + "Type-checked as std::shared_ptr" + ], + "validated_at": "LocalTx::LocalTx (constructor, via type system)" + }, + { + "field": "sfAccount", + "flow": [ + "txn->getAccountID(sfAccount)", + "LocalTx::LocalTx (constructor)", + "m_account" + ], + "origin": "STTx (transaction) field", + "transformations": [ + "Extracted and cached as AccountID" + ], + "validated_at": "LocalTx::LocalTx (constructor)" + }, + { + "field": "m_expire", + "flow": [ + "Set in LocalTx::LocalTx", + "Used in LocalTx::isExpired", + "Checked in LocalTxsImp::sweep" + ], + "origin": "LocalTx::LocalTx (constructor)", + "transformations": [ + "Set to index + holdLedgers, possibly reduced by sfLastLedgerSequence" + ], + "validated_at": "LocalTx::isExpired" + }, + { + "field": "m_account", + "flow": [ + "Set in LocalTx::LocalTx", + "Used in LocalTx::getAccount", + "Used in LocalTxsImp::sweep for account lookup" + ], + "origin": "LocalTx::LocalTx (constructor)", + "transformations": [ + "None after extraction" + ], + "validated_at": "LocalTx::LocalTx (constructor)" + }, + { + "field": "m_seqProxy", + "flow": [ + "Set in LocalTx::LocalTx", + "Used in LocalTx::getSeqProxy", + "Used in LocalTxsImp::sweep for sequence checks" + ], + "origin": "LocalTx::LocalTx (constructor)", + "transformations": [ + "Extracted from txn->getSeqProxy()" + ], + "validated_at": "LocalTx::LocalTx (constructor)" + } + ], + "description": "Implements tracking and management of local transactions that have not yet been included in a fully-validated ledger, ensuring they are retried on new open ledgers until confirmed or expired.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "txn (STTx pointer) via type system", + "validation", + "missing", + "check" + ], + "evidence": "Field txn (STTx pointer) via type system validated by None (manual validation, type system, business logic)", + "issue_pattern": "Missing validation for txn (STTx pointer) via type system", + "why_false_positive": "None (manual validation, type system, business logic) validates txn (STTx pointer) via type system automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLastLedgerSequence", + "empty", + "string", + "validation" + ], + "evidence": "LocalTx constructor at LocalTx::LocalTx (constructor)", + "issue_pattern": "Missing empty string validation for sfLastLedgerSequence", + "why_false_positive": "LocalTx constructor validates sfLastLedgerSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "txn (STTx pointer)", + "empty", + "string", + "validation" + ], + "evidence": "Type system (std::shared_ptr) at LocalTx::LocalTx (constructor)", + "issue_pattern": "Missing empty string validation for txn (STTx pointer)", + "why_false_positive": "Type system (std::shared_ptr) validates txn (STTx pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "txn (STTx pointer)", + "type", + "validation", + "check" + ], + "evidence": "Type system (std::shared_ptr) at LocalTx::LocalTx (constructor)", + "issue_pattern": "Missing type validation for txn (STTx pointer)", + "why_false_positive": "Type system (std::shared_ptr) validates txn (STTx pointer) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "sfAccount", + "empty", + "string", + "validation" + ], + "evidence": "txn->getAccountID(sfAccount) at LocalTx::LocalTx (constructor)", + "issue_pattern": "Missing empty string validation for sfAccount", + "why_false_positive": "txn->getAccountID(sfAccount) validates sfAccount for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/LocalTxs.cpp", + "functions": [ + { + "args": [ + "LedgerIndex index", + "std::shared_ptr const& txn" + ], + "lineno": 23, + "name": "LocalTx" + }, + { + "args": [], + "lineno": 36, + "name": "getID" + }, + { + "args": [], + "lineno": 41, + "name": "getSeqProxy" + }, + { + "args": [ + "LedgerIndex i" + ], + "lineno": 46, + "name": "isExpired" + }, + { + "args": [], + "lineno": 51, + "name": "getTX" + }, + { + "args": [], + "lineno": 56, + "name": "getAccount" + }, + { + "args": [], + "lineno": 65, + "name": "LocalTxsImp" + }, + { + "args": [ + "LedgerIndex index", + "std::shared_ptr const& txn" + ], + "lineno": 69, + "name": "push_back" + }, + { + "args": [], + "lineno": 75, + "name": "getTxSet" + }, + { + "args": [ + "ReadView const& view" + ], + "lineno": 89, + "name": "sweep" + }, + { + "args": [], + "lineno": 116, + "name": "size" + }, + { + "args": [], + "lineno": 124, + "name": "make_LocalTxs" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by integration and unit tests in the rippled codebase, especially those testing local transaction handling, transaction expiration, and ledger replay. Look for tests in files such as 'test/ledger/LocalTxs_test.cpp', 'test/app/ledger/LocalTxs_test.cpp', or broader transaction/ledger tests. However, explicit validation of sfLastLedgerSequence, sfAccount extraction, and sequence proxy logic may not be directly unit tested unless there are targeted tests for LocalTx construction and LocalTxsImp::sweep. Edge cases (e.g., missing sfAccount, malformed transactions, or boundary expiration) may not be fully covered unless specifically tested.", + "validation_architecture": { + "auto_validated_fields": [ + "txn (STTx pointer) via type system" + ], + "framework": "None (manual validation, type system, business logic)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (handled via logic, not exception)", + "field": "sfLastLedgerSequence", + "location": "LocalTx::LocalTx (constructor)", + "validated_by": "LocalTx constructor", + "validates": [ + "Checks if sfLastLedgerSequence field is present in the transaction (txn->isFieldPresent(sfLastLedgerSequence))", + "If present, sets m_expire to the minimum of (index + holdLedgers) and (sfLastLedgerSequence + 1)" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 0.9, + "error_thrown": "none (relies on type safety)", + "field": "txn (STTx pointer)", + "location": "LocalTx::LocalTx (constructor)", + "validated_by": "Type system (std::shared_ptr)", + "validates": [ + "Ensures txn is a shared pointer to a constant STTx object" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "none (assumes field is present, no explicit check)", + "field": "sfAccount", + "location": "LocalTx::LocalTx (constructor)", + "validated_by": "txn->getAccountID(sfAccount)", + "validates": [ + "Retrieves the account ID from the transaction using sfAccount" + ], + "validation_type": "business_logic|type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/LocalTxs.cpp.ai.md b/src/xrpld/app/ledger/detail/LocalTxs.cpp.ai.md new file mode 100644 index 0000000000..63e5b3f90c --- /dev/null +++ b/src/xrpld/app/ledger/detail/LocalTxs.cpp.ai.md @@ -0,0 +1,72 @@ +# `src/xrpld/app/ledger/detail/LocalTxs.cpp` + +## Purpose + +This file solves a specific and subtle failure mode in XRPL consensus: a locally-submitted transaction can silently disappear when the server's view of consensus diverges from the network majority. The file-level comment tells the story in seven steps — a client submits a transaction, the local server believes it was included in its consensus ledger, but the majority ledger wins without that transaction. The server rebuilds an open ledger that has neither the transaction nor a predecessor to it, so a follow-up transaction from the same account fails with `terPRE_SEQ` before it can even be relayed. + +The fix is to hold locally-submitted transactions in a short-lived buffer and re-apply them to every new open ledger until they appear in a fully-validated ledger or expire. This is `LocalTxs`. + +--- + +## Architecture: Two Private Classes, One Factory + +The public interface is declared in `LocalTxs.h` as a pure abstract base class. The implementation lives entirely within this `.cpp` file, split into two private classes: `LocalTx` (the per-transaction record) and `LocalTxsImp` (the container). The factory function `make_LocalTxs()` returns a heap-allocated `LocalTxsImp` through the abstract interface. This encapsulation means callers never depend on the concrete type — `NetworkOPsImp` stores a `std::unique_ptr` and `RCLConsensus::Adaptor` holds a `LocalTxs&` reference. + +--- + +## `LocalTx`: Cached Transaction Wrapper + +`LocalTx` wraps a `std::shared_ptr` along with data extracted at construction time: the transaction ID (`m_id`), the submitting account (`m_account`), the sequence/ticket proxy (`m_seqProxy`), and a computed expiry ledger index (`m_expire`). + +The expiry logic is meaningful. The default expiry is `index + LocalTxs::holdLedgers` (currently 5 ledgers, a constant on the base class). If the transaction itself carries an `sfLastLedgerSequence` field, the expiry is tightened to `min(default, sfLastLedgerSequence + 1)` — there is no point holding a transaction past its own declared deadline. This means the in-memory hold time mirrors the transaction's own validity window, preventing stale transactions from polluting every new open ledger indefinitely. + +Caching the account ID and `SeqProxy` at construction time avoids repeated field lookups inside `sweep()`, which iterates the full list on every validated ledger. + +--- + +## `LocalTxsImp`: Thread-Safe List + +The container is a `std::list` protected by a `std::mutex`. All four public methods — `push_back`, `getTxSet`, `sweep`, and `size` — acquire the mutex with `std::lock_guard`. Using `std::list` rather than `std::vector` is intentional: `sweep()` calls `std::list::remove_if`, which deletes matching elements in a single pass without invalidating iterators on unaffected nodes or requiring a shift of remaining elements. + +### `push_back` + +Called by `NetworkOPsImp` after a locally-submitted transaction passes initial validation and is applied to the open ledger. The current ledger index is passed as the `index` anchor for computing expiry. + +### `getTxSet` + +Returns a `CanonicalTXSet` constructed from all currently-held transactions. `CanonicalTXSet` sorts transactions per-account by `SeqProxy`, which ensures that when the consensus engine applies local transactions to a new open ledger they are presented in a valid ordering — sequence numbers in ascending order, tickets ordered correctly among them. The set is built under the lock and then returned by value; the caller (ultimately `NetworkOPsImp::doAdvance` and the `RCLConsensus` adaptor building a new open ledger) applies it outside the lock. + +### `sweep` + +This is the most complex method. It is called by `NetworkOPsImp::updateLocalTx` after a new ledger is fully validated. It removes entries that are no longer needed, using `remove_if` with a lambda that consults the validated ledger's `ReadView`. + +Three removal conditions are checked in order: + +1. **Expiry**: if `view.header().seq > m_expire`, the transaction is beyond its hold window and dropped regardless of other state. +2. **Already applied**: `view.txExists(txn.getID())` — the transaction appears in the validated ledger, so it succeeded on some path and should no longer be replayed. +3. **Sequence/ticket invalidation**: this branch runs only if the account exists in the validated ledger (a missing account means the transaction might still be the account-creating transaction, so it is kept). + +For sequence-based transactions (`seqProx.isSeq()`): the transaction is dropped if `acctSeq > seqProx`, meaning the account's on-ledger sequence number has already advanced past this transaction's sequence — either the transaction was applied through a different mechanism or a conflicting transaction consumed that slot. + +For ticket-based transactions (`seqProx.isTicket()`): the logic is more nuanced. If the account's current sequence is still below or equal to the ticket's value, the ticket hasn't been created yet — this is treated as a "future ticket" and the transaction is kept (the comment notes this hold is still bounded by `m_expire`). If the account sequence has surpassed the ticket value, the ticket should already exist on-ledger; if `keylet::ticket(acctID, seqProx)` does not resolve in the view, the ticket was consumed or never created, and the transaction is removed. + +--- + +## Integration Points + +`NetworkOPsImp` owns the `LocalTxs` instance (`m_localTX`) and coordinates all three lifecycle operations: +- **`push_back`** is called in the transaction application path when a transaction is flagged as local and passes the hold criteria. +- **`getTxSet`** is called during `doAdvance` to obtain the retry set when building a new open ledger after consensus. +- **`sweep`** is called via `updateLocalTx` each time a fully-validated ledger arrives. + +`RCLConsensus::Adaptor` also calls `localTxs_.getTxSet()` during open ledger construction after a consensus round completes, ensuring local transactions are folded back into the ledger even during normal consensus operation. + +--- + +## Design Tradeoffs + +The 5-ledger hold window (`holdLedgers = 5`) is explicitly documented in the header as "essentially arbitrary" — large enough to survive a couple of consensus rounds but small enough not to accumulate stale work indefinitely. The `sfLastLedgerSequence` tightening prevents the buffer from holding transactions longer than the submitter intended. + +The use of `std::list` over `std::deque` or `std::vector` prioritizes stable in-place deletion during `sweep` over cache-friendly iteration. Given that this list is expected to be small (locally-submitted transactions from a single server instance), the allocation overhead of `std::list` is acceptable. + +No exceptions are thrown anywhere in this file. All error handling is via silent removal: a transaction that cannot be validated is simply not held, and a transaction that proves impossible is swept away on the next validated ledger boundary. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp.ai.json b/src/xrpld/app/ledger/detail/OpenLedger.cpp.ai.json new file mode 100644 index 0000000000..2e8b482334 --- /dev/null +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp.ai.json @@ -0,0 +1,872 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "std::shared_ptr const& ledger", + "CachedSLEs& cache", + "beast::Journal journal" + ], + "lineno": 8, + "name": "OpenLedger" + } + ], + "code_paths": [ + { + "call_chain": [ + "OpenLedger::accept", + "OpenLedger::create", + "apply (free function)", + "app.getTxQ().apply" + ], + "entry_point": "OpenLedger::accept", + "purpose": "Accepts a new ledger, applies pending and local transactions, relays transactions, and updates the open ledger view.", + "validation_points": [ + "OpenLedger::create (validates ledger rules and pointer)", + "apply (validates transactions and ledger state)", + "app.getTxQ().apply (validates local transactions)" + ] + }, + { + "call_chain": [ + "OpenLedger::modify", + "modify_type (lambda or function passed in)", + "OpenView copy/assignment" + ], + "entry_point": "OpenLedger::modify", + "purpose": "Allows atomic modification of the open ledger view via a user-supplied function.", + "validation_points": [ + "modify_type (user function is responsible for validation)", + "OpenView copy (implicit type and state validation)" + ] + }, + { + "call_chain": [ + "OpenLedger::empty", + "current_->txCount()" + ], + "entry_point": "OpenLedger::empty", + "purpose": "Checks if the open ledger has any transactions.", + "validation_points": [ + "current_->txCount() (ensures current_ is valid and counts transactions)" + ] + }, + { + "call_chain": [ + "OpenLedger::current" + ], + "entry_point": "OpenLedger::current", + "purpose": "Returns the current open ledger view.", + "validation_points": [ + "std::lock_guard (thread safety, ensures current_ is valid)" + ] + } + ], + "data_flows": [ + { + "field": "ledger (std::shared_ptr)", + "flow": [ + "OpenLedger::OpenLedger (constructor)", + "OpenLedger::create", + "current_ (OpenView)" + ], + "origin": "Constructor argument to OpenLedger", + "transformations": [ + "Passed as const shared_ptr", + "Used to create OpenView via create()" + ], + "validated_at": "OpenLedger::create (type and constness checked)" + }, + { + "field": "cache (CachedSLEs&)", + "flow": [ + "OpenLedger::OpenLedger", + "cache_ member" + ], + "origin": "Constructor argument to OpenLedger", + "transformations": [ + "Stored as reference", + "Used in OpenView and transaction application" + ], + "validated_at": "Constructor (reference, non-null by type)" + }, + { + "field": "current_ (OpenView)", + "flow": [ + "OpenLedger::create", + "current_ member", + "Used in empty(), modify(), accept()" + ], + "origin": "OpenLedger::create", + "transformations": [ + "Created from ledger rules and ledger", + "Copied or replaced in modify/accept" + ], + "validated_at": "OpenLedger::create (ensures rules and ledger are valid)" + }, + { + "field": "current_->txCount()", + "flow": [ + "OpenLedger::empty", + "current_->txCount()" + ], + "origin": "OpenView::txCount()", + "transformations": [ + "Counts transactions in current_" + ], + "validated_at": "txCount() method (ensures current_ is valid)" + }, + { + "field": "locals (OrderedTxs const&)", + "flow": [ + "OpenLedger::accept", + "for loop over locals", + "app.getTxQ().apply" + ], + "origin": "accept() argument", + "transformations": [ + "Iterated and each transaction applied to next OpenView" + ], + "validated_at": "app.getTxQ().apply (validates transaction)" + }, + { + "field": "retries (OrderedTxs&)", + "flow": [ + "OpenLedger::accept", + "apply (free function)" + ], + "origin": "accept() argument", + "transformations": [ + "Passed to apply for retry logic" + ], + "validated_at": "apply (validates retry transactions)" + }, + { + "field": "tx (std::shared_ptr)", + "flow": [ + "OpenLedger::accept", + "apply", + "app.getTxQ().apply", + "Relay logic" + ], + "origin": "current_->txs or locals", + "transformations": [ + "Checked for batch flag", + "Relayed if not recently relayed" + ], + "validated_at": "apply, relay logic (flag and field checks)" + } + ], + "description": "Implements the OpenLedger class, which manages the open ledger state in the XRPL node, including transaction application, modification, and relay logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "shared_ptr types (non-null checked by dereference)", + "validation", + "missing", + "check" + ], + "evidence": "Field shared_ptr types (non-null checked by dereference) validated by C++ type system, shared_ptr, business logic in apply/applyTxQ", + "issue_pattern": "Missing validation for shared_ptr types (non-null checked by dereference)", + "why_false_positive": "C++ type system, shared_ptr, business logic in apply/applyTxQ validates shared_ptr types (non-null checked by dereference) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "reference types (must be valid)", + "validation", + "missing", + "check" + ], + "evidence": "Field reference types (must be valid) validated by C++ type system, shared_ptr, business logic in apply/applyTxQ", + "issue_pattern": "Missing validation for reference types (must be valid)", + "why_false_positive": "C++ type system, shared_ptr, business logic in apply/applyTxQ validates reference types (must be valid) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "callable types (checked by if (f))", + "validation", + "missing", + "check" + ], + "evidence": "Field callable types (checked by if (f)) validated by C++ type system, shared_ptr, business logic in apply/applyTxQ", + "issue_pattern": "Missing validation for callable types (checked by if (f))", + "why_false_positive": "C++ type system, shared_ptr, business logic in apply/applyTxQ validates callable types (checked by if (f)) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "ledger (std::shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "implicit type checking (shared_ptr, constness) at OpenLedger constructor", + "issue_pattern": "Missing empty string validation for ledger (std::shared_ptr)", + "why_false_positive": "implicit type checking (shared_ptr, constness) validates ledger (std::shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger (std::shared_ptr)", + "type", + "validation", + "check" + ], + "evidence": "implicit type checking (shared_ptr, constness) at OpenLedger constructor", + "issue_pattern": "Missing type validation for ledger (std::shared_ptr)", + "why_false_positive": "implicit type checking (shared_ptr, constness) validates ledger (std::shared_ptr) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "cache (CachedSLEs&)", + "empty", + "string", + "validation" + ], + "evidence": "implicit type checking (reference, non-null) at OpenLedger constructor", + "issue_pattern": "Missing empty string validation for cache (CachedSLEs&)", + "why_false_positive": "implicit type checking (reference, non-null) validates cache (CachedSLEs&) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "cache (CachedSLEs&)", + "type", + "validation", + "check" + ], + "evidence": "implicit type checking (reference, non-null) at OpenLedger constructor", + "issue_pattern": "Missing type validation for cache (CachedSLEs&)", + "why_false_positive": "implicit type checking (reference, non-null) validates cache (CachedSLEs&) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "current_ (OpenView)", + "empty", + "string", + "validation" + ], + "evidence": "create(ledger->rules(), ledger) at OpenLedger constructor", + "issue_pattern": "Missing empty string validation for current_ (OpenView)", + "why_false_positive": "create(ledger->rules(), ledger) validates current_ (OpenView) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "current_->txCount()", + "empty", + "string", + "validation" + ], + "evidence": "txCount() method at OpenLedger::empty()", + "issue_pattern": "Missing empty string validation for current_->txCount()", + "why_false_positive": "txCount() method validates current_->txCount() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "current_ (OpenView)", + "empty", + "string", + "validation" + ], + "evidence": "std::lock_guard, shared_ptr at OpenLedger::current()", + "issue_pattern": "Missing empty string validation for current_ (OpenView)", + "why_false_positive": "std::lock_guard, shared_ptr validates current_ (OpenView) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "current_ (OpenView)", + "type", + "validation", + "check" + ], + "evidence": "std::lock_guard, shared_ptr at OpenLedger::current()", + "issue_pattern": "Missing type validation for current_ (OpenView)", + "why_false_positive": "std::lock_guard, shared_ptr validates current_ (OpenView) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "f (modify_type const&)", + "empty", + "string", + "validation" + ], + "evidence": "callable check (if (f)) at OpenLedger::modify, OpenLedger::accept", + "issue_pattern": "Missing empty string validation for f (modify_type const&)", + "why_false_positive": "callable check (if (f)) validates f (modify_type const&) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "f (modify_type const&)", + "type", + "validation", + "check" + ], + "evidence": "callable check (if (f)) at OpenLedger::modify, OpenLedger::accept", + "issue_pattern": "Missing type validation for f (modify_type const&)", + "why_false_positive": "callable check (if (f)) validates f (modify_type const&) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "next (OpenView)", + "empty", + "string", + "validation" + ], + "evidence": "std::make_shared(*current_) at OpenLedger::modify", + "issue_pattern": "Missing empty string validation for next (OpenView)", + "why_false_positive": "std::make_shared(*current_) validates next (OpenView) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "next (OpenView)", + "type", + "validation", + "check" + ], + "evidence": "std::make_shared(*current_) at OpenLedger::modify", + "issue_pattern": "Missing type validation for next (OpenView)", + "why_false_positive": "std::make_shared(*current_) validates next (OpenView) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "ledger (std::shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "ledger->seq() at OpenLedger::accept", + "issue_pattern": "Missing empty string validation for ledger (std::shared_ptr)", + "why_false_positive": "ledger->seq() validates ledger (std::shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger (std::shared_ptr)", + "type", + "validation", + "check" + ], + "evidence": "ledger->seq() at OpenLedger::accept", + "issue_pattern": "Missing type validation for ledger (std::shared_ptr)", + "why_false_positive": "ledger->seq() validates ledger (std::shared_ptr) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "current_->txs", + "empty", + "string", + "validation" + ], + "evidence": "empty() check, iteration at OpenLedger::accept", + "issue_pattern": "Missing empty string validation for current_->txs", + "why_false_positive": "empty() check, iteration validates current_->txs for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "current_->txs", + "type", + "validation", + "check" + ], + "evidence": "empty() check, iteration at OpenLedger::accept", + "issue_pattern": "Missing type validation for current_->txs", + "why_false_positive": "empty() check, iteration validates current_->txs type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "locals (OrderedTxs const&)", + "empty", + "string", + "validation" + ], + "evidence": "iteration at OpenLedger::accept", + "issue_pattern": "Missing empty string validation for locals (OrderedTxs const&)", + "why_false_positive": "iteration validates locals (OrderedTxs const&) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "locals (OrderedTxs const&)", + "type", + "validation", + "check" + ], + "evidence": "iteration at OpenLedger::accept", + "issue_pattern": "Missing type validation for locals (OrderedTxs const&)", + "why_false_positive": "iteration validates locals (OrderedTxs const&) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "item.second (from locals)", + "empty", + "string", + "validation" + ], + "evidence": "app.getTxQ().apply at OpenLedger::accept", + "issue_pattern": "Missing empty string validation for item.second (from locals)", + "why_false_positive": "app.getTxQ().apply validates item.second (from locals) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "txpair.first (from next->txs)", + "empty", + "string", + "validation" + ], + "evidence": "tx->getTransactionID() at OpenLedger::accept", + "issue_pattern": "Missing empty string validation for txpair.first (from next->txs)", + "why_false_positive": "tx->getTransactionID() validates txpair.first (from next->txs) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "txpair.first (from next->txs)", + "type", + "validation", + "check" + ], + "evidence": "tx->getTransactionID() at OpenLedger::accept", + "issue_pattern": "Missing type validation for txpair.first (from next->txs)", + "why_false_positive": "tx->getTransactionID() validates txpair.first (from next->txs) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/OpenLedger.cpp", + "functions": [ + { + "args": [], + "lineno": 15, + "name": "OpenLedger::empty" + }, + { + "args": [], + "lineno": 20, + "name": "OpenLedger::current" + }, + { + "args": [ + "f" + ], + "lineno": 25, + "name": "OpenLedger::modify" + }, + { + "args": [ + "app", + "rules", + "ledger", + "locals", + "retriesFirst", + "retries", + "flags", + "suffix", + "f" + ], + "lineno": 37, + "name": "OpenLedger::accept" + }, + { + "args": [ + "rules", + "ledger" + ], + "lineno": 91, + "name": "OpenLedger::create" + }, + { + "args": [ + "app", + "view", + "tx", + "retry", + "flags", + "j" + ], + "lineno": 97, + "name": "OpenLedger::apply_one" + }, + { + "args": [ + "tx" + ], + "lineno": 110, + "name": "debugTxstr" + }, + { + "args": [ + "set" + ], + "lineno": 116, + "name": "debugTostr" + }, + { + "args": [ + "set" + ], + "lineno": 123, + "name": "debugTostr" + }, + { + "args": [ + "view" + ], + "lineno": 140, + "name": "debugTostr" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "Core OpenLedger logic is likely covered by integration and unit tests in files such as 'test/ledger/OpenLedger_test.cpp', 'test/app/ledger/AcceptLedger_test.cpp', and possibly 'test/app/misc/TxQ_test.cpp'. However, some validation paths (e.g., batch transaction relay logic, error handling for invalid pointers, and multi-threaded access) may not be fully covered, especially edge cases involving concurrent modification or rare transaction flags. LCOV_EXCL_START/STOP comments indicate some relay logic is excluded from coverage. There may be gaps in tests for error handling and assertion failures (e.g., XRPL_ASSERT).", + "validation_architecture": { + "auto_validated_fields": [ + "shared_ptr types (non-null checked by dereference)", + "reference types (must be valid)", + "callable types (checked by if (f))" + ], + "framework": "C++ type system, shared_ptr, business logic in apply/applyTxQ", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.7, + "error_thrown": "std::bad_alloc or undefined behavior if null", + "field": "ledger (std::shared_ptr)", + "location": "OpenLedger constructor", + "validated_by": "implicit type checking (shared_ptr, constness)", + "validates": [ + "ledger must be a valid shared_ptr to const Ledger" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "undefined behavior if reference is invalid", + "field": "cache (CachedSLEs&)", + "location": "OpenLedger constructor", + "validated_by": "implicit type checking (reference, non-null)", + "validates": [ + "cache must be a valid reference" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "exception if ledger is invalid or create fails", + "field": "current_ (OpenView)", + "location": "OpenLedger constructor", + "validated_by": "create(ledger->rules(), ledger)", + "validates": [ + "ledger must have valid rules", + "ledger must be valid" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (returns bool)", + "field": "current_->txCount()", + "location": "OpenLedger::empty()", + "validated_by": "txCount() method", + "validates": [ + "current_ must be valid", + "txCount() must be callable" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (returns shared_ptr)", + "field": "current_ (OpenView)", + "location": "OpenLedger::current()", + "validated_by": "std::lock_guard, shared_ptr", + "validates": [ + "current_ must be valid shared_ptr" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (skips if not callable)", + "field": "f (modify_type const&)", + "location": "OpenLedger::modify, OpenLedger::accept", + "validated_by": "callable check (if (f))", + "validates": [ + "f must be callable" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "std::bad_alloc if allocation fails", + "field": "next (OpenView)", + "location": "OpenLedger::modify", + "validated_by": "std::make_shared(*current_)", + "validates": [ + "current_ must be valid" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "undefined behavior if ledger is null", + "field": "ledger (std::shared_ptr)", + "location": "OpenLedger::accept", + "validated_by": "ledger->seq()", + "validates": [ + "ledger must be valid shared_ptr" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "none (skips if empty)", + "field": "current_->txs", + "location": "OpenLedger::accept", + "validated_by": "empty() check, iteration", + "validates": [ + "current_->txs must be iterable" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "none (skips if empty)", + "field": "locals (OrderedTxs const&)", + "location": "OpenLedger::accept", + "validated_by": "iteration", + "validates": [ + "locals must be iterable" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "exception if apply fails (not shown in this file)", + "field": "item.second (from locals)", + "location": "OpenLedger::accept", + "validated_by": "app.getTxQ().apply", + "validates": [ + "item.second must be valid transaction" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "exception if tx is null or invalid", + "field": "txpair.first (from next->txs)", + "location": "OpenLedger::accept", + "validated_by": "tx->getTransactionID()", + "validates": [ + "tx must be valid shared_ptr" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp.ai.md b/src/xrpld/app/ledger/detail/OpenLedger.cpp.ai.md new file mode 100644 index 0000000000..e15349832f --- /dev/null +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp.ai.md @@ -0,0 +1,58 @@ +# `OpenLedger.cpp` — Managing the In-Progress Ledger + +## Role in the System + +Every XRPL validator node maintains two views of ledger state: the closed, validated history, and the *open ledger* — the mutable accumulator where incoming transactions land before the next consensus round closes them into a new validated ledger. `OpenLedger` is the class that owns that accumulator. It sits between the consensus engine (which calls `accept()` when a round completes) and the transaction-processing layer (which calls `modify()` to inject individual transactions as they arrive from the network or the local queue). + +The class wraps `OpenView`, an append-only ledger view that mirrors SLE state from a parent closed ledger, and makes it safely accessible to concurrent readers and writers. + +## Two-Mutex Concurrency Design + +The single most important architectural decision in this file is the *two-mutex* layout declared in the header: + +- `modify_mutex_` — a coarse, write-time lock that serialises all calls to `modify()` and, critically, is also held for the entire transaction-application phase inside `accept()`. +- `current_mutex_` — a fine, brief lock that protects only the `current_` shared pointer itself during swap. + +The asymmetry is intentional. Reading `current()` is a single lock-acquire and pointer copy, so it is cheap and can happen at any time from any thread without blocking writers. The `modify_mutex_` does the heavy work: by holding it through the full body of `accept()`, the code ensures that any `modify()` call racing from a network I/O thread will either complete *before* `accept()` begins (and its transaction will be picked up from `current_->txs`) or wait until the new open view is published. This prevents a narrow window where a freshly submitted transaction could be accepted into neither the closing ledger nor the new open ledger. + +The short comment in the source — "Block calls to modify, otherwise new tx going into the open ledger would get lost" — is the key invariant. + +## The `accept()` Lifecycle + +`accept()` is called by the consensus layer (`RCLConsensus`) and by `NetworkOPs` (for out-of-band ledger advances) after a ledger has been built and closed. Its job is to reconstitute the open ledger on top of the new closed state. The sequence is deliberate: + +1. **Create a fresh `OpenView`** rooted on the newly closed ledger via `create()`. This wraps the ledger in a `CachedLedger`, which decorates it with an `CachedSLEs` overlay so repeated SLE lookups during transaction application are served from memory rather than the backing store. + +2. **Apply retry transactions first, outside the lock.** If `retriesFirst` is true (set when there were disputed transactions in the consensus round), the existing `retries` set is applied against the new view *before* the `modify_mutex_` is acquired. This is safe because `retries` is caller-owned; it runs outside the lock specifically to minimise the critical section. + +3. **Acquire `modify_mutex_`, then apply the previous open ledger's transactions.** All transactions from `current_->txs` are forwarded to the new view. Using `boost::adaptors::transform` extracts only the `STTx` from each pair in `current_->txs`, keeping the `apply()` template generic over any forward range of transactions. + +4. **Call the optional modifier `f`.** The caller (typically `RCLConsensus`) may supply a `modify_type` lambda to inject additional changes atomically with the view transition, e.g. to insert a fee escalation marker. + +5. **Apply local transactions via `TxQ`.** `locals` (an `OrderedTxs` alias for `CanonicalTXSet`) contains transactions originating on this node that haven't been included in a validated ledger yet. They are applied through `TxQ::apply`, which enforces queue ordering and fee logic. + +6. **Relay recovered transactions.** After all application is done, `accept()` walks `next->txs` and calls `app.getHashRouter().shouldRelay()` for each. This prevents flooding peers with transactions they already know about. Inner batch transactions (flagged with `tfInnerBatchTxn`) are explicitly skipped — they are sub-transactions of a parent batch and must not be relayed independently. The relay path is marked `LCOV_EXCL_START` because batch support is gated on a feature flag that is not active in test environments, making those branches unreachable in coverage runs. + +7. **Atomically publish the new view.** `current_mutex_` is acquired and `current_` is move-assigned from `next`, releasing the old snapshot. + +## The `modify()` Copy-on-Write Pattern + +`modify()` implements a copy-on-write approach: it copies `current_` into a fresh `OpenView`, invokes the caller's function, and only acquires `current_mutex_` to publish if the function reported changes. This means readers always see a consistent, complete snapshot via `current()` — they never observe a partially-mutated view because any mutation builds on a full copy. The pattern also makes the modification function's semantics straightforward: it receives a mutable `OpenView&`, makes changes, and returns `true` if anything changed. + +## `apply_one()` and the Retry Loop + +`apply_one()` (defined in `OpenLedger.cpp` though declared in the header alongside the template `apply()`) translates the raw `TER` result from `xrpl::apply()` into the three-way `Result` enum: `success`, `failure`, or `retry`. The mapping is: + +- **success**: transaction was applied, or the TxQ queued it (`terQUEUED`). +- **failure**: `tef`/`tem`/`tel` errors — permanently invalid; drop it. +- **retry**: everything else — might succeed later, keep it in the retry set. + +The template `apply()` in the header drives the outer retry loop. It makes up to `LEDGER_TOTAL_PASSES` (3) passes over the `retries` set, reducing to non-retry mode after `LEDGER_RETRY_PASSES` (1) pass without progress. The assertion at the end catches the invariant that after the loop ends, either retries is empty or the last pass was not a retry pass — ensuring every transaction was seen in at least one final non-retry pass. + +## Debug Helpers + +The `debugTxstr()` and three overloads of `debugTostr()` produce short hash prefixes (4 characters) for transaction sets expressed as `OrderedTxs`, `SHAMap`, or `ReadView`. They exist purely for trace-level log messages during development. The `SHAMap` overload includes a `try/catch` around deserialization because `SHAMap` items are raw bytes whose integrity is not guaranteed during debugging. + +## Construction and the `CachedLedger` Bridge + +The constructor immediately calls `create()` to produce an initial `OpenView`. `create()` wraps the caller's closed ledger in `CachedLedger`, which couples a `CachedSLEs` reference (a node-wide SLE cache) to the ledger. Every SLE access during transaction application therefore hits the node cache before going to the underlying storage, which is critical for throughput since hundreds of transactions may read the same accounts in a single open ledger cycle. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp.ai.json b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp.ai.json new file mode 100644 index 0000000000..acd51aa439 --- /dev/null +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp.ai.json @@ -0,0 +1,531 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "uint256 const& ledgerHash", + "std::unique_ptr peerSet" + ], + "lineno": 7, + "name": "SkipListAcquire" + } + ], + "code_paths": [ + { + "call_chain": [ + "SkipListAcquire::init", + "SkipListAcquire::trigger", + "peerSet_->addPeers (lambda)", + "peer->supportsFeature / peer->hasLedger", + "peerSet_->sendRequest / fallback logic" + ], + "entry_point": "SkipListAcquire::init", + "purpose": "Initializes skip list acquisition, triggers peer selection and validation, and requests data from peers or falls back to generic ledger acquisition.", + "validation_points": [ + "SkipListAcquire::isDone() (init)", + "app_.getLedgerMaster().getLedgerByHash(hash_) (trigger)", + "peer->supportsFeature(ProtocolFeature::LedgerReplay) (addPeers lambda)", + "peer->hasLedger(hash_, 0) (addPeers lambda)", + "noFeaturePeerCount_ >= MAX_NO_FEATURE_PEER_COUNT (addPeers lambda)" + ] + }, + { + "call_chain": [ + "SkipListAcquire::onTimer", + "SkipListAcquire::trigger" + ], + "entry_point": "SkipListAcquire::onTimer", + "purpose": "Handles timeouts, retries acquisition, or marks as failed after too many timeouts.", + "validation_points": [ + "timeouts_ > SUB_TASK_MAX_TIMEOUTS (onTimer)", + "See validations in trigger (as above)" + ] + }, + { + "call_chain": [ + "SkipListAcquire::processData", + "std::make_shared", + "sle->getFieldV256(sfHashes)", + "onSkipListAcquired" + ], + "entry_point": "SkipListAcquire::processData", + "purpose": "Processes incoming data, validates it, and triggers further processing if valid.", + "validation_points": [ + "XRPL_ASSERT(ledgerSeq != 0 && item) (processData)", + "isDone() (processData)", + "sle->getFieldV256(sfHashes).value() !empty (processData)" + ] + } + ], + "data_flows": [ + { + "field": "hash_", + "flow": [ + "Constructor", + "Stored as member hash_", + "Used in trigger() for ledger lookup and peer queries", + "Used in processData() for logging and data association" + ], + "origin": "Constructor argument (ledgerHash)", + "transformations": [ + "None (passed and stored as-is)" + ], + "validated_at": "app_.getLedgerMaster().getLedgerByHash(hash_) (trigger), peer->hasLedger(hash_, 0) (addPeers lambda)" + }, + { + "field": "noFeaturePeerCount_", + "flow": [ + "Incremented in addPeers lambda when peer does not support LedgerReplay", + "Compared to MAX_NO_FEATURE_PEER_COUNT to trigger fallback" + ], + "origin": "Class member, initialized to 0", + "transformations": [ + "Incremented" + ], + "validated_at": "if (++noFeaturePeerCount_ >= MAX_NO_FEATURE_PEER_COUNT) (addPeers lambda)" + }, + { + "field": "peer", + "flow": [ + "Selected by addPeers", + "Validated for supportsFeature and hasLedger", + "Used to send TMProofPathRequest if valid" + ], + "origin": "peerSet_->addPeers (lambda parameter)", + "transformations": [ + "Checked for features and ledger possession" + ], + "validated_at": "peer->supportsFeature(ProtocolFeature::LedgerReplay), peer->hasLedger(hash_, 0) (addPeers lambda)" + }, + { + "field": "item (SHAMapItem)", + "flow": [ + "Passed to processData", + "Used to construct SLE", + "SLE used to extract skip list hashes" + ], + "origin": "processData argument (from network/peer)", + "transformations": [ + "Deserialized into SLE", + "Field extracted via getFieldV256" + ], + "validated_at": "XRPL_ASSERT(ledgerSeq != 0 && item), sle->getFieldV256(sfHashes).value() !empty (processData)" + }, + { + "field": "fallBack_", + "flow": [ + "Set to true if noFeaturePeerCount_ exceeds threshold", + "Triggers fallback acquisition via inboundLedgers_.acquire" + ], + "origin": "Class member, default false", + "transformations": [ + "Set to true" + ], + "validated_at": "if (++noFeaturePeerCount_ >= MAX_NO_FEATURE_PEER_COUNT) (addPeers lambda)" + } + ], + "description": "Implements the SkipListAcquire class, which manages the acquisition of skip lists (used for ledger replay) from the network or local ledgers in the XRPL system. Handles peer selection, data retrieval, timeouts, and callback notification.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "isDone()", + "empty", + "string", + "validation" + ], + "evidence": "isDone() method at SkipListAcquire::init", + "issue_pattern": "Missing empty string validation for isDone()", + "why_false_positive": "isDone() method validates isDone() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Ledger existence (hash_)", + "empty", + "string", + "validation" + ], + "evidence": "app_.getLedgerMaster().getLedgerByHash(hash_) at SkipListAcquire::trigger", + "issue_pattern": "Missing empty string validation for Ledger existence (hash_)", + "why_false_positive": "app_.getLedgerMaster().getLedgerByHash(hash_) validates Ledger existence (hash_) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "peer supports LedgerReplay", + "empty", + "string", + "validation" + ], + "evidence": "peer->supportsFeature(ProtocolFeature::LedgerReplay) at SkipListAcquire::trigger (lambda in addPeers)", + "issue_pattern": "Missing empty string validation for peer supports LedgerReplay", + "why_false_positive": "peer->supportsFeature(ProtocolFeature::LedgerReplay) validates peer supports LedgerReplay for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "peer has ledger (hash_)", + "empty", + "string", + "validation" + ], + "evidence": "peer->hasLedger(hash_, 0) at SkipListAcquire::trigger (lambda in addPeers)", + "issue_pattern": "Missing empty string validation for peer has ledger (hash_)", + "why_false_positive": "peer->hasLedger(hash_, 0) validates peer has ledger (hash_) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "noFeaturePeerCount_", + "empty", + "string", + "validation" + ], + "evidence": "comparison with LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT at SkipListAcquire::trigger (lambda in addPeers)", + "issue_pattern": "Missing empty string validation for noFeaturePeerCount_", + "why_false_positive": "comparison with LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT validates noFeaturePeerCount_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "noFeaturePeerCount_", + "range", + "bounds", + "validation" + ], + "evidence": "comparison with LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT at SkipListAcquire::trigger (lambda in addPeers)", + "issue_pattern": "Missing range validation for noFeaturePeerCount_", + "why_false_positive": "comparison with LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT validates noFeaturePeerCount_ range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/SkipListAcquire.cpp", + "functions": [ + { + "args": [ + "Application& app", + "InboundLedgers& inboundLedgers", + "uint256 const& ledgerHash", + "std::unique_ptr peerSet" + ], + "lineno": 7, + "name": "SkipListAcquire" + }, + { + "args": [], + "lineno": 22, + "name": "~SkipListAcquire" + }, + { + "args": [ + "int numPeers" + ], + "lineno": 26, + "name": "init" + }, + { + "args": [ + "std::size_t limit", + "ScopedLockType& sl" + ], + "lineno": 34, + "name": "trigger" + }, + { + "args": [ + "bool progress", + "ScopedLockType& sl" + ], + "lineno": 74, + "name": "onTimer" + }, + { + "args": [], + "lineno": 87, + "name": "pmDowncast" + }, + { + "args": [ + "std::uint32_t ledgerSeq", + "boost::intrusive_ptr const& item" + ], + "lineno": 92, + "name": "processData" + }, + { + "args": [ + "OnSkipListDataCB&& cb" + ], + "lineno": 115, + "name": "addDataCallback" + }, + { + "args": [], + "lineno": 124, + "name": "getData" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "ScopedLockType& sl" + ], + "lineno": 130, + "name": "retrieveSkipList" + }, + { + "args": [ + "std::vector const& skipList", + "std::uint32_t ledgerSeq", + "ScopedLockType& sl" + ], + "lineno": 143, + "name": "onSkipListAcquired" + }, + { + "args": [ + "ScopedLockType& sl" + ], + "lineno": 151, + "name": "notify" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is likely tested indirectly via integration tests for ledger replay, inbound ledger acquisition, and peer selection. Look for test files in the test/ledger, test/app/ledger, or test/overlay directories, such as LedgerReplay_test.cpp, InboundLedger_test.cpp, or PeerSet_test.cpp. Direct unit tests for SkipListAcquire may be limited, especially for edge cases like fallback logic, peer feature validation, and error handling in processData. Exception handling and timer-triggered retries may not be fully covered. Manual or integration tests may be required to ensure all validation paths are exercised.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom business logic, no explicit validation framework", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "isDone()", + "location": "SkipListAcquire::init", + "validated_by": "isDone() method", + "validates": [ + "Checks if the acquisition is already complete before triggering" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "Ledger existence (hash_)", + "location": "SkipListAcquire::trigger", + "validated_by": "app_.getLedgerMaster().getLedgerByHash(hash_)", + "validates": [ + "Checks if the ledger with hash_ already exists locally before requesting from peers" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (peer is filtered out)", + "field": "peer supports LedgerReplay", + "location": "SkipListAcquire::trigger (lambda in addPeers)", + "validated_by": "peer->supportsFeature(ProtocolFeature::LedgerReplay)", + "validates": [ + "Checks if peer supports the LedgerReplay protocol feature before sending request" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (peer is filtered out)", + "field": "peer has ledger (hash_)", + "location": "SkipListAcquire::trigger (lambda in addPeers)", + "validated_by": "peer->hasLedger(hash_, 0)", + "validates": [ + "Checks if peer has the required ledger before sending request" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets fallBack_ flag and changes timer)", + "field": "noFeaturePeerCount_", + "location": "SkipListAcquire::trigger (lambda in addPeers)", + "validated_by": "comparison with LedgerReplayParameters::MAX_NO_FEATURE_PEER_COUNT", + "validates": [ + "Checks if the number of peers without LedgerReplay feature exceeds a maximum" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.cpp.ai.md b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp.ai.md new file mode 100644 index 0000000000..281c79d405 --- /dev/null +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.cpp.ai.md @@ -0,0 +1,43 @@ +# SkipListAcquire.cpp + +## Role in the Ledger Replay Subsystem + +`SkipListAcquire` is one of two sub-task types in the XRPL ledger replay system (the other being `LedgerDeltaAcquire`). Its sole responsibility is to retrieve the **skip list** for a given ledger — a special account-state object stored under `keylet::skip()` whose `sfHashes` field is a `STVector256` of ancestor ledger hashes at exponentially-spaced intervals. The `LedgerReplayTask` cannot determine which ancestor ledgers to download until it has this list, making `SkipListAcquire` the mandatory first step for every replay operation. + +`LedgerReplayer` creates one `SkipListAcquire` per unique finish-ledger hash, storing a `weak_ptr` in its `skipLists_` map. If two replay tasks share the same finish hash they also share a single `SkipListAcquire`, and both tasks register callbacks that fire when the skip list is available. + +## TimeoutCounter Base Class + +`SkipListAcquire` inherits from `TimeoutCounter`, which implements a timer-driven active-object loop. After construction, calling `init()` starts the loop: `setTimer()` arms an async timer; when it fires, `queueJob()` submits a job to the application's job queue; the job calls `invokeOnTimer()`, which calls the virtual `onTimer()` hook; if the object is still not done, the loop repeats. The concrete subclass overrides `onTimer()` to retry, give up, or take whatever action it needs. This means `SkipListAcquire` never blocks — all network interaction is asynchronous and driven by job-queue callbacks. + +The `pmDowncast()` override returning `shared_from_this()` as `weak_ptr` is the mechanism by which `TimeoutCounter` safely extends its own lifetime when scheduling jobs: it captures a weak pointer to itself, preventing use-after-free if the object is destroyed between the timer firing and the job executing. + +## Acquisition Strategy and the Fallback Path + +`trigger()` is the core logic, called both during `init()` and on each timer expiry. It has three layers: + +1. **Local short-circuit**: If the ledger is already present in `LedgerMaster`, `retrieveSkipList()` reads `sfHashes` directly from the ledger object without touching the network. This is the fast path for a node that already has the ledger cached. + +2. **LedgerReplay protocol** (primary): `peerSet_->addPeers()` selects connected peers, filtered by both `peer->supportsFeature(ProtocolFeature::LedgerReplay)` and `peer->hasLedger(hash_, 0)`. For qualifying peers, a `TMProofPathRequest` is sent requesting the `keylet::skip()` item from the account state map. This protocol returns a Merkle proof path, meaning the caller can cryptographically verify the response before passing it to `processData()`. + +3. **Generic fallback**: Peers that don't support `LedgerReplay` increment `noFeaturePeerCount_`. Once this counter reaches `MAX_NO_FEATURE_PEER_COUNT` (2), `fallBack_` is set, the timer interval is tripled from 250 ms to 1000 ms, and the acquisition falls back to `inboundLedgers_.acquire()` with `Reason::GENERIC` — a full ledger download. The timer extension is deliberate: a full ledger download takes far longer than a single proof-path response, so polling more slowly avoids unnecessary work. + +The two modes are not mutually exclusive: after `fallBack_` is set, `trigger()` still attempts to expand the peer set on each timeout (since the first `if (!fallBack_)` block is skipped, not the entire function), and `inboundLedgers_.acquire()` is also called on every `onTimer()` trigger. + +## Data Processing and Verification + +`processData()` is called by `LedgerReplayer::gotSkipList()` with data that has already been verified against the ledger hash before arrival. The method deserializes the raw `SHAMapItem` bytes into an `SLE` (Serialized Ledger Entry) using `SerialIter`, then extracts the `sfHashes` field. The entire deserialization is wrapped in a bare `catch(...)` — if anything throws, `failed_` is set and all callbacks are notified with `successful=false`. This is intentional defensive coding: malformed or unexpected network data should never crash the node, and the replay task can handle failures by giving up or retrying at a higher level. + +The `XRPL_ASSERT` at the top of `processData()` checks that `ledgerSeq != 0` and `item` is non-null — these are preconditions documented by the fact that the `LedgerReplayer` only routes verified data here. The assertion exists as a development-time guard, not a runtime safety net. + +## Callback Notification and Concurrency + +`addDataCallback()` allows any number of `LedgerReplayTask` objects to register `OnSkipListDataCB` callbacks. The design handles a subtle race: if a callback is registered after the `SkipListAcquire` has already completed (e.g., a second replay task attaches to a `SkipListAcquire` that already succeeded), `addDataCallback()` detects `isDone()` and immediately calls `notify()` rather than leaving the callback enqueued indefinitely. + +`notify()` uses an explicit unlock/re-lock around the callback invocations. It `std::swap`s the callback vector out under lock, then drops the lock, fires each callback, and re-acquires the lock before returning. This is necessary because the callbacks re-enter `LedgerReplayer` and `LedgerReplayTask`, which have their own mutexes — calling them with the `SkipListAcquire` mutex held would create a lock-ordering hazard. The swap-then-unlock pattern ensures each callback is called exactly once even if `addDataCallback()` is called concurrently. + +`TimeoutCounter` uses a `recursive_mutex` rather than a plain `mutex`. This is required because both `init()` and `onTimer()` call `trigger()` while holding the lock, and `trigger()` may in turn call `retrieveSkipList()` then `onSkipListAcquired()` then `notify()` — all inside the same lock scope. A non-recursive mutex would deadlock immediately. + +## Lifecycle and Ownership + +`LedgerReplayer` holds a `weak_ptr` in its `skipLists_` map. The strong reference is held by `LedgerReplayTask`, which stores a `shared_ptr` to keep the object alive for the duration of the task. When all replay tasks complete and release their `shared_ptr`, the `SkipListAcquire` is destroyed. The `LedgerReplayer::sweep()` operation is where the stale `weak_ptr` entries are cleaned up. This ownership model means the skip list fetcher lives exactly as long as it is needed, without requiring explicit cancellation. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.h.ai.json b/src/xrpld/app/ledger/detail/SkipListAcquire.h.ai.json new file mode 100644 index 0000000000..c1dc118f07 --- /dev/null +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.h.ai.json @@ -0,0 +1,179 @@ +{ + "args": [ + { + "lineno": 41, + "name": "app" + }, + { + "lineno": 42, + "name": "inboundLedgers" + }, + { + "lineno": 43, + "name": "ledgerHash" + }, + { + "lineno": 44, + "name": "peerSet" + }, + { + "lineno": 47, + "name": "numPeers" + }, + { + "lineno": 54, + "name": "ledgerSeq" + }, + { + "lineno": 54, + "name": "item" + }, + { + "lineno": 62, + "name": "cb" + }, + { + "lineno": 72, + "name": "progress" + }, + { + "lineno": 72, + "name": "peerSetLock" + }, + { + "lineno": 82, + "name": "limit" + }, + { + "lineno": 82, + "name": "sl" + }, + { + "lineno": 89, + "name": "ledger" + }, + { + "lineno": 89, + "name": "sl" + }, + { + "lineno": 96, + "name": "skipList" + }, + { + "lineno": 96, + "name": "ledgerSeq" + }, + { + "lineno": 96, + "name": "sl" + } + ], + "classes": [ + { + "args": [ + "app", + "inboundLedgers", + "ledgerHash", + "peerSet" + ], + "lineno": 19, + "name": "SkipListAcquire" + }, + { + "args": [ + "ledgerSeq", + "skipList" + ], + "lineno": 32, + "name": "SkipListData" + } + ], + "description": "Defines the SkipListAcquire class, which manages the retrieval of a skip list from a ledger over the network, including callback handling, peer management, and data processing.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/SkipListAcquire.h", + "functions": [ + { + "args": [ + "numPeers" + ], + "lineno": 46, + "name": "init" + }, + { + "args": [ + "ledgerSeq", + "item" + ], + "lineno": 53, + "name": "processData" + }, + { + "args": [ + "cb" + ], + "lineno": 61, + "name": "addDataCallback" + }, + { + "args": [], + "lineno": 66, + "name": "getData" + }, + { + "args": [ + "progress", + "peerSetLock" + ], + "lineno": 71, + "name": "onTimer" + }, + { + "args": [], + "lineno": 75, + "name": "pmDowncast" + }, + { + "args": [ + "limit", + "sl" + ], + "lineno": 81, + "name": "trigger" + }, + { + "args": [ + "ledger", + "sl" + ], + "lineno": 88, + "name": "retrieveSkipList" + }, + { + "args": [ + "skipList", + "ledgerSeq", + "sl" + ], + "lineno": 95, + "name": "onSkipListAcquired" + }, + { + "args": [ + "sl" + ], + "lineno": 103, + "name": "notify" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "test" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/SkipListAcquire.h.ai.md b/src/xrpld/app/ledger/detail/SkipListAcquire.h.ai.md new file mode 100644 index 0000000000..182ca95498 --- /dev/null +++ b/src/xrpld/app/ledger/detail/SkipListAcquire.h.ai.md @@ -0,0 +1,45 @@ +# `SkipListAcquire.h` — Network Acquisition of a Ledger's Skip List + +## Role in the System + +`SkipListAcquire` is a subtask in the ledger replay subsystem. When a validator needs to replay a range of historical ledgers (for gap recovery or catch-up), it first needs to know which ledger hashes form the chain it must reconstruct. XRPL encodes this information as a *skip list* — a compact, exponentially-spaced list of ancestor ledger hashes stored at the well-known `keylet::skip()` key in every ledger's state tree. This class is responsible for fetching that skip list for a specific ledger identified by its 256-bit hash, either from local storage or from the peer network. + +The class sits alongside `LedgerDeltaAcquire` (which fetches a ledger's header and transactions) and `TransactionAcquire` inside `src/xrpld/app/ledger/detail/`, sharing the same `TimeoutCounter` infrastructure. `LedgerReplayer` orchestrates both kinds of subtasks, keeping a `hash_map>` so that multiple concurrent replay tasks that need the same ledger's skip list share a single acquisition object. + +## Class Design + +`SkipListAcquire` inherits from three bases: + +- **`TimeoutCounter`** — provides the asynchronous timer loop: after construction, `init()` arms a repeating deadline timer (250 ms by default, sourced from `LedgerReplayParameters::SUB_TASK_TIMEOUT`). Each expiry calls the virtual `onTimer()` override. If progress stalls beyond `SUB_TASK_MAX_TIMEOUTS` (10) expirations the task is marked failed. The base class uses a `recursive_mutex` exposed as `mtx_` and a `ScopedLockType = std::unique_lock` that all derived classes share. +- **`std::enable_shared_from_this`** — required so `pmDowncast()` can safely hand the base class a `weak_ptr` pointing to the same control block. +- **`CountedObject`** — a lightweight diagnostic wrapper that tracks live instance counts, useful for monitoring the replay subsystem's memory footprint. + +## Acquisition Flow + +`init(numPeers)` is the entry point. It acquires the mutex, calls `trigger()` to begin the first round, then calls `setTimer()` to arm the retry clock. + +`trigger(limit, sl)` implements a **local-first** strategy: it checks `LedgerMaster::getLedgerByHash()` before going to the network. If the ledger already exists locally (e.g., the node downloaded it independently), `retrieveSkipList()` extracts the skip list from the state map via `keylet::skip()` — avoiding any peer communication. This short-circuit is important because the replay subsystem is often triggered precisely when the node is catching up and may already have the needed ledger. + +If the ledger is absent locally, `trigger()` sends a `TMProofPathRequest` network message to up to `limit` new peers, filtered to those that both claim to hold the ledger (`peer->hasLedger(hash_, 0)`) and support the `ProtocolFeature::LedgerReplay` protocol extension. The response path eventually calls `processData()` on the matching `SkipListAcquire` instance in `LedgerReplayer::gotSkipList()`. + +## Fallback to Legacy Acquisition + +A non-obvious design decision: XRPL's `LedgerReplay` feature is relatively new, and peers on the network may not support it. If `trigger()` encounters `MAX_NO_FEATURE_PEER_COUNT` (2) peers that lack the feature, it activates `fallBack_ = true`, switches the timer interval to the longer `SUB_TASK_FALLBACK_TIMEOUT` (1000 ms), and starts calling `InboundLedgers::acquire()` — the older, full-ledger download path. This is heavier but guaranteed to work with any peer. The extended timeout compensates for the fact that full ledger downloads take longer than proof-path responses. + +## Data Path and `processData()` + +When a peer responds, the overlay layer delivers verified data to `processData(ledgerSeq, item)`. The `item` is a `SHAMapItem` whose content has already been verified against the ledger hash by the caller — the note in the header makes this contract explicit. Inside, the method deserializes the item into a `SLE` (State Ledger Entry), reads the `sfHashes` field (the skip list), and calls `onSkipListAcquired()` if the list is non-empty. Any deserialization exception is caught with an empty catch block and translated into a failure, since bad data from a peer should not propagate as an exception. + +## Immutable Result and `SkipListData` + +Once acquired, the result is stored as `std::shared_ptr` — a shared pointer to an immutable `SkipListData` struct holding the ledger sequence number and the vector of hashes. The `const` here is load-bearing: because the pointer itself is `shared_ptr<... const>`, all readers get a stable snapshot without needing to hold the mutex. `getData()` simply locks, copies the shared pointer, and returns it. + +## Callback Management and `notify()` + +Multiple callers can register completion callbacks via `addDataCallback(cb)` before the task finishes. If a callback is added *after* the task has already completed, `addDataCallback` immediately calls `notify()` — handling the race gracefully rather than silently dropping the notification. + +`notify()` has a careful locking protocol to avoid re-entrant deadlocks: it swaps the entire callback vector out into a local `toCall`, calls `sl.unlock()` before invoking any of them, and relocks afterward. This means callbacks are fired outside the mutex, permitting them to call back into the `SkipListAcquire` API (e.g., `getData()`) or trigger further work without deadlocking on the `recursive_mutex`. All private methods requiring the lock accept `ScopedLockType& sl` as an explicit parameter, making the locking discipline visible at the call site rather than buried in per-method acquire/release pairs. + +## Test Seam + +The `friend class test::LedgerReplayClient` declaration grants the test harness direct access to private state, a common pattern in this codebase for integration tests that need to inject verified data or inspect internal fields without going through the full network stack. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TimeoutCounter.cpp.ai.json b/src/xrpld/app/ledger/detail/TimeoutCounter.cpp.ai.json new file mode 100644 index 0000000000..f985dc5059 --- /dev/null +++ b/src/xrpld/app/ledger/detail/TimeoutCounter.cpp.ai.json @@ -0,0 +1,258 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TimeoutCounter::TimeoutCounter" + ], + "entry_point": "TimeoutCounter::TimeoutCounter", + "purpose": "Constructs a TimeoutCounter object, initializes fields, and validates the timer interval.", + "validation_points": [ + "XRPL_ASSERT on timerInterval_" + ] + }, + { + "call_chain": [ + "TimeoutCounter::setTimer", + "TimeoutCounter::queueJob (via async_wait callback)" + ], + "entry_point": "TimeoutCounter::setTimer", + "purpose": "Sets an asynchronous timer; when it expires, attempts to queue a job.", + "validation_points": [ + "TimeoutCounter::queueJob (jobLimit check)" + ] + }, + { + "call_chain": [ + "TimeoutCounter::queueJob", + "app_.getJobQueue().addJob (if jobLimit not exceeded)", + "TimeoutCounter::invokeOnTimer (via job callback)" + ], + "entry_point": "TimeoutCounter::queueJob", + "purpose": "Queues a job if jobLimit is not exceeded; otherwise, defers by resetting the timer.", + "validation_points": [ + "Explicit jobLimit check" + ] + }, + { + "call_chain": [ + "TimeoutCounter::invokeOnTimer", + "TimeoutCounter::onTimer (not shown in code, likely virtual/overridable)", + "TimeoutCounter::setTimer (if not done)" + ], + "entry_point": "TimeoutCounter::invokeOnTimer", + "purpose": "Handles timer expiration, increments timeout count, and triggers onTimer logic.", + "validation_points": [] + }, + { + "call_chain": [ + "TimeoutCounter::cancel" + ], + "entry_point": "TimeoutCounter::cancel", + "purpose": "Cancels the TimeoutCounter, marking it as failed.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "timerInterval_", + "flow": [ + "constructor parameter", + "assigned to timerInterval_", + "validated by XRPL_ASSERT", + "used in setTimer (timer_.expires_after)" + ], + "origin": "TimeoutCounter constructor parameter: interval", + "transformations": [ + "Validated to be >10ms and <30s" + ], + "validated_at": "TimeoutCounter::TimeoutCounter (XRPL_ASSERT)" + }, + { + "field": "queueJobParameter_.jobLimit", + "flow": [ + "constructor parameter", + "assigned to queueJobParameter_", + "checked in queueJob (if jobLimit is set and job count >= jobLimit)" + ], + "origin": "TimeoutCounter constructor parameter: QueueJobParameter.jobLimit", + "transformations": [ + "Explicitly checked before queuing job" + ], + "validated_at": "TimeoutCounter::queueJob (explicit check)" + }, + { + "field": "queueJobParameter_", + "flow": [ + "constructor parameter", + "std::move into queueJobParameter_", + "used in queueJob for jobType, jobName, jobLimit" + ], + "origin": "TimeoutCounter constructor parameter: QueueJobParameter", + "transformations": [ + "Moved into member, fields accessed directly" + ], + "validated_at": "TimeoutCounter::queueJob (jobLimit check)" + }, + { + "field": "timer_", + "flow": [ + "constructed in constructor", + "used in setTimer (expires_after, async_wait)" + ], + "origin": "Constructed in TimeoutCounter::TimeoutCounter with app_.getIOContext()", + "transformations": [ + "None (standard asio timer usage)" + ], + "validated_at": "Indirectly via timerInterval_ validation" + }, + { + "field": "hash_", + "flow": [ + "constructor parameter", + "assigned to hash_", + "used in logging in invokeOnTimer and cancel" + ], + "origin": "TimeoutCounter constructor parameter: hash", + "transformations": [ + "None" + ], + "validated_at": "Not validated" + } + ], + "description": "Implements the TimeoutCounter class, which manages timed retries and job queueing for network ledger acquisition tasks in the XRPL node.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "interval (timerInterval_)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at TimeoutCounter constructor", + "issue_pattern": "Missing empty string validation for interval (timerInterval_)", + "why_false_positive": "XRPL_ASSERT macro validates interval (timerInterval_) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "interval (timerInterval_)", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT macro at TimeoutCounter constructor", + "issue_pattern": "Missing range validation for interval (timerInterval_)", + "why_false_positive": "XRPL_ASSERT macro validates interval (timerInterval_) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "queueJobParameter_.jobLimit", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in code at TimeoutCounter::queueJob", + "issue_pattern": "Missing empty string validation for queueJobParameter_.jobLimit", + "why_false_positive": "explicit check in code validates queueJobParameter_.jobLimit for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/TimeoutCounter.cpp", + "functions": [ + { + "args": [ + "Application& app", + "uint256 const& hash", + "std::chrono::milliseconds interval", + "QueueJobParameter&& jobParameter", + "beast::Journal journal" + ], + "lineno": 7, + "name": "TimeoutCounter" + }, + { + "args": [ + "ScopedLockType& sl" + ], + "lineno": 22, + "name": "setTimer" + }, + { + "args": [ + "ScopedLockType& sl" + ], + "lineno": 37, + "name": "queueJob" + }, + { + "args": [], + "lineno": 54, + "name": "invokeOnTimer" + }, + { + "args": [], + "lineno": 75, + "name": "cancel" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "TimeoutCounter is an internal detail class. Direct unit tests for this file are unlikely; instead, coverage would come from higher-level ledger or job queue tests. Look for tests in files like 'test/ledger/TimeoutCounter_test.cpp', 'test/app/ledger/LedgerMaster_test.cpp', or 'test/core/JobQueue_test.cpp'. Validation of timerInterval_ and jobLimit is only enforced at runtime, so tests would need to construct TimeoutCounter with invalid values to trigger assertions or job deferral. Gaps: No evidence of explicit negative tests for interval bounds or jobLimit enforcement; onTimer is not shown, so its testability is unknown.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), explicit C++ logic", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws, depending on XRPL_ASSERT implementation)", + "field": "interval (timerInterval_)", + "location": "TimeoutCounter constructor", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "interval must be greater than 10ms", + "interval must be less than 30s" + ], + "validation_type": "range" + }, + { + "confidence": 0.9, + "error_thrown": "None (defers job scheduling if limit exceeded)", + "field": "queueJobParameter_.jobLimit", + "location": "TimeoutCounter::queueJob", + "validated_by": "explicit check in code", + "validates": [ + "If jobLimit is set and current job count for jobType >= jobLimit, job is deferred" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TimeoutCounter.cpp.ai.md b/src/xrpld/app/ledger/detail/TimeoutCounter.cpp.ai.md new file mode 100644 index 0000000000..bcf81150bf --- /dev/null +++ b/src/xrpld/app/ledger/detail/TimeoutCounter.cpp.ai.md @@ -0,0 +1,41 @@ +# `TimeoutCounter.cpp` — Recurring Timeout Loop for Ledger Acquisition + +`TimeoutCounter` is the base class for all network ledger-acquisition tasks in the XRPL node. It solves a specific infrastructure problem: how to repeatedly attempt an asynchronous fetch (transaction sets, ledger deltas, skip lists) from network peers, safely manage the object's lifetime across timer callbacks and job-queue callbacks, apply backpressure under load, and distinguish between genuine timeouts and partial progress — all without busy-waiting or blocking threads. + +## The Async Loop + +The class comment in the header describes `TimeoutCounter` as an "active object", and the implementation in this file delivers exactly that. The loop is: + +1. `setTimer` arms a Boost.Asio `basic_waitable_timer` for `timerInterval_`. +2. When the timer fires, its async callback locks the mutex and calls `queueJob`. +3. `queueJob` posts a job to the application's `JobQueue`. If the queue is already saturated (see backpressure below), it restarts the timer instead. +4. The job queue eventually invokes the job, which calls `invokeOnTimer`. +5. `invokeOnTimer` acquires the lock, calls the pure-virtual `onTimer`, then restarts the timer via `setTimer` — unless `isDone()` is true, which terminates the loop. + +Every step checks `isDone()` before doing any real work. `isDone()` returns true when either `complete_` or `failed_` is set. This makes the loop terminate cleanly regardless of which event — success from the subtype or cancellation from outside — happens first. + +## Lifetime Safety via `pmDowncast()` + +Both the timer callback and the job callback capture only a `weak_ptr`, obtained via the pure-virtual `pmDowncast()`. Each concrete subclass (`TransactionAcquire`, `LedgerDeltaAcquire`, `SkipListAcquire`) inherits from `std::enable_shared_from_this` and implements `pmDowncast()` by returning `weak_from_this()` cast to `weak_ptr`. + +The weak-pointer pattern means that if the acquisition object is dropped by the `InboundLedgers` map while a timer or job is still in flight, the `lock()` call in the callback simply returns null and returns immediately. There is no dangling reference, and no explicit "deregister" step is required. The alternative — storing a `shared_ptr` in the lambda — would create a reference cycle that prevents the object from ever being destroyed once the loop starts. + +## Backpressure via `jobLimit` + +`queueJob` checks the optional `QueueJobParameter::jobLimit` before submitting to the `JobQueue`. If `getJobCountTotal(jobType)` is at or above the limit, the method logs a deferral message and calls `setTimer` again rather than adding another job. This is a deliberate backpressure valve: under heavy network load the node may accumulate many concurrent acquisition tasks, and piling more jobs on an already-saturated queue would only make things worse. The retry-via-timer approach lets the queue drain naturally before re-trying. + +## Progress Tracking in `invokeOnTimer` + +The `progress_` flag is the mechanism through which subtypes communicate partial forward progress to the base class. When the subtype receives some useful peer data but hasn't yet assembled a complete ledger object, it sets `progress_ = true` while holding the mutex. When `invokeOnTimer` runs next, it sees `progress_` is set, resets it to `false`, and calls `onTimer(true, sl)` — signalling that progress was made. If `progress_` is still `false`, the subtype has made no observable progress since the last tick, so `invokeOnTimer` increments `timeouts_` and calls `onTimer(false, sl)`. The subtype's `onTimer` implementation uses the boolean argument to decide whether to persist with more peers or to set `failed_ = true` and end the loop. + +## `cancel()` Is Deliberately Soft + +`cancel()` only sets `failed_ = true` under the lock. It does not call `timer_.cancel()` or attempt to remove any pending job. As the header comment notes: when the outstanding timer or job wakes up and tries to lock the weak pointer, it will call `isDone()`, see the failed state, and return immediately. This design avoids the complexity and potential races of canceling an Asio timer mid-flight, at the cost of a small delay before the loop fully stops. The approach is safe because the Boost.Asio `operation_aborted` check in `setTimer`'s lambda handles the case where the timer fires with an error code, ensuring no spurious `queueJob` call if the timer is naturally cancelled by the io_context shutting down. + +## Constructor Invariant + +The constructor asserts `timerInterval_ > 10ms && timerInterval_ < 30s`. The lower bound prevents the timer from hammering the job queue with effectively zero delay; the upper bound caps the worst-case wait before a failed acquisition is detected and retried. These bounds are wide enough to accommodate all three known subtypes but narrow enough to catch misconfigured callers at startup. + +## Relationship to Subtypes + +All three concrete subtypes — `TransactionAcquire` (SHAMap transaction sets), `LedgerDeltaAcquire` (ledger header and transactions for replay), and `SkipListAcquire` (skip-list data) — follow the same pattern: they inherit `TimeoutCounter`, implement `onTimer` and `pmDowncast`, call `setTimer` to start the loop, set `progress_` when partial data arrives, and set `complete_` when the fetch is fully satisfied. The base class owns the timing and queuing machinery; the subtypes own only the protocol-specific data handling. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TimeoutCounter.h.ai.json b/src/xrpld/app/ledger/detail/TimeoutCounter.h.ai.json new file mode 100644 index 0000000000..84dec9cad2 --- /dev/null +++ b/src/xrpld/app/ledger/detail/TimeoutCounter.h.ai.json @@ -0,0 +1,63 @@ +{ + "args": [ + { + "lineno": 51, + "name": "app" + }, + { + "lineno": 52, + "name": "targetHash" + }, + { + "lineno": 53, + "name": "timeoutInterval" + }, + { + "lineno": 54, + "name": "jobParameter" + }, + { + "lineno": 55, + "name": "journal" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "uint256 const& targetHash", + "std::chrono::milliseconds timeoutInterval", + "QueueJobParameter&& jobParameter", + "beast::Journal journal" + ], + "lineno": 29, + "name": "TimeoutCounter" + }, + { + "args": [], + "lineno": 46, + "name": "QueueJobParameter" + } + ], + "description": "Defines the TimeoutCounter class, an active object that manages asynchronous timeout and retry logic for tasks (such as fetching a ledger) using timers and job queues. Subclasses override hook methods to implement specific behavior on timeout.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/TimeoutCounter.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "cancel" + }, + { + "args": [], + "lineno": 74, + "name": "isDone" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TimeoutCounter.h.ai.md b/src/xrpld/app/ledger/detail/TimeoutCounter.h.ai.md new file mode 100644 index 0000000000..923f462d7d --- /dev/null +++ b/src/xrpld/app/ledger/detail/TimeoutCounter.h.ai.md @@ -0,0 +1,39 @@ +# `TimeoutCounter` — Asynchronous Timeout-and-Retry Base Class + +## Role in the System + +`TimeoutCounter` is the foundational "active object" for all peer-driven data acquisition tasks in the XRPL node. When the ledger subsystem needs to fetch a ledger delta, a transaction set, or a skip-list from the network, it cannot block waiting for peers to respond. Instead, each acquisition task is modeled as a `TimeoutCounter` subclass that drives itself through a recurring timer-plus-job-queue loop, retrying peer requests until the data arrives or a failure threshold is reached. + +Three concrete subclasses exist in the same `detail/` directory: `TransactionAcquire` (fetches SHAMap transaction sets from peers), `LedgerDeltaAcquire` (fetches ledger header and transactions for replay), and `SkipListAcquire` (fetches the skip-list stored inside a particular ledger). All three inherit the loop machinery from `TimeoutCounter` and only override `onTimer()` and `pmDowncast()`. + +## The Asynchronous Loop + +The loop is documented in the class header itself, and the implementation in `TimeoutCounter.cpp` reveals its full mechanics: + +1. **`setTimer(sl)`** arms a `boost::asio::basic_waitable_timer` for `timerInterval_` in the future. The completion handler captures a `weak_ptr` (obtained via the pure-virtual `pmDowncast()`), so the timer handler never extends the lifetime of the acquisition object. + +2. When the timer fires, the handler locks the object through the `weak_ptr`, acquires `mtx_`, and calls **`queueJob(sl)`**. If the job queue already holds too many concurrent jobs of the same type (checked against `QueueJobParameter::jobLimit`), `queueJob` drops back to `setTimer` rather than piling work into an overloaded queue — a lightweight admission-control mechanism. + +3. **`invokeOnTimer()`** is the private job body. It acquires the lock, checks `isDone()`, inspects the `progress_` flag, and invokes the virtual `onTimer(bool progress, ScopedLockType&)`. If `progress_` was true, it is cleared and `onTimer` is told progress was made; otherwise the `timeouts_` counter is incremented and `onTimer` hears `false`. After `onTimer` returns, if the object is still not done, `invokeOnTimer` immediately arms the next timer by calling `setTimer` again. + +This sequencing means there is at most one live timer and one queued job at any moment; the chain is strict and self-restarting. + +## The `progress_` Flag and Backpressure + +The class deliberately runs two concurrent async streams: the timeout loop and whatever peer-communication logic the subclass implements to actually acquire data. Subclasses set `progress_ = true` whenever they receive a partial response that moves them closer to completion. `invokeOnTimer` then clears the flag and passes the `progress` boolean to `onTimer`, giving the subclass a chance to extend the deadline rather than immediately mark the task as failed. This pattern avoids premature failure on slow-but-alive peers while still catching completely silent peers within a bounded number of additional intervals. + +## Terminal State and Cancellation + +`isDone()` returns true when either `complete_` or `failed_` is set. Both `invokeOnTimer` and `queueJob` check `isDone()` at entry and short-circuit immediately, ensuring that once a task concludes no further timer arms or job submissions occur. The public `cancel()` method sets `failed_` under the lock but — crucially — **does not cancel the outstanding timer or dequeue the pending job**. The comment in the header explains why: cancelling a Boost ASIO timer or removing a queued job is complex and error-prone; instead, the next time either fires they see `isDone() == true` and return early. This is a deliberate "lazy cancellation" design that sacrifices one superfluous timer expiry in exchange for significantly simpler state management. + +## Lifetime Safety via `pmDowncast()` + +All subclasses also inherit `std::enable_shared_from_this`. The pure-virtual `pmDowncast()` returns a `std::weak_ptr` by calling `weak_from_this()` on the concrete derived type. This is necessary because `enable_shared_from_this` is templated on the derived type, not on `TimeoutCounter` directly. Every timer completion handler and job closure captures only this `weak_ptr`, so if the acquisition object is destroyed before the callback fires (e.g., the inbound-ledger manager gave up on it), the `wptr.lock()` call in the handler returns `nullptr` and the callback exits cleanly without accessing a dangling object. + +## Locking Model + +The lock type is `std::unique_lock`. Recursive mutexes are generally suspect, but here they are justified: `setTimer` and `queueJob` are called both from external paths (with the lock already held by the caller) and from within the locked `invokeOnTimer`, making a non-recursive mutex deadlock. The public `ScopedLockType` alias is exposed to subclasses so their `onTimer` implementations can call base-class helpers (`setTimer`, `queueJob`) without worrying about double-locking — the recursion handles it. The lock is passed by reference through the entire call stack, making the locking discipline explicit. + +## Construction Invariants + +The constructor asserts `timerInterval_ > 10ms && timerInterval_ < 30s`. This enforces a reasonable operating range: a sub-10ms interval would flood the job queue, while a 30-second-or-longer interval suggests misconfiguration. The `QueueJobParameter` struct bundles the `JobType`, a display name, and an optional concurrency cap together so each subclass can declare its job-queue identity in one place. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.cpp.ai.json b/src/xrpld/app/ledger/detail/TransactionAcquire.cpp.ai.json new file mode 100644 index 0000000000..242a28e2f8 --- /dev/null +++ b/src/xrpld/app/ledger/detail/TransactionAcquire.cpp.ai.json @@ -0,0 +1,614 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TransactionAcquire::onTimer", + "TransactionAcquire::trigger", + "TransactionAcquire::done" + ], + "entry_point": "TransactionAcquire::onTimer", + "purpose": "Handles periodic timeout events for transaction set acquisition, triggers peer requests or marks as failed/complete.", + "validation_points": [ + "onTimer: if (timeouts_ > MAX_TIMEOUTS)", + "onTimer: if (timeouts_ >= NORM_TIMEOUTS)", + "trigger: if (complete_)", + "trigger: if (failed_)", + "trigger: if (!mHaveRoot)", + "trigger: if (!mMap->isValid())" + ] + }, + { + "call_chain": [ + "TransactionAcquire::trigger", + "TransactionAcquire::done" + ], + "entry_point": "TransactionAcquire::trigger", + "purpose": "Handles the logic for requesting missing transaction set nodes from peers, or marks acquisition as complete/failed.", + "validation_points": [ + "trigger: if (complete_)", + "trigger: if (failed_)", + "trigger: if (!mHaveRoot)", + "trigger: if (!mMap->isValid())" + ] + }, + { + "call_chain": [ + "TransactionAcquire::done" + ], + "entry_point": "TransactionAcquire::done", + "purpose": "Finalizes the acquisition process, either marking as failed or passing the acquired set to the application.", + "validation_points": [ + "done: if (failed_)" + ] + } + ], + "data_flows": [ + { + "field": "timeouts_", + "flow": [ + "TimeoutCounter (base class)", + "TransactionAcquire::onTimer (checks and increments)", + "TransactionAcquire::trigger (affects query type)" + ], + "origin": "Inherited from TimeoutCounter, incremented on each timer event", + "transformations": [ + "Incremented on each timeout", + "Compared to NORM_TIMEOUTS and MAX_TIMEOUTS for validation" + ], + "validated_at": "onTimer: if (timeouts_ > MAX_TIMEOUTS), if (timeouts_ >= NORM_TIMEOUTS)" + }, + { + "field": "complete_", + "flow": [ + "TransactionAcquire::trigger (set when mMap->isValid() and no missing nodes)", + "TransactionAcquire::done (checked for logging)" + ], + "origin": "TransactionAcquire member, set to true when acquisition is complete", + "transformations": [ + "Set to true when acquisition is complete" + ], + "validated_at": "trigger: if (complete_)" + }, + { + "field": "failed_", + "flow": [ + "TransactionAcquire::onTimer (set if timeouts_ > MAX_TIMEOUTS)", + "TransactionAcquire::trigger (set if mMap is invalid or other failure)", + "TransactionAcquire::done (checked for logging)" + ], + "origin": "TransactionAcquire member, set to true on failure", + "transformations": [ + "Set to true on failure" + ], + "validated_at": "onTimer: if (timeouts_ > MAX_TIMEOUTS), trigger: if (failed_)" + }, + { + "field": "mHaveRoot", + "flow": [ + "TransactionAcquire::trigger (checked to determine if root node is present)" + ], + "origin": "TransactionAcquire member, set when root node is acquired", + "transformations": [ + "Set when root node is acquired" + ], + "validated_at": "trigger: if (!mHaveRoot)" + }, + { + "field": "mMap", + "flow": [ + "TransactionAcquire::trigger (checked for validity, used to get missing nodes)", + "TransactionAcquire::done (set immutable, passed to giveSet)" + ], + "origin": "Constructed in TransactionAcquire constructor as new SHAMap", + "transformations": [ + "Built up as nodes are acquired", + "Set immutable when complete" + ], + "validated_at": "trigger: if (!mMap->isValid())" + } + ], + "description": "Implements the TransactionAcquire class, which manages the acquisition of transaction sets (SHAMap) from peers in the XRPL network, handling timeouts, peer requests, and node data integration for consensus operations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "peerSet (via unique_ptr)", + "validation", + "missing", + "check" + ], + "evidence": "Field peerSet (via unique_ptr) validated by C++ type system, business logic checks, SHAMap/ProtoBuf for hash", + "issue_pattern": "Missing validation for peerSet (via unique_ptr)", + "why_false_positive": "C++ type system, business logic checks, SHAMap/ProtoBuf for hash validates peerSet (via unique_ptr) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "hash (via uint256 type)", + "validation", + "missing", + "check" + ], + "evidence": "Field hash (via uint256 type) validated by C++ type system, business logic checks, SHAMap/ProtoBuf for hash", + "issue_pattern": "Missing validation for hash (via uint256 type)", + "why_false_positive": "C++ type system, business logic checks, SHAMap/ProtoBuf for hash validates hash (via uint256 type) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "timeouts_", + "empty", + "string", + "validation" + ], + "evidence": "if (timeouts_ > MAX_TIMEOUTS) at onTimer", + "issue_pattern": "Missing empty string validation for timeouts_", + "why_false_positive": "if (timeouts_ > MAX_TIMEOUTS) validates timeouts_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "timeouts_", + "range", + "bounds", + "validation" + ], + "evidence": "if (timeouts_ > MAX_TIMEOUTS) at onTimer", + "issue_pattern": "Missing range validation for timeouts_", + "why_false_positive": "if (timeouts_ > MAX_TIMEOUTS) validates timeouts_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "timeouts_", + "empty", + "string", + "validation" + ], + "evidence": "if (timeouts_ >= NORM_TIMEOUTS) at onTimer", + "issue_pattern": "Missing empty string validation for timeouts_", + "why_false_positive": "if (timeouts_ >= NORM_TIMEOUTS) validates timeouts_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "timeouts_", + "range", + "bounds", + "validation" + ], + "evidence": "if (timeouts_ >= NORM_TIMEOUTS) at onTimer", + "issue_pattern": "Missing range validation for timeouts_", + "why_false_positive": "if (timeouts_ >= NORM_TIMEOUTS) validates timeouts_ range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "complete_", + "empty", + "string", + "validation" + ], + "evidence": "if (complete_) at trigger", + "issue_pattern": "Missing empty string validation for complete_", + "why_false_positive": "if (complete_) validates complete_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "failed_", + "empty", + "string", + "validation" + ], + "evidence": "if (failed_) at trigger", + "issue_pattern": "Missing empty string validation for failed_", + "why_false_positive": "if (failed_) validates failed_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mHaveRoot", + "empty", + "string", + "validation" + ], + "evidence": "if (!mHaveRoot) at trigger", + "issue_pattern": "Missing empty string validation for mHaveRoot", + "why_false_positive": "if (!mHaveRoot) validates mHaveRoot for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "peerSet", + "empty", + "string", + "validation" + ], + "evidence": "std::move(peerSet) into mPeerSet at TransactionAcquire constructor", + "issue_pattern": "Missing empty string validation for peerSet", + "why_false_positive": "std::move(peerSet) into mPeerSet validates peerSet for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "peerSet", + "type", + "validation", + "check" + ], + "evidence": "std::move(peerSet) into mPeerSet at TransactionAcquire constructor", + "issue_pattern": "Missing type validation for peerSet", + "why_false_positive": "std::move(peerSet) into mPeerSet validates peerSet type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "hash", + "empty", + "string", + "validation" + ], + "evidence": "passed to SHAMap and set_ledgerhash at TransactionAcquire constructor, trigger", + "issue_pattern": "Missing empty string validation for hash", + "why_false_positive": "passed to SHAMap and set_ledgerhash validates hash for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/TransactionAcquire.cpp", + "functions": [ + { + "args": [ + "Application& app", + "uint256 const& hash", + "std::unique_ptr peerSet" + ], + "lineno": 18, + "name": "TransactionAcquire" + }, + { + "args": [], + "lineno": 29, + "name": "done" + }, + { + "args": [ + "bool progress", + "ScopedLockType& psl" + ], + "lineno": 51, + "name": "onTimer" + }, + { + "args": [], + "lineno": 65, + "name": "pmDowncast" + }, + { + "args": [ + "std::shared_ptr const& peer" + ], + "lineno": 70, + "name": "trigger" + }, + { + "args": [ + "std::vector> const& data", + "std::shared_ptr const& peer" + ], + "lineno": 110, + "name": "takeNodes" + }, + { + "args": [ + "std::size_t limit" + ], + "lineno": 163, + "name": "addPeers" + }, + { + "args": [ + "int numPeers" + ], + "lineno": 171, + "name": "init" + }, + { + "args": [], + "lineno": 179, + "name": "stillNeed" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "TransactionAcquire is a core ledger acquisition component. Typical test coverage would be in integration/system tests for ledger sync and transaction set acquisition, likely in test files under 'src/test/ledger/' or 'src/test/app/'. Direct unit tests for TransactionAcquire may be limited due to its reliance on network and peer infrastructure. Validation paths (timeouts_, complete_, failed_, mHaveRoot) are likely exercised indirectly via tests that simulate timeouts, peer failures, and successful/failed acquisition. Gaps may exist in direct unit testing of edge cases (e.g., MAX_TIMEOUTS boundary, mMap invalidity, root node missing). Mocking or test harnesses would be needed for full coverage.", + "validation_architecture": { + "auto_validated_fields": [ + "peerSet (via unique_ptr)", + "hash (via uint256 type)" + ], + "framework": "C++ type system, business logic checks, SHAMap/ProtoBuf for hash", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (sets failed_ = true, calls done())", + "field": "timeouts_", + "location": "onTimer", + "validated_by": "if (timeouts_ > MAX_TIMEOUTS)", + "validates": [ + "timeouts_ does not exceed MAX_TIMEOUTS (20)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (calls trigger(nullptr))", + "field": "timeouts_", + "location": "onTimer", + "validated_by": "if (timeouts_ >= NORM_TIMEOUTS)", + "validates": [ + "timeouts_ is at least NORM_TIMEOUTS (4)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (logs and returns)", + "field": "complete_", + "location": "trigger", + "validated_by": "if (complete_)", + "validates": [ + "prevents trigger if already complete" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (logs and returns)", + "field": "failed_", + "location": "trigger", + "validated_by": "if (failed_)", + "validates": [ + "prevents trigger if already failed" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (controls message sending)", + "field": "mHaveRoot", + "location": "trigger", + "validated_by": "if (!mHaveRoot)", + "validates": [ + "checks if root node is present before requesting ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (relies on std::unique_ptr semantics)", + "field": "peerSet", + "location": "TransactionAcquire constructor", + "validated_by": "std::move(peerSet) into mPeerSet", + "validates": [ + "peerSet must be a valid unique_ptr" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (relies on uint256 type and SHAMap/ProtoBuf validation)", + "field": "hash", + "location": "TransactionAcquire constructor, trigger", + "validated_by": "passed to SHAMap and set_ledgerhash", + "validates": [ + "hash must be a valid uint256" + ], + "validation_type": "type/format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.cpp.ai.md b/src/xrpld/app/ledger/detail/TransactionAcquire.cpp.ai.md new file mode 100644 index 0000000000..67537d3f9e --- /dev/null +++ b/src/xrpld/app/ledger/detail/TransactionAcquire.cpp.ai.md @@ -0,0 +1,53 @@ +# TransactionAcquire.cpp + +## Role in the System + +During XRPL consensus, every validating node must hold the same candidate transaction set — a `SHAMap` identified by a 256-bit hash — before it can vote. When a node learns about a transaction set it doesn't yet possess, `TransactionAcquire` is the object that orchestrates its retrieval from the peer network. It knows nothing about ledger accounting or consensus logic; its sole job is to reconstruct a complete, valid `SHAMap` by fetching missing tree nodes from connected peers, then hand the finished map off to `InboundTransactions::giveSet()`. + +## Inheritance and Ownership + +`TransactionAcquire` inherits from `TimeoutCounter`, which wires an Boost.ASIO steady timer to the job queue. Every `TX_ACQUIRE_TIMEOUT` (250 ms) the job queue fires `onTimer()`, letting the object either retry failed peer requests or declare the acquisition permanently failed. The class also inherits `enable_shared_from_this` so that it can safely produce `weak_ptr` for the timer callback — avoiding a use-after-free if the object is destroyed between the timer firing and the callback running. + +The `PeerSet` is owned exclusively via `unique_ptr` inside the object. This is the overlay abstraction that knows how to select peers and send `TMGetLedger` protobuf messages to them; `TransactionAcquire` never manages peer connections directly. + +## SHAMap Construction Strategy + +The acquired map is created in the constructor as an in-memory, `setUnbacked()` `SHAMap` of type `TRANSACTION`. Marking it unbacked is crucial: it tells the map not to write newly received nodes into the node database (the SQLite or RocksDB store), because this is a transient consensus artifact, not a persisted ledger object. + +The acquisition proceeds in two phases, enforced by the `mHaveRoot` flag: + +1. **Root phase.** Until the root node is received, `trigger()` sends a `TMGetLedger` message with `liTS_CANDIDATE` type, requesting the root node explicitly (`SHAMapNodeID().getRawString()` encodes the root). A `querydepth` of 3 hints to the responding peer that the requester probably needs the entire subtree, enabling bulk delivery. + +2. **Node-fill phase.** Once the root is present, `trigger()` calls `mMap->getMissingNodes(256, &sf)` to enumerate up to 256 missing interior or leaf nodes and requests them all in a single message. This repeats every timeout cycle or whenever new data arrives, until `getMissingNodes` returns an empty list. + +This two-phase design is necessary because a `SHAMap` cannot enumerate missing descendants without a root node. Requesting the root first gives the map enough structure to traverse its gaps. + +## Data Ingestion: `takeNodes()` + +`takeNodes()` is called by the network layer when a peer responds with node data. It holds the object's `recursive_mutex` for its entire duration, preventing concurrent timer events from interfering with the map being modified. + +For each incoming `(SHAMapNodeID, Slice)` pair, the method routes to either `addRootNode()` or `addKnownNode()`, updating `mHaveRoot` on the first successful root addition. `ConsensusTransSetSF` is passed as the sync filter: it consults the application's `TempNodeCache` on `getNode()` (so locally cached nodes don't need re-fetching) and populates that cache on `gotNode()` (so future fetches can be served locally). If any node is rejected by the map, the method returns `SHAMapAddNode::invalid()`, signalling a misbehaving peer to the caller. + +After processing the batch, `takeNodes()` calls `trigger(peer)` on the same peer to immediately ask for the next wave of missing nodes. This keeps the pipeline full rather than waiting for the next 250 ms timeout. + +## Timeout and Retry Policy + +`onTimer()` implements a two-threshold policy controlled by the constants `NORM_TIMEOUTS = 4` and `MAX_TIMEOUTS = 20`: + +- Below 4 timeouts, only `addPeers(1)` is called — one additional peer is recruited on each cycle, progressively widening the query surface without flooding the network immediately. +- At or after 4 timeouts, `trigger(nullptr)` is also called, broadcasting the request to all peers in the current set rather than targeting a specific one. +- After 20 timeouts (~5 seconds of 250 ms intervals) with no completion, `failed_` is set and `done()` is called. + +When `timeouts_ >= NORM_TIMEOUTS`, the outbound `TMGetLedger` messages also set `qtINDIRECT`, instructing the receiving peer to relay the request to peers it knows that have the data. This is an escalation from direct peer-to-peer retrieval to a gossip-style fan-out. + +## `done()` and the Lock Constraint + +A key comment in `done()` explains a subtle constraint: `done()` is called while still holding the `PeerSet` lock, so it cannot perform "real work" directly. Instead, if acquisition succeeded, it makes the map immutable (`setImmutable()`) and schedules a `jtTXN_DATA` job on the application job queue to call `InboundTransactions::giveSet()`. The captures are by value — `hash` and `map` as `shared_ptr` — ensuring the data remains valid even if the `TransactionAcquire` object is destroyed before the job runs. The comment explicitly acknowledges that job queue rejection during shutdown is acceptable: `giveSet()` is only needed to update in-progress consensus state. + +## `stillNeed()`: Reset Without Restart + +`stillNeed()` is called when consensus determines it still requires a transaction set that was previously being acquired but may have timed out. Rather than destroying and recreating the `TransactionAcquire`, this method clamps `timeouts_` back down to `NORM_TIMEOUTS` and clears `failed_`, effectively granting the acquisition more time without losing any nodes already collected in `mMap`. This avoids redundant network traffic and is safe because the map was `setUnbacked()` — no database state needs to be rolled back. + +## Concurrency Summary + +The `recursive_mutex mtx_` (inherited from `TimeoutCounter`) protects `mHaveRoot`, `mMap`, `complete_`, `failed_`, and `timeouts_` against concurrent access by the timer job and the network I/O thread calling `takeNodes()`. The `pmDowncast()` override returning a `weak_ptr` is the mechanism by which the timer avoids holding a strong reference — if consensus discards the `TransactionAcquire` before the timer fires, the weak pointer expires and the callback silently no-ops. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.h.ai.json b/src/xrpld/app/ledger/detail/TransactionAcquire.h.ai.json new file mode 100644 index 0000000000..fd23363265 --- /dev/null +++ b/src/xrpld/app/ledger/detail/TransactionAcquire.h.ai.json @@ -0,0 +1,91 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app", + "uint256 const& hash", + "std::unique_ptr peerSet" + ], + "lineno": 10, + "name": "TransactionAcquire" + } + ], + "description": "Defines the TransactionAcquire class, which manages the acquisition of a transaction set (SHAMap) from peers in the XRPL network, handling peer coordination, timeouts, and data collection.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/TransactionAcquire.h", + "functions": [ + { + "args": [ + "Application& app", + "uint256 const& hash", + "std::unique_ptr peerSet" + ], + "lineno": 13, + "name": "TransactionAcquire" + }, + { + "args": [], + "lineno": 14, + "name": "~TransactionAcquire" + }, + { + "args": [ + "std::vector> const& data", + "std::shared_ptr const&" + ], + "lineno": 16, + "name": "takeNodes" + }, + { + "args": [ + "int startPeers" + ], + "lineno": 21, + "name": "init" + }, + { + "args": [], + "lineno": 24, + "name": "stillNeed" + }, + { + "args": [ + "bool progress", + "ScopedLockType& peerSetLock" + ], + "lineno": 31, + "name": "onTimer" + }, + { + "args": [], + "lineno": 34, + "name": "done" + }, + { + "args": [ + "std::size_t limit" + ], + "lineno": 37, + "name": "addPeers" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 39, + "name": "trigger" + }, + { + "args": [], + "lineno": 40, + "name": "pmDowncast" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TransactionAcquire.h.ai.md b/src/xrpld/app/ledger/detail/TransactionAcquire.h.ai.md new file mode 100644 index 0000000000..7b300dcef3 --- /dev/null +++ b/src/xrpld/app/ledger/detail/TransactionAcquire.h.ai.md @@ -0,0 +1,54 @@ +# `TransactionAcquire` — Peer-Driven Transaction Set Fetcher + +## Role in the System + +During XRPL consensus, every validating node must agree on an identical set of candidate transactions before proposing and finalizing a ledger. When a node has not yet seen a particular transaction set — identified by the `uint256` hash of its `SHAMap` root — it must reconstruct that map by querying connected peers for its nodes. `TransactionAcquire` is the object that drives this retrieval from start to finish. + +The class sits inside `xrpld/app/ledger/detail/`, keeping it an implementation detail of the ledger acquisition subsystem. Its lifetime is managed entirely by `InboundTransactions`, which creates one instance per missing transaction set hash and delivers incoming peer data to it. + +## Inheritance and Design Shape + +`TransactionAcquire` inherits from three bases: + +- **`TimeoutCounter`** — provides the asynchronous timer loop, the mutex (`mtx_`), and the terminal state flags (`complete_`, `failed_`, `progress_`). It repeatedly fires `onTimer()` at 250 ms intervals until the object reports `isDone()`. +- **`std::enable_shared_from_this`** — needed because `pmDowncast()` must hand a `std::weak_ptr` back to the base without slicing. `TimeoutCounter` cannot call `shared_from_this()` directly; each concrete subclass implements `pmDowncast()` to return its own weak pointer, which `TransactionAcquire::pmDowncast()` satisfies via `shared_from_this()`. +- **`CountedObject`** — injects diagnostic object-count tracking at no runtime cost. + +The `PeerSet` is injected as a `std::unique_ptr` via the constructor, enabling test doubles and clean ownership semantics — `TransactionAcquire` alone owns it. + +## SHAMap Retrieval Strategy + +The constructor creates an empty `SHAMap` of type `TRANSACTION`, keyed by the target hash, and immediately calls `setUnbacked()`. This is the critical first act: it tells the map that its nodes need not be written to any persistent node store. The map is purely an in-memory reconstruction vehicle; once complete, it is handed off and discarded. + +Retrieval proceeds in two phases controlled by the `mHaveRoot` boolean: + +1. **Root phase**: Until the root node arrives, `trigger()` sends a `TMGetLedger` message with `liTS_CANDIDATE` type and `querydepth=3`. Requesting depth 3 at the outset is deliberate — the sender will likely include interior nodes proactively, reducing round trips for small transaction sets. + +2. **Interior-node phase**: Once `mHaveRoot` is set, `trigger()` calls `mMap->getMissingNodes(256, &sf)` using a `ConsensusTransSetSF` sync filter. The filter mediates between the low-level map sync code and the application's temporary node cache, allowing nodes to be found in cache before hitting the network. Up to 256 missing `SHAMapNodeID`s are batched into a single `TMGetLedger` request. + +## Data Ingestion: `takeNodes()` + +`takeNodes()` is the sole entry point for peer-delivered data. It acquires the mutex, validates terminal state (returning silently if already complete or failed), then iterates the incoming `(SHAMapNodeID, Slice)` pairs: + +- Root nodes go through `addRootNode()`, which validates the hash matches the expected `SHAMapHash{hash_}`. +- Non-root nodes go through `addKnownNode()`, which also runs the sync filter. + +A malformed node causes an early `SHAMapAddNode::invalid()` return without poisoning the whole acquisition — except for non-root bad nodes, which do abort with `invalid()`. After integrating the data, `trigger()` is called again on the responding peer, immediately requesting whatever the map still needs. This pipeline of receive→request drives rapid convergence when a peer has the full set. + +## Timeout and Peer Escalation + +`onTimer()` implements a two-tier failure policy using two constants: `NORM_TIMEOUTS = 4` (1 second) and `MAX_TIMEOUTS = 20` (5 seconds). Before reaching `NORM_TIMEOUTS`, the timer just adds one more peer via `addPeers(1)`. After `NORM_TIMEOUTS`, it also calls `trigger(nullptr)`, which sends a `qtINDIRECT` query — asking the contacted peer to relay the request deeper into the network rather than only checking itself. After `MAX_TIMEOUTS`, `failed_` is set and `done()` is called. + +`addPeers()` delegates to `PeerSet::addPeers()` with two lambdas: a selection predicate (`peer->hasTxSet(hash_)`) and an on-add callback (`trigger(peer)`). This ensures only peers advertising the transaction set are contacted, and each newly added peer is immediately triggered. + +## Completion and the Lock-Constrained `done()` + +When `trigger()` finds no missing nodes and the map is valid, it sets `complete_ = true` and calls `done()`. The comment in `done()` is architecturally important: it is called while holding the `PeerSet` lock, so it cannot safely call arbitrary application code. The solution is to schedule a `jtTXN_DATA` job that calls `app_.getInboundTransactions().giveSet(hash, map, true)`. This deferred handoff integrates the completed `SHAMap` into the consensus state without creating lock-order hazards. + +## `stillNeed()` — Preventing Premature Abandonment + +`stillNeed()` resets `timeouts_` back to `NORM_TIMEOUTS` and clears `failed_`. It exists because consensus may re-request a transaction set that a previous acquisition attempt failed to complete — for example, when a delayed proposal references the same set ID. Without this escape hatch, a first failed attempt would permanently poison the acquisition object for that hash. By clamping timeouts to the lower bound and clearing failure, the object gets a fresh window of attempts. + +## Relationship to `InboundTransactions` + +`TransactionAcquire` does not manage its own registration or deregistration. `InboundTransactions` is responsible for creating instances, routing `TMGetLedger` responses to them via `gotData()`, and processing the completed `SHAMap` via `giveSet()`. The rename comment in the header (`// VFALCO TODO rename to PeerTxRequest`) reflects a recognized naming inconsistency: the class acquires a transaction *set*, not a single transaction, so the name borrowed from ledger acquisition is slightly misleading. \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TransactionMaster.cpp.ai.json b/src/xrpld/app/ledger/detail/TransactionMaster.cpp.ai.json new file mode 100644 index 0000000000..be0cb5eaa6 --- /dev/null +++ b/src/xrpld/app/ledger/detail/TransactionMaster.cpp.ai.json @@ -0,0 +1,465 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app" + ], + "lineno": 11, + "name": "TransactionMaster" + } + ], + "code_paths": [ + { + "call_chain": [ + "TransactionMaster::fetch", + "TransactionMaster::fetch_from_cache", + "Transaction::isValidated", + "Transaction::load" + ], + "entry_point": "TransactionMaster::fetch(uint256 const& txnID, error_code_i& ec)", + "purpose": "Fetches a transaction by ID, first from cache, then from persistent storage if not validated.", + "validation_points": [ + "txn->isValidated() (checks if cached transaction is validated)", + "Transaction::load (validates txnID and loads transaction from storage)" + ] + }, + { + "call_chain": [ + "TransactionMaster::fetch", + "TransactionMaster::fetch_from_cache", + "Transaction::isValidated", + "Transaction::load" + ], + "entry_point": "TransactionMaster::fetch(uint256 const& txnID, ClosedInterval const& range, error_code_i& ec)", + "purpose": "Fetches a transaction by ID within a ledger range, with similar validation as above.", + "validation_points": [ + "txn->isValidated()", + "Transaction::load (with range)" + ] + }, + { + "call_chain": [ + "TransactionMaster::fetch", + "TransactionMaster::fetch_from_cache", + "if (!iTx) { ... }", + "SerialIter, getVL, STTx constructor" + ], + "entry_point": "TransactionMaster::fetch(boost::intrusive_ptr const& item, SHAMapNodeType type, std::uint32_t uCommitLedger)", + "purpose": "Fetches a transaction from a SHAMapItem, validates type and parses transaction data.", + "validation_points": [ + "type == SHAMapNodeType::tnTRANSACTION_NM / tnTRANSACTION_MD (validates type)", + "SerialIter, getVL, STTx constructor (validates item->slice() data)" + ] + }, + { + "call_chain": [ + "TransactionMaster::inLedger", + "TransactionMaster::fetch_from_cache", + "txn->setStatus" + ], + "entry_point": "TransactionMaster::inLedger(uint256 const& hash, std::uint32_t ledger, std::optional tseq, std::optional netID)", + "purpose": "Checks if a transaction is in a specific ledger and updates its status.", + "validation_points": [ + "txn != nullptr (existence check, not deep validation)" + ] + } + ], + "data_flows": [ + { + "field": "txnID (transaction hash)", + "flow": [ + "Input parameter", + "fetch_from_cache (cache lookup)", + "If not found or not validated, Transaction::load (persistent storage)", + "If loaded, canonicalize_replace_client (cache update)" + ], + "origin": "Input parameter to fetch/inLedger functions", + "transformations": [ + "Used as cache key", + "Passed to Transaction::load for DB lookup", + "Canonicalized in cache" + ], + "validated_at": "Transaction::load (validates existence and correctness of txnID)" + }, + { + "field": "txn (Transaction object)", + "flow": [ + "fetch_from_cache", + "If found, txn->isValidated()", + "If not validated, returned as-is", + "If loaded, canonicalize_replace_client" + ], + "origin": "Fetched from cache or loaded from storage", + "transformations": [ + "Status may be updated (setStatus)", + "Canonicalized in cache" + ], + "validated_at": "txn->isValidated()" + }, + { + "field": "item->slice() (SHAMapItem data)", + "flow": [ + "item->slice()", + "SerialIter constructed from slice", + "If type == tnTRANSACTION_NM: STTx constructed from SerialIter", + "If type == tnTRANSACTION_MD: getVL() extracts blob, new SerialIter, STTx constructed" + ], + "origin": "SHAMapItem passed to fetch", + "transformations": [ + "Parsed by SerialIter", + "Possibly getVL() extracts variable-length data", + "Constructed into STTx" + ], + "validated_at": "SerialIter, getVL, STTx constructor (parsing and validation of binary data)" + }, + { + "field": "type (SHAMapNodeType)", + "flow": [ + "Input parameter", + "if/else if (type == ...)", + "Determines parsing logic for item->slice()" + ], + "origin": "Input parameter to fetch(SHAMapItem, type, ...)", + "transformations": [ + "Controls code path for parsing" + ], + "validated_at": "if (type == SHAMapNodeType::tnTRANSACTION_NM) / else if (type == SHAMapNodeType::tnTRANSACTION_MD)" + } + ], + "description": "Implements the TransactionMaster class, which manages transaction caching, retrieval, and canonicalization for the XRPL application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "txnID (type: uint256)", + "validation", + "missing", + "check" + ], + "evidence": "Field txnID (type: uint256) validated by Custom (no external validation framework detected)", + "issue_pattern": "Missing validation for txnID (type: uint256)", + "why_false_positive": "Custom (no external validation framework detected) validates txnID (type: uint256) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "type (SHAMapNodeType enum)", + "validation", + "missing", + "check" + ], + "evidence": "Field type (SHAMapNodeType enum) validated by Custom (no external validation framework detected)", + "issue_pattern": "Missing validation for type (SHAMapNodeType enum)", + "why_false_positive": "Custom (no external validation framework detected) validates type (SHAMapNodeType enum) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "item->slice() (binary format for STTx)", + "validation", + "missing", + "check" + ], + "evidence": "Field item->slice() (binary format for STTx) validated by Custom (no external validation framework detected)", + "issue_pattern": "Missing validation for item->slice() (binary format for STTx)", + "why_false_positive": "Custom (no external validation framework detected) validates item->slice() (binary format for STTx) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "txn (Transaction object from cache)", + "empty", + "string", + "validation" + ], + "evidence": "txn->isValidated() at TransactionMaster::fetch (both overloads)", + "issue_pattern": "Missing empty string validation for txn (Transaction object from cache)", + "why_false_positive": "txn->isValidated() validates txn (Transaction object from cache) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "txnID (transaction hash)", + "empty", + "string", + "validation" + ], + "evidence": "Transaction::load at TransactionMaster::fetch (both overloads)", + "issue_pattern": "Missing empty string validation for txnID (transaction hash)", + "why_false_positive": "Transaction::load validates txnID (transaction hash) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (SHAMapNodeType)", + "empty", + "string", + "validation" + ], + "evidence": "if (type == SHAMapNodeType::tnTRANSACTION_NM) / else if (type == SHAMapNodeType::tnTRANSACTION_MD) at TransactionMaster::fetch(boost::intrusive_ptr const&, SHAMapNodeType, std::uint32_t)", + "issue_pattern": "Missing empty string validation for type (SHAMapNodeType)", + "why_false_positive": "if (type == SHAMapNodeType::tnTRANSACTION_NM) / else if (type == SHAMapNodeType::tnTRANSACTION_MD) validates type (SHAMapNodeType) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "type (SHAMapNodeType)", + "type", + "validation", + "check" + ], + "evidence": "if (type == SHAMapNodeType::tnTRANSACTION_NM) / else if (type == SHAMapNodeType::tnTRANSACTION_MD) at TransactionMaster::fetch(boost::intrusive_ptr const&, SHAMapNodeType, std::uint32_t)", + "issue_pattern": "Missing type validation for type (SHAMapNodeType)", + "why_false_positive": "if (type == SHAMapNodeType::tnTRANSACTION_NM) / else if (type == SHAMapNodeType::tnTRANSACTION_MD) validates type (SHAMapNodeType) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "item->slice() (SHAMapItem data)", + "empty", + "string", + "validation" + ], + "evidence": "SerialIter, getVL, STTx constructor at TransactionMaster::fetch(boost::intrusive_ptr const&, SHAMapNodeType, std::uint32_t)", + "issue_pattern": "Missing empty string validation for item->slice() (SHAMapItem data)", + "why_false_positive": "SerialIter, getVL, STTx constructor validates item->slice() (SHAMapItem data) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "item->slice() (SHAMapItem data)", + "format", + "validation", + "invalid" + ], + "evidence": "SerialIter, getVL, STTx constructor at TransactionMaster::fetch(boost::intrusive_ptr const&, SHAMapNodeType, std::uint32_t)", + "issue_pattern": "Missing format validation for item->slice() (SHAMapItem data)", + "why_false_positive": "SerialIter, getVL, STTx constructor validates item->slice() (SHAMapItem data) format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/ledger/detail/TransactionMaster.cpp", + "functions": [ + { + "args": [ + "Application& app" + ], + "lineno": 8, + "name": "TransactionMaster" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t ledger", + "std::optional tseq", + "std::optional netID" + ], + "lineno": 16, + "name": "inLedger" + }, + { + "args": [ + "uint256 const& txnID" + ], + "lineno": 27, + "name": "fetch_from_cache" + }, + { + "args": [ + "uint256 const& txnID", + "error_code_i& ec" + ], + "lineno": 32, + "name": "fetch" + }, + { + "args": [ + "uint256 const& txnID", + "ClosedInterval const& range", + "error_code_i& ec" + ], + "lineno": 48, + "name": "fetch" + }, + { + "args": [ + "boost::intrusive_ptr const& item", + "SHAMapNodeType type", + "std::uint32_t uCommitLedger" + ], + "lineno": 64, + "name": "fetch" + }, + { + "args": [ + "std::shared_ptr* pTransaction" + ], + "lineno": 89, + "name": "canonicalize" + }, + { + "args": [], + "lineno": 99, + "name": "sweep" + }, + { + "args": [], + "lineno": 104, + "name": "getCache" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The TransactionMaster class is a core ledger component and is likely tested indirectly via integration and ledger tests. Direct unit tests for TransactionMaster may exist in files like Transaction_test.cpp, TransactionMaster_test.cpp, or Ledger_test.cpp. However, the specific validation paths (e.g., isValidated, SHAMapNodeType parsing, SerialIter/STTx construction) may not be exhaustively unit tested for all edge cases (e.g., malformed SHAMapItem data, rare SHAMapNodeType values). There may be gaps in negative testing for corrupted or unexpected input data, especially for the binary parsing logic. Fuzzing or property-based tests would be beneficial for the item->slice() parsing paths.", + "validation_architecture": { + "auto_validated_fields": [ + "txnID (type: uint256)", + "type (SHAMapNodeType enum)", + "item->slice() (binary format for STTx)" + ], + "framework": "Custom (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (returns early with partial result)", + "field": "txn (Transaction object from cache)", + "location": "TransactionMaster::fetch (both overloads)", + "validated_by": "txn->isValidated()", + "validates": [ + "Checks if cached transaction is validated before returning meta" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "error_code_i (set via reference), or returns TxSearched variant", + "field": "txnID (transaction hash)", + "location": "TransactionMaster::fetch (both overloads)", + "validated_by": "Transaction::load", + "validates": [ + "Checks if transaction exists in database/storage", + "Checks if transaction is within specified ledger range (in overload with range param)" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (branches on type)", + "field": "type (SHAMapNodeType)", + "location": "TransactionMaster::fetch(boost::intrusive_ptr const&, SHAMapNodeType, std::uint32_t)", + "validated_by": "if (type == SHAMapNodeType::tnTRANSACTION_NM) / else if (type == SHAMapNodeType::tnTRANSACTION_MD)", + "validates": [ + "Ensures correct deserialization method for SHAMapItem based on node type" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "Potential exception from STTx constructor or SerialIter if data is malformed", + "field": "item->slice() (SHAMapItem data)", + "location": "TransactionMaster::fetch(boost::intrusive_ptr const&, SHAMapNodeType, std::uint32_t)", + "validated_by": "SerialIter, getVL, STTx constructor", + "validates": [ + "Checks that SHAMapItem data can be parsed as a valid transaction" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/ledger/detail/TransactionMaster.cpp.ai.md b/src/xrpld/app/ledger/detail/TransactionMaster.cpp.ai.md new file mode 100644 index 0000000000..8df8f29ab0 --- /dev/null +++ b/src/xrpld/app/ledger/detail/TransactionMaster.cpp.ai.md @@ -0,0 +1,37 @@ +# `TransactionMaster.cpp` — Application-Level Transaction Cache and Retrieval + +`TransactionMaster` is the single point of authority for in-memory transaction state across the XRPL node. It wraps a `TaggedCache` and presents a unified lookup API used by everything from RPC handlers and the P2P overlay to the consensus engine and the database persistence layer. Its core mission is to avoid redundant database reads by keeping recently-seen `Transaction` objects alive in a shared-pointer cache, while also ensuring that all subsystems that hold a reference to a given transaction share the same object. + +## Cache Configuration + +The constructor allocates a `TaggedCache` named `"TransactionCache"` with a capacity of **65,536 entries** and a **30-minute TTL**. Those parameters are hard-coded at construction time via `Application&`, which also supplies the `stopwatch()` used for time-based expiry and the `beast::Journal` instance for cache diagnostics. The class is non-copyable by design — it is a singleton-like application service instantiated once in `ApplicationImp` and returned by `Application::getMasterTransaction()`. + +## Three `fetch` Overloads + +The three `fetch` overloads each serve a different caller context, but they share a common philosophy: check the in-memory cache first; fall back to persistent storage only when necessary. + +**Hash-only fetch** (`fetch(uint256, error_code_i&)`) and its **range-bounded variant** (`fetch(uint256, ClosedInterval, error_code_i&)`) are the entry points used by RPC handlers and the `tx` command. Both begin with `fetch_from_cache()`. The subtle decision that follows is worth noting: if the cache hit is an *unvalidated* transaction (`isValidated()` returns `false` when `mLedgerIndex == 0`), the function returns it immediately without hitting the database. An unvalidated transaction is still pending — it has no ledger metadata — so there is nothing to load from storage anyway. If the cached transaction *is* validated (already committed to a closed ledger), the code bypasses the cache hit and delegates to `Transaction::load()` to retrieve the `TxMeta` alongside it. The cache alone never stores metadata; metadata only exists in the database after ledger close. + +After a successful `Transaction::load()`, the freshly-loaded object is inserted or deduplicated in the cache via `canonicalize_replace_client()` before returning to the caller. + +The **`ClosedInterval` overload** passes the ledger range through to `Transaction::load()`, which restricts the database search to those ledger sequences. If the searched range was not fully present in the database at query time, `load()` returns a `TxSearched` enum rather than a transaction pair; `fetch` propagates this variant as-is so callers can distinguish "transaction doesn't exist" from "we can't be sure — our history is incomplete." + +**SHAMap-item fetch** (`fetch(SHAMapItem, SHAMapNodeType, uCommitLedger)`) is used during ledger application when iterating over a `SHAMap`'s transaction leaves. The function handles two distinct wire encodings. For `tnTRANSACTION_NM` (no metadata) nodes, the item's raw slice is fed directly into a `SerialIter` and used to construct an `STTx`. For `tnTRANSACTION_MD` nodes (transaction with embedded metadata), the serialized format prefixes the transaction bytes with a variable-length header, so `getVL()` is called to extract the inner transaction blob before the `STTx` is constructed. Neither branch updates the `mCache` — this overload returns a `shared_ptr` rather than a `Transaction` wrapper, reflecting that its callers only need the raw protocol object during ledger processing. When the item *is* found in cache, the cached `Transaction`'s status can be updated to `COMMITTED` if `uCommitLedger` is non-zero, propagating commit information back to any other holder of that shared pointer. + +## Canonicalization + +`canonicalize(std::shared_ptr*)` and the internal call to `canonicalize_replace_client()` enforce a deduplication invariant: for any transaction hash, there is exactly one live `Transaction` object. `TaggedCache::canonicalize_replace_client()` looks up the key under its internal lock; if the key already exists (cache hit), the caller's pointer is **redirected to the existing cached instance** (the "client" pointer is replaced). If the key is absent, the new object is inserted and the caller's pointer is left pointing at it. The net effect is that all in-flight holders — `NetworkOPs`, `PeerImp`, any active RPC response — end up pointing at the same `Transaction` object, so a `setStatus(COMMITTED, ...)` call from any one of them is immediately visible to all others through the shared reference without any additional synchronization. + +`canonicalize()` guards against inserting a zero hash (`beast::zero` check), which would correspond to a `Transaction` that failed its own construction and has no valid ID. + +## Status Propagation via `inLedger()` + +`inLedger()` is called by the database write-back path in `Node.cpp` when a transaction is being persisted after ledger close. If the transaction is in cache, `setStatus(COMMITTED, ledger, tseq, netID)` is called on it; otherwise the function returns `false`, meaning the transaction was never tracked in memory and no in-memory state needs updating. The return value is used by callers as an "already knew about this" signal. + +## Sweep and Cache Exposure + +`sweep()` delegates directly to `TaggedCache::sweep()`, which evicts entries whose strong references have expired and whose weak references can no longer be promoted. It is called periodically by `ApplicationImp`'s sweep loop, which logs the cache size before and after. `getCache()` exposes a mutable reference to the underlying `TaggedCache` directly; `SHAMapStoreImp` uses this during online deletion to freshen the cache, preventing valid entries from being incorrectly evicted during a history rotation. + +## Thread Safety + +`TransactionMaster` itself performs no locking. Thread safety is provided entirely by `TaggedCache`'s internal `std::mutex`, which serializes all insertions, lookups, and canonicalize operations. The `Transaction::mApplying` flag and `SubmitResult` state are explicitly excluded from this protection model — per the comments in `Transaction.h`, those fields are accessed only under `NetworkOPsImp`'s own lock, and a rare race has been accepted as a deliberate tradeoff since the worst consequence is a redundant transaction attempt. \ No newline at end of file diff --git a/src/xrpld/app/main/Application.cpp.ai.json b/src/xrpld/app/main/Application.cpp.ai.json new file mode 100644 index 0000000000..f0b04e8148 --- /dev/null +++ b/src/xrpld/app/main/Application.cpp.ai.json @@ -0,0 +1,831 @@ +{ + "args": [ + { + "lineno": 61, + "name": "Config& config" + }, + { + "lineno": 61, + "name": "Endpoints const& endpoints" + }, + { + "lineno": 168, + "name": "Config const& config" + }, + { + "lineno": 1012, + "name": "boost::program_options::variables_map const& cmdline" + }, + { + "lineno": 1372, + "name": "bool withTimers" + }, + { + "lineno": 1447, + "name": "std::string msg" + }, + { + "lineno": 1466, + "name": "bool check" + }, + { + "lineno": 1822, + "name": "std::string const& name" + }, + { + "lineno": 1792, + "name": "std::string& reason" + }, + { + "lineno": 1632, + "name": "std::string const& ledgerID" + }, + { + "lineno": 1632, + "name": "bool replay" + }, + { + "lineno": 1632, + "name": "bool isFileName" + }, + { + "lineno": 1632, + "name": "std::optional trapTxID" + }, + { + "lineno": 1557, + "name": "std::string const& name" + }, + { + "lineno": 71, + "name": "beast::insight::Event ev" + }, + { + "lineno": 71, + "name": "beast::Journal journal" + }, + { + "lineno": 71, + "name": "std::chrono::milliseconds interval" + }, + { + "lineno": 71, + "name": "boost::asio::io_context& ios" + }, + { + "lineno": 1837, + "name": "std::unique_ptr config" + }, + { + "lineno": 1837, + "name": "std::unique_ptr logs" + }, + { + "lineno": 1837, + "name": "std::unique_ptr timeKeeper" + } + ], + "classes": [ + { + "args": [ + "std::unique_ptr config", + "std::unique_ptr logs", + "std::unique_ptr timeKeeper" + ], + "lineno": 66, + "name": "ApplicationImp" + }, + { + "args": [ + "beast::insight::Event ev", + "beast::Journal journal", + "std::chrono::milliseconds interval", + "boost::asio::io_context& ios" + ], + "lineno": 71, + "name": "io_latency_sampler" + } + ], + "code_paths": [ + { + "call_chain": [ + "Application::setup", + "fixConfigPorts", + "Config::validate", + "NodeIdentity::loadNodeIdentity", + "LedgerMaster::setup", + "LedgerCleaner::setup", + "LedgerReplayer::setup" + ], + "entry_point": "Application::setup", + "purpose": "Initializes the application, validates configuration, sets up node identity, and prepares ledger components.", + "validation_points": [ + "fixConfigPorts (validates and fixes port configs)", + "Config::validate (validates config fields)", + "NodeIdentity::loadNodeIdentity (validates node keys)", + "LedgerMaster::setup (validates ledger hash/ID inputs)", + "LedgerCleaner::setup (validates ledger state)", + "LedgerReplayer::setup (validates replay ledger IDs)" + ] + }, + { + "call_chain": [ + "RPC handler", + "parse JSON input", + "jss:: template-based validation", + "STParsedJSON (for transaction fields)", + "applyTransaction" + ], + "entry_point": "RPC/JSON API handler (e.g., doCommand)", + "purpose": "Handles incoming JSON RPC requests, validates input fields, parses transactions, and applies them.", + "validation_points": [ + "jss:: template-based validation (validates JSON fields)", + "STParsedJSON (validates transaction fields)", + "applyTransaction (validates transaction logic and signatures)" + ] + }, + { + "call_chain": [ + "Application::run", + "start", + "GRPCServer::start", + "RPC/GRPC request handler", + "validation as above" + ], + "entry_point": "Application::run", + "purpose": "Runs the main application loop, starts network servers, and processes incoming requests.", + "validation_points": [ + "GRPCServer::start (validates server config)", + "RPC/GRPC request handler (validates input as above)" + ] + } + ], + "data_flows": [ + { + "field": "port", + "flow": [ + "Config::load", + "fixConfigPorts", + "Config::validate", + "Application::setup", + "Network server startup" + ], + "origin": "Config file (parsed in Config)", + "transformations": [ + "fixConfigPorts may adjust or correct port values", + "Config::validate checks for valid port ranges and conflicts" + ], + "validated_at": "fixConfigPorts, Config::validate" + }, + { + "field": "node_public_key / node_private_key", + "flow": [ + "Config::load", + "NodeIdentity::loadNodeIdentity", + "Application::setup", + "Used for signing/validation" + ], + "origin": "Config file or generated at startup", + "transformations": [ + "NodeIdentity::loadNodeIdentity parses and validates keys" + ], + "validated_at": "NodeIdentity::loadNodeIdentity" + }, + { + "field": "ledger_hash / ledger_id", + "flow": [ + "RPC handler or internal trigger", + "LedgerMaster / LedgerCleaner / LedgerReplayer", + "Ledger lookup and processing" + ], + "origin": "RPC input or internal process", + "transformations": [ + "Validated for format and existence in LedgerMaster, LedgerCleaner, LedgerReplayer" + ], + "validated_at": "LedgerMaster, LedgerCleaner, LedgerReplayer" + }, + { + "field": "transaction JSON fields (amount, account, signature, etc.)", + "flow": [ + "RPC handler", + "jss:: template-based validation", + "STParsedJSON", + "applyTransaction", + "TransactionMaster" + ], + "origin": "RPC/JSON input", + "transformations": [ + "jss:: validation checks field presence/types", + "STParsedJSON parses and validates transaction structure", + "applyTransaction checks signatures and business logic" + ], + "validated_at": "jss:: validation, STParsedJSON, applyTransaction" + } + ], + "description": "This file implements the main ApplicationImp class for the XRPL server (xrpld), handling initialization, startup, shutdown, and core runtime logic for the ledger, networking, validation, and server management.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "JSON RPC fields (via jss::)", + "validation", + "missing", + "check" + ], + "evidence": "Field JSON RPC fields (via jss::) validated by jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable", + "issue_pattern": "Missing validation for JSON RPC fields (via jss::)", + "why_false_positive": "jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable validates JSON RPC fields (via jss::) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Config fields (via Config class)", + "validation", + "missing", + "check" + ], + "evidence": "Field Config fields (via Config class) validated by jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable", + "issue_pattern": "Missing validation for Config fields (via Config class)", + "why_false_positive": "jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable validates Config fields (via Config class) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Node identity keys", + "validation", + "missing", + "check" + ], + "evidence": "Field Node identity keys validated by jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable", + "issue_pattern": "Missing validation for Node identity keys", + "why_false_positive": "jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable validates Node identity keys automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Ledger hashes", + "validation", + "missing", + "check" + ], + "evidence": "Field Ledger hashes validated by jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable", + "issue_pattern": "Missing validation for Ledger hashes", + "why_false_positive": "jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable validates Ledger hashes automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Transaction fields", + "validation", + "missing", + "check" + ], + "evidence": "Field Transaction fields validated by jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable", + "issue_pattern": "Missing validation for Transaction fields", + "why_false_positive": "jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable validates Transaction fields automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "JSON input fields (via jss:: namespace)", + "empty", + "string", + "validation" + ], + "evidence": "jss:: template-based validation at various RPC/JSON parsing entry points (e.g., STParsedJSON, LedgerToJson, etc.)", + "issue_pattern": "Missing empty string validation for JSON input fields (via jss:: namespace)", + "why_false_positive": "jss:: template-based validation validates JSON input fields (via jss:: namespace) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Config fields (ports, node identity, etc.)", + "empty", + "string", + "validation" + ], + "evidence": "Config class and fixConfigPorts function at fixConfigPorts, ApplicationImp::setup, Config constructor", + "issue_pattern": "Missing empty string validation for Config fields (ports, node identity, etc.)", + "why_false_positive": "Config class and fixConfigPorts function validates Config fields (ports, node identity, etc.) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Node identity (public/private keys)", + "empty", + "string", + "validation" + ], + "evidence": "NodeIdentity::loadNodeIdentity at ApplicationImp::setup (calls NodeIdentity::loadNodeIdentity)", + "issue_pattern": "Missing empty string validation for Node identity (public/private keys)", + "why_false_positive": "NodeIdentity::loadNodeIdentity validates Node identity (public/private keys) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "Ledger hash/ID inputs", + "empty", + "string", + "validation" + ], + "evidence": "LedgerMaster, LedgerCleaner, LedgerReplayer at LedgerMaster::getLedgerByHash, LedgerCleaner::doClean, etc.", + "issue_pattern": "Missing empty string validation for Ledger hash/ID inputs", + "why_false_positive": "LedgerMaster, LedgerCleaner, LedgerReplayer validates Ledger hash/ID inputs for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Transaction fields (amount, account, signature, etc.)", + "empty", + "string", + "validation" + ], + "evidence": "STParsedJSON, applyTransaction, TransactionMaster at Transaction parsing and apply logic", + "issue_pattern": "Missing empty string validation for Transaction fields (amount, account, signature, etc.)", + "why_false_positive": "STParsedJSON, applyTransaction, TransactionMaster validates Transaction fields (amount, account, signature, etc.) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "Amendment/Feature flags", + "empty", + "string", + "validation" + ], + "evidence": "AmendmentTable at AmendmentTable::addInitial, AmendmentTable::enable", + "issue_pattern": "Missing empty string validation for Amendment/Feature flags", + "why_false_positive": "AmendmentTable validates Amendment/Feature flags for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.6, + "detection_keywords": [ + "Database connection parameters", + "empty", + "string", + "validation" + ], + "evidence": "DatabaseCon, SQLiteDatabase at DatabaseCon constructor, SQLiteDatabase::open", + "issue_pattern": "Missing empty string validation for Database connection parameters", + "why_false_positive": "DatabaseCon, SQLiteDatabase validates Database connection parameters for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "file handle", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::ifstream", + "issue_pattern": "Missing file handle cleanup", + "why_false_positive": "std::ifstream provides automatic file handle cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/Application.cpp", + "functions": [ + { + "args": [ + "Config& config", + "Endpoints const& endpoints" + ], + "lineno": 61, + "name": "fixConfigPorts" + }, + { + "args": [ + "Config const& config" + ], + "lineno": 168, + "name": "numberOfThreads" + }, + { + "args": [ + "boost::program_options::variables_map const& cmdline" + ], + "lineno": 1012, + "name": "setup" + }, + { + "args": [ + "bool withTimers" + ], + "lineno": 1372, + "name": "start" + }, + { + "args": [], + "lineno": 1397, + "name": "run" + }, + { + "args": [ + "std::string msg" + ], + "lineno": 1447, + "name": "signalStop" + }, + { + "args": [], + "lineno": 1461, + "name": "checkSigs" + }, + { + "args": [ + "bool check" + ], + "lineno": 1466, + "name": "checkSigs" + }, + { + "args": [], + "lineno": 1471, + "name": "isStopping" + }, + { + "args": [], + "lineno": 1476, + "name": "fdRequired" + }, + { + "args": [], + "lineno": 1502, + "name": "startGenesisLedger" + }, + { + "args": [], + "lineno": 1522, + "name": "getLastFullLedger" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 1557, + "name": "loadLedgerFromFile" + }, + { + "args": [ + "std::string const& ledgerID", + "bool replay", + "bool isFileName", + "std::optional trapTxID" + ], + "lineno": 1632, + "name": "loadOldLedger" + }, + { + "args": [ + "std::string& reason" + ], + "lineno": 1792, + "name": "serverOkay" + }, + { + "args": [ + "std::string const& name" + ], + "lineno": 1822, + "name": "getJournal" + }, + { + "args": [], + "lineno": 1827, + "name": "setMaxDisallowedLedger" + }, + { + "args": [ + "std::unique_ptr config", + "std::unique_ptr logs", + "std::unique_ptr timeKeeper" + ], + "lineno": 1837, + "name": "make_Application" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic file handle cleanup", + "false_positive_risk": "Missing file handle cleanup", + "resource": "file handle", + "type": "raii_wrapper", + "wrapper_type": "std::ifstream" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 59, + "name": "xrpl" + } + ], + "test_coverage_notes": "Core validation logic is typically covered by unit tests in the 'test' and 'unittest' directories, such as 'test/Config_test.cpp', 'test/NodeIdentity_test.cpp', 'test/ledger/LedgerMaster_test.cpp', and 'test/app/Transaction_test.cpp'. JSON/RPC validation is often tested in 'rpc' or 'json' test suites. However, integration between config validation and application startup may not be fully covered, and edge cases in fixConfigPorts or error handling for malformed configs may lack direct tests. Some validation paths (e.g., LedgerCleaner, LedgerReplayer) may only be tested indirectly via higher-level integration tests.", + "validation_architecture": { + "auto_validated_fields": [ + "JSON RPC fields (via jss::)", + "Config fields (via Config class)", + "Node identity keys", + "Ledger hashes", + "Transaction fields" + ], + "framework": "jss:: template-based validation, STParsedJSON, Config, NodeIdentity, AmendmentTable", + "validation_layer": "entry_point (JSON/config/database input), business_logic (ledger/transaction/application logic)" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "std::exception (typically std::runtime_error or custom parse error)", + "field": "JSON input fields (via jss:: namespace)", + "location": "various RPC/JSON parsing entry points (e.g., STParsedJSON, LedgerToJson, etc.)", + "validated_by": "jss:: template-based validation", + "validates": [ + "Presence of required JSON fields", + "Type correctness (string, int, object, etc.)", + "Format (e.g., hex, base58, etc.)" + ], + "validation_type": "type|format|required fields" + }, + { + "confidence": 0.8, + "error_thrown": "std::exception (invalid_argument, runtime_error)", + "field": "Config fields (ports, node identity, etc.)", + "location": "fixConfigPorts, ApplicationImp::setup, Config constructor", + "validated_by": "Config class and fixConfigPorts function", + "validates": [ + "Port numbers are valid integers and in allowed range", + "Required config fields are present", + "Format of IP addresses and endpoints" + ], + "validation_type": "type|range|format" + }, + { + "confidence": 0.8, + "error_thrown": "std::exception (runtime_error, custom identity error)", + "field": "Node identity (public/private keys)", + "location": "ApplicationImp::setup (calls NodeIdentity::loadNodeIdentity)", + "validated_by": "NodeIdentity::loadNodeIdentity", + "validates": [ + "Key presence", + "Key format (base58, hex, etc.)", + "Key type (ed25519/secp256k1)" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "std::exception (runtime_error, not_found)", + "field": "Ledger hash/ID inputs", + "location": "LedgerMaster::getLedgerByHash, LedgerCleaner::doClean, etc.", + "validated_by": "LedgerMaster, LedgerCleaner, LedgerReplayer", + "validates": [ + "Hash is correct length (256 bits)", + "Hash is valid hex", + "Ledger exists in database" + ], + "validation_type": "format|type|range" + }, + { + "confidence": 0.8, + "error_thrown": "std::exception (parse error, logic error)", + "field": "Transaction fields (amount, account, signature, etc.)", + "location": "Transaction parsing and apply logic", + "validated_by": "STParsedJSON, applyTransaction, TransactionMaster", + "validates": [ + "Amount is numeric and within allowed range", + "Account is valid address", + "Signature is present and valid" + ], + "validation_type": "type|format|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "std::exception (logic_error)", + "field": "Amendment/Feature flags", + "location": "AmendmentTable::addInitial, AmendmentTable::enable", + "validated_by": "AmendmentTable", + "validates": [ + "Feature flag is known", + "Feature flag is not duplicated" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.6, + "error_thrown": "std::exception (database_error, invalid_argument)", + "field": "Database connection parameters", + "location": "DatabaseCon constructor, SQLiteDatabase::open", + "validated_by": "DatabaseCon, SQLiteDatabase", + "validates": [ + "File path is valid", + "Connection string is correct format" + ], + "validation_type": "format|type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/Application.cpp.ai.md b/src/xrpld/app/main/Application.cpp.ai.md new file mode 100644 index 0000000000..5c9b5eb2ce --- /dev/null +++ b/src/xrpld/app/main/Application.cpp.ai.md @@ -0,0 +1,72 @@ +# `src/xrpld/app/main/Application.cpp` + +This file is the structural heart of the `xrpld` server process. It implements `ApplicationImp`, the concrete realization of the `Application` abstract interface, which acts as the global service registry and lifecycle manager for every significant subsystem in the XRPL node. Every component — from the ledger and consensus engine to the peer overlay, RPC handler, gRPC server, and transaction queue — is constructed, started, and stopped through this single class. + +## Class Hierarchy and Ownership Model + +`ApplicationImp` inherits from both `Application` (which itself extends `ServiceRegistry` and `beast::PropertyStream::Source`) and `BasicApp`. `BasicApp` is a slim wrapper that owns the `boost::asio::io_context` and a thread pool that drives it. The design makes the `io_context` outlive all children by keeping it at the `BasicApp` layer, above the subsystems that schedule work onto it. + +The `Application` interface exposes pure virtual accessors for every major subsystem — `getLedgerMaster()`, `getOPs()`, `getOverlay()`, etc. — making `ApplicationImp` the canonical service locator. Almost every subsystem receives an `Application&` reference to `*this` at construction, enabling cross-component access through a single stable pointer rather than individual dependency injection into each relationship. This is a conscious tradeoff: it creates tight coupling at the application layer while keeping subsystem interfaces narrow. + +The sole public creation mechanism is `make_Application()`, a factory that constructs an `ApplicationImp` via `std::make_unique`. The implementation class is never exposed in the header. + +## The Constructor's Deliberate Inertia + +`ApplicationImp`'s constructor is dominated by a massive member initializer list that instantiates every subsystem. A critical design rule is enforced here, explicitly documented in the constructor body: **no threads, sockets, or real I/O work may start in the constructor**. That responsibility belongs to `setup()` (configuration and database work) and `start()` (service activation). The reason is that unit tests construct `Application` objects without running them, so anything that starts in the constructor would fail to stop cleanly. + +The constructor also generates `instanceCookie_`, a random 64-bit value used to distinguish each process incarnation. The CSPRNG is seeded externally before this call. + +The `JobQueue` worker count is computed inline during construction using a lambda. It scales with `NODE_SIZE` and `hardware_concurrency`: small systems get 2–6 workers; "large" (size ≥ 4) systems with 16+ cores get up to 14. Similarly, the I/O thread count for `BasicApp` tops out at 6, reduced to 1 for under-provisioned or single-core systems. + +## `io_latency_sampler`: The Heartbeat Monitor + +An inner class, `io_latency_sampler`, wraps `beast::io_latency_probe` to measure round-trip dispatch latency on the `io_context`. Every 100ms it posts a probe job; the elapsed time before the probe is picked up is the IO latency. Values ≥ 10ms are emitted as telemetry events; values ≥ 500ms produce a warning log. This is the primary observable signal for detecting when the event loop is overloaded — a critical concern because the consensus engine posts time-sensitive work here. + +## `setup()`: Configuration to Live State + +`setup()` is a long sequential initialization that transforms a loaded `Config` object into a running-but-not-yet-serving application state. It is approximately 400 lines and is flagged with a TODO to break it up. + +The sequence proceeds as: signal handler registration (SIGINT/SIGTERM → `signalStop()`) → database initialization → peer reservation loading → validator max-ledger fence (`setMaxDisallowedLedger()`) → amendment table construction → pathfinder table initialization → ledger startup mode selection → order book setup → node identity loading → cluster config → validator manifests and trusted validator list → validator sites → overlay (peer network) creation → first consensus round → server handler (RPC/WebSocket ports) setup and port fixup → standalone/network mode → `[rpc_startup]` command execution → validator site refresh start. + +The ledger startup mode is a fork: +- `Fresh` — creates a brand new genesis ledger with all currently desired amendments enabled. +- `Load` / `LoadFile` / `Replay` — calls `loadOldLedger()`, falling back to a fresh genesis if `FAST_LOAD` is set and the ledger cannot be found. +- `Network` — starts from a genesis but immediately flags the node as needing to acquire the network ledger. + +The amendment table is constructed from `detail::supportedAmendments()`, enriched by the `[amendments]` (vote yes) and `[veto_amendments]` (vote no) config sections, and then linked to the current validator quorum keys via `trustChanged()`. + +## `start()` and the Ordered Activation Problem + +`start(bool withTimers)` activates services in a deliberate sequence. The `withTimers` parameter suppresses sweep and entropy timers in unit test mode. After timers, each service's `start()` is called: IO latency sampler, DNS resolver, load manager, SHAMapStore, overlay, gRPC server, ledger cleaner, perf log. + +The gRPC server is notable: after `grpcServer_->start()`, another call to `fixConfigPorts` updates the config with the gRPC port (in case port 0 was used for auto-assignment). `fixConfigPorts()` at the bottom of the file solves the bootstrap problem where port configuration must be known before binding, but the actual bound port may differ (e.g., when `port = 0` triggers OS port assignment). It walks the live `Endpoints` map and back-fills the actual port numbers into the `Config` sections. + +## `run()`, `signalStop()`, and the Stop Protocol + +`run()` is the blocking main loop. After optionally arming the stall detector, it executes: + +```cpp +isTimeToStop.wait(false, std::memory_order_relaxed); +``` + +This is C++20 atomic_flag waiting — a lock-free futex-like block until `signalStop()` sets the flag and calls `notify_all()`. `signalStop()` uses `test_and_set()` to ensure the stop message is logged exactly once regardless of how many threads call it concurrently — the system can receive SIGTERM, a DB space check failure, and a PerfLog timeout all simultaneously. + +The shutdown sequence that follows is carefully ordered and documented as fragile. `waitHandlerCounter_.join()` blocks until all pending Asio timer callbacks (sweep, entropy) drain — this is essential because those handlers capture `this` and would access destroyed members if allowed to fire after `run()` returns. Then manifests are persisted, and services are stopped in reverse dependency order: load manager, SHAMapStore, job queue, overlay, gRPC, NetworkOPs, server handler, ledger replayer, inbound transactions, inbound ledgers, ledger cleaner, node store, perf log. + +## `doSweep()`: Cache Pressure Management + +The sweep timer fires at a configurable interval (defaulting to a `SizedItem` value based on node size). `doSweep()` iterates through every major cache — `NodeFamily`, `TransactionMaster`, `LedgerMaster`, `TempNodeCache`, `RCLValidations`, `InboundLedgers`, `LedgerReplayer`, `AcceptedLedgerCache`, `CachedSLEs` — calling each one's sweep or expire method and logging the before/after size at debug level. It also checks whether the transaction database has sufficient disk space remaining and calls `signalStop()` if it does not. After all sweeps, `mallocTrim()` returns freed memory to the OS, counteracting the tendency of `malloc` arenas to hold pages indefinitely. The timer is rescheduled by `doSweep()` itself at the end, creating a chain rather than a fixed interval. + +## `loadOldLedger()` and Replay Mode + +`loadOldLedger()` is the most defensively-written function in the file. It accepts a ledger identifier that could be a 64-char hex hash, a decimal sequence number, the string `"latest"`, a filename, or empty. After locating the ledger in the relational or node store (falling back to `InboundLedger::checkLocal()` for raw node-store recovery), it validates integrity in three layers: the account hash must be non-zero, `walkLedger()` must find all nodes present, and `isSensible()` must pass. Any failure triggers `UNREACHABLE` with `LCOV_EXCL` markers indicating the paths are not expected in tests. + +In replay mode, the function also loads the parent ledger, builds a `LedgerReplay` structure, and injects all transactions from the replay target into the open ledger using `rawTxInsert`. An optional `trapTxID` can mark a specific transaction for halting — a debug feature for instrumenting consensus replays. + +## `setMaxDisallowedLedger()` and Validator Safety + +For a freshly-started validator, re-signing proposals for ledgers that were already closed and persisted before a crash could cause equivocation. `setMaxDisallowedLedger()` queries the maximum ledger sequence from the relational database at startup and stores it as an `std::atomic`. NetworkOPs uses this value to refuse signing proposals for ledger sequences at or below it, providing a safety fence around restart-driven double-signing. + +## `serverOkay()`: Health Gate for Load Balancers + +When `ELB_SUPPORT` is enabled in config, `serverOkay()` gates the HTTP 200 response used by load balancers. It returns false (with a human-readable reason) if the server is stopping, hasn't yet acquired the network ledger, is amendment-blocked (too old to understand current transactions), has no valid validator list (UNL-blocked), is not yet syncing, has fallen behind on ledgers, or is locally overloaded. The ordering matters — amendment-blocked and UNL-blocked checks appear before the sync check because a node stuck in those states should be drained from the pool regardless of apparent sync status. \ No newline at end of file diff --git a/src/xrpld/app/main/Application.h.ai.json b/src/xrpld/app/main/Application.h.ai.json new file mode 100644 index 0000000000..2fa7e0850b --- /dev/null +++ b/src/xrpld/app/main/Application.h.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 33, + "name": "Key" + }, + { + "lineno": 34, + "name": "T" + }, + { + "lineno": 35, + "name": "IsKeyCache" + }, + { + "lineno": 36, + "name": "SharedWeakUnionPointer" + }, + { + "lineno": 37, + "name": "SharedPointerType" + }, + { + "lineno": 38, + "name": "Hash" + }, + { + "lineno": 39, + "name": "KeyEqual" + }, + { + "lineno": 40, + "name": "Mutex" + }, + { + "lineno": 109, + "name": "Adaptor" + } + ], + "classes": [ + { + "args": [], + "lineno": 32, + "name": "TaggedCache" + }, + { + "args": [], + "lineno": 61, + "name": "Application" + } + ], + "description": "This header file defines the Application class and related forward declarations for the XRPL (XRP Ledger) server application, including interfaces for configuration, networking, ledger management, and other core components. It also provides the factory function to create an Application instance.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/Application.h", + "functions": [ + { + "args": [ + "std::unique_ptr config", + "std::unique_ptr logs", + "std::unique_ptr timeKeeper" + ], + "lineno": 120, + "name": "make_Application" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 16, + "name": "unl" + }, + { + "lineno": 19, + "name": "Resource" + }, + { + "lineno": 22, + "name": "NodeStore" + }, + { + "lineno": 25, + "name": "perf" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/Application.h.ai.md b/src/xrpld/app/main/Application.h.ai.md new file mode 100644 index 0000000000..eaa1beb90c --- /dev/null +++ b/src/xrpld/app/main/Application.h.ai.md @@ -0,0 +1,60 @@ +# `Application.h` — The XRPL Node Application Interface + +## Role in the System + +`Application.h` defines the abstract interface that represents a running XRPL validator or tracking node as a whole. It is the top-level type through which `Main.cpp` drives the node lifecycle — calling `setup()`, `start()`, `run()`, and ultimately `signalStop()`. The concrete implementation is `ApplicationImp`, defined in `Application.cpp`, but nothing outside that file ever sees the concrete type; all callers work through the `Application` pointer returned by `make_Application()`. + +This header is deliberately thin. Decades of growth in a monolithic "god object" have been progressively refactored out of `Application` and into `ServiceRegistry`, its base class. The `ServiceRegistry` interface (in `include/xrpl/core/ServiceRegistry.h`) holds every "give me a reference to subsystem X" accessor — covering the ledger store, overlay network, job queue, fee tracker, validator list, transaction queue, and more. `Application.h` retains only the lifecycle methods and a handful of cross-cutting concerns that are genuinely node-wide rather than per-service. + +## Inheritance Design + +`Application` inherits from two bases: + +- **`ServiceRegistry`** — the service-locator half of the object, providing typed accessors for every subsystem. Components that only need service access and not lifecycle control can hold a `ServiceRegistry&` reference, reducing coupling. The comment in `ServiceRegistry.h` is explicit: _"This is temporary until we migrate all code to use ServiceRegistry"_ — meaning the long-term goal is to break the monolith further, with `getApp()` serving as the escape hatch during migration. + +- **`beast::PropertyStream::Source`** — wires the application into the diagnostic property tree, so the `/server_info` RPC and admin commands can walk the entire object graph and emit nested key/value data for debugging. + +`ApplicationImp` adds a third base, **`BasicApp`**, which owns the `boost::asio::io_context` and the thread pool that drives all asynchronous I/O. `BasicApp` ensures the `io_context` outlives all child components that post work to it — a common lifetime hazard in Asio programs. Thread count is chosen at construction time based on `hardware_concurrency` and the configured `NODE_SIZE`, defaulting to six threads on adequately provisioned hardware. + +## Lifecycle Methods + +`setup()` accepts the parsed command-line options and initializes all subsystems: it opens databases, loads keys, configures the overlay, and wires up timers. `start()` begins background activity (I/O threads, job queue, sweep timers) and can optionally skip timer startup for unit tests. `run()` blocks until the node is told to stop. `signalStop()` is the canonical shutdown path, accepting a human-readable reason string; it sets an atomic flag that the run loop checks. + +## The Master Mutex + +```cpp +using MutexType = std::recursive_mutex; +virtual MutexType& getMasterMutex() = 0; +``` + +The master mutex serializes access to the open ledger and to the global consensus state (which ledger is last-closed, what round the consensus engine is in). A `std::recursive_mutex` is used deliberately: the consensus and ledger code contains call chains that legitimately re-enter while already holding the lock, and a plain `mutex` would deadlock there. The VFALCO comment acknowledges this is not ideal — it is a historical artifact of the monolithic design. + +## Forward Declarations and Header Loops + +The header opens with roughly thirty forward declarations across the `xrpl` namespace. The comment `// VFALCO TODO Fix forward declares required for header dependency loops` is honest about why: `Application.h` is included by nearly every subsystem in the repository. If it pulled in even a few of their full headers transitively, build times would explode and circular dependencies would become unavoidable. The concrete `ApplicationImp` in the `.cpp` file pays the full cost, including ~45 headers, so that cost is incurred only once per build. + +The `TaggedCache` template is forward-declared with its full parameter list at the top of the file, and then two aliases are defined — `CachedSLEs` and `NodeCache` — so users of the interface can name those cache types without seeing their implementation. + +## Key Interface Points + +**`instanceID()`** returns a random 64-bit cookie minted at construction time (always non-zero: `1 + rand_int(..., UINT64_MAX - 1)`). This identifies a particular node process run. Peer messages and validation records carry instance context that can be discarded if they predate the current instance, preventing stale state from a previous crash from polluting a fresh start. + +**`getMaxDisallowedLedger()`** is a safety valve for validators returning from downtime. A validator must not sign proposals for ledgers older than the last one it successfully persisted — doing so would create a fork risk if the rest of the network has already moved on. This method returns the persisted high-water mark, and the consensus engine uses it to suppress signatures for earlier ledgers. + +**`checkSigs()` / `checkSigs(bool)`** expose a mutable flag controlling whether incoming transactions have their signatures verified. Disabling verification is used during certain bootstrapping or testing scenarios where the overhead of cryptographic verification would distort measurements. + +**`serverOkay(std::string& reason)`** is the health-check predicate. It returns `true` when the node is fully synchronized and operational, and fills `reason` with a human-readable explanation when it is not. The HTTP `/health` endpoint and the `server_info` RPC both delegate to this method. + +**`fdRequired()`** returns the number of file descriptors the node needs. `Main.cpp` calls `adjustDescriptorLimit()` using this value before the application starts, raising the OS `RLIMIT_NOFILE` if needed — failing to do so would cause the overlay to run out of descriptors and refuse new peer connections under load. + +## Factory Function + +```cpp +std::unique_ptr +make_Application( + std::unique_ptr config, + std::unique_ptr logs, + std::unique_ptr timeKeeper); +``` + +Ownership of the three foundational objects — configuration, logging, and the time source — transfers into the application at construction. Taking them by `unique_ptr` makes the transfer explicit and prevents accidental sharing. The factory function hides `ApplicationImp` from the entire rest of the codebase; this is the only way to construct an `Application`, enforcing the abstraction boundary between the interface header and its implementation. \ No newline at end of file diff --git a/src/xrpld/app/main/BasicApp.cpp.ai.json b/src/xrpld/app/main/BasicApp.cpp.ai.json new file mode 100644 index 0000000000..ddc854155b --- /dev/null +++ b/src/xrpld/app/main/BasicApp.cpp.ai.json @@ -0,0 +1,97 @@ +{ + "args": [ + { + "lineno": 6, + "name": "numberOfThreads" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "BasicApp::BasicApp", + "boost::asio::make_work_guard", + "std::thread::emplace_back (lambda)", + "beast::setCurrentThreadName", + "boost::asio::io_context::run" + ], + "entry_point": "BasicApp::BasicApp", + "purpose": "Initializes the BasicApp, sets up the io_context work guard, spawns threads to run the io_context event loop.", + "validation_points": [] + }, + { + "call_chain": [ + "BasicApp::~BasicApp", + "work_.reset", + "std::thread::join" + ], + "entry_point": "BasicApp::~BasicApp", + "purpose": "Cleans up the BasicApp, stops the io_context work guard, joins all threads.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "numberOfThreads", + "flow": [ + "Constructor argument", + "threads_.reserve(numberOfThreads)", + "while loop decrements numberOfThreads", + "threads_.emplace_back (lambda captures numberOfThreads)" + ], + "origin": "Constructor argument to BasicApp::BasicApp", + "transformations": [ + "Used to reserve thread vector capacity", + "Decremented in loop to control thread creation", + "Passed to lambda for thread naming" + ], + "validated_at": "No explicit validation; assumes caller provides a sensible value" + }, + { + "field": "io_context_", + "flow": [ + "work_.emplace(boost::asio::make_work_guard(io_context_))", + "threads_.emplace_back (lambda calls io_context_.run())" + ], + "origin": "Member variable of BasicApp", + "transformations": [ + "Wrapped in a work guard to keep io_context_ alive", + "Run in multiple threads" + ], + "validated_at": "No explicit validation" + } + ], + "description": "Implements the BasicApp class, which manages an io_context and a thread pool for asynchronous operations, including thread naming and proper shutdown.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/BasicApp.cpp", + "functions": [ + { + "args": [ + "std::size_t numberOfThreads" + ], + "lineno": 6, + "name": "BasicApp::BasicApp" + }, + { + "args": [], + "lineno": 19, + "name": "BasicApp::~BasicApp" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [], + "test_coverage_notes": "This code is a low-level infrastructure component (thread and io_context management). There are no explicit validation code paths (e.g., input checking, error handling) in this file. Test coverage would likely be indirect, via higher-level integration or system tests that exercise server startup/shutdown and concurrency. There are probably no unit tests directly for BasicApp, and no validation-specific tests. Gaps: No checks for zero or excessive thread counts, no error handling for thread creation failures, no validation of io_context_ state.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/app/main/BasicApp.cpp.ai.md b/src/xrpld/app/main/BasicApp.cpp.ai.md new file mode 100644 index 0000000000..02f863d5ac --- /dev/null +++ b/src/xrpld/app/main/BasicApp.cpp.ai.md @@ -0,0 +1,40 @@ +# BasicApp.cpp — `io_context` Thread Pool Bootstrap + +`BasicApp` exists to solve a C++ object lifetime problem that arises throughout the rippled application: many subsystems post work to Boost.Asio's `io_context`, and those subsystems must be guaranteed to complete their async operations and be fully destroyed *before* the `io_context` and its driving threads are torn down. The class is deliberately minimal — just 27 lines of implementation — so that all the lifetime guarantees derive from straightforward RAII and C++ construction/destruction ordering rather than from any complex shutdown protocol. + +## What the Class Owns + +Three members, declared in this exact order: + +``` +std::optional> work_; +std::vector threads_; +boost::asio::io_context io_context_; +``` + +The ordering is not coincidental. C++ destroys members in reverse declaration order, so `io_context_` is the last member to be destroyed. But the real lifetime guarantee comes from the inheritance relationship: `ApplicationImp` (in `Application.cpp`) inherits from `BasicApp`. Because base-class destructors run *after* the derived class destructor, every member of `ApplicationImp` — the job queue, the ledger master, the network peers, and hundreds of other objects that hold references to `io_context_` — is torn down before `BasicApp`'s destructor ever fires. The header comment documents this intent explicitly: *"This is so that the io_context can outlive all the children."* + +## Construction: Work Guard and Thread Pool + +The constructor first installs a `boost::asio::executor_work_guard` via `work_.emplace(...)`. Without this guard, `io_context::run()` would return immediately if no handlers are pending, causing every thread to exit before any real work has been scheduled. The work guard keeps the run loop alive unconditionally until the guard itself is released. + +With the guard in place, the constructor spins up `numberOfThreads` threads, each calling `io_context_.run()`. Boost.Asio's `io_context` is thread-safe for concurrent `run()` calls, so all threads participate in dispatching handlers from the same shared queue. The thread count is determined by `ApplicationImp`'s constructor calling a local `numberOfThreads()` heuristic, which returns 1 on single- or dual-core machines (or when the node is configured for minimal resources) and 6 otherwise. + +Each thread names itself `"io svc #N"` using `beast::setCurrentThreadName()`. The name index comes from the post-decrement loop variable, so threads are numbered from `N-1` down to `0`. This is a diagnostic convenience: crash dumps, `top`, or `htop` output will label these threads clearly rather than showing a generic process name. + +## Destruction: Draining the Pool + +The destructor has a precise two-step sequence: + +1. `work_.reset()` — destroying the `optional` drops the work guard. The `io_context` is now free to return from `run()` once all remaining queued handlers complete. +2. `for (auto& t : threads_) t.join()` — blocks until every thread has exited its `run()` loop. + +The key invariant is that by the time `~BasicApp()` is called, all derived-class members have already been destroyed (because `ApplicationImp`'s destructor ran first). Any async operations those members may have posted will have already been cancelled or completed. Releasing the work guard therefore allows the threads to drain cleanly and exit without missing in-flight work. + +If the work guard were not used — or if the destructor only called `io_context_.stop()` instead — there would be a risk of handlers being abandoned mid-execution. The chosen approach is the safe path: work drains naturally, then threads join. + +## Role in the Larger Application + +`ApplicationImp` inherits `BasicApp` as its first base class and passes a computed thread count at the top of its initializer list, before any other members are initialized. This guarantees the thread pool is running before any subsystem tries to post work. The `get_io_context()` accessor exposes the `io_context` reference to the rest of the application, and `get_number_of_threads()` lets callers — such as the job queue configuration — query pool size at runtime. + +The design is a clean separation of concerns: `BasicApp` handles exactly the problem of owning and safely shutting down an `io_context`-backed thread pool; everything else is `ApplicationImp`'s responsibility. \ No newline at end of file diff --git a/src/xrpld/app/main/BasicApp.h.ai.json b/src/xrpld/app/main/BasicApp.h.ai.json new file mode 100644 index 0000000000..8a84aba478 --- /dev/null +++ b/src/xrpld/app/main/BasicApp.h.ai.json @@ -0,0 +1,33 @@ +{ + "args": [ + { + "lineno": 13, + "name": "numberOfThreads" + } + ], + "classes": [ + { + "args": [ + "std::size_t numberOfThreads" + ], + "lineno": 8, + "name": "BasicApp" + } + ], + "description": "Defines the BasicApp class, which manages a boost::asio::io_context and a thread pool for asynchronous operations, allowing the io_context to outlive its child threads.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/BasicApp.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "get_io_context" + }, + { + "args": [], + "lineno": 23, + "name": "get_number_of_threads" + } + ], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/src/xrpld/app/main/BasicApp.h.ai.md b/src/xrpld/app/main/BasicApp.h.ai.md new file mode 100644 index 0000000000..21e7480f27 --- /dev/null +++ b/src/xrpld/app/main/BasicApp.h.ai.md @@ -0,0 +1,38 @@ +# `BasicApp.h` — IO Context Lifecycle Foundation + +`BasicApp` is a small but architecturally critical base class that owns the `boost::asio::io_context` and its backing thread pool. Its entire purpose is captured in the source comment: *"This is so that the io_context can outlive all the children."* Everything else in the XRPL node application that performs asynchronous I/O — network connections, timers, async jobs — posts work onto this context and assumes it remains live throughout their own lifetimes. + +## Lifetime Ordering via Inheritance + +The key design decision is that `BasicApp` is used as a **base class**, not a member variable. In `Application.cpp`, `ApplicationImp` is declared as: + +```cpp +class ApplicationImp : public Application, public BasicApp +``` + +C++ guarantees that base class subobjects are constructed before derived class members and destroyed after them. Because `BasicApp` appears in the inheritance list, the `io_context_` it owns is initialized before any `ApplicationImp` member, and crucially, `BasicApp::~BasicApp()` runs after all of `ApplicationImp`'s own members have been destroyed. This bracketing guarantees that any subsystem component (ledger store, peer manager, job queue, etc.) can safely dispatch to the `io_context_` at any point during its own destruction without touching a dangling context. + +If the `io_context_` were instead held as a member of `ApplicationImp`, its destruction order relative to other members would be positional and fragile — a refactor could silently break the ordering guarantee. Embedding it in a base class makes the constraint part of the type system. + +## The Work Guard Pattern + +The `work_` member is a `std::optional>`. The Boost.Asio `executor_work_guard` prevents an `io_context` from exiting `run()` when it has no pending handlers. Without it, threads calling `io_context_.run()` would return immediately if the work queue momentarily emptied, making the thread pool fragile during startup or quiet periods. + +Wrapping it in `std::optional` is the idiomatic mechanism for controlled release: `work_.reset()` destroys the guard in `~BasicApp()`, signaling the `io_context` that it is free to stop once all outstanding handlers drain. The destructor then joins each worker thread, waiting for clean completion before the `io_context_` object itself goes out of scope. + +## Thread Pool Construction + +In the constructor, threads are launched eagerly — all `numberOfThreads` are spawned immediately and each calls `io_context_.run()`, blocking until the context stops. Each thread is named `"io svc #N"` via `beast::setCurrentThreadName()`, which aids in debugging by making threads identifiable in profilers and core dumps. The number of threads is reported by `get_number_of_threads()`, primarily used by subsystems that want to reason about parallelism capacity. + +## Destructor Sequence + +The destructor follows a strict two-phase shutdown: + +1. `work_.reset()` — drops the work guard, unblocking `io_context_.run()` once pending handlers finish. +2. `for (auto& t : threads_) t.join()` — waits for every thread to exit before returning. + +This ordering is essential. Resetting the work guard without joining would allow the `BasicApp` destructor to return while threads are still executing inside the `io_context_`, which would then be destroyed, producing undefined behavior. The join ensures that the context's internal state and all in-flight handlers complete before the destructor exits. + +## Interface Surface + +The public API is minimal by design: `get_io_context()` returns a reference to the `io_context_` so derived classes and collaborators can post work, and `get_number_of_threads()` exposes thread count for informational purposes. There is no mechanism to resize the pool at runtime — the thread count is fixed at construction, which is appropriate for a server process where the concurrency budget is determined at startup from configuration. \ No newline at end of file diff --git a/src/xrpld/app/main/CollectorManager.cpp.ai.json b/src/xrpld/app/main/CollectorManager.cpp.ai.json new file mode 100644 index 0000000000..4088233aff --- /dev/null +++ b/src/xrpld/app/main/CollectorManager.cpp.ai.json @@ -0,0 +1,236 @@ +{ + "args": [ + { + "lineno": 6, + "name": "params" + }, + { + "lineno": 6, + "name": "journal" + } + ], + "classes": [ + { + "args": [ + "params", + "journal" + ], + "lineno": 5, + "name": "CollectorManagerImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "make_CollectorManager", + "CollectorManagerImp::CollectorManagerImp" + ], + "entry_point": "make_CollectorManager", + "purpose": "Creates and configures a CollectorManager instance based on configuration parameters.", + "validation_points": [ + "CollectorManagerImp::CollectorManagerImp: server string compared to 'statsd'", + "CollectorManagerImp::CollectorManagerImp: address parsed via beast::IP::Endpoint::from_string" + ] + } + ], + "data_flows": [ + { + "field": "server", + "flow": [ + "params (input)", + "get(params, \"server\")", + "server local variable", + "if (server == \"statsd\")" + ], + "origin": "params Section (configuration input)", + "transformations": [ + "Extracted as string from params", + "Compared to literal 'statsd'" + ], + "validated_at": "CollectorManagerImp::CollectorManagerImp (string comparison)" + }, + { + "field": "address", + "flow": [ + "params (input)", + "get(params, \"address\")", + "beast::IP::Endpoint::from_string(get(params, \"address\"))", + "address local variable", + "used in StatsDCollector::New" + ], + "origin": "params Section (configuration input)", + "transformations": [ + "Extracted as string from params", + "Parsed/validated as IP endpoint" + ], + "validated_at": "CollectorManagerImp::CollectorManagerImp (beast::IP::Endpoint::from_string)" + }, + { + "field": "prefix", + "flow": [ + "params (input)", + "get(params, \"prefix\")", + "prefix local variable", + "used in StatsDCollector::New" + ], + "origin": "params Section (configuration input)", + "transformations": [ + "Extracted as string from params" + ], + "validated_at": "Not explicitly validated in this code" + } + ], + "description": "Implements the CollectorManagerImp class, which manages metrics collectors (such as StatsD or NullCollector) and groups for application monitoring in the xrpl namespace.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "server", + "empty", + "string", + "validation" + ], + "evidence": "string comparison (== \"statsd\") at CollectorManagerImp constructor", + "issue_pattern": "Missing empty string validation for server", + "why_false_positive": "string comparison (== \"statsd\") validates server for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "address", + "empty", + "string", + "validation" + ], + "evidence": "beast::IP::Endpoint::from_string at CollectorManagerImp constructor", + "issue_pattern": "Missing empty string validation for address", + "why_false_positive": "beast::IP::Endpoint::from_string validates address for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "address", + "format", + "validation", + "invalid" + ], + "evidence": "beast::IP::Endpoint::from_string at CollectorManagerImp constructor", + "issue_pattern": "Missing format validation for address", + "why_false_positive": "beast::IP::Endpoint::from_string validates address format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/CollectorManager.cpp", + "functions": [ + { + "args": [ + "params", + "journal" + ], + "lineno": 44, + "name": "make_CollectorManager" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage. Typical tests would be in files like CollectorManager_test.cpp or integration tests for configuration parsing. The validation of 'server' and 'address' depends on correct configuration input; error handling for invalid addresses is not shown here. There may be gaps in testing invalid or missing 'address' or 'server' values, and no explicit tests for malformed input are visible in this code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "beast::IP::Endpoint (for address parsing), manual string checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (defaults to NullCollector if not 'statsd')", + "field": "server", + "location": "CollectorManagerImp constructor", + "validated_by": "string comparison (== \"statsd\")", + "validates": [ + "checks if 'server' field equals 'statsd'" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "throws on invalid address (likely std::exception or custom error from from_string)", + "field": "address", + "location": "CollectorManagerImp constructor", + "validated_by": "beast::IP::Endpoint::from_string", + "validates": [ + "validates that 'address' is a valid IP endpoint string" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/CollectorManager.cpp.ai.md b/src/xrpld/app/main/CollectorManager.cpp.ai.md new file mode 100644 index 0000000000..7859f9d5f9 --- /dev/null +++ b/src/xrpld/app/main/CollectorManager.cpp.ai.md @@ -0,0 +1,32 @@ +# CollectorManager.cpp + +`CollectorManager.cpp` provides the application-level entry point for XRPL's metrics telemetry infrastructure. Its purpose is narrow but important: translate node configuration into a live `beast::insight::Collector` instance that the rest of the application uses to emit counters, gauges, events, and meters — optionally shipping them to a StatsD endpoint. + +## Role in the Application + +The XRP Ledger daemon (`rippled`) exposes internal performance metrics through the `beast::insight` subsystem, a thin abstraction over metrics backends. During application initialization, `Application.cpp` calls `make_CollectorManager()` with the contents of the `[insight]` configuration section (keyed by `SECTION_INSIGHT`) and receives back a `std::unique_ptr`. The resulting object is stored for the application's lifetime and surfaced to subsystems via `getCollectorManager()`. + +## Implementation Pattern + +The file uses an opaque-implementation pattern: the abstract `CollectorManager` interface is declared in the header with only two pure-virtual methods, while the concrete class `CollectorManagerImp` is defined entirely within the `.cpp` file and never visible to callers. The `make_CollectorManager()` factory function is the sole public construction path. This keeps the concrete implementation's dependencies (StatsD, IP address parsing) out of every translation unit that includes the header, avoiding unnecessary recompilation. + +## Collector Selection and Fallback + +The constructor of `CollectorManagerImp` reads a single configuration key — `server` — from the `params` section: + +- If `server == "statsd"`, it constructs a `beast::insight::StatsDCollector` by additionally reading `address` (parsed through `beast::IP::Endpoint::from_string`) and `prefix` (a string prepended to all metric names). +- For any other value — including an absent key or unrecognized backend name — it falls back silently to `beast::insight::NullCollector::New()`. + +The fallback-to-null design is deliberate and defensively sound. All application code that emits metrics does so unconditionally, without checking whether a real backend is connected. `NullCollector` absorbs all metric calls as no-ops. This means misconfiguration or a missing `[insight]` block never crashes the node; it simply disables telemetry. The tradeoff is that silent misconfiguration is possible — if `server` is misspelled, the node starts without metrics and gives no warning. + +Address validation is delegated entirely to `beast::IP::Endpoint::from_string`. If an invalid address is supplied alongside `server = statsd`, `from_string` will throw (or produce an undefined endpoint), propagating the error up through the constructor and ultimately failing application startup. There is no explicit try/catch here, so the error handling is caller-enforced. + +## Groups Abstraction + +After creating the collector, the constructor calls `beast::insight::make_Groups(m_collector)`, storing the result as `std::unique_ptr`. The `group(name)` method delegates to `m_groups->get(name)`, which performs a find-or-create lookup for a named `Group::ptr`. + +`Groups` lets different subsystems partition their metrics under separate name prefixes without ever needing direct access to the underlying collector. A component asks the `CollectorManager` for the group named `"ledger"` or `"consensus"`, then creates its counters and gauges within that group's namespace. This keeps metric naming organized and prevents collisions across unrelated subsystems. + +## Ownership and Lifetime + +`m_collector` is held as `beast::insight::Collector::ptr`, which is a `std::shared_ptr`. The `Groups` object holds its own `shared_ptr` to the same collector internally, which is why `CollectorManagerImp` can destroy its own `m_collector` reference without invalidating any `Group` objects that were handed out — group lifetimes can safely outlive the manager reference if needed, though in practice `CollectorManagerImp`'s `unique_ptr` destructor tears down both `m_groups` and `m_collector` in order when the application shuts down. \ No newline at end of file diff --git a/src/xrpld/app/main/CollectorManager.h.ai.json b/src/xrpld/app/main/CollectorManager.h.ai.json new file mode 100644 index 0000000000..14680710b1 --- /dev/null +++ b/src/xrpld/app/main/CollectorManager.h.ai.json @@ -0,0 +1,38 @@ +{ + "args": [ + { + "lineno": 19, + "name": "params" + }, + { + "lineno": 19, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "CollectorManager" + } + ], + "description": "Defines the CollectorManager interface for providing the beast::insight::Collector service, including its API and a factory function.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/CollectorManager.h", + "functions": [ + { + "args": [ + "params", + "journal" + ], + "lineno": 19, + "name": "make_CollectorManager" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/CollectorManager.h.ai.md b/src/xrpld/app/main/CollectorManager.h.ai.md new file mode 100644 index 0000000000..17dfeb38bf --- /dev/null +++ b/src/xrpld/app/main/CollectorManager.h.ai.md @@ -0,0 +1,27 @@ +# CollectorManager.h — Metrics Collection Service Interface + +`CollectorManager` is the application-level service interface that gates access to the `beast::insight` metrics subsystem for the entire XRPL node. Its sole purpose is to provide a stable seam between application startup configuration and the rest of the codebase's telemetry needs. + +## Role in the System + +The `beast::insight` framework defines a family of metric primitives — counters, gauges, events, meters, and hooks — that can be wired to an external monitoring backend. `CollectorManager` acts as the application's ownership and access point for the concrete `Collector` implementation chosen at startup. It is created once, held as `std::unique_ptr` by `Application`, and consumed by a dozen or more subsystems that each receive a `shared_ptr` or a named `Group` during construction: `JobQueue`, `Resource::Manager`, `LedgerMaster`, `InboundLedgers`, `InboundTransactions`, `NetworkOPs`, `Overlay`, the I/O latency sampler, and more. + +## Interface Design + +The interface exposes only two methods. `collector()` returns a reference to the shared `Collector` pointer — the root factory for all metric objects. `group(name)` returns a named `Group`, which is itself a `Collector` subtype that automatically prefixes every metric it creates with the group name. Subsystems that need namespaced metrics (e.g., the job queue using `"jobq"`) call `group()` to get a scoped factory; subsystems that manage their own namespacing call `collector()` directly. + +Keeping the interface this thin is intentional: `CollectorManager` is not itself a metric creator. It is a lifecycle owner and a locator. Metric creation belongs to the `Collector` interface and is the concern of whichever subsystem needs it. + +## Implementation and Configuration + +The concrete type `CollectorManagerImp` (defined in `CollectorManager.cpp`) is hidden behind the factory function `make_CollectorManager(params, journal)`, which reads from the `[insight]` config section. When `server = "statsd"`, it instantiates a `StatsDCollector` that ships UDP datagrams to the configured StatsD address with a metric-name prefix. Any other value (or absent configuration) produces a `NullCollector` — an implementation whose operations are intentional no-ops. + +This two-path design is a key reliability decision: operators who have not set up a StatsD server pay no overhead and experience no errors. The `NullCollector` fallback means every call site throughout the codebase can unconditionally record metrics without checking whether a backend is configured. The null path is not added later as a workaround — it is the expected default. + +Groups are managed through a `beast::insight::Groups` object, which is constructed over the chosen collector and handles group lifecycle. Calling `group("name")` returns a shared pointer that is cached by the `Groups` object, so multiple subsystems asking for the same group name receive the same instance. + +## Lifecycle and Ownership + +`CollectorManager` is constructed near the top of `Application`'s member-initialization list, before `JobQueue`, `SHAMapStore`, or any network subsystem. This ordering is necessary because those constructors immediately call `m_collectorManager->group(...)` or `m_collectorManager->collector()`. The raw `Collector::ptr` (a `shared_ptr`) is handed to each consumer, so the collector itself outlives the `CollectorManager` wrapper only if a consumer holds the last reference — though in practice `Application` destruction order ensures orderly teardown. + +The abstract interface means test harnesses or alternative implementations can replace `CollectorManagerImp` without touching any consumer. Since all consumers hold a `shared_ptr`, they are entirely decoupled from the management layer. \ No newline at end of file diff --git a/src/xrpld/app/main/GRPCServer.cpp.ai.json b/src/xrpld/app/main/GRPCServer.cpp.ai.json new file mode 100644 index 0000000000..1c92759016 --- /dev/null +++ b/src/xrpld/app/main/GRPCServer.cpp.ai.json @@ -0,0 +1,377 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "service", + "cq", + "app", + "bindListener", + "handler", + "forward", + "requiredCondition", + "loadType", + "secureGatewayIPs" + ], + "lineno": 27, + "name": "GRPCServerImpl::CallData" + }, + { + "args": [ + "app" + ], + "lineno": 210, + "name": "GRPCServerImpl" + }, + { + "args": [], + "lineno": 420, + "name": "GRPCServer" + } + ], + "code_paths": [ + { + "call_chain": [ + "GRPCServerImpl::CallData::process()", + "app_.getJobQueue().postCoro(...)", + "GRPCServerImpl::CallData::process(std::shared_ptr)", + "getUsage()", + "clientIsUnlimited()", + "getClientIpAddress()", + "getClientEndpoint()", + "getEndpoint(peer)" + ], + "entry_point": "GRPCServerImpl::CallData::process()", + "purpose": "Handles incoming gRPC requests, schedules processing, checks resource usage, and validates client endpoint.", + "validation_points": [ + "getEndpoint(peer): Validates and parses the peer (endpoint string) using beast::IP::Endpoint::from_string_checked" + ] + }, + { + "call_chain": [ + "getClientEndpoint()", + "getEndpoint(peer)" + ], + "entry_point": "getClientEndpoint()", + "purpose": "Extracts and validates the client's network endpoint from a string.", + "validation_points": [ + "getEndpoint(peer): Validates endpoint string" + ] + } + ], + "data_flows": [ + { + "field": "peer (endpoint string)", + "flow": [ + "gRPC request arrives", + "peer string extracted (possibly in getClientIpAddress or getClientEndpoint)", + "getEndpoint(peer) called", + "beast::IP::Endpoint::from_string_checked(peerClean)", + "beast::IP::to_asio_endpoint(endpoint.value())", + "used for client identification, resource limiting, or logging" + ], + "origin": "gRPC context (ctx_) or request metadata", + "transformations": [ + "peer string may be cleaned (scheme/port stripped)", + "converted to beast::IP::Endpoint", + "converted to boost::asio::ip::tcp::endpoint" + ], + "validated_at": "getEndpoint (via beast::IP::Endpoint::from_string_checked)" + }, + { + "field": "request_ (gRPC request object)", + "flow": [ + "gRPCServerImpl::CallData constructed", + "request_ populated by gRPC", + "handler_ or forward_ invoked with request_", + "response generated and sent via responder_" + ], + "origin": "gRPC framework (populated by incoming RPC)", + "transformations": [ + "request_ is passed as-is to handler/forward", + "may be inspected for fields, but not validated in this file" + ], + "validated_at": "Not validated in this file" + } + ], + "description": "Implements the gRPC server for the XRPLedger, including request handling, server lifecycle management, and resource usage enforcement.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "peer (endpoint string passed to getEndpoint)", + "validation", + "missing", + "check" + ], + "evidence": "Field peer (endpoint string passed to getEndpoint) validated by beast::IP (for endpoint parsing/validation)", + "issue_pattern": "Missing validation for peer (endpoint string passed to getEndpoint)", + "why_false_positive": "beast::IP (for endpoint parsing/validation) validates peer (endpoint string passed to getEndpoint) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "peer (endpoint string)", + "empty", + "string", + "validation" + ], + "evidence": "beast::IP::Endpoint::from_string_checked at getEndpoint (static helper function)", + "issue_pattern": "Missing empty string validation for peer (endpoint string)", + "why_false_positive": "beast::IP::Endpoint::from_string_checked validates peer (endpoint string) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "peer (endpoint string)", + "format", + "validation", + "invalid" + ], + "evidence": "beast::IP::Endpoint::from_string_checked at getEndpoint (static helper function)", + "issue_pattern": "Missing format validation for peer (endpoint string)", + "why_false_positive": "beast::IP::Endpoint::from_string_checked validates peer (endpoint string) format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/GRPCServer.cpp", + "functions": [ + { + "args": [ + "peer" + ], + "lineno": 10, + "name": "getEndpoint" + }, + { + "args": [], + "lineno": 54, + "name": "clone" + }, + { + "args": [], + "lineno": 62, + "name": "process" + }, + { + "args": [ + "coro" + ], + "lineno": 84, + "name": "process" + }, + { + "args": [], + "lineno": 134, + "name": "isFinished" + }, + { + "args": [], + "lineno": 140, + "name": "getLoadType" + }, + { + "args": [ + "isUnlimited" + ], + "lineno": 146, + "name": "getRole" + }, + { + "args": [], + "lineno": 156, + "name": "getUser" + }, + { + "args": [], + "lineno": 167, + "name": "getClientIpAddress" + }, + { + "args": [], + "lineno": 174, + "name": "getClientEndpoint" + }, + { + "args": [], + "lineno": 179, + "name": "clientIsUnlimited" + }, + { + "args": [ + "response", + "isUnlimited" + ], + "lineno": 192, + "name": "setIsUnlimited" + }, + { + "args": [], + "lineno": 202, + "name": "getUsage" + }, + { + "args": [ + "app" + ], + "lineno": 210, + "name": "GRPCServerImpl" + }, + { + "args": [], + "lineno": 253, + "name": "shutdown" + }, + { + "args": [], + "lineno": 273, + "name": "handleRpcs" + }, + { + "args": [], + "lineno": 340, + "name": "setupListeners" + }, + { + "args": [], + "lineno": 393, + "name": "start" + }, + { + "args": [], + "lineno": 414, + "name": "getEndpoint" + }, + { + "args": [], + "lineno": 420, + "name": "start" + }, + { + "args": [], + "lineno": 432, + "name": "stop" + }, + { + "args": [], + "lineno": 441, + "name": "~GRPCServer" + }, + { + "args": [], + "lineno": 446, + "name": "getEndpoint" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is primarily server-side infrastructure. Validation of the peer endpoint string is handled in getEndpoint, which is likely tested indirectly via integration or system tests that exercise gRPC endpoints with various client addresses. There is no evidence of direct unit tests for getEndpoint in this file. Test coverage for malformed or malicious endpoint strings depends on higher-level gRPC server tests, possibly in test files under workflow/XRPLF-rippled-develop/test or similar directories. Direct unit tests for endpoint parsing/validation may be missing unless beast::IP::Endpoint::from_string_checked is tested elsewhere. Error handling (exceptions, empty optionals) is present but not explicitly tested here.", + "validation_architecture": { + "auto_validated_fields": [ + "peer (endpoint string passed to getEndpoint)" + ], + "framework": "beast::IP (for endpoint parsing/validation)", + "validation_layer": "entry_point (input parsing before use in server setup)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::exception (caught and ignored, returns std::nullopt)", + "field": "peer (endpoint string)", + "location": "getEndpoint (static helper function)", + "validated_by": "beast::IP::Endpoint::from_string_checked", + "validates": [ + "Checks if the input string is a valid IP address (IPv4 or IPv6), optionally with port and scheme", + "Handles malformed input by catching exceptions" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/GRPCServer.cpp.ai.md b/src/xrpld/app/main/GRPCServer.cpp.ai.md new file mode 100644 index 0000000000..71efccaf2d --- /dev/null +++ b/src/xrpld/app/main/GRPCServer.cpp.ai.md @@ -0,0 +1,61 @@ +# GRPCServer.cpp + +## Role and Context + +`GRPCServer.cpp` implements the asynchronous gRPC server that runs alongside the existing JSON/HTTP RPC interface in `xrpld`. It exposes a small set of ledger-data RPCs — `GetLedger`, `GetLedgerData`, `GetLedgerDiff`, and `GetLedgerEntry` — over Google's Protocol Buffer-based transport. The server is optional and only activates when a `[port_grpc]` section is present in the node's configuration file, meaning existing deployments are unaffected. + +## Architecture: Three Layers + +The implementation is split across three collaborating types. + +**`GRPCServer`** is the public RAII wrapper. It owns a `std::thread` and a `GRPCServerImpl`, and presents a clean `start()` / `stop()` interface. The destructor asserts `running_ == false`, making it a programming error to destroy the server without first stopping it. The thread is named `xrpld: grpc` via `beast::setCurrentThreadName`. + +**`GRPCServerImpl`** handles everything at the gRPC infrastructure level: parsing configuration, building the gRPC server, owning the completion queue (`cq_`), and running the `handleRpcs()` event loop. The constructor reads `ip`, `port`, and the optional `secure_gateway` list from the `[port_grpc]` config section. The `secure_gateway` field accepts a comma-separated list of IP addresses; these IPs receive privileged access (see below). If either `ip` or `port` is absent, `serverAddress_` stays empty and `start()` returns false without binding. + +**`CallData`** is a private inner template class of `GRPCServerImpl` that captures the state needed for one in-flight or pending RPC. It inherits from both `Processor` (the polymorphic interface used by the event loop) and `std::enable_shared_from_this` (so it can capture itself in the coroutine lambda safely). Each instantiation is bound to a concrete protobuf Request/Response pair and carries three function objects: `bindListener_` (how to register with the async service), `handler_` (how to populate a response), and `forward_` (stub method for forwarding, though forwarding logic is declared in the header but not exercised in the current `.cpp` paths). + +## Completion Queue Event Loop + +The gRPC async server pattern used here is the canonical "reactor on a completion queue" approach, but implemented manually. The flow during normal operation: + +1. `setupListeners()` creates one `CallData` per RPC type and calls `bindListener_` on each, registering them with the async service and completion queue. Each idle `CallData` acts as a listener token. +2. `handleRpcs()` loops on `cq_->Next(&tag, &ok)`. The `tag` is the raw pointer of whichever `CallData` has an event. +3. When a request arrives (`ok == true`, `!ptr->isFinished()`), the loop calls `ptr->clone()` first to create a replacement listener, then `ptr->process()` to begin handling the request. This ensures the server never goes deaf while a request is in flight. +4. When a response has been sent (`ok == true`, `ptr->isFinished()`), the loop erases the pointer from the live set, destroying the `CallData`. +5. When the server is shut down, idle listeners are cancelled and returned with `ok == false`, causing the loop to erase them immediately. Active requests must complete (send a response) before `cq_->Next()` finally returns false and the loop exits. + +The comment in `handleRpcs()` is explicit about why the requests collection is a `vector` rather than an `unordered_set`: deletion requires a `shared_ptr`, but `cq_->Next()` hands back a raw pointer as tag, and the erase lambda does a linear scan and swap-with-back to avoid an O(n) shift. + +## The `finished_` Flag Timing Invariant + +The two-phase `process()` design contains a subtle but critical ordering constraint. The public `process()` (called from the event loop) sets `finished_ = true` *before* dispatching work to the job queue. This is necessary because `responder_.Finish()` inside the worker coroutine posts a completion event back to `cq_`, and by the time `handleRpcs()` dequeues that event, it must see `isFinished() == true` to know the object should be destroyed rather than treated as a new request. If `finished_` were set after dispatch, a race would exist where the response completion arrives before the flag is visible. The field itself is declared `std::atomic_bool` in the header as a forward-looking safety measure, even though the current single-thread design on the completion queue side does not strictly require atomicity today. + +## Request Processing and Resource Enforcement + +The actual per-request work runs inside a `JobQueue::Coro` posted with type `jtRPC`. If the job queue has already been stopped, `process()` immediately calls `responder_.FinishWithError` with `INTERNAL`. This prevents new gRPC requests from being accepted during node shutdown. + +Inside the coroutine (`process(coro)`), the server: + +1. Resolves the `Resource::Consumer` for this client via `getUsage()`, which calls `app_.getResourceManager().newInboundEndpoint(...)` using the decoded client endpoint. If the endpoint cannot be decoded, it throws. +2. Checks whether the client is "unlimited" by verifying that the request contains a non-empty `user` field and that the client IP appears in the `secureGatewayIPs_` list. This two-part check prevents IP-spoofed escalation. +3. If not unlimited, calls `usage.disconnect()`. If the usage balance exceeds threshold, the server responds with `RESOURCE_EXHAUSTED` rather than processing the request. +4. Charges the request's `loadType` (uniformly `feeMediumBurdenRPC` for all four current RPCs) and resolves the `Role`: `IDENTIFIED` for unlimited clients, `USER` otherwise. +5. Checks `RPC::conditionMet()` — the `NO_CONDITION` value used for all current RPCs means this always passes, but the hook is in place for future RPCs that require specific server states (e.g., full history). +6. Calls the handler, receives a `(Response, grpc::Status)` pair, stamps `is_unlimited` on the response if applicable (using protobuf reflection to check whether the response type even has that field), and sends it via `responder_.Finish()`. + +## Endpoint Parsing + +The file-local `getEndpoint()` helper deals with the variety of formats gRPC uses to represent peer addresses, which may include a scheme prefix such as `ipv4:` or `ipv6:`. The logic finds the first and last `:` in the string: if they differ, there is either an IPv6 address or a scheme prefix, and the helper strips everything up to and including the first colon. The cleaned string is then handed to `beast::IP::Endpoint::from_string_checked`, which returns an empty optional rather than throwing on malformed input. All exceptions at this layer are silently swallowed, and an empty `std::optional` is propagated up to the callers that use it for logging and rate-limiting — a reasonable defensive choice since a peer address that cannot be decoded should not crash the server. + +## Shutdown Sequence + +`GRPCServerImpl::shutdown()` follows the ordering required by gRPC: `server_->Shutdown()` first, then `cq_->Shutdown()`. Reversing these would cause a hang, because active requests need the server alive to send their responses before the completion queue can drain. The comments in the source spell this out explicitly, as it is a non-obvious gRPC API constraint. + +## Configuration Summary + +The `[port_grpc]` section supports: +- `ip` — bind address (required) +- `port` — TCP port (required; port 0 is accepted and the actually-bound port is stored in `serverPort_` after `BuildAndStart`) +- `secure_gateway` — comma-separated list of trusted IP addresses whose requests include a `user` field and are exempt from rate limiting + +No TLS support is wired in; the server always uses `grpc::InsecureServerCredentials()`. Security for production deployments is expected to be handled at the network or proxy layer. \ No newline at end of file diff --git a/src/xrpld/app/main/GRPCServer.h.ai.json b/src/xrpld/app/main/GRPCServer.h.ai.json new file mode 100644 index 0000000000..ab022d07e1 --- /dev/null +++ b/src/xrpld/app/main/GRPCServer.h.ai.json @@ -0,0 +1,248 @@ +{ + "args": [ + { + "lineno": 61, + "name": "app" + }, + { + "lineno": 101, + "name": "service" + }, + { + "lineno": 102, + "name": "cq" + }, + { + "lineno": 103, + "name": "app" + }, + { + "lineno": 104, + "name": "bindListener" + }, + { + "lineno": 105, + "name": "handler" + }, + { + "lineno": 106, + "name": "forward" + }, + { + "lineno": 107, + "name": "requiredCondition" + }, + { + "lineno": 108, + "name": "loadType" + }, + { + "lineno": 109, + "name": "secureGatewayIPs" + }, + { + "lineno": 163, + "name": "isUnlimited" + }, + { + "lineno": 198, + "name": "response" + }, + { + "lineno": 198, + "name": "isUnlimited" + }, + { + "lineno": 211, + "name": "context" + } + ], + "classes": [ + { + "args": [], + "lineno": 10, + "name": "Processor" + }, + { + "args": [ + "Application& app" + ], + "lineno": 34, + "name": "GRPCServerImpl" + }, + { + "args": [ + "org::xrpl::rpc::v1::XRPLedgerAPIService::AsyncService& service, grpc::ServerCompletionQueue& cq, Application& app, BindListener bindListener, Handler handler, Forward forward, RPC::Condition requiredCondition, Resource::Charge loadType, std::vector const& secureGatewayIPs" + ], + "lineno": 97, + "name": "CallData" + }, + { + "args": [ + "Application& app" + ], + "lineno": 216, + "name": "GRPCServer" + } + ], + "description": "Defines the gRPC server implementation for XRPL, including request processing, listener setup, and client endpoint handling. Provides classes for managing asynchronous gRPC requests and server lifecycle.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/GRPCServer.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "process" + }, + { + "args": [], + "lineno": 25, + "name": "clone" + }, + { + "args": [], + "lineno": 30, + "name": "isFinished" + }, + { + "args": [], + "lineno": 67, + "name": "shutdown" + }, + { + "args": [], + "lineno": 72, + "name": "start" + }, + { + "args": [], + "lineno": 77, + "name": "handleRpcs" + }, + { + "args": [], + "lineno": 82, + "name": "setupListeners" + }, + { + "args": [], + "lineno": 87, + "name": "getEndpoint" + }, + { + "args": [], + "lineno": 143, + "name": "process" + }, + { + "args": [], + "lineno": 146, + "name": "isFinished" + }, + { + "args": [], + "lineno": 149, + "name": "clone" + }, + { + "args": [ + "coro" + ], + "lineno": 154, + "name": "process" + }, + { + "args": [], + "lineno": 158, + "name": "getLoadType" + }, + { + "args": [ + "isUnlimited" + ], + "lineno": 162, + "name": "getRole" + }, + { + "args": [], + "lineno": 166, + "name": "getUsage" + }, + { + "args": [], + "lineno": 170, + "name": "getClientIpAddress" + }, + { + "args": [], + "lineno": 175, + "name": "getClientEndpoint" + }, + { + "args": [], + "lineno": 181, + "name": "getProxiedClientIpAddress" + }, + { + "args": [], + "lineno": 187, + "name": "getProxiedClientEndpoint" + }, + { + "args": [], + "lineno": 193, + "name": "getUser" + }, + { + "args": [ + "response", + "isUnlimited" + ], + "lineno": 197, + "name": "setIsUnlimited" + }, + { + "args": [], + "lineno": 202, + "name": "clientIsUnlimited" + }, + { + "args": [], + "lineno": 206, + "name": "wasForwarded" + }, + { + "args": [ + "context" + ], + "lineno": 210, + "name": "forwardToP2p" + }, + { + "args": [], + "lineno": 221, + "name": "start" + }, + { + "args": [], + "lineno": 224, + "name": "stop" + }, + { + "args": [], + "lineno": 227, + "name": "~GRPCServer" + }, + { + "args": [], + "lineno": 229, + "name": "getEndpoint" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/GRPCServer.h.ai.md b/src/xrpld/app/main/GRPCServer.h.ai.md new file mode 100644 index 0000000000..d4805879c0 --- /dev/null +++ b/src/xrpld/app/main/GRPCServer.h.ai.md @@ -0,0 +1,51 @@ +# `GRPCServer.h` — Asynchronous gRPC Server for the XRP Ledger API + +This header defines the complete infrastructure for running an asynchronous gRPC server inside `xrpld`. It exposes a binary protobuf/gRPC API (`XRPLedgerAPIService` v1) as an alternative to the existing JSON-RPC interface, primarily targeting high-throughput API servers like Clio that need efficient ledger data access. The file contains three cooperating types: the abstract `Processor` interface, the monolithic `GRPCServerImpl` (which houses the async event loop and the template `CallData` request handler), and the thin `GRPCServer` wrapper that owns the background thread. + +## The Async Completion Queue Pattern + +Rather than using gRPC's simpler synchronous API, the implementation adopts gRPC's async server pattern built around a `grpc::ServerCompletionQueue`. In this model, a `CallData` object does not block waiting for a request — it registers itself as a listener by calling `bindListener_()` at construction time and then returns. The gRPC runtime delivers a notification (via `cq_->Next()`) with a raw pointer tag identifying which `CallData` was triggered. The event loop in `handleRpcs()` casts this tag back to a `Processor*` and decides what to do. + +This design avoids one-thread-per-request overhead and integrates naturally with `xrpld`'s existing single-threaded event dispatch discipline. + +## `Processor` — the Request Lifecycle Contract + +The `Processor` interface enforces a strict three-method protocol: `process()` performs the work (callable only once per instance), `clone()` produces a fresh listener of the same type ready to accept the next request, and `isFinished()` signals when the object is safe to destroy. `Processor` is non-copyable, making the clone-not-copy idiom explicit. + +## `CallData` — One Object Per In-Flight Request + +`CallData` is a private template nested inside `GRPCServerImpl`, parameterized by a protobuf request type and its matching response type. Each instance carries its own `grpc::ServerContext`, a `grpc::ServerAsyncResponseWriter`, and the three injected function objects: + +- `BindListener` — a member-function pointer such as `AsyncService::RequestGetLedger`, used to re-arm the completion queue for the next incoming request. +- `Handler` — the actual business logic (e.g., `doLedgerGrpc`), returning `std::pair`. +- `Forward` — a stub method pointer for proxying the request to a peer node. + +**The clone-before-process invariant** is critical. When `handleRpcs()` sees a new request arrive, it calls `clone()` on the `CallData` first — creating a new listener for subsequent requests of the same type — and only then calls `process()` on the original. Without this ordering, the server would stop accepting new requests of that type while one was being processed. + +**The `finished_` flag timing** is equally subtle. `process()` sets `finished_ = true` *before* dispatching work to the job queue. The reason: as soon as `responder_.Finish()` or `responder_.FinishWithError()` is called (inside the coroutine), the completion queue returns this same `CallData*` as a tag again (signaling the send completed). The event loop checks `isFinished()` to decide whether to clone-and-process or destroy. If `finished_` were set after the send, there would be a race window where the tag was returned but `isFinished()` still returned false. The flag is `std::atomic` as a defensive measure even though currently single-threaded, as the comment explains. + +## Job Queue Integration + +Rather than processing requests inline, `process()` submits a coroutine to `app_.getJobQueue()` as `jtRPC`. This keeps gRPC request handling inside `xrpld`'s cooperative scheduling fabric, sharing thread pool resources and load-shedding logic with JSON-RPC requests. If the job queue is already shut down, `process()` immediately replies with `INTERNAL` error before the coroutine runs. + +Inside the coroutine, `process(coro)` assembles a `RPC::GRPCContext` — the gRPC analog of `RPC::JsonContext`, reusing the same `Context` base struct with a protobuf `params` field instead of `Json::Value`. This context is passed to the registered `Handler`. + +## Resource Management and the Secure Gateway + +The resource layer is fully integrated. `getUsage()` registers the client's IP with `app_.getResourceManager()` and returns a `Resource::Consumer`. If the consumer's balance exceeds the disconnect threshold (and the client is not unlimited), the request is refused with `RESOURCE_EXHAUSTED` before touching the handler. + +Clients connecting from an IP in `secureGatewayIPs_` (configured via `secure_gateway` in `[port_grpc]`) and providing a non-empty `user` field in their request are granted `Role::IDENTIFIED`, which exempts them from resource limits. This allows a trusted Clio proxy to send requests on behalf of many end users without being rate-limited at the gateway level. The `setIsUnlimited()` method uses protobuf reflection to stamp `is_unlimited = true` into the response if the field exists, letting the caller know their elevated status. + +## `handleRpcs()` — The Event Loop + +`handleRpcs()` runs on its own dedicated thread (`"xrpld: grpc"`). It calls `setupListeners()` to seed one `CallData` per RPC method, then enters a `cq_->Next()` loop. Each iteration yields either a new request (`ok = true`, `isFinished() = false`), a completed send (`ok = true`, `isFinished() = true`), or a cancelled listener during shutdown (`ok = false`). Cancellations and completed objects are erased from the `requests_` vector; new requests cause a clone to be pushed before calling `process()`. The loop exits when `cq_->Next()` returns false, meaning the completion queue has drained after `cq_->Shutdown()`. + +## Registered RPC Methods + +`setupListeners()` registers exactly four ledger-query RPCs: `GetLedger`, `GetLedgerData`, `GetLedgerDiff`, and `GetLedgerEntry`. All carry `feeMediumBurdenRPC` load and `NO_CONDITION`, meaning they require no special server state (such as a fully synced ledger) before executing. These are deliberately read-only bulk data operations, matching the workload pattern of state-synchronisation tools. + +## `GRPCServer` — Lifecycle Wrapper + +`GRPCServer` is a thin facade over `GRPCServerImpl`. It holds the impl as a value member (no heap allocation), owns the `std::thread`, and enforces a clean lifecycle contract: `start()` launches `handleRpcs()` on the thread only if `GRPCServerImpl::start()` successfully binds a port; `stop()` calls `impl_.shutdown()` (which drains the queue) and then `thread_.join()`. The destructor asserts `!running_`, which means callers must invoke `stop()` before destroying the object — a hard invariant rather than a silent cleanup. + +`getEndpoint()` exposes the actual bound address and port after startup, supporting port `0` in configuration for OS-assigned port allocation, which is useful in test harnesses that need to avoid port conflicts. \ No newline at end of file diff --git a/src/xrpld/app/main/LoadManager.cpp.ai.json b/src/xrpld/app/main/LoadManager.cpp.ai.json new file mode 100644 index 0000000000..a72611cdf3 --- /dev/null +++ b/src/xrpld/app/main/LoadManager.cpp.ai.json @@ -0,0 +1,325 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "LoadManager::start", + "XRPL_ASSERT", + "std::thread constructor (LoadManager::run)" + ], + "entry_point": "LoadManager::start", + "purpose": "Starts the LoadManager background thread, ensuring only one thread is running.", + "validation_points": [ + "XRPL_ASSERT(!thread_.joinable(), ...): Validates that no thread is already running before starting a new one." + ] + }, + { + "call_chain": [ + "LoadManager::stop", + "cv_.notify_all", + "thread_.join" + ], + "entry_point": "LoadManager::stop", + "purpose": "Stops the LoadManager thread safely.", + "validation_points": [ + "thread_.joinable() check: Ensures thread is joinable before joining." + ] + }, + { + "call_chain": [ + "LoadManager::activateStallDetector" + ], + "entry_point": "LoadManager::activateStallDetector", + "purpose": "Arms the stall detector and sets the last heartbeat time.", + "validation_points": [] + }, + { + "call_chain": [ + "LoadManager::heartbeat" + ], + "entry_point": "LoadManager::heartbeat", + "purpose": "Updates the last heartbeat time to now.", + "validation_points": [] + }, + { + "call_chain": [ + "LoadManager::run", + "cv_.wait_until", + "Check armed_ and timeSpentStalled", + "JLOG (logging)", + "app_.getJobQueue().isOverloaded()", + "app_.getJobQueue().getJson(0)" + ], + "entry_point": "LoadManager::run", + "purpose": "Main loop for stall detection and reporting.", + "validation_points": [ + "Checks armed_ and timeSpentStalled to determine if a stall is occurring." + ] + } + ], + "data_flows": [ + { + "field": "thread_", + "flow": [ + "constructor", + "start() (assigned new std::thread)", + "stop() (joined and cleaned up)" + ], + "origin": "LoadManager constructor (default-initialized)", + "transformations": [ + "Assigned a new std::thread in start()", + "Joined and destroyed in stop()" + ], + "validated_at": "XRPL_ASSERT(!thread_.joinable()) in start()" + }, + { + "field": "armed_", + "flow": [ + "constructor", + "activateStallDetector() (set to true)", + "run() (read for stall detection logic)" + ], + "origin": "LoadManager constructor (default false)", + "transformations": [ + "Set to true in activateStallDetector()" + ], + "validated_at": "Checked in run() before stall logic" + }, + { + "field": "lastHeartbeat_", + "flow": [ + "constructor", + "activateStallDetector() (set to now)", + "heartbeat() (set to now)", + "run() (read to compute timeSpentStalled)" + ], + "origin": "LoadManager constructor (default-initialized)", + "transformations": [ + "Set to current time in activateStallDetector() and heartbeat()" + ], + "validated_at": "Used in run() to compute timeSpentStalled" + }, + { + "field": "stop_", + "flow": [ + "constructor", + "stop() (set to true)", + "run() (checked in wait_until predicate)" + ], + "origin": "LoadManager constructor (default false)", + "transformations": [ + "Set to true in stop()" + ], + "validated_at": "Checked in run() loop to break and exit thread" + } + ], + "description": "Implements the LoadManager class for monitoring and managing server load and detecting stalls in the XRPL server application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "thread_ (std::thread member)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at LoadManager::start", + "issue_pattern": "Missing empty string validation for thread_ (std::thread member)", + "why_false_positive": "XRPL_ASSERT macro validates thread_ (std::thread member) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/LoadManager.cpp", + "functions": [ + { + "args": [ + "Application& app", + "beast::Journal journal" + ], + "lineno": 12, + "name": "LoadManager::LoadManager" + }, + { + "args": [], + "lineno": 17, + "name": "LoadManager::~LoadManager" + }, + { + "args": [], + "lineno": 31, + "name": "LoadManager::activateStallDetector" + }, + { + "args": [], + "lineno": 38, + "name": "LoadManager::heartbeat" + }, + { + "args": [], + "lineno": 46, + "name": "LoadManager::start" + }, + { + "args": [], + "lineno": 55, + "name": "LoadManager::stop" + }, + { + "args": [], + "lineno": 69, + "name": "LoadManager::run" + }, + { + "args": [ + "Application& app", + "beast::Journal journal" + ], + "lineno": 120, + "name": "make_LoadManager" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage (no test code or test macros). Likely, tests for LoadManager would exist in the unit test suite for the application, possibly in files like 'LoadManager_test.cpp' or integration tests for server startup/shutdown and stall detection. The XRPL_ASSERT validation in start() is critical but may not be directly tested unless there are tests that attempt to start LoadManager twice. The stall detection logic (armed_, lastHeartbeat_) would require timing-based or mock-based tests, which are often tricky and may not be fully covered. Exception handling in the destructor is also hard to test. Overall, validation of thread_ via XRPL_ASSERT is the main explicit validation, and coverage for this is likely partial unless specifically tested for double-start scenarios.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT macro (custom assertion, not a framework)", + "notes": "No explicit input/field validation framework is used. Validation is performed via assertions and C++ type system. No template, JSON, or external input validation is present in this code.", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "thread_ (std::thread member)", + "location": "LoadManager::start", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures thread_ is not already joinable before starting a new thread", + "Prevents starting the LoadManager multiple times" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/LoadManager.cpp.ai.md b/src/xrpld/app/main/LoadManager.cpp.ai.md new file mode 100644 index 0000000000..4ddafd128e --- /dev/null +++ b/src/xrpld/app/main/LoadManager.cpp.ai.md @@ -0,0 +1,45 @@ +# `LoadManager.cpp` — Server Stall Detection and Load Monitoring + +`LoadManager` is a small but safety-critical subsystem that runs a single dedicated background thread responsible for two jobs: detecting when the XRPL server's main processing loop has stalled, and adjusting the node's local transaction fee when the job queue is overloaded. The file is the complete implementation of the class declared in `LoadManager.h`. + +## Why It Exists + +The XRPL server processes everything — consensus rounds, transaction validation, ledger closes — through a timed event loop in `NetworkOPs`. If that loop stalls (deadlock, starvation, an I/O thread hung under the master mutex), the server appears alive but is silently frozen. Without an independent watchdog thread, such a stall would be invisible: no logs, no automatic recovery. `LoadManager` provides that watchdog, operating outside the main event loop so it can report stalls even when the event loop itself is blocked. + +## Thread Lifecycle + +`start()` spawns `std::thread{&LoadManager::run, this}` and enforces via `XRPL_ASSERT(!thread_.joinable())` that only one thread is ever running. `stop()` sets `stop_ = true` under the mutex and calls `cv_.notify_all()`, waking the sleeping thread so it can exit cleanly. `thread_.join()` then blocks until the thread finishes. + +The destructor calls `stop()` inside a `try/catch` that swallows exceptions, which is the standard defensive pattern for destructors that must not propagate — a stall during shutdown should not also crash the process with an uncaught exception. + +## Stall Detection: The Heartbeat Pattern + +The stall detector relies on a two-party contract. Every ~1 second, `NetworkOPs::processHeartbeatTimer()` — the main ledger/consensus timer — calls `LoadManager::heartbeat()` under the master mutex, which updates `lastHeartbeat_` to the current monotonic time. Concurrently, the `run()` loop wakes each second, snapshots `lastHeartbeat_` under the lock, unlocks, and then computes `timeSpentStalled = now - lastHeartbeat_`. + +If `heartbeat()` is being called regularly, `timeSpentStalled` stays near zero. If the event loop is blocked, `heartbeat()` stops being called, and `timeSpentStalled` grows. The threshold cascade is: + +- **10 seconds**: First warning log. If the job queue is also overloaded, its full JSON state is dumped. +- **90 seconds**: Log level escalates from `warn` to `fatal` — same heartbeat threshold, but the message category changes. +- **600 seconds**: `LogicError("Fatal server stall detected")` is thrown. At this point the stall-resolution mechanisms have clearly failed and the process is expected to abort. + +The reporting fires every `reportingIntervalSeconds` (10s) via the expression `(timeSpentStalled % reportingIntervalSeconds) == 0s`, which suppresses duplicate log lines within each 10-second window. This avoids flooding the log with one message per second once a stall is detected. + +## The "Armed" State + +The stall detector starts disarmed (`armed_ = false`). `activateStallDetector()` must be called explicitly — in practice, `Application::run()` calls it after all initialization is complete, just before entering the main wait on `isTimeToStop`. A VFALCO note in `Application.cpp` even questions whether this arming step is necessary at all, suggesting it was introduced specifically to prevent false stall alarms during what could be a lengthy startup sequence. The armed flag prevents the watchdog from firing during cold-start initialization, when long operations like loading the ledger state from disk could otherwise trigger spurious stall reports. + +## Fee Adjustment — An Unusual Placement + +The `run()` function contains a fee-adjustment block that reads the `JobQueue` overload state and calls either `getFeeTrack().raiseLocalFee()` or `lowerLocalFee()`. Critically, this block sits **after** the `while (true)` loop — it executes exactly once, when the thread exits because `stop_` has been set. This means fee adjustment happens only at shutdown, which is architecturally odd. + +A VFALCO TODO comment at the call site notes the intent to replace the direct `reportFeeChange()` call with a listener/observer pattern. This suggests the fee-adjustment logic is vestigial from an earlier design where `run()` also managed periodic fee updates from within the loop. The current code, as written, raises or lowers the fee once on teardown, then notifies `NetworkOPs` via `app_.getOPs().reportFeeChange()`. + +## Concurrency Design + +The class uses a single `std::mutex` guarding `lastHeartbeat_`, `armed_`, and the condition variable `cv_`. The background thread uses `std::unique_lock` with `cv_.wait_until()` for the 1-second sleep, releasing the lock during the actual stall-time computation so `heartbeat()` on the main thread can proceed without contention. The brief copy-under-lock / compute-outside-lock pattern at lines 99–101 is a correct and minimal critical section: only the two `std::chrono::time_point` and `bool` copies are taken inside the lock, and the `duration_cast` arithmetic is done after releasing it. + +The `stop_` flag itself is also protected by `mutex_` (not `std::atomic`) because it must be coordinated with the condition variable notification: `stop_ = true` and `cv_.notify_all()` must be atomic from the waiting thread's perspective, which `std::lock_guard` on `mutex_` ensures. + +## Relationship to Surrounding Code + +`LoadManager` is constructed by the `make_LoadManager()` factory (which uses `new` directly because the constructor is private, exposing it only to the `friend` factory) and stored as `m_loadManager` in `ApplicationImpl`. `NetworkOPs` holds a reference to the `LoadManager` through `Application::getLoadManager()` and calls `heartbeat()` once per consensus timer tick. `LoadFeeTrack` provides the fee escalation/reduction primitives, and `JobQueue::isOverloaded()` / `getJson()` supply the diagnostic data that the monitor logs when a stall is detected. \ No newline at end of file diff --git a/src/xrpld/app/main/LoadManager.h.ai.json b/src/xrpld/app/main/LoadManager.h.ai.json new file mode 100644 index 0000000000..4d5d7b1e07 --- /dev/null +++ b/src/xrpld/app/main/LoadManager.h.ai.json @@ -0,0 +1,56 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app, beast::Journal journal" + ], + "lineno": 13, + "name": "LoadManager" + } + ], + "description": "Defines the LoadManager class, which manages load sources and detects server stalls in the XRPL server, including thread management and stall detection mechanisms.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/LoadManager.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "activateStallDetector" + }, + { + "args": [], + "lineno": 49, + "name": "heartbeat" + }, + { + "args": [], + "lineno": 57, + "name": "start" + }, + { + "args": [], + "lineno": 60, + "name": "stop" + }, + { + "args": [], + "lineno": 64, + "name": "run" + }, + { + "args": [ + "Application& app", + "beast::Journal journal" + ], + "lineno": 80, + "name": "make_LoadManager" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/LoadManager.h.ai.md b/src/xrpld/app/main/LoadManager.h.ai.md new file mode 100644 index 0000000000..f1cd4e4741 --- /dev/null +++ b/src/xrpld/app/main/LoadManager.h.ai.md @@ -0,0 +1,38 @@ +# LoadManager.h — Server Stall Detection and Load Monitoring + +`LoadManager` is a lightweight watchdog component that gives the XRPL server operator visibility into two failure conditions: a stalled job queue and unsustainable peer-driven load. It owns a dedicated background thread that ticks every second and enforces a liveness contract between itself and the rest of the application. + +## Role in the System + +The class sits at the intersection of `Application`, `NetworkOPs`, and `LoadFeeTrack`. It is created in `Application`'s constructor via the `make_LoadManager()` factory, started during `Application::setup()`, and stopped during shutdown. The companion `NetworkOPsImp::processHeartbeatTimer()` — which fires on every consensus heartbeat tick — calls `LoadManager::heartbeat()` to prove liveness; if the heartbeat stops arriving, it means the job queue has stalled and work is not being processed. + +## Stall Detection + +The core logic lives in the private `run()` method. The thread sleeps for one second at a time using `cv_.wait_until()`, which doubles as the shutdown signal: when `stop()` sets `stop_ = true` and signals the condition variable, the wait returns immediately and the loop exits. + +Each tick, the thread checks how long ago the last `heartbeat()` call was recorded. The escalation ladder is: + +- **0–10 s**: silent. Transient latency is not a stall. +- **≥ 10 s (and every 10-second multiple thereafter)**: `warn`-level log. If the `JobQueue` reports overload at this point, its JSON state is dumped alongside the warning. +- **≥ 90 s**: severity upgrades to `fatal`. The job queue state is always dumped at this level. +- **≥ 600 s**: `LogicError("Fatal server stall detected")` is thrown, causing a controlled crash. This represents complete failure of any self-healing mechanism. + +The `armed_` flag is a deliberate interlock: the stall check does nothing until `activateStallDetector()` has been called. As the VFALCO comment in the header notes, this prevents false positives during the potentially lengthy initialization phase, where heartbeats naturally don't arrive yet. `Application` calls `activateStallDetector()` only after full setup completes (line 1493 of `Application.cpp`). + +`heartbeat()` captures `steady_clock::now()` *before* acquiring `mutex_`; this is intentional. If the mutex were contended, measuring time inside the lock would make the stored timestamp reflect lock-wait latency rather than the actual moment liveness was confirmed. + +## Load Fee Adjustment + +There is a fee-tracking block that runs once after the `while(true)` loop exits — that is, only at shutdown. It checks whether the `JobQueue` is overloaded and calls either `FeeTrack::raiseLocalFee()` or `FeeTrack::lowerLocalFee()` accordingly; if the fee changed, it calls `app_.getOPs().reportFeeChange()`. A `// VFALCO TODO` comment acknowledges this should be driven by a listener/observer pattern rather than inline polling. In its current form, this code serves as a single final fee reconciliation at teardown rather than an ongoing regulatory loop. + +## Construction and Lifecycle + +The constructor is `private`. The only way to create a `LoadManager` is through `make_LoadManager()`, which returns `std::unique_ptr`. This factory pattern enforces exclusive ownership and prevents stack allocation, which would be unsafe for an object that owns a thread. + +The destructor calls `stop()` inside a `try`/`catch`, swallowing any exception. This is a standard defensive pattern for objects that join a thread in their destructor: if `stop()` were to throw (e.g., due to a previously unhandled error), letting it propagate from a destructor would call `std::terminate`. The warning log on catch ensures the exception is not silently lost. + +`start()` asserts `!thread_.joinable()` before spawning the thread, guarding against accidental double-starts. `stop()` checks `thread_.joinable()` before calling `join()`, making it safe to call from both `~LoadManager` and an explicit external shutdown path without risk of joining an already-joined thread. + +## Concurrency Model + +Three shared fields — `lastHeartbeat_`, `armed_`, and `stop_` — are all protected by `mutex_`. The background thread copies `lastHeartbeat_` and `armed_` under the lock, then unlocks before doing the stall arithmetic and logging. This minimizes lock hold time: log calls and duration arithmetic happen outside the critical section, ensuring that verbose stall-log output cannot block the `heartbeat()` callers on the main processing path. \ No newline at end of file diff --git a/src/xrpld/app/main/Main.cpp.ai.json b/src/xrpld/app/main/Main.cpp.ai.json new file mode 100644 index 0000000000..aff0b7e6f6 --- /dev/null +++ b/src/xrpld/app/main/Main.cpp.ai.json @@ -0,0 +1,291 @@ +{ + "args": [ + { + "lineno": 282, + "name": "argc" + }, + { + "lineno": 282, + "name": "argv" + } + ], + "classes": [ + { + "args": [ + "std::string const& patterns = \"\"" + ], + "lineno": 120, + "name": "multi_selector" + } + ], + "code_paths": [ + { + "call_chain": [ + "main", + "run", + "adjustDescriptorLimit" + ], + "entry_point": "main", + "purpose": "Starts the application, parses arguments, validates environment (including file descriptor limits), and launches the main app logic.", + "validation_points": [ + "main: Platform validation via preprocessor macros (#if/#error)", + "run: Calls adjustDescriptorLimit to validate file descriptor limits" + ] + }, + { + "call_chain": [ + "main", + "runUnitTests" + ], + "entry_point": "main", + "purpose": "Runs unit tests if the application is started in test mode (ENABLE_TESTS).", + "validation_points": [ + "main: Platform validation via preprocessor macros (#if/#error)" + ] + } + ], + "data_flows": [ + { + "field": "platform (OS)", + "flow": [ + "Compile-time macros", + "Preprocessor checks in Main.cpp", + "Compilation allowed or fails" + ], + "origin": "Compile-time environment (BOOST_OS_* macros)", + "transformations": [ + "None (direct check)" + ], + "validated_at": "Preprocessor (#if/#error) at top of Main.cpp" + }, + { + "field": "needed (number of file descriptors)", + "flow": [ + "run() determines needed", + "run() calls adjustDescriptorLimit(needed, j)", + "adjustDescriptorLimit checks system limits", + "Returns true/false to run()" + ], + "origin": "run() function (likely determined by config or application requirements)", + "transformations": [ + "Compared to current system limit (getrlimit)", + "May attempt to raise limit (setrlimit)", + "Logs and returns false if insufficient" + ], + "validated_at": "adjustDescriptorLimit" + } + ], + "description": "Main entry point and command-line interface for the xrpld server application. Handles command-line parsing, configuration, server startup, and test execution.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "platform (OS)", + "empty", + "string", + "validation" + ], + "evidence": "preprocessor macros (#if/#error) at global scope (top of file)", + "issue_pattern": "Missing empty string validation for platform (OS)", + "why_false_positive": "preprocessor macros (#if/#error) validates platform (OS) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "needed (number of file descriptors)", + "empty", + "string", + "validation" + ], + "evidence": "adjustDescriptorLimit function at adjustDescriptorLimit", + "issue_pattern": "Missing empty string validation for needed (number of file descriptors)", + "why_false_positive": "adjustDescriptorLimit function validates needed (number of file descriptors) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "needed (number of file descriptors)", + "range", + "bounds", + "validation" + ], + "evidence": "adjustDescriptorLimit function at adjustDescriptorLimit", + "issue_pattern": "Missing range validation for needed (number of file descriptors)", + "why_false_positive": "adjustDescriptorLimit function validates needed (number of file descriptors) range" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/Main.cpp", + "functions": [ + { + "args": [ + "int needed", + "beast::Journal j" + ], + "lineno": 32, + "name": "adjustDescriptorLimit" + }, + { + "args": [ + "po::options_description const& desc" + ], + "lineno": 70, + "name": "printHelp" + }, + { + "args": [ + "Runner& runner", + "multi_selector const& pred" + ], + "lineno": 168, + "name": "anyMissing" + }, + { + "args": [ + "std::string const& pattern", + "std::string const& argument", + "bool quiet", + "bool log", + "bool child", + "bool ipv6", + "std::size_t num_jobs", + "int argc", + "char** argv" + ], + "lineno": 185, + "name": "runUnitTests" + }, + { + "args": [ + "int argc", + "char** argv" + ], + "lineno": 282, + "name": "run" + }, + { + "args": [ + "int argc", + "char** argv" + ], + "lineno": 670, + "name": "main" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 29, + "name": "xrpl" + }, + { + "lineno": 157, + "name": "test" + } + ], + "test_coverage_notes": "Unit test support is present (runUnitTests, ENABLE_TESTS, test/unit_test/multi_runner.h), but the platform validation (preprocessor macros) is not testable at runtime and is only enforced at compile time. adjustDescriptorLimit could be tested via unit tests that mock or manipulate RLIMIT_NOFILE, but no explicit test files are referenced in this file. The main validation logic (platform and file descriptor limits) is not directly covered by runtime tests in this file; coverage would depend on external test harnesses or integration tests that exercise startup conditions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "C++ standard, Boost, preprocessor macros", + "validation_layer": "entry_point (main.cpp, system setup)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "compile-time error (#error)", + "field": "platform (OS)", + "location": "global scope (top of file)", + "validated_by": "preprocessor macros (#if/#error)", + "validates": [ + "Checks that the code is being compiled on a supported platform (Linux, Windows, MacOS)", + "Ensures that only one platform is detected at compile time" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (caller may throw or exit); logs fatal error", + "field": "needed (number of file descriptors)", + "location": "adjustDescriptorLimit", + "validated_by": "adjustDescriptorLimit function", + "validates": [ + "Checks if the system has at least 'needed' file descriptors available", + "Attempts to raise the limit if insufficient", + "Logs and returns false if not enough descriptors are available" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/Main.cpp.ai.md b/src/xrpld/app/main/Main.cpp.ai.md new file mode 100644 index 0000000000..3597eec7ed --- /dev/null +++ b/src/xrpld/app/main/Main.cpp.ai.md @@ -0,0 +1,53 @@ +# `Main.cpp` — Server Entry Point and CLI Orchestration + +`Main.cpp` is the outermost shell of the `xrpld` daemon. It owns the binary's `main()` function, translates raw command-line arguments into typed configuration, and dispatches to exactly one of three mutually exclusive execution paths: the full server, an RPC client call to a running server, or the unit test suite. Every other component in `app/main/` — `ApplicationImp`, `BasicApp`, `LoadManager`, `NodeIdentity`, and friends — exists downstream of the decisions made here. + +## The `main()` Function and Platform Setup + +`main()` is deliberately thin. On Windows it primes timezone initialization via an `_ftime` call before spawning any coroutines, working around a Boost bug (ticket #10657) where `GetTimeZoneInformation` misbehaves when first called from a coroutine context. It registers `google::protobuf::ShutdownProtobufLibrary` via `atexit` to ensure clean protobuf teardown on exit, then immediately delegates to `xrpl::run()`. This separation quarantines platform-specific setup and makes the real logic independently callable. + +Before the first line of runtime code, compile-time `#error` directives enforce that exactly one of Linux, Windows, or macOS is detected by the Boost.OS macros. A second check ensures no two platform macros are simultaneously active. These are zero-cost build-time assertions that catch misconfigured toolchains or future platform additions that haven't been audited for correctness. + +## The `run()` Function: Decision Tree and Configuration Assembly + +`run()` starts by naming its thread `"xrpld-main"` via `beast::setCurrentThreadName`, which makes the thread identifiable in debuggers and log output. It then builds a `boost::program_options` descriptor in three visible groups (`gen`, `data`, `rpc`) plus a hidden group. The hidden group contains `--unittest-child` and the deprecated `--fg` flag — options that must be accepted without producing parse errors but should never appear in help text. Positional arguments are mapped to `--parameters`, which activates the RPC client path. + +After parsing, `run()` follows a strict linear decision tree: + +1. **`--help` / `--version`** — Print to stdout/stderr and exit before touching the filesystem or constructing any objects. +2. **`--unittest`** (when `ENABLE_TESTS` is defined) — Delegate to `runUnitTests()` and exit with its result code. +3. **Full `Config` construction** — `config->setup()` reads the config file, resolves paths, and applies `--quiet`, `--silent`, and `--standalone` flags. +4. **`--vacuum`** — Opens the transaction database and runs `doVacuumDB`, then exits. +5. **Startup mode resolution** — Sets `config->START_UP` to the appropriate `StartUpType` enum value based on combinations of `--ledger`, `--replay`, `--load`, `--net`, `--start`, and `--ledgerfile`. +6. **Positional parameters present** — Acts as an RPC client via `RPCCall::fromCommandLine`. +7. **No parameters** — Constructs and runs the full server. + +The ordering is load-bearing: the unit test path and RPC paths both exit before the two-phase file descriptor adjustment and full `Application` construction, keeping those lightweight paths lean. + +Rather than plumbing individual flags into `ApplicationImp` as separate arguments, `run()` builds a `Config` object and decorates it with all CLI overrides. Mutually exclusive combinations are caught inline: `--net` with `--load` or `--replay` is rejected with a human-readable message before anything expensive starts. The `--trap_tx_hash` / `--replay` dependency is similarly enforced — trap logic is meaningless outside of ledger replay, so using `--trap_tx_hash` alone is an immediate error. The trap hash itself is validated with `uint256::parseHex` before being stored, catching malformed hex strings at the earliest possible point. + +The `--force_ledger_present_range` option is a testing escape hatch that overrides the node's advertised present-ledger range. It parses a `"min,max"` string, validates `min <= max`, and stores it as `config->FORCED_LEDGER_RANGE_PRESENT`. Keeping this validation inline rather than inside `Config::setup()` intentionally separates the testing concern from the production configuration path. + +## File Descriptor Management: `adjustDescriptorLimit()` + +`adjustDescriptorLimit()` uses POSIX `getrlimit`/`setrlimit` to ensure the process holds at least as many open file descriptors as requested. It attempts to raise the soft limit if the current soft limit is below the requested count, intentionally ignoring `rlim_max` since processes can often be configured to exceed it. If the system still can't satisfy the request, the function logs a fatal message and returns `false`. + +The function is called **twice** in the server path: once with a floor of 1024 before `Application` is constructed, and again after `app->setup()` returns `app->fdRequired()`. The two-phase approach solves a chicken-and-egg problem — you need *some* descriptors to open config files and database connections, but you don't know the true requirement until after setup has parsed peer counts and database configurations. If either call fails, `run()` returns `-1` immediately, because a descriptor-starved server will fail unpredictably later rather than cleanly. + +## Unit Test Infrastructure + +When built with `ENABLE_TESTS`, the file includes a local `multi_selector` class that wraps multiple `beast::unit_test::selector` instances, enabling comma-separated filter patterns like `--unittest "ripple.ledger,ripple.app"`. The OR semantics (any matching selector admits a suite) are intentional: AND semantics would make it impossible to run disjoint suites in one invocation. + +`runUnitTests()` supports three execution modes controlled by the `child` flag and `num_jobs` count: + +- **Single-job, non-child**: Creates `multi_runner_parent` and `multi_runner_child` in the same process. This is the normal developer workflow. +- **Multi-job parent**: Spawns `num_jobs` child processes via `boost::process::v1::child`, each receiving the original `argv` plus `--unittest-child`. The parent collects exit codes; signal-terminated children (caught by `catch(...)` around `c.wait()`) increment both the bad-exit and terminated-child counters, correctly propagating signal-based failures. +- **Child process**: Runs `multi_runner_child` directly, contributing results to the parent through the IPC mechanism in `multi_runner.h` (built on Boost.Interprocess shared memory and message queues). + +The `anyMissing()` helper prevents a subtle false-success failure mode: if a filter pattern matches no test suites, the runner would report zero failures and exit with `EXIT_SUCCESS`, silently skipping tests the developer expected to run. `anyMissing` compares `runner.suites()` against `pred.size()` and treats each unmatched pattern as an explicit failure. + +The `test::envUseIPv4` atomic (declared `extern` and defined in the test namespace) controls whether loopback addresses in tests resolve to IPv4 or IPv6, toggled via `--unittest-ipv6`. Declaring it `extern` here keeps test-infrastructure headers out of the main compilation unit. + +## Ownership and Lifetime + +`Main.cpp` is the only file in this directory that interacts directly with `boost::program_options`. All downstream components receive already-parsed data through the `Config` object or direct method arguments. The `Application` instance, the `Logs` object, and the `TimeKeeper` are all constructed here and transferred via `std::unique_ptr`, establishing the ownership and lifetime that `ApplicationImp` and the entire server stack depend on. Constructing `Logs` at the CLI-specified severity threshold before passing it in ensures that even earliest startup messages respect the user's verbosity preference rather than defaulting to a hard-coded level inside the application layer. \ No newline at end of file diff --git a/src/xrpld/app/main/NodeIdentity.cpp.ai.json b/src/xrpld/app/main/NodeIdentity.cpp.ai.json new file mode 100644 index 0000000000..86f428c165 --- /dev/null +++ b/src/xrpld/app/main/NodeIdentity.cpp.ai.json @@ -0,0 +1,214 @@ +{ + "args": [ + { + "lineno": 8, + "name": "app" + }, + { + "lineno": 8, + "name": "cmdline" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "getNodeIdentity(app, cmdline)", + "parseGenericSeed (if cmdline contains 'nodeid')", + "parseBase58 (if config contains SECTION_NODE_SEED)", + "generateSecretKey", + "derivePublicKey", + "getWalletDB().checkoutDb() (if no seed)", + "clearNodeIdentity (if cmdline contains 'newnodeid')", + "getNodeIdentity(*db) (fallback)" + ], + "entry_point": "getNodeIdentity(Application& app, boost::program_options::variables_map const& cmdline)", + "purpose": "Determines the node's identity keypair, validating input from command line or config, or falling back to database.", + "validation_points": [ + "parseGenericSeed (validates 'nodeid' from command line)", + "parseBase58 (validates seed from config file)" + ] + } + ], + "data_flows": [ + { + "field": "nodeid (command line argument)", + "flow": [ + "cmdline['nodeid']", + "parseGenericSeed", + "Seed (if valid)", + "generateSecretKey", + "derivePublicKey", + "return {publicKey, secretKey}" + ], + "origin": "cmdline['nodeid'] (command line input)", + "transformations": [ + "String \u2192 Seed (parseGenericSeed)", + "Seed \u2192 SecretKey (generateSecretKey)", + "SecretKey \u2192 PublicKey (derivePublicKey)" + ], + "validated_at": "parseGenericSeed" + }, + { + "field": "SECTION_NODE_SEED (config file section)", + "flow": [ + "config file", + "parseBase58", + "Seed (if valid)", + "generateSecretKey", + "derivePublicKey", + "return {publicKey, secretKey}" + ], + "origin": "app.config().section(SECTION_NODE_SEED).lines().front()", + "transformations": [ + "Base58 string \u2192 Seed (parseBase58)", + "Seed \u2192 SecretKey (generateSecretKey)", + "SecretKey \u2192 PublicKey (derivePublicKey)" + ], + "validated_at": "parseBase58" + }, + { + "field": "newnodeid (command line argument)", + "flow": [ + "cmdline['newnodeid']", + "clearNodeIdentity(*db)", + "getNodeIdentity(*db)" + ], + "origin": "cmdline['newnodeid'] (command line input)", + "transformations": [ + "Triggers clearing of node identity in DB" + ], + "validated_at": "N/A (no direct validation, just triggers DB clear)" + } + ], + "description": "Provides functionality to retrieve or generate the node identity (public and secret key pair) for an XRPL node, using command line arguments, configuration file, or wallet database.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "nodeid (command line argument)", + "empty", + "string", + "validation" + ], + "evidence": "parseGenericSeed at getNodeIdentity", + "issue_pattern": "Missing empty string validation for nodeid (command line argument)", + "why_false_positive": "parseGenericSeed validates nodeid (command line argument) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "nodeid (command line argument)", + "format", + "validation", + "invalid" + ], + "evidence": "parseGenericSeed at getNodeIdentity", + "issue_pattern": "Missing format validation for nodeid (command line argument)", + "why_false_positive": "parseGenericSeed validates nodeid (command line argument) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "SECTION_NODE_SEED (config file section)", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at getNodeIdentity", + "issue_pattern": "Missing empty string validation for SECTION_NODE_SEED (config file section)", + "why_false_positive": "parseBase58 validates SECTION_NODE_SEED (config file section) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "SECTION_NODE_SEED (config file section)", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at getNodeIdentity", + "issue_pattern": "Missing format validation for SECTION_NODE_SEED (config file section)", + "why_false_positive": "parseBase58 validates SECTION_NODE_SEED (config file section) format" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/NodeIdentity.cpp", + "functions": [ + { + "args": [ + "app", + "cmdline" + ], + "lineno": 8, + "name": "getNodeIdentity" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage is likely present in integration/system tests that start the node with various command line arguments and config files (e.g., tests for node identity, config parsing, and error handling). Unit tests may exist for parseGenericSeed and parseBase58 in their respective modules. However, direct unit tests for getNodeIdentity's error handling (invalid nodeid, invalid config seed, fallback to DB) may be missing or only covered indirectly. Edge cases (e.g., both nodeid and config missing, malformed seeds) should be explicitly tested. Look for test files in test/app/main/ or test/core/ for coverage; gaps may exist if only happy paths are tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (parseGenericSeed, parseBase58)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Invalid 'nodeid' in command line\")", + "field": "nodeid (command line argument)", + "location": "getNodeIdentity", + "validated_by": "parseGenericSeed", + "validates": [ + "Checks if the 'nodeid' string can be parsed as a valid generic seed", + "Ensures the seed is not null/invalid" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Invalid [node_seed] in configuration file\")", + "field": "SECTION_NODE_SEED (config file section)", + "location": "getNodeIdentity", + "validated_by": "parseBase58", + "validates": [ + "Checks if the first line of the [node_seed] config section is a valid base58-encoded seed", + "Ensures the seed is not null/invalid" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/NodeIdentity.cpp.ai.md b/src/xrpld/app/main/NodeIdentity.cpp.ai.md new file mode 100644 index 0000000000..08f4a36885 --- /dev/null +++ b/src/xrpld/app/main/NodeIdentity.cpp.ai.md @@ -0,0 +1,40 @@ +# NodeIdentity.cpp + +`NodeIdentity.cpp` solves a single focused problem: given the running `Application` and its command-line arguments, determine the cryptographic keypair that uniquely identifies this server instance on the XRPL peer network. Every validator and relay node must have a stable identity so that peers can authenticate connections and recognize the node across restarts. This file provides the single function `getNodeIdentity()` that resolves that identity with a clearly-ordered priority chain. + +## Resolution Priority + +The function implements a three-tier fallback strategy, evaluated in order: + +**1. `--nodeid` command-line flag** — The highest-priority override. If the operator passes `--nodeid ` on the command line, `parseGenericSeed()` attempts to decode it as a seed in any recognized format (base58, hex, etc.). A parse failure is fatal: `Throw` with a clear message aborts startup immediately. This path is useful for ephemeral deployments, automated testing environments, or scripted node launches where key material is injected at runtime rather than stored on disk. + +**2. `[node_seed]` config file section** — If no command-line override is present but the configuration file contains a `[node_seed]` section, `parseBase58()` decodes the first line. This path yields deterministic identity: the same seed always produces the same keypair, so an operator who recorded their seed can reconstruct their node's identity after a database wipe. A malformed seed also throws `std::runtime_error`, preventing startup with a bad configuration. + +**3. Wallet database (`WalletDB`)** — If neither override is provided, the function checks out a SOCI database session from the application's wallet database. Before querying, it checks for the `--newnodeid` flag: if present, `clearNodeIdentity(*db)` executes `DELETE FROM NodeIdentity`, intentionally erasing any previously stored identity. Then `getNodeIdentity(*db)` (defined in `src/libxrpl/server/Wallet.cpp`) handles the rest. That function first tries to load and validate an existing row from the `NodeIdentity` table — it confirms the public key genuinely derives from the stored private key before trusting it. If no valid row exists (because the table is empty or the stored keys are inconsistent), it calls `randomKeyPair()` to generate a fresh secp256k1 keypair and inserts it, making the new identity persistent for all future restarts. + +## Key Derivation + +When a seed is resolved from the command line or config, the derivation is deterministic and always uses `KeyType::secp256k1`: + +``` +Seed → generateSecretKey(secp256k1, seed) → SecretKey +SecretKey → derivePublicKey(secp256k1, secretKey) → PublicKey +``` + +The explicit choice of secp256k1 (as opposed to ed25519, which XRPL also supports) is a fixed requirement for node identity. This is distinct from *validator* identity, which uses separate key material and may use different curves. + +## Integration Point + +The result is consumed immediately in `Application.cpp` (line 1268) during the application startup sequence: + +```cpp +nodeIdentity_ = getNodeIdentity(*this, cmdline); +``` + +After this call, `nodeIdentity_` (a `std::pair`) is available to the rest of the application — used to sign peer protocol messages and advertise the node's identity during connection handshakes. + +## Design Observations + +The separation between the high-level `getNodeIdentity(Application&, variables_map&)` in this file and the low-level `getNodeIdentity(soci::session&)` in `Wallet.cpp` is deliberate. The database-level function has no knowledge of configuration or command-line context; it deals only with persistence mechanics. This file owns the policy: which source wins, how errors are reported, and when the database should be reset. The layering keeps the wallet utilities reusable without coupling them to the application bootstrap path. + +The `--newnodeid` flag enables deliberate key rotation without editing the config file or manually touching the database. Because the database-level function auto-generates and persists a new keypair when it finds an empty table, the sequence `clear → generate → persist` happens atomically within the same checked-out session, which prevents a partially-initialized state from surviving a crash between steps. \ No newline at end of file diff --git a/src/xrpld/app/main/NodeIdentity.h.ai.json b/src/xrpld/app/main/NodeIdentity.h.ai.json new file mode 100644 index 0000000000..e52dec70f7 --- /dev/null +++ b/src/xrpld/app/main/NodeIdentity.h.ai.json @@ -0,0 +1,32 @@ +{ + "args": [ + { + "lineno": 14, + "name": "app" + }, + { + "lineno": 14, + "name": "cmdline" + } + ], + "classes": [], + "description": "Declares the getNodeIdentity function, which retrieves the cryptographic credentials (public and secret keys) identifying the server instance, based on the application object and command line parameters.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/NodeIdentity.h", + "functions": [ + { + "args": [ + "app", + "cmdline" + ], + "lineno": 14, + "name": "getNodeIdentity" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/NodeIdentity.h.ai.md b/src/xrpld/app/main/NodeIdentity.h.ai.md new file mode 100644 index 0000000000..1ac8421eaf --- /dev/null +++ b/src/xrpld/app/main/NodeIdentity.h.ai.md @@ -0,0 +1,24 @@ +# `NodeIdentity.h` — Node Cryptographic Identity Interface + +This header declares the single entry point for resolving a rippled server's cryptographic identity: the secp256k1 key pair that uniquely identifies the node on the XRP Ledger peer-to-peer network. Every node, whether validator or relay, needs a stable public/private key pair so that peers can authenticate connections and recognize the same node across restarts. + +## The `getNodeIdentity` Function + +```cpp +std::pair +getNodeIdentity(Application& app, boost::program_options::variables_map const& cmdline); +``` + +The function takes both the running `Application` — giving access to the configuration file and wallet database — and the parsed command-line arguments. It returns a `std::pair` representing the resolved identity. `Application::nodeIdentity_` is populated from this return value during startup in `Application.cpp`. + +The implementation in `NodeIdentity.cpp` enforces a clear three-tier priority chain: + +1. **`--nodeid` command-line flag**: The seed is parsed via `parseGenericSeed()`. This is the highest priority and useful for scripted or ephemeral deployments where the identity should not be persisted. +2. **`[node_seed]` config file section** (`SECTION_NODE_SEED`): The value is parsed as a Base58-encoded `Seed`. This supports deterministic static identity via config, common for validators that want a fixed, operator-controlled key. +3. **Persistent wallet database**: If neither override is present, the function falls back to `getNodeIdentity(*db)` from ``, which reads or auto-generates a stable secp256k1 keypair stored in the local SQLite wallet database. + +The `--newnodeid` command-line flag triggers `clearNodeIdentity(*db)` before the database lookup, forcing generation of a fresh keypair — the mechanism for intentional key rotation. + +## Design Rationale + +The header keeps the interface minimal and the concerns separated. The priority logic sits entirely in `NodeIdentity.cpp`, while the raw database operations (`getNodeIdentity(soci::session&)`, `clearNodeIdentity(soci::session&)`) are delegated to ``. This means `NodeIdentity.h` is the application-layer facade: it owns the *selection policy* for which identity source takes precedence, not the mechanics of storage or generation. Invalid seeds at either the command-line or config level throw `std::runtime_error` immediately, failing fast at startup rather than propagating a broken identity into the running node. \ No newline at end of file diff --git a/src/xrpld/app/main/NodeStoreScheduler.cpp.ai.json b/src/xrpld/app/main/NodeStoreScheduler.cpp.ai.json new file mode 100644 index 0000000000..62b520c6c6 --- /dev/null +++ b/src/xrpld/app/main/NodeStoreScheduler.cpp.ai.json @@ -0,0 +1,260 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "JobQueue& jobQueue" + ], + "lineno": 5, + "name": "NodeStoreScheduler" + } + ], + "code_paths": [ + { + "call_chain": [ + "NodeStoreScheduler::scheduleTask", + "jobQueue_.isStopped()", + "jobQueue_.addJob()", + "task.performScheduledTask()" + ], + "entry_point": "NodeStoreScheduler::scheduleTask", + "purpose": "Schedules a NodeStore task for execution, either via the job queue or synchronously if the queue is stopped or rejects the job.", + "validation_points": [ + "jobQueue_.isStopped() (early exit if stopped)", + "if (!jobQueue_.addJob(...)) (fallback to synchronous execution if job not added)" + ] + }, + { + "call_chain": [ + "NodeStoreScheduler::onFetch", + "jobQueue_.isStopped()", + "jobQueue_.addLoadEvents()" + ], + "entry_point": "NodeStoreScheduler::onFetch", + "purpose": "Records a fetch event in the job queue, unless the queue is stopped.", + "validation_points": [ + "jobQueue_.isStopped() (early exit if stopped)" + ] + }, + { + "call_chain": [ + "NodeStoreScheduler::onBatchWrite", + "jobQueue_.isStopped()", + "jobQueue_.addLoadEvents()" + ], + "entry_point": "NodeStoreScheduler::onBatchWrite", + "purpose": "Records a batch write event in the job queue, unless the queue is stopped.", + "validation_points": [ + "jobQueue_.isStopped() (early exit if stopped)" + ] + } + ], + "data_flows": [ + { + "field": "jobQueue_", + "flow": [ + "NodeStoreScheduler::NodeStoreScheduler (constructor)", + "NodeStoreScheduler::scheduleTask / onFetch / onBatchWrite", + "jobQueue_.isStopped()", + "jobQueue_.addJob() / jobQueue_.addLoadEvents()" + ], + "origin": "Injected via NodeStoreScheduler constructor (JobQueue& jobQueue)", + "transformations": [ + "Checked for stopped state", + "Used to schedule jobs or record load events" + ], + "validated_at": "jobQueue_.isStopped() in all public methods" + }, + { + "field": "task (NodeStore::Task&)", + "flow": [ + "scheduleTask argument", + "Lambda passed to jobQueue_.addJob", + "task.performScheduledTask()" + ], + "origin": "Passed as argument to scheduleTask", + "transformations": [ + "Wrapped in lambda for deferred execution", + "Executed synchronously if jobQueue_.addJob fails" + ], + "validated_at": "Indirectly validated by jobQueue_.isStopped() and jobQueue_.addJob()" + }, + { + "field": "report (NodeStore::FetchReport or BatchWriteReport)", + "flow": [ + "onFetch/onBatchWrite argument", + "jobQueue_.addLoadEvents()" + ], + "origin": "Passed as argument to onFetch/onBatchWrite", + "transformations": [ + "FetchType checked to select job type (jtNS_ASYNC_READ or jtNS_SYNC_READ)", + "Elapsed time and count extracted" + ], + "validated_at": "jobQueue_.isStopped() in onFetch/onBatchWrite" + } + ], + "description": "Implements the NodeStoreScheduler class, which schedules and manages NodeStore-related tasks and load events using a JobQueue.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "jobQueue_ (JobQueue state)", + "empty", + "string", + "validation" + ], + "evidence": "jobQueue_.isStopped() at NodeStoreScheduler::scheduleTask", + "issue_pattern": "Missing empty string validation for jobQueue_ (JobQueue state)", + "why_false_positive": "jobQueue_.isStopped() validates jobQueue_ (JobQueue state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "jobQueue_ (JobQueue state)", + "empty", + "string", + "validation" + ], + "evidence": "jobQueue_.isStopped() at NodeStoreScheduler::onFetch", + "issue_pattern": "Missing empty string validation for jobQueue_ (JobQueue state)", + "why_false_positive": "jobQueue_.isStopped() validates jobQueue_ (JobQueue state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "jobQueue_ (JobQueue state)", + "empty", + "string", + "validation" + ], + "evidence": "jobQueue_.isStopped() at NodeStoreScheduler::onBatchWrite", + "issue_pattern": "Missing empty string validation for jobQueue_ (JobQueue state)", + "why_false_positive": "jobQueue_.isStopped() validates jobQueue_ (JobQueue state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "jobQueue_.addJob return value", + "empty", + "string", + "validation" + ], + "evidence": "if (!jobQueue_.addJob(...)) at NodeStoreScheduler::scheduleTask", + "issue_pattern": "Missing empty string validation for jobQueue_.addJob return value", + "why_false_positive": "if (!jobQueue_.addJob(...)) validates jobQueue_.addJob return value for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/NodeStoreScheduler.cpp", + "functions": [ + { + "args": [ + "JobQueue& jobQueue" + ], + "lineno": 5, + "name": "NodeStoreScheduler" + }, + { + "args": [ + "NodeStore::Task& task" + ], + "lineno": 9, + "name": "scheduleTask" + }, + { + "args": [ + "NodeStore::FetchReport const& report" + ], + "lineno": 22, + "name": "onFetch" + }, + { + "args": [ + "NodeStore::BatchWriteReport const& report" + ], + "lineno": 32, + "name": "onBatchWrite" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "Direct unit tests for NodeStoreScheduler are not shown in this file. Likely test files would be in the test/app/main or test/nodestore directories, possibly named NodeStoreScheduler_test.cpp or similar. Tests should cover: (1) scheduleTask with jobQueue stopped and not stopped, (2) addJob failing, (3) onFetch/onBatchWrite with jobQueue stopped and not stopped. Gaps: No evidence of tests for synchronous fallback path (when addJob fails), or for correct event recording in addLoadEvents. Integration tests may cover these paths indirectly.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (custom logic, no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (early return)", + "field": "jobQueue_ (JobQueue state)", + "location": "NodeStoreScheduler::scheduleTask", + "validated_by": "jobQueue_.isStopped()", + "validates": [ + "Checks if the JobQueue is stopped before scheduling a task" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return)", + "field": "jobQueue_ (JobQueue state)", + "location": "NodeStoreScheduler::onFetch", + "validated_by": "jobQueue_.isStopped()", + "validates": [ + "Checks if the JobQueue is stopped before adding load events for fetch" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return)", + "field": "jobQueue_ (JobQueue state)", + "location": "NodeStoreScheduler::onBatchWrite", + "validated_by": "jobQueue_.isStopped()", + "validates": [ + "Checks if the JobQueue is stopped before adding load events for batch write" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (fallback to synchronous execution)", + "field": "jobQueue_.addJob return value", + "location": "NodeStoreScheduler::scheduleTask", + "validated_by": "if (!jobQueue_.addJob(...))", + "validates": [ + "Checks if the job was successfully added to the queue; if not, runs the task synchronously" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/NodeStoreScheduler.cpp.ai.md b/src/xrpld/app/main/NodeStoreScheduler.cpp.ai.md new file mode 100644 index 0000000000..593ee702d9 --- /dev/null +++ b/src/xrpld/app/main/NodeStoreScheduler.cpp.ai.md @@ -0,0 +1,32 @@ +# NodeStoreScheduler.cpp + +`NodeStoreScheduler` is a narrow adapter that bridges the `NodeStore::Scheduler` interface — defined in the storage layer — with the application-level `JobQueue`. It exists because the NodeStore backend is designed to be scheduler-agnostic: it knows nothing about how tasks get dispatched or how performance telemetry gets recorded. `NodeStoreScheduler` supplies those answers without coupling the storage layer to application internals. + +## Architecture Role + +The class inherits from `NodeStore::Scheduler`, a pure-virtual interface in `include/xrpl/nodestore/Scheduler.h` that declares three hooks: `scheduleTask()`, `onFetch()`, and `onBatchWrite()`. The first schedules actual deferred write work; the latter two are observability callbacks invoked after I/O completes. `NodeStoreScheduler` is the only concrete implementation in rippled, wiring both concerns into the application's central `JobQueue`. + +## Task Scheduling: Correctness Under Shutdown + +`scheduleTask()` posts the incoming `NodeStore::Task` reference as a `jtWRITE` job — a job type with defined concurrency limits and latency thresholds (1750ms warning, 2500ms critical per `JobTypes.h`). The method first checks `jobQueue_.isStopped()` for a fast exit when the system is clearly shutting down. + +The subtler case is the fallback path: + +```cpp +if (!jobQueue_.addJob(jtWRITE, "NObjStore", [&task]() { task.performScheduledTask(); })) +{ + task.performScheduledTask(); +} +``` + +There is an unavoidable time-of-check-time-of-use gap between the `isStopped()` guard and the `addJob()` call. During a concurrent shutdown, the queue can refuse the job even though `isStopped()` returned false a microsecond earlier. The fallback makes `scheduleTask()` unconditionally safe: the task is never silently dropped, it just runs synchronously on the caller's thread instead of on a job-queue worker. This is a deliberate correctness tradeoff — doing the write synchronously is preferable to losing it entirely during a graceful shutdown. + +## Load Telemetry: onFetch and onBatchWrite + +`onFetch()` and `onBatchWrite()` have no fallback because they record metrics rather than perform mandatory work. Both call `jobQueue_.addLoadEvents()` with a dedicated job type and timing data. `onFetch()` distinguishes between `jtNS_SYNC_READ` and `jtNS_ASYNC_READ` based on the `FetchType` in the `FetchReport`. `onBatchWrite()` submits `jtNS_WRITE` along with the write count and elapsed duration from the `BatchWriteReport`. + +Looking at `JobTypes.h`, these three measurement types (`jtNS_SYNC_READ`, `jtNS_ASYNC_READ`, `jtNS_WRITE`) are registered with zero concurrency limits — they are not executable job slots but purely measurement labels used to populate the job queue's load-monitoring subsystem. This is how the server produces the NodeStore I/O statistics visible in administrative RPCs. Both callbacks silently return if the queue is stopped; there is no point recording metrics that will never be read. + +## Design Observations + +Holding `jobQueue_` as a reference rather than a pointer makes the dependency non-optional and avoids a null check on every call. The class carries no state beyond this single reference and no ownership of any resources, keeping its lifetime semantics trivial. Because the `NodeStore::Scheduler` interface is virtual, the NodeStore layer can be tested in isolation by substituting a mock scheduler without touching any `JobQueue` machinery. \ No newline at end of file diff --git a/src/xrpld/app/main/NodeStoreScheduler.h.ai.json b/src/xrpld/app/main/NodeStoreScheduler.h.ai.json new file mode 100644 index 0000000000..ee4275bfd0 --- /dev/null +++ b/src/xrpld/app/main/NodeStoreScheduler.h.ai.json @@ -0,0 +1,68 @@ +{ + "args": [ + { + "lineno": 13, + "name": "jobQueue" + }, + { + "lineno": 15, + "name": "task" + }, + { + "lineno": 18, + "name": "report" + }, + { + "lineno": 20, + "name": "report" + } + ], + "classes": [ + { + "args": [ + "JobQueue& jobQueue" + ], + "lineno": 11, + "name": "NodeStoreScheduler" + } + ], + "description": "Defines the NodeStoreScheduler class, which implements a NodeStore::Scheduler using a JobQueue to schedule and manage node store tasks.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/NodeStoreScheduler.h", + "functions": [ + { + "args": [ + "JobQueue& jobQueue" + ], + "lineno": 13, + "name": "NodeStoreScheduler" + }, + { + "args": [ + "NodeStore::Task& task" + ], + "lineno": 15, + "name": "scheduleTask" + }, + { + "args": [ + "NodeStore::FetchReport const& report" + ], + "lineno": 18, + "name": "onFetch" + }, + { + "args": [ + "NodeStore::BatchWriteReport const& report" + ], + "lineno": 20, + "name": "onBatchWrite" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/NodeStoreScheduler.h.ai.md b/src/xrpld/app/main/NodeStoreScheduler.h.ai.md new file mode 100644 index 0000000000..0c5982fb37 --- /dev/null +++ b/src/xrpld/app/main/NodeStoreScheduler.h.ai.md @@ -0,0 +1,31 @@ +# NodeStoreScheduler.h + +`NodeStoreScheduler` is a narrow adapter that connects the `NodeStore::Scheduler` interface to the application's central `JobQueue`. Its entire purpose is to let NodeStore backends remain ignorant of the application's threading model while still benefiting from it. + +## Role in the System + +The `NodeStore::Scheduler` abstract interface (defined in `include/xrpl/nodestore/Scheduler.h`) exists so that NodeStore backends can dispatch asynchronous work and report I/O timing metrics without depending on any particular execution environment. `NodeStoreScheduler` is the concrete implementation of that interface for the production `rippled` application: it routes scheduled tasks through the `JobQueue` worker pool and feeds timing data back into `JobQueue`'s load-tracking system. + +## Task Scheduling + +`scheduleTask()` is the workhorse method. It posts the given `NodeStore::Task` as a `jtWRITE` job named `"NObjStore"` on the application `JobQueue`. The lambda captures `task` by reference — safe because `NodeStore::BatchWriter` (the primary `Task` implementor) waits for completion before destroying itself. + +The shutdown handling in `scheduleTask()` deserves attention. There are two distinct failure points, and each is handled differently: + +1. If `jobQueue_.isStopped()` is already true, the method returns immediately without executing the task at all. At this point the process is unwinding and persistent state is not a concern. + +2. If `addJob()` returns `false` (which can happen during the brief window when the queue is draining but not yet fully stopped), the task is executed **synchronously on the caller's thread**. This fallback is critical: a dropped write task could leave the node store's batch in a partially-flushed state, so the code ensures the work always completes, even if it blocks the calling thread. + +This two-tier approach reflects a real operational concern: batch writes that are silently discarded could corrupt the node object database. + +## Performance Reporting + +`onFetch()` and `onBatchWrite()` exist purely for telemetry. When a NodeStore backend completes a read or a batch write, it calls these methods with a report containing elapsed time (and, for reads, whether the fetch was synchronous or asynchronous, and whether the object was found). `NodeStoreScheduler` maps these directly to `JobQueue::addLoadEvents()`, using the job types `jtNS_ASYNC_READ`, `jtNS_SYNC_READ`, and `jtNS_WRITE` respectively. This wires NodeStore I/O metrics into `JobQueue`'s broader load-monitoring infrastructure, where they can influence scheduling decisions and be surfaced in admin diagnostics. + +Both reporting methods guard with `isStopped()` and return early if the queue has shut down. Unlike `scheduleTask()`, there is no synchronous fallback — telemetry data during shutdown is not meaningful, and there is no state correctness at risk. + +## Design Notes + +The class holds `jobQueue_` as a reference, not a pointer or shared ownership, encoding a strict lifetime dependency: the `JobQueue` must outlive any `NodeStoreScheduler` that references it. In practice this is guaranteed by the application's object construction and destruction order in `ApplicationImp`. + +The separation between task scheduling and performance reporting in the `Scheduler` interface is deliberate: it allows a mock or test scheduler to implement lightweight no-op reporting while still correctly driving task dispatch, which is useful when testing NodeStore backends in isolation. \ No newline at end of file diff --git a/src/xrpld/app/main/Tuning.h.ai.json b/src/xrpld/app/main/Tuning.h.ai.json new file mode 100644 index 0000000000..ef26b801a6 --- /dev/null +++ b/src/xrpld/app/main/Tuning.h.ai.json @@ -0,0 +1,14 @@ +{ + "args": [], + "classes": [], + "description": "Defines constants related to XRPL, such as cache sizes and expiration times, within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/main/Tuning.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/main/Tuning.h.ai.md b/src/xrpld/app/main/Tuning.h.ai.md new file mode 100644 index 0000000000..1f0d2da318 --- /dev/null +++ b/src/xrpld/app/main/Tuning.h.ai.md @@ -0,0 +1,23 @@ +# `Tuning.h` — System-Wide Performance Constants + +`Tuning.h` is a deliberate single-location home for hard-coded performance constants that affect multiple subsystems in the XRPL node. Rather than scattering magic numbers across `NodeFamily.cpp`, `NetworkOPs.cpp`, and whatever else grows to depend on them, this header provides named, typed constants in the `xrpl` namespace so their intent is explicit and their values are easy to review or adjust in one place. + +## `fullBelowTargetSize` and `fullBelowExpiration` + +These two constants configure the `FullBelowCache` instantiated in `NodeFamily::NodeFamily()`. The "full below" concept is specific to SHAMap synchronization: when a node can confirm that every descendant of a given inner node is already present in its local store, that inner node's hash is inserted into this cache. Subsequent sync passes consult the cache first — if a child hash `touch_if_exists()` hits, the entire subtree is skipped, avoiding redundant traversal of potentially thousands of nodes already known to be complete. + +`fullBelowTargetSize = 524288` sets the capacity target at 512 Ki entries (512 × 1024). This is deliberately large because the cache must cover hashes from the current ledger's state tree and transaction tree simultaneously, across potentially millions of accounts and ledger objects. Undersizing it would cause thrashing: recently confirmed subtrees would evict each other, turning cache hits back into full traversals. + +`fullBelowExpiration = std::chrono::minutes{10}` controls how long a "full below" record is trusted. Because ledger state mutates with every new ledger close (typically every 3–4 seconds), a subtree that was complete ten minutes ago may no longer be complete for the current in-progress sync. Ten minutes is a pragmatic choice: long enough to stay warm across the natural sweep interval that calls `fbCache_->sweep()`, short enough that stale entries from much-older ledger generations do not permanently pollute the cache. + +Notably, the `TreeNodeCache` — the other cache in `NodeFamily` — does not use constants from `Tuning.h`. Its size and age come from `app.config().getValueFor(SizedItem::treeCacheSize/treeCacheAge)`, reflecting operator-tunable values. The full-below cache, by contrast, is an internal SHAMap sync optimization with no operator-facing knob; fixed constants are appropriate there. + +## `maxPoppedTransactions` + +`maxPoppedTransactions = 10` bounds the work done in a single "cascade submit" pass inside `NetworkOPs`. After a transaction is successfully applied to the open ledger, the node checks whether the same account has additional pending transactions queued up with the next sequential sequence numbers or tickets. If so, it eagerly pops them — up to 10 — and submits them as a batch in the same processing cycle. + +The limit exists to prevent a single productive account from monopolizing the processing loop. An account could in theory have hundreds of queued transactions; without this cap, the loop iterating with `popAcctTransaction()` would consume unbounded time while the batch lock remains contested. By limiting each triggered cascade to 10 transactions, the scheduler ensures fairness across accounts and keeps per-iteration latency predictable. + +## Design Rationale + +Centralizing these constants in `app/main/Tuning.h` — even though their consumers live in `shamap/` and `app/misc/` — reflects a deliberate architectural choice: performance-sensitive magic numbers should be discoverable from a single file rather than embedded in constructors or loop conditions. Any engineer tuning node performance under load knows to look here first. The use of `constexpr` (rather than `#define`) ensures they are type-safe, respect namespace scoping, and can be used in `if constexpr` contexts without macro-hazard side effects. \ No newline at end of file diff --git a/src/xrpld/app/misc/AmendmentTableImpl.h.ai.json b/src/xrpld/app/misc/AmendmentTableImpl.h.ai.json new file mode 100644 index 0000000000..f77ee7e22d --- /dev/null +++ b/src/xrpld/app/misc/AmendmentTableImpl.h.ai.json @@ -0,0 +1,52 @@ +{ + "args": [ + { + "lineno": 10, + "name": "registry" + }, + { + "lineno": 11, + "name": "majorityTime" + }, + { + "lineno": 12, + "name": "supported" + }, + { + "lineno": 13, + "name": "enabled" + }, + { + "lineno": 14, + "name": "vetoed" + }, + { + "lineno": 15, + "name": "journal" + } + ], + "classes": [], + "description": "Header file declaring the make_AmendmentTable function for creating an AmendmentTable in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/AmendmentTableImpl.h", + "functions": [ + { + "args": [ + "registry", + "majorityTime", + "supported", + "enabled", + "vetoed", + "journal" + ], + "lineno": 8, + "name": "make_AmendmentTable" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/AmendmentTableImpl.h.ai.md b/src/xrpld/app/misc/AmendmentTableImpl.h.ai.md new file mode 100644 index 0000000000..8df6333b91 --- /dev/null +++ b/src/xrpld/app/misc/AmendmentTableImpl.h.ai.md @@ -0,0 +1,29 @@ +# `AmendmentTableImpl.h` + +This header exists as the factory boundary between the abstract `AmendmentTable` interface (defined in `xrpl/ledger/AmendmentTable.h`) and its concrete implementation. It declares a single factory function, `make_AmendmentTable`, that returns a fully-constructed implementation hidden behind a `std::unique_ptr`. Callers never need to know the implementation type, and the translation unit containing the implementation can be compiled independently. + +## Why a Separate Factory Header? + +The pattern mirrors several other components in this directory: `FeeVote.h` exposes `make_FeeVote`, and `NegativeUNLVote` follows the same convention. The rationale is architectural layering: the `AmendmentTable` interface lives in the `xrpl/ledger` layer — a lower-level, application-agnostic library — while the concrete `AmendmentTableImpl` class lives here in the application layer. Keeping them separate prevents the lower-level ledger library from pulling in application-layer concerns (like `ServiceRegistry`), while still allowing the application to instantiate the concrete object at startup. + +Notably, the factory signature is declared in *two* places: once here in `AmendmentTableImpl.h` (the app-layer include), and once again at the bottom of `xrpl/ledger/AmendmentTable.h` itself. The latter declaration means code that only depends on the interface header can still call the factory, enabling the linker to resolve the definition from `detail/AmendmentTable.cpp` without requiring an explicit include of this implementation header. The `Impl` suffix in the filename signals that this header belongs to the implementation side of the split. + +## `make_AmendmentTable` Parameters + +The parameters collectively encode the full runtime policy for amendment governance: + +- **`ServiceRegistry& registry`**: Provides access to application-wide services. In `Application.cpp`, the application itself (`*this`) is passed here, as it implements `ServiceRegistry`. This allows the implementation to register listeners for ledger close events and consensus callbacks without coupling to the full application type. + +- **`std::chrono::seconds majorityTime`**: The continuous duration a supermajority of validators must support an amendment before it is scheduled for activation. In practice this is set from `config().AMENDMENT_MAJORITY_TIME`. Injecting it rather than hardcoding it allows test suites to use an artificially short interval and allows the parameter to be tuned at deployment without recompilation. + +- **`std::vector const& supported`**: The set of amendments this node's compiled software understands, each carrying a name, a 256-bit hash ID, and a `VoteBehavior` indicating the node's default voting stance. Amendments unknown to the node are neither supported nor vetoed — they are simply invisible to the voting logic, which is the safe default for forward compatibility with future amendments. + +- **`Section const& enabled` / `Section const& vetoed`**: Configuration file sections (from `[amendments]` and `[veto_amendments]` respectively) declaring operator overrides. The two-section separation matters: an operator can suppress a node's vote for a supported amendment via `vetoed` without claiming the amendment doesn't exist, and can force-enable a local amendment override via `enabled`. The implementation parses these sections with a hex-ID + name regex to extract `uint256` amendment identifiers. + +- **`beast::Journal journal`**: Standard XRPL structured logging sink, injected to allow the implementation to emit diagnostics under the `"Amendments"` log category. + +## Relationship to Sibling Files + +The concrete class `AmendmentTableImpl` is defined entirely within `detail/AmendmentTable.cpp` — its class definition does not appear in any header. `make_AmendmentTable` at the end of that file simply calls `std::make_unique(...)` and returns it as the base-class pointer. This opaque-implementation approach means changes to the internal `TrustedVotes` anti-flapping mechanism, the `AmendmentState` per-amendment struct, or any other internal detail require recompiling only the implementation translation unit, not the entire application. + +At runtime, the single `AmendmentTable` object constructed here is the central authority for all amendment lifecycle management: it tallies validator votes (via `doVoting`), tracks majority windows (suppressing transient flapping via the `TrustedVotes` class), detects unsupported-but-enabled amendments that put the node at risk of falling out of consensus, and serves the `feature` RPC endpoint through its `getJson` overloads. \ No newline at end of file diff --git a/src/xrpld/app/misc/DeliverMax.h.ai.json b/src/xrpld/app/misc/DeliverMax.h.ai.json new file mode 100644 index 0000000000..645a150676 --- /dev/null +++ b/src/xrpld/app/misc/DeliverMax.h.ai.json @@ -0,0 +1,51 @@ +{ + "args": [ + { + "lineno": 19, + "name": "tx_json" + }, + { + "lineno": 19, + "name": "txnType" + }, + { + "lineno": 19, + "name": "apiVersion" + } + ], + "classes": [ + { + "args": [], + "lineno": 6, + "name": "Value" + } + ], + "description": "Provides a function to copy the 'Amount' field to the 'DeliverMax' field in the JSON output of Payment transactions, with behavior depending on API version.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/DeliverMax.h", + "functions": [ + { + "args": [ + "tx_json", + "txnType", + "apiVersion" + ], + "lineno": 19, + "name": "insertDeliverMax" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "Json" + }, + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/DeliverMax.h.ai.md b/src/xrpld/app/misc/DeliverMax.h.ai.md new file mode 100644 index 0000000000..069ddfd580 --- /dev/null +++ b/src/xrpld/app/misc/DeliverMax.h.ai.md @@ -0,0 +1,25 @@ +# `DeliverMax.h` — RPC Field Alias for Payment Transaction Amount + +This header declares a single utility function that handles a deliberate, versioned rename of the `Amount` field in Payment transaction JSON output. It lives in `xrpl::RPC` because the translation is purely an API-layer concern: the on-ledger binary serialization continues to use `Amount` unchanged, but JSON responses presented to API consumers go through this normalization step. + +## The Problem It Solves + +The `Amount` field name is semantically ambiguous for Payment transactions. In the ledger protocol, `Amount` on a Payment represents the *maximum* the sender is willing to deliver — the actual delivered amount can be lower when partial payments or path-finding are involved. The name `DeliverMax` is a much clearer description of this semantics. Renaming a widely-used field in a live API requires a careful two-phase migration: first add the alias so clients can adapt, then remove the old name once the ecosystem has moved to the newer API version. + +## `insertDeliverMax()` + +```cpp +void insertDeliverMax(Json::Value& tx_json, TxType txnType, unsigned int apiVersion); +``` + +The implementation in `detail/DeliverMax.cpp` is compact and intentional: + +1. **Guard on field presence**: only acts when `Amount` is actually present in the JSON object, avoiding crashes on partial or pre-serialized objects. +2. **Guard on transaction type**: only `ttPAYMENT` is affected. Every other transaction type that carries an `Amount` field (offers, escrows, etc.) passes through completely untouched. +3. **Version-conditional removal**: when `apiVersion > 1`, the `Amount` key is removed after copying, forcing clients on the newer API to exclusively use `DeliverMax`. Clients still on v1 receive both keys, preserving backward compatibility without any special-casing at the call sites. + +## Call Sites + +`insertDeliverMax` is called after every path that serializes a transaction to JSON for RPC output: `Tx.cpp`, `LedgerToJson.cpp`, `TransactionSign.cpp`, `AccountTx.cpp`, `TxHistory.cpp`, `TransactionEntry.cpp`, and `NetworkOPs.cpp`. Because the logic is isolated here rather than duplicated across those handlers, the aliasing behavior is guaranteed to be consistent regardless of which endpoint a client uses to retrieve a transaction. + +The forward declaration of `Json::Value` (rather than a full include of the Json headers) keeps this header lightweight — it only pulls in `TxFormats.h` for the `TxType` enum, so including it in RPC handlers adds minimal compilation overhead. \ No newline at end of file diff --git a/src/xrpld/app/misc/FeeVote.h.ai.json b/src/xrpld/app/misc/FeeVote.h.ai.json new file mode 100644 index 0000000000..cc2e7cd85b --- /dev/null +++ b/src/xrpld/app/misc/FeeVote.h.ai.json @@ -0,0 +1,80 @@ +{ + "args": [ + { + "lineno": 15, + "name": "lastFees" + }, + { + "lineno": 15, + "name": "rules" + }, + { + "lineno": 15, + "name": "val" + }, + { + "lineno": 23, + "name": "lastClosedLedger" + }, + { + "lineno": 23, + "name": "parentValidations" + }, + { + "lineno": 23, + "name": "initialPosition" + }, + { + "lineno": 33, + "name": "setup" + }, + { + "lineno": 33, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "FeeVote" + } + ], + "description": "Defines the FeeVote interface for managing and processing fee voting logic in the XRPL, including methods for validation and voting, and a factory function to create FeeVote instances.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/FeeVote.h", + "functions": [ + { + "args": [ + "Fees const& lastFees", + "Rules const& rules", + "STValidation& val" + ], + "lineno": 15, + "name": "doValidation" + }, + { + "args": [ + "std::shared_ptr const& lastClosedLedger", + "std::vector> const& parentValidations", + "std::shared_ptr const& initialPosition" + ], + "lineno": 23, + "name": "doVoting" + }, + { + "args": [ + "FeeSetup const& setup", + "beast::Journal journal" + ], + "lineno": 33, + "name": "make_FeeVote" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/FeeVote.h.ai.md b/src/xrpld/app/misc/FeeVote.h.ai.md new file mode 100644 index 0000000000..5d76d51fa2 --- /dev/null +++ b/src/xrpld/app/misc/FeeVote.h.ai.md @@ -0,0 +1,27 @@ +# `FeeVote.h` — Validator Fee-Voting Interface + +`FeeVote.h` defines the pure abstract interface through which a validator node participates in the XRPL's on-ledger fee governance mechanism. The XRP Ledger has no central authority to adjust network fees; instead, validators reach consensus on desired fee levels by embedding preferences in their validation messages and, at periodic "flag ledgers," injecting pseudo-transactions that move the live fee schedule closer to the collective preference. + +## The Two-Phase Protocol + +Fee governance works in two distinct phases, each corresponding to one interface method. + +**Phase 1 — `doValidation()`** runs whenever this node produces a validation for a closed ledger. It receives the fee schedule currently in effect (`Fees const& lastFees`), the active rules set (to detect the `featureXRPFees` amendment), and the `STValidation` object being constructed. If any of this node's target fees (`reference_fee`, `account_reserve`, `owner_reserve` from its `FeeSetup` configuration) differ from the current on-ledger values, the method stamps the desired values into the validation's serialized fields. This is gossip: the node broadcasts its preferences to every other trusted validator so that votes can be tallied later. No fee changes occur at this step. + +**Phase 2 — `doVoting()`** runs only at flag ledgers (every 256 ledgers, detected via `isFlagLedger()`). At this point the node has collected a `parentValidations` vector containing recent `STValidation` objects from trusted peers. The implementation tallies each field — base fee, base reserve, and increment reserve — using a `detail::VotableValue` tally object per field, then calls `getVotes()` to determine the winning value. If any winning value differs from the current on-ledger setting, a `ttFEE` pseudo-transaction is serialized and inserted into `initialPosition` (a `SHAMap` representing the candidate next ledger). If other validators agree, they will have injected the same transaction, and it will survive consensus. + +## Design Decisions + +**Interface-only header, factory construction.** The header deliberately exposes only the abstract `FeeVote` class and the `make_FeeVote()` factory. The concrete `FeeVoteImpl` lives entirely in `FeeVoteImpl.cpp`. This is not incidental: callers depend only on the interface, making the voting logic substitutable for tests and keeping build dependencies minimal. The forward-declared `struct FeeSetup` keeps `Config.h` out of this header's transitive include chain. + +**Conservative vote resolution.** `VotableValue::getVotes()` only considers votes that fall *between* the current value and the operator's target. The winning value is whichever receives the most votes within that range. This prevents any single minority of validators from pushing fees past the point where the network's median preference lies. Fees change incrementally over multiple flag ledgers rather than jumping in a single step. + +**Amendment-aware field handling.** The `featureXRPFees` amendment changed the wire representation of fee fields from legacy integers (`sfBaseFee` as `uint64`, `sfReserveBase`/`sfReserveIncrement` as `uint32`) to native XRP drop amounts (`sfBaseFeeDrops`, `sfReserveBaseDrops`, `sfReserveIncrementDrops`). Both `doValidation()` and `doVoting()` branch on `rules.enabled(featureXRPFees)` to read and write the correct field types. For the legacy path, the implementation performs safe narrowing checks (`dropsAs()`) and treats out-of-range or non-native values as abstentions (`noVote()`), rather than throwing, because validation field values originate from external, untrusted network peers. + +**Trusted-only tallying.** In `doVoting()`, the loop over `parentValidations` skips any validation that is not marked trusted via `val->isTrusted()`. This ensures that Sybil validators or unknown nodes cannot artificially skew fee votes. + +## Relationship to `FeeSetup` and `Config` + +`FeeSetup` (defined in `Config.h`) represents the *operator's preference* — the fees they wish the network to eventually converge on. Its defaults are `reference_fee = 10` drops, `account_reserve = 10 XRP`, and `owner_reserve` per item. These are loaded from the node's config file via `setup_FeeVote()` and passed to `make_FeeVote()` at startup. The resulting `FeeVote` object is owned by the application layer and invoked by the consensus machinery at the appropriate moments. + +The interface's two-parameter split — `doValidation` operating on `Fees` + `Rules` rather than a full `ReadView`, and `doVoting` receiving the complete `ReadView` — reflects their different needs: validation stamping only needs the current fee values and amendment rules, while vote tallying needs access to the full ledger context including sequence number and `SHAMap` manipulation. \ No newline at end of file diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp.ai.json b/src/xrpld/app/misc/FeeVoteImpl.cpp.ai.json new file mode 100644 index 0000000000..01f1e5adba --- /dev/null +++ b/src/xrpld/app/misc/FeeVoteImpl.cpp.ai.json @@ -0,0 +1,509 @@ +{ + "args": [ + { + "lineno": 16, + "name": "current" + }, + { + "lineno": 16, + "name": "target" + } + ], + "classes": [ + { + "args": [ + "value_type current", + "value_type target" + ], + "lineno": 11, + "name": "VotableValue" + }, + { + "args": [ + "FeeSetup const& setup", + "beast::Journal journal" + ], + "lineno": 54, + "name": "FeeVoteImpl" + } + ], + "code_paths": [ + { + "call_chain": [ + "FeeVoteImpl::doValidation" + ], + "entry_point": "FeeVoteImpl::doValidation", + "purpose": "Validates and potentially sets fee-related fields in a validation object (STValidation) based on current ledger fees, protocol rules, and target configuration.", + "validation_points": [ + "FeeVoteImpl::doValidation (checks protocol rule featureXRPFees, compares current vs target fees, only sets values if within valid range as per comment and logic)" + ] + }, + { + "call_chain": [ + "FeeVoteImpl::doVoting", + "detail::VotableValue::addVote", + "detail::VotableValue::getVotes" + ], + "entry_point": "FeeVoteImpl::doVoting", + "purpose": "Aggregates votes from parent validations and determines the most popular fee values to propose for the next ledger.", + "validation_points": [ + "detail::VotableValue::getVotes (ensures only values between current and target are considered, picks most voted value)" + ] + }, + { + "call_chain": [ + "FeeVoteImpl::FeeVoteImpl", + "detail::VotableValue::VotableValue (constructor)" + ], + "entry_point": "FeeVoteImpl::FeeVoteImpl", + "purpose": "Constructs the FeeVoteImpl object, initializing the target fee setup and registering the initial vote.", + "validation_points": [ + "detail::VotableValue::VotableValue (validates target_ by adding it as a vote)" + ] + } + ], + "data_flows": [ + { + "field": "target_ (FeeSetup)", + "flow": [ + "FeeVoteImpl::FeeVoteImpl (setup)", + "target_ member", + "used in doValidation and doVoting" + ], + "origin": "FeeVoteImpl constructor argument (setup)", + "transformations": [ + "Stored as member variable", + "Passed to VotableValue as target" + ], + "validated_at": "detail::VotableValue::VotableValue (constructor)" + }, + { + "field": "vote values (current, target)", + "flow": [ + "Passed to VotableValue (current_, target_)", + "Votes added via addVote/noVote", + "Aggregated in voteMap_", + "Evaluated in getVotes" + ], + "origin": "Ledger fees (current), FeeSetup (target)", + "transformations": [ + "Mapped and counted in voteMap_", + "Filtered to range between current and target in getVotes" + ], + "validated_at": "detail::VotableValue::getVotes" + }, + { + "field": "fees (base, reserve, increment)", + "flow": [ + "doValidation receives lastFees and target_", + "Compares lastFees fields to target_ fields", + "If different, sets value in STValidation" + ], + "origin": "Fees struct (lastFees), FeeSetup (target_)", + "transformations": [ + "Type conversion (to32/to64) if featureXRPFees is not enabled", + "Direct assignment if featureXRPFees is enabled" + ], + "validated_at": "FeeVoteImpl::doValidation (range and protocol rule checks)" + }, + { + "field": "featureXRPFees (protocol rule)", + "flow": [ + "doValidation receives Rules", + "Checks rules.enabled(featureXRPFees)", + "Determines which voting logic to use" + ], + "origin": "Rules object", + "transformations": [ + "Boolean check" + ], + "validated_at": "FeeVoteImpl::doValidation" + } + ], + "description": "Implements the fee voting mechanism for the XRPL ledger, allowing validators to vote on transaction fees and reserves, and to propose changes via transactions in the consensus process.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "target_ (FeeSetup)", + "empty", + "string", + "validation" + ], + "evidence": "VotableValue constructor at detail::VotableValue::VotableValue", + "issue_pattern": "Missing empty string validation for target_ (FeeSetup)", + "why_false_positive": "VotableValue constructor validates target_ (FeeSetup) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "vote values (current, target)", + "empty", + "string", + "validation" + ], + "evidence": "detail::VotableValue::getVotes at detail::VotableValue::getVotes", + "issue_pattern": "Missing empty string validation for vote values (current, target)", + "why_false_positive": "detail::VotableValue::getVotes validates vote values (current, target) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "vote values (current, target)", + "range", + "bounds", + "validation" + ], + "evidence": "detail::VotableValue::getVotes at detail::VotableValue::getVotes", + "issue_pattern": "Missing range validation for vote values (current, target)", + "why_false_positive": "detail::VotableValue::getVotes validates vote values (current, target) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "fees (base, reserve, increment)", + "empty", + "string", + "validation" + ], + "evidence": "FeeVoteImpl::doValidation at FeeVoteImpl::doValidation", + "issue_pattern": "Missing empty string validation for fees (base, reserve, increment)", + "why_false_positive": "FeeVoteImpl::doValidation validates fees (base, reserve, increment) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "featureXRPFees (protocol rule)", + "empty", + "string", + "validation" + ], + "evidence": "rules.enabled(featureXRPFees) at FeeVoteImpl::doValidation", + "issue_pattern": "Missing empty string validation for featureXRPFees (protocol rule)", + "why_false_positive": "rules.enabled(featureXRPFees) validates featureXRPFees (protocol rule) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "fee values (base, reserve, increment)", + "empty", + "string", + "validation" + ], + "evidence": "comment in FeeVoteImpl::doValidation at FeeVoteImpl::doValidation", + "issue_pattern": "Missing empty string validation for fee values (base, reserve, increment)", + "why_false_positive": "comment in FeeVoteImpl::doValidation validates fee values (base, reserve, increment) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "fee values (base, reserve, increment)", + "range", + "bounds", + "validation" + ], + "evidence": "comment in FeeVoteImpl::doValidation at FeeVoteImpl::doValidation", + "issue_pattern": "Missing range validation for fee values (base, reserve, increment)", + "why_false_positive": "comment in FeeVoteImpl::doValidation validates fee values (base, reserve, increment) range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/FeeVoteImpl.cpp", + "functions": [ + { + "args": [ + "value_type vote" + ], + "lineno": 22, + "name": "VotableValue::addVote" + }, + { + "args": [], + "lineno": 26, + "name": "VotableValue::noVote" + }, + { + "args": [], + "lineno": 30, + "name": "VotableValue::current" + }, + { + "args": [], + "lineno": 34, + "name": "VotableValue::getVotes" + }, + { + "args": [ + "FeeSetup const& setup", + "beast::Journal journal" + ], + "lineno": 61, + "name": "FeeVoteImpl::FeeVoteImpl" + }, + { + "args": [ + "Fees const& lastFees", + "Rules const& rules", + "STValidation& v" + ], + "lineno": 65, + "name": "FeeVoteImpl::doValidation" + }, + { + "args": [ + "std::shared_ptr const& lastClosedLedger", + "std::vector> const& set", + "std::shared_ptr const& initialPosition" + ], + "lineno": 99, + "name": "FeeVoteImpl::doVoting" + }, + { + "args": [ + "FeeSetup const& setup", + "beast::Journal journal" + ], + "lineno": 202, + "name": "make_FeeVote" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "detail" + } + ], + "test_coverage_notes": "The code is likely tested by integration and unit tests for fee voting and validation logic. Look for test files such as FeeVote_test.cpp, Ledger_test.cpp, or Validation_test.cpp in the test or unit_test directories. However, the code relies on protocol rules and ledger state, so full coverage requires tests that simulate different rule activations (featureXRPFees enabled/disabled), out-of-range fee values, and voting scenarios. Gaps may exist if tests do not cover edge cases like invalid fee ranges, exception handling, or all protocol rule branches.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom logic (no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.7, + "error_thrown": "none (no exception, just sets up vote map)", + "field": "target_ (FeeSetup)", + "location": "detail::VotableValue::VotableValue", + "validated_by": "VotableValue constructor", + "validates": [ + "Ensures the target value is always included in the vote map" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (out-of-range values are ignored)", + "field": "vote values (current, target)", + "location": "detail::VotableValue::getVotes", + "validated_by": "detail::VotableValue::getVotes", + "validates": [ + "Only considers votes between current and target (inclusive)" + ], + "validation_type": "range" + }, + { + "confidence": 0.8, + "error_thrown": "none (does not throw, just skips setting field)", + "field": "fees (base, reserve, increment)", + "location": "FeeVoteImpl::doValidation", + "validated_by": "FeeVoteImpl::doValidation", + "validates": [ + "Checks if current fee != target fee before voting", + "Skips voting if values are equal" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (conditional logic, not exception)", + "field": "featureXRPFees (protocol rule)", + "location": "FeeVoteImpl::doValidation", + "validated_by": "rules.enabled(featureXRPFees)", + "validates": [ + "Only performs voting if featureXRPFees is enabled" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (out-of-range values are ignored, not set)", + "field": "fee values (base, reserve, increment)", + "location": "FeeVoteImpl::doValidation", + "validated_by": "comment in FeeVoteImpl::doValidation", + "validates": [ + "If values are out of valid range, do not send a value" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/FeeVoteImpl.cpp.ai.md b/src/xrpld/app/misc/FeeVoteImpl.cpp.ai.md new file mode 100644 index 0000000000..d5cfcc30ec --- /dev/null +++ b/src/xrpld/app/misc/FeeVoteImpl.cpp.ai.md @@ -0,0 +1,49 @@ +# `FeeVoteImpl.cpp` — Validator Fee Voting Logic + +## Role in the System + +XRPL's base fees and reserve requirements are not fixed in source code — they can be changed through a decentralized on-ledger voting process. `FeeVoteImpl.cpp` is the sole implementation of this mechanism. Each validating node embeds its fee preferences into every validation it broadcasts, and at every 256th ("flag") ledger, those preferences are tallied to produce a pseudo-transaction that may update the network's fee schedule. + +The file defines two collaborating types: `detail::VotableValue` (the vote-counting primitive, private to this translation unit) and `FeeVoteImpl` (the `FeeVote` interface implementation exposed via the `make_FeeVote()` factory). The abstract `FeeVote` interface in `FeeVote.h` forward-declares `FeeSetup` and exposes only `doValidation()` and `doVoting()`, keeping all voting logic behind the interface and out of headers. + +## `VotableValue`: Safe Incremental Change + +The design of `VotableValue` encodes a deliberate philosophy: fee changes should be gradual and consensus-driven, never a large step imposed by a minority. The class holds the ledger's *current* fee value and the node operator's *target* preference. Its vote map accumulates one tally per distinct `XRPAmount` value proposed across all trusted validators. + +The key safety property is enforced in `getVotes()`: + +```cpp +if ((key <= std::max(target_, current_)) && (key >= std::min(target_, current_)) && (val > weight)) +``` + +Only votes that fall strictly within the range `[min(current, target), max(current, target)]` are considered. A validator trying to push a fee far beyond any locally-configured target has its vote silently discarded. This bounding prevents a small group of adversarial or misconfigured validators from swinging fees by an arbitrary amount in one ledger cycle — movement is always capped by the distribution of operator preferences. + +The constructor immediately registers the local node's own vote (`++voteMap_[target_]`), so the local preference is counted even before external validations are processed. + +Validators that don't include a fee field in their validation are handled by `noVote()`, which calls `addVote(current_)`. Abstaining is treated as a vote for the status quo, meaning changes require active positive consensus rather than passive majority. + +## Two-Phase Voting: Validation vs. Consensus + +Fee voting is split across two consensus phases, both routed through `RCLConsensus.cpp`. + +**Phase 1 — `doValidation()`** runs when a validator is about to sign and broadcast a validation message for a just-closed ledger. If the node's target fee differs from the ledger's current fee, it encodes its preferred value in the `STValidation` object. This field acts as the node's public ballot: every peer that receives this validation can observe the preference. + +**Phase 2 — `doVoting()`** runs only at flag ledgers (every 256 ledgers), gated by `XRPL_ASSERT(isFlagLedger(...))`. It receives all trusted parent validations and tallies the votes for each of the three fee parameters (base fee, base reserve, reserve increment) independently. If any of the three tallied outcomes differs from the current ledger value, the method constructs a `ttFEE` pseudo-transaction and inserts it into the `SHAMap` representing the node's initial consensus proposal. This pseudo-transaction carries the winning vote values, not the node's own preference — the outcome is whatever achieved plurality within the safe range. + +The guard in `RCLConsensus.cpp` adds an extra condition: `doVoting()` is only called if the number of available trusted validations meets quorum. This prevents a fee change from being proposed based on an unrepresentative sample during network partitions. + +## `featureXRPFees` Amendment Dual Code Path + +Both `doValidation()` and `doVoting()` maintain two parallel code branches controlled by `rules.enabled(featureXRPFees)`. Before the amendment activated, fees in validations and pseudo-transactions were encoded as legacy integer fields: `sfBaseFee` (uint64), `sfReserveBase` (uint32), `sfReserveIncrement` (uint32), along with the deprecated `sfReferenceFeeUnits`. After the amendment, they become `sfBaseFeeDrops`, `sfReserveBaseDrops`, and `sfReserveIncrementDrops`, all typed as `SF_AMOUNT` (native XRP). + +This dual path is necessary for a live network upgrade: during the transition window, the codebase must be able to read and write both formats. The legacy branch includes an explicit `dropsAs<>()` narrowing conversion with a fallback default — if the `XRPAmount` doesn't fit in a 32-bit value, the current ledger value is used rather than producing a truncated garbage value. + +In the legacy vote-parsing path inside `doVoting()`, external values arriving as raw integers are validated against both the `numeric_limits` of the underlying `XRPType` and `isLegalAmountSigned()`. Out-of-range votes call `noVote()` rather than throwing — the comment explicitly notes "Don't throw because this value is provided by an external entity," a defensive coding decision that isolates the node from malformed peer input. + +## Pseudo-Transaction Construction + +When `doVoting()` determines at least one fee parameter should change, it builds an `STTx` with transaction type `ttFEE`. This is a synthetic ledger-management transaction, not a user-submitted transaction: its `sfAccount` is the zero `AccountID` and it carries a `sfLedgerSequence` equal to `lastClosedLedger->seq() + 1`. The transaction is serialized and inserted into the SHAMap via `addGiveItem()`. If the SHAMap already contains an equivalent fee pseudo-transaction (possible if two validators propose identical changes and their proposals are merged), `addGiveItem()` returns false and the duplicate is logged at warning level. + +## Factory and Ownership + +`make_FeeVote()` is the only public entry point for obtaining a `FeeVoteImpl`. It returns `std::unique_ptr`, enforcing that callers own the object exclusively and cannot observe the concrete type. The `FeeSetup` struct (defined in `Config.h`) defaults to 10 drops base fee, 10 XRP base reserve, and 2 XRP owner reserve — these are the protocol's conservative recommended defaults. Operator configuration overrides are applied at construction time and stored immutably in `target_`. \ No newline at end of file diff --git a/src/xrpld/app/misc/NegativeUNLVote.cpp.ai.json b/src/xrpld/app/misc/NegativeUNLVote.cpp.ai.json new file mode 100644 index 0000000000..7f9ca2a75e --- /dev/null +++ b/src/xrpld/app/misc/NegativeUNLVote.cpp.ai.json @@ -0,0 +1,299 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "NodeID const& myId", + "beast::Journal j" + ], + "lineno": 7, + "name": "NegativeUNLVote" + } + ], + "code_paths": [ + { + "call_chain": [ + "NegativeUNLVote::doVoting", + "buildScoreTable", + "findAllCandidates", + "choose", + "addTx" + ], + "entry_point": "NegativeUNLVote::doVoting", + "purpose": "Performs the Negative UNL voting process: builds validator reliability scores, determines candidates for disabling/enabling, and adds transactions to modify the Negative UNL.", + "validation_points": [ + "XRPL_ASSERT(nidToKeyMap.contains(n), ...) in doVoting (twice: once for disable, once for enable)" + ] + } + ], + "data_flows": [ + { + "field": "unlKeys (hash_set)", + "flow": [ + "doVoting parameter", + "for loop: calcNodeID(k) for each k", + "nidToKeyMap.emplace(nid, k)", + "unlNodeIDs.emplace(nid)", + "nidToKeyMap used for validation and transaction creation" + ], + "origin": "Input parameter to doVoting", + "transformations": [ + "PublicKey \u2192 NodeID via calcNodeID", + "Mapped into nidToKeyMap and unlNodeIDs" + ], + "validated_at": "XRPL_ASSERT(nidToKeyMap.contains(n), ...) in doVoting" + }, + { + "field": "NodeID n (candidate to disable/enable)", + "flow": [ + "choose returns NodeID n", + "XRPL_ASSERT(nidToKeyMap.contains(n), ...)", + "nidToKeyMap.at(n) retrieves PublicKey", + "addTx called with PublicKey" + ], + "origin": "Result of choose(prevLedger->header().hash, candidates...)", + "transformations": [ + "NodeID selected from candidate set", + "Mapped back to PublicKey" + ], + "validated_at": "XRPL_ASSERT(nidToKeyMap.contains(n), ...)" + }, + { + "field": "negUnlKeys (hash_set)", + "flow": [ + "negUnlKeys initialized from prevLedger", + "negUnlToDisable/negUnlToReEnable applied", + "for loop: calcNodeID(k) for each k", + "negUnlNodeIDs.emplace(nid)", + "nidToKeyMap updated if nid not present" + ], + "origin": "prevLedger->negativeUNL()", + "transformations": [ + "PublicKey \u2192 NodeID via calcNodeID", + "Set insertions/removals based on ledger state" + ], + "validated_at": "nidToKeyMap updated to ensure mapping exists before validation" + } + ], + "description": "Implements the NegativeUNLVote class, which manages voting logic for the Negative UNL (Unique Node List) feature in XRPL consensus, including scoring validators, selecting candidates for disabling/re-enabling, and constructing related transactions.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "NodeID presence in nidToKeyMap", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at NegativeUNLVote::doVoting", + "issue_pattern": "Missing empty string validation for NodeID presence in nidToKeyMap", + "why_false_positive": "XRPL_ASSERT validates NodeID presence in nidToKeyMap for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "NodeID presence in nidToKeyMap", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at NegativeUNLVote::doVoting", + "issue_pattern": "Missing empty string validation for NodeID presence in nidToKeyMap", + "why_false_positive": "XRPL_ASSERT validates NodeID presence in nidToKeyMap for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/NegativeUNLVote.cpp", + "functions": [ + { + "args": [ + "NodeID const& myId", + "beast::Journal j" + ], + "lineno": 7, + "name": "NegativeUNLVote::NegativeUNLVote" + }, + { + "args": [ + "std::shared_ptr const& prevLedger", + "hash_set const& unlKeys", + "RCLValidations& validations", + "std::shared_ptr const& initialSet" + ], + "lineno": 11, + "name": "NegativeUNLVote::doVoting" + }, + { + "args": [ + "LedgerIndex seq", + "PublicKey const& vp", + "NegativeUNLModify modify", + "std::shared_ptr const& initialSet" + ], + "lineno": 67, + "name": "NegativeUNLVote::addTx" + }, + { + "args": [ + "uint256 const& randomPadData", + "std::vector const& candidates" + ], + "lineno": 86, + "name": "NegativeUNLVote::choose" + }, + { + "args": [ + "std::shared_ptr const& prevLedger", + "hash_set const& unl", + "RCLValidations& validations" + ], + "lineno": 102, + "name": "NegativeUNLVote::buildScoreTable" + }, + { + "args": [ + "hash_set const& unl", + "hash_set const& negUnl", + "hash_map const& scoreTable" + ], + "lineno": 157, + "name": "NegativeUNLVote::findAllCandidates" + }, + { + "args": [ + "LedgerIndex seq", + "hash_set const& nowTrusted" + ], + "lineno": 210, + "name": "NegativeUNLVote::newValidators" + }, + { + "args": [ + "LedgerIndex seq" + ], + "lineno": 222, + "name": "NegativeUNLVote::purgeNewValidators" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is core consensus logic and likely covered by integration/consensus tests, possibly in files like test/consensus/NegativeUNL_test.cpp or test/app/misc/NegativeUNLVote_test.cpp. However, the specific XRPL_ASSERT validations (NodeID presence in nidToKeyMap) are only triggered on logic errors (should never fail in correct operation), so negative test coverage (ensuring assertion triggers on bad input) may be missing. There may be limited or no direct unit tests for error paths where nidToKeyMap is missing a NodeID, as this would indicate a bug in the code logic or data preparation. Test coverage is likely strong for normal flows but weak for assertion/validation failures.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "NodeID presence in nidToKeyMap", + "location": "NegativeUNLVote::doVoting", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the NodeID selected to disable is present in nidToKeyMap before proceeding" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "NodeID presence in nidToKeyMap", + "location": "NegativeUNLVote::doVoting", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the NodeID selected to re-enable is present in nidToKeyMap before proceeding" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/NegativeUNLVote.cpp.ai.md b/src/xrpld/app/misc/NegativeUNLVote.cpp.ai.md new file mode 100644 index 0000000000..9e51deb397 --- /dev/null +++ b/src/xrpld/app/misc/NegativeUNLVote.cpp.ai.md @@ -0,0 +1,47 @@ +# `NegativeUNLVote.cpp` — Negative UNL Voting Logic + +## Purpose and Context + +The Negative UNL (N-UNL) is a XRPL consensus feature that allows the network to maintain liveness when a subset of trusted validators goes offline. When enough validators agree that a peer validator has stopped participating, they can flag it in a special ledger structure. From that point forward, quorum calculations exclude the flagged validator — the network continues to make progress without waiting for it to return. This file implements `NegativeUNLVote`, the class that drives that decision: scoring validators on recent participation, identifying candidates for disabling or re-enabling, and injecting the corresponding `ttUNL_MODIFY` pseudo-transactions into the consensus round. + +## When Voting Fires + +`doVoting()` is called from `RCLConsensus::Adaptor::buildInitialSet()` specifically when `prevLedger->isVotingLedger()` returns true — i.e., when the previous ledger is at position `seq % 256 == 255`, one ledger before a flag ledger. This means N-UNL changes land in flag ledgers themselves (multiples of 256). Fee votes and amendment votes fire one ledger later, when `prevLedger->isFlagLedger()` is true. The separation ensures that by the time fee/amendment voting runs, the N-UNL for that flag ledger is already resolved. + +## Building the Score Table + +`buildScoreTable()` is the measurement phase. It reads the ancestor skip list from the previous ledger (`keylet::skip()`), which efficiently stores the hashes of the prior 256 ledgers. It then queries `RCLValidations::getTrustedForLedger()` for each of those ledger hashes, incrementing a counter for each UNL validator that submitted a trusted validation. + +Two guards prevent voting on stale or skewed data: + +1. **Insufficient history**: If the skip list contains fewer than `FLAG_LEDGER_INTERVAL` (256) entries, the ledger chain is too short to score reliably. The function returns an empty optional, and `doVoting()` silently skips the round. + +2. **Local node participation check**: Before returning the score table, the function checks whether the local node itself validated at least `negativeUNLMinLocalValsToVote` (90% of 256 ≈ 230) of those ancestor ledgers. If the local node was itself offline or lagging, its view of other validators' participation is unreliable — it may have simply missed validations that the rest of the network received. Returning `{}` here is the conservative correct choice: a node that can't vouch for its own recent history shouldn't be influencing which validators get flagged as unreliable. + +The function also instructs the validation container to retain history: `validations.setSeqToKeep(seq - 1, seq + FLAG_LEDGER_INTERVAL)` ensures the next voting round has the data it needs, even as old validation messages are normally pruned. + +## Finding Candidates + +`findAllCandidates()` applies two watermarks to the score table: + +- **To disable**: A validator with fewer than `negativeUNLLowWaterMark` (50% of 256 = 128) validations is considered persistently offline. To be a disable candidate it must also: not already appear in the N-UNL, not be a new validator (guarded by `newValidators_`), and the current N-UNL must have room (capped at 25% of the UNL via `negativeUNLMaxListed`). + +- **To re-enable**: A validator already in the N-UNL that has recovered and delivered more than `negativeUNLHighWaterMark` (80% of 256 ≈ 204) validations becomes a re-enable candidate. The asymmetric thresholds (50% to enter, 80% to exit) create deliberate hysteresis — a flaky validator that sits near 50% doesn't oscillate in and out of the N-UNL. + +There is a special second pass for re-enable candidates: if a validator has been removed from *all* nodes' UNLs but still appears in the N-UNL, it will never accumulate validations (it's no longer trusted). `findAllCandidates()` catches this by adding any N-UNL member that is absent from the current UNL as a re-enable candidate, cleaning up the ledger state. + +Importantly, the function projects the "next" N-UNL by applying any pending changes from the previous ledger's `validatorToDisable` and `validatorToReEnable` fields before running the candidate search. This avoids double-counting transitions that are already in-flight. + +## Deterministic Selection + +If multiple validators qualify as candidates, `choose()` selects exactly one. It XORs every candidate's `NodeID` against the first 20 bytes of the parent ledger's hash, treating the result as an unsigned integer, and picks the minimum. All validating nodes see the same parent ledger hash, so all honest nodes pick the same candidate and propose the same `ttUNL_MODIFY` transaction — this is how the pseudo-transaction achieves consensus without an explicit proposal round. + +## Injecting the Pseudo-Transaction + +`addTx()` constructs an `STTx` of type `ttUNL_MODIFY`, serializes it, and inserts it into the `initialSet` SHAMap as a `tnTRANSACTION_NM` (non-malleable transaction) item. The transaction carries three fields: a disable/re-enable flag (`sfUNLModifyDisabling`), the target ledger sequence (`sfLedgerSequence`), and the validator's master public key (`sfUNLModifyValidator`). Because all nodes independently derive the same transaction, it naturally reaches consensus as part of the ledger's transaction set without being submitted via the normal transaction queue. + +## New Validator Protection + +`newValidators_` is a `hash_map` tracking validators that were recently added to the trust list. A validator is immune from disabling for two full flag periods (`newValidatorDisableSkip = 512 ledgers`) after it first appears as trusted. This protects a legitimately new validator that simply hasn't had time to accumulate a history. `newValidators()` is called from the consensus adaptor when trust-set changes are detected; `purgeNewValidators()` is called at the start of each `doVoting()` call to evict entries that have aged out. + +Both `newValidators()` and `purgeNewValidators()` hold `mutex_` during their mutations. However, `findAllCandidates()` reads `newValidators_` without the lock. This is safe in practice because `doVoting()` calls `purgeNewValidators()` (under the lock) immediately before `findAllCandidates()`, and `doVoting()` itself runs on the consensus thread — the same thread that would be disrupted by any concurrent N-UNL state change. The `newValidators()` path, if called from a different thread, could race with the `findAllCandidates()` read, but the consequence would be benign: at worst, a newly trusted validator would be considered for disabling one round earlier than intended. \ No newline at end of file diff --git a/src/xrpld/app/misc/NegativeUNLVote.h.ai.json b/src/xrpld/app/misc/NegativeUNLVote.h.ai.json new file mode 100644 index 0000000000..fce9fe4d74 --- /dev/null +++ b/src/xrpld/app/misc/NegativeUNLVote.h.ai.json @@ -0,0 +1,168 @@ +{ + "args": [ + { + "lineno": 49, + "name": "myId" + }, + { + "lineno": 49, + "name": "j" + }, + { + "lineno": 59, + "name": "prevLedger" + }, + { + "lineno": 60, + "name": "unlKeys" + }, + { + "lineno": 61, + "name": "validations" + }, + { + "lineno": 62, + "name": "initialSet" + }, + { + "lineno": 77, + "name": "seq" + }, + { + "lineno": 77, + "name": "nowTrusted" + }, + { + "lineno": 92, + "name": "vp" + }, + { + "lineno": 93, + "name": "modify" + }, + { + "lineno": 104, + "name": "randomPadData" + }, + { + "lineno": 105, + "name": "candidates" + }, + { + "lineno": 119, + "name": "unl" + }, + { + "lineno": 136, + "name": "negUnl" + }, + { + "lineno": 137, + "name": "scoreTable" + } + ], + "classes": [ + { + "args": [ + "NodeID const& myId", + "beast::Journal j" + ], + "lineno": 36, + "name": "NegativeUNLVote" + }, + { + "args": [], + "lineno": 86, + "name": "Candidates" + } + ], + "description": "Defines the NegativeUNLVote class, which manages the process of voting to disable or re-enable validators on the Negative UNL (Unique Node List) in the XRPL consensus protocol, including reliability scoring and candidate selection.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/NegativeUNLVote.h", + "functions": [ + { + "args": [ + "NodeID const& myId", + "beast::Journal j" + ], + "lineno": 49, + "name": "NegativeUNLVote" + }, + { + "args": [], + "lineno": 50, + "name": "~NegativeUNLVote" + }, + { + "args": [ + "std::shared_ptr const& prevLedger", + "hash_set const& unlKeys", + "RCLValidations& validations", + "std::shared_ptr const& initialSet" + ], + "lineno": 59, + "name": "doVoting" + }, + { + "args": [ + "LedgerIndex seq", + "hash_set const& nowTrusted" + ], + "lineno": 77, + "name": "newValidators" + }, + { + "args": [ + "LedgerIndex seq", + "PublicKey const& vp", + "NegativeUNLModify modify", + "std::shared_ptr const& initialSet" + ], + "lineno": 92, + "name": "addTx" + }, + { + "args": [ + "uint256 const& randomPadData", + "std::vector const& candidates" + ], + "lineno": 104, + "name": "choose" + }, + { + "args": [ + "std::shared_ptr const& prevLedger", + "hash_set const& unl", + "RCLValidations& validations" + ], + "lineno": 119, + "name": "buildScoreTable" + }, + { + "args": [ + "hash_set const& unl", + "hash_set const& negUnl", + "hash_map const& scoreTable" + ], + "lineno": 136, + "name": "findAllCandidates" + }, + { + "args": [ + "LedgerIndex seq" + ], + "lineno": 149, + "name": "purgeNewValidators" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 19, + "name": "test" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/NegativeUNLVote.h.ai.md b/src/xrpld/app/misc/NegativeUNLVote.h.ai.md new file mode 100644 index 0000000000..3f24646b59 --- /dev/null +++ b/src/xrpld/app/misc/NegativeUNLVote.h.ai.md @@ -0,0 +1,48 @@ +# `NegativeUNLVote.h` — Validator Reliability Scoring and Negative UNL Vote Management + +## Purpose and Context + +The Negative UNL (Unique Node List) is an XRPL protocol mechanism for handling temporarily unreliable validators without destabilizing consensus quorum requirements. When a validator goes offline or becomes unreliable, its absence forces the network to reach a higher fraction of the *remaining* validators to make consensus, potentially stalling the ledger. The Negative UNL lets the network collectively agree to exclude a validator from quorum calculations while it remains offline, effectively treating it as absent rather than absent-but-counted. + +`NegativeUNLVote.h` declares the `NegativeUNLVote` class, the singleton manager that implements local vote casting for Negative UNL changes. Once per flag ledger boundary (every `FLAG_LEDGER_INTERVAL` = 256 ledgers), the class scores all validators' recent participation, identifies candidates to disable or re-enable, and injects `ttUNL_MODIFY` pseudo-transactions into the candidate transaction set for the upcoming flag ledger. Because every honest node runs the same deterministic algorithm against the same shared data, they all produce the same pseudo-transaction and it achieves consensus without an explicit vote-collection round. + +## Key Design Decisions + +### Water Mark Thresholds + +The class defines four static constants that govern the entire scoring logic: + +- `negativeUNLLowWaterMark` = 50% of 256 (128 validations) — a validator sending fewer than 128 validations in the last flag period is a candidate to be disabled. +- `negativeUNLHighWaterMark` = 80% of 256 (204 validations) — a disabled validator must exceed this to be re-enabled, creating deliberate hysteresis so a flapping validator isn't toggled on every cycle. +- `negativeUNLMinLocalValsToVote` = 90% of 256 (230 validations) — the local node itself must have issued at least this many validations before it participates in the vote. A node that was itself offline cannot reliably measure others and should abstain. +- `negativeUNLMaxListed` = 25% — at most a quarter of the UNL may be on the Negative UNL simultaneously, preventing the protocol from hollowing out quorum entirely. + +The wide gap between the low and high watermarks (50% vs 80%) is intentional hysteresis. Without it, a validator sitting around the threshold could oscillate and cause repeated toggle transactions. + +### Deterministic Candidate Selection via `choose()` + +When `findAllCandidates()` returns multiple validators qualifying for disable or re-enable, only one is acted on per cycle. Rather than a separate voting round, `choose()` XORs each `NodeID` with the first 20 bytes of the previous ledger's hash (used as a random pad), then picks the candidate whose XOR result is numerically smallest. Since every node has the same parent ledger hash, they all pick the same winner — a lightweight leader-election idiom common in the XRPL codebase. A single transaction per cycle is also a rate-limiter: the protocol processes at most one disable and one re-enable per flag period. + +### New Validator Grace Period + +Adding a validator to the UNL and then immediately marking it offline because it has no recent scoring history would be harmful. The `newValidators_` map (a `hash_map`) records when each newly trusted validator was observed, and `findAllCandidates()` excludes them from disable candidates until `newValidatorDisableSkip` ledgers (512 = two full flag periods) have elapsed. `purgeNewValidators()` removes entries from the map when they age out, called at the start of each `doVoting` invocation. + +This is the only state that requires mutex protection. `doVoting()` is called on the consensus thread while `newValidators()` may be called from the validator-list update path; the `mutex_` guards only `newValidators_` accesses. + +### Score Table Construction and Validation History + +`buildScoreTable()` queries `RCLValidations` for every ledger in the last 256 slots, counting how many trusted validators validated each one. Before querying, it calls `validations.setSeqToKeep()` to pin the validation history window, preventing the container from garbage-collecting messages that are still needed for this calculation. The score table maps `NodeID → uint32_t` (validation count). If the ledger lacks 256 ancestors in its skip list (i.e., the chain is too young), the function returns `std::nullopt` and voting is skipped entirely for that cycle. + +### NegUnl Projection for Candidate Evaluation + +`doVoting()` does not use the ledger's current `negativeUNL()` set verbatim. Instead it projects one step ahead: it inserts `validatorToDisable` (if pending) and removes `validatorToReEnable` (if pending) to compute what the Negative UNL will be *after* the current flag ledger closes. This projection ensures `findAllCandidates()` doesn't re-nominate a validator that is already queued for action. + +### Pseudo-Transaction Injection + +`addTx()` constructs a `STTx` of type `ttUNL_MODIFY`, sets `sfUNLModifyDisabling` to 1 or 0, records the ledger sequence and the validator's master public key, then adds it to the `SHAMap` as `tnTRANSACTION_NM` (non-maleable, unsigned). These pseudo-transactions are not user-submitted and require no fee or signature; they exist purely as a ledger-state-modification mechanism agreed upon by all validating nodes running the same algorithm. + +## Integration with RCLConsensus + +`doVoting()` is invoked from `RCLConsensus::buildInitialSet()` specifically when `prevLedger->isVotingLedger()` is true, meaning the *next* ledger to close is a flag ledger and Negative UNL changes should be embedded. The `newValidators()` call happens separately in `RCLConsensus::startRound()` whenever the trusted validator set changes. Both paths share the same `NegativeUNLVote` instance (`nUnlVote_`) owned by `RCLConsensus`. + +Two test classes are granted `friend` access — `NegativeUNLVoteInternal_test` and `NegativeUNLVoteScoreTable_test` — to exercise the private scoring and candidate-selection logic directly. \ No newline at end of file diff --git a/src/xrpld/app/misc/NetworkOPs.cpp.ai.json b/src/xrpld/app/misc/NetworkOPs.cpp.ai.json new file mode 100644 index 0000000000..ff9d3b3d56 --- /dev/null +++ b/src/xrpld/app/misc/NetworkOPs.cpp.ai.json @@ -0,0 +1,1349 @@ +{ + "args": [ + { + "lineno": 4317, + "name": "uTipIndex" + }, + { + "lineno": 4317, + "name": "sleOfferDir" + }, + { + "lineno": 4318, + "name": "saDirRate" + }, + { + "lineno": 4318, + "name": "amountFromQuality" + }, + { + "lineno": 4318, + "name": "getQuality" + }, + { + "lineno": 4320, + "name": "cdirFirst" + }, + { + "lineno": 4320, + "name": "view" + }, + { + "lineno": 4320, + "name": "uBookEntry" + }, + { + "lineno": 4320, + "name": "offerIndex" + }, + { + "lineno": 4322, + "name": "JLOG" + }, + { + "lineno": 4322, + "name": "m_journal" + }, + { + "lineno": 4328, + "name": "bDone" + }, + { + "lineno": 4330, + "name": "sleOffer" + }, + { + "lineno": 4330, + "name": "keylet" + }, + { + "lineno": 4333, + "name": "uOfferOwnerID" + }, + { + "lineno": 4333, + "name": "sfAccount" + }, + { + "lineno": 4334, + "name": "saTakerGets" + }, + { + "lineno": 4334, + "name": "sfTakerGets" + }, + { + "lineno": 4335, + "name": "saTakerPays" + }, + { + "lineno": 4335, + "name": "sfTakerPays" + }, + { + "lineno": 4336, + "name": "STAmount" + }, + { + "lineno": 4336, + "name": "saOwnerFunds" + }, + { + "lineno": 4337, + "name": "firstOwnerOffer" + }, + { + "lineno": 4339, + "name": "book" + }, + { + "lineno": 4345, + "name": "bGlobalFreeze" + }, + { + "lineno": 4352, + "name": "umBalanceEntry" + }, + { + "lineno": 4352, + "name": "umBalance" + }, + { + "lineno": 4361, + "name": "accountHolds" + }, + { + "lineno": 4364, + "name": "fhZERO_IF_FROZEN" + }, + { + "lineno": 4365, + "name": "ahZERO_IF_UNAUTHORIZED" + }, + { + "lineno": 4366, + "name": "viewJ" + }, + { + "lineno": 4369, + "name": "beast" + }, + { + "lineno": 4373, + "name": "Json" + }, + { + "lineno": 4373, + "name": "jvOffer" + }, + { + "lineno": 4373, + "name": "JsonOptions" + }, + { + "lineno": 4375, + "name": "saTakerGetsFunded" + }, + { + "lineno": 4376, + "name": "saOwnerFundsLimit" + }, + { + "lineno": 4377, + "name": "Rate" + }, + { + "lineno": 4377, + "name": "offerRate" + }, + { + "lineno": 4377, + "name": "parityRate" + }, + { + "lineno": 4379, + "name": "rate" + }, + { + "lineno": 4381, + "name": "uTakerID" + }, + { + "lineno": 4387, + "name": "divide" + }, + { + "lineno": 4397, + "name": "multiply" + }, + { + "lineno": 4397, + "name": "asset" + }, + { + "lineno": 4397, + "name": "std" + }, + { + "lineno": 4403, + "name": "saOwnerPays" + }, + { + "lineno": 4408, + "name": "jvOffers" + }, + { + "lineno": 4409, + "name": "jvOf" + }, + { + "lineno": 4410, + "name": "jss" + }, + { + "lineno": 4415, + "name": "cdirNext" + }, + { + "lineno": 4417, + "name": "bDirectAdvance" + }, + { + "lineno": 4436, + "name": "lpLedger" + }, + { + "lineno": 4439, + "name": "bProof" + }, + { + "lineno": 4440, + "name": "iLimit" + }, + { + "lineno": 4441, + "name": "jvMarker" + }, + { + "lineno": 4442, + "name": "jvResult" + }, + { + "lineno": 4446, + "name": "MetaView" + }, + { + "lineno": 4446, + "name": "lesActive" + }, + { + "lineno": 4446, + "name": "tapNONE" + }, + { + "lineno": 4447, + "name": "OrderBookIterator" + }, + { + "lineno": 4447, + "name": "obIterator" + }, + { + "lineno": 4449, + "name": "transferRate" + }, + { + "lineno": 4451, + "name": "bGlobalFreeze" + }, + { + "lineno": 4455, + "name": "SLE" + }, + { + "lineno": 4455, + "name": "pointer" + }, + { + "lineno": 4455, + "name": "sleOffer" + }, + { + "lineno": 4470, + "name": "currency" + }, + { + "lineno": 4474, + "name": "isNegative" + }, + { + "lineno": 4477, + "name": "zero" + }, + { + "lineno": 4502, + "name": "isZero" + }, + { + "lineno": 4516, + "name": "collect_metrics" + }, + { + "lineno": 4517, + "name": "counters" + }, + { + "lineno": 4517, + "name": "mode" + }, + { + "lineno": 4517, + "name": "start" + }, + { + "lineno": 4517, + "name": "initialSync" + }, + { + "lineno": 4517, + "name": "accounting_" + }, + { + "lineno": 4518, + "name": "current" + }, + { + "lineno": 4518, + "name": "chrono" + }, + { + "lineno": 4518, + "name": "steady_clock" + }, + { + "lineno": 4518, + "name": "microseconds" + }, + { + "lineno": 4521, + "name": "m_statsMutex" + }, + { + "lineno": 4522, + "name": "m_stats" + }, + { + "lineno": 4524, + "name": "OperatingMode" + }, + { + "lineno": 4524, + "name": "DISCONNECTED" + }, + { + "lineno": 4526, + "name": "CONNECTED" + }, + { + "lineno": 4528, + "name": "SYNCING" + }, + { + "lineno": 4530, + "name": "TRACKING" + }, + { + "lineno": 4532, + "name": "FULL" + }, + { + "lineno": 4538, + "name": "StateAccounting" + }, + { + "lineno": 4538, + "name": "om" + }, + { + "lineno": 4539, + "name": "now" + }, + { + "lineno": 4541, + "name": "lock" + }, + { + "lineno": 4541, + "name": "mutex_" + }, + { + "lineno": 4542, + "name": "counters_" + }, + { + "lineno": 4545, + "name": "initialSyncUs_" + }, + { + "lineno": 4546, + "name": "processStart_" + }, + { + "lineno": 4549, + "name": "dur" + }, + { + "lineno": 4549, + "name": "start_" + }, + { + "lineno": 4551, + "name": "mode_" + }, + { + "lineno": 4555, + "name": "json" + }, + { + "lineno": 4555, + "name": "obj" + }, + { + "lineno": 4556, + "name": "getCounterData" + }, + { + "lineno": 4563, + "name": "states_" + }, + { + "lineno": 4565, + "name": "state" + }, + { + "lineno": 4567, + "name": "to_string" + }, + { + "lineno": 4568, + "name": "count" + }, + { + "lineno": 4579, + "name": "make_NetworkOPs" + }, + { + "lineno": 4580, + "name": "ServiceRegistry" + }, + { + "lineno": 4580, + "name": "registry" + }, + { + "lineno": 4581, + "name": "clock_type" + }, + { + "lineno": 4581, + "name": "clock" + }, + { + "lineno": 4582, + "name": "standalone" + }, + { + "lineno": 4583, + "name": "minPeerCount" + }, + { + "lineno": 4584, + "name": "startValid" + }, + { + "lineno": 4585, + "name": "JobQueue" + }, + { + "lineno": 4585, + "name": "jobQueue" + }, + { + "lineno": 4586, + "name": "LedgerMaster" + }, + { + "lineno": 4586, + "name": "ledgerMaster" + }, + { + "lineno": 4587, + "name": "ValidatorKeys" + }, + { + "lineno": 4587, + "name": "validatorKeys" + }, + { + "lineno": 4588, + "name": "boost" + }, + { + "lineno": 4588, + "name": "asio" + }, + { + "lineno": 4588, + "name": "io_context" + }, + { + "lineno": 4588, + "name": "ioCtx" + }, + { + "lineno": 4589, + "name": "beast" + }, + { + "lineno": 4589, + "name": "Journal" + }, + { + "lineno": 4589, + "name": "journal" + }, + { + "lineno": 4590, + "name": "insight" + }, + { + "lineno": 4590, + "name": "Collector" + }, + { + "lineno": 4590, + "name": "ptr" + }, + { + "lineno": 4590, + "name": "collector" + }, + { + "lineno": 4592, + "name": "make_unique" + }, + { + "lineno": 4592, + "name": "NetworkOPsImp" + } + ], + "chunked": true, + "classes": [ + { + "args": [], + "lineno": 54, + "name": "NetworkOPsImp" + }, + { + "args": [], + "lineno": 60, + "name": "TransactionStatus" + }, + { + "args": [], + "lineno": 92, + "name": "StateAccounting" + }, + { + "args": [], + "lineno": 151, + "name": "ServerFeeSummary" + }, + { + "args": [], + "lineno": 453, + "name": "SubAccountHistoryIndex" + }, + { + "args": [], + "lineno": 464, + "name": "SubAccountHistoryInfo" + }, + { + "args": [], + "lineno": 468, + "name": "SubAccountHistoryInfoWeak" + }, + { + "args": [], + "lineno": 799, + "name": "Stats" + } + ], + "description": "NetworkOPs.cpp implements the core network operations logic for an XRPL server node, managing transaction processing (including batching, validation, and application), consensus state transitions, ledger publication, and real-time event subscriptions for clients. It handles server state management, peer and validator communications, fee reporting, and provides interfaces for monitoring, account history streaming, and various subscription types (ledger, transactions, validations, etc.). The file defines the main NetworkOPsImp class, which coordinates these activities, ensuring the node stays in sync with the network, processes transactions efficiently, and provides up-to-date information to connected clients and monitoring tools. This section of NetworkOPs.cpp implements core logic for retrieving and formatting order book offers (getBookPage), tracking and reporting server operational state metrics (collect_metrics, StateAccounting::mode, StateAccounting::json), and constructing the main NetworkOPs object (make_NetworkOPs) for the XRPL server. The getBookPage function fetches offers from the ledger, calculates available funds, applies transfer fees, and formats the results as JSON. The metrics functions track server state durations and transitions for monitoring and diagnostics.", + "file_path": "/root/projects/transia/athenah-ai/workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/NetworkOPs.cpp", + "functions": [ + { + "args": [], + "lineno": 181, + "name": "getOperatingMode" + }, + { + "args": [ + "bool const admin" + ], + "lineno": 186, + "name": "strOperatingMode" + }, + { + "args": [], + "lineno": 191, + "name": "setStandAlone" + }, + { + "args": [], + "lineno": 196, + "name": "setNeedNetworkLedger" + }, + { + "args": [], + "lineno": 201, + "name": "clearNeedNetworkLedger" + }, + { + "args": [], + "lineno": 206, + "name": "isNeedNetworkLedger" + }, + { + "args": [], + "lineno": 211, + "name": "isFull" + }, + { + "args": [ + "bool forAdmin" + ], + "lineno": 216, + "name": "getHostId" + }, + { + "args": [], + "lineno": 229, + "name": "setStateTimer" + }, + { + "args": [ + "boost::asio::steady_timer& timer", + "std::chrono::milliseconds const& expiry_time", + "std::function onExpire", + "std::function onError" + ], + "lineno": 237, + "name": "setTimer" + }, + { + "args": [], + "lineno": 255, + "name": "setHeartbeatTimer" + }, + { + "args": [], + "lineno": 265, + "name": "setClusterTimer" + }, + { + "args": [ + "SubAccountHistoryInfoWeak subInfo" + ], + "lineno": 277, + "name": "setAccountHistoryJobTimer" + }, + { + "args": [], + "lineno": 287, + "name": "processHeartbeatTimer" + }, + { + "args": [], + "lineno": 340, + "name": "processClusterTimer" + }, + { + "args": [ + "OperatingMode const mode", + "bool const admin" + ], + "lineno": 362, + "name": "strOperatingMode" + }, + { + "args": [ + "std::shared_ptr const& iTrans" + ], + "lineno": 374, + "name": "submitTransaction" + }, + { + "args": [ + "std::shared_ptr& transaction" + ], + "lineno": 414, + "name": "preProcessTransaction" + }, + { + "args": [ + "std::shared_ptr& transaction", + "bool bUnlimited", + "bool bLocal", + "FailHard failType" + ], + "lineno": 447, + "name": "processTransaction" + }, + { + "args": [ + "std::shared_ptr transaction", + "bool bUnlimited", + "FailHard failType" + ], + "lineno": 460, + "name": "doTransactionAsync" + }, + { + "args": [ + "std::shared_ptr transaction", + "bool bUnlimited", + "FailHard failType" + ], + "lineno": 476, + "name": "doTransactionSync" + }, + { + "args": [ + "std::unique_lock& lock", + "std::function const&)> retryCallback" + ], + "lineno": 489, + "name": "doTransactionSyncBatch" + }, + { + "args": [ + "CanonicalTXSet const& set" + ], + "lineno": 507, + "name": "processTransactionSet" + }, + { + "args": [], + "lineno": 547, + "name": "transactionBatch" + }, + { + "args": [ + "std::unique_lock& batchLock" + ], + "lineno": 555, + "name": "apply" + }, + { + "args": [ + "std::shared_ptr lpLedger", + "AccountID const& account" + ], + "lineno": 677, + "name": "getOwnerInfo" + }, + { + "args": [], + "lineno": 713, + "name": "isBlocked" + }, + { + "args": [], + "lineno": 718, + "name": "isAmendmentBlocked" + }, + { + "args": [], + "lineno": 723, + "name": "setAmendmentBlocked" + }, + { + "args": [], + "lineno": 729, + "name": "isAmendmentWarned" + }, + { + "args": [], + "lineno": 734, + "name": "setAmendmentWarned" + }, + { + "args": [], + "lineno": 739, + "name": "clearAmendmentWarned" + }, + { + "args": [], + "lineno": 744, + "name": "isUNLBlocked" + }, + { + "args": [], + "lineno": 749, + "name": "setUNLBlocked" + }, + { + "args": [], + "lineno": 754, + "name": "clearUNLBlocked" + }, + { + "args": [ + "Overlay::PeerSequence const& peerList", + "uint256& networkClosed" + ], + "lineno": 759, + "name": "checkLastClosedLedger" + }, + { + "args": [ + "std::shared_ptr const& newLCL" + ], + "lineno": 819, + "name": "switchLastClosedLedger" + }, + { + "args": [ + "uint256 const& networkClosed", + "std::unique_ptr const& clog" + ], + "lineno": 857, + "name": "beginConsensus" + }, + { + "args": [ + "RCLCxPeerPos peerPos" + ], + "lineno": 911, + "name": "processTrustedProposal" + }, + { + "args": [ + "std::shared_ptr const& map", + "bool fromAcquire" + ], + "lineno": 930, + "name": "mapComplete" + }, + { + "args": [ + "std::unique_ptr const& clog" + ], + "lineno": 945, + "name": "endConsensus" + }, + { + "args": [], + "lineno": 991, + "name": "consensusViewChange" + }, + { + "args": [ + "Manifest const& mo" + ], + "lineno": 997, + "name": "pubManifest" + }, + { + "args": [ + "XRPAmount fee", + "TxQ::Metrics escalationMetrics", + "LoadFeeTrack const& loadFeeTrack" + ], + "lineno": 1022, + "name": "ServerFeeSummary" + }, + { + "args": [ + "NetworkOPsImp::ServerFeeSummary const& b" + ], + "lineno": 1029, + "name": "operator!=" + }, + { + "args": [ + "std::uint64_t v" + ], + "lineno": 1045, + "name": "trunc32" + }, + { + "args": [], + "lineno": 1052, + "name": "pubServer" + }, + { + "args": [ + "ConsensusPhase phase" + ], + "lineno": 1087, + "name": "pubConsensus" + }, + { + "args": [ + "std::shared_ptr const& val" + ], + "lineno": 1107, + "name": "pubValidation" + }, + { + "args": [ + "std::function const& func" + ], + "lineno": 1167, + "name": "pubPeerStatus" + }, + { + "args": [ + "OperatingMode om" + ], + "lineno": 1191, + "name": "setMode" + }, + { + "args": [ + "std::shared_ptr const& val", + "std::string const& source" + ], + "lineno": 1212, + "name": "recvValidation" + }, + { + "args": [], + "lineno": 1247, + "name": "getConsensusInfo" + }, + { + "args": [ + "bool human", + "bool admin", + "bool counters" + ], + "lineno": 1252, + "name": "getServerInfo" + }, + { + "args": [], + "lineno": 1437, + "name": "clearLedgerFetch" + }, + { + "args": [], + "lineno": 1442, + "name": "getLedgerFetchInfo" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "std::shared_ptr const& transaction", + "TER result" + ], + "lineno": 1447, + "name": "pubProposedTransaction" + }, + { + "args": [ + "std::shared_ptr const& lpAccepted" + ], + "lineno": 1477, + "name": "pubLedger" + }, + { + "args": [], + "lineno": 1537, + "name": "reportFeeChange" + }, + { + "args": [ + "ConsensusPhase phase" + ], + "lineno": 1547, + "name": "reportConsensusStateChange" + }, + { + "args": [ + "ReadView const& view" + ], + "lineno": 1552, + "name": "updateLocalTx" + }, + { + "args": [], + "lineno": 1556, + "name": "getLocalTxCount" + }, + { + "args": [ + "std::shared_ptr const& transaction", + "TER result", + "bool validated", + "std::shared_ptr const& ledger", + "std::optional> meta" + ], + "lineno": 1562, + "name": "transJson" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "AcceptedLedgerTx const& transaction", + "bool last" + ], + "lineno": 1622, + "name": "pubValidatedTransaction" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "AcceptedLedgerTx const& transaction", + "bool last" + ], + "lineno": 1662, + "name": "pubAccountTransaction" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "std::shared_ptr const& tx", + "TER result" + ], + "lineno": 1747, + "name": "pubProposedAccountTransaction" + }, + { + "args": [ + "InfoSub::ref isrListener", + "hash_set const& vnaAccountIDs", + "bool rt" + ], + "lineno": 1787, + "name": "subAccount" + }, + { + "args": [ + "InfoSub::ref isrListener", + "hash_set const& vnaAccountIDs", + "bool rt" + ], + "lineno": 1812, + "name": "unsubAccount" + }, + { + "args": [ + "std::uint64_t uSeq", + "hash_set const& vnaAccountIDs", + "bool rt" + ], + "lineno": 1825, + "name": "unsubAccountInternal" + }, + { + "args": [ + "SubAccountHistoryInfoWeak subInfo" + ], + "lineno": 1843, + "name": "addAccountHistoryJob" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "SubAccountHistoryInfoWeak& subInfo" + ], + "lineno": 2027, + "name": "subAccountHistoryStart" + }, + { + "args": [ + "InfoSub::ref isrListener", + "AccountID const& accountId" + ], + "lineno": 2057, + "name": "subAccountHistory" + }, + { + "args": [ + "InfoSub::ref isrListener", + "AccountID const& account", + "bool historyOnly" + ], + "lineno": 2087, + "name": "unsubAccountHistory" + }, + { + "args": [ + "std::uint64_t seq", + "AccountID const& account", + "bool historyOnly" + ], + "lineno": 2097, + "name": "unsubAccountHistoryInternal" + }, + { + "args": [ + "InfoSub::ref isrListener", + "Book const& book" + ], + "lineno": 2117, + "name": "subBook" + }, + { + "args": [ + "std::uint64_t uSeq", + "Book const& book" + ], + "lineno": 2128, + "name": "unsubBook" + }, + { + "args": [ + "std::optional consensusDelay" + ], + "lineno": 2135, + "name": "acceptLedger" + }, + { + "args": [ + "InfoSub::ref isrListener", + "Json::Value& jvResult" + ], + "lineno": 2152, + "name": "subLedger" + }, + { + "args": [ + "InfoSub::ref isrListener" + ], + "lineno": 2172, + "name": "subBookChanges" + }, + { + "args": [ + "std::uint64_t uSeq" + ], + "lineno": 2178, + "name": "unsubLedger" + }, + { + "args": [ + "std::uint64_t uSeq" + ], + "lineno": 2184, + "name": "unsubBookChanges" + }, + { + "args": [ + "InfoSub::ref isrListener" + ], + "lineno": 2190, + "name": "subManifests" + }, + { + "args": [ + "std::uint64_t uSeq" + ], + "lineno": 2196, + "name": "unsubManifests" + }, + { + "args": [ + "InfoSub::ref isrListener", + "Json::Value& jvResult", + "bool admin" + ], + "lineno": 2202, + "name": "subServer" + }, + { + "args": [ + "std::uint64_t uSeq" + ], + "lineno": 2221, + "name": "unsubServer" + }, + { + "args": [ + "InfoSub::ref isrListener" + ], + "lineno": 2227, + "name": "subTransactions" + }, + { + "args": [ + "std::uint64_t uSeq" + ], + "lineno": 2233, + "name": "unsubTransactions" + }, + { + "args": [ + "InfoSub::ref isrListener" + ], + "lineno": 2239, + "name": "subRTTransactions" + }, + { + "args": [ + "std::uint64_t uSeq" + ], + "lineno": 2245, + "name": "unsubRTTransactions" + }, + { + "args": [ + "InfoSub::ref isrListener" + ], + "lineno": 2251, + "name": "subValidations" + }, + { + "args": [ + "Json::Value& obj" + ], + "lineno": 2256, + "name": "stateAccounting" + }, + { + "args": [ + "std::uint64_t uSeq" + ], + "lineno": 2261, + "name": "unsubValidations" + }, + { + "args": [ + "InfoSub::ref isrListener" + ], + "lineno": 2267, + "name": "subPeerStatus" + }, + { + "args": [ + "std::uint64_t uSeq" + ], + "lineno": 2273, + "name": "unsubPeerStatus" + }, + { + "args": [ + "InfoSub::ref isrListener" + ], + "lineno": 2279, + "name": "subConsensus" + }, + { + "args": [ + "std::uint64_t uSeq" + ], + "lineno": 2285, + "name": "unsubConsensus" + }, + { + "args": [ + "std::string const& strUrl" + ], + "lineno": 2291, + "name": "findRpcSub" + }, + { + "args": [ + "std::string const& strUrl", + "InfoSub::ref rspEntry" + ], + "lineno": 2301, + "name": "addRpcSub" + }, + { + "args": [ + "std::string const& strUrl" + ], + "lineno": 2308, + "name": "tryRemoveRpcSub" + }, + { + "args": [ + "std::shared_ptr& lpLedger", + "Book const& book", + "AccountID const& uTakerID", + "bool const bProof", + "unsigned int iLimit", + "Json::Value const& jvMarker", + "Json::Value& jvResult" + ], + "lineno": 2332, + "name": "getBookPage" + }, + { + "args": [ + "std::shared_ptr lpLedger", + "Book const& book", + "AccountID const& uTakerID", + "bool const bProof", + "unsigned int iLimit", + "Json::Value const& jvMarker", + "Json::Value& jvResult" + ], + "lineno": 4403, + "name": "NetworkOPsImp::getBookPage" + }, + { + "args": [], + "lineno": 4497, + "name": "NetworkOPsImp::collect_metrics" + }, + { + "args": [ + "OperatingMode om" + ], + "lineno": 4526, + "name": "NetworkOPsImp::StateAccounting::mode" + }, + { + "args": [ + "Json::Value& obj" + ], + "lineno": 4545, + "name": "NetworkOPsImp::StateAccounting::json" + }, + { + "args": [ + "ServiceRegistry& registry", + "NetworkOPs::clock_type& clock", + "bool standalone", + "std::size_t minPeerCount", + "bool startValid", + "JobQueue& jobQueue", + "LedgerMaster& ledgerMaster", + "ValidatorKeys const& validatorKeys", + "boost::asio::io_context& ioCtx", + "beast::Journal journal", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 4570, + "name": "make_NetworkOPs" + } + ], + "language": "cpp", + "namespaces": [ + { + "lineno": 54, + "name": "xrpl" + }, + { + "lineno": 233, + "name": "xrpl" + } + ], + "num_chunks": 2 +} \ No newline at end of file diff --git a/src/xrpld/app/misc/NetworkOPs.cpp.ai.md b/src/xrpld/app/misc/NetworkOPs.cpp.ai.md new file mode 100644 index 0000000000..ae172b0a1d --- /dev/null +++ b/src/xrpld/app/misc/NetworkOPs.cpp.ai.md @@ -0,0 +1,81 @@ +# `src/xrpld/app/misc/NetworkOPs.cpp` + +## Role and Purpose + +`NetworkOPs.cpp` is one of the largest and most central files in the rippled server. It contains the complete implementation of `NetworkOPsImp`, which realizes the `NetworkOPs` abstract interface. The class acts as the primary coordinator between the consensus engine, the ledger system, the peer overlay, the transaction processing pipeline, and the real-time subscription infrastructure that clients use to observe ledger activity. Almost every major subsystem in the server routes through this class at some point during normal operation. + +## `NetworkOPsImp`: Core Implementation Class + +`NetworkOPsImp` is a `final` class defined within the `xrpl` namespace. It is constructed via the free function `make_NetworkOPs()` and is never instantiated directly by the rest of the codebase. The constructor takes an extensive set of dependencies — `ServiceRegistry`, `LedgerMaster`, `JobQueue`, `ValidatorKeys`, `boost::asio::io_context`, and an insight metrics `Collector` — which it wires together. The `ServiceRegistry` reference is used throughout the implementation to reach nearly every other application subsystem. + +A notable initialization detail is that if `start_valid` is true (signalling that the node has been configured to skip initial sync), the operating mode starts as `OperatingMode::FULL` and `minPeerCount_` is set to zero. This matters for validator nodes that already have the full ledger history and need to begin participating in consensus immediately. + +## Transaction Processing Pipeline + +The file implements a sophisticated two-path transaction pipeline controlled by three primitives: a `std::mutex` (`mMutex`), a `std::condition_variable` (`mCond`), and a `DispatchState` enum with values `none`, `scheduled`, and `running`. + +**Local (synchronous) path** — `doTransactionSync()`: Used for transactions submitted by a directly connected RPC client. The calling thread joins the batch, then blocks on `mCond` until the batch containing its transaction completes. The retry loop in `doTransactionSyncBatch()` continues looping as long as `transaction->getApplying()` is still set, meaning the calling thread will wait through multiple batch cycles if the batch is already running when it arrives. + +**Remote (asynchronous) path** — `doTransactionAsync()`: Used for peer-relayed transactions. The transaction is enqueued and a `jtBATCH` job is posted to the `JobQueue`. Fire-and-forget: the submitter returns immediately. + +The actual application happens in `apply()`, which swaps the pending `mTransactions` vector under the lock, unlocks, then acquires both the `masterMutex` and `ledgerMaster` mutex using `std::lock()` for deadlock-safe dual acquisition. It calls `TxQ::apply()` for each transaction against the open ledger view. After the batch, it re-acquires the lock, clears the `applying` flag on each transaction, notifies all waiters via `mCond.notify_all()`, and sets `mDispatchState = none`. + +A critical design choice here: the batch lock is released before calling into the transaction queue and the open ledger. This avoids holding the batch mutex while performing expensive ledger mutations, but it means the condition variable wait loop must re-check the guard condition, not just wake up once. + +After a successful application, each transaction is broadcast to peers via `Overlay::relay()` and, if it was included in the open ledger, `pubProposedTransaction()` is called to notify real-time subscribers. + +## Operating Mode State Machine + +The server progresses through five `OperatingMode` states: `DISCONNECTED`, `CONNECTED`, `SYNCING`, `TRACKING`, and `FULL`. These modes are stored in `std::atomic mMode` for lock-free reads by other threads. + +`setMode()` has intentional collapsing logic: attempting to set `CONNECTED` when the validated ledger is less than one minute old automatically promotes to `SYNCING`, and vice versa. This prevents oscillation around the CONNECTED/SYNCING boundary for nodes that are nearly caught up. + +Blocking conditions — `amendmentBlocked_` and `unlBlocked_` — are enforced as `std::atomic` flags. Both `setAmendmentBlocked()` and `setUNLBlocked()` forcibly call `setMode(OperatingMode::CONNECTED)`, preventing the node from advancing to FULL while blocked. The composite `isBlocked()` check returns true if either flag is set. `getServerInfo()` injects machine-readable warning objects into its output when these conditions are active, giving operators structured error data rather than just logs. + +## Heartbeat Timer and Consensus Entry Point + +`processHeartbeatTimer()` fires on the `ledgerGRANULARITY` interval (typically around 1 second). It is the main entry point into the consensus engine for timer-driven events. Under the master mutex it: + +1. Calls `LoadManager::heartbeat()` to track resource usage. +2. Counts connected peers and drops to `DISCONNECTED` if below `minPeerCount_`, returning without advancing consensus. This prevents a partially connected node from mistakenly driving the consensus clock. +3. Transitions from `DISCONNECTED` to `CONNECTED` when peer count recovers. +4. Calls `mConsensus.timerEntry()` with the current network close time to advance the RCL consensus state machine. +5. Detects consensus phase changes and dispatches `reportConsensusStateChange()` for subscription notifications. + +The timer re-arms itself unconditionally at the end with another call to `setHeartbeatTimer()`. The `setTimer()` helper wraps the Boost Asio timer in a `ClosureCounter`, ensuring that outstanding async handlers are tracked and joined during `stop()` to prevent use-after-free on teardown. + +## Consensus Lifecycle Methods + +`beginConsensus()` starts a new consensus round by calling `mConsensus.startRound()` and returns false if the node is not sufficiently connected. + +`endConsensus()` is more complex. It first clears stale peer status entries (peers that report the previous LCL as their current ledger), then calls `checkLastClosedLedger()` to determine whether the node agrees with the network on the last closed ledger. This comparison uses both the validation trie and a raw peer census. If the node's LCL disagrees with the network's preferred LCL, `switchLastClosedLedger()` is called — an abnormal "JUMP" path that resets the node's ledger pointer and drops the mode to `CONNECTED`. Assuming consensus on the LCL, `endConsensus()` promotes the operating mode toward `FULL` if the current ledger's close time is sufficiently recent, then chains into `beginConsensus()` for the next round. + +`consensusViewChange()` drops FULL or TRACKING nodes to CONNECTED when the consensus view changes, a signal that the current proposal set may be invalid. + +## Subscription Infrastructure + +`NetworkOPsImp` maintains several distinct subscription maps. The `mStreamMaps` array holds nine `SubMapType` slots indexed by `SubTypes` enum values: `sLedger`, `sManifests`, `sServer`, `sTransactions`, `sRTTransactions`, `sValidations`, `sPeerStatus`, `sConsensusPhase`, and `sBookChanges`. All operations on these maps are serialized by a single `mSubLock` mutex. + +Account-level subscriptions (`mSubAccount`, `mSubRTAccount`) map `AccountID → SubMapType`. The dual maps separate validated-transaction notifications from real-time (proposed) notifications, since some clients want only confirmed results while others want speculative feed. + +The account-history subscription system (`mSubAccountHistory`, `addAccountHistoryJob()`) is a more complex flow: on subscribe, a background `jtCLIENT_ACCT_HIST` job is posted that pages backward through the `RelationalDatabase` (currently SQLite only) to replay historical transactions in chronological order, while the forward live stream catches new events simultaneously. The `SubAccountHistoryInfoWeak` struct holds a `weak_ptr` to the `InfoSub` sink so that if the client disconnects mid-stream, the job detects the expired pointer and self-terminates cleanly. + +All publisher methods (`pubLedger`, `pubValidatedTransaction`, `pubValidation`, etc.) use the same idiom: iterate the relevant stream map, attempt to lock each `weak_ptr`, send to live subscribers, and erase expired entries inline. This lazy GC avoids a separate cleanup pass but means subscriber lifetime is only truly observed during publication. + +## `StateAccounting` and Metrics + +The inner `StateAccounting` class records cumulative microseconds and transition counts for each operating mode using an array of `Counters`. It captures the time of the last transition (`start_`) and records the first-ever entry into `FULL` mode as `initialSyncUs_`, giving operators a metric for cold-start sync time. The `StateAccounting::json()` method adds a live estimate of time-in-current-state by computing `steady_clock::now() - start_` and appending it to the current-mode counter before formatting the JSON. + +The `Stats` struct, populated by the `collect_metrics()` hook, maps these counters to `beast::insight::Gauge` gauges that flow into the metrics infrastructure (Prometheus, StatsD, etc.). This hook-based design means the metrics system pulls data only when it polls, rather than pushing on every state change. + +## `ServerFeeSummary` and Fee Change Detection + +`ServerFeeSummary` captures a snapshot of `loadFactorServer`, `loadBaseServer`, `baseFee`, and optional `TxQ::Metrics` escalation fields. `reportFeeChange()` compares the current summary against the cached `mLastFeeSummary` and, if anything changed, posts a `jtCLIENT_FEE_CHANGE` job that calls `pubServer()`. This ensures subscribers to the `server` stream receive fee updates promptly without constant polling, and avoids spurious notifications when fees are stable. + +## `getBookPage` and Order Book Queries + +`getBookPage()` iterates the ledger's offer directory sorted by quality (price ratio). For each offer it computes the effective available funds by querying `accountHolds()` once per offer owner (caching subsequent results in `umBalance`) and then applying the currency issuer's transfer fee. If the owner's balance covers the full offer, `taker_gets_funded` is omitted; if not, the funded amounts are annotated separately so clients can display partially-funded orders correctly. The `#ifndef USE_NEW_BOOK_PAGE` guard reveals there is a dead alternative implementation using an `OrderBookIterator`, noted to have poor performance, which has been compiled out but preserved for reference. + +## Construction via `make_NetworkOPs` + +The `make_NetworkOPs()` factory function returns a `std::unique_ptr` backed by `NetworkOPsImp`. The implementation exposes no public constructor, keeping the concrete class fully hidden from callers. This allows the subsystem to be replaced in tests by substituting a different `NetworkOPs` implementation without changing any call sites. \ No newline at end of file diff --git a/src/xrpld/app/misc/SHAMapStore.h.ai.json b/src/xrpld/app/misc/SHAMapStore.h.ai.json new file mode 100644 index 0000000000..becc2997d4 --- /dev/null +++ b/src/xrpld/app/misc/SHAMapStore.h.ai.json @@ -0,0 +1,47 @@ +{ + "args": [ + { + "lineno": 22, + "name": "ledger" + }, + { + "lineno": 34, + "name": "fetch_depth" + }, + { + "lineno": 37, + "name": "readThreads" + }, + { + "lineno": 41, + "name": "canDelete" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "SHAMapStore" + } + ], + "description": "Defines the SHAMapStore interface for managing the database, online delete thread, and related SQLite database for XRPL ledger storage and deletion. Provides methods for ledger closure, starting/stopping the store, managing deletion, and querying storage state.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/SHAMapStore.h", + "functions": [ + { + "args": [ + "app", + "scheduler", + "journal" + ], + "lineno": 74, + "name": "make_SHAMapStore" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/SHAMapStore.h.ai.md b/src/xrpld/app/misc/SHAMapStore.h.ai.md new file mode 100644 index 0000000000..5f1e1523ad --- /dev/null +++ b/src/xrpld/app/misc/SHAMapStore.h.ai.md @@ -0,0 +1,37 @@ +# `SHAMapStore.h` — Abstract Interface for Ledger Storage Lifecycle Management + +`SHAMapStore` is the pure abstract interface through which the rest of the XRPL node interacts with the ledger database management subsystem. Its two primary responsibilities are: (1) owning the creation and configuration of the `NodeStore::Database` backend that persists SHAMap node data on disk, and (2) driving the *online delete* process — the mechanism by which old ledger history is pruned from a running node without downtime or restart. + +The concrete implementation lives in `SHAMapStoreImp` (declared in `SHAMapStoreImp.h`, implemented in `SHAMapStoreImp.cpp`). This header exists as an abstraction boundary so that the rest of the application — `LedgerMaster`, fee infrastructure, and configuration consumers — can interact with storage management without coupling to the background thread, database rotation logic, or SQLite bookkeeping that the implementation encapsulates. Callers obtain a `SHAMapStore&` through `Application::getSHAMapStore()` and never need to know the concrete type. + +## The Online Delete Model + +The fundamental challenge online delete solves is: a validator node accumulates ledger history indefinitely; operators may not want to keep all of it; but reads and writes to the store must continue without pause while old data is discarded. The `SHAMapStore` interface is shaped entirely by this problem. + +The implementation uses a **dual-backend rotation** strategy. `makeNodeStore()` creates a `NodeStore::DatabaseRotatingImp` backed by two on-disk stores (a "writable" backend and an "archive" backend). As new ledgers are validated and old ones are no longer needed, the archive backend is cleared and the two backends are atomically swapped — the old writable store retires, and a fresh backend becomes current. This lets deletion proceed against the retiring backend without blocking reads or writes to the active one. + +## Interface Walk-Through + +`onLedgerClosed()` is the system's heartbeat. `LedgerMaster` calls it every time a ledger is validated, passing a `shared_ptr`. Inside `SHAMapStoreImp`, this posts the ledger to the background deletion thread via a condition variable — no deletion work happens on the calling thread. The interface makes this integration point explicit as a first-class method rather than a callback or observer slot, reflecting how central it is to the design. + +`makeNodeStore()` is called once at startup. It returns a `unique_ptr` and has the important side effect of initializing the internal `dbRotating_` pointer that the deletion loop uses. When `online_delete` is not configured, it creates a simple single-backend database; when online delete is enabled, it creates the `DatabaseRotatingImp` pair and records both backend paths in the SQLite state database for crash-safe recovery on restart. + +`clampFetchDepth()` is a subtle but important correctness guard. `LedgerMaster` uses it to limit how far back the node will attempt to fetch ledger data from peers. When online delete is active, fetching a ledger older than `deleteInterval_` is counterproductive — that ledger may be deleted before the fetch completes, or may already be deleted. This method returns `min(fetch_depth, deleteInterval_)` when deletion is active, and passes through unchanged otherwise. + +`setCanDelete()` and `getCanDelete()` govern the deletion boundary. The "can delete" ledger index is the highest ledger that the background thread is authorized to delete. In normal (non-advisory) mode this is implicitly managed by `deleteInterval_`. In **advisory delete** mode (`advisoryDelete()` returns `true`), an external administrative call must explicitly advance this boundary before any deletion occurs — useful when operators want fine-grained control or are coordinating a rolling upgrade across multiple validators. + +`getLastRotated()` returns the ledger sequence of the most recent completed rotation boundary (or a rotation in progress). This is persisted in the SQLite state database so it survives restarts. The distinction between `getLastRotated()` and `getCanDelete()` matters: `lastRotated` is where the backend last swapped; `canDelete` is the administrative ceiling up to which deletion is permitted. + +`minimumOnline()` provides the peer-to-peer acquisition lower bound. When online delete is active and about to clear history through sequence N, `minimumOnline_` is bumped to N+1. This prevents the node from advertising or trying to serve ledgers it is in the process of deleting. If online delete is not configured, or has never run, this returns an empty `optional` — meaning the node will try to maintain history as far back as it can. + +`rendezvous()` is a synchronization primitive for callers that must wait until the background thread is idle (i.e., `working_` is false). This is used during shutdown to avoid tearing down state while a rotation is mid-flight. + +`fdRequired()` reports the number of file descriptors the store needs, so the application can set `ulimit` appropriately before opening backends. The dual-backend rotation model uses more file descriptors than a single-backend configuration. + +## Factory Function + +`make_SHAMapStore()` takes an `Application&`, a `NodeStore::Scheduler&`, and a `beast::Journal`, following the same factory pattern used throughout XRPL's application layer (compare `make_AmendmentTable`, `make_NetworkOPs`). This keeps construction details — including the concrete subclass, configuration parsing, and validation of `online_delete` vs. `ledger_history` consistency — out of call sites, and allows the interface to evolve without changing consumers. + +## Design Rationale + +The decision to express this as a pure interface rather than a concrete class is worth noting. `SHAMapStoreImp` has a significant background thread and substantial mutable state. Putting an abstraction boundary here means that unit tests can substitute a no-op `SHAMapStore` without spawning deletion threads or requiring a real database, and that the threading model remains an implementation detail rather than part of the public contract. The interface surface is deliberately minimal: only the operations that external subsystems actually need are exposed. \ No newline at end of file diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.json b/src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.json new file mode 100644 index 0000000000..424bffa6db --- /dev/null +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.json @@ -0,0 +1,691 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "SHAMapStoreImp::SHAMapStoreImp", + "Config& config{app.config()}", + "Section& section{config.section(ConfigSection::nodeDatabase())}", + "Validation checks on section and its fields", + "state_db_.init(config, dbName_)" + ], + "entry_point": "SHAMapStoreImp::SHAMapStoreImp", + "purpose": "Constructs the SHAMapStoreImp object, validates configuration, sets up state DB and parameters for online deletion.", + "validation_points": [ + "section.empty()", + "boost::iequals(get(section, \"type\"), \"RocksDB\")", + "!section.exists(\"cache_mb\")", + "!section.exists(\"filter_bits\") && (config.NODE_SIZE >= 2)", + "get_if_exists(section, \"online_delete\", deleteInterval_)", + "deleteInterval_ < minInterval", + "config.LEDGER_HISTORY > deleteInterval_" + ] + }, + { + "call_chain": [ + "SHAMapStoreImp::SavedStateDB::init", + "initStateDB(sqlDb_, config, dbName)" + ], + "entry_point": "SHAMapStoreImp::SavedStateDB::init", + "purpose": "Initializes the SavedStateDB with the provided config and database name.", + "validation_points": [] + }, + { + "call_chain": [ + "SHAMapStoreImp::SavedStateDB::getCanDelete", + "xrpl::getCanDelete(sqlDb_)" + ], + "entry_point": "SHAMapStoreImp::SavedStateDB::getCanDelete", + "purpose": "Fetches the current canDelete ledger index from the state DB.", + "validation_points": [] + }, + { + "call_chain": [ + "SHAMapStoreImp::SavedStateDB::setCanDelete", + "xrpl::setCanDelete(sqlDb_, canDelete)" + ], + "entry_point": "SHAMapStoreImp::SavedStateDB::setCanDelete", + "purpose": "Sets the canDelete ledger index in the state DB.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "section (Config Section)", + "flow": [ + "config.section(ConfigSection::nodeDatabase())", + "section", + "Validation checks (empty, type, cache_mb, filter_bits, online_delete, etc.)", + "Used to set up NodeStore and deletion parameters" + ], + "origin": "config.section(ConfigSection::nodeDatabase())", + "transformations": [ + "Checked for emptiness", + "Type checked for 'RocksDB'", + "Defaults set for cache_mb and filter_bits if missing", + "online_delete and related fields extracted" + ], + "validated_at": "SHAMapStoreImp::SHAMapStoreImp" + }, + { + "field": "deleteInterval_", + "flow": [ + "get_if_exists(section, \"online_delete\", deleteInterval_)", + "deleteInterval_", + "Used in further validation (minInterval, LEDGER_HISTORY)", + "Passed to state_db_.init and dbPaths()" + ], + "origin": "get_if_exists(section, \"online_delete\", deleteInterval_)", + "transformations": [ + "Set from config if exists", + "Compared to minInterval and LEDGER_HISTORY" + ], + "validated_at": "SHAMapStoreImp::SHAMapStoreImp" + }, + { + "field": "cache_mb", + "flow": [ + "section.exists(\"cache_mb\")", + "If missing, set to config.getValueFor(SizedItem::hashNodeDBCache)", + "Used in NodeStore configuration" + ], + "origin": "section[\"cache_mb\"]", + "transformations": [ + "Defaulted if missing" + ], + "validated_at": "SHAMapStoreImp::SHAMapStoreImp" + }, + { + "field": "filter_bits", + "flow": [ + "section.exists(\"filter_bits\")", + "If missing and NODE_SIZE >= 2, set to '10'", + "Used in NodeStore configuration" + ], + "origin": "section[\"filter_bits\"]", + "transformations": [ + "Defaulted if missing and NODE_SIZE >= 2" + ], + "validated_at": "SHAMapStoreImp::SHAMapStoreImp" + }, + { + "field": "type", + "flow": [ + "get(section, \"type\")", + "Compared with 'RocksDB' (case-insensitive)", + "If RocksDB, triggers cache_mb and filter_bits defaults" + ], + "origin": "section[\"type\"]", + "transformations": [ + "Case-insensitive comparison" + ], + "validated_at": "SHAMapStoreImp::SHAMapStoreImp" + } + ], + "description": "Implements the SHAMapStoreImp class, which manages the online deletion and rotation of ledger data in the XRPL node database, including database initialization, backend rotation, cache management, and health checks.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ConfigSection::nodeDatabase() (config section)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (section.empty()) at SHAMapStoreImp constructor", + "issue_pattern": "Missing empty string validation for ConfigSection::nodeDatabase() (config section)", + "why_false_positive": "explicit check (section.empty()) validates ConfigSection::nodeDatabase() (config section) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "section['type']", + "empty", + "string", + "validation" + ], + "evidence": "boost::iequals(get(section, \"type\"), \"RocksDB\") at SHAMapStoreImp constructor", + "issue_pattern": "Missing empty string validation for section['type']", + "why_false_positive": "boost::iequals(get(section, \"type\"), \"RocksDB\") validates section['type'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "section['cache_mb']", + "empty", + "string", + "validation" + ], + "evidence": "!section.exists(\"cache_mb\") at SHAMapStoreImp constructor", + "issue_pattern": "Missing empty string validation for section['cache_mb']", + "why_false_positive": "!section.exists(\"cache_mb\") validates section['cache_mb'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "section['filter_bits']", + "empty", + "string", + "validation" + ], + "evidence": "!section.exists(\"filter_bits\") && (config.NODE_SIZE >= 2) at SHAMapStoreImp constructor", + "issue_pattern": "Missing empty string validation for section['filter_bits']", + "why_false_positive": "!section.exists(\"filter_bits\") && (config.NODE_SIZE >= 2) validates section['filter_bits'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "section['online_delete']", + "empty", + "string", + "validation" + ], + "evidence": "get_if_exists(section, \"online_delete\", deleteInterval_) at SHAMapStoreImp constructor", + "issue_pattern": "Missing empty string validation for section['online_delete']", + "why_false_positive": "get_if_exists(section, \"online_delete\", deleteInterval_) validates section['online_delete'] for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "section['online_delete']", + "type", + "validation", + "check" + ], + "evidence": "get_if_exists(section, \"online_delete\", deleteInterval_) at SHAMapStoreImp constructor", + "issue_pattern": "Missing type validation for section['online_delete']", + "why_false_positive": "get_if_exists(section, \"online_delete\", deleteInterval_) validates section['online_delete'] type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "section['delete_batch']", + "empty", + "string", + "validation" + ], + "evidence": "get_if_exists(section, \"delete_batch\", deleteBatch_) at SHAMapStoreImp constructor", + "issue_pattern": "Missing empty string validation for section['delete_batch']", + "why_false_positive": "get_if_exists(section, \"delete_batch\", deleteBatch_) validates section['delete_batch'] for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "section['delete_batch']", + "type", + "validation", + "check" + ], + "evidence": "get_if_exists(section, \"delete_batch\", deleteBatch_) at SHAMapStoreImp constructor", + "issue_pattern": "Missing type validation for section['delete_batch']", + "why_false_positive": "get_if_exists(section, \"delete_batch\", deleteBatch_) validates section['delete_batch'] type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "section['back_off_milliseconds'] / section['backOff']", + "empty", + "string", + "validation" + ], + "evidence": "get_if_exists(section, ..., temp) at SHAMapStoreImp constructor", + "issue_pattern": "Missing empty string validation for section['back_off_milliseconds'] / section['backOff']", + "why_false_positive": "get_if_exists(section, ..., temp) validates section['back_off_milliseconds'] / section['backOff'] for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "section['back_off_milliseconds'] / section['backOff']", + "type", + "validation", + "check" + ], + "evidence": "get_if_exists(section, ..., temp) at SHAMapStoreImp constructor", + "issue_pattern": "Missing type validation for section['back_off_milliseconds'] / section['backOff']", + "why_false_positive": "get_if_exists(section, ..., temp) validates section['back_off_milliseconds'] / section['backOff'] type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/SHAMapStoreImp.cpp", + "functions": [ + { + "args": [ + "BasicConfig const& config", + "std::string const& dbName" + ], + "lineno": 11, + "name": "SHAMapStoreImp::SavedStateDB::init" + }, + { + "args": [], + "lineno": 17, + "name": "SHAMapStoreImp::SavedStateDB::getCanDelete" + }, + { + "args": [ + "LedgerIndex canDelete" + ], + "lineno": 23, + "name": "SHAMapStoreImp::SavedStateDB::setCanDelete" + }, + { + "args": [], + "lineno": 29, + "name": "SHAMapStoreImp::SavedStateDB::getState" + }, + { + "args": [ + "SavedState const& state" + ], + "lineno": 35, + "name": "SHAMapStoreImp::SavedStateDB::setState" + }, + { + "args": [ + "LedgerIndex seq" + ], + "lineno": 41, + "name": "SHAMapStoreImp::SavedStateDB::setLastRotated" + }, + { + "args": [ + "Application& app", + "NodeStore::Scheduler& scheduler", + "beast::Journal journal" + ], + "lineno": 47, + "name": "SHAMapStoreImp::SHAMapStoreImp" + }, + { + "args": [ + "int readThreads" + ], + "lineno": 84, + "name": "SHAMapStoreImp::makeNodeStore" + }, + { + "args": [ + "std::shared_ptr const& ledger" + ], + "lineno": 112, + "name": "SHAMapStoreImp::onLedgerClosed" + }, + { + "args": [], + "lineno": 120, + "name": "SHAMapStoreImp::rendezvous" + }, + { + "args": [], + "lineno": 127, + "name": "SHAMapStoreImp::fdRequired" + }, + { + "args": [ + "std::uint64_t& nodeCount", + "SHAMapTreeNode const& node" + ], + "lineno": 131, + "name": "SHAMapStoreImp::copyNode" + }, + { + "args": [], + "lineno": 141, + "name": "SHAMapStoreImp::run" + }, + { + "args": [], + "lineno": 210, + "name": "SHAMapStoreImp::dbPaths" + }, + { + "args": [ + "std::string path" + ], + "lineno": 271, + "name": "SHAMapStoreImp::makeBackendRotating" + }, + { + "args": [ + "LedgerIndex lastRotated", + "std::string const& TableName", + "std::function()> const& getMinSeq", + "std::function const& deleteBeforeSeq" + ], + "lineno": 288, + "name": "SHAMapStoreImp::clearSql" + }, + { + "args": [ + "LedgerIndex validatedSeq" + ], + "lineno": 320, + "name": "SHAMapStoreImp::clearCaches" + }, + { + "args": [], + "lineno": 327, + "name": "SHAMapStoreImp::freshenCaches" + }, + { + "args": [ + "LedgerIndex lastRotated" + ], + "lineno": 334, + "name": "SHAMapStoreImp::clearPrior" + }, + { + "args": [], + "lineno": 366, + "name": "SHAMapStoreImp::healthWait" + }, + { + "args": [], + "lineno": 386, + "name": "SHAMapStoreImp::stop" + }, + { + "args": [], + "lineno": 399, + "name": "SHAMapStoreImp::minimumOnline" + }, + { + "args": [ + "Application& app", + "NodeStore::Scheduler& scheduler", + "beast::Journal journal" + ], + "lineno": 412, + "name": "make_SHAMapStore" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this code is likely found in integration and configuration tests for SHAMapStore, online_delete, and NodeStore configuration. Look for test files such as 'SHAMapStore_test.cpp', 'OnlineDelete_test.cpp', or configuration validation tests in the 'test' or 'unittest' directories. Gaps: There may be limited coverage for negative cases (e.g., missing config sections, invalid values for online_delete, or type mismatches). Exception paths (Throw) should be explicitly tested for robust coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation (no external validation framework)", + "validation_layer": "business_logic (constructor-level config validation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw)", + "field": "ConfigSection::nodeDatabase() (config section)", + "location": "SHAMapStoreImp constructor", + "validated_by": "explicit check (section.empty())", + "validates": [ + "Presence of [nodeDatabase] section in config file" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (conditional logic only)", + "field": "section['type']", + "location": "SHAMapStoreImp constructor", + "validated_by": "boost::iequals(get(section, \"type\"), \"RocksDB\")", + "validates": [ + "Checks if backend type is RocksDB to apply further defaults" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (sets default if missing)", + "field": "section['cache_mb']", + "location": "SHAMapStoreImp constructor", + "validated_by": "!section.exists(\"cache_mb\")", + "validates": [ + "If missing, sets default cache_mb value" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (sets default if missing)", + "field": "section['filter_bits']", + "location": "SHAMapStoreImp constructor", + "validated_by": "!section.exists(\"filter_bits\") && (config.NODE_SIZE >= 2)", + "validates": [ + "If missing and NODE_SIZE >= 2, sets default filter_bits" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (optional assignment)", + "field": "section['online_delete']", + "location": "SHAMapStoreImp constructor", + "validated_by": "get_if_exists(section, \"online_delete\", deleteInterval_)", + "validates": [ + "Assigns online_delete to deleteInterval_ if present" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (optional assignment)", + "field": "section['delete_batch']", + "location": "SHAMapStoreImp constructor", + "validated_by": "get_if_exists(section, \"delete_batch\", deleteBatch_)", + "validates": [ + "Assigns delete_batch to deleteBatch_ if present" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (optional assignment)", + "field": "section['back_off_milliseconds'] / section['backOff']", + "location": "SHAMapStoreImp constructor", + "validated_by": "get_if_exists(section, ..., temp)", + "validates": [ + "Assigns back_off_milliseconds or backOff to temp if present" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.md b/src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.md new file mode 100644 index 0000000000..5b0f536fb7 --- /dev/null +++ b/src/xrpld/app/misc/SHAMapStoreImp.cpp.ai.md @@ -0,0 +1,60 @@ +# `SHAMapStoreImp.cpp` — Online Ledger Deletion and Backend Rotation + +## Purpose and Role + +`SHAMapStoreImp` is the concrete implementation of the `SHAMapStore` interface and owns the entire lifecycle of the XRPL node's persistent ledger object store. Its central responsibility is **online deletion**: continuously and safely purging old ledger data from a running node without stopping it, ensuring that disk usage stays bounded over the lifetime of a validator or full-history node. + +The file implements a background thread that watches for newly validated ledgers and — when enough history has accumulated — rotates the underlying node store backends, deletes old SQL rows, and invalidates stale caches, all while the node continues to process new ledgers. + +## Architecture: Two-Backend Rotation + +The core design insight is that the node store uses **two physical backends** simultaneously: a `writableBackend_` that accepts new writes, and an `archiveBackend_` that holds older data still potentially needed for reads. During a rotation: + +1. The current writable backend becomes the new archive. +2. The old archive is scheduled for deletion. +3. A fresh empty backend becomes the new writable. + +This swap is performed atomically inside `DatabaseRotatingImp::rotate()`, which holds a mutex across the pointer reassignments and only calls the state-persistence callback after the swap, ensuring the saved state always reflects a valid configuration. The result is that the node never needs to stop to reclaim disk space, and reads transparently fall through to either backend. + +## `SavedStateDB`: Crash-Safe State Persistence + +The inner class `SavedStateDB` wraps a small SQLite database (named `"state"`) that records three things across process restarts: the paths of the writable and archive backends on disk, and the ledger sequence at which the last rotation occurred. Every operation acquires a `std::mutex` lock, making this safe to call from both the deletion thread and any external RPC handler. + +The reason for a dedicated SQLite record rather than just reconstructing state from the filesystem is crash safety. If the process dies mid-rotation, the stored names tell `dbPaths()` exactly which directories are valid. `dbPaths()` runs during construction and cross-checks the recorded paths against what actually exists in the filesystem directory. If only one backend is present when two are expected — or one is missing entirely — the constructor throws `std::runtime_error` with a detailed recovery message rather than silently starting in an inconsistent state. It also handles the case where the operator has moved their database to a new path, updating the parent-directory component while preserving the backend's filename. + +## The Deletion Thread: `run()` + +`start()` spawns a single background thread running `run()` if `deleteInterval_` is non-zero. The thread waits on a condition variable for notifications from `onLedgerClosed()`, which is called by `LedgerMaster` every time a ledger validates. The signal passes the validated ledger through `newLedger_` protected by a mutex, and the thread extracts it at wakeup. + +The rotation condition (`readyToRotate`) requires two things: the validated sequence must be at least `deleteInterval_` ledgers past the last rotation point, and `canDelete_` must be at least `lastRotated - 1`. The second condition enables **advisory delete** — when configured, an external RPC call must explicitly advance `canDelete_` to authorize any deletion, giving operators manual control. Without advisory delete, `canDelete_` starts at `std::numeric_limits::max()`, meaning the interval alone gates rotation. + +When `readyToRotate` is true, the thread executes a multi-phase sequence: + +1. **`clearPrior()`** sets `minimumOnline_` to block peer-to-peer fetching of ledgers about to be deleted, clears in-memory ledger objects, then deletes rows from the `Ledgers`, `Transactions`, and `AccountTransactions` SQLite tables via `clearSql()`. +2. **Node copy**: The validated ledger's state SHAMap is snapshot-traversed via `visitNodes`, and each node is re-fetched through `copyNode()` with `duplicate=true`. This re-inserts live objects into the writable backend before the old archive disappears. +3. **`freshenCaches()`**: The tree-node cache and transaction cache hold keys whose backing storage is in the soon-to-be-discarded archive. By re-fetching each key with `duplicate=true`, the objects are migrated into the writable backend. +4. A new empty backend is created via `makeBackendRotating()`. +5. **`clearCaches()`** flushes `LedgerMaster`'s prior-ledger cache and resets the `FullBelowCache` — critically, its generation counter must be bumped to prevent stale "full below" markers from causing incorrect assumptions about SHAMap completeness after the rotation. +6. **`dbRotating_->rotate()`** performs the atomic backend swap and, inside the callback, writes the new `{writableDb, archiveDb, lastRotated}` triple to `SavedStateDB`. + +Health checks gate every major phase: `healthWait()` is called before and after each step and blocks the deletion thread if the node is not in `FULL` operating mode or if the last validated ledger is older than `ageThreshold_` (default 60 seconds). The thread sleeps for `recoveryWaitTime_` (default 5 seconds) between re-checks, logging a warning on each iteration. If `stop_` is set during any wait, `healthWait()` returns `stopping` and the caller unwinds. The inner `copyNode()` and `freshenCache()` loops also call `healthWait()` every `checkHealthInterval_` (1000) nodes, so even a copy of a large ledger can be interrupted cleanly. + +## `clearSql()`: Batched, Interruptible SQL Cleanup + +Deleting large ranges of rows in a single SQL transaction would lock the database and starve concurrent readers (e.g., RPC queries). `clearSql()` instead deletes in configurable batches (`deleteBatch_`, default 100 rows) and sleeps for `backOff_` milliseconds (default 100ms) between batches. It also checks `healthWait()` between batches, allowing the operation to be aborted safely if the node loses sync. The same pattern applies to all three tables (`Ledgers`, `Transactions`, `AccountTransactions`), and transaction tables are skipped entirely if `useTxTables()` is false. + +## `rendezvous()`: External Synchronization + +The `rendezvous()` method allows external callers to block until the deletion thread has become idle. The `working_` atomic flag is set to `true` when `onLedgerClosed` delivers a new ledger, and the thread clears it and notifies `rendezvous_` at the top of each idle loop. This is used during graceful shutdown and in tests that need to assert on post-rotation state. + +## `makeBackendRotating()` and Backend Naming + +New backends are created under the configured `path` directory with a `rippledb.XXXX` name, where `XXXX` is a random four-character suffix from `boost::filesystem::unique_path`. When a specific path is provided (on startup, from saved state), that path is used directly. After construction, `dbPaths()` removes any directories in the node store path that match the `rippledb` prefix but are neither the active writable nor archive backend — cleaning up after crash-interrupted rotations from previous runs. + +## RocksDB Defaults + +The constructor silently injects `cache_mb` and `filter_bits` defaults when the backend type is RocksDB and these keys are absent. The `filter_bits = 10` Bloom filter default is only applied for `NODE_SIZE >= 2` (medium and large node configurations), where the memory cost is justified by read-performance gains on large datasets. + +## Concurrency Model + +The deletion thread, the main application thread (which calls `onLedgerClosed`), and potential RPC threads (which call `setCanDelete` and `minimumOnline`) all share state. The design uses a layered concurrency strategy: `mutex_` + `cond_` for ledger delivery and `working_`/`stop_` signaling; `SavedStateDB::mutex_` for SQLite access; `DatabaseRotatingImp::mutex_` for backend pointer swaps; and `std::atomic` for `minimumOnline_` and `canDelete_`, which are read on hot paths without acquiring a lock. \ No newline at end of file diff --git a/src/xrpld/app/misc/SHAMapStoreImp.h.ai.json b/src/xrpld/app/misc/SHAMapStoreImp.h.ai.json new file mode 100644 index 0000000000..650420846a --- /dev/null +++ b/src/xrpld/app/misc/SHAMapStoreImp.h.ai.json @@ -0,0 +1,216 @@ +{ + "args": [ + { + "lineno": 56, + "name": "app" + }, + { + "lineno": 56, + "name": "scheduler" + }, + { + "lineno": 56, + "name": "journal" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "NodeStore::Scheduler& scheduler", + "beast::Journal journal" + ], + "lineno": 15, + "name": "SHAMapStoreImp" + }, + { + "args": [], + "lineno": 17, + "name": "SavedStateDB" + } + ], + "description": "Implements the SHAMapStoreImp class, which manages the online deletion of old ledgers and related state in the XRPL node, including database rotation, health checks, and cache management.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/SHAMapStoreImp.h", + "functions": [ + { + "args": [ + "config", + "dbName" + ], + "lineno": 27, + "name": "init" + }, + { + "args": [], + "lineno": 30, + "name": "getCanDelete" + }, + { + "args": [ + "canDelete" + ], + "lineno": 31, + "name": "setCanDelete" + }, + { + "args": [], + "lineno": 32, + "name": "getState" + }, + { + "args": [ + "state" + ], + "lineno": 33, + "name": "setState" + }, + { + "args": [ + "seq" + ], + "lineno": 34, + "name": "setLastRotated" + }, + { + "args": [ + "fetch_depth" + ], + "lineno": 74, + "name": "clampFetchDepth" + }, + { + "args": [ + "readThreads" + ], + "lineno": 79, + "name": "makeNodeStore" + }, + { + "args": [ + "seq" + ], + "lineno": 82, + "name": "setCanDelete" + }, + { + "args": [], + "lineno": 89, + "name": "advisoryDelete" + }, + { + "args": [], + "lineno": 94, + "name": "getLastRotated" + }, + { + "args": [], + "lineno": 100, + "name": "getCanDelete" + }, + { + "args": [ + "ledger" + ], + "lineno": 106, + "name": "onLedgerClosed" + }, + { + "args": [], + "lineno": 108, + "name": "rendezvous" + }, + { + "args": [], + "lineno": 109, + "name": "fdRequired" + }, + { + "args": [], + "lineno": 111, + "name": "minimumOnline" + }, + { + "args": [ + "nodeCount", + "node" + ], + "lineno": 117, + "name": "copyNode" + }, + { + "args": [], + "lineno": 118, + "name": "run" + }, + { + "args": [], + "lineno": 119, + "name": "dbPaths" + }, + { + "args": [ + "path" + ], + "lineno": 121, + "name": "makeBackendRotating" + }, + { + "args": [ + "cache" + ], + "lineno": 123, + "name": "freshenCache" + }, + { + "args": [ + "lastRotated", + "TableName", + "getMinSeq", + "deleteBeforeSeq" + ], + "lineno": 137, + "name": "clearSql" + }, + { + "args": [ + "validatedSeq" + ], + "lineno": 142, + "name": "clearCaches" + }, + { + "args": [], + "lineno": 143, + "name": "freshenCaches" + }, + { + "args": [ + "lastRotated" + ], + "lineno": 144, + "name": "clearPrior" + }, + { + "args": [], + "lineno": 154, + "name": "healthWait" + }, + { + "args": [], + "lineno": 160, + "name": "start" + }, + { + "args": [], + "lineno": 166, + "name": "stop" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/SHAMapStoreImp.h.ai.md b/src/xrpld/app/misc/SHAMapStoreImp.h.ai.md new file mode 100644 index 0000000000..9ae783e62f --- /dev/null +++ b/src/xrpld/app/misc/SHAMapStoreImp.h.ai.md @@ -0,0 +1,49 @@ +# SHAMapStoreImp.h — Online Delete and Database Rotation Engine + +`SHAMapStoreImp` is the concrete implementation of the `SHAMapStore` interface and serves as the engine behind XRPL's *online delete* feature — the ability to discard ledger history older than a configured threshold while the node continues to operate without interruption. Its header defines the full class layout, two concurrency primitives, a nested SQLite state tracker, and the tunable parameters that govern the deletion lifecycle. + +## The Core Problem: Unbounded Ledger Growth + +Without intervention, a full-history XRPL node accumulates data indefinitely. `SHAMapStoreImp` solves this by maintaining exactly two on-disk `NodeStore::Backend` instances — a *writable* backend for new writes and an *archive* backend for reads — via `DatabaseRotating`. When enough new ledgers have accumulated, the archive backend (which holds the oldest data) is atomically replaced with a fresh empty backend, and the old archive is discarded. This is the "rotation." + +## SavedStateDB: Crash-Safe State Across Restarts + +The inner class `SavedStateDB` wraps a `soci::session` (a SQLite connection) and a `std::mutex`. It records which backend paths were active at shutdown (`writableDb`, `archiveDb`) and the last rotated ledger index. This allows the node to resume correctly after a crash: `dbPaths()` reads this state, validates that both backend directories still exist on disk, removes any stale `rippledb.*` directories left by a previously interrupted rotation, and throws a descriptive `std::runtime_error` if the stored paths are inconsistent — which protects against serving corrupt or incomplete data. + +The class defaults to a null journal so it compiles without error when online delete is disabled; `init()` is only called when `deleteInterval_` is non-zero. + +## Configuration Parameters + +All tunable values are read from the `[node_db]` section during construction. `deleteInterval_` sets the minimum number of ledgers to retain (minimum 256 in network mode, 8 in standalone). The constructor enforces `online_delete >= ledger_history` to ensure the node never attempts to retain more SQL history than the NodeStore retains. Additional knobs include: + +- `deleteBatch_` (default 100): how many rows to delete per SQL batch to avoid long table locks +- `backOff_` (default 100 ms): pause between SQL batches to yield to other readers +- `ageThreshold_` (default 60 s): minimum age for a ledger before it qualifies for deletion +- `recoveryWaitTime_` (default 5 s): how long to sleep when the node is out-of-sync during a `healthWait()` call + +## The Deletion Thread and `run()` + +`start()` spawns a dedicated thread only when `deleteInterval_` is set. The thread body in `run()` waits on `cond_` for `onLedgerClosed()` to deliver a newly validated ledger, then evaluates a single condition: `validatedSeq >= lastRotated + deleteInterval_ && canDelete_ >= lastRotated - 1`. If true, it executes the rotation in several ordered phases: + +1. **`clearPrior(lastRotated)`** — removes all SQL records (Transactions, AccountTransactions, etc.) for ledger sequences below `lastRotated`, using `clearSql()` which iterates in batches, pausing briefly after each to avoid lock contention. +2. **Snapshot copy** — calls `visitNodes()` on the current validated ledger's state map, invoking `copyNode()` for each node. `copyNode()` calls `fetchNodeObject()` on `dbRotating_`, which promotes the node from the archive backend to the writable backend if it only exists in the archive. Health checks are interleaved every `checkHealthInterval_` (1000) nodes. +3. **`freshenCaches()`** — walks the in-memory ledger and transaction caches, triggering the same promote-on-read for any cached keys, ensuring hot data survives the upcoming rotation. +4. **`makeBackendRotating()`** — creates a fresh, empty backend with a uniquely generated path (`rippledb.XXXX`). +5. **`clearCaches(validatedSeq)`** — drops in-memory caches to prevent stale references. +6. **`dbRotating_->rotate()`** — atomically swaps the new empty backend into the writable slot and demotes the current writable to archive. The callback passed to `rotate()` persists the new state to `SavedStateDB` *inside the lock*, guaranteeing that if the node crashes during rotation the state file always reflects reality. + +## Health Gating and `healthWait()` + +Every expensive operation in `run()` is bracketed by `healthWait()`, which blocks until the server is either fully synced or stopping. This prevents online delete from proceeding while the node is catching up to the network, which could cause it to discard data it still needs. The return type `HealthResult` is an enum with values `keepGoing` or `stopping`, and callers use `[[nodiscard]]` semantics to force handling at every call site. + +## Advisory Delete and Operator Control + +When `advisoryDelete_` is enabled, rotation only proceeds if `canDelete_` has been explicitly advanced by an operator via the `can_delete` RPC command. This decouples the automated scheduling from actual deletion, useful for operators who want predictable deletion windows. `setCanDelete()` updates both the in-memory `canDelete_` atomic (used in the hot rotation check) and the persistent `SavedStateDB` record. In non-advisory mode `canDelete_` starts at `std::numeric_limits::max()`, effectively always permitting deletion. + +## Synchronization Design + +The class uses two condition variables against a single mutex. `cond_` gates the deletion thread waiting for a new closed ledger. `rendezvous_` allows external callers (e.g., during ordered shutdown) to block via `rendezvous()` until the deletion thread has signaled `working_ = false`. The separation avoids a subtle issue: if a single condition variable were used, a caller waiting for "done" could consume the "new ledger" notification intended for the thread. The `working_` atomic provides a fast non-locking path in `rendezvous()` to skip the wait entirely when the thread is already idle. + +## `clampFetchDepth()` and `minimumOnline()` + +`clampFetchDepth()` ensures the node never fetches more history from peers than it plans to retain — if `deleteInterval_` is 1000, fetching depth beyond 1000 is wasteful. `minimumOnline()` returns the lower bound of ledger history currently stored and is used by the peer-to-peer layer to decline requests for ledgers that have already been deleted, preventing the node from advertising data it can no longer serve. \ No newline at end of file diff --git a/src/xrpld/app/misc/Transaction.h.ai.json b/src/xrpld/app/misc/Transaction.h.ai.json new file mode 100644 index 0000000000..1fe42b13ef --- /dev/null +++ b/src/xrpld/app/misc/Transaction.h.ai.json @@ -0,0 +1,380 @@ +{ + "args": [ + { + "lineno": 36, + "name": "std::shared_ptr const&" + }, + { + "lineno": 36, + "name": "std::string&" + }, + { + "lineno": 36, + "name": "Application&" + }, + { + "lineno": 41, + "name": "boost::optional const&" + }, + { + "lineno": 41, + "name": "boost::optional const&" + }, + { + "lineno": 41, + "name": "Blob const&" + }, + { + "lineno": 82, + "name": "TER terResult" + }, + { + "lineno": 87, + "name": "TransStatus status" + }, + { + "lineno": 87, + "name": "std::uint32_t ledgerSeq" + }, + { + "lineno": 87, + "name": "std::optional transactionSeq" + }, + { + "lineno": 87, + "name": "std::optional networkID" + }, + { + "lineno": 94, + "name": "TransStatus status" + }, + { + "lineno": 99, + "name": "LedgerIndex ledger" + }, + { + "lineno": 194, + "name": "LedgerIndex li" + }, + { + "lineno": 195, + "name": "XRPAmount fee" + }, + { + "lineno": 196, + "name": "std::uint32_t accSeqNext" + }, + { + "lineno": 197, + "name": "std::uint32_t accSeqAvail" + }, + { + "lineno": 213, + "name": "LedgerIndex validatedLedger" + }, + { + "lineno": 214, + "name": "XRPAmount fee" + }, + { + "lineno": 215, + "name": "std::uint32_t accountSeq" + }, + { + "lineno": 216, + "name": "std::uint32_t availableSeq" + }, + { + "lineno": 221, + "name": "JsonOptions options" + }, + { + "lineno": 221, + "name": "bool binary" + }, + { + "lineno": 266, + "name": "uint256 const& id" + }, + { + "lineno": 266, + "name": "Application& app" + }, + { + "lineno": 269, + "name": "error_code_i& ec" + }, + { + "lineno": 273, + "name": "ClosedInterval const& range" + }, + { + "lineno": 280, + "name": "std::optional> const& range" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const&", + "std::string&", + "Application&" + ], + "lineno": 35, + "name": "Transaction" + }, + { + "args": [], + "lineno": 132, + "name": "SubmitResult" + }, + { + "args": [ + "LedgerIndex li", + "XRPAmount fee", + "std::uint32_t accSeqNext", + "std::uint32_t accSeqAvail" + ], + "lineno": 192, + "name": "CurrentLedgerState" + }, + { + "args": [], + "lineno": 231, + "name": "Locator" + } + ], + "description": "Defines the Transaction class for constructing, examining, and managing the state of XRPL transactions, including status, results, ledger state, and methods for loading and locating transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/Transaction.h", + "functions": [ + { + "args": [ + "std::shared_ptr const&", + "std::string&", + "Application&" + ], + "lineno": 36, + "name": "Transaction" + }, + { + "args": [ + "boost::optional const&", + "boost::optional const&", + "Blob const&", + "Application&" + ], + "lineno": 41, + "name": "transactionFromSQL" + }, + { + "args": [ + "boost::optional const&" + ], + "lineno": 48, + "name": "sqlTransactionStatus" + }, + { + "args": [], + "lineno": 52, + "name": "getSTransaction" + }, + { + "args": [], + "lineno": 57, + "name": "getID" + }, + { + "args": [], + "lineno": 62, + "name": "getLedger" + }, + { + "args": [], + "lineno": 67, + "name": "isValidated" + }, + { + "args": [], + "lineno": 72, + "name": "getStatus" + }, + { + "args": [], + "lineno": 77, + "name": "getResult" + }, + { + "args": [ + "TER terResult" + ], + "lineno": 82, + "name": "setResult" + }, + { + "args": [ + "TransStatus status", + "std::uint32_t ledgerSeq", + "std::optional transactionSeq", + "std::optional networkID" + ], + "lineno": 87, + "name": "setStatus" + }, + { + "args": [ + "TransStatus status" + ], + "lineno": 94, + "name": "setStatus" + }, + { + "args": [ + "LedgerIndex ledger" + ], + "lineno": 99, + "name": "setLedger" + }, + { + "args": [], + "lineno": 106, + "name": "setApplying" + }, + { + "args": [], + "lineno": 115, + "name": "getApplying" + }, + { + "args": [], + "lineno": 124, + "name": "clearApplying" + }, + { + "args": [], + "lineno": 134, + "name": "clear" + }, + { + "args": [], + "lineno": 142, + "name": "any" + }, + { + "args": [], + "lineno": 157, + "name": "getSubmitResult" + }, + { + "args": [], + "lineno": 163, + "name": "clearSubmitResult" + }, + { + "args": [], + "lineno": 169, + "name": "setApplied" + }, + { + "args": [], + "lineno": 175, + "name": "setQueued" + }, + { + "args": [], + "lineno": 181, + "name": "setBroadcast" + }, + { + "args": [], + "lineno": 187, + "name": "setKept" + }, + { + "args": [], + "lineno": 202, + "name": "getCurrentLedgerState" + }, + { + "args": [ + "LedgerIndex validatedLedger", + "XRPAmount fee", + "std::uint32_t accountSeq", + "std::uint32_t availableSeq" + ], + "lineno": 210, + "name": "setCurrentLedgerState" + }, + { + "args": [ + "JsonOptions options", + "bool binary" + ], + "lineno": 221, + "name": "getJson" + }, + { + "args": [], + "lineno": 236, + "name": "isFound" + }, + { + "args": [], + "lineno": 244, + "name": "getNodestoreHash" + }, + { + "args": [], + "lineno": 251, + "name": "getLedgerSequence" + }, + { + "args": [], + "lineno": 258, + "name": "getLedgerRangeSearched" + }, + { + "args": [ + "uint256 const& id", + "Application& app" + ], + "lineno": 266, + "name": "locate" + }, + { + "args": [ + "uint256 const& id", + "Application& app", + "error_code_i& ec" + ], + "lineno": 269, + "name": "load" + }, + { + "args": [ + "uint256 const& id", + "Application& app", + "ClosedInterval const& range", + "error_code_i& ec" + ], + "lineno": 273, + "name": "load" + }, + { + "args": [ + "uint256 const& id", + "Application& app", + "std::optional> const& range", + "error_code_i& ec" + ], + "lineno": 280, + "name": "load" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/Transaction.h.ai.md b/src/xrpld/app/misc/Transaction.h.ai.md new file mode 100644 index 0000000000..55663aceb2 --- /dev/null +++ b/src/xrpld/app/misc/Transaction.h.ai.md @@ -0,0 +1,39 @@ +# `Transaction.h` — Application-Layer Transaction Wrapper + +`Transaction.h` defines the `Transaction` class, which wraps an immutable `STTx` (the serialized on-wire transaction object) with the mutable lifecycle state the application layer needs to track a transaction from initial receipt through final commitment in a validated ledger. The distinction matters: `STTx` is a pure protocol type concerned only with encoding and signature; `Transaction` is the application's working object, understanding where the transaction sits in the submission pipeline and what outcomes have been recorded about it. + +## The `TransStatus` State Machine + +The `TransStatus` enum documents the nine states a transaction can occupy from the application's perspective. `NEW` means recently received; `INVALID` signals rejection (bad signature, insufficient balance); `INCLUDED` means it entered the current open ledger; `COMMITTED` means it is confirmed in a validated ledger. The transitions between these states are driven by `NetworkOPsImp` as batches are applied and ledgers close. `HELD` covers the case where the transaction is not yet valid — typically because its sequence number is ahead of the account's current sequence — and `OBSOLETE` handles the case where another transaction rendered this one unnecessary. + +`isValidated()` encodes the convention that `mLedgerIndex == 0` means the transaction has not yet been committed to any validated ledger. This sentinel value is set in the constructor and cleared only when `setStatus()` records a concrete ledger sequence. + +## The `mApplying` Flag and Intentional Lock Reuse + +`mApplying` is a raw `bool` used to prevent a transaction from being enqueued into more than one batch simultaneously. Rather than protecting it with its own mutex, the class explicitly delegates locking to `NetworkOPsImp`'s own lock — every callsite in `NetworkOPs.cpp` already holds that lock when calling `setApplying()`, `getApplying()`, or `clearApplying()`. + +The comment in the private section documents why this relaxed approach is acceptable: if a race somehow reads a stale `false`, the transaction gets re-submitted and collects `tefALREADY` or `tefPAST_SEQ`, which are harmless. If a stale `true` is read, the transaction is simply skipped for one cycle, which is equally benign. This is a deliberate performance trade-off: avoiding a second mutex in a hot path, with thoroughly documented bounded consequences if the assumption breaks. + +## `SubmitResult` — Multi-Dimensional Submission Outcome + +`SubmitResult` is a small four-flag struct distinguishing the independent outcomes that can follow a `submit` RPC call. A transaction can simultaneously be `applied` (tentatively applied to the open ledger), `broadcast` (sent to peer nodes), `queued` (placed in the transaction queue), and `kept` (placed in the local transaction set for retry). These outcomes are not mutually exclusive. The `any()` helper reduces the struct to the single `accepted` field the client sees in the JSON response. The submit RPC handler in `Submit.cpp` reads all four flags and emits them individually, giving clients a precise diagnostic of what happened to their transaction. + +## `CurrentLedgerState` — Snapshot for Client Diagnostics + +`CurrentLedgerState` captures a point-in-time view of the ledger as seen when the transaction was processed: the last validated ledger index, the minimum fee required to enter the open ledger, and two sequence numbers for the submitting account (`accountSeqNext` — the next sequence expected from the account, and `accountSeqAvail` — the next sequence the transaction queue will accept). This snapshot is written by `NetworkOPsImp` during transaction processing and read by the submit RPC handler to populate `account_sequence_next`, `account_sequence_available`, `open_ledger_cost`, and `validated_ledger_index` in the response. The field is `std::optional` because it is only meaningful after processing; a freshly constructed `Transaction` carries none of it. + +## `Locator` and the `load()` Family + +`Locator` is a type-safe discriminated union over two alternatives using `std::variant`: either a `std::pair` (a nodestore hash and ledger sequence identifying an exact match), or a `ClosedInterval` recording the ledger range that was searched and came up empty. Callers must check `isFound()` before calling either `getNodestoreHash()` / `getLedgerSequence()` or `getLedgerRangeSearched()`; calling the wrong getter throws via `std::get`. + +The three public `load()` overloads — one without a range, one with a concrete `ClosedInterval`, and a private canonical one taking `std::optional>` — funnel into a single implementation that calls through to `RelationalDatabase::getTransaction()`. The range parameter allows RPC callers that already know the ledger range their node has available to pass that constraint in, enabling a meaningful `TxSearched` response (indicating whether all, some, or an unknown portion of history was examined) rather than a bare not-found. + +The return type of `load()` is itself a `std::variant` between a `(Transaction, TxMeta)` pair when the transaction is found and a `TxSearched` enum value when it is not. This design avoids nullable pointer gymnastics and communicates the absence reason precisely. + +## SQL Compatibility and `transactionFromSQL` + +Two static members — `transactionFromSQL()` and `sqlTransactionStatus()` — accept `boost::optional` parameters rather than `std::optional`. This is not an oversight; the SOCI database library used throughout `rippled` requires `boost::optional` for nullable column binding, and changing the parameter types would break the SQL integration layer. `sqlTransactionStatus()` maps a single-character SQL status code to a `TransStatus` enum value, isolating the SQL schema representation from the rest of the class. + +## `getJson()` and CTID Output + +`getJson()` calls through to `STTx::getJson()` but explicitly strips the `include_date` option before passing it down, then re-applies date lookup using `LedgerMaster::getCloseTimeBySeq()` if the option was requested. This two-step indirection exists because the ledger close time is not stored inside the serialized transaction; it must be fetched from `LedgerMaster` using the `mLedgerIndex`. The method also computes a Concise Transaction Identifier (CTID, from XLS-15d) if both `mTxnSeq` and a network ID are available, encoding the ledger sequence, transaction index within the ledger, and network ID into a compact hex string added under `jss::ctid`. The `inLedger` field is emitted only when the API version predates V2, preserving backward compatibility while `ledger_index` becomes the canonical field going forward. \ No newline at end of file diff --git a/src/xrpld/app/misc/TxQ.h.ai.json b/src/xrpld/app/misc/TxQ.h.ai.json new file mode 100644 index 0000000000..144f6be3bb --- /dev/null +++ b/src/xrpld/app/misc/TxQ.h.ai.json @@ -0,0 +1,358 @@ +{ + "args": [ + { + "lineno": 42, + "name": "setup" + }, + { + "lineno": 42, + "name": "j" + }, + { + "lineno": 127, + "name": "feeLevel_" + }, + { + "lineno": 128, + "name": "lastValid_" + }, + { + "lineno": 129, + "name": "consequences_" + }, + { + "lineno": 130, + "name": "account_" + }, + { + "lineno": 131, + "name": "seqProxy_" + }, + { + "lineno": 132, + "name": "txn_" + }, + { + "lineno": 133, + "name": "retriesRemaining_" + }, + { + "lineno": 134, + "name": "preflightResult_" + }, + { + "lineno": 135, + "name": "lastResult_" + }, + { + "lineno": 235, + "name": "sleAccount" + }, + { + "lineno": 240, + "name": "view" + }, + { + "lineno": 252, + "name": "tx" + }, + { + "lineno": 266, + "name": "account" + }, + { + "lineno": 282, + "name": "app" + }, + { + "lineno": 713, + "name": "Config" + }, + { + "lineno": 715, + "name": "level" + }, + { + "lineno": 715, + "name": "baseFee" + }, + { + "lineno": 721, + "name": "drops" + } + ], + "classes": [ + { + "args": [ + "setup", + "j" + ], + "lineno": 41, + "name": "TxQ" + }, + { + "args": [], + "lineno": 54, + "name": "Setup" + }, + { + "args": [], + "lineno": 99, + "name": "Metrics" + }, + { + "args": [ + "feeLevel_", + "lastValid_", + "consequences_", + "account_", + "seqProxy_", + "txn_", + "retriesRemaining_", + "preflightResult_", + "lastResult_" + ], + "lineno": 126, + "name": "TxDetails" + }, + { + "args": [], + "lineno": 246, + "name": "FeeAndSeq" + }, + { + "args": [ + "setup", + "j" + ], + "lineno": 307, + "name": "FeeMetrics" + }, + { + "args": [], + "lineno": 370, + "name": "Snapshot" + }, + { + "args": [ + "txn", + "txID", + "feeLevel", + "flags", + "pfResult" + ], + "lineno": 441, + "name": "MaybeTx" + }, + { + "args": [], + "lineno": 527, + "name": "OrderCandidates" + }, + { + "args": [ + "txn" + ], + "lineno": 553, + "name": "TxQAccount" + } + ], + "description": "Implements the Transaction Queue (TxQ) for XRPL, managing transaction queuing, fee escalation, and application to the open ledger, including metrics, account management, and queue operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/TxQ.h", + "functions": [ + { + "args": [ + "app", + "view", + "tx", + "flags", + "j" + ], + "lineno": 180, + "name": "apply" + }, + { + "args": [ + "app", + "view" + ], + "lineno": 202, + "name": "accept" + }, + { + "args": [ + "app", + "view", + "timeLeap" + ], + "lineno": 222, + "name": "processClosedLedger" + }, + { + "args": [ + "sleAccount" + ], + "lineno": 234, + "name": "nextQueuableSeq" + }, + { + "args": [ + "view" + ], + "lineno": 239, + "name": "getMetrics" + }, + { + "args": [ + "view", + "tx" + ], + "lineno": 251, + "name": "getTxRequiredFeeAndSeq" + }, + { + "args": [ + "account" + ], + "lineno": 265, + "name": "getAccountTxs" + }, + { + "args": [], + "lineno": 273, + "name": "getTxs" + }, + { + "args": [ + "app" + ], + "lineno": 281, + "name": "doRPC" + }, + { + "args": [ + "sleAccount", + "" + ], + "lineno": 289, + "name": "nextQueuableSeqImpl" + }, + { + "args": [ + "view", + "flags", + "metricsSnapshot", + "lock" + ], + "lineno": 573, + "name": "getRequiredFeeLevel" + }, + { + "args": [ + "app", + "view", + "tx", + "flags", + "j" + ], + "lineno": 581, + "name": "tryDirectApply" + }, + { + "args": [ + "replacedTxIter", + "tx" + ], + "lineno": 589, + "name": "removeFromByFee" + }, + { + "args": [], + "lineno": 661, + "name": "isFull" + }, + { + "args": [ + "", + "", + "", + "", + "", + "", + "" + ], + "lineno": 668, + "name": "canBeHeld" + }, + { + "args": [ + "it" + ], + "lineno": 678, + "name": "erase" + }, + { + "args": [ + "it" + ], + "lineno": 682, + "name": "eraseAndAdvance" + }, + { + "args": [ + "txQAccount", + "begin", + "end" + ], + "lineno": 687, + "name": "erase" + }, + { + "args": [ + "app", + "view", + "tx", + "accountIter", + "", + "feeLevelPaid", + "pfResult", + "txExtraCount", + "flags", + "metricsSnapshot", + "j" + ], + "lineno": 697, + "name": "tryClearAccountQueueUpThruTx" + }, + { + "args": [ + "Config" + ], + "lineno": 713, + "name": "setup_TxQ" + }, + { + "args": [ + "level", + "baseFee" + ], + "lineno": 715, + "name": "toDrops" + }, + { + "args": [ + "drops", + "baseFee" + ], + "lineno": 721, + "name": "toFeeLevel" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/TxQ.h.ai.md b/src/xrpld/app/misc/TxQ.h.ai.md new file mode 100644 index 0000000000..c6df600fef --- /dev/null +++ b/src/xrpld/app/misc/TxQ.h.ai.md @@ -0,0 +1,55 @@ +# `TxQ.h` — Transaction Queue and Fee Escalation Engine + +## Role in the System + +`TxQ.h` defines the `TxQ` class, which is the core of XRPL's fee escalation and transaction queuing subsystem. Its fundamental purpose is to ensure the network remains usable under both normal and adversarial load conditions. When the open ledger fills up and the required fee rises too high for a transaction to enter immediately, `TxQ` holds that transaction in an ordered backlog so it can be applied to the *next* open ledger. This prevents legitimate low-fee transactions from being silently dropped while simultaneously making flooding attacks prohibitively expensive. + +## The Fee Escalation Model + +The escalation formula that drives the entire system is embedded in `FeeMetrics::scaleFeeLevel()`: + +``` +requiredFeeLevel = escalationMultiplier × (txCountInLedger²) / (txnsExpected²) +``` + +When the open ledger count is below `txnsExpected`, the required level stays at `baseLevel` (256, the minimum for a single-signed reference transaction). Once the count crosses that threshold, the required fee grows quadratically — by the time `txCountInLedger` doubles `txnsExpected`, the required level is four times the multiplier, which itself starts at 500× base. The result is a fee cliff that quickly becomes economically intractable for attackers while allowing legitimate users to pay a higher fee for priority access. + +The `escalationMultiplier` itself adapts after every closed ledger: `FeeMetrics::update()` computes the median fee level of all transactions in the validated ledger. Using the median — not the mean or the maximum — keeps the multiplier honest; a handful of high-fee transactions by a single user cannot inflate it unfairly. If consensus is slow (`timeLeap == true`), the target `txnsExpected_` is cut by `slowConsensusDecreasePercent` (default 50%), causing the escalation curve to steepen faster the next round. Under healthy consensus it grows by `normalConsensusIncreasePercent` (default 20%) when the ledger is over capacity. + +## Dual Index: `byFee_` and `byAccount_` + +The queue maintains two indexes simultaneously. `byFee_` is a `boost::intrusive::multiset` ordered by `OrderCandidates`, giving the queue its priority-queue behaviour. `byAccount_` is a `std::map`, where each `TxQAccount` holds a `std::map` for per-sequence ordering. + +The choice of `boost::intrusive` is deliberate and architecturally important: a `MaybeTx` object lives in exactly one memory location and is simultaneously a node in both indexes via `byFeeListHook`. There are no copies and no heap-allocated pointers between the structures. This makes insertion and removal `O(log n)` with no additional allocation, which matters when many transactions are in flight during a burst. + +## `MaybeTx` and Its Invariants + +`MaybeTx` wraps a queued transaction and carries all the state needed to re-apply it later. The most important field is `pfResult`, an `std::optional`. The comment on this field states the invariant clearly: it is *never* allowed to be empty; the `std::optional` wrapper exists purely to allow in-place construction via `emplace` without a copy assignment. `preflight` is expensive, so the result is cached and reused across subsequent apply attempts unless the ledger rules change. + +The retry budget is `retriesAllowed = 10`. A transaction at the front of the queue that returns a `ter` (retriable error) from the transactor is left in the queue and given up to 10 more attempts. Once exhausted it is dropped. This prevents the queue from being wedged by transactions that will never succeed. Two per-account penalty flags in `TxQAccount` reinforce this: `retryPenalty` (reduced to 2 retries for all account transactions after one is dropped for excessive retries) and `dropPenalty` (causes account transactions to be discarded first when the queue is nearly full). + +## Pseudo-Random Ordering Within a Fee Level + +`OrderCandidates::operator()` sorts `MaybeTx` by descending fee level, and — when two entries share the same fee level — by `txID ^ MaybeTx::parentHashComp` ascending. `parentHashComp` is a static field set to the parent ledger hash when a new ledger opens. This XOR produces a deterministic but unpredictable ordering that is *identical* across all validators seeing the same ledger. The design goal is for validators to build nearly identical queues after consensus, which increases the probability that their initial transaction proposals overlap and consensus converges quickly. A plain sort by `txID` would also be deterministic but potentially exploitable; XOR with the parent hash prevents submitters from crafting a transaction ID that always wins within a fee tier. + +## Lifecycle Methods + +Three methods correspond to the three phases of XRPL's ledger cycle: + +- **`apply()`**: Called when a new transaction arrives. It first tries to apply the transaction directly to the open ledger via `tryDirectApply()`. If the fee is too low for the open ledger but acceptable for the queue, it calls `canBeHeld()` to check per-account limits, `LastLedgerSequence` constraints, and whether dependent transactions can still be paid. Returns `{ terQUEUED, false }` on successful queuing. The special path `tryClearAccountQueueUpThruTx()` handles the case where a new high-fee transaction for an account can pay enough to flush the entire pre-existing queue chain for that account in one shot — an "all-or-nothing" atomic commitment that avoids partial queue drain. + +- **`accept()`**: Called at the start of each new open ledger, after the previous ledger closes. It drains the queue from highest fee level down, applying each `MaybeTx` to the open ledger via a sandbox. The loop stops as soon as the required fee level rises above the next candidate's level. The `eraseAndAdvance()` method implements a subtle ordering: it prefers the *next transaction for the same account* (if it has a higher or equal fee) over the globally next-in-fee entry, preserving sequence ordering for an account's chain of transactions. + +- **`processClosedLedger()`**: Called when a ledger is validated. It invokes `FeeMetrics::update()` to recompute the escalation multiplier and `txnsExpected_`, updates `maxSize_` (the dynamic queue size cap based on `ledgersInQueue × recentLedgerSize`), and evicts any entry whose `LastLedgerSequence` has expired. + +## Thread Safety + +All mutable state (`feeMetrics_`, `byFee_`, `byAccount_`, `maxSize_`) is protected by `mutex_`. The internal `nextQueuableSeqImpl()` takes a `std::lock_guard const&` by value, forcing callers to hold the lock — a compile-time enforcement of the locking discipline rather than a runtime assertion. The public `nextQueuableSeq()` acquires the lock itself, while the private implementation is reused by other locked callers. A comment notes that most queue operations happen under the app-wide master lock, but `mutex_` exists specifically for the `fee` RPC command, which does not hold the master lock. + +## Fee Unit Helpers + +The free functions `toDrops()` and `toFeeLevel()` convert between raw XRP drops and normalized fee levels using `mulDiv` to avoid floating point. Both use saturating arithmetic: on overflow `toDrops()` returns `STAmount::cMaxNativeN` (the maximum representable native amount) and `toFeeLevel()` returns `UINT64_MAX`, ensuring callers never see a fee that appears artificially cheap due to integer wrap. + +## Configuration via `Setup` + +`setup_TxQ(Config const&)` constructs a `TxQ::Setup` from the application configuration file. The defaults embedded in `Setup` encode the experimentally chosen constants described in `FeeEscalation.md`: `ledgersInQueue = 20`, `queueSizeMin = 2000`, `retrySequencePercent = 25`, `maximumTxnPerAccount = 10`, and `minimumLastLedgerBuffer = 2`. The standalone mode override (`minimumTxnInLedgerSA = 1000`) ensures unit tests do not have to saturate the queue before testing fee escalation behaviour. \ No newline at end of file diff --git a/src/xrpld/app/misc/ValidatorKeys.h.ai.json b/src/xrpld/app/misc/ValidatorKeys.h.ai.json new file mode 100644 index 0000000000..f512bbb67e --- /dev/null +++ b/src/xrpld/app/misc/ValidatorKeys.h.ai.json @@ -0,0 +1,67 @@ +{ + "args": [ + { + "lineno": 23, + "name": "masterPublic_" + }, + { + "lineno": 23, + "name": "public_" + }, + { + "lineno": 23, + "name": "secret_" + }, + { + "lineno": 38, + "name": "config" + }, + { + "lineno": 38, + "name": "j" + } + ], + "classes": [ + { + "args": [ + "Config const& config", + "beast::Journal j" + ], + "lineno": 13, + "name": "ValidatorKeys" + }, + { + "args": [ + "PublicKey const& masterPublic_", + "PublicKey const& public_", + "SecretKey const& secret_" + ], + "lineno": 17, + "name": "Keys" + } + ], + "description": "Defines the ValidatorKeys class, which encapsulates validator keys and manifest information as set in the configuration file for the XRPL software. It includes a nested Keys struct for grouping key material, and provides methods for configuration validation.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/ValidatorKeys.h", + "functions": [ + { + "args": [ + "Config const& config", + "beast::Journal j" + ], + "lineno": 38, + "name": "ValidatorKeys" + }, + { + "args": [], + "lineno": 41, + "name": "configInvalid" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/ValidatorKeys.h.ai.md b/src/xrpld/app/misc/ValidatorKeys.h.ai.md new file mode 100644 index 0000000000..2efd021392 --- /dev/null +++ b/src/xrpld/app/misc/ValidatorKeys.h.ai.md @@ -0,0 +1,39 @@ +# `ValidatorKeys.h` — Validator Key Material and Manifest Container + +## Role in the System + +`ValidatorKeys` is a configuration-time data container that parses and holds the validator's cryptographic key material and manifest string as declared in the `rippled.cfg` configuration file. It exists as a bridge between raw configuration text and the typed, validated key objects consumed by the consensus engine (`RCLConsensus`) and the application startup logic. Its construction is the single point where configuration correctness is validated — any misconfiguration is caught here and surfaced through `configInvalid()`, preventing the node from starting. + +## Two Configuration Paths + +The constructor in `detail/ValidatorKeys.cpp` handles two mutually exclusive configuration sections: + +**`[validator_token]` (modern path):** A token encodes an ephemeral signing key and a manifest blob. The constructor calls `loadValidatorToken`, derives the public key from the secret, and then `deserializeManifest` to unpack the manifest. It cross-checks that the derived public key matches the manifest's `signingKey`. If they agree, all three key fields are populated: `masterKey` from the manifest (the stable long-term identity), `pk` as the ephemeral signing key, and `secretKey` from the token. The manifest's `sequence` number is stored to track key rotation, and the raw manifest string is preserved for later submission to the manifest database. + +**`[validation_seed]` (legacy path):** A bare seed is parsed; both the master and signing keys are derived identically from it (`keys.emplace(pk, pk, sk)`), and `sequence` is set to 0. There is no separate master vs. signing key distinction — the node signs directly with its permanent identity, bypassing the manifest rotation mechanism. + +If both sections are present simultaneously, `configInvalid_` is set immediately and the constructor returns early. This mutual-exclusion guard prevents ambiguous configurations that could cause silent key mismatches. + +## The `Keys` Struct Invariant + +The nested `Keys` struct enforces an all-or-nothing grouping of `masterPublicKey`, `publicKey`, and `secretKey`. Its constructor is deliberately `delete`d (no default construction), so a `Keys` value can only exist when all three fields are simultaneously provided. The outer `keys` member is `std::optional`, which cleanly represents two states: the node is a validator (populated) or it is not (empty). A critical design note embedded in the header itself warns that the *absence* of `keys` must not be conflated with an *invalid* configuration — a node without a `[validator_token]` or `[validation_seed]` section is perfectly valid as a non-validating node. + +## `configInvalid_` vs. Empty Keys + +These are two orthogonal concerns. `configInvalid_` is set when configuration *attempted* to configure a validator but supplied incorrect data (bad token encoding, public key mismatch, unparseable seed, or contradictory sections). An empty `keys` simply means the node chose not to act as a validator. Callers in `Application.cpp` check `configInvalid()` to gate startup entirely, while they check `keys.has_value()` independently to enable validator-specific behaviors like disallowed ledger tracking and manifest submission. + +## How It Flows into Application Startup + +`Application` holds a `ValidatorKeys const validatorKeys_` (immutable after construction), initialized with `validatorKeys_(*config_, m_journal)` before any network or consensus subsystems are created. The startup sequence then: + +1. Calls `validatorKeys_.configInvalid()` and aborts if `true`. +2. Passes `validatorKeys_.manifest` to `validatorManifests_->load(...)`, inserting this node's own manifest into the manifest store so peers receive it. +3. Passes `validatorKeys_.keys->publicKey` as the `localSigningKey` to `ValidatorList::load`, so the node can recognize its own signing key in trust calculations. + +## Role in Consensus + +`RCLConsensus::Adaptor` stores a `ValidatorKeys const&` reference throughout its lifetime. Every time the node proposes a consensus position or emits a ledger validation, it checks `validatorKeys_.keys` and, if present, signs the outgoing message with `keys->publicKey` and `keys->secretKey`. The `nodeID` (a hash of the master public key derived via `calcNodeID`) appears in outgoing proposals and validations as the node's stable identity on the network. If `masterPublicKey != publicKey` (i.e., token mode with key rotation), the startup log records both identities and the manifest sequence, providing a clear audit trail of which ephemeral key is currently active. + +## Design Trade-offs + +Storing the raw `manifest` string rather than a deserialized `Manifest` object keeps this class lightweight and independent of the manifest subsystem's full machinery — the manifest is only needed once during startup to load into the wallet DB. The `sequence` field stored separately avoids re-parsing the manifest at every call site. Using `std::optional` rather than null pointers or sentinel values for the key group makes presence testing explicit and type-safe, eliminating an entire class of null-dereference bugs at consensus signing time. \ No newline at end of file diff --git a/src/xrpld/app/misc/ValidatorList.h.ai.json b/src/xrpld/app/misc/ValidatorList.h.ai.json new file mode 100644 index 0000000000..891219d10e --- /dev/null +++ b/src/xrpld/app/misc/ValidatorList.h.ai.json @@ -0,0 +1,109 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 52, + "name": "TrustChanges" + }, + { + "args": [], + "lineno": 59, + "name": "ValidatorBlobInfo" + }, + { + "args": [ + "ManifestCache& validatorManifests", + "ManifestCache& publisherManifests", + "TimeKeeper& timeKeeper", + "std::string const& databasePath", + "beast::Journal j", + "std::optional minimumQuorum = std::nullopt" + ], + "lineno": 87, + "name": "ValidatorList" + }, + { + "args": [], + "lineno": 89, + "name": "ValidatorList::PublisherList" + }, + { + "args": [], + "lineno": 106, + "name": "ValidatorList::PublisherListCollection" + }, + { + "args": [], + "lineno": 170, + "name": "ValidatorList::PublisherListStats" + }, + { + "args": [], + "lineno": 187, + "name": "ValidatorList::MessageWithHash" + } + ], + "description": "This file defines the ValidatorList class and related structures/enums for managing trusted validator lists in the XRPL (XRP Ledger) node software. It handles loading, applying, updating, and broadcasting validator lists, as well as tracking trusted keys, publishers, and quorum calculations.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/ValidatorList.h", + "functions": [ + { + "args": [ + "disposition" + ], + "lineno": 49, + "name": "to_string" + }, + { + "args": [ + "Hasher& h", + "ValidatorBlobInfo const& blobInfo" + ], + "lineno": 496, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::vector const& blobs" + ], + "lineno": 505, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "std::map const& blobs" + ], + "lineno": 511, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "TMValidatorList const& msg" + ], + "lineno": 523, + "name": "hash_append" + }, + { + "args": [ + "Hasher& h", + "TMValidatorListCollection const& msg" + ], + "lineno": 530, + "name": "hash_append" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "protocol" + }, + { + "lineno": 18, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/ValidatorList.h.ai.md b/src/xrpld/app/misc/ValidatorList.h.ai.md new file mode 100644 index 0000000000..6d92902777 --- /dev/null +++ b/src/xrpld/app/misc/ValidatorList.h.ai.md @@ -0,0 +1,65 @@ +# `ValidatorList.h` — Trusted Validator (UNL) Management + +`ValidatorList` is the central authority for determining which validator nodes this XRPL node will trust for ledger validation. It manages the entire lifecycle of the Unique Node List (UNL): loading initial configuration, applying publisher-signed updates, rotating future-dated lists into active use, computing quorum values, and broadcasting new lists to peers. Nothing about consensus trust lives outside this class and its companion `.cpp`. + +## Why This Design Exists + +XRPL consensus requires each node to maintain a set of validator public keys it will accept validations from. A ledger is considered fully validated once the node receives signed validations from a quorum of its trusted set. Rather than hard-coding these keys, the protocol supports *publisher-signed lists*: well-known organizations publish signed JSON blobs listing trusted validator keys. This lets the trusted set evolve without requiring every node operator to manually reconfigure their node for each change. + +The dual-trust model — trust publishers to sign lists, trust lists to name validators — means a compromise of a publisher's *signing* key only requires the publisher to rotate to a new ephemeral key (via a manifest), not reconfigure every node. Similarly, validator operators can rotate their signing keys by issuing new manifests signed with their master key. `ValidatorList` maintains both a `ManifestCache& validatorManifests_` (for validators) and `ManifestCache& publisherManifests_` (for publishers), and understands that a `PublicKey` arriving in a validation message may be an ephemeral signing key that must be resolved back to a master key. + +## Internal Data Hierarchy + +The nesting of data structures reflects the protocol's version evolution. + +`PublisherList` holds the data for one specific version of a publisher's list: the sorted vector of validator master `PublicKey`s, the raw encoded blob, signature, optional per-blob manifest, sequence number, and validity window (`validFrom` / `validUntil`). + +`PublisherListCollection` groups everything from a single publisher. It contains: +- `current` — the active `PublisherList` (highest sequence whose effective date has arrived) +- `remaining` — a `std::map` of future-dated lists indexed by sequence number, sorted ascending +- `maxSequence` — the highest sequence number ever seen from this publisher +- `fullHash` — a precomputed hash over the entire collection for quick duplicate detection + +The semantic rule documented inline is important: once a list with sequence *N* becomes current, all lists with sequences below *N* are permanently obsolete — there is no rollback to a previous list, even if the current one expires. This prevents an attacker from replaying an old list to reactivate removed validators. + +The global `publisherLists_` hash map from `PublicKey` → `PublisherListCollection` is the master data store. A separate `keyListings_` hash map counts how many publisher lists each validator master key currently appears on — this reference count is what enables the `listThreshold_` policy. + +## List Application Pipeline + +Incoming lists arrive through `applyListsAndBroadcast()`, which delegates the actual state update to `applyLists()`, which in turn calls the private `applyList()` once per blob. The work inside `applyList()` follows a strict order: deserialize and `verify()` the manifest, check signature and JSON structure, classify the sequence relative to what's known, then update `current` or `remaining`. After all blobs are processed, `applyLists()` purges any `remaining` entries made redundant (sequence ≤ current, or a later-sequenced entry with an earlier or equal effective date that would be skipped over anyway). The collection is then persisted via `cacheValidatorFile()` and its `fullHash` is recomputed. + +The `verify()` private method enforces the full disposition lattice: an unrecognized publisher master key returns `untrusted`; a revoked manifest returns `untrusted` and immediately calls `removePublisherList()`; a failed cryptographic signature returns `invalid`; an already-current sequence returns `same_sequence`; a sequence below current returns `stale`; an already-known future sequence returns `known_sequence`; an expired list (past its `validUntil`) still advances the publisher's sequence number, returning `expired`; a future-dated list returns `pending`. Only `accepted` and `expired` sequences cause `keyListings_` to be updated. + +Both `ListDisposition` and `PublisherStatus` enums are explicitly ordered by desirability (lower integer = better), which allows `PublisherListStats::bestDisposition()` and `worstDisposition()` to cheaply summarize the result of processing a multi-blob V2 collection. + +## Trust Computation: `updateTrusted()` + +`updateTrusted()` is the heartbeat of trust management, called at the start of each consensus round. Under an exclusive lock it performs four sequential tasks: + +1. **List rotation**: For each publisher collection, if any `remaining` entry has a `validFrom` ≤ `closeTime`, the highest such entry is moved into `current`. Entries skipped over are erased. This triggers `updatePublisherList()` to diff the old and new validator sets and adjust `keyListings_` counts, then `broadcastBlobs()` to propagate the newly activated list to peers. + +2. **Expiry handling**: If a publisher's `current` list has passed its `validUntil`, `removePublisherList()` clears all its validators from `keyListings_` and marks the publisher `expired`. `NetworkOPs::setUNLBlocked()` is called to signal that ledger validation is degraded. + +3. **Trust set rebuild**: `trustedMasterKeys_` is rebuilt from scratch based on `keyListings_` counts vs. `listThreshold_`, excluding any revoked manifests. If any keys changed, `trustedSigningKeys_` is also rebuilt by resolving each trusted master key to its current ephemeral signing key via `validatorManifests_`. + +4. **Quorum calculation**: `calculateQuorum()` computes the new quorum. The baseline formula is `max(ceil(effectiveUnlSize × 0.8), ceil(unlSize × 0.6))`, derived from Theorem 8 of the XRP LCP Analysis paper (arXiv:1802.07242). The 0.6 floor comes from the Negative UNL protocol's `AbsoluteMinimumQuorum`. If too many publishers are unavailable (exceeding `min(listThreshold_, N - listThreshold_ + 1)` where N is total publishers), quorum is set to `std::numeric_limits::max()` — effectively making validation impossible until lists are recovered. The `quorum_` field is `std::atomic` so it can be read without acquiring the lock. + +## The `listThreshold_` Policy + +`listThreshold_` controls how many publisher lists a validator must appear on to be trusted. When `load()` is called without an explicit threshold, it defaults to 1 for fewer than three publishers, or to `(N/2) + 1` for N ≥ 3 publishers — a majority rule. This prevents a single compromised publisher from unilaterally adding malicious validators to the trusted set. The local node's own key is always inserted into `keyListings_` with a count of exactly `listThreshold_` so the node never excludes itself. + +## Concurrency Architecture + +`ValidatorList` uses a `shared_mutex` (`boost::thread::shared_mutex` aliased to the standard type) for reader-writer separation. The private overloads of `trusted()`, `getTrustedKey()`, `expires()`, and `count()` accept an already-held lock object to avoid double-locking when callers need to hold the lock across multiple reads. This "lock-passing" idiom also documents locking contracts at the type level: a `lock_guard const&` parameter signals that the caller must hold an exclusive lock; `shared_lock const&` signals a read lock. The `applyList()` private method requires an exclusive `lock_guard` even though it may only be reading, because it can mutate `publisherLists_` — the locking contract makes this clear. + +## Network Propagation + +`broadcastBlobs()` uses `HashRouter::shouldRelay()` to determine which peers have not yet seen a given list hash, then calls `sendValidatorList()` per peer. Peers are checked for protocol feature support: `ValidatorList2Propagation` (V2) peers receive all blobs with sequence numbers greater than what they've already seen, batched into `TMValidatorListCollection` messages that are split by binary recursion if they exceed `maximumMessageSize`. V1-only peers receive only the `current` blob as a `TMValidatorList` message. The `MessageWithHash` struct caches already-built `Message` objects so that if multiple V2 peers have the same known sequence number, the protobuf serialization is done only once. + +## File Caching and Bootstrap + +`cacheValidatorFile()` writes each publisher's full collection to `dataPath_ / "cache."` as a styled JSON file. On startup, if URL fetching fails, `loadLists()` scans the publisher map for any that are still `unavailable` and returns `file://` URIs pointing to cached files. `ValidatorSites` (not shown here) then treats these as normal HTTP sources and re-applies them, providing offline bootstrap when the internet-hosted list is temporarily unreachable. + +## Negative UNL Integration + +`negativeUNL_` holds master keys of validators that have been voted into the negative UNL by consensus — validators that have been unreliable recently. `setNegativeUNL()` is called when a new ledger is validated to sync this set. `negativeUNLFilter()` removes validations from these validators before they're counted toward quorum in the consensus engine. Crucially, the quorum formula accounts for the negative UNL by computing `effectiveUnlSize = unlSize - |negativeUNL_ ∩ trustedMasterKeys_|`, lowering the quorum while keeping the 60% floor based on the original `unlSize`. \ No newline at end of file diff --git a/src/xrpld/app/misc/ValidatorSite.h.ai.json b/src/xrpld/app/misc/ValidatorSite.h.ai.json new file mode 100644 index 0000000000..71e4b22cf0 --- /dev/null +++ b/src/xrpld/app/misc/ValidatorSite.h.ai.json @@ -0,0 +1,170 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app", + "std::optional j = std::nullopt", + "std::chrono::seconds timeout = std::chrono::seconds{20}" + ], + "lineno": 41, + "name": "ValidatorSite" + }, + { + "args": [ + "std::string uri" + ], + "lineno": 52, + "name": "Site" + }, + { + "args": [], + "lineno": 54, + "name": "Status" + }, + { + "args": [ + "std::string uri_" + ], + "lineno": 60, + "name": "Resource" + } + ], + "description": "This file defines the ValidatorSite class, which manages the set of configured remote sites used to fetch the latest published recommended validator lists for XRPL. It handles fetching, parsing, and updating validator lists from remote sources at regular intervals, with support for JSON parsing, redirects, and thread safety.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/ValidatorSite.h", + "functions": [ + { + "args": [ + "Application& app", + "std::optional j = std::nullopt", + "std::chrono::seconds timeout = std::chrono::seconds{20}" + ], + "lineno": 66, + "name": "ValidatorSite" + }, + { + "args": [], + "lineno": 67, + "name": "~ValidatorSite" + }, + { + "args": [ + "std::vector const& siteURIs" + ], + "lineno": 75, + "name": "load" + }, + { + "args": [], + "lineno": 89, + "name": "start" + }, + { + "args": [], + "lineno": 99, + "name": "join" + }, + { + "args": [], + "lineno": 109, + "name": "stop" + }, + { + "args": [], + "lineno": 119, + "name": "getJson" + }, + { + "args": [ + "std::vector const& siteURIs", + "std::lock_guard const&" + ], + "lineno": 124, + "name": "load" + }, + { + "args": [ + "std::lock_guard const&", + "std::lock_guard const&" + ], + "lineno": 128, + "name": "setTimer" + }, + { + "args": [ + "std::size_t siteIdx", + "error_code const& ec" + ], + "lineno": 132, + "name": "onRequestTimeout" + }, + { + "args": [ + "std::size_t siteIdx", + "error_code const& ec" + ], + "lineno": 136, + "name": "onTimer" + }, + { + "args": [ + "boost::system::error_code const& ec", + "endpoint_type const& endpoint", + "detail::response_type const& res", + "std::size_t siteIdx" + ], + "lineno": 140, + "name": "onSiteFetch" + }, + { + "args": [ + "boost::system::error_code const& ec", + "std::string const& res", + "std::size_t siteIdx" + ], + "lineno": 146, + "name": "onTextFetch" + }, + { + "args": [ + "std::shared_ptr resource", + "std::size_t siteIdx", + "std::lock_guard const&" + ], + "lineno": 151, + "name": "makeRequest" + }, + { + "args": [ + "std::string const& res", + "std::size_t siteIdx", + "std::lock_guard const&" + ], + "lineno": 157, + "name": "parseJsonResponse" + }, + { + "args": [ + "detail::response_type const& res", + "std::size_t siteIdx", + "std::lock_guard const&" + ], + "lineno": 163, + "name": "processRedirect" + }, + { + "args": [ + "std::lock_guard const&" + ], + "lineno": 169, + "name": "missingSite" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/ValidatorSite.h.ai.md b/src/xrpld/app/misc/ValidatorSite.h.ai.md new file mode 100644 index 0000000000..ff612fe02e --- /dev/null +++ b/src/xrpld/app/misc/ValidatorSite.h.ai.md @@ -0,0 +1,57 @@ +# `ValidatorSite.h` — Remote Validator List Fetcher + +## Role and Purpose + +`ValidatorSite` is the component responsible for keeping a rippled node's local view of trusted validators current by periodically fetching signed validator lists from remote URLs. Validator lists are the mechanism XRPL uses to establish which nodes a given server considers trustworthy consensus participants. Because these lists are signed by well-known publishers and carry an expiry timestamp, they must be refreshed before they expire — that is the core problem this class solves. + +The class sits between the network I/O layer (the `Work` hierarchy) and the cryptographic trust layer (`ValidatorList`). It handles the full lifecycle: URI parsing, HTTP/HTTPS/file transport selection, redirect following, JSON parsing, and finally handing off a verified payload to `ValidatorList::applyListsAndBroadcast()`. + +## The `Site` Struct and Three-Pointer Resource Model + +The most subtle data structure in the header is the nested `Site`, which tracks three distinct `shared_ptr` objects for a single configured URL: + +- `loadedResource` — the URI exactly as it appeared in the config file; never changes after construction. +- `startingResource` — the URI used at the start of each scheduled refresh cycle; equals `loadedResource` initially, but is updated when a *permanent* HTTP redirect (301/308) is received. This means the server automatically tracks permanent moves without requiring operator intervention. +- `activeResource` — the URI currently being requested within a single cycle; equals `startingResource` except when a temporary redirect (302/307) is in flight. It is cleared to `nullptr` once the fetch completes, which serves as the guard condition inside `onRequestTimeout` to detect the rare race where both the timeout and the response handler are simultaneously queued. + +This three-pointer design carefully encodes redirect semantics: permanent redirects update the baseline for future cycles; temporary redirects are followed inline but forgotten afterward. Redirects are capped at `max_redirects = 3` to prevent loops. + +## Concurrency Model + +The class uses two mutexes with a rigidly enforced acquisition order, documented explicitly in the header: + +> *If both mutex are to be locked at the same time, `sites_mutex_` must be locked before `state_mutex_` or we may deadlock.* + +`sites_mutex_` protects the `sites_` vector and all per-site state. `state_mutex_` protects timer scheduling, `fetching_`, `pending_`, and `stopping_` state. The three atomic flags (`fetching_`, `pending_`, `stopping_`) allow lightweight checks without taking either lock in performance-sensitive paths. + +Private methods that require a lock already held accept `std::lock_guard const&` by reference as a proof-of-holding parameter. This is a compile-time enforcement idiom: the caller is forced to name the guard, making the locking intent explicit and preventing accidental calls on unlocked state. `setTimer()`, `makeRequest()`, `parseJsonResponse()`, `processRedirect()`, and `missingSite()` all use this pattern. + +A `std::condition_variable cv_` coordinates the lifecycle methods: `join()` waits for `!pending_`, `stop()` waits for `!fetching_`, and the destructor participates in this same wait if `stop()` has already been called concurrently. + +## Timer-Driven Scheduling Loop + +`setTimer()` scans `sites_` for the entry with the earliest `nextRefresh` time and arms a single `boost::asio::basic_waitable_timer` against that deadline. When the timer fires, `onTimer()` records a new `nextRefresh` (current time plus `refreshInterval`), resets the redirect counter, and calls `makeRequest()` with `startingResource`. After each completed fetch — whether successful or failed — `onSiteFetch()` or `onTextFetch()` calls `setTimer()` again, perpetuating the cycle. + +The default refresh interval is 5 minutes, but a remote site can override it via the optional `refreshInterval` JSON field (clamped to [1, 1440] minutes). Fetch errors use a faster `error_retry_interval` of 30 seconds so transient outages recover promptly. + +The overall design processes only one site at a time: a single `std::weak_ptr work_` and the `fetching_` flag ensure no two fetch operations overlap, simplifying state management at the cost of some parallelism. + +## Transport Abstraction via `Work` + +`makeRequest()` selects a concrete `Work` subclass based on the URI scheme: + +- `WorkSSL` for `https://` — uses Boost.Asio with SSL context from the application config, with a remembered endpoint (`lastRequestEndpoint`) and a connection-reuse hint (`lastRequestSuccessful`) to skip redundant DNS lookups on repeated hits to the same host. +- `WorkPlain` for `http://` — same endpoint caching, no TLS. +- `WorkFile` for `file://` — reads up to 1 MB from a local path asynchronously via `boost::asio::strand`, useful for testing or air-gapped deployments with pre-distributed list files. + +A request timeout of 20 seconds (configurable at construction, primarily for tests) is implemented by setting the same `timer_` to fire after `requestTimeout_` immediately after `Work::run()` is called. The `timeoutCancel` lambda captures the reverse: when the response arrives first, it calls `timer_.cancel_one()` to prevent a spurious timeout from running. + +## JSON Parsing and List Application + +`parseJsonResponse()` validates the JSON envelope — `manifest`, `version`, `blob`(s), and `signature` — then calls `ValidatorList::parseBlobs()` for version-specific payload extraction, and hands the result to `ValidatorList::applyListsAndBroadcast()`. This method both updates the local validator trust state and propagates the list to connected peers via the overlay network. The response is hashed with `sha512Half` over manifest, blobs, and version before broadcast, so peers can deduplicate via `HashRouter`. + +`ListDisposition` return values from `applyListsAndBroadcast` are logged per-disposition at debug or warn level and stored in `Site::Status::disposition` for the `getJson()` status API. The disposition ordering is itself semantically significant: values are defined from "best" to "worst" so `bestDisposition()` can return the most informative result when multiple blobs are applied in a single response. + +## Fallback to Local Cache + +`missingSite()` is called in two situations: when no site URIs are configured at all, and when a fetch fails. It asks `ValidatorList::loadLists()` for any previously cached list files and attempts to load them as if they were fetched from remote. This prevents a validator from losing its trusted-validator configuration purely due to transient network unavailability, falling back gracefully to the last known good state. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/AccountTxPaging.cpp.ai.json b/src/xrpld/app/misc/detail/AccountTxPaging.cpp.ai.json new file mode 100644 index 0000000000..905362e7ef --- /dev/null +++ b/src/xrpld/app/misc/detail/AccountTxPaging.cpp.ai.json @@ -0,0 +1,410 @@ +{ + "args": [ + { + "lineno": 12, + "name": "to" + }, + { + "lineno": 13, + "name": "ledger_index" + }, + { + "lineno": 14, + "name": "status" + }, + { + "lineno": 15, + "name": "rawTxn" + }, + { + "lineno": 16, + "name": "rawMeta" + }, + { + "lineno": 17, + "name": "app" + }, + { + "lineno": 39, + "name": "seq" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "convertBlobsToTxResult", + "STTx constructor (via SerialIter)", + "Transaction constructor", + "TxMeta constructor", + "Transaction::setStatus" + ], + "entry_point": "convertBlobsToTxResult", + "purpose": "Converts raw transaction and metadata blobs into Transaction and TxMeta objects, validates them, and appends to output vector.", + "validation_points": [ + "STTx constructor validates rawTxn", + "TxMeta constructor validates rawMeta", + "Transaction::sqlTransactionStatus validates status", + "app.getLedgerMaster().getLedgerBySeq validates ledger_index (indirectly, if used elsewhere)", + "metaset->getAsObject().isFieldPresent(sfTransactionIndex) validates presence of sfTransactionIndex" + ] + }, + { + "call_chain": [ + "saveLedgerAsync", + "app.getLedgerMaster().getLedgerBySeq", + "pendSaveValidated" + ], + "entry_point": "saveLedgerAsync", + "purpose": "Asynchronously triggers saving of a validated ledger if it exists.", + "validation_points": [ + "app.getLedgerMaster().getLedgerBySeq validates ledger_index (seq)" + ] + } + ], + "data_flows": [ + { + "field": "rawTxn", + "flow": [ + "convertBlobsToTxResult parameter", + "makeSlice(rawTxn)", + "SerialIter it(makeSlice(rawTxn))", + "STTx constructor(it)", + "std::make_shared(txn, ...)" + ], + "origin": "Input parameter to convertBlobsToTxResult", + "transformations": [ + "Converted to slice", + "Parsed by SerialIter", + "Validated and parsed by STTx constructor" + ], + "validated_at": "STTx constructor" + }, + { + "field": "rawMeta", + "flow": [ + "convertBlobsToTxResult parameter", + "TxMeta constructor(tr->getID(), ledger_index, rawMeta)", + "metaset->getAsObject()" + ], + "origin": "Input parameter to convertBlobsToTxResult", + "transformations": [ + "Parsed and validated by TxMeta constructor" + ], + "validated_at": "TxMeta constructor" + }, + { + "field": "status", + "flow": [ + "convertBlobsToTxResult parameter", + "Transaction::sqlTransactionStatus(status)", + "Transaction::setStatus" + ], + "origin": "Input parameter to convertBlobsToTxResult", + "transformations": [ + "Mapped/validated by sqlTransactionStatus" + ], + "validated_at": "Transaction::sqlTransactionStatus" + }, + { + "field": "ledger_index", + "flow": [ + "convertBlobsToTxResult parameter", + "TxMeta constructor", + "Transaction::setStatus", + "saveLedgerAsync parameter", + "app.getLedgerMaster().getLedgerBySeq(seq)" + ], + "origin": "Input parameter to convertBlobsToTxResult and saveLedgerAsync", + "transformations": [ + "Used as-is, checked for existence in getLedgerBySeq" + ], + "validated_at": "app.getLedgerMaster().getLedgerBySeq" + }, + { + "field": "sfTransactionIndex", + "flow": [ + "TxMeta constructor parses rawMeta", + "metaset->getAsObject().isFieldPresent(sfTransactionIndex)", + "metaset->getAsObject().getFieldU32(sfTransactionIndex) (if present)", + "Transaction::setStatus" + ], + "origin": "Field in TxMeta object (from rawMeta)", + "transformations": [ + "Checked for presence, extracted as U32" + ], + "validated_at": "metaset->getAsObject().isFieldPresent(sfTransactionIndex)" + } + ], + "description": "Implements utility functions for converting raw transaction and metadata blobs into transaction results and for asynchronously saving a ledger in the context of the XRPL application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "rawTxn (by STTx)", + "validation", + "missing", + "check" + ], + "evidence": "Field rawTxn (by STTx) validated by Custom (xrpld internal classes: STTx, TxMeta, Transaction)", + "issue_pattern": "Missing validation for rawTxn (by STTx)", + "why_false_positive": "Custom (xrpld internal classes: STTx, TxMeta, Transaction) validates rawTxn (by STTx) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "rawMeta (by TxMeta)", + "validation", + "missing", + "check" + ], + "evidence": "Field rawMeta (by TxMeta) validated by Custom (xrpld internal classes: STTx, TxMeta, Transaction)", + "issue_pattern": "Missing validation for rawMeta (by TxMeta)", + "why_false_positive": "Custom (xrpld internal classes: STTx, TxMeta, Transaction) validates rawMeta (by TxMeta) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "status (by sqlTransactionStatus)", + "validation", + "missing", + "check" + ], + "evidence": "Field status (by sqlTransactionStatus) validated by Custom (xrpld internal classes: STTx, TxMeta, Transaction)", + "issue_pattern": "Missing validation for status (by sqlTransactionStatus)", + "why_false_positive": "Custom (xrpld internal classes: STTx, TxMeta, Transaction) validates status (by sqlTransactionStatus) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index (by getLedgerBySeq)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_index (by getLedgerBySeq) validated by Custom (xrpld internal classes: STTx, TxMeta, Transaction)", + "issue_pattern": "Missing validation for ledger_index (by getLedgerBySeq)", + "why_false_positive": "Custom (xrpld internal classes: STTx, TxMeta, Transaction) validates ledger_index (by getLedgerBySeq) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfTransactionIndex (by isFieldPresent)", + "validation", + "missing", + "check" + ], + "evidence": "Field sfTransactionIndex (by isFieldPresent) validated by Custom (xrpld internal classes: STTx, TxMeta, Transaction)", + "issue_pattern": "Missing validation for sfTransactionIndex (by isFieldPresent)", + "why_false_positive": "Custom (xrpld internal classes: STTx, TxMeta, Transaction) validates sfTransactionIndex (by isFieldPresent) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "rawTxn", + "empty", + "string", + "validation" + ], + "evidence": "STTx constructor (via SerialIter) at convertBlobsToTxResult", + "issue_pattern": "Missing empty string validation for rawTxn", + "why_false_positive": "STTx constructor (via SerialIter) validates rawTxn for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "rawMeta", + "empty", + "string", + "validation" + ], + "evidence": "TxMeta constructor at convertBlobsToTxResult", + "issue_pattern": "Missing empty string validation for rawMeta", + "why_false_positive": "TxMeta constructor validates rawMeta for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "status", + "empty", + "string", + "validation" + ], + "evidence": "Transaction::sqlTransactionStatus at convertBlobsToTxResult", + "issue_pattern": "Missing empty string validation for status", + "why_false_positive": "Transaction::sqlTransactionStatus validates status for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "app.getLedgerMaster().getLedgerBySeq(seq) at saveLedgerAsync", + "issue_pattern": "Missing empty string validation for ledger_index", + "why_false_positive": "app.getLedgerMaster().getLedgerBySeq(seq) validates ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfTransactionIndex", + "empty", + "string", + "validation" + ], + "evidence": "metaset->getAsObject().isFieldPresent(sfTransactionIndex) at convertBlobsToTxResult", + "issue_pattern": "Missing empty string validation for sfTransactionIndex", + "why_false_positive": "metaset->getAsObject().isFieldPresent(sfTransactionIndex) validates sfTransactionIndex for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/AccountTxPaging.cpp", + "functions": [ + { + "args": [ + "RelationalDatabase::AccountTxs& to", + "std::uint32_t ledger_index", + "std::string const& status", + "Blob const& rawTxn", + "Blob const& rawMeta", + "Application& app" + ], + "lineno": 10, + "name": "convertBlobsToTxResult" + }, + { + "args": [ + "Application& app", + "std::uint32_t seq" + ], + "lineno": 38, + "name": "saveLedgerAsync" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is low-level and likely tested indirectly via higher-level transaction and ledger tests. Direct unit tests for convertBlobsToTxResult and saveLedgerAsync may not exist, but their behavior is exercised by integration tests that check transaction history, ledger persistence, and database correctness. Test files might include those under 'test/app/misc', 'test/app/ledger', or 'test/rpc'. Gaps: There may be limited direct negative testing for malformed rawTxn/rawMeta blobs, or for edge cases in status/ledger_index validation. Direct fuzzing or malformed input tests would strengthen coverage.", + "validation_architecture": { + "auto_validated_fields": [ + "rawTxn (by STTx)", + "rawMeta (by TxMeta)", + "status (by sqlTransactionStatus)", + "ledger_index (by getLedgerBySeq)", + "sfTransactionIndex (by isFieldPresent)" + ], + "framework": "Custom (xrpld internal classes: STTx, TxMeta, Transaction)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "std::exception (likely, if deserialization fails)", + "field": "rawTxn", + "location": "convertBlobsToTxResult", + "validated_by": "STTx constructor (via SerialIter)", + "validates": [ + "rawTxn is valid serialized transaction", + "rawTxn can be parsed into STTx" + ], + "validation_type": "format|type" + }, + { + "confidence": 0.9, + "error_thrown": "std::exception (likely, if deserialization fails)", + "field": "rawMeta", + "location": "convertBlobsToTxResult", + "validated_by": "TxMeta constructor", + "validates": [ + "rawMeta is valid serialized metadata", + "rawMeta can be parsed into TxMeta" + ], + "validation_type": "format|type" + }, + { + "confidence": 0.7, + "error_thrown": "unknown (depends on sqlTransactionStatus implementation)", + "field": "status", + "location": "convertBlobsToTxResult", + "validated_by": "Transaction::sqlTransactionStatus", + "validates": [ + "status is mapped to a valid transaction status" + ], + "validation_type": "business_logic|enum" + }, + { + "confidence": 0.8, + "error_thrown": "none (function returns nullptr if not found)", + "field": "ledger_index", + "location": "saveLedgerAsync", + "validated_by": "app.getLedgerMaster().getLedgerBySeq(seq)", + "validates": [ + "ledger_index exists in ledger master" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 0.8, + "error_thrown": "none (conditional check)", + "field": "sfTransactionIndex", + "location": "convertBlobsToTxResult", + "validated_by": "metaset->getAsObject().isFieldPresent(sfTransactionIndex)", + "validates": [ + "sfTransactionIndex field exists in meta object" + ], + "validation_type": "existence" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/AccountTxPaging.cpp.ai.md b/src/xrpld/app/misc/detail/AccountTxPaging.cpp.ai.md new file mode 100644 index 0000000000..b4c4d17e1f --- /dev/null +++ b/src/xrpld/app/misc/detail/AccountTxPaging.cpp.ai.md @@ -0,0 +1,32 @@ +# `AccountTxPaging.cpp` — Blob Deserialization and Async Ledger Saving for Account Tx Paging + +This file lives in the `detail` subdirectory of `src/xrpld/app/misc/` and provides two small but architecturally load-bearing utilities that bridge the relational database layer with the in-memory transaction object model. Both functions exist to be injected as callbacks into SQLite paging routines, keeping deserialization logic out of the database backend itself. + +## Context: How These Functions Are Used + +The SQLite database backend (`SQLiteDatabase.cpp`) implements paginated account transaction queries — `oldestAccountTxPage`, `newestAccountTxPage`, and their variants. Each of these constructs two callback lambdas before invoking the lower-level SQL scan: + +- `onTransaction`: called for every matching row, responsible for converting raw database bytes into usable objects. +- `onUnsavedLedger`: called whenever the scan encounters a ledger sequence that has not yet been persisted — an opportunity for opportunistic re-save. + +`convertBlobsToTxResult` and `saveLedgerAsync` are the implementations behind those callbacks, factored out here so they aren't duplicated across the four page-direction variants. + +## `convertBlobsToTxResult` + +This function takes two raw binary blobs — `rawTxn` (a serialized `STTx`) and `rawMeta` (serialized transaction metadata) — along with the containing ledger index and a SQL status string, and appends a fully-constructed `AccountTx` pair (`shared_ptr`, `shared_ptr`) to the output vector `to`. + +The deserialization pipeline is straightforward: `rawTxn` is wrapped in a `makeSlice` call, fed to a `SerialIter`, and passed to the `STTx` constructor which validates and parses the binary format, throwing on malformed input. The resulting `STTx` is wrapped in a `Transaction` object using the application context. `rawMeta` is separately deserialized into a `TxMeta` using the transaction's ID and ledger index as anchors. + +The more interesting logic is the conditional branch on `sfTransactionIndex`. XRPL supports CTIDs — compact transaction identifiers that encode ledger sequence, transaction index within the ledger, and network ID into a compact form. Generating a CTID requires the transaction's position within its ledger, which lives in the metadata as `sfTransactionIndex`. Not all historical metadata records contain this field (older data may predate the format), so the function checks `isFieldPresent` before deciding which overload of `setStatus` to call. When the field is present, `setStatus` receives the full four-argument form including `getFieldU32(sfTransactionIndex)` and the network ID from `app.getNetworkIDService().getNetworkID()`, enabling CTID construction. When absent, the two-argument fallback is used, which marks the transaction located and valid but without a CTID. + +The `reason` string in the `Transaction` constructor is an out-parameter for capturing human-readable deserialization error descriptions. The function ignores its value after construction, relying on the constructors themselves to throw exceptions on unrecoverable parse failures rather than inspecting the error string. + +## `saveLedgerAsync` + +This is a one-liner wrapper around `getLedgerBySeq` plus `pendSaveValidated`. Its entire purpose is to handle the case where a paginated transaction scan encounters a ledger that exists in memory (or can be retrieved from the ledger master) but has not yet been written to the relational database. Without this callback, successive queries over the same range might repeatedly find ledger data absent from the database. + +The call to `pendSaveValidated(app, l, false, false)` passes `isSynchronous=false` and `isCurrent=false`, meaning the save is queued asynchronously and the ledger is not the current validated tip. This is intentional: blocking the paging query while a ledger saves to disk would degrade RPC response latency. The guard `if (auto l = ...)` silently does nothing if the ledger is unavailable from the master — appropriate because `getLedgerBySeq` can return null for pruned or not-yet-acquired ledgers, and the callback has no recovery path in that case. + +## Design Rationale + +Splitting these two functions into a `detail/` header-and-implementation pair, rather than inlining them as lambdas in `SQLiteDatabase.cpp`, keeps the blob-to-object deserialization logic in a single place that all database backends can share. The `RelationalDatabase::AccountTxs` type (`std::vector, shared_ptr>>`) is owned by the interface layer, so the conversion code correctly belongs in the application layer above the database backend rather than inside it. This also makes the deserialization path independently testable and prevents the SQLite-specific paging code from accumulating application-layer concerns. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/AccountTxPaging.h.ai.json b/src/xrpld/app/misc/detail/AccountTxPaging.h.ai.json new file mode 100644 index 0000000000..2abf4a2411 --- /dev/null +++ b/src/xrpld/app/misc/detail/AccountTxPaging.h.ai.json @@ -0,0 +1,35 @@ +{ + "args": [], + "classes": [], + "description": "This header file declares utility functions for processing account transaction data and saving ledger information asynchronously within the xrpl namespace, likely for use in the XRPL (XRP Ledger) server application.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/AccountTxPaging.h", + "functions": [ + { + "args": [ + "RelationalDatabase::AccountTxs& to", + "std::uint32_t ledger_index", + "std::string const& status", + "Blob const& rawTxn", + "Blob const& rawMeta", + "Application& app" + ], + "lineno": 10, + "name": "convertBlobsToTxResult" + }, + { + "args": [ + "Application& app", + "std::uint32_t seq" + ], + "lineno": 18, + "name": "saveLedgerAsync" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/AccountTxPaging.h.ai.md b/src/xrpld/app/misc/detail/AccountTxPaging.h.ai.md new file mode 100644 index 0000000000..bd7aa737a0 --- /dev/null +++ b/src/xrpld/app/misc/detail/AccountTxPaging.h.ai.md @@ -0,0 +1,37 @@ +# `AccountTxPaging.h` — Blob-to-Transaction Conversion and Ledger Save Callbacks + +This header lives in `src/xrpld/app/misc/detail/` and declares two small utility functions that bridge the gap between the raw SQLite storage layer and the high-level transaction object model used by the rest of the application. The `detail/` placement signals that these are internal helpers, not public API surfaces. + +## Role in the Paging Pipeline + +The XRPL SQLite backend stores transactions as raw serialized blobs — the binary wire encoding of the `STTx` object and its accompanying `TxMeta`. When a caller pages through an account's transaction history via `SQLiteDatabase::oldestAccountTxPage` or `newestAccountTxPage`, the SQL query engine works purely in terms of raw bytes and integers. The two functions declared here are passed as callbacks into that query engine so the reconstruction into rich C++ objects happens exactly once per row, right at the boundary where SQL row data leaves the database layer. + +## `convertBlobsToTxResult` + +```cpp +void convertBlobsToTxResult( + RelationalDatabase::AccountTxs& to, + std::uint32_t ledger_index, + std::string const& status, + Blob const& rawTxn, + Blob const& rawMeta, + Application& app); +``` + +`RelationalDatabase::AccountTxs` is `std::vector, std::shared_ptr>>`. This function deserializes `rawTxn` through a `SerialIter` into an `STTx`, wraps it in a `Transaction`, then deserializes `rawMeta` into a `TxMeta`. It then attempts to extract the `sfTransactionIndex` field from the metadata object — if that field is present, the CTID (Concise Transaction Identifier) can be computed from the ledger sequence, transaction index, and network ID, so `setStatus` is called with the full four-argument form. Without a valid transaction index, only the two-argument fallback form is used. The result pair is emplace-backed into `to`. + +The conditional CTID path is important: CTID is a compact, human-friendly transaction reference that requires knowing the transaction's position within its ledger. Metadata produced by older ledger versions or certain edge cases may lack `sfTransactionIndex`, so the code defensively handles both cases rather than asserting the field exists. + +## `saveLedgerAsync` + +```cpp +void saveLedgerAsync(Application& app, std::uint32_t seq); +``` + +During a paged account-transaction query, the SQL layer may encounter ledger sequence numbers that refer to validated ledgers which haven't yet been persisted to the database. When that happens, the paging engine calls this function with the missing sequence number. It looks the ledger up in `LedgerMaster` by sequence and, if found, calls `pendSaveValidated` with `isSynchronous=false` — scheduling the ledger's write to disk without blocking the current paging request. This is a lazy-persistence mechanism: the paging query can proceed immediately, and the ledger gets saved in the background so future queries will find it in the database without re-triggering the save. + +## Why These Are Separate Functions + +In `SQLiteDatabase.cpp`, each paging method constructs a lambda that closes over `ret` and `app` and calls `convertBlobsToTxResult`, and binds `saveLedgerAsync` via `std::bind` for the unsaved-ledger callback. Extracting these two operations into named free functions rather than embedding the deserialization logic inside the lambdas serves a few purposes: it keeps the lambda bodies trivially readable, it avoids duplicating identical deserialization logic across `oldestAccountTxPage` and `newestAccountTxPage`, and it places the application-layer logic (`Transaction`, `TxMeta`, `LedgerMaster`) at a clean boundary away from the SQL query machinery in the `detail` namespace under `rdb/backend`. + +The header's single include of `` is sufficient to name the `AccountTxs` parameter type; all heavier application-layer includes (`Transaction.h`, `LedgerMaster.h`, `Application.h`) are pushed into the `.cpp` translation unit, keeping include costs minimal for any code that needs only to declare these functions. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/AmendmentTable.cpp.ai.json b/src/xrpld/app/misc/detail/AmendmentTable.cpp.ai.json new file mode 100644 index 0000000000..ed7cca0084 --- /dev/null +++ b/src/xrpld/app/misc/detail/AmendmentTable.cpp.ai.json @@ -0,0 +1,742 @@ +{ + "args": [ + { + "lineno": 9, + "name": "section" + }, + { + "lineno": 61, + "name": "allTrusted" + }, + { + "lineno": 61, + "name": "lock" + }, + { + "lineno": 84, + "name": "rules" + }, + { + "lineno": 84, + "name": "valSet" + }, + { + "lineno": 84, + "name": "closeTime" + }, + { + "lineno": 84, + "name": "j" + }, + { + "lineno": 217, + "name": "amendment" + }, + { + "lineno": 292, + "name": "amendmentHash" + }, + { + "lineno": 388, + "name": "name" + }, + { + "lineno": 312, + "name": "vote" + }, + { + "lineno": 307, + "name": "state" + }, + { + "lineno": 307, + "name": "isAdmin" + }, + { + "lineno": 307, + "name": "v" + }, + { + "lineno": 592, + "name": "id" + }, + { + "lineno": 592, + "name": "fs" + }, + { + "lineno": 466, + "name": "enabled" + }, + { + "lineno": 492, + "name": "enabledAmendments" + }, + { + "lineno": 492, + "name": "majorityAmendments" + }, + { + "lineno": 492, + "name": "valSet" + }, + { + "lineno": 555, + "name": "ledgerSeq" + }, + { + "lineno": 563, + "name": "majority" + }, + { + "lineno": 622, + "name": "amendmentID" + }, + { + "lineno": 316, + "name": "registry" + }, + { + "lineno": 316, + "name": "majorityTime" + }, + { + "lineno": 316, + "name": "supported" + }, + { + "lineno": 316, + "name": "vetoed" + }, + { + "lineno": 316, + "name": "journal" + } + ], + "classes": [ + { + "args": [], + "lineno": 38, + "name": "TrustedVotes" + }, + { + "args": [], + "lineno": 186, + "name": "AmendmentState" + }, + { + "args": [ + "rules", + "trustedVotes", + "lock" + ], + "lineno": 202, + "name": "AmendmentSet" + }, + { + "args": [ + "registry", + "majorityTime", + "supported", + "enabled", + "vetoed", + "journal" + ], + "lineno": 259, + "name": "AmendmentTableImpl" + } + ], + "code_paths": [ + { + "call_chain": [ + "parseSection" + ], + "entry_point": "parseSection", + "purpose": "Parses a Section object containing amendment lines, validates and extracts amendment IDs and descriptions.", + "validation_points": [ + "boost::regex_match(line, match, re1) - validates line format", + "id.parseHex(match[1]) - validates amendment ID is valid hex" + ] + }, + { + "call_chain": [ + "AmendmentTable::add", + "parseSection" + ], + "entry_point": "AmendmentTable::add", + "purpose": "Adds amendments from a Section to the AmendmentTable, parsing and validating each entry.", + "validation_points": [ + "parseSection: regex and hex validation" + ] + }, + { + "call_chain": [ + "AmendmentTable::AmendmentTable", + "parseSection" + ], + "entry_point": "AmendmentTable::AmendmentTable (constructor)", + "purpose": "Initializes the AmendmentTable, parsing and validating amendments from config/Section.", + "validation_points": [ + "parseSection: regex and hex validation" + ] + } + ], + "data_flows": [ + { + "field": "line (from section.lines())", + "flow": [ + "Section::lines()", + "parseSection (for loop over lines)", + "regex_match(line, match, re1)", + "match[1] (amendment ID), match[2] (description)" + ], + "origin": "Section::lines() (likely from config file or similar source)", + "transformations": [ + "Regex extraction of amendment ID and description" + ], + "validated_at": "regex_match(line, match, re1)" + }, + { + "field": "match[1] (amendment ID)", + "flow": [ + "regex_match(line, match, re1)", + "match[1]", + "id.parseHex(match[1])", + "uint256 id" + ], + "origin": "Extracted from line by regex", + "transformations": [ + "String (hex) to uint256" + ], + "validated_at": "id.parseHex(match[1])" + }, + { + "field": "names (vector of (id, description))", + "flow": [ + "parseSection", + "return value", + "used by AmendmentTable::add or AmendmentTable::AmendmentTable" + ], + "origin": "Built in parseSection from validated lines", + "transformations": [ + "Aggregation of validated amendments" + ], + "validated_at": "parseSection (both regex and parseHex)" + } + ], + "description": "Implements the AmendmentTable and related logic for tracking, voting, and enabling protocol amendments in the XRPL server. Handles amendment state, validator votes, persistence, and JSON reporting.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "line (from section.lines())", + "empty", + "string", + "validation" + ], + "evidence": "boost::regex_match with re1 at parseSection", + "issue_pattern": "Missing empty string validation for line (from section.lines())", + "why_false_positive": "boost::regex_match with re1 validates line (from section.lines()) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "line (from section.lines())", + "format", + "validation", + "invalid" + ], + "evidence": "boost::regex_match with re1 at parseSection", + "issue_pattern": "Missing format validation for line (from section.lines())", + "why_false_positive": "boost::regex_match with re1 validates line (from section.lines()) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "match[1] (amendment ID)", + "empty", + "string", + "validation" + ], + "evidence": "uint256::parseHex at parseSection", + "issue_pattern": "Missing empty string validation for match[1] (amendment ID)", + "why_false_positive": "uint256::parseHex validates match[1] (amendment ID) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/AmendmentTable.cpp", + "functions": [ + { + "args": [ + "section" + ], + "lineno": 9, + "name": "parseSection" + }, + { + "args": [ + "allTrusted", + "lock" + ], + "lineno": 61, + "name": "trustChanged" + }, + { + "args": [ + "rules", + "valSet", + "closeTime", + "j", + "lock" + ], + "lineno": 84, + "name": "recordVotes" + }, + { + "args": [ + "rules", + "lock" + ], + "lineno": 168, + "name": "getVotes" + }, + { + "args": [ + "amendment" + ], + "lineno": 217, + "name": "passes" + }, + { + "args": [ + "amendment" + ], + "lineno": 229, + "name": "votes" + }, + { + "args": [], + "lineno": 237, + "name": "trustedValidations" + }, + { + "args": [], + "lineno": 241, + "name": "threshold" + }, + { + "args": [ + "amendment", + "lock" + ], + "lineno": 292, + "name": "add" + }, + { + "args": [ + "amendment", + "lock" + ], + "lineno": 297, + "name": "get" + }, + { + "args": [ + "amendment", + "lock" + ], + "lineno": 302, + "name": "get" + }, + { + "args": [ + "v", + "amendment", + "state", + "isAdmin", + "lock" + ], + "lineno": 307, + "name": "injectJson" + }, + { + "args": [ + "amendment", + "name", + "vote" + ], + "lineno": 312, + "name": "persistVote" + }, + { + "args": [ + "registry", + "majorityTime", + "supported", + "enabled", + "vetoed", + "journal" + ], + "lineno": 316, + "name": "AmendmentTableImpl" + }, + { + "args": [ + "name" + ], + "lineno": 388, + "name": "find" + }, + { + "args": [ + "amendment" + ], + "lineno": 406, + "name": "veto" + }, + { + "args": [ + "amendment" + ], + "lineno": 418, + "name": "unVeto" + }, + { + "args": [ + "amendment" + ], + "lineno": 430, + "name": "enable" + }, + { + "args": [ + "amendment" + ], + "lineno": 444, + "name": "isEnabled" + }, + { + "args": [ + "amendment" + ], + "lineno": 450, + "name": "isSupported" + }, + { + "args": [], + "lineno": 456, + "name": "hasUnsupportedEnabled" + }, + { + "args": [], + "lineno": 461, + "name": "firstUnsupportedExpected" + }, + { + "args": [ + "enabled" + ], + "lineno": 466, + "name": "doValidation" + }, + { + "args": [], + "lineno": 487, + "name": "getDesired" + }, + { + "args": [ + "rules", + "closeTime", + "enabledAmendments", + "majorityAmendments", + "valSet" + ], + "lineno": 492, + "name": "doVoting" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 555, + "name": "needValidatedLedger" + }, + { + "args": [ + "ledgerSeq", + "enabled", + "majority" + ], + "lineno": 563, + "name": "doValidatedLedger" + }, + { + "args": [ + "allTrusted" + ], + "lineno": 587, + "name": "trustChanged" + }, + { + "args": [ + "v", + "id", + "fs", + "isAdmin", + "lock" + ], + "lineno": 592, + "name": "injectJson" + }, + { + "args": [ + "isAdmin" + ], + "lineno": 610, + "name": "getJson" + }, + { + "args": [ + "amendmentID", + "isAdmin" + ], + "lineno": 622, + "name": "getJson" + }, + { + "args": [ + "registry", + "majorityTime", + "supported", + "enabled", + "vetoed", + "journal" + ], + "lineno": 635, + "name": "make_AmendmentTable" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic in parseSection is critical for config/Section parsing. Typical test coverage would be in unit tests for AmendmentTable or config parsing, e.g., AmendmentTable_test.cpp or ConfigSections_test.cpp. Tests should cover valid/invalid lines, invalid hex, and malformed entries. Gaps may exist if there are no tests for malformed amendment lines or invalid hex IDs. Exception handling paths (Throw) should be tested for negative cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "boost::regex, custom Throw<> exception, uint256::parseHex", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "line (from section.lines())", + "location": "parseSection", + "validated_by": "boost::regex_match with re1", + "validates": [ + "Line matches regex: optional whitespace, 64 hex chars, whitespace, non-whitespace description" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (via Throw<>)", + "field": "match[1] (amendment ID)", + "location": "parseSection", + "validated_by": "uint256::parseHex", + "validates": [ + "match[1] is a valid hexadecimal string for uint256" + ], + "validation_type": "format|type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/AmendmentTable.cpp.ai.md b/src/xrpld/app/misc/detail/AmendmentTable.cpp.ai.md new file mode 100644 index 0000000000..36922422ee --- /dev/null +++ b/src/xrpld/app/misc/detail/AmendmentTable.cpp.ai.md @@ -0,0 +1,67 @@ +# `src/xrpld/app/misc/detail/AmendmentTable.cpp` + +## Role and Purpose + +This file is the sole implementation of the XRP Ledger's amendment governance system. Amendments are named 256-bit feature flags that change transaction-processing rules once a supermajority of the network's trusted validators vote for them over a sustained window. `AmendmentTableImpl` implements the abstract `AmendmentTable` interface from `xrpl/ledger/AmendmentTable.h` and is the engine that drives every step of that lifecycle: collecting validator votes, tallying them, tracking majority windows, injecting governance pseudo-transactions into candidate ledgers, and persisting per-node vote preferences across restarts. + +## Internal Class Hierarchy + +Four cooperating types are defined entirely within this translation unit. + +### `parseSection()` + +A file-local helper that converts a `Section` (a key-value block from the config file) into a vector of `(uint256 id, string name)` pairs. It uses a compiled `boost::regex` to enforce the exact format expected — 64 hex digits, whitespace, a non-whitespace description — throwing `std::runtime_error` on any malformed line. Two validations are layered: the regex rejects anything that isn't structurally correct, and `uint256::parseHex` independently validates the hex string, making the config parser fail early with informative messages rather than silently producing wrong state. + +### `TrustedVotes` + +This class addresses a subtle liveness problem called "flapping." During consensus, validators broadcast `STValidation` messages that include the set of amendments they support. Near a flag ledger (where majority is computed), a validator that temporarily loses synchronisation will not broadcast. Without caching, its "yes" votes disappear, making an amendment appear to lose support even though the validator hasn't changed its opinion. + +`TrustedVotes` maintains a `hash_map`, keyed on validator identity. Each entry stores the most recent upvotes alongside an expiry timestamp. When `recordVotes()` is called at each voting round, it updates entries from incoming validations and sets their timeout 24 hours into the future. Entries that have not been refreshed within 24 hours are expired to `empty` — the code calls this "losing confidence." The 24h window is deliberately long: it means a flaky validator can be offline for up to 24 hours without disturbing the amendment vote record, so actual flapping can happen no more frequently than once per day from any single validator. + +The `trustChanged()` method rebuilds the map when the UNL changes, preserving existing vote history for validators still in the UNL and dropping data for validators that have been removed. + +Critically, every public method requires a `std::lock_guard const&` parameter, passing the lock by reference. This is a deliberate API contract: the caller must hold `AmendmentTableImpl::mutex_` before calling these methods. The lock is taken externally, not internally, which prevents lock inversion and makes the synchronisation relationship visible at compile time. + +### `AmendmentSet` + +A snapshot of one voting round. Its constructor calls `TrustedVotes::getVotes()` to get the per-amendment vote counts and the number of validators with active (non-expired) votes. The threshold is derived from `amendmentMajorityCalcThreshold`, which is `std::ratio<80, 100>` — 80% of active validators must vote yes. + +The `passes()` predicate applies a strict "greater than threshold" test (`votes > threshold`), with a single exception: when there is exactly one trusted validator, `>=` is used instead of `>`, since achieving more than 100% is mathematically impossible. This edge-case handling ensures single-validator test networks and early bootstrap configurations still work correctly. + +### `AmendmentState` + +A plain struct holding the current local knowledge about one amendment: whether it is `enabled` (a one-way flag — once true, never reset), whether this server has code `supported` for it, the local `vote` preference (`up`, `down`, or `obsolete`), and the human-readable name. `enabled` being one-way reflects the protocol guarantee that amendments are irreversible once activated. + +## `AmendmentTableImpl` — Core Logic + +### Construction and Persistence Migration + +The constructor integrates with `wallet.db` through three functions from `xrpl/server/Wallet.h`: `createFeatureVotes`, `readAmendments`, and `voteAmendment`. On startup, `createFeatureVotes` is called to create the `FeatureVotes` table if it does not already exist and returns whether the table previously existed. This drives a one-time migration: if the table is new, config-file `[amendments]` and `[veto_amendments]` sections are parsed and written into the database. If the table already exists, the config sections are silently ignored (with a warning) because the database is the authoritative source. This migration avoids double-applying config on restart and moves toward a fully database-driven governance state. + +An invariant respected throughout: once an amendment's `vote` is set to `obsolete` (for features being phased out), no subsequent `veto()`, `unVeto()`, or DB read can override it. The `obsolete` state also suppresses the amendment from appearing in `doValidation()` output, so the server will never broadcast a vote for it. + +### `doValidation()` — Populating Outbound Validations + +Called by consensus during validation construction, this method returns the list of amendment IDs that should be included in the local `STValidation` broadcast. The filter is: amendments that are `supported`, not vetoed (`vote == up`), and not already `enabled` on the current ledger. The result is sorted for determinism. Callers embed this list in the `sfAmendments` field of the validation message. + +### `doVoting()` — The Core Tally + +Called at every flag ledger, this is where `TrustedVotes::recordVotes()` is invoked to update cached validator votes, an `AmendmentSet` is constructed to snapshot the tally, and then a decision is reached for each known amendment: + +- If validators reach supermajority and the ledger does not yet record majority: `tfGotMajority` +- If the ledger records majority but validators no longer agree: `tfLostMajority` +- If the ledger records majority *and* the majority window has elapsed (closeTime ≥ majorityTime + `majorityTime_`) and the local node votes yes: flag `0` (trigger enablement) + +The return is a `std::map` of amendment IDs to flag values. The caller in `AmendmentTable.h`'s non-virtual `doVoting()` adapter translates each entry into a `ttAMENDMENT` pseudo-transaction injected into the initial ledger position before consensus begins. Keeping the map construction separate from the pseudo-transaction injection is a deliberate layering: `AmendmentTableImpl` stays independent of the ledger and SHAMap code. + +### `doValidatedLedger()` and `needValidatedLedger()` + +After consensus, `doValidatedLedger()` is called with the full set of enabled amendments and the current majority-in-progress set from the validated ledger. `enable()` is called for each newly enabled amendment; if any is unsupported, `unsupportedEnabled_` is set to `true`. The method also recomputes `firstUnsupportedExpected_` — the earliest wall-clock time at which an unsupported amendment will be forcibly enabled. This lets the server surface a degradation warning well before the event. + +`needValidatedLedger()` avoids processing every ledger by checking whether the current and previous sequences fall within the same 256-ledger band `((seq - 1) / 256)`. Flag ledgers occur at multiples of 256, so amendment state can only change when that quotient changes. + +## Concurrency Model + +All mutable state in `AmendmentTableImpl` is protected by a single `mutable std::mutex mutex_`. Read-only query methods (`isEnabled`, `isSupported`, `hasUnsupportedEnabled`, `firstUnsupportedExpected`, `getJson`) acquire the mutex using `std::lock_guard` on entry and hold it for the duration. Methods that must delegate to `TrustedVotes` acquire `mutex_` first and then pass the `lock_guard` reference into `TrustedVotes` methods — ensuring that `TrustedVotes` internal state is always accessed under the outer lock and never acquires its own, eliminating any possibility of nested locking or deadlock. + +The `lastVote_` pointer, a `std::unique_ptr`, is replaced atomically under the lock at the end of `doVoting()`. It is read under the lock in `injectJson()` to populate vote counts in admin-facing JSON responses. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/DeliverMax.cpp.ai.json b/src/xrpld/app/misc/detail/DeliverMax.cpp.ai.json new file mode 100644 index 0000000000..6b9dac7b7c --- /dev/null +++ b/src/xrpld/app/misc/detail/DeliverMax.cpp.ai.json @@ -0,0 +1,240 @@ +{ + "args": [ + { + "lineno": 8, + "name": "tx_json" + }, + { + "lineno": 8, + "name": "txnType" + }, + { + "lineno": 8, + "name": "apiVersion" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "insertDeliverMax" + ], + "entry_point": "insertDeliverMax", + "purpose": "Handles the migration of the 'Amount' field to 'DeliverMax' in payment transactions, depending on API version.", + "validation_points": [ + "tx_json.isMember(jss::Amount) - Validates presence of 'Amount' field", + "txnType == ttPAYMENT - Validates transaction type is PAYMENT", + "apiVersion > 1 - Validates API version is greater than 1" + ] + } + ], + "data_flows": [ + { + "field": "Amount", + "flow": [ + "tx_json['Amount'] (input)", + "Checked by tx_json.isMember(jss::Amount)", + "If valid and txnType == ttPAYMENT, copied to tx_json['DeliverMax']", + "If apiVersion > 1, tx_json['Amount'] is removed" + ], + "origin": "Input JSON (tx_json) provided to insertDeliverMax", + "transformations": [ + "Copied to 'DeliverMax' field if conditions met", + "Removed from JSON if apiVersion > 1" + ], + "validated_at": "tx_json.isMember(jss::Amount)" + }, + { + "field": "txnType", + "flow": [ + "txnType (argument)", + "Compared to ttPAYMENT" + ], + "origin": "Function argument to insertDeliverMax", + "transformations": [ + "Used as a condition to gate copying/removal logic" + ], + "validated_at": "txnType == ttPAYMENT" + }, + { + "field": "apiVersion", + "flow": [ + "apiVersion (argument)", + "Compared to 1" + ], + "origin": "Function argument to insertDeliverMax", + "transformations": [ + "If > 1, triggers removal of 'Amount' from tx_json" + ], + "validated_at": "apiVersion > 1" + } + ], + "description": "Provides a utility function to insert or modify the DeliverMax field in a transaction JSON object for payment transactions, depending on the API version.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Amount", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::isMember at insertDeliverMax", + "issue_pattern": "Missing empty string validation for Amount", + "why_false_positive": "Json::Value::isMember validates Amount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "txnType", + "empty", + "string", + "validation" + ], + "evidence": "explicit comparison (txnType == ttPAYMENT) at insertDeliverMax", + "issue_pattern": "Missing empty string validation for txnType", + "why_false_positive": "explicit comparison (txnType == ttPAYMENT) validates txnType for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "apiVersion", + "empty", + "string", + "validation" + ], + "evidence": "explicit comparison (apiVersion > 1) at insertDeliverMax", + "issue_pattern": "Missing empty string validation for apiVersion", + "why_false_positive": "explicit comparison (apiVersion > 1) validates apiVersion for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/DeliverMax.cpp", + "functions": [ + { + "args": [ + "tx_json", + "txnType", + "apiVersion" + ], + "lineno": 7, + "name": "insertDeliverMax" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "RPC" + } + ], + "test_coverage_notes": "The function is simple and likely tested indirectly via higher-level transaction processing or API tests. Direct unit tests for insertDeliverMax may not exist unless specifically written in the test suite for DeliverMax or payment transaction handling. Gaps may exist if there are no tests for edge cases (e.g., missing 'Amount', non-PAYMENT txnType, apiVersion boundary). Test files to check: any under 'test' or 'unittest' directories related to payment transactions, DeliverMax, or JSON transaction construction.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "JsonCpp (Json::Value), jss:: (field name constants)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (conditional branch, no exception)", + "field": "Amount", + "location": "insertDeliverMax", + "validated_by": "Json::Value::isMember", + "validates": [ + "Checks if the 'Amount' field exists in the input JSON object before proceeding" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional branch, no exception)", + "field": "txnType", + "location": "insertDeliverMax", + "validated_by": "explicit comparison (txnType == ttPAYMENT)", + "validates": [ + "Checks if the transaction type is 'ttPAYMENT' before copying 'Amount' to 'DeliverMax'" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (conditional branch, no exception)", + "field": "apiVersion", + "location": "insertDeliverMax", + "validated_by": "explicit comparison (apiVersion > 1)", + "validates": [ + "Checks if the API version is greater than 1 before removing the 'Amount' field" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/DeliverMax.cpp.ai.md b/src/xrpld/app/misc/detail/DeliverMax.cpp.ai.md new file mode 100644 index 0000000000..d93e60abb9 --- /dev/null +++ b/src/xrpld/app/misc/detail/DeliverMax.cpp.ai.md @@ -0,0 +1,35 @@ +# `DeliverMax.cpp` — API Field Alias for Payment Amount + +This file implements a single utility function, `insertDeliverMax()`, that manages the transition of the `Amount` field in `Payment` transaction JSON from its legacy name to its semantically clearer successor, `DeliverMax`. The rename was introduced to make clear that the field represents the *maximum* amount a sender is willing to deliver — crucial for cross-currency and partial payments where actual delivery may differ — rather than an unambiguous delivered quantity. + +## What the Function Does + +`insertDeliverMax(Json::Value& tx_json, TxType txnType, unsigned int apiVersion)` operates on the outbound JSON representation of a transaction before it is returned to an API caller. Its logic is governed by two independent guards: + +1. **Transaction type check** (`txnType == ttPAYMENT`): The `Amount` field exists on several transaction types (e.g., `OfferCreate`, `EscrowCreate`), where it carries entirely different semantics. The rename to `DeliverMax` is only meaningful for payments, so all other types are ignored. + +2. **API version gate** (`apiVersion > 1`): For backward compatibility with API v1 clients, the function copies `Amount` into `DeliverMax` but leaves the original `Amount` field intact, so old clients continue to work. For API v2 and later, `Amount` is then removed — callers are required to read `DeliverMax` exclusively. This is a clean break enforced at the serialization boundary rather than in the protocol layer itself. + +## Design Rationale + +The decision to handle this at the JSON-output layer — rather than renaming the field in the `STTx` object or the ledger's binary serialization — is architecturally deliberate. The canonical on-ledger representation and wire format remain unchanged (`Amount` is the protocol-level field name). The rename is purely a presentation concern for API consumers, so it belongs in the RPC serialization path. This avoids any consensus-layer or storage format changes while still delivering a cleaner API surface. + +The function is placed in `xrpl::RPC`, the namespace for RPC-layer utilities, even though the header lives in `app/misc/` — reflecting the mixed layering common to XRPL's RPC helpers. + +## Call Sites + +`insertDeliverMax()` is called from at least five separate RPC handlers: + +- `LedgerToJson.cpp` — when serializing ledger contents including transaction history +- `Tx.cpp` — the `tx` command, for individual transaction lookup +- `AccountTx.cpp` — account transaction history +- `TransactionEntry.cpp` — transaction lookup within a specific ledger +- `TxHistory.cpp` — legacy transaction history endpoint + +Each call site retrieves `context.apiVersion` from the active RPC context and the transaction type from the parsed `STTx` object, then passes both directly into this function. The uniformity of these call sites is intentional: rather than duplicating the version-gate logic in every handler, this single function centralizes the field migration policy. + +## Invariants and Edge Cases + +The function is entirely silent on failure — if `tx_json` lacks an `Amount` member, or if the transaction type is not `ttPAYMENT`, it returns without modification and without logging. This is appropriate because the function is called on all transactions during serialization, and silence for non-payment types is correct behavior, not an error condition. + +The `jss::Amount` and `jss::DeliverMax` field name constants come from the compile-time string table in `jss.h`, where `DeliverMax` is explicitly documented as an alias to `Amount`. This ensures field name strings are consistent across the entire codebase and eliminates raw string literals from the RPC layer. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/Transaction.cpp.ai.json b/src/xrpld/app/misc/detail/Transaction.cpp.ai.json new file mode 100644 index 0000000000..ee776aafda --- /dev/null +++ b/src/xrpld/app/misc/detail/Transaction.cpp.ai.json @@ -0,0 +1,465 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Transaction::transactionFromSQL", + "rangeCheckedCast", + "STTx constructor", + "Transaction::Transaction", + "Transaction::setStatus", + "Transaction::sqlTransactionStatus" + ], + "entry_point": "Transaction::transactionFromSQL", + "purpose": "Deserializes a transaction from SQL, validates fields, constructs Transaction object.", + "validation_points": [ + "rangeCheckedCast (ledgerSeq validation)", + "STTx constructor (mTransaction validation)", + "safe_cast in sqlTransactionStatus (status validation)" + ] + }, + { + "call_chain": [ + "Transaction::load", + "RelationalDatabase::getTransaction", + "Transaction::transactionFromSQL" + ], + "entry_point": "Transaction::load", + "purpose": "Loads a transaction from the database, triggers deserialization and validation.", + "validation_points": [ + "Transaction::transactionFromSQL (see above)" + ] + }, + { + "call_chain": [ + "Transaction::Transaction", + "mTransaction->getTransactionID()" + ], + "entry_point": "Transaction::Transaction", + "purpose": "Constructs a Transaction object, validates mTransaction via STTx.", + "validation_points": [ + "STTx constructor (mTransaction validation)" + ] + }, + { + "call_chain": [ + "Transaction::setStatus" + ], + "entry_point": "Transaction::setStatus", + "purpose": "Sets status and related fields on Transaction.", + "validation_points": [] + }, + { + "call_chain": [ + "Transaction::sqlTransactionStatus", + "safe_cast" + ], + "entry_point": "Transaction::sqlTransactionStatus", + "purpose": "Converts SQL status string to enum, validates via safe_cast.", + "validation_points": [ + "safe_cast (status validation)" + ] + } + ], + "data_flows": [ + { + "field": "mTransaction (STTx const& stx)", + "flow": [ + "rawTxn (input to transactionFromSQL)", + "SerialIter it(makeSlice(rawTxn))", + "std::make_shared(it)", + "Transaction::Transaction (mTransaction = stx)" + ], + "origin": "Deserialized from rawTxn in transactionFromSQL", + "transformations": [ + "Deserialization from binary blob", + "Validation in STTx constructor" + ], + "validated_at": "STTx constructor" + }, + { + "field": "ledgerSeq (boost::optional)", + "flow": [ + "ledgerSeq (input)", + "rangeCheckedCast(ledgerSeq.value_or(0))", + "Transaction::setLedger" + ], + "origin": "Input to transactionFromSQL", + "transformations": [ + "Optional to uint64_t to uint32_t conversion", + "Range checking" + ], + "validated_at": "rangeCheckedCast" + }, + { + "field": "status (boost::optional)", + "flow": [ + "status (input)", + "Transaction::sqlTransactionStatus(status)", + "safe_cast((*status)[0])", + "TransStatus enum" + ], + "origin": "Input to transactionFromSQL", + "transformations": [ + "String to enum conversion", + "Validation via safe_cast" + ], + "validated_at": "safe_cast in sqlTransactionStatus" + }, + { + "field": "mLedgerIndex", + "flow": [ + "Transaction::setStatus or setLedger", + "mLedgerIndex", + "Transaction::getJson" + ], + "origin": "Set in setStatus or setLedger", + "transformations": [ + "Assignment", + "Output in JSON" + ], + "validated_at": "Indirectly via rangeCheckedCast in transactionFromSQL" + } + ], + "description": "Implements the Transaction class and related functions for handling, loading, and serializing transactions in the XRPL application, including status management, SQL conversion, and JSON output.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "STTx fields (via deserialization/constructor)", + "validation", + "missing", + "check" + ], + "evidence": "Field STTx fields (via deserialization/constructor) validated by STTx (transaction object validation), safe_cast (type/range), rangeCheckedCast (type/range), XRPL_ASSERT (assertion)", + "issue_pattern": "Missing validation for STTx fields (via deserialization/constructor)", + "why_false_positive": "STTx (transaction object validation), safe_cast (type/range), rangeCheckedCast (type/range), XRPL_ASSERT (assertion) validates STTx fields (via deserialization/constructor) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledgerSeq (via rangeCheckedCast)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledgerSeq (via rangeCheckedCast) validated by STTx (transaction object validation), safe_cast (type/range), rangeCheckedCast (type/range), XRPL_ASSERT (assertion)", + "issue_pattern": "Missing validation for ledgerSeq (via rangeCheckedCast)", + "why_false_positive": "STTx (transaction object validation), safe_cast (type/range), rangeCheckedCast (type/range), XRPL_ASSERT (assertion) validates ledgerSeq (via rangeCheckedCast) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "status (via safe_cast and enum mapping)", + "validation", + "missing", + "check" + ], + "evidence": "Field status (via safe_cast and enum mapping) validated by STTx (transaction object validation), safe_cast (type/range), rangeCheckedCast (type/range), XRPL_ASSERT (assertion)", + "issue_pattern": "Missing validation for status (via safe_cast and enum mapping)", + "why_false_positive": "STTx (transaction object validation), safe_cast (type/range), rangeCheckedCast (type/range), XRPL_ASSERT (assertion) validates status (via safe_cast and enum mapping) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "mTransaction (STTx const& stx)", + "empty", + "string", + "validation" + ], + "evidence": "STTx constructor (deserialization/validation) at Transaction::Transaction (constructor)", + "issue_pattern": "Missing empty string validation for mTransaction (STTx const& stx)", + "why_false_positive": "STTx constructor (deserialization/validation) validates mTransaction (STTx const& stx) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledgerSeq (boost::optional)", + "empty", + "string", + "validation" + ], + "evidence": "rangeCheckedCast at Transaction::transactionFromSQL", + "issue_pattern": "Missing empty string validation for ledgerSeq (boost::optional)", + "why_false_positive": "rangeCheckedCast validates ledgerSeq (boost::optional) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "status (boost::optional)", + "empty", + "string", + "validation" + ], + "evidence": "safe_cast at Transaction::sqlTransactionStatus", + "issue_pattern": "Missing empty string validation for status (boost::optional)", + "why_false_positive": "safe_cast validates status (boost::optional) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/Transaction.cpp", + "functions": [ + { + "args": [ + "std::shared_ptr const& stx", + "std::string& reason", + "Application& app" + ], + "lineno": 12, + "name": "Transaction::Transaction" + }, + { + "args": [ + "TransStatus ts", + "std::uint32_t lseq", + "std::optional tseq", + "std::optional netID" + ], + "lineno": 32, + "name": "Transaction::setStatus" + }, + { + "args": [ + "boost::optional const& status" + ], + "lineno": 44, + "name": "Transaction::sqlTransactionStatus" + }, + { + "args": [ + "boost::optional const& ledgerSeq", + "boost::optional const& status", + "Blob const& rawTxn", + "Application& app" + ], + "lineno": 67, + "name": "Transaction::transactionFromSQL" + }, + { + "args": [ + "uint256 const& id", + "Application& app", + "error_code_i& ec" + ], + "lineno": 81, + "name": "Transaction::load" + }, + { + "args": [ + "uint256 const& id", + "Application& app", + "ClosedInterval const& range", + "error_code_i& ec" + ], + "lineno": 86, + "name": "Transaction::load" + }, + { + "args": [ + "uint256 const& id", + "Application& app", + "std::optional> const& range", + "error_code_i& ec" + ], + "lineno": 92, + "name": "Transaction::load" + }, + { + "args": [ + "JsonOptions options", + "bool binary" + ], + "lineno": 104, + "name": "Transaction::getJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation paths (STTx deserialization, rangeCheckedCast, safe_cast) are likely covered by integration and unit tests for transaction deserialization, SQL import/export, and ledger replay. Look for tests in files like Transaction_test.cpp, STTx_test.cpp, and database integration tests. However, edge cases for invalid ledgerSeq, malformed status strings, and corrupted rawTxn blobs may not be exhaustively tested. Exception handling in Transaction::Transaction (reason string) may not be directly tested unless error injection is used. Template-based validation (rangeCheckedCast, safe_cast) should have their own unit tests, but coverage for all possible input types and error cases should be verified.", + "validation_architecture": { + "auto_validated_fields": [ + "STTx fields (via deserialization/constructor)", + "ledgerSeq (via rangeCheckedCast)", + "status (via safe_cast and enum mapping)" + ], + "framework": "STTx (transaction object validation), safe_cast (type/range), rangeCheckedCast (type/range), XRPL_ASSERT (assertion)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "std::exception (on getTransactionID)", + "field": "mTransaction (STTx const& stx)", + "location": "Transaction::Transaction (constructor)", + "validated_by": "STTx constructor (deserialization/validation)", + "validates": [ + "STTx is well-formed and can be deserialized", + "Transaction ID can be computed (fields present and valid)" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "throws if out of uint32_t range", + "field": "ledgerSeq (boost::optional)", + "location": "Transaction::transactionFromSQL", + "validated_by": "rangeCheckedCast", + "validates": [ + "ledgerSeq is within uint32_t range" + ], + "validation_type": "range|type" + }, + { + "confidence": 0.9, + "error_thrown": "XRPL_ASSERT (assertion failure if unknown status)", + "field": "status (boost::optional)", + "location": "Transaction::sqlTransactionStatus", + "validated_by": "safe_cast", + "validates": [ + "status[0] is a valid TxnSql enum value" + ], + "validation_type": "type|enum mapping" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/Transaction.cpp.ai.md b/src/xrpld/app/misc/detail/Transaction.cpp.ai.md new file mode 100644 index 0000000000..4ccd574a46 --- /dev/null +++ b/src/xrpld/app/misc/detail/Transaction.cpp.ai.md @@ -0,0 +1,41 @@ +# `Transaction.cpp` — Application-Layer Transaction Wrapper + +## Role and Context + +`Transaction.cpp` implements the `Transaction` class — the application-level shell that wraps the protocol-level `STTx` and tracks everything the node needs to know about a transaction's lifecycle: where it sits in the ledger, what status it carries, and how to serialize it for RPC clients. While `STTx` in `xrpl/protocol` is a pure serialization type that knows nothing about ledgers or databases, `Transaction` layers on ledger placement (`mLedgerIndex`), position within that ledger (`mTxnSeq`), submission bookkeeping (`SubmitResult`, `mApplying`), and the plumbing that connects all of that to the relational database store. + +## Construction and Failure Signaling + +The constructor is declared `noexcept` yet contains a `try/catch` block — an intentional design where failure is communicated via the `reason` output parameter rather than through exception propagation. If `getTransactionID()` throws (because the `STTx` is malformed or incomplete), the reason string is populated and the object is left in a partially constructed state with `mStatus` unset (it never reaches `NEW`). Callers must check whether the resulting `shared_ptr` is usable before proceeding. This is preferable to throwing in a constructor of a reference-counted type because it avoids the complexity of exception-safe resource cleanup in every call site. + +## Status Lifecycle and SQL Mapping + +The `TransStatus` enum in the header defines nine states: `NEW`, `INVALID`, `INCLUDED`, `CONFLICTED`, `COMMITTED`, `HELD`, `REMOVED`, `OBSOLETE`, and `INCOMPLETE`. These represent the full lifecycle from first receipt through final ledger inclusion or discard. + +When transactions are persisted to and retrieved from the relational database, their status is stored as a single character string (the `TxnSql` encoding). `sqlTransactionStatus()` handles the reverse mapping, using `safe_cast` on the first character of the status string. It bridges an intentional impedance mismatch: `boost::optional` is used — not `std::optional` — because SOCI, the SQL library used for database access, requires `boost::optional` in its result binding interface. The `XRPL_ASSERT` on the `txnSqlUnknown` default ensures that any unrecognized database value will be caught in non-production builds. + +## Deserialization from the Database: `transactionFromSQL` + +`transactionFromSQL()` is the canonical factory for reconstructing a `Transaction` from rows returned by the SQL layer. It performs three sequential validation steps, each targeting a different type of corruption: + +1. `rangeCheckedCast(ledgerSeq.value_or(0))` — ensures the `uint64_t` ledger sequence from the database actually fits in the `uint32_t` used everywhere else in the protocol. A missing ledger sequence is treated as zero, consistent with the "not yet in a ledger" state. +2. `std::make_shared(SerialIter it)` — deserializes the raw blob via `STTx`'s serialized-form constructor, which validates structure and field types. +3. `sqlTransactionStatus(status)` — converts the stored status character to the in-memory enum as described above. + +This layered approach means a corrupted database row will fail at the earliest point where the specific corruption is detectable, with each failure mode producing a different kind of diagnostic. + +## The `load()` Overload Chain + +Three public/private `load()` static methods form a small dispatch chain. The two public overloads — one without a range, one with a `ClosedInterval` — both normalize to `std::optional>` and call the private third overload, which delegates directly to `RelationalDatabase::getTransaction()`. This design keeps the public API ergonomic (callers don't need to wrap their range in an optional) while centralizing the actual database call. The return type — `std::variant, TxSearched>` — distinguishes between finding a transaction, not finding it but having searched all ledgers in the range (`TxSearched::All`), or not finding it with only a partial search (`TxSearched::Some` or `TxSearched::Unknown`). This gives RPC handlers enough information to give meaningful responses about whether the transaction definitely does not exist versus whether the answer is simply unknown. + +## JSON Output and the CTID + +`getJson()` is the most complex method in the file. It builds on `STTx::getJson()` but explicitly strips the `include_date` flag before forwarding to the inner call, then re-adds the date at the outer level via `LedgerMaster::getCloseTimeBySeq()`. This separation exists because `STTx` has no ledger awareness; close-time lookup requires application context that `STTx` cannot access. + +The method also handles API versioning: the deprecated `inLedger` field is emitted only when the `disable_API_prior_V2` option is absent, ensuring older clients still receive the field while newer ones see only `ledger_index`. A commented TODO notes a planned `disable_API_prior_V3` that would also suppress both `date` and `ledger_index`. + +The CTID computation (XLS-15d, Concise Transaction Identifier) encodes ledger sequence, transaction position within that ledger, and network ID into a 16-hex-digit string. The priority logic is deliberate: if the `STTx` itself carries an `sfNetworkID` field, that value overrides `mNetworkID` stored on the `Transaction` object. Transactions on networks that set `sfNetworkID` are therefore self-identifying regardless of how the node's local network configuration is set, which matters in multi-network deployments and replay scenarios. `RPC::encodeCTID()` returns `std::nullopt` if any component exceeds its bit-field limit (ledger sequence above 28 bits, indices or network ID above 16 bits), so the CTID is only emitted when all components are representable. + +## Concurrency Note + +The `mApplying` flag and `SubmitResult` fields in the header (not implemented here, but part of the same class) are documented to be accessed exclusively under `NetworkOPsImp`'s own lock rather than a lock dedicated to `Transaction`. The comment in the header explicitly accepts weak consistency: a race on `mApplying` at worst causes a transaction to be attempted twice, which the engine handles gracefully with `tefALREADY`. This is a conscious performance tradeoff — avoiding a per-transaction lock when the consequences of the race are benign. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/TxQ.cpp.ai.json b/src/xrpld/app/misc/detail/TxQ.cpp.ai.json new file mode 100644 index 0000000000..19de7a1780 --- /dev/null +++ b/src/xrpld/app/misc/detail/TxQ.cpp.ai.json @@ -0,0 +1,878 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TxQ::FeeMetrics::update", + "getFeeLevelPaid", + "calculateBaseFee", + "tx[sfFee].xrp()", + "mulDiv" + ], + "entry_point": "TxQ::FeeMetrics::update", + "purpose": "Updates fee metrics for the transaction queue by calculating fee levels for all transactions in the ledger view.", + "validation_points": [ + "getFeeLevelPaid: baseFee.signum() > 0 (XRPL_ASSERT)", + "getFeeLevelPaid: effectiveFeePaid.signum() <= 0 || baseFee.signum() <= 0 (early return)", + "getFeeLevelPaid: mulDiv result checked with value_or" + ] + }, + { + "call_chain": [ + "getFeeLevelPaid", + "calculateBaseFee", + "tx[sfFee].xrp()", + "mulDiv" + ], + "entry_point": "getFeeLevelPaid", + "purpose": "Calculates the fee level paid for a transaction, normalizing the fee paid by the base fee.", + "validation_points": [ + "baseFee.signum() > 0 (XRPL_ASSERT)", + "effectiveFeePaid.signum() <= 0 || baseFee.signum() <= 0 (early return)", + "mulDiv result checked with value_or" + ] + }, + { + "call_chain": [ + "TxQ::FeeMetrics::scaleFeeLevel", + "increase" + ], + "entry_point": "TxQ::FeeMetrics::scaleFeeLevel", + "purpose": "Scales a fee level by a percentage increase.", + "validation_points": [ + "increase: mulDiv result checked with value_or" + ] + } + ], + "data_flows": [ + { + "field": "sfFee", + "flow": [ + "STTx (tx)", + "tx[sfFee].xrp()", + "feePaid (in getFeeLevelPaid)", + "effectiveFeePaid" + ], + "origin": "STTx (transaction object)", + "transformations": [ + "Extracted as XRPAmount", + "Potentially incremented by mod (if baseFee is zero)" + ], + "validated_at": "getFeeLevelPaid: effectiveFeePaid.signum() <= 0" + }, + { + "field": "baseFee", + "flow": [ + "calculateBaseFee(view, tx)", + "baseFee (in getFeeLevelPaid)", + "baseFee + mod" + ], + "origin": "calculateBaseFee(view, tx)", + "transformations": [ + "May be incremented by mod if baseFee is zero" + ], + "validated_at": "getFeeLevelPaid: baseFee.signum() > 0 (XRPL_ASSERT)" + }, + { + "field": "effectiveFeePaid", + "flow": [ + "feePaid + mod", + "effectiveFeePaid", + "mulDiv(effectiveFeePaid, TxQ::baseLevel, baseFee)" + ], + "origin": "feePaid + mod", + "transformations": [ + "Sum of feePaid and mod" + ], + "validated_at": "getFeeLevelPaid: effectiveFeePaid.signum() <= 0" + }, + { + "field": "mulDiv result", + "flow": [ + "mulDiv(...)", + "FeeLevel64 (return value of getFeeLevelPaid)" + ], + "origin": "mulDiv(effectiveFeePaid, TxQ::baseLevel, baseFee)", + "transformations": [ + "If mulDiv fails, value_or returns max uint64" + ], + "validated_at": "getFeeLevelPaid: .value_or(FeeLevel64(max))" + }, + { + "field": "sfLastLedgerSequence", + "flow": [ + "STTx (tx)", + "tx.isFieldPresent(sfLastLedgerSequence)", + "tx.getFieldU32(sfLastLedgerSequence)" + ], + "origin": "STTx (transaction object)", + "transformations": [ + "Optional extraction" + ], + "validated_at": "getLastLedgerSequence: presence checked before extraction" + } + ], + "description": "Implements the transaction queue (TxQ) logic for the XRPL server, managing queuing, prioritization, and application of transactions based on fee levels, account state, and ledger conditions.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "STTx field presence/type (via isFieldPresent/getFieldU32)", + "validation", + "missing", + "check" + ], + "evidence": "Field STTx field presence/type (via isFieldPresent/getFieldU32) validated by XRPL protocol (STTx, XRPAmount, XRPL_ASSERT, mulDiv), custom assertion and type-safe accessors", + "issue_pattern": "Missing validation for STTx field presence/type (via isFieldPresent/getFieldU32)", + "why_false_positive": "XRPL protocol (STTx, XRPAmount, XRPL_ASSERT, mulDiv), custom assertion and type-safe accessors validates STTx field presence/type (via isFieldPresent/getFieldU32) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "XRPAmount signum (positive/zero/negative)", + "validation", + "missing", + "check" + ], + "evidence": "Field XRPAmount signum (positive/zero/negative) validated by XRPL protocol (STTx, XRPAmount, XRPL_ASSERT, mulDiv), custom assertion and type-safe accessors", + "issue_pattern": "Missing validation for XRPAmount signum (positive/zero/negative)", + "why_false_positive": "XRPL protocol (STTx, XRPAmount, XRPL_ASSERT, mulDiv), custom assertion and type-safe accessors validates XRPAmount signum (positive/zero/negative) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "mulDiv overflow checking", + "validation", + "missing", + "check" + ], + "evidence": "Field mulDiv overflow checking validated by XRPL protocol (STTx, XRPAmount, XRPL_ASSERT, mulDiv), custom assertion and type-safe accessors", + "issue_pattern": "Missing validation for mulDiv overflow checking", + "why_false_positive": "XRPL protocol (STTx, XRPAmount, XRPL_ASSERT, mulDiv), custom assertion and type-safe accessors validates mulDiv overflow checking automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfFee (transaction fee)", + "empty", + "string", + "validation" + ], + "evidence": "tx[sfFee].xrp() at getFeeLevelPaid", + "issue_pattern": "Missing empty string validation for sfFee (transaction fee)", + "why_false_positive": "tx[sfFee].xrp() validates sfFee (transaction fee) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "baseFee (calculated base fee)", + "empty", + "string", + "validation" + ], + "evidence": "calculateBaseFee(view, tx) at getFeeLevelPaid", + "issue_pattern": "Missing empty string validation for baseFee (calculated base fee)", + "why_false_positive": "calculateBaseFee(view, tx) validates baseFee (calculated base fee) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "baseFee.signum()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(baseFee.signum() > 0, ...) at getFeeLevelPaid", + "issue_pattern": "Missing empty string validation for baseFee.signum()", + "why_false_positive": "XRPL_ASSERT(baseFee.signum() > 0, ...) validates baseFee.signum() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "effectiveFeePaid.signum()", + "empty", + "string", + "validation" + ], + "evidence": "if (effectiveFeePaid.signum() <= 0 || baseFee.signum() <= 0) at getFeeLevelPaid", + "issue_pattern": "Missing empty string validation for effectiveFeePaid.signum()", + "why_false_positive": "if (effectiveFeePaid.signum() <= 0 || baseFee.signum() <= 0) validates effectiveFeePaid.signum() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "mulDiv result", + "empty", + "string", + "validation" + ], + "evidence": "mulDiv(effectiveFeePaid, TxQ::baseLevel, baseFee).value_or(...) at getFeeLevelPaid", + "issue_pattern": "Missing empty string validation for mulDiv result", + "why_false_positive": "mulDiv(effectiveFeePaid, TxQ::baseLevel, baseFee).value_or(...) validates mulDiv result for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfLastLedgerSequence", + "empty", + "string", + "validation" + ], + "evidence": "tx.isFieldPresent(sfLastLedgerSequence) at getLastLedgerSequence", + "issue_pattern": "Missing empty string validation for sfLastLedgerSequence", + "why_false_positive": "tx.isFieldPresent(sfLastLedgerSequence) validates sfLastLedgerSequence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLastLedgerSequence (type)", + "empty", + "string", + "validation" + ], + "evidence": "tx.getFieldU32(sfLastLedgerSequence) at getLastLedgerSequence", + "issue_pattern": "Missing empty string validation for sfLastLedgerSequence (type)", + "why_false_positive": "tx.getFieldU32(sfLastLedgerSequence) validates sfLastLedgerSequence (type) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sfLastLedgerSequence (type)", + "type", + "validation", + "check" + ], + "evidence": "tx.getFieldU32(sfLastLedgerSequence) at getLastLedgerSequence", + "issue_pattern": "Missing type validation for sfLastLedgerSequence (type)", + "why_false_positive": "tx.getFieldU32(sfLastLedgerSequence) validates sfLastLedgerSequence (type) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "mulDiv result (increase)", + "empty", + "string", + "validation" + ], + "evidence": "mulDiv(level, 100 + increasePercent, 100).value_or(...) at increase", + "issue_pattern": "Missing empty string validation for mulDiv result (increase)", + "why_false_positive": "mulDiv(level, 100 + increasePercent, 100).value_or(...) validates mulDiv result (increase) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "feeLevels.size() == size", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(size == feeLevels.size(), ...) at TxQ::FeeMetrics::update", + "issue_pattern": "Missing empty string validation for feeLevels.size() == size", + "why_false_positive": "XRPL_ASSERT(size == feeLevels.size(), ...) validates feeLevels.size() == size for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/TxQ.cpp", + "functions": [ + { + "args": [ + "ReadView const& view", + "STTx const& tx" + ], + "lineno": 8, + "name": "getFeeLevelPaid" + }, + { + "args": [ + "STTx const& tx" + ], + "lineno": 29, + "name": "getLastLedgerSequence" + }, + { + "args": [ + "FeeLevel64 level", + "std::uint32_t increasePercent" + ], + "lineno": 37, + "name": "increase" + }, + { + "args": [ + "Application& app", + "ReadView const& view", + "bool timeLeap", + "TxQ::Setup const& setup" + ], + "lineno": 44, + "name": "TxQ::FeeMetrics::update" + }, + { + "args": [ + "Snapshot const& snapshot", + "OpenView const& view" + ], + "lineno": 109, + "name": "TxQ::FeeMetrics::scaleFeeLevel" + }, + { + "args": [ + "std::size_t xIn" + ], + "lineno": 124, + "name": "sumOfFirstSquares" + }, + { + "args": [ + "Snapshot const& snapshot", + "OpenView const& view", + "std::size_t extraCount", + "std::size_t seriesSize" + ], + "lineno": 151, + "name": "TxQ::FeeMetrics::escalatedSeriesFeeLevel" + }, + { + "args": [ + "std::shared_ptr const& txn_", + "TxID const& txID_", + "FeeLevel64 feeLevel_", + "ApplyFlags const flags_", + "PreflightResult const& pfResult_" + ], + "lineno": 185, + "name": "TxQ::MaybeTx::MaybeTx" + }, + { + "args": [ + "Application& app", + "OpenView& view", + "beast::Journal j" + ], + "lineno": 194, + "name": "TxQ::MaybeTx::apply" + }, + { + "args": [ + "std::shared_ptr const& txn" + ], + "lineno": 210, + "name": "TxQ::TxQAccount::TxQAccount" + }, + { + "args": [ + "AccountID const& account_" + ], + "lineno": 213, + "name": "TxQ::TxQAccount::TxQAccount" + }, + { + "args": [ + "SeqProxy seqProx" + ], + "lineno": 216, + "name": "TxQ::TxQAccount::getPrevTx" + }, + { + "args": [ + "MaybeTx&& txn" + ], + "lineno": 225, + "name": "TxQ::TxQAccount::add" + }, + { + "args": [ + "SeqProxy seqProx" + ], + "lineno": 236, + "name": "TxQ::TxQAccount::remove" + }, + { + "args": [ + "Setup const& setup", + "beast::Journal j" + ], + "lineno": 243, + "name": "TxQ::TxQ" + }, + { + "args": [], + "lineno": 247, + "name": "TxQ::~TxQ" + }, + { + "args": [], + "lineno": 250, + "name": "TxQ::isFull" + }, + { + "args": [ + "STTx const& tx", + "ApplyFlags const flags", + "OpenView const& view", + "std::shared_ptr const& sleAccount", + "AccountMap::iterator const& accountIter", + "std::optional const& replacementIter", + "std::lock_guard const& lock" + ], + "lineno": 258, + "name": "TxQ::canBeHeld" + }, + { + "args": [ + "TxQ::FeeMultiSet::const_iterator_type candidateIter" + ], + "lineno": 292, + "name": "TxQ::erase" + }, + { + "args": [ + "TxQ::FeeMultiSet::const_iterator_type candidateIter" + ], + "lineno": 303, + "name": "TxQ::eraseAndAdvance" + }, + { + "args": [ + "TxQ::TxQAccount& txQAccount", + "TxQ::TxQAccount::TxMap::const_iterator begin", + "TxQ::TxQAccount::TxMap::const_iterator end" + ], + "lineno": 324, + "name": "TxQ::erase" + }, + { + "args": [ + "Application& app", + "OpenView& view", + "STTx const& tx", + "TxQ::AccountMap::iterator const& accountIter", + "TxQAccount::TxMap::iterator beginTxIter", + "FeeLevel64 feeLevelPaid", + "PreflightResult const& pfResult", + "std::size_t const txExtraCount", + "ApplyFlags flags", + "FeeMetrics::Snapshot const& metricsSnapshot", + "beast::Journal j" + ], + "lineno": 335, + "name": "TxQ::tryClearAccountQueueUpThruTx" + }, + { + "args": [ + "Application& app", + "OpenView& view", + "std::shared_ptr const& tx", + "ApplyFlags flags", + "beast::Journal j" + ], + "lineno": 441, + "name": "TxQ::apply" + }, + { + "args": [ + "Application& app", + "ReadView const& view", + "bool timeLeap" + ], + "lineno": 803, + "name": "TxQ::processClosedLedger" + }, + { + "args": [ + "Application& app", + "OpenView& view" + ], + "lineno": 849, + "name": "TxQ::accept" + }, + { + "args": [ + "std::shared_ptr const& sleAccount" + ], + "lineno": 1012, + "name": "TxQ::nextQueuableSeq" + }, + { + "args": [ + "std::shared_ptr const& sleAccount", + "std::lock_guard const&" + ], + "lineno": 1020, + "name": "TxQ::nextQueuableSeqImpl" + }, + { + "args": [ + "OpenView& view", + "ApplyFlags flags", + "FeeMetrics::Snapshot const& metricsSnapshot", + "std::lock_guard const& lock" + ], + "lineno": 1052, + "name": "TxQ::getRequiredFeeLevel" + }, + { + "args": [ + "Application& app", + "OpenView& view", + "std::shared_ptr const& tx", + "ApplyFlags flags", + "beast::Journal j" + ], + "lineno": 1060, + "name": "TxQ::tryDirectApply" + }, + { + "args": [ + "std::optional const& replacedTxIter", + "std::shared_ptr const& tx" + ], + "lineno": 1107, + "name": "TxQ::removeFromByFee" + }, + { + "args": [ + "OpenView const& view" + ], + "lineno": 1126, + "name": "TxQ::getMetrics" + }, + { + "args": [ + "OpenView const& view", + "std::shared_ptr const& tx" + ], + "lineno": 1145, + "name": "TxQ::getTxRequiredFeeAndSeq" + }, + { + "args": [ + "AccountID const& account" + ], + "lineno": 1162, + "name": "TxQ::getAccountTxs" + }, + { + "args": [], + "lineno": 1177, + "name": "TxQ::getTxs" + }, + { + "args": [ + "Application& app" + ], + "lineno": 1190, + "name": "TxQ::doRPC" + }, + { + "args": [ + "Config const& config" + ], + "lineno": 1232, + "name": "setup_TxQ" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 122, + "name": "detail" + } + ], + "test_coverage_notes": "The core validation logic (fee calculation, signum checks, mulDiv error handling) is likely covered by unit tests in the rippled codebase, especially in files like TxQ_test.cpp, FeeMetrics_test.cpp, and possibly STTx_test.cpp. However, edge cases such as zero or negative fees, baseFee calculation anomalies, and mulDiv overflow/underflow may not be exhaustively tested. There is no direct evidence in this file of test hooks or coverage annotations. Integration tests that exercise transaction submission and queueing (e.g., via RPC or ledger simulation) would also indirectly test these paths. Gaps may exist for rare error paths (e.g., mulDiv failure, baseFee == 0, or mod logic).", + "validation_architecture": { + "auto_validated_fields": [ + "STTx field presence/type (via isFieldPresent/getFieldU32)", + "XRPAmount signum (positive/zero/negative)", + "mulDiv overflow checking" + ], + "framework": "XRPL protocol (STTx, XRPAmount, XRPL_ASSERT, mulDiv), custom assertion and type-safe accessors", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "If feePaid is not present or not convertible, likely throws exception from STTx accessor", + "field": "sfFee (transaction fee)", + "location": "getFeeLevelPaid", + "validated_by": "tx[sfFee].xrp()", + "validates": [ + "Ensures sfFee field is present and convertible to XRPAmount" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "If calculation fails, may throw exception from calculateBaseFee", + "field": "baseFee (calculated base fee)", + "location": "getFeeLevelPaid", + "validated_by": "calculateBaseFee(view, tx)", + "validates": [ + "Ensures baseFee is calculated and is a valid XRPAmount" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "baseFee.signum()", + "location": "getFeeLevelPaid", + "validated_by": "XRPL_ASSERT(baseFee.signum() > 0, ...)", + "validates": [ + "baseFee must be positive" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Returns FeeLevel64(0) (not exception, but error value)", + "field": "effectiveFeePaid.signum()", + "location": "getFeeLevelPaid", + "validated_by": "if (effectiveFeePaid.signum() <= 0 || baseFee.signum() <= 0)", + "validates": [ + "effectiveFeePaid must be positive" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "If mulDiv fails (overflow), returns max FeeLevel64", + "field": "mulDiv result", + "location": "getFeeLevelPaid", + "validated_by": "mulDiv(effectiveFeePaid, TxQ::baseLevel, baseFee).value_or(...)", + "validates": [ + "Checks for overflow in fee calculation" + ], + "validation_type": "range|overflow" + }, + { + "confidence": 1.0, + "error_thrown": "Returns std::nullopt if not present", + "field": "sfLastLedgerSequence", + "location": "getLastLedgerSequence", + "validated_by": "tx.isFieldPresent(sfLastLedgerSequence)", + "validates": [ + "Checks if sfLastLedgerSequence field is present" + ], + "validation_type": "presence" + }, + { + "confidence": 0.9, + "error_thrown": "If field is present but not uint32, likely throws from STTx", + "field": "sfLastLedgerSequence (type)", + "location": "getLastLedgerSequence", + "validated_by": "tx.getFieldU32(sfLastLedgerSequence)", + "validates": [ + "Ensures sfLastLedgerSequence is a uint32" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "If mulDiv fails (overflow), returns xrpl::muldiv_max", + "field": "mulDiv result (increase)", + "location": "increase", + "validated_by": "mulDiv(level, 100 + increasePercent, 100).value_or(...)", + "validates": [ + "Checks for overflow in fee level increase calculation" + ], + "validation_type": "range|overflow" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "feeLevels.size() == size", + "location": "TxQ::FeeMetrics::update", + "validated_by": "XRPL_ASSERT(size == feeLevels.size(), ...)", + "validates": [ + "Ensures number of fee levels matches number of transactions" + ], + "validation_type": "business_logic|consistency" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/TxQ.cpp.ai.md b/src/xrpld/app/misc/detail/TxQ.cpp.ai.md new file mode 100644 index 0000000000..6fa55c1d0e --- /dev/null +++ b/src/xrpld/app/misc/detail/TxQ.cpp.ai.md @@ -0,0 +1,104 @@ +# `src/xrpld/app/misc/detail/TxQ.cpp` + +## Role and Purpose + +This file is the implementation of XRPL's transaction queue — the mechanism that absorbs transactions when the open ledger is too full (or their fee too low) to accept them immediately. It works in concert with fee escalation: once the open ledger passes a target transaction count, the required fee rises quadratically, and any transaction that cannot clear that bar is held here and retried when the next open ledger opens. The design goal is to simultaneously protect validators from spam, provide fair ordering for legitimate high-volume senders, and give transactions a second chance at inclusion rather than dropping them outright. + +The file is purely an implementation detail; the public interface lives in `TxQ.h`. + +--- + +## Fee Level Utilities + +Three small file-scope helpers establish the vocabulary used throughout: + +`getFeeLevelPaid()` translates a raw XRP fee into a normalized `FeeLevel64`, where `TxQ::baseLevel` (256) represents the reference cost of a single-signed transaction. The normalization is `mulDiv(feePaid, 256, baseFee)`. The interesting edge case is when `calculateBaseFee` returns zero — something possible on networks where ordinary transactions are free. In that case a small `mod` value (the default base fee, or 1 drop as a last resort) is added to both numerator and denominator so that the comparison remains meaningful. `mulDiv` itself is overflow-safe and returns `std::optional`; the call site resolves overflow to `UINT64_MAX`, which ensures an overflowing fee is treated as extremely high rather than zero. + +`getLastLedgerSequence()` is a thin wrapper that returns `std::nullopt` if the field is absent, sparing callers from presence checks. + +`increase()` multiplies a fee level by `(100 + increasePercent) / 100`, used when computing the minimum fee bump required to replace a queued transaction (default 25%). + +--- + +## FeeMetrics + +`FeeMetrics` tracks two adaptive scalars that drive the escalation curve: `txnsExpected_` (the per-ledger transaction budget) and `escalationMultiplier_` (derived from the median fee of the last closed ledger). + +### `FeeMetrics::update()` — Adaptive Budget + +Called once per closed ledger via `processClosedLedger()`. It iterates every transaction in the validated ledger, computes its fee level, sorts the resulting vector, and takes the median as the new multiplier. The update to `txnsExpected_` uses two modes: + +- **Slow consensus** (`timeLeap == true`): Immediately cuts `txnsExpected_` by `slowConsensusDecreasePercent` (default 50%), bounded below by `minimumTxnCount_`. The circular buffer of recent high-traffic counts is also wiped, preventing a prior spike from inflating the budget during a slow period. +- **Normal consensus**: Pushes a `normalConsensusIncreasePercent`-bumped version of the actual count into a circular buffer and takes the max. Growth is fast (the max element of recent history), while shrinkage is slow: if the max element is already below `txnsExpected_`, the new value is only 10% of the way toward that max, providing hysteresis. + +### `FeeMetrics::scaleFeeLevel()` — Quadratic Escalation + +Below `txnsExpected_`, the required fee stays at `baseLevel` (flat). Once the open ledger exceeds `txnsExpected_`, the formula is: + +``` +required = multiplier * current² / target² +``` + +This is quadratic, not linear, which matters: each additional transaction past the threshold raises the bar significantly. With a multiplier of `baseLevel * 500` (the default minimum), a ledger at 2× capacity already requires 2,000× the reference fee. This makes targeted flooding extremely expensive without penalizing normal traffic that stays under the budget. + +### `FeeMetrics::escalatedSeriesFeeLevel()` — Fee Averaging for Series + +Given a range of transactions queued for an account plus an incoming high-fee transaction, this calculates the total fee requirement for all of them together. It uses the closed-form sum of squares `x(x+1)(2x+1)/6`, implemented in `detail::sumOfFirstSquares()` with an overflow guard at x ≥ 2²¹. The sum covers the ledger slots from `current` (the slot the first queued transaction would occupy) to `current + seriesSize - 1`. Static `assert` statements at the bottom of the `detail` namespace serve as embedded unit tests for the formula. + +--- + +## `MaybeTx` — A Queued Transaction + +`MaybeTx` stores a shared pointer to the immutable `STTx`, its computed fee level, `SeqProxy`, preflight result, and a `retriesRemaining` counter (default 10). The static member `parentHashComp` is set each cycle to the open ledger's parent hash; it is used by the intrusive multiset comparator to break fee-level ties, giving pseudo-random but deterministic ordering across validators that see the same ledger. + +`MaybeTx::apply()` handles rule changes across open-ledger boundaries by re-running `preflight` if the ledger rules or apply flags have changed since the transaction was first queued. + +--- + +## Dual-Index Storage + +The queue maintains two complementary views: + +- **`byFee_`**: A Boost intrusive multiset ordered by `(feeLevel DESC, parentHash XOR txID)`. Intrusive containers are used because `MaybeTx` objects live in `TxQAccount::TxMap`; the intrusive hooks are embedded directly in those objects, avoiding secondary heap allocations. The XOR with the parent hash is the determinism mechanism: every node re-evaluating `accept()` will sort equal-fee transactions the same way. +- **`byAccount_`**: An `std::map` where each `TxQAccount` holds a `std::map` (`TxMap`). The `SeqProxy` ordering ensures sequence-based transactions are processed in strict order while ticket-based transactions can appear anywhere in the map. + +Because `byFee_` holds pointers into `TxMap` nodes, erasing from one structure requires erasing from the other in the same critical section. The `erase()` and `eraseAndAdvance()` methods enforce this invariant. `eraseAndAdvance()` additionally implements the "appropriate next candidate" rule: after removing a sequence-based transaction, if the account's next transaction has a higher fee level than the global queue's next candidate, it jumps directly to the account-next, avoiding unnecessary backtracking. + +--- + +## `TxQ::apply()` — The Submission Gate + +The main public entry point is the most complex path: + +1. **Preflight** validates transaction structure without touching the ledger. +2. **`tryDirectApply()`** fast-paths transactions whose sequence matches the account root and whose fee clears the escalation threshold — they go straight into the ledger, bypassing the queue. If a queued transaction with the same `SeqProxy` exists, it is removed. +3. **Account and ticket checks**: the account must exist; if using a ticket, the ticket SLE must already be in the ledger (not merely queued to be created — this prevents dependency chains that could deadlock). +4. **Blocker rules**: transactions like `SetRegularKey` that invalidate subsequent transactions are allowed only when the account queue is empty or they replace the single existing entry. +5. **Replacement detection**: if the same `SeqProxy` is already queued, the incoming transaction must pay at least 25% more (configurable via `retrySequencePercent`) or it is rejected. +6. **`MultiTxn` sandbox**: when there are existing queued transactions for the account, a shadow `ApplyViewImpl` is constructed that pre-deducts the fees and potential spend of all prior queued transactions from the account's balance. `preclaim` then runs against this adjusted view, catching cases where the account cannot afford the candidate on top of its existing commitments. +7. **`tryClearAccountQueueUpThruTx()`**: if the candidate pays an escalated fee, this attempts to apply all queued transactions ahead of it plus itself in a sandbox. If successful, all queued transactions are erased and the result propagates. This is the fee-averaging path that lets a single high-fee transaction unblock an account queue. +8. **`canBeHeld()`**: enforces `LastLedgerSequence` buffer, per-account queue limit (default 10), and sequence gap rules. +9. **Queue eviction**: if the queue is full and the candidate outbids the account with the lowest average fee level, that account's last transaction is evicted to make room. +10. Finally, the transaction is added to both `byAccount_` and `byFee_`, returning `terQUEUED`. + +--- + +## `processClosedLedger()` and `accept()` + +`processClosedLedger()` runs after a ledger closes. It updates fee metrics, recomputes `maxSize_` as `max(txnsExpected * ledgersInQueue, queueSizeMin)` (but only on timely ledgers — slow ledgers do not expand capacity), and sweeps out expired transactions (setting `dropPenalty` on the account). + +`accept()` drains the queue into the new open ledger. It iterates `byFee_` from highest to lowest, skipping sequence-based transactions that aren't the head of their account's queue. It applies candidates until the fee threshold is exceeded. Failed transactions with `tef` or `tem` results, or an exhausted retry count, are evicted. Temporary failures (`ter`) decrement the retry counter and leave the transaction in place. A pressure relief valve triggers near 95% queue capacity: accounts with a `dropPenalty` that still have multiple queued transactions have their last transaction evicted proactively. + +After draining, `byFee_` is completely cleared and rebuilt from `byAccount_` with the new parent hash. The code comments note this was the fastest method benchmarked versus in-place re-sort or incremental migration. + +--- + +## Concurrency Model + +A single `std::mutex mutex_` serializes all queue mutations. `tryDirectApply()` computes the fee comparison before acquiring the lock, acquiring it only in the success path to remove any superseded queued transaction. `nextQueuableSeqImpl()` takes the lock by reference rather than acquiring it independently, a pattern repeated throughout for re-entrant callers that already hold the lock. + +--- + +## `setup_TxQ()` + +Parses the `[transaction_queue]` section of the node configuration. The only validation beyond type parsing is that `maximum_txn_in_ledger` may not be lower than `minimum_txn_in_ledger` or `minimum_txn_in_ledger_standalone` — violating this throws `std::runtime_error` at startup, preventing a silently broken configuration from reaching production. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/ValidatorKeys.cpp.ai.json b/src/xrpld/app/misc/detail/ValidatorKeys.cpp.ai.json new file mode 100644 index 0000000000..64ea63a545 --- /dev/null +++ b/src/xrpld/app/misc/detail/ValidatorKeys.cpp.ai.json @@ -0,0 +1,303 @@ +{ + "args": [ + { + "lineno": 9, + "name": "config" + }, + { + "lineno": 9, + "name": "j" + } + ], + "classes": [ + { + "args": [ + "Config const& config", + "beast::Journal j" + ], + "lineno": 8, + "name": "ValidatorKeys" + } + ], + "code_paths": [ + { + "call_chain": [ + "ValidatorKeys::ValidatorKeys", + "Config::exists", + "Config::section", + "loadValidatorToken", + "base64_decode", + "deserializeManifest", + "derivePublicKey", + "parseBase58", + "generateSecretKey" + ], + "entry_point": "ValidatorKeys::ValidatorKeys(Config const& config, beast::Journal j)", + "purpose": "Constructs ValidatorKeys object, validates config for validator token or seed, loads and validates keys and manifests.", + "validation_points": [ + "Config::exists (checks for presence of config sections)", + "loadValidatorToken (validates token format and content)", + "deserializeManifest (validates manifest structure and content)", + "pk != m->signingKey (validates signing key matches manifest)", + "parseBase58 (validates seed format)" + ] + } + ], + "data_flows": [ + { + "field": "validator_token", + "flow": [ + "Config file", + "Config::section(SECTION_VALIDATOR_TOKEN).lines()", + "loadValidatorToken", + "token->manifest (base64_decode)", + "deserializeManifest", + "m->signingKey" + ], + "origin": "Config file, [validator_token] section", + "transformations": [ + "Read as lines from config", + "Parsed into ValidatorToken struct", + "Manifest base64-decoded", + "Manifest deserialized" + ], + "validated_at": "loadValidatorToken, deserializeManifest, pk != m->signingKey" + }, + { + "field": "validation_seed", + "flow": [ + "Config file", + "Config::section(SECTION_VALIDATION_SEED).lines().front()", + "parseBase58", + "generateSecretKey", + "derivePublicKey" + ], + "origin": "Config file, [validation_seed] section", + "transformations": [ + "Read as line from config", + "Parsed from base58 to Seed", + "Seed used to generate SecretKey", + "SecretKey used to derive PublicKey" + ], + "validated_at": "parseBase58" + }, + { + "field": "configInvalid_", + "flow": [ + "Set to false by default", + "Set to true if both sections exist or validation fails" + ], + "origin": "ValidatorKeys::ValidatorKeys", + "transformations": [ + "Set to true on any validation failure" + ], + "validated_at": "At each validation failure point" + }, + { + "field": "keys", + "flow": [ + "Emplaced with (m->masterKey, pk, token->validationSecret) if token path", + "Emplaced with (pk, pk, sk) if seed path" + ], + "origin": "Constructed in ValidatorKeys::ValidatorKeys", + "transformations": [ + "Populated only after successful validation" + ], + "validated_at": "After all validation checks pass" + }, + { + "field": "manifest", + "flow": [ + "Loaded from token", + "base64_decoded", + "deserialized", + "m->signingKey checked", + "manifest moved to ValidatorKeys::manifest" + ], + "origin": "token->manifest", + "transformations": [ + "base64 decode", + "deserialize" + ], + "validated_at": "deserializeManifest, pk != m->signingKey" + } + ], + "description": "Implements the ValidatorKeys class constructor, which loads and validates validator keys from configuration, supporting both validator tokens and validation seeds, and sets up related key and manifest data.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "[validator_token] and [validation_seed] config sections", + "empty", + "string", + "validation" + ], + "evidence": "Config::exists at ValidatorKeys::ValidatorKeys (constructor)", + "issue_pattern": "Missing empty string validation for [validator_token] and [validation_seed] config sections", + "why_false_positive": "Config::exists validates [validator_token] and [validation_seed] config sections for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "validator token (config.section(SECTION_VALIDATOR_TOKEN))", + "empty", + "string", + "validation" + ], + "evidence": "loadValidatorToken at ValidatorKeys::ValidatorKeys (constructor)", + "issue_pattern": "Missing empty string validation for validator token (config.section(SECTION_VALIDATOR_TOKEN))", + "why_false_positive": "loadValidatorToken validates validator token (config.section(SECTION_VALIDATOR_TOKEN)) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "validator token manifest (token->manifest)", + "empty", + "string", + "validation" + ], + "evidence": "deserializeManifest(base64_decode(token->manifest)) at ValidatorKeys::ValidatorKeys (constructor)", + "issue_pattern": "Missing empty string validation for validator token manifest (token->manifest)", + "why_false_positive": "deserializeManifest(base64_decode(token->manifest)) validates validator token manifest (token->manifest) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "validator token manifest (token->manifest)", + "format", + "validation", + "invalid" + ], + "evidence": "deserializeManifest(base64_decode(token->manifest)) at ValidatorKeys::ValidatorKeys (constructor)", + "issue_pattern": "Missing format validation for validator token manifest (token->manifest)", + "why_false_positive": "deserializeManifest(base64_decode(token->manifest)) validates validator token manifest (token->manifest) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "validator token signing key", + "empty", + "string", + "validation" + ], + "evidence": "pk != m->signingKey at ValidatorKeys::ValidatorKeys (constructor)", + "issue_pattern": "Missing empty string validation for validator token signing key", + "why_false_positive": "pk != m->signingKey validates validator token signing key for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "validation seed (config.section(SECTION_VALIDATION_SEED))", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at ValidatorKeys::ValidatorKeys (constructor)", + "issue_pattern": "Missing empty string validation for validation seed (config.section(SECTION_VALIDATION_SEED))", + "why_false_positive": "parseBase58 validates validation seed (config.section(SECTION_VALIDATION_SEED)) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/ValidatorKeys.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "ValidatorKeys is a core class for validator identity. Typical test coverage would be in unit tests for validator key management, config parsing, and manifest validation. Look for test files like ValidatorKeys_test.cpp, Manifest_test.cpp, or Config_test.cpp. Gaps may exist in negative test cases (invalid tokens, mismatched keys, malformed manifests, both sections present). Integration tests may cover config loading but may not exhaustively test all validation branches.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom validation using Config, loadValidatorToken, parseBase58, and manifest deserialization", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Sets configInvalid_ = true, logs fatal error (no exception thrown)", + "field": "[validator_token] and [validation_seed] config sections", + "location": "ValidatorKeys::ValidatorKeys (constructor)", + "validated_by": "Config::exists", + "validates": [ + "Ensures both SECTION_VALIDATOR_TOKEN and SECTION_VALIDATION_SEED are not specified at the same time" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Sets configInvalid_ = true, logs fatal error (no exception thrown)", + "field": "validator token (config.section(SECTION_VALIDATOR_TOKEN))", + "location": "ValidatorKeys::ValidatorKeys (constructor)", + "validated_by": "loadValidatorToken", + "validates": [ + "Checks if the validator token can be loaded and parsed" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "Sets configInvalid_ = true, logs fatal error (no exception thrown)", + "field": "validator token manifest (token->manifest)", + "location": "ValidatorKeys::ValidatorKeys (constructor)", + "validated_by": "deserializeManifest(base64_decode(token->manifest))", + "validates": [ + "Checks if the manifest can be base64-decoded and deserialized" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "Sets configInvalid_ = true, logs fatal error (no exception thrown)", + "field": "validator token signing key", + "location": "ValidatorKeys::ValidatorKeys (constructor)", + "validated_by": "pk != m->signingKey", + "validates": [ + "Ensures the derived public key matches the signing key in the manifest" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Sets configInvalid_ = true, logs fatal error (no exception thrown)", + "field": "validation seed (config.section(SECTION_VALIDATION_SEED))", + "location": "ValidatorKeys::ValidatorKeys (constructor)", + "validated_by": "parseBase58", + "validates": [ + "Checks if the seed can be parsed from base58 encoding" + ], + "validation_type": "format|type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/ValidatorKeys.cpp.ai.md b/src/xrpld/app/misc/detail/ValidatorKeys.cpp.ai.md new file mode 100644 index 0000000000..d110a98602 --- /dev/null +++ b/src/xrpld/app/misc/detail/ValidatorKeys.cpp.ai.md @@ -0,0 +1,30 @@ +# `ValidatorKeys.cpp` — Validator Identity Bootstrap from Configuration + +This file implements the single constructor of `ValidatorKeys`, the class responsible for loading a validator node's cryptographic identity at startup. It bridges the raw configuration file and the rest of the validator machinery by parsing, validating, and materialising the key material that the node will use to sign ledger validations. + +## Role in the Validator System + +An XRPL validator node has a dual-key identity. A long-lived **master key** certifies the validator's identity; a short-lived **ephemeral signing key** is what actually signs each validation message. The indirection exists so that if an ephemeral key is compromised the operator can rotate it — generating a new signing key and a new **manifest** (an XRPL-specific certificate signed by the master key) — without every peer needing to update its trusted-key list. The manifest system is documented in detail in `include/xrpl/server/Manifest.h`. + +`ValidatorKeys` is the object that carries the resolved result of this bootstrapping: the master public key, the current signing public key, the corresponding signing secret key, the raw base64 manifest string, the manifest sequence number, and the node ID derived from the master key. All downstream code (the manifest cache, the validation signing path, the peer handshake) obtains these values from a single `ValidatorKeys` instance constructed once at application startup. + +## Two Configuration Paths + +The constructor supports two mutually exclusive configuration sections, differentiated at parse time: + +**`[validator_token]`** is the modern, production path. The operator generates an offline master key pair and uses it to produce a `ValidatorToken` blob — a small JSON object containing a base64-encoded manifest and a hex-encoded ephemeral secret key. `loadValidatorToken()` (defined in `Manifest.cpp`) decodes this JSON, extracts the raw 32-byte secret, and returns a `ValidatorToken` struct. The constructor then derives the public key from that secret via `derivePublicKey(KeyType::secp256k1, token->validationSecret)` and cross-checks it against the signing key embedded in the deserialized manifest (`pk != m->signingKey`). This check is the critical correctness invariant: it confirms that the secret key in the token and the manifest it accompanies are internally consistent, preventing a misconfiguration where an operator pastes mismatched components. On success the `Keys` struct is populated with `(m->masterKey, pk, token->validationSecret)`, clearly distinguishing master from signing identity. + +**`[validation_seed]`** is the legacy, non-manifest path. The operator provides only a base58-encoded seed. The constructor derives the secret key with `generateSecretKey` and the public key with `derivePublicKey`, then constructs `Keys` with `(pk, pk, sk)` — deliberately collapsing master and signing key into the same value. Sequence is left at zero and no manifest string is stored, signalling to callers that there is no manifest indirection in use. This path remains functional for development and test setups but offers no key-rotation capability. + +If both sections are present simultaneously the constructor sets `configInvalid_` and returns immediately with a fatal log, before any key material is touched. This explicit mutual-exclusion guard prevents ambiguous configuration that could silently favour one section over the other. + +## Error Signalling Without Exceptions + +The constructor never throws. Any validation failure — unparseable token, manifest that cannot be deserialized, a secret key whose derived public key doesn't match the manifest's signing key, or an invalid base58 seed — sets `configInvalid_ = true` and emits a `j.fatal()` log entry, then returns with `keys` left as `std::nullopt`. The caller is responsible for checking `configInvalid()` before using the object. The header comment on `keys` explicitly warns against using its presence as a proxy for validity: a node may have a well-formed configuration with no validator identity at all (i.e., running as a non-validator), in which case neither section is present and `keys` is empty but `configInvalid_` remains false. + +## Key Relationships + +- `ValidatorToken` (from `Manifest.h`) is a plain struct holding `manifest: std::string` and `validationSecret: SecretKey`. The constructor moves `token->manifest` into `ValidatorKeys::manifest` to avoid a copy of a potentially large base64 string. +- `deserializeManifest()` returns `std::optional` and does not verify the manifest signature itself; that responsibility sits with `Manifest::verify()` and the `ManifestCache`, which will later call `applyManifest()` using the raw manifest string stored in `ValidatorKeys::manifest`. +- `calcNodeID(m->masterKey)` in the token path and `calcNodeID(pk)` in the seed path both derive the `NodeID` from the **master** public key — ensuring the node's peer-network identity is tied to the stable long-term key, not the rotatable ephemeral one. +- `SECTION_VALIDATOR_TOKEN` and `SECTION_VALIDATION_SEED` are string macros defined in `ConfigSections.h`, mapping to the `[validator_token]` and `[validation_seed]` headings in `rippled.cfg`. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp.ai.json b/src/xrpld/app/misc/detail/ValidatorList.cpp.ai.json new file mode 100644 index 0000000000..03b85d4414 --- /dev/null +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp.ai.json @@ -0,0 +1,863 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ValidatorList::load", + "boost::regex (parsing keys)", + "ValidatorList::PublisherListStats (constructed)", + "ValidatorList::PublisherListStats::mergeDispositions", + "ValidatorList::PublisherListStats::bestDisposition / worstDisposition" + ], + "entry_point": "ValidatorList::load", + "purpose": "Loads validator and publisher keys from configuration, parses and validates them, updates internal validator list state.", + "validation_points": [ + "boost::regex used to validate and parse publisher keys", + "dispositions.empty() check in bestDisposition/worstDisposition", + "minimumQuorum validated via std::optional::value_or(1) in ValidatorList constructor" + ] + }, + { + "call_chain": [ + "to_string(ListDisposition)", + "switch statement on ListDisposition" + ], + "entry_point": "to_string(ListDisposition)", + "purpose": "Converts ListDisposition enum to string, validates enum value.", + "validation_points": [ + "switch statement ensures only valid enum values are handled, returns 'unknown' for invalid" + ] + }, + { + "call_chain": [ + "ValidatorList::ValidatorList", + "minimumQuorum.value_or(1)" + ], + "entry_point": "ValidatorList::ValidatorList (constructor)", + "purpose": "Constructs ValidatorList, validates minimumQuorum parameter.", + "validation_points": [ + "minimumQuorum validated via value_or(1) to ensure non-zero quorum" + ] + } + ], + "data_flows": [ + { + "field": "ListDisposition", + "flow": [ + "Created in PublisherListStats or as function argument", + "Passed to to_string, bestDisposition, worstDisposition, mergeDispositions", + "Used in switch statement or map" + ], + "origin": "Function arguments, internal state", + "transformations": [ + "Enum value checked in switch (to_string)", + "Counted in map (dispositions)" + ], + "validated_at": "to_string (switch), bestDisposition/worstDisposition (dispositions.empty() check)" + }, + { + "field": "dispositions (map)", + "flow": [ + "Incremented in PublisherListStats constructor", + "Merged in mergeDispositions", + "Checked in bestDisposition/worstDisposition" + ], + "origin": "PublisherListStats constructor", + "transformations": [ + "Incremented, merged, checked for emptiness" + ], + "validated_at": "bestDisposition/worstDisposition (dispositions.empty())" + }, + { + "field": "minimumQuorum", + "flow": [ + "Passed to ValidatorList constructor", + "Set to quorum_ via value_or(1)" + ], + "origin": "ValidatorList constructor argument (std::optional)", + "transformations": [ + "If not set, defaults to 1" + ], + "validated_at": "ValidatorList constructor (minimumQuorum.value_or(1))" + }, + { + "field": "publisherKeys", + "flow": [ + "Passed to ValidatorList::load", + "Iterated and parsed with boost::regex", + "Used to construct PublisherListStats" + ], + "origin": "ValidatorList::load argument", + "transformations": [ + "Regex parsing, whitespace trimming" + ], + "validated_at": "Regex match in ValidatorList::load" + } + ], + "description": "Implements the ValidatorList class for managing trusted validator lists, publisher lists, quorum calculation, validator list propagation, and related logic in the XRPL (XRP Ledger) server.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "enum ListDisposition (type safety)", + "validation", + "missing", + "check" + ], + "evidence": "Field enum ListDisposition (type safety) validated by C++ type system, enum, std::optional, custom logic", + "issue_pattern": "Missing validation for enum ListDisposition (type safety)", + "why_false_positive": "C++ type system, enum, std::optional, custom logic validates enum ListDisposition (type safety) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "minimumQuorum (std::optional defaulting)", + "validation", + "missing", + "check" + ], + "evidence": "Field minimumQuorum (std::optional defaulting) validated by C++ type system, enum, std::optional, custom logic", + "issue_pattern": "Missing validation for minimumQuorum (std::optional defaulting)", + "why_false_positive": "C++ type system, enum, std::optional, custom logic validates minimumQuorum (std::optional defaulting) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "dispositions map (checked for emptiness)", + "validation", + "missing", + "check" + ], + "evidence": "Field dispositions map (checked for emptiness) validated by C++ type system, enum, std::optional, custom logic", + "issue_pattern": "Missing validation for dispositions map (checked for emptiness)", + "why_false_positive": "C++ type system, enum, std::optional, custom logic validates dispositions map (checked for emptiness) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "ListDisposition enum", + "empty", + "string", + "validation" + ], + "evidence": "to_string switch statement at to_string(ListDisposition disposition)", + "issue_pattern": "Missing empty string validation for ListDisposition enum", + "why_false_positive": "to_string switch statement validates ListDisposition enum for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "dispositions map", + "empty", + "string", + "validation" + ], + "evidence": "dispositions.empty() check at PublisherListStats::bestDisposition, PublisherListStats::worstDisposition", + "issue_pattern": "Missing empty string validation for dispositions map", + "why_false_positive": "dispositions.empty() check validates dispositions map for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "minimumQuorum", + "empty", + "string", + "validation" + ], + "evidence": "std::optional::value_or(1) at ValidatorList constructor", + "issue_pattern": "Missing empty string validation for minimumQuorum", + "why_false_positive": "std::optional::value_or(1) validates minimumQuorum for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/ValidatorList.cpp", + "functions": [ + { + "args": [ + "disposition" + ], + "lineno": 9, + "name": "to_string" + }, + { + "args": [ + "d" + ], + "lineno": 38, + "name": "ValidatorList::PublisherListStats::PublisherListStats" + }, + { + "args": [ + "d", + "key", + "stat", + "seq" + ], + "lineno": 42, + "name": "ValidatorList::PublisherListStats::PublisherListStats" + }, + { + "args": [], + "lineno": 48, + "name": "ValidatorList::PublisherListStats::bestDisposition" + }, + { + "args": [], + "lineno": 52, + "name": "ValidatorList::PublisherListStats::worstDisposition" + }, + { + "args": [ + "src" + ], + "lineno": 56, + "name": "ValidatorList::PublisherListStats::mergeDispositions" + }, + { + "args": [ + "message_", + "hash_", + "num_" + ], + "lineno": 63, + "name": "ValidatorList::MessageWithHash::MessageWithHash" + }, + { + "args": [ + "validatorManifests", + "publisherManifests", + "timeKeeper", + "databasePath", + "j", + "minimumQuorum" + ], + "lineno": 68, + "name": "ValidatorList::ValidatorList" + }, + { + "args": [ + "localSigningKey", + "configKeys", + "publisherKeys", + "listThreshold" + ], + "lineno": 78, + "name": "ValidatorList::load" + }, + { + "args": [ + "lock", + "pubKey" + ], + "lineno": 134, + "name": "ValidatorList::getCacheFileName" + }, + { + "args": [ + "pubKey", + "pubCollection", + "j" + ], + "lineno": 141, + "name": "ValidatorList::buildFileData" + }, + { + "args": [ + "pubKey", + "pubCollection", + "forceVersion", + "j" + ], + "lineno": 148, + "name": "ValidatorList::buildFileData" + }, + { + "args": [ + "lock", + "pubKey" + ], + "lineno": 185, + "name": "ValidatorList::cacheValidatorFile" + }, + { + "args": [ + "version", + "body" + ], + "lineno": 202, + "name": "ValidatorList::parseBlobs" + }, + { + "args": [ + "body" + ], + "lineno": 246, + "name": "ValidatorList::parseBlobs" + }, + { + "args": [ + "body" + ], + "lineno": 253, + "name": "ValidatorList::parseBlobs" + }, + { + "args": [ + "messages", + "largeMsg", + "maxSize", + "begin", + "end" + ], + "lineno": 271, + "name": "splitMessageParts" + }, + { + "args": [ + "messages", + "largeMsg", + "maxSize", + "begin", + "end" + ], + "lineno": 277, + "name": "splitMessage" + }, + { + "args": [ + "messages", + "largeMsg", + "maxSize", + "begin", + "end" + ], + "lineno": 292, + "name": "splitMessageParts" + }, + { + "args": [ + "messages", + "rawVersion", + "rawManifest", + "currentBlob", + "maxSize" + ], + "lineno": 324, + "name": "buildValidatorListMessage" + }, + { + "args": [ + "messages", + "peerSequence", + "rawVersion", + "rawManifest", + "blobInfos", + "maxSize" + ], + "lineno": 345, + "name": "buildValidatorListMessage" + }, + { + "args": [ + "messageVersion", + "peerSequence", + "maxSequence", + "rawVersion", + "rawManifest", + "blobInfos", + "messages", + "maxSize" + ], + "lineno": 372, + "name": "ValidatorList::buildValidatorListMessages" + }, + { + "args": [ + "peer", + "peerSequence", + "publisherKey", + "maxSequence", + "rawVersion", + "rawManifest", + "blobInfos", + "messages", + "hashRouter", + "j" + ], + "lineno": 410, + "name": "ValidatorList::sendValidatorList" + }, + { + "args": [ + "peer", + "peerSequence", + "publisherKey", + "maxSequence", + "rawVersion", + "rawManifest", + "blobInfos", + "hashRouter", + "j" + ], + "lineno": 445, + "name": "ValidatorList::sendValidatorList" + }, + { + "args": [ + "blobInfos", + "lists" + ], + "lineno": 457, + "name": "ValidatorList::buildBlobInfos" + }, + { + "args": [ + "lists" + ], + "lineno": 466, + "name": "ValidatorList::buildBlobInfos" + }, + { + "args": [ + "publisherKey", + "lists", + "maxSequence", + "hash", + "overlay", + "hashRouter", + "j" + ], + "lineno": 474, + "name": "ValidatorList::broadcastBlobs" + }, + { + "args": [ + "manifest", + "version", + "blobs", + "siteUri", + "hash", + "overlay", + "hashRouter", + "networkOPs" + ], + "lineno": 511, + "name": "ValidatorList::applyListsAndBroadcast" + }, + { + "args": [ + "manifest", + "version", + "blobs", + "siteUri", + "hash" + ], + "lineno": 541, + "name": "ValidatorList::applyLists" + }, + { + "args": [ + "pubKey", + "current", + "oldList", + "lock" + ], + "lineno": 589, + "name": "ValidatorList::updatePublisherList" + }, + { + "args": [ + "globalManifest", + "localManifest", + "blob", + "signature", + "version", + "siteUri", + "hash", + "lock" + ], + "lineno": 627, + "name": "ValidatorList::applyList" + }, + { + "args": [], + "lineno": 735, + "name": "ValidatorList::loadLists" + }, + { + "args": [ + "lock", + "list", + "manifest", + "blob", + "signature" + ], + "lineno": 779, + "name": "ValidatorList::verify" + }, + { + "args": [ + "identity" + ], + "lineno": 834, + "name": "ValidatorList::listed" + }, + { + "args": [ + "read_lock", + "identity" + ], + "lineno": 841, + "name": "ValidatorList::trusted" + }, + { + "args": [ + "identity" + ], + "lineno": 846, + "name": "ValidatorList::trusted" + }, + { + "args": [ + "identity" + ], + "lineno": 851, + "name": "ValidatorList::getListedKey" + }, + { + "args": [ + "read_lock", + "identity" + ], + "lineno": 858, + "name": "ValidatorList::getTrustedKey" + }, + { + "args": [ + "identity" + ], + "lineno": 864, + "name": "ValidatorList::getTrustedKey" + }, + { + "args": [ + "identity" + ], + "lineno": 869, + "name": "ValidatorList::trustedPublisher" + }, + { + "args": [], + "lineno": 875, + "name": "ValidatorList::localPublicKey" + }, + { + "args": [ + "lock", + "publisherKey", + "reason" + ], + "lineno": 880, + "name": "ValidatorList::removePublisherList" + }, + { + "args": [ + "read_lock" + ], + "lineno": 900, + "name": "ValidatorList::count" + }, + { + "args": [], + "lineno": 905, + "name": "ValidatorList::count" + }, + { + "args": [ + "read_lock" + ], + "lineno": 910, + "name": "ValidatorList::expires" + }, + { + "args": [], + "lineno": 950, + "name": "ValidatorList::expires" + }, + { + "args": [], + "lineno": 955, + "name": "ValidatorList::getJson" + }, + { + "args": [ + "func" + ], + "lineno": 1037, + "name": "ValidatorList::for_each_listed" + }, + { + "args": [ + "func" + ], + "lineno": 1044, + "name": "ValidatorList::for_each_available" + }, + { + "args": [ + "pubKey", + "forceVersion" + ], + "lineno": 1062, + "name": "ValidatorList::getAvailable" + }, + { + "args": [ + "unlSize", + "effectiveUnlSize", + "seenSize" + ], + "lineno": 1082, + "name": "ValidatorList::calculateQuorum" + }, + { + "args": [ + "seenValidators", + "closeTime", + "ops", + "overlay", + "hashRouter" + ], + "lineno": 1127, + "name": "ValidatorList::updateTrusted" + }, + { + "args": [], + "lineno": 1247, + "name": "ValidatorList::getTrustedMasterKeys" + }, + { + "args": [], + "lineno": 1252, + "name": "ValidatorList::getListThreshold" + }, + { + "args": [], + "lineno": 1257, + "name": "ValidatorList::getNegativeUNL" + }, + { + "args": [ + "negUnl" + ], + "lineno": 1262, + "name": "ValidatorList::setNegativeUNL" + }, + { + "args": [ + "validations" + ], + "lineno": 1267, + "name": "ValidatorList::negativeUNLFilter" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 18, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested by integration and unit tests for validator list loading and management. Tests would exist in files like ValidatorList_test.cpp or ValidatorList_test.py (if bindings exist). Key validation paths (enum handling, minimumQuorum defaulting, regex parsing) should be covered, but edge cases (invalid enum, empty publisherKeys, minimumQuorum=0) may not be fully tested unless explicitly checked. No test code is shown here, so coverage of error/edge cases is uncertain.", + "validation_architecture": { + "auto_validated_fields": [ + "enum ListDisposition (type safety)", + "minimumQuorum (std::optional defaulting)", + "dispositions map (checked for emptiness)" + ], + "framework": "C++ type system, enum, std::optional, custom logic", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "returns 'unknown' for invalid enum", + "field": "ListDisposition enum", + "location": "to_string(ListDisposition disposition)", + "validated_by": "to_string switch statement", + "validates": [ + "enum value is recognized" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "returns ListDisposition::invalid if empty", + "field": "dispositions map", + "location": "PublisherListStats::bestDisposition, PublisherListStats::worstDisposition", + "validated_by": "dispositions.empty() check", + "validates": [ + "dispositions map is not empty before accessing" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (defaults to 1 if not set)", + "field": "minimumQuorum", + "location": "ValidatorList constructor", + "validated_by": "std::optional::value_or(1)", + "validates": [ + "minimumQuorum is set, otherwise uses default" + ], + "validation_type": "range|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/ValidatorList.cpp.ai.md b/src/xrpld/app/misc/detail/ValidatorList.cpp.ai.md new file mode 100644 index 0000000000..f0e28d0b91 --- /dev/null +++ b/src/xrpld/app/misc/detail/ValidatorList.cpp.ai.md @@ -0,0 +1,95 @@ +# ValidatorList.cpp + +## Role in the System + +This file implements `ValidatorList`, the authority on which validators an XRPL node trusts for consensus. The XRP Ledger achieves finality when a quorum of trusted validators agree on the same ledger; this class answers at every step: *which validators are trusted?*, *what quorum is required?*, and *has a validator's publisher list changed?* + +Two independent trust sources coexist inside the class. Static keys loaded from the local config (`[validators]`) are stored in `localPublisherList` — they never expire and carry no publisher identity. Dynamically-signed lists downloaded from network publishers are stored in `publisherLists_`, keyed by the publisher's master `PublicKey`. The class merges both sources into a single authoritative set of `trustedMasterKeys_` and a parallel set of `trustedSigningKeys_` (ephemeral keys derived via manifests). + +--- + +## Key Data Structures + +**`PublisherListCollection`** (private) holds everything known about a single publisher. It separates `current` (the VL currently in effect — the highest sequence that has ever become active) from `remaining` (future VLs whose `validFrom` is still in the future). This split exists because the v2 format allows publishers to pre-publish successive lists before the current one expires, creating a chain of overlapping validity windows. `remaining` is a `std::map` ordered by sequence. + +**`keyListings_`** is a `hash_map` counting how many publisher lists each validator master key appears on. A validator is only eligible for trust if this count meets `listThreshold_`. The count is incrementally maintained by `updatePublisherList()` using a two-pointer merge-walk over sorted `list` vectors — O(n) rather than two O(n log n) set differences. + +**`ListDisposition`** is an enum whose integer ordering is intentional and load-bearing. Lower values are "better" (`accepted = 0`, `expired`, `pending`, …, `invalid`). `PublisherListStats` wraps a `std::map` counting occurrences, and `bestDisposition()` / `worstDisposition()` simply return `begin()->first` / `rbegin()->first` — the ordering of the map mirrors the ordering of the enum. + +--- + +## Initialization Path + +`load()` runs at startup and accepts config keys, publisher keys, and an optional `listThreshold`. Publisher keys are hex-decoded and validated with `publicKeyType()`; any key whose manifest has already been revoked is marked `PublisherStatus::revoked` immediately. The local node's own signing key, if configured, is resolved to its master key via `validatorManifests_.getMasterKey()` and inserted into `keyListings_` with a count equal to `listThreshold_`, preventing the node from ignoring itself. + +`listThreshold_` defaults to 1 for fewer than three publishers, and `(N/2)+1` for three or more — a simple majority requirement. A manually-specified threshold bypasses this formula (the config class enforces range). + +The `minimumQuorum_` parameter (from `--quorum` on the command line) overrides all quorum calculation. Its presence triggers a `JLOG` warning at every quorum computation to signal that the node is operating in potentially unsafe mode. + +--- + +## Ingest Path: `applyList` → `applyLists` → `applyListsAndBroadcast` + +`applyLists()` is the entry point for a complete VL payload — it loops over all `ValidatorBlobInfo` entries in the payload and calls `applyList()` on each. After the loop, it performs a cleanup pass on `remaining`: any entry whose sequence is ≤ `current.sequence` or whose `validFrom` is not strictly later than the next entry's `validFrom` is pruned. This enforces the invariant that `remaining` contains only strictly increasing, non-redundant future VLs. + +`applyList()` delegates cryptographic verification to `verify()`, which: +1. Rejects keys not in `publisherLists_` as `untrusted`. +2. Applies the manifest through `publisherManifests_`, atomically handling revocation — a revoked manifest clears `remaining` and removes the current list before returning `untrusted`. +3. Verifies the blob signature using the publisher's ephemeral signing key. +4. Parses and validates JSON fields (`sequence`, `expiration`, optionally `effective`, `validators`). +5. Classifies the result: `expired` if `validUntil ≤ now`, `pending` if `validFrom > now`, `stale` if sequence is older than `current.sequence`, `same_sequence`, `known_sequence`, or `accepted`. + +Both `accepted` and `expired` advance the `current` slot. An `expired` list is stored because it represents the most recent authoritative VL from that publisher even though its time window has passed — it cannot be superseded by an older sequence. + +After `applyLists()` completes, `cacheValidatorFile()` serializes the full `PublisherListCollection` to a `cache.` file on disk. On the next restart, `loadLists()` returns `file://` URIs for these files, which `ValidatorSite` fetches as if they were remote URLs, bootstrapping list state without waiting for network refetches. + +--- + +## Propagation Machinery + +The propagation layer is version-aware: + +- Peers advertising `ValidatorListPropagation` receive v1 messages (`TMValidatorList`, single blob). +- Peers advertising `ValidatorList2Propagation` receive v2 messages (`TMValidatorListCollection`, multiple blobs) enabling delivery of the full pending chain in one round-trip. + +`broadcastBlobs()` consults `hashRouter.shouldRelay()`, then iterates `overlay.getActivePeers()`. For each eligible peer it only sends VLs with higher sequence numbers than what the peer already has. V2 messages are grouped by `peerSequence` so the same pre-built `MessageWithHash` is reused across peers with the same known sequence. + +`buildValidatorListMessages()` lazily constructs messages: the first call builds and caches the protobuf; subsequent calls reuse it. If a `TMValidatorListCollection` exceeds `maximumMessageSize`, `splitMessage()` recursively bisects the blob list until each fragment fits, potentially downgrading to individual `TMValidatorList` messages when a partition reaches size 1. + +--- + +## Trust Update Cycle: `updateTrusted` + +Called at the start of each consensus round while holding an exclusive write lock. + +**Pending rotation.** For each publisher collection, if the first entry in `remaining` has `validFrom ≤ closeTime`, `updateTrusted` scans forward to find the *last* candidate that is ready to go live, moves it into `current`, calls `updatePublisherList()` to reconcile `keyListings_`, and erases all consumed entries from `remaining` in one `erase(first, next(iter))`. + +**Expiry.** If `current.validUntil ≤ closeTime`, `removePublisherList()` decrements `keyListings_` for every validator in that publisher's list, clears the list, and sets status to `expired`. `ops.setUNLBlocked()` signals that consensus is degraded. + +**Trust set reconstruction.** The method sweeps `trustedMasterKeys_` removing keys whose listing count dropped below `listThreshold_` or whose manifest was revoked, then sweeps `keyListings_` to add newly eligible keys. If either set changes, `trustedSigningKeys_` is rebuilt from scratch. + +**Quorum calculation.** `calculateQuorum()` uses 80% of the effective UNL size (UNL minus Negative UNL validators), floored at 60% of the full UNL size (the `AbsoluteMinimumQuorum` from the Negative UNL protocol). Before reaching the formula it checks whether unavailable publishers exceed `errorThreshold` — the minimum of `listThreshold_` and `N - listThreshold_ + 1`. If so, quorum is set to `std::numeric_limits::max()`, making consensus impossible until lists are restored. This is the primary liveness-safety tradeoff: the node prefers to stall rather than reach quorum with an incomplete trust picture. + +--- + +## Concurrency Model + +`mutex_` is a `std::shared_mutex`. Read-heavy queries (`listed()`, `trusted()`, `getTrustedKey()`, `expires()`, `getJson()`, `for_each_listed()`) acquire `std::shared_lock`. Write paths (`applyLists()`, `updateTrusted()`, `load()`, `setNegativeUNL()`) acquire `std::lock_guard`. Several methods are overloaded with a lock-token parameter (e.g., `trusted(shared_lock const&, …)`) so callers that have already acquired the lock can call them without re-locking — a pattern used heavily inside `updateTrusted()` and `getJson()`. + +`quorum_` is `std::atomic`, allowing the consensus engine to read it without acquiring the mutex; it is only written from `updateTrusted()` while holding the exclusive lock. + +--- + +## Negative UNL Integration + +`negativeUNL_` is a `hash_set` set externally via `setNegativeUNL()`. In `updateTrusted()`, validators in this set are subtracted from `effectiveUnlSize`, lowering the quorum threshold gracefully during periods of validator downtime. `negativeUNLFilter()` provides a post-hoc filter on raw validation messages, removing validations from negative-UNL validators before they reach the consensus engine. + +--- + +## Design Decisions Worth Noting + +The `current`/`remaining` split avoids any ambiguity about which list is "live" — the invariant is enforced at multiple points (`applyLists` cleanup, `updateTrusted` rotation) rather than computed on demand, keeping trust queries as O(1) lookups into pre-computed sets. + +The `keyListings_` reference count (rather than per-publisher membership sets) means "is this key trusted?" is a single hash lookup, not a scan across all publisher lists. The merge-walk in `updatePublisherList()` works because `PublisherList::list` is always sorted (enforced by `std::sort` at the end of `applyList`'s list-building loop). + +The `buildFileData()` static method serializes a `PublisherListCollection` back to JSON — the inverse of `parseBlobs()`. A `forceVersion` parameter allows the `/vl/` HTTP endpoint to serve a v1-format response to clients that do not support v2, downgrading a multi-blob collection to its current blob only. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/ValidatorSite.cpp.ai.json b/src/xrpld/app/misc/detail/ValidatorSite.cpp.ai.json new file mode 100644 index 0000000000..8e3896f5bb --- /dev/null +++ b/src/xrpld/app/misc/detail/ValidatorSite.cpp.ai.json @@ -0,0 +1,679 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ValidatorSite::load(siteURIs)", + "ValidatorSite::load(siteURIs, lock_sites)", + "ValidatorSite::Site::Site(uri)", + "ValidatorSite::Site::Resource::Resource(uri)" + ], + "entry_point": "ValidatorSite::load(std::vector const& siteURIs)", + "purpose": "Loads and validates a list of validator site URIs, constructing Site and Resource objects for each.", + "validation_points": [ + "ValidatorSite::Site::Resource::Resource(uri): URI parsing and scheme/domain/path validation" + ] + }, + { + "call_chain": [ + "ValidatorSite::missingSite(lock_sites)", + "app_.getValidators().loadLists()", + "ValidatorSite::load(sites, lock_sites)", + "ValidatorSite::Site::Site(uri)", + "ValidatorSite::Site::Resource::Resource(uri)" + ], + "entry_point": "ValidatorSite::missingSite(std::lock_guard const& lock_sites)", + "purpose": "Handles the case where no validator sites are configured, attempts to load default sites.", + "validation_points": [ + "ValidatorSite::Site::Resource::Resource(uri): URI parsing and scheme/domain/path validation" + ] + }, + { + "call_chain": [ + "ValidatorSite::start()", + "ValidatorSite::setTimer(l0, l1) (not shown in code, but implied)" + ], + "entry_point": "ValidatorSite::start()", + "purpose": "Starts the periodic refresh timer for validator site fetching.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "uri (validator site URI)", + "flow": [ + "Input to ValidatorSite::load", + "Passed to ValidatorSite::Site::Site(uri)", + "Passed to ValidatorSite::Site::Resource::Resource(uri)", + "Parsed and validated (parseUrl, scheme/domain/path checks)", + "Stored in Resource::uri and Resource::pUrl" + ], + "origin": "Input to ValidatorSite::load (from config or API)", + "transformations": [ + "parseUrl splits URI into scheme/domain/path/port", + "Scheme/domain/path validated and possibly normalized (e.g., Windows path adjustment)" + ], + "validated_at": "ValidatorSite::Site::Resource::Resource(uri)" + }, + { + "field": "pUrl (parsed URL struct)", + "flow": [ + "Created in Resource::Resource", + "Fields (scheme, domain, path, port) validated and possibly modified", + "Used for subsequent network/file operations" + ], + "origin": "Result of parseUrl in Resource::Resource", + "transformations": [ + "Scheme/domain/path/port set from parseUrl", + "Port defaulted if missing (80 for http, 443 for https)", + "Path adjusted for Windows" + ], + "validated_at": "Immediately after parseUrl in Resource::Resource" + } + ], + "description": "Implements the ValidatorSite class, which manages fetching, parsing, and refreshing validator list sites (from HTTP, HTTPS, or file URIs) for the XRPL network. Handles site loading, request scheduling, error handling, redirects, and JSON parsing of validator lists.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "uri", + "empty", + "string", + "validation" + ], + "evidence": "parseUrl at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing empty string validation for uri", + "why_false_positive": "parseUrl validates uri for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "uri", + "format", + "validation", + "invalid" + ], + "evidence": "parseUrl at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing format validation for uri", + "why_false_positive": "parseUrl validates uri format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "pUrl.scheme", + "empty", + "string", + "validation" + ], + "evidence": "manual string comparison at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing empty string validation for pUrl.scheme", + "why_false_positive": "manual string comparison validates pUrl.scheme for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "pUrl.scheme", + "format", + "validation", + "invalid" + ], + "evidence": "manual string comparison at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing format validation for pUrl.scheme", + "why_false_positive": "manual string comparison validates pUrl.scheme format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "pUrl.domain (for file scheme)", + "empty", + "string", + "validation" + ], + "evidence": "manual check at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing empty string validation for pUrl.domain (for file scheme)", + "why_false_positive": "manual check validates pUrl.domain (for file scheme) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "pUrl.domain (for file scheme)", + "format", + "validation", + "invalid" + ], + "evidence": "manual check at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing format validation for pUrl.domain (for file scheme)", + "why_false_positive": "manual check validates pUrl.domain (for file scheme) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "pUrl.path (for file scheme)", + "empty", + "string", + "validation" + ], + "evidence": "manual check at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing empty string validation for pUrl.path (for file scheme)", + "why_false_positive": "manual check validates pUrl.path (for file scheme) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "pUrl.path (for file scheme)", + "format", + "validation", + "invalid" + ], + "evidence": "manual check at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing format validation for pUrl.path (for file scheme)", + "why_false_positive": "manual check validates pUrl.path (for file scheme) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "pUrl.domain (for http/https scheme)", + "empty", + "string", + "validation" + ], + "evidence": "manual check at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing empty string validation for pUrl.domain (for http/https scheme)", + "why_false_positive": "manual check validates pUrl.domain (for http/https scheme) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "pUrl.domain (for http/https scheme)", + "format", + "validation", + "invalid" + ], + "evidence": "manual check at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing format validation for pUrl.domain (for http/https scheme)", + "why_false_positive": "manual check validates pUrl.domain (for http/https scheme) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "pUrl.port (for http/https scheme)", + "empty", + "string", + "validation" + ], + "evidence": "manual assignment at ValidatorSite::Site::Resource::Resource (constructor)", + "issue_pattern": "Missing empty string validation for pUrl.port (for http/https scheme)", + "why_false_positive": "manual assignment validates pUrl.port (for http/https scheme) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/ValidatorSite.cpp", + "functions": [ + { + "args": [ + "std::string uri_" + ], + "lineno": 13, + "name": "ValidatorSite::Site::Resource::Resource" + }, + { + "args": [ + "std::string uri" + ], + "lineno": 38, + "name": "ValidatorSite::Site::Site" + }, + { + "args": [ + "Application& app", + "std::optional j", + "std::chrono::seconds timeout" + ], + "lineno": 44, + "name": "ValidatorSite::ValidatorSite" + }, + { + "args": [], + "lineno": 54, + "name": "ValidatorSite::~ValidatorSite" + }, + { + "args": [ + "std::lock_guard const& lock_sites" + ], + "lineno": 66, + "name": "ValidatorSite::missingSite" + }, + { + "args": [ + "std::vector const& siteURIs" + ], + "lineno": 72, + "name": "ValidatorSite::load" + }, + { + "args": [ + "std::vector const& siteURIs", + "std::lock_guard const& lock_sites" + ], + "lineno": 78, + "name": "ValidatorSite::load" + }, + { + "args": [], + "lineno": 97, + "name": "ValidatorSite::start" + }, + { + "args": [], + "lineno": 105, + "name": "ValidatorSite::join" + }, + { + "args": [], + "lineno": 111, + "name": "ValidatorSite::stop" + }, + { + "args": [ + "std::lock_guard const& site_lock", + "std::lock_guard const& state_lock" + ], + "lineno": 130, + "name": "ValidatorSite::setTimer" + }, + { + "args": [ + "std::shared_ptr resource", + "std::size_t siteIdx", + "std::lock_guard const& sites_lock" + ], + "lineno": 146, + "name": "ValidatorSite::makeRequest" + }, + { + "args": [ + "std::size_t siteIdx", + "error_code const& ec" + ], + "lineno": 196, + "name": "ValidatorSite::onRequestTimeout" + }, + { + "args": [ + "std::size_t siteIdx", + "error_code const& ec" + ], + "lineno": 217, + "name": "ValidatorSite::onTimer" + }, + { + "args": [ + "std::string const& res", + "std::size_t siteIdx", + "std::lock_guard const& sites_lock" + ], + "lineno": 241, + "name": "ValidatorSite::parseJsonResponse" + }, + { + "args": [ + "detail::response_type const& res", + "std::size_t siteIdx", + "std::lock_guard const& sites_lock" + ], + "lineno": 307, + "name": "ValidatorSite::processRedirect" + }, + { + "args": [ + "boost::system::error_code const& ec", + "endpoint_type const& endpoint", + "detail::response_type const& res", + "std::size_t siteIdx" + ], + "lineno": 338, + "name": "ValidatorSite::onSiteFetch" + }, + { + "args": [ + "boost::system::error_code const& ec", + "std::string const& res", + "std::size_t siteIdx" + ], + "lineno": 399, + "name": "ValidatorSite::onTextFetch" + }, + { + "args": [], + "lineno": 429, + "name": "ValidatorSite::getJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "ValidatorSite and its validation logic are typically tested via integration tests that load validator site URIs from configuration or API. Tests should cover valid and invalid URIs, all supported schemes (file, http, https), and error handling for malformed URIs. However, there is no evidence in this file of direct unit tests for Resource::Resource or parseUrl validation logic. Gaps may exist in edge cases (e.g., Windows path handling, unsupported schemes, empty paths/domains). Test files likely to cover this are in the test/app/misc or test/app/misc/detail directories, but explicit test coverage for all validation branches should be verified.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation, parseUrl utility, C++ exceptions", + "validation_layer": "entry_point (Resource constructor, i.e. object creation)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error(\"URI '... cannot be parsed\")", + "field": "uri", + "location": "ValidatorSite::Site::Resource::Resource (constructor)", + "validated_by": "parseUrl", + "validates": [ + "URI string is parseable into URL components" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error(\"Unsupported scheme: '...'\")", + "field": "pUrl.scheme", + "location": "ValidatorSite::Site::Resource::Resource (constructor)", + "validated_by": "manual string comparison", + "validates": [ + "Scheme is one of: file, http, https" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error(\"file URI cannot contain a hostname\")", + "field": "pUrl.domain (for file scheme)", + "location": "ValidatorSite::Site::Resource::Resource (constructor)", + "validated_by": "manual check", + "validates": [ + "file: URI does not have a hostname/domain" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error(\"file URI must contain a path\")", + "field": "pUrl.path (for file scheme)", + "location": "ValidatorSite::Site::Resource::Resource (constructor)", + "validated_by": "manual check", + "validates": [ + "file: URI has a non-empty path" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error(\"http[s] URI must contain a hostname\")", + "field": "pUrl.domain (for http/https scheme)", + "location": "ValidatorSite::Site::Resource::Resource (constructor)", + "validated_by": "manual check", + "validates": [ + "http[s]: URI has a non-empty hostname/domain" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "none (default port assigned)", + "field": "pUrl.port (for http/https scheme)", + "location": "ValidatorSite::Site::Resource::Resource (constructor)", + "validated_by": "manual assignment", + "validates": [ + "http: default port 80 assigned if missing", + "https: default port 443 assigned if missing" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/ValidatorSite.cpp.ai.md b/src/xrpld/app/misc/detail/ValidatorSite.cpp.ai.md new file mode 100644 index 0000000000..820f8433cb --- /dev/null +++ b/src/xrpld/app/misc/detail/ValidatorSite.cpp.ai.md @@ -0,0 +1,54 @@ +# `ValidatorSite.cpp` — Periodic Validator List Fetcher + +## Role in the System + +XRPL nodes maintain a set of trusted validators — the validators whose signatures they count when forming consensus. That trust set comes from signed *validator lists* published by trusted list-publishers. `ValidatorSite.cpp` implements `ValidatorSite`, the component responsible for discovering those lists: it reads a set of configured URIs, fetches JSON list payloads on a recurring schedule, verifies their structure, and hands them to `ValidatorList::applyListsAndBroadcast()` for cryptographic validation and peer propagation. Without this component, a node would never learn of validator set changes from its configured list-publishers. + +## URI Parsing and the Three-Resource Model + +Every configured URL becomes a `Site` object. Each `Site` holds three distinct `shared_ptr` pointers, and the distinction matters: + +- **`loadedResource`**: The original URI as read from configuration. Never changes. +- **`startingResource`**: The URI actually fetched at each scheduled interval. Initially the same as `loadedResource`, but updated to the redirect destination when a *permanent* redirect (301 or 308) is received, so future polls go directly to the canonical address. +- **`activeResource`**: The URI of the in-flight request. Changes during redirect chains but resets at the start of each fresh poll cycle. + +This three-way split means `getJson()` can always report what the operator configured *and* where the node is actually fetching from. Permanent redirect updates to `startingResource` are an optimization: after discovering a permanent redirect once, the node skips the intermediate URL on every subsequent poll rather than chasing the redirect every time. + +The `Site::Resource` constructor is the sole point of URI validation. It calls `parseUrl()` and then applies scheme-specific rules: `file:` URIs must have no hostname and a non-empty path; `http:` and `https:` URIs must have a hostname; absent ports default to 80 and 443 respectively. On Windows, a leading `/` is stripped from the file path. Any violation throws `std::runtime_error`, causing `load()` to return `false` immediately rather than proceeding with a broken site. + +## Async Fetch Lifecycle + +The timer is driven by Boost.ASIO's `basic_waitable_timer`. `setTimer()` scans `sites_` for the entry with the earliest `nextRefresh` timestamp and arms the timer to fire at that moment. The timer fires `onTimer()`, which resets the redirect counter, advances `nextRefresh` by the site's `refreshInterval`, and calls `makeRequest()`. + +`makeRequest()` dispatches to one of three `detail::Work` subclasses depending on scheme: +- **`WorkSSL`** — TLS-wrapped HTTP, initialized with the application's SSL context and configuration. +- **`WorkPlain`** — Plain HTTP. +- **`WorkFile`** — Reads a local file via the ASIO strand, delivering its contents as a string. + +The `Work` interface is intentionally minimal: `run()` and `cancel()`. The caller holds only a `std::weak_ptr` (`work_`) in `ValidatorSite`, which allows `stop()` to cancel work-in-progress without extending the object's lifetime — the lambda closures inside `makeRequest()` hold the strong reference for as long as the I/O operation is active. + +Completion is handled by two different callbacks: `onSiteFetch()` for HTTP/HTTPS responses (which carries a full `boost::beast::http::response`) and `onTextFetch()` for file responses (which carries the raw string). Both ultimately call `parseJsonResponse()` and then re-arm the timer via `setTimer()`. + +## Dual Use of the Timer and Request Timeout + +A subtle but important design choice: `ValidatorSite` uses the *same* `timer_` object for two distinct purposes. After `makeRequest()` starts the network operation, it immediately overwrites the timer with a fresh `expires_after(requestTimeout_)` deadline, arming `onRequestTimeout()`. The response handler (`onSiteFetch` / `onTextFetch`) begins by calling `timeoutCancel()`, which fires `timer_.cancel_one()` to discard the timeout watchdog before it fires. If the request exceeds the deadline instead, `onRequestTimeout()` calls `work_.cancel()` to abort the in-flight I/O. This works because only one purpose is active at a time, but the tight coupling means both the fetch completion callback and the timeout handler must be careful not to interfere with each other — hence the guard checking `ec != boost::asio::error::operation_aborted` in `onTimer()` and checking `site.activeResource` before logging in `onRequestTimeout()` (the comment there acknowledges the rare race where both can be queued simultaneously). + +## Concurrency and Lock Discipline + +The class uses two mutexes with a strict acquisition order documented in the header: `sites_mutex_` must always be locked *before* `state_mutex_`. The private helper methods (`setTimer`, `makeRequest`, `parseJsonResponse`, `processRedirect`) accept their required locks by `const& std::lock_guard`, a compile-time proof that callers hold the appropriate lock. This pattern prevents accidental calls from unlocked contexts while avoiding redundant lock acquisitions. + +`fetching_`, `pending_`, and `stopping_` are `std::atomic`, allowing `join()` and `stop()` to wait on a `std::condition_variable` (`cv_`) without spinning. The destructor handles the case where `stop()` may already have been initiated externally by checking `stopping_` before calling `stop()` again, and waits for `fetching_` to clear via the condition variable rather than calling `stop()` a second time. + +## JSON Response Parsing and List Application + +`parseJsonResponse()` validates the mandatory `manifest` and `version` fields, then delegates blob parsing to `ValidatorList::parseBlobs()` which understands both v1 (single blob) and v2 (multiple blobs) list formats. The function computes `sha512Half(manifest, blobs, version)` as the list's content hash and passes it to `applyListsAndBroadcast()`. That function handles cryptographic verification, deduplication, and peer broadcast. The result carries a map of `ListDisposition` outcomes — accepted, stale, untrusted, same_sequence, etc. — which are logged individually at appropriate severity levels. + +The server-controlled `refresh_interval` field in the response allows publishers to tune their polling frequency dynamically. `ValidatorSite` clamps the value to `[1min, 24h]` before applying it, protecting against misconfigured or malicious sites that might attempt to suppress future fetches by sending an extremely long interval or exhaust the scheduler with a zero interval. + +## Fallback to Locally Cached Lists + +`missingSite()` is called both during initial `load()` when the URI list is empty and inside `onSiteFetch()` when a fetch fails. It calls `ValidatorList::loadLists()` to retrieve any locally persisted copies of previously fetched validator lists, then calls `load()` on those paths. This provides resilience: a node that loses connectivity to all its configured sites can still operate with the most recently fetched list rather than being left with no validators at all. + +## Redirect Handling + +`processRedirect()` enforces a cap of `max_redirects = 3` per poll cycle and explicitly forbids `file:` scheme redirects — only HTTP and HTTPS are valid redirect destinations. The redirect counter is reset to zero at the start of each fresh `onTimer()` cycle so that a series of valid redirects spread across poll intervals does not permanently exhaust the redirect budget. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/Work.h.ai.json b/src/xrpld/app/misc/detail/Work.h.ai.json new file mode 100644 index 0000000000..e2d7e9e42a --- /dev/null +++ b/src/xrpld/app/misc/detail/Work.h.ai.json @@ -0,0 +1,35 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "Work" + } + ], + "description": "Defines an abstract base class 'Work' for asynchronous tasks with run and cancel methods, and sets up a response type alias for HTTP responses using Boost.Beast, within the xrpl::detail namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/Work.h", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "run" + }, + { + "args": [], + "lineno": 16, + "name": "cancel" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/Work.h.ai.md b/src/xrpld/app/misc/detail/Work.h.ai.md new file mode 100644 index 0000000000..412dd8e4a8 --- /dev/null +++ b/src/xrpld/app/misc/detail/Work.h.ai.md @@ -0,0 +1,44 @@ +# `detail/Work.h` — Abstract Interface for Asynchronous HTTP Fetch Tasks + +## Role in the System + +`Work.h` is the root of a small but architecturally significant abstraction layer inside `xrpld/app/misc/detail/`. It defines the `Work` interface and the shared `response_type` alias that bind together a three-way implementation family — `WorkPlain`, `WorkSSL`, and `WorkFile` — all of which drive validator-list fetching in `ValidatorSite`. + +The file exists to give `ValidatorSite` a single, transport-agnostic handle it can store, start, and cancel without knowing whether the underlying fetch goes over plain TCP, TLS, or the local filesystem. This is the classic *command* or *task object* pattern applied to async I/O. + +## The `Work` Interface + +The class is intentionally minimal: a virtual destructor and two pure virtual methods, `run()` and `cancel()`. There is no state, no constructor arguments, no coupling to Boost.Asio. That sparseness is deliberate — it allows `ValidatorSite` to hold a `std::shared_ptr` and dispatch `run()` or `cancel()` without touching any transport-specific type. + +`cancel()` is as important as `run()`. Validator-list refreshes are timer-driven; when the node is shutting down or a refresh is superseded, the in-flight operation must be abortable cleanly. Because `cancel()` is part of the interface contract, callers never need to downcast or conditionally call transport-specific shutdown methods. + +## The `response_type` Alias + +```cpp +using response_type = boost::beast::http::response; +``` + +This type alias lives at namespace scope inside `xrpl::detail` rather than inside the class. That placement is meaningful: the callback signature used by `WorkBase` passes a `response_type&&` to callers, and `ValidatorSite.cpp` references `detail::response_type` directly in its `onSiteFetch` handler. Centralizing it in this header avoids duplicating the Boost.Beast template instantiation in every consumer. + +`string_body` is chosen because validator list payloads are JSON text documents of moderate size. A streaming or dynamic-body alternative would add complexity without benefit here. + +## The Inheritance Hierarchy + +`Work.h` is the apex. The concrete types form a CRTP chain: + +- `WorkBase` inherits `Work` and implements the common async HTTP GET flow: DNS resolution → TCP connect → (optional TLS handshake via `impl()`) → HTTP write → HTTP read → callback. It uses `boost::asio::strand` to serialize all handlers for a given request. +- `WorkPlain` extends `WorkBase` for plain TCP. It exposes the raw `socket_` as `stream()`. +- `WorkSSL` extends `WorkBase` for TLS. It wraps `socket_` in a `boost::asio::ssl::stream` and adds an `onHandshake` step between connect and HTTP write. +- `WorkFile` (not shown in detail) handles `file://` URLs directly without network I/O, satisfying the same `Work` interface for local validator lists used in testing or air-gapped deployments. + +The CRTP in `WorkBase` — `impl().stream()` and `impl().onConnect()` — lets the base class drive the I/O state machine while delegating transport-specific operations to the concrete subclass without virtual dispatch on the hot path. The `Work` virtual interface is only exercised by `ValidatorSite`, not inside the async handler chain itself. + +## Lifetime and Cancellation Contract + +`WorkBase`'s destructor fires the callback with `boost::system::errc::not_a_socket` if it is destroyed while the callback is still live (i.e., before a response or explicit failure was delivered). This defensive pattern ensures `ValidatorSite` always receives exactly one callback invocation per `Work` object — either a successful response, a failure from `fail()`, or this destructor sentinel — preventing silent dropped fetches. + +`cancel()` posts to the strand if not already running on it, then cancels both the resolver and the socket. This two-phase approach is necessary because Boost.Asio resolver and socket cancellation are separate operations, and the strand ensures the cancel runs after any already-queued handlers complete. + +## Why This Design Over Alternatives + +A simpler approach would have `ValidatorSite` branch on URL scheme everywhere. The `Work` abstraction moves that branch to a single factory site in `ValidatorSite::makeRequest` (choosing `WorkSSL`, `WorkPlain`, or `WorkFile`) and then operates uniformly. This keeps the site-management logic — retry counts, redirect following, refresh scheduling — free of transport details, which is the primary maintenance benefit. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkBase.h.ai.json b/src/xrpld/app/misc/detail/WorkBase.h.ai.json new file mode 100644 index 0000000000..a4c1566587 --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkBase.h.ai.json @@ -0,0 +1,142 @@ +{ + "args": [ + { + "lineno": 44, + "name": "host" + }, + { + "lineno": 44, + "name": "path" + }, + { + "lineno": 44, + "name": "port" + }, + { + "lineno": 44, + "name": "ios" + }, + { + "lineno": 44, + "name": "lastEndpoint" + }, + { + "lineno": 44, + "name": "lastStatus" + }, + { + "lineno": 44, + "name": "cb" + } + ], + "classes": [ + { + "args": [ + "std::string const& host", + "std::string const& path", + "std::string const& port", + "boost::asio::io_context& ios", + "endpoint_type const& lastEndpoint", + "bool lastStatus", + "callback_type cb" + ], + "lineno": 18, + "name": "WorkBase" + } + ], + "description": "Defines a generic asynchronous HTTP work base class template (WorkBase) for performing HTTP GET requests using Boost.Asio and Boost.Beast, providing a framework for derived classes to implement specific HTTP work logic in the XRPL codebase.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/WorkBase.h", + "functions": [ + { + "args": [ + "std::string const& host", + "std::string const& path", + "std::string const& port", + "boost::asio::io_context& ios", + "endpoint_type const& lastEndpoint", + "bool lastStatus", + "callback_type cb" + ], + "lineno": 44, + "name": "WorkBase" + }, + { + "args": [], + "lineno": 53, + "name": "~WorkBase" + }, + { + "args": [], + "lineno": 56, + "name": "impl" + }, + { + "args": [], + "lineno": 61, + "name": "run" + }, + { + "args": [], + "lineno": 64, + "name": "cancel" + }, + { + "args": [ + "error_code const& ec" + ], + "lineno": 67, + "name": "fail" + }, + { + "args": [ + "error_code const& ec", + "results_type results" + ], + "lineno": 69, + "name": "onResolve" + }, + { + "args": [ + "error_code const& ec", + "endpoint_type const& endpoint" + ], + "lineno": 71, + "name": "onConnect" + }, + { + "args": [], + "lineno": 73, + "name": "onStart" + }, + { + "args": [ + "error_code const& ec" + ], + "lineno": 75, + "name": "onRequest" + }, + { + "args": [ + "error_code const& ec" + ], + "lineno": 77, + "name": "onResponse" + }, + { + "args": [], + "lineno": 80, + "name": "close" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkBase.h.ai.md b/src/xrpld/app/misc/detail/WorkBase.h.ai.md new file mode 100644 index 0000000000..d9415a8ed9 --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkBase.h.ai.md @@ -0,0 +1,52 @@ +# `WorkBase.h` — CRTP Foundation for Asynchronous HTTP GET Requests + +## Role in the System + +`WorkBase` is the shared implementation backbone for the XRPL node's outbound HTTP client used when fetching validator list files. The `ValidatorSite` subsystem needs to periodically download signed validator lists from remote URLs (https, http, or file paths). Rather than duplicating the full async pipeline for plain TCP and TLS connections, `WorkBase` encodes the common async state machine — DNS resolution, TCP connection, HTTP request writing, HTTP response reading — and leaves only transport-layer differences to concrete subclasses. + +The two concrete subclasses are `WorkPlain` (plain TCP, used for `http://` URLs) and `WorkSSL` (TLS, used for `https://` URLs). A third sibling, `WorkFile`, handles `file://` URLs and is entirely independent, inheriting directly from `Work` without using `WorkBase`. + +## CRTP Design + +`WorkBase` uses the Curiously Recurring Template Pattern: the template parameter `Impl` is always the concrete derived class itself (`WorkPlain` or `WorkSSL`). The `impl()` helper downcasts `this` to `Impl&`, enabling the base to invoke two customization points on the concrete type without virtual dispatch: + +- `impl().onConnect(ec)` — called by the base when TCP connection completes. `WorkPlain` proceeds directly to `onStart()`; `WorkSSL` initiates a TLS handshake first. +- `impl().stream()` — returns the writable/readable I/O stream. `WorkPlain` returns the raw `socket_type&`; `WorkSSL` returns a `boost::asio::ssl::stream`. + +Both concrete classes also inherit `std::enable_shared_from_this`, and `WorkBase` uses `impl().shared_from_this()` exclusively when binding callbacks. This is the key reason CRTP is preferred over a single virtual class: `shared_from_this()` must return `shared_ptr`, not `shared_ptr>`, so calling it through the concrete type ensures correct reference counting. + +The concrete classes declare `friend class WorkBase` to allow the base to call their private `onConnect()` and `stream()` members, keeping those implementation details out of the public API. + +## Async State Machine + +The lifecycle of a single HTTP GET proceeds through this chain, all serialized on a single `boost::asio::strand`: + +1. **`run()`** — Initiates async DNS resolution via `resolver_.async_resolve()`. If not already on the strand, it re-posts itself to the strand first. This cross-thread safety pattern repeats in `cancel()`. + +2. **`onResolve()`** — On success, calls `boost::asio::async_connect()` against the full set of resolved endpoints. + +3. **`onConnect()`** (base) → **`impl().onConnect()`** — Records the successfully connected endpoint in `lastEndpoint_`, then delegates to the concrete type. This two-level dispatch is what allows `WorkSSL` to interpose a TLS handshake before the HTTP exchange begins. + +4. **`onStart()`** — Constructs the HTTP/1.1 GET request with the `Host` header and sets the `User-Agent` to `BuildInfo::getFullVersionString()` (the rippled version string). Writes the request to `impl().stream()`. + +5. **`onRequest()`** — After the request is flushed, issues an async read into `readBuf_` (a `boost::beast::multi_buffer`) to receive the full HTTP response into `res_` (a `response_type` with string body). + +6. **`onResponse()`** — Closes the socket, fires the callback with the response, and nullifies `cb_` to prevent double-invocation. + +## Callback Contract and Error Handling + +The callback signature `void(error_code const&, endpoint_type const&, response_type&&)` carries three values: the outcome, the resolved endpoint (for connection prioritization on retry), and the response body. Callers can distinguish success from transport failure using the error code, and can use `lastEndpoint_` to prefer the same server on subsequent requests. + +`fail()` is the single failure exit point: it fires the callback with the error and then nullifies `cb_`. This nullification is the guard against double-invocation — every subsequent path in the state machine first checks `if (cb_)`. The destructor also checks and fires `cb_` if it was never cleared, reporting `not_a_socket` as the error code. This ensures the caller always receives exactly one callback regardless of how the object is destroyed — whether cleanly after `onResponse()` or abnormally when cancelled or dropped. + +`cancel()` is safe to call from any thread. It cancels the resolver and the socket (suppressing the socket's error code since cancellation is intentional), relying on the strand dispatch to serialize with other async operations. + +The `close()` helper shuts down only the send direction of the socket before closing it. This is a graceful TCP half-close: it signals to the remote server that the client is done sending, allowing the server to flush any remaining data, before the connection is fully torn down. + +## Thread Safety + +All async operations are bound to a single `boost::asio::strand`. The strand guarantees that no two handlers execute concurrently, even when the underlying `io_context` is driven by a thread pool. The `run()` and `cancel()` entry points both check `strand_.running_in_this_thread()` and re-post themselves if needed, making it safe to call from arbitrary threads without external locking. + +## Relationship to `ValidatorSite` + +`ValidatorSite.cpp` constructs `WorkPlain` or `WorkSSL` using `std::make_shared<>` depending on URL scheme, then calls `->run()`. The `lastEndpoint` and `lastStatus` constructor parameters carry state from the previous fetch cycle, allowing the validator site fetcher to track which server endpoint was last used and whether it was successful — supporting rudimentary connection affinity or retry strategies at the higher layer. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkFile.h.ai.json b/src/xrpld/app/misc/detail/WorkFile.h.ai.json new file mode 100644 index 0000000000..683b27327e --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkFile.h.ai.json @@ -0,0 +1,64 @@ +{ + "args": [ + { + "lineno": 27, + "name": "path" + }, + { + "lineno": 27, + "name": "ios" + }, + { + "lineno": 27, + "name": "cb" + } + ], + "classes": [ + { + "args": [ + "std::string const& path, boost::asio::io_context& ios, callback_type cb" + ], + "lineno": 13, + "name": "WorkFile" + } + ], + "description": "Implements the WorkFile class for asynchronous file operations using Boost.Asio, allowing file contents to be read and returned via a callback.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/WorkFile.h", + "functions": [ + { + "args": [ + "path", + "ios", + "cb" + ], + "lineno": 27, + "name": "WorkFile" + }, + { + "args": [], + "lineno": 28, + "name": "~WorkFile" + }, + { + "args": [], + "lineno": 30, + "name": "run" + }, + { + "args": [], + "lineno": 32, + "name": "cancel" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkFile.h.ai.md b/src/xrpld/app/misc/detail/WorkFile.h.ai.md new file mode 100644 index 0000000000..1806a0e0b0 --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkFile.h.ai.md @@ -0,0 +1,56 @@ +# WorkFile.h — Local-File Variant of the Work Abstraction + +## Purpose and Context + +`WorkFile` is one of three concrete implementations of the `Work` interface in the validator-list fetching subsystem. The other two — `WorkPlain` and `WorkSSL` (both derived from the CRTP template `WorkBase`) — handle HTTP and HTTPS validator list sources respectively. `WorkFile` covers the third URL scheme: `file://`, allowing a node operator to point the validator-list configuration at a path on the local filesystem instead of a remote server. + +In `ValidatorSite.cpp`, the three classes are used interchangeably: after parsing the configured URL, `ValidatorSite` constructs either a `WorkPlain`, `WorkSSL`, or `WorkFile` instance depending on the scheme, stores the result in a `std::shared_ptr`, and calls `run()`. The rest of the fetch lifecycle — cancellation on timeout, retry scheduling — operates uniformly through the base `Work` interface. + +## Class Design + +`WorkFile` inherits directly from `Work` (not from `WorkBase`) because the HTTP machinery in `WorkBase` — DNS resolution, TCP connection, Beast HTTP read/write, endpoint tracking — is entirely irrelevant for a local file read. This avoids dragging in socket state and resolver logic for a straightforward filesystem operation. + +One subtle consequence is that `WorkFile` **overrides the `response_type`** alias. At namespace scope in `Work.h`, `response_type` is defined as `boost::beast::http::response`. `WorkFile` redefines it locally as `std::string`. This lets the callback type be: + +```cpp +using callback_type = std::function; +``` + +… where `response_type` resolves to `std::string` within `WorkFile`'s scope. The HTTP-based `WorkBase::callback_type` also includes a `endpoint_type` argument (because `ValidatorSite` records the endpoint for retry prioritization), but `WorkFile`'s callback omits that: there is no network endpoint to record. + +`WorkFile` uses `std::enable_shared_from_this` so that `run()` can safely extend its own lifetime when posting to the strand, a pattern required by Asio's asynchronous dispatch model. + +## Execution Model + +`run()` uses a strand to serialize execution: + +```cpp +if (!strand_.running_in_this_thread()) + return boost::asio::post( + ios_, + boost::asio::bind_executor(strand_, std::bind(&WorkFile::run, shared_from_this()))); +``` + +If `run()` is called from outside the strand (the common case — `ValidatorSite` calls it from its own strand), it re-posts itself to `ios_` bound to the `WorkFile` strand. Once on the strand, it calls `getFileContents(ec, path_, megabytes(1))` synchronously, then fires the callback and nulls it out. + +The 1 MB cap passed to `getFileContents` is a deliberate resource guard. A malformed or hostile validator-list file cannot cause unbounded memory allocation; `getFileContents` will return an error if the file exceeds this limit. + +## Lifecycle and Safety Invariants + +The destructor encodes a critical safety invariant shared with `WorkBase`: + +```cpp +WorkFile::~WorkFile() +{ + if (cb_) + cb_(make_error_code(boost::system::errc::interrupted), {}); +} +``` + +If the object is destroyed before `run()` has fired the callback (e.g., because the work was cancelled before it started, or the `io_context` was torn down), the destructor ensures the caller's callback is always invoked exactly once — never silently dropped. `WorkBase` applies the same pattern using `errc::not_a_socket` instead of `errc::interrupted`, each chosen to signal the appropriate failure mode to the caller. + +After `run()` fires the callback it immediately sets `cb_ = nullptr`, which prevents the destructor from invoking it a second time. This single-invocation guarantee is enforced by the `XRPL_ASSERT(cb_, ...)` placed just before the call in `run()`, catching programmer errors where the callback has already been consumed. + +## `cancel()` Is a No-op + +The `cancel()` override is intentionally empty. Unlike `WorkBase::cancel()`, which must interrupt an in-flight async TCP/DNS operation, `WorkFile` has no cancellable I/O: the file read is synchronous within the posted task. Once the task has been posted, either it will run and complete, or the `io_context` will shut down and the destructor's safety callback will fire. There is no intermediate state that requires intervention. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkPlain.h.ai.json b/src/xrpld/app/misc/detail/WorkPlain.h.ai.json new file mode 100644 index 0000000000..5e5710a133 --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkPlain.h.ai.json @@ -0,0 +1,96 @@ +{ + "args": [ + { + "lineno": 15, + "name": "host" + }, + { + "lineno": 15, + "name": "path" + }, + { + "lineno": 15, + "name": "port" + }, + { + "lineno": 15, + "name": "ios" + }, + { + "lineno": 15, + "name": "lastEndpoint" + }, + { + "lineno": 15, + "name": "lastStatus" + }, + { + "lineno": 15, + "name": "cb" + }, + { + "lineno": 26, + "name": "ec" + } + ], + "classes": [ + { + "args": [ + "host", + "path", + "port", + "ios", + "lastEndpoint", + "lastStatus", + "cb" + ], + "lineno": 11, + "name": "WorkPlain" + } + ], + "description": "Implements WorkPlain, a class for performing asynchronous work over TCP/IP using Boost.Asio, as part of the xrpl::detail namespace. Inherits from WorkBase and manages plain (non-SSL) socket connections.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/WorkPlain.h", + "functions": [ + { + "args": [ + "host", + "path", + "port", + "ios", + "lastEndpoint", + "lastStatus", + "cb" + ], + "lineno": 15, + "name": "WorkPlain" + }, + { + "args": [], + "lineno": 23, + "name": "~WorkPlain" + }, + { + "args": [ + "ec" + ], + "lineno": 26, + "name": "onConnect" + }, + { + "args": [], + "lineno": 29, + "name": "stream" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkPlain.h.ai.md b/src/xrpld/app/misc/detail/WorkPlain.h.ai.md new file mode 100644 index 0000000000..1a4d4d0db5 --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkPlain.h.ai.md @@ -0,0 +1,36 @@ +# `WorkPlain.h` — Plain TCP Work Implementation + +`WorkPlain` is the plain-TCP (unencrypted) concrete implementation of the asynchronous HTTP work hierarchy used by the XRPL validator site fetching infrastructure. It lives in `xrpl::detail` alongside its counterpart `WorkSSL` (HTTPS) and `WorkFile` (filesystem), and is selected by `ValidatorSite.cpp` whenever a validator list endpoint uses an `http://` URL scheme. + +## Role in the Work Hierarchy + +The validator site subsystem abstracts transport behind a three-class family: + +- `Work` — root interface with `run()` and `cancel()`. +- `WorkBase` — CRTP template implementing the full async state machine: DNS resolution, TCP connection, HTTP request write, HTTP response read, and callback delivery. +- `WorkPlain` / `WorkSSL` / `WorkFile` — concrete leaves providing transport-specific behaviour. + +`WorkPlain` is the simplest leaf. Because plain TCP needs nothing beyond an established socket before HTTP can begin, its role within the CRTP contract reduces to exactly two methods. + +## CRTP Customization Points + +`WorkBase` calls two methods on the derived type — without virtual dispatch — using a `static_cast` through `impl()`: + +- **`stream()`** — `WorkBase` passes this to Boost.Beast's `async_write` and `async_read` calls. `WorkPlain::stream()` returns a plain `socket_type&` (a `boost::asio::ip::tcp::socket`). In contrast, `WorkSSL::stream()` returns an `ssl::stream`, which is why the customization point exists at all. +- **`onConnect(error_code)`** — called by `WorkBase::onConnect` (the base-class overload that captures the resolved endpoint) after a successful TCP connection. For `WorkPlain`, this is a two-line method: if `ec` is set, propagate failure via `fail(ec)`; otherwise, call `onStart()` to begin the HTTP exchange immediately. `WorkSSL` inserts a TLS handshake step here before calling `onStart()`. + +`WorkBase` is declared a `friend` so it can invoke the private `onConnect()` and `stream()` methods. This keeps the lifecycle hooks invisible outside the base, enforcing that external callers only ever interact through the `Work` interface. + +## Lifetime Management + +`WorkPlain` inherits from `std::enable_shared_from_this` because every async Boost.Asio operation captures the owning `shared_ptr` in its completion handler. `WorkBase::run()` and every subsequent handler call `impl().shared_from_this()` to extend the object's lifetime until the entire chain completes: resolve → connect → write → read → callback. Without this pattern, the object could be destroyed while outstanding handlers still hold function pointers into it. + +## Design Decisions + +All method bodies are defined inline in the header. Given that the constructor is a single delegating initializer and `onConnect` is two lines, a separate `.cpp` file would be nearly empty. `WorkSSL`, by contrast, has its own translation unit because `onConnect` must initiate an asynchronous TLS handshake — non-trivial enough to warrant separation. + +The constructor parameters `lastEndpoint` and `lastStatus` carry state from the previous fetch cycle. `ValidatorSite` uses these to track which resolved endpoint was last used successfully, enabling connection affinity — a hint to prefer the same server on the next refresh — without encoding that logic inside `WorkPlain` itself. + +## Relationship to Sibling Files + +Understanding `WorkPlain` in isolation is only half the picture. The real logic lives in `WorkBase.h`, which owns the entire async state machine and calls back into `WorkPlain` only at the two customization points described above. `Work.h` provides the `response_type` alias (`boost::beast::http::response`) and the pure-virtual interface that lets `ValidatorSite` hold any of the three work types as a uniform `shared_ptr`. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkSSL.cpp.ai.json b/src/xrpld/app/misc/detail/WorkSSL.cpp.ai.json new file mode 100644 index 0000000000..a4b414850f --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkSSL.cpp.ai.json @@ -0,0 +1,349 @@ +{ + "args": [ + { + "lineno": 6, + "name": "host" + }, + { + "lineno": 7, + "name": "path" + }, + { + "lineno": 8, + "name": "port" + }, + { + "lineno": 9, + "name": "ios" + }, + { + "lineno": 10, + "name": "j" + }, + { + "lineno": 11, + "name": "config" + }, + { + "lineno": 12, + "name": "lastEndpoint" + }, + { + "lineno": 13, + "name": "lastStatus" + }, + { + "lineno": 14, + "name": "cb" + }, + { + "lineno": 24, + "name": "ec" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "WorkSSL::WorkSSL", + "context_.preConnectVerify(stream_, host_)" + ], + "entry_point": "WorkSSL::WorkSSL", + "purpose": "Constructor initializes SSL context and validates SSL parameters and host before connection.", + "validation_points": [ + "context_.preConnectVerify(stream_, host_)" + ] + }, + { + "call_chain": [ + "WorkSSL::onConnect", + "context_.postConnectVerify(stream_, host_)", + "stream_.async_handshake(...)" + ], + "entry_point": "WorkSSL::onConnect", + "purpose": "Handles post-connect SSL validation and initiates SSL handshake if validation passes.", + "validation_points": [ + "context_.postConnectVerify(stream_, host_)", + "if (err) { fail(err); return; }" + ] + }, + { + "call_chain": [ + "WorkSSL::onHandshake", + "if (ec) { fail(ec); return; }", + "onStart()" + ], + "entry_point": "WorkSSL::onHandshake", + "purpose": "Handles result of SSL handshake, fails on error, otherwise proceeds to start work.", + "validation_points": [ + "if (ec) { fail(ec); return; }" + ] + } + ], + "data_flows": [ + { + "field": "host_", + "flow": [ + "WorkSSL::WorkSSL(host, ...)", + "host_ (member variable)", + "context_.preConnectVerify(stream_, host_)", + "context_.postConnectVerify(stream_, host_)" + ], + "origin": "Passed as 'host' parameter to WorkSSL::WorkSSL", + "transformations": [ + "Stored as member variable", + "Passed to SSL context validation functions" + ], + "validated_at": "context_.preConnectVerify and context_.postConnectVerify" + }, + { + "field": "stream_", + "flow": [ + "WorkSSL::WorkSSL", + "stream_ (member variable)", + "context_.preConnectVerify(stream_, host_)", + "context_.postConnectVerify(stream_, host_)", + "stream_.async_handshake(...)" + ], + "origin": "Constructed in WorkSSL::WorkSSL from socket_ and context_.context()", + "transformations": [ + "Used for SSL operations and validation" + ], + "validated_at": "context_.preConnectVerify and context_.postConnectVerify" + }, + { + "field": "error_code (ec)", + "flow": [ + "onConnect(ec)", + "if (ec) ... else context_.postConnectVerify", + "onHandshake(ec)", + "if (ec) ... else onStart()" + ], + "origin": "Passed to onConnect/onHandshake from async operations", + "transformations": [ + "Checked for error, triggers fail() on error" + ], + "validated_at": "if (ec) checks in onConnect and onHandshake" + } + ], + "description": "Implements the WorkSSL class, which handles asynchronous SSL/TLS connections and handshakes for network operations in the XRPL application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "SSL context parameters (directory, file, verify mode)", + "validation", + "missing", + "check" + ], + "evidence": "Field SSL context parameters (directory, file, verify mode) validated by Custom SSL context validation (context_.preConnectVerify, context_.postConnectVerify), Boost.Asio error_code", + "issue_pattern": "Missing validation for SSL context parameters (directory, file, verify mode)", + "why_false_positive": "Custom SSL context validation (context_.preConnectVerify, context_.postConnectVerify), Boost.Asio error_code validates SSL context parameters (directory, file, verify mode) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "host_ (for SSL verification)", + "validation", + "missing", + "check" + ], + "evidence": "Field host_ (for SSL verification) validated by Custom SSL context validation (context_.preConnectVerify, context_.postConnectVerify), Boost.Asio error_code", + "issue_pattern": "Missing validation for host_ (for SSL verification)", + "why_false_positive": "Custom SSL context validation (context_.preConnectVerify, context_.postConnectVerify), Boost.Asio error_code validates host_ (for SSL verification) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "error_code from Boost.Asio operations", + "validation", + "missing", + "check" + ], + "evidence": "Field error_code from Boost.Asio operations validated by Custom SSL context validation (context_.preConnectVerify, context_.postConnectVerify), Boost.Asio error_code", + "issue_pattern": "Missing validation for error_code from Boost.Asio operations", + "why_false_positive": "Custom SSL context validation (context_.preConnectVerify, context_.postConnectVerify), Boost.Asio error_code validates error_code from Boost.Asio operations automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "SSL context and host (SSL parameters, host_)", + "empty", + "string", + "validation" + ], + "evidence": "context_.preConnectVerify(stream_, host_) at WorkSSL constructor", + "issue_pattern": "Missing empty string validation for SSL context and host (SSL parameters, host_)", + "why_false_positive": "context_.preConnectVerify(stream_, host_) validates SSL context and host (SSL parameters, host_) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "SSL context and host (SSL parameters, host_)", + "empty", + "string", + "validation" + ], + "evidence": "context_.postConnectVerify(stream_, host_) at WorkSSL::onConnect", + "issue_pattern": "Missing empty string validation for SSL context and host (SSL parameters, host_)", + "why_false_positive": "context_.postConnectVerify(stream_, host_) validates SSL context and host (SSL parameters, host_) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "error_code from async operations", + "empty", + "string", + "validation" + ], + "evidence": "if (ec) ... fail(ec) at WorkSSL::onConnect, WorkSSL::onHandshake", + "issue_pattern": "Missing empty string validation for error_code from async operations", + "why_false_positive": "if (ec) ... fail(ec) validates error_code from async operations for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "error_code from async operations", + "type", + "validation", + "check" + ], + "evidence": "if (ec) ... fail(ec) at WorkSSL::onConnect, WorkSSL::onHandshake", + "issue_pattern": "Missing type validation for error_code from async operations", + "why_false_positive": "if (ec) ... fail(ec) validates error_code from async operations type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/WorkSSL.cpp", + "functions": [ + { + "args": [ + "host", + "path", + "port", + "ios", + "j", + "config", + "lastEndpoint", + "lastStatus", + "cb" + ], + "lineno": 5, + "name": "WorkSSL::WorkSSL" + }, + { + "args": [ + "ec" + ], + "lineno": 23, + "name": "WorkSSL::onConnect" + }, + { + "args": [ + "ec" + ], + "lineno": 39, + "name": "WorkSSL::onHandshake" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 1, + "name": "xrpl" + }, + { + "lineno": 2, + "name": "detail" + } + ], + "test_coverage_notes": "This code is likely tested indirectly via integration or functional tests that exercise SSL connections (e.g., network fetches, peer connections). Direct unit tests for WorkSSL may not exist unless there are specific test files for SSL work in the test/ or unittest/ directories (e.g., test_work_ssl.cpp, WorkSSL_test.cpp). Validation logic in context_.preConnectVerify and postConnectVerify may be tested separately in SSL context tests. Gaps: No evidence of direct unit tests for error handling paths (failures in preConnectVerify, postConnectVerify, or handshake errors). Exception throwing in constructor may be hard to test unless explicitly covered.", + "validation_architecture": { + "auto_validated_fields": [ + "SSL context parameters (directory, file, verify mode)", + "host_ (for SSL verification)", + "error_code from Boost.Asio operations" + ], + "framework": "Custom SSL context validation (context_.preConnectVerify, context_.postConnectVerify), Boost.Asio error_code", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "std::runtime_error (via Throw)", + "field": "SSL context and host (SSL parameters, host_)", + "location": "WorkSSL constructor", + "validated_by": "context_.preConnectVerify(stream_, host_)", + "validates": [ + "SSL context is valid and properly configured", + "Host name is valid for SSL verification", + "SSL certificates and trust chain are valid (pre-connect)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "fail(err) (likely throws or signals error via callback)", + "field": "SSL context and host (SSL parameters, host_)", + "location": "WorkSSL::onConnect", + "validated_by": "context_.postConnectVerify(stream_, host_)", + "validates": [ + "SSL context and host are valid after connection", + "SSL certificates and trust chain are valid (post-connect)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "fail(ec) (likely throws or signals error via callback)", + "field": "error_code from async operations", + "location": "WorkSSL::onConnect, WorkSSL::onHandshake", + "validated_by": "if (ec) ... fail(ec)", + "validates": [ + "Checks if error_code is set (non-zero)", + "Ensures no error occurred in async connect/handshake" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkSSL.cpp.ai.md b/src/xrpld/app/misc/detail/WorkSSL.cpp.ai.md new file mode 100644 index 0000000000..5b17b7738f --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkSSL.cpp.ai.md @@ -0,0 +1,40 @@ +# `WorkSSL.cpp` — TLS-Secured HTTP Client Worker + +`WorkSSL.cpp` provides the SSL/TLS concrete implementation of the asynchronous HTTP client hierarchy used within `rippled` to fetch data from remote HTTPS endpoints — most notably for fetching validator lists, amendment data, and other signed content that the node retrieves from external web services. + +## Role in the Work Hierarchy + +The file implements a three-level CRTP inheritance chain: `Work` (pure virtual interface with `run()` and `cancel()`) → `WorkBase` (template base holding the Boost.Asio plumbing — resolver, socket, request/response state, and strand) → `WorkSSL` (this file; SSL stream layered on top of the base TCP socket). The sibling `WorkPlain` performs the same job over unencrypted TCP, and its `onConnect` is trivially just a null-check followed by `onStart()`. `WorkSSL`'s `onConnect` must do substantially more: it must validate the SSL context after the TCP layer succeeds and then perform an async TLS handshake before anything is written. + +Because `WorkBase` is a class template parameterised on `Impl`, it calls `impl().onConnect()` via the CRTP cast, dispatching to either `WorkSSL::onConnect` or `WorkPlain::onConnect` at compile time without a virtual call. The same pattern applies to `impl().stream()`: `WorkPlain::stream()` returns a raw `socket_type&`, while `WorkSSL::stream()` returns the `boost::asio::ssl::stream` — the two are plug-compatible from `WorkBase`'s perspective because `async_write`/`async_read` accept any stream satisfying the Beast `AsyncStream` concept. + +## Construction and Pre-connect Verification + +The constructor initialises `context_` (an `HTTPClientSSLContext`) from three `Config` fields: `SSL_VERIFY_DIR`, `SSL_VERIFY_FILE`, and `SSL_VERIFY`. `HTTPClientSSLContext` registers system CA certificates (or loads a custom verify file), optionally appending a directory of additional trust anchors. The `stream_` is then constructed by wrapping the base class's `socket_` with the SSL context — notably the stream takes `socket_` *by reference*, so the same socket object owned by `WorkBase` is reused. + +Before `WorkSSL` construction completes, `preConnectVerify()` is called synchronously. This call uses OpenSSL's `SSL_set_tlsext_host_name` to configure the Server Name Indication (SNI) extension, which must be set *before* the TCP connection is established so the correct TLS server certificate can be selected on the remote end. If `SSL_VERIFY` is disabled in config, `preConnectVerify` also sets `verify_none` on the stream. If SNI configuration fails, an `error_code` is returned and the constructor throws `std::runtime_error` via the `Throw<>` macro. This is the only place in the pipeline where a synchronous exception is appropriate: there is no async callback registered yet, so there is no other way to report a hard configuration failure back to the caller. + +## The Async Pipeline Extension + +After `WorkBase::run()` resolves the host and establishes the TCP connection, `WorkBase::onConnect` records the endpoint and delegates to `impl().onConnect(ec)`. For `WorkSSL` this is `WorkSSL::onConnect`: + +```cpp +void WorkSSL::onConnect(error_code const& ec) +{ + auto err = ec ? ec : context_.postConnectVerify(stream_, host_); + if (err) { fail(err); return; } + stream_.async_handshake(..., &WorkSSL::onHandshake, ...); +} +``` + +`postConnectVerify` runs after the TCP layer is up but before any TLS bytes are sent. When peer verification is enabled (`SSL_VERIFY=true`), it calls `set_verify_peer` and then installs an RFC 6125 hostname verification callback (`HTTPClientSSLContext::rfc6125_verify`) that delegates to Boost.Asio's `host_name_verification`, logging a warning if the certificate's CN/SAN does not match `host_`. If peer verification is disabled, `postConnectVerify` is a no-op. Any error from either step causes an immediate `fail()` call, which delivers the error code to the registered callback and clears the callback pointer to prevent double-delivery. + +Only if both SSL setup steps succeed does `WorkSSL` issue `stream_.async_handshake(client, ...)`, passing `shared_from_this()` through `bind_executor` to keep the object alive while the TLS negotiation is in flight. + +`onHandshake` is minimal by design: check the error code, call `fail()` on failure, or call `onStart()` on success. `onStart()` is defined on `WorkBase` and initiates the HTTP GET by calling `async_write` on `impl().stream()` — which at this point is the fully-negotiated SSL stream. + +## Error Handling and Lifetime + +Two distinct error-reporting mechanisms are in play. Hard synchronous failures (bad SSL config at construction time) throw exceptions immediately. All async failures — TCP connect errors, `postConnectVerify` errors, handshake errors, write errors, and read errors — are reported through the `fail()` method, which invokes `cb_` exactly once and then nulls it. `WorkBase`'s destructor also fires `cb_` if it was never called (with `not_a_socket`), which covers cancellation paths. This ensures the caller always receives exactly one callback regardless of which async stage fails. + +The `strand_` inherited from `WorkBase` serialises all async completions. Every `bind_executor` call in both `WorkBase` and `WorkSSL` wraps handlers in this strand, guaranteeing that `onConnect`, `onHandshake`, `onStart`, `onRequest`, and `onResponse` never execute concurrently even if the `io_context` runs on multiple threads. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkSSL.h.ai.json b/src/xrpld/app/misc/detail/WorkSSL.h.ai.json new file mode 100644 index 0000000000..069b0ad8b3 --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkSSL.h.ai.json @@ -0,0 +1,115 @@ +{ + "args": [ + { + "lineno": 23, + "name": "host" + }, + { + "lineno": 24, + "name": "path" + }, + { + "lineno": 25, + "name": "port" + }, + { + "lineno": 26, + "name": "ios" + }, + { + "lineno": 27, + "name": "j" + }, + { + "lineno": 28, + "name": "config" + }, + { + "lineno": 29, + "name": "lastEndpoint" + }, + { + "lineno": 30, + "name": "lastStatus" + }, + { + "lineno": 31, + "name": "cb" + }, + { + "lineno": 42, + "name": "ec" + } + ], + "classes": [ + { + "args": [ + "host", + "path", + "port", + "ios", + "j", + "config", + "lastEndpoint", + "lastStatus", + "cb" + ], + "lineno": 15, + "name": "WorkSSL" + } + ], + "description": "Defines the WorkSSL class, which handles asynchronous work over SSL connections as part of the XRPL application's networking layer.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/WorkSSL.h", + "functions": [ + { + "args": [ + "host", + "path", + "port", + "ios", + "j", + "config", + "lastEndpoint", + "lastStatus", + "cb" + ], + "lineno": 22, + "name": "WorkSSL" + }, + { + "args": [], + "lineno": 32, + "name": "~WorkSSL" + }, + { + "args": [], + "lineno": 36, + "name": "stream" + }, + { + "args": [ + "ec" + ], + "lineno": 42, + "name": "onConnect" + }, + { + "args": [ + "ec" + ], + "lineno": 45, + "name": "onHandshake" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/WorkSSL.h.ai.md b/src/xrpld/app/misc/detail/WorkSSL.h.ai.md new file mode 100644 index 0000000000..9fd22f644d --- /dev/null +++ b/src/xrpld/app/misc/detail/WorkSSL.h.ai.md @@ -0,0 +1,44 @@ +# `WorkSSL.h` — TLS-secured HTTP Work Unit + +## Role in the System + +`WorkSSL` is the TLS-layer specialization of the XRPL node's outbound HTTP client machinery. It lives alongside `WorkPlain` as one of two concrete implementations of `Work`, the abstract interface for "a single asynchronous HTTP GET request." The broader mechanism is used by the validator list fetcher, amendment voting subsystem, and other services that need to pull data from external HTTPS endpoints (typically `vl.ripple.com` and similar trusted sources). + +## CRTP Inheritance Chain + +The design uses the Curiously Recurring Template Pattern to share connection and I/O logic without virtual dispatch overhead. `Work` (in `Work.h`) provides a minimal interface: `run()` and `cancel()`. `WorkBase` is a template that handles DNS resolution, TCP connection, HTTP request construction, and HTTP response reading — the complete request lifecycle except for the stream type and the post-TCP-connect handshake. The template calls back into `Impl` through two customization points: `impl().stream()` to get the I/O stream to read/write, and `impl().onConnect(ec)` to perform any transport-layer setup once the TCP socket is established. + +`WorkSSL` and `WorkPlain` are the two `Impl` specializations. `WorkPlain::onConnect` trivially calls `onStart()` immediately; `WorkSSL::onConnect` must first run the TLS handshake. The friend declaration `friend class WorkBase` gives the base template access to the private `onConnect` and `stream` members — the CRTP coupling is deliberate and contained. + +## TLS Stream Ownership Model + +A key detail in the member layout: + +```cpp +HTTPClientSSLContext context_; +stream_type stream_; // boost::asio::ssl::stream +``` + +`stream_type` wraps a *reference* to `socket_` (owned by `WorkBase`), not a copy. This is mandatory because Asio's SSL stream takes ownership of async operations on the underlying socket through the reference, while the raw TCP socket object must remain in scope for its lifetime. By wrapping `socket_&`, `WorkSSL` layered the TLS state on top without transferring socket ownership. + +## Two-Phase SSL Verification + +The OpenSSL/Asio interface imposes a temporal ordering constraint that forces SSL setup to be split across two points in the connection lifecycle. `WorkSSL` handles this explicitly: + +**Constructor (before TCP connect):** `context_.preConnectVerify(stream_, host_)` calls `SSL_set_tlsext_host_name` to configure the TLS SNI extension. SNI tells the server which certificate to present during the handshake and *must* be set before the TCP connection is initiated. If this fails (an OpenSSL error), the constructor throws `std::runtime_error` immediately — there is no valid state to recover to. + +**`onConnect` (after TCP connect, before TLS handshake):** `context_.postConnectVerify(stream_, host_)` installs the peer verification mode and callback. If `SSL_VERIFY` is enabled in config, it sets `verify_peer` mode and registers `rfc6125_verify` as the certificate check callback, which delegates to Asio's `host_name_verification` (RFC 6125 hostname matching) and logs a warning on failure. This cannot be done earlier because the SSL stream's verify settings require the stream to be in a connected state. + +Only after both checks pass does the async TLS handshake begin. `onHandshake` then calls `onStart()` to transmit the HTTP request over the now-encrypted channel. + +## Configuration Surface + +`HTTPClientSSLContext` is constructed from three config values: `SSL_VERIFY_DIR`, `SSL_VERIFY_FILE`, and `SSL_VERIFY`. The TLS protocol is fixed to `tlsv12_client` — hardcoded in `WorkSSL.cpp`, not exposed as a parameter. This is a deliberate minimum-version floor for outbound connections rather than a negotiable option. If a verify file is provided, the system certificate store is bypassed entirely; if a verify directory is provided, it is added as an additional search path on top of the default store. + +## Lifetime and Concurrency Safety + +All async callbacks are bound to a `strand_` (inherited from `WorkBase`) and capture `shared_from_this()`, which returns a `shared_ptr` (via `std::enable_shared_from_this`). This guarantees the object outlives all pending async operations and that callbacks never race with each other even when the underlying `io_context` runs on a thread pool. The `callback_type` (a `std::function` holding the caller's completion handler) is nulled out after first invocation to prevent double-delivery — critical because the destructor of `WorkBase` fires a synthetic error callback if one is still pending. + +## Relationship to `WorkPlain` + +`WorkPlain` is structurally identical but omits `HTTPClientSSLContext` and `stream_type` entirely — its `stream()` method returns `socket_` directly. The symmetry makes the two classes useful for understanding each other: every complexity in `WorkSSL` relative to `WorkPlain` corresponds to something SSL requires that plain TCP does not. \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/setup_HashRouter.cpp.ai.json b/src/xrpld/app/misc/detail/setup_HashRouter.cpp.ai.json new file mode 100644 index 0000000000..c11350db38 --- /dev/null +++ b/src/xrpld/app/misc/detail/setup_HashRouter.cpp.ai.json @@ -0,0 +1,234 @@ +{ + "args": [ + { + "lineno": 7, + "name": "config" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "setup_HashRouter", + "set(tmp, \"hold_time\", section)", + "set(tmp, \"relay_time\", section)" + ], + "entry_point": "setup_HashRouter", + "purpose": "Reads configuration values for HashRouter, validates them, and constructs a Setup object.", + "validation_points": [ + "After set() for hold_time: range check (tmp < 12)", + "After set() for relay_time: range check (tmp < 8)", + "After both: relay_time vs hold_time comparison" + ] + } + ], + "data_flows": [ + { + "field": "hold_time", + "flow": [ + "Config.section(\"hashrouter\")", + "set(tmp, \"hold_time\", section)", + "tmp (int)", + "range check (tmp < 12)", + "setup.holdTime = seconds(tmp)", + "HashRouter::Setup.holdTime" + ], + "origin": "Config.section(\"hashrouter\")", + "transformations": [ + "String/int value from config parsed into tmp (int)", + "Range checked (must be >= 12)", + "Converted to std::chrono::seconds", + "Assigned to setup.holdTime" + ], + "validated_at": "Immediately after set() call for hold_time" + }, + { + "field": "relay_time", + "flow": [ + "Config.section(\"hashrouter\")", + "set(tmp, \"relay_time\", section)", + "tmp (int)", + "range check (tmp < 8)", + "setup.relayTime = seconds(tmp)", + "HashRouter::Setup.relayTime" + ], + "origin": "Config.section(\"hashrouter\")", + "transformations": [ + "String/int value from config parsed into tmp (int)", + "Range checked (must be >= 8)", + "Converted to std::chrono::seconds", + "Assigned to setup.relayTime" + ], + "validated_at": "Immediately after set() call for relay_time" + }, + { + "field": "relay_time vs hold_time", + "flow": [ + "setup.holdTime and setup.relayTime assigned", + "if (setup.relayTime > setup.holdTime)", + "Throw if invalid" + ], + "origin": "setup.holdTime and setup.relayTime (after assignment)", + "transformations": [ + "Comparison between two std::chrono::seconds values" + ], + "validated_at": "After both holdTime and relayTime are set" + } + ], + "description": "Configures and validates the setup parameters for the HashRouter component based on configuration input, ensuring timing constraints are met.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hold_time", + "empty", + "string", + "validation" + ], + "evidence": "set() function and explicit range check at setup_HashRouter", + "issue_pattern": "Missing empty string validation for hold_time", + "why_false_positive": "set() function and explicit range check validates hold_time for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "hold_time", + "range", + "bounds", + "validation" + ], + "evidence": "set() function and explicit range check at setup_HashRouter", + "issue_pattern": "Missing range validation for hold_time", + "why_false_positive": "set() function and explicit range check validates hold_time range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "relay_time", + "empty", + "string", + "validation" + ], + "evidence": "set() function and explicit range check at setup_HashRouter", + "issue_pattern": "Missing empty string validation for relay_time", + "why_false_positive": "set() function and explicit range check validates relay_time for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "relay_time", + "range", + "bounds", + "validation" + ], + "evidence": "set() function and explicit range check at setup_HashRouter", + "issue_pattern": "Missing range validation for relay_time", + "why_false_positive": "set() function and explicit range check validates relay_time range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "relay_time vs hold_time", + "empty", + "string", + "validation" + ], + "evidence": "explicit comparison at setup_HashRouter", + "issue_pattern": "Missing empty string validation for relay_time vs hold_time", + "why_false_positive": "explicit comparison validates relay_time vs hold_time for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/detail/setup_HashRouter.cpp", + "functions": [ + { + "args": [ + "config" + ], + "lineno": 6, + "name": "setup_HashRouter" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage would be in unit tests for configuration and HashRouter setup, likely in files such as 'test_hashrouter.cpp', 'test_config.cpp', or similar. Tests should cover: (1) valid config values, (2) hold_time < 12 triggers error, (3) relay_time < 8 triggers error, (4) relay_time > hold_time triggers error, (5) missing values. Gaps may exist if there are no negative tests for each validation branch, or if config parsing is not mocked/fuzzed for edge cases. No test files are referenced in this code, so actual coverage must be checked in the test directory.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (set() function, manual checks)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "hold_time", + "location": "setup_HashRouter", + "validated_by": "set() function and explicit range check", + "validates": [ + "Ensures hold_time is present in config (via set())", + "Ensures hold_time >= 12" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "relay_time", + "location": "setup_HashRouter", + "validated_by": "set() function and explicit range check", + "validates": [ + "Ensures relay_time is present in config (via set())", + "Ensures relay_time >= 8" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "relay_time vs hold_time", + "location": "setup_HashRouter", + "validated_by": "explicit comparison", + "validates": [ + "Ensures relay_time <= hold_time" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/detail/setup_HashRouter.cpp.ai.md b/src/xrpld/app/misc/detail/setup_HashRouter.cpp.ai.md new file mode 100644 index 0000000000..ab001934b2 --- /dev/null +++ b/src/xrpld/app/misc/detail/setup_HashRouter.cpp.ai.md @@ -0,0 +1,36 @@ +# `setup_HashRouter.cpp` — HashRouter Configuration Parser + +## Role in the System + +`setup_HashRouter.cpp` sits in the `detail/` subfolder of `src/xrpld/app/misc/` and implements a single free function: `xrpl::setup_HashRouter(Config const&)`. Its sole responsibility is to read timing parameters from the `[hashrouter]` section of the node's configuration file, validate them against XRPL-specific invariants, and return a populated `HashRouter::Setup` struct that the `HashRouter` constructor will store and use for the lifetime of the process. + +This follows a recurring pattern in the rippled codebase: each subsystem with configurable parameters has a corresponding `setup_*` function (usually in `detail/`) that owns the parsing and validation logic, keeping the subsystem class itself decoupled from the raw `Config` object. + +## What `HashRouter::Setup` Controls + +`HashRouter` is the peer-message deduplication table for the P2P overlay. When a transaction or validation arrives, its hash is stored in an `aged_unordered_map`; subsequent duplicate messages from any peer are suppressed using this table. Two timing fields in `HashRouter::Setup` control its behavior: + +- **`holdTime`** (default 300 s): the expiration lifetime of a hash entry. Once an entry ages out of the map, the same hash can be accepted again — necessary for long-lived objects that might legitimately re-appear. +- **`relayTime`** (default 30 s): the minimum interval before a previously-relayed message may be relayed a second time. This dampens relay storms without permanently suppressing legitimate re-broadcasts. + +## Parsing and Validation Logic + +The function uses the `set()` utility from `BasicConfig.h` to optionally read each key from the config section. If a key is absent, `set()` returns `false` and the field retains its default value, so neither `hold_time` nor `relay_time` is mandatory in the config file. + +When a value is present, three invariants are enforced and any violation throws `std::runtime_error`, aborting startup: + +1. **`hold_time >= 12` seconds** — the error message calls this "the approximate validation time for three ledgers." The XRPL ledger closes roughly every 4 seconds, so 12 seconds ensures a hash entry outlives a full three-ledger validation cycle before it can be evicted. + +2. **`relay_time >= 8` seconds** — similarly called "the approximate validation time for two ledgers." This floors the relay-dampening window at two consensus rounds to prevent re-relay storms that would swamp the network before validators have had a chance to converge. + +3. **`relay_time <= hold_time`** — a cross-field consistency check. If `relayTime` exceeded `holdTime`, an entry could expire from the map before its relay-cooldown elapsed, making the relay throttle meaningless and opening a narrow window for duplicate relays of the same message. + +The floor values are derived from ledger timing constants rather than arbitrary numbers. The comments in the exception messages make this explicit, tying the constraints to observable network behavior rather than internal implementation details. + +## Why These Defaults Are Deliberately Undocumented + +The `HashRouter::Setup` comment in the header states that while the fields are configurable, they are undocumented in the user-facing configuration guide. Changing them without network-wide coordination could cause nodes to disagree on how long to suppress duplicate messages, potentially fragmenting message propagation across the overlay. The `setup_HashRouter` function acts as the only gate between the config file and these values, making it the right place to enforce the minimum safety envelope even for operators who override the defaults. + +## Relationship to Sibling Files + +The declaration lives in `src/xrpld/app/misc/setup_HashRouter.h`, which simply forward-declares `Config` and re-exports the `HashRouter::Setup` return type from ``. The implementation in `detail/` follows the convention of hiding all `Config` parsing machinery from callers — application code that constructs a `HashRouter` only sees the strongly-typed `Setup` struct, never the raw string-map config section. \ No newline at end of file diff --git a/src/xrpld/app/misc/make_NetworkOPs.h.ai.json b/src/xrpld/app/misc/make_NetworkOPs.h.ai.json new file mode 100644 index 0000000000..c10904cf39 --- /dev/null +++ b/src/xrpld/app/misc/make_NetworkOPs.h.ai.json @@ -0,0 +1,43 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 12, + "name": "LedgerMaster" + }, + { + "args": [], + "lineno": 13, + "name": "ValidatorKeys" + } + ], + "description": "This header file declares the factory function make_NetworkOPs for creating a NetworkOPs instance, and forward-declares related classes and includes necessary dependencies for network operations in the XRPL server.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/make_NetworkOPs.h", + "functions": [ + { + "args": [ + "registry", + "clock", + "standalone", + "minPeerCount", + "startValid", + "jobQueue", + "ledgerMaster", + "validatorKeys", + "ioCtx", + "journal", + "collector" + ], + "lineno": 15, + "name": "make_NetworkOPs" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/make_NetworkOPs.h.ai.md b/src/xrpld/app/misc/make_NetworkOPs.h.ai.md new file mode 100644 index 0000000000..bf23d7cbba --- /dev/null +++ b/src/xrpld/app/misc/make_NetworkOPs.h.ai.md @@ -0,0 +1,25 @@ +# `make_NetworkOPs.h` — Factory Declaration for the Network Operations Subsystem + +This header exposes a single factory function, `make_NetworkOPs`, that constructs and returns the node's central `NetworkOPs` object. It is the only public surface for creating what is one of the most complex objects in `rippled`: the component that orchestrates consensus participation, transaction submission, subscription management, and server-state tracking for an XRPL node. + +## The Factory Pattern and Why It Matters Here + +The concrete implementation, `NetworkOPsImp`, is defined entirely inside `NetworkOPs.cpp` and never exposed in any header. Callers only ever see the abstract `NetworkOPs` interface (pulled in via ``). This strict separation means the thousands of lines in `NetworkOPs.cpp` never contaminate compilation units that only need to call into `NetworkOPs` methods — a meaningful build-time benefit given how widely the object is used. The pattern matches other factory headers in the same directory (`make_Overlay.h`, `setup_HashRouter.h`), reflecting a consistent subsystem-construction idiom across the codebase. + +Ownership is transferred to the caller as `std::unique_ptr`, making the lifetime contract unambiguous: whoever calls this function owns the object and is responsible for its destruction. + +## Parameter Roles and Their Influence on Initial State + +The parameters are more than passive configuration — several actively shape the `NetworkOPsImp` state machine at construction time: + +- **`standalone`** — a boolean that, when true, tells the node it will never have peers. This eliminates peer-count quorum checks and is used for private or testing deployments. +- **`startValid`** — when true, the node's `OperatingMode` is initialized to `FULL` rather than `DISCONNECTED`. The constructor also sets `minPeerCount_` to zero in this case, because a node declared valid at startup does not need to wait for peer connections before processing ledgers. +- **`minPeerCount`** — otherwise taken from `config_->NETWORK_QUORUM` in `Application.cpp`; it sets the floor of connected peers required before the node considers itself networked. + +These three flags together encode the node's participation role at startup and are the primary inputs that drive how quickly (or whether) the node transitions from `DISCONNECTED` through `SYNCING` to `FULL`. + +The remaining parameters are injected services that `NetworkOPsImp` holds by reference or shared ownership throughout its lifetime: `ServiceRegistry` provides access to other application-level services; `clock` is the monotonic clock used for timing consensus rounds and heartbeat timers; `JobQueue` offloads async work; `LedgerMaster` is the authoritative ledger store; `ValidatorKeys` carries the node's validator identity and master public key; `ioCtx` backs the internal Asio timers (`heartbeatTimer_`, `clusterTimer_`, `accountHistoryTxTimer_`); `journal` and `collector` handle logging and metrics respectively. + +## Call Site + +`make_NetworkOPs` is called exactly once, inside the `Application` constructor in `Application.cpp`, where it initializes the `m_networkOPs` member alongside every other major subsystem. The result is stored as `std::unique_ptr`, and the raw pointer is subsequently shared throughout the application via `Application::getOPs()`. \ No newline at end of file diff --git a/src/xrpld/app/misc/setup_HashRouter.h.ai.json b/src/xrpld/app/misc/setup_HashRouter.h.ai.json new file mode 100644 index 0000000000..1a0b3dd693 --- /dev/null +++ b/src/xrpld/app/misc/setup_HashRouter.h.ai.json @@ -0,0 +1,27 @@ +{ + "args": [ + { + "lineno": 9, + "name": "config" + } + ], + "classes": [], + "description": "Provides a function to create a HashRouter setup from configuration within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/misc/setup_HashRouter.h", + "functions": [ + { + "args": [ + "config" + ], + "lineno": 9, + "name": "setup_HashRouter" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/misc/setup_HashRouter.h.ai.md b/src/xrpld/app/misc/setup_HashRouter.h.ai.md new file mode 100644 index 0000000000..17110490b3 --- /dev/null +++ b/src/xrpld/app/misc/setup_HashRouter.h.ai.md @@ -0,0 +1,21 @@ +# `setup_HashRouter.h` — Configuration Bridge for `HashRouter` + +This header declares the single factory function `setup_HashRouter()`, which translates XRPL node configuration into a `HashRouter::Setup` value object. It lives at the boundary between the application's configuration layer (`xrpld`) and the lower-level protocol library (`xrpl/core`), following the pattern used throughout the codebase of pairing a `setup_*` function with each configurable component. + +## Role in the System + +`HashRouter` is the peer-to-peer suppression table: it tracks which network peers have already seen a given hash (transaction, validation, etc.) and uses that to avoid redundant relaying. Its `Setup` struct holds two timing parameters — `holdTime` (how long an entry stays in the map, defaulting to 300 seconds) and `relayTime` (the minimum interval before the same item is relayed again, defaulting to 30 seconds). Rather than having `HashRouter` know about the config format directly, `setup_HashRouter()` owns that translation in the `xrpld` layer and returns a fully-constructed `Setup` by value. + +## Implementation Constraints + +The implementation in `detail/setup_HashRouter.cpp` reads the `[hashrouter]` section of the config file and enforces hard lower bounds with documented rationale: + +- `hold_time` must be at least **12 seconds** — described as "the approximate validation time for three ledgers." Shorter hold times would allow duplicate messages to flood the routing table between ledger closes. +- `relay_time` must be at least **8 seconds** — "the approximate validation time for two ledgers." +- `relay_time` must not exceed `hold_time`, ensuring a relayed entry is never evicted before its relay suppression window expires. + +Violations throw `std::runtime_error` at startup, making misconfiguration a hard failure rather than a silent behavioral change. This is intentional: the comment in `HashRouter.h` explicitly warns that these parameters are undocumented and require network-wide coordination to change, so local misconfiguration is treated as a programming error. + +## Design Note + +The forward declaration of `Config` in the header (rather than including `xrpld/core/Config.h`) keeps this header lightweight — callers that only need the declaration don't pull in the full config machinery. The implementation file includes `Config.h` directly where the field lookups actually happen. \ No newline at end of file diff --git a/src/xrpld/app/rdb/PeerFinder.h.ai.json b/src/xrpld/app/rdb/PeerFinder.h.ai.json new file mode 100644 index 0000000000..f9b1a9a3bb --- /dev/null +++ b/src/xrpld/app/rdb/PeerFinder.h.ai.json @@ -0,0 +1,90 @@ +{ + "args": [ + { + "lineno": 15, + "name": "session" + }, + { + "lineno": 15, + "name": "config" + }, + { + "lineno": 15, + "name": "j" + }, + { + "lineno": 25, + "name": "session" + }, + { + "lineno": 25, + "name": "currentSchemaVersion" + }, + { + "lineno": 25, + "name": "j" + }, + { + "lineno": 35, + "name": "session" + }, + { + "lineno": 35, + "name": "func" + }, + { + "lineno": 44, + "name": "session" + }, + { + "lineno": 44, + "name": "v" + } + ], + "classes": [], + "description": "Provides functions to initialize, update, read, and save entries in the peer finder database for the xrpl project.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/rdb/PeerFinder.h", + "functions": [ + { + "args": [ + "session", + "config", + "j" + ], + "lineno": 15, + "name": "initPeerFinderDB" + }, + { + "args": [ + "session", + "currentSchemaVersion", + "j" + ], + "lineno": 25, + "name": "updatePeerFinderDB" + }, + { + "args": [ + "session", + "func" + ], + "lineno": 35, + "name": "readPeerFinderDB" + }, + { + "args": [ + "session", + "v" + ], + "lineno": 44, + "name": "savePeerFinderDB" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/rdb/PeerFinder.h.ai.md b/src/xrpld/app/rdb/PeerFinder.h.ai.md new file mode 100644 index 0000000000..5cc32cb817 --- /dev/null +++ b/src/xrpld/app/rdb/PeerFinder.h.ai.md @@ -0,0 +1,25 @@ +# `src/xrpld/app/rdb/PeerFinder.h` + +## Role and Purpose + +This header is the public SQL boundary for the XRP Ledger's peer-discovery bootstrap cache. It declares four free functions that encapsulate all SQLite access needed to persist, migrate, and retrieve the list of known peers that the `PeerFinder` subsystem uses on startup to locate the network. The design deliberately separates SQL mechanics from the abstract `PeerFinder::Store` interface, keeping persistence logic out of the core networking abstraction. + +## Relationship to Surrounding Architecture + +Three components work together here. `PeerFinder::Store` (in `peerfinder/detail/Store.h`) is a pure abstract interface defining `load()` and `save()` — it knows nothing about SQL. `StoreSqdb` (in `peerfinder/detail/StoreSqdb.h`) is the concrete SQLite implementation of that interface; it owns a `soci::session` and delegates all SQL work to the four functions declared in this header. The `detail/PeerFinder.cpp` translation unit provides the bodies. This layering means the networking subsystem can be tested against mock `Store` implementations without any database involvement, while the SQL layer can be audited and changed independently. + +## The Four Functions + +`initPeerFinderDB` opens a SOCI database session using connection parameters drawn from the node's `BasicConfig` under the `"peerfinder"` section, then creates the `SchemaVersion` tracking table and the `PeerFinder_BootstrapCache` table (address TEXT UNIQUE, valence INTEGER) if they do not already exist. The entire setup is wrapped in a `soci::transaction` so schema creation is atomic — a half-initialized database cannot be left behind by a crash during first startup. + +`updatePeerFinderDB` implements schema migration against the `currentSchemaVersion` constant (currently `4`, defined as an enum in `StoreSqdb`). It reads the stored version from `SchemaVersion`, then applies version-gated migration blocks in descending order. The most significant migration, triggered when the on-disk version is below 4, removes the historical `uptime` column from the bootstrap cache. Because older SQLite does not support `DROP COLUMN`, the function follows the standard SQLite workaround: create a replacement table without the unwanted column, copy all valid rows, drop the original, rename the replacement, and recreate the index. Bad address strings encountered during the copy are logged and skipped rather than aborting the migration. Versions below 3 trigger removal of several legacy endpoint tables that have no modern counterpart. After all migrations the function writes the current schema version back to `SchemaVersion` using `INSERT OR REPLACE`. A `THROW` is used if the on-disk version exceeds `currentSchemaVersion`, which would imply a downgrade scenario the code does not attempt to handle. + +A notable SOCI quirk is documented in the implementation: querying an optional integer requires `boost::optional` rather than `std::optional` because the SOCI binding layer predates C++17 and lacks specializations for the standard type. This is a narrow dependency worth knowing when maintaining migration code. + +`readPeerFinderDB` fetches every row from `PeerFinder_BootstrapCache` and fires a caller-provided `std::function` for each one. Returning a callback rather than a container keeps the raw string-to-`beast::IP::Endpoint` conversion — and the associated validity filtering — in `StoreSqdb::load()` where it belongs, and avoids allocating an intermediate vector of strings. + +`savePeerFinderDB` persists the current in-memory bootstrap cache by executing a full `DELETE FROM PeerFinder_BootstrapCache` followed by a bulk `INSERT` of the supplied `PeerFinder::Store::Entry` vector, all inside a single transaction. The replacement-rather-than-merge approach is intentional: the bootstrap cache is treated as an opaque snapshot, not a partially-updated set. Using SOCI's vector binding (`soci::use(s)`, `soci::use(valence)`) issues the inserts as a single prepared statement over parallel address and valence arrays, which is meaningfully faster than row-by-row iteration for a cache that could contain hundreds of peers. + +## Design Observations + +The header's choice of free functions over a class is consistent with the `rdb/` module convention: these functions are stateless SQL helpers, and all state (the `soci::session`, the journal, the schema version constant) lives in `StoreSqdb`. The separation makes the SQL logic easy to unit-test in isolation by constructing a throw-away in-memory session. The `initPeerFinderDB` / `updatePeerFinderDB` split, rather than a single `openAndMigrate` function, also allows `StoreSqdb::open()` to call them in sequence while keeping each step's logic self-contained. \ No newline at end of file diff --git a/src/xrpld/app/rdb/backend/SQLiteDatabase.h.ai.json b/src/xrpld/app/rdb/backend/SQLiteDatabase.h.ai.json new file mode 100644 index 0000000000..24ebaf8498 --- /dev/null +++ b/src/xrpld/app/rdb/backend/SQLiteDatabase.h.ai.json @@ -0,0 +1,347 @@ +{ + "args": [ + { + "lineno": 292, + "name": "registry" + }, + { + "lineno": 292, + "name": "config" + }, + { + "lineno": 292, + "name": "jobQueue" + }, + { + "lineno": 295, + "name": "SQLiteDatabase&& rhs" + } + ], + "classes": [ + { + "args": [ + "ServiceRegistry& registry, Config const& config, JobQueue& jobQueue", + "SQLiteDatabase(SQLiteDatabase const&) = delete", + "SQLiteDatabase(SQLiteDatabase&& rhs) noexcept" + ], + "lineno": 10, + "name": "SQLiteDatabase" + } + ], + "description": "Defines the SQLiteDatabase class, a final implementation of RelationalDatabase for managing ledger and transaction data in SQLite databases within the xrpl namespace. Provides methods for querying, saving, and deleting ledger and transaction records, as well as utility functions for database management.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/rdb/backend/SQLiteDatabase.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "getMinLedgerSeq" + }, + { + "args": [], + "lineno": 25, + "name": "getMaxLedgerSeq" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 32, + "name": "getLedgerInfoByIndex" + }, + { + "args": [], + "lineno": 39, + "name": "getNewestLedgerInfo" + }, + { + "args": [ + "ledgerHash" + ], + "lineno": 46, + "name": "getLedgerInfoByHash" + }, + { + "args": [ + "ledgerIndex" + ], + "lineno": 53, + "name": "getHashByIndex" + }, + { + "args": [ + "ledgerIndex" + ], + "lineno": 60, + "name": "getHashesByIndex" + }, + { + "args": [ + "minSeq", + "maxSeq" + ], + "lineno": 68, + "name": "getHashesByIndex" + }, + { + "args": [ + "startIndex" + ], + "lineno": 77, + "name": "getTxHistory" + }, + { + "args": [], + "lineno": 85, + "name": "getTransactionsMinLedgerSeq" + }, + { + "args": [], + "lineno": 92, + "name": "getAccountTransactionsMinLedgerSeq" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 99, + "name": "deleteTransactionByLedgerSeq" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 106, + "name": "deleteBeforeLedgerSeq" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 113, + "name": "deleteTransactionsBeforeLedgerSeq" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 120, + "name": "deleteAccountTransactionsBeforeLedgerSeq" + }, + { + "args": [], + "lineno": 127, + "name": "getTransactionCount" + }, + { + "args": [], + "lineno": 134, + "name": "getAccountTransactionCount" + }, + { + "args": [], + "lineno": 141, + "name": "getLedgerCountMinMax" + }, + { + "args": [ + "ledger", + "current" + ], + "lineno": 149, + "name": "saveValidatedLedger" + }, + { + "args": [ + "ledgerFirstIndex" + ], + "lineno": 157, + "name": "getLimitedOldestLedgerInfo" + }, + { + "args": [ + "ledgerFirstIndex" + ], + "lineno": 165, + "name": "getLimitedNewestLedgerInfo" + }, + { + "args": [ + "options" + ], + "lineno": 173, + "name": "getOldestAccountTxs" + }, + { + "args": [ + "options" + ], + "lineno": 182, + "name": "getNewestAccountTxs" + }, + { + "args": [ + "options" + ], + "lineno": 191, + "name": "getOldestAccountTxsB" + }, + { + "args": [ + "options" + ], + "lineno": 200, + "name": "getNewestAccountTxsB" + }, + { + "args": [ + "options" + ], + "lineno": 209, + "name": "oldestAccountTxPage" + }, + { + "args": [ + "options" + ], + "lineno": 219, + "name": "newestAccountTxPage" + }, + { + "args": [ + "options" + ], + "lineno": 229, + "name": "oldestAccountTxPageB" + }, + { + "args": [ + "options" + ], + "lineno": 239, + "name": "newestAccountTxPageB" + }, + { + "args": [ + "id", + "range", + "ec" + ], + "lineno": 249, + "name": "getTransaction" + }, + { + "args": [], + "lineno": 263, + "name": "getKBUsedAll" + }, + { + "args": [], + "lineno": 270, + "name": "getKBUsedLedger" + }, + { + "args": [], + "lineno": 277, + "name": "getKBUsedTransaction" + }, + { + "args": [], + "lineno": 284, + "name": "closeLedgerDB" + }, + { + "args": [], + "lineno": 290, + "name": "closeTransactionDB" + }, + { + "args": [ + "registry", + "config", + "jobQueue" + ], + "lineno": 292, + "name": "SQLiteDatabase" + }, + { + "args": [ + "SQLiteDatabase&& rhs" + ], + "lineno": 295, + "name": "SQLiteDatabase" + }, + { + "args": [ + "SQLiteDatabase const&" + ], + "lineno": 298, + "name": "operator=" + }, + { + "args": [ + "SQLiteDatabase&&" + ], + "lineno": 300, + "name": "operator=" + }, + { + "args": [ + "config" + ], + "lineno": 307, + "name": "ledgerDbHasSpace" + }, + { + "args": [ + "config" + ], + "lineno": 314, + "name": "transactionDbHasSpace" + }, + { + "args": [ + "config", + "setup", + "checkpointerSetup" + ], + "lineno": 323, + "name": "makeLedgerDBs" + }, + { + "args": [], + "lineno": 332, + "name": "existsLedger" + }, + { + "args": [], + "lineno": 340, + "name": "existsTransaction" + }, + { + "args": [], + "lineno": 348, + "name": "checkoutLedger" + }, + { + "args": [], + "lineno": 356, + "name": "checkoutTransaction" + }, + { + "args": [ + "registry", + "config", + "jobQueue" + ], + "lineno": 366, + "name": "setup_RelationalDatabase" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/rdb/backend/SQLiteDatabase.h.ai.md b/src/xrpld/app/rdb/backend/SQLiteDatabase.h.ai.md new file mode 100644 index 0000000000..7d36aca162 --- /dev/null +++ b/src/xrpld/app/rdb/backend/SQLiteDatabase.h.ai.md @@ -0,0 +1,50 @@ +# `SQLiteDatabase.h` — SQLite Backend for the XRPL Relational Database Layer + +## Role in the System + +`SQLiteDatabase` is the concrete, `final` implementation of the abstract `RelationalDatabase` interface for nodes that persist ledger and transaction history in local SQLite files. It sits in `src/xrpld/app/rdb/backend/` and forms the production storage layer that the rest of `rippled` uses when querying ledger headers, transaction records, and account transaction history via the `account_tx`, `tx`, and `ledger` RPCs. + +The design separates the SQLite-specific backend from the rest of the codebase through two boundaries: the abstract `RelationalDatabase` interface (defined in `include/xrpl/rdb/RelationalDatabase.h`), and an internal `detail` namespace (declared in `detail/Node.h`) that encapsulates all raw SQL execution behind typed free functions. `SQLiteDatabase` acts as a coordinator: it checks preconditions, checks out a database session, and delegates to the appropriate `detail::` function. + +## Dual-Database Architecture + +The class manages two distinct SQLite connections held as `std::unique_ptr` members: + +- `ledgerDb_` — stores the `Ledgers` table containing ledger headers and hash chains. +- `txdb_` — stores the `Transactions` and `AccountTransactions` tables. + +Splitting ledger metadata from transaction data is deliberate. Validators and history-less nodes often operate without transaction tables at all. The `useTxTables_` boolean guards every transaction-related method: when false, those methods return empty or zero immediately without touching the database. This means a node can be configured to suppress all transaction storage with no code-path overhead beyond a single branch check. + +The private `existsLedger()` and `existsTransaction()` helpers check whether their respective `unique_ptr` is non-null before any operation. `makeLedgerDBs()` can partially succeed — opening the ledger DB while failing the transaction DB — and the existence guards ensure that no method ever dereferences a null pointer. This is a defensive pattern that makes partial initialization safe rather than requiring a two-phase "open or throw" model. + +## Session Checkout Pattern + +`checkoutLedger()` and `checkoutTransaction()` both call `checkoutDb()` on the underlying `DatabaseCon`, returning an RAII session handle scoped to the calling operation. This is a connection-pool pattern adapted for SQLite's single-writer model. By holding the session only for the duration of a single query or write, concurrent callers serialize their access at the `DatabaseCon` level rather than at the `SQLiteDatabase` level, keeping the public API free of explicit locking. The `DatabaseCon` abstraction (via SOCI) is responsible for the pool management and WAL-mode checkpointing setup. + +## Account Transaction Query Surface + +Eight closely related methods form the account transaction query API, mapping to the `account_tx` RPC: + +- **Offset-based variants** (`getOldestAccountTxs`, `getNewestAccountTxs`, `getOldestAccountTxsB`, `getNewestAccountTxsB`) accept `AccountTxOptions` containing an account, ledger range, numeric offset, and limit. They return full result sets in either deserialized form (`AccountTxs` — vector of `Transaction`/`TxMeta` pairs) or binary form (`MetaTxsList` — vector of raw blob tuples). + +- **Marker-based paging variants** (`oldestAccountTxPage`, `newestAccountTxPage`, `oldestAccountTxPageB`, `newestAccountTxPageB`) accept `AccountTxPageOptions` with an `optional` cursor and return both a result set and a new marker for the next page. The page length is hardcoded as a `static const` inside each method: 200 entries for deserialized variants, 500 for binary. The higher binary limit reflects that binary results skip deserialization overhead and can be streamed more cheaply to the caller. + +The paging implementations pass two callbacks into the `detail::` layer: `onUnsavedLedger` (which triggers async ledger saves for ledgers found in the DB but not yet applied to the in-memory ledger store) and `onTransaction` (which accumulates results). This inversion of control keeps the SQL cursor logic entirely within `detail::` while letting `SQLiteDatabase` determine how to convert raw data into the appropriate return type. + +## `getTransaction` and the `TxSearched` Sentinel + +`getTransaction()` returns `std::variant` rather than `optional`. When a transaction is not found by hash, the absence could be definitive (the ledger that would contain this transaction is present in the DB and the transaction isn't there) or ambiguous (the DB has gaps). The `TxSearched` enum encodes this: `TxSearched::All` means the caller-supplied ledger range is fully covered and the transaction definitively does not exist; `TxSearched::Some` means coverage is partial; `TxSearched::Unknown` means no range was provided, or the transaction DB is disabled. This three-state result allows the RPC handler to give callers a precise answer about search coverage rather than a generic "not found." + +## `saveValidatedLedger` — The Write Path + +`saveValidatedLedger()` is the single write entry point, called as the node accepts each new validated ledger. It passes both database handles directly to `detail::saveValidatedLedger`, which writes the ledger header to `ledgerDb_` and any associated transactions to `txdb_` within a single coordinated write. The `current` parameter distinguishes real-time validated ledgers from historical ledgers being replayed during catchup, allowing the detail layer to skip certain async-save side effects during history backfill. + +## Ownership and Lifecycle + +Copy construction and copy assignment are deleted; move construction is permitted but move assignment is deleted. The move constructor uses `std::exchange` to transfer ownership of the two `unique_ptr` database connections. Deleting move assignment avoids a partially-moved-from state after the registry reference wrapper (which is non-owning) and the moved database handles would be inconsistent. + +`closeLedgerDB()` and `closeTransactionDB()` allow the application to explicitly release the file handles, used during graceful shutdown to ensure SQLite flushes WAL frames before process exit. + +## `setup_RelationalDatabase` Factory Function + +The free function `setup_RelationalDatabase()` constructs a `SQLiteDatabase` by value, reading the database path and checkpointer configuration from the node's `Config`. Its docstring notes it is "recommended to use as a singleton, but not enforced," which allows test harnesses to instantiate multiple independent databases without a global state requirement. \ No newline at end of file diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp.ai.json b/src/xrpld/app/rdb/backend/detail/Node.cpp.ai.json new file mode 100644 index 0000000000..e36ab9cc50 --- /dev/null +++ b/src/xrpld/app/rdb/backend/detail/Node.cpp.ai.json @@ -0,0 +1,685 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "makeLedgerDBs", + "kilobytes(config.getValueFor(SizedItem::lgrDBCache))", + "kilobytes(config.getValueFor(SizedItem::txnDBCache))", + "PRAGMA table_info(AccountTransactions)" + ], + "entry_point": "makeLedgerDBs", + "purpose": "Initializes ledger and transaction databases, sets cache sizes, and validates AccountTransactions table schema.", + "validation_points": [ + "kilobytes(config.getValueFor(SizedItem::lgrDBCache))", + "kilobytes(config.getValueFor(SizedItem::txnDBCache))", + "setup.startUp == StartUpType::Load/LoadFile/Replay", + "PRAGMA table_info(AccountTransactions) (checks for primary key)" + ] + }, + { + "call_chain": [ + "to_string" + ], + "entry_point": "to_string", + "purpose": "Converts TableType enum to string, validates enum completeness.", + "validation_points": [ + "static_assert(TableTypeCount == 3)", + "switch default: UNREACHABLE" + ] + } + ], + "data_flows": [ + { + "field": "config.getValueFor(SizedItem::lgrDBCache)", + "flow": [ + "Config::getValueFor", + "makeLedgerDBs", + "kilobytes()", + "PRAGMA cache_size" + ], + "origin": "Config object (external configuration)", + "transformations": [ + "Value fetched from config", + "Converted to kilobytes (type/format check)", + "Used in SQL PRAGMA statement" + ], + "validated_at": "kilobytes()" + }, + { + "field": "config.getValueFor(SizedItem::txnDBCache)", + "flow": [ + "Config::getValueFor", + "makeLedgerDBs", + "kilobytes()", + "PRAGMA cache_size" + ], + "origin": "Config object (external configuration)", + "transformations": [ + "Value fetched from config", + "Converted to kilobytes (type/format check)", + "Used in SQL PRAGMA statement" + ], + "validated_at": "kilobytes()" + }, + { + "field": "setup.startUp", + "flow": [ + "DatabaseCon::Setup", + "makeLedgerDBs", + "if (!setup.standAlone || setup.startUp == ...)" + ], + "origin": "DatabaseCon::Setup struct (likely from config or startup code)", + "transformations": [ + "Enum value checked for specific startup types" + ], + "validated_at": "Explicit enum comparison in makeLedgerDBs" + }, + { + "field": "TableType", + "flow": [ + "to_string(TableType)", + "switch(TableType)", + "return string" + ], + "origin": "Function argument (enum)", + "transformations": [ + "Enum checked for completeness (static_assert)", + "Switch statement ensures only valid values" + ], + "validated_at": "static_assert and switch default (UNREACHABLE)" + }, + { + "field": "AccountTransactions table primary key", + "flow": [ + "PRAGMA table_info(AccountTransactions)", + "soci::statement fetch loop", + "if (pk == 1)" + ], + "origin": "Database schema (SQLite table)", + "transformations": [ + "Schema introspection via PRAGMA", + "Checks if any column is primary key" + ], + "validated_at": "PRAGMA table_info(AccountTransactions) in makeLedgerDBs" + } + ], + "description": "This file provides various utility functions for interacting with and managing the XRPL ledger and transaction relational databases, including SQL query generation, ledger and transaction retrieval, insertion, deletion, and pagination, as well as disk space checks. It is part of the XRPL server backend.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "TableType (enum)", + "empty", + "string", + "validation" + ], + "evidence": "static_assert and switch default (UNREACHABLE) at to_string(TableType type)", + "issue_pattern": "Missing empty string validation for TableType (enum)", + "why_false_positive": "static_assert and switch default (UNREACHABLE) validates TableType (enum) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "config.getValueFor(SizedItem::lgrDBCache)", + "empty", + "string", + "validation" + ], + "evidence": "kilobytes() (likely type/format check) at makeLedgerDBs", + "issue_pattern": "Missing empty string validation for config.getValueFor(SizedItem::lgrDBCache)", + "why_false_positive": "kilobytes() (likely type/format check) validates config.getValueFor(SizedItem::lgrDBCache) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "config.getValueFor(SizedItem::txnDBCache)", + "empty", + "string", + "validation" + ], + "evidence": "kilobytes() (likely type/format check) at makeLedgerDBs", + "issue_pattern": "Missing empty string validation for config.getValueFor(SizedItem::txnDBCache)", + "why_false_positive": "kilobytes() (likely type/format check) validates config.getValueFor(SizedItem::txnDBCache) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "setup.startUp", + "empty", + "string", + "validation" + ], + "evidence": "explicit enum comparison at makeLedgerDBs", + "issue_pattern": "Missing empty string validation for setup.startUp", + "why_false_positive": "explicit enum comparison validates setup.startUp for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "AccountTransactions table primary key", + "empty", + "string", + "validation" + ], + "evidence": "PRAGMA table_info(AccountTransactions); at makeLedgerDBs", + "issue_pattern": "Missing empty string validation for AccountTransactions table primary key", + "why_false_positive": "PRAGMA table_info(AccountTransactions); validates AccountTransactions table primary key for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/rdb/backend/detail/Node.cpp", + "functions": [ + { + "args": [ + "TableType type" + ], + "lineno": 15, + "name": "to_string" + }, + { + "args": [ + "Config const& config", + "DatabaseCon::Setup const& setup", + "DatabaseCon::CheckpointerSetup const& checkpointerSetup", + "beast::Journal j" + ], + "lineno": 32, + "name": "makeLedgerDBs" + }, + { + "args": [ + "soci::session& session", + "TableType type" + ], + "lineno": 70, + "name": "getMinLedgerSeq" + }, + { + "args": [ + "soci::session& session", + "TableType type" + ], + "lineno": 80, + "name": "getMaxLedgerSeq" + }, + { + "args": [ + "soci::session& session", + "TableType type", + "LedgerIndex ledgerSeq" + ], + "lineno": 90, + "name": "deleteByLedgerSeq" + }, + { + "args": [ + "soci::session& session", + "TableType type", + "LedgerIndex ledgerSeq" + ], + "lineno": 95, + "name": "deleteBeforeLedgerSeq" + }, + { + "args": [ + "soci::session& session", + "TableType type" + ], + "lineno": 100, + "name": "getRows" + }, + { + "args": [ + "soci::session& session", + "TableType type" + ], + "lineno": 109, + "name": "getRowsMinMax" + }, + { + "args": [ + "DatabaseCon& ldgDB", + "std::unique_ptr const& txnDB", + "Application& app", + "std::shared_ptr const& ledger", + "bool current" + ], + "lineno": 120, + "name": "saveValidatedLedger" + }, + { + "args": [ + "soci::session& session", + "std::string const& sqlSuffix", + "beast::Journal j" + ], + "lineno": 232, + "name": "getLedgerInfo" + }, + { + "args": [ + "soci::session& session", + "LedgerIndex ledgerSeq", + "beast::Journal j" + ], + "lineno": 273, + "name": "getLedgerInfoByIndex" + }, + { + "args": [ + "soci::session& session", + "beast::Journal j" + ], + "lineno": 281, + "name": "getNewestLedgerInfo" + }, + { + "args": [ + "soci::session& session", + "LedgerIndex ledgerFirstIndex", + "beast::Journal j" + ], + "lineno": 289, + "name": "getLimitedOldestLedgerInfo" + }, + { + "args": [ + "soci::session& session", + "LedgerIndex ledgerFirstIndex", + "beast::Journal j" + ], + "lineno": 297, + "name": "getLimitedNewestLedgerInfo" + }, + { + "args": [ + "soci::session& session", + "uint256 const& ledgerHash", + "beast::Journal j" + ], + "lineno": 305, + "name": "getLedgerInfoByHash" + }, + { + "args": [ + "soci::session& session", + "LedgerIndex ledgerIndex" + ], + "lineno": 313, + "name": "getHashByIndex" + }, + { + "args": [ + "soci::session& session", + "LedgerIndex ledgerIndex", + "beast::Journal j" + ], + "lineno": 334, + "name": "getHashesByIndex" + }, + { + "args": [ + "soci::session& session", + "LedgerIndex minSeq", + "LedgerIndex maxSeq", + "beast::Journal j" + ], + "lineno": 355, + "name": "getHashesByIndex" + }, + { + "args": [ + "soci::session& session", + "Application& app", + "LedgerIndex startIndex", + "int quantity" + ], + "lineno": 386, + "name": "getTxHistory" + }, + { + "args": [ + "Application& app", + "std::string selection", + "RelationalDatabase::AccountTxOptions const& options", + "bool descending", + "bool binary", + "bool count", + "beast::Journal j" + ], + "lineno": 426, + "name": "transactionsSQL" + }, + { + "args": [ + "soci::session& session", + "Application& app", + "LedgerMaster& ledgerMaster", + "RelationalDatabase::AccountTxOptions const& options", + "bool descending", + "beast::Journal j" + ], + "lineno": 474, + "name": "getAccountTxs" + }, + { + "args": [ + "soci::session& session", + "Application& app", + "LedgerMaster& ledgerMaster", + "RelationalDatabase::AccountTxOptions const& options", + "beast::Journal j" + ], + "lineno": 527, + "name": "getOldestAccountTxs" + }, + { + "args": [ + "soci::session& session", + "Application& app", + "LedgerMaster& ledgerMaster", + "RelationalDatabase::AccountTxOptions const& options", + "beast::Journal j" + ], + "lineno": 536, + "name": "getNewestAccountTxs" + }, + { + "args": [ + "soci::session& session", + "Application& app", + "RelationalDatabase::AccountTxOptions const& options", + "bool descending", + "beast::Journal j" + ], + "lineno": 545, + "name": "getAccountTxsB" + }, + { + "args": [ + "soci::session& session", + "Application& app", + "RelationalDatabase::AccountTxOptions const& options", + "beast::Journal j" + ], + "lineno": 591, + "name": "getOldestAccountTxsB" + }, + { + "args": [ + "soci::session& session", + "Application& app", + "RelationalDatabase::AccountTxOptions const& options", + "beast::Journal j" + ], + "lineno": 599, + "name": "getNewestAccountTxsB" + }, + { + "args": [ + "soci::session& session", + "std::function const& onUnsavedLedger", + "std::function const& onTransaction", + "RelationalDatabase::AccountTxPageOptions const& options", + "std::uint32_t page_length", + "bool forward" + ], + "lineno": 608, + "name": "accountTxPage" + }, + { + "args": [ + "soci::session& session", + "std::function const& onUnsavedLedger", + "std::function const& onTransaction", + "RelationalDatabase::AccountTxPageOptions const& options", + "std::uint32_t page_length" + ], + "lineno": 701, + "name": "oldestAccountTxPage" + }, + { + "args": [ + "soci::session& session", + "std::function const& onUnsavedLedger", + "std::function const& onTransaction", + "RelationalDatabase::AccountTxPageOptions const& options", + "std::uint32_t page_length" + ], + "lineno": 710, + "name": "newestAccountTxPage" + }, + { + "args": [ + "soci::session& session", + "Application& app", + "uint256 const& id", + "std::optional> const& range", + "error_code_i& ec" + ], + "lineno": 719, + "name": "getTransaction" + }, + { + "args": [ + "soci::session& session", + "Config const& config", + "beast::Journal j" + ], + "lineno": 771, + "name": "dbHasSpace" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + }, + { + "lineno": 13, + "name": "detail" + } + ], + "test_coverage_notes": "The code is low-level and likely tested indirectly via integration or system tests that exercise database initialization and configuration. Direct unit tests for validation logic (e.g., invalid TableType, malformed config values, missing primary key in AccountTransactions) are not evident in this file. Test coverage for error paths (e.g., UNREACHABLE, invalid config) may be limited unless explicitly tested in higher-level test suites. Look for tests in files like 'test/rdb/DatabaseCon_test.cpp', 'test/app/ledger/LedgerDB_test.cpp', or integration tests that start up the server with various configs. Schema validation (PRAGMA table_info) is rarely unit tested unless schema migration or corruption is a concern.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation, SQLite PRAGMA, static_assert, enum checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE macro (likely throws or aborts)", + "field": "TableType (enum)", + "location": "to_string(TableType type)", + "validated_by": "static_assert and switch default (UNREACHABLE)", + "validates": [ + "Ensures TableType enum is within expected range (Ledgers, Transactions, AccountTransactions)", + "Compile-time check that TableTypeCount == 3" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "If kilobytes() or config.getValueFor throws on invalid input", + "field": "config.getValueFor(SizedItem::lgrDBCache)", + "location": "makeLedgerDBs", + "validated_by": "kilobytes() (likely type/format check)", + "validates": [ + "Ensures lgrDBCache value is convertible to kilobytes (integer, positive)" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.7, + "error_thrown": "If kilobytes() or config.getValueFor throws on invalid input", + "field": "config.getValueFor(SizedItem::txnDBCache)", + "location": "makeLedgerDBs", + "validated_by": "kilobytes() (likely type/format check)", + "validates": [ + "Ensures txnDBCache value is convertible to kilobytes (integer, positive)" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "N/A (controls logic flow, not error)", + "field": "setup.startUp", + "location": "makeLedgerDBs", + "validated_by": "explicit enum comparison", + "validates": [ + "Checks if startUp is one of Load, LoadFile, Replay" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "N/A in shown code (but likely throws or logs error if PK missing in full code)", + "field": "AccountTransactions table primary key", + "location": "makeLedgerDBs", + "validated_by": "PRAGMA table_info(AccountTransactions);", + "validates": [ + "Checks if AccountTransactions table has a primary key" + ], + "validation_type": "business_logic|schema" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/rdb/backend/detail/Node.cpp.ai.md b/src/xrpld/app/rdb/backend/detail/Node.cpp.ai.md new file mode 100644 index 0000000000..9a211bdd43 --- /dev/null +++ b/src/xrpld/app/rdb/backend/detail/Node.cpp.ai.md @@ -0,0 +1,57 @@ +# `src/xrpld/app/rdb/backend/detail/Node.cpp` + +## Role in the System + +`Node.cpp` is the implementation layer for the SQLite-backed *node-mode* relational database in the XRPL server. It lives in the `xrpl::detail` namespace and is consumed by the higher-level `NodeStore`-adjacent relational DB classes. The file is the direct bridge between ledger/transaction domain objects and the underlying SQLite tables (`Ledgers`, `Transactions`, `AccountTransactions`) via the SOCI database abstraction library. + +The split between this file and its header (`Node.h`) is clean: the header defines the `TableType` enum, the `DatabasePairValid` return struct, and the public function signatures, while the `.cpp` contains all query construction, result deserialization, and error-handling logic. This keeps the SQLite-specific details out of consumers' compilation units. + +## Database Initialization: `makeLedgerDBs` + +`makeLedgerDBs` is the startup entry point. It opens two SQLite databases — the ledger DB (`Ledgers` table) and, when `config.useTxTables()` is true, the transaction DB (`Transactions` and `AccountTransactions` tables) — wrapping each in a `DatabaseCon` and returning them as `std::unique_ptr`s inside the `DatabasePairValid` struct. After opening each database it immediately issues a `PRAGMA cache_size` command, converting the config-specified value through `kilobytes()` to set SQLite's page cache in KB (the negative sign tells SQLite to interpret the argument as kilobytes rather than page count). + +The most architecturally interesting part of `makeLedgerDBs` is its schema probe. When the node is not in stand-alone mode, or when starting from a snapshot (`Load`, `LoadFile`, `Replay`), it runs `PRAGMA table_info(AccountTransactions)` and inspects each column's `pk` field. If any column is marked as a primary key, it returns `valid = false` in the result struct. This is a backwards-compatibility check: old schema versions of `AccountTransactions` had a primary key that was later removed for write-performance reasons. The caller uses the `valid` flag to signal that a schema migration is required before this database can be used safely. + +## `TableType` Enum and `to_string` + +The file uses a small `TableType` enum (`Ledgers`, `Transactions`, `AccountTransactions`) to parameterize the generic table-management utilities. A `static_assert(TableTypeCount == 3)` inside the file-private `to_string` ensures at compile time that the switch statement covers every enum value. The `default` branch calls `UNREACHABLE`, making it impossible to silently query the wrong table name at runtime. This is the standard pattern in the codebase for guarding enums that must stay in sync with their switch tables. + +## Generic Table Utilities + +`getMinLedgerSeq`, `getMaxLedgerSeq`, `getRows`, and `getRowsMinMax` are stateless helpers that all accept a `TableType` and delegate table-name resolution to `to_string`. A recurring pattern is the use of `boost::optional` rather than `std::optional` as the SOCI bind target: SOCI's type system predates standard optional support, so every result that can be SQL `NULL` must go through `boost::optional`, then be converted on the way out. + +`deleteByLedgerSeq` and `deleteBeforeLedgerSeq` provide point and range deletions respectively (`WHERE LedgerSeq == N` vs `WHERE LedgerSeq < N`). These are used by the online-delete subsystem to prune old ledgers. + +## Ledger Persistence: `saveValidatedLedger` + +This is the most complex and critical function. When a ledger is validated, it must be written atomically across two databases and also stored in the node store (the key-value store for raw ledger data). The function: + +1. Validates ledger integrity up-front — checks that `accountHash` is non-zero and matches the state map hash, asserts that `txHash` matches the transaction map hash. These failures call `UNREACHABLE` because they signal ledger corruption rather than a recoverable error. +2. Serializes the ledger header with `HashPrefix::ledgerMaster` and stores it in the node store via `app.getNodeStore().store(hotLEDGER, ...)`. +3. Fetches or constructs an `AcceptedLedger` from the cache. A cache miss triggers full traversal of the ledger's transaction tree. If any nodes are missing (exception), it calls `ledgerMaster.failedSave` and signals `PendingSaves` before returning `false` — a clean failure that lets upstream retry logic operate. +4. Within a single SOCI transaction on the transaction DB, it deletes any previously saved rows for the same sequence (idempotent re-save), then inserts one row in `AccountTransactions` per affected account per transaction, batching all affected-account rows into a single bulk `INSERT INTO AccountTransactions ... VALUES (...)` string. The 128-byte-per-row size estimate in `sql.reserve` is a micro-optimization to avoid repeated heap reallocations. +5. In a separate transaction on the ledger DB, writes the `Ledgers` row via parameterized SOCI `use()` bindings (safe from SQL injection). + +The two-database design means there is no single cross-database SOCI transaction. The function handles this by writing to the transaction DB first; if the ledger DB write fails after that, the transaction rows are orphaned but the downstream startup check (`PRAGMA table_info`) and the delete-before-insert pattern ensure correctness on restart. + +## Ledger Lookup Functions + +All ledger-by-various-criteria queries share the file-private `getLedgerInfo` function, which accepts a raw SQL suffix string. This is a deliberate internal factoring: `getLedgerInfoByIndex`, `getNewestLedgerInfo`, `getLimitedOldestLedgerInfo`, `getLimitedNewestLedgerInfo`, and `getLedgerInfoByHash` each build a WHERE/ORDER BY suffix and delegate to the common implementation. Each hash field returned from the DB is parsed via `uint256::parseHex`, and any parse failure returns an empty optional with a debug-level journal entry rather than crashing or returning a corrupted header. + +`getHashByIndex` and the two `getHashesByIndex` overloads use the `SeqLedger` index hint explicitly (`INDEXED BY SeqLedger`) to ensure SQLite uses the index on `LedgerSeq` rather than a full table scan — an important performance consideration for databases that may hold millions of ledger rows. + +## Account Transaction Queries + +`transactionsSQL` is a private SQL builder that constructs the `INNER JOIN AccountTransactions ... Transactions` query for account-level lookups. It enforces two hard page-size caps: 200 rows for decoded (JSON-deserialized) results and 500 for binary results. These caps exist because decoded transactions are significantly more expensive to deserialize and because RPC clients cannot be trusted to supply reasonable `limit` values. The `bUnlimited` flag (admin-only) bypasses the per-query cap. + +`getAccountTxs` (accessed via the public `getOldestAccountTxs`/`getNewestAccountTxs` wrappers) deserializes each row into a `Transaction` + `TxMeta` pair. If `txnMeta` is empty it triggers `pendSaveValidated` on the ledger — a defensive workaround for a historic database bug where metadata could be written as NULL. The binary variant `getAccountTxsB` skips deserialization and returns raw blobs. + +## Cursor-Based Pagination: `accountTxPage` + +The `accountTxPage` function implements stateless marker-based pagination, which is essential for streaming large account transaction histories across multiple RPC calls. The marker encodes `(ledgerSeq, txnSeq)`. When a marker is present, the SQL query becomes a `UNION` of two subqueries: one covering the range *beyond* the marker ledger and one covering the *same* marker ledger but with a `TxnSeq` comparison (`>=` or `<=` depending on direction). This avoids the O(offset) cost of a SQL `OFFSET` clause. + +The function always queries for `numberOfResults + 1` rows; if the extra row is found, it becomes the new marker and is not returned to the caller. The result blobs are explicitly cleared after each `onTransaction` callback invocation — the comment explains why: some callbacks move the blob data out, some copy it, and clearing ensures the next `convert()` call can reuse the allocation without depending on move semantics. + +## Disk Space Check: `dbHasSpace` + +`dbHasSpace` guards against two distinct failure modes. First, it checks OS-level free disk space via `boost::filesystem::space` against a 512 MB threshold. Second, if transaction tables are in use, it queries SQLite's internal free-page counter (`PRAGMA page_count` vs `PRAGMA max_page_count`) to detect SQLite's own file-size ceiling. The page size and max-page-count values are cached in `static` lambdas because they do not change between calls, while the current page count is queried fresh each time. If the SQLite free space falls below 512 MB, the log message explicitly tells operators to run `xrpld` with the `vacuum` parameter, because SQLite cannot reclaim space without a full `VACUUM` rewrite. \ No newline at end of file diff --git a/src/xrpld/app/rdb/backend/detail/Node.h.ai.json b/src/xrpld/app/rdb/backend/detail/Node.h.ai.json new file mode 100644 index 0000000000..eb7c19cd88 --- /dev/null +++ b/src/xrpld/app/rdb/backend/detail/Node.h.ai.json @@ -0,0 +1,261 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 14, + "name": "DatabasePairValid" + } + ], + "description": "This file provides utility functions and structures for interacting with XRPL ledger and transaction relational databases, including opening databases, querying ledger and transaction information, deleting entries, and paginating account transactions.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/rdb/backend/detail/Node.h", + "functions": [ + { + "args": [ + "config", + "setup", + "checkpointerSetup", + "j" + ], + "lineno": 23, + "name": "makeLedgerDBs" + }, + { + "args": [ + "session", + "type" + ], + "lineno": 36, + "name": "getMinLedgerSeq" + }, + { + "args": [ + "session", + "type" + ], + "lineno": 45, + "name": "getMaxLedgerSeq" + }, + { + "args": [ + "session", + "type", + "ledgerSeq" + ], + "lineno": 54, + "name": "deleteByLedgerSeq" + }, + { + "args": [ + "session", + "type", + "ledgerSeq" + ], + "lineno": 65, + "name": "deleteBeforeLedgerSeq" + }, + { + "args": [ + "session", + "type" + ], + "lineno": 76, + "name": "getRows" + }, + { + "args": [ + "session", + "type" + ], + "lineno": 87, + "name": "getRowsMinMax" + }, + { + "args": [ + "ldgDB", + "txnDB", + "app", + "ledger", + "current" + ], + "lineno": 100, + "name": "saveValidatedLedger" + }, + { + "args": [ + "session", + "ledgerSeq", + "j" + ], + "lineno": 113, + "name": "getLedgerInfoByIndex" + }, + { + "args": [ + "session", + "j" + ], + "lineno": 122, + "name": "getNewestLedgerInfo" + }, + { + "args": [ + "session", + "ledgerFirstIndex", + "j" + ], + "lineno": 133, + "name": "getLimitedOldestLedgerInfo" + }, + { + "args": [ + "session", + "ledgerFirstIndex", + "j" + ], + "lineno": 144, + "name": "getLimitedNewestLedgerInfo" + }, + { + "args": [ + "session", + "ledgerHash", + "j" + ], + "lineno": 155, + "name": "getLedgerInfoByHash" + }, + { + "args": [ + "session", + "ledgerIndex" + ], + "lineno": 166, + "name": "getHashByIndex" + }, + { + "args": [ + "session", + "ledgerIndex", + "j" + ], + "lineno": 177, + "name": "getHashesByIndex" + }, + { + "args": [ + "session", + "minSeq", + "maxSeq", + "j" + ], + "lineno": 191, + "name": "getHashesByIndex" + }, + { + "args": [ + "session", + "app", + "startIndex", + "quantity" + ], + "lineno": 208, + "name": "getTxHistory" + }, + { + "args": [ + "session", + "app", + "ledgerMaster", + "options", + "j" + ], + "lineno": 225, + "name": "getOldestAccountTxs" + }, + { + "args": [ + "session", + "app", + "ledgerMaster", + "options", + "j" + ], + "lineno": 247, + "name": "getNewestAccountTxs" + }, + { + "args": [ + "session", + "app", + "options", + "j" + ], + "lineno": 269, + "name": "getOldestAccountTxsB" + }, + { + "args": [ + "session", + "app", + "options", + "j" + ], + "lineno": 291, + "name": "getNewestAccountTxsB" + }, + { + "args": [ + "session", + "onUnsavedLedger", + "onTransaction", + "options", + "page_length" + ], + "lineno": 313, + "name": "oldestAccountTxPage" + }, + { + "args": [ + "session", + "onUnsavedLedger", + "onTransaction", + "options", + "page_length" + ], + "lineno": 336, + "name": "newestAccountTxPage" + }, + { + "args": [ + "session", + "app", + "id", + "range", + "ec" + ], + "lineno": 359, + "name": "getTransaction" + }, + { + "args": [ + "session", + "config", + "j" + ], + "lineno": 382, + "name": "dbHasSpace" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/rdb/backend/detail/Node.h.ai.md b/src/xrpld/app/rdb/backend/detail/Node.h.ai.md new file mode 100644 index 0000000000..709f7d97ba --- /dev/null +++ b/src/xrpld/app/rdb/backend/detail/Node.h.ai.md @@ -0,0 +1,60 @@ +# `src/xrpld/app/rdb/backend/detail/Node.h` + +## Role in the System + +`Node.h` is the internal function contract for the node-mode SQLite relational database in the XRPL server. It sits inside the `xrpl::detail` namespace — a deliberate signal that nothing outside the `rdb/backend` layer should include it directly. The public-facing surface lives one level up in `SQLiteDatabase`, which implements the `RelationalDatabase` abstract interface consumed by the rest of the application. `Node.h` and its counterpart `Node.cpp` exist to keep all SQLite-specific query construction, schema management, and result deserialization out of both the public interface and callers' compilation units. + +The header declares the full vocabulary of SQL operations needed to manage two SQLite databases: a ledger DB (holding the `Ledgers` table) and a transaction DB (holding `Transactions` and `AccountTransactions`). Every public function in `SQLiteDatabase` delegates into one of the routines declared here, forwarding its internally held `soci::session` references as explicit parameters rather than relying on member state. This stateless design is the key architectural choice: the `detail` functions are pure in the sense that they have no coupling to the owning `SQLiteDatabase` object and can therefore be tested or composed independently. + +## Supporting Types + +`DatabasePairValid` is the startup return value of `makeLedgerDBs`. It bundles two `std::unique_ptr` — one for ledgers, one for transactions — with a `bool valid` flag. The `valid` field encodes the result of a schema compatibility probe: old database files had a primary key on `AccountTransactions` that was removed for write-performance reasons, and `valid = false` tells the caller that a migration is needed before the database can be used. Collapsing these three pieces into a single struct avoids output-parameter clutter on a factory function that already takes four inputs. + +`TableType` is a scoped enum covering the three tables the detail layer manages: `Ledgers`, `Transactions`, and `AccountTransactions`. The companion constant `TableTypeCount = 3` is coupled to it by a comment warning that it must be updated whenever the enum changes, and the implementation enforces this at compile time via `static_assert`. This pattern is common throughout the codebase: the assert turns a silent mismatch between the enum and a table-name switch into a build error. + +## Database Initialization + +`makeLedgerDBs` is the entry point for opening both databases. It accepts the server `Config`, a `DatabaseCon::Setup` (which encodes filesystem paths and tuning parameters), a `DatabaseCon::CheckpointerSetup` (WAL-mode checkpointing configuration), and a journal. The `valid` flag in the returned `DatabasePairValid` represents an irreversible pass/fail check — if the existing schema is incompatible the caller must act before any read or write operations proceed. + +## Table-Level Introspection and Pruning + +Six generic utilities operate on an arbitrary table identified by `TableType`: + +- `getMinLedgerSeq` and `getMaxLedgerSeq` return the ledger-sequence bounds of whatever data is currently stored, returning `std::optional` to distinguish an empty table from sequence zero. +- `getRows` and `getRowsMinMax` provide row counts; `getRowsMinMax` additionally returns the sequence bounds in a single `RelationalDatabase::CountMinMax` to avoid a three-round-trip cost for callers that need all three values. +- `deleteByLedgerSeq` removes all rows matching a specific ledger sequence, and `deleteBeforeLedgerSeq` removes all rows at or below a given sequence. These are the workhorses of the online-delete subsystem, which continuously prunes old ledgers to keep database size bounded. + +All six accept a raw `soci::session&` rather than a higher-level connection object, consistent with the overall design that keeps session lifetime management outside this layer. + +## Ledger Persistence + +`saveValidatedLedger` is the most critical write path. It receives both database handles, the `Application` object (needed for the node store), and the validated `Ledger`. The asymmetric signature — `ldgDB` as a plain reference but `txnDB` as a `const unique_ptr&` — reflects the fact that transaction DB writes are conditional: if `useTxTables()` is false in the config, the pointer is null and the transaction-writing path is skipped entirely. The function returns `bool` to indicate whether the save succeeded, allowing upstream retry logic to operate on soft failures like missing tree nodes. + +## Ledger Lookup Functions + +Eight functions retrieve ledger header data through various keying strategies: + +- `getLedgerInfoByIndex` and `getLedgerInfoByHash` are the primary lookup paths for a single ledger. +- `getNewestLedgerInfo`, `getLimitedOldestLedgerInfo`, and `getLimitedNewestLedgerInfo` handle boundary queries, with the "limited" variants applying a minimum-sequence floor — useful during startup when the server wants to discover what ledgers are available within a usable range. +- `getHashByIndex` returns just the ledger hash, while `getHashesByIndex` returns both the ledger hash and its parent hash as a `LedgerHashPair`. The overload that accepts `minSeq`/`maxSeq` returns a `std::map` for bulk range queries, avoiding N round-trips when the hash chain needs to be validated over many ledgers. + +## Account Transaction Queries + +This is where the function surface becomes broader. The API provides four parametric query functions controlled by `RelationalDatabase::AccountTxOptions`, which specifies the target account, ledger range, result count, and pagination offset: + +- `getOldestAccountTxs` and `getNewestAccountTxs` return deserialized `Transaction` + `TxMeta` pairs in ascending or descending order respectively, consuming `LedgerMaster` to resolve ledger context during deserialization. +- `getOldestAccountTxsB` and `getNewestAccountTxsB` are the binary variants — they return raw serialized blobs via `txnMetaLedgerType` tuples without deserializing, which is significantly cheaper and used when the RPC caller has requested the `binary` flag. + +The sign convention on the integer return value is noteworthy: a non-negative value means "number of transactions processed," while a negative value means "number of transactions skipped." This encodes the pagination skip count compactly without an additional out-parameter. + +## Cursor-Based Pagination + +`oldestAccountTxPage` and `newestAccountTxPage` implement stateless marker-based pagination — the preferred approach for streaming large account histories across multiple RPC calls. Rather than using SQL `OFFSET` (which has O(offset) cost), the marker encodes a `(ledgerSeq, txnSeq)` position that is translated into a `WHERE` predicate on the next call. Both functions accept two callbacks rather than returning a container: `onUnsavedLedger` is called for any ledger sequence in the range that has no database row (allowing the caller to trigger background saves), and `onTransaction` receives each result blob by move. Returning a `std::optional` signals whether more results exist: a populated marker means the caller should issue a follow-up request, while an empty optional means the range is exhausted. + +## Disk Space Guard + +`dbHasSpace` is a defensive pre-flight check called before write operations. It tests two independent failure modes: OS-level free space on the database filesystem (via `boost::filesystem::space`) and SQLite's internal page-count ceiling. The journal is used to emit actionable operator guidance if space is low, telling administrators to run the server with the `vacuum` parameter to reclaim SQLite free pages. + +## Relationship to Surrounding Files + +`Node.h` is included only by `Node.cpp` (the implementation) and by `SQLiteDatabase.cpp` (the `SQLiteDatabase` method bodies). This strict include discipline ensures that SQLite and SOCI details never leak into the broader application build graph. The `xrpl::detail` namespace reinforces this boundary: any code that tries to call these functions directly is visibly reaching into an implementation detail, making such coupling obvious during review. \ No newline at end of file diff --git a/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp.ai.json b/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp.ai.json new file mode 100644 index 0000000000..e071212eab --- /dev/null +++ b/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp.ai.json @@ -0,0 +1,726 @@ +{ + "args": [ + { + "lineno": 11, + "name": "config" + }, + { + "lineno": 12, + "name": "setup" + }, + { + "lineno": 13, + "name": "checkpointerSetup" + }, + { + "lineno": 70, + "name": "ledgerSeq" + }, + { + "lineno": 83, + "name": "ledgerSeq" + }, + { + "lineno": 94, + "name": "ledgerSeq" + }, + { + "lineno": 106, + "name": "ledgerSeq" + }, + { + "lineno": 154, + "name": "ledger" + }, + { + "lineno": 154, + "name": "current" + }, + { + "lineno": 165, + "name": "ledgerSeq" + }, + { + "lineno": 191, + "name": "ledgerFirstIndex" + }, + { + "lineno": 204, + "name": "ledgerFirstIndex" + }, + { + "lineno": 217, + "name": "ledgerHash" + }, + { + "lineno": 230, + "name": "ledgerIndex" + }, + { + "lineno": 256, + "name": "minSeq" + }, + { + "lineno": 256, + "name": "maxSeq" + }, + { + "lineno": 269, + "name": "startIndex" + }, + { + "lineno": 282, + "name": "options" + }, + { + "lineno": 297, + "name": "options" + }, + { + "lineno": 312, + "name": "options" + }, + { + "lineno": 325, + "name": "options" + }, + { + "lineno": 338, + "name": "options" + }, + { + "lineno": 366, + "name": "options" + }, + { + "lineno": 394, + "name": "options" + }, + { + "lineno": 419, + "name": "options" + }, + { + "lineno": 444, + "name": "id" + }, + { + "lineno": 444, + "name": "range" + }, + { + "lineno": 444, + "name": "ec" + }, + { + "lineno": 460, + "name": "rhs" + }, + { + "lineno": 466, + "name": "config" + }, + { + "lineno": 478, + "name": "config" + }, + { + "lineno": 525, + "name": "registry" + }, + { + "lineno": 525, + "name": "config" + }, + { + "lineno": 525, + "name": "jobQueue" + }, + { + "lineno": 541, + "name": "registry" + }, + { + "lineno": 541, + "name": "config" + }, + { + "lineno": 541, + "name": "jobQueue" + } + ], + "classes": [ + { + "args": [ + "ServiceRegistry& registry, Config const& config, JobQueue& jobQueue", + "SQLiteDatabase&& rhs" + ], + "lineno": 525, + "name": "SQLiteDatabase" + } + ], + "code_paths": [ + { + "call_chain": [ + "getMinLedgerSeq", + "existsLedger", + "checkoutLedger", + "detail::getMinLedgerSeq" + ], + "entry_point": "getMinLedgerSeq", + "purpose": "Fetches the minimum ledger sequence from the ledger database if it exists.", + "validation_points": [ + "existsLedger" + ] + }, + { + "call_chain": [ + "getTransactionsMinLedgerSeq", + "useTxTables_ check", + "existsTransaction", + "checkoutTransaction", + "detail::getMinLedgerSeq" + ], + "entry_point": "getTransactionsMinLedgerSeq", + "purpose": "Fetches the minimum ledger sequence from the transactions table if transaction tables are enabled and exist.", + "validation_points": [ + "useTxTables_ check", + "existsTransaction" + ] + }, + { + "call_chain": [ + "deleteTransactionByLedgerSeq", + "useTxTables_ check", + "existsTransaction", + "checkoutTransaction", + "detail::deleteByLedgerSeq" + ], + "entry_point": "deleteTransactionByLedgerSeq", + "purpose": "Deletes transactions for a specific ledger sequence if transaction tables are enabled and exist.", + "validation_points": [ + "useTxTables_ check", + "existsTransaction" + ] + }, + { + "call_chain": [ + "getTransactionCount", + "useTxTables_ check", + "existsTransaction", + "checkoutTransaction", + "detail::getRows" + ], + "entry_point": "getTransactionCount", + "purpose": "Returns the count of transactions if transaction tables are enabled and exist.", + "validation_points": [ + "useTxTables_ check", + "existsTransaction" + ] + }, + { + "call_chain": [ + "getLedgerCountMinMax", + "existsLedger", + "checkoutLedger", + "detail::getRowsMinMax" + ], + "entry_point": "getLedgerCountMinMax", + "purpose": "Returns the min, max, and count of ledgers if the ledger database exists.", + "validation_points": [ + "existsLedger" + ] + } + ], + "data_flows": [ + { + "field": "useTxTables_", + "flow": [ + "constructor/config", + "member field", + "checked in public methods (e.g., getTransactionsMinLedgerSeq, deleteTransactionByLedgerSeq)", + "controls access to transaction table logic" + ], + "origin": "Class member, likely set at construction/configuration", + "transformations": [ + "Boolean check (if true, allow transaction table operations)" + ], + "validated_at": "Start of each transaction-related method" + }, + { + "field": "ledgerDb_", + "flow": [ + "makeLedgerDBs", + "ledgerDb_ member", + "used by checkoutLedger", + "passed to detail::* functions" + ], + "origin": "Set by makeLedgerDBs (returns ledger DB connection)", + "transformations": [ + "std::move in makeLedgerDBs", + "shared_ptr passed around" + ], + "validated_at": "existsLedger() before use" + }, + { + "field": "txdb_", + "flow": [ + "makeLedgerDBs", + "txdb_ member", + "used by checkoutTransaction", + "passed to detail::* functions" + ], + "origin": "Set by makeLedgerDBs (returns transaction DB connection)", + "transformations": [ + "std::move in makeLedgerDBs", + "shared_ptr passed around" + ], + "validated_at": "existsTransaction() before use" + }, + { + "field": "ledgerSeq (LedgerIndex)", + "flow": [ + "function parameter", + "passed to detail::* functions" + ], + "origin": "Function parameter (e.g., deleteTransactionByLedgerSeq)", + "transformations": [ + "None in this file" + ], + "validated_at": "Indirectly validated by existence checks before use" + } + ], + "description": "Implements the SQLiteDatabase class for managing ledger and transaction data in SQLite databases within the XRPL (XRP Ledger) server, providing methods for querying, saving, and deleting ledger and transaction records.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "useTxTables_", + "empty", + "string", + "validation" + ], + "evidence": "explicit boolean check at getTransactionsMinLedgerSeq, getAccountTransactionsMinLedgerSeq, deleteTransactionByLedgerSeq, deleteTransactionsBeforeLedgerSeq, deleteAccountTransactionsBeforeLedgerSeq", + "issue_pattern": "Missing empty string validation for useTxTables_", + "why_false_positive": "explicit boolean check validates useTxTables_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger database existence", + "empty", + "string", + "validation" + ], + "evidence": "existsLedger() at getMinLedgerSeq, getMaxLedgerSeq, deleteBeforeLedgerSeq", + "issue_pattern": "Missing empty string validation for ledger database existence", + "why_false_positive": "existsLedger() validates ledger database existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction database existence", + "empty", + "string", + "validation" + ], + "evidence": "existsTransaction() at getTransactionsMinLedgerSeq, getAccountTransactionsMinLedgerSeq, deleteTransactionByLedgerSeq, deleteTransactionsBeforeLedgerSeq, deleteAccountTransactionsBeforeLedgerSeq", + "issue_pattern": "Missing empty string validation for transaction database existence", + "why_false_positive": "existsTransaction() validates transaction database existence for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp", + "functions": [ + { + "args": [ + "config", + "setup", + "checkpointerSetup" + ], + "lineno": 11, + "name": "makeLedgerDBs" + }, + { + "args": [], + "lineno": 19, + "name": "getMinLedgerSeq" + }, + { + "args": [], + "lineno": 30, + "name": "getTransactionsMinLedgerSeq" + }, + { + "args": [], + "lineno": 44, + "name": "getAccountTransactionsMinLedgerSeq" + }, + { + "args": [], + "lineno": 58, + "name": "getMaxLedgerSeq" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 70, + "name": "deleteTransactionByLedgerSeq" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 83, + "name": "deleteBeforeLedgerSeq" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 94, + "name": "deleteTransactionsBeforeLedgerSeq" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 106, + "name": "deleteAccountTransactionsBeforeLedgerSeq" + }, + { + "args": [], + "lineno": 118, + "name": "getTransactionCount" + }, + { + "args": [], + "lineno": 130, + "name": "getAccountTransactionCount" + }, + { + "args": [], + "lineno": 142, + "name": "getLedgerCountMinMax" + }, + { + "args": [ + "ledger", + "current" + ], + "lineno": 154, + "name": "saveValidatedLedger" + }, + { + "args": [ + "ledgerSeq" + ], + "lineno": 165, + "name": "getLedgerInfoByIndex" + }, + { + "args": [], + "lineno": 178, + "name": "getNewestLedgerInfo" + }, + { + "args": [ + "ledgerFirstIndex" + ], + "lineno": 191, + "name": "getLimitedOldestLedgerInfo" + }, + { + "args": [ + "ledgerFirstIndex" + ], + "lineno": 204, + "name": "getLimitedNewestLedgerInfo" + }, + { + "args": [ + "ledgerHash" + ], + "lineno": 217, + "name": "getLedgerInfoByHash" + }, + { + "args": [ + "ledgerIndex" + ], + "lineno": 230, + "name": "getHashByIndex" + }, + { + "args": [ + "ledgerIndex" + ], + "lineno": 243, + "name": "getHashesByIndex" + }, + { + "args": [ + "minSeq", + "maxSeq" + ], + "lineno": 256, + "name": "getHashesByIndex" + }, + { + "args": [ + "startIndex" + ], + "lineno": 269, + "name": "getTxHistory" + }, + { + "args": [ + "options" + ], + "lineno": 282, + "name": "getOldestAccountTxs" + }, + { + "args": [ + "options" + ], + "lineno": 297, + "name": "getNewestAccountTxs" + }, + { + "args": [ + "options" + ], + "lineno": 312, + "name": "getOldestAccountTxsB" + }, + { + "args": [ + "options" + ], + "lineno": 325, + "name": "getNewestAccountTxsB" + }, + { + "args": [ + "options" + ], + "lineno": 338, + "name": "oldestAccountTxPage" + }, + { + "args": [ + "options" + ], + "lineno": 366, + "name": "newestAccountTxPage" + }, + { + "args": [ + "options" + ], + "lineno": 394, + "name": "oldestAccountTxPageB" + }, + { + "args": [ + "options" + ], + "lineno": 419, + "name": "newestAccountTxPageB" + }, + { + "args": [ + "id", + "range", + "ec" + ], + "lineno": 444, + "name": "getTransaction" + }, + { + "args": [ + "rhs" + ], + "lineno": 460, + "name": "SQLiteDatabase" + }, + { + "args": [ + "config" + ], + "lineno": 466, + "name": "ledgerDbHasSpace" + }, + { + "args": [ + "config" + ], + "lineno": 478, + "name": "transactionDbHasSpace" + }, + { + "args": [], + "lineno": 491, + "name": "getKBUsedAll" + }, + { + "args": [], + "lineno": 499, + "name": "getKBUsedLedger" + }, + { + "args": [], + "lineno": 507, + "name": "getKBUsedTransaction" + }, + { + "args": [], + "lineno": 517, + "name": "closeLedgerDB" + }, + { + "args": [], + "lineno": 521, + "name": "closeTransactionDB" + }, + { + "args": [ + "registry", + "config", + "jobQueue" + ], + "lineno": 525, + "name": "SQLiteDatabase" + }, + { + "args": [ + "registry", + "config", + "jobQueue" + ], + "lineno": 541, + "name": "setup_RelationalDatabase" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is primarily backend logic for database access and validation. Typical test coverage would be in integration or unit tests for the database backend, possibly in files like test_sqlite_database.cpp, test_relational_database.cpp, or higher-level ledger/transaction tests. Validation paths (useTxTables_, existsLedger, existsTransaction) are simple boolean/gate checks, so coverage would require tests for both true/false branches. Gaps may exist if tests do not explicitly cover cases where databases do not exist or useTxTables_ is false. Exception handling is not directly tested here, so error path coverage depends on tests for the detail::* functions and database connection failures.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual checks, no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "useTxTables_", + "location": "getTransactionsMinLedgerSeq, getAccountTransactionsMinLedgerSeq, deleteTransactionByLedgerSeq, deleteTransactionsBeforeLedgerSeq, deleteAccountTransactionsBeforeLedgerSeq", + "validated_by": "explicit boolean check", + "validates": [ + "Checks if transaction tables are enabled before proceeding" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "ledger database existence", + "location": "getMinLedgerSeq, getMaxLedgerSeq, deleteBeforeLedgerSeq", + "validated_by": "existsLedger()", + "validates": [ + "Checks if ledger database exists before accessing or modifying it" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (early return)", + "field": "transaction database existence", + "location": "getTransactionsMinLedgerSeq, getAccountTransactionsMinLedgerSeq, deleteTransactionByLedgerSeq, deleteTransactionsBeforeLedgerSeq, deleteAccountTransactionsBeforeLedgerSeq", + "validated_by": "existsTransaction()", + "validates": [ + "Checks if transaction database exists before accessing or modifying it" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp.ai.md b/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp.ai.md new file mode 100644 index 0000000000..13b9b9cbc2 --- /dev/null +++ b/src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp.ai.md @@ -0,0 +1,53 @@ +# `SQLiteDatabase.cpp` — SQLite Backend for the XRPL Relational Database + +## Role in the System + +`SQLiteDatabase` is the concrete SQLite implementation of the abstract `RelationalDatabase` interface defined in `xrpl/rdb/RelationalDatabase.h`. It manages two distinct SQLite database files — a **ledger database** (`ledgerDb_`) that stores ledger headers and metadata, and a **transaction database** (`txdb_`) that stores raw transaction records and the `AccountTransactions` index table. This separation allows nodes configured without transaction history to operate using only the ledger database. + +The class sits between the application layer (ledger validation, RPC handlers) and the raw SOCI SQL execution layer in `detail::` (implemented in `Node.cpp`). Every public method in this file is a thin dispatch wrapper: it guards against missing databases and then hands off to the corresponding `detail::` function. No SQL is executed here. + +## The Two-Guard Pattern + +Nearly every method follows the same three-step idiom before touching any database: + +1. Check `useTxTables_` (transaction methods only): if the node is configured not to store transactions, return immediately with an empty/zero/false result. +2. Call `existsLedger()` or `existsTransaction()`: these simply cast the respective `std::unique_ptr` to `bool`, providing a null-safe check without dereferencing. +3. Call `checkoutLedger()` or `checkoutTransaction()`, which calls `DatabaseCon::checkoutDb()` to obtain a RAII-scoped session from the connection pool. + +This pattern is deliberately defensive. Databases can be absent because they failed to open (during startup) or were explicitly closed via `closeLedgerDB()` / `closeTransactionDB()`. Rather than crashing, every method gracefully returns an empty optional, an empty container, or a zero count. + +## `useTxTables_` and the Lite-Node Configuration + +The `useTxTables_` flag, set from `config.useTxTables()` at construction time, controls whether all transaction-related methods are active. When false, methods like `getTransactionsMinLedgerSeq()`, `deleteTransactionByLedgerSeq()`, `getTransactionCount()`, and the full account-transaction query family all short-circuit immediately. This allows XRPL nodes operating in a "history-lite" mode — where on-disk transaction storage is disabled — to share the same `RelationalDatabase` interface without special casing at call sites. + +## Transaction Query API Design + +The transaction query surface is organized along two orthogonal axes: + +**Direction** — Oldest vs. Newest: the `getOldestAccountTxs` / `getNewestAccountTxs` family queries in ascending vs. descending account-sequence order. + +**Serialization** — Plain vs. Binary ("B"): the plain variants (`getOldestAccountTxs`, `getNewestAccountTxs`) return deserialized `std::shared_ptr` and `std::shared_ptr` objects via the `AccountTxs` type alias. The binary variants (suffixed `B`, like `getOldestAccountTxsB`) return raw `Blob` tuples as `MetaTxsList`, skipping deserialization entirely. The binary path is more efficient for RPC handlers that will re-serialize the data anyway. + +**Paging** — List vs. Page: the list variants accept an `AccountTxOptions` with `offset` and `limit` fields. The paging variants (`oldestAccountTxPage`, `newestAccountTxPage`, and their `B` counterparts) accept an `AccountTxPageOptions` with a cursor `marker` (ledger sequence + transaction sequence pair) for stateless pagination. The paging methods use a callback-based design: the detail layer iterates the database and fires `onTransaction` lambdas for each result, decoupling result accumulation from SQL iteration. This means the same `detail::oldestAccountTxPage` / `detail::newestAccountTxPage` functions are reused by both the plain and binary page methods, with different lambdas passed in. + +The page lengths are constants local to each paging method: 200 for the deserialized `AccountTx` pages (heavier, due to object construction) and 500 for the binary `MetaTxsList` pages (lighter, since they are raw blobs). + +## Paging Callbacks and `saveLedgerAsync` + +The paging methods bind a second callback, `onUnsavedLedger`, constructed via `std::bind(saveLedgerAsync, std::ref(registry_.get().getApp()), _1)`. This allows the detail layer to asynchronously persist any ledger it encounters during the scan that is not yet in the database, without blocking the query. This is a correctness safeguard: account transaction queries walk ledger ranges, and a ledger not yet durably saved would create gaps in the pagination marker chain. + +## Construction and Initialization + +The constructor takes a `ServiceRegistry&`, `Config const&`, and `JobQueue&`. It calls `makeLedgerDBs()` which delegates to `detail::makeLedgerDBs()` — a function that opens both SQLite files, returning a `DatabasePairValid` struct containing two `unique_ptr` values and a success flag. The returned pointers are moved into `ledgerDb_` and `txdb_`. If setup fails, the constructor logs at fatal severity and throws `std::runtime_error`, making database failure a hard startup error rather than a silent degraded mode. + +The move constructor uses `std::exchange` to transfer `ledgerDb_` and `txdb_`, which sets the source members to null as a side effect — important to ensure the moved-from object's `existsLedger()` and `existsTransaction()` will correctly return false. + +## Disk Space and Metrics + +`getKBUsedAll()` calls the SOCI-level `xrpl::getKBUsedAll()` on the ledger session to report total database disk usage. `getKBUsedLedger()` and `getKBUsedTransaction()` call `xrpl::getKBUsedDB()` against their respective sessions. These functions are exposed for monitoring and administrative RPCs that report server resource consumption. + +`ledgerDbHasSpace()` and `transactionDbHasSpace()` check against configured thresholds, allowing the server to reject new work before disk exhaustion causes write failures. + +## Relationship to `detail::` and `DatabaseCon` + +All SQL execution lives in `detail::` functions declared in `Node.h`. The `DatabaseCon` class (from `xrpl/rdb/DatabaseCon.h`) manages the underlying SOCI connection pool and provides `checkoutDb()`, which returns a RAII session guard. The `SQLiteDatabase` class never manipulates SOCI sessions directly — it only holds the `DatabaseCon` unique pointers and passes checked-out sessions down to detail functions. This layering keeps the dispatch logic here free of SQL syntax and makes both layers independently testable. \ No newline at end of file diff --git a/src/xrpld/app/rdb/detail/PeerFinder.cpp.ai.json b/src/xrpld/app/rdb/detail/PeerFinder.cpp.ai.json new file mode 100644 index 0000000000..1bc327c775 --- /dev/null +++ b/src/xrpld/app/rdb/detail/PeerFinder.cpp.ai.json @@ -0,0 +1,460 @@ +{ + "args": [ + { + "lineno": 5, + "name": "session" + }, + { + "lineno": 5, + "name": "config" + }, + { + "lineno": 5, + "name": "j" + }, + { + "lineno": 27, + "name": "session" + }, + { + "lineno": 27, + "name": "currentSchemaVersion" + }, + { + "lineno": 27, + "name": "j" + }, + { + "lineno": 120, + "name": "session" + }, + { + "lineno": 120, + "name": "func" + }, + { + "lineno": 135, + "name": "session" + }, + { + "lineno": 135, + "name": "v" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "initPeerFinderDB" + ], + "entry_point": "initPeerFinderDB", + "purpose": "Initializes the PeerFinder database schema and tables.", + "validation_points": [ + "SchemaVersion.version: validated by manual check (if/else) in updatePeerFinderDB", + "PeerFinder_BootstrapCache.address: validated by SQL schema constraint (UNIQUE, NOT NULL)", + "PeerFinder_BootstrapCache.valence: validated by SQL schema (INTEGER type)", + "PeerFinder_BootstrapCache.id: validated by SQL schema (PRIMARY KEY AUTOINCREMENT)" + ] + }, + { + "call_chain": [ + "updatePeerFinderDB" + ], + "entry_point": "updatePeerFinderDB", + "purpose": "Checks and updates the PeerFinder database schema version, migrates data if needed.", + "validation_points": [ + "SchemaVersion.version: validated by manual check (if/else) against currentSchemaVersion", + "PeerFinder_BootstrapCache.address: validated by SQL schema constraint (UNIQUE, NOT NULL)", + "PeerFinder_BootstrapCache.valence: validated by SQL schema (INTEGER type)", + "PeerFinder_BootstrapCache.id: validated by SQL schema (PRIMARY KEY AUTOINCREMENT)" + ] + }, + { + "call_chain": [ + "readPeerFinderDB" + ], + "entry_point": "readPeerFinderDB", + "purpose": "Reads PeerFinder bootstrap cache entries from the database.", + "validation_points": [ + "PeerFinder_BootstrapCache.address: validated by SQL schema constraint (UNIQUE, NOT NULL)", + "PeerFinder_BootstrapCache.valence: validated by SQL schema (INTEGER type)" + ] + }, + { + "call_chain": [ + "savePeerFinderDB" + ], + "entry_point": "savePeerFinderDB", + "purpose": "Saves PeerFinder bootstrap cache entries to the database.", + "validation_points": [ + "PeerFinder_BootstrapCache.address: validated by SQL schema constraint (UNIQUE, NOT NULL)", + "PeerFinder_BootstrapCache.valence: validated by SQL schema (INTEGER type)" + ] + } + ], + "data_flows": [ + { + "field": "SchemaVersion.version", + "flow": [ + "Database (SchemaVersion.version)", + "Read into boost::optional vO in updatePeerFinderDB", + "Compared to currentSchemaVersion in updatePeerFinderDB", + "Used to determine if migration is needed" + ], + "origin": "SchemaVersion table in database", + "transformations": [ + "Read from DB", + "Defaulted to 0 if not present", + "Compared via if/else" + ], + "validated_at": "updatePeerFinderDB (manual check via if/else)" + }, + { + "field": "PeerFinder_BootstrapCache.address", + "flow": [ + "Database (PeerFinder_BootstrapCache.address)", + "Read into string s in updatePeerFinderDB", + "Converted to beast::IP::Endpoint via from_string(s)", + "Checked for validity (is_unspecified)", + "Used to populate PeerFinder::Store::Entry" + ], + "origin": "PeerFinder_BootstrapCache table in database", + "transformations": [ + "Read from DB", + "String to Endpoint conversion", + "Validity check" + ], + "validated_at": "SQL schema (UNIQUE, NOT NULL), and runtime check (is_unspecified)" + }, + { + "field": "PeerFinder_BootstrapCache.valence", + "flow": [ + "Database (PeerFinder_BootstrapCache.valence)", + "Read into int valence in updatePeerFinderDB", + "Assigned to PeerFinder::Store::Entry.valence" + ], + "origin": "PeerFinder_BootstrapCache table in database", + "transformations": [ + "Read from DB", + "Direct assignment" + ], + "validated_at": "SQL schema (INTEGER type)" + }, + { + "field": "PeerFinder_BootstrapCache.id", + "flow": [ + "Database (PeerFinder_BootstrapCache.id)", + "Used as primary key for entries" + ], + "origin": "PeerFinder_BootstrapCache table in database", + "transformations": [ + "Auto-incremented by DB" + ], + "validated_at": "SQL schema (PRIMARY KEY AUTOINCREMENT)" + } + ], + "description": "Implements database initialization, migration, reading, and saving logic for the PeerFinder component in the xrpl namespace, managing peer bootstrap cache and schema versioning using SOCI.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "PeerFinder_BootstrapCache.address (UNIQUE, NOT NULL)", + "validation", + "missing", + "check" + ], + "evidence": "Field PeerFinder_BootstrapCache.address (UNIQUE, NOT NULL) validated by SOCI (C++ DB access), SQLite schema constraints", + "issue_pattern": "Missing validation for PeerFinder_BootstrapCache.address (UNIQUE, NOT NULL)", + "why_false_positive": "SOCI (C++ DB access), SQLite schema constraints validates PeerFinder_BootstrapCache.address (UNIQUE, NOT NULL) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "PeerFinder_BootstrapCache.valence (INTEGER)", + "validation", + "missing", + "check" + ], + "evidence": "Field PeerFinder_BootstrapCache.valence (INTEGER) validated by SOCI (C++ DB access), SQLite schema constraints", + "issue_pattern": "Missing validation for PeerFinder_BootstrapCache.valence (INTEGER)", + "why_false_positive": "SOCI (C++ DB access), SQLite schema constraints validates PeerFinder_BootstrapCache.valence (INTEGER) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "PeerFinder_BootstrapCache.id (INTEGER PRIMARY KEY AUTOINCREMENT)", + "validation", + "missing", + "check" + ], + "evidence": "Field PeerFinder_BootstrapCache.id (INTEGER PRIMARY KEY AUTOINCREMENT) validated by SOCI (C++ DB access), SQLite schema constraints", + "issue_pattern": "Missing validation for PeerFinder_BootstrapCache.id (INTEGER PRIMARY KEY AUTOINCREMENT)", + "why_false_positive": "SOCI (C++ DB access), SQLite schema constraints validates PeerFinder_BootstrapCache.id (INTEGER PRIMARY KEY AUTOINCREMENT) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "SchemaVersion.version", + "empty", + "string", + "validation" + ], + "evidence": "manual check (if/else) at updatePeerFinderDB", + "issue_pattern": "Missing empty string validation for SchemaVersion.version", + "why_false_positive": "manual check (if/else) validates SchemaVersion.version for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "SchemaVersion.version", + "empty", + "string", + "validation" + ], + "evidence": "manual check (if/else) at updatePeerFinderDB", + "issue_pattern": "Missing empty string validation for SchemaVersion.version", + "why_false_positive": "manual check (if/else) validates SchemaVersion.version for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "PeerFinder_BootstrapCache.address", + "empty", + "string", + "validation" + ], + "evidence": "SQL schema constraint (UNIQUE, NOT NULL) at initPeerFinderDB", + "issue_pattern": "Missing empty string validation for PeerFinder_BootstrapCache.address", + "why_false_positive": "SQL schema constraint (UNIQUE, NOT NULL) validates PeerFinder_BootstrapCache.address for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "PeerFinder_BootstrapCache.address", + "format", + "validation", + "invalid" + ], + "evidence": "SQL schema constraint (UNIQUE, NOT NULL) at initPeerFinderDB", + "issue_pattern": "Missing format validation for PeerFinder_BootstrapCache.address", + "why_false_positive": "SQL schema constraint (UNIQUE, NOT NULL) validates PeerFinder_BootstrapCache.address format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "PeerFinder_BootstrapCache.valence", + "empty", + "string", + "validation" + ], + "evidence": "SQL schema (INTEGER type) at initPeerFinderDB", + "issue_pattern": "Missing empty string validation for PeerFinder_BootstrapCache.valence", + "why_false_positive": "SQL schema (INTEGER type) validates PeerFinder_BootstrapCache.valence for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "PeerFinder_BootstrapCache.valence", + "type", + "validation", + "check" + ], + "evidence": "SQL schema (INTEGER type) at initPeerFinderDB", + "issue_pattern": "Missing type validation for PeerFinder_BootstrapCache.valence", + "why_false_positive": "SQL schema (INTEGER type) validates PeerFinder_BootstrapCache.valence type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "PeerFinder_BootstrapCache.id", + "empty", + "string", + "validation" + ], + "evidence": "SQL schema (PRIMARY KEY AUTOINCREMENT) at initPeerFinderDB", + "issue_pattern": "Missing empty string validation for PeerFinder_BootstrapCache.id", + "why_false_positive": "SQL schema (PRIMARY KEY AUTOINCREMENT) validates PeerFinder_BootstrapCache.id for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "PeerFinder_BootstrapCache.id", + "type", + "validation", + "check" + ], + "evidence": "SQL schema (PRIMARY KEY AUTOINCREMENT) at initPeerFinderDB", + "issue_pattern": "Missing type validation for PeerFinder_BootstrapCache.id", + "why_false_positive": "SQL schema (PRIMARY KEY AUTOINCREMENT) validates PeerFinder_BootstrapCache.id type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/rdb/detail/PeerFinder.cpp", + "functions": [ + { + "args": [ + "session", + "config", + "j" + ], + "lineno": 5, + "name": "initPeerFinderDB" + }, + { + "args": [ + "session", + "currentSchemaVersion", + "j" + ], + "lineno": 27, + "name": "updatePeerFinderDB" + }, + { + "args": [ + "session", + "func" + ], + "lineno": 120, + "name": "readPeerFinderDB" + }, + { + "args": [ + "session", + "v" + ], + "lineno": 135, + "name": "savePeerFinderDB" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage (no test code or test macros). Likely, integration or unit tests exist elsewhere (e.g., in test/app/rdb/PeerFinder_test.cpp or similar). The schema versioning logic and migration paths may not be fully covered by tests, especially for edge cases (e.g., corrupted DB, unexpected schema versions). The runtime validation of address strings (is_unspecified) may not be thoroughly tested for all malformed input cases. SQL constraint violations (e.g., duplicate addresses) are likely only tested indirectly via integration tests.", + "validation_architecture": { + "auto_validated_fields": [ + "PeerFinder_BootstrapCache.address (UNIQUE, NOT NULL)", + "PeerFinder_BootstrapCache.valence (INTEGER)", + "PeerFinder_BootstrapCache.id (INTEGER PRIMARY KEY AUTOINCREMENT)" + ], + "framework": "SOCI (C++ DB access), SQLite schema constraints", + "validation_layer": "business_logic (manual checks in C++), database schema (SQL constraints)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error", + "field": "SchemaVersion.version", + "location": "updatePeerFinderDB", + "validated_by": "manual check (if/else)", + "validates": [ + "Checks if database schema version is higher than expected (version > currentSchemaVersion)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (just logs)", + "field": "SchemaVersion.version", + "location": "updatePeerFinderDB", + "validated_by": "manual check (if/else)", + "validates": [ + "Checks if database schema version is lower than expected (version < currentSchemaVersion)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "SQL constraint violation (exception thrown by SOCI/SQLite)", + "field": "PeerFinder_BootstrapCache.address", + "location": "initPeerFinderDB", + "validated_by": "SQL schema constraint (UNIQUE, NOT NULL)", + "validates": [ + "Ensures address is unique", + "Ensures address is not null" + ], + "validation_type": "format" + }, + { + "confidence": 0.8, + "error_thrown": "SQL type error (exception thrown by SOCI/SQLite)", + "field": "PeerFinder_BootstrapCache.valence", + "location": "initPeerFinderDB", + "validated_by": "SQL schema (INTEGER type)", + "validates": [ + "Ensures valence is an integer" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "SQL type error (exception thrown by SOCI/SQLite)", + "field": "PeerFinder_BootstrapCache.id", + "location": "initPeerFinderDB", + "validated_by": "SQL schema (PRIMARY KEY AUTOINCREMENT)", + "validates": [ + "Ensures id is an integer and unique" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/app/rdb/detail/PeerFinder.cpp.ai.md b/src/xrpld/app/rdb/detail/PeerFinder.cpp.ai.md new file mode 100644 index 0000000000..2a7ee142a6 --- /dev/null +++ b/src/xrpld/app/rdb/detail/PeerFinder.cpp.ai.md @@ -0,0 +1,38 @@ +# `src/xrpld/app/rdb/detail/PeerFinder.cpp` + +This file is the SQLite persistence layer for the XRPL node's PeerFinder subsystem. PeerFinder is responsible for discovering and maintaining peer connections; to survive restarts without having to re-discover the network from scratch, it persists a "bootstrap cache" of known peer endpoints and their associated valence scores to disk. This file implements the four free functions that perform all DDL, migration, read, and write operations against that cache. + +## Role in the System + +The direct consumer of these functions is `StoreSqdb` (`src/xrpld/peerfinder/detail/StoreSqdb.h`), a concrete implementation of the abstract `PeerFinder::Store` interface. `StoreSqdb` owns a `soci::session` and delegates every database interaction to the four functions here. This separation keeps raw SQL out of the PeerFinder business logic and centralizes all schema knowledge in one file, consistent with how the rest of the XRPL codebase partitions database concerns under `src/xrpld/app/rdb/`. + +## Schema + +The database contains two tables. `SchemaVersion` maps a component name (here `'PeerFinder'`) to an integer version number, serving as a migration checkpoint. `PeerFinder_BootstrapCache` holds the actual data: an auto-incremented `id`, a `TEXT UNIQUE NOT NULL` `address` (a serialized `beast::IP::Endpoint`), and an `INTEGER` `valence` representing a peer's connection quality score. An index on `address` accelerates uniqueness checks and lookups. + +## Initialization and Migration Split + +`initPeerFinderDB` and `updatePeerFinderDB` are deliberately separate entry points, called in sequence by `StoreSqdb::open()`. Initialization uses `CREATE TABLE IF NOT EXISTS`, making it idempotent and safe on any database regardless of prior state. Migration, called immediately after, reads the stored schema version and applies forward-only patches. This design means a crash during initial schema creation leaves no half-initialized state (the entire `initPeerFinderDB` runs inside a single `soci::transaction`), and migration logic never needs to compensate for incomplete initialization. + +## Migration Logic in `updatePeerFinderDB` + +The current schema version is `4`, encoded as a compile-time enum constant in `StoreSqdb`. `updatePeerFinderDB` receives this value and compares it against whatever version is stored in the database: + +- If the stored version is *higher* than expected, the code throws `std::runtime_error`. This is the forward-compatibility guard — an older binary must not silently corrupt a database written by a newer one. +- If the stored version is *lower*, the appropriate migration blocks run. + +The `version < 4` block removes a now-defunct `uptime` column from `PeerFinder_BootstrapCache`. SQLite does not support `DROP COLUMN` (at least not in the versions targeted here), so the standard workaround is used: create a replacement table `PeerFinder_BootstrapCache_Next` with the new schema, copy all rows with address-string validation, drop the original, and rename. During the copy, each address string is parsed through `beast::IP::Endpoint::from_string` and checked with `is_unspecified()`; corrupted rows are logged and discarded rather than carried forward. + +The `version < 3` block simply drops three legacy tables (`LegacyEndpoints`, `PeerFinderLegacyEndpoints`, `PeerFinder_LegacyEndpoints`) that existed in even older schema versions. The two blocks are independent and ordered so that a database at version 2 executes both migrations in a single transaction, while a database at version 3 only executes the `< 4` block. + +After all applicable patches, the function upserts the current version into `SchemaVersion` using `INSERT OR REPLACE`, ensuring the next startup sees the updated version regardless of whether a prior version row existed. + +## Read and Write Patterns + +`readPeerFinderDB` uses a cursor-based iteration rather than loading all rows into a vector first. It prepares a `soci::statement`, calls `execute()`, then calls `fetch()` in a loop, invoking the caller-supplied `std::function` for each row. Passing a callback rather than returning a container keeps the string-to-endpoint conversion and validity filtering in `StoreSqdb::load()` where semantic ownership belongs, and avoids an intermediate heap allocation. + +`savePeerFinderDB` implements a full-replacement strategy: it deletes all rows, then bulk-inserts the new set. This matches how `Bootcache` manages its in-memory state — it maintains a ranked set and periodically writes the whole thing to disk, so incremental diff-based persistence would add complexity without benefit. The bulk insert uses SOCI's vector binding (`soci::use(s), soci::use(valence)` where both are `std::vector`), which results in a single prepared statement executing across all rows rather than N individual INSERT calls. The entire operation is wrapped in `soci::transaction` to prevent a partial write from leaving the cache in an inconsistent state. + +## SOCI Compatibility Note + +The migration code includes a notable comment: SOCI requires `boost::optional` rather than `std::optional` for nullable output parameters. This is a known limitation of SOCI's type-binding layer at the versions used by this codebase, and the comment flags it explicitly to prevent well-meaning refactors from breaking compilation by switching to the standard library type. \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/Taker.h.ai.json b/src/xrpld/app/tx/detail/Taker.h.ai.json new file mode 100644 index 0000000000..95edb84d62 --- /dev/null +++ b/src/xrpld/app/tx/detail/Taker.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/app/tx/detail/Taker.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/src/xrpld/app/tx/detail/Taker.h.ai.md b/src/xrpld/app/tx/detail/Taker.h.ai.md new file mode 100644 index 0000000000..e9967d00c5 --- /dev/null +++ b/src/xrpld/app/tx/detail/Taker.h.ai.md @@ -0,0 +1,27 @@ +# `src/xrpld/app/tx/detail/Taker.h` — Retired Offer Crossing Header + +## Current State + +This file is currently **empty** (zero bytes). It is a vacant placeholder at a historically significant path in the XRPL offer-crossing pipeline. The content was removed in commit `433eeabfa` under the message *"chore: Remove unused code after flow cross retirement (#5575)"* — a cleanup pass that followed the retirement of the `FlowCross` amendment. + +## What the File Previously Contained + +Before removal, `Taker.h` declared two classes and a supporting enum that together formed the original offer-crossing engine for `OfferCreate` transactions. + +**`CrossType`** was a scoped enum with three values — `XrpToIou`, `IouToXrp`, and `IouToIou` — classifying the direction of an individual crossing to drive the correct flow calculation branch. + +**`BasicTaker`** was an abstract base class that tracked the full state of the active party (the "taker") as it consumed offers from an order book. Its private members included the taker's `AccountID`, a `Quality` representing the submitted offer's exchange rate, a `threshold_` quality below which offers would be rejected, the original and remaining `Amounts`, the input and output `Issue` references, cached transfer `Rate` values for both currency legs, and the `CrossType`. The protected inner struct `BasicTaker::Flow` bundled two `Amounts` — `order` (the amounts transacted between counterparties) and `issuers` (the gross amounts including gateway transfer fees) — with a `sanity_check()` that enforced neither leg could be XRP-to-XRP and all values had to be non-negative. + +`BasicTaker` exposed the core crossing logic through three private `flow_*` methods: `flow_xrp_to_iou`, `flow_iou_to_xrp`, and `flow_iou_to_iou`. Each computed the precise flow achievable given the offer's stated amounts, the taker's remaining quantity, the owner's available funds, and the applicable gateway rates. A static `effective_rate()` helper produced the actual rate for a given currency transfer between two accounts, collapsing to `parityRate` when either participant was the issuer. Public methods included `remaining_offer()` (the amount still to be placed after crossing), `reject(Quality)` (quality threshold check), `unfunded()` (taker has run out of input), `done()` (order fully satisfied or unfunded), and the two overloads of `do_cross` — one for direct single-offer crossing and one for bridged crossing through a two-offer XRP intermediate. + +**`Taker`** was the concrete subclass that bound `BasicTaker` to an actual `ApplyView` and executed ledger mutations. Its `cross(Offer&)` and `cross(Offer& leg1, Offer& leg2)` methods drove the crossing loop; the private `fill()` overloads translated computed `Flow` values into real ledger operations via `transferXRP`, `redeemIOU`, and `issueIOU`. Alongside this, `Taker` tracked XRP flow through autobridging (`xrp_flow_`) and maintained counters for direct and bridge crossings for diagnostic purposes. The `get_funds()` override queried the ledger view for the actual spendable balance. + +## Why It Was Deleted + +The `Taker` approach predates the `FlowCross` amendment. It maintained a bespoke offer-matching loop: for each offer consumed from the book, the `Taker` object calculated flows independently and applied transfers directly. This logic existed in parallel with the payment engine's `flow()` function, which could already handle offer crossing as a special case of payment path evaluation. + +The `FlowCross` amendment unified offer crossing with the payment engine. Once that amendment was retired (made always-active as of consensus in the codebase, via commit `#5562`), the `Taker` class and its 1,394-line test file (`Taker_test.cpp`) became unreachable dead code. Commit `#5575` removed all of it — `Taker.h`, `Taker.cpp`, the tests, and the call sites in `CreateOffer.cpp`. + +## What Replaced It + +Offer crossing now delegates entirely to `flowCross()` in `OfferCreate.cpp`, which calls the `flow()` payment engine from `src/libxrpl/tx/paths/Flow.cpp`. The order-book cursor role previously served by `Taker`'s inner loop is now handled by `FlowOfferStream` in `src/libxrpl/tx/paths/OfferStream.cpp`. This consolidation eliminated the duplicated quality-matching, transfer-rate accounting, and XRP autobridging logic that `BasicTaker` previously maintained separately. \ No newline at end of file diff --git a/src/xrpld/consensus/Consensus.cpp.ai.json b/src/xrpld/consensus/Consensus.cpp.ai.json new file mode 100644 index 0000000000..608a85614f --- /dev/null +++ b/src/xrpld/consensus/Consensus.cpp.ai.json @@ -0,0 +1,569 @@ +{ + "args": [ + { + "lineno": 9, + "name": "anyTransactions" + }, + { + "lineno": 10, + "name": "prevProposers" + }, + { + "lineno": 11, + "name": "proposersClosed" + }, + { + "lineno": 12, + "name": "proposersValidated" + }, + { + "lineno": 13, + "name": "prevRoundTime" + }, + { + "lineno": 14, + "name": "timeSincePrevClose" + }, + { + "lineno": 15, + "name": "openTime" + }, + { + "lineno": 16, + "name": "idleInterval" + }, + { + "lineno": 17, + "name": "parms" + }, + { + "lineno": 18, + "name": "j" + }, + { + "lineno": 19, + "name": "clog" + }, + { + "lineno": 63, + "name": "agreeing" + }, + { + "lineno": 64, + "name": "total" + }, + { + "lineno": 65, + "name": "count_self" + }, + { + "lineno": 66, + "name": "minConsensusPct" + }, + { + "lineno": 67, + "name": "reachedMax" + }, + { + "lineno": 68, + "name": "stalled" + }, + { + "lineno": 69, + "name": "clog" + }, + { + "lineno": 112, + "name": "prevProposers" + }, + { + "lineno": 113, + "name": "currentProposers" + }, + { + "lineno": 114, + "name": "currentAgree" + }, + { + "lineno": 115, + "name": "currentFinished" + }, + { + "lineno": 116, + "name": "previousAgreeTime" + }, + { + "lineno": 117, + "name": "currentAgreeTime" + }, + { + "lineno": 118, + "name": "stalled" + }, + { + "lineno": 119, + "name": "parms" + }, + { + "lineno": 120, + "name": "proposing" + }, + { + "lineno": 121, + "name": "j" + }, + { + "lineno": 122, + "name": "clog" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "shouldCloseLedger" + ], + "entry_point": "shouldCloseLedger", + "purpose": "Determines whether the current ledger should be closed based on network and timing conditions.", + "validation_points": [ + "prevRoundTime validated by if ((prevRoundTime < -1s) || (prevRoundTime > 10min))", + "timeSincePrevClose validated by if ((timeSincePrevClose > 10min))", + "proposersClosed, proposersValidated, prevProposers validated by if ((proposersClosed + proposersValidated) > (prevProposers / 2))", + "anyTransactions validated by if (!anyTransactions)", + "openTime, parms.ledgerMIN_CLOSE validated by if (openTime < parms.ledgerMIN_CLOSE)", + "openTime, prevRoundTime validated by if (openTime < (prevRoundTime / 2))" + ] + }, + { + "call_chain": [ + "checkConsensusReached" + ], + "entry_point": "checkConsensusReached", + "purpose": "Checks if consensus has been reached based on the number of agreeing and total proposers.", + "validation_points": [ + "agreeing, total, minConsensusPct validated by logic inside checkConsensusReached (not fully shown in provided code)" + ] + }, + { + "call_chain": [ + "checkConsensus" + ], + "entry_point": "checkConsensus", + "purpose": "Determines the consensus state (not fully shown in provided code). Likely calls checkConsensusReached internally.", + "validation_points": [ + "Depends on internal logic, likely calls checkConsensusReached for validation" + ] + } + ], + "data_flows": [ + { + "field": "prevRoundTime", + "flow": [ + "consensus state", + "shouldCloseLedger(prevRoundTime)", + "if-statement validation", + "used in openTime comparison" + ], + "origin": "Passed as argument to shouldCloseLedger (likely from consensus state tracking previous round duration)", + "transformations": [ + "Compared to -1s and 10min for sanity check", + "Used in openTime < (prevRoundTime / 2) to throttle ledger closing" + ], + "validated_at": "shouldCloseLedger: if ((prevRoundTime < -1s) || (prevRoundTime > 10min))" + }, + { + "field": "timeSincePrevClose", + "flow": [ + "system clock", + "shouldCloseLedger(timeSincePrevClose)", + "if-statement validation", + "used in idle interval check" + ], + "origin": "Passed as argument to shouldCloseLedger (likely from system clock minus last ledger close time)", + "transformations": [ + "Compared to 10min for sanity check", + "Compared to idleInterval if no transactions" + ], + "validated_at": "shouldCloseLedger: if ((timeSincePrevClose > 10min))" + }, + { + "field": "proposersClosed, proposersValidated, prevProposers", + "flow": [ + "network state", + "shouldCloseLedger(proposersClosed, proposersValidated, prevProposers)", + "if-statement validation", + "used to determine if majority has closed" + ], + "origin": "Passed as arguments to shouldCloseLedger (from network state tracking)", + "transformations": [ + "Summed and compared to half of prevProposers" + ], + "validated_at": "shouldCloseLedger: if ((proposersClosed + proposersValidated) > (prevProposers / 2))" + }, + { + "field": "anyTransactions", + "flow": [ + "transaction queue", + "shouldCloseLedger(anyTransactions)", + "if-statement validation", + "used to determine idle closing" + ], + "origin": "Passed as argument to shouldCloseLedger (from transaction queue or ledger state)", + "transformations": [ + "Boolean check" + ], + "validated_at": "shouldCloseLedger: if (!anyTransactions)" + }, + { + "field": "openTime, parms.ledgerMIN_CLOSE", + "flow": [ + "ledger timer", + "shouldCloseLedger(openTime, parms.ledgerMIN_CLOSE)", + "if-statement validation", + "used to enforce minimum open time" + ], + "origin": "openTime from ledger open duration, parms.ledgerMIN_CLOSE from configuration", + "transformations": [ + "Compared to enforce minimum ledger open time" + ], + "validated_at": "shouldCloseLedger: if (openTime < parms.ledgerMIN_CLOSE)" + } + ], + "description": "Implements consensus-related logic for the XRPL ledger, including determining when to close a ledger, checking if consensus is reached, and evaluating consensus state.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "prevRoundTime", + "empty", + "string", + "validation" + ], + "evidence": "manual if-statement at shouldCloseLedger", + "issue_pattern": "Missing empty string validation for prevRoundTime", + "why_false_positive": "manual if-statement validates prevRoundTime for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "prevRoundTime", + "range", + "bounds", + "validation" + ], + "evidence": "manual if-statement at shouldCloseLedger", + "issue_pattern": "Missing range validation for prevRoundTime", + "why_false_positive": "manual if-statement validates prevRoundTime range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "timeSincePrevClose", + "empty", + "string", + "validation" + ], + "evidence": "manual if-statement at shouldCloseLedger", + "issue_pattern": "Missing empty string validation for timeSincePrevClose", + "why_false_positive": "manual if-statement validates timeSincePrevClose for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "timeSincePrevClose", + "range", + "bounds", + "validation" + ], + "evidence": "manual if-statement at shouldCloseLedger", + "issue_pattern": "Missing range validation for timeSincePrevClose", + "why_false_positive": "manual if-statement validates timeSincePrevClose range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "proposersClosed", + "proposersValidated", + "prevProposers" + ], + "empty", + "string", + "validation" + ], + "evidence": "manual if-statement at shouldCloseLedger", + "issue_pattern": "Missing empty string validation for ['proposersClosed', 'proposersValidated', 'prevProposers']", + "why_false_positive": "manual if-statement validates ['proposersClosed', 'proposersValidated', 'prevProposers'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "anyTransactions", + "empty", + "string", + "validation" + ], + "evidence": "manual if-statement at shouldCloseLedger", + "issue_pattern": "Missing empty string validation for anyTransactions", + "why_false_positive": "manual if-statement validates anyTransactions for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "openTime", + "parms.ledgerMIN_CLOSE" + ], + "empty", + "string", + "validation" + ], + "evidence": "manual if-statement at shouldCloseLedger", + "issue_pattern": "Missing empty string validation for ['openTime', 'parms.ledgerMIN_CLOSE']", + "why_false_positive": "manual if-statement validates ['openTime', 'parms.ledgerMIN_CLOSE'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "openTime", + "prevRoundTime" + ], + "empty", + "string", + "validation" + ], + "evidence": "manual if-statement at shouldCloseLedger", + "issue_pattern": "Missing empty string validation for ['openTime', 'prevRoundTime']", + "why_false_positive": "manual if-statement validates ['openTime', 'prevRoundTime'] for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/consensus/Consensus.cpp", + "functions": [ + { + "args": [ + "anyTransactions", + "prevProposers", + "proposersClosed", + "proposersValidated", + "prevRoundTime", + "timeSincePrevClose", + "openTime", + "idleInterval", + "parms", + "j", + "clog" + ], + "lineno": 7, + "name": "shouldCloseLedger" + }, + { + "args": [ + "agreeing", + "total", + "count_self", + "minConsensusPct", + "reachedMax", + "stalled", + "clog" + ], + "lineno": 61, + "name": "checkConsensusReached" + }, + { + "args": [ + "prevProposers", + "currentProposers", + "currentAgree", + "currentFinished", + "previousAgreeTime", + "currentAgreeTime", + "stalled", + "parms", + "proposing", + "j", + "clog" + ], + "lineno": 110, + "name": "checkConsensus" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "The provided code is core consensus logic and is likely tested in integration and unit tests under the rippled (xrpld) codebase. Typical test files would be found in test/consensus/ or test/ledger/ directories, such as Consensus_test.cpp, LedgerTiming_test.cpp, or LedgerClose_test.cpp. These tests would cover scenarios like normal ledger closing, idle closing, minimum open time, and abnormal timing. However, edge cases such as extremely large or negative prevRoundTime, or network partitions affecting proposersClosed/Validated, may not be exhaustively tested. Manual validation logic (e.g., sanity checks on timing) may lack explicit unit tests for all branches, especially for rare or 'should never happen' conditions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (logs warning, returns true)", + "field": "prevRoundTime", + "location": "shouldCloseLedger", + "validated_by": "manual if-statement", + "validates": [ + "prevRoundTime is not less than -1s", + "prevRoundTime is not greater than 10min" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (logs warning, returns true)", + "field": "timeSincePrevClose", + "location": "shouldCloseLedger", + "validated_by": "manual if-statement", + "validates": [ + "timeSincePrevClose is not greater than 10min" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns true)", + "field": [ + "proposersClosed", + "proposersValidated", + "prevProposers" + ], + "location": "shouldCloseLedger", + "validated_by": "manual if-statement", + "validates": [ + "proposersClosed + proposersValidated > prevProposers / 2" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns bool)", + "field": "anyTransactions", + "location": "shouldCloseLedger", + "validated_by": "manual if-statement", + "validates": [ + "if no transactions, only close if timeSincePrevClose >= idleInterval" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": [ + "openTime", + "parms.ledgerMIN_CLOSE" + ], + "location": "shouldCloseLedger", + "validated_by": "manual if-statement", + "validates": [ + "openTime >= parms.ledgerMIN_CLOSE" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": [ + "openTime", + "prevRoundTime" + ], + "location": "shouldCloseLedger", + "validated_by": "manual if-statement", + "validates": [ + "openTime >= prevRoundTime / 2" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/consensus/Consensus.cpp.ai.md b/src/xrpld/consensus/Consensus.cpp.ai.md new file mode 100644 index 0000000000..d8631af140 --- /dev/null +++ b/src/xrpld/consensus/Consensus.cpp.ai.md @@ -0,0 +1,45 @@ +# Consensus.cpp + +This file implements the three free functions that encode the core timing and agreement logic of the XRPL consensus algorithm: `shouldCloseLedger`, `checkConsensusReached`, and `checkConsensus`. These are pure decision functions — they take observable network state as inputs and return boolean or enum results. All state is owned elsewhere (in the `Consensus` template class declared in `Consensus.h`); this file holds only the policies that drive phase transitions. + +## shouldCloseLedger + +`shouldCloseLedger()` decides whether the currently-open ledger should transition to the `Establish` phase. It is called by the consensus timer and on transaction receipt. The function applies a strict priority ordering of checks: + +1. **Sanity bounds.** If `prevRoundTime` or `timeSincePrevClose` fall outside plausible ranges (`-1s`–`10min`), the ledger closes immediately with a warning log. These represent clock errors or node startup edge cases where normal timing heuristics would produce garbage. + +2. **Majority already closed.** If more than half of known proposers have already closed this ledger (`proposersClosed + proposersValidated > prevProposers / 2`), the node follows. This is a straightforward network-following rule to prevent stragglers from causing unnecessary divergence. + +3. **Idle case (no transactions).** The ledger closes only when `timeSincePrevClose >= idleInterval` (default `ledgerIDLE_INTERVAL` = 15 seconds). Without this, an idle network would close empty ledgers at the minimum close rate, wasting resources. + +4. **Minimum open time.** The ledger will not close before `ledgerMIN_CLOSE` (2 seconds) has elapsed regardless of other signals. This ensures other validators have time to compute the LCL and receive transactions. + +5. **Rate limiting.** If `openTime < prevRoundTime / 2`, closing is blocked. This is a deliberate throttle: a fast node cannot drive the network faster than twice the prior round's pace, which would exclude slower validators and create instability. The design explicitly prioritizes liveness of slower nodes over throughput. + +## checkConsensusReached + +`checkConsensusReached()` is an internal helper that converts raw vote counts into a binary reached/not-reached answer. The non-obvious cases: + +- **Zero peers (`total == 0`).** The function refuses to declare consensus until `reachedMax` (i.e., `currentAgreeTime > ledgerMAX_CONSENSUS`, 15 seconds). This guards against a race condition where a node hasn't yet received any proposals from the network and might prematurely close on its own position — which would likely cause a desync when the actual network position arrives later. + +- **Stalled (`stalled == true`).** Consensus is declared immediately, bypassing the percentage check. The `stalled` condition means all disputed transactions have clear supermajority agreement either for or against inclusion. A Byzantine minority cannot manipulate which transactions make the cut by hovering votes near the threshold, because once everyone's position is stable and clear, the network commits. + +- **Self-counting.** When `count_self` is true, the local node's own position is added to both `agreeing` and `total` before the percentage is computed. `checkConsensus()` passes `proposing` here, so validating nodes count themselves while observers do not. + +## checkConsensus + +`checkConsensus()` returns a `ConsensusState` enum (`No`, `Yes`, `MovedOn`, `Expired`) and calls `checkConsensusReached()` twice with different arguments to answer two distinct questions: + +**First call:** Has the network, including us, reached 80% agreement on our position? Uses `currentAgree`, `currentProposers`, and `proposing` (counting self). If yes → `ConsensusState::Yes`. + +**Second call:** Have 80% of peers already *finished* this round (moved on to the next ledger), even though we haven't agreed with them? Uses `currentFinished` with `count_self = false`. If yes → `ConsensusState::MovedOn`. This is a distinct outcome from `Yes` — the local node recognizes it has lost the race and must follow the network, not that it genuinely reached agreement. + +Before either check, two time-based guards apply: a hard minimum duration floor of `ledgerMIN_CONSENSUS` (1950ms), and a laggard-protection rule that requires extra time when fewer than 75% of the previous round's proposers are currently visible. If neither consensus check passes, the function checks for abandonment using a dynamic timeout of `previousAgreeTime × ledgerABANDON_CONSENSUS_FACTOR` (factor = 10), clamped between `ledgerMAX_CONSENSUS` (15s) and `ledgerABANDON_CONSENSUS` (120s), returning `ConsensusState::Expired` if exceeded. + +## Logging + +Every function accepts both a `beast::Journal j` and a `std::unique_ptr const& clog`. The `CLOG(clog)` macro appends to the stringstream only when it is non-null, while `JLOG` writes to the structured journal. This dual-path design avoids string formatting cost on the hot path while enabling full consensus round traces when the caller provides a log buffer for debugging. Passing null simply skips the append with no risk of dereference. + +## Relationship to Other Files + +`ConsensusParms.h` owns all numeric thresholds. `ConsensusTypes.h` defines `ConsensusState` and related enums. The large `Consensus` template in `Consensus.h` calls both `shouldCloseLedger()` during the `open` phase and `checkConsensus()` during the `establish` phase. Keeping this policy logic in a `.cpp` file as free functions — rather than private methods of the template — makes them independently testable and keeps the template header focused on state management. \ No newline at end of file diff --git a/src/xrpld/consensus/Consensus.h.ai.json b/src/xrpld/consensus/Consensus.h.ai.json new file mode 100644 index 0000000000..ce5a300f58 --- /dev/null +++ b/src/xrpld/consensus/Consensus.h.ai.json @@ -0,0 +1,155 @@ +{ + "args": [ + { + "lineno": 20, + "name": "anyTransactions" + }, + { + "lineno": 21, + "name": "prevProposers" + }, + { + "lineno": 22, + "name": "proposersClosed" + }, + { + "lineno": 23, + "name": "proposersValidated" + }, + { + "lineno": 24, + "name": "prevRoundTime" + }, + { + "lineno": 25, + "name": "timeSincePrevClose" + }, + { + "lineno": 26, + "name": "openTime" + }, + { + "lineno": 27, + "name": "idleInterval" + }, + { + "lineno": 28, + "name": "parms" + }, + { + "lineno": 29, + "name": "j" + }, + { + "lineno": 30, + "name": "clog" + }, + { + "lineno": 46, + "name": "currentProposers" + }, + { + "lineno": 47, + "name": "currentAgree" + }, + { + "lineno": 48, + "name": "currentFinished" + }, + { + "lineno": 49, + "name": "previousAgreeTime" + }, + { + "lineno": 50, + "name": "currentAgreeTime" + }, + { + "lineno": 51, + "name": "stalled" + }, + { + "lineno": 53, + "name": "proposing" + }, + { + "lineno": 564, + "name": "participants" + }, + { + "lineno": 565, + "name": "percent" + } + ], + "classes": [ + { + "args": [ + "clock_type const& clock", + "Adaptor& adaptor", + "beast::Journal j" + ], + "lineno": 153, + "name": "Consensus" + }, + { + "args": [ + "ConsensusMode m" + ], + "lineno": 167, + "name": "Consensus::MonitoredMode" + } + ], + "description": "This file implements the core logic for the XRPL consensus algorithm, including the Consensus class template, which manages the consensus process for ledgers, transaction sets, and peer proposals. It provides mechanisms for starting consensus rounds, handling peer proposals, progressing through consensus phases, managing disputes, and determining when consensus is reached. The file also includes utility functions for consensus thresholds and ledger closing decisions.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/consensus/Consensus.h", + "functions": [ + { + "args": [ + "anyTransactions", + "prevProposers", + "proposersClosed", + "proposersValidated", + "prevRoundTime", + "timeSincePrevClose", + "openTime", + "idleInterval", + "parms", + "j", + "clog" + ], + "lineno": 19, + "name": "shouldCloseLedger" + }, + { + "args": [ + "prevProposers", + "currentProposers", + "currentAgree", + "currentFinished", + "previousAgreeTime", + "currentAgreeTime", + "stalled", + "parms", + "proposing", + "j", + "clog" + ], + "lineno": 44, + "name": "checkConsensus" + }, + { + "args": [ + "participants", + "percent" + ], + "lineno": 563, + "name": "participantsNeeded" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/consensus/Consensus.h.ai.md b/src/xrpld/consensus/Consensus.h.ai.md new file mode 100644 index 0000000000..0411042560 --- /dev/null +++ b/src/xrpld/consensus/Consensus.h.ai.md @@ -0,0 +1,55 @@ +# `Consensus.h` — XRPL Consensus Algorithm Core + +This file contains the entire implementation of the XRPL consensus algorithm. It defines the `Consensus` class template, two free-standing decision functions (`shouldCloseLedger`, `checkConsensus`), and a threshold helper (`participantsNeeded`). Because the class is fully templated and header-only, the implementation lives entirely here. + +## Role in the System + +The XRPL consensus protocol must agree on two things each round: which transactions to include in the next ledger, and when that ledger closes. `Consensus.h` encodes the full state machine and decision logic that drives a single node through that process, while delegating all application-specific concerns — networking, ledger storage, signature validation — to an `Adaptor` template parameter. This is a textbook policy-based design: the core algorithm is isolated and testable without a running network, and the same code drives both the production `RCLConsensus` and unit-test stubs. + +## The Adaptor Contract + +The `Adaptor` class provides four required type aliases (`Ledger_t`, `TxSet_t`, `NodeID_t`, `PeerPosition_t`) plus a collection of callbacks and queries. Critical callbacks include `onClose()` (called when the open ledger closes, producing the initial `ConsensusResult`), `onAccept()` (called when the round concludes successfully), and `onForceAccept()` (standalone/simulate path). Information queries like `proposersValidated()`, `proposersFinished()`, and `getPrevLedger()` feed the timing and correctness checks. Networking hooks — `propose()`, `share()` (three overloads for position, tx set, and individual transaction) — let the algorithm remain agnostic about transport. + +## Phase State Machine + +Each round transitions through three phases declared in `ConsensusPhase`: `open → establish → accepted`. The machine is driven externally via periodic `timerEntry()` calls; the `Consensus` object itself has no thread or timer. + +**Open phase (`phaseOpen`):** The ledger sits open accumulating transactions. On each timer call, `shouldCloseLedger()` decides whether to close based on how many peers have already proposed, whether there are pending transactions, and elapsed time. The close-time reference is subtle: if the previous close wasn't fully agreed upon (mode is `wrongLedger`, or peers disagreed on close time), the node falls back to `prevCloseTime_`, an internally tracked timestamp, rather than the ledger's recorded close time. This guards against propagating bad close-time estimates. + +**Establish phase (`phaseEstablish`):** Once `closeLedger()` fires, the node calls `adaptor_.onClose()` to freeze the open ledger into a `ConsensusResult`, broadcasts its transaction set, and, if proposing, announces its position. From here, every timer tick calls `updateOurPositions()` then checks `shouldPause()` and `haveConsensus()`. The minimum guard `ledgerMIN_CONSENSUS` (1950 ms) is enforced before any position updates begin, ensuring every node has a chance to cast an initial vote. + +**Accepted phase:** Once both transaction-set and close-time consensus are reached, `phaseEstablish` sets `phase_` to `accepted` and calls `adaptor_.onAccept()`. The `Consensus` object goes quiet until the next `startRound()` call. + +## Avalanche Convergence + +XRPL consensus uses an avalanche algorithm to converge on a common transaction set. `ConsensusParms` encodes four states (`init → mid → late → stuck`) with escalating yes-vote requirements: 50%, 65%, 70%, 95%. The state advances as `convergePercent_` — the ratio of current round time to previous round time — crosses the thresholds (50%, 85%, 200% of previous round). This dynamic threshold design matters: early in a round, inclusion of a transaction needs only a bare majority, which makes it easy for new legitimate transactions to enter consensus. As time passes and the round should be converging, the required agreement rises, forcing outlier nodes to adopt the majority position. + +`updateOurPositions()` drives this per-tick. It prunes stale peer proposals (older than `proposeFRESHNESS`), updates each `DisputedTx` vote via `dispute.updateVote(convergePercent_, ...)`, and rebuilds a `MutableTxSet` if any vote flipped. If our position changes, the new set is shared with peers and re-proposed. + +## Dispute Tracking + +`createDisputes()` is called whenever a new peer TxSet arrives that differs from our own. It calls `TxSet::compare()` to find differing transactions and creates a `DisputedTx` object for each. Each `DisputedTx` is initialized with our vote (do we have this tx?) and then immediately populated with every known peer's vote. The `result_->compares` set ensures we only compare each pair of sets once, avoiding quadratic work. + +`updateDisputes()` is the incremental path: it registers one peer's vote across all existing disputes. Crucially, any time a peer's vote changes, `peerUnchangedCounter_` is reset to zero. This counter feeds the staleness detection in `haveConsensus()`: if no peer has changed any vote for `avSTALLED_ROUNDS` consecutive timer rounds and close-time consensus is already achieved, the round is declared *stalled*, triggering consensus to proceed without the normal 80% threshold. + +## Wrong-Ledger Recovery + +At the start of every `timerEntry()` call, `checkLedger()` asks the adaptor for the network's preferred previous ledger via `getPrevLedger()`. If this diverges from `prevLedgerID_`, `handleWrongLedger()` fires. The node immediately calls `leaveConsensus()` — broadcasting a bow-out proposal and dropping to `observing` mode — then clears peer state and calls `playbackProposals()` to replay any buffered peer positions for the new ledger ID. If the correct ledger can be acquired, `startRoundInternal()` restarts in `switchedLedger` mode; otherwise the node enters `wrongLedger` mode and waits. + +The `recentPeerPositions_` map (capped at 10 proposals per peer) is the buffer that makes `playbackProposals()` work. It stores proposals regardless of which ledger they reference, so when the node switches context it can retroactively process messages it received while on the wrong ledger. This is a deliberate trade-off: a small bounded buffer beats dropping proposals on ledger switches. + +## `MonitoredMode` Inner Class + +`MonitoredMode` wraps the `ConsensusMode` enum and overrides `set()` to automatically call `adaptor_.onModeChange(before, after)` on every transition. This design ensures the adaptor's state (e.g., logging, metrics, operating mode decisions) is always in sync with the consensus mode. There is no way to change the mode and silently skip the notification. + +## `shouldPause()` — Laggard Waiting + +When a validator's own latest locally-validated ledger is behind the network's validated ledger, the algorithm may pause to wait for lagging validators before finalizing consensus. The pause decision uses a 5-phase cycle (0–4), where each phase requires a higher fraction of known-current validators. Phase 0 requires only enough non-laggards to satisfy quorum; phase 4 requires all validators. Intermediate phases interpolate linearly between those bounds. The phase cycles with `(ahead - 1) % 5`, so if being ahead persists, the requirement escalates then resets — encoding the judgment that no single threshold is universally better and that persistent divergence suggests a systemic problem beyond the scope of this mechanism. + +## Free-Standing Functions + +`shouldCloseLedger()` is not a method because it is independently testable and has no dependence on Consensus object state. It accepts raw observations (peer counts, elapsed times) and returns a boolean. Similarly, `checkConsensus()` determines the `ConsensusState` from vote counts and timing without accessing the object. `participantsNeeded(participants, percent)` rounds `participants * percent / 100` to the nearest integer (with a minimum of 1), used throughout for threshold calculations. + +## Thread Safety and the `clog` Pattern + +The class explicitly disclaims thread safety and defers synchronization to the caller. A second diagnostic pattern appears throughout: most public and private methods accept `std::unique_ptr const& clog = {}`. When non-null, the `CLOG` macro appends detailed step-by-step state to this log object, giving callers access to a complete narrative of a single consensus round's decisions — distinct from the journal's debug output, and suitable for post-hoc audit of a specific round. \ No newline at end of file diff --git a/src/xrpld/consensus/ConsensusParms.h.ai.json b/src/xrpld/consensus/ConsensusParms.h.ai.json new file mode 100644 index 0000000000..b424590887 --- /dev/null +++ b/src/xrpld/consensus/ConsensusParms.h.ai.json @@ -0,0 +1,58 @@ +{ + "args": [ + { + "lineno": 98, + "name": "p" + }, + { + "lineno": 99, + "name": "currentState" + }, + { + "lineno": 100, + "name": "percentTime" + }, + { + "lineno": 101, + "name": "currentRounds" + }, + { + "lineno": 102, + "name": "minimumRounds" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "ConsensusParms" + }, + { + "args": [], + "lineno": 77, + "name": "AvalancheCutoff" + } + ], + "description": "Defines the ConsensusParms struct containing parameters and tuning constants for the XRPL consensus algorithm, including timing, thresholds, and avalanche state logic. Also provides a helper function to determine consensus weight requirements based on state and timing.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/consensus/ConsensusParms.h", + "functions": [ + { + "args": [ + "p", + "currentState", + "percentTime", + "currentRounds", + "minimumRounds" + ], + "lineno": 97, + "name": "getNeededWeight" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/consensus/ConsensusParms.h.ai.md b/src/xrpld/consensus/ConsensusParms.h.ai.md new file mode 100644 index 0000000000..165ad5e38d --- /dev/null +++ b/src/xrpld/consensus/ConsensusParms.h.ai.md @@ -0,0 +1,50 @@ +# `ConsensusParms.h` — Consensus Algorithm Tuning Constants + +This header is the single source of truth for every numeric constant that governs the XRP Ledger's consensus algorithm. It lives at the boundary between the abstract consensus engine (`Consensus.h`) and the concrete protocol rules, collecting in one place all the timing windows, percentage thresholds, and state-machine cutoffs that the engine queries on every tick. Nothing here is meant to be altered at runtime — every member is `const`, making the struct effectively a named-constant bundle. + +## Two Temporal Domains + +The struct explicitly comments on a dual-clock architecture. Validation and proposal parameters (`validationVALID_WALL`, `validationVALID_LOCAL`, `validationVALID_EARLY`, `proposeFRESHNESS`, `proposeINTERVAL`) operate in **NetClock time** at second resolution, because these values are compared against ledger close timestamps that travel over the network and must be meaningful across machines. The remaining consensus-loop timers operate in **millisecond resolution** against an internal monotonic clock, because the engine needs sub-second granularity to decide when to advance its own state. Mixing these up would produce subtle bugs; the comment acts as a firewall reminding callers which domain each constant belongs to. + +## Validation and Proposal Windows + +`validationVALID_WALL` (5 minutes) and `validationVALID_LOCAL` (3 minutes) serve complementary defensive purposes. The wall-time window protects against very old validations referencing stale ledger close times; the local-observation window handles the rare case where the network produces an unusually small number of validations, allowing faster recovery by keeping freshly-seen validations relevant a bit longer. `validationVALID_EARLY` (3 minutes) is the mirror guard on the other side — it rejects validations timestamped suspiciously far in the past, providing a buffer against extreme clock skew. + +`proposeFRESHNESS` (20 seconds) is the staleness threshold for peer proposals; proposals older than this are discarded. `proposeINTERVAL` (12 seconds) forces a node to re-broadcast its own position before the freshness window expires, guaranteeing that the network always sees the node as active even when the node's position hasn't changed. + +## Consensus Timing + +`ledgerIDLE_INTERVAL` (15 s) is the maximum time a ledger may remain open with no activity before being forcibly closed. `ledgerMIN_CLOSE` (2 s) ensures all nodes have a moment to compute the last-closed ledger before the next round begins. `ledgerGRANULARITY` (1 s) is the tick interval — how often `phaseEstablish` is called and progress is evaluated. The test suite treats it as a unit of simulated time, connecting peers at fractions of `ledgerGRANULARITY`. + +`ledgerMIN_CONSENSUS` (1950 ms) and `ledgerMAX_CONSENSUS` (15 s) bound the normal consensus window. The comment on `ledgerMAX_CONSENSUS` is deliberate: this cap must stay comfortably below `validationFRESHNESS` so that a validator waiting for laggards is not mistaken for an offline node by its peers. `ledgerABANDON_CONSENSUS` (120 s) is the absolute timeout; after this the engine drops the round. The companion `ledgerABANDON_CONSENSUS_FACTOR` (10) participates in a guard in `Consensus.h` that prevents abandoning a round too early when individual `phaseEstablish` calls are themselves unusually slow. + +## The Avalanche State Machine + +The most architecturally interesting part of the file is the four-state avalanche machine that governs transaction-vote convergence. + +``` +init → mid → late → stuck (loops) +``` + +`AvalancheState` is an unscoped enum with values `{init, mid, late, stuck}`. `AvalancheCutoff` is a tiny POD struct that bundles three facts about a state: `consensusTime` (the percentage of the previous round's duration that must elapse before this state activates), `consensusPct` (the minimum yes-vote fraction required to include a transaction while in this state), and `next` (the successor state). These are collected into `avalancheCutoffs`, a `std::map`: + +| State | Time threshold | Required yes-vote % | Next state | +|--------|---------------|---------------------|------------| +| `init` | 0% | 50% | `mid` | +| `mid` | 50% | 65% | `late` | +| `late` | 85% | 70% | `stuck` | +| `stuck`| 200% | 95% | `stuck` | + +The ratchet is asymmetric by design: once a node has been in consensus twice as long as the prior round without converging, the 95 % `stuck` threshold makes it extraordinarily unlikely that any new transaction will be added to the position, forcing the dispute to resolve by attrition. The `stuck` state loops back to itself because there is nowhere else to go — the comment "once we're stuck, we're stuck" is a protocol guarantee that the threshold never relaxes. + +Using a `std::map` rather than a `switch` statement is itself a design choice: it allows state traversal to be data-driven and supports hypothetical looping state machines where a later state transitions back to an earlier one. The map is constructed by hand from compile-time literals, so `at()` calls on it are documented as safe despite the theoretical throw. + +## `getNeededWeight()` + +This free function is the sole interface through which the consensus engine reads the avalanche map. It takes the current state, the elapsed time expressed as a percentage of the previous round, a round counter, and a minimum-rounds guard. It returns a pair: the `consensusPct` in effect right now, and an `optional` indicating whether the caller should advance to the next state (the caller is responsible for the actual state update). The optional is `nullopt` when no transition occurs, giving callers a clean check: `if (newState) avalancheState_ = *newState`. + +The `minimumRounds` parameter prevents premature escalation: even if the time percentage is high enough to warrant `mid`, the node won't advance until it has spent at least `avMIN_ROUNDS` rounds in the current state. This guards against clock jitter pushing a fast node through all four states before slower peers have had a chance to vote. + +`DisputedTx::updateVote()` calls `getNeededWeight()` per-transaction, threading `convergePercent_` (computed in `phaseEstablish` as elapsed ms divided by the max of `prevRoundTime_` and `avMIN_CONSENSUS_TIME`) through each dispute. The close-time consensus path in `Consensus.h` calls it separately with round counts forced to zero because close-time convergence does not track discrete voting rounds. + +`avCT_CONSENSUS_PCT` (75 %) is the threshold used exclusively for close-time consensus — separate from the transaction-vote avalanche thresholds because close-time agreement is a simpler majority question rather than a multi-round ratchet. `avSTALLED_ROUNDS` (4) is the round count after which a vote that hasn't moved is declared stalled in `DisputedTx::stalled()`, allowing the engine to detect deadlocked disputes rather than spinning indefinitely. \ No newline at end of file diff --git a/src/xrpld/consensus/ConsensusProposal.h.ai.json b/src/xrpld/consensus/ConsensusProposal.h.ai.json new file mode 100644 index 0000000000..2980cf760e --- /dev/null +++ b/src/xrpld/consensus/ConsensusProposal.h.ai.json @@ -0,0 +1,167 @@ +{ + "args": [ + { + "lineno": 38, + "name": "prevLedger" + }, + { + "lineno": 39, + "name": "seq" + }, + { + "lineno": 40, + "name": "position" + }, + { + "lineno": 41, + "name": "closeTime" + }, + { + "lineno": 42, + "name": "now" + }, + { + "lineno": 43, + "name": "nodeID" + }, + { + "lineno": 106, + "name": "newPosition" + }, + { + "lineno": 107, + "name": "newCloseTime" + }, + { + "lineno": 108, + "name": "now" + }, + { + "lineno": 122, + "name": "now" + }, + { + "lineno": 98, + "name": "cutoff" + }, + { + "lineno": 169, + "name": "a" + }, + { + "lineno": 170, + "name": "b" + } + ], + "classes": [ + { + "args": [ + "prevLedger", + "seq", + "position", + "closeTime", + "now", + "nodeID" + ], + "lineno": 22, + "name": "ConsensusProposal" + } + ], + "description": "Defines the ConsensusProposal template class, representing a proposed position taken by a node during a round of consensus in the XRPL protocol. It encapsulates proposal metadata, state transitions, and provides utility methods for rendering, JSON output, and signing.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/consensus/ConsensusProposal.h", + "functions": [ + { + "args": [], + "lineno": 49, + "name": "nodeID" + }, + { + "args": [], + "lineno": 54, + "name": "position" + }, + { + "args": [], + "lineno": 59, + "name": "prevLedger" + }, + { + "args": [], + "lineno": 68, + "name": "proposeSeq" + }, + { + "args": [], + "lineno": 75, + "name": "closeTime" + }, + { + "args": [], + "lineno": 80, + "name": "seenTime" + }, + { + "args": [], + "lineno": 86, + "name": "isInitial" + }, + { + "args": [], + "lineno": 92, + "name": "isBowOut" + }, + { + "args": [ + "cutoff" + ], + "lineno": 97, + "name": "isStale" + }, + { + "args": [ + "newPosition", + "newCloseTime", + "now" + ], + "lineno": 104, + "name": "changePosition" + }, + { + "args": [ + "now" + ], + "lineno": 120, + "name": "bowOut" + }, + { + "args": [], + "lineno": 127, + "name": "render" + }, + { + "args": [], + "lineno": 136, + "name": "getJson" + }, + { + "args": [], + "lineno": 151, + "name": "signingHash" + }, + { + "args": [ + "a", + "b" + ], + "lineno": 167, + "name": "operator==" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/consensus/ConsensusProposal.h.ai.md b/src/xrpld/consensus/ConsensusProposal.h.ai.md new file mode 100644 index 0000000000..fe1be86348 --- /dev/null +++ b/src/xrpld/consensus/ConsensusProposal.h.ai.md @@ -0,0 +1,42 @@ +# `ConsensusProposal.h` — Consensus Position Record + +## Role in the System + +`ConsensusProposal` is the fundamental unit of communication in the XRPL Byzantine Fault Tolerant consensus protocol. During each consensus round, every participating validator must broadcast its view of two things: which set of transactions should be included in the next ledger, and when that ledger should close. This class encapsulates both, together with the metadata needed to authenticate the proposal, track its progression, and decide when to ignore it. + +The class lives inside `src/xrpld/consensus/`, which houses the generic, adapter-pattern consensus engine. It is a header-only template, intentionally decoupled from concrete XRPL types. The real-world instantiation — `ConsensusProposal` — happens in `RCLCxPeerPos.h`, which wraps it with a cryptographic signature and public key for network propagation. + +## Template Design + +The three template parameters (`NodeID_t`, `LedgerID_t`, `Position_t`) mirror the broader pattern used throughout the consensus module, where `Consensus.h` defines `Proposal_t` as `ConsensusProposal`. This separation means the consensus logic is testable without pulling in the full XRPL type machinery: unit tests can instantiate `ConsensusProposal` over simple integer types while production code uses 256-bit hashes. The position type is the hash of a transaction set (`TxSet_t::ID`), not the set itself — proposals are compact identifiers, not the full data. + +## The Sequence Number Protocol + +The most architecturally significant design element is `proposeSeq_`, a monotonically increasing `uint32_t` used to order successive positions from the same peer within a single consensus round. Two sentinel values define lifecycle boundaries: + +- `seqJoin` (0): The very first proposal a peer broadcasts when it joins a round (`isInitial()` returns true). The `ConsensusTypes.h` `ConsensusCloseTimes` struct specifically collects these initial close time estimates to measure inter-peer clock drift. +- `seqLeave` (0xffffffff): Signals that a peer is voluntarily exiting consensus via `bowOut()`. This is distinct from simply going silent; it lets the rest of the network immediately subtract that peer from the denominator when counting agreement. + +`changePosition()` increments `proposeSeq_` each time a peer updates its stance, but explicitly guards against incrementing past `seqLeave`. This ensures a peer that has bowed out cannot accidentally re-enter by receiving an update call. + +In `Consensus.h`, the engine checks `isBowOut()` before forwarding peer positions to dispute resolution (line 766), and similarly gates whether to re-broadcast the local node's own position (line 1591). The sequence number is also included in the signing hash, so replaying an older proposal with a forged higher sequence is not possible without breaking the signature. + +## Lazy Signing Hash + +`signingHash()` computes a `sha512Half` over `HashPrefix::proposal`, the sequence number, the close time epoch count, the previous ledger ID, and the position hash. This five-field digest is what peers sign with their private key and what verifiers check via `RCLCxPeerPos::checkSign()`. + +The hash is stored as `mutable std::optional signingHash_` and is computed only on first access. Both `changePosition()` and `bowOut()` explicitly call `signingHash_.reset()` before mutating any fields, invalidating the cached value. This lazy pattern avoids recomputing the digest on construction — which would be wasted work for proposals received from peers that are immediately re-signed and forwarded — while guaranteeing correctness: any mutation path must clear the cache first, or the accessor would return a stale hash. + +## Staleness and the `time_` Field + +`seenTime()` reflects when the position was last updated (either constructed or mutated via `changePosition`/`bowOut`). `isStale(cutoff)` compares `time_ <= cutoff` to detect peers that have gone silent. In `Consensus.h`, two cutoffs are computed — one for peer proposals (line 1437) and one for the local node's own position (line 1555) — and proposals older than their respective cutoff are discarded or replaced. This prevents the consensus engine from permanently blocking on a dead peer's stale proposal. + +Notably, `time_` is the *wall-clock* time the proposal was seen locally, not the ledger close time. The ledger close time (`closeTime_`) is a separate field representing the proposing peer's estimate of when the ledger should close, using `NetClock` (the network's consensus clock). Conflating these two would be a subtle bug, so the naming distinction — `seenTime()` vs `closeTime()` — is deliberate. + +## Relationship to `ConsensusResult` + +`ConsensusTypes.h` defines `ConsensusResult`, which holds a `Proposal_t position` field representing the *local node's* current position throughout the round. This is the one mutable `ConsensusProposal` in the system; all peer proposals are immutable once received. The `Consensus.h` engine mutates `result_->position` exclusively through `changePosition()` and `bowOut()`, making the sequence number and cache invalidation contract straightforward to audit. + +## Equality and Debugging + +The free `operator==` compares all six fields including `seenTime()`, making two proposals equal only if they were seen at the same instant. This is appropriate for de-duplication (the hash router suppression in `RCLCxPeerPos` handles network-level dedup separately), but callers should be aware that two logically identical positions received at different times will not compare equal. `render()` and `getJson()` provide human-readable diagnostics; `getJson()` omits `transaction_hash` and `propose_seq` when the proposal is a bow-out, which avoids logging a meaningless `0xffffffff` into the JSON output. \ No newline at end of file diff --git a/src/xrpld/consensus/ConsensusTypes.h.ai.json b/src/xrpld/consensus/ConsensusTypes.h.ai.json new file mode 100644 index 0000000000..3569c4a696 --- /dev/null +++ b/src/xrpld/consensus/ConsensusTypes.h.ai.json @@ -0,0 +1,35 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 120, + "name": "ConsensusTimer" + } + ], + "description": "Defines types, enums, and utility classes/structs for representing and managing consensus participation, phases, timing, close times, and results in the XRPL consensus process.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/consensus/ConsensusTypes.h", + "functions": [ + { + "args": [ + "ConsensusMode m" + ], + "lineno": 49, + "name": "to_string" + }, + { + "args": [ + "ConsensusPhase p" + ], + "lineno": 97, + "name": "to_string" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/consensus/ConsensusTypes.h.ai.md b/src/xrpld/consensus/ConsensusTypes.h.ai.md new file mode 100644 index 0000000000..9195e5c870 --- /dev/null +++ b/src/xrpld/consensus/ConsensusTypes.h.ai.md @@ -0,0 +1,71 @@ +# `ConsensusTypes.h` — Core Vocabulary for the XRPL Consensus Algorithm + +This header is the shared type vocabulary for the entire XRPL consensus subsystem. It defines no algorithm logic — that lives in `Consensus.h` — but every piece of consensus state machinery (`ConsensusMode`, `ConsensusPhase`, `ConsensusResult`, etc.) originates here and is consumed throughout the round lifecycle. Reading this file is effectively reading the state-machine skeleton of the consensus protocol. + +## `ConsensusMode` — How a Node Participates + +``` +proposing observing + \ / + \---> wrongLedger <---/ + ^ + | + v + switchedLedger +``` + +A node enters each round in one of two initial states: `proposing` (actively broadcasting its position) or `observing` (listening but silent). If at any point the node detects that it is building on the wrong prior ledger, it transitions to `wrongLedger` and tries to acquire the correct one. Once it successfully switches, it becomes `switchedLedger`. + +The critical design choice is that `switchedLedger` is a distinct mode rather than just resetting to `observing`. It carries the semantic history — "we caught up mid-round" — which `Consensus.h` uses when computing close times: code at the close-ledger transition explicitly guards against `wrongLedger` mode when deciding whether the previous ledger's close time is authoritative. `switchedLedger` behaves like `observing` in most voting logic, but the mode label is preserved throughout the round so diagnostics, JSON snapshots, and close-time calculations can account for the recovery. + +The `MonitoredMode` helper inside `Consensus.h` wraps `ConsensusMode` to ensure `Adaptor::onModeChange()` is always called when the mode transitions, making silent state changes structurally impossible. + +## `ConsensusPhase` — Where a Round Is Right Now + +``` + "close" "accept" + open ------- > establish ---------> accepted + ^ | | + |---------------| | + ^ "startRound" | + |------------------------------------| +``` + +Three phases govern the coarse structure of a single ledger round. The `open` phase is the accumulation window: transactions arrive but no position has been declared. `establish` begins when the node closes its open ledger and starts exchanging proposals. `accepted` is the quiescent state where the ledger has been committed and the node waits for `startRound` to kick off the next cycle. + +The unusual path — going back to `open` mid-`establish` — happens inside `Consensus::handleWrongLedger`. Rather than aborting and starting fresh from outside, the consensus engine internally re-enters the open phase on the correct ledger, preserving the surrounding state. `ConsensusPhase` guards almost every major entry point in `Consensus.h`: `timerEntry`, `gotTxSet`, and `peerProposal` all short-circuit immediately if the phase is `accepted`, preventing stale work from contaminating the next round. + +## `ConsensusTimer` — Dual-Mode Elapsed Time + +`ConsensusTimer` is a small class with two distinct `tick()` overloads. The wall-clock overload computes `duration_cast(tp - start_)` from a `steady_clock::time_point`, giving real elapsed time. The fixed-increment overload accumulates a caller-provided `milliseconds` delta, enabling deterministic simulation in unit tests without mocking the system clock. Both methods update the same `dur_` field, so `read()` is always valid regardless of which variant was used. This is the timing substrate for `ConsensusResult::roundTime`, which `Consensus.h` uses to record how long the `establish` phase took and feed into the `prevRoundTime_` heuristic for the next round's timeout. + +## `ConsensusCloseTimes` — Distributed Clock Drift Tracking + +Each peer's initial consensus proposal includes that peer's view of when the ledger closed. `ConsensusCloseTimes` collects those views: `peers` is a `std::map` (a histogram of proposed close times), and `self` holds the local node's own estimate. The choice of `std::map` over `hash_map` is deliberate — the comment says "keep ordered for predictable traverse." During close-time consensus resolution, the engine iterates this map in ascending time order to find the most agreed-upon close time bucket, so deterministic ordering matters for reproducibility across nodes. `rawCloseTimes_` in `Consensus.h` is the live instance populated during the `establish` phase. + +## `ConsensusState` — The Outcome of Calling `checkConsensus` + +`ConsensusState` is a four-value enum that captures every meaningful outcome from the `checkConsensus` free function: + +- `No` — insufficient agreement has been reached yet. +- `Yes` — the local node and the network agree on the transaction set. +- `MovedOn` — enough of the network has finished the round that the local node should accept whatever the network decided, even if it didn't directly achieve agreement. This handles slow nodes that fall behind. +- `Expired` — the consensus time limit has hard-expired, forcing acceptance to prevent indefinite stalls. + +The distinction between `MovedOn` and `Expired` matters operationally: `MovedOn` is normal network dynamics; `Expired` is a safety valve triggered only when the round has gone on far too long, potentially with stalled disputed transactions. Both cause `phase_` to transition to `accepted`, but they are logged and recorded differently in `ConsensusResult::state`. + +## `ConsensusResult` — The Aggregated Round Outcome + +`ConsensusResult` is a policy-parameterized struct that bundles everything needed to finalize and hand off a consensus round. It is instantiated once per round during `closeLedger` and lives in `Consensus::result_` as a `std::optional`. + +Its constructor enforces a hard invariant via `XRPL_ASSERT`: + +```cpp +XRPL_ASSERT(txns.id() == position.position(), "xrpl::ConsensusResult : valid inputs"); +``` + +The transaction set's ID must match the proposal's stated position. This is the core consistency guarantee: the node's declared position is always a commitment to a specific transaction set, not an approximate label. Any construction path that violates this cannot proceed. + +The `disputes` map holds the live `DisputedTx` objects — only transactions where peers disagree. The `compares` set is a work-avoidance cache: when the engine processes a new peer transaction set, it checks `compares` first to avoid recomputing disputes for sets already processed. This is especially important during `establish`, where multiple peers may share the same transaction set ID. The `proposers` field snapshots how many peers participated, feeding into the next round's threshold calculations via `prevProposers_`. + +Together, these types form a tight vocabulary: `ConsensusPhase` and `ConsensusMode` describe where the engine is and how it participates; `ConsensusTimer` and `ConsensusCloseTimes` track timing evidence; `ConsensusState` records what the engine decided; and `ConsensusResult` packages the conclusions for handoff to the application layer via `Adaptor::onAccept`. \ No newline at end of file diff --git a/src/xrpld/consensus/DisputedTx.h.ai.json b/src/xrpld/consensus/DisputedTx.h.ai.json new file mode 100644 index 0000000000..0d2d5f564e --- /dev/null +++ b/src/xrpld/consensus/DisputedTx.h.ai.json @@ -0,0 +1,133 @@ +{ + "args": [ + { + "lineno": 27, + "name": "tx" + }, + { + "lineno": 27, + "name": "ourVote" + }, + { + "lineno": 27, + "name": "numPeers" + }, + { + "lineno": 27, + "name": "j" + }, + { + "lineno": 102, + "name": "o" + }, + { + "lineno": 113, + "name": "peer" + }, + { + "lineno": 113, + "name": "votesYes" + }, + { + "lineno": 124, + "name": "peer" + }, + { + "lineno": 135, + "name": "percentTime" + }, + { + "lineno": 135, + "name": "proposing" + }, + { + "lineno": 135, + "name": "p" + } + ], + "classes": [ + { + "args": [ + "tx", + "ourVote", + "numPeers", + "j" + ], + "lineno": 18, + "name": "DisputedTx" + } + ], + "description": "Defines the DisputedTx template class, which represents a transaction under dispute during the XRPL consensus process, tracking peer votes and managing voting logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/consensus/DisputedTx.h", + "functions": [ + { + "args": [], + "lineno": 32, + "name": "ID" + }, + { + "args": [], + "lineno": 38, + "name": "getOurVote" + }, + { + "args": [ + "p", + "proposing", + "peersUnchanged", + "j", + "clog" + ], + "lineno": 43, + "name": "stalled" + }, + { + "args": [], + "lineno": 97, + "name": "tx" + }, + { + "args": [ + "o" + ], + "lineno": 102, + "name": "setOurVote" + }, + { + "args": [ + "peer", + "votesYes" + ], + "lineno": 113, + "name": "setVote" + }, + { + "args": [ + "peer" + ], + "lineno": 124, + "name": "unVote" + }, + { + "args": [ + "percentTime", + "proposing", + "p" + ], + "lineno": 135, + "name": "updateVote" + }, + { + "args": [], + "lineno": 143, + "name": "getJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/consensus/DisputedTx.h.ai.md b/src/xrpld/consensus/DisputedTx.h.ai.md new file mode 100644 index 0000000000..59969fa234 --- /dev/null +++ b/src/xrpld/consensus/DisputedTx.h.ai.md @@ -0,0 +1,60 @@ +# `DisputedTx.h` — Per-Transaction Dispute Tracking During XRPL Consensus + +## Role in the System + +XRPL's consensus protocol works by having validators iteratively converge on a shared transaction set. When two validators propose different sets, the transactions that appear in one set but not the other become *disputed*. `DisputedTx` is the object that manages one such dispute: it records every peer's yes/no vote on a single transaction and drives the local node's own vote toward consensus over time. + +Critically, there is no `DisputedTx` object for transactions that all validators agree on — only for the transactions that genuinely differ between at least two observed positions. Objects are created inside `Consensus::createDisputes()` when the engine compares its own transaction set to a peer's, and they live inside `ConsensusResult::disputes` (a `hash_map`) for exactly the duration of the establish phase. + +## Vote Storage + +Peer votes are kept in a `boost::container::flat_map`. The flat map is a sorted, contiguous-memory structure that outperforms `std::map` for small-to-medium collections due to cache locality. Capacity is reserved up-front in the constructor using `numPeers` — the number of currently-connected validators — to avoid rehashing during the burst of incoming `setVote` calls that follows dispute creation. + +`setVote()` maintains two independent integer counters, `yays_` and `nays_`, so that percentage computations in `updateVote()` never need to scan the full map. The method returns `true` on any *change* (including a brand-new vote), which allows the caller in `Consensus::phaseEstablish()` to track whether any peer has moved during a given round via `peerUnchangedCounter_`. `unVote()` is called when a peer disconnects or its position is superseded, keeping the counters accurate. + +## The Avalanche Voting State Machine + +The most subtle part of the class is how `updateVote()` decides to flip the local node's vote. XRPL's consensus uses a strategy borrowed from *avalanche* protocols: the required percentage of "yes" votes needed to adopt a transaction rises as the consensus round grows longer. This escalating threshold prevents a small minority of lagging validators from indefinitely blocking convergence, while still giving genuine disagreements time to resolve. + +`ConsensusParms` defines four states with their thresholds: + +| State | Enters after (% of prior round) | Required "yes" | Next state | +|---------|----------------------------------|----------------|------------| +| `init` | 0% (always) | 50% | `mid` | +| `mid` | 50% | 65% | `late` | +| `late` | 85% | 70% | `stuck` | +| `stuck` | 200% | 95% | `stuck` | + +`updateVote()` calls `getNeededWeight()` (defined in `ConsensusParms.h`) on every invocation, passing the current `avalancheState_` and a counter `avalancheCounter_` that increments with each call. A state transition only occurs when both the time threshold is met *and* the state has been active for at least `avMIN_ROUNDS` rounds, preventing rapid-fire advancement through states. When a transition fires, `avalancheCounter_` resets to zero, enforcing a minimum dwell time in each state. + +The `stuck` state is deliberately self-looping — once `percentTime` exceeds 200% of the prior round, the required threshold jumps to 95% and stays there. At that point it is almost certain that network topology or a genuinely controversial transaction is preventing agreement; the near-unanimity requirement forces the transaction to be either widely accepted or definitively dropped. + +## Proposing vs. Non-Proposing Behavior + +`updateVote()` receives a `proposing` flag that fundamentally changes how votes are counted. When the local node is actively proposing: + +``` +weight = (yays_ * 100 + (ourVote_ ? 100 : 0)) / (nays_ + yays_ + 1) +newPosition = weight > requiredPct +``` + +The node counts its own vote as one more unit alongside all peers, and the integer percentage must exceed the current avalanche threshold. + +When not proposing (i.e., the node is in "observer" mode), the logic simplifies to `newPosition = yays_ > nays_` and `weight = -1`. The observer never drives the threshold logic — it simply follows majority direction. This asymmetry prevents non-proposing nodes from inadvertently distorting the weighted vote that proposing nodes rely on. + +## Stall Detection + +`stalled()` is a diagnostic predicate called by `Consensus::checkConsensus()` once close-time consensus has been established but disputed transactions remain. A transaction is declared stalled when all of the following hold: + +1. The avalanche state machine has reached a terminal condition — `nextCutoff.consensusTime <= currentCutoff.consensusTime`, i.e., we're in the `stuck` loop. +2. At least `avMIN_ROUNDS` rounds have elapsed in the current state. +3. Either `peersUnchanged >= avSTALLED_ROUNDS` (peers haven't shifted) *or* `currentVoteCounter_ >= avSTALLED_ROUNDS` (our own vote hasn't moved). Using *or* rather than *and* is deliberate: a malicious peer that flip-flops its vote to reset the peer counter cannot indefinitely prevent the stall declaration as long as our own vote has stabilised. +4. The current vote split exceeds `minCONSENSUS_PCT` (80%) in either direction, i.e., there is lopsided agreement. + +A stall is flagged as an error in the journal because consensus stalling on even a single transaction is an abnormal event that warrants investigation. + +## Design Choices + +The class is a header-only template rather than a concrete class tied to XRPL's transaction and node-ID types. This mirrors the broader `Consensus` design where the algorithm itself is decoupled from the ledger implementation, making the consensus layer independently testable against simulated transaction types. `Tx_t::ID` is extracted as `TxID_t` via a nested typedef, mirroring the convention used throughout `ConsensusTypes.h`. + +`currentVoteCounter_` — the number of consecutive rounds without a vote flip — feeds both `stalled()` (stall guard) and the log output in `updateVote()`. Resetting it to zero on every flip means it accurately measures the *current* streak, not a historical maximum, which is the right signal for both purposes. \ No newline at end of file diff --git a/src/xrpld/consensus/LedgerTrie.h.ai.json b/src/xrpld/consensus/LedgerTrie.h.ai.json new file mode 100644 index 0000000000..b5916dbf79 --- /dev/null +++ b/src/xrpld/consensus/LedgerTrie.h.ai.json @@ -0,0 +1,226 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Seq s", + "ID i", + "Ledger const lgr" + ], + "lineno": 13, + "name": "SpanTip" + }, + { + "args": [], + "lineno": 36, + "name": "Span" + }, + { + "args": [], + "lineno": 133, + "name": "Node" + }, + { + "args": [], + "lineno": 200, + "name": "LedgerTrie" + } + ], + "description": "Implements a compressed trie data structure (LedgerTrie) for tracking ledger ancestry and validation support in the XRPL consensus process. Provides efficient insertion, removal, and querying of ledger support, as well as determining the preferred ledger for consensus.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/consensus/LedgerTrie.h", + "functions": [ + { + "args": [ + "s" + ], + "lineno": 28, + "name": "ancestor" + }, + { + "args": [], + "lineno": 61, + "name": "start" + }, + { + "args": [], + "lineno": 65, + "name": "end" + }, + { + "args": [ + "spot" + ], + "lineno": 69, + "name": "from" + }, + { + "args": [ + "spot" + ], + "lineno": 75, + "name": "before" + }, + { + "args": [], + "lineno": 81, + "name": "startID" + }, + { + "args": [ + "o" + ], + "lineno": 86, + "name": "diff" + }, + { + "args": [], + "lineno": 91, + "name": "tip" + }, + { + "args": [ + "val" + ], + "lineno": 101, + "name": "clamp" + }, + { + "args": [ + "from", + "to" + ], + "lineno": 106, + "name": "sub" + }, + { + "args": [ + "o", + "s" + ], + "lineno": 116, + "name": "operator<<" + }, + { + "args": [ + "a", + "b" + ], + "lineno": 122, + "name": "merge" + }, + { + "args": [ + "child" + ], + "lineno": 146, + "name": "erase" + }, + { + "args": [ + "o", + "s" + ], + "lineno": 157, + "name": "operator<<" + }, + { + "args": [], + "lineno": 161, + "name": "getJson" + }, + { + "args": [ + "ledger" + ], + "lineno": 217, + "name": "find" + }, + { + "args": [ + "ledger", + "parent" + ], + "lineno": 244, + "name": "findByLedgerID" + }, + { + "args": [ + "o", + "curr", + "offset" + ], + "lineno": 259, + "name": "dumpImpl" + }, + { + "args": [ + "ledger", + "count" + ], + "lineno": 277, + "name": "insert" + }, + { + "args": [ + "ledger", + "count" + ], + "lineno": 340, + "name": "remove" + }, + { + "args": [ + "ledger" + ], + "lineno": 399, + "name": "tipSupport" + }, + { + "args": [ + "ledger" + ], + "lineno": 412, + "name": "branchSupport" + }, + { + "args": [ + "largestIssued" + ], + "lineno": 441, + "name": "getPreferred" + }, + { + "args": [], + "lineno": 540, + "name": "empty" + }, + { + "args": [ + "o" + ], + "lineno": 546, + "name": "dump" + }, + { + "args": [], + "lineno": 552, + "name": "getJson" + }, + { + "args": [], + "lineno": 561, + "name": "checkInvariants" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 34, + "name": "ledger_trie_detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/consensus/LedgerTrie.h.ai.md b/src/xrpld/consensus/LedgerTrie.h.ai.md new file mode 100644 index 0000000000..dfdeaf0f8f --- /dev/null +++ b/src/xrpld/consensus/LedgerTrie.h.ai.md @@ -0,0 +1,71 @@ +# `LedgerTrie.h` — Compressed Ancestry Trie for Consensus Support Tracking + +## Purpose and Role + +`LedgerTrie.h` implements a compressed trie (prefix tree) data structure that tracks how much validator support each ledger in the XRPL network has received. It is the core data structure powering the "preferred ledger" calculation in XRPL's consensus algorithm, living inside the broader `Validations<>` template in `Validations.h`, which holds a `LedgerTrie trie_` member and feeds validator observations into it. + +The fundamental insight the trie exploits is that ledger history is a **string over the alphabet of ledger IDs**: a ledger at sequence `N` implicitly defines a length-`N` string where the `i`-th character is the ID of its ancestor at sequence `i`. Two ledgers that share ancestry share a common prefix of this string. A compressed trie over these strings groups validators onto shared ancestry branches efficiently, making it possible to ask "which branch of history has the most support right now?" in a single tree traversal rather than by scanning all validators. + +## Class Hierarchy + +### `SpanTip` + +A lightweight value type carrying the sequence number and ID of the tip of a span, plus a copy of the ledger itself so that ancestor IDs at earlier sequence numbers can be looked up via `ancestor(Seq s)`. This avoids repeatedly reaching into the ledger object from call sites that just need to identify a position in the trie. `SpanTip` is the return type of `getPreferred()` — the consensus engine receives it as the resolved preferred ledger. + +### `ledger_trie_detail::Span` + +A `Span` represents a contiguous half-open interval `[start_, end_)` of ledger sequence numbers, backed by a single `Ledger` instance from which ancestor IDs can be retrieved. The key operations are `diff()` — which delegates to the free function `mismatch(ledger_, other)` to find the first sequence where histories diverge — and the slicing operations `from(spot)` and `before(spot)` that return sub-spans. The `merge()` free friend function combines two overlapping spans by taking the endpoints from the one with the higher sequence number, since that ledger necessarily knows its own ancestry further back. + +This design cleanly separates "what ledger IDs exist at each position" (owned by the `Ledger` value inside the span) from "which range of positions this node covers" (the `[start_, end_)` pair). Spans themselves are cheap to copy because `Ledger` is documented to be lightweight. + +### `ledger_trie_detail::Node` + +A trie node owns a `Span`, two support counters, a `parent` raw pointer, and a `std::vector>` children list. The ownership model is strictly top-down: each node owns its children but holds only a raw non-owning pointer to its parent. + +`tipSupport` is the number of current validations whose exact ledger matches the tip of this node's span. `branchSupport` is `tipSupport` plus the sum of all descendants' `branchSupport` — it counts every validator that has validated this ledger or any of its descendants. These two counters are maintained incrementally on every insert and remove by walking up the parent chain. The `erase(child)` method uses a swap-and-pop idiom to remove a child in O(1). + +## `LedgerTrie` — The Trie Itself + +### Compression Invariant + +The trie maintains a strict structural invariant: **any non-root node with zero `tipSupport` must have at least two children**. A node with zero tip support and exactly one child is redundant — it can be merged with its child with no loss of information. Both `insert()` and `remove()` actively enforce this. + +The root node is exempt from this invariant and represents the genesis ledger; it always exists even when the trie is logically empty (checked via `root->branchSupport == 0`). + +### `insert(ledger, count)` + +Insertion first calls `find(ledger)`, which walks the trie to locate the node with the longest common prefix with the incoming ledger. The difference point `diffSeq` identifies where the incoming ledger diverges from the found node's span. The logic then handles up to two structural modifications: + +1. **Split** (`oldSuffix` exists): The found node `loc` shares only a prefix with the new ledger. The suffix of `loc`'s span is extracted into a new child node, which inherits `loc`'s existing children and support counts. `loc` is truncated to the prefix, with `tipSupport` set to 0. This is the classic compressed-trie prefix split. + +2. **Branch** (`newSuffix` exists): A new leaf node for the remainder of the incoming ledger is appended as a new child. + +After structural surgery, `tipSupport` is incremented on the target node and `branchSupport` is propagated up to the root via the parent chain. `seqSupport[ledger.seq()]` is also incremented — this per-sequence-number index is crucial for the preferred-ledger algorithm. + +### `remove(ledger, count)` + +Removal uses `findByLedgerID()` (an O(n) exact-match search) rather than the prefix-based `find()`, because tip support must be decremented from an exact ledger node, not just the best prefix match. After decrementing, `branchSupport` is decremented up the parent chain. The compression step then walks up from the removed node: leaf nodes with zero `tipSupport` are deleted, and nodes with zero `tipSupport` and exactly one child are merged with that child using `merge()`. + +### `getPreferred(largestIssued)` + +This is the most algorithmically significant method. It answers: *given the current distribution of validator support, which specific ledger should this node work toward?* + +The algorithm walks the trie from the root, at each step applying a **"preferred by branch"** rule. The key concept is **uncommitted support**: validators whose last issued validation is at a sequence number smaller than the current frontier haven't yet expressed a preference for the current generation of ledgers. Because they *might* validate any branch, they are potential support for competing branches and must be treated as a threat to the current best candidate. + +`seqSupport` is a `std::map` that counts how many validators have their tip at each sequence number. The algorithm iterates `seqSupport` entries in order, accumulating validators who are "behind" the current position into the `uncommitted` counter. A branch is considered safe to advance into only when its `branchSupport` exceeds `uncommitted` — i.e., even if every uncommitted validator piled onto any other branch, the current best branch would still win. + +Within a single node's span (where there is no branching), the algorithm advances sequence by sequence, checking each `seqSupport` entry. When advancing past the end of a node's span, it picks among children by `branchSupport`, with `startID()` as a deterministic tie-breaker. A child is chosen only if its `branchSupport` lead over the second-best child exceeds `uncommitted` (with the tie-breaker winner getting one extra margin point). If no child clears the bar, the current node's tip is returned as the preferred ledger. + +This conservative strategy — requiring a margin *strictly greater than* uncommitted validators — is intentional. It prevents thrashing between competing branches when validators are slow to report, a realistic condition in a distributed network. + +### `checkInvariants()` + +A debugging and testing method that performs a full DFS of the trie, verifying: (1) no non-root zero-tip-support node has fewer than two children; (2) every node's `branchSupport` equals its `tipSupport` plus the sum of its children's `branchSupport`; (3) parent pointers are correct; and (4) the `seqSupport` map matches the sum of `tipSupport` values grouped by sequence number. The consensus framework test suite calls this after every mutation. + +## Integration with `Validations` + +`Validations.h` uses `LedgerTrie` through a `withTrie()` accessor that flushes stale validations before delegating to a lambda. Validations are inserted via `updateTrie()` when a validator's ledger is acquired, and removed via `removeTrie()` when they expire. The `getPreferred()` call passes `localSeqEnforcer_.largest()` — the largest sequence number this node itself has validated — as the `largestIssued` anchor, ensuring that validations behind the local frontier count as uncommitted. `branchSupport() - tipSupport()` is also used directly to count validators who are descendants of a given ledger (i.e., have moved past it), used when computing child counts for consensus decisions. + +## Ledger Template Contract + +The `Ledger` type must be cheap to copy, provide `seq()`, support indexed ancestor lookup via `operator[](Seq)` returning `ID{0}` for unknowns, construct a genesis ledger via a tag type `MakeGenesis{}`, and participate in a free function `mismatch(Ledger, Ledger)` that returns the first sequence number where the two ledgers may differ. The **unique history invariant** — that agreement on any ancestor ID implies agreement on all earlier ancestors — is what makes the trie representation sound: a ledger history truly is a string in the formal sense, with no branching within a single ledger's ancestry chain. \ No newline at end of file diff --git a/src/xrpld/consensus/Validations.h.ai.json b/src/xrpld/consensus/Validations.h.ai.json new file mode 100644 index 0000000000..3a92874a67 --- /dev/null +++ b/src/xrpld/consensus/Validations.h.ai.json @@ -0,0 +1,54 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "ValidationParms" + }, + { + "args": [ + "Seq" + ], + "lineno": 56, + "name": "SeqEnforcer" + }, + { + "args": [ + "ValidationParms const& p", + "beast::abstract_clock& c", + "Ts&&... ts" + ], + "lineno": 137, + "name": "Validations" + } + ], + "description": "This file implements the core logic for managing and tracking ledger validations in the XRPL consensus process. It defines timing parameters for validation staleness, enforces validation sequence requirements, and provides a generic Validations class template for storing, querying, and updating validations from network nodes, including trusted/untrusted status, expiration, and trie-based ancestry tracking.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/consensus/Validations.h", + "functions": [ + { + "args": [ + "ValidationParms const& p", + "NetClock::time_point now", + "NetClock::time_point signTime", + "NetClock::time_point seenTime" + ], + "lineno": 87, + "name": "isCurrent" + }, + { + "args": [ + "ValStatus m" + ], + "lineno": 109, + "name": "to_string" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/consensus/Validations.h.ai.md b/src/xrpld/consensus/Validations.h.ai.md new file mode 100644 index 0000000000..d65cdaf54e --- /dev/null +++ b/src/xrpld/consensus/Validations.h.ai.md @@ -0,0 +1,65 @@ +# `Validations.h` — Ledger Validation Tracking for XRPL Consensus + +## Purpose and Role + +`Validations.h` implements the core data structure that tracks validator attestations as they arrive over the network during XRPL's consensus process. Every time a trusted validator finalizes a ledger, it broadcasts a signed `Validation` message. This header defines the machinery that receives those messages, decides whether each one is fresh and legitimate, indexes them multiple ways for efficient queries, and feeds them into a `LedgerTrie` to determine which ledger chain the network as a whole prefers. + +The file is entirely generic: the `Validations` class template is parameterized so that simulation environments and the production rippled application can share the same logic while substituting test clocks, storage backends, or mutex types. The production instantiation wires it to `RCLValidationsAdaptor` in `RCLValidations.h`, which wraps `STValidation` objects and real XRP Ledger instances. + +## `ValidationParms` — Protocol Timing Constants + +`ValidationParms` holds the four timing thresholds that govern staleness and expiration. Rather than `static constexpr`, these are mutable instance members — an intentional trade-off documented in the source: simulation code needs to inject alternate values to stress-test edge cases without recompiling. The most important thresholds are `validationCURRENT_WALL` (5 min window around signing time), `validationCURRENT_LOCAL` (3 min window from first local observation), and `validationSET_EXPIRES` (10 min lifetime for per-ledger validation sets). `validationFRESHNESS` (20 s) is a separate concept used only in laggard detection — it identifies validators that are *online right now* versus just historically known. + +## `isCurrent()` — Dual-Clock Staleness Check + +The free function `isCurrent()` checks two independent time conditions simultaneously. The sign time (from the remote validator's clock) must fall within a window around the local network time, guarding against both future-dated and ancient validations. The seen time (when this node first received the validation, in local steady-clock time) provides an additional backstop for the extremely rare case where network time drifts badly. The implementation comment explains that arithmetic avoids overflow/underflow of unsigned 32-bit timestamps by promoting to signed 64-bit — important because `signTime` comes from untrusted external nodes. + +## `SeqEnforcer` — Monotonic Sequence Invariant + +`SeqEnforcer` enforces that each validator's validations have strictly increasing ledger sequence numbers. Without this, a compromised or replayed message could appear to re-validate an old ledger. However, the enforcer deliberately resets its high-water mark after `validationSET_EXPIRES` has elapsed with no new validation from that node — so a validator returning after a long offline period can start fresh at whatever the current sequence is rather than being permanently locked out. + +## `Validations` — The Core Data Structure + +### Storage Layout + +The class maintains five coordinated data structures protected by a single `mutex_`: + +- **`current_`** (`hash_map`): The most recent valid validation from each known node. This is the fast-path for quorum queries and is continuously pruned for staleness. +- **`byLedger_`** (aged unordered map, `ID → {NodeID → Validation}`): All validations grouped by ledger hash, with LRU-style time-based expiry. The `aged_unordered_map` container tracks the last access time per entry; `beast::expire()` removes entries untouched for longer than `validationSET_EXPIRES`. +- **`bySequence_`** (aged unordered map, `Seq → {NodeID → Validation}`): Validations grouped by sequence number, used exclusively for Byzantine detection. Allows the `add()` path to check whether a given sequence already has a conflicting validation from the same node. +- **`trie_`** (`LedgerTrie`): A compressed trie over ledger ancestry, keyed on sequence-indexed ancestor IDs. The trie drives the `getPreferred()` computation. +- **`acquiring_`** (`hash_map<{Seq,ID}, hash_set>`): A holding pen for trusted validations whose target ledger has not yet been locally acquired. When the ledger finally arrives, all waiting node IDs are atomically inserted into the trie. + +### `add()` — The Critical Path + +When a validation arrives, `add()` executes a sequence of escalating checks. First, the staleness guard runs before even acquiring the lock. Inside the lock, it looks up `bySequence_` to detect whether any prior validation from this node already exists for the same sequence number. If the sequence enforcer rejects the validation (non-monotonic), the code additionally inspects the stored entry to classify the violation: + +- Same sequence, different ledger or sign time → `ValStatus::conflicting` (possible Byzantine validator) +- Same sequence and ledger, different cookie → `ValStatus::multiple` (likely misconfiguration, duplicate restart) +- Otherwise → `ValStatus::badSeq` (plain sequence regression) + +Only after passing these gates does the validation enter `current_` and `byLedger_`. Trusted validations are routed to `updateTrie()`, which either immediately inserts the associated ledger into the trie (if the ledger is locally available) or parks the node in `acquiring_` to wait. + +### Trie Management and `withTrie()` + +Every trie query flows through `withTrie()`, which first calls `current()` to flush any stale entries from `current_` and update the trie accordingly, then calls `checkAcquired()` to promote any ledgers that have become locally available since the last query. This lazy-flush design keeps the trie accurate without requiring a separate background sweep. + +`lastLedger_` tracks exactly which ledger each node currently contributes to the trie, enabling `removeTrie()` to efficiently undo a node's previous contribution before inserting its new one. This is how validation updates are atomic from the trie's perspective. + +### `getPreferred()` — Preferred Ledger Selection + +The main `getPreferred(Ledger const& curr)` overload follows a three-tier fallback. Normally it delegates to `trie_.getPreferred(localSeqEnforcer_.largest())`, which returns the tip of the heaviest weighted branch accounting for all trusted validators. If no trusted validations are trie-resident yet (typical at startup), it falls back to the `acquiring_` map, selecting the (Seq, ID) pair with the most validators waiting on it. If that is also empty, it returns `std::nullopt`, which causes the caller to use raw peer counts. + +Once a preferred ledger is identified, `getPreferred()` applies a conservative "don't switch unnecessarily" heuristic: if the preferred ledger is the immediate child of the current working ledger, the node stays put (it may be about to generate that ledger itself). It only switches to an equal or earlier sequence if the ledgers are on genuinely different chains. + +### UNL Changes and `trustChanged()` + +When the Unique Node List changes at runtime, `trustChanged()` iterates both `current_` and the full `byLedger_` index to propagate the new trusted status. Newly trusted nodes have their current validations inserted into the trie; newly untrusted nodes are removed from the trie. This keeps the trie exclusively reflecting currently trusted validators, which is what quorum computation requires. + +### Expiry Pinning with `setSeqToKeep()` + +The `expire()` method normally lets `beast::expire()` evict aged entries from both indexes. The `setSeqToKeep()` mechanism provides an override: callers can designate a half-open range `[low, high)` of sequence numbers that must not be evicted. The `expire()` implementation "touches" all matching entries shortly before their natural expiration time, resetting their LRU timestamp. To avoid doing this work on every `expire()` call, a `refreshTime` static variable throttles the touch to once per near-expiry window — roughly `validationSET_EXPIRES - validationFRESHNESS` apart. + +### Concurrency Model + +All mutable state is protected by a single `Mutex` (defaulting to `std::mutex` in the production adaptor). Private helper methods receive a `std::lock_guard const&` parameter to document that the caller must hold the lock; no re-entrant locking occurs. The `adaptor_` instance is explicitly excluded from this lock — it manages its own synchronization. Public methods uniformly acquire the lock before delegating to private helpers, and the `withTrie()` helper is the only path into the trie to ensure the flush-then-query invariant is always maintained. \ No newline at end of file diff --git a/src/xrpld/core/Config.h.ai.json b/src/xrpld/core/Config.h.ai.json new file mode 100644 index 0000000000..7526ad2617 --- /dev/null +++ b/src/xrpld/core/Config.h.ai.json @@ -0,0 +1,161 @@ +{ + "args": [ + { + "lineno": 170, + "name": "strConf" + }, + { + "lineno": 170, + "name": "bQuiet" + }, + { + "lineno": 170, + "name": "bSilent" + }, + { + "lineno": 170, + "name": "bStandalone" + }, + { + "lineno": 179, + "name": "fileContents" + }, + { + "lineno": 210, + "name": "item" + }, + { + "lineno": 210, + "name": "node" + }, + { + "lineno": 232, + "name": "section" + }, + { + "lineno": 234, + "name": "c" + }, + { + "lineno": 234, + "name": "j" + } + ], + "classes": [ + { + "args": [], + "lineno": 27, + "name": "FeeSetup" + }, + { + "args": [], + "lineno": 45, + "name": "Config" + } + ], + "description": "This file defines configuration structures and logic for the XRPL server, including fee schedules, network and peer settings, and various tunable parameters. It provides the Config class for managing server configuration, FeeSetup for fee voting, and related setup functions.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/core/Config.h", + "functions": [ + { + "args": [], + "lineno": 54, + "name": "getDebugLogFile" + }, + { + "args": [], + "lineno": 62, + "name": "load" + }, + { + "args": [], + "lineno": 38, + "name": "toFees" + }, + { + "args": [], + "lineno": 181, + "name": "quiet" + }, + { + "args": [], + "lineno": 185, + "name": "silent" + }, + { + "args": [], + "lineno": 189, + "name": "standalone" + }, + { + "args": [], + "lineno": 193, + "name": "useTxTables" + }, + { + "args": [], + "lineno": 197, + "name": "canSign" + }, + { + "args": [ + "item", + "node" + ], + "lineno": 210, + "name": "getValueFor" + }, + { + "args": [], + "lineno": 227, + "name": "journal" + }, + { + "args": [ + "strConf", + "bQuiet", + "bSilent", + "bStandalone" + ], + "lineno": 170, + "name": "setup" + }, + { + "args": [ + "bQuiet", + "bSilent", + "bStandalone" + ], + "lineno": 175, + "name": "setupControl" + }, + { + "args": [ + "fileContents" + ], + "lineno": 179, + "name": "loadFromString" + }, + { + "args": [ + "section" + ], + "lineno": 232, + "name": "setup_FeeVote" + }, + { + "args": [ + "c", + "j" + ], + "lineno": 234, + "name": "setup_DatabaseCon" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 20, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/core/Config.h.ai.md b/src/xrpld/core/Config.h.ai.md new file mode 100644 index 0000000000..08f6e5a36b --- /dev/null +++ b/src/xrpld/core/Config.h.ai.md @@ -0,0 +1,51 @@ +# `src/xrpld/core/Config.h` — Server Configuration + +`Config.h` declares the central configuration object for the `xrpld` node process. It defines `FeeSetup`, the `SizedItem` scaling enum, and the `Config` class itself, along with two free functions that extract specialized sub-configurations. Every major subsystem in the node receives a `const Config&` reference at startup, so this header is one of the most widely included files in the codebase. + +## Inheritance and Architecture + +`Config` inherits from `BasicConfig`, which is an INI-section store: it holds a map of named `Section` objects, each containing raw lines and key/value pairs parsed from the configuration file. The philosophy encoded directly in `Config.h` is that `Config` as a derived class is **deprecated**. The comment reads: *"This entire derived class is deprecated. For new config information use the style implied in the base class."* New subsystems are expected to fetch their own `Section` from `BasicConfig` and parse it locally, rather than adding new public members to `Config`. Despite this intent, `Config` still carries a large surface of public fields, reflecting accumulated organic growth. + +## Loading Lifecycle + +The three-stage loading process is initiated by `setup()`, which accepts the config file path string and three boolean mode flags. Internally it delegates to: + +1. `setupControl(bQuiet, bSilent, bStandalone)` — sets operational mode flags and **auto-detects `NODE_SIZE`** from hardware. It reads system RAM using a platform-specific helper (`detail::getMemorySize()`, implemented separately for Linux, macOS, and Windows) and then cross-references the `ramSizeGB` row of the `sizedItems` table to pick an initial tier. The result is then capped by half the number of hardware threads so that a machine with many cores but little RAM doesn't over-commit. This auto-detection runs only in networked mode; standalone mode always defaults to `NODE_SIZE = 0` (tiny). + +2. `load()` — locates and reads the config file, following a search priority: explicit `--conf` path → current working directory (checking both `xrpld.cfg` and the legacy `rippled.cfg`) → XDG config directories (`$XDG_CONFIG_HOME//`) → system defaults (`/etc/opt//`). File contents are read with `getFileContents()` and then forwarded to `loadFromString()`. + +3. `loadFromString()` — the actual parsing workhorse. It calls `parseIniFile()` to convert text into an `IniFileSections` map, builds the `BasicConfig` section store, and then iterates over specific sections to populate `Config`'s typed members. This method is also called directly in unit tests via the public `loadFromString()` API, deliberately bypassing file I/O. + +After `load()` returns, `setup()` initialises SSL contexts and, in standalone mode, forces `LEDGER_HISTORY = 0`. + +## The `SizedItem` / `NODE_SIZE` Scaling System + +The `SizedItem` enum indexes into a compile-time table of five-column arrays, where columns correspond to node sizes 0–4 (tiny, small, medium, large, huge). The table is defined in the `.cpp` as `sizedItems` and covers thirteen tunable quantities: sweep intervals, SHAMap tree cache sizes and ages, ledger cache sizes, database cache budgets, the open/final ledger limit, burst size, the RAM thresholds used for auto-detection, and the account-ID cache size. + +`getValueFor(SizedItem item, std::optional node)` looks up the appropriate column. If `node` is unseated it uses the configured `NODE_SIZE`. A `static_assert` in the implementation verifies that the enum ordinals match array positions, catching any future reordering at compile time. The design deliberately separates *what* to scale from *how much*: consumers call `getValueFor` without knowing what hardware tier the node is running on. + +## `FeeSetup` and Fee Voting + +`FeeSetup` holds the three baseline fee parameters: `reference_fee` (10 drops), `account_reserve` (10 XRP), and `owner_reserve` (2 XRP). These are the values the node will vote to establish on the ledger via the `FeeVote` mechanism during each voting ledger. `toFees()` converts the struct into a `Fees` object suitable for ledger construction. The free function `setup_FeeVote(Section const&)` reads these values from the `[voting]` config section, with the legacy `[fee_default]` section able to override `reference_fee` for offline signing workflows. + +## Security-Sensitive Defaults + +Two fields warrant attention because their defaults reflect explicit security choices: + +- `signingEnabled_` is `false` by default and only exposed via `canSign()`. Allowing a public node to sign arbitrary transactions using submitted secret keys is a significant credential-exposure risk, and the server refuses to do so unless the operator explicitly sets `[signing_support] = 1` in the config. + +- Validator nodes automatically receive `PATH_SEARCH_MAX = 0` during `loadFromString()` if either `[validation_seed]` or `[validator_token]` sections are present. Path-finding is computationally expensive and irrelevant for validators; allowing it wastes resources and could delay consensus-critical processing. The operator can override this explicitly. + +## Peer Connectivity and Relay Controls + +Peer limits are expressed in three overlapping fields. The legacy `PEERS_MAX` applies a single ceiling to all connections; the newer `PEERS_IN_MAX` and `PEERS_OUT_MAX` provide separate inbound/outbound ceilings. `loadFromString()` enforces that if either of the newer fields is configured, both must be present — partial configuration is rejected with an exception. If `PEERS_MAX` is set, the newer fields are ignored entirely, preserving backward compatibility. + +Relay policy for untrusted validations and proposals uses a three-valued integer (`1` = relay all, `0` = relay trusted only, `-1` = drop completely), mapped from the human-readable strings `"all"`, `"trusted"`, and `"drop_untrusted"` in the config file. + +## Experimental P2P Routing Features + +The header contains two clearly annotated `!!TEMPORARY CODE BLOCK!!` zones controlling prototype reduce-relay algorithms: VP (validator/proposal) squelching and TX reduce-relay. These expose tunable knobs (`VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS`, `TX_RELAY_PERCENTAGE`, `TX_REDUCE_RELAY_MIN_PEERS`) that are expected to be removed once the underlying routing algorithm matures. The deprecation comments are unusually candid about this provisional state. + +## `setup_DatabaseCon` + +The free function `setup_DatabaseCon(Config const& c, std::optional)` reads the `[database_path]` and node-database sections from a `Config` to produce a `DatabaseCon::Setup` struct used by the SQLite backend. Declaring it here—rather than in a database-specific header—is a layering compromise: it requires `Config.h` to include ``, which the inline comment flags as a known levelization violation (`VFALCO Breaks levelization`), alongside the `` include that similarly does not belong at this layer. \ No newline at end of file diff --git a/src/xrpld/core/ConfigSections.h.ai.json b/src/xrpld/core/ConfigSections.h.ai.json new file mode 100644 index 0000000000..d059ccadd5 --- /dev/null +++ b/src/xrpld/core/ConfigSections.h.ai.json @@ -0,0 +1,31 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "ConfigSection" + } + ], + "description": "Defines configuration section names and a deprecated ConfigSection struct for the XRPL project.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/core/ConfigSections.h", + "functions": [ + { + "args": [], + "lineno": 10, + "name": "nodeDatabase" + }, + { + "args": [], + "lineno": 14, + "name": "importNodeDatabase" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/core/ConfigSections.h.ai.md b/src/xrpld/core/ConfigSections.h.ai.md new file mode 100644 index 0000000000..7176a97437 --- /dev/null +++ b/src/xrpld/core/ConfigSections.h.ai.md @@ -0,0 +1,26 @@ +# `ConfigSections.h` — Configuration Section Name Registry + +This header serves a single, focused purpose: it is the canonical registry of every named section that the XRPL node configuration system (`rippled.cfg`) recognizes. Rather than scattering raw string literals across the codebase, all callers include this file and reference the constant by name, so a section rename requires only one edit. + +## Two-Tier Design (and Why One Tier Is Deprecated) + +The file exposes two parallel mechanisms that reflect different eras of the configuration system's evolution. + +**`ConfigSection` struct** (lines 8–22) is the older mechanism, kept alive under a `// VFALCO DEPRECATED` banner. It provides two static `std::string`-returning methods, `nodeDatabase()` → `"node_db"` and `importNodeDatabase()` → `"import_db"`, which name the on-disk key-value store sections. These were wrapped in a struct presumably to namespace them and avoid bare string literals, but the struct itself adds no state — the `explicit ConfigSection() = default` constructor is never meant to be called. The callers that remain (primarily test utilities such as `envconfig.cpp` and `SHAMapStore_test.cpp`) use expressions like `cfg->section(ConfigSection::nodeDatabase())` and `cfg->overwrite(ConfigSection::nodeDatabase(), ...)` to manipulate the in-memory `BasicConfig` section map. These call sites predate the newer interface but have not been migrated yet. + +**`#define SECTION_*` macros** (lines 25–78) cover the full range of top-level `[section]` blocks a `rippled.cfg` file may contain. They are plain C-string literals used directly in `Config::load()` (via `detail/Config.cpp`) with helpers like `getSingleSection()`, `getIniFileSection()`, and `exists()`. Because they are raw string literals rather than `std::string` variables, they can be freely concatenated at compile time inside error messages, e.g.: + +```cpp +Throw("Cannot have both [" SECTION_VALIDATION_SEED + "] and [" SECTION_VALIDATOR_TOKEN "] config sections"); +``` + +The `// VFALCO TODO` annotation at line 24 acknowledges that macros are the wrong tool and that these should become typed constants (e.g., `constexpr std::string_view`), but the migration has not happened. + +## Scope of Coverage + +The 54 macros span every major subsystem: peer networking (`SECTION_IPS`, `SECTION_IPS_FIXED`, `SECTION_PEERS_MAX`, `SECTION_OVERLAY`), validator infrastructure (`SECTION_VALIDATORS`, `SECTION_VALIDATOR_TOKEN`, `SECTION_VALIDATOR_LIST_SITES`, `SECTION_VALIDATOR_LIST_THRESHOLD`), amendment governance (`SECTION_AMENDMENTS`, `SECTION_VETO_AMENDMENTS`, `SECTION_AMENDMENT_MAJORITY_TIME`), threading (`SECTION_WORKERS`, `SECTION_IO_WORKERS`, `SECTION_PREFETCH_WORKERS`), SSL (`SECTION_SSL_VERIFY`, `SECTION_SSL_VERIFY_FILE`, `SECTION_SSL_VERIFY_DIR`), path-finding (`SECTION_PATH_SEARCH*`), reduce-relay (`SECTION_REDUCE_RELAY`), and several operational toggles. Each macro's string value matches exactly what a node operator types as a bracketed heading in `rippled.cfg`. + +## Relationship to `Config.h` and `BasicConfig` + +`Config` (in `Config.h`) derives from `BasicConfig`, which owns a `std::map` keyed on these exact string values. `ConfigSections.h` is therefore the bridge between the human-readable config file format and the typed C++ `Config` object — every `getSingleSection()` or `section()` call in `detail/Config.cpp` resolves to a key whose spelling is guaranteed by one of these constants. \ No newline at end of file diff --git a/src/xrpld/core/NetworkIDServiceImpl.h.ai.json b/src/xrpld/core/NetworkIDServiceImpl.h.ai.json new file mode 100644 index 0000000000..503e72efea --- /dev/null +++ b/src/xrpld/core/NetworkIDServiceImpl.h.ai.json @@ -0,0 +1,45 @@ +{ + "args": [ + { + "lineno": 16, + "name": "networkID" + } + ], + "classes": [ + { + "args": [ + "std::uint32_t networkID" + ], + "lineno": 13, + "name": "NetworkIDServiceImpl" + } + ], + "description": "Defines NetworkIDServiceImpl, an implementation of NetworkIDService that caches and provides the network ID from the application Config.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/core/NetworkIDServiceImpl.h", + "functions": [ + { + "args": [ + "networkID" + ], + "lineno": 16, + "name": "NetworkIDServiceImpl" + }, + { + "args": [], + "lineno": 18, + "name": "~NetworkIDServiceImpl" + }, + { + "args": [], + "lineno": 21, + "name": "getNetworkID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/core/NetworkIDServiceImpl.h.ai.md b/src/xrpld/core/NetworkIDServiceImpl.h.ai.md new file mode 100644 index 0000000000..3367a04b2b --- /dev/null +++ b/src/xrpld/core/NetworkIDServiceImpl.h.ai.md @@ -0,0 +1,21 @@ +# `NetworkIDServiceImpl.h` — Concrete Network Identity Provider + +`NetworkIDServiceImpl` is the private, application-layer implementation of the public `NetworkIDService` interface. Its entire purpose is to capture the `network_id` value from the application config at startup and serve it as a cheap, lock-free read for the lifetime of the process. + +## Interface Layering + +The abstract base `NetworkIDService` lives in `include/xrpl/core/` — the public library boundary — and declares a single pure virtual method `getNetworkID() const noexcept`. This separation is deliberate: the concept that "a network has an ID" is a protocol-level concern that any consumer can depend on without knowing anything about config parsing. The concrete `NetworkIDServiceImpl`, however, lives in `src/xrpld/core/` (the private `xrpld` application layer), keeping the config-reading detail out of the public interface. + +## Construction and Ownership + +The constructor takes a `std::uint32_t networkID` directly rather than a `Config` reference, so the caller (`Application.cpp`) pre-extracts the value via `config_->NETWORK_ID`: + +```cpp +networkIDService_(std::make_unique(config_->NETWORK_ID)) +``` + +`Application` stores the result as `unique_ptr`, so all downstream consumers see only the abstract interface. The value is immutable after construction — the private `networkID_` member is set once in the member-initializer list and never touched again — making `getNetworkID()` trivially thread-safe without any synchronization. + +## Design Choices + +Marking the class `final` signals that this is a leaf node in the hierarchy; the polymorphism lives entirely in `NetworkIDService`. The `noexcept` qualifier on `getNetworkID()` is significant: it allows callers in hot paths (transaction validation, routing) to call it without exception bookkeeping. The XRPL network ID namespace assigns 0 to mainnet, 1 to testnet, 2 to devnet, and 1025+ to custom networks — the last range is important because custom-network transactions are required to carry an explicit `NetworkID` field, and this service is the canonical source for validating that field. \ No newline at end of file diff --git a/src/xrpld/core/TimeKeeper.h.ai.json b/src/xrpld/core/TimeKeeper.h.ai.json new file mode 100644 index 0000000000..ed108363cd --- /dev/null +++ b/src/xrpld/core/TimeKeeper.h.ai.json @@ -0,0 +1,50 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 8, + "name": "TimeKeeper" + } + ], + "description": "Manages various times used by the server, including network time and predicted close time, with adjustments for the XRPL epoch.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/core/TimeKeeper.h", + "functions": [ + { + "args": [ + "when" + ], + "lineno": 13, + "name": "adjust" + }, + { + "args": [], + "lineno": 36, + "name": "now" + }, + { + "args": [], + "lineno": 54, + "name": "closeTime" + }, + { + "args": [], + "lineno": 62, + "name": "closeOffset" + }, + { + "args": [ + "by" + ], + "lineno": 68, + "name": "adjustCloseTime" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/core/TimeKeeper.h.ai.md b/src/xrpld/core/TimeKeeper.h.ai.md new file mode 100644 index 0000000000..ef6ee6ea9b --- /dev/null +++ b/src/xrpld/core/TimeKeeper.h.ai.md @@ -0,0 +1,35 @@ +# `TimeKeeper.h` — Network Time Management for XRPL Consensus + +`TimeKeeper` solves a specific distributed-systems problem: nodes in the XRP Ledger network each have their own wall clock, yet consensus requires every node to agree on a single "close time" for each ledger round. This class maintains two related but distinct time values — the local wall-clock expressed in XRPL network time, and a consensus-adjusted estimate of what the rest of the network considers the current time. + +## Inheritance and the `NetClock` Epoch + +`TimeKeeper` inherits from `beast::abstract_clock`, which is a simple interface providing a virtual `now()` method. The `abstract_clock` pattern exists specifically to support dependency injection: callers that only need `now()` can hold a reference to `abstract_clock` and receive either the real `TimeKeeper` in production or a `ManualTimeKeeper` in tests. + +`NetClock` itself is defined in `chrono.h` with an unusual epoch: January 1, 2000 rather than the Unix epoch of January 1, 1970. The 946,684,800-second offset between the two is computed at compile time using Howard Hinnant's `date` library and confirmed with a `static_assert`. The `adjust()` helper (a private `constexpr` static) applies this translation on every call to `now()`, converting `std::chrono::system_clock::time_point` to `NetClock::time_point` by subtracting `epoch_offset`. The code's own comment notes that this epoch was "arbitrarily defined" by Arthur Britto and David Schwartz during early XRPL development with no stated rationale — it has no semantic significance, but every timestamp in the protocol inherits this convention. + +## Two Time Concepts: `now()` and `closeTime()` + +`now()` returns the server's local wall clock expressed in `NetClock` units. It is the server's unilateral view of the current time and does not incorporate any network feedback. Other nodes can infer this value indirectly from published proposals and validations, but it is not transmitted directly. + +`closeTime()` returns `now() + closeOffset_`. The `closeOffset_` member is a running correction accumulated from peer observations. It represents the server's estimate of how far its own clock deviates from the network's consensus view of time. The "predicted close time" that results is what the server uses as the notional center of the network — an important distinction because it decouples the server's actual clock from the value it uses when participating in ledger close negotiations. + +## The `adjustCloseTime()` Algorithm + +`adjustCloseTime()` is called by `RCLConsensus` at the end of each consensus round after computing a weighted average of peers' close-time votes. The `by` argument is the difference between that network estimate and the server's own time — positive if the network appears to be ahead, negative if behind. + +The adjustment logic implements a damped proportional controller: + +- **Small offsets (|by| ≤ 1s):** Treated as noise. Rather than applying the tiny correction, the existing `closeOffset_` is decayed by multiplying by `3/4`. This causes the offset to converge toward zero when the node is approximately in sync, preventing jitter from minor timing disagreements between peers from perturbing the estimate. + +- **Larger offsets (|by| > 1s):** The offset moves by `(by + 3s) / 4` for positive values and `(by - 3s) / 4` for negative values. The ±3-second bias before the quarter-division means corrections lean slightly away from zero before dampening, ensuring meaningful drift is corrected rather than washed out. The quarter-step is conservative enough to avoid overcorrection but aggressive enough to converge over a few rounds. + +The early-exit when both `by` and `offset` are zero avoids the atomic read-modify-write entirely when the server is already synchronized, a minor but intentional optimization. + +## Concurrency Design + +`closeOffset_` is a `std::atomic`. The `adjustCloseTime()` implementation loads the current value, computes the new value via a lambda, and applies it with `compare_exchange_strong`. The code comment is explicit that this CAS is a "weak check" — the caller serializes calls to `adjustCloseTime()` externally so a CAS failure is safe to ignore without retry. The atomic type is used not to protect against races between multiple concurrent adjusters, but to ensure that readers of `closeOffset_` (via `closeTime()` or `closeOffset()`) always see a consistent value without needing a mutex. + +## Test Support + +`ManualTimeKeeper` in `src/test/jtx/ManualTimeKeeper.h` subclasses `TimeKeeper` and overrides `now()` with an atomically settable `time_point`. Tests call `set()` to advance or rewind time, which affects `closeTime()` as well since the offset is additive. The test harness uses this for scenarios like forcing a ledger expiration check to trigger by jumping the clock forward by weeks. \ No newline at end of file diff --git a/src/xrpld/core/detail/Config.cpp.ai.json b/src/xrpld/core/detail/Config.cpp.ai.json new file mode 100644 index 0000000000..3362a6596e --- /dev/null +++ b/src/xrpld/core/detail/Config.cpp.ai.json @@ -0,0 +1,512 @@ +{ + "args": [ + { + "lineno": 62, + "name": "strInput" + }, + { + "lineno": 62, + "name": "bTrim" + }, + { + "lineno": 101, + "name": "secSource" + }, + { + "lineno": 101, + "name": "strSection" + }, + { + "lineno": 110, + "name": "secSource" + }, + { + "lineno": 110, + "name": "strSection" + }, + { + "lineno": 110, + "name": "strValue" + }, + { + "lineno": 110, + "name": "j" + }, + { + "lineno": 134, + "name": "name" + }, + { + "lineno": 145, + "name": "bQuiet" + }, + { + "lineno": 145, + "name": "bSilent" + }, + { + "lineno": 145, + "name": "bStandalone" + }, + { + "lineno": 167, + "name": "strConf" + }, + { + "lineno": 167, + "name": "bQuiet" + }, + { + "lineno": 167, + "name": "bSilent" + }, + { + "lineno": 167, + "name": "bStandalone" + }, + { + "lineno": 246, + "name": "config" + }, + { + "lineno": 285, + "name": "fileContents" + }, + { + "lineno": 648, + "name": "item" + }, + { + "lineno": 648, + "name": "node" + }, + { + "lineno": 658, + "name": "section" + }, + { + "lineno": 677, + "name": "c" + }, + { + "lineno": 677, + "name": "j" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "xrpl::Config::Config (constructor)", + "xrpl::Config::setup", + "xrpl::detail::getMemorySize" + ], + "entry_point": "xrpl::detail::getMemorySize", + "purpose": "Determines the total physical memory available on the system to size internal caches and parameters.", + "validation_points": [ + "Windows: GlobalMemoryStatusEx(&msx) return value", + "Linux: sysinfo(&si) == 0", + "MacOS: sysctl(mib, 2, &ram, &size, NULL, 0) == 0" + ] + }, + { + "call_chain": [ + "xrpl::Config::Config (constructor)", + "xrpl::Config::setup", + "xrpl::parseIniFile" + ], + "entry_point": "parseIniFile", + "purpose": "Parses the configuration file into sections for further processing.", + "validation_points": [ + "Input string is parsed and validated for INI format correctness (details not shown in provided code)." + ] + } + ], + "data_flows": [ + { + "field": "msx.ullTotalPhys (Windows)", + "flow": [ + "GlobalMemoryStatusEx(&msx)", + "msx.ullTotalPhys", + "return value of getMemorySize", + "used in Config setup for sizing" + ], + "origin": "OS API call GlobalMemoryStatusEx fills MEMORYSTATUSEX struct", + "transformations": [ + "Casted to std::uint64_t" + ], + "validated_at": "GlobalMemoryStatusEx(&msx) return value checked for success" + }, + { + "field": "si.totalram and si.mem_unit (Linux)", + "flow": [ + "sysinfo(&si)", + "si.totalram * si.mem_unit", + "return value of getMemorySize", + "used in Config setup for sizing" + ], + "origin": "OS API call sysinfo fills sysinfo struct", + "transformations": [ + "Multiplied to get total bytes", + "Casted to std::uint64_t" + ], + "validated_at": "sysinfo(&si) == 0 checked for success" + }, + { + "field": "ram (MacOS)", + "flow": [ + "sysctl(mib, 2, &ram, &size, NULL, 0)", + "ram", + "return value of getMemorySize", + "used in Config setup for sizing" + ], + "origin": "OS API call sysctl fills ram variable", + "transformations": [ + "Casted to std::uint64_t" + ], + "validated_at": "sysctl(...) == 0 checked for success" + }, + { + "field": "Config file contents", + "flow": [ + "File read", + "parseIniFile", + "IniFileSections", + "Config setup" + ], + "origin": "Read from disk (not shown in snippet)", + "transformations": [ + "Parsed into sections and key-value pairs" + ], + "validated_at": "parseIniFile (input string validated for INI format)" + } + ], + "description": "This file implements configuration parsing, loading, and management for the XRPL node software (xrpld). It handles reading configuration files, setting up node parameters based on system resources, and provides utility functions for accessing and validating configuration values.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "MEMORYSTATUSEX struct (Windows) / sysinfo struct (Linux) / sysctl (MacOS)", + "empty", + "string", + "validation" + ], + "evidence": "OS API call return value at getMemorySize()", + "issue_pattern": "Missing empty string validation for MEMORYSTATUSEX struct (Windows) / sysinfo struct (Linux) / sysctl (MacOS)", + "why_false_positive": "OS API call return value validates MEMORYSTATUSEX struct (Windows) / sysinfo struct (Linux) / sysctl (MacOS) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "msx.ullTotalPhys (Windows)", + "empty", + "string", + "validation" + ], + "evidence": "GlobalMemoryStatusEx(&msx) return value at getMemorySize() (Windows)", + "issue_pattern": "Missing empty string validation for msx.ullTotalPhys (Windows)", + "why_false_positive": "GlobalMemoryStatusEx(&msx) return value validates msx.ullTotalPhys (Windows) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "si.totalram and si.mem_unit (Linux)", + "empty", + "string", + "validation" + ], + "evidence": "sysinfo(&si) == 0 at getMemorySize() (Linux)", + "issue_pattern": "Missing empty string validation for si.totalram and si.mem_unit (Linux)", + "why_false_positive": "sysinfo(&si) == 0 validates si.totalram and si.mem_unit (Linux) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "ram (MacOS)", + "empty", + "string", + "validation" + ], + "evidence": "sysctl(mib, 2, &ram, &size, NULL, 0) == 0 at getMemorySize() (MacOS)", + "issue_pattern": "Missing empty string validation for ram (MacOS)", + "why_false_positive": "sysctl(mib, 2, &ram, &size, NULL, 0) == 0 validates ram (MacOS) for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/core/detail/Config.cpp", + "functions": [ + { + "args": [], + "lineno": 13, + "name": "getMemorySize" + }, + { + "args": [], + "lineno": 29, + "name": "getMemorySize" + }, + { + "args": [], + "lineno": 45, + "name": "getMemorySize" + }, + { + "args": [ + "strInput", + "bTrim" + ], + "lineno": 62, + "name": "parseIniFile" + }, + { + "args": [ + "secSource", + "strSection" + ], + "lineno": 101, + "name": "getIniFileSection" + }, + { + "args": [ + "secSource", + "strSection", + "strValue", + "j" + ], + "lineno": 110, + "name": "getSingleSection" + }, + { + "args": [ + "name" + ], + "lineno": 134, + "name": "getEnvVar" + }, + { + "args": [], + "lineno": 141, + "name": "Config" + }, + { + "args": [ + "bQuiet", + "bSilent", + "bStandalone" + ], + "lineno": 145, + "name": "setupControl" + }, + { + "args": [ + "strConf", + "bQuiet", + "bSilent", + "bStandalone" + ], + "lineno": 167, + "name": "setup" + }, + { + "args": [ + "config" + ], + "lineno": 246, + "name": "checkZeroPorts" + }, + { + "args": [], + "lineno": 267, + "name": "load" + }, + { + "args": [ + "fileContents" + ], + "lineno": 285, + "name": "loadFromString" + }, + { + "args": [], + "lineno": 627, + "name": "getDebugLogFile" + }, + { + "args": [ + "item", + "node" + ], + "lineno": 648, + "name": "getValueFor" + }, + { + "args": [ + "section" + ], + "lineno": 658, + "name": "setup_FeeVote" + }, + { + "args": [ + "c", + "j" + ], + "lineno": 677, + "name": "setup_DatabaseCon" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + }, + { + "lineno": 11, + "name": "detail" + }, + { + "lineno": 27, + "name": "xrpl" + }, + { + "lineno": 28, + "name": "detail" + }, + { + "lineno": 44, + "name": "xrpl" + }, + { + "lineno": 45, + "name": "detail" + }, + { + "lineno": 61, + "name": "xrpl" + } + ], + "test_coverage_notes": "The provided code is low-level and platform-specific. Direct unit tests for getMemorySize are unlikely due to OS dependencies, but higher-level Config tests may indirectly cover these paths. Tests likely exist in files such as Config_test.cpp or similar, focusing on configuration parsing and setup. However, platform-specific memory detection error paths (e.g., OS API failures) are probably not directly tested, and mocking would be required for full coverage. Validation of config file parsing is likely tested, but edge cases (malformed INI, permission errors) may not be fully covered.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "OS API return value checking", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "None (returns 0 on failure)", + "field": "MEMORYSTATUSEX struct (Windows) / sysinfo struct (Linux) / sysctl (MacOS)", + "location": "getMemorySize()", + "validated_by": "OS API call return value", + "validates": [ + "Checks if OS API call to get memory size succeeds before using value" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "None (returns 0 on failure)", + "field": "msx.ullTotalPhys (Windows)", + "location": "getMemorySize() (Windows)", + "validated_by": "GlobalMemoryStatusEx(&msx) return value", + "validates": [ + "Ensures memory size is only read if API call succeeds" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "None (returns 0 on failure)", + "field": "si.totalram and si.mem_unit (Linux)", + "location": "getMemorySize() (Linux)", + "validated_by": "sysinfo(&si) == 0", + "validates": [ + "Ensures memory size is only read if API call succeeds" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "None (returns 0 on failure)", + "field": "ram (MacOS)", + "location": "getMemorySize() (MacOS)", + "validated_by": "sysctl(mib, 2, &ram, &size, NULL, 0) == 0", + "validates": [ + "Ensures memory size is only read if API call succeeds" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/core/detail/Config.cpp.ai.md b/src/xrpld/core/detail/Config.cpp.ai.md new file mode 100644 index 0000000000..cad3a38e29 --- /dev/null +++ b/src/xrpld/core/detail/Config.cpp.ai.md @@ -0,0 +1,65 @@ +# `src/xrpld/core/detail/Config.cpp` + +## Role in the System + +`Config.cpp` is the implementation file for the XRPL node daemon's configuration subsystem. It is responsible for the full lifecycle of node configuration: discovering the config file on disk, parsing the INI-format text into sections, interrogating system hardware to choose sensible defaults, and populating the `Config` object's typed fields that the rest of `xrpld` reads throughout its lifetime. This file is the single place that translates the human-authored `xrpld.cfg` into the concrete values driving peer limits, cache sizes, fee votes, SQLite pragmas, and more. + +## Cross-Platform Memory Detection + +Three separate definitions of `detail::getMemorySize()` live inside `#if BOOST_OS_WINDOWS`, `#if BOOST_OS_LINUX`, and `#if BOOST_OS_MACOS` guards. Each queries the OS for total physical RAM using the appropriate API (`GlobalMemoryStatusEx` on Windows, `sysinfo` on Linux, `sysctl(CTL_HW, HW_MEMSIZE)` on macOS) and returns a `std::uint64_t` byte count. On failure, all three return `0` silently; the caller (`setupControl`) treats zero as "unknown" and defaults conservatively. The `[[nodiscard]]` attribute prevents unintentional discard of the return value. Placing these inside `xrpl::detail` and guarding with Boost's OS macros means none of the platform-specific headers bleed into the general build. + +## The `sizedItems` Table and Node Size Autodetection + +A file-local `constexpr` array of 13 `{SizedItem, array}` pairs encodes five node size tiers — `tiny`, `small`, `medium`, `large`, `huge` — for every tuneable internal parameter (`treeCacheSize`, `ledgerSize`, `burstSize`, etc.). This design centralises all hardware-tier thresholds in one reviewable block rather than scattering magic numbers across subsystems. + +A `static_assert` with a `constexpr` lambda verifies at compile time that the array order exactly matches the `SizedItem` enum ordinals. If someone adds an enum entry without updating the table, the build fails immediately. + +`Config::setupControl()` uses `ramSize_` (total RAM in GB, computed at construction) to walk the `ramSizeGB` row of this table, finding the first tier whose RAM threshold exceeds the detected hardware. It then applies a second cap: `min(hardware_concurrency / 2, computed_tier)`. The intuition is that a machine with many cores but little RAM should be sized by RAM, and vice versa. Standalone mode skips autodetection and stays at `tiny` (tier 0), because a developer instance does not need large caches. + +## INI Parsing Pipeline + +`parseIniFile()` is a straightforward line-oriented parser. It normalises CR/LF and CR line endings to LF first, then processes each line: blank lines and `#`-prefixed comments are skipped; `[section_name]` lines start a new section; everything else is appended to the current section's string vector. The result is `IniFileSections`, a `map>`. The default section (lines before the first `[...]` header) is keyed by an empty string. + +`getIniFileSection()` is a thin pointer accessor into this map. `getSingleSection()` adds a semantic constraint: it only succeeds if the section contains exactly one line, logging a warning and returning `false` for any other cardinality. This enforces the common pattern where scalar config values should appear exactly once; callers use the boolean return to decide whether to update their field. + +## Config File Location Discovery + +`Config::setup()` implements a prioritised search for the config file when no explicit `--conf` path is given. The search order is: + +1. Current working directory, looking first for `xrpld.cfg` then the legacy `rippled.cfg`. +2. XDG Base Directory paths derived from `$HOME`, `$XDG_CONFIG_HOME`, and `$XDG_DATA_HOME` (with the XDG defaults applied if those variables are absent). +3. System-level `/etc/opt/`. + +The `do { ... } while (false)` idiom with `break` lets each candidate exit the search chain early, avoiding the need for nested `if` chains or gotos. Data directory discovery mirrors the config directory logic, defaulting to a `db/` subdirectory relative to wherever the config file was found, but overridable via `[database_path]` in the config itself. + +After `load()` runs, `setup()` creates the data directory with `create_directories`, throwing a `std::runtime_error` if that fails. Standalone mode clears `dataDir` so no database directory is created at all. + +## `loadFromString()`: The Main Parsing Logic + +Separating `load()` (reads from disk) from `loadFromString()` (works on a raw string) is a deliberate testability decision: unit tests inject config content without touching the filesystem. + +`loadFromString()` calls `parseIniFile()`, then calls the base-class `build()` to populate the generic `BasicConfig` section map, and then iterates through the known section names, materialising typed fields on the `Config` object. A shared `strTemp` string is used as an intermediate buffer for `getSingleSection()` calls, which is then lexically cast to the destination type using `beast::lexicalCastThrow`. + +Several cross-cutting concerns are handled here: + +- **IP address colon normalisation**: The `[ips]` and `[ips_fixed]` entries traditionally use space as an IP/port separator, but many admins write `host:port`. A regex replace converts the trailing `:port` suffix to ` port`, but carefully skips any line containing more than one colon (IPv6 addresses). + +- **Validator configuration**: Validator keys and UNL list sites are not loaded in standalone mode. When a `[validators_file]` path is specified, that file is parsed with `parseIniFile` and its `[validators]`, `[validator_keys]`, and `[validator_list_keys]` sections are merged into the main config. The file must contain at least one of these sections or a `std::runtime_error` is thrown. The `[validator_list_threshold]` value is checked to not exceed the number of configured list keys. + +- **Mutually exclusive validator config**: Having both `[validation_seed]` and `[validator_token]` in the same file is caught and rejected with an explicit error, as these represent two incompatible ways of specifying validator identity. + +- **Feature flags**: The `[features]` section lists amendment names. Each is looked up in the registered feature registry; an unknown name throws rather than silently being ignored. + +- **Relay policy**: `RELAY_UNTRUSTED_VALIDATIONS` and `RELAY_UNTRUSTED_PROPOSALS` use a three-way enum encoded as `1`/`0`/`-1` meaning "relay all", "relay trusted only", "drop". The strings `all`, `trusted`, and `drop_untrusted` map to these values. + +- **`[reduce_relay]` deprecation**: A temporary code block handles the rename from `vp_enable` to `vp_base_squelch_enable`. Both are read and either can set the flag, but using both simultaneously in the same config file is an error. The code itself is annotated as temporary with prominent comments. + +- **Network quorum sanity check**: After all peer limits are parsed, the code verifies that `NETWORK_QUORUM` does not exceed `PEERS_MAX`. This cross-field validation cannot be done field-by-field; it requires both values to be known first. + +## `setup_DatabaseCon()` and SQLite Safety Levels + +The free function `setup_DatabaseCon()` translates the `[sqlite]` config section into a `DatabaseCon::Setup` struct containing PRAGMA strings to be applied to every opened database. The design provides a `safety_level` high-level API (either `high` or `low`) that atomically sets `journal_mode`, `synchronous`, and `temp_store` together. If `safety_level` is present, mixing in individual settings for any of those three is forbidden and throws. This prevents partially-lowered durability settings that might not be intentional. When `low` safety is set on a node with significant ledger history (above `SQLITE_TUNING_CUTOFF`), a journal warning is emitted. The `page_size` must be a power of two between 512 and 65536; this is validated with a bitwise `(page_size & (page_size - 1)) != 0` check. + +## Error Handling Strategy + +The file consistently uses `Throw()` — the XRPL codebase's `std::throw_with_nested`-based wrapper — for all validation failures. File-read errors in `load()` are treated as recoverable: they log to `stderr` and return without updating state, allowing the node to start with purely default values if no config file is found. This is a deliberate UX choice so that running `xrpld` with no config file in a development environment does not immediately crash. Parse errors and constraint violations in `loadFromString()`, however, are terminal. \ No newline at end of file diff --git a/src/xrpld/core/detail/NetworkIDServiceImpl.cpp.ai.json b/src/xrpld/core/detail/NetworkIDServiceImpl.cpp.ai.json new file mode 100644 index 0000000000..3a0f5e33f9 --- /dev/null +++ b/src/xrpld/core/detail/NetworkIDServiceImpl.cpp.ai.json @@ -0,0 +1,88 @@ +{ + "args": [ + { + "lineno": 6, + "name": "networkID" + } + ], + "classes": [ + { + "args": [ + "networkID" + ], + "lineno": 5, + "name": "NetworkIDServiceImpl" + } + ], + "code_paths": [ + { + "call_chain": [ + "NetworkIDServiceImpl::NetworkIDServiceImpl" + ], + "entry_point": "NetworkIDServiceImpl::NetworkIDServiceImpl", + "purpose": "Constructs the NetworkIDServiceImpl object with a given networkID.", + "validation_points": [] + }, + { + "call_chain": [ + "NetworkIDServiceImpl::getNetworkID" + ], + "entry_point": "NetworkIDServiceImpl::getNetworkID", + "purpose": "Returns the stored networkID value.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "networkID_", + "flow": [ + "Constructor argument", + "Assigned to member variable networkID_", + "Returned by getNetworkID()" + ], + "origin": "Constructor argument (std::uint32_t networkID) to NetworkIDServiceImpl", + "transformations": [ + "Direct assignment; no transformation" + ], + "validated_at": "No validation occurs in this code" + } + ], + "description": "Implements the NetworkIDServiceImpl class, which provides access to a network ID value for the XRPL system.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/core/detail/NetworkIDServiceImpl.cpp", + "functions": [ + { + "args": [ + "networkID" + ], + "lineno": 6, + "name": "NetworkIDServiceImpl::NetworkIDServiceImpl" + }, + { + "args": [], + "lineno": 10, + "name": "NetworkIDServiceImpl::getNetworkID" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no validation logic in this code (e.g., no range checks or assertions on networkID). The code is a simple data holder. Test coverage would likely be limited to construction and retrieval of the networkID value. Tests would be expected in files like NetworkIDServiceImpl_test.cpp or similar, but unless tests explicitly check for invalid or edge-case networkID values, validation paths are not exercised. There are no validation code paths to test in this implementation.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": null, + "validation_layer": null + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/core/detail/NetworkIDServiceImpl.cpp.ai.md b/src/xrpld/core/detail/NetworkIDServiceImpl.cpp.ai.md new file mode 100644 index 0000000000..027a52385a --- /dev/null +++ b/src/xrpld/core/detail/NetworkIDServiceImpl.cpp.ai.md @@ -0,0 +1,19 @@ +# `NetworkIDServiceImpl.cpp` — Concrete Network Identity Provider + +This file provides the sole concrete implementation of the `NetworkIDService` abstract interface, giving the rest of the XRPL node a single, stable point of access to the numeric network identifier. + +## Role in the Service Architecture + +`NetworkIDService` (defined in the public `include/xrpl/core/` tree) declares one pure virtual method: `getNetworkID() const noexcept`. Keeping the interface in the public include tree while hiding `NetworkIDServiceImpl` inside `src/xrpld/` is deliberate — components that only need to *query* the network ID depend solely on the thin abstract base; only `Application.cpp`, which owns the concrete wiring, needs to know about the implementation class. + +## Immutable-by-Design Cache + +The constructor accepts a `std::uint32_t networkID` and stores it directly into the private member `networkID_`. There is no setter and no post-construction mutation path. This is appropriate because network identity is a launch-time property: a running node cannot switch networks without restarting. By resolving the value once — from `Config::NETWORK_ID`, which itself parses the `network_id` config file section and maps well-known aliases (`"main"` → 0, `"testnet"` → 1, `"devnet"` → 2, or any raw integer for custom networks ≥ 1025) — `NetworkIDServiceImpl` guarantees every caller gets the same answer for the lifetime of the process. + +## `noexcept` Contract + +`getNetworkID()` propagates the `noexcept` guarantee declared on the virtual method. Because it only returns a stored integer this is trivially safe, but the explicit annotation matters: call sites involved in transaction validation and signing (e.g., `TransactionSign.cpp`, `Transactor.cpp`) may be invoked in contexts where exception propagation is undesirable. + +## Construction Site + +`Application.cpp` constructs the service as `std::make_unique(config_->NETWORK_ID)` and stores it as a `unique_ptr`, enforcing the interface-only dependency for all downstream consumers. \ No newline at end of file diff --git a/src/xrpld/overlay/Cluster.h.ai.json b/src/xrpld/overlay/Cluster.h.ai.json new file mode 100644 index 0000000000..46602cf00f --- /dev/null +++ b/src/xrpld/overlay/Cluster.h.ai.json @@ -0,0 +1,141 @@ +{ + "args": [ + { + "lineno": 32, + "name": "j" + }, + { + "lineno": 39, + "name": "node" + }, + { + "lineno": 53, + "name": "identity" + }, + { + "lineno": 54, + "name": "name" + }, + { + "lineno": 55, + "name": "loadFee" + }, + { + "lineno": 56, + "name": "reportTime" + }, + { + "lineno": 63, + "name": "func" + }, + { + "lineno": 76, + "name": "nodes" + }, + { + "lineno": 16, + "name": "lhs" + }, + { + "lineno": 16, + "name": "rhs" + } + ], + "classes": [ + { + "args": [ + "beast::Journal j" + ], + "lineno": 9, + "name": "Cluster" + }, + { + "args": [], + "lineno": 11, + "name": "Comparator" + } + ], + "description": "Defines the xrpl::Cluster class, which manages a set of cluster nodes, providing membership checks, updates, iteration, and loading from configuration.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/Cluster.h", + "functions": [ + { + "args": [ + "beast::Journal j" + ], + "lineno": 32, + "name": "Cluster" + }, + { + "args": [ + "PublicKey const& node" + ], + "lineno": 39, + "name": "member" + }, + { + "args": [], + "lineno": 46, + "name": "size" + }, + { + "args": [ + "PublicKey const& identity", + "std::string name", + "std::uint32_t loadFee", + "NetClock::time_point reportTime" + ], + "lineno": 52, + "name": "update" + }, + { + "args": [ + "std::function func" + ], + "lineno": 62, + "name": "for_each" + }, + { + "args": [ + "Section const& nodes" + ], + "lineno": 75, + "name": "load" + }, + { + "args": [], + "lineno": 11, + "name": "Comparator" + }, + { + "args": [ + "ClusterNode const& lhs", + "ClusterNode const& rhs" + ], + "lineno": 16, + "name": "operator()" + }, + { + "args": [ + "ClusterNode const& lhs", + "PublicKey const& rhs" + ], + "lineno": 21, + "name": "operator()" + }, + { + "args": [ + "PublicKey const& lhs", + "ClusterNode const& rhs" + ], + "lineno": 26, + "name": "operator()" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/Cluster.h.ai.md b/src/xrpld/overlay/Cluster.h.ai.md new file mode 100644 index 0000000000..a4810931dd --- /dev/null +++ b/src/xrpld/overlay/Cluster.h.ai.md @@ -0,0 +1,27 @@ +# `xrpld/overlay/Cluster.h` — Trusted Cluster Node Registry + +`Cluster` manages the set of validator or relay nodes belonging to a trusted operator cluster in the XRPL peer-to-peer overlay. In XRPL's architecture, a "cluster" is a group of nodes run by the same administrative entity. Members can propagate state to each other efficiently and are treated with elevated trust — for example, exempted from load-based fee throttling that applies to anonymous peers. This class is the single authority for cluster membership queries and state maintenance throughout the overlay layer. + +## Data Model + +Internally, cluster members are held in a `std::set`. The choice of `std::set` over `std::unordered_set` is deliberate: it enables stable iteration order and, more importantly, supports the heterogeneous lookup trick discussed below. Each `ClusterNode` (defined in `ClusterNode.h`) carries four fields: an immutable `PublicKey` identity, a human-readable name/comment string, a load fee integer, and a `NetClock::time_point` recording when the node last reported its state. The public key serves as the unique identifier and sort key. + +## Transparent Comparator and Heterogeneous Lookup + +The private `Comparator` struct is the most architecturally interesting piece of this header. It provides three overloads of `operator()`: `(ClusterNode, ClusterNode)`, `(ClusterNode, PublicKey)`, and `(PublicKey, ClusterNode)`. By tagging the struct with `using is_transparent = std::true_type;`, it opts into C++14 heterogeneous associative container lookup. This means `nodes_.find(identity)` in `member()` and `update()` accepts a raw `PublicKey` directly rather than requiring construction of a dummy `ClusterNode` object. Without this, every membership check would require allocating a temporary node just to perform the lookup — wasteful and semantically awkward since partial construction of a `ClusterNode` would be needed (its constructor is `delete`d for the default case). + +## Thread Safety + +All public methods are guarded by a `mutable std::mutex`. The `mutable` qualifier on both `mutex_` and `j_` is required because `member()`, `size()`, and `for_each()` are `const` methods that still need to acquire the lock and log. The `for_each()` method holds the lock for its entire iteration pass and explicitly documents that calling `update()` from within the callback is forbidden — doing so would deadlock on the non-recursive mutex. + +## Update Semantics and the Erase-Reinsert Pattern + +`update()` enforces a monotonic-time invariant: it only applies a new state record if `reportTime` is strictly newer than the stored report time. This guards against stale gossip from cluster peers arriving out of order. Because `std::set` elements are logically `const` after insertion (you cannot modify a key in-place without corrupting the container's ordering), updating a node requires erasing the old entry and reinserting a new one. The implementation captures the iterator hint from `erase()` and passes it to `emplace_hint()`, making the reinsert O(1) amortized rather than O(log n). There is also a name-preservation rule: if the incoming `name` argument is empty, the existing name is carried forward, so a status update from a cluster peer that omits a name does not silently blank out the human-readable label loaded from config. + +## Loading from Configuration + +`load()` parses a `[cluster_nodes]` config section where each line is a base58-encoded node public key optionally followed by a whitespace-separated comment. It uses a compiled `static boost::regex` (initialized once) to tokenize each line, then calls `parseBase58` with `TokenType::NodePublic` to decode the key. Duplicate entries produce a warning but are skipped rather than failing the load — the first entry wins. Malformed lines or invalid public keys return `false` immediately, signalling the application startup to abort or warn. + +## Relationship to `ClusterNode` + +`ClusterNode` is a simple value type — no virtual methods, no shared state. `Cluster` owns all `ClusterNode` instances exclusively inside its `std::set`. The separation of `ClusterNode.h` from `Cluster.h` allows other parts of the overlay (e.g., `PeerImp`) to accept `ClusterNode const&` in callbacks via `for_each()` without pulling in the full cluster management machinery. \ No newline at end of file diff --git a/src/xrpld/overlay/ClusterNode.h.ai.json b/src/xrpld/overlay/ClusterNode.h.ai.json new file mode 100644 index 0000000000..dfbaf5c58f --- /dev/null +++ b/src/xrpld/overlay/ClusterNode.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 13, + "name": "identity" + }, + { + "lineno": 14, + "name": "name" + }, + { + "lineno": 15, + "name": "fee" + }, + { + "lineno": 16, + "name": "rtime" + } + ], + "classes": [ + { + "args": [ + "identity", + "name", + "fee", + "rtime" + ], + "lineno": 8, + "name": "ClusterNode" + } + ], + "description": "Defines the ClusterNode class, representing a node in a cluster with identity, name, load fee, and report time.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/ClusterNode.h", + "functions": [ + { + "args": [ + "identity", + "name", + "fee", + "rtime" + ], + "lineno": 12, + "name": "ClusterNode" + }, + { + "args": [], + "lineno": 22, + "name": "name" + }, + { + "args": [], + "lineno": 27, + "name": "getLoadFee" + }, + { + "args": [], + "lineno": 32, + "name": "getReportTime" + }, + { + "args": [], + "lineno": 37, + "name": "identity" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/ClusterNode.h.ai.md b/src/xrpld/overlay/ClusterNode.h.ai.md new file mode 100644 index 0000000000..9d324ed03c --- /dev/null +++ b/src/xrpld/overlay/ClusterNode.h.ai.md @@ -0,0 +1,27 @@ +# ClusterNode.h + +`ClusterNode` is a small, immutable-identity value type that represents a single trusted peer within an XRPL cluster. It lives in `src/xrpld/overlay/ClusterNode.h` and is the element type stored inside `Cluster`'s `std::set`. The class is intentionally minimal: it captures exactly the four pieces of information that cluster members exchange with one another — who you are, what you call yourself, how loaded you are, and when you last reported. + +## Role in the Cluster Subsystem + +An XRPL cluster is a set of validator nodes operated by the same entity that extend automatic trust to each other. Cluster membership is declared in the server configuration (the `[cluster_nodes]` section) as a list of base58-encoded node public keys with optional human-readable names. At runtime, cluster peers periodically broadcast `TMCluster` protocol messages containing each known member's current load fee and report timestamp. `ClusterNode` is the in-memory record that holds that gossip state for a single peer. + +`Cluster` (defined in `Cluster.h`) owns a `std::set` keyed on `PublicKey`. The `Comparator` struct uses transparent comparison (`is_transparent = std::true_type`) so the set can be searched with a raw `PublicKey` without constructing a dummy `ClusterNode` — enabling the erase/insert idiom in `Cluster::update()` to work cleanly alongside direct `find(PublicKey)` lookups. + +## Fields and Their Purpose + +`identity_` is declared `const` — it is the primary key and must never change after construction. This is why `Cluster::update()` cannot mutate a `ClusterNode` in place: `std::set` elements are logically immutable (you cannot modify a key once inserted without invalidating the container's order). Instead, `update()` erases the old node and re-inserts a freshly constructed one with the same identity but updated `mLoadFee` and `mReportTime`. + +`mLoadFee` carries the fee level that this cluster peer is currently advertising under load. `NetworkOPs` aggregates all peer load fees via `Cluster::for_each()` to compute a cluster-wide fee, which is then pushed into the fee tracker via `setClusterFee()`. This lets the network apply a consistent fee floor that reflects the busiest trusted node. + +`mReportTime` uses `NetClock::time_point` — the ledger's logical network clock, not wall-clock time. `Cluster::update()` rejects a new report if `reportTime <= iter->getReportTime()`, ensuring the cluster state only advances monotonically. Using network time rather than system time avoids skew problems between nodes with different clocks. + +`name_` is the human-assigned label from the config file or the peer's self-reported name in a `TMCluster` message. It is preserved across updates: if a gossiped update arrives with an empty name, `Cluster::update()` retains the previously known name rather than overwriting it with blank — a small but deliberate defensive choice. + +## Construction and Defaults + +The default constructor is deleted, enforcing that every `ClusterNode` must have a meaningful identity. `fee` and `rtime` default to zero/epoch respectively so that a freshly-loaded config entry (which carries no live state) starts at the lowest possible values and will immediately accept the first real update from that peer. + +## Usage Patterns + +The class exposes only `const` accessors. All mutation goes through `Cluster::update()`, which takes a mutex lock and performs the erase-reinsert round trip. This keeps `ClusterNode` thread-safe by design: its data is never written after construction, so any concurrent reader holding a reference obtained under the lock observes a consistent snapshot. Peers check cluster membership (`cluster()` in `PeerImp`) to decide whether to skip load-shedding, relay proposals unconditionally, and bypass signature verification on validations — making the accuracy of the cluster state operationally significant. \ No newline at end of file diff --git a/src/xrpld/overlay/Compression.h.ai.json b/src/xrpld/overlay/Compression.h.ai.json new file mode 100644 index 0000000000..f59a4afe69 --- /dev/null +++ b/src/xrpld/overlay/Compression.h.ai.json @@ -0,0 +1,77 @@ +{ + "args": [ + { + "lineno": 25, + "name": "in" + }, + { + "lineno": 26, + "name": "inSize" + }, + { + "lineno": 27, + "name": "decompressed" + }, + { + "lineno": 28, + "name": "decompressedSize" + }, + { + "lineno": 29, + "name": "algorithm" + }, + { + "lineno": 62, + "name": "in" + }, + { + "lineno": 63, + "name": "inSize" + }, + { + "lineno": 64, + "name": "bf" + }, + { + "lineno": 65, + "name": "algorithm" + } + ], + "classes": [], + "description": "Provides compression and decompression utilities for XRPL, supporting LZ4 algorithm, with error handling and logging.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/Compression.h", + "functions": [ + { + "args": [ + "in", + "inSize", + "decompressed", + "decompressedSize", + "algorithm" + ], + "lineno": 22, + "name": "decompress" + }, + { + "args": [ + "in", + "inSize", + "bf", + "algorithm" + ], + "lineno": 59, + "name": "compress" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "compression" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/Compression.h.ai.md b/src/xrpld/overlay/Compression.h.ai.md new file mode 100644 index 0000000000..6fa35a68dc --- /dev/null +++ b/src/xrpld/overlay/Compression.h.ai.md @@ -0,0 +1,31 @@ +# `Compression.h` — Overlay Compression Facade + +This header provides the compression abstraction layer used by the XRPL peer-to-peer overlay when serializing and transmitting protocol messages. It sits between the high-level `Message` class (which decides *whether* to compress a message) and the low-level `CompressionAlgorithms.h` (which implements the actual codec), serving as a thin dispatch layer that centralizes algorithm selection, exception suppression, and the wire-format constants that define the XRPL binary message protocol. + +## Wire Format Constants and Their Significance + +The two size constants — `headerBytes = 6` and `headerBytesCompressed = 10` — are not arbitrary; they define the two legal XRPL message header formats on the wire. An uncompressed message header occupies exactly 6 bytes: 4 bytes encoding the payload length (with the top 6 bits reserved/zeroed) followed by 2 bytes for the protobuf message type. A compressed message header occupies 10 bytes: the same first 4 bytes now carry the compression algorithm in the top 4 bits, followed by 2 bytes for the message type, followed by 4 additional bytes encoding the original uncompressed size. That extra 4-byte field is what pushes the compressed header from 6 to 10 bytes and is the reason `Message::compress()` checks whether the compressed payload actually saves more than 4 bytes over the uncompressed form before committing to the compressed path. + +These constants are consumed directly by `ProtocolMessage.h` for header parsing and by `Message.cpp` for buffer sizing, making them a shared specification across the send and receive paths. + +## The `Algorithm` Enum Encoding + +The `Algorithm` enum carries an important invariant documented with an inline comment: all values other than `None` must have the high bit set, and the low nibble must be zero. `LZ4 = 0x90` satisfies this — binary `1001 0000`. This encoding is deliberate: on the receive side, `ProtocolMessage::parseMessageHeader()` first checks `*iter & 0x80` to detect a compressed message, then extracts the algorithm via `*iter & 0xF0`, and validates that the reserved bits `*iter & 0x0C` are zero. The enum value can therefore be extracted directly by masking the first wire byte, with no further translation needed. Adding a new algorithm in the future would require choosing a value with the high bit set and a zero low nibble (e.g., `0xA0`, `0xB0`) to remain compatible with this decoding scheme. + +`None = 0x00` deliberately uses a zero high bit, which is how the uncompressed path is signaled at the protocol level without any special-casing in the parser. + +## `compress()` and `decompress()` — Exception Boundaries + +Both functions are function templates that delegate immediately to `CompressionAlgorithms.h`, but they serve a distinct purpose: they are **exception-to-zero-return converters**. The underlying `lz4Compress` and `lz4Decompress` implementations throw `std::runtime_error` on failure, but the overlay's send and receive paths cannot propagate exceptions. These wrappers catch all exceptions and return `0`, which callers treat as a failure signal. + +The asymmetry in template parameters reflects the asymmetry in network I/O. `decompress()` accepts a `ZeroCopyInputStream` (a protobuf abstraction for scatter-gather network buffers) because incoming data arrives in discontiguous chunks from Boost.Asio. The stream-based overload of `lz4Decompress` handles chunk stitching: it tries to use the first chunk directly if it is large enough to hold the entire compressed payload, and only falls back to a contiguous copy if the data spans multiple chunks. `compress()`, by contrast, operates on data that is already serialized into a contiguous `buffer_` in `Message`, so it takes a raw `void const*`. + +The `BufferFactory` template parameter for `compress()` implements a lazy allocation pattern: rather than pre-allocating a fixed output buffer, the caller provides a callable that accepts the required capacity (computed from `LZ4_compressBound`) and returns a pointer to an appropriately sized buffer. In practice, `Message::compress()` passes a lambda that calls `bufferCompressed_.resize()` and returns a pointer offset by `headerBytesCompressed`, so the codec writes compressed bytes directly into the final wire buffer leaving the header region intact. + +## Algorithm Extensibility Guard + +Both dispatch functions contain an `else` branch that logs a warning and calls `UNREACHABLE`. These branches are marked `LCOV_EXCL_START`/`LCOV_EXCL_STOP` because they cannot be reached with the current algorithm set. The `UNREACHABLE` macro is a deliberate design signal: if a new `Algorithm` enum value is added without updating these dispatch functions, the sanitizer or assertion framework will catch it at runtime rather than silently falling through to a zero-return. This makes the enum an open-coded extensibility point with a mechanical safety net. + +## Relationship to Sibling Files + +`Compression.h` is included by `Message.h`, making it a transitive dependency for anything that constructs or inspects overlay messages. `ProtocolMessage.h` uses the `headerBytes`, `headerBytesCompressed`, and `Algorithm` symbols directly for parsing incoming streams. `CompressionAlgorithms.h` is the only non-overlay dependency and contains the actual LZ4 calls; its placement in `include/xrpl/basics/` (the shared protocol library) rather than `src/xrpld/overlay/` keeps the codec reusable outside the overlay while `Compression.h` provides the overlay-specific dispatch policy on top of it. \ No newline at end of file diff --git a/src/xrpld/overlay/Message.h.ai.json b/src/xrpld/overlay/Message.h.ai.json new file mode 100644 index 0000000000..542ab0f07f --- /dev/null +++ b/src/xrpld/overlay/Message.h.ai.json @@ -0,0 +1,93 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "::google::protobuf::Message const& message", + "protocol::MessageType type", + "std::optional const& validator = {}" + ], + "lineno": 22, + "name": "Message" + } + ], + "description": "This file defines the xrpl::Message class, which handles the packaging, serialization, and optional compression of protocol buffer messages for network transmission in the XRPL overlay. It provides methods for buffer management, compression, header manipulation, and metadata access.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/Message.h", + "functions": [ + { + "args": [ + "::google::protobuf::Message const& message", + "protocol::MessageType type", + "std::optional const& validator = {}" + ], + "lineno": 27, + "name": "Message" + }, + { + "args": [], + "lineno": 34, + "name": "getBufferSize" + }, + { + "args": [ + "::google::protobuf::Message const& message" + ], + "lineno": 38, + "name": "messageSize" + }, + { + "args": [ + "::google::protobuf::Message const& message" + ], + "lineno": 41, + "name": "totalSize" + }, + { + "args": [ + "Compressed tryCompressed" + ], + "lineno": 48, + "name": "getBuffer" + }, + { + "args": [], + "lineno": 54, + "name": "getCategory" + }, + { + "args": [], + "lineno": 60, + "name": "getValidatorKey" + }, + { + "args": [ + "std::uint8_t* in", + "std::uint32_t payloadBytes", + "int type", + "Algorithm compression", + "std::uint32_t uncompressedBytes" + ], + "lineno": 71, + "name": "setHeader" + }, + { + "args": [], + "lineno": 81, + "name": "compress" + }, + { + "args": [ + "std::uint8_t const* in" + ], + "lineno": 88, + "name": "getType" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/Message.h.ai.md b/src/xrpld/overlay/Message.h.ai.md new file mode 100644 index 0000000000..3366be8156 --- /dev/null +++ b/src/xrpld/overlay/Message.h.ai.md @@ -0,0 +1,42 @@ +# `xrpld/overlay/Message.h` — Wire Protocol Message Framing + +`Message` is the serialization envelope for every protobuf message that travels between XRPL peers on the overlay network. It solves a specific problem that is easy to underestimate: a single broadcast message — a validation, a transaction, a ledger chunk — may be sent to dozens of peers simultaneously. This class encapsulates the serialized bytes once, compresses them at most once, and lets all concurrent sends share the same buffer safely. + +## What a `Message` Contains + +At construction, the protobuf object is serialized immediately into `buffer_`, prepended by a 6-byte wire header. The header format is documented in detail inside `setHeader()` in the `.cpp`: + +- **Uncompressed (6 bytes):** 6 reserved zero bits, a 26-bit payload size, and a 16-bit message type. All multi-byte values are big-endian. +- **Compressed (10 bytes):** The first bit signals compression; the next 3 bits name the algorithm (currently only LZ4, encoded as `0x90`); 2 reserved bits; 26-bit *compressed* payload size; 16-bit message type; 32-bit original uncompressed size. + +The distinction matters because the receiver needs to know how large a buffer to allocate for decompression before it can decode the protobuf. Both formats share the same first 6 bytes, with the compressed form appending 4 extra bytes for the original size field. + +The hard limit `maximumMessageSize = megabytes(64)` is enforced by the receiving path — a 26-bit payload field physically cannot represent more than 64 MiB. + +## Lazy, Once-Only Compression + +The second buffer, `bufferCompressed_`, is populated lazily. `getBuffer(Compressed::On)` calls `std::call_once(once_flag_, &Message::compress, this)`, which means compression runs exactly once no matter how many threads call `getBuffer` concurrently. This is the central design trade-off: CPU for LZ4 compression is spent once and amortized across all N peer sends, rather than once per peer. + +The `compress()` method applies a hard-coded compressibility policy before even attempting compression: + +1. Messages smaller than 70 bytes are skipped entirely — LZ4 overhead would negate any benefit. +2. Only a specific whitelist of message types is eligible: `mtMANIFESTS`, `mtENDPOINTS`, `mtTRANSACTION`, `mtGET_LEDGER`, `mtLEDGER_DATA`, `mtGET_OBJECTS`, `mtVALIDATOR_LIST`, `mtVALIDATOR_LIST_COLLECTION`, `mtREPLAY_DELTA_RESPONSE`, and `mtTRANSACTIONS`. High-frequency small messages like `mtPING`, `mtVALIDATION`, `mtPROPOSE_LEDGER`, and `mtSTATUS_CHANGE` are deliberately excluded. +3. Even if LZ4 is attempted, if the compressed result is not smaller than the uncompressed payload minus the extra 4 header bytes (the net savings threshold), `bufferCompressed_` is cleared and the uncompressed buffer is returned by `getBuffer()` as a fallback. + +This fallback is why `getBuffer()` checks `bufferCompressed_.empty()` after calling `compress()` rather than unconditionally returning the compressed buffer. + +## Traffic Accounting + +At construction time, `TrafficCount::categorize()` classifies the message into one of roughly 50 fine-grained traffic categories (`transaction`, `validation`, `ledger_data`, `get_hash_ledger`, etc.) and stores the result in `category_`. This is intentional: the category is computed once during serialization, before the message is queued. In `PeerImp::send()`, the category is read via `getCategory()` to report outbound traffic metrics without re-inspecting the protobuf type. + +## Squelch Integration + +The optional `validatorKey_` field is provided specifically for `mtVALIDATION` and `mtPROPOSE_LEDGER` messages. In `PeerImp::send()`, if a validator key is present, the squelch check `squelch_.expireSquelch(*validator)` gates whether the message is actually transmitted to that peer. When a peer is squelched for a given validator, the message bytes are counted under `TrafficCount::squelch_suppressed` and the send is skipped without ever calling `boost::asio::async_write`. Passing the key through `Message` rather than the call site keeps the broadcast path uniform — the same `shared_ptr` flows through all peer send queues, and each `PeerImp` independently decides whether to skip or transmit it. + +## Lifetime and Async Safety + +`Message` inherits `std::enable_shared_from_this`. The key invariant is that `getBuffer()` returns a `const&` to an internal `std::vector`. In `PeerImp`, the message is held in `send_queue_` (a `std::queue>`), which keeps the object alive for the duration of the `boost::asio::async_write` call that passes `buffer_.data()` directly to the kernel. The message is dequeued only after `onWriteMessage` fires, ensuring the buffer lifetime outlives the async operation. Compression writes to `bufferCompressed_` are protected by `std::call_once`, so there is no race between the compression attempt and concurrent reads of the compressed buffer by other threads calling `getBuffer`. + +## Relationship to `Compression.h` + +`Compression.h` provides the `compress()` and `decompress()` template functions over LZ4 (via `CompressionAlgorithms.h`), plus the `Algorithm` and `Compressed` enums and the header size constants (`headerBytes = 6`, `headerBytesCompressed = 10`). `Message` owns the higher-level policy of *when* to compress; `Compression.h` supplies the mechanism. The `Compressed::On/Off` enum flows all the way from the peer-level negotiation (stored as `compressionEnabled_` in `PeerImp`) through `getBuffer()` to `compress()`. \ No newline at end of file diff --git a/src/xrpld/overlay/Overlay.h.ai.json b/src/xrpld/overlay/Overlay.h.ai.json new file mode 100644 index 0000000000..bfd455859b --- /dev/null +++ b/src/xrpld/overlay/Overlay.h.ai.json @@ -0,0 +1,203 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 19, + "name": "Overlay" + }, + { + "args": [], + "lineno": 32, + "name": "Setup" + } + ], + "description": "Defines the Overlay class, which manages the set of connected peers in the XRPL network, providing interfaces for peer management, broadcasting, relaying, and diagnostics.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/Overlay.h", + "functions": [ + { + "args": [], + "lineno": 27, + "name": "Overlay" + }, + { + "args": [], + "lineno": 44, + "name": "~Overlay" + }, + { + "args": [], + "lineno": 46, + "name": "start" + }, + { + "args": [], + "lineno": 50, + "name": "stop" + }, + { + "args": [ + "bundle", + "request", + "remote_address" + ], + "lineno": 56, + "name": "onHandoff" + }, + { + "args": [ + "address" + ], + "lineno": 64, + "name": "connect" + }, + { + "args": [], + "lineno": 68, + "name": "limit" + }, + { + "args": [], + "lineno": 74, + "name": "size" + }, + { + "args": [], + "lineno": 80, + "name": "json" + }, + { + "args": [], + "lineno": 86, + "name": "getActivePeers" + }, + { + "args": [ + "index" + ], + "lineno": 92, + "name": "checkTracking" + }, + { + "args": [ + "id" + ], + "lineno": 97, + "name": "findPeerByShortID" + }, + { + "args": [ + "pubKey" + ], + "lineno": 102, + "name": "findPeerByPublicKey" + }, + { + "args": [ + "m" + ], + "lineno": 107, + "name": "broadcast" + }, + { + "args": [ + "m" + ], + "lineno": 111, + "name": "broadcast" + }, + { + "args": [ + "m", + "uid", + "validator" + ], + "lineno": 116, + "name": "relay" + }, + { + "args": [ + "m", + "uid", + "validator" + ], + "lineno": 125, + "name": "relay" + }, + { + "args": [ + "hash", + "m", + "toSkip" + ], + "lineno": 134, + "name": "relay" + }, + { + "args": [ + "f" + ], + "lineno": 146, + "name": "foreach" + }, + { + "args": [], + "lineno": 156, + "name": "incJqTransOverflow" + }, + { + "args": [], + "lineno": 158, + "name": "getJqTransOverflow" + }, + { + "args": [], + "lineno": 163, + "name": "incPeerDisconnect" + }, + { + "args": [], + "lineno": 165, + "name": "getPeerDisconnect" + }, + { + "args": [], + "lineno": 167, + "name": "incPeerDisconnectCharges" + }, + { + "args": [], + "lineno": 169, + "name": "getPeerDisconnectCharges" + }, + { + "args": [], + "lineno": 179, + "name": "networkID" + }, + { + "args": [], + "lineno": 188, + "name": "txMetrics" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "boost" + }, + { + "lineno": 14, + "name": "asio" + }, + { + "lineno": 15, + "name": "ssl" + }, + { + "lineno": 18, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/Overlay.h.ai.md b/src/xrpld/overlay/Overlay.h.ai.md new file mode 100644 index 0000000000..de8e2bcd93 --- /dev/null +++ b/src/xrpld/overlay/Overlay.h.ai.md @@ -0,0 +1,45 @@ +# `Overlay.h` — Abstract Peer Network Interface + +`Overlay.h` defines the `Overlay` abstract class, the single point of authority for everything related to the XRPL peer-to-peer mesh. Every other subsystem that needs to talk to, query, or broadcast across peers does so through this interface. The concrete implementation lives in `detail/OverlayImpl.h` and is created exclusively via the `make_Overlay()` factory declared in `make_Overlay.h`, keeping the full implementation hidden from callers. + +## Position in the Architecture + +The class inherits from `beast::PropertyStream::Source`, registering itself under the name `"peers"` so that the server's diagnostics tree can walk into it without any additional wiring. The constructor comment acknowledges this inheritance is an "unfortunate problem with the API" — `PropertyStream::Source` demands the name at construction time, forcing the otherwise default-constructible base to be explicit. This is a rare place where a design constraint from a utility library bleeds into the abstraction boundary. + +The `Setup` struct collects all configuration that must be known at construction time: a shared SSL context (shared because multiple peer connections reuse the same TLS settings), the server's public IP address, a per-IP connection limit (`ipLimit`), crawl options controlling what the `/crawl` endpoint exposes, an optional `networkID`, and a `vlEnabled` flag that controls Validator List propagation. Separating this into a value type rather than passing parameters through `make_Overlay` keeps the factory signature stable as configuration grows. + +## Connection Lifecycle + +`onHandoff()` is the entry point for inbound connections. The HTTP server layer receives a raw TLS stream and an HTTP upgrade request, and calls this method to decide whether the request belongs to the overlay. The method receives ownership of the `ssl_stream` via `unique_ptr` and returns a `Handoff` indicating whether it accepted the connection; if it declines, the caller handles it as a regular HTTP request. The asymmetric design — accepting an owning pointer rather than a reference — ensures the stream cannot be used by the caller after a successful handoff. + +Outbound connections are initiated by `connect()`, which is fire-and-forget: it schedules an asynchronous connection attempt and returns immediately. The caller has no way to observe the result through this interface; success or failure is handled inside the overlay and reflected in the peer count. + +## Peer Discovery and Access + +`size()` returns only peers that have completed the peer protocol handshake, not those mid-negotiation. This distinction matters for capacity planning: `limit()` returns the configured maximum, and the gap between `limit()` and `size()` represents available slots. + +`getActivePeers()` takes a snapshot of the current peer list at the moment of the call, returning a `std::vector>`. The snapshot design is deliberate: it avoids holding an internal lock across the caller's iteration, trading consistency (a peer might disconnect between the snapshot and a subsequent call) for safety and simplicity. The `foreach()` template is built directly on top of `getActivePeers()` and exists purely as a convenience wrapper; it is non-virtual because the snapshot semantics are defined entirely by `getActivePeers()`. + +`findPeerByShortID()` and `findPeerByPublicKey()` allow callers to retrieve a specific peer. The short ID (`Peer::id_t`, a `uint32_t`) is an ephemeral connection-local identifier that is stable only for the lifetime of the connection. The public key lookup is used by the consensus and validation layers, which operate in terms of validator identity rather than connection identity. + +## Broadcast vs. Relay + +The interface distinguishes two dissemination modes with different semantics: + +`broadcast()` sends a `TMProposeSet` or `TMValidation` message to every active peer without any deduplication logic. This is used when the node itself originates the message. + +`relay()` is for forwarding a message received from a peer. It takes the serialized message, a `uint256` deduplication key, and the validator's public key. It returns the set of `Peer::id_t` values that already sent the node this exact message — in other words, the peers that already have it and do not need it forwarded back. The returned set enables the caller to track which peers contributed to coverage without re-broadcasting to them. This is the squelch/reduce-relay mechanism described in the [XRPL blog post on message routing optimizations](https://xrpl.org/blog/2021/message-routing-optimizations-pt-1-proposal-validation-relaying.html): rather than flooding every peer unconditionally, the overlay selects a small number of "source" peers per validator and temporarily squelches the rest. + +Transaction relay uses a third overload with a different signature: it takes the transaction hash, an `optional>` for the full message (which may be absent if only the hash is being queued), and a `toSkip` set of peers that have already seen it. When the tx reduce-relay feature is active, the overlay randomly selects a subset of peers to receive the full message immediately and queues the hash for the remainder, to be flushed later via `Peer::sendTxQueue()`. + +## Telemetry Counters + +The increment/get pairs for `jqTransOverflow`, `peerDisconnect`, and `peerDisconnectCharges` are deliberately split rather than combined into a single atomic fetch-and-increment. This allows monitoring code to read the current total without side effects, while producers only call the increment variant. The `peerDisconnectCharges` counter specifically tracks disconnections triggered by the resource charging system — cases where a peer was consuming excessive resources rather than disconnecting due to normal network events. + +## Network Partitioning + +`networkID()` returns an optional `uint32_t` identifying which network the node belongs to (0 = mainnet, 1 = testnet, 2 = devnet). During the peer handshake, both sides exchange their network ID, and a mismatch causes the connection to be rejected. This is the primary mechanism preventing stale or misconfigured testnet nodes from accidentally joining mainnet and consuming relay bandwidth. + +## `Promote` Enum + +The `Promote` enum (`automatic`, `never`, `always`) controls whether an incoming connection is eligible for promotion from an outbound-only slot to a full bidirectional peer slot. It is defined on `Overlay` rather than on `Peer` because the promotion decision is a policy of the overlay as a whole, not of the individual connection. \ No newline at end of file diff --git a/src/xrpld/overlay/Peer.h.ai.json b/src/xrpld/overlay/Peer.h.ai.json new file mode 100644 index 0000000000..033a5ff429 --- /dev/null +++ b/src/xrpld/overlay/Peer.h.ai.json @@ -0,0 +1,24 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "Peer" + } + ], + "description": "Defines the Peer class, representing a peer connection in the XRPL overlay network, including its interface for network communication, identity, ledger synchronization, and protocol feature support.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/Peer.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "Resource" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/Peer.h.ai.md b/src/xrpld/overlay/Peer.h.ai.md new file mode 100644 index 0000000000..a63cf13248 --- /dev/null +++ b/src/xrpld/overlay/Peer.h.ai.md @@ -0,0 +1,46 @@ +# `src/xrpld/overlay/Peer.h` — Abstract Peer Interface + +## Role in the System + +`Peer.h` defines the pure abstract interface that represents a single authenticated, handshake-complete peer connection in the XRPL overlay network. It sits at the heart of the overlay subsystem: `Overlay` manages a collection of these objects, `predicates.h` filters and iterates over them, and the concrete implementation `PeerImp` (in `detail/`) provides the actual async I/O machinery. The entire rest of the codebase never needs to include `PeerImp.h` — they work exclusively through this interface. + +This separation is intentional. `PeerImp` is a substantial class with a complex multi-stage SSL shutdown state machine, boost circular buffers, shared mutexes, and protocol-buffer handling. Exposing a clean interface here means calling code can route messages, query ledger state, and apply resource charges without being coupled to that complexity. + +## Ownership and Identity + +The `using ptr = std::shared_ptr` alias makes shared ownership the canonical handle for a peer. This is important because a peer's lifetime is inherently uncertain — it can disconnect at any moment — and multiple subsystems (the overlay, consensus, transaction routing) hold references concurrently. + +The companion `using id_t = std::uint32_t` establishes a separate, stable numeric identifier. The comment is explicit: this integer *can be stored in tables* and outlives the peer object itself. When a subsystem stores a `Peer::id_t` rather than a `Peer::ptr`, it avoids accidentally extending the peer's lifetime while still having a handle it can later use with `Overlay::findPeerByShortID()` to check whether the peer is still live. `predicates.h`'s `peer_in_set` relies on exactly this: it holds a `std::set` to express "relay to these peers and no others," which is safe across disconnect events in a way that a set of raw pointers would not be. + +## Protocol Feature Negotiation + +The `ProtocolFeature` enum enumerates opt-in capabilities that a peer may or may not have negotiated during the handshake: + +- `ValidatorListPropagation` and `ValidatorList2Propagation` — whether the peer can receive newer validator list formats +- `LedgerReplay` — support for targeted ledger replay requests + +`supportsFeature(ProtocolFeature)` lets the overlay consult per-peer capability before sending a message type the peer can't handle. This is a backward-compatibility mechanism: the same `Peer` interface serves peers running different protocol versions. + +## Transaction Reduce-Relay + +Three methods — `addTxQueue()`, `sendTxQueue()`, and `removeTxQueue()` — form the transaction reduce-relay interface. Rather than broadcasting a full transaction to every peer the moment it arrives (a flooding approach), the reduce-relay optimization accumulates transaction hashes in a per-peer queue and ships them in a single batch. `ReduceRelayCommon.h` defines the policy constants: a maximum queue size of 10,000 hashes, and timing thresholds for selecting which peers receive full transactions versus just hash notifications. The `txReduceRelayEnabled()` predicate gates all of this per-peer so the feature degrades gracefully when talking to older peers that don't support it. `compressionEnabled()` serves the same gating role for LZ4 payload compression. + +## Resource Charging + +`charge(Resource::Charge const& fee, std::string const& context)` adjusts a peer's load balance score when it imposes cost on the local node — for example, by sending malformed messages, triggering expensive validation, or causing job queue overflows. The `context` string (added to aid diagnostics) identifies what triggered the charge, which flows into `Overlay`'s `incPeerDisconnectCharges()` accounting when a peer is finally disconnected for excessive consumption. This is the load-shedding mechanism that protects the node from abusive peers. + +## Ledger Synchronization Queries + +The ledger-related methods reflect the state the peer has advertised about its own ledger history: + +- `getClosedLedgerHash()` / `hasLedger()` / `ledgerRange()` — expose what the peer claims to have, used when requesting missing ledger data from the network +- `hasTxSet()` — checks whether this peer has a specific transaction set (SHAMap), relevant during consensus +- `cycleStatus()` / `hasRange()` — support the peer tracking system that monitors whether a peer is following the current ledger + +## Validator List Sequence Tracking + +`publisherListSequence()` and `setPublisherListSequence()` maintain a per-peer, per-publisher sequence number. The squelch system in `Slot.h` uses this to ensure that once a peer has propagated a given validator list version, it isn't unnecessarily re-sent. The keying by `PublicKey` allows tracking across multiple independent validator list publishers simultaneously. + +## Relationship to `predicates.h` + +`predicates.h` demonstrates how this interface is consumed in practice. The `send_always`, `send_if_pred`, and `send_if_not_pred` function objects wrap `peer->send(msg)` and combine with predicates like `peer_in_cluster` (testing `peer->cluster()`) and `peer_in_set` (testing `peer->id()`). The `Overlay::foreach()` template ties this together, iterating a snapshot of active peers and applying the visitor. This pattern keeps broadcast logic compositional and testable without binding it to `PeerImp`. \ No newline at end of file diff --git a/src/xrpld/overlay/PeerSet.h.ai.json b/src/xrpld/overlay/PeerSet.h.ai.json new file mode 100644 index 0000000000..00eec5cd06 --- /dev/null +++ b/src/xrpld/overlay/PeerSet.h.ai.json @@ -0,0 +1,117 @@ +{ + "args": [ + { + "lineno": 23, + "name": "limit" + }, + { + "lineno": 24, + "name": "hasItem" + }, + { + "lineno": 25, + "name": "onPeerAdded" + }, + { + "lineno": 33, + "name": "message" + }, + { + "lineno": 33, + "name": "peer" + }, + { + "lineno": 39, + "name": "message" + }, + { + "lineno": 39, + "name": "type" + }, + { + "lineno": 39, + "name": "peer" + }, + { + "lineno": 55, + "name": "app" + }, + { + "lineno": 62, + "name": "app" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "PeerSet" + }, + { + "args": [], + "lineno": 48, + "name": "PeerSetBuilder" + } + ], + "description": "Defines the PeerSet class for managing a set of peers to retrieve data (like ledgers or transaction sets) from the network, as well as related builder and dummy implementations.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/PeerSet.h", + "functions": [ + { + "args": [ + "limit", + "hasItem", + "onPeerAdded" + ], + "lineno": 22, + "name": "addPeers" + }, + { + "args": [ + "message", + "peer" + ], + "lineno": 32, + "name": "sendRequest" + }, + { + "args": [ + "message", + "type", + "peer" + ], + "lineno": 38, + "name": "sendRequest" + }, + { + "args": [], + "lineno": 43, + "name": "getPeerIds" + }, + { + "args": [], + "lineno": 51, + "name": "build" + }, + { + "args": [ + "app" + ], + "lineno": 55, + "name": "make_PeerSetBuilder" + }, + { + "args": [ + "app" + ], + "lineno": 62, + "name": "make_DummyPeerSet" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/PeerSet.h.ai.md b/src/xrpld/overlay/PeerSet.h.ai.md new file mode 100644 index 0000000000..6702e7f2c7 --- /dev/null +++ b/src/xrpld/overlay/PeerSet.h.ai.md @@ -0,0 +1,34 @@ +# `PeerSet.h` — Peer Management Interface for Distributed Data Retrieval + +## Role in the System + +When a rippled node needs data it does not have locally — a historical ledger, a transaction set, a SHAMap subtree — it must fetch it from the overlay network by querying connected peers. `PeerSet` is the abstraction that governs that process: it owns the set of peers being queried, selects new peers to try, and dispatches protobuf messages to them. + +The class sits at the boundary between the overlay network layer (`src/xrpld/overlay/`) and the ledger acquisition subsystem (`src/xrpld/app/ledger/`). Callers such as `InboundLedger`, `TransactionAcquire`, and `LedgerReplayer` each hold a `std::unique_ptr` and drive it as their private transport handle. This ownership model is intentional: each in-flight retrieval task owns its peer set exclusively, so there is no shared-state contention between concurrent acquisitions. + +## Interface Design + +The interface exposes three pure-virtual operations and one non-virtual template helper. + +`addPeers(limit, hasItem, onPeerAdded)` is the peer-selection entry point. The caller passes a `limit` on how many new peers to add, a predicate `hasItem` that returns whether a specific peer likely holds the target data, and a callback `onPeerAdded` to execute immediately when a peer is accepted. The concrete implementation in `PeerSetImpl::addPeers()` scores every known peer via `Peer::getScore(hasItem(peer))`, sorts them in descending score order, and then iterates to add up to `limit` peers that haven't been tracked before. The score-based ranking favors peers that have signaled they hold the item, while still considering connection quality. Deduplication is enforced by a `std::set peers_` that prevents the same peer from being re-added across multiple `addPeers` calls for the same acquisition. + +`sendRequest` is overloaded in two forms. The virtual form takes a raw `google::protobuf::Message`, the `protocol::MessageType` enum, and an optional `shared_ptr`. When `peer` is non-null the message goes only to that peer; when null it broadcasts to every tracked peer by looking each `Peer::id_t` up in the overlay's `findPeerByShortID`. The non-virtual template wrapper on top deduces the `MessageType` automatically via `protocolMessageType()` (defined in `ProtocolMessage.h` for each concrete protobuf type like `TMGetLedger` or `TMReplayDeltaRequest`). This design solves the templated-virtual-function impossibility: the type-safe API lives in the non-virtual template, while the single virtual dispatch point handles the erased protobuf base class. + +`getPeerIds()` exposes the set of numeric peer IDs already added, letting callers check how many peers are engaged or avoid re-requesting data from peers that have already been contacted. + +## The Builder and Dummy Patterns + +`PeerSetBuilder` is a minimal abstract factory with a single `build()` method. Its purpose is testability and initialization-order safety. Subsystems like `InboundLedgersImp` and `InboundTransactions` receive a `std::unique_ptr` at construction and call `build()` each time a new acquisition is started. This means the concrete `PeerSetImpl`—which holds an `Application&` reference—is created on demand rather than upfront, and in tests a mock builder can substitute a controlled implementation without touching production code paths. + +`make_DummyPeerSet()` serves a specific, documented niche: `ApplicationImp::loadOldLedger()` constructs `InboundLedger` objects to replay historical state but does not want or need live peer network activity. Rather than introducing an optional nullable pointer that every call site must guard, the codebase injects a `DummyPeerSet` that fulfills the interface contract while logging an error and doing nothing. This "fail loudly but don't crash" design catches accidental calls in development without crashing production nodes during startup replay. + +## Lifetime and Concurrency Ownership + +Each `PeerSet` instance is heap-allocated and owned by exactly one retrieval task via `std::unique_ptr`. The `InboundLedger` stores it as `std::unique_ptr mPeerSet`, as does `TransactionAcquire`. Because ownership is exclusive and callers serialize access through their own mutex (`ScopedLockType` in `TimeoutCounter`-derived classes), the `PeerSetImpl` itself needs no internal locking. The `peers_` set is mutated only inside `addPeers` calls, which the caller already holds a lock over. + +## Relationship to Other Files + +- **`detail/PeerSet.cpp`** — provides `PeerSetImpl`, `PeerSetBuilderImpl`, and `DummyPeerSet`, all hidden behind the factory functions. None of these classes appear in any header. +- **`Peer.h`** — defines `Peer::id_t` (`uint32_t`) and `Peer::getScore(bool)`, which are both central to peer selection logic. +- **`detail/ProtocolMessage.h`** — defines the `protocolMessageType()` overloads that the template `sendRequest` wrapper depends on. +- **`InboundLedger.h`**, **`TransactionAcquire.h`**, **`LedgerReplayer.h`** — all hold `std::unique_ptr` and are the primary consumers of this interface. The `InboundLedger` additionally uses `PeerSet::getPeerIds()` to count active peers when deciding whether to give up on an acquisition. \ No newline at end of file diff --git a/src/xrpld/overlay/ReduceRelayCommon.h.ai.json b/src/xrpld/overlay/ReduceRelayCommon.h.ai.json new file mode 100644 index 0000000000..ab1c788bc3 --- /dev/null +++ b/src/xrpld/overlay/ReduceRelayCommon.h.ai.json @@ -0,0 +1,18 @@ +{ + "args": [], + "classes": [], + "description": "Defines constants and configuration parameters for the reduce-relay feature in XRPL, which optimizes message routing and reduces gossip flooding among peers.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/ReduceRelayCommon.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "reduce_relay" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/ReduceRelayCommon.h.ai.md b/src/xrpld/overlay/ReduceRelayCommon.h.ai.md new file mode 100644 index 0000000000..abd053afc6 --- /dev/null +++ b/src/xrpld/overlay/ReduceRelayCommon.h.ai.md @@ -0,0 +1,40 @@ +# `ReduceRelayCommon.h` — Shared Constants for Reduce-Relay Gossip Optimization + +This header is the single source of truth for all tuning parameters that govern the XRPL reduce-relay feature. It contains nothing but a set of `constexpr` constants nested inside `xrpl::reduce_relay`, making it a pure configuration manifest rather than an implementation file. Every component of the reduce-relay subsystem — `Slot.h`, `Squelch.h`, and `PeerImp.cpp` — imports this header to share the same numeric policy. + +## Why Reduce-Relay Exists + +Prior to this optimization, every validator message (proposals and validations) was flooded to all connected peers, a classic gossip broadcast. As peer counts grow, this creates O(n) redundant relay work per message. The reduce-relay feature, described in the [XRPL blog post](https://xrpl.org/blog/2021/message-routing-optimizations-pt-1-proposal-validation-relaying.html) referenced in the file, replaces flooding with a selective scheme: for each validator, only a small number of "selected" peers are allowed to relay its messages. All other peers are *squelched* — they receive a `TMSquelch` protocol message telling them to stop forwarding that validator's messages for a bounded time window. + +## Squelch Duration Constants + +The squelch window is intentionally randomized between `MIN_UNSQUELCH_EXPIRE` (300 s) and a computed upper bound, rather than fixed. This prevents the thundering-herd effect where all squelches expire simultaneously, which would cause every formerly-squelched peer to resume relaying in the same second and flood the network anew. + +The upper bound of the window is computed as: + +``` +max_squelch = min( + max(MAX_UNSQUELCH_EXPIRE_DEFAULT, SQUELCH_PER_PEER * number_of_peers), + MAX_UNSQUELCH_EXPIRE_PEERS +) +``` + +`MAX_UNSQUELCH_EXPIRE_DEFAULT` (600 s) is the baseline ceiling. `SQUELCH_PER_PEER` (10 s/peer) scales the ceiling with the number of peers being squelched, so a node with more overlay connections grants longer squelch windows to avoid premature re-flooding. `MAX_UNSQUELCH_EXPIRE_PEERS` (3600 s) caps the absolute maximum at one hour, preventing pathologically long suppression on very large peer sets. `Squelch::addSquelch()` enforces these bounds as a validity check when processing incoming `TMSquelch` messages from upstream peers. + +## Peer Selection Thresholds + +`MIN_MESSAGE_THRESHOLD` (19) and `MAX_MESSAGE_THRESHOLD` (20) define a two-stage counting gate inside `Slot`. A peer is added to the *considered pool* once it surpasses `MIN_MESSAGE_THRESHOLD` messages. Only when `MAX_SELECTED_PEERS` (5) members of the considered pool each individually cross `MAX_MESSAGE_THRESHOLD` does a selection round fire. The one-message gap between thresholds is intentional: it lets the system observe that a peer has *continued* to send after crossing the first threshold before committing it to the candidate set. The selection round then randomly picks exactly `MAX_SELECTED_PEERS` peers from the considered pool and squelches the rest. + +`MAX_SELECTED_PEERS = 5` is the cap on how many relay sources are permitted per validator. This is a direct tradeoff between redundancy (resilience if selected peers drop or idle) and bandwidth reduction. Fewer than five would save more bandwidth but risk message loss if selected peers disconnect. + +## Idle Detection + +`IDLED` (8 s) is the no-message threshold for declaring a peer inactive. `Slot::deleteIdlePeer()` checks every tracked peer and if the gap since its last message exceeds `IDLED`, it is treated as if it disconnected. If one of the *selected* peers idles, all squelched peers are immediately unsquelched and the slot reverts to `Counting` state to re-run peer selection. This constant is also reused by `Slots` as the expiry duration for the `peersWithMessage_` aged map, which deduplicates messages seen within the same idle window. + +## Boot-Up Delay + +`WAIT_ON_BOOTUP` (10 minutes) prevents the reduce-relay machinery from activating immediately after the server starts. `Slots::reduceRelayReady()` returns false until this much time has elapsed since epoch. The rationale is that a freshly starting node has not yet established a stable peer set; squelching peers prematurely during this churn phase could silence relay paths before a healthy selection is possible. + +## Transaction Queue Cap + +`MAX_TX_QUEUE_SIZE` (10,000) limits the number of transaction hashes that can accumulate in a peer's outbound `TMTransactions` batch. `PeerImp::addTxQueue()` flushes the queue whenever this limit is hit, and `doTransactions()` rejects incoming requests with more than this many hashes as malformed. The comment in the source makes the motivation explicit: at high TPS, unbounded accumulation could push `TMTransactions` payloads past the 64 MB protocol message size limit, so this constant is sized to stay safely below that ceiling. \ No newline at end of file diff --git a/src/xrpld/overlay/Slot.h.ai.json b/src/xrpld/overlay/Slot.h.ai.json new file mode 100644 index 0000000000..aebd7ea4d1 --- /dev/null +++ b/src/xrpld/overlay/Slot.h.ai.json @@ -0,0 +1,238 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 33, + "name": "SquelchHandler" + }, + { + "args": [ + "SquelchHandler const& handler", + "beast::Journal journal", + "uint16_t maxSelectedPeers" + ], + "lineno": 54, + "name": "Slot" + }, + { + "args": [ + "ServiceRegistry& registry", + "SquelchHandler const& handler", + "Config const& config" + ], + "lineno": 324, + "name": "Slots" + } + ], + "description": "Implements the reduce relay mechanism for peer message squelching and relay selection in the XRPL overlay network, including peer state management, slot management per validator, and squelch/unsquelch logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/Slot.h", + "functions": [ + { + "args": [ + "TP const& t" + ], + "lineno": 27, + "name": "epoch" + }, + { + "args": [ + "PublicKey const& validator" + ], + "lineno": 109, + "name": "Slot::deleteIdlePeer" + }, + { + "args": [ + "PublicKey const& validator", + "id_t id", + "protocol::MessageType type", + "ignored_squelch_callback callback" + ], + "lineno": 124, + "name": "Slot::update" + }, + { + "args": [ + "std::size_t npeers" + ], + "lineno": 210, + "name": "Slot::getSquelchDuration" + }, + { + "args": [ + "PublicKey const& validator", + "id_t id", + "bool erase" + ], + "lineno": 224, + "name": "Slot::deletePeer" + }, + { + "args": [], + "lineno": 260, + "name": "Slot::resetCounts" + }, + { + "args": [], + "lineno": 267, + "name": "Slot::initCounting" + }, + { + "args": [ + "PeerState state" + ], + "lineno": 274, + "name": "Slot::inState" + }, + { + "args": [ + "PeerState state" + ], + "lineno": 281, + "name": "Slot::notInState" + }, + { + "args": [], + "lineno": 288, + "name": "Slot::getSelected" + }, + { + "args": [], + "lineno": 297, + "name": "Slot::getPeers" + }, + { + "args": [], + "lineno": 349, + "name": "Slots::baseSquelchReady" + }, + { + "args": [], + "lineno": 354, + "name": "Slots::reduceRelayReady" + }, + { + "args": [ + "uint256 const& key", + "PublicKey const& validator", + "id_t id", + "protocol::MessageType type" + ], + "lineno": 366, + "name": "Slots::updateSlotAndSquelch" + }, + { + "args": [ + "uint256 const& key", + "PublicKey const& validator", + "id_t id", + "protocol::MessageType type", + "typename Slot::ignored_squelch_callback callback" + ], + "lineno": 377, + "name": "Slots::updateSlotAndSquelch" + }, + { + "args": [], + "lineno": 400, + "name": "Slots::deleteIdlePeers" + }, + { + "args": [ + "PublicKey const& validator", + "PeerState state" + ], + "lineno": 409, + "name": "Slots::inState" + }, + { + "args": [ + "PublicKey const& validator", + "PeerState state" + ], + "lineno": 418, + "name": "Slots::notInState" + }, + { + "args": [ + "PublicKey const& validator", + "SlotState state" + ], + "lineno": 427, + "name": "Slots::inState" + }, + { + "args": [ + "PublicKey const& validator" + ], + "lineno": 434, + "name": "Slots::getSelected" + }, + { + "args": [ + "PublicKey const& validator" + ], + "lineno": 442, + "name": "Slots::getPeers" + }, + { + "args": [ + "PublicKey const& validator" + ], + "lineno": 451, + "name": "Slots::getState" + }, + { + "args": [ + "id_t id", + "bool erase" + ], + "lineno": 460, + "name": "Slots::deletePeer" + }, + { + "args": [ + "uint256 const& key", + "id_t id" + ], + "lineno": 468, + "name": "Slots::addPeerMessage" + }, + { + "args": [ + "uint256 const& key", + "PublicKey const& validator", + "id_t id", + "protocol::MessageType type", + "typename Slot::ignored_squelch_callback callback" + ], + "lineno": 491, + "name": "Slots::updateSlotAndSquelch" + }, + { + "args": [ + "id_t id", + "bool erase" + ], + "lineno": 507, + "name": "Slots::deletePeer" + }, + { + "args": [], + "lineno": 513, + "name": "Slots::deleteIdlePeers" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + }, + { + "lineno": 17, + "name": "reduce_relay" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/Slot.h.ai.md b/src/xrpld/overlay/Slot.h.ai.md new file mode 100644 index 0000000000..23657f9e48 --- /dev/null +++ b/src/xrpld/overlay/Slot.h.ai.md @@ -0,0 +1,73 @@ +# `Slot.h` — Reduce-Relay Squelch Logic for Validator Messages + +## Role in the System + +XRPL nodes maintain an overlay mesh where each node forwards incoming validator messages (proposals and validations) to all connected peers — a flooding gossip model. At scale this creates quadratic message overhead: every validator message is relayed O(peers × validators) times across the network. `Slot.h` implements the *reduce-relay* mechanism that cuts this down by designating a small fixed set of peers as the authoritative source for each validator's messages, then instructing all other peers to stop forwarding those messages via a timed "squelch" command. + +The entire file lives in the `xrpl::reduce_relay` namespace and is a header-only template implementation consisting of three cooperating abstractions: `SquelchHandler`, `Slot`, and `Slots`. + +## `SquelchHandler` — Inversion of Control for Callbacks + +`SquelchHandler` is a pure abstract base with two virtual methods: `squelch()` and `unsquelch()`. `OverlayImpl` inherits from it and provides the real implementation that sends `TMSquelch` protocol messages to peers. The indirection exists explicitly for testability — the comment in the source notes it allows "on the fly changing callbacks" in unit tests. This pattern avoids coupling the selection algorithm to the network layer and lets tests inject mock handlers without modifying `Slot` logic. + +## `Slot` — Per-Validator Peer Selection State Machine + +Each `Slot` is associated with exactly one validator (by public key) and owns all state needed to decide which peers should relay that validator's messages. The constructor is `private` — only `Slots` (its `friend`) can create instances. + +### Dual State Machines + +There are two independent state machines running in parallel: + +**`SlotState`** tracks the slot as a whole: `Counting` (actively counting messages to decide which peers to select) or `Selected` (peers have been chosen, counting is suspended). + +**`PeerState`** tracks each individual peer within the slot: `Counting` (receiving and accumulating messages), `Selected` (designated relay source), or `Squelched` (instructed to stop forwarding). + +### The Selection Algorithm — `update()` + +The core logic lives in `update()`. On each incoming message from a validator routed through peer `id`: + +1. **New peer**: Insert with `PeerState::Counting` and call `initCounting()` to reset the whole slot back to counting state. This ensures a newly-seen peer doesn't get permanently excluded from the selection pool. + +2. **Expired squelch**: If the peer was `Squelched` but its expiry timestamp has passed, reset it to `Counting` and reinitiate the counting round. This is how squelch periods naturally expire without an active timer. + +3. **Active counting**: Increment the peer's message count. Once it exceeds `MIN_MESSAGE_THRESHOLD` (19 messages), the peer is added to the `considered_` pool — candidates for selection. When a peer's count hits `MAX_MESSAGE_THRESHOLD + 1` (21), it increments `reachedThreshold_`. + +4. **Inactivity guard**: If `lastSelected_` is more than `2 × MAX_UNSQUELCH_EXPIRE_DEFAULT` (20 minutes) in the past, the slot resets via `initCounting()` rather than proceeding. This handles the case where the node was nearly disconnected and its peer set has churned. + +5. **Selection trigger**: When `reachedThreshold_` reaches `maxSelectedPeers_` (configurable, default 5), selection fires. The algorithm randomly draws peers from `considered_`, skipping any that have been idle for more than `IDLED` (8 seconds). If fewer than `maxSelectedPeers_` non-idle peers can be found, `initCounting()` resets everything and defers to the next round — it would rather wait than squelch with an incomplete picture. + +6. **Squelching**: For every peer not in `selected` that isn't already `Squelched`, the handler's `squelch()` callback fires with a randomized duration, and the peer's state is set to `Squelched`. All message counts reset and the slot transitions to `SlotState::Selected`. + +The two-threshold design (`MIN_MESSAGE_THRESHOLD` / `MAX_MESSAGE_THRESHOLD`) is deliberate: a peer must receive at least 19 messages to enter the candidate pool, but selection only triggers when 5 peers have reached 20. This prevents premature selection when only a few fast peers have been heard from. + +### Squelch Duration — `getSquelchDuration()` + +Durations are randomized in the range `[MIN_UNSQUELCH_EXPIRE, max_squelch]` where `max_squelch = min(max(600s, 10s × npeers), 3600s)`. The `npeers` parameter is the number of peers being squelched in this round. The scaling ensures that nodes with many peers assign longer squelch windows, reducing the thundering-herd effect where all squelches expire simultaneously and all peers rush to relay again at once. + +### Recovery from Peer Loss — `deletePeer()` and `deleteIdlePeer()` + +The system must react gracefully when a selected relay source disappears. Two mechanisms handle this: + +`deletePeer(validator, id, erase)` is called when a peer disconnects. If the removed peer was `Selected`, the slot calls `unsquelch()` for every currently-`Squelched` peer, resets all state to `Counting`, and transitions back to `SlotState::Counting` — triggering a fresh selection round. The `erase` flag distinguishes a true disconnect (remove the `PeerInfo` entry) from an idle transition (keep the entry but reset its counts). + +`deleteIdlePeer()` is called periodically and walks all peers. Any peer whose `lastMessage` timestamp is older than `IDLED` (8s) is treated as a silent disconnect and passed to `deletePeer(..., false)`. This catches peers that have stopped sending without a clean disconnect event. + +## `Slots` — Container and Lifecycle Manager + +`Slots` owns all per-validator `Slot` instances in a `hash_map>`. It is the only entry point callers use. + +### Message Deduplication — `peersWithMessage_` + +`peersWithMessage_` is a `beast::aged_unordered_map` keyed by message hash (`uint256`), storing the set of peer IDs that have forwarded each message. Before updating any slot, `addPeerMessage()` checks this map — if the (message, peer) pair has already been seen, the update is skipped entirely. Entries age out after `IDLED` (8s), matching the TTL window within which a peer is expected to forward any given message. This is declared `inline static`, meaning it is shared across all `Slots` instantiations rather than per-instance. + +### Boot Delay + +`reduceRelayReady()` returns false until `WAIT_ON_BOOTUP` (10 minutes) has elapsed since process start. This gives the node time to establish a representative set of peer connections before the selection algorithm begins squelching — avoiding a scenario where only the first two or three peers to connect are ever chosen. + +### Idle Slot Expiry — `deleteIdlePeers()` + +Beyond idle peer detection, `deleteIdlePeers()` also deletes entire slots whose `lastSelected_` is older than `MAX_UNSQUELCH_EXPIRE_DEFAULT` (600s). A validator that has stopped producing messages shouldn't leave stale state indefinitely; removing the slot frees memory and ensures a clean start if the validator resumes. + +### Integration with `OverlayImpl` + +`OverlayImpl` holds a `reduce_relay::Slots` member and inherits from `SquelchHandler`. When `PeerImp` receives a validation or proposal that has been relayed within the `IDLED` window, it calls `overlay_.updateSlotAndSquelch()`. The slot system then fires `squelch()` back through `OverlayImpl`, which sends `TMSquelch` messages over the wire to tell remote peers to stop forwarding. The peer-side processing of received `TMSquelch` messages is handled separately in `PeerImp`, which uses a per-peer `Squelch` object tracking which validators it has been told to suppress. \ No newline at end of file diff --git a/src/xrpld/overlay/Squelch.h.ai.json b/src/xrpld/overlay/Squelch.h.ai.json new file mode 100644 index 0000000000..9cd0c8a081 --- /dev/null +++ b/src/xrpld/overlay/Squelch.h.ai.json @@ -0,0 +1,62 @@ +{ + "args": [ + { + "lineno": 16, + "name": "journal" + }, + { + "lineno": 25, + "name": "validator" + }, + { + "lineno": 25, + "name": "squelchDuration" + } + ], + "classes": [ + { + "args": [ + "beast::Journal journal" + ], + "lineno": 13, + "name": "Squelch" + } + ], + "description": "Implements a Squelch class template for managing and expiring squelch (suppression) of relaying messages from validators in the XRPL overlay network.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/Squelch.h", + "functions": [ + { + "args": [ + "validator", + "squelchDuration" + ], + "lineno": 25, + "name": "addSquelch" + }, + { + "args": [ + "validator" + ], + "lineno": 33, + "name": "removeSquelch" + }, + { + "args": [ + "validator" + ], + "lineno": 39, + "name": "expireSquelch" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "reduce_relay" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/Squelch.h.ai.md b/src/xrpld/overlay/Squelch.h.ai.md new file mode 100644 index 0000000000..f0091c47b9 --- /dev/null +++ b/src/xrpld/overlay/Squelch.h.ai.md @@ -0,0 +1,69 @@ +# `Squelch.h` — Per-Peer Validator Relay Suppression + +## Role in the System + +`Squelch.h` implements the downstream half of the XRPL reduce-relay protocol. The reduce-relay system was introduced to replace naive gossip flooding of validator messages (validations and proposals) with a smarter, peer-selected relay tree. The core idea — described on the [XRPL blog](https://xrpl.org/blog/2021/message-routing-optimizations-pt-1-proposal-validation-relaying.html) — is that when many peers all forward the same validator's message, most of those forwards are redundant. A coordinating node can select a small subset (typically 5, controlled by `MAX_SELECTED_PEERS`) to continue relaying and instruct the rest to stop. + +`Squelch` is the memory of that instruction on the silenced peer. Each `PeerImp` instance owns a `Squelch` object and consults it before forwarding any outgoing message that carries a validator's public key. This makes `Squelch` the enforcement point: it is not involved in deciding *which* peers to suppress — that decision is made upstream in `Slot`/`Slots` (see `Slot.h`) — it only records and enforces the resulting directive locally. + +## Architecture: Two Sides of Squelching + +The reduce-relay subsystem has two distinct roles that live in different objects: + +- **`Slots`** (upstream node, `Slot.h`): counts inbound validator messages per peer, selects a preferred relay set once `MAX_SELECTED_PEERS` peers hit `MAX_MESSAGE_THRESHOLD`, and calls `SquelchHandler::squelch()` for each peer that should stop relaying. This fires a `TMSquelch` protobuf message over the wire. +- **`Squelch`** (receiving peer, `Squelch.h`): processes incoming `TMSquelch` messages and records their effect locally. When this peer's `PeerImp::send()` is called with a validator-keyed message, it calls `expireSquelch()` first. A `false` return short-circuits the send and increments the `squelch_suppressed` traffic counter. + +This separation of concerns means the coordinating node's complex selection logic is entirely decoupled from the suppression enforcement on each individual peer. + +## Class Design + +`Squelch` is a straightforward clock-parameterized template. The only state is `squelched_`, a `hash_map` that maps a validator's public key to the absolute time when its squelch expires. The clock parameterization (`clock_type`) follows the same pattern used throughout the overlay codebase: production code uses `UptimeClock`, and unit tests inject a controlled mock clock to advance time deterministically without real delays. + +### `addSquelch(validator, squelchDuration)` + +Records a new squelch expiry. Before storing, it validates that `squelchDuration` falls within `[MIN_UNSQUELCH_EXPIRE (300 s), MAX_UNSQUELCH_EXPIRE_PEERS (3600 s)]`. These bounds come from `ReduceRelayCommon.h` and mirror the range that `Slot::getSquelchDuration()` uses when computing squelch durations — the upstream node picks a random value in that range so that squelches stagger their expirations and don't all expire simultaneously. + +If the duration falls outside these bounds, `addSquelch` does two defensive things: it logs an error and explicitly calls `removeSquelch`, clearing any previously active entry. This ensures a malformed `TMSquelch` cannot be exploited to leave stale squelch state. The caller in `PeerImp::onMessage` then charges the sender a `feeInvalidData` resource fee. + +### `expireSquelch(validator)` — Lazy Cleanup + +This method is called on every outbound send for a validator-keyed message. It returns `true` when the message should be forwarded (either no squelch exists, or the squelch has expired) and `false` when the suppression is still active. + +The expiry is intentionally lazy: there is no background timer or cleanup thread. The squelch map entry is only removed when `expireSquelch` is called *after* the expiry time has passed. This is safe because the check is always performed before sending, so the suppression always takes effect for the correct duration, and the cleanup happens naturally the first time a new message arrives after expiry — which is also when it matters. + +On the `Slot` side, `Slot::update()` mirrors this: when a message arrives from a peer whose `PeerState` is `Squelched` but whose `expire` time has passed, it transitions that peer back to `Counting` state and initiates a new selection round. + +### `removeSquelch(validator)` + +A simple erase from the map. Called when `PeerImp::onMessage(TMSquelch)` receives a message with `squelch == false` (the unsquelch signal), which the upstream `OverlayImpl` sends when a selected peer disconnects or goes idle and squelched peers need to be freed. + +## Integration with `PeerImp` + +``` +// PeerImp::send() — outbound message gate +auto validator = m->getValidatorKey(); +if (validator && !squelch_.expireSquelch(*validator)) +{ + overlay_.reportOutboundTraffic(TrafficCount::squelch_suppressed, ...); + return; // message dropped +} +``` + +This single guard at the send path is the only place `Squelch` intervenes in normal message flow. The `TMSquelch` handler populates the map: + +``` +// PeerImp::onMessage(TMSquelch) +if (!m->squelch()) + squelch_.removeSquelch(key); +else if (!squelch_.addSquelch(key, std::chrono::seconds{duration})) + fee_.update(Resource::feeInvalidData, "squelch duration"); +``` + +A guard prevents a validator node from squelching its own messages: if the incoming `TMSquelch`'s key matches `app_.getValidationPublicKey()`, the message is silently discarded. This prevents a misbehaving peer from exploiting the protocol to silence a validator at its own node. + +## Summary of Invariants + +- The squelch map holds only entries within the valid duration window; out-of-range entries are rejected and any stale entry for that key is proactively cleared. +- Expiry is checked lazily at send time; no background cleanup task is required. +- The `Squelch` class enforces suppression but makes no selection decisions — that responsibility belongs entirely to `Slot` and `Slots` in `Slot.h`. +- Clock parameterization keeps the class fully unit-testable without mocking the OS clock. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Cluster.cpp.ai.json b/src/xrpld/overlay/detail/Cluster.cpp.ai.json new file mode 100644 index 0000000000..f7495245b8 --- /dev/null +++ b/src/xrpld/overlay/detail/Cluster.cpp.ai.json @@ -0,0 +1,400 @@ +{ + "args": [ + { + "lineno": 12, + "name": "j" + }, + { + "lineno": 16, + "name": "identity" + }, + { + "lineno": 31, + "name": "name" + }, + { + "lineno": 31, + "name": "loadFee" + }, + { + "lineno": 31, + "name": "reportTime" + }, + { + "lineno": 48, + "name": "func" + }, + { + "lineno": 55, + "name": "nodes" + } + ], + "classes": [ + { + "args": [ + "beast::Journal j" + ], + "lineno": 12, + "name": "Cluster" + } + ], + "code_paths": [ + { + "call_chain": [ + "Cluster::load", + "boost::regex_match", + "parseBase58", + "Cluster::member", + "Cluster::update" + ], + "entry_point": "Cluster::load", + "purpose": "Loads a list of node entries from a config section, validates and adds them to the cluster.", + "validation_points": [ + "boost::regex_match (validates node entry string format)", + "parseBase58 (validates node identity string)", + "Cluster::member (checks for duplicate node identity)" + ] + }, + { + "call_chain": [ + "Cluster::update" + ], + "entry_point": "Cluster::update", + "purpose": "Updates or inserts a node in the cluster, with validation on report time and name.", + "validation_points": [ + "reportTime <= iter->getReportTime() (validates report time freshness)", + "name.empty() (validates name presence)" + ] + }, + { + "call_chain": [ + "Cluster::member" + ], + "entry_point": "Cluster::member", + "purpose": "Checks if a node identity is a member of the cluster.", + "validation_points": [ + "nodes_.find(identity) (validates existence of node identity)" + ] + } + ], + "data_flows": [ + { + "field": "node entry string (n)", + "flow": [ + "Section::values()", + "Cluster::load (for loop over n)", + "boost::regex_match (validates n)", + "match[1] (node identity string)", + "parseBase58 (converts to PublicKey)", + "Cluster::update" + ], + "origin": "Section::values() (config file or input source)", + "transformations": [ + "Regex parsing to extract identity and optional name", + "Base58 decoding to PublicKey" + ], + "validated_at": "boost::regex_match" + }, + { + "field": "node identity (match[1])", + "flow": [ + "match[1]", + "parseBase58", + "id (PublicKey)", + "Cluster::member", + "Cluster::update" + ], + "origin": "Extracted from regex match on node entry string", + "transformations": [ + "Base58 decoding" + ], + "validated_at": "parseBase58" + }, + { + "field": "node identity (id)", + "flow": [ + "id", + "Cluster::member", + "Cluster::update" + ], + "origin": "parseBase58", + "transformations": [ + "Lookup in nodes_ map" + ], + "validated_at": "Cluster::member" + }, + { + "field": "reportTime", + "flow": [ + "Cluster::update", + "reportTime <= iter->getReportTime()" + ], + "origin": "Passed to Cluster::update (from load or elsewhere)", + "transformations": [ + "Comparison with existing node's reportTime" + ], + "validated_at": "Cluster::update (reportTime <= iter->getReportTime())" + }, + { + "field": "name", + "flow": [ + "match[2]", + "trim_whitespace", + "Cluster::update", + "name.empty()" + ], + "origin": "Extracted from regex match[2] or passed to update", + "transformations": [ + "Whitespace trimming", + "Fallback to existing name if empty" + ], + "validated_at": "Cluster::update (name.empty())" + } + ], + "description": "Implements the xrpl::Cluster class, which manages a set of cluster nodes, allowing for membership checks, updates, iteration, and loading from configuration.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node entry string (n)", + "empty", + "string", + "validation" + ], + "evidence": "boost::regex_match at Cluster::load", + "issue_pattern": "Missing empty string validation for node entry string (n)", + "why_false_positive": "boost::regex_match validates node entry string (n) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "node entry string (n)", + "format", + "validation", + "invalid" + ], + "evidence": "boost::regex_match at Cluster::load", + "issue_pattern": "Missing format validation for node entry string (n)", + "why_false_positive": "boost::regex_match validates node entry string (n) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node identity (match[1])", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at Cluster::load", + "issue_pattern": "Missing empty string validation for node identity (match[1])", + "why_false_positive": "parseBase58 validates node identity (match[1]) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node identity (id)", + "empty", + "string", + "validation" + ], + "evidence": "member(*id) at Cluster::load", + "issue_pattern": "Missing empty string validation for node identity (id)", + "why_false_positive": "member(*id) validates node identity (id) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "reportTime", + "empty", + "string", + "validation" + ], + "evidence": "reportTime <= iter->getReportTime() at Cluster::update", + "issue_pattern": "Missing empty string validation for reportTime", + "why_false_positive": "reportTime <= iter->getReportTime() validates reportTime for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "name", + "empty", + "string", + "validation" + ], + "evidence": "name.empty() at Cluster::update", + "issue_pattern": "Missing empty string validation for name", + "why_false_positive": "name.empty() validates name for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/Cluster.cpp", + "functions": [ + { + "args": [ + "beast::Journal j" + ], + "lineno": 12, + "name": "Cluster" + }, + { + "args": [ + "PublicKey const& identity" + ], + "lineno": 16, + "name": "member" + }, + { + "args": [], + "lineno": 25, + "name": "size" + }, + { + "args": [ + "PublicKey const& identity", + "std::string name", + "std::uint32_t loadFee", + "NetClock::time_point reportTime" + ], + "lineno": 31, + "name": "update" + }, + { + "args": [ + "std::function func" + ], + "lineno": 48, + "name": "for_each" + }, + { + "args": [ + "Section const& nodes" + ], + "lineno": 55, + "name": "load" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage would be in unit tests for Cluster, ClusterNode, and overlay/Cluster. Tests should cover: valid/invalid node entry strings, duplicate identities, invalid base58 identities, reportTime ordering, and name handling. Gaps may exist if there are no tests for malformed entries, duplicate detection, or edge cases in reportTime/name logic. Look for test files like test_overlay_Cluster.cpp, test_ClusterNode.cpp, or integration tests for config loading. If these are missing, validation and error handling paths may not be fully tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "boost::regex, parseBase58, manual checks", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false from function (no exception); logs error", + "field": "node entry string (n)", + "location": "Cluster::load", + "validated_by": "boost::regex_match", + "validates": [ + "Checks that the entry matches the expected format: optional whitespace, alphanumeric node identity, optional comment" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns false from function (no exception); logs error", + "field": "node identity (match[1])", + "location": "Cluster::load", + "validated_by": "parseBase58", + "validates": [ + "Checks that the node identity is a valid base58-encoded NodePublic key" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "skips entry (continue); logs warning", + "field": "node identity (id)", + "location": "Cluster::load", + "validated_by": "member(*id)", + "validates": [ + "Checks that the node identity is not already present in the cluster (no duplicates)" + ], + "validation_type": "business_logic|uniqueness" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "reportTime", + "location": "Cluster::update", + "validated_by": "reportTime <= iter->getReportTime()", + "validates": [ + "Checks that the new reportTime is strictly greater than the previous reportTime for the node" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 0.8, + "error_thrown": "sets name to previous value if empty (no error/exception)", + "field": "name", + "location": "Cluster::update", + "validated_by": "name.empty()", + "validates": [ + "If name is empty, uses the previous name for the node" + ], + "validation_type": "business_logic|defaulting" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Cluster.cpp.ai.md b/src/xrpld/overlay/detail/Cluster.cpp.ai.md new file mode 100644 index 0000000000..4623e2a2da --- /dev/null +++ b/src/xrpld/overlay/detail/Cluster.cpp.ai.md @@ -0,0 +1,49 @@ +# `Cluster.cpp` — Trusted Peer Cluster Registry + +## Role in the System + +`Cluster.cpp` implements `xrpl::Cluster`, the in-process registry of trusted peer nodes that form an XRPL server cluster. A cluster is a group of `rippled` instances under common administrative control — typically a hub-and-spoke topology where each member is listed in the others' `[cluster_nodes]` configuration section. Being in the cluster grants a peer special trust: its messages are treated with higher credibility, its load-fee gossip is propagated, and it is permitted to behave differently than an untrusted stranger on the overlay. + +`Application` owns a single `Cluster` instance (created in `Application.cpp` line 387), loads it from configuration at startup via `SECTION_CLUSTER_NODES` (`[cluster_nodes]`), and then makes it available to the rest of the codebase through `Application::cluster()`. The overlay layer checks cluster membership per-connection to decide how to handle incoming data. + +## Data Structure and Comparator + +The registry is stored as `std::set`. `ClusterNode` is a simple value type carrying four fields: a `PublicKey` identity (const), a human-readable name, a `uint32_t` load fee, and a `NetClock::time_point` report time. + +The inner `Comparator` struct uses `is_transparent = std::true_type` — the heterogeneous lookup extension. This lets the set's `find()`, `lower_bound()`, and similar methods accept a bare `PublicKey` without constructing a temporary `ClusterNode`. The three overloads of `operator()` handle every combination of `(ClusterNode, ClusterNode)`, `(ClusterNode, PublicKey)`, and `(PublicKey, ClusterNode)` comparisons, all delegating to `PublicKey::operator<`. This is the reason `member()` and `update()` can call `nodes_.find(identity)` directly with a `PublicKey` argument. + +## `update()` — The Erase-Reinsert Pattern + +`std::set` elements are immutable through iterators because the set's ordering invariant would break if a key could silently change. This creates a design constraint: to update a node's name, load fee, or report time, the implementation must erase the old node and insert a new one. + +`update()` handles this carefully: + +1. **Stale rejection**: if the incoming `reportTime` is not strictly greater than the stored one, the update is rejected immediately (`return false`). This guards against replayed or out-of-order gossip messages. +2. **Name persistence**: if the caller passes an empty name but an entry already exists, the prior name is retained (`name = iter->name()`). Once a human-readable label is assigned, it cannot be silently erased by receiving a nameless heartbeat. +3. **Erase-then-reinsert**: the iterator returned by `nodes_.erase(iter)` is passed as a hint to `nodes_.emplace_hint(iter, ...)`, allowing the set to place the new element near where the old one was — a minor performance optimisation since the key is unchanged. + +When called from `load()` with the default `reportTime = NetClock::time_point{}` and no existing entry, the time check never fires and the node is simply inserted. + +## `load()` — Config Parsing + +`load()` is called once at startup with the `Section` parsed from the `[cluster_nodes]` block. Each line is expected to be a base58-encoded node public key, optionally followed by whitespace and a free-form comment (the node's name). + +The regex used is deliberately lenient about whitespace around the key and comment but strict about what constitutes valid comment characters — only alphanumeric characters are accepted for the identity token (`[[:alnum:]]+`), while the optional comment accepts anything non-whitespace up to trailing whitespace. Lines containing unexpected punctuation immediately after the key (e.g. `nHxxx!Comment`) fail the regex and cause `load()` to return `false` immediately, aborting the entire load. This fail-fast behaviour means a partially-parsed section is never used: the test suite explicitly verifies that even nodes appearing after a bad entry are not registered. + +The validation pipeline for each entry is: +1. `boost::regex_match` — rejects anything not matching the expected format +2. `parseBase58(TokenType::NodePublic, ...)` — confirms the alphanumeric token is a valid base58 node public key +3. `member(*id)` — silently skips duplicates with a `warn()` log (does not abort) +4. `update(*id, trim_whitespace(match[2]))` — registers the node + +The distinction between invalid format (hard failure, `return false`) and duplicate identity (soft warning, `continue`) reflects the difference between a configuration error that should block startup and an operator oversight that can be tolerated. + +## Thread Safety + +Every public method acquires `mutex_` via `std::lock_guard` before accessing `nodes_`. This makes all operations safe to call from any thread — important because overlay connections arrive on I/O threads while `load()` runs on the startup thread and `for_each()` may be called from RPC handlers or the cluster gossip handler. + +The header comment on `for_each()` explicitly warns that `update()` must not be called from within its callback. The reason is straightforward: both hold the same non-recursive `std::mutex`. Calling `update()` from inside the `for_each` callback would attempt to re-lock `mutex_` on the same thread, causing a deadlock. + +## Relationship to the Overlay + +`Peer.h` declares `isClusterMember()` and overlay peers use `Cluster::member()` to populate that flag at connection time. The `for_each()` iterator is used by cluster-gossip handlers to broadcast node-state updates to all current cluster members, and by the `peers` RPC command to report cluster status. The load-fee and report-time fields on `ClusterNode` carry the gossip payload that allows each cluster member to know the current load across the whole group. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp.ai.json b/src/xrpld/overlay/detail/ConnectAttempt.cpp.ai.json new file mode 100644 index 0000000000..1685bf4b27 --- /dev/null +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp.ai.json @@ -0,0 +1,464 @@ +{ + "args": [ + { + "lineno": 11, + "name": "app" + }, + { + "lineno": 12, + "name": "io_context" + }, + { + "lineno": 13, + "name": "remote_endpoint" + }, + { + "lineno": 14, + "name": "usage" + }, + { + "lineno": 15, + "name": "context" + }, + { + "lineno": 16, + "name": "id" + }, + { + "lineno": 17, + "name": "slot" + }, + { + "lineno": 18, + "name": "journal" + }, + { + "lineno": 19, + "name": "overlay" + }, + { + "lineno": 100, + "name": "ec" + }, + { + "lineno": 133, + "name": "reason" + }, + { + "lineno": 138, + "name": "name" + }, + { + "lineno": 144, + "name": "step" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "boost::asio::io_context& io_context", + "endpoint_type const& remote_endpoint", + "Resource::Consumer usage", + "shared_context const& context", + "std::uint32_t id", + "std::shared_ptr const& slot", + "beast::Journal journal", + "OverlayImpl& overlay" + ], + "lineno": 9, + "name": "ConnectAttempt" + } + ], + "code_paths": [ + { + "call_chain": [ + "ConnectAttempt::stop", + "ConnectAttempt::shutdown" + ], + "entry_point": "ConnectAttempt::stop", + "purpose": "Stops the connection attempt, ensuring shutdown is performed on the correct strand/thread.", + "validation_points": [ + "ConnectAttempt::shutdown (XRPL_ASSERT(strand_.running_in_this_thread()))" + ] + }, + { + "call_chain": [ + "ConnectAttempt::run", + "stream_.next_layer().async_connect", + "ConnectAttempt::onConnect (not shown in snippet)" + ], + "entry_point": "ConnectAttempt::run", + "purpose": "Initiates the asynchronous connection process to a remote endpoint.", + "validation_points": [ + "ConnectAttempt::run (indirect: posts to strand if not in thread)" + ] + }, + { + "call_chain": [ + "ConnectAttempt::shutdown", + "ConnectAttempt::tryAsyncShutdown" + ], + "entry_point": "ConnectAttempt::shutdown", + "purpose": "Performs shutdown logic, including validation of strand context and async SSL shutdown if needed.", + "validation_points": [ + "ConnectAttempt::shutdown (XRPL_ASSERT(strand_.running_in_this_thread()))", + "ConnectAttempt::tryAsyncShutdown (XRPL_ASSERT(strand_.running_in_this_thread()))" + ] + }, + { + "call_chain": [ + "ConnectAttempt::tryAsyncShutdown", + "stream_.async_shutdown", + "ConnectAttempt::onShutdown" + ], + "entry_point": "ConnectAttempt::tryAsyncShutdown", + "purpose": "Handles the asynchronous SSL shutdown handshake if required.", + "validation_points": [ + "ConnectAttempt::tryAsyncShutdown (XRPL_ASSERT(strand_.running_in_this_thread()))" + ] + } + ], + "data_flows": [ + { + "field": "strand_", + "flow": [ + "ConnectAttempt::ConnectAttempt (constructor)", + "Used in ConnectAttempt::stop, run, shutdown, tryAsyncShutdown, async operations" + ], + "origin": "Constructed in ConnectAttempt constructor using boost::asio::make_strand(io_context)", + "transformations": [ + "Used to post functions to correct thread/strand", + "Checked for running_in_this_thread() in validation" + ], + "validated_at": "ConnectAttempt::shutdown, ConnectAttempt::tryAsyncShutdown (XRPL_ASSERT)" + }, + { + "field": "socket_", + "flow": [ + "ConnectAttempt::ConnectAttempt (constructor)", + "Used in stop, shutdown, tryAsyncShutdown, run" + ], + "origin": "stream_ptr_->next_layer().socket() in constructor", + "transformations": [ + "Checked for is_open()", + "Used for async_connect and shutdown" + ], + "validated_at": "Indirectly, via strand_ validation before use" + }, + { + "field": "remote_endpoint_", + "flow": [ + "ConnectAttempt::ConnectAttempt (constructor)", + "Used in run for async_connect" + ], + "origin": "Passed as argument to ConnectAttempt constructor", + "transformations": [ + "Passed directly to async_connect" + ], + "validated_at": "Not explicitly validated in shown code" + }, + { + "field": "shutdown_", + "flow": [ + "Set in shutdown()", + "Checked in tryAsyncShutdown()" + ], + "origin": "Class member, set in shutdown()", + "transformations": [ + "Boolean flag to indicate shutdown in progress" + ], + "validated_at": "Not explicitly validated, but only set/checked on strand" + }, + { + "field": "ioPending_", + "flow": [ + "Set to true in run()", + "Checked in tryAsyncShutdown()" + ], + "origin": "Class member, set in run()", + "transformations": [ + "Boolean flag to indicate IO operation in progress" + ], + "validated_at": "Not explicitly validated, but only set/checked on strand" + } + ], + "description": "Implements the ConnectAttempt class, which manages the lifecycle of an outbound peer connection attempt in the XRPL overlay network, handling asynchronous connection, handshake, protocol negotiation, and error handling.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "strand context (thread safety for async operations)", + "validation", + "missing", + "check" + ], + "evidence": "Field strand context (thread safety for async operations) validated by XRPL_ASSERT (custom assertion macro), boost::asio strand for thread safety", + "issue_pattern": "Missing validation for strand context (thread safety for async operations)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), boost::asio strand for thread safety validates strand context (thread safety for async operations) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "strand_.running_in_this_thread()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at shutdown", + "issue_pattern": "Missing empty string validation for strand_.running_in_this_thread()", + "why_false_positive": "XRPL_ASSERT macro validates strand_.running_in_this_thread() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "strand_.running_in_this_thread()", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at tryAsyncShutdown", + "issue_pattern": "Missing empty string validation for strand_.running_in_this_thread()", + "why_false_positive": "XRPL_ASSERT macro validates strand_.running_in_this_thread() for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/ConnectAttempt.cpp", + "functions": [ + { + "args": [ + "Application& app", + "boost::asio::io_context& io_context", + "endpoint_type const& remote_endpoint", + "Resource::Consumer usage", + "shared_context const& context", + "std::uint32_t id", + "std::shared_ptr const& slot", + "beast::Journal journal", + "OverlayImpl& overlay" + ], + "lineno": 9, + "name": "ConnectAttempt" + }, + { + "args": [], + "lineno": 29, + "name": "~ConnectAttempt" + }, + { + "args": [], + "lineno": 36, + "name": "stop" + }, + { + "args": [], + "lineno": 51, + "name": "run" + }, + { + "args": [], + "lineno": 70, + "name": "shutdown" + }, + { + "args": [], + "lineno": 83, + "name": "tryAsyncShutdown" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 99, + "name": "onShutdown" + }, + { + "args": [], + "lineno": 120, + "name": "close" + }, + { + "args": [ + "std::string const& reason" + ], + "lineno": 132, + "name": "fail" + }, + { + "args": [ + "std::string const& name", + "error_code ec" + ], + "lineno": 137, + "name": "fail" + }, + { + "args": [ + "ConnectionStep step" + ], + "lineno": 143, + "name": "setTimer" + }, + { + "args": [], + "lineno": 186, + "name": "cancelTimer" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 197, + "name": "onTimer" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 222, + "name": "onConnect" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 259, + "name": "onHandshake" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 307, + "name": "onWrite" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 334, + "name": "onRead" + }, + { + "args": [], + "lineno": 355, + "name": "processResponse" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by integration and unit tests for overlay/peer connection logic, typically found in test files such as overlay/Overlay_test.cpp, PeerFinder_test.cpp, or similar. However, the specific validation of strand_.running_in_this_thread() via XRPL_ASSERT is not always directly tested unless tests deliberately invoke ConnectAttempt methods from the wrong thread/strand. There may be gaps in coverage for error paths (e.g., shutdown during various connection steps, or strand misuse). Tests that simulate multi-threaded misuse or forced shutdowns would be needed for full validation path coverage.", + "validation_architecture": { + "auto_validated_fields": [ + "strand context (thread safety for async operations)" + ], + "framework": "XRPL_ASSERT (custom assertion macro), boost::asio strand for thread safety", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "strand_.running_in_this_thread()", + "location": "shutdown", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures shutdown is called from the correct thread/strand context" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "AssertionError (via XRPL_ASSERT)", + "field": "strand_.running_in_this_thread()", + "location": "tryAsyncShutdown", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Ensures tryAsyncShutdown is called from the correct thread/strand context" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp.ai.md b/src/xrpld/overlay/detail/ConnectAttempt.cpp.ai.md new file mode 100644 index 0000000000..604ad2cf46 --- /dev/null +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp.ai.md @@ -0,0 +1,55 @@ +# `ConnectAttempt.cpp` — Outbound Peer Connection Lifecycle Manager + +`ConnectAttempt` is the dialer for the XRPL overlay network. When the `PeerFinder` subsystem decides the node needs more connections, `OverlayImpl` creates a `ConnectAttempt` to drive the entire process of reaching a remote peer — from raw TCP socket through TLS encryption and an HTTP-based capability handshake — and either promoting the result into a live `PeerImp` or reporting failure back to `PeerFinder`. This file contains the implementation of that state machine. + +## Connection Pipeline + +The connection follows a strict five-phase sequence managed by async callbacks chained through Boost.Asio: + +1. `run()` → `async_connect` → `onConnect()` +2. `onConnect()` → `async_handshake` (TLS client) → `onHandshake()` +3. `onHandshake()` → `async_write` (HTTP upgrade request) → `onWrite()` +4. `onWrite()` → `async_read` (HTTP upgrade response) → `onRead()` +5. `onRead()` → `processResponse()` → `PeerImp` construction or failure + +Each callback in the chain checks for errors, a pending shutdown flag, and socket validity before proceeding. If any check fails, it routes to `shutdown()`. This strict guard-and-delegate structure keeps each callback lean and the error paths uniform. + +## Dual-Timer Design + +The most architecturally distinctive feature is the two-timer system. A **global timer** (`timer_`) is set once at the start of `run()` with a 25-second hard ceiling. A **step timer** (`stepTimer_`) is reset at the start of each phase with tighter, phase-specific limits: 8 seconds each for TCP connect and TLS handshake, 3 seconds each for the HTTP write and read, and 2 seconds for TLS shutdown. + +Both timers share the single `onTimer()` callback. Because Boost.Asio will call the handler with `operation_aborted` when a timer is cancelled, the callback simply calls `std::chrono::steady_clock::now()` and compares it against each timer's expiry to determine which one fired. This avoids the need for separate handler functions or tagging parameters, at the cost of one additional time comparison. + +The global timer is only ever started once — guarded by checking that its expiry equals the default-constructed `time_point{}`. Subsequent calls to `setTimer()` only update the step timer, which automatically cancels the previous step timer via `expires_after`. This means the global timer ticks continuously across all phases while each phase has its own tighter budget, giving good diagnostic granularity in logs without needing two separate timeout mechanisms per phase. + +## Thread Safety: Strand Enforcement + +All operations are serialized on `strand_`, a `boost::asio::strand` constructed from the `io_context`. The two public entry points, `run()` and `stop()`, both contain a guard that re-posts to the strand if called from outside it, making them safe to call from any thread. The private `shutdown()` and `tryAsyncShutdown()` methods assert via `XRPL_ASSERT` that they are always running on the strand — a fast-fail for programming errors. + +## `ioPending_` and Safe Shutdown Interleaving + +A boolean `ioPending_` tracks whether an async I/O operation is in flight. `tryAsyncShutdown()` is a no-op when `ioPending_` is true; it only proceeds once the outstanding operation completes and calls back. Each async callback clears `ioPending_` first, then checks the `shutdown_` flag and calls `tryAsyncShutdown()` if needed. This prevents calling `stream_.async_shutdown()` while a `beast::http::async_write` or `async_read` is still pending on the same stream, which would be undefined behavior. + +## Selective TLS Shutdown + +`tryAsyncShutdown()` only initiates an SSL `async_shutdown` if the TLS handshake has been completed. If the connection is torn down during `TcpConnect` or `TlsHandshake` phases, there is no TLS session to close, so `close()` is called directly to shut the socket. This distinction prevents calling `async_shutdown` on a stream that never completed its handshake, which can cause spurious errors. + +`onShutdown()` deliberately suppresses logging for several expected error codes: `eof` (clean closure), `operation_aborted` (timeout-driven cancellation), `stream_truncated` (peer closed without a handshake), and "application data after close notify" (a benign SSL condition that some peers trigger). Only genuinely unexpected errors are logged. + +## Ownership Transfer via `slot_` Nullability + +The `slot_` member (a `std::shared_ptr`) encodes whether the connection succeeded. On destruction, the destructor checks `if (slot_ != nullptr)` and calls `peerFinder().on_closed(slot_)`. This cleanup is essential — every slot opened in `PeerFinder` must eventually be closed or the finder's bookkeeping becomes inconsistent. + +When `processResponse()` successfully creates a `PeerImp`, it moves `slot_` into the new peer object. The `ConnectAttempt` then holds a null `slot_`, so its destructor skips the `on_closed` call. The `stream_ptr_` unique pointer is likewise moved into `PeerImp`, leaving the `ConnectAttempt` without ownership of the socket, which is correct because the object is about to be destroyed. + +## `processResponse()`: The Handshake Verification Core + +This is the most security-sensitive method. A well-behaved peer returns HTTP 101 (Switching Protocols) with an `Upgrade` header listing its supported protocol versions. The code extracts these, verifies that exactly one is both present and supported locally via `isProtocolSupported()`, and rejects any ambiguous or unsupported negotiation. + +Before accepting the connection, `verifyHandshake()` validates the peer's claimed identity using the TLS-derived shared value computed by `makeSharedValue()`. This shared value is derived from the TLS session's finished messages, ensuring that if a man-in-the-middle is present the two sides will compute different values and the verification will fail. If the peer is a cluster member, that is noted in the log and used by `peerFinder().activate()`. + +A separate branch handles a `503 Service Unavailable` response carrying a JSON body with a `peer-ips` array. This is the XRPL redirect mechanism: an overloaded peer that cannot accept a new connection provides a list of alternative addresses. `ConnectAttempt` extracts and validates these endpoints and forwards them to `overlay_.peerFinder().onRedirects()`, where they feed back into the peer discovery pool. Non-redirect 503 responses (e.g., from a plain HTTP server) are rejected with a warning log rather than treated as redirect candidates. + +## Relationship to `PeerImp` and `OverlayImpl` + +`ConnectAttempt` inherits from `OverlayImpl::Child`, a minimal interface that requires implementing `stop()`. `OverlayImpl` holds a weak pointer to every child and calls `stop()` on all of them during overlay shutdown. Once the connection is promoted, `overlay_.add_active(peer)` registers the new `PeerImp` and the `ConnectAttempt` falls out of scope, causing its destructor to run (slot already null, so no cleanup needed on the `PeerFinder` side). The `PeerImp` then owns all subsequent I/O on that connection. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ConnectAttempt.h.ai.json b/src/xrpld/overlay/detail/ConnectAttempt.h.ai.json new file mode 100644 index 0000000000..6ecb820a9a --- /dev/null +++ b/src/xrpld/overlay/detail/ConnectAttempt.h.ai.json @@ -0,0 +1,250 @@ +{ + "args": [ + { + "lineno": 98, + "name": "app" + }, + { + "lineno": 99, + "name": "io_context" + }, + { + "lineno": 100, + "name": "remote_endpoint" + }, + { + "lineno": 101, + "name": "usage" + }, + { + "lineno": 102, + "name": "context" + }, + { + "lineno": 103, + "name": "id" + }, + { + "lineno": 104, + "name": "slot" + }, + { + "lineno": 105, + "name": "journal" + }, + { + "lineno": 106, + "name": "overlay" + }, + { + "lineno": 131, + "name": "step" + }, + { + "lineno": 149, + "name": "ec" + }, + { + "lineno": 155, + "name": "ec" + }, + { + "lineno": 157, + "name": "ec" + }, + { + "lineno": 159, + "name": "ec" + }, + { + "lineno": 164, + "name": "reason" + }, + { + "lineno": 165, + "name": "name" + }, + { + "lineno": 165, + "name": "ec" + }, + { + "lineno": 168, + "name": "ec" + }, + { + "lineno": 182, + "name": "step" + }, + { + "lineno": 201, + "name": "s" + }, + { + "lineno": 201, + "name": "ec" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "boost::asio::io_context& io_context", + "endpoint_type const& remote_endpoint", + "Resource::Consumer usage", + "shared_context const& context", + "Peer::id_t id", + "std::shared_ptr const& slot", + "beast::Journal journal", + "OverlayImpl& overlay" + ], + "lineno": 32, + "name": "ConnectAttempt" + } + ], + "description": "Defines the ConnectAttempt class, which manages outbound peer connection attempts in the XRPL network with comprehensive timeout handling and step diagnostics.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/ConnectAttempt.h", + "functions": [ + { + "args": [ + "Application& app", + "boost::asio::io_context& io_context", + "endpoint_type const& remote_endpoint", + "Resource::Consumer usage", + "shared_context const& context", + "Peer::id_t id", + "std::shared_ptr const& slot", + "beast::Journal journal", + "OverlayImpl& overlay" + ], + "lineno": 97, + "name": "ConnectAttempt" + }, + { + "args": [], + "lineno": 109, + "name": "~ConnectAttempt" + }, + { + "args": [], + "lineno": 115, + "name": "stop" + }, + { + "args": [], + "lineno": 121, + "name": "run" + }, + { + "args": [ + "ConnectionStep step" + ], + "lineno": 130, + "name": "setTimer" + }, + { + "args": [], + "lineno": 139, + "name": "cancelTimer" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 148, + "name": "onTimer" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 154, + "name": "onConnect" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 156, + "name": "onHandshake" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 158, + "name": "onWrite" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 160, + "name": "onRead" + }, + { + "args": [ + "std::string const& reason" + ], + "lineno": 163, + "name": "fail" + }, + { + "args": [ + "std::string const& name", + "error_code ec" + ], + "lineno": 164, + "name": "fail" + }, + { + "args": [], + "lineno": 165, + "name": "shutdown" + }, + { + "args": [], + "lineno": 166, + "name": "tryAsyncShutdown" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 167, + "name": "onShutdown" + }, + { + "args": [], + "lineno": 168, + "name": "close" + }, + { + "args": [], + "lineno": 176, + "name": "processResponse" + }, + { + "args": [ + "ConnectionStep step" + ], + "lineno": 181, + "name": "stepToString" + }, + { + "args": [ + "std::string const& s", + "boost::system::error_code& ec" + ], + "lineno": 200, + "name": "parse_endpoint" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ConnectAttempt.h.ai.md b/src/xrpld/overlay/detail/ConnectAttempt.h.ai.md new file mode 100644 index 0000000000..0fc165543d --- /dev/null +++ b/src/xrpld/overlay/detail/ConnectAttempt.h.ai.md @@ -0,0 +1,43 @@ +# `ConnectAttempt.h` — Outbound Peer Connection State Machine + +`ConnectAttempt` encapsulates the complete lifecycle of one outbound TCP/TLS/HTTP connection attempt to a remote XRPL peer. It lives inside the `xrpld/overlay/detail/` subsystem and is instantiated by `OverlayImpl` whenever the PeerFinder decides a new outbound connection should be made. Once the handshake succeeds, ownership of the underlying stream is transferred to a newly constructed `PeerImp` and the `ConnectAttempt` object dissolves. On any failure it simply shuts down and the PeerFinder slot is released from the destructor. + +## Inheritance and Ownership + +The class inherits from two bases. `OverlayImpl::Child` registers it with the overlay's child-tracking table, ensuring that `stop()` will be called on every live attempt when the overlay shuts down — a straightforward plugin pattern that avoids a separate registry. `std::enable_shared_from_this` is required because every async callback captures a `shared_ptr` to the attempt via `shared_from_this()`. This guarantees the object stays alive for the duration of any in-flight operation, which is the standard idiom for safely using Boost.Asio with reference-counted objects. + +## Dual-Timer Design + +The most architecturally interesting feature is the two-timer scheme. A single global `timer_` fires after `connectTimeout` (25 seconds) and acts as the hard limit for the entire operation. A second `stepTimer_` is reset at each phase transition with a much tighter bound: 8 s for TCP connect, 8 s for TLS handshake, 3 s for the HTTP write, 3 s for the HTTP read, and 2 s for SSL shutdown. Both timers share a single `onTimer()` handler that distinguishes them by comparing their expiry times to `steady_clock::now()`. + +The reason to keep two timers rather than just one is diagnostic precision. A slow TLS handshake that doesn't exceed 25 seconds total would be invisible with only a global timeout; the step timer surfaces exactly which phase is hanging and logs it. The global timer is initialized lazily — `setTimer()` checks whether `timer_.expiry()` is still at the epoch before arming it — so it is set exactly once for the lifetime of the attempt regardless of how many phase transitions occur. + +## Connection Pipeline + +`run()` is the entry point. It arms `ConnectionStep::TcpConnect` and calls `async_connect` on the underlying TCP socket. Each completion handler (`onConnect`, `onHandshake`, `onWrite`, `onRead`) follows an identical pattern: + +1. Clear `ioPending_`. +2. Check for `operation_aborted` (timer cancellation during shutdown) and route to `tryAsyncShutdown()`. +3. Check for any other error and call `fail()`. +4. Check the `shutdown_` flag. +5. Set `ioPending_ = true`, arm the next step timer, and issue the next async operation. + +The `ioPending_` flag is the key to safe shutdown sequencing. `tryAsyncShutdown()` will not start the SSL shutdown handshake until `ioPending_` is false, because starting an async SSL shutdown while another async operation is pending on the same stream would be undefined behaviour. Instead it defers, and the next completion handler calls `tryAsyncShutdown()` when it clears the flag. This avoids a race without any extra locks. + +## TLS Without Verification + +A subtle policy decision in `onConnect` is `stream_.set_verify_mode(boost::asio::ssl::verify_none)`. The peer identity is not validated through the TLS certificate chain; instead, both sides derive a `sharedValue` from the TLS session's finished data via `makeSharedValue()`, then cryptographically bind their node public keys to that value in the HTTP headers using `buildHandshake()`. This is a MITM-resistant proof-of-possession scheme that doesn't require a PKI: if a man-in-the-middle breaks the TLS session, the finished-data values on each side will differ, and the handshake will fail even though TLS certificate verification was skipped. + +## `processResponse()` — The Critical Path + +After HTTP headers are read, `processResponse()` handles three outcomes. If the peer responded with `101 Switching Protocols`, it validates the negotiated XRPL protocol version, recomputes the shared value, calls `verifyHandshake()` to authenticate the remote node's public key, and calls `PeerFinder::activate()` to officially register the peer. On success it constructs a `PeerImp`, moves the stream pointer and the `PeerFinder::Slot` into it (nulling `slot_` in the process — the destructor checks for this to avoid a double-close), and hands the peer to `OverlayImpl::add_active()`. + +If the peer returns `503 Service Unavailable` with a JSON body containing a `"peer-ips"` array, it is treated as a redirect: the IPs are parsed via the inline `parse_endpoint()` helper and forwarded to `PeerFinder::onRedirects()` so the discovery layer can connect to the suggested peers. Any other response code is treated as a hard failure. + +## Thread Safety + +All mutable state is accessed only on the `strand_`. Both `run()` and `stop()` begin with a guard that posts to the strand if the call arrives from outside it, so callers don't need to worry about synchronization. This makes the class safe to stop from any thread, which is necessary because `OverlayImpl::stopChildren()` may be called from the main thread while the strand runs on an IO thread pool. + +## Resource Cleanup + +If `ConnectAttempt` is destroyed before a successful connection (error, timeout, or external stop), the destructor calls `overlay_.peerFinder().on_closed(slot_)` to free the PeerFinder slot. When a `PeerImp` is created, `slot_` is moved into it, leaving the local member null, so the destructor's guard correctly skips the release. This single ownership pattern ensures every slot is closed exactly once. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Handshake.cpp.ai.json b/src/xrpld/overlay/detail/Handshake.cpp.ai.json new file mode 100644 index 0000000000..934d327d5d --- /dev/null +++ b/src/xrpld/overlay/detail/Handshake.cpp.ai.json @@ -0,0 +1,464 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "makeFeaturesRequestHeader" + ], + "entry_point": "makeFeaturesRequestHeader", + "purpose": "Constructs the X-Protocol-Ctl header for outgoing requests, encoding enabled features.", + "validation_points": [] + }, + { + "call_chain": [ + "makeFeaturesResponseHeader", + "isFeatureValue", + "getFeatureValue", + "beast::rfc2616::token_in_list", + "featureEnabled" + ], + "entry_point": "makeFeaturesResponseHeader", + "purpose": "Constructs the X-Protocol-Ctl header for outgoing responses, but only includes features that were requested and validated in the incoming request.", + "validation_points": [ + "isFeatureValue (validates feature value in request header)", + "featureEnabled (validates feature enabled flag in request header)" + ] + }, + { + "call_chain": [ + "isFeatureValue", + "getFeatureValue", + "beast::rfc2616::token_in_list" + ], + "entry_point": "isFeatureValue", + "purpose": "Checks if a specific feature in the X-Protocol-Ctl header matches a given value.", + "validation_points": [ + "getFeatureValue (extracts feature value using regex)", + "beast::rfc2616::token_in_list (validates value against allowed tokens)" + ] + }, + { + "call_chain": [ + "featureEnabled", + "isFeatureValue", + "getFeatureValue", + "beast::rfc2616::token_in_list" + ], + "entry_point": "featureEnabled", + "purpose": "Checks if a feature is enabled (value == '1') in the X-Protocol-Ctl header.", + "validation_points": [ + "isFeatureValue (validates feature enabled flag)" + ] + }, + { + "call_chain": [ + "getFeatureValue" + ], + "entry_point": "getFeatureValue", + "purpose": "Extracts the value of a feature from the X-Protocol-Ctl header using regex.", + "validation_points": [ + "boost::regex_search (validates header format and extracts value)" + ] + } + ], + "data_flows": [ + { + "field": "X-Protocol-Ctl header", + "flow": [ + "HTTP headers received", + "getFeatureValue extracts feature value using regex", + "isFeatureValue checks value using token_in_list", + "featureEnabled checks if value == '1'", + "makeFeaturesResponseHeader uses isFeatureValue/featureEnabled to decide what to echo" + ], + "origin": "Incoming HTTP request/response headers", + "transformations": [ + "Regex extraction (getFeatureValue)", + "Token validation (token_in_list)", + "Boolean check for '1' (featureEnabled)" + ], + "validated_at": "getFeatureValue, isFeatureValue, featureEnabled" + }, + { + "field": "feature values (e.g., lz4, 1)", + "flow": [ + "getFeatureValue extracts value", + "isFeatureValue validates value", + "featureEnabled checks for '1'", + "makeFeaturesResponseHeader includes only validated features" + ], + "origin": "Parsed from X-Protocol-Ctl header", + "transformations": [ + "String extraction", + "Token comparison", + "Conditional inclusion in response header" + ], + "validated_at": "isFeatureValue, featureEnabled" + }, + { + "field": "Outgoing X-Protocol-Ctl header", + "flow": [ + "Feature flags (bools) passed to makeFeaturesRequestHeader/makeFeaturesResponseHeader", + "Header string constructed", + "Header attached to outgoing HTTP request/response" + ], + "origin": "makeFeaturesRequestHeader or makeFeaturesResponseHeader", + "transformations": [ + "Manual string construction", + "Conditional inclusion based on validation" + ], + "validated_at": "makeFeaturesResponseHeader (validates before echoing features)" + } + ], + "description": "Implements XRPL peer handshake logic, including feature negotiation, handshake request/response construction, and cryptographic verification for secure peer connections.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "X-Protocol-Ctl header", + "empty", + "string", + "validation" + ], + "evidence": "boost::regex_search at getFeatureValue", + "issue_pattern": "Missing empty string validation for X-Protocol-Ctl header", + "why_false_positive": "boost::regex_search validates X-Protocol-Ctl header for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "X-Protocol-Ctl header", + "format", + "validation", + "invalid" + ], + "evidence": "boost::regex_search at getFeatureValue", + "issue_pattern": "Missing format validation for X-Protocol-Ctl header", + "why_false_positive": "boost::regex_search validates X-Protocol-Ctl header format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "feature value in X-Protocol-Ctl header", + "empty", + "string", + "validation" + ], + "evidence": "beast::rfc2616::token_in_list at isFeatureValue", + "issue_pattern": "Missing empty string validation for feature value in X-Protocol-Ctl header", + "why_false_positive": "beast::rfc2616::token_in_list validates feature value in X-Protocol-Ctl header for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "feature value in X-Protocol-Ctl header", + "format", + "validation", + "invalid" + ], + "evidence": "beast::rfc2616::token_in_list at isFeatureValue", + "issue_pattern": "Missing format validation for feature value in X-Protocol-Ctl header", + "why_false_positive": "beast::rfc2616::token_in_list validates feature value in X-Protocol-Ctl header format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "feature enabled flag in X-Protocol-Ctl header", + "empty", + "string", + "validation" + ], + "evidence": "isFeatureValue at featureEnabled", + "issue_pattern": "Missing empty string validation for feature enabled flag in X-Protocol-Ctl header", + "why_false_positive": "isFeatureValue validates feature enabled flag in X-Protocol-Ctl header for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "feature values in request header", + "empty", + "string", + "validation" + ], + "evidence": "manual string construction at makeFeaturesRequestHeader", + "issue_pattern": "Missing empty string validation for feature values in request header", + "why_false_positive": "manual string construction validates feature values in request header for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "feature values in response header", + "empty", + "string", + "validation" + ], + "evidence": "isFeatureValue, featureEnabled at makeFeaturesResponseHeader", + "issue_pattern": "Missing empty string validation for feature values in response header", + "why_false_positive": "isFeatureValue, featureEnabled validates feature values in response header for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/Handshake.cpp", + "functions": [ + { + "args": [ + "headers", + "feature" + ], + "lineno": 11, + "name": "getFeatureValue" + }, + { + "args": [ + "headers", + "feature", + "value" + ], + "lineno": 22, + "name": "isFeatureValue" + }, + { + "args": [ + "headers", + "feature" + ], + "lineno": 31, + "name": "featureEnabled" + }, + { + "args": [ + "comprEnabled", + "ledgerReplayEnabled", + "txReduceRelayEnabled", + "vpReduceRelayEnabled" + ], + "lineno": 35, + "name": "makeFeaturesRequestHeader" + }, + { + "args": [ + "headers", + "comprEnabled", + "ledgerReplayEnabled", + "txReduceRelayEnabled", + "vpReduceRelayEnabled" + ], + "lineno": 47, + "name": "makeFeaturesResponseHeader" + }, + { + "args": [ + "ssl", + "get" + ], + "lineno": 61, + "name": "hashLastMessage" + }, + { + "args": [ + "ssl", + "journal" + ], + "lineno": 81, + "name": "makeSharedValue" + }, + { + "args": [ + "h", + "sharedValue", + "networkID", + "public_ip", + "remote_ip", + "app" + ], + "lineno": 104, + "name": "buildHandshake" + }, + { + "args": [ + "headers", + "sharedValue", + "networkID", + "public_ip", + "remote", + "app" + ], + "lineno": 137, + "name": "verifyHandshake" + }, + { + "args": [ + "crawlPublic", + "comprEnabled", + "ledgerReplayEnabled", + "txReduceRelayEnabled", + "vpReduceRelayEnabled" + ], + "lineno": 238, + "name": "makeRequest" + }, + { + "args": [ + "crawlPublic", + "req", + "public_ip", + "remote_ip", + "sharedValue", + "networkID", + "protocol", + "app" + ], + "lineno": 257, + "name": "makeResponse" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is low-level and likely tested indirectly via overlay handshake integration or protocol negotiation tests. Direct unit tests for getFeatureValue, isFeatureValue, and featureEnabled may exist in overlay/detail or protocol test suites, but are not visible in this file. Manual string construction and regex parsing are error-prone and should be covered by tests for malformed/missing/extra header values, but such edge cases may not be fully tested. There is a risk that malformed X-Protocol-Ctl headers or unexpected feature values are not robustly tested. Look for tests in overlay/Overlay_test.cpp, overlay/Handshake_test.cpp, or protocol/HTTPHeaders_test.cpp. Gaps likely exist for malformed header formats, multiple features, or invalid tokens.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "boost::regex, beast::rfc2616", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns std::nullopt if not matched)", + "field": "X-Protocol-Ctl header", + "location": "getFeatureValue", + "validated_by": "boost::regex_search", + "validates": [ + "Checks if the X-Protocol-Ctl header exists", + "Validates that the header contains the feature in the format feature=([^;\\s]+)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false if not matched)", + "field": "feature value in X-Protocol-Ctl header", + "location": "isFeatureValue", + "validated_by": "beast::rfc2616::token_in_list", + "validates": [ + "Checks if the extracted feature value matches the expected value using RFC2616 token list rules" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false if not matched)", + "field": "feature enabled flag in X-Protocol-Ctl header", + "location": "featureEnabled", + "validated_by": "isFeatureValue", + "validates": [ + "Checks if the feature value is '1' (enabled)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none", + "field": "feature values in request header", + "location": "makeFeaturesRequestHeader", + "validated_by": "manual string construction", + "validates": [ + "Only includes features in header if corresponding boolean is true" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none", + "field": "feature values in response header", + "location": "makeFeaturesResponseHeader", + "validated_by": "isFeatureValue, featureEnabled", + "validates": [ + "Only includes features in response if both local and remote support/enable them" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Handshake.cpp.ai.md b/src/xrpld/overlay/detail/Handshake.cpp.ai.md new file mode 100644 index 0000000000..6037d0d36f --- /dev/null +++ b/src/xrpld/overlay/detail/Handshake.cpp.ai.md @@ -0,0 +1,49 @@ +# `src/xrpld/overlay/detail/Handshake.cpp` + +## Role in the System + +This file implements the application-layer handshake that XRPL peer nodes exchange immediately after a TLS connection is established. Its purpose is twofold: it establishes mutually authenticated peer identity using secp256k1 signatures and cryptographically binds those identities to the specific TLS session in use, while simultaneously negotiating which optional protocol features the two peers will use for the lifetime of the connection. + +The handshake sits at the boundary between the raw TLS stream (provided by Boost.Asio/Beast) and the `PeerImp` / `ConnectAttempt` layer that manages live peer state. Neither side begins exchanging XRPL protocol messages until `verifyHandshake` succeeds. + +## Cryptographic Shared Value + +The most subtle piece in this file is `makeSharedValue`. Before any application header is validated, both sides independently derive a `uint256` from the TLS session itself using `hashLastMessage`, which calls either `SSL_get_finished` or `SSL_get_peer_finished` to retrieve the TLS finished messages. Each message is SHA-512 hashed, the two resulting 512-bit values are XOR-ed, and the XOR result is reduced to 256 bits via `sha512Half`. + +This design is explicitly flagged as non-standard (see the inline comment referencing OpenSSL issue #5509 and XRPLF/rippled #2413). The reason for this approach: the TLS finished message is derived from a transcript of the full handshake, so both peers compute the same value only when they are literally the same TLS session endpoints. A man-in-the-middle would terminate two separate TLS sessions, producing different finished messages and therefore a different shared value. A guard against a degenerate edge case — two identical finished messages whose XOR is zero — is present and treated as a hard failure. + +The `sharedValue` flows directly into `buildHandshake` and `verifyHandshake`. The connecting peer signs it with its node private key; the accepting peer verifies that signature against the claimed public key. The comment in `verifyHandshake` makes the two-for-one security property explicit: the verification simultaneously proves possession of the private key *and* that the TLS session is end-to-end with the claimed node, not proxied. + +## Handshake Construction and Verification + +`buildHandshake` populates a set of HTTP fields that are attached to either an outbound request (by `ConnectAttempt`) or the 101 Switching Protocols response (by `PeerImp`). The fields inserted include: + +- **`Network-ID`** — optional; allows early detection of cross-network connections before wasting resources on full negotiation. +- **`Network-Time`** — the local XRPL clock value; the recipient enforces a ±20-second tolerance in `verifyHandshake`. This prevents replay and clock-skew attacks. +- **`Public-Key`** — base58-encoded secp256k1 node identity key. +- **`Session-Signature`** — the shared value signed by the node private key, base64-encoded. +- **`Instance-Cookie`** — a runtime-unique identifier used elsewhere to detect duplicate connections. +- **`Server-Domain`** — optional TOML domain hint. +- **`Local-IP`** / **`Remote-IP`** — only inserted if the corresponding IP is public and non-unspecified; used during `verifyHandshake` to cross-check that both sides agree on the addresses they observe for each other, which can surface NAT misconfigurations. + +`verifyHandshake` checks every one of these fields defensively and throws `std::runtime_error` on any failure. The use of exceptions rather than error codes is a deliberate pattern in this layer (noted in the `.ai.json`): callers in `ConnectAttempt` and `OverlayImpl` wrap the call in a try/catch and tear down the connection on any exception, making the control flow clean without threading error codes through multiple levels. + +The self-connection check (`publicKey == app.nodeIdentity().first`) catches the case where a node accidentally connects to itself — something the `peerFinder` also guards against at the TCP level, but a second check here catches cases that slip through (e.g., connecting via a different IP). + +## Feature Negotiation + +Four optional protocol extensions are negotiated via the `X-Protocol-Ctl` HTTP header: LZ4 message compression (`compr`), ledger replay (`ledgerreplay`), transaction reduce-relay (`txrr`), and validation/proposal reduce-relay (`vprr`). The format is `feature=value[;feature=value]*`. + +The asymmetry between `makeFeaturesRequestHeader` and `makeFeaturesResponseHeader` captures the standard capability negotiation pattern: the initiator unconditionally advertises everything it supports; the responder only echoes back features that are both locally configured *and* present in the request. This ensures both peers arrive at the same set of enabled features without a separate acknowledgment round-trip. + +`getFeatureValue` uses `boost::regex` to extract a feature's value from the `X-Protocol-Ctl` string, returning `std::nullopt` when absent. `isFeatureValue` layers RFC 2616 token-list semantics on top via `beast::rfc2616::token_in_list`, which correctly handles comma-separated value lists. `featureEnabled` is a thin convenience wrapper that checks for the value `"1"`. The header-only `peerFeatureEnabled` template (in `Handshake.h`) combines local configuration with peer-reported capability for use throughout the peer management layer. + +## HTTP Request and Response Assembly + +`makeRequest` builds the outbound HTTP/1.1 GET request that initiates the peer connection. It follows the WebSocket-style protocol upgrade pattern: `Connection: Upgrade`, `Upgrade: `, `Connect-As: Peer`. This allows the entire peer handshake to look like an HTTP upgrade from the perspective of any intermediate infrastructure. + +`makeResponse` constructs the 101 Switching Protocols response, calls `makeFeaturesResponseHeader` to echo negotiated features back, and then delegates to `buildHandshake` to insert all the identity and authentication fields. The `Upgrade` field in the response is set to the single agreed `ProtocolVersion`, narrowing from the list of versions in the request. + +## Caller Context + +`ConnectAttempt` uses `makeSharedValue` → `makeRequest` + `buildHandshake` → async HTTP write, then on the response uses `makeSharedValue` + `verifyHandshake` to complete the outbound side. `PeerImp::doAccept` handles the inbound side: `OverlayImpl` already ran `verifyHandshake` once to extract the public key for routing, and `PeerImp` calls `makeSharedValue` a second time to produce the shared value needed for `makeResponse`. The double computation is safe and intentional — the TLS state is stable at that point. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Handshake.h.ai.json b/src/xrpld/overlay/detail/Handshake.h.ai.json new file mode 100644 index 0000000000..e24b119b6c --- /dev/null +++ b/src/xrpld/overlay/detail/Handshake.h.ai.json @@ -0,0 +1,137 @@ +{ + "args": [], + "classes": [], + "description": "This file provides functions and utilities for handling the handshake and feature negotiation process in the XRPL peer-to-peer overlay network, including SSL shared value computation, HTTP handshake building and verification, and protocol feature negotiation.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/Handshake.h", + "functions": [ + { + "args": [ + "stream_type& ssl", + "beast::Journal journal" + ], + "lineno": 28, + "name": "makeSharedValue" + }, + { + "args": [ + "boost::beast::http::fields& h", + "uint256 const& sharedValue", + "std::optional networkID", + "beast::IP::Address public_ip", + "beast::IP::Address remote_ip", + "Application& app" + ], + "lineno": 39, + "name": "buildHandshake" + }, + { + "args": [ + "boost::beast::http::fields const& headers", + "uint256 const& sharedValue", + "std::optional networkID", + "beast::IP::Address public_ip", + "beast::IP::Address remote", + "Application& app" + ], + "lineno": 54, + "name": "verifyHandshake" + }, + { + "args": [ + "bool crawlPublic", + "bool comprEnabled", + "bool ledgerReplayEnabled", + "bool txReduceRelayEnabled", + "bool vpReduceRelayEnabled" + ], + "lineno": 73, + "name": "makeRequest" + }, + { + "args": [ + "bool crawlPublic", + "http_request_type const& req", + "beast::IP::Address public_ip", + "beast::IP::Address remote_ip", + "uint256 const& sharedValue", + "std::optional networkID", + "ProtocolVersion version", + "Application& app" + ], + "lineno": 89, + "name": "makeResponse" + }, + { + "args": [ + "boost::beast::http::fields const& headers", + "std::string const& feature" + ], + "lineno": 120, + "name": "getFeatureValue" + }, + { + "args": [ + "boost::beast::http::fields const& headers", + "std::string const& feature", + "std::string const& value" + ], + "lineno": 130, + "name": "isFeatureValue" + }, + { + "args": [ + "boost::beast::http::fields const& headers", + "std::string const& feature" + ], + "lineno": 142, + "name": "featureEnabled" + }, + { + "args": [ + "headers const& request", + "std::string const& feature", + "std::string value", + "bool config" + ], + "lineno": 151, + "name": "peerFeatureEnabled" + }, + { + "args": [ + "headers const& request", + "std::string const& feature", + "bool config" + ], + "lineno": 161, + "name": "peerFeatureEnabled" + }, + { + "args": [ + "bool comprEnabled", + "bool ledgerReplayEnabled", + "bool txReduceRelayEnabled", + "bool vpReduceRelayEnabled" + ], + "lineno": 170, + "name": "makeFeaturesRequestHeader" + }, + { + "args": [ + "http_request_type const& headers", + "bool comprEnabled", + "bool ledgerReplayEnabled", + "bool txReduceRelayEnabled", + "bool vpReduceRelayEnabled" + ], + "lineno": 185, + "name": "makeFeaturesResponseHeader" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 18, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Handshake.h.ai.md b/src/xrpld/overlay/detail/Handshake.h.ai.md new file mode 100644 index 0000000000..1405b08c05 --- /dev/null +++ b/src/xrpld/overlay/detail/Handshake.h.ai.md @@ -0,0 +1,74 @@ +# `overlay/detail/Handshake.h` — Peer Handshake and Feature Negotiation + +This header is the interface contract for the XRPL peer-to-peer overlay's connection establishment sequence. When a rippled node opens or accepts a TCP connection to another peer, the link is not promoted to the live protocol stream immediately; instead it undergoes a two-phase negotiation — first an SSL/TLS setup, then an HTTP/1.1 Upgrade exchange that carries cryptographic identity proof and capability advertisement. Every function declared here belongs to one of those two phases, and the implementation in `Handshake.cpp` is the sole place in the codebase where those mechanics live. + +## Transport Type Aliases + +The file opens by pinning down the concrete network types used throughout the overlay: + +```cpp +using socket_type = boost::beast::tcp_stream; +using stream_type = boost::beast::ssl_stream; +using request_type = boost::beast::http::request; +using http_request_type = boost::beast::http::request; +using http_response_type = boost::beast::http::response; +``` + +`request_type` (empty body) is the outbound HTTP upgrade request; the inbound copy the server receives carries a `dynamic_body` and is `http_request_type`. This distinction matters because the server must echo certain request headers back in its `http_response_type`, which is also dynamic-body. Aliasing these here lets the entire overlay subsystem share one authoritative definition. + +## MITM Protection via SSL Finished Messages — `makeSharedValue` + +The central security primitive is `makeSharedValue`. It derives a 256-bit value from the TLS session's *Finished* handshake messages using OpenSSL's `SSL_get_finished` and `SSL_get_peer_finished`. Both sides hash their own and their peer's Finished messages (each through SHA-512), XOR the two hashes together, and then compress to 256 bits via `sha512Half`. Because TLS Finished messages are keyed to the specific session key material, two peers who share a direct TLS connection will compute identical values, while a man-in-the-middle who terminates two separate TLS sessions will produce a different XOR result on each side. + +The implementation guards against a degenerate case: if both Finished hashes are identical (XOR result is zero), the handshake is aborted. This prevents an attacker from constructing a scenario where the cancellation property of XOR yields a convincing but meaningless shared secret. + +This approach is acknowledged in the code as "non-standard" with references to an OpenSSL issue and a rippled tracking issue, since TLS channel binding via Finished messages is fragile in the presence of session resumption. The alternative — standard TLS channel bindings — has not yet been adopted. + +## Building and Verifying the HTTP Identity Handshake + +`buildHandshake` populates a set of HTTP header fields that together prove a node's identity and describe its current state: + +- **`Public-Key`** — the node's secp256k1 public key, base58-encoded. +- **`Session-Signature`** — a digital signature over `sharedValue` made with the node's private key. This is the critical proof of identity: it demonstrates the sender holds the private key matching the claimed `Public-Key`, and that the signature is bound to this specific TLS session. +- **`Network-ID`** — an optional numeric tag that allows nodes to detect cross-network connections and reject them early. +- **`Network-Time`** — the sender's current network time, allowing the receiver to reject peers whose clocks have drifted beyond a 20-second tolerance. +- **`Instance-Cookie`** — a per-process unique ID, used implicitly to detect reconnections from the same instance. +- **`Local-IP` / `Remote-IP`** — the node's perceived public IP and what it believes the remote's IP is, enabling the receiver to cross-check connectivity information. +- **`Closed-Ledger` / `Previous-Ledger`** — the hashes of the most recently closed ledger, giving the peer immediate context about the sender's ledger state. + +`verifyHandshake` performs the inverse: it validates every field `buildHandshake` populated. The verification order is deliberately layered — network ID and clock skew checks come before the expensive cryptographic signature check. The signature check itself serves the dual purpose of confirming key ownership and confirming end-to-end TLS (no proxy). After verification the function returns the remote's `PublicKey`, which callers use to identify the peer going forward. All failure modes throw `std::runtime_error`, and callers in `ConnectAttempt.cpp` and `PeerImp.cpp` wrap the call in try/catch and disconnect on any exception. + +The self-connection check (`publicKey == app.nodeIdentity().first`) prevents a node from successfully handshaking with itself — a real concern if a node accidentally connects to its own listening port. + +The IP cross-check logic deserves attention: `Local-IP` (what the sender reports as their public IP) should match the IP address from which the receiver sees the connection arriving. `Remote-IP` (what the sender reports as the receiver's IP) should match the receiver's own known public IP. These checks catch situations where NAT or misconfiguration causes IP mismatches and can also detect certain proxy scenarios. + +## HTTP Upgrade Messages — `makeRequest` and `makeResponse` + +`makeRequest` constructs the outbound `GET /` HTTP/1.1 upgrade request. It sets `Upgrade` to the supported protocol version list from `supportedProtocolVersions()` and `Connect-As: Peer`. The `Crawl` header advertises whether the node's IP should be publicly discoverable by peer crawlers. Crucially, the built HTTP fields produced by `makeRequest` do not include the identity headers (`Public-Key`, `Session-Signature`, etc.); those are added by a separate `buildHandshake` call on the same fields object. + +`makeResponse` produces the `101 Switching Protocols` response. It selects a single concrete protocol version (already negotiated by the caller), mirrors the `Crawl` policy, and calls both `buildHandshake` (for identity fields) and `makeFeaturesResponseHeader` (for capability echo) in one shot. + +## Feature Negotiation via `X-Protocol-Ctl` + +The header defines a mini-protocol for advertising optional capabilities through the `X-Protocol-Ctl` HTTP header. The wire format is: + +``` +X-Protocol-Ctl: feature1=value1[,value2]*[; feature2=value1]* +``` + +Four features are currently defined as `constexpr` string literals: + +| Constant | Wire name | Purpose | +|---|---|---| +| `FEATURE_COMPR` | `compr` | LZ4 message compression | +| `FEATURE_VPRR` | `vprr` | Validation/proposal reduce-relay (base squelch) | +| `FEATURE_TXRR` | `txrr` | Transaction reduce-relay | +| `FEATURE_LEDGER_REPLAY` | `ledgerreplay` | Ledger replay subsystem | + +Requesters advertise all locally-enabled features via `makeFeaturesRequestHeader`, using `=lz4` for compression (a specific algorithm choice, not just a flag) and `=1` for boolean features. Responders use `makeFeaturesResponseHeader`, which enables a feature in the response only if both the local configuration enables it and the peer's request also declares it. This is a conservative AND-gate: neither side unilaterally enables a feature the other didn't offer. + +The helper `getFeatureValue` parses the raw header using a `boost::regex` search, returning the feature's value string if present. `isFeatureValue` checks membership using RFC 2616 token-list semantics, and `featureEnabled` is a convenience wrapper that tests for the value `"1"`. The two template overloads of `peerFeatureEnabled` additionally gate on a local boolean configuration flag, making the final enable/disable decision a single predicate usable by `PeerImp` after the handshake completes. + +## Usage Flow + +In `ConnectAttempt.cpp` (outbound) the sequence is: compute `makeSharedValue` → call `makeRequest` → call `buildHandshake` on the same field set → send. On receiving the response: compute `makeSharedValue` again → call `verifyHandshake` → if all checks pass, promote the connection. In `PeerImp.cpp` (inbound) the sequence is: the OverlayImpl layer already validated the request; `PeerImp` computes `makeSharedValue` once more (the comment notes this should not fail since OverlayImpl already succeeded) → calls `makeResponse` to build and send the 101 response with identity and capability headers. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Message.cpp.ai.json b/src/xrpld/overlay/detail/Message.cpp.ai.json new file mode 100644 index 0000000000..92296a15dc --- /dev/null +++ b/src/xrpld/overlay/detail/Message.cpp.ai.json @@ -0,0 +1,269 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Message::Message", + "Message::messageSize", + "Message::setHeader", + "google::protobuf::Message::SerializeToArray", + "Message::getBufferSize", + "Message::totalSize" + ], + "entry_point": "Message::Message", + "purpose": "Constructs a Message object from a protobuf message, sets up buffer, header, and serializes payload.", + "validation_points": [ + "XRPL_ASSERT(messageBytes, ...)", + "XRPL_ASSERT(getBufferSize() == totalSize(message), ...)" + ] + }, + { + "call_chain": [ + "Message::compress", + "Message::getType", + "xrpl::compression::compress", + "Message::setHeader" + ], + "entry_point": "Message::compress", + "purpose": "Compresses the message payload if eligible, resizes buffer, and sets compressed header.", + "validation_points": [ + "lambda in compress: if (messageBytes <= 70) return false;" + ] + } + ], + "data_flows": [ + { + "field": "messageBytes", + "flow": [ + "Message::messageSize", + "Message::Message (local variable)", + "XRPL_ASSERT(messageBytes, ...)", + "buffer_.resize(headerBytes + messageBytes)", + "message.SerializeToArray(buffer_.data() + headerBytes, messageBytes)", + "XRPL_ASSERT(getBufferSize() == totalSize(message), ...)" + ], + "origin": "Message::messageSize(message)", + "transformations": [ + "Calculated from protobuf message", + "Used to size buffer", + "Used to serialize payload" + ], + "validated_at": "XRPL_ASSERT(messageBytes, ...)" + }, + { + "field": "buffer_", + "flow": [ + "buffer_ (member)", + "buffer_.resize(headerBytes + messageBytes)", + "message.SerializeToArray(buffer_.data() + headerBytes, messageBytes)", + "used in compress()" + ], + "origin": "Message::Message (constructed as member)", + "transformations": [ + "Resized to fit header + payload", + "Filled with header and serialized payload" + ], + "validated_at": "XRPL_ASSERT(getBufferSize() == totalSize(message), ...)" + }, + { + "field": "message payload (protobuf)", + "flow": [ + "Message::Message parameter", + "message.SerializeToArray", + "buffer_", + "Message::compress (payload extracted from buffer_)", + "xrpl::compression::compress" + ], + "origin": "Message::Message parameter", + "transformations": [ + "Serialized to buffer_", + "Optionally compressed" + ], + "validated_at": "lambda in compress: if (messageBytes <= 70) return false;" + } + ], + "description": "Implements the xrpl::Message class for serializing, compressing, and handling protocol messages, including header formatting and buffer management for XRPL overlay network communication.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "messageBytes (size of protobuf message)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at Message::Message (constructor)", + "issue_pattern": "Missing empty string validation for messageBytes (size of protobuf message)", + "why_false_positive": "XRPL_ASSERT validates messageBytes (size of protobuf message) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "buffer size vs. message size", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at Message::Message (constructor)", + "issue_pattern": "Missing empty string validation for buffer size vs. message size", + "why_false_positive": "XRPL_ASSERT validates buffer size vs. message size for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "messageBytes (size of message payload)", + "empty", + "string", + "validation" + ], + "evidence": "lambda in Message::compress at Message::compress", + "issue_pattern": "Missing empty string validation for messageBytes (size of message payload)", + "why_false_positive": "lambda in Message::compress validates messageBytes (size of message payload) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "messageBytes (size of message payload)", + "range", + "bounds", + "validation" + ], + "evidence": "lambda in Message::compress at Message::compress", + "issue_pattern": "Missing range validation for messageBytes (size of message payload)", + "why_false_positive": "lambda in Message::compress validates messageBytes (size of message payload) range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/Message.cpp", + "functions": [ + { + "args": [ + "::google::protobuf::Message const& message", + "protocol::MessageType type", + "std::optional const& validator" + ], + "lineno": 6, + "name": "Message::Message" + }, + { + "args": [ + "::google::protobuf::Message const& message" + ], + "lineno": 23, + "name": "Message::messageSize" + }, + { + "args": [ + "::google::protobuf::Message const& message" + ], + "lineno": 32, + "name": "Message::totalSize" + }, + { + "args": [], + "lineno": 37, + "name": "Message::compress" + }, + { + "args": [ + "std::uint8_t* in", + "std::uint32_t payloadBytes", + "int type", + "Algorithm compression", + "std::uint32_t uncompressedBytes" + ], + "lineno": 77, + "name": "Message::setHeader" + }, + { + "args": [], + "lineno": 110, + "name": "Message::getBufferSize" + }, + { + "args": [ + "Compressed tryCompressed" + ], + "lineno": 114, + "name": "Message::getBuffer" + }, + { + "args": [ + "std::uint8_t const* in" + ], + "lineno": 126, + "name": "Message::getType" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested by overlay/message-related unit and integration tests, possibly in files like test_overlay.cpp, test_Message.cpp, or protocol/message tests. However, the specific XRPL_ASSERT validations (e.g., non-empty message, buffer size matching, compressibility threshold) may not be directly tested unless there are explicit tests for malformed or edge-case messages. Compression logic may be tested indirectly via message send/receive tests, but edge cases (e.g., messageBytes == 0, buffer size mismatches, compressible vs. non-compressible types) may lack direct coverage. Review of test files is needed to confirm coverage of all validation paths.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system, protobuf serialization", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "messageBytes (size of protobuf message)", + "location": "Message::Message (constructor)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that messageBytes is non-zero (non-empty message input)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or throws)", + "field": "buffer size vs. message size", + "location": "Message::Message (constructor)", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Checks that the buffer size matches the expected total message size" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "None (used for logic branching, not error)", + "field": "messageBytes (size of message payload)", + "location": "Message::compress", + "validated_by": "lambda in Message::compress", + "validates": [ + "Checks if messageBytes > 70 to determine compressibility" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Message.cpp.ai.md b/src/xrpld/overlay/detail/Message.cpp.ai.md new file mode 100644 index 0000000000..9a31898044 --- /dev/null +++ b/src/xrpld/overlay/detail/Message.cpp.ai.md @@ -0,0 +1,29 @@ +# `xrpld/overlay/detail/Message.cpp` + +## Role and Context + +`Message.cpp` implements the `Message` class — the fundamental unit of wire-format data exchanged between XRPL nodes in the peer-to-peer overlay network. Every protocol buffer message that travels over a peer connection passes through this class: it is constructed from a raw protobuf object, serialized into a length-typed byte buffer with a structured binary header, and optionally LZ4-compressed before transmission. Because a single `Message` may be broadcast to dozens of peers simultaneously, the class is designed for construction-once, read-many usage and handles concurrent access safely. + +## Buffer Layout and Header Format + +The wire format exists in two variants whose sizes differ by exactly 4 bytes. The uncompressed header is 6 bytes: 4 bytes encode the payload length (with the top nibble zeroed, leaving 28 effective bits, enough for the 64 MB maximum enforced by `maximumMessageSize`), followed by 2 bytes for the message type. The compressed header is 10 bytes: the same 4-byte length word but with the high bit set and bits 2–4 encoding the compression algorithm, 2 bytes for the message type, and a final 4 bytes carrying the original uncompressed size (needed by the receiver to pre-allocate a decompression buffer). + +`setHeader()` encodes this with a `pack` lambda that writes a 32-bit value in big-endian order, masking the top byte with `0x0F` to preserve the compression flag bits. After writing the size, the algorithm byte (e.g. `Algorithm::LZ4 = 0x90`, whose high bit and upper nibble encode the compression presence and algorithm ID) is OR'd into the first byte of the buffer via the saved pointer `h`. This two-pass approach — write size, then patch flags — keeps the logic clean while encoding both fields into the same 32-bit word. + +## Construction and Eager Serialization + +The constructor immediately serializes the protobuf message into `buffer_` at construction time. The layout is `[header | serialized payload]`, sized to exactly `headerBytes + messageBytes`. Two `XRPL_ASSERT` calls bracket the serialization: the first validates that `messageBytes` is nonzero (an empty protobuf message would be a protocol bug), and the second verifies that `buffer_.size()` equals `totalSize(message)` — confirming that buffer allocation and serialization agreed on the payload length. The `messageSize()` static helper abstracts the protobuf version difference between `ByteSize()` (returns `int`, risk of signed overflow on large payloads) and `ByteSizeLong()` (returns `size_t`, available from protobuf 3.11.0 onwards), guarded by a compile-time version check. + +The traffic category and optional validator public key are also captured at construction. `TrafficCount::categorize()` inspects the protobuf message content to classify it into one of many fine-grained categories (transaction, validation, proposal, ledger data, etc.). Storing the result in `category_` avoids re-inspection on every send; it is read freely by the peer layer via `getCategory()` to drive traffic accounting. The `validatorKey_` field stores the originating validator's public key for Validation and Proposal messages, enabling the squelch subsystem to suppress redundant rebroadcast to specific peers. + +## Lazy, Once-Only Compression + +Compression is deliberately deferred. `getBuffer(Compressed::On)` calls `std::call_once(once_flag_, &Message::compress, this)`, meaning the first peer connection to request a compressed form triggers compression, and all subsequent callers — potentially on different threads — receive the already-computed result. This is the central concurrency design: a single `Message` object is shared across all peers in a broadcast, and compression work is done once rather than once-per-peer. + +The `compress()` method applies a two-level eligibility filter. First, messages ≤70 bytes are unconditionally skipped — at that size, LZ4 overhead (the larger compressed header alone costs 4 extra bytes) cannot be recovered. Second, only a specific subset of message types are eligible: bulk data messages like `mtLEDGER_DATA`, `mtTRANSACTION`, `mtVALIDATOR_LIST`, `mtGET_LEDGER`, `mtREPLAY_DELTA_RESPONSE`, and `mtTRANSACTIONS`. Latency-sensitive control messages — `mtPING`, `mtPROPOSE_LEDGER`, `mtVALIDATION`, `mtSTATUS_CHANGE`, `mtHAVE_SET` — are excluded entirely. These tend to be small and time-critical; paying compression latency for them would be counterproductive. + +If the message type is eligible, `xrpl::compression::compress()` is called with a lambda buffer factory that pre-allocates `bufferCompressed_` including space for the `headerBytesCompressed` header. After compression, the actual gain is checked: if `compressedSize < (messageBytes - (headerBytesCompressed - headerBytes))` — that is, the compressed payload is smaller than the uncompressed payload minus the 4-byte header overhead — then `bufferCompressed_` is trimmed to the true size and a compressed header is written into it. Otherwise, `bufferCompressed_` is reset to zero length, and `getBuffer()` falls back to returning the uncompressed `buffer_`. This ensures callers always get the smaller of the two representations. + +## Relationship to the Overlay Send Path + +When a peer connection sends a message, it calls `getBuffer(Compressed::On)` or `getBuffer(Compressed::Off)` depending on whether compression was negotiated with that peer during handshake. The shared `Message` object thus amortizes both serialization and (if applicable) compression across an entire broadcast fan-out. The `getType()` static helper, which reads the message type from bytes 4–5 of a raw header pointer, is used internally during `compress()` to re-extract the type from the already-serialized `buffer_` rather than carrying the type as a separate field. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp.ai.json b/src/xrpld/overlay/detail/OverlayImpl.cpp.ai.json new file mode 100644 index 0000000000..8e43348190 --- /dev/null +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp.ai.json @@ -0,0 +1,843 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "OverlayImpl& overlay" + ], + "lineno": 22, + "name": "OverlayImpl::Child" + }, + { + "args": [ + "OverlayImpl& overlay" + ], + "lineno": 31, + "name": "OverlayImpl::Timer" + }, + { + "args": [ + "Application& app", + "Setup const& setup", + "ServerHandler& serverHandler", + "Resource::Manager& resourceManager", + "Resolver& resolver", + "boost::asio::io_context& io_context", + "BasicConfig const& config", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 69, + "name": "OverlayImpl" + } + ], + "code_paths": [ + { + "call_chain": [ + "OverlayImpl::Timer::async_wait", + "OverlayImpl::Timer::on_timer" + ], + "entry_point": "OverlayImpl::Timer::async_wait", + "purpose": "Schedules and handles periodic timer events for overlay maintenance tasks.", + "validation_points": [ + "OverlayImpl::Timer::on_timer: if (ec || stopping_)", + "OverlayImpl::Timer::on_timer: if (ec && ec != boost::asio::error::operation_aborted)" + ] + }, + { + "call_chain": [ + "OverlayImpl::Timer::on_timer", + "overlay_.m_peerFinder->once_per_second()", + "overlay_.sendEndpoints()", + "overlay_.autoConnect()", + "overlay_.sendTxQueue()", + "overlay_.deleteIdlePeers()", + "OverlayImpl::Timer::async_wait" + ], + "entry_point": "OverlayImpl::Timer::on_timer", + "purpose": "Handles timer expiration, runs periodic overlay tasks, and reschedules itself.", + "validation_points": [ + "OverlayImpl::Timer::on_timer: if (ec || stopping_)", + "OverlayImpl::Timer::on_timer: if (ec && ec != boost::asio::error::operation_aborted)" + ] + }, + { + "call_chain": [ + "OverlayImpl::Timer::stop", + "timer_.cancel()" + ], + "entry_point": "OverlayImpl::Timer::stop", + "purpose": "Stops the timer and cancels any pending asynchronous wait.", + "validation_points": [ + "OverlayImpl::Timer::on_timer: if (ec || stopping_)" + ] + } + ], + "data_flows": [ + { + "field": "ec (error_code)", + "flow": [ + "async_wait schedules on_timer with error_code", + "on_timer receives ec", + "on_timer checks ec for errors and operation_aborted" + ], + "origin": "Passed as argument to OverlayImpl::Timer::on_timer by boost::asio::async_wait", + "transformations": [ + "Checked for truthiness (error present)", + "Compared against boost::asio::error::operation_aborted" + ], + "validated_at": "OverlayImpl::Timer::on_timer" + }, + { + "field": "stopping_ (bool)", + "flow": [ + "stop() sets stopping_", + "on_timer checks stopping_", + "If stopping_ is true, on_timer returns early" + ], + "origin": "OverlayImpl::Timer::stop sets stopping_ = true", + "transformations": [ + "Set to true to indicate timer should stop" + ], + "validated_at": "OverlayImpl::Timer::on_timer" + }, + { + "field": "timer_ (boost::asio::steady_timer)", + "flow": [ + "Timer constructed with timer_", + "async_wait sets timer_ expiration and schedules on_timer", + "stop cancels timer_" + ], + "origin": "Constructed in OverlayImpl::Timer::Timer", + "transformations": [ + "expires_after sets expiration", + "cancel aborts pending waits" + ], + "validated_at": "N/A (timer_ is not validated, but its state is checked via ec)" + }, + { + "field": "overlay_.timer_count_", + "flow": [ + "on_timer increments timer_count_", + "Used to determine when to call deleteIdlePeers" + ], + "origin": "OverlayImpl instance field, incremented in on_timer", + "transformations": [ + "Incremented each timer tick", + "Modulo check for periodic action" + ], + "validated_at": "N/A (used as counter, not validated)" + } + ], + "description": "Implements the OverlayImpl class, which manages peer connections, peer discovery, message relaying, and network overlay operations for the XRPL node. Handles peer handshakes, connection attempts, traffic metrics, crawl/server info endpoints, and peer management logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ec (error_code)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (ec || stopping_)) at OverlayImpl::Timer::on_timer", + "issue_pattern": "Missing empty string validation for ec (error_code)", + "why_false_positive": "explicit check (if (ec || stopping_)) validates ec (error_code) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ec (error_code)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check (if (ec && ec != boost::asio::error::operation_aborted)) at OverlayImpl::Timer::on_timer", + "issue_pattern": "Missing empty string validation for ec (error_code)", + "why_false_positive": "explicit check (if (ec && ec != boost::asio::error::operation_aborted)) validates ec (error_code) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/OverlayImpl.cpp", + "functions": [ + { + "args": [ + "OverlayImpl& overlay" + ], + "lineno": 23, + "name": "OverlayImpl::Child::Child" + }, + { + "args": [], + "lineno": 27, + "name": "OverlayImpl::Child::~Child" + }, + { + "args": [ + "OverlayImpl& overlay" + ], + "lineno": 32, + "name": "OverlayImpl::Timer::Timer" + }, + { + "args": [], + "lineno": 36, + "name": "OverlayImpl::Timer::stop" + }, + { + "args": [], + "lineno": 43, + "name": "OverlayImpl::Timer::async_wait" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 51, + "name": "OverlayImpl::Timer::on_timer" + }, + { + "args": [ + "Application& app", + "Setup const& setup", + "ServerHandler& serverHandler", + "Resource::Manager& resourceManager", + "Resolver& resolver", + "boost::asio::io_context& io_context", + "BasicConfig const& config", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 70, + "name": "OverlayImpl::OverlayImpl" + }, + { + "args": [ + "std::unique_ptr&& stream_ptr", + "http_request_type&& request", + "endpoint_type remote_endpoint" + ], + "lineno": 101, + "name": "OverlayImpl::onHandoff" + }, + { + "args": [ + "http_request_type const& request" + ], + "lineno": 202, + "name": "OverlayImpl::isPeerUpgrade" + }, + { + "args": [ + "std::uint32_t id" + ], + "lineno": 208, + "name": "OverlayImpl::makePrefix" + }, + { + "args": [ + "std::shared_ptr const& slot", + "http_request_type const& request", + "address_type remote_address" + ], + "lineno": 215, + "name": "OverlayImpl::makeRedirectResponse" + }, + { + "args": [ + "std::shared_ptr const& slot", + "http_request_type const& request", + "address_type remote_address", + "std::string text" + ], + "lineno": 233, + "name": "OverlayImpl::makeErrorResponse" + }, + { + "args": [ + "beast::IP::Endpoint const& remote_endpoint" + ], + "lineno": 249, + "name": "OverlayImpl::connect" + }, + { + "args": [ + "std::shared_ptr const& peer" + ], + "lineno": 273, + "name": "OverlayImpl::add_active" + }, + { + "args": [ + "std::shared_ptr const& slot" + ], + "lineno": 299, + "name": "OverlayImpl::remove" + }, + { + "args": [], + "lineno": 307, + "name": "OverlayImpl::start" + }, + { + "args": [], + "lineno": 372, + "name": "OverlayImpl::stop" + }, + { + "args": [ + "beast::PropertyStream::Map& stream" + ], + "lineno": 386, + "name": "OverlayImpl::onWrite" + }, + { + "args": [ + "std::shared_ptr const& peer" + ], + "lineno": 404, + "name": "OverlayImpl::activate" + }, + { + "args": [ + "Peer::id_t id" + ], + "lineno": 422, + "name": "OverlayImpl::onPeerDeactivate" + }, + { + "args": [ + "std::shared_ptr const& m", + "std::shared_ptr const& from" + ], + "lineno": 427, + "name": "OverlayImpl::onManifests" + }, + { + "args": [ + "TrafficCount::category cat", + "int size" + ], + "lineno": 470, + "name": "OverlayImpl::reportInboundTraffic" + }, + { + "args": [ + "TrafficCount::category cat", + "int size" + ], + "lineno": 474, + "name": "OverlayImpl::reportOutboundTraffic" + }, + { + "args": [], + "lineno": 480, + "name": "OverlayImpl::size" + }, + { + "args": [], + "lineno": 486, + "name": "OverlayImpl::limit" + }, + { + "args": [], + "lineno": 491, + "name": "OverlayImpl::getOverlayInfo" + }, + { + "args": [], + "lineno": 515, + "name": "OverlayImpl::getServerInfo" + }, + { + "args": [], + "lineno": 540, + "name": "OverlayImpl::getServerCounts" + }, + { + "args": [], + "lineno": 545, + "name": "OverlayImpl::getUnlInfo" + }, + { + "args": [], + "lineno": 567, + "name": "OverlayImpl::json" + }, + { + "args": [ + "http_request_type const& req", + "Handoff& handoff" + ], + "lineno": 574, + "name": "OverlayImpl::processCrawl" + }, + { + "args": [ + "http_request_type const& req", + "Handoff& handoff" + ], + "lineno": 599, + "name": "OverlayImpl::processValidatorList" + }, + { + "args": [ + "http_request_type const& req", + "Handoff& handoff" + ], + "lineno": 646, + "name": "OverlayImpl::processHealth" + }, + { + "args": [ + "http_request_type const& req", + "Handoff& handoff" + ], + "lineno": 701, + "name": "OverlayImpl::processRequest" + }, + { + "args": [], + "lineno": 705, + "name": "OverlayImpl::getActivePeers" + }, + { + "args": [ + "std::set const& toSkip", + "std::size_t& active", + "std::size_t& disabled", + "std::size_t& enabledInSkip" + ], + "lineno": 714, + "name": "OverlayImpl::getActivePeers" + }, + { + "args": [ + "std::uint32_t index" + ], + "lineno": 739, + "name": "OverlayImpl::checkTracking" + }, + { + "args": [ + "Peer::id_t const& id" + ], + "lineno": 744, + "name": "OverlayImpl::findPeerByShortID" + }, + { + "args": [ + "PublicKey const& pubKey" + ], + "lineno": 754, + "name": "OverlayImpl::findPeerByPublicKey" + }, + { + "args": [ + "protocol::TMProposeSet& m" + ], + "lineno": 765, + "name": "OverlayImpl::broadcast" + }, + { + "args": [ + "protocol::TMProposeSet& m", + "uint256 const& uid", + "PublicKey const& validator" + ], + "lineno": 770, + "name": "OverlayImpl::relay" + }, + { + "args": [ + "protocol::TMValidation& m" + ], + "lineno": 782, + "name": "OverlayImpl::broadcast" + }, + { + "args": [ + "protocol::TMValidation& m", + "uint256 const& uid", + "PublicKey const& validator" + ], + "lineno": 787, + "name": "OverlayImpl::relay" + }, + { + "args": [], + "lineno": 799, + "name": "OverlayImpl::getManifestsMessage" + }, + { + "args": [ + "uint256 const& hash", + "std::optional> tx", + "std::set const& toSkip" + ], + "lineno": 819, + "name": "OverlayImpl::relay" + }, + { + "args": [ + "Child& child" + ], + "lineno": 872, + "name": "OverlayImpl::remove" + }, + { + "args": [], + "lineno": 879, + "name": "OverlayImpl::stopChildren" + }, + { + "args": [], + "lineno": 900, + "name": "OverlayImpl::autoConnect" + }, + { + "args": [], + "lineno": 907, + "name": "OverlayImpl::sendEndpoints" + }, + { + "args": [], + "lineno": 921, + "name": "OverlayImpl::sendTxQueue" + }, + { + "args": [ + "PublicKey const& validator", + "bool squelch", + "uint32_t squelchDuration" + ], + "lineno": 927, + "name": "makeSquelchMessage" + }, + { + "args": [ + "PublicKey const& validator", + "Peer::id_t id" + ], + "lineno": 935, + "name": "OverlayImpl::unsquelch" + }, + { + "args": [ + "PublicKey const& validator", + "Peer::id_t id", + "uint32_t squelchDuration" + ], + "lineno": 942, + "name": "OverlayImpl::squelch" + }, + { + "args": [ + "uint256 const& key", + "PublicKey const& validator", + "std::set&& peers", + "protocol::MessageType type" + ], + "lineno": 949, + "name": "OverlayImpl::updateSlotAndSquelch" + }, + { + "args": [ + "uint256 const& key", + "PublicKey const& validator", + "Peer::id_t peer", + "protocol::MessageType type" + ], + "lineno": 974, + "name": "OverlayImpl::updateSlotAndSquelch" + }, + { + "args": [ + "Peer::id_t id" + ], + "lineno": 995, + "name": "OverlayImpl::deletePeer" + }, + { + "args": [], + "lineno": 1006, + "name": "OverlayImpl::deleteIdlePeers" + }, + { + "args": [ + "BasicConfig const& config" + ], + "lineno": 1017, + "name": "setup_Overlay" + }, + { + "args": [ + "Application& app", + "Overlay::Setup const& setup", + "ServerHandler& serverHandler", + "Resource::Manager& resourceManager", + "Resolver& resolver", + "boost::asio::io_context& io_context", + "BasicConfig const& config", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 1102, + "name": "make_Overlay" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + }, + { + "lineno": 18, + "name": "CrawlOptions" + } + ], + "test_coverage_notes": "The code shown is primarily internal implementation for overlay timer management. Typical test coverage would be in integration or system tests that exercise overlay peer management, timer events, and error handling. Unit tests may exist for OverlayImpl and Timer, but explicit validation of error_code handling (ec) and stopping_ logic may not be directly tested unless there are tests that simulate timer errors or forced stops. Test files likely to cover this code include overlay/Overlay_test.cpp, overlay/PeerFinder_test.cpp, and possibly integration tests for peer connectivity and timer-driven events. Gaps may exist in testing rare error paths (e.g., specific boost::asio error codes) and concurrent stop/timer edge cases.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Boost.Asio error handling, custom logging (JLOG), C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (early return, logs error if not operation_aborted)", + "field": "ec (error_code)", + "location": "OverlayImpl::Timer::on_timer", + "validated_by": "explicit check (if (ec || stopping_))", + "validates": [ + "Checks if timer operation resulted in error or is stopping", + "Logs error if error_code is not operation_aborted" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (logs error)", + "field": "ec (error_code)", + "location": "OverlayImpl::Timer::on_timer", + "validated_by": "explicit check (if (ec && ec != boost::asio::error::operation_aborted))", + "validates": [ + "Checks if error_code is not operation_aborted", + "Logs error message" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp.ai.md b/src/xrpld/overlay/detail/OverlayImpl.cpp.ai.md new file mode 100644 index 0000000000..d5e7ffa0f9 --- /dev/null +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp.ai.md @@ -0,0 +1,99 @@ +# `OverlayImpl.cpp` — XRPL Overlay Network Implementation + +## Role and Purpose + +`OverlayImpl.cpp` contains the concrete implementation of the `Overlay` abstract interface, making it the operational heart of an XRPL node's peer-to-peer network layer. The class manages the full lifecycle of peer connections — from initial TCP acceptance and HTTP upgrade through cryptographic handshake, protocol negotiation, active message relay, and eventual teardown. It also exposes three internal HTTP endpoints (`/crawl`, `/health`, `/vl/`) that the network topology tools, monitoring systems, and validator list clients call directly. + +The file runs to roughly 1,550 lines and implements `OverlayImpl`, `OverlayImpl::Child`, `OverlayImpl::Timer`, and the free functions `setup_Overlay` and `make_Overlay`. + +--- + +## The `Child` Lifetime Mechanism + +`OverlayImpl::Child` is a lightweight RAII base class for any object that lives inside the overlay but is owned independently: `Timer`, `PeerImp`, and `ConnectAttempt` all inherit from it. Its destructor calls `overlay_.remove(*this)`, which erases the child from the `list_` flat-map and, if the map becomes empty, signals `cond_` to wake the thread blocked in `stop()`. + +This pattern means `stop()` can simply dispatch `stopChildren()` to the strand, then block on `cond_` until the list drains — every child cleans itself up. `stopChildren()` copies all child pointers into a local `vector>` before calling `stop()` on any of them, avoiding iterator invalidation since a child's `stop()` may immediately trigger its destructor and a re-entrant call to `OverlayImpl::remove`. + +--- + +## Per-Second Timer + +`OverlayImpl::Timer` inherits from both `Child` and `enable_shared_from_this`. It schedules itself with a one-second `boost::asio::steady_timer` bound to the overlay's dedicated strand. Each `on_timer` invocation: + +1. Calls `m_peerFinder->once_per_second()` for peer-discovery bookkeeping. +2. Calls `sendEndpoints()` to push freshly computed endpoint advertisements to peers. +3. Calls `autoConnect()` to open new outbound connections as suggested by PeerFinder. +4. Optionally calls `sendTxQueue()` when the TX reduce-relay feature is enabled. +5. Every four ticks (`Tuning::checkIdlePeers = 4`), calls `deleteIdlePeers()` to purge stale squelch slots. + +The timer reschedules itself at the end of each successful tick. On error, it distinguishes between a deliberate `operation_aborted` cancel (silent) and a genuine ASIO error (logged). The `stopping_` flag ensures no rescheduling occurs once `stop()` has been called on the same strand — these two always run sequentially by design. + +--- + +## Inbound Handshake: `onHandoff` + +When the HTTP server routes a new TCP connection to the overlay, `onHandoff()` takes ownership of the raw TLS stream. It first passes the request through `processRequest()` to handle `/crawl`, `/health`, and `/vl/` — these return immediately without creating a peer. If the connection is not an HTTP upgrade, it is also returned unhandled. + +For genuine peer upgrade requests, the method performs several gating checks in sequence: + +- Resource limit: `m_resourceManager.newInboundEndpoint()` disconnects abusive IPs before any further work. +- Slot availability: `m_peerFinder->new_inbound_slot()` enforces per-IP connection limits and self-connection prevention. +- Protocol version: `negotiateProtocolVersion()` verifies the `Upgrade` header lists a mutually supported XRPL wire version. +- Security cookie: `makeSharedValue()` extracts the TLS channel binding value used to prevent man-in-the-middle attacks. +- Cryptographic handshake: `verifyHandshake()` validates the node's signature and network ID from the HTTP headers. + +Only after all checks pass is a `PeerImp` created and inserted into `m_peers` (slot-keyed) and `list_`. The critical comment at the insertion site explains why `peer->run()` must be called while holding `mutex_`: without the lock, a concurrent `stop()` could drain the list and return before the new peer is running, leaving orphaned I/O. + +On any failure, the slot is released via `m_peerFinder->on_closed(slot)` to keep PeerFinder's accounting accurate, and the appropriate HTTP 400 or 503 response is returned. + +--- + +## Two-Phase Peer Registration + +There are two peer registries: `m_peers` maps `PeerFinder::Slot → weak_ptr`, and `ids_` maps `Peer::id_t → weak_ptr`. Only `ids_` is used for broadcast and relay operations. + +For inbound peers, `m_peers` is populated in `onHandoff` but `ids_` is not added until `activate()` is called after the peer completes its protocol handshake. For outbound peers handled by `ConnectAttempt`, both maps are populated together in `add_active()`. The split exists because `m_peers` is needed for PeerFinder slot management before a public key is known, while `ids_` represents the set of peers ready to receive and forward protocol messages. + +--- + +## Message Relay and Broadcast + +Proposals and validations have two dispatch paths. `broadcast()` sends directly to all active peers with no deduplication. `relay()` consults `HashRouter::shouldRelay()` first: if the message has already been relayed (hash seen), it returns an empty set and nothing is sent. If it should be relayed, the set of peers that already sent it is excluded from forwarding, which the caller uses to track which peers to skip in future rounds. + +Transaction relay (`relay(uint256, optional, set)`) is more involved. Pseudo-transactions are never relayed. If `TX_REDUCE_RELAY_ENABLE` is off, the transaction goes to all peers not in `toSkip`. When reduce-relay is active and the peer count exceeds a threshold, a quota of enabled peers is computed from `TX_REDUCE_RELAY_MIN_PEERS` and `TX_RELAY_PERCENTAGE`. Peers with the feature disabled always receive the full message for backward compatibility. Peers above the quota receive only the hash via `addTxQueue()`, which is later served by periodic `sendTxQueue()` pulls. The peer list is shuffled with `default_prng()` before selection to avoid systematic bias. + +--- + +## Squelch System + +The squelch system addresses validator message flooding. When many peers independently relay the same validator's messages, `updateSlotAndSquelch()` selects a subset of "selected" source peers and sends `TMSquelch` messages instructing the remaining peers to stop forwarding that validator's messages for a specified duration. + +All squelch logic is routed through the overlay strand via `post(strand_, ...)`. This is non-negotiable: `Slots` is not thread-safe, and multiple relaying peers on different threads would otherwise race. The overloads accept either a full set of peers (batch update) or a single peer ID, but both funnel into `slots_.updateSlotAndSquelch()`. The `squelch()` and `unsquelch()` methods look up the peer by short ID and send the serialized `makeSquelchMessage()` protobuf directly. + +--- + +## HTTP Endpoints + +`processCrawl()` serves `/crawl` with a JSON document whose content is governed by a bitmask (`CrawlOptions::Overlay | ServerInfo | ServerCounts | Unl`). Peer entries include public key, direction, uptime, and optionally IP/port if the peer's crawl flag permits. + +`processHealth()` implements a three-tier classification (healthy / warning / critical) based on ledger age, peer count, server state, amendment status, and load factor. Crucially, the HTTP status code itself encodes the result — 200, 503, or 500 respectively — so a load balancer can gate on status without parsing JSON. + +`processValidatorList()` serves `/vl/` and optionally `/vl//`, returning a signed validator list blob fetched from `ValidatorList`. A 404 is returned if the key is not recognized. + +--- + +## Bootstrap and PeerFinder Integration + +On `start()`, `PeerFinder::Config` is built from the application config, then `m_peerFinder` is configured and started. Bootstrap IPs come from `[ips]` in the config, falling back to `[ips_fixed]`, and if both are empty, to four hardcoded well-known nodes operated by Ripple Labs, ISRDC, XRPL Kuwait, and XRPL Commons. All resolution is asynchronous via `m_resolver.resolve()`, which populates PeerFinder's fallback list once DNS results arrive. Fixed peers (`[ips_fixed]`) are registered separately as always-reconnect entries. + +`autoConnect()` and `sendEndpoints()` are thin wrappers that delegate to PeerFinder and then dispatch the resulting actions to individual peers. This keeps topology policy inside PeerFinder and mechanism inside `OverlayImpl`. + +--- + +## Concurrency Notes + +The `mutex_` is declared as `std::recursive_mutex` — a known technical debt acknowledged by a `// VFALCO use std::mutex` comment. The recursion arises from paths where `onHandoff` or `add_active` calls `peer->run()` while holding the lock, and `run()` may trigger I/O that eventually calls back into the overlay. The overlay strand handles timer and squelch work independently; the mutex guards the peer registries accessed from multiple application threads. The `work_` optional executor guard keeps the `io_context` alive until `stopChildren()` sets it to `std::nullopt`. + +## `setup_Overlay` and `make_Overlay` + +`setup_Overlay()` parses the `[overlay]`, `[crawl]`, `[vl]`, and `[network_id]` config sections into an `Overlay::Setup` struct, creating an SSL context and validating all fields (rejecting private IPs as `public_ip`, rejecting negative IP limits). It maps the symbolic network names `main`, `testnet`, and `devnet` to their numeric IDs 0, 1, and 2. `make_Overlay()` is a factory function that constructs an `OverlayImpl` and returns it as `unique_ptr`, keeping the concrete type out of translation units that only need the interface. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/OverlayImpl.h.ai.json b/src/xrpld/overlay/detail/OverlayImpl.h.ai.json new file mode 100644 index 0000000000..48397e479b --- /dev/null +++ b/src/xrpld/overlay/detail/OverlayImpl.h.ai.json @@ -0,0 +1,325 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app", + "Setup const& setup", + "ServerHandler& serverHandler", + "Resource::Manager& resourceManager", + "Resolver& resolver", + "boost::asio::io_context& io_context", + "BasicConfig const& config", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 32, + "name": "OverlayImpl" + }, + { + "args": [ + "OverlayImpl& overlay" + ], + "lineno": 13, + "name": "Child" + }, + { + "args": [ + "OverlayImpl& overlay" + ], + "lineno": 27, + "name": "Timer" + }, + { + "args": [ + "std::string const& name", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 377, + "name": "TrafficGauges" + }, + { + "args": [ + "Handler const& handler", + "beast::insight::Collector::ptr const& collector", + "std::unordered_map&& trafficGauges_" + ], + "lineno": 383, + "name": "Stats" + } + ], + "description": "Defines the OverlayImpl class, which implements the Overlay interface for managing peer-to-peer connections, message relaying, traffic metrics, and related overlay network operations in the XRPL node.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/OverlayImpl.h", + "functions": [ + { + "args": [ + "UnaryFunc&& f" + ], + "lineno": 143, + "name": "for_each" + }, + { + "args": [ + "http_request_type const& request" + ], + "lineno": 170, + "name": "isPeerUpgrade" + }, + { + "args": [ + "boost::beast::http::response const& response" + ], + "lineno": 172, + "name": "isPeerUpgrade" + }, + { + "args": [ + "boost::beast::http::header const& req" + ], + "lineno": 180, + "name": "is_upgrade" + }, + { + "args": [ + "boost::beast::http::header const& req" + ], + "lineno": 189, + "name": "is_upgrade" + }, + { + "args": [ + "std::uint32_t id" + ], + "lineno": 197, + "name": "makePrefix" + }, + { + "args": [ + "TrafficCount::category cat", + "int bytes" + ], + "lineno": 199, + "name": "reportInboundTraffic" + }, + { + "args": [ + "TrafficCount::category cat", + "int bytes" + ], + "lineno": 201, + "name": "reportOutboundTraffic" + }, + { + "args": [], + "lineno": 203, + "name": "incJqTransOverflow" + }, + { + "args": [], + "lineno": 207, + "name": "getJqTransOverflow" + }, + { + "args": [], + "lineno": 211, + "name": "incPeerDisconnect" + }, + { + "args": [], + "lineno": 215, + "name": "getPeerDisconnect" + }, + { + "args": [], + "lineno": 219, + "name": "incPeerDisconnectCharges" + }, + { + "args": [], + "lineno": 223, + "name": "getPeerDisconnectCharges" + }, + { + "args": [], + "lineno": 227, + "name": "networkID" + }, + { + "args": [ + "uint256 const& key", + "PublicKey const& validator", + "std::set&& peers", + "protocol::MessageType type" + ], + "lineno": 238, + "name": "updateSlotAndSquelch" + }, + { + "args": [ + "uint256 const& key", + "PublicKey const& validator", + "Peer::id_t peer", + "protocol::MessageType type" + ], + "lineno": 246, + "name": "updateSlotAndSquelch" + }, + { + "args": [ + "Peer::id_t id" + ], + "lineno": 253, + "name": "deletePeer" + }, + { + "args": [], + "lineno": 257, + "name": "txMetrics" + }, + { + "args": [ + "Args... args" + ], + "lineno": 262, + "name": "addTxMetrics" + }, + { + "args": [ + "PublicKey const& validator", + "Peer::id_t const id", + "std::uint32_t squelchDuration" + ], + "lineno": 273, + "name": "squelch" + }, + { + "args": [ + "PublicKey const& validator", + "Peer::id_t id" + ], + "lineno": 276, + "name": "unsquelch" + }, + { + "args": [ + "std::shared_ptr const& slot", + "http_request_type const& request", + "address_type remote_address" + ], + "lineno": 278, + "name": "makeRedirectResponse" + }, + { + "args": [ + "std::shared_ptr const& slot", + "http_request_type const& request", + "address_type remote_address", + "std::string msg" + ], + "lineno": 283, + "name": "makeErrorResponse" + }, + { + "args": [ + "http_request_type const& req", + "Handoff& handoff" + ], + "lineno": 292, + "name": "processCrawl" + }, + { + "args": [ + "http_request_type const& req", + "Handoff& handoff" + ], + "lineno": 299, + "name": "processValidatorList" + }, + { + "args": [ + "http_request_type const& req", + "Handoff& handoff" + ], + "lineno": 307, + "name": "processHealth" + }, + { + "args": [ + "http_request_type const& req", + "Handoff& handoff" + ], + "lineno": 314, + "name": "processRequest" + }, + { + "args": [], + "lineno": 322, + "name": "getOverlayInfo" + }, + { + "args": [], + "lineno": 329, + "name": "getServerInfo" + }, + { + "args": [], + "lineno": 336, + "name": "getServerCounts" + }, + { + "args": [], + "lineno": 343, + "name": "getUnlInfo" + }, + { + "args": [ + "beast::PropertyStream::Map& stream" + ], + "lineno": 355, + "name": "onWrite" + }, + { + "args": [ + "Child& child" + ], + "lineno": 362, + "name": "remove" + }, + { + "args": [], + "lineno": 364, + "name": "stopChildren" + }, + { + "args": [], + "lineno": 366, + "name": "autoConnect" + }, + { + "args": [], + "lineno": 368, + "name": "sendEndpoints" + }, + { + "args": [], + "lineno": 370, + "name": "sendTxQueue" + }, + { + "args": [], + "lineno": 374, + "name": "deleteIdlePeers" + }, + { + "args": [], + "lineno": 386, + "name": "collect_metrics" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/OverlayImpl.h.ai.md b/src/xrpld/overlay/detail/OverlayImpl.h.ai.md new file mode 100644 index 0000000000..7daef9d625 --- /dev/null +++ b/src/xrpld/overlay/detail/OverlayImpl.h.ai.md @@ -0,0 +1,47 @@ +# `OverlayImpl.h` — Concrete P2P Overlay Manager + +`OverlayImpl` is the central class that implements the XRPL node's peer-to-peer layer. It inherits from two bases: the abstract `Overlay` interface (used by the rest of the application to interact with connected peers) and `reduce_relay::SquelchHandler` (a callback interface the squelch/unsquelch machinery calls back into). This dual inheritance separates the public API from the implementation side of the reduce-relay protocol cleanly enough that unit tests can substitute a lighter-weight `SquelchHandler` without instantiating the full overlay. + +## Peer Registration and Identity + +The class tracks connected peers through two complementary maps. `m_peers` maps a `PeerFinder::Slot` (the peer-discovery layer's notion of a connection slot) to a `weak_ptr`. `ids_` maps the node-local integer `Peer::id_t` to the same `weak_ptr`. Both deliberately use `weak_ptr` rather than `shared_ptr` to avoid ownership cycles — `PeerImp` objects are reference-counted throughout the system, and the overlay should not artificially extend their lifetimes. The integer ID counter `next_id_` is `std::atomic`, so IDs can be assigned without acquiring the main `mutex_`. + +The lifecycle progression is: an incoming or outgoing TCP connection is admitted, a `PeerFinder::Slot` is registered in `m_peers`, and once the TLS/HTTP handshake completes, `activate()` is called to install the peer in `ids_` and assign it a stable short ID. When the peer disconnects, `onPeerDeactivate()` removes the ID entry and `remove()` removes the slot entry separately — the two-step removal matches the two-step registration. + +The `for_each()` template is the standard way to visit all active peers. It acquires the mutex only long enough to copy a snapshot of `weak_ptr` handles into a local vector, then releases the lock before iterating. This is intentional: a visitor might attempt to acquire the same mutex (deadlock prevention), and `PeerImp` destruction can invalidate iterators directly on `ids_`. + +## Child Lifecycle and the Timer + +The nested `Child` class is a registration pattern for sub-objects whose lifetimes are tied to the overlay. Every `Child` holds a reference to `OverlayImpl&` and registers itself in the `flat_map> list_`. A `boost::container::flat_map` is chosen here because the number of children is small and the contiguous storage is cache-friendly for the infrequent iteration during `stopChildren()`. + +`Timer` is the only `Child` in normal operation. It inherits both `Child` and `std::enable_shared_from_this` so it can extend its own lifetime safely inside the async callback chain. Each tick calls `on_timer()`, which drives the periodic housekeeping — `autoConnect()`, `sendEndpoints()`, `sendTxQueue()`, and `deleteIdlePeers()` — before rescheduling itself. The `stopping_` flag prevents a shutdown race where a pending async wait would otherwise rearm after `stop()` was called. + +## Reduce-Relay: Slots and Squelching + +The most architecturally significant feature in `OverlayImpl` is the validator message reduce-relay system. The node may receive the same `TMValidation` or `TMProposeSet` from every peer simultaneously; for a well-connected node with dozens of peers, this is severe message amplification. + +`reduce_relay::Slots slots_` maintains per-validator counting state. When `updateSlotAndSquelch()` is called (by `PeerImp` after relaying a proposal or validation), it forwards the message key, validator public key, and originating peer ID into `Slots::updateSlotAndSquelch()`. The `Slots` object deduplicates calls by key/peer pair and, once a validator's messages have been observed from enough peers, randomly selects a fixed number of "selected" peers and tells the rest to stop relaying by sending `TMSquelch`. The callback for sending those squelch messages is provided by `OverlayImpl::squelch()` and `OverlayImpl::unsquelch()` — private implementations of the `SquelchHandler` interface that look up the target `PeerImp` by ID and dispatch the protocol message. + +There are two overloads of `updateSlotAndSquelch()`: one taking `std::set&&` for the broadcast case (multiple peers relayed this message simultaneously) and one taking a single `Peer::id_t` for the common single-peer case to avoid a heap allocation. When `deletePeer()` is called on peer teardown, the `Slots` container must unsquelch all peers that were previously suppressed because of the now-gone selected peer, restoring the counting state for that validator's slot. + +## Traffic Accounting and Metrics Export + +`TrafficCount m_traffic` classifies every inbound and outbound byte by protocol message type (transaction, proposal, validation, ledger data sub-types, squelch overhead, etc.) using `std::atomic` counters inside each category bucket — no lock is needed for the hot-path increment path. + +At collection time, `collect_metrics()` (installed as a `beast::insight::Hook` on the `Collector`) copies the atomic counters into the `Stats::trafficGauges` map under `m_statsMutex`. The reason for the second mutex is that the `TrafficGauges` struct's `beast::insight::Gauge` members are not themselves atomic; they are collector-managed objects that expect single-writer access. The separation of `m_statsMutex` from the main `mutex_` keeps the stats collection path from serializing with the peer management path. + +The atomic counters `jqTransOverflow_`, `peerDisconnects_`, and `peerDisconnectsCharges_` serve a similar purpose at a higher level — they are hot-path diagnostic counters that can be bumped from any thread without locking. `peerDisconnectsCharges_` specifically tracks disconnects initiated because of excessive resource consumption, giving operators visibility into whether the resource manager is actively enforcing limits. + +The `metrics::TxMetrics txMetrics_` field aggregates rolling-average statistics for the transaction reduce-relay feature (selected peers per relayed tx, suppressed peers, etc.). Its `addTxMetrics()` wrapper checks `strand_.running_in_this_thread()` and posts back to the strand if called off-strand, making `TxMetrics` effectively strand-confined without adding a per-call mutex. + +## HTTP Handoff and Crawl Endpoint + +Incoming TCP connections share the same listener as the HTTP RPC server. `onHandoff()` routes them: `isPeerUpgrade()` checks for an HTTP Upgrade header (HTTP/1.1 GET with `Connection: upgrade`), and if present the SSL stream is handed to a new `PeerImp` for the full peer handshake. Non-upgrade requests are dispatched to `processRequest()`, which tries `processCrawl()`, `processValidatorList()`, and `processHealth()` in turn. Crawl responses (`/crawl`) return JSON composed from `getOverlayInfo()`, `getServerInfo()`, `getServerCounts()`, and `getUnlInfo()` according to the `[crawl]` config flags. The validator list endpoint (`/vl/`) allows external crawlers to fetch the UNL that this node trusts for a given validator public key. + +## Manifest Caching + +`getManifestsMessage()` returns a cached `shared_ptr` containing the serialized `TMManifests` payload sent to newly connected peers. The cache is validated by comparing `manifestListSeq_` against the current manifest sequence. `manifestLock_` (a plain `std::mutex`, not the recursive `mutex_` used for peer maps) protects both the message pointer and the sequence number. This avoids re-serializing the full manifest list for every new peer handshake. + +## Concurrency Notes + +The primary lock is `std::recursive_mutex mutex_`, which protects `m_peers`, `ids_`, and related peer-map state. The inline comment `// VFALCO use std::mutex` acknowledges this as technical debt — recursive mutexes can hide unintended reentrancy. The associated `std::condition_variable_any cond_` (which requires a `Lockable` rather than a plain `BasicLockable`, hence is compatible with the recursive mutex) is used during shutdown to wait for all children to drain. The `work_` guard (`boost::asio::executor_work_guard`) prevents the `io_context` from exiting while the overlay is running, and is released as part of `stop()` to let the context complete its work queue and exit cleanly. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/PeerImp.cpp.ai.json b/src/xrpld/overlay/detail/PeerImp.cpp.ai.json new file mode 100644 index 0000000000..857f192d03 --- /dev/null +++ b/src/xrpld/overlay/detail/PeerImp.cpp.ai.json @@ -0,0 +1,1292 @@ +{ + "args": [ + { + "lineno": 19, + "name": "app" + }, + { + "lineno": 19, + "name": "id" + }, + { + "lineno": 19, + "name": "slot" + }, + { + "lineno": 19, + "name": "request" + }, + { + "lineno": 19, + "name": "publicKey" + }, + { + "lineno": 19, + "name": "protocol" + }, + { + "lineno": 19, + "name": "consumer" + }, + { + "lineno": 19, + "name": "stream_ptr" + }, + { + "lineno": 19, + "name": "overlay" + }, + { + "lineno": 69, + "name": "pBuffStr" + }, + { + "lineno": 730, + "name": "m" + }, + { + "lineno": 730, + "name": "size" + }, + { + "lineno": 730, + "name": "uncompressed_size" + }, + { + "lineno": 730, + "name": "isCompressed" + }, + { + "lineno": 730, + "name": "type" + }, + { + "lineno": 379, + "name": "name" + }, + { + "lineno": 379, + "name": "ec" + }, + { + "lineno": 389, + "name": "reason" + }, + { + "lineno": 478, + "name": "interval" + }, + { + "lineno": 491, + "name": "fingerprint" + }, + { + "lineno": 2362, + "name": "bytes" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "id_t id", + "std::shared_ptr const& slot", + "http_request_type&& request", + "PublicKey const& publicKey", + "ProtocolVersion protocol", + "Resource::Consumer consumer", + "std::unique_ptr&& stream_ptr", + "OverlayImpl& overlay" + ], + "lineno": 19, + "name": "PeerImp" + }, + { + "args": [], + "lineno": 2361, + "name": "Metrics" + } + ], + "code_paths": [ + { + "call_chain": [ + "PeerImp::PeerImp", + "getFingerprint", + "peerFeatureEnabled" + ], + "entry_point": "PeerImp::PeerImp", + "purpose": "Constructs a PeerImp object, validates and initializes peer identity, protocol, and features.", + "validation_points": [ + "getFingerprint(slot->remote_endpoint(), publicKey, to_string(id))", + "peerFeatureEnabled(headers_, FEATURE_COMPR, ...)", + "peerFeatureEnabled(headers_, FEATURE_TXRR, ...)", + "peerFeatureEnabled(headers_, FEATURE_LEDGER_REPLAY, ...)" + ] + }, + { + "call_chain": [ + "PeerImp::run", + "PeerImp::send", + "PeerImp::charge" + ], + "entry_point": "PeerImp::run", + "purpose": "Starts peer session, manages message sending and resource charging.", + "validation_points": [ + "Indirect: send/charge may validate message structure or resource usage, but not direct input validation." + ] + }, + { + "call_chain": [ + "PeerImp::addTxQueue", + "PeerImp::sendTxQueue" + ], + "entry_point": "PeerImp::addTxQueue", + "purpose": "Adds a transaction to the peer's queue and sends it.", + "validation_points": [ + "Indirect: Transaction data may be validated before queuing, but not shown in this snippet." + ] + } + ], + "data_flows": [ + { + "field": "publicKey", + "flow": [ + "PeerImp::PeerImp argument", + "getFingerprint(slot->remote_endpoint(), publicKey, to_string(id))", + "fingerprint_", + "prefix_ = makePrefix(fingerprint_)" + ], + "origin": "Constructor argument (from handshake/connection setup)", + "transformations": [ + "Used as input to getFingerprint to generate a unique peer fingerprint" + ], + "validated_at": "getFingerprint" + }, + { + "field": "slot->remote_endpoint()", + "flow": [ + "slot->remote_endpoint()", + "getFingerprint(slot->remote_endpoint(), ...)", + "fingerprint_" + ], + "origin": "slot (shared_ptr)", + "transformations": [ + "Used as input to getFingerprint" + ], + "validated_at": "getFingerprint" + }, + { + "field": "protocol", + "flow": [ + "PeerImp::PeerImp argument", + "protocol_ (member variable)" + ], + "origin": "Constructor argument (from handshake/connection setup)", + "transformations": [ + "Type-enforced as ProtocolVersion" + ], + "validated_at": "Constructor (type enforcement)" + }, + { + "field": "stream_ptr", + "flow": [ + "PeerImp::PeerImp argument", + "std::move(stream_ptr)", + "stream_ptr_ (member variable)", + "stream_(*stream_ptr_)" + ], + "origin": "Constructor argument (unique_ptr)", + "transformations": [ + "Ownership transferred via std::move, dereferenced for stream_" + ], + "validated_at": "Constructor (move semantics and dereference)" + }, + { + "field": "headers_", + "flow": [ + "PeerImp::PeerImp argument", + "request_ (moved)", + "headers_(request_)", + "peerFeatureEnabled(headers_, ...)" + ], + "origin": "request_ (http_request_type&& request)", + "transformations": [ + "Parsed from HTTP request, checked for feature flags" + ], + "validated_at": "peerFeatureEnabled" + } + ], + "description": "Implements the PeerImp class, which manages peer connections, message handling, resource charging, and protocol logic for XRPL's overlay network. Handles peer lifecycle, message parsing, relay, validation, and resource management.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "protocol (by type)", + "validation", + "missing", + "check" + ], + "evidence": "Field protocol (by type) validated by C++ type system, custom utility functions (e.g., getFingerprint, peerFeatureEnabled), possible jss:: JSON validation (not shown in snippet)", + "issue_pattern": "Missing validation for protocol (by type)", + "why_false_positive": "C++ type system, custom utility functions (e.g., getFingerprint, peerFeatureEnabled), possible jss:: JSON validation (not shown in snippet) validates protocol (by type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "stream_ptr (by dereference)", + "validation", + "missing", + "check" + ], + "evidence": "Field stream_ptr (by dereference) validated by C++ type system, custom utility functions (e.g., getFingerprint, peerFeatureEnabled), possible jss:: JSON validation (not shown in snippet)", + "issue_pattern": "Missing validation for stream_ptr (by dereference)", + "why_false_positive": "C++ type system, custom utility functions (e.g., getFingerprint, peerFeatureEnabled), possible jss:: JSON validation (not shown in snippet) validates stream_ptr (by dereference) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "publicKey (by fingerprint utility)", + "validation", + "missing", + "check" + ], + "evidence": "Field publicKey (by fingerprint utility) validated by C++ type system, custom utility functions (e.g., getFingerprint, peerFeatureEnabled), possible jss:: JSON validation (not shown in snippet)", + "issue_pattern": "Missing validation for publicKey (by fingerprint utility)", + "why_false_positive": "C++ type system, custom utility functions (e.g., getFingerprint, peerFeatureEnabled), possible jss:: JSON validation (not shown in snippet) validates publicKey (by fingerprint utility) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "headers_ (by feature negotiation utility)", + "validation", + "missing", + "check" + ], + "evidence": "Field headers_ (by feature negotiation utility) validated by C++ type system, custom utility functions (e.g., getFingerprint, peerFeatureEnabled), possible jss:: JSON validation (not shown in snippet)", + "issue_pattern": "Missing validation for headers_ (by feature negotiation utility)", + "why_false_positive": "C++ type system, custom utility functions (e.g., getFingerprint, peerFeatureEnabled), possible jss:: JSON validation (not shown in snippet) validates headers_ (by feature negotiation utility) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "publicKey", + "empty", + "string", + "validation" + ], + "evidence": "getFingerprint(slot->remote_endpoint(), publicKey, to_string(id)) at PeerImp::PeerImp (constructor)", + "issue_pattern": "Missing empty string validation for publicKey", + "why_false_positive": "getFingerprint(slot->remote_endpoint(), publicKey, to_string(id)) validates publicKey for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.6, + "detection_keywords": [ + "slot->remote_endpoint()", + "empty", + "string", + "validation" + ], + "evidence": "getFingerprint(slot->remote_endpoint(), ...) at PeerImp::PeerImp (constructor)", + "issue_pattern": "Missing empty string validation for slot->remote_endpoint()", + "why_false_positive": "getFingerprint(slot->remote_endpoint(), ...) validates slot->remote_endpoint() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "protocol", + "empty", + "string", + "validation" + ], + "evidence": "ProtocolVersion (type enforcement) at PeerImp::PeerImp (constructor)", + "issue_pattern": "Missing empty string validation for protocol", + "why_false_positive": "ProtocolVersion (type enforcement) validates protocol for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "protocol", + "type", + "validation", + "check" + ], + "evidence": "ProtocolVersion (type enforcement) at PeerImp::PeerImp (constructor)", + "issue_pattern": "Missing type validation for protocol", + "why_false_positive": "ProtocolVersion (type enforcement) validates protocol type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "stream_ptr", + "empty", + "string", + "validation" + ], + "evidence": "std::move(stream_ptr) and dereference at PeerImp::PeerImp (constructor)", + "issue_pattern": "Missing empty string validation for stream_ptr", + "why_false_positive": "std::move(stream_ptr) and dereference validates stream_ptr for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "headers_ (from request_)", + "empty", + "string", + "validation" + ], + "evidence": "peerFeatureEnabled(headers_, FEATURE_COMPR, \"lz4\", app_.config().COMPRESSIO...) at PeerImp::PeerImp (constructor)", + "issue_pattern": "Missing empty string validation for headers_ (from request_)", + "why_false_positive": "peerFeatureEnabled(headers_, FEATURE_COMPR, \"lz4\", app_.config().COMPRESSIO...) validates headers_ (from request_) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::weak_ptr" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Missing null check for std::weak_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::weak_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/PeerImp.cpp", + "functions": [ + { + "args": [ + "Application& app", + "id_t id", + "std::shared_ptr const& slot", + "http_request_type&& request", + "PublicKey const& publicKey", + "ProtocolVersion protocol", + "Resource::Consumer consumer", + "std::unique_ptr&& stream_ptr", + "OverlayImpl& overlay" + ], + "lineno": 19, + "name": "PeerImp" + }, + { + "args": [], + "lineno": 61, + "name": "~PeerImp" + }, + { + "args": [ + "std::string const& pBuffStr" + ], + "lineno": 69, + "name": "stringIsUint256Sized" + }, + { + "args": [], + "lineno": 74, + "name": "run" + }, + { + "args": [], + "lineno": 124, + "name": "stop" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 143, + "name": "send" + }, + { + "args": [], + "lineno": 191, + "name": "sendTxQueue" + }, + { + "args": [ + "uint256 const& hash" + ], + "lineno": 208, + "name": "addTxQueue" + }, + { + "args": [ + "uint256 const& hash" + ], + "lineno": 225, + "name": "removeTxQueue" + }, + { + "args": [ + "Resource::Charge const& fee", + "std::string const& context" + ], + "lineno": 242, + "name": "charge" + }, + { + "args": [], + "lineno": 253, + "name": "crawl" + }, + { + "args": [], + "lineno": 261, + "name": "cluster" + }, + { + "args": [], + "lineno": 266, + "name": "getVersion" + }, + { + "args": [], + "lineno": 271, + "name": "json" + }, + { + "args": [ + "ProtocolFeature f" + ], + "lineno": 323, + "name": "supportsFeature" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t seq" + ], + "lineno": 335, + "name": "hasLedger" + }, + { + "args": [ + "std::uint32_t& minSeq", + "std::uint32_t& maxSeq" + ], + "lineno": 349, + "name": "ledgerRange" + }, + { + "args": [ + "uint256 const& hash" + ], + "lineno": 355, + "name": "hasTxSet" + }, + { + "args": [], + "lineno": 361, + "name": "cycleStatus" + }, + { + "args": [ + "std::uint32_t uMin", + "std::uint32_t uMax" + ], + "lineno": 370, + "name": "hasRange" + }, + { + "args": [ + "std::string const& name", + "error_code ec" + ], + "lineno": 379, + "name": "fail" + }, + { + "args": [ + "std::string const& reason" + ], + "lineno": 389, + "name": "fail" + }, + { + "args": [], + "lineno": 410, + "name": "tryAsyncShutdown" + }, + { + "args": [], + "lineno": 426, + "name": "shutdown" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 438, + "name": "onShutdown" + }, + { + "args": [], + "lineno": 459, + "name": "close" + }, + { + "args": [ + "std::chrono::seconds interval" + ], + "lineno": 478, + "name": "setTimer" + }, + { + "args": [ + "std::string const& fingerprint" + ], + "lineno": 491, + "name": "makePrefix" + }, + { + "args": [ + "error_code const& ec" + ], + "lineno": 497, + "name": "onTimer" + }, + { + "args": [], + "lineno": 540, + "name": "cancelTimer" + }, + { + "args": [], + "lineno": 550, + "name": "doAccept" + }, + { + "args": [], + "lineno": 594, + "name": "name" + }, + { + "args": [], + "lineno": 599, + "name": "domain" + }, + { + "args": [], + "lineno": 606, + "name": "doProtocolStart" + }, + { + "args": [ + "error_code ec", + "std::size_t bytes_transferred" + ], + "lineno": 629, + "name": "onReadMessage" + }, + { + "args": [ + "error_code ec", + "std::size_t bytes_transferred" + ], + "lineno": 687, + "name": "onWriteMessage" + }, + { + "args": [ + "std::uint16_t type" + ], + "lineno": 726, + "name": "onMessageUnknown" + }, + { + "args": [ + "std::uint16_t type", + "std::shared_ptr<::google::protobuf::Message> const& m", + "std::size_t size", + "std::size_t uncompressed_size", + "bool isCompressed" + ], + "lineno": 730, + "name": "onMessageBegin" + }, + { + "args": [ + "std::uint16_t", + "std::shared_ptr<::google::protobuf::Message> const&" + ], + "lineno": 755, + "name": "onMessageEnd" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 760, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 771, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 792, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 841, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 882, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m", + "bool eraseTxQueue", + "bool batch" + ], + "lineno": 887, + "name": "handleTransaction" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 963, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1042, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1062, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1072, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1092, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1102, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1177, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1242, + "name": "onMessage" + }, + { + "args": [ + "std::uint32_t validationSeq" + ], + "lineno": 1347, + "name": "checkTracking" + }, + { + "args": [ + "std::uint32_t seq1", + "std::uint32_t seq2" + ], + "lineno": 1359, + "name": "checkTracking" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1375, + "name": "onMessage" + }, + { + "args": [ + "std::string const& messageType", + "std::string const& manifest", + "std::uint32_t version", + "std::vector const& blobs" + ], + "lineno": 1392, + "name": "onValidatorListMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1537, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1562, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1592, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1657, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1777, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1788, + "name": "handleHaveTransactions" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1822, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 1842, + "name": "onMessage" + }, + { + "args": [ + "uint256 const& hash", + "std::lock_guard const& lockedRecentLock" + ], + "lineno": 1877, + "name": "addLedger" + }, + { + "args": [ + "std::shared_ptr const& packet" + ], + "lineno": 1887, + "name": "doFetchPack" + }, + { + "args": [ + "std::shared_ptr const& packet" + ], + "lineno": 1912, + "name": "doTransactions" + }, + { + "args": [ + "HashRouterFlags flags", + "bool checkSignature", + "std::shared_ptr const& stx", + "bool batch" + ], + "lineno": 1947, + "name": "checkTransaction" + }, + { + "args": [ + "bool isTrusted", + "std::shared_ptr const& packet", + "RCLCxPeerPos peerPos" + ], + "lineno": 2037, + "name": "checkPropose" + }, + { + "args": [ + "std::shared_ptr const& val", + "uint256 const& key", + "std::shared_ptr const& packet" + ], + "lineno": 2072, + "name": "checkValidation" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "protocol::TMLedgerData& ledgerData" + ], + "lineno": 2107, + "name": "sendLedgerBase" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 2132, + "name": "getLedger" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 2187, + "name": "getTxSet" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 2207, + "name": "processLedgerRequest" + }, + { + "args": [ + "bool haveItem" + ], + "lineno": 2327, + "name": "getScore" + }, + { + "args": [], + "lineno": 2357, + "name": "isHighLatency" + }, + { + "args": [ + "std::uint64_t bytes" + ], + "lineno": 2362, + "name": "add_message" + }, + { + "args": [], + "lineno": 2377, + "name": "average_bytes" + }, + { + "args": [], + "lineno": 2383, + "name": "total_bytes" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::weak_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code snippet does not show direct unit tests. The comment indicates that unit tests are missing for PeerImp due to a recent hotfix. Validation logic (getFingerprint, peerFeatureEnabled) may be tested elsewhere, but PeerImp construction and its validation paths are likely not directly covered. Integration tests may exercise these paths indirectly via overlay/peer connection tests, but explicit validation and error handling for malformed input or feature negotiation is likely under-tested. Test files to look for: test_overlay.cpp, test_peer.cpp, test_feature_flags.cpp, but these may not exist or may not cover all validation branches.", + "validation_architecture": { + "auto_validated_fields": [ + "protocol (by type)", + "stream_ptr (by dereference)", + "publicKey (by fingerprint utility)", + "headers_ (by feature negotiation utility)" + ], + "framework": "C++ type system, custom utility functions (e.g., getFingerprint, peerFeatureEnabled), possible jss:: JSON validation (not shown in snippet)", + "validation_layer": "constructor (entry_point for PeerImp objects)" + }, + "validations": [ + { + "confidence": 0.7, + "error_thrown": "likely std::exception or custom error (not shown in snippet)", + "field": "publicKey", + "location": "PeerImp::PeerImp (constructor)", + "validated_by": "getFingerprint(slot->remote_endpoint(), publicKey, to_string(id))", + "validates": [ + "publicKey is used to generate a fingerprint; likely checks for valid key format and uniqueness" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 0.6, + "error_thrown": "likely std::exception or custom error (not shown in snippet)", + "field": "slot->remote_endpoint()", + "location": "PeerImp::PeerImp (constructor)", + "validated_by": "getFingerprint(slot->remote_endpoint(), ...)", + "validates": [ + "remote_endpoint is used in fingerprint; likely checks for valid endpoint format" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "compile-time type error", + "field": "protocol", + "location": "PeerImp::PeerImp (constructor)", + "validated_by": "ProtocolVersion (type enforcement)", + "validates": [ + "protocol must be of type ProtocolVersion" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "std::bad_alloc or std::exception if null", + "field": "stream_ptr", + "location": "PeerImp::PeerImp (constructor)", + "validated_by": "std::move(stream_ptr) and dereference", + "validates": [ + "stream_ptr must not be null (dereferenced immediately)" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "likely throws if headers_ is malformed or missing required fields", + "field": "headers_ (from request_)", + "location": "PeerImp::PeerImp (constructor)", + "validated_by": "peerFeatureEnabled(headers_, FEATURE_COMPR, \"lz4\", app_.config().COMPRESSIO...)", + "validates": [ + "headers_ must contain valid feature negotiation fields" + ], + "validation_type": "business_logic|format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/PeerImp.cpp.ai.md b/src/xrpld/overlay/detail/PeerImp.cpp.ai.md new file mode 100644 index 0000000000..dcc5d2bfba --- /dev/null +++ b/src/xrpld/overlay/detail/PeerImp.cpp.ai.md @@ -0,0 +1,40 @@ +# PeerImp.cpp + +## Role and Purpose + +`PeerImp` is the concrete, single-connection implementation of XRPL's peer-to-peer overlay. Each instance represents one live TLS-over-TCP session with a remote rippled node. The class sits at the intersection of three concerns: asynchronous I/O lifecycle, the XRPL binary wire protocol, and overlay-level resource accounting. Everything that touches an active peer — reading messages, sending messages, answering ledger queries, relaying validations and proposals, enforcing rate limits, and shutting down cleanly — is centralized here. + +--- + +## Construction and Feature Negotiation + +The constructor takes an already-established TLS stream, the peer's verified `PublicKey`, the negotiated `ProtocolVersion`, and HTTP upgrade headers. Three optional capabilities are negotiated at construction via `peerFeatureEnabled`: **LZ4 compression**, **TX reduce-relay**, and **ledger replay**. These flags are stored permanently and characterise the session's capability set. A `fingerprint_` derived from the remote endpoint, public key, and numeric ID prefixes every log line from the two journals (`journal_` for connection events, `p_journal_` for protocol events). + +--- + +## Strand-Based Thread Safety + +All mutable state is serialized through a single Boost.Asio `strand_`. Every public method self-posts to the strand if not already running there. A separate `recentLock_` mutex protects the fields that diagnostics and consensus code read from outside the strand (ledger hashes, sequence ranges, latency, tracking time). + +--- + +## Shutdown State Machine + +Shutdown follows four stages coordinated by `shutdown_` and `shutdownStarted_` flags. `shutdown()` sets the flag and cancels I/O; `tryAsyncShutdown()` defers the SSL close until no reads or writes are in flight; `onShutdown()` handles the SSL completion; `close()` closes the raw socket. A 5-second safety timer forces `close()` if the SSL teardown hangs. + +--- + +## Message Dispatch and Resource Charging + +`onReadMessage` drives the read loop, calling `invokeProtocolMessage` repeatedly to dispatch to typed `onMessage` overloads. `onMessageBegin`/`onMessageEnd` bracket each dispatch: the former resets a `ChargeWithContext fee_` accumulator, the latter applies the final charge via `charge()`. Individual handlers call `fee_.update()` to escalate the charge tier for malformed, duplicated, or expensive data. If a peer's resource balance reaches the drop threshold, it is disconnected immediately. + +--- + +## Key Design Patterns Across Handlers + +- **`stringIsUint256Sized` guard**: Called before every raw-byte-to-`uint256` conversion to prevent silent misuse of protobuf `string` fields. +- **Duplicate suppression**: `HashRouter::addSuppressionPeer` is checked before processing proposals, validations, and validator lists, preventing relay storms. +- **Job queue dispatch**: All cryptographic and CPU-heavy work (signature verification, transaction application, ledger assembly) is posted to the job queue rather than blocking the network strand. Job lambdas capture the peer as `weak_ptr` and silently no-op if the peer was destroyed. +- **TX reduce-relay**: Hash announcements (`TMHaveTransactions`) replace full transaction flooding when the feature is negotiated. `addTxQueue`/`sendTxQueue` batch hashes into per-peer sets; the peer's `handleHaveTransactions` requests only the missing ones. +- **Latency measurement**: A random 32-bit cookie in each `TMPing` prevents spoofed pong responses. RTT is smoothed with an 8-factor EWMA. +- **Peer tracking**: The `Tracking` atomic enum (`unknown` / `converged` / `diverged`) is updated by comparing the peer's reported ledger sequence against the locally validated index. Outbound peers that remain non-converged past configurable timeouts are disconnected as "Not useful". \ No newline at end of file diff --git a/src/xrpld/overlay/detail/PeerImp.h.ai.json b/src/xrpld/overlay/detail/PeerImp.h.ai.json new file mode 100644 index 0000000000..25e115fb9f --- /dev/null +++ b/src/xrpld/overlay/detail/PeerImp.h.ai.json @@ -0,0 +1,685 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "PeerImp const&", + "Application& app", + "id_t id", + "std::shared_ptr const& slot", + "http_request_type&& request", + "PublicKey const& publicKey", + "ProtocolVersion protocol", + "Resource::Consumer consumer", + "std::unique_ptr&& stream_ptr", + "OverlayImpl& overlay", + "Application& app", + "std::unique_ptr&& stream_ptr", + "Buffers const& buffers", + "std::shared_ptr&& slot", + "http_response_type&& response", + "Resource::Consumer usage", + "PublicKey const& publicKey", + "ProtocolVersion protocol", + "id_t id", + "OverlayImpl& overlay" + ], + "lineno": 54, + "name": "PeerImp" + }, + { + "args": [], + "lineno": 143, + "name": "Metrics" + }, + { + "args": [], + "lineno": 120, + "name": "ChargeWithContext" + } + ], + "description": "This file defines the PeerImp class, which manages peer-to-peer connections in the XRPL overlay network. It handles message exchange, connection health monitoring, protocol features, and implements a multi-stage, thread-safe shutdown mechanism for peer connections.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/PeerImp.h", + "functions": [ + { + "args": [ + "Application& app", + "id_t id", + "std::shared_ptr const& slot", + "http_request_type&& request", + "PublicKey const& publicKey", + "ProtocolVersion protocol", + "Resource::Consumer consumer", + "std::unique_ptr&& stream_ptr", + "OverlayImpl& overlay" + ], + "lineno": 180, + "name": "PeerImp" + }, + { + "args": [ + "Application& app", + "std::unique_ptr&& stream_ptr", + "Buffers const& buffers", + "std::shared_ptr&& slot", + "http_response_type&& response", + "Resource::Consumer usage", + "PublicKey const& publicKey", + "ProtocolVersion protocol", + "id_t id", + "OverlayImpl& overlay" + ], + "lineno": 183, + "name": "PeerImp" + }, + { + "args": [], + "lineno": 195, + "name": "~PeerImp" + }, + { + "args": [], + "lineno": 197, + "name": "pJournal" + }, + { + "args": [], + "lineno": 202, + "name": "slot" + }, + { + "args": [], + "lineno": 208, + "name": "run" + }, + { + "args": [], + "lineno": 211, + "name": "stop" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 215, + "name": "send" + }, + { + "args": [], + "lineno": 218, + "name": "sendTxQueue" + }, + { + "args": [ + "uint256 const& hash" + ], + "lineno": 222, + "name": "addTxQueue" + }, + { + "args": [ + "uint256 const& hash" + ], + "lineno": 227, + "name": "removeTxQueue" + }, + { + "args": [ + "FwdIt first", + "FwdIt last" + ], + "lineno": 232, + "name": "sendEndpoints" + }, + { + "args": [], + "lineno": 241, + "name": "getRemoteAddress" + }, + { + "args": [ + "Resource::Charge const& fee", + "std::string const& context" + ], + "lineno": 247, + "name": "charge" + }, + { + "args": [], + "lineno": 252, + "name": "id" + }, + { + "args": [], + "lineno": 258, + "name": "crawl" + }, + { + "args": [], + "lineno": 261, + "name": "cluster" + }, + { + "args": [ + "std::uint32_t validationSeq" + ], + "lineno": 266, + "name": "checkTracking" + }, + { + "args": [ + "std::uint32_t seq1", + "std::uint32_t seq2" + ], + "lineno": 269, + "name": "checkTracking" + }, + { + "args": [], + "lineno": 273, + "name": "getNodePublic" + }, + { + "args": [], + "lineno": 278, + "name": "getVersion" + }, + { + "args": [], + "lineno": 281, + "name": "uptime" + }, + { + "args": [], + "lineno": 286, + "name": "json" + }, + { + "args": [ + "ProtocolFeature f" + ], + "lineno": 289, + "name": "supportsFeature" + }, + { + "args": [ + "PublicKey const& pubKey" + ], + "lineno": 291, + "name": "publisherListSequence" + }, + { + "args": [ + "PublicKey const& pubKey", + "std::size_t const seq" + ], + "lineno": 299, + "name": "setPublisherListSequence" + }, + { + "args": [], + "lineno": 307, + "name": "getClosedLedgerHash" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t seq" + ], + "lineno": 312, + "name": "hasLedger" + }, + { + "args": [ + "std::uint32_t& minSeq", + "std::uint32_t& maxSeq" + ], + "lineno": 314, + "name": "ledgerRange" + }, + { + "args": [ + "uint256 const& hash" + ], + "lineno": 316, + "name": "hasTxSet" + }, + { + "args": [], + "lineno": 318, + "name": "cycleStatus" + }, + { + "args": [ + "std::uint32_t uMin", + "std::uint32_t uMax" + ], + "lineno": 320, + "name": "hasRange" + }, + { + "args": [ + "bool haveItem" + ], + "lineno": 323, + "name": "getScore" + }, + { + "args": [], + "lineno": 326, + "name": "isHighLatency" + }, + { + "args": [], + "lineno": 329, + "name": "compressionEnabled" + }, + { + "args": [], + "lineno": 333, + "name": "txReduceRelayEnabled" + }, + { + "args": [ + "std::string const& name", + "error_code ec" + ], + "lineno": 343, + "name": "fail" + }, + { + "args": [ + "std::string const& reason" + ], + "lineno": 357, + "name": "fail" + }, + { + "args": [], + "lineno": 370, + "name": "shutdown" + }, + { + "args": [], + "lineno": 382, + "name": "tryAsyncShutdown" + }, + { + "args": [ + "error_code ec" + ], + "lineno": 395, + "name": "onShutdown" + }, + { + "args": [], + "lineno": 410, + "name": "close" + }, + { + "args": [ + "std::chrono::seconds interval" + ], + "lineno": 423, + "name": "setTimer" + }, + { + "args": [ + "error_code const& ec" + ], + "lineno": 434, + "name": "onTimer" + }, + { + "args": [], + "lineno": 447, + "name": "cancelTimer" + }, + { + "args": [ + "std::string const& fingerprint" + ], + "lineno": 453, + "name": "makePrefix" + }, + { + "args": [], + "lineno": 455, + "name": "doAccept" + }, + { + "args": [], + "lineno": 457, + "name": "name" + }, + { + "args": [], + "lineno": 459, + "name": "domain" + }, + { + "args": [], + "lineno": 464, + "name": "doProtocolStart" + }, + { + "args": [ + "error_code ec", + "std::size_t bytes_transferred" + ], + "lineno": 467, + "name": "onReadMessage" + }, + { + "args": [ + "error_code ec", + "std::size_t bytes_transferred" + ], + "lineno": 470, + "name": "onWriteMessage" + }, + { + "args": [ + "std::shared_ptr const& m", + "bool eraseTxQueue", + "bool batch" + ], + "lineno": 478, + "name": "handleTransaction" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 491, + "name": "handleHaveTransactions" + }, + { + "args": [], + "lineno": 499, + "name": "fingerprint" + }, + { + "args": [], + "lineno": 504, + "name": "prefix" + }, + { + "args": [ + "std::uint16_t type" + ], + "lineno": 514, + "name": "onMessageUnknown" + }, + { + "args": [ + "std::uint16_t type", + "std::shared_ptr<::google::protobuf::Message> const& m", + "std::size_t size", + "std::size_t uncompressed_size", + "bool isCompressed" + ], + "lineno": 517, + "name": "onMessageBegin" + }, + { + "args": [ + "std::uint16_t type", + "std::shared_ptr<::google::protobuf::Message> const& m" + ], + "lineno": 522, + "name": "onMessageEnd" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 524, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 525, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 526, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 527, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 528, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 529, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 530, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 531, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 532, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 533, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 534, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 535, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 536, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 537, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 538, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 539, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 540, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 541, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 542, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 543, + "name": "onMessage" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 544, + "name": "onMessage" + }, + { + "args": [ + "uint256 const& hash", + "std::lock_guard const& lockedRecentLock" + ], + "lineno": 551, + "name": "addLedger" + }, + { + "args": [ + "std::shared_ptr const& packet" + ], + "lineno": 554, + "name": "doFetchPack" + }, + { + "args": [ + "std::string const& messageType", + "std::string const& manifest", + "std::uint32_t version", + "std::vector const& blobs" + ], + "lineno": 557, + "name": "onValidatorListMessage" + }, + { + "args": [ + "std::shared_ptr const& packet" + ], + "lineno": 563, + "name": "doTransactions" + }, + { + "args": [ + "HashRouterFlags flags", + "bool checkSignature", + "std::shared_ptr const& stx", + "bool batch" + ], + "lineno": 568, + "name": "checkTransaction" + }, + { + "args": [ + "bool isTrusted", + "std::shared_ptr const& packet", + "RCLCxPeerPos peerPos" + ], + "lineno": 574, + "name": "checkPropose" + }, + { + "args": [ + "std::shared_ptr const& val", + "uint256 const& key", + "std::shared_ptr const& packet" + ], + "lineno": 579, + "name": "checkValidation" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "protocol::TMLedgerData& ledgerData" + ], + "lineno": 585, + "name": "sendLedgerBase" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 589, + "name": "getLedger" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 592, + "name": "getTxSet" + }, + { + "args": [ + "std::shared_ptr const& m" + ], + "lineno": 595, + "name": "processLedgerRequest" + }, + { + "args": [ + "FwdIt first", + "FwdIt last" + ], + "lineno": 601, + "name": "sendEndpoints" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/PeerImp.h.ai.md b/src/xrpld/overlay/detail/PeerImp.h.ai.md new file mode 100644 index 0000000000..d576cb020a --- /dev/null +++ b/src/xrpld/overlay/detail/PeerImp.h.ai.md @@ -0,0 +1,91 @@ +# `PeerImp.h` — Active Peer Connection Management + +`PeerImp` is the concrete implementation of a live peer-to-peer connection in the XRPL overlay network. Where the abstract `Peer` interface and `Overlay` facade express *what* the network can do, `PeerImp` is *how* a single connected node actually behaves: it owns the SSL/TCP socket, drives the asynchronous read/write loops, dispatches every incoming protobuf message to the right handler, tracks ledger convergence, enforces resource budgets, and orchestrates a careful multi-stage shutdown when the connection ends. Every validator, full-history node, and ordinary rippled peer that connects to a running node is eventually managed as a `PeerImp`. + +## Inheritance and Ownership + +The class inherits from three bases simultaneously: + +- **`Peer`** — the public interface consumed by the rest of the application (consensus, ledger acquisition, broadcast). All calls from outside the overlay go through this pure-virtual handle. +- **`std::enable_shared_from_this`** — mandatory because virtually every async callback captures `shared_from_this()` to keep the peer alive across I/O completion. The workaround comment `// Work-around for calling shared_from_this in constructors` explains why `run()` is virtual and deferred rather than called in the constructor body. +- **`OverlayImpl::Child`** — registers the peer with the overlay manager so that `stop()` can be broadcast to all children. The `~PeerImp` destructor calls `overlay_.deletePeer()`, `overlay_.onPeerDeactivate()`, and `overlay_.peerFinder().on_closed()` in sequence, cleaning up the PeerFinder slot and traffic tracking before destruction completes. + +## Dual-Constructor Pattern + +Two constructors exist for different connection directions: + +- The **inbound** constructor accepts a completed HTTP upgrade request (`http_request_type&&`), reads feature headers from `request_`, and sets `inbound_ = true`. `run()` subsequently calls `doAccept()`. +- The **outbound** constructor (template on `Buffers`) accepts the HTTP response from the remote side, reads feature headers from `response_`, pre-populates `read_buffer_` from any bytes already received during the handshake, and sets `inbound_ = false`. `run()` calls `doProtocolStart()` directly. + +Both paths resolve feature negotiation at construction time by inspecting HTTP headers for `FEATURE_COMPR` (lz4 compression), `FEATURE_TXRR` (transaction reduce-relay), `FEATURE_LEDGER_REPLAY`, and `FEATURE_VPRR` (validator-proposal reduce-relay base squelch). The result is stored in three boolean/enum fields (`compressionEnabled_`, `txReduceRelayEnabled_`, `ledgerReplayEnabled_`) that remain immutable for the life of the connection. + +## Transport Layer + +The type stack is: +``` +socket_type (TCP socket) + → middle_type (beast::tcp_stream) + → stream_type (beast::ssl_stream) +``` + +`stream_ptr_` owns the SSL stream as a `unique_ptr`. The references `socket_` and `stream_` are kept as plain references after construction to avoid repeated pointer dereferences in the hot I/O path. All async operations are serialized through `strand_`, a `boost::asio::strand` that guarantees sequenced execution of callbacks without a mutex — the primary thread-safety mechanism for all connection-level state. + +## Message Dispatch + +The protocol message loop is driven by `doProtocolStart()` → `onReadMessage()`. Each call to `onReadMessage` consumes bytes from `read_buffer_` (a `boost::beast::multi_buffer`), parses one or more protobuf frames, and dispatches to the appropriate overloaded `onMessage()`. Twenty distinct message types are handled, including: + +- **`TMTransaction` / `TMTransactions`** — single and batched transaction relay, routed through `handleTransaction()` which calls `checkTransaction()` for signature verification and fee-based routing +- **`TMProposeSet` / `TMValidation`** — consensus proposals and validations, dispatched to `checkPropose()` and `checkValidation()` respectively after trust checks +- **`TMGetLedger` / `TMLedgerData`** — ledger data requests and responses, handled by `processLedgerRequest()` which calls `getLedger()` or `getTxSet()` depending on the request mode +- **`TMEndpoints`** — peer discovery endpoints propagated through PeerFinder +- **`TMSquelch`** — reduce-relay control messages that call `squelch_.addSquelch()` or `squelch_.removeSquelch()` to suppress redundant validator message forwarding +- **`TMHaveTransactions` / `TMTransactions`** — the transaction reduce-relay protocol: peers advertise unheld hashes, and the receiver fetches what it's missing via `handleHaveTransactions()` / `doTransactions()` +- **`TMProofPathRequest/Response` / `TMReplayDeltaRequest/Response`** — ledger replay messages, delegated to `ledgerReplayMsgHandler_` + +`onMessageBegin()` and `onMessageEnd()` bracket each dispatch for metrics accounting: `metrics_.recv` accumulates incoming byte counts in a 30-slot circular rolling average. + +## Tracking State Machine + +`tracking_` is a `std::atomic` with three values: `converged`, `unknown`, and `diverged`. The `checkTracking()` overloads compare a recently-validated ledger sequence against the peer's advertised `closedLedgerHash_`. From `Tuning.h`, a peer within 24 ledgers is converged; beyond 128 ledgers it is diverged. The atomic avoids locking in the hot validation-received path. `trackingTime_` records when the state last changed; unknown-state peers that remain unknown too long will be disconnected by the timer handler. + +## Shutdown State Machine + +The shutdown design is the most architecturally intricate part of the class. Two boolean flags coordinate the process: + +- **`shutdown_`**: set the moment any shutdown trigger fires — signals all pending I/O to drain rather than start new work +- **`shutdownStarted_`**: set when the SSL `async_shutdown` is actually posted — prevents double-initiation + +The progression is: + +``` +stop() / fail() / onTimer() + → shutdown() — sets shutdown_, cancels peer timer, posts 5s safety timer + → tryAsyncShutdown()— gates on !readPending_ && !writePending_ + → stream_.async_shutdown() + → onShutdown() — cancels safety timer, calls close() + → close() — socket_.close(), notifies overlay +``` + +`tryAsyncShutdown()` is the key gate: it will not initiate the SSL handshake while a read or write is still in flight. `onReadMessage` and `onWriteMessage` each call `tryAsyncShutdown()` on completion when `shutdown_` is set, so the last in-flight operation always triggers the graceful termination. The 5-second `shutdownTimerInterval` safety timer in `onTimer` calls `close()` directly if the graceful SSL path stalls, preventing hanging connections. All these callbacks run on `strand_`, so no additional mutex is needed for flag access. + +## Resource Accounting + +`ChargeWithContext` is a small inner struct that accumulates the *highest* resource charge incurred while processing a batch of incoming messages. Its `update()` method asserts monotonic growth — the charge can only escalate, never decrease — reflecting a "worst case per message batch" policy. The accumulated fee is eventually applied to `usage_` (a `Resource::Consumer`) via `charge()`. This batching avoids per-fragment charge calls in the inner read loop, concentrating cost attribution at message boundaries. + +The `Metrics` inner class tracks per-direction bandwidth with a 30-bucket circular rolling average (one bucket per second) using its own `shared_mutex` for thread-safe read access from diagnostic paths while writes happen on the strand. The `average_bytes()` accessor exposes current throughput for `json()` diagnostics and peer scoring. + +## Locking Discipline + +The code contains a candid 2019 audit comment acknowledging that locking evolved haphazardly. The practical rules are: + +- **`recentLock_`** (plain `mutex`) guards `closedLedgerHash_`, `previousLedgerHash_`, `minLedger_`, `maxLedger_`, `recentLedgers_`, `recentTxSets_`, `trackingTime_`, `latency_`, `publisherListSequences_`, and `last_status_`. The `addLedger()` helper takes the lock by value (`std::lock_guard const&`) as a deliberate API forcing callers to hold the lock before calling — a statically-enforced contract. +- **`nameMutex_`** (a `shared_mutex`) guards `name_`, allowing concurrent reads from multiple threads while writes are exclusive. +- All I/O-related state (`shutdown_`, `shutdownStarted_`, `readPending_`, `writePending_`, `send_queue_`, `txQueue_`) is protected entirely by the strand — no mutex needed because they are only touched inside strand-dispatched callbacks. + +## Transaction Reduce-Relay + +When `txReduceRelayEnabled_` is true, `PeerImp` operates in a gossip-pull mode: instead of forwarding full transactions, the node collects unsent transaction hashes into `txQueue_` (capped at `reduce_relay::MAX_TX_QUEUE_SIZE`) and flushes them as batched `TMHaveTransactions` messages once per second via `sendTxQueue()`. The peer receiving this advertisement requests any hashes it hasn't seen. This asymmetry — advertise hashes, pull bodies on demand — drastically reduces duplicate full-transaction traffic across the mesh without sacrificing reliability. + +## Relationship to `OverlayImpl` + +`PeerImp` is declared `friend class OverlayImpl`, reflecting that the overlay manager needs to access internals for slot cleanup, traffic reporting via `reportOutboundTraffic()`, and squelch callbacks. The `Squelch` member (`squelch_`) is per-peer state for the validator-proposal reduce-relay protocol: it tracks which validators are currently squelched on this specific link, independent of the overlay-wide squelch table in `OverlayImpl`. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/PeerReservationTable.cpp.ai.json b/src/xrpld/overlay/detail/PeerReservationTable.cpp.ai.json new file mode 100644 index 0000000000..04da222e4c --- /dev/null +++ b/src/xrpld/overlay/detail/PeerReservationTable.cpp.ai.json @@ -0,0 +1,456 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "PeerReservationTable::insert_or_assign", + "table_.find", + "table_.erase", + "table_.insert", + "connection_->checkoutDb", + "insertPeerReservation" + ], + "entry_point": "PeerReservationTable::insert_or_assign", + "purpose": "Insert or update a peer reservation in the table and persist it to the database.", + "validation_points": [ + "table_.find (checks if reservation exists)", + "table_.insert (validates uniqueness in unordered_set)", + "insertPeerReservation (validates nodeId and description)" + ] + }, + { + "call_chain": [ + "PeerReservationTable::erase", + "table_.find", + "table_.erase", + "connection_->checkoutDb", + "deletePeerReservation" + ], + "entry_point": "PeerReservationTable::erase", + "purpose": "Remove a peer reservation from the table and database.", + "validation_points": [ + "table_.find (checks if reservation exists before erasing)" + ] + }, + { + "call_chain": [ + "PeerReservationTable::load", + "connection.checkoutDb", + "getPeerReservationTable", + "table_.insert" + ], + "entry_point": "PeerReservationTable::load", + "purpose": "Load all peer reservations from the database into the in-memory table.", + "validation_points": [ + "table_.insert (validates uniqueness in unordered_set)" + ] + }, + { + "call_chain": [ + "PeerReservation::toJson", + "toBase58(TokenType::NodePublic, nodeId)", + "description.empty()" + ], + "entry_point": "PeerReservation::toJson", + "purpose": "Serialize a peer reservation to JSON.", + "validation_points": [ + "description.empty() (validates description is non-empty before adding to JSON)", + "toBase58 (validates nodeId format)" + ] + } + ], + "data_flows": [ + { + "field": "description", + "flow": [ + "PeerReservation.description", + "PeerReservationTable::insert_or_assign", + "insertPeerReservation", + "PeerReservation::toJson" + ], + "origin": "PeerReservation object (constructor or database load)", + "transformations": [ + "Checked for emptiness before adding to JSON", + "Passed to insertPeerReservation for DB persistence" + ], + "validated_at": "PeerReservation::toJson (description.empty()), insertPeerReservation" + }, + { + "field": "nodeId", + "flow": [ + "PeerReservation.nodeId", + "PeerReservationTable::insert_or_assign", + "insertPeerReservation", + "PeerReservation::toJson" + ], + "origin": "PeerReservation object (constructor or database load)", + "transformations": [ + "Converted to base58 string for JSON", + "Passed to insertPeerReservation for DB persistence" + ], + "validated_at": "toBase58(TokenType::NodePublic, nodeId), insertPeerReservation" + }, + { + "field": "reservation (PeerReservation)", + "flow": [ + "insert_or_assign argument", + "table_.find", + "table_.erase (if exists)", + "table_.insert", + "insertPeerReservation" + ], + "origin": "Function argument to insert_or_assign", + "transformations": [ + "Checked for existence in table_", + "Inserted or replaced in table_", + "Persisted to DB" + ], + "validated_at": "table_.find, table_.insert, insertPeerReservation" + }, + { + "field": "table_ (std::unordered_set)", + "flow": [ + "table_", + "insert_or_assign / erase / load", + "table_.insert / table_.erase / table_.find" + ], + "origin": "In-memory data member of PeerReservationTable", + "transformations": [ + "Ensures uniqueness of PeerReservation objects" + ], + "validated_at": "table_.insert (enforces uniqueness), table_.find" + } + ], + "description": "Implements the PeerReservationTable and PeerReservation logic for managing peer reservations in the XRPL network, including JSON serialization, insertion, deletion, and database loading.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "nodeId (via toBase58 and DB layer)", + "validation", + "missing", + "check" + ], + "evidence": "Field nodeId (via toBase58 and DB layer) validated by jss:: (JSON field naming), toBase58 (key encoding), DB layer (insertPeerReservation)", + "issue_pattern": "Missing validation for nodeId (via toBase58 and DB layer)", + "why_false_positive": "jss:: (JSON field naming), toBase58 (key encoding), DB layer (insertPeerReservation) validates nodeId (via toBase58 and DB layer) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "description (via DB layer and empty check)", + "validation", + "missing", + "check" + ], + "evidence": "Field description (via DB layer and empty check) validated by jss:: (JSON field naming), toBase58 (key encoding), DB layer (insertPeerReservation)", + "issue_pattern": "Missing validation for description (via DB layer and empty check)", + "why_false_positive": "jss:: (JSON field naming), toBase58 (key encoding), DB layer (insertPeerReservation) validates description (via DB layer and empty check) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "description", + "empty", + "string", + "validation" + ], + "evidence": "std::string::empty() at PeerReservation::toJson", + "issue_pattern": "Missing empty string validation for description", + "why_false_positive": "std::string::empty() validates description for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "nodeId", + "empty", + "string", + "validation" + ], + "evidence": "toBase58(TokenType::NodePublic, nodeId) at PeerReservation::toJson", + "issue_pattern": "Missing empty string validation for nodeId", + "why_false_positive": "toBase58(TokenType::NodePublic, nodeId) validates nodeId for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "nodeId", + "format", + "validation", + "invalid" + ], + "evidence": "toBase58(TokenType::NodePublic, nodeId) at PeerReservation::toJson", + "issue_pattern": "Missing format validation for nodeId", + "why_false_positive": "toBase58(TokenType::NodePublic, nodeId) validates nodeId format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "reservation (PeerReservation)", + "empty", + "string", + "validation" + ], + "evidence": "std::unordered_set::find at PeerReservationTable::insert_or_assign", + "issue_pattern": "Missing empty string validation for reservation (PeerReservation)", + "why_false_positive": "std::unordered_set::find validates reservation (PeerReservation) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "table_ (PeerReservation set)", + "empty", + "string", + "validation" + ], + "evidence": "std::unordered_set::insert at PeerReservationTable::insert_or_assign", + "issue_pattern": "Missing empty string validation for table_ (PeerReservation set)", + "why_false_positive": "std::unordered_set::insert validates table_ (PeerReservation set) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "table_ (PeerReservation set)", + "type", + "validation", + "check" + ], + "evidence": "std::unordered_set::insert at PeerReservationTable::insert_or_assign", + "issue_pattern": "Missing type validation for table_ (PeerReservation set)", + "why_false_positive": "std::unordered_set::insert validates table_ (PeerReservation set) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "reservation.nodeId, reservation.description", + "empty", + "string", + "validation" + ], + "evidence": "insertPeerReservation at PeerReservationTable::insert_or_assign", + "issue_pattern": "Missing empty string validation for reservation.nodeId, reservation.description", + "why_false_positive": "insertPeerReservation validates reservation.nodeId, reservation.description for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/PeerReservationTable.cpp", + "functions": [ + { + "args": [], + "lineno": 11, + "name": "PeerReservation::toJson" + }, + { + "args": [], + "lineno": 19, + "name": "PeerReservationTable::list" + }, + { + "args": [ + "connection" + ], + "lineno": 38, + "name": "PeerReservationTable::load" + }, + { + "args": [ + "reservation" + ], + "lineno": 54, + "name": "PeerReservationTable::insert_or_assign" + }, + { + "args": [ + "nodeId" + ], + "lineno": 84, + "name": "PeerReservationTable::erase" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Likely test coverage would be in integration or unit tests for PeerReservationTable, possibly in files like test_overlay.cpp, test_PeerReservationTable.cpp, or higher-level application setup tests. Validation of description, nodeId, and reservation uniqueness should be tested, but coverage for edge cases (e.g., empty description, duplicate nodeId, concurrent access) is not visible here and may be missing if not explicitly tested elsewhere.", + "validation_architecture": { + "auto_validated_fields": [ + "nodeId (via toBase58 and DB layer)", + "description (via DB layer and empty check)" + ], + "framework": "jss:: (JSON field naming), toBase58 (key encoding), DB layer (insertPeerReservation)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none", + "field": "description", + "location": "PeerReservation::toJson", + "validated_by": "std::string::empty()", + "validates": [ + "Checks if description is non-empty before adding to JSON output" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "Exception if nodeId is invalid (depends on toBase58 implementation)", + "field": "nodeId", + "location": "PeerReservation::toJson", + "validated_by": "toBase58(TokenType::NodePublic, nodeId)", + "validates": [ + "Ensures nodeId is a valid NodePublic key and can be encoded as Base58" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "reservation (PeerReservation)", + "location": "PeerReservationTable::insert_or_assign", + "validated_by": "std::unordered_set::find", + "validates": [ + "Checks if reservation already exists before inserting" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (unless memory allocation fails)", + "field": "table_ (PeerReservation set)", + "location": "PeerReservationTable::insert_or_assign", + "validated_by": "std::unordered_set::insert", + "validates": [ + "Ensures only valid PeerReservation objects are stored" + ], + "validation_type": "type" + }, + { + "confidence": 0.7, + "error_thrown": "Depends on insertPeerReservation (likely exception or error code on DB constraint violation)", + "field": "reservation.nodeId, reservation.description", + "location": "PeerReservationTable::insert_or_assign", + "validated_by": "insertPeerReservation", + "validates": [ + "Validates DB constraints for nodeId and description" + ], + "validation_type": "format|type|business_logic (depends on insertPeerReservation implementation)" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/PeerReservationTable.cpp.ai.md b/src/xrpld/overlay/detail/PeerReservationTable.cpp.ai.md new file mode 100644 index 0000000000..3d34277b13 --- /dev/null +++ b/src/xrpld/overlay/detail/PeerReservationTable.cpp.ai.md @@ -0,0 +1,33 @@ +# `PeerReservationTable.cpp` — Persistent Peer Slot Registry + +This file implements `PeerReservationTable` and the serialization method for `PeerReservation`, providing the XRPL overlay with a managed allowlist of nodes that are guaranteed a connection slot. When the overlay has reached its maximum peer count it will still accept inbound connections from reserved nodes, making this mechanism essential for validators that must maintain connectivity to trusted peers regardless of network load. + +## Structure and Lifecycle + +`PeerReservation` is a plain value type carrying a `PublicKey nodeId` and an optional human-readable `description`. Its identity for equality and hashing purposes is entirely determined by `nodeId` — the companion `KeyEqual` functor and the `hash_append` overload in the header both ignore `description`. This means two `PeerReservation` objects with the same public key but different descriptions are considered the same reservation, which is the intended semantic: a node either has a reserved slot or it doesn't. + +`PeerReservationTable` undergoes two-phase construction, a pattern forced on it by `ApplicationImp::setup`. The object is created first (with an optional `Journal`), and the database connection is only wired up later via `load()`. The class stores a raw `DatabaseCon*` pointer rather than owning the resource — `connection_` is a non-owning observer that remains valid for the lifetime of the application. All post-`load` mutations (`insert_or_assign`, `erase`) call `connection_->checkoutDb()` directly on this stored pointer, so `load()` must always be called before either mutation method. + +## The `load()` Non-Failure Contract + +`load()` has an intentionally lenient error contract: it always returns `true`. The comment explains this is to fit the error-handling convention of `ApplicationImp::setup`, where `false` signals a fatal startup failure. Because the application can always start with an empty reservation table and reconcile state later, a database read failure during startup is treated as yielding zero reservations rather than aborting the process. This is a deliberate resilience decision, not an oversight. + +## Concurrency Model + +All four public methods lock `mutex_` via `std::lock_guard`, making the table safe for concurrent use by the overlay (which checks `contains()` on inbound connections) and the RPC handlers (which call `insert_or_assign` and `erase`). The `list()` method minimizes lock hold time with a deliberate scope split: it copies the set into a `std::vector` while holding the lock, then releases the lock before calling `std::sort`. Sorting a snapshot outside the lock is correct because `list()` is used only for informational RPC responses, where a momentary stale view is acceptable. + +## The `insert_or_assign` Workaround + +`std::unordered_set` has no `insert_or_assign` method, and its iterator-based API makes an efficient in-place update impossible without a `find`+`erase`+`insert` sequence. The code acknowledges this explicitly with a reference to a Stack Overflow discussion of the container design limitation. The implementation saves the hint iterator's successor before erasing (since the found position becomes invalid after erase), then passes that hint to `insert`. In practice this is inconsequential because the reservation table is tiny and mutations are infrequent admin operations, but the code is careful to do it correctly anyway. + +The method returns a `std::optional` containing the displaced reservation if one existed. This enables RPC handlers to report the previous state in their response without a separate lookup. + +## Database Persistence Layer + +All persistence is delegated to three free functions in `src/libxrpl/server/Wallet.cpp`: `getPeerReservationTable`, `insertPeerReservation`, and `deletePeerReservation`. These functions use SOCI to interact with a `PeerReservations` SQL table (defined in `include/xrpl/rdb/DBInit.h`). `insertPeerReservation` uses an upsert idiom (`INSERT ... ON CONFLICT ... DO UPDATE SET Description=excluded.Description`) so it naturally handles both new insertions and description updates without needing separate SQL for each case. The in-memory erase-before-insert in `insert_or_assign` mirrors this upsert semantic at the application level. + +`erase()` is correctly defensive: it only issues the `DELETE` SQL if the key was found in the in-memory set, avoiding a no-op database round-trip for unknown node IDs. Like `insert_or_assign`, it returns the removed reservation wrapped in `std::optional`, again to support informative RPC responses. + +## JSON Serialization + +`PeerReservation::toJson()` encodes `nodeId` as a Base58-encoded node public key string under `jss::node`. The `description` field is omitted entirely when empty, keeping the JSON compact. The encoding via `toBase58(TokenType::NodePublic, ...)` is the canonical on-wire representation of node identities throughout XRPL — the same format used in configuration files and peer protocol messages. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/PeerSet.cpp.ai.json b/src/xrpld/overlay/detail/PeerSet.cpp.ai.json new file mode 100644 index 0000000000..42dbf1221f --- /dev/null +++ b/src/xrpld/overlay/detail/PeerSet.cpp.ai.json @@ -0,0 +1,430 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app" + ], + "lineno": 8, + "name": "PeerSetImpl" + }, + { + "args": [ + "Application& app" + ], + "lineno": 62, + "name": "PeerSetBuilderImpl" + }, + { + "args": [ + "Application& app" + ], + "lineno": 79, + "name": "DummyPeerSet" + } + ], + "code_paths": [ + { + "call_chain": [ + "PeerSetImpl::addPeers", + "overlay.foreach (lambda)", + "peer->getScore", + "std::sort", + "for (auto const& pair : pairs)", + "peers_.insert(peer->id())", + "onPeerAdded(peer)" + ], + "entry_point": "PeerSetImpl::addPeers", + "purpose": "Adds up to 'limit' new peers to the PeerSet, scoring and sorting them, and invoking a callback for each added peer.", + "validation_points": [ + "peers_.insert(peer->id()).second (validates peer uniqueness)", + "if (++accepted >= limit) break; (validates limit)" + ] + }, + { + "call_chain": [ + "PeerSetImpl::sendRequest", + "if (peer)", + "peer->send(packet)", + "else", + "for (auto id : peers_)", + "app_.getOverlay().findPeerByShortID(id)", + "p->send(packet)" + ], + "entry_point": "PeerSetImpl::sendRequest", + "purpose": "Sends a message to a specific peer if provided, or to all peers in the set.", + "validation_points": [ + "if (peer) (validates peer is not null)" + ] + }, + { + "call_chain": [ + "PeerSetImpl::getPeerIds" + ], + "entry_point": "PeerSetImpl::getPeerIds", + "purpose": "Returns the set of peer IDs currently tracked.", + "validation_points": [] + }, + { + "call_chain": [ + "PeerSetBuilderImpl::build", + "std::make_unique(app_)" + ], + "entry_point": "PeerSetBuilderImpl::build", + "purpose": "Constructs a new PeerSetImpl instance.", + "validation_points": [] + }, + { + "call_chain": [ + "DummyPeerSet::addPeers / sendRequest / getPeerIds" + ], + "entry_point": "DummyPeerSet::*", + "purpose": "Dummy implementations that log errors if called.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "peer (std::shared_ptr const&)", + "flow": [ + "addPeers/sendRequest parameter", + "overlay.foreach (addPeers) or direct use (sendRequest)", + "peer->id() (addPeers), peer->send(packet) (sendRequest)" + ], + "origin": "Passed into addPeers/sendRequest by caller", + "transformations": [ + "Scored in addPeers", + "Sorted in addPeers", + "Checked for uniqueness in addPeers", + "Used for sending in sendRequest" + ], + "validated_at": "if (peer) in sendRequest; peers_.insert(peer->id()).second in addPeers" + }, + { + "field": "peer->id() (Peer::id_t)", + "flow": [ + "peer->id()", + "peers_.insert(peer->id())", + "peers_ set" + ], + "origin": "peer object", + "transformations": [ + "Inserted into set to ensure uniqueness" + ], + "validated_at": "peers_.insert(peer->id()).second (addPeers)" + }, + { + "field": "limit (std::size_t)", + "flow": [ + "addPeers parameter", + "for loop in addPeers", + "if (++accepted >= limit) break;" + ], + "origin": "Passed into addPeers by caller", + "transformations": [ + "Incremented accepted counter" + ], + "validated_at": "if (++accepted >= limit) break;" + }, + { + "field": "peers_ (std::set)", + "flow": [ + "populated in addPeers via peers_.insert(peer->id())", + "read in sendRequest (for broadcasting)", + "read in getPeerIds" + ], + "origin": "PeerSetImpl member, initially empty", + "transformations": [ + "Grows as new unique peers are added" + ], + "validated_at": "peers_.insert(peer->id()).second (ensures uniqueness)" + } + ], + "description": "Implements PeerSet and PeerSetBuilder classes for managing sets of peers in the XRPL overlay network, including logic for adding peers, sending requests, and dummy/test implementations.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "peer (std::shared_ptr const&)", + "empty", + "string", + "validation" + ], + "evidence": "if (peer) at PeerSetImpl::sendRequest", + "issue_pattern": "Missing empty string validation for peer (std::shared_ptr const&)", + "why_false_positive": "if (peer) validates peer (std::shared_ptr const&) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "peer->id() (Peer::id_t)", + "empty", + "string", + "validation" + ], + "evidence": "peers_.insert(peer->id()).second at PeerSetImpl::addPeers", + "issue_pattern": "Missing empty string validation for peer->id() (Peer::id_t)", + "why_false_positive": "peers_.insert(peer->id()).second validates peer->id() (Peer::id_t) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "limit (std::size_t)", + "empty", + "string", + "validation" + ], + "evidence": "if (++accepted >= limit) break; at PeerSetImpl::addPeers", + "issue_pattern": "Missing empty string validation for limit (std::size_t)", + "why_false_positive": "if (++accepted >= limit) break; validates limit (std::size_t) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/PeerSet.cpp", + "functions": [ + { + "args": [ + "Application& app" + ], + "lineno": 20, + "name": "PeerSetImpl::PeerSetImpl" + }, + { + "args": [ + "std::size_t limit", + "std::function const&)> hasItem", + "std::function const&)> onPeerAdded" + ], + "lineno": 24, + "name": "PeerSetImpl::addPeers" + }, + { + "args": [ + "::google::protobuf::Message const& message", + "protocol::MessageType type", + "std::shared_ptr const& peer" + ], + "lineno": 41, + "name": "PeerSetImpl::sendRequest" + }, + { + "args": [], + "lineno": 59, + "name": "PeerSetImpl::getPeerIds" + }, + { + "args": [ + "Application& app" + ], + "lineno": 64, + "name": "PeerSetBuilderImpl::PeerSetBuilderImpl" + }, + { + "args": [], + "lineno": 68, + "name": "PeerSetBuilderImpl::build" + }, + { + "args": [ + "Application& app" + ], + "lineno": 76, + "name": "make_PeerSetBuilder" + }, + { + "args": [ + "Application& app" + ], + "lineno": 81, + "name": "DummyPeerSet::DummyPeerSet" + }, + { + "args": [ + "std::size_t limit", + "std::function const&)> hasItem", + "std::function const&)> onPeerAdded" + ], + "lineno": 85, + "name": "DummyPeerSet::addPeers" + }, + { + "args": [ + "::google::protobuf::Message const& message", + "protocol::MessageType type", + "std::shared_ptr const& peer" + ], + "lineno": 92, + "name": "DummyPeerSet::sendRequest" + }, + { + "args": [], + "lineno": 98, + "name": "DummyPeerSet::getPeerIds" + }, + { + "args": [ + "Application& app" + ], + "lineno": 108, + "name": "make_DummyPeerSet" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, PeerSet and PeerSetImpl would be tested via integration or unit tests in files like test_overlay.cpp, test_PeerSet.cpp, or similar. The validation logic (peer uniqueness, limit enforcement, null peer checks) would need explicit tests for: (1) adding duplicate peers, (2) exceeding the limit, (3) sending to null vs. specific peers. If such tests do not exist, these paths may not be fully covered. DummyPeerSet is likely only tested for error logging if at all. Gaps: No evidence of tests for edge cases (e.g., empty overlay, all peers already present, limit=0, null peer in sendRequest).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation, C++ STL containers)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (branching logic)", + "field": "peer (std::shared_ptr const&)", + "location": "PeerSetImpl::sendRequest", + "validated_by": "if (peer)", + "validates": [ + "Checks if peer is not null before calling peer->send(packet)" + ], + "validation_type": "type/null-check" + }, + { + "confidence": 1.0, + "error_thrown": "none (continues loop if duplicate)", + "field": "peer->id() (Peer::id_t)", + "location": "PeerSetImpl::addPeers", + "validated_by": "peers_.insert(peer->id()).second", + "validates": [ + "Ensures peer is not already in peers_ set before adding" + ], + "validation_type": "business_logic (uniqueness)" + }, + { + "confidence": 1.0, + "error_thrown": "none (loop break)", + "field": "limit (std::size_t)", + "location": "PeerSetImpl::addPeers", + "validated_by": "if (++accepted >= limit) break;", + "validates": [ + "Ensures no more than 'limit' peers are added" + ], + "validation_type": "range (upper bound)" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/PeerSet.cpp.ai.md b/src/xrpld/overlay/detail/PeerSet.cpp.ai.md new file mode 100644 index 0000000000..169e7e7376 --- /dev/null +++ b/src/xrpld/overlay/detail/PeerSet.cpp.ai.md @@ -0,0 +1,35 @@ +# `src/xrpld/overlay/detail/PeerSet.cpp` + +## Role in the System + +This file provides the concrete implementations backing the `PeerSet` and `PeerSetBuilder` abstract interfaces declared in `PeerSet.h`. A `PeerSet` is the mechanism by which data-acquisition subsystems — `InboundLedger`, `TransactionAcquire`, `LedgerDeltaAcquire`, and similar — select a working subset of overlay peers to query for missing data (ledgers, transaction sets, skip lists). The file defines three distinct classes: `PeerSetImpl` (the live implementation), `PeerSetBuilderImpl` (a factory), and `DummyPeerSet` (a null-object for offline loading). + +## `PeerSetImpl` — Scored Peer Selection and Broadcast + +`PeerSetImpl` holds a `std::set` rather than a set of `shared_ptr`. This is a deliberate design choice: overlay peers can disconnect at any moment, and storing numeric IDs avoids holding long-lived `shared_ptr` references that would keep disconnected peer objects alive. Peer pointers are only resolved transiently via `overlay.findPeerByShortID()` at the moment a message needs to be sent. + +### `addPeers()` + +The algorithm for adding peers is more sophisticated than a simple random selection. It asks every connected peer for a score via `peer->getScore(hasItem(peer))`, where `hasItem` is a caller-supplied predicate that returns `true` if the peer already possesses the target data. By passing this boolean into `getScore`, the scoring function can rank peers that are known to have the item higher, making it likely that requests reach useful peers first. The resulting `(score, peer)` pairs are sorted descending, and up to `limit` peers that are not already in `peers_` are accepted. Uniqueness is enforced implicitly via the `std::set::insert` return value — a `false` second element means the ID is already tracked and the peer is skipped without calling `onPeerAdded`. + +The two callback parameters (`hasItem` and `onPeerAdded`) keep this class completely domain-agnostic. `PeerSet` never needs to know whether the data being retrieved is a ledger, a transaction set, or anything else — callers inject their own logic. `InboundLedger`, for instance, passes `peer->hasLedger(hash_, mSeq)` as `hasItem` and a lambda that sends an initial fetch request as `onPeerAdded`. + +### `sendRequest()` + +`sendRequest()` handles both unicast and broadcast in a single method. When the `peer` parameter is non-null, the message is sent only to that peer. When it is null, the method iterates over all tracked `Peer::id_t` values, resolves each to a live `Peer` via `findPeerByShortID`, and calls `send()` only if the peer is still connected (a null return from `findPeerByShortID` is silently skipped). The `Message` object wrapping the protobuf payload is allocated once as a `shared_ptr` and shared across all send calls in the broadcast path, avoiding redundant serialization. + +The public `sendRequest()` template in the header derives the `protocol::MessageType` enum value via `protocolMessageType(message)` and delegates to this virtual method, so callers never have to specify the type tag manually. + +## `PeerSetBuilderImpl` — Factory Pattern for Testability + +Rather than constructing `PeerSetImpl` directly, consumers receive a `PeerSetBuilder` whose `build()` method produces a fresh `PeerSet`. This indirection allows test code to substitute a mock builder that returns instrumented or no-op `PeerSet` instances without touching production wiring. The `make_PeerSetBuilder(Application&)` free function is the entry point used by real application code to obtain a `PeerSetBuilderImpl`. + +## `DummyPeerSet` — Null-Object for Offline Ledger Loading + +`DummyPeerSet` exists for one specific scenario documented in `PeerSet.h`: `ApplicationImp::loadOldLedger()`. When the node is replaying or loading a historical ledger at startup from local storage, an `InboundLedger` object is still constructed (to reuse its ledger-assembly logic), but no peer communication should occur. Rather than adding special-case branches throughout `InboundLedger`, a `DummyPeerSet` is injected. All three interface methods log an error if called — they represent programming errors if triggered in this context, not expected no-ops — and `getPeerIds()` returns a reference to a `static` empty set to satisfy the return type contract without undefined behavior. + +This is a clean application of the null-object pattern: the shape of the interface is preserved, the surrounding code needs no conditional guards, and any accidental peer interaction during offline loading surfaces immediately as an error log rather than silently misbehaving. + +## Invariants and Failure Modes + +The `peers_` set provides an automatic duplicate-prevention invariant: a peer that has already contributed to a retrieval attempt cannot be re-added by a subsequent `addPeers()` call. This matters because `addPeers()` is called repeatedly as a retrieval times out and retries — the growing `peers_` set acts as an exclusion list ensuring each peer is tried at most once per acquisition. If all overlay peers have been exhausted (i.e., every scored peer is already in `peers_`), `addPeers()` simply adds zero new peers without error, leaving the higher-level `TimeoutCounter` to decide whether to declare the acquisition failed. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h.ai.json b/src/xrpld/overlay/detail/ProtocolMessage.h.ai.json new file mode 100644 index 0000000000..53ecd37308 --- /dev/null +++ b/src/xrpld/overlay/detail/ProtocolMessage.h.ai.json @@ -0,0 +1,102 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 61, + "name": "MessageHeader" + } + ], + "description": "This file provides utilities for parsing, handling, and invoking protocol messages in the XRPL overlay network, including support for compressed and uncompressed messages, message header parsing, and dispatching to appropriate protocol message handlers.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/ProtocolMessage.h", + "functions": [ + { + "args": [ + "protocol::TMGetLedger const&" + ], + "lineno": 13, + "name": "protocolMessageType" + }, + { + "args": [ + "protocol::TMReplayDeltaRequest const&" + ], + "lineno": 18, + "name": "protocolMessageType" + }, + { + "args": [ + "protocol::TMProofPathRequest const&" + ], + "lineno": 23, + "name": "protocolMessageType" + }, + { + "args": [ + "int type" + ], + "lineno": 28, + "name": "protocolMessageName" + }, + { + "args": [ + "BufferSequence const& bufs" + ], + "lineno": 74, + "name": "buffersBegin" + }, + { + "args": [ + "BufferSequence const& bufs" + ], + "lineno": 80, + "name": "buffersEnd" + }, + { + "args": [ + "boost::system::error_code& ec", + "BufferSequence const& bufs", + "std::size_t size" + ], + "lineno": 87, + "name": "parseMessageHeader" + }, + { + "args": [ + "MessageHeader const& header", + "Buffers const& buffers" + ], + "lineno": 154, + "name": "parseMessageContent" + }, + { + "args": [ + "MessageHeader const& header", + "Buffers const& buffers", + "Handler& handler" + ], + "lineno": 181, + "name": "invoke" + }, + { + "args": [ + "Buffers const& buffers", + "Handler& handler", + "std::size_t& hint" + ], + "lineno": 206, + "name": "invokeProtocolMessage" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 58, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ProtocolMessage.h.ai.md b/src/xrpld/overlay/detail/ProtocolMessage.h.ai.md new file mode 100644 index 0000000000..5e82fc1902 --- /dev/null +++ b/src/xrpld/overlay/detail/ProtocolMessage.h.ai.md @@ -0,0 +1,59 @@ +# `ProtocolMessage.h` — Overlay Wire Protocol Deserialization + +This header is the message-decoding spine of the XRPL overlay P2P network. It owns the entire pipeline from raw Boost.Asio buffer sequences — as received from a peer TCP connection — to typed protobuf message objects dispatched to a handler. No other layer touches the on-wire bytes for protocol messages; everything passes through here. + +## Wire Format and `MessageHeader` + +The XRPL overlay uses two distinct header layouts, distinguished by whether the high bit of the first byte is set. + +**Uncompressed (6 bytes):** The top 6 bits of the first byte must be zero — they act as a format guard. The remaining 26 bits encode the payload size. The next 2 bytes hold the message type. + +**Compressed (10 bytes):** The high bit is always 1, allowing `parseMessageHeader()` to branch on `*iter & 0x80`. The next 3 bits carry the compression algorithm in the top nibble of the first byte (`0x90` for LZ4; bits 2–3 must be zero and are validated). The 26-bit payload size follows, then 2 bytes of message type, then 4 bytes of uncompressed size. The extra 4 bytes explain the jump from 6 to 10 bytes — the receiver needs the uncompressed size to allocate the decompression target buffer. + +`MessageHeader` is a plain struct that captures all decoded fields: `total_wire_size` (header + payload), `header_size` (6 or 10), `payload_wire_size` (compressed or raw payload), `uncompressed_size`, `message_type`, and the `compression::Algorithm` enum. Keeping these pre-computed avoids re-parsing during the content phase. + +## Three-Stage Decoding Pipeline + +### `parseMessageHeader()` + +A buffer-sequence iterator walks the first bytes and populates a `MessageHeader`, returning `std::optional`. The error code distinguishes three states: + +- `errc::success` with a null optional: not enough bytes yet — caller should wait for more data. +- `errc::no_message`: the header guard bits didn't match either format — malformed stream, drop the connection. +- `errc::protocol_error`: bits 2–3 of the first byte were set, or the algorithm identifier isn't LZ4 — invalid compression framing. + +This three-way return avoids exception overhead on the hot path, which is important since this runs once per received message. + +### `parseMessageContent()` + +A `std::enable_if` guard constrains `T` to subclasses of `google::protobuf::Message`, catching misuse at compile time. The function constructs a `ZeroCopyInputStream` adapter (defined in `ZeroCopyStream.h`) over the raw buffer sequence, then calls `Skip(header.header_size)` to skip past the header bytes without copying them. + +For uncompressed messages, protobuf's `ParseFromZeroCopyStream` reads directly from the adapter, incurring zero extra copies even when the data spans multiple non-contiguous ASIO buffer segments. + +For compressed messages, the path must allocate a contiguous `std::vector` of `header.uncompressed_size` bytes, run `xrpl::compression::decompress()` (LZ4 via `ZeroCopyInputStream`), and then call `ParseFromArray`. The copy is unavoidable because LZ4 decompression requires a contiguous output region and protobuf parsing also needs one. The allocation is bounded by the 64 MiB `maximumMessageSize` check applied before this function is reached. + +### `invoke()` + +Glues parsing to dispatch. It calls `parseMessageContent`, then fires three callbacks on the handler: `onMessageBegin()` (receives wire size, uncompressed size, and a compression flag — used by `PeerImp` for traffic metrics), `onMessage()` (the typed processing callback), and `onMessageEnd()` (post-message hook). Separating begin/end from the message itself lets the handler bracket processing with timing or resource accounting. + +## Public Entry Point: `invokeProtocolMessage()` + +This is the single function called from `PeerImp`'s read loop. It: + +1. Calls `parseMessageHeader()` and returns early if the header is incomplete or malformed. +2. Enforces a 64 MiB ceiling on both `payload_wire_size` and `uncompressed_size` via `maximumMessageSize` — a safeguard against memory exhaustion attacks. +3. Checks `handler.compressionEnabled()`. If the peer negotiated uncompressed-only but sent a compressed header, the connection is closed with `protocol_error`. This prevents a peer from forcing CPU work without negotiation. +4. If `total_wire_size > size`, sets `hint = total_wire_size - size` — the exact number of bytes still needed — and returns zero consumed bytes. The caller passes this hint back to the ASIO read operation to reduce system call overhead. +5. Dispatches on `header->message_type` via an exhaustive `switch`, instantiating `detail::invoke()` for each known protobuf type. Unknown types call `handler.onMessageUnknown()` and return success, enabling forward-compatible protocol evolution without hard failures. + +The function returns `{bytes_consumed, error_code}`. A zero `bytes_consumed` with no error means "incomplete — try again later"; a non-zero error means the peer should be disconnected. + +## Handler Concept + +`Handler` is an unconstrained template parameter, resolved duck-typing style. The concrete implementation is `PeerImp`, which provides `compressionEnabled()`, `onMessageBegin()`, `onMessage()` (via overload for each protobuf type), `onMessageEnd()`, and `onMessageUnknown()`. Using a template rather than a virtual base class lets the compiler inline all dispatch at the call site — important given that `invokeProtocolMessage` sits directly on the message-processing hot path. + +## Supporting Utilities + +`protocolMessageName()` maps integer type codes to human-readable strings for logging. The three `protocolMessageType()` overloads do the inverse for a small subset of types (`TMGetLedger`, `TMReplayDeltaRequest`, `TMProofPathRequest`), used by outbound code that needs to construct a wire header from a message object rather than a numeric constant. + +`buffersBegin()` and `buffersEnd()` are small helpers that instantiate `boost::asio::buffers_iterator` with a fixed `uint8_t` value type, preventing repeated verbose template instantiation throughout `parseMessageHeader`. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ProtocolVersion.cpp.ai.json b/src/xrpld/overlay/detail/ProtocolVersion.cpp.ai.json new file mode 100644 index 0000000000..10f26c833e --- /dev/null +++ b/src/xrpld/overlay/detail/ProtocolVersion.cpp.ai.json @@ -0,0 +1,356 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "parseProtocolVersions" + ], + "entry_point": "parseProtocolVersions", + "purpose": "Parses a comma-separated string of protocol version tokens (e.g., 'XRPL/2.1,XRPL/2.2') into a vector of ProtocolVersion, validating each token.", + "validation_points": [ + "boost::regex_match: Validates protocol version string format", + "beast::lexicalCastChecked: Validates major version is a valid integer", + "beast::lexicalCastChecked: Validates minor version is a valid integer", + "to_string(proto) == s: Sanity check that parsed version matches input string" + ] + }, + { + "call_chain": [ + "static_assert (lambda)" + ], + "entry_point": "supportedProtocolList (static initialization)", + "purpose": "At compile time, validates that the supportedProtocolList is sorted and contains no duplicates.", + "validation_points": [ + "static_assert: Validates supportedProtocolList order and uniqueness" + ] + }, + { + "call_chain": [ + "negotiateProtocolVersion" + ], + "entry_point": "negotiateProtocolVersion", + "purpose": "Negotiates the highest mutually supported protocol version between local and peer lists.", + "validation_points": [ + "Relies on parseProtocolVersions to provide validated input" + ] + }, + { + "call_chain": [ + "isProtocolSupported" + ], + "entry_point": "isProtocolSupported", + "purpose": "Checks if a given ProtocolVersion is in supportedProtocolList.", + "validation_points": [ + "Relies on prior validation of ProtocolVersion construction" + ] + } + ], + "data_flows": [ + { + "field": "protocol version string (e.g., 'XRPL/2.1')", + "flow": [ + "Input string", + "beast::rfc2616::split_commas (splits into tokens)", + "boost::regex_match (validates format)", + "beast::lexicalCastChecked (parses major/minor)", + "make_protocol (constructs ProtocolVersion)", + "to_string(proto) == s (sanity check)", + "result vector" + ], + "origin": "Input to parseProtocolVersions (from network or config)", + "transformations": [ + "Split into tokens", + "Regex validation", + "String to integer conversion", + "ProtocolVersion construction", + "Sanity check" + ], + "validated_at": "boost::regex_match, beast::lexicalCastChecked, to_string(proto) == s" + }, + { + "field": "supportedProtocolList", + "flow": [ + "Definition", + "static_assert lambda (compile-time validation)", + "Used in negotiateProtocolVersion, isProtocolSupported" + ], + "origin": "Static constexpr array in source file", + "transformations": [ + "Compile-time validation for order and uniqueness" + ], + "validated_at": "static_assert (compile time)" + }, + { + "field": "ProtocolVersion (major, minor)", + "flow": [ + "Extracted from regex match groups", + "beast::lexicalCastChecked (string to uint16_t)", + "make_protocol (constructs ProtocolVersion)", + "Used in negotiation and support checks" + ], + "origin": "Parsed from protocol version string in parseProtocolVersions", + "transformations": [ + "String to integer conversion", + "Struct construction" + ], + "validated_at": "beast::lexicalCastChecked" + } + ], + "description": "Implements protocol version negotiation, parsing, and support checking for XRPL overlay communication, including utilities for string conversion and validation of supported protocol versions.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "supportedProtocolList", + "empty", + "string", + "validation" + ], + "evidence": "static_assert with custom lambda at global scope (static_assert after supportedProtocolList definition)", + "issue_pattern": "Missing empty string validation for supportedProtocolList", + "why_false_positive": "static_assert with custom lambda validates supportedProtocolList for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "protocol version string (e.g., 'XRPL/2.1')", + "empty", + "string", + "validation" + ], + "evidence": "boost::regex_match at parseProtocolVersions", + "issue_pattern": "Missing empty string validation for protocol version string (e.g., 'XRPL/2.1')", + "why_false_positive": "boost::regex_match validates protocol version string (e.g., 'XRPL/2.1') for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "protocol version string (e.g., 'XRPL/2.1')", + "format", + "validation", + "invalid" + ], + "evidence": "boost::regex_match at parseProtocolVersions", + "issue_pattern": "Missing format validation for protocol version string (e.g., 'XRPL/2.1')", + "why_false_positive": "boost::regex_match validates protocol version string (e.g., 'XRPL/2.1') format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "major version (parsed from protocol string)", + "empty", + "string", + "validation" + ], + "evidence": "beast::lexicalCastChecked at parseProtocolVersions", + "issue_pattern": "Missing empty string validation for major version (parsed from protocol string)", + "why_false_positive": "beast::lexicalCastChecked validates major version (parsed from protocol string) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "major version (parsed from protocol string)", + "type", + "validation", + "check" + ], + "evidence": "beast::lexicalCastChecked at parseProtocolVersions", + "issue_pattern": "Missing type validation for major version (parsed from protocol string)", + "why_false_positive": "beast::lexicalCastChecked validates major version (parsed from protocol string) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "minor version (parsed from protocol string)", + "empty", + "string", + "validation" + ], + "evidence": "beast::lexicalCastChecked at parseProtocolVersions", + "issue_pattern": "Missing empty string validation for minor version (parsed from protocol string)", + "why_false_positive": "beast::lexicalCastChecked validates minor version (parsed from protocol string) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "minor version (parsed from protocol string)", + "type", + "validation", + "check" + ], + "evidence": "beast::lexicalCastChecked at parseProtocolVersions", + "issue_pattern": "Missing type validation for minor version (parsed from protocol string)", + "why_false_positive": "beast::lexicalCastChecked validates minor version (parsed from protocol string) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "protocol version string (sanity check)", + "empty", + "string", + "validation" + ], + "evidence": "to_string(proto) == s at parseProtocolVersions", + "issue_pattern": "Missing empty string validation for protocol version string (sanity check)", + "why_false_positive": "to_string(proto) == s validates protocol version string (sanity check) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/ProtocolVersion.cpp", + "functions": [ + { + "args": [ + "ProtocolVersion const& p" + ], + "lineno": 38, + "name": "to_string" + }, + { + "args": [ + "boost::beast::string_view const& value" + ], + "lineno": 43, + "name": "parseProtocolVersions" + }, + { + "args": [ + "std::vector const& versions" + ], + "lineno": 77, + "name": "negotiateProtocolVersion" + }, + { + "args": [ + "boost::beast::string_view const& versions" + ], + "lineno": 95, + "name": "negotiateProtocolVersion" + }, + { + "args": [], + "lineno": 101, + "name": "supportedProtocolVersions" + }, + { + "args": [ + "ProtocolVersion const& v" + ], + "lineno": 117, + "name": "isProtocolSupported" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested by unit tests for protocol negotiation, parsing, and validation. Look for test files such as 'ProtocolVersion_test.cpp', 'Overlay_test.cpp', or integration tests for peer negotiation. Gaps may exist if there are no tests for malformed protocol strings, duplicate/unsorted supportedProtocolList, or edge cases in version negotiation. Compile-time static_assert is not directly testable at runtime, so errors here would only be caught at build time.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "boost::regex, beast::lexicalCastChecked, static_assert", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "compile-time error (static_assert failure)", + "field": "supportedProtocolList", + "location": "global scope (static_assert after supportedProtocolList definition)", + "validated_by": "static_assert with custom lambda", + "validates": [ + "List is not empty", + "List is sorted in strictly ascending order", + "List contains no duplicates" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "input is skipped (no exception, just not parsed)", + "field": "protocol version string (e.g., 'XRPL/2.1')", + "location": "parseProtocolVersions", + "validated_by": "boost::regex_match", + "validates": [ + "String starts with 'XRPL/'", + "Major version is a number >= 2, no leading zeroes", + "Minor version is a number, no leading zeroes unless zero", + "String matches the exact pattern" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "input is skipped (no exception, just not parsed)", + "field": "major version (parsed from protocol string)", + "location": "parseProtocolVersions", + "validated_by": "beast::lexicalCastChecked", + "validates": [ + "Major version can be converted to uint16_t" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "input is skipped (no exception, just not parsed)", + "field": "minor version (parsed from protocol string)", + "location": "parseProtocolVersions", + "validated_by": "beast::lexicalCastChecked", + "validates": [ + "Minor version can be converted to uint16_t" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "input is skipped (no exception, just not parsed)", + "field": "protocol version string (sanity check)", + "location": "parseProtocolVersions", + "validated_by": "to_string(proto) == s", + "validates": [ + "Parsed protocol version, when converted back to string, matches original input" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ProtocolVersion.cpp.ai.md b/src/xrpld/overlay/detail/ProtocolVersion.cpp.ai.md new file mode 100644 index 0000000000..ae8be48db2 --- /dev/null +++ b/src/xrpld/overlay/detail/ProtocolVersion.cpp.ai.md @@ -0,0 +1,37 @@ +# ProtocolVersion.cpp — Overlay Protocol Version Negotiation + +This file implements the protocol version negotiation machinery used when two XRPL nodes establish a peer connection over the overlay network. During the HTTP upgrade handshake that initiates a peer session, each node advertises which protocol versions it supports, and the two sides must agree on the highest mutually supported version. This file owns the entire lifecycle of that negotiation: declaring what the local node supports, advertising it in HTTP headers, parsing what a peer sends, and picking the winner. + +## The Supported Protocol Table + +The canonical source of truth is `supportedProtocolList`, a `constexpr` array of `ProtocolVersion` pairs (currently `{2,1}` and `{2,2}`). The design choice to make this a compile-time constant rather than a runtime configuration is intentional — the set of versions a given binary speaks is a fixed property of that build, not something that should vary between runs or be configurable. + +The compile-time `static_assert` that immediately follows the declaration is a notable defensive pattern. Because `std::is_sorted` is not `constexpr`-compatible prior to C++20, the code rolls its own loop inside a lambda to verify that the list is strictly ascending and non-empty. If a developer accidentally adds a duplicate or out-of-order entry, the build fails with a clear message. The comment explicitly notes this can be simplified once the codebase moves to C++20. + +## Parsing: Defense in Depth + +`parseProtocolVersions()` accepts an RFC 2616-style comma-separated header value (the HTTP `Upgrade` header) and returns only those tokens that are valid protocol version strings. The parsing applies three layers of validation before accepting any entry: + +1. **Regex format check** — a `boost::regex` compiled once as a static local validates the exact lexical form: `XRPL/` prefix, a major version that is either a single digit `2–9` or a multi-digit number with no leading zeroes, a literal `.`, and a minor version that is either `0` or a non-zero-leading number. This also implicitly rejects the legacy `RTXP/x.y` format used by older nodes. + +2. **Lexical cast** — even after the regex passes, `beast::lexicalCastChecked` converts each captured group to `uint16_t`. This guards against numbers that are syntactically valid but would overflow a 16-bit integer (e.g., `XRPL/99999.0` passes the regex but fails the cast). + +3. **Round-trip sanity check** — after constructing the `ProtocolVersion` struct via `make_protocol()`, the code converts it back to string with `to_string()` and verifies it equals the original token. This eliminates any edge case where the regex or integer conversion could produce a value whose canonical string form differs from the input — for instance catching phantom discrepancies introduced by leading zeros that somehow survived earlier checks. + +After collection, the result is sorted and deduplicated. This guarantees the contract stated in the header comment: the returned vector is always in strictly ascending order with no duplicates, which is a prerequisite for the set-intersection logic in `negotiateProtocolVersion`. + +## Negotiation: Highest Common Version + +`negotiateProtocolVersion()` is overloaded. The string-view overload delegates to `parseProtocolVersions` first; the vector overload contains the actual logic. The goal is to find the highest version present in both the peer's list and the local `supportedProtocolList`. + +The implementation uses `std::set_intersection` with a `boost::make_function_output_iterator` wrapping a lambda that simply overwrites a single `std::optional` with each item it receives. Because `set_intersection` produces output in sorted order, the final value written into the optional is always the greatest element in the intersection. This avoids allocating a temporary container just to read `back()` — an elegant pattern that trades readability for allocation efficiency. + +If no intersection exists (the peer speaks only versions this node doesn't recognize), the optional remains empty, and the caller can close the connection. + +## Integration with the Handshake + +Looking at `Handshake.cpp`, the outbound connection path calls `supportedProtocolVersions()` to populate the HTTP `Upgrade` header before sending the connection request. `supportedProtocolVersions()` returns a reference to a `static` `std::string` that is built exactly once via a lazy initializer — iterating `supportedProtocolList` and joining the entries as `"XRPL/2.1, XRPL/2.2"`. This avoids repeated string allocation on every outgoing handshake. + +In `ConnectAttempt.cpp`, when processing the peer's HTTP 101 response, the code takes a different path than `negotiateProtocolVersion`: it explicitly calls `parseProtocolVersions` on the `Upgrade` header of the response, checks that the peer selected exactly one version, and then calls `isProtocolSupported()` to verify that the peer's selection is actually in the local table. This asymmetry exists because in the upgrade response the peer selects a single version; using `negotiateProtocolVersion` here would silently accept a fraudulently advertised version that the local node doesn't actually speak. `isProtocolSupported()` performs a linear search through the small `supportedProtocolList` array — entirely appropriate given the list will never have more than a handful of entries. + +The overall design enforces a strict separation between what is acceptable to send (the static table) and what might arrive from untrusted peers (the parser), with the negotiation logic operating in the clean, validated domain of sorted vectors. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ProtocolVersion.h.ai.json b/src/xrpld/overlay/detail/ProtocolVersion.h.ai.json new file mode 100644 index 0000000000..3963722583 --- /dev/null +++ b/src/xrpld/overlay/detail/ProtocolVersion.h.ai.json @@ -0,0 +1,92 @@ +{ + "args": [ + { + "lineno": 17, + "name": "major" + }, + { + "lineno": 17, + "name": "minor" + }, + { + "lineno": 23, + "name": "p" + }, + { + "lineno": 32, + "name": "s" + }, + { + "lineno": 44, + "name": "versions" + }, + { + "lineno": 48, + "name": "versions" + }, + { + "lineno": 54, + "name": "v" + } + ], + "classes": [], + "description": "Defines the ProtocolVersion type and declares functions for handling XRPL peer-to-peer protocol versions, including parsing, negotiation, and support checks.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/ProtocolVersion.h", + "functions": [ + { + "args": [ + "major", + "minor" + ], + "lineno": 17, + "name": "make_protocol" + }, + { + "args": [ + "p" + ], + "lineno": 23, + "name": "to_string" + }, + { + "args": [ + "s" + ], + "lineno": 32, + "name": "parseProtocolVersions" + }, + { + "args": [ + "versions" + ], + "lineno": 44, + "name": "negotiateProtocolVersion" + }, + { + "args": [ + "versions" + ], + "lineno": 48, + "name": "negotiateProtocolVersion" + }, + { + "args": [], + "lineno": 51, + "name": "supportedProtocolVersions" + }, + { + "args": [ + "v" + ], + "lineno": 54, + "name": "isProtocolSupported" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ProtocolVersion.h.ai.md b/src/xrpld/overlay/detail/ProtocolVersion.h.ai.md new file mode 100644 index 0000000000..04ed2275b0 --- /dev/null +++ b/src/xrpld/overlay/detail/ProtocolVersion.h.ai.md @@ -0,0 +1,35 @@ +# `ProtocolVersion.h` — XRPL Peer Protocol Version Negotiation Interface + +This header defines the type system and declares the full API for managing version negotiation of the XRPL peer-to-peer wire protocol. It lives in `overlay/detail/` because version negotiation is an implementation concern of the overlay layer, not part of its public interface to the rest of the node. + +## The `ProtocolVersion` Type + +`ProtocolVersion` is a simple type alias for `std::pair`, encoding a major and minor version number. Using `std::pair` rather than a dedicated struct is a deliberate design shortcut: pair's built-in lexicographic comparison makes `<`, `==`, and ordering just work — a critical property since much of the negotiation logic depends on sorting and set intersection over version lists. The `make_protocol(major, minor)` factory is a thin inline convenience wrapper that names the construction intent at call sites rather than having callers write raw brace-initialization. + +The canonical string form of a version is `"XRPL/major.minor"` (e.g. `"XRPL/2.1"`), produced by `to_string()`. This format is used directly as the value of the HTTP `Upgrade` header during the peer handshake. At the time of writing, the implementation supports `{2,1}` and `{2,2}`. + +## Parsing: `parseProtocolVersions()` + +`parseProtocolVersions()` takes a `boost::beast::string_view` containing the raw value of the HTTP `Upgrade` header — a comma-separated list of tokens such as `"RTXP/1.2, XRPL/2.1, XRPL/2.2"`. It extracts and returns only those tokens that represent valid XRPL protocol versions, applying three layers of validation in the implementation: + +1. **Regex filter**: only `XRPL/` prefixed tokens with a major version of 2 or higher (no leading zeros) and a valid minor (no leading zeros except plain `0`) pass through. This explicitly excludes the legacy `RTXP` protocol used before version 2. +2. **Numeric range check**: values are cast to `uint16_t` via `beast::lexicalCastChecked`, rejecting anything that overflows. +3. **Round-trip sanity check**: the parsed version is converted back to a string and compared to the original token, guarding against any edge case where parsing and formatting could diverge. + +The return value is guaranteed sorted in ascending order and free of duplicates, which is a prerequisite for the set-intersection logic in `negotiateProtocolVersion()`. + +## Negotiation: `negotiateProtocolVersion()` + +Two overloads handle the negotiation step. The `string_view` overload is a convenience form for callers that hold a raw header value (the common case in `OverlayImpl.cpp`); it delegates internally to `parseProtocolVersions()` and then calls the `vector` overload. The vector overload contains the actual logic: it computes the intersection between the peer's advertised versions and the local `supportedProtocolList` (the compile-time array of versions this build speaks), and returns the **highest** version in that intersection as a `std::optional`. Returning `std::nullopt` signals that no mutually acceptable version exists, and the connection should be rejected. + +The choice of highest version rather than lowest is intentional — it ensures both sides use the most capable protocol they both support, maximizing available features while maintaining backward compatibility with older peers. + +## Build-time Version Registry + +`supportedProtocolVersions()` returns a `const std::string&` holding the full, comma-separated `Upgrade` header value (e.g. `"XRPL/2.1, XRPL/2.2"`). This string is computed once at first call and cached in a function-local static, avoiding repeated allocation. It is used by `Handshake.cpp` when constructing the outbound HTTP `GET` request during peer connection establishment. + +`isProtocolSupported()` is a simple point-lookup into the same compile-time list, used when a specific version identity needs to be checked rather than a full negotiation performed. + +## Relationship to the Handshake Flow + +The full negotiation flow across the overlay: `Handshake.cpp:makeRequest()` inserts the result of `supportedProtocolVersions()` into the outbound `Upgrade` header. On the receiving side, `OverlayImpl.cpp` extracts that header value and calls `negotiateProtocolVersion()`. If negotiation succeeds, the resulting `ProtocolVersion` is passed into `makeResponse()` (back in `Handshake.cpp`) and stored on the `PeerImp` instance, where it governs message framing and feature availability for the entire lifetime of that peer session. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/TrafficCount.cpp.ai.json b/src/xrpld/overlay/detail/TrafficCount.cpp.ai.json new file mode 100644 index 0000000000..a5ff43b1fb --- /dev/null +++ b/src/xrpld/overlay/detail/TrafficCount.cpp.ai.json @@ -0,0 +1,392 @@ +{ + "args": [ + { + "lineno": 19, + "name": "message" + }, + { + "lineno": 20, + "name": "type" + }, + { + "lineno": 21, + "name": "inbound" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TrafficCount::categorize" + ], + "entry_point": "TrafficCount::categorize", + "purpose": "Categorizes a protobuf message by type and content for traffic counting, using validation on message type and fields.", + "validation_points": [ + "type_lookup.find(type)", + "type == protocol::mtHAVE_SET", + "dynamic_cast(&message)", + "msg->type() == protocol::liTS_CANDIDATE / liTX_NODE / liAS_NODE", + "!msg->has_requestcookie()", + "dynamic_cast(&message)", + "msg->itype() == protocol::liTS_CANDIDATE / liTX_NODE / liAS_NODE", + "msg->has_requestcookie()", + "dynamic_cast(&message)", + "msg->type() == protocol::TMGetObjectByHash::otLEDGER" + ] + } + ], + "data_flows": [ + { + "field": "type (protocol::MessageType)", + "flow": [ + "categorize(type)", + "type_lookup.find(type)", + "if (type == protocol::mtHAVE_SET)" + ], + "origin": "Passed as argument to TrafficCount::categorize", + "transformations": [ + "Looked up in type_lookup map", + "Compared to protocol::mtHAVE_SET" + ], + "validated_at": "type_lookup.find(type), type == protocol::mtHAVE_SET" + }, + { + "field": "message (protobuf::Message)", + "flow": [ + "categorize(message)", + "dynamic_cast(&message)", + "dynamic_cast(&message)", + "dynamic_cast(&message)" + ], + "origin": "Passed as argument to TrafficCount::categorize", + "transformations": [ + "Dynamic cast to specific message types" + ], + "validated_at": "dynamic_cast(&message), dynamic_cast(&message), dynamic_cast(&message)" + }, + { + "field": "msg->type() (protocol::TMLedgerData::type)", + "flow": [ + "dynamic_cast(&message)", + "msg->type()", + "if (msg->type() == protocol::liTS_CANDIDATE / liTX_NODE / liAS_NODE)" + ], + "origin": "protocol::TMLedgerData message", + "transformations": [ + "Compared to enum values" + ], + "validated_at": "msg->type() == protocol::liTS_CANDIDATE / liTX_NODE / liAS_NODE" + }, + { + "field": "msg->has_requestcookie()", + "flow": [ + "dynamic_cast(&message) or dynamic_cast(&message)", + "msg->has_requestcookie()", + "if (!msg->has_requestcookie())" + ], + "origin": "protocol::TMLedgerData or protocol::TMGetLedger message", + "transformations": [ + "Boolean check" + ], + "validated_at": "!msg->has_requestcookie()" + }, + { + "field": "msg->itype() (protocol::TMGetLedger::itype)", + "flow": [ + "dynamic_cast(&message)", + "msg->itype()", + "if (msg->itype() == protocol::liTS_CANDIDATE / liTX_NODE / liAS_NODE)" + ], + "origin": "protocol::TMGetLedger message", + "transformations": [ + "Compared to enum values" + ], + "validated_at": "msg->itype() == protocol::liTS_CANDIDATE / liTX_NODE / liAS_NODE" + } + ], + "description": "Implements categorization logic for network traffic messages in the XRPL overlay, mapping protocol message types to traffic categories for monitoring and reporting purposes.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (protocol::MessageType)", + "empty", + "string", + "validation" + ], + "evidence": "type_lookup.find(type) at TrafficCount::categorize", + "issue_pattern": "Missing empty string validation for type (protocol::MessageType)", + "why_false_positive": "type_lookup.find(type) validates type (protocol::MessageType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (protocol::MessageType)", + "empty", + "string", + "validation" + ], + "evidence": "type == protocol::mtHAVE_SET at TrafficCount::categorize", + "issue_pattern": "Missing empty string validation for type (protocol::MessageType)", + "why_false_positive": "type == protocol::mtHAVE_SET validates type (protocol::MessageType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "message (dynamic_cast to protocol::TMLedgerData)", + "empty", + "string", + "validation" + ], + "evidence": "dynamic_cast(&message) at TrafficCount::categorize", + "issue_pattern": "Missing empty string validation for message (dynamic_cast to protocol::TMLedgerData)", + "why_false_positive": "dynamic_cast(&message) validates message (dynamic_cast to protocol::TMLedgerData) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "message (dynamic_cast to protocol::TMLedgerData)", + "type", + "validation", + "check" + ], + "evidence": "dynamic_cast(&message) at TrafficCount::categorize", + "issue_pattern": "Missing type validation for message (dynamic_cast to protocol::TMLedgerData)", + "why_false_positive": "dynamic_cast(&message) validates message (dynamic_cast to protocol::TMLedgerData) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "msg->type() (protocol::TMLedgerData::type)", + "empty", + "string", + "validation" + ], + "evidence": "msg->type() == protocol::liTS_CANDIDATE / liTX_NODE / liAS_NODE at TrafficCount::categorize", + "issue_pattern": "Missing empty string validation for msg->type() (protocol::TMLedgerData::type)", + "why_false_positive": "msg->type() == protocol::liTS_CANDIDATE / liTX_NODE / liAS_NODE validates msg->type() (protocol::TMLedgerData::type) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "msg->has_requestcookie()", + "empty", + "string", + "validation" + ], + "evidence": "!msg->has_requestcookie() at TrafficCount::categorize", + "issue_pattern": "Missing empty string validation for msg->has_requestcookie()", + "why_false_positive": "!msg->has_requestcookie() validates msg->has_requestcookie() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "message (dynamic_cast to protocol::TMGetLedger)", + "empty", + "string", + "validation" + ], + "evidence": "dynamic_cast(&message) at TrafficCount::categorize", + "issue_pattern": "Missing empty string validation for message (dynamic_cast to protocol::TMGetLedger)", + "why_false_positive": "dynamic_cast(&message) validates message (dynamic_cast to protocol::TMGetLedger) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "message (dynamic_cast to protocol::TMGetLedger)", + "type", + "validation", + "check" + ], + "evidence": "dynamic_cast(&message) at TrafficCount::categorize", + "issue_pattern": "Missing type validation for message (dynamic_cast to protocol::TMGetLedger)", + "why_false_positive": "dynamic_cast(&message) validates message (dynamic_cast to protocol::TMGetLedger) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "msg->itype() (protocol::TMGetLedger::itype)", + "empty", + "string", + "validation" + ], + "evidence": "msg->itype() == protocol::liTS_CANDIDATE at TrafficCount::categorize", + "issue_pattern": "Missing empty string validation for msg->itype() (protocol::TMGetLedger::itype)", + "why_false_positive": "msg->itype() == protocol::liTS_CANDIDATE validates msg->itype() (protocol::TMGetLedger::itype) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "msg->has_requestcookie() (TMGetLedger)", + "empty", + "string", + "validation" + ], + "evidence": "msg->has_requestcookie() at TrafficCount::categorize", + "issue_pattern": "Missing empty string validation for msg->has_requestcookie() (TMGetLedger)", + "why_false_positive": "msg->has_requestcookie() validates msg->has_requestcookie() (TMGetLedger) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/TrafficCount.cpp", + "functions": [ + { + "args": [ + "message", + "type", + "inbound" + ], + "lineno": 18, + "name": "TrafficCount::categorize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "This file is a utility for categorizing overlay messages and is likely tested indirectly via overlay network or traffic counting tests. Direct unit tests for TrafficCount::categorize may not exist unless specifically written in test_overlay or test_trafficcount files. Validation paths involving dynamic_cast and field checks (e.g., has_requestcookie, type enums) may not be fully covered if test messages do not exercise all message types and field combinations. Gaps may exist for rare message types or edge cases (e.g., unknown types, malformed messages).", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation via C++ logic and protobuf type checks)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (falls through to further logic)", + "field": "type (protocol::MessageType)", + "location": "TrafficCount::categorize", + "validated_by": "type_lookup.find(type)", + "validates": [ + "Checks if the message type is recognized and mapped to a category" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns category based on inbound flag)", + "field": "type (protocol::MessageType)", + "location": "TrafficCount::categorize", + "validated_by": "type == protocol::mtHAVE_SET", + "validates": [ + "Checks if the message type is mtHAVE_SET and selects category based on inbound" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (if cast fails, continues to next check)", + "field": "message (dynamic_cast to protocol::TMLedgerData)", + "location": "TrafficCount::categorize", + "validated_by": "dynamic_cast(&message)", + "validates": [ + "Checks if message is of type protocol::TMLedgerData" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns category based on type and inbound/has_requestcookie)", + "field": "msg->type() (protocol::TMLedgerData::type)", + "location": "TrafficCount::categorize", + "validated_by": "msg->type() == protocol::liTS_CANDIDATE / liTX_NODE / liAS_NODE", + "validates": [ + "Checks if TMLedgerData type matches specific enum values" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns category based on presence of requestcookie)", + "field": "msg->has_requestcookie()", + "location": "TrafficCount::categorize", + "validated_by": "!msg->has_requestcookie()", + "validates": [ + "Checks if TMLedgerData has a requestcookie field set" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (if cast fails, continues to next check)", + "field": "message (dynamic_cast to protocol::TMGetLedger)", + "location": "TrafficCount::categorize", + "validated_by": "dynamic_cast(&message)", + "validates": [ + "Checks if message is of type protocol::TMGetLedger" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns category based on itype and inbound/has_requestcookie)", + "field": "msg->itype() (protocol::TMGetLedger::itype)", + "location": "TrafficCount::categorize", + "validated_by": "msg->itype() == protocol::liTS_CANDIDATE", + "validates": [ + "Checks if TMGetLedger itype matches specific enum value" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns category based on presence of requestcookie)", + "field": "msg->has_requestcookie() (TMGetLedger)", + "location": "TrafficCount::categorize", + "validated_by": "msg->has_requestcookie()", + "validates": [ + "Checks if TMGetLedger has a requestcookie field set" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/TrafficCount.cpp.ai.md b/src/xrpld/overlay/detail/TrafficCount.cpp.ai.md new file mode 100644 index 0000000000..d16f805c61 --- /dev/null +++ b/src/xrpld/overlay/detail/TrafficCount.cpp.ai.md @@ -0,0 +1,43 @@ +# `TrafficCount.cpp` — P2P Message Categorization for Network Traffic Telemetry + +This file implements a single static method: `TrafficCount::categorize()`. Its purpose is to inspect an incoming or outgoing XRPL overlay protocol message and assign it to one of roughly fifty fine-grained traffic `category` values, enabling the node's traffic monitoring infrastructure to track bandwidth and message counts broken down by message type and data-flow direction. + +## Why This Exists + +The XRPL overlay network multiplex many distinct data-exchange patterns through a small number of protobuf message types. A single `protocol::mtLEDGER_DATA` wire type can carry ledger data in response to a direct request, in response to a forwarded request, or as an unsolicited share — and these scenarios have very different implications for network load analysis. Similarly, `mtGET_OBJECTS` can be a request for a ledger, a transaction, a state node, a CAS object, or a fetch pack. A flat message-type counter would be useless for diagnosing bottlenecks or capacity planning. `categorize()` bridges the gap between the coarse wire protocol and the granular telemetry the operators need. + +## Two-Stage Classification Strategy + +The function uses a two-stage approach that reflects the different structural complexity of the message types it handles. + +For messages whose type alone is sufficient to determine the traffic category, a file-scope `const std::unordered_map` named `type_lookup` handles classification in O(1). This covers sixteen types including `mtPING`, `mtTRANSACTION`, `mtVALIDATION`, `mtPROPOSE_LEDGER`, `mtSQUELCH`, and the newer `mtHAVE_TRANSACTIONS`/`mtTRANSACTIONS` pair. The map is a `const` at file scope, initialized once, and never modified — this is the correct design because the lookup is called on every received and sent message. + +`mtHAVE_SET` is handled as a one-line special case immediately after the map lookup. Its category depends purely on the `inbound` flag: `get_set` if the node received it (it's learning what a peer holds) versus `share_set` if the node sent it (it's announcing what it holds). This direction split is common enough to be worth its own named pair. + +## Protobuf-Level Inspection for Complex Types + +The remaining complexity in the file handles three message types that carry sub-type or role information embedded in protobuf fields, not in the message type discriminator. + +**`TMLedgerData`** (ledger data responses) is the most nuanced. The code `dynamic_cast(&message)` tests the runtime type and, on success, inspects `msg->type()` to determine the ledger data variety (transaction set candidate, transaction node, account state node, or generic), then uses a two-condition expression to pick between a `*_get` and `*_share` category: + +```cpp +(inbound && !msg->has_requestcookie()) ? ld_tsc_get : ld_tsc_share +``` + +The `requestcookie` test is the subtle part. In the XRPL overlay, a `TMLedgerData` carrying a `requestcookie` is a response that was routed through an intermediate peer — even if the local node received it (inbound), the cookie marks it as data that was originally requested by someone else and forwarded. Without the cookie, an inbound `TMLedgerData` represents data the local node actively fetched. This semantic distinction maps to `*_get` vs `*_share` in the category names. + +**`TMGetLedger`** (ledger data requests) reverses the cookie logic slightly: + +```cpp +(inbound || msg->has_requestcookie()) ? gl_tsc_share : gl_tsc_get +``` + +An inbound request means a peer is asking for data the local node holds — that's the node "sharing." An outbound `TMGetLedger` without a cookie is the local node actively requesting. Presence of a cookie again signals a forwarded request, which is categorized under the "share" branch since the node is acting as a relay. + +**`TMGetObjectByHash`** handles object hash requests and responses. Here the `query()` field plays the role that the message direction plays for ledger data: a true `query` flag marks a request (the node wants data), false marks a response (the node is providing data). The expression `msg->query() == inbound` evaluates to true precisely when the message is a request flowing in the expected direction — a query arriving inbound means a peer is requesting, which is a "share" operation for the local node. This bidirectional role-detection applies across six object sub-types: ledger, transaction, transaction node, account state node, CAS object, and fetch pack. An `otTRANSACTIONS` object type has no get/share split and always maps to `get_transactions`. + +## Integration with `Message` and `OverlayImpl` + +`categorize()` is called at two points in the call graph. In `Message.cpp`, the `Message` constructor invokes it immediately with `inbound = false` to classify outbound messages at construction time, storing the result in `category_`. This means categorization cost is paid once per outbound message, not per recipient peer. In `OverlayImpl`, `reportInboundTraffic()` and `reportOutboundTraffic()` call `TrafficCount::addCount()` with the category, which updates atomically-maintained per-category byte and message counters inside the `TrafficCount` object that `OverlayImpl` owns as `m_traffic`. + +If none of the static map lookup, the `mtHAVE_SET` check, or the three `dynamic_cast` branches match, `categorize()` returns `TrafficCount::category::unknown`. This is a safe fallback; callers that receive `unknown` can still increment the `unknown` counter rather than crashing or silently discarding the traffic. The `unknown` category is always registered in the `counts_` map, so `addCount()` will always find it. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/TrafficCount.h.ai.json b/src/xrpld/overlay/detail/TrafficCount.h.ai.json new file mode 100644 index 0000000000..09e51f5251 --- /dev/null +++ b/src/xrpld/overlay/detail/TrafficCount.h.ai.json @@ -0,0 +1,49 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 19, + "name": "TrafficCount" + }, + { + "args": [ + "cat" + ], + "lineno": 25, + "name": "TrafficStats" + } + ], + "description": "Defines the TrafficCount class for tracking and categorizing network traffic (bytes and messages) by message type/category in the XRPL overlay network.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/TrafficCount.h", + "functions": [ + { + "args": [ + "cat", + "inbound", + "bytes" + ], + "lineno": 109, + "name": "addCount" + }, + { + "args": [], + "lineno": 132, + "name": "getCounts" + }, + { + "args": [ + "cat" + ], + "lineno": 139, + "name": "to_string" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/TrafficCount.h.ai.md b/src/xrpld/overlay/detail/TrafficCount.h.ai.md new file mode 100644 index 0000000000..9900bee637 --- /dev/null +++ b/src/xrpld/overlay/detail/TrafficCount.h.ai.md @@ -0,0 +1,51 @@ +# `TrafficCount.h` — Overlay Network Traffic Accounting + +## Role in the System + +`TrafficCount` provides fine-grained, per-category accounting of bytes and message counts flowing through the XRPL overlay network. It sits in the `detail/` subdirectory of the overlay layer and is owned by `OverlayImpl`, which exposes it through `reportInboundTraffic()` and `reportOutboundTraffic()` thin wrappers. The class answers the question: *across all ~60 distinct protocol message categories, how many bytes and messages have entered or left this node?* That data is periodically harvested by `collect_metrics()` and pushed to whatever external monitoring system the node operator configures (e.g., Graphite/StatsD via the `beast::insight` collector). + +## `TrafficStats` — The Per-Category Counter + +`TrafficStats` is a plain data holder: four `std::atomic` fields (`bytesIn`, `bytesOut`, `messagesIn`, `messagesOut`) plus a human-readable `name` string derived via `to_string()` at construction. Using atomics avoids a mutex on every message receive/send, which matters because dozens of peer connections update these counters concurrently from different threads. + +The copy constructor is non-trivial: it calls `.load()` on each atomic to capture a consistent snapshot. This is what allows `getCounts()` to return the map by `const&` while callers can still take a point-in-time snapshot for reporting without holding any lock. The `operator bool()` conversion returns `true` only if at least one message has been counted in either direction — allowing monitoring code to skip categories with no activity. + +## The `category` Enum — Taxonomy of Protocol Traffic + +The `category` enum has roughly 60 named entries organized around several axes: + +**Protocol message type** — `transaction`, `proposal`, `validation`, `validatorlist`, `manifests`, `overlay`, `squelch`, `base` (ping, status change). + +**Sub-category breakdowns** — `transaction_duplicate`, `proposal_untrusted`, `proposal_duplicate`, `validation_untrusted`, `validation_duplicate`. These are incremented *in addition to* the parent category, not instead of it, so operators can see both raw volume and breakdown without needing arithmetic. + +**Ledger data exchange** — `TMLedgerData` and `TMGetLedger` messages carry an internal type field (`liTS_CANDIDATE`, `liTX_NODE`, `liAS_NODE`) that determines whether they carry transaction sets, transaction tree nodes, or account state nodes. Each combination plus direction (`_get` vs `_share`) gets its own category, yielding the `ld_*` and `gl_*` families. + +**`TMGetObjectByHash`** — similarly subdivided by the object type field (`otLEDGER`, `otTRANSACTION`, `otTRANSACTION_NODE`, `otSTATE_NODE`, `otCAS_OBJECT`, `otFETCH_PACK`, `otTRANSACTIONS`). + +**Special sentinel categories**: `total` accumulates raw wire bytes for every recognized message exactly once per send/receive, independently of the detailed categorization. The class comment explicitly notes that `unknown` traffic is *not* rolled into `total`. `unknown` catches any protobuf message type that falls through all the classification logic. + +**Squelch categories**: `squelch_suppressed` records bytes that were *not* actually transmitted because the destination peer had been squelched, and `squelch_ignored` records bytes arriving from peers that are ignoring squelch instructions. Both are reported with `bytes = 0` or the suppressed buffer size respectively, giving operators visibility into the effectiveness of the squelch mechanism. + +## `categorize()` — The Classification Engine + +Implemented in `TrafficCount.cpp`, `categorize()` is a static method that takes a `google::protobuf::Message`, its `protocol::MessageType`, and a direction flag. + +For the majority of message types a single static `unordered_map` lookup suffices — `mtPING`, `mtSTATUS_CHANGE` → `base`; `mtTRANSACTION` → `transaction`; etc. The remaining message types require inspecting message internals, so the method uses `dynamic_cast` to downcast the base protobuf reference to the specific generated message class: + +- For `TMLedgerData` it checks `msg->type()` and `msg->has_requestcookie()`. The presence of a request cookie indicates whether this is a response (share) rather than an unsolicited push (get). +- For `TMGetLedger` the direction semantics are inverted: an inbound message or one bearing a cookie signals a share rather than a get. +- For `TMGetObjectByHash` the internal `query()` flag combined with the direction determines get/share, while `type()` selects the object type subcategory. + +This design cleanly separates classification (pure read of the message) from accounting (mutation of atomic counters), enabling callers to invoke `addCount()` multiple times with different categories for the same wire message — once for the specific category, once for a sub-category (e.g., `transaction_duplicate`), and once for `total`. + +## Usage Pattern in `PeerImp` + +On the inbound path in `PeerImp`, the message category is resolved once via `categorize()`, then `reportInboundTraffic()` is called twice: once with `category::total` (raw wire size) and once with the resolved category. For duplicate transactions, untrusted proposals, and untrusted validations, an additional `reportInboundTraffic()` call with the sub-category fires later in the message handler after the necessary checks. + +On the outbound path, if the squelch logic suppresses a message, `reportOutboundTraffic(squelch_suppressed, ...)` is called instead of the normal send, making the suppression visible in metrics without distorting the per-category send counters. + +## Storage and Initialization + +`counts_` is a `std::unordered_map` whose entire population is specified as an inline member initializer in the class definition. Every known category including `total` and `unknown` is pre-inserted at construction. `addCount()` performs a map lookup and silently returns if the category key is absent — a safety valve that also conveniently allows callers to pass arbitrary `category` values without risk. The `XRPL_ASSERT` guards against values outside the enum range in debug builds. + +`to_string()` uses a static local `unordered_map` to produce monitoring-friendly names like `"ledger_data_Transaction_Set_candidate_get"`. The `unknown` category is intentionally absent from this map; the method returns the literal string `"unknown"` by falling through to a default return, so `TrafficStats` objects in `counts_` for the unknown category still get the correct display name. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Tuning.h.ai.json b/src/xrpld/overlay/detail/Tuning.h.ai.json new file mode 100644 index 0000000000..5ecb8a5609 --- /dev/null +++ b/src/xrpld/overlay/detail/Tuning.h.ai.json @@ -0,0 +1,18 @@ +{ + "args": [], + "classes": [], + "description": "Defines tuning parameters and constants for XRPL overlay/networking behavior, such as ledger convergence limits, send queue thresholds, and buffer sizes.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/Tuning.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "Tuning" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/Tuning.h.ai.md b/src/xrpld/overlay/detail/Tuning.h.ai.md new file mode 100644 index 0000000000..b777e63594 --- /dev/null +++ b/src/xrpld/overlay/detail/Tuning.h.ai.md @@ -0,0 +1,38 @@ +# `Tuning.h` — Overlay Networking Constants + +This file is the single source of truth for all hard-coded threshold values that govern peer-to-peer behavior in the XRPL overlay network. It lives inside the `detail/` subdirectory alongside `PeerImp.h` and `OverlayImpl.h`, and virtually every policy decision in `PeerImp.cpp` and `OverlayImpl.cpp` traces back to a constant defined here. + +## Ledger Tracking: Convergence and Divergence + +`convergedLedgerLimit = 24` and `divergedLedgerLimit = 128` are the thresholds used by `PeerImp::checkTracking()` to classify a peer's ledger state. When the absolute difference between the peer's reported ledger sequence and the network's validated ledger sequence is below 24, the peer is marked `Tracking::converged`. When it exceeds 128, the peer is marked `Tracking::diverged` and a timestamp is recorded — if that diverged state persists for longer than `app_.config().MAX_DIVERGED_TIME`, the peer is disconnected. The gap between the two values (24 vs. 128) is deliberate: it creates hysteresis so a peer that is slightly behind doesn't oscillate between states every few ledgers, while a genuinely stalled peer gets a firm cutoff. + +## Reply Node Limits: Soft and Hard Caps + +`softMaxReplyNodes = 8192` and `hardMaxReplyNodes = 12288` protect against oversized ledger-data replies. In `processLedgerRequest()`, the outgoing loop stops appending nodes once it reaches `softMaxReplyNodes`, leaving headroom before the hard cap. Incoming `TMGetLedgerData` replies are rejected outright if they contain more than `hardMaxReplyNodes` nodes. The soft/hard split separates "stop building" from "reject as malformed" — an honest node always stops well before the hard cap, so any message exceeding it is treated as either a bug or an attack. + +## Send Queue Backpressure: Three Tiers + +The send queue constants implement a layered flow-control policy: + +- **`targetSendQueue = 128`**: The queue size below which a peer is considered well-behaved. Each time a message is enqueued and the queue is below this target, the `large_sendq_` counter resets to zero. +- **`sendqIntervals = 4`**: Number of consecutive timer ticks (each tick is roughly one second) during which the queue must remain at or above `targetSendQueue` before the peer is forcibly disconnected via `fail("Large send queue")`. This tolerates short bursts without punishing peers for momentary slowness. +- **`dropSendQueue = 192`**: A harder cutoff. When the queue reaches this size, new query responses are refused (`send_queue_.size() >= Tuning::dropSendQueue` guards appear in at least two places in `PeerImp.cpp`). This prevents the node from doing expensive ledger lookups for a peer that cannot consume the results. +- **`sendQueueLogFreq = 64`**: Throttles debug logging: a queue size is only logged once per 64 messages enqueued, avoiding log spam during a slow peer event. + +Together these create a graduated response: tolerate, log, refuse queries, then disconnect. + +## Idle Peer Checking + +`checkIdlePeers = 4` is used in `OverlayImpl`'s periodic timer callback as a modulo divisor: `(++timer_count_ % Tuning::checkIdlePeers) == 0`. Rather than running the idle-peer scan on every timer tick, this amortizes its cost across four ticks, reducing contention over the peer list during normal operation. + +## Query Depth Cap + +`maxQueryDepth = 3` limits how deeply a ledger data request may recurse through indirect queries. A node receiving a `TMGetLedger` message with `querydepth` exceeding this value rejects it with a `badData` error. This prevents an attacker from manufacturing a chain of indirect ledger requests that forces the node to fan out expensive I/O work. + +## Socket Read Buffer + +`readBufferBytes = 16384` is declared as `constexpr std::size_t` rather than as part of the anonymous enum, because it is used directly as the argument to `boost::asio::buffer` size calculations and `DynamicBuffer::prepare()`, which expect a `std::size_t`. Placing it in the enum would require repeated casts. The 16 KiB value matches a common TCP socket buffer alignment and is large enough to absorb most individual protocol messages in a single read. + +## Design Note + +All values are grouped inside the nested `Tuning` namespace within `xrpl`, with integer constants expressed as an anonymous `enum`. This is a pre-C++17 idiom that gives compile-time integer constants with internal linkage without requiring `constexpr` declarations or out-of-line definitions. Centralizing all of these thresholds in one header means that performance tuning or protocol hardening only requires changes in one place — no magic numbers are scattered across `PeerImp.cpp` or `OverlayImpl.cpp`. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/TxMetrics.cpp.ai.json b/src/xrpld/overlay/detail/TxMetrics.cpp.ai.json new file mode 100644 index 0000000000..a4adf94a42 --- /dev/null +++ b/src/xrpld/overlay/detail/TxMetrics.cpp.ai.json @@ -0,0 +1,565 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "TxMetrics::addMetrics(type, val)", + "switch(type) { ... add(tx, val) ... }", + "add(lambda)", + "m.addMetrics(val) (SingleMetrics/MultipleMetrics)" + ], + "entry_point": "TxMetrics::addMetrics(protocol::MessageType type, std::uint32_t val)", + "purpose": "Adds a metric value to the appropriate metric bucket based on message type.", + "validation_points": [ + "switch(type): Validates that 'type' is a known protocol::MessageType (validation by switch statement, default returns)", + "val: Validated by C++ type system (std::uint32_t), but no explicit range check" + ] + }, + { + "call_chain": [ + "TxMetrics::addMetrics(selected, suppressed, notenabled)", + "selectedPeers.addMetrics(selected)", + "suppressedPeers.addMetrics(suppressed)", + "notEnabled.addMetrics(notenabled)" + ], + "entry_point": "TxMetrics::addMetrics(std::uint32_t selected, std::uint32_t suppressed, std::uint32_t notenabled)", + "purpose": "Adds metrics for selected, suppressed, and not enabled peers.", + "validation_points": [ + "selected, suppressed, notenabled: Validated by C++ type system (std::uint32_t), but no explicit range check" + ] + }, + { + "call_chain": [ + "TxMetrics::addMetrics(missing)", + "missingTx.addMetrics(missing)" + ], + "entry_point": "TxMetrics::addMetrics(std::uint32_t missing)", + "purpose": "Adds a metric for missing transactions.", + "validation_points": [ + "missing: Validated by C++ type system (std::uint32_t), but no explicit range check" + ] + }, + { + "call_chain": [ + "MultipleMetrics::addMetrics(val1, val2)", + "m1.addMetrics(val1)", + "m2.addMetrics(val2)" + ], + "entry_point": "MultipleMetrics::addMetrics(std::uint32_t val1, std::uint32_t val2)", + "purpose": "Adds two metric values to two separate SingleMetrics.", + "validation_points": [ + "val1, val2: Validated by C++ type system (std::uint32_t), but no explicit range check" + ] + }, + { + "call_chain": [ + "SingleMetrics::addMetrics(val)", + "accum += val", + "if (timeElapsedInSecs >= 1s) { ... update rollingAvg ... }" + ], + "entry_point": "SingleMetrics::addMetrics(std::uint32_t val)", + "purpose": "Accumulates values and computes rolling averages over time.", + "validation_points": [ + "val: Validated by C++ type system (std::uint32_t), but no explicit range check" + ] + }, + { + "call_chain": [ + "TxMetrics::json()", + "std::to_string(...rollingAvg...)", + "ret[jss::field] = ...", + "return ret" + ], + "entry_point": "TxMetrics::json() const", + "purpose": "Serializes all metrics to a JSON object for reporting.", + "validation_points": [ + "Json field names: Validated by jss:: namespace (macro/constant)", + "Json output fields: Validated by std::to_string (type safety, but not semantic validation)" + ] + } + ], + "data_flows": [ + { + "field": "val (metric value)", + "flow": [ + "addMetrics(type, val)", + "switch(type) -> add(lambda)", + "m.addMetrics(val)", + "SingleMetrics::addMetrics(val)", + "accum += val", + "rollingAvg updated" + ], + "origin": "Function parameter to addMetrics (from caller, likely overlay network code)", + "transformations": [ + "Accumulated in SingleMetrics", + "Averaged over time window", + "Stored as rollingAvg" + ], + "validated_at": "switch(type) (type), C++ type system (val)" + }, + { + "field": "selected, suppressed, notenabled", + "flow": [ + "addMetrics(selected, suppressed, notenabled)", + "selectedPeers.addMetrics(selected)", + "suppressedPeers.addMetrics(suppressed)", + "notEnabled.addMetrics(notenabled)", + "SingleMetrics::addMetrics(val)" + ], + "origin": "Function parameters to addMetrics (from caller)", + "transformations": [ + "Accumulated and averaged in SingleMetrics" + ], + "validated_at": "C++ type system" + }, + { + "field": "missing", + "flow": [ + "addMetrics(missing)", + "missingTx.addMetrics(missing)", + "SingleMetrics::addMetrics(val)" + ], + "origin": "Function parameter to addMetrics (from caller)", + "transformations": [ + "Accumulated and averaged in SingleMetrics" + ], + "validated_at": "C++ type system" + }, + { + "field": "rollingAvg", + "flow": [ + "SingleMetrics::addMetrics(val)", + "rollingAvg updated", + "TxMetrics::json() reads rollingAvg", + "std::to_string(rollingAvg)", + "Json::Value ret" + ], + "origin": "Computed in SingleMetrics::addMetrics", + "transformations": [ + "Averaged over time window", + "Converted to string for JSON" + ], + "validated_at": "std::to_string (type safety only)" + }, + { + "field": "Json field names (e.g., jss::txr_tx_cnt)", + "flow": [ + "TxMetrics::json()", + "ret[jss::field] = ...", + "return ret" + ], + "origin": "jss:: namespace (likely macro/constants)", + "transformations": [ + "None (field name mapping)" + ], + "validated_at": "jss:: namespace (macro/constant validation)" + } + ], + "description": "Implements transaction metrics collection and reporting for XRPL overlay, including methods to add and aggregate metrics for different message types and to export metrics as JSON.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "protocol::MessageType (enum)", + "validation", + "missing", + "check" + ], + "evidence": "Field protocol::MessageType (enum) validated by C++ type system, jss:: for JSON field names", + "issue_pattern": "Missing validation for protocol::MessageType (enum)", + "why_false_positive": "C++ type system, jss:: for JSON field names validates protocol::MessageType (enum) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "std::uint32_t fields (val, selected, suppressed, notenabled, missing)", + "validation", + "missing", + "check" + ], + "evidence": "Field std::uint32_t fields (val, selected, suppressed, notenabled, missing) validated by C++ type system, jss:: for JSON field names", + "issue_pattern": "Missing validation for std::uint32_t fields (val, selected, suppressed, notenabled, missing)", + "why_false_positive": "C++ type system, jss:: for JSON field names validates std::uint32_t fields (val, selected, suppressed, notenabled, missing) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "JSON field names via jss::", + "validation", + "missing", + "check" + ], + "evidence": "Field JSON field names via jss:: validated by C++ type system, jss:: for JSON field names", + "issue_pattern": "Missing validation for JSON field names via jss::", + "why_false_positive": "C++ type system, jss:: for JSON field names validates JSON field names via jss:: automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type (protocol::MessageType)", + "empty", + "string", + "validation" + ], + "evidence": "switch statement at TxMetrics::addMetrics(protocol::MessageType type, std::uint32_t val)", + "issue_pattern": "Missing empty string validation for type (protocol::MessageType)", + "why_false_positive": "switch statement validates type (protocol::MessageType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "val, selected, suppressed, notenabled, missing (std::uint32_t)", + "empty", + "string", + "validation" + ], + "evidence": "C++ type system at All addMetrics functions", + "issue_pattern": "Missing empty string validation for val, selected, suppressed, notenabled, missing (std::uint32_t)", + "why_false_positive": "C++ type system validates val, selected, suppressed, notenabled, missing (std::uint32_t) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "val, selected, suppressed, notenabled, missing (std::uint32_t)", + "type", + "validation", + "check" + ], + "evidence": "C++ type system at All addMetrics functions", + "issue_pattern": "Missing type validation for val, selected, suppressed, notenabled, missing (std::uint32_t)", + "why_false_positive": "C++ type system validates val, selected, suppressed, notenabled, missing (std::uint32_t) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "val, selected, suppressed, notenabled, missing (std::uint32_t)", + "empty", + "string", + "validation" + ], + "evidence": "No explicit range check at All addMetrics functions", + "issue_pattern": "Missing empty string validation for val, selected, suppressed, notenabled, missing (std::uint32_t)", + "why_false_positive": "No explicit range check validates val, selected, suppressed, notenabled, missing (std::uint32_t) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "val, selected, suppressed, notenabled, missing (std::uint32_t)", + "range", + "bounds", + "validation" + ], + "evidence": "No explicit range check at All addMetrics functions", + "issue_pattern": "Missing range validation for val, selected, suppressed, notenabled, missing (std::uint32_t)", + "why_false_positive": "No explicit range check validates val, selected, suppressed, notenabled, missing (std::uint32_t) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Json output fields (e.g., tx.m1.rollingAvg)", + "empty", + "string", + "validation" + ], + "evidence": "std::to_string (C++ standard library) at TxMetrics::json()", + "issue_pattern": "Missing empty string validation for Json output fields (e.g., tx.m1.rollingAvg)", + "why_false_positive": "std::to_string (C++ standard library) validates Json output fields (e.g., tx.m1.rollingAvg) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Json output fields (e.g., tx.m1.rollingAvg)", + "format", + "validation", + "invalid" + ], + "evidence": "std::to_string (C++ standard library) at TxMetrics::json()", + "issue_pattern": "Missing format validation for Json output fields (e.g., tx.m1.rollingAvg)", + "why_false_positive": "std::to_string (C++ standard library) validates Json output fields (e.g., tx.m1.rollingAvg) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "Json field names (e.g., jss::txr_tx_cnt)", + "empty", + "string", + "validation" + ], + "evidence": "jss:: namespace (likely macro or constant validation) at TxMetrics::json()", + "issue_pattern": "Missing empty string validation for Json field names (e.g., jss::txr_tx_cnt)", + "why_false_positive": "jss:: namespace (likely macro or constant validation) validates Json field names (e.g., jss::txr_tx_cnt) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Json field names (e.g., jss::txr_tx_cnt)", + "format", + "validation", + "invalid" + ], + "evidence": "jss:: namespace (likely macro or constant validation) at TxMetrics::json()", + "issue_pattern": "Missing format validation for Json field names (e.g., jss::txr_tx_cnt)", + "why_false_positive": "jss:: namespace (likely macro or constant validation) validates Json field names (e.g., jss::txr_tx_cnt) format" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/TxMetrics.cpp", + "functions": [ + { + "args": [ + "protocol::MessageType type", + "std::uint32_t val" + ], + "lineno": 9, + "name": "TxMetrics::addMetrics" + }, + { + "args": [ + "std::uint32_t selected", + "std::uint32_t suppressed", + "std::uint32_t notenabled" + ], + "lineno": 38, + "name": "TxMetrics::addMetrics" + }, + { + "args": [ + "std::uint32_t missing" + ], + "lineno": 46, + "name": "TxMetrics::addMetrics" + }, + { + "args": [ + "std::uint32_t val2" + ], + "lineno": 51, + "name": "MultipleMetrics::addMetrics" + }, + { + "args": [ + "std::uint32_t val1", + "std::uint32_t val2" + ], + "lineno": 55, + "name": "MultipleMetrics::addMetrics" + }, + { + "args": [ + "std::uint32_t val" + ], + "lineno": 60, + "name": "SingleMetrics::addMetrics" + }, + { + "args": [], + "lineno": 80, + "name": "TxMetrics::json" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "metrics" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, code in 'xrpld/overlay/detail' is tested via integration or unit tests in the 'test' or 'unittest' directories of the repository. The main validation paths (switch on MessageType, type safety via std::uint32_t, and JSON serialization) are not explicitly tested here. Potential gaps: No explicit range or semantic validation (e.g., negative values, overflow), and no tests for malformed or unexpected input types. Test coverage may rely on higher-level integration tests that exercise overlay metrics reporting, but direct unit tests for edge cases (e.g., invalid MessageType, large values) may be missing.", + "validation_architecture": { + "auto_validated_fields": [ + "protocol::MessageType (enum)", + "std::uint32_t fields (val, selected, suppressed, notenabled, missing)", + "JSON field names via jss::" + ], + "framework": "C++ type system, jss:: for JSON field names", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (default: return)", + "field": "type (protocol::MessageType)", + "location": "TxMetrics::addMetrics(protocol::MessageType type, std::uint32_t val)", + "validated_by": "switch statement", + "validates": [ + "Ensures only known protocol::MessageType values are processed", + "Unknown types are ignored" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "compile-time type error if wrong type", + "field": "val, selected, suppressed, notenabled, missing (std::uint32_t)", + "location": "All addMetrics functions", + "validated_by": "C++ type system", + "validates": [ + "Ensures only unsigned 32-bit integers are accepted" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none", + "field": "val, selected, suppressed, notenabled, missing (std::uint32_t)", + "location": "All addMetrics functions", + "validated_by": "No explicit range check", + "validates": [ + "No negative values possible due to unsigned type", + "No upper bound check" + ], + "validation_type": "range" + }, + { + "confidence": 0.9, + "error_thrown": "std::invalid_argument or std::out_of_range (if conversion fails, but unlikely for unsigned integer)", + "field": "Json output fields (e.g., tx.m1.rollingAvg)", + "location": "TxMetrics::json()", + "validated_by": "std::to_string (C++ standard library)", + "validates": [ + "Ensures numeric values are converted to string for JSON" + ], + "validation_type": "format" + }, + { + "confidence": 0.95, + "error_thrown": "compile-time error if undefined", + "field": "Json field names (e.g., jss::txr_tx_cnt)", + "location": "TxMetrics::json()", + "validated_by": "jss:: namespace (likely macro or constant validation)", + "validates": [ + "Ensures only valid JSON field names are used" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/TxMetrics.cpp.ai.md b/src/xrpld/overlay/detail/TxMetrics.cpp.ai.md new file mode 100644 index 0000000000..93ac555e3e --- /dev/null +++ b/src/xrpld/overlay/detail/TxMetrics.cpp.ai.md @@ -0,0 +1,35 @@ +# `TxMetrics.cpp` — Transaction Reduce-Relay Metrics + +## Purpose + +This file implements the metrics collection subsystem for XRPL's **transaction reduce-relay** feature — a peer-selection optimization that suppresses redundant transaction forwarding across the overlay network. Rather than flooding every connected peer, the relay logic selects a reduced set of targets, and these metrics track the effectiveness and volume of that decision-making in real time. + +The file defines three cooperating types in the `xrpl::metrics` namespace: `SingleMetrics` (a one-dimensional rolling average accumulator), `MultipleMetrics` (pairs two `SingleMetrics` instances to track both count and byte size for a single message class), and `TxMetrics` (the top-level aggregator owning one `MultipleMetrics` per monitored protocol message type plus several standalone `SingleMetrics` for peer-selection diagnostics). + +## `SingleMetrics` — Rolling Average Engine + +`SingleMetrics::addMetrics()` is the computational core of the entire file. Its design reflects a deliberate tradeoff between accuracy and overhead: rather than computing a continuous moving average on every call, it amortizes the update into 1-second intervals. + +On each call the incoming `val` is added to `accum` and the sample counter `N` is incremented. When a `std::chrono::steady_clock` check reveals that at least one second has elapsed, the accumulator is condensed into a single average for that interval and pushed into a `boost::circular_buffer` with capacity 30. The overall `rollingAvg` is then recomputed as the arithmetic mean of all values in that buffer before `accum`, `N`, and `intervalStart` are reset. + +The `perTimeUnit` flag selects between two averaging modes. When `true` (the default), the divisor is the elapsed integer seconds, giving a **rate per second** — appropriate for byte and message counts that accumulate continuously. When `false`, the divisor is `N` (the sample count in the interval), giving a **per-observation average** — appropriate for quantities like "how many peers were selected for this transaction", where each call represents one independent decision rather than contributing to a cumulative rate. + +The 30-slot circular buffer means the `rollingAvg` reflects the trailing 30-second history. Slots default to `0ull`, so a window with no activity naturally dilutes the average toward zero as non-zero entries age out — there is no explicit decay or reset; the circular buffer handles it structurally. + +## `MultipleMetrics` — Count/Size Pairs + +`MultipleMetrics` exists purely as a convenience wrapper around two `SingleMetrics` (`m1` for count, `m2` for bytes). The two-argument `addMetrics(val1, val2)` delegates straight through; the one-argument overload hardcodes `val1 = 1`, which is the expected pattern for protocol messages: each received message increments the count by one while contributing a variable byte size. Callers like `PeerImp` record message arrivals this way without needing to assemble both values at the call site. + +## `TxMetrics` — Aggregator and JSON Export + +`TxMetrics` aggregates five `MultipleMetrics` members covering the protocol message types most relevant to relay decisions: `mtTRANSACTION`, `mtHAVE_TRANSACTIONS`, `mtGET_LEDGER`, `mtLEDGER_DATA`, and `mtTRANSACTIONS`. Unknown message types pass through `addMetrics(type, val)`'s `switch` silently — the `default: return` avoids any error path, which is correct since new message types should not crash metrics collection. + +The three `SingleMetrics` instances for peer selection — `selectedPeers`, `suppressedPeers`, and `notEnabled` — are all constructed with `perTimeUnit=false`, giving per-transaction-event averages rather than per-second rates. This makes diagnostic sense: the operator wants to know "on average, how many peers does each tx touch or skip?" not "how many peers per second?". `missingTx`, by contrast, uses the default `perTimeUnit=true` to report missing-transaction requests as a frequency. + +`json()` serializes all thirteen `rollingAvg` values through `jss::` field-name constants (e.g., `jss::txr_tx_cnt`, `jss::txr_selected_cnt`). All values are stringified via `std::to_string` rather than stored as JSON integers — a minor quirk that consumers must account for. + +## Thread Safety + +`TxMetrics` holds a `mutable std::mutex` and every public method (including the `const json()`) acquires a `std::lock_guard` before touching any metric state. The subordinate `SingleMetrics` and `MultipleMetrics` types have no locks of their own; they rely entirely on `TxMetrics`' mutex. + +In practice, `OverlayImpl` owns a `TxMetrics txMetrics_` member and wraps all mutations in a variadic `addTxMetrics()` template that posts to an Asio strand when called off-strand. This means most write-path calls arrive serialized on the strand without contention, and the mutex primarily guards the read path — `json()` can be invoked by any RPC-handling thread independently of the strand. The two-layer design (strand for writers, mutex for reader/writer arbitration) avoids requiring RPC threads to post their query through the strand, at the cost of one additional lock acquisition per report. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/TxMetrics.h.ai.json b/src/xrpld/overlay/detail/TxMetrics.h.ai.json new file mode 100644 index 0000000000..95c02cf485 --- /dev/null +++ b/src/xrpld/overlay/detail/TxMetrics.h.ai.json @@ -0,0 +1,128 @@ +{ + "args": [ + { + "lineno": 18, + "name": "ptu" + }, + { + "lineno": 43, + "name": "ptu1" + }, + { + "lineno": 43, + "name": "ptu2" + }, + { + "lineno": 68, + "name": "type" + }, + { + "lineno": 68, + "name": "val" + }, + { + "lineno": 74, + "name": "selected" + }, + { + "lineno": 74, + "name": "suppressed" + }, + { + "lineno": 74, + "name": "notEnabled" + }, + { + "lineno": 79, + "name": "missing" + } + ], + "classes": [ + { + "args": [ + "ptu" + ], + "lineno": 15, + "name": "SingleMetrics" + }, + { + "args": [ + "ptu1", + "ptu2" + ], + "lineno": 41, + "name": "MultipleMetrics" + }, + { + "args": [], + "lineno": 56, + "name": "TxMetrics" + } + ], + "description": "Defines metrics structures and methods for tracking and reporting rolling averages and statistics related to transaction relay and protocol message metrics in the XRPL overlay network.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/TxMetrics.h", + "functions": [ + { + "args": [ + "val" + ], + "lineno": 32, + "name": "SingleMetrics::addMetrics" + }, + { + "args": [ + "val2" + ], + "lineno": 48, + "name": "MultipleMetrics::addMetrics" + }, + { + "args": [ + "val1", + "val2" + ], + "lineno": 52, + "name": "MultipleMetrics::addMetrics" + }, + { + "args": [ + "type", + "val" + ], + "lineno": 67, + "name": "TxMetrics::addMetrics" + }, + { + "args": [ + "selected", + "suppressed", + "notEnabled" + ], + "lineno": 73, + "name": "TxMetrics::addMetrics" + }, + { + "args": [ + "missing" + ], + "lineno": 78, + "name": "TxMetrics::addMetrics" + }, + { + "args": [], + "lineno": 83, + "name": "TxMetrics::json" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "metrics" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/TxMetrics.h.ai.md b/src/xrpld/overlay/detail/TxMetrics.h.ai.md new file mode 100644 index 0000000000..c55a075a43 --- /dev/null +++ b/src/xrpld/overlay/detail/TxMetrics.h.ai.md @@ -0,0 +1,57 @@ +# `TxMetrics.h` — Rolling-Average Metrics for Transaction Reduce-Relay + +This header defines the `xrpl::metrics` namespace structures that underpin observable telemetry for the **transaction reduce-relay** feature. The reduce-relay mechanism deliberately withholds redundant transaction relays to peers that already have a transaction, cutting network traffic at the cost of needing careful instrumentation. These structures give operators visibility into how aggressively the feature suppresses relays and how the resulting protocol message traffic compares to an unconstrained baseline. + +## Three-Layer Abstraction + +The design nests cleanly into three levels of increasing specificity. + +### `SingleMetrics` — The Primitive Rolling Average + +`SingleMetrics` tracks a single numeric stream. On every call to `addMetrics(val)` it accumulates `val` into `accum` and increments the sample count `N`. Once at least one second has elapsed since `intervalStart`, it computes a per-interval value and resets: + +- If `perTimeUnit` is `true`, the interval value is `accum / elapsed_seconds` — a **throughput rate** (bytes per second, messages per second). +- If `perTimeUnit` is `false`, the interval value is `accum / N` — a **sample mean** (average peers per transaction decision). + +The interval value is pushed into a `boost::circular_buffer` of capacity 30. The published `rollingAvg` is the mean of whatever is currently in that buffer. Because the buffer is pre-filled with zeros, the first 30 one-second windows are dampened downward — a deliberate smoothing artifact rather than a bug. Over steady-state operation the buffer represents roughly 30 seconds of history. + +The choice of `perTimeUnit` as a constructor flag (defaulting `true`) means the same struct handles both classes of measurement without subtyping or templates. + +### `MultipleMetrics` — Count/Size Pairing + +`MultipleMetrics` holds two `SingleMetrics` instances (`m1` and `m2`) and provides two `addMetrics` overloads. When called with a single argument `addMetrics(val2)`, it synthesizes a count of 1 for `m1` automatically — this is the typical protocol-message path, where the caller knows only the byte size and the count-per-second is implicit. When called with two arguments `addMetrics(val1, val2)`, both are forwarded explicitly. In practice `m1` always tracks message count and `m2` tracks byte size. + +### `TxMetrics` — The Top-Level Aggregate + +`TxMetrics` holds a `MultipleMetrics` instance for each tracked protocol message type: + +| Field | Protocol Message | +|---|---| +| `tx` | `mtTRANSACTION` | +| `haveTx` | `mtHAVE_TRANSACTIONS` | +| `getLedger` | `mtGET_LEDGER` | +| `ledgerData` | `mtLEDGER_DATA` | +| `transactions` | `mtTRANSACTIONS` | + +It also carries three `SingleMetrics` with `perTimeUnit=false` that capture the relay-decision outcome per transaction event: `selectedPeers` (peers chosen to receive the relay), `suppressedPeers` (peers that were skipped), and `notEnabled` (peers with reduce-relay disabled). These are sample averages, not rates, because the interesting signal is the ratio of selected to suppressed per transaction, not the absolute volume over time. + +A fourth `SingleMetrics` (`missingTx`, `perTimeUnit=true`) tracks how frequently `TMTransactions` bundles arrive carrying transaction data that the local node didn't have — a proxy for how much network load the reduce-relay is generating when peers must reactively request missing transactions. + +## Concurrency Model + +`TxMetrics` owns a `mutable std::mutex mutex`. All three `addMetrics` overloads on `TxMetrics` acquire this lock before touching any `SingleMetrics` or `MultipleMetrics` state. `json()` also holds the lock while reading every `rollingAvg` field, since those reads are not atomic. + +Importantly, `SingleMetrics` and `MultipleMetrics` themselves are **not** independently thread-safe — the lock lives one level up at `TxMetrics`. In `OverlayImpl`, the `txMetrics_` member is additionally guarded by the overlay's Boost.Asio strand: `addTxMetrics()` posts to the strand if the caller is not already on it. This creates a two-level protection scheme — the strand serializes writes from peer I/O threads, while the mutex protects against concurrent reads by RPC threads that call `txMetrics()` on a different execution context. + +## Integration and Exposure + +`OverlayImpl` holds a single `metrics::TxMetrics txMetrics_` and exposes it two ways: + +- `addTxMetrics(...)` — variadic template that forwards to `txMetrics_.addMetrics(...)`, called from `PeerImp::onMessageBegin()` (on every tracked inbound message) and from the relay-decision code in `OverlayImpl` whenever a transaction is dispatched to the peer set. +- `txMetrics()` — implements the pure virtual `Overlay::txMetrics() const`, returning `txMetrics_.json()`. + +Metric collection is gated on the `TX_REDUCE_RELAY_ENABLE` or `TX_REDUCE_RELAY_METRICS` config flags, so it can be enabled for diagnostic purposes on nodes that are not yet enforcing the relay reduction. + +## JSON Output + +`TxMetrics::json()` produces a flat `Json::Value` object with 13 fields, all registered in `jss.h` under `txr_*` names. A quirk worth noting: every value is serialized via `std::to_string` and stored as a **JSON string**, not a number. Consumers (such as the `get_counts` RPC command) must parse these strings if they need numeric comparison. \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ZeroCopyStream.h.ai.json b/src/xrpld/overlay/detail/ZeroCopyStream.h.ai.json new file mode 100644 index 0000000000..d3f11503dc --- /dev/null +++ b/src/xrpld/overlay/detail/ZeroCopyStream.h.ai.json @@ -0,0 +1,164 @@ +{ + "args": [ + { + "lineno": 16, + "name": "Buffers" + }, + { + "lineno": 88, + "name": "Streambuf" + } + ], + "classes": [ + { + "args": [ + "Buffers const& buffers" + ], + "lineno": 17, + "name": "ZeroCopyInputStream" + }, + { + "args": [ + "Streambuf& streambuf", + "std::size_t blockSize" + ], + "lineno": 89, + "name": "ZeroCopyOutputStream" + } + ], + "description": "Implements zero-copy input and output streams for use with Protocol Buffers, wrapping Boost.Asio buffer sequences and streambufs to efficiently read from and write to network buffers.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/detail/ZeroCopyStream.h", + "functions": [ + { + "args": [ + "Buffers const& buffers" + ], + "lineno": 25, + "name": "ZeroCopyInputStream" + }, + { + "args": [ + "void const** data", + "int* size" + ], + "lineno": 28, + "name": "Next" + }, + { + "args": [ + "int count" + ], + "lineno": 31, + "name": "BackUp" + }, + { + "args": [ + "int count" + ], + "lineno": 34, + "name": "Skip" + }, + { + "args": [], + "lineno": 37, + "name": "ByteCount" + }, + { + "args": [ + "Buffers const& buffers" + ], + "lineno": 49, + "name": "ZeroCopyInputStream::ZeroCopyInputStream" + }, + { + "args": [ + "void const** data", + "int* size" + ], + "lineno": 55, + "name": "ZeroCopyInputStream::Next" + }, + { + "args": [ + "int count" + ], + "lineno": 65, + "name": "ZeroCopyInputStream::BackUp" + }, + { + "args": [ + "int count" + ], + "lineno": 70, + "name": "ZeroCopyInputStream::Skip" + }, + { + "args": [ + "Streambuf& streambuf", + "std::size_t blockSize" + ], + "lineno": 97, + "name": "ZeroCopyOutputStream" + }, + { + "args": [], + "lineno": 99, + "name": "~ZeroCopyOutputStream" + }, + { + "args": [ + "void** data", + "int* size" + ], + "lineno": 102, + "name": "Next" + }, + { + "args": [ + "int count" + ], + "lineno": 105, + "name": "BackUp" + }, + { + "args": [], + "lineno": 108, + "name": "ByteCount" + }, + { + "args": [ + "Streambuf& streambuf", + "std::size_t blockSize" + ], + "lineno": 120, + "name": "ZeroCopyOutputStream::ZeroCopyOutputStream" + }, + { + "args": [], + "lineno": 127, + "name": "ZeroCopyOutputStream::~ZeroCopyOutputStream" + }, + { + "args": [ + "void** data", + "int* size" + ], + "lineno": 132, + "name": "ZeroCopyOutputStream::Next" + }, + { + "args": [ + "int count" + ], + "lineno": 146, + "name": "ZeroCopyOutputStream::BackUp" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/detail/ZeroCopyStream.h.ai.md b/src/xrpld/overlay/detail/ZeroCopyStream.h.ai.md new file mode 100644 index 0000000000..54c7500619 --- /dev/null +++ b/src/xrpld/overlay/detail/ZeroCopyStream.h.ai.md @@ -0,0 +1,27 @@ +# `ZeroCopyStream.h` + +This file provides the glue between Protocol Buffers' serialization engine and Boost.Asio's buffer model, enabling zero-copy encoding and decoding of protobuf messages directly into and out of network I/O buffers. It exists because protobuf's `ZeroCopyInputStream` and `ZeroCopyOutputStream` interfaces are the canonical way to feed protobuf parsers and serializers from arbitrary memory sources — but the XRPL overlay network works with Asio buffer sequences and streambufs. Without this adapter layer, every inbound message would require an intermediate copy into a flat byte array before protobuf could parse it, which is unacceptable on a high-throughput network path. + +## `ZeroCopyInputStream` + +This template wraps any type satisfying `ConstBufferSequence` — the same scatter-gather concept used throughout Asio — and exposes it as a protobuf `ZeroCopyInputStream`. The design is intentionally iterator-based: the class maintains three pieces of state — an end sentinel `last_`, a "current buffer" iterator `first_`, and a `const_buffer` named `pos_` representing where within the current buffer the next read will begin. + +The reason `pos_` is tracked separately from `*first_` is to support `BackUp()` and `Skip()`. Protobuf's zero-copy contract allows callers to return unconsumed bytes via `BackUp()`, and `Skip()` must advance through buffers without surfacing data. Both operations require sub-buffer granularity. `BackUp()` decrements `first_`, then recomputes `pos_` as an offset within that buffer — effectively rewinding into the middle of a prior chunk without touching memory. `Skip()` walks forward across buffer boundaries, advancing `pos_` within the current buffer when the skip target falls short of its end, or consuming entire buffers and moving `first_` until the skip count is exhausted. + +One subtle defensive detail: in the constructor, if `buffers` is empty (`first_ == last_`), `pos_` is initialized to a null buffer of size zero. This ensures `Next()` immediately returns `false` without dereferencing the end iterator — avoiding a special case elsewhere in the call paths. `ByteCount()` returns a running total of bytes surfaced to the parser, maintained by incrementing `count_` in `Next()` and adjusting it symmetrically in `BackUp()` and `Skip()`. + +In practice, `ZeroCopyInputStream` is used directly by `detail::parseMessageContent()` in `ProtocolMessage.h`. For uncompressed messages, it calls `m->ParseFromZeroCopyStream(&stream)`, letting protobuf walk the Asio receive buffers without any copy. For LZ4-compressed messages, the stream is passed to `xrpl::compression::decompress()`, which reads the compressed bytes in-place before protobuf sees the result. The `Skip(header.header_size)` call at the start of `parseMessageContent()` is the mechanism that advances past the framing header before handing the stream to the protobuf decoder. + +## `ZeroCopyOutputStream` + +The output side wraps a `Streambuf` — a type matching the `boost::asio::streambuf` interface — and implements `ZeroCopyOutputStream`. Writing to a streambuf follows a prepare/commit protocol: `prepare(n)` reserves `n` bytes of write space and returns a mutable buffer sequence; once data is written, `commit(n)` advances the streambuf's write pointer. The class manages this lifecycle across protobuf's repeated `Next()` calls. + +The `commit_` field is the key: it tracks how many bytes were promised in the most recent `Next()` call but not yet committed to the streambuf. This is a deferred-commit pattern. When `Next()` is called again, it first commits the previous block before calling `prepare(blockSize_)` for a new one. This batches commits rather than issuing one per protobuf write, reducing overhead for messages that span multiple blocks. + +The destructor is critical to correctness. Protobuf does not guarantee a terminal `BackUp()` or second `Next()` call after it finishes serializing — so the final outstanding `commit_` would be lost if the destructor didn't flush it. `~ZeroCopyOutputStream()` calls `streambuf_.commit(commit_)` if `commit_ != 0`, making this an RAII flush of the streambuf tail. Without this, bytes written in the last `Next()` call would be silently dropped. + +`BackUp()` handles over-allocation: protobuf often requests more buffer than it actually uses, then calls `BackUp(count)` to return the excess. The implementation commits only `commit_ - count` bytes and zeroes `commit_` to prevent a double-commit in the destructor or the next `Next()` call. The `XRPL_ASSERT(count <= commit_)` guards the internal invariant that you cannot back up more bytes than were vended in the last `Next()`. + +## Position in the Overlay Architecture + +Both adapters are header-only templates, meaning no virtual dispatch is introduced beyond the one already mandated by protobuf's abstract base classes. The template parameter approach ensures the compiler can inline the iterator operations and buffer size queries, keeping the per-byte overhead minimal on the message parsing hot path. The `blockSize` constructor argument on `ZeroCopyOutputStream` decouples allocation granularity from the serialization logic, allowing it to be tuned via constants defined elsewhere (such as `Tuning.h`) without touching this file. Together, these two classes are the lowest-level I/O adapters in the overlay stack, sitting directly between raw Asio buffers and the protobuf layer that all peer-to-peer message types flow through. \ No newline at end of file diff --git a/src/xrpld/overlay/make_Overlay.h.ai.json b/src/xrpld/overlay/make_Overlay.h.ai.json new file mode 100644 index 0000000000..31d5f1ce30 --- /dev/null +++ b/src/xrpld/overlay/make_Overlay.h.ai.json @@ -0,0 +1,36 @@ +{ + "args": [], + "classes": [], + "description": "Provides setup and factory functions for creating and configuring the Overlay network component in the XRPL server.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/make_Overlay.h", + "functions": [ + { + "args": [ + "config" + ], + "lineno": 9, + "name": "setup_Overlay" + }, + { + "args": [ + "app", + "setup", + "serverHandler", + "resourceManager", + "resolver", + "io_context", + "config", + "collector" + ], + "lineno": 13, + "name": "make_Overlay" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/make_Overlay.h.ai.md b/src/xrpld/overlay/make_Overlay.h.ai.md new file mode 100644 index 0000000000..720bf26c52 --- /dev/null +++ b/src/xrpld/overlay/make_Overlay.h.ai.md @@ -0,0 +1,50 @@ +# `make_Overlay.h` — Factory Interface for the Overlay Subsystem + +This header defines the two-phase construction interface for the XRPL overlay network: a configuration-parsing function and a factory that produces the concrete `Overlay` implementation. It exists as an intentional dependency seam, keeping construction machinery entirely separate from the `Overlay.h` abstract interface that the rest of the codebase depends on. + +## Why a Separate Factory Header + +`Overlay.h` is included broadly across the XRPL server — consensus, ledger acquisition, transaction relaying, and RPC all reference the `Overlay` interface. Embedding `make_Overlay` and `setup_Overlay` in that header would force every consumer to also compile against `ServerHandler`, `Resolver`, Boost.Asio, and other heavyweight construction-time dependencies. By isolating those two functions here, the vast majority of the codebase pays only the cost of the abstract interface, while application startup code — the sole caller of the factory — bears the full include burden. This is a classic dependency-inversion boundary and directly mirrors how other major subsystems in rippled are structured (compare `make_NetworkOPs.h`, `make_Manager.h` in peerfinder, etc.). + +## Two-Phase Construction + +**`setup_Overlay(BasicConfig const&)`** reads four configuration sections and builds an `Overlay::Setup` value object with no live resources attached. From `[overlay]` it extracts an SSL context (via `make_SSLContext`), an optional `ip_limit` cap on connections per IP address, and an optional `public_ip` address that must resolve to a non-private IP or the function throws. From `[crawl]` it assembles a bitmask of `CrawlOptions` flags governing what topology information the node will expose to crawlers (overlay peers, server info, counts, UNL). From `[vl]` it toggles whether the validator list subsystem is active. Finally it parses `[network_id]`, accepting the string aliases `"main"` (0), `"testnet"` (1), and `"devnet"` (2) in addition to raw integers, storing the result in an `std::optional`. The function throws immediately on any invalid configuration, so errors surface at startup before any I/O infrastructure is allocated — exactly the right time to fail loudly. + +**`make_Overlay(...)`** accepts the parsed `Setup` alongside all live runtime dependencies and returns `std::unique_ptr`. Internally it is a one-liner that constructs `OverlayImpl` — the concrete class defined entirely within the `detail/` subdirectory — and returns it behind the abstract pointer. No caller ever sees the concrete type; the implementation is fully opaque behind this boundary. + +The split matters because it separates concerns cleanly: `setup_Overlay` validates static configuration and can be unit-tested against `BasicConfig` alone, while `make_Overlay` wires up live system resources and cannot meaningfully run without them. There is no intermediate "partially initialized" overlay state to reason about. + +## Dependency Surface + +The parameters to `make_Overlay` reveal the overlay's runtime requirements: + +- **`Application&`** — the central application context; `OverlayImpl` uses it to reach the ledger master, job queue, hash router, and most other subsystems. +- **`ServerHandler&`** — incoming peer connections arrive as HTTP upgrade requests through the same HTTP server infrastructure that handles RPC and admin endpoints. The overlay must integrate with that HTTP layer rather than owning a separate listening socket. This is why `ServerHandler` is passed at construction rather than discovered later. +- **`Resource::Manager&`** — rate-limiting and resource accounting for peer connections, shared with the RPC server and consistent across the whole node's back-pressure strategy. +- **`Resolver&`** — async DNS resolution for bootstrapping peer addresses from hostnames. Keeping this as an injected dependency makes DNS behavior testable and replaceable. +- **`boost::asio::io_context&`** — the single shared I/O context for all async operations. `OverlayImpl` creates its own strand from this context for thread-safe internal dispatch. +- **`BasicConfig const&`** — a second pass at raw config, allowing `OverlayImpl` to read subsections (e.g., peerfinder settings) beyond what `setup_Overlay` extracts into `Setup`. +- **`beast::insight::Collector::ptr const&`** — the metrics collector for overlay-specific counters and gauges. Passing it at construction ensures all performance instrumentation is registered once during initialization and remains stable for the server's lifetime. + +## Usage in Application Bootstrap + +`Application.cpp` calls both functions together during server initialization: + +```cpp +overlay_ = make_Overlay( + *this, + setup_Overlay(*config_), + *serverHandler_, + *m_resourceManager, + *m_resolver, + get_io_context(), + *config_, + m_collectorManager->collector()); +add(*overlay_); // register with PropertyStream +``` + +A comment in that code acknowledges a known technical debt: the overlay is instantiated unconditionally, even in standalone mode where it should be a no-op, because some downstream code incorrectly calls `app.overlay()` regardless of networking state. This does not affect the factory interface itself, but explains why the `if (!config_.standalone())` guard mentioned in the comment was never added. + +## Relationship to Sibling Files + +This header is the sole construction entry point for the system implemented across `Overlay.h`, `Peer.h`, `Message.h`, `Slot.h`, `Squelch.h`, `ReduceRelayCommon.h`, and the `detail/` directory. The `Overlay::Setup` struct populated by `setup_Overlay` lives in `Overlay.h` and carries exactly the configuration fields that `OverlayImpl` needs at construction time. Everything beyond this narrow interface is an implementation detail invisible to the rest of the server. \ No newline at end of file diff --git a/src/xrpld/overlay/predicates.h.ai.json b/src/xrpld/overlay/predicates.h.ai.json new file mode 100644 index 0000000000..55abe957cd --- /dev/null +++ b/src/xrpld/overlay/predicates.h.ai.json @@ -0,0 +1,149 @@ +{ + "args": [ + { + "lineno": 15, + "name": "m" + }, + { + "lineno": 20, + "name": "peer" + }, + { + "lineno": 34, + "name": "m" + }, + { + "lineno": 34, + "name": "p" + }, + { + "lineno": 39, + "name": "peer" + }, + { + "lineno": 54, + "name": "m" + }, + { + "lineno": 54, + "name": "f" + }, + { + "lineno": 66, + "name": "m" + }, + { + "lineno": 66, + "name": "p" + }, + { + "lineno": 71, + "name": "peer" + }, + { + "lineno": 86, + "name": "m" + }, + { + "lineno": 86, + "name": "f" + }, + { + "lineno": 95, + "name": "match" + }, + { + "lineno": 99, + "name": "peer" + }, + { + "lineno": 111, + "name": "skip" + }, + { + "lineno": 115, + "name": "peer" + }, + { + "lineno": 129, + "name": "peers" + }, + { + "lineno": 133, + "name": "peer" + } + ], + "classes": [ + { + "args": [ + "m" + ], + "lineno": 10, + "name": "send_always" + }, + { + "args": [ + "m", + "p" + ], + "lineno": 29, + "name": "send_if_pred" + }, + { + "args": [ + "m", + "p" + ], + "lineno": 61, + "name": "send_if_not_pred" + }, + { + "args": [ + "match" + ], + "lineno": 93, + "name": "match_peer" + }, + { + "args": [ + "skip" + ], + "lineno": 109, + "name": "peer_in_cluster" + }, + { + "args": [ + "peers" + ], + "lineno": 127, + "name": "peer_in_set" + } + ], + "description": "This file provides utility structs and helper functions for sending messages to peers in the XRPL overlay network, with various selection predicates for targeting specific peers or groups of peers.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/overlay/predicates.h", + "functions": [ + { + "args": [ + "m", + "f" + ], + "lineno": 49, + "name": "send_if" + }, + { + "args": [ + "m", + "f" + ], + "lineno": 81, + "name": "send_if_not" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/overlay/predicates.h.ai.md b/src/xrpld/overlay/predicates.h.ai.md new file mode 100644 index 0000000000..d910e44323 --- /dev/null +++ b/src/xrpld/overlay/predicates.h.ai.md @@ -0,0 +1,35 @@ +# `predicates.h` — Peer Selection and Message Dispatch Combinators + +This header solves a recurring problem in overlay network management: how do you send a message to a filtered subset of connected peers without writing bespoke iteration logic at every call site? Rather than scattering `if` statements throughout broadcast code, this file provides a small combinator library of callable structs that compose selection logic with message dispatch. All types are designed to be passed as functors into `Overlay::foreach()`, which iterates over all active peers and invokes the functor with each peer's `shared_ptr`. + +## The Dispatch/Predicate Split + +The core design separates two orthogonal concerns: *which peers to select* and *whether to send*. This produces two conceptual layers: + +**Dispatch functors** — `send_always`, `send_if_pred`, and `send_if_not_pred` — take a `Message` reference and implement `operator()(shared_ptr)`. These are what get passed to the iteration mechanism. + +**Selection predicates** — `match_peer`, `peer_in_cluster`, and `peer_in_set` — take a `shared_ptr` and return `bool`. They are composable, reusable, and independent of message dispatch. + +The free functions `send_if()` and `send_if_not()` wire these two layers together by deducing the predicate type from its argument, sparing callers from manually spelling out `send_if_pred`. This template helper pattern was especially useful before C++17 class template argument deduction became widespread and remains the idiomatic call pattern here. + +## Reference Semantics and Lifetime + +All dispatch functors hold `std::shared_ptr const&` — a reference to a shared pointer, not a copy of it. `Message` objects can be substantial (they carry serialized, optionally compressed protobuf payloads), and the `shared_ptr` already manages the object's lifetime. Because these functors are ephemeral stack-allocated temporaries created and consumed within a single `foreach` call frame, the reference is safe as long as the caller's `shared_ptr` stays alive for the loop's duration — a reasonable invariant that the typical usage pattern naturally satisfies. + +Similarly, `send_if_pred` and `send_if_not_pred` store `Predicate const&`. The predicate must therefore outlive the dispatch functor, which again holds naturally in the intended usage of same-frame temporaries. + +## Predicate Details + +`match_peer` identifies a specific peer by raw pointer identity (`peer.get() == matchPeer`). The null check (`if (matchPeer)`) means a default-constructed `match_peer` with `nullptr` never matches anything, making it a safe no-op "skip nobody" sentinel. This default-null behavior is exploited by `peer_in_cluster`, which embeds a `match_peer skipPeer` member to optionally exclude one peer (typically the message originator) from a cluster broadcast, without requiring separate exclusion logic at the call site. + +`peer_in_cluster` chains these checks: first it skips the excluded peer via `skipPeer(peer)`, then verifies `peer->cluster()`. Composing `match_peer` rather than duplicating the raw-pointer comparison keeps the intent clear and avoids subtle mistakes with null handling. + +`peer_in_set` performs a lookup against a `std::set` held by `const&`, providing O(log n) membership tests at each peer visitation. Holding a reference keeps the predicate lightweight and delegates lifetime ownership to the caller. This predicate supports the targeted data-request workflows in `PeerSet.h`, where a working set of peer IDs is assembled and messages need to be delivered only to those specific participants. + +## Why Named Functors Over Lambdas + +Structuring these as named types rather than raw lambdas serves composability: `peer_in_cluster` can be combined with `send_if` or `send_if_not` interchangeably, and the types appear in error messages, making misuse diagnosable. The `return_type = void` alias on each dispatch functor is a documentation-level constraint marker signalling the expected interface for anything consumed by `foreach`. A pure lambda approach would be terser per call site but would lose the reuse, the named documentation, and the composability that makes complex relay logic readable. + +## Relationship to Sibling Files + +This header sits at the intersection of `Peer.h` — which defines `Peer::id_t`, `Peer::cluster()`, and the pure-virtual `Peer::send()` — and `Message.h`, which provides the `Message` type being dispatched. The iteration host, `Overlay::foreach()`, lives in `Overlay.h` but is not included here; the predicates themselves have no dependency on the collection that iterates them. This separation keeps the dependency graph shallow: a consumer who only needs to call `Overlay::foreach(send_always{msg})` pulls in only `Message.h` and `Peer.h`, not the heavier `Overlay.h` infrastructure. \ No newline at end of file diff --git a/src/xrpld/peerfinder/PeerfinderManager.h.ai.json b/src/xrpld/peerfinder/PeerfinderManager.h.ai.json new file mode 100644 index 0000000000..6d6d8b86bd --- /dev/null +++ b/src/xrpld/peerfinder/PeerfinderManager.h.ai.json @@ -0,0 +1,320 @@ +{ + "args": [ + { + "lineno": 62, + "name": "config" + }, + { + "lineno": 63, + "name": "port" + }, + { + "lineno": 64, + "name": "validationPublicKey" + }, + { + "lineno": 65, + "name": "ipLimit" + }, + { + "lineno": 70, + "name": "lhs" + }, + { + "lineno": 70, + "name": "rhs" + }, + { + "lineno": 78, + "name": "ep" + }, + { + "lineno": 78, + "name": "hops_" + }, + { + "lineno": 99, + "name": "result" + }, + { + "lineno": 55, + "name": "map" + }, + { + "lineno": 138, + "name": "name" + }, + { + "lineno": 138, + "name": "addresses" + }, + { + "lineno": 144, + "name": "strings" + }, + { + "lineno": 158, + "name": "local_endpoint" + }, + { + "lineno": 158, + "name": "remote_endpoint" + }, + { + "lineno": 170, + "name": "slot" + }, + { + "lineno": 170, + "name": "endpoints" + }, + { + "lineno": 186, + "name": "remote_address" + }, + { + "lineno": 186, + "name": "eps" + }, + { + "lineno": 200, + "name": "key" + }, + { + "lineno": 200, + "name": "reserved" + } + ], + "classes": [ + { + "args": [], + "lineno": 24, + "name": "Config" + }, + { + "args": [], + "lineno": 73, + "name": "Endpoint" + }, + { + "args": [], + "lineno": 113, + "name": "Manager" + } + ], + "description": "PeerFinder configuration and management interfaces for XRPL, including peer slot management, endpoint metadata, and connection logic.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/PeerfinderManager.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "Config" + }, + { + "args": [], + "lineno": 47, + "name": "calcOutPeers" + }, + { + "args": [], + "lineno": 51, + "name": "applyTuning" + }, + { + "args": [ + "map" + ], + "lineno": 55, + "name": "onWrite" + }, + { + "args": [ + "config", + "port", + "validationPublicKey", + "ipLimit" + ], + "lineno": 61, + "name": "makeConfig" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 69, + "name": "operator==" + }, + { + "args": [], + "lineno": 76, + "name": "Endpoint" + }, + { + "args": [ + "ep", + "hops_" + ], + "lineno": 78, + "name": "Endpoint" + }, + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 84, + "name": "operator<" + }, + { + "args": [ + "result" + ], + "lineno": 98, + "name": "to_string" + }, + { + "args": [], + "lineno": 113, + "name": "Manager" + }, + { + "args": [], + "lineno": 117, + "name": "~Manager" + }, + { + "args": [ + "config" + ], + "lineno": 123, + "name": "setConfig" + }, + { + "args": [], + "lineno": 127, + "name": "start" + }, + { + "args": [], + "lineno": 130, + "name": "stop" + }, + { + "args": [], + "lineno": 133, + "name": "config" + }, + { + "args": [ + "name", + "addresses" + ], + "lineno": 138, + "name": "addFixedPeer" + }, + { + "args": [ + "name", + "strings" + ], + "lineno": 144, + "name": "addFallbackStrings" + }, + { + "args": [ + "local_endpoint", + "remote_endpoint" + ], + "lineno": 158, + "name": "new_inbound_slot" + }, + { + "args": [ + "remote_endpoint" + ], + "lineno": 165, + "name": "new_outbound_slot" + }, + { + "args": [ + "slot", + "endpoints" + ], + "lineno": 170, + "name": "on_endpoints" + }, + { + "args": [ + "slot" + ], + "lineno": 175, + "name": "on_closed" + }, + { + "args": [ + "slot" + ], + "lineno": 181, + "name": "on_failure" + }, + { + "args": [ + "remote_address", + "eps" + ], + "lineno": 186, + "name": "onRedirects" + }, + { + "args": [ + "slot", + "local_endpoint" + ], + "lineno": 194, + "name": "onConnected" + }, + { + "args": [ + "slot", + "key", + "reserved" + ], + "lineno": 200, + "name": "activate" + }, + { + "args": [ + "slot" + ], + "lineno": 205, + "name": "redirect" + }, + { + "args": [], + "lineno": 209, + "name": "autoconnect" + }, + { + "args": [], + "lineno": 212, + "name": "buildEndpointsForPeers" + }, + { + "args": [], + "lineno": 217, + "name": "once_per_second" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 14, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/PeerfinderManager.h.ai.md b/src/xrpld/peerfinder/PeerfinderManager.h.ai.md new file mode 100644 index 0000000000..1a9ffdf45d --- /dev/null +++ b/src/xrpld/peerfinder/PeerfinderManager.h.ai.md @@ -0,0 +1,41 @@ +# `PeerfinderManager.h` — PeerFinder Public Interface + +This header is the sole public-facing contract for the PeerFinder subsystem. Everything else in `src/xrpld/peerfinder/` is internal implementation detail. It defines the three foundational types that the rest of the XRPL daemon interacts with: the `Config` settings struct, the `Endpoint` gossip record, and the `Manager` abstract interface that orchestrates all peer discovery and slot management. + +## Why PeerFinder Exists + +When an XRPL node starts up it must bootstrap into the peer-to-peer overlay: discover live peer addresses, establish outbound connections, accept inbound connections up to a quota, redirect full-capacity callers to other peers, and continuously maintain healthy connectivity. PeerFinder is the self-contained module that handles all of this. It is deliberately isolated from socket I/O — the `Manager` interface uses plain `beast::IP::Endpoint` and `boost::asio::ip::tcp::endpoint` values, and the caller drives the actual network calls based on what the manager says to do. + +## `Config` — Operational Parameters + +`Config` is a plain struct with an important set of derived computations baked in through three methods. + +`calcOutPeers()` implements the fractional outbound peer policy: `outPeers = max(maxPeers * 15% + 0.5, 10)`. The 15% comes from `Tuning::outPercent` and the hard floor of 10 comes from `Tuning::minOutCount`. Keeping outbound connections to roughly 15% of total capacity means the majority of slots are reserved for inbound connections, which is essential for the overlay to remain open to new participants. + +`applyTuning()` enforces a subtle per-IP admission limit. When `ipLimit` is zero (unset), it computes a value of 2 plus up to 3 extra slots scaled by how much `inPeers` exceeds the default 21-peer maximum. The critical constraint that follows is `ipLimit = max(1, min(ipLimit, inPeers / 2))` — no single IP address may consume more than half the inbound slots. + +`makeConfig()` is the factory that bridges the server-level `xrpl::Config` to `PeerFinder::Config`. Two notable policy decisions are embedded here. First, validators automatically get `peerPrivate = true` regardless of the operator's explicit `PEER_PRIVATE` setting, because a validator's IP address should stay hidden to reduce its attack surface. Second, `autoConnect` is suppressed for standalone mode (used in testing) and for private peers where autonomous connections would defeat the privacy goal. The method supports two configuration modes: a legacy `PEERS_MAX`-based mode where in/out split is derived by percentage, and a newer explicit `PEERS_IN_MAX`/`PEERS_OUT_MAX` mode where both halves are set directly. + +## `Endpoint` — Gossip Address Record + +`Endpoint` pairs a `beast::IP::Endpoint` address with a `hops` count. The hop count is the distance from the originating peer in the gossip chain: a peer advertising itself sends endpoints with `hops = 0`; each relay node increments the count. Endpoints with `hops > Tuning::maxHops` (6) are dropped. The `operator<` overloads ordering by address only, which allows the set to be deduplicated without caring about which relay path delivered an entry. + +This hop metadata is non-trivial architecturally. The Livecache deliberately cycles through all available hop depths when selecting addresses to hand out, ensuring each connected peer eventually sees addresses from the farthest corners of the overlay. This breadth-first horizon expansion is how PeerFinder drives the overlay toward lower diameter and higher connectivity without any central coordination. + +## `Result` — Slot Admission Outcomes + +The `Result` enum enumerates the five outcomes that can happen when the manager evaluates a new connection: `success`, `full` (slot quota exhausted), `inboundDisabled` (node not accepting inbound at all), `duplicatePeer` (already connected to this remote), and `ipLimitExceeded` (per-IP cap reached). The accompanying `to_string()` is a `string_view`-returning `noexcept` function — deliberately avoiding heap allocation since this is called in logging hot paths during busy connection activity. + +## `Manager` — The Core Interface + +`Manager` extends `beast::PropertyStream::Source` so its internal state can be streamed into the node's diagnostic property tree without coupling to any specific monitoring backend. The implementation is `ManagerImp` in `detail/PeerfinderManager.cpp`, instantiated by `make_Manager()` in `make_Manager.h`. + +The interface methods fall into three groups. + +**Bootstrapping setup** (`addFixedPeer`, `addFallbackStrings`) is called during startup to register known-good addresses. Fixed peers bypass connection limits entirely and are prioritized by the connection strategy — the node tries to establish all fixed connections before using the Livecache or Bootcache. + +**Slot lifecycle events** mirror the TCP connection state machine. `new_inbound_slot` and `new_outbound_slot` allocate a `Slot` when a socket is first seen, returning both the slot and an initial `Result`. The caller then drives the slot through `onConnected` (once TCP connect succeeds, providing the resolved local endpoint), `activate` (after the cryptographic handshake, when the peer's `PublicKey` is known), and finally `on_closed` or `on_failure`. The split between `onConnected` and `activate` is deliberate: a TCP connection succeeding does not yet prove the remote is a legitimate XRPL peer. + +**Periodic and gossip operations** (`on_endpoints`, `buildEndpointsForPeers`, `redirect`, `autoconnect`, `once_per_second`) form the steady-state maintenance loop. `once_per_second` is the master tick; the caller is expected to invoke it on a one-second timer. `buildEndpointsForPeers` returns the batch of endpoint lists that should be broadcast to each connected peer as `mtENDPOINTS` messages. `redirect` provides addresses to hand to a peer being rejected because the node is full. `onRedirects` processes addresses received when this node was redirected by a busy remote. + +The abstract clock alias `clock_type = beast::abstract_clock` is injected rather than taken from `std::chrono::steady_clock` directly. This makes the entire time-dependent logic (Livecache TTLs, boot cache cooldown, connection retry backoff) testable with a controlled fake clock in unit tests without modifying any production code paths. \ No newline at end of file diff --git a/src/xrpld/peerfinder/Slot.h.ai.json b/src/xrpld/peerfinder/Slot.h.ai.json new file mode 100644 index 0000000000..ca8ca40672 --- /dev/null +++ b/src/xrpld/peerfinder/Slot.h.ai.json @@ -0,0 +1,70 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "Slot" + } + ], + "description": "Defines the Slot class, representing properties and state associated with a peer-to-peer overlay connection in the XRPL PeerFinder module.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/Slot.h", + "functions": [ + { + "args": [], + "lineno": 17, + "name": "~Slot" + }, + { + "args": [], + "lineno": 20, + "name": "inbound" + }, + { + "args": [], + "lineno": 26, + "name": "fixed" + }, + { + "args": [], + "lineno": 33, + "name": "reserved" + }, + { + "args": [], + "lineno": 39, + "name": "state" + }, + { + "args": [], + "lineno": 43, + "name": "remote_endpoint" + }, + { + "args": [], + "lineno": 47, + "name": "local_endpoint" + }, + { + "args": [], + "lineno": 50, + "name": "listening_port" + }, + { + "args": [], + "lineno": 56, + "name": "public_key" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/Slot.h.ai.md b/src/xrpld/peerfinder/Slot.h.ai.md new file mode 100644 index 0000000000..176eff00ce --- /dev/null +++ b/src/xrpld/peerfinder/Slot.h.ai.md @@ -0,0 +1,41 @@ +# `Slot.h` — PeerFinder Connection Slot Interface + +`Slot.h` defines the public abstract interface for a single peer-to-peer overlay connection within XRPL's `PeerFinder` subsystem. It is the read-only view of a connection that the rest of the system observes: the `Manager` creates and owns slots internally, but hands `shared_ptr` handles outward so callers can query state without being able to mutate it through this interface. + +## Role in the System + +`PeerFinder` manages the full lifecycle of every TCP connection in the XRPL peer overlay, from initial socket acceptance through handshake completion, active data exchange, and teardown. `Slot` is the per-connection record that carries both the immutable socket identity (remote endpoint, inbound/outbound direction) and the mutable state that evolves as the connection progresses. The abstract interface is deliberately minimal — consumers of `Slot` only ever need to read its properties; all mutations happen through `SlotImp`, the concrete implementation in `detail/`. + +## State Machine + +The `State` enum encodes five ordered lifecycle phases: + +- `accept` — the socket has been accepted (inbound) but no handshake has started yet. +- `connect` — an outbound connection attempt is in progress. +- `connected` — the TCP connection is established; the handshake is underway. +- `active` — the handshake completed successfully and the peer is fully participating. +- `closing` — the connection is winding down. + +`Logic.h` enforces the valid state transitions: for example, `activate()` asserts the slot is in `accept` or `connected` before advancing to `active`, and `on_closed()` checks the slot is `active` before updating internal counters. The enum ordering matters because `Logic.h` also uses it in `stateString()` diagnostics. + +## Connection Classification Flags + +Three boolean properties classify the nature of a slot beyond its current state: + +`inbound()` distinguishes who initiated the connection. This feeds the slot counters in `Counts` (which track `in_active` vs. `out_active` separately) and affects peer-privacy rules — only outbound connections are suppressed when `peerPrivate` is enabled. The two `SlotImp` constructors reflect this split: the inbound form accepts both a `local_endpoint` and `remote_endpoint`, while the outbound form only takes the remote address (the local endpoint is filled in later via `onConnected`). + +`fixed()` marks connections whose remote address appears in the operator-configured fixed-peers list. Fixed connections are exempt from the normal slot budget enforced by `Counts::can_activate()`, because operators use them to guarantee connectivity to trusted cluster nodes regardless of how full the peer slots are. `Logic.h` treats a disconnected fixed outbound peer as a permanent reconnect obligation. + +`reserved()` is set during `activate()` once the handshake reveals the peer's identity. A slot is reserved if the peer is in the cluster or has an explicit reservation. Like `fixed`, reserved slots are not counted against the public slot cap. Crucially, `reserved` is unknown until the handshake completes — the comment on the interface makes this timing dependency explicit, preventing consumers from relying on it pre-handshake. + +## Endpoint and Identity Accessors + +`remote_endpoint()` returns a concrete `beast::IP::Endpoint` — it is always known from the moment the slot is created. `local_endpoint()` is `optional` because for inbound connections the local address is populated during construction, but for outbound connections it is only filled in when `Manager::onConnected()` fires and the OS-assigned local port can be read from the socket. + +`listening_port()` is separately `optional` because it reflects the port the *remote* peer advertises as its inbound listener, which is communicated during the handshake via `mtENDPOINTS`. `SlotImp` stores this as an `atomic` using the sentinel value `-1` for "unknown", enabling lock-free reads from multiple threads while the handshake thread writes it. The `Slot` interface exposes this as `optional` — hiding the sentinel encoding entirely from consumers. + +`public_key()` is `optional` for the same reason: it is populated only after a successful handshake. `Logic.h` uses it as a deduplication key — if `activate()` detects a key that matches an already-active slot, it returns `Result::duplicatePeer` and the connection is rejected. + +## Design: Abstract Interface over `shared_ptr` + +The choice of a pure-virtual class rather than a plain struct is deliberate separation-of-concern. The `Manager` interface takes and returns `shared_ptr` for all its event callbacks (`on_endpoints`, `on_closed`, `activate`, etc.), allowing the overlay layer to hold references to slots across asynchronous operations without coupling to `SlotImp`'s mutable API. The concrete `SlotImp` adds write-setters, the `recent_t` address-deduplication cache, and anti-flooding timestamps — none of which the caller should touch directly. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Bootcache.cpp.ai.json b/src/xrpld/peerfinder/detail/Bootcache.cpp.ai.json new file mode 100644 index 0000000000..a0bc9568fc --- /dev/null +++ b/src/xrpld/peerfinder/detail/Bootcache.cpp.ai.json @@ -0,0 +1,399 @@ +{ + "args": [ + { + "lineno": 9, + "name": "store" + }, + { + "lineno": 9, + "name": "clock" + }, + { + "lineno": 9, + "name": "journal" + }, + { + "lineno": 72, + "name": "endpoint" + }, + { + "lineno": 83, + "name": "endpoint" + }, + { + "lineno": 97, + "name": "endpoint" + }, + { + "lineno": 116, + "name": "endpoint" + }, + { + "lineno": 142, + "name": "map" + } + ], + "classes": [ + { + "args": [ + "Store& store", + "clock_type& clock", + "beast::Journal journal" + ], + "lineno": 9, + "name": "Bootcache" + } + ], + "code_paths": [ + { + "call_chain": [ + "Bootcache::load", + "m_store.load (callback)", + "m_map.insert(value_type(endpoint, valence))" + ], + "entry_point": "Bootcache::load", + "purpose": "Loads endpoints and their valence from persistent storage into the bootcache map.", + "validation_points": [ + "m_map.insert(value_type(endpoint, valence))" + ] + }, + { + "call_chain": [ + "Bootcache::insert", + "m_map.insert(value_type(endpoint, 0))" + ], + "entry_point": "Bootcache::insert", + "purpose": "Attempts to insert a new endpoint with initial valence 0 into the bootcache.", + "validation_points": [ + "m_map.insert(value_type(endpoint, 0))" + ] + }, + { + "call_chain": [ + "Bootcache::insertStatic", + "m_map.insert(value_type(endpoint, staticValence))", + "if !result.second && valence < staticValence: m_map.erase, m_map.insert(value_type(endpoint, staticValence))" + ], + "entry_point": "Bootcache::insertStatic", + "purpose": "Inserts a static endpoint with a high valence, or upgrades an existing endpoint's valence if too low.", + "validation_points": [ + "m_map.insert(value_type(endpoint, staticValence))", + "valence comparison and possible replacement" + ] + }, + { + "call_chain": [ + "Bootcache::on_success", + "m_map.insert(value_type(endpoint, 1))", + "if !result.second: update valence, m_map.erase, m_map.insert(value_type(endpoint, entry))" + ], + "entry_point": "Bootcache::on_success", + "purpose": "Records a successful connection to an endpoint, incrementing its valence.", + "validation_points": [ + "m_map.insert(value_type(endpoint, 1))", + "valence update and re-insert" + ] + }, + { + "call_chain": [ + "Bootcache::on_failure", + "m_map.insert(value_type(endpoint, -1))", + "if !result.second: update valence, m_map.erase, m_map.insert(value_type(endpoint, entry))" + ], + "entry_point": "Bootcache::on_failure", + "purpose": "Records a failed connection to an endpoint, decrementing its valence.", + "validation_points": [ + "m_map.insert(value_type(endpoint, -1))", + "valence update and re-insert" + ] + } + ], + "data_flows": [ + { + "field": "endpoint (beast::IP::Endpoint)", + "flow": [ + "Input parameter", + "m_map.insert(value_type(endpoint, valence))", + "m_map (internal storage)" + ], + "origin": "Input to Bootcache methods (load, insert, insertStatic, on_success, on_failure)", + "transformations": [ + "Assigned initial valence (from storage, 0, 1, -1, or staticValence)", + "May be replaced if valence is updated (insertStatic, on_success, on_failure)" + ], + "validated_at": "m_map.insert (checks for uniqueness, may reject duplicates)" + }, + { + "field": "valence (int)", + "flow": [ + "Loaded from store or set in function", + "Paired with endpoint in value_type", + "Stored in m_map", + "May be updated and re-inserted" + ], + "origin": "From persistent store (load), or set by insert/insertStatic/on_success/on_failure", + "transformations": [ + "Incremented on success", + "Decremented on failure", + "Set to staticValence in insertStatic" + ], + "validated_at": "Compared and possibly replaced in insertStatic, on_success, on_failure" + } + ], + "description": "Implements the Bootcache class for managing a cache of peer endpoints with valence scores in the XRPL PeerFinder subsystem, including insertion, pruning, updating, and serialization logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "endpoint (beast::IP::Endpoint)", + "empty", + "string", + "validation" + ], + "evidence": "m_map.insert(value_type(endpoint, valence)) at Bootcache::load", + "issue_pattern": "Missing empty string validation for endpoint (beast::IP::Endpoint)", + "why_false_positive": "m_map.insert(value_type(endpoint, valence)) validates endpoint (beast::IP::Endpoint) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "endpoint (beast::IP::Endpoint)", + "empty", + "string", + "validation" + ], + "evidence": "m_map.insert(value_type(endpoint, 0)) at Bootcache::insert", + "issue_pattern": "Missing empty string validation for endpoint (beast::IP::Endpoint)", + "why_false_positive": "m_map.insert(value_type(endpoint, 0)) validates endpoint (beast::IP::Endpoint) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "endpoint (beast::IP::Endpoint)", + "empty", + "string", + "validation" + ], + "evidence": "m_map.insert(value_type(endpoint, staticValence)) and valence comparison at Bootcache::insertStatic", + "issue_pattern": "Missing empty string validation for endpoint (beast::IP::Endpoint)", + "why_false_positive": "m_map.insert(value_type(endpoint, staticValence)) and valence comparison validates endpoint (beast::IP::Endpoint) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "endpoint (beast::IP::Endpoint)", + "empty", + "string", + "validation" + ], + "evidence": "m_map.insert(value_type(endpoint, 1)) and valence update at Bootcache::on_success", + "issue_pattern": "Missing empty string validation for endpoint (beast::IP::Endpoint)", + "why_false_positive": "m_map.insert(value_type(endpoint, 1)) and valence update validates endpoint (beast::IP::Endpoint) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Bootcache.cpp", + "functions": [ + { + "args": [ + "Store& store", + "clock_type& clock", + "beast::Journal journal" + ], + "lineno": 9, + "name": "Bootcache" + }, + { + "args": [], + "lineno": 15, + "name": "~Bootcache" + }, + { + "args": [], + "lineno": 19, + "name": "empty" + }, + { + "args": [], + "lineno": 24, + "name": "size" + }, + { + "args": [], + "lineno": 29, + "name": "begin" + }, + { + "args": [], + "lineno": 34, + "name": "cbegin" + }, + { + "args": [], + "lineno": 39, + "name": "end" + }, + { + "args": [], + "lineno": 44, + "name": "cend" + }, + { + "args": [], + "lineno": 49, + "name": "clear" + }, + { + "args": [], + "lineno": 56, + "name": "load" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint" + ], + "lineno": 72, + "name": "insert" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint" + ], + "lineno": 83, + "name": "insertStatic" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint" + ], + "lineno": 97, + "name": "on_success" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint" + ], + "lineno": 116, + "name": "on_failure" + }, + { + "args": [], + "lineno": 135, + "name": "periodicActivity" + }, + { + "args": [ + "beast::PropertyStream::Map& map" + ], + "lineno": 142, + "name": "onWrite" + }, + { + "args": [], + "lineno": 153, + "name": "prune" + }, + { + "args": [], + "lineno": 175, + "name": "update" + }, + { + "args": [], + "lineno": 191, + "name": "checkUpdate" + }, + { + "args": [], + "lineno": 197, + "name": "flagForUpdate" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "PeerFinder" + } + ], + "test_coverage_notes": "The code is likely tested by PeerFinder and Bootcache unit/integration tests, typically found in files like test/peerfinder/Bootcache_test.cpp or test/peerfinder/PeerFinder_test.cpp. These would test insertion, loading, success/failure handling, and static insertion. However, edge cases such as duplicate endpoint insertion, valence boundary conditions, and concurrent modifications may not be fully covered unless explicitly tested. There is no evidence in this file of direct test hooks or assertions beyond XRPL_ASSERT, so test coverage depends on external test files.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom logic (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "None (logs error via JLOG if duplicate)", + "field": "endpoint (beast::IP::Endpoint)", + "location": "Bootcache::load", + "validated_by": "m_map.insert(value_type(endpoint, valence))", + "validates": [ + "Ensures endpoint is not already present in m_map before insertion" + ], + "validation_type": "business_logic (duplicate check)" + }, + { + "confidence": 0.9, + "error_thrown": "None", + "field": "endpoint (beast::IP::Endpoint)", + "location": "Bootcache::insert", + "validated_by": "m_map.insert(value_type(endpoint, 0))", + "validates": [ + "Ensures endpoint is not already present in m_map before insertion" + ], + "validation_type": "business_logic (duplicate check)" + }, + { + "confidence": 0.9, + "error_thrown": "None", + "field": "endpoint (beast::IP::Endpoint)", + "location": "Bootcache::insertStatic", + "validated_by": "m_map.insert(value_type(endpoint, staticValence)) and valence comparison", + "validates": [ + "Ensures endpoint is not already present in m_map before insertion", + "If present, ensures valence is at least staticValence, otherwise replaces" + ], + "validation_type": "business_logic (duplicate check and valence threshold)" + }, + { + "confidence": 0.8, + "error_thrown": "None", + "field": "endpoint (beast::IP::Endpoint)", + "location": "Bootcache::on_success", + "validated_by": "m_map.insert(value_type(endpoint, 1)) and valence update", + "validates": [ + "Ensures endpoint is not already present in m_map before insertion", + "If present, updates valence to be at least 0, then increments" + ], + "validation_type": "business_logic (duplicate check and valence update)" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Bootcache.cpp.ai.md b/src/xrpld/peerfinder/detail/Bootcache.cpp.ai.md new file mode 100644 index 0000000000..2032c7198a --- /dev/null +++ b/src/xrpld/peerfinder/detail/Bootcache.cpp.ai.md @@ -0,0 +1,51 @@ +# `Bootcache.cpp` — Persistent Bootstrap Address Cache + +## Role in the System + +When an XRPL node starts cold — with no active peers and no idea who to connect to — it needs an address book. `Bootcache` is that book. It sits inside the `PeerFinder` subsystem and maintains a ranked list of IP endpoints that the node has previously encountered or been statically configured with. The cache is loaded from a SQLite-backed `Store` at startup, kept in memory during the session, and flushed back to disk when entries change. + +Unlike the `Livecache` (which holds short-lived gossip addresses), the `Bootcache` is meant to survive restarts and encode long-term connection history so the node can immediately prioritize endpoints it has successfully reached in the past. + +## The Bimap Data Structure + +The central design choice in this file is the use of a Boost `bimap` with two differently-typed views: + +- **Left side** (`unordered_set_of`): maps each `beast::IP::Endpoint` to a hash bucket for O(1) lookup by address. This is needed when recording connection outcomes — `on_success` and `on_failure` must find an endpoint instantly. +- **Right side** (`multiset_of`): sorts the `Entry` objects (wrapping `int valence`) for ordered traversal. `Entry::operator<` is deliberately inverted: it returns `true` when `lhs.valence() > rhs.valence()`, so the right-side multiset stores entries with higher valence at `begin()` and lower valence at `end()`. + +This dual-keyed structure lets `Logic.h` iterate `bootcache_.begin()` to `bootcache_.end()` and get endpoints ranked from most reliable to least, while still being able to look up any specific endpoint in O(1) when a connection attempt finishes. The `const_iterator` exposed publicly is a `boost::transform_iterator` that strips the `Entry` and yields only the `beast::IP::Endpoint`, since callers only need addresses, not scores. + +## Valence: A Streak Counter, Not a Lifetime Score + +Valence is a signed integer where positive means consecutive successes and negative means consecutive failures. The `on_success` and `on_failure` implementations enforce a clamping behavior that makes valence behave as a streak counter rather than a cumulative tally. + +In `on_success`, before incrementing, the valence is clamped to a minimum of zero: +``` +entry.valence() = std::max(entry.valence(), 0); +++entry.valence(); +``` +This means a peer that has failed five times in a row (valence = -5) but then succeeds once resets its failure streak before recording the success — it goes to valence=1, not valence=-4. The same asymmetric logic applies in `on_failure` with a `std::min(entry.valence(), 0)`. This keeps valence as an honest signal of recent behavior rather than a permanent reputation. + +Since `boost::bimap` values are immutable once inserted (mutating a key would break the sorted invariant on the right side), updating valence requires a careful erase-then-reinsert sequence, which both `on_success` and `on_failure` perform. + +## Two Insertion Paths + +`insert()` adds a dynamically learned endpoint (valence = 0) and is idempotent — if the address is already present it returns `false` without any modification. This is the path taken when `Logic` receives gossiped addresses from peers. + +`insertStatic()` handles statically configured bootstrap nodes (from the configuration file). These receive `staticValence = 32`, which places them near the top of the sorted order so the node preferentially connects to its own trusted seeds first. If the same endpoint already exists with a lower valence, `insertStatic` explicitly erases and reinserts it to enforce the minimum: a manually configured address is never deprioritized by historical failures. + +## Pruning on Overflow + +The cache has a soft limit of 1000 entries (`Tuning::bootcacheSize`). Whenever an insert would push the count over this threshold, `prune()` is called immediately after the insert. It removes 10% of entries (`Tuning::bootcachePrunePercent`) from the end of the right-side map — the lowest-valence entries — using a manual forward-iterator loop that walks backward from `m_map.right.end()`. The comment in the code is honest: Boost bimap doesn't handle erasing via reverse iterators cleanly, so the loop manually decrements a forward iterator before calling `erase`. + +## Deferred Write-Back with Cooldown + +Every mutation that should survive a restart — inserts, static inserts, and connection outcome records — calls `flagForUpdate()`. Rather than writing to the database immediately, this sets `m_needsUpdate = true` and then calls `checkUpdate()`, which only flushes to the `Store` if the cooldown timer (`Tuning::bootcacheCooldownTime = 60s`) has elapsed. + +This batching design is important because connection events can arrive in rapid bursts (multiple peers connecting or failing in quick succession), and each would otherwise trigger a full serialization of the entire cache to disk. The 60-second cooldown ensures at most one database write per minute under heavy load. The timer resets after each successful write, so write frequency is self-limiting. + +The destructor unconditionally calls `update()`, bypassing the cooldown check. This RAII flush ensures the cache isn't silently lost when the process shuts down cleanly between two cooldown windows — the final state of the cache is always persisted. + +## Interaction with `Logic` + +`Logic.h` owns a `Bootcache` instance directly (not by pointer) and drives the lifecycle: `load()` at startup, `periodicActivity()` (which delegates to `checkUpdate()`) on each maintenance tick, `insertStatic()` for configured seeds, `insert()` for gossiped addresses, `on_success()`/`on_failure()` for connection outcomes, and iteration to fill outbound connection candidates. The `onWrite()` method feeds the current cache state into the `beast::PropertyStream` diagnostic framework, making valence scores visible in monitoring output. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Bootcache.h.ai.json b/src/xrpld/peerfinder/detail/Bootcache.h.ai.json new file mode 100644 index 0000000000..70d57e62f9 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Bootcache.h.ai.json @@ -0,0 +1,155 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Store& store", + "clock_type& clock", + "beast::Journal journal" + ], + "lineno": 27, + "name": "Bootcache" + }, + { + "args": [ + "int valence" + ], + "lineno": 29, + "name": "Entry" + }, + { + "args": [], + "lineno": 48, + "name": "Transform" + } + ], + "description": "Defines the Bootcache class, which manages a cache of IP addresses useful for establishing initial peer connections in the XRPL PeerFinder subsystem. Each entry tracks connection success/failure (valence) and supports persistence, ranking, and periodic updates.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Bootcache.h", + "functions": [ + { + "args": [ + "Store& store", + "clock_type& clock", + "beast::Journal journal" + ], + "lineno": 56, + "name": "Bootcache" + }, + { + "args": [], + "lineno": 58, + "name": "~Bootcache" + }, + { + "args": [], + "lineno": 61, + "name": "empty" + }, + { + "args": [], + "lineno": 65, + "name": "size" + }, + { + "args": [], + "lineno": 69, + "name": "begin" + }, + { + "args": [], + "lineno": 71, + "name": "cbegin" + }, + { + "args": [], + "lineno": 73, + "name": "end" + }, + { + "args": [], + "lineno": 75, + "name": "cend" + }, + { + "args": [], + "lineno": 77, + "name": "clear" + }, + { + "args": [], + "lineno": 81, + "name": "load" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint" + ], + "lineno": 84, + "name": "insert" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint" + ], + "lineno": 87, + "name": "insertStatic" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint" + ], + "lineno": 90, + "name": "on_success" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint" + ], + "lineno": 93, + "name": "on_failure" + }, + { + "args": [], + "lineno": 96, + "name": "periodicActivity" + }, + { + "args": [ + "beast::PropertyStream::Map& map" + ], + "lineno": 99, + "name": "onWrite" + }, + { + "args": [], + "lineno": 103, + "name": "prune" + }, + { + "args": [], + "lineno": 104, + "name": "update" + }, + { + "args": [], + "lineno": 105, + "name": "checkUpdate" + }, + { + "args": [], + "lineno": 106, + "name": "flagForUpdate" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Bootcache.h.ai.md b/src/xrpld/peerfinder/detail/Bootcache.h.ai.md new file mode 100644 index 0000000000..08533e06eb --- /dev/null +++ b/src/xrpld/peerfinder/detail/Bootcache.h.ai.md @@ -0,0 +1,50 @@ +# `Bootcache.h` — PeerFinder Bootstrap Address Cache + +## Role in the System + +`Bootcache` is the persistent address cache that XRPL's `PeerFinder` subsystem consults whenever the node needs to establish new outbound connections but lacks live peer data. It is the "cold start" solution: when a node is freshly launched or has lost all its peers, it pulls ranked IP endpoints from this cache to begin reconnecting. The cache sits inside `Logic`, alongside the `Livecache` (which holds freshly gossiped, time-expiring addresses), and is loaded from and flushed to a persistent `Store` (backed by an SQLite database in production). + +## The Valence Reputation Score + +Each cached endpoint carries an integer *valence* that acts as a lightweight reputation score. The sign encodes the trend direction: a positive valence records the number of *consecutive* successful connection handshakes, and a negative valence records consecutive failures. The key behavioral detail is that the streak resets to zero before crossing sign boundaries. In `on_success()`, the code does: + +```cpp +entry.valence() = std::max(entry.valence(), 0); +++entry.valence(); +``` + +A previously failing peer (negative valence) is not rewarded immediately with a high positive score — it resets to 0 first, then increments to 1. The same clamping happens in `on_failure()` in reverse. This prevents a long history of successes from shielding a peer that suddenly starts refusing connections: the first failure drives it back toward 0 and then negative territory, quickly demoting it in connection priority. + +Static addresses (from `[validators]` or hand-configured peers) receive a valence of `staticValence = 32` via `insertStatic()`. If an address is already present with a lower valence, the old entry is replaced. This ensures permanently trusted peers start at the front of the connection queue and survive pruning. + +## The Bimap Data Structure + +The central data member is a `boost::bimap`: + +```cpp +using left_t = boost::bimaps::unordered_set_of; +using right_t = boost::bimaps::multiset_of>; +using map_type = boost::bimap; +``` + +The left side is an unordered set of `IP::Endpoint`, providing O(1) lookup by address. The right side is a multiset sorted by `Entry::operator<`, which sorts in *decreasing* valence order (higher valence sorts before lower valence). This dual-indexed structure makes two otherwise conflicting operations cheap: "does this endpoint already exist?" (left side, hash lookup) and "iterate endpoints from most reliable to least" (right side, sorted traversal). There is no standard container that satisfies both requirements simultaneously. + +Because `boost::bimap` treats both sides as keys, neither can be mutated in place. Valence updates in `on_success()` and `on_failure()` therefore follow an erase-then-reinsert pattern, which preserves the sorted invariant on the right side. + +## Iterator Projection via `Transform` + +The public `const_iterator` type is a `boost::transform_iterator` wrapping the right-side (valence-sorted) iterator, using the `Transform` functor to project each bimap entry back to its `beast::IP::Endpoint`. Callers — specifically `Logic::connectGrab()`, which iterates `bootcache_.begin()` to `bootcache_.end()` — see a sequence of endpoints ranked from highest to lowest valence without needing to know anything about the bimap internals or the `Entry` wrapper type. + +## Throttled Persistence + +Writes to the backing `Store` are debounced with a 60-second cooldown (`Tuning::bootcacheCooldownTime`). The flag `m_needsUpdate` tracks whether any mutation has occurred since the last save. `flagForUpdate()` sets this flag and immediately calls `checkUpdate()`, which only proceeds with the write if `m_clock.now()` has passed `m_whenUpdate`. After a write, the cooldown timer is reset. This pattern avoids hammering the database on rapid connection bursts (e.g., when a node reconnects to many peers in quick succession after a disconnect). + +`periodicActivity()` is called from the `Logic` event loop on a timer and simply delegates to `checkUpdate()`, giving the cache a regular opportunity to flush deferred writes. The destructor calls `update()` unconditionally, bypassing the cooldown check, so the final state is always persisted on clean shutdown regardless of whether the cooldown has elapsed. + +## Pruning Policy + +When `size()` exceeds `Tuning::bootcacheSize` (1000 entries), `prune()` removes the lowest-valence entries until the cache is back under the limit. It trims `bootcachePrunePercent` (10%) of the current size, iterating the right-side multiset in reverse — from the least-reputable endpoint backward. Pruning is triggered on every insert path (`insert`, `insertStatic`, `on_success`, `on_failure`) and after `load()`, so the cache never grows unboundedly even under a flood of incoming address redirects (`Tuning::maxRedirects` bounds per-connection redirects at the `Logic` layer, but the cache-level pruning acts as a final backstop). + +## Relationship to `Store` + +`Store` is a pure-virtual interface with two methods: `load(callback)`, which fires the callback for each persisted `(endpoint, valence)` pair, and `save(vector)`. `Bootcache::load()` clears the in-memory map first and then rebuilds it entirely from the store, calling `prune()` afterward to trim any stale overgrowth. The concrete implementation in production is `StoreSqdb`, which persists to an sqdb/SQLite file on disk. The abstract interface means `Bootcache` can be unit-tested with a trivial in-memory store without touching the filesystem. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Checker.h.ai.json b/src/xrpld/peerfinder/detail/Checker.h.ai.json new file mode 100644 index 0000000000..18e199a1b3 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Checker.h.ai.json @@ -0,0 +1,129 @@ +{ + "args": [ + { + "lineno": 54, + "name": "io_context" + }, + { + "lineno": 101, + "name": "owner" + }, + { + "lineno": 101, + "name": "handler" + }, + { + "lineno": 89, + "name": "endpoint" + }, + { + "lineno": 94, + "name": "op" + }, + { + "lineno": 116, + "name": "ec" + } + ], + "classes": [ + { + "args": [ + "boost::asio::io_context& io_context" + ], + "lineno": 13, + "name": "Checker" + }, + { + "args": [], + "lineno": 18, + "name": "basic_async_op" + }, + { + "args": [ + "Checker& owner", + "boost::asio::io_context& io_context", + "Handler&& handler" + ], + "lineno": 29, + "name": "async_op" + } + ], + "description": "Defines a template class Checker that tests remote listening sockets to ensure they are connectable, providing asynchronous connection testing and management of pending operations using Boost.Asio.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Checker.h", + "functions": [ + { + "args": [ + "boost::asio::io_context& io_context" + ], + "lineno": 54, + "name": "Checker" + }, + { + "args": [], + "lineno": 67, + "name": "~Checker" + }, + { + "args": [], + "lineno": 74, + "name": "stop" + }, + { + "args": [], + "lineno": 81, + "name": "wait" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint", + "Handler&& handler" + ], + "lineno": 89, + "name": "async_connect" + }, + { + "args": [ + "basic_async_op& op" + ], + "lineno": 94, + "name": "remove" + }, + { + "args": [ + "Checker& owner", + "boost::asio::io_context& io_context", + "Handler&& handler" + ], + "lineno": 101, + "name": "async_op" + }, + { + "args": [], + "lineno": 107, + "name": "~async_op" + }, + { + "args": [], + "lineno": 111, + "name": "stop" + }, + { + "args": [ + "error_code const& ec" + ], + "lineno": 116, + "name": "operator()" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Checker.h.ai.md b/src/xrpld/peerfinder/detail/Checker.h.ai.md new file mode 100644 index 0000000000..60ed3b799e --- /dev/null +++ b/src/xrpld/peerfinder/detail/Checker.h.ai.md @@ -0,0 +1,59 @@ +# `Checker.h` — Async Reachability Probe for PeerFinder + +## Purpose + +`Checker` sits inside the `PeerFinder` subsystem and answers one question: *can the rest of the network actually reach a peer at its advertised listening address?* When an inbound peer connects and reports the address it claims to accept incoming connections on, XRPL cannot simply trust that address — the port might be firewalled, NAT-translated incorrectly, or entirely wrong. `Checker` performs an outbound TCP connection attempt to that reported endpoint, then reports success or failure back to `Logic`. The result is recorded on the `SlotImp` as `canAccept`, gating whether that address is ever propagated to other peers via the live cache. + +## Class Structure + +`Checker` is templated on a Boost.Asio protocol type (defaulting to `boost::asio::ip::tcp`), primarily to enable substitution of a mock protocol in tests. It manages a collection of in-flight async operations using a `boost::intrusive::list` of `basic_async_op` objects, protected by a `std::mutex` and a `std::condition_variable`. + +The internal type hierarchy achieves type erasure in two layers. `basic_async_op` is a polymorphic base with virtual `stop()` and `operator()(error_code)` methods, and it embeds a `boost::intrusive::list_base_hook` directly — eliminating the extra heap allocation that `std::list>` would require. The concrete `async_op` template inherits from it, binding together a `socket_type`, the caller-supplied completion handler, and a back-reference to the owning `Checker`. + +## Ownership and Lifetime Contract + +The most subtle aspect of the design is how `async_op` lifetime is managed. In `async_connect`, the op is created as a `std::shared_ptr>`, pushed into the intrusive list, and then the socket's `async_connect` is called with a lambda that captures that same `shared_ptr`: + +```cpp +op->socket_.async_connect( + ..., + std::bind(&basic_async_op::operator(), op, std::placeholders::_1)); +``` + +The lambda keeps the `async_op` alive for the entire duration of the asynchronous operation — regardless of what the caller does after returning from `async_connect`. When the I/O completes (or is canceled), Asio invokes the lambda, which dispatches through the virtual `operator()` to the user's handler. When the lambda is destroyed, the `shared_ptr` reference count drops to zero, the `async_op` destructor runs, and it calls `checker_.remove(*this)`. `remove()` erases the op from the intrusive list under lock and signals the condition variable if the list is now empty. + +This pattern is idiomatic "self-managing async operation" design: the op registers itself into a tracked collection at construction and deregisters at destruction, with no separate cleanup step required. + +## Stop and Wait Protocol + +`stop()` marks `stop_ = true` under lock, then calls `socket_.cancel(ec)` on every live operation. This issues asynchronous cancellation — it does not wait for completion and returns immediately. Handlers will receive `operation_aborted` errors rather than completion values. + +`wait()` blocks on `cond_` until the intrusive list is empty, meaning all pending handlers (whether they completed normally or with `operation_aborted`) have run and their `async_op` destructors have executed. + +The destructor calls only `wait()`, not `stop()`. This is intentional: destruction just drains whatever is pending; if you want cancellation before destruction, you must call `stop()` first. In `ManagerImp`, this sequencing is explicit: + +```cpp +void stop() override { + work_.reset(); + checker_.stop(); // cancel pending I/O + m_logic.stop(); +} + +~ManagerImp() override { stop(); } // Checker::~Checker() then calls wait() +``` + +## Integration with Logic + +`Logic` holds a reference to `Checker` as a template parameter, keeping the dependency injectable and mockable. When a newly-connected inbound peer advertises its listening endpoints via `on_endpoints`, `Logic` calls: + +```cpp +m_checker.async_connect( + ep.address, + std::bind(&Logic::checkComplete, this, + slot->remote_endpoint(), ep.address, + std::placeholders::_1)); +``` + +The `checkComplete` callback receives the `error_code`. If it is `operation_aborted`, it returns silently (the slot was already torn down). Otherwise, a non-zero error sets `slot.canAccept = false` and notifies the boot cache of a failure; success sets `canAccept = true` and records the confirmed listening port on the slot. Only after this check passes is the peer's address eligible to enter the live cache and be gossiped to other nodes. + +The `async_connect` documentation explicitly notes that "the execution guarantees offered by asio handlers are NOT enforced," meaning the handler runs on whatever thread services the `io_context`. `Logic::checkComplete` accordingly takes `lock_` immediately upon entry to safely access the `slots_` map. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Counts.h.ai.json b/src/xrpld/peerfinder/detail/Counts.h.ai.json new file mode 100644 index 0000000000..9564efa78a --- /dev/null +++ b/src/xrpld/peerfinder/detail/Counts.h.ai.json @@ -0,0 +1,148 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "Counts" + } + ], + "description": "Manages and tracks the counts of various peer connection slots (inbound, outbound, fixed, reserved, etc.) for the XRPL PeerFinder subsystem.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Counts.h", + "functions": [ + { + "args": [ + "s" + ], + "lineno": 15, + "name": "add" + }, + { + "args": [ + "s" + ], + "lineno": 21, + "name": "remove" + }, + { + "args": [ + "s" + ], + "lineno": 27, + "name": "can_activate" + }, + { + "args": [], + "lineno": 41, + "name": "attempts_needed" + }, + { + "args": [], + "lineno": 48, + "name": "attempts" + }, + { + "args": [], + "lineno": 53, + "name": "out_max" + }, + { + "args": [], + "lineno": 59, + "name": "out_active" + }, + { + "args": [], + "lineno": 66, + "name": "fixed" + }, + { + "args": [], + "lineno": 71, + "name": "fixed_active" + }, + { + "args": [ + "config" + ], + "lineno": 78, + "name": "onConfig" + }, + { + "args": [], + "lineno": 86, + "name": "acceptCount" + }, + { + "args": [], + "lineno": 91, + "name": "connectCount" + }, + { + "args": [], + "lineno": 96, + "name": "closingCount" + }, + { + "args": [], + "lineno": 101, + "name": "in_max" + }, + { + "args": [], + "lineno": 106, + "name": "inboundActive" + }, + { + "args": [], + "lineno": 111, + "name": "totalActive" + }, + { + "args": [], + "lineno": 117, + "name": "inboundSlotsFree" + }, + { + "args": [], + "lineno": 125, + "name": "outboundSlotsFree" + }, + { + "args": [], + "lineno": 133, + "name": "isConnectedToNetwork" + }, + { + "args": [ + "map" + ], + "lineno": 146, + "name": "onWrite" + }, + { + "args": [], + "lineno": 157, + "name": "state_string" + }, + { + "args": [ + "s", + "n" + ], + "lineno": 167, + "name": "adjust" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Counts.h.ai.md b/src/xrpld/peerfinder/detail/Counts.h.ai.md new file mode 100644 index 0000000000..be53461259 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Counts.h.ai.md @@ -0,0 +1,47 @@ +# `Counts.h` — Peer Slot Accounting for PeerFinder + +`Counts` is a pure bookkeeping class that tracks the current occupancy of every connection-slot category managed by the PeerFinder subsystem. It answers the resource-management questions that `Logic.h` needs in order to make policy decisions: Are there free inbound slots? Should more outbound attempts be initiated? Can this newly-handshaked slot be promoted to active? Without accurate counts, the connection manager would be blind to its own state. + +## Relationship to `Logic` and `Slot` + +`Logic` (in `Logic.h`) holds a single `Counts counts_` member alongside the `Slots` map. Every time a slot is created, transitions states, or is destroyed, `Logic` calls `counts_.add()` or `counts_.remove()` — or both in sequence when a slot's state changes. `Logic` guards all these operations under its own `std::recursive_mutex lock_`, so `Counts` itself carries no synchronization. This is a deliberate design choice: `Counts` is a value-semantics helper embedded inside a larger guarded object, and giving it its own mutex would create nested-lock complexity for no benefit. + +The `Slot` abstract interface (from `Slot.h`) exposes the three properties `Counts` needs — `state()`, `inbound()`, `fixed()`, and `reserved()` — without exposing mutable internals. + +## The `adjust()` Core + +All counting logic funnels through the private `adjust(Slot const& s, int n)` method, where `n` is `+1` for additions and `-1` for removals. `add()` and `remove()` are thin wrappers that call `adjust` with the appropriate sign. This single-entry-point pattern ensures that every counter that tracks a given slot property is updated together, making it impossible for add and remove to diverge in the fields they touch. + +The `adjust()` method interprets the slot's state enum (`accept`, `connect`, `connected`, `active`, `closing`) with a switch statement and updates the corresponding counters. The assertions embedded in the cases enforce expected invariants: `accept` state must be inbound, while `connect` and `connected` states must be outbound. These are caught in debug builds where they would otherwise silently corrupt the counts. + +## The Fixed/Reserved Carve-Out + +The most architecturally significant design choice in `Counts` is how fixed and reserved connections are treated. Fixed peers — those explicitly listed in the node's configuration — and reserved peers (cluster members, peers with explicit reservations) are tracked in separate counters (`m_fixed`, `m_fixed_active`, `m_reserved`) and are explicitly excluded from the inbound/outbound active tallies used to enforce slot limits. This appears in two places: + +- In `adjust()`, the `active` case only increments `m_in_active` or `m_out_active` when `!s.fixed() && !s.reserved()`. +- In `can_activate()`, if `s.fixed() || s.reserved()` is true, the method returns `true` unconditionally, bypassing the limit check against `m_in_active`/`m_out_active`. + +The rationale is that fixed and reserved connections represent administrative intent — the operator explicitly wants these peers — so they must not be blocked by capacity policies that apply to ordinary public peers. Fixed peers are also what allow a private cluster to form before the public peer quota fills up. + +## State Transition Accounting + +The `Slot::State` enum defines five states: `accept`, `connect`, `connected`, `active`, `closing`. Of these: + +- `accept` — inbound connection pending handshake — increments `m_acceptCount`. +- `connect` and `connected` — outbound attempt in progress — both increment `m_attempts`, the outbound connection attempt counter. +- `active` — fully established peer — increments `m_active`, plus conditionally `m_in_active`, `m_out_active`, and `m_fixed_active` as appropriate. +- `closing` — graceful teardown — increments `m_closingCount`. + +When `Logic` transitions a slot between states, it calls `counts_.remove(*slot)` with the old state, then transitions the slot, then calls `counts_.add(*slot)` with the new state. This sequence ensures counts are always consistent with the slot map. + +## Connection Policy Gates + +`can_activate()` is the primary admission-control gate. Before promoting a slot from `connected` or `accept` to `active`, `Logic` calls this method. For non-fixed, non-reserved connections, it enforces `m_in_active < m_in_max` (inbound) or `m_out_active < m_out_max` (outbound). The limits themselves come from the `Config` object via `onConfig()`, which sets `m_out_max = config.outPeers` and `m_in_max = config.inPeers` (only if `config.wantIncoming` is set; otherwise `m_in_max` stays at its zero default, rejecting all inbound slots). + +`attempts_needed()` computes how many more outbound connection attempts `Logic` should launch by comparing `m_attempts` against `Tuning::maxConnectAttempts` (20). This caps the in-flight connection storm — the node will not initiate more than 20 simultaneous outbound attempts regardless of how many free slots remain. + +`isConnectedToNetwork()` has a subtle implementation: it returns `true` only when `m_out_max == 0`. This reflects the specific edge case where a node is configured with zero desired outbound connections (e.g., a pure listener), in which case it considers itself "connected" without needing to establish any outbound peers. In the common case where `m_out_max > 0`, the method always returns `false`, and `Logic` uses other state (comparing `m_out_active` against `m_out_max`) to decide whether to drive more connections. + +## Diagnostics + +`onWrite()` serialises the current state into a `beast::PropertyStream::Map`, producing labelled fields `accept`, `connect`, `close`, `in` (as `active/max`), `out` (as `active/max`), `fixed`, `reserved`, and `total`. `state_string()` returns a compact human-readable summary (e.g., `"3/8 out, 10/21 in, 2 connecting, 0 closing"`) used in log messages throughout `Logic`. Both methods are read-only and carry no side effects, making them safe to call for monitoring at any point. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Endpoint.cpp.ai.json b/src/xrpld/peerfinder/detail/Endpoint.cpp.ai.json new file mode 100644 index 0000000000..7b8646bfdc --- /dev/null +++ b/src/xrpld/peerfinder/detail/Endpoint.cpp.ai.json @@ -0,0 +1,140 @@ +{ + "args": [ + { + "lineno": 6, + "name": "ep" + }, + { + "lineno": 6, + "name": "hops_" + } + ], + "classes": [ + { + "args": [ + "beast::IP::Endpoint const& ep", + "std::uint32_t hops_" + ], + "lineno": 6, + "name": "Endpoint" + } + ], + "code_paths": [ + { + "call_chain": [ + "External code (e.g., PeerFinderManager, PeerFinder logic)", + "Endpoint::Endpoint" + ], + "entry_point": "Endpoint::Endpoint(beast::IP::Endpoint const& ep, std::uint32_t hops_)", + "purpose": "Constructs an Endpoint object, validating and storing the number of hops and the network address.", + "validation_points": [ + "Endpoint::Endpoint: hops_ is validated via std::min(hops_, Tuning::maxHops + 1)" + ] + } + ], + "data_flows": [ + { + "field": "hops", + "flow": [ + "Caller provides hops_", + "Endpoint::Endpoint receives hops_", + "hops is set to std::min(hops_, Tuning::maxHops + 1)", + "hops stored as member variable" + ], + "origin": "Constructor argument hops_ (passed from caller, e.g., PeerFinder logic)", + "transformations": [ + "hops_ is clamped to a maximum of Tuning::maxHops + 1 using std::min" + ], + "validated_at": "Endpoint::Endpoint (constructor)" + }, + { + "field": "address", + "flow": [ + "Caller provides ep", + "Endpoint::Endpoint receives ep", + "address is set to ep", + "address stored as member variable" + ], + "origin": "Constructor argument ep (beast::IP::Endpoint)", + "transformations": [ + "No transformation; direct assignment" + ], + "validated_at": "No explicit validation in this code" + } + ], + "description": "Defines the constructor for the Endpoint class in the xrpl::PeerFinder namespace, initializing endpoint address and hop count with a maximum limit.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hops_", + "empty", + "string", + "validation" + ], + "evidence": "std::min at Endpoint::Endpoint (constructor)", + "issue_pattern": "Missing empty string validation for hops_", + "why_false_positive": "std::min validates hops_ for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "hops_", + "range", + "bounds", + "validation" + ], + "evidence": "std::min at Endpoint::Endpoint (constructor)", + "issue_pattern": "Missing range validation for hops_", + "why_false_positive": "std::min validates hops_ range" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Endpoint.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "PeerFinder" + } + ], + "test_coverage_notes": "Direct tests for Endpoint validation (hops clamping) would likely be found in PeerFinder or Endpoint unit tests, possibly in files like test/peerfinder/ or test/unit/PeerFinder*.cpp. If such tests exist, they should check that hops is never set above Tuning::maxHops + 1. If not, this validation path may be untested. There is no evidence in this file of explicit test hooks or assertions, so coverage depends on higher-level PeerFinder tests. Gaps may exist if tests do not provide hops_ values above Tuning::maxHops + 1 to verify clamping.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation using std::min)", + "validation_layer": "business_logic (constructor)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (value is clamped, not rejected)", + "field": "hops_", + "location": "Endpoint::Endpoint (constructor)", + "validated_by": "std::min", + "validates": [ + "Ensures hops_ does not exceed Tuning::maxHops + 1", + "If hops_ > Tuning::maxHops + 1, sets hops to Tuning::maxHops + 1" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Endpoint.cpp.ai.md b/src/xrpld/peerfinder/detail/Endpoint.cpp.ai.md new file mode 100644 index 0000000000..b8a1e7eed7 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Endpoint.cpp.ai.md @@ -0,0 +1,34 @@ +# `PeerFinder::Endpoint` Constructor — `Endpoint.cpp` + +## Role in the System + +`Endpoint.cpp` provides the single non-trivial constructor for `PeerFinder::Endpoint`, the fundamental data type that PeerFinder uses to gossip peer addresses across the XRPL network. An `Endpoint` pairs a `beast::IP::Endpoint` (IP address and port) with a hop count: an unsigned integer representing how many relay hops away the peer was when it last advertised itself. + +The file is intentionally minimal — the struct is declared in `PeerfinderManager.h`, the default constructor is defaulted there, and only this parameterized constructor needs a separate definition because it references `Tuning::maxHops` from `Tuning.h`. + +## The Hop-Count Cap + +The only logic in the constructor is: + +```cpp +: hops(std::min(hops_, Tuning::maxHops + 1)), address(ep) +``` + +`Tuning::maxHops` is 6. So every `Endpoint` stored in the system has a `hops` value in the range `[0, 7]`. The `+1` is deliberate: it creates a sentinel value meaning "beyond the horizon" rather than truncating to exactly `maxHops`. + +Why clamp in the constructor rather than validate at the call site? Because `Endpoint` objects are built directly from data received over the network — an adversarial or buggy peer could send a hop count near `UINT32_MAX`. Clamping at construction ensures that no matter what raw value arrives, the stored field stays within a known domain. The constructor does not throw; it silently clamps. Silent clamping is preferable to rejection here because the value will be further filtered by `Logic::preprocess` before it ever reaches the livecache. + +## Relationship to `Logic::preprocess` + +The clamped value interacts with the gossip pipeline in `Logic.h`. When a peer sends a `TMEndpoints` message, `Logic::on_endpoints` calls `preprocess`, which: + +1. **Drops** any endpoint where `ep.hops > Tuning::maxHops` (i.e., `hops > 6`). An incoming value of 6 survives; anything ≥ 7 is erased. +2. **Increments** surviving hop counts by one (`++ep.hops`) before storing them in the livecache. This models the additional relay hop added by re-broadcasting. + +So an endpoint arriving with `hops == 6` enters the livecache as `hops == 7`. If that endpoint were then forwarded again, the `Endpoint` constructor would clamp it back to 7, and `preprocess` would immediately drop it because `7 > maxHops`. The cap is therefore the natural terminus of gossip propagation: no address travels more than six hops from its source. + +The sentinel value of 7 also never appears in addresses sent outward — `Handouts.h` skips endpoints with `hops == 0` (which are self-advertisements, only meaningful to the receiving peer), and the livecache discards anything that has propagated past `maxHops`. + +## Design Notes + +The choice of `maxHops + 1` as the cap rather than `maxHops` is careful: if the cap were exactly `maxHops`, a constructor-clamped value of 6 would pass `preprocess`'s `> 6` check and then increment to 7, surviving into the livecache with a hop count that would cause it to be dropped next time it is forwarded. The current cap gives the same outcome — an excessively large incoming `hops_` is stored as 7, immediately discarded by `preprocess`'s filter — without any ambiguity about the boundary condition. It is a one-line defensive contract that makes the gossip-propagation invariant self-enforcing at the data layer. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Fixed.h.ai.json b/src/xrpld/peerfinder/detail/Fixed.h.ai.json new file mode 100644 index 0000000000..0c878f48e8 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Fixed.h.ai.json @@ -0,0 +1,59 @@ +{ + "args": [ + { + "lineno": 10, + "name": "clock" + }, + { + "lineno": 22, + "name": "now" + }, + { + "lineno": 29, + "name": "now" + } + ], + "classes": [ + { + "args": [ + "clock" + ], + "lineno": 8, + "name": "Fixed" + } + ], + "description": "Defines the Fixed class, which manages metadata for a fixed peer slot, including connection attempt timing and backoff logic based on connection failures or successes.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Fixed.h", + "functions": [ + { + "args": [], + "lineno": 16, + "name": "when" + }, + { + "args": [ + "now" + ], + "lineno": 21, + "name": "failure" + }, + { + "args": [ + "now" + ], + "lineno": 28, + "name": "success" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Fixed.h.ai.md b/src/xrpld/peerfinder/detail/Fixed.h.ai.md new file mode 100644 index 0000000000..3a3b5c9483 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Fixed.h.ai.md @@ -0,0 +1,45 @@ +# `Fixed.h` — Fixed Peer Connection Backoff Metadata + +`Fixed` is a small state-management class inside the `PeerFinder` subsystem that tracks reconnection timing for *fixed slots* — peer endpoints that are explicitly configured in the node's settings (e.g., trusted validators or known-good relays). Unlike dynamically-discovered peers, fixed peers must be persistently pursued: if a connection attempt fails, the system should retry, but not so aggressively that it floods an unreachable host. + +## What a "Fixed Slot" Means + +In PeerFinder's connection strategy (`Logic.h`), the peer set is divided into slots discovered through normal gossip and slots that are statically configured. The latter are stored in `fixed_`, a `std::map`. Every entry in that map owns one `Fixed` instance, which answers a single question for the connection scheduler: *is it time to try this address again?* + +## Class Design + +`Fixed` holds two private members: + +- `m_when` — a `clock_type::time_point` representing the earliest moment a new connection attempt is permitted. +- `m_failures` — a `std::size_t` counting consecutive failures since the last successful connection (zero-initialized). + +The constructor takes a `clock_type&` reference and sets `m_when = clock.now()`, so a freshly-registered fixed peer is immediately eligible for its first connection attempt. The copy constructor is defaulted, allowing `Fixed` objects to be stored by value in the map without ceremony. + +## Backoff Logic + +`failure()` and `success()` are the only mutating methods, and together they implement a Fibonacci-based exponential backoff. When a connection attempt fails: + +```cpp +m_failures = std::min(m_failures + 1, Tuning::connectionBackoff.size() - 1); +m_when = now + std::chrono::minutes(Tuning::connectionBackoff[m_failures]); +``` + +`Tuning::connectionBackoff` is the array `{1, 1, 2, 3, 5, 8, 13, 21, 34, 55}`. Each element is a wait time in minutes. The failure index advances through this sequence with each consecutive failure, capping at index 9 (55 minutes) so the backoff never grows beyond that. The Fibonacci progression is a deliberate choice: it is gentler than a pure exponential doubling yet still grows quickly enough to avoid hammering a down peer every few seconds. + +When a connection succeeds, `success()` resets `m_failures` to 0 and sets `m_when = now`, making the peer immediately eligible again if it were to disconnect and need reconnection. + +## How the Scheduler Uses It + +`Logic::get_fixed()` iterates the `fixed_` map and selects peers whose `when() <= now`, skipping any that are on cooldown or already connected: + +```cpp +if (iter->second.when() <= now && squelches.find(address) == squelches.end() && ...) +``` + +`Logic` is also responsible for calling `failure()` or `success()` on the appropriate `Fixed` instance when a connection outcome is known. `Fixed` itself has no knowledge of socket state — it is purely a timing record, keeping the backoff policy cleanly separated from connection mechanics. + +## Design Notes + +The cap at `connectionBackoff.size() - 1` is a defensive invariant: it prevents the `m_failures` counter from ever indexing out-of-bounds even if `failure()` is called far more times than there are backoff levels. The maximum retry interval of 55 minutes is also pragmatically bounded — a fixed peer should never be abandoned entirely, so waiting longer than an hour would undermine the purpose of the fixed-slot concept. + +Because `Fixed` is only ever accessed while `Logic`'s internal mutex is held, there is no per-object synchronization needed; the class is deliberately free of any concurrency machinery. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Handouts.h.ai.json b/src/xrpld/peerfinder/detail/Handouts.h.ai.json new file mode 100644 index 0000000000..a738655012 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Handouts.h.ai.json @@ -0,0 +1,117 @@ +{ + "args": [ + { + "lineno": 19, + "name": "t" + }, + { + "lineno": 19, + "name": "h" + }, + { + "lineno": 39, + "name": "first" + }, + { + "lineno": 39, + "name": "last" + }, + { + "lineno": 39, + "name": "seq_first" + }, + { + "lineno": 39, + "name": "seq_last" + }, + { + "lineno": 69, + "name": "slot" + }, + { + "lineno": 80, + "name": "ep" + }, + { + "lineno": 122, + "name": "slot" + }, + { + "lineno": 133, + "name": "ep" + }, + { + "lineno": 179, + "name": "needed" + }, + { + "lineno": 179, + "name": "squelches" + }, + { + "lineno": 185, + "name": "endpoint" + } + ], + "classes": [ + { + "args": [ + "SlotImp::ptr const& slot" + ], + "lineno": 67, + "name": "RedirectHandouts" + }, + { + "args": [ + "SlotImp::ptr const& slot" + ], + "lineno": 120, + "name": "SlotHandouts" + }, + { + "args": [ + "std::size_t needed", + "Squelches& squelches" + ], + "lineno": 176, + "name": "ConnectHandouts" + } + ], + "description": "This file provides utilities for distributing network endpoint information among peers in the XRPL PeerFinder system, including classes and functions for handing out endpoints for connection redirection, periodic sharing, and automatic connection attempts.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Handouts.h", + "functions": [ + { + "args": [ + "Target& t", + "HopContainer& h" + ], + "lineno": 18, + "name": "handout_one" + }, + { + "args": [ + "TargetFwdIter first", + "TargetFwdIter last", + "SeqFwdIter seq_first", + "SeqFwdIter seq_last" + ], + "lineno": 38, + "name": "handout" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "PeerFinder" + }, + { + "lineno": 9, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Handouts.h.ai.md b/src/xrpld/peerfinder/detail/Handouts.h.ai.md new file mode 100644 index 0000000000..6af1c21d36 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Handouts.h.ai.md @@ -0,0 +1,27 @@ +# Handouts.h — Endpoint Distribution Logic for PeerFinder + +This file implements the "handout" mechanism at the heart of PeerFinder's peer exchange protocol. When an XRPL node shares knowledge of other nodes, it cannot simply broadcast everything it knows to everyone — it must distribute endpoints fairly across recipients, avoid redundancy, respect per-slot history, and prevent abuse. `Handouts.h` encodes those business rules in three concrete target types and a generic distribution algorithm that drives all of them. + +## The Core Distribution Algorithm + +`detail::handout_one()` is the atomic unit: given a single target `t` and a hop container `h`, it walks the container looking for the first endpoint the target will accept via `try_insert()`. When one is accepted, it calls `h.move_back(it)` to rotate that endpoint to the tail of the container. This is a deliberate fairness mechanism — it prevents the same endpoint from being handed to every target in the same pass before other endpoints get a turn, implementing a weak round-robin at the source level. The function is guarded by an `XRPL_ASSERT` that the target is not already full before it is called. + +The outer `handout()` function drives the whole distribution in a double loop: for each pass it iterates the sequence containers (one per hop level in the Livecache), and for each hop container it attempts to give one endpoint to each non-full target via `handout_one`. The outer loop repeats until either all targets are full or a full pass yields zero insertions, whichever comes first. The `all_full` short-circuit exits immediately when no target has remaining capacity, avoiding a complete scan of remaining hop containers. This structure ensures endpoints are distributed as evenly as possible across all recipients and hop levels before any recipient receives a second item from the same source. + +## Three Target Types + +All three target classes satisfy the same duck-typed interface — `full()` returning a `bool` and `try_insert()` returning a `bool` — which allows `handout()` to operate on any of them generically without virtual dispatch. There is no abstract base class; this is zero-cost structural polymorphism at the template level. + +**`RedirectHandouts`** is used when a newly connecting peer must be turned away because the node has no free slots. Rather than simply refusing, it collects up to `Tuning::redirectEndpointCount` (10) alternative addresses to send back via the TMEndpoints redirect mechanism. Its `try_insert()` applies several filters: it rejects endpoints beyond `Tuning::maxHops` (6), rejects hops-0 entries (the node's own address), rejects the connecting peer's own IP, and deduplicates by address ignoring port. The port deduplication comment is explicit: "Ignore port for security reasons," preventing an adversary from flooding the redirect list with the same host on many different ports. + +**`SlotHandouts`** handles the periodic endpoint broadcasts sent to already-connected peers via TMEndpoints gossip. It collects up to `Tuning::numberOfEndpoints` (12, computed as `2 * maxHops`) endpoints per slot. Beyond the standard filters (hop limit, deduplication, don't send a peer its own address), it consults `slot_->recent.filter()` before accepting an endpoint. This `recent_t` cache on each `SlotImp` tracks what endpoints have been sent to or received from that peer recently, preventing the same address from being sent again until it has aged out of the peer's cache. Crucially, on successful insertion `try_insert` also calls `slot_->recent.insert()` for the accepted endpoint — even though the endpoint didn't originate from that slot. This pessimistic update ensures the next periodic handout won't re-send the same item before it has expired at the far end. + +**`ConnectHandouts`** collects candidate addresses for the node to actively dial. It takes a `needed` count and a reference to a shared `Squelches` object — a `beast::aged_set` owned by the `Logic` layer. When `try_insert()` accepts an endpoint it calls `m_squelches.insert()`: if the address is already in the squelch set the insert fails (returning `false`), and the endpoint is silently skipped. If it's new, it is added to both the squelch set and the output list. This means `ConnectHandouts` both reads and writes the squelch set in a single operation, avoiding any two-phase update. The squelch set is time-based, so recently-attempted addresses naturally age out and become eligible again after `Tuning::recentAttemptDuration` (60 seconds). Because the squelch set is taken by reference rather than by value, squelches from prior connection rounds persist across calls, capping reconnect frequency without requiring any additional bookkeeping in the caller. + +## How Logic.h Consumes These Types + +`Logic.h` constructs these objects and passes them to `handout()` in three distinct scenarios. For redirects, it creates a single `RedirectHandouts`, shuffles `livecache_.hops`, and calls `handout` with that one target. For periodic broadcasts, it creates a `SlotHandouts` per connected slot, then calls `handout` across all of them simultaneously — this is the multi-target case where the fairness properties of `handout()` matter most, preventing early slots from monopolizing the best endpoints. For autoconnect, `ConnectHandouts` receives endpoints from both the Livecache (preferring reversed hop order, to try closer-to-leaf nodes first) and from the Bootcache as a fallback. + +## Template Oddity + +All three classes use the `template ` pattern on their constructors and `try_insert()` method bodies defined in the header. This is an unusual but functional idiom that defers instantiation: the method bodies are template function definitions, so they are only compiled when actually used, avoiding multiple-definition errors without splitting into a `.cpp` file. It's a header-only implementation technique for what would otherwise need to be a non-inline member function. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Livecache.h.ai.json b/src/xrpld/peerfinder/detail/Livecache.h.ai.json new file mode 100644 index 0000000000..2661f960b6 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Livecache.h.ai.json @@ -0,0 +1,136 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 23, + "name": "LivecacheBase" + }, + { + "args": [ + "endpoint_" + ], + "lineno": 28, + "name": "Element" + }, + { + "args": [ + "list" + ], + "lineno": 44, + "name": "Hop" + }, + { + "args": [ + "clock", + "journal", + "alloc" + ], + "lineno": 98, + "name": "Livecache" + }, + { + "args": [ + "alloc" + ], + "lineno": 120, + "name": "hops_t" + } + ], + "description": "Implements the Livecache class for PeerFinder, which manages a short-lived cache of relayed Endpoint messages, organizing them by hop count and providing expiration, insertion, and statistics functionality.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Livecache.h", + "functions": [ + { + "args": [ + "clock", + "journal", + "alloc" + ], + "lineno": 109, + "name": "Livecache" + }, + { + "args": [], + "lineno": 163, + "name": "empty" + }, + { + "args": [], + "lineno": 169, + "name": "size" + }, + { + "args": [], + "lineno": 175, + "name": "expire" + }, + { + "args": [ + "ep" + ], + "lineno": 191, + "name": "insert" + }, + { + "args": [ + "map" + ], + "lineno": 227, + "name": "onWrite" + }, + { + "args": [], + "lineno": 253, + "name": "shuffle" + }, + { + "args": [], + "lineno": 266, + "name": "histogram" + }, + { + "args": [ + "alloc" + ], + "lineno": 274, + "name": "hops_t" + }, + { + "args": [ + "e" + ], + "lineno": 279, + "name": "insert" + }, + { + "args": [ + "e", + "numHops" + ], + "lineno": 292, + "name": "reinsert" + }, + { + "args": [ + "e" + ], + "lineno": 307, + "name": "remove" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + }, + { + "lineno": 17, + "name": "PeerFinder" + }, + { + "lineno": 20, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Livecache.h.ai.md b/src/xrpld/peerfinder/detail/Livecache.h.ai.md new file mode 100644 index 0000000000..39fc1d2fc2 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Livecache.h.ai.md @@ -0,0 +1,51 @@ +# `Livecache.h` — Short-Lived Peer Endpoint Cache + +## Role in the System + +`Livecache` stores the transient stream of peer endpoint advertisements that flow through the XRPL gossip protocol. When a peer sends an `mtENDPOINTS` message, the addresses it contains are inserted into this cache. The cache's defining characteristic is deliberate ephemerality: entries expire after just 30 seconds (`Tuning::liveCacheSecondsToLive`). The reasoning is direct — peers only advertise themselves when they have open connection slots, so a stale advertisement is worse than no advertisement at all. + +This contrasts sharply with `Bootcache`, the sibling component that persists addresses to disk across restarts. `Livecache` makes no attempt to verify connectivity of its entries and is explicitly unsuitable for bootstrapping. It is the hot, real-time view of who has open slots right now, while `Bootcache` is the cold, verified record of who has historically been reachable. + +## Data Structure Design + +The cache uses two cooperating data structures. Primary storage is a `beast::aged_map` — a time-ordered associative container that supports efficient chronological scanning for expiration. Secondary indexing uses arrays of `boost::intrusive::list` partitioned by hop count. + +The `Element` type is the key to making these two structures work together without extra allocation. It inherits from `boost::intrusive::list_base_hook<>`, embedding the list linkage directly in the struct. This means each `Element` can simultaneously be the value in the aged map *and* a node in one of the hop-count lists with no heap allocation for list membership. When `hops.remove(e)` or `hops.insert(e)` is called, it operates directly on the `Element` reference retrieved from the map. + +## Hop-Count Partitioning + +The `hops_t` nested class owns an array of `list_type` of size `1 + Tuning::maxHops + 1` (9 entries for `maxHops = 6`). Each slot holds all endpoints known to be that many hops away. Index 0 represents the local node itself. Indices 1 through `maxHops` are normal relayable endpoints. Index `maxHops + 1` is a special holding area: when the caller receives an endpoint message from a peer that was already at `maxHops`, it increments the hop count before calling `insert()`, landing the address in this overflow bucket. These addresses are used for outgoing connection attempts and redirect responses but are never propagated further — distributing them would push them past the hop limit. + +A parallel `Histogram` array maintains counts per hop bucket, supporting the `histogram()` diagnostic string output. + +## Insert Policy: Prefer the Shortest Path + +The `insert()` method implements a keep-best-hops policy. Three cases arise when inserting an `Endpoint`: + +1. **New address**: inserted into the aged map, added to the hop list at its reported hop count. +2. **Duplicate at a higher hop count**: silently dropped. If the same peer is known closer, the more distant advertisement carries no new information. +3. **Duplicate at a lower or equal hop count**: the entry's timestamp is refreshed via `m_cache.touch()`, and if the new hop count is actually lower, `hops.reinsert()` moves the element to the correct bucket. Refreshing ensures the TTL is extended for an address still being actively advertised. + +The `XRPL_ASSERT` on entry enforces the invariant that no address exceeds `maxHops + 1`, which must be upheld by the caller before calling `insert()`. + +## Security: Mandatory Shuffle Before Handout + +The `hops_t::insert()` method always places new elements at the front of the list with `push_front`. The code comment is explicit: *"This has security implications without a shuffle."* A malicious peer could repeatedly advertise its own address to ensure it appears at the head of every hop bucket, biasing which addresses other nodes connect to or relay. + +The defence is that `Logic` calls `hops.shuffle()` before every handout — for `redirect()`, `autoconnect()`, and `buildEndpointsForPeers()`. The shuffle copies each intrusive list into a temporary `std::vector` of references, randomizes with `default_prng()`, then rebuilds the list. After shuffling, the insertion-order bias is neutralized. + +After an endpoint is handed out, `Hop::move_back()` moves it to the tail of its bucket list. This provides fairness under repeated handouts: endpoints that were just given out recede to the back, giving other entries a turn at the front. + +## Iterator Architecture + +Two levels of `boost::transform_iterator` compose the public interface. At the inner level, `Hop::Transform` converts `Element` references into `Endpoint` const references, so callers never see the intrusive hook machinery. At the outer level, `hops_t::Transform` converts `list_type` references into `Hop` view objects, so iterating over `hops` yields a sequence of hop-bucket views. + +The `beast::maybe_const` utility enables the single `Hop` template to serve both const and mutable contexts from a shared implementation. The `move_back()` method on `Hop` requires a `const_cast` to mutate the list through what is always a const iterator — safe in practice because the underlying `Element` objects are always non-const; the constness is an artifact of the iterator type, not the storage. + +## Expiration + +`expire()` scans `m_cache.chronological` from oldest to newest, stopping as soon as it finds an entry newer than `Tuning::liveCacheSecondsToLive`. For each expired entry, `hops.remove(e)` unlinks the element from its bucket list before erasing it from the map. `Logic::once_per_second()` drives this call. + +## Template Allocator Parameter + +`Livecache` is parameterized on an allocator, defaulting to `std::allocator`. In production the default is used. The allocator is threaded through to the `aged_map` constructor, enabling the unit test infrastructure to inject a custom allocator for deterministic memory tracking. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Logic.h.ai.json b/src/xrpld/peerfinder/detail/Logic.h.ai.json new file mode 100644 index 0000000000..9fb6b18747 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Logic.h.ai.json @@ -0,0 +1,281 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "clock_type& clock", + "Store& store", + "Checker& checker", + "beast::Journal journal" + ], + "lineno": 36, + "name": "Logic" + } + ], + "description": "Implements the core logic for maintaining and managing peer slots, connections, and address caches in the XRPL PeerFinder subsystem, including connection strategies, slot state transitions, endpoint message handling, and statistics reporting.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Logic.h", + "functions": [ + { + "args": [ + "clock_type& clock", + "Store& store", + "Checker& checker", + "beast::Journal journal" + ], + "lineno": 54, + "name": "Logic" + }, + { + "args": [], + "lineno": 74, + "name": "load" + }, + { + "args": [], + "lineno": 86, + "name": "stop" + }, + { + "args": [ + "Config const& c" + ], + "lineno": 104, + "name": "config" + }, + { + "args": [], + "lineno": 110, + "name": "config" + }, + { + "args": [ + "std::string const& name", + "beast::IP::Endpoint const& ep" + ], + "lineno": 115, + "name": "addFixedPeer" + }, + { + "args": [ + "std::string const& name", + "std::vector const& addresses" + ], + "lineno": 119, + "name": "addFixedPeer" + }, + { + "args": [ + "beast::IP::Endpoint const& remoteAddress", + "beast::IP::Endpoint const& checkedAddress", + "boost::system::error_code ec" + ], + "lineno": 143, + "name": "checkComplete" + }, + { + "args": [ + "beast::IP::Endpoint const& local_endpoint", + "beast::IP::Endpoint const& remote_endpoint" + ], + "lineno": 186, + "name": "new_inbound_slot" + }, + { + "args": [ + "beast::IP::Endpoint const& remote_endpoint" + ], + "lineno": 227, + "name": "new_outbound_slot" + }, + { + "args": [ + "SlotImp::ptr const& slot", + "beast::IP::Endpoint const& local_endpoint" + ], + "lineno": 255, + "name": "onConnected" + }, + { + "args": [ + "SlotImp::ptr const& slot", + "PublicKey const& key", + "bool reserved" + ], + "lineno": 282, + "name": "activate" + }, + { + "args": [ + "SlotImp::ptr const& slot" + ], + "lineno": 334, + "name": "redirect" + }, + { + "args": [], + "lineno": 347, + "name": "autoconnect" + }, + { + "args": [], + "lineno": 429, + "name": "buildEndpointsForPeers" + }, + { + "args": [], + "lineno": 489, + "name": "once_per_second" + }, + { + "args": [ + "SlotImp::ptr const& slot", + "Endpoints& list" + ], + "lineno": 504, + "name": "preprocess" + }, + { + "args": [ + "SlotImp::ptr const& slot", + "Endpoints list" + ], + "lineno": 547, + "name": "on_endpoints" + }, + { + "args": [ + "SlotImp::ptr const& slot" + ], + "lineno": 601, + "name": "remove" + }, + { + "args": [ + "SlotImp::ptr const& slot" + ], + "lineno": 634, + "name": "on_closed" + }, + { + "args": [ + "SlotImp::ptr const& slot" + ], + "lineno": 678, + "name": "on_failure" + }, + { + "args": [ + "FwdIter first", + "FwdIter last", + "boost::asio::ip::tcp::endpoint const& remote_address" + ], + "lineno": 684, + "name": "onRedirects" + }, + { + "args": [ + "beast::IP::Endpoint const& endpoint" + ], + "lineno": 697, + "name": "fixed" + }, + { + "args": [ + "beast::IP::Address const& address" + ], + "lineno": 705, + "name": "fixed" + }, + { + "args": [ + "std::size_t needed", + "Container& c", + "typename ConnectHandouts::Squelches& squelches" + ], + "lineno": 713, + "name": "get_fixed" + }, + { + "args": [ + "std::shared_ptr const& source" + ], + "lineno": 728, + "name": "addStaticSource" + }, + { + "args": [ + "std::shared_ptr const& source" + ], + "lineno": 732, + "name": "addSource" + }, + { + "args": [ + "IPAddresses const& list" + ], + "lineno": 742, + "name": "addBootcacheAddresses" + }, + { + "args": [ + "std::shared_ptr const& source" + ], + "lineno": 754, + "name": "fetch" + }, + { + "args": [ + "beast::IP::Endpoint const& address" + ], + "lineno": 803, + "name": "is_valid_address" + }, + { + "args": [ + "beast::PropertyStream::Set& set", + "Slots const& slots" + ], + "lineno": 813, + "name": "writeSlots" + }, + { + "args": [ + "beast::PropertyStream::Map& map" + ], + "lineno": 826, + "name": "onWrite" + }, + { + "args": [], + "lineno": 868, + "name": "counts" + }, + { + "args": [ + "Slot::State state" + ], + "lineno": 872, + "name": "stateString" + }, + { + "args": [ + "FwdIter first", + "FwdIter last", + "boost::asio::ip::tcp::endpoint const& remote_address" + ], + "lineno": 889, + "name": "Logic::onRedirects" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + }, + { + "lineno": 20, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Logic.h.ai.md b/src/xrpld/peerfinder/detail/Logic.h.ai.md new file mode 100644 index 0000000000..6ee4571738 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Logic.h.ai.md @@ -0,0 +1,78 @@ +# `Logic.h` — PeerFinder Connection Strategy Engine + +## Role and Purpose + +`Logic` is the central decision-making class of the XRPL PeerFinder subsystem. Every policy question the network layer cannot answer on its own flows through here: which addresses to attempt connections to, which incoming connections to accept or reject, what gossip endpoint information to share with each peer, and how to record success or failure. The class is deliberately isolated from the actual I/O machinery by being templated on a `Checker` type, which allows unit tests to inject a mock connectivity tester while production code supplies the real async TCP prober. + +## Data Model + +Six data structures together capture the complete peer topology state at any instant: + +`slots_` is the master table — a `std::map>` keyed by remote endpoint. Every live connection, inbound or outbound, regardless of state, has an entry here. The map is the single source of truth for "are we connected to this address?" + +`connectedAddresses_` is a parallel `std::multiset` (port-stripped) used exclusively to enforce `Config::ipLimit` — the maximum number of connections from a single IP address. Tracking addresses separately from the full endpoint is intentional: two connections from the same host on different ports count against the same limit. + +`keys_` is a `std::set` that prevents duplicate connections to the same cryptographic identity, even if two connection attempts arrive from different remote ports. + +`fixed_` maps configured always-on peer endpoints to `Fixed` metadata objects that carry Fibonacci backoff state. When a fixed peer's outbound connection fails, `Fixed::failure()` advances the backoff index through a pre-computed array `{1, 1, 2, 3, 5, 8, 13, 21, 34, 55}` minutes, ensuring reconnect attempts back off gracefully without requiring external timer infrastructure. + +`livecache_` is a short-lived (30-second TTL) gossip cache populated from `mtENDPOINTS` messages received from active peers. It is organised internally by hop count, enabling the handout algorithm to prefer topologically nearby peers. + +`bootcache_` is a persistent address store backed by the injected `Store` interface (an SQLite database in production). When the livecache is empty and no outbound attempts are in flight, `autoconnect()` falls back to bootcache entries. The bootcache is loaded at startup and updated incrementally. + +## Slot Lifecycle + +Slots transition through a state machine with distinct paths for inbound and outbound connections: + +For **outbound** connections, `new_outbound_slot()` creates a `SlotImp` in `Slot::connect` state (counted as an in-flight attempt by `Counts`). When the TCP handshake completes, `onConnected()` advances the slot to `Slot::connected` and performs self-connect detection — it checks whether the newly discovered local endpoint already exists as a *remote* endpoint in `slots_`, which would indicate a loopback connection to ourselves. The `activate()` method then handles the XRPL handshake: it checks for duplicate public keys, determines whether the peer's slot type (fixed, reserved, or ordinary) gives it capacity bypass rights, and if capacity allows, moves the slot to `Slot::active`. + +For **inbound** connections, `new_inbound_slot()` applies per-IP limits and duplicate checks before allocating a slot in `Slot::accept` state. The slot then waits for `activate()` to be called after the XRPL handshake completes. If `counts_.can_activate()` returns false because all inbound slots are filled, the slot receives `Result::full` and the caller is expected to redirect or close. + +`on_closed()` performs cleanup: removes from `slots_`, `keys_`, and `connectedAddresses_`, updates `Counts`, and records fixed-slot failures when a fixed peer closes before reaching `active` state. + +## Outbound Connection Strategy (`autoconnect`) + +`autoconnect()` implements a strict four-tier priority order, returning early at the first tier that produces results: + +1. **Fixed peers**: If fewer fixed connections are active than configured, `get_fixed()` scans `fixed_` for entries whose backoff `when()` has elapsed and that are not already in the squelch set or the current slot table. Fixed peers are returned in preference to everything else. + +2. **Livecache**: The livecache hops list is shuffled and passed to the `handout()` algorithm via a `ConnectHandouts` receiver. The reverse-order iteration (from highest hops to lowest) means addresses furthest from the broadcasting peer are preferred — they carry more topological diversity. + +3. **Bootcache refill**: Commented placeholder for DNS-based address resolution when both the livecache is empty and no attempts are in flight. + +4. **Bootcache fallback**: Iterates the bootcache directly until the `ConnectHandouts` receiver is full. + +Between tiers, if attempts are in flight but a tier produced no new candidates, `autoconnect()` returns an empty list and waits — avoiding the thundering-herd problem of launching redundant connection attempts. + +The `m_squelches` aged set persists across calls to `autoconnect()` with a 60-second TTL, preventing rapid reconnection attempts to the same address. + +## Endpoint Gossip: Receiving (`on_endpoints` + `preprocess`) + +When an active peer sends an `mtENDPOINTS` message, `on_endpoints()` first enforces a per-slot rate limit (`whenAcceptEndpoints`) of one accepted message per `Tuning::secondsPerMessage` (151 seconds, chosen as a prime deliberately). Oversized messages are randomly sampled down to `Tuning::numberOfEndpointsMax`. + +`preprocess()` then cleans the list: +- Entries exceeding `Tuning::maxHops` (6) are silently discarded. +- Exactly one `hops == 0` entry is allowed; it announces the sender's own listening port. Its IP is replaced with the sender's actual socket address since the sender doesn't know its own public IP. +- Non-public and unspecified addresses are dropped. +- Duplicates within the list are dropped. +- All surviving hop counts are incremented by one before storage, so that when we retransmit these entries, the hop count reflects our own distance to the origin. + +For first-hop entries (now stored at `hops == 1`), if the slot has not yet been connectivity-tested, `on_endpoints()` triggers `m_checker.async_connect()` to verify the peer's claimed listening port is actually reachable. The slot is marked with `connectivityCheckInProgress` to prevent duplicate checks. Only peers that pass this test are admitted to the livecache. + +## Endpoint Gossip: Broadcasting (`buildEndpointsForPeers`) + +`buildEndpointsForPeers()` is called periodically and assembles the endpoint lists to broadcast to each active peer. It shuffles the active slot list (to vary broadcast order across cycles) and creates a `SlotHandouts` receiver for each active peer. The `handout()` template function then distributes livecache entries fairly across all receivers in a round-robin pass by hop level. + +Self-advertisement uses a notable trick: rather than including the node's own public IP (which may not be known), an endpoint with `hops == 0` and the all-zeros IPv6 address is injected. Recipients that receive a zero-address entry at hops 0 are specified to use the TCP socket's remote address instead. This cleanly sidesteps the "what is my own public IP" problem without requiring STUN or similar external discovery. + +Each peer's `recent` cache (an aged map in `SlotImp`) prevents the same endpoint from being sent to the same peer until the cache entry expires, limiting redundant gossip. + +## Concurrency + +All methods acquire `std::recursive_mutex lock_`. The recursive variant is required because `on_closed()` calls `remove()`, which is also independently callable and must be lockable. `checkComplete()` explicitly checks for `boost::asio::error::operation_aborted` before acquiring the lock, allowing the async Checker callback to safely no-op when the operation was cancelled during shutdown. + +The `stopping_` flag and `fetchSource_` handle the shutdown race: `stop()` sets `stopping_` and cancels any in-progress source fetch while holding the lock; `fetch()` checks `stopping_` both before starting and after completing the synchronous source fetch to avoid processing results during teardown. + +## Observability + +`onWrite()` serialises the complete internal state — bootcache size, fixed count, per-slot details, aggregated `Counts`, `Config`, livecache stats, and bootcache stats — into a `beast::PropertyStream::Map`. This feeds the administrative `peers` RPC endpoint and server-side diagnostics without requiring a separate query path. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp.ai.json b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp.ai.json new file mode 100644 index 0000000000..ffa05ab14e --- /dev/null +++ b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp.ai.json @@ -0,0 +1,346 @@ +{ + "args": [ + { + "lineno": 11, + "name": "lhs" + }, + { + "lineno": 11, + "name": "rhs" + }, + { + "lineno": 41, + "name": "map" + }, + { + "lineno": 49, + "name": "cfg" + }, + { + "lineno": 49, + "name": "port" + }, + { + "lineno": 49, + "name": "validationPublicKey" + }, + { + "lineno": 49, + "name": "ipLimit" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Config::makeConfig", + "Config::calcOutPeers", + "Config::applyTuning" + ], + "entry_point": "Config::makeConfig", + "purpose": "Creates and validates a PeerFinder::Config object from external configuration and runtime parameters.", + "validation_points": [ + "Config::calcOutPeers (validates outPeers)", + "Config::applyTuning (validates ipLimit)", + "Config::makeConfig (validates maxPeers, inPeers, wantIncoming)" + ] + }, + { + "call_chain": [ + "Config::applyTuning" + ], + "entry_point": "Config::applyTuning", + "purpose": "Applies business rules and clamps ipLimit based on inPeers and other constraints.", + "validation_points": [ + "Config::applyTuning (validates ipLimit)" + ] + }, + { + "call_chain": [ + "Config::calcOutPeers" + ], + "entry_point": "Config::calcOutPeers", + "purpose": "Calculates outPeers based on maxPeers and tuning constants.", + "validation_points": [ + "Config::calcOutPeers (validates outPeers)" + ] + } + ], + "data_flows": [ + { + "field": "outPeers", + "flow": [ + "Config::makeConfig", + "Config::calcOutPeers (if PEERS_OUT_MAX == 0)", + "Config::applyTuning (no further change)", + "Config object returned" + ], + "origin": "Config::calcOutPeers (default constructor) or cfg.PEERS_OUT_MAX (makeConfig)", + "transformations": [ + "Calculated as max((maxPeers * Tuning::outPercent + 50) / 100, Tuning::minOutCount)", + "Set directly from cfg.PEERS_OUT_MAX if provided" + ], + "validated_at": "Config::calcOutPeers" + }, + { + "field": "ipLimit", + "flow": [ + "Config::makeConfig (set from param)", + "Config::applyTuning (may be modified based on inPeers and business rules)", + "Config object returned" + ], + "origin": "Parameter to Config::makeConfig or default (0)", + "transformations": [ + "If 0, set to 2, then possibly increased if inPeers > Tuning::defaultMaxPeers", + "Clamped to [1, inPeers/2]" + ], + "validated_at": "Config::applyTuning" + }, + { + "field": "maxPeers", + "flow": [ + "Config::makeConfig (set from cfg.PEERS_MAX or 0)", + "Config::calcOutPeers (used in calculation)", + "Config object returned" + ], + "origin": "cfg.PEERS_MAX or 0", + "transformations": [ + "Set to at least Tuning::minOutCount if PEERS_OUT_MAX/IN_MAX are zero" + ], + "validated_at": "Config::makeConfig" + }, + { + "field": "inPeers", + "flow": [ + "Config::makeConfig (set from cfg.PEERS_IN_MAX or calculated)", + "Config::applyTuning (used to clamp ipLimit)", + "Config object returned" + ], + "origin": "cfg.PEERS_IN_MAX or calculated from maxPeers - outPeers", + "transformations": [ + "Set to maxPeers - outPeers if PEERS_IN_MAX is zero", + "Otherwise set directly from cfg.PEERS_IN_MAX" + ], + "validated_at": "Config::makeConfig" + }, + { + "field": "wantIncoming", + "flow": [ + "Config::makeConfig (set based on peerPrivate and port)", + "Config object returned" + ], + "origin": "(!peerPrivate) && (port != 0)", + "transformations": [ + "Set to false if peerPrivate is true or port is 0" + ], + "validated_at": "Config::makeConfig" + } + ], + "description": "Implements the PeerFinder::Config class and related functions for configuring peer connection parameters in the XRPL peer finder subsystem.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "outPeers", + "empty", + "string", + "validation" + ], + "evidence": "Config::calcOutPeers at Config::calcOutPeers", + "issue_pattern": "Missing empty string validation for outPeers", + "why_false_positive": "Config::calcOutPeers validates outPeers for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ipLimit", + "empty", + "string", + "validation" + ], + "evidence": "Config::applyTuning at Config::applyTuning", + "issue_pattern": "Missing empty string validation for ipLimit", + "why_false_positive": "Config::applyTuning validates ipLimit for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "maxPeers", + "empty", + "string", + "validation" + ], + "evidence": "Config::makeConfig at Config::makeConfig", + "issue_pattern": "Missing empty string validation for maxPeers", + "why_false_positive": "Config::makeConfig validates maxPeers for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "inPeers", + "empty", + "string", + "validation" + ], + "evidence": "Config::makeConfig at Config::makeConfig", + "issue_pattern": "Missing empty string validation for inPeers", + "why_false_positive": "Config::makeConfig validates inPeers for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "wantIncoming", + "empty", + "string", + "validation" + ], + "evidence": "Config::makeConfig at Config::makeConfig", + "issue_pattern": "Missing empty string validation for wantIncoming", + "why_false_positive": "Config::makeConfig validates wantIncoming for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp", + "functions": [ + { + "args": [ + "lhs", + "rhs" + ], + "lineno": 11, + "name": "operator==" + }, + { + "args": [], + "lineno": 20, + "name": "Config::calcOutPeers" + }, + { + "args": [], + "lineno": 26, + "name": "Config::applyTuning" + }, + { + "args": [ + "map" + ], + "lineno": 41, + "name": "Config::onWrite" + }, + { + "args": [ + "cfg", + "port", + "validationPublicKey", + "ipLimit" + ], + "lineno": 49, + "name": "Config::makeConfig" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "PeerFinder" + } + ], + "test_coverage_notes": "The code is primarily validated via construction and transformation in makeConfig, calcOutPeers, and applyTuning. Typical test coverage would be in unit tests for PeerFinder::Config, likely in files such as test/peerfinder/PeerFinder_test.cpp or similar. Tests should cover edge cases for PEERS_MAX, PEERS_OUT_MAX, PEERS_IN_MAX, ipLimit, and peer privacy settings. Gaps may exist if there are no tests for: (1) ipLimit clamping logic, (2) interaction between peerPrivate and wantIncoming, (3) min/max boundary conditions for outPeers and inPeers, (4) standalone mode. If these tests do not exist, validation logic could be insufficiently covered.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "none (manual validation in business logic)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (value is clamped)", + "field": "outPeers", + "location": "Config::calcOutPeers", + "validated_by": "Config::calcOutPeers", + "validates": [ + "Ensures outPeers is at least Tuning::minOutCount", + "Calculates outPeers as a percentage of maxPeers" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (value is clamped)", + "field": "ipLimit", + "location": "Config::applyTuning", + "validated_by": "Config::applyTuning", + "validates": [ + "If ipLimit is 0, sets a default value (2)", + "Increases ipLimit if inPeers > Tuning::defaultMaxPeers", + "Clamps ipLimit between 1 and inPeers/2" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (value is clamped)", + "field": "maxPeers", + "location": "Config::makeConfig", + "validated_by": "Config::makeConfig", + "validates": [ + "If cfg.PEERS_MAX != 0, assigns to config.maxPeers", + "Ensures config.maxPeers is at least Tuning::minOutCount" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (value is set to 0 if negative)", + "field": "inPeers", + "location": "Config::makeConfig", + "validated_by": "Config::makeConfig", + "validates": [ + "If config.maxPeers >= config.outPeers, sets inPeers to the difference", + "Otherwise, sets inPeers to 0" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "wantIncoming", + "location": "Config::makeConfig", + "validated_by": "Config::makeConfig", + "validates": [ + "Sets wantIncoming to false if peerPrivate is true or port is 0" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp.ai.md b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp.ai.md new file mode 100644 index 0000000000..9e2d268358 --- /dev/null +++ b/src/xrpld/peerfinder/detail/PeerfinderConfig.cpp.ai.md @@ -0,0 +1,55 @@ +# `PeerfinderConfig.cpp` — PeerFinder Connection Configuration + +This file implements the method bodies for `PeerFinder::Config`, the value-type struct that governs how an XRPL node discovers and manages peer connections. It sits at the boundary between the coarse server-wide `xrpl::Config` (parsed from `rippled.cfg`) and the internal `PeerFinder::Manager` that executes the connection policy at runtime. + +## Responsibilities + +`PeerFinder::Config` holds five numerically-related slot counts (`maxPeers`, `outPeers`, `inPeers`), several behavioral flags (`autoConnect`, `wantIncoming`, `peerPrivate`), and the per-IP connection ceiling (`ipLimit`). This file provides three things: the logic for deriving `outPeers` from `maxPeers`, the business rules that sanity-clamp `ipLimit`, and the factory function that translates an operator's `rippled.cfg` into a validated `Config` object. + +## Default Construction and `calcOutPeers()` + +The constructor initializes `outPeers` immediately via `calcOutPeers()`. This works because `maxPeers` already has its in-class default of `Tuning::defaultMaxPeers` (21) when the constructor runs, giving a coherent default `Config` object even without calling `makeConfig()`. The formula uses rounding-half-up integer arithmetic: + +```cpp +std::max((maxPeers * Tuning::outPercent + 50) / 100, std::size_t(Tuning::minOutCount)) +``` + +With `outPercent = 15` and `defaultMaxPeers = 21`, the raw result is 3, which is immediately overridden by `minOutCount = 10`. The hard floor exists deliberately: the comment in `Tuning.h` notes that it is enforced *outside* the Logic layer so unit tests can use small peer counts without hitting edge cases in the connection-management logic. + +## `makeConfig()` — the Configuration Bridge + +`makeConfig()` is a static factory that maps `xrpl::Config` fields to `PeerFinder::Config` fields. The xrpl::Config comments reveal a migration in progress: `PEERS_MAX` is the legacy combined limit, while `PEERS_OUT_MAX` and `PEERS_IN_MAX` are its intended replacements. The factory handles both modes explicitly: + +- **Legacy mode** (`PEERS_OUT_MAX == 0 && PEERS_IN_MAX == 0`): `maxPeers` is set from `PEERS_MAX` (or left at the default), `outPeers` is calculated via `calcOutPeers()`, and `inPeers` is the remainder. If the node can't or won't accept incoming connections, `outPeers` is set equal to `maxPeers` and `inPeers` becomes zero. + +- **New mode** (either `PEERS_OUT_MAX` or `PEERS_IN_MAX` is non-zero): the split is set directly from config, and `maxPeers` is deliberately set to `0`. This sentinel value signals that `maxPeers` is not meaningful in this mode — the two individual limits stand on their own. + +### Two-tier Validator Privacy + +The privacy logic is intentionally ordered. First, `wantIncoming` is derived from `!peerPrivate && port != 0`. Then, if `validationPublicKey` is non-empty (i.e., the node is a validator), `peerPrivate` is forced true *after* `wantIncoming` has already been set. The effect is that validators configured with a key but without an explicit `[peer_private]` setting will still advertise `wantIncoming = true` internally — they can accept inbound connections — but they will request that peers never republish their address via gossip. This is what the code comment calls "soft" peer privacy: it protects validators from being widely advertised while allowing them to build connections organically. + +If the operator explicitly sets `[peer_private]` in config, the earlier path sets both `peerPrivate` and `wantIncoming = false` together, producing full privacy where the node refuses all inbound connections. + +### Standalone Mode and `autoConnect` + +`autoConnect` is disabled when the node runs standalone (`cfg.standalone()`) or has explicit peer privacy. Standalone mode is used for development or testing without a live network; having automatic peer discovery enabled there would be a logic error. + +## `applyTuning()` — Defending Incoming Slots + +`applyTuning()` enforces one key invariant: no single remote IP address should be able to exhaust all incoming connection slots. If `ipLimit` was left at zero by the caller, a base value of 2 is assigned, then scaled upward proportionally for nodes with unusually large `inPeers` counts (one extra slot per multiple of `defaultMaxPeers`). The final line clamps the result to `[1, inPeers/2]`: + +```cpp +ipLimit = std::max(1, std::min(ipLimit, static_cast(inPeers / 2))); +``` + +The lower bound of 1 means a node with even a single inbound slot can still accept connections. The upper bound of `inPeers / 2` ensures that at minimum two distinct source IPs are needed to fill all inbound slots, providing basic resistance to connection monopolization by a single actor. This is applied at the end of `makeConfig()` as the final "business rule enforcement" step, making the correct call order explicit to readers. + +## `onWrite()` — Diagnostics + +`onWrite()` serializes the configuration into a `beast::PropertyStream::Map` for the live admin inspection surface. The `Manager` class inherits from `beast::PropertyStream::Source`, and it delegates to this method when a monitoring or admin endpoint queries the peerfinder subsystem's current configuration. + +## Relationship to Other Files + +- **`Tuning.h`**: defines all the numeric constants (`outPercent`, `minOutCount`, `defaultMaxPeers`) consumed here. Keeping these in a separate header allows them to be shared with both the config logic and the connection-management `Logic` class without circular dependencies. +- **`PeerfinderManager.h`**: declares the `Config` struct, the `Manager` abstract interface, and the `Result` enum. This file only provides the method implementations; the data members and their in-class defaults live in the header. +- **`xrpld/core/Config.h`**: the source of `PEERS_MAX`, `PEERS_OUT_MAX`, `PEERS_IN_MAX`, `PEER_PRIVATE`, and `standalone()`. The comments in that file confirm the legacy/new mode duality that `makeConfig()` must navigate. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp.ai.json b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp.ai.json new file mode 100644 index 0000000000..b5566235c1 --- /dev/null +++ b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp.ai.json @@ -0,0 +1,701 @@ +{ + "args": [ + { + "lineno": 19, + "name": "io_context" + }, + { + "lineno": 20, + "name": "clock" + }, + { + "lineno": 21, + "name": "journal" + }, + { + "lineno": 22, + "name": "config" + }, + { + "lineno": 23, + "name": "collector" + }, + { + "lineno": 53, + "name": "config" + }, + { + "lineno": 61, + "name": "name" + }, + { + "lineno": 61, + "name": "addresses" + }, + { + "lineno": 66, + "name": "name" + }, + { + "lineno": 66, + "name": "strings" + }, + { + "lineno": 70, + "name": "name" + }, + { + "lineno": 70, + "name": "url" + }, + { + "lineno": 76, + "name": "local_endpoint" + }, + { + "lineno": 76, + "name": "remote_endpoint" + }, + { + "lineno": 82, + "name": "remote_endpoint" + }, + { + "lineno": 87, + "name": "slot" + }, + { + "lineno": 87, + "name": "endpoints" + }, + { + "lineno": 92, + "name": "slot" + }, + { + "lineno": 97, + "name": "slot" + }, + { + "lineno": 102, + "name": "remote_address" + }, + { + "lineno": 102, + "name": "eps" + }, + { + "lineno": 109, + "name": "slot" + }, + { + "lineno": 109, + "name": "local_endpoint" + }, + { + "lineno": 114, + "name": "slot" + }, + { + "lineno": 114, + "name": "key" + }, + { + "lineno": 114, + "name": "reserved" + }, + { + "lineno": 119, + "name": "slot" + }, + { + "lineno": 146, + "name": "map" + }, + { + "lineno": 151, + "name": "handler" + }, + { + "lineno": 151, + "name": "collector" + }, + { + "lineno": 173, + "name": "io_context" + }, + { + "lineno": 174, + "name": "clock" + }, + { + "lineno": 175, + "name": "journal" + }, + { + "lineno": 176, + "name": "config" + }, + { + "lineno": 177, + "name": "collector" + } + ], + "classes": [ + { + "args": [ + "boost::asio::io_context& io_context", + "clock_type& clock", + "beast::Journal journal", + "BasicConfig const& config", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 13, + "name": "ManagerImp" + }, + { + "args": [ + "Handler const& handler", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 149, + "name": "Stats" + } + ], + "code_paths": [ + { + "call_chain": [ + "ManagerImp::setConfig", + "m_logic.config(config)" + ], + "entry_point": "setConfig", + "purpose": "Sets and validates the PeerFinder configuration.", + "validation_points": [ + "m_logic.config(config)" + ] + }, + { + "call_chain": [ + "ManagerImp::addFixedPeer", + "m_logic.addFixedPeer(name, addresses)" + ], + "entry_point": "addFixedPeer", + "purpose": "Adds a fixed peer (static peer) to the PeerFinder, validating name and addresses.", + "validation_points": [ + "m_logic.addFixedPeer(name, addresses)" + ] + }, + { + "call_chain": [ + "ManagerImp::addFallbackStrings", + "SourceStrings::New(name, strings)", + "m_logic.addStaticSource(...)" + ], + "entry_point": "addFallbackStrings", + "purpose": "Adds fallback peer sources from string lists, validating name and strings.", + "validation_points": [ + "SourceStrings::New(name, strings)" + ] + }, + { + "call_chain": [ + "ManagerImp::new_inbound_slot", + "m_logic.new_inbound_slot(local_endpoint, remote_endpoint)" + ], + "entry_point": "new_inbound_slot", + "purpose": "Creates a new inbound peer slot, validating local and remote endpoints.", + "validation_points": [ + "m_logic.new_inbound_slot(local_endpoint, remote_endpoint)" + ] + } + ], + "data_flows": [ + { + "field": "Config object", + "flow": [ + "setConfig(config)", + "m_logic.config(config)", + "Logic::config(config) (validation and storage)" + ], + "origin": "Passed to setConfig (external input)", + "transformations": [ + "Config object may be checked for validity, normalized, or have defaults applied" + ], + "validated_at": "m_logic.config(config)" + }, + { + "field": "Fixed peer name and addresses", + "flow": [ + "addFixedPeer(name, addresses)", + "m_logic.addFixedPeer(name, addresses)", + "Logic::addFixedPeer(name, addresses) (validation and storage)" + ], + "origin": "Passed to addFixedPeer (external input)", + "transformations": [ + "Addresses may be parsed, checked for duplicates, or validated for format" + ], + "validated_at": "m_logic.addFixedPeer(name, addresses)" + }, + { + "field": "Fallback source name and strings", + "flow": [ + "addFallbackStrings(name, strings)", + "SourceStrings::New(name, strings) (validation)", + "m_logic.addStaticSource(...)" + ], + "origin": "Passed to addFallbackStrings (external input)", + "transformations": [ + "Strings parsed into endpoints, checked for validity" + ], + "validated_at": "SourceStrings::New(name, strings)" + }, + { + "field": "local_endpoint, remote_endpoint", + "flow": [ + "new_inbound_slot(local_endpoint, remote_endpoint)", + "m_logic.new_inbound_slot(local_endpoint, remote_endpoint)", + "Logic::new_inbound_slot(local_endpoint, remote_endpoint) (validation and slot creation)" + ], + "origin": "Passed to new_inbound_slot (external input, e.g. from network accept)", + "transformations": [ + "Endpoints checked for validity, uniqueness, and possibly blacklisting" + ], + "validated_at": "m_logic.new_inbound_slot(local_endpoint, remote_endpoint)" + } + ], + "description": "Implements the PeerFinder Manager for XRPL, managing peer connections, slots, and related logic, including metrics collection and configuration.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "Config object", + "empty", + "string", + "validation" + ], + "evidence": "m_logic.config(config) at setConfig", + "issue_pattern": "Missing empty string validation for Config object", + "why_false_positive": "m_logic.config(config) validates Config object for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "name, addresses (for fixed peers)", + "empty", + "string", + "validation" + ], + "evidence": "m_logic.addFixedPeer(name, addresses) at addFixedPeer", + "issue_pattern": "Missing empty string validation for name, addresses (for fixed peers)", + "why_false_positive": "m_logic.addFixedPeer(name, addresses) validates name, addresses (for fixed peers) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "name, strings (for fallback strings)", + "empty", + "string", + "validation" + ], + "evidence": "SourceStrings::New(name, strings) and m_logic.addStaticSource at addFallbackStrings", + "issue_pattern": "Missing empty string validation for name, strings (for fallback strings)", + "why_false_positive": "SourceStrings::New(name, strings) and m_logic.addStaticSource validates name, strings (for fallback strings) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "local_endpoint, remote_endpoint", + "empty", + "string", + "validation" + ], + "evidence": "m_logic.new_inbound_slot(local_endpoint, remote_endpoint) at new_inbound_slot", + "issue_pattern": "Missing empty string validation for local_endpoint, remote_endpoint", + "why_false_positive": "m_logic.new_inbound_slot(local_endpoint, remote_endpoint) validates local_endpoint, remote_endpoint for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/PeerfinderManager.cpp", + "functions": [ + { + "args": [ + "boost::asio::io_context& io_context", + "clock_type& clock", + "beast::Journal journal", + "BasicConfig const& config", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 18, + "name": "ManagerImp" + }, + { + "args": [], + "lineno": 32, + "name": "~ManagerImp" + }, + { + "args": [], + "lineno": 36, + "name": "stop" + }, + { + "args": [ + "Config const& config" + ], + "lineno": 52, + "name": "setConfig" + }, + { + "args": [], + "lineno": 56, + "name": "config" + }, + { + "args": [ + "std::string const& name", + "std::vector const& addresses" + ], + "lineno": 60, + "name": "addFixedPeer" + }, + { + "args": [ + "std::string const& name", + "std::vector const& strings" + ], + "lineno": 65, + "name": "addFallbackStrings" + }, + { + "args": [ + "std::string const& name", + "std::string const& url" + ], + "lineno": 69, + "name": "addFallbackURL" + }, + { + "args": [ + "beast::IP::Endpoint const& local_endpoint", + "beast::IP::Endpoint const& remote_endpoint" + ], + "lineno": 75, + "name": "new_inbound_slot" + }, + { + "args": [ + "beast::IP::Endpoint const& remote_endpoint" + ], + "lineno": 81, + "name": "new_outbound_slot" + }, + { + "args": [ + "std::shared_ptr const& slot", + "Endpoints const& endpoints" + ], + "lineno": 86, + "name": "on_endpoints" + }, + { + "args": [ + "std::shared_ptr const& slot" + ], + "lineno": 91, + "name": "on_closed" + }, + { + "args": [ + "std::shared_ptr const& slot" + ], + "lineno": 96, + "name": "on_failure" + }, + { + "args": [ + "boost::asio::ip::tcp::endpoint const& remote_address", + "std::vector const& eps" + ], + "lineno": 101, + "name": "onRedirects" + }, + { + "args": [ + "std::shared_ptr const& slot", + "beast::IP::Endpoint const& local_endpoint" + ], + "lineno": 108, + "name": "onConnected" + }, + { + "args": [ + "std::shared_ptr const& slot", + "PublicKey const& key", + "bool reserved" + ], + "lineno": 113, + "name": "activate" + }, + { + "args": [ + "std::shared_ptr const& slot" + ], + "lineno": 118, + "name": "redirect" + }, + { + "args": [], + "lineno": 123, + "name": "autoconnect" + }, + { + "args": [], + "lineno": 128, + "name": "once_per_second" + }, + { + "args": [], + "lineno": 133, + "name": "buildEndpointsForPeers" + }, + { + "args": [], + "lineno": 138, + "name": "start" + }, + { + "args": [ + "beast::PropertyStream::Map& map" + ], + "lineno": 145, + "name": "onWrite" + }, + { + "args": [], + "lineno": 159, + "name": "collect_metrics" + }, + { + "args": [], + "lineno": 167, + "name": "Manager" + }, + { + "args": [ + "boost::asio::io_context& io_context", + "clock_type& clock", + "beast::Journal journal", + "BasicConfig const& config", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 171, + "name": "make_Manager" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "PeerFinder" + } + ], + "test_coverage_notes": "PeerFinderManager and its logic are typically tested via integration and unit tests in the PeerFinder subsystem. Likely test files: PeerFinder_test.cpp, PeerFinderLogic_test.cpp, and possibly config/Config_test.cpp. Tests should cover config validation, fixed peer addition, fallback source parsing, and slot creation. However, addFallbackURL is not implemented and thus not tested. Some validation logic may be tested only indirectly via higher-level PeerFinder tests. There may be gaps in negative testing (invalid input cases) and in direct unit tests for validation branches inside Logic and SourceStrings.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (Logic, SourceStrings, StoreSqdb, Checker)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.7, + "error_thrown": "unknown (depends on Logic::config implementation)", + "field": "Config object", + "location": "setConfig", + "validated_by": "m_logic.config(config)", + "validates": [ + "structure", + "values", + "business rules of Config" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "unknown (depends on Logic::addFixedPeer implementation)", + "field": "name, addresses (for fixed peers)", + "location": "addFixedPeer", + "validated_by": "m_logic.addFixedPeer(name, addresses)", + "validates": [ + "name non-empty", + "addresses valid endpoints" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "unknown (depends on SourceStrings::New implementation)", + "field": "name, strings (for fallback strings)", + "location": "addFallbackStrings", + "validated_by": "SourceStrings::New(name, strings) and m_logic.addStaticSource", + "validates": [ + "name non-empty", + "strings valid peer addresses" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "unknown (depends on Logic::new_inbound_slot implementation)", + "field": "local_endpoint, remote_endpoint", + "location": "new_inbound_slot", + "validated_by": "m_logic.new_inbound_slot(local_endpoint, remote_endpoint)", + "validates": [ + "endpoints are valid", + "not blacklisted", + "not duplicate" + ], + "validation_type": "format|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/PeerfinderManager.cpp.ai.md b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp.ai.md new file mode 100644 index 0000000000..7f1e4d7a1e --- /dev/null +++ b/src/xrpld/peerfinder/detail/PeerfinderManager.cpp.ai.md @@ -0,0 +1,39 @@ +# `PeerfinderManager.cpp` — PeerFinder Manager Implementation + +## Role in the System + +This file is the assembly point for the XRPL peer-discovery subsystem. It defines `ManagerImp`, the concrete implementation of the abstract `Manager` interface declared in `PeerfinderManager.h`. Rather than exposing the implementation type to callers, the file provides a single factory function `make_Manager()` that returns a `std::unique_ptr`. This keeps the entire implementation — including its heavyweight dependencies — hidden behind the public interface, a deliberate PIMPL-style isolation that allows the subsystem to evolve without touching call sites. + +## What `ManagerImp` Assembles + +`ManagerImp` does not contain significant algorithmic logic itself; its job is composition. It holds and wires together five major collaborators: + +- **`StoreSqdb m_store`** — SQLite-backed persistence (schema version 4) that saves and loads the bootstrap peer cache across process restarts. It is opened lazily inside `start()` so that no disk I/O occurs during construction. +- **`Checker checker_`** — An async TCP socket prober that verifies whether a candidate peer's listening port is actually reachable. It is constructed with the shared `io_context` so its async operations participate in the same event loop as the rest of the node. +- **`Logic m_logic`** — The templated core of peer-finding: it manages the live and boot caches, slot lifecycle, connection counts, endpoint handout strategy, and all policy decisions. Nearly every `Manager` virtual method in `ManagerImp` is a one-liner that forwards to `m_logic`. +- **`boost::asio::executor_work_guard` (via `work_`)** — Stored as `std::optional` and constructed `std::in_place` during initialization to keep the `io_context` running even when the queue is transiently empty. Destroying it (by calling `work_.reset()`) is the signal to let the context drain after queued handlers complete. This is the canonical Asio shutdown pattern. +- **`Stats` (nested private struct)** — Registers a metrics hook with a `beast::insight::Collector`, exposing two gauges: `Active_Inbound_Peers` and `Active_Outbound_Peers`. The `collect_metrics()` callback reads live counts directly out of `m_logic.counts_`, protected by `m_statsMutex` to guard against concurrent metric collection. + +## Lifecycle: Start and Stop + +`start()` performs two ordered operations: it opens the SQLite store (which runs schema migration if the on-disk version is outdated) and then calls `m_logic.load()` to populate the in-memory bootcache from persisted entries. Nothing is written to disk before `start()` is called, making construction cheap and safe. + +`stop()` is idempotent by design. It checks `if (work_)` before acting, so calling it twice — including from the destructor — is harmless. The sequence on shutdown is deliberate: reset the work guard first (unblock the io_context), then `checker_.stop()` (cancel all pending async socket probes), then `m_logic.stop()` (cancel any in-flight source fetch). This ordering matters because `Logic` may hold shared references to `Checker` operations that must be cancelled before the checker itself destructs. + +The destructor simply delegates to `stop()`, so `ManagerImp` is safe to destroy at any point after construction without requiring an explicit shutdown call. + +## Slot Type Coercion + +The public API accepts `std::shared_ptr`, but `Logic` operates on the concrete `SlotImp` type. Every slot-event method (`on_endpoints`, `on_closed`, `on_failure`, `onConnected`, `activate`, `redirect`) performs a `std::dynamic_pointer_cast` before forwarding. This reflects a deliberate layering boundary: callers interact through the opaque `Slot` interface while the internal machinery requires the full `SlotImp`. The cast is safe as long as all `Slot` instances in circulation were created by `m_logic` (which they are — `new_inbound_slot` and `new_outbound_slot` both return `shared_ptr` wrapped as `shared_ptr`). + +## The Unimplemented Method + +`addFallbackURL()` exists in `ManagerImp` but its body contains only a comment: `// VFALCO TODO This needs to be implemented`. The corresponding pure-virtual declaration is also commented out of the `Manager` base class header with an explicit note that it is unimplemented. Fallback sources are currently only supplied as static string lists via `addFallbackStrings()`, which wraps the strings in a `SourceStrings` object and registers it as a static source on the logic layer. + +## Metrics Design + +The `Stats` struct uses `beast::insight::Collector` hooks: a single `hook` object whose callback is `collect_metrics()`, plus two `Gauge` objects. The hook mechanism means the collector drives collection — it calls the registered handler when it wants fresh data rather than the application pushing values on every change. The mutex around `collect_metrics()` is therefore protecting against concurrent calls from the collector framework, not from peer connection events. + +## Dependency and Interface Summary + +`ManagerImp` is completely opaque to its callers — the class definition never appears in any header. The only externally visible symbol beyond `Manager` itself is the `make_Manager` factory. This enforces that all peerfinder consumer code programs to the `Manager` interface, keeping compile-time dependencies minimal and test substitution straightforward. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/SlotImp.cpp.ai.json b/src/xrpld/peerfinder/detail/SlotImp.cpp.ai.json new file mode 100644 index 0000000000..67fb0df8d2 --- /dev/null +++ b/src/xrpld/peerfinder/detail/SlotImp.cpp.ai.json @@ -0,0 +1,461 @@ +{ + "args": [ + { + "lineno": 8, + "name": "local_endpoint" + }, + { + "lineno": 8, + "name": "remote_endpoint" + }, + { + "lineno": 9, + "name": "fixed" + }, + { + "lineno": 10, + "name": "clock" + }, + { + "lineno": 37, + "name": "state_" + }, + { + "lineno": 63, + "name": "now" + }, + { + "lineno": 80, + "name": "ep" + }, + { + "lineno": 80, + "name": "hops" + } + ], + "classes": [ + { + "args": [ + "beast::IP::Endpoint const& local_endpoint", + "beast::IP::Endpoint const& remote_endpoint", + "bool fixed", + "clock_type& clock" + ], + "lineno": 7, + "name": "SlotImp" + }, + { + "args": [ + "clock_type& clock" + ], + "lineno": 76, + "name": "recent_t" + } + ], + "code_paths": [ + { + "call_chain": [ + "SlotImp::state" + ], + "entry_point": "SlotImp::state", + "purpose": "Transitions the slot to a new state, with multiple validation checks to ensure only legal state transitions.", + "validation_points": [ + "XRPL_ASSERT(state_ != active, ...)", + "XRPL_ASSERT(state_ != m_state, ...)", + "XRPL_ASSERT(state_ != accept && state_ != connect, ...)", + "XRPL_ASSERT(state_ != connected || (!m_inbound && m_state == connect), ...)", + "XRPL_ASSERT(state_ != closing || m_state != connect, ...)" + ] + }, + { + "call_chain": [ + "SlotImp::activate" + ], + "entry_point": "SlotImp::activate", + "purpose": "Activates a slot, transitioning it to the 'active' state, with validation to ensure only valid prior states.", + "validation_points": [ + "XRPL_ASSERT(m_state == accept || m_state == connected, ...)" + ] + }, + { + "call_chain": [ + "SlotImp::recent_t::insert" + ], + "entry_point": "SlotImp::recent_t::insert", + "purpose": "Inserts or updates an endpoint in the recent cache, ensuring hop count is minimized.", + "validation_points": [] + }, + { + "call_chain": [ + "SlotImp::recent_t::filter" + ], + "entry_point": "SlotImp::recent_t::filter", + "purpose": "Checks if an endpoint should be filtered based on recent cache and hop count.", + "validation_points": [] + }, + { + "call_chain": [ + "SlotImp::recent_t::expire" + ], + "entry_point": "SlotImp::recent_t::expire", + "purpose": "Expires old entries from the recent cache.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "m_state", + "flow": [ + "constructor sets m_state", + "SlotImp::state validates and sets m_state", + "SlotImp::activate validates and sets m_state to 'active'" + ], + "origin": "SlotImp constructor (initialized to 'accept' or 'connect')", + "transformations": [ + "Set to initial state in constructor", + "Validated and possibly changed in state()", + "Validated and set to 'active' in activate()" + ], + "validated_at": "SlotImp::state, SlotImp::activate" + }, + { + "field": "state_ (input to SlotImp::state)", + "flow": [ + "Caller provides state_", + "SlotImp::state validates state_", + "If valid, assigns to m_state" + ], + "origin": "Caller of SlotImp::state", + "transformations": [ + "Checked for illegal transitions", + "Assigned to m_state if valid" + ], + "validated_at": "SlotImp::state" + }, + { + "field": "m_inbound", + "flow": [ + "constructor sets m_inbound", + "used in SlotImp::state for validation of connected state transition" + ], + "origin": "SlotImp constructor (true/false based on which constructor)", + "transformations": [ + "Set at construction", + "Read-only in validation" + ], + "validated_at": "SlotImp::state" + }, + { + "field": "recent (SlotImp::recent_t)", + "flow": [ + "constructor initializes recent", + "recent_t::insert/expire/filter manipulate recent.cache" + ], + "origin": "SlotImp constructor (initialized with clock)", + "transformations": [ + "insert: adds/updates endpoint with hop count", + "filter: checks endpoint/hop count", + "expire: removes old entries" + ], + "validated_at": "No explicit validation" + }, + { + "field": "whenAcceptEndpoints", + "flow": [ + "activate() sets whenAcceptEndpoints = now" + ], + "origin": "Not set in constructor; set in activate()", + "transformations": [ + "Set to current time when slot becomes active" + ], + "validated_at": "Indirectly, via activate()'s state validation" + } + ], + "description": "Implements the SlotImp class for managing peer connection slots in the PeerFinder subsystem, including state transitions, activation, and recent endpoint tracking.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "state_ (input state)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SlotImp::state", + "issue_pattern": "Missing empty string validation for state_ (input state)", + "why_false_positive": "XRPL_ASSERT validates state_ (input state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "state_ (input state)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SlotImp::state", + "issue_pattern": "Missing empty string validation for state_ (input state)", + "why_false_positive": "XRPL_ASSERT validates state_ (input state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "state_ (input state)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SlotImp::state", + "issue_pattern": "Missing empty string validation for state_ (input state)", + "why_false_positive": "XRPL_ASSERT validates state_ (input state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "state_ (input state)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SlotImp::state", + "issue_pattern": "Missing empty string validation for state_ (input state)", + "why_false_positive": "XRPL_ASSERT validates state_ (input state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "state_ (input state)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SlotImp::state", + "issue_pattern": "Missing empty string validation for state_ (input state)", + "why_false_positive": "XRPL_ASSERT validates state_ (input state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_state (current state)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at SlotImp::activate", + "issue_pattern": "Missing empty string validation for m_state (current state)", + "why_false_positive": "XRPL_ASSERT validates m_state (current state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "hops (input parameter)", + "empty", + "string", + "validation" + ], + "evidence": "manual comparison at SlotImp::recent_t::insert", + "issue_pattern": "Missing empty string validation for hops (input parameter)", + "why_false_positive": "manual comparison validates hops (input parameter) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/SlotImp.cpp", + "functions": [ + { + "args": [ + "beast::IP::Endpoint const& local_endpoint", + "beast::IP::Endpoint const& remote_endpoint", + "bool fixed", + "clock_type& clock" + ], + "lineno": 7, + "name": "SlotImp" + }, + { + "args": [ + "beast::IP::Endpoint const& remote_endpoint", + "bool fixed", + "clock_type& clock" + ], + "lineno": 23, + "name": "SlotImp" + }, + { + "args": [ + "State state_" + ], + "lineno": 36, + "name": "state" + }, + { + "args": [ + "clock_type::time_point const& now" + ], + "lineno": 62, + "name": "activate" + }, + { + "args": [], + "lineno": 73, + "name": "~Slot" + }, + { + "args": [ + "clock_type& clock" + ], + "lineno": 76, + "name": "recent_t" + }, + { + "args": [ + "beast::IP::Endpoint const& ep", + "std::uint32_t hops" + ], + "lineno": 79, + "name": "insert" + }, + { + "args": [ + "beast::IP::Endpoint const& ep", + "std::uint32_t hops" + ], + "lineno": 90, + "name": "filter" + }, + { + "args": [], + "lineno": 101, + "name": "expire" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "PeerFinder" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, validation logic in SlotImp::state and SlotImp::activate would be tested in PeerFinder or Slot-related unit tests, possibly in files like test_peerfinder.cpp or test_Slot.cpp. However, unless these tests explicitly check for illegal state transitions and assertion failures, coverage may be incomplete. The recent_t cache logic (insert/filter/expire) should also be tested for correct endpoint/hop count handling, but again, no direct test references are present. Gaps may exist in testing all assertion branches, especially for negative/invalid transitions.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), manual logic", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely abort or exception, depending on XRPL_ASSERT implementation)", + "field": "state_ (input state)", + "location": "SlotImp::state", + "validated_by": "XRPL_ASSERT", + "validates": [ + "state_ is not 'active' (must not set state directly to active)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure", + "field": "state_ (input state)", + "location": "SlotImp::state", + "validated_by": "XRPL_ASSERT", + "validates": [ + "state_ is not equal to current m_state (must be a state transition)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure", + "field": "state_ (input state)", + "location": "SlotImp::state", + "validated_by": "XRPL_ASSERT", + "validates": [ + "state_ is not an initial state (accept or connect)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure", + "field": "state_ (input state)", + "location": "SlotImp::state", + "validated_by": "XRPL_ASSERT", + "validates": [ + "If state_ is 'connected', then m_inbound must be false and m_state must be 'connect' (only outbound connect can become connected)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure", + "field": "state_ (input state)", + "location": "SlotImp::state", + "validated_by": "XRPL_ASSERT", + "validates": [ + "If state_ is 'closing', m_state must not be 'connect' (can't gracefully close on outbound connect attempt)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Assertion failure", + "field": "m_state (current state)", + "location": "SlotImp::activate", + "validated_by": "XRPL_ASSERT", + "validates": [ + "m_state must be 'accept' or 'connected' before activating" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "None (logic branch, not an error)", + "field": "hops (input parameter)", + "location": "SlotImp::recent_t::insert", + "validated_by": "manual comparison", + "validates": [ + "If endpoint already exists, only update hops if new hops <= existing hops" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/SlotImp.cpp.ai.md b/src/xrpld/peerfinder/detail/SlotImp.cpp.ai.md new file mode 100644 index 0000000000..501c2cefab --- /dev/null +++ b/src/xrpld/peerfinder/detail/SlotImp.cpp.ai.md @@ -0,0 +1,47 @@ +# `SlotImp.cpp` — Peer Connection Slot Implementation + +`SlotImp` is the concrete implementation of the abstract `Slot` interface defined in `Slot.h`. Within the PeerFinder subsystem, a "slot" represents a single peer-to-peer overlay connection and carries all metadata the connection manager needs to reason about it: direction (inbound vs. outbound), current lifecycle state, remote and local endpoints, the peer's public key (once the handshake completes), and a cache of recently observed endpoints. `SlotImp.cpp` implements the two pieces of `Slot` that require non-trivial logic — the state machine and the recent-address tracker — while the header provides all the trivial accessors inline. + +## Two Constructors, Two Directions + +The class has dual constructors that reflect an architectural reality: inbound and outbound connections carry meaningfully different initial conditions. + +The **inbound constructor** takes both `local_endpoint` and `remote_endpoint` and initializes `m_state` to `accept`. Because a freshly accepted socket has not yet been interrogated, `checked` and `canAccept` are both false — the PeerFinder does not yet know whether the remote address is publicly reachable. + +The **outbound constructor** omits `local_endpoint` (it's unknown until the OS assigns a port) and initializes `m_state` to `connect`. Crucially, it sets `checked = true` and `canAccept = true`. The reasoning is straightforward: the act of successfully connecting to a remote address proves it is reachable, so no further connectivity check is needed. + +`m_inbound` and `m_fixed` are `bool const` — set at construction and never changed. This is intentional: the direction of a connection is an immutable fact, and making it `const` lets the compiler and human readers alike trust that no code path reassigns it. + +## State Machine and Its Invariants + +`Slot::State` is an enum with five values: `accept`, `connect`, `connected`, `active`, and `closing`. The transitions are enforced by `state()` and `activate()`, both armed with `XRPL_ASSERT` guards. + +The design decision to split state mutation into two methods — `state()` and `activate()` — is deliberate. `activate()` is the **sole path** to the `active` state and simultaneously records `whenAcceptEndpoints = now`. Combining these two side effects in a single method ensures the endpoint-spam throttle timestamp is always set when a slot goes live; calling `state(active)` directly would skip that assignment. The first `XRPL_ASSERT` inside `state()` enforces this: passing `active` there is a programming error. + +Within `state()`, four additional assertions encode the peer state machine topology: + +- No transition into initial states (`accept`, `connect`) — those are entry points only, set by the constructor. +- `connected` is only reachable from outbound slots (`!m_inbound`) currently in the `connect` state. Inbound slots jump straight from `accept` to `active` via `activate()` without passing through `connected`, because inbound acceptance does not go through the TCP connect phase. +- `closing` is forbidden while still in the `connect` state, because an in-progress outbound attempt that hasn't completed yet should be aborted, not gracefully closed. + +These asserts are compile-time-silent but will abort at runtime in debug builds the moment a caller attempts an illegal transition — a hard fail rather than silent data corruption. + +## `recent_t` — Per-Slot Endpoint Deduplication + +The inner class `recent_t` implements a bounded, time-decaying cache of `(IP::Endpoint, hops)` pairs. Its purpose is to prevent redundant endpoint gossip: the PeerFinder should not send a peer an address it already knows about at an equal or closer hop distance. + +The cache is a `beast::aged_unordered_map`, which is a hash map that also maintains insertion/access order for LRU-style expiry. Both received endpoints (what the peer told us) and forwarded endpoints (what we sent to the peer) are recorded here. + +`insert()` emplace-inserts a new entry. If the endpoint is already cached, it updates the stored hop count only when the new value is less than or equal to the existing value, and `touch()`es the entry to reset its age. The `<=` inequality is called out in a comment as significant to other logic. + +`filter()` returns `true` (meaning "suppress this send") when the cached hop count for the endpoint is less than or equal to the hop count we're about to send. This matches `insert()`'s semantics: if we received an endpoint at hop 2, we filter any outbound announcement at hop 2 or higher — because the peer clearly already has better knowledge. But if we want to announce it at hop 1, we allow it through. + +`expire()` calls `beast::expire(cache, Tuning::liveCacheSecondsToLive)`, which prunes all entries older than 30 seconds. This TTL aligns with the endpoint broadcast cadence: `mtENDPOINTS` messages are sent roughly every 151 seconds (`Tuning::secondsPerMessage`), but endpoint freshness decays faster so that a peer that drops off and reconnects will re-receive address updates correctly. + +## `m_listening_port` and Thread Safety + +The listening port is stored as `std::atomic` with a sentinel value of `-1` (`unknownPort`). The atomic wrapper exists because port discovery — triggered when the remote peer's handshake reveals which port it listens on — may race against reads from the connection management thread. The `listening_port()` accessor translates the atomic integer back to `std::optional`, returning `nullopt` for the sentinel. + +## Relationship to PeerFinder Logic + +`SlotImp` is a pure data/state object; it contains no scheduling, I/O, or networking code. It is consumed by the `Logic` class, which holds `shared_ptr` instances keyed by remote endpoint and drives all state transitions. The `recent_t` cache is populated during endpoint gossip processing and consulted during endpoint broadcasting, giving the Logic a compact per-connection memory of what each peer already knows — without requiring cross-slot coordination. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/SlotImp.h.ai.json b/src/xrpld/peerfinder/detail/SlotImp.h.ai.json new file mode 100644 index 0000000000..a8386b8ca6 --- /dev/null +++ b/src/xrpld/peerfinder/detail/SlotImp.h.ai.json @@ -0,0 +1,211 @@ +{ + "args": [ + { + "lineno": 15, + "name": "local_endpoint" + }, + { + "lineno": 16, + "name": "remote_endpoint" + }, + { + "lineno": 17, + "name": "fixed" + }, + { + "lineno": 18, + "name": "clock" + }, + { + "lineno": 21, + "name": "remote_endpoint" + }, + { + "lineno": 21, + "name": "fixed" + }, + { + "lineno": 21, + "name": "clock" + }, + { + "lineno": 70, + "name": "port" + }, + { + "lineno": 74, + "name": "endpoint" + }, + { + "lineno": 78, + "name": "endpoint" + }, + { + "lineno": 82, + "name": "key" + }, + { + "lineno": 86, + "name": "reserved_" + }, + { + "lineno": 94, + "name": "state_" + }, + { + "lineno": 96, + "name": "now" + }, + { + "lineno": 105, + "name": "clock" + }, + { + "lineno": 110, + "name": "ep" + }, + { + "lineno": 110, + "name": "hops" + }, + { + "lineno": 115, + "name": "ep" + }, + { + "lineno": 115, + "name": "hops" + } + ], + "classes": [ + { + "args": [ + "beast::IP::Endpoint const& local_endpoint, beast::IP::Endpoint const& remote_endpoint, bool fixed, clock_type& clock", + "beast::IP::Endpoint const& remote_endpoint, bool fixed, clock_type& clock" + ], + "lineno": 11, + "name": "SlotImp" + }, + { + "args": [ + "clock_type& clock" + ], + "lineno": 104, + "name": "recent_t" + } + ], + "description": "Defines the SlotImp class, an implementation of the Slot interface for managing peer connection slots in the XRPL PeerFinder subsystem, including metadata, state, and recent address tracking.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/SlotImp.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "inbound" + }, + { + "args": [], + "lineno": 27, + "name": "fixed" + }, + { + "args": [], + "lineno": 32, + "name": "reserved" + }, + { + "args": [], + "lineno": 37, + "name": "state" + }, + { + "args": [], + "lineno": 42, + "name": "remote_endpoint" + }, + { + "args": [], + "lineno": 47, + "name": "local_endpoint" + }, + { + "args": [], + "lineno": 52, + "name": "public_key" + }, + { + "args": [], + "lineno": 57, + "name": "prefix" + }, + { + "args": [], + "lineno": 62, + "name": "listening_port" + }, + { + "args": [ + "port" + ], + "lineno": 70, + "name": "set_listening_port" + }, + { + "args": [ + "endpoint" + ], + "lineno": 74, + "name": "local_endpoint" + }, + { + "args": [ + "endpoint" + ], + "lineno": 78, + "name": "remote_endpoint" + }, + { + "args": [ + "key" + ], + "lineno": 82, + "name": "public_key" + }, + { + "args": [ + "reserved_" + ], + "lineno": 86, + "name": "reserved" + }, + { + "args": [ + "state_" + ], + "lineno": 94, + "name": "state" + }, + { + "args": [ + "now" + ], + "lineno": 96, + "name": "activate" + }, + { + "args": [], + "lineno": 120, + "name": "expire" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/SlotImp.h.ai.md b/src/xrpld/peerfinder/detail/SlotImp.h.ai.md new file mode 100644 index 0000000000..29685159c2 --- /dev/null +++ b/src/xrpld/peerfinder/detail/SlotImp.h.ai.md @@ -0,0 +1,45 @@ +# `SlotImp.h` — Concrete Peer Connection Slot + +`SlotImp` is the concrete implementation of the abstract `Slot` interface in the `xrpl::PeerFinder` subsystem. Where `Slot` defines the read-only query surface exposed to the rest of the codebase, `SlotImp` carries the mutable state, the state-machine logic, and the per-peer bookkeeping that the `Logic` class relies on to manage the node's peer topology. + +## Role in PeerFinder + +`Logic` maintains a `std::map>` as its primary slot registry. When a new TCP connection is accepted or an outbound attempt is initiated, `Logic::new_inbound_slot()` or `Logic::new_outbound_slot()` constructs a `SlotImp` and inserts it there. All subsequent lifecycle calls — `onConnected`, `activate`, `on_endpoints`, `on_closed` — take a `SlotImp::ptr` and mutate it directly. `SlotImp::ptr` is simply `std::shared_ptr`, which allows `Logic` to pass slots around internally while the `Manager` API exposes the weaker `std::shared_ptr` to callers outside the detail namespace. + +## Construction and Direction + +Two constructors model the two fundamentally different connection origins: + +- The **inbound** constructor takes both local and remote endpoints, sets `m_inbound = true`, and initialises state to `accept`. Both `checked` and `canAccept` start `false` because reachability of the peer's advertised address hasn't been verified yet. +- The **outbound** constructor takes only the remote endpoint, sets `m_inbound = false`, and initialises state to `connect`. Critically, `checked` and `canAccept` are set `true` immediately — a successful outbound TCP connection is itself proof of reachability, so no separate connectivity probe is needed. + +`m_fixed` and `m_inbound` are `bool const`, cementing the connection's origin permanently. The `fixed` flag marks connections to explicitly configured peers (cluster members, validator peers) that should be maintained unconditionally. + +## State Machine + +The `Slot::State` enum defines five states: `accept → active` (inbound path) and `connect → connected → active` (outbound path), with `closing` reachable from most intermediate states. The `state(State)` setter enforces the invariants via `XRPL_ASSERT`: + +- `active` can only be reached through `activate()`, never through the generic setter. +- You cannot transition back to initial states (`accept`, `connect`). +- `connected` is only valid for an outbound slot currently in `connect`. +- `closing` cannot be set on a slot still in `connect` — you cannot gracefully close what hasn't connected yet. + +`activate()` handles the single promotion to `active`, and it also stamps `whenAcceptEndpoints = now`. This timestamp gates how soon the node will accept the next `mtENDPOINTS` message from this peer — a flood-control mechanism against endpoint spam. + +## Listening Port: Atomic with Sentinel + +`m_listening_port` is declared `std::atomic` with a sentinel value `unknownPort = -1`. The `listening_port()` getter returns `std::nullopt` if the value is still `-1`, otherwise casts to `std::uint16_t` for the result. Using an `int32_t` rather than `uint16_t` avoids the unsigned representation ambiguity for "not yet known" while remaining lock-free. The port is set after handshake when the remote peer advertises it, potentially from a different thread from whoever reads it. + +## `recent_t` — Per-Peer Address Deduplication + +The nested `recent_t` class solves a specific gossip protocol problem: when the node relays peer endpoint lists to its connections, it should avoid echoing addresses back to the peer that originally reported them, and avoid repeating addresses already relayed recently. `recent_t` wraps a `beast::aged_unordered_map` where the mapped value is the lowest hop count seen for that endpoint. + +`insert()` updates the cache when an endpoint arrives or is sent. If the endpoint is already present, it only updates the hop count (and refreshes the age) when the new hop count is **less than or equal** to the existing one — preserving the closest known distance. + +`filter()` returns `true` (meaning "suppress this address") when the peer has already seen the endpoint at an equal or smaller hop count. The `<=` condition is intentional and explicitly noted in both places: sending an endpoint at a *higher* hop count than the peer already knows is useful, but sending it at the same or shorter distance is not. + +`expire()` is called periodically via `SlotImp::expire()` and trims entries older than `Tuning::liveCacheSecondsToLive` (30 seconds), matching the live-cache TTL so the per-slot filter stays synchronised with the broader endpoint cache lifecycle. + +## Deprecated Public Members + +`checked`, `canAccept`, `connectivityCheckInProgress`, and `whenAcceptEndpoints` are marked DEPRECATED but remain as raw public data members. They predate a planned refactor of the connectivity-check mechanism and are still read and written directly by `Logic`. Their persistence is a pragmatic concession — removing them requires restructuring the checker workflow, which is tracked separately from the core slot abstraction. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Source.h.ai.json b/src/xrpld/peerfinder/detail/Source.h.ai.json new file mode 100644 index 0000000000..6433245094 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Source.h.ai.json @@ -0,0 +1,53 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 15, + "name": "Source" + }, + { + "args": [], + "lineno": 19, + "name": "Results" + } + ], + "description": "Defines the abstract Source class for providing peer addresses in the XRPL PeerFinder module, used for bootstrapping or fallback when local peer caches are unavailable.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Source.h", + "functions": [ + { + "args": [], + "lineno": 32, + "name": "~Source" + }, + { + "args": [], + "lineno": 34, + "name": "name" + }, + { + "args": [], + "lineno": 36, + "name": "cancel" + }, + { + "args": [ + "results", + "journal" + ], + "lineno": 39, + "name": "fetch" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Source.h.ai.md b/src/xrpld/peerfinder/detail/Source.h.ai.md new file mode 100644 index 0000000000..4b8cccc81a --- /dev/null +++ b/src/xrpld/peerfinder/detail/Source.h.ai.md @@ -0,0 +1,27 @@ +# `Source.h` — Abstract Peer Address Provider + +## Role in the System + +`Source.h` defines the `Source` abstract base class within the `xrpl::PeerFinder` namespace. It sits at the heart of the PeerFinder bootstrapping strategy: when a node starts cold (no local peer cache) or exhausts all known-working addresses, it must find new peers from somewhere. `Source` is the uniform interface behind every mechanism that can supply a list of IP addresses for that purpose. + +The comment in the header names the intended implementations: static addresses from the config file, addresses from a local file on disk, peer lists fetched from a remote HTTPS URL, or DNS-based peer discovery. The only shipped concrete subclass in this codebase is `SourceStrings`, which backs the `[ips]` and `[ips_fixed]` config-file stanzas. + +## Interface Design + +The class is deliberately minimal — three pure or defaulted virtual methods and one nested result type: + +- `name()` returns a diagnostic label used in log messages by `Logic::fetch()`. It carries no operational meaning beyond identifying the source in log output. +- `fetch(Results& results, beast::Journal journal)` is the single operation that matters: populate `results.addresses` with `beast::IP::Endpoint` values, or set `results.error` on failure. The `Results` struct is a value type (not a future or callback), reflecting that the fetch is **synchronous** — the `Logic` layer calls it while holding no lock, but documents this as a known concern (`// VFALCO NOTE The fetch is synchronous, not sure if that's a good thing`). +- `cancel()` has a default no-op implementation. It exists for sources that might eventually run asynchronously (e.g., an HTTP fetch that could be in-flight when the node stops). `Logic` tracks the currently executing `Source` in `fetchSource_` and calls `cancel()` on it during shutdown to give future async implementations a hook to abort early. Since all current implementations are synchronous and `cancel()` is a no-op, this is purely defensive future-proofing. + +## Lifecycle and Usage in `Logic` + +`Logic` maintains two separate collections: `m_sources` (sources polled periodically) and the immediate use via `addStaticSource()`, which calls `fetch()` directly at registration time. The `fetch()` wrapper in `Logic` records the current source in `fetchSource_` before calling through and clears it afterward, checking `stopping_` on both sides of the call — this is the cancellation rendezvous point. If `stopping_` is set between the two checks, `Logic` drops the results silently rather than inserting addresses into a shutting-down bootcache. + +Successful fetches pipe their `IPAddresses` vector into `Bootcache::insertStatic()`, seeding the bootstrap cache that the connection engine draws from when it has no live peers to connect to. + +## Design Tradeoffs + +The synchronous `fetch()` signature is the most notable constraint. It simplifies implementations (no strand/executor threading concerns, no callback lifetime management) but means a slow or unresponsive remote source can stall the bootstrap thread. The `cancel()` hook was clearly added with the intention of revisiting this — it provides the extension point needed to move to an async model without changing the interface, if a concrete async implementation is ever written. + +The `Results` struct uses `boost::system::error_code` rather than exceptions, keeping error propagation explicit and cheap for the common success case. `IPAddresses` is `std::vector`, the same type used throughout PeerFinder, so results flow directly into the bootcache without conversion. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/SourceStrings.cpp.ai.json b/src/xrpld/peerfinder/detail/SourceStrings.cpp.ai.json new file mode 100644 index 0000000000..a69d281320 --- /dev/null +++ b/src/xrpld/peerfinder/detail/SourceStrings.cpp.ai.json @@ -0,0 +1,262 @@ +{ + "args": [ + { + "lineno": 7, + "name": "name" + }, + { + "lineno": 7, + "name": "strings" + }, + { + "lineno": 17, + "name": "results" + }, + { + "lineno": 17, + "name": "journal" + } + ], + "classes": [ + { + "args": [ + "name", + "strings" + ], + "lineno": 6, + "name": "SourceStringsImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "SourceStrings::New", + "SourceStringsImp::SourceStringsImp", + "SourceStringsImp::fetch" + ], + "entry_point": "SourceStrings::New", + "purpose": "Creates a new SourceStringsImp instance and later fetches peer addresses from string list, validating them.", + "validation_points": [ + "SourceStringsImp::fetch: beast::IP::Endpoint::from_string (validates m_strings[i])", + "SourceStringsImp::fetch: is_unspecified (validates endpoint validity)" + ] + } + ], + "data_flows": [ + { + "field": "m_strings (peer address strings)", + "flow": [ + "SourceStrings::New (input)", + "SourceStringsImp::SourceStringsImp (constructor stores in m_strings)", + "SourceStringsImp::fetch (reads m_strings[i])", + "beast::IP::Endpoint::from_string (parses string to endpoint)", + "is_unspecified (checks endpoint validity)", + "results.addresses.push_back (stores valid endpoint)" + ], + "origin": "Passed as 'strings' argument to SourceStrings::New, then to SourceStringsImp::SourceStringsImp", + "transformations": [ + "String parsed to Endpoint object", + "Endpoint checked for 'unspecified' (invalid) status" + ], + "validated_at": "SourceStringsImp::fetch (via from_string and is_unspecified)" + }, + { + "field": "ep (beast::IP::Endpoint)", + "flow": [ + "from_string (parses m_strings[i] to ep)", + "is_unspecified (checks if ep is valid)", + "If valid, pushed to results.addresses" + ], + "origin": "Created from m_strings[i] via from_string", + "transformations": [ + "String to Endpoint conversion", + "Validity check" + ], + "validated_at": "Immediately after creation in fetch loop" + } + ], + "description": "Implements a source of peer addresses from a list of strings, providing methods to fetch and return valid endpoints for use in peer discovery.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "m_strings[i] (peer address string)", + "empty", + "string", + "validation" + ], + "evidence": "beast::IP::Endpoint::from_string at SourceStringsImp::fetch", + "issue_pattern": "Missing empty string validation for m_strings[i] (peer address string)", + "why_false_positive": "beast::IP::Endpoint::from_string validates m_strings[i] (peer address string) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "m_strings[i] (peer address string)", + "format", + "validation", + "invalid" + ], + "evidence": "beast::IP::Endpoint::from_string at SourceStringsImp::fetch", + "issue_pattern": "Missing format validation for m_strings[i] (peer address string)", + "why_false_positive": "beast::IP::Endpoint::from_string validates m_strings[i] (peer address string) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ep (endpoint validity)", + "empty", + "string", + "validation" + ], + "evidence": "is_unspecified at SourceStringsImp::fetch", + "issue_pattern": "Missing empty string validation for ep (endpoint validity)", + "why_false_positive": "is_unspecified validates ep (endpoint validity) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/SourceStrings.cpp", + "functions": [ + { + "args": [ + "name", + "strings" + ], + "lineno": 7, + "name": "SourceStringsImp::SourceStringsImp" + }, + { + "args": [], + "lineno": 11, + "name": "SourceStringsImp::~SourceStringsImp" + }, + { + "args": [], + "lineno": 13, + "name": "SourceStringsImp::name" + }, + { + "args": [ + "results", + "journal" + ], + "lineno": 17, + "name": "SourceStringsImp::fetch" + }, + { + "args": [ + "name", + "strings" + ], + "lineno": 36, + "name": "SourceStrings::New" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "PeerFinder" + } + ], + "test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, this code would be tested via higher-level PeerFinder or SourceStrings tests, possibly in files like 'test/peerfinder/SourceStrings.test.cpp' or integration tests that exercise peer source loading. Gaps: No explicit unit tests for validation logic in this file; edge cases (malformed addresses, duplicates, unspecified endpoints) may not be directly tested unless covered by broader PeerFinder tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "beast::IP::Endpoint", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (invalid endpoints are skipped, not thrown)", + "field": "m_strings[i] (peer address string)", + "location": "SourceStringsImp::fetch", + "validated_by": "beast::IP::Endpoint::from_string", + "validates": [ + "Checks if m_strings[i] is a valid IP endpoint string (IP address and optional port)", + "If from_string fails (endpoint is unspecified), the entry is skipped" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "none (unspecified endpoints are skipped)", + "field": "ep (endpoint validity)", + "location": "SourceStringsImp::fetch", + "validated_by": "is_unspecified", + "validates": [ + "Ensures only specified (valid) endpoints are added to results.addresses" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/SourceStrings.cpp.ai.md b/src/xrpld/peerfinder/detail/SourceStrings.cpp.ai.md new file mode 100644 index 0000000000..7118123040 --- /dev/null +++ b/src/xrpld/peerfinder/detail/SourceStrings.cpp.ai.md @@ -0,0 +1,41 @@ +# `SourceStrings.cpp` — Static String-Based Peer Address Source + +## Role in the System + +Within the XRPL PeerFinder subsystem, peer discovery depends on a hierarchy of fallback sources that supply bootstrap addresses when the local peer cache is empty or stale. `SourceStrings.cpp` implements the simplest of these: a `Source` that converts a static list of plain-text strings (typically pulled from the node's configuration file) into validated `beast::IP::Endpoint` objects for consumption by the peer connection logic. + +The file exists because configuration-time peer addresses arrive as raw strings, but the rest of PeerFinder works exclusively with parsed `Endpoint` objects. This file owns that conversion boundary, validating and normalizing the strings in one place rather than spreading the parsing logic across callers. + +## Design: Hidden Implementation Behind a Factory + +The public interface in `SourceStrings.h` declares only a base class and a single static factory method, `SourceStrings::New()`. The actual implementation lives in the private class `SourceStringsImp`, which is defined entirely inside this `.cpp` file. Callers receive a `std::shared_ptr` — the abstract base — so they never see or depend on `SourceStringsImp` directly. + +This is a deliberate pimpl-adjacent pattern: the concrete type is hidden behind the translation unit boundary. The benefit is that changes to `SourceStringsImp` — adding fields, changing parsing behavior — never force recompilation of anything that includes `SourceStrings.h`. The tradeoff is that unit-testing the implementation requires going through the factory, but for a class this simple that is not a meaningful restriction. + +## The `fetch()` Method and Validation Logic + +`SourceStringsImp::fetch()` is where all substantive work happens. Given the stored list of strings, it iterates and attempts to parse each one with `beast::IP::Endpoint::from_string()`. The result is immediately checked with `is_unspecified()`. Only endpoints that parse successfully and produce a valid (specified) address are appended to `results.addresses`; malformed or empty strings are silently dropped. + +There is a subtle redundancy in the loop worth noting: + +```cpp +beast::IP::Endpoint ep(beast::IP::Endpoint::from_string(m_strings[i])); +if (is_unspecified(ep)) + ep = beast::IP::Endpoint::from_string(m_strings[i]); +if (!is_unspecified(ep)) + results.addresses.push_back(ep); +``` + +The string is parsed once into `ep`. If that parse fails (producing an unspecified endpoint), the code parses the *same string again* — an idempotent retry that produces an identical result. This second parse does not change the outcome. The effective behavior is simply: parse the string; if valid, keep it. The duplicate `from_string` call appears to be vestigial code, likely left from an earlier attempt to apply a fallback parsing strategy. + +Invalid addresses produce no error — not a logged warning, not an entry in `results.error`. This is intentional for a static source: misconfigured strings are a configuration problem, not a runtime fault, and the node should continue connecting to whichever addresses *do* parse correctly. + +## Integration Point: `PeerfinderManager` + +`SourceStrings::New()` is called from exactly one place in the codebase: `PeerfinderManagerImp::addFallbackStrings()` in `PeerfinderManager.cpp`. That method wraps the constructed source and passes it to `m_logic.addStaticSource()`, registering it as a bootstrap fallback. From that point the source is owned by the logic layer, which calls `fetch()` when the peer connection pool needs additional bootstrap candidates. + +The `journal` parameter accepted by `fetch()` is unused in this implementation. Other `Source` subclasses — such as those fetching from a remote URL — use it to log HTTP errors or DNS failures. For a static string list there is nothing asynchronous to report, so the journal is accepted only to satisfy the `Source` interface contract. + +## Summary + +`SourceStrings.cpp` is a small, focused adapter: it bridges the gap between raw configuration strings and the typed endpoint world that PeerFinder operates in. Its design choices — hidden implementation class, factory construction, silent dropping of bad inputs — reflect the conventions of the broader PeerFinder module, where sources are pluggable, callers are insulated from implementation details, and bootstrap failures are non-fatal by design. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/SourceStrings.h.ai.json b/src/xrpld/peerfinder/detail/SourceStrings.h.ai.json new file mode 100644 index 0000000000..f4fd52357f --- /dev/null +++ b/src/xrpld/peerfinder/detail/SourceStrings.h.ai.json @@ -0,0 +1,42 @@ +{ + "args": [ + { + "lineno": 16, + "name": "name" + }, + { + "lineno": 16, + "name": "strings" + } + ], + "classes": [ + { + "args": [], + "lineno": 9, + "name": "SourceStrings" + } + ], + "description": "Defines the SourceStrings class, which provides peer addresses from a static set of strings for the PeerFinder subsystem.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/SourceStrings.h", + "functions": [ + { + "args": [ + "name", + "strings" + ], + "lineno": 16, + "name": "New" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/SourceStrings.h.ai.md b/src/xrpld/peerfinder/detail/SourceStrings.h.ai.md new file mode 100644 index 0000000000..d422ad2217 --- /dev/null +++ b/src/xrpld/peerfinder/detail/SourceStrings.h.ai.md @@ -0,0 +1,27 @@ +# `SourceStrings.h` — Static Peer Address Source for PeerFinder + +## Role in the System + +`SourceStrings.h` declares `SourceStrings`, a concrete subclass of the `Source` bootstrap interface used by the XRPL `PeerFinder` subsystem. Its purpose is to wrap a pre-configured list of IP address strings — typically read from the node's configuration file — into a form that the PeerFinder bootstrap logic can query uniformly alongside other address sources such as remote HTTPS endpoints or DNS-based sources. + +PeerFinder uses `Source` objects as fallbacks during startup when the local peer cache is empty or stale. `SourceStrings` represents the simplest and most immediate of these fallbacks: hard-coded or operator-specified peer addresses. + +## Design: Public Interface, Hidden Implementation + +The header exposes only the minimal public surface needed for callers: a type alias `Strings = std::vector` and a static factory method `New(name, strings)`. The actual implementation class, `SourceStringsImp`, lives entirely in `SourceStrings.cpp` and inherits privately from `SourceStrings`, which itself inherits from `Source`. This two-level inheritance isolates all parsing logic and stored state from header consumers, keeping compile dependencies minimal and the ABI stable. + +The `New()` factory returns a `std::shared_ptr` — not `std::shared_ptr` — which means callers work exclusively through the `Source` interface. This deliberate upcast at the boundary prevents accidental coupling to the concrete type and matches how `PeerfinderManager` passes the result directly to `m_logic.addStaticSource()`. + +## What `fetch()` Does + +The `SourceStringsImp::fetch()` override (in the `.cpp`) iterates the stored string vector, attempting to parse each entry into a `beast::IP::Endpoint` via `Endpoint::from_string()`. Any entry that parses to an unspecified (invalid) endpoint is silently skipped; valid endpoints are appended to `results.addresses`. There is a minor quirk in the implementation: a failed parse attempt is immediately retried with the identical string before the `is_unspecified` guard — effectively a no-op retry — likely a leftover from an earlier version that tried alternate parsing strategies. + +The `Results` struct inherited from `Source` provides both an error code and an address list, but for `SourceStrings` the error code is never set: since the data is already in memory, there are no I/O failure modes. The `cancel()` hook inherited from `Source` is also a no-op for the same reason — there is no asynchronous operation to interrupt. + +## Call Site + +`SourceStrings::New` is invoked exactly once, inside `PeerfinderManager::addFallbackStrings()`, which forwards the node's configured IP strings from the rippled config layer into the PeerFinder logic engine as a static (non-refreshable) source. Static sources are fetched once and never re-polled, which is appropriate for a list of fixed operator-defined addresses. + +## Summary + +`SourceStrings.h` is a small but structurally important piece of the PeerFinder bootstrap chain. Its value lies not in complexity but in clean separation: it hides the parsing and storage details behind the `Source` interface, uses a factory method to prevent direct construction of the concrete type, and feeds operator-configured peer addresses into the same polymorphic source pipeline used by dynamic sources like remote URL fetchers. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Store.h.ai.json b/src/xrpld/peerfinder/detail/Store.h.ai.json new file mode 100644 index 0000000000..401ec18863 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Store.h.ai.json @@ -0,0 +1,44 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 7, + "name": "Store" + }, + { + "args": [], + "lineno": 21, + "name": "Entry" + } + ], + "description": "Defines an abstract interface (Store) for persisting PeerFinder data, including loading and saving a bootstrap cache of peer endpoints and their valence.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Store.h", + "functions": [ + { + "args": [ + "load_callback const& cb" + ], + "lineno": 15, + "name": "load" + }, + { + "args": [ + "std::vector const& v" + ], + "lineno": 26, + "name": "save" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Store.h.ai.md b/src/xrpld/peerfinder/detail/Store.h.ai.md new file mode 100644 index 0000000000..92336af89d --- /dev/null +++ b/src/xrpld/peerfinder/detail/Store.h.ai.md @@ -0,0 +1,39 @@ +# `peerfinder/detail/Store.h` — Abstract Persistence Interface for the Bootstrap Cache + +`Store.h` defines a narrow abstract interface that acts as the persistence boundary for the PeerFinder subsystem's bootstrap cache. Its sole responsibility is to serialize and deserialize the set of known peer endpoints that a node can use to re-establish network connections after a restart. Nothing in this file deals with connection logic, ranking, or routing — it is purely a load/save contract. + +## Role in the System + +When an XRPL node starts up, it needs a warm list of candidate peer addresses to connect to before it has received gossip from the live network. The `Bootcache` class (in `Bootcache.h`) maintains this list in memory, using a bidirectional map keyed on both the `beast::IP::Endpoint` (for fast lookup) and an entry `valence` value (for ranked iteration). `Store` is the interface through which `Bootcache` persists that map to and from durable storage across restarts. + +`Bootcache` holds a `Store&` reference injected at construction, and `Logic` (the central PeerFinder coordinator) wires everything together. This layering lets the in-memory cache operate entirely independently of the storage mechanism — only two moments cross the boundary: initial load at startup and periodic saves triggered by `Bootcache::periodicActivity()`. + +## Interface Design + +The interface has exactly two pure virtual methods and one nested value type: + +```cpp +using load_callback = std::function; +virtual std::size_t load(load_callback const& cb) = 0; + +struct Entry { beast::IP::Endpoint endpoint; int valence{}; }; +virtual void save(std::vector const& v) = 0; +``` + +The `load` method uses a callback rather than returning a container. This is a deliberate design choice: the caller (`Bootcache::load()`) wants to insert each record directly into its bimap as it streams in. Returning a `std::vector` would require an intermediate allocation that is immediately consumed and discarded. The callback eliminates the temporary, and the returned `std::size_t` lets the caller log or act on how many entries were actually valid and consumed. + +The `save` method goes in the opposite direction — it takes a snapshot of the whole cache as a flat vector of `Entry` structs and overwrites the persistent store entirely. This is a full replace, not a diff or an incremental append, which matches how `StoreSqdb` works: it clears and rewrites the table atomically. The simplicity of the semantics (no primary keys, no update-or-insert logic at the `Store` level) keeps the interface stable regardless of what the underlying storage does internally. + +## The `valence` Field + +The `Entry::valence` integer encodes connection quality history. A positive valence indicates consecutive successful handshakes to that peer; a negative value indicates consecutive failed attempts. This value is what `Bootcache` uses to sort candidates in decreasing priority order when selecting outbound connection targets. By persisting valence alongside the address, the node avoids wasting connection budget on historically unreliable peers immediately after restart. + +## Concrete Implementation: `StoreSqdb` + +The only concrete implementation in this codebase is `StoreSqdb` (in `StoreSqdb.h`), which stores data in a local SQLite database via SOCI. It delegates the actual SQL to functions defined in `xrpld/app/rdb/PeerFinder.h` (`readPeerFinderDB`, `savePeerFinderDB`, `updatePeerFinderDB`), following the repository's pattern of keeping raw SQL out of domain classes. `StoreSqdb` also handles schema migration: it tracks a `currentSchemaVersion` (currently 4) and calls `update()` on open to convert older on-disk formats. + +The separation between `Store` (the abstract interface) and `StoreSqdb` (the SQLite implementation) means the bootstrap cache can be tested with an in-memory mock without touching any database code, and future storage backends (e.g., a flat file or a key-value store) could be substituted without touching `Bootcache` or `Logic`. + +## Summary + +`Store.h` is intentionally minimal — 32 lines including the namespace boilerplate. It exists to enforce a clean separation of concerns: the in-memory peer ranking logic in `Bootcache` never sees SQL, file handles, or schema versions, and the storage layer never needs to understand valence ordering or bimap internals. The callback-based `load` and full-replace `save` semantics reflect the actual usage pattern and avoid unnecessary data copies at both endpoints of the persistence boundary. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/StoreSqdb.h.ai.json b/src/xrpld/peerfinder/detail/StoreSqdb.h.ai.json new file mode 100644 index 0000000000..d8cefcefbc --- /dev/null +++ b/src/xrpld/peerfinder/detail/StoreSqdb.h.ai.json @@ -0,0 +1,77 @@ +{ + "args": [ + { + "lineno": 17, + "name": "journal" + }, + { + "lineno": 25, + "name": "config" + }, + { + "lineno": 33, + "name": "cb" + }, + { + "lineno": 54, + "name": "v" + } + ], + "classes": [ + { + "args": [ + "journal" + ], + "lineno": 10, + "name": "StoreSqdb" + } + ], + "description": "Provides a database-backed implementation of the PeerFinder::Store interface using SQLite for persistence of peer bootstrap cache data in the XRPL network.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/StoreSqdb.h", + "functions": [ + { + "args": [ + "config" + ], + "lineno": 25, + "name": "open" + }, + { + "args": [ + "cb" + ], + "lineno": 33, + "name": "load" + }, + { + "args": [ + "v" + ], + "lineno": 54, + "name": "save" + }, + { + "args": [], + "lineno": 61, + "name": "update" + }, + { + "args": [ + "config" + ], + "lineno": 68, + "name": "init" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/StoreSqdb.h.ai.md b/src/xrpld/peerfinder/detail/StoreSqdb.h.ai.md new file mode 100644 index 0000000000..d0ce3126e4 --- /dev/null +++ b/src/xrpld/peerfinder/detail/StoreSqdb.h.ai.md @@ -0,0 +1,31 @@ +# `StoreSqdb.h` — SQLite-backed Bootstrap Cache Persistence + +## Role in the System + +`StoreSqdb` is the SQLite persistence implementation for the PeerFinder subsystem's bootstrap cache. PeerFinder manages how an XRPL node discovers and connects to peers; its bootstrap cache is the on-disk record of previously known peer endpoints and their associated *valence* scores (a signed integer reflecting how reliable a peer has proven to be). Without persistence, a node that restarts cold has no memory of peers it successfully contacted before, forcing it back to hardcoded seeds. `StoreSqdb` solves this by writing and reading a SQLite table via SOCI. + +## Class Structure and Design + +`StoreSqdb` is a concrete subclass of the abstract `Store` interface defined in `Store.h`. That interface is deliberately minimal: it mandates only two operations — `load(load_callback)` to enumerate persisted entries and `save(std::vector)` to overwrite them. Everything above that contract — the choice of database engine, schema versioning, connection management — is an implementation detail owned entirely by `StoreSqdb`. + +The class holds two private members: a `beast::Journal` for structured logging and a `soci::session` representing an open SQLite connection. The session is a value member (not a pointer), so the database connection lifetime is tied directly to the object lifetime. There is no external locking; PeerFinder's upper layers are assumed to serialize access. + +## Initialization and Schema Migration + +`open()` is the entry point after construction. It calls the private `init()` then `update()` in sequence. `initPeerFinderDB()` (defined in `app/rdb/detail/PeerFinder.cpp`) opens the SQLite file from the `BasicConfig`, creates the `SchemaVersion` and `PeerFinder_BootstrapCache` tables if they do not already exist, and adds an index on the `address` column — all wrapped in a single transaction. `updatePeerFinderDB()` then reads the stored schema version number and applies any necessary migrations up to `currentSchemaVersion = 4`. + +The migration logic reveals the schema's history. Versions below 3 accumulated legacy endpoint tables (`LegacyEndpoints`, `PeerFinderLegacyEndpoints`, `PeerFinder_LegacyEndpoints`) that are simply dropped. The version-4 migration is more involved: an older schema included an `uptime` column in `PeerFinder_BootstrapCache` that was later removed. Because SQLite does not support `DROP COLUMN`, the migration creates a new table `PeerFinder_BootstrapCache_Next` with the cleaned schema, bulk-copies all valid rows into it, drops the old table and index, renames the new table, and recreates the index. This entire operation runs inside a single SOCI transaction so it is atomic. If the stored version is *higher* than `currentSchemaVersion`, the code throws `std::runtime_error` rather than silently operating on a newer, unknown schema — a conservative safety check that prevents data corruption from running stale code against a database written by a newer binary. + +## Load and Save Semantics + +`load()` streams rows from `PeerFinder_BootstrapCache` via a prepared SOCI statement and, for each row, invokes the caller-supplied callback with a parsed `beast::IP::Endpoint` and the stored valence. Parsing is validated: `beast::IP::Endpoint::from_string()` can return an unspecified endpoint for malformed strings, and `StoreSqdb` explicitly checks `is_unspecified()` before forwarding to the callback, logging a `journal.error()` for any bad address. This guards against corrupted or manually edited database rows silently injecting zero endpoints into the peer set. + +`save()` takes a full snapshot approach: it issues `DELETE FROM PeerFinder_BootstrapCache` followed by a bulk `INSERT` of all entries, inside a single transaction. This is not an upsert or a diff — it completely replaces the table's contents on every call. The bulk insert exploits SOCI's vector binding (`soci::use(vectorOfStrings), soci::use(vectorOfInts)`), which batches all rows into a single parameterized statement rather than looping individual inserts. This is efficient for the expected scale of the bootstrap cache (hundreds of entries at most) and keeps the transaction short. + +## Separation of Concerns + +A notable design choice is that `StoreSqdb` delegates all SQL to free functions declared in `xrpld/app/rdb/PeerFinder.h`. `StoreSqdb` itself contains no SQL literals. This layering means the database logic can be tested or replaced independently of the `Store` interface, and the raw SQL lives in the `rdb` (relational database) layer, consistent with how other XRPL subsystems separate their persistence code. `StoreSqdb` is effectively an adapter that wires the `Store` virtual interface to the `rdb` free-function API, passing through its `soci::session` as the shared state. + +## SOCI and `boost::optional` + +One subtlety visible in the implementation is that SOCI's `INTO` clause requires `boost::optional`, not `std::optional`, for nullable columns — a legacy constraint of the SOCI library version used here. The schema version query in `updatePeerFinderDB()` explicitly comments on this, using `boost::optional` and calling `.value_or(0)` to treat a missing row as version 0 (a brand-new database). \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Tuning.h.ai.json b/src/xrpld/peerfinder/detail/Tuning.h.ai.json new file mode 100644 index 0000000000..601cac6020 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Tuning.h.ai.json @@ -0,0 +1,22 @@ +{ + "args": [], + "classes": [], + "description": "Defines heuristically tuned constants and configuration parameters for the PeerFinder component in the XRPL project, including connection policies, bootcache, and livecache settings.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/Tuning.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "PeerFinder" + }, + { + "lineno": 9, + "name": "Tuning" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/Tuning.h.ai.md b/src/xrpld/peerfinder/detail/Tuning.h.ai.md new file mode 100644 index 0000000000..f1c2743278 --- /dev/null +++ b/src/xrpld/peerfinder/detail/Tuning.h.ai.md @@ -0,0 +1,48 @@ +# `Tuning.h` — PeerFinder Heuristic Constants + +`Tuning.h` is the single source of truth for every magic number in the `PeerFinder` subsystem. Rather than scattering literal integers and durations across a dozen implementation files, all heuristically chosen values live here under the `xrpl::PeerFinder::Tuning` namespace, making them easy to find, review, and adjust together. + +## Automatic Connection Policy + +The first `enum` block governs how the node initiates and maintains outbound connections to the P2P overlay. + +`secondsPerConnect` (10 s) is the cadence at which the connection logic wakes up and dispatches a new batch of outbound attempts. `maxConnectAttempts` (20) caps how many of those attempts can be in-flight simultaneously; `Counts::attempts_needed()` consults this value directly to avoid opening a connection storm against the rest of the network. + +The outbound slot policy is a two-part rule: take the larger of `outPercent * maxPeers` (rounded) or the hard floor `minOutCount` (10). The 15% figure in `outPercent` is the key architectural choice — it keeps inbound capacity dominant so the node remains openly reachable, while still guaranteeing that even a tiny deployment (say, 10 total peers) maintains at least 10 self-initiated connections for robust topology. `PeerfinderConfig.cpp` expresses this as: + +```cpp +std::max((maxPeers * Tuning::outPercent + 50) / 100, std::size_t(Tuning::minOutCount)) +``` + +`defaultMaxPeers` (21) is the in-class initializer used by `Config`. An odd number is no accident: the 15% rule on 21 rounds to 3 outbound peers, which is a reasonable default for a light node. + +`maxRedirects` (30) limits how many addresses a single redirecting peer may provide when the node's slots are full. This is a security bound — without it a malicious peer could flood the address caches by sending an unbounded redirect list. + +## Fixed Connection Backoff + +The `connectionBackoff` array `{1, 1, 2, 3, 5, 8, 13, 21, 34, 55}` is the Fibonacci sequence in minutes. It governs how long `Fixed` slots (operator-configured peers) must wait between retries after a connection failure. `Fixed::failure()` increments an index clamped to the array's last position, so the backoff saturates at 55 minutes rather than growing unboundedly. A successful connection resets the index to zero and the `when` time to now, so a peer that reconnects cleanly gets immediate re-consideration. The Fibonacci progression is a deliberate balance: it grows fast enough to avoid hammering an unreachable host but is bounded to prevent permanent lockout. + +## Bootcache + +The bootcache is persistent storage (SQLite-backed) of peer addresses that survived across restarts. Two constants control its size: + +- `bootcacheSize` (1 000) is the threshold above which `Bootcache::trim()` fires. +- `bootcachePrunePercent` (10) means 10% of entries are removed per trim, bringing 1 000 entries down to 900 rather than slashing the cache aggressively. + +`bootcacheCooldownTime` (60 s) is a write-coalescing guard. Frequent connection events (valence changes, insertions) call `flagForUpdate()`, but actual SQLite writes only happen if 60 seconds have elapsed since the last flush. The comment in the file notes the intent: this window should be larger than the typical lifetime of a peer that connects, dumps addresses, and disconnects — ensuring the useful addresses are captured in a single write rather than triggering multiple redundant writes per ephemeral peer. + +## Livecache + +The livecache is the in-memory, gossip-driven address cache populated by `mtENDPOINTS` messages. + +`maxHops` (6) is the horizon depth of the gossip graph. Any endpoint arriving with `hops > 6` is silently dropped. The `Endpoint` constructor enforces this on ingress by clamping to `maxHops + 1` (a sentinel meaning "beyond horizon"), while `Logic::on_endpoints()` and the `Handouts` filters reject anything exceeding the limit. Six hops provides a network diameter large enough to reach far corners of a large P2P graph while preventing the cache from being polluted by stale, long-chain entries. + +`numberOfEndpoints` (12, computed as `2 * maxHops`) is how many endpoints each `mtENDPOINTS` message should carry. `numberOfEndpointsMax` (clamped to `max(24, 64)` = 64) is the upper bound accepted from a peer; messages exceeding this are randomly trimmed by `Logic::on_endpoints()` before insertion. + +`redirectEndpointCount` (10) controls how many addresses are handed to a newly connecting peer that gets redirected (i.e., when slots are full). Keeping this to 10 avoids bandwidth waste while giving the client enough alternatives. + +`secondsPerMessage` (151 s) is the per-peer rate limit on how often `mtENDPOINTS` messages are sent or accepted. The comment acknowledges it is prime intentionally — a prime interval de-synchronizes the broadcast timers across nodes, preventing coordinated message floods where many peers all gossip simultaneously. + +`liveCacheSecondsToLive` (30 s) is the TTL for a livecache entry. `Livecache::expire()` and `SlotImp::expire()` both use this value; note it is much shorter than `secondsPerMessage` (151 s). This asymmetry is deliberate: entries age out quickly so that a peer which drops offline does not linger in the cache, yet broadcasts are infrequent to keep control-plane traffic low. The short TTL means a node must receive a fresh `mtENDPOINTS` to keep a remote address visible. + +`recentAttemptDuration` (60 s) suppresses retries to addresses that were recently attempted. `Logic::once_per_second()` expires the squelch map using this duration, ensuring an address is not hammered repeatedly within the same connection cycle. \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/iosformat.h.ai.json b/src/xrpld/peerfinder/detail/iosformat.h.ai.json new file mode 100644 index 0000000000..e0cac42f48 --- /dev/null +++ b/src/xrpld/peerfinder/detail/iosformat.h.ai.json @@ -0,0 +1,200 @@ +{ + "args": [ + { + "lineno": 12, + "name": "width_" + }, + { + "lineno": 43, + "name": "width_" + }, + { + "lineno": 43, + "name": "fill_" + }, + { + "lineno": 56, + "name": "width_" + }, + { + "lineno": 56, + "name": "pad_" + }, + { + "lineno": 56, + "name": "fill_" + }, + { + "lineno": 87, + "name": "text_" + }, + { + "lineno": 87, + "name": "width_" + }, + { + "lineno": 87, + "name": "pad_" + }, + { + "lineno": 87, + "name": "right_" + } + ], + "classes": [ + { + "args": [ + "width_" + ], + "lineno": 11, + "name": "leftw" + }, + { + "args": [ + "width_", + "fill_" + ], + "lineno": 41, + "name": "divider" + }, + { + "args": [ + "width_", + "pad_", + "fill_" + ], + "lineno": 55, + "name": "fpad" + }, + { + "args": [ + "text_", + "width_", + "pad_", + "right_" + ], + "lineno": 84, + "name": "field_t" + } + ], + "description": "Provides a collection of stream manipulators and formatting utilities for producing formatted log output, including left justification, section headings, dividers, padded fields, and field justification for streams.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/detail/iosformat.h", + "functions": [ + { + "args": [ + "ios", + "p" + ], + "lineno": 18, + "name": "operator<<" + }, + { + "args": [ + "title", + "width", + "fill" + ], + "lineno": 28, + "name": "heading" + }, + { + "args": [ + "os", + "d" + ], + "lineno": 48, + "name": "operator<<" + }, + { + "args": [ + "os", + "f" + ], + "lineno": 62, + "name": "operator<<" + }, + { + "args": [ + "t" + ], + "lineno": 75, + "name": "to_string" + }, + { + "args": [ + "os", + "f" + ], + "lineno": 101, + "name": "operator<<" + }, + { + "args": [ + "text", + "width", + "pad", + "right" + ], + "lineno": 116, + "name": "field" + }, + { + "args": [ + "text", + "width", + "pad", + "right" + ], + "lineno": 124, + "name": "field" + }, + { + "args": [ + "t", + "width", + "pad", + "right" + ], + "lineno": 132, + "name": "field" + }, + { + "args": [ + "text", + "width", + "pad" + ], + "lineno": 139, + "name": "rField" + }, + { + "args": [ + "text", + "width", + "pad" + ], + "lineno": 146, + "name": "rField" + }, + { + "args": [ + "t", + "width", + "pad" + ], + "lineno": 153, + "name": "rField" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "beast" + }, + { + "lineno": 72, + "name": "detail" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/detail/iosformat.h.ai.md b/src/xrpld/peerfinder/detail/iosformat.h.ai.md new file mode 100644 index 0000000000..17105ded22 --- /dev/null +++ b/src/xrpld/peerfinder/detail/iosformat.h.ai.md @@ -0,0 +1,25 @@ +# `iosformat.h` — Stream Formatting Utilities for PeerFinder Diagnostics + +## Role and Context + +This header lives in `src/xrpld/peerfinder/detail/` and provides a compact set of stream manipulators and formatting helpers used throughout the PeerFinder subsystem's logging output. It belongs to the `beast` namespace rather than any rippled-specific namespace, signalling that it was originally conceived as a reusable library primitive. The file has no XRPL-specific dependencies — only ``, ``, and `` — making it a pure formatting layer that could be lifted out of context without modification. + +The PeerFinder module is responsible for discovering, scoring, and managing peer connections on the XRP Ledger network. Its internal `Logic.h` and `Livecache.h` components produce a large volume of debug-level log lines describing connection decisions, endpoint scoring, and cache mutations. Without consistent column alignment, those log lines become hard to scan at a glance. `iosformat.h` exists to keep that alignment without scattering `std::setw`/`std::left` boilerplate across every log site. + +## Key Primitives + +**`leftw`** is the workhorse of the file and the only construct that actually appears in production log code. It is a lightweight stream manipulator — not for `std::ostream` but for the lower-level `std::basic_ios` — that sets left-justification and a field width in one shot. Because it targets `std::basic_ios` rather than `std::ostream`, it modifies the stream's sticky format state rather than emitting characters. Every log call in `Logic.h` and `Livecache.h` begins with `beast::leftw(18)`, establishing a fixed 18-character column for the log prefix (e.g., `"Logic connect "`, `"Livecache insert "`) before appending variable-length address or count data. This pattern produces scannable output in log files without needing a full logging framework with field-width support. + +**`heading()`** is a free function that pads a title string out to a fixed width (default 80) filled with dashes. It takes the title by value, appends a space separator, then uses `string::resize()` with a fill character to extend it. The approach is allocation-friendly: `reserve()` is called upfront to avoid reallocation during the `push_back`/`resize` sequence. This is suitable for producing section-break lines in diagnostic dumps or multi-line status reports. + +**`divider`** is the outputtable counterpart: it emits a solid line of fill characters (default: 80 dashes) directly to an `std::ostream`. Unlike `heading()`, which returns a string, `divider` defers rendering until it is streamed, fitting naturally into chained `operator<<` expressions. Both `heading()` and `divider` default to 80 columns, a deliberate mirroring that lets callers mix them to produce box-formatted diagnostic sections. + +**`fpad`** emits a block of whitespace of a specified width plus an optional extra pad amount. Its constructor merges the two into a single `width` member (`width_ + pad_`), so at stream time it just emits one `basic_string` of repeated fill characters. This is useful for visually indenting or spacing columns in tabular output, though it sees no direct use in the files examined. + +**`field_t` and the `field()`/`rField()` factories** provide the most capable formatting: a fixed-width text column with optional trailing pad and a left/right justification flag. The template is parameterized on `CharT`/`Traits`/`Allocator` for wide-character generality, but in practice all usages are narrowed to `char` through the convenience overloads. Crucially, three overloads of `field()` accept `std::basic_string`, a raw `CharT const*`, and an arbitrary `T` respectively. The third overload converts `T` to a string via `detail::to_string()`, which streams the value into a `std::stringstream` — a deliberate indirection that avoids requiring the caller to know whether their value is a string, integer, or address object. `rField()` is simply `field()` with `right = true` hardcoded, provided as a named alias to make call sites read more clearly than passing a boolean flag. + +## Design Observations + +The split between `leftw` (modifying `std::basic_ios`) and the rest of the utilities (writing to `std::ostream`) is intentional: `leftw` integrates with the stream's built-in width/justification mechanism, while the others bypass it entirely and emit pre-computed strings. This means `leftw`'s width effect is consumed by the very next field write (standard `std::ios` semantics), while `field_t` controls padding independently of the stream state. The two approaches are complementary: `leftw` handles the log prefix column, while `field_t`/`fpad`/`divider` would be used for multi-column tabular output where each column must be managed explicitly. + +The `detail::to_string()` helper is kept in an inner `beast::detail` namespace to signal that it is not part of the public API surface — it exists solely to support the generic `field()` and `rField()` overloads. Using `std::stringstream` rather than `std::to_string` or `std::format` makes it work for any type that has a streaming `operator<<`, which is the broadest possible compatibility contract at the cost of a heap allocation per call. \ No newline at end of file diff --git a/src/xrpld/peerfinder/make_Manager.h.ai.json b/src/xrpld/peerfinder/make_Manager.h.ai.json new file mode 100644 index 0000000000..e0cf2bf393 --- /dev/null +++ b/src/xrpld/peerfinder/make_Manager.h.ai.json @@ -0,0 +1,51 @@ +{ + "args": [ + { + "lineno": 12, + "name": "io_context" + }, + { + "lineno": 13, + "name": "clock" + }, + { + "lineno": 14, + "name": "journal" + }, + { + "lineno": 15, + "name": "config" + }, + { + "lineno": 16, + "name": "collector" + } + ], + "classes": [], + "description": "This file declares a factory function to create a new PeerFinder Manager instance for the XRPL (Ripple) network, within the xrpl::PeerFinder namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/peerfinder/make_Manager.h", + "functions": [ + { + "args": [ + "io_context", + "clock", + "journal", + "config", + "collector" + ], + "lineno": 11, + "name": "make_Manager" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "PeerFinder" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/peerfinder/make_Manager.h.ai.md b/src/xrpld/peerfinder/make_Manager.h.ai.md new file mode 100644 index 0000000000..5cc8adf1d3 --- /dev/null +++ b/src/xrpld/peerfinder/make_Manager.h.ai.md @@ -0,0 +1,51 @@ +# `make_Manager.h` — PeerFinder Factory Declaration + +This header is the public entry point for constructing the PeerFinder subsystem. It declares a single factory function, `make_Manager()`, that produces an owned `Manager` instance, hiding the concrete implementation type entirely from callers. + +## Role in the System + +The PeerFinder subsystem is responsible for discovering, tracking, and managing peer connections on the XRP Ledger network. It maintains a database of known peer endpoints, governs slot allocation for inbound and outbound connections, and periodically runs logic to achieve the target peer counts defined by `Config`. The `Manager` abstract class (declared in `PeerfinderManager.h`) is the façade through which all of this behaviour is driven. + +`make_Manager.h` provides the sole mechanism by which a `Manager` is created. The concrete class — `ManagerImp`, defined inside `detail/PeerfinderManager.cpp` — is never exposed in any public header. This enforces a hard compile-time boundary: consumers of the PeerFinder subsystem link against the interface, not the implementation. + +## The Factory Function + +```cpp +std::unique_ptr +make_Manager( + boost::asio::io_context& io_context, + clock_type& clock, + beast::Journal journal, + BasicConfig const& config, + beast::insight::Collector::ptr const& collector); +``` + +The implementation is a one-liner that delegates directly to `std::make_unique(...)`. The indirection through a named factory rather than exposing the constructor directly is deliberate: it keeps `ManagerImp` out of header scope, preventing any translation unit from constructing or depending on the concrete type. + +Each parameter maps to a distinct concern: + +- **`io_context`** — The Boost.Asio executor used for all asynchronous timer and I/O operations inside the manager. PeerFinder schedules periodic tasks (endpoint fetching, cache flushes) through this context. +- **`clock`** — A `beast::abstract_clock` used for all time-based decisions. Accepting an abstract clock rather than calling `std::chrono::steady_clock::now()` directly makes the manager unit-testable with a mock clock. +- **`journal`** — A `beast::Journal` sink for structured diagnostic output, labelled `"PeerFinder"` by the caller in practice. +- **`config`** — A `BasicConfig` carrying the raw configuration (from the server's config file) used during construction. The manager converts this into its own `PeerFinder::Config` via `Config::makeConfig()`. +- **`collector`** — A `beast::insight::Collector` pointer for publishing metrics. The manager registers internal stats counters (active inbound/outbound peer counts, etc.) against this collector. + +## Caller Context + +The sole production callsite is in `OverlayImpl`'s constructor (`src/xrpld/overlay/detail/OverlayImpl.cpp`), where `m_peerFinder` is initialised as a member: + +```cpp +, m_peerFinder( + PeerFinder::make_Manager( + io_context, + stopwatch(), + app_.getJournal("PeerFinder"), + config, + collector)) +``` + +The `Overlay` layer owns the `Manager` for its entire lifetime and is the only entity that drives it — calling `start()`, `stop()`, `new_inbound_slot()`, `new_outbound_slot()`, `on_closed()`, `once_per_second()`, and so on. Peer connection events propagate upward through `Slot` handles; the manager never calls back into the overlay directly. + +## Design Pattern + +The header follows the same factory-function idiom used elsewhere in the rippled codebase (e.g., `make_Overlay.h` for the overlay subsystem, `Resource::make_Manager` for the resource manager). The pattern achieves three things simultaneously: the `unique_ptr` return communicates exclusive ownership clearly; the opaque concrete type prevents accidental direct construction; and the header dependency is kept minimal — only `PeerfinderManager.h`, ``, and `` are required to consume this interface. \ No newline at end of file diff --git a/src/xrpld/perflog/detail/PerfLogImp.cpp.ai.json b/src/xrpld/perflog/detail/PerfLogImp.cpp.ai.json new file mode 100644 index 0000000000..1950b9cb85 --- /dev/null +++ b/src/xrpld/perflog/detail/PerfLogImp.cpp.ai.json @@ -0,0 +1,704 @@ +{ + "args": [ + { + "lineno": 15, + "name": "labels" + }, + { + "lineno": 15, + "name": "jobTypes" + }, + { + "lineno": 217, + "name": "setup" + }, + { + "lineno": 217, + "name": "app" + }, + { + "lineno": 217, + "name": "journal" + }, + { + "lineno": 217, + "name": "signalStop" + }, + { + "lineno": 228, + "name": "method" + }, + { + "lineno": 228, + "name": "requestId" + }, + { + "lineno": 245, + "name": "finish" + }, + { + "lineno": 273, + "name": "type" + }, + { + "lineno": 285, + "name": "dur" + }, + { + "lineno": 285, + "name": "startTime" + }, + { + "lineno": 285, + "name": "instance" + }, + { + "lineno": 320, + "name": "resize" + }, + { + "lineno": 355, + "name": "section" + }, + { + "lineno": 355, + "name": "configDir" + } + ], + "classes": [ + { + "args": [ + "labels", + "jobTypes" + ], + "lineno": 15, + "name": "PerfLogImp::Counters" + }, + { + "args": [ + "setup", + "app", + "journal", + "signalStop" + ], + "lineno": 217, + "name": "PerfLogImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "PerfLogImp::Counters::Counters" + ], + "entry_point": "PerfLogImp::Counters::Counters", + "purpose": "Constructor for Counters, initializes internal maps for RPC and JobQueue counters using provided labels and jobTypes.", + "validation_points": [ + "rpc_.emplace(label, Rpc()) - validates label uniqueness", + "jq_.emplace(jobType, Jq()) - validates jobType uniqueness" + ] + }, + { + "call_chain": [ + "PerfLogImp::Counters::countersJson" + ], + "entry_point": "PerfLogImp::Counters::countersJson", + "purpose": "Serializes the current state of counters to JSON, including only non-zero counters.", + "validation_points": [ + "if ((proc.second.value.started == 0u) && (proc.second.value.finished == 0u) && (proc.second.value.errored == 0u)) continue; // validates non-zero counters", + "jss::started, jss::finished, etc. - field name validation via jss:: namespace" + ] + } + ], + "data_flows": [ + { + "field": "labels (set)", + "flow": [ + "Constructor argument", + "for loop over labels", + "rpc_.emplace(label, Rpc())", + "rpc_ map" + ], + "origin": "Constructor argument to PerfLogImp::Counters", + "transformations": [ + "Inserted as key into rpc_ map" + ], + "validated_at": "rpc_.emplace(label, Rpc()) - ensures uniqueness, else UNREACHABLE" + }, + { + "field": "jobTypes (JobTypes map)", + "flow": [ + "Constructor argument", + "for loop over jobTypes", + "jq_.emplace(jobType, Jq())", + "jq_ map" + ], + "origin": "Constructor argument to PerfLogImp::Counters", + "transformations": [ + "Inserted as key into jq_ map" + ], + "validated_at": "jq_.emplace(jobType, Jq()) - ensures uniqueness, else UNREACHABLE" + }, + { + "field": "proc.second.value.started, finished, errored (uint32_t counters)", + "flow": [ + "rpc_ or jq_ map", + "for loop in countersJson", + "std::lock_guard lock", + "if ((started == 0u) && (finished == 0u) && (errored == 0u)) continue", + "JSON serialization" + ], + "origin": "rpc_ and jq_ maps (populated in constructor)", + "transformations": [ + "Checked for non-zero before inclusion in JSON" + ], + "validated_at": "Explicit check in for loop in countersJson" + }, + { + "field": "Json field names (jss::started, jss::finished, etc.)", + "flow": [ + "Used as keys in JSON objects in countersJson" + ], + "origin": "jss:: namespace (likely macro or constant)", + "transformations": [ + "None (constant field names)" + ], + "validated_at": "Compile-time via jss:: namespace" + }, + { + "field": "rpcobj[proc.first] (label as JSON key)", + "flow": [ + "rpc_ map key", + "used as key in rpcobj JSON object" + ], + "origin": "proc.first from rpc_ map", + "transformations": [ + "Converted to std::string if necessary" + ], + "validated_at": "std::unordered_map key type (std::string)" + } + ], + "description": "Implements performance logging for the XRPL server, including tracking and reporting of RPC and job queue statistics, log file management, and integration with application state.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "JSON field names via jss::", + "validation", + "missing", + "check" + ], + "evidence": "Field JSON field names via jss:: validated by jss:: (JSON field name validation), std::unordered_map (type/uniqueness), UNREACHABLE macro (error enforcement)", + "issue_pattern": "Missing validation for JSON field names via jss::", + "why_false_positive": "jss:: (JSON field name validation), std::unordered_map (type/uniqueness), UNREACHABLE macro (error enforcement) validates JSON field names via jss:: automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Map key uniqueness via std::unordered_map", + "validation", + "missing", + "check" + ], + "evidence": "Field Map key uniqueness via std::unordered_map validated by jss:: (JSON field name validation), std::unordered_map (type/uniqueness), UNREACHABLE macro (error enforcement)", + "issue_pattern": "Missing validation for Map key uniqueness via std::unordered_map", + "why_false_positive": "jss:: (JSON field name validation), std::unordered_map (type/uniqueness), UNREACHABLE macro (error enforcement) validates Map key uniqueness via std::unordered_map automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Type correctness via C++ type system", + "validation", + "missing", + "check" + ], + "evidence": "Field Type correctness via C++ type system validated by jss:: (JSON field name validation), std::unordered_map (type/uniqueness), UNREACHABLE macro (error enforcement)", + "issue_pattern": "Missing validation for Type correctness via C++ type system", + "why_false_positive": "jss:: (JSON field name validation), std::unordered_map (type/uniqueness), UNREACHABLE macro (error enforcement) validates Type correctness via C++ type system automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "labels (set of char const*)", + "empty", + "string", + "validation" + ], + "evidence": "std::unordered_map::emplace + UNREACHABLE macro at PerfLogImp::Counters::Counters (constructor)", + "issue_pattern": "Missing empty string validation for labels (set of char const*)", + "why_false_positive": "std::unordered_map::emplace + UNREACHABLE macro validates labels (set of char const*) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "jobTypes (JobTypes map)", + "empty", + "string", + "validation" + ], + "evidence": "std::unordered_map::emplace + UNREACHABLE macro at PerfLogImp::Counters::Counters (constructor)", + "issue_pattern": "Missing empty string validation for jobTypes (JobTypes map)", + "why_false_positive": "std::unordered_map::emplace + UNREACHABLE macro validates jobTypes (JobTypes map) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "proc.second.value.started, finished, errored (uint32_t counters)", + "empty", + "string", + "validation" + ], + "evidence": "explicit check in for loop at PerfLogImp::Counters::countersJson", + "issue_pattern": "Missing empty string validation for proc.second.value.started, finished, errored (uint32_t counters)", + "why_false_positive": "explicit check in for loop validates proc.second.value.started, finished, errored (uint32_t counters) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "Json field names (jss::started, jss::finished, etc.)", + "empty", + "string", + "validation" + ], + "evidence": "jss:: namespace (likely macro or constant validation) at PerfLogImp::Counters::countersJson", + "issue_pattern": "Missing empty string validation for Json field names (jss::started, jss::finished, etc.)", + "why_false_positive": "jss:: namespace (likely macro or constant validation) validates Json field names (jss::started, jss::finished, etc.) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Json field names (jss::started, jss::finished, etc.)", + "format", + "validation", + "invalid" + ], + "evidence": "jss:: namespace (likely macro or constant validation) at PerfLogImp::Counters::countersJson", + "issue_pattern": "Missing format validation for Json field names (jss::started, jss::finished, etc.)", + "why_false_positive": "jss:: namespace (likely macro or constant validation) validates Json field names (jss::started, jss::finished, etc.) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "rpcobj[proc.first] (label as JSON key)", + "empty", + "string", + "validation" + ], + "evidence": "std::unordered_map key type (std::string) at PerfLogImp::Counters::countersJson", + "issue_pattern": "Missing empty string validation for rpcobj[proc.first] (label as JSON key)", + "why_false_positive": "std::unordered_map key type (std::string) validates rpcobj[proc.first] (label as JSON key) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "rpcobj[proc.first] (label as JSON key)", + "type", + "validation", + "check" + ], + "evidence": "std::unordered_map key type (std::string) at PerfLogImp::Counters::countersJson", + "issue_pattern": "Missing type validation for rpcobj[proc.first] (label as JSON key)", + "why_false_positive": "std::unordered_map key type (std::string) validates rpcobj[proc.first] (label as JSON key) type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/perflog/detail/PerfLogImp.cpp", + "functions": [ + { + "args": [ + "labels", + "jobTypes" + ], + "lineno": 15, + "name": "PerfLogImp::Counters::Counters" + }, + { + "args": [], + "lineno": 38, + "name": "PerfLogImp::Counters::countersJson" + }, + { + "args": [], + "lineno": 99, + "name": "PerfLogImp::Counters::currentJson" + }, + { + "args": [], + "lineno": 143, + "name": "PerfLogImp::openLog" + }, + { + "args": [], + "lineno": 170, + "name": "PerfLogImp::run" + }, + { + "args": [], + "lineno": 191, + "name": "PerfLogImp::report" + }, + { + "args": [ + "setup", + "app", + "journal", + "signalStop" + ], + "lineno": 217, + "name": "PerfLogImp::PerfLogImp" + }, + { + "args": [], + "lineno": 224, + "name": "PerfLogImp::~PerfLogImp" + }, + { + "args": [ + "method", + "requestId" + ], + "lineno": 228, + "name": "PerfLogImp::rpcStart" + }, + { + "args": [ + "method", + "requestId", + "finish" + ], + "lineno": 245, + "name": "PerfLogImp::rpcEnd" + }, + { + "args": [ + "type" + ], + "lineno": 273, + "name": "PerfLogImp::jobQueue" + }, + { + "args": [ + "type", + "dur", + "startTime", + "instance" + ], + "lineno": 285, + "name": "PerfLogImp::jobStart" + }, + { + "args": [ + "type", + "dur", + "instance" + ], + "lineno": 304, + "name": "PerfLogImp::jobFinish" + }, + { + "args": [ + "resize" + ], + "lineno": 320, + "name": "PerfLogImp::resizeJobs" + }, + { + "args": [], + "lineno": 327, + "name": "PerfLogImp::rotate" + }, + { + "args": [], + "lineno": 335, + "name": "PerfLogImp::start" + }, + { + "args": [], + "lineno": 341, + "name": "PerfLogImp::stop" + }, + { + "args": [ + "section", + "configDir" + ], + "lineno": 355, + "name": "setup_PerfLog" + }, + { + "args": [ + "setup", + "app", + "journal", + "signalStop" + ], + "lineno": 372, + "name": "make_PerfLog" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 14, + "name": "perf" + } + ], + "test_coverage_notes": "The code uses UNREACHABLE macros for validation failures, which are typically excluded from coverage (LCOV_EXCL_START/STOP). The explicit checks for non-zero counters are likely covered by tests that exercise RPC/job queue activity. However, edge cases such as duplicate labels/jobTypes or empty input sets may not be directly tested unless there are negative/exception tests. Test files likely to cover this code would be in the perflog or rpc subsystem, e.g., test_perflog.cpp, test_PerfLogImp.cpp, or integration tests that exercise performance logging. There may be gaps in testing for the UNREACHABLE paths and for malformed/invalid input sets.", + "validation_architecture": { + "auto_validated_fields": [ + "JSON field names via jss::", + "Map key uniqueness via std::unordered_map", + "Type correctness via C++ type system" + ], + "framework": "jss:: (JSON field name validation), std::unordered_map (type/uniqueness), UNREACHABLE macro (error enforcement)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE macro (likely aborts or throws)", + "field": "labels (set of char const*)", + "location": "PerfLogImp::Counters::Counters (constructor)", + "validated_by": "std::unordered_map::emplace + UNREACHABLE macro", + "validates": [ + "Ensures each label is unique in the rpc_ map", + "Prevents duplicate insertion of labels" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE macro (likely aborts or throws)", + "field": "jobTypes (JobTypes map)", + "location": "PerfLogImp::Counters::Counters (constructor)", + "validated_by": "std::unordered_map::emplace + UNREACHABLE macro", + "validates": [ + "Ensures each jobType is unique in the jq_ map", + "Prevents duplicate insertion of jobTypes" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "continue (skips entry, no exception)", + "field": "proc.second.value.started, finished, errored (uint32_t counters)", + "location": "PerfLogImp::Counters::countersJson", + "validated_by": "explicit check in for loop", + "validates": [ + "Skips entries where all counters are zero", + "Prevents empty/irrelevant data from being serialized" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (compile-time constant enforcement)", + "field": "Json field names (jss::started, jss::finished, etc.)", + "location": "PerfLogImp::Counters::countersJson", + "validated_by": "jss:: namespace (likely macro or constant validation)", + "validates": [ + "Ensures only valid/expected JSON field names are used" + ], + "validation_type": "format" + }, + { + "confidence": 0.7, + "error_thrown": "N/A (compile-time type enforcement)", + "field": "rpcobj[proc.first] (label as JSON key)", + "location": "PerfLogImp::Counters::countersJson", + "validated_by": "std::unordered_map key type (std::string)", + "validates": [ + "Ensures label is a string suitable for JSON key" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/perflog/detail/PerfLogImp.cpp.ai.md b/src/xrpld/perflog/detail/PerfLogImp.cpp.ai.md new file mode 100644 index 0000000000..1c8549bb73 --- /dev/null +++ b/src/xrpld/perflog/detail/PerfLogImp.cpp.ai.md @@ -0,0 +1,54 @@ +# `PerfLogImp.cpp` — Performance Logging Implementation + +## Purpose and Role + +`PerfLogImp.cpp` provides the concrete implementation of the `PerfLog` interface declared in `include/xrpl/core/PerfLog.h`. Its job is to give the XRPL server operator a time-series view of server internals: how many RPC methods started, finished, or errored; how long jobs waited in the queue before executing; what tasks are running right now; and how the node store is behaving. The output is a stream of JSON lines written to a dedicated log file at a configurable interval (defaulting to one second). + +The file is the only implementation of `PerfLog` in the codebase. It is instantiated via the `make_PerfLog()` factory function and exists for the lifetime of `Application`. + +## Class Structure + +`PerfLogImp` extends `PerfLog` and owns a private `Counters` struct that consolidates all tracking state. `Counters` in turn holds two pre-built lookup maps — `rpc_` keyed by method-name string and `jq_` keyed by `JobType` enum — plus two secondary maps and a vector for tracking in-flight work. The separation lets the hot-path increment methods (`rpcStart`, `jobStart`, etc.) do a single map lookup and a single lock/increment, keeping the critical section minimal. + +The header defines a `Locked` template that bundles a value with its own `std::mutex`. Each entry in `rpc_` and `jq_` is a `Locked` or `Locked`, meaning fine-grained per-entry locking rather than a single global counter lock. This matters because RPC and job queue events arrive on many threads concurrently. + +## Pre-Population and Map Immutability + +The constructor populates `rpc_` from `xrpl::RPC::getHandlerNames()` and `jq_` from `JobTypes::instance()` before any worker threads are spawned. After that, the maps' structure is frozen — keys are never added or removed at runtime. The header comment states this explicitly: *"rpc_ and jq_ do not need mutex protection because all keys and values are created before more threads are started."* This is a deliberate design choice: instead of a read-write lock protecting the whole map, callers only need to lock the individual `Locked` entry they found. Map lookups by key are lock-free and safe because the map itself is never mutated. + +If a caller passes an unrecognized method name or job type — which would be a programming error — the `UNREACHABLE` macro fires. These paths are marked `LCOV_EXCL_START`/`LCOV_EXCL_STOP` because they should never execute in production. + +## RPC Lifecycle Tracking + +`rpcStart(method, requestId)` increments `started` on the per-method entry and inserts a `MethodStart` pair (a `char const*` into the map key and a `steady_clock::now()` timestamp) into `methods_`, keyed by `requestId`. Using a `char const*` pointer rather than copying the string is a small but intentional optimization — the `std::string` key is stable in the unordered_map and will never move, so the pointer is safe and avoids allocation. + +`rpcEnd()` (invoked by the public `rpcFinish()` and `rpcError()` wrappers) looks up and erases the `methods_` entry, captures the start timestamp, then locks the per-entry mutex and increments either `finished` or `errored` and accumulates elapsed microseconds. The `bool finish` parameter is the only branch. Both the `methodsMutex_` and the per-entry counter mutex are released before the function returns. + +## Job Queue Lifecycle + +Job tracking spans three distinct phases: +1. `jobQueue(type)` — increments `queued` when a job enters the queue. +2. `jobStart(type, dur, startTime, instance)` — called when a worker thread picks up the job. `dur` is the already-computed wait time (passed in by the caller rather than recomputed here). The `instance` parameter is the integer slot index of the worker thread and indexes directly into the `jobs_` vector, recording `{type, startTime}` so `currentJson()` can report what each thread is doing right now. +3. `jobFinish(type, dur, instance)` — accumulates running duration and resets the `jobs_[instance]` slot to `{jtINVALID, steady_time_point()}`, signaling that the slot is idle. + +`resizeJobs(resize)` extends the `jobs_` vector when the job queue creates a new worker thread. New slots are pre-filled with `jtINVALID`. Resizing only ever grows; shrinking is not supported because threads are not removed in practice. + +## Background Reporting Thread + +`start()` launches a background thread that runs `run()`, named `"perflog"` via `beast::setCurrentThreadName`. The loop uses `cond_.wait_until()` against `lastLog_ + setup_.logInterval`, meaning it sleeps for the remainder of the interval and wakes either on expiry or when `stop_` is set. This avoids drift accumulation compared to a naive `sleep_for`. + +On each wakeup the loop checks the `rotate_` flag (set by `rotate()`, typically called on SIGHUP). If set, `openLog()` is called to close and reopen the file before `report()` runs. The flag is only tested and cleared inside the locked section of `run()`, ensuring that a `rotate()` call racing with the report cycle is not lost. + +`report()` assembles a JSON object with: ISO wall-clock timestamp, worker count (from `jobs_.size()`), hostname, aggregate counters from `countersJson()`, node store counts via `app_.getNodeStore().getCountsJson()`, currently-running activities from `currentJson()`, and server-state accounting from `app_.getOPs().stateAccounting()`. The assembled object is serialized as a single compact JSON line via `Json::Compact`, then `std::endl` flushes the stream. Each line in the log file is therefore self-contained and parseable independently. + +## Dual-Clock Design + +The implementation deliberately uses two clocks for different purposes. `std::chrono::steady_clock` is used for all duration measurements — it is monotonic and immune to NTP jumps or operator clock adjustments that would corrupt elapsed-time values. `std::chrono::system_clock` is used for the wall-clock timestamp recorded in the log file and for scheduling the `lastLog_` interval. This distinction ensures that duration statistics remain reliable even when system time is adjusted. + +## Log File Management + +`openLog()` is called from the constructor (to open the file immediately) and from `run()` after a rotation signal. If the parent directory does not exist, `boost::filesystem::create_directories` is called. On failure, the fatal journal log is written and `signalStop_()` is invoked to bring the server down rather than silently continuing without performance data. The file is opened in append mode (`std::ios::app`) so rotation does not truncate existing data before an external log manager has processed it. + +## Configuration + +`setup_PerfLog()` reads two keys from the `[perf]` section of `xrpld.cfg`: `perf_log` (a file path, resolved relative to the config directory if not absolute) and `log_interval` (an integer number of seconds). If `perf_log` is empty, the `PerfLogImp` instance still exists but the background thread is never started and `report()` returns immediately — the counters remain valid and can be queried via `countersJson()` / `currentJson()` for the `get_counts` RPC command. \ No newline at end of file diff --git a/src/xrpld/perflog/detail/PerfLogImp.h.ai.json b/src/xrpld/perflog/detail/PerfLogImp.h.ai.json new file mode 100644 index 0000000000..7dbfed1080 --- /dev/null +++ b/src/xrpld/perflog/detail/PerfLogImp.h.ai.json @@ -0,0 +1,105 @@ +{ + "args": [ + { + "lineno": 13, + "name": "T" + }, + { + "lineno": 39, + "name": "Setup" + }, + { + "lineno": 40, + "name": "Application" + }, + { + "lineno": 41, + "name": "beast::Journal" + }, + { + "lineno": 42, + "name": "std::function" + } + ], + "classes": [ + { + "args": [ + "T const& value", + "T&& value", + "Locked const& rhs", + "Locked&& rhs" + ], + "lineno": 13, + "name": "Locked" + }, + { + "args": [ + "Setup const& setup", + "Application& app", + "beast::Journal journal", + "std::function&& signalStop" + ], + "lineno": 38, + "name": "PerfLogImp" + }, + { + "args": [ + "std::set const& labels", + "JobTypes const& jobTypes" + ], + "lineno": 44, + "name": "Counters" + }, + { + "args": [], + "lineno": 51, + "name": "Rpc" + }, + { + "args": [], + "lineno": 61, + "name": "Jq" + } + ], + "description": "Defines the PerfLogImp class, an implementation of PerfLog for tracking and logging performance counters and currently executing tasks in the XRPL application.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/perflog/detail/PerfLogImp.h", + "functions": [ + { + "args": [ + "std::string const& method", + "std::uint64_t const requestId" + ], + "lineno": 97, + "name": "rpcFinish" + }, + { + "args": [ + "std::string const& method", + "std::uint64_t const requestId" + ], + "lineno": 102, + "name": "rpcError" + }, + { + "args": [], + "lineno": 112, + "name": "countersJson" + }, + { + "args": [], + "lineno": 117, + "name": "currentJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "perf" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/perflog/detail/PerfLogImp.h.ai.md b/src/xrpld/perflog/detail/PerfLogImp.h.ai.md new file mode 100644 index 0000000000..40d2953cc2 --- /dev/null +++ b/src/xrpld/perflog/detail/PerfLogImp.h.ai.md @@ -0,0 +1,49 @@ +# `PerfLogImp.h` — Performance Log Concrete Implementation + +`PerfLogImp` is the single concrete implementation of the abstract `PerfLog` interface. It lives in `xrpld/perflog/detail/` (a `detail/` subdirectory, signaling it is not part of the public API), and it is instantiated only through the `make_PerfLog()` factory function defined in the same translation unit. + +Its purpose is twofold: accumulate real-time performance counters for every RPC method and every `JobQueue` task type, and periodically serialize those counters as compact JSON to a dedicated performance log file. Both concerns are handled inside a single background thread driven by a `std::condition_variable`. + +--- + +## `Locked` — A typed mutex box + +The file opens with a small utility template `Locked` that bundles a value of type `T` with a `mutable std::mutex`. This is not a general-purpose smart wrapper (it deliberately omits `operator->` or automatic RAII access); instead it is a naming convention—wherever you see a `Locked` in the counter map, the rule is *always lock the mutex before touching the value*. The copy and move constructors copy or move only the **value**, not the mutex, which is correct: a newly constructed copy starts with an unlocked mutex of its own. + +--- + +## `Counters` — the bookkeeping core + +The nested `Counters` struct owns all runtime state that multiple threads touch: + +**`Rpc` sub-struct** records, per named RPC handler, three counters (`started`, `finished`, `errored`) and a cumulative `duration` in microseconds. The `rpc_` member is an `unordered_map>` keyed by the handler name string. It is populated entirely in the `Counters` constructor from the compile-time set of handler names returned by `xrpl::RPC::getHandlerNames()`, and no keys are ever inserted or removed after that. This **write-once-read-many** structure is the key insight: because the map topology is frozen before any worker threads start, `rpc_.find()` is safe without a map-level lock. Only the per-entry `Locked` mutex is needed when mutating a counter. + +**`Jq` sub-struct** does the same for job queue tasks, tracking `queued`, `started`, `finished`, and two duration accumulators—how long a job waited in the queue versus how long it ran. The `jq_` map is similarly pre-populated from `JobTypes::instance()` before threads launch. + +**`jobs_`** is a `vector>` sized by the number of `JobQueue` worker threads (via `resizeJobs()`). Each slot corresponds to one worker by its `instance` index; a running slot holds the job type and when it started, an idle slot holds `jtINVALID`. This vector is protected by a dedicated `jobsMutex_`, separate from the per-entry mutexes. + +**`methods_`** is an `unordered_map` tracking in-flight RPC requests by their `requestId`. The value is a `(char const*, steady_time_point)` pair recording the method name pointer and start time. It uses a separate `methodsMutex_`. Erasing the entry at completion avoids unbounded growth and provides the start timestamp needed to compute call duration. + +--- + +## `PerfLogImp` — the background reporter + +The class holds a `Setup` (log file path and interval), an `Application&` reference, a `beast::Journal`, a `std::function` called `signalStop_` for fatal-error escalation, and the `Counters` instance. The log file itself is a plain `std::ofstream logFile_`. + +**Thread lifecycle.** `start()` spawns a thread running `run()`. `run()` loops, sleeping on a `condition_variable` until either the log interval elapses or `stop_` or `rotate_` is set. The sleep uses `wait_until` with a deadline anchored to `lastLog_ + setup_.logInterval`, so drift is bounded. `stop()` sets `stop_ = true`, notifies the condition variable, and joins the thread—ensuring clean shutdown even in the destructor, which calls `stop()` unconditionally. + +**Log rotation.** UNIX daemons rotate logs by sending a signal. `rotate()` sets `rotate_ = true` and wakes the thread via `cond_.notify_one()`. The thread checks this flag, calls `openLog()` (which closes and re-opens the file in append mode, creating parent directories if needed), then clears the flag. This is the standard signal-safe rotation pattern. + +**Fatal file errors.** If `openLog()` cannot create the log directory or open the file, it calls `signalStop_()`, triggering a graceful application shutdown. A `PerfLog` that cannot write is effectively broken, and the server should not silently continue with a dead performance subsystem. + +**Reporting.** `report()` builds a single JSON document containing a timestamp, the worker count (`jobs_.size()`), hostname, cumulative counters from `countersJson()`, a snapshot of currently executing jobs and methods from `currentJson()`, node store statistics, and server state accounting. It writes a single compact JSON line followed by `std::endl` (which flushes the stream, ensuring each record reaches disk atomically with respect to log rotation). + +--- + +## Concurrency design + +The locking strategy is deliberately tiered. The `rpc_` and `jq_` maps are structurally frozen—no map-level lock is needed for lookups, only for per-entry counter mutation. The `jobs_` vector uses one coarse mutex because its entire content is copied for a `currentJson()` snapshot, minimizing lock contention on the hot path. The `methods_` map uses its own mutex because insertions and deletions happen concurrently across request threads. + +The `run()` loop's `mutex_`/`cond_` pair is used only to synchronize the `stop_` and `rotate_` flags with the background thread—it is never held while doing I/O, keeping the critical section minimal. + +`rpcFinish()` and `rpcError()` are thin inline wrappers over the private `rpcEnd()` helper, which captures the boolean `finish` flag. This avoids duplicating the lookup-and-erase logic for `methods_` and the counter mutation for `rpc_`. \ No newline at end of file diff --git a/src/xrpld/rpc/BookChanges.h.ai.json b/src/xrpld/rpc/BookChanges.h.ai.json new file mode 100644 index 0000000000..0be10106ad --- /dev/null +++ b/src/xrpld/rpc/BookChanges.h.ai.json @@ -0,0 +1,61 @@ +{ + "args": [ + { + "lineno": 22, + "name": "lpAccepted" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "Value" + }, + { + "args": [], + "lineno": 16, + "name": "ReadView" + }, + { + "args": [], + "lineno": 17, + "name": "Transaction" + }, + { + "args": [], + "lineno": 18, + "name": "TxMeta" + }, + { + "args": [], + "lineno": 19, + "name": "STTx" + } + ], + "description": "Defines a function to compute and summarize changes to order books (book changes) in an XRPL ledger, extracting volume and rate information for affected offers, and returning the result as a JSON object.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/BookChanges.h", + "functions": [ + { + "args": [ + "lpAccepted" + ], + "lineno": 22, + "name": "computeBookChanges" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 10, + "name": "Json" + }, + { + "lineno": 15, + "name": "xrpl" + }, + { + "lineno": 21, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/BookChanges.h.ai.md b/src/xrpld/rpc/BookChanges.h.ai.md new file mode 100644 index 0000000000..9edf8219b8 --- /dev/null +++ b/src/xrpld/rpc/BookChanges.h.ai.md @@ -0,0 +1,50 @@ +# `BookChanges.h` — Per-Ledger Order Book Change Aggregator + +## Role in the System + +`BookChanges.h` provides the single template function `computeBookChanges`, which scans every transaction in a closed ledger, extracts offer-crossing activity from transaction metadata, and aggregates the results into OHLCV-style (Open, High, Low, Close, Volume) market data per currency pair. The output drives two distinct consumption paths: the `book_changes` WebSocket subscription stream (pushed to subscribers on every validated ledger in `NetworkOPs.cpp`) and the `book_changes` RPC endpoint (on-demand via `handlers/orderbook/BookChanges.cpp`). + +## Why a Header-Only Template + +The function is parameterized over `L`, the ledger type, rather than being bound to a concrete `ReadView`. This means it compiles against any type that exposes `.txs` (an iterable of `(STTx, TxMeta)` pairs) and `.header()` (a struct with `seq`, `hash`, `validated`, and `closeTime`). The design avoids introducing a virtual dispatch layer and allows the same logic to work with both production ledger objects and lightweight test fixtures without an explicit interface hierarchy. + +## How Offer Activity Is Extracted + +The function iterates over the `sfAffectedNodes` array in each transaction's metadata. Several filters are applied aggressively before any computation: + +- Only nodes with `sfLedgerEntryType == ltOFFER` are considered; all other on-ledger objects are ignored. +- `sfCreatedNode` entries are skipped. A freshly created offer that wasn't crossed has no volume yet. +- Nodes missing either `sfFinalFields` or `sfPreviousFields` are skipped. Without both snapshots, the delta cannot be computed. The code comments note this is typical of *cancelled* offers where no actual crossing occurred, so skipping them is semantically correct. +- Offers deleted by an explicit `OfferCancel` transaction (or by `sfOfferSequence` in an `OfferCreate`) are filtered out by comparing the offer's `sfSequence` against the cancel target. This avoids attributing volume to a removal that had no economic activity. + +For nodes that pass all filters, the volume delta is `finalFields.sfTakerGets - previousFields.sfTakerGets` (and similarly for `sfTakerPays`). Because `FinalFields` reflects the state after the transaction and `PreviousFields` the state before, the difference precisely captures how much of each side was consumed by crossing. + +## Canonical Pair Key and Rate Convention + +The tally map is keyed by a string like `"XRP_drops|USD/rGateway"` or `"USD/rA|BTC/rB"`. The ordering rule is: XRP always occupies the first position; if neither asset is XRP, the lexicographically smaller asset string comes first. This canonical ordering, controlled by the `noswap` boolean, means a single book is represented by exactly one key regardless of which direction individual offers were placed. Both sides of each trade contribute to the same accumulator. + +The exchange rate is `divide(first, second, noIssue())`. Since a rate is a dimensionless ratio between two amounts, no real issuer context applies — `noIssue()` (a static sentinel with `noCurrency()` / `noAccount()`) satisfies the `STAmount::divide` interface while making clear the result is not attributable to any specific IOU issuer. + +## OHLCV Tally Structure + +Each entry in the `tally` map is a 7-element tuple: + +| Index | Meaning | +|---|---| +| 0 | Side A cumulative volume | +| 1 | Side B cumulative volume | +| 2 | Highest rate seen (H) | +| 3 | Lowest rate seen (L) | +| 4 | Rate of the first trade (O) — set once, never updated | +| 5 | Rate of the most recent trade (C) — overwritten each iteration | +| 6 | Optional `uint256` domain ID | + +Open and close reflect transaction order within the ledger as iterated; they are not timestamps. The domain field supports the permissioned DEX extension where offers can be tagged with a `sfDomainID`, enabling market data to be segregated by permissioned pool. The domain of the *last* processed trade for a given pair wins, which is consistent since all offers in one permissioned book share the same domain. + +## JSON Output Shape + +The returned `Json::Value` contains `type`, `validated`, `ledger_index`, `ledger_hash`, `ledger_time`, and a `changes` array. Each element of `changes` carries `currency_a` / `currency_b` (for classic IOU/XRP pairs) or `mpt_issuance_id_a` / `mpt_issuance_id_b` (for MPT-based pairs), along with `volume_a`, `volume_b`, `high`, `low`, `open`, `close`, and an optional `domain`. The asset-type dispatch uses `STAmount::asset().visit(...)` with two lambda overloads, keeping IOU and MPT serialization paths cleanly separated without a type-tag check. + +## Defensive Patterns and Invariants + +The zero-division guard (`if (second == beast::zero) continue`) prevents a pathological rate computation if metadata is somehow malformed. The comment labels it "defensively programmed, should (probably) never happen" — the hedge is honest, since offer crossing logic in the transaction engine should never produce a zero-pays delta alongside a non-zero-gets delta, but the metadata consumer has no way to enforce that invariant independently. Similarly, the final absolute-value normalization (`if (first < beast::zero) first = -first`) corrects for deltas that come out negative when the subtraction order produces a negative intermediate. \ No newline at end of file diff --git a/src/xrpld/rpc/CTID.h.ai.json b/src/xrpld/rpc/CTID.h.ai.json new file mode 100644 index 0000000000..445197a9b6 --- /dev/null +++ b/src/xrpld/rpc/CTID.h.ai.json @@ -0,0 +1,52 @@ +{ + "args": [ + { + "lineno": 23, + "name": "ledgerSeq" + }, + { + "lineno": 24, + "name": "txnIndex" + }, + { + "lineno": 25, + "name": "networkID" + }, + { + "lineno": 44, + "name": "ctid" + } + ], + "classes": [], + "description": "Provides functions to encode and decode Concise Transaction IDs (CTIDs) as specified in XLS-15d, allowing conversion between ledger sequence, transaction index, network ID, and their compact hexadecimal representation.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/CTID.h", + "functions": [ + { + "args": [ + "ledgerSeq", + "txnIndex", + "networkID" + ], + "lineno": 19, + "name": "encodeCTID" + }, + { + "args": [ + "ctid" + ], + "lineno": 39, + "name": "decodeCTID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/CTID.h.ai.md b/src/xrpld/rpc/CTID.h.ai.md new file mode 100644 index 0000000000..f8c4d3eb1f --- /dev/null +++ b/src/xrpld/rpc/CTID.h.ai.md @@ -0,0 +1,39 @@ +## CTID.h — Concise Transaction ID Encoding/Decoding + +This header implements the **Concise Transaction ID (CTID)** specification from [XLS-15d](https://github.com/XRPLF/XRPL-Standards/discussions/34), a compact encoding that uniquely identifies a transaction by its position in a ledger *on a specific network*. A bare transaction hash says nothing about which network it appeared on or where within a ledger it lives — CTID packs all three coordinates (ledger sequence, transaction index, network ID) into a single 16-character hex string that is easier to communicate and validate than a 64-character hash. + +### Binary Layout + +The CTID is a 64-bit value formatted as 16 uppercase hex digits: + +``` +[4 bits: 0xC] [28 bits: ledgerSeq] [16 bits: txnIndex] [16 bits: networkID] +``` + +The top nibble is always `C` (binary `1100`), acting as a magic prefix that distinguishes a CTID from arbitrary hex strings and enables fast structural validation. During encoding the constant `0xC000'0000ULL` is added to the ledger sequence before shifting it into the high 32 bits. Any valid CTID therefore begins with `C`, so decoders can immediately reject non-CTID values by masking with `0xF000'0000'0000'0000` and comparing against `0xC000'0000'0000'0000`. The practical capacity is ~268 million ledgers, 65,536 transactions per ledger, and 65,536 distinct networks. + +### `encodeCTID` + +`encodeCTID` is `inline` and `noexcept`, returning `std::optional` to signal failure without exceptions. Range checks are explicit upfront: ledger sequence is capped at 28 bits (`0x0FFF'FFFF`), while `txnIndex` and `networkID` are each bounded to 16 bits. Any out-of-range input returns `std::nullopt` immediately — the right choice over silent truncation, which would silently produce a CTID pointing to the wrong transaction. + +The output uses `std::stringstream` with `std::hex`, `std::uppercase`, `std::setw(16)`, and `std::setfill('0')` to guarantee exactly 16 characters regardless of leading zeros. This zero-padding is load-bearing: the decoder validates a fixed 16-character length as its first step. + +### `decodeCTID` + +The template design avoids separate overloads for each string-like type. A single function body uses `if constexpr` to dispatch between string types (`std::string`, `std::string_view`, `char*`, `const char*`) and integral types. Unsupported types fall through to `return std::nullopt`, making the template safe to instantiate with unexpected types without a compile error. + +For string inputs, two guards fire before any numeric conversion: a length check (exactly 16 characters) and a `boost::regex_match` against `^[0-9A-Fa-f]{16}$`. The `std::stoull` call that follows is wrapped in a `try/catch` block annotated with `LCOV_EXCL_START/STOP` — an explicit acknowledgment that this exception path is theoretically unreachable given the prior validation, but guarded anyway for defensive correctness. Using `boost::regex` rather than `std::regex` is consistent with the broader XRPL codebase and avoids the historically poor performance of several `std::regex` implementations. The `` header included at the top is not used directly and appears to be a vestige. + +For integral inputs the cast to `uint64_t` is direct, and the same prefix mask check applies. This centralizes the structural invariant after type-specific parsing rather than duplicating it per branch. + +The return type `std::optional>` reflects actual bit widths precisely: ledger sequence returns as `uint32_t` (28 bits fit) while `txnIndex` and `networkID` return as `uint16_t`, enabling compile-time range reasoning by callers. + +### Call Sites + +In `Tx.cpp` (the `tx` RPC handler), `encodeCTID` is called on outbound transaction responses once the transaction is confirmed in a validated ledger — reading ledger sequence, transaction index from `sfTransactionIndex`, and the node's network ID from the application-level `NetworkIDService`. The result populates the `ctid` field in the JSON response. On inbound requests, `decodeCTID` parses a caller-supplied `ctid` parameter to extract the ledger and index needed for lookup, after first verifying the decoded network ID matches the local node's ID (to reject CTIDs from other networks). + +In `Transaction.cpp` (the `Transaction` class's `getJson` path), CTID encoding similarly gates on having a validated ledger index and a transaction sequence. Notably, if the serialized transaction carries an `sfNetworkID` field, that value overrides the local node's network ID when computing the CTID — supporting cross-network transaction objects that carry their origin network identity inline. + +### Design Characteristics + +The header is fully self-contained: its only dependencies are `boost/regex.hpp`, ``, and ``. It carries no ledger, session, or application state, making it safe to include from any layer without dragging in heavy subsystem headers. Both functions are `inline` header-only implementations, appropriate given their small size and the template requirement for `decodeCTID`. The `noexcept` specification is honest: both functions guarantee no exceptions will escape, handling all failure modes through `std::nullopt` returns. \ No newline at end of file diff --git a/src/xrpld/rpc/Context.h.ai.json b/src/xrpld/rpc/Context.h.ai.json new file mode 100644 index 0000000000..36a1303685 --- /dev/null +++ b/src/xrpld/rpc/Context.h.ai.json @@ -0,0 +1,41 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 15, + "name": "Context" + }, + { + "args": [], + "lineno": 28, + "name": "JsonContext" + }, + { + "args": [], + "lineno": 31, + "name": "JsonContext::Headers" + }, + { + "args": [ + "RequestType" + ], + "lineno": 41, + "name": "GRPCContext" + } + ], + "description": "Defines context structures for handling RPC calls in the XRPL application, including base context, JSON-specific context, and gRPC-specific context.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/Context.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/Context.h.ai.md b/src/xrpld/rpc/Context.h.ai.md new file mode 100644 index 0000000000..40062ae042 --- /dev/null +++ b/src/xrpld/rpc/Context.h.ai.md @@ -0,0 +1,45 @@ +# `src/xrpld/rpc/Context.h` — RPC Call Context + +## Role in the System + +`Context.h` defines the dependency bundle that every RPC handler receives. Rather than threading individual subsystem references through every function signature, the XRPL RPC layer packages all shared execution state into a single aggregate struct and passes it as the handler's sole input for infrastructure concerns. This is the classic "context object" pattern, applied here at the boundary between the HTTP/gRPC transport layer and the per-command handler implementations. + +## The Base `Context` Struct + +`Context` holds everything a handler could need to interact with the running node: + +- **`app`** — a reference to the top-level `Application` singleton, the root of the node's service graph (wallet, config, database, etc.). +- **`netOps`** — a `NetworkOPs` reference used to query network and consensus state, check operating mode, and submit transactions. +- **`ledgerMaster`** — exposes ledger history, the current open ledger, and validated ledger state. +- **`loadType`** — a `Resource::Charge` that the handler updates to signal how much resource cost the call should incur. It is populated by the caller before dispatch and read back after to assess usage fees, making it an *in-out* field despite being a plain reference. +- **`consumer`** — the `Resource::Consumer` record for the calling client, used by the resource management layer to track and enforce per-client rate limits. +- **`role`** — the `Role` enum value (`GUEST`, `USER`, `IDENTIFIED`, `ADMIN`, `PROXY`, or `FORBID`) resolved from port config and request credentials before the context is constructed. Handlers use it to gate admin-only operations. +- **`coro`** — an optional `shared_ptr` to a `JobQueue::Coro`. When set, this allows long-running handlers to yield execution back to the job queue cooperatively, avoiding thread starvation. Most handlers leave this null. +- **`infoSub`** — an optional `InfoSub::pointer` that, when set, represents an open WebSocket session. Subscription-oriented handlers (`subscribe`, `unsubscribe`) use it to register or deregister the session for event feeds. +- **`apiVersion`** — an unsigned integer encoding the client's requested API version. Handlers use this to shape response formats and error codes — for example, `conditionMet()` in `Handler.h` returns `rpcNO_NETWORK` on v1 but `rpcNOT_SYNCED` on later versions. +- **`j`** — a `beast::Journal` for structured logging. Stored by value (journals are cheap handles) rather than by pointer. + +All members are non-owning references or lightweight handles. `Context` itself owns nothing and has no lifecycle implications — it is purely a view into resources owned elsewhere. + +## Protocol-Specific Subtypes + +### `JsonContext` + +`JsonContext` extends `Context` for the JSON-RPC path (HTTP and WebSocket). It adds two fields: + +- **`params`** — the parsed `Json::Value` carrying the request's parameter object. +- **`headers`** — a nested `Headers` struct holding `user` and `forwardedFor` as `std::string_view`. These are sourced from HTTP headers set by upstream proxies (e.g., a `secure_gateway` acting as an authenticating proxy). Using `string_view` here is intentional — the views refer into the longer-lived HTTP request buffer that exists for the duration of the call, avoiding a copy. + +In `ServerHandler.cpp`, a `JsonContext` is aggregate-initialized with a brace-enclosed `Context` base followed by `params` and `headers`. The three-level initialization `{ {base...}, params, {user, fwdFor} }` mirrors the struct's inheritance layout exactly. + +Every JSON-RPC handler signature is typed as `Status(JsonContext&, Json::Value&)` (see `Handler::Method` in `Handler.h`). The `JsonContext` is the primary dispatch vehicle; `Handler.h`'s `conditionMet()` template accepts any `T` that exposes the `Context` fields, so it works transparently for both subtypes. + +### `GRPCContext` + +`GRPCContext` is a simple class template that extends `Context` with a single typed `params` field holding the decoded protobuf request object. The template parameter `RequestType` corresponds to the generated protobuf message type for each gRPC method (e.g., `org::xrpl::rpc::v1::GetLedgerRequest`). This keeps the gRPC handlers strongly typed while sharing all the infrastructure plumbing from the base `Context`. + +## Design Observations + +The split between `Context` and its two subtypes reflects a deliberate separation of transport concerns. The base struct captures everything that is protocol-neutral (node state, authorization, resource management), while the subtypes carry only what differs between JSON-over-HTTP and binary protobuf. This means infrastructure utilities like `conditionMet()` and `isUnlimited()` can be written against the base without needing to know about request parameters at all. + +The choice to use references rather than pointers for `app`, `netOps`, and `ledgerMaster` is a deliberate invariant: a `Context` cannot be constructed with null subsystems. This is appropriate because context objects are always created in the hot path inside a running server — the subsystems are guaranteed live by the time any RPC call can arrive. \ No newline at end of file diff --git a/src/xrpld/rpc/DeliveredAmount.h.ai.json b/src/xrpld/rpc/DeliveredAmount.h.ai.json new file mode 100644 index 0000000000..7b589bfad6 --- /dev/null +++ b/src/xrpld/rpc/DeliveredAmount.h.ai.json @@ -0,0 +1,63 @@ +{ + "args": [], + "classes": [], + "description": "Provides functions to insert and retrieve the delivered amount for payment and check cash transactions in XRPL, adding a 'delivered_amount' field to transaction metadata if applicable.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/DeliveredAmount.h", + "functions": [ + { + "args": [ + "meta", + "ReadView const&", + "std::shared_ptr const&", + "TxMeta const&" + ], + "lineno": 23, + "name": "insertDeliveredAmount" + }, + { + "args": [ + "meta", + "RPC::JsonContext const&", + "std::shared_ptr const&", + "TxMeta const&" + ], + "lineno": 29, + "name": "insertDeliveredAmount" + }, + { + "args": [ + "meta", + "RPC::JsonContext const&", + "std::shared_ptr const&", + "TxMeta const&" + ], + "lineno": 33, + "name": "insertDeliveredAmount" + }, + { + "args": [ + "RPC::Context const& context", + "std::shared_ptr const& serializedTx", + "TxMeta const& transactionMeta", + "LedgerIndex const& ledgerIndex" + ], + "lineno": 37, + "name": "getDeliveredAmount" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "Json" + }, + { + "lineno": 12, + "name": "xrpl" + }, + { + "lineno": 19, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/DeliveredAmount.h.ai.md b/src/xrpld/rpc/DeliveredAmount.h.ai.md new file mode 100644 index 0000000000..4c7a074c29 --- /dev/null +++ b/src/xrpld/rpc/DeliveredAmount.h.ai.md @@ -0,0 +1,37 @@ +# `DeliveredAmount.h` — Delivered Amount RPC Utilities + +## Role in the System + +This header declares the public interface for computing and injecting the `delivered_amount` field into transaction metadata JSON responses. It exists because the XRP Ledger has a historical correctness problem with payment transactions: the `Amount` field on a `Payment` (or `CheckCash`) transaction records the *intended* delivery, not the *actual* delivery. When the `tfPartialPayment` flag is set, the ledger may settle for less than the stated `Amount`. Without a separate, authoritative record of what was actually delivered, downstream consumers — exchanges, wallets, compliance systems — cannot determine the true transfer size from the `Amount` field alone. + +The `DeliveredAmount` field in `TxMeta` was added to the protocol starting at ledger sequence **4594095** (January 24, 2014) to record the actual delivery. This module's job is to bridge the gap: given a transaction and its metadata, produce the correct `delivered_amount` to embed in the RPC response JSON. + +## The Three-Tier Source Resolution + +The implementation in `detail/DeliveredAmount.cpp` follows a three-tier fallback to find the delivered amount: + +1. **TxMeta field present**: If `TxMeta::getDeliveredAmount()` returns a value, that is authoritative. This is the normal case for all ledgers after 4594095. + +2. **Ledger is confirmed post-deployment**: If the `DeliveredAmount` field is absent from metadata but the ledger index is ≥ 4594095, *or* the ledger closed after timestamp 446000000s (February 2014), then its absence is meaningful — it means the full `Amount` was delivered. The `Amount` field is returned. + +3. **Pre-deployment ledger**: If neither condition holds, the delivered amount genuinely cannot be determined. The string `"unavailable"` is written into the JSON, a sentinel that intentionally cannot be parsed into a valid `STAmount`, preventing consumers from misinterpreting it. + +This logic is gated by the `canHaveDeliveredAmount()` helper (internal to the `.cpp`), which ensures only `ttPAYMENT`, `ttCHECK_CASH`, and `ttACCOUNT_DELETE` transactions that completed with `tesSUCCESS` go through the resolution path at all. + +## Overload Design + +The header exposes three overloads of `insertDeliveredAmount` to serve two distinct call sites: + +The **`ReadView const&` overload** is used by `LedgerToJson.cpp` during full-ledger serialization. Here the ledger view is already in hand, so the sequence number and close time are read directly from `ledger.header()`. No `LedgerMaster` lookup is needed. + +The **`RPC::JsonContext const&` overloads** are used by the `tx` and `account_tx` RPC handlers. In this path the ledger sequence is derived from `TxMeta::getLgrSeq()`, and the close time must be fetched lazily from `context.ledgerMaster.getCloseTimeBySeq()`. The two variants differ only in whether the transaction is wrapped in an application-level `Transaction` object or exposed as a raw `STTx const`; the former simply delegates to the latter by calling `transaction->getSTransaction()`. + +The lazy-evaluation design — both the `getLedgerIndex` and `getCloseTime` arguments are passed as callables in the template-based private implementation — avoids the `LedgerMaster` lookup entirely when `TxMeta` already carries the `DeliveredAmount` field, which is the common case for modern ledgers. + +## `getDeliveredAmount` — The Non-Mutating Accessor + +`getDeliveredAmount(RPC::Context const&, shared_ptr, TxMeta const&, LedgerIndex const&)` returns an `std::optional` for callers that need the value rather than side-effecting a JSON object. It takes the base `RPC::Context` (not `JsonContext`) because it does not need the request parameters — only `context.ledgerMaster` for the lazy close-time lookup. This overload is exposed for consumers like the `Simulate` RPC handler that needs to reason about the delivered amount without building the full JSON response inline. + +## Relationship to Protocol + +The header pulls in `xrpl/protocol/Protocol.h` for `LedgerIndex` and `xrpl/protocol/STAmount.h` for the return type of `getDeliveredAmount`. Everything else (`ReadView`, `Transaction`, `TxMeta`, `STTx`) is forward-declared, keeping compile-time coupling minimal. The actual inclusion of heavy ledger and app headers is deferred entirely to the `.cpp` implementation. \ No newline at end of file diff --git a/src/xrpld/rpc/GRPCHandlers.h.ai.json b/src/xrpld/rpc/GRPCHandlers.h.ai.json new file mode 100644 index 0000000000..d220df2933 --- /dev/null +++ b/src/xrpld/rpc/GRPCHandlers.h.ai.json @@ -0,0 +1,43 @@ +{ + "args": [], + "classes": [], + "description": "This file declares gRPC handler functions for XRPL ledger-related RPCs, each taking a GRPCContext with a specific protobuf request type and returning a response and gRPC status.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/GRPCHandlers.h", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 17, + "name": "doLedgerGrpc" + }, + { + "args": [ + "context" + ], + "lineno": 20, + "name": "doLedgerEntryGrpc" + }, + { + "args": [ + "context" + ], + "lineno": 23, + "name": "doLedgerDataGrpc" + }, + { + "args": [ + "context" + ], + "lineno": 26, + "name": "doLedgerDiffGrpc" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/GRPCHandlers.h.ai.md b/src/xrpld/rpc/GRPCHandlers.h.ai.md new file mode 100644 index 0000000000..d6961a5eae --- /dev/null +++ b/src/xrpld/rpc/GRPCHandlers.h.ai.md @@ -0,0 +1,44 @@ +# `GRPCHandlers.h` — gRPC Ledger Handler Declarations + +This header is the public interface for XRPL's gRPC API surface. It declares four handler functions that together expose the node's ledger state over Protocol Buffers/gRPC, forming a structured alternative to the traditional JSON-RPC endpoints. Where JSON-over-HTTP serves browser clients and scripts, the gRPC path is intended for high-throughput machine clients — sidechains, data indexers, and sync services — that benefit from typed schemas and binary serialization. + +## The Handler Contract + +Every function follows an identical signature pattern: + +```cpp +std::pair +doXxxGrpc(RPC::GRPCContext& context); +``` + +The return value bundles the protobuf response with a `grpc::Status`. This is a deliberate design choice spelled out in the file's comment: **if the status is not `OK`, only the status is forwarded to the client and the response object is discarded**. This means implementations can construct a partial response without risk — they simply return early with an error status and a default-constructed response, as seen in `doLedgerDiffGrpc` where each failure path does exactly `return {response, errorStatus}`. + +## `GRPCContext` — The Context Template + +The handlers accept `RPC::GRPCContext`, defined in `Context.h`: + +```cpp +template +struct GRPCContext : public Context +{ + RequestType params; +}; +``` + +`GRPCContext` extends the base `Context` struct — which carries the `Application` reference, `LedgerMaster`, `NetworkOPs`, resource consumer/charge, `Role`, coroutine handle, and `beast::Journal` — by adding a `params` member of the concrete protobuf request type. This mirrors `JsonContext`, which extends the same `Context` base but holds `Json::Value params` instead. Both JSON and gRPC handlers therefore share the same infrastructure for role checking, load accounting, and coroutine dispatch, differing only in how their parameters arrive. + +## The Four Handlers + +**`doLedgerGrpc`** returns full ledger header information for a specified ledger sequence or hash. This is the gRPC equivalent of the `ledger` JSON-RPC command. + +**`doLedgerEntryGrpc`** fetches a single ledger state object by its key. Clients use this to retrieve account root objects, offers, trust lines, and other `SLE` types without pulling the entire state map. + +**`doLedgerDataGrpc`** returns a paginated slice of the ledger's state map — the raw SHAMap leaves in serialized form. This is the primary mechanism for external services to perform full ledger replication, one page at a time. + +**`doLedgerDiffGrpc`** computes the difference between two ledgers by comparing their state SHAMaps. The implementation in `LedgerDiff.cpp` calls `baseLedger->stateMap().compare(desiredLedger->stateMap(), differences, maxDifferences)` and encodes each changed, added, or deleted object into the response. This is purpose-built for light clients and clio-style indexers that need to track incremental ledger mutations without replaying the full transaction set. + +## Wiring into the Server + +The declarations here are consumed by `GRPCServer.h` and `GRPCServer.cpp`, which implement the asynchronous gRPC completion-queue event loop. `GRPCServerImpl::CallData` is a template that stores a `Handler` — a `std::function` pointing to one of these four declarations. When a request arrives on the completion queue, `CallData::process()` posts a coroutine job to the `JobQueue`, constructs a `GRPCContext` by populating all `Context` fields from the server's `Application`, and invokes the stored handler. The result's `grpc::Status` drives whether `responder_.Finish(result, status, ...)` or `responder_.FinishWithError(status, ...)` is called, enforcing the error-suppresses-response contract at the transport layer rather than in each handler. + +Resource exhaustion, role checks, and condition preconditions (e.g., synced-node requirements) are evaluated in `CallData::process()` before the handler is ever called, so the handlers themselves can assume the request is already authorized and the node is in a valid state to serve it. \ No newline at end of file diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h.ai.json b/src/xrpld/rpc/MPTokenIssuanceID.h.ai.json new file mode 100644 index 0000000000..89569ca240 --- /dev/null +++ b/src/xrpld/rpc/MPTokenIssuanceID.h.ai.json @@ -0,0 +1,43 @@ +{ + "args": [], + "classes": [], + "description": "Provides functions to add an 'mpt_issuance_id' field to the transaction metadata for successful MPTokenIssuanceCreate transactions in the XRPL, including logic to determine eligibility and extract the ID.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/MPTokenIssuanceID.h", + "functions": [ + { + "args": [ + "serializedTx", + "transactionMeta" + ], + "lineno": 17, + "name": "canHaveMPTokenIssuanceID" + }, + { + "args": [ + "transactionMeta" + ], + "lineno": 21, + "name": "getIDFromCreatedIssuance" + }, + { + "args": [ + "response", + "transaction", + "transactionMeta" + ], + "lineno": 24, + "name": "insertMPTokenIssuanceID" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/MPTokenIssuanceID.h.ai.md b/src/xrpld/rpc/MPTokenIssuanceID.h.ai.md new file mode 100644 index 0000000000..654731958f --- /dev/null +++ b/src/xrpld/rpc/MPTokenIssuanceID.h.ai.md @@ -0,0 +1,33 @@ +# `xrpld/rpc/MPTokenIssuanceID.h` + +This header is part of a small, focused module that solves a narrow but important problem: the `mpt_issuance_id` of a newly created MPToken issuance is not stored as an explicit field inside the ledger object or the transaction itself — it is derived at runtime from two fields (`Sequence` and `Issuer`) inside the `MPTokenIssuance` ledger entry created by the transaction. This header declares the three functions that together compute and inject that derived identifier into RPC JSON responses. + +## Context: Why a Derived Identifier? + +`MPTID` is a `base_uint<192>` (a 192-bit opaque identifier) computed by `makeMptID(sequence, account)` as defined in `Indexes.h`. Because the ID is deterministically reconstructable from data already present in the transaction metadata, the protocol does not redundantly store it. However, API consumers need it: callers submitting an `MPTokenIssuanceCreate` transaction need to know what identifier to use when subsequently managing or trading the issuance. This module bridges the gap by extracting the ID from the transaction's `CreatedNode` metadata and injecting it into the JSON response after the fact. + +## The Three-Function Design + +The module exposes three functions in `xrpl::RPC`, and their division of responsibility mirrors the pattern established by `DeliveredAmount.h` for the analogous `delivered_amount` enrichment. + +`canHaveMPTokenIssuanceID()` is a pure eligibility predicate. It guards against unnecessary work and prevents accidental ID injection on irrelevant transactions. The implementation checks two conditions: the transaction type must be `ttMPTOKEN_ISSUANCE_CREATE`, and `transactionMeta.getResultTER()` must indicate `tesSUCCESS`. Both guards are necessary — a failed `MPTokenIssuanceCreate` does not actually create a ledger entry, so there is no `CreatedNode` to scan and no identifier to inject. + +`getIDFromCreatedIssuance()` does the actual extraction. It walks the `TxMeta` node list looking for a node whose `LedgerEntryType` is `ltMPTOKEN_ISSUANCE` and whose field name is `sfCreatedNode`. When found, it reads `sfSequence` and `sfIssuer` out of the `sfNewFields` sub-object and calls `makeMptID()` to reconstruct the 192-bit identifier. The `std::optional` return type handles the case where no such node exists, rather than using an exception or sentinel value — consistent with modern XRPL C++ idioms throughout the codebase. + +`insertMPTokenIssuanceID()` is the public-facing orchestration point. It calls `canHaveMPTokenIssuanceID()` first and returns early if the check fails, then delegates to `getIDFromCreatedIssuance()`. If both succeed, it writes `response[jss::mpt_issuance_id]` as a stringified `MPTID` directly into the `Json::Value` passed by reference. Taking the response as a non-const reference for in-place mutation is the same pattern used by `insertDeliveredAmount()` in `DeliveredAmount.h`. + +## Call Sites + +`insertMPTokenIssuanceID()` is invoked at every point in the codebase where transaction metadata is serialized to JSON for API consumers: + +- `Tx.cpp` (the `tx` RPC command) calls it immediately after `insertDeliveredAmount()` and `insertNFTSyntheticInJson()`. +- `AccountTx.cpp` applies the same enrichment trio when building the transaction list for an account. +- `NetworkOPs.cpp` enriches the metadata when broadcasting transaction results to subscribers. +- `LedgerToJson.cpp` applies it when serializing full ledger contents. +- `Simulate.cpp` applies the same enrichment so simulated transaction responses are structurally identical to real submission responses. + +This consistent pattern — always appearing alongside `insertDeliveredAmount()` and `insertNFTSyntheticInJson()` — reflects that all three are post-processing enrichments for fields that are derived rather than stored. The header formalizes the `MPTokenIssuanceID` enrichment as a first-class sibling in that enrichment layer. + +## Design Notes + +Separating `canHaveMPTokenIssuanceID()` and `getIDFromCreatedIssuance()` from `insertMPTokenIssuanceID()` is a deliberate testability affordance: callers can invoke the predicate and extractor independently in unit tests without needing a full `Json::Value` response object. The `STLedgerEntry::getJson()` path in the protocol layer also independently calls `makeMptID()` directly to inject `mpt_issuance_id` when serializing an `ltMPTOKEN_ISSUANCE` object, so the derived identifier appears consistently whether accessed through a transaction lookup or a direct ledger entry fetch. \ No newline at end of file diff --git a/src/xrpld/rpc/Output.h.ai.json b/src/xrpld/rpc/Output.h.ai.json new file mode 100644 index 0000000000..17a3bd8c80 --- /dev/null +++ b/src/xrpld/rpc/Output.h.ai.json @@ -0,0 +1,35 @@ +{ + "args": [ + { + "lineno": 10, + "name": "s" + }, + { + "lineno": 11, + "name": "b" + } + ], + "classes": [], + "description": "Defines a utility for outputting strings in the xrpl::RPC namespace, including a function to create an output functor that appends to a std::string.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/Output.h", + "functions": [ + { + "args": [ + "s" + ], + "lineno": 9, + "name": "stringOutput" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/Output.h.ai.md b/src/xrpld/rpc/Output.h.ai.md new file mode 100644 index 0000000000..01bc5c9731 --- /dev/null +++ b/src/xrpld/rpc/Output.h.ai.md @@ -0,0 +1,9 @@ +# `src/xrpld/rpc/Output.h` + +This header defines a lightweight streaming-sink abstraction for the XRPL RPC layer, mirroring the pattern established in `include/xrpl/json/Output.h` for the JSON serialization subsystem. + +`Output` is a type alias for `std::function`. It models a streaming write sink — a callable accepting successive string fragments without any knowledge of where those bytes ultimately land. Consumers of RPC response data (network sockets, in-memory buffers, test harnesses) all satisfy the same `Output` interface, which decouples response generation from delivery. + +`stringOutput()` is the sole concrete factory provided here. It captures a `std::string&` by reference and returns a lambda that appends each fragment via `s.append(b.data(), b.size())`. This is the standard sink for collecting an entire RPC response into a single string, commonly used in tests or when a caller needs a fully-materialized result. + +The notable difference from its counterpart in `Json::Output` is the Boost string type used: this file depends on the older `boost::utility/string_ref` header, while `include/xrpl/json/Output.h` uses `boost::beast::string_view`. Both model the same non-owning string-view concept, but the divergence suggests this RPC-layer variant predates the migration toward Boost.Beast primitives throughout the codebase. No files in the repository currently `#include` this header directly, making it a vestigial parallel to the canonical JSON output infrastructure rather than an actively consumed interface. \ No newline at end of file diff --git a/src/xrpld/rpc/RPCCall.h.ai.json b/src/xrpld/rpc/RPCCall.h.ai.json new file mode 100644 index 0000000000..c0520ac8d8 --- /dev/null +++ b/src/xrpld/rpc/RPCCall.h.ai.json @@ -0,0 +1,68 @@ +{ + "args": [], + "classes": [], + "description": "Provides functions for processing XRPL RPC calls, including command line and network invocation, as well as utilities for converting command line arguments to JSON and invoking the RPC client internally.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/RPCCall.h", + "functions": [ + { + "args": [ + "Config const& config", + "std::vector const& vCmd", + "Logs& logs" + ], + "lineno": 19, + "name": "fromCommandLine" + }, + { + "args": [ + "boost::asio::io_context& io_context", + "std::string const& strIp", + "std::uint16_t const iPort", + "std::string const& strUsername", + "std::string const& strPassword", + "std::string const& strPath", + "std::string const& strMethod", + "Json::Value const& jvParams", + "bool const bSSL", + "bool quiet", + "Logs& logs", + "std::function callbackFuncP", + "std::unordered_map headers" + ], + "lineno": 23, + "name": "fromNetwork" + }, + { + "args": [ + "std::vector const& args", + "Json::Value& retParams", + "unsigned int apiVersion", + "beast::Journal j" + ], + "lineno": 39, + "name": "rpcCmdToJson" + }, + { + "args": [ + "std::vector const& args", + "Config const& config", + "Logs& logs", + "unsigned int apiVersion", + "std::unordered_map const& headers" + ], + "lineno": 46, + "name": "rpcClient" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 17, + "name": "RPCCall" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/RPCCall.h.ai.md b/src/xrpld/rpc/RPCCall.h.ai.md new file mode 100644 index 0000000000..9a0428294b --- /dev/null +++ b/src/xrpld/rpc/RPCCall.h.ai.md @@ -0,0 +1,34 @@ +# `RPCCall.h` — RPC Command Origination and Dispatch + +This header declares the entry points for *originating* XRPL RPC calls — the client-side machinery for turning a command into an HTTP request and getting a response back. It is the counterpart to the server-side `RPCHandler.h` (which handles calls arriving at a running node), sitting instead at the boundary where the `xrpld` binary or a test harness initiates communication with a node. + +## The Two Dispatch Surfaces + +The file exposes two fundamentally different invocation modes inside the `RPCCall` nested namespace. + +`RPCCall::fromCommandLine()` is the entry point for the `xrpld` CLI. It takes a raw `std::vector` of CLI tokens and the node's `Config`, drives the full parse-serialize-dispatch pipeline, writes the formatted response to `stdout`, and returns an integer exit code. Its implementation is deliberately thin: it calls `rpcClient()` with `RPC::apiCommandLineVersion` and prints the result. + +`RPCCall::fromNetwork()` is for callers that have already structured their request as a `Json::Value` and know their target connection parameters (IP, port, credentials, path, optional HTTP headers). It accepts a `boost::asio::io_context&` and registers async `HTTPClient` callbacks through it — it does not block and does not own the event loop. The optional `callbackFuncP` receives the parsed `Json::Value` response; when omitted, the response is discarded. Inside, it injects HTTP Basic Auth into the headers map before delegating to `HTTPClient::request()` with a 256 MB response cap and a 30-second timeout — constants defined only in the implementation, not exposed here. + +## The "Trusted Interface" Design Note + +The comment above the `RPCCall` namespace is architecturally significant: *"This is a trusted interface, the user is expected to provide valid input… Error catching and reporting is not a requirement."* This is not negligence — it is an intentional design boundary. The CLI is operated by node administrators who understand the protocol; strict input validation and rich diagnostics belong in the server-side handler, which operates in an adversarial environment. Keeping the client path lean avoids duplicating the full validation logic of `RPCHandler.h`. + +## `rpcCmdToJson()` — The Parsing Bridge + +Declared outside the `RPCCall` namespace (making it a more general utility), `rpcCmdToJson()` translates a raw argument vector into a structured `Json::Value` ready for network submission. Internally it constructs an `RPCParser` — a private class in the implementation that maps each method name to a dedicated parse function via a static sorted command table. The dual-output pattern (return value + `retParams` out-parameter) separates the *translated request body* from the *raw invocation record* (`{ method, params }`). The latter is attached to error responses as `"rpc"` so callers can see exactly what was sent. The `apiVersion` parameter ensures the assembled JSON carries the correct `"api_version"` field; it is injected unconditionally unless the parse already produced an error or the request already carries a version. + +## `rpcClient()` — The Shared Internal Path + +`rpcClient()` is the most significant function in this interface. Its doc comment explicitly identifies it as the shared path used by both the CLI binary and unit tests, and its design reflects that dual purpose. Rather than driving I/O directly, it returns `std::pair` — an exit code and response payload that test code can assert against without printing to stdout or managing a running server. Internally it: + +1. Calls `rpcCmdToJson()` to translate the argument vector. +2. Reads server connection info from `Config` (falling back gracefully if no config is available — the `setup_ServerHandler` call is wrapped in a swallowed `std::exception` catch). +3. Spins up a *local* `boost::asio::io_context`, calls `RPCCall::fromNetwork()`, then calls `isService.run()` to block until the async operation completes. This is the pattern for "synchronous over async" — the calling thread blocks, but the underlying transport is still event-loop-driven. +4. Extracts the result from the JSON-RPC response envelope, mapping transport failures to `rpcJSON_RPC` and embedding diagnostic context (`"rpc"`, `"request_sent"`) in error outputs. + +The `static_assert` at the top of the implementation (`rpcBAD_SYNTAX == 1 && rpcSUCCESS == 0`) is a guard ensuring the integer exit code contract with shell callers is not accidentally broken by changes to the error enum. + +## Relationship to Sibling Files + +`RPCCall.h` deliberately pulls in only `Config.h`, `ServiceRegistry.h`, `json_value.h`, and Boost.Asio — keeping the client path decoupled from the heavier server machinery. `RPCHandler.h` is included only in the `.cpp` implementation (for `setup_ServerHandler`), not in this header. `Role.h` and `Status.h`, which govern access control and error representation inside the handler pipeline, are entirely absent. `ServerHandler.h` is touched only to extract connection setup — the client has no awareness of how the server processes the request it sends. \ No newline at end of file diff --git a/src/xrpld/rpc/RPCHandler.h.ai.json b/src/xrpld/rpc/RPCHandler.h.ai.json new file mode 100644 index 0000000000..aec013af12 --- /dev/null +++ b/src/xrpld/rpc/RPCHandler.h.ai.json @@ -0,0 +1,36 @@ +{ + "args": [], + "classes": [], + "description": "Declares functions for executing RPC commands and determining required roles in the XRPL RPC subsystem.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/RPCHandler.h", + "functions": [ + { + "args": [ + "RPC::JsonContext&", + "Json::Value&" + ], + "lineno": 11, + "name": "doCommand" + }, + { + "args": [ + "unsigned int version", + "bool betaEnabled", + "std::string const& method" + ], + "lineno": 13, + "name": "roleRequired" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/RPCHandler.h.ai.md b/src/xrpld/rpc/RPCHandler.h.ai.md new file mode 100644 index 0000000000..3dd6a8c50f --- /dev/null +++ b/src/xrpld/rpc/RPCHandler.h.ai.md @@ -0,0 +1,44 @@ +# `RPCHandler.h` — RPC Command Dispatch Interface + +This header is the public face of the XRPL RPC command dispatch layer. It exposes exactly two free functions that together cover the entire lifecycle of an RPC request: executing the command and pre-checking what permission level it requires. + +## What This File Does + +`RPCHandler.h` sits at the boundary between the transport layer (HTTP and WebSocket servers) and the per-command handler implementations scattered throughout `xrpld/rpc/handlers/`. Its two declarations are the only entry points needed by the rest of the system to drive the full RPC pipeline. + +```cpp +Status doCommand(RPC::JsonContext&, Json::Value&); +Role roleRequired(unsigned int version, bool betaEnabled, std::string const& method); +``` + +The corresponding implementation lives in `detail/RPCHandler.cpp`, which is deliberately hidden from callers — `Handler.h`, `Tuning.h`, and the command table are all detail-layer internals that nothing outside the RPC subsystem needs to see. + +## `doCommand` — The Dispatch Loop + +`doCommand` accepts a fully-populated `RPC::JsonContext` and an output `Json::Value`, resolves the command name from the incoming parameters, enforces every gate in sequence, and calls the matched handler. The function encapsulates several distinct responsibilities that would otherwise be scattered across each handler: + +**Load shedding.** Before doing anything else, the internal `fillHandler` helper counts jobs at `jtCLIENT` priority or higher. If the queue exceeds `Tuning::maxJobQueueClients`, it returns `rpcTOO_BUSY` immediately. Clients with `ADMIN` or `IDENTIFIED` roles bypass this check via `isUnlimited()`, ensuring operators retain access under load. + +**Command resolution.** The function extracts the command name from either the `command` or `method` JSON field (both forms are accepted for compatibility). If both fields are present with differing values, the request is rejected as `rpcUNKNOWN_COMMAND`. The name is passed to `getHandler(version, betaEnabled, name)` which consults a static table of `Handler` structs. + +**Role enforcement.** If the matched `Handler` carries `Role::ADMIN` and the caller's `context.role` is anything less, `fillHandler` returns `rpcNO_PERMISSION`. This check deliberately happens before network condition checks — no reason to inspect ledger state for a caller that cannot execute the command regardless. + +**Network condition gates.** The `conditionMet()` call in `Handler.h` checks amendment-blocked status, UNL-blocked status, the node's `OperatingMode`, and validated-ledger age. Commands requiring `NEEDS_NETWORK_CONNECTION`, `NEEDS_CURRENT_LEDGER`, or `NEEDS_CLOSED_LEDGER` will fail with appropriate error codes if the node is not ready. Importantly, the error codes themselves are version-sensitive: API v1 gets legacy names like `rpcNO_NETWORK` and `rpcNO_CURRENT`, while API v2 normalises these to `rpcNOT_SYNCED`. + +**Instrumented dispatch.** The internal `callMethod` helper calls the handler's `valueMethod_` function inside a `try`/`catch`. It records start/finish/error events in the `PerfLog`, creates a `JobQueue` load event for timing, and logs elapsed time at debug level. Unhandled exceptions are caught and translated to `rpcINTERNAL`, and if the request was priced at `feeReferenceRPC` the load charge is escalated to `feeExceptionRPC`. + +The response shape differs between HTTP and WebSocket transports — HTTP wraps `status` inside the `result` object, WebSocket places it outside — but `doCommand` is transport-agnostic: it only fills the `result` `Json::Value`, leaving the envelope construction to the server layers above it. + +## `roleRequired` — Pre-Authorisation Probe + +`roleRequired` is a pure lookup: given a method name, API version, and the `betaEnabled` flag, it returns the `Role` the caller must hold to execute that command. If the method does not exist in the handler table, it returns `Role::FORBID`. + +The function is called by the server layer *before* the request is queued or dispatched, allowing transport-level code to reject unauthorised requests immediately — without allocating any RPC context or touching the job queue. This is the correct place for that check: `doCommand` also re-validates the role internally, but having the server layer do an early reject avoids unnecessary work. + +## The `Role` Model + +The `Role` enum (`GUEST`, `USER`, `IDENTIFIED`, `ADMIN`, `PROXY`, `FORBID`) is defined in `Role.h`. `ADMIN` is the only role that unlocks administrative RPC commands; `IDENTIFIED` and `ADMIN` are both "unlimited" for load-shedding purposes. The `FORBID` sentinel returned by `roleRequired` for unknown methods signals that the connection should be closed rather than queued. + +## Design Philosophy + +The header's minimal surface — two free functions, no classes — reflects a deliberate separation between the stable external contract and the volatile implementation details. The `Handler` table, tuning constants, and condition-checking machinery are all confined to the `detail/` subdirectory. Any file that needs to dispatch or pre-check an RPC call includes only this header and `Context.h`; it never needs to know how the handler table is structured or how conditions are evaluated. \ No newline at end of file diff --git a/src/xrpld/rpc/RPCSub.h.ai.json b/src/xrpld/rpc/RPCSub.h.ai.json new file mode 100644 index 0000000000..70f0fb4b49 --- /dev/null +++ b/src/xrpld/rpc/RPCSub.h.ai.json @@ -0,0 +1,73 @@ +{ + "args": [ + { + "lineno": 11, + "name": "strUsername" + }, + { + "lineno": 13, + "name": "strPassword" + }, + { + "lineno": 20, + "name": "source" + }, + { + "lineno": 21, + "name": "io_context" + }, + { + "lineno": 22, + "name": "jobQueue" + }, + { + "lineno": 23, + "name": "strUrl" + }, + { + "lineno": 24, + "name": "strUsername" + }, + { + "lineno": 25, + "name": "strPassword" + }, + { + "lineno": 26, + "name": "registry" + } + ], + "classes": [ + { + "args": [ + "InfoSub::Source& source" + ], + "lineno": 7, + "name": "RPCSub" + } + ], + "description": "Defines the RPCSub class for JSON RPC subscriptions and a factory function to create RPCSub instances in the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/RPCSub.h", + "functions": [ + { + "args": [ + "source", + "io_context", + "jobQueue", + "strUrl", + "strUsername", + "strPassword", + "registry" + ], + "lineno": 19, + "name": "make_RPCSub" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/RPCSub.h.ai.md b/src/xrpld/rpc/RPCSub.h.ai.md new file mode 100644 index 0000000000..be570da39a --- /dev/null +++ b/src/xrpld/rpc/RPCSub.h.ai.md @@ -0,0 +1,35 @@ +# RPCSub.h — Outbound Push Subscription for JSON-RPC + +`RPCSub.h` defines the abstract interface for the XRPL server's outbound subscription mechanism: when a remote HTTP/HTTPS endpoint wants to *receive* ledger events pushed to it rather than polling, an `RPCSub` object is the subscription handle that drives that delivery. This is the "webhook" side of the XRPL subscription system. + +## Context: where RPCSub fits in the subscription hierarchy + +The broader subscription infrastructure revolves around `InfoSub` (defined in `include/xrpl/server/InfoSub.h`), which is the base class for all client subscription handles. `InfoSub` tracks which accounts, ledgers, order books, and transaction streams a client has subscribed to, manages a resource-consumption `Consumer`, and declares the single pure-virtual `send(Json::Value const&, bool)` method that each concrete subclass must implement to deliver events. WebSocket connections have their own `InfoSub` subclass; `RPCSub` is the subclass for outbound HTTP/HTTPS delivery. + +`RPCSub` adds just two interface methods on top of `InfoSub`: `setUsername()` and `setPassword()`. These allow credentials for the remote endpoint to be updated after the subscription is created, which is why they are virtual rather than constructor parameters. + +## Factory and implementation hiding + +The header exposes no concrete class. The actual implementation, `RPCSubImp`, lives entirely in `src/xrpld/rpc/detail/RPCSub.cpp` and is never visible to callers. The `make_RPCSub()` factory function constructs and returns a `std::shared_ptr` backed by `RPCSubImp`. This design keeps the implementation details — the deque, the lock, the connection state — out of the ABI and out of any callers' translation units. + +One unexplained parameter is noted with a `VFALCO` comment directly in the header: `boost::asio::io_context& io_context`. The reason becomes clear in the implementation: `RPCCall::fromNetwork()` takes an `io_context` to schedule the async HTTP POST, so the subscription must hold a reference to it. The comment reflects that this coupling felt architecturally awkward to the original author. + +## How event delivery works in RPCSubImp + +The concrete `RPCSubImp` implementation has a straightforward producer-consumer design built around `mLock` (the mutex inherited from `InfoSub`), `mDeque` (a `std::deque>`), and a `mSending` boolean flag. + +When `send()` is called — typically from the server's event-publishing path — it acquires the lock, appends the event together with a monotonically increasing sequence number (`mSeq++`) to the deque, and then, **only if no send job is already running**, enqueues a `jtCLIENT_SUBSCRIBE` job on the `JobQueue`. This lazy-start pattern means no thread is ever spinning or blocked when the queue is idle; the worker only exists while there is work to do. + +The worker (`sendThread`) runs a drain loop: it acquires the lock, pops the front of the deque, copies the event out, releases the lock, and *then* calls `RPCCall::fromNetwork()` outside the lock. This is a deliberate separation: holding the lock during an outbound HTTP call would serialize all event producers against a potentially slow network endpoint. The sequence number is attached to the JSON object as `"seq"` just before sending, giving the remote endpoint a way to detect gaps or reordering. + +If `fromNetwork` throws (network error, connection refused), the exception is caught and logged, and the loop continues draining. Events are never retried — if delivery fails, the event is silently dropped. The loop terminates when the deque is empty, at which point `mSending` is set to `false` under the lock, allowing the next `send()` call to start a fresh job. + +## Construction-time validation + +`RPCSubImp`'s constructor calls `parseUrl()` on the supplied URL string and throws `std::runtime_error` immediately if it is malformed or uses a scheme other than `http` or `https`. This fail-fast policy means that callers of `make_RPCSub` must be prepared to catch an exception — the subscription is never in a partially-constructed unusable state. + +The `ServiceRegistry` parameter is used only for two things at construction: obtaining a `beast::Journal` for structured logging under the `"RPCSub"` category, and retrieving the global `Logs&` reference that `RPCCall::fromNetwork` requires for its own internal logging. + +## Relationship to InfoSub::Source + +`InfoSub::Source` (the inner class of `InfoSub`) carries three `RPCSub`-specific virtual methods: `findRpcSub`, `addRpcSub`, and `tryRemoveRpcSub`. The comment in `InfoSub.h` is candid: `// VFALCO TODO Remove — This was added for one particular partner`. These methods allow the source (typically the `NetworkOPs` implementation) to maintain a URL-keyed registry of active outbound subscriptions, so duplicate subscriptions to the same URL can be detected and deduplicated. `RPCSub` is thus a somewhat isolated feature within the subscription system, serving a narrow use case that was apparently added for a specific integration need and carries technical-debt markers throughout. \ No newline at end of file diff --git a/src/xrpld/rpc/Request.h.ai.json b/src/xrpld/rpc/Request.h.ai.json new file mode 100644 index 0000000000..f6b5db7104 --- /dev/null +++ b/src/xrpld/rpc/Request.h.ai.json @@ -0,0 +1,43 @@ +{ + "args": [ + { + "lineno": 14, + "name": "journal_" + }, + { + "lineno": 15, + "name": "method_" + }, + { + "lineno": 16, + "name": "params_" + }, + { + "lineno": 17, + "name": "app_" + } + ], + "classes": [ + { + "args": [ + "beast::Journal journal_, std::string const& method_, Json::Value& params_, Application& app_" + ], + "lineno": 11, + "name": "Request" + } + ], + "description": "Defines the RPC::Request struct, which encapsulates the context and data for an RPC request in the XRPL application, including logging, method name, parameters, resource fee, result, and application reference.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/Request.h", + "functions": [], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/Request.h.ai.md b/src/xrpld/rpc/Request.h.ai.md new file mode 100644 index 0000000000..c8e4d8493c --- /dev/null +++ b/src/xrpld/rpc/Request.h.ai.md @@ -0,0 +1,29 @@ +# `RPC::Request` — Minimal RPC Dispatch Envelope + +`Request` is a plain data-carrying struct that bundles together everything a single RPC invocation needs: a logging journal, the method name, the caller-supplied JSON parameters, a mutable resource charge, the JSON result buffer, and a reference to the application. It lives in `src/xrpld/rpc/Request.h` and belongs to the `xrpl::RPC` namespace alongside the more elaborate `Context` and `JsonContext` types defined in `Context.h`. + +## Design Rationale: Struct Over Class + +The choice of `struct` with entirely public members is deliberate. `Request` is not an encapsulated object — it is a pass-by-reference bundle of state for internal RPC dispatch. Handlers read `journal`, `method`, `params`, and `app` directly, and write their output directly into `result`. Accessor functions would add noise with no safety benefit, since the consumers are trusted internal handler implementations, not user-facing abstractions. + +## The Resource Charge Field + +`fee` is initialized in the constructor to `Resource::feeReferenceRPC`, the standard baseline charge defined in `xrpl/resource/Fees.h`. The comment annotates it `[in, out]`, explicitly marking it as a field that handlers may — and in costly cases should — escalate before returning. The resource management subsystem uses this value to debit the client's resource budget after the handler completes. Defaulting to reference-level rather than zero is a deliberate defensive choice: a handler that forgets to set a fee still imposes a non-trivial cost on the caller, preventing resource exhaustion through repeated low-effort calls. + +The `Resource::Charge` type (`xrpl/resource/Charge.h`) pairs a numeric cost with a human-readable label. The fee schedule in `Fees.h` defines a tiered set of charges — from `feeMalformedRPC` for immediately-rejected requests up to `feeHeavyBurdenRPC` for expensive operations — giving handlers a vocabulary for communicating actual load to the resource manager. + +## Member Lifetimes + +`journal` is stored by value, but `beast::Journal` is a lightweight handle (not a full logger), so copying it is cheap and safe. `params` is accepted by non-const reference in the constructor but stored as a `Json::Value` copy, meaning the `Request` owns its parameter data independently of the caller's buffer. `result` is default-initialized as an empty `Json::Value` and is expected to be populated by the dispatched handler; the caller reads it back after execution. + +`app` is stored as a raw reference, which imposes a lifetime constraint: no `Request` instance may outlive the `Application` object. In practice this is safe — `Application` is a process-scoped singleton that outlives all RPC activity — but it is an implicit invariant rather than an enforced one. + +## Deleted Assignment Operator + +The private, unimplemented `operator=` is a pre-C++11 convention for signaling non-assignability. Reference members (`app`) already cause the compiler to delete copy-assignment, so this declaration is belt-and-suspenders: it makes the design intent explicit and surfaces the constraint at the declaration site rather than in a compiler error downstream. + +## Relationship to `RPC::Context` + +The sibling `Context.h` defines `Context`, `JsonContext`, and `GRPCContext` — richer dispatch envelopes that carry session-level metadata including the client's `Role`, a `Resource::Consumer` to charge directly, `NetworkOPs`, `LedgerMaster`, a coroutine handle, a subscriber pointer, and an API version number. The active dispatch path in `RPCHandler.cpp` and `ServerHandler.cpp` operates on `JsonContext`, not `Request`. + +`Request` is comparatively minimal: it carries only what a pure handler function needs to read inputs and write outputs, with no session policy. This narrower scope suits scenarios where the full session context is not available or not needed, and where the caller prefers to manage resource accounting indirectly through the returned `fee` rather than through a live `Resource::Consumer`. \ No newline at end of file diff --git a/src/xrpld/rpc/Role.h.ai.json b/src/xrpld/rpc/Role.h.ai.json new file mode 100644 index 0000000000..e2e7bad811 --- /dev/null +++ b/src/xrpld/rpc/Role.h.ai.json @@ -0,0 +1,60 @@ +{ + "args": [], + "classes": [], + "description": "Defines administrative roles and utility functions for determining user privileges, resource limits, and IP-based access control for XRPL server RPC endpoints.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/Role.h", + "functions": [ + { + "args": [ + "required", + "port", + "params", + "remoteIp", + "user" + ], + "lineno": 27, + "name": "requestRole" + }, + { + "args": [ + "manager", + "remoteAddress", + "role", + "user", + "forwardedFor" + ], + "lineno": 39, + "name": "requestInboundEndpoint" + }, + { + "args": [ + "role" + ], + "lineno": 48, + "name": "isUnlimited" + }, + { + "args": [ + "remoteIp", + "nets4", + "nets6" + ], + "lineno": 56, + "name": "ipAllowed" + }, + { + "args": [ + "request" + ], + "lineno": 67, + "name": "forwardedFor" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/Role.h.ai.md b/src/xrpld/rpc/Role.h.ai.md new file mode 100644 index 0000000000..e1e1a96d42 --- /dev/null +++ b/src/xrpld/rpc/Role.h.ai.md @@ -0,0 +1,58 @@ +# `src/xrpld/rpc/Role.h` — RPC Access Control and Privilege Classification + +This header is the gatekeeper for XRPL's RPC permission model. Every inbound JSON-RPC or WebSocket request enters the server with an unknown privilege level; this file defines the classification logic that converts a raw IP address, a `Port` configuration, and optional HTTP credentials into one of six discrete roles, which downstream code then uses to gate commands and enforce resource limits. + +## The `Role` Enum + +```cpp +enum class Role { GUEST, USER, IDENTIFIED, ADMIN, PROXY, FORBID }; +``` + +The six values represent a coarse privilege ladder, but not a strictly linear one: + +- `GUEST` — unauthenticated external caller; subject to rate limiting, cannot run admin commands. +- `USER` — defined but currently unused in the assignment logic; reserved for future use. +- `IDENTIFIED` — a caller arriving through a trusted reverse proxy (`secure_gateway`) that has forwarded a user identity via the `X-User` HTTP header. Granted unlimited resources like `ADMIN`, but cannot run all admin-only commands. This models a multi-tenant operator where each end-user is metered at the proxy level, not the node. +- `ADMIN` — caller from an admin-listed IP (and optionally matching a username/password); has full command access and unlimited resources. +- `PROXY` — a trusted reverse proxy connection where no user identity header was provided. The proxy itself is unlimited for the purpose of forwarding, but individual end users are tracked by their forwarded IP address. +- `FORBID` — access denied; returned when a command requires `ADMIN` but the caller failed the IP/credential check. + +The critical design choice here is separating `IDENTIFIED` from `ADMIN`. A secure gateway (such as a load balancer or API gateway you control) can get resource-exempt throughput for its users without those users having the ability to call `stop`, `ledger_request`, or other administrative commands. This prevents a privileged pipe from becoming a backdoor. + +## `requestRole()` — The Central Decision Function + +The implementation in `detail/Role.cpp` evaluates three independent checks in order: + +1. **Admin check**: `ipAllowed(remoteIp, port.admin_nets_v4, port.admin_nets_v6)` AND `passwordUnrequiredOrSentCorrect(port, params)`. If both pass, return `ADMIN` immediately. +2. **Required-but-denied**: If the calling code required `ADMIN` but step 1 failed, return `FORBID`. +3. **Secure gateway check**: If the remote IP falls within `port.secure_gateway_nets_v4/v6`, the result depends on whether a non-empty `user` string arrived in the `X-User` HTTP header — `IDENTIFIED` if present, `PROXY` if absent. +4. **Fallthrough**: `GUEST`. + +The `user` parameter comes from the HTTP layer (`JsonContext::Headers::user`), which is populated only after `ipAllowed()` confirms the request came through a trusted secure gateway. This means untrusted callers cannot self-elevate to `IDENTIFIED` by simply sending the `X-User` header — the IP gate must pass first. + +## `ipAllowed()` — Subnet Membership + +Rather than exact-IP comparison, `ipAllowed()` treats the remote address as a `/32` (IPv4) or `/128` (IPv6) host network, then checks whether it is a subnet of or equal to each configured CIDR block. The dual-vector approach (`nets4` / `nets6`) keeps the two address families completely separate, avoiding any cross-family confusion. The function serves double duty: it's called for both the `admin_nets` check and the `secure_gateway_nets` check. + +## `requestInboundEndpoint()` — Connecting Role to the Resource Manager + +This function translates the assigned role into a `Resource::Consumer`, the object that tracks and enforces rate limits for the lifetime of the connection: + +- `ADMIN` and `IDENTIFIED` (via `isUnlimited`) → `manager.newUnlimitedEndpoint(remoteAddress)` — bypasses all throttling. +- `PROXY` → `manager.newInboundEndpoint(remoteAddress, /*isProxied=*/true, forwardedFor)` — uses the forwarded IP for per-client accounting rather than the proxy's own address. +- `GUEST` → `manager.newInboundEndpoint(remoteAddress, false, {})` — standard rate-limited endpoint tracked by the actual remote IP. + +This is where the `PROXY` role's semantics become concrete: the resource manager uses the `X-Forwarded-For` / `Forwarded` IP to create a consumer entry that is distinct from the proxy's own IP, so many end users behind the same proxy each get their own rate-limit bucket rather than sharing (and potentially DoS'ing) one. + +## `forwardedFor()` — Defensive Header Parsing + +The `forwardedFor()` function (implemented in `detail/Role.cpp`) is more complex than it first appears. It handles two header standards in priority order: + +1. **RFC 7239 `Forwarded` field** — finds the first `for=` token (case-insensitively), then delegates to `extractIpAddrFromField()`. +2. **De-facto `X-Forwarded-For` field** — takes the first comma-delimited entry. + +`extractIpAddrFromField()` is a careful parser that strips leading/trailing whitespace, handles optional double-quote wrapping, removes `[...]` brackets from IPv6 literals, and strips any appended port number for IPv4 addresses. The care taken here reflects the real-world messiness of proxy header formats and the security sensitivity: this value is used for per-user rate limiting, so both false negatives (losing the real IP) and false positives (accepting a spoofed value) have consequences. + +## Usage Context + +`Role.h` is included by `Context.h`, which embeds a `Role` field directly into `RPC::Context` — the struct passed to every handler. This means every handler can call `isUnlimited(context.role)` or branch on `context.role == Role::ADMIN` without having to repeat any access-control logic. The `WSInfoSub` class in `detail/WSInfoSub.h` demonstrates the WebSocket path: it calls `ipAllowed()` directly on construction to decide whether to capture the `X-User` and `X-Forwarded-For` headers at all, ensuring that only gateway-sourced WebSocket subscriptions carry an identity. \ No newline at end of file diff --git a/src/xrpld/rpc/ServerHandler.h.ai.json b/src/xrpld/rpc/ServerHandler.h.ai.json new file mode 100644 index 0000000000..7ba909dd2d --- /dev/null +++ b/src/xrpld/rpc/ServerHandler.h.ai.json @@ -0,0 +1,194 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "ServerHandlerCreator const&", + "Application& app", + "boost::asio::io_context& io_context", + "JobQueue& jobQueue", + "NetworkOPs& networkOPs", + "Resource::Manager& resourceManager", + "CollectorManager& cm" + ], + "lineno": 21, + "name": "ServerHandler" + }, + { + "args": [], + "lineno": 23, + "name": "Setup" + }, + { + "args": [], + "lineno": 29, + "name": "client_t" + }, + { + "args": [], + "lineno": 74, + "name": "ServerHandlerCreator" + } + ], + "description": "Defines the ServerHandler class responsible for managing server setup, session handling, and request processing in the XRPL server application. Includes configuration structures, handler methods, and factory functions.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/ServerHandler.h", + "functions": [ + { + "args": [ + "Port const& lhs", + "Port const& rhs" + ], + "lineno": 18, + "name": "operator<" + }, + { + "args": [], + "lineno": 49, + "name": "makeContexts" + }, + { + "args": [ + "Setup const& setup", + "beast::Journal journal" + ], + "lineno": 109, + "name": "setup" + }, + { + "args": [], + "lineno": 112, + "name": "setup" + }, + { + "args": [], + "lineno": 117, + "name": "endpoints" + }, + { + "args": [], + "lineno": 121, + "name": "stop" + }, + { + "args": [ + "Session& session", + "boost::asio::ip::tcp::endpoint endpoint" + ], + "lineno": 127, + "name": "onAccept" + }, + { + "args": [ + "Session& session", + "std::unique_ptr&& bundle", + "http_request_type&& request", + "boost::asio::ip::tcp::endpoint const& remote_address" + ], + "lineno": 130, + "name": "onHandoff" + }, + { + "args": [ + "Session& session", + "http_request_type&& request", + "boost::asio::ip::tcp::endpoint const& remote_address" + ], + "lineno": 137, + "name": "onHandoff" + }, + { + "args": [ + "Session& session" + ], + "lineno": 143, + "name": "onRequest" + }, + { + "args": [ + "std::shared_ptr session", + "std::vector const& buffers" + ], + "lineno": 146, + "name": "onWSMessage" + }, + { + "args": [ + "Session& session", + "boost::system::error_code const&" + ], + "lineno": 150, + "name": "onClose" + }, + { + "args": [ + "Server&" + ], + "lineno": 153, + "name": "onStopped" + }, + { + "args": [ + "std::shared_ptr const& session", + "std::shared_ptr const& coro", + "Json::Value const& jv" + ], + "lineno": 157, + "name": "processSession" + }, + { + "args": [ + "std::shared_ptr const&", + "std::shared_ptr coro" + ], + "lineno": 162, + "name": "processSession" + }, + { + "args": [ + "Port const& port", + "std::string const& request", + "beast::IP::Endpoint const& remoteIPAddress", + "Output const&", + "std::shared_ptr coro", + "std::string_view forwardedFor", + "std::string_view user" + ], + "lineno": 165, + "name": "processRequest" + }, + { + "args": [ + "http_request_type const& request" + ], + "lineno": 173, + "name": "statusResponse" + }, + { + "args": [ + "Config const& c", + "std::ostream& log" + ], + "lineno": 177, + "name": "setup_ServerHandler" + }, + { + "args": [ + "Application& app", + "boost::asio::io_context&", + "JobQueue&", + "NetworkOPs&", + "Resource::Manager&", + "CollectorManager& cm" + ], + "lineno": 180, + "name": "make_ServerHandler" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/ServerHandler.h.ai.md b/src/xrpld/rpc/ServerHandler.h.ai.md new file mode 100644 index 0000000000..cd6b1b7599 --- /dev/null +++ b/src/xrpld/rpc/ServerHandler.h.ai.md @@ -0,0 +1,55 @@ +# `ServerHandler.h` — RPC and WebSocket Request Gateway + +`ServerHandler` is the central hub that bridges the XRPL node's low-level TCP/TLS server with the RPC and WebSocket processing pipeline. It owns the `Server` object (the Boost.Beast–backed acceptor layer), enforces per-port connection limits, routes incoming connections to either the overlay peer protocol or the JSON-RPC subsystem, and dispatches all request work to the `JobQueue` as coroutines. + +## Construction and the `ServerHandlerCreator` Token Pattern + +`ServerHandler` cannot be constructed directly by arbitrary code. Its constructor requires a `ServerHandlerCreator` value — a private type with no public constructor. The only code that can create such a value is the `make_ServerHandler()` factory function, which is declared as a friend. This is a deliberate capability-key idiom: the constructor must be `public` so that `std::make_unique` can call it, yet the private token type makes it impossible for any caller outside the factory to satisfy the constructor's signature. This avoids the pitfalls of private constructors combined with `make_unique` (which would require additional `friend` declarations in standard library internals). + +## `Setup` and Configuration + +`ServerHandler::Setup` is a plain data structure populated from `rippled.cfg` by `setup_ServerHandler()`. It holds a vector of `Port` descriptors (each Port carries its name, bind address, protocols, TLS context, and optional user/password), a `client_t` sub-struct for when the process makes outgoing RPC calls to itself or another node (with optional TLS and credentials), and a `boost::asio::ip::tcp::endpoint` for the overlay peer listener. `makeContexts()` — called on the `Setup` after parsing — instantiates the SSL contexts for any port that advertises `https`, `wss`, or similar protocols. + +The `operator<(Port, Port)` free function defined at the top of the file orders `Port` objects by name; this allows `std::reference_wrapper` keys to work in `count_`, the `std::map` that tracks live connection counts per port. + +## The Handler Concept and Session Lifecycle + +`make_Server()` (in `Server.h`) is a template factory that wraps any type satisfying a duck-typed handler concept inside a `ServerImpl`. `ServerHandler` fills that concept by exposing six callbacks: + +- **`onAccept()`** — Called for every new TCP connection. Atomically increments the port's connection count and rejects connections once the configured limit is reached. The mutex protects `count_` because accepts can fire on multiple I/O threads. + +- **`onHandoff()`** — The routing decision point. If the HTTP request carries a WebSocket upgrade header, the session is promoted via `session.websocketUpgrade()`, a `WSInfoSub` subscriber object is attached to `WSSession::appDefined`, and the handoff is marked `moved`. If the port carries the `peer` protocol and an SSL bundle is present, the connection is forwarded to `app_.getOverlay()`. A bare GET `/` on a WebSocket port is answered immediately with `statusResponse()`. Otherwise an empty `Handoff` is returned, which signals the server to proceed to `onRequest()`. + +- **`onRequest()`** — Handles HTTP RPC. After checking that the port has the `http`/`https` protocol and that the Basic-Auth header matches the port's credentials, the session is *detached* (preventing the server from closing it while processing is in flight) and a coroutine of job type `jtCLIENT_RPC` is posted to the `JobQueue`. If the queue rejects the post (i.e., the node is shutting down), a 503 is sent immediately and the session is closed. + +- **`onWSMessage()`** — Called for each WebSocket text frame. JSON parsing happens inline on the I/O thread (fast path); if the parse fails or the request exceeds `RPC::Tuning::maxRequestSize`, an error is sent back without touching the job queue. Valid messages are dispatched as `jtCLIENT_WEBSOCKET` coroutines; if the queue is full, the session is closed with a `going_away` close frame. + +- **`onClose()`** — Decrements the per-port counter under lock, mirroring `onAccept()`. + +- **`onStopped()`** — Called by the server once all connections have been closed. Sets `stopped_ = true` and notifies the `condition_variable` so that any thread blocked in `stop()` can proceed. + +## Shutdown Synchronization + +`stop()` calls `m_server->close()` — which triggers the asynchronous teardown of all listeners and sessions — then blocks on `condition_.wait()` until `onStopped()` fires. This two-phase shutdown guarantees that callers of `stop()` do not return prematurely while in-flight I/O is still referencing `ServerHandler` members. The `mutex_` / `condition_variable` pair is reused for both the connection-count tracking and the stop notification, keeping the synchronization surface small. + +## `processRequest()`: HTTP RPC Engine + +The private `processRequest()` function is the workhorse for HTTP JSON-RPC. It handles both single and batch requests: a top-level object with `method == "batch"` is treated as a batch, iterating over `params` as an array of individual calls and returning a JSON array of results. This design lets clients amortize round-trip latency for multiple ledger queries. + +For each request in the loop, `processRequest()`: +1. Resolves the API version (with fallback to `apiVersionIfUnspecified`). +2. Determines the required `Role` for the method via `RPC::roleRequired()` and the caller's actual `Role` via `requestRole()` (which checks IP ranges, admin credentials, and secure-gateway headers). +3. Allocates a `Resource::Consumer` — unlimited for admin roles, metered for everything else. If the consumer is already disconnected due to load, a 503 (or per-item error in batch mode) is returned without executing the command. +4. Constructs an `RPC::JsonContext` and calls `RPC::doCommand()`. +5. Formats the response according to the `ripplerpc` protocol version: v2+ separates success/error at the envelope level; v1 always places results under `result`. +6. Masks sensitive fields (`passphrase`, `secret`, `seed`, `seed_hex`) in error responses before they are written back to the client. + +The HTTP status code is also version-gated: `ripplerpc >= "3.0"` maps ledger error codes to appropriate HTTP statuses via `RPC::error_code_http_status()`; earlier versions always return 200. + +## WebSocket Session Processing + +`processSession(WSSession&, ...)` retrieves the `WSInfoSub` stored in `WSSession::appDefined`, checks the resource consumer's disconnect threshold, validates command presence, resolves the role, and calls `RPC::doCommand()`. The response format for WebSocket differs slightly from HTTP: `id`, `jsonrpc`, `ripplerpc`, and `api_version` fields from the request are echoed in the response, and a `type: response` field is always included. Sensitive keys in error responses are masked by the same `` replacement logic as the HTTP path. + +## Metrics + +Three metrics are registered against the `"rpc"` group of the `CollectorManager`: `rpc_requests_` (a counter incremented per request), `rpc_size_` (an event recording payload bytes), and `rpc_time_` (an event recording processing duration). Request duration is also logged at varying severity — `debug` for sub-second, `warn` for ≥1 s, `error` for ≥10 s — to make slow RPC calls observable without configuration. \ No newline at end of file diff --git a/src/xrpld/rpc/Status.h.ai.json b/src/xrpld/rpc/Status.h.ai.json new file mode 100644 index 0000000000..d423ffb21a --- /dev/null +++ b/src/xrpld/rpc/Status.h.ai.json @@ -0,0 +1,121 @@ +{ + "args": [ + { + "lineno": 27, + "name": "code" + }, + { + "lineno": 27, + "name": "d" + }, + { + "lineno": 31, + "name": "ter" + }, + { + "lineno": 31, + "name": "d" + }, + { + "lineno": 35, + "name": "e" + }, + { + "lineno": 35, + "name": "d" + }, + { + "lineno": 39, + "name": "e" + }, + { + "lineno": 39, + "name": "s" + }, + { + "lineno": 71, + "name": "object" + } + ], + "classes": [ + { + "args": [], + "lineno": 16, + "name": "Status" + } + ], + "description": "Defines the xrpl::RPC::Status struct, which represents the result of an operation that might fail, wrapping legacy codes and providing a uniform interface for error handling in the XRPL RPC system.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/Status.h", + "functions": [ + { + "args": [], + "lineno": 44, + "name": "codeString" + }, + { + "args": [], + "lineno": 47, + "name": "operator bool" + }, + { + "args": [], + "lineno": 52, + "name": "operator!" + }, + { + "args": [], + "lineno": 57, + "name": "toTER" + }, + { + "args": [], + "lineno": 64, + "name": "toErrorCode" + }, + { + "args": [ + "object" + ], + "lineno": 71, + "name": "inject" + }, + { + "args": [], + "lineno": 81, + "name": "messages" + }, + { + "args": [], + "lineno": 86, + "name": "message" + }, + { + "args": [], + "lineno": 90, + "name": "type" + }, + { + "args": [], + "lineno": 95, + "name": "toString" + }, + { + "args": [ + "object" + ], + "lineno": 99, + "name": "fillJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/Status.h.ai.md b/src/xrpld/rpc/Status.h.ai.md new file mode 100644 index 0000000000..4d865fea78 --- /dev/null +++ b/src/xrpld/rpc/Status.h.ai.md @@ -0,0 +1,35 @@ +# `Status.h` — Unified Error Result Type for the XRPL RPC Layer + +## Role and Motivation + +The XRPL codebase has two established error representation systems that predate any unified approach: `TER` (Transaction Engine Result), which flows through the transaction processing pipeline, and `error_code_i`, the RPC-layer error enumeration defined in ``. RPC handlers must frequently work with both — validating a transaction produces a `TER`, while reporting protocol-level problems produces an `error_code_i`. Without a unifying type, return paths become a tangle of conditionals and separate type conversions at every call site. + +`Status` solves this by acting as a tagged union over these two legacy code spaces, plus a raw integer fallback, while exposing a single interface for the rest of the RPC infrastructure to consume. It also carries an optional list of freeform message strings, allowing callers to attach diagnostic context that neither `TER` nor `error_code_i` can express on their own. In practice, handlers like `doCommand()` and `callMethod()` in `RPCHandler.cpp` return `Status` directly, and `AccountTx.cpp` uses `std::variant` to propagate errors through multi-step ledger range lookups. + +## Struct Design + +`Status` inherits from `std::exception`, signaling intent to support throw/catch flows even though the dominant usage pattern in the codebase is value-based error propagation. The `Type` enum (`none`, `TER`, `error_code_i`) serves as the discriminant tag for the stored `code_` integer. The raw `code_` is always stored as a plain `int`: `TERtoInt` converts a `TER` on construction, and `TER::fromInt` recovers it in `toTER()`. + +The `OK` sentinel is `0`. This is coherent because both `TER` (`tesSUCCESS == 0`) and `error_code_i` (`rpcSUCCESS == 0`) use zero to mean success. As a consequence, `operator bool()` — returning `code_ != OK` — reads naturally as "if something went wrong" and works consistently regardless of which code space the `Status` was constructed from. The complementary `operator!()` lets call sites write `if (!status)` to test for clean success. + +## Constructor Design and the `enable_if` Guard + +There are four constructors. The template constructor accepting any integral type uses `std::enable_if_t::value>` to deliberately exclude enum types. This prevents silent narrowing: if an `error_code_i` enum value accidentally bound to the integer template path, `type_` would remain `Type::none` instead of `Type::error_code_i`, causing `inject()` and `toErrorCode()` to silently misbehave. The two explicit enum constructors — one for `TER`, one for `error_code_i` — each set `type_` correctly, so the guard enforces that only truly untyped integer codes (raw internal status values carrying no TER or RPC semantic) go through the generic path. + +There is also a convenience constructor `Status(error_code_i, std::string const&)` that accepts a single string rather than a `std::vector`, matching the common single-message case seen throughout handler code such as `RPC::Status{rpcINVALID_PARAMS, "ledgerHashMalformed"}`. + +## `inject()` and the JSON Bridge + +The `inject()` method is the primary bridge to the JSON response layer. It calls `toErrorCode()` and, if the result is non-zero, delegates to `inject_error()` from ``. That function populates the JSON object with `jss::error` (a human-readable token like `"invalidParams"`), an error code integer, and an error message from the `ErrorInfo` table. If the `Status` carries attached messages, the first one is forwarded as a supplemental message string, overriding the default message from `ErrorInfo`. + +This design keeps JSON serialization deferred to output time rather than embedded in construction, which matters because not every `Status` ends up in a JSON response — some are used purely for internal control flow or later translated to other transport formats. + +The `fillJson()` method is explicitly noted in the header as "not currently used." Its presence documents intent to support a fully spec-compliant JSON-RPC 2.0 error object (per the specification at `jsonrpc.org/specification#error_object`), distinct from the current ad-hoc `inject()` approach. + +## Precondition Assertions on `toTER()` and `toErrorCode()` + +Both downcast methods are guarded by `XRPL_ASSERT` on the `type_` tag. Calling `toTER()` on a `Status` whose type is `error_code_i` would produce a nonsensical `TER` value, and silently doing so could corrupt transaction result semantics. The assertions make this a detectable programming error rather than silent data corruption. Callers are expected to check `type()` before invoking either method — the pattern in `AccountTx.cpp` is representative: `error.toErrorCode() != rpcSUCCESS` guards the path before `error.inject(response)` is called. + +## Relationship to Sibling Files + +`RPCHandler.cpp` uses `Status` as the direct return type of `doCommand()` and `callMethod()`, making it the canonical result type of the entire handler dispatch loop. `ErrorCodes.h` supplies the `error_code_i` enumeration and the `inject_error()` and `ErrorInfo` machinery that `Status::inject()` delegates to. `TER.h` supplies the transaction engine result type that `Status::toTER()` recovers. Together, these three files define the full error vocabulary of the RPC subsystem — `Status.h` exists specifically as the adapter layer that lets handler code speak in all three dialects through one type. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/AccountAssets.cpp.ai.json b/src/xrpld/rpc/detail/AccountAssets.cpp.ai.json new file mode 100644 index 0000000000..54d24e5d15 --- /dev/null +++ b/src/xrpld/rpc/detail/AccountAssets.cpp.ai.json @@ -0,0 +1,500 @@ +{ + "args": [ + { + "lineno": 7, + "name": "account" + }, + { + "lineno": 8, + "name": "lrCache" + }, + { + "lineno": 9, + "name": "includeXRP" + }, + { + "lineno": 42, + "name": "account" + }, + { + "lineno": 43, + "name": "lrCache" + }, + { + "lineno": 44, + "name": "includeXRP" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "accountSourceAssets", + "lrCache->getRippleLines", + "rspEntry.getBalance", + "rspEntry.getLimitPeer", + "assets.insert", + "assets.erase", + "lrCache->getMPTs", + "rspEntry.isZeroBalance", + "rspEntry.isMaxedOut", + "assets.insert" + ], + "entry_point": "accountSourceAssets", + "purpose": "Collects all source assets (currencies) an account can send, including XRP if requested, IOUs with positive balance or available credit, and MPTs with nonzero, non-maxed balances.", + "validation_points": [ + "if (includeXRP)", + "if (auto const lines = lrCache->getRippleLines(account, ...))", + "if (saBalance > beast::zero || (rspEntry.getLimitPeer() && ((-saBalance) < rspEntry.getLimitPeer())))", + "assets.erase(badCurrency())", + "if (auto const mpts = lrCache->getMPTs(account))", + "if (!rspEntry.isZeroBalance() && !rspEntry.isMaxedOut())" + ] + }, + { + "call_chain": [ + "accountDestAssets", + "lrCache->getRippleLines", + "rspEntry.getBalance", + "rspEntry.getLimit", + "assets.insert", + "assets.erase", + "lrCache->getMPTs", + "rspEntry.isZeroBalance", + "rspEntry.isMaxedOut", + "assets.insert" + ], + "entry_point": "accountDestAssets", + "purpose": "Collects all destination assets (currencies) an account can receive, including XRP if requested, IOUs with available limit, and MPTs with zero balance and not maxed out.", + "validation_points": [ + "if (includeXRP)", + "if (auto const lines = lrCache->getRippleLines(account, ...))", + "if (saBalance < rspEntry.getLimit())", + "assets.erase(badCurrency())", + "if (auto const mpts = lrCache->getMPTs(account))", + "if (rspEntry.isZeroBalance() && !rspEntry.isMaxedOut())" + ] + } + ], + "data_flows": [ + { + "field": "includeXRP", + "flow": [ + "accountSourceAssets/accountDestAssets argument", + "if (includeXRP)", + "assets.insert(xrpCurrency())" + ], + "origin": "Function argument", + "transformations": [ + "Boolean check to determine if XRP should be included in assets" + ], + "validated_at": "if (includeXRP)" + }, + { + "field": "account", + "flow": [ + "accountSourceAssets/accountDestAssets argument", + "lrCache->getRippleLines(account, ...)", + "lrCache->getMPTs(account)" + ], + "origin": "Function argument", + "transformations": [ + "Used as key to fetch trust lines and MPTs" + ], + "validated_at": "Indirectly validated by existence in cache; no explicit validation" + }, + { + "field": "saBalance", + "flow": [ + "rspEntry.getBalance()", + "if (saBalance > beast::zero || ...)", + "assets.insert(saBalance.get().currency)" + ], + "origin": "rspEntry.getBalance()", + "transformations": [ + "Compared to zero and to peer limit; negated for credit check" + ], + "validated_at": "if (saBalance > beast::zero || ...)" + }, + { + "field": "rspEntry.getLimitPeer()", + "flow": [ + "rspEntry.getLimitPeer()", + "if (rspEntry.getLimitPeer() && ((-saBalance) < rspEntry.getLimitPeer()))" + ], + "origin": "rspEntry.getLimitPeer()", + "transformations": [ + "Checked for existence and compared to negative balance" + ], + "validated_at": "if (rspEntry.getLimitPeer() && ((-saBalance) < rspEntry.getLimitPeer()))" + }, + { + "field": "currency", + "flow": [ + "saBalance.get().currency", + "assets.insert(...)", + "assets.erase(badCurrency())" + ], + "origin": "saBalance.get().currency", + "transformations": [ + "Inserted into set, then possibly erased if matches badCurrency()" + ], + "validated_at": "assets.erase(badCurrency())" + }, + { + "field": "MPTs (Multi-Party Trustlines)", + "flow": [ + "lrCache->getMPTs(account)", + "for (auto const& rspEntry : *mpts)", + "if (!rspEntry.isZeroBalance() && !rspEntry.isMaxedOut()) / if (rspEntry.isZeroBalance() && !rspEntry.isMaxedOut())", + "assets.insert(rspEntry.getMptID())" + ], + "origin": "lrCache->getMPTs(account)", + "transformations": [ + "Filtered by balance and maxed-out status" + ], + "validated_at": "if (!rspEntry.isZeroBalance() && !rspEntry.isMaxedOut()) / if (rspEntry.isZeroBalance() && !rspEntry.isMaxedOut())" + } + ], + "description": "This file defines functions to determine the set of assets (currencies or tokens) that a given account can send (source assets) or receive (destination assets) on the XRPL, using cached ledger data.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "includeXRP", + "empty", + "string", + "validation" + ], + "evidence": "if (includeXRP) at accountSourceAssets, accountDestAssets", + "issue_pattern": "Missing empty string validation for includeXRP", + "why_false_positive": "if (includeXRP) validates includeXRP for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "lrCache->getRippleLines(account, ...) at accountSourceAssets, accountDestAssets", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "lrCache->getRippleLines(account, ...) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "saBalance", + "empty", + "string", + "validation" + ], + "evidence": "saBalance > beast::zero at accountSourceAssets", + "issue_pattern": "Missing empty string validation for saBalance", + "why_false_positive": "saBalance > beast::zero validates saBalance for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "saBalance", + "range", + "bounds", + "validation" + ], + "evidence": "saBalance > beast::zero at accountSourceAssets", + "issue_pattern": "Missing range validation for saBalance", + "why_false_positive": "saBalance > beast::zero validates saBalance range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rspEntry.getLimitPeer()", + "empty", + "string", + "validation" + ], + "evidence": "rspEntry.getLimitPeer() && ((-saBalance) < rspEntry.getLimitPeer()) at accountSourceAssets", + "issue_pattern": "Missing empty string validation for rspEntry.getLimitPeer()", + "why_false_positive": "rspEntry.getLimitPeer() && ((-saBalance) < rspEntry.getLimitPeer()) validates rspEntry.getLimitPeer() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "currency", + "empty", + "string", + "validation" + ], + "evidence": "assets.erase(badCurrency()) at accountSourceAssets, accountDestAssets", + "issue_pattern": "Missing empty string validation for currency", + "why_false_positive": "assets.erase(badCurrency()) validates currency for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "saBalance", + "empty", + "string", + "validation" + ], + "evidence": "saBalance < rspEntry.getLimit() at accountDestAssets", + "issue_pattern": "Missing empty string validation for saBalance", + "why_false_positive": "saBalance < rspEntry.getLimit() validates saBalance for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "saBalance", + "range", + "bounds", + "validation" + ], + "evidence": "saBalance < rspEntry.getLimit() at accountDestAssets", + "issue_pattern": "Missing range validation for saBalance", + "why_false_positive": "saBalance < rspEntry.getLimit() validates saBalance range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rspEntry", + "empty", + "string", + "validation" + ], + "evidence": "!rspEntry.isZeroBalance() && !rspEntry.isMaxedOut() at accountSourceAssets", + "issue_pattern": "Missing empty string validation for rspEntry", + "why_false_positive": "!rspEntry.isZeroBalance() && !rspEntry.isMaxedOut() validates rspEntry for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "rspEntry", + "empty", + "string", + "validation" + ], + "evidence": "rspEntry.isZeroBalance() && !rspEntry.isMaxedOut() at accountDestAssets", + "issue_pattern": "Missing empty string validation for rspEntry", + "why_false_positive": "rspEntry.isZeroBalance() && !rspEntry.isMaxedOut() validates rspEntry for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/AccountAssets.cpp", + "functions": [ + { + "args": [ + "account", + "lrCache", + "includeXRP" + ], + "lineno": 6, + "name": "accountSourceAssets" + }, + { + "args": [ + "account", + "lrCache", + "includeXRP" + ], + "lineno": 41, + "name": "accountDestAssets" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely tested indirectly via higher-level RPC or ledger tests, such as those in 'test/rpc/AccountAssets_test.cpp', 'test/rpc/AccountLines_test.cpp', or integration tests that exercise account asset queries. However, there is no evidence of direct unit tests for these specific validation branches (e.g., edge cases for badCurrency, MPTs, or limit/credit logic). Test coverage may be lacking for: (1) badCurrency filtering, (2) MPTs with edge-case balances, (3) accounts with no trust lines, (4) includeXRP=false scenarios, and (5) negative/zero balances with/without peer credit. Manual or fuzz testing may be needed for full validation path coverage.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (custom logic, no explicit validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none", + "field": "includeXRP", + "location": "accountSourceAssets, accountDestAssets", + "validated_by": "if (includeXRP)", + "validates": [ + "Checks if XRP should be included as an asset" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (returns nullptr if not found)", + "field": "account", + "location": "accountSourceAssets, accountDestAssets", + "validated_by": "lrCache->getRippleLines(account, ...)", + "validates": [ + "Checks if account has ripple lines (trust lines)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "saBalance", + "location": "accountSourceAssets", + "validated_by": "saBalance > beast::zero", + "validates": [ + "Checks if balance is positive (has IOUs to send)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "rspEntry.getLimitPeer()", + "location": "accountSourceAssets", + "validated_by": "rspEntry.getLimitPeer() && ((-saBalance) < rspEntry.getLimitPeer())", + "validates": [ + "Checks if peer extends credit and if there is credit left" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "currency", + "location": "accountSourceAssets, accountDestAssets", + "validated_by": "assets.erase(badCurrency())", + "validates": [ + "Removes any asset with a 'bad' currency code" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "saBalance", + "location": "accountDestAssets", + "validated_by": "saBalance < rspEntry.getLimit()", + "validates": [ + "Checks if account can take more of the asset (below trust line limit)" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "rspEntry", + "location": "accountSourceAssets", + "validated_by": "!rspEntry.isZeroBalance() && !rspEntry.isMaxedOut()", + "validates": [ + "Checks if MPT entry is not zero balance and not maxed out" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "rspEntry", + "location": "accountDestAssets", + "validated_by": "rspEntry.isZeroBalance() && !rspEntry.isMaxedOut()", + "validates": [ + "Checks if MPT entry is zero balance and not maxed out" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/AccountAssets.cpp.ai.md b/src/xrpld/rpc/detail/AccountAssets.cpp.ai.md new file mode 100644 index 0000000000..dce6ee17f8 --- /dev/null +++ b/src/xrpld/rpc/detail/AccountAssets.cpp.ai.md @@ -0,0 +1,37 @@ +# `AccountAssets.cpp` — Asset Eligibility for Path-Finding + +This file implements two free functions, `accountSourceAssets` and `accountDestAssets`, that classify which assets an account is capable of sending or receiving. Both are utility helpers for the XRPL path-finding subsystem, called directly from `PathRequest.cpp` when constructing or updating a payment path request. Their output — a `hash_set` — feeds into the `Pathfinder` to narrow the search space before the expensive graph traversal begins. + +## Role in the Path-Finding Pipeline + +`PathRequest` uses `accountSourceAssets` when the caller has not specified a `SendMax` asset, to enumerate every possible source currency from the sender's holdings. `accountDestAssets` is called to populate the `destination_currencies` field in the path-find response (both the streaming `path_find` API and the legacy `ripple_path_find` API). By computing these sets eagerly from a shared `AssetCache`, the path finder avoids redundant ledger reads and can prune entire asset classes before graph traversal. + +The `AssetCache` parameter is a ledger-snapshot cache shared within a single pathfinding session. It holds pre-fetched trust lines keyed by account and `LineDirection`, and a separate map of `PathFindMPT` entries per account. Both functions pass `LineDirection::outgoing`, which returns all trust lines for the queried account rather than filtering out those with rippling disabled — appropriate for the source/destination endpoint itself as opposed to an intermediate hop. + +## `accountSourceAssets` — What Can an Account Send? + +The function builds the eligible set in three stages: + +**XRP**: Optionally inserted unconditionally if `includeXRP` is true. The comment `YYY Only bother if they are above reserve` notes a known limitation — no reserve check is performed. The XRP native currency is included as a `PathAsset` via `xrpCurrency()`. + +**IOU trust lines**: Each outgoing `PathFindTrustLine` is evaluated against two conditions that mirror the XRPL's on-ledger credit semantics. A currency is sendable if either the account's balance is positive (it holds issued currency it can push outward) or the account has a negative balance that is less negative than the peer's trust limit (the peer has extended enough credit that there is still room to draw from). The second condition uses `(-saBalance) < getLimitPeer()` — negating the balance to express "amount currently owed" and comparing to the peer's ceiling. This captures the case where an account is a borrower with available headroom. + +**MPTs (Multi-Protocol Tokens)**: An MPT issuance is included as a source asset if the account holds a non-zero balance (`!isZeroBalance()`) and the issuance has not been fully saturated (`!isMaxedOut()`). The `maxedOut_` flag reflects whether the issuer's total outstanding amount has reached its configured maximum, which would prevent any further transfers. + +After both IOU and MPT passes, `assets.erase(badCurrency())` removes the reserved sentinel currency code, a defensive step that prevents a malformed or zero-currency entry from propagating into path search. + +## `accountDestAssets` — What Can an Account Receive? + +The structure parallels `accountSourceAssets` but applies the opposite criterion. An IOU currency is receivable if the account's current balance is below its self-imposed trust limit (`saBalance < getLimit()`), meaning there is room to absorb more. Crucially, this check is against the account's own limit, not the peer's, because the destination controls how much of a given IOU it is willing to hold. + +The MPT filter is inverted relative to the source check: `isZeroBalance() && !isMaxedOut()`. A zero balance here indicates the account holds none of the MPT yet (fresh capacity), while `!isMaxedOut()` confirms the issuer can still satisfy new transfers. This asymmetry between source and destination is intentional — a source needs tokens on hand to push, while a destination is only a meaningful landing spot when it has no existing holding and the supply ceiling permits issuance. + +The comment "Even if account doesn't exist" on the XRP insertion reflects a deliberate policy: XRP is always a valid destination currency regardless of whether the destination account is funded, because account creation itself requires receiving XRP above the base reserve. + +## Design Observations + +Both functions deliberately avoid throwing exceptions. The `if (auto const lines = ...)` and `if (auto const mpts = ...)` guards use optional-like conditional binding, returning early or simply skipping that asset class if the cache has no entry for the account. This matters for accounts that have never opened a trust line or issued an MPT — no crash, no error propagation, just an empty or XRP-only result. + +The use of `hash_set` is appropriate because `PathAsset` is a `std::variant` and deduplication across the trust line scan is important; an account could have multiple lines denominated in the same currency with different peers, but the path finder only needs one entry per currency in its initial asset set. + +The shared `AssetCache` parameter is the performance critical piece. Since a single path request can probe many intermediate accounts, the cache amortizes ledger reads across all `accountSourceAssets`/`accountDestAssets` calls within a session, avoiding redundant SLE lookups on a read-only ledger snapshot. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/AccountAssets.h.ai.json b/src/xrpld/rpc/detail/AccountAssets.h.ai.json new file mode 100644 index 0000000000..932635fd99 --- /dev/null +++ b/src/xrpld/rpc/detail/AccountAssets.h.ai.json @@ -0,0 +1,58 @@ +{ + "args": [ + { + "lineno": 10, + "name": "account" + }, + { + "lineno": 11, + "name": "cache" + }, + { + "lineno": 12, + "name": "includeXRP" + }, + { + "lineno": 15, + "name": "account" + }, + { + "lineno": 16, + "name": "lrLedger" + }, + { + "lineno": 17, + "name": "includeXRP" + } + ], + "classes": [], + "description": "This header file declares functions for retrieving destination and source assets associated with an account, using an asset cache, within the xrpl namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/AccountAssets.h", + "functions": [ + { + "args": [ + "account", + "cache", + "includeXRP" + ], + "lineno": 9, + "name": "accountDestAssets" + }, + { + "args": [ + "account", + "lrLedger", + "includeXRP" + ], + "lineno": 14, + "name": "accountSourceAssets" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/AccountAssets.h.ai.md b/src/xrpld/rpc/detail/AccountAssets.h.ai.md new file mode 100644 index 0000000000..eecffc31f6 --- /dev/null +++ b/src/xrpld/rpc/detail/AccountAssets.h.ai.md @@ -0,0 +1,26 @@ +# `AccountAssets.h` — Asset Eligibility Interface for the Path-Finder + +This header declares two free functions that answer a focused question for the XRPL payment path-finding subsystem: given an account, which assets can it currently send, and which can it receive? The functions expose only what `PathRequest.cpp` needs to seed a path-finding session without coupling callers to the `AssetCache` internals or the raw trust line / MPT representations. + +## Context in the Path-Finding Pipeline + +`PathRequest` drives the `ripple_path_find` and `path_find` RPC commands. Before invoking the expensive graph traversal in `Pathfinder`, it needs to scope the search to assets that are actually usable. Two call sites in `PathRequest.cpp` consume these functions: + +- When building the initial response, `accountDestAssets` is called with the destination account to populate `destination_currencies` — the list the caller uses to understand which assets the destination can accept. +- When auto-detecting source currencies (i.e., no explicit `send_max` was provided), `accountSourceAssets` is called with the sending account to discover its spendable assets, capped at `RPC::Tuning::max_auto_src_cur`. + +Both functions receive an `AssetCache` — a short-lived, ledger-snapshot cache that pre-fetches and memoizes trust lines and MPT entries for a single pathfinding session. Sharing the cache across repeated calls for the same account avoids redundant ledger reads and keeps memory proportional to accounts actually visited. + +## The Two Functions + +`accountSourceAssets` determines what an account can pay with. For IOU trust lines it checks two conditions per line: either the account holds a positive balance, or the peer extends a credit limit that hasn't been fully consumed (i.e., `(-balance) < limitPeer`). This captures both the case where the sender literally has tokens and the case where the sender can effectively borrow up to the peer's credit extension. For Multi-Purpose Tokens (MPTs), a holding is source-eligible only when the balance is non-zero and the MPT is not at its maximum outstanding amount. After processing trust lines, `badCurrency()` is explicitly erased from the result — a defensive measure ensuring the protocol's sentinel "bad currency" value can never propagate as a valid payment asset. + +`accountDestAssets` checks what an account can receive. For trust lines the condition is simply `balance < ownLimit`: the account hasn't consumed its self-imposed credit ceiling, so more of the IOU can flow in. For MPTs the logic inverts relative to the source check: the account must have a *zero* balance while not being maxed out. This reflects that an MPT holder relationship is established at zero before funds arrive, so a freshly authorized (or recently emptied) holder is the right receive target. The inline comment "// Even if account doesn't exist" clarifies the XRP case — native XRP can be sent to an unfunded account, so XRP is added to the destination set independently of whether the account has an on-ledger entry. + +## The `includeXRP` Flag + +Both functions accept a `bool includeXRP` parameter rather than hard-coding XRP inclusion. Callers use this to suppress XRP in scenarios where it is irrelevant — for example, when the destination account has set the `lsfDisallowXRP` flag, `PathRequest` passes `!disallowXRP` to `accountDestAssets`. This keeps the policy decision at the RPC layer where the business rule lives, rather than buried inside the asset-enumeration logic. + +## Relationship to `AssetCache` and `PathAsset` + +The `AssetCache` dependency (declared in `AssetCache.h`) is the only include this header requires beyond `UintTypes.h`. `AssetCache` internally uses `PathFindTrustLine` and `PathFindMPT` wrappers around ledger SLE data, both designed to minimize per-instance memory for the path-finder's high-volume usage. The return type `hash_set` is a variant-like type that can hold either a currency code (for IOUs and XRP) or an `MPTID`, allowing source and destination asset sets to uniformly represent both token classes. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/AssetCache.cpp.ai.json b/src/xrpld/rpc/detail/AssetCache.cpp.ai.json new file mode 100644 index 0000000000..f8def66cc6 --- /dev/null +++ b/src/xrpld/rpc/detail/AssetCache.cpp.ai.json @@ -0,0 +1,307 @@ +{ + "args": [ + { + "lineno": 8, + "name": "ledger" + }, + { + "lineno": 8, + "name": "j" + }, + { + "lineno": 18, + "name": "accountID" + }, + { + "lineno": 18, + "name": "direction" + }, + { + "lineno": 74, + "name": "account" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "AssetCache::getRippleLines", + "PathFindTrustLine::getItems (if cache miss)", + "std::make_shared>", + "XRPL_ASSERT (multiple points)" + ], + "entry_point": "AssetCache::getRippleLines", + "purpose": "Fetches trust lines for an account in a given direction, using a cache to avoid recomputation. Handles cache invalidation and ensures only one set of trust lines per account is stored.", + "validation_points": [ + "XRPL_ASSERT(size <= totalLineCount_, ...): Ensures the number of lines being erased does not exceed the total count.", + "XRPL_ASSERT(it->second == nullptr, ...): Ensures that a new cache entry is only created if it was previously null.", + "XRPL_ASSERT(!it->second || !it->second->empty(), ...): Ensures that the cache entry is either null or non-empty after population." + ] + } + ], + "data_flows": [ + { + "field": "totalLineCount_", + "flow": [ + "constructor (set to 0)", + "incremented in getRippleLines when new trust lines are added", + "decremented in getRippleLines when old trust lines are erased" + ], + "origin": "Initialized to 0 in AssetCache constructor", + "transformations": [ + "Incremented by size of new trust lines vector", + "Decremented by size of erased trust lines vector" + ], + "validated_at": "XRPL_ASSERT(size <= totalLineCount_, ...): Ensures we never subtract more than we have" + }, + { + "field": "lines_ (cache map)", + "flow": [ + "constructor (empty)", + "populated in getRippleLines via emplace", + "entries erased in getRippleLines if direction flip detected", + "entries updated with new trust lines" + ], + "origin": "Initialized empty in AssetCache constructor", + "transformations": [ + "Emplaced with nullptr, then set to shared_ptr>", + "Erased if direction flip and subset/superset logic applies" + ], + "validated_at": "XRPL_ASSERT(it->second == nullptr, ...): Ensures no double population" + }, + { + "field": "accountID", + "flow": [ + "passed to hasher_ to generate hash", + "used to construct AccountKey", + "used in PathFindTrustLine::getItems" + ], + "origin": "Input parameter to getRippleLines", + "transformations": [ + "Hashed for cache key", + "Used as lookup and for trust line retrieval" + ], + "validated_at": "Indirectly validated by cache logic and XRPL_ASSERTs on cache state" + }, + { + "field": "direction", + "flow": [ + "used to construct AccountKey and otherkey", + "used to determine cache logic (subset/superset)", + "passed to PathFindTrustLine::getItems" + ], + "origin": "Input parameter to getRippleLines", + "transformations": [ + "Flipped to get otherkey", + "Determines which trust lines are fetched" + ], + "validated_at": "Indirectly validated by cache logic and XRPL_ASSERTs" + } + ], + "description": "Implements the AssetCache class for caching trust lines and multiparty tokens (MPTs) for accounts in an XRPL ledger, optimizing repeated lookups for pathfinding and asset queries.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_ (type-checked as std::shared_ptr)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_ (type-checked as std::shared_ptr) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for ledger_ (type-checked as std::shared_ptr)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates ledger_ (type-checked as std::shared_ptr) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "accountID (type-checked as AccountID)", + "validation", + "missing", + "check" + ], + "evidence": "Field accountID (type-checked as AccountID) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for accountID (type-checked as AccountID)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates accountID (type-checked as AccountID) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "direction (type-checked as LineDirection)", + "validation", + "missing", + "check" + ], + "evidence": "Field direction (type-checked as LineDirection) validated by XRPL_ASSERT (custom assertion macro), C++ type system", + "issue_pattern": "Missing validation for direction (type-checked as LineDirection)", + "why_false_positive": "XRPL_ASSERT (custom assertion macro), C++ type system validates direction (type-checked as LineDirection) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "totalLineCount_ vs size", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at AssetCache::getRippleLines", + "issue_pattern": "Missing empty string validation for totalLineCount_ vs size", + "why_false_positive": "XRPL_ASSERT validates totalLineCount_ vs size for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/AssetCache.cpp", + "functions": [ + { + "args": [ + "std::shared_ptr const& ledger", + "beast::Journal j" + ], + "lineno": 8, + "name": "AssetCache::AssetCache" + }, + { + "args": [], + "lineno": 13, + "name": "AssetCache::~AssetCache" + }, + { + "args": [ + "AccountID const& accountID", + "LineDirection direction" + ], + "lineno": 18, + "name": "AssetCache::getRippleLines" + }, + { + "args": [ + "xrpl::AccountID const& account" + ], + "lineno": 74, + "name": "AssetCache::getMPTs" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage (no test code or test macros). Likely, tests would exist in the unit test suite for the RPC or ledger modules, possibly in files like test/rpc/AssetCache_test.cpp or test/rpc/PathFindTrustLine_test.cpp. The critical validation paths (XRPL_ASSERTs) are not directly tested here; coverage depends on whether tests exercise cache invalidation, direction flipping, and trust line population/erasure. Gaps may exist if tests do not cover edge cases such as direction flips, empty trust lines, or cache overwrites.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger_ (type-checked as std::shared_ptr)", + "accountID (type-checked as AccountID)", + "direction (type-checked as LineDirection)" + ], + "framework": "XRPL_ASSERT (custom assertion macro), C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely throws or aborts)", + "field": "totalLineCount_ vs size", + "location": "AssetCache::getRippleLines", + "validated_by": "XRPL_ASSERT", + "validates": [ + "Ensures that the number of trust lines being erased (size) does not exceed the totalLineCount_", + "Prevents underflow or logic error in trust line accounting" + ], + "validation_type": "range|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/AssetCache.cpp.ai.md b/src/xrpld/rpc/detail/AssetCache.cpp.ai.md new file mode 100644 index 0000000000..06fcbfea34 --- /dev/null +++ b/src/xrpld/rpc/detail/AssetCache.cpp.ai.md @@ -0,0 +1,41 @@ +# `AssetCache.cpp` — Pathfinder Asset Cache for Trust Lines and MPTs + +## Role in the System + +`AssetCache` is a per-ledger, thread-safe cache that sits between the XRPL pathfinding engine and the ledger state database. It exists because the `Pathfinder` algorithm repeatedly queries trust lines and MPTokenIssuance objects for the same accounts during a single path-search pass — without a cache, each traversal step would re-read ledger SLEs from disk. The cache is scoped to a single immutable `ReadView` (one ledger version), which means its contents never become stale within the lifetime of a path request. + +The class is declared as `final` and inherits `CountedObject`, placing it under the XRPL object-counting diagnostic framework so live instances can be observed in memory stats. The destructor logs the final cache size at `debug` level — how many accounts were touched and how many distinct trust lines were materialized — giving operators insight into path-search workload. + +## Trust Line Caching: The Direction Superset Optimization + +The central complexity in `AssetCache` is `getRippleLines()`, which caches `PathFindTrustLine` vectors keyed by `AccountKey` — a composite of `AccountID`, `LineDirection`, and a precomputed hash. The `LineDirection` enum captures whether an account is "outgoing" (source or rippling-enabled side) or "incoming" (rippling-disabled side) on a path. + +The direction distinction matters because `PathFindTrustLine::getItems()` filters trust lines differently depending on the direction: outgoing accounts return **all** trust lines, while incoming accounts return only the subset with rippling enabled. This means the outgoing set is always a superset of the incoming set for the same account. + +`getRippleLines()` exploits this relationship to prevent the same account from occupying two cache entries with overlapping data. The logic at the top of the function: + +1. Computes both `key` (the requested direction) and `otherkey` (the opposite direction). +2. Checks whether an entry already exists for `otherkey`. +3. If an **incoming** entry already exists and **outgoing** is requested, the incoming subset is **erased** and replaced by the larger outgoing set. The comment makes the motivation explicit: *"The full set will be built below, and will be returned, if needed, on subsequent calls for either value of outgoing."* +4. If an **outgoing** entry already exists and **incoming** is requested, the function simply **returns the outgoing superset** directly, redirecting the key to avoid storing a duplicate. + +This means the cache always converges to storing at most one entry per account — the outgoing set — regardless of the order in which direction variants are requested. The `totalLineCount_` counter is kept consistent across these insertions and erasures, and an `XRPL_ASSERT` guards against underflow when subtracting the erased entry's size. + +After the direction-reconciliation logic, the function emplaces a `nullptr` sentinel and then populates it with the results of `PathFindTrustLine::getItems()`. This two-phase approach (emplace with null, then fill) is guarded by a second `XRPL_ASSERT` ensuring the emplace only fires when the slot genuinely started as null, preventing accidental double-population. + +The returned value is a `shared_ptr>` rather than a raw vector. The header comment explains the memory rationale: the estimate is that over 90% of accounts on the ledger have no usable trust lines for a given path search, so storing a null `shared_ptr` for those accounts costs far less than allocating empty vectors. The `shared_ptr` wrapper also lets map entries be safely shared across threads without copying. + +## MPT Caching: `getMPTs()` + +Multi-Purpose Token (MPT) caching in `getMPTs()` is structurally simpler. For a given account, it iterates through all items in the account's owner directory via `forEachItem()` and collects two categories: + +- `ltMPTOKEN_ISSUANCE` entries: the account is the issuer; a `PathFindMPT` is built with `zeroBalance = false` and `maxedOut` derived from whether `sfOutstandingAmount` has reached `maxMPTAmount`. +- `ltMPTOKEN` entries: the account is a token holder; `zeroBalance` reflects whether `sfMPTAmount == 0`, and `maxedOut` is determined by reading the corresponding issuance SLE. If the issuance SLE cannot be found, `maxedOut` defaults to `true` (conservative safe assumption). + +Like the trust line cache, accounts with no MPTs are stored as `nullptr` rather than an empty vector. The function returns `const&` to the internal `shared_ptr`, avoiding an unnecessary reference-count increment on the hot path. + +## Concurrency Model + +Both `getRippleLines()` and `getMPTs()` acquire `mLock` — a plain `std::mutex` — via `std::lock_guard` for the entire operation. This provides straightforward mutual exclusion: concurrent `Pathfinder` instances working on the same ledger (via a shared `AssetCache`) serialize their cache lookups and insertions. The granularity is intentionally coarse; the operations complete quickly (a hash map lookup or a single ledger directory scan), so the cost of holding the lock is small relative to the alternative of fine-grained per-entry locking. + +The `ledger_` member is a `shared_ptr`, meaning the cache keeps the ledger snapshot alive as long as any `Pathfinder` holds an `AssetCache` reference — this is correct and intentional, as the ledger must not be freed while path results derived from it are still being assembled. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/AssetCache.h.ai.json b/src/xrpld/rpc/detail/AssetCache.h.ai.json new file mode 100644 index 0000000000..b4a42db573 --- /dev/null +++ b/src/xrpld/rpc/detail/AssetCache.h.ai.json @@ -0,0 +1,112 @@ +{ + "args": [ + { + "lineno": 12, + "name": "l" + }, + { + "lineno": 12, + "name": "j" + }, + { + "lineno": 33, + "name": "accountID" + }, + { + "lineno": 33, + "name": "direction" + }, + { + "lineno": 45, + "name": "account" + }, + { + "lineno": 57, + "name": "account" + }, + { + "lineno": 57, + "name": "direction" + }, + { + "lineno": 57, + "name": "hash" + }, + { + "lineno": 62, + "name": "other" + }, + { + "lineno": 65, + "name": "other" + }, + { + "lineno": 69, + "name": "lhs" + }, + { + "lineno": 84, + "name": "key" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const& l", + "beast::Journal j" + ], + "lineno": 10, + "name": "AssetCache" + }, + { + "args": [ + "AccountID const& account", + "LineDirection direction", + "std::size_t hash" + ], + "lineno": 54, + "name": "AccountKey" + }, + { + "args": [], + "lineno": 80, + "name": "Hash" + } + ], + "description": "Defines the AssetCache class, which caches trust lines and multipath tokens (MPTs) for accounts in an XRPL ledger to optimize pathfinding operations.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/AssetCache.h", + "functions": [ + { + "args": [], + "lineno": 18, + "name": "getLedger" + }, + { + "args": [ + "accountID", + "direction" + ], + "lineno": 32, + "name": "getRippleLines" + }, + { + "args": [ + "account" + ], + "lineno": 44, + "name": "getMPTs" + }, + { + "args": [], + "lineno": 74, + "name": "get_hash" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/AssetCache.h.ai.md b/src/xrpld/rpc/detail/AssetCache.h.ai.md new file mode 100644 index 0000000000..4402d463c5 --- /dev/null +++ b/src/xrpld/rpc/detail/AssetCache.h.ai.md @@ -0,0 +1,40 @@ +# AssetCache + +`AssetCache` is a per-ledger, thread-safe cache purpose-built for the XRPL `Pathfinder`. During a pathfinding traversal, the engine iterates over many accounts and must repeatedly query their trust lines and Multi-Purpose Token (MPT) holdings. Without a cache, every step along a candidate path would re-read those entries from the underlying ledger view — an expensive operation at scale. `AssetCache` amortizes that cost by loading each account's asset data once and reusing it for the duration of a single pathfinding session. + +## Ledger Binding + +An `AssetCache` is constructed with a `std::shared_ptr`, which is a read-only snapshot of a specific ledger version. All queries are answered against that snapshot, so the cache stays coherent. The ledger sequence is logged on both construction and destruction, making it straightforward to correlate cache lifetimes with ledger state during debugging. The destructor also logs the total number of accounts and distinct trust lines accumulated, which is useful for performance analysis. + +## Trust Line Caching and the Direction Optimization + +The most interesting part of `AssetCache` is how it handles `getRippleLines()`. The `Pathfinder` must know which trust lines an account can actually use at a given position in a path, and that depends on `LineDirection`. An *outgoing* account (the source, or an account reached via a rippling-enabled trust line) can use all of its trust lines. An *incoming* account (reached via a no-ripple trust line) can only use trust lines where rippling is enabled on its side — a strict subset. + +Storing two separate sets for the same account — one for incoming, one for outgoing — would waste memory, since the outgoing set is always a superset of the incoming set. The implementation exploits this relationship directly. The internal `AccountKey` struct encodes both the `AccountID` and the `LineDirection`, and `getRippleLines()` always checks for the *opposite* direction's entry before inserting a new one: + +- If the cache holds an **outgoing** entry and an **incoming** request arrives, the outgoing set is already a superset of what's needed. The method returns the outgoing set directly without loading anything new, and updates the lookup key so the returned pointer corresponds to the stored entry. +- If the cache holds an **incoming** entry and an **outgoing** request arrives, the stored subset is now incorrect for the broader query. The old entry is erased (with `totalLineCount_` adjusted accordingly) and a fresh full set is loaded from the ledger. + +This logic ensures at most one `vector` exists per account in the cache at any time, keeping peak memory use minimal even when the pathfinder queries the same account with different directions in different traversals. + +## The `shared_ptr` Idiom + +Rather than storing a `vector` directly in the map, the cache stores `std::shared_ptr>`. When an account has no trust lines, the map entry is inserted with a `nullptr` rather than an empty vector. The comment in the header explains the reasoning: more than 90% of accounts are estimated to have no trust lines at all. Storing a null pointer for those accounts avoids allocating a vector object entirely, which — at the scale of pathfinding across a large ledger — is a meaningful memory saving. Callers must check the returned pointer for null before iterating. + +The `shared_ptr` wrapper also serves a secondary purpose: entries can be safely handed out to callers while the map is unlocked. If a future eviction or re-fetch were added, outstanding `shared_ptr` copies held by the `Pathfinder` would remain valid. + +## AccountKey and Hashing + +`AccountKey` is a small private struct that bundles `AccountID`, `LineDirection`, and a pre-computed hash. The hash is computed once from the `AccountID` alone (using `xrpl::hardened_hash<>`) and reused for both the outgoing and incoming keys, since they share the same account. This avoids re-hashing when checking for the opposite-direction entry. The `Hash` functor is a passthrough to `get_hash()`, so the map never recomputes the hash during lookup. + +## MPT Caching + +`getMPTs()` follows a simpler pattern. It walks the account's directory entries via `forEachItem`, collecting any `ltMPTOKEN_ISSUANCE` entries (tokens the account has issued) and `ltMPTOKEN` entries (tokens the account holds). Each `PathFindMPT` captures three facts: the MPTID, whether the holder's balance is zero, and whether the issuance has reached its maximum outstanding amount. These flags allow the `Pathfinder` to skip unusable MPT paths early without additional ledger reads. Like trust lines, the map stores a null pointer rather than an empty vector when an account has no MPTs. + +## Thread Safety + +`AssetCache` is shared across path requests via `std::shared_ptr`, and both `getRippleLines()` and `getMPTs()` acquire `mLock` for their entire duration. There is no lock-free fast path or reader-writer split — the expectation is that the coarse-grained mutex is sufficient given that cache hits are rare (first call per account always misses) and the locked section is short. + +## Relationship to Callers + +`Pathfinder` holds a `std::shared_ptr` and passes it through the path-expansion steps. `PathRequest` also receives an `AssetCache` on `doCreate()`, `doUpdate()`, and `findPaths()`, allowing it to share the same cache across multiple pathfinder invocations within a single request lifecycle. The cache is thus scoped to one client request session, not shared across requests, which keeps it from accumulating stale entries across different ledgers. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/DeliveredAmount.cpp.ai.json b/src/xrpld/rpc/detail/DeliveredAmount.cpp.ai.json new file mode 100644 index 0000000000..5b024146d5 --- /dev/null +++ b/src/xrpld/rpc/detail/DeliveredAmount.cpp.ai.json @@ -0,0 +1,538 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "insertDeliveredAmount", + "canHaveDeliveredAmount", + "getDeliveredAmount (template)", + "transactionMeta.getDeliveredAmount", + "serializedTx->isFieldPresent(sfAmount)", + "serializedTx->getFieldAmount(sfAmount)" + ], + "entry_point": "insertDeliveredAmount", + "purpose": "Inserts the delivered amount into a JSON meta object for a transaction, if available and valid.", + "validation_points": [ + "insertDeliveredAmount: calls canHaveDeliveredAmount to check if delivered amount is possible", + "canHaveDeliveredAmount: validates serializedTx is not null, checks transaction type and result", + "getDeliveredAmount: validates serializedTx is not null, checks transactionMeta.getDeliveredAmount().has_value(), checks serializedTx->isFieldPresent(sfAmount), validates ledger index and close time" + ] + }, + { + "call_chain": [ + "getDeliveredAmount (template)", + "transactionMeta.getDeliveredAmount", + "serializedTx->isFieldPresent(sfAmount)", + "serializedTx->getFieldAmount(sfAmount)" + ], + "entry_point": "getDeliveredAmount (template)", + "purpose": "Determines the delivered amount for a transaction, considering ledger version and transaction metadata.", + "validation_points": [ + "getDeliveredAmount: serializedTx null check", + "getDeliveredAmount: transactionMeta.getDeliveredAmount().has_value()", + "getDeliveredAmount: serializedTx->isFieldPresent(sfAmount)", + "getDeliveredAmount: ledger index and close time validation" + ] + }, + { + "call_chain": [ + "canHaveDeliveredAmount" + ], + "entry_point": "canHaveDeliveredAmount", + "purpose": "Checks if a transaction could possibly have a delivered amount, based on type and result.", + "validation_points": [ + "canHaveDeliveredAmount: serializedTx null check", + "canHaveDeliveredAmount: transaction type and result validation" + ] + } + ], + "data_flows": [ + { + "field": "serializedTx", + "flow": [ + "insertDeliveredAmount argument", + "canHaveDeliveredAmount argument", + "getDeliveredAmount argument", + "used for field presence and type checks" + ], + "origin": "Passed as argument to insertDeliveredAmount/getDeliveredAmount/canHaveDeliveredAmount", + "transformations": [ + "Checked for null", + "Used to get transaction type", + "Used to check for sfAmount field", + "Used to extract sfAmount value" + ], + "validated_at": "canHaveDeliveredAmount (null check), getDeliveredAmount (null check), isFieldPresent(sfAmount)" + }, + { + "field": "transactionMeta.getDeliveredAmount()", + "flow": [ + "insertDeliveredAmount argument", + "getDeliveredAmount argument", + "transactionMeta.getDeliveredAmount()" + ], + "origin": "transactionMeta object (argument)", + "transformations": [ + "Checked for has_value()", + "If present, returned as delivered amount" + ], + "validated_at": "getDeliveredAmount (has_value() check)" + }, + { + "field": "sfAmount", + "flow": [ + "serializedTx->isFieldPresent(sfAmount)", + "serializedTx->getFieldAmount(sfAmount)" + ], + "origin": "serializedTx object", + "transformations": [ + "Presence checked", + "Value extracted if present" + ], + "validated_at": "getDeliveredAmount (isFieldPresent(sfAmount))" + }, + { + "field": "ledger index and close time", + "flow": [ + "ledger.header()", + "getLedgerIndex lambda", + "getCloseTime lambda", + "used in getDeliveredAmount" + ], + "origin": "ledger.header() via ReadView", + "transformations": [ + "Compared to hardcoded thresholds (4594095, 446000000s)" + ], + "validated_at": "getDeliveredAmount (ledger index and close time check)" + }, + { + "field": "meta[jss::delivered_amount]", + "flow": [ + "insertDeliveredAmount", + "set to delivered amount or 'unavailable'" + ], + "origin": "Json::Value meta object", + "transformations": [ + "Set to delivered amount JSON if available", + "Set to string 'unavailable' if not" + ], + "validated_at": "insertDeliveredAmount (after getDeliveredAmount result)" + } + ], + "description": "Implements logic to determine and insert the delivered amount for a transaction, considering ledger version and transaction metadata, for use in XRPL RPC responses.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "serializedTx", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at getDeliveredAmount", + "issue_pattern": "Missing empty string validation for serializedTx", + "why_false_positive": "explicit null check validates serializedTx for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "serializedTx", + "type", + "validation", + "check" + ], + "evidence": "explicit null check at getDeliveredAmount", + "issue_pattern": "Missing type validation for serializedTx", + "why_false_positive": "explicit null check validates serializedTx type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "deliveredAmount (from transactionMeta)", + "empty", + "string", + "validation" + ], + "evidence": "transactionMeta.getDeliveredAmount().has_value() at getDeliveredAmount", + "issue_pattern": "Missing empty string validation for deliveredAmount (from transactionMeta)", + "why_false_positive": "transactionMeta.getDeliveredAmount().has_value() validates deliveredAmount (from transactionMeta) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sfAmount (field presence in serializedTx)", + "empty", + "string", + "validation" + ], + "evidence": "serializedTx->isFieldPresent(sfAmount) at getDeliveredAmount", + "issue_pattern": "Missing empty string validation for sfAmount (field presence in serializedTx)", + "why_false_positive": "serializedTx->isFieldPresent(sfAmount) validates sfAmount (field presence in serializedTx) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger index and close time", + "empty", + "string", + "validation" + ], + "evidence": "getLedgerIndex() >= 4594095 || getCloseTime() > NetClock::time_point{446000000s} at getDeliveredAmount", + "issue_pattern": "Missing empty string validation for ledger index and close time", + "why_false_positive": "getLedgerIndex() >= 4594095 || getCloseTime() > NetClock::time_point{446000000s} validates ledger index and close time for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "serializedTx", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at canHaveDeliveredAmount", + "issue_pattern": "Missing empty string validation for serializedTx", + "why_false_positive": "explicit null check validates serializedTx for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "serializedTx", + "type", + "validation", + "check" + ], + "evidence": "explicit null check at canHaveDeliveredAmount", + "issue_pattern": "Missing type validation for serializedTx", + "why_false_positive": "explicit null check validates serializedTx type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction type (tt)", + "empty", + "string", + "validation" + ], + "evidence": "serializedTx->getTxnType() == ttPAYMENT || ttCHECK_CASH || ttACCOUNT_DELETE at canHaveDeliveredAmount", + "issue_pattern": "Missing empty string validation for transaction type (tt)", + "why_false_positive": "serializedTx->getTxnType() == ttPAYMENT || ttCHECK_CASH || ttACCOUNT_DELETE validates transaction type (tt) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction result (transactionMeta.getResultTER())", + "empty", + "string", + "validation" + ], + "evidence": "transactionMeta.getResultTER() == tesSUCCESS at canHaveDeliveredAmount", + "issue_pattern": "Missing empty string validation for transaction result (transactionMeta.getResultTER())", + "why_false_positive": "transactionMeta.getResultTER() == tesSUCCESS validates transaction result (transactionMeta.getResultTER()) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/DeliveredAmount.cpp", + "functions": [ + { + "args": [ + "getLedgerIndex", + "getCloseTime", + "serializedTx", + "transactionMeta" + ], + "lineno": 18, + "name": "getDeliveredAmount" + }, + { + "args": [ + "serializedTx", + "transactionMeta" + ], + "lineno": 56, + "name": "canHaveDeliveredAmount" + }, + { + "args": [ + "meta", + "ledger", + "serializedTx", + "transactionMeta" + ], + "lineno": 70, + "name": "insertDeliveredAmount" + }, + { + "args": [ + "context", + "serializedTx", + "transactionMeta", + "getLedgerIndex" + ], + "lineno": 92, + "name": "getDeliveredAmount" + }, + { + "args": [ + "context", + "serializedTx", + "transactionMeta", + "ledgerIndex" + ], + "lineno": 109, + "name": "getDeliveredAmount" + }, + { + "args": [ + "meta", + "context", + "transaction", + "transactionMeta" + ], + "lineno": 117, + "name": "insertDeliveredAmount" + }, + { + "args": [ + "meta", + "context", + "transaction", + "transactionMeta" + ], + "lineno": 124, + "name": "insertDeliveredAmount" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "RPC" + } + ], + "test_coverage_notes": "The code is likely covered by integration and unit tests for transaction RPCs, especially those that check the 'delivered_amount' field in transaction metadata. Tests would be found in files like 'test/rpc/DeliveredAmount_test.cpp', 'test/rpc/Transaction_test.cpp', or similar. Edge cases such as missing fields, old ledgers, and failed transactions should be tested, but coverage gaps may exist for rare ledger boundary conditions (e.g., ledgers just before/after 4594095 or close times near 446000000s), and for malformed or null serializedTx inputs. Template-based code may have limited direct unit tests unless specifically instantiated in test code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None explicit in this file; uses explicit C++ checks and protocol enums", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns empty optional)", + "field": "serializedTx", + "location": "getDeliveredAmount", + "validated_by": "explicit null check", + "validates": [ + "Checks if serializedTx is not null before proceeding" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns deliveredAmount if present)", + "field": "deliveredAmount (from transactionMeta)", + "location": "getDeliveredAmount", + "validated_by": "transactionMeta.getDeliveredAmount().has_value()", + "validates": [ + "Checks if deliveredAmount is present in transactionMeta" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (skips logic if not present)", + "field": "sfAmount (field presence in serializedTx)", + "location": "getDeliveredAmount", + "validated_by": "serializedTx->isFieldPresent(sfAmount)", + "validates": [ + "Checks if sfAmount field is present in serializedTx" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns empty optional if not satisfied)", + "field": "ledger index and close time", + "location": "getDeliveredAmount", + "validated_by": "getLedgerIndex() >= 4594095 || getCloseTime() > NetClock::time_point{446000000s}", + "validates": [ + "Checks if ledger index is after DeliveredAmount feature activation", + "Checks if close time is after DeliveredAmount feature activation" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false)", + "field": "serializedTx", + "location": "canHaveDeliveredAmount", + "validated_by": "explicit null check", + "validates": [ + "Checks if serializedTx is not null before proceeding" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false if not valid type)", + "field": "transaction type (tt)", + "location": "canHaveDeliveredAmount", + "validated_by": "serializedTx->getTxnType() == ttPAYMENT || ttCHECK_CASH || ttACCOUNT_DELETE", + "validates": [ + "Checks if transaction type is one of ttPAYMENT, ttCHECK_CASH, ttACCOUNT_DELETE" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (returns false if not tesSUCCESS)", + "field": "transaction result (transactionMeta.getResultTER())", + "location": "canHaveDeliveredAmount", + "validated_by": "transactionMeta.getResultTER() == tesSUCCESS", + "validates": [ + "Checks if transaction result is tesSUCCESS" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/DeliveredAmount.cpp.ai.md b/src/xrpld/rpc/detail/DeliveredAmount.cpp.ai.md new file mode 100644 index 0000000000..6a6ac9ccfd --- /dev/null +++ b/src/xrpld/rpc/detail/DeliveredAmount.cpp.ai.md @@ -0,0 +1,42 @@ +# `DeliveredAmount.cpp` — Resolving the True Payment Delivery Amount for RPC + +## Purpose and Context + +On the XRP Ledger, a payment transaction carries an `Amount` field that specifies the *intended* or *maximum* delivery. For partial payments — where fees, exchange rates, or path constraints reduce what actually arrives — the `Amount` field can be misleading to clients. This file implements the logic that resolves the actual delivered quantity and injects a `delivered_amount` field into JSON RPC responses, giving consumers a reliable number to work with. + +The file is consumed in three places: `LedgerToJson.cpp` (which serializes transactions embedded in full ledger responses), `Tx.cpp` (the `tx` RPC command), and `NetworkOPs.cpp` (real-time transaction subscription notifications). In every case, the caller builds a JSON metadata object and then calls one of the `insertDeliveredAmount` overloads to stamp the final answer into `meta["delivered_amount"]`. + +## The Historical Boundary Problem + +The `DeliveredAmount` metadata field was not always written into transaction metadata. It was introduced at ledger **4594095**, validated live on January 24, 2014. Before that ledger, partial payment transactions exist on-chain without an explicit `sfDeliveredAmount` record, creating an ambiguity: if the field is absent, does that mean the full `Amount` was delivered, or that the ledger predates the feature? + +The core template function `getDeliveredAmount` handles this with a three-tier resolution: + +1. **Modern metadata wins first**: If `transactionMeta.getDeliveredAmount()` returns a value, that is authoritative and is returned immediately. This covers all transactions from mid-2014 onward. + +2. **Historical inference**: If the metadata field is absent but the transaction has an `sfAmount` field, the function checks whether the ledger is new enough to be trustworthy — `getLedgerIndex() >= 4594095 || getCloseTime() > NetClock::time_point{446000000s}`. Both conditions identify the post-DeliveredAmount era; the close time check (roughly February 2014) is a fallback in case ledger sequence alone is ambiguous. If either condition holds, the `Amount` field is returned directly, because a missing `DeliveredAmount` in a post-fix ledger means the full amount was delivered. + +3. **Unknowable past**: Ledgers that predate the threshold and lack the metadata field return `std::nullopt`, which causes the JSON to be populated with the sentinel string `"unavailable"`. + +The choice to use `"unavailable"` as a string rather than omitting the field entirely is deliberate. A downstream consumer can distinguish three states: field absent (not a payment transaction), field `"unavailable"` (was a payment but cannot be determined from this ledger), and field present with a numeric amount (definitively known). + +## Lazy Evaluation via Template Callables + +The innermost `getDeliveredAmount` is a file-local template parameterized on `GetLedgerIndex` and `GetCloseTime`. The comment at the top of the file explains the rationale: these values can be non-trivial to compute. In particular, `getCloseTimeBySeq()` on a `LedgerMaster` may require a database lookup by sequence number. Since the common case (modern ledger with an explicit `sfDeliveredAmount`) returns early before ever touching those values, making them lambdas avoids the cost entirely in the hot path. The ledger index and close time are only evaluated if and when the code reaches the historical fallback branch. + +This design is the reason for the two internal overloads: one accepts a `ReadView` directly (where header info is already in memory as `info.seq` and `info.closeTime`) and one accepts an `RPC::Context` (which must delegate to `context.ledgerMaster.getCloseTimeBySeq()`). Both ultimately call the same template, just with different lambda bodies. + +## Gate Function: `canHaveDeliveredAmount` + +Before any amount resolution happens, `canHaveDeliveredAmount` acts as a type gate. Only three transaction types can produce a `delivered_amount`: `ttPAYMENT`, `ttCHECK_CASH`, and `ttACCOUNT_DELETE`. Additionally, the transaction result must be `tesSUCCESS` — a failed transaction by definition delivers nothing and should not have the field set at all. This guard is invoked at every public entry point before the more expensive resolution logic runs. + +## Public API Surface + +The header exposes three overloads of `insertDeliveredAmount` and one of `getDeliveredAmount`: + +- `insertDeliveredAmount(meta, ReadView, STTx, TxMeta)` — used by `LedgerToJson` when iterating a complete closed ledger object. Ledger header data is directly accessible so close time and sequence are trivially available. +- `insertDeliveredAmount(meta, JsonContext, Transaction, TxMeta)` — thin wrapper that extracts the underlying `STTx` from a `Transaction` object and delegates to the third overload. +- `insertDeliveredAmount(meta, JsonContext, STTx, TxMeta)` — the live RPC path, where the ledger is not directly in scope and close time must be resolved through `LedgerMaster`. +- `getDeliveredAmount(Context, STTx, TxMeta, LedgerIndex)` — exposed separately for callers that need the `STAmount` value directly rather than inserting into JSON (used in the Simulate handler). + +All overloads share the same resolution chain and produce the same result; the overload structure exists purely to accommodate the different calling contexts across the codebase. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Handler.cpp.ai.json b/src/xrpld/rpc/detail/Handler.cpp.ai.json new file mode 100644 index 0000000000..31ca6f25d2 --- /dev/null +++ b/src/xrpld/rpc/detail/Handler.cpp.ai.json @@ -0,0 +1,286 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "entries" + ], + "lineno": 97, + "name": "HandlerTable" + } + ], + "code_paths": [ + { + "call_chain": [ + "byRef(Function f)", + "lambda (JsonContext& context, Json::Value& result)", + "f(context)", + "result = f(context)", + "if (result.type() != Json::objectValue) { ... }" + ], + "entry_point": "byRef", + "purpose": "Wraps a handler function to ensure its result is a JSON object; adapts old-style handlers to new interface.", + "validation_points": [ + "result.type() != Json::objectValue (ensures handler returns JSON object)" + ] + }, + { + "call_chain": [ + "handle(JsonContext& context, Object& object)", + "XRPL_ASSERT(context.apiVersion >= HandlerImpl::minApiVer && context.apiVersion <= HandlerImpl::maxApiVer, ...)", + "HandlerImpl handler(context)", + "handler.check()", + "if (status) { status.inject(object); } else { handler.writeResult(object); }" + ], + "entry_point": "handle", + "purpose": "Generic handler invoker that validates API version, checks handler preconditions, and writes result or error.", + "validation_points": [ + "XRPL_ASSERT on context.apiVersion (API version validation)", + "handler.check() (handler-specific validation)" + ] + }, + { + "call_chain": [ + "handlerFrom()", + "returns Handler struct with handle as method" + ], + "entry_point": "handlerFrom", + "purpose": "Registers a handler with its metadata and validation logic.", + "validation_points": [ + "Delegates to handle, so inherits its validation" + ] + } + ], + "data_flows": [ + { + "field": "context.apiVersion", + "flow": [ + "JsonContext (input)", + "handle(...)", + "XRPL_ASSERT(context.apiVersion ...)", + "HandlerImpl handler(context)" + ], + "origin": "JsonContext (populated from incoming RPC request)", + "transformations": [ + "Checked for min/max allowed API version" + ], + "validated_at": "XRPL_ASSERT in handle" + }, + { + "field": "result", + "flow": [ + "byRef lambda parameter", + "result = f(context)", + "if (result.type() != Json::objectValue) { ... }" + ], + "origin": "Output parameter to handler lambda (byRef)", + "transformations": [ + "Assigned from handler function", + "If not object, wrapped via makeObjectValue" + ], + "validated_at": "byRef: result.type() != Json::objectValue" + }, + { + "field": "handler function result", + "flow": [ + "handler function (e.g., doAccountInfo)", + "byRef lambda", + "result assignment", + "possibly wrapped by makeObjectValue" + ], + "origin": "doAccountInfo, doAccountCurrencies, etc.", + "transformations": [ + "May be wrapped to ensure JSON object" + ], + "validated_at": "byRef: result.type() != Json::objectValue" + } + ], + "description": "Defines and registers RPC handlers for the XRPL server, mapping handler names to their implementations, roles, and API version constraints. Provides lookup and registration mechanisms for handlers.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "context.apiVersion", + "validation", + "missing", + "check" + ], + "evidence": "Field context.apiVersion validated by XRPL_ASSERT macro, Json::Value type checking", + "issue_pattern": "Missing validation for context.apiVersion", + "why_false_positive": "XRPL_ASSERT macro, Json::Value type checking validates context.apiVersion automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "handler result type", + "validation", + "missing", + "check" + ], + "evidence": "Field handler result type validated by XRPL_ASSERT macro, Json::Value type checking", + "issue_pattern": "Missing validation for handler result type", + "why_false_positive": "XRPL_ASSERT macro, Json::Value type checking validates handler result type automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "context.apiVersion", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at handle (template function)", + "issue_pattern": "Missing empty string validation for context.apiVersion", + "why_false_positive": "XRPL_ASSERT macro validates context.apiVersion for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "context.apiVersion", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT macro at handle (template function)", + "issue_pattern": "Missing range validation for context.apiVersion", + "why_false_positive": "XRPL_ASSERT macro validates context.apiVersion range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result (from handler function)", + "empty", + "string", + "validation" + ], + "evidence": "result.type() != Json::objectValue at byRef (template function)", + "issue_pattern": "Missing empty string validation for result (from handler function)", + "why_false_positive": "result.type() != Json::objectValue validates result (from handler function) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "result (from handler function)", + "type", + "validation", + "check" + ], + "evidence": "result.type() != Json::objectValue at byRef (template function)", + "issue_pattern": "Missing type validation for result (from handler function)", + "why_false_positive": "result.type() != Json::objectValue validates result (from handler function) type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/Handler.cpp", + "functions": [ + { + "args": [ + "f" + ], + "lineno": 13, + "name": "byRef" + }, + { + "args": [ + "context", + "object" + ], + "lineno": 27, + "name": "handle" + }, + { + "args": [], + "lineno": 44, + "name": "handlerFrom" + }, + { + "args": [ + "version", + "betaEnabled", + "name" + ], + "lineno": 154, + "name": "getHandler" + }, + { + "args": [], + "lineno": 159, + "name": "getHandlerNames" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "RPC" + } + ], + "test_coverage_notes": "Test coverage is likely provided by integration and unit tests for each handler (e.g., doAccountInfo, doAccountCurrencies, etc.), as well as tests for API versioning and error handling. However, the specific validation for result.type() != Json::objectValue is marked LCOV_EXCL_START, indicating it is not covered by tests (since handlers are expected to always return objects). API version validation via XRPL_ASSERT may be tested in API versioning tests, but assertion failures may not be directly tested unless negative tests are written. There may be gaps in testing for unreachable/exceptional paths and for handlers returning non-object JSON.", + "validation_architecture": { + "auto_validated_fields": [ + "context.apiVersion", + "handler result type" + ], + "framework": "XRPL_ASSERT macro, Json::Value type checking", + "validation_layer": "middleware (handler dispatch layer)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws)", + "field": "context.apiVersion", + "location": "handle (template function)", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "context.apiVersion >= HandlerImpl::minApiVer", + "context.apiVersion <= HandlerImpl::maxApiVer" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE macro (likely aborts or throws)", + "field": "result (from handler function)", + "location": "byRef (template function)", + "validated_by": "result.type() != Json::objectValue", + "validates": [ + "result is a JSON object" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Handler.cpp.ai.md b/src/xrpld/rpc/detail/Handler.cpp.ai.md new file mode 100644 index 0000000000..a43494b940 --- /dev/null +++ b/src/xrpld/rpc/detail/Handler.cpp.ai.md @@ -0,0 +1,44 @@ +# `src/xrpld/rpc/detail/Handler.cpp` + +## Role in the System + +This file is the central dispatch registry for every RPC method the XRPL server exposes. Its job is to answer one question: given a method name and API version, which handler should run, and with what access constraints? Everything that lives here serves that purpose — a static table of ~70 handler entries, a versioned lookup mechanism, and two bridging utilities that let handlers evolve their interface over time. + +## The Two Handler Styles + +The file manages two distinct handler shapes that coexist in the table. + +**Old-style handlers** are free functions with the signature `Json::Value do(RPC::JsonContext&)`. They compute a result and return it by value. Because every handler from before the interface evolved takes this form, the `byRef()` adapter function exists to shim them into the canonical `Status(JsonContext&, Json::Value&)` signature the dispatch layer expects. `byRef()` constructs a lambda that calls the old function, assigns the returned value to the output reference, then checks that the result is a JSON object. If it isn't — which the `LCOV_EXCL_START` marker declares unreachable under correct operation — `makeObjectValue()` wraps it defensively before returning. The `UNREACHABLE` macro there signals a programming contract violation rather than a user error. + +**New-style handlers** are classes with static metadata (`name`, `minApiVer`, `maxApiVer`, `role`, `condition`) and two instance methods: `check()` to validate preconditions and `writeResult()` to populate the output. `LedgerHandler` and `VersionHandler` are the only current new-style handlers. The `handle()` template drives them: it asserts the incoming API version is within the handler's declared range, constructs an instance, calls `check()`, and either injects the returned error status or calls `writeResult()`. The `handlerFrom()` factory then wraps this invocation in a `Handler` value-struct for table storage. + +The reason two styles coexist rather than migrating everything to class form is pragmatic: old-style is simpler to write for methods that don't need version-specific behavior, while new-style provides richer compile-time enforcement and cleaner two-phase dispatch for more complex methods. + +## The `HandlerTable` Singleton + +`HandlerTable` is an internal class that builds and owns the live dispatch table. It is a `static`-local singleton — `instance()` returns a `const&` to the single object initialized on first call, guaranteed thread-safe by C++11. Once built, the table is immutable. + +The backing store is `std::multimap`. The multimap — rather than a plain map — is the key design decision: it allows multiple entries under the same method name, each covering a non-overlapping API version range. This enables a single method name to have entirely different implementations across API versions without routing logic spread across the handlers themselves. The lookup in `getHandler()` does an `equal_range()` on the name, then finds the entry whose `[minApiVer_, maxApiVer_]` bracket contains the requested version. + +Version overlap is a fatal configuration error. `overlappingApiVersion()` scans same-named entries for range intersection using the standard interval-overlap test (`a.min <= b.max && a.max >= b.min`). If any overlap is found during construction, `LogicError()` is called immediately, crashing the server before it begins accepting requests. This makes conflicting handler registrations impossible to deploy silently. The `addHandler()` private method, used during construction for `LedgerHandler` and `VersionHandler`, runs the same check with `static_assert` guards at compile time for the version bounds themselves. + +## Version-Gated Lookup + +`getHandler()` is the public entry point for the dispatch layer. It takes three parameters: the API version of the incoming request, a `betaEnabled` flag, and the method name. + +The version gate comes first: if the requested version is below `apiMinimumSupportedVersion` or above the effective maximum (either `apiMaximumSupportedVersion` or `apiBetaVersion` depending on the flag), the method returns `nullptr` immediately regardless of whether a handler exists. This makes beta-only API methods invisible to non-beta nodes at the lookup level rather than requiring each handler to check the flag internally. + +## Metadata Carried Per Handler + +Each `Handler` struct records: +- `name_`: the JSON method name (e.g., `"account_info"`) +- `valueMethod_`: the callable with the canonical `Status(JsonContext&, Json::Value&)` signature +- `role_`: `Role::USER` or `Role::ADMIN`, consumed by the authorization layer +- `condition_`: a bitmask from the `Condition` enum indicating required network/ledger state +- `minApiVer_` / `maxApiVer_`: the API version bracket + +The `Condition` enum values — `NO_CONDITION`, `NEEDS_NETWORK_CONNECTION`, `NEEDS_CURRENT_LEDGER`, `NEEDS_CLOSED_LEDGER` — are used by `conditionMet()` in `Handler.h` to check amendment blocking, UNL expiry, operating mode, and ledger age before dispatch. For example, `fee`, `owner_info`, `ledger_accept`, and `submit` all require `NEEDS_CURRENT_LEDGER`; `tx` and `ledger_cleaner` require `NEEDS_NETWORK_CONNECTION`. Several handlers are version-bounded at registration: `ledger_header` and `tx_history` carry explicit `{1, 1}` limits in their `handlerArray` entries, making them inaccessible to clients using API version 2 or later. + +## Access Patterns + +Two public functions expose the table: `getHandler()` for dispatch (returns a pointer into the immutable table or `nullptr`) and `getHandlerNames()` for introspection (returns a `std::set` of raw string pointers into the table entries). The pointer-to-static-string approach for names avoids copying but requires that callers treat them as interned constants rather than independently managed strings. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Handler.h.ai.json b/src/xrpld/rpc/detail/Handler.h.ai.json new file mode 100644 index 0000000000..6fc2fa45a4 --- /dev/null +++ b/src/xrpld/rpc/detail/Handler.h.ai.json @@ -0,0 +1,84 @@ +{ + "args": [ + { + "lineno": 42, + "name": "value" + }, + { + "lineno": 42, + "name": "field" + }, + { + "lineno": 52, + "name": "condition_required" + }, + { + "lineno": 52, + "name": "context" + }, + { + "lineno": 38, + "name": "version" + }, + { + "lineno": 38, + "name": "betaEnabled" + } + ], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "Handler" + } + ], + "description": "Defines the RPC Handler structure and related utilities for XRPL, including handler registration, condition checking, and helper functions for JSON responses.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/Handler.h", + "functions": [ + { + "args": [ + "version", + "betaEnabled", + "" + ], + "lineno": 38, + "name": "getHandler" + }, + { + "args": [ + "value", + "field" + ], + "lineno": 41, + "name": "makeObjectValue" + }, + { + "args": [], + "lineno": 48, + "name": "getHandlerNames" + }, + { + "args": [ + "condition_required", + "context" + ], + "lineno": 51, + "name": "conditionMet" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "Json" + }, + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 14, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Handler.h.ai.md b/src/xrpld/rpc/detail/Handler.h.ai.md new file mode 100644 index 0000000000..4d8c099c20 --- /dev/null +++ b/src/xrpld/rpc/detail/Handler.h.ai.md @@ -0,0 +1,68 @@ +# `src/xrpld/rpc/detail/Handler.h` — RPC Handler Registry and Dispatch Primitives + +This header is the definitional core of the XRPL RPC subsystem. It declares the `Handler` struct that represents a single dispatchable RPC method, the `Condition` enumeration that encodes the node-state requirements for calling that method, the gating predicate `conditionMet()` that enforces those requirements at call time, and the lookup functions that connect incoming requests to the right handler. Together with its companion implementation file `Handler.cpp`, it forms the registry that maps every JSON-RPC method name to executable code. + +## The `Handler` Struct + +`Handler` is a plain aggregate that records everything the dispatch layer needs to know about one RPC endpoint: + +```cpp +struct Handler +{ + char const* name_; + Method valueMethod_; + Role role_; + RPC::Condition condition_; + unsigned minApiVer_ = apiMinimumSupportedVersion; + unsigned maxApiVer_ = apiMaximumValidVersion; +}; +``` + +`Method` is a type alias for `std::function`. The output is written into the reference parameter rather than returned by value, which avoids an extra copy of the (potentially large) JSON result and lets the function signal success/failure through `Status` independently of the output value. + +The `minApiVer_` / `maxApiVer_` range fields are what allow multiple entries with the same `name_` to coexist in the registry for different protocol generations. For example, `ledger_header` is registered only for API version 1 (`{..., 1, 1}`), whereas most handlers span versions 1 through the maximum valid version. When a new handler implementation must behave differently across API versions, two `Handler` entries with non-overlapping ranges can be inserted for the same method name — the `HandlerTable` in `Handler.cpp` asserts at startup that no two entries for the same name have an overlapping version range. + +## The `Condition` Enum + +```cpp +enum Condition { + NO_CONDITION = 0, + NEEDS_NETWORK_CONNECTION = 1, + NEEDS_CURRENT_LEDGER = 1 << 1, + NEEDS_CLOSED_LEDGER = 1 << 2, +}; +``` + +This bitmask encodes the minimum node state an RPC endpoint requires. Methods like `fee`, `path_find`, and `submit` need a current ledger; `tx` requires a network connection; `ledger_closed` needs a closed ledger. Methods that operate on historical data or perform local crypto (`channel_authorize`, `sign`, `random`) carry `NO_CONDITION` and are always executable, even on a completely isolated node. + +## `conditionMet()` — the Gating Predicate + +This function template is the single enforcement point for the `Condition` contract. It checks four independent conditions in order: + +1. **Amendment block** — if the node has been amendment-blocked (a supermajority of validators have enabled an amendment this node does not support), any non-trivial RPC call is rejected with `rpcAMENDMENT_BLOCKED`. Allowing reads from an amendment-blocked node would surface ledger state that diverges from the rest of the network. + +2. **UNL block** — if the node's validator list has expired, non-trivial calls return `rpcEXPIRED_VALIDATOR_LIST`. The node cannot safely assess which ledger is authoritative without a current UNL. + +3. **Network operating mode** — the node must be at least `SYNCING` (as reported by `NetworkOPs::getOperatingMode()`). If it is only `DISCONNECTED` or `CONNECTED`, the call returns `rpcNO_NETWORK` (API v1) or `rpcNOT_SYNCED` (API v2+). The API-version split is intentional: v2 consolidates several legacy error codes into the single `rpcNOT_SYNCED` for a cleaner client experience, while v1 keeps the legacy codes for backward compatibility. + +4. **Ledger freshness** — in networked mode (not standalone), the last validated ledger must not be older than `Tuning::maxValidatedLedgerAge` (2 minutes), and the current ledger index must not be more than 10 behind the validated index. If either condition fails, the call returns `rpcNO_CURRENT` (v1) or `rpcNOT_SYNCED` (v2+). The 10-ledger tolerance avoids transient false positives during normal operation. + +5. **Closed ledger availability** — if `NEEDS_CLOSED_LEDGER` is set, `LedgerMaster::getClosedLedger()` must return a valid pointer. If not, the response is `rpcNO_CLOSED` / `rpcNOT_SYNCED`. + +The function is templated on `T` (the context type) rather than hard-coded to `JsonContext` so that it can be reused with gRPC contexts that share the same `Context` base. All required fields — `app`, `netOps`, `ledgerMaster`, `j`, `apiVersion` — come from the `Context` struct declared in `Context.h`. + +## Handler Lookup — `getHandler()` and the `HandlerTable` + +`getHandler(version, betaEnabled, name)` is the public entry point for dispatch. Its implementation in `Handler.cpp` delegates to the `HandlerTable` singleton, a `std::multimap` built once at first use. The map is keyed by method name, with `equal_range` used to iterate all entries for that name, and the correct entry selected by version range membership. Passing a version outside `[apiMinimumSupportedVersion, apiMaximumSupportedVersion]` (or `apiBetaVersion` if beta is enabled) immediately returns `nullptr` — the caller interprets that as an unknown or unsupported method. + +At construction time, the table loads `handlerArray` (the ~70 statically declared handlers) and then calls `addHandler()` and `addHandler()` for the newer class-based handler style. The class-based variant (`handlerFrom()`) reads `name`, `role`, `condition`, `minApiVer`, and `maxApiVer` as static class members and wraps a two-phase `check()` / `writeResult()` dispatch into a `Method` lambda — providing a more structured pattern for handlers that need clean validation separated from result generation. + +The older functional handlers are adapted via the private `byRef()` helper, which wraps a function that returns `Json::Value` into the `Method` signature that writes to a reference. An `UNREACHABLE` guard asserts that the returned value is always a JSON object, since the RPC protocol only permits JSON objects at the top level. + +## `makeObjectValue()` Utility + +A small convenience template that constructs a `Json::Value` of object type with a single key-value pair, defaulting the key to `jss::message`. It is used in `byRef()` as a defensive fallback when a legacy handler accidentally returns a non-object JSON value. + +## Design Rationale + +The split between the header and the implementation file is deliberate. `Handler.h` exposes only the types needed to check and dispatch — `Handler`, `Condition`, `conditionMet()`, and the lookup interface — while the actual handler table (with its 70+ entries and their concrete implementations pulled from `handlers/Handlers.h`) lives entirely in the `.cpp` translation unit. This keeps compilation dependencies tight: code that only needs to *call* `getHandler()` or check conditions does not transitively include every RPC handler's header. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/LegacyPathFind.cpp.ai.json b/src/xrpld/rpc/detail/LegacyPathFind.cpp.ai.json new file mode 100644 index 0000000000..a4bb5369e2 --- /dev/null +++ b/src/xrpld/rpc/detail/LegacyPathFind.cpp.ai.json @@ -0,0 +1,245 @@ +{ + "args": [ + { + "lineno": 9, + "name": "isAdmin" + }, + { + "lineno": 9, + "name": "app" + } + ], + "classes": [ + { + "args": [ + "bool isAdmin, Application& app" + ], + "lineno": 8, + "name": "LegacyPathFind" + } + ], + "code_paths": [ + { + "call_chain": [ + "RPC handler (e.g., doLegacyPathFind or similar)", + "constructs LegacyPathFind", + "LegacyPathFind::LegacyPathFind" + ], + "entry_point": "LegacyPathFind::LegacyPathFind(bool isAdmin, Application& app)", + "purpose": "Handles the creation of a pathfinding context for a legacy pathfind RPC request, enforcing resource and permission limits.", + "validation_points": [ + "isAdmin checked at start of constructor", + "jobCount checked after isAdmin", + "app.getFeeTrack().isLoadedLocal() checked after jobCount", + "inProgress atomic counter checked and incremented" + ] + } + ], + "data_flows": [ + { + "field": "isAdmin", + "flow": [ + "RPC handler receives request", + "extracts isAdmin from context", + "passes isAdmin to LegacyPathFind constructor" + ], + "origin": "RPC context (likely from session/user permissions)", + "transformations": [ + "Used as boolean flag to bypass resource checks" + ], + "validated_at": "LegacyPathFind constructor (if isAdmin)" + }, + { + "field": "jobCount", + "flow": [ + "Application instance", + "JobQueue queried for jtCLIENT jobs", + "jobCount compared to Tuning::maxPathfindJobCount" + ], + "origin": "app.getJobQueue().getJobCountGE(jtCLIENT)", + "transformations": [ + "Compared to constant threshold" + ], + "validated_at": "LegacyPathFind constructor (after isAdmin check)" + }, + { + "field": "isLoadedLocal", + "flow": [ + "Application instance", + "FeeTrack queried for local load status", + "used in logical OR with jobCount check" + ], + "origin": "app.getFeeTrack().isLoadedLocal()", + "transformations": [ + "Boolean check" + ], + "validated_at": "LegacyPathFind constructor (after jobCount check)" + }, + { + "field": "inProgress", + "flow": [ + "static counter (class-level)", + "read and compared to Tuning::maxPathfindsInProgress", + "conditionally incremented via compare_exchange_strong", + "decremented in destructor if m_isOk" + ], + "origin": "static std::atomic LegacyPathFind::inProgress", + "transformations": [ + "Atomic increment/decrement", + "Range-checked before increment" + ], + "validated_at": "LegacyPathFind constructor (after previous checks)" + }, + { + "field": "m_isOk", + "flow": [ + "set to true if validation passes", + "used in destructor to determine if inProgress should be decremented" + ], + "origin": "LegacyPathFind instance variable", + "transformations": [ + "Set to true only if all validations pass" + ], + "validated_at": "Set in constructor after all validations" + } + ], + "description": "Implements the LegacyPathFind class, which manages the concurrency and permission logic for legacy pathfinding jobs in the XRPL server, ensuring limits on simultaneous pathfinding operations and prioritizing admin users.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "isAdmin", + "empty", + "string", + "validation" + ], + "evidence": "LegacyPathFind constructor (if statement) at LegacyPathFind::LegacyPathFind", + "issue_pattern": "Missing empty string validation for isAdmin", + "why_false_positive": "LegacyPathFind constructor (if statement) validates isAdmin for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "jobCount (from app.getJobQueue().getJobCountGE(jtCLIENT))", + "empty", + "string", + "validation" + ], + "evidence": "LegacyPathFind constructor (comparison with Tuning::maxPathfindJobCount) at LegacyPathFind::LegacyPathFind", + "issue_pattern": "Missing empty string validation for jobCount (from app.getJobQueue().getJobCountGE(jtCLIENT))", + "why_false_positive": "LegacyPathFind constructor (comparison with Tuning::maxPathfindJobCount) validates jobCount (from app.getJobQueue().getJobCountGE(jtCLIENT)) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "app.getFeeTrack().isLoadedLocal()", + "empty", + "string", + "validation" + ], + "evidence": "LegacyPathFind constructor (if statement) at LegacyPathFind::LegacyPathFind", + "issue_pattern": "Missing empty string validation for app.getFeeTrack().isLoadedLocal()", + "why_false_positive": "LegacyPathFind constructor (if statement) validates app.getFeeTrack().isLoadedLocal() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "inProgress (static atomic counter)", + "empty", + "string", + "validation" + ], + "evidence": "LegacyPathFind constructor (range check and atomic compare_exchange_strong) at LegacyPathFind::LegacyPathFind", + "issue_pattern": "Missing empty string validation for inProgress (static atomic counter)", + "why_false_positive": "LegacyPathFind constructor (range check and atomic compare_exchange_strong) validates inProgress (static atomic counter) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/LegacyPathFind.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "RPC" + } + ], + "test_coverage_notes": "Testing for this code would typically be found in RPC-level integration tests, possibly in files like 'Path_find_test.cpp', 'PathRequest_test.cpp', or 'LegacyPathFind_test.cpp' if present. Tests should cover: admin vs non-admin requests, job queue limits, fee track load status, and concurrent pathfind requests (inProgress limit). Gaps may exist if there are no direct unit tests for LegacyPathFind, or if concurrency/race conditions are not explicitly tested. If only high-level RPC tests exist, edge cases for atomic increments and resource exhaustion may not be fully covered.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation in constructor, no external validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (early return, m_isOk remains false)", + "field": "isAdmin", + "location": "LegacyPathFind::LegacyPathFind", + "validated_by": "LegacyPathFind constructor (if statement)", + "validates": [ + "Checks if caller is admin to bypass further validation and limits" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return, m_isOk remains false)", + "field": "jobCount (from app.getJobQueue().getJobCountGE(jtCLIENT))", + "location": "LegacyPathFind::LegacyPathFind", + "validated_by": "LegacyPathFind constructor (comparison with Tuning::maxPathfindJobCount)", + "validates": [ + "Checks if the number of client jobs exceeds the maximum allowed pathfind jobs" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return, m_isOk remains false)", + "field": "app.getFeeTrack().isLoadedLocal()", + "location": "LegacyPathFind::LegacyPathFind", + "validated_by": "LegacyPathFind constructor (if statement)", + "validates": [ + "Checks if the local server is under heavy load and blocks pathfinding if so" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (early return, m_isOk remains false)", + "field": "inProgress (static atomic counter)", + "location": "LegacyPathFind::LegacyPathFind", + "validated_by": "LegacyPathFind constructor (range check and atomic compare_exchange_strong)", + "validates": [ + "Checks if the number of concurrent pathfinds in progress exceeds the maximum allowed" + ], + "validation_type": "range|business_logic|concurrency" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/LegacyPathFind.cpp.ai.md b/src/xrpld/rpc/detail/LegacyPathFind.cpp.ai.md new file mode 100644 index 0000000000..189444630a --- /dev/null +++ b/src/xrpld/rpc/detail/LegacyPathFind.cpp.ai.md @@ -0,0 +1,49 @@ +# `LegacyPathFind.cpp` — Pathfinding Concurrency Guard + +## Role and Purpose + +`LegacyPathFind` is a small RAII guard that controls access to the synchronous `ripple_path_find` RPC operation. Path-finding in XRPL is computationally expensive — it performs graph traversal across the order-book to find liquidity paths between two currencies — so allowing unbounded concurrent execution would starve the job queue and degrade the whole server. This file implements a tiered admission-control scheme that keeps the server healthy under load while giving administrative clients an escape hatch. + +## Where It Is Used + +The guard appears in exactly two call sites. In `RipplePathFind.cpp`, the `ripple_path_find` RPC handler constructs it immediately after ledger lookup: + +```cpp +RPC::LegacyPathFind const lpf(isUnlimited(context.role), context.app); +if (!lpf.isOk()) + return rpcError(rpcTOO_BUSY); +``` + +And in `TransactionSign.cpp`, it gates auto-pathfinding when a transaction sign request includes `build_path`: + +```cpp +LegacyPathFind const lpf(isUnlimited(role), app); +if (!lpf.isOk()) + return rpcError(rpcTOO_BUSY); +``` + +In both cases the guard lives for the scope of the expensive operation, releasing its slot automatically when it goes out of scope. + +## Admission Control Logic + +The constructor runs a sequential chain of checks for non-admin requests. Any failure leaves `m_isOk` false and returns immediately with no side effects — the global counter is not touched: + +**1. Admin bypass.** If `isAdmin` is true (derived from `isUnlimited(context.role)` in both call sites), the counter is incremented unconditionally and the constructor returns. Administrators are never throttled, regardless of how busy the server is. + +**2. Job queue pressure.** `app.getJobQueue().getJobCountGE(jtCLIENT)` returns the total number of active client-class jobs. If this count exceeds `Tuning::maxPathfindJobCount` (50), the request is rejected. This prevents path-finding from competing with ordinary RPC work when the server is already saturated with client requests. + +**3. Local fee load.** `app.getFeeTrack().isLoadedLocal()` returns true when the server's own CPU or memory load is high enough that the fee system has raised the local fee multiplier. Triggering either of the first two checks is sufficient for rejection — they are combined with `||`. + +**4. Concurrent pathfind ceiling.** The last gate uses `Tuning::maxPathfindsInProgress` (2), enforced via a lock-free CAS loop on the static `std::atomic inProgress`. The loop reads the current value, returns if it has already reached the ceiling, and otherwise attempts `compare_exchange_strong` to increment it. If another thread races to increment between the load and the CAS, the exchange fails and the loop retries rather than over-counting. This is the only spot in the constructor where a retry occurs; all earlier checks are single-shot tests. + +## Why CAS Instead of a Mutex + +The CAS loop is the right tool here for two reasons. First, the counter update is trivially cheap — a single integer increment — so a mutex would add more overhead than it saves. Second, the loop's retry path is bounded in practice: `maxPathfindsInProgress` is only 2, so at most one competing thread can cause a retry, and contention is extremely low. Using `std::memory_order_release` on success and `std::memory_order_relaxed` on failure is a deliberate choice — the release fence ensures that the increment is visible to other threads that subsequently load `inProgress` with acquire semantics, while the relaxed on failure avoids a needless memory barrier when nothing was changed. + +## RAII Invariant + +`m_isOk` is the only instance variable aside from the static counter. It defaults to `false` and is only set to `true` in paths where `inProgress` was actually incremented. The destructor checks this flag before decrementing, making it safe to construct a `LegacyPathFind` that fails admission — the destructor becomes a no-op in that case. This invariant means callers cannot accidentally release a slot they never acquired, even if the object is constructed and destroyed in unusual control flows. + +## Tuning Constants + +`maxPathfindsInProgress = 2` is deliberately low. Path-finding can be orders of magnitude more expensive than a typical RPC call, and two concurrent operations is enough to serve genuine administrative needs while keeping the system responsive. The broader `maxPathfindJobCount = 50` check provides an earlier, lighter-weight gate before the atomic is even consulted. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/LegacyPathFind.h.ai.json b/src/xrpld/rpc/detail/LegacyPathFind.h.ai.json new file mode 100644 index 0000000000..992a25f977 --- /dev/null +++ b/src/xrpld/rpc/detail/LegacyPathFind.h.ai.json @@ -0,0 +1,45 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "bool isAdmin, Application& app" + ], + "lineno": 9, + "name": "LegacyPathFind" + } + ], + "description": "Defines the LegacyPathFind class used in the XRPL RPC subsystem to manage legacy pathfinding requests, including tracking in-progress operations and admin status.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/LegacyPathFind.h", + "functions": [ + { + "args": [ + "bool isAdmin", + "Application& app" + ], + "lineno": 11, + "name": "LegacyPathFind" + }, + { + "args": [], + "lineno": 12, + "name": "~LegacyPathFind" + }, + { + "args": [], + "lineno": 14, + "name": "isOk" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/LegacyPathFind.h.ai.md b/src/xrpld/rpc/detail/LegacyPathFind.h.ai.md new file mode 100644 index 0000000000..0a560997a1 --- /dev/null +++ b/src/xrpld/rpc/detail/LegacyPathFind.h.ai.md @@ -0,0 +1,40 @@ +# `LegacyPathFind.h` — Concurrency Guard for Synchronous Path-Finding + +## Role in the System + +`LegacyPathFind` is a lightweight RAII guard that controls access to the synchronous, single-shot path-finding code path in XRPL's RPC subsystem. The `ripple_path_find` RPC command is itself marked deprecated, and within it there are two execution branches: an asynchronous coroutine path (used when no ledger is specified) and a synchronous blocking path (used when the caller explicitly names a ledger). `LegacyPathFind` is the concurrency limiter for the latter — the synchronous "legacy" execution. The guard is constructed at the entry point of the synchronous branch and, if `isOk()` returns false, the handler immediately returns `rpcTOO_BUSY` to the caller. + +## Design: Lock-Free Slot Acquisition + +The class holds a single `static std::atomic inProgress` counter shared across all instances. The constructor either claims a slot in this counter or fails — there is no blocking wait. This is an intentional design choice for an RPC handler: latency matters more than fairness. A caller that cannot get a slot right away is turned away immediately rather than queued, preventing unbounded resource accumulation. + +For non-admin callers, the constructor performs two pre-checks before attempting to claim a slot: + +1. **Job queue saturation**: if the `jtCLIENT` job count in the `JobQueue` exceeds `Tuning::maxPathfindJobCount` (50), the server is already under pressure handling client work, and path-find is declined. +2. **Local load**: if `getFeeTrack().isLoadedLocal()` is true, the server is throttling itself due to CPU or memory pressure; path-find is again declined. + +Only when both checks pass does the constructor enter a CAS loop to atomically increment `inProgress` up to the hard cap of `Tuning::maxPathfindsInProgress` (2). The `compare_exchange_strong` with `memory_order_release` on success and `memory_order_relaxed` on failure is the idiomatic pattern: the release ensures the increment is visible before any path-finding work begins, while the relaxed load on failure avoids unnecessary synchronisation overhead when retrying. + +## Admin Bypass + +If `isAdmin` is true, the constructor unconditionally increments `inProgress` and sets `m_isOk = true`, skipping every load check and the CAS concurrency cap. This reflects a deliberate policy: administrative connections are trusted to perform heavier operations that would be refused to public clients. The cap of 2 concurrent path-finds is a resource protection for public-facing traffic only. + +## RAII Cleanup + +The destructor decrements `inProgress` only if `m_isOk` is true — that is, only if the constructor actually claimed a slot. This means failed acquisitions never alter the counter, and there is no double-decrement risk even if the object is destroyed in an unusual path. The asymmetry between the admin increment (unconditional `++`) and the non-admin CAS is safe here because both paths set `m_isOk` before returning. + +## Usage Context + +In `doRipplePathFind` (`RipplePathFind.cpp`), `LegacyPathFind` is stack-allocated as a `const` object immediately before calling `PathRequestManager::doLegacyPathRequest`: + +```cpp +RPC::LegacyPathFind const lpf(isUnlimited(context.role), context.app); +if (!lpf.isOk()) + return rpcError(rpcTOO_BUSY); +``` + +Its lifetime is tied to the synchronous call. The moment `doLegacyPathRequest` returns and the handler exits scope, `lpf`'s destructor releases the slot, freeing capacity for the next caller. The class has no other public state — `isOk()` is the only observable result of construction. + +## Summary + +`LegacyPathFind` is a minimal but carefully reasoned concurrency gate. The `static atomic` shared counter is the key: it makes the maximum of 2 simultaneous legacy path-finds a process-wide invariant without requiring a mutex. The admin bypass, the pre-flight load checks, and the CAS loop together form a three-layer defense against resource exhaustion on a deprecated but still callable interface. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/MPT.h.ai.json b/src/xrpld/rpc/detail/MPT.h.ai.json new file mode 100644 index 0000000000..aedd9046a1 --- /dev/null +++ b/src/xrpld/rpc/detail/MPT.h.ai.json @@ -0,0 +1,60 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "MPTID const& mptID", + "MPTID const& mptID, bool zeroBalance, bool maxedOut" + ], + "lineno": 6, + "name": "PathFindMPT" + } + ], + "description": "Defines the PathFindMPT class, which represents a Multi-Party Token (MPT) identifier and associated state for pathfinding in the XRPL protocol.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/MPT.h", + "functions": [ + { + "args": [ + "mptID" + ], + "lineno": 13, + "name": "PathFindMPT" + }, + { + "args": [ + "mptID", + "zeroBalance", + "maxedOut" + ], + "lineno": 17, + "name": "PathFindMPT" + }, + { + "args": [], + "lineno": 22, + "name": "operator MPTID const&" + }, + { + "args": [], + "lineno": 25, + "name": "getMptID" + }, + { + "args": [], + "lineno": 29, + "name": "isZeroBalance" + }, + { + "args": [], + "lineno": 33, + "name": "isMaxedOut" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/MPT.h.ai.md b/src/xrpld/rpc/detail/MPT.h.ai.md new file mode 100644 index 0000000000..b7dbcd4551 --- /dev/null +++ b/src/xrpld/rpc/detail/MPT.h.ai.md @@ -0,0 +1,37 @@ +# `src/xrpld/rpc/detail/MPT.h` + +## Role in the System + +This header defines `PathFindMPT`, a compact value type used exclusively within the XRPL pathfinding subsystem. Its purpose is to carry an `MPTID` (a 192-bit Multi-Party Token identifier) together with two boolean flags that encode economically relevant state about that token — state that needs to be inspected during path construction without re-reading the ledger on every evaluation step. + +The type lives in `src/xrpld/rpc/detail/`, alongside `AssetCache.h`, `AssetCache.cpp`, and `Pathfinder.cpp`. It represents the MPT analogue of `PathFindTrustLine`: where trust-line pathfinding stores per-account, per-line trust state in a cache, MPT pathfinding stores per-account issuance state as `std::vector` in `AssetCache`. + +## The Two Boolean Flags + +`zeroBalance_` records whether the account's MPToken balance is currently zero. The comment makes a critical constraint explicit: this is always `false` for the issuer. When `AssetCache::getMPTs()` processes an `ltMPTOKEN_ISSUANCE` ledger object (i.e., the issuer's side), it constructs `PathFindMPT` with `zeroBalance = false` unconditionally. When it processes an `ltMPTOKEN` (a holder's token object), it reads `sfMPTAmount` and sets the flag accordingly. This distinction matters because a holder with a zero balance can receive but not meaningfully send, which affects whether a path through that account is viable. + +`maxedOut_` records whether the issuance's `sfOutstandingAmount` has reached its configured maximum (`maxMPTAmount`). When an issuance is maxed out, no additional tokens can be minted, which means certain payment paths are effectively closed. This flag is computed both for issuers (from their `ltMPTOKEN_ISSUANCE` directly) and for holders (by looking up the associated issuance from the ledger). The holder-side lookup has a subtle fallback: if the issuance ledger entry cannot be found, `maxedOut` defaults to `true` — a conservative choice that treats orphaned or missing issuances as non-viable routes rather than potentially routing through them. + +## Why Cache State on the Identifier + +Path construction in `Pathfinder.cpp` iterates over many candidate accounts and assets using a templated lambda (`forAssets`) that handles both `std::vector` and `std::vector` via `if constexpr` dispatch. By the time the pathfinder calls into this logic, the ledger has already been read once by `AssetCache::getMPTs()` and the results are stored behind a shared pointer. Each subsequent evaluation of a candidate MPT in the path avoids another ledger read and instead checks the pre-populated flags on the `PathFindMPT` object directly. + +This is important because path search is combinatorial: the same token may appear repeatedly across many candidate paths for a single `path_find` or `ripple_path_find` RPC call. + +## Implicit Conversion and API Design + +The `operator MPTID const&()` conversion operator allows a `PathFindMPT` to be passed directly to any function that accepts an `MPTID`, such as `getMPTIssuer()` called in `Pathfinder.cpp`: + +```cpp +return getMPTIssuer(asset); // asset is PathFindMPT; implicit conversion to MPTID +``` + +This means the pathfinder loop, which is templated over the asset-vector type, can call `getMPTIssuer(asset)` without knowing whether `asset` is an `MPTID` or a `PathFindMPT`. The explicit `getMptID()` accessor mirrors the same accessor on `MPTIssue` and provides a named alternative where implicit conversion would be ambiguous or where `constexpr` evaluation requires it. + +## Relationship to MPTIssue + +`MPTIssue` (defined in `xrpl/protocol/MPTIssue.h`) is the protocol-layer type that adapts `MPTID` to the `Issue` interface for use in amounts, comparisons, and JSON serialization. `PathFindMPT` is narrower — it exists only for pathfinding and carries no serialization logic. The class is `final`, has all-`const` members, and offers no mutation methods, reinforcing that it is a read-once snapshot of ledger state at the start of a path search session. + +## MPT Pathfinding Constraints + +Unlike trust lines, MPT does not support rippling. The pathfinder reflects this by always assigning `LineDirection::incoming` for MPT assets, regardless of the account's position in the path, and by always treating the issuer account (extracted via `getMPTIssuer`) as the other party. This is a deliberate asymmetry that `PathFindMPT`'s design accommodates: the two flags are defined relative to the holder/issuer distinction rather than a directional flow, making the type self-consistent with the non-rippling constraint. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp.ai.json b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp.ai.json new file mode 100644 index 0000000000..1aec815499 --- /dev/null +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp.ai.json @@ -0,0 +1,521 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "insertMPTokenIssuanceID", + "canHaveMPTokenIssuanceID", + "getIDFromCreatedIssuance" + ], + "entry_point": "insertMPTokenIssuanceID", + "purpose": "Inserts the MPToken Issuance ID into a JSON response if the transaction is a successful MPToken Issuance Create.", + "validation_points": [ + "canHaveMPTokenIssuanceID: serializedTx null check", + "canHaveMPTokenIssuanceID: serializedTx->getTxnType() == ttMPTOKEN_ISSUANCE_CREATE", + "canHaveMPTokenIssuanceID: isTesSuccess(transactionMeta.getResultTER())", + "getIDFromCreatedIssuance: node.getFieldU16(sfLedgerEntryType) == ltMPTOKEN_ISSUANCE", + "getIDFromCreatedIssuance: node.getFName() == sfCreatedNode" + ] + } + ], + "data_flows": [ + { + "field": "serializedTx", + "flow": [ + "insertMPTokenIssuanceID argument", + "canHaveMPTokenIssuanceID argument", + "used for null check and getTxnType()" + ], + "origin": "Passed as argument to insertMPTokenIssuanceID", + "transformations": [ + "Checked for null", + "getTxnType() called to extract transaction type" + ], + "validated_at": "canHaveMPTokenIssuanceID" + }, + { + "field": "transactionMeta.getResultTER()", + "flow": [ + "insertMPTokenIssuanceID argument", + "canHaveMPTokenIssuanceID argument", + "getResultTER() called", + "isTesSuccess() called" + ], + "origin": "TxMeta object passed to insertMPTokenIssuanceID", + "transformations": [ + "Checked for transaction engine result success" + ], + "validated_at": "canHaveMPTokenIssuanceID" + }, + { + "field": "transactionMeta.getNodes()", + "flow": [ + "insertMPTokenIssuanceID argument", + "getIDFromCreatedIssuance argument", + "for loop over getNodes()" + ], + "origin": "TxMeta object passed to insertMPTokenIssuanceID", + "transformations": [ + "Iterated over nodes" + ], + "validated_at": "getIDFromCreatedIssuance" + }, + { + "field": "node.getFieldU16(sfLedgerEntryType)", + "flow": [ + "getIDFromCreatedIssuance for loop", + "getFieldU16(sfLedgerEntryType) called", + "compared to ltMPTOKEN_ISSUANCE" + ], + "origin": "Each node in transactionMeta.getNodes()", + "transformations": [ + "Type-checked" + ], + "validated_at": "getIDFromCreatedIssuance" + }, + { + "field": "node.getFName()", + "flow": [ + "getIDFromCreatedIssuance for loop", + "getFName() called", + "compared to sfCreatedNode" + ], + "origin": "Each node in transactionMeta.getNodes()", + "transformations": [ + "Type-checked" + ], + "validated_at": "getIDFromCreatedIssuance" + }, + { + "field": "mptNode.getFieldU32(sfSequence), mptNode.getAccountID(sfIssuer)", + "flow": [ + "getIDFromCreatedIssuance", + "makeMptID called with these fields" + ], + "origin": "mptNode = node.peekAtField(sfNewFields).downcast()", + "transformations": [ + "Used to construct MPTID" + ], + "validated_at": "Implicitly validated by prior checks" + }, + { + "field": "response[jss::mpt_issuance_id]", + "flow": [ + "insertMPTokenIssuanceID", + "set if result from getIDFromCreatedIssuance is present" + ], + "origin": "Json::Value response passed to insertMPTokenIssuanceID", + "transformations": [ + "Set to stringified MPTID" + ], + "validated_at": "Only set if all prior validations pass" + } + ], + "description": "Provides utility functions for handling MPToken Issuance IDs in XRPL transactions, including checking if a transaction can have an issuance ID, extracting the ID from transaction metadata, and inserting it into a JSON response.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "STObject field accessors (getFieldU32, getAccountID, getFieldU16)", + "validation", + "missing", + "check" + ], + "evidence": "Field STObject field accessors (getFieldU32, getAccountID, getFieldU16) validated by Ripple STObject/STTx/TxMeta validation, jss:: for JSON keys", + "issue_pattern": "Missing validation for STObject field accessors (getFieldU32, getAccountID, getFieldU16)", + "why_false_positive": "Ripple STObject/STTx/TxMeta validation, jss:: for JSON keys validates STObject field accessors (getFieldU32, getAccountID, getFieldU16) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Transaction type (getTxnType)", + "validation", + "missing", + "check" + ], + "evidence": "Field Transaction type (getTxnType) validated by Ripple STObject/STTx/TxMeta validation, jss:: for JSON keys", + "issue_pattern": "Missing validation for Transaction type (getTxnType)", + "why_false_positive": "Ripple STObject/STTx/TxMeta validation, jss:: for JSON keys validates Transaction type (getTxnType) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Transaction result (getResultTER)", + "validation", + "missing", + "check" + ], + "evidence": "Field Transaction result (getResultTER) validated by Ripple STObject/STTx/TxMeta validation, jss:: for JSON keys", + "issue_pattern": "Missing validation for Transaction result (getResultTER)", + "why_false_positive": "Ripple STObject/STTx/TxMeta validation, jss:: for JSON keys validates Transaction result (getResultTER) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "serializedTx (transaction)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at canHaveMPTokenIssuanceID", + "issue_pattern": "Missing empty string validation for serializedTx (transaction)", + "why_false_positive": "explicit null check validates serializedTx (transaction) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "serializedTx (transaction)", + "type", + "validation", + "check" + ], + "evidence": "explicit null check at canHaveMPTokenIssuanceID", + "issue_pattern": "Missing type validation for serializedTx (transaction)", + "why_false_positive": "explicit null check validates serializedTx (transaction) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "serializedTx->getTxnType()", + "empty", + "string", + "validation" + ], + "evidence": "explicit value check at canHaveMPTokenIssuanceID", + "issue_pattern": "Missing empty string validation for serializedTx->getTxnType()", + "why_false_positive": "explicit value check validates serializedTx->getTxnType() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transactionMeta.getResultTER()", + "empty", + "string", + "validation" + ], + "evidence": "isTesSuccess function at canHaveMPTokenIssuanceID", + "issue_pattern": "Missing empty string validation for transactionMeta.getResultTER()", + "why_false_positive": "isTesSuccess function validates transactionMeta.getResultTER() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node.getFieldU16(sfLedgerEntryType)", + "empty", + "string", + "validation" + ], + "evidence": "explicit value check at getIDFromCreatedIssuance", + "issue_pattern": "Missing empty string validation for node.getFieldU16(sfLedgerEntryType)", + "why_false_positive": "explicit value check validates node.getFieldU16(sfLedgerEntryType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node.getFName()", + "empty", + "string", + "validation" + ], + "evidence": "explicit value check at getIDFromCreatedIssuance", + "issue_pattern": "Missing empty string validation for node.getFName()", + "why_false_positive": "explicit value check validates node.getFName() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "mptNode.getFieldU32(sfSequence)", + "empty", + "string", + "validation" + ], + "evidence": "STObject method (likely internal validation) at getIDFromCreatedIssuance", + "issue_pattern": "Missing empty string validation for mptNode.getFieldU32(sfSequence)", + "why_false_positive": "STObject method (likely internal validation) validates mptNode.getFieldU32(sfSequence) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "mptNode.getAccountID(sfIssuer)", + "empty", + "string", + "validation" + ], + "evidence": "STObject method (likely internal validation) at getIDFromCreatedIssuance", + "issue_pattern": "Missing empty string validation for mptNode.getAccountID(sfIssuer)", + "why_false_positive": "STObject method (likely internal validation) validates mptNode.getAccountID(sfIssuer) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp", + "functions": [ + { + "args": [ + "serializedTx", + "transactionMeta" + ], + "lineno": 7, + "name": "canHaveMPTokenIssuanceID" + }, + { + "args": [ + "transactionMeta" + ], + "lineno": 21, + "name": "getIDFromCreatedIssuance" + }, + { + "args": [ + "response", + "transaction", + "transactionMeta" + ], + "lineno": 36, + "name": "insertMPTokenIssuanceID" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "RPC" + } + ], + "test_coverage_notes": "The code is likely covered by unit/integration tests for MPToken issuance transactions in the rippled codebase, especially those that test the RPC layer and transaction metadata processing. Tests should cover: (1) successful MPToken issuance, (2) failed transactions, (3) transactions of wrong type, (4) malformed or missing nodes. However, if there are no explicit tests for insertMPTokenIssuanceID or getIDFromCreatedIssuance, edge cases (e.g., missing fields, null pointers, unexpected node types) may not be fully covered. Test files to check: workflow/XRPLF-rippled-develop/test/rpc/MPTokenIssuanceID_test.cpp, or more generally, any test file under test/rpc/ or test/tx/ that exercises MPToken issuance and RPC responses.", + "validation_architecture": { + "auto_validated_fields": [ + "STObject field accessors (getFieldU32, getAccountID, getFieldU16)", + "Transaction type (getTxnType)", + "Transaction result (getResultTER)" + ], + "framework": "Ripple STObject/STTx/TxMeta validation, jss:: for JSON keys", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "serializedTx (transaction)", + "location": "canHaveMPTokenIssuanceID", + "validated_by": "explicit null check", + "validates": [ + "Checks if transaction pointer is not null" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "serializedTx->getTxnType()", + "location": "canHaveMPTokenIssuanceID", + "validated_by": "explicit value check", + "validates": [ + "Checks if transaction type is ttMPTOKEN_ISSUANCE_CREATE" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (no exception)", + "field": "transactionMeta.getResultTER()", + "location": "canHaveMPTokenIssuanceID", + "validated_by": "isTesSuccess function", + "validates": [ + "Checks if transaction result is a success code" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "continues loop (no exception)", + "field": "node.getFieldU16(sfLedgerEntryType)", + "location": "getIDFromCreatedIssuance", + "validated_by": "explicit value check", + "validates": [ + "Checks if ledger entry type is ltMPTOKEN_ISSUANCE" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "continues loop (no exception)", + "field": "node.getFName()", + "location": "getIDFromCreatedIssuance", + "validated_by": "explicit value check", + "validates": [ + "Checks if node is a CreatedNode" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws if field missing or wrong type (framework dependent)", + "field": "mptNode.getFieldU32(sfSequence)", + "location": "getIDFromCreatedIssuance", + "validated_by": "STObject method (likely internal validation)", + "validates": [ + "Checks that sfSequence field exists and is uint32" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "likely throws if field missing or wrong type (framework dependent)", + "field": "mptNode.getAccountID(sfIssuer)", + "location": "getIDFromCreatedIssuance", + "validated_by": "STObject method (likely internal validation)", + "validates": [ + "Checks that sfIssuer field exists and is AccountID" + ], + "validation_type": "type|format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp.ai.md b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp.ai.md new file mode 100644 index 0000000000..bfa6a8866c --- /dev/null +++ b/src/xrpld/rpc/detail/MPTokenIssuanceID.cpp.ai.md @@ -0,0 +1,25 @@ +# `src/xrpld/rpc/detail/MPTokenIssuanceID.cpp` + +## Purpose + +This file provides the RPC-layer utility responsible for surfacing the `mpt_issuance_id` field in transaction metadata JSON responses. When a `MPTokenIssuanceCreate` transaction succeeds, the XRP Ledger engine allocates a new `MPTokenIssuance` ledger object and records it in the transaction's metadata. The identifier of that object is a 192-bit `MPTID` — composed of the issuer's 32-bit account sequence (big-endian) concatenated with the issuer's 160-bit `AccountID` — but it does not appear verbatim in the transaction fields. This file extracts that ID from the transaction metadata and injects it into the JSON response as a convenience for API consumers. + +## Design Pattern + +The implementation follows the exact same three-function guard/extract/insert pattern used by the neighbouring `DeliveredAmount.cpp`: a predicate gates eligibility, a separate extractor digs into transaction metadata, and a public entry point composes both. This separation makes each concern independently testable and keeps the public API surface minimal. + +## Function Breakdown + +`canHaveMPTokenIssuanceID` is the gatekeeper. It enforces three conditions in sequence: the serialized transaction pointer must be non-null, its type must be `ttMPTOKEN_ISSUANCE_CREATE`, and the transaction engine result must satisfy `isTesSuccess`. The third condition matters because failed transactions may still produce metadata nodes — for example, they may have modified accounts for fee purposes — but no `MPTokenIssuance` object will have been created. Checking the TER here avoids scanning nodes unnecessarily and prevents false positives. + +`getIDFromCreatedIssuance` walks the transaction metadata node set looking for an entry whose `sfLedgerEntryType` is `ltMPTOKEN_ISSUANCE` and whose field name (as returned by `getFName()`) is `sfCreatedNode`. Both checks are needed: `sfLedgerEntryType` confirms the object type, while `sfCreatedNode` confirms the operation — the engine records newly created ledger objects under `sfCreatedNode`, modified ones under `sfModifiedNode`. Once the right node is identified, the function reads `sfNewFields` (the post-creation state snapshot stored in metadata), casts it to `STObject`, and calls `makeMptID(sfSequence, sfIssuer)` to reconstruct the canonical identifier. The result is wrapped in `std::optional`, with `std::nullopt` returned if no matching node is found — a defensive choice since the caller already filters on success, but metadata structure should never be assumed complete. + +`insertMPTokenIssuanceID` is the public entry point. It delegates first to `canHaveMPTokenIssuanceID`, then to `getIDFromCreatedIssuance`, and only writes `response[jss::mpt_issuance_id]` when both succeed. The response field is a stringified `MPTID`. + +## Call Sites + +The function is called from multiple response-building paths: `LedgerToJson.cpp` (when serializing ledger transactions to JSON under both `jss::meta` and `jss::metaData`), `NetworkOPs.cpp` (for streaming events), `Tx.cpp`, `AccountTx.cpp`, and `Simulate.cpp`. All call sites pass the raw `STTx` and a freshly constructed `TxMeta` so the extraction is always performed against authoritative, committed metadata. + +## Relationship to `MPTID` + +`MPTID` is defined in `include/xrpl/protocol/UintTypes.h` as `base_uint<192>`. `makeMptID` (declared in `Indexes.h`) packs the 32-bit sequence and 160-bit account into that 192-bit value. The ID is deterministic and globally unique because each account sequence number is consumed exactly once per account, and a given account can only issue an MPToken issuance once per sequence. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathRequest.cpp.ai.json b/src/xrpld/rpc/detail/PathRequest.cpp.ai.json new file mode 100644 index 0000000000..0699006463 --- /dev/null +++ b/src/xrpld/rpc/detail/PathRequest.cpp.ai.json @@ -0,0 +1,815 @@ +{ + "args": [ + { + "lineno": 14, + "name": "app" + }, + { + "lineno": 15, + "name": "subscriber" + }, + { + "lineno": 16, + "name": "id" + }, + { + "lineno": 17, + "name": "owner" + }, + { + "lineno": 18, + "name": "journal" + }, + { + "lineno": 30, + "name": "completion" + }, + { + "lineno": 31, + "name": "consumer" + }, + { + "lineno": 103, + "name": "crCache" + }, + { + "lineno": 152, + "name": "cache" + }, + { + "lineno": 152, + "name": "value" + }, + { + "lineno": 181, + "name": "jvParams" + }, + { + "lineno": 70, + "name": "newOnly" + }, + { + "lineno": 70, + "name": "index" + }, + { + "lineno": 354, + "name": "level" + }, + { + "lineno": 355, + "name": "jvArray" + }, + { + "lineno": 356, + "name": "continueCallback" + }, + { + "lineno": 430, + "name": "fast" + } + ], + "classes": [ + { + "args": [ + "Application& app, std::shared_ptr const& subscriber, int id, PathRequestManager& owner, beast::Journal journal", + "Application& app, std::function const& completion, Resource::Consumer& consumer, int id, PathRequestManager& owner, beast::Journal journal" + ], + "lineno": 12, + "name": "PathRequest" + } + ], + "code_paths": [ + { + "call_chain": [ + "PathRequestManager::doCreate", + "PathRequest::parseJson", + "PathRequest::isValid" + ], + "entry_point": "PathRequest::doCreate / PathRequestManager::doCreate", + "purpose": "Handles creation of a new PathRequest from incoming RPC JSON, parses and validates input.", + "validation_points": [ + "PathRequest::parseJson (input parsing, type checks)", + "PathRequest::isValid (semantic validation of accounts, amounts, etc.)" + ] + }, + { + "call_chain": [ + "PathRequestManager::updateAll", + "PathRequest::needsUpdate" + ], + "entry_point": "PathRequest::needsUpdate", + "purpose": "Determines if a PathRequest needs to be updated based on ledger index and state.", + "validation_points": [ + "PathRequest::needsUpdate (checks mInProgress, mLastIndex, index)" + ] + }, + { + "call_chain": [ + "PathRequestManager::updateAll", + "PathRequest::updateComplete" + ], + "entry_point": "PathRequest::updateComplete", + "purpose": "Marks a PathRequest as no longer in progress and triggers completion callback if set.", + "validation_points": [ + "PathRequest::updateComplete (asserts mInProgress, resets state)" + ] + }, + { + "call_chain": [ + "PathRequest::doCreate", + "PathRequest::isValid" + ], + "entry_point": "PathRequest::isValid", + "purpose": "Validates the logical correctness of the PathRequest (accounts exist, amounts valid, etc.)", + "validation_points": [ + "PathRequest::isValid (multiple semantic checks, error setting)" + ] + } + ], + "data_flows": [ + { + "field": "subscriber", + "flow": [ + "constructor argument", + "stored as wpSubscriber (weak_ptr)", + "used to get consumer_ via subscriber->getConsumer()" + ], + "origin": "Passed to PathRequest constructor as std::shared_ptr", + "transformations": [ + "Type-checked by shared_ptr", + "Dereferenced for getConsumer()" + ], + "validated_at": "constructor (type system), getConsumer()" + }, + { + "field": "consumer_", + "flow": [ + "constructor", + "stored as member", + "used for resource tracking/limits" + ], + "origin": "Set in constructor from subscriber->getConsumer() or directly passed", + "transformations": [ + "None (direct assignment)" + ], + "validated_at": "constructor (type system)" + }, + { + "field": "id (iIdentifier)", + "flow": [ + "constructor argument", + "stored as iIdentifier", + "used for logging, identification" + ], + "origin": "Passed to constructor", + "transformations": [ + "None" + ], + "validated_at": "constructor (type system: int)" + }, + { + "field": "mLastIndex", + "flow": [ + "constructor", + "checked in isNew, needsUpdate", + "updated after processing" + ], + "origin": "Initialized to 0 in constructor", + "transformations": [ + "Compared to 0 or ledger index" + ], + "validated_at": "isNew, needsUpdate" + }, + { + "field": "raSrcAccount / raDstAccount", + "flow": [ + "parseJson (from JSON)", + "stored as member", + "used in isValid for existence checks" + ], + "origin": "Set during parseJson from input JSON", + "transformations": [ + "Parsed from string to AccountID" + ], + "validated_at": "isValid (null check, existence in ledger)" + }, + { + "field": "saSendMax / saDstAmount", + "flow": [ + "parseJson (from JSON)", + "stored as member", + "used in isValid for amount checks" + ], + "origin": "Set during parseJson from input JSON", + "transformations": [ + "Parsed from JSON to STAmount" + ], + "validated_at": "isValid (amount logic, non-negativity, etc.)" + } + ], + "description": "Implements the PathRequest class, which manages and processes pathfinding requests for payments in the XRPL (XRP Ledger) system, including parsing input, validating requests, finding payment paths, and updating request status.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "subscriber (type, shared_ptr)", + "validation", + "missing", + "check" + ], + "evidence": "Field subscriber (type, shared_ptr) validated by C++ type system, RAII, business logic checks", + "issue_pattern": "Missing validation for subscriber (type, shared_ptr)", + "why_false_positive": "C++ type system, RAII, business logic checks validates subscriber (type, shared_ptr) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "consumer (type, reference)", + "validation", + "missing", + "check" + ], + "evidence": "Field consumer (type, reference) validated by C++ type system, RAII, business logic checks", + "issue_pattern": "Missing validation for consumer (type, reference)", + "why_false_positive": "C++ type system, RAII, business logic checks validates consumer (type, reference) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "id (type, int)", + "validation", + "missing", + "check" + ], + "evidence": "Field id (type, int) validated by C++ type system, RAII, business logic checks", + "issue_pattern": "Missing validation for id (type, int)", + "why_false_positive": "C++ type system, RAII, business logic checks validates id (type, int) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "subscriber", + "empty", + "string", + "validation" + ], + "evidence": "std::shared_ptr (RAII, type system) at PathRequest constructor", + "issue_pattern": "Missing empty string validation for subscriber", + "why_false_positive": "std::shared_ptr (RAII, type system) validates subscriber for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "subscriber", + "type", + "validation", + "check" + ], + "evidence": "std::shared_ptr (RAII, type system) at PathRequest constructor", + "issue_pattern": "Missing type validation for subscriber", + "why_false_positive": "std::shared_ptr (RAII, type system) validates subscriber type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "consumer", + "empty", + "string", + "validation" + ], + "evidence": "Resource::Consumer& (type system) at PathRequest constructor (overload 2)", + "issue_pattern": "Missing empty string validation for consumer", + "why_false_positive": "Resource::Consumer& (type system) validates consumer for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "consumer", + "type", + "validation", + "check" + ], + "evidence": "Resource::Consumer& (type system) at PathRequest constructor (overload 2)", + "issue_pattern": "Missing type validation for consumer", + "why_false_positive": "Resource::Consumer& (type system) validates consumer type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "id", + "empty", + "string", + "validation" + ], + "evidence": "int (type system) at PathRequest constructor", + "issue_pattern": "Missing empty string validation for id", + "why_false_positive": "int (type system) validates id for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "id", + "type", + "validation", + "check" + ], + "evidence": "int (type system) at PathRequest constructor", + "issue_pattern": "Missing type validation for id", + "why_false_positive": "int (type system) validates id type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "subscriber", + "empty", + "string", + "validation" + ], + "evidence": "subscriber->getConsumer() (method call, pointer dereference) at PathRequest constructor", + "issue_pattern": "Missing empty string validation for subscriber", + "why_false_positive": "subscriber->getConsumer() (method call, pointer dereference) validates subscriber for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mLastIndex", + "empty", + "string", + "validation" + ], + "evidence": "comparison (mLastIndex == 0) at isNew()", + "issue_pattern": "Missing empty string validation for mLastIndex", + "why_false_positive": "comparison (mLastIndex == 0) validates mLastIndex for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mInProgress", + "empty", + "string", + "validation" + ], + "evidence": "if (mInProgress) at needsUpdate()", + "issue_pattern": "Missing empty string validation for mInProgress", + "why_false_positive": "if (mInProgress) validates mInProgress for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "mLastIndex", + "empty", + "string", + "validation" + ], + "evidence": "if (newOnly && (mLastIndex != 0)) at needsUpdate()", + "issue_pattern": "Missing empty string validation for mLastIndex", + "why_false_positive": "if (newOnly && (mLastIndex != 0)) validates mLastIndex for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/PathRequest.cpp", + "functions": [ + { + "args": [ + "Application& app", + "std::shared_ptr const& subscriber", + "int id", + "PathRequestManager& owner", + "beast::Journal journal" + ], + "lineno": 13, + "name": "PathRequest" + }, + { + "args": [ + "Application& app", + "std::function const& completion", + "Resource::Consumer& consumer", + "int id", + "PathRequestManager& owner", + "beast::Journal journal" + ], + "lineno": 28, + "name": "PathRequest" + }, + { + "args": [], + "lineno": 43, + "name": "~PathRequest" + }, + { + "args": [], + "lineno": 62, + "name": "isNew" + }, + { + "args": [ + "bool newOnly", + "LedgerIndex index" + ], + "lineno": 69, + "name": "needsUpdate" + }, + { + "args": [], + "lineno": 87, + "name": "hasCompletion" + }, + { + "args": [], + "lineno": 91, + "name": "updateComplete" + }, + { + "args": [ + "std::shared_ptr const& crCache" + ], + "lineno": 102, + "name": "isValid" + }, + { + "args": [ + "std::shared_ptr const& cache", + "Json::Value const& value" + ], + "lineno": 151, + "name": "doCreate" + }, + { + "args": [ + "Json::Value const& jvParams" + ], + "lineno": 180, + "name": "parseJson" + }, + { + "args": [], + "lineno": 312, + "name": "doClose" + }, + { + "args": [ + "Json::Value const&" + ], + "lineno": 319, + "name": "doStatus" + }, + { + "args": [], + "lineno": 326, + "name": "doAborting" + }, + { + "args": [ + "std::shared_ptr const& cache", + "hash_map>& pathasset_map", + "PathAsset const& asset", + "STAmount const& dst_amount", + "int const level", + "std::function const& continueCallback" + ], + "lineno": 331, + "name": "getPathFinder" + }, + { + "args": [ + "std::shared_ptr const& cache", + "int const level", + "Json::Value& jvArray", + "std::function const& continueCallback" + ], + "lineno": 353, + "name": "findPaths" + }, + { + "args": [ + "std::shared_ptr const& cache", + "bool fast", + "std::function const& continueCallback" + ], + "lineno": 429, + "name": "doUpdate" + }, + { + "args": [], + "lineno": 502, + "name": "getSubscriber" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "PathRequest and related validation logic are typically tested in the XRPLF/rippled repo under 'src/test/rpc' (e.g., PathRequest_test.cpp, Path_find_test.cpp, PathRequestManager_test.cpp). These tests cover valid/invalid input, account existence, amount logic, and update logic. However, edge cases such as malformed JSON, rare race conditions in needsUpdate/updateComplete, and resource exhaustion scenarios may not be fully covered. Fuzzing or property-based tests for parseJson and isValid would improve coverage.", + "validation_architecture": { + "auto_validated_fields": [ + "subscriber (type, shared_ptr)", + "consumer (type, reference)", + "id (type, int)" + ], + "framework": "C++ type system, RAII, business logic checks", + "validation_layer": "constructor (type), business_logic (methods)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "n/a (compile-time type enforcement)", + "field": "subscriber", + "location": "PathRequest constructor", + "validated_by": "std::shared_ptr (RAII, type system)", + "validates": [ + "subscriber must be a valid shared_ptr" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (compile-time type enforcement)", + "field": "consumer", + "location": "PathRequest constructor (overload 2)", + "validated_by": "Resource::Consumer& (type system)", + "validates": [ + "consumer must be a valid Resource::Consumer reference" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (compile-time type enforcement)", + "field": "id", + "location": "PathRequest constructor", + "validated_by": "int (type system)", + "validates": [ + "id must be an integer" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "undefined behavior if subscriber is null", + "field": "subscriber", + "location": "PathRequest constructor", + "validated_by": "subscriber->getConsumer() (method call, pointer dereference)", + "validates": [ + "subscriber must not be null" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (returns bool)", + "field": "mLastIndex", + "location": "isNew()", + "validated_by": "comparison (mLastIndex == 0)", + "validates": [ + "checks if request is new (mLastIndex == 0)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (returns false)", + "field": "mInProgress", + "location": "needsUpdate()", + "validated_by": "if (mInProgress)", + "validates": [ + "checks if another thread is handling this" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (returns false)", + "field": "mLastIndex", + "location": "needsUpdate()", + "validated_by": "if (newOnly && (mLastIndex != 0))", + "validates": [ + "checks if only new requests are handled" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathRequest.cpp.ai.md b/src/xrpld/rpc/detail/PathRequest.cpp.ai.md new file mode 100644 index 0000000000..f46206eaf3 --- /dev/null +++ b/src/xrpld/rpc/detail/PathRequest.cpp.ai.md @@ -0,0 +1,50 @@ +# `PathRequest.cpp` — Per-Request Pathfinding State Machine + +`PathRequest` represents a single client's request to discover viable payment paths across the XRP Ledger's trust-line graph. It is the computational unit that `PathRequestManager` schedules, tracks, and updates: one object per outstanding `path_find` subscription or per-call `ripple_path_find` invocation. The file contains everything from JSON parsing and ledger-state validation through the actual pathfinding loop and `RippleCalc` cost estimation. + +## Two Constructor Flavors, One State Machine + +The class has two public constructors, each representing a distinct API contract. The subscription constructor (`path_find` semantics) stores a `std::weak_ptr` to the connected client and has no completion callback — results are pushed on every ledger close. The legacy constructor (`ripple_path_find` semantics) accepts a `std::function` callback and a `Resource::Consumer` reference directly, with the intention that it runs once and fires the callback when done. `hasCompletion()` is the runtime predicate used throughout the file to branch on which mode is active, most visibly in `doCreate` (which skips the initial fast pass for legacy requests) and in `findPaths` (which adds `paths_canonical` to the JSON output for the old API). + +Holding the subscriber as a `weak_ptr` is a deliberate choice: the subscriber can be destroyed by the network layer at any time, and `PathRequest` must not prevent that cleanup. `getSubscriber()` re-locks the weak pointer on demand; a null return means the subscription is gone. + +## Two-Phase Initialization: `doCreate` → `doUpdate` + +`doCreate` is the entry point called by `PathRequestManager` when a client first submits a request. It chains `parseJson` → `isValid`, and — only for subscription mode — immediately calls `doUpdate(cache, true)` for a fast preliminary answer. This "fast" pass uses `PATH_SEARCH_FAST` depth, giving the client something useful in the same request/response cycle before the background engine later runs a deeper search. If either parsing or validation fails, the error is embedded in `jvStatus` and returned to the caller; the request is effectively dead before it is ever queued. + +## Parsing: `parseJson` + +`parseJson` is strictly syntactic and structural. It enforces the presence of `source_account`, `destination_account`, and `destination_amount`; decodes them into `AccountID` and `STAmount`; and handles the optional `send_max`, `source_currencies`, and `domain` fields. Several non-obvious rules are codified here: + +- `send_max` is only legal when `destination_amount` is the "convert all" sentinel (value `-1`, represented as `STAmount` with max-flag). This enforces the invariant that `send_max` is a source-side cap on a "deliver as much as possible" payment. +- Source currencies can be specified as either traditional `currency`/`issuer` pairs or as `mpt_issuance_id` hex strings, reflecting XRPL's newer Multi-Purpose Token standard. The `PathAsset` variant type and `sciSourceAssets` set (of type `std::set`) hold both kinds without separate code paths. +- When `send_max` is present, the source-currency list is filtered to only the asset matching `send_max`. Issuer reconciliation logic handles cases where neither the explicit issuer nor the `send_max` issuer is the source account. +- The `domain` parameter (a 256-bit identifier) restricts pathfinding to a defined permissioned domain. It is parsed and stored as `std::optional` and threaded through to both `Pathfinder` construction and `RippleCalc::rippleCalculate`. + +Return values use preprocessor constants (`PFR_PJ_INVALID`, `PFR_PJ_NOCHANGE`) rather than an enum, which is a legacy holdover from before the code was unified with MPT support. + +## Ledger-State Validation: `isValid` + +`isValid` runs against a live `AssetCache` (a snapshot of the current ledger). It confirms that the source account exists and that, if the destination is new, the payment is XRP and meets the reserve. For an existing destination account, it populates `destination_currencies` in `jvStatus` via `accountDestAssets`, respecting the `lsfDisallowXRP` flag. This output is consumed by the client to build a currency picker UI without a separate RPC call. `isValid` is called both during `doCreate` and at the start of every `doUpdate`, because ledger state can change between updates. + +## Core Pathfinding Loop: `findPaths` + +`findPaths` is the heart of the file. It determines the effective set of source assets: explicit (`sciSourceAssets`), derived from `send_max`, or automatically enumerated from the source account's holdings via `accountSourceAssets` — capped at `RPC::Tuning::max_auto_src_cur` (88) to prevent runaway work. When source and destination are the same account, assets matching the destination amount are excluded. + +For each candidate source asset, `getPathFinder` either retrieves or constructs a `Pathfinder` for that asset from a local `hash_map` keyed by `PathAsset`. The map lives only for the duration of one `findPaths` call, so `Pathfinder` objects are never reused across ledger updates — each update gets a fresh view of the graph. The `Pathfinder` runs `findPaths` at the configured depth level, then `computePathRanks` to score candidates, limiting results to `max_paths_` (hard-coded at 4). + +`getBestPaths` returns a `STPathSet` plus a `fullLiquidityPath` — a single path that may unlock more liquidity but was too expensive to include in the ranked set. The code then calls `path::RippleCalc::rippleCalculate` on a `PaymentSandbox` (an ephemeral, non-committing view of the ledger) to get realistic `actualAmountIn`/`actualAmountOut` estimates. If the calculation fails with `terNO_LINE` or `tecPATH_PARTIAL` and a `fullLiquidityPath` exists, it retries with that path appended — a two-shot fallback that often rescues otherwise partial paths. This retry is intentionally skipped for `convert_all_` mode because partial payments are already allowed there. + +The resource fee charged to the client follows the formula `clamp(size² + 34, 50, 400)`, where `size` is the number of source assets evaluated. The quadratic term captures the fact that path complexity grows super-linearly with source currencies; the clamp keeps the cost in a [50, 400] range regardless of edge cases. + +## Adaptive Search Depth: `doUpdate` and `iLevel` + +`doUpdate` manages `iLevel`, the search depth passed to `Pathfinder`. The level adapts over successive updates based on three signals: whether the server is locally loaded (`app_.getFeeTrack().isLoadedLocal()`), whether this is a fast pass, and whether the last update found any paths (`bLastSuccess`). Under load, depth is capped at `PATH_SEARCH_FAST`; if the last update succeeded, depth decrements toward `PATH_SEARCH`; if it failed, depth increments toward `PATH_SEARCH_MAX` unless load prevents it. This feedback loop prevents expensive searches when the server is stressed and gradually invests more effort into requests that are actually finding results. + +## Concurrency: Two Locks with Distinct Purposes + +`PathRequest` uses two recursive mutexes. `mIndexLock` guards the scheduling state (`mLastIndex`, `mInProgress`) that `PathRequestManager` reads from multiple threads via `needsUpdate` and `updateComplete`. `needsUpdate` atomically checks and sets `mInProgress`, ensuring only one background thread processes a given request at a time and that stale ledger indices are not reprocessed. `mLock` guards `jvStatus`, the JSON result object that may be read by the subscriber thread concurrently with a background update writing a new result. Using `recursive_mutex` rather than `mutex` avoids deadlock when `isValid` (which writes `jvStatus`) is called from `doUpdate` while `mLock` is already held. + +## Timing Instrumentation + +Each `PathRequest` records three `steady_clock` time points: `created_` (at construction), `quick_reply_` (first fast-pass completion), and `full_reply_` (first full-pass completion). These are reported to `PathRequestManager` via `reportFast`/`reportFull`, which feed `beast::insight::Event` metrics for monitoring. The destructor logs the total lifetime and both latencies at `info` level, making it straightforward to correlate pathfinding responsiveness with ledger activity in production logs. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathRequest.h.ai.json b/src/xrpld/rpc/detail/PathRequest.h.ai.json new file mode 100644 index 0000000000..51faabec88 --- /dev/null +++ b/src/xrpld/rpc/detail/PathRequest.h.ai.json @@ -0,0 +1,122 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Application& app, std::shared_ptr const& subscriber, int id, PathRequestManager&, beast::Journal journal", + "Application& app, std::function const& completion, Resource::Consumer& consumer, int id, PathRequestManager&, beast::Journal journal" + ], + "lineno": 27, + "name": "PathRequest" + } + ], + "description": "Defines the PathRequest class, which represents a pathfinding request in the XRPL system, handling client requests for payment paths, managing request state, and interfacing with pathfinding logic and subscribers.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/PathRequest.h", + "functions": [ + { + "args": [], + "lineno": 49, + "name": "isNew" + }, + { + "args": [ + "newOnly", + "index" + ], + "lineno": 50, + "name": "needsUpdate" + }, + { + "args": [], + "lineno": 54, + "name": "updateComplete" + }, + { + "args": [ + "std::shared_ptr const&", + "Json::Value const&" + ], + "lineno": 57, + "name": "doCreate" + }, + { + "args": [], + "lineno": 60, + "name": "doClose" + }, + { + "args": [ + "Json::Value const&" + ], + "lineno": 61, + "name": "doStatus" + }, + { + "args": [], + "lineno": 62, + "name": "doAborting" + }, + { + "args": [ + "std::shared_ptr const&", + "bool", + "std::function const&" + ], + "lineno": 66, + "name": "doUpdate" + }, + { + "args": [], + "lineno": 70, + "name": "getSubscriber" + }, + { + "args": [], + "lineno": 72, + "name": "hasCompletion" + }, + { + "args": [ + "std::shared_ptr const&" + ], + "lineno": 76, + "name": "isValid" + }, + { + "args": [ + "std::shared_ptr const&", + "hash_map>&", + "PathAsset const&", + "STAmount const&", + "int const", + "std::function const&" + ], + "lineno": 79, + "name": "getPathFinder" + }, + { + "args": [ + "std::shared_ptr const&", + "int const", + "Json::Value&", + "std::function const&" + ], + "lineno": 89, + "name": "findPaths" + }, + { + "args": [ + "Json::Value const&" + ], + "lineno": 96, + "name": "parseJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathRequest.h.ai.md b/src/xrpld/rpc/detail/PathRequest.h.ai.md new file mode 100644 index 0000000000..cbbd7402df --- /dev/null +++ b/src/xrpld/rpc/detail/PathRequest.h.ai.md @@ -0,0 +1,53 @@ +# PathRequest.h — Per-request Pathfinding State Machine + +`PathRequest` is the object that backs a single in-flight payment-path query on the XRPL node. It bridges the RPC layer — where clients ask "how can I pay X to account Y?" — and the low-level `Pathfinder`/`RippleCalc` machinery that searches the ledger's trust-line and order-book graph. The class header is deliberately narrow; almost all logic lives in the paired `.cpp`. + +## Two Operational Modes + +The dual constructors reflect two distinct client-facing APIs with very different lifetime semantics: + +**`path_find` mode** (WebSocket subscription): The first constructor takes a `shared_ptr` subscriber. The node keeps the `PathRequest` alive for the duration of the subscription and re-runs `doUpdate()` on every ledger close, pushing new results to the subscriber. The subscriber holds the only strong reference; `PathRequestManager` stores only `weak_ptr` references in its `requests_` vector, so the request is automatically cleaned up when the WebSocket connection drops. + +**`ripple_path_find` mode** (legacy one-shot): The second constructor takes a `std::function` completion callback and a `Resource::Consumer` reference directly. No subscriber exists; the request runs once, calls the completion function, and the coroutine that holds the strong pointer lets it go. `hasCompletion()` and the presence of `fCompletion` distinguish this mode throughout the implementation. + +The comment in the header — *"The request issuer must maintain a strong pointer"* — is a hard ownership invariant. `PathRequestManager` never prevents garbage collection; it is entirely the calling context's responsibility. + +## Lifecycle and State Transitions + +`doCreate()` is called immediately after construction. It runs `parseJson()` to validate and internalize the client's JSON parameters (source account, destination account, destination amount, optional source currencies, optional send max, optional domain), then `isValid()` to check ledger-level preconditions (accounts exist, amount meets reserve, etc.). For WebSocket mode only, it then calls `doUpdate(cache, /*fast=*/true)` to produce a quick preliminary result before the full search runs on the background thread. + +`needsUpdate()` is the scheduler gate called by `PathRequestManager::updateAll()`. It atomically checks `mInProgress` and `mLastIndex` under `mIndexLock`, setting `mInProgress = true` only if the request is eligible. This prevents two threads from running the same request simultaneously. `updateComplete()` clears `mInProgress` and fires the one-shot completion callback (erasing it afterward to prevent double-firing). + +## Path Search: Fast vs. Full, Level Adaptation + +`doUpdate()` runs the actual pathfinding. The `fast` flag distinguishes a quick preliminary pass from a complete search. The `iLevel` integer controls how deeply `Pathfinder` explores the graph on each invocation. It adapts on every call: + +- On the first pass: `PATH_SEARCH_FAST` if the node is under load or if this is a fast pass; `PATH_SEARCH` otherwise. +- Transitioning from fast to normal: bumps to `PATH_SEARCH`, then decrements by one if the server is loaded. +- On subsequent updates: increments toward `PATH_SEARCH_MAX` when the server is idle and the last search failed; decrements when loaded or when the search previously succeeded. + +This adaptive throttle is a deliberate backpressure mechanism — expensive graph searches are scaled back under CPU pressure so pathfinding doesn't starve consensus. + +`quick_reply_` and `full_reply_` timestamps are set on the first fast and first full completions respectively, and reported to `PathRequestManager` via `reportFast()` / `reportFull()` for metrics collection. + +## findPaths(): Source Enumeration and RippleCalc Integration + +`findPaths()` does three things. First, it collects the set of source assets: from explicit `source_currencies` in the request, from `send_max`'s asset type, or auto-discovered by querying the source account's trust lines and MPT holdings (capped by `RPC::Tuning::max_auto_src_cur`). Second, for each source asset it calls `getPathFinder()`, which lazily constructs and caches a `Pathfinder` in a local `hash_map>`. Third, it runs `path::RippleCalc::rippleCalculate()` to simulate the payment end-to-end and obtain actual `source_amount` figures. + +`getPathFinder()` constructs the `Pathfinder`, calls `findPaths(level, continueCallback)` and `computePathRanks(max_paths_)` on it. If `findPaths` fails (bad request — e.g., no usable trust lines), the `unique_ptr` is reset to null and that source asset is silently skipped. The pathfinder map is local to the `findPaths()` call stack; `mContext` is the persistent state, holding the best `STPathSet` per asset across updates so `getBestPaths()` can use previously discovered paths as seeds. + +The retry path is worth noting: if `RippleCalc` returns `terNO_LINE` or `tecPATH_PARTIAL` and `Pathfinder` produced a `fullLiquidityPath` (a single path that consumes all available liquidity), `findPaths()` appends it and re-runs `rippleCalculate`. This recovers from the case where the ranked paths alone are insufficient but the liquidity path covers the gap. + +## Resource Accounting + +The `Resource::Consumer` charge is quadratic in the number of source currencies evaluated: `clamp((size * size) + 34, 50, 400)`. This reflects the super-linear computational cost of adding more source assets — each new asset requires its own `Pathfinder` graph traversal — and caps both the minimum (50) and maximum (400) charge per update cycle. + +## Concurrency Design + +Two separate `std::recursive_mutex` fields serve distinct purposes. `mLock` protects `jvStatus` — the cached JSON result returned to callers of `doStatus()` and `doClose()`. `mIndexLock` protects the scheduling fields `mLastIndex` and `mInProgress`. Keeping them separate avoids holding the broader `mLock` during potentially long operations in `doUpdate()` while still serializing scheduler state checks. `recursive_mutex` is used (rather than plain `mutex`) because completion callbacks may re-enter the lock through the call chain. + +The `continueCallback` passed to `doUpdate()` and propagated through `findPaths()`, `getPathFinder()`, and into `Pathfinder` itself allows the `PathRequestManager` to abort a long-running search mid-flight — for example, when a new ledger closes and the current search is already stale. + +## Constants and Limits + +`max_paths_ = 4` is a compile-time cap on the number of alternative paths returned per source currency. It balances practical usefulness (wallets rarely need more than four options) against response payload size and the cost of `computePathRanks`. The `PFR_PJ_INVALID` / `PFR_PJ_NOCHANGE` preprocessor constants used in `parseJson()`'s return value are a relic of the original C-style API; `PFR_PJ_NOCHANGE` (0) serves as the success return since there is no separate "changed" state to signal. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathRequestManager.cpp.ai.json b/src/xrpld/rpc/detail/PathRequestManager.cpp.ai.json new file mode 100644 index 0000000000..bac12f4c3b --- /dev/null +++ b/src/xrpld/rpc/detail/PathRequestManager.cpp.ai.json @@ -0,0 +1,579 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "PathRequestManager::updateAll", + "PathRequestManager::getAssetCache", + "AssetCache::getLedger", + "PathRequest::needsUpdate", + "PathRequest::getSubscriber", + "InfoSub::getRequest" + ], + "entry_point": "PathRequestManager::updateAll", + "purpose": "Updates all path requests with the latest ledger and asset cache, removing or updating requests as needed.", + "validation_points": [ + "getAssetCache: Validates assetCache pointer, ledger pointer, and ledger sequence numbers.", + "updateAll: Validates requests_ vector by type, request pointer by lock(), and InfoSub pointer by getSubscriber." + ] + }, + { + "call_chain": [ + "PathRequestManager::getAssetCache", + "AssetCache::getLedger" + ], + "entry_point": "PathRequestManager::getAssetCache", + "purpose": "Retrieves or creates an AssetCache for a given ledger, ensuring the cache is up-to-date and valid.", + "validation_points": [ + "getAssetCache: Validates assetCache pointer, ledger pointer, and ledger sequence numbers." + ] + }, + { + "call_chain": [ + "PathRequestManager::insertPathRequest", + "requests_.push_back" + ], + "entry_point": "PathRequestManager::insertPathRequest", + "purpose": "Inserts a new path request into the manager's request list.", + "validation_points": [ + "insertPathRequest: Validates request pointer by type (weak_ptr)." + ] + }, + { + "call_chain": [ + "PathRequestManager::makePathRequest", + "PathRequest::create" + ], + "entry_point": "PathRequestManager::makePathRequest", + "purpose": "Creates a new path request and adds it to the manager.", + "validation_points": [ + "makePathRequest: Validates input parameters and request pointer." + ] + } + ], + "data_flows": [ + { + "field": "ledger (std::shared_ptr)", + "flow": [ + "Function argument", + "getAssetCache", + "AssetCache constructor", + "AssetCache::getLedger" + ], + "origin": "Passed as argument to getAssetCache and updateAll", + "transformations": [ + "Checked for validity (non-null)", + "Used to get sequence number", + "Possibly wrapped in AssetCache" + ], + "validated_at": "getAssetCache (type system, pointer validity, sequence number checks)" + }, + { + "field": "assetCache (std::shared_ptr)", + "flow": [ + "assetCache_ (weak_ptr)", + "lock() to shared_ptr", + "getLedger()", + "Used in updateAll" + ], + "origin": "Weak pointer member assetCache_", + "transformations": [ + "lock() to check validity", + "Created anew if invalid or outdated" + ], + "validated_at": "getAssetCache (pointer validity, sequence number checks)" + }, + { + "field": "ledger sequence numbers (lineSeq, lgrSeq)", + "flow": [ + "getLedger()->seq()", + "Compared in getAssetCache", + "Used to decide cache validity" + ], + "origin": "lineSeq from assetCache->getLedger()->seq(), lgrSeq from ledger->seq()", + "transformations": [ + "Compared for range and business logic" + ], + "validated_at": "getAssetCache (if conditions on sequence numbers)" + }, + { + "field": "requests_ (std::vector)", + "flow": [ + "Copied under lock in updateAll", + "Iterated in updateAll", + "Each weak_ptr locked to shared_ptr" + ], + "origin": "Member variable of PathRequestManager", + "transformations": [ + "lock() to shared_ptr", + "Removed if expired or not needed" + ], + "validated_at": "updateAll (type system, pointer validity via lock())" + }, + { + "field": "request (PathRequest::pointer)", + "flow": [ + "wr.lock()", + "Checked for validity", + "Used in needsUpdate, getSubscriber" + ], + "origin": "Locked from PathRequest::wptr in requests_", + "transformations": [ + "Checked for null", + "Passed to other functions" + ], + "validated_at": "updateAll (pointer validity via lock())" + } + ], + "description": "Implements the PathRequestManager class, which manages pathfinding requests for the XRPL ledger, including updating, inserting, and processing both new and legacy path requests.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "pointer types (shared_ptr, weak_ptr)", + "validation", + "missing", + "check" + ], + "evidence": "Field pointer types (shared_ptr, weak_ptr) validated by C++ type system, RAII, business logic checks, no explicit jss:: or template validation in this file", + "issue_pattern": "Missing validation for pointer types (shared_ptr, weak_ptr)", + "why_false_positive": "C++ type system, RAII, business logic checks, no explicit jss:: or template validation in this file validates pointer types (shared_ptr, weak_ptr) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "container types (vector)", + "validation", + "missing", + "check" + ], + "evidence": "Field container types (vector) validated by C++ type system, RAII, business logic checks, no explicit jss:: or template validation in this file", + "issue_pattern": "Missing validation for container types (vector)", + "why_false_positive": "C++ type system, RAII, business logic checks, no explicit jss:: or template validation in this file validates container types (vector) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "constness", + "validation", + "missing", + "check" + ], + "evidence": "Field constness validated by C++ type system, RAII, business logic checks, no explicit jss:: or template validation in this file", + "issue_pattern": "Missing validation for constness", + "why_false_positive": "C++ type system, RAII, business logic checks, no explicit jss:: or template validation in this file validates constness automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger (std::shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "type system (smart pointer, constness) at PathRequestManager::getAssetCache", + "issue_pattern": "Missing empty string validation for ledger (std::shared_ptr)", + "why_false_positive": "type system (smart pointer, constness) validates ledger (std::shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger (std::shared_ptr)", + "type", + "validation", + "check" + ], + "evidence": "type system (smart pointer, constness) at PathRequestManager::getAssetCache", + "issue_pattern": "Missing type validation for ledger (std::shared_ptr)", + "why_false_positive": "type system (smart pointer, constness) validates ledger (std::shared_ptr) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "assetCache (std::shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "pointer validity check (assetCache ? ... : ...) at PathRequestManager::getAssetCache", + "issue_pattern": "Missing empty string validation for assetCache (std::shared_ptr)", + "why_false_positive": "pointer validity check (assetCache ? ... : ...) validates assetCache (std::shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger sequence numbers (lineSeq, lgrSeq)", + "empty", + "string", + "validation" + ], + "evidence": "range and business logic checks (if conditions) at PathRequestManager::getAssetCache", + "issue_pattern": "Missing empty string validation for ledger sequence numbers (lineSeq, lgrSeq)", + "why_false_positive": "range and business logic checks (if conditions) validates ledger sequence numbers (lineSeq, lgrSeq) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "requests_ (std::vector)", + "empty", + "string", + "validation" + ], + "evidence": "type system (vector of weak_ptr) at PathRequestManager::updateAll", + "issue_pattern": "Missing empty string validation for requests_ (std::vector)", + "why_false_positive": "type system (vector of weak_ptr) validates requests_ (std::vector) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "requests_ (std::vector)", + "type", + "validation", + "check" + ], + "evidence": "type system (vector of weak_ptr) at PathRequestManager::updateAll", + "issue_pattern": "Missing type validation for requests_ (std::vector)", + "why_false_positive": "type system (vector of weak_ptr) validates requests_ (std::vector) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "request (PathRequest::pointer)", + "empty", + "string", + "validation" + ], + "evidence": "pointer validity check (request = wr.lock()) at PathRequestManager::updateAll", + "issue_pattern": "Missing empty string validation for request (PathRequest::pointer)", + "why_false_positive": "pointer validity check (request = wr.lock()) validates request (PathRequest::pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "subscriber (InfoSub::pointer)", + "empty", + "string", + "validation" + ], + "evidence": "getSubscriber lambda (ipSub && ipSub->getRequest() == request) at PathRequestManager::updateAll (getSubscriber lambda)", + "issue_pattern": "Missing empty string validation for subscriber (InfoSub::pointer)", + "why_false_positive": "getSubscriber lambda (ipSub && ipSub->getRequest() == request) validates subscriber (InfoSub::pointer) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/PathRequestManager.cpp", + "functions": [ + { + "args": [ + "ledger", + "authoritative" + ], + "lineno": 10, + "name": "getAssetCache" + }, + { + "args": [ + "inLedger" + ], + "lineno": 34, + "name": "updateAll" + }, + { + "args": [], + "lineno": 128, + "name": "requestsPending" + }, + { + "args": [ + "req" + ], + "lineno": 134, + "name": "insertPathRequest" + }, + { + "args": [ + "subscriber", + "inLedger", + "requestJson" + ], + "lineno": 146, + "name": "makePathRequest" + }, + { + "args": [ + "req", + "completion", + "consumer", + "inLedger", + "request" + ], + "lineno": 162, + "name": "makeLegacyPathRequest" + }, + { + "args": [ + "consumer", + "inLedger", + "request" + ], + "lineno": 186, + "name": "doLegacyPathRequest" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this code likely exists in integration or functional tests for pathfinding and RPC requests, such as tests in 'test/rpc/PathRequest_test.cpp', 'test/app/PathRequestManager_test.cpp', or similar. However, direct unit tests for validation logic (e.g., sequence number edge cases, assetCache invalidation, expired requests) may be limited or missing. Edge cases such as rapid ledger jumps, expired weak_ptrs, or invalid assetCache scenarios should be explicitly tested if not already covered.", + "validation_architecture": { + "auto_validated_fields": [ + "pointer types (shared_ptr, weak_ptr)", + "container types (vector)", + "constness" + ], + "framework": "C++ type system, RAII, business logic checks, no explicit jss:: or template validation in this file", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "n/a (compile-time/type safety)", + "field": "ledger (std::shared_ptr)", + "location": "PathRequestManager::getAssetCache", + "validated_by": "type system (smart pointer, constness)", + "validates": [ + "ledger must be a valid shared_ptr to ReadView const" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (logic branch, not exception)", + "field": "assetCache (std::shared_ptr)", + "location": "PathRequestManager::getAssetCache", + "validated_by": "pointer validity check (assetCache ? ... : ...)", + "validates": [ + "assetCache is checked for null before dereference" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (logic branch, not exception)", + "field": "ledger sequence numbers (lineSeq, lgrSeq)", + "location": "PathRequestManager::getAssetCache", + "validated_by": "range and business logic checks (if conditions)", + "validates": [ + "lineSeq == 0 (no ledger)", + "authoritative && (lgrSeq > lineSeq) (newer authoritative ledger)", + "authoritative && ((lgrSeq + 8) < lineSeq) (jumped way back)", + "lgrSeq > (lineSeq + 8) (jumped way forward)" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "n/a", + "field": "requests_ (std::vector)", + "location": "PathRequestManager::updateAll", + "validated_by": "type system (vector of weak_ptr)", + "validates": [ + "requests_ is a vector of weak_ptr, checked for lock() validity" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (logic branch, not exception)", + "field": "request (PathRequest::pointer)", + "location": "PathRequestManager::updateAll", + "validated_by": "pointer validity check (request = wr.lock())", + "validates": [ + "request is checked for null after lock()" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "n/a (returns nullptr, calls doAborting)", + "field": "subscriber (InfoSub::pointer)", + "location": "PathRequestManager::updateAll (getSubscriber lambda)", + "validated_by": "getSubscriber lambda (ipSub && ipSub->getRequest() == request)", + "validates": [ + "subscriber is valid and matches request", + "if not, request->doAborting() is called" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathRequestManager.cpp.ai.md b/src/xrpld/rpc/detail/PathRequestManager.cpp.ai.md new file mode 100644 index 0000000000..937c0a4913 --- /dev/null +++ b/src/xrpld/rpc/detail/PathRequestManager.cpp.ai.md @@ -0,0 +1,50 @@ +# PathRequestManager.cpp + +`PathRequestManager` is the central coordinator for all live payment pathfinding requests in rippled. Payment pathfinding is inherently expensive — it involves graph traversal over trust lines and order books — so this class exists to batch and amortize that work: a shared `AssetCache` is built once per ledger and reused across all concurrent client requests, and a background thread processes the full request list whenever the validated ledger advances. + +## The Three Pathfinding Modes + +The class exposes three distinct entry points that correspond to two different RPC APIs and their operating models: + +**`makePathRequest`** is the subscription-based `path_find` WebSocket command. A client registers interest and is pushed an update every time a new ledger validates. The `PathRequest` holds a weak reference back to the `InfoSub` subscriber. When `updateAll` fires, it re-checks liveness via the `getSubscriber` lambda before each update, allowing the manager to silently discard requests whose clients have disconnected without any explicit cleanup message from the client. + +**`makeLegacyPathRequest`** is the asynchronous variant of the old `ripple_path_find` command. It registers the request with the manager and signals `LedgerMaster` via `newPathRequest()` to schedule a background path-find pass. A completion callback is invoked when the update is done. If `newPathRequest()` returns false (job queue at capacity), the method immediately returns `rpcTOO_BUSY` and resets the request pointer — the caller must handle this gracefully. + +**`doLegacyPathRequest`** is the fully synchronous fallback. It creates a fresh, ephemeral `AssetCache` bound to the caller's ledger and runs `doUpdate` immediately without registering any persistent state. It never enters the `requests_` vector and never interacts with the background thread. + +## AssetCache Lifecycle: Deliberate Weak Ownership + +The manager holds `assetCache_` as a `std::weak_ptr`, not a `shared_ptr`. This is an intentional design decision explained by a comment in `getAssetCache`: if the member were a `shared_ptr`, the cache would be kept alive even after all requests needing it had finished. Instead, the cache lives only as long as there is at least one outstanding path request or in-progress update holding a `shared_ptr` to it. In `getAssetCache`, the new cache is first assigned to a local `shared_ptr` and only then stored to `assetCache_` — if it were assigned directly to the weak member first, it would be immediately destroyed since no other owner exists yet. + +## Cache Invalidation in `getAssetCache` + +The `authoritative` parameter distinguishes whether the caller is driving the main background sweep (authoritative) or is a one-shot or setup call (non-authoritative). The cache is rebuilt under four conditions: + +1. No prior cache exists (`lineSeq == 0`). +2. An authoritative call presents a strictly newer ledger — the normal advance case. +3. An authoritative call presents a ledger more than 8 slots *older* than the cached one — a backward jump indicating chain reorganization or a sync restart. +4. Any call presents a ledger more than 8 slots *newer* than the cached one — a forward jump that would make the cache stale by too large a margin. + +The ±8 tolerance prevents rebuilding the cache on every minor ledger gap during initial sync or fast validation while still catching situations where the cache would become meaningfully incorrect. + +## The `updateAll` Processing Loop + +`updateAll` runs on a `jtPATH_FIND` job queue thread dispatched by `LedgerMaster`. The central challenge it solves is that new `path_find` subscriptions can arrive while the existing queue is being processed. If the loop processed each request once and exited, a client subscribing near the end of the pass would wait an entire ledger close before seeing a first result. The loop handles this with a re-entrant structure driven by `LedgerMaster::isNewPathRequest()`: + +- If a new request arrives mid-pass (detected by comparing the `newRequests` flag before and after iterating), `mustBreak` is set and the loop restarts from the beginning with `newRequests = true`, ensuring the newcomer is processed promptly. +- If the pass started with `newRequests = true`, it performs one more pass after draining to handle any requests that arrived during the second pass. +- The loop exits only when no new requests appeared during the last full pass. + +The request list is copied into a local vector under lock, and the lock is released before iterating. This keeps the critical section minimal — individual `doUpdate` calls can be lengthy, and holding the lock would block `insertPathRequest` from adding new subscriptions. + +## Subscriber Validity and the `getSubscriber` Lambda + +The `getSubscriber` lambda enforces a two-part liveness check: the `InfoSub` weak pointer must be lockable *and* `ipSub->getRequest()` must still point to the same `PathRequest`. The second check handles the case where a client closes and reopens a `path_find` session in quick succession. In that scenario the old `PathRequest` still has a valid `InfoSub`, but the subscriber's "current request" has been replaced by the new one. The lambda catches this mismatch and calls `request->doAborting()` to clean up the stale request. + +Just before calling `doUpdate`, the code explicitly calls `ipSub.reset()` to release the subscriber reference. This is intentional: if the client disconnects during the (potentially long) path computation, the `InfoSub` can be freed immediately rather than being kept alive by the local shared pointer for the duration of the update. After `doUpdate` returns, `getSubscriber` is called again to acquire a fresh pointer; if that second lock fails, the update result is silently discarded. + +## Request Ordering and Backpressure + +`insertPathRequest` maintains a sorted invariant in `requests_`: new (unserviced) requests are inserted before already-serviced ones. It finds the first request where `!r->isNew()` using `std::find_if` and inserts immediately before it. This ensures that during a pass started because of new arrivals, those new requests are encountered early and serviced quickly rather than buried behind a large queue of already-updated subscriptions. + +Rate limiting is integrated at the `Consumer::warn()` check point in `updateAll`. If the RPC resource manager considers the client to be generating excessive load, the update cycle is skipped for that iteration, but the request is retained in the queue and will be retried on the next ledger pass. This gives the rate limiter a natural integration point without requiring any separate eviction mechanism. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathRequestManager.h.ai.json b/src/xrpld/rpc/detail/PathRequestManager.h.ai.json new file mode 100644 index 0000000000..671e6f1ee9 --- /dev/null +++ b/src/xrpld/rpc/detail/PathRequestManager.h.ai.json @@ -0,0 +1,117 @@ +{ + "args": [ + { + "lineno": 13, + "name": "app" + }, + { + "lineno": 13, + "name": "journal" + }, + { + "lineno": 13, + "name": "collector" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "beast::Journal journal", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 11, + "name": "PathRequestManager" + } + ], + "description": "Defines the PathRequestManager class, which manages and updates pathfinding requests in the XRPL server, handling both new and legacy path requests, asset caching, and event reporting.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/PathRequestManager.h", + "functions": [ + { + "args": [ + "Application& app", + "beast::Journal journal", + "beast::insight::Collector::ptr const& collector" + ], + "lineno": 12, + "name": "PathRequestManager" + }, + { + "args": [ + "std::shared_ptr const& ledger" + ], + "lineno": 25, + "name": "updateAll" + }, + { + "args": [], + "lineno": 29, + "name": "requestsPending" + }, + { + "args": [ + "std::shared_ptr const& ledger", + "bool authoritative" + ], + "lineno": 32, + "name": "getAssetCache" + }, + { + "args": [ + "std::shared_ptr const& subscriber", + "std::shared_ptr const& ledger", + "Json::Value const& request" + ], + "lineno": 38, + "name": "makePathRequest" + }, + { + "args": [ + "PathRequest::pointer& req", + "std::function completion", + "Resource::Consumer& consumer", + "std::shared_ptr const& inLedger", + "Json::Value const& request" + ], + "lineno": 45, + "name": "makeLegacyPathRequest" + }, + { + "args": [ + "Resource::Consumer& consumer", + "std::shared_ptr const& inLedger", + "Json::Value const& request" + ], + "lineno": 53, + "name": "doLegacyPathRequest" + }, + { + "args": [ + "std::chrono::milliseconds ms" + ], + "lineno": 60, + "name": "reportFast" + }, + { + "args": [ + "std::chrono::milliseconds ms" + ], + "lineno": 65, + "name": "reportFull" + }, + { + "args": [ + "PathRequest::pointer const&" + ], + "lineno": 71, + "name": "insertPathRequest" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathRequestManager.h.ai.md b/src/xrpld/rpc/detail/PathRequestManager.h.ai.md new file mode 100644 index 0000000000..f161f5584f --- /dev/null +++ b/src/xrpld/rpc/detail/PathRequestManager.h.ai.md @@ -0,0 +1,49 @@ +# `PathRequestManager` — Pathfinding Request Lifecycle Hub + +## Role and Context + +`PathRequestManager` is the single coordination point for all active pathfinding requests in the XRPL server. When a client wants to find a payment path across the trust-line graph, the request flows through this class. It holds the collection of live `PathRequest` objects, drives their periodic updates as new ledgers close, manages the shared `AssetCache` used during graph traversal, and surfaces timing telemetry to the metrics system. + +The class lives in `Application` and is invoked from two directions: RPC handlers that create requests, and `LedgerMaster` which calls `updateAll()` on a dedicated pathfinding thread whenever a new ledger is ready. + +## Two Request Flavors + +The class exposes three factory methods that reflect two distinct client-facing RPCs with meaningfully different lifecycles. + +`makePathRequest()` serves the `path_find` WebSocket command. The client subscribes and expects an ongoing stream of path updates as the ledger evolves. The created `PathRequest` is stored in the subscriber's `InfoSub` slot (`subscriber->setRequest(req)`) and in the manager's weak-pointer list. Ownership lives with the subscriber; the manager only holds a `wptr`. When the subscriber disconnects, the `shared_ptr` is released, the weak pointer expires, and the manager silently drops it on the next `updateAll()` pass. + +`makeLegacyPathRequest()` serves `ripple_path_find` via a coroutine. The caller receives a `PathRequest::pointer` (out-parameter) and provides a completion callback that is invoked once the path engine finishes its first pass. The manager inserts the request into the list and signals `LedgerMaster::newPathRequest()` to schedule a pathfinding job. If the job queue is too busy (`newPathRequest()` returns false), the request is torn down immediately and the caller receives `rpcTOO_BUSY`. + +`doLegacyPathRequest()` is the synchronous one-shot variant, designed for cases where the caller supplies its own ledger and wants an immediate answer without queuing. It creates a fresh `AssetCache` directly, runs `doCreate()` then `doUpdate()` inline, and returns the result. This path bypasses the manager's request list entirely — the `PathRequest` is constructed, used, and discarded in one call. + +## Weak-Pointer Collection and Priority Ordering + +The internal `requests_` vector holds `PathRequest::wptr` entries — weak pointers, not shared pointers. This is a deliberate ownership design: the manager does not keep requests alive. If a subscriber disconnects or a coroutine completes, the `shared_ptr` held by the subscriber/coroutine drops to zero, the weak pointer in the list becomes stale, and the manager cleans it up during the next update sweep. This prevents the manager from silently prolonging the lifetime of defunct request objects. + +Insertion order matters. `insertPathRequest()` scans forward for the first already-serviced request (one where `isNew()` returns false) and inserts the new request before it. This ensures freshly-created requests are processed first in the upcoming `updateAll()` pass rather than queuing behind older requests that have already received at least one reply. + +## `updateAll()` — The Batch Update Loop + +This is the most complex method. `LedgerMaster` calls it on each pathfinding thread cycle, passing the current closed ledger. The logic handles a subtle race: new requests can arrive while the loop is running. + +The method takes a snapshot of the request list and asset cache under the lock, then iterates without holding the lock (so individual `PathRequest::doUpdate()` calls — which may be expensive graph searches — don't block request insertion). For each live request it checks: + +- If the subscriber is still connected and its `InfoSub` still references this request (guarding against a new `path_find` superseding the previous one on the same connection). +- Whether the request `needsUpdate()` for the current ledger index. +- Whether the subscriber's resource consumption warrants a warning (in which case the update is skipped but the request is kept). + +After a successful update, the result is tagged with `"type": "path_find"` and sent to the subscriber. If instead the request has a completion function (legacy mode), `doUpdate()` runs and the completion is fired internally. + +The outer `do`/`while` loop handles new-request preemption. If `LedgerMaster::isNewPathRequest()` transitions from false to true mid-loop, `mustBreak` is set, the current pass is abandoned, and the loop restarts with `newRequests = true`. This ensures a newly arrived subscription doesn't wait through the full backlog of existing requests before getting its first path result. + +## `AssetCache` Lifetime Strategy + +`AssetCache` wraps a ledger snapshot and caches the trust-line and MPT data fetched during pathfinding. It is potentially large, and is shared across all requests in a single update pass. + +The manager holds it as a `std::weak_ptr`. A strong reference is returned from `getAssetCache()`, which promotes the weak pointer. The cache is rebuilt when the ledger sequence changes beyond a threshold (more than 8 ledgers ahead or behind). The comment in the implementation is worth noting: the local `shared_ptr` variable is assigned before the member `weak_ptr` to ensure there is at least one strong reference alive before the weak pointer is set — assigning only the `weak_ptr` would cause immediate expiry. + +## Concurrency and Telemetry + +The `mLock` member is a `std::recursive_mutex`, needed because `updateAll()` calls `getAssetCache()` while holding the lock, and `getAssetCache()` also acquires it internally. The `mLastIdentifier` counter is `std::atomic`, guaranteeing unique per-request IDs without entering the main lock. + +`mFast` and `mFull` are `beast::insight::Event` handles. Individual `PathRequest` objects call back through `reportFast()` and `reportFull()` to record how long their quick-reply and full-reply passes took, feeding the server's telemetry collector. This indirection keeps timing instrumentation in the manager rather than having `PathRequest` depend on the collector directly. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp.ai.json b/src/xrpld/rpc/detail/Pathfinder.cpp.ai.json new file mode 100644 index 0000000000..058c79bdc7 --- /dev/null +++ b/src/xrpld/rpc/detail/Pathfinder.cpp.ai.json @@ -0,0 +1,381 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 18, + "name": "AccountCandidate" + }, + { + "args": [], + "lineno": 44, + "name": "CostedPath" + }, + { + "args": [], + "lineno": 50, + "name": "PathCost" + } + ], + "code_paths": [ + { + "call_chain": [ + "findPaths", + "getPathLiquidity", + "computePathRanks", + "amountFromPathAsset", + "assetFromPathAsset" + ], + "entry_point": "findPaths", + "purpose": "Main pathfinding entry point. Accepts payment pathfinding requests, validates input, computes possible paths, ranks them, and estimates liquidity.", + "validation_points": [ + "findPaths (input validation: source/destination accounts, amounts, currencies)", + "getPathLiquidity (validates path liquidity and drops trivial/no-liquidity paths)", + "amountFromPathAsset (validates asset/amount conversion)", + "assetFromPathAsset (validates asset structure)" + ] + }, + { + "call_chain": [ + "compareAccountCandidate" + ], + "entry_point": "compareAccountCandidate", + "purpose": "Sorts account candidates for pathfinding based on priority and account ID.", + "validation_points": [ + "compareAccountCandidate (validates sort order, but not external input)" + ] + }, + { + "call_chain": [ + "pathTypeToString" + ], + "entry_point": "pathTypeToString", + "purpose": "Converts internal path type enum to string for logging/debugging.", + "validation_points": [ + "pathTypeToString (validates enum values, but not external input)" + ] + } + ], + "data_flows": [ + { + "field": "Pathfinder::PathType", + "flow": [ + "findPaths (input parsing)", + "pathTypeToString (for logging/debugging)", + "CostedPath (used for ranking and sorting paths)", + "computePathRanks (used for ranking)" + ], + "origin": "Constructed in findPaths based on input payment request", + "transformations": [ + "Parsed from input, mapped to enum, converted to string, used for sorting" + ], + "validated_at": "findPaths (input parsing and enum mapping)" + }, + { + "field": "AccountID", + "flow": [ + "findPaths (input parsing)", + "AccountCandidate (wrapped for sorting)", + "compareAccountCandidate (used for sorting)" + ], + "origin": "Input from payment request (source/destination)", + "transformations": [ + "Parsed from input, wrapped in AccountCandidate, sorted" + ], + "validated_at": "findPaths (input validation: checks for valid account IDs)" + }, + { + "field": "Amount", + "flow": [ + "findPaths (input parsing)", + "amountFromPathAsset (conversion/validation)", + "getPathLiquidity (liquidity estimation)", + "computePathRanks (ranking based on liquidity)" + ], + "origin": "Input from payment request", + "transformations": [ + "Parsed from input, converted to internal representation, validated, used for calculations" + ], + "validated_at": "amountFromPathAsset (validates amount structure and value)" + } + ], + "description": "Implements the core pathfinding engine for the XRPL, providing algorithms to find, rank, and select payment paths between accounts and assets, considering liquidity, quality, and path constraints.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/Pathfinder.cpp", + "functions": [ + { + "args": [ + "seq", + "first", + "second" + ], + "lineno": 27, + "name": "compareAccountCandidate" + }, + { + "args": [ + "type" + ], + "lineno": 54, + "name": "pathTypeToString" + }, + { + "args": [ + "amount", + "maxPaths" + ], + "lineno": 77, + "name": "smallestUsefulAmount" + }, + { + "args": [ + "pathAsset", + "srcIssuer", + "srcAccount" + ], + "lineno": 82, + "name": "amountFromPathAsset" + }, + { + "args": [ + "pathAsset", + "account" + ], + "lineno": 91, + "name": "assetFromPathAsset" + }, + { + "args": [ + "searchLevel", + "continueCallback" + ], + "lineno": 120, + "name": "findPaths" + }, + { + "args": [ + "path", + "minDstAmount", + "amountOut", + "qualityOut" + ], + "lineno": 232, + "name": "getPathLiquidity" + }, + { + "args": [ + "maxPaths", + "continueCallback" + ], + "lineno": 282, + "name": "computePathRanks" + }, + { + "args": [ + "path" + ], + "lineno": 312, + "name": "isDefaultPath" + }, + { + "args": [ + "path" + ], + "lineno": 326, + "name": "removeIssuer" + }, + { + "args": [ + "maxPaths", + "paths", + "rankedPaths", + "continueCallback" + ], + "lineno": 338, + "name": "rankPaths" + }, + { + "args": [ + "maxPaths", + "fullLiquidityPath", + "extraPaths", + "srcIssuer", + "continueCallback" + ], + "lineno": 387, + "name": "getBestPaths" + }, + { + "args": [ + "asset" + ], + "lineno": 470, + "name": "issueMatchesOrigin" + }, + { + "args": [ + "pathAsset", + "account", + "direction", + "isDstAsset", + "dstAccount", + "continueCallback" + ], + "lineno": 478, + "name": "getPathsOut" + }, + { + "args": [ + "currentPaths", + "incompletePaths", + "addFlags", + "continueCallback" + ], + "lineno": 547, + "name": "addLinks" + }, + { + "args": [ + "pathType", + "continueCallback" + ], + "lineno": 557, + "name": "addPathsForType" + }, + { + "args": [ + "fromAccount", + "toAccount", + "currency" + ], + "lineno": 627, + "name": "isNoRipple" + }, + { + "args": [ + "currentPath" + ], + "lineno": 635, + "name": "isNoRippleOut" + }, + { + "args": [ + "pathSet", + "path" + ], + "lineno": 654, + "name": "addUniquePath" + }, + { + "args": [ + "currentPath", + "incompletePaths", + "addFlags", + "continueCallback" + ], + "lineno": 662, + "name": "addLink" + }, + { + "args": [ + "string" + ], + "lineno": 1002, + "name": "makePath" + }, + { + "args": [ + "type", + "costs" + ], + "lineno": 1027, + "name": "fillPaths" + }, + { + "args": [], + "lineno": 1047, + "name": "initPathTable" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 112, + "name": "xrpl" + }, + { + "lineno": 13, + "name": "" + } + ], + "test_coverage_notes": "Pathfinder is a core component and is likely tested via high-level integration and unit tests. Typical test files would be in test/rpc/Pathfinder_test.cpp or similar, and possibly in test/paths/ or test/app/paths/. Tests likely cover valid/invalid path requests, edge cases (no liquidity, trivial paths), and sorting/ranking logic. Gaps may exist in deep validation of malformed input, rare path types, or error handling for unexpected enum values. Exception handling paths may not be fully covered.", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Pathfinder.cpp.ai.md b/src/xrpld/rpc/detail/Pathfinder.cpp.ai.md new file mode 100644 index 0000000000..7a782ed997 --- /dev/null +++ b/src/xrpld/rpc/detail/Pathfinder.cpp.ai.md @@ -0,0 +1,70 @@ +# `Pathfinder.cpp` — Core Payment Path Discovery Engine + +`Pathfinder.cpp` implements the complete payment path search algorithm for the XRP Ledger. When a client calls `ripple_path_find` or the server wants to compute a payment path, this file's logic is what drives the discovery, pruning, ranking, and selection of viable multi-hop routes between two accounts across potentially many currencies or MPT assets. + +## Architecture at a Glance + +The `Pathfinder` class is constructed with a source account, destination account, source and destination assets, and an `AssetCache` (a memoized wrapper around the current ledger's trust lines and MPT holdings). It then drives a three-phase pipeline: + +1. **`findPaths()`** — graph traversal that enumerates candidate complete paths +2. **`computePathRanks()`** — liquidity simulation for each candidate path +3. **`getBestPaths()`** — selection of the top-N paths that together cover the required payment + +These three phases correspond to the comment at the top of the file and are also reflected in the call-graph comment inside the header. + +## The Path Table: Encoding Domain Knowledge as a Static Table + +The most distinctive design decision in this file is the static `mPathTable`, a `std::map` initialized once at startup by `initPathTable()`. Rather than writing ad-hoc graph search logic for each of the five payment categories (`pt_XRP_to_XRP`, `pt_XRP_to_nonXRP`, `pt_nonXRP_to_XRP`, `pt_nonXRP_to_same`, `pt_nonXRP_to_nonXRP`), the developer encoded the topology of useful payment routes as short strings: + +``` +"sfd" → source → book → destination +"sfad" → source → book → account → destination +"saxfd" → source → account → XRP book → book → destination +``` + +Each character maps to a `NodeType` enum value via `makePath()`. The `'s'` (source), `'a'` (accounts/trust-line hops), `'b'` (all order books), `'x'` (book-to-XRP only), `'f'` (book to destination currency), and `'d'` (destination account) encode the full space of useful route shapes. Each entry carries a `searchLevel` cost (0–10) so that `findPaths()` can limit the search depth to faster, cheaper paths at lower levels and expand to more aggressive paths at higher levels without re-running the entire traversal. + +This approach means path shape knowledge is declarative and auditable without reading algorithmic code. + +## Recursive Memoized Graph Traversal + +`addPathsForType()` drives traversal with an elegant recursive memoization pattern. Given a `PathType` like `{nt_SOURCE, nt_ACCOUNTS, nt_BOOKS, nt_DESTINATION}`, it first strips the last node type to get the parent path type, recurses to get all partial paths of that parent type (already memoized in `mPaths`), then extends those partial paths by one hop via `addLinks()`. Completed paths land in `mCompletePaths`; incomplete ones are stored in `mPaths[type]` for subsequent type extensions that share the same prefix. + +This means the engine never re-expands the same prefix twice, even when multiple entries in the path table share a common prefix like `"sf"`. The memoization is keyed by `PathType` (a `std::vector`), and the map is populated lazily on demand. + +The search is bounded by `PATHFINDER_MAX_COMPLETE_PATHS = 1000`, a hard cutoff that prevents combinatorial explosion on large well-connected ledger states. + +## Account Candidate Scoring and Fan-Out Control + +Inside `addLink()`, when extending a path through an account hop, the engine enumerates all trust-line peers or MPT holders at the path's current endpoint and scores each as an `AccountCandidate`. Accounts that connect directly to the destination get a `highPriority` value of 10,000. Other accounts are scored by `getPathsOut()`, which counts viable outgoing paths (order book entries plus non-frozen, non-noRipple trust lines) with a 10,000 bonus if the peer is the destination. Results are memoized in `mPathsOutCountMap`. + +The fan-out is then hard-capped: at most 10 candidates when branching away from the source, or 50 when branching from the source account itself. Sorting via `compareAccountCandidate()` uses three keys: priority descending, account ID descending (deterministic), and `(priority ^ ledger_seq)` as a pseudo-random tie-breaker. The XOR with ledger sequence is subtle but deliberate: it ensures that across different ledger versions, the ordering of equally-scored candidates varies, preventing systematic starvation of any particular route. + +## noRipple Awareness + +The engine respects the `noRipple` flag on trust lines. `isNoRippleOut()` checks whether the last account node in the current partial path has set noRipple on the outgoing side. If so, the next account step must proceed via an account that has rippling enabled on the incoming side (`LineDirection::incoming`). This is implemented by choosing the correct `LineDirection` when querying `AssetCache::getRippleLines()`. Without this, the engine would generate paths that would fail at execution time. + +## Liquidity Estimation via Simulation + +`getPathLiquidity()` doesn't estimate liquidity analytically — it runs an actual `RippleCalc::rippleCalculate` against a `PaymentSandbox` (which wraps the ledger read-only). This is expensive but correct. A two-pass strategy is used: the first pass checks whether the path can deliver at least the minimum useful amount (`dstAmount / (maxPaths + 2)`); if it fails, the path is dropped entirely. If it succeeds, a second partial-payment pass checks for additional liquidity above that minimum and accumulates it. + +For "convert all" payments (where the destination amount is the ledger maximum, meaning "send as much as possible"), `partialPaymentAllowed` is set from the first pass. + +## Default Path Accounting in `computePathRanks()` + +`computePathRanks()` begins by computing how much the default path (no explicit intermediate hops) can deliver, by calling `RippleCalc` with an empty `STPathSet`. This contribution is subtracted from `mRemainingAmount` so that the explicitly discovered paths only need to cover the residual. This correctly handles the common case where direct trust-line paths already satisfy a large fraction of the payment. + +## Path Selection in `getBestPaths()` + +`getBestPaths()` merges two sorted-by-quality iterators — `mPathRanks` (from discovered paths) and `extraPathRanks` (re-ranked from client-injected `extraPaths`) — in a single linear pass. The selection rule is: fill up to `maxPaths` slots in quality order, but the last slot must contribute enough liquidity to cover the remaining amount (ensuring the full payment can succeed if there's no liquidity overlap). If no regular path covers the full amount independently, the function also searches for a `fullLiquidityPath` — a single path with capacity ≥ the entire payment that can be used as a fallback. + +## MPT Dual-Path Handling + +Throughout `addLink()` and `getPathsOut()`, both IOU trust lines and MPTokens are handled via the `PathAsset::visit()` visitor pattern. The key difference: IOU trust lines are bidirectional (either peer can be the outgoing node), while MPTs are not — they always use `LineDirection::incoming` because MPT rippling semantics differ. The code uses a C++20 `if constexpr` lambda template (`forAssets`) to share the candidate collection logic while differentiating asset-type behavior at compile time. + +## Relationship to Surrounding Files + +- **`Pathfinder.h`** declares the class interface, `PathType`, `PaymentType`, `PathRank`, and the flag constants (`afADD_ACCOUNTS`, `afADD_BOOKS`, etc.). +- **`AssetCache.h`** provides a mutex-protected, memoized view of trust lines and MPT holdings for a specific ledger, avoiding redundant ledger reads during traversal. +- **`PathfinderUtils.h`** provides `largestAmount()`, `convertAmount()`, and `convertAllCheck()` used to detect and handle "convert all" payment semantics. +- **`RippleCalc`** (via `xrpl/tx/paths/RippleCalc.h`) is the actual payment execution engine used by `getPathLiquidity()` for liquidity probing. The `PaymentSandbox` ensures these probes don't mutate ledger state. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Pathfinder.h.ai.json b/src/xrpld/rpc/detail/Pathfinder.h.ai.json new file mode 100644 index 0000000000..702cdb1b3e --- /dev/null +++ b/src/xrpld/rpc/detail/Pathfinder.h.ai.json @@ -0,0 +1,198 @@ +{ + "args": [ + { + "lineno": 19, + "name": "cache" + }, + { + "lineno": 20, + "name": "srcAccount" + }, + { + "lineno": 21, + "name": "dstAccount" + }, + { + "lineno": 22, + "name": "uSrcPathAsset" + }, + { + "lineno": 23, + "name": "uSrcIssuer" + }, + { + "lineno": 24, + "name": "dstAmount" + }, + { + "lineno": 25, + "name": "srcAmount" + }, + { + "lineno": 26, + "name": "domain" + }, + { + "lineno": 27, + "name": "app" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const& cache", + "AccountID const& srcAccount", + "AccountID const& dstAccount", + "PathAsset const& uSrcPathAsset", + "std::optional const& uSrcIssuer", + "STAmount const& dstAmount", + "std::optional const& srcAmount", + "std::optional const& domain", + "Application& app" + ], + "lineno": 15, + "name": "Pathfinder" + } + ], + "description": "Defines the Pathfinder class, which calculates payment paths in the XRPL ledger, including path ranking and liquidity analysis for payments.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/Pathfinder.h", + "functions": [ + { + "args": [ + "std::shared_ptr const& cache", + "AccountID const& srcAccount", + "AccountID const& dstAccount", + "PathAsset const& uSrcPathAsset", + "std::optional const& uSrcIssuer", + "STAmount const& dstAmount", + "std::optional const& srcAmount", + "std::optional const& domain", + "Application& app" + ], + "lineno": 18, + "name": "Pathfinder" + }, + { + "args": [], + "lineno": 32, + "name": "initPathTable" + }, + { + "args": [ + "int searchLevel", + "std::function const& continueCallback" + ], + "lineno": 34, + "name": "findPaths" + }, + { + "args": [ + "int maxPaths", + "std::function const& continueCallback" + ], + "lineno": 38, + "name": "computePathRanks" + }, + { + "args": [ + "int maxPaths", + "STPath& fullLiquidityPath", + "STPathSet const& extraPaths", + "AccountID const& srcIssuer", + "std::function const& continueCallback" + ], + "lineno": 44, + "name": "getBestPaths" + }, + { + "args": [ + "PathType const& type", + "std::function const& continueCallback" + ], + "lineno": 87, + "name": "addPathsForType" + }, + { + "args": [ + "Asset const&" + ], + "lineno": 90, + "name": "issueMatchesOrigin" + }, + { + "args": [ + "PathAsset const& pathAsset", + "AccountID const& account", + "LineDirection direction", + "bool isDestPathAsset", + "AccountID const& dest", + "std::function const& continueCallback" + ], + "lineno": 93, + "name": "getPathsOut" + }, + { + "args": [ + "STPath const& currentPath", + "STPathSet& incompletePaths", + "int addFlags", + "std::function const& continueCallback" + ], + "lineno": 100, + "name": "addLink" + }, + { + "args": [ + "STPathSet const& currentPaths", + "STPathSet& incompletePaths", + "int addFlags", + "std::function const& continueCallback" + ], + "lineno": 107, + "name": "addLinks" + }, + { + "args": [ + "STPath const& path", + "STAmount const& minDstAmount", + "STAmount& amountOut", + "uint64_t& qualityOut" + ], + "lineno": 113, + "name": "getPathLiquidity" + }, + { + "args": [ + "STPath const& currentPath" + ], + "lineno": 120, + "name": "isNoRippleOut" + }, + { + "args": [ + "AccountID const& fromAccount", + "AccountID const& toAccount", + "Currency const& currency" + ], + "lineno": 124, + "name": "isNoRipple" + }, + { + "args": [ + "int maxPaths", + "STPathSet const& paths", + "std::vector& rankedPaths", + "std::function const& continueCallback" + ], + "lineno": 128, + "name": "rankPaths" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Pathfinder.h.ai.md b/src/xrpld/rpc/detail/Pathfinder.h.ai.md new file mode 100644 index 0000000000..d40a1b65d4 --- /dev/null +++ b/src/xrpld/rpc/detail/Pathfinder.h.ai.md @@ -0,0 +1,75 @@ +# `Pathfinder.h` — Payment Path Discovery Engine + +## Role in the System + +`Pathfinder` is the core engine responsible for discovering viable multi-hop payment paths across the XRP Ledger. When an account wants to send a payment in one currency and have the recipient receive a different currency — or route funds through intermediary accounts and order books — the pathfinder searches the ledger's trust-line graph and order-book structure to enumerate candidate routes. Its output is an `STPathSet` consumed by `RippleCalc` (the actual payment simulation engine), which then determines exact amounts and exchange rates. + +The class lives under `rpc/detail/` because pathfinding is a service exposed to API callers (the `path_find` and `ripple_path_find` RPC commands), not to transaction processing itself. Transaction processing in consensus uses only the paths the client already submitted. + +## The Three-Phase Pipeline + +`Pathfinder` presents a deliberate three-step API that callers must invoke in sequence: + +1. **`findPaths(searchLevel)`** — Enumerate candidate paths by template expansion, populating `mCompletePaths`. +2. **`computePathRanks(maxPaths)`** — Score each candidate via a simulated payment using `RippleCalc`, building `mPathRanks`. +3. **`getBestPaths(maxPaths, fullLiquidityPath, extraPaths, srcIssuer)`** — Merge ranked internal paths with any caller-supplied `extraPaths` and return the final `STPathSet`. + +Separating enumeration from ranking is a deliberate performance decision: discovery is graph traversal (cheap), while ranking requires calling `RippleCalc` on a `PaymentSandbox` for every candidate (expensive). The split lets callers control how many ranked paths they want and allows the streaming `path_find` API to inject previously-found `extraPaths` across repeated invocations without re-running the full graph search. + +## Path Templates and the Payment Type Table + +The key design insight is that the topology of useful payment paths is small and domain-specific. Rather than a generic BFS, `findPaths()` works from a static lookup table (`mPathTable`) that maps each `PaymentType` to an ordered list of `PathType` templates at various search costs. + +A `PathType` is a sequence of `NodeType` tokens — for example, `{nt_SOURCE, nt_BOOKS, nt_ACCOUNTS, nt_DESTINATION}`, represented compactly as the string `"sbad"`. `initPathTable()` seeds this table once at startup with domain-encoded patterns covering each of the five `PaymentType` categories: + +- `pt_XRP_to_XRP` — no cross-currency routing needed; the table is empty because only direct paths apply. +- `pt_XRP_to_nonXRP` — patterns like `"sfd"` (source → book → gateway), `"sfad"`, `"sfaad"`. +- `pt_nonXRP_to_XRP` — patterns like `"sxd"` (source sells token, buys XRP via book). +- `pt_nonXRP_to_same` — same-currency routing through gateways or books. +- `pt_nonXRP_to_nonXRP` — cross-currency paths, the most complex category. + +Each template has an integer `searchLevel` cost (0 = trivially cheap, 10 = most aggressive). `findPaths()` only expands templates whose cost is ≤ the caller-supplied `searchLevel`, giving a natural depth-budget knob. Streaming path-find responses can start at level 1 for a fast first reply and increment toward 10 for subsequent responses. + +## Graph Expansion: `addLink` and `addLinks` + +Template expansion proceeds node-by-node using `addLink()` / `addLinks()`. Internally, `addLink()` takes a partially-built `STPath`, examines the last node's type, and appends all valid next-hop candidates according to the `addFlags` bitmask: + +- `afADD_ACCOUNTS` — extend the path through trust-line counterparties obtained from `AssetCache::getRippleLines()` or `getMPTs()`. +- `afADD_BOOKS` — extend through order books from `OrderBookDB`. +- `afOB_XRP` — only the XRP-denominated book. +- `afOB_LAST` — the book must lead to the destination currency (used for the `nt_DEST_BOOK` node). +- `afAC_LAST` — must terminate at the destination account. + +When a path becomes complete (reaches the destination), it is appended to `mCompletePaths`. The search caps at `PATHFINDER_MAX_COMPLETE_PATHS = 1000` to prevent unbounded memory growth. + +The `isNoRippleOut()` / `isNoRipple()` helpers prune dead-end branches early: a path through an account that has set the `lsfNoRipple` flag on its outgoing trust line cannot carry funds, so that branch is discarded without ever probing further. + +`mPathsOutCountMap` (keyed by `Asset`) tracks how many paths have been discovered branching out from each asset type. This short-circuits redundant expansion of the same order book or trust-line cluster when the count already exceeds a useful threshold. + +## Ranking and Liquidity Measurement + +`getPathLiquidity()` measures how much value a candidate path can actually deliver. It wraps `RippleCalc::rippleCalculate()` in a `PaymentSandbox` (a copy-on-write snapshot of ledger state) so no actual changes occur. For non-`convert_all_` paths it runs the calculation twice: once to verify the path meets a minimum threshold (`smallestUsefulAmount`), and again with `partialPaymentAllowed = true` to capture the full available liquidity. This two-pass approach avoids discarding paths that can't single-handedly complete a payment but could contribute useful liquidity when combined with others. + +`rankPaths()` scores all survivors with a three-key comparator: better exchange rate (lower quality number, i.e. less input per output) first; then higher liquidity; then shorter path. Shorter paths are preferred at equal quality because they carry less counterparty risk and succeed more reliably against the actual ledger state. + +`computePathRanks()` first executes the default (empty) path to measure how much liquidity it contributes, then subtracts that from `mRemainingAmount`. Subsequent paths are only required to cover the remainder, which avoids over-counting when the default path already satisfies part of the payment. + +## `getBestPaths` and the Covering Path + +After ranking, `getBestPaths()` performs a merge-sort between the newly ranked `mPathRanks` and any caller-supplied `extraPathRanks` (re-ranked from `extraPaths`), filling the output `STPathSet` up to `maxPaths`. A notable edge case: the last slot requires a path whose liquidity meets or exceeds `remaining`, ensuring the selected paths are collectively sufficient to complete the payment assuming no liquidity overlap. + +If no single selected path can cover the full remaining amount on its own, the method fills `fullLiquidityPath` with the best such "covering" path found in the tail of the ranked list. The caller can then attempt the payment with and without this extra path, falling back if needed. + +## Cooperative Cancellation and Load Tracking + +Every public and private method accepts an optional `std::function continueCallback`. Callers can supply a predicate that returns `false` when the search should abort (e.g., when an RPC connection closes). This cooperative model avoids preemptive cancellation complexity while keeping latency bounded. + +The constructor registers a `LoadEvent` via `app_.getJobQueue().makeLoadEvent(jtPATH_FIND, ...)` to track CPU time in the job queue's load-balancing system. Pathfinding is one of the most expensive RPC operations, and this accounting prevents it from starving other work on a busy server. + +## `AssetCache` and the Shared Ledger View + +Rather than reading trust lines directly from the ledger on every `addLink()` call, `Pathfinder` works through a shared `AssetCache`. The cache wraps a single frozen `ReadView const` snapshot and memoizes `getRippleLines()` results keyed by `(AccountID, LineDirection)`. This is critical for performance: the same account may appear on many partial paths, and re-reading its trust-line objects from the ledger on each encounter would be prohibitively expensive. The cache is owned by the caller (e.g., the streaming path-find session) and shared across multiple `Pathfinder` invocations, so warm entries persist for the lifetime of an interactive session. + +## Domain-Scoped Pathfinding + +The optional `domain` parameter (`std::optional`) threads through to `RippleCalc` to restrict path simulation to a specific permissioned domain, supporting the XRPL's domain-scoped payment channels feature. Paths that violate domain constraints will be pruned at the liquidity-measurement stage rather than during enumeration. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathfinderUtils.h.ai.json b/src/xrpld/rpc/detail/PathfinderUtils.h.ai.json new file mode 100644 index 0000000000..9929274c30 --- /dev/null +++ b/src/xrpld/rpc/detail/PathfinderUtils.h.ai.json @@ -0,0 +1,54 @@ +{ + "args": [ + { + "lineno": 8, + "name": "amt" + }, + { + "lineno": 10, + "name": "issue" + }, + { + "lineno": 21, + "name": "all" + }, + { + "lineno": 30, + "name": "a" + } + ], + "classes": [], + "description": "Provides utility functions for handling and converting STAmount values, including determining the largest possible amount for a given asset and checking if an amount represents 'all' of an asset.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/PathfinderUtils.h", + "functions": [ + { + "args": [ + "amt" + ], + "lineno": 7, + "name": "largestAmount" + }, + { + "args": [ + "amt", + "all" + ], + "lineno": 20, + "name": "convertAmount" + }, + { + "args": [ + "a" + ], + "lineno": 29, + "name": "convertAllCheck" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/PathfinderUtils.h.ai.md b/src/xrpld/rpc/detail/PathfinderUtils.h.ai.md new file mode 100644 index 0000000000..deedc11c99 --- /dev/null +++ b/src/xrpld/rpc/detail/PathfinderUtils.h.ai.md @@ -0,0 +1,33 @@ +# PathfinderUtils.h + +This header provides three small inline utilities that implement the "send/receive all available liquidity" semantic for the XRPL pathfinding subsystem. It lives in `src/xrpld/rpc/detail/` alongside `Pathfinder.cpp` and `PathRequest.cpp`, both of which include it directly. + +## The "Convert All" Sentinel Pattern + +The XRPL path-finding RPC supports a mode where a sender wants to drain the maximum available liquidity through discovered paths rather than satisfy a fixed destination amount. This is signaled at the RPC layer by passing a destination amount equal to the largest representable value for the given asset type — a sentinel value rather than a separate flag or `std::optional`. + +`largestAmount()` constructs this sentinel. It dispatches on the asset type via the variant visitor pattern on `STAmount::asset()`: + +- **XRP**: returns `INITIAL_XRP` (100,000,000,000 XRP = 10^17 drops, the entire initial supply — the largest valid XRP amount by protocol convention). +- **IOU**: returns an `STAmount` with mantissa `cMaxValue` (9,999,999,999,999,999) and exponent `cMaxOffset` (80). These are the boundary values of XRPL's floating-point IOU encoding, yielding approximately 10^96 — effectively unbounded for any realistic liquidity. +- **MPT (Multi-Purpose Token)**: returns an `STAmount` with mantissa `maxMPTokenAmount` (0x7FFF'FFFF'FFFF'FFFF = 2^63−1) and exponent 0. MPTs use integer rather than floating-point encoding, so `cMaxValue`/`cMaxOffset` are inapplicable; the maximum is the largest signed 63-bit integer the protocol permits. + +The three-branch design reflects that XRPL now has three fundamentally different amount representations (native drops, IOU floating-point, MPT integer), each with its own maximum. Using a single `largestAmount` abstraction prevents callers from having to know which representation applies. + +## `convertAmount` and `convertAllCheck` + +`convertAmount(amt, all)` is the entry point for consumer code: when `all` is `false`, the original amount passes through unchanged; when `true`, it delegates to `largestAmount`. This is the function called by `PathRequest::doUpdate()` and `Pathfinder::computePathRanks()` when computing the destination amount to feed into `RippleCalc`. + +`convertAllCheck(a)` is the inverse detection function. It compares an amount against `largestAmount(a)` to determine whether it was already the sentinel, returning `true` if so. In `Pathfinder`'s constructor, `convert_all_` is initialized as: + +```cpp +convert_all_(convertAllCheck(mDstAmount)) +``` + +This boolean then controls two downstream behaviours: `convertAmount` selects either the real or sentinel amount, and — critically — `partialPaymentAllowed` is set to `true` in the `RippleCalc` input when `convert_all_` is active. Requiring a partial payment is what makes the pathfinder search for maximum liquidity rather than insisting on an exact fill. During path ranking, `largestAmount` is used again as the minimum acceptable destination amount, which biases selection toward high-liquidity paths. + +## Design Tradeoffs + +The sentinel-value approach conflates the amount field with a semantic flag, which is non-obvious. The upside is seamless compatibility with `STAmount`-typed interfaces throughout the pathfinding stack — no overloads, no optionals, no protocol changes needed. The downside is that a legitimate request for the exact maximum representable amount is indistinguishable from the "convert all" intent; in practice this is not a problem because no real payment would ever specify `INITIAL_XRP` or IOU `cMaxValue` as an exact destination. + +The functions are all `inline` and header-only, appropriate for their trivial size and the fact that they are called from only two translation units in the same directory. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCCall.cpp.ai.json b/src/xrpld/rpc/detail/RPCCall.cpp.ai.json new file mode 100644 index 0000000000..c7b1c69291 --- /dev/null +++ b/src/xrpld/rpc/detail/RPCCall.cpp.ai.json @@ -0,0 +1,588 @@ +{ + "args": [ + { + "lineno": 15, + "name": "strHost" + }, + { + "lineno": 15, + "name": "strPath" + }, + { + "lineno": 15, + "name": "strMsg" + }, + { + "lineno": 15, + "name": "mapRequestHeaders" + }, + { + "lineno": 38, + "name": "apiVersion" + }, + { + "lineno": 38, + "name": "j" + }, + { + "lineno": 671, + "name": "strMethod" + }, + { + "lineno": 671, + "name": "params" + }, + { + "lineno": 671, + "name": "id" + }, + { + "lineno": 765, + "name": "args" + }, + { + "lineno": 765, + "name": "retParams" + }, + { + "lineno": 765, + "name": "apiVersion" + }, + { + "lineno": 765, + "name": "j" + }, + { + "lineno": 813, + "name": "args" + }, + { + "lineno": 813, + "name": "config" + }, + { + "lineno": 813, + "name": "logs" + }, + { + "lineno": 813, + "name": "apiVersion" + }, + { + "lineno": 813, + "name": "headers" + } + ], + "classes": [ + { + "args": [ + "apiVersion", + "j" + ], + "lineno": 38, + "name": "RPCParser" + }, + { + "args": [ + "std::string" + ], + "lineno": 677, + "name": "RequestNotParsable" + }, + { + "args": [], + "lineno": 683, + "name": "RPCCallImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "rpcClient", + "JSONRPCRequest", + "rpcCmdToJson", + "RPCParser::jvParseLedger / RPCParser::jvParseCurrencyIssuer", + "createHTTPPost" + ], + "entry_point": "rpcClient", + "purpose": "Handles an RPC client request, parses and validates input, constructs JSON-RPC message, and builds HTTP POST for transmission.", + "validation_points": [ + "RPCParser::jvParseLedger (validates strLedger)", + "RPCParser::jvParseCurrencyIssuer (validates strCurrencyIssuer)", + "createHTTPPost (validates strMsg and mapRequestHeaders implicitly)" + ] + }, + { + "call_chain": [ + "createHTTPPost" + ], + "entry_point": "createHTTPPost", + "purpose": "Formats an HTTP POST request with headers and body, including validation of message length and header formatting.", + "validation_points": [ + "createHTTPPost (validates strMsg size, iterates mapRequestHeaders for header format)" + ] + } + ], + "data_flows": [ + { + "field": "strLedger", + "flow": [ + "User input", + "rpcClient", + "rpcCmdToJson", + "RPCParser::jvParseLedger", + "jvRequest (JSON object)" + ], + "origin": "User/client input (e.g., command line, API call)", + "transformations": [ + "Checked for special values ('current', 'closed', 'validated')", + "If 64 chars, treated as hash", + "Otherwise, cast to uint32_t" + ], + "validated_at": "RPCParser::jvParseLedger" + }, + { + "field": "strCurrencyIssuer", + "flow": [ + "User input", + "rpcClient", + "rpcCmdToJson", + "RPCParser::jvParseCurrencyIssuer", + "jvRequest (JSON object)" + ], + "origin": "User/client input (e.g., command line, API call)", + "transformations": [ + "Regex match for currency/issuer format", + "Splits into currency and issuer fields" + ], + "validated_at": "RPCParser::jvParseCurrencyIssuer" + }, + { + "field": "strMsg", + "flow": [ + "rpcCmdToJson", + "createHTTPPost", + "HTTP POST body" + ], + "origin": "Output of rpcCmdToJson (JSON string)", + "transformations": [ + "Used as HTTP POST body", + "Content-Length header set to strMsg.size()" + ], + "validated_at": "createHTTPPost (Content-Length, format)" + }, + { + "field": "mapRequestHeaders", + "flow": [ + "rpcClient", + "createHTTPPost", + "HTTP POST headers" + ], + "origin": "User/client input or internal construction", + "transformations": [ + "Iterated and inserted as HTTP headers" + ], + "validated_at": "createHTTPPost (header format)" + } + ], + "description": "This file implements the XRPL RPC command-line and network client logic, including parsing command-line arguments into JSON-RPC requests, handling various RPC commands, and sending/receiving requests over HTTP(S). It defines a parser for RPC commands, request/response handling, and utility functions for interacting with an XRPL server.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_index validated by xrpl::jss (JSON field validation), beast::lexicalCast (type validation), explicit C++ logic", + "issue_pattern": "Missing validation for ledger_index", + "why_false_positive": "xrpl::jss (JSON field validation), beast::lexicalCast (type validation), explicit C++ logic validates ledger_index automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash validated by xrpl::jss (JSON field validation), beast::lexicalCast (type validation), explicit C++ logic", + "issue_pattern": "Missing validation for ledger_hash", + "why_false_positive": "xrpl::jss (JSON field validation), beast::lexicalCast (type validation), explicit C++ logic validates ledger_hash automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "currency", + "validation", + "missing", + "check" + ], + "evidence": "Field currency validated by xrpl::jss (JSON field validation), beast::lexicalCast (type validation), explicit C++ logic", + "issue_pattern": "Missing validation for currency", + "why_false_positive": "xrpl::jss (JSON field validation), beast::lexicalCast (type validation), explicit C++ logic validates currency automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "issuer", + "validation", + "missing", + "check" + ], + "evidence": "Field issuer validated by xrpl::jss (JSON field validation), beast::lexicalCast (type validation), explicit C++ logic", + "issue_pattern": "Missing validation for issuer", + "why_false_positive": "xrpl::jss (JSON field validation), beast::lexicalCast (type validation), explicit C++ logic validates issuer automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "strLedger", + "empty", + "string", + "validation" + ], + "evidence": "jvParseLedger (explicit logic) at jvParseLedger", + "issue_pattern": "Missing empty string validation for strLedger", + "why_false_positive": "jvParseLedger (explicit logic) validates strLedger for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "strMsg", + "empty", + "string", + "validation" + ], + "evidence": "createHTTPPost (implicit via Content-Length and HTTP format) at createHTTPPost", + "issue_pattern": "Missing empty string validation for strMsg", + "why_false_positive": "createHTTPPost (implicit via Content-Length and HTTP format) validates strMsg for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "strMsg", + "format", + "validation", + "invalid" + ], + "evidence": "createHTTPPost (implicit via Content-Length and HTTP format) at createHTTPPost", + "issue_pattern": "Missing format validation for strMsg", + "why_false_positive": "createHTTPPost (implicit via Content-Length and HTTP format) validates strMsg format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.5, + "detection_keywords": [ + "mapRequestHeaders", + "empty", + "string", + "validation" + ], + "evidence": "createHTTPPost (implicit via iteration and string insertion) at createHTTPPost", + "issue_pattern": "Missing empty string validation for mapRequestHeaders", + "why_false_positive": "createHTTPPost (implicit via iteration and string insertion) validates mapRequestHeaders for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "mapRequestHeaders", + "format", + "validation", + "invalid" + ], + "evidence": "createHTTPPost (implicit via iteration and string insertion) at createHTTPPost", + "issue_pattern": "Missing format validation for mapRequestHeaders", + "why_false_positive": "createHTTPPost (implicit via iteration and string insertion) validates mapRequestHeaders format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.6, + "detection_keywords": [ + "strCurrencyIssuer", + "empty", + "string", + "validation" + ], + "evidence": "jvParseCurrencyIssuer (regex/format, partial code shown) at jvParseCurrencyIssuer", + "issue_pattern": "Missing empty string validation for strCurrencyIssuer", + "why_false_positive": "jvParseCurrencyIssuer (regex/format, partial code shown) validates strCurrencyIssuer for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "strCurrencyIssuer", + "format", + "validation", + "invalid" + ], + "evidence": "jvParseCurrencyIssuer (regex/format, partial code shown) at jvParseCurrencyIssuer", + "issue_pattern": "Missing format validation for strCurrencyIssuer", + "why_false_positive": "jvParseCurrencyIssuer (regex/format, partial code shown) validates strCurrencyIssuer format" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/RPCCall.cpp", + "functions": [ + { + "args": [ + "strHost", + "strPath", + "strMsg", + "mapRequestHeaders" + ], + "lineno": 15, + "name": "createHTTPPost" + }, + { + "args": [ + "strMethod", + "params", + "id" + ], + "lineno": 671, + "name": "JSONRPCRequest" + }, + { + "args": [ + "args", + "retParams", + "apiVersion", + "j" + ], + "lineno": 765, + "name": "rpcCmdToJson" + }, + { + "args": [ + "args", + "config", + "logs", + "apiVersion", + "headers" + ], + "lineno": 813, + "name": "rpcClient" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 29, + "name": "xrpl" + }, + { + "lineno": 1002, + "name": "RPCCall" + } + ], + "test_coverage_notes": "Typical test coverage for these paths would be in integration/system tests for RPC client (e.g., tests exercising command-line or HTTP RPC calls). Unit tests may exist for createHTTPPost and RPCParser methods, but regex and ledger parsing edge cases (invalid hashes, malformed currency/issuer) may not be fully covered. Tests for error handling (exceptions on invalid input) are likely but not guaranteed. Look for test files in test/rpc, test/unittest/rpc, or similar directories. Gaps may exist in negative testing (malformed headers, invalid Content-Length, non-string ledger values).", + "validation_architecture": { + "auto_validated_fields": [ + "ledger_index", + "ledger_hash", + "currency", + "issuer" + ], + "framework": "xrpl::jss (JSON field validation), beast::lexicalCast (type validation), explicit C++ logic", + "validation_layer": "business_logic (inside RPCParser and HTTP construction)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::exception (via beast::lexicalCast if not uint32_t)", + "field": "strLedger", + "location": "jvParseLedger", + "validated_by": "jvParseLedger (explicit logic)", + "validates": [ + "Checks if strLedger is one of 'current', 'closed', 'validated' (string match)", + "Checks if strLedger is 64 characters (potentially a uint256 hash)", + "Attempts to cast strLedger to uint32_t (throws if not a valid integer)" + ], + "validation_type": "format|type" + }, + { + "confidence": 0.7, + "error_thrown": "None (relies on downstream HTTP/JSON parsing)", + "field": "strMsg", + "location": "createHTTPPost", + "validated_by": "createHTTPPost (implicit via Content-Length and HTTP format)", + "validates": [ + "Ensures Content-Length matches strMsg.size()", + "Ensures HTTP headers are well-formed" + ], + "validation_type": "format" + }, + { + "confidence": 0.5, + "error_thrown": "None (assumes keys/values are valid strings)", + "field": "mapRequestHeaders", + "location": "createHTTPPost", + "validated_by": "createHTTPPost (implicit via iteration and string insertion)", + "validates": [ + "Assumes all keys and values are valid HTTP header strings" + ], + "validation_type": "format" + }, + { + "confidence": 0.6, + "error_thrown": "Unknown (code incomplete, likely throws or returns error if format invalid)", + "field": "strCurrencyIssuer", + "location": "jvParseCurrencyIssuer", + "validated_by": "jvParseCurrencyIssuer (regex/format, partial code shown)", + "validates": [ + "Checks if input matches expected currency/issuer format (likely regex or string split)" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCCall.cpp.ai.md b/src/xrpld/rpc/detail/RPCCall.cpp.ai.md new file mode 100644 index 0000000000..ffdcd1fa63 --- /dev/null +++ b/src/xrpld/rpc/detail/RPCCall.cpp.ai.md @@ -0,0 +1,65 @@ +# `src/xrpld/rpc/detail/RPCCall.cpp` + +This file is the backbone of the XRPL command-line RPC client. It answers the question: given a user-typed command like `account_info rXYZ validated`, how does that sequence of strings become a valid HTTP POST request sent to a running `xrpld` node, and how does the response come back? It owns everything from argument parsing to HTTP framing to asynchronous response handling. + +## Overall Structure + +The file lives entirely in `namespace xrpl` and exports a handful of symbols through `RPCCall.h`: `RPCCall::fromCommandLine`, `RPCCall::fromNetwork`, `rpcCmdToJson`, and `rpcClient`. Internally it defines three supporting types — `RPCParser`, `RPCCallImp`, and a file-local `RequestNotParsable` exception — plus the free function `createHTTPPost`. + +## `createHTTPPost` — Minimal HTTP Framing + +This function produces a raw HTTP/1.0 POST string, not a proper HTTP library abstraction. The comment at the top is explicit: *"This ain't Apache."* The choice of HTTP/1.0 rather than 1.1 avoids persistent-connection complexity for what is essentially a fire-and-forget single-request client. `Content-Length` is set directly from `strMsg.size()`, and additional headers from `mapRequestHeaders` are appended as-is. No validation is performed on header key/value format — the caller is trusted to supply valid strings. + +## `RPCParser` — Positional Argument to JSON Translation + +`RPCParser` is the most substantial piece of the file. It converts a command name and its positional string arguments into a `Json::Value` object that matches the expected JSON-RPC parameter schema for each method. The class is instantiated per-call and carries an `apiVersion_` member plus a `beast::Journal` for tracing. + +The `parseCommand` method drives dispatch through a static `constexpr` array of `Command` structs — each carrying the method name, a member-function pointer to a `parseXxx` method, a minimum parameter count, and a maximum (with `-1` meaning unbounded). Dispatch is a plain linear scan over roughly fifty entries. That's an intentional tradeoff: the array is small, entirely L1-cache-resident, and avoids the indirection of a hash map. The arity check happens before the parse function is called, so each parser can assert its preconditions without defensive parameter-count guards. + +The `parseXxx` methods follow a consistent pattern: construct a `Json::Value` object from positional params, return an `rpcError(...)` JSON object on bad input rather than throwing. This keeps error handling at the caller level. + +Several helper methods are worth noting: + +- `jvParseLedger` is the canonical ledger identifier normalizer. It distinguishes the string sentinels `"current"`, `"closed"`, and `"validated"` (written to `ledger_index`), 64-character hex strings (written to `ledger_hash`), and numeric sequences cast via `beast::lexicalCast` (which throws on non-integer input). The fact that this still carries a `// TODO New routine` comment signals that not all callers have been migrated. + +- `jvParseCurrencyIssuer` uses a Boost.Regex pattern anchored to three ISO-charset characters, optionally followed by a `/` and issuer string. It returns an `RPC::make_param_error` JSON value on mismatch — again, not an exception. + +- `validPublicKey` accepts both base58-encoded and hex-encoded public keys, covering both account and node key types. + +- `parseJson` and `parseJson2` handle passthrough of pre-formed JSON payloads. `parseJson2` additionally validates that the batch or single-call payload carries both `jsonrpc: "2.0"` and `ripplerpc: "2.0"` fields — the latter being XRPL's own extension discriminator — and preserves error context (`id`, `jsonrpc`, `ripplerpc`) when parsing fails. + +- Event-driven commands (`subscribe`, `unsubscribe`, `path_find`) are routed to `parseEvented`, which unconditionally returns `rpcNO_EVENTS`. This encodes the architectural boundary: this HTTP-based synchronous path simply does not support WebSocket-style subscriptions. + +## `JSONRPCRequest` — JSON-RPC Envelope + +A thin wrapper that assembles `{ "method": ..., "params": ..., "id": ... }` and appends a newline. This conforms to JSON-RPC 1.0 framing, which the comment notes is used for compatibility despite partial adoption of 1.1/2.0 conventions elsewhere. + +## `RPCCallImp` — Async Callback Plumbing + +`RPCCallImp` is a utility struct with only static methods, acting as a namespace for the two async callbacks passed to `HTTPClient::request`: + +- `onRequest` calls `createHTTPPost` and `JSONRPCRequest` to write the raw HTTP payload into a `boost::asio::streambuf`. + +- `onResponse` is where the response lifecycle is managed. An empty reply body throws `std::runtime_error` ("no response from server"). A body beginning with `"Unable to parse request"` or `"invalid_API_version"` throws the file-local `RequestNotParsable` — this distinction matters because `rpcClient` catches it separately to emit `rpcINVALID_PARAMS` instead of the generic `rpcINTERNAL`. Any body that fails JSON parsing also throws `std::runtime_error`. Only after successful parsing does it invoke the callback. + +## `rpcCmdToJson` — Parser Orchestration + +`rpcCmdToJson` instantiates `RPCParser`, collects all arguments after `args[0]` into a JSON array, dispatches `parseCommand`, and then injects `api_version` into every non-error result that doesn't already carry one. The injection applies element-wise for batch arrays. This is where API versioning becomes part of the wire format for outgoing CLI requests. + +## `rpcClient` — Full Execution Path + +`rpcClient` is the workhorse called by both the command-line entry point and unit tests. Its flow is: + +1. Parse arguments via `rpcCmdToJson`. +2. If the parse returned an error JSON object, return immediately without touching the network. +3. Otherwise, attempt to read the server address from `ServerHandler::Setup` (gracefully ignoring exceptions so the client works without a config file), then override with `config.rpc_ip` if present. +4. Inject `admin_user` / `admin_password` from the client config into the request. +5. Create a local `boost::asio::io_context`, call `RPCCall::fromNetwork`, then `isService.run()` synchronously, which blocks until the one outstanding async request completes. +6. Unwrap the `"result"` key from the response, or synthesize `rpcJSON_RPC` on transport error. +7. Catch `RequestNotParsable` (from `onResponse`) → `rpcINVALID_PARAMS`; catch any other exception → `rpcINTERNAL`. + +The design choice to use `io_context::run()` synchronously — creating and destroying the io_context per call — is deliberate for the CLI use case. It avoids shared-state complexity while paying a modest setup cost that is immaterial for interactive one-shot commands. + +## `RPCCall::fromCommandLine` and `RPCCall::fromNetwork` + +`fromCommandLine` is the outermost CLI entry point. It simply calls `rpcClient` with `RPC::apiCommandLineVersion` and prints the result to stdout as styled JSON. `fromNetwork` is the reusable async entry point. It builds the HTTP Basic authorization header from username/password with base64 encoding, sets a 256 MiB response cap (`RPC_REPLY_MAX_BYTES`) and a 30-second timeout, then hands everything to `HTTPClient::request`. Both constants are hardcoded — there is no per-request configuration for them. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCHandler.cpp.ai.json b/src/xrpld/rpc/detail/RPCHandler.cpp.ai.json new file mode 100644 index 0000000000..cc731d5369 --- /dev/null +++ b/src/xrpld/rpc/detail/RPCHandler.cpp.ai.json @@ -0,0 +1,494 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "fillHandler" + ], + "entry_point": "fillHandler", + "purpose": "Validates incoming RPC request parameters and determines the appropriate handler for the command.", + "validation_points": [ + "isUnlimited(context.role) - validates role", + "context.params.isMember(jss::command) / jss::method - validates command presence", + "context.params[jss::command].asString() == context.params[jss::method].asString() - validates command consistency" + ] + }, + { + "call_chain": [ + "callMethod", + "fillHandler", + "getHandler", + "handler->invoke" + ], + "entry_point": "callMethod", + "purpose": "Top-level dispatcher for executing an RPC command, including validation and handler invocation.", + "validation_points": [ + "fillHandler (all validations above)", + "handler->invoke (template-based parameter validation, e.g., ledger_index, api_version, request)" + ] + }, + { + "call_chain": [ + "doCommand", + "callMethod", + "fillHandler", + "getHandler", + "handler->invoke" + ], + "entry_point": "doCommand", + "purpose": "Processes an RPC command from the HTTP/WebSocket layer, including validation, execution, and error handling.", + "validation_points": [ + "callMethod (see above)", + "Exception handling for validation errors" + ] + }, + { + "call_chain": [ + "roleRequired" + ], + "entry_point": "roleRequired", + "purpose": "Determines the minimum role required for a given command.", + "validation_points": [ + "Checks command against role requirements" + ] + } + ], + "data_flows": [ + { + "field": "role", + "flow": [ + "context.role", + "fillHandler", + "isUnlimited(context.role)", + "job queue check" + ], + "origin": "context.role (set by HTTP/WebSocket layer based on authentication)", + "transformations": [ + "Checked for 'unlimited' status to bypass job queue limits" + ], + "validated_at": "fillHandler" + }, + { + "field": "command", + "flow": [ + "context.params", + "fillHandler (isMember checks, asString extraction)", + "getHandler (lookup handler for command)", + "handler->invoke (template validation)" + ], + "origin": "context.params[jss::command] or context.params[jss::method] (from incoming JSON request)", + "transformations": [ + "Checked for presence", + "Checked for consistency if both command and method present", + "String extracted for handler lookup" + ], + "validated_at": "fillHandler" + }, + { + "field": "ledger_index", + "flow": [ + "context.params", + "handler->invoke (template-based validation)", + "Used in ledger lookup" + ], + "origin": "context.params[jss::ledger_index] (from incoming JSON request)", + "transformations": [ + "Validated for type and range by template validation framework" + ], + "validated_at": "handler->invoke (template validation)" + }, + { + "field": "api_version", + "flow": [ + "context.apiVersion", + "getHandler (selects handler implementation)", + "handler->invoke (template validation)" + ], + "origin": "context.apiVersion (set by HTTP/WebSocket layer, possibly from params)", + "transformations": [ + "Used to select handler and error message format" + ], + "validated_at": "handler->invoke (template validation)" + }, + { + "field": "request", + "flow": [ + "context.params", + "handler->invoke (template validation)", + "Used for command execution" + ], + "origin": "context.params (entire incoming JSON request)", + "transformations": [ + "Validated for required fields and types by template validation" + ], + "validated_at": "handler->invoke (template validation)" + } + ], + "description": "Implements core logic for dispatching and executing XRPL RPC commands, including handler lookup, permission checks, and method invocation, with support for both HTTP and WebSocket response formats.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "command", + "validation", + "missing", + "check" + ], + "evidence": "Field command validated by jss:: (JSON field specifiers, likely macro/template-based validation)", + "issue_pattern": "Missing validation for command", + "why_false_positive": "jss:: (JSON field specifiers, likely macro/template-based validation) validates command automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_index validated by jss:: (JSON field specifiers, likely macro/template-based validation)", + "issue_pattern": "Missing validation for ledger_index", + "why_false_positive": "jss:: (JSON field specifiers, likely macro/template-based validation) validates ledger_index automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "api_version", + "validation", + "missing", + "check" + ], + "evidence": "Field api_version validated by jss:: (JSON field specifiers, likely macro/template-based validation)", + "issue_pattern": "Missing validation for api_version", + "why_false_positive": "jss:: (JSON field specifiers, likely macro/template-based validation) validates api_version automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "request", + "validation", + "missing", + "check" + ], + "evidence": "Field request validated by jss:: (JSON field specifiers, likely macro/template-based validation)", + "issue_pattern": "Missing validation for request", + "why_false_positive": "jss:: (JSON field specifiers, likely macro/template-based validation) validates request automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "role", + "empty", + "string", + "validation" + ], + "evidence": "isUnlimited(context.role) at fillHandler", + "issue_pattern": "Missing empty string validation for role", + "why_false_positive": "isUnlimited(context.role) validates role for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "command", + "empty", + "string", + "validation" + ], + "evidence": "jss::command (likely via template or macro expansion) at JSON request parsing (implied in comments and error responses)", + "issue_pattern": "Missing empty string validation for command", + "why_false_positive": "jss::command (likely via template or macro expansion) validates command for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "jss::ledger_index (likely via template or macro expansion) at JSON request parsing (implied in comments and error responses)", + "issue_pattern": "Missing empty string validation for ledger_index", + "why_false_positive": "jss::ledger_index (likely via template or macro expansion) validates ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "api_version", + "empty", + "string", + "validation" + ], + "evidence": "jss::api_version (likely via template or macro expansion) at JSON request parsing (implied in comments and error responses)", + "issue_pattern": "Missing empty string validation for api_version", + "why_false_positive": "jss::api_version (likely via template or macro expansion) validates api_version for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "request", + "empty", + "string", + "validation" + ], + "evidence": "jss::request (likely via template or macro expansion) at JSON error response construction", + "issue_pattern": "Missing empty string validation for request", + "why_false_positive": "jss::request (likely via template or macro expansion) validates request for empty strings" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/RPCHandler.cpp", + "functions": [ + { + "args": [ + "context", + "result" + ], + "lineno": 74, + "name": "fillHandler" + }, + { + "args": [ + "context", + "method", + "name", + "result" + ], + "lineno": 110, + "name": "callMethod" + }, + { + "args": [ + "context", + "result" + ], + "lineno": 154, + "name": "doCommand" + }, + { + "args": [ + "version", + "betaEnabled", + "method" + ], + "lineno": 186, + "name": "roleRequired" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + }, + { + "lineno": 20, + "name": "RPC" + } + ], + "test_coverage_notes": "Core validation logic is likely covered by integration tests in the 'rippled' test suite, especially those under 'src/test/rpc' (e.g., RPCHandler_test.cpp, Handler_test.cpp, or WebSocket/RPC HTTP tests). Template-based parameter validation is often tested via command-specific tests (e.g., ledger, account_info). However, edge cases such as conflicting 'command' and 'method', missing required fields, or role-based access control may not be exhaustively tested. There may be gaps in negative testing for malformed or ambiguous requests, and for job queue overload scenarios.", + "validation_architecture": { + "auto_validated_fields": [ + "command", + "ledger_index", + "api_version", + "request" + ], + "framework": "jss:: (JSON field specifiers, likely macro/template-based validation)", + "validation_layer": "entry_point (JSON RPC handler, before business logic)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "error_code_i (custom error code)", + "field": "role", + "location": "fillHandler", + "validated_by": "isUnlimited(context.role)", + "validates": [ + "Checks if the user role has unlimited access (admin/privileged)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "error_code_i (e.g., missingCommand)", + "field": "command", + "location": "JSON request parsing (implied in comments and error responses)", + "validated_by": "jss::command (likely via template or macro expansion)", + "validates": [ + "Checks that the 'command' field is present and is a string" + ], + "validation_type": "format|type" + }, + { + "confidence": 0.8, + "error_thrown": "error_code_i (e.g., invalidParams)", + "field": "ledger_index", + "location": "JSON request parsing (implied in comments and error responses)", + "validated_by": "jss::ledger_index (likely via template or macro expansion)", + "validates": [ + "Checks that 'ledger_index' is present, is an integer, and within valid range" + ], + "validation_type": "type|range" + }, + { + "confidence": 0.7, + "error_thrown": "error_code_i (e.g., invalidParams)", + "field": "api_version", + "location": "JSON request parsing (implied in comments and error responses)", + "validated_by": "jss::api_version (likely via template or macro expansion)", + "validates": [ + "Checks that 'api_version' is present and is a supported integer value" + ], + "validation_type": "type|range" + }, + { + "confidence": 0.7, + "error_thrown": "error_code_i (e.g., invalidParams)", + "field": "request", + "location": "JSON error response construction", + "validated_by": "jss::request (likely via template or macro expansion)", + "validates": [ + "Checks that 'request' is a valid JSON object" + ], + "validation_type": "type|format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCHandler.cpp.ai.md b/src/xrpld/rpc/detail/RPCHandler.cpp.ai.md new file mode 100644 index 0000000000..24b0c774bb --- /dev/null +++ b/src/xrpld/rpc/detail/RPCHandler.cpp.ai.md @@ -0,0 +1,47 @@ +# `RPCHandler.cpp` — RPC Command Dispatch Engine + +## Overview + +`RPCHandler.cpp` is the central execution hub for all XRPL RPC commands, shared by both the HTTP and WebSocket transports. When a client sends a JSON request — whether over a REST-style HTTP call or a persistent WebSocket connection — it eventually arrives here for validation, authorization, and dispatch to the appropriate command handler. The file is deliberately thin: it owns the dispatch pipeline but not the business logic of any individual command. + +The large block comment at the top of the file captures an important protocol quirk: HTTP and WebSocket responses have structurally different JSON shapes. For HTTP, `status` lives *inside* the `result` object; for WebSocket, it lives *outside* at the top level. `RPCHandler.cpp` doesn't normalize this — that divergence is handled by the transport layers. Its job is to populate the `result` object itself. + +## `fillHandler()` — The Pre-Dispatch Gatekeeper + +`fillHandler()` is an anonymous-namespace function that performs every validation required before a command handler is invoked. Its responsibilities, in order, are: + +**Job queue backpressure.** For non-privileged callers (`isUnlimited` returns false), the function counts all jobs at `jtCLIENT` priority or above and rejects the request with `rpcTOO_BUSY` if the count exceeds `Tuning::maxJobQueueClients` (500). This is the node's primary defense against client-induced job queue saturation. Admin/unlimited clients bypass this gate intentionally — operators need access even under load. + +**Command field resolution.** The XRPL protocol accepts the command name under either `"command"` (the canonical JSON-RPC field) or `"method"` (the legacy WebSocket field). `fillHandler()` accepts either, but if a request provides *both* with *different* values it returns `rpcUNKNOWN_COMMAND`. This prevents ambiguous dispatch rather than silently picking one field over the other. + +**Handler lookup.** `getHandler(version, betaEnabled, strCommand)` maps the command string to a `Handler` struct, taking the API version and a beta-API flag from the application config. Unknown commands return `nullptr`, producing `rpcUNKNOWN_COMMAND`. The version parameter matters here: handlers declare `minApiVer_` and `maxApiVer_` ranges, so the same command name can map to different implementations across API versions. + +**Role-based access control.** If the resolved handler requires `Role::ADMIN`, the caller's `context.role` must match. This is the enforcement point for administrative RPC commands (such as `stop` or `peers`); the role itself was set by the HTTP or WebSocket layer based on the origin IP and authentication credentials. + +**Node condition checks.** `conditionMet()` (defined in `Handler.h`) evaluates whether the node is in a suitable state for commands that carry a non-`NO_CONDITION` flag. It guards against amendment-blocked nodes, expired validator lists, insufficient network synchronization, and stale validated ledgers. Importantly, `conditionMet()` is API-version-aware: a v1 caller receives the legacy `rpcNO_NETWORK` error code while a v2 caller receives the more precise `rpcNOT_SYNCED`. This backward-compatibility branching is baked directly into the condition-check layer rather than scattered across individual handlers. + +## `callMethod()` — Instrumented Invocation + +`callMethod()` is a file-local template function that wraps the actual handler call with observability and exception safety. It generates a monotonically increasing `requestId` via a `static std::atomic` — a simple but thread-safe counter that produces unique IDs for the performance log without needing a mutex. + +The function brackets handler execution with `perfLog.rpcStart()` and `perfLog.rpcFinish()` calls, and also registers a `makeLoadEvent` with the job queue under the name `"cmd:"`. Wall-clock duration is measured with `std::chrono::system_clock` and logged at debug level on completion, providing a lightweight per-call trace in the node's log. + +Exception handling is where a subtle resource policy lives: if any `std::exception` escapes the handler, and the request was classified as `feeReferenceRPC` (the standard cost tier), `callMethod()` escalates `context.loadType` to `feeExceptionRPC`. This elevation causes the resource management layer to charge the client a higher fee for the call that caused a server-side crash — an automatic deterrent against requests that abuse the server while appearing within the normal API surface. + +After escalation, `inject_error(rpcINTERNAL, result)` writes the generic internal-error JSON into the result object, and the function returns `rpcINTERNAL`. No exception ever propagates out of `callMethod()`, giving the HTTP and WebSocket layers a clean contract. + +## `doCommand()` — The Public Entry Point + +`doCommand()` is the sole public function in this file (along with `roleRequired()`). It calls `fillHandler()`, injects any gating error into the result, and then invokes `callMethod()` via the handler's `valueMethod_` function pointer. The only branching here is an optional logging path: when HTTP headers carry non-empty `user` or `X-Forwarded-For` values, start/finish log lines bracket the call with that identity. This supports audit logging for API gateways that forward client IP addresses, without burdening the common code path with string construction when headers are empty. + +If `handler->valueMethod_` is null (which should not occur for any registered handler but is defensively checked), the function returns `rpcUNKNOWN_COMMAND`. This is belt-and-suspenders protection against a handler registration that omits the method function. + +## `roleRequired()` — Out-of-Band Role Query + +`roleRequired()` is a utility exposed to callers that need to know a command's access level *before* constructing a full `JsonContext` — for example, to pre-filter WebSocket subscriptions or validate access during connection setup. It delegates to `getHandler()` and returns `Role::FORBID` for unknown commands, making it safe to call for any arbitrary method string. + +## Design Tradeoffs + +The three-function pipeline — `fillHandler` → `callMethod` → handler — cleanly separates concerns: gating/routing, instrumentation/safety, and business logic. The cost is indirection: every RPC call passes through two layers before touching any domain code. For a latency-sensitive path this matters, but the `makeLoadEvent` and `perfLog` calls indicate that throughput accounting is already the dominant overhead, so the extra function calls are noise. + +The shared `static std::atomic requestId` inside `callMethod()` deserves mention: it is reset to zero only at process start, never wraps in practice for a single node's lifetime, and is incremented unconditionally across all concurrent callers — making it a correct lightweight correlation token for performance logs without requiring a centralized ID service. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp.ai.json b/src/xrpld/rpc/detail/RPCHelpers.cpp.ai.json new file mode 100644 index 0000000000..7700694bb9 --- /dev/null +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp.ai.json @@ -0,0 +1,519 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "parseAccountIds", + "parseBase58" + ], + "entry_point": "parseAccountIds", + "purpose": "Validates and parses an array of AccountID strings from JSON input.", + "validation_points": [ + "parseAccountIds: Checks if each element is a string.", + "parseBase58: Validates if string is a valid AccountID." + ] + }, + { + "call_chain": [ + "isRelatedToAccount", + "SLE::getType", + "SLE::getFieldAmount", + "SLE::getAccountID", + "SLE::isFieldPresent" + ], + "entry_point": "isRelatedToAccount", + "purpose": "Checks if a ledger entry (SLE) is related to a given account by examining type and relevant fields.", + "validation_points": [ + "isRelatedToAccount: Validates SLE type and presence of fields (sfLowLimit, sfHighLimit, sfAccount, sfDestination, sfOwner)." + ] + }, + { + "call_chain": [ + "getStartHint", + "SLE::getType", + "SLE::getFieldAmount", + "SLE::getFieldU64", + "SLE::isFieldPresent" + ], + "entry_point": "getStartHint", + "purpose": "Extracts a 'start hint' value from a ledger entry, validating type and field presence.", + "validation_points": [ + "getStartHint: Validates SLE type and presence of sfLowLimit, sfHighLimit, sfOwnerNode fields." + ] + }, + { + "call_chain": [ + "readLimitField", + "context.params", + "isUnlimited" + ], + "entry_point": "readLimitField", + "purpose": "Validates and clamps the 'limit' field from JSON RPC input.", + "validation_points": [ + "readLimitField: Checks if 'limit' is present, is an unsigned integer, is nonzero, and clamps to allowed range." + ] + } + ], + "data_flows": [ + { + "field": "AccountID (from JSON)", + "flow": [ + "JSON input", + "parseAccountIds", + "parseBase58", + "hash_set result" + ], + "origin": "JSON input array (jvArray)", + "transformations": [ + "Type check (isString)", + "Base58 decoding and validation" + ], + "validated_at": "parseAccountIds (type and format validation)" + }, + { + "field": "SLE fields (sfLowLimit, sfHighLimit, sfAccount, sfDestination, sfOwner)", + "flow": [ + "SLE object", + "isRelatedToAccount", + "getFieldAmount/getAccountID/isFieldPresent", + "Comparison to input AccountID" + ], + "origin": "SLE (ledger entry) object", + "transformations": [ + "Field presence check", + "Field extraction", + "Comparison" + ], + "validated_at": "isRelatedToAccount (type and field presence/ownership validation)" + }, + { + "field": "limit (from JSON)", + "flow": [ + "JSON input", + "readLimitField", + "Type/Range validation", + "Clamping to allowed range", + "Output as unsigned int" + ], + "origin": "context.params[jss::limit]", + "transformations": [ + "Type check (isUInt/isInt >= 0)", + "Range check (nonzero, clamp to min/max)" + ], + "validated_at": "readLimitField" + }, + { + "field": "start hint (sfLowNode, sfHighNode, sfOwnerNode)", + "flow": [ + "SLE object", + "getStartHint", + "getType/isFieldPresent", + "getFieldU64" + ], + "origin": "SLE fields", + "transformations": [ + "Type check", + "Field presence check", + "Field extraction" + ], + "validated_at": "getStartHint" + } + ], + "description": "This file provides various helper functions for XRPL RPC operations, including parsing account IDs, handling seeds and keypairs, validating ledger entry types, and parsing asset-related JSON fields.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "AccountID (via parseBase58)", + "validation", + "missing", + "check" + ], + "evidence": "Field AccountID (via parseBase58) validated by xrpl::protocol (parseBase58, SLE field accessors), Json::Value", + "issue_pattern": "Missing validation for AccountID (via parseBase58)", + "why_false_positive": "xrpl::protocol (parseBase58, SLE field accessors), Json::Value validates AccountID (via parseBase58) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "SLE field presence (via isFieldPresent)", + "validation", + "missing", + "check" + ], + "evidence": "Field SLE field presence (via isFieldPresent) validated by xrpl::protocol (parseBase58, SLE field accessors), Json::Value", + "issue_pattern": "Missing validation for SLE field presence (via isFieldPresent)", + "why_false_positive": "xrpl::protocol (parseBase58, SLE field accessors), Json::Value validates SLE field presence (via isFieldPresent) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "SLE type (via getType)", + "validation", + "missing", + "check" + ], + "evidence": "Field SLE type (via getType) validated by xrpl::protocol (parseBase58, SLE field accessors), Json::Value", + "issue_pattern": "Missing validation for SLE type (via getType)", + "why_false_positive": "xrpl::protocol (parseBase58, SLE field accessors), Json::Value validates SLE type (via getType) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "jvArray elements (AccountID strings)", + "empty", + "string", + "validation" + ], + "evidence": "parseAccountIds (parseBase58) at parseAccountIds", + "issue_pattern": "Missing empty string validation for jvArray elements (AccountID strings)", + "why_false_positive": "parseAccountIds (parseBase58) validates jvArray elements (AccountID strings) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sle->getType()", + "empty", + "string", + "validation" + ], + "evidence": "isRelatedToAccount (SLE type checks) at isRelatedToAccount", + "issue_pattern": "Missing empty string validation for sle->getType()", + "why_false_positive": "isRelatedToAccount (SLE type checks) validates sle->getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLowLimit, sfHighLimit, sfAccount, sfDestination, sfOwner", + "empty", + "string", + "validation" + ], + "evidence": "isRelatedToAccount (isFieldPresent, getAccountID) at isRelatedToAccount", + "issue_pattern": "Missing empty string validation for sfLowLimit, sfHighLimit, sfAccount, sfDestination, sfOwner", + "why_false_positive": "isRelatedToAccount (isFieldPresent, getAccountID) validates sfLowLimit, sfHighLimit, sfAccount, sfDestination, sfOwner for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sle->getType()", + "empty", + "string", + "validation" + ], + "evidence": "getStartHint (SLE type checks) at getStartHint", + "issue_pattern": "Missing empty string validation for sle->getType()", + "why_false_positive": "getStartHint (SLE type checks) validates sle->getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "sfLowLimit, sfHighLimit, sfOwnerNode", + "empty", + "string", + "validation" + ], + "evidence": "getStartHint (isFieldPresent, getFieldU64) at getStartHint", + "issue_pattern": "Missing empty string validation for sfLowLimit, sfHighLimit, sfOwnerNode", + "why_false_positive": "getStartHint (isFieldPresent, getFieldU64) validates sfLowLimit, sfHighLimit, sfOwnerNode for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/RPCHelpers.cpp", + "functions": [ + { + "args": [ + "sle", + "accountID" + ], + "lineno": 13, + "name": "getStartHint" + }, + { + "args": [ + "ledger", + "sle", + "accountID" + ], + "lineno": 28, + "name": "isRelatedToAccount" + }, + { + "args": [ + "jvArray" + ], + "lineno": 56, + "name": "parseAccountIds" + }, + { + "args": [ + "limit", + "range", + "context" + ], + "lineno": 70, + "name": "readLimitField" + }, + { + "args": [ + "value" + ], + "lineno": 92, + "name": "parseXrplLibSeed" + }, + { + "args": [ + "params", + "error" + ], + "lineno": 104, + "name": "getSeedFromRPC" + }, + { + "args": [ + "params", + "error", + "apiVersion" + ], + "lineno": 143, + "name": "keypairForSignature" + }, + { + "args": [ + "params" + ], + "lineno": 238, + "name": "chooseLedgerEntryType" + }, + { + "args": [ + "type" + ], + "lineno": 282, + "name": "isAccountObjectsValidType" + }, + { + "args": [ + "asset", + "params", + "name", + "j" + ], + "lineno": 295, + "name": "parseSubUnsubJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 12, + "name": "RPC" + } + ], + "test_coverage_notes": "The code is likely covered by unit and integration tests for RPC handlers that use these helpers, such as account_objects, account_lines, and trustline-related RPCs. Tests would be found in files like 'test/rpc/AccountObjects_test.cpp', 'test/rpc/AccountLines_test.cpp', and possibly 'test/rpc/handlers/AccountObjects_test.cpp'. However, direct unit tests for these helpers may be limited; edge cases (e.g., malformed JSON, invalid AccountID strings, missing fields in SLEs) may not be fully covered unless explicitly tested in those files. Template-based validation may have generic test coverage, but specific field validation (e.g., for parseAccountIds or readLimitField) should be checked for negative and boundary cases.", + "validation_architecture": { + "auto_validated_fields": [ + "AccountID (via parseBase58)", + "SLE field presence (via isFieldPresent)", + "SLE type (via getType)" + ], + "framework": "xrpl::protocol (parseBase58, SLE field accessors), Json::Value", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns empty hash_set()", + "field": "jvArray elements (AccountID strings)", + "location": "parseAccountIds", + "validated_by": "parseAccountIds (parseBase58)", + "validates": [ + "Each element is a string (jv.isString())", + "Each string is valid base58 AccountID (parseBase58)" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "returns false", + "field": "sle->getType()", + "location": "isRelatedToAccount", + "validated_by": "isRelatedToAccount (SLE type checks)", + "validates": [ + "SLE type is checked for ltRIPPLE_STATE, ltSIGNER_LIST, ltNFTOKEN_OFFER" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "returns false", + "field": "sfLowLimit, sfHighLimit, sfAccount, sfDestination, sfOwner", + "location": "isRelatedToAccount", + "validated_by": "isRelatedToAccount (isFieldPresent, getAccountID)", + "validates": [ + "Field presence checked (isFieldPresent)", + "AccountID extracted only if present" + ], + "validation_type": "presence|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "returns 0", + "field": "sle->getType()", + "location": "getStartHint", + "validated_by": "getStartHint (SLE type checks)", + "validates": [ + "SLE type is checked for ltRIPPLE_STATE" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "returns 0", + "field": "sfLowLimit, sfHighLimit, sfOwnerNode", + "location": "getStartHint", + "validated_by": "getStartHint (isFieldPresent, getFieldU64)", + "validates": [ + "Field presence checked (isFieldPresent)", + "Field value extracted only if present" + ], + "validation_type": "presence|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCHelpers.cpp.ai.md b/src/xrpld/rpc/detail/RPCHelpers.cpp.ai.md new file mode 100644 index 0000000000..6913ce3966 --- /dev/null +++ b/src/xrpld/rpc/detail/RPCHelpers.cpp.ai.md @@ -0,0 +1,49 @@ +# `src/xrpld/rpc/detail/RPCHelpers.cpp` + +This file is the utility belt for the XRPL RPC layer. Rather than belonging to any single command, it centralises cross-cutting concerns — ledger object ownership tests, paginated-query limit management, seed and keypair derivation, ledger entry type selection, and order-book asset parsing — so that individual RPC handlers can stay thin and consistent. Nearly every paginated or cryptographic RPC handler in the codebase calls at least one function from here. + +## Ledger Object Ownership — `getStartHint` and `isRelatedToAccount` + +Paginated enumeration of account-owned objects requires knowing where in the owner directory to resume. `getStartHint` extracts the 64-bit directory node index that serves as this cursor. For trust lines (`ltRIPPLE_STATE`) it must pick the *correct* side: a trust line has both a `sfLowNode` and a `sfHighNode`, and the relevant one depends on whether the requesting account is the low-limit or high-limit party. Everything else uses the generic `sfOwnerNode`, and objects that lack that field return 0 (start from the beginning). + +`isRelatedToAccount` answers whether a given SLE (Serialized Ledger Entry) should be visible to a particular account. The function is deliberately type-aware: + +- **Trust lines** (`ltRIPPLE_STATE`) belong to both parties; the function returns true for either side. +- **Objects with `sfAccount`** default to ownership by that account. Escrows, payment channels, and checks also appear in the *destination* account's directory, so `sfDestination` is checked as a fallback — but only when the object has `sfAccount`. +- **NFToken Offers** (`ltNFTOKEN_OFFER`) require a special carve-out. They are *not* added to the destination's directory, so only `sfOwner` is tested even though `sfDestination` may be present. A comment in the code makes this explicit to prevent future contributors from "fixing" the apparent inconsistency. +- **Signer lists** (`ltSIGNER_LIST`) are identified by comparing the SLE's raw ledger key against the canonical `keylet::signers(accountID)` — there is no `sfAccount` field to check on this type. + +Together, these two functions power `account_objects` and related handlers that page through an account's owned entries. + +## Pagination Limits — `readLimitField` + +All paginated RPC calls share the same limit-parameter semantics, enforced in one place by `readLimitField`. It reads the optional `limit` field, applies the per-command `Tuning::LimitRange` (10–400 for most account queries, defined in `Tuning.h`), and clamps the value to `[rmin, rmax]` for non-admin roles. Admin connections (`isUnlimited(context.role)`) bypass clamping, allowing unrestricted result sets. This single function prevents both accidental and malicious abuse of the server's bandwidth by ordinary API consumers. + +## Seed Parsing — `parseXrplLibSeed` and `getSeedFromRPC` + +The XRPL ecosystem has a legacy compatibility wrinkle: the `xrpl.js` client library encodes Ed25519 seeds with a non-standard two-byte header (`0xE1 0x4B`) before the 16-byte seed material, producing an 18-byte payload after Base58 decoding. The server itself never emits seeds in this form, but `parseXrplLibSeed` detects the pattern and silently unwraps it so that users who copy-paste from their JavaScript wallet don't get a confusing error. + +`getSeedFromRPC` enforces the rule that exactly one of `passphrase`, `seed`, or `seed_hex` may be provided. It uses a static dispatch table — an array of `(field name, parser lambda)` pairs — and counts how many are present in the request before invoking any of them. This avoids ambiguity when multiple fields arrive and produces a precise error listing all valid alternatives. + +## Keypair Derivation — `keypairForSignature` + +This is the most intricate function in the file. It reconciles several overlapping input conventions: + +1. The legacy `secret` field accepts a raw string seed and implies secp256k1. +2. The structured form (`key_type` + one of `passphrase`, `seed`, or `seed_hex`) allows choosing Ed25519. +3. The XrplLib non-standard Ed25519 encoding (detected by `parseXrplLibSeed`) overrides the default key type. +4. If an XrplLib seed is detected but the caller explicitly requested non-Ed25519 via `key_type`, the function returns an error rather than silently using the wrong curve. + +`jss::secret` and `jss::key_type` are mutually exclusive by design — `secret` is the legacy path that bundles seed format and key type together, while `key_type` is the explicit, forward-compatible API. Mixing them is rejected. + +Error message formatting differs by API version: callers on API v2+ receive the structured `rpcBAD_KEY_TYPE` code, while v1 clients get the older `invalid_field_error` string form. There is also a code comment explaining why `strcmp` is used instead of pointer comparison for `jss::` string constants — a known MSVC compiler bug with `constexpr char*` means pointer equality can fail even for the same logical constant. + +## Ledger Entry Type Selection — `chooseLedgerEntryType` and `isAccountObjectsValidType` + +`chooseLedgerEntryType` builds its lookup table at compile time using the X-macro pattern over `ledger_entries.macro`. Each entry in the macro file expands to a `(canonical_name, rpc_name, type_tag)` tuple. The match logic accepts either the canonical name case-insensitively (e.g., `"ripplestate"`) or the RPC name case-sensitively (e.g., `"state"`), accommodating both historical usage and newer consistent naming. If no `type` parameter is provided, it returns `ltANY`, signalling that all types should be returned. + +`isAccountObjectsValidType` then guards the `account_objects` handler from being asked to filter on global consensus objects (`ltAMENDMENTS`, `ltDIR_NODE`, `ltFEE_SETTINGS`, `ltLEDGER_HASHES`, `ltNEGATIVE_UNL`) that can never appear in an account's owner directory. The allow-list-by-exclusion approach means newly added account-owned types automatically become valid without any change here. + +## Order Book Asset Parsing — `parseSubUnsubJson` + +The subscribe and unsubscribe handlers pass `taker_pays` and `taker_gets` fields through `parseSubUnsubJson` to decode both the traditional `currency`/`issuer` form and the newer MPT (Multi-Purpose Token) issuance ID format. The function maps the field name to the correct pair of error codes so that the caller can distinguish a malformed source asset from a malformed destination asset — important for user-facing error messages when subscribing to order book streams. Mutually exclusive presence of `mpt_issuance_id` alongside `currency` or `issuer` fields is caught explicitly and returns `rpcINVALID_PARAMS`. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCHelpers.h.ai.json b/src/xrpld/rpc/detail/RPCHelpers.h.ai.json new file mode 100644 index 0000000000..d8d3892686 --- /dev/null +++ b/src/xrpld/rpc/detail/RPCHelpers.h.ai.json @@ -0,0 +1,100 @@ +{ + "args": [], + "classes": [], + "description": "This header file declares utility functions for XRPL RPC handling, including account object traversal, parsing account IDs, extracting seeds and keypairs from RPC parameters, validating ledger entry types, and parsing subscribe/unsubscribe parameters.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/RPCHelpers.h", + "functions": [ + { + "args": [ + "sle", + "accountID" + ], + "lineno": 22, + "name": "getStartHint" + }, + { + "args": [ + "ledger", + "sle", + "accountID" + ], + "lineno": 34, + "name": "isRelatedToAccount" + }, + { + "args": [ + "jvArray" + ], + "lineno": 47, + "name": "parseAccountIds" + }, + { + "args": [ + "limit", + "range", + "context" + ], + "lineno": 57, + "name": "readLimitField" + }, + { + "args": [ + "params", + "error" + ], + "lineno": 71, + "name": "getSeedFromRPC" + }, + { + "args": [ + "params" + ], + "lineno": 82, + "name": "parseXrplLibSeed" + }, + { + "args": [ + "params" + ], + "lineno": 91, + "name": "chooseLedgerEntryType" + }, + { + "args": [ + "type" + ], + "lineno": 104, + "name": "isAccountObjectsValidType" + }, + { + "args": [ + "params", + "error", + "apiVersion" + ], + "lineno": 115, + "name": "keypairForSignature" + }, + { + "args": [ + "asset", + "jv", + "name", + "j" + ], + "lineno": 132, + "name": "parseSubUnsubJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + }, + { + "lineno": 15, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCHelpers.h.ai.md b/src/xrpld/rpc/detail/RPCHelpers.h.ai.md new file mode 100644 index 0000000000..eaaf0f710d --- /dev/null +++ b/src/xrpld/rpc/detail/RPCHelpers.h.ai.md @@ -0,0 +1,31 @@ +# `RPCHelpers.h` — RPC Utility Functions + +This header declares the shared utility layer for XRPL's JSON-RPC server. Rather than a single cohesive abstraction, it is a purposeful collection of free functions that every RPC method handler can reach for when dealing with the three recurring concerns of any ledger query API: normalising input, traversing account-owned objects, and deriving cryptographic material from caller-supplied secrets. + +## Account Object Traversal + +Two functions serve the `account_objects` family of RPC calls. `getStartHint()` returns the numeric owner-directory position encoded inside an SLE, which is used as a resumption marker when paginating results. The subtlety is trust lines (`ltRIPPLE_STATE`): a trust line is a shared object between two accounts, so the ledger stores *two* back-pointers — `sfLowNode` and `sfHighNode`. `getStartHint()` checks which side of the trust line the querying account occupies and returns the correct one. For all other object types it simply reads `sfOwnerNode`, falling back to zero if the field is absent. + +`isRelatedToAccount()` mirrors that logic for ownership checks. It answers the question "does this SLE belong to this account?" with careful per-type rules. Trust lines again need a two-sided check. Standard objects with an `sfAccount` field additionally check `sfDestination`, which is how the server correctly surfaces Escrows, Payment Channels, and Checks that are addressed *to* the caller — those objects are listed in the destination account's owner directory. The function explicitly documents the exception: `ltNFTOKEN_OFFER` is **never** added to a destination's directory, so the destination field is intentionally ignored for that type. + +## Pagination Control + +`readLimitField()` enforces the `LimitRange` constraints declared in `Tuning.h`. The design detail worth noting is the role check: callers with `isUnlimited()` role (typically administrative or internal connections) bypass the min/max clamp entirely. This allows trusted tooling to request large result sets without the server silently truncating them. The function returns `std::optional` where `std::nullopt` signals success and a populated `Json::Value` is an inline error response — the inverse of most optional patterns, but consistent with how RPC handlers compose error responses. + +## Seed and Keypair Extraction + +`getSeedFromRPC()` enforces a strict "exactly one of three" rule: callers must supply exactly one of `passphrase`, `seed` (Base58), or `seed_hex`. Supplying zero or more than one is a hard parameter error. The three parsers are stored as a static array of `(field name, parser)` pairs, so adding a new encoding format later only requires extending that array. + +`parseXrplLibSeed()` is a compatibility shim for keys produced by the JavaScript `xrpl-lib` library, which encodes Ed25519 seeds with a non-standard two-byte prefix (`0xE1 0x4B`). The XRPL server never *produces* such encodings, but it accepts them to prevent confusing errors when users paste in keys from their JS tooling. + +`keypairForSignature()` orchestrates the full derivation pipeline. It expands the accepted secret fields to include the legacy `secret` name (used without `key_type`), and it layers in explicit algorithm selection via `key_type`. If a XrplLib Ed25519 seed is detected but the caller also passed `key_type: secp256k1`, the function returns an error rather than silently using the wrong algorithm. The function also applies an API version branch: in API v2 and later, an unknown `key_type` returns `rpcBAD_KEY_TYPE`; in v1 it falls back to the older `invalid_field_error` form for backward compatibility. A comment explains why `strcmp` is used instead of pointer equality for `jss::` string constants — MSVC may not deduplicate string literal addresses across translation units. + +## Ledger Entry Type Selection + +`chooseLedgerEntryType()` builds its lookup table at compile time via the `ledger_entries.macro` include trick. The macro is redefined inline to produce rows of `(canonical name, RPC name, LedgerEntryType enum value)`, expanding the entire protocol-defined set of entry types in one shot. Lookup accepts either the canonical name (case-insensitive) or the RPC name (case-sensitive). This design means new ledger entry types registered in `ledger_entries.macro` are automatically available to the `type` filter parameter with no manual additions needed. + +`isAccountObjectsValidType()` acts as a guard over that selection for `account_objects`. It rejects types like `ltAMENDMENTS`, `ltDIR_NODE`, `ltFEE_SETTINGS`, `ltLEDGER_HASHES`, and `ltNEGATIVE_UNL` — these are ledger-global objects that cannot be owned by an account and would never appear in an account's owner directory. + +## Subscribe/Unsubscribe Asset Parsing + +`parseSubUnsubJson()` handles the dual-asset model introduced with multi-purpose token (MPT) support. A caller may specify either a classic IOU via `currency`/`issuer` fields, or an MPT via `mpt_issuance_id`, but never both in the same object. The function rejects mixed parameters, then parses the appropriate `Asset` variant. Error codes are parameterised by whether the field being parsed is `taker_pays` or `taker_gets`, preserving the semantically distinct error codes the API has historically returned for the two sides of a book subscription. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp.ai.json b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp.ai.json new file mode 100644 index 0000000000..97ef59e807 --- /dev/null +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp.ai.json @@ -0,0 +1,505 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "ledgerFromRequest", + "ledgerFromHash | ledgerFromIndex | getLedger" + ], + "entry_point": "ledgerFromRequest", + "purpose": "Validates and resolves a ledger reference from RPC JSON parameters (ledger, ledger_hash, ledger_index) and retrieves the corresponding ledger object.", + "validation_points": [ + "ledgerFromRequest: checks for mutually exclusive fields, type checks, and dispatches to ledgerFromHash or ledgerFromIndex for further validation" + ] + }, + { + "call_chain": [ + "ledgerFromHash", + "getLedger" + ], + "entry_point": "ledgerFromHash", + "purpose": "Validates a ledger hash (hex string), parses it, and retrieves the ledger by hash.", + "validation_points": [ + "ledgerFromHash: validates hex string, parses hash" + ] + }, + { + "call_chain": [ + "ledgerFromIndex", + "getLedger" + ], + "entry_point": "ledgerFromIndex", + "purpose": "Validates a ledger index (string or number), parses it, and retrieves the ledger by index or shortcut.", + "validation_points": [ + "ledgerFromIndex: validates string/number, checks for special values (current, validated, closed), parses to uint32" + ] + }, + { + "call_chain": [ + "isValidatedOld" + ], + "entry_point": "isValidatedOld", + "purpose": "Checks if the validated ledger is considered 'old' based on age.", + "validation_points": [ + "isValidatedOld: checks standalone mode, compares ledger age" + ] + } + ], + "data_flows": [ + { + "field": "ledger", + "flow": [ + "context.params['ledger']", + "ledgerFromRequest", + "ledgerFromHash or ledgerFromIndex", + "getLedger" + ], + "origin": "context.params['ledger']", + "transformations": [ + "Type checked (string/number)", + "If string of length 64: treated as hash", + "Else: treated as index or shortcut" + ], + "validated_at": "ledgerFromRequest" + }, + { + "field": "ledger_hash", + "flow": [ + "context.params['ledger_hash']", + "ledgerFromRequest", + "ledgerFromHash", + "getLedger" + ], + "origin": "context.params['ledger_hash']", + "transformations": [ + "Type checked (string)", + "Parsed as hex string to uint256" + ], + "validated_at": "ledgerFromHash" + }, + { + "field": "ledger_index", + "flow": [ + "context.params['ledger_index']", + "ledgerFromRequest", + "ledgerFromIndex", + "getLedger" + ], + "origin": "context.params['ledger_index']", + "transformations": [ + "Type checked (string/number)", + "If string: checked for 'current', 'validated', 'closed'", + "Else: parsed as uint32" + ], + "validated_at": "ledgerFromIndex" + } + ], + "description": "This file provides helper functions for looking up and acquiring ledgers in the XRPL server, handling both JSON and gRPC contexts, validating input parameters, and returning appropriate error codes or ledger data.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger validated by jss:: (JSON Static String), template-based validation, beast::lexicalCastChecked, uint256::parseHex", + "issue_pattern": "Missing validation for ledger", + "why_false_positive": "jss:: (JSON Static String), template-based validation, beast::lexicalCastChecked, uint256::parseHex validates ledger automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash validated by jss:: (JSON Static String), template-based validation, beast::lexicalCastChecked, uint256::parseHex", + "issue_pattern": "Missing validation for ledger_hash", + "why_false_positive": "jss:: (JSON Static String), template-based validation, beast::lexicalCastChecked, uint256::parseHex validates ledger_hash automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_index validated by jss:: (JSON Static String), template-based validation, beast::lexicalCastChecked, uint256::parseHex", + "issue_pattern": "Missing validation for ledger_index", + "why_false_positive": "jss:: (JSON Static String), template-based validation, beast::lexicalCastChecked, uint256::parseHex validates ledger_index automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger_hash", + "empty", + "string", + "validation" + ], + "evidence": "ledgerFromHash (template function) at ledgerFromHash", + "issue_pattern": "Missing empty string validation for ledger_hash", + "why_false_positive": "ledgerFromHash (template function) validates ledger_hash for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger_hash", + "format", + "validation", + "invalid" + ], + "evidence": "ledgerFromHash (template function) at ledgerFromHash", + "issue_pattern": "Missing format validation for ledger_hash", + "why_false_positive": "ledgerFromHash (template function) validates ledger_hash format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "ledgerFromIndex (template function) at ledgerFromIndex", + "issue_pattern": "Missing empty string validation for ledger_index", + "why_false_positive": "ledgerFromIndex (template function) validates ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger", + "empty", + "string", + "validation" + ], + "evidence": "ledgerFromRequest (template function) at ledgerFromRequest", + "issue_pattern": "Missing empty string validation for ledger", + "why_false_positive": "ledgerFromRequest (template function) validates ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger, ledger_hash, ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "ledgerFromRequest (template function) at ledgerFromRequest", + "issue_pattern": "Missing empty string validation for ledger, ledger_hash, ledger_index", + "why_false_positive": "ledgerFromRequest (template function) validates ledger, ledger_hash, ledger_index for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp", + "functions": [ + { + "args": [ + "LedgerMaster& ledgerMaster", + "bool standalone" + ], + "lineno": 10, + "name": "isValidatedOld" + }, + { + "args": [ + "T& ledger", + "Json::Value hash", + "Context const& context", + "Json::StaticString const fieldName" + ], + "lineno": 16, + "name": "ledgerFromHash" + }, + { + "args": [ + "T& ledger", + "Json::Value indexValue", + "Context const& context", + "Json::StaticString const fieldName" + ], + "lineno": 27, + "name": "ledgerFromIndex" + }, + { + "args": [ + "T& ledger", + "JsonContext const& context" + ], + "lineno": 44, + "name": "ledgerFromRequest" + }, + { + "args": [ + "T& ledger", + "GRPCContext const& context" + ], + "lineno": 104, + "name": "ledgerFromRequest" + }, + { + "args": [ + "T& ledger", + "org::xrpl::rpc::v1::LedgerSpecifier const& specifier", + "Context const& context" + ], + "lineno": 120, + "name": "ledgerFromSpecifier" + }, + { + "args": [ + "T& ledger", + "uint256 const& ledgerHash", + "Context const& context" + ], + "lineno": 157, + "name": "getLedger" + }, + { + "args": [ + "T& ledger", + "uint32_t ledgerIndex", + "Context const& context" + ], + "lineno": 165, + "name": "getLedger" + }, + { + "args": [ + "T& ledger", + "LedgerShortcut shortcut", + "Context const& context" + ], + "lineno": 188, + "name": "getLedger" + }, + { + "args": [ + "std::shared_ptr& ledger", + "JsonContext const& context", + "Json::Value& result" + ], + "lineno": 241, + "name": "lookupLedger" + }, + { + "args": [ + "std::shared_ptr& ledger", + "JsonContext const& context" + ], + "lineno": 259, + "name": "lookupLedger" + }, + { + "args": [ + "RPC::JsonContext const& context" + ], + "lineno": 266, + "name": "getOrAcquireLedger" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 7, + "name": "RPC" + } + ], + "test_coverage_notes": "The validation logic is central to RPC ledger resolution. Typical test coverage would be in unit tests for RPC handlers (e.g., ledger, account_info, tx, etc.), likely in files such as 'test/rpc/ledger_test.cpp', 'test/rpc/account_info_test.cpp', or similar. These tests should cover valid and invalid combinations of 'ledger', 'ledger_hash', and 'ledger_index', including type errors, mutually exclusive field errors, and edge cases (e.g., invalid hash, out-of-range index, legacy field usage). Gaps may exist if there are no tests for deprecated 'ledger' field, or for malformed input types (e.g., non-string, non-number). Direct unit tests for RPCLedgerHelpers.cpp may be missing if only tested via higher-level RPC tests.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger", + "ledger_hash", + "ledger_index" + ], + "framework": "jss:: (JSON Static String), template-based validation, beast::lexicalCastChecked, uint256::parseHex", + "validation_layer": "business_logic (in RPC detail helpers, not at entry point)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "{rpcINVALID_PARAMS, expected_field_message(fieldName, \"hex string\")}", + "field": "ledger_hash", + "location": "ledgerFromHash", + "validated_by": "ledgerFromHash (template function)", + "validates": [ + "Checks if ledger_hash parses as a valid hex string (uint256::parseHex)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "{rpcINVALID_PARAMS, expected_field_message(fieldName, \"string or number\")}", + "field": "ledger_index", + "location": "ledgerFromIndex", + "validated_by": "ledgerFromIndex (template function)", + "validates": [ + "Checks if ledger_index is one of the strings: 'current', 'validated', 'closed'", + "If not, attempts to parse as uint32_t using beast::lexicalCastChecked" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "{rpcINVALID_PARAMS, expected_field_message(jss::ledger, \"string or number\")}", + "field": "ledger", + "location": "ledgerFromRequest", + "validated_by": "ledgerFromRequest (template function)", + "validates": [ + "Checks if legacy 'ledger' field is string, unsigned int, or int", + "If string and length is 64, treats as hash (delegates to ledgerFromHash)" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "{rpcINVALID_PARAMS, \"Exactly one of ... can be specified.\"}", + "field": "ledger, ledger_hash, ledger_index", + "location": "ledgerFromRequest", + "validated_by": "ledgerFromRequest (template function)", + "validates": [ + "Checks that at most one of 'ledger', 'ledger_hash', or 'ledger_index' is present" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp.ai.md b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp.ai.md new file mode 100644 index 0000000000..a8ca0a64b8 --- /dev/null +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.cpp.ai.md @@ -0,0 +1,43 @@ +# `RPCLedgerHelpers.cpp` — Ledger Resolution for RPC Handlers + +This file implements the authoritative ledger-resolution layer for both the JSON-RPC and gRPC surfaces of `rippled`. Every RPC handler that needs to operate against a specific ledger — `account_info`, `account_lines`, `ledger_data`, `ledger_entry`, and dozens more — delegates the parsing, validation, and lookup of that ledger to the functions defined here. The file's primary job is translating the many ways a caller can express "give me ledger X" (a hash, a sequence number, a named shortcut, or nothing at all) into a concrete, validated `std::shared_ptr` or `std::shared_ptr`. + +## Input Validation Architecture + +The JSON entry point is the anonymous-namespace `ledgerFromRequest(T& ledger, JsonContext const& context)`. It enforces that at most one of three fields is present in the request: `ledger` (legacy, deprecated), `ledger_hash`, or `ledger_index`. If more than one is supplied, it returns `rpcINVALID_PARAMS` immediately. The error message deliberately omits mentioning the deprecated `ledger` field when both `ledger_hash` and `ledger_index` are present, nudging callers toward the non-deprecated API. + +The legacy `ledger` field uses a heuristic to decide whether its string value is a hash or an index: a string of exactly 64 characters is treated as a hash. This is internally consistent but means a 64-digit decimal number would be misinterpreted — a reasonable trade-off for an explicitly deprecated code path. + +`ledgerFromHash()` parses the value as a hex `uint256`, returning `rpcINVALID_PARAMS` on any parse failure. `ledgerFromIndex()` accepts the canonical shortcuts `"current"`, `"validated"`, `"closed"` (with empty string treated as `"current"`), or a decimal integer parsed via `beast::lexicalCastChecked`. Any other string is rejected. + +## The `getLedger()` Overloads and Staleness Policy + +The three `getLedger()` overloads are the actual points of contact with `LedgerMaster`. Their staleness behavior is deliberately asymmetric: + +**By hash**: no staleness check at all. The caller specified an immutable object by its identity; there is no concept of "too old" for a specific historical ledger. It either exists in cache or it doesn't. + +**By sequence index**: looks up the ledger, and if found, checks whether its sequence exceeds the last validated index *while* the node's validation is stale. This prevents an out-of-sync node from serving a ledger it thinks is current but that the network has since superseded. The function also tries the current open ledger as a fallback when `getLedgerBySeq()` returns null, handling the common case where the caller asks for the ledger that's still being built. + +**By `LedgerShortcut`**: checks staleness *before* attempting lookup. For `Validated`, this means a stale node returns an error rather than returning a potentially wrong "validated" ledger. For `Current` and `Closed`, an additional sequence-gap check applies: if the shortcut ledger's sequence is more than 10 behind the last validated index, the node considers itself unsynced and returns an error. This `minSequenceGap` of 10 is a hard-coded heuristic protecting against returning data from a node that is clearly behind the network. + +The staleness check itself is `isValidatedOld()`, which compares `LedgerMaster::getValidatedLedgerAge()` against `Tuning::maxValidatedLedgerAge` (2 minutes). Crucially, in standalone mode (development/test networks with no peers) this check always returns `false`, so local-only nodes are never blocked by network-dependent staleness logic. + +The API version distinction (`context.apiVersion == 1`) maps old error codes to new ones: `rpcNO_NETWORK` / `"InsufficientNetworkMode"` for v1, `rpcNOT_SYNCED` / `"notSynced"` for v2 and later. + +## Template Design and Explicit Instantiations + +All the `getLedger()`, `ledgerFromRequest()`, and `ledgerFromSpecifier()` functions are function templates parameterized on the ledger type `T`. In practice they are only ever instantiated for `std::shared_ptr`, but the template form avoids coupling the implementation to a specific pointer type. Because the implementations live in `.cpp`, the file closes with explicit instantiations for `std::shared_ptr` (for the three `getLedger()` overloads) and for each of the three gRPC request types that need `ledgerFromRequest<>`. + +## gRPC Protocol Support + +The gRPC path enters through `ledgerFromRequest(T& ledger, GRPCContext const& context)`, which extracts the `LedgerSpecifier` protobuf field from the request and delegates to `ledgerFromSpecifier()`. That function switches on the protobuf `LedgerCase` enum — `kHash`, `kSequence`, `kShortcut`, or unset — mapping each to the same underlying `getLedger()` overloads used by the JSON path. The three explicitly instantiated gRPC request types (`GetLedgerEntryRequest`, `GetLedgerDataRequest`, `GetLedgerRequest`) all follow this exact path. + +## `lookupLedger()` — The Common Handler Entry Point + +The two `lookupLedger()` overloads are the primary interface for JSON-RPC handlers. After resolving the ledger via `ledgerFromRequest()`, they populate the JSON response with the appropriate identifying fields: closed ledgers get `ledger_hash` and `ledger_index`, while the open (current) ledger gets only `ledger_current_index`. The `validated` boolean is added by calling `LedgerMaster::isValidated()` against the resolved ledger. The two-overload design separates the "fill an existing result object" pattern from the "create and return a result" pattern, giving callers flexibility in how they compose their response JSON. + +## `getOrAcquireLedger()` — Network-Triggered Acquisition + +This function is semantically distinct from the others: it can trigger active fetching of a missing ledger from the network via `InboundLedgers::acquire()`. It is used exclusively by the `ledger_request` admin command. Unlike the other helpers, it mandates exactly one of `ledger_hash` or `ledger_index` — the deprecated `ledger` field is not supported — and returns `Expected, Json::Value>` rather than a `Status`, encoding both success value and error in a single return. + +The sequence-index path has an interesting two-step lookup: it first tries to find the target ledger's hash directly from the validated ledger's skip list via `hashOfSeq()`. If the skip list doesn't reach that far back (typical for very old ledgers), it computes a "candidate" reference ledger at an aligned boundary via `getCandidateLedger()`, acquires *that* ledger first, then uses its skip list to locate the actual target's hash. If neither step yields a result, `acquire()` schedules a network fetch and the function returns a JSON object describing the in-progress acquisition — a polling model rather than a blocking one. In standalone mode, the network acquisition path is bypassed and the function falls back to `getLedgerByHash()`, since there are no peers to fetch from. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.h.ai.json b/src/xrpld/rpc/detail/RPCLedgerHelpers.h.ai.json new file mode 100644 index 0000000000..2ad578b3fa --- /dev/null +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.h.ai.json @@ -0,0 +1,132 @@ +{ + "args": [ + { + "lineno": 23, + "name": "ledger" + }, + { + "lineno": 23, + "name": "ledgerHash" + }, + { + "lineno": 23, + "name": "context" + }, + { + "lineno": 41, + "name": "ledgerIndex" + }, + { + "lineno": 59, + "name": "shortcut" + }, + { + "lineno": 93, + "name": "result" + }, + { + "lineno": 129, + "name": "specifier" + } + ], + "classes": [ + { + "args": [], + "lineno": 16, + "name": "ReadView" + }, + { + "args": [], + "lineno": 17, + "name": "Transaction" + }, + { + "args": [], + "lineno": 20, + "name": "JsonContext" + } + ], + "description": "This header file provides utility functions for retrieving and looking up ledgers in the XRPL (XRP Ledger) system, supporting both JSON and gRPC RPC contexts. It includes templated and non-templated functions for fetching ledgers by hash, index, shortcut, or specifier, and for returning ledger data or errors in various formats.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/RPCLedgerHelpers.h", + "functions": [ + { + "args": [ + "ledger", + "ledgerHash", + "context" + ], + "lineno": 23, + "name": "getLedger" + }, + { + "args": [ + "ledger", + "ledgerIndex", + "context" + ], + "lineno": 41, + "name": "getLedger" + }, + { + "args": [ + "ledger", + "shortcut", + "context" + ], + "lineno": 59, + "name": "getLedger" + }, + { + "args": [ + "ledger", + "context" + ], + "lineno": 77, + "name": "lookupLedger" + }, + { + "args": [ + "ledger", + "context", + "result" + ], + "lineno": 93, + "name": "lookupLedger" + }, + { + "args": [ + "ledger", + "context" + ], + "lineno": 111, + "name": "ledgerFromRequest" + }, + { + "args": [ + "ledger", + "specifier", + "context" + ], + "lineno": 129, + "name": "ledgerFromSpecifier" + }, + { + "args": [ + "context" + ], + "lineno": 148, + "name": "getOrAcquireLedger" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 14, + "name": "xrpl" + }, + { + "lineno": 19, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCLedgerHelpers.h.ai.md b/src/xrpld/rpc/detail/RPCLedgerHelpers.h.ai.md new file mode 100644 index 0000000000..c245e7c73c --- /dev/null +++ b/src/xrpld/rpc/detail/RPCLedgerHelpers.h.ai.md @@ -0,0 +1,68 @@ +# `RPCLedgerHelpers.h` — RPC Ledger Resolution Interface + +This header declares the full set of ledger-lookup utilities used by XRPL's RPC layer. Its central job is translating the many ways a caller can describe a ledger — by hash, by sequence number, by shortcut string, or via a gRPC `LedgerSpecifier` — into a concrete `shared_ptr` that downstream RPC handlers can interrogate. Every public RPC command that operates on ledger state ultimately routes through one of these declarations. + +## Why a Separate File + +Ledger resolution is subtle enough to justify its own module. The same resolution logic is needed by dozens of handlers (account lookups, transaction queries, ledger data dumps, etc.), for both JSON-over-HTTP and gRPC transports, and for both a fast path that serves from in-memory caches and a slower path that can *acquire* missing ledgers over the peer network. Centralizing this logic prevents drift between the two transports and makes it easy to enforce invariants like staleness checks in one place. + +## The `getLedger` Overloads + +Three overloads cover every identification strategy: + +```cpp +template +Status getLedger(T& ledger, uint256 const& ledgerHash, Context const&); + +template +Status getLedger(T& ledger, uint32_t ledgerIndex, Context const&); + +template +Status getLedger(T& ledger, LedgerShortcut shortcut, Context const&); +``` + +The template parameter `T` is almost always `std::shared_ptr`, but the template keeps the interface open without virtual dispatch. All three return a `Status` whose `operator bool()` is truthy on failure, enabling the `if (auto status = ...)` guard pattern seen throughout the codebase. + +The implementation reveals important defensive behaviours. The hash-based overload is the simplest — it delegates directly to `LedgerMaster::getLedgerByHash()` and returns `rpcLGR_NOT_FOUND` if the ledger isn't cached. The sequence-based overload adds a fallback: if `getLedgerBySeq()` misses, it checks whether the requested sequence is the open (current) ledger, because the open ledger by definition has not yet been persisted to the sequence index. After finding a ledger by sequence, it additionally guards against network staleness: if the ledger's sequence exceeds the validated index *and* the last validated ledger is more than two minutes old (`Tuning::maxValidatedLedgerAge`), the ledger is rejected and `rpcNOT_SYNCED` is returned. This prevents a drifted node from confidently serving ledger data it cannot vouch for. + +The shortcut overload (`LedgerShortcut::Current`, `Closed`, or `Validated`) checks staleness *first* before fetching anything. For the `Validated` case it calls `LedgerMaster::getValidatedLedger()`. For `Current` and `Closed` it enforces an additional guard: if the returned ledger's sequence is more than 10 behind the validated index, it treats the node as out of sync. This `minSequenceGap` of 10 prevents situations where a node is technically running but deeply behind the chain. Assertions also verify the open/closed state invariants — `Current` must be open, `Validated` and `Closed` must not be. + +Error codes are API-version–aware throughout: API v1 callers receive the legacy `rpcNO_NETWORK` / `"InsufficientNetworkMode"` response; API v2+ callers receive `rpcNOT_SYNCED` / `"notSynced"`. This preserves backward compatibility while improving terminology for newer clients. + +## The `lookupLedger` Pair + +Two overloads serve JSON-RPC handlers: + +```cpp +Json::Value lookupLedger(std::shared_ptr&, JsonContext const&); + +Status lookupLedger(std::shared_ptr&, JsonContext const&, Json::Value& result); +``` + +The `Status`-returning variant is the canonical form: it parses the request params for `ledger_hash`, `ledger_index`, or the deprecated `ledger` field, resolves whichever was supplied, and populates `result` with `ledger_hash` and `ledger_index` (for closed ledgers) or `ledger_current_index` (for the open ledger), plus a `validated` boolean. The `Json::Value`-returning overload simply wraps this — it creates a local result, calls the canonical form, injects any error via `Status::inject()`, and returns. This two-layer design lets callers that already have a `result` object avoid an extra copy, while callers that just need a fresh response object get a one-liner. + +The deprecated `ledger` field is still supported but deliberately kept quiet in error messages — if a client mistakenly supplies both `ledger` and `ledger_hash`, the error message omits `ledger` from the "exactly one of" text to avoid encouraging its use. + +## gRPC Path: `ledgerFromRequest` and `ledgerFromSpecifier` + +```cpp +template +Status ledgerFromRequest(T& ledger, GRPCContext const& context); + +template +Status ledgerFromSpecifier(T& ledger, + org::xrpl::rpc::v1::LedgerSpecifier const&, Context const&); +``` + +The gRPC transport uses protobuf's `LedgerSpecifier` oneof, which can carry a hash bytes field, a sequence number, or one of the shortcut enum values. `ledgerFromSpecifier` switches on `ledger_case()` and fans out to the appropriate `getLedger` overload. The explicit template instantiations in the `.cpp` file cover `GetLedgerEntryRequest`, `GetLedgerDataRequest`, and `GetLedgerRequest`, keeping link-time instantiation tightly controlled. + +## `getOrAcquireLedger`: The Acquire Path + +```cpp +Expected, Json::Value> +getOrAcquireLedger(RPC::JsonContext const& context); +``` + +This function stands apart from the rest. It is designed for use cases (such as transaction proof queries) where the caller can tolerate waiting for a ledger to be fetched from the network rather than only serving from cache. It is also stricter: it accepts exactly one of `ledger_hash` or `ledger_index` with no shortcut strings and no legacy `ledger` field. The return type uses `Expected` (analogous to `std::expected`) rather than the output-parameter plus `Status` idiom, reflecting a newer API design philosophy in the codebase. + +For index-based lookup, the function cannot directly store an index→hash mapping without walking the ledger history. It calls `hashOfSeq()` on the validated ledger. If the hash cannot be found there (e.g., the sequence is very old), it falls back to `getCandidateLedger()` to find an intermediate ledger that does carry the needed hash in its skip list, then attempts to acquire that intermediate ledger via `InboundLedgers`. The function reports intermediate acquisition state in the error `Json::Value` (with an `acquiring` field), allowing callers to poll and retry. In standalone mode, where there is no peer network, it falls back to `LedgerMaster::getLedgerByHash()` directly, since the inbound ledger system is non-functional without peers. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCSub.cpp.ai.json b/src/xrpld/rpc/detail/RPCSub.cpp.ai.json new file mode 100644 index 0000000000..7282444bbf --- /dev/null +++ b/src/xrpld/rpc/detail/RPCSub.cpp.ai.json @@ -0,0 +1,431 @@ +{ + "args": [ + { + "lineno": 18, + "name": "source" + }, + { + "lineno": 19, + "name": "io_context" + }, + { + "lineno": 20, + "name": "jobQueue" + }, + { + "lineno": 21, + "name": "strUrl" + }, + { + "lineno": 22, + "name": "strUsername" + }, + { + "lineno": 23, + "name": "strPassword" + }, + { + "lineno": 24, + "name": "registry" + } + ], + "classes": [ + { + "args": [ + "source", + "io_context", + "jobQueue", + "strUrl", + "strUsername", + "strPassword", + "registry" + ], + "lineno": 11, + "name": "RPCSubImp" + } + ], + "code_paths": [ + { + "call_chain": [ + "RPCSubImp::RPCSubImp", + "parseUrl", + "manual scheme/port checks" + ], + "entry_point": "RPCSubImp::RPCSubImp", + "purpose": "Constructs a subscription object, validates and parses the URL, sets up connection parameters.", + "validation_points": [ + "parseUrl (validates strUrl)", + "manual scheme check (validates parsedURL.scheme)", + "manual port check (validates parsedURL.port)" + ] + }, + { + "call_chain": [ + "RPCSubImp::send", + "JobQueue::addJob", + "RPCSubImp::sendThread", + "RPCCall::fromNetwork" + ], + "entry_point": "RPCSubImp::send", + "purpose": "Queues and sends JSON-RPC events to the remote endpoint.", + "validation_points": [ + "No new validation; relies on constructor's validation of connection parameters." + ] + }, + { + "call_chain": [ + "RPCSubImp::setUsername" + ], + "entry_point": "RPCSubImp::setUsername", + "purpose": "Sets the username for the RPC connection.", + "validation_points": [ + "No validation." + ] + }, + { + "call_chain": [ + "RPCSubImp::setPassword" + ], + "entry_point": "RPCSubImp::setPassword", + "purpose": "Sets the password for the RPC connection.", + "validation_points": [ + "No validation." + ] + } + ], + "data_flows": [ + { + "field": "strUrl", + "flow": [ + "RPCSubImp::RPCSubImp(strUrl)", + "parseUrl(pUrl, strUrl)", + "parsedURL fields assigned to member variables (mIp, mPort, mPath, mSSL)" + ], + "origin": "Constructor argument to RPCSubImp", + "transformations": [ + "Parsed into parsedURL struct", + "Scheme checked for 'http'/'https'", + "Port defaulted if missing" + ], + "validated_at": "parseUrl and manual scheme/port checks in constructor" + }, + { + "field": "parsedURL.scheme", + "flow": [ + "parseUrl", + "manual if/else in constructor", + "sets mSSL" + ], + "origin": "Result of parseUrl", + "transformations": [ + "Checked for 'http'/'https', else error" + ], + "validated_at": "manual check in constructor" + }, + { + "field": "parsedURL.port", + "flow": [ + "parseUrl", + "manual if/else in constructor", + "sets mPort" + ], + "origin": "Result of parseUrl", + "transformations": [ + "If missing, defaulted to 443 (SSL) or 80 (non-SSL)" + ], + "validated_at": "manual check in constructor" + }, + { + "field": "mUsername", + "flow": [ + "RPCSubImp::RPCSubImp(strUsername) or setUsername(strUsername)", + "assigned to mUsername", + "used in RPCCall::fromNetwork" + ], + "origin": "Constructor argument or setUsername()", + "transformations": [ + "Direct assignment" + ], + "validated_at": "Not validated" + }, + { + "field": "mPassword", + "flow": [ + "RPCSubImp::RPCSubImp(strPassword) or setPassword(strPassword)", + "assigned to mPassword", + "used in RPCCall::fromNetwork" + ], + "origin": "Constructor argument or setPassword()", + "transformations": [ + "Direct assignment" + ], + "validated_at": "Not validated" + }, + { + "field": "jvObj", + "flow": [ + "send(jvObj)", + "queued in mDeque", + "sendThread() dequeues and adds 'seq'", + "passed to RPCCall::fromNetwork" + ], + "origin": "Argument to send()", + "transformations": [ + "seq field added before sending" + ], + "validated_at": "Not validated" + } + ], + "description": "Implements a subscription object (RPCSubImp) for JSON-RPC in the XRPL server, handling sending of JSON events to remote endpoints via HTTP(S), with support for authentication and job queueing.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "strUrl", + "empty", + "string", + "validation" + ], + "evidence": "parseUrl() at RPCSubImp constructor", + "issue_pattern": "Missing empty string validation for strUrl", + "why_false_positive": "parseUrl() validates strUrl for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "strUrl", + "format", + "validation", + "invalid" + ], + "evidence": "parseUrl() at RPCSubImp constructor", + "issue_pattern": "Missing format validation for strUrl", + "why_false_positive": "parseUrl() validates strUrl format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "parsedURL.scheme", + "empty", + "string", + "validation" + ], + "evidence": "manual check (if/else) at RPCSubImp constructor", + "issue_pattern": "Missing empty string validation for parsedURL.scheme", + "why_false_positive": "manual check (if/else) validates parsedURL.scheme for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "parsedURL.port", + "empty", + "string", + "validation" + ], + "evidence": "manual check (if/else) at RPCSubImp constructor", + "issue_pattern": "Missing empty string validation for parsedURL.port", + "why_false_positive": "manual check (if/else) validates parsedURL.port for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/RPCSub.cpp", + "functions": [ + { + "args": [ + "jvObj", + "broadcast" + ], + "lineno": 46, + "name": "send" + }, + { + "args": [ + "strUsername" + ], + "lineno": 62, + "name": "setUsername" + }, + { + "args": [ + "strPassword" + ], + "lineno": 68, + "name": "setPassword" + }, + { + "args": [], + "lineno": 75, + "name": "sendThread" + }, + { + "args": [ + "source", + "io_context", + "jobQueue", + "strUrl", + "strUsername", + "strPassword", + "registry" + ], + "lineno": 139, + "name": "make_RPCSub" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage (no test code or test macros). Likely, tests would exist in higher-level integration or unit test files for RPC subscriptions, possibly under test/rpc or test/subscription directories. The critical validation logic (URL parsing, scheme/port checks) is only exercised via construction of RPCSubImp, so tests must construct this object with various URLs to cover validation paths. There is no evidence of negative tests for invalid usernames/passwords, nor for malformed JSON events. Gaps: No validation or tests for username/password content, no explicit tests for malformed or unsupported URLs unless covered elsewhere.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom/manual validation (parseUrl, explicit checks)", + "validation_layer": "entry_point (constructor)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Failed to parse url.\")", + "field": "strUrl", + "location": "RPCSubImp constructor", + "validated_by": "parseUrl()", + "validates": [ + "URL is syntactically valid and can be parsed" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::runtime_error (\"Only http and https is supported.\")", + "field": "parsedURL.scheme", + "location": "RPCSubImp constructor", + "validated_by": "manual check (if/else)", + "validates": [ + "Scheme is either 'http' or 'https'" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (default port assigned if missing)", + "field": "parsedURL.port", + "location": "RPCSubImp constructor", + "validated_by": "manual check (if/else)", + "validates": [ + "Port is present or defaulted to 443/80 based on scheme" + ], + "validation_type": "type|range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RPCSub.cpp.ai.md b/src/xrpld/rpc/detail/RPCSub.cpp.ai.md new file mode 100644 index 0000000000..2334923d6f --- /dev/null +++ b/src/xrpld/rpc/detail/RPCSub.cpp.ai.md @@ -0,0 +1,35 @@ +# RPCSub.cpp — Outbound HTTP(S) Push Subscriptions + +## Role in the System + +`RPCSub.cpp` implements the XRPL server's ability to *push* subscription events to a remote HTTP or HTTPS endpoint. Where the normal subscription model delivers events over open WebSocket connections back to a connected client, this mechanism works in the opposite direction: the server initiates an outbound HTTP POST carrying the event payload as a JSON-RPC call. A comment in `InfoSub.h` notes it was "added for one particular partner" as a push-to-URL integration — which explains why the feature has a somewhat isolated, specialized character in an otherwise general-purpose RPC layer. + +## Class Structure and Visibility + +The implementation uses a deliberate PIMPL-like pattern. The header `RPCSub.h` exposes only the abstract base class `RPCSub` (itself extending `InfoSub`) and the `make_RPCSub()` factory function. The concrete implementation, `RPCSubImp`, lives entirely within this `.cpp` file and is never visible outside the translation unit. Callers hold `std::shared_ptr` and never see any of the internal deque, lock, or connection state. This keeps the private internals out of the ABI entirely. + +The inheritance chain is `RPCSubImp` → `RPCSub` → `InfoSub`. `InfoSub` is the framework base for all subscription objects — it manages subscription sets for accounts, ledgers, order books, and similar feeds, and provides the protected `mLock` mutex. `RPCSub` adds only the `setUsername()` and `setPassword()` virtual interface for credential management. `RPCSubImp` provides the concrete `send()` implementation that is the entire reason this class exists. + +## URL Validation at Construction Time + +The constructor eagerly validates and decomposes the target URL rather than deferring any of this work. `parseUrl()` is called immediately; a failure throws `std::runtime_error("Failed to parse url.")`. The scheme is then manually checked: `https` sets `mSSL = true`, anything other than `http` or `https` throws `std::runtime_error("Only http and https is supported.")`. If no port is present in the URL, it defaults to 443 for HTTPS or 80 for HTTP. This fail-fast approach means the `RPCSubImp` object either comes out of construction fully ready to use or doesn't exist at all — there is no partially-initialized state. + +## Asynchronous Dispatch Without a Dedicated Thread + +The design of `send()` and `sendThread()` is the most interesting aspect of the implementation. Rather than maintaining a persistent background thread per subscription, it uses the server's shared `JobQueue` on demand. + +`send()` acquires `mLock`, pushes a `(sequence_number, json_value)` pair onto `mDeque`, and then checks the `mSending` flag. If no send job is currently active, it submits a `jtCLIENT_SUBSCRIBE` job to the `JobQueue` which will call `sendThread()` on a worker thread. If `mSending` is already true, the item is simply queued and the existing job will pick it up — no second job is spawned. This single-worker-per-subscription invariant avoids ordering problems that would arise if two concurrent jobs raced to drain the same deque. + +`sendThread()` runs a `do...while` drain loop. Each iteration takes the lock only long enough to pop one event from the front of the deque (or detect that it is empty and clear `mSending`). The actual HTTP call to `RPCCall::fromNetwork()` is made *outside* the lock. This is deliberate and important: `fromNetwork()` performs a synchronous network operation on the io_context, which could block for an arbitrarily long time. Holding the lock across that call would prevent `send()` from enqueuing new events for the entire duration of the round-trip. + +Each event receives a monotonically increasing `seq` field injected by `sendThread()` before the call. The remote receiver can use this to detect dropped or reordered deliveries. + +## Concurrency Subtlety with Credentials + +`mIp`, `mPort`, `mPath`, and `mSSL` are set once in the constructor and never mutated, making them safe to read from `sendThread()` without the lock. However, `mUsername` and `mPassword` can be updated at any time via `setUsername()` and `setPassword()`, both of which correctly acquire `mLock` before mutating those fields. The read of `mUsername` and `mPassword` in `sendThread()` happens outside the lock, creating a potential data race under the C++ memory model. In practice, string assignment is unlikely to cause observable corruption, and credential updates are an uncommon operation, but this is a real inconsistency in the locking discipline that the existing `XXX` comment nearby hints at. + +## Relationship to RPCCall::fromNetwork + +`RPCCall::fromNetwork()` takes a `boost::asio::io_context&` to schedule the outbound HTTP request. This is why `RPCSubImp` stores and passes through the io_context — a coupling that the header author found architecturally uncomfortable enough to annotate with `// VFALCO Why is the io_context needed?`. The answer lies in how `fromNetwork` works: it creates a Boost.Asio async HTTP session that must be associated with a running io_context. The subscription does not own the io_context; it merely borrows a reference to the one managed by the server's network layer. + +The call to `fromNetwork()` passes `"event"` as the method name and `true` for the `quiet` flag, suppressing verbose logging on the call site. No response callback is registered, so the send is effectively fire-and-forget: if the remote endpoint rejects the call or returns an error, the exception is caught and logged, but no retry or backpressure mechanism exists. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RippleLineCache.cpp.ai.json b/src/xrpld/rpc/detail/RippleLineCache.cpp.ai.json new file mode 100644 index 0000000000..c1b4437178 --- /dev/null +++ b/src/xrpld/rpc/detail/RippleLineCache.cpp.ai.json @@ -0,0 +1,170 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RippleLineCache::RippleLineCache", + "parseBase58", + "RippleLineCache::getLedger" + ], + "entry_point": "RippleLineCache::RippleLineCache", + "purpose": "Constructs a RippleLineCache object, validates the accountID, and sets up the ledger reference.", + "validation_points": [ + "parseBase58 (validates accountID)", + "RippleLineCache::getLedger (checks ledger_ != nullptr)" + ] + }, + { + "call_chain": [ + "RippleLineCache::getRippleLines", + "parseBase58", + "RippleLineCache::getLedger" + ], + "entry_point": "RippleLineCache::getRippleLines", + "purpose": "Retrieves trust lines for a given account, validating the accountID and ensuring the ledger is valid.", + "validation_points": [ + "parseBase58 (validates accountID)", + "RippleLineCache::getLedger (checks ledger_ != nullptr)" + ] + } + ], + "data_flows": [ + { + "field": "accountID", + "flow": [ + "RPC request", + "parseBase58", + "RippleLineCache/account context", + "Used in trust line lookup" + ], + "origin": "Input parameter to RippleLineCache or getRippleLines (typically from RPC request)", + "transformations": [ + "String base58 \u2192 AccountID object via parseBase58" + ], + "validated_at": "parseBase58" + }, + { + "field": "ledger_", + "flow": [ + "RPC context/ledger lookup", + "RippleLineCache::RippleLineCache", + "Stored as member ledger_", + "Checked in getLedger and used for trust line queries" + ], + "origin": "Passed to RippleLineCache constructor (from RPC context or ledger lookup)", + "transformations": [ + "Pointer/reference assignment" + ], + "validated_at": "getLedger (ledger_ != nullptr)" + } + ], + "description": "No source code provided; the file is empty.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "accountID", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at RippleLineCache::getLineCache", + "issue_pattern": "Missing empty string validation for accountID", + "why_false_positive": "parseBase58 validates accountID for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "accountID", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at RippleLineCache::getLineCache", + "issue_pattern": "Missing format validation for accountID", + "why_false_positive": "parseBase58 validates accountID format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger", + "empty", + "string", + "validation" + ], + "evidence": "ledger_ != nullptr at RippleLineCache::RippleLineCache (constructor)", + "issue_pattern": "Missing empty string validation for ledger", + "why_false_positive": "ledger_ != nullptr validates ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger", + "type", + "validation", + "check" + ], + "evidence": "ledger_ != nullptr at RippleLineCache::RippleLineCache (constructor)", + "issue_pattern": "Missing type validation for ledger", + "why_false_positive": "ledger_ != nullptr validates ledger type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/RippleLineCache.cpp", + "functions": [], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [], + "test_coverage_notes": "RippleLineCache and related validation logic are typically tested in unit tests under 'test/rpc' or 'test/app/ledger'. Tests likely cover valid and invalid accountID parsing and ledger presence. However, edge cases such as malformed base58 strings, null ledger pointers, or unusual ledger states may not be exhaustively tested. Look for tests in files like 'RippleLineCache_test.cpp', 'RPCHandler_test.cpp', or integration tests for trust line RPCs. Gaps may exist for negative validation paths and error handling.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (no external validation framework detected)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns boost::none on failure)", + "field": "accountID", + "location": "RippleLineCache::getLineCache", + "validated_by": "parseBase58", + "validates": [ + "Checks if the input string is a valid Base58-encoded AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "assertion failure (assert(ledger_))", + "field": "ledger", + "location": "RippleLineCache::RippleLineCache (constructor)", + "validated_by": "ledger_ != nullptr", + "validates": [ + "Ensures ledger pointer is not null" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RippleLineCache.cpp.ai.md b/src/xrpld/rpc/detail/RippleLineCache.cpp.ai.md new file mode 100644 index 0000000000..3228fc88f7 --- /dev/null +++ b/src/xrpld/rpc/detail/RippleLineCache.cpp.ai.md @@ -0,0 +1,15 @@ +# `RippleLineCache.cpp` + +This file is empty — it contains no source code. It is a 0-byte stub that has been fully gutted, retaining only its filename and build-system entry point. + +## Historical Role + +Based on the name, adjacent files, and preserved AI metadata, `RippleLineCache` was the original per-request, ledger-scoped cache for trust line (`RippleState`) objects consumed by the pathfinding subsystem. Its interface, reconstructable from the metadata, included a constructor accepting a ledger reference, a `getLedger()` accessor, and a `getRippleLines(AccountID)` method that returned a vector of trust lines for a given account. The motivation was straightforward: path-finding over the trust-line graph requires many lookups of the same account's lines, so a session-level cache avoided repeated ledger reads for any single path search. + +## What Replaced It + +This functionality is now handled entirely by `AssetCache` (`AssetCache.h` / `AssetCache.cpp` in the same directory). `AssetCache` generalises the original concept in two significant ways. First, it caches both trust lines (via `getRippleLines()`) and MPT (Multi-Purpose Token) issuance entries (via `getMPTs()`), reflecting the ledger's evolution to support alternative token standards. Second, `getRippleLines()` accepts a `LineDirection` parameter so that the pathfinder can request either the full set of an account's trust lines (when the account is an "outgoing" node — a source or a rippling-enabled hop) or only the subset with rippling enabled on the account's own side (when the account is "incoming"). The cache key is an `AccountKey` struct that bundles `AccountID`, `LineDirection`, and a precomputed `hardened_hash` value. Entries are stored as `shared_ptr>` inside a `hash_map`, so entries can be removed without invalidating live references held by concurrent readers. Access is serialised with a `std::mutex`. + +## Current Status + +Both `RippleLineCache.cpp` and `RippleLineCache.h` are empty. Despite this, two files still `#include` the now-empty header: `Pathfinder.cpp` and `PathRequestManager.h`. Because the header provides no declarations, those includes are entirely inert — they compile cleanly but contribute nothing. `PathRequestManager.h` simultaneously includes `AssetCache.h` and declares `assetCache_` as a `std::weak_ptr`, confirming that `AssetCache` is the live implementation. No translation unit anywhere in the repository references the `RippleLineCache` type by name; the only occurrences are in AI metadata files and the empty source stubs themselves. The files are safe-to-delete dead weight, likely retained to avoid breaking a build-system glob or as an artefact of the refactoring that produced `AssetCache`. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RippleLineCache.h.ai.json b/src/xrpld/rpc/detail/RippleLineCache.h.ai.json new file mode 100644 index 0000000000..c351765390 --- /dev/null +++ b/src/xrpld/rpc/detail/RippleLineCache.h.ai.json @@ -0,0 +1,9 @@ +{ + "args": [], + "classes": [], + "description": "No source code provided; the file is empty.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/RippleLineCache.h", + "functions": [], + "language": "c header", + "namespaces": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/RippleLineCache.h.ai.md b/src/xrpld/rpc/detail/RippleLineCache.h.ai.md new file mode 100644 index 0000000000..4ee2be15c1 --- /dev/null +++ b/src/xrpld/rpc/detail/RippleLineCache.h.ai.md @@ -0,0 +1,15 @@ +# `RippleLineCache.h` + +This file is empty — it contains zero bytes. Both `RippleLineCache.h` and its companion `RippleLineCache.cpp` are 0-byte stub files that have been fully gutted, leaving only their filenames in the repository. + +## Historical Role + +Based on the name, sibling files, and preserved AI metadata, `RippleLineCache` formerly provided a ledger-scoped, in-memory cache of trust line (`RippleState`) objects for use by the pathfinding subsystem. Its interface included a constructor that accepted a ledger reference, a `getLedger()` accessor, and a `getRippleLines(AccountID)` method that returned a vector of trust lines for a given account — avoiding repeated ledger reads during the computationally intensive path search. + +## What Replaced It + +This functionality now lives in `AssetCache` (`AssetCache.h` / `AssetCache.cpp` in the same directory). `AssetCache` expands the old concept in two ways: it caches both trust lines (via `getRippleLines()`) and MPT (Multi-Purpose Token) entries (via `getMPTs()`), and it introduces a `LineDirection` parameter to `getRippleLines()` so that the pathfinder can request only the subset of trust lines usable in a given traversal direction — returning all lines for an "outgoing" account but filtering to rippling-enabled lines only for "incoming" accounts. The cache key is an `AccountKey` struct combining `AccountID`, `LineDirection`, and a precomputed `hardened_hash` value, stored in an `unordered_map` backed by `shared_ptr>` entries to allow safe concurrent removal. + +## Current Status + +No source file in the repository `#include`s either stub file. Grepping the entire codebase for `RippleLineCache` returns only the two empty source files and their AI metadata — no consumer translation units. The files are safe-to-delete dead weight, likely retained as a historical artifact of the refactoring that produced `AssetCache`, or to avoid breaking a build-system glob that enumerates `.cpp` files by pattern. Any engineer encountering these files should confirm deletion safety with `git log` to recover the original implementation history. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Role.cpp.ai.json b/src/xrpld/rpc/detail/Role.cpp.ai.json new file mode 100644 index 0000000000..f1c49ce21d --- /dev/null +++ b/src/xrpld/rpc/detail/Role.cpp.ai.json @@ -0,0 +1,385 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "requestRole", + "isAdmin", + [ + "ipAllowed", + "passwordUnrequiredOrSentCorrect" + ] + ], + "entry_point": "requestRole", + "purpose": "Determines the role (ADMIN, IDENTIFIED, PROXY, GUEST, FORBID) for an incoming request based on IP, credentials, and required role.", + "validation_points": [ + "ipAllowed: Validates remoteIp against admin_nets_v4/v6", + "passwordUnrequiredOrSentCorrect: Validates admin_user/admin_password in params" + ] + }, + { + "call_chain": [ + "isAdmin", + [ + "ipAllowed", + "passwordUnrequiredOrSentCorrect" + ] + ], + "entry_point": "isAdmin", + "purpose": "Checks if the request is from an admin by validating IP and credentials.", + "validation_points": [ + "ipAllowed: Validates remoteIp against admin_nets_v4/v6", + "passwordUnrequiredOrSentCorrect: Validates admin_user/admin_password in params" + ] + }, + { + "call_chain": [ + "isUnlimited (Role, Port, Json::Value, Endpoint, string)", + "requestRole", + "isAdmin", + [ + "ipAllowed", + "passwordUnrequiredOrSentCorrect" + ] + ], + "entry_point": "isUnlimited", + "purpose": "Checks if a request should have unlimited resources by determining its role.", + "validation_points": [ + "ipAllowed: Validates remoteIp against admin_nets_v4/v6", + "passwordUnrequiredOrSentCorrect: Validates admin_user/admin_password in params" + ] + }, + { + "call_chain": [ + "requestInboundEndpoint", + "isUnlimited" + ], + "entry_point": "requestInboundEndpoint", + "purpose": "Creates a resource consumer for an inbound endpoint, using role and user info.", + "validation_points": [ + "isUnlimited: Validates role (which is determined by requestRole and thus by ipAllowed/passwordUnrequiredOrSentCorrect)" + ] + } + ], + "data_flows": [ + { + "field": "params['admin_password']", + "flow": [ + "params['admin_password'] (input)", + "passwordUnrequiredOrSentCorrect", + "isAdmin", + "requestRole" + ], + "origin": "JSON RPC request parameters", + "transformations": [ + "Checked with isString()", + "Compared to port.admin_password" + ], + "validated_at": "passwordUnrequiredOrSentCorrect" + }, + { + "field": "params['admin_user']", + "flow": [ + "params['admin_user'] (input)", + "passwordUnrequiredOrSentCorrect", + "isAdmin", + "requestRole" + ], + "origin": "JSON RPC request parameters", + "transformations": [ + "Checked with isString()", + "Compared to port.admin_user" + ], + "validated_at": "passwordUnrequiredOrSentCorrect" + }, + { + "field": "remoteIp", + "flow": [ + "remoteIp (input)", + "ipAllowed", + "isAdmin", + "requestRole" + ], + "origin": "beast::IP::Endpoint from incoming connection", + "transformations": [ + "Converted to string", + "Appended with /32 or /128", + "Converted to boost::asio::ip::network_v4/v6", + "Checked with is_subnet_of or ==" + ], + "validated_at": "ipAllowed" + }, + { + "field": "user", + "flow": [ + "user (input)", + "requestRole" + ], + "origin": "Extracted from request (e.g., HTTP Basic Auth)", + "transformations": [ + "Checked for empty()" + ], + "validated_at": "requestRole (if !user.empty() for IDENTIFIED role)" + }, + { + "field": "port.admin_nets_v4 / port.admin_nets_v6", + "flow": [ + "port.admin_nets_v4/v6 (config)", + "XRPL_ASSERT in passwordUnrequiredOrSentCorrect", + "ipAllowed" + ], + "origin": "Server configuration", + "transformations": [ + "Checked for empty() in XRPL_ASSERT", + "Used as subnet list in ipAllowed" + ], + "validated_at": "XRPL_ASSERT in passwordUnrequiredOrSentCorrect, ipAllowed" + } + ], + "description": "This file provides utility functions for determining user roles and permissions (such as admin, guest, proxy, identified) based on IP address, HTTP request headers, and authentication parameters in the context of the XRPL server's RPC interface.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "port.admin_nets_v4 and port.admin_nets_v6", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT macro at passwordUnrequiredOrSentCorrect", + "issue_pattern": "Missing empty string validation for port.admin_nets_v4 and port.admin_nets_v6", + "why_false_positive": "XRPL_ASSERT macro validates port.admin_nets_v4 and port.admin_nets_v6 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "params['admin_password']", + "empty", + "string", + "validation" + ], + "evidence": "isString() method and string comparison at passwordUnrequiredOrSentCorrect", + "issue_pattern": "Missing empty string validation for params['admin_password']", + "why_false_positive": "isString() method and string comparison validates params['admin_password'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "params['admin_user']", + "empty", + "string", + "validation" + ], + "evidence": "isString() method and string comparison at passwordUnrequiredOrSentCorrect", + "issue_pattern": "Missing empty string validation for params['admin_user']", + "why_false_positive": "isString() method and string comparison validates params['admin_user'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "remoteIp (beast::IP::Address)", + "empty", + "string", + "validation" + ], + "evidence": "boost::asio::ip::make_network_v4/make_network_v6, is_subnet_of, == at ipAllowed", + "issue_pattern": "Missing empty string validation for remoteIp (beast::IP::Address)", + "why_false_positive": "boost::asio::ip::make_network_v4/make_network_v6, is_subnet_of, == validates remoteIp (beast::IP::Address) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "user (string_view)", + "empty", + "string", + "validation" + ], + "evidence": "empty() check at requestRole", + "issue_pattern": "Missing empty string validation for user (string_view)", + "why_false_positive": "empty() check validates user (string_view) for empty strings" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/Role.cpp", + "functions": [ + { + "args": [ + "Port const& port", + "Json::Value const& params" + ], + "lineno": 6, + "name": "passwordUnrequiredOrSentCorrect" + }, + { + "args": [ + "beast::IP::Address const& remoteIp", + "std::vector const& nets4", + "std::vector const& nets6" + ], + "lineno": 19, + "name": "ipAllowed" + }, + { + "args": [ + "Port const& port", + "Json::Value const& params", + "beast::IP::Address const& remoteIp" + ], + "lineno": 49, + "name": "isAdmin" + }, + { + "args": [ + "Role const& required", + "Port const& port", + "Json::Value const& params", + "beast::IP::Endpoint const& remoteIp", + "std::string_view user" + ], + "lineno": 54, + "name": "requestRole" + }, + { + "args": [ + "Role const& role" + ], + "lineno": 74, + "name": "isUnlimited" + }, + { + "args": [ + "Role const& required", + "Port const& port", + "Json::Value const& params", + "beast::IP::Endpoint const& remoteIp", + "std::string const& user" + ], + "lineno": 80, + "name": "isUnlimited" + }, + { + "args": [ + "Resource::Manager& manager", + "beast::IP::Endpoint const& remoteAddress", + "Role const& role", + "std::string_view user", + "std::string_view forwardedFor" + ], + "lineno": 87, + "name": "requestInboundEndpoint" + }, + { + "args": [ + "std::string_view field" + ], + "lineno": 97, + "name": "extractIpAddrFromField" + }, + { + "args": [ + "http_request_type const& request" + ], + "lineno": 176, + "name": "forwardedFor" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is critical for security (admin access, IP whitelisting, credential checks). Typical test coverage would be in unit tests for the RPC layer, possibly in files like 'test/rpc/Role_test.cpp', 'test/rpc/ServerHandler_test.cpp', or integration tests for admin endpoints. Gaps may exist if tests do not cover edge cases such as malformed IPs, empty admin nets, incorrect/missing credentials, or IPv6 subnets. There is no evidence in this file of direct test hooks or test-only code, so coverage depends on external test suites. The XRPL_ASSERT macro may not be tested unless assertions are enabled in test builds.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom logic, boost::asio, XRPL_ASSERT, Json::Value", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Assertion failure (likely aborts or logs error)", + "field": "port.admin_nets_v4 and port.admin_nets_v6", + "location": "passwordUnrequiredOrSentCorrect", + "validated_by": "XRPL_ASSERT macro", + "validates": [ + "Checks that at least one of admin_nets_v4 or admin_nets_v6 is non-empty" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns false if invalid)", + "field": "params['admin_password']", + "location": "passwordUnrequiredOrSentCorrect", + "validated_by": "isString() method and string comparison", + "validates": [ + "Checks that params['admin_password'] is a string", + "Checks that params['admin_password'] matches port.admin_password" + ], + "validation_type": "type, business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns false if invalid)", + "field": "params['admin_user']", + "location": "passwordUnrequiredOrSentCorrect", + "validated_by": "isString() method and string comparison", + "validates": [ + "Checks that params['admin_user'] is a string", + "Checks that params['admin_user'] matches port.admin_user" + ], + "validation_type": "type, business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns false if not allowed)", + "field": "remoteIp (beast::IP::Address)", + "location": "ipAllowed", + "validated_by": "boost::asio::ip::make_network_v4/make_network_v6, is_subnet_of, ==", + "validates": [ + "Checks if remoteIp is in any of the allowed IPv4 or IPv6 subnets" + ], + "validation_type": "business_logic, format" + }, + { + "confidence": 1.0, + "error_thrown": "None (role changes based on presence)", + "field": "user (string_view)", + "location": "requestRole", + "validated_by": "empty() check", + "validates": [ + "Checks if user is non-empty to assign IDENTIFIED role" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Role.cpp.ai.md b/src/xrpld/rpc/detail/Role.cpp.ai.md new file mode 100644 index 0000000000..0813c355ee --- /dev/null +++ b/src/xrpld/rpc/detail/Role.cpp.ai.md @@ -0,0 +1,51 @@ +# `src/xrpld/rpc/detail/Role.cpp` — RPC Access Control and Role Assignment + +This file implements the access-control layer that sits at the entry point of every inbound RPC and WebSocket connection. Its job is to classify a connecting client into one of five privilege levels — `GUEST`, `PROXY`, `IDENTIFIED`, `ADMIN`, or `FORBID` — and then hand off to the resource-management layer with the appropriate throttling policy. It is the first line of defence against unauthorized use of privileged RPC commands and against resource exhaustion. + +## The Role Taxonomy + +The `Role` enum (declared in `Role.h`) establishes a hierarchy of trust: + +- **GUEST** — an anonymous connection from an unrecognized IP. Subject to resource limits. +- **PROXY** — a connection forwarded through a configured `secure_gateway` reverse proxy, but without a user identity in the forwarded headers. Subject to limits based on the real client IP. +- **IDENTIFIED** — a connection through a trusted proxy that *did* supply a user identity. Granted unlimited resources, but still restricted from a few administrative RPC commands. +- **ADMIN** — a connection that passed both the IP whitelist and optional credential check. Unlimited resources and full RPC access. +- **FORBID** — returned when an operation required `ADMIN` but the caller did not qualify. This sentinel value lets callers short-circuit without separate admin-check logic. + +## Role Determination Pipeline + +The central function is `requestRole()`. It evaluates the incoming connection in strict priority order: + +1. Call `isAdmin()`, which in turn calls `ipAllowed()` against `port.admin_nets_v4`/`admin_nets_v6` and then `passwordUnrequiredOrSentCorrect()`. If both pass, return `Role::ADMIN` immediately. +2. If the `required` parameter is `Role::ADMIN` but step 1 failed, return `Role::FORBID`. This prevents privilege escalation without exposing a detailed reason. +3. Call `ipAllowed()` against `port.secure_gateway_nets_v4`/`secure_gateway_nets_v6`. If the remote IP belongs to a trusted proxy network, return `Role::IDENTIFIED` when a non-empty `user` string was extracted from the request headers, or `Role::PROXY` otherwise. +4. Fall through to `Role::GUEST`. + +The `required` parameter is a declared minimum. The caller asserts what privilege level its operation needs; `requestRole()` either meets or denies that bar. This is more composable than scattering `isAdmin()` calls across handlers. + +## IP Subnet Matching + +`ipAllowed()` avoids simple string comparison in favour of proper CIDR semantics. It converts the remote address to a host-prefix network (`/32` for IPv4, `/128` for IPv6) using `boost::asio::ip::make_network_v4/v6`, then tests `is_subnet_of(configured_net) || (host_net == configured_net)` against every entry in the configured lists. The dual check is needed because Boost's `is_subnet_of` treats a /32 as a proper subnet of a /24 but two identical /32s as equal-but-not-a-subnet; the `||` handles the edge case where the admin network itself is configured as a single host address. + +## Credential Validation + +`passwordUnrequiredOrSentCorrect()` reflects an important operational choice: credentials are optional. If neither `admin_user` nor `admin_password` is set on the port, then any request from an admin-whitelisted IP is accepted. If either field is set, both must be present in the JSON `params` as string values matching the port configuration exactly. The function carries a precondition enforced by `XRPL_ASSERT`: it should only be called when the admin net list is non-empty, which is guaranteed by the `isAdmin()` call chain. + +Credentials are sent in-band in the JSON-RPC body (`params["admin_user"]`, `params["admin_password"]`), not in HTTP headers. The `isString()` guard before the comparison ensures a type-confusion attack through JSON cannot bypass the check with a non-string value. + +## Resource Consumer Allocation + +`requestInboundEndpoint()` bridges role determination to the `Resource::Manager`. Unlimited roles (`ADMIN` and `IDENTIFIED`) receive a `newUnlimitedEndpoint` consumer that bypasses all rate-limiting accounting. All others go through `newInboundEndpoint`, which takes a boolean flag indicating whether the connection is proxied and the `forwardedFor` string so that rate limiting is applied to the *real* client IP rather than the proxy's address. + +## Forwarded-For Header Parsing + +`forwardedFor()` and the file-local `extractIpAddrFromField()` exist because reverse proxies are first-class citizens in the XRPL deployment model. The function supports both standards: + +- **RFC 7239 `Forwarded:`** — scans for `for=` (case-insensitively), then extracts up to the next `,` or `;` separator to isolate the first hop. +- **Legacy `X-Forwarded-For:`** — takes only the first comma-delimited entry, which by convention is the originating client. + +`extractIpAddrFromField()` is deliberately defensive. It handles leading/trailing whitespace (including CRLF), double-quoted addresses (legal in RFC 7239), IPv6 addresses wrapped in square brackets, and IPv4 addresses with an appended port number (`:8080`). The IPv6 detection heuristic — skip leading hex digits, and if the next character is a colon it must be IPv6 — correctly avoids stripping the address when it has no port suffix. Invalid or malformed inputs return an empty `string_view` rather than throwing, so a malformed header never breaks the role-assignment path. + +## Relationship to the Broader RPC System + +In `ServerHandler.cpp`, every new HTTP and WebSocket connection immediately calls `requestRole()` followed by `requestInboundEndpoint()`, embedding the resulting `Resource::Consumer` into the session object before any RPC dispatch occurs. This ensures throttling is installed unconditionally and cannot be bypassed by arriving on an unusual code path. Individual RPC command handlers subsequently inspect the role to gate privileged operations, but they depend entirely on this file having set the role correctly at connection time. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp.ai.json b/src/xrpld/rpc/detail/ServerHandler.cpp.ai.json new file mode 100644 index 0000000000..237acb6ca4 --- /dev/null +++ b/src/xrpld/rpc/detail/ServerHandler.cpp.ai.json @@ -0,0 +1,805 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 24, + "name": "Peer" + }, + { + "args": [], + "lineno": 25, + "name": "LedgerMaster" + }, + { + "args": [], + "lineno": 26, + "name": "Transaction" + }, + { + "args": [], + "lineno": 27, + "name": "ValidatorKeys" + }, + { + "args": [], + "lineno": 28, + "name": "CanonicalTXSet" + } + ], + "code_paths": [ + { + "call_chain": [ + "ServerHandler::onHandoff", + "isStatusRequest", + "authorized" + ], + "entry_point": "ServerHandler::onHandoff", + "purpose": "Handles incoming HTTP requests, determines if the request is a status request, and validates HTTP Authorization.", + "validation_points": [ + "isStatusRequest: Validates HTTP method, version, target, and body.", + "authorized: Validates Authorization header, base64 decodes credentials, checks for colon separator, compares user/pass." + ] + }, + { + "call_chain": [ + "ServerHandler::onAccept", + "authorized" + ], + "entry_point": "ServerHandler::onAccept", + "purpose": "Handles new connections, validates HTTP Authorization header.", + "validation_points": [ + "authorized: Validates Authorization header, base64 decodes credentials, checks for colon separator, compares user/pass." + ] + } + ], + "data_flows": [ + { + "field": "HTTP Authorization header", + "flow": [ + "HTTP request received", + "headers extracted into std::map", + "passed to authorized()" + ], + "origin": "Incoming HTTP request headers", + "transformations": [ + "Header value is trimmed", + "Base64 portion extracted (after 'Basic ')", + "Base64 decoded to user:pass string", + "Split at ':' to get user and password" + ], + "validated_at": "authorized" + }, + { + "field": "HTTP method, version, target, body", + "flow": [ + "HTTP request received", + "passed to isStatusRequest()" + ], + "origin": "Incoming HTTP request object", + "transformations": [ + "Checked for GET method", + "Checked for HTTP/1.1+ version", + "Checked for '/' target", + "Checked for empty body" + ], + "validated_at": "isStatusRequest" + }, + { + "field": "Base64-encoded credentials", + "flow": [ + "Extracted from header", + "Trimmed", + "Passed to base64_decode" + ], + "origin": "Authorization header value", + "transformations": [ + "Base64 decoded to user:pass string" + ], + "validated_at": "authorized (via base64_decode)" + }, + { + "field": "Decoded credentials format", + "flow": [ + "Decoded string", + "std::string::find(':') to locate separator" + ], + "origin": "Result of base64_decode", + "transformations": [ + "Split into user and password" + ], + "validated_at": "authorized (colon check)" + } + ], + "description": "Implements the ServerHandler for XRPL, handling HTTP/WebSocket RPC requests, authorization, session management, and server setup/configuration.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "HTTP method", + "validation", + "missing", + "check" + ], + "evidence": "Field HTTP method validated by Custom validation, base64_decode, boost::beast::http", + "issue_pattern": "Missing validation for HTTP method", + "why_false_positive": "Custom validation, base64_decode, boost::beast::http validates HTTP method automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "HTTP version", + "validation", + "missing", + "check" + ], + "evidence": "Field HTTP version validated by Custom validation, base64_decode, boost::beast::http", + "issue_pattern": "Missing validation for HTTP version", + "why_false_positive": "Custom validation, base64_decode, boost::beast::http validates HTTP version automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "HTTP target", + "validation", + "missing", + "check" + ], + "evidence": "Field HTTP target validated by Custom validation, base64_decode, boost::beast::http", + "issue_pattern": "Missing validation for HTTP target", + "why_false_positive": "Custom validation, base64_decode, boost::beast::http validates HTTP target automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "HTTP body", + "validation", + "missing", + "check" + ], + "evidence": "Field HTTP body validated by Custom validation, base64_decode, boost::beast::http", + "issue_pattern": "Missing validation for HTTP body", + "why_false_positive": "Custom validation, base64_decode, boost::beast::http validates HTTP body automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Authorization header", + "validation", + "missing", + "check" + ], + "evidence": "Field Authorization header validated by Custom validation, base64_decode, boost::beast::http", + "issue_pattern": "Missing validation for Authorization header", + "why_false_positive": "Custom validation, base64_decode, boost::beast::http validates Authorization header automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "HTTP Authorization header", + "empty", + "string", + "validation" + ], + "evidence": "authorized (custom function) at authorized", + "issue_pattern": "Missing empty string validation for HTTP Authorization header", + "why_false_positive": "authorized (custom function) validates HTTP Authorization header for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "HTTP request method, version, target, body", + "empty", + "string", + "validation" + ], + "evidence": "isStatusRequest (custom function) at isStatusRequest", + "issue_pattern": "Missing empty string validation for HTTP request method, version, target, body", + "why_false_positive": "isStatusRequest (custom function) validates HTTP request method, version, target, body for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "Base64-encoded credentials", + "empty", + "string", + "validation" + ], + "evidence": "base64_decode (xrpl::base64_decode) at authorized", + "issue_pattern": "Missing empty string validation for Base64-encoded credentials", + "why_false_positive": "base64_decode (xrpl::base64_decode) validates Base64-encoded credentials for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Base64-encoded credentials", + "format", + "validation", + "invalid" + ], + "evidence": "base64_decode (xrpl::base64_decode) at authorized", + "issue_pattern": "Missing format validation for Base64-encoded credentials", + "why_false_positive": "base64_decode (xrpl::base64_decode) validates Base64-encoded credentials format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Decoded credentials format", + "empty", + "string", + "validation" + ], + "evidence": "std::string::find(':') at authorized", + "issue_pattern": "Missing empty string validation for Decoded credentials format", + "why_false_positive": "std::string::find(':') validates Decoded credentials format for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Decoded credentials format", + "format", + "validation", + "invalid" + ], + "evidence": "std::string::find(':') at authorized", + "issue_pattern": "Missing format validation for Decoded credentials format", + "why_false_positive": "std::string::find(':') validates Decoded credentials format format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::unique_ptr" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Missing null check for std::unique_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::unique_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/ServerHandler.cpp", + "functions": [ + { + "args": [ + "http_request_type const& request" + ], + "lineno": 27, + "name": "isStatusRequest" + }, + { + "args": [ + "http_request_type const& request", + "boost::beast::http::status status" + ], + "lineno": 33, + "name": "statusRequestResponse" + }, + { + "args": [ + "Port const& port", + "std::map const& h" + ], + "lineno": 48, + "name": "authorized" + }, + { + "args": [ + "ServerHandlerCreator const&", + "Application& app", + "boost::asio::io_context& io_context", + "JobQueue& jobQueue", + "NetworkOPs& networkOPs", + "Resource::Manager& resourceManager", + "CollectorManager& cm" + ], + "lineno": 66, + "name": "ServerHandler" + }, + { + "args": [], + "lineno": 80, + "name": "~ServerHandler" + }, + { + "args": [ + "Setup const& setup", + "beast::Journal journal" + ], + "lineno": 83, + "name": "setup" + }, + { + "args": [], + "lineno": 101, + "name": "stop" + }, + { + "args": [ + "Session& session", + "boost::asio::ip::tcp::endpoint endpoint" + ], + "lineno": 113, + "name": "onAccept" + }, + { + "args": [ + "Session& session", + "std::unique_ptr&& bundle", + "http_request_type&& request", + "boost::asio::ip::tcp::endpoint const& remote_address" + ], + "lineno": 126, + "name": "onHandoff" + }, + { + "args": [ + "Session& session" + ], + "lineno": 163, + "name": "makeOutput" + }, + { + "args": [ + "boost::beast::http::fields const& h" + ], + "lineno": 168, + "name": "build_map" + }, + { + "args": [ + "ConstBufferSequence const& bs" + ], + "lineno": 179, + "name": "buffers_to_string" + }, + { + "args": [ + "Session& session" + ], + "lineno": 189, + "name": "onRequest" + }, + { + "args": [ + "std::shared_ptr session", + "std::vector const& buffers" + ], + "lineno": 210, + "name": "onWSMessage" + }, + { + "args": [ + "Session& session", + "boost::system::error_code const&" + ], + "lineno": 241, + "name": "onClose" + }, + { + "args": [ + "Server&" + ], + "lineno": 246, + "name": "onStopped" + }, + { + "args": [ + "Json::Value const& request", + "T const& duration", + "beast::Journal& journal" + ], + "lineno": 254, + "name": "logDuration" + }, + { + "args": [ + "std::shared_ptr const& session", + "std::shared_ptr const& coro", + "Json::Value const& jv" + ], + "lineno": 267, + "name": "processSession" + }, + { + "args": [ + "std::shared_ptr const& session", + "std::shared_ptr coro" + ], + "lineno": 340, + "name": "processSession" + }, + { + "args": [ + "Json::Int code", + "Json::Value&& message" + ], + "lineno": 357, + "name": "make_json_error" + }, + { + "args": [ + "Port const& port", + "std::string const& request", + "beast::IP::Endpoint const& remoteIPAddress", + "Output const& output", + "std::shared_ptr coro", + "std::string_view forwardedFor", + "std::string_view user" + ], + "lineno": 366, + "name": "processRequest" + }, + { + "args": [ + "http_request_type const& request" + ], + "lineno": 563, + "name": "statusResponse" + }, + { + "args": [], + "lineno": 590, + "name": "makeContexts" + }, + { + "args": [ + "ParsedPort const& parsed", + "std::ostream& log" + ], + "lineno": 609, + "name": "to_Port" + }, + { + "args": [ + "Config const& config", + "std::ostream& log" + ], + "lineno": 641, + "name": "parse_Ports" + }, + { + "args": [ + "ServerHandler::Setup& setup" + ], + "lineno": 701, + "name": "setup_Client" + }, + { + "args": [ + "ServerHandler::Setup& setup" + ], + "lineno": 723, + "name": "setup_Overlay" + }, + { + "args": [ + "Config const& config", + "std::ostream& log" + ], + "lineno": 736, + "name": "setup_ServerHandler" + }, + { + "args": [ + "Application& app", + "boost::asio::io_context& io_context", + "JobQueue& jobQueue", + "NetworkOPs& networkOPs", + "Resource::Manager& resourceManager", + "CollectorManager& cm" + ], + "lineno": 747, + "name": "make_ServerHandler" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::unique_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [ + { + "audit_note": "Cannot be null after construction, auto cleanup", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "exclusive", + "type": "unique_ptr" + }, + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 22, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is typically tested in integration or functional tests for the HTTP/RPC server. Likely test files include those under 'test/rpc', 'test/server', or similar directories (e.g., test/rpc/ServerHandler_test.cpp, test/server/Authorization_test.cpp). These would test correct/incorrect Authorization headers, malformed base64, missing colon, wrong credentials, and status request handling. Gaps may exist if there are no explicit tests for malformed base64, missing colon, or edge cases in HTTP version/target/body validation. Unit tests for 'authorized' and 'isStatusRequest' functions are recommended if not present.", + "validation_architecture": { + "auto_validated_fields": [ + "HTTP method", + "HTTP version", + "HTTP target", + "HTTP body", + "Authorization header" + ], + "framework": "Custom validation, base64_decode, boost::beast::http", + "validation_layer": "middleware (request pre-processing, authentication)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (caller may throw or reject request)", + "field": "HTTP Authorization header", + "location": "authorized", + "validated_by": "authorized (custom function)", + "validates": [ + "Checks if port.user and port.password are set", + "Checks if 'authorization' header exists", + "Checks if header starts with 'Basic '", + "Base64 decodes credentials", + "Checks for ':' separator in decoded credentials", + "Splits into username and password", + "Compares username and password to configured values" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns bool (caller may throw or reject request)", + "field": "HTTP request method, version, target, body", + "location": "isStatusRequest", + "validated_by": "isStatusRequest (custom function)", + "validates": [ + "Checks HTTP version >= 11", + "Checks target is '/'", + "Checks body is empty", + "Checks method is GET" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "returns empty string or malformed result (caller may reject)", + "field": "Base64-encoded credentials", + "location": "authorized", + "validated_by": "base64_decode (xrpl::base64_decode)", + "validates": [ + "Checks if base64 string is decodable" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "returns false (caller may throw or reject request)", + "field": "Decoded credentials format", + "location": "authorized", + "validated_by": "std::string::find(':')", + "validates": [ + "Checks if decoded credentials contain ':' separator" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp.ai.md b/src/xrpld/rpc/detail/ServerHandler.cpp.ai.md new file mode 100644 index 0000000000..9286d7255f --- /dev/null +++ b/src/xrpld/rpc/detail/ServerHandler.cpp.ai.md @@ -0,0 +1,70 @@ +# ServerHandler.cpp + +`ServerHandler` is the central dispatcher that bridges the XRPL node's HTTP and WebSocket server infrastructure to its RPC execution layer. Every external client connection — whether a curl call, a WebSocket subscription, or a load balancer health probe — flows through this class before any RPC command is touched. + +## Architectural Role + +The file sits at the boundary between `xrpl::Server` (the low-level Boost.Beast HTTP/WebSocket server) and `RPC::doCommand` (the command dispatch table). `ServerHandler` implements the `Server`'s callback interface (`onAccept`, `onHandoff`, `onRequest`, `onWSMessage`, `onClose`, `onStopped`) and is responsible for authentication, rate-limiting, protocol routing, JSON parsing, role resolution, and metric collection — all before a single RPC handler sees the request. + +## Construction and the `ServerHandlerCreator` Pattern + +The constructor is deliberately public, because `std::make_unique` needs public access. But direct construction outside of `make_ServerHandler()` is prevented by requiring a `ServerHandlerCreator` argument — a private nested type only `make_ServerHandler` can name (declared a friend). This token idiom grants controlled construction through RAII factory while remaining compatible with `std::make_unique`. + +During construction the handler registers three metrics with the `CollectorManager`: an RPC request counter, a response-size event, and a response-time event. These feed into whatever stats backend the node is configured to report to. + +## Port Setup and Auto-Port Detection + +`setup()` calls `m_server->ports()`, which actually binds the listening sockets, then iterates the resulting `endpoints_` map to back-fill any port configured as `0` (OS-assigned). The same pass identifies the first HTTP/HTTPS port as the `client` endpoint and the first `peer` port as the `overlay` endpoint. This is important for the local RPC client and for the overlay to know which address to advertise. + +`parse_Ports()` reads the `[server]` config section, iterates named port subsections, converts each `ParsedPort` into a `Port` via `to_Port()`, and enforces constraints: exactly zero or one `peer` protocol is allowed in cluster mode; standalone mode strips all `peer` entries. The gRPC port (`SECTION_PORT_GRPC`) is intentionally skipped here — it is parsed by a separate `GRPCServer` class. + +## Connection Lifecycle + +### Accept Phase — `onAccept()` + +Called on every new TCP connection before any HTTP is read. It atomically increments `count_[port]` under `mutex_` and rejects the connection immediately if the port's `limit` is exceeded. `onClose()` symmetrically decrements the counter, so the limit is enforced as a live ceiling rather than a watermark. + +### Handoff Phase — `onHandoff()` + +This is the first point where the actual HTTP request is examined. The method handles three distinct cases: + +1. **WebSocket upgrade**: If the request carries an `Upgrade: websocket` header, `session.websocketUpgrade()` is called to obtain a `WSSession`. A `WSInfoSub` is created as the session's `appDefined` payload, its `Resource::Consumer` is initialized with a GUEST role for the purpose of connection accounting, and `ws->run()` is kicked off. The session is marked `moved`; the HTTP layer is finished with it. + +2. **Peer overlay**: If the port carries the `peer` protocol and a raw TLS stream bundle was handed in, the entire connection is forwarded to `app_.getOverlay().onHandoff()`. This is how inbound peer-to-peer connections are routed to the overlay subsystem without going through the RPC path at all. + +3. **Status request**: A plain HTTP `GET /` on a WebSocket-capable port receives a health-check response from `statusResponse()`. If `app_.serverOkay()` returns true the response is HTTP 200 with a brief HTML body; otherwise it is HTTP 500 with a reason string. Load balancers use this endpoint to decide whether to send traffic to the node. + +If the request matches none of these cases an empty `Handoff` is returned, signalling to the server framework that the request should proceed to `onRequest()`. + +## HTTP RPC Processing — `onRequest()` + +`onRequest()` runs on the I/O thread. It performs two fast checks: that the port's protocol set includes `http` or `https`, and that `authorized()` accepts the credentials. Both failures return HTTP 403. `authorized()` parses HTTP Basic Auth by extracting the base64 payload, decoding it with `xrpl::base64_decode`, splitting at `:`, and comparing against the port's configured `user`/`password`. When no credentials are configured the function short-circuits to `true`, making auth opt-in per port. + +If both checks pass, `session.detach()` promotes the session to a `shared_ptr` and `m_jobQueue.postCoro(jtCLIENT_RPC, ...)` schedules `processSession(Session)` as a coroutine job. Detaching is the key step: it moves ownership off the I/O thread so the coroutine can yield inside `RPC::doCommand` without stalling accept handling for other connections. If the job queue rejects the post (typically during shutdown), a 503 is returned and the session is closed immediately. + +## WebSocket RPC Processing — `onWSMessage()` + +Incoming WebSocket frames are assembled into a `Json::Value`. Oversized or unparseable messages receive an inline `jsonInvalid` error response without touching the job queue. Valid messages are dispatched as `jtCLIENT_WEBSOCKET` coroutines. The `WSInfoSub` (stored as `session->appDefined`) carries the consumer and user identity; `processSession(WSSession)` checks `consumer.disconnect()` first, returning `rpcSLOW_DOWN` if the endpoint has been throttled. + +## The `processRequest()` Pipeline + +This is the most involved function in the file — roughly 400 lines covering both single and batch HTTP RPC calls. + +**Batch mode** is triggered when `method == "batch"` with a `params` array. The outer loop iterates each sub-request independently, accumulating results into a JSON array. Errors in individual sub-requests do not abort the batch; they are appended with an inline `error` object using JSON-RPC 2.0-style numeric codes (`method_not_found = -32601`, `server_overloaded = -32604`, etc.). + +For each request (batch element or singleton), the pipeline proceeds: +1. **API version resolution** — `RPC::getAPIVersionNumber` extracts the requested API version from `params[0]` or, for batch, from the top-level object. Invalid versions generate either an HTTP 400 or a per-element error. +2. **Role determination** — `RPC::roleRequired` maps the method name to its minimum required role, then `requestRole` evaluates the client's actual role based on IP network membership and port configuration (admin nets, secure_gateway nets, etc.). +3. **Resource consumer acquisition** — Unlimited roles get `newUnlimitedEndpoint`; all others get `newInboundEndpoint`. If `consumer.disconnect()` is true the request is rejected as overloaded. +4. **Header trust revocation** — If the role is neither `IDENTIFIED` nor `PROXY`, the `forwardedFor` and `user` string views are zeroed out. This ensures that headers like `X-Forwarded-For` and `X-User` cannot be trusted when they arrive from a non-gateway source. +5. **Command dispatch** — `RPC::doCommand(context, result)` is called inside a try/catch. Exceptions produce `rpcINTERNAL` errors (marked `LCOV_EXCL` since they indicate bugs rather than normal failure paths). +6. **Response formatting** — The format diverges on `ripplerpc` version: 2.0+ uses a structured `{error: {code, message}}` shape; earlier versions nest results under `result` and echo back the request on errors. Starting at version 3.0, the HTTP status code is derived from the RPC error code via `RPC::error_code_http_status`; older versions always return HTTP 200. +7. **Credential masking** — When echoing the request back in an error response, fields `passphrase`, `secret`, `seed`, and `seed_hex` are replaced with `""` to prevent credentials from leaking into error logs. + +## Duration Logging + +`logDuration()` is a templated helper that applies tiered severity: debug below 1 second, warning at 1–10 seconds, and error above 10 seconds. The thresholds reflect the expectation that well-formed RPC calls should complete in well under a second; anything past 1 second indicates contention or a ledger under heavy load. + +## Shutdown Coordination + +`stop()` calls `m_server->close()` then blocks on `condition_.wait()` until `stopped_` is set. `onStopped()` is the server's final callback after all connections have drained, at which point it sets `stopped_` and notifies the waiting thread. The destructor sets `m_server = nullptr` to release the `unique_ptr`, which triggers the server's own destruction. This ensures clean teardown ordering: server closes first, handler waits, handler is then destroyed. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Status.cpp.ai.json b/src/xrpld/rpc/detail/Status.cpp.ai.json new file mode 100644 index 0000000000..c61625638b --- /dev/null +++ b/src/xrpld/rpc/detail/Status.cpp.ai.json @@ -0,0 +1,522 @@ +{ + "args": [ + { + "lineno": 38, + "name": "value" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "Status::codeString" + ], + "entry_point": "Status::codeString", + "purpose": "Converts the Status object to a string representation of its code, depending on its type.", + "validation_points": [ + "if (!*this) return \"\";", + "if (type_ == Type::none) return std::to_string(code_);", + "if (type_ == Status::Type::TER) {...}", + "if (type_ == Status::Type::error_code_i) {...}", + "UNREACHABLE(...)" + ] + }, + { + "call_chain": [ + "Status::fillJson", + "Status::codeString" + ], + "entry_point": "Status::fillJson", + "purpose": "Fills a JSON object with error information from the Status object.", + "validation_points": [ + "if (!*this) return;", + "Status::codeString() (contains its own validation)" + ] + }, + { + "call_chain": [ + "Status::toString", + "Status::codeString", + "Status::message" + ], + "entry_point": "Status::toString", + "purpose": "Returns a string combining the code string and message, or empty if invalid.", + "validation_points": [ + "if (*this) ... else return \"\";", + "Status::codeString() (contains its own validation)" + ] + } + ], + "data_flows": [ + { + "field": "type_", + "flow": [ + "constructor/assignment", + "Status::* methods", + "validation in codeString/fillJson/toString" + ], + "origin": "Set during Status object construction or assignment", + "transformations": [ + "Compared against enum values to determine code path" + ], + "validated_at": "Status::codeString (multiple if statements), UNREACHABLE" + }, + { + "field": "code_", + "flow": [ + "constructor/assignment", + "Status::codeString", + "Status::fillJson" + ], + "origin": "Set during Status object construction or assignment", + "transformations": [ + "Converted to string if type_ == Type::none", + "Inserted into JSON" + ], + "validated_at": "Indirectly via type_ validation in codeString" + }, + { + "field": "messages_", + "flow": [ + "constructor/assignment", + "Status::message", + "Status::fillJson" + ], + "origin": "Set during Status object construction or via methods", + "transformations": [ + "Concatenated with '/' in message()", + "Appended to JSON array in fillJson" + ], + "validated_at": "Not explicitly validated; checked for empty() in fillJson" + }, + { + "field": "*this (Status object validity)", + "flow": [ + "Status::* methods", + "if (!*this) ..." + ], + "origin": "Status object state (likely via operator bool)", + "transformations": [ + "Short-circuits method logic if invalid" + ], + "validated_at": "First line of codeString, fillJson, toString" + } + ], + "description": "Implements methods for the RPC Status class, providing error/status code string conversion, JSON serialization, and message formatting for XRPL RPC responses.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "*this (Status object validity)", + "validation", + "missing", + "check" + ], + "evidence": "Field *this (Status object validity) validated by XRPL custom (jss:: for JSON keys, XRPL_ASSERT, UNREACHABLE)", + "issue_pattern": "Missing validation for *this (Status object validity)", + "why_false_positive": "XRPL custom (jss:: for JSON keys, XRPL_ASSERT, UNREACHABLE) validates *this (Status object validity) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "type_ (Status type)", + "validation", + "missing", + "check" + ], + "evidence": "Field type_ (Status type) validated by XRPL custom (jss:: for JSON keys, XRPL_ASSERT, UNREACHABLE)", + "issue_pattern": "Missing validation for type_ (Status type)", + "why_false_positive": "XRPL custom (jss:: for JSON keys, XRPL_ASSERT, UNREACHABLE) validates type_ (Status type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "messages_ (Status messages list)", + "validation", + "missing", + "check" + ], + "evidence": "Field messages_ (Status messages list) validated by XRPL custom (jss:: for JSON keys, XRPL_ASSERT, UNREACHABLE)", + "issue_pattern": "Missing validation for messages_ (Status messages list)", + "why_false_positive": "XRPL custom (jss:: for JSON keys, XRPL_ASSERT, UNREACHABLE) validates messages_ (Status messages list) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "*this (Status object validity)", + "empty", + "string", + "validation" + ], + "evidence": "operator bool() (likely Status::operator bool) at Status::codeString", + "issue_pattern": "Missing empty string validation for *this (Status object validity)", + "why_false_positive": "operator bool() (likely Status::operator bool) validates *this (Status object validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type_ (Status type)", + "empty", + "string", + "validation" + ], + "evidence": "if (type_ == Type::none) at Status::codeString", + "issue_pattern": "Missing empty string validation for type_ (Status type)", + "why_false_positive": "if (type_ == Type::none) validates type_ (Status type) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "type_ (Status type)", + "type", + "validation", + "check" + ], + "evidence": "if (type_ == Type::none) at Status::codeString", + "issue_pattern": "Missing type validation for type_ (Status type)", + "why_false_positive": "if (type_ == Type::none) validates type_ (Status type) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type_ (Status type)", + "empty", + "string", + "validation" + ], + "evidence": "if (type_ == Status::Type::TER) at Status::codeString", + "issue_pattern": "Missing empty string validation for type_ (Status type)", + "why_false_positive": "if (type_ == Status::Type::TER) validates type_ (Status type) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "type_ (Status type)", + "type", + "validation", + "check" + ], + "evidence": "if (type_ == Status::Type::TER) at Status::codeString", + "issue_pattern": "Missing type validation for type_ (Status type)", + "why_false_positive": "if (type_ == Status::Type::TER) validates type_ (Status type) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type_ (Status type)", + "empty", + "string", + "validation" + ], + "evidence": "if (type_ == Status::Type::error_code_i) at Status::codeString", + "issue_pattern": "Missing empty string validation for type_ (Status type)", + "why_false_positive": "if (type_ == Status::Type::error_code_i) validates type_ (Status type) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "type_ (Status type)", + "type", + "validation", + "check" + ], + "evidence": "if (type_ == Status::Type::error_code_i) at Status::codeString", + "issue_pattern": "Missing type validation for type_ (Status type)", + "why_false_positive": "if (type_ == Status::Type::error_code_i) validates type_ (Status type) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type_ (Status type)", + "empty", + "string", + "validation" + ], + "evidence": "UNREACHABLE macro at Status::codeString (final else)", + "issue_pattern": "Missing empty string validation for type_ (Status type)", + "why_false_positive": "UNREACHABLE macro validates type_ (Status type) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "*this (Status object validity)", + "empty", + "string", + "validation" + ], + "evidence": "operator bool() (likely Status::operator bool) at Status::fillJson", + "issue_pattern": "Missing empty string validation for *this (Status object validity)", + "why_false_positive": "operator bool() (likely Status::operator bool) validates *this (Status object validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "messages_ (Status messages list)", + "empty", + "string", + "validation" + ], + "evidence": "if (!messages_.empty()) at Status::fillJson", + "issue_pattern": "Missing empty string validation for messages_ (Status messages list)", + "why_false_positive": "if (!messages_.empty()) validates messages_ (Status messages list) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "*this (Status object validity)", + "empty", + "string", + "validation" + ], + "evidence": "operator bool() (likely Status::operator bool) at Status::toString", + "issue_pattern": "Missing empty string validation for *this (Status object validity)", + "why_false_positive": "operator bool() (likely Status::operator bool) validates *this (Status object validity) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/Status.cpp", + "functions": [ + { + "args": [], + "lineno": 7, + "name": "Status::codeString" + }, + { + "args": [ + "value" + ], + "lineno": 38, + "name": "Status::fillJson" + }, + { + "args": [], + "lineno": 56, + "name": "Status::message" + }, + { + "args": [], + "lineno": 68, + "name": "Status::toString" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "RPC" + } + ], + "test_coverage_notes": "The code contains LCOV_EXCL_START/STOP around the UNREACHABLE macro, indicating that the unreachable branch is excluded from coverage and likely not tested. The main validation paths (operator bool, type_ checks) are likely tested in unit tests for Status, but coverage for all enum values and error conditions depends on the thoroughness of those tests. Test files would likely be found in the test/rpc or test/unit/rpc directories, possibly named Status_test.cpp or similar. There may be gaps in coverage for rare or unreachable type_ values, and for the exact behavior of fillJson with multiple messages.", + "validation_architecture": { + "auto_validated_fields": [ + "*this (Status object validity)", + "type_ (Status type)", + "messages_ (Status messages list)" + ], + "framework": "XRPL custom (jss:: for JSON keys, XRPL_ASSERT, UNREACHABLE)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Returns empty string (no exception)", + "field": "*this (Status object validity)", + "location": "Status::codeString", + "validated_by": "operator bool() (likely Status::operator bool)", + "validates": [ + "Status object is valid before proceeding" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Returns code_ as string (no exception)", + "field": "type_ (Status type)", + "location": "Status::codeString", + "validated_by": "if (type_ == Type::none)", + "validates": [ + "Status type is checked for 'none'" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "XRPL_ASSERT if transResultInfo fails (likely aborts or logs error)", + "field": "type_ (Status type)", + "location": "Status::codeString", + "validated_by": "if (type_ == Status::Type::TER)", + "validates": [ + "Status type is checked for 'TER'", + "transResultInfo must succeed" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "No exception, returns error info as string", + "field": "type_ (Status type)", + "location": "Status::codeString", + "validated_by": "if (type_ == Status::Type::error_code_i)", + "validates": [ + "Status type is checked for 'error_code_i'" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE macro (likely aborts or asserts)", + "field": "type_ (Status type)", + "location": "Status::codeString (final else)", + "validated_by": "UNREACHABLE macro", + "validates": [ + "Status type must be one of the handled types" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Returns early (no exception)", + "field": "*this (Status object validity)", + "location": "Status::fillJson", + "validated_by": "operator bool() (likely Status::operator bool)", + "validates": [ + "Status object is valid before filling JSON" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "No exception, skips appending if empty", + "field": "messages_ (Status messages list)", + "location": "Status::fillJson", + "validated_by": "if (!messages_.empty())", + "validates": [ + "messages_ is checked for non-empty before appending" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "Returns empty string (no exception)", + "field": "*this (Status object validity)", + "location": "Status::toString", + "validated_by": "operator bool() (likely Status::operator bool)", + "validates": [ + "Status object is valid before converting to string" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Status.cpp.ai.md b/src/xrpld/rpc/detail/Status.cpp.ai.md new file mode 100644 index 0000000000..ce1c11fd53 --- /dev/null +++ b/src/xrpld/rpc/detail/Status.cpp.ai.md @@ -0,0 +1,35 @@ +# `src/xrpld/rpc/detail/Status.cpp` + +This file contains the method implementations for `RPC::Status`, the unified error-result type for XRPL's RPC layer. The header (`Status.h`) declares the class and its constructors; this translation unit provides all the non-trivial method bodies that convert internal status state into human-readable strings and JSON output. + +## Context: Why `Status` Exists + +XRPL's RPC layer sits atop two separate legacy error code systems. `TER` (Transaction Engine Result) codes cover outcomes like `tesSUCCESS` and `tecNO_DST_INSUF_XRP`, expressed as an enum with a range spanning several hundred values. `error_code_i` covers RPC-level failures like `rpcINVALID_PARAMS` or `rpcNO_NETWORK`. Neither type is directly interchangeable. `Status` bridges both under a single type that can be uniformly tested, formatted, and serialised — including a third fallback mode (`Type::none`) for raw integer codes that belong to neither domain. + +## `codeString()`: Type-Discriminated Dispatch + +The central method is `codeString()`. Its structure mirrors a tagged union dispatch: it checks `type_` sequentially and delegates to the appropriate lookup function for each code space. + +For `Type::none`, it simply converts `code_` with `std::to_string`, making this the escape hatch for callers who pass bare integers at construction. + +For `Type::TER`, it calls `transResultInfo(toTER(), s1, s2)`, which fills two strings with the TER token (e.g., `"tecNO_DST_INSUF_XRP"`) and its human-readable description. The return value is captured with `[[maybe_unused]]` but immediately checked via `XRPL_ASSERT`. This pattern acknowledges that in release builds the assertion compiles away, yet the call is still needed for its output arguments `s1` and `s2`. The assert enforces an invariant rather than handling a runtime condition: if a `Status` was constructed from a `TER` value, `transResultInfo` *must* succeed, so failure here means the object was constructed incorrectly. + +For `Type::error_code_i`, it calls `get_error_info(toErrorCode())` to retrieve the `ErrorInfo` struct, then formats `token: message` using an `ostringstream`. The use of `.c_str()` on the returned strings is defensive — the underlying `ErrorInfo` fields are C-string-backed string views in some implementations. + +After all three branches, `UNREACHABLE` terminates any code path representing an unknown `type_` value, wrapped in `LCOV_EXCL_START`/`STOP` directives. This tells the coverage toolchain to ignore the dead branch explicitly, documenting the design intent: this path should never execute in a well-formed program. The `[[maybe_unused]]`/`XRPL_ASSERT`/`UNREACHABLE` pattern across the TER branch demonstrates a consistent defensive stance — assert invariants in debug, exclude unreachable paths from coverage metrics, never silently produce garbage. + +## `fillJson()`: JSON-RPC 2.0 Error Object + +`fillJson()` populates a `Json::Value` with a structured `error` sub-object adhering to the JSON-RPC 2.0 error object shape. The fields written are `error.code` (the raw integer `code_`) and `error.message` (the result of `codeString()`). If `messages_` is non-empty, an additional `error.data` array is appended, providing supplemental diagnostic strings beyond what the base code conveys. + +The early-return guard `if (!*this)` (equivalent to `if (code_ == OK)`) means callers can invoke `fillJson()` unconditionally — no-op on success, error population on failure. The header comment notes this method is "not currently used," which is architecturally notable: it represents a more spec-compliant error serialisation path than the `inject()` method (which calls `inject_error` from the `ErrorCodes` infrastructure), preserved as a future migration target toward proper JSON-RPC 2.0 conformance. + +## `message()` and `toString()` + +`message()` concatenates `messages_` with `/` delimiters, making the vector of freeform strings into a single diagnostic string. The `/` separator is a simple convention rather than structured formatting. + +`toString()` is the composite diagnostic representation: `codeString() + ":" + message()`. If the `Status` is OK (`!*this`), it returns an empty string immediately, consistent with the convention that a falsy `Status` produces no output from any serialisation method. + +## Design Cohesion + +The `operator bool()` defined inline in the header (`return code_ != OK`) is the pivotal invariant that all three output methods respect. Because `code_ == 0` means success across all three error domains (TER's `tesSUCCESS` is 0, `rpcSUCCESS` is 0, and raw integer 0 is the conventional success sentinel), this single comparison correctly short-circuits output for successful statuses regardless of `type_`. The uniformity is intentional and carefully preserved in every method body here. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp.ai.json b/src/xrpld/rpc/detail/TransactionSign.cpp.ai.json new file mode 100644 index 0000000000..28be167a8e --- /dev/null +++ b/src/xrpld/rpc/detail/TransactionSign.cpp.ai.json @@ -0,0 +1,823 @@ +{ + "args": [ + { + "lineno": 61, + "name": "accountState" + }, + { + "lineno": 62, + "name": "accountID" + }, + { + "lineno": 63, + "name": "publicKey" + }, + { + "lineno": 85, + "name": "params" + }, + { + "lineno": 86, + "name": "tx_json" + }, + { + "lineno": 87, + "name": "srcAddressID" + }, + { + "lineno": 88, + "name": "role" + }, + { + "lineno": 89, + "name": "app" + }, + { + "lineno": 90, + "name": "doPath" + }, + { + "lineno": 186, + "name": "tx_json" + }, + { + "lineno": 187, + "name": "role" + }, + { + "lineno": 188, + "name": "verify" + }, + { + "lineno": 189, + "name": "validatedLedgerAge" + }, + { + "lineno": 190, + "name": "config" + }, + { + "lineno": 191, + "name": "feeTrack" + }, + { + "lineno": 192, + "name": "apiVersion" + }, + { + "lineno": 241, + "name": "params" + }, + { + "lineno": 242, + "name": "role" + }, + { + "lineno": 243, + "name": "signingArgs" + }, + { + "lineno": 244, + "name": "validatedLedgerAge" + }, + { + "lineno": 245, + "name": "app" + }, + { + "lineno": 410, + "name": "stTx" + }, + { + "lineno": 411, + "name": "rules" + }, + { + "lineno": 412, + "name": "app" + }, + { + "lineno": 470, + "name": "tpTrans" + }, + { + "lineno": 471, + "name": "apiVersion" + }, + { + "lineno": 491, + "name": "app" + }, + { + "lineno": 492, + "name": "config" + }, + { + "lineno": 493, + "name": "tx" + }, + { + "lineno": 540, + "name": "role" + }, + { + "lineno": 541, + "name": "config" + }, + { + "lineno": 542, + "name": "feeTrack" + }, + { + "lineno": 543, + "name": "txQ" + }, + { + "lineno": 544, + "name": "app" + }, + { + "lineno": 545, + "name": "tx" + }, + { + "lineno": 546, + "name": "mult" + }, + { + "lineno": 547, + "name": "div" + }, + { + "lineno": 567, + "name": "request" + }, + { + "lineno": 568, + "name": "role" + }, + { + "lineno": 569, + "name": "doAutoFill" + }, + { + "lineno": 570, + "name": "config" + }, + { + "lineno": 571, + "name": "feeTrack" + }, + { + "lineno": 572, + "name": "txQ" + }, + { + "lineno": 573, + "name": "app" + }, + { + "lineno": 617, + "name": "jvRequest" + }, + { + "lineno": 618, + "name": "apiVersion" + }, + { + "lineno": 619, + "name": "failType" + }, + { + "lineno": 620, + "name": "role" + }, + { + "lineno": 621, + "name": "validatedLedgerAge" + }, + { + "lineno": 622, + "name": "app" + }, + { + "lineno": 641, + "name": "jvRequest" + }, + { + "lineno": 642, + "name": "apiVersion" + }, + { + "lineno": 643, + "name": "failType" + }, + { + "lineno": 644, + "name": "role" + }, + { + "lineno": 645, + "name": "validatedLedgerAge" + }, + { + "lineno": 646, + "name": "app" + }, + { + "lineno": 647, + "name": "processTransaction" + }, + { + "lineno": 684, + "name": "jvRequest" + }, + { + "lineno": 701, + "name": "signers" + }, + { + "lineno": 701, + "name": "signingForID" + }, + { + "lineno": 735, + "name": "jvRequest" + }, + { + "lineno": 736, + "name": "apiVersion" + }, + { + "lineno": 737, + "name": "failType" + }, + { + "lineno": 738, + "name": "role" + }, + { + "lineno": 739, + "name": "validatedLedgerAge" + }, + { + "lineno": 740, + "name": "app" + }, + { + "lineno": 784, + "name": "jvRequest" + }, + { + "lineno": 785, + "name": "apiVersion" + }, + { + "lineno": 786, + "name": "failType" + }, + { + "lineno": 787, + "name": "role" + }, + { + "lineno": 788, + "name": "validatedLedgerAge" + }, + { + "lineno": 789, + "name": "app" + }, + { + "lineno": 790, + "name": "processTransaction" + } + ], + "classes": [ + { + "args": [], + "lineno": 28, + "name": "SigningForParams" + }, + { + "args": [], + "lineno": 217, + "name": "transactionPreProcessResult" + } + ], + "code_paths": [ + { + "call_chain": [ + "transactionSign", + "transactionPreProcessImpl", + "checkTxJsonFields", + "SigningForParams::isMultiSigning", + "SigningForParams::getSigner", + "acctMatchesPubKey" + ], + "entry_point": "transactionSign", + "purpose": "Handles the signing of a transaction, including validation of signing account and public key.", + "validation_points": [ + "checkTxJsonFields (validates tx_json fields)", + "SigningForParams::isMultiSigning (validates if multi-signing is active)", + "SigningForParams::getSigner (validates multiSigningAcctID_ is set)", + "acctMatchesPubKey (validates that the public key matches the account and is enabled)" + ] + }, + { + "call_chain": [ + "transactionSign", + "transactionPreProcessImpl", + "SigningForParams::validMultiSign" + ], + "entry_point": "transactionSign", + "purpose": "Checks if multi-signature parameters are valid before proceeding.", + "validation_points": [ + "SigningForParams::validMultiSign (validates multiSignPublicKey_ and multiSignature_)" + ] + }, + { + "call_chain": [ + "transactionSign", + "transactionPreProcessImpl", + "SigningForParams::getPublicKey" + ], + "entry_point": "transactionSign", + "purpose": "Retrieves and validates the public key for signing.", + "validation_points": [ + "SigningForParams::getPublicKey (throws if multiSignPublicKey_ is not set)" + ] + } + ], + "data_flows": [ + { + "field": "multiSigningAcctID_", + "flow": [ + "SigningForParams constructor", + "SigningForParams::isMultiSigning", + "SigningForParams::getSigner", + "acctMatchesPubKey" + ], + "origin": "Set in SigningForParams constructor (from input AccountID)", + "transformations": [ + "Checked for nullptr to determine multi-signing mode", + "Dereferenced for account validation" + ], + "validated_at": "SigningForParams::isMultiSigning, SigningForParams::getSigner" + }, + { + "field": "multiSignPublicKey_", + "flow": [ + "SigningForParams::setPublicKey", + "SigningForParams::validMultiSign", + "SigningForParams::getPublicKey", + "acctMatchesPubKey" + ], + "origin": "Set via SigningForParams::setPublicKey (from input PublicKey)", + "transformations": [ + "Optional: checked for presence", + "Dereferenced for validation and signing" + ], + "validated_at": "SigningForParams::validMultiSign, SigningForParams::getPublicKey" + }, + { + "field": "multiSignature_", + "flow": [ + "SigningForParams::moveMultiSignature", + "SigningForParams::validMultiSign", + "SigningForParams::getSignature" + ], + "origin": "Set via SigningForParams::moveMultiSignature (from input Buffer)", + "transformations": [ + "Moved into member variable", + "Checked for non-empty" + ], + "validated_at": "SigningForParams::validMultiSign" + }, + { + "field": "tx_json fields", + "flow": [ + "transactionSign", + "transactionPreProcessImpl", + "checkTxJsonFields" + ], + "origin": "Input JSON from RPC request", + "transformations": [ + "Parsed and validated for required fields" + ], + "validated_at": "checkTxJsonFields" + } + ], + "description": "Implements transaction signing, submission, and multisigning logic for XRPL RPC, including validation, autofill, fee calculation, and JSON handling.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "multiSigningAcctID_", + "empty", + "string", + "validation" + ], + "evidence": "isMultiSigning() at SigningForParams::isMultiSigning()", + "issue_pattern": "Missing empty string validation for multiSigningAcctID_", + "why_false_positive": "isMultiSigning() validates multiSigningAcctID_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "multiSignPublicKey_", + "empty", + "string", + "validation" + ], + "evidence": "getPublicKey() at SigningForParams::getPublicKey()", + "issue_pattern": "Missing empty string validation for multiSignPublicKey_", + "why_false_positive": "getPublicKey() validates multiSignPublicKey_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "multiSignature_", + "empty", + "string", + "validation" + ], + "evidence": "validMultiSign() at SigningForParams::validMultiSign()", + "issue_pattern": "Missing empty string validation for multiSignature_", + "why_false_positive": "validMultiSign() validates multiSignature_ for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "multiSigningAcctID_", + "empty", + "string", + "validation" + ], + "evidence": "getSigner() at SigningForParams::getSigner()", + "issue_pattern": "Missing empty string validation for multiSigningAcctID_", + "why_false_positive": "getSigner() validates multiSigningAcctID_ for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/TransactionSign.cpp", + "functions": [ + { + "args": [ + "accountState", + "accountID", + "publicKey" + ], + "lineno": 61, + "name": "acctMatchesPubKey" + }, + { + "args": [ + "params", + "tx_json", + "srcAddressID", + "role", + "app", + "doPath" + ], + "lineno": 85, + "name": "checkPayment" + }, + { + "args": [ + "tx_json", + "role", + "verify", + "validatedLedgerAge", + "config", + "feeTrack", + "apiVersion" + ], + "lineno": 186, + "name": "checkTxJsonFields" + }, + { + "args": [ + "params", + "role", + "signingArgs", + "validatedLedgerAge", + "app" + ], + "lineno": 241, + "name": "transactionPreProcessImpl" + }, + { + "args": [ + "stTx", + "rules", + "app" + ], + "lineno": 410, + "name": "transactionConstructImpl" + }, + { + "args": [ + "tpTrans", + "apiVersion" + ], + "lineno": 470, + "name": "transactionFormatResultImpl" + }, + { + "args": [ + "app", + "config", + "tx" + ], + "lineno": 491, + "name": "getTxFee" + }, + { + "args": [ + "role", + "config", + "feeTrack", + "txQ", + "app", + "tx", + "mult", + "div" + ], + "lineno": 540, + "name": "getCurrentNetworkFee" + }, + { + "args": [ + "request", + "role", + "doAutoFill", + "config", + "feeTrack", + "txQ", + "app" + ], + "lineno": 567, + "name": "checkFee" + }, + { + "args": [ + "jvRequest", + "apiVersion", + "failType", + "role", + "validatedLedgerAge", + "app" + ], + "lineno": 617, + "name": "transactionSign" + }, + { + "args": [ + "jvRequest", + "apiVersion", + "failType", + "role", + "validatedLedgerAge", + "app", + "processTransaction" + ], + "lineno": 641, + "name": "transactionSubmit" + }, + { + "args": [ + "jvRequest" + ], + "lineno": 684, + "name": "checkMultiSignFields" + }, + { + "args": [ + "signers", + "signingForID" + ], + "lineno": 701, + "name": "sortAndValidateSigners" + }, + { + "args": [ + "jvRequest", + "apiVersion", + "failType", + "role", + "validatedLedgerAge", + "app" + ], + "lineno": 735, + "name": "transactionSignFor" + }, + { + "args": [ + "jvRequest", + "apiVersion", + "failType", + "role", + "validatedLedgerAge", + "app", + "processTransaction" + ], + "lineno": 784, + "name": "transactionSubmitMultiSigned" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 19, + "name": "xrpl" + }, + { + "lineno": 20, + "name": "RPC" + }, + { + "lineno": 23, + "name": "detail" + } + ], + "test_coverage_notes": "The main validation logic is likely covered by integration and unit tests for transaction signing and multi-signing. Typical test files would be in the 'test' or 'unittest' directories, such as 'test/rpc/TransactionSign_test.cpp' or similar. However, coverage gaps may exist for edge cases: (1) error paths in SigningForParams (e.g., exceptions thrown when accessing unset fields), (2) multi-signature validation failures, (3) malformed or missing tx_json fields. Template-based validation may be harder to test exhaustively unless explicit negative tests exist. Review of test files is needed to confirm coverage of all exception and validation branches.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom logic, with some framework hints (e.g., jss:: for JSON validation elsewhere)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LogicError (if accessed incorrectly)", + "field": "multiSigningAcctID_", + "location": "SigningForParams::isMultiSigning()", + "validated_by": "isMultiSigning()", + "validates": [ + "Checks if multiSigningAcctID_ is not nullptr to determine if multi-signing is active" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "LogicError", + "field": "multiSignPublicKey_", + "location": "SigningForParams::getPublicKey()", + "validated_by": "getPublicKey()", + "validates": [ + "Ensures multiSignPublicKey_ is set before access" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns bool)", + "field": "multiSignature_", + "location": "SigningForParams::validMultiSign()", + "validated_by": "validMultiSign()", + "validates": [ + "Checks that multi-signing is active, public key is set, and signature buffer is not empty" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "LogicError", + "field": "multiSigningAcctID_", + "location": "SigningForParams::getSigner()", + "validated_by": "getSigner()", + "validates": [ + "Ensures multiSigningAcctID_ is not nullptr before access" + ], + "validation_type": "type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp.ai.md b/src/xrpld/rpc/detail/TransactionSign.cpp.ai.md new file mode 100644 index 0000000000..d706f57eeb --- /dev/null +++ b/src/xrpld/rpc/detail/TransactionSign.cpp.ai.md @@ -0,0 +1,59 @@ +# `src/xrpld/rpc/detail/TransactionSign.cpp` + +This file implements the complete server-side pipeline for the four XRPL RPC operations that involve transaction signing: `sign`, `submit`, `sign_for`, and `submit_multisigned`. It lives in `namespace xrpl::RPC` with internal helpers tucked into `namespace xrpl::RPC::detail`, and its public surface is declared in the companion header `TransactionSign.h`. + +## Architectural Role + +The file acts as the bridge between raw JSON arriving over an RPC connection and a fully validated, signed, serialized `STTx` that can be broadcast to the network. Nothing here touches consensus or the ledger engine directly; it converts, validates, signs, and hands off to `NetworkOPs::processTransaction`. The same signing infrastructure supports both single-key and multi-party (threshold) signing, unified through a single pre-processing path. + +## `SigningForParams` — Mode Discriminator + +The file-private class `SigningForParams` is the linchpin that allows `transactionPreProcessImpl` to serve both single-signing and multi-signing without code duplication. It holds a raw `const*` to an `AccountID` (the signer's account in multi-signing mode, `nullptr` for single-signing), an `std::optional`, and a `Buffer` for the computed multi-signature. + +The design choice to use a raw pointer rather than an `std::optional` is deliberate: the pointer's nullness is the mode discriminator (`isMultiSigning()` / `isSingleSigning()`), and the pointed-to value is always owned by the caller's stack frame. The copy constructor is explicitly deleted, making the intent clear that `SigningForParams` is not meant to escape the local call chain. Accessors `getSigner()` and `getPublicKey()` call `LogicError()` rather than UB-producing dereferences if invoked in the wrong mode, encoding the invariant that callers must check mode before accessing mode-specific data. + +An additional optional field `signatureTarget_` carries an `SField` reference that routes the signature into a nested inner object rather than the transaction root — used by the `signature_target` RPC parameter for signing custom inner objects. + +## `transactionPreProcessResult` — Poor Man's `std::expected` + +The internal struct `transactionPreProcessResult` models a discriminated union: it carries either a `Json::Value` error or a `std::shared_ptr`. Both fields are `const`, both implicit constructors are deleted, and only move semantics are permitted. Callers uniformly test `!preprocResult.second` to distinguish the two states. This predates `std::expected` (C++23) and serves the same purpose: a typed, non-nullable return that cannot be accidentally ignored. + +## `transactionPreProcessImpl` — The Core Pipeline + +This static function is where all the heavy lifting happens for `transactionSign`, `transactionSubmit`, and `transactionSignFor`. Its pipeline in order: + +1. **Key extraction** — `keypairForSignature()` resolves the key pair from the request's `secret`, `seed`, `seed_hex`, or `passphrase` fields. +2. **Signature target resolution** — If `signature_target` is present in the request, the code looks up the corresponding `SField` and its `SOTemplate` from `InnerObjectFormats`. An unknown target is rejected immediately. +3. **Basic field validation** — `checkTxJsonFields()` gates on `TransactionType`, `Account`, ledger staleness (using different error codes for API v1 vs. v2), and cluster load. +4. **Sequence auto-fill** — In online mode with single-signing, if `Sequence` is absent and no `TicketSequence` is present, the next queuable sequence is fetched from `TxQ`. Ticket-based transactions receive sequence 0. Multi-signing skips this entirely (`editFields()` returns false) because the transaction should already be fully formed before multi-signers add their contributions. +5. **NetworkID auto-fill** — Networks with ID > 1024 (sidechains, testnets) have their ID injected automatically to prevent cross-network replay. +6. **Fee check** — Delegated to `checkFee()`. +7. **Payment-specific validation** — Delegated to `checkPayment()`, which resolves the `DeliverMax`/`Amount` alias, validates destination, and optionally runs the `Pathfinder` for XRP/IOU paths (MPT-denominated amounts cannot use path-finding unless `featureMPTokensV2` is enabled). +8. **Signing mode exclusivity** — Enforces that a transaction cannot simultaneously have a `TxnSignature` field while multi-signing, and cannot have a `Signers` array while single-signing. +9. **Account–key binding** — For single-signing, `acctMatchesPubKey()` verifies the public key matches the account's master key (unless `lsfDisableMaster` is set) or its designated regular key. For delegated transactions, the binding check is performed against the delegate account's ledger entry rather than the transaction's `Account` field. +10. **STTx construction** — `STParsedJSONObject` converts the JSON to the binary serialized form. For multi-signing, `SigningPubKey` is set to an empty byte string (protocol requirement); for single-signing it receives the actual public key. +11. **Signing** — Multi-signing calls `buildMultiSigningData()` to hash the transaction with the signer's account, then signs and stores the result in `signingArgs`. Single-signing calls `stTx->sign()`. + +## `acctMatchesPubKey` — Key Validation with Three Cases + +This helper handles the nuance that a ledger account can be in one of three authentication states: (a) no ledger entry yet (unactivated account where only the master key is valid), (b) ledger entry with master key enabled (master or regular key accepted), (c) ledger entry with `lsfDisableMaster` set (only regular key accepted). The function encodes all three cases cleanly with early returns and produces typed error codes (`rpcBAD_SECRET`, `rpcMASTER_DISABLED`) rather than booleans, which are threaded up to the RPC response. + +## `transactionConstructImpl` — Transaction Sterilization + +After signing, this function performs a roundtrip serialization test: the `STTx` is serialized to bytes, deserialized into a fresh `STTx const`, and the two are compared for equivalence. If they differ — or if signature validation fails — the function returns an internal error. This is a defensive correctness invariant: it guarantees that what is broadcast to the P2P network is byte-for-byte identical to what was signed, ruling out any internal representation bug. If `app.checkSigs()` is false (configurable for testing or trusted environments), the hash router is pre-seeded with `Validity::SigGoodOnly` to skip the cryptographic signature check while still confirming structural correctness. + +## Fee Pipeline + +`getTxFee()` temporarily patches the incoming `tx_json` with placeholder values for `Fee`, `Sequence`, `SigningPubKey`, and `TxnSignature` (and per-signer placeholders for multi-signed transactions), then parses it into an `STTx` purely to call `calculateBaseFee()`. This is necessary because the protocol fee depends on transaction type and content (e.g., the number of signers), not just type alone. The result feeds into `getCurrentNetworkFee()`, which applies load scaling via `scaleFeeLoad()` and then takes the maximum with the TxQ's current escalated fee level. The caller-specified `fee_mult_max`/`fee_div_max` ceiling is enforced last with a `mulDiv()` overflow-safe computation. + +## Public API Surface + +`transactionSign` and `transactionSubmit` differ only in whether they call `processTransaction` at the end. Both use a default-constructed `SigningForParams()` (single-signing mode). + +`transactionSignFor` is the incremental multi-signing endpoint. It adds one signer's contribution to an in-progress multi-signed transaction. The function parses the `account` field, constructs `SigningForParams` with that account ID, calls `transactionPreProcessImpl` (which deposits the computed signature into `signForParams`), then injects a new `STObject` Signer entry into the `sfSigners` array. After each injection, `sortAndValidateSigners()` sorts the array by `AccountID` (a protocol requirement for signature aggregation) and rejects duplicates or self-signing. + +`transactionSubmitMultiSigned` handles the final submission once all signers have contributed. It does not re-sign; it validates structural correctness (empty `SigningPubKey`, no `TxnSignature`, non-zero XRP fee), validates and sorts the existing Signers array, sterilizes via `transactionConstructImpl`, and submits. + +## Offline Mode + +The `offline: true` request parameter bypasses all online checks: ledger staleness, account existence, and field auto-fill. In offline mode, the caller must supply `Sequence` themselves. This enables air-gapped signing workflows where the private key material never touches a connected machine. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/TransactionSign.h.ai.json b/src/xrpld/rpc/detail/TransactionSign.h.ai.json new file mode 100644 index 0000000000..f1e80f962f --- /dev/null +++ b/src/xrpld/rpc/detail/TransactionSign.h.ai.json @@ -0,0 +1,124 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "Application" + }, + { + "args": [], + "lineno": 12, + "name": "LoadFeeTrack" + }, + { + "args": [], + "lineno": 13, + "name": "Transaction" + }, + { + "args": [], + "lineno": 14, + "name": "TxQ" + } + ], + "description": "This header file declares functions and types for handling transaction signing, fee calculation, and submission in the XRPL server's RPC subsystem.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/TransactionSign.h", + "functions": [ + { + "args": [ + "role", + "config", + "feeTrack", + "txQ", + "app", + "tx", + "mult", + "div" + ], + "lineno": 15, + "name": "getCurrentNetworkFee" + }, + { + "args": [ + "request", + "role", + "doAutoFill", + "config", + "feeTrack", + "txQ", + "app" + ], + "lineno": 38, + "name": "checkFee" + }, + { + "args": [ + "netOPs" + ], + "lineno": 59, + "name": "getProcessTxnFn" + }, + { + "args": [ + "params", + "apiVersion", + "failType", + "role", + "validatedLedgerAge", + "app" + ], + "lineno": 71, + "name": "transactionSign" + }, + { + "args": [ + "params", + "apiVersion", + "failType", + "role", + "validatedLedgerAge", + "app", + "processTransaction" + ], + "lineno": 79, + "name": "transactionSubmit" + }, + { + "args": [ + "params", + "apiVersion", + "failType", + "role", + "validatedLedgerAge", + "app" + ], + "lineno": 88, + "name": "transactionSignFor" + }, + { + "args": [ + "params", + "apiVersion", + "failType", + "role", + "validatedLedgerAge", + "app", + "processTransaction" + ], + "lineno": 96, + "name": "transactionSubmitMultiSigned" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 18, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/TransactionSign.h.ai.md b/src/xrpld/rpc/detail/TransactionSign.h.ai.md new file mode 100644 index 0000000000..b70c95b5a9 --- /dev/null +++ b/src/xrpld/rpc/detail/TransactionSign.h.ai.md @@ -0,0 +1,55 @@ +# `src/xrpld/rpc/detail/TransactionSign.h` + +## Role in the System + +This header is the public interface of the RPC transaction pipeline — the boundary between the JSON-speaking RPC handler layer and the signing/submission machinery inside the node. It declares the six functions and one type alias that together implement the `sign`, `submit`, `sign_for`, and `submit_multisigned` RPC methods exposed to clients. + +The file lives in `rpc/detail/`, which is the private implementation layer of the RPC subsystem. Callers are the RPC command handlers in `xrpld/rpc/handlers/`; the implementations are entirely in the companion `TransactionSign.cpp`. + +## The Four Entry Points + +The four transaction-handling functions split along two axes: single vs. multi-signature, and sign-only vs. sign-and-submit. + +**`transactionSign`** prepares and cryptographically signs a transaction on the server side, returning the signed `tx_blob` and `tx_json` to the caller without broadcasting it. This is the `sign` RPC method: the caller typically takes the result and submits it independently. + +**`transactionSubmit`** does everything `transactionSign` does, then calls into the P2P layer to broadcast the transaction. This is the `submit` RPC method. + +**`transactionSignFor`** handles a single participant in a multi-signature quorum. A multi-signed transaction requires signatures from several accounts; each signer calls `sign_for` in turn, adding their `Signer` entry to the `Signers` array in `tx_json`. The server signs on behalf of the specified `account` using that account's supplied secret. + +**`transactionSubmitMultiSigned`** accepts a fully assembled multi-signed transaction (the `Signers` array already populated) and submits it. This corresponds to the `submit_multisigned` RPC method. + +All four functions take `params` by value rather than const reference. This is intentional: the implementation mutates the JSON freely during preprocessing — auto-filling `Fee`, `Sequence`, `SigningPubKey`, resolving payment paths, and normalizing `DeliverMax` to `Amount` — without touching the caller's original object. + +## Fee Infrastructure + +The two fee functions exist because fee calculation is a non-trivial multi-step process that also needs to be exposed separately. + +**`getCurrentNetworkFee`** computes the recommended fee for a specific transaction against the current open ledger. It takes the *maximum* of two fee estimates: the load-scaled base fee (from `LoadFeeTrack`, which rises under heavy CPU/IO load) and the open-ledger escalated fee (from `TxQ`, which rises as the transaction queue fills). Administrative and identified roles bypass the load-scaling component, paying only the escalation-based fee. The result is capped against a `mult/div` ratio to prevent the auto-fill from injecting excessively high fees. + +**`checkFee`** is the higher-level gatekeeper called during preprocessing. If `Fee` is already present in the transaction JSON it returns immediately; if `doAutoFill` is false and `Fee` is absent it returns an error; otherwise it reads the optional `fee_mult_max` / `fee_div_max` fields from the outer request and delegates to `getCurrentNetworkFee`. The defaults — `defaultAutoFillFeeMultiplier = 10` and `defaultAutoFillFeeDivisor = 1` from `Tuning.h` — mean the server will autofill any fee up to 10× the ledger's base reference fee before refusing. + +## `ProcessTransactionFn` and the Dependency-Injection Pattern + +```cpp +using ProcessTransactionFn = std::function& transaction, + bool bUnlimited, + bool bLocal, + NetworkOPs::FailHard failType)>; +``` + +The submit functions do not call `NetworkOPs::processTransaction` directly. Instead they accept a `ProcessTransactionFn` callback. `getProcessTxnFn(NetworkOPs&)` is the inline factory that produces the real implementation by capturing `netOPs` in a lambda. + +This indirection serves testability. Unit tests can supply a mock `ProcessTransactionFn` that records what was submitted without spinning up a full `NetworkOPs`. The pattern is deliberate: sign-only functions (`transactionSign`, `transactionSignFor`) don't accept a `ProcessTransactionFn` at all — the absence of the parameter makes it impossible for them to accidentally submit. + +## Role and Access Control + +Every entry point accepts a `Role` parameter (from `xrpld/rpc/Role.h`), which is an enum with values `GUEST`, `USER`, `IDENTIFIED`, `ADMIN`, `PROXY`, `FORBID`. Within the implementation, `isUnlimited(role)` is checked in two places: once inside `checkTxJsonFields` to gate out requests when the cluster is overloaded (`feeTrack.isLoadedCluster()`), and again in `getCurrentNetworkFee` to exempt administrative callers from the load-scaled fee component. Unlimited roles also pass `bUnlimited = true` to `processTransaction`, which controls queue priority. + +## `validatedLedgerAge` Guard + +Every entry point also takes a `std::chrono::seconds validatedLedgerAge`. If the node's most recent validated ledger is older than `Tuning::maxValidatedLedgerAge` (2 minutes), the function rejects the request with `rpcNO_CURRENT` (API v1) or `rpcNOT_SYNCED` (API v2+). This prevents clients from signing transactions against a stale fee schedule or sequence number state. The version-gated error code difference is an example of the API versioning strategy used throughout the RPC layer: old error names are preserved for v1 compatibility while newer names are more descriptive. + +## Relationship to the Implementation + +The heavy lifting is all in `TransactionSign.cpp`, which defines several internal `detail`-namespace helpers invisible to callers: `SigningForParams` (a move-only state object tracking single vs. multi-sign mode, the signer's public key, and the computed signature buffer), `transactionPreProcessImpl` (keypair extraction, field validation, `STTx` construction, cryptographic signing), `transactionConstructImpl` (wraps the `STTx` in a `Transaction`, then round-trip serializes and deserializes it to "sterilize" it — catching any internal inconsistencies before the transaction is accepted into the queue), and `transactionFormatResultImpl` (converts the finished `Transaction` into the JSON response with `tx_blob`, `tx_json`, and optional engine result fields). \ No newline at end of file diff --git a/src/xrpld/rpc/detail/TrustLine.cpp.ai.json b/src/xrpld/rpc/detail/TrustLine.cpp.ai.json new file mode 100644 index 0000000000..1f0d463c05 --- /dev/null +++ b/src/xrpld/rpc/detail/TrustLine.cpp.ai.json @@ -0,0 +1,415 @@ +{ + "args": [ + { + "lineno": 8, + "name": "sle" + }, + { + "lineno": 8, + "name": "viewAccount" + }, + { + "lineno": 27, + "name": "accountID" + }, + { + "lineno": 36, + "name": "view" + }, + { + "lineno": 36, + "name": "direction" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const& sle", + "AccountID const& viewAccount" + ], + "lineno": 8, + "name": "TrustLineBase" + }, + { + "args": [], + "lineno": 27, + "name": "PathFindTrustLine" + }, + { + "args": [ + "std::shared_ptr const& sle", + "AccountID const& viewAccount" + ], + "lineno": 59, + "name": "RPCTrustLine" + } + ], + "code_paths": [ + { + "call_chain": [ + "PathFindTrustLine::getItems", + "detail::getTrustLineItems", + "PathFindTrustLine::makeItem" + ], + "entry_point": "PathFindTrustLine::getItems", + "purpose": "Fetches all PathFindTrustLine items for an account from a ledger view, validating each SLE before constructing a PathFindTrustLine.", + "validation_points": [ + "PathFindTrustLine::makeItem: Validates sle is non-null and sle->getType() == ltRIPPLE_STATE" + ] + }, + { + "call_chain": [ + "RPCTrustLine::getItems", + "detail::getTrustLineItems", + "RPCTrustLine::makeItem" + ], + "entry_point": "RPCTrustLine::getItems", + "purpose": "Fetches all RPCTrustLine items for an account from a ledger view, validating each SLE before constructing an RPCTrustLine.", + "validation_points": [ + "RPCTrustLine::makeItem: Validates sle is non-null and sle->getType() == ltRIPPLE_STATE" + ] + } + ], + "data_flows": [ + { + "field": "sle (shared_ptr)", + "flow": [ + "forEachItem yields sleCur", + "detail::getTrustLineItems receives sleCur", + "T::makeItem (PathFindTrustLine::makeItem or RPCTrustLine::makeItem) receives sle", + "Validation: if (!sle || sle->getType() != ltRIPPLE_STATE) return {}", + "If valid, construct TrustLine object" + ], + "origin": "forEachItem callback parameter (from ledger view)", + "transformations": [ + "Checked for null", + "Checked for correct type" + ], + "validated_at": "PathFindTrustLine::makeItem and RPCTrustLine::makeItem" + }, + { + "field": "mLowLimit, mHighLimit, mBalance, mFlags", + "flow": [ + "sle (validated in makeItem)", + "TrustLineBase(sle, viewAccount) extracts fields via getFieldAmount/getFieldU32", + "Stored in TrustLineBase members" + ], + "origin": "Fields extracted from sle in TrustLineBase constructor", + "transformations": [ + "mBalance.negate() if mViewLowest is false" + ], + "validated_at": "sle validated before TrustLineBase constructed" + }, + { + "field": "lowQualityIn_, lowQualityOut_, highQualityIn_, highQualityOut_", + "flow": [ + "sle (validated in RPCTrustLine::makeItem)", + "RPCTrustLine(sle, viewAccount) extracts fields via getFieldU32", + "Stored in RPCTrustLine members" + ], + "origin": "Fields extracted from sle in RPCTrustLine constructor", + "transformations": [], + "validated_at": "sle validated before RPCTrustLine constructed" + } + ], + "description": "Implements trust line handling for XRPL, including base and derived classes for trust lines, methods to extract trust line data from ledger entries, and utilities to retrieve trust lines for accounts.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle (shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at PathFindTrustLine::makeItem", + "issue_pattern": "Missing empty string validation for sle (shared_ptr)", + "why_false_positive": "explicit null check validates sle (shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sle (shared_ptr)", + "type", + "validation", + "check" + ], + "evidence": "explicit null check at PathFindTrustLine::makeItem", + "issue_pattern": "Missing type validation for sle (shared_ptr)", + "why_false_positive": "explicit null check validates sle (shared_ptr) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle->getType()", + "empty", + "string", + "validation" + ], + "evidence": "explicit type check at PathFindTrustLine::makeItem", + "issue_pattern": "Missing empty string validation for sle->getType()", + "why_false_positive": "explicit type check validates sle->getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sle->getType()", + "type", + "validation", + "check" + ], + "evidence": "explicit type check at PathFindTrustLine::makeItem", + "issue_pattern": "Missing type validation for sle->getType()", + "why_false_positive": "explicit type check validates sle->getType() type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle (shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "explicit null check at RPCTrustLine::makeItem", + "issue_pattern": "Missing empty string validation for sle (shared_ptr)", + "why_false_positive": "explicit null check validates sle (shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sle (shared_ptr)", + "type", + "validation", + "check" + ], + "evidence": "explicit null check at RPCTrustLine::makeItem", + "issue_pattern": "Missing type validation for sle (shared_ptr)", + "why_false_positive": "explicit null check validates sle (shared_ptr) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle->getType()", + "empty", + "string", + "validation" + ], + "evidence": "explicit type check at RPCTrustLine::makeItem", + "issue_pattern": "Missing empty string validation for sle->getType()", + "why_false_positive": "explicit type check validates sle->getType() for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "sle->getType()", + "type", + "validation", + "check" + ], + "evidence": "explicit type check at RPCTrustLine::makeItem", + "issue_pattern": "Missing type validation for sle->getType()", + "why_false_positive": "explicit type check validates sle->getType() type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/TrustLine.cpp", + "functions": [ + { + "args": [ + "int" + ], + "lineno": 20, + "name": "TrustLineBase::getJson" + }, + { + "args": [ + "AccountID const& accountID", + "std::shared_ptr const& sle" + ], + "lineno": 27, + "name": "PathFindTrustLine::makeItem" + }, + { + "args": [ + "AccountID const& accountID", + "ReadView const& view", + "LineDirection direction" + ], + "lineno": 36, + "name": "detail::getTrustLineItems" + }, + { + "args": [ + "AccountID const& accountID", + "ReadView const& view", + "LineDirection direction" + ], + "lineno": 54, + "name": "PathFindTrustLine::getItems" + }, + { + "args": [ + "AccountID const& accountID", + "std::shared_ptr const& sle" + ], + "lineno": 65, + "name": "RPCTrustLine::makeItem" + }, + { + "args": [ + "AccountID const& accountID", + "ReadView const& view" + ], + "lineno": 71, + "name": "RPCTrustLine::getItems" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 34, + "name": "detail" + } + ], + "test_coverage_notes": "This code is typically tested via higher-level RPC or ledger tests that exercise trust line listing, pathfinding, and account lines. Likely test files: rpc/AccountLines_test.cpp, rpc/PathFind_test.cpp, ledger/TrustLine_test.cpp. Direct unit tests for makeItem validation (null/type) may not exist; coverage may be indirect via tests that pass invalid SLEs or malformed ledger entries. Gaps: No explicit unit tests for makeItem's validation logic; edge cases (null SLE, wrong type) may not be directly tested.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, C++ idioms)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "sle (shared_ptr)", + "location": "PathFindTrustLine::makeItem", + "validated_by": "explicit null check", + "validates": [ + "Checks if sle is not null before proceeding" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "sle->getType()", + "location": "PathFindTrustLine::makeItem", + "validated_by": "explicit type check", + "validates": [ + "Checks if SLE type is ltRIPPLE_STATE" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "sle (shared_ptr)", + "location": "RPCTrustLine::makeItem", + "validated_by": "explicit null check", + "validates": [ + "Checks if sle is not null before proceeding" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt (no exception)", + "field": "sle->getType()", + "location": "RPCTrustLine::makeItem", + "validated_by": "explicit type check", + "validates": [ + "Checks if SLE type is ltRIPPLE_STATE" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/TrustLine.cpp.ai.md b/src/xrpld/rpc/detail/TrustLine.cpp.ai.md new file mode 100644 index 0000000000..aec96e9f00 --- /dev/null +++ b/src/xrpld/rpc/detail/TrustLine.cpp.ai.md @@ -0,0 +1,42 @@ +# TrustLine.cpp + +This file implements the trust line wrapper hierarchy used by two distinct XRPL subsystems: the payment pathfinder and the `account_lines` RPC command. It solves the fundamental perspective problem inherent in how XRPL stores trust lines and provides typed collections through a shared iteration template. + +## The Low/High Perspective Problem + +Every trust line in the XRP Ledger is stored as a single `ltRIPPLE_STATE` SLE with two sides — a "low" account and a "high" account — determined by lexicographic ordering of their `AccountID` values. This ledger-centric representation is opaque from any one account's point of view: a caller asking about their own balance needs the sign of that balance flipped if they happen to be the "high" party. + +`TrustLineBase` resolves this at construction time. The `mViewLowest` flag is set to `true` when the requesting `viewAccount` matches `mLowLimit.getIssuer()`. If `mViewLowest` is `false` (the caller is the "high" party), the constructor immediately calls `mBalance.negate()`. All subsequent accessors — `getBalance()`, `getLimit()`, `getLimitPeer()`, `getAuth()`, `getNoRipple()`, `getFreeze()`, `getDeepFreeze()`, and their `*Peer` counterparts — use `mViewLowest` to select the correct flag bits or limit field without any further branching. This one-time normalization at construction keeps the rest of the codebase clean. + +## The Two Derived Types + +`PathFindTrustLine` is the lightweight variant used by the path-finding engine. The header comments explicitly warn that there can be tens of millions of live instances during pathfinding, making memory layout critical. Accordingly, `PathFindTrustLine` adds no fields beyond those in `TrustLineBase`. It inherits constructors directly (`using TrustLineBase::TrustLineBase`) and only exposes the static factory interface. + +`RPCTrustLine` extends the base with four quality rate fields: `lowQualityIn_`, `lowQualityOut_`, `highQualityIn_`, `highQualityOut_`. These correspond to the `quality_in` and `quality_out` values surfaced in the `account_lines` JSON response. The `getQualityIn()` and `getQualityOut()` accessors again use `mViewLowest` to select the perspective-correct rate. These fields are not needed by the pathfinder, which is why they are isolated in the heavier `RPCTrustLine` subclass. + +Both classes mix in `CountedObject` via CRTP, which tracks live object counts. This is a practical concession to the scale at which `PathFindTrustLine` instances are created — it enables the monitoring infrastructure to report on pathfinder memory pressure. + +## Collection Building via the Shared Template + +`detail::getTrustLineItems` is a private function template (hidden in the inner `detail` namespace) that both `PathFindTrustLine::getItems` and `RPCTrustLine::getItems` delegate to. It uses `forEachItem` to iterate an account's owner directory, calling `T::makeItem` on every yielded SLE. The owner directory contains every ledger object an account owns — offers, escrows, checks, NFT pages, and more — so `makeItem` must filter aggressively. + +Both `makeItem` implementations apply two guards: a null check on the `shared_ptr` and a type check for `ltRIPPLE_STATE`. If either fails, `std::nullopt` is returned and the item is silently skipped. No exception is thrown; the error is contained entirely within the factory. This pattern makes `getTrustLineItems` robust to heterogeneous owner directories without any caller-visible error handling. + +After iteration, `shrink_to_fit()` is called on the result vector. This is directly motivated by the pathfinder's usage — the comment notes that the returned list "may be around for a while," so freeing unused capacity from the initial over-allocation is worth the potential reallocation. + +## Direction Filtering for the Pathfinder + +`PathFindTrustLine::getItems` accepts a `LineDirection` parameter. The filter inside `getTrustLineItems` reads: + +```cpp +if (ret && (direction == LineDirection::outgoing || !ret->getNoRipple())) + items.push_back(std::move(*ret)); +``` + +`LineDirection::outgoing` always includes a trust line (it is the default). `LineDirection::incoming` — used when the pathfinder encounters an account reached via a NoRipple-disabled line — includes only trust lines where the `NoRipple` flag is not set on the viewing account's side. This precisely implements the XRPL pathfinding rule that an incoming account cannot further propagate payments through its own NoRipple-flagged lines. + +`RPCTrustLine::getItems` omits the direction parameter entirely; the `account_lines` command always returns all trust lines regardless of rippling configuration. + +## Relationship to AccountLines.cpp + +`AccountLines.cpp`'s `doAccountLines` handler does not call `RPCTrustLine::getItems` directly. Instead it uses `forEachItemAfter` for paginated iteration and calls `RPCTrustLine::makeItem` individually per matching SLE, then passes each `RPCTrustLine` to `addLine()`, which converts it to the full JSON representation including balance, limits, quality rates, and all flag fields. The `getJson(int)` method on `TrustLineBase` — which outputs only `low_id` and `high_id` — is therefore a minimal debug/diagnostic helper rather than a production JSON serializer. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/TrustLine.h.ai.json b/src/xrpld/rpc/detail/TrustLine.h.ai.json new file mode 100644 index 0000000000..1d1a1f017c --- /dev/null +++ b/src/xrpld/rpc/detail/TrustLine.h.ai.json @@ -0,0 +1,176 @@ +{ + "args": [ + { + "lineno": 29, + "name": "sle" + }, + { + "lineno": 29, + "name": "viewAccount" + } + ], + "classes": [ + { + "args": [ + "std::shared_ptr const& sle", + "AccountID const& viewAccount" + ], + "lineno": 27, + "name": "TrustLineBase" + }, + { + "args": [], + "lineno": 126, + "name": "PathFindTrustLine" + }, + { + "args": [ + "std::shared_ptr const& sle", + "AccountID const& viewAccount" + ], + "lineno": 140, + "name": "RPCTrustLine" + } + ], + "description": "Defines classes and enums for representing and interacting with trust lines in the XRPL ledger, including wrappers for pathfinding and RPC use cases.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/TrustLine.h", + "functions": [ + { + "args": [], + "lineno": 38, + "name": "key" + }, + { + "args": [], + "lineno": 43, + "name": "getAccountID" + }, + { + "args": [], + "lineno": 48, + "name": "getAccountIDPeer" + }, + { + "args": [], + "lineno": 53, + "name": "getAuth" + }, + { + "args": [], + "lineno": 58, + "name": "getAuthPeer" + }, + { + "args": [], + "lineno": 63, + "name": "getNoRipple" + }, + { + "args": [], + "lineno": 68, + "name": "getNoRipplePeer" + }, + { + "args": [], + "lineno": 73, + "name": "getDirection" + }, + { + "args": [], + "lineno": 78, + "name": "getDirectionPeer" + }, + { + "args": [], + "lineno": 83, + "name": "getFreeze" + }, + { + "args": [], + "lineno": 88, + "name": "getDeepFreeze" + }, + { + "args": [], + "lineno": 93, + "name": "getFreezePeer" + }, + { + "args": [], + "lineno": 98, + "name": "getDeepFreezePeer" + }, + { + "args": [], + "lineno": 103, + "name": "getBalance" + }, + { + "args": [], + "lineno": 108, + "name": "getLimit" + }, + { + "args": [], + "lineno": 113, + "name": "getLimitPeer" + }, + { + "args": [ + "int" + ], + "lineno": 118, + "name": "getJson" + }, + { + "args": [ + "AccountID const& accountID", + "std::shared_ptr const& sle" + ], + "lineno": 132, + "name": "makeItem" + }, + { + "args": [ + "AccountID const& accountID", + "ReadView const& view", + "LineDirection direction" + ], + "lineno": 135, + "name": "getItems" + }, + { + "args": [], + "lineno": 147, + "name": "getQualityIn" + }, + { + "args": [], + "lineno": 152, + "name": "getQualityOut" + }, + { + "args": [ + "AccountID const& accountID", + "std::shared_ptr const& sle" + ], + "lineno": 157, + "name": "makeItem" + }, + { + "args": [ + "AccountID const& accountID", + "ReadView const& view" + ], + "lineno": 160, + "name": "getItems" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/TrustLine.h.ai.md b/src/xrpld/rpc/detail/TrustLine.h.ai.md new file mode 100644 index 0000000000..ddadf35ddd --- /dev/null +++ b/src/xrpld/rpc/detail/TrustLine.h.ai.md @@ -0,0 +1,49 @@ +# `TrustLine.h` — Trust Line Wrapper for Path Finding and RPC + +This header defines the data abstraction layer between raw trust line ledger entries (`ltRIPPLE_STATE` SLEs) and the two major consumers that need to read trust line data: the path finder and the `account_lines` RPC handler. It exists to solve a fundamental asymmetry in how XRPL stores trust lines and to give each consumer a type that carries exactly the fields it needs. + +## The Low/High Asymmetry Problem + +Every trust line in the XRPL ledger is stored as a single `ltRIPPLE_STATE` SLE shared between two accounts. The ledger canonicalizes the two sides as "low" and "high" based on the lexicographic ordering of the `AccountID` values, and stores the balance from the low account's perspective (positive means the high side owes the low side). Per-side metadata — limits, authorization flags, freeze flags, no-ripple flags — is stored in separate `sfLowLimit`/`sfHighLimit` and `lsfLow*`/`lsfHigh*` fields within the same object. + +`TrustLineBase` normalizes this away. Its constructor accepts the `viewAccount` parameter and sets `mViewLowest` to indicate whether the caller is the low or high participant on the line. Every getter then dispatches on this flag: + +```cpp +bool getNoRipple() const { + return mFlags & (mViewLowest ? lsfLowNoRipple : lsfHighNoRipple); +} +``` + +The constructor also negates `mBalance` when the view account is the high side, so `getBalance()` always returns a value with the correct sign relative to the caller. This normalization is the sole purpose of the class — once constructed, callers never need to think about which "side" of the SLE they're reading. + +## Memory Constraints and the `PathFindTrustLine` Split + +The class comment is explicit: the path finder can easily create tens of millions of `TrustLineBase` instances. This pressure drove the decision to split the class into two derived types rather than carrying all fields in one. + +`PathFindTrustLine` inherits only from `TrustLineBase` and adds no data members beyond those in the base. It omits the quality-in/quality-out transfer fee rates because the path finder does not need them during path enumeration — fees are handled at a later stage. Keeping these four `Rate` fields out of `PathFindTrustLine` measurably reduces the per-instance footprint when tens of millions of instances are live in memory. + +`RPCTrustLine` extends `TrustLineBase` with `lowQualityIn_`, `lowQualityOut_`, `highQualityIn_`, `highQualityOut_`, reading them from `sfLowQualityIn` etc. in the SLE. The `getQualityIn()`/`getQualityOut()` accessors again use `mViewLowest` to surface the caller's side. This variant is used exclusively by the `account_lines` handler, which serializes all four quality values into the JSON response. + +## `LineDirection` and Path Traversal Filtering + +```cpp +enum class LineDirection : bool { incoming = false, outgoing = true }; +``` + +This `bool`-backed enum classifies an account's role in a payment path. An "outgoing" account (no-ripple flag off) can be a transit hop; an "incoming" account (no-ripple flag on) is a dead end for further rippling. `TrustLineBase::getDirection()` and `getDirectionPeer()` derive direction directly from the no-ripple flags. + +`PathFindTrustLine::getItems()` takes a `LineDirection` argument and forwards it to the shared template `getTrustLineItems()`. When direction is `outgoing`, all trust lines are returned. When `incoming`, the helper filters out any line where `getNoRipple()` is true — those lines cannot carry value further along the path and would only waste path-finding cycles. `RPCTrustLine::getItems()` omits the direction parameter entirely because `account_lines` always wants all trust lines regardless. + +## Factory Pattern and Generic Iteration + +Both subclasses expose a static `makeItem(accountID, sle)` factory that returns `std::optional`, yielding empty when the SLE is null or not of type `ltRIPPLE_STATE`. The shared implementation template `getTrustLineItems()` uses `forEachItem()` to walk the account's owner directory — which can contain offers, escrows, and other SLE types — and calls `T::makeItem()` on each entry. Non-trust-line entries silently produce `std::nullopt` and are skipped. This avoids any need for the template to know which SLE types it will encounter. + +After building the vector, `shrink_to_fit()` is called explicitly because these vectors may persist for a long time inside `AssetCache`, and trimming excess capacity across millions of instances has meaningful impact. + +## Copy and Move Semantics + +`TrustLineBase` deletes copy assignment but allows copy construction and move construction. The `const` members `mLowLimit` and `mHighLimit` would make assignment ill-formed regardless, but the explicit `= delete` makes the design intent clear. The derived classes inherit these semantics and add no constructors of their own beyond `PathFindTrustLine`'s inherited constructor and `RPCTrustLine`'s explicit two-argument constructor. + +## `CountedObject` Tracking + +Both `PathFindTrustLine` and `RPCTrustLine` inherit from `CountedObject`, a lock-free reference-count mixin. This increments a global atomic counter per type on construction and decrements it on destruction, enabling live instance counts to be queried at runtime. Given the path finder's potential to create millions of instances, this is a practical diagnostic: if memory pressure spikes, the counter reveals exactly how many `PathFindTrustLine` objects are alive. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Tuning.h.ai.json b/src/xrpld/rpc/detail/Tuning.h.ai.json new file mode 100644 index 0000000000..acda9b4727 --- /dev/null +++ b/src/xrpld/rpc/detail/Tuning.h.ai.json @@ -0,0 +1,41 @@ +{ + "args": [ + { + "lineno": 46, + "name": "isBinary" + } + ], + "classes": [ + { + "args": [], + "lineno": 11, + "name": "LimitRange" + } + ], + "description": "Defines tuned constants and limit parameters for various XRPL RPC commands within the xrpl::RPC::Tuning namespace.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/Tuning.h", + "functions": [ + { + "args": [ + "isBinary" + ], + "lineno": 46, + "name": "pageLength" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 3, + "name": "xrpl" + }, + { + "lineno": 4, + "name": "RPC" + }, + { + "lineno": 7, + "name": "Tuning" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/Tuning.h.ai.md b/src/xrpld/rpc/detail/Tuning.h.ai.md new file mode 100644 index 0000000000..740e229b4e --- /dev/null +++ b/src/xrpld/rpc/detail/Tuning.h.ai.md @@ -0,0 +1,43 @@ +# `xrpld/rpc/detail/Tuning.h` + +This header is the single authoritative source for all tunable constants in the XRPL RPC subsystem. Rather than scattering magic numbers throughout the handlers, every paginated command, every throttle decision, and every request-size gate references a named constant defined here — making capacity planning visible and changes safe. The entire file lives inside the nested namespace `xrpl::RPC::Tuning` and is consumed across nearly a dozen handler and infrastructure files. + +## `LimitRange` and Paginated Commands + +The `LimitRange` struct captures the three-sided contract that every paginated RPC command makes with its callers: a floor (`rmin`), a sensible default (`rDefault`), and a ceiling (`rmax`). The central enforcement lives in `RPCHelpers.cpp`'s `readLimitField()`: + +```cpp +limit = std::max(range.rmin, std::min(range.rmax, limit)); +``` + +This clamping only fires for ordinary clients. Connections that carry an *unlimited* role (typically authenticated admin or local connections) bypass it entirely — they receive exactly what they asked for, uncapped. This design keeps the API honest for public endpoints while giving operators full access from trusted connections. + +Most account-scoped commands (`accountLines`, `accountChannels`, `accountObjects`, `accountOffers`, `accountTx`) share identical limits — `{10, 200, 400}` — reflecting the uniform internal cost model for iterating ledger objects by account. Two commands stand out from this pattern: + +- **`bookOffers`** uses `{0, 60, 100}`. Its floor is zero, not ten. A limit of zero in `readLimitField()` triggers a separate invalid-field error, so the zero minimum is practically unreachable through normal clients; it exists to allow internal callers to request a purely structural probe without requiring a minimum page. +- **`nftOffers`** uses `{50, 250, 500}`, a higher ceiling than account-level commands. NFT offer books can legitimately accumulate hundreds of bids, so the ceiling is raised to avoid forcing callers into excessive pagination. +- **`noRippleCheck`** uses `{10, 300, 400}`, a higher default than most, because the no-ripple audit command is typically invoked precisely to scan a large set of trust lines and collects its results in a single pass. + +## Page Length and the Binary/JSON Asymmetry + +`pageLength(bool isBinary)` selects between `binaryPageLength = 2048` and `jsonPageLength = 256` — an 8× gap. This isn't arbitrary. JSON-encoded ledger objects carry field names, string representations of amounts, and human-readable type tags; the same data in binary (XRPL's canonical serialisation format) is roughly an order of magnitude more compact. Capping JSON responses at 256 objects and binary at 2048 keeps the wire payload and memory pressure at comparable levels. `LedgerData.cpp` applies this directly: + +```cpp +auto maxLimit = RPC::Tuning::pageLength(isBinary); +``` + +## Pathfinding Throttles + +Two constants gate the ripple path-finding subsystem. `maxPathfindsInProgress = 2` is an atomic counter guard in `LegacyPathFind.cpp`: if two path-find operations are already running concurrently, new requests are rejected immediately rather than queued. `maxPathfindJobCount = 50` is a second gate applied to the broader job queue depth — if the job queue is already that long (or local load is high), path-find requests are refused before they are even submitted. Together these prevent path-finding — the most computationally expensive RPC operation — from starving other work. + +The limits for source currencies in path-find requests (`max_src_cur = 18`, `max_auto_src_cur = 88`) cap the combinatorial explosion that occurs when the pathfinder searches across many source currency candidates. The auto-source limit is higher because those currencies are generated algorithmically by the server rather than specified by the user, and the server can budget for them more accurately. + +## Infrastructure Limits + +`maxJobQueueClients = 500` is checked in `RPCHandler.cpp` before submitting an RPC dispatch job. If the queue already has 500 pending client requests, new arrivals are dropped, preventing unbounded memory growth under burst traffic. + +`maxRequestSize = 1_000_000` (1 MB) is enforced in `ServerHandler.cpp` before any JSON parsing begins. Parsing is deferred past this check deliberately — a malicious 500 MB payload that passes the size gate would cause the JSON parser to allocate aggressively; rejecting it cheaply at the byte-count level prevents that class of amplification attack. + +`maxValidatedLedgerAge = 2 minutes` is used in `TransactionSign.cpp` to refuse transaction signing when the node's most-recently validated ledger is more than two minutes old. A stale ledger implies the node may be partitioned or behind, and signing a transaction against it could reference incorrect fee levels or account states. + +`defaultAutoFillFeeMultiplier = 10` and `defaultAutoFillFeeDivisor = 1` seed the fee auto-fill calculation in `TransactionSign.cpp`. The effective multiplier (10×) is intentionally conservative — auto-filled fees must be high enough to clear the network under typical load without user intervention, and the 10× factor over the current base fee provides that headroom. \ No newline at end of file diff --git a/src/xrpld/rpc/detail/WSInfoSub.h.ai.json b/src/xrpld/rpc/detail/WSInfoSub.h.ai.json new file mode 100644 index 0000000000..7566dafd23 --- /dev/null +++ b/src/xrpld/rpc/detail/WSInfoSub.h.ai.json @@ -0,0 +1,50 @@ +{ + "args": [], + "classes": [ + { + "args": [ + "Source& source", + "std::shared_ptr const& ws" + ], + "lineno": 11, + "name": "WSInfoSub" + } + ], + "description": "Defines the WSInfoSub class, which represents a WebSocket-based subscription for XRPL server notifications, handling user and forwarded-for information and sending JSON messages over a WebSocket session.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/detail/WSInfoSub.h", + "functions": [ + { + "args": [ + "Source& source", + "std::shared_ptr const& ws" + ], + "lineno": 16, + "name": "WSInfoSub" + }, + { + "args": [], + "lineno": 30, + "name": "user" + }, + { + "args": [], + "lineno": 35, + "name": "forwarded_for" + }, + { + "args": [ + "Json::Value const& jv", + "bool" + ], + "lineno": 40, + "name": "send" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/detail/WSInfoSub.h.ai.md b/src/xrpld/rpc/detail/WSInfoSub.h.ai.md new file mode 100644 index 0000000000..2376b5e3c3 --- /dev/null +++ b/src/xrpld/rpc/detail/WSInfoSub.h.ai.md @@ -0,0 +1,52 @@ +# `WSInfoSub.h` — WebSocket Subscription Handle + +`WSInfoSub` is the concrete WebSocket implementation of the abstract `InfoSub` subscription handle. Its purpose is narrow and well-defined: it bridges the XRPL notification system (which publishes ledger events, transactions, and server status updates to subscribed clients) to a live WebSocket session managed by the network layer. Every WebSocket connection that goes through `ServerHandler` gets exactly one `WSInfoSub` attached to it as its `appDefined` payload. + +## Role in the Subscription Architecture + +`InfoSub` defines the abstract contract: track which accounts, books, and event streams a client is subscribed to, and expose a `send()` method that delivers a `Json::Value` to that client. All subscription registration and event dispatch in the RPC subsystem goes through `InfoSub::pointer` handles, so the publisher never needs to know whether its audience is connected via WebSocket, HTTP long-poll, or any other transport. `WSInfoSub` fulfills this contract specifically for the WebSocket case. + +The `InfoSub::Source` interface (passed to the constructor) is the bidirectional gateway to the subscription registry—typically `NetworkOPs`. When `WSInfoSub` is constructed it registers with this source; when it is destroyed the base class destructor unregisters all subscriptions on behalf of the now-closed connection. + +## Construction and Trusted-Proxy Identity Extraction + +The constructor accepts a `std::shared_ptr` but stores it as a `std::weak_ptr`. This is a deliberate ownership decision: the `WSSession` controls its own lifetime (it is owned by the server I/O layer), while the `WSInfoSub` is owned by `session->appDefined` and by any active subscription registrations held in the source. A strong back-reference would create a cycle that neither side could break cleanly. + +During construction, the code performs a trust check before reading any proxy-provided headers: + +```cpp +if (ipAllowed( + beast::IPAddressConversion::from_asio(ws->remote_endpoint()).address(), + ws->port().secure_gateway_nets_v4, + ws->port().secure_gateway_nets_v6)) +``` + +Only if the remote address falls within the port's configured `secure_gateway` networks are the `X-User` header and `X-Forwarded-For` header trusted. This is the correct defense against header spoofing: a direct client claiming to be an admin user through a forged `X-User` header would be ignored unless the connection came through a known trusted reverse proxy. The resulting `user_` and `fwdfor_` strings are then used immediately after construction in `ServerHandler::onUpgrade()` to establish the `Resource::Consumer` with the appropriate role and rate-limit bucket. + +## `send()` Implementation + +```cpp +void send(Json::Value const& jv, bool) override { + auto sp = ws_.lock(); + if (!sp) + return; + boost::beast::multi_buffer sb; + Json::stream(jv, [&](void const* data, std::size_t n) { + sb.commit(boost::asio::buffer_copy(sb.prepare(n), boost::asio::buffer(data, n))); + }); + auto m = std::make_shared>(std::move(sb)); + sp->send(m); +} +``` + +The `ws_.lock()` guard handles the race between session teardown and pending notification delivery. If the underlying `WSSession` has already been destroyed (connection closed, server shutdown), the lock returns a null pointer and the send is silently dropped. There is no error propagation because there is no caller waiting for the result—events are fire-and-forget from the publisher's perspective. + +Serialization uses `Json::stream()` rather than `Json::FastWriter` or `Json::Value::toStyledString()`, writing directly into chunks of a `boost::beast::multi_buffer` via the callback. This avoids materializing the full JSON as an intermediate `std::string` before copying it into the I/O buffer, which is a meaningful allocation saving for high-frequency subscription events like ledger closes or transaction streams. + +The serialized buffer is wrapped in a `StreambufWSMsg` and handed to `WSSession::send()`. The `StreambufWSMsg` template (defined in `WSSession.h`) implements the chunked `prepare()` protocol that the async WebSocket write loop uses to drain the buffer incrementally. + +The unnamed `bool` parameter (`broadcast`) is accepted but ignored. Its semantic in the base class is to signal whether the send is a broadcast (sent to many subscribers simultaneously) versus a targeted reply. The WebSocket transport layer does not differentiate—every `send()` is an individual async write queued on the session—so the flag carries no meaning here. + +## Integration Point in `ServerHandler` + +`ServerHandler::onUpgrade()` creates the `WSInfoSub`, calls `requestInboundEndpoint()` passing `is->user()` and `is->forwarded_for()` to establish rate-limit accounting, and then stores the shared pointer as `ws->appDefined`. Later, every incoming WebSocket frame goes through `ServerHandler::processSession(WSSession)`, which immediately casts `session->appDefined` back to a `WSInfoSub` to check `getConsumer().disconnect()`—i.e., whether the endpoint has exceeded its resource budget and should be disconnected. The `user()` and `forwarded_for()` accessors return `std::string_view` over the stored `std::string` members, keeping the interface zero-copy while the object is alive. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ChannelVerify.cpp.ai.json b/src/xrpld/rpc/handlers/ChannelVerify.cpp.ai.json new file mode 100644 index 0000000000..5b6c6d924b --- /dev/null +++ b/src/xrpld/rpc/handlers/ChannelVerify.cpp.ai.json @@ -0,0 +1,471 @@ +{ + "args": [ + { + "lineno": 16, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doChannelVerify" + ], + "entry_point": "doChannelVerify", + "purpose": "Handles the RPC request to verify a payment channel signature. Validates input fields, parses and transforms them, and performs cryptographic verification.", + "validation_points": [ + "params.isMember(jss::public_key)", + "params.isMember(jss::channel_id)", + "params.isMember(jss::amount)", + "params.isMember(jss::signature)", + "parseBase58(TokenType::AccountPublic, strPk)", + "strUnHex(strPk)", + "publicKeyType(makeSlice(*pkHex))", + "channelId.parseHex(params[jss::channel_id].asString())", + "to_uint64(params[jss::amount].asString())", + "strUnHex(params[jss::signature].asString())" + ] + } + ], + "data_flows": [ + { + "field": "public_key", + "flow": [ + "params[jss::public_key]", + "strPk = params[jss::public_key].asString()", + "parseBase58(TokenType::AccountPublic, strPk)", + "if parseBase58 fails: strUnHex(strPk) -> publicKeyType(makeSlice(*pkHex)) -> pk.emplace(makeSlice(*pkHex))", + "used in verify(*pk, ...)" + ], + "origin": "params[jss::public_key] (JSON RPC input)", + "transformations": [ + "String extraction", + "Base58 decoding", + "If Base58 fails: Hex decoding and type checking" + ], + "validated_at": "params.isMember(jss::public_key), parseBase58, strUnHex, publicKeyType" + }, + { + "field": "channel_id", + "flow": [ + "params[jss::channel_id]", + "params[jss::channel_id].asString()", + "channelId.parseHex(...)", + "used in serializePayChanAuthorization(msg, channelId, ...)" + ], + "origin": "params[jss::channel_id] (JSON RPC input)", + "transformations": [ + "String extraction", + "Hex parsing" + ], + "validated_at": "params.isMember(jss::channel_id), channelId.parseHex" + }, + { + "field": "amount", + "flow": [ + "params[jss::amount]", + "params[jss::amount].asString()", + "to_uint64(...)", + "used in XRPAmount(drops)", + "used in serializePayChanAuthorization(msg, channelId, XRPAmount(drops))" + ], + "origin": "params[jss::amount] (JSON RPC input)", + "transformations": [ + "String extraction", + "String to uint64 conversion" + ], + "validated_at": "params.isMember(jss::amount), to_uint64" + }, + { + "field": "signature", + "flow": [ + "params[jss::signature]", + "params[jss::signature].asString()", + "strUnHex(...)", + "used in verify(*pk, msg.slice(), makeSlice(*sig))" + ], + "origin": "params[jss::signature] (JSON RPC input)", + "transformations": [ + "String extraction", + "Hex decoding" + ], + "validated_at": "params.isMember(jss::signature), strUnHex" + } + ], + "description": "Implements an RPC handler function to verify a payment channel signature in the XRPL (XRP Ledger) server.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::public_key) at doChannelVerify", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "params.isMember(jss::public_key) validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "channel_id", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::channel_id) at doChannelVerify", + "issue_pattern": "Missing empty string validation for channel_id", + "why_false_positive": "params.isMember(jss::channel_id) validates channel_id for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::amount) at doChannelVerify", + "issue_pattern": "Missing empty string validation for amount", + "why_false_positive": "params.isMember(jss::amount) validates amount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "signature", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::signature) at doChannelVerify", + "issue_pattern": "Missing empty string validation for signature", + "why_false_positive": "params.isMember(jss::signature) validates signature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58(TokenType::AccountPublic, strPk) at doChannelVerify", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "parseBase58(TokenType::AccountPublic, strPk) validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "public_key", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58(TokenType::AccountPublic, strPk) at doChannelVerify", + "issue_pattern": "Missing format validation for public_key", + "why_false_positive": "parseBase58(TokenType::AccountPublic, strPk) validates public_key format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "strUnHex(strPk) + publicKeyType(makeSlice(*pkHex)) at doChannelVerify", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "strUnHex(strPk) + publicKeyType(makeSlice(*pkHex)) validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "channel_id", + "empty", + "string", + "validation" + ], + "evidence": "channelId.parseHex(params[jss::channel_id].asString()) at doChannelVerify", + "issue_pattern": "Missing empty string validation for channel_id", + "why_false_positive": "channelId.parseHex(params[jss::channel_id].asString()) validates channel_id for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "channel_id", + "format", + "validation", + "invalid" + ], + "evidence": "channelId.parseHex(params[jss::channel_id].asString()) at doChannelVerify", + "issue_pattern": "Missing format validation for channel_id", + "why_false_positive": "channelId.parseHex(params[jss::channel_id].asString()) validates channel_id format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::amount].isString() ? to_uint64(params[jss::amount].asString()) : std::nullopt at doChannelVerify", + "issue_pattern": "Missing empty string validation for amount", + "why_false_positive": "params[jss::amount].isString() ? to_uint64(params[jss::amount].asString()) : std::nullopt validates amount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "signature", + "empty", + "string", + "validation" + ], + "evidence": "strUnHex(params[jss::signature].asString()) at doChannelVerify", + "issue_pattern": "Missing empty string validation for signature", + "why_false_positive": "strUnHex(params[jss::signature].asString()) validates signature for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "signature", + "format", + "validation", + "invalid" + ], + "evidence": "strUnHex(params[jss::signature].asString()) at doChannelVerify", + "issue_pattern": "Missing format validation for signature", + "why_false_positive": "strUnHex(params[jss::signature].asString()) validates signature format" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ChannelVerify.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 15, + "name": "doChannelVerify" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The function doChannelVerify is typically tested via RPC integration tests, likely found in files such as 'rpc/Channel_test.cpp', 'rpc/PayChan_test.cpp', or similar. Unit tests may exist for parseBase58, strUnHex, and verify, but direct unit tests for doChannelVerify may be limited. Edge cases such as malformed public keys, invalid hex, missing fields, and signature verification failures should be covered. Gaps may exist if tests do not cover all malformed input scenarios, especially for alternate public key encodings or boundary values for amount and signature fields.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC + jss:: field tags + explicit C++ validation", + "validation_layer": "business_logic (doChannelVerify is the RPC handler entry point)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::public_key)", + "field": "public_key", + "location": "doChannelVerify", + "validated_by": "params.isMember(jss::public_key)", + "validates": [ + "field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::channel_id)", + "field": "channel_id", + "location": "doChannelVerify", + "validated_by": "params.isMember(jss::channel_id)", + "validates": [ + "field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::amount)", + "field": "amount", + "location": "doChannelVerify", + "validated_by": "params.isMember(jss::amount)", + "validates": [ + "field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::signature)", + "field": "signature", + "location": "doChannelVerify", + "validated_by": "params.isMember(jss::signature)", + "validates": [ + "field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcPUBLIC_MALFORMED)", + "field": "public_key", + "location": "doChannelVerify", + "validated_by": "parseBase58(TokenType::AccountPublic, strPk)", + "validates": [ + "public_key is valid base58 AccountPublic" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcPUBLIC_MALFORMED)", + "field": "public_key", + "location": "doChannelVerify", + "validated_by": "strUnHex(strPk) + publicKeyType(makeSlice(*pkHex))", + "validates": [ + "if not base58, try hex decode", + "if hex decode fails, error", + "if publicKeyType is not valid, error" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcCHANNEL_MALFORMED)", + "field": "channel_id", + "location": "doChannelVerify", + "validated_by": "channelId.parseHex(params[jss::channel_id].asString())", + "validates": [ + "channel_id is valid hex string for uint256" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcCHANNEL_AMT_MALFORMED)", + "field": "amount", + "location": "doChannelVerify", + "validated_by": "params[jss::amount].isString() ? to_uint64(params[jss::amount].asString()) : std::nullopt", + "validates": [ + "amount is a string", + "amount string can be parsed as uint64" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "signature", + "location": "doChannelVerify", + "validated_by": "strUnHex(params[jss::signature].asString())", + "validates": [ + "signature is a valid hex string", + "signature is not empty" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ChannelVerify.cpp.ai.md b/src/xrpld/rpc/handlers/ChannelVerify.cpp.ai.md new file mode 100644 index 0000000000..9478d5926f --- /dev/null +++ b/src/xrpld/rpc/handlers/ChannelVerify.cpp.ai.md @@ -0,0 +1,63 @@ +# `ChannelVerify.cpp` — Payment Channel Signature Verification RPC Handler + +## Purpose and System Role + +`ChannelVerify.cpp` implements the `channel_verify` JSON-RPC command, one half of a two-part off-ledger payment channel authorization protocol. Its counterpart, `ChannelAuthorize` in `admin/signing/ChannelAuthorize.cpp`, produces cryptographic signatures over a (channel ID, amount) pair; this handler takes that same triple — public key, channel ID, amount, and signature — and reports whether the signature is cryptographically valid. Because the check is purely mathematical, the handler never consults ledger state: no database reads, no validators, no network calls. + +This design is intentional. Payment channels on the XRPL allow the channel source account to authorize off-ledger micro-payments to a counterparty by signing claim messages. The counterparty can use `channel_verify` to confirm a claim is authentic before accepting it — without submitting anything to the network. Only when the counterparty eventually wants to close or advance the channel do they post a `PaymentChannelClaim` transaction. + +## Canonical Message Format + +The cryptographic verification is anchored to the exact same serialization that `ChannelAuthorize` uses when producing a signature. Both sides call `serializePayChanAuthorization()` from `include/xrpl/protocol/PayChan.h`: + +```cpp +inline void +serializePayChanAuthorization(Serializer& msg, uint256 const& key, XRPAmount const& amt) +{ + msg.add32(HashPrefix::paymentChannelClaim); + msg.addBitString(key); + msg.add64(amt.drops()); +} +``` + +The `HashPrefix::paymentChannelClaim` prefix is a 4-byte domain separator that prevents this serialization from being mistakenly interpreted as a ledger hash or transaction, a pattern used consistently throughout XRPL's cryptographic boundary. The channel ID is a 256-bit ledger object key, and the amount is the cumulative authorized drop count serialized as a 64-bit big-endian integer. `doChannelVerify` reconstructs this byte string from the caller's inputs and passes it directly to the generic `verify()` function alongside the supplied public key and signature bytes. + +## Dual-Encoding Public Key Parsing + +The most structurally interesting part of `doChannelVerify` is its two-phase public key parsing. The primary attempt decodes the key as base58-encoded `AccountPublic` token (the `rXXXXX`-style encoding familiar from XRPL account addresses). If that fails, the handler falls back to raw hexadecimal — first decoding the hex string, then calling `publicKeyType()` to validate the key type (secp256k1 or Ed25519). Only after both attempts fail does it return `rpcPUBLIC_MALFORMED`. + +```cpp +pk = parseBase58(TokenType::AccountPublic, strPk); +if (!pk) +{ + auto pkHex = strUnHex(strPk); + if (!pkHex) + return rpcError(rpcPUBLIC_MALFORMED); + auto const pkType = publicKeyType(makeSlice(*pkHex)); + if (!pkType) + return rpcError(rpcPUBLIC_MALFORMED); + pk.emplace(makeSlice(*pkHex)); +} +``` + +This dual-path design makes the RPC accessible to two classes of callers: tools that work with the XRPL's native base58 encoding, and lower-level clients (like validators or custom wallets) that operate directly on raw key bytes. The hex fallback is not merely a convenience — it ensures that tooling which generates keys in non-XRPL-specific contexts can still invoke verification. + +## Input Validation Strategy + +All four required fields (`public_key`, `channel_id`, `amount`, `signature`) are checked for presence in a single loop before any parsing begins, returning `missing_field_error` immediately. This fail-fast pass avoids partial state setup and keeps error messages unambiguous. + +The amount field enforces an explicit type check — `params[jss::amount].isString()` must be true before `to_uint64` is attempted, yielding `rpcCHANNEL_AMT_MALFORMED` on failure. This guards against JSON numeric literals being passed instead of strings, which matters because JSON numbers lose precision for 64-bit integers (XRP drops can reach 10^17). The signature field additionally checks that the decoded bytes are non-empty, returning `rpcINVALID_PARAMS` rather than the more specific channel error, consistent with the pattern used by other signature-handling RPC commands across the codebase. + +## Result Contract + +The handler returns exactly one key on success: + +```json +{ "signature_verified": true | false } +``` + +There is no exception path for a cryptographically invalid signature — `verify()` returns a boolean. This is a meaningful design choice: the caller should never need to distinguish between a network or server error and a bad signature through exception handling. A bad signature is a valid, expected outcome, so it maps cleanly to `false` rather than an error code. + +## Relationship to `ChannelAuthorize` + +`doChannelAuthorize` resides in the `admin/signing/` subtree because it requires access to a secret key. `doChannelVerify`, by contrast, carries no such restriction and is exposed without role checks — verifying a signature is a read-only, stateless operation that reveals nothing sensitive. The two handlers form a symmetric pair: `ChannelAuthorize` computes `sign(sk, serialize(channelId, drops))` and returns the hex-encoded result; `ChannelVerify` computes `verify(pk, serialize(channelId, drops), sig)` and reports the boolean outcome. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/Handlers.h.ai.json b/src/xrpld/rpc/handlers/Handlers.h.ai.json new file mode 100644 index 0000000000..54bb38e08a --- /dev/null +++ b/src/xrpld/rpc/handlers/Handlers.h.ai.json @@ -0,0 +1,519 @@ +{ + "args": [], + "classes": [], + "description": "This header file declares a set of RPC handler functions for the XRPL server, each handling a specific RPC command related to accounts, ledgers, transactions, server state, and other XRPL features.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/Handlers.h", + "functions": [ + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 6, + "name": "doAccountCurrencies" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 8, + "name": "doAccountInfo" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 10, + "name": "doAccountLines" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 12, + "name": "doAccountChannels" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 14, + "name": "doAccountNFTs" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 16, + "name": "doAccountObjects" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 18, + "name": "doAccountOffers" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 20, + "name": "doAccountTx" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 22, + "name": "doAMMInfo" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 24, + "name": "doBookOffers" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 26, + "name": "doBookChanges" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 28, + "name": "doBlackList" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 30, + "name": "doCanDelete" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 32, + "name": "doChannelAuthorize" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 34, + "name": "doChannelVerify" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 36, + "name": "doConnect" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 38, + "name": "doConsensusInfo" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 40, + "name": "doDepositAuthorized" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 42, + "name": "doFeature" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 44, + "name": "doFee" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 46, + "name": "doFetchInfo" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 48, + "name": "doGatewayBalances" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 50, + "name": "doGetCounts" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 52, + "name": "doGetAggregatePrice" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 54, + "name": "doLedgerAccept" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 56, + "name": "doLedgerCleaner" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 58, + "name": "doLedgerClosed" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 60, + "name": "doLedgerCurrent" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 62, + "name": "doLedgerData" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 64, + "name": "doLedgerEntry" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 66, + "name": "doLedgerHeader" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 68, + "name": "doLedgerRequest" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 70, + "name": "doLogLevel" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 72, + "name": "doLogRotate" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 74, + "name": "doManifest" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 76, + "name": "doNFTBuyOffers" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 78, + "name": "doNFTSellOffers" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 80, + "name": "doNoRippleCheck" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 82, + "name": "doOwnerInfo" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 84, + "name": "doPathFind" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 86, + "name": "doPause" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 88, + "name": "doPeers" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 90, + "name": "doPing" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 92, + "name": "doPrint" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 94, + "name": "doRandom" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 96, + "name": "doResume" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 98, + "name": "doPeerReservationsAdd" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 100, + "name": "doPeerReservationsDel" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 102, + "name": "doPeerReservationsList" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 104, + "name": "doRipplePathFind" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 106, + "name": "doServerDefinitions" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 108, + "name": "doServerInfo" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 110, + "name": "doServerState" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 112, + "name": "doSign" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 114, + "name": "doSignFor" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 116, + "name": "doSimulate" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 118, + "name": "doStop" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 120, + "name": "doSubmit" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 122, + "name": "doSubmitMultiSigned" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 124, + "name": "doSubscribe" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 126, + "name": "doTransactionEntry" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 128, + "name": "doTxJson" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 130, + "name": "doTxHistory" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 132, + "name": "doTxReduceRelay" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 134, + "name": "doUnlList" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 136, + "name": "doUnsubscribe" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 138, + "name": "doValidationCreate" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 140, + "name": "doWalletPropose" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 142, + "name": "doValidators" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 144, + "name": "doValidatorListSites" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 146, + "name": "doValidatorInfo" + }, + { + "args": [ + "RPC::JsonContext&" + ], + "lineno": 148, + "name": "doVaultInfo" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/Handlers.h.ai.md b/src/xrpld/rpc/handlers/Handlers.h.ai.md new file mode 100644 index 0000000000..77c7244bc2 --- /dev/null +++ b/src/xrpld/rpc/handlers/Handlers.h.ai.md @@ -0,0 +1,45 @@ +# `src/xrpld/rpc/handlers/Handlers.h` + +## Role in the System + +This header is the central manifest of every "old-style" RPC handler function in the `rippled` server. It declares 67 free functions — one per public API method — all sharing the same signature: `Json::Value doXxx(RPC::JsonContext&)`. The file's sole purpose is to make all handler symbols available in a single include, which `Handler.cpp` then uses to populate the static dispatch table that routes incoming RPC requests to their implementations. + +## Design Pattern: Old-Style vs New-Style Handlers + +The XRPL RPC subsystem supports two handler registration patterns. The functions declared here represent the **old-style pattern**: plain free functions that accept a `JsonContext&` and return a `Json::Value`. The newer alternative, exemplified by `LedgerHandler` in the only header this file directly includes (`handlers/ledger/Ledger.h`), uses a class with static metadata members (`name`, `role`, `condition`, `minApiVer`, `maxApiVer`) and instance methods `check()` and `writeResult()`. + +The `Handler.cpp` file bridges these two worlds. For the old-style functions it wraps each one with a `byRef()` lambda adapter that converts the return-by-value signature into the `Handler::Method` type expected by the dispatch table. The adapter also enforces a defensive invariant: if a handler accidentally returns a non-object `Json::Value`, it logs the violation and wraps it. New-style handlers are registered directly via `addHandler()`, which reads their static members at compile time and uses `static_assert` to catch version range errors before they reach runtime. + +The deliberate structural comment in the header file on lines 110–112 — `doServerInfo` labelled "for humans" versus `doServerState` labelled "for machines" — illustrates how the old-style pattern encodes operational intent through naming and comments rather than through typed metadata. + +## The Dispatch Table Relationship + +The `HandlerTable` singleton in `Handler.cpp` is where the functions declared here gain their operational identity. Each handler entry pairs the function pointer with a string method name (e.g., `"account_info"`), a `Role` (`USER` or `ADMIN`), and a `Condition` bitmask. The `Condition` enum has three meaningful values — `NEEDS_NETWORK_CONNECTION`, `NEEDS_CURRENT_LEDGER`, and `NEEDS_CLOSED_LEDGER` — and the `conditionMet()` function in `Handler.h` gates every non-`NO_CONDITION` handler against the node's current sync state, amendment-blocked status, and UNL validity before the handler itself ever runs. + +The table uses a `std::multimap` rather than a simple `std::map`, because the same method name can be registered under different API version ranges. This supports the versioned API model where behavior changes between `apiMinimumSupportedVersion` and `apiMaximumSupportedVersion`. The `ledger_header` and `tx_history` handlers, for example, are restricted to API version 1 only (`minApiVer=1, maxApiVer=1`), meaning they were deprecated before API version 2 was introduced. + +## The `RPC::JsonContext` Parameter + +Every handler receives an `RPC::JsonContext&`, which inherits from `RPC::Context`. The base provides the full application environment: `Application& app`, `NetworkOPs& netOps`, `LedgerMaster& ledgerMaster`, `Resource::Consumer& consumer`, `Role role`, and an optional coroutine handle for async handlers. `JsonContext` adds `Json::Value params` (the parsed request body) and HTTP header forwarding fields for user identity and proxy chain tracking. This consolidated context design means handlers never need to reach for global state — everything needed for request execution and audit logging arrives in one argument. + +## Handler Groupings and Feature Surface + +The 67 declarations cover the full XRPL public and administrative API surface: + +- **Account queries**: `doAccountInfo`, `doAccountLines`, `doAccountChannels`, `doAccountNFTs`, `doAccountObjects`, `doAccountOffers`, `doAccountTx`, `doAccountCurrencies` — the core account inspection methods. +- **DEX and orderbook**: `doBookOffers`, `doBookChanges`, `doAMMInfo`, `doGetAggregatePrice` — covering both traditional order-book and Automated Market Maker surfaces. +- **Payment channels**: `doChannelAuthorize`, `doChannelVerify` — off-ledger payment channel primitives. +- **NFTs**: `doNFTBuyOffers`, `doNFTSellOffers` — NFT marketplace queries. +- **Ledger access**: `doLedgerClosed`, `doLedgerCurrent`, `doLedgerData`, `doLedgerEntry`, `doLedgerHeader`, `doLedgerRequest`, `doLedgerAccept`, `doLedgerCleaner` — the last two are admin-only, with `doLedgerAccept` forcing ledger advancement in standalone mode. +- **Transactions**: `doSign`, `doSignFor`, `doSubmit`, `doSubmitMultiSigned`, `doSimulate`, `doTransactionEntry`, `doTxJson`, `doTxHistory`, `doTxReduceRelay` — covering the complete transaction lifecycle from signing to historical lookup. `doSimulate` is a dry-run execution path that validates without broadcasting. +- **Path finding**: `doPathFind`, `doRipplePathFind` — streaming (`doPathFind`) and one-shot (`doRipplePathFind`) path search. +- **Server operations**: `doServerInfo`, `doServerState`, `doServerDefinitions`, `doFee`, `doPause`, `doResume`, `doStop` — `doPause`/`doResume` allow operators to temporarily suspend transaction processing without shutting down the node. +- **Peer and network admin**: `doConnect`, `doPeers`, `doPeerReservationsAdd`, `doPeerReservationsDel`, `doPeerReservationsList`, `doBlackList`, `doFetchInfo`, `doConsensusInfo` — privileged network management methods. +- **Validator infrastructure**: `doValidators`, `doValidatorListSites`, `doValidatorInfo`, `doUnlList`, `doManifest` — UNL and validator set introspection. +- **Utility and legacy**: `doPing`, `doRandom`, `doOwnerInfo`, `doGatewayBalances`, `doNoRippleCheck`, `doDepositAuthorized`, `doCanDelete`, `doFeature`, `doGetCounts`, `doPrint`, `doLogLevel`, `doLogRotate`, `doWalletPropose`, `doValidationCreate` — operational diagnostics and some helpers retained for compatibility. +- **Subscriptions**: `doSubscribe`, `doUnsubscribe` — the WebSocket streaming subscription interface. +- **Newer features**: `doVaultInfo` — vault ledger object inspection, reflecting ongoing protocol development. + +## Architectural Significance + +The deliberate separation of this header from `Handler.cpp` is what makes the dispatch table editable without touching any handler implementation file. Adding a new method requires only declaring it here, implementing it in its own translation unit, and inserting one entry in the `handlerArray` in `Handler.cpp`. The `byRef()` adapter makes the registration mechanical and uniform. The single include of `handlers/ledger/Ledger.h` — rather than `Context.h` directly — is intentional: it ensures that every translation unit including `Handlers.h` receives not just the function declarations but also the full `JsonContext` type definition needed to write a valid call site or a new handler body. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/VaultInfo.cpp.ai.json b/src/xrpld/rpc/handlers/VaultInfo.cpp.ai.json new file mode 100644 index 0000000000..ba0def6ed0 --- /dev/null +++ b/src/xrpld/rpc/handlers/VaultInfo.cpp.ai.json @@ -0,0 +1,539 @@ +{ + "args": [ + { + "lineno": 9, + "name": "params" + }, + { + "lineno": 9, + "name": "jvResult" + }, + { + "lineno": 46, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doVaultInfo", + "parseVault", + "RPC::inject_error" + ], + "entry_point": "doVaultInfo", + "purpose": "Handles the 'vault_info' RPC command, validates input, computes vault index, fetches ledger entries, and returns vault info or error.", + "validation_points": [ + "parseVault: Validates vault_id (parseHex), owner (parseBase58), seq (isInt/isUInt, range), and field combination logic", + "doVaultInfo: Checks parseVault result for beast::zero (malformedRequest), checks ledger entries for existence (entryNotFound)" + ] + } + ], + "data_flows": [ + { + "field": "vault_id", + "flow": [ + "context.params[jss::vault_id]", + "parseVault: params[jss::vault_id].asString()", + "uint256::parseHex", + "uNodeIndex", + "doVaultInfo: keylet::vault(uNodeIndex)" + ], + "origin": "context.params[jss::vault_id] (JSON RPC input)", + "transformations": [ + "String to uint256 via parseHex", + "Used as key for ledger lookup" + ], + "validated_at": "parseVault (parseHex failure triggers error)" + }, + { + "field": "owner", + "flow": [ + "context.params[jss::owner]", + "parseVault: params[jss::owner].asString()", + "parseBase58", + "id", + "keylet::vault(*id, seq)" + ], + "origin": "context.params[jss::owner] (JSON RPC input)", + "transformations": [ + "Base58 string to AccountID", + "Used in keylet::vault to compute vault index" + ], + "validated_at": "parseVault (parseBase58 failure triggers error)" + }, + { + "field": "seq", + "flow": [ + "context.params[jss::seq]", + "parseVault: params[jss::seq]", + "isInt/isUInt, asDouble() range checks", + "params[jss::seq].asUInt()", + "keylet::vault(*id, seq)" + ], + "origin": "context.params[jss::seq] (JSON RPC input)", + "transformations": [ + "JSON value to unsigned int", + "Used in keylet::vault to compute vault index" + ], + "validated_at": "parseVault (type and range checks)" + }, + { + "field": "vault_id/owner/seq combination", + "flow": [ + "context.params", + "parseVault: checks presence/absence of vault_id, owner, seq", + "if/else logic to select valid combinations", + "error if invalid" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Manual logic to enforce only one valid combination" + ], + "validated_at": "parseVault (combination logic)" + } + ], + "description": "Implements the doVaultInfo RPC handler for retrieving information about a vault and its associated issuance from the ledger, including helper logic to parse vault identifiers from request parameters.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vault_id", + "empty", + "string", + "validation" + ], + "evidence": "uint256::parseHex at parseVault", + "issue_pattern": "Missing empty string validation for vault_id", + "why_false_positive": "uint256::parseHex validates vault_id for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "vault_id", + "format", + "validation", + "invalid" + ], + "evidence": "uint256::parseHex at parseVault", + "issue_pattern": "Missing format validation for vault_id", + "why_false_positive": "uint256::parseHex validates vault_id format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "owner", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at parseVault", + "issue_pattern": "Missing empty string validation for owner", + "why_false_positive": "parseBase58 validates owner for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "owner", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at parseVault", + "issue_pattern": "Missing format validation for owner", + "why_false_positive": "parseBase58 validates owner format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "seq", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::seq].isInt() || params[jss::seq].isUInt() at parseVault", + "issue_pattern": "Missing empty string validation for seq", + "why_false_positive": "params[jss::seq].isInt() || params[jss::seq].isUInt() validates seq for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "seq", + "type", + "validation", + "check" + ], + "evidence": "params[jss::seq].isInt() || params[jss::seq].isUInt() at parseVault", + "issue_pattern": "Missing type validation for seq", + "why_false_positive": "params[jss::seq].isInt() || params[jss::seq].isUInt() validates seq type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "seq", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::seq].asDouble() <= 0.0 || params[jss::seq].asDouble() > double(Json::Value::maxUInt) at parseVault", + "issue_pattern": "Missing empty string validation for seq", + "why_false_positive": "params[jss::seq].asDouble() <= 0.0 || params[jss::seq].asDouble() > double(Json::Value::maxUInt) validates seq for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "seq", + "range", + "bounds", + "validation" + ], + "evidence": "params[jss::seq].asDouble() <= 0.0 || params[jss::seq].asDouble() > double(Json::Value::maxUInt) at parseVault", + "issue_pattern": "Missing range validation for seq", + "why_false_positive": "params[jss::seq].asDouble() <= 0.0 || params[jss::seq].asDouble() > double(Json::Value::maxUInt) validates seq range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vault_id, owner, seq (combination)", + "empty", + "string", + "validation" + ], + "evidence": "manual logic (if/else on presence of fields) at parseVault", + "issue_pattern": "Missing empty string validation for vault_id, owner, seq (combination)", + "why_false_positive": "manual logic (if/else on presence of fields) validates vault_id, owner, seq (combination) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger existence", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doVaultInfo", + "issue_pattern": "Missing empty string validation for ledger existence", + "why_false_positive": "RPC::lookupLedger validates ledger existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vault_id/owner/seq (malformed request)", + "empty", + "string", + "validation" + ], + "evidence": "parseVault return value check (uNodeIndex == beast::zero) at doVaultInfo", + "issue_pattern": "Missing empty string validation for vault_id/owner/seq (malformed request)", + "why_false_positive": "parseVault return value check (uNodeIndex == beast::zero) validates vault_id/owner/seq (malformed request) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vault entry existence", + "empty", + "string", + "validation" + ], + "evidence": "lpLedger->read(keylet::vault(uNodeIndex)) at doVaultInfo", + "issue_pattern": "Missing empty string validation for vault entry existence", + "why_false_positive": "lpLedger->read(keylet::vault(uNodeIndex)) validates vault entry existence for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "issuance entry existence", + "empty", + "string", + "validation" + ], + "evidence": "lpLedger->read(keylet::mptIssuance(sleVault->at(sfShareMPTID))) at doVaultInfo", + "issue_pattern": "Missing empty string validation for issuance entry existence", + "why_false_positive": "lpLedger->read(keylet::mptIssuance(sleVault->at(sfShareMPTID))) validates issuance entry existence for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/VaultInfo.cpp", + "functions": [ + { + "args": [ + "params", + "jvResult" + ], + "lineno": 9, + "name": "parseVault" + }, + { + "args": [ + "context" + ], + "lineno": 46, + "name": "doVaultInfo" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by RPC-level tests for the 'vault_info' command, especially in files like 'rpc/Vault_test.cpp', 'rpc/VaultInfo_test.cpp', or integration tests for RPC handlers. Validation paths (invalid vault_id, malformed owner, bad seq, invalid field combinations) should be tested. Gaps may exist if tests do not cover all error branches (e.g., negative seq, seq > maxUInt, both vault_id and owner/seq present, missing all fields). No evidence of unit tests for parseVault itself; coverage depends on RPC test thoroughness.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl RPC framework (jss::, RPC::inject_error, parseBase58, uint256::parseHex)", + "validation_layer": "business_logic (parseVault, doVaultInfo)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcINVALID_PARAMS, jvResult)", + "field": "vault_id", + "location": "parseVault", + "validated_by": "uint256::parseHex", + "validates": [ + "vault_id must be a valid hex string" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcACT_MALFORMED, jvResult)", + "field": "owner", + "location": "parseVault", + "validated_by": "parseBase58", + "validates": [ + "owner must be a valid base58-encoded AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcINVALID_PARAMS, jvResult)", + "field": "seq", + "location": "parseVault", + "validated_by": "params[jss::seq].isInt() || params[jss::seq].isUInt()", + "validates": [ + "seq must be an integer or unsigned integer" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcINVALID_PARAMS, jvResult)", + "field": "seq", + "location": "parseVault", + "validated_by": "params[jss::seq].asDouble() <= 0.0 || params[jss::seq].asDouble() > double(Json::Value::maxUInt)", + "validates": [ + "seq must be > 0 and <= maxUInt" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcINVALID_PARAMS, jvResult)", + "field": "vault_id, owner, seq (combination)", + "location": "parseVault", + "validated_by": "manual logic (if/else on presence of fields)", + "validates": [ + "Either vault_id is present and owner/seq are absent, OR owner and seq are present and vault_id is absent" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns error result from lookupLedger", + "field": "ledger existence", + "location": "doVaultInfo", + "validated_by": "RPC::lookupLedger", + "validates": [ + "ledger must exist and be accessible" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "jvResult[jss::error] = \"malformedRequest\"", + "field": "vault_id/owner/seq (malformed request)", + "location": "doVaultInfo", + "validated_by": "parseVault return value check (uNodeIndex == beast::zero)", + "validates": [ + "parseVault must return a valid node index" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "jvResult[jss::error] = \"entryNotFound\"", + "field": "vault entry existence", + "location": "doVaultInfo", + "validated_by": "lpLedger->read(keylet::vault(uNodeIndex))", + "validates": [ + "vault entry must exist in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "jvResult[jss::error] = \"entryNotFound\"", + "field": "issuance entry existence", + "location": "doVaultInfo", + "validated_by": "lpLedger->read(keylet::mptIssuance(sleVault->at(sfShareMPTID)))", + "validates": [ + "issuance entry must exist in ledger" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/VaultInfo.cpp.ai.md b/src/xrpld/rpc/handlers/VaultInfo.cpp.ai.md new file mode 100644 index 0000000000..4e43cff42d --- /dev/null +++ b/src/xrpld/rpc/handlers/VaultInfo.cpp.ai.md @@ -0,0 +1,33 @@ +# `VaultInfo.cpp` — RPC Handler for Vault and Share Issuance Lookup + +This file implements the `doVaultInfo` RPC handler, introduced in XRPL server version 3.1.0 as part of the XLS-66 Lending Protocol. It allows clients to query the current state of a vault ledger object together with its associated MPT (Multi-Purpose Token) share issuance. Vaults are a compound ledger construct: the vault object (`ltVAULT`) carries ownership, configuration, and a reference to a share token ID, while a separate `ltMPTOKEN_ISSUANCE` object tracks the share tokens distributed to vault participants. The handler must therefore locate and assemble two distinct ledger entries before returning a coherent response. + +## Vault Identifier Parsing — `parseVault` + +The static helper `parseVault` handles the dual addressing scheme for vaults. Clients can locate a vault in two mutually exclusive ways: + +1. **Direct lookup** via `vault_id`: a hex-encoded 256-bit ledger object key passed as a string. +2. **Derived lookup** via `owner` + `seq`: the creating account's base58-encoded address and the sequence number from the creating transaction, which together deterministically produce the same 256-bit key via `keylet::vault(accountID, seq)`. + +The function enforces strict mutual exclusivity — exactly one combination must be present. Providing all three fields, mixing forms (e.g. `owner` without `seq`), or providing none all trigger `rpcINVALID_PARAMS`. This mirrors the addressing pattern used elsewhere in the XRPL RPC layer for objects that can be referenced by either a canonical ID or a logical key pair, and eliminates any ambiguity about which form takes precedence. + +The `seq` validation is deliberately layered. It first checks that the JSON value is an integer or unsigned integer type (rejecting strings and floats), then checks range via `asDouble()`: the value must be positive and must not exceed `Json::Value::maxUInt`. The double-comparison approach is a practical hedge against JSON parsers that may lose integer precision or fail to distinguish zero from a negative float at the type level; it also guards against sequence zero, which is invalid in XRPL. + +`parseVault` returns `std::optional`, using `std::nullopt` to signal parse failure, but notably it injects the error into `jvResult` before returning rather than leaving that to the caller. The caller in `doVaultInfo` then checks for `beast::zero` as a sentinel after calling `.value_or(beast::zero)`. This is a minor coupling: `beast::zero` technically represents the all-zeros 256-bit value, but in practice XRPL ledger object keys are derived from cryptographic hashes and will never be the zero value, making this sentinel safe. + +## Handler Logic — `doVaultInfo` + +`doVaultInfo` follows the standard XRPL RPC handler structure: resolve the target ledger via `RPC::lookupLedger`, then operate on an immutable `ReadView const`. The ledger is resolved before vault parsing — if the ledger reference is invalid the handler returns immediately without parsing vault parameters, since there is nothing to look up. + +The two-phase ledger read is the structural core of the handler: + +1. Read the vault SLE via `keylet::vault(uNodeIndex)`, using the already-resolved 256-bit key (the inline overload from `Indexes.h` that wraps the key as `ltVAULT`). +2. If the vault SLE exists, read the MPT issuance via `keylet::mptIssuance(sleVault->at(sfShareMPTID))`, extracting the share token ID from the vault object itself. + +The conditional chain collapses both cases into a single null check: `sleIssuance` is initialized to `nullptr` when `sleVault` is null, so the check `!sleVault || !sleIssuance` handles vault-not-found and orphaned-vault-without-issuance with the same `"entryNotFound"` error. A vault without a corresponding issuance object would represent ledger inconsistency, so treating both absences identically is correct — there is no useful partial response to return. + +The response structure embeds the issuance JSON under `vault.shares`, nesting share token metadata directly within the vault object rather than returning both as parallel top-level fields. This reflects the conceptual model of XLS-66: shares are a property of the vault, not a separate entity at the API level. Both the vault SLE and issuance SLE are serialized in full via `getJson(JsonOptions::none)`. + +## Relationship to the Broader RPC System + +`doVaultInfo` is declared in `Handlers.h` alongside the full set of RPC command handlers and reaches callers via the standard dispatch table, using the same `Json::Value(RPC::JsonContext&)` signature. The handler directory at the time of writing contains relatively few files, making `VaultInfo.cpp` one of the more recent additions. Its dual-identifier addressing pattern and two-object ledger read are somewhat distinctive compared to simpler handlers like `ChannelVerify.cpp`, but both are grounded in the same `lookupLedger` + `ReadView` idiom that characterizes the entire RPC handler layer. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountChannels.cpp.ai.json b/src/xrpld/rpc/handlers/account/AccountChannels.cpp.ai.json new file mode 100644 index 0000000000..79db953546 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountChannels.cpp.ai.json @@ -0,0 +1,587 @@ +{ + "args": [ + { + "lineno": 10, + "name": "jsonLines" + }, + { + "lineno": 10, + "name": "line" + }, + { + "lineno": 34, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doAccountChannels", + "RPC::lookupLedger", + "parseBase58", + "ledger->exists(keylet::account(accountID))", + "readLimitField", + "addChannel" + ], + "entry_point": "doAccountChannels", + "purpose": "Handles the 'account_channels' RPC request: validates input, locates ledger, parses account, checks existence, processes optional fields, and collects payment channel data.", + "validation_points": [ + "doAccountChannels: params.isMember(jss::account)", + "doAccountChannels: params[jss::account].isString()", + "RPC::lookupLedger: ledger_hash / ledger_index validation", + "doAccountChannels: parseBase58(params[jss::account].asString())", + "doAccountChannels: ledger->exists(keylet::account(accountID))", + "doAccountChannels: parseBase58(strDst) (if destination_account present)", + "doAccountChannels: readLimitField (limit validation)", + "doAccountChannels: params.isMember(jss::marker) && params[jss::marker].isString()" + ] + }, + { + "call_chain": [ + "addChannel" + ], + "entry_point": "addChannel", + "purpose": "Formats a payment channel SLE into a JSON object for the response.", + "validation_points": [ + "addChannel: publicKeyType(line[sfPublicKey]) (checks public key validity)" + ] + } + ], + "data_flows": [ + { + "field": "account", + "flow": [ + "params[jss::account]", + "doAccountChannels: isMember/isString validation", + "parseBase58(params[jss::account].asString())", + "accountID", + "ledger->exists(keylet::account(accountID))", + "used for directory traversal and filtering" + ], + "origin": "params[jss::account] (JSON RPC input)", + "transformations": [ + "String \u2192 AccountID (via parseBase58)", + "Checked for existence in ledger" + ], + "validated_at": "doAccountChannels: isMember, isString, parseBase58, ledger->exists" + }, + { + "field": "ledger_hash / ledger_index", + "flow": [ + "params", + "RPC::lookupLedger", + "ledger (ReadView const)" + ], + "origin": "params (JSON RPC input)", + "transformations": [ + "Validated and resolved to a ledger object" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "destination_account", + "flow": [ + "params[jss::destination_account]", + "strDst", + "parseBase58(strDst)", + "raDstAccount (optional)" + ], + "origin": "params[jss::destination_account] (optional, JSON RPC input)", + "transformations": [ + "String \u2192 AccountID (if present and valid)" + ], + "validated_at": "doAccountChannels: parseBase58(strDst)" + }, + { + "field": "limit", + "flow": [ + "params[\"limit\"]", + "readLimitField", + "limit (unsigned int)" + ], + "origin": "params[\"limit\"] (optional, JSON RPC input)", + "transformations": [ + "String/int \u2192 unsigned int (with bounds checking)" + ], + "validated_at": "readLimitField" + }, + { + "field": "marker", + "flow": [ + "params[\"marker\"]", + "isString validation", + "parsed into startAfter (uint256) and startHint (uint64_t)" + ], + "origin": "params[\"marker\"] (optional, JSON RPC input)", + "transformations": [ + "String \u2192 (uint256, uint64_t) via parsing" + ], + "validated_at": "doAccountChannels: params.isMember(jss::marker) && params[jss::marker].isString()" + }, + { + "field": "channel data (SLE)", + "flow": [ + "ledger directory", + "SLE objects", + "addChannel", + "jsonChannels (JSON array in response)" + ], + "origin": "ledger directory traversal", + "transformations": [ + "SLE \u2192 JSON object (field mapping, formatting, public key encoding)" + ], + "validated_at": "addChannel: publicKeyType(line[sfPublicKey])" + } + ], + "description": "Implements the XRPL RPC method 'account_channels', which returns payment channels associated with a given account, supporting pagination and filtering by destination account.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "account (presence, type, format, existence)", + "validation", + "missing", + "check" + ], + "evidence": "Field account (presence, type, format, existence) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for account (presence, type, format, existence)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates account (presence, type, format, existence) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "destination_account (format if present)", + "validation", + "missing", + "check" + ], + "evidence": "Field destination_account (format if present) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for destination_account (format if present)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates destination_account (format if present) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "limit (type, range if present)", + "validation", + "missing", + "check" + ], + "evidence": "Field limit (type, range if present) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for limit (type, range if present)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates limit (type, range if present) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash / ledger_index (existence via lookupLedger)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash / ledger_index (existence via lookupLedger) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for ledger_hash / ledger_index (existence via lookupLedger)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates ledger_hash / ledger_index (existence via lookupLedger) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::account) at doAccountChannels", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "params.isMember(jss::account) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::account].isString() at doAccountChannels", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "params[jss::account].isString() validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "account", + "type", + "validation", + "check" + ], + "evidence": "params[jss::account].isString() at doAccountChannels", + "issue_pattern": "Missing type validation for account", + "why_false_positive": "params[jss::account].isString() validates account type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger_hash / ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doAccountChannels", + "issue_pattern": "Missing empty string validation for ledger_hash / ledger_index", + "why_false_positive": "RPC::lookupLedger validates ledger_hash / ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58(params[jss::account].asString()) at doAccountChannels", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "parseBase58(params[jss::account].asString()) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "account", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58(params[jss::account].asString()) at doAccountChannels", + "issue_pattern": "Missing format validation for account", + "why_false_positive": "parseBase58(params[jss::account].asString()) validates account format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "ledger->exists(keylet::account(accountID)) at doAccountChannels", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "ledger->exists(keylet::account(accountID)) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "destination_account", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58(strDst) at doAccountChannels", + "issue_pattern": "Missing empty string validation for destination_account", + "why_false_positive": "parseBase58(strDst) validates destination_account for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "destination_account", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58(strDst) at doAccountChannels", + "issue_pattern": "Missing format validation for destination_account", + "why_false_positive": "parseBase58(strDst) validates destination_account format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "limit", + "empty", + "string", + "validation" + ], + "evidence": "readLimitField(limit, RPC::Tuning::accountChannels, context) at doAccountChannels", + "issue_pattern": "Missing empty string validation for limit", + "why_false_positive": "readLimitField(limit, RPC::Tuning::accountChannels, context) validates limit for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/AccountChannels.cpp", + "functions": [ + { + "args": [ + "jsonLines", + "line" + ], + "lineno": 10, + "name": "addChannel" + }, + { + "args": [ + "context" + ], + "lineno": 34, + "name": "doAccountChannels" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation and data flow logic is exercised by integration and unit tests for the 'account_channels' RPC. Likely test files: 'rpc/account_channels_test.cpp', 'rpc/AccountChannels_test.cpp', or similar in the test suite. These should cover: missing/invalid account, malformed account, non-existent account, invalid ledger, invalid limit, invalid marker, and correct channel listing. Gaps may exist for edge cases in marker parsing, destination_account validation, and malformed public keys in SLEs. Exception handling paths (e.g., parse errors, ledger lookup failures) should be explicitly tested.", + "validation_architecture": { + "auto_validated_fields": [ + "account (presence, type, format, existence)", + "destination_account (format if present)", + "limit (type, range if present)", + "ledger_hash / ledger_index (existence via lookupLedger)" + ], + "framework": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "validation_layer": "entry_point (doAccountChannels)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::account)", + "field": "account", + "location": "doAccountChannels", + "validated_by": "params.isMember(jss::account)", + "validates": [ + "Checks that the 'account' field is present in the input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::account)", + "field": "account", + "location": "doAccountChannels", + "validated_by": "params[jss::account].isString()", + "validates": [ + "Checks that the 'account' field is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "result (error JSON from lookupLedger)", + "field": "ledger_hash / ledger_index", + "location": "doAccountChannels", + "validated_by": "RPC::lookupLedger", + "validates": [ + "Checks that the specified ledger exists and is accessible" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_MALFORMED)", + "field": "account", + "location": "doAccountChannels", + "validated_by": "parseBase58(params[jss::account].asString())", + "validates": [ + "Checks that the 'account' field is a valid base58-encoded AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_NOT_FOUND)", + "field": "account", + "location": "doAccountChannels", + "validated_by": "ledger->exists(keylet::account(accountID))", + "validates": [ + "Checks that the account exists in the specified ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_MALFORMED)", + "field": "destination_account", + "location": "doAccountChannels", + "validated_by": "parseBase58(strDst)", + "validates": [ + "Checks that the 'destination_account' field, if present, is a valid base58-encoded AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "*err (error JSON from readLimitField)", + "field": "limit", + "location": "doAccountChannels", + "validated_by": "readLimitField(limit, RPC::Tuning::accountChannels, context)", + "validates": [ + "Checks that the 'limit' field, if present, is an integer", + "Checks that the 'limit' field is within allowed range (as per RPC::Tuning::accountChannels)" + ], + "validation_type": "range|type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountChannels.cpp.ai.md b/src/xrpld/rpc/handlers/account/AccountChannels.cpp.ai.md new file mode 100644 index 0000000000..27eb11d161 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountChannels.cpp.ai.md @@ -0,0 +1,39 @@ +# `AccountChannels.cpp` — RPC Handler for `account_channels` + +This file implements the XRPL RPC method `account_channels`, which returns the set of payment channels for which a given account is the source (funding) party. It lives among a cluster of account-query handlers in `src/xrpld/rpc/handlers/account/` — all of which share the same pattern of ledger lookup, owner-directory traversal, and cursor-based pagination. + +## Role in the System + +Payment channels (`ltPAYCHAN`) allow an account to lock up XRP and make rapid off-ledger micropayments to a destination account. The `account_channels` query gives callers a read-only view of these objects as they exist in a specific ledger version. It is a pure read path — no state is modified and the result is drawn entirely from the immutable `ReadView` of the ledger. + +## The Two Functions + +`addChannel(jsonLines, line)` is a pure serialization helper. It receives an already-resolved `SLE const&` of type `ltPAYCHAN` and appends a JSON object to the response array. The mandatory fields — `channel_id`, `account`, `destination_account`, `amount`, `balance`, `settle_delay` — are always emitted. Optional fields (`expiration`, `cancel_after`, `source_tag`, `destination_tag`) are emitted only when present in the SLE via the `[~sfField]` optional accessor idiom. The public key gets special treatment: `publicKeyType()` validates that the raw bytes in `sfPublicKey` actually represent a recognized key type before constructing a `PublicKey` object. This guards against encoding garbage if the field is somehow set to a zero-length or malformed value, and it explains why the key is emitted in both human-readable base58 (`jss::public_key`) and raw hex (`jss::public_key_hex`) forms — two representations for two different client needs. + +`doAccountChannels(context)` is the RPC entry point. It performs a strict validation sequence before touching the ledger: + +1. `account` presence and string-type checks, then `parseBase58` for format validity, then `ledger->exists(keylet::account(accountID))` for on-ledger existence. +2. Optional `destination_account` is parsed the same way, converting the empty-string sentinel to `std::nullopt` to cleanly separate "not provided" from "provided but invalid." +3. `readLimitField` enforces the `RPC::Tuning::accountChannels` bounds of `{min=10, default=200, max=400}`. + +## Pagination Design + +The handler uses the same compound-marker scheme as `account_lines` and `account_offers`. The marker string is `","`. The `uint256` is the ledger key of the last-seen SLE; the `uint64` is the directory page index (the hint) that makes resumption efficient. + +The `forEachItemAfter` function in `DirectoryHelpers.cpp` accepts this hint and attempts to jump directly to the right page in the owner directory before scanning forward. Without it, each resumption of a paginated query would have to walk from the beginning of the owner directory — a significant cost for accounts with many objects. + +The traversal asks for `limit + 1` items. Inside the callback, the `limit`-th item speculatively sets `marker` and `nextHint`. The marker is only included in the response when `count` actually reaches `limit + 1`, i.e., when the iterator confirmed a subsequent item exists. This prevents emitting an empty-page marker that would cause the client to make a useless follow-up request. The comment at line 179 calls this out explicitly: both conditions — `count == limit + 1` and `marker.has_value()` — must be true. + +## Filtering and Scope + +The callback filters SLEs on two criteria: type must be `ltPAYCHAN`, and `sfAccount` must equal `accountID`. The second condition is necessary because an account's owner directory in the XRPL is a generic index of all objects owned by that account, and technically can contain objects of various types. Channels where the queried account is only the *destination* appear in the source account's directory, not the destination's. This means `account_channels` deliberately returns only outgoing (source) channels. An optional `destination_account` filter narrows results further within the callback. + +## Marker Security Check + +Before starting the directory walk, if a marker is provided, the handler reads the SLE pointed to by `startAfter` and calls `RPC::isRelatedToAccount(*ledger, sle, accountID)`. This check prevents a client from supplying a marker that belongs to a different account's objects, which would cause the traversal to start in the wrong account's namespace. Failing this check returns `rpcINVALID_PARAMS` rather than silently returning unrelated results. + +## Error Handling and Cost + +The `UNREACHABLE` macro at the null-SLE branch (marked `LCOV_EXCL_START`) documents a defensive invariant: `forEachItemAfter` only calls the callback with keys that exist in the directory, so a null `sleCur` here would imply ledger corruption, not a recoverable error. The code returns `false` (stopping iteration) but the condition is logically impossible in a healthy system. + +The handler assigns `Resource::feeMediumBurdenRPC` to `context.loadType`, reflecting that directory traversal over potentially hundreds of items is moderately expensive compared to point lookups. This feeds into the RPC server's resource management, which can throttle or deprioritize expensive client connections. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp.ai.json b/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp.ai.json new file mode 100644 index 0000000000..8307262ac6 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp.ai.json @@ -0,0 +1,430 @@ +{ + "args": [ + { + "lineno": 11, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doAccountCurrencies", + "RPC::lookupLedger", + "parseBase58", + "RPCTrustLine::getItems" + ], + "entry_point": "doAccountCurrencies", + "purpose": "Handles the account_currencies RPC request, validates input, fetches ledger and account, computes send/receive currencies.", + "validation_points": [ + "doAccountCurrencies: params.isMember(jss::account) || params.isMember(jss::ident)", + "doAccountCurrencies: params[jss::account].isString() / params[jss::ident].isString()", + "RPC::lookupLedger: validates ledger existence and parameters", + "doAccountCurrencies: parseBase58(strIdent) validates account identifier" + ] + } + ], + "data_flows": [ + { + "field": "account / ident", + "flow": [ + "context.params", + "doAccountCurrencies: checked for presence (isMember)", + "doAccountCurrencies: checked for type (isString)", + "doAccountCurrencies: value assigned to strIdent", + "parseBase58(strIdent)", + "accountID (used for ledger lookup and trust line queries)" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "String extraction from JSON", + "Base58 parsing to AccountID" + ], + "validated_at": "doAccountCurrencies: explicit if-checks and parseBase58" + }, + { + "field": "ledger", + "flow": [ + "doAccountCurrencies: calls RPC::lookupLedger", + "RPC::lookupLedger: validates and assigns ledger pointer", + "ledger used for account existence check and trust line queries" + ], + "origin": "context (via RPC::lookupLedger)", + "transformations": [ + "Pointer assignment", + "Validation of ledger existence" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "accountID", + "flow": [ + "strIdent", + "parseBase58(strIdent)", + "accountID", + "ledger->exists(keylet::account(accountID))", + "RPCTrustLine::getItems(accountID, *ledger)" + ], + "origin": "parseBase58(strIdent)", + "transformations": [ + "Base58 string to AccountID" + ], + "validated_at": "parseBase58(strIdent)" + }, + { + "field": "send/receive currencies", + "flow": [ + "RPCTrustLine::getItems", + "for-loop over trust lines", + "insert into send/receive sets", + "erase badCurrency", + "to_string for JSON output" + ], + "origin": "RPCTrustLine::getItems(accountID, *ledger)", + "transformations": [ + "Set insertion", + "Filtering (badCurrency)", + "String conversion" + ], + "validated_at": "No explicit validation; relies on trust line data integrity" + } + ], + "description": "Implements the doAccountCurrencies RPC handler, which returns the currencies an account can send or receive based on its trust lines in the XRPL ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account / ident", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at doAccountCurrencies", + "issue_pattern": "Missing empty string validation for account / ident", + "why_false_positive": "explicit if-check validates account / ident for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::account].isString() at doAccountCurrencies", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "params[jss::account].isString() validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "account", + "type", + "validation", + "check" + ], + "evidence": "params[jss::account].isString() at doAccountCurrencies", + "issue_pattern": "Missing type validation for account", + "why_false_positive": "params[jss::account].isString() validates account type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ident", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::ident].isString() at doAccountCurrencies", + "issue_pattern": "Missing empty string validation for ident", + "why_false_positive": "params[jss::ident].isString() validates ident for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ident", + "type", + "validation", + "check" + ], + "evidence": "params[jss::ident].isString() at doAccountCurrencies", + "issue_pattern": "Missing type validation for ident", + "why_false_positive": "params[jss::ident].isString() validates ident type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doAccountCurrencies", + "issue_pattern": "Missing empty string validation for ledger", + "why_false_positive": "RPC::lookupLedger validates ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account identifier (strIdent)", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at doAccountCurrencies", + "issue_pattern": "Missing empty string validation for account identifier (strIdent)", + "why_false_positive": "parseBase58 validates account identifier (strIdent) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "account identifier (strIdent)", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at doAccountCurrencies", + "issue_pattern": "Missing format validation for account identifier (strIdent)", + "why_false_positive": "parseBase58 validates account identifier (strIdent) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account existence", + "empty", + "string", + "validation" + ], + "evidence": "ledger->exists(keylet::account(accountID)) at doAccountCurrencies", + "issue_pattern": "Missing empty string validation for account existence", + "why_false_positive": "ledger->exists(keylet::account(accountID)) validates account existence for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 10, + "name": "doAccountCurrencies" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in RPC-level integration tests, e.g., 'account_currencies' tests in test/rpc/AccountCurrencies_test.cpp or similar. Tests should cover: missing/invalid account/ident fields, malformed account IDs, non-existent accounts, and correct send/receive currency computation. Gaps may exist if edge cases (e.g., non-string types, malformed Base58, missing ledger) are not explicitly tested. No evidence in this file of unit tests for parseBase58 or ledger lookup error paths; these may rely on upstream component tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss::, RPC::, parseBase58, etc.)", + "validation_layer": "entry_point (handler function doAccountCurrencies)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::account)", + "field": "account / ident", + "location": "doAccountCurrencies", + "validated_by": "explicit if-check", + "validates": [ + "At least one of 'account' or 'ident' must be present in params" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::account)", + "field": "account", + "location": "doAccountCurrencies", + "validated_by": "params[jss::account].isString()", + "validates": [ + "'account' field must be a string if present" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::ident)", + "field": "ident", + "location": "doAccountCurrencies", + "validated_by": "params[jss::ident].isString()", + "validates": [ + "'ident' field must be a string if present" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "result (error from lookupLedger)", + "field": "ledger", + "location": "doAccountCurrencies", + "validated_by": "RPC::lookupLedger", + "validates": [ + "Ledger must exist and be accessible" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcACT_MALFORMED, result)", + "field": "account identifier (strIdent)", + "location": "doAccountCurrencies", + "validated_by": "parseBase58", + "validates": [ + "'account' or 'ident' must be a valid base58-encoded AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_NOT_FOUND)", + "field": "account existence", + "location": "doAccountCurrencies", + "validated_by": "ledger->exists(keylet::account(accountID))", + "validates": [ + "Account must exist in the ledger" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp.ai.md b/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp.ai.md new file mode 100644 index 0000000000..4680c7de2e --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountCurrencies.cpp.ai.md @@ -0,0 +1,53 @@ +# `AccountCurrencies.cpp` — `account_currencies` RPC Handler + +## Purpose + +This file implements `doAccountCurrencies`, the handler for the XRPL `account_currencies` RPC command. Its job is narrow but useful: given an account identifier, enumerate the distinct currencies that account is currently capable of sending and receiving, based purely on its trust line state at a specific ledger. The result is two flat arrays of currency codes (`send_currencies` and `receive_currencies`), making it a lightweight alternative to the full `account_lines` command when callers only need to know *what* currencies are in play, not the complete trust line details. + +## Validation Pipeline + +The function applies a strict sequential validation chain before doing any ledger work: + +1. **Presence check**: At least one of `account` or `ident` must appear in the request params. `ident` is a legacy alias for `account`, preserved for backwards compatibility with older clients; `account` takes priority. +2. **Type check**: Whichever field is present must be a JSON string, not a number or object, returning `invalid_field_error` otherwise. +3. **Ledger resolution**: `RPC::lookupLedger` resolves the target ledger — defaulting to the current validated ledger or respecting an explicit `ledger_index`/`ledger_hash` parameter. If the ledger cannot be found, the result from `lookupLedger` is returned directly (it already carries the appropriate error). +4. **Base58 decode**: `parseBase58` converts the string to a 20-byte `AccountID`. An invalid Base58 string injects `rpcACT_MALFORMED` into the result. +5. **Account existence**: `ledger->exists(keylet::account(accountID))` ensures the account has an entry in the ledger's state map. A missing account returns `rpcACT_NOT_FOUND`. + +Each step gates on the previous; the code never proceeds to trust line enumeration until all preconditions pass. + +## Send vs. Receive Determination + +The core logic iterates over trust lines returned by `RPCTrustLine::getItems`, which enumerates all `RippleState` ledger objects associated with the account. For each line the handler examines two conditions: + +```cpp +if (saBalance < rspEntry.getLimit()) + receive.insert(saBalance.get().currency); +if ((-saBalance) < rspEntry.getLimitPeer()) + send.insert(saBalance.get().currency); +``` + +`getBalance()` returns the balance from the queried account's perspective: positive means the account holds the peer's IOU; negative means the peer holds the account's IOU. `getLimit()` is the account's own trust limit — how much of the peer's IOU it is willing to hold. If the balance is below that limit, there is still room to receive more, so the currency goes into the `receive` set. Conversely, negating the balance gives the quantity of the account's own IOU that the peer currently holds. If that is below the peer's trust limit (`getLimitPeer()`), the peer can still accept more, meaning the account can still send — regardless of whether it holds a positive balance today. + +This distinction matters architecturally: a currency appears in `send_currencies` if there is *capacity on the peer side*, not if the sender currently has a balance. An account with a zero balance on a trust line can still appear as a sender if the peer's trust limit isn't exhausted. + +## `badCurrency()` Filtering + +After the loop, both sets explicitly remove `badCurrency()`: + +```cpp +send.erase(badCurrency()); +receive.erase(badCurrency()); +``` + +`badCurrency()` is a deliberately reserved sentinel — the three-letter code that looks like "XRP" in ASCII — which is disallowed on trust lines to prevent confusion with native XRP. Trust line data should never contain it in practice, but the erase call is a defensive guard against any malformed state that might have slipped through, ensuring it never surfaces in the API response. + +## Relationship to `RPCTrustLine` and `AccountLines` + +`RPCTrustLine` (declared in `TrustLine.h`) is the RPC-layer wrapper around `RippleState` SLEs. It normalises the "low account / high account" binary that the ledger uses to store trust line directionality into a consistent view from a chosen account's perspective. `RPCTrustLine::getItems` performs the directory walk over the account's owner directory to collect all associated trust lines. The same class and `getItems` method are used by `AccountLines.cpp`, which exposes the full trust line detail including quality in/out, freeze flags, and peer authorization. `AccountCurrencies` consumes a strict subset of that data — only `getBalance()`, `getLimit()`, and `getLimitPeer()` — which is why it needs no pagination and returns quickly even for accounts with many trust lines. + +The ledger is accessed through `ReadView const&`, the immutable read-only interface, so the handler holds a consistent snapshot throughout and cannot observe mid-request mutations. + +## Error Handling and Resource Management + +All errors are returned as `Json::Value` objects, consistent with the rest of the xrpld RPC layer — there are no exceptions. The ledger is held via `std::shared_ptr`, keeping the ledger snapshot alive for the duration of the call without risk of early deallocation. The two `std::set` containers used to deduplicate currencies are stack-local and automatically cleaned up on return. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp.ai.json b/src/xrpld/rpc/handlers/account/AccountInfo.cpp.ai.json new file mode 100644 index 0000000000..c815969494 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp.ai.json @@ -0,0 +1,257 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doAccountInfo", + "RPC::lookupLedger", + "parseBase58", + "injectSLE" + ], + "entry_point": "doAccountInfo", + "purpose": "Handles the 'account_info' RPC request: validates input, looks up ledger, parses account, injects ledger entry info into JSON.", + "validation_points": [ + "doAccountInfo: params.isMember(jss::account) + isString", + "doAccountInfo: params.isMember(jss::ident) + isString", + "doAccountInfo: parseBase58(strIdent) (validates account format)" + ] + } + ], + "data_flows": [ + { + "field": "account", + "flow": [ + "context.params", + "doAccountInfo: params.isMember(jss::account)", + "doAccountInfo: strIdent = params[jss::account].asString()", + "parseBase58(strIdent)", + "accountID", + "ledger->read(keylet::account(accountID))", + "injectSLE" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Checked for presence (isMember)", + "Checked for type (isString)", + "Converted to string (asString)", + "Parsed to AccountID (parseBase58)", + "Used as key for ledger lookup" + ], + "validated_at": "doAccountInfo: isMember + isString + parseBase58" + }, + { + "field": "ident", + "flow": [ + "context.params", + "doAccountInfo: params.isMember(jss::ident)", + "doAccountInfo: strIdent = params[jss::ident].asString()", + "parseBase58(strIdent)", + "accountID", + "ledger->read(keylet::account(accountID))", + "injectSLE" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Checked for presence (isMember)", + "Checked for type (isString)", + "Converted to string (asString)", + "Parsed to AccountID (parseBase58)", + "Used as key for ledger lookup" + ], + "validated_at": "doAccountInfo: isMember + isString + parseBase58" + } + ], + "description": "Implements the account_info RPC handler for the XRPL, providing detailed information about an account, including account data, flags, pseudo account fields, signer lists, and queued transactions if requested.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "isMember + isString + invalid_field_error/missing_field_error at doAccountInfo", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "isMember + isString + invalid_field_error/missing_field_error validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ident", + "empty", + "string", + "validation" + ], + "evidence": "isMember + isString + invalid_field_error at doAccountInfo", + "issue_pattern": "Missing empty string validation for ident", + "why_false_positive": "isMember + isString + invalid_field_error validates ident for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/AccountInfo.cpp", + "functions": [ + { + "args": [ + "jv", + "sle" + ], + "lineno": 18, + "name": "injectSLE" + }, + { + "args": [ + "context" + ], + "lineno": 49, + "name": "doAccountInfo" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic for 'account' and 'ident' fields (presence, type, and format) is straightforward and likely covered by RPC-level tests. Look for tests in files like 'account_info_test.cpp', 'AccountInfo_test.cpp', or generic RPC handler tests. Gaps may exist for edge cases: missing fields, wrong types, malformed account strings, or both 'account' and 'ident' missing. There is no evidence in this file of unit tests for injectSLE, so coverage for Gravatar URL logic or non-account-root handling may be limited unless tested elsewhere.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss::, RPC::error helpers)", + "validation_layer": "entry_point (handler function doAccountInfo)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error or RPC::missing_field_error", + "field": "account", + "location": "doAccountInfo", + "validated_by": "isMember + isString + invalid_field_error/missing_field_error", + "validates": [ + "Checks if 'account' field is present in params", + "Checks if 'account' field is a string" + ], + "validation_type": "type|presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error", + "field": "ident", + "location": "doAccountInfo", + "validated_by": "isMember + isString + invalid_field_error", + "validates": [ + "Checks if 'ident' field is present in params (if 'account' is not present)", + "Checks if 'ident' field is a string" + ], + "validation_type": "type|presence" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp.ai.md b/src/xrpld/rpc/handlers/account/AccountInfo.cpp.ai.md new file mode 100644 index 0000000000..49d9be56fe --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp.ai.md @@ -0,0 +1,64 @@ +# `AccountInfo.cpp` — RPC Handler for `account_info` + +## Role and Context + +`AccountInfo.cpp` implements `doAccountInfo`, the server-side handler for the XRPL `account_info` JSON-RPC method. It sits inside `src/xrpld/rpc/handlers/account/`, alongside sibling handlers like `AccountLines`, `AccountOffers`, and `AccountObjects`. Together these form the account-centric slice of the RPC surface; `AccountInfo` is the lowest-level entry point — it reads the account root object directly, rather than enumerating linked objects. + +The file also defines `injectSLE`, a small helper that serializes a `SLE` (Serialized Ledger Entry) to JSON, augmented with a Gravatar URL when an email hash is present. + +## Input Parsing and Validation + +`doAccountInfo` accepts the account identifier under either the canonical `account` key or the legacy `ident` alias. The `ident` alias predates the current API and is preserved purely for backward compatibility. Both paths enforce `isString()` before proceeding; a missing or wrong-typed value returns an `invalid_field_error` or `missing_field_error` immediately, before any ledger access occurs. + +The account string is decoded via `parseBase58`, which rejects malformed addresses with `rpcACT_MALFORMED`. Ledger resolution (`RPC::lookupLedger`) runs before account parsing: if the caller requested a non-existent ledger hash or index, the function returns early with the ledger-error response and never touches account state. + +## Flag Serialization + +Rather than returning raw bitmask integers, the handler translates ledger-specific flags into a human-readable `account_flags` JSON object — a named boolean for each flag. Three static constexpr structures organise these: + +- **`lsFlags`** — the nine core flags always included regardless of enabled amendments (e.g., `defaultRipple`, `depositAuth`, `disableMasterKey`). +- **`disallowIncomingFlags`** — four "disallow incoming" flags introduced with NFToken and related amendments, but included unconditionally in the response once the server is current enough to know the field names. +- **`allowTrustLineClawbackFlag`** and **`allowTrustLineLockingFlag`** — individually gated on `featureClawback` and `featureTokenEscrow` respectively. These only appear in the response when the requested ledger has those amendments active, preventing false `false` readings against older ledgers. + +This design separates amendment-awareness (whether a field exists in the ledger) from the response shape: unconstrained flags are always serialized so clients can depend on a stable key set, while genuinely amendment-dependent flags are withheld until meaningful. + +## Pseudo-Account Detection + +`getPseudoAccountFields()` returns a lazily-initialized static list of `SField` pointers whose definitions carry the `sMD_PseudoAccount` metadata flag. Pseudo-accounts are special on-ledger accounts created by protocol features such as `SingleAssetVault` or `LendingProtocol`; they cannot submit transactions, carry no reserve requirement, and have a designated owner field linking them to their controlling object. + +The handler iterates these fields and, if any is present on the account root, sets `result["pseudo_account"]["type"]` to the trimmed field name (stripping a trailing `"ID"` suffix for readability). The invariant that only one such field can be set is enforced elsewhere (by `InvariantCheck`), so the loop breaks after the first match. + +## Signer Lists and API Version Shim + +When `signer_lists: true` is requested, the handler reads `keylet::signers(accountID)` and places the result in a single-element JSON array — pre-allocated as an array on the assumption that future protocol versions might allow multiple signer lists per account. + +Where the result is placed depends on `context.apiVersion`: + +- **API v1**: the `signer_lists` array is nested inside `account_data`, matching the original (mis)documented behaviour. +- **API v2+**: it moves to the top-level response object as documented on xrpl.org. + +API v2 also enforces strict boolean typing on the `signer_lists` parameter itself. In v1, any truthy string was silently accepted; from v2 onward, a non-boolean value returns `rpcINVALID_PARAMS`. This is a deliberate breaking change introduced with the versioned API. + +## Transaction Queue Data + +When `queue: true` is requested, the handler calls `TxQ::getAccountTxs(accountID)` and serializes each pending `TxDetails` entry. This path is gated with an explicit check that the target ledger is open (`ledger->open()`): the transaction queue is a property of the current open ledger only, and asking for queue state against a closed or validated ledger is rejected with `rpcINVALID_PARAMS`. This is not merely a semantic convenience — the queue is cleared at ledger close, so its contents are meaningless relative to any historical ledger. + +For each queued transaction the handler records its `seq` or `ticket` number, fee level, last-valid ledger sequence, absolute fee in drops, and `max_spend_drops` (fee plus potential spend). It also flags whether any queued transaction is an `auth_change` — a "blocker" in TxQ terminology — meaning it alters the account's signing authority, which can invalidate downstream transactions in the same queue. + +At the summary level, the response collects `sequence_count`, `ticket_count`, `lowest_sequence`, `highest_sequence`, `lowest_ticket`, `highest_ticket`, `auth_change_queued`, and `max_spend_drops_total`. These summary fields are emitted only when their associated counts are non-zero, keeping the response compact when the queue is simple. + +## `injectSLE` — Gravatar and Validity Marker + +`injectSLE` serializes the SLE via `getJson(JsonOptions::none)` and then conditionally appends a `urlgravatar` field constructed from the lowercase hex of `sfEmailHash`. The Gravatar URL uses HTTP rather than HTTPS — a known technical debt called out with a `VFALCO TODO` comment. If the SLE is not an `ltACCOUNT_ROOT`, it sets `"Invalid": true` as a defensive signal to callers; in practice `doAccountInfo` only passes account-root SLEs, so this branch is a safety net rather than an expected path. + +## Error Handling Summary + +| Condition | Error | +|---|---| +| `account` / `ident` wrong type | `rpcINVALID_FIELD` | +| Neither field present | `rpcMISSING_FIELD` | +| Unknown ledger | ledger-level error from `lookupLedger` | +| Malformed Base58 address | `rpcACT_MALFORMED` | +| `queue: true` on non-open ledger | `rpcINVALID_PARAMS` | +| `signer_lists` non-boolean (v2+) | `rpcINVALID_PARAMS` | +| Account not found in ledger | `rpcACT_NOT_FOUND` | \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountLines.cpp.ai.json b/src/xrpld/rpc/handlers/account/AccountLines.cpp.ai.json new file mode 100644 index 0000000000..1e4d73dd7e --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountLines.cpp.ai.json @@ -0,0 +1,251 @@ +{ + "args": [ + { + "lineno": 13, + "name": "jsonLines" + }, + { + "lineno": 13, + "name": "line" + }, + { + "lineno": 46, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doAccountLines", + "RPC::lookupLedger", + "parseBase58", + "ledger->exists", + "readLimitField", + "addLine" + ], + "entry_point": "doAccountLines", + "purpose": "Handles the 'account_lines' RPC request, validates input, fetches trust lines for an account, and formats the response.", + "validation_points": [ + "doAccountLines: params.isMember(jss::account)", + "doAccountLines: params[jss::account].isString()", + "RPC::lookupLedger: validates ledger parameters", + "parseBase58: validates account string format", + "ledger->exists: checks account existence in ledger", + "parseBase58 (peer): validates peer account if present", + "readLimitField: validates 'limit' parameter" + ] + } + ], + "data_flows": [ + { + "field": "account", + "flow": [ + "context.params[jss::account]", + "doAccountLines: isMember/isString validation", + "parseBase58", + "ledger->exists(keylet::account(accountID))", + "used as accountID for trust line lookup" + ], + "origin": "context.params[jss::account]", + "transformations": [ + "Checked for presence and type", + "Parsed from base58 string to AccountID", + "Checked for existence in ledger" + ], + "validated_at": "doAccountLines (isMember, isString, parseBase58, ledger->exists)" + }, + { + "field": "ledger_hash / ledger_index", + "flow": [ + "context.params", + "RPC::lookupLedger" + ], + "origin": "context.params", + "transformations": [ + "Validated and used to select ledger" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "peer", + "flow": [ + "context.params[jss::peer]", + "doAccountLines: isMember check", + "parseBase58 (if present)", + "used as filter for trust lines" + ], + "origin": "context.params[jss::peer]", + "transformations": [ + "Checked for presence", + "Parsed from base58 string to AccountID (if present)" + ], + "validated_at": "doAccountLines (parseBase58 for peer)" + }, + { + "field": "limit", + "flow": [ + "context.params[jss::limit]", + "readLimitField", + "used to limit number of trust lines returned" + ], + "origin": "context.params[jss::limit]", + "transformations": [ + "Validated for type and range" + ], + "validated_at": "readLimitField" + }, + { + "field": "ignore_default", + "flow": [ + "context.params[jss::ignore_default]", + "doAccountLines: isMember check and asBool()", + "used to filter out default trust lines" + ], + "origin": "context.params[jss::ignore_default]", + "transformations": [ + "Checked for presence", + "Converted to bool" + ], + "validated_at": "doAccountLines (isMember, asBool)" + } + ], + "description": "Implements the doAccountLines RPC handler for XRPL, which retrieves trust lines (credit lines) for a given account, supporting pagination, filtering, and various trust line flags.", + "false_positive_patterns": [ + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/AccountLines.cpp", + "functions": [ + { + "args": [ + "jsonLines", + "line" + ], + "lineno": 13, + "name": "addLine" + }, + { + "args": [ + "context" + ], + "lineno": 46, + "name": "doAccountLines" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in the RPC test suite, e.g., 'account_lines' tests in files like 'rpc-account-lines-test.cpp' or similar. These tests likely cover valid/invalid 'account', 'peer', 'limit', and 'ignore_default' parameters, as well as ledger selection. Gaps may exist for edge cases such as malformed base58 strings, non-existent accounts, or extreme 'limit' values. There may be limited coverage for error injection paths (e.g., malformed peer, missing ledger, etc.).", + "validation_architecture": {}, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountLines.cpp.ai.md b/src/xrpld/rpc/handlers/account/AccountLines.cpp.ai.md new file mode 100644 index 0000000000..2c0b7d25ff --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountLines.cpp.ai.md @@ -0,0 +1,49 @@ +# AccountLines.cpp + +This file implements the `account_lines` RPC command for the XRP Ledger, exposing an account's trust lines (IOU credit relationships) to external callers. It is one of the most frequently-called read-only RPC handlers because trust lines are the fundamental mechanism by which non-XRP assets circulate on the ledger. + +## Role in the System + +Trust lines live in the ledger as `ltRIPPLE_STATE` entries stored inside each account's owner directory. The `account_lines` command walks that directory, filters and shapes each entry, and returns a paginated JSON array. Two functions divide the work cleanly: `addLine()` serializes one trust line into JSON, and `doAccountLines()` handles the full request lifecycle. + +## `addLine()` — Trust Line Serialization + +`addLine()` appends one `RPCTrustLine` to the JSON array, producing the canonical shape of every element in the `lines` response array. The balance field follows XRPL's sign convention: a positive value means the requesting account holds the peer's IOUs (it is owed value), while a negative value means the peer holds the requesting account's IOUs (the account is the issuer side of that balance). + +Flag fields — `authorized`, `peer_authorized`, `no_ripple`, `no_ripple_peer`, `freeze`, `freeze_peer`, `deep_freeze`, `deep_freeze_peer` — are emitted only when `true`. This is a deliberate compactness choice: omitting false flags reduces response size substantially when most lines are in a neutral state. A caller that needs to distinguish "not set" from "set to false" can rely on the absence of the key as the false case. + +`RPCTrustLine` is the `TrustLineBase` subclass purpose-built for this handler. Unlike its sibling `PathFindTrustLine` (used by the path-finding engine where millions of instances are allocated and memory pressure is critical), `RPCTrustLine` carries four extra `Rate` fields for `quality_in` and `quality_out`. These quality values are only needed by the RPC layer, not by the payment engine, which motivates keeping them out of `PathFindTrustLine` entirely. + +The `TrustLineBase` type stores every trust line from the perspective of a nominated "view account" by recording a `mViewLowest` boolean at construction time. Internally, XRPL orders trust line entries by account ID (numerically lower account is the "low" side). `mViewLowest` normalizes all flag and limit accessors so that `getAuth()`, `getFreeze()`, `getLimit()`, etc., always return the value relative to the requesting account regardless of which side of the ledger entry it occupies. + +## `doAccountLines()` — Request Handler + +### Input Validation + +Validation is layered. The function first checks that `account` is present and is a string, then calls `RPC::lookupLedger()` to resolve the requested ledger version. `parseBase58()` decodes the account string and `ledger->exists(keylet::account(accountID))` confirms the account actually exists in that ledger. A missing account returns `rpcACT_NOT_FOUND` rather than an empty lines array, making the distinction explicit. + +The optional `peer` parameter follows the same base58 validation path. If the parameter is provided but fails to parse, `rpcACT_MALFORMED` is injected and the request is rejected. This prevents a caller from passing a malformed peer value and receiving silently-empty results. + +`readLimitField()` applies the tuning constants from `RPC::Tuning::accountLines` — a minimum of 10, a default of 200, and a maximum of 400. Values outside this range are clamped or rejected with an error. + +### `ignore_default` Filtering + +The `ignore_default` flag lets callers skip trust lines that are in the default state on the requesting account's side. A line is default on that side when neither `lsfLowReserve` nor `lsfHighReserve` is set (depending on whether the account is the low or high party). A default-state line does not consume an owner reserve slot, making it effectively invisible to most balance-sheet analyses. Callers indexing "active" trust lines can set `ignore_default: true` to avoid iterating through many zero-balance, unfunded lines. + +The flag check uses the raw `sfLowLimit` issuer field to determine which side the account is on rather than going through `RPCTrustLine`, because at filter time the handler wants to avoid constructing the full wrapper object for lines that will be discarded anyway. + +### Cursor-Based Pagination + +Pagination uses `forEachItemAfter()` from `DirectoryHelpers.h`, which traverses the account's owner directory starting just after a given 256-bit key. The handler requests `limit + 1` items from the iterator. If the callback fires exactly `limit + 1` times, there are more items; the marker is set at the limit-th item's key and emitted in the response. + +This off-by-one pattern is the canonical XRPL pagination idiom: request one extra item to detect whether a next page exists, but only collect up to `limit` results. The double-check `count == limit + 1 && marker` guards against an edge case where the limit-th item happens to be the last item in the directory — in that case `count` reaches `limit + 1` only if the callback was actually called that many times. + +The marker itself is a comma-separated string encoding a hex-encoded key and a 64-bit page hint (`","`). The hint is used by `forEachItemAfter()` to skip directly to the directory page containing the resume point rather than scanning from the beginning. The hint is derived from `getStartHint()`, which reads the page-index metadata embedded in the owner directory SLE. + +### Marker Security + +Before resuming a paginated request, the handler reads the SLE at the marker's key with `ledger->read({ltANY, startAfter})` and then calls `RPC::isRelatedToAccount()` to verify that object belongs to the account in the request. Without this check, a malicious caller could supply a valid marker pointing into another account's owner directory and receive entries that should not be accessible through this account's namespace. The check returns `rpcINVALID_PARAMS` on failure, which also covers the case where the marker points to a ledger object that no longer exists (e.g., because the trust line was deleted between pages). + +### Resource Metering + +The handler sets `context.loadType = Resource::feeMediumBurdenRPC` unconditionally before returning. This signals to the resource management layer that the request warrants moderate throttling. Walking an owner directory and constructing wrapper objects for hundreds of trust lines is meaningfully more expensive than a point lookup, but less than operations requiring cryptographic verification or path finding. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp.ai.json b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp.ai.json new file mode 100644 index 0000000000..0631298527 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp.ai.json @@ -0,0 +1,602 @@ +{ + "args": [ + { + "lineno": 22, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doAccountNFTs", + "parseBase58", + "RPC::lookupLedger", + "ledger->exists(keylet::account(accountID))", + "readLimitField" + ], + "entry_point": "doAccountNFTs", + "purpose": "Handles the 'account_nfts' RPC command, validating input, resolving the ledger, checking account existence, and paginating NFT results.", + "validation_points": [ + "params.isMember(jss::account) // required field check", + "params[jss::account].isString() // type check", + "parseBase58(params[jss::account].asString()) // format check", + "RPC::lookupLedger(ledger, context) // ledger existence/validity", + "ledger->exists(keylet::account(accountID)) // account existence", + "readLimitField(limit, RPC::Tuning::accountNFTokens, context) // limit field validation", + "params.isMember(jss::marker) // marker presence", + "params[jss::marker].isString() // marker type", + "marker.parseHex(m.asString()) // marker format" + ] + } + ], + "data_flows": [ + { + "field": "account", + "flow": [ + "context.params[jss::account]", + "params.isMember(jss::account) // presence check", + "params[jss::account].isString() // type check", + "parseBase58(params[jss::account].asString()) // decode", + "accountID = id.value()", + "ledger->exists(keylet::account(accountID)) // existence check", + "used for NFT page keylets" + ], + "origin": "context.params[jss::account] (JSON RPC input)", + "transformations": [ + "Checked for presence", + "Checked for string type", + "Parsed from base58 to AccountID" + ], + "validated_at": "params.isMember, params.isString, parseBase58, ledger->exists" + }, + { + "field": "ledger_hash / ledger_index", + "flow": [ + "context.params[ledger_hash or ledger_index]", + "RPC::lookupLedger(ledger, context)", + "ledger used for further queries" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Validated and resolved to a ledger pointer" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "limit", + "flow": [ + "context.params[jss::limit]", + "readLimitField(limit, RPC::Tuning::accountNFTokens, context)", + "limit used to control NFT pagination" + ], + "origin": "context.params[jss::limit] (JSON RPC input, optional)", + "transformations": [ + "Checked for integer type, range, and capped to max" + ], + "validated_at": "readLimitField" + }, + { + "field": "marker", + "flow": [ + "context.params[jss::marker]", + "params.isMember(jss::marker)", + "params[jss::marker].isString()", + "marker.parseHex(m.asString())", + "used to resume NFT pagination" + ], + "origin": "context.params[jss::marker] (JSON RPC input, optional)", + "transformations": [ + "Checked for presence", + "Checked for string type", + "Parsed from hex string to uint256" + ], + "validated_at": "params.isMember, params.isString, marker.parseHex" + } + ], + "description": "Implements the doAccountNFTs RPC command, which retrieves NFT objects owned by a specified account from the ledger, supporting pagination, limits, and marker-based continuation.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "account (presence, type, format)", + "validation", + "missing", + "check" + ], + "evidence": "Field account (presence, type, format) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for account (presence, type, format)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates account (presence, type, format) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "limit (type, range)", + "validation", + "missing", + "check" + ], + "evidence": "Field limit (type, range) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for limit (type, range)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates limit (type, range) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "marker (type, format, optional)", + "validation", + "missing", + "check" + ], + "evidence": "Field marker (type, format, optional) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for marker (type, format, optional)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates marker (type, format, optional) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash / ledger_index (existence, business logic)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash / ledger_index (existence, business logic) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for ledger_hash / ledger_index (existence, business logic)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates ledger_hash / ledger_index (existence, business logic) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::account) at doAccountNFTs", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "params.isMember(jss::account) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::account].isString() at doAccountNFTs", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "params[jss::account].isString() validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "account", + "type", + "validation", + "check" + ], + "evidence": "params[jss::account].isString() at doAccountNFTs", + "issue_pattern": "Missing type validation for account", + "why_false_positive": "params[jss::account].isString() validates account type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58(params[jss::account].asString()) at doAccountNFTs", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "parseBase58(params[jss::account].asString()) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "account", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58(params[jss::account].asString()) at doAccountNFTs", + "issue_pattern": "Missing format validation for account", + "why_false_positive": "parseBase58(params[jss::account].asString()) validates account format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash / ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger(ledger, context) at doAccountNFTs", + "issue_pattern": "Missing empty string validation for ledger_hash / ledger_index", + "why_false_positive": "RPC::lookupLedger(ledger, context) validates ledger_hash / ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "ledger->exists(keylet::account(accountID)) at doAccountNFTs", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "ledger->exists(keylet::account(accountID)) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "limit", + "empty", + "string", + "validation" + ], + "evidence": "readLimitField(limit, RPC::Tuning::accountNFTokens, context) at doAccountNFTs", + "issue_pattern": "Missing empty string validation for limit", + "why_false_positive": "readLimitField(limit, RPC::Tuning::accountNFTokens, context) validates limit for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "marker", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::marker) at doAccountNFTs", + "issue_pattern": "Missing empty string validation for marker", + "why_false_positive": "params.isMember(jss::marker) validates marker for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "marker", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::marker].isString() at doAccountNFTs", + "issue_pattern": "Missing empty string validation for marker", + "why_false_positive": "params[jss::marker].isString() validates marker for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "marker", + "type", + "validation", + "check" + ], + "evidence": "params[jss::marker].isString() at doAccountNFTs", + "issue_pattern": "Missing type validation for marker", + "why_false_positive": "params[jss::marker].isString() validates marker type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "marker", + "empty", + "string", + "validation" + ], + "evidence": "marker.parseHex(m.asString()) at doAccountNFTs", + "issue_pattern": "Missing empty string validation for marker", + "why_false_positive": "marker.parseHex(m.asString()) validates marker for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "marker", + "format", + "validation", + "invalid" + ], + "evidence": "marker.parseHex(m.asString()) at doAccountNFTs", + "issue_pattern": "Missing format validation for marker", + "why_false_positive": "marker.parseHex(m.asString()) validates marker format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/AccountNFTs.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 22, + "name": "doAccountNFTs" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ], + "test_coverage_notes": "The doAccountNFTs handler is typically tested via RPC integration tests, likely in files such as 'account_nfts_test.cpp', 'rpc_account_nfts_test.cpp', or similar. These tests should cover valid and invalid 'account' fields, missing fields, malformed account IDs, non-existent accounts, invalid markers, and limit boundaries. Gaps may exist in edge cases for marker handling, malformed marker values, and maximum limit enforcement. Unit tests for parseBase58, readLimitField, and lookupLedger are also relevant. If any of these are missing, especially for negative/invalid input cases, coverage is incomplete.", + "validation_architecture": { + "auto_validated_fields": [ + "account (presence, type, format)", + "limit (type, range)", + "marker (type, format, optional)", + "ledger_hash / ledger_index (existence, business logic)" + ], + "framework": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "validation_layer": "entry_point (doAccountNFTs handler)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::account)", + "field": "account", + "location": "doAccountNFTs", + "validated_by": "params.isMember(jss::account)", + "validates": [ + "Checks that the 'account' field is present in the input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::account)", + "field": "account", + "location": "doAccountNFTs", + "validated_by": "params[jss::account].isString()", + "validates": [ + "Checks that the 'account' field is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_MALFORMED)", + "field": "account", + "location": "doAccountNFTs", + "validated_by": "parseBase58(params[jss::account].asString())", + "validates": [ + "Checks that the 'account' string is a valid Base58-encoded AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "result (error JSON from lookupLedger)", + "field": "ledger_hash / ledger_index", + "location": "doAccountNFTs", + "validated_by": "RPC::lookupLedger(ledger, context)", + "validates": [ + "Checks that the specified ledger exists and is accessible" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_NOT_FOUND)", + "field": "account", + "location": "doAccountNFTs", + "validated_by": "ledger->exists(keylet::account(accountID))", + "validates": [ + "Checks that the account exists in the specified ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "*err (error from readLimitField)", + "field": "limit", + "location": "doAccountNFTs", + "validated_by": "readLimitField(limit, RPC::Tuning::accountNFTokens, context)", + "validates": [ + "Checks that the 'limit' field is an integer (if present)", + "Checks that the 'limit' is within allowed range (as per RPC::Tuning::accountNFTokens)" + ], + "validation_type": "range|type" + }, + { + "confidence": 1.0, + "error_thrown": "none (just sets markerSet)", + "field": "marker", + "location": "doAccountNFTs", + "validated_by": "params.isMember(jss::marker)", + "validates": [ + "Checks if the 'marker' field is present (optional)" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::expected_field_error(jss::marker, \"string\")", + "field": "marker", + "location": "doAccountNFTs", + "validated_by": "params[jss::marker].isString()", + "validates": [ + "Checks that the 'marker' field is a string if present" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::marker)", + "field": "marker", + "location": "doAccountNFTs", + "validated_by": "marker.parseHex(m.asString())", + "validates": [ + "Checks that the 'marker' string is a valid hex-encoded uint256" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountNFTs.cpp.ai.md b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp.ai.md new file mode 100644 index 0000000000..40dafd8759 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountNFTs.cpp.ai.md @@ -0,0 +1,52 @@ +# `AccountNFTs.cpp` — RPC Handler for `account_nfts` + +## Role and Purpose + +This file implements `doAccountNFTs`, the single entry point for the `account_nfts` JSON-RPC command. Its job is to enumerate all NFTs held in an account's NFToken page directory, delivering them in a consistent order with full support for cursor-based pagination. It sits alongside the other per-account RPC handlers (`AccountLines`, `AccountObjects`, `AccountOffers`, etc.) and follows the same request/response contract used across that family. + +## The NFToken Storage Model + +Understanding the code requires understanding how NFTs are stored on ledger. Rather than a flat list, an account's NFTs are distributed across a chain of `NFTokenPage` ledger objects. Each page is identified by a composite 256-bit key: the owner's 160-bit `AccountID` concatenated with a 96-bit suffix derived from the tokens it can hold. The pages form a singly-linked list via the `sfNextPageMin` field — each page holds a reference to the minimum key of the next page. + +Within each page, tokens are sorted by their low 96 bits (the `nft::pageMask`: `0x00...00ffffffffffffffffffffffff`), which covers the issuer, taxon, and serial number fields packed into a `uint256` token ID. The high 160 bits (flags and transfer fee) are _not_ part of the sort key. This is a crucial detail that directly shapes the pagination logic. + +The `NFTokenID` itself is a packed big-endian 256-bit field: +- **[255:240]** — 16-bit flags (`getFlags`) +- **[239:224]** — 16-bit transfer fee in tenths of a basis point (`getTransferFee`) +- **[223:64]** — 160-bit issuer `AccountID` (`getIssuer`) +- **[63:32]** — 32-bit ciphered taxon (`getTaxon`, XOR-deciphered on read) +- **[31:0]** — 32-bit mint serial number (`getSerial`) + +## Request Validation + +The handler opens with a standard validation sequence common to this handler family. `jss::account` is required, must be a string, and must decode successfully as a Base58 `AccountID`. The ledger is resolved via `RPC::lookupLedger`, which handles `ledger_hash`/`ledger_index` negotiation and returns an error result if the ledger cannot be found. Account existence is then confirmed with `ledger->exists(keylet::account(accountID))`. + +The optional `limit` field is validated by `readLimitField` against `RPC::Tuning::accountNFTokens`, which sets a minimum of 20, default of 100, and maximum of 400. The optional `marker` must be a hex-encoded `uint256` — it is parsed via `marker.parseHex()` and fails early if malformed. + +## Page Traversal and the Marker Protocol + +Rather than scanning from page zero every time, the handler uses `ledger->succ(first.key, last.key.next())` to skip directly to the first page at or after the marker position. `keylet::nftpage(keylet::nftpage_min(accountID), marker)` produces the synthetic lower-bound key for this search. The `succ` call returns the actual next existing page key within the range `[first.key, last.key)`, so the initial read already skips empty ledger space efficiently. + +The inner loop walks each page's `sfNFTokens` array and then follows `sfNextPageMin` to advance to the next page. The iteration terminates when `sfNextPageMin` is absent (end of the chain) or the count limit is reached. + +### The Two-Level Marker Comparison + +The trickiest logic in the file handles the position-finding within the first page. Because page sort order uses only the low 96 bits of the token ID, multiple tokens on the same page can share the same masked value (same issuer/taxon/serial, differing only in flags or fee). The handler addresses this with two flags and a two-level comparison: + +1. **`maskedNftokenID < maskedMarker`** — Skip tokens that sort before the marker using page-ordering semantics. +2. **`maskedNftokenID == maskedMarker && nftokenID < marker`** — Within the same page-sort position, skip tokens whose full ID still precedes the marker. +3. **`nftokenID == marker`** — The exact marker token itself is skipped (it was already returned in the previous page of results); `markerFound` is set to `true`. + +Once `pastMarker` is `true`, the loop emits tokens normally. If a marker was supplied but never found (`markerSet && !markerFound`) — checked both mid-loop after the first valid token and at loop exit — the handler returns `RPC::invalid_field_error(jss::marker)`. This guards against stale or corrupted markers that reference tokens no longer present in the ledger. + +## Response Enrichment + +The raw `STObject` for each NFT is serialized with `getJson(JsonOptions::none)`, but the token ID fields are decoded and injected as top-level response fields: `Flags`, `Issuer`, `NFTokenTaxon`, and `nft_serial`. This decomposition spares the caller from having to parse the packed 256-bit ID themselves. The `TransferFee` field is conditionally included — it is omitted entirely when zero, since a zero transfer fee is semantically equivalent to "not set." + +## Pagination Output + +When the count limit is hit mid-page, the handler writes `result[jss::limit]` and `result[jss::marker]` (set to the hex string of the last-emitted token's full `sfNFTokenID`) and returns immediately. On a complete scan with no truncation, `result[jss::account]` is set to the Base58 account string and `context.loadType` is set to `Resource::feeMediumBurdenRPC`, signaling to the fee framework that this is a moderately expensive query. The asymmetry — account is only echoed back in the non-truncated case — means clients should use the marker's presence, not the account field, to detect pagination. + +## Relationship to Sibling Handlers + +`AccountNFTs.cpp` is the NFT-specific counterpart to `AccountObjects.cpp`. `AccountObjects` enumerates all ledger object types in an owner directory; `AccountNFTs` dives specifically into the `NFTokenPage` chain with awareness of the NFT ID layout. The two share the same surrounding infrastructure (`lookupLedger`, `readLimitField`, `Tuning`, `JsonContext`) but differ in their iteration strategy: `AccountObjects` uses a general owner-directory walk, while this handler exploits the page-chain structure unique to NFTs for efficient prefix-skipping at the page level. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp.ai.json b/src/xrpld/rpc/handlers/account/AccountObjects.cpp.ai.json new file mode 100644 index 0000000000..9833316ffe --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp.ai.json @@ -0,0 +1,392 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doAccountObjects", + "getAccountObjects" + ], + "entry_point": "doAccountObjects", + "purpose": "Handles the RPC request for account_objects, parses/validates input, and gathers account objects from the ledger.", + "validation_points": [ + "doAccountObjects: parses and checks input fields (account, type, dirIndex, entryIndex, limit)", + "getAccountObjects: validates dirIndex, entryIndex, typeFilter" + ] + }, + { + "call_chain": [ + "getAccountObjects" + ], + "entry_point": "getAccountObjects", + "purpose": "Gathers all ledger objects for an account, applying filters and pagination.", + "validation_points": [ + "dirIndex: checked for existence in ledger", + "typeFilter: checked via typeMatchesFilter lambda", + "entryIndex: checked via bitmask (entryIndex & ~nft::pageMask)", + "limit: implicitly validated by type (uint32_t)" + ] + } + ], + "data_flows": [ + { + "field": "dirIndex", + "flow": [ + "doAccountObjects (parse from request)", + "getAccountObjects (argument)", + "manual check: if (!dirIndex.isZero() && !ledger.read({ltDIR_NODE, dirIndex}))", + "used to determine directory traversal" + ], + "origin": "Parsed from RPC request in doAccountObjects", + "transformations": [ + "Checked for zero", + "Checked for existence in ledger" + ], + "validated_at": "getAccountObjects (early in function)" + }, + { + "field": "typeFilter", + "flow": [ + "doAccountObjects (parse from request)", + "getAccountObjects (argument)", + "typeMatchesFilter lambda used to check if ledger entry matches filter" + ], + "origin": "Parsed from RPC request in doAccountObjects", + "transformations": [ + "Converted from string to vector", + "Checked via lambda" + ], + "validated_at": "getAccountObjects (typeMatchesFilter lambda)" + }, + { + "field": "entryIndex", + "flow": [ + "doAccountObjects (parse from request)", + "getAccountObjects (argument)", + "Checked: if (firstNFTPage.key != (entryIndex & ~nft::pageMask))", + "Used to determine NFT page traversal" + ], + "origin": "Parsed from RPC request in doAccountObjects", + "transformations": [ + "Bitmask applied: (entryIndex & ~nft::pageMask)" + ], + "validated_at": "getAccountObjects (NFT page traversal logic)" + }, + { + "field": "limit", + "flow": [ + "doAccountObjects (parse from request)", + "getAccountObjects (argument)", + "Used as uint32_t mlimit for pagination" + ], + "origin": "Parsed from RPC request in doAccountObjects", + "transformations": [ + "Casted/parsed to uint32_t", + "Decremented in loop" + ], + "validated_at": "Implicitly by type; not explicitly range-checked" + } + ], + "description": "Implements the logic for the XRPL 'account_objects' RPC command, which retrieves all ledger objects owned by a given account, with support for filtering by type, pagination, and handling special cases like NFT pages and deletion blockers.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "jss::account_objects (JSON structure, not input)", + "validation", + "missing", + "check" + ], + "evidence": "Field jss::account_objects (JSON structure, not input) validated by xrpld/rpc (jss:: for JSON fields, Ledger/Keylet for business logic)", + "issue_pattern": "Missing validation for jss::account_objects (JSON structure, not input)", + "why_false_positive": "xrpld/rpc (jss:: for JSON fields, Ledger/Keylet for business logic) validates jss::account_objects (JSON structure, not input) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "LedgerEntryType (type safety via enum)", + "validation", + "missing", + "check" + ], + "evidence": "Field LedgerEntryType (type safety via enum) validated by xrpld/rpc (jss:: for JSON fields, Ledger/Keylet for business logic)", + "issue_pattern": "Missing validation for LedgerEntryType (type safety via enum)", + "why_false_positive": "xrpld/rpc (jss:: for JSON fields, Ledger/Keylet for business logic) validates LedgerEntryType (type safety via enum) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "AccountID (type safety, not shown in this snippet)", + "validation", + "missing", + "check" + ], + "evidence": "Field AccountID (type safety, not shown in this snippet) validated by xrpld/rpc (jss:: for JSON fields, Ledger/Keylet for business logic)", + "issue_pattern": "Missing validation for AccountID (type safety, not shown in this snippet)", + "why_false_positive": "xrpld/rpc (jss:: for JSON fields, Ledger/Keylet for business logic) validates AccountID (type safety, not shown in this snippet) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "dirIndex", + "empty", + "string", + "validation" + ], + "evidence": "manual check with ledger.read({ltDIR_NODE, dirIndex}) at getAccountObjects", + "issue_pattern": "Missing empty string validation for dirIndex", + "why_false_positive": "manual check with ledger.read({ltDIR_NODE, dirIndex}) validates dirIndex for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "typeFilter", + "empty", + "string", + "validation" + ], + "evidence": "typeMatchesFilter lambda at getAccountObjects", + "issue_pattern": "Missing empty string validation for typeFilter", + "why_false_positive": "typeMatchesFilter lambda validates typeFilter for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "entryIndex", + "empty", + "string", + "validation" + ], + "evidence": "bitmask check: (entryIndex & ~nft::pageMask) at getAccountObjects", + "issue_pattern": "Missing empty string validation for entryIndex", + "why_false_positive": "bitmask check: (entryIndex & ~nft::pageMask) validates entryIndex for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "limit", + "empty", + "string", + "validation" + ], + "evidence": "implicit (used as uint32_t, not explicitly checked) at getAccountObjects", + "issue_pattern": "Missing empty string validation for limit", + "why_false_positive": "implicit (used as uint32_t, not explicitly checked) validates limit for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/AccountObjects.cpp", + "functions": [ + { + "args": [ + "ledger", + "account", + "typeFilter", + "dirIndex", + "entryIndex", + "limit", + "jvResult" + ], + "lineno": 15, + "name": "getAccountObjects" + }, + { + "args": [ + "context" + ], + "lineno": 133, + "name": "doAccountObjects" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this code would be in the RPC integration and unit test suites, such as 'account_objects' tests in 'rippled' or 'xrpld' test directories (e.g., test/rpc/AccountObjects_test.cpp, test/handlers/AccountObjects_test.cpp). These would cover valid/invalid dirIndex, typeFilter, entryIndex, and limit values, as well as pagination and marker logic. Gaps may exist in edge cases for malformed dirIndex/entryIndex, very large limits, or unusual typeFilter combinations. Explicit negative tests for bitmask logic or non-existent directory nodes may be missing.", + "validation_architecture": { + "auto_validated_fields": [ + "jss::account_objects (JSON structure, not input)", + "LedgerEntryType (type safety via enum)", + "AccountID (type safety, not shown in this snippet)" + ], + "framework": "xrpld/rpc (jss:: for JSON fields, Ledger/Keylet for business logic)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns false (caller must handle error)", + "field": "dirIndex", + "location": "getAccountObjects", + "validated_by": "manual check with ledger.read({ltDIR_NODE, dirIndex})", + "validates": [ + "dirIndex is not zero", + "dirIndex exists as a DIR_NODE in the ledger" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (controls logic flow, not error)", + "field": "typeFilter", + "location": "getAccountObjects", + "validated_by": "typeMatchesFilter lambda", + "validates": [ + "typeFilter contains valid LedgerEntryType values" + ], + "validation_type": "business_logic|type" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (controls logic flow, not error)", + "field": "entryIndex", + "location": "getAccountObjects", + "validated_by": "bitmask check: (entryIndex & ~nft::pageMask)", + "validates": [ + "entryIndex matches expected NFT page index format" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "N/A (no explicit error, but type enforces unsigned int)", + "field": "limit", + "location": "getAccountObjects", + "validated_by": "implicit (used as uint32_t, not explicitly checked)", + "validates": [ + "limit is a non-negative integer (uint32_t)" + ], + "validation_type": "type|range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountObjects.cpp.ai.md b/src/xrpld/rpc/handlers/account/AccountObjects.cpp.ai.md new file mode 100644 index 0000000000..d3bf3a61ae --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountObjects.cpp.ai.md @@ -0,0 +1,35 @@ +# `AccountObjects.cpp` — `account_objects` RPC Handler + +This file implements the `account_objects` RPC command, which returns all ledger objects owned by a specific XRPL account. It is one of the most structurally complex account-query handlers because the XRP Ledger stores two fundamentally different kinds of account-owned objects in two separate data structures: the **owner directory** (a linked list of `ltDIR_NODE` pages) and **NFT pages** (`ltNFTOKEN_PAGE`). Bridging those two structures behind a single paginated API is the central challenge the file solves. + +## Dual-Structure Traversal in `getAccountObjects` + +The workhorse function `getAccountObjects` must traverse two disjoint ledger structures in sequence. NFT pages are stored as a sorted range of `ltNFTOKEN_PAGE` entries whose keys are derived from the account ID (upper 160 bits) plus a 96-bit token sort value. They are _not_ listed in the owner directory. Every other owned object — offers, trust lines, escrows, payment channels, checks, and so on — lives in the owner directory. + +The function handles this by iterating NFT pages first, then pivoting to the owner directory. The boolean `iterateNFTPages` encodes the conditions under which NFT pages should be included: the type filter must not exclude `ltNFTOKEN_PAGE`, and — critically — `dirIndex` must be zero. A non-zero `dirIndex` in a resumed-pagination call means the client is resuming mid-owner-directory, which implies all NFT pages were already returned in a prior response. This avoids redundant re-emission of NFT data on resume. + +The NFT page iteration uses `ledger.succ()` to find the first extant page within `[nftpage_min(account), nftpage_max(account)]`, then follows the `sfNextPageMin` field on each page as a linked-list pointer to the next page. This chain traversal is inherently forward-only and safe: if the page doesn't exist, the loop terminates. + +## The Marker Protocol and Its Edge Cases + +Pagination is expressed as a marker string `","` where both components are hex-encoded 256-bit values. The choice to use a comma-separated pair — rather than a single opaque cursor — is deliberate: it lets the handler determine which phase (NFT or directory) a resumed request belongs to by inspecting `dirIndex`. A zero `dirIndex` combined with a non-zero `entryIndex` that matches `firstNFTPage.key == (entryIndex & ~nft::pageMask)` signals a resume mid-NFT-page-chain. + +The bitmask `~nft::pageMask` strips the lower 96 bits of `entryIndex` (the token-sort portion) leaving only the upper 160-bit account prefix. If that prefix equals `firstNFTPage.key`, the marker encodes an NFT page key for this account; otherwise `iterateNFTPages` is cleared and the marker is treated as a directory entry. + +A subtle edge case arises when NFT pages fill the response exactly to the `limit`. At that boundary the code emits a marker of the form `"0,"` — `dirIndex` is literally the string "0" — so the next call resumes from that NFT page position. A second boundary exists when the NFT phase ends and the directory phase immediately hits `mlimit` before consuming a single entry: the code at line 167–172 emits a directory-style marker using the first entry of the current directory node even though `i == 0`, preventing a lost-entry scenario. + +`mlimit` (a mutable copy of `limit`) tracks remaining capacity uniformly across both phases so that the two loops share a single budget without needing to communicate remaining capacity through a return value. + +## Request Handling in `doAccountObjects` + +`doAccountObjects` validates the incoming JSON request and dispatches to `getAccountObjects`. The two most architecturally interesting decisions here are the `deletion_blockers_only` mode and the type validation gate. + +**`deletion_blockers_only`** constructs a compile-time table of `{jss::name, LedgerEntryType}` pairs covering every ledger entry type that prevents account deletion: `ltCHECK`, `ltESCROW`, `ltNFTOKEN_PAGE`, `ltPAYCHAN`, `ltRIPPLE_STATE`, cross-chain objects (`ltXCHAIN_OWNED_CLAIM_ID`, `ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID`, `ltBRIDGE`), and newer token types (`ltMPTOKEN_ISSUANCE`, `ltMPTOKEN`, `ltPERMISSIONED_DOMAIN`, `ltVAULT`). When a `type` parameter is also present alongside `deletion_blockers_only`, the table is further filtered to the intersection. This mode lets callers quickly audit which objects block account removal without a full scan. + +**Type validation** via `isAccountObjectsValidType` rejects a small set of global ledger state types (`ltAMENDMENTS`, `ltDIR_NODE`, `ltFEE_SETTINGS`, `ltLEDGER_HASHES`, `ltNEGATIVE_UNL`) that are ledger-global rather than account-owned. Requesting these would either return nothing or produce confusing results, so the handler surfaces a clean `rpcINVALID_PARAMS` instead. + +## Resource and Error Handling + +The handler is tagged `feeMediumBurdenRPC`, reflecting that a full account-objects scan can require many ledger reads across chained directory nodes and NFT pages. The pagination limit is enforced via `RPC::Tuning::accountObjects` (min 10, default 200, max 400), parsed through `readLimitField` which handles clamping automatically. + +`getAccountObjects` returns `false` only in one specific case: when `entryIndex` is non-zero and cannot be located in the expected directory node. This maps directly to `rpcINVALID_PARAMS` for `jss::marker` at the call site, giving the client a precise signal that their marker is stale or corrupted. All other early exits — missing directory, no objects — return `true` with an empty `account_objects` array, distinguishing "nothing to return" from "your marker was invalid." \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountOffers.cpp.ai.json b/src/xrpld/rpc/handlers/account/AccountOffers.cpp.ai.json new file mode 100644 index 0000000000..d7efe54832 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountOffers.cpp.ai.json @@ -0,0 +1,683 @@ +{ + "args": [ + { + "lineno": 11, + "name": "offer" + }, + { + "lineno": 11, + "name": "offers" + }, + { + "lineno": 27, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doAccountOffers", + "RPC::lookupLedger", + "parseBase58", + "ledger->exists(keylet::account(accountID))", + "readLimitField", + "forEachItemAfter", + "appendOfferJson" + ], + "entry_point": "doAccountOffers", + "purpose": "Handles the account_offers RPC request: validates input, locates ledger, parses account, checks account existence, processes pagination, and serializes offers.", + "validation_points": [ + "doAccountOffers: params.isMember(jss::account)", + "doAccountOffers: params[jss::account].isString()", + "RPC::lookupLedger: ledger_hash / ledger_index validation", + "parseBase58: account string format validation", + "ledger->exists(keylet::account(accountID)): account existence validation", + "readLimitField: limit field validation", + "doAccountOffers: params.isMember(jss::marker) && params[jss::marker].isString()", + "doAccountOffers: marker parsing and ownership validation" + ] + } + ], + "data_flows": [ + { + "field": "account", + "flow": [ + "params[jss::account]", + "doAccountOffers: isMember/isString validation", + "parseBase58", + "accountID", + "ledger->exists(keylet::account(accountID))", + "used in forEachItemAfter" + ], + "origin": "params[jss::account] (JSON RPC input)", + "transformations": [ + "Checked for presence and type", + "Parsed from base58 string to AccountID", + "Checked for existence in ledger" + ], + "validated_at": "doAccountOffers (multiple points: presence, type, parse, existence)" + }, + { + "field": "ledger_hash / ledger_index", + "flow": [ + "params", + "RPC::lookupLedger", + "ledger" + ], + "origin": "params (JSON RPC input)", + "transformations": [ + "Validated and resolved to a ledger pointer" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "limit", + "flow": [ + "params[jss::limit]", + "readLimitField", + "limit variable", + "forEachItemAfter" + ], + "origin": "params[jss::limit] (JSON RPC input, optional)", + "transformations": [ + "Validated for type and range", + "Used to control pagination" + ], + "validated_at": "readLimitField" + }, + { + "field": "marker", + "flow": [ + "params[jss::marker]", + "doAccountOffers: isString validation", + "parsed into startAfter and startHint", + "ownership checked via ledger->read and RPC::isRelatedToAccount" + ], + "origin": "params[jss::marker] (JSON RPC input, optional)", + "transformations": [ + "Parsed from string to uint256 and uint64_t", + "Validated for format and ownership" + ], + "validated_at": "doAccountOffers (marker parsing and ownership check)" + }, + { + "field": "offers", + "flow": [ + "forEachItemAfter", + "offers vector", + "appendOfferJson", + "result[jss::offers] (JSON output)" + ], + "origin": "ledger (via forEachItemAfter)", + "transformations": [ + "Ledger objects transformed to JSON via appendOfferJson" + ], + "validated_at": "Offers themselves are not directly validated here; only the account and pagination context are validated" + } + ], + "description": "Implements the doAccountOffers RPC handler for XRPL, which retrieves and formats the list of offers (orders) for a given account, supporting pagination and limit parameters.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "account (presence, type, format, existence)", + "validation", + "missing", + "check" + ], + "evidence": "Field account (presence, type, format, existence) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for account (presence, type, format, existence)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates account (presence, type, format, existence) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash / ledger_index (via lookupLedger)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash / ledger_index (via lookupLedger) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for ledger_hash / ledger_index (via lookupLedger)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates ledger_hash / ledger_index (via lookupLedger) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "limit (type, range)", + "validation", + "missing", + "check" + ], + "evidence": "Field limit (type, range) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for limit (type, range)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates limit (type, range) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "marker (type, format)", + "validation", + "missing", + "check" + ], + "evidence": "Field marker (type, format) validated by xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "issue_pattern": "Missing validation for marker (type, format)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField) validates marker (type, format) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::account) at doAccountOffers", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "params.isMember(jss::account) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::account].isString() at doAccountOffers", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "params[jss::account].isString() validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "account", + "type", + "validation", + "check" + ], + "evidence": "params[jss::account].isString() at doAccountOffers", + "issue_pattern": "Missing type validation for account", + "why_false_positive": "params[jss::account].isString() validates account type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash / ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doAccountOffers", + "issue_pattern": "Missing empty string validation for ledger_hash / ledger_index", + "why_false_positive": "RPC::lookupLedger validates ledger_hash / ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at doAccountOffers", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "parseBase58 validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "account", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at doAccountOffers", + "issue_pattern": "Missing format validation for account", + "why_false_positive": "parseBase58 validates account format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "ledger->exists(keylet::account(accountID)) at doAccountOffers", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "ledger->exists(keylet::account(accountID)) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "limit", + "empty", + "string", + "validation" + ], + "evidence": "readLimitField at doAccountOffers", + "issue_pattern": "Missing empty string validation for limit", + "why_false_positive": "readLimitField validates limit for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "marker", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::marker) at doAccountOffers", + "issue_pattern": "Missing empty string validation for marker", + "why_false_positive": "params.isMember(jss::marker) validates marker for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "marker", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::marker].isString() at doAccountOffers", + "issue_pattern": "Missing empty string validation for marker", + "why_false_positive": "params[jss::marker].isString() validates marker for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "marker", + "type", + "validation", + "check" + ], + "evidence": "params[jss::marker].isString() at doAccountOffers", + "issue_pattern": "Missing type validation for marker", + "why_false_positive": "params[jss::marker].isString() validates marker type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "marker", + "empty", + "string", + "validation" + ], + "evidence": "std::getline(marker, value, ',') at doAccountOffers", + "issue_pattern": "Missing empty string validation for marker", + "why_false_positive": "std::getline(marker, value, ',') validates marker for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "marker", + "format", + "validation", + "invalid" + ], + "evidence": "std::getline(marker, value, ',') at doAccountOffers", + "issue_pattern": "Missing format validation for marker", + "why_false_positive": "std::getline(marker, value, ',') validates marker format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "marker (index part)", + "empty", + "string", + "validation" + ], + "evidence": "startAfter.parseHex(value) at doAccountOffers", + "issue_pattern": "Missing empty string validation for marker (index part)", + "why_false_positive": "startAfter.parseHex(value) validates marker (index part) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "marker (index part)", + "format", + "validation", + "invalid" + ], + "evidence": "startAfter.parseHex(value) at doAccountOffers", + "issue_pattern": "Missing format validation for marker (index part)", + "why_false_positive": "startAfter.parseHex(value) validates marker (index part) format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/AccountOffers.cpp", + "functions": [ + { + "args": [ + "offer", + "offers" + ], + "lineno": 11, + "name": "appendOfferJson" + }, + { + "args": [ + "context" + ], + "lineno": 27, + "name": "doAccountOffers" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in the RPC integration and unit test suites, such as 'account_offers' tests in 'rippled' or 'xrpld' test directories (e.g., test/rpc/account_offers_test.cpp, test/rpc/AccountOffers_test.cpp). These tests likely cover: valid/invalid account input, missing fields, malformed account, non-existent account, pagination (marker), and limit boundaries. Gaps may include: edge cases for marker parsing (malformed marker), ownership validation of marker, and rare ledger state conditions. Template-based validation and error injection paths may not be exhaustively tested unless specifically targeted in negative/edge-case tests.", + "validation_architecture": { + "auto_validated_fields": [ + "account (presence, type, format, existence)", + "ledger_hash / ledger_index (via lookupLedger)", + "limit (type, range)", + "marker (type, format)" + ], + "framework": "xrpld RPC framework (jss::, RPC::, parseBase58, readLimitField)", + "validation_layer": "entry_point (doAccountOffers)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::account)", + "field": "account", + "location": "doAccountOffers", + "validated_by": "params.isMember(jss::account)", + "validates": [ + "Checks if 'account' field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::account)", + "field": "account", + "location": "doAccountOffers", + "validated_by": "params[jss::account].isString()", + "validates": [ + "Checks if 'account' field is a string" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "result from lookupLedger (error JSON)", + "field": "ledger_hash / ledger_index", + "location": "doAccountOffers", + "validated_by": "RPC::lookupLedger", + "validates": [ + "Checks if ledger exists and is valid (hash/index)" + ], + "validation_type": "format|existence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcACT_MALFORMED, result)", + "field": "account", + "location": "doAccountOffers", + "validated_by": "parseBase58", + "validates": [ + "Checks if 'account' string is valid Base58 AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_NOT_FOUND)", + "field": "account", + "location": "doAccountOffers", + "validated_by": "ledger->exists(keylet::account(accountID))", + "validates": [ + "Checks if account exists in the ledger" + ], + "validation_type": "business_logic|existence" + }, + { + "confidence": 1.0, + "error_thrown": "return *err (error JSON from readLimitField)", + "field": "limit", + "location": "doAccountOffers", + "validated_by": "readLimitField", + "validates": [ + "Checks if 'limit' is present (optional)", + "Checks if 'limit' is integer", + "Checks if 'limit' is within allowed range (Tuning::accountOffers)" + ], + "validation_type": "type|range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (optional field)", + "field": "marker", + "location": "doAccountOffers", + "validated_by": "params.isMember(jss::marker)", + "validates": [ + "Checks if 'marker' field is present (optional)" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::expected_field_error(jss::marker, \"string\")", + "field": "marker", + "location": "doAccountOffers", + "validated_by": "params[jss::marker].isString()", + "validates": [ + "Checks if 'marker' is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::marker)", + "field": "marker", + "location": "doAccountOffers", + "validated_by": "std::getline(marker, value, ',')", + "validates": [ + "Checks if 'marker' can be split into two comma-separated values" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::marker)", + "field": "marker (index part)", + "location": "doAccountOffers", + "validated_by": "startAfter.parseHex(value)", + "validates": [ + "Checks if first part of 'marker' is valid hex" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountOffers.cpp.ai.md b/src/xrpld/rpc/handlers/account/AccountOffers.cpp.ai.md new file mode 100644 index 0000000000..a782ac4c06 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountOffers.cpp.ai.md @@ -0,0 +1,61 @@ +# `AccountOffers.cpp` — `account_offers` RPC Handler + +This file implements the `account_offers` JSON-RPC command, which returns the list of open DEX offers currently owned by a given account. It is one of several handlers in the `src/xrpld/rpc/handlers/account/` directory that share the same paginated-directory-walk pattern: `AccountChannels.cpp`, `AccountLines.cpp`, and `AccountObjects.cpp` all follow the same structure. The file contributes two symbols to the `xrpl` namespace: a serialization helper (`appendOfferJson`) and the handler entry point (`doAccountOffers`). + +## Offer Serialization: `appendOfferJson` + +`appendOfferJson` converts a single `ltOFFER` ledger entry (`SLE`) into a JSON object and appends it to the output array. The non-obvious part is how the exchange rate is reported: + +```cpp +STAmount const dirRate = amountFromQuality(getQuality(offer->getFieldH256(sfBookDirectory))); +obj[jss::quality] = dirRate.getText(); +``` + +XRPL offer objects carry a `sfBookDirectory` field that stores the 256-bit hash key of the order-book directory node they belong to. The last 64 bits of that hash encode the offer's exchange rate as a fixed-point quality value. `getQuality()` extracts those 64 bits, and `amountFromQuality()` reconstructs an `STAmount` whose decimal string representation is the rate (units of `TakerPays` per unit of `TakerGets`). This avoids dividing `TakerPays` by `TakerGets` at read time; the quality is already materialized in the directory hash, so reading it is free. An optional `sfExpiration` field is only included in the output when present, keeping the response lean for non-expiring offers. + +## Handler Flow: `doAccountOffers` + +The handler follows a rigid validation-first pipeline enforced by early returns before any ledger iteration begins. + +**Parameter validation** proceeds in this order: `account` field presence and string type; `ledger_hash`/`ledger_index` resolution via `RPC::lookupLedger`; Base58 parsing of the account address via `parseBase58` (returning `rpcACT_MALFORMED` on failure); account existence in the resolved ledger via `keylet::account`; and finally the `limit` field through `readLimitField`, which clamps the caller's value to the `[10, 400]` range defined in `Tuning::accountOffers` with a default of 200. + +**Marker validation** is more involved than a simple string parse. The marker token is a comma-separated pair: a hex-encoded 256-bit ledger object key (`startAfter`) and a 64-bit decimal directory hint (`startHint`). Parsing uses `std::getline` with a `','` delimiter followed by `uint256::parseHex` and `boost::lexical_cast`, each of which returns an error response on failure. Crucially, after parsing, the handler reads the ledger object at `startAfter` and calls `RPC::isRelatedToAccount` to verify that the object actually belongs to the requested account: + +```cpp +auto const sle = ledger->read({ltANY, startAfter}); +if (!sle) + return rpcError(rpcINVALID_PARAMS); +if (!RPC::isRelatedToAccount(*ledger, sle, accountID)) + return rpcError(rpcINVALID_PARAMS); +``` + +This ownership check prevents a caller from crafting a marker that skips into another account's directory region, which would be a ledger-state information leak. An invalid or out-of-context marker is treated as `rpcINVALID_PARAMS` rather than a not-found error, making the distinction explicit. + +## Pagination: `forEachItemAfter` and the Off-by-One Design + +Iteration uses `forEachItemAfter(*ledger, accountID, startAfter, startHint, limit + 1, callback)`, which traverses the account's owner directory starting after `startAfter`. The `startHint` is the directory page index—a value recovered via `RPC::getStartHint` when the previous page's marker was created—and allows `forEachItemAfter` to seek directly to the right page rather than scanning from the root. This is the primary performance optimization for accounts with large numbers of owned objects. + +The `limit + 1` request is intentional. The callback increments `count` on each invocation and applies two thresholds: + +- At `count == limit`: record the current SLE's key and hint as the potential next-page marker. +- At `count <= limit`: push the SLE into the offers vector (only if `ltOFFER`). + +The limit+1 item is therefore iterated but never stored in results—its sole purpose is to confirm that there *is* a next page. After iteration, the handler checks both `count == limit + 1` and `marker` before emitting the marker in the response: + +```cpp +if (count == limit + 1 && marker) +{ + result[jss::limit] = limit; + result[jss::marker] = to_string(*marker) + "," + std::to_string(nextHint); +} +``` + +The two-condition check is documented with an inline comment. `marker` is set when `count` first reaches `limit`, but if the directory contains exactly `limit` items and no more, the loop ends before reaching `limit + 1`, so `count` stays at `limit`. In that case, `marker` would be set but `count != limit + 1`, so no marker is returned — correct behavior. + +The callback also guards against a null SLE pointer with an `UNREACHABLE` macro wrapped in `LCOV_EXCL_START/STOP`. This is a documented defensive invariant: `forEachItemAfter` only calls the callback with keys proven to exist in the directory, so a null SLE would imply ledger corruption rather than a normal error path. The `return false` stops iteration, but the code is expected to be dead in any healthy deployment. + +The type filter `sle->getType() == ltOFFER` inside the callback is necessary because an account's owner directory can contain multiple object types (`ltRIPPLE_STATE`, `ltESCROW`, etc.). Only `ltOFFER` entries are collected; others are counted for pagination tracking but not serialized. + +## Resource Accounting + +At the end of the handler, `context.loadType = Resource::feeMediumBurdenRPC` marks the request as medium-cost for the server's rate-limiting subsystem. Scanning an account's directory—especially across multiple pages—requires proportional ledger I/O, and this classification ensures clients performing large paginated sweeps are throttled appropriately relative to simple point-lookup commands. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountTx.cpp.ai.json b/src/xrpld/rpc/handlers/account/AccountTx.cpp.ai.json new file mode 100644 index 0000000000..b8e0759626 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountTx.cpp.ai.json @@ -0,0 +1,490 @@ +{ + "args": [ + { + "lineno": 19, + "name": "context" + }, + { + "lineno": 19, + "name": "params" + }, + { + "lineno": 74, + "name": "ledgerSpecifier" + }, + { + "lineno": 130, + "name": "args" + }, + { + "lineno": 186, + "name": "res" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doAccountTx", + "parseLedgerArgs", + "getLedgerRange", + "populateJsonResponse" + ], + "entry_point": "doAccountTx", + "purpose": "Handles the account_tx RPC command, parses and validates ledger arguments, fetches transactions, and formats the response.", + "validation_points": [ + "parseLedgerArgs (validates ledger_index_min, ledger_index_max, ledger_hash, ledger_index)" + ] + }, + { + "call_chain": [ + "parseLedgerArgs" + ], + "entry_point": "parseLedgerArgs", + "purpose": "Parses and validates ledger-related arguments from the RPC request parameters.", + "validation_points": [ + "Explicit if statements for mutual exclusion and type/range checks on ledger_index_min, ledger_index_max, ledger_hash, ledger_index" + ] + } + ], + "data_flows": [ + { + "field": "ledger_index_min", + "flow": [ + "params[jss::ledger_index_min]", + "parseLedgerArgs", + "min (local variable)", + "LedgerRange{min, max}", + "LedgerSpecifier" + ], + "origin": "params[jss::ledger_index_min] (JSON RPC input)", + "transformations": [ + "Checked for presence (isMember)", + "Checked for non-negative (asInt() >= 0)", + "Converted to unsigned (asUInt())" + ], + "validated_at": "parseLedgerArgs" + }, + { + "field": "ledger_index_max", + "flow": [ + "params[jss::ledger_index_max]", + "parseLedgerArgs", + "max (local variable)", + "LedgerRange{min, max}", + "LedgerSpecifier" + ], + "origin": "params[jss::ledger_index_max] (JSON RPC input)", + "transformations": [ + "Checked for presence (isMember)", + "Checked for non-negative (asInt() >= 0)", + "Converted to unsigned (asUInt())" + ], + "validated_at": "parseLedgerArgs" + }, + { + "field": "ledger_hash", + "flow": [ + "params[jss::ledger_hash]", + "parseLedgerArgs", + "hashValue (local variable)", + "LedgerHash hash", + "hash.parseHex(hashValue.asString())", + "LedgerSpecifier" + ], + "origin": "params[jss::ledger_hash] (JSON RPC input)", + "transformations": [ + "Checked for presence (isMember)", + "Checked for string type (isString())", + "Parsed as hex (parseHex())" + ], + "validated_at": "parseLedgerArgs" + }, + { + "field": "ledger_index", + "flow": [ + "params[jss::ledger_index]", + "parseLedgerArgs", + "ledger (local variable)", + "LedgerSpecifier" + ], + "origin": "params[jss::ledger_index] (JSON RPC input)", + "transformations": [ + "Checked for presence (isMember)", + "If numeric: asUInt()", + "If string: compared to known values ('current', 'closed', 'validated')" + ], + "validated_at": "parseLedgerArgs" + } + ], + "description": "Implements the account_tx RPC handler for querying transactions related to a specific account, including ledger range parsing, result formatting, and pagination support.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + [ + "ledger_index_min", + "ledger_index_max", + "ledger_hash", + "ledger_index" + ], + "empty", + "string", + "validation" + ], + "evidence": "explicit logic (if statements) at parseLedgerArgs", + "issue_pattern": "Missing empty string validation for ['ledger_index_min', 'ledger_index_max', 'ledger_hash', 'ledger_index']", + "why_false_positive": "explicit logic (if statements) validates ['ledger_index_min', 'ledger_index_max', 'ledger_hash', 'ledger_index'] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger_index_min", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::ledger_index_min].asInt() >= 0 at parseLedgerArgs", + "issue_pattern": "Missing empty string validation for ledger_index_min", + "why_false_positive": "params[jss::ledger_index_min].asInt() >= 0 validates ledger_index_min for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger_index_min", + "range", + "bounds", + "validation" + ], + "evidence": "params[jss::ledger_index_min].asInt() >= 0 at parseLedgerArgs", + "issue_pattern": "Missing range validation for ledger_index_min", + "why_false_positive": "params[jss::ledger_index_min].asInt() >= 0 validates ledger_index_min range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger_index_max", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::ledger_index_max].asInt() >= 0 at parseLedgerArgs", + "issue_pattern": "Missing empty string validation for ledger_index_max", + "why_false_positive": "params[jss::ledger_index_max].asInt() >= 0 validates ledger_index_max for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger_index_max", + "range", + "bounds", + "validation" + ], + "evidence": "params[jss::ledger_index_max].asInt() >= 0 at parseLedgerArgs", + "issue_pattern": "Missing range validation for ledger_index_max", + "why_false_positive": "params[jss::ledger_index_max].asInt() >= 0 validates ledger_index_max range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger_hash", + "empty", + "string", + "validation" + ], + "evidence": "hashValue.isString() at parseLedgerArgs", + "issue_pattern": "Missing empty string validation for ledger_hash", + "why_false_positive": "hashValue.isString() validates ledger_hash for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger_hash", + "type", + "validation", + "check" + ], + "evidence": "hashValue.isString() at parseLedgerArgs", + "issue_pattern": "Missing type validation for ledger_hash", + "why_false_positive": "hashValue.isString() validates ledger_hash type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger_hash", + "empty", + "string", + "validation" + ], + "evidence": "hash.parseHex(hashValue.asString()) at parseLedgerArgs", + "issue_pattern": "Missing empty string validation for ledger_hash", + "why_false_positive": "hash.parseHex(hashValue.asString()) validates ledger_hash for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledger_hash", + "format", + "validation", + "invalid" + ], + "evidence": "hash.parseHex(hashValue.asString()) at parseLedgerArgs", + "issue_pattern": "Missing format validation for ledger_hash", + "why_false_positive": "hash.parseHex(hashValue.asString()) validates ledger_hash format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/AccountTx.cpp", + "functions": [ + { + "args": [ + "context", + "params" + ], + "lineno": 19, + "name": "parseLedgerArgs" + }, + { + "args": [ + "context", + "ledgerSpecifier" + ], + "lineno": 74, + "name": "getLedgerRange" + }, + { + "args": [ + "context", + "args" + ], + "lineno": 130, + "name": "doAccountTxHelp" + }, + { + "args": [ + "res", + "args", + "context" + ], + "lineno": 186, + "name": "populateJsonResponse" + }, + { + "args": [ + "context" + ], + "lineno": 282, + "name": "doAccountTx" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 16, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this code would be in the RPC handler test suites, such as 'account_tx' tests in files like 'account_tx_test.cpp', 'AccountTx_test.cpp', or generic RPC/ledger handler tests. These should cover valid and invalid combinations of ledger_index_min, ledger_index_max, ledger_hash, and ledger_index, including type errors, range errors, and mutual exclusion logic. Gaps may exist if tests do not cover all mutual exclusion cases (e.g., specifying both ledger_index_min and ledger_hash), or malformed types (e.g., non-string ledger_hash, negative indices). Tests should also cover edge cases like empty strings, boundary values (0, UINT32_MAX), and malformed hex strings for ledger_hash.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss:: for JSON field names, RPC::Status for error reporting)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::Status{rpcINVALID_PARAMS, \"invalidParams\"}", + "field": [ + "ledger_index_min", + "ledger_index_max", + "ledger_hash", + "ledger_index" + ], + "location": "parseLedgerArgs", + "validated_by": "explicit logic (if statements)", + "validates": [ + "If ledger_index_min or ledger_index_max is specified, ledger_hash or ledger_index must NOT be specified (mutual exclusivity)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "None (defaults to 0 if invalid)", + "field": "ledger_index_min", + "location": "parseLedgerArgs", + "validated_by": "params[jss::ledger_index_min].asInt() >= 0", + "validates": [ + "ledger_index_min must be >= 0; otherwise, defaults to 0" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "None (defaults to UINT32_MAX if invalid)", + "field": "ledger_index_max", + "location": "parseLedgerArgs", + "validated_by": "params[jss::ledger_index_max].asInt() >= 0", + "validates": [ + "ledger_index_max must be >= 0; otherwise, defaults to UINT32_MAX" + ], + "validation_type": "range" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::Status{rpcINVALID_PARAMS, \"ledgerHashNotString\"}", + "field": "ledger_hash", + "location": "parseLedgerArgs", + "validated_by": "hashValue.isString()", + "validates": [ + "ledger_hash must be a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::Status{rpcINVALID_PARAMS, \"ledgerHashMalformed\"}", + "field": "ledger_hash", + "location": "parseLedgerArgs", + "validated_by": "hash.parseHex(hashValue.asString())", + "validates": [ + "ledger_hash must be a valid hex string" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/AccountTx.cpp.ai.md b/src/xrpld/rpc/handlers/account/AccountTx.cpp.ai.md new file mode 100644 index 0000000000..bc4d4b69bd --- /dev/null +++ b/src/xrpld/rpc/handlers/account/AccountTx.cpp.ai.md @@ -0,0 +1,71 @@ +# `AccountTx.cpp` — `account_tx` RPC Handler + +## Role and Context + +This file implements `doAccountTx`, the entry point for the `account_tx` JSON-RPC command. Clients use this command to retrieve all transactions that affected a specific account, optionally filtered to a ledger range and with cursor-based pagination for large result sets. The handler lives in `src/xrpld/rpc/handlers/account/` alongside other per-account RPC handlers (`AccountInfo`, `AccountLines`, etc.) and follows the same structural pattern: parse input, validate, delegate to storage, serialize response. + +The implementation is split into five cohesive functions rather than one monolith. This makes each concern independently testable and keeps the serialization logic (`populateJsonResponse`) clearly separated from the data-access logic (`doAccountTxHelp`). + +## Data Model + +The handler operates entirely through types defined in `RelationalDatabase` (declared in `include/xrpl/rdb/RelationalDatabase.h`). Key types: + +- `LedgerSpecifier` is a `std::variant` that uniformly represents every way a caller can identify a ledger or range of ledgers. +- `AccountTxArgs` bundles the parsed request fields — account, optional ledger specifier, `binary` flag, `forward` flag, limit, and optional pagination marker. +- `AccountTxResult` bundles the query output — a `std::variant` holding either rich objects or raw bytes, plus the effective ledger range and the next pagination cursor. +- `AccountTxMarker` is a `{ledgerSeq, txnSeq}` pair that encodes the resumption point for paginated queries. Using ledger and transaction sequence numbers rather than raw SQL row offsets keeps the cursor stable across concurrent writes. + +## Input Parsing Pipeline + +### `parseLedgerArgs` + +This function translates raw JSON `params` into a `LedgerSpecifier` (or an error). The ledger selection logic has a strict priority order: `ledger_index_min`/`max` range → `ledger_hash` → `ledger_index` → no specifier. The function enforces mutual exclusivity at API version 2 and above: supplying both a min/max range and a single-ledger identifier (`ledger_hash` or `ledger_index`) is an `rpcINVALID_PARAMS` error. In v1, this combination was silently tolerated; the v2 strictness is intentional to make the API unambiguous. + +A subtle convention governs negative index values: `ledger_index_min < 0` silently defaults to `0` (earliest available), and `ledger_index_max < 0` silently defaults to `UINT32_MAX` (latest). This follows the long-established XRPL convention where `-1` in a numeric field means "unbounded" rather than causing an error. Both unsigned conversions are guarded by checking `asInt() >= 0` first. + +### `getLedgerRange` + +Given an optional `LedgerSpecifier`, this function resolves it to a concrete `LedgerRange` bounded by the node's currently validated ledger window (obtained via `ledgerMaster.getValidatedRange()`). If the node has no validated range at all, the response differs by API version: v1 returns the legacy `rpcLGR_IDXS_INVALID`, while v2+ returns `rpcNOT_SYNCED`, which more clearly tells the client the server isn't ready. + +For range specifiers, v2+ applies strict bounds checking — if the requested min/max extends beyond what the node has validated, `rpcLGR_IDX_MALFORMED` is returned. In v1, the request was silently clamped to the available range instead. The v2 change prevents clients from unknowingly querying incomplete data. + +For single-ledger specifiers (hash, sequence, shortcut), the function calls the shared `getLedger()` helper, then verifies the retrieved ledger falls within the validated window via `ledgerMaster.isValidated()`. If it is unvalidated or out of range, `rpcLGR_NOT_VALIDATED` is returned. + +## Core Query Dispatch: `doAccountTxHelp` + +This function sets the RPC resource load class to `feeMediumBurdenRPC` upfront, then resolves the ledger range and constructs `AccountTxPageOptions` (account, range, marker, limit, admin status). The actual database call is selected based on the `(binary, forward)` combination: + +| `binary` | `forward` | Database method | +|----------|-----------|-----------------| +| false | false | `newestAccountTxPage()` | +| false | true | `oldestAccountTxPage()` | +| true | false | `newestAccountTxPageB()` | +| true | true | `oldestAccountTxPageB()` | + +The `B`-suffixed variants return `MetaTxsList` — a vector of `(tx_blob, meta_blob, ledger_seq)` tuples — which avoids deserializing transaction objects when the client has requested binary output. This is a deliberate performance path: binary clients are often indexers that will parse the blobs themselves. + +## Response Formatting: `populateJsonResponse` + +The serialization step has the most API-version divergence. For JSON (non-binary) results: + +- **API v1**: each transaction entry uses `tx` as the key and includes date in the transaction JSON. +- **API v2+**: uses `tx_json`, sets `JsonOptions::disable_API_prior_V2`, and promotes several fields (`hash`, `ledger_index`, `ledger_hash`, `close_time_iso`) to the top level of the transaction entry rather than embedding them inside the transaction object. The ledger hash is looked up by sequence via `ledgerMaster.getHashBySeq()`, and close time via `getCloseTimeBySeq()`. + +For binary results the key for metadata changes from `meta` (v1) to `meta_blob` (v2+). + +Beyond the basic serialization, `populateJsonResponse` applies four enrichment passes: + +1. `RPC::insertDeliverMax()` adds the `DeliverMax` field to payment transactions, needed because the on-ledger `Amount` field can be confusing for partial payments. +2. `insertDeliveredAmount()` adds a `delivered_amount` field to the metadata of successful payment and check-cash transactions. If metadata has the original value it is used directly; if not, the transaction `Amount` field is used; if neither is available the field is set to `"unavailable"`. The last case exists for historical transactions predating the metadata field. +3. `RPC::insertNFTSyntheticInJson()` synthesizes NFT-related response fields derived from transaction metadata that were added after the original NFT amendments. +4. `RPC::insertMPTokenIssuanceID()` injects the computed `mpt_issuance_id` into the metadata of successful `MPTokenIssuanceCreate` transactions. + +The `UNREACHABLE` macro on the missing-metadata branch (where `txnMeta` is null alongside a valid `txn`) signals that this state should never occur in a well-formed database — it's a developer-facing invariant assertion rather than a handled error path. + +## Entry Point: `doAccountTx` + +The public entry point guards on `config().useTxTables()` first — if the node is not maintaining a transaction index, it immediately returns `rpcNOT_ENABLED` before doing any work. + +At API v2+, `binary` and `forward` are validated to be actual JSON booleans rather than strings or numbers. Prior to v2, the XRPL API accepted string truthy values in these fields due to loose JSON coercion in the original JSON library; the v2 cleanup enforces correctness. The marker is validated to have both a `ledger` and a `seq` field of integer type, with an explicit and descriptive error message if either is absent or non-numeric. + +The overall flow — `doAccountTx` → `parseLedgerArgs` → `doAccountTxHelp` (which calls `getLedgerRange` internally) → `populateJsonResponse` — keeps each function's responsibility narrow and enables the separation of database logic from JSON serialization. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp.ai.json b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp.ai.json new file mode 100644 index 0000000000..9c490c8dca --- /dev/null +++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp.ai.json @@ -0,0 +1,467 @@ +{ + "args": [ + { + "lineno": 27, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doGatewayBalances", + "RPC::lookupLedger", + "parseBase58", + "ledger->exists(keylet::account(accountID))", + "addHotWallet (lambda)", + "parseBase58 (for hotwallets)" + ], + "entry_point": "doGatewayBalances", + "purpose": "Handles the 'gateway_balances' RPC command, validating input, looking up the ledger and accounts, parsing hotwallets, and preparing the response.", + "validation_points": [ + "RPC::lookupLedger: Validates ledger existence and accessibility.", + "params.isMember(jss::account) || params.isMember(jss::ident): Validates presence of account identifier.", + "parseBase58: Validates account/ident is a valid base58 AccountID.", + "ledger->exists(keylet::account(accountID)): Validates that the account exists in the ledger.", + "params.isMember(jss::hotwallet): Checks if hotwallet field is present.", + "addHotWallet: Validates each hotwallet entry is a valid AccountID." + ] + } + ], + "data_flows": [ + { + "field": "ledger", + "flow": [ + "context.params", + "RPC::lookupLedger", + "ledger (local variable)", + "used for account existence and trustline queries" + ], + "origin": "RPC::lookupLedger (from context)", + "transformations": [ + "lookupLedger parses params for ledger specifier, fetches ledger" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "account / ident", + "flow": [ + "params.isMember(jss::account) || params.isMember(jss::ident)", + "strIdent = params[jss::account] or params[jss::ident]", + "parseBase58(strIdent)", + "accountID", + "ledger->exists(keylet::account(accountID))" + ], + "origin": "context.params[jss::account] or context.params[jss::ident]", + "transformations": [ + "Checked for presence", + "Parsed from string to AccountID", + "Checked for existence in ledger" + ], + "validated_at": "Presence: params.isMember; Format: parseBase58; Existence: ledger->exists" + }, + { + "field": "hotwallet", + "flow": [ + "params.isMember(jss::hotwallet)", + "hw = params[jss::hotwallet]", + "addHotWallet(hw[i]) or addHotWallet(hw)", + "parseBase58(j.asString())", + "hotWallets set" + ], + "origin": "context.params[jss::hotwallet]", + "transformations": [ + "Checked for presence", + "If array or string, each entry parsed as AccountID", + "Invalid entries cause error" + ], + "validated_at": "addHotWallet lambda (parseBase58)" + } + ], + "description": "Implements the doGatewayBalances RPC handler for the XRPL, which calculates and returns gateway obligations, balances, assets, frozen balances, and locked (escrowed) funds for a specified account and its hot wallets.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "account/ident (presence, format)", + "validation", + "missing", + "check" + ], + "evidence": "Field account/ident (presence, format) validated by xrpld RPC framework, jss:: field tags, parseBase58 template", + "issue_pattern": "Missing validation for account/ident (presence, format)", + "why_false_positive": "xrpld RPC framework, jss:: field tags, parseBase58 template validates account/ident (presence, format) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "hotwallet (type, format)", + "validation", + "missing", + "check" + ], + "evidence": "Field hotwallet (type, format) validated by xrpld RPC framework, jss:: field tags, parseBase58 template", + "issue_pattern": "Missing validation for hotwallet (type, format)", + "why_false_positive": "xrpld RPC framework, jss:: field tags, parseBase58 template validates hotwallet (type, format) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doGatewayBalances", + "issue_pattern": "Missing empty string validation for ledger", + "why_false_positive": "RPC::lookupLedger validates ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account / ident", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::account) || params.isMember(jss::ident) at doGatewayBalances", + "issue_pattern": "Missing empty string validation for account / ident", + "why_false_positive": "params.isMember(jss::account) || params.isMember(jss::ident) validates account / ident for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account / ident", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at doGatewayBalances", + "issue_pattern": "Missing empty string validation for account / ident", + "why_false_positive": "parseBase58 validates account / ident for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "account / ident", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at doGatewayBalances", + "issue_pattern": "Missing format validation for account / ident", + "why_false_positive": "parseBase58 validates account / ident format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "ledger->exists(keylet::account(accountID)) at doGatewayBalances", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "ledger->exists(keylet::account(accountID)) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "hotwallet", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::hotwallet) at doGatewayBalances", + "issue_pattern": "Missing empty string validation for hotwallet", + "why_false_positive": "params.isMember(jss::hotwallet) validates hotwallet for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "hotwallet", + "type", + "validation", + "check" + ], + "evidence": "params.isMember(jss::hotwallet) at doGatewayBalances", + "issue_pattern": "Missing type validation for hotwallet", + "why_false_positive": "params.isMember(jss::hotwallet) validates hotwallet type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hotwallet (element)", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at doGatewayBalances (addHotWallet lambda)", + "issue_pattern": "Missing empty string validation for hotwallet (element)", + "why_false_positive": "parseBase58 validates hotwallet (element) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "hotwallet (element)", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at doGatewayBalances (addHotWallet lambda)", + "issue_pattern": "Missing format validation for hotwallet (element)", + "why_false_positive": "parseBase58 validates hotwallet (element) format" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/GatewayBalances.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 27, + "name": "doGatewayBalances" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in the RPC integration or unit test suites, e.g., 'rpc_gateway_balances_test.cpp' or similar. Tests should cover: missing/invalid ledger, missing/invalid account/ident, non-existent account, invalid hotwallet (wrong type, invalid base58), and valid requests. Gaps may exist if edge cases (e.g., null hotwallet, malformed arrays, or API version differences in error codes) are not explicitly tested. No test files are referenced in the code, so coverage must be verified in the test suite.", + "validation_architecture": { + "auto_validated_fields": [ + "account/ident (presence, format)", + "hotwallet (type, format)" + ], + "framework": "xrpld RPC framework, jss:: field tags, parseBase58 template", + "validation_layer": "business_logic (handler entry point)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns error result (early return)", + "field": "ledger", + "location": "doGatewayBalances", + "validated_by": "RPC::lookupLedger", + "validates": [ + "ledger existence", + "ledger lookup by parameters" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::account)", + "field": "account / ident", + "location": "doGatewayBalances", + "validated_by": "params.isMember(jss::account) || params.isMember(jss::ident)", + "validates": [ + "at least one of 'account' or 'ident' must be present" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_MALFORMED)", + "field": "account / ident", + "location": "doGatewayBalances", + "validated_by": "parseBase58", + "validates": [ + "account/ident must be valid base58 AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcACT_NOT_FOUND, result)", + "field": "account", + "location": "doGatewayBalances", + "validated_by": "ledger->exists(keylet::account(accountID))", + "validates": [ + "account must exist in ledger (for apiVersion > 1)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "returns error result (early return with rpcError)", + "field": "hotwallet", + "location": "doGatewayBalances", + "validated_by": "params.isMember(jss::hotwallet)", + "validates": [ + "hotwallet must be string, array of strings, or null" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "returns false, triggers rpcError(rpcACT_MALFORMED)", + "field": "hotwallet (element)", + "location": "doGatewayBalances (addHotWallet lambda)", + "validated_by": "parseBase58", + "validates": [ + "each hotwallet entry must be valid base58 AccountID" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/GatewayBalances.cpp.ai.md b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp.ai.md new file mode 100644 index 0000000000..30a2ce0dae --- /dev/null +++ b/src/xrpld/rpc/handlers/account/GatewayBalances.cpp.ai.md @@ -0,0 +1,50 @@ +# `GatewayBalances.cpp` — `gateway_balances` RPC Handler + +## Role and Purpose + +This file implements `doGatewayBalances`, the sole handler for the `gateway_balances` RPC command. The command exists to give XRPL gateways (token issuers) a complete financial snapshot: how much they owe customers in each currency, how much their hot wallets currently hold, any unusual positive balances (assets the gateway itself holds), obligations they have frozen, and funds locked in escrow. It is fundamentally an account-centric audit tool for the common XRPL issuer architecture where a cold wallet issues tokens and distributes them through hot wallets. + +## Gateway Architecture Context + +XRPL gateways operate with a cold/hot wallet security model. The cold wallet (identified in the `account` field) is the actual issuer and signs infrequently; hot wallets hold issued balances for operational distribution and can be compromised without exposing the issuer's key. This handler is designed around that model. The caller enumerates their hot wallets via the `hotwallet` parameter, and the handler classifies every trust line on the cold wallet accordingly. + +## Input Validation + +Validation proceeds in layers. First, `RPC::lookupLedger` resolves the requested ledger and returns early if it cannot be found. The account identifier accepts either `account` or `ident` as the field name — `ident` is a legacy alias. `parseBase58` rejects malformed addresses before any ledger access. Account existence is only enforced for API version 2 and above; in v1, querying a nonexistent account returns an empty result rather than an error, preserving backward compatibility. + +The `hotwallet` field is polymorphic by design: it accepts a single string, an array of strings, or null (treated as an empty array). Each entry is parsed through the same `parseBase58` path. If any entry is invalid, the error code differs by API version — `rpcINVALID_HOTWALLET` in v1, `rpcINVALID_PARAMS` in v2+. This was a deliberate correction in error taxonomy: `rpcINVALID_HOTWALLET` was a domain-specific code that did not fit cleanly into the general parameter-error scheme used by v2. + +After validation the handler immediately sets `context.loadType = Resource::feeHeavyBurdenRPC`, declaring this call expensive. Traversing the owner directory is O(n) in the number of trust lines, and gateways can have thousands of them. + +## Trust Line Traversal and Classification + +`forEachItem(*ledger, accountID, ...)` walks the cold wallet's owner directory. Every `SLE` in the directory is examined: + +**Escrow handling**: If the entry is an `ltESCROW`, its `sfAmount` field is accumulated into the `locked` map, keyed by currency. MPT-denominated escrows are skipped explicitly (`escrow.holds()` guard) — the locked totals only reflect classic IOU/XRP escrows. This is a deliberate scope limitation since the `gateway_balances` model was designed before MPTs existed. + +**Trust line classification**: For non-escrow entries, `PathFindTrustLine::makeItem(accountID, sle)` attempts to interpret the SLE as a trust line from the cold wallet's perspective. The `PathFindTrustLine` class (from `TrustLine.h`) normalises the low/high account asymmetry of the `RippleState` ledger object into a consistent view where `getBalance()` and `getAccountIDPeer()` are always expressed relative to the cold wallet. + +With a normalised trust line available, the handler classifies it into one of four buckets based on the balance sign and peer identity: + +1. **Hot wallet peer** — if the peer is in `hotWallets`, the balance is negated and placed in `hotBalances[peer]`. Negation is necessary because a negative balance on the cold wallet side means tokens are held by the counterparty. +2. **Positive balance (asset)** — a positive balance means someone owes the gateway, which is unusual. These go into `assets[peer]`. +3. **Frozen obligation** — `rs->getFreeze()` checks whether the cold wallet has frozen this specific trust line. The (negated) balance goes into `frozenBalances[peer]`. +4. **Normal obligation** — the common case: a customer holds gateway-issued tokens. These are aggregated by currency into `sums`, computing the total outstanding liabilities. Each `sums[currency]` entry is a running total across all customer trust lines in that currency. + +## Overflow Handling in Accumulation + +Both `sums` (obligations) and `locked` (escrow totals) use arithmetic that can overflow `STAmount`'s mantissa range. The code wraps the `+=` / `-=` operations in `try-catch (std::runtime_error)` and, on overflow, clamps to `STAmount::cMaxValue`/`STAmount::cMaxOffset`. The comment in the code is honest about the implication: very large sums are approximations anyway. This is a graceful degradation rather than a hard error — a gateway with astronomically large obligations gets a capped but still-useful report rather than a failed RPC call. + +The first assignment into a zero-initialised bucket (`bal == beast::zero`) is done by direct assignment rather than addition. This is required because `STAmount` uses the issue/currency fields of the first assigned value to tag the bucket; addition against a default-constructed zero would not carry the correct currency code. + +## Output Structure + +The response contains up to five keys, each omitted if empty: + +- `obligations` — a flat object mapping currency code to aggregate total, covering all non-hot, non-frozen customer liabilities. +- `balances` — a per-hot-wallet object, each value being an array of `{currency, value}` pairs. +- `frozen_balances` — same structure as `balances` but for frozen obligations. +- `assets` — same structure for unusual positive positions. +- `locked` — a flat object like `obligations`, mapping currency to total escrowed amount. + +The `populateResult` lambda deduplicates the three per-account maps into a single output pattern, avoiding repetition in the serialisation code. Because only non-empty maps generate output keys, a gateway with no assets gets no `assets` key at all, keeping the response compact. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp.ai.json b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp.ai.json new file mode 100644 index 0000000000..885cf2e693 --- /dev/null +++ b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp.ai.json @@ -0,0 +1,558 @@ +{ + "args": [ + { + "lineno": 10, + "name": "context" + }, + { + "lineno": 11, + "name": "txArray" + }, + { + "lineno": 12, + "name": "accountID" + }, + { + "lineno": 13, + "name": "sequence" + }, + { + "lineno": 14, + "name": "ledger" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doNoRippleCheck", + "readLimitField", + "RPC::lookupLedger", + "parseBase58", + "ledger->read(keylet::account(accountID))", + "fillTransaction (conditionally)" + ], + "entry_point": "doNoRippleCheck", + "purpose": "Handles the 'noripple_check' RPC request, validates input, fetches ledger/account, checks trust lines, and optionally prepares recommended transactions.", + "validation_points": [ + "doNoRippleCheck: params.isMember(jss::account)", + "doNoRippleCheck: params.isMember(\"role\")", + "doNoRippleCheck: params[jss::account].isString()", + "doNoRippleCheck: role == \"gateway\" || role == \"user\"", + "doNoRippleCheck: readLimitField(limit, ...)", + "doNoRippleCheck: params[jss::transactions].isBool() (apiVersion > 1)", + "parseBase58: validates account string format", + "ledger->read(keylet::account(accountID)): validates account exists" + ] + } + ], + "data_flows": [ + { + "field": "account", + "flow": [ + "params[jss::account]", + "doNoRippleCheck: isMember, isString validation", + "parseBase58: string \u2192 AccountID", + "ledger->read(keylet::account(accountID)): fetch SLE", + "used for trust line checks, transaction construction" + ], + "origin": "params[jss::account] (JSON RPC input)", + "transformations": [ + "String validated as present and string type", + "Parsed to AccountID (base58 decode)", + "Used as key to fetch ledger entry" + ], + "validated_at": "doNoRippleCheck (isMember, isString), parseBase58 (format), ledger->read (existence)" + }, + { + "field": "role", + "flow": [ + "params[\"role\"]", + "doNoRippleCheck: isMember validation", + "doNoRippleCheck: asString()", + "doNoRippleCheck: compared to \"gateway\"/\"user\"" + ], + "origin": "params[\"role\"] (JSON RPC input)", + "transformations": [ + "String validated as present", + "String compared to allowed values" + ], + "validated_at": "doNoRippleCheck (isMember, value check)" + }, + { + "field": "limit", + "flow": [ + "params[\"limit\"]", + "doNoRippleCheck: readLimitField(limit, ...)", + "limit used to restrict number of problems reported" + ], + "origin": "params[\"limit\"] (JSON RPC input, optional)", + "transformations": [ + "Validated and clamped by readLimitField" + ], + "validated_at": "readLimitField" + }, + { + "field": "transactions", + "flow": [ + "params[\"transactions\"]", + "doNoRippleCheck: isMember, asBool()", + "doNoRippleCheck: isBool() check for apiVersion > 1", + "Controls whether recommended transactions are generated" + ], + "origin": "params[\"transactions\"] (JSON RPC input, optional)", + "transformations": [ + "Boolean validated (apiVersion > 1: must be bool)", + "Used as flag for transaction generation" + ], + "validated_at": "doNoRippleCheck (isBool() for apiVersion > 1)" + } + ], + "description": "Implements the doNoRippleCheck RPC handler for the XRPL, which checks an account's no ripple settings and recommends or generates transactions to fix configuration issues.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "account (presence, type)", + "validation", + "missing", + "check" + ], + "evidence": "Field account (presence, type) validated by xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger)", + "issue_pattern": "Missing validation for account (presence, type)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger) validates account (presence, type) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "role (presence, enum)", + "validation", + "missing", + "check" + ], + "evidence": "Field role (presence, enum) validated by xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger)", + "issue_pattern": "Missing validation for role (presence, enum)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger) validates role (presence, enum) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "limit (type, range via readLimitField)", + "validation", + "missing", + "check" + ], + "evidence": "Field limit (type, range via readLimitField) validated by xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger)", + "issue_pattern": "Missing validation for limit (type, range via readLimitField)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger) validates limit (type, range via readLimitField) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "transactions (type for API v2+)", + "validation", + "missing", + "check" + ], + "evidence": "Field transactions (type for API v2+) validated by xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger)", + "issue_pattern": "Missing validation for transactions (type for API v2+)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger) validates transactions (type for API v2+) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash|ledger_index (existence, format via lookupLedger)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash|ledger_index (existence, format via lookupLedger) validated by xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger)", + "issue_pattern": "Missing validation for ledger_hash|ledger_index (existence, format via lookupLedger)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger) validates ledger_hash|ledger_index (existence, format via lookupLedger) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::account) at doNoRippleCheck", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "params.isMember(jss::account) validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "role", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(\"role\") at doNoRippleCheck", + "issue_pattern": "Missing empty string validation for role", + "why_false_positive": "params.isMember(\"role\") validates role for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::account].isString() at doNoRippleCheck", + "issue_pattern": "Missing empty string validation for account", + "why_false_positive": "params[jss::account].isString() validates account for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "account", + "type", + "validation", + "check" + ], + "evidence": "params[jss::account].isString() at doNoRippleCheck", + "issue_pattern": "Missing type validation for account", + "why_false_positive": "params[jss::account].isString() validates account type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "role", + "empty", + "string", + "validation" + ], + "evidence": "role == \"gateway\" || role == \"user\" at doNoRippleCheck", + "issue_pattern": "Missing empty string validation for role", + "why_false_positive": "role == \"gateway\" || role == \"user\" validates role for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "limit", + "empty", + "string", + "validation" + ], + "evidence": "readLimitField(limit, RPC::Tuning::noRippleCheck, context) at doNoRippleCheck", + "issue_pattern": "Missing empty string validation for limit", + "why_false_positive": "readLimitField(limit, RPC::Tuning::noRippleCheck, context) validates limit for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transactions", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::transactions].isBool() (for apiVersion > 1u) at doNoRippleCheck", + "issue_pattern": "Missing empty string validation for transactions", + "why_false_positive": "params[jss::transactions].isBool() (for apiVersion > 1u) validates transactions for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "transactions", + "type", + "validation", + "check" + ], + "evidence": "params[jss::transactions].isBool() (for apiVersion > 1u) at doNoRippleCheck", + "issue_pattern": "Missing type validation for transactions", + "why_false_positive": "params[jss::transactions].isBool() (for apiVersion > 1u) validates transactions type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash|ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger(ledger, context) at doNoRippleCheck", + "issue_pattern": "Missing empty string validation for ledger_hash|ledger_index", + "why_false_positive": "RPC::lookupLedger(ledger, context) validates ledger_hash|ledger_index for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp", + "functions": [ + { + "args": [ + "context", + "txArray", + "accountID", + "sequence", + "ledger" + ], + "lineno": 9, + "name": "fillTransaction" + }, + { + "args": [ + "context" + ], + "lineno": 27, + "name": "doNoRippleCheck" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in files like 'noripple_check_test.cpp', 'Account_test.cpp', or generic RPC handler test suites. Tests should cover: missing/invalid 'account' and 'role', invalid account format, non-existent account, invalid 'limit', invalid 'transactions' type (especially for apiVersion > 1), and correct/incorrect role values. Gaps may exist if tests do not cover all combinations of missing/invalid fields, or do not test apiVersion-specific validation (e.g., 'transactions' type enforcement).", + "validation_architecture": { + "auto_validated_fields": [ + "account (presence, type)", + "role (presence, enum)", + "limit (type, range via readLimitField)", + "transactions (type for API v2+)", + "ledger_hash|ledger_index (existence, format via lookupLedger)" + ], + "framework": "xrpld RPC framework (jss::, RPC::, readLimitField, lookupLedger)", + "validation_layer": "entry_point (doNoRippleCheck is the RPC handler)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(\"account\")", + "field": "account", + "location": "doNoRippleCheck", + "validated_by": "params.isMember(jss::account)", + "validates": [ + "Checks if 'account' field is present in input params" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(\"role\")", + "field": "role", + "location": "doNoRippleCheck", + "validated_by": "params.isMember(\"role\")", + "validates": [ + "Checks if 'role' field is present in input params" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::account)", + "field": "account", + "location": "doNoRippleCheck", + "validated_by": "params[jss::account].isString()", + "validates": [ + "Checks if 'account' field is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(\"role\")", + "field": "role", + "location": "doNoRippleCheck", + "validated_by": "role == \"gateway\" || role == \"user\"", + "validates": [ + "Checks if 'role' is either 'gateway' or 'user'" + ], + "validation_type": "business_logic|enum" + }, + { + "confidence": 1.0, + "error_thrown": "*err (error returned by readLimitField)", + "field": "limit", + "location": "doNoRippleCheck", + "validated_by": "readLimitField(limit, RPC::Tuning::noRippleCheck, context)", + "validates": [ + "Checks if 'limit' is a valid integer", + "Checks if 'limit' is within allowed range (as per RPC::Tuning::noRippleCheck)" + ], + "validation_type": "range|type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::transactions)", + "field": "transactions", + "location": "doNoRippleCheck", + "validated_by": "params[jss::transactions].isBool() (for apiVersion > 1u)", + "validates": [ + "Checks if 'transactions' field is a boolean (for API version > 1)" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "result (error returned by lookupLedger)", + "field": "ledger_hash|ledger_index", + "location": "doNoRippleCheck", + "validated_by": "RPC::lookupLedger(ledger, context)", + "validates": [ + "Checks if ledger_hash or ledger_index (if provided) are valid and refer to an existing ledger" + ], + "validation_type": "format|existence|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp.ai.md b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp.ai.md new file mode 100644 index 0000000000..5c9f85ef6b --- /dev/null +++ b/src/xrpld/rpc/handlers/account/NoRippleCheck.cpp.ai.md @@ -0,0 +1,57 @@ +# `NoRippleCheck.cpp` — `noripple_check` RPC Handler + +## Role in the System + +This file implements the `doNoRippleCheck` RPC handler, which powers the `noripple_check` command on the XRP Ledger. The command exists to help both gateway operators and ordinary users detect misconfigured trust line No Ripple flags — a common source of unintended payment routing behavior. Rather than just reporting problems, the handler can optionally generate fully-formed, ready-to-submit transaction templates that correct each issue. + +The file lives alongside other account-scoped RPC handlers (`AccountLines.cpp`, `AccountObjects.cpp`, etc.) and follows the same structural contract: a single exported `do*` function receiving an `RPC::JsonContext` and returning a `Json::Value`. + +## The No Ripple Model + +The No Ripple flag controls whether the XRPL's path-finding engine may "ripple" through an account's trust lines when routing a payment. The correct configuration depends on the account's role: + +- **Gateways (issuers)** should enable the account-level `lsfDefaultRipple` flag, which makes newly created trust lines ripple by default. They must also ensure no individual trust line has `lsfLowNoRipple` or `lsfHighNoRipple` set on their side, as this would block payments from moving through their issued currency. +- **Users** should have `lsfDefaultRipple` cleared and should set `NoRipple` on each individual trust line. Without this, a user's balances can be silently routed through by third-party payments, which most users do not intend. + +The handler encodes these invariants directly: the `roleGateway` boolean controls which branch of logic applies throughout. + +## Input Validation + +Validation is layered. The handler first performs explicit field checks: presence of `account` and `role`, type assertion on `account`, and an allowlist check that `role` is either `"gateway"` or `"user"`. The `limit` parameter is delegated to `readLimitField` with the `RPC::Tuning::noRippleCheck` range (minimum 10, default 300, maximum 400). Ledger resolution is handled by `RPC::lookupLedger`, which abstracts away `ledger_hash` and `ledger_index` parsing. + +A notable versioning quirk: the `transactions` field historically accepted any truthy value due to `Json::Value::asBool()` being permissive. Starting with API version 2, the handler explicitly rejects non-boolean values with `isBool()`. This is a backward-compatibility boundary explicitly noted in an inline comment linking to the public documentation. + +## Trust Line Traversal + +The core analysis uses `forEachItemAfter` to walk the account's owner directory, which is the ledger structure holding all objects owned by an account. The callback filters for `ltRIPPLE_STATE` objects — the shared bilateral trust line ledger entries. + +Each `ltRIPPLE_STATE` stores both sides' limits as `sfLowLimit` and `sfHighLimit`, with "low" and "high" determined by the numeric order of the two account IDs. The handler identifies which side corresponds to the queried account via: + +```cpp +bool const bLow = accountID == ownedItem->getFieldAmount(sfLowLimit).getIssuer(); +``` + +It then reads the appropriate directional flag (`lsfLowNoRipple` if `bLow`, else `lsfHighNoRipple`). This is the key to correctly interpreting the shared trust line object from one account's perspective. + +The callback returns `true` on every item processed (triggering the count toward `limit`), meaning the `limit` parameter bounds the number of trust lines inspected rather than just the number of problems reported. + +## Transaction Template Generation + +The `fillTransaction()` helper populates the three fields common to every generated transaction: `Sequence` (taken from the account's current ledger sequence and post-incremented to handle multiple fixes in one response), `Account`, and `Fee`. The fee is not a hardcoded value — it is computed by `scaleFeeLoad` from the current base fee and the server's active fee track, giving the client a fee estimate calibrated to present network load. + +For the account-level `DefaultRipple` problem, the handler emits an `AccountSet` transaction with `SetFlag = 8` (the flag number for `asfDefaultRipple`). For each misconfigured trust line, it emits a `TrustSet` transaction. Building the `LimitAmount` for `TrustSet` requires care: the amount must express the queried account's own trust limit, but the `Issue` account field inside `STAmount` must be set to the peer account (the issuer from the queried account's perspective). The handler reconstructs this from the opposing side's `sfHighLimit`/`sfLowLimit`: + +```cpp +STAmount limitAmount(ownedItem->getFieldAmount(bLow ? sfLowLimit : sfHighLimit)); +limitAmount.get().account = peer; +``` + +The `Flags` field is set to `tfClearNoRipple` or `tfSetNoRipple` depending on whether the existing line has the flag set and the role requires it cleared or established. + +## The `dummy` Variable Pattern + +When `transactions` is `false`, the code still calls `jvTransactions.append(...)` inside the trust line loop. Rather than branching on `transactions` at every append site, the handler assigns `jvTransactions` to either a real array inside `result` or to a local `dummy` Json::Value. Appending to `dummy` is a no-op that is simply discarded. The `NOLINT(misc-const-correctness)` annotation on `dummy` is there because a linter would suggest marking it `const`, but it is mutated by the appends — the annotation documents this intentional design. + +## Failure Modes + +`doNoRippleCheck` returns early with distinct errors for each validation failure. After `lookupLedger`, the account's `SLE` (State Ledger Entry) is fetched and checked for existence; a missing account returns `rpcACT_NOT_FOUND`. A malformed base58 account string returns `rpcACT_MALFORMED` injected into the partial result (to preserve any ledger info already populated by `lookupLedger`). The `problems` array is always present in a successful response, even if empty. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/OwnerInfo.cpp.ai.json b/src/xrpld/rpc/handlers/account/OwnerInfo.cpp.ai.json new file mode 100644 index 0000000000..3883ac5f5b --- /dev/null +++ b/src/xrpld/rpc/handlers/account/OwnerInfo.cpp.ai.json @@ -0,0 +1,204 @@ +{ + "args": [ + { + "lineno": 12, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doOwnerInfo" + ], + "entry_point": "doOwnerInfo", + "purpose": "Handles the RPC request for owner info, validates input, parses account, and fetches owner info from ledgers.", + "validation_points": [ + "Manual check for presence of 'account' or 'ident' field (isMember)", + "parseBase58 for syntactic/semantic validation of account identifier" + ] + } + ], + "data_flows": [ + { + "field": "account / ident", + "flow": [ + "context.params", + "Manual check: isMember(jss::account) || isMember(jss::ident)", + "strIdent = params[account] or params[ident]", + "parseBase58(strIdent)", + "accountID (optional)", + "context.netOps.getOwnerInfo(ledger, accountID)" + ], + "origin": "context.params (JSON RPC request parameters)", + "transformations": [ + "String extraction from JSON", + "Base58 parsing and validation to AccountID" + ], + "validated_at": "Manual isMember check (required field), then parseBase58 (format/semantic validation)" + }, + { + "field": "ret[jss::accepted], ret[jss::current]", + "flow": [ + "accountID.has_value() ? getOwnerInfo(ledger, accountID) : rpcError(rpcACT_MALFORMED)", + "ret[jss::accepted] and ret[jss::current]" + ], + "origin": "Result of getOwnerInfo or rpcError", + "transformations": [ + "Conditional: If accountID is valid, fetch info; else, return error" + ], + "validated_at": "accountID.has_value() check (result of parseBase58 validation)" + } + ], + "description": "Implements the doOwnerInfo RPC handler, which retrieves owner information for a given account from both the closed and current ledgers.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account / ident", + "empty", + "string", + "validation" + ], + "evidence": "manual check (isMember) at doOwnerInfo", + "issue_pattern": "Missing empty string validation for account / ident", + "why_false_positive": "manual check (isMember) validates account / ident for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "account / ident", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at doOwnerInfo", + "issue_pattern": "Missing empty string validation for account / ident", + "why_false_positive": "parseBase58 validates account / ident for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "account / ident", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at doOwnerInfo", + "issue_pattern": "Missing format validation for account / ident", + "why_false_positive": "parseBase58 validates account / ident format" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/account/OwnerInfo.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 11, + "name": "doOwnerInfo" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage would be in RPC handler or integration test suites, e.g., tests exercising the /account_owner_info or similar endpoint. Tests should cover: missing 'account'/'ident' field (missing_field_error), malformed account (rpcACT_MALFORMED), valid account (returns owner info). Gaps may exist if there are no negative tests for malformed Base58 or missing fields, or if edge cases (e.g., both fields present, empty string) are not tested. Look for test files like 'rpc_account_owner_info_test.cpp', 'Account_test.cpp', or integration tests in the RPC suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC + jss:: (JSON field tags)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::account)", + "field": "account / ident", + "location": "doOwnerInfo", + "validated_by": "manual check (isMember)", + "validates": [ + "at least one of 'account' or 'ident' must be present in params" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_MALFORMED)", + "field": "account / ident", + "location": "doOwnerInfo", + "validated_by": "parseBase58", + "validates": [ + "string must be valid base58-encoded AccountID" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/account/OwnerInfo.cpp.ai.md b/src/xrpld/rpc/handlers/account/OwnerInfo.cpp.ai.md new file mode 100644 index 0000000000..8b3dcf461a --- /dev/null +++ b/src/xrpld/rpc/handlers/account/OwnerInfo.cpp.ai.md @@ -0,0 +1,27 @@ +# `OwnerInfo.cpp` — Legacy Owner Info RPC Handler + +This file implements `doOwnerInfo`, the handler behind the `owner_info` RPC command. It is one of the oldest account-query endpoints in the rippled codebase, predating the modern `account_objects` family. Today it survives primarily for backward compatibility, as evidenced by its placement in the "Utility and legacy" category in the handler registry. + +## What It Does + +`doOwnerInfo` fetches a compact snapshot of an account's owned ledger objects — specifically its open offers and trust lines — from two simultaneous views of the ledger: the most recently **closed** (validated) ledger and the **current** (open, in-progress) ledger. The response is a JSON object with two keys, `accepted` and `current`, each containing the output of `NetworkOPs::getOwnerInfo` against the respective ledger state. This dual-ledger view lets callers see both settled state and any pending changes that haven't yet been incorporated into a validated ledger. + +The handler is registered in `Handler.cpp` with the `NEEDS_CURRENT_LEDGER` condition, meaning the RPC framework will reject the call outright if no current ledger is available rather than letting the handler fail mid-execution. This is consistent with the other handlers (`fee`, `path_find`, `submit`) that depend on live ledger state. + +## Input Handling and the `ident` Alias + +The handler accepts the account identifier under either `account` or `ident`. The `ident` field is a legacy alias that predates the standardization of `account` as the canonical parameter name across the XRP Ledger API. Rather than removing `ident` and breaking old clients, the handler checks for both with a manual `isMember` guard. If neither is present, it returns `RPC::missing_field_error(jss::account)` — using the canonical name in the error even though `ident` is also valid. + +Once a string is extracted, it is decoded with `parseBase58`, which returns `std::optional`. This single call handles both syntactic validation (legal Base58Check characters) and semantic validation (correct checksum, correct payload length for an AccountID). If decoding fails, `accountID` is empty and both the `accepted` and `current` fields are set to `rpcError(rpcACT_MALFORMED)` rather than bailing out early. This means a malformed input produces a structurally complete response with error objects under both ledger keys instead of a top-level error — a quirk of the legacy design. + +## What `NetworkOPs::getOwnerInfo` Returns + +The heavy lifting is delegated to `NetworkOPsImp::getOwnerInfo` in `NetworkOPs.cpp`. That function walks the account's owner directory (`keylet::ownerDir`) page by page, following the `sfIndexNext` chain until the entire directory is exhausted. For each entry it reads the child SLE and dispatches on its type: `ltOFFER` entries are appended to a `jss::offers` array, and `ltRIPPLE_STATE` (trust line) entries go into a `jss::ripple_lines` array. `ltACCOUNT_ROOT` and `ltDIR_NODE` entries are treated as unreachable by design and guarded with `UNREACHABLE`. The result is a flat JSON object — no pagination, no cursor — which is why `owner_info` is impractical for accounts with large object counts and why `account_objects` (with its `marker`-based pagination) supersedes it for production use. + +## Design Observations + +The duplicate query pattern — calling `getOwnerInfo` once for the closed ledger and once for the current ledger — is straightforward but subtly expensive: it traverses the entire owner directory twice per call. Modern handlers avoid this by accepting an explicit `ledger_index` or `ledger_hash` parameter and performing a single lookup against the requested ledger view. + +The error propagation design is also notable: instead of an early return on a malformed account, both result fields are individually guarded by `accountID.has_value()`. This keeps the response shape consistent regardless of whether the account was valid, though it means the same `rpcACT_MALFORMED` error appears twice in the output for a bad input — one under `accepted` and one under `current`. Callers checking only one key might miss the error in the other. + +There are no exceptions thrown or caught here. The `std::optional` from `parseBase58` and the `Json::Value` from `getOwnerInfo` carry all success and failure information purely through return values, consistent with the error-handling conventions used across the RPC handler layer. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/BlackList.cpp.ai.json b/src/xrpld/rpc/handlers/admin/BlackList.cpp.ai.json new file mode 100644 index 0000000000..e0b81bbb89 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/BlackList.cpp.ai.json @@ -0,0 +1,189 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doBlackList" + ], + "entry_point": "doBlackList", + "purpose": "Handles the 'blacklist' RPC command, optionally filtering by a 'threshold' parameter.", + "validation_points": [ + "if (context.params.isMember(jss::threshold))" + ] + } + ], + "data_flows": [ + { + "field": "threshold", + "flow": [ + "context.params", + "context.params[jss::threshold]", + "context.params[jss::threshold].asInt()", + "rm.getJson(context.params[jss::threshold].asInt())" + ], + "origin": "context.params (JSON input from RPC request)", + "transformations": [ + "Checked for presence with isMember", + "Converted to int with asInt" + ], + "validated_at": "if (context.params.isMember(jss::threshold))" + } + ], + "description": "Implements the doBlackList RPC handler for the xrpl namespace, which interacts with the ResourceManager to return JSON data, optionally filtered by a threshold parameter.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "threshold", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::isMember at doBlackList", + "issue_pattern": "Missing empty string validation for threshold", + "why_false_positive": "Json::Value::isMember validates threshold for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "threshold", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::asInt at doBlackList", + "issue_pattern": "Missing empty string validation for threshold", + "why_false_positive": "Json::Value::asInt validates threshold for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "threshold", + "type", + "validation", + "check" + ], + "evidence": "Json::Value::asInt at doBlackList", + "issue_pattern": "Missing type validation for threshold", + "why_false_positive": "Json::Value::asInt validates threshold type" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/BlackList.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doBlackList" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence in this file of test coverage. Typical test files would be under workflow/XRPLF-rippled-develop/test or workflow/XRPLF-rippled-develop/unittest, possibly named after the handler (e.g., BlackList_test.cpp or rpc_blacklist_test.cpp). The validation is minimal: only presence and integer conversion are checked. There is no type or range validation, so malformed or out-of-range values may not be handled robustly. Tests should cover: missing threshold, threshold as non-integer, threshold as integer, and absence of threshold. Gaps: No explicit error handling for invalid types or values.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "JsonCpp (Json::Value), jss:: (field name constants)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (conditional branch)", + "field": "threshold", + "location": "doBlackList", + "validated_by": "Json::Value::isMember", + "validates": [ + "checks if 'threshold' field exists in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "Json::LogicError (if not convertible to int)", + "field": "threshold", + "location": "doBlackList", + "validated_by": "Json::Value::asInt", + "validates": [ + "checks if 'threshold' can be interpreted as integer" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/BlackList.cpp.ai.md b/src/xrpld/rpc/handlers/admin/BlackList.cpp.ai.md new file mode 100644 index 0000000000..326b36cdf8 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/BlackList.cpp.ai.md @@ -0,0 +1,35 @@ +# `BlackList.cpp` — Admin RPC Handler for Resource Consumption Inspection + +`BlackList.cpp` implements `doBlackList`, the handler behind the `"blacklist"` admin RPC command. Its purpose is to surface the node's internal resource-consumption accounting to operators in real time: specifically, it reports which connected endpoints (inbound or outbound peers and clients) have accumulated a load balance at or above a given threshold, making them candidates for rate-limiting or disconnection. + +## Role in the System + +The handler is registered in `src/xrpld/rpc/detail/Handler.cpp` with `Role::ADMIN`, which means it is only callable by a locally authenticated administrator — not by ordinary network clients. This access control is enforced by the RPC dispatch layer before `doBlackList` is ever invoked; the handler itself does not need to recheck the caller's role. + +The file is one of two handlers in the `admin/` subdirectory alongside `UnlList.cpp`. Both are thin shims: they pull an application subsystem handle from the `RPC::JsonContext` and delegate serialization entirely to that subsystem. `doBlackList` calls through to `Resource::Manager`, while `doUnlList` does the same for the validator list. + +## What the Handler Does + +```cpp +Json::Value +doBlackList(RPC::JsonContext& context) +{ + auto& rm = context.app.getResourceManager(); + if (context.params.isMember(jss::threshold)) + return rm.getJson(context.params[jss::threshold].asInt()); + + return rm.getJson(); +} +``` + +The function fetches a reference to the global `Resource::Manager` singleton through `Application::getResourceManager()`. If the caller supplies a `threshold` field in the request JSON, it is extracted as an integer and forwarded to `Manager::getJson(int threshold)`. Otherwise, the zero-argument `getJson()` is called, which internally calls `getJson(warningThreshold)` where `warningThreshold = 5000` (the balance at which the resource system begins issuing warnings). + +`Manager::getJson(int threshold)` iterates over all tracked inbound entries and emits a JSON object for each whose combined `local_balance + remote_balance` meets or exceeds the threshold. Each emitted entry reports the local balance, remote balance (learned via gossip from other nodes), and connection type. The threshold parameter is therefore a filter: at the default of 5000 you see every endpoint already in warning territory; a caller passing `0` would see every tracked endpoint regardless of load. + +## Design Notes + +The name "blacklist" reflects the operational interpretation: endpoints at or above `warningThreshold` are the ones the resource system considers misbehaving and may warn or drop. The handler exposes exactly that view. Adjusting the threshold downward lets operators inspect endpoints approaching but not yet at warning level; adjusting it upward narrows the view to endpoints near or past the disconnection threshold (25000). + +The handler performs no explicit type validation beyond `Json::Value::asInt()`. If `threshold` is present but not convertible to an integer, `asInt()` throws a `Json::LogicError`. There is no range check: a negative threshold is silently accepted and would cause `getJson` to return every endpoint (since all balances are non-negative). This is not exploitable — the command is admin-only — but it is a gap worth noting for defensive callers. + +The decision to delegate serialization entirely to `Resource::Manager::getJson` rather than building the JSON response in the handler itself keeps the RPC layer free of resource-accounting knowledge. The `Manager` interface defines two overloads of `getJson` precisely to support this pattern: callers that do not care about the threshold get the sensible default, and callers that want control pass their own value. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/UnlList.cpp.ai.json b/src/xrpld/rpc/handlers/admin/UnlList.cpp.ai.json new file mode 100644 index 0000000000..21556d7cf7 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/UnlList.cpp.ai.json @@ -0,0 +1,146 @@ +{ + "args": [ + { + "lineno": 10, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doUnlList", + "context.app.getValidators()", + "ValidatorList::for_each_listed", + "lambda (for each listed validator)" + ], + "entry_point": "doUnlList", + "purpose": "Handles the 'unl_list' admin RPC command, returning the list of validators (UNL) and their trusted status.", + "validation_points": [ + "Template-based validation likely occurs before doUnlList is called (not in this file).", + "No explicit validation in doUnlList itself; assumes context is valid and user is authorized." + ] + } + ], + "data_flows": [ + { + "field": "context", + "flow": [ + "RPC framework receives HTTP/JSON-RPC request", + "Framework validates and parses request, constructs JsonContext", + "JsonContext passed to doUnlList" + ], + "origin": "RPC::JsonContext passed to doUnlList by the RPC framework", + "transformations": [ + "Request is parsed and validated by framework before doUnlList is called" + ], + "validated_at": "Framework level, before doUnlList" + }, + { + "field": "publicKey", + "flow": [ + "ValidatorList stores validator public keys", + "for_each_listed iterates and passes each publicKey to lambda in doUnlList", + "Lambda encodes publicKey to base58 and adds to JSON output" + ], + "origin": "ValidatorList::for_each_listed iterates over listed validators", + "transformations": [ + "publicKey is converted to base58 string via toBase58" + ], + "validated_at": "Assumed valid as stored in ValidatorList; no explicit validation in this function" + }, + { + "field": "trusted", + "flow": [ + "ValidatorList stores trusted status for each validator", + "for_each_listed passes trusted to lambda", + "Lambda adds trusted boolean to JSON output" + ], + "origin": "ValidatorList::for_each_listed provides trusted status for each validator", + "transformations": [ + "No transformation; boolean copied directly" + ], + "validated_at": "Assumed valid as stored in ValidatorList; no explicit validation in this function" + } + ], + "description": "Implements the doUnlList RPC handler, which returns a list of validators (UNL) with their public keys and trusted status.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/UnlList.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 9, + "name": "doUnlList" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is likely tested indirectly via admin RPC integration tests (e.g., in test/rpc/ or test/handlers/ directories). There is no evidence of direct unit tests for doUnlList in this file. Validation of input is handled by the RPC framework and not tested here. Gaps: No explicit tests for malformed context or ValidatorList corruption; relies on framework and ValidatorList correctness.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss:: for JSON field names, not validation)", + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/UnlList.cpp.ai.md b/src/xrpld/rpc/handlers/admin/UnlList.cpp.ai.md new file mode 100644 index 0000000000..b468bed1d9 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/UnlList.cpp.ai.md @@ -0,0 +1,25 @@ +# `UnlList.cpp` — `unl_list` Admin RPC Handler + +## Role in the System + +This file implements `doUnlList`, the handler for the `unl_list` admin RPC command. Its single responsibility is to snapshot the node's current Unique Node List (UNL) — the set of validators the node has been configured to track — and report each one's public key along with a boolean indicating whether it is actively trusted for consensus purposes. + +The handler is registered in `Handler.cpp` as `{"unl_list", byRef(&doUnlList), Role::ADMIN, NO_CONDITION}`. The `Role::ADMIN` constraint means the endpoint is never exposed to ordinary WebSocket callers; only operator-level connections with admin credentials can reach it. The `NO_CONDITION` flag means no ledger state or network connection is required — the UNL is a node-local configuration concept independent of chain state. + +## Listed vs. Trusted + +The two-field response per validator (`pubkey_validator` and `trusted`) reflects a meaningful distinction in XRPL's validator model. A validator is *listed* when it appears in the node's configured publisher lists. It is *trusted* only if it also satisfies overlap and quorum thresholds computed by `ValidatorList`. A validator can be listed but not trusted — for instance, if the publisher list it came from does not provide sufficient overlap with other configured lists, or if the node has recently received an updated list that deprioritizes the key. Surfacing both dimensions lets operators diagnose configuration problems: a fully trusted list confirms healthy UNL composition, while listed-but-untrusted entries signal misconfiguration or list staleness. + +## Implementation + +`doUnlList` is a thin shim. It calls `context.app.getValidators().for_each_listed(...)`, passing a lambda that builds one JSON object per validator and appends it to the `unl` array of the response. Each entry encodes the raw `PublicKey` as a base58 string via `toBase58(TokenType::NodePublic, publicKey)` — the standard Node Public representation used throughout XRPL tooling. + +The handler performs no input validation and contains no error paths. There are no parameters to check; the RPC framework handles authentication and request parsing before dispatch, and the validator subsystem is always available in a running node. The result is always a well-formed JSON object. + +## Thread Safety via `for_each_listed` + +The thread-safety contract lives entirely in `ValidatorList::for_each_listed` (implemented in `detail/ValidatorList.cpp`). That method acquires a `std::shared_lock` on the list's internal `mutex_` before iterating over `keyListings_`, then calls the internal `trusted()` method under the same lock for each key. Because the handler's lambda only reads from the captured `ValidatorList` state through this controlled iterator, `doUnlList` itself needs no synchronization — `for_each_listed` guarantees a consistent point-in-time snapshot even if the validator list is being concurrently updated by a background publisher-list refresh. + +## Relationship to Other Handlers + +The sibling `admin/status/Validators.cpp` (`doValidators`) and `admin/status/ValidatorListSites.cpp` (`doValidatorListSites`) provide richer validator diagnostics — including publisher list metadata, expiry times, and site fetch state. `doUnlList` is the minimal form: just the set of keys and their trust status. The `BlackList.cpp` sibling follows the same structural pattern (thin shim delegating to an application subsystem), making both handlers easy to audit for security purposes. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp.ai.json b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp.ai.json new file mode 100644 index 0000000000..fc60b7bf08 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp.ai.json @@ -0,0 +1,523 @@ +{ + "args": [ + { + "lineno": 13, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doCanDelete" + ], + "entry_point": "doCanDelete", + "purpose": "Handles the 'can_delete' RPC command, validates and processes the can_delete parameter, and sets or gets the can_delete value in SHAMapStore.", + "validation_points": [ + "if (!context.app.getSHAMapStore().advisoryDelete())", + "if (context.params.isMember(jss::can_delete))", + "if (canDelete.isUInt())", + "else { ... boost::to_lower ... canDeleteStr.find_first_not_of(\"0123456789\") ... }", + "else if (canDeleteStr == \"never\" / \"always\" / \"now\")", + "else if (uint256 lh; lh.parseHex(canDeleteStr))", + "else { return RPC::make_error(rpcINVALID_PARAMS); }" + ] + } + ], + "data_flows": [ + { + "field": "can_delete (RPC param)", + "flow": [ + "context.params", + "context.params.get(jss::can_delete, 0)", + "canDelete (Json::Value)", + "if canDelete.isUInt() \u2192 canDelete.asUInt()", + "else \u2192 canDelete.asString() \u2192 boost::to_lower", + "if numeric string \u2192 beast::lexicalCast", + "if 'never'/'always'/'now' \u2192 mapped to uint32_t", + "if hex \u2192 parseHex \u2192 getLedgerByHash \u2192 ledger->header().seq", + "context.app.getSHAMapStore().setCanDelete(canDeleteSeq)" + ], + "origin": "context.params (JSON input from RPC request)", + "transformations": [ + "Type check (isUInt)", + "String conversion (asString, boost::to_lower)", + "Numeric check (find_first_not_of)", + "String to uint32_t (lexicalCast)", + "Special string mapping ('never', 'always', 'now')", + "Hex parsing (parseHex)", + "Ledger lookup (getLedgerByHash)", + "Ledger sequence extraction (ledger->header().seq)" + ], + "validated_at": [ + "isMember(jss::can_delete)", + "isUInt()", + "find_first_not_of(\"0123456789\")", + "parseHex", + "ledger existence check", + "else { return RPC::make_error(rpcINVALID_PARAMS); }" + ] + }, + { + "field": "canDeleteSeq", + "flow": [ + "canDelete (Json::Value)", + "canDeleteSeq (uint32_t, via various transformations)", + "context.app.getSHAMapStore().setCanDelete(canDeleteSeq)", + "ret[jss::can_delete] = ... (output JSON)" + ], + "origin": "Derived from canDelete param (see above)", + "transformations": [ + "Direct assignment (asUInt)", + "String to uint32_t (lexicalCast)", + "Special string mapping", + "Ledger sequence extraction" + ], + "validated_at": [ + "All validation on canDelete param applies here" + ] + } + ], + "description": "Implements the doCanDelete RPC handler for the XRPL server, allowing querying or setting the can_delete ledger sequence for SHAMapStore ledger deletion advisory.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "context.app.getSHAMapStore().advisoryDelete() at doCanDelete (first line)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "context.app.getSHAMapStore().advisoryDelete() validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::can_delete) at doCanDelete (if statement)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "context.params.isMember(jss::can_delete) validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "canDelete.isUInt() at doCanDelete (if branch)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "canDelete.isUInt() validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "can_delete", + "type", + "validation", + "check" + ], + "evidence": "canDelete.isUInt() at doCanDelete (if branch)", + "issue_pattern": "Missing type validation for can_delete", + "why_false_positive": "canDelete.isUInt() validates can_delete type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "canDelete.asString() + boost::to_lower at doCanDelete (else branch)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "canDelete.asString() + boost::to_lower validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "can_delete", + "format", + "validation", + "invalid" + ], + "evidence": "canDelete.asString() + boost::to_lower at doCanDelete (else branch)", + "issue_pattern": "Missing format validation for can_delete", + "why_false_positive": "canDelete.asString() + boost::to_lower validates can_delete format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "canDeleteStr.find_first_not_of(\"0123456789\") == std::string::npos at doCanDelete (string digit check)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "canDeleteStr.find_first_not_of(\"0123456789\") == std::string::npos validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "can_delete", + "format", + "validation", + "invalid" + ], + "evidence": "canDeleteStr.find_first_not_of(\"0123456789\") == std::string::npos at doCanDelete (string digit check)", + "issue_pattern": "Missing format validation for can_delete", + "why_false_positive": "canDeleteStr.find_first_not_of(\"0123456789\") == std::string::npos validates can_delete format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "beast::lexicalCast(canDeleteStr) at doCanDelete (digit string branch)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "beast::lexicalCast(canDeleteStr) validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "canDeleteStr == \"never\" at doCanDelete (string match)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "canDeleteStr == \"never\" validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "canDeleteStr == \"always\" at doCanDelete (string match)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "canDeleteStr == \"always\" validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "canDeleteStr == \"now\" at doCanDelete (string match)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "canDeleteStr == \"now\" validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "uint256 lh; lh.parseHex(canDeleteStr) at doCanDelete (hex parse branch)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "uint256 lh; lh.parseHex(canDeleteStr) validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "can_delete", + "format", + "validation", + "invalid" + ], + "evidence": "uint256 lh; lh.parseHex(canDeleteStr) at doCanDelete (hex parse branch)", + "issue_pattern": "Missing format validation for can_delete", + "why_false_positive": "uint256 lh; lh.parseHex(canDeleteStr) validates can_delete format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "can_delete", + "empty", + "string", + "validation" + ], + "evidence": "else (final else branch) at doCanDelete (final else)", + "issue_pattern": "Missing empty string validation for can_delete", + "why_false_positive": "else (final else branch) validates can_delete for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 12, + "name": "doCanDelete" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The function is likely tested by RPC-level integration tests, especially those covering the 'can_delete' admin command. Tests should cover: missing param, numeric, string ('never', 'always', 'now'), invalid string, hex ledger hash (valid/invalid), and SHAMapStore advisoryDelete enabled/disabled. Gaps may exist if there are no tests for edge cases (e.g., very large numbers, malformed hex, non-existent ledgers, or SHAMapStore not ready). Test files may include: rpc-admin.test.cpp, SHAMapStore_test.cpp, or integration tests for ledger deletion. No explicit unit tests for this function are shown in the provided context.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC + jss:: (JSON field tags) + beast::lexicalCast", + "validation_layer": "business_logic (inside handler function, not at entry/middleware)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcNOT_ENABLED (via RPC::make_error)", + "field": "can_delete", + "location": "doCanDelete (first line)", + "validated_by": "context.app.getSHAMapStore().advisoryDelete()", + "validates": [ + "advisory delete feature is enabled in SHAMapStore" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (optional field, no error)", + "field": "can_delete", + "location": "doCanDelete (if statement)", + "validated_by": "context.params.isMember(jss::can_delete)", + "validates": [ + "checks if can_delete parameter is present" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "none (handled by else branch)", + "field": "can_delete", + "location": "doCanDelete (if branch)", + "validated_by": "canDelete.isUInt()", + "validates": [ + "checks if can_delete is an unsigned integer" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "none (handled by further checks)", + "field": "can_delete", + "location": "doCanDelete (else branch)", + "validated_by": "canDelete.asString() + boost::to_lower", + "validates": [ + "converts can_delete to lowercase string for further validation" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "none (handled by further checks)", + "field": "can_delete", + "location": "doCanDelete (string digit check)", + "validated_by": "canDeleteStr.find_first_not_of(\"0123456789\") == std::string::npos", + "validates": [ + "checks if can_delete string contains only digits" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "exception if cast fails (not explicitly caught here)", + "field": "can_delete", + "location": "doCanDelete (digit string branch)", + "validated_by": "beast::lexicalCast(canDeleteStr)", + "validates": [ + "converts digit string to uint32_t" + ], + "validation_type": "type/format" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "can_delete", + "location": "doCanDelete (string match)", + "validated_by": "canDeleteStr == \"never\"", + "validates": [ + "checks if can_delete string is 'never'" + ], + "validation_type": "format/value" + }, + { + "confidence": 1.0, + "error_thrown": "none", + "field": "can_delete", + "location": "doCanDelete (string match)", + "validated_by": "canDeleteStr == \"always\"", + "validates": [ + "checks if can_delete string is 'always'" + ], + "validation_type": "format/value" + }, + { + "confidence": 1.0, + "error_thrown": "rpcNOT_READY (via RPC::make_error) if getLastRotated() == 0u", + "field": "can_delete", + "location": "doCanDelete (string match)", + "validated_by": "canDeleteStr == \"now\"", + "validates": [ + "checks if can_delete string is 'now' and if last rotated ledger is available" + ], + "validation_type": "format/value" + }, + { + "confidence": 1.0, + "error_thrown": "rpcLGR_NOT_FOUND (via RPC::make_error) if ledger not found", + "field": "can_delete", + "location": "doCanDelete (hex parse branch)", + "validated_by": "uint256 lh; lh.parseHex(canDeleteStr)", + "validates": [ + "checks if can_delete string is a valid hex and matches a ledger hash" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "rpcINVALID_PARAMS (via RPC::make_error)", + "field": "can_delete", + "location": "doCanDelete (final else)", + "validated_by": "else (final else branch)", + "validates": [ + "catches all invalid can_delete values not matching any accepted format" + ], + "validation_type": "format/value" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp.ai.md b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp.ai.md new file mode 100644 index 0000000000..c050fb98bb --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp.ai.md @@ -0,0 +1,43 @@ +# `CanDelete.cpp` — `can_delete` RPC Handler + +## Role in the System + +`CanDelete.cpp` implements `doCanDelete`, the admin RPC handler for the `can_delete` command. It exists to let node operators manually control XRPL's online-deletion feature when running in **advisory delete** mode. In that mode, the `SHAMapStore` background thread will only prune historical ledger data up to an explicitly approved sequence number — the node won't automatically discard ledgers based solely on its configured deletion depth. This file is the control surface that reads or writes that threshold. + +## Advisory Delete Guard + +The first thing `doCanDelete` does is check `context.app.getSHAMapStore().advisoryDelete()`. If the node was not configured with `advisory_delete=1` in its `[node_db]` config section, the handler immediately returns `rpcNOT_ENABLED`. This is a hard gate: the `can_delete` command is meaningless on a node running automatic deletion, and exposing it there would give operators a false sense of control. The flag is read from `SHAMapStoreImp::advisoryDelete_`, set at startup from the config file. + +## Read vs. Write Dispatch + +The function is both a getter and a setter, decided by the presence of the `can_delete` key in the request parameters. If the parameter is absent, the handler calls `getSHAMapStore().getCanDelete()` and returns the current threshold. If the parameter is present, it parses the value, calls `setCanDelete(canDeleteSeq)`, and returns the newly set value. Reflecting the set value back (rather than just returning success) allows callers to verify the actual resulting state with a single round-trip. + +## Multi-Format Input Parsing + +The parameter accepts several representations of a ledger sequence, processed in a deliberate priority order: + +1. **Native JSON unsigned integer** (`canDelete.isUInt()`): used directly as the sequence number. This fast path avoids string allocation entirely. + +2. **All-digit string**: detected with `find_first_not_of("0123456789")` before any other string comparison. The digits-only check comes first so that a string like `"12345"` isn't accidentally caught by the keyword matching that follows. Conversion uses `beast::lexicalCast`, which throws on overflow rather than silently truncating. + +3. **Keyword `"never"`**: maps to sequence `0`. Setting the threshold to zero effectively tells the store it may not delete any ledger — pausing pruning without disabling the feature. + +4. **Keyword `"always"`**: maps to `std::numeric_limits::max()`. This is a deliberate sentinel meaning "delete everything eligible," authorizing the store to prune as aggressively as its configuration allows. + +5. **Keyword `"now"`**: resolves to `getSHAMapStore().getLastRotated()`, which is the sequence of the most recently completed rotation point. If `getLastRotated()` returns zero — meaning no rotation has occurred yet since startup — the handler returns `rpcNOT_READY` rather than setting the threshold to an invalid value. + +6. **256-bit hex ledger hash**: `uint256::parseHex` is tried when none of the above match. If it parses, the handler does a full ledger lookup via `context.ledgerMaster.getLedgerByHash()`. A missing ledger returns `rpcLGR_NOT_FOUND`. A found ledger yields `ledger->header().seq`, converting the user-supplied hash into the sequence number that `SHAMapStore` actually operates on. + +7. **Everything else**: falls to `rpcINVALID_PARAMS`. + +All string comparisons are performed after `boost::to_lower`, making the keywords case-insensitive. This normalization happens before the keyword and hex branches, so `"NEVER"`, `"Always"`, and `"NOW"` are all valid inputs. + +## Design Observations + +The ordering of the string parsing branches is significant. Checking for all-digits before keywords prevents a numeric string from being mistaken for a keyword. Placing the hex check last (before the error fallback) is intentional: a 64-character hex string will never be all-digits, never match a keyword, and `parseHex` consumes the string cleanly or fails, making the final `else` a true catch-all. + +The `now` keyword is the most operationally useful form: an operator who wants to approve deletion up to the current state of the ledger store can simply issue `can_delete=now` without needing to query the rotation sequence first. The `rpcNOT_READY` guard protects against issuing that command before the store has completed its first rotation cycle. + +## Relationship to `SHAMapStore` + +`doCanDelete` is a thin orchestration layer over the `SHAMapStore` interface defined in `SHAMapStore.h`. The actual policy enforcement — whether the background deletion thread actually deletes ledgers based on `canDelete` — lives entirely in `SHAMapStoreImp`. This handler's only job is input normalization and delegation. It does not hold locks, spawn threads, or interact with the database directly. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp.ai.json b/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp.ai.json new file mode 100644 index 0000000000..b1ee9913b4 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp.ai.json @@ -0,0 +1,71 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doLedgerCleaner", + "Application::getLedgerCleaner", + "LedgerCleaner::clean" + ], + "entry_point": "doLedgerCleaner", + "purpose": "Handles the 'ledger_cleaner' RPC command, passing parameters to the LedgerCleaner subsystem to configure or trigger cleaning.", + "validation_points": [ + "Potentially in LedgerCleaner::clean (not shown in this file); no validation in doLedgerCleaner itself." + ] + } + ], + "data_flows": [ + { + "field": "context.params", + "flow": [ + "RPC JSON request", + "context.params", + "LedgerCleaner::clean" + ], + "origin": "RPC::JsonContext (populated from incoming RPC JSON request)", + "transformations": [ + "No transformation in doLedgerCleaner; params passed directly to clean()" + ], + "validated_at": "Not validated in doLedgerCleaner; presumed to be validated (if at all) inside LedgerCleaner::clean" + } + ], + "description": "Implements the doLedgerCleaner RPC handler, which configures and triggers the ledger cleaning process in the XRPL application.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doLedgerCleaner" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is a thin wrapper and does not perform validation itself. Test coverage would need to exist for both the RPC handler (ensuring correct routing and response) and for LedgerCleaner::clean (where validation should occur). Likely test files: rpc/handlers/LedgerCleaner_test.cpp, app/ledger/LedgerCleaner_test.cpp, or integration tests for the 'ledger_cleaner' RPC command. If LedgerCleaner::clean lacks input validation, this is a gap. No validation is present in this handler, so input validation coverage depends entirely on the implementation and tests of LedgerCleaner::clean.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected in this file", + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp.ai.md b/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp.ai.md new file mode 100644 index 0000000000..4f4f286337 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/data/LedgerCleaner.cpp.ai.md @@ -0,0 +1,34 @@ +# `rpc/handlers/admin/data/LedgerCleaner.cpp` + +This file is the RPC entry point for the `ledger_cleaner` admin command. It contains a single four-line function, `doLedgerCleaner`, that bridges the JSON-RPC layer to the background `LedgerCleaner` subsystem responsible for auditing and repairing ledger and transaction database continuity on a running node. + +## Role in the RPC Dispatch Layer + +The handler is registered in `src/xrpld/rpc/detail/Handler.cpp` as: + +```cpp +{"ledger_cleaner", byRef(&doLedgerCleaner), Role::ADMIN, NEEDS_NETWORK_CONNECTION} +``` + +The `Role::ADMIN` constraint means the command is only reachable from a privileged (local or explicitly-credentialed) connection — it cannot be triggered by an arbitrary network peer. `NEEDS_NETWORK_CONNECTION` gates it further, requiring the node to be in an active network state before the cleaner can be started. + +## What `doLedgerCleaner` Does + +```cpp +Json::Value +doLedgerCleaner(RPC::JsonContext& context) +{ + context.app.getLedgerCleaner().clean(context.params); + return RPC::makeObjectValue("Cleaner configured"); +} +``` + +The entire function body is two statements. It forwards the raw `context.params` JSON directly to `LedgerCleaner::clean()` without transformation or pre-validation, and immediately returns a static confirmation string. This is a deliberate fire-and-forget design: `clean()` is documented to schedule work asynchronously on an implementation-defined internal thread and return without blocking. + +## Intentional Delegation of Validation + +Because `LedgerCleaner` is an abstract class (`clean()` is pure virtual), all parameter interpretation — including which ledger sequence range to clean, whether to fix transaction entries, etc. — lives in the concrete implementation rather than here. The handler has no knowledge of what parameters are meaningful; it just provides a channel. This keeps the handler trivially auditable and concentrates the business logic in one place. + +## Contrast with Sibling Handlers + +The adjacent `CanDelete.cpp` handler takes the opposite approach: it does all validation inline (parsing ledger IDs, hash lookups, keyword values like `"now"` / `"always"` / `"never"`) and is synchronous. `LedgerRequest.cpp` is similarly synchronous and returns structured ledger data. `doLedgerCleaner` is unique in this group for being purely write-through and asynchronous — the caller gets no status about what the cleaner did, only a confirmation that it was configured. Any runtime progress would need to be observed through logs or the `PropertyStream` interface that `LedgerCleaner` inherits from `beast::PropertyStream::Source`. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp.ai.json b/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp.ai.json new file mode 100644 index 0000000000..97e4118525 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp.ai.json @@ -0,0 +1,232 @@ +{ + "args": [ + { + "lineno": 14, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doLedgerRequest", + "RPC::getOrAcquireLedger" + ], + "entry_point": "doLedgerRequest", + "purpose": "Handles an RPC request for ledger data, validates input, fetches the ledger, and serializes it to JSON.", + "validation_points": [ + "RPC::getOrAcquireLedger (validates ledger_hash and ledger_index)" + ] + } + ], + "data_flows": [ + { + "field": "ledger_hash", + "flow": [ + "context.params[\"ledger_hash\"]", + "RPC::getOrAcquireLedger(context)", + "ledger->header().seq (if found)", + "addJson (serialization)", + "jvResult (output JSON)" + ], + "origin": "context.params (JSON input from RPC request)", + "transformations": [ + "Parsed from JSON input", + "Validated for format and existence in getOrAcquireLedger", + "Used to fetch ledger object" + ], + "validated_at": "RPC::getOrAcquireLedger" + }, + { + "field": "ledger_index", + "flow": [ + "context.params[\"ledger_index\"]", + "RPC::getOrAcquireLedger(context)", + "ledger->header().seq (if found)", + "addJson (serialization)", + "jvResult (output JSON)" + ], + "origin": "context.params (JSON input from RPC request)", + "transformations": [ + "Parsed from JSON input", + "Validated for type/range/existence in getOrAcquireLedger", + "Used to fetch ledger object" + ], + "validated_at": "RPC::getOrAcquireLedger" + } + ], + "description": "Implements the doLedgerRequest RPC handler, which retrieves a specified ledger (by hash or index) and returns its details in JSON format.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash validated by xrpld RPC framework (jss::, RPC::, ErrorCodes)", + "issue_pattern": "Missing validation for ledger_hash", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, ErrorCodes) validates ledger_hash automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_index validated by xrpld RPC framework (jss::, RPC::, ErrorCodes)", + "issue_pattern": "Missing validation for ledger_index", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, ErrorCodes) validates ledger_index automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash", + "empty", + "string", + "validation" + ], + "evidence": "RPC::getOrAcquireLedger at doLedgerRequest", + "issue_pattern": "Missing empty string validation for ledger_hash", + "why_false_positive": "RPC::getOrAcquireLedger validates ledger_hash for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "RPC::getOrAcquireLedger at doLedgerRequest", + "issue_pattern": "Missing empty string validation for ledger_index", + "why_false_positive": "RPC::getOrAcquireLedger validates ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 13, + "name": "doLedgerRequest" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The doLedgerRequest function is typically tested via RPC integration tests that exercise ledger request endpoints, such as 'ledger_request' or similar. These tests are likely found in files like 'rpc/ledger_request_test.cpp', 'rpc/LedgerHandler_test.cpp', or broader RPC test suites. Validation of ledger_hash and ledger_index is covered if tests provide valid and invalid values for these fields. Gaps may exist if edge cases (e.g., malformed hashes, out-of-range indices, missing fields) are not explicitly tested. There is no evidence of direct unit tests for doLedgerRequest in this file; coverage relies on higher-level RPC tests.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger_hash", + "ledger_index" + ], + "framework": "xrpld RPC framework (jss::, RPC::, ErrorCodes)", + "validation_layer": "business_logic (in RPC::getOrAcquireLedger, called from handler entry point)" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "res.error() (returns error JSON, likely with error code from xrpl/protocol/ErrorCodes.h)", + "field": "ledger_hash", + "location": "doLedgerRequest", + "validated_by": "RPC::getOrAcquireLedger", + "validates": [ + "Checks if ledger_hash is present if provided", + "Checks if ledger_hash is a valid hash format", + "Checks if ledger_hash refers to an existing ledger" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "res.error() (returns error JSON, likely with error code from xrpl/protocol/ErrorCodes.h)", + "field": "ledger_index", + "location": "doLedgerRequest", + "validated_by": "RPC::getOrAcquireLedger", + "validates": [ + "Checks if ledger_index is present if provided", + "Checks if ledger_index is a valid integer or string representing a ledger index", + "Checks if ledger_index refers to an existing ledger" + ], + "validation_type": "type|range|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp.ai.md b/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp.ai.md new file mode 100644 index 0000000000..83f4bfb5cb --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/data/LedgerRequest.cpp.ai.md @@ -0,0 +1,44 @@ +# `LedgerRequest.cpp` — Admin RPC Handler for Historical Ledger Acquisition + +## Role and Context + +`LedgerRequest.cpp` implements `doLedgerRequest`, the server-side handler for the `ledger_request` admin RPC command. Its job is narrowly scoped: locate a specific historical ledger by hash or sequence number — acquiring it from the network if it isn't already cached locally — then return its full JSON representation. Because it can initiate peer-to-peer ledger downloads, it lives under `handlers/admin/data/` and is only accessible to admin-credentialed callers. + +This handler is deliberately distinct from the public `Ledger` handler in `handlers/ledger/Ledger.cpp`. The public ledger RPC queries the local validated and recent ledger history. `ledger_request` is the operator's tool for explicitly pulling historical ledgers into a node's local store, making it heavier and more appropriate as an admin operation. + +## Resource Cost Declaration + +The first thing the handler does is declare its expense: + +```cpp +context.loadType = Resource::feeHeavyBurdenRPC; +``` + +`feeHeavyBurdenRPC` carries a cost weight of 3000 — the highest tier in the resource fee schedule, shared with operations like path-finding and multi-signed submit. This is correct: a call may trigger `InboundLedgers::acquire()`, which can initiate a full network ledger fetch. Setting `loadType` early allows the resource manager to rate-limit or deprioritize the caller even before the work begins. + +## The `getOrAcquireLedger` Contract + +All the substantive logic is delegated to `RPC::getOrAcquireLedger(context)`, defined in `RPCLedgerHelpers.cpp`. Its signature returns `Expected, Json::Value>` — the monadic result type that either holds the ledger or an error JSON object, without exceptions or output-parameter mutation. + +The function enforces a strict input contract: **exactly one** of `ledger_hash` or `ledger_index` must be present. Providing both, or neither, produces a `rpcBAD_PARAM` error. This is tighter than most XRPL RPC handlers, which treat both as optional and fall back to the current ledger — appropriate here because the caller is explicitly requesting a specific historical ledger. + +**Hash path:** The hash string is parsed from hex into a `uint256`. If the string is malformed, an `expected_field_error` is returned immediately. The hash is then passed directly to `InboundLedgers::acquire()` which both checks local caches and, if needed, requests the ledger from peers. + +**Index path:** The sequence number path is more complex. First, the handler checks that the node's validated ledger isn't stale (beyond `RPC::Tuning::maxValidatedLedgerAge`), since resolving a sequence to a hash requires a live tip. It then walks the skip-list embedded in recent ledgers (`hashOfSeq`) to translate the requested index into a hash. If the required reference ledger isn't locally available, `getOrAcquireLedger` tries to fetch it from the inbound ledger subsystem and returns an intermediate `rpcLGR_NOT_FOUND` response containing an `acquiring` field — signaling the client that the server is in the process of obtaining the ledger and the request should be retried. Standalone mode (used in development and testing) skips network acquisition and falls back to `LedgerMaster::getLedgerByHash`. + +## Serialization + +On success, the handler constructs the response: + +```cpp +jvResult[jss::ledger_index] = ledger->header().seq; +addJson(jvResult, {*ledger, &context, 0}); +``` + +The `LedgerFill` constructor (in `LedgerToJson.h`) takes the `ReadView` and `context`, with `options = 0` — meaning no transaction dump, no state dump, no expansion. The zero-options default produces a compact header-level JSON representation. The `context` pointer is needed so `LedgerFill` can retrieve the close time for the ledger sequence from `LedgerMaster`. The `ledger_index` field is explicitly set before `addJson` so it's present even in partial or error-adjacent paths. + +## Design Observations + +The handler's body is only nine executable lines, but this brevity is load-bearing rather than superficial. The `Expected`-based return from `getOrAcquireLedger` means the handler needs no explicit error-code checks or conditional branching — the `if (!res.has_value()) return res.error()` idiom propagates the error JSON directly, whether it is a parameter validation failure, a sync error, or an ongoing acquisition response. This design cleanly separates what the handler cares about (declare cost, acquire ledger, serialize result) from the policy of how ledgers are resolved (encapsulated in the helper). + +The handler cannot be called with `current`, `closed`, or `validated` shortcut strings — `getOrAcquireLedger` only accepts explicit hash or numeric index, reinforcing that this command targets historical, potentially off-chain ledgers rather than the live chain state. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp.ai.json b/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp.ai.json new file mode 100644 index 0000000000..c4f2d97765 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp.ai.json @@ -0,0 +1,330 @@ +{ + "args": [ + { + "lineno": 8, + "name": "params" + }, + { + "lineno": 22, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doValidationCreate", + "validationSeed", + "parseGenericSeed (if secret provided)", + "generateSecretKey", + "derivePublicKey", + "toBase58", + "seedAs1751" + ], + "entry_point": "doValidationCreate", + "purpose": "Handles the admin RPC command to create a new validation keypair, optionally using a provided secret.", + "validation_points": [ + "doValidationCreate: checks if seed is valid (if (!seed) return rpcError(rpcBAD_SEED))", + "validationSeed: checks if params contains 'secret' (if (!params.isMember(jss::secret)))", + "validationSeed: parses and validates the secret with parseGenericSeed" + ] + } + ], + "data_flows": [ + { + "field": "params", + "flow": [ + "context.params", + "validationSeed(params)", + "parseGenericSeed(params[jss::secret].asString()) (if present)", + "Seed (optional, returned from validationSeed)", + "used in generateSecretKey" + ], + "origin": "context.params (RPC JSON input)", + "transformations": [ + "Extracts 'secret' field if present", + "Parses secret string into Seed object" + ], + "validated_at": "validationSeed (checks presence and parses secret)" + }, + { + "field": "secret", + "flow": [ + "params['secret']", + "parseGenericSeed(secret)", + "Seed (optional)" + ], + "origin": "params['secret'] (user input)", + "transformations": [ + "String to Seed conversion", + "Validation of format and content" + ], + "validated_at": "parseGenericSeed (inside validationSeed)" + }, + { + "field": "seed", + "flow": [ + "validationSeed(params)", + "doValidationCreate: if (!seed) return rpcError(rpcBAD_SEED)", + "generateSecretKey(KeyType::secp256k1, *seed)", + "derivePublicKey(KeyType::secp256k1, private_key)", + "toBase58, seedAs1751" + ], + "origin": "validationSeed(params)", + "transformations": [ + "Seed is used to generate private key", + "Private key used to derive public key", + "Keys and seed encoded to Base58 and 1751 format" + ], + "validated_at": "doValidationCreate (if (!seed) ...), validationSeed" + } + ], + "description": "Implements an RPC command to create a new validation key pair and seed for the XRPL, requiring admin access.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "params (as JSON object, by context)", + "validation", + "missing", + "check" + ], + "evidence": "Field params (as JSON object, by context) validated by xrpld jss:: (JSON field tags), parseGenericSeed, RPC::JsonContext", + "issue_pattern": "Missing validation for params (as JSON object, by context)", + "why_false_positive": "xrpld jss:: (JSON field tags), parseGenericSeed, RPC::JsonContext validates params (as JSON object, by context) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "secret (as string, by parseGenericSeed)", + "validation", + "missing", + "check" + ], + "evidence": "Field secret (as string, by parseGenericSeed) validated by xrpld jss:: (JSON field tags), parseGenericSeed, RPC::JsonContext", + "issue_pattern": "Missing validation for secret (as string, by parseGenericSeed)", + "why_false_positive": "xrpld jss:: (JSON field tags), parseGenericSeed, RPC::JsonContext validates secret (as string, by parseGenericSeed) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "secret", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::secret) at validationSeed", + "issue_pattern": "Missing empty string validation for secret", + "why_false_positive": "params.isMember(jss::secret) validates secret for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "secret", + "empty", + "string", + "validation" + ], + "evidence": "parseGenericSeed at validationSeed", + "issue_pattern": "Missing empty string validation for secret", + "why_false_positive": "parseGenericSeed validates secret for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "params", + "empty", + "string", + "validation" + ], + "evidence": "context.params at doValidationCreate", + "issue_pattern": "Missing empty string validation for params", + "why_false_positive": "context.params validates params for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "params", + "type", + "validation", + "check" + ], + "evidence": "context.params at doValidationCreate", + "issue_pattern": "Missing type validation for params", + "why_false_positive": "context.params validates params type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "seed", + "empty", + "string", + "validation" + ], + "evidence": "if (!seed) return rpcError(rpcBAD_SEED) at doValidationCreate", + "issue_pattern": "Missing empty string validation for seed", + "why_false_positive": "if (!seed) return rpcError(rpcBAD_SEED) validates seed for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp", + "functions": [ + { + "args": [ + "params" + ], + "lineno": 8, + "name": "validationSeed" + }, + { + "args": [ + "context" + ], + "lineno": 22, + "name": "doValidationCreate" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is likely covered by admin RPC handler tests, especially those testing the 'validation_create' or similar endpoints. Tests should cover: (1) valid secret provided, (2) no secret provided (random seed), (3) invalid secret (parseGenericSeed fails), (4) missing or malformed params. Gaps may exist if there are no tests for edge cases (e.g., empty string, non-string secret, malformed JSON). Test files to check: rpc/handlers/admin/keygen/ValidationCreate_test.cpp, or more general admin RPC handler test suites. If parseGenericSeed or randomSeed are not directly tested, their error handling may be missed.", + "validation_architecture": { + "auto_validated_fields": [ + "params (as JSON object, by context)", + "secret (as string, by parseGenericSeed)" + ], + "framework": "xrpld jss:: (JSON field tags), parseGenericSeed, RPC::JsonContext", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (falls back to randomSeed)", + "field": "secret", + "location": "validationSeed", + "validated_by": "params.isMember(jss::secret)", + "validates": [ + "Checks if 'secret' field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "returns std::nullopt if invalid, triggers rpcError(rpcBAD_SEED) in doValidationCreate", + "field": "secret", + "location": "validationSeed", + "validated_by": "parseGenericSeed", + "validates": [ + "Checks if 'secret' string can be parsed as a valid seed (format/type validation)", + "Ensures the seed is a valid cryptographic value" + ], + "validation_type": "format|type" + }, + { + "confidence": 0.8, + "error_thrown": "none (assumes params is a Json::Value)", + "field": "params", + "location": "doValidationCreate", + "validated_by": "context.params", + "validates": [ + "Assumes params is a valid JSON object (type validation by framework/context)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcBAD_SEED)", + "field": "seed", + "location": "doValidationCreate", + "validated_by": "if (!seed) return rpcError(rpcBAD_SEED)", + "validates": [ + "Ensures a valid seed was generated or parsed before proceeding" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp.ai.md b/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp.ai.md new file mode 100644 index 0000000000..2c8f59eb41 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/keygen/ValidationCreate.cpp.ai.md @@ -0,0 +1,42 @@ +# `ValidationCreate.cpp` — Admin RPC Handler for Validator Key Generation + +## Role in the System + +`ValidationCreate.cpp` implements the `validation_create` admin-only RPC command, which generates a secp256k1 key pair suitable for use as a validator's signing identity on the XRP Ledger. Validators use this key pair to sign validation messages that are broadcast during consensus; the public key appears in Unique Node Lists (UNLs) that operators publish, while the private key (or its seed) is stored in the validator's `rippled.cfg`. + +The handler lives alongside `WalletPropose.cpp` in the `keygen/` subdirectory, reflecting a conceptual grouping: both commands produce cryptographic key material from seeds. However, their purposes diverge. `wallet_propose` creates account credentials (account IDs, `secp256k1` or `ed25519`, entropy warnings for passphrases), whereas `validation_create` is narrowly scoped to the validator use case — always secp256k1, always returning `NodePublic`/`NodePrivate` token types. + +## The `validationSeed()` Helper + +The file-local `validationSeed()` function encapsulates the seed-sourcing decision: if no `secret` field is present in the request params, it calls `randomSeed()` to generate 128 bits of cryptographically secure entropy. If a `secret` is provided, it delegates to `parseGenericSeed()`, which accepts multiple formats — Base58-encoded family seeds, passphrase strings (hashed via SHA512-Half to 128 bits), and legacy RFC 1751 mnemonic word lists. On parse failure, `parseGenericSeed()` returns `std::nullopt`, which propagates back as `std::nullopt` from `validationSeed()`. The `Seed` class itself enforces no-default-construction and securely zeroes its 16-byte buffer in its destructor, ensuring key material does not linger in memory after the `Seed` object is destroyed. + +## `doValidationCreate()` — The RPC Entry Point + +``` +validationSeed → generateSecretKey → derivePublicKey → toBase58 / seedAs1751 +``` + +The main handler follows a simple linear pipeline. After extracting the seed, it immediately gates on the `if (!seed)` check and returns `rpcBAD_SEED` before touching any crypto — preventing downstream code from receiving an invalid state. It then calls `generateSecretKey(KeyType::secp256k1, *seed)` and `derivePublicKey(KeyType::secp256k1, private_key)` to produce the key pair deterministically from the seed. + +The four fields returned to the caller reflect different operational needs: + +| Field | Encoding | Typical Use | +|---|---|---| +| `validation_public_key` | Base58 `NodePublic` token | Published in UNLs; trusted by peers | +| `validation_private_key` | Base58 `NodePrivate` token | Stored locally; signs validation messages | +| `validation_seed` | Base58 `FamilySeed` token | `[validation_seed]` in `rippled.cfg` (older approach) | +| `validation_key` | RFC 1751 mnemonic | Legacy human-readable backup of the seed | + +The `NodePublic` / `NodePrivate` token types encode differently from the `AccountPublic` / `AccountPublic` tokens used by `wallet_propose`, making it structurally impossible to confuse a validator key for an account key at the Base58 layer. + +## Why Admin-Only? + +The source comment captures it directly: *"This command requires Role::ADMIN access because it makes no sense to ask an untrusted server for this."* In `Handler.cpp`, the command is registered as `Role::ADMIN`. The security implication is that if a non-operator server generated your validator seed, the server could log or return a seed it controls, subverting the validator's identity. Requiring admin access forces the operator to call this command against their own node. + +## Contrast with `wallet_propose` + +`WalletPropose.cpp` is substantially more complex: it handles `ed25519` vs `secp256k1` selection, detects XrplLib-encoded ed25519 seeds, runs a Shannon-entropy check on passphrases to warn about brain-wallet weakness, and returns account-layer identifiers. `ValidationCreate.cpp` deliberately omits all of that — validator keys always use secp256k1 (the XRPL's canonical validator algorithm) and there is no account ID to derive. The simplicity is intentional; the handler does exactly what a validator bootstrap script needs and nothing more. + +## Error Handling + +The sole error path is `rpcBAD_SEED`, returned when `parseGenericSeed()` cannot interpret the supplied `secret` string. There is no error for a missing `secret` — that case silently falls through to random key generation, which is the expected no-argument behavior documented in the CLI help text (`validation_create [||]`). No exceptions are thrown or caught; the `std::optional` return type of `validationSeed()` serves as the error channel. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp.ai.json b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp.ai.json new file mode 100644 index 0000000000..4dda7f749b --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp.ai.json @@ -0,0 +1,417 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doWalletPropose", + "walletPropose" + ], + "entry_point": "doWalletPropose", + "purpose": "Handles the wallet_propose RPC command, generating a wallet from user-supplied or random seed/passphrase/key_type.", + "validation_points": [ + "walletPropose: params[jss::key_type].isString()", + "walletPropose: keyTypeFromString(params[jss::key_type].asString())", + "walletPropose: RPC::parseXrplLibSeed(params[jss::passphrase])", + "walletPropose: RPC::parseXrplLibSeed(params[jss::seed])", + "walletPropose: RPC::getSeedFromRPC(params, err)" + ] + } + ], + "data_flows": [ + { + "field": "key_type", + "flow": [ + "params[jss::key_type]", + "walletPropose: checked with isString()", + "walletPropose: converted with keyTypeFromString()", + "used to select key type for generateKeyPair" + ], + "origin": "params[jss::key_type] (JSON RPC input)", + "transformations": [ + "Type check (isString)", + "String to enum (keyTypeFromString)" + ], + "validated_at": "walletPropose: isString() and keyTypeFromString()" + }, + { + "field": "passphrase", + "flow": [ + "params[jss::passphrase]", + "walletPropose: checked for presence", + "walletPropose: parsed with RPC::parseXrplLibSeed", + "If not libSeed, passed to RPC::getSeedFromRPC if present", + "Used to derive seed" + ], + "origin": "params[jss::passphrase] (JSON RPC input)", + "transformations": [ + "Parsed as possible XrplLib seed", + "If not, used as passphrase for seed derivation" + ], + "validated_at": "walletPropose: RPC::parseXrplLibSeed and RPC::getSeedFromRPC" + }, + { + "field": "seed", + "flow": [ + "params[jss::seed]", + "walletPropose: checked for presence", + "walletPropose: parsed with RPC::parseXrplLibSeed", + "If not libSeed, passed to RPC::getSeedFromRPC if present", + "Used to derive wallet keys" + ], + "origin": "params[jss::seed] (JSON RPC input)", + "transformations": [ + "Parsed as possible XrplLib seed", + "If not, used as seed for key derivation" + ], + "validated_at": "walletPropose: RPC::parseXrplLibSeed and RPC::getSeedFromRPC" + }, + { + "field": "seed_hex", + "flow": [ + "params[jss::seed_hex]", + "walletPropose: checked for presence", + "walletPropose: passed to RPC::getSeedFromRPC if present", + "Used to derive wallet keys" + ], + "origin": "params[jss::seed_hex] (JSON RPC input)", + "transformations": [ + "Parsed as hex seed" + ], + "validated_at": "walletPropose: RPC::getSeedFromRPC" + }, + { + "field": "params", + "flow": [ + "doWalletPropose: receives context.params", + "walletPropose: receives params", + "All field validations and extractions occur here" + ], + "origin": "RPC::JsonContext.params (JSON RPC input)", + "transformations": [ + "Field extraction", + "Validation", + "Seed/key derivation" + ], + "validated_at": "walletPropose: multiple points" + } + ], + "description": "Implements the walletPropose RPC handler for generating XRPL wallet keys, including entropy estimation for passphrases and support for different key types and seed formats.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "key_type (type and value)", + "validation", + "missing", + "check" + ], + "evidence": "Field key_type (type and value) validated by jss:: (JSON field tags), RPC:: (helper functions), keyTypeFromString, parseXrplLibSeed, getSeedFromRPC", + "issue_pattern": "Missing validation for key_type (type and value)", + "why_false_positive": "jss:: (JSON field tags), RPC:: (helper functions), keyTypeFromString, parseXrplLibSeed, getSeedFromRPC validates key_type (type and value) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "passphrase (format as seed)", + "validation", + "missing", + "check" + ], + "evidence": "Field passphrase (format as seed) validated by jss:: (JSON field tags), RPC:: (helper functions), keyTypeFromString, parseXrplLibSeed, getSeedFromRPC", + "issue_pattern": "Missing validation for passphrase (format as seed)", + "why_false_positive": "jss:: (JSON field tags), RPC:: (helper functions), keyTypeFromString, parseXrplLibSeed, getSeedFromRPC validates passphrase (format as seed) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "seed (format as seed)", + "validation", + "missing", + "check" + ], + "evidence": "Field seed (format as seed) validated by jss:: (JSON field tags), RPC:: (helper functions), keyTypeFromString, parseXrplLibSeed, getSeedFromRPC", + "issue_pattern": "Missing validation for seed (format as seed)", + "why_false_positive": "jss:: (JSON field tags), RPC:: (helper functions), keyTypeFromString, parseXrplLibSeed, getSeedFromRPC validates seed (format as seed) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "seed_hex (format as seed)", + "validation", + "missing", + "check" + ], + "evidence": "Field seed_hex (format as seed) validated by jss:: (JSON field tags), RPC:: (helper functions), keyTypeFromString, parseXrplLibSeed, getSeedFromRPC", + "issue_pattern": "Missing validation for seed_hex (format as seed)", + "why_false_positive": "jss:: (JSON field tags), RPC:: (helper functions), keyTypeFromString, parseXrplLibSeed, getSeedFromRPC validates seed_hex (format as seed) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "key_type", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::key_type].isString() at walletPropose", + "issue_pattern": "Missing empty string validation for key_type", + "why_false_positive": "params[jss::key_type].isString() validates key_type for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "key_type", + "type", + "validation", + "check" + ], + "evidence": "params[jss::key_type].isString() at walletPropose", + "issue_pattern": "Missing type validation for key_type", + "why_false_positive": "params[jss::key_type].isString() validates key_type type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "key_type", + "empty", + "string", + "validation" + ], + "evidence": "keyTypeFromString(params[jss::key_type].asString()) at walletPropose", + "issue_pattern": "Missing empty string validation for key_type", + "why_false_positive": "keyTypeFromString(params[jss::key_type].asString()) validates key_type for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "passphrase", + "empty", + "string", + "validation" + ], + "evidence": "RPC::parseXrplLibSeed(params[jss::passphrase]) at walletPropose", + "issue_pattern": "Missing empty string validation for passphrase", + "why_false_positive": "RPC::parseXrplLibSeed(params[jss::passphrase]) validates passphrase for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "seed", + "empty", + "string", + "validation" + ], + "evidence": "RPC::parseXrplLibSeed(params[jss::seed]) at walletPropose", + "issue_pattern": "Missing empty string validation for seed", + "why_false_positive": "RPC::parseXrplLibSeed(params[jss::seed]) validates seed for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "passphrase|seed|seed_hex", + "empty", + "string", + "validation" + ], + "evidence": "RPC::getSeedFromRPC(params, err) at walletPropose", + "issue_pattern": "Missing empty string validation for passphrase|seed|seed_hex", + "why_false_positive": "RPC::getSeedFromRPC(params, err) validates passphrase|seed|seed_hex for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp", + "functions": [ + { + "args": [ + "input" + ], + "lineno": 10, + "name": "estimate_entropy" + }, + { + "args": [ + "context" + ], + "lineno": 36, + "name": "doWalletPropose" + }, + { + "args": [ + "params" + ], + "lineno": 41, + "name": "walletPropose" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in the unit/integration test suites for RPC handlers, e.g., in files like 'test_wallet_propose.cpp', 'rpc_wallet_propose_test.cpp', or similar. Tests should cover: valid/invalid key_type, passphrase, seed, seed_hex; missing/invalid fields; entropy warnings for weak passphrases; XrplLib seed detection. Gaps may exist if tests do not cover all combinations of malformed input (e.g., non-string key_type, invalid seed formats, conflicting fields), or do not check for proper error codes and warnings. No test files are referenced in the provided code, so actual coverage must be verified in the repository.", + "validation_architecture": { + "auto_validated_fields": [ + "key_type (type and value)", + "passphrase (format as seed)", + "seed (format as seed)", + "seed_hex (format as seed)" + ], + "framework": "jss:: (JSON field tags), RPC:: (helper functions), keyTypeFromString, parseXrplLibSeed, getSeedFromRPC", + "validation_layer": "business_logic (walletPropose is the main validation entry point for this handler)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::expected_field_error(jss::key_type, \"string\")", + "field": "key_type", + "location": "walletPropose", + "validated_by": "params[jss::key_type].isString()", + "validates": [ + "Checks that key_type is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "key_type", + "location": "walletPropose", + "validated_by": "keyTypeFromString(params[jss::key_type].asString())", + "validates": [ + "Checks that key_type is a valid/recognized key type string" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcBAD_SEED) if keyType is not ed25519", + "field": "passphrase", + "location": "walletPropose", + "validated_by": "RPC::parseXrplLibSeed(params[jss::passphrase])", + "validates": [ + "Checks if passphrase can be parsed as an XRPLLib seed", + "If so, ensures keyType is ed25519" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcBAD_SEED) if keyType is not ed25519", + "field": "seed", + "location": "walletPropose", + "validated_by": "RPC::parseXrplLibSeed(params[jss::seed])", + "validates": [ + "Checks if seed can be parsed as an XRPLLib seed", + "If so, ensures keyType is ed25519" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "err (Json::Value error object)", + "field": "passphrase|seed|seed_hex", + "location": "walletPropose", + "validated_by": "RPC::getSeedFromRPC(params, err)", + "validates": [ + "Checks that at least one of passphrase, seed, or seed_hex is present and valid", + "Validates the format and type of the seed input" + ], + "validation_type": "format|type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp.ai.md b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp.ai.md new file mode 100644 index 0000000000..e7eb1414f7 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.cpp.ai.md @@ -0,0 +1,45 @@ +# `WalletPropose.cpp` — Admin Key Generation RPC Handler + +## Role and Purpose + +This file implements the `wallet_propose` admin RPC command, which generates a complete XRPL account identity — seed, key pair, and account address — from a caller-supplied or randomly-generated seed. It sits in the `admin/keygen` handler path, reflecting that key generation is exclusively an admin-level operation not exposed to untrusted clients. + +The entry point `doWalletPropose()` is a thin wrapper that extracts `context.params` and delegates to the free function `walletPropose()`. Separating the two allows `walletPropose()` to be tested and reused independently of the RPC dispatch context — the test suite in `KeyGeneration_test.cpp` calls it directly with raw `Json::Value` parameters. + +## Seed Resolution Chain + +The core of `walletPropose()` is a priority-ordered seed resolution strategy. The caller may supply a seed through three mutually-exclusive channels: `passphrase` (a human-readable string hashed to a seed), `seed` (a base58-encoded seed), or `seed_hex` (a hex-encoded seed). If none are provided, `randomSeed()` generates a cryptographically secure random seed. + +Before falling through to the standard `getSeedFromRPC()` helper, there is a first-pass check for XrplLib-encoded seeds. The XrplLib JavaScript library historically encoded Ed25519 seeds using a non-standard base58 prefix of `0xE1 0x4B`, distinct from rippled's own encoding. The `parseXrplLibSeed()` helper in `RPCHelpers.cpp` detects this 18-byte form and returns the 16-byte seed content, setting the `libSeed` flag. This matters because XrplLib seeds are unambiguously Ed25519; if a caller supplies one but also requests `key_type: "secp256k1"`, the handler returns `rpcBAD_SEED` rather than silently producing a wrong key type. + +## Key Type Defaulting + +`keyType` starts as `std::nullopt`. If the caller requests an explicit `key_type`, the string is validated via `keyTypeFromString()` (returning `nullopt` on unrecognized values, which yields `rpcINVALID_PARAMS`). If a XrplLib seed is detected, `keyType` is forced to `KeyType::ed25519`. If no key type is ever specified, it defaults to `KeyType::secp256k1` — preserving historical compatibility with clients that predate Ed25519 support on the ledger. + +## Output Fields + +After `generateKeyPair(*keyType, *seed)` produces the public key, `walletPropose()` assembles a result object with six fields representing the same seed and key in different encodings: + +- `master_seed` — base58-encoded seed (the canonical wallet backup format) +- `master_seed_hex` — raw hex of the seed bytes +- `master_key` — [RFC 1751](https://www.rfc-editor.org/rfc/rfc1751) mnemonic encoding via `seedAs1751()`, intended for humans writing it down +- `account_id` — base58check address derived from `calcAccountID(publicKey)` +- `public_key` — base58-encoded public key with `AccountPublic` token prefix +- `public_key_hex` — raw hex public key +- `key_type` — the resolved algorithm as a string + +## Passphrase Entropy Warning + +The `estimate_entropy()` function computes [Shannon entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory)) over character frequencies in the passphrase, then multiplies by length to get a total bit estimate, floored to be conservative. If the estimate falls below 80 bits, the response includes a strong warning about brute-force vulnerability. If it equals or exceeds 80 bits, a softer advisory is still emitted, because any deterministic passphrase-to-seed derivation ("brain wallet") is inherently weaker than a truly random seed. + +One subtle defensive check prevents spurious warnings: before running the entropy test, the handler compares the raw passphrase string against the seed's own 1751 encoding, base58 form, and hex form. If they match, the user is passing the seed itself — just formatted as a passphrase — not a memorable phrase. In that case, no warning is emitted, since the entropy of a randomly-generated seed is already adequate regardless of how it was transmitted in the request. + +The `libSeed` flag also suppresses the warning for XrplLib-detected seeds, since those follow a defined deterministic format rather than being user-invented phrases. + +## Error Handling + +The handler uses three distinct error paths: `RPC::expected_field_error()` for type mismatches (non-string `key_type`), `rpcError(rpcINVALID_PARAMS)` for unrecognized key type strings, `rpcError(rpcBAD_SEED)` for XrplLib seed / key type conflicts, and an out-parameter `err` Json value populated by `getSeedFromRPC()` for malformed seed inputs. No exceptions are used; all failure paths return early with a `Json::Value` error object. + +## Relationship to Test Coverage + +`KeyGeneration_test.cpp` exercises `walletPropose()` directly with known constant vectors for both `secp256k1` and `ed25519`, validating all six output fields plus the entropy warning text. The test data (`"REINDEER FLOTILLA"` as a low-entropy passphrase and a high-entropy random-looking passphrase) explicitly covers both warning tiers, giving good confidence in the entropy threshold logic. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.h.ai.json b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.h.ai.json new file mode 100644 index 0000000000..6035d7dcf7 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.h.ai.json @@ -0,0 +1,27 @@ +{ + "args": [ + { + "lineno": 6, + "name": "params" + } + ], + "classes": [], + "description": "Header file declaring the walletPropose function for generating or proposing a wallet, likely for use in XRPL RPC handlers.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.h", + "functions": [ + { + "args": [ + "params" + ], + "lineno": 6, + "name": "walletPropose" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.h.ai.md b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.h.ai.md new file mode 100644 index 0000000000..86fdd43bf6 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/keygen/WalletPropose.h.ai.md @@ -0,0 +1,25 @@ +# `WalletPropose.h` — Wallet Key Generation RPC Declaration + +This header declares `walletPropose()`, the core key-generation function powering the XRPL `wallet_propose` admin RPC command. Its single-line declaration belies a deliberate architectural split: the function accepts a raw `Json::Value` parameter set rather than the full `RPC::JsonContext`, allowing the key-generation logic to be called and tested independently from the RPC machinery. + +## Role in the System + +`wallet_propose` is an admin-restricted RPC command that generates a new XRPL account — a cryptographic key pair and its derived account ID — without submitting anything to the ledger. It lives under `src/xrpld/rpc/handlers/admin/keygen/`, alongside `ValidationCreate.cpp`, grouping all privileged key-material generation in one place. The companion `doWalletPropose()` function (declared in `Handlers.h` and implemented in `WalletPropose.cpp`) is the thin RPC adapter that unwraps the `JsonContext` and delegates straight to `walletPropose(context.params)`. + +## The `walletPropose` Function + +The function handles three distinct input scenarios: + +1. **No seed material provided** — generates a fresh cryptographically random seed via `randomSeed()`. +2. **User-supplied passphrase, seed, or seed_hex** — first attempts to parse as an XrplLib-encoded seed via `RPC::parseXrplLibSeed()`, falling back to the standard `RPC::getSeedFromRPC()` path. XrplLib uses a non-standard encoding for Ed25519 seeds; detecting it early prevents user confusion about key type mismatches. +3. **XrplLib seed detected** — locks the key type to `Ed25519` and returns an error (`rpcBAD_SEED`) if the caller explicitly requested a conflicting algorithm. + +After resolving the seed and key type (defaulting to `secp256k1` when unspecified), it calls `generateKeyPair(*keyType, *seed)` and builds the response object with six fields: `master_seed` (Base58), `master_seed_hex`, `master_key` (1751-word mnemonic encoding), `account_id`, `public_key` (Base58), and `public_key_hex`. + +## Entropy Warning Design + +A notable feature is the passphrase entropy check. When a passphrase is used and it doesn't look like an already-encoded seed (Base58, hex, or 1751 mnemonic), `estimate_entropy()` computes a Shannon-entropy-based bit estimate. Below 80 bits the response includes a strong "vulnerable to brute-force attacks" warning; above it, a softer advisory is added. This is a deliberate user-safety mechanism — "brain wallets" derived from weak passphrases are a known attack vector on blockchain accounts. + +## Why This Header Exists + +The separation of the bare `walletPropose(Json::Value const&)` signature into its own header — rather than only exposing `doWalletPropose(RPC::JsonContext&)` — allows the key generation logic to be exercised directly in unit tests (see `test/rpc/KeyGeneration_test.cpp`) without constructing a full server context. The header's only dependency is ``, keeping the include footprint minimal and the function usable from any layer that can construct a JSON object. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp.ai.json b/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp.ai.json new file mode 100644 index 0000000000..0c86c6ad21 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp.ai.json @@ -0,0 +1,259 @@ +{ + "args": [ + { + "lineno": 12, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doLogLevel" + ], + "entry_point": "doLogLevel", + "purpose": "Handles the 'log_level' RPC command, which queries or sets log severity levels globally or per partition.", + "validation_points": [ + "if (!context.params.isMember(jss::severity))", + "Logs::fromString(context.params[jss::severity].asString())", + "if (!context.params.isMember(jss::partition))", + "if (context.params.isMember(jss::partition))", + "if (boost::iequals(partition, \"base\"))" + ] + } + ], + "data_flows": [ + { + "field": "severity", + "flow": [ + "context.params[jss::severity] (input)", + "context.params.isMember(jss::severity) (existence check)", + "context.params[jss::severity].asString() (string extraction)", + "Logs::fromString(...) (string to enum conversion)", + "if (sv == lsINVALID) (enum validity check)", + "Logs::toSeverity(sv) (enum to internal severity type)", + "context.app.getLogs().threshold(severity) or .get(partition).threshold(severity) (applied)" + ], + "origin": "context.params[jss::severity] (JSON RPC input)", + "transformations": [ + "JSON string \u2192 enum (Logs::fromString)", + "enum \u2192 internal severity (Logs::toSeverity)" + ], + "validated_at": [ + "context.params.isMember(jss::severity)", + "Logs::fromString(...)", + "if (sv == lsINVALID)" + ] + }, + { + "field": "partition", + "flow": [ + "context.params[jss::partition] (input)", + "context.params.isMember(jss::partition) (existence check)", + "context.params[jss::partition].asString() (string extraction)", + "boost::iequals(partition, \"base\") (case-insensitive comparison)", + "context.app.getLogs().threshold(severity) or .get(partition).threshold(severity) (applied)" + ], + "origin": "context.params[jss::partition] (JSON RPC input)", + "transformations": [ + "JSON string \u2192 std::string", + "case-insensitive comparison (boost::iequals)" + ], + "validated_at": [ + "context.params.isMember(jss::partition)", + "boost::iequals(partition, \"base\")" + ] + } + ], + "description": "Implements the doLogLevel RPC handler for querying and setting log severity levels in the xrpl application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "severity", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::severity) at doLogLevel", + "issue_pattern": "Missing empty string validation for severity", + "why_false_positive": "context.params.isMember(jss::severity) validates severity for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "severity", + "empty", + "string", + "validation" + ], + "evidence": "Logs::fromString(context.params[jss::severity].asString()) at doLogLevel", + "issue_pattern": "Missing empty string validation for severity", + "why_false_positive": "Logs::fromString(context.params[jss::severity].asString()) validates severity for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "partition", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::partition) at doLogLevel", + "issue_pattern": "Missing empty string validation for partition", + "why_false_positive": "context.params.isMember(jss::partition) validates partition for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "partition", + "empty", + "string", + "validation" + ], + "evidence": "boost::iequals(partition, \"base\") at doLogLevel", + "issue_pattern": "Missing empty string validation for partition", + "why_false_positive": "boost::iequals(partition, \"base\") validates partition for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 11, + "name": "doLogLevel" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in the RPC integration or unit test suites, such as 'rpc_log_level_test.cpp' or similar files. Tests should cover: (1) querying log levels with no params, (2) setting global severity, (3) setting partition severity (including 'base'), (4) invalid severity values, (5) invalid/missing partition. Gaps may exist if there are no tests for edge cases (e.g., unknown partition names, malformed severity strings, or missing required fields). No test files are referenced in this code, so actual coverage must be checked in the test directory.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "jss:: (JSON field tags), Json::Value, custom Logs::* functions", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns log levels instead)", + "field": "severity", + "location": "doLogLevel", + "validated_by": "context.params.isMember(jss::severity)", + "validates": [ + "checks if 'severity' field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "severity", + "location": "doLogLevel", + "validated_by": "Logs::fromString(context.params[jss::severity].asString())", + "validates": [ + "converts string to LogSeverity enum", + "checks if value is a valid log severity string" + ], + "validation_type": "business_logic|format" + }, + { + "confidence": 1.0, + "error_thrown": "none (sets base threshold if not present)", + "field": "partition", + "location": "doLogLevel", + "validated_by": "context.params.isMember(jss::partition)", + "validates": [ + "checks if 'partition' field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "none (special handling for 'base')", + "field": "partition", + "location": "doLogLevel", + "validated_by": "boost::iequals(partition, \"base\")", + "validates": [ + "checks if partition string equals 'base' (case-insensitive)", + "routes logic accordingly" + ], + "validation_type": "business_logic|format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp.ai.md b/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp.ai.md new file mode 100644 index 0000000000..edf657ca3d --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/log/LogLevel.cpp.ai.md @@ -0,0 +1,33 @@ +# `LogLevel.cpp` — `log_level` Admin RPC Handler + +## Role in the System + +This file implements `doLogLevel`, the sole handler for the `log_level` admin RPC command in the XRPL node. It is registered in the central dispatch table (`Handler.cpp`) as an `ADMIN`-role command, meaning it is only available to operators with privileged API access. Its purpose is runtime log-level control: operators can query the current severity thresholds across all logging partitions, or change them globally or per subsystem without restarting the node. + +The sibling file `LogRotate.cpp` in the same directory handles the companion `logrotate` command; together they form the complete set of runtime log management primitives. + +## How the Handler Works + +`doLogLevel` implements a single three-mode dispatch off the shape of its JSON input: + +**Query mode** (no `severity` field): Returns a JSON object keyed as `levels` containing the base threshold and all per-partition thresholds. It calls `context.app.getLogs().threshold()` to retrieve the global severity, then `partition_severities()` to enumerate every named subsystem (e.g., `Ledger`, `Consensus`, `Network`) alongside its current threshold as a human-readable string. + +**Global set mode** (`severity` only, no `partition`): Parses the severity string and calls `context.app.getLogs().threshold(severity)` to update the global base threshold. All partitions that haven't been individually overridden will be filtered at this level. + +**Partition set mode** (`severity` + `partition`): Sets the threshold on a specific named partition via `context.app.getLogs().get(partition).threshold(severity)`. The name `"base"` is treated as a special alias for the global threshold — detected via `boost::iequals` so that `"Base"`, `"BASE"`, or any mixed-case variant is handled identically. + +## Severity Type Pipeline + +The severity value traverses a notable two-step conversion chain. The raw JSON string is first converted to the deprecated `LogSeverity` enum via `Logs::fromString()`, which returns `lsINVALID` for any unrecognised string. This sentinel is checked immediately; an unrecognised severity string produces `rpcINVALID_PARAMS` rather than silently clamping or defaulting. The valid enum value is then converted to `beast::severities::Severity` via `Logs::toSeverity()` before being applied. This indirection exists because the codebase originally used the `LogSeverity` enum throughout but migrated to `beast::Journal`'s severity type; `fromString`/`toSeverity` are the translation boundary between the legacy interface and the current one, as `Log.h` explicitly marks `LogSeverity` as deprecated. + +## The `Logs` Partitioning Model + +The `Logs` class (declared in `xrpl/basics/Log.h`) maintains a `std::map` of named `Sink` objects, each wrapping a `beast::Journal::Sink`. Every subsystem acquires a named journal at startup (e.g., `app.getLogs().journal("Consensus")`), and each sink inherits the global threshold unless individually overridden. `partition_severities()` iterates that map to produce the name→severity-string pairs returned in query mode. The map key comparison uses `boost::beast::iless`, meaning partition names are themselves case-insensitive at the storage level — consistent with the `boost::iequals` check in the handler. + +## Structural Oddity + +There is a subtle unreachable code path at the bottom of the function. The handler checks `!context.params.isMember(jss::partition)` at line 41; if that passes, it sets the base threshold and returns. The immediately following `if (context.params.isMember(jss::partition))` at line 49 is therefore only reached when `partition` *is* present, making the condition trivially true every time control reaches it. The final `return rpcError(rpcINVALID_PARAMS)` at line 66 is unreachable. This is a structural artifact of incremental growth — the code reads as if a future branch was anticipated — but it introduces no functional defect. + +## Validation Summary + +Input validation is intentionally minimal and follows the handler idiom common across `xrpld`. Presence of `jss::severity` is used as the branch discriminator rather than a hard requirement — its absence is the legitimate query path. Once present, the severity string is validated by `Logs::fromString()` returning `lsINVALID`, which is the only error return in the entire function. Unknown partition names are silently accepted: `Logs::get()` creates a new sink on demand if none exists by that name, so mistyped partition names result in a newly created partition threshold rather than an error — operators should be careful with spelling. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp.ai.json b/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp.ai.json new file mode 100644 index 0000000000..f8ff6da729 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp.ai.json @@ -0,0 +1,101 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RPC framework dispatches to doLogRotate", + "doLogRotate", + "context.app.getPerfLog().rotate()", + "context.app.getLogs().rotate()", + "RPC::makeObjectValue" + ], + "entry_point": "doLogRotate", + "purpose": "Handles the 'log_rotate' RPC command, rotating log files and returning the result.", + "validation_points": [ + "RPC framework (before doLogRotate): likely validates admin permissions and request structure", + "No explicit validation in doLogRotate itself" + ] + } + ], + "data_flows": [ + { + "field": "context", + "flow": [ + "RPC framework receives request", + "Constructs JsonContext (context)", + "Passes context to doLogRotate" + ], + "origin": "RPC::JsonContext (constructed by RPC framework from incoming HTTP/JSON-RPC request)", + "transformations": [ + "No transformation in doLogRotate; context is used as-is" + ], + "validated_at": "Likely validated by RPC framework before handler is called (e.g., admin access, request shape)" + }, + { + "field": "context.app", + "flow": [ + "context", + "context.app", + "context.app.getPerfLog().rotate()", + "context.app.getLogs().rotate()" + ], + "origin": "Application instance (injected into context by framework)", + "transformations": [ + "Used to access logging subsystems; no transformation" + ], + "validated_at": "Not validated in doLogRotate; assumed valid by design" + }, + { + "field": "RPC response", + "flow": [ + "context.app.getLogs().rotate()", + "RPC::makeObjectValue", + "Returned as JSON to client" + ], + "origin": "Result of context.app.getLogs().rotate()", + "transformations": [ + "Result is wrapped in a JSON object" + ], + "validated_at": "No validation of output in doLogRotate" + } + ], + "description": "Implements the doLogRotate RPC handler, which rotates log files and performance logs in the XRPL application.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doLogRotate" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is likely tested via integration or functional tests that exercise the 'log_rotate' RPC command, typically in files like 'rpc-admin_test.cpp', 'log_test.cpp', or similar. There is no evidence of unit tests for doLogRotate itself, and no explicit validation logic in the function. Validation (such as admin access) is expected to be enforced by the RPC framework before this handler is invoked. Gaps: No direct unit test coverage for doLogRotate; relies on higher-level tests and framework validation.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected", + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp.ai.md b/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp.ai.md new file mode 100644 index 0000000000..ce7996a862 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/log/LogRotate.cpp.ai.md @@ -0,0 +1,22 @@ +# `LogRotate.cpp` — `logrotate` RPC Handler + +This file implements `doLogRotate`, the handler behind the `logrotate` admin RPC command. Its entire body is two lines: one to rotate the performance log, and one to rotate the application log and return the result. The simplicity is intentional — all complexity lives in the subsystems it delegates to. + +## What it does + +`doLogRotate` invokes `rotate()` on two distinct logging systems exposed through the `Application` object: + +1. **`context.app.getPerfLog().rotate()`** — triggers log rotation on the performance log, a separate structured file tracking job queue timings and operational metrics. `PerfLog::rotate()` is a pure virtual method defined in `PerfLog.h`, meaning the concrete implementation varies but the interface contract is stable. + +2. **`context.app.getLogs().rotate()`** — rotates the main application log. The `Logs` class owns an inner `Logs::File` object that keeps the log file open for the process lifetime. The `rotate()` method (backed by `File::closeAndReopen()`) closes the current file descriptor and reopens it at the same path. This is the standard pattern for interoperating with external log management tools like `logrotate(8)`, which rename or truncate the file before signaling the process to reopen it. The `rotate()` call returns a `std::string` status message, which `RPC::makeObjectValue` wraps into the JSON response sent back to the caller. + +## Access control and registration + +The handler is registered in `Handler.cpp` as: +```cpp +{"logrotate", byRef(&doLogRotate), Role::ADMIN, NO_CONDITION} +``` + +`Role::ADMIN` means the RPC dispatch framework rejects any caller that has not been granted admin access before this function is ever reached. `NO_CONDITION` means no particular ledger state is required. The handler itself performs no validation — it assumes the framework has already enforced the access boundary. + +The sibling `LogLevel.cpp` follows the same pattern for the `log_level` command, and both live under `handlers/admin/log/` as a natural grouping of log-management admin operations. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/Connect.cpp.ai.json b/src/xrpld/rpc/handlers/admin/peer/Connect.cpp.ai.json new file mode 100644 index 0000000000..1ba53add19 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/Connect.cpp.ai.json @@ -0,0 +1,250 @@ +{ + "args": [ + { + "lineno": 17, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doConnect" + ], + "entry_point": "doConnect", + "purpose": "Handles the RPC 'connect' command to manually connect to a peer by IP and port.", + "validation_points": [ + "context.app.config().standalone() - Validates not in standalone mode", + "context.params.isMember(jss::ip) - Validates 'ip' field is present", + "context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(Json::intValue) - Validates 'port' is integer if present" + ] + } + ], + "data_flows": [ + { + "field": "ip", + "flow": [ + "context.params[jss::ip] (input)", + "context.params.isMember(jss::ip) (validation)", + "context.params[jss::ip].asString() (conversion to string)", + "beast::IP::Endpoint::from_string(ip_str) (conversion to endpoint)", + "context.app.getOverlay().connect(ip.at_port(iPort)) (used for connection)" + ], + "origin": "context.params[jss::ip] (JSON RPC input)", + "transformations": [ + "Checked for presence", + "Converted from JSON value to string", + "Parsed into IP endpoint" + ], + "validated_at": "if (!context.params.isMember(jss::ip))" + }, + { + "field": "port", + "flow": [ + "context.params[jss::port] (input, optional)", + "context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(Json::intValue) (validation)", + "context.params[jss::port].asInt() (conversion to int, if present)", + "DEFAULT_PEER_PORT (fallback if not present)", + "ip.at_port(iPort) (used for connection)" + ], + "origin": "context.params[jss::port] (JSON RPC input, optional)", + "transformations": [ + "Checked for presence", + "Checked for integer convertibility", + "Converted from JSON value to int", + "Defaulted if not present" + ], + "validated_at": "if (context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(Json::intValue))" + }, + { + "field": "standalone", + "flow": [ + "context.app.config().standalone() (checked at start)", + "If true, returns error and aborts" + ], + "origin": "context.app.config().standalone() (application config)", + "transformations": [ + "Boolean check" + ], + "validated_at": "if (context.app.config().standalone())" + } + ], + "description": "Implements the doConnect RPC handler for connecting to a peer by IP and port in the XRPL server application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "standalone mode (application state)", + "empty", + "string", + "validation" + ], + "evidence": "context.app.config().standalone() at doConnect", + "issue_pattern": "Missing empty string validation for standalone mode (application state)", + "why_false_positive": "context.app.config().standalone() validates standalone mode (application state) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ip", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::ip) at doConnect", + "issue_pattern": "Missing empty string validation for ip", + "why_false_positive": "context.params.isMember(jss::ip) validates ip for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "port", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(Json::intValue) at doConnect", + "issue_pattern": "Missing empty string validation for port", + "why_false_positive": "context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(Json::intValue) validates port for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "port", + "type", + "validation", + "check" + ], + "evidence": "context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(Json::intValue) at doConnect", + "issue_pattern": "Missing type validation for port", + "why_false_positive": "context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(Json::intValue) validates port type" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/peer/Connect.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 17, + "name": "doConnect" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The doConnect handler is typically tested via RPC integration tests, likely found in files such as 'rpc_peer_test.cpp', 'PeerFinder_test.cpp', or 'Overlay_test.cpp'. These tests would cover valid/invalid IP and port inputs, missing fields, and standalone mode. However, edge cases such as malformed IPs, non-integer ports, and behavior in standalone mode should be explicitly checked. There may be gaps if tests do not cover all error paths (e.g., missing 'ip', invalid 'port', or standalone mode). Unit tests for the handler itself may be limited; most coverage is likely via higher-level integration tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss:: for JSON field names, Json::Value for type checking)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcNOT_SYNCED (via RPC::make_error)", + "field": "standalone mode (application state)", + "location": "doConnect", + "validated_by": "context.app.config().standalone()", + "validates": [ + "checks if the application is in standalone mode, which disallows peer connections" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "missing_field_error (via RPC::missing_field_error)", + "field": "ip", + "location": "doConnect", + "validated_by": "context.params.isMember(jss::ip)", + "validates": [ + "checks if the 'ip' field is present in the input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "rpcINVALID_PARAMS (via rpcError)", + "field": "port", + "location": "doConnect", + "validated_by": "context.params.isMember(jss::port) && !context.params[jss::port].isConvertibleTo(Json::intValue)", + "validates": [ + "checks if 'port' is present and convertible to integer" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/Connect.cpp.ai.md b/src/xrpld/rpc/handlers/admin/peer/Connect.cpp.ai.md new file mode 100644 index 0000000000..52b085556e --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/Connect.cpp.ai.md @@ -0,0 +1,44 @@ +# `Connect.cpp` — Admin RPC Handler for Manual Peer Connection + +This file implements `doConnect`, the administrative RPC handler that lets an operator instruct a running XRPL node to establish an outbound peer connection to a specific IP address and port. It lives in `src/xrpld/rpc/handlers/admin/peer/` alongside sibling handlers for peer management (`Peers.cpp`, `PeerReservationsAdd.cpp`, etc.), and represents the operator-driven counterpart to the overlay's automatic peer discovery logic. + +## Role in the System + +The XRPL overlay network discovers and connects to peers autonomously via the PeerFinder subsystem. However, there are legitimate operational reasons to force a connection manually: connecting to a known validator that isn't in the bootstrap list, bridging nodes in an isolated test network, or recovering connectivity after a network partition. `doConnect` exposes this capability through the privileged RPC interface. In `Handler.cpp`, it is registered as: + +```cpp +{"connect", byRef(&doConnect), Role::ADMIN, NO_CONDITION} +``` + +The `Role::ADMIN` requirement means only callers with administrative credentials can invoke it — this prevents arbitrary external actors from directing the node's peering behavior. + +## Validation Logic + +`doConnect` performs three sequential guards before touching the overlay: + +**Standalone mode check.** If the node is running in standalone mode (`context.app.config().standalone()`), no peer connections are possible by definition. The handler returns `rpcNOT_SYNCED` immediately. Using `rpcNOT_SYNCED` rather than a more specific error code is a mild semantic misuse, but it's consistent with how the codebase signals "this operation is meaningless in the current operating mode." + +**Required `ip` field.** The IP address is mandatory. Its absence returns a structured missing-field error via `RPC::missing_field_error(jss::ip)`, which produces a well-formed JSON error response with the field name embedded. + +**Optional `port` type check.** The port is optional but type-constrained: if present, it must be convertible to `Json::intValue`. The check uses `isConvertibleTo` rather than `isInt` to tolerate JSON numbers arriving as floats that happen to be whole-number values, without accepting strings. If the port is absent, the fallback is `DEFAULT_PEER_PORT` (2459, the IANA-assigned XRPL peer port, defined in `SystemParameters.h`). + +## IP Parsing and the Silent No-Op + +After validation, the IP string is parsed into a `beast::IP::Endpoint` via `beast::IP::Endpoint::from_string()`. This function returns an "unspecified" endpoint object when parsing fails rather than throwing. The guard: + +```cpp +if (!is_unspecified(ip)) + context.app.getOverlay().connect(ip.at_port(iPort)); +``` + +means that a syntactically invalid IP string causes the connection attempt to be silently skipped. The response to the caller is still "attempting connection to IP:..." — so there is no feedback distinguishing a well-formed IP from a malformed one. This is a subtle behavioral gap: the operator might believe a connection was initiated when in fact the IP failed to parse. A comment in the source (`// XXX Might allow domain for manual connections`) suggests the original author was aware the IP parsing is limited and that DNS resolution was considered but never implemented. + +## Asynchronous Delegation + +`Overlay::connect()` is declared as returning `void` and documented as non-blocking: "The call returns immediately, the connection attempt is performed asynchronously." `doConnect` therefore returns an informational string immediately without waiting for the TCP handshake to complete or fail. The response message (`"attempting connection to IP:... port: ..."`) signals intent, not outcome. There is no callback or status-polling mechanism exposed at the RPC layer; operators who want confirmation must query the peer list afterward. + +## Design Observations + +The function is intentionally thin — it performs no routing logic, no retry management, and no deduplication of already-connected peers. All of that responsibility belongs to `OverlayImpl`, which is the concrete implementation behind the `Overlay` abstract interface. This keeps the RPC handler strictly in the role of input validation and dispatch, consistent with the rest of the `handlers/admin/` layer. + +The lack of range validation on the port integer (no check that `iPort` is in `[1, 65535]`) means a caller could pass `0` or a negative value without triggering `rpcINVALID_PARAMS`. The overlay implementation absorbs any such misuse downstream. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp.ai.json b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp.ai.json new file mode 100644 index 0000000000..619b2ffb31 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp.ai.json @@ -0,0 +1,306 @@ +{ + "args": [ + { + "lineno": 13, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doPeerReservationsAdd" + ], + "entry_point": "doPeerReservationsAdd", + "purpose": "Handles the 'peer_reservations_add' RPC command, validates input, parses public key, and adds a peer reservation.", + "validation_points": [ + "params.isMember(jss::public_key)", + "params[jss::public_key].isString()", + "params.isMember(jss::description) && params[jss::description].isString()", + "parseBase58(TokenType::NodePublic, params[jss::public_key].asString())" + ] + } + ], + "data_flows": [ + { + "field": "public_key", + "flow": [ + "context.params", + "params.isMember(jss::public_key) (existence check)", + "params[jss::public_key].isString() (type check)", + "parseBase58(TokenType::NodePublic, params[jss::public_key].asString()) (parsing/validation)", + "nodeId (PublicKey object)", + "PeerReservation{nodeId, desc}", + "context.app.getPeerReservations().insert_or_assign(...)" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Checked for presence", + "Checked for string type", + "Parsed from base58 string to PublicKey object" + ], + "validated_at": [ + "params.isMember(jss::public_key)", + "params[jss::public_key].isString()", + "parseBase58(...)" + ] + }, + { + "field": "description", + "flow": [ + "context.params", + "params.isMember(jss::description) (optional existence check)", + "params[jss::description].isString() (type check)", + "desc = params[jss::description].asString()", + "PeerReservation{nodeId, desc}", + "context.app.getPeerReservations().insert_or_assign(...)" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Checked for presence (optional)", + "Checked for string type", + "Extracted as string" + ], + "validated_at": [ + "params.isMember(jss::description) && params[jss::description].isString()" + ] + } + ], + "description": "Implements the doPeerReservationsAdd RPC handler, which adds a peer reservation by validating and parsing a public key and optional description from the RPC context parameters, and returns the result as JSON.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::public_key) at doPeerReservationsAdd", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "params.isMember(jss::public_key) validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::public_key].isString() at doPeerReservationsAdd", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "params[jss::public_key].isString() validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "public_key", + "type", + "validation", + "check" + ], + "evidence": "params[jss::public_key].isString() at doPeerReservationsAdd", + "issue_pattern": "Missing type validation for public_key", + "why_false_positive": "params[jss::public_key].isString() validates public_key type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "description", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::description].isString() at doPeerReservationsAdd", + "issue_pattern": "Missing empty string validation for description", + "why_false_positive": "params[jss::description].isString() validates description for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "description", + "type", + "validation", + "check" + ], + "evidence": "params[jss::description].isString() at doPeerReservationsAdd", + "issue_pattern": "Missing type validation for description", + "why_false_positive": "params[jss::description].isString() validates description type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58(TokenType::NodePublic, params[jss::public_key].asString()) at doPeerReservationsAdd", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "parseBase58(TokenType::NodePublic, params[jss::public_key].asString()) validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "public_key", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58(TokenType::NodePublic, params[jss::public_key].asString()) at doPeerReservationsAdd", + "issue_pattern": "Missing format validation for public_key", + "why_false_positive": "parseBase58(TokenType::NodePublic, params[jss::public_key].asString()) validates public_key format" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 12, + "name": "doPeerReservationsAdd" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage for this handler likely exists in integration or RPC-level test suites, such as 'peer_reservations_add' tests in the rippled or xrpld test directories (e.g., rpc_peer_reservations_test.cpp, admin_handler_test.cpp, or similar). These tests should cover: missing public_key, non-string public_key, invalid base58 public_key, optional/malformed description, and successful reservation. Gaps may exist if there are no tests for edge cases (e.g., extremely long descriptions, duplicate reservations, or malformed JSON input). No direct unit tests for parseBase58 or PeerReservation insertion are visible here; those may be tested elsewhere.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC + jss:: field tags + parseBase58 template", + "validation_layer": "entry_point (handler function doPeerReservationsAdd)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::public_key)", + "field": "public_key", + "location": "doPeerReservationsAdd", + "validated_by": "params.isMember(jss::public_key)", + "validates": [ + "Checks that 'public_key' field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::expected_field_error(jss::public_key, \"a string\")", + "field": "public_key", + "location": "doPeerReservationsAdd", + "validated_by": "params[jss::public_key].isString()", + "validates": [ + "Checks that 'public_key' is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::expected_field_error(jss::description, \"a string\")", + "field": "description", + "location": "doPeerReservationsAdd", + "validated_by": "params[jss::description].isString()", + "validates": [ + "If present, checks that 'description' is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcPUBLIC_MALFORMED)", + "field": "public_key", + "location": "doPeerReservationsAdd", + "validated_by": "parseBase58(TokenType::NodePublic, params[jss::public_key].asString())", + "validates": [ + "Checks that 'public_key' is a valid base58-encoded NodePublic key" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp.ai.md b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp.ai.md new file mode 100644 index 0000000000..06fb39102d --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsAdd.cpp.ai.md @@ -0,0 +1,31 @@ +# `PeerReservationsAdd.cpp` — `peer_reservations_add` RPC Handler + +## Role in the System + +This file implements `doPeerReservationsAdd`, the admin RPC handler that backs the `peer_reservations_add` command. Peer reservations give a specific XRPL node a guaranteed connection slot in the overlay network: when a peer's public key has a reservation, the overlay will hold a slot open for it even when the general inbound connection pool is full. This handler is the write path — it inserts or replaces a reservation in the live `PeerReservationTable` and persists the change to the relational database. + +The file lives under `src/xrpld/rpc/handlers/admin/peer/`, alongside `PeerReservationsDel.cpp` and `PeerReservationsList.cpp`, which form the complete CRUD surface for peer reservations. All three are admin-only commands. + +## Validation Pipeline + +`doPeerReservationsAdd` validates its two inputs — `public_key` (required) and `description` (optional) — through a strict, layered sequence before touching any application state: + +1. **Presence check** — `params.isMember(jss::public_key)` returns `missing_field_error` if the key is absent. +2. **Type check** — `params[jss::public_key].isString()` returns `expected_field_error` if the JSON value is not a string. +3. **Cryptographic parse** — `parseBase58(TokenType::NodePublic, ...)` attempts to decode the string as a NodePublic-type base58 key. Failure returns `rpcPUBLIC_MALFORMED`. + +A notable comment embedded in the source explicitly argues against pulling field extraction into a helper that returns `Json::Value` — because copying whole JSON objects just to propagate an error code is expensive and clutters the calling code. The comment acknowledges that exceptions would be cleaner for control flow but notes their runtime cost for error paths, and calls out that an error monad (essentially a typed `std::optional`) would be the right abstraction for this recurring pattern. The code doesn't implement that monad yet, but the comment serves as a design marker for future refactoring. + +A second comment explains why only base58 encoding is accepted rather than hex (even though `channel_verify` accepts both): per an explicit design preference, node key input is standardized to base58. This eliminates an entire class of ambiguous-format bugs at the cost of one decoder option. + +## Upsert Semantics and Response Shape + +Once the `PublicKey` is parsed, the handler calls `context.app.getPeerReservations().insert_or_assign(PeerReservation{nodeId, desc})`. This is an upsert: if no reservation exists for that node, one is created; if one already exists, it is replaced. Either way, the `PeerReservationTable` holds a mutex-protected `std::unordered_set` keyed by `nodeId`, and the underlying SQL store is updated atomically within the same lock. + +The return value from `insert_or_assign` is `std::optional` — the previous reservation if one was displaced, or `std::nullopt` for a fresh insert. The handler surfaces this directly in the JSON response under `jss::previous`, serialized via `PeerReservation::toJson()` which emits the node key in base58 and the description (omitting the description field entirely if it was empty). A fresh insert returns an empty JSON object `{}`. + +This pattern — returning the displaced value — is shared symmetrically with `doPeerReservationsDel`, which also returns a `jss::previous` field on successful deletion, providing callers idempotent-safe confirmation of what changed. + +## Relationship to `PeerReservationTable` + +`PeerReservationTable` (defined in `include/xrpl/core/PeerReservationTable.h`, implemented in `src/xrpld/overlay/detail/PeerReservationTable.cpp`) is a thread-safe in-memory registry backed by a SQL table. Its `insert_or_assign` method intentionally uses a remove-then-reinsert approach rather than in-place mutation because `std::unordered_set` keys are immutable — a recognized limitation documented in the source with a Stack Overflow reference. The comment notes this is acceptable because reservations are small, the table is expected to be short, and the method is rarely called. The handler itself has no concurrency concerns of its own — all thread safety is encapsulated within the table. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp.ai.json b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp.ai.json new file mode 100644 index 0000000000..f3d4827709 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp.ai.json @@ -0,0 +1,241 @@ +{ + "args": [ + { + "lineno": 12, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doPeerReservationsDel" + ], + "entry_point": "doPeerReservationsDel", + "purpose": "Handles the RPC request to delete a peer reservation by public key.", + "validation_points": [ + "params.isMember(jss::public_key)", + "params[jss::public_key].isString()", + "parseBase58(TokenType::NodePublic, params[jss::public_key].asString())" + ] + } + ], + "data_flows": [ + { + "field": "public_key", + "flow": [ + "context.params", + "params[jss::public_key]", + "parseBase58(..., params[jss::public_key].asString())", + "nodeId (PublicKey)", + "context.app.getPeerReservations().erase(nodeId)" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Checked for presence in params (isMember)", + "Checked for type string (isString)", + "Parsed from base58 string to PublicKey object (parseBase58)" + ], + "validated_at": [ + "params.isMember(jss::public_key)", + "params[jss::public_key].isString()", + "parseBase58(...)" + ] + } + ], + "description": "Implements the RPC handler for deleting a peer reservation by public key in the XRPL server.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::public_key) at doPeerReservationsDel", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "params.isMember(jss::public_key) validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::public_key].isString() at doPeerReservationsDel", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "params[jss::public_key].isString() validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "public_key", + "type", + "validation", + "check" + ], + "evidence": "params[jss::public_key].isString() at doPeerReservationsDel", + "issue_pattern": "Missing type validation for public_key", + "why_false_positive": "params[jss::public_key].isString() validates public_key type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58(TokenType::NodePublic, ...) at doPeerReservationsDel", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "parseBase58(TokenType::NodePublic, ...) validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "public_key", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58(TokenType::NodePublic, ...) at doPeerReservationsDel", + "issue_pattern": "Missing format validation for public_key", + "why_false_positive": "parseBase58(TokenType::NodePublic, ...) validates public_key format" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 10, + "name": "doPeerReservationsDel" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The function doPeerReservationsDel is likely tested in integration or functional tests for the admin/peer RPC endpoints, possibly in files like 'PeerReservations_test.cpp', 'PeerReservation_test.cpp', or broader admin RPC handler test suites. The validation paths (missing field, wrong type, malformed key) should be covered by tests that send requests with missing, non-string, or invalid public_key values. However, if tests only cover the happy path (valid public_key), negative cases (missing/invalid/malformed) may not be fully tested. There is no evidence in this file of direct unit tests or test hooks; coverage depends on the surrounding test infrastructure.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss::, RPC::, parseBase58)", + "validation_layer": "entry_point (handler function doPeerReservationsDel)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::public_key)", + "field": "public_key", + "location": "doPeerReservationsDel", + "validated_by": "params.isMember(jss::public_key)", + "validates": [ + "Checks that the 'public_key' field is present in the input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::expected_field_error(jss::public_key, \"a string\")", + "field": "public_key", + "location": "doPeerReservationsDel", + "validated_by": "params[jss::public_key].isString()", + "validates": [ + "Checks that the 'public_key' field is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcPUBLIC_MALFORMED)", + "field": "public_key", + "location": "doPeerReservationsDel", + "validated_by": "parseBase58(TokenType::NodePublic, ...)", + "validates": [ + "Checks that the 'public_key' string is a valid base58-encoded NodePublic key", + "Checks that the decoded value can be parsed as a PublicKey" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp.ai.md b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp.ai.md new file mode 100644 index 0000000000..7e20883fd7 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsDel.cpp.ai.md @@ -0,0 +1,34 @@ +# `PeerReservationsDel.cpp` — Admin RPC Handler for Removing Peer Reservations + +## Role in the System + +`doPeerReservationsDel` is the server-side handler for the `peer_reservations_del` admin RPC command. It allows a node operator to remove a reserved peer slot for a specific validator or trusted peer node identified by its NodePublic key. Peer reservations guarantee that certain nodes can always connect, bypassing the normal peer-count limits enforced by the overlay layer. This handler is one of three that collectively manage the reservation lifecycle: `PeerReservationsAdd` creates or updates a reservation, `PeerReservationsList` enumerates them, and this file deletes one. + +## Request Handling and Validation + +The handler performs three sequential validation steps on the `public_key` parameter before touching any application state: + +1. **Presence check** — `params.isMember(jss::public_key)` returns `RPC::missing_field_error` if the field is absent entirely. +2. **Type check** — `params[jss::public_key].isString()` returns `RPC::expected_field_error` if it is present but not a JSON string. +3. **Format check** — `parseBase58(TokenType::NodePublic, ...)` decodes the base58 string into a `PublicKey`; failure returns `rpcPUBLIC_MALFORMED`. + +The `TokenType::NodePublic` specifier ensures only node-identity keys are accepted, not account keys or other base58-encoded types that share the same character alphabet. If any step fails, the function returns early with a structured error object — the application state is never touched. + +This three-step pattern is identical to `doPeerReservationsAdd`, which the comment on line 19 acknowledges explicitly. Both handlers note that a hypothetical error-monad abstraction would reduce repetition here, but that design tradeoff was deliberately left in place in favour of the simpler, explicit approach. + +## Deletion and Return Value + +After validation, the handler delegates to `context.app.getPeerReservations().erase(nodeId)`, which is implemented in `PeerReservationTable::erase()` (`src/xrpld/overlay/detail/PeerReservationTable.cpp`). That function: + +- Acquires the table's internal mutex before any read or write, making the operation thread-safe with respect to concurrent peer connection events. +- Looks up the in-memory `std::unordered_set` for an entry matching `nodeId`. +- If found, captures the existing `PeerReservation`, erases it from the set, and calls `deletePeerReservation()` to remove the record from the persistent relational database — ensuring the reservation does not survive a restart. +- Returns `std::optional` — present if something was removed, empty if the key had no reservation. + +Back in the RPC handler, the response is an empty JSON object by default. If `erase()` returned a previous reservation, its `toJson()` output (containing `node` as the base58 public key and optionally `description`) is placed under the `previous` key in the response. This design lets callers determine whether they actually removed something or silently hit a no-op — the handler does not error on a missing key, making the delete operation idempotent. + +## Idempotency and Failure Modes + +A deliberate design choice is that deleting a non-existent reservation is not an error — it simply returns an empty object. This is appropriate for administrative tooling where operators may retry commands or run scripts that are not tracking server state precisely. + +The only failure modes are malformed input (the three validation paths above) and, implicitly, database errors — though `PeerReservationTable::erase()` does not surface database failures back to the RPC handler; the `deletePeerReservation` call is expected to succeed if the in-memory lookup succeeded. This reflects a pragmatic consistency model: the in-memory table is the source of truth for live connection decisions, and the database is the durability layer for restarts. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp.ai.json b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp.ai.json new file mode 100644 index 0000000000..c193a3f582 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp.ai.json @@ -0,0 +1,128 @@ +{ + "args": [ + { + "lineno": 8, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RPC::handleRequest (generic handler, not shown here)", + "doPeerReservationsList" + ], + "entry_point": "doPeerReservationsList", + "purpose": "Handles the 'peer_reservations_list' RPC command, returning a list of peer reservations as JSON.", + "validation_points": [ + "Template-based validation likely occurs in the generic RPC handler before doPeerReservationsList is called. No explicit validation in doPeerReservationsList itself." + ] + } + ], + "data_flows": [ + { + "field": "reservations", + "flow": [ + "context (RPC::JsonContext)", + "context.app (Application instance)", + "context.app.getPeerReservations() (PeerReservations manager)", + "list() (returns vector/list of Reservation objects)", + "for loop over reservations", + "reservation.toJson() (serializes each reservation)", + "jaReservations.append(...) (adds to JSON array)", + "result[jss::reservations] (final JSON output)" + ], + "origin": "context.app.getPeerReservations().list()", + "transformations": [ + "Reservation objects are serialized to JSON via toJson()" + ], + "validated_at": "No explicit validation in this function; assumes upstream validation in RPC handler" + } + ], + "description": "Implements a function to list peer reservations in the XRPL application, returning them as a JSON array.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 7, + "name": "doPeerReservationsList" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test coverage in this file. Testing is likely handled at a higher level (integration or RPC handler tests). Template-based validation is assumed to be tested via generic RPC handler tests, not specifically for this handler. There is no field-level or input validation in doPeerReservationsList, so edge cases (e.g., malformed reservations) are not tested here. Test files to check: rpc/PeerReservations_test.cpp, rpc/PeerReservationsList_test.cpp, or generic RPC handler tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld/rpc + jss:: (JSON field naming)", + "notes": [ + "No explicit input or field validation is performed in doPeerReservationsList.", + "The function does not accept or process user input; it only enumerates internal state.", + "jss:: is used for JSON field naming, not validation.", + "Any validation of PeerReservation objects or their toJson() output would occur elsewhere (e.g., in PeerReservations::list() or PeerReservation::toJson()), but is not visible in this code." + ], + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp.ai.md b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp.ai.md new file mode 100644 index 0000000000..58c63da2ee --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/PeerReservationsList.cpp.ai.md @@ -0,0 +1,34 @@ +# `PeerReservationsList.cpp` — `peer_reservations_list` RPC Handler + +This file implements `doPeerReservationsList`, the read-only member of the three-part peer reservation management family alongside `PeerReservationsAdd.cpp` and `PeerReservationsDel.cpp`. It handles the `peer_reservations_list` admin RPC command, returning a snapshot of all currently configured peer reservations as a JSON array. + +## Role in the System + +Peer reservations are a network-management feature that allows a node operator to guarantee connection slots for specific trusted peers, identified by their base58-encoded `NodePublic` keys. The three handlers — add, delete, and list — form the complete CRUD surface for this feature. The list handler exists so operators can inspect the current state of the table through the same RPC interface used to mutate it, rather than having to query the underlying SQLite database directly. + +The handler is registered in `Handler.cpp` with `Role::ADMIN` and `NO_CONDITION`: + +```cpp +{"peer_reservations_list", byRef(&doPeerReservationsList), Role::ADMIN, NO_CONDITION}, +``` + +The `ADMIN` role means the handler is only accessible over the admin port (typically `127.0.0.1`), not the public-facing JSON-RPC or WebSocket interfaces. `NO_CONDITION` indicates no ledger state is required — the data lives entirely in an in-memory table backed by persistent storage, independent of ledger availability. + +## Implementation + +`doPeerReservationsList` accepts no input parameters. Unlike its siblings, it requires no validation pass because there is nothing to validate — the function is a pure enumeration of internal state. All three handlers follow the standard `RPC::JsonContext` contract and return a `Json::Value`, but only this one ignores `context.params` entirely. + +The work is delegated in a single chain: `context.app.getPeerReservations().list()`. The `getPeerReservations()` call returns the `PeerReservationTable` singleton managed by the application, and `list()` performs a mutex-guarded copy of the internal `std::unordered_set`, then sorts the copy before returning it. This design is worth noting: the handler receives a stable, sorted `std::vector` snapshot rather than a live view of the concurrent data structure. Sorting on each `list()` call ensures deterministic output even though the underlying container is unordered, without requiring the table to maintain a sorted invariant during writes. + +Serialization is pushed back to the data layer. Each `PeerReservation` object exposes a `toJson()` method that converts the internal `PublicKey nodeId` back to base58 via `toBase58(TokenType::NodePublic, nodeId)` and conditionally includes `jss::description` only when non-empty. The handler has no knowledge of what fields a reservation contains; it only iterates and appends: + +```cpp +for (auto const& reservation : reservations) + jaReservations.append(reservation.toJson()); +``` + +The result is a JSON object with a single `"reservations"` array key (`jss::reservations`). An empty table produces `{"reservations": []}` rather than an error, consistent with how the `PeerReservationTable::load()` function always succeeds (returning `true`) even when no rows exist in the database. + +## Design Observations + +The handler is intentionally thin. Compared to `doPeerReservationsAdd`, which validates field presence and type, parses base58, and returns the previous reservation if overwritten, the list handler has nothing comparable to do. This asymmetry is natural: write operations must guard against malformed input and return diagnostic information, while a read on internal state can assume correctness. The pattern across all three handlers — delegate to `getPeerReservations()`, serialize with `toJson()` — keeps the RPC layer free of business logic. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/Peers.cpp.ai.json b/src/xrpld/rpc/handlers/admin/peer/Peers.cpp.ai.json new file mode 100644 index 0000000000..d505ed5e88 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/Peers.cpp.ai.json @@ -0,0 +1,396 @@ +{ + "args": [ + { + "lineno": 12, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doPeers" + ], + "entry_point": "doPeers", + "purpose": "Handles the /peers RPC endpoint, returning peer and cluster information, with legacy support for API version 1.", + "validation_points": [ + "context.apiVersion validated by comparison (if (context.apiVersion == 1))", + "p[jss::track] validated by isMember and asString (if (p.isMember(jss::track)))", + "node.name() validated by empty() check (if (!node.name().empty()))", + "node.getLoadFee() validated by comparison to ref and 0 (if ((node.getLoadFee() != ref) && (node.getLoadFee() != 0)))", + "node.getReportTime() validated by comparison to NetClock::time_point{} (if (node.getReportTime() != NetClock::time_point{}))" + ] + } + ], + "data_flows": [ + { + "field": "context.apiVersion", + "flow": [ + "context.apiVersion", + "if (context.apiVersion == 1)", + "legacy peer field transformation" + ], + "origin": "RPC::JsonContext (input to doPeers)", + "transformations": [ + "Compared to integer 1 to determine legacy behavior" + ], + "validated_at": "doPeers (if (context.apiVersion == 1))" + }, + { + "field": "p[jss::track]", + "flow": [ + "context.app.getOverlay().json()", + "for (auto& p : jvResult[jss::peers])", + "if (p.isMember(jss::track))", + "auto const s = p[jss::track].asString()", + "if (s == 'diverged' || s == 'unknown')", + "p['sanity'] = ... (transformation)" + ], + "origin": "Each peer object in jvResult[jss::peers] (from context.app.getOverlay().json())", + "transformations": [ + "Checked for presence (isMember)", + "Converted to string (asString)", + "Mapped to new field 'sanity' if value matches" + ], + "validated_at": "doPeers (if (p.isMember(jss::track)))" + }, + { + "field": "node.name()", + "flow": [ + "context.app.getCluster().for_each(...)", + "node.name()", + "if (!node.name().empty())", + "json[jss::tag] = node.name()" + ], + "origin": "ClusterNode::name() in for_each over cluster nodes", + "transformations": [ + "Checked for non-empty string before assignment" + ], + "validated_at": "doPeers (if (!node.name().empty()))" + }, + { + "field": "node.getLoadFee()", + "flow": [ + "context.app.getCluster().for_each(...)", + "node.getLoadFee()", + "if ((node.getLoadFee() != ref) && (node.getLoadFee() != 0))", + "json[jss::fee] = static_cast(node.getLoadFee()) / ref" + ], + "origin": "ClusterNode::getLoadFee() in for_each over cluster nodes", + "transformations": [ + "Compared to reference fee and zero before division and assignment" + ], + "validated_at": "doPeers (if ((node.getLoadFee() != ref) && (node.getLoadFee() != 0)))" + }, + { + "field": "node.getReportTime()", + "flow": [ + "context.app.getCluster().for_each(...)", + "node.getReportTime()", + "if (node.getReportTime() != NetClock::time_point{})", + "json[jss::age] = (node.getReportTime() >= now) ? 0 : (now - node.getReportTime()).count()" + ], + "origin": "ClusterNode::getReportTime() in for_each over cluster nodes", + "transformations": [ + "Checked for non-default time_point before calculating age" + ], + "validated_at": "doPeers (if (node.getReportTime() != NetClock::time_point{}))" + } + ], + "description": "Implements the doPeers RPC handler, which returns information about connected peers and cluster nodes in the XRPL network, including legacy support for API version 1.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Json::Value fields via isMember/type checks", + "validation", + "missing", + "check" + ], + "evidence": "Field Json::Value fields via isMember/type checks validated by Json::Value, jss:: (JSON field tags), implicit C++ type system", + "issue_pattern": "Missing validation for Json::Value fields via isMember/type checks", + "why_false_positive": "Json::Value, jss:: (JSON field tags), implicit C++ type system validates Json::Value fields via isMember/type checks automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "apiVersion (by type and value)", + "validation", + "missing", + "check" + ], + "evidence": "Field apiVersion (by type and value) validated by Json::Value, jss:: (JSON field tags), implicit C++ type system", + "issue_pattern": "Missing validation for apiVersion (by type and value)", + "why_false_positive": "Json::Value, jss:: (JSON field tags), implicit C++ type system validates apiVersion (by type and value) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ClusterNode fields (name, identity, load fee, report time)", + "validation", + "missing", + "check" + ], + "evidence": "Field ClusterNode fields (name, identity, load fee, report time) validated by Json::Value, jss:: (JSON field tags), implicit C++ type system", + "issue_pattern": "Missing validation for ClusterNode fields (name, identity, load fee, report time)", + "why_false_positive": "Json::Value, jss:: (JSON field tags), implicit C++ type system validates ClusterNode fields (name, identity, load fee, report time) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "context.apiVersion", + "empty", + "string", + "validation" + ], + "evidence": "implicit type (int/uint) and comparison at doPeers", + "issue_pattern": "Missing empty string validation for context.apiVersion", + "why_false_positive": "implicit type (int/uint) and comparison validates context.apiVersion for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "p[jss::track]", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::isMember, Json::Value::asString at doPeers", + "issue_pattern": "Missing empty string validation for p[jss::track]", + "why_false_positive": "Json::Value::isMember, Json::Value::asString validates p[jss::track] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "node.name()", + "empty", + "string", + "validation" + ], + "evidence": "std::string::empty() at doPeers (lambda in for_each)", + "issue_pattern": "Missing empty string validation for node.name()", + "why_false_positive": "std::string::empty() validates node.name() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "node.getLoadFee()", + "empty", + "string", + "validation" + ], + "evidence": "comparison to ref and 0 at doPeers (lambda in for_each)", + "issue_pattern": "Missing empty string validation for node.getLoadFee()", + "why_false_positive": "comparison to ref and 0 validates node.getLoadFee() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "node.getReportTime()", + "empty", + "string", + "validation" + ], + "evidence": "comparison to NetClock::time_point{} at doPeers (lambda in for_each)", + "issue_pattern": "Missing empty string validation for node.getReportTime()", + "why_false_positive": "comparison to NetClock::time_point{} validates node.getReportTime() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "node.identity() == self", + "empty", + "string", + "validation" + ], + "evidence": "equality comparison at doPeers (lambda in for_each)", + "issue_pattern": "Missing empty string validation for node.identity() == self", + "why_false_positive": "equality comparison validates node.identity() == self for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/peer/Peers.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 11, + "name": "doPeers" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The doPeers handler is typically tested via RPC integration tests that exercise the /peers endpoint. Likely test files include rpc-peers-test.cpp, cluster_test.cpp, and overlay_test.cpp. These tests would cover normal and legacy (apiVersion == 1) cases, peer field transformations, and cluster node reporting. However, edge cases such as malformed peer objects, missing fields, or unusual cluster node states (e.g., empty name, zero/invalid fees, default report times) may not be fully covered unless explicitly tested. There is no evidence of unit tests for the validation logic itself; most coverage is likely through end-to-end or integration tests.", + "validation_architecture": { + "auto_validated_fields": [ + "Json::Value fields via isMember/type checks", + "apiVersion (by type and value)", + "ClusterNode fields (name, identity, load fee, report time)" + ], + "framework": "Json::Value, jss:: (JSON field tags), implicit C++ type system", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "none (branching only)", + "field": "context.apiVersion", + "location": "doPeers", + "validated_by": "implicit type (int/uint) and comparison", + "validates": [ + "apiVersion is compared to 1 to determine legacy logic" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "none (branching only)", + "field": "p[jss::track]", + "location": "doPeers", + "validated_by": "Json::Value::isMember, Json::Value::asString", + "validates": [ + "Checks if 'track' field exists in peer object", + "Converts 'track' field to string before comparison" + ], + "validation_type": "existence|type" + }, + { + "confidence": 0.8, + "error_thrown": "none (branching only)", + "field": "node.name()", + "location": "doPeers (lambda in for_each)", + "validated_by": "std::string::empty()", + "validates": [ + "Checks if cluster node name is non-empty before adding to JSON" + ], + "validation_type": "existence|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (branching only)", + "field": "node.getLoadFee()", + "location": "doPeers (lambda in for_each)", + "validated_by": "comparison to ref and 0", + "validates": [ + "Only adds fee field if node.getLoadFee() != ref and != 0" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (branching only)", + "field": "node.getReportTime()", + "location": "doPeers (lambda in for_each)", + "validated_by": "comparison to NetClock::time_point{}", + "validates": [ + "Only adds age field if report time is not default constructed" + ], + "validation_type": "existence|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (branching only)", + "field": "node.identity() == self", + "location": "doPeers (lambda in for_each)", + "validated_by": "equality comparison", + "validates": [ + "Skips adding self node to cluster list" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/peer/Peers.cpp.ai.md b/src/xrpld/rpc/handlers/admin/peer/Peers.cpp.ai.md new file mode 100644 index 0000000000..8ff9a2842d --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/peer/Peers.cpp.ai.md @@ -0,0 +1,28 @@ +# `src/xrpld/rpc/handlers/admin/peer/Peers.cpp` + +## Role and Purpose + +`Peers.cpp` implements `doPeers`, the admin RPC handler that responds to the `peers` command. Its job is to aggregate two distinct views of the network into a single JSON response: the real-time overlay snapshot (every TCP-connected peer) and the cluster status table (trusted cluster members and their self-reported load). It is intentionally thin — all the heavy data lives in the overlay and cluster subsystems; this handler merely assembles and formats it. + +## Overlay Peer Data + +The bulk of the response comes from `context.app.getOverlay().json()`, which iterates every active `PeerImp` connection and serialises its current state. Each peer entry includes fields like `complete_ledgers` and, critically, `track`. The `track` field is populated inside `PeerImp::json()` by reading the atomic `tracking_` member, which reflects how well that peer's ledger history aligns with the local node: `"diverged"` means the peer is on a different chain, `"unknown"` means consensus has not yet been established, and the absence of the field means the peer has converged. + +## API Version 1 Legacy Compatibility + +Modern XRPL API clients use the `track` field directly. However, API version 1 clients — the pre-2.0 interface — expected a field named `sanity` with values `"insane"` (for diverged) and `"unknown"`. The handler post-processes the overlay JSON in-place when `context.apiVersion == 1`, injecting a `"sanity"` key into each peer object that has a `track` field. The mapping is `"diverged"` → `"insane"` and `"unknown"` → `"unknown"`. Peers in the `"converged"` state produce no `track` entry at all, so they also receive no `sanity` annotation — correct for both API versions, as converged peers were simply absent from older tooling's concern. + +This pattern — emit canonical output, then patch for legacy consumers before returning — keeps the overlay serialisation layer clean and avoids threading version-awareness through `PeerImp::json()`. + +## Cluster Node Reporting + +The second section of the response aggregates the configured cluster. `Cluster::for_each()` takes a lock on the internal `std::set` and invokes the provided lambda once per node. The handler applies several defensive guards: + +- **Self-exclusion**: `node.identity() == self` skips the local node. Because the local node is normally a member of its own cluster configuration, omitting this check would cause the node to report itself among its cluster peers — misleading for operators and inconsistent with how cluster membership is used elsewhere. +- **Optional name tag**: `node.name()` is only emitted as `jss::tag` when non-empty. Cluster entries in `rippled.cfg` may or may not carry a human-readable comment, and the response omits the field rather than emitting an empty string. +- **Fee ratio**: The load fee is expressed as `node.getLoadFee() / ref` — a floating-point multiplier relative to the network's `loadBase`. Only emitted when it differs from the reference and is non-zero, which avoids polluting the output with a `1.0` ratio for every normal node. A value greater than `1.0` signals that a cluster peer is under load and applying a fee premium. +- **Age in seconds**: `node.getReportTime()` holds the `NetClock::time_point` at which the cluster node last broadcast its status. If that time is the zero-constructed default (never reported), the `age` field is suppressed entirely. Otherwise, age is computed as `(now - reportTime).count()`, clamped to zero for the unlikely case where `reportTime >= now` (clock skew guard). + +## Relationships + +`doPeers` has no local state or concurrency concerns of its own. It delegates to three subsystems: `Overlay` for live peer data, `Cluster` for trusted-peer metadata, and `TimeKeeper` for the current network clock. The `LoadFeeTrack::getLoadBase()` call provides the denominator for fee normalisation. The handler itself is stateless — it reads application-wide singletons through the `RPC::JsonContext` handle and returns a `Json::Value` by value, which is the standard idiom for all admin RPC handlers in this directory. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp.ai.json b/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp.ai.json new file mode 100644 index 0000000000..02af8c8810 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp.ai.json @@ -0,0 +1,197 @@ +{ + "args": [ + { + "lineno": 12, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doLedgerAccept" + ], + "entry_point": "doLedgerAccept", + "purpose": "Handles the 'ledger_accept' RPC command, which forces ledger closure in standalone mode.", + "validation_points": [ + "if (!context.app.config().standalone())" + ] + } + ], + "data_flows": [ + { + "field": "standalone (config flag)", + "flow": [ + "context (RPC::JsonContext)", + "context.app (Application)", + "context.app.config() (Config)", + "standalone()" + ], + "origin": "context.app.config().standalone()", + "transformations": [ + "Boolean check: determines if server is in standalone mode" + ], + "validated_at": "doLedgerAccept: if (!context.app.config().standalone())" + }, + { + "field": "jvResult[jss::error]", + "flow": [ + "doLedgerAccept", + "Set to 'notStandAlone' if not in standalone mode", + "Returned as RPC response" + ], + "origin": "Local variable in doLedgerAccept", + "transformations": [ + "Assigned error string if validation fails" + ], + "validated_at": "doLedgerAccept: after standalone() check" + }, + { + "field": "jvResult[jss::ledger_current_index]", + "flow": [ + "doLedgerAccept", + "context.ledgerMaster.getCurrentLedgerIndex()", + "Assigned to jvResult", + "Returned as RPC response" + ], + "origin": "context.ledgerMaster.getCurrentLedgerIndex()", + "transformations": [ + "No transformation; direct assignment" + ], + "validated_at": "Only set if standalone() validation passes" + } + ], + "description": "Implements the doLedgerAccept RPC handler, which accepts a new ledger if the application is running in standalone mode.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "standalone mode (application config)", + "empty", + "string", + "validation" + ], + "evidence": "context.app.config().standalone() at doLedgerAccept", + "issue_pattern": "Missing empty string validation for standalone mode (application config)", + "why_false_positive": "context.app.config().standalone() validates standalone mode (application config) for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 11, + "name": "doLedgerAccept" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is typically tested via RPC integration tests that exercise admin/server_control endpoints in standalone mode. Likely test files: 'rpc/ServerControl_test.cpp', 'rpc/LedgerAccept_test.cpp', or similar. Key validation (standalone mode check) should be covered by tests that attempt 'ledger_accept' in both standalone and networked modes. Gaps may exist if tests do not explicitly check error response for non-standalone mode or do not verify correct ledger index returned after acceptance.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld/rpc (jss:: for JSON fields, but not used for input validation here)", + "notes": "No explicit input or field validation is performed on user-supplied fields in this function. The only validation is a business logic check on server mode. All other parameters are assumed to be validated upstream or are not present.", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "jvResult[jss::error] = \"notStandAlone\" (returns error in JSON result)", + "field": "standalone mode (application config)", + "location": "doLedgerAccept", + "validated_by": "context.app.config().standalone()", + "validates": [ + "Checks if the server is running in standalone mode before allowing ledger accept" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp.ai.md b/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp.ai.md new file mode 100644 index 0000000000..ca8dbb21b4 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/server_control/LedgerAccept.cpp.ai.md @@ -0,0 +1,43 @@ +# `LedgerAccept.cpp` — Admin RPC Handler for Forced Ledger Closure + +## Role and Purpose + +`LedgerAccept.cpp` implements the `ledger_accept` admin RPC command, which forces the node to close and advance to the next ledger without waiting for peers or the normal consensus timer. This capability exists exclusively for standalone mode — the operating configuration used by developers writing integration tests, by the `jtx` test harness, and by anyone running a local node that is intentionally isolated from the live network. In that context there are no validator peers to coordinate with, so ledger progression has to be triggered on demand rather than driven by consensus rounds. + +The file is one of two handlers in the `admin/server_control/` directory; its sibling `Stop.cpp` handles the `stop` command. Both are thin entry points that do very little work themselves and delegate to deeper application services. + +## What `doLedgerAccept` Does + +The handler's logic fits in under twenty lines. After receiving the `RPC::JsonContext` it first checks `context.app.config().standalone()`. If the node is **not** in standalone mode the function returns immediately with `{"error": "notStandAlone"}` — no locking, no side effects. This early rejection is the primary guard against accidental use on a live validator. + +If standalone mode is confirmed, the handler acquires the application-wide master mutex via `std::unique_lock` before doing anything else: + +```cpp +std::unique_lock const lock{context.app.getMasterMutex()}; +context.netOps.acceptLedger(); +jvResult[jss::ledger_current_index] = context.ledgerMaster.getCurrentLedgerIndex(); +``` + +With the lock held it calls `context.netOps.acceptLedger()` and then reads back the new current ledger index to return in the response. + +## Why the Master Mutex Matters Here + +`Application::getMasterMutex()` returns a `std::recursive_mutex` that is the single serialisation point for all mutations to the open ledger and to the consensus engine state. The same lock is acquired inside `NetworkOPsImp::processHeartbeatTimer()` and inside `RCLConsensus` when it builds a new open ledger. By taking it before calling `acceptLedger()` the RPC handler ensures it is not racing with the heartbeat timer or any other ledger-state mutation that might be in flight. The RAII `std::unique_lock` guarantees release even if `acceptLedger()` throws. + +## What `acceptLedger()` Actually Does + +`NetworkOPsImp::acceptLedger()` (in `NetworkOPs.cpp`) contains an `XRPL_ASSERT` that the node is in standalone mode, then drives a synthetic consensus round: + +```cpp +beginConsensus(m_ledgerMaster.getClosedLedger()->header().hash, {}); +mConsensus.simulate(registry_.get().getTimeKeeper().closeTime(), consensusDelay); +return m_ledgerMaster.getCurrentLedger()->header().seq; +``` + +It bypasses the normal peer-vote protocol entirely by calling `mConsensus.simulate()` — a code path that exists solely to move the consensus state machine forward without real peer participation. The result is a fully validated new ledger sequence as if consensus had concluded normally, including fee adjustments and any pending transactions in the open ledger. + +## Response and Usage + +On success the JSON response contains a single field `ledger_current_index` holding the sequence number of the ledger that is now open for new transactions (one past the one just closed). Test frameworks use this value to synchronize assertions — they issue transactions, call `ledger_accept`, and then query state at the returned index with confidence the transactions have been processed. + +The `doLedgerAccept` signature itself accepts no parameters from the caller's JSON; there is no input to validate beyond the mode check, which explains why the validation architecture metadata notes that all validation here is purely business-logic (mode enforcement) rather than input sanitization. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp.ai.json b/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp.ai.json new file mode 100644 index 0000000000..c2e1986347 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp.ai.json @@ -0,0 +1,77 @@ +{ + "args": [ + { + "lineno": 12, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RPC::Handler (dispatches based on RPC command)", + "doStop" + ], + "entry_point": "doStop", + "purpose": "Handles the 'stop' RPC command, signaling the server to stop.", + "validation_points": [ + "RPC::Handler (likely validates permissions and command legitimacy before calling doStop)" + ] + } + ], + "data_flows": [ + { + "field": "context", + "flow": [ + "RPC request arrives (JSON-RPC or WebSocket)", + "RPC::JsonContext is constructed with request data and application context", + "doStop receives context", + "context.app.signalStop(\"RPC\") is called", + "RPC::makeObjectValue(systemName() + \" server stopping\") is returned" + ], + "origin": "RPC::JsonContext (constructed by RPC framework from incoming HTTP/WebSocket request)", + "transformations": [ + "No transformation of input data; context is passed as-is", + "No user input is parsed or used in doStop" + ], + "validated_at": "Validation (such as admin permission checks) is expected to occur before doStop is called, likely in the RPC::Handler dispatch logic" + } + ], + "description": "Implements the doStop RPC handler, which signals the application to stop and returns a JSON message indicating the server is stopping.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 11, + "name": "doStop" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 6, + "name": "RPC" + } + ], + "test_coverage_notes": "The doStop function itself contains no input validation or branching logic; it simply signals the application to stop and returns a static message. Validation (such as admin checks) is expected to occur in the RPC handler dispatch code, not shown here. Test coverage for doStop would likely be indirect, via integration or functional tests that issue the 'stop' RPC command and verify server shutdown behavior. Unit tests for doStop are unlikely or unnecessary due to its trivial logic. Gaps: No direct validation or error handling is present in doStop; if the RPC handler dispatch code does not properly restrict access, unauthorized users could potentially trigger a server stop.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected", + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp.ai.md b/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp.ai.md new file mode 100644 index 0000000000..212060edd6 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/server_control/Stop.cpp.ai.md @@ -0,0 +1,40 @@ +# `Stop.cpp` — RPC Handler for Graceful Server Shutdown + +This file implements `doStop`, the handler for the `stop` RPC command. Its entire body is two lines: signal the application to stop, then return a confirmation message. The simplicity is deliberate — every meaningful concern is separated into the surrounding infrastructure. + +## Role in the Dispatch System + +The handler is registered in `src/xrpld/rpc/detail/Handler.cpp` as: + +```cpp +{"stop", byRef(&doStop), Role::ADMIN, NO_CONDITION}, +``` + +Two properties of this registration are significant. First, `Role::ADMIN` means the RPC dispatch layer rejects any caller that does not hold admin credentials before `doStop` is ever reached — there is no permission check inside the function itself because the framework enforces it unconditionally at a higher level. Second, `NO_CONDITION` means the handler does not require the server to be synced to the network or hold a current ledger; a node that is disconnected or still starting up can still be stopped via RPC, which is exactly the right behavior for an administrative shutdown command. + +## What `signalStop("RPC")` Actually Does + +`doStop` delegates entirely to `Application::signalStop()`, whose implementation in `ApplicationImp` is: + +```cpp +void ApplicationImp::signalStop(std::string msg) +{ + if (!isTimeToStop.test_and_set(std::memory_order_acquire)) + { + JLOG(m_journal.warn()) << "Server stopping: " << msg; + isTimeToStop.notify_all(); + } +} +``` + +`isTimeToStop` is a C++20 `std::atomic_flag`. The `test_and_set` call is a lock-free atomic that returns `false` only on the first invocation, ensuring the stop message is logged exactly once regardless of concurrent callers. After setting the flag, `notify_all()` unblocks the `run()` loop, which is suspended on `isTimeToStop.wait(false, std::memory_order_relaxed)`. The string argument `"RPC"` becomes the reason recorded in the log — operators can distinguish an administrative RPC shutdown from a SIGTERM (`"Signal: 15"`), a DB space exhaustion (`"Out of transaction DB space"`), or a PerfLog timeout. + +## Response-Before-Shutdown Ordering + +`doStop` returns the confirmation message immediately after calling `signalStop()`, before the server actually begins teardown. This ordering is intentional: the `run()` loop wakes up and starts unwinding only after the current RPC dispatch returns and the response is queued for delivery. If the handler waited for teardown before returning, the response would never reach the caller because the HTTP/WebSocket layer would already be shutting down. The client therefore receives `"rippled server stopping"` reliably, while the actual shutdown proceeds asynchronously. + +## Relationship to Other Files + +`doStop` is declared in `src/xrpld/rpc/handlers/Handlers.h` alongside every other RPC handler function. The `systemName()` call used to compose the response message returns the server's compiled-in product name (e.g., `"rippled"`), making the response self-identifying if the binary is rebranded. The `RPC::makeObjectValue` helper from `Handler.h` wraps the string into a `{"message": "..."}` JSON object, consistent with the response envelope used across all scalar-valued handlers. + +The file sits in `handlers/admin/server_control/`, a directory that currently also holds `LedgerAccept.cpp` — the two together represent the only commands that make irreversible changes to the running server's state rather than reading it. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp.ai.json b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp.ai.json new file mode 100644 index 0000000000..47434628d0 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp.ai.json @@ -0,0 +1,403 @@ +{ + "args": [ + { + "lineno": 17, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doChannelAuthorize", + "RPC::keypairForSignature", + "sign" + ], + "entry_point": "doChannelAuthorize", + "purpose": "Handles the channel authorization signing request, validates input, derives keypair, signs message.", + "validation_points": [ + "doChannelAuthorize: context.role and context.app.config().canSign() (server signing support)", + "doChannelAuthorize: params.isMember(jss::channel_id) (channel_id presence)", + "doChannelAuthorize: params.isMember(jss::amount) (amount presence)", + "doChannelAuthorize: params.isMember(jss::key_type) and params.isMember(jss::secret) (key/secret presence)", + "RPC::keypairForSignature: keypair/secret/key_type validation", + "doChannelAuthorize: channelId.parseHex (channel_id format)", + "doChannelAuthorize: to_uint64(params[jss::amount].asString()) (amount format)" + ] + } + ], + "data_flows": [ + { + "field": "channel_id", + "flow": [ + "context.params[jss::channel_id]", + "doChannelAuthorize: params.isMember(jss::channel_id) (presence check)", + "doChannelAuthorize: channelId.parseHex(params[jss::channel_id].asString()) (format check)", + "serializePayChanAuthorization(msg, channelId, ...)" + ], + "origin": "context.params[jss::channel_id] (JSON RPC input)", + "transformations": [ + "Checked for presence", + "Parsed from hex string to uint256" + ], + "validated_at": "doChannelAuthorize (presence and format)" + }, + { + "field": "amount", + "flow": [ + "context.params[jss::amount]", + "doChannelAuthorize: params.isMember(jss::amount) (presence check)", + "doChannelAuthorize: to_uint64(params[jss::amount].asString()) (format check)", + "serializePayChanAuthorization(msg, ..., XRPAmount(drops))" + ], + "origin": "context.params[jss::amount] (JSON RPC input)", + "transformations": [ + "Checked for presence", + "Parsed from string to uint64_t" + ], + "validated_at": "doChannelAuthorize (presence and format)" + }, + { + "field": "secret / key_type", + "flow": [ + "context.params[jss::secret], context.params[jss::key_type]", + "doChannelAuthorize: params.isMember(jss::key_type) and params.isMember(jss::secret) (presence check)", + "RPC::keypairForSignature(params, result, context.apiVersion) (validation and keypair derivation)" + ], + "origin": "context.params[jss::secret], context.params[jss::key_type] (JSON RPC input)", + "transformations": [ + "Checked for presence", + "Validated and used to derive keypair" + ], + "validated_at": "doChannelAuthorize (presence), RPC::keypairForSignature (validation)" + }, + { + "field": "keypair (PublicKey, SecretKey)", + "flow": [ + "RPC::keypairForSignature(params, result, context.apiVersion)", + "doChannelAuthorize: keyPair assignment", + "sign(pk, sk, msg.slice())" + ], + "origin": "Derived from secret/key_type via RPC::keypairForSignature", + "transformations": [ + "Derived from input", + "Used for signing" + ], + "validated_at": "RPC::keypairForSignature" + }, + { + "field": "signature", + "flow": [ + "sign(pk, sk, msg.slice())", + "result[jss::signature] = strHex(buf)", + "Returned in JSON result" + ], + "origin": "Result of sign(pk, sk, msg.slice())", + "transformations": [ + "Binary signature encoded as hex string" + ], + "validated_at": "N/A (output only)" + } + ], + "description": "Implements the doChannelAuthorize RPC handler for authorizing XRP payment channels by signing a claim with a provided secret key.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "role / server signing support", + "empty", + "string", + "validation" + ], + "evidence": "context.role and context.app.config().canSign() at doChannelAuthorize (first if statement)", + "issue_pattern": "Missing empty string validation for role / server signing support", + "why_false_positive": "context.role and context.app.config().canSign() validates role / server signing support for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "channel_id", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::channel_id) at doChannelAuthorize (for loop over required fields)", + "issue_pattern": "Missing empty string validation for channel_id", + "why_false_positive": "params.isMember(jss::channel_id) validates channel_id for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::amount) at doChannelAuthorize (for loop over required fields)", + "issue_pattern": "Missing empty string validation for amount", + "why_false_positive": "params.isMember(jss::amount) validates amount for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "secret / key_type", + "empty", + "string", + "validation" + ], + "evidence": "!params.isMember(jss::key_type) && !params.isMember(jss::secret) at doChannelAuthorize (after required fields check)", + "issue_pattern": "Missing empty string validation for secret / key_type", + "why_false_positive": "!params.isMember(jss::key_type) && !params.isMember(jss::secret) validates secret / key_type for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "keypair (secret, key_type, etc.)", + "empty", + "string", + "validation" + ], + "evidence": "RPC::keypairForSignature at doChannelAuthorize (keypairForSignature call)", + "issue_pattern": "Missing empty string validation for keypair (secret, key_type, etc.)", + "why_false_positive": "RPC::keypairForSignature validates keypair (secret, key_type, etc.) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "channel_id (format)", + "empty", + "string", + "validation" + ], + "evidence": "channelId.parseHex(params[jss::channel_id].asString()) at doChannelAuthorize (after keypair validation)", + "issue_pattern": "Missing empty string validation for channel_id (format)", + "why_false_positive": "channelId.parseHex(params[jss::channel_id].asString()) validates channel_id (format) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "channel_id (format)", + "format", + "validation", + "invalid" + ], + "evidence": "channelId.parseHex(params[jss::channel_id].asString()) at doChannelAuthorize (after keypair validation)", + "issue_pattern": "Missing format validation for channel_id (format)", + "why_false_positive": "channelId.parseHex(params[jss::channel_id].asString()) validates channel_id (format) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amount (drops)", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::amount].isString() ? to_uint64(params[jss::amount].asString()) : std::nullopt at doChannelAuthorize (optDrops assignment)", + "issue_pattern": "Missing empty string validation for amount (drops)", + "why_false_positive": "params[jss::amount].isString() ? to_uint64(params[jss::amount].asString()) : std::nullopt validates amount (drops) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 17, + "name": "doChannelAuthorize" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in the RPC integration or unit test suites, e.g., 'test/rpc/ChannelAuthorize_test.cpp', 'test/rpc/Sign_test.cpp', or similar. Tests should cover: missing/invalid channel_id, missing/invalid amount, missing/invalid secret/key_type, invalid keypair, malformed channel_id/amount, and successful signing. Gaps may exist if edge cases (e.g., malformed but present fields, unsupported key types, or exception handling in sign()) are not explicitly tested. No test files are referenced in this code, so coverage must be verified in the test suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss::, RPC::, etc.)", + "validation_layer": "business_logic (handler entry point)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::make_error(rpcNOT_SUPPORTED, ...)", + "field": "role / server signing support", + "location": "doChannelAuthorize (first if statement)", + "validated_by": "context.role and context.app.config().canSign()", + "validates": [ + "Checks if user is ADMIN or server allows signing" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::channel_id)", + "field": "channel_id", + "location": "doChannelAuthorize (for loop over required fields)", + "validated_by": "params.isMember(jss::channel_id)", + "validates": [ + "Checks if channel_id is present in params" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::amount)", + "field": "amount", + "location": "doChannelAuthorize (for loop over required fields)", + "validated_by": "params.isMember(jss::amount)", + "validates": [ + "Checks if amount is present in params" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::secret)", + "field": "secret / key_type", + "location": "doChannelAuthorize (after required fields check)", + "validated_by": "!params.isMember(jss::key_type) && !params.isMember(jss::secret)", + "validates": [ + "Checks if secret is present if key_type is not specified" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "Error returned in result (RPC::contains_error(result))", + "field": "keypair (secret, key_type, etc.)", + "location": "doChannelAuthorize (keypairForSignature call)", + "validated_by": "RPC::keypairForSignature", + "validates": [ + "Validates secret/key_type format and compatibility", + "Checks key validity and type", + "Returns error if invalid" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcCHANNEL_MALFORMED)", + "field": "channel_id (format)", + "location": "doChannelAuthorize (after keypair validation)", + "validated_by": "channelId.parseHex(params[jss::channel_id].asString())", + "validates": [ + "Checks if channel_id is valid 256-bit hex" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcCHANNEL_AMT_MALFORMED)", + "field": "amount (drops)", + "location": "doChannelAuthorize (optDrops assignment)", + "validated_by": "params[jss::amount].isString() ? to_uint64(params[jss::amount].asString()) : std::nullopt", + "validates": [ + "Checks if amount is a string", + "Checks if string can be parsed as uint64" + ], + "validation_type": "type|format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp.ai.md b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp.ai.md new file mode 100644 index 0000000000..c660a86dc7 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/signing/ChannelAuthorize.cpp.ai.md @@ -0,0 +1,50 @@ +# `ChannelAuthorize.cpp` — Payment Channel Claim Signing Handler + +## Role in the System + +This file implements `doChannelAuthorize`, the RPC handler behind the `channel_authorize` API command. Its sole purpose is to produce a cryptographic signature that authorizes a payment channel claim for a specified amount. The resulting signature can then be handed to a channel recipient, who redeems it on-ledger via a `PaymentChannelClaim` transaction without needing the channel owner to be online at redemption time. The signing counterpart that validates these signatures is `doChannelVerify` in `ChannelVerify.cpp`, which uses the same `serializePayChanAuthorization` serialization to reconstruct the message and verify it. + +## Access Control Gate + +The handler opens with a two-condition authorization check: + +```cpp +if (context.role != Role::ADMIN && !context.app.config().canSign()) + return RPC::make_error(rpcNOT_SUPPORTED, "Signing is not supported by this server."); +``` + +This reflects the XRPL server's design philosophy around key material exposure. Signing with a secret key on a public-facing node is a security risk. The check allows signing only when the caller is authenticated as `ADMIN` (i.e., connecting over the admin interface), or when the node has been explicitly configured to allow signing (`[signing_support]` in the config). Non-admin callers on a default-configuration public node are rejected outright. + +## Validation Pipeline + +Input validation proceeds in a deliberate sequence that prioritizes early exit for cheap checks: + +1. **Required field presence** — `channel_id` and `amount` are checked in a loop before touching key material. Missing either produces a `missing_field_error`. + +2. **Key material presence** — A subtle compatibility guard covers legacy clients: if neither `key_type` nor `secret` is present, it emits a `missing_field_error` for `secret`. If `key_type` is present but `secret` is absent, `keypairForSignature` handles the error internally (the caller might be providing `seed`, `seed_hex`, or `passphrase` instead). The early check only fires to give a clear error when both fields are completely absent. + +3. **Keypair derivation** — `RPC::keypairForSignature` in `RPCHelpers.cpp` resolves the full credential pipeline: it accepts `secret`, `passphrase`, `seed`, or `seed_hex` and supports both `secp256k1` (default) and `ed25519` key types. Notably, if `key_type` is specified, the plain `secret` field is rejected — the caller must use a more explicit seed encoding. The function returns `std::optional>` and writes errors into the `result` JSON value passed by reference. The `XRPL_ASSERT` following the call enforces the invariant that exactly one of {valid keypair, populated error} is true; the `if (!keyPair || RPC::contains_error(result))` guard below it handles both failure cases defensively. + +4. **Channel ID format** — The hex string is parsed into a `uint256`. An incorrect length or non-hex characters yields `rpcCHANNEL_MALFORMED`. + +5. **Amount format** — The amount must arrive as a JSON string, not a number. `params[jss::amount].isString()` is checked before calling `to_uint64`; if the value is a JSON integer or the string can't be parsed as a 64-bit unsigned integer, `rpcCHANNEL_AMT_MALFORMED` is returned. Requiring a string type sidesteps JSON parser precision loss for large uint64 values that exceed JavaScript's safe integer range. + +## Message Serialization and Signing + +Once all inputs are valid, the message to sign is constructed by `serializePayChanAuthorization` (defined inline in `PayChan.h`): + +```cpp +msg.add32(HashPrefix::paymentChannelClaim); // 'CLM\0' — 4-byte domain separator +msg.addBitString(key); // 32-byte channel ID +msg.add64(amt.drops()); // 8-byte XRP amount in drops +``` + +The `HashPrefix::paymentChannelClaim` value (`'CLM'`) acts as a domain separator that prevents this signature from being misinterpreted as a signature over any other data structure in the XRPL protocol. This is a standard pattern across XRPL signing operations — every signable object type has its own prefix. The resulting 44-byte message is signed with `sign(pk, sk, msg.slice())`, and the raw binary signature is hex-encoded for JSON transport via `strHex`. + +## Error Handling Design + +The `try/catch` around the `sign()` call is marked `// LCOV_EXCL_START` — the test suite can't trigger it under normal conditions. It exists as a last-resort defensive wrapper, since `sign()` implementations theoretically could throw if underlying cryptographic operations encounter unexpected state. In practice, for both `secp256k1` and `ed25519` paths, `sign()` operates on validated key material and a well-formed serializer buffer, so exceptions are not expected. + +## Relationship to `ChannelVerify` + +The `doChannelVerify` handler in `ChannelVerify.cpp` is the exact inverse: it accepts `public_key`, `channel_id`, `amount`, and `signature`, reconstructs the same serialized message via `serializePayChanAuthorization`, and calls `verify()` to confirm the signature. Crucially, `doChannelVerify` carries no admin restriction — verifying a signature is a read-only, key-material-free operation safe for any caller. The split between the two handlers cleanly separates the privileged signing operation from the unprivileged verification operation. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/signing/Sign.cpp.ai.json b/src/xrpld/rpc/handlers/admin/signing/Sign.cpp.ai.json new file mode 100644 index 0000000000..f25d548ea9 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/signing/Sign.cpp.ai.json @@ -0,0 +1,312 @@ +{ + "args": [ + { + "lineno": 13, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doSign", + "RPC::transactionSign" + ], + "entry_point": "doSign", + "purpose": "Handles the /sign RPC command: validates permissions, parses and validates transaction parameters, and performs transaction signing.", + "validation_points": [ + "doSign: role/canSign() check", + "doSign: fail_hard parameter check", + "RPC::transactionSign: tx_json, secret, and other parameter validation" + ] + } + ], + "data_flows": [ + { + "field": "role", + "flow": [ + "context.role", + "doSign: if (context.role != Role::ADMIN && !context.app.config().canSign())", + "used to allow/deny signing" + ], + "origin": "context.role (set by RPC framework based on authentication)", + "transformations": [ + "Compared against Role::ADMIN", + "Combined with config().canSign()" + ], + "validated_at": "doSign" + }, + { + "field": "canSign", + "flow": [ + "context.app.config().canSign()", + "doSign: if (context.role != Role::ADMIN && !context.app.config().canSign())", + "used to allow/deny signing" + ], + "origin": "context.app.config().canSign() (server config)", + "transformations": [ + "Boolean config value" + ], + "validated_at": "doSign" + }, + { + "field": "fail_hard", + "flow": [ + "context.params", + "doSign: context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool()", + "NetworkOPs::doFailHard", + "passed as failType to RPC::transactionSign" + ], + "origin": "context.params[jss::fail_hard] (JSON RPC input)", + "transformations": [ + "Checked for presence and boolean value", + "Converted to NetworkOPs::FailHard enum" + ], + "validated_at": "doSign" + }, + { + "field": "tx_json", + "flow": [ + "context.params", + "passed to RPC::transactionSign", + "validated and parsed inside RPC::transactionSign" + ], + "origin": "context.params[\"tx_json\"] (JSON RPC input)", + "transformations": [ + "Parsed as JSON object", + "Validated for required fields and structure" + ], + "validated_at": "RPC::transactionSign" + }, + { + "field": "secret", + "flow": [ + "context.params", + "passed to RPC::transactionSign", + "validated and used for signing inside RPC::transactionSign" + ], + "origin": "context.params[\"secret\"] (JSON RPC input)", + "transformations": [ + "Parsed as string", + "Validated for format and presence" + ], + "validated_at": "RPC::transactionSign" + } + ], + "description": "Implements the doSign RPC handler, which signs a transaction using a provided secret. Intended for admin use and deprecated in favor of standalone signing tools.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "jss::fail_hard", + "validation", + "missing", + "check" + ], + "evidence": "Field jss::fail_hard validated by xrpld RPC framework (jss::, RPC::, JsonContext)", + "issue_pattern": "Missing validation for jss::fail_hard", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, JsonContext) validates jss::fail_hard automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "tx_json", + "validation", + "missing", + "check" + ], + "evidence": "Field tx_json validated by xrpld RPC framework (jss::, RPC::, JsonContext)", + "issue_pattern": "Missing validation for tx_json", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, JsonContext) validates tx_json automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "secret", + "validation", + "missing", + "check" + ], + "evidence": "Field secret validated by xrpld RPC framework (jss::, RPC::, JsonContext)", + "issue_pattern": "Missing validation for secret", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, JsonContext) validates secret automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "role / canSign()", + "empty", + "string", + "validation" + ], + "evidence": "if (context.role != Role::ADMIN && !context.app.config().canSign()) at doSign", + "issue_pattern": "Missing empty string validation for role / canSign()", + "why_false_positive": "if (context.role != Role::ADMIN && !context.app.config().canSign()) validates role / canSign() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "fail_hard", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool() at doSign", + "issue_pattern": "Missing empty string validation for fail_hard", + "why_false_positive": "context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool() validates fail_hard for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "params (tx_json, secret, etc.)", + "empty", + "string", + "validation" + ], + "evidence": "RPC::transactionSign at doSign (delegated to transactionSign)", + "issue_pattern": "Missing empty string validation for params (tx_json, secret, etc.)", + "why_false_positive": "RPC::transactionSign validates params (tx_json, secret, etc.) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/signing/Sign.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 13, + "name": "doSign" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "The doSign handler is typically tested via RPC integration tests that exercise the /sign endpoint with various roles, parameters, and error conditions. Likely test files include rpc_sign_test.cpp, transaction_sign_test.cpp, or similar. Validation of role/canSign, fail_hard, and parameter presence/format should be covered. Gaps may exist in edge cases (e.g., malformed tx_json, missing secret, or config permutations). Direct unit tests for doSign may be limited if most validation is delegated to RPC::transactionSign.", + "validation_architecture": { + "auto_validated_fields": [ + "jss::fail_hard", + "tx_json", + "secret" + ], + "framework": "xrpld RPC framework (jss::, RPC::, JsonContext)", + "validation_layer": "entry_point (doSign), business_logic (transactionSign)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::make_error(rpcNOT_SUPPORTED, ...)", + "field": "role / canSign()", + "location": "doSign", + "validated_by": "if (context.role != Role::ADMIN && !context.app.config().canSign())", + "validates": [ + "Checks if the user is ADMIN or if the server is configured to allow signing" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "N/A (used to set failType, not directly validated for error)", + "field": "fail_hard", + "location": "doSign", + "validated_by": "context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool()", + "validates": [ + "Checks if fail_hard is present and is a boolean" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "Delegated to transactionSign (likely returns JSON error object)", + "field": "params (tx_json, secret, etc.)", + "location": "doSign (delegated to transactionSign)", + "validated_by": "RPC::transactionSign", + "validates": [ + "Validates presence and structure of tx_json", + "Validates presence and format of secret", + "Validates transaction fields inside tx_json" + ], + "validation_type": "format|type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/signing/Sign.cpp.ai.md b/src/xrpld/rpc/handlers/admin/signing/Sign.cpp.ai.md new file mode 100644 index 0000000000..81efc98485 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/signing/Sign.cpp.ai.md @@ -0,0 +1,50 @@ +# `Sign.cpp` — `doSign` RPC Handler + +`Sign.cpp` implements the `doSign` RPC handler — the server-side entry point for the `sign` JSON-RPC command. Its role is to accept a raw transaction object and a signing secret, produce a signed transaction blob, and return it to the caller without submitting it to the network. The file is intentionally minimal: it enforces access control, tags the resource cost, delegates all cryptographic and structural work to `RPC::transactionSign`, and appends a deprecation notice before returning. + +## Access Control: Two Paths to Permission + +The first thing `doSign` does is decide whether the caller is even allowed to sign. The check is: + +```cpp +if (context.role != Role::ADMIN && !context.app.config().canSign()) + return RPC::make_error(rpcNOT_SUPPORTED, "Signing is not supported by this server."); +``` + +This reflects a deliberate security policy. XRPL validators and public API nodes are not signing oracles: they should never accept raw key material from untrusted clients. The gate has two independent conditions that must both fail before access is denied. Admin connections (typically authenticated via a local port or credentials) bypass the config check entirely, since the operator is considered trusted. Non-admin callers are only permitted if the server has been explicitly configured with `[signing_support] = 1` — setting `signingEnabled_` to `true` in `Config`. That field defaults to `false`, so signing over a public endpoint is opt-in, not opt-out. This design reflects a deliberate hardening decision: a server inadvertently exposed to the internet will refuse to act as a signing oracle by default. + +## Resource Burden + +Before delegating, the handler marks the request as `Resource::feeHeavyBurdenRPC`. This feeds the server's rate-limiting and resource-metering system. Signing is computationally non-trivial (key derivation, serialization, ECDSA/Ed25519 signing), so it is correctly classified as a heavier burden than simple read queries. This prevents a client from hammering the signing endpoint without consequence. + +## `fail_hard` Handling + +The `fail_hard` flag is extracted defensively: + +```cpp +NetworkOPs::FailHard const failType = NetworkOPs::doFailHard( + context.params.isMember(jss::fail_hard) && context.params[jss::fail_hard].asBool()); +``` + +The short-circuit `isMember()` check guards against accessing a missing field, converting its absence to `false`. This is slightly more careful than the parallel `doSignFor` handler in `SignFor.cpp`, which reads `context.params[jss::fail_hard].asBool()` directly. Since `doSign` only signs without submitting, `fail_hard` is passed through to `transactionSign` where it influences how errors in the signing pipeline are treated rather than network submission behaviour. + +## Delegation to `transactionSign` + +All substantive work — parsing `tx_json`, resolving the key from `secret` (or `seed` / `seed_hex` / `passphrase` variants), auto-filling fields like `Sequence` and `Fee` from the validated ledger, serializing the transaction, and computing the cryptographic signature — is handled by `RPC::transactionSign` in `detail/TransactionSign.h`. The handler passes `params` by value (as the function signature requires for local modification), the current API version, the `failType`, the caller's role, and the age of the most recently validated ledger. The ledger age matters because `transactionSign` will reject signing requests if the server's view of the ledger is stale, preventing the issuance of transactions based on outdated state. + +## Deprecation Annotation + +Regardless of success or failure, `doSign` appends a `deprecated` field to every response: + +```cpp +ret[jss::deprecated] = + "This command has been deprecated and will be " + "removed in a future version of the server. Please " + "migrate to a standalone signing tool."; +``` + +This follows the broader XRPL project direction of moving signing responsibility out of `rippled` entirely. The security argument is straightforward: a server node should not handle secret keys. Standalone tools such as `xrpl.js`, `xrpl-py`, or hardware wallets sign locally without the key ever leaving the client. The deprecation message is always present — not conditional on the caller's role — signalling that even admin callers should migrate. + +## Relationship to Sibling Handlers + +The `signing/` subdirectory contains three related handlers: `Sign.cpp`, `SignFor.cpp`, and `ChannelAuthorize.cpp`. `Sign.cpp` and `SignFor.cpp` are structurally near-identical, sharing the same access-control check, resource fee, and deprecation message. The difference is that `SignFor` calls `transactionSignFor`, which appends a signature to a transaction without replacing existing signatures — used for multi-signing flows where a designated signer adds their signature on behalf of another account. `Sign.cpp` is the simpler single-signer case. Both are deprecated in favour of client-side signing. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp.ai.json b/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp.ai.json new file mode 100644 index 0000000000..2de32ec8f9 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp.ai.json @@ -0,0 +1,329 @@ +{ + "args": [ + { + "lineno": 12, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doSignFor", + "RPC::transactionSignFor" + ], + "entry_point": "doSignFor", + "purpose": "Handles the 'sign_for' RPC command, performing validation and then signing a transaction on behalf of another account.", + "validation_points": [ + "doSignFor: Validates role and server signing capability (if (context.role != Role::ADMIN && !context.app.config().canSign()))", + "RPC::transactionSignFor: Validates tx_json, account, secret, and other transaction parameters" + ] + } + ], + "data_flows": [ + { + "field": "context.role", + "flow": [ + "context.role", + "doSignFor: checked in if statement", + "used to determine if signing is allowed" + ], + "origin": "RPC::JsonContext (injected by RPC framework)", + "transformations": [ + "Compared against Role::ADMIN" + ], + "validated_at": "doSignFor" + }, + { + "field": "context.app.config().canSign()", + "flow": [ + "context.app.config().canSign()", + "doSignFor: checked in if statement", + "used to determine if signing is allowed" + ], + "origin": "Application config (server settings)", + "transformations": [ + "Boolean check" + ], + "validated_at": "doSignFor" + }, + { + "field": "context.params[jss::fail_hard]", + "flow": [ + "context.params[jss::fail_hard]", + "doSignFor: asBool() conversion", + "assigned to failHard", + "converted to failType via NetworkOPs::doFailHard", + "passed to RPC::transactionSignFor" + ], + "origin": "RPC request JSON parameters", + "transformations": [ + "JSON boolean extraction", + "converted to internal failType" + ], + "validated_at": "Implicitly validated by asBool() and downstream in transactionSignFor" + }, + { + "field": "context.params (tx_json, account, secret, etc.)", + "flow": [ + "context.params", + "passed directly to RPC::transactionSignFor" + ], + "origin": "RPC request JSON parameters", + "transformations": [ + "No transformation in doSignFor; validation and transformation occur in transactionSignFor" + ], + "validated_at": "RPC::transactionSignFor" + } + ], + "description": "Implements the doSignFor RPC handler, which signs a transaction on behalf of a specified account using provided credentials. This command is deprecated.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "tx_json", + "validation", + "missing", + "check" + ], + "evidence": "Field tx_json validated by jss:: (JSON field tags), RPC::transactionSignFor (template-based validation)", + "issue_pattern": "Missing validation for tx_json", + "why_false_positive": "jss:: (JSON field tags), RPC::transactionSignFor (template-based validation) validates tx_json automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "account", + "validation", + "missing", + "check" + ], + "evidence": "Field account validated by jss:: (JSON field tags), RPC::transactionSignFor (template-based validation)", + "issue_pattern": "Missing validation for account", + "why_false_positive": "jss:: (JSON field tags), RPC::transactionSignFor (template-based validation) validates account automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "secret", + "validation", + "missing", + "check" + ], + "evidence": "Field secret validated by jss:: (JSON field tags), RPC::transactionSignFor (template-based validation)", + "issue_pattern": "Missing validation for secret", + "why_false_positive": "jss:: (JSON field tags), RPC::transactionSignFor (template-based validation) validates secret automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "fail_hard", + "validation", + "missing", + "check" + ], + "evidence": "Field fail_hard validated by jss:: (JSON field tags), RPC::transactionSignFor (template-based validation)", + "issue_pattern": "Missing validation for fail_hard", + "why_false_positive": "jss:: (JSON field tags), RPC::transactionSignFor (template-based validation) validates fail_hard automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "role / canSign()", + "empty", + "string", + "validation" + ], + "evidence": "if (context.role != Role::ADMIN && !context.app.config().canSign()) at doSignFor", + "issue_pattern": "Missing empty string validation for role / canSign()", + "why_false_positive": "if (context.role != Role::ADMIN && !context.app.config().canSign()) validates role / canSign() for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "fail_hard", + "empty", + "string", + "validation" + ], + "evidence": "context.params[jss::fail_hard].asBool() at doSignFor", + "issue_pattern": "Missing empty string validation for fail_hard", + "why_false_positive": "context.params[jss::fail_hard].asBool() validates fail_hard for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "fail_hard", + "type", + "validation", + "check" + ], + "evidence": "context.params[jss::fail_hard].asBool() at doSignFor", + "issue_pattern": "Missing type validation for fail_hard", + "why_false_positive": "context.params[jss::fail_hard].asBool() validates fail_hard type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "params (tx_json, account, secret, etc.)", + "empty", + "string", + "validation" + ], + "evidence": "RPC::transactionSignFor at doSignFor (delegated to transactionSignFor)", + "issue_pattern": "Missing empty string validation for params (tx_json, account, secret, etc.)", + "why_false_positive": "RPC::transactionSignFor validates params (tx_json, account, secret, etc.) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 11, + "name": "doSignFor" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The doSignFor handler is typically tested via RPC integration tests that exercise the 'sign_for' endpoint. Likely test files include 'sign_for_test.cpp', 'transaction_signing_test.cpp', or generic RPC handler tests. These tests should cover: (1) role-based access (admin vs. non-admin), (2) server signing enablement, (3) fail_hard parameter handling, (4) parameter validation (tx_json, account, secret). Gaps may exist in edge cases for malformed input, config permutations, or deprecated warning coverage. Template-based validation in transactionSignFor may be tested indirectly, but explicit unit tests for validation logic may be lacking.", + "validation_architecture": { + "auto_validated_fields": [ + "tx_json", + "account", + "secret", + "fail_hard" + ], + "framework": "jss:: (JSON field tags), RPC::transactionSignFor (template-based validation)", + "validation_layer": "entry_point (doSignFor), business_logic (transactionSignFor)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::make_error(rpcNOT_SUPPORTED, ...)", + "field": "role / canSign()", + "location": "doSignFor", + "validated_by": "if (context.role != Role::ADMIN && !context.app.config().canSign())", + "validates": [ + "Checks if the user is ADMIN or if server is configured to allow signing" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "Implicit (if not bool, may throw or default)", + "field": "fail_hard", + "location": "doSignFor", + "validated_by": "context.params[jss::fail_hard].asBool()", + "validates": [ + "Checks that fail_hard is a boolean" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "Depends on transactionSignFor (likely RPC error codes)", + "field": "params (tx_json, account, secret, etc.)", + "location": "doSignFor (delegated to transactionSignFor)", + "validated_by": "RPC::transactionSignFor", + "validates": [ + "tx_json: object type", + "account: presence and format", + "secret: presence and format", + "other transaction fields: type, format, business rules" + ], + "validation_type": "format|type|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp.ai.md b/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp.ai.md new file mode 100644 index 0000000000..f08ba42cc0 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/signing/SignFor.cpp.ai.md @@ -0,0 +1,50 @@ +# `SignFor.cpp` — RPC Handler for Multi-Signature Contribution + +## Role in the System + +`SignFor.cpp` implements `doSignFor`, the entry-point handler for the `sign_for` RPC command. In the XRPL multi-signature workflow, a transaction requires signatures from multiple authorized co-signers before it can be submitted. Rather than gathering those signatures out-of-band, `sign_for` lets one party contribute their single cryptographic signature to a partially-built transaction's `Signers` array. This file is the thin HTTP/RPC boundary that authorizes the call and delegates all real work to `RPC::transactionSignFor` in `detail/TransactionSign.cpp`. + +The handler is registered in `Handler.cpp` with `Role::USER`, meaning any connected client can issue the command over the RPC interface. However the file immediately applies a stricter secondary gate: the caller must either be `Role::ADMIN` or the node must be started with `[signing]` enabled in its configuration (`canSign()`). This two-level design intentionally distinguishes between *routing permission* (handled by the RPC dispatch table) and *feature permission* (handled locally in the handler). Nodes that aren't explicitly opt-in signing servers—such as public validators and gateways—return `rpcNOT_SUPPORTED` before touching any cryptographic material. + +## Design and Relationship to `Sign.cpp` + +The `sign` and `sign_for` commands live as siblings in the same `admin/signing/` directory and share identical access-control logic: + +```cpp +if (context.role != Role::ADMIN && !context.app.config().canSign()) + return RPC::make_error(rpcNOT_SUPPORTED, "Signing is not supported by this server."); +``` + +They diverge in their semantics: `doSign` creates a complete single-signature for a transaction and expects only `tx_json` and `secret`, while `doSignFor` targets multi-signature construction and requires an additional `account` field identifying whose signature slot is being filled. The delegation targets differ correspondingly — `transactionSign` vs. `transactionSignFor`. + +There is a subtle difference in how `fail_hard` is read between the two siblings. `doSign` guards with `isMember` before calling `asBool()`, while `doSignFor` calls `asBool()` directly on the possibly-absent field, relying on `Json::Value`'s safe-default behavior to return `false` when the field is missing. Both are functionally correct, but the inconsistency is worth noting for future maintenance. + +## Resource Classification + +Before delegating, the handler sets: + +```cpp +context.loadType = Resource::feeHeavyBurdenRPC; +``` + +This marks the request as computationally expensive for the server's load-tracking system. Signing involves elliptic-curve cryptography, transaction serialization, and ledger lookups. Classifying it as `feeHeavyBurdenRPC` allows the resource manager to throttle or penalize clients who issue many signing requests, which is especially important on public nodes where `canSign()` may be enabled. + +## Delegation to `transactionSignFor` + +The handler passes six arguments to `RPC::transactionSignFor`: the raw JSON params (by value so the callee can modify them freely), the API version, the `failType` enum derived from `fail_hard`, the caller's role, the age of the most recently validated ledger, and the application reference. Inside `transactionSignFor`, the real work happens: + +1. The `account` field is decoded to an `AccountID`. +2. `checkMultiSignFields` verifies that `tx_json` carries the mandatory `Sequence` and `SigningPubKey` fields, which must be pre-filled by the caller for multi-signing (unlike single-signing where the server can auto-fill them). +3. The transaction is pre-processed through `transactionPreProcessImpl` with a `SigningForParams` context, which handles key derivation and signature computation. +4. The ledger is consulted to verify that the provided secret actually belongs to the claimed `account` via `acctMatchesPubKey`. +5. The resulting `STObject` signer entry (containing `Account`, `TxnSignature`, and `SigningPubKey`) is injected into the `Signers` array of the serialized transaction and returned as JSON. + +None of this logic lives in `SignFor.cpp` itself — the file is purely an access-control and resource-accounting shim. + +## Deprecation + +The handler appends a `deprecated` field to every successful response: + +> "This command has been deprecated and will be removed in a future version of the server. Please migrate to a standalone signing tool." + +This mirrors identical deprecation warnings in `doSign` and reflects a deliberate architectural direction: server-side signing exposes private keys to the network layer and requires trusting the node operator, making it fundamentally unsafe for production use. XRPL clients are expected to sign locally using libraries such as `xrpl.js` or the C++ `xrpl` library, submitting only a pre-signed blob. The `sign_for` command remains available behind both the admin gate and the `canSign()` opt-in, but its presence in the response signals to callers that the migration clock is running. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp.ai.json b/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp.ai.json new file mode 100644 index 0000000000..64e14d460b --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp.ai.json @@ -0,0 +1,130 @@ +{ + "args": [ + { + "lineno": 10, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RPC framework dispatches request", + "doConsensusInfo" + ], + "entry_point": "doConsensusInfo", + "purpose": "Handles the 'consensus_info' RPC command, returning consensus state information.", + "validation_points": [ + "Template-based validation (not shown in this file, likely handled by RPC framework before doConsensusInfo is called)" + ] + } + ], + "data_flows": [ + { + "field": "context", + "flow": [ + "RPC framework receives request", + "Constructs JsonContext (parsing/validation may occur here)", + "Passes context to doConsensusInfo" + ], + "origin": "RPC::JsonContext (constructed by RPC framework from incoming HTTP/JSON-RPC request)", + "transformations": [ + "May be validated and normalized by framework before reaching handler" + ], + "validated_at": "RPC framework (before doConsensusInfo)" + }, + { + "field": "ret[jss::info]", + "flow": [ + "doConsensusInfo calls context.netOps.getConsensusInfo()", + "Result assigned to ret[jss::info]", + "ret returned as JSON response" + ], + "origin": "context.netOps.getConsensusInfo()", + "transformations": [ + "getConsensusInfo() likely gathers and formats consensus state data" + ], + "validated_at": "Assumed internal validation in getConsensusInfo() or earlier" + } + ], + "description": "Provides an RPC handler function to retrieve consensus information from the network operations object in the xrpl namespace.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 9, + "name": "doConsensusInfo" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is a simple pass-through with no direct input fields to validate. Validation is likely handled by the RPC framework's template-based system before this function is called. There are probably integration or functional tests for the 'consensus_info' RPC command, but unit tests for this function are unlikely or unnecessary due to its trivial logic. Gaps: No direct validation or transformation is performed here, so test coverage would focus on the RPC framework and getConsensusInfo().", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss::, RPC::JsonContext)", + "validation_layer": "entry_point" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp.ai.md b/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp.ai.md new file mode 100644 index 0000000000..1ab752af71 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/ConsensusInfo.cpp.ai.md @@ -0,0 +1,51 @@ +# `ConsensusInfo.cpp` — RPC Handler for `consensus_info` + +This file implements `doConsensusInfo`, the RPC handler that surfaces the ledger node's current consensus engine state to administrators. It exists as one of several diagnostic handlers in the `admin/status/` directory, alongside peers like `doFetchInfo` and `doValidatorInfo`, all of which follow the same pattern: receive an `RPC::JsonContext`, call one method on the network-operations subsystem, and return the result wrapped in a standard JSON envelope. + +## Role in the System + +The `consensus_info` command is an admin-only diagnostic tool. In `src/xrpld/rpc/detail/Handler.cpp`, it is registered as: + +```cpp +{"consensus_info", byRef(&doConsensusInfo), Role::ADMIN, NO_CONDITION} +``` + +The `Role::ADMIN` restriction means the command is only reachable by connections authenticated as administrator clients — it is not exposed to ordinary user-facing API consumers. `NO_CONDITION` means no ledger state prerequisite is checked before dispatch; the handler runs regardless of whether the node is synchronized or has a current or closed ledger in hand. + +In `RPCCall.cpp`, the command is mapped to `parseAsIs` with both minimum and maximum argument counts set to zero. Any command-line invocation that passes extra arguments is immediately rejected with a `badSyntax` error before the handler is ever reached. + +## The Handler + +`doConsensusInfo` is a pure pass-through: + +```cpp +Json::Value +doConsensusInfo(RPC::JsonContext& context) +{ + Json::Value ret(Json::objectValue); + ret[jss::info] = context.netOps.getConsensusInfo(); + return ret; +} +``` + +The `RPC::JsonContext` carries a reference to the `NetworkOPs` interface, which abstracts the node's network and consensus operations. `getConsensusInfo()` is implemented in `NetworkOPsImp` as a single-line delegation: + +```cpp +Json::Value +NetworkOPsImp::getConsensusInfo() +{ + return mConsensus.getJson(true); +} +``` + +`mConsensus` is the live `Consensus` instance. The `true` argument selects the verbose response path in `Consensus::getJson(bool full)`, which is explicitly documented in `Consensus.h` as being "called by the `consensus_info` RPC." That method assembles a JSON object containing fields like `proposing`, `proposers`, `synched`, `ledger_seq`, `close_granularity`, and (in full mode) the current consensus phase, dispute sets, and timing information. + +## Design Rationale + +The handler itself performs no transformation, no validation, and no error handling. This is intentional and consistent throughout the `admin/status/` module. All input validation is done either by the RPC framework before dispatch (argument count enforcement via `parseAsIs`) or by the role-check gate (`Role::ADMIN`). Because `consensus_info` accepts no user parameters, there is nothing to validate at the handler level — the only meaningful action is to snapshot the subsystem and return it. + +Keeping handlers this thin has a practical benefit: when `Consensus::getJson()` changes — say, a new consensus phase is added — the RPC layer requires no changes. The boundary between "how consensus state is serialized" and "how it reaches the RPC caller" stays clean. + +## Testing + +`src/test/server/ServerStatus_test.cpp` exercises `getConsensusInfo()` directly on the `NetworkOPs` object to assert `validating` status under various amendment-blocked and UNL-blocked scenarios. The `consensus_info` command-line form is tested in `RPCCall_test.cpp`, which confirms that the minimal invocation produces correct JSON and that extra arguments trigger a `badSyntax` error. There is no dedicated unit test for `doConsensusInfo` itself — given its role as a one-line adapter, such a test would only validate the JSON key name `jss::info`, which the framework tests cover transitively. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp.ai.json b/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp.ai.json new file mode 100644 index 0000000000..f17e0cdc2a --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp.ai.json @@ -0,0 +1,176 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doFetchInfo" + ], + "entry_point": "doFetchInfo", + "purpose": "Handles the 'fetch_info' RPC command, optionally clearing ledger fetch info and returning current fetch info.", + "validation_points": [ + "if (context.params.isMember(jss::clear) && context.params[jss::clear].asBool())" + ] + } + ], + "data_flows": [ + { + "field": "clear", + "flow": [ + "context.params", + "context.params[jss::clear]", + "context.netOps.clearLedgerFetch() (if validated)", + "ret[jss::clear] = true" + ], + "origin": "context.params (JSON input from RPC request)", + "transformations": [ + "Checked for presence with isMember", + "Converted to bool with asBool", + "Triggers clearLedgerFetch() if true" + ], + "validated_at": "if (context.params.isMember(jss::clear) && context.params[jss::clear].asBool())" + }, + { + "field": "info", + "flow": [ + "context.netOps.getLedgerFetchInfo()", + "ret[jss::info]" + ], + "origin": "context.netOps.getLedgerFetchInfo()", + "transformations": [ + "No transformation; direct assignment" + ], + "validated_at": "N/A (no validation in this function)" + } + ], + "description": "Implements the doFetchInfo RPC handler, which returns information about ledger fetch operations and optionally clears fetch state if requested.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "clear", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::isMember, Json::Value::asBool at doFetchInfo", + "issue_pattern": "Missing empty string validation for clear", + "why_false_positive": "Json::Value::isMember, Json::Value::asBool validates clear for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "clear", + "type", + "validation", + "check" + ], + "evidence": "Json::Value::isMember, Json::Value::asBool at doFetchInfo", + "issue_pattern": "Missing type validation for clear", + "why_false_positive": "Json::Value::isMember, Json::Value::asBool validates clear type" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doFetchInfo" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no direct evidence of test coverage in this file. Typical test coverage would be in RPC handler integration or unit tests, likely under test/rpc/ or test/handlers/ directories. Tests should cover: (1) 'clear' present and true, (2) 'clear' present and false, (3) 'clear' absent, (4) malformed 'clear' (non-bool). Gaps: No explicit validation for type of 'clear' (e.g., if not bool), and no error handling for malformed input in this function.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss:: for field names, Json::Value for type access)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (implicit validation, no error thrown)", + "field": "clear", + "location": "doFetchInfo", + "validated_by": "Json::Value::isMember, Json::Value::asBool", + "validates": [ + "Checks if 'clear' field exists in input JSON", + "Checks if 'clear' field is truthy (interpreted as boolean)" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp.ai.md b/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp.ai.md new file mode 100644 index 0000000000..a5ce8fcce0 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/FetchInfo.cpp.ai.md @@ -0,0 +1,23 @@ +# `FetchInfo.cpp` — `fetch_info` Admin RPC Handler + +This file implements `doFetchInfo`, the server-side handler for the `fetch_info` admin RPC command. Its role is to expose the internal state of the inbound ledger fetch subsystem to node operators and, optionally, to clear recorded fetch failures on demand. + +## Context in the Admin RPC Layer + +`doFetchInfo` lives in `src/xrpld/rpc/handlers/admin/status/` alongside a set of single-purpose diagnostic handlers — `doConsensusInfo`, `doValidatorInfo`, `doGetCounts`, `doPrint`, and others. All follow the same structural pattern: receive an `RPC::JsonContext`, delegate to a method on `context.netOps` (the `NetworkOPs` abstraction), and return a `Json::Value`. The handler is registered in `Handler.cpp` under the command name `"fetch_info"` with `Role::ADMIN`, meaning it is only accessible to callers presenting admin credentials. + +## What the Handler Does + +The function performs two operations, one conditional: + +1. **Optional state reset**: If the incoming JSON parameters include a `"clear"` field that evaluates to `true`, `context.netOps.clearLedgerFetch()` is called. This delegates immediately to `InboundLedgers::clearFailures()`, which wipes the set of ledger hashes that have been marked as failed fetches. This is useful when an operator wants to retry fetching ledgers that were previously abandoned — for example, after a transient network partition healed. The response echoes `"clear": true` to confirm the action was taken. + +2. **Status snapshot**: Regardless of the `clear` flag, the handler calls `context.netOps.getLedgerFetchInfo()`, which routes to `InboundLedgers::getInfo()`. The returned `Json::Value` snapshot is placed under the top-level `"info"` key in the response. + +## Design Notes + +The `clear` parameter is handled with a deliberate double-guard: `isMember(jss::clear)` confirms the field exists before `asBool()` reads it. This prevents a type-coercion exception if the field is absent, and `asBool()` handles non-boolean values gracefully via `Json::Value`'s internal coercion rules. No explicit error is thrown for unexpected types — the framework's JSON coercion is treated as sufficient protection, consistent with how sibling handlers in this directory deal with optional parameters. + +The clear-then-read ordering is intentional: failures are cleared first so that the immediately returned `"info"` snapshot reflects the post-clear state. This makes a single `fetch_info {"clear": true}` call both an action and a confirmation, eliminating the need for a separate read round-trip. + +Because the handler contains no business logic beyond delegation and optional state reset, the `NetworkOPs` abstraction is the real complexity boundary. The thin adapter design keeps RPC handlers auditable and ensures the actual fetch tracking state lives exclusively in the `InboundLedgers` subsystem. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp.ai.json b/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp.ai.json new file mode 100644 index 0000000000..06d4290f68 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp.ai.json @@ -0,0 +1,201 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doGetCounts", + "getCountsJson" + ], + "entry_point": "doGetCounts", + "purpose": "Handles the RPC 'get_counts' admin/status request, validates and processes the min_count parameter, and returns system object and cache statistics.", + "validation_points": [ + "doGetCounts: min_count is extracted from context.params and converted to unsigned int via asUInt()" + ] + }, + { + "call_chain": [ + "getCountsJson", + "CountedObjects::getInstance().getCounts", + "app.get*() methods" + ], + "entry_point": "getCountsJson", + "purpose": "Aggregates various system and cache statistics, using minObjectCount as a filter for object counts.", + "validation_points": [ + "getCountsJson: minObjectCount is assumed to be validated by caller (doGetCounts)" + ] + } + ], + "data_flows": [ + { + "field": "min_count", + "flow": [ + "context.params[jss::min_count]", + "doGetCounts: asUInt() conversion", + "minCount local variable", + "getCountsJson(minCount)", + "CountedObjects::getCounts(minCount)" + ], + "origin": "context.params[jss::min_count] (JSON RPC input)", + "transformations": [ + "If present, min_count is converted to unsigned int via asUInt()", + "If absent, defaults to 10" + ], + "validated_at": "doGetCounts: asUInt() conversion (type and range check by asUInt())" + }, + { + "field": "objectCounts", + "flow": [ + "getCountsJson: objectCounts", + "for loop: each [k, v] inserted into ret JSON" + ], + "origin": "CountedObjects::getInstance().getCounts(minObjectCount)", + "transformations": [ + "Filtered by minObjectCount" + ], + "validated_at": "Indirectly validated by min_count validation" + }, + { + "field": "context.params", + "flow": [ + "doGetCounts: context.params", + "doGetCounts: checks isMember(jss::min_count)", + "doGetCounts: passes minCount to getCountsJson" + ], + "origin": "RPC JSON request", + "transformations": [ + "Extraction of min_count field" + ], + "validated_at": "doGetCounts: isMember + asUInt()" + } + ], + "description": "Provides RPC handler and utility functions to gather and return various runtime and cache statistics, database usage, and uptime information for the XRPL server, typically for admin/status endpoints.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "min_count", + "empty", + "string", + "validation" + ], + "evidence": "manual type/range check (likely, based on comment and context usage) at doGetCounts", + "issue_pattern": "Missing empty string validation for min_count", + "why_false_positive": "manual type/range check (likely, based on comment and context usage) validates min_count for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp", + "functions": [ + { + "args": [ + "text", + "seconds", + "unitName", + "unitVal" + ], + "lineno": 13, + "name": "textTime" + }, + { + "args": [ + "app", + "minObjectCount" + ], + "lineno": 29, + "name": "getCountsJson" + }, + { + "args": [ + "context" + ], + "lineno": 87, + "name": "doGetCounts" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is likely tested via admin/status RPC integration tests, e.g., in test suites for the 'get_counts' endpoint. Unit tests for doGetCounts or getCountsJson are unlikely unless specifically written. Edge cases (e.g., negative, non-numeric, or very large min_count) may not be fully covered unless there are explicit tests for invalid input types/ranges. The asUInt() method will throw or return 0 for invalid types, but this is not explicitly handled in the code, so test coverage for error handling is likely incomplete.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss:: for field names, RPC::JsonContext for input)", + "validation_layer": "entry_point (doGetCounts is the handler for the RPC method)" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "RPC::expected_field_error or similar (inferred, not shown in snippet)", + "field": "min_count", + "location": "doGetCounts", + "validated_by": "manual type/range check (likely, based on comment and context usage)", + "validates": [ + "Checks if min_count is present in context.params", + "If present, checks if min_count is an integer", + "If not present, defaults to 10 (per comment)", + "If present and not integer, throws error" + ], + "validation_type": "type|range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp.ai.md b/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp.ai.md new file mode 100644 index 0000000000..7660273571 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/GetCounts.cpp.ai.md @@ -0,0 +1,33 @@ +## `GetCounts.cpp` — Runtime Diagnostics and Cache Statistics + +This file implements the `get_counts` RPC command, an administrative endpoint that provides a single-call snapshot of the runtime health of a running `rippled` instance. It surfaces live object reference counts, database disk usage, multiple cache hit rates, node store write pressure, and formatted uptime — essentially everything an operator or developer needs to assess internal performance state without digging through logs or external monitoring tools. + +The file is registered in `Handler.cpp` as `{"get_counts", byRef(&doGetCounts), Role::ADMIN, NO_CONDITION}`, meaning it is accessible only to trusted admin connections and carries no ledger-state precondition. + +### `textTime` — Destructive Uptime Formatter + +The static helper `textTime` is an unusual design worth noting: it takes `seconds` by reference and modifies it in place, stripping off the largest time unit on each call. The caller in `getCountsJson` chains five calls for years, days, hours, minutes, and seconds. Each call divides the remaining time by the unit's value, appends to the output string, and subtracts what it consumed. Because the remainder decreases at each step, there is no risk of double-counting, and units with a zero count produce no output — so "2 hours, 30 seconds" is emitted cleanly without a spurious "0 minutes" in the middle. The plural suffix is handled inline: `if (i > 1) text += "s"`. This is a tightly scoped utility with no external exposure. + +### `getCountsJson` — The Aggregation Core + +`getCountsJson(Application& app, int minObjectCount)` is the real work of the file, and it is deliberately factored out from the RPC dispatch layer. Its declaration in `GetCounts.h` means it can be called by non-RPC paths — tests or internal monitoring code — without going through the handler's parameter-parsing machinery. + +**Object counts via `CountedObjects`.** The first thing the function does is call `CountedObjects::getInstance().getCounts(minObjectCount)`, which walks a lock-free singly-linked list of `Counter` objects registered by every class that inherits from `CountedObject`. The CRTP template registers a static counter at program startup using an atomic compare-and-swap to prepend to the list head. `getCounts` returns only the entries whose live count meets the `minObjectCount` threshold — this filters out object types with very few active instances, reducing noise for operators. The result is flattened directly into the response JSON as `{ClassName: liveCount}` pairs. + +**Relational database usage.** The block that queries `app.getRelationalDatabase()` is guarded by `app.config().useTxTables()`. When the node is configured without transaction tables (for example, a reporting-only node or a stripped-down configuration), this entire block is skipped. When enabled, it reports `dbKBTotal`, `dbKBLedger`, and `dbKBTransaction` — kilobyte usage for the full SQLite database, the ledger table, and the transaction table respectively. Each value is only written into the response if it is nonzero, keeping the output compact. `local_txs` (the count of transactions held in the local queue via `NetworkOPs::getLocalTxCount`) is bundled in the same guard block because it is only meaningful when the transaction subsystem is active. + +**Write pressure and historical sync rate.** `write_load` from `NodeStore::Database::getWriteLoad()` is an estimate of pending write operations — useful for detecting a falling-behind node store. `historical_perminute` from `InboundLedgers::fetchRate()` measures how many historical ledgers are being fetched per minute, which spikes during catchup and drops to near-zero on a synced node. + +**Cache health.** Four distinct caches are probed: +- `SLE_hit_rate` — the hit rate on the cached State Ledger Entries (SLEs), the on-ledger account/object records. +- `ledger_hit_rate` — `LedgerMaster`'s internal ledger cache hit rate. +- `AL_size` and `AL_hit_rate` — the size and hit rate of the `AcceptedLedger` cache, which holds recently finalized ledger structures. +- `fullbelow_size`, `treenode_cache_size`, and `treenode_track_size` — statistics from the `NodeFamily`'s SHAMap tree caches. `fullbelow_size` reflects how many nodes are known to have no missing children (avoiding unnecessary fetches). `treenode_cache_size` is the number of cached `SHAMapTreeNode` objects; `treenode_track_size` is the total tracked (including weaker references). + +After these, the function delegates back to `NodeStore::Database::getCountsJson(ret)` to let the node store append its own internal counters (I/O stats, fetch counts, and similar) directly into the same JSON object. + +### `doGetCounts` — RPC Entry Point + +`doGetCounts` is a thin dispatch wrapper. It reads an optional `min_count` parameter from the request (defaulting to `10` if absent) and calls `getCountsJson`. The `asUInt()` call on the JSON parameter performs implicit type coercion — a non-numeric or negative value would produce `0`, silently widening to "show everything". The default of `10` exists as a practical noise filter: object types with fewer than 10 live instances are rarely interesting in production diagnostics. + +The separation of `doGetCounts` from `getCountsJson` is a consistent pattern across this `admin/status` directory. The RPC handler layer deals only with request parsing and context; the actual data aggregation is independently accessible. This makes `getCountsJson` directly usable from places like server-info dump routines or internal health checks without having to construct a fake `RPC::JsonContext`. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/GetCounts.h.ai.json b/src/xrpld/rpc/handlers/admin/status/GetCounts.h.ai.json new file mode 100644 index 0000000000..785f1bb8a0 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/GetCounts.h.ai.json @@ -0,0 +1,23 @@ +{ + "args": [], + "classes": [], + "description": "This header file declares a function to generate a JSON object containing counts of various objects or resources in the XRPL application, likely for administrative or monitoring purposes.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/status/GetCounts.h", + "functions": [ + { + "args": [ + "Application& app", + "int minObjectCount" + ], + "lineno": 7, + "name": "getCountsJson" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/GetCounts.h.ai.md b/src/xrpld/rpc/handlers/admin/status/GetCounts.h.ai.md new file mode 100644 index 0000000000..392f52c89c --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/GetCounts.h.ai.md @@ -0,0 +1,7 @@ +# `GetCounts.h` — Admin Status: Object and Resource Count Snapshot + +This header exposes a single free function, `getCountsJson`, which aggregates a broad snapshot of live object counts and runtime health metrics from the XRPL node into a `Json::Value` object. It sits in the `admin/status` RPC handler subdirectory and exists as a separately declared header — rather than being a private implementation detail of `GetCounts.cpp` — because it is consumed from two distinct call sites: the `doGetCounts` RPC handler (in `GetCounts.cpp`) and `OverlayImpl::getCountsJson` (in `overlay/detail/OverlayImpl.cpp`), which embeds the same snapshot into peer-overlay status reporting. + +The `minObjectCount` parameter acts as a noise filter: `CountedObjects::getInstance().getCounts(minObjectCount)` returns only those tracked types whose live instance count meets or exceeds the threshold, preventing the output from being cluttered by rarely-instantiated objects. The RPC entry point defaults this to 10 (overridable via the `min_count` request parameter), while `OverlayImpl` hard-codes 10 for its internal use. + +The separation of `getCountsJson` from `doGetCounts` is a deliberate interface boundary. It allows non-RPC subsystems to obtain the same diagnostic payload without depending on `RPC::JsonContext` or the full RPC dispatch machinery — only on `Application&`. The header's sole `#include` of `Application.h` enforces that minimal dependency surface. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/Print.cpp.ai.json b/src/xrpld/rpc/handlers/admin/status/Print.cpp.ai.json new file mode 100644 index 0000000000..78a54bc1bc --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/Print.cpp.ai.json @@ -0,0 +1,281 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doPrint" + ], + "entry_point": "doPrint", + "purpose": "Handles the 'print' RPC command, validates input, and writes application state to a property stream.", + "validation_points": [ + "context.params.isObject()", + "context.params[jss::params].isArray()", + "context.params[jss::params][0u].isString()" + ] + } + ], + "data_flows": [ + { + "field": "context.params", + "flow": [ + "RPC JSON request", + "context.params", + "doPrint", + "validation (isObject)", + "used to access jss::params" + ], + "origin": "RPC::JsonContext (populated from incoming RPC JSON request)", + "transformations": [ + "Checked for object type" + ], + "validated_at": "doPrint (context.params.isObject())" + }, + { + "field": "context.params[jss::params]", + "flow": [ + "context.params", + "context.params[jss::params]", + "doPrint", + "validation (isArray)", + "used to access [0u]" + ], + "origin": "context.params (JSON object from RPC request)", + "transformations": [ + "Checked for array type" + ], + "validated_at": "doPrint (context.params[jss::params].isArray())" + }, + { + "field": "context.params[jss::params][0u]", + "flow": [ + "context.params[jss::params]", + "context.params[jss::params][0u]", + "doPrint", + "validation (isString)", + "used as argument to context.app.write" + ], + "origin": "context.params[jss::params] (JSON array from RPC request)", + "transformations": [ + "Checked for string type", + "Converted to std::string via asString()" + ], + "validated_at": "doPrint (context.params[jss::params][0u].isString())" + } + ], + "description": "Implements the doPrint RPC handler, which outputs application state or information to a JSON property stream, optionally filtered by a string parameter.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "context.params", + "empty", + "string", + "validation" + ], + "evidence": "isObject() method (Json::Value API) at doPrint", + "issue_pattern": "Missing empty string validation for context.params", + "why_false_positive": "isObject() method (Json::Value API) validates context.params for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "context.params", + "type", + "validation", + "check" + ], + "evidence": "isObject() method (Json::Value API) at doPrint", + "issue_pattern": "Missing type validation for context.params", + "why_false_positive": "isObject() method (Json::Value API) validates context.params type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "context.params[jss::params]", + "empty", + "string", + "validation" + ], + "evidence": "isArray() method (Json::Value API) at doPrint", + "issue_pattern": "Missing empty string validation for context.params[jss::params]", + "why_false_positive": "isArray() method (Json::Value API) validates context.params[jss::params] for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "context.params[jss::params]", + "type", + "validation", + "check" + ], + "evidence": "isArray() method (Json::Value API) at doPrint", + "issue_pattern": "Missing type validation for context.params[jss::params]", + "why_false_positive": "isArray() method (Json::Value API) validates context.params[jss::params] type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "context.params[jss::params][0u]", + "empty", + "string", + "validation" + ], + "evidence": "isString() method (Json::Value API) at doPrint", + "issue_pattern": "Missing empty string validation for context.params[jss::params][0u]", + "why_false_positive": "isString() method (Json::Value API) validates context.params[jss::params][0u] for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "context.params[jss::params][0u]", + "type", + "validation", + "check" + ], + "evidence": "isString() method (Json::Value API) at doPrint", + "issue_pattern": "Missing type validation for context.params[jss::params][0u]", + "why_false_positive": "isString() method (Json::Value API) validates context.params[jss::params][0u] type" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/status/Print.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doPrint" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code relies on the structure of the incoming JSON RPC request. Typical test coverage would be in RPC handler or integration tests, likely under test suites for admin/status or print RPC commands. Tests should cover: (1) valid object/array/string input, (2) missing or malformed params (object/array/string), (3) default path (no string param). Gaps may exist if there are no negative tests for malformed input or if the property stream output is not validated. No explicit unit tests are shown in this file; coverage depends on higher-level RPC handler tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Json::Value API, jss:: (JSON field name constants)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (branching logic, not exception)", + "field": "context.params", + "location": "doPrint", + "validated_by": "isObject() method (Json::Value API)", + "validates": [ + "Checks that context.params is a JSON object" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "None (branching logic, not exception)", + "field": "context.params[jss::params]", + "location": "doPrint", + "validated_by": "isArray() method (Json::Value API)", + "validates": [ + "Checks that context.params[jss::params] is a JSON array" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "None (branching logic, not exception)", + "field": "context.params[jss::params][0u]", + "location": "doPrint", + "validated_by": "isString() method (Json::Value API)", + "validates": [ + "Checks that the first element of context.params[jss::params] is a string" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/Print.cpp.ai.md b/src/xrpld/rpc/handlers/admin/status/Print.cpp.ai.md new file mode 100644 index 0000000000..b8929ca6ce --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/Print.cpp.ai.md @@ -0,0 +1,27 @@ +# `Print.cpp` — Admin RPC Handler for Application Property Stream Dump + +## Role in the System + +`Print.cpp` implements `doPrint`, an admin-only RPC command that exposes the entire `rippled` application's internal diagnostic state as a JSON object. It sits among a small cluster of status handlers (`ConsensusInfo`, `FetchInfo`, `GetCounts`, `ValidatorInfo`, etc.) in `src/xrpld/rpc/handlers/admin/status/` — but where those handlers target specific subsystems, `doPrint` exposes the full `beast::PropertyStream` tree rooted at the `Application` object itself. + +## The PropertyStream Architecture + +`Application` extends `beast::PropertyStream::Source`, making the application object the root of a named, hierarchical tree of diagnostic sources. Every major subsystem — consensus engine, ledger master, network operations, job queue, and so on — registers itself as a child source. This tree can be traversed and serialized on demand, providing a runtime introspection view of the node's internal state without coupling any component to a particular output format. + +`beast::PropertyStream::Source` exposes two relevant `write()` overloads: one that serializes the full tree recursively, and one that accepts a dot-delimited path string to target a specific named sub-source (or, if the path ends with `*`, to recursively dump that subtree's children). `doPrint` uses whichever variant is appropriate based on the incoming RPC parameters. + +`JsonPropertyStream` is the concrete `PropertyStream` sink used here. It implements the abstract `PropertyStream` interface by building up a `Json::Value` object tree, buffered in a stack of `Json::Value*` pointers. When `app.write()` drives the stream, it populates this structure; `stream.top()` then returns the accumulated root `Json::Value`, which becomes the RPC response. + +## Parameter Handling and Validation + +The optional path filter follows the nested `params` convention used throughout the RPC layer: the caller passes `{"params": ["some.path"]}`. The guard condition — `context.params.isObject() && context.params[jss::params].isArray() && context.params[jss::params][0u].isString()` — validates all three type levels before accessing the string. If any check fails, the function silently falls back to dumping the entire tree. There is no error return path; the handler always succeeds, making it robust to malformed or missing parameters. + +This "silent fallback to full dump" is a deliberate design choice for a diagnostic tool: a caller who passes a bad filter still gets useful output rather than an error response. The tradeoff is that a typo in a path string is invisible to the caller. + +## Concurrency Notes + +Serialization safety is handled inside `beast::PropertyStream::Source::write()`, which acquires a `std::recursive_mutex` on the source before iterating its children. `doPrint` itself takes no locks; it relies entirely on the `PropertyStream` infrastructure to mediate access to subsystem state. This is consistent with the other admin status handlers in this directory, which similarly read state via thread-safe accessor methods rather than acquiring top-level locks. + +## Relationship to Sibling Handlers + +`doPrint` is the lowest-level, highest-breadth handler in the `admin/status` group. The other handlers in this directory (`ConsensusInfo`, `ValidatorInfo`, etc.) are purpose-built for specific subsystems and return structured, pre-shaped JSON. `doPrint` makes no assumptions about what the application contains — it delegates entirely to the property stream tree, so it automatically reflects new subsystems that register themselves as `PropertyStream::Source` children without requiring any changes to the handler itself. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp.ai.json b/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp.ai.json new file mode 100644 index 0000000000..0ac2d07cc0 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp.ai.json @@ -0,0 +1,229 @@ +{ + "args": [ + { + "lineno": 11, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doValidatorInfo", + "context.app.getValidationPublicKey", + "context.app.getValidatorManifests().getMasterKey", + "context.app.getValidatorManifests().getManifest", + "context.app.getValidatorManifests().getSequence", + "context.app.getValidatorManifests().getDomain" + ], + "entry_point": "doValidatorInfo", + "purpose": "Returns information about the validator's keys and manifest, if configured as a validator.", + "validation_points": [ + "context.app.getValidationPublicKey() // Checks if node is configured as validator", + "context.app.getValidatorManifests().getMasterKey(*validationPK) // Validates and resolves master key" + ] + } + ], + "data_flows": [ + { + "field": "validationPK", + "flow": [ + "context.app.getValidationPublicKey()", + "if (!validationPK) return error", + "used as input to getMasterKey(*validationPK)", + "used as comparison to mk", + "used as value for ephemeral_key" + ], + "origin": "context.app.getValidationPublicKey()", + "transformations": [ + "Checked for presence (validation)", + "Dereferenced for use as key", + "Compared to mk to determine if ephemeral" + ], + "validated_at": "Immediately after retrieval (if (!validationPK))" + }, + { + "field": "mk (master key)", + "flow": [ + "getMasterKey(*validationPK)", + "toBase58(TokenType::NodePublic, mk)", + "compared to validationPK", + "used as key for getManifest, getSequence, getDomain" + ], + "origin": "context.app.getValidatorManifests().getMasterKey(*validationPK)", + "transformations": [ + "Base58 encoding for output", + "Used as lookup key for manifest/sequence/domain" + ], + "validated_at": "Implicitly validated by getMasterKey (returns valid master key or fallback)" + }, + { + "field": "manifest", + "flow": [ + "getManifest(mk)", + "if present, base64_encode(*manifest)", + "added to ret[jss::manifest]" + ], + "origin": "context.app.getValidatorManifests().getManifest(mk)", + "transformations": [ + "Base64 encoding" + ], + "validated_at": "Presence checked (if (manifest))" + }, + { + "field": "seq", + "flow": [ + "getSequence(mk)", + "if present, added to ret[jss::seq]" + ], + "origin": "context.app.getValidatorManifests().getSequence(mk)", + "transformations": [], + "validated_at": "Presence checked (if (seq))" + }, + { + "field": "domain", + "flow": [ + "getDomain(mk)", + "if present, added to ret[jss::domain]" + ], + "origin": "context.app.getValidatorManifests().getDomain(mk)", + "transformations": [], + "validated_at": "Presence checked (if (domain))" + } + ], + "description": "Implements the doValidatorInfo RPC handler, which returns information about the validator's keys, manifest, sequence, and domain if the server is configured as a validator.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "validationPK (Validation Public Key)", + "empty", + "string", + "validation" + ], + "evidence": "context.app.getValidationPublicKey() at doValidatorInfo", + "issue_pattern": "Missing empty string validation for validationPK (Validation Public Key)", + "why_false_positive": "context.app.getValidationPublicKey() validates validationPK (Validation Public Key) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "mk (Master Key)", + "empty", + "string", + "validation" + ], + "evidence": "context.app.getValidatorManifests().getMasterKey(*validationPK) at doValidatorInfo", + "issue_pattern": "Missing empty string validation for mk (Master Key)", + "why_false_positive": "context.app.getValidatorManifests().getMasterKey(*validationPK) validates mk (Master Key) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 10, + "name": "doValidatorInfo" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is likely tested via RPC integration tests, e.g., in test suites for validator admin endpoints. Unit tests may exist for getValidationPublicKey and ValidatorManifests, but direct unit tests for doValidatorInfo may be limited. Edge cases such as missing validationPK, mk == validationPK (no manifest), and presence/absence of manifest/sequence/domain should be tested. Gaps may exist in testing error handling (not_validator_error), and in scenarios with malformed or missing manifest data.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld/rpc (jss:: for JSON field names, but not for validation itself)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::not_validator_error()", + "field": "validationPK (Validation Public Key)", + "location": "doValidatorInfo", + "validated_by": "context.app.getValidationPublicKey()", + "validates": [ + "Checks if the server is configured as a validator by verifying the presence of a validation public key" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "None (returns early if mk == validationPK)", + "field": "mk (Master Key)", + "location": "doValidatorInfo", + "validated_by": "context.app.getValidatorManifests().getMasterKey(*validationPK)", + "validates": [ + "Checks if the master key is the same as the validation public key, implying no ephemeral key or manifest" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp.ai.md b/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp.ai.md new file mode 100644 index 0000000000..6604b13222 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp.ai.md @@ -0,0 +1,27 @@ +# `ValidatorInfo.cpp` — Admin RPC Handler for Local Validator Identity + +## Role in the System + +This file implements `doValidatorInfo`, an admin-only RPC handler that reports the local node's own validator key configuration back to the operator. It answers a specific operational question: "what validator identity is this node currently using, and what manifest is active?" This is distinct from the general-purpose `Manifest.cpp` handler in `server_info/`, which looks up manifests for *any* validator by accepting a public key as a request parameter. `doValidatorInfo` takes no parameters — it always describes the local node's own state, and it is only accessible via the admin API. + +## The Two-Key Validator Architecture + +Understanding this handler requires understanding how XRPL validator keys work. Validators use a two-tier key scheme described in `include/xrpl/server/Manifest.h`. A **master key** is kept offline under strict access control and is never used to sign ledger validations directly. An **ephemeral (signing) key** is installed on the live validator and signs validations in production. The master key signs a **manifest** — essentially a certificate — that binds the master key to the current ephemeral key, along with a sequence number and optional domain claim. This lets operators rotate the hot signing key (e.g., after a suspected compromise) without requiring every other node on the network to update their config: they just receive a new manifest via gossip, verify it against the already-known master key, and replace the old ephemeral key. + +`ManifestCache` (`ManifestCache` in `Manifest.h`) is the in-memory store for these manifests, indexed by master public key and reverse-indexed by ephemeral key. Its `getMasterKey()` method has a crucial identity-fallback behavior: if the supplied key is not found as a signing (ephemeral) key in any manifest, it returns the key itself unchanged. This fallback is the mechanism `doValidatorInfo` uses to detect the no-manifest case. + +## Control Flow and Design Decisions + +The handler first calls `context.app.getValidationPublicKey()`, which returns `std::optional`. This method (`Application.cpp` line 537) reads `validatorKeys_.keys->publicKey` — the ephemeral signing key parsed from the node's `[validator_token]` config entry. If the node has no validator key configured at all, it returns an empty optional, and the handler immediately returns `RPC::not_validator_error()`. + +When a key is present, the handler passes it to `getValidatorManifests().getMasterKey(*validationPK)`. Because the node stores its own key as the *ephemeral* key, `getMasterKey` returns the master key from the manifest. If `mk == validationPK`, the fallback identity was triggered: the node has no manifest in the cache (or is running in a simplified key mode where the validation key is also the master key). In this case the handler returns early with only `master_key` populated. The early return is intentional — `ephemeral_key`, `manifest`, `seq`, and `domain` are only meaningful when there is an active manifest relationship between two distinct keys. + +When master and ephemeral keys differ, the handler populates the remaining fields by querying the same `ManifestCache` through four separate optional-returning accessors: `getManifest()`, `getSequence()`, and `getDomain()`. Each is independently guarded with `if (auto const ...)`, meaning any field may be absent without being an error. In practice, `manifest` and `seq` should always be present when a manifest exists, but the optional pattern defends against transient cache states or future schema changes. The raw manifest bytes returned by `getManifest()` are binary-serialized XRPL objects, so they are `base64_encode`d for JSON transport. + +## Encoding Conventions + +Both master and ephemeral keys are formatted with `toBase58(TokenType::NodePublic, ...)`, producing the `n...` prefixed base58check strings familiar in XRPL validator configuration. The manifest blob itself is base64 because it is a binary serialized XRPL `STObject`, not human-readable text. The `jss::` namespace constants (`master_key`, `ephemeral_key`, `manifest`, `seq`, `domain`) are compile-time string constants ensuring JSON field name consistency across the codebase. + +## Relationship to Sibling Handlers + +The `admin/status/` directory groups read-only diagnostic handlers requiring admin authorization. `Validators.cpp` (a two-line handler) delegates entirely to `ValidatorList::getJson()` for the *set* of trusted validators from the UNL. `ValidatorInfo.cpp` is complementary: it reports only the node's own identity within that set. The parallel public-facing `server_info/Manifest.cpp` serves external callers who need to inspect any validator's manifest by submitting a public key — it does not check whether the node is itself a validator. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp.ai.json b/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp.ai.json new file mode 100644 index 0000000000..ab5843e29d --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp.ai.json @@ -0,0 +1,83 @@ +{ + "args": [ + { + "lineno": 8, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doValidatorListSites", + "context.app.getValidatorSites()", + "ValidatorSites::getJson()" + ], + "entry_point": "doValidatorListSites", + "purpose": "Returns a JSON representation of the configured validator list sites for the XRPL node.", + "validation_points": [ + "No explicit validation in doValidatorListSites; any validation would occur inside getValidatorSites() or getJson(), but not visible in this file." + ] + } + ], + "data_flows": [ + { + "field": "context", + "flow": [ + "RPC framework receives request", + "Constructs RPC::JsonContext", + "Passes context to doValidatorListSites" + ], + "origin": "RPC::JsonContext passed to doValidatorListSites (from RPC framework, likely from HTTP/JSON-RPC request)", + "transformations": [ + "No transformation in this function; context is passed as-is." + ], + "validated_at": "Not validated in this function." + }, + { + "field": "validator sites data", + "flow": [ + "context.app.getValidatorSites()", + "ValidatorSites::getJson()" + ], + "origin": "Application::getValidatorSites()", + "transformations": [ + "getJson() serializes internal ValidatorSites data to JSON." + ], + "validated_at": "Any validation would occur inside ValidatorSites or getJson(), not in this function." + } + ], + "description": "Provides an RPC handler function that returns the validator list sites as JSON by calling the application's ValidatorSites component.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 7, + "name": "doValidatorListSites" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "This function is a thin wrapper with no explicit validation or transformation. Test coverage would be indirect, via RPC integration or handler tests. Likely tested in end-to-end or RPC handler test suites (e.g., test/rpc/ValidatorListSites_test.cpp or similar), but not unit tested directly. Validation of input is not present here; any validation of validator site data would be tested in ValidatorSites or its methods. Gaps: No direct validation or error handling is tested here; relies on downstream components.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp.ai.md b/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp.ai.md new file mode 100644 index 0000000000..9776a8677d --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/ValidatorListSites.cpp.ai.md @@ -0,0 +1,29 @@ +# `ValidatorListSites.cpp` — RPC Handler for Validator List Sites Status + +## Role in the System + +This file implements `doValidatorListSites`, the RPC handler backing the `validator_list_sites` admin command. Its entire job is to expose the runtime state of the node's configured validator list fetching machinery as a JSON response. The handler is registered in `Handler.cpp` with `Role::ADMIN` and no feature-flag condition, placing it squarely in the set of diagnostic/status commands restricted to trusted operators. + +## What It Does + +The function body is a single expression: + +```cpp +return context.app.getValidatorSites().getJson(); +``` + +`context.app` is the singleton `Application` instance. `getValidatorSites()` returns the `ValidatorSite` subsystem, which manages the periodic HTTP fetching of published validator lists from a configurable set of remote URIs. The `getJson()` call on that subsystem serializes its current internal state — the per-site URIs, last refresh timestamps, refresh intervals, last disposition (whether a fetched list was accepted or rejected), redirect tracking, and request success flags — into a `Json::Value` that the RPC framework delivers back to the caller. + +## Design Rationale + +The handler is intentionally a pass-through with no transformation or validation logic of its own. This follows the same pattern seen in the sibling `Validators.cpp`, which does `context.app.getValidators().getJson()` for the active validator set. The logic for what constitutes valid state and how to serialize it belongs entirely to `ValidatorSite`, keeping the RPC layer free of domain knowledge. Any future change to the fields reported, or to how site health is assessed, can be made in `ValidatorSite::getJson()` without touching the handler. + +## `ValidatorSite` Context + +`ValidatorSite` (declared in `ValidatorSite.h`) manages a `std::vector` where each `Site` tracks three resource pointers: the originally configured URI (`loadedResource`), the starting resource used at each refresh cycle (`startingResource`, updated only on permanent redirects), and the currently active resource (`activeResource`, updated on temporary redirects). This three-pointer design lets the subsystem correctly handle both 301 and 302 redirects without losing the configured origin. Each site also carries a `lastRefreshStatus` (an `optional` recording time, `ListDisposition`, and a message), enabling the `getJson()` output to surface whether the last fetch succeeded and what action was taken on the returned list. + +Two mutexes — `sites_mutex_` and `state_mutex_` — protect different aspects of the subsystem, with the documented invariant that `sites_mutex_` must always be acquired before `state_mutex_` to avoid deadlock. Because `getJson()` is marked `const` and takes both locks internally, the RPC handler can safely call it from any thread without concern for the fetch timer's concurrent activity. + +## Relationship to Adjacent Handlers + +Within `src/xrpld/rpc/handlers/admin/status/`, this handler sits alongside `Validators.cpp` (active validator set), `ValidatorInfo.cpp` (individual validator details), `ConsensusInfo.cpp`, `FetchInfo.cpp`, `GetCounts.cpp`, and `Print.cpp`. All are similarly thin wrappers that delegate entirely to application subsystems, reflecting a consistent architectural boundary: the RPC layer dispatches requests and formats responses; domain state lives in the `app/misc/` subsystem layer. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/Validators.cpp.ai.json b/src/xrpld/rpc/handlers/admin/status/Validators.cpp.ai.json new file mode 100644 index 0000000000..373826f123 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/Validators.cpp.ai.json @@ -0,0 +1,81 @@ +{ + "args": [ + { + "lineno": 8, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doValidators", + "context.app.getValidators()", + "ValidatorList::getJson()" + ], + "entry_point": "doValidators", + "purpose": "Returns the current validator list as JSON in response to an RPC call.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "RPC::JsonContext context", + "flow": [ + "RPC framework receives request", + "Constructs JsonContext with request data and Application reference", + "Passed to doValidators" + ], + "origin": "Created by the RPC framework when an RPC request is received.", + "transformations": [ + "No transformation in doValidators; context is passed as-is." + ], + "validated_at": "No validation occurs in doValidators." + }, + { + "field": "ValidatorList (from context.app.getValidators())", + "flow": [ + "context.app.getValidators() retrieves ValidatorList", + "ValidatorList::getJson() serializes validator data to JSON" + ], + "origin": "Application singleton holds ValidatorList instance.", + "transformations": [ + "ValidatorList data is serialized to JSON via getJson()." + ], + "validated_at": "Any validation would occur inside ValidatorList methods, but not in this code." + } + ], + "description": "Implements the doValidators RPC handler, which returns validator information from the application's ValidatorList as JSON.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/admin/status/Validators.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 7, + "name": "doValidators" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is a simple passthrough with no input validation or transformation. Test coverage would be in integration or RPC handler tests, likely in files such as 'rpc/handlers/Validators_test.cpp' or broader RPC test suites. There is no direct validation logic to test here; coverage would focus on correct JSON output and error handling if ValidatorList is unavailable. Gaps: No field-level validation or error handling is present in this code path.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/admin/status/Validators.cpp.ai.md b/src/xrpld/rpc/handlers/admin/status/Validators.cpp.ai.md new file mode 100644 index 0000000000..25e1d45f43 --- /dev/null +++ b/src/xrpld/rpc/handlers/admin/status/Validators.cpp.ai.md @@ -0,0 +1,34 @@ +# `src/xrpld/rpc/handlers/admin/status/Validators.cpp` + +## Role in the System + +This file implements `doValidators`, the server-side handler for the `validators` admin RPC command. Its entire body is a single expression: it calls `ValidatorList::getJson()` via the application context and returns the result directly to the RPC framework. The file exists as a necessary binding point between the RPC dispatch table and the validator management subsystem — nothing more. + +## Handler Registration and Access Control + +The `"validators"` command is registered in `src/xrpld/rpc/detail/Handler.cpp` as `Role::ADMIN` with `NO_CONDITION`: + +```cpp +{"validators", byRef(&doValidators), Role::ADMIN, NO_CONDITION}, +``` + +`Role::ADMIN` means the caller must connect via an authenticated admin channel (local socket or explicitly whitelisted admin IP). `NO_CONDITION` means there is no prerequisite on ledger or network state — the handler will respond even if the node hasn't synced. This is appropriate because validator list metadata is available as soon as the application has started and loaded its configuration, independent of ledger progress. + +## What the Response Contains + +All the content comes from `ValidatorList::getJson()`, which the `ValidatorList.h` header documents as thread-safe ("may be called concurrently"). The method serializes the full state of the application's active UNL (Unique Node List): trusted signing keys, quorum thresholds, publisher information, list sequence numbers, expiry timestamps, and per-publisher disposition flags (`ListDisposition` values such as `accepted`, `expired`, `pending`, `stale`, etc.). None of this serialization or decision logic lives in this handler file. + +## Deliberate Minimalism + +The handler performs no input validation, no parameter extraction, and no error handling. This is by design: the `validators` RPC carries no request parameters, and any failure modes (e.g., an uninitialized `ValidatorList`) are the responsibility of the subsystem. The pattern is consistent across the `admin/status/` directory — `doValidatorListSites` in the adjacent `ValidatorListSites.cpp` is structurally identical, delegating directly to `ValidatorSite::getJson()`. + +## Relationship to Sibling Validators Handlers + +Three related handlers in this directory serve distinct diagnostic purposes: + +- **`doValidators`** (this file): Full publisher list state — all trusted keys, quorum, expiry, per-list disposition. Answers *what validators does this node trust and why?* +- **`doValidatorInfo`** (`ValidatorInfo.cpp`): Self-referential — describes *this node's own* validator identity (ephemeral signing key, master key, manifest, sequence). Errors if the node is not configured as a validator. +- **`doValidatorListSites`** (`ValidatorListSites.cpp`): Describes the remote HTTP/S sites this node fetches validator lists from, including last-fetch timestamps and status. Answers *where does this node get its UNL?* +- **`doUnlList`** (`UnlList.cpp`, in the parent `admin/` directory): A narrower view — just the set of trusted node public keys without publisher-level metadata. + +Together these four handlers expose complementary slices of the same underlying trust infrastructure, with `doValidators` providing the broadest and most operationally useful view for diagnosing validator list health. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.cpp.ai.json b/src/xrpld/rpc/handlers/ledger/Ledger.cpp.ai.json new file mode 100644 index 0000000000..dcb3c16df7 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/Ledger.cpp.ai.json @@ -0,0 +1,671 @@ +{ + "args": [ + { + "lineno": 12, + "name": "context" + }, + { + "lineno": 61, + "name": "value" + } + ], + "classes": [ + { + "args": [ + "JsonContext& context" + ], + "lineno": 11, + "name": "LedgerHandler" + } + ], + "code_paths": [ + { + "call_chain": [ + "LedgerHandler::check", + "lookupLedger", + "isUnlimited", + "getFeeTrack().isLoadedLocal", + "context_.app.getTxQ().getTxs" + ], + "entry_point": "LedgerHandler::check", + "purpose": "Validates input parameters for the ledger RPC, checks permissions, server load, and ledger existence before proceeding.", + "validation_points": [ + "lookupLedger (validates ledger/ledger_hash/ledger_index)", + "isUnlimited (validates 'full' and 'accounts' permissions)", + "getFeeTrack().isLoadedLocal && !isUnlimited (validates server load for 'full' or 'accounts')", + "ledger_ && ledger_->open() (validates 'queue' parameter)" + ] + }, + { + "call_chain": [ + "LedgerHandler::writeResult", + "copyFrom", + "addJson", + "context_.app.getLedgerMaster().getClosedLedger", + "context_.app.getLedgerMaster().getCurrentLedger" + ], + "entry_point": "LedgerHandler::writeResult", + "purpose": "Formats and writes the result JSON, including ledger data and warnings.", + "validation_points": [ + "No direct validation, but relies on prior validation in check()" + ] + }, + { + "call_chain": [ + "doLedgerGrpc", + "RPC::ledgerFromRequest" + ], + "entry_point": "doLedgerGrpc", + "purpose": "Handles gRPC ledger requests, validates and fetches ledger data.", + "validation_points": [ + "RPC::ledgerFromRequest (validates ledger parameters)" + ] + } + ], + "data_flows": [ + { + "field": "ledger / ledger_hash / ledger_index", + "flow": [ + "context_.params", + "LedgerHandler::check (needsLedger)", + "lookupLedger(ledger_, context_, result_)", + "ledger_ (used in writeResult)" + ], + "origin": "context_.params (JSON-RPC request)", + "transformations": [ + "Checked for presence", + "Validated and resolved to ledger_" + ], + "validated_at": "lookupLedger" + }, + { + "field": "full", + "flow": [ + "context_.params[jss::full]", + "LedgerHandler::check (bool full)", + "options_ |= LedgerFill::full", + "Permission checked via isUnlimited(context_.role)", + "Used in addJson" + ], + "origin": "context_.params[jss::full]", + "transformations": [ + "Converted to bool", + "Used to set options_ bitmask" + ], + "validated_at": "isUnlimited" + }, + { + "field": "accounts", + "flow": [ + "context_.params[jss::accounts]", + "LedgerHandler::check (bool accounts)", + "options_ |= LedgerFill::dumpState", + "Permission checked via isUnlimited(context_.role)", + "Used in addJson" + ], + "origin": "context_.params[jss::accounts]", + "transformations": [ + "Converted to bool", + "Used to set options_ bitmask" + ], + "validated_at": "isUnlimited" + }, + { + "field": "queue", + "flow": [ + "context_.params[jss::queue]", + "LedgerHandler::check (bool queue)", + "options_ |= LedgerFill::dumpQueue", + "If true: validated by ledger_ && ledger_->open()", + "queueTxs_ = context_.app.getTxQ().getTxs()", + "Used in addJson" + ], + "origin": "context_.params[jss::queue]", + "transformations": [ + "Converted to bool", + "Used to set options_ bitmask", + "Triggers fetching of queueTxs_" + ], + "validated_at": "ledger_ && ledger_->open()" + }, + { + "field": "role", + "flow": [ + "context_.role", + "isUnlimited(context_.role)", + "Used to gate 'full' and 'accounts' access" + ], + "origin": "context_.role (injected by framework)", + "transformations": [ + "Checked for admin/unlimited status" + ], + "validated_at": "isUnlimited" + }, + { + "field": "server load", + "flow": [ + "context_.app.getFeeTrack().isLoadedLocal()", + "if (full || accounts) { ... if (isLoadedLocal && !isUnlimited) ... }", + "May return rpcTOO_BUSY" + ], + "origin": "context_.app.getFeeTrack().isLoadedLocal()", + "transformations": [ + "Checked for server busy state" + ], + "validated_at": "getFeeTrack().isLoadedLocal() && !isUnlimited" + } + ], + "description": "Implements the LedgerHandler class and related gRPC handler for serving ledger data via JSON and gRPC in the XRPL server, including permission checks, ledger serialization, and response formatting.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for ledger", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates ledger automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for ledger_hash", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates ledger_hash automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_index validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for ledger_index", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates ledger_index automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "full", + "validation", + "missing", + "check" + ], + "evidence": "Field full validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for full", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates full automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "transactions", + "validation", + "missing", + "check" + ], + "evidence": "Field transactions validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for transactions", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates transactions automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "accounts", + "validation", + "missing", + "check" + ], + "evidence": "Field accounts validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for accounts", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates accounts automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "expand", + "validation", + "missing", + "check" + ], + "evidence": "Field expand validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for expand", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates expand automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "binary", + "validation", + "missing", + "check" + ], + "evidence": "Field binary validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for binary", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates binary automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "owner_funds", + "validation", + "missing", + "check" + ], + "evidence": "Field owner_funds validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for owner_funds", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates owner_funds automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "queue", + "validation", + "missing", + "check" + ], + "evidence": "Field queue validated by xrpl::jss (JSON field access/validation), custom business logic", + "issue_pattern": "Missing validation for queue", + "why_false_positive": "xrpl::jss (JSON field access/validation), custom business logic validates queue automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger / ledger_hash / ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "lookupLedger at LedgerHandler::check", + "issue_pattern": "Missing empty string validation for ledger / ledger_hash / ledger_index", + "why_false_positive": "lookupLedger validates ledger / ledger_hash / ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "full", + "empty", + "string", + "validation" + ], + "evidence": "isUnlimited at LedgerHandler::check", + "issue_pattern": "Missing empty string validation for full", + "why_false_positive": "isUnlimited validates full for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "accounts", + "empty", + "string", + "validation" + ], + "evidence": "isUnlimited at LedgerHandler::check", + "issue_pattern": "Missing empty string validation for accounts", + "why_false_positive": "isUnlimited validates accounts for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "full or accounts (when server is busy)", + "empty", + "string", + "validation" + ], + "evidence": "getFeeTrack().isLoadedLocal() && !isUnlimited at LedgerHandler::check", + "issue_pattern": "Missing empty string validation for full or accounts (when server is busy)", + "why_false_positive": "getFeeTrack().isLoadedLocal() && !isUnlimited validates full or accounts (when server is busy) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "queue", + "empty", + "string", + "validation" + ], + "evidence": "ledger_ && ledger_->open() at LedgerHandler::check", + "issue_pattern": "Missing empty string validation for queue", + "why_false_positive": "ledger_ && ledger_->open() validates queue for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "full, transactions, accounts, expand, binary, owner_funds, queue", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::field].asBool() at LedgerHandler::check", + "issue_pattern": "Missing empty string validation for full, transactions, accounts, expand, binary, owner_funds, queue", + "why_false_positive": "params[jss::field].asBool() validates full, transactions, accounts, expand, binary, owner_funds, queue for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "full, transactions, accounts, expand, binary, owner_funds, queue", + "type", + "validation", + "check" + ], + "evidence": "params[jss::field].asBool() at LedgerHandler::check", + "issue_pattern": "Missing type validation for full, transactions, accounts, expand, binary, owner_funds, queue", + "why_false_positive": "params[jss::field].asBool() validates full, transactions, accounts, expand, binary, owner_funds, queue type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ledger/Ledger.cpp", + "functions": [ + { + "args": [ + "JsonContext& context" + ], + "lineno": 12, + "name": "LedgerHandler::LedgerHandler" + }, + { + "args": [], + "lineno": 16, + "name": "LedgerHandler::check" + }, + { + "args": [ + "Json::Value& value" + ], + "lineno": 61, + "name": "LedgerHandler::writeResult" + }, + { + "args": [ + "RPC::GRPCContext& context" + ], + "lineno": 93, + "name": "doLedgerGrpc" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "RPC" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in files like test/rpc/Ledger_test.cpp, test/rpc/LedgerHandler_test.cpp, or integration tests for the ledger RPC endpoint. Tests should cover: valid/invalid ledger identifiers, permission checks for 'full' and 'accounts', server busy conditions, queue parameter with open/closed ledgers, and deprecated field warnings. Gaps may exist in edge cases for malformed input, concurrent load conditions, or rare permission combinations. gRPC path (doLedgerGrpc) may have less coverage if tests focus on HTTP/JSON-RPC.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger", + "ledger_hash", + "ledger_index", + "full", + "transactions", + "accounts", + "expand", + "binary", + "owner_funds", + "queue" + ], + "framework": "xrpl::jss (JSON field access/validation), custom business logic", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Status (rpc error code, e.g., rpcLGR_NOT_FOUND)", + "field": "ledger / ledger_hash / ledger_index", + "location": "LedgerHandler::check", + "validated_by": "lookupLedger", + "validates": [ + "Presence and validity of ledger identifier (by name, hash, or index)", + "Ledger exists and can be loaded" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcNO_PERMISSION", + "field": "full", + "location": "LedgerHandler::check", + "validated_by": "isUnlimited", + "validates": [ + "User has permission to request full ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcNO_PERMISSION", + "field": "accounts", + "location": "LedgerHandler::check", + "validated_by": "isUnlimited", + "validates": [ + "User has permission to request all account state nodes" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcTOO_BUSY", + "field": "full or accounts (when server is busy)", + "location": "LedgerHandler::check", + "validated_by": "getFeeTrack().isLoadedLocal() && !isUnlimited", + "validates": [ + "Server is not overloaded for heavy requests unless user is unlimited" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcINVALID_PARAMS", + "field": "queue", + "location": "LedgerHandler::check", + "validated_by": "ledger_ && ledger_->open()", + "validates": [ + "Queue can only be requested for open ledgers" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (implicit type conversion, but could throw if not bool-convertible)", + "field": "full, transactions, accounts, expand, binary, owner_funds, queue", + "location": "LedgerHandler::check", + "validated_by": "params[jss::field].asBool()", + "validates": [ + "Field is present and can be interpreted as boolean" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.cpp.ai.md b/src/xrpld/rpc/handlers/ledger/Ledger.cpp.ai.md new file mode 100644 index 0000000000..57c3529105 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/Ledger.cpp.ai.md @@ -0,0 +1,43 @@ +# `src/xrpld/rpc/handlers/ledger/Ledger.cpp` + +This file is the implementation hub for the XRPL `ledger` RPC command, serving two distinct transport layers from the same source: the JSON-RPC path via `LedgerHandler` and the gRPC path via `doLedgerGrpc`. Its central job is to locate a ledger, apply the caller's requested output options, and serialize the result in the appropriate wire format. + +## `LedgerHandler` — JSON-RPC path + +`LedgerHandler` follows the two-phase handler pattern used throughout the XRPL RPC framework: `check()` runs first to validate, resolve, and prepare state; `writeResult()` runs second to emit the response. This split ensures that authorization and resource checks occur before any serialization work begins. + +### `check()` + +The method starts with a deliberate early-out: if none of `ledger`, `ledger_hash`, or `ledger_index` are present in the request, it returns immediately with `Status::OK` without setting `ledger_`. This is intentional — it signals `writeResult()` to fall back to the dual-ledger response mode (described below). + +When a ledger identifier is present, `lookupLedger` resolves and populates `ledger_` and seeds `result_` with preliminary ledger metadata. All flag parameters (`full`, `transactions`, `accounts`, `expand`, `binary`, `owner_funds`, `queue`) are then OR'd together into `options_` as a `LedgerFill::Options` bitmask, decoupling the parameter-parsing phase from the later serialization call. + +Two permission gates protect expensive operations. Requesting `full` or `accounts` (which dumps the entire account state trie) is restricted to admin/unlimited roles via `isUnlimited`. Even for admin users, if the local server is under high load (`getFeeTrack().isLoadedLocal()`), the request is rejected with `rpcTOO_BUSY`. The comment in the source acknowledges this is a practical throttle: *"Until some sane way to get full ledgers has been implemented, disallow retrieving all state nodes."* Binary mode incurs `feeMediumBurdenRPC` while JSON mode incurs `feeHeavyBurdenRPC`, reflecting the real CPU cost difference. + +The `queue` flag has a separate structural constraint: the transaction queue only exists for the open ledger. Requesting queue state against a validated or historical ledger is semantically meaningless, so `check()` enforces `ledger_->open()` and returns `rpcINVALID_PARAMS` otherwise. + +### `writeResult()` + +If `ledger_` is set, `writeResult()` calls `copyFrom` to merge any metadata already accumulated in `result_` (populated by `lookupLedger`) and then invokes `addJson` with the resolved ledger, context, options bitmask, and queue transactions. The `LedgerFill` struct passed to `addJson` bundles all of these together and also fetches the ledger's close time from `LedgerMaster`. + +If `ledger_` is *not* set (the no-identifier case), the method emits *both* the current closed and the current open ledger as sibling fields. This gives callers a consistent snapshot of the validator frontier without requiring them to know sequence numbers. + +Before returning, `writeResult()` checks for the deprecated `type` field and, if present, appends a `warnings` array entry with `warnRPC_FIELDS_DEPRECATED`. This follows the XRPL API convention of using structured warnings rather than errors for backward-compatibility notifications. + +## `doLedgerGrpc` — gRPC path + +The gRPC handler is a standalone free function targeting a protobuf `GetLedgerRequest`. It shares the ledger-lookup plumbing via `RPC::ledgerFromRequest` but diverges significantly in what it can return, as it is designed for efficient state synchronization by external indexing clients (primarily Clio). + +**Header serialization** is unconditional: the ledger header is serialized with `addRaw` and emitted as a raw binary blob, giving clients the canonical header bytes for hash verification. + +**Transaction output** is straightforward but guarded by a try-catch around the iteration of `ledger->txs`. If any transaction fails to deserialize mid-iteration, the handler logs an error and breaks — the caller receives whatever transactions were processed. This partial-failure tolerance is intentional: it is preferable to return a usable partial response and log the anomaly than to abort the entire RPC for a single corrupt entry. + +**State diff via `SHAMap::Delta`** is the most architecturally significant feature. When `get_objects` is set, the handler loads the parent ledger (sequence − 1) from `LedgerMaster`, then calls `base->stateMap().compare(desired->stateMap(), differences, maxDifferences)`. Both ledgers must be `dynamic_pointer_cast`-able to `Ledger const` — plain `ReadView` is insufficient — because `stateMap()` is only exposed on the concrete `Ledger` class. The diff produces a map of keys to before/after blob pairs, from which CREATED, MODIFIED, and DELETED entries are classified. This mechanism is far more bandwidth-efficient than dumping entire ledger state for each sequence, and it is the primary reason gRPC was introduced alongside JSON-RPC. + +**DEX book successor tracking** is the most intricate portion. When `get_object_neighbors` is enabled, for any `ltDIR_NODE` entry that lacks an `sfOwner` field (indicating it is an order book quality directory node rather than an account owner directory), the handler checks if it represents the *first* quality tier for a given offer book. If a quality node was just created, the handler records the book base and the node's key as the new best offer pointer. If deleted, it finds the new first remaining node in `desired->stateMap()`. This data allows Clio to track DEX best-offer positions across ledger transitions without a full trie scan, using `keylet::quality` and `getQualityNext` to bound the search range. + +**Performance instrumentation** wraps the entire function body: wall-clock duration in milliseconds is logged at `warn` level along with object and transaction counts. The division of duration by count is computed even when counts are zero, which would produce a divide-by-zero in practice — this is a minor robustness gap in the observability code path. + +## Relationship to Sibling Files + +The `ledger/` directory contains several related handlers (`LedgerData`, `LedgerEntry`, `LedgerDiff`, `LedgerHeader`, etc.), each addressing a narrower slice of ledger query functionality. `Ledger.cpp` is the broadest, handling the general-purpose `ledger` command that most clients use first. The shared lookup infrastructure (`lookupLedger`, `ledgerFromRequest`) is centralized in `RPCLedgerHelpers.h`, keeping ledger resolution logic out of individual handlers. The `LedgerFill` struct in `LedgerToJson.h` acts as the bridge between this handler's parameter interpretation and the actual JSON serialization logic. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.h.ai.json b/src/xrpld/rpc/handlers/ledger/Ledger.h.ai.json new file mode 100644 index 0000000000..ba46e4987c --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/Ledger.h.ai.json @@ -0,0 +1,71 @@ +{ + "args": [ + { + "lineno": 44, + "name": "context_" + }, + { + "lineno": 45, + "name": "ledger_" + }, + { + "lineno": 46, + "name": "queueTxs_" + }, + { + "lineno": 47, + "name": "result_" + }, + { + "lineno": 48, + "name": "options_" + } + ], + "classes": [ + { + "args": [ + "JsonContext&" + ], + "lineno": 26, + "name": "LedgerHandler" + } + ], + "description": "Defines the LedgerHandler class for handling the 'ledger' RPC command, including its interface, options, and metadata for API versioning and roles.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ledger/Ledger.h", + "functions": [ + { + "args": [ + "JsonContext&" + ], + "lineno": 28, + "name": "LedgerHandler" + }, + { + "args": [], + "lineno": 30, + "name": "check" + }, + { + "args": [ + "Json::Value&" + ], + "lineno": 33, + "name": "writeResult" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 12, + "name": "Json" + }, + { + "lineno": 17, + "name": "xrpl" + }, + { + "lineno": 18, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/Ledger.h.ai.md b/src/xrpld/rpc/handlers/ledger/Ledger.h.ai.md new file mode 100644 index 0000000000..eda64870af --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/Ledger.h.ai.md @@ -0,0 +1,47 @@ +# `LedgerHandler` — `src/xrpld/rpc/handlers/ledger/Ledger.h` + +## Role in the System + +`LedgerHandler` is the server-side implementation of the `ledger` JSON-RPC command — the primary endpoint clients use to inspect ledger state, transactions, and the transaction queue. The header defines the class contract that the RPC dispatch framework uses to wire this command into the live handler table. + +It is one of only two "new-style" class-based handlers in the codebase (the other being `VersionHandler`). All other RPC commands in `rippled` are registered as bare function pointers in the legacy `handlerArray` table in `Handler.cpp`. `LedgerHandler` and `VersionHandler` are added separately via the `addHandler()` template during `HandlerTable` construction. + +## Handler Protocol + +The class follows a two-phase protocol enforced by `addHandler()`: + +1. **`check()`** — validates incoming request parameters, resolves the target ledger via `lookupLedger()`, builds the `options_` bitmask from boolean flags (`full`, `transactions`, `accounts`, `expand`, `binary`, `owner_funds`, `queue`), enforces permission checks, and optionally fetches transaction-queue snapshots. It returns a `Status` that aborts dispatch on error. + +2. **`writeResult(Json::Value&)`** — serializes the response. If a specific ledger was resolved, it merges the intermediate JSON from `lookupLedger` with a full `addJson()` call that respects the `options_` bitmask and the captured `queueTxs_`. If no ledger selector was given at all, it takes the fallback path and returns *both* the current open and last-closed ledger headers under `open` and `closed` keys. + +## Static Metadata as a Compile-Time Contract + +The `static constexpr` members are not just documentation — `addHandler()` reads them at registration time and validates them with `static_assert`: + +```cpp +static constexpr char name[] = "ledger"; +static constexpr unsigned minApiVer = RPC::apiMinimumSupportedVersion; +static constexpr unsigned maxApiVer = RPC::apiMaximumValidVersion; +static constexpr Role role = Role::USER; +static constexpr Condition condition = NO_CONDITION; +``` + +`role = Role::USER` means the command is available to all authenticated callers; admin privilege is not required. `condition = NO_CONDITION` means the handler may run regardless of network synchronisation state — unlike commands that require `NEEDS_CURRENT_LEDGER` or `NEEDS_CLOSED_LEDGER`, the `ledger` RPC is safe to serve even during initial sync, because it can always return whatever state is locally available. + +## Private State and Why It's Structured This Way + +Four private members carry state across the two phases: + +- `context_` — a reference to the `JsonContext` (which bundles `Application&`, `LedgerMaster&`, `NetworkOPs&`, role, API version, and the raw `Json::Value params`). Held by reference rather than copied because the context is owned by the request lifetime. +- `ledger_` — a `shared_ptr` populated by `lookupLedger()`. The `ReadView` abstraction allows `check()` and `writeResult()` to be indifferent to whether the ledger is the open ledger, a closed ledger, or a historical ledger — all present the same read-only interface. +- `queueTxs_` — a `std::vector` snapshot of the transaction queue, fetched only when `queue=true` is requested *and* the resolved ledger is open. The validation in `check()` explicitly rejects `queue=true` against a closed/validated ledger with `rpcINVALID_PARAMS`, because the queue concept is only meaningful for the in-progress ledger. +- `options_` — an integer bitmask combining `LedgerFill` flag constants. Building this once in `check()` means `writeResult()` can pass a single integer to `addJson()` rather than re-parsing parameters. +- `result_` — an intermediate `Json::Value` populated by `lookupLedger()` during `check()`. Because `lookupLedger` may emit its own diagnostic fields, this is merged into the final output via `copyFrom()` in `writeResult()`. + +## Permission and Load Guards + +`check()` gates the expensive full-ledger and full-account-state dump operations behind `isUnlimited(context_.role)`, which returns `true` only for `IDENTIFIED` and `ADMIN` roles. If the local node is also under heavy load (`isLoadedLocal()`), even an admin caller is rejected with `rpcTOO_BUSY`. This deliberately prevents public WebSocket clients from triggering multi-megabyte serialisations of the entire state tree. Binary mode (`binary=true`) is charged at `feeMediumBurdenRPC` vs `feeHeavyBurdenRPC` for JSON, reflecting the significantly smaller output. + +## gRPC Companion + +The `.cpp` file also defines `doLedgerGrpc()` — a free function outside the `RPC` namespace that serves the equivalent protobuf `GetLedgerRequest`. This function shares no code path with `LedgerHandler`; it handles `get_objects` by computing a `SHAMap::Delta` between consecutive ledgers and optionally returning predecessor/successor neighbours for each changed object. The two implementations coexist in the same translation unit but represent entirely separate protocol surfaces with different capability sets. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp.ai.json b/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp.ai.json new file mode 100644 index 0000000000..d7247e17f6 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp.ai.json @@ -0,0 +1,182 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RPC framework dispatches to doLedgerClosed", + "doLedgerClosed", + "context.ledgerMaster.getClosedLedger", + "XRPL_ASSERT(ledger, ...)" + ], + "entry_point": "doLedgerClosed", + "purpose": "Handles the 'ledger_closed' RPC command, retrieves the most recently closed ledger, validates its existence, and returns its index and hash.", + "validation_points": [ + "XRPL_ASSERT(ledger, ...)" + ] + } + ], + "data_flows": [ + { + "field": "ledger", + "flow": [ + "context (RPC::JsonContext)", + "context.ledgerMaster", + "getClosedLedger()", + "ledger (pointer)", + "XRPL_ASSERT(ledger, ...)", + "ledger->header().seq / ledger->header().hash", + "jvResult[jss::ledger_index] / jvResult[jss::ledger_hash]", + "return jvResult" + ], + "origin": "context.ledgerMaster.getClosedLedger()", + "transformations": [ + "ledger is dereferenced after XRPL_ASSERT", + "ledger->header().seq and ledger->header().hash extracted", + "ledger->header().hash converted to string via to_string" + ], + "validated_at": "XRPL_ASSERT(ledger, ...)" + }, + { + "field": "ledger_index", + "flow": [ + "ledger->header().seq", + "jvResult[jss::ledger_index]" + ], + "origin": "ledger->header().seq", + "transformations": [ + "Direct assignment to JSON result" + ], + "validated_at": "ledger validated by XRPL_ASSERT" + }, + { + "field": "ledger_hash", + "flow": [ + "ledger->header().hash", + "to_string(ledger->header().hash)", + "jvResult[jss::ledger_hash]" + ], + "origin": "ledger->header().hash", + "transformations": [ + "Converted to string", + "Assigned to JSON result" + ], + "validated_at": "ledger validated by XRPL_ASSERT" + } + ], + "description": "Provides the implementation of the doLedgerClosed function, which returns information about the most recently closed ledger in JSON format.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger (closed ledger pointer)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT at doLedgerClosed", + "issue_pattern": "Missing empty string validation for ledger (closed ledger pointer)", + "why_false_positive": "XRPL_ASSERT validates ledger (closed ledger pointer) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doLedgerClosed" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is typically tested via RPC integration tests that call the 'ledger_closed' method and check the returned ledger_index and ledger_hash. Likely test files: rpc/ledger_closed_test.cpp, rpc/LedgerHandler_test.cpp, or similar. Unit tests for LedgerMaster::getClosedLedger may exist. Gaps: No explicit test for XRPL_ASSERT failure (i.e., if getClosedLedger returns null), as this is an invariant violation and would typically crash or abort in debug builds. No direct test for malformed or missing context, as context is constructed by the RPC framework.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "XRPL_ASSERT (custom assertion macro), jss:: (field naming, not validation)", + "notes": "No input or field validation is performed on user-supplied data in this handler. The only validation is an internal assertion that the closed ledger pointer is non-null. jss:: is used for JSON field naming, not validation. No template, type, range, or format validation is present in this code.", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "assertion failure (likely aborts or throws in debug)", + "field": "ledger (closed ledger pointer)", + "location": "doLedgerClosed", + "validated_by": "XRPL_ASSERT", + "validates": [ + "ledger is not null", + "closed ledger must exist before proceeding" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp.ai.md b/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp.ai.md new file mode 100644 index 0000000000..461cf0f69a --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerClosed.cpp.ai.md @@ -0,0 +1,50 @@ +# `LedgerClosed.cpp` — `ledger_closed` RPC Handler + +This file implements `doLedgerClosed`, the handler for the `ledger_closed` RPC command. It answers the narrowly scoped question: *what is the most recently closed ledger?* — returning exactly two fields, `ledger_index` and `ledger_hash`, with no user-supplied input parameters. + +## Ledger State Terminology + +The XRPL node maintains three conceptually distinct ledger states, each accessible through a sibling handler in this directory: + +- **Current** (`doLedgerCurrent`) — the open, in-progress ledger being assembled from incoming transactions. Not yet closed. +- **Closed** (`doLedgerClosed`, this file) — the ledger that has just completed the consensus process and been closed, but whose validation quorum has not necessarily been reached yet. Called the "finalized" or "accepted" ledger in `LedgerMaster.h`. +- **Validated** — the last ledger confirmed by a supermajority of trusted validators; the most authoritative state. + +The closed ledger is therefore a step ahead of the open one but a step behind the validated one. Clients that need a stable, consensus-confirmed reference point without waiting for full validation use this endpoint — for example, to track ledger sequence progression or confirm a transaction was included in a specific closed ledger. + +## Handler Registration and Precondition + +In `Handler.cpp`, `doLedgerClosed` is registered as: + +```cpp +{"ledger_closed", byRef(&doLedgerClosed), Role::USER, NEEDS_CLOSED_LEDGER}, +``` + +The `NEEDS_CLOSED_LEDGER` precondition is significant: the RPC dispatch framework checks that a closed ledger actually exists *before* invoking the handler. This makes the `XRPL_ASSERT` inside `doLedgerClosed` a true invariant guard — it cannot be triggered by normal client requests, only by a programming error in the framework itself. If the assertion ever fires, it represents a broken invariant (the precondition was satisfied but `getClosedLedger()` still returned null) rather than an expected failure mode. The handler is also `Role::USER`, meaning no admin authentication is required. + +## Implementation + +The handler is minimal by design: + +```cpp +Json::Value doLedgerClosed(RPC::JsonContext& context) +{ + auto ledger = context.ledgerMaster.getClosedLedger(); + XRPL_ASSERT(ledger, "xrpl::doLedgerClosed : non-null closed ledger"); + + Json::Value jvResult; + jvResult[jss::ledger_index] = ledger->header().seq; + jvResult[jss::ledger_hash] = to_string(ledger->header().hash); + return jvResult; +} +``` + +`LedgerMaster::getClosedLedger()` returns a `std::shared_ptr` from `mClosedLedger`, an internal cache of the last closed ledger. The `header()` accessor exposes the ledger's `seq` (a `uint32_t` sequence number written directly to JSON as an integer) and `hash` (a `uint256` converted to a lowercase hex string via `to_string`). The handler consumes no request parameters — the `context` is used only to reach `ledgerMaster`. + +## Contrast with `doLedgerCurrent` + +The peer handler `LedgerCurrent.cpp` is similarly trivial but exposes only `ledger_current_index` — no hash, because the open ledger's hash is undefined until it closes. `doLedgerClosed` returns both sequence and hash precisely because the closed ledger is immutable at query time. This asymmetry between the two handlers reflects a real protocol property: a closed ledger has a canonical identity, while the current one does not. + +## No Input Validation Needed + +Because this handler accepts no client-supplied fields, there is nothing to validate from the user's side. The `jss::` constants (`jss::ledger_index`, `jss::ledger_hash`) are compile-time string literals that serve as type-safe JSON field name keys, not runtime-validated inputs. The sole validation — the `XRPL_ASSERT` — guards an internal invariant, not external data. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp.ai.json b/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp.ai.json new file mode 100644 index 0000000000..d6e1b37a83 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp.ai.json @@ -0,0 +1,131 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RPC framework dispatches request", + "RPC::JsonHandler::process", + "xrpl::doLedgerCurrent" + ], + "entry_point": "doLedgerCurrent", + "purpose": "Handles the 'ledger_current' RPC command, returning the current ledger index.", + "validation_points": [ + "Template-based validation in RPC::JsonHandler::process (before doLedgerCurrent is called)" + ] + } + ], + "data_flows": [ + { + "field": "context", + "flow": [ + "RPC framework receives request", + "Parses JSON and builds RPC::JsonContext", + "Passes context to doLedgerCurrent" + ], + "origin": "RPC framework constructs RPC::JsonContext from incoming HTTP/JSON-RPC request", + "transformations": [ + "Context is constructed from request; may be validated by template framework before handler is called" + ], + "validated_at": "Template-based validation in RPC::JsonHandler::process" + }, + { + "field": "ledger_current_index", + "flow": [ + "doLedgerCurrent calls context.ledgerMaster.getCurrentLedgerIndex()", + "Value assigned to jvResult[jss::ledger_current_index]", + "jvResult returned as JSON response" + ], + "origin": "context.ledgerMaster.getCurrentLedgerIndex()", + "transformations": [ + "No transformation; value is directly fetched and inserted into result" + ], + "validated_at": "No explicit validation in doLedgerCurrent; assumes ledgerMaster is valid" + } + ], + "description": "Provides the implementation for retrieving the current ledger index in the XRPL server via the doLedgerCurrent RPC handler.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doLedgerCurrent" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The doLedgerCurrent handler is typically covered by RPC integration tests that issue the 'ledger_current' command and check the response. Likely test files: 'rpc/ledger_current_test.cpp', 'rpc/LedgerHandler_test.cpp', or similar. There is no explicit input validation in doLedgerCurrent itself; all validation is assumed to be handled by the RPC framework's template-based validation before the handler is invoked. Edge cases (e.g., malformed context, ledgerMaster not initialized) are not directly tested here and would rely on framework-level or integration tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld/rpc (jss::, RPC::JsonContext)", + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp.ai.md b/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp.ai.md new file mode 100644 index 0000000000..8f10296d37 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerCurrent.cpp.ai.md @@ -0,0 +1,39 @@ +# `LedgerCurrent.cpp` — RPC Handler for the Open Ledger Index + +## Role in the System + +This file implements `doLedgerCurrent`, the server-side handler for the `ledger_current` JSON-RPC command. Its entire job is to report the sequence number of the **open (in-progress) ledger** — the ledger that is currently accepting transactions and has not yet been closed and validated. + +It lives alongside a small family of related handlers in `src/xrpld/rpc/handlers/ledger/`: `LedgerClosed.cpp`, `Ledger.cpp`, `LedgerData.cpp`, `LedgerDiff.cpp`, `LedgerEntry.cpp`, and `LedgerHeader.cpp`. Each handler answers a distinct query about ledger state; `LedgerCurrent` answers the narrowest possible one. + +## What It Does and Why It's Simple + +```cpp +Json::Value +doLedgerCurrent(RPC::JsonContext& context) +{ + Json::Value jvResult; + jvResult[jss::ledger_current_index] = context.ledgerMaster.getCurrentLedgerIndex(); + return jvResult; +} +``` + +The handler makes a single call to `LedgerMaster::getCurrentLedgerIndex()` and packages the result under the `ledger_current_index` JSON key. There is no input parsing, no validation logic, and no error handling — and that sparseness is intentional. + +The open ledger has no finalized hash yet. Transactions are still being applied to it; its state root is not committed. Because there is nothing to identify the ledger other than its sequence number, the response carries only `ledger_current_index`. Compare this to `doLedgerClosed`, which returns both `ledger_index` and `ledger_hash` — the closed ledger has a deterministic, immutable hash that clients can use for consistency checks. The current ledger cannot offer that guarantee. + +This design means clients polling `ledger_current` get a lightweight, always-available answer: "the network is currently building ledger N." No lock on ledger state is needed beyond what `LedgerMaster` already manages internally. + +## Dependency on `RPC::JsonContext` + +All RPC handlers in this codebase receive an `RPC::JsonContext&` rather than raw parameters. The context bundles all server-side dependencies — including a reference to `LedgerMaster` — so handlers never need to reach into the application singleton directly. This keeps handlers easily testable and clearly scoped to what they actually touch. + +`LedgerMaster` is the authoritative source of ledger lifecycle state: which ledger is open, which is closed, and which is validated. `getCurrentLedgerIndex()` reads the current open ledger's `LedgerIndex` (a `uint32_t` sequence number) directly, without acquiring the ledger object itself. + +## Validation Architecture + +There are no RPC input parameters to validate — `ledger_current` takes no arguments — so the handler has no validation logic at all. The RPC framework's template-based dispatch (`RPC::JsonHandler::process`) performs request-level checks before invoking the handler, so by the time `doLedgerCurrent` runs, the context is guaranteed well-formed. The `jss::ledger_current_index` accessor is a compile-time string constant from the protocol's `jss` namespace, not a dynamic lookup, so there is no risk of key misspelling. + +## Summary + +`LedgerCurrent.cpp` is an intentionally minimal handler. Its simplicity reflects a real constraint in the ledger model: an open ledger has an identity (its sequence number) but not yet a fingerprint (its hash). Returning only the index is the correct and complete response, not an omission. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerData.cpp.ai.json b/src/xrpld/rpc/handlers/ledger/LedgerData.cpp.ai.json new file mode 100644 index 0000000000..8e133d9f18 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerData.cpp.ai.json @@ -0,0 +1,440 @@ +{ + "args": [ + { + "lineno": 18, + "name": "context" + }, + { + "lineno": 87, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doLedgerData", + "RPC::lookupLedger", + "RPC::chooseLedgerEntryType" + ], + "entry_point": "doLedgerData", + "purpose": "Handles the /ledger_data RPC request, validates input, fetches ledger, paginates state nodes, and returns results.", + "validation_points": [ + "RPC::lookupLedger (ledger existence/validity)", + "manual check (marker: isString + key.parseHex)", + "manual check (limit: isIntegral, range, maxLimit, isUnlimited)", + "RPC::chooseLedgerEntryType (type validation)" + ] + }, + { + "call_chain": [ + "doLedgerDataGrpc", + "RPC::ledgerFromRequest" + ], + "entry_point": "doLedgerDataGrpc", + "purpose": "Handles the gRPC GetLedgerData request, validates ledger, and (presumably) processes similar logic as doLedgerData.", + "validation_points": [ + "RPC::ledgerFromRequest (ledger existence/validity)" + ] + } + ], + "data_flows": [ + { + "field": "ledger", + "flow": [ + "context.params", + "RPC::lookupLedger (doLedgerData) / RPC::ledgerFromRequest (doLedgerDataGrpc)", + "lpLedger/ledger (shared_ptr)", + "used for ledger data extraction" + ], + "origin": "context.params (JSON) or context.params (gRPC)", + "transformations": [ + "Validated for existence and accessibility", + "Converted to ReadView pointer" + ], + "validated_at": "RPC::lookupLedger (doLedgerData), RPC::ledgerFromRequest (doLedgerDataGrpc)" + }, + { + "field": "marker", + "flow": [ + "context.params[jss::marker]", + "manual check: isString && key.parseHex", + "key (ReadView::key_type)", + "used as resume point in ledger traversal" + ], + "origin": "context.params[jss::marker]", + "transformations": [ + "Checked for string type", + "Parsed as hex to key_type" + ], + "validated_at": "manual check in doLedgerData" + }, + { + "field": "limit", + "flow": [ + "context.params[jss::limit]", + "manual check: isIntegral", + "limit = jLimit.asInt()", + "range check: (limit < 0) || (limit > maxLimit && !isUnlimited)", + "used to control number of ledger entries returned" + ], + "origin": "context.params[jss::limit]", + "transformations": [ + "Checked for integer type", + "Capped to maxLimit unless unlimited role" + ], + "validated_at": "manual check in doLedgerData" + }, + { + "field": "type", + "flow": [ + "context.params[jss::type]", + "RPC::chooseLedgerEntryType", + "type (used to filter ledger entries)" + ], + "origin": "context.params[jss::type]", + "transformations": [ + "Validated and mapped to internal ledger entry type" + ], + "validated_at": "RPC::chooseLedgerEntryType" + }, + { + "field": "binary", + "flow": [ + "context.params[jss::binary]", + "isBinary = params[jss::binary].asBool()", + "affects output format (binary vs JSON)" + ], + "origin": "context.params[jss::binary]", + "transformations": [ + "Converted to bool", + "Controls serialization format" + ], + "validated_at": "implicit (no explicit validation, but type conversion)" + } + ], + "description": "Implements the ledger_data RPC handler for XRPL, providing functions to retrieve state nodes from a ledger in both JSON and gRPC formats, with support for pagination, filtering, and binary output.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger (via lookupLedger)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger (via lookupLedger) validated by xrpld RPC framework (jss::, RPC::, Json::Value)", + "issue_pattern": "Missing validation for ledger (via lookupLedger)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, Json::Value) validates ledger (via lookupLedger) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "type (via chooseLedgerEntryType)", + "validation", + "missing", + "check" + ], + "evidence": "Field type (via chooseLedgerEntryType) validated by xrpld RPC framework (jss::, RPC::, Json::Value)", + "issue_pattern": "Missing validation for type (via chooseLedgerEntryType)", + "why_false_positive": "xrpld RPC framework (jss::, RPC::, Json::Value) validates type (via chooseLedgerEntryType) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doLedgerData", + "issue_pattern": "Missing empty string validation for ledger", + "why_false_positive": "RPC::lookupLedger validates ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "marker", + "empty", + "string", + "validation" + ], + "evidence": "manual check (isString + key.parseHex) at doLedgerData", + "issue_pattern": "Missing empty string validation for marker", + "why_false_positive": "manual check (isString + key.parseHex) validates marker for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "limit", + "empty", + "string", + "validation" + ], + "evidence": "manual check (isIntegral) at doLedgerData", + "issue_pattern": "Missing empty string validation for limit", + "why_false_positive": "manual check (isIntegral) validates limit for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "limit", + "type", + "validation", + "check" + ], + "evidence": "manual check (isIntegral) at doLedgerData", + "issue_pattern": "Missing type validation for limit", + "why_false_positive": "manual check (isIntegral) validates limit type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "limit", + "empty", + "string", + "validation" + ], + "evidence": "manual check (range, maxLimit, isUnlimited) at doLedgerData", + "issue_pattern": "Missing empty string validation for limit", + "why_false_positive": "manual check (range, maxLimit, isUnlimited) validates limit for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "type", + "empty", + "string", + "validation" + ], + "evidence": "RPC::chooseLedgerEntryType at doLedgerData", + "issue_pattern": "Missing empty string validation for type", + "why_false_positive": "RPC::chooseLedgerEntryType validates type for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ledger/LedgerData.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 18, + "name": "doLedgerData" + }, + { + "args": [ + "context" + ], + "lineno": 87, + "name": "doLedgerDataGrpc" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is in doLedgerData, which is typically tested via RPC integration tests. Likely test files: 'ledger_data_test.cpp', 'rpc_ledger_test.cpp', or similar in the test/rpc or test/ledger directories. Tests should cover valid/invalid ledger, marker, limit, and type values. Gaps may exist for edge cases (e.g., malformed marker, negative/overflowing limit, unknown type). Template-based validation is not fully shown here, so coverage of template-specific errors may be incomplete. gRPC path (doLedgerDataGrpc) may have less coverage if tests focus on JSON RPC.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger (via lookupLedger)", + "type (via chooseLedgerEntryType)" + ], + "framework": "xrpld RPC framework (jss::, RPC::, Json::Value)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "returns error Json::Value if ledger not found", + "field": "ledger", + "location": "doLedgerData", + "validated_by": "RPC::lookupLedger", + "validates": [ + "ledger existence", + "ledger accessibility" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::expected_field_error(jss::marker, \"valid\")", + "field": "marker", + "location": "doLedgerData", + "validated_by": "manual check (isString + key.parseHex)", + "validates": [ + "marker is string", + "marker is valid hex" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::expected_field_error(jss::limit, \"integer\")", + "field": "limit", + "location": "doLedgerData", + "validated_by": "manual check (isIntegral)", + "validates": [ + "limit is integer" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "limit is clamped to maxLimit if out of range", + "field": "limit", + "location": "doLedgerData", + "validated_by": "manual check (range, maxLimit, isUnlimited)", + "validates": [ + "limit is non-negative", + "limit does not exceed maxLimit unless unlimited role" + ], + "validation_type": "range|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcStatus.inject(jvResult) (error injected in result)", + "field": "type", + "location": "doLedgerData", + "validated_by": "RPC::chooseLedgerEntryType", + "validates": [ + "type is valid ledger entry type" + ], + "validation_type": "business_logic|type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerData.cpp.ai.md b/src/xrpld/rpc/handlers/ledger/LedgerData.cpp.ai.md new file mode 100644 index 0000000000..6dc9062488 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerData.cpp.ai.md @@ -0,0 +1,49 @@ +# LedgerData.cpp — `ledger_data` RPC Handler + +This file implements the `ledger_data` command, which exposes the raw state-node contents of a closed ledger for bulk retrieval. It is one of two mechanisms (alongside `ledger_entry` for single-object lookups) that let external clients download a complete ledger's state. Both a JSON-RPC entry point (`doLedgerData`) and a gRPC entry point (`doLedgerDataGrpc`) are provided in the same file. + +## Purpose and Design Context + +The XRP Ledger's state database is a sorted map of 256-bit keys to serialized ledger objects (SLEs). The `ledger_data` command exposes a paginated forward scan of this sorted map, returning up to a caller-controlled number of entries per call. The canonical use case is ledger synchronization and archival: a tool can walk the entire state of any available ledger sequentially by following the opaque `marker` token from response to response. + +## `doLedgerData` — JSON-RPC Path + +After resolving the target ledger via `RPC::lookupLedger`, the handler validates and interprets four optional parameters: + +**`marker`** is a hex-encoded 256-bit key that represents the resume position from a previous call. If present, it is decoded directly into a `ReadView::key_type` (a `uint256`). Rejection on malformed input is explicit via `RPC::expected_field_error`. + +**`limit`** controls how many entries to return. It defaults to `RPC::Tuning::pageLength(isBinary)`, which is 2048 for binary output and 256 for JSON. Callers without an "unlimited" role (`isUnlimited(context.role)`) have their limit silently clamped to this ceiling; privileged administrative clients may request larger pages. + +**`binary`** toggles the output format. In binary mode each entry is serialized via `serializeHex(*sle)`, producing a compact hex blob suitable for efficient transport. In JSON mode each entry is expanded via `sle->getJson(JsonOptions::none)`. The two formats share the same pagination logic but have distinct page-size limits — binary pages are 8× larger than JSON pages, reflecting the much higher cost of JSON serialization. + +**`type`** optionally filters the result set to a single `LedgerEntryType` (e.g., only `Offer` objects, only `RippleState` objects). `RPC::chooseLedgerEntryType` maps the string name to the internal enum and returns an error status for unknown types. When type is `ltANY`, no filtering occurs. + +### Pagination via Upper-Bound Iteration + +The traversal uses `lpLedger->sles.upper_bound(key)`, which returns an iterator to the first state entry with a key strictly greater than the resume marker. On the first call, `key` is the zero hash, so iteration starts from the beginning of the state map. + +The "limit exhausted" condition is handled with a deliberate off-by-one: when the counter `limit` reaches zero, the handler sets `marker = sle->key() - 1` and breaks. On the next call, `upper_bound(marker)` lands exactly on `sle->key()` again, so no entry is skipped and no entry is duplicated. This fence-post arithmetic is the only non-obvious invariant in the function. + +The entry read inside the loop uses `keylet::unchecked((*i)->key())` — an intentional bypass of keylet validation that is safe here because the key originates from the ledger's own state map, not from untrusted client input. + +### First-Page Header Injection + +When no marker is present (i.e., this is the first page of a scan), the response includes the full ledger header under the `jss::ledger` key via `getJson(LedgerFill(...))`. Subsequent pages skip this because the caller already has the header and repeating it would add noise to every continuation call. + +## `doLedgerDataGrpc` — gRPC Path + +The gRPC handler mirrors the JSON path but with several differences reflecting its different calling contract: + +**Binary-only output.** Every SLE is serialized with `sle->add(s)` into a `Serializer` buffer, then written raw into the protobuf response. There is no JSON-format option. + +**Fixed page limit.** The gRPC handler ignores any limit field in the request and always applies `RPC::Tuning::pageLength(true)` (2048 entries). Role-based limit expansion is absent. + +**Bidirectional range.** The gRPC request can include both a `marker` (start key) and an `end_marker` (end key), which sets the iterator endpoint `e` to `ledger->sles.upper_bound(*key)`. This bounds the scan from both ends, enabling the caller to split the keyspace into disjoint ranges and parallelize state downloads — a pattern that has no equivalent in the JSON API. + +Error codes map to gRPC status codes: `rpcINVALID_PARAMS` becomes `INVALID_ARGUMENT`; all other failures become `NOT_FOUND`. Marker bytes are written to the response protobuf rather than a JSON string field. + +## Resource and Safety Considerations + +Both functions hold the ledger alive via `std::shared_ptr` for the duration of iteration. Because `ReadView` is immutable and the sles map is accessed read-only, no locking is required inside the scan loop. The shared ownership model means the ledger cannot be evicted from memory mid-iteration even if the background ledger manager advances to a new ledger during processing. + +The `isUnlimited` role guard on the JSON path protects ordinary clients from requesting arbitrarily large pages that would serialize thousands of SLEs synchronously on the IO thread, while still allowing monitoring tools and internal administrative clients to walk ledgers efficiently. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp.ai.json b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp.ai.json new file mode 100644 index 0000000000..195cbb2128 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp.ai.json @@ -0,0 +1,379 @@ +{ + "args": [ + { + "lineno": 6, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doLedgerDiffGrpc", + "RPC::ledgerFromSpecifier", + "std::dynamic_pointer_cast", + "baseLedger->stateMap().compare" + ], + "entry_point": "doLedgerDiffGrpc", + "purpose": "Handles a gRPC request to compute the difference between two ledgers, validating both ledgers and the diff operation.", + "validation_points": [ + "RPC::ledgerFromSpecifier (validates base_ledger and desired_ledger existence)", + "std::dynamic_pointer_cast (validates that the ReadView is a validated Ledger)", + "baseLedger->stateMap().compare (validates that the diff is computable and not too large)" + ] + } + ], + "data_flows": [ + { + "field": "base_ledger", + "flow": [ + "request.base_ledger()", + "RPC::ledgerFromSpecifier(baseLedgerRv, ...)", + "std::dynamic_pointer_cast(baseLedgerRv)", + "baseLedger->stateMap()" + ], + "origin": "request.base_ledger() (from gRPC request)", + "transformations": [ + "Parsed from gRPC request", + "Looked up as a ledger (ReadView) via ledgerFromSpecifier", + "Casted to Ledger const* (must be validated ledger)" + ], + "validated_at": "RPC::ledgerFromSpecifier, std::dynamic_pointer_cast" + }, + { + "field": "desired_ledger", + "flow": [ + "request.desired_ledger()", + "RPC::ledgerFromSpecifier(desiredLedgerRv, ...)", + "std::dynamic_pointer_cast(desiredLedgerRv)", + "desiredLedger->stateMap()" + ], + "origin": "request.desired_ledger() (from gRPC request)", + "transformations": [ + "Parsed from gRPC request", + "Looked up as a ledger (ReadView) via ledgerFromSpecifier", + "Casted to Ledger const* (must be validated ledger)" + ], + "validated_at": "RPC::ledgerFromSpecifier, std::dynamic_pointer_cast" + }, + { + "field": "differences", + "flow": [ + "baseLedger->stateMap().compare(desiredLedger->stateMap(), differences, maxDifferences)", + "differences (SHAMap::Delta) populated", + "for loop over differences", + "response.mutable_ledger_objects()->add_objects()" + ], + "origin": "baseLedger->stateMap().compare(desiredLedger->stateMap(), ...)", + "transformations": [ + "Computed as a diff between two SHAMaps", + "Each difference is mapped to a proto response object" + ], + "validated_at": "baseLedger->stateMap().compare (returns false if too many differences)" + }, + { + "field": "include_blobs", + "flow": [ + "request.include_blobs()", + "if (request.include_blobs()) { diff->set_data(...) }" + ], + "origin": "request.include_blobs() (from gRPC request)", + "transformations": [ + "Boolean flag controls whether blob data is included in response" + ], + "validated_at": "No explicit validation (assumed boolean from proto)" + } + ], + "description": "Implements a gRPC handler function to compute and return the differences between two XRPL ledger states, given their specifiers, for use in the XRPL gRPC API.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "base_ledger", + "empty", + "string", + "validation" + ], + "evidence": "RPC::ledgerFromSpecifier at doLedgerDiffGrpc", + "issue_pattern": "Missing empty string validation for base_ledger", + "why_false_positive": "RPC::ledgerFromSpecifier validates base_ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "desired_ledger", + "empty", + "string", + "validation" + ], + "evidence": "RPC::ledgerFromSpecifier at doLedgerDiffGrpc", + "issue_pattern": "Missing empty string validation for desired_ledger", + "why_false_positive": "RPC::ledgerFromSpecifier validates desired_ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "base_ledger", + "empty", + "string", + "validation" + ], + "evidence": "std::dynamic_pointer_cast at doLedgerDiffGrpc", + "issue_pattern": "Missing empty string validation for base_ledger", + "why_false_positive": "std::dynamic_pointer_cast validates base_ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "base_ledger", + "type", + "validation", + "check" + ], + "evidence": "std::dynamic_pointer_cast at doLedgerDiffGrpc", + "issue_pattern": "Missing type validation for base_ledger", + "why_false_positive": "std::dynamic_pointer_cast validates base_ledger type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "desired_ledger", + "empty", + "string", + "validation" + ], + "evidence": "std::dynamic_pointer_cast at doLedgerDiffGrpc", + "issue_pattern": "Missing empty string validation for desired_ledger", + "why_false_positive": "std::dynamic_pointer_cast validates desired_ledger for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "desired_ledger", + "type", + "validation", + "check" + ], + "evidence": "std::dynamic_pointer_cast at doLedgerDiffGrpc", + "issue_pattern": "Missing type validation for desired_ledger", + "why_false_positive": "std::dynamic_pointer_cast validates desired_ledger type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "differences (between ledgers)", + "empty", + "string", + "validation" + ], + "evidence": "baseLedger->stateMap().compare at doLedgerDiffGrpc", + "issue_pattern": "Missing empty string validation for differences (between ledgers)", + "why_false_positive": "baseLedger->stateMap().compare validates differences (between ledgers) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "inDesired (ledger object blob)", + "empty", + "string", + "validation" + ], + "evidence": "XRPL_ASSERT(inDesired->size() > 0, ...) at doLedgerDiffGrpc", + "issue_pattern": "Missing empty string validation for inDesired (ledger object blob)", + "why_false_positive": "XRPL_ASSERT(inDesired->size() > 0, ...) validates inDesired (ledger object blob) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "inDesired (ledger object blob)", + "range", + "bounds", + "validation" + ], + "evidence": "XRPL_ASSERT(inDesired->size() > 0, ...) at doLedgerDiffGrpc", + "issue_pattern": "Missing range validation for inDesired (ledger object blob)", + "why_false_positive": "XRPL_ASSERT(inDesired->size() > 0, ...) validates inDesired (ledger object blob) range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 5, + "name": "doLedgerDiffGrpc" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ], + "test_coverage_notes": "The function doLedgerDiffGrpc is likely tested via integration or gRPC handler tests, possibly in files like test/rpc/ledger_diff_test.cpp or test/rpc/GRPCHandlers_test.cpp. Validation paths (ledger existence, type, diff size) should be covered by tests for missing/invalid ledgers and large diffs. However, edge cases such as malformed ledger specifiers, non-Ledger ReadViews, and extremely large diffs may not be exhaustively tested unless specifically targeted. There is no evidence in this file of direct unit tests; coverage depends on higher-level RPC/gRPC test suites.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (RPC::ledgerFromSpecifier, gRPC error handling, XRPL_ASSERT)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "grpc::Status(NOT_FOUND, 'base ledger not found')", + "field": "base_ledger", + "location": "doLedgerDiffGrpc", + "validated_by": "RPC::ledgerFromSpecifier", + "validates": [ + "Checks if the base_ledger specified in the request exists and can be loaded" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "grpc::Status(NOT_FOUND, 'desired ledger not found')", + "field": "desired_ledger", + "location": "doLedgerDiffGrpc", + "validated_by": "RPC::ledgerFromSpecifier", + "validates": [ + "Checks if the desired_ledger specified in the request exists and can be loaded" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "grpc::Status(NOT_FOUND, 'base ledger not validated')", + "field": "base_ledger", + "location": "doLedgerDiffGrpc", + "validated_by": "std::dynamic_pointer_cast", + "validates": [ + "Checks that the loaded base_ledger is of type Ledger and is validated" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "grpc::Status(NOT_FOUND, 'desired ledger not validated')", + "field": "desired_ledger", + "location": "doLedgerDiffGrpc", + "validated_by": "std::dynamic_pointer_cast", + "validates": [ + "Checks that the loaded desired_ledger is of type Ledger and is validated" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "grpc::Status(RESOURCE_EXHAUSTED, 'too many differences between specified ledgers')", + "field": "differences (between ledgers)", + "location": "doLedgerDiffGrpc", + "validated_by": "baseLedger->stateMap().compare", + "validates": [ + "Checks that the number of differences between ledgers does not exceed maxDifferences (int max)" + ], + "validation_type": "business_logic|range" + }, + { + "confidence": 0.9, + "error_thrown": "assertion failure (likely aborts process)", + "field": "inDesired (ledger object blob)", + "location": "doLedgerDiffGrpc", + "validated_by": "XRPL_ASSERT(inDesired->size() > 0, ...)", + "validates": [ + "Ensures that the ledger object blob in desired ledger is non-empty before using" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp.ai.md b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp.ai.md new file mode 100644 index 0000000000..eb36ec12fa --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerDiff.cpp.ai.md @@ -0,0 +1,36 @@ +# `LedgerDiff.cpp` — gRPC Ledger State Diff Handler + +This file implements the single gRPC handler `doLedgerDiffGrpc`, which answers the `GetLedgerDiff` RPC call. Its purpose is to expose the difference between the state maps of two arbitrary validated ledgers — essentially allowing clients to walk forward (or backward) through ledger history by seeing exactly which ledger objects were added, changed, or deleted between any two points. This is primarily used by state-synchronization clients, indexers, and validation tools that need efficient incremental updates rather than fetching full ledger snapshots. + +## Ledger Resolution and the Two-Phase Cast + +The handler accepts a `GetLedgerDiffRequest` carrying two `LedgerSpecifier` fields — `base_ledger` and `desired_ledger`. Resolution happens in two phases, and the distinction between them is architecturally important. + +The first phase calls `RPC::ledgerFromSpecifier`, which resolves the specifier (sequence number, hash, or shortcut like `validated`) into a `std::shared_ptr`. `ReadView` is the ledger abstraction layer that covers both fully validated closed ledgers and the current open ledger being built. Both would satisfy `ledgerFromSpecifier`. + +The second phase downcasts each `ReadView` to `std::shared_ptr` via `std::dynamic_pointer_cast`. This is not incidental — `Ledger` (as opposed to the general `ReadView` interface) is the concrete class that exposes `stateMap()`, providing access to the underlying `SHAMap` of account state objects. The open ledger does not have a finalized SHAMap in the same sense, and any non-`Ledger` `ReadView` (such as a transient view or the currently-building ledger) will yield a null shared pointer from the cast. The handler reports `NOT_FOUND / "base ledger not validated"` or `"desired ledger not validated"` in that case. Using the cast as a validation gate is cleaner than a separate flag on `ReadView` because it leverages C++ type safety rather than a boolean predicate that could drift out of sync. + +## SHAMap Diffing + +Once both `Ledger const` objects are in hand, the actual diff is computed by calling `baseLedger->stateMap().compare(desiredLedger->stateMap(), differences, maxDifferences)`. + +`SHAMap::Delta` is a `std::map` where each `DeltaItem` is a `std::pair` — the first element is the item as it exists in the base map, the second is the item in the desired map. A `nullptr` first element means the key was added; a `nullptr` second element means it was deleted; both non-null means the key was modified with a different serialized object. + +`maxDifferences` is set to `std::numeric_limits::max()`, effectively no cap. The `compare()` method still returns `false` if the internal walk exhausts `maxCount`, but with `INT_MAX` this only fires in genuinely pathological cases. The `RESOURCE_EXHAUSTED` gRPC status is preserved in that branch as a defensive measure against unexpected divergence between two supposedly nearby ledgers. + +## Response Encoding and the `include_blobs` Flag + +The loop over `differences` translates each `SHAMap::Delta` entry into a proto `LedgerObject` message. The key is always emitted (the `uint256` raw bytes identify which state object changed). The data blob — the serialized `STObject` payload — is only emitted when `request.include_blobs()` is true. This two-mode behavior lets callers perform cheap presence checks (just the key set) without the bandwidth cost of fetching every modified object's serialized form. + +An important asymmetry: only the *desired* side blob (`inDesired`) is ever included, never the *base* side. Clients learn the new state of each changed or added key, but deleted keys carry no data since there is no desired-state blob. This matches the typical use case (syncing forward to a new ledger state) but would require callers to do an additional object fetch if they need the old value of a modified key. + +The `XRPL_ASSERT(inDesired->size() > 0, ...)` before writing the blob guards against a zero-length `SHAMapItem`, which would indicate corruption in the SHAMap tree. Because `SHAMapItem` contains serialized ledger objects, zero bytes is never a valid item, and this assert catches any inadvertent construction of an empty item before it propagates to the wire. + +## Error Handling Summary + +Four distinct `grpc::Status` error codes are possible: +- `NOT_FOUND` for either specifier resolving to nothing +- `NOT_FOUND` for either specifier resolving to a non-`Ledger` `ReadView` +- `RESOURCE_EXHAUSTED` if `SHAMap::compare` returns false (diff exceeded `INT_MAX` entries) + +All are returned by value as part of the `std::pair` return type — no exceptions are thrown and no global state is mutated, consistent with the stateless gRPC handler convention used throughout the XRPL RPC layer. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp.ai.json b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp.ai.json new file mode 100644 index 0000000000..750a95902a --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp.ai.json @@ -0,0 +1,1199 @@ +{ + "args": [ + { + "lineno": 19, + "name": "keylet" + }, + { + "lineno": 19, + "name": "params" + }, + { + "lineno": 19, + "name": "fieldName" + }, + { + "lineno": 19, + "name": "apiVersion" + }, + { + "lineno": 27, + "name": "params" + }, + { + "lineno": 27, + "name": "fieldName" + }, + { + "lineno": 27, + "name": "apiVersion" + }, + { + "lineno": 38, + "name": "params" + }, + { + "lineno": 38, + "name": "fieldName" + }, + { + "lineno": 38, + "name": "expectedType" + }, + { + "lineno": 48, + "name": "params" + }, + { + "lineno": 48, + "name": "fieldName" + }, + { + "lineno": 48, + "name": "apiVersion" + }, + { + "lineno": 67, + "name": "params" + }, + { + "lineno": 67, + "name": "fieldName" + }, + { + "lineno": 67, + "name": "apiVersion" + }, + { + "lineno": 80, + "name": "params" + }, + { + "lineno": 80, + "name": "fieldName" + }, + { + "lineno": 80, + "name": "apiVersion" + }, + { + "lineno": 102, + "name": "params" + }, + { + "lineno": 102, + "name": "fieldName" + }, + { + "lineno": 102, + "name": "apiVersion" + }, + { + "lineno": 130, + "name": "params" + }, + { + "lineno": 130, + "name": "fieldName" + }, + { + "lineno": 130, + "name": "apiVersion" + }, + { + "lineno": 136, + "name": "cred" + }, + { + "lineno": 136, + "name": "fieldName" + }, + { + "lineno": 136, + "name": "apiVersion" + }, + { + "lineno": 154, + "name": "params" + }, + { + "lineno": 154, + "name": "fieldName" + }, + { + "lineno": 154, + "name": "apiVersion" + }, + { + "lineno": 172, + "name": "jv" + }, + { + "lineno": 210, + "name": "dp" + }, + { + "lineno": 210, + "name": "fieldName" + }, + { + "lineno": 210, + "name": "apiVersion" + }, + { + "lineno": 246, + "name": "params" + }, + { + "lineno": 246, + "name": "fieldName" + }, + { + "lineno": 246, + "name": "apiVersion" + }, + { + "lineno": 259, + "name": "params" + }, + { + "lineno": 259, + "name": "fieldName" + }, + { + "lineno": 259, + "name": "apiVersion" + }, + { + "lineno": 292, + "name": "params" + }, + { + "lineno": 292, + "name": "fieldName" + }, + { + "lineno": 292, + "name": "apiVersion" + }, + { + "lineno": 308, + "name": "keylet" + }, + { + "lineno": 308, + "name": "params" + }, + { + "lineno": 308, + "name": "fieldName" + }, + { + "lineno": 308, + "name": "apiVersion" + }, + { + "lineno": 319, + "name": "params" + }, + { + "lineno": 319, + "name": "fieldName" + }, + { + "lineno": 319, + "name": "apiVersion" + }, + { + "lineno": 332, + "name": "params" + }, + { + "lineno": 332, + "name": "fieldName" + }, + { + "lineno": 332, + "name": "apiVersion" + }, + { + "lineno": 347, + "name": "params" + }, + { + "lineno": 347, + "name": "fieldName" + }, + { + "lineno": 347, + "name": "apiVersion" + }, + { + "lineno": 362, + "name": "params" + }, + { + "lineno": 362, + "name": "fieldName" + }, + { + "lineno": 362, + "name": "apiVersion" + }, + { + "lineno": 378, + "name": "params" + }, + { + "lineno": 378, + "name": "fieldName" + }, + { + "lineno": 378, + "name": "apiVersion" + }, + { + "lineno": 389, + "name": "params" + }, + { + "lineno": 389, + "name": "fieldName" + }, + { + "lineno": 389, + "name": "apiVersion" + }, + { + "lineno": 395, + "name": "params" + }, + { + "lineno": 395, + "name": "fieldName" + }, + { + "lineno": 395, + "name": "apiVersion" + }, + { + "lineno": 401, + "name": "params" + }, + { + "lineno": 401, + "name": "fieldName" + }, + { + "lineno": 401, + "name": "apiVersion" + }, + { + "lineno": 417, + "name": "params" + }, + { + "lineno": 417, + "name": "fieldName" + }, + { + "lineno": 417, + "name": "apiVersion" + }, + { + "lineno": 431, + "name": "params" + }, + { + "lineno": 431, + "name": "fieldName" + }, + { + "lineno": 431, + "name": "apiVersion" + }, + { + "lineno": 437, + "name": "pd" + }, + { + "lineno": 437, + "name": "fieldName" + }, + { + "lineno": 437, + "name": "apiVersion" + }, + { + "lineno": 454, + "name": "jvRippleState" + }, + { + "lineno": 454, + "name": "fieldName" + }, + { + "lineno": 454, + "name": "apiVersion" + }, + { + "lineno": 484, + "name": "params" + }, + { + "lineno": 484, + "name": "fieldName" + }, + { + "lineno": 484, + "name": "apiVersion" + }, + { + "lineno": 490, + "name": "params" + }, + { + "lineno": 490, + "name": "fieldName" + }, + { + "lineno": 490, + "name": "apiVersion" + }, + { + "lineno": 504, + "name": "params" + }, + { + "lineno": 504, + "name": "fieldName" + }, + { + "lineno": 504, + "name": "apiVersion" + }, + { + "lineno": 518, + "name": "claim_id" + }, + { + "lineno": 518, + "name": "fieldName" + }, + { + "lineno": 518, + "name": "apiVersion" + }, + { + "lineno": 540, + "name": "claim_id" + }, + { + "lineno": 540, + "name": "fieldName" + }, + { + "lineno": 540, + "name": "apiVersion" + }, + { + "lineno": 563, + "name": "context" + }, + { + "lineno": 646, + "name": "context" + } + ], + "classes": [ + { + "args": [], + "lineno": 558, + "name": "LedgerEntry" + } + ], + "code_paths": [ + { + "call_chain": [ + "parseIndex", + "parseObjectID", + "LedgerEntryHelpers::parse" + ], + "entry_point": "parseIndex", + "purpose": "Parses a ledger entry index from params, handling both string and object forms, and dispatches to fixed keylets or hex parsing.", + "validation_points": [ + "params.isString() in parseIndex", + "index == jss::amendments.c_str(), etc. in parseIndex", + "LedgerEntryHelpers::parse in parseObjectID" + ] + }, + { + "call_chain": [ + "parseAccountRoot", + "LedgerEntryHelpers::parse" + ], + "entry_point": "parseAccountRoot", + "purpose": "Parses an AccountRoot ledger entry, validating the AccountID.", + "validation_points": [ + "LedgerEntryHelpers::parse in parseAccountRoot" + ] + }, + { + "call_chain": [ + "parseAMM", + "LedgerEntryHelpers::hasRequired", + "LedgerEntryHelpers::requiredAsset (twice)" + ], + "entry_point": "parseAMM", + "purpose": "Parses an AMM ledger entry, validating required asset fields.", + "validation_points": [ + "params.isObject() in parseAMM", + "LedgerEntryHelpers::hasRequired in parseAMM", + "LedgerEntryHelpers::requiredAsset in parseAMM" + ] + }, + { + "call_chain": [ + "fixed", + "parseFixed" + ], + "entry_point": "fixed", + "purpose": "Handles fixed-location ledger entries (e.g., amendments, fees) with no parameterization.", + "validation_points": [ + "parseFixed (validates params as needed)" + ] + }, + { + "call_chain": [ + "parseObjectID", + "LedgerEntryHelpers::parse" + ], + "entry_point": "parseObjectID", + "purpose": "Parses a generic object ID from params, expects a hex string or object.", + "validation_points": [ + "LedgerEntryHelpers::parse in parseObjectID" + ] + } + ], + "data_flows": [ + { + "field": "params", + "flow": [ + "RPC request", + "parseIndex/parseAccountRoot/parseAMM/etc.", + "LedgerEntryHelpers::parse or other helpers", + "keylet::* or error" + ], + "origin": "RPC request JSON", + "transformations": [ + "Type checks (isString, isObject)", + "Field extraction (asString, asObject)", + "Parsing (hex decode, AccountID parse, Asset parse)" + ], + "validated_at": "First step in each parse* function (e.g., params.isString(), LedgerEntryHelpers::parse)" + }, + { + "field": "index", + "flow": [ + "params.asString()", + "compared to jss::amendments, jss::fee, etc.", + "if not matched, passed to parseObjectID" + ], + "origin": "params.asString() in parseIndex", + "transformations": [ + "String comparison", + "Dispatch to fixed keylet or hex parsing" + ], + "validated_at": "parseIndex (string comparison and fallback to parseObjectID)" + }, + { + "field": "AccountID", + "flow": [ + "params", + "LedgerEntryHelpers::parse", + "keylet::account(*account).key" + ], + "origin": "params (could be string or object)", + "transformations": [ + "AccountID parsing (base58 decode, validation)" + ], + "validated_at": "LedgerEntryHelpers::parse in parseAccountRoot" + }, + { + "field": "asset, asset2", + "flow": [ + "params", + "LedgerEntryHelpers::hasRequired (checks presence)", + "LedgerEntryHelpers::requiredAsset (parses and validates)", + "keylet::amm(*asset, *asset2).key" + ], + "origin": "params[\"asset\"], params[\"asset2\"] in parseAMM", + "transformations": [ + "Presence check", + "Asset parsing and validation" + ], + "validated_at": "LedgerEntryHelpers::hasRequired and LedgerEntryHelpers::requiredAsset in parseAMM" + } + ], + "description": "This file implements the logic for parsing and handling various types of ledger entries in the XRPL (XRP Ledger) for the ledger_entry RPC command, including parsing input parameters, computing ledger entry indices, and returning ledger entry data in JSON or binary format. It also provides a gRPC handler for ledger entry retrieval.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "params (type and format via LedgerEntryHelpers::parse)", + "validation", + "missing", + "check" + ], + "evidence": "Field params (type and format via LedgerEntryHelpers::parse) validated by LedgerEntryHelpers, jss:: (JSON field names), Keylet, template-based parsing", + "issue_pattern": "Missing validation for params (type and format via LedgerEntryHelpers::parse)", + "why_false_positive": "LedgerEntryHelpers, jss:: (JSON field names), Keylet, template-based parsing validates params (type and format via LedgerEntryHelpers::parse) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "special string fields (amendments, fee, nunl, hashes via jss:: constants)", + "validation", + "missing", + "check" + ], + "evidence": "Field special string fields (amendments, fee, nunl, hashes via jss:: constants) validated by LedgerEntryHelpers, jss:: (JSON field names), Keylet, template-based parsing", + "issue_pattern": "Missing validation for special string fields (amendments, fee, nunl, hashes via jss:: constants)", + "why_false_positive": "LedgerEntryHelpers, jss:: (JSON field names), Keylet, template-based parsing validates special string fields (amendments, fee, nunl, hashes via jss:: constants) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "params", + "empty", + "string", + "validation" + ], + "evidence": "LedgerEntryHelpers::parse at parseObjectID", + "issue_pattern": "Missing empty string validation for params", + "why_false_positive": "LedgerEntryHelpers::parse validates params for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "params", + "empty", + "string", + "validation" + ], + "evidence": "LedgerEntryHelpers::parse at parseAccountRoot", + "issue_pattern": "Missing empty string validation for params", + "why_false_positive": "LedgerEntryHelpers::parse validates params for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "params", + "empty", + "string", + "validation" + ], + "evidence": "params.isString() at parseIndex", + "issue_pattern": "Missing empty string validation for params", + "why_false_positive": "params.isString() validates params for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "params", + "type", + "validation", + "check" + ], + "evidence": "params.isString() at parseIndex", + "issue_pattern": "Missing type validation for params", + "why_false_positive": "params.isString() validates params type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "params", + "empty", + "string", + "validation" + ], + "evidence": "index == jss::amendments.c_str(), index == jss::fee.c_str(), etc. at parseIndex", + "issue_pattern": "Missing empty string validation for params", + "why_false_positive": "index == jss::amendments.c_str(), index == jss::fee.c_str(), etc. validates params for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "params", + "empty", + "string", + "validation" + ], + "evidence": "parseFixed at fixed (lambda), parseFixed", + "issue_pattern": "Missing empty string validation for params", + "why_false_positive": "parseFixed validates params for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "error_handling" + ], + "confidence": 0.8, + "detection_keywords": [ + "error", + "return", + "value", + "check" + ], + "evidence": "Code uses throw statements", + "issue_pattern": "Missing error return value", + "why_false_positive": "Function uses exceptions for error reporting, not return codes" + }, + { + "applies_to": [ + "error_handling", + "return_value" + ], + "confidence": 0.82, + "detection_keywords": [ + "error", + "code", + "return", + "status" + ], + "evidence": "Code uses exception-based error handling", + "issue_pattern": "Function does not return error code", + "why_false_positive": "Errors are reported via exceptions, not return values" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp", + "functions": [ + { + "args": [ + "Keylet const& keylet", + "Json::Value const& params", + "Json::StaticString const& fieldName", + "unsigned const apiVersion" + ], + "lineno": 19, + "name": "parseFixed" + }, + { + "args": [ + "Keylet const& keylet" + ], + "lineno": 27, + "name": "fixed" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "std::string const& expectedType" + ], + "lineno": 38, + "name": "parseObjectID" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 48, + "name": "parseIndex" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 67, + "name": "parseAccountRoot" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 80, + "name": "parseAMM" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 102, + "name": "parseBridge" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 130, + "name": "parseCheck" + }, + { + "args": [ + "Json::Value const& cred", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 136, + "name": "parseCredential" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 154, + "name": "parseDelegate" + }, + { + "args": [ + "Json::Value const& jv" + ], + "lineno": 172, + "name": "parseAuthorizeCredentials" + }, + { + "args": [ + "Json::Value const& dp", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 210, + "name": "parseDepositPreauth" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 246, + "name": "parseDID" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 259, + "name": "parseDirectoryNode" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 292, + "name": "parseEscrow" + }, + { + "args": [ + "Keylet const& keylet", + "Json::Value const& params", + "Json::StaticString const& fieldName", + "unsigned const apiVersion" + ], + "lineno": 308, + "name": "parseFixed" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 319, + "name": "parseLedgerHashes" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 332, + "name": "parseLoanBroker" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 347, + "name": "parseLoan" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 362, + "name": "parseMPToken" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 378, + "name": "parseMPTokenIssuance" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 389, + "name": "parseNFTokenOffer" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 395, + "name": "parseNFTokenPage" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 401, + "name": "parseOffer" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 417, + "name": "parseOracle" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 431, + "name": "parsePayChannel" + }, + { + "args": [ + "Json::Value const& pd", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 437, + "name": "parsePermissionedDomain" + }, + { + "args": [ + "Json::Value const& jvRippleState", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 454, + "name": "parseRippleState" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 484, + "name": "parseSignerList" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 490, + "name": "parseTicket" + }, + { + "args": [ + "Json::Value const& params", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 504, + "name": "parseVault" + }, + { + "args": [ + "Json::Value const& claim_id", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 518, + "name": "parseXChainOwnedClaimID" + }, + { + "args": [ + "Json::Value const& claim_id", + "Json::StaticString const fieldName", + "unsigned const apiVersion" + ], + "lineno": 540, + "name": "parseXChainOwnedCreateAccountClaimID" + }, + { + "args": [ + "RPC::JsonContext& context" + ], + "lineno": 563, + "name": "doLedgerEntry" + }, + { + "args": [ + "RPC::GRPCContext& context" + ], + "lineno": 646, + "name": "doLedgerEntryGrpc" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Errors reported via exceptions, not return codes", + "false_positive_risk": "Missing error return value", + "pattern": "throw statement", + "type": "exception_throwing" + }, + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 15, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by unit tests for the LedgerEntry RPC handler, especially for valid/invalid params, malformed AccountID, missing fields, and fixed keylet lookups. Tests should exist in files like 'ledger_entry_test.cpp', 'rpc_ledger_entry_test.cpp', or similar. However, edge cases such as malformed nested objects, unexpected types, or rare keylet types may not be fully covered. Template-based validation and error reporting via exceptions may be harder to test for all error paths. There may be gaps in coverage for API version-specific logic (e.g., apiVersion > 2u in parseIndex) and for all possible permutations of malformed input.", + "validation_architecture": { + "auto_validated_fields": [ + "params (type and format via LedgerEntryHelpers::parse)", + "special string fields (amendments, fee, nunl, hashes via jss:: constants)" + ], + "framework": "LedgerEntryHelpers, jss:: (JSON field names), Keylet, template-based parsing", + "validation_layer": "business_logic (handler-level, not entry-point)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "LedgerEntryHelpers::invalidFieldError (returns Json::Value error)", + "field": "params", + "location": "parseObjectID", + "validated_by": "LedgerEntryHelpers::parse", + "validates": [ + "Checks if params can be parsed as a uint256 (hex string or object)", + "Ensures correct type/format for ledger object ID" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "LedgerEntryHelpers::invalidFieldError (returns Json::Value error)", + "field": "params", + "location": "parseAccountRoot", + "validated_by": "LedgerEntryHelpers::parse", + "validates": [ + "Checks if params can be parsed as an AccountID (address format validation)" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "None (branches to parseObjectID if not string)", + "field": "params", + "location": "parseIndex", + "validated_by": "params.isString()", + "validates": [ + "Checks if params is a string (for special index keywords)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "None (returns fixed keylet if matches, else falls through)", + "field": "params", + "location": "parseIndex", + "validated_by": "index == jss::amendments.c_str(), index == jss::fee.c_str(), etc.", + "validates": [ + "Checks if string matches special ledger entry keywords (amendments, fee, nunl, hashes)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "LedgerEntryHelpers::invalidFieldError (returns Json::Value error)", + "field": "params", + "location": "fixed (lambda), parseFixed", + "validated_by": "parseFixed", + "validates": [ + "Checks if fixed-keylet objects are correctly referenced (no extra parameters needed)" + ], + "validation_type": "business_logic|type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp.ai.md b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp.ai.md new file mode 100644 index 0000000000..2d22d08b27 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp.ai.md @@ -0,0 +1,47 @@ +# `LedgerEntry.cpp` — `ledger_entry` RPC Handler + +## Role in the System + +This file implements the `ledger_entry` RPC command, the primary mechanism for clients to read individual objects directly from the XRP Ledger state tree. Given a target ledger and a descriptor for one specific ledger state object (SLE), the handler computes the object's `uint256` index key, fetches it from the `ReadView`, and returns it either as JSON or as its raw serialized bytes. A parallel `doLedgerEntryGrpc` function handles the same lookup over gRPC, accepting a raw key rather than semantic parameters. + +## Parser Table Architecture + +The file's dominant design decision is the dispatch table. `doLedgerEntry` builds a static `std::array` where each element holds three things: the JSON field name that signals which entry type is requested, a `FunctionType` parser callable, and the `LedgerEntryType` enum value used to type-check the result. Rather than maintaining this list manually, the table is populated via the `LEDGER_ENTRY` X-macro from ``, which is the single authoritative enumeration of all ledger entry types. The macro expands to `{jss::rpcName, parse##name, tag}` entries, so adding a new ledger entry type to the macro file automatically ensures it appears in this handler. + +Two extra aliases — `account_root` for `parseAccountRoot` and `ripple_state` for `parseRippleState` — are appended by hand after the macro block, preserving backward compatibility with historical field names that predate the standardized naming convention. A generic `index` entry is also appended, allowing direct lookup by hex key with an optional human-readable alias in API v3+. + +## The `Expected` Pattern + +Every parser function is a `FunctionType`: `(Json::Value const&, Json::StaticString, unsigned apiVersion) → Expected`. On success the parser returns the computed `uint256` key; on failure it returns an `Unexpected` carrying a fully-formed error JSON object. `doLedgerEntry` checks the result and short-circuits immediately on error, propagating the error JSON directly as the RPC response. This avoids the exception overhead in the hot parsing path while keeping each parser self-contained and composable — the caller never inspects parser internals, only the `Expected` outcome. + +## Dual-Form Input + +Nearly every parser accepts its value in two forms: a raw hex string (direct key lookup) or a structured JSON object (semantic derivation via `keylet::*`). `parseObjectID` handles the hex case uniformly. The structured case requires type-specific logic — for example, `parseOffer` extracts `{account, seq}` and calls `keylet::offer(*id, *seq)`, while `parseRippleState` extracts a 2-element `accounts` array plus a `currency` string and calls `keylet::line(*id1, *id2, uCurrency)`. This dual-form design lets clients request entries by semantic description without knowing (or computing) the internal hash. + +## Fixed-Location Singletons + +Three ledger objects are singletons with well-known fixed keys — Amendments, Fee Settings, and Negative UNL. The `fixed(Keylet)` factory captures a `Keylet` by value and returns a lambda that calls `parseFixed`. `parseFixed` either validates a `true` boolean value (meaning "fetch this singleton") or falls through to hex-string parsing. The `parseAmendments`, `parseFeeSettings`, and `parseNegativeUNL` constants are built this way at static-initialization time. In API v3+, `parseIndex` also accepts plain strings (`"amendments"`, `"fee"`, `"nunl"`, `"hashes"`) to reach these singletons without knowing their key hashes. + +## The Bridge Parser's Special Case + +`parseBridge` is architecturally distinct from every other parser: it requires two sibling fields in the top-level params (`bridge` and `bridge_account`) rather than reading from a single nested sub-object. To handle this, `doLedgerEntry` has an explicit check — when `fieldName == jss::bridge`, it passes the entire `context.params` object to the parser rather than `context.params[fieldName]`. This is the only place in the dispatch loop where the parser receives more than its own sub-field's value. + +## `parseDepositPreauth` — Credential Sorting Invariant + +The deposit preauth parser enforces an exclusive-or constraint: exactly one of `authorized` or `authorized_credentials` must be present. The credential-array path invokes the helper `parseAuthorizeCredentials`, which validates array bounds (non-empty, not longer than `maxCredentialsArraySize`) and constructs an `STArray` of `sfCredential` objects. Before computing the keylet, the array is sorted by `credentials::makeSorted` — this is not cosmetic. The keylet for a credential-based deposit preauth is keyed on a canonical sorted representation, so supplying credentials in any order must resolve to the same ledger object. An empty sorted result is treated as a malformed input. + +## `LedgerEntryHelpers` and Template-Based Parsing + +All primitive extraction is delegated to the `LedgerEntryHelpers` namespace defined in `LedgerEntryHelpers.h`. It provides explicit specializations of `parse()` for `AccountID`, `uint256`, `uint192`, `uint32_t`, and `Asset`, plus `parseHexBlob` for variable-length binary fields. The `required` template combines a presence check (`isMember` + `isNull`) with a type parse, producing a consistent `Expected` with a uniform error structure (`error`, `error_code: rpcINVALID_PARAMS`, `error_message`). The `requiredAccountID`, `requiredUInt32`, `requiredUInt256`, `requiredUInt192`, and `requiredAsset` wrappers then forward to `required` with the appropriate error tag string. Because these are all inline functions in the header, the parse chain compiles down without any call overhead. + +## Post-Fetch Type Validation + +After a parser computes a key and `doLedgerEntry` fetches the SLE from the ledger, it cross-checks the SLE's actual `LedgerEntryType` against the `expectedType` stored in the dispatch table entry. If they differ (and `expectedType` is not `ltANY`), the handler returns `rpcUNEXPECTED_LEDGER_TYPE` rather than the node. This matters most for the raw-hex and `index` paths: a client could supply a valid hex key that happens to belong to a different object type. The check ensures that field-typed parsers (`ripple_state`, `offer`, etc.) only return objects of the expected type, preventing silent misinterpretation. + +## API Version Branching + +Version-specific behavior appears in two places. `parseIndex` only recognizes the human-readable singleton aliases (`"amendments"`, etc.) when `apiVersion > 2`. `doLedgerEntry` returns the legacy `"unknownOption"` error for API v1 when no recognized field is present, but returns a structured `make_param_error` in v2+. `Json::error` exceptions during parsing are re-thrown in v1 (preserving historical behavior) but caught and translated to `rpcINVALID_PARAMS` in v2+, preventing unstructured exception propagation to newer clients. + +## gRPC Variant + +`doLedgerEntryGrpc` performs a stripped-down version of the same operation — it takes a raw 32-byte key from the protobuf request, resolves the ledger via `RPC::ledgerFromRequest`, and returns the SLE serialized with a `Serializer`. There is no semantic parsing, no type checking, and no JSON/binary toggle: the gRPC path always returns bytes. Errors are mapped to gRPC status codes (`INVALID_ARGUMENT` for bad params, `NOT_FOUND` for ledger or entry not found). \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h.ai.json b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h.ai.json new file mode 100644 index 0000000000..be74095e5d --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h.ai.json @@ -0,0 +1,344 @@ +{ + "args": [ + { + "lineno": 12, + "name": "field" + }, + { + "lineno": 12, + "name": "err" + }, + { + "lineno": 20, + "name": "err" + }, + { + "lineno": 20, + "name": "field" + }, + { + "lineno": 20, + "name": "type" + }, + { + "lineno": 28, + "name": "err" + }, + { + "lineno": 28, + "name": "message" + }, + { + "lineno": 36, + "name": "params" + }, + { + "lineno": 37, + "name": "fields" + }, + { + "lineno": 38, + "name": "err" + }, + { + "lineno": 49, + "name": "param" + }, + { + "lineno": 53, + "name": "params" + }, + { + "lineno": 54, + "name": "fieldName" + }, + { + "lineno": 55, + "name": "err" + }, + { + "lineno": 56, + "name": "expectedType" + }, + { + "lineno": 64, + "name": "param" + }, + { + "lineno": 75, + "name": "params" + }, + { + "lineno": 76, + "name": "fieldName" + }, + { + "lineno": 77, + "name": "err" + }, + { + "lineno": 81, + "name": "param" + }, + { + "lineno": 81, + "name": "maxLength" + }, + { + "lineno": 90, + "name": "params" + }, + { + "lineno": 91, + "name": "fieldName" + }, + { + "lineno": 92, + "name": "maxLength" + }, + { + "lineno": 93, + "name": "err" + }, + { + "lineno": 102, + "name": "param" + }, + { + "lineno": 114, + "name": "params" + }, + { + "lineno": 115, + "name": "fieldName" + }, + { + "lineno": 116, + "name": "err" + }, + { + "lineno": 120, + "name": "param" + }, + { + "lineno": 128, + "name": "params" + }, + { + "lineno": 129, + "name": "fieldName" + }, + { + "lineno": 130, + "name": "err" + }, + { + "lineno": 134, + "name": "param" + }, + { + "lineno": 142, + "name": "params" + }, + { + "lineno": 143, + "name": "fieldName" + }, + { + "lineno": 144, + "name": "err" + }, + { + "lineno": 148, + "name": "param" + }, + { + "lineno": 156, + "name": "params" + }, + { + "lineno": 157, + "name": "fieldName" + }, + { + "lineno": 158, + "name": "err" + }, + { + "lineno": 160, + "name": "params" + } + ], + "classes": [], + "description": "This file provides helper functions for parsing and validating fields in JSON RPC requests related to ledger entries in the XRPL codebase. It includes utilities for extracting and validating AccountIDs, blobs, numbers, hashes, assets, and bridge fields from JSON parameters, returning appropriate error objects when validation fails.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h", + "functions": [ + { + "args": [ + "field", + "err" + ], + "lineno": 11, + "name": "missingFieldError" + }, + { + "args": [ + "err", + "field", + "type" + ], + "lineno": 19, + "name": "invalidFieldError" + }, + { + "args": [ + "err", + "message" + ], + "lineno": 27, + "name": "malformedError" + }, + { + "args": [ + "params", + "fields", + "err" + ], + "lineno": 35, + "name": "hasRequired" + }, + { + "args": [ + "param" + ], + "lineno": 48, + "name": "parse" + }, + { + "args": [ + "params", + "fieldName", + "err", + "expectedType" + ], + "lineno": 52, + "name": "required" + }, + { + "args": [ + "param" + ], + "lineno": 63, + "name": "parse" + }, + { + "args": [ + "params", + "fieldName", + "err" + ], + "lineno": 74, + "name": "requiredAccountID" + }, + { + "args": [ + "param", + "maxLength" + ], + "lineno": 80, + "name": "parseHexBlob" + }, + { + "args": [ + "params", + "fieldName", + "maxLength", + "err" + ], + "lineno": 89, + "name": "requiredHexBlob" + }, + { + "args": [ + "param" + ], + "lineno": 101, + "name": "parse" + }, + { + "args": [ + "params", + "fieldName", + "err" + ], + "lineno": 113, + "name": "requiredUInt32" + }, + { + "args": [ + "param" + ], + "lineno": 119, + "name": "parse" + }, + { + "args": [ + "params", + "fieldName", + "err" + ], + "lineno": 127, + "name": "requiredUInt256" + }, + { + "args": [ + "param" + ], + "lineno": 133, + "name": "parse" + }, + { + "args": [ + "params", + "fieldName", + "err" + ], + "lineno": 141, + "name": "requiredUInt192" + }, + { + "args": [ + "param" + ], + "lineno": 147, + "name": "parse" + }, + { + "args": [ + "params", + "fieldName", + "err" + ], + "lineno": 155, + "name": "requiredAsset" + }, + { + "args": [ + "params" + ], + "lineno": 159, + "name": "parseBridgeFields" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + }, + { + "lineno": 10, + "name": "LedgerEntryHelpers" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h.ai.md b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h.ai.md new file mode 100644 index 0000000000..5a68c5fe66 --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntryHelpers.h.ai.md @@ -0,0 +1,43 @@ +# LedgerEntryHelpers.h + +## Role in the System + +`LedgerEntryHelpers.h` is a header-only utility namespace (`xrpl::LedgerEntryHelpers`) that provides the low-level JSON parsing and validation vocabulary for the `ledger_entry` RPC command. It exists because `LedgerEntry.cpp` must dispatch across roughly twenty-five distinct ledger object types — each with its own set of required fields, type constraints, and error messages — and sharing that plumbing in a single place keeps the per-type parse functions in `LedgerEntry.cpp` readable and consistent. No other translation unit includes this header; it is private infrastructure for one consumer. + +## The Two-Layer Parse Pattern + +The central design is a pair of composable layers: a low-level `parse()` template family and a higher-level `required()` combinator. + +`parse()` is declared as a template but implemented only via explicit specializations for the concrete types the RPC layer cares about: `AccountID`, `uint256`, `uint192`, `std::uint32_t`, and `Asset`. Each specialization takes a single `Json::Value` and returns `std::optional` — either the successfully decoded value or `std::nullopt`, with no error detail. This keeps the parsing logic pure and composable; callers that want to try-parse without an immediate error (e.g., `parseDirectoryNode` in `LedgerEntry.cpp` probing whether a field is a hash before treating it as a named owner) can call `parse()` directly. + +`required()` wraps `parse()` with presence and null checks and lifts the result into `Expected`. This is XRPL's pre-C++23 approximation of `std::expected`, implemented over `boost::outcome`. When the field is absent or null, `required()` calls `missingFieldError`; when it is present but parses as the wrong type, it calls `invalidFieldError`. The result propagates to callers via `Unexpected(result.error())` return sites in `LedgerEntry.cpp`, enabling clean early-exit error propagation without exceptions. + +Concrete wrappers — `requiredAccountID`, `requiredUInt32`, `requiredUInt256`, `requiredUInt192`, `requiredAsset` — are thin shims that pass the appropriate human-readable type name string (`"AccountID"`, `"number"`, `"Hash256"`, etc.) to `required()`, which feeds it into the `error_message` via `RPC::expected_field_message`. This keeps type description strings co-located with the parse logic rather than scattered across call sites. + +## Error Construction + +Three error builders cover all failure modes: + +- `missingFieldError(field, err)` — for fields that are absent or null. The `err` parameter overrides the default `"malformedRequest"` error code string, which enables domain-specific codes like `"malformedLockingChainDoor"` when checking the bridge sub-fields. +- `invalidFieldError(err, field, type)` — for fields that are present but fail parsing. The `type` string (e.g., `"AccountID"`, `"Hash256"`) is passed to `RPC::expected_field_message` to form a human-readable `error_message`. +- `malformedError(err, message)` — for semantic failures where both the error code and the full message are caller-defined (e.g., "Cannot have a trustline to self.", "Must have exactly one of `owner` and `dir_root` fields."). + +All three set `error_code` to `rpcINVALID_PARAMS` and return `Unexpected`, which is immediately compatible with `Expected` return types throughout `LedgerEntry.cpp`. + +## Noteworthy Specializations + +**`parse`** includes a `isZero()` guard after `parseBase58`. The all-zero account ID is a sentinel in the XRPL data model that cannot correspond to a real account; rejecting it here prevents it from silently producing an incorrect keylet. + +**`parse`** accepts both JSON integer and JSON string forms via `beast::lexicalCastChecked`. Some clients serialize numbers as strings, and this accommodates them defensively. The check `param.isInt() && param.asInt() >= 0` allows positive signed integers to be treated as unsigned, rejecting negative values cleanly. + +**`parseHexBlob` / `requiredHexBlob`** are not part of the template system because they take an additional `maxLength` parameter — the credential-type field, for instance, is length-bounded. These functions decode a hex string via `strUnHex` and reject empty blobs or blobs exceeding the caller-specified maximum. + +**`hasRequired`** accepts an `initializer_list` and short-circuits on the first missing or null field, returning the corresponding `missingFieldError`. This is used as a pre-flight check in parsers that need multiple fields before attempting individual extraction — `parseAMM`, `parseRippleState`, `parseBridgeFields`, and `parseAuthorizeCredentials` in `LedgerEntry.cpp` all call it before parsing individual fields. + +## `parseBridgeFields` + +The most complex helper in the namespace assembles an `STXChainBridge` from a nested JSON object containing four sub-fields: `LockingChainDoor`, `LockingChainIssue`, `IssuingChainDoor`, `IssuingChainIssue`. The door fields are `AccountID`s parsed via `requiredAccountID`; the issue fields are `Issue` structs parsed via `issueFromJson`, which throws `std::runtime_error` on malformed input — caught here and converted to `invalidFieldError`. The resulting `STXChainBridge` value is returned as `Expected`. This helper is reused across three distinct bridge-related object types in `LedgerEntry.cpp` (`parseBridge`, `parseXChainOwnedClaimID`, `parseXChainOwnedCreateAccountClaimID`). + +## Design Rationale + +Keeping all functions `inline` in a header rather than compiled into a `.cpp` file is appropriate given the single-consumer design — there is no ODR risk and the compiler sees all definitions when compiling `LedgerEntry.cpp`. The `Expected` discipline, rather than returning sentinel values or throwing, means that every parse function in `LedgerEntry.cpp` propagates errors via the same `if (!result) return Unexpected(result.error())` idiom, making the control flow uniform and auditable across all twenty-five object types. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp.ai.json b/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp.ai.json new file mode 100644 index 0000000000..bfc63a0e6a --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp.ai.json @@ -0,0 +1,301 @@ +{ + "args": [ + { + "lineno": 13, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doLedgerHeader", + "RPC::lookupLedger", + "Ledger retrieval and validation" + ], + "entry_point": "doLedgerHeader", + "purpose": "Handles the ledger_header RPC command, validates input, retrieves the requested ledger, serializes and returns its header.", + "validation_points": [ + "RPC::lookupLedger: Validates ledger_hash and ledger_index from context.params", + "RPC::JsonContext: Validates the overall request JSON structure and types (via framework and constructor)" + ] + } + ], + "data_flows": [ + { + "field": "ledger_hash", + "flow": [ + "context.params", + "RPC::lookupLedger (validation and lookup)", + "lpLedger (shared_ptr)", + "lpLedger->header()", + "addRaw (serializes header)", + "strHex (converts to hex string)", + "jvResult[jss::ledger_data] (output JSON)" + ], + "origin": "context.params (JSON RPC request)", + "transformations": [ + "Validated as a hex string or integer (by lookupLedger)", + "Used to look up ledger", + "Header serialized to binary", + "Binary converted to hex string" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "ledger_index", + "flow": [ + "context.params", + "RPC::lookupLedger (validation and lookup)", + "lpLedger (shared_ptr)", + "lpLedger->header()", + "addRaw (serializes header)", + "strHex (converts to hex string)", + "jvResult[jss::ledger_data] (output JSON)" + ], + "origin": "context.params (JSON RPC request)", + "transformations": [ + "Validated as integer or string (by lookupLedger)", + "Used to look up ledger", + "Header serialized to binary", + "Binary converted to hex string" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "context (request JSON)", + "flow": [ + "RPC framework", + "RPC::JsonContext (constructor validates structure/types)", + "doLedgerHeader (receives validated context)" + ], + "origin": "RPC framework (incoming HTTP/JSON request)", + "transformations": [ + "Validated for required fields, types, and structure" + ], + "validated_at": "RPC::JsonContext (constructor and framework)" + } + ], + "description": "Implements the doLedgerHeader function, which retrieves and serializes the header of a specified ledger into JSON format for RPC responses.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash validated by xrpld RPC framework (jss::, RPC::JsonContext, RPC::lookupLedger)", + "issue_pattern": "Missing validation for ledger_hash", + "why_false_positive": "xrpld RPC framework (jss::, RPC::JsonContext, RPC::lookupLedger) validates ledger_hash automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_index validated by xrpld RPC framework (jss::, RPC::JsonContext, RPC::lookupLedger)", + "issue_pattern": "Missing validation for ledger_index", + "why_false_positive": "xrpld RPC framework (jss::, RPC::JsonContext, RPC::lookupLedger) validates ledger_index automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger_hash / ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doLedgerHeader", + "issue_pattern": "Missing empty string validation for ledger_hash / ledger_index", + "why_false_positive": "RPC::lookupLedger validates ledger_hash / ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "context (request JSON)", + "empty", + "string", + "validation" + ], + "evidence": "RPC::JsonContext (constructor and framework) at doLedgerHeader (indirect, via context parameter)", + "issue_pattern": "Missing empty string validation for context (request JSON)", + "why_false_positive": "RPC::JsonContext (constructor and framework) validates context (request JSON) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 11, + "name": "doLedgerHeader" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The doLedgerHeader function is typically tested via RPC integration tests that exercise the ledger_header command with various ledger_hash and ledger_index values. Likely test files include rpc/ledger_header_test.cpp, rpc/LedgerHandler_test.cpp, or broader RPC handler test suites. Validation of malformed or missing ledger_hash/ledger_index is usually covered, but edge cases (e.g., invalid types, boundary values, or malicious input) may not be exhaustively tested. Direct unit tests for RPC::lookupLedger and JsonContext validation may exist, but gaps could include fuzzing, deeply nested/invalid JSON, or rare ledger lookup failures.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger_hash", + "ledger_index" + ], + "framework": "xrpld RPC framework (jss::, RPC::JsonContext, RPC::lookupLedger)", + "validation_layer": "entry_point (RPC handler), middleware (lookupLedger), business_logic (ledger existence)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Returns error JSON (e.g., rpcLGR_NOT_FOUND, rpcINVALID_PARAMS)", + "field": "ledger_hash / ledger_index", + "location": "doLedgerHeader", + "validated_by": "RPC::lookupLedger", + "validates": [ + "ledger_hash: checks if present, correct format (hex), valid length", + "ledger_index: checks if present, is integer, within valid range", + "at least one of ledger_hash or ledger_index must be provided", + "ledger exists in backend" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "Returns error JSON (e.g., rpcINVALID_PARAMS)", + "field": "context (request JSON)", + "location": "doLedgerHeader (indirect, via context parameter)", + "validated_by": "RPC::JsonContext (constructor and framework)", + "validates": [ + "request is valid JSON", + "fields are correct types (ledger_hash: string, ledger_index: int or string)", + "no extraneous fields" + ], + "validation_type": "type|format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp.ai.md b/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp.ai.md new file mode 100644 index 0000000000..b72269a8fb --- /dev/null +++ b/src/xrpld/rpc/handlers/ledger/LedgerHeader.cpp.ai.md @@ -0,0 +1,27 @@ +# `LedgerHeader.cpp` — `doLedgerHeader` RPC Handler + +This file implements `doLedgerHeader`, the handler for the `ledger_header` JSON-RPC command. The command's specific purpose — distinct from the broader `ledger` command — is to expose the raw binary encoding of a ledger's header alongside its JSON representation, giving clients everything they need to verify the ledger's cryptographic integrity independently of the server. + +## What the Handler Does + +`doLedgerHeader` follows a tight three-step pattern. First, `RPC::lookupLedger` resolves the requested ledger from `context.params`, accepting either a `ledger_hash` (256-bit hex) or a `ledger_index` (integer or a string shortcut such as `"current"`, `"closed"`, or `"validated"`). If the ledger cannot be found or the parameters are malformed, `lookupLedger` populates `jvResult` with an error code (`rpcLGR_NOT_FOUND`, `rpcINVALID_PARAMS`, etc.) and returns a null `shared_ptr`, causing `doLedgerHeader` to return the error immediately. No input validation lives in this file itself — it is fully delegated to the RPC framework and `lookupLedger`. + +Second, it serializes the ledger header into canonical binary form. `addRaw(lpLedger->header(), s)` invokes the two-argument overload of `addRaw` declared in `include/xrpl/protocol/LedgerHeader.h`, which defaults `includeHash` to `false`. The serialization encodes the header's fields in a fixed wire layout: sequence number, total XRP drops, parent hash, transaction tree hash, account-state tree hash, parent close time, close time, close-time resolution, and close flags. Because the hash itself is excluded, the resulting bytes are precisely the pre-image that, when prefixed with `HashPrefix::ledgerMaster` and hashed with SHA-512 half, should reproduce the ledger's hash. A client who receives `ledger_data` and the accompanying `ledger_hash` can therefore verify the server's claim without trusting the server. This is the non-obvious design motivation for returning the binary at all: it enables trustless verification of the header fields. + +Third, `addJson(jvResult, {*lpLedger, &context, 0})` appends the human-readable JSON representation of those same header fields. The `LedgerFill` struct is constructed with `options = 0`, which means no transaction dump, no state dump, and no full expansion — only the header-level fields are populated. Passing `&context` into `LedgerFill` also causes its constructor to call `context.ledgerMaster.getCloseTimeBySeq(ledger.seq())`, fetching the actual network close time from the ledger master's internal index and storing it as `closeTime` on the fill object so `addJson` can include it. + +## The Trust Caveat + +The comment just before the `addJson` call is architecturally important: + +> This information isn't verified: they should only use it if they trust us. + +The raw `ledger_data` blob is self-verifying — the client can recompute the hash. The JSON fields derived from `addJson` are not: they are the server's interpretation of the ledger contents and cannot be independently re-verified from the response alone. This distinction explains the dual-output design rather than returning only JSON. The binary form is the trust anchor; the JSON form is a convenience layer. + +## Relationship to Sibling Handlers + +Within the `handlers/ledger/` module, `LedgerHeader.cpp` occupies a narrow, specialized niche. `LedgerClosed.cpp` returns only the sequence number and hash of the most recently closed ledger — no binary, no full JSON. `LedgerData.cpp` returns ledger state objects in bulk. `Ledger.cpp` (the main `ledger` command) is the broader handler that optionally includes transactions and state data via flags. `LedgerHeader.cpp` is the one handler where a client explicitly wants the binary-serialized header, which makes it most relevant to use cases involving proof of ledger integrity, archive verification, or inter-server cross-checking. + +## Resource and Memory Safety + +The ledger is held through a `std::shared_ptr`, keeping the ledger object alive for the duration of the call and guaranteeing cleanup at function return without any manual memory management. `Serializer s` is stack-allocated; `s.peekData()` returns a const reference to its internal buffer, which is valid until `s` goes out of scope — `strHex` converts it to a `std::string` before that happens, so there is no lifetime hazard. The overall function is short-lived and synchronous, with no concurrency concerns internal to its implementation. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp.ai.json b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp.ai.json new file mode 100644 index 0000000000..2cdb703aaa --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp.ai.json @@ -0,0 +1,498 @@ +{ + "args": [ + { + "lineno": 11, + "name": "v" + }, + { + "lineno": 11, + "name": "j" + }, + { + "lineno": 21, + "name": "tp" + }, + { + "lineno": 29, + "name": "context" + } + ], + "classes": [ + { + "args": [], + "lineno": 44, + "name": "ValuesFromContextParams" + } + ], + "code_paths": [ + { + "call_chain": [ + "doAMMInfo", + "getValuesFromContextParams (lambda)", + "getAsset", + "assetFromJson" + ], + "entry_point": "doAMMInfo", + "purpose": "Handles the AMM info RPC request, validates and parses input parameters, fetches ledger data, and returns AMM info.", + "validation_points": [ + "invalid lambda (parameter combination validation)", + "getAsset (calls assetFromJson, validates asset fields)", + "parseBase58 (validates amm_account and account fields)", + "ledger->read(keylet::account(*id)) (checks existence of accounts in ledger)", + "ammID->isZero() (checks AMM existence)", + "XRPL_ASSERT (internal consistency check for asset/ammID presence)" + ] + } + ], + "data_flows": [ + { + "field": "asset", + "flow": [ + "context.params[jss::asset]", + "getAsset", + "assetFromJson", + "asset1 (local variable)", + "keylet::amm(*asset1, *asset2) or further logic" + ], + "origin": "context.params[jss::asset]", + "transformations": [ + "JSON \u2192 Asset object via assetFromJson" + ], + "validated_at": "getAsset (calls assetFromJson, catches exceptions)" + }, + { + "field": "asset2", + "flow": [ + "context.params[jss::asset2]", + "getAsset", + "assetFromJson", + "asset2 (local variable)", + "keylet::amm(*asset1, *asset2) or further logic" + ], + "origin": "context.params[jss::asset2]", + "transformations": [ + "JSON \u2192 Asset object via assetFromJson" + ], + "validated_at": "getAsset (calls assetFromJson, catches exceptions)" + }, + { + "field": "amm_account", + "flow": [ + "context.params[jss::amm_account]", + "parseBase58", + "ledger->read(keylet::account(*id))", + "sle->getFieldH256(sfAMMID)", + "ammID" + ], + "origin": "context.params[jss::amm_account]", + "transformations": [ + "Base58 string \u2192 AccountID", + "AccountID \u2192 SLE lookup", + "SLE \u2192 AMMID" + ], + "validated_at": "parseBase58 (format), ledger->read (existence), ammID->isZero() (AMM existence)" + }, + { + "field": "account", + "flow": [ + "context.params[jss::account]", + "parseBase58", + "ledger->read(keylet::account(*accountID))" + ], + "origin": "context.params[jss::account]", + "transformations": [ + "Base58 string \u2192 AccountID", + "AccountID \u2192 SLE lookup" + ], + "validated_at": "parseBase58 (format), ledger->read (existence)" + }, + { + "field": "params (combination)", + "flow": [ + "context.params", + "invalid lambda", + "getValuesFromContextParams" + ], + "origin": "context.params", + "transformations": [ + "Checks for valid combinations of asset/asset2/amm_account" + ], + "validated_at": "invalid lambda (before and after field extraction, depending on API version)" + } + ], + "description": "Implements the doAMMInfo RPC handler for the XRPL server, providing information about Automated Market Maker (AMM) pools, including asset balances, trading fees, auction slots, and vote slots, based on JSON RPC context parameters.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger (via RPC::lookupLedger)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger (via RPC::lookupLedger) validated by XRPL RPC framework (jss::, assetFromJson, parseBase58, RPC::lookupLedger)", + "issue_pattern": "Missing validation for ledger (via RPC::lookupLedger)", + "why_false_positive": "XRPL RPC framework (jss::, assetFromJson, parseBase58, RPC::lookupLedger) validates ledger (via RPC::lookupLedger) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "asset, asset2 (via assetFromJson in getAsset)", + "validation", + "missing", + "check" + ], + "evidence": "Field asset, asset2 (via assetFromJson in getAsset) validated by XRPL RPC framework (jss::, assetFromJson, parseBase58, RPC::lookupLedger)", + "issue_pattern": "Missing validation for asset, asset2 (via assetFromJson in getAsset)", + "why_false_positive": "XRPL RPC framework (jss::, assetFromJson, parseBase58, RPC::lookupLedger) validates asset, asset2 (via assetFromJson in getAsset) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "amm_account (via parseBase58)", + "validation", + "missing", + "check" + ], + "evidence": "Field amm_account (via parseBase58) validated by XRPL RPC framework (jss::, assetFromJson, parseBase58, RPC::lookupLedger)", + "issue_pattern": "Missing validation for amm_account (via parseBase58)", + "why_false_positive": "XRPL RPC framework (jss::, assetFromJson, parseBase58, RPC::lookupLedger) validates amm_account (via parseBase58) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "asset", + "empty", + "string", + "validation" + ], + "evidence": "getAsset (calls assetFromJson) at getValuesFromContextParams (lambda in doAMMInfo)", + "issue_pattern": "Missing empty string validation for asset", + "why_false_positive": "getAsset (calls assetFromJson) validates asset for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "asset2", + "empty", + "string", + "validation" + ], + "evidence": "getAsset (calls assetFromJson) at getValuesFromContextParams (lambda in doAMMInfo)", + "issue_pattern": "Missing empty string validation for asset2", + "why_false_positive": "getAsset (calls assetFromJson) validates asset2 for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "amm_account", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at getValuesFromContextParams (lambda in doAMMInfo)", + "issue_pattern": "Missing empty string validation for amm_account", + "why_false_positive": "parseBase58 validates amm_account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "amm_account (existence in ledger)", + "empty", + "string", + "validation" + ], + "evidence": "ledger->read(keylet::account(*id)) at getValuesFromContextParams (lambda in doAMMInfo)", + "issue_pattern": "Missing empty string validation for amm_account (existence in ledger)", + "why_false_positive": "ledger->read(keylet::account(*id)) validates amm_account (existence in ledger) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "params (combination of asset, asset2, amm_account)", + "empty", + "string", + "validation" + ], + "evidence": "invalid lambda at getValuesFromContextParams (lambda in doAMMInfo)", + "issue_pattern": "Missing empty string validation for params (combination of asset, asset2, amm_account)", + "why_false_positive": "invalid lambda validates params (combination of asset, asset2, amm_account) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doAMMInfo", + "issue_pattern": "Missing empty string validation for ledger", + "why_false_positive": "RPC::lookupLedger validates ledger for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp", + "functions": [ + { + "args": [ + "Json::Value const& v", + "beast::Journal j" + ], + "lineno": 11, + "name": "getAsset" + }, + { + "args": [ + "NetClock::time_point tp" + ], + "lineno": 21, + "name": "to_iso8601" + }, + { + "args": [ + "RPC::JsonContext& context" + ], + "lineno": 29, + "name": "doAMMInfo" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this code would be in the RPC handler test suites, such as 'test/rpc/AMMInfo_test.cpp', 'test/rpc/Orderbook_test.cpp', or similar files. These tests should cover valid and invalid combinations of asset, asset2, and amm_account, malformed asset/account fields, and missing/extra parameters. Gaps may exist if there are no tests for edge cases (e.g., both asset and amm_account present, malformed base58, non-existent accounts, or AMM not found). Exception handling in getAsset and error propagation from assetFromJson should be explicitly tested. If API versioning logic is not tested (apVersion < 3 vs >= 3), this is a potential gap.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger (via RPC::lookupLedger)", + "asset, asset2 (via assetFromJson in getAsset)", + "amm_account (via parseBase58)" + ], + "framework": "XRPL RPC framework (jss::, assetFromJson, parseBase58, RPC::lookupLedger)", + "validation_layer": "business_logic (in handler), some framework-level (RPC::lookupLedger, parseBase58)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcISSUE_MALFORMED (via Unexpected), logs std::runtime_error", + "field": "asset", + "location": "getValuesFromContextParams (lambda in doAMMInfo)", + "validated_by": "getAsset (calls assetFromJson)", + "validates": [ + "Checks if 'asset' field is present", + "Parses 'asset' JSON to Asset object", + "Catches parsing errors (malformed/invalid asset format)" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcISSUE_MALFORMED (via Unexpected), logs std::runtime_error", + "field": "asset2", + "location": "getValuesFromContextParams (lambda in doAMMInfo)", + "validated_by": "getAsset (calls assetFromJson)", + "validates": [ + "Checks if 'asset2' field is present", + "Parses 'asset2' JSON to Asset object", + "Catches parsing errors (malformed/invalid asset2 format)" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcACT_MALFORMED (via Unexpected)", + "field": "amm_account", + "location": "getValuesFromContextParams (lambda in doAMMInfo)", + "validated_by": "parseBase58", + "validates": [ + "Checks if 'amm_account' field is present", + "Validates 'amm_account' is a valid base58 AccountID" + ], + "validation_type": "format|type" + }, + { + "confidence": 0.8, + "error_thrown": "Not shown in snippet, but likely returns Unexpected or error if not found", + "field": "amm_account (existence in ledger)", + "location": "getValuesFromContextParams (lambda in doAMMInfo)", + "validated_by": "ledger->read(keylet::account(*id))", + "validates": [ + "Checks if 'amm_account' exists in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcINVALID_PARAMS (via Unexpected)", + "field": "params (combination of asset, asset2, amm_account)", + "location": "getValuesFromContextParams (lambda in doAMMInfo)", + "validated_by": "invalid lambda", + "validates": [ + "Checks that either both 'asset' and 'asset2' are present, or 'amm_account' is present, but not both", + "Ensures correct parameter combinations for API version < 3" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "Returns error result if ledger not found", + "field": "ledger", + "location": "doAMMInfo", + "validated_by": "RPC::lookupLedger", + "validates": [ + "Checks that requested ledger exists and is accessible" + ], + "validation_type": "business_logic|existence" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp.ai.md b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp.ai.md new file mode 100644 index 0000000000..fb98918420 --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/AMMInfo.cpp.ai.md @@ -0,0 +1,54 @@ +# `AMMInfo.cpp` — `doAMMInfo` RPC Handler + +## Role in the System + +This file implements the `doAMMInfo` RPC endpoint, which allows JSON-RPC callers to inspect an Automated Market Maker (AMM) pool on the XRPL. The response bundles together the pool's reserve balances, the current trading fee, vote slot state, and the active auction slot — everything a client or DEX front-end needs to reason about an AMM's current state. It sits in `src/xrpld/rpc/handlers/orderbook/`, alongside other orderbook-oriented handlers like `BookOffers` and `GetAggregatePrice`. + +--- + +## Helper Functions + +### `getAsset` + +`assetFromJson` (from `AMMCore`) throws `std::runtime_error` when given malformed input. Rather than letting that exception bubble through the RPC layer uncontrolled, `getAsset` wraps the call in a `try/catch` and translates any parse failure into `Unexpected(rpcISSUE_MALFORMED)`. This is the standard XRPL RPC convention: structured error codes at the boundary, never raw exceptions. The `Expected` return type makes the error path type-safe and explicit at each call site. + +### `to_iso8601` + +XRPL's internal clock (`NetClock`) counts seconds since the Ripple epoch — 2000-01-01 00:00:00 UTC — while the standard Unix epoch starts in 1970. `to_iso8601` bridges this by adding `epoch_offset` (946,684,800 seconds) to the duration before constructing a `system_clock::time_point` and formatting it with the Howard Hinnant `date` library. This conversion is used exclusively for the auction slot's `sfExpiration` field, ensuring the expiration timestamp is presented in a universally readable format. + +--- + +## `doAMMInfo` — Parameter Validation Architecture + +The handler begins with a standard `RPC::lookupLedger` call, which resolves the target ledger (open, validated, or a specific historical ledger) and handles all ledger-level errors before any AMM logic runs. + +Parameter extraction and validation is encapsulated in a nested lambda `getValuesFromContextParams` that returns `Expected`. This pattern keeps error propagation explicit without requiring output parameters or exception abuse; each failure path returns `Unexpected(error_code)` and the caller checks with `if (!r)` before destructuring. + +The core constraint is that callers must supply **either** the `asset`+`asset2` pair **or** `amm_account`, but not both — captured by the `invalid` lambda: + +```cpp +(params.isMember(jss::asset) != params.isMember(jss::asset2)) || +(params.isMember(jss::asset) == params.isMember(jss::amm_account)) +``` + +There is a subtle API version sensitivity here. For `apiVersion < 3`, the `invalid` check fires **before** any individual fields are parsed. For `apiVersion >= 3`, the check fires **after** parsing `asset`, `asset2`, and `amm_account`. The comment in the code notes these are "identical check" blocks placed at different positions intentionally — the ordering controls which error fires first when a caller provides conflicting inputs under older vs. newer API versions, preserving backward-compatible error sequencing. + +When `amm_account` is provided, the handler walks the ledger: it reads the account's `SLE` (state ledger entry), extracts `sfAMMID` from it, and uses that to derive the AMM keylet. If `sfAMMID` is zero, the account exists but is not an AMM account, and `rpcACT_NOT_FOUND` is returned. The two lookup paths — `keylet::amm(*asset1, *asset2)` and `keylet::amm(*ammID)` — converge at the same AMM SLE, and if the lookup was by ID alone, the assets are back-filled from the SLE's `sfAsset`/`sfAsset2` fields. + +An `XRPL_ASSERT` enforces the internal consistency invariant that after validation, exactly one of (`asset1`+`asset2`) or `ammID` is set — catching any future refactoring that breaks the mutual-exclusion logic at debug time. + +--- + +## Response Construction + +**Pool balances** are fetched via `ammPoolHolds` with `FreezeHandling::fhIGNORE_FREEZE`. This is intentional: an AMM cannot suspend operations simply because an issuer has frozen a trust line. The freeze state is reported separately — `asset_frozen` and `asset2_frozen` fields are appended for non-XRP assets only, since XRP has no freeze mechanism. This design means callers always see the real pool depth and must independently decide how to act on freeze status. + +**LP token balance** is context-dependent. When the request includes an `account` parameter, `ammLPHolds` returns that specific account's liquidity provider balance (how many LP tokens the account holds). Without `account`, the total `sfLPTokenBalance` from the AMM SLE is returned — the aggregate outstanding supply. + +**Vote slots** are iterated from `sfVoteSlots` if present and serialized as a JSON array with per-voter account, fee, and weight. The array is only included in the response if non-empty, keeping the output clean for AMMs without active governance. + +**Auction slot** serialization has a layered presence check. First, `sfAuctionSlot` must exist on the AMM object. An assertion guards that once the `fixInnerObjTemplate` amendment is active, the field is always present. Second, within the slot, `sfAccount` must be set — the slot is "active" only if someone holds it. An unowned slot has no account and is silently omitted from the response. + +When the auction slot is active, `ammAuctionTimeSlot` computes which of the 20 time intervals (each 72 minutes of a 24-hour cycle) the slot is currently in, using `parentCloseTime` from the ledger header. If the slot has expired, `ammAuctionTimeSlot` returns `std::nullopt` and the handler emits `AUCTION_SLOT_TIME_INTERVALS` (20) as `time_interval` — a sentinel value indicating expiry rather than omitting the field. Authorized accounts (`sfAuthAccounts`) are listed if present, since they benefit from the discounted fee during the slot period. + +**Ledger identification** follows XRPL conventions: `ledger_current_index` is appended only when neither `ledger_index` nor `ledger_hash` was specified by the caller (meaning the current open ledger was used), and `validated` is set by querying `LedgerMaster`. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp.ai.json b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp.ai.json new file mode 100644 index 0000000000..b77f9f3c67 --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp.ai.json @@ -0,0 +1,181 @@ +{ + "args": [ + { + "lineno": 8, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doBookChanges", + "RPC::lookupLedger", + "RPC::computeBookChanges" + ], + "entry_point": "doBookChanges", + "purpose": "Handles the 'book_changes' RPC request: validates the ledger context, then computes and returns book changes for the specified ledger.", + "validation_points": [ + "RPC::lookupLedger" + ] + } + ], + "data_flows": [ + { + "field": "ledger", + "flow": [ + "context", + "RPC::lookupLedger (validates and assigns to ledger)", + "doBookChanges (checks ledger != nullptr)", + "RPC::computeBookChanges (uses ledger)" + ], + "origin": "context (RPC::JsonContext) - likely from user RPC request parameters", + "transformations": [ + "Validated and resolved from context by RPC::lookupLedger", + "Checked for nullptr in doBookChanges" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "result", + "flow": [ + "RPC::lookupLedger (returns result if ledger invalid)", + "doBookChanges (returns result if ledger == nullptr)" + ], + "origin": "RPC::lookupLedger", + "transformations": [ + "May contain error information if ledger lookup fails" + ], + "validated_at": "RPC::lookupLedger" + } + ], + "description": "Implements the doBookChanges RPC handler, which retrieves and returns book changes from a specified ledger in the XRPL system.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger identifier (hash, index, etc.) in context", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger identifier (hash, index, etc.) in context validated by xrpld RPC framework (custom, not external)", + "issue_pattern": "Missing validation for ledger identifier (hash, index, etc.) in context", + "why_false_positive": "xrpld RPC framework (custom, not external) validates ledger identifier (hash, index, etc.) in context automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.95, + "detection_keywords": [ + "ledger (from context)", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doBookChanges", + "issue_pattern": "Missing empty string validation for ledger (from context)", + "why_false_positive": "RPC::lookupLedger validates ledger (from context) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 7, + "name": "doBookChanges" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this handler would typically be found in RPC integration or unit tests, possibly under files like 'BookChanges_test.cpp', 'OrderBook_test.cpp', or generic RPC handler test suites. The critical validation path (ledger lookup and validation) is covered if tests provide both valid and invalid ledger parameters. Gaps may exist if edge cases (e.g., malformed context, missing ledger fields, or computeBookChanges error handling) are not explicitly tested. No test files are referenced in the provided code, so coverage must be verified in the broader codebase.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger identifier (hash, index, etc.) in context" + ], + "framework": "xrpld RPC framework (custom, not external)", + "validation_layer": "entry_point (handler function doBookChanges)" + }, + "validations": [ + { + "confidence": 0.95, + "error_thrown": "Returns error JSON (not exception); ledger remains nullptr", + "field": "ledger (from context)", + "location": "doBookChanges", + "validated_by": "RPC::lookupLedger", + "validates": [ + "Checks if requested ledger exists and is accessible", + "Validates ledger identifier (hash, index, etc.) from context", + "Ensures ledger pointer is set if valid" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp.ai.md b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp.ai.md new file mode 100644 index 0000000000..0a7f62de7c --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/BookChanges.cpp.ai.md @@ -0,0 +1,38 @@ +# `BookChanges.cpp` — RPC Handler for `book_changes` + +## Role and Purpose + +This file implements `doBookChanges`, the entry-point handler wired into the XRPL RPC dispatch table for the `book_changes` API method. Its job is to resolve a ledger from the caller's request parameters and hand it to `RPC::computeBookChanges`, which does the substantive work. The handler itself is deliberately minimal — twelve lines of meaningful code — because all ledger parameter parsing, validation, and error construction belong to the shared RPC infrastructure, not to individual handlers. + +## Handler Structure + +``` +doBookChanges(context) + └── RPC::lookupLedger(ledger, context) ← resolves & validates the ledger + (returns error JSON on failure) + └── RPC::computeBookChanges(ledger) ← computes the result +``` + +`RPC::lookupLedger` accepts the standard XRPL ledger-selector parameters: the string shorthands `"validated"`, `"current"`, and `"closed"`, plus explicit `ledger_hash` and `ledger_index` fields. It populates the `std::shared_ptr` and simultaneously returns a `Json::Value` that contains error fields if the lookup failed. The handler checks the pointer — not the JSON — to decide whether to short-circuit, then discards the partially-populated result in the failure case by returning it directly. This pattern is consistent across all ledger-reading handlers in the `xrpld/rpc/handlers/` subtree (see `BookOffers.cpp` for a more complex example of the same idiom). + +Because `book_changes` requires no additional request parameters beyond the ledger selector, the handler is simpler than most of its siblings. `doBookOffers`, for instance, must parse and validate `taker_pays`, `taker_gets`, optional `taker` account, optional `domain`, and a pagination `marker` before it can call into business logic. The absence of that complexity here is intentional: `book_changes` is an aggregate query across *all* books in the ledger, not a paginated query into a specific order book. + +## The Real Work: `computeBookChanges` + +The substance lives in `BookChanges.h`, which defines a function template `RPC::computeBookChanges`. The template parameter allows the same computation to be driven by any `ReadView`-compatible type, enabling reuse from both RPC handlers and subscription feeds without requiring a virtual interface. + +The algorithm walks the ledger's transaction set and mines the *transaction metadata* rather than the live order book. Each transaction carries an `sfAffectedNodes` array in its metadata, and the function inspects only nodes of type `ltOFFER` that were modified or deleted — created offers represent new resting liquidity, not trades. For each such node, it reads `sfFinalFields` and `sfPreviousFields` to compute the delta in `TakerGets` and `TakerPays`, which directly measures how much of each asset actually changed hands in that crossing event. + +A notable defensive filter: if the transaction type is `ttOFFER_CANCEL` or `ttOFFER_CREATE` and carries an `sfOfferSequence` field, any deleted offer whose sequence matches is excluded from the tally. This correctly omits offers that were removed by an explicit cancellation rather than by being consumed in a trade. + +Currency pair keys are canonicalized before insertion into the `std::map` tally. XRP-denominated pairs always place XRP as side A; for two IOU pairs, sides are ordered by their asset string representation so that the same logical market is always keyed identically regardless of which direction a given offer was facing. + +The tally map accumulates per-pair OHLCV-style statistics: cumulative volume on each side, plus the first-seen rate (open), last-seen rate (close), and running high/low. The rate is computed using `divide(first, second, noIssue())`, representing the price of side A denominated in side B. The `noIssue()` sentinel suppresses the usual issuer tainting on synthetic division results. + +At serialization time, XRP volumes are labeled `"XRP_drops"` to make the unit unambiguous, while IOU assets use the standard `currency/issuer` representation and MPT assets use their `mpt_issuance_id`. The response JSON also includes ledger metadata (`ledger_index`, `ledger_hash`, `ledger_time`, `validated`) drawn directly from the ledger header. + +## Design Observations + +The split between the thin `.cpp` handler and the header-only template is a deliberate architectural choice: `computeBookChanges` was designed to be called not just from the synchronous RPC path but also potentially from streaming subscription contexts that push ledger-close events to WebSocket clients, both of which can supply a `ReadView` but come from different call sites. Keeping the algorithm in a template header avoids duplication without introducing a runtime abstraction layer. + +The use of `std::shared_ptr` for the ledger handle is the standard XRPL ownership model for ledger snapshots. Reference-counted shared ownership is necessary here because multiple concurrent RPC requests may hold references to the same historical ledger object, and the ledger cache must not evict it while any handler is still reading it. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp.ai.json b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp.ai.json new file mode 100644 index 0000000000..c317d1a2c4 --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp.ai.json @@ -0,0 +1,508 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doBookOffers", + "validateTakerJSON", + "parseTakerAssetJSON", + "parseTakerIssuerJSON" + ], + "entry_point": "doBookOffers", + "purpose": "Handles the 'book_offers' RPC request, validates and parses taker fields, constructs asset/issuer objects for order book querying.", + "validation_points": [ + "validateTakerJSON (validates presence, exclusivity, and type of taker.currency and taker.mpt_issuance_id)", + "parseTakerAssetJSON (validates format of taker.currency and taker.mpt_issuance_id)", + "parseTakerIssuerJSON (validates format and presence of taker.issuer if currency is present)" + ] + } + ], + "data_flows": [ + { + "field": "taker.currency", + "flow": [ + "RPC request", + "doBookOffers extracts taker", + "validateTakerJSON checks presence/type", + "parseTakerAssetJSON validates format and parses to Issue", + "parseTakerIssuerJSON validates issuer if present", + "Used to construct Asset/Issue for order book lookup" + ], + "origin": "JSON RPC request (input parameter)", + "transformations": [ + "Checked for presence and type (string)", + "Parsed to Issue.currency via to_currency", + "Combined with issuer to form Issue" + ], + "validated_at": "validateTakerJSON (presence/type), parseTakerAssetJSON (format)" + }, + { + "field": "taker.mpt_issuance_id", + "flow": [ + "RPC request", + "doBookOffers extracts taker", + "validateTakerJSON checks presence/type/exclusivity", + "parseTakerAssetJSON validates format and parses to MPTID", + "Used to construct Asset for order book lookup" + ], + "origin": "JSON RPC request (input parameter)", + "transformations": [ + "Checked for presence and type (string)", + "Parsed to MPTID via parseHex" + ], + "validated_at": "validateTakerJSON (presence/type/exclusivity), parseTakerAssetJSON (format)" + }, + { + "field": "taker.issuer", + "flow": [ + "RPC request", + "doBookOffers extracts taker", + "validateTakerJSON checks exclusivity (cannot be present with mpt_issuance_id)", + "parseTakerIssuerJSON validates presence/type/format", + "Used to set Issue.account" + ], + "origin": "JSON RPC request (input parameter)", + "transformations": [ + "Checked for presence and type (string)", + "Parsed to Issue.account via to_issuer" + ], + "validated_at": "validateTakerJSON (exclusivity), parseTakerIssuerJSON (presence/type/format)" + } + ], + "description": "Implements the core logic for the XRPL 'book_offers' RPC handler, including validation and parsing of taker assets/issuers, and fetching order book offers from the ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "taker.currency", + "validation", + "missing", + "check" + ], + "evidence": "Field taker.currency validated by xrpl RPC framework (jss::, RPC::, Json::Value)", + "issue_pattern": "Missing validation for taker.currency", + "why_false_positive": "xrpl RPC framework (jss::, RPC::, Json::Value) validates taker.currency automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "taker.mpt_issuance_id", + "validation", + "missing", + "check" + ], + "evidence": "Field taker.mpt_issuance_id validated by xrpl RPC framework (jss::, RPC::, Json::Value)", + "issue_pattern": "Missing validation for taker.mpt_issuance_id", + "why_false_positive": "xrpl RPC framework (jss::, RPC::, Json::Value) validates taker.mpt_issuance_id automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "taker.issuer", + "validation", + "missing", + "check" + ], + "evidence": "Field taker.issuer validated by xrpl RPC framework (jss::, RPC::, Json::Value)", + "issue_pattern": "Missing validation for taker.issuer", + "why_false_positive": "xrpl RPC framework (jss::, RPC::, Json::Value) validates taker.issuer automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "taker.currency and taker.mpt_issuance_id", + "empty", + "string", + "validation" + ], + "evidence": "validateTakerJSON at validateTakerJSON", + "issue_pattern": "Missing empty string validation for taker.currency and taker.mpt_issuance_id", + "why_false_positive": "validateTakerJSON validates taker.currency and taker.mpt_issuance_id for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "taker.mpt_issuance_id with taker.currency or taker.issuer", + "empty", + "string", + "validation" + ], + "evidence": "validateTakerJSON at validateTakerJSON", + "issue_pattern": "Missing empty string validation for taker.mpt_issuance_id with taker.currency or taker.issuer", + "why_false_positive": "validateTakerJSON validates taker.mpt_issuance_id with taker.currency or taker.issuer for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "taker.currency and taker.mpt_issuance_id types", + "empty", + "string", + "validation" + ], + "evidence": "validateTakerJSON at validateTakerJSON", + "issue_pattern": "Missing empty string validation for taker.currency and taker.mpt_issuance_id types", + "why_false_positive": "validateTakerJSON validates taker.currency and taker.mpt_issuance_id types for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "taker.currency and taker.mpt_issuance_id types", + "type", + "validation", + "check" + ], + "evidence": "validateTakerJSON at validateTakerJSON", + "issue_pattern": "Missing type validation for taker.currency and taker.mpt_issuance_id types", + "why_false_positive": "validateTakerJSON validates taker.currency and taker.mpt_issuance_id types type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "taker.currency format", + "empty", + "string", + "validation" + ], + "evidence": "parseTakerAssetJSON at parseTakerAssetJSON", + "issue_pattern": "Missing empty string validation for taker.currency format", + "why_false_positive": "parseTakerAssetJSON validates taker.currency format for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "taker.currency format", + "format", + "validation", + "invalid" + ], + "evidence": "parseTakerAssetJSON at parseTakerAssetJSON", + "issue_pattern": "Missing format validation for taker.currency format", + "why_false_positive": "parseTakerAssetJSON validates taker.currency format format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "taker.mpt_issuance_id format", + "empty", + "string", + "validation" + ], + "evidence": "parseTakerAssetJSON at parseTakerAssetJSON", + "issue_pattern": "Missing empty string validation for taker.mpt_issuance_id format", + "why_false_positive": "parseTakerAssetJSON validates taker.mpt_issuance_id format for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "taker.mpt_issuance_id format", + "format", + "validation", + "invalid" + ], + "evidence": "parseTakerAssetJSON at parseTakerAssetJSON", + "issue_pattern": "Missing format validation for taker.mpt_issuance_id format", + "why_false_positive": "parseTakerAssetJSON validates taker.mpt_issuance_id format format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "taker.issuer type", + "empty", + "string", + "validation" + ], + "evidence": "parseTakerIssuerJSON at parseTakerIssuerJSON", + "issue_pattern": "Missing empty string validation for taker.issuer type", + "why_false_positive": "parseTakerIssuerJSON validates taker.issuer type for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "taker.issuer type", + "type", + "validation", + "check" + ], + "evidence": "parseTakerIssuerJSON at parseTakerIssuerJSON", + "issue_pattern": "Missing type validation for taker.issuer type", + "why_false_positive": "parseTakerIssuerJSON validates taker.issuer type type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp", + "functions": [ + { + "args": [ + "taker", + "name" + ], + "lineno": 12, + "name": "validateTakerJSON" + }, + { + "args": [ + "asset", + "taker", + "name", + "j" + ], + "lineno": 32, + "name": "parseTakerAssetJSON" + }, + { + "args": [ + "asset", + "taker", + "name", + "j" + ], + "lineno": 67, + "name": "parseTakerIssuerJSON" + }, + { + "args": [ + "context" + ], + "lineno": 110, + "name": "doBookOffers" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 10, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this code would be in the RPC handler tests, likely in files such as 'test/rpc/BookOffers_test.cpp' or similar. These tests should cover valid and invalid combinations of taker.currency, taker.mpt_issuance_id, and taker.issuer, including type errors, missing fields, and malformed values. Gaps may exist if there are no tests for edge cases (e.g., both currency and mpt_issuance_id present, non-string types, invalid hex for mpt_issuance_id, or issuer present with mpt_issuance_id). Template-based validation may not be fully exercised if tests only cover happy paths.", + "validation_architecture": { + "auto_validated_fields": [ + "taker.currency", + "taker.mpt_issuance_id", + "taker.issuer" + ], + "framework": "xrpl RPC framework (jss::, RPC::, Json::Value)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error", + "field": "taker.currency and taker.mpt_issuance_id", + "location": "validateTakerJSON", + "validated_by": "validateTakerJSON", + "validates": [ + "At least one of taker.currency or taker.mpt_issuance_id must be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error", + "field": "taker.mpt_issuance_id with taker.currency or taker.issuer", + "location": "validateTakerJSON", + "validated_by": "validateTakerJSON", + "validates": [ + "If taker.mpt_issuance_id is present, taker.currency and taker.issuer must NOT be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::expected_field_error", + "field": "taker.currency and taker.mpt_issuance_id types", + "location": "validateTakerJSON", + "validated_by": "validateTakerJSON", + "validates": [ + "taker.currency (if present) must be a string", + "taker.mpt_issuance_id (if present) must be a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::make_error (rpcSRC_CUR_MALFORMED or rpcDST_AMT_MALFORMED)", + "field": "taker.currency format", + "location": "parseTakerAssetJSON", + "validated_by": "parseTakerAssetJSON", + "validates": [ + "taker.currency must be a valid currency string (checked by to_currency)" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::make_error (rpcSRC_CUR_MALFORMED or rpcDST_AMT_MALFORMED)", + "field": "taker.mpt_issuance_id format", + "location": "parseTakerAssetJSON", + "validated_by": "parseTakerAssetJSON", + "validates": [ + "taker.mpt_issuance_id must be a valid hex string (checked by MPTID::parseHex)" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "RPC::expected_field", + "field": "taker.issuer type", + "location": "parseTakerIssuerJSON", + "validated_by": "parseTakerIssuerJSON", + "validates": [ + "taker.issuer (if present) must be a string" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp.ai.md b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp.ai.md new file mode 100644 index 0000000000..280925dc6d --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/BookOffers.cpp.ai.md @@ -0,0 +1,51 @@ +# `BookOffers.cpp` — `book_offers` RPC Handler + +## Role in the System + +`BookOffers.cpp` is the server-side handler for the `book_offers` JSON-RPC method, one of the most commonly called APIs on the XRP Ledger. It lets clients enumerate active offers in a specific trading pair — essentially reading the order book for the XRPL's native decentralized exchange (DEX). The file contains three validation helpers and the entry-point function `doBookOffers`, which is dispatched by the RPC framework when a `book_offers` request arrives. + +## Asset Model and the Two-Sided Book + +The handler must resolve two sides of a trading pair from JSON input: `taker_pays` (what the taker offers) and `taker_gets` (what the taker receives). Each side is an `Asset`, which is a `std::variant`. An `Issue` covers both XRP (a currency with a sentinel XRP account) and IOUs (currency + issuer account); an `MPTIssue` represents a Multi-Purpose Token identified by a 192-bit `MPTID`. The `Book` struct wraps `Asset in`, `Asset out`, and an optional `domain` field for domain-scoped books, then gets forwarded verbatim to the ledger traversal layer. + +## Three-Phase Validation Design + +The input for each side of the book is validated in three distinct passes, deliberately separated into reusable functions: + +**`validateTakerJSON`** enforces structural constraints on the raw JSON before any parsing. It rejects a taker object that has neither `currency` nor `mpt_issuance_id`, that mixes `mpt_issuance_id` with `currency` or `issuer` (MPT and IOU/XRP specifications are mutually exclusive), or that provides these fields as non-strings. + +**`parseTakerAssetJSON`** translates the validated string representations into typed values. For IOU/XRP paths it calls `to_currency()` to populate an `Issue`; for MPT paths it calls `MPTID::parseHex()`. This function uses a small lambda to select the correct error code contextually — `rpcSRC_CUR_MALFORMED` for `taker_pays`, `rpcDST_AMT_MALFORMED` for `taker_gets` — without duplicating the body. + +**`parseTakerIssuerJSON`** then fills in the `Issue::account` (issuer). For XRP it defaults to `xrpAccount()` and rejects any explicit issuer. For IOU it requires an explicit issuer, validates it with `to_issuer()`, rejects the sentinel `noAccount()`, and rejects a non-XRP currency paired with the XRP account. MPT assets skip this function entirely since there is no issuer concept. + +This three-function split avoids any duplication between the `taker_pays` and `taker_gets` sides, keeping the logic in `doBookOffers` itself to a clean sequence of six consecutive `if (auto const err = ...)` calls. + +## Concurrency and Load Shedding + +Before touching the ledger at all, `doBookOffers` checks the `jtCLIENT` job count: + +```cpp +if (context.app.getJobQueue().getJobCountGE(jtCLIENT) > 200) + return rpcError(rpcTOO_BUSY); +``` + +This is an early-exit load-shedder: if the server is already processing more than 200 concurrent client jobs, the request is rejected immediately with `rpcTOO_BUSY`, avoiding any further work. The comment in the source (`// VFALCO TODO Here is a terrible place for this kind of business logic`) acknowledges this is a legacy placement; conceptually it belongs in the dispatch layer, but it has lived here long enough to be part of the contract. + +## Pagination, Domain, and the Taker Identity + +Three optional fields feed into the final `getBookPage` call: + +- **`limit`**: validated through `readLimitField` against `RPC::Tuning::bookOffers` (default 60, maximum 100). Values outside the range are clamped or rejected. +- **`marker`**: a `Json::Value` passed opaquely to `getBookPage`, enabling cursor-based pagination across multiple calls without server-side state. +- **`domain`**: a hex `uint256` parsed from the `domain` JSON field, stored in `std::optional` and forwarded as part of the `Book` struct. This scopes the query to offers within a specific domain, a feature added to support partitioned order books. +- **`taker`**: an optional Base58-encoded `AccountID` representing the perspective from which to evaluate offers. When absent, `beast::zero` is passed as a neutral identity. The taker identity matters because quality calculations in `getBookPage` can account for an existing account's balances and trust lines. + +## Delegation to `NetworkOPs::getBookPage` + +`doBookOffers` performs no ledger traversal. After building the fully validated `Book` and resolving all optional fields, it delegates entirely to `context.netOps.getBookPage`, passing the `ReadView`, the book spec, taker, pagination metadata, and a `jvResult` output parameter by reference. Results are written into `jvResult` by `getBookPage`, and the handler then stamps the response with `feeMediumBurdenRPC` to reflect the non-trivial read cost of scanning offer directory pages. + +The clean separation between the parsing/validation surface here and the iteration logic in `NetworkOPsImp::getBookPage` means that the same traversal engine is also reused by the `Subscribe.cpp` handler for streaming order book subscriptions — both call `getBookPage` with identical signatures. + +## Error Taxonomy + +The handler produces a precise set of error codes that map semantically to which side of the book was malformed: `rpcSRC_CUR_MALFORMED` and `rpcSRC_ISR_MALFORMED` for problems in `taker_pays`; `rpcDST_AMT_MALFORMED` and `rpcDST_ISR_MALFORMED` for problems in `taker_gets`. The symmetry is maintained through the lambda-based error selector in the two parse functions, keeping the discriminating logic local rather than requiring callers to pass separate error codes. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp.ai.json b/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp.ai.json new file mode 100644 index 0000000000..67cc65011b --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp.ai.json @@ -0,0 +1,644 @@ +{ + "args": [ + { + "lineno": 16, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doDepositAuthorized", + "parseBase58", + "RPC::lookupLedger", + "ledger->exists", + "ledger->read", + "uint256::parseHex", + "ledger->read (credential)" + ], + "entry_point": "doDepositAuthorized", + "purpose": "Handles the DepositAuthorized RPC request, validating input fields, checking ledger state, and verifying credentials if present.", + "validation_points": [ + "params.isMember(jss::source_account)", + "params[jss::source_account].isString()", + "parseBase58(params[jss::source_account].asString())", + "params.isMember(jss::destination_account)", + "params[jss::destination_account].isString()", + "parseBase58(params[jss::destination_account].asString())", + "RPC::lookupLedger", + "ledger->exists(keylet::account(srcAcct))", + "ledger->read(keylet::account(dstAcct))", + "params.isMember(jss::credentials)", + "creds.isArray() && creds", + "creds.size() <= maxCredentialsArraySize", + "jo.isString() (for each credential)", + "credH.parseHex(credS) (for each credential)", + "ledger->read(keylet::credential(credH)) (for each credential)" + ] + } + ], + "data_flows": [ + { + "field": "source_account", + "flow": [ + "context.params", + "params[jss::source_account]", + "parseBase58(...)", + "srcAcct", + "ledger->exists(keylet::account(srcAcct))" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Checked for presence (isMember)", + "Checked for type (isString)", + "Parsed from base58 to AccountID" + ], + "validated_at": "Immediately upon entry in doDepositAuthorized" + }, + { + "field": "destination_account", + "flow": [ + "context.params", + "params[jss::destination_account]", + "parseBase58(...)", + "dstAcct", + "ledger->read(keylet::account(dstAcct))" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Checked for presence (isMember)", + "Checked for type (isString)", + "Parsed from base58 to AccountID" + ], + "validated_at": "Immediately after source_account validation" + }, + { + "field": "ledger_hash / ledger_index", + "flow": [ + "context.params", + "RPC::lookupLedger(ledger, context)", + "ledger" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Validated and resolved to a ledger pointer" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "credentials", + "flow": [ + "context.params", + "params[jss::credentials]", + "creds (local variable)", + "for each jo in creds", + "jo.asString()", + "uint256 credH.parseHex(credS)", + "ledger->read(keylet::credential(credH))" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Checked for presence (isMember)", + "Checked for type (isArray)", + "Checked for non-empty", + "Checked for max array size", + "Each element checked for type (isString)", + "Each string parsed as hex to uint256" + ], + "validated_at": "If present, validated in credentialsPresent block" + } + ], + "description": "Implements the doDepositAuthorized RPC handler, which checks if a deposit from a source account to a destination account is authorized, optionally using credentials, in the XRPL ledger.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "source_account", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::source_account) at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for source_account", + "why_false_positive": "params.isMember(jss::source_account) validates source_account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "source_account", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::source_account].isString() at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for source_account", + "why_false_positive": "params[jss::source_account].isString() validates source_account for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "source_account", + "type", + "validation", + "check" + ], + "evidence": "params[jss::source_account].isString() at doDepositAuthorized", + "issue_pattern": "Missing type validation for source_account", + "why_false_positive": "params[jss::source_account].isString() validates source_account type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "source_account", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58(params[jss::source_account].asString()) at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for source_account", + "why_false_positive": "parseBase58(params[jss::source_account].asString()) validates source_account for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "source_account", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58(params[jss::source_account].asString()) at doDepositAuthorized", + "issue_pattern": "Missing format validation for source_account", + "why_false_positive": "parseBase58(params[jss::source_account].asString()) validates source_account format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "destination_account", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::destination_account) at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for destination_account", + "why_false_positive": "params.isMember(jss::destination_account) validates destination_account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "destination_account", + "empty", + "string", + "validation" + ], + "evidence": "params[jss::destination_account].isString() at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for destination_account", + "why_false_positive": "params[jss::destination_account].isString() validates destination_account for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "destination_account", + "type", + "validation", + "check" + ], + "evidence": "params[jss::destination_account].isString() at doDepositAuthorized", + "issue_pattern": "Missing type validation for destination_account", + "why_false_positive": "params[jss::destination_account].isString() validates destination_account type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "destination_account", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58(params[jss::destination_account].asString()) at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for destination_account", + "why_false_positive": "parseBase58(params[jss::destination_account].asString()) validates destination_account for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "destination_account", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58(params[jss::destination_account].asString()) at doDepositAuthorized", + "issue_pattern": "Missing format validation for destination_account", + "why_false_positive": "parseBase58(params[jss::destination_account].asString()) validates destination_account format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash / ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger(ledger, context) at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for ledger_hash / ledger_index", + "why_false_positive": "RPC::lookupLedger(ledger, context) validates ledger_hash / ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "source_account (ledger existence)", + "empty", + "string", + "validation" + ], + "evidence": "ledger->exists(keylet::account(srcAcct)) at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for source_account (ledger existence)", + "why_false_positive": "ledger->exists(keylet::account(srcAcct)) validates source_account (ledger existence) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "destination_account (ledger existence)", + "empty", + "string", + "validation" + ], + "evidence": "ledger->read(keylet::account(dstAcct)) at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for destination_account (ledger existence)", + "why_false_positive": "ledger->read(keylet::account(dstAcct)) validates destination_account (ledger existence) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credentials", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::credentials) at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for credentials", + "why_false_positive": "params.isMember(jss::credentials) validates credentials for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "credentials", + "empty", + "string", + "validation" + ], + "evidence": "creds.isArray() && creds at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for credentials", + "why_false_positive": "creds.isArray() && creds validates credentials for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "credentials", + "empty", + "string", + "validation" + ], + "evidence": "creds.size() > maxCredentialsArraySize at doDepositAuthorized", + "issue_pattern": "Missing empty string validation for credentials", + "why_false_positive": "creds.size() > maxCredentialsArraySize validates credentials for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "credentials", + "range", + "bounds", + "validation" + ], + "evidence": "creds.size() > maxCredentialsArraySize at doDepositAuthorized", + "issue_pattern": "Missing range validation for credentials", + "why_false_positive": "creds.size() > maxCredentialsArraySize validates credentials range" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 16, + "name": "doDepositAuthorized" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is tightly coupled to the doDepositAuthorized function. Typical test coverage would be found in RPC handler tests, likely in files such as 'test/rpc/DepositAuthorized_test.cpp' or similar. Tests should cover: missing/invalid source_account, missing/invalid destination_account, malformed account IDs, missing/invalid credentials array, credentials array too long, invalid credential strings, and non-existent accounts in the ledger. Gaps may exist if there are no tests for edge cases like empty credentials arrays, credentials with invalid hex, or credentials that do not exist in the ledger. Also, negative tests for malformed JSON types (e.g., integer instead of string) should be verified.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss::, RPC::, parseBase58, etc.)", + "validation_layer": "entry_point (doDepositAuthorized handler)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::source_account)", + "field": "source_account", + "location": "doDepositAuthorized", + "validated_by": "params.isMember(jss::source_account)", + "validates": [ + "field is present in input" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::make_error(rpcINVALID_PARAMS, ...)", + "field": "source_account", + "location": "doDepositAuthorized", + "validated_by": "params[jss::source_account].isString()", + "validates": [ + "field is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_MALFORMED)", + "field": "source_account", + "location": "doDepositAuthorized", + "validated_by": "parseBase58(params[jss::source_account].asString())", + "validates": [ + "field is valid base58 AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::destination_account)", + "field": "destination_account", + "location": "doDepositAuthorized", + "validated_by": "params.isMember(jss::destination_account)", + "validates": [ + "field is present in input" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::make_error(rpcINVALID_PARAMS, ...)", + "field": "destination_account", + "location": "doDepositAuthorized", + "validated_by": "params[jss::destination_account].isString()", + "validates": [ + "field is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcACT_MALFORMED)", + "field": "destination_account", + "location": "doDepositAuthorized", + "validated_by": "parseBase58(params[jss::destination_account].asString())", + "validates": [ + "field is valid base58 AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 0.9, + "error_thrown": "result from lookupLedger (various errors)", + "field": "ledger_hash / ledger_index", + "location": "doDepositAuthorized", + "validated_by": "RPC::lookupLedger(ledger, context)", + "validates": [ + "ledger exists and is accessible" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcSRC_ACT_NOT_FOUND, result)", + "field": "source_account (ledger existence)", + "location": "doDepositAuthorized", + "validated_by": "ledger->exists(keylet::account(srcAcct))", + "validates": [ + "source account exists in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcDST_ACT_NOT_FOUND, result)", + "field": "destination_account (ledger existence)", + "location": "doDepositAuthorized", + "validated_by": "ledger->read(keylet::account(dstAcct))", + "validates": [ + "destination account exists in ledger" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (flag only, not error)", + "field": "credentials", + "location": "doDepositAuthorized", + "validated_by": "params.isMember(jss::credentials)", + "validates": [ + "credentials field is present (optional)" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::make_error(rpcINVALID_PARAMS, ...)", + "field": "credentials", + "location": "doDepositAuthorized", + "validated_by": "creds.isArray() && creds", + "validates": [ + "credentials is a non-empty array" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "not shown in snippet, but likely error/exception", + "field": "credentials", + "location": "doDepositAuthorized", + "validated_by": "creds.size() > maxCredentialsArraySize", + "validates": [ + "credentials array does not exceed max size" + ], + "validation_type": "range" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp.ai.md b/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp.ai.md new file mode 100644 index 0000000000..9a5da1c8e5 --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/DepositAuthorized.cpp.ai.md @@ -0,0 +1,61 @@ +# `DepositAuthorized.cpp` — RPC Handler for Deposit Authorization Queries + +## Purpose and Context + +This file implements `doDepositAuthorized`, the RPC handler behind the `deposit_authorized` API call. Its sole job is to answer the question: *given a source account, a destination account, and an optional set of credential IDs, would a payment from source to destination currently be authorized under the destination's DepositAuth rules?* This is a pure read-only query — no ledger state is mutated. + +The handler exists because the XRPL DepositAuth amendment (flag `lsfDepositAuth` on an account) lets destinations opt in to a gatekeeping model where incoming payments must be pre-approved. Before constructing and signing a transaction, a sender or an application can call this endpoint to check authorization without paying a transaction fee or risking a `tecNO_PERMISSION` error. + +## Authorization Logic + +After resolving the ledger and verifying both accounts exist, the handler computes a single `reqAuth` boolean: + +```cpp +bool const reqAuth = ((sleDest->getFlags() & lsfDepositAuth) != 0u) && (srcAcct != dstAcct); +``` + +The self-deposit short-circuit (`srcAcct != dstAcct`) is intentional: an account sending to itself is always permitted regardless of the DepositAuth flag. This mirrors the rule enforced at transaction-apply time. + +When `reqAuth` is true, authorization can be satisfied in one of two ways: + +1. **Account-based preauthorization**: the destination has posted a `DepositPreauth` ledger object keyed by `keylet::depositPreauth(dstAcct, srcAcct)`, explicitly whitelisting the source account. + +2. **Credential-based preauthorization**: the caller provides credential IDs, those credentials are valid for the source, and the destination has posted a `DepositPreauth` object keyed by `keylet::depositPreauth(dstAcct, sorted)`, where `sorted` is a canonical `std::set>` of `(issuer, credentialType)` pairs derived from the supplied credentials. + +These two checks are ORed together — if either passes, `deposit_authorized` is `true`. + +## Credential Validation Pipeline + +The optional `credentials` parameter accepts an array of `uint256` credential hashes (hex-encoded). For each credential, the handler: + +1. Parses the hex string to a `uint256` key. +2. Looks up the `Credential` SLE via `keylet::credential(credH)`. +3. Confirms the credential has the `lsfAccepted` flag set — unaccepted (pending) credentials are invalid. +4. Calls `credentials::checkExpired` against the ledger's `parentCloseTime`, rejecting credentials whose `sfExpiration` has passed. +5. Confirms `sfSubject` matches `srcAcct` — credentials must belong to the source account, not some other party. +6. Inserts `(sfIssuer, sfCredentialType)` into `sorted`; a duplicate insertion means the caller supplied redundant credentials, which is an error. + +The sorted-set representation is not incidental. The on-ledger `DepositPreauth` object for credential-based authorization is keyed using precisely this sorted canonical form, so building the same structure here enables a deterministic, single-lookup existence check against the ledger. + +## The `lifeExtender` Pattern + +The `sorted` set stores `Slice` values — non-owning, pointer-based views into the `sfCredentialType` field data inside each `SLE`. The SLEs themselves are reference-counted `shared_ptr` objects managed by the ledger cache. Inserting a `Slice` into the set and then allowing the `shared_ptr` to drop out of scope would leave a dangling reference. + +`lifeExtender` is a `std::vector>` that does nothing except hold extra references to keep those SLEs alive for exactly as long as `sorted` is in scope. Both variables live on the same stack frame, so the SLEs are guaranteed alive through the `keylet::depositPreauth(dstAcct, sorted)` call. This is a deliberate RAII lifetime extension, not incidental storage. + +## Validation Architecture + +Input validation is layered to provide precise error codes: + +- **Presence and type**: `isMember` + `isString` → `rpcINVALID_PARAMS`. +- **Format**: `parseBase58` → `rpcACT_MALFORMED` for invalid account addresses. +- **Ledger resolution**: `RPC::lookupLedger` handles `ledger_hash`/`ledger_index` with its own error propagation. +- **Ledger existence**: `rpcSRC_ACT_NOT_FOUND` / `rpcDST_ACT_NOT_FOUND` for accounts that don't exist in the chosen ledger. +- **Credential semantics**: `rpcBAD_CREDENTIALS` with specific reason strings for each failure mode (not found, not accepted, expired, wrong subject, duplicates). +- **Array bounds**: the credential array is capped at `maxCredentialsArraySize` (8, from `Protocol.h`) before iteration, preventing unbounded ledger reads. + +The destination account is read with `ledger->read` (not just `ledger->exists`) because the SLE flags must be inspected. The source account only needs an existence check, so the cheaper `ledger->exists` is used there. + +## Relationship to Transaction Processing + +This handler mirrors the authorization checks performed at transaction-apply time by `verifyDepositPreauth` (declared in `CredentialHelpers.h`). The key difference is that `verifyDepositPreauth` operates on an `ApplyView` and can mutate state (deleting expired credentials as a side effect), while this handler uses a read-only `ReadView`. As a consequence, the RPC query may return `deposit_authorized = true` for a credential that is currently valid but becomes expired before the transaction is submitted — the handler deliberately uses `parentCloseTime` (the most recently validated close time) rather than any speculative future time, so results are as current as the selected ledger allows. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp.ai.json b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp.ai.json new file mode 100644 index 0000000000..2e42c4d92c --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp.ai.json @@ -0,0 +1,578 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doGetAggregatePrice", + "iteratePriceData", + "getStats" + ], + "entry_point": "doGetAggregatePrice", + "purpose": "Handles the RPC request to get aggregate price data from the orderbook, traverses historical price data, and computes statistics.", + "validation_points": [ + "doGetAggregatePrice: Validates input parameters and ledger state before calling iteratePriceData.", + "iteratePriceData: Validates sle (shared_ptr) and chain (STObject const*) for nullptr, checks history count, validates prevTx (sfPreviousTxnID) and prevSeq (sfPreviousTxnLgrSeq) fields.", + "getStats: Assumes valid iterators and data, no explicit validation." + ] + } + ], + "data_flows": [ + { + "field": "sle (shared_ptr)", + "flow": [ + "doGetAggregatePrice: sle is fetched from ledger", + "iteratePriceData: sle is passed as argument and checked for nullptr", + "Used as oracle and chain pointers for further traversal" + ], + "origin": "Obtained from ledger lookup in doGetAggregatePrice", + "transformations": [ + "Checked for nullptr before dereferencing", + "Used to access fields and traverse historical data" + ], + "validated_at": "iteratePriceData (nullptr check at function start)" + }, + { + "field": "chain (STObject const*)", + "flow": [ + "iteratePriceData: chain initialized from sle.get()", + "Used to access sfPreviousTxnID and sfPreviousTxnLgrSeq", + "Updated in loop to point to historical nodes" + ], + "origin": "Initially points to sle.get()", + "transformations": [ + "Checked for nullptr before use", + "Updated to point to new/modified nodes in metadata" + ], + "validated_at": "iteratePriceData (nullptr check and loop exit conditions)" + }, + { + "field": "history (iteration count)", + "flow": [ + "iteratePriceData: history incremented each loop", + "Checked against maxHistory (3)" + ], + "origin": "Local variable in iteratePriceData", + "transformations": [ + "Incremented per iteration" + ], + "validated_at": "iteratePriceData (if (++history > maxHistory) return;)" + }, + { + "field": "prevTx (sfPreviousTxnID)", + "flow": [ + "iteratePriceData: prevTx extracted from chain", + "Used to fetch transaction metadata from ledger" + ], + "origin": "Extracted from chain->getFieldH256(sfPreviousTxnID)", + "transformations": [ + "Validated by getFieldH256 (throws if missing)" + ], + "validated_at": "iteratePriceData (getFieldH256 throws if field missing)" + }, + { + "field": "prevSeq (sfPreviousTxnLgrSeq)", + "flow": [ + "iteratePriceData: prevSeq extracted from chain", + "Used to fetch previous ledger" + ], + "origin": "Extracted from chain->getFieldU32(sfPreviousTxnLgrSeq)", + "transformations": [ + "Validated by getFieldU32 (throws if missing)" + ], + "validated_at": "iteratePriceData (getFieldU32 throws if field missing)" + }, + { + "field": "meta (transaction metadata)", + "flow": [ + "iteratePriceData: meta assigned from txRead", + "Used to iterate sfAffectedNodes for historical price data" + ], + "origin": "Fetched from ledger->txRead(prevTx).second", + "transformations": [ + "Checked for nullptr before use" + ], + "validated_at": "iteratePriceData (implicit: if meta is nullptr, loop will not proceed)" + } + ], + "description": "Provides an RPC handler for aggregating and returning price data from multiple oracles, including statistical calculations such as mean, median, and standard deviation, with support for trimming outliers and filtering by time threshold.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfPreviousTxnID", + "validation", + "missing", + "check" + ], + "evidence": "Field sfPreviousTxnID validated by xrpl/protocol/jss, STObject field accessors, LedgerMaster", + "issue_pattern": "Missing validation for sfPreviousTxnID", + "why_false_positive": "xrpl/protocol/jss, STObject field accessors, LedgerMaster validates sfPreviousTxnID automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfPreviousTxnLgrSeq", + "validation", + "missing", + "check" + ], + "evidence": "Field sfPreviousTxnLgrSeq validated by xrpl/protocol/jss, STObject field accessors, LedgerMaster", + "issue_pattern": "Missing validation for sfPreviousTxnLgrSeq", + "why_false_positive": "xrpl/protocol/jss, STObject field accessors, LedgerMaster validates sfPreviousTxnLgrSeq automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfLedgerEntryType", + "validation", + "missing", + "check" + ], + "evidence": "Field sfLedgerEntryType validated by xrpl/protocol/jss, STObject field accessors, LedgerMaster", + "issue_pattern": "Missing validation for sfLedgerEntryType", + "why_false_positive": "xrpl/protocol/jss, STObject field accessors, LedgerMaster validates sfLedgerEntryType automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "sfNewFields", + "validation", + "missing", + "check" + ], + "evidence": "Field sfNewFields validated by xrpl/protocol/jss, STObject field accessors, LedgerMaster", + "issue_pattern": "Missing validation for sfNewFields", + "why_false_positive": "xrpl/protocol/jss, STObject field accessors, LedgerMaster validates sfNewFields automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "sle (shared_ptr)", + "empty", + "string", + "validation" + ], + "evidence": "nullptr check at iteratePriceData (while loop, oracle == nullptr)", + "issue_pattern": "Missing empty string validation for sle (shared_ptr)", + "why_false_positive": "nullptr check validates sle (shared_ptr) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "chain (STObject const*)", + "empty", + "string", + "validation" + ], + "evidence": "nullptr check at iteratePriceData (while loop, chain == nullptr)", + "issue_pattern": "Missing empty string validation for chain (STObject const*)", + "why_false_positive": "nullptr check validates chain (STObject const*) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "history (iteration count)", + "empty", + "string", + "validation" + ], + "evidence": "range check (history > maxHistory) at iteratePriceData (while loop)", + "issue_pattern": "Missing empty string validation for history (iteration count)", + "why_false_positive": "range check (history > maxHistory) validates history (iteration count) for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "history (iteration count)", + "range", + "bounds", + "validation" + ], + "evidence": "range check (history > maxHistory) at iteratePriceData (while loop)", + "issue_pattern": "Missing range validation for history (iteration count)", + "why_false_positive": "range check (history > maxHistory) validates history (iteration count) range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "prevTx (sfPreviousTxnID)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldH256 at iteratePriceData", + "issue_pattern": "Missing empty string validation for prevTx (sfPreviousTxnID)", + "why_false_positive": "getFieldH256 validates prevTx (sfPreviousTxnID) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "prevSeq (sfPreviousTxnLgrSeq)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldU32 at iteratePriceData", + "issue_pattern": "Missing empty string validation for prevSeq (sfPreviousTxnLgrSeq)", + "why_false_positive": "getFieldU32 validates prevSeq (sfPreviousTxnLgrSeq) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger (LedgerMaster::getLedgerBySeq)", + "empty", + "string", + "validation" + ], + "evidence": "nullptr check at iteratePriceData (if (!ledger))", + "issue_pattern": "Missing empty string validation for ledger (LedgerMaster::getLedgerBySeq)", + "why_false_positive": "nullptr check validates ledger (LedgerMaster::getLedgerBySeq) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "meta (ledger->txRead(prevTx).second)", + "empty", + "string", + "validation" + ], + "evidence": "nullptr check at iteratePriceData (meta->getFieldArray)", + "issue_pattern": "Missing empty string validation for meta (ledger->txRead(prevTx).second)", + "why_false_positive": "nullptr check validates meta (ledger->txRead(prevTx).second) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "node.getFieldU16(sfLedgerEntryType)", + "empty", + "string", + "validation" + ], + "evidence": "getFieldU16 at iteratePriceData (for loop over meta->getFieldArray)", + "issue_pattern": "Missing empty string validation for node.getFieldU16(sfLedgerEntryType)", + "why_false_positive": "getFieldU16 validates node.getFieldU16(sfLedgerEntryType) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "node.isFieldPresent(sfNewFields)", + "empty", + "string", + "validation" + ], + "evidence": "isFieldPresent at iteratePriceData (for loop)", + "issue_pattern": "Missing empty string validation for node.isFieldPresent(sfNewFields)", + "why_false_positive": "isFieldPresent validates node.isFieldPresent(sfNewFields) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp", + "functions": [ + { + "args": [ + "context", + "sle", + "f" + ], + "lineno": 18, + "name": "iteratePriceData" + }, + { + "args": [ + "begin", + "end" + ], + "lineno": 74, + "name": "getStats" + }, + { + "args": [ + "context" + ], + "lineno": 104, + "name": "doGetAggregatePrice" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 13, + "name": "xrpl" + } + ], + "test_coverage_notes": "The code is likely covered by integration and unit tests for the orderbook and price oracle RPC endpoints. Tests should exist for normal operation, missing fields (to trigger exceptions), and edge cases (e.g., history > maxHistory, missing metadata, or ledger not found). However, some error paths (e.g., ledger->txRead returning nullptr, or meta missing expected nodes) may not be fully covered, as indicated by LCOV_EXCL_LINE comments. Test files may include: rpc_orderbook_test.cpp, rpc_priceoracle_test.cpp, and possibly ledger/history traversal tests. Gaps may exist in negative/error path coverage, especially for rare or malformed ledger states.", + "validation_architecture": { + "auto_validated_fields": [ + "sfPreviousTxnID", + "sfPreviousTxnLgrSeq", + "sfLedgerEntryType", + "sfNewFields" + ], + "framework": "xrpl/protocol/jss, STObject field accessors, LedgerMaster", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "early return (no exception)", + "field": "sle (shared_ptr)", + "location": "iteratePriceData (while loop, oracle == nullptr)", + "validated_by": "nullptr check", + "validates": [ + "checks if the ledger object exists before dereferencing" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "early return (no exception)", + "field": "chain (STObject const*)", + "location": "iteratePriceData (while loop, chain == nullptr)", + "validated_by": "nullptr check", + "validates": [ + "checks if the chain object exists before dereferencing" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "early return (no exception)", + "field": "history (iteration count)", + "location": "iteratePriceData (while loop)", + "validated_by": "range check (history > maxHistory)", + "validates": [ + "limits the number of historical lookups to maxHistory (3)" + ], + "validation_type": "range" + }, + { + "confidence": 0.9, + "error_thrown": "throws if field missing or wrong type", + "field": "prevTx (sfPreviousTxnID)", + "location": "iteratePriceData", + "validated_by": "getFieldH256", + "validates": [ + "ensures sfPreviousTxnID is present and is a 256-bit hash" + ], + "validation_type": "type|format" + }, + { + "confidence": 0.9, + "error_thrown": "throws if field missing or wrong type", + "field": "prevSeq (sfPreviousTxnLgrSeq)", + "location": "iteratePriceData", + "validated_by": "getFieldU32", + "validates": [ + "ensures sfPreviousTxnLgrSeq is present and is a uint32" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "early return (no exception)", + "field": "ledger (LedgerMaster::getLedgerBySeq)", + "location": "iteratePriceData (if (!ledger))", + "validated_by": "nullptr check", + "validates": [ + "ensures the referenced ledger exists before accessing" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "would throw if meta is nullptr (not explicitly checked here)", + "field": "meta (ledger->txRead(prevTx).second)", + "location": "iteratePriceData (meta->getFieldArray)", + "validated_by": "nullptr check", + "validates": [ + "ensures transaction metadata exists before accessing fields" + ], + "validation_type": "type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "throws if field missing or wrong type", + "field": "node.getFieldU16(sfLedgerEntryType)", + "location": "iteratePriceData (for loop over meta->getFieldArray)", + "validated_by": "getFieldU16", + "validates": [ + "ensures sfLedgerEntryType is present and is a uint16" + ], + "validation_type": "type|format" + }, + { + "confidence": 1.0, + "error_thrown": "returns false if not present (no exception)", + "field": "node.isFieldPresent(sfNewFields)", + "location": "iteratePriceData (for loop)", + "validated_by": "isFieldPresent", + "validates": [ + "checks if sfNewFields exists in the node" + ], + "validation_type": "presence" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp.ai.md b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp.ai.md new file mode 100644 index 0000000000..32651485f5 --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/GetAggregatePrice.cpp.ai.md @@ -0,0 +1,45 @@ +# `GetAggregatePrice.cpp` — RPC Handler for Multi-Oracle Price Aggregation + +This file implements `doGetAggregatePrice`, the server-side handler for the `get_aggregate_price` RPC method. The method exists to give clients a manipulation-resistant price reference for any token pair by aggregating and statistically summarizing data from multiple on-chain `PriceOracle` ledger objects (defined under XLS-47). Because a single oracle could be stale or dishonest, the design expects callers to submit a list of oracle identifiers and the handler synthesizes a defensible aggregate from whatever valid prices it can recover. + +## The `Prices` Bimap and Why It Was Chosen + +The central data structure is a `boost::bimap` aliased as `Prices`: + +```cpp +using Prices = + bimap>, multiset_of>; +``` + +The left side indexes by `lastUpdateTime` in descending order; the right side indexes by `STAmount` in ascending order. Two orthogonal sorting requirements exist simultaneously: time-window filtering needs prices sorted newest-first so stale entries can be erased by calling `prices.left.upper_bound(upperBound)` through `prices.left.end()`, while statistical operations (median, trimmed mean) need prices sorted by value. Keeping two separate containers synchronized manually would be error-prone; the bimap makes both views automatically consistent — erasing an entry from one side removes it from the other. + +## Historical Chain Walking in `iteratePriceData` + +A non-obvious complication arises when an oracle exists on the ledger but was last updated without touching the specific token pair requested. In that case the current `sfPriceDataSeries` won't have an entry for that pair, even though an older version of the oracle did. `iteratePriceData` handles this by following each oracle's transaction history backward through the ledger, up to three hops (`maxHistory = 3`). + +The function maintains two conceptually separate pointers as raw `STObject const*`: + +- **`oracle`** — points to the object that has `sfPriceDataSeries`. Initially this is the live `SLE`; after the first history hop it becomes either `sfNewFields` (if the previous transaction created the oracle) or `sfFinalFields` (if it modified one) inside a `CreatedNode`/`ModifiedNode` metadata entry. +- **`chain`** — points to the object that has `sfPreviousTxnID` and `sfPreviousTxnLgrSeq` for the next hop. Initially it equals `oracle` (both point to the live SLE), but diverges once inside transaction metadata, where those navigation fields live on the `ModifiedNode`/`CreatedNode` wrapper rather than on the nested field object. + +The `prevChain == chain` guard at the top of the loop detects the case where the inner search through `sfAffectedNodes` failed to find an `ltORACLE` entry at all, so the loop exits cleanly rather than looping forever. An `isNew` flag set when `sfNewFields` is present short-circuits on the first history lookup: if the very first previous transaction was a create, there is nothing further behind it to examine. + +This design avoids fetching unnecessary ledgers — the callback `f` returns `true` the moment it finds a matching price, so the loop stops immediately without retrieving history that won't be used. + +## Statistical Computation in `getStats` + +`getStats` takes a begin/end pair of `Prices::right_const_iterator` (the value-sorted view) and returns a tuple of mean, sample standard deviation, and count. It uses `std::accumulate` twice: once to sum the `STAmount` values for the mean, then again to accumulate the squared deviations from that mean. The standard deviation uses the `n-1` denominator (Bessel's correction), appropriate since these are samples from a broader population of potential oracle reports. `root2` computes the square root via XRPL's `Number` type, which carries enough precision for financial arithmetic. + +## Median and Trimmed Mean + +After `getStats` reports full-dataset statistics, the handler computes the median directly from the right (price-ordered) view of the bimap. For even-length datasets it averages the two middle elements; for odd lengths it selects the middle element. The `itAdvance` lambda wraps `std::advance` to return an iterator by value, making the inline arithmetic readable. + +If the caller supplied a `trim` parameter (1–25, validated against `maxTrim = 25`), the trimmed statistics are computed by passing `prices.right.begin() + trimCount` and `prices.right.end() - trimCount` to `getStats`. This symmetric trim removes the same number of extreme low and high prices from both ends of the sorted set, reducing the influence of outliers without discarding the whole dataset. + +## Input Validation Strategy + +The handler uses `std::variant` as the return type for its local `getField` lambda, making parse failure first-class rather than exception-driven. The `getCurrency` lambda is the exception to this pattern — it wraps `currencyFromJson` in a try-catch because that function throws on malformed input. Both approaches convert their errors into `RPC::inject_error` calls before returning early. Up to 200 oracle references are accepted (`maxOracles = 200`); exceeding this or supplying an empty list both produce `rpcORACLE_MALFORMED`. Trim of zero is also rejected (`trim == 0` means no trimming was intended but the field was supplied with an invalid value). + +## Relationship to the Oracle Object Model + +Each oracle is identified by `(account, oracle_document_id)`, resolved to a ledger object via `keylet::oracle`. The `sfPriceDataSeries` field on that object is an array of `{sfBaseAsset, sfQuoteAsset, sfAssetPrice, sfScale}` tuples. The `sfAssetPrice` is a raw `uint64` mantissa, and `sfScale` is an unsigned exponent whose negation is passed to `STAmount` so that `price * 10^(-scale)` is the actual value. This keeps oracle values as integers on-chain while representing fractional prices losslessly. The handler never interprets `sfAssetPrice` as a signed number, matching the on-chain convention enforced by `OracleSet`. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp.ai.json b/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp.ai.json new file mode 100644 index 0000000000..9b82b4463b --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp.ai.json @@ -0,0 +1,192 @@ +{ + "args": [ + { + "lineno": 8, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doNFTBuyOffers", + "enumerateNFTOffers" + ], + "entry_point": "doNFTBuyOffers", + "purpose": "Handles the NFT buy offers RPC request: validates input, parses NFT ID, and enumerates buy offers for the given NFT.", + "validation_points": [ + "doNFTBuyOffers: context.params.isMember(jss::nft_id)", + "doNFTBuyOffers: nftId.parseHex(context.params[jss::nft_id].asString())" + ] + } + ], + "data_flows": [ + { + "field": "nft_id", + "flow": [ + "context.params (input JSON)", + "context.params[jss::nft_id] (accessed as string)", + "nftId.parseHex(...) (parsed to uint256)", + "nftId (used in enumerateNFTOffers)" + ], + "origin": "context.params (JSON RPC request body)", + "transformations": [ + "Checked for presence in JSON object", + "Extracted as string", + "Parsed from hex string to uint256" + ], + "validated_at": "doNFTBuyOffers (presence and hex format)" + } + ], + "description": "Implements the doNFTBuyOffers RPC handler, which returns buy offers for a given NFT ID after validating the input.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "nft_id", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::nft_id) at doNFTBuyOffers", + "issue_pattern": "Missing empty string validation for nft_id", + "why_false_positive": "context.params.isMember(jss::nft_id) validates nft_id for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "nft_id", + "empty", + "string", + "validation" + ], + "evidence": "nftId.parseHex(context.params[jss::nft_id].asString()) at doNFTBuyOffers", + "issue_pattern": "Missing empty string validation for nft_id", + "why_false_positive": "nftId.parseHex(context.params[jss::nft_id].asString()) validates nft_id for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "nft_id", + "format", + "validation", + "invalid" + ], + "evidence": "nftId.parseHex(context.params[jss::nft_id].asString()) at doNFTBuyOffers", + "issue_pattern": "Missing format validation for nft_id", + "why_false_positive": "nftId.parseHex(context.params[jss::nft_id].asString()) validates nft_id format" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 7, + "name": "doNFTBuyOffers" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage would be in the RPC handler test suite, likely in files such as 'test/rpc/NFTBuyOffers_test.cpp' or similar. Tests should cover: missing 'nft_id' field, invalid hex in 'nft_id', and valid requests. Gaps may exist if edge cases (e.g., empty string, non-string types, boundary hex values) are not tested. No direct evidence of test files is provided here, so actual coverage should be verified in the test directory.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss::, RPC::JsonContext, RPC::error helpers)", + "validation_layer": "entry_point" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::nft_id)", + "field": "nft_id", + "location": "doNFTBuyOffers", + "validated_by": "context.params.isMember(jss::nft_id)", + "validates": [ + "Checks if 'nft_id' field is present in the input JSON parameters" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::nft_id)", + "field": "nft_id", + "location": "doNFTBuyOffers", + "validated_by": "nftId.parseHex(context.params[jss::nft_id].asString())", + "validates": [ + "Checks if 'nft_id' is a valid hexadecimal string suitable for uint256" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp.ai.md b/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp.ai.md new file mode 100644 index 0000000000..caa27e1567 --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/NFTBuyOffers.cpp.ai.md @@ -0,0 +1,25 @@ +# `NFTBuyOffers.cpp` — RPC Handler for NFT Buy Offer Enumeration + +## Role in the System + +`NFTBuyOffers.cpp` implements `doNFTBuyOffers`, the server-side handler for the `nft_buy_offers` JSON-RPC method. This method allows clients to retrieve the list of outstanding buy offers for a given NFT on the XRP Ledger. The file is deliberately thin: it owns only input validation and keylet selection, delegating all ledger traversal and response construction to the shared `enumerateNFTOffers` helper. + +## Handler Structure + +`doNFTBuyOffers` follows the standard two-step validation pattern used throughout the RPC handler layer. + +First, it checks for the presence of `nft_id` in the incoming JSON parameters using `context.params.isMember(jss::nft_id)`. If the field is missing, the handler returns immediately via `RPC::missing_field_error`, which emits a well-formed JSON error response without touching the ledger. Second, it attempts to parse the string value as a 256-bit hex identifier using `nftId.parseHex(...)`. A malformed hex string — wrong length, non-hex characters — causes `parseHex` to return false, and the handler responds with `RPC::invalid_field_error`. These two guards ensure that the downstream `enumerateNFTOffers` call always receives a structurally valid `uint256`. + +The actual work then dispatches to `enumerateNFTOffers(context, nftId, keylet::nft_buys(nftId))`. The third argument is the architectural differentiator: `keylet::nft_buys(nftId)` produces a `Keylet` that uniquely identifies the on-ledger directory object holding buy-side NFT offers for this token. The corresponding sell-side handler (`NFTSellOffers.cpp`) is structurally identical, differing only in passing `keylet::nft_sells(nftId)` instead. + +## Relationship to `NFTOffersHelpers.h` + +The real complexity lives in `enumerateNFTOffers` inside `NFTOffersHelpers.h`. That function handles ledger selection (via `RPC::lookupLedger`), existence checks on the offer directory, pagination via `marker`/`limit` parameters, and directory traversal using `forEachItemAfter`. It also serializes each `ltNFTOKEN_OFFER` ledger object into JSON — including fields like `nft_offer_index`, `flags`, `owner`, optional `destination`, optional `expiration`, and `amount` — via `appendNftOfferJson`. The pagination design appends one extra offer beyond the requested limit; if the result set is full, the last entry becomes the `marker` for the next query and is popped from the response, ensuring callers can resume exactly where they left off. + +By sharing `enumerateNFTOffers` between the buy and sell handlers, the codebase avoids duplicating pagination logic, ledger access patterns, and error handling. The only caller-supplied distinction is the keylet — the directory structure on the ledger physically separates buy and sell offers per NFT, and the keylet encodes which side to walk. + +## Design Notes + +The handler does not gate on any feature flags or ledger version checks at this layer; that concern belongs to the registration layer where `doNFTBuyOffers` is wired into the RPC dispatch table. The `context.loadType` is set to `Resource::feeMediumBurdenRPC` inside `enumerateNFTOffers`, reflecting that paginated ledger scans carry non-trivial I/O cost and should be rate-limited accordingly. + +The use of `jss::nft_id` — a compile-time string constant from the `jss` namespace — ensures the field name is consistent across all RPC handler code and cannot silently diverge from the wire protocol due to a typo. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h.ai.json b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h.ai.json new file mode 100644 index 0000000000..ddc3645d57 --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h.ai.json @@ -0,0 +1,33 @@ +{ + "args": [], + "classes": [], + "description": "Provides helper functions for enumerating NFT offers in the XRPL ledger, including JSON serialization of NFT offer objects and paginated directory traversal for RPC handlers.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h", + "functions": [ + { + "args": [ + "app", + "offer", + "offers" + ], + "lineno": 13, + "name": "appendNftOfferJson" + }, + { + "args": [ + "context", + "nftId", + "directory" + ], + "lineno": 36, + "name": "enumerateNFTOffers" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h.ai.md b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h.ai.md new file mode 100644 index 0000000000..c7fec6fd19 --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/NFTOffersHelpers.h.ai.md @@ -0,0 +1,29 @@ +# `NFTOffersHelpers.h` — Shared Enumeration Logic for NFT Offer RPC Handlers + +This header-only module lives in `src/xrpld/rpc/handlers/orderbook/` and provides the shared implementation behind the `nft_buy_offers` and `nft_sell_offers` RPC commands. Both commands need identical pagination and JSON-serialization behavior — they differ only in which ledger directory they traverse (buy-side vs. sell-side). Rather than duplicate that logic, this file factors it into two `inline` free functions that each handler calls with a different `Keylet`. + +## The Two-Function Contract + +`appendNftOfferJson` is a pure serializer: given a single `SLE` representing an `ltNFTOKEN_OFFER` ledger object, it appends a JSON object to an accumulator array. It always emits `nft_offer_index` (the offer's ledger key), `flags`, `owner`, and `amount`. The optional fields `destination` and `expiration` are emitted only when present in the SLE, using `isFieldPresent` rather than accessing them unconditionally — this correctly reflects that these fields are optional in the on-ledger format. + +`enumerateNFTOffers` is the pagination engine. It accepts the full RPC `context`, the target `nftId`, and a `directory` keylet (either `keylet::nft_buys(nftId)` or `keylet::nft_sells(nftId)`). It resolves the ledger, walks the directory with optional cursor resumption, and returns the complete JSON response. + +## Pagination Design + +The pagination pattern follows the standard XRPL RPC cursor model but has a subtle off-by-one nuance worth understanding. The `limit` parameter is bounded by `RPC::Tuning::nftOffers` (min 50, default 250, max 500). + +**Fresh queries (no `marker`):** `reserve` is set to `limit + 1`. `forEachItemAfter` is called with this inflated reserve. If exactly `reserve` items come back, the extra item signals that more pages exist: the last item is captured as the next `marker`, then popped off before serialization. If fewer than `reserve` items come back, the result fits on a single page and no marker is emitted. + +**Resume queries (with `marker`):** The marker string is parsed as a hex `uint256` — the ledger key of the last offer seen in the previous response. The code immediately reads that SLE and validates that its `sfNFTokenID` matches the requested `nftId`. This cross-token check prevents a client from accidentally or maliciously using a marker from one NFT's offer list to paginate through another's. The `sfNFTokenOfferNode` field from that SLE is extracted as `startHint` — this is the directory page number, which `forEachItemAfter` uses to jump directly to the right page rather than scanning from page zero. The marker offer itself is appended immediately (it was the "last seen" item, now it becomes the first item of the new page). `reserve` remains `limit` (not incremented), and `forEachItemAfter` fetches up to `reserve` more items after the marker position. + +## Interaction with `forEachItemAfter` + +The underlying traversal is provided by `forEachItemAfter` from `xrpl/ledger/helpers/DirectoryHelpers.h`. That function walks a linked-list of directory pages, invoking a callback for each entry. The callback here filters strictly for `ltNFTOKEN_OFFER` type objects — any other object type in the directory causes the callback to return `false` and stops iteration. If `forEachItemAfter` itself returns `false` (indicating a corrupted or inconsistent directory structure), `enumerateNFTOffers` returns `rpcINVALID_PARAMS`, treating the marker as invalid. + +## Rate Limiting + +After successful enumeration, `context.loadType = Resource::feeMediumBurdenRPC` is set. This is not an error indicator — it is a signal to the RPC framework's resource tracking system that this request carries a medium cost, which influences throttling decisions for clients who issue many such calls. + +## Why a Header-Only Design + +The `doNFTBuyOffers` and `doNFTSellOffers` implementations in their respective `.cpp` files are trivially thin: parse the `nft_id` parameter, then call `enumerateNFTOffers` with the appropriate directory keylet. The entire substance of both commands lives here. Placing the shared logic in a header with `inline` functions avoids creating a separate `.cpp` translation unit just to share ~100 lines of code, and keeps the relationship between the two handlers immediately visible to anyone reading either handler file. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp.ai.json b/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp.ai.json new file mode 100644 index 0000000000..d7c175e47b --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp.ai.json @@ -0,0 +1,192 @@ +{ + "args": [ + { + "lineno": 8, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doNFTSellOffers", + "enumerateNFTOffers" + ], + "entry_point": "doNFTSellOffers", + "purpose": "Handles the NFT sell offers RPC request: validates input, parses NFT ID, and enumerates sell offers for the given NFT.", + "validation_points": [ + "doNFTSellOffers: context.params.isMember(jss::nft_id)", + "doNFTSellOffers: nftId.parseHex(context.params[jss::nft_id].asString())" + ] + } + ], + "data_flows": [ + { + "field": "nft_id", + "flow": [ + "context.params[jss::nft_id]", + "asString()", + "nftId.parseHex(...)", + "nftId (parsed uint256)", + "enumerateNFTOffers(context, nftId, keylet::nft_sells(nftId))" + ], + "origin": "context.params (JSON RPC request body)", + "transformations": [ + "Extracted from JSON as string", + "Parsed from hex string to uint256" + ], + "validated_at": "doNFTSellOffers: presence checked (isMember), format checked (parseHex)" + } + ], + "description": "Implements the doNFTSellOffers RPC handler, which returns sell offers for a given NFT by validating input and invoking enumerateNFTOffers.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "nft_id", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::nft_id) at doNFTSellOffers", + "issue_pattern": "Missing empty string validation for nft_id", + "why_false_positive": "context.params.isMember(jss::nft_id) validates nft_id for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "nft_id", + "empty", + "string", + "validation" + ], + "evidence": "nftId.parseHex(context.params[jss::nft_id].asString()) at doNFTSellOffers", + "issue_pattern": "Missing empty string validation for nft_id", + "why_false_positive": "nftId.parseHex(context.params[jss::nft_id].asString()) validates nft_id for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "nft_id", + "format", + "validation", + "invalid" + ], + "evidence": "nftId.parseHex(context.params[jss::nft_id].asString()) at doNFTSellOffers", + "issue_pattern": "Missing format validation for nft_id", + "why_false_positive": "nftId.parseHex(context.params[jss::nft_id].asString()) validates nft_id format" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 7, + "name": "doNFTSellOffers" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "Test coverage likely exists in RPC handler or integration test suites for NFT sell offers (e.g., tests exercising the 'nft_sell_offers' RPC method). Validation for missing or invalid 'nft_id' is directly handled and should be tested. Gaps may exist if there are no explicit negative tests for malformed or missing 'nft_id', or if edge cases (e.g., non-string types, boundary values) are not covered. No unit tests are visible in this file; coverage depends on higher-level RPC tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss:: for JSON field names, RPC::* for error handling)", + "validation_layer": "entry_point" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::nft_id)", + "field": "nft_id", + "location": "doNFTSellOffers", + "validated_by": "context.params.isMember(jss::nft_id)", + "validates": [ + "Checks if 'nft_id' field is present in the input JSON parameters" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(jss::nft_id)", + "field": "nft_id", + "location": "doNFTSellOffers", + "validated_by": "nftId.parseHex(context.params[jss::nft_id].asString())", + "validates": [ + "Checks if 'nft_id' is a valid hex string that can be parsed into a uint256" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp.ai.md b/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp.ai.md new file mode 100644 index 0000000000..059e7f7fad --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/NFTSellOffers.cpp.ai.md @@ -0,0 +1,17 @@ +# `NFTSellOffers.cpp` — RPC Handler for `nft_sell_offers` + +This file implements `doNFTSellOffers`, the entry-point handler for the `nft_sell_offers` RPC method. Its job is minimal by design: validate the caller-supplied NFT identifier and route the request to the shared enumeration logic in `NFTOffersHelpers.h`. + +## Role in the System + +The `nft_sell_offers` method allows clients to query all active sell offers associated with a specific NFT on the XRP Ledger. The handler lives alongside its mirror image `NFTBuyOffers.cpp` in the `orderbook` handler group — the two files are structurally identical, differing only in which on-ledger directory keylet they pass to the shared helper. + +## Handler Logic + +`doNFTSellOffers` performs two sequential input validations before doing anything else. First it checks that `nft_id` is present in the request parameters, returning a `missing_field_error` immediately if not. Then it attempts to parse the string value as a 256-bit hex identifier via `uint256::parseHex`; a failure here produces an `invalid_field_error`. These two guards mean any downstream code can rely on `nftId` being a well-formed `uint256`. + +The actual work is entirely delegated to `enumerateNFTOffers` (defined inline in `NFTOffersHelpers.h`), which handles ledger selection, pagination via markers, directory traversal using `forEachItemAfter`, and JSON serialization of each offer through `appendNftOfferJson`. The critical argument distinguishing this handler from `doNFTBuyOffers` is the keylet: `keylet::nft_sells(nftId)` targets the NFT's sell-offer directory on the ledger, whereas the buy-offer handler passes `keylet::nft_buys(nftId)` instead. Both directories are maintained by the ledger as linked lists of `ltNFTOKEN_OFFER` objects. + +## Design Choice: Thin Handler, Shared Core + +Keeping the handler file at roughly 12 lines of functional code is a deliberate separation of concerns. Input validation and routing belong here; result construction, ledger access, and pagination logic belong in the shared helper. This avoids duplicating the pagination fence-post arithmetic and marker validation between the buy and sell variants, both of which would otherwise be identical. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/PathFind.cpp.ai.json b/src/xrpld/rpc/handlers/orderbook/PathFind.cpp.ai.json new file mode 100644 index 0000000000..f806166a21 --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/PathFind.cpp.ai.json @@ -0,0 +1,369 @@ +{ + "args": [ + { + "lineno": 11, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doPathFind" + ], + "entry_point": "doPathFind", + "purpose": "Handles the 'path_find' RPC command, dispatching subcommands (create, close, status) and performing validation on input and context.", + "validation_points": [ + "if (context.app.config().PATH_SEARCH_MAX == 0)", + "if (!context.params.isMember(jss::subcommand) || !context.params[jss::subcommand].isString())", + "if (!context.infoSub)", + "if (sSubCommand == ...)", + "if (!request)" + ] + }, + { + "call_chain": [ + "doPathFind", + "context.app.getPathRequestManager().makePathRequest" + ], + "entry_point": "doPathFind (subcommand == 'create')", + "purpose": "Handles creation of a new path find request, after validation.", + "validation_points": [ + "if (context.app.config().PATH_SEARCH_MAX == 0)", + "if (!context.params.isMember(jss::subcommand) || !context.params[jss::subcommand].isString())", + "if (!context.infoSub)" + ] + }, + { + "call_chain": [ + "doPathFind", + "context.infoSub->getRequest", + "request->doClose or request->doStatus" + ], + "entry_point": "doPathFind (subcommand == 'close' or 'status')", + "purpose": "Handles closing or status querying of an existing path find request.", + "validation_points": [ + "if (!request)" + ] + } + ], + "data_flows": [ + { + "field": "context.app.config().PATH_SEARCH_MAX", + "flow": [ + "config file/env", + "context.app.config()", + "doPathFind validation" + ], + "origin": "Application configuration (config file/env)", + "transformations": [], + "validated_at": "if (context.app.config().PATH_SEARCH_MAX == 0)" + }, + { + "field": "context.params[jss::subcommand]", + "flow": [ + "RPC JSON input", + "context.params", + "doPathFind validation", + "sSubCommand assignment", + "subcommand dispatch" + ], + "origin": "RPC JSON request input", + "transformations": [ + "Checked for presence and type (isString)", + "Converted to std::string (asString)" + ], + "validated_at": "if (!context.params.isMember(jss::subcommand) || !context.params[jss::subcommand].isString())" + }, + { + "field": "context.infoSub", + "flow": [ + "RPC session setup", + "context.infoSub", + "doPathFind validation", + "used for setApiVersion, clearRequest, getRequest" + ], + "origin": "Session/subscription context (set up by RPC framework)", + "transformations": [], + "validated_at": "if (!context.infoSub)" + }, + { + "field": "sSubCommand", + "flow": [ + "context.params[jss::subcommand]", + "asString()", + "sSubCommand", + "if/else dispatch" + ], + "origin": "context.params[jss::subcommand].asString()", + "transformations": [ + "String conversion" + ], + "validated_at": "if (sSubCommand == ...)" + }, + { + "field": "request (InfoSubRequest::pointer)", + "flow": [ + "context.infoSub", + "getRequest()", + "request", + "doClose/doStatus" + ], + "origin": "context.infoSub->getRequest()", + "transformations": [], + "validated_at": "if (!request)" + } + ], + "description": "Implements the doPathFind RPC handler for XRPL, handling pathfinding subcommands ('create', 'close', 'status') for payment routing, using the application's PathRequestManager and InfoSub system.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "subcommand (via jss::subcommand and isString)", + "validation", + "missing", + "check" + ], + "evidence": "Field subcommand (via jss::subcommand and isString) validated by jss:: (JSON field tags), rpcError (error handling), Json::Value (JSON parsing)", + "issue_pattern": "Missing validation for subcommand (via jss::subcommand and isString)", + "why_false_positive": "jss:: (JSON field tags), rpcError (error handling), Json::Value (JSON parsing) validates subcommand (via jss::subcommand and isString) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "infoSub (presence checked before use)", + "validation", + "missing", + "check" + ], + "evidence": "Field infoSub (presence checked before use) validated by jss:: (JSON field tags), rpcError (error handling), Json::Value (JSON parsing)", + "issue_pattern": "Missing validation for infoSub (presence checked before use)", + "why_false_positive": "jss:: (JSON field tags), rpcError (error handling), Json::Value (JSON parsing) validates infoSub (presence checked before use) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "PATH_SEARCH_MAX (config parameter)", + "empty", + "string", + "validation" + ], + "evidence": "if (context.app.config().PATH_SEARCH_MAX == 0) at doPathFind", + "issue_pattern": "Missing empty string validation for PATH_SEARCH_MAX (config parameter)", + "why_false_positive": "if (context.app.config().PATH_SEARCH_MAX == 0) validates PATH_SEARCH_MAX (config parameter) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "subcommand (context.params[jss::subcommand])", + "empty", + "string", + "validation" + ], + "evidence": "if (!context.params.isMember(jss::subcommand) || !context.params[jss::subcommand].isString()) at doPathFind", + "issue_pattern": "Missing empty string validation for subcommand (context.params[jss::subcommand])", + "why_false_positive": "if (!context.params.isMember(jss::subcommand) || !context.params[jss::subcommand].isString()) validates subcommand (context.params[jss::subcommand]) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "infoSub (context.infoSub)", + "empty", + "string", + "validation" + ], + "evidence": "if (!context.infoSub) at doPathFind", + "issue_pattern": "Missing empty string validation for infoSub (context.infoSub)", + "why_false_positive": "if (!context.infoSub) validates infoSub (context.infoSub) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "subcommand value (context.params[jss::subcommand])", + "empty", + "string", + "validation" + ], + "evidence": "if (sSubCommand == ...) at doPathFind", + "issue_pattern": "Missing empty string validation for subcommand value (context.params[jss::subcommand])", + "why_false_positive": "if (sSubCommand == ...) validates subcommand value (context.params[jss::subcommand]) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "InfoSubRequest (context.infoSub->getRequest())", + "empty", + "string", + "validation" + ], + "evidence": "if (!request) at doPathFind (in 'close' and 'status' subcommands)", + "issue_pattern": "Missing empty string validation for InfoSubRequest (context.infoSub->getRequest())", + "why_false_positive": "if (!request) validates InfoSubRequest (context.infoSub->getRequest()) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/PathFind.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 10, + "name": "doPathFind" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this handler is likely found in integration or functional test suites for the RPC interface, such as 'rpc_path_find_test.cpp', 'PathRequest_test.cpp', or similar files in the test/rpc or test/app directories. These tests should cover valid/invalid subcommands, missing/invalid parameters, and subscription context. However, edge cases such as missing InfoSub, invalid config, or malformed subcommands may not be exhaustively tested. There may be limited or no unit tests for the validation logic itself, as it is tightly coupled to the RPC context and session management.", + "validation_architecture": { + "auto_validated_fields": [ + "subcommand (via jss::subcommand and isString)", + "infoSub (presence checked before use)" + ], + "framework": "jss:: (JSON field tags), rpcError (error handling), Json::Value (JSON parsing)", + "validation_layer": "entry_point (doPathFind is the RPC handler entry)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNOT_SUPPORTED)", + "field": "PATH_SEARCH_MAX (config parameter)", + "location": "doPathFind", + "validated_by": "if (context.app.config().PATH_SEARCH_MAX == 0)", + "validates": [ + "Ensures pathfinding is enabled in config" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "subcommand (context.params[jss::subcommand])", + "location": "doPathFind", + "validated_by": "if (!context.params.isMember(jss::subcommand) || !context.params[jss::subcommand].isString())", + "validates": [ + "Checks that 'subcommand' field exists in params", + "Checks that 'subcommand' is a string" + ], + "validation_type": "presence|type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNO_EVENTS)", + "field": "infoSub (context.infoSub)", + "location": "doPathFind", + "validated_by": "if (!context.infoSub)", + "validates": [ + "Checks that infoSub subscription object is present" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "subcommand value (context.params[jss::subcommand])", + "location": "doPathFind", + "validated_by": "if (sSubCommand == ...)", + "validates": [ + "Checks that subcommand is one of: 'create', 'close', 'status'" + ], + "validation_type": "business_logic|enum" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNO_PF_REQUEST)", + "field": "InfoSubRequest (context.infoSub->getRequest())", + "location": "doPathFind (in 'close' and 'status' subcommands)", + "validated_by": "if (!request)", + "validates": [ + "Checks that a path find request exists for the subscription" + ], + "validation_type": "presence" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/PathFind.cpp.ai.md b/src/xrpld/rpc/handlers/orderbook/PathFind.cpp.ai.md new file mode 100644 index 0000000000..e34c12a4ab --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/PathFind.cpp.ai.md @@ -0,0 +1,44 @@ +# `PathFind.cpp` — RPC Handler for Persistent Pathfinding Subscriptions + +## Role in the System + +This file implements `doPathFind`, the RPC entry point for the XRPL `path_find` command. Unlike `ripple_path_find` (which answers synchronously), `path_find` is a **subscription-oriented** command that pushes repeated payment-path updates to a connected client as ledger state evolves. Its small size belies its architectural significance: it sits at the boundary between the stateless RPC dispatch layer and the stateful, asynchronous `PathRequestManager` subsystem. + +## Why a Subscription Model + +Payment paths on XRPL are affected by every ledger close — order books shift, trust lines change, liquidity moves. A static one-shot answer ages quickly. The `path_find` API reflects this by allowing a client to register a long-lived request; the server then re-evaluates paths after each ledger and pushes updated results through the same WebSocket connection. This is why the handler unconditionally rejects calls that arrive without an active `infoSub` subscription object (`rpcNO_EVENTS`): HTTP connections have no persistent channel to push updates through, so the feature only makes sense over WebSocket. + +## The Three Subcommands + +The handler is a dispatcher over three subcommands: + +**`create`** initializes a new path-find request for the current subscriber. The handler first assigns `Resource::feeHeavyBurdenRPC` to `context.loadType`, reflecting the computational cost of path searches (they traverse order books and credit lines across the full ledger graph). It then calls `context.infoSub->clearRequest()` to discard any prior active request on this connection before delegating to `PathRequestManager::makePathRequest()`. This replace-not-accumulate design means a client always has at most one active path-find per WebSocket connection. + +**`close`** tears down the current path-find request. The handler retrieves the `InfoSubRequest` pointer via `context.infoSub->getRequest()` and returns `rpcNO_PF_REQUEST` if none exists, then calls `doClose()` on the `PathRequest` object and clears the reference in `infoSub`. No resource charging occurs here — the cost was borne at creation. + +**`status`** is a lightweight query that returns the most recently computed path result without triggering a new computation. It also checks for an active request and delegates to `PathRequest::doStatus()`. + +An unrecognised subcommand string falls through all three checks and returns `rpcINVALID_PARAMS`. + +## Validation Layering + +The validations are intentionally ordered from cheapest to most expensive: + +1. **Config gate** — `PATH_SEARCH_MAX == 0` rejects immediately if pathfinding is administratively disabled on this node, before any parameter parsing occurs. +2. **Parameter presence and type** — the `subcommand` field must exist and be a JSON string; otherwise `rpcINVALID_PARAMS`. +3. **Subscription presence** — `context.infoSub` is checked before any subscription method is called; its absence returns `rpcNO_EVENTS`. +4. **Active request presence** — for `close` and `status`, the existence of a live `InfoSubRequest` on the subscription object is verified; its absence returns `rpcNO_PF_REQUEST`. + +## Key Types and Relationships + +`PathRequest` inherits from `InfoSubRequest`, the abstract base in `InfoSub.h` that defines the `doClose()` and `doStatus()` interface. `InfoSub` holds exactly one `std::shared_ptr` at a time, managed via `setRequest()`, `getRequest()`, and `clearRequest()`. This single-slot design ensures the subscription can only track one active pathfinding session, keeping lifetime management straightforward. + +`PathRequestManager` owns a `std::vector` — weak pointers that are promoted during its periodic `updateAll()` pass. This lets the manager enumerate all living requests without extending their lifetimes: if the owning `InfoSub` is destroyed (WebSocket disconnected), the corresponding `PathRequest` expires automatically, and `updateAll()` silently skips the stale entry. + +The `doPathFind` handler acquires the closed ledger via `context.ledgerMaster.getClosedLedger()` and passes it to `makePathRequest()`. Using the closed (validated) ledger rather than the current open ledger ensures path computations reflect a stable, consistent view of the network state. + +## Architectural Notes + +The heavy-burden charge on `create` but not on `close` or `status` is a deliberate rate-limiting decision. Path computation is expensive; reading back a cached result is cheap. Charging only at initialization prevents abuse without penalising normal use. + +The `context.infoSub->setApiVersion(context.apiVersion)` call on every invocation (before the subcommand dispatch) propagates the client's negotiated API version into the subscription object. This matters for serialization: `PathRequest::doUpdate()` uses this version when formatting the pushed updates, so a client that negotiated v2 always receives v2-formatted path results regardless of when the update fires. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp.ai.json b/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp.ai.json new file mode 100644 index 0000000000..79e3d5566d --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp.ai.json @@ -0,0 +1,344 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doRipplePathFind" + ], + "entry_point": "doRipplePathFind", + "purpose": "Handles the ripple_path_find RPC request, performing validation and dispatching to the pathfinding engine.", + "validation_points": [ + "if (context.app.config().PATH_SEARCH_MAX == 0)", + "if (!context.params.isMember(jss::ledger) && !context.params.isMember(jss::ledger_index) && !context.params.isMember(jss::ledger_hash))", + "if (context.app.getLedgerMaster().getValidatedLedgerAge() > RPC::Tuning::maxValidatedLedgerAge)" + ] + } + ], + "data_flows": [ + { + "field": "PATH_SEARCH_MAX", + "flow": [ + "config file or command line", + "AppConfig object", + "context.app.config().PATH_SEARCH_MAX", + "doRipplePathFind" + ], + "origin": "context.app.config().PATH_SEARCH_MAX (server config)", + "transformations": [ + "Read as integer from config", + "Accessed directly" + ], + "validated_at": "doRipplePathFind: if (context.app.config().PATH_SEARCH_MAX == 0)" + }, + { + "field": "ledger / ledger_index / ledger_hash", + "flow": [ + "Client JSON request", + "context.params", + "doRipplePathFind" + ], + "origin": "context.params (JSON RPC request parameters)", + "transformations": [ + "Checked for presence via isMember" + ], + "validated_at": "doRipplePathFind: if (!context.params.isMember(jss::ledger) && ...)" + }, + { + "field": "validated ledger age", + "flow": [ + "LedgerMaster tracks validated ledger timestamps", + "getValidatedLedgerAge() returns age in seconds", + "doRipplePathFind" + ], + "origin": "context.app.getLedgerMaster().getValidatedLedgerAge()", + "transformations": [ + "Compared to RPC::Tuning::maxValidatedLedgerAge" + ], + "validated_at": "doRipplePathFind: if (context.app.getLedgerMaster().getValidatedLedgerAge() > RPC::Tuning::maxValidatedLedgerAge)" + } + ], + "description": "Implements the deprecated doRipplePathFind RPC handler for pathfinding in the XRPL, handling both default and ledger-specified pathfinding requests, including coroutine/job queue management for asynchronous execution.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger validated by jss:: (JSON field tags), rpcError (error handling), config() (application config validation)", + "issue_pattern": "Missing validation for ledger", + "why_false_positive": "jss:: (JSON field tags), rpcError (error handling), config() (application config validation) validates ledger automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_index validated by jss:: (JSON field tags), rpcError (error handling), config() (application config validation)", + "issue_pattern": "Missing validation for ledger_index", + "why_false_positive": "jss:: (JSON field tags), rpcError (error handling), config() (application config validation) validates ledger_index automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash (via jss::)", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash (via jss::) validated by jss:: (JSON field tags), rpcError (error handling), config() (application config validation)", + "issue_pattern": "Missing validation for ledger_hash (via jss::)", + "why_false_positive": "jss:: (JSON field tags), rpcError (error handling), config() (application config validation) validates ledger_hash (via jss::) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "PATH_SEARCH_MAX (via config)", + "validation", + "missing", + "check" + ], + "evidence": "Field PATH_SEARCH_MAX (via config) validated by jss:: (JSON field tags), rpcError (error handling), config() (application config validation)", + "issue_pattern": "Missing validation for PATH_SEARCH_MAX (via config)", + "why_false_positive": "jss:: (JSON field tags), rpcError (error handling), config() (application config validation) validates PATH_SEARCH_MAX (via config) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "PATH_SEARCH_MAX (config)", + "empty", + "string", + "validation" + ], + "evidence": "if (context.app.config().PATH_SEARCH_MAX == 0) at doRipplePathFind", + "issue_pattern": "Missing empty string validation for PATH_SEARCH_MAX (config)", + "why_false_positive": "if (context.app.config().PATH_SEARCH_MAX == 0) validates PATH_SEARCH_MAX (config) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledger, ledger_index, ledger_hash (request params)", + "empty", + "string", + "validation" + ], + "evidence": "if (!context.params.isMember(jss::ledger) && ...) at doRipplePathFind", + "issue_pattern": "Missing empty string validation for ledger, ledger_index, ledger_hash (request params)", + "why_false_positive": "if (!context.params.isMember(jss::ledger) && ...) validates ledger, ledger_index, ledger_hash (request params) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "validated ledger age", + "empty", + "string", + "validation" + ], + "evidence": "if (context.app.getLedgerMaster().getValidatedLedgerAge() > RPC::Tuning::maxValidatedLedgerAge) at doRipplePathFind", + "issue_pattern": "Missing empty string validation for validated ledger age", + "why_false_positive": "if (context.app.getLedgerMaster().getValidatedLedgerAge() > RPC::Tuning::maxValidatedLedgerAge) validates validated ledger age for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doRipplePathFind" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this handler is likely found in integration or functional test suites for the ripple_path_find RPC. Look for test files such as 'ripple_path_find_test.cpp', 'Path_find_test.cpp', or generic RPC handler tests in the 'test/rpc' or 'test/orderbook' directories. Unit tests may mock the Context and config to test validation branches (e.g., PATH_SEARCH_MAX == 0, missing ledger params, stale validated ledger). Gaps may exist in edge cases (e.g., simultaneous config and ledger errors, coroutine/job queue shutdown scenarios). Template-based validation may be tested elsewhere, but direct coverage of these specific validation branches should be confirmed.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger", + "ledger_index", + "ledger_hash (via jss::)", + "PATH_SEARCH_MAX (via config)" + ], + "framework": "jss:: (JSON field tags), rpcError (error handling), config() (application config validation)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNOT_SUPPORTED)", + "field": "PATH_SEARCH_MAX (config)", + "location": "doRipplePathFind", + "validated_by": "if (context.app.config().PATH_SEARCH_MAX == 0)", + "validates": [ + "Ensures pathfinding is enabled in config" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (defaults to pathfinding defaults)", + "field": "ledger, ledger_index, ledger_hash (request params)", + "location": "doRipplePathFind", + "validated_by": "if (!context.params.isMember(jss::ledger) && ...)", + "validates": [ + "Checks if any ledger specifier is present in params" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNO_NETWORK) or rpcError(rpcNOT_SYNCED)", + "field": "validated ledger age", + "location": "doRipplePathFind", + "validated_by": "if (context.app.getLedgerMaster().getValidatedLedgerAge() > RPC::Tuning::maxValidatedLedgerAge)", + "validates": [ + "Checks if the validated ledger is too old for pathfinding" + ], + "validation_type": "range|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp.ai.md b/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp.ai.md new file mode 100644 index 0000000000..1a836ca7ce --- /dev/null +++ b/src/xrpld/rpc/handlers/orderbook/RipplePathFind.cpp.ai.md @@ -0,0 +1,46 @@ +# `RipplePathFind.cpp` — Deprecated One-Shot Pathfinding RPC Handler + +This file implements `doRipplePathFind`, the handler for the `ripple_path_find` JSON-RPC method. The comment at line 12 is blunt: *"This interface is deprecated."* The successor is `path_find` (implemented in `PathFind.cpp` in the same directory), which uses a WebSocket subscription model to push continuous updates. `ripple_path_find` remains for backwards compatibility — it is a single synchronous-looking RPC call that finds one or more payment paths between two parties and returns the result in the same HTTP response. + +## Two Completely Different Execution Paths + +The function branches sharply on whether the caller supplied a specific ledger (`ledger`, `ledger_index`, or `ledger_hash` in the request params). This is not just an implementation detail — it reflects two entirely different semantics clients can request. + +### Default Path (No Ledger Specified) + +When no ledger is pinned, the handler trusts the network's latest validated state and dispatches to the live path-finding engine via `PathRequestManager::makeLegacyPathRequest()`. This path is only available in networked (non-standalone) mode. Before going further, it checks that the validated ledger is not too stale: if `getValidatedLedgerAge()` exceeds `RPC::Tuning::maxValidatedLedgerAge`, the call is rejected with `rpcNO_NETWORK` (API v1) or `rpcNOT_SYNCED` (API v2+), a version-gated error distinction introduced to give cleaner semantics to newer clients. + +The execution in this branch is asynchronous but appears synchronous to the caller. The `doRipplePathFind` function runs inside a `JobQueue::Coro` — a cooperative coroutine on the server's job queue thread pool. Here is the thread choreography documented at length in the source itself: + +1. `makeLegacyPathRequest()` enqueues a path-finding job and receives a `PathRequest::pointer` back. It also captures a *completion lambda* that will fire when path-finding finishes. +2. If the job was accepted (`request` is non-null), the coroutine calls `context.coro->yield()` — suspending itself and releasing its thread back to the job queue. +3. The path-finding job runs on (potentially) a different job queue thread. Upon completion it invokes the completion lambda. +4. The lambda calls `coroCopy->post()` to re-enqueue this coroutine for execution. If `post()` fails (i.e., the job queue is shutting down and rejecting new work), it falls back to `coroCopy->resume()` — running the coroutine to completion on the *current* thread rather than letting the application hang on shutdown. +5. The coroutine resumes after the `yield()` call and calls `request->doStatus()` to retrieve the completed path-finding result. + +The `coroCopy` variable in the lambda is a deliberate design choice: capturing `context.coro` by value as a `shared_ptr` keeps the coroutine object alive for the duration of the lambda, preventing the storage from being destroyed between `resume()` returning and the lambda returning. This is a subtle lifetime hazard that the code explicitly calls out. + +### Ledger-Specified Path (Synchronous, Historical) + +When the caller pins a specific ledger, the handler is fully synchronous and runs directly without involving coroutines. It calls `RPC::lookupLedger()` to resolve the ledger and then constructs an `RPC::LegacyPathFind` guard before invoking `PathRequestManager::doLegacyPathRequest()`. + +`LegacyPathFind` is a RAII concurrency throttle defined in `LegacyPathFind.h/.cpp`. Its constructor checks whether the path-finding request should be admitted: + +- **Admin callers** (`isUnlimited(context.role)`) bypass all limits and are always admitted. +- **Non-admin callers** face two checks: the job queue's `jtCLIENT` job count must be below `Tuning::maxPathfindJobCount`, and the server must not be locally load-shedding. If both pass, an atomic compare-and-exchange loop increments a static `inProgress` counter, rejecting the request if it would exceed `Tuning::maxPathfindsInProgress`. + +If `LegacyPathFind::isOk()` returns false, the handler returns `rpcTOO_BUSY` immediately. The RAII destructor decrements the counter when the guard goes out of scope, so the concurrency slot is always released regardless of how `doLegacyPathRequest()` exits. + +The result of `doLegacyPathRequest()` is merged with any fields already placed in `jvResult` by `lookupLedger()` (typically ledger metadata). The merge uses `std::move` to avoid copying the string keys. + +## Resource Marking and Gate Checks + +The very first gate — before any branching — checks `config().PATH_SEARCH_MAX == 0`. This allows operators to administratively disable all path-finding at the server level, returning `rpcNOT_SUPPORTED` cleanly. The `loadType` is immediately set to `Resource::feeHeavyBurdenRPC`, ensuring the caller is charged for a heavyweight operation regardless of which path is taken. Path-finding is one of the most compute-intensive operations in the ledger RPC surface. + +## Relationship to `PathFind.cpp` + +`PathFind.cpp` in the same directory handles `path_find`, the non-deprecated WebSocket-based API. It shares the same `PathRequestManager` backend and the same `PATH_SEARCH_MAX` guard, but routes through `makePathRequest()` (subscription-based) rather than `makeLegacyPathRequest()`. The legacy name throughout `PathRequestManager` — `makeLegacyPathRequest`, `doLegacyPathRequest` — traces back to this file being the older of the two. The `PathRequest` class itself has two constructors reflecting this split: one takes an `InfoSub` subscriber for push updates, and one takes a completion callback for the one-shot legacy model. + +## Shutdown Safety + +The detailed shutdown commentary embedded in the source (dated May 2017) highlights a real hazard: during application teardown the job queue stops accepting new work, which can strand a coroutine waiting for a `post()` that will never happen. The `resume()` fallback in the completion lambda is the explicit defense against this scenario, allowing the coroutine to drain on the path-finding thread rather than deadlocking. This pattern recurs elsewhere in the codebase wherever coroutines interact with the job queue during shutdown. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/Feature.cpp.ai.json b/src/xrpld/rpc/handlers/server_info/Feature.cpp.ai.json new file mode 100644 index 0000000000..a3c8cc9768 --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/Feature.cpp.ai.json @@ -0,0 +1,326 @@ +{ + "args": [ + { + "lineno": 13, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doFeature" + ], + "entry_point": "doFeature", + "purpose": "Handles the 'feature' RPC command, validating and processing feature and vetoed parameters, and returning feature amendment info.", + "validation_points": [ + "context.params[jss::feature].isString() - Validates 'feature' is a string if present", + "table.find(...) and feature.parseHex(...) - Validates 'feature' is a known name or valid hex", + "context.role == Role::ADMIN - Validates admin role for 'vetoed' param", + "context.params[jss::vetoed].asBool() - Validates 'vetoed' is a boolean", + "table.getJson(feature, isAdmin) - Validates feature exists and is accessible" + ] + } + ], + "data_flows": [ + { + "field": "feature", + "flow": [ + "context.params[jss::feature]", + "isString() check", + "table.find(...)", + "feature.parseHex(...) if not found by name", + "table.getJson(feature, isAdmin)", + "jvReply" + ], + "origin": "context.params[jss::feature] (JSON RPC input)", + "transformations": [ + "Checked for string type", + "Looked up by name in AmendmentTable", + "Parsed as hex if not found by name", + "Used to fetch feature info as JSON" + ], + "validated_at": "isString() check, table.find/parseHex, table.getJson" + }, + { + "field": "vetoed", + "flow": [ + "context.params[jss::vetoed]", + "isAdmin check (context.role)", + "asBool() check", + "table.veto(feature) or table.unVeto(feature)" + ], + "origin": "context.params[jss::vetoed] (JSON RPC input)", + "transformations": [ + "Checked for admin role", + "Converted to boolean", + "Triggers veto/unveto action on AmendmentTable" + ], + "validated_at": "isAdmin check, asBool()" + }, + { + "field": "features", + "flow": [ + "table.getJson(isAdmin)", + "features[to_string(h)][jss::majority] = ... (majority amendments added)", + "jvReply[jss::features] = features" + ], + "origin": "table.getJson(isAdmin)", + "transformations": [ + "Fetched as JSON object", + "Augmented with majority info" + ], + "validated_at": "N/A (output construction)" + } + ], + "description": "Implements the doFeature RPC handler for querying and modifying amendment (feature) status in the XRPL ledger, including vetoing/unvetoing features and reporting majority status.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "feature", + "empty", + "string", + "validation" + ], + "evidence": "isString() method (Json::Value API) at doFeature (lines 13-18)", + "issue_pattern": "Missing empty string validation for feature", + "why_false_positive": "isString() method (Json::Value API) validates feature for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "feature", + "type", + "validation", + "check" + ], + "evidence": "isString() method (Json::Value API) at doFeature (lines 13-18)", + "issue_pattern": "Missing type validation for feature", + "why_false_positive": "isString() method (Json::Value API) validates feature type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "feature", + "empty", + "string", + "validation" + ], + "evidence": "AmendmentTable::find and parseHex at doFeature (lines 41-46)", + "issue_pattern": "Missing empty string validation for feature", + "why_false_positive": "AmendmentTable::find and parseHex validates feature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "vetoed", + "empty", + "string", + "validation" + ], + "evidence": "isAdmin check (role check) at doFeature (lines 48-52)", + "issue_pattern": "Missing empty string validation for vetoed", + "why_false_positive": "isAdmin check (role check) validates vetoed for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "vetoed", + "empty", + "string", + "validation" + ], + "evidence": "asBool() method (Json::Value API) at doFeature (lines 53-59)", + "issue_pattern": "Missing empty string validation for vetoed", + "why_false_positive": "asBool() method (Json::Value API) validates vetoed for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "vetoed", + "type", + "validation", + "check" + ], + "evidence": "asBool() method (Json::Value API) at doFeature (lines 53-59)", + "issue_pattern": "Missing type validation for vetoed", + "why_false_positive": "asBool() method (Json::Value API) validates vetoed type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "feature", + "empty", + "string", + "validation" + ], + "evidence": "table.getJson(feature, isAdmin) return value at doFeature (lines 61-63)", + "issue_pattern": "Missing empty string validation for feature", + "why_false_positive": "table.getJson(feature, isAdmin) return value validates feature for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/server_info/Feature.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 13, + "name": "doFeature" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in RPC integration and unit tests, e.g., 'server_info', 'feature', or 'amendment' RPC test files. Tests should cover: (1) valid/invalid 'feature' param (string, missing, bad hex), (2) admin/non-admin 'vetoed' param, (3) correct error codes for bad input, (4) correct output for valid requests. Gaps may exist if tests do not cover all error branches (e.g., non-string 'feature', invalid hex, non-admin vetoed, missing feature in getJson). Look for test files like 'rpc_feature_test.cpp', 'server_info_test.cpp', or similar in the test suite.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Json::Value (jsoncpp), jss:: (field name constants), AmendmentTable", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "feature", + "location": "doFeature (lines 13-18)", + "validated_by": "isString() method (Json::Value API)", + "validates": [ + "Ensures 'feature' parameter is a string if present" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcBAD_FEATURE)", + "field": "feature", + "location": "doFeature (lines 41-46)", + "validated_by": "AmendmentTable::find and parseHex", + "validates": [ + "Checks if 'feature' matches a known feature name", + "If not, attempts to parse as hex feature ID", + "If both fail, returns error" + ], + "validation_type": "format|business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNO_PERMISSION)", + "field": "vetoed", + "location": "doFeature (lines 48-52)", + "validated_by": "isAdmin check (role check)", + "validates": [ + "Ensures only admin users can set 'vetoed' field" + ], + "validation_type": "business_logic|authorization" + }, + { + "confidence": 0.8, + "error_thrown": "none (implicit, asBool() coerces)", + "field": "vetoed", + "location": "doFeature (lines 53-59)", + "validated_by": "asBool() method (Json::Value API)", + "validates": [ + "Coerces 'vetoed' to boolean (no explicit error, but type is enforced by usage)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcBAD_FEATURE)", + "field": "feature", + "location": "doFeature (lines 61-63)", + "validated_by": "table.getJson(feature, isAdmin) return value", + "validates": [ + "Checks that the feature exists in the amendment table" + ], + "validation_type": "business_logic|existence" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/Feature.cpp.ai.md b/src/xrpld/rpc/handlers/server_info/Feature.cpp.ai.md new file mode 100644 index 0000000000..84202a539d --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/Feature.cpp.ai.md @@ -0,0 +1,29 @@ +# `Feature.cpp` — RPC Handler for Amendment Queries and Veto Control + +This file implements `doFeature`, the single RPC handler that exposes the XRPL amendment system to external clients and operators. It sits at the boundary between two concerns: read-only status reporting (any client can ask which amendments are known, enabled, or approaching activation) and privileged mutation (administrators can mark amendments as vetoed or un-vetoed, preventing or allowing the local validator from voting for them). + +## Role in the Amendment Lifecycle + +XRPL's amendment process is a multi-phase consensus mechanism: validators express support, amendments gain a supermajority, wait two weeks, and then activate permanently. The `AmendmentTable` manages the voting state internally, while `getMajorityAmendments()` (from `View.h`) reads directly from the validated ledger's amendment SLE (Stored Ledger Entry) to report which amendments have achieved validator supermajority but have not yet crossed the activation threshold. `doFeature` bridges both: it pulls the current majority snapshot from the ledger and overlays it onto the `AmendmentTable`'s JSON output. + +## Two Operating Modes + +The handler operates in one of two modes depending on whether the `feature` parameter is present. + +**Listing all features** (no `feature` param): `table.getJson(isAdmin)` returns a JSON object keyed by amendment hash, which is then augmented with majority timestamps sourced from `getMajorityAmendments(*valLedger)`. The majority data comes from the ledger itself rather than the `AmendmentTable`, because the table's internal vote tracking and the ledger's stored state can briefly diverge — reading from the validated ledger is authoritative. The result is wrapped in a top-level `features` key. + +**Querying or controlling a specific feature** (with `feature` param): The handler first validates that the parameter is a string (returning `rpcINVALID_PARAMS` otherwise), then resolves the feature identifier in two steps. It tries `table.find(name)` to look up by human-readable name; if that fails, it attempts `feature.parseHex(...)` to interpret the parameter as a 256-bit amendment hash. This two-step resolution allows callers to use either the amendment name (e.g., `"PayChan"`) or its raw hash. If both fail, `rpcBAD_FEATURE` is returned. The same error code is also returned if `table.getJson(feature, isAdmin)` yields a falsy value, which catches the edge case where the resolved hash doesn't correspond to a known amendment. + +## Authorization Model + +The handler is registered in `Handler.cpp` with `Role::USER`, meaning any connected client can invoke it. However, the mutation path — setting the `vetoed` field — is gated inside the handler by an explicit `isAdmin` check. Non-admin callers who supply `vetoed` receive `rpcNO_PERMISSION`. This split is deliberate: the registration role controls routing-level access, but the handler itself enforces fine-grained per-operation authorization. + +The `isAdmin` flag also propagates into both overloads of `table.getJson()`, letting the `AmendmentTable` implementation conditionally include or hide sensitive metadata (such as vote counts or internal voting state) from public consumers. + +## The Majority Timestamp Convention + +Majority timestamps in the response are emitted as raw `time_since_epoch().count()` ticks from `NetClock::time_point`. This is XRPL's network clock in seconds since the Ripple epoch (January 1, 2000), not Unix time. Callers must account for this offset. The choice to expose the raw epoch count rather than an ISO string is consistent with how other time values appear in XRPL's RPC responses: compact, machine-parseable, and epoch-agnostic. + +## Structural Pattern + +Like the other handlers in this directory (`Fee.cpp`, `ServerInfo.cpp`, `Manifest.cpp`), `doFeature` is a thin orchestration layer: it validates inputs, delegates to `AmendmentTable` and `LedgerMaster`, and assembles the JSON response. No amendment logic lives here. This keeps the RPC layer decoupled from consensus machinery — changes to how voting works or how features are stored do not require touching this file. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/Fee.cpp.ai.json b/src/xrpld/rpc/handlers/server_info/Fee.cpp.ai.json new file mode 100644 index 0000000000..bdcdfd1775 --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/Fee.cpp.ai.json @@ -0,0 +1,130 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doFee", + "context.app.getTxQ()", + "TxQ::doRPC(context.app)" + ], + "entry_point": "doFee", + "purpose": "Handles the 'fee' RPC request, retrieves fee-related info from the transaction queue, and returns it as JSON.", + "validation_points": [ + "doFee: result.type() == Json::objectValue" + ] + } + ], + "data_flows": [ + { + "field": "result", + "flow": [ + "TxQ::doRPC(context.app) returns Json::Value result", + "doFee receives result", + "doFee checks result.type()", + "doFee returns result if valid" + ], + "origin": "TxQ::doRPC(context.app)", + "transformations": [ + "No transformation; result is passed through if valid" + ], + "validated_at": "doFee: result.type() == Json::objectValue" + }, + { + "field": "context.params", + "flow": [ + "context.params provided to doFee", + "If result.type() is invalid, RPC::inject_error(rpcINTERNAL, context.params) is called", + "context.params returned as error response" + ], + "origin": "RPC::JsonContext (input to doFee)", + "transformations": [ + "Error injected into context.params if validation fails" + ], + "validated_at": "doFee: result.type() == Json::objectValue (failure path)" + } + ], + "description": "Implements the doFee RPC handler, which retrieves current fee information from the transaction queue in the XRPL application.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "result from getTxQ().doRPC(context.app)", + "empty", + "string", + "validation" + ], + "evidence": "type check (result.type() == Json::objectValue) at doFee", + "issue_pattern": "Missing empty string validation for result from getTxQ().doRPC(context.app)", + "why_false_positive": "type check (result.type() == Json::objectValue) validates result from getTxQ().doRPC(context.app) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "result from getTxQ().doRPC(context.app)", + "type", + "validation", + "check" + ], + "evidence": "type check (result.type() == Json::objectValue) at doFee", + "issue_pattern": "Missing type validation for result from getTxQ().doRPC(context.app)", + "why_false_positive": "type check (result.type() == Json::objectValue) validates result from getTxQ().doRPC(context.app) type" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/server_info/Fee.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doFee" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation (result.type() == Json::objectValue) is likely always true in normal operation, as the failure path is marked LCOV_EXCL_START/STOP (excluded from coverage). This suggests the error path is not tested. Tests for the normal path would be in RPC handler or integration tests for the 'fee' RPC, possibly in files like 'server_info_test.cpp', 'rpc_fee_test.cpp', or similar. There is no evidence of direct unit tests for the error path, and coverage tools will not report on it.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "Custom (xrpld/rpc, Json::Value type system)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "UNREACHABLE (likely assertion or abort), RPC::inject_error(rpcINTERNAL, context.params)", + "field": "result from getTxQ().doRPC(context.app)", + "location": "doFee", + "validated_by": "type check (result.type() == Json::objectValue)", + "validates": [ + "Ensures that the result returned from getTxQ().doRPC(context.app) is a JSON object" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/Fee.cpp.ai.md b/src/xrpld/rpc/handlers/server_info/Fee.cpp.ai.md new file mode 100644 index 0000000000..ebc97e083f --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/Fee.cpp.ai.md @@ -0,0 +1,35 @@ +# `Fee.cpp` — `fee` RPC Handler + +## Role in the System + +`Fee.cpp` implements `doFee`, the server-side handler for the XRPL `fee` RPC command. This command exposes the node's current transaction fee environment to clients — covering both the raw drop amounts and the normalized fee-level representation used internally by the transaction queue. It sits in the `server_info` handler directory alongside other node-introspection endpoints such as `ServerInfo.cpp` and `ServerState.cpp`. + +The handler is registered in `Handler.cpp` as: + +```cpp +{"fee", byRef(&doFee), Role::USER, NEEDS_CURRENT_LEDGER} +``` + +Two things stand out here. First, the `Role::USER` designation means any connected client can call this endpoint without admin credentials — fee visibility is intentionally public. Second, the `NEEDS_CURRENT_LEDGER` condition means the framework gates the call: if no current open ledger exists, the handler is never reached and the framework itself returns an error. This pre-condition makes the null-view check inside `TxQ::doRPC` a belt-and-suspenders guard rather than a genuine runtime branch. + +## Design: Pure Delegation + +The body of `doFee` is deliberately minimal: + +```cpp +auto result = context.app.getTxQ().doRPC(context.app); +if (result.type() == Json::objectValue) + return result; +``` + +All actual data collection and JSON construction is owned by `TxQ::doRPC`. That method queries the current open ledger view to compute `TxQ::Metrics`, then assembles a JSON object containing two sub-objects: `levels` (fee levels in normalized fee-level units) and `drops` (absolute amounts in XRP drops). The fields it populates include `ledger_current_index`, `current_queue_size`, `max_queue_size`, `expected_ledger_size`, the four `levels` entries (`reference_level`, `minimum_level`, `median_level`, `open_ledger_level`), and four `drops` entries (`base_fee`, `median_fee`, `minimum_fee`, `open_ledger_fee`). + +This separation is a consistent pattern across the `server_info` directory: the RPC layer provides the dispatch point and structural contract (returns `Json::Value`, handles errors via `inject_error`), while the application layer owns the logic. `doFee` is the most extreme case — it contributes nothing but the `objectValue` type check. + +## The Type Check and the Unreachable Path + +The `result.type() == Json::objectValue` guard protects against `TxQ::doRPC` returning something other than a JSON object. Looking at `TxQ::doRPC`, the only way it returns a non-object is if `app.getOpenLedger().current()` returns null, in which case it returns a default-constructed `Json::Value` (type `nullValue`) after triggering a `BOOST_ASSERT`. That path exists because `TxQ::doRPC` may theoretically be called from contexts beyond just this handler. + +In practice, for `doFee` specifically, this cannot happen: the `NEEDS_CURRENT_LEDGER` condition in Handler.cpp ensures the open ledger is available before `doFee` is invoked. Accordingly, the failure branch is annotated `LCOV_EXCL_START / LCOV_EXCL_STOP` — explicitly excluded from coverage reporting — and the `UNREACHABLE` macro marks it as a programming-error trap rather than a recoverable condition. If somehow reached, `inject_error(rpcINTERNAL, context.params)` mutates the incoming `context.params` and returns it as an error response. + +This pattern — validate the contract even when the contract is already guaranteed by the dispatch layer — reflects XRPL's defensive philosophy: each layer checks its own invariants rather than relying entirely on callers to uphold them. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/Manifest.cpp.ai.json b/src/xrpld/rpc/handlers/server_info/Manifest.cpp.ai.json new file mode 100644 index 0000000000..405694cf70 --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/Manifest.cpp.ai.json @@ -0,0 +1,205 @@ +{ + "args": [ + { + "lineno": 11, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doManifest" + ], + "entry_point": "doManifest", + "purpose": "Handles the 'manifest' RPC request, validates the public_key parameter, parses it, and retrieves manifest details for a validator.", + "validation_points": [ + "params.isMember(jss::public_key)", + "parseBase58(TokenType::NodePublic, requested)" + ] + } + ], + "data_flows": [ + { + "field": "public_key", + "flow": [ + "context.params[jss::public_key]", + "requested (asString())", + "parseBase58(TokenType::NodePublic, requested)", + "pk (PublicKey object or nullopt)", + "context.app.getValidatorManifests().getMasterKey(*pk)", + "mk (master key)", + "context.app.getValidatorManifests().getSigningKey(mk)", + "ek (ephemeral key)", + "context.app.getValidatorManifests().getManifest(mk)", + "context.app.getValidatorManifests().getSequence(mk)", + "context.app.getValidatorManifests().getDomain(mk)", + "ret (JSON response)" + ], + "origin": "context.params (JSON RPC input)", + "transformations": [ + "Extracted as string from JSON", + "Parsed from base58 to PublicKey object", + "Used as lookup key for master key", + "Used as lookup key for ephemeral key, manifest, sequence, domain", + "Encoded to base58 for output" + ], + "validated_at": [ + "params.isMember(jss::public_key)", + "parseBase58(TokenType::NodePublic, requested)" + ] + } + ], + "description": "Implements the doManifest RPC handler, which retrieves and returns validator manifest information (including master and ephemeral keys, sequence, and domain) for a given public key in the XRPL server.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "params.isMember(jss::public_key) at doManifest", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "params.isMember(jss::public_key) validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "public_key", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58(TokenType::NodePublic, requested) at doManifest", + "issue_pattern": "Missing empty string validation for public_key", + "why_false_positive": "parseBase58(TokenType::NodePublic, requested) validates public_key for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "public_key", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58(TokenType::NodePublic, requested) at doManifest", + "issue_pattern": "Missing format validation for public_key", + "why_false_positive": "parseBase58(TokenType::NodePublic, requested) validates public_key format" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/server_info/Manifest.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 11, + "name": "doManifest" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage would be in the RPC handler test suite, likely in files such as 'server_info_test.cpp', 'manifest_test.cpp', or generic RPC tests. Tests should cover: missing public_key, invalid base58 public_key, valid public_key with/without manifest, and edge cases (e.g., no ephemeral key, no domain, etc.). Gaps may exist if there are no negative tests for malformed input, or if the manifest retrieval logic is not directly tested via RPC. Template-based validation is not explicitly unit tested here; coverage depends on parseBase58 and RPC error injection tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl/protocol/jss (field names), parseBase58 (template-based format validation), RPC error handling", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::missing_field_error(jss::public_key)", + "field": "public_key", + "location": "doManifest", + "validated_by": "params.isMember(jss::public_key)", + "validates": [ + "Checks that the 'public_key' field is present in the input JSON params" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::inject_error(rpcINVALID_PARAMS, ret)", + "field": "public_key", + "location": "doManifest", + "validated_by": "parseBase58(TokenType::NodePublic, requested)", + "validates": [ + "Checks that the 'public_key' value is a valid Base58-encoded NodePublic key", + "Checks that the decoded value is a valid PublicKey" + ], + "validation_type": "format" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/Manifest.cpp.ai.md b/src/xrpld/rpc/handlers/server_info/Manifest.cpp.ai.md new file mode 100644 index 0000000000..e597101ca5 --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/Manifest.cpp.ai.md @@ -0,0 +1,36 @@ +# `Manifest.cpp` — `doManifest` RPC Handler + +## Role and Context + +This file implements the `doManifest` RPC endpoint, which allows any caller to look up the validator manifest associated with a given public key. In XRPL's validator infrastructure, a *manifest* is a signed certificate that binds a long-lived master key to a rotating ephemeral signing key, solving a critical operational security problem: if a validator's day-to-day signing key is compromised, the operator can rotate it without requiring every node operator on the network to update their trusted-keys config. The network propagates the new manifest as gossip, and peers update their cached key mappings automatically. + +The `ManifestCache` (declared in `include/xrpl/server/Manifest.h`) is the runtime store for this two-level key system. It maintains two maps: one from master key to the most recent `Manifest` struct, and a reverse map from the current ephemeral signing key back to its master key. The `doManifest` handler exposes a read-only view of this cache over RPC. + +## The Key Lookup Strategy + +The most non-obvious aspect of `doManifest` is how it handles the ambiguity of the caller's input. The user supplies a `public_key` that might be either a master key or a currently-active ephemeral key — the handler doesn't know which. The implementation resolves this without an explicit branch by exploiting the semantics of `getMasterKey`: + +```cpp +auto const mk = context.app.getValidatorManifests().getMasterKey(*pk); +auto const ek = context.app.getValidatorManifests().getSigningKey(mk); +``` + +`getMasterKey` returns the input key unchanged when no reverse mapping exists (i.e., when the key is already a master key or completely unknown). This means `mk` always holds a candidate master key regardless of what the caller supplied. The handler then calls `getSigningKey(mk)` to fetch the active ephemeral key for that master. If `ek` comes back as `std::nullopt`, the handler returns early with only the `requested` field populated — a silent "not found" rather than an error. This is intentional: a missing ephemeral key means the manifest is either absent or revoked, neither of which is an error condition from the perspective of a querying client. + +## Input Validation + +Validation is two-stage. First, the handler checks for the presence of the `public_key` field using `params.isMember(jss::public_key)`, returning a structured `missing_field_error` if absent. Second, it attempts to decode the string value as a Base58-encoded node public key via `parseBase58(TokenType::NodePublic, requested)`. A failure here produces `rpcINVALID_PARAMS` injected into the response. These two checks together reject empty strings, malformed keys, and keys of the wrong token type (e.g., account addresses) before any cache lookup occurs. + +## Response Construction + +The response is built incrementally and is intentionally sparse — optional fields are only added when they are present. The `requested` field always mirrors the caller's input, which is useful for clients that batch multiple queries. The `manifest` field, when present, holds the raw serialized manifest encoded in Base64, suitable for storage or forwarding to another node. The `details` object always includes `master_key` and `ephemeral_key` (re-encoded in Base58), while `seq` and `domain` are conditionally present based on what the cached manifest recorded. + +This design means a client can detect the presence of a manifest by checking for the `details` key, and can detect revocation or absence by checking whether `details` is absent after a successful public key parse. + +## Relationship to `ValidatorInfo.cpp` + +A closely related handler, `doValidatorInfo` in `src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp`, follows the same `getMasterKey` → `getManifest` → `getSequence` → `getDomain` pattern, but it skips the input key lookup by starting directly from the node's own configured validation public key. `doManifest` is the public-facing peer: it accepts arbitrary keys from external clients and tolerates lookup misses gracefully. + +## Thread Safety + +All `ManifestCache` methods called here — `getMasterKey`, `getSigningKey`, `getManifest`, `getSequence`, `getDomain` — are documented as safe for concurrent calls, protected internally by a `std::shared_mutex`. The handler itself holds no locks and carries no mutable state, making it safe to call from any RPC thread without additional synchronization. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp.ai.json b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp.ai.json new file mode 100644 index 0000000000..801a31f584 --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp.ai.json @@ -0,0 +1,281 @@ +{ + "args": [ + { + "lineno": 18, + "name": "inp" + }, + { + "lineno": 25, + "name": "hash" + }, + { + "lineno": 246, + "name": "context" + } + ], + "classes": [ + { + "args": [], + "lineno": 13, + "name": "ServerDefinitions" + } + ], + "code_paths": [ + { + "call_chain": [ + "doServerDefinitions", + "ServerDefinitions (constructor)", + "ServerDefinitions::translate", + "ServerDefinitions::hashMatches", + "ServerDefinitions::get" + ], + "entry_point": "doServerDefinitions", + "purpose": "Handles the server_info/server_definitions RPC, constructs ServerDefinitions, translates type names, validates hash, and returns definitions.", + "validation_points": [ + "ServerDefinitions::translate (validates and transforms input type strings)", + "ServerDefinitions::hashMatches (validates hash equality)" + ] + } + ], + "data_flows": [ + { + "field": "inp (input string to translate)", + "flow": [ + "sTypeMap/rawName", + "ServerDefinitions::translate(inp)", + "typeName (used as key in defs_[jss::TYPES])" + ], + "origin": "Passed to ServerDefinitions::translate (from sTypeMap or similar sources in ServerDefinitions constructor)", + "transformations": [ + "String replacement (e.g., UINT\u2192Hash/UInt, OBJECT\u2192STObject, etc.)", + "Snake_case to CamelCase conversion" + ], + "validated_at": "ServerDefinitions::translate" + }, + { + "field": "hash (uint256)", + "flow": [ + "RPC input or internal state", + "ServerDefinitions::hashMatches(hash)", + "Comparison with defsHash_" + ], + "origin": "Passed to ServerDefinitions::hashMatches (likely from RPC input or internal state)", + "transformations": [ + "Equality check" + ], + "validated_at": "ServerDefinitions::hashMatches" + }, + { + "field": "defs_ (Json::Value)", + "flow": [ + "ServerDefinitions()", + "defs_ populated with translated type names and values", + "Returned by ServerDefinitions::get()" + ], + "origin": "Constructed in ServerDefinitions constructor", + "transformations": [ + "Populated with validated and transformed type names" + ], + "validated_at": "Type names validated in translate before insertion" + } + ], + "description": "Defines and implements the server-side logic for generating and returning XRPL server definitions, including types, fields, transaction/ledger formats, flags, and a hash for integrity verification, for use in RPC responses.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "jss::TYPES (field names for JSON output, not input validation)", + "validation", + "missing", + "check" + ], + "evidence": "Field jss::TYPES (field names for JSON output, not input validation) validated by xrpl::jss (JSON field naming/validation), boost::algorithm (string ops)", + "issue_pattern": "Missing validation for jss::TYPES (field names for JSON output, not input validation)", + "why_false_positive": "xrpl::jss (JSON field naming/validation), boost::algorithm (string ops) validates jss::TYPES (field names for JSON output, not input validation) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "inp (input string to translate)", + "empty", + "string", + "validation" + ], + "evidence": "ServerDefinitions::translate (manual logic) at ServerDefinitions::translate", + "issue_pattern": "Missing empty string validation for inp (input string to translate)", + "why_false_positive": "ServerDefinitions::translate (manual logic) validates inp (input string to translate) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "inp (input string to translate)", + "format", + "validation", + "invalid" + ], + "evidence": "ServerDefinitions::translate (manual logic) at ServerDefinitions::translate", + "issue_pattern": "Missing format validation for inp (input string to translate)", + "why_false_positive": "ServerDefinitions::translate (manual logic) validates inp (input string to translate) format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "hash (uint256)", + "empty", + "string", + "validation" + ], + "evidence": "ServerDefinitions::hashMatches (equality check) at ServerDefinitions::hashMatches", + "issue_pattern": "Missing empty string validation for hash (uint256)", + "why_false_positive": "ServerDefinitions::hashMatches (equality check) validates hash (uint256) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp", + "functions": [ + { + "args": [ + "inp" + ], + "lineno": 18, + "name": "translate" + }, + { + "args": [ + "hash" + ], + "lineno": 25, + "name": "hashMatches" + }, + { + "args": [], + "lineno": 30, + "name": "get" + }, + { + "args": [], + "lineno": 35, + "name": "ServerDefinitions" + }, + { + "args": [ + "context" + ], + "lineno": 246, + "name": "doServerDefinitions" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + }, + { + "lineno": 13, + "name": "detail" + } + ], + "test_coverage_notes": "The code is likely covered by RPC handler tests for server_info/server_definitions, which would exercise the construction of ServerDefinitions, translation of type names, and hash validation. However, there is no evidence in this file of direct unit tests for ServerDefinitions::translate or hashMatches. Edge cases in string translation (e.g., unusual type names, malformed input) may not be fully tested unless explicitly covered in dedicated unit tests. Test files to check: workflow/XRPLF-rippled-develop/test/rpc/server_info_test.cpp or similar. Gaps: No explicit fuzz or malformed input tests for translate; hashMatches is a simple equality check and likely covered indirectly.", + "validation_architecture": { + "auto_validated_fields": [ + "jss::TYPES (field names for JSON output, not input validation)" + ], + "framework": "xrpl::jss (JSON field naming/validation), boost::algorithm (string ops)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 0.8, + "error_thrown": "none (no exception, fallback to transformation)", + "field": "inp (input string to translate)", + "location": "ServerDefinitions::translate", + "validated_by": "ServerDefinitions::translate (manual logic)", + "validates": [ + "Checks if inp contains 'UINT', '512', '384', '256', '192', '160', '128'", + "Checks if inp matches keys in replacements map", + "Transforms snake_case to CamelCase" + ], + "validation_type": "format" + }, + { + "confidence": 0.7, + "error_thrown": "none (returns bool)", + "field": "hash (uint256)", + "location": "ServerDefinitions::hashMatches", + "validated_by": "ServerDefinitions::hashMatches (equality check)", + "validates": [ + "Checks if provided hash matches internal defsHash_" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp.ai.md b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp.ai.md new file mode 100644 index 0000000000..584cd12fdd --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp.ai.md @@ -0,0 +1,52 @@ +# `ServerDefinitions.cpp` — Protocol Schema Introspection RPC Handler + +This file implements the `server_definitions` RPC endpoint, which exposes the complete XRPL protocol schema as a JSON object. Its purpose is to let clients — libraries, tooling, explorers — discover the current ledger's serialization rules, type system, transaction formats, and flag definitions at runtime without needing hard-coded knowledge of the protocol. The entire output is assembled once at server startup, hashed for cache-validation, and then served as a static payload. + +## Architecture: Lazy Singleton Behind a Hash Gate + +The public entry point is `doServerDefinitions(RPC::JsonContext&)`. Inside, the `detail::ServerDefinitions` class is stored as a `static` local — a Meyers singleton, constructed exactly once under the C++11 guarantee of thread-safe static initialization. All subsequent calls reuse the already-built `Json::Value`. + +The handler accepts an optional `hash` query parameter. If the client passes the hash it already holds, the server calls `hashMatches()` and returns only `{"hash": "..."}` when they match. This bandwidth optimization matters because the full definitions payload is large and changes only on software upgrades. A mismatch causes the full definitions to be returned and the client can update its cached copy. + +## What the Constructor Builds + +`ServerDefinitions()` populates nine logical sections into a single `defs_` JSON object: + +**`TYPES`** is a map from human-readable type names (`AccountID`, `Hash256`, `UInt32`, `Blob`, etc.) to their integer `SerializedTypeID` codes. The raw source is `sTypeMap`, a macro-generated `std::map` built from the `STYPE(STI_XXX, N)` macro table in `SField.h`. Each raw name has its `STI_` prefix stripped before being fed to `translate()`. + +**`LEDGER_ENTRY_TYPES`** and **`TRANSACTION_TYPES`** are populated by iterating over the singleton instances of `LedgerFormats` and `TxFormats` respectively, extracting name/type-code pairs. Both seed an `Invalid = -1` sentinel. + +**`FIELDS`** is an array of `[name, {nth, isVLEncoded, isSerialized, isSigningField, type}]` pairs. Six entries are hard-coded at the start — `Generic`, `Invalid`, `ObjectEndMarker`, `ArrayEndMarker`, `taker_gets_funded` (nth=258), and `taker_pays_funded` (nth=259) — because they either have no canonical registry entry or require explicit control over their serialization attributes. The remaining fields come from `SField::getKnownCodeToField()`. Fields with empty names are silently skipped. + +Three field attributes are derived inline by numeric type ID: + +- `isVLEncoded` is true only for types 7 (Blob), 8 (AccountID), and 19 (Vector256). These are the three variable-length types in the serialization format. +- `isSerialized` is false for fields whose type code is ≥ 10000 (the container pseudo-types `STI_TRANSACTION`, `STI_LEDGERENTRY`, `STI_VALIDATION`, `STI_METADATA`) and for fields literally named `hash` or `index`, which are computed values rather than stored fields. +- `isSigningField` delegates to `SField::shouldInclude(false)`. + +**`TRANSACTION_RESULTS`** is populated from `transResults()`, mapping TER code names like `tesSUCCESS` and `tecDIR_FULL` to their integer codes. + +**`TRANSACTION_FORMATS`** and **`LEDGER_ENTRY_FORMATS`** expose per-type field schemas including optionality. Each section has a `common` key listing the fields shared by all transactions (or all ledger entries), with per-type entries listing only the type-specific fields. Common fields are tracked in a `std::set` and skipped during per-type iteration, so clients get a clean two-level inheritance model. + +**`TRANSACTION_FLAGS`** and **`LEDGER_ENTRY_FLAGS`** are populated from `getAllTxFlags()` and `getAllLedgerFlags()`, both of which are Meyers singletons in `TxFlags.h` built from X-macro tables. The result is a map keyed by transaction/entry type name (plus a `universal` key for globally-applicable flags), each containing a flat name-to-bitmask map. + +**`ACCOUNT_SET_FLAGS`** maps `AccountSet`-specific flag names (`asfDisallowXRP`, `asfGlobalFreeze`, etc.) to their integer identifiers via `getAsfFlagMap()`. + +## The `translate()` Function + +This private static method converts raw `STI_`-stripped names into the naming convention expected by clients. The translation rules encode semantic knowledge about the XRPL type system: + +- `UINT` combined with a fixed bit-width (128, 160, 192, 256, 384, 512) becomes `Hash` — e.g., `UINT256` → `Hash256`. This reflects the fact that these fixed-width types are used exclusively as cryptographic digests in the protocol, not arithmetic integers. +- `UINT` without a recognized bit-width suffix becomes `UInt` — e.g., `UINT32` → `UInt32`. +- A fixed lookup table handles special cases: `VL` → `Blob`, `ACCOUNT` → `AccountID`, `OBJECT` → `STObject`, `ARRAY` → `STArray`, etc. +- All other names are converted from `SCREAMING_SNAKE_CASE` to `CamelCase` by splitting on underscores and title-casing each token. + +The lambda-based helper structure (`replace`, `contains`) is idiomatic for a function that tests the same input multiple ways. The comment noting a future use of `string::contains` from C++23 indicates an intentional minimum-language-version constraint at time of writing. + +## Hash Integrity + +After all sections are built, the constructor serializes `defs_` via `Json::FastWriter` and computes `sha512Half` over the resulting string. The resulting `uint256` is stored as `defsHash_` and also embedded into `defs_[jss::hash]` as a string. This means the hash covers the entire definitions payload but not the hash field itself — the hash is appended after the fact. Clients can use this hash for cache invalidation: any protocol change that adds a new ledger entry type, SField, transaction type, or flag will produce a different definitions hash. + +## Relationship to Sibling Handlers + +This file lives alongside `ServerInfo.cpp`, `ServerState.cpp`, `Fee.cpp`, `Feature.cpp`, and `Manifest.cpp` in `src/xrpld/rpc/handlers/server_info/`. While the other handlers report runtime state (current fee, active amendments, server health), `ServerDefinitions` is unique in being purely static — it reports protocol structure, not runtime state, and it never changes during a server's lifetime. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp.ai.json b/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp.ai.json new file mode 100644 index 0000000000..7be165acfc --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp.ai.json @@ -0,0 +1,222 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doServerInfo", + "context.netOps.getServerInfo" + ], + "entry_point": "doServerInfo", + "purpose": "Handles the 'server_info' RPC request, gathers server status and optionally counters, returns as JSON.", + "validation_points": [ + "doServerInfo: context.params.isMember(jss::counters)", + "doServerInfo: context.params[jss::counters].asBool()" + ] + } + ], + "data_flows": [ + { + "field": "counters", + "flow": [ + "context.params", + "context.params.isMember(jss::counters)", + "context.params[jss::counters].asBool()", + "context.netOps.getServerInfo(third argument)", + "ret[jss::info]" + ], + "origin": "context.params (JSON input from RPC request)", + "transformations": [ + "Checked for presence with isMember", + "Converted to bool with asBool", + "Passed as boolean flag to getServerInfo" + ], + "validated_at": "doServerInfo: isMember and asBool" + }, + { + "field": "role", + "flow": [ + "context.role", + "context.role == Role::ADMIN", + "context.netOps.getServerInfo(second argument)", + "ret[jss::info]" + ], + "origin": "context.role (set by RPC framework based on authentication)", + "transformations": [ + "Compared to Role::ADMIN", + "Passed as boolean flag to getServerInfo" + ], + "validated_at": "Implicitly validated by enum comparison" + }, + { + "field": "params", + "flow": [ + "context.params", + "context.params.isMember(jss::counters)", + "context.params[jss::counters].asBool()" + ], + "origin": "context.params (JSON input from RPC request)", + "transformations": [ + "Checked for presence", + "Converted to bool" + ], + "validated_at": "doServerInfo: isMember and asBool" + } + ], + "description": "Implements the doServerInfo RPC handler, which returns server information in JSON format, including optional counters and admin details based on the context.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "counters", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::isMember at doServerInfo", + "issue_pattern": "Missing empty string validation for counters", + "why_false_positive": "Json::Value::isMember validates counters for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "counters", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::asBool at doServerInfo", + "issue_pattern": "Missing empty string validation for counters", + "why_false_positive": "Json::Value::asBool validates counters for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "counters", + "type", + "validation", + "check" + ], + "evidence": "Json::Value::asBool at doServerInfo", + "issue_pattern": "Missing type validation for counters", + "why_false_positive": "Json::Value::asBool validates counters type" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doServerInfo" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this handler is likely found in integration or functional test suites for the 'server_info' RPC endpoint, such as 'server_info_test.cpp', 'ServerInfo_test.cpp', or broader RPC handler tests. These would cover cases with and without the 'counters' field, and with different roles (ADMIN vs non-ADMIN). Gaps may exist in edge cases: malformed 'counters' values (non-bool), missing fields, or unusual role values. There is no explicit type validation for 'counters' beyond asBool(), so tests for non-bool types would be valuable.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld RPC framework (jss::, Json::Value)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (returns false if not present)", + "field": "counters", + "location": "doServerInfo", + "validated_by": "Json::Value::isMember", + "validates": [ + "checks if 'counters' field exists in input JSON" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "Json::LogicError (if not convertible to bool)", + "field": "counters", + "location": "doServerInfo", + "validated_by": "Json::Value::asBool", + "validates": [ + "checks if 'counters' field is boolean or convertible to boolean" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp.ai.md b/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp.ai.md new file mode 100644 index 0000000000..c0d355723a --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/ServerInfo.cpp.ai.md @@ -0,0 +1,27 @@ +# `ServerInfo.cpp` — `server_info` RPC Handler + +## Role in the System + +`ServerInfo.cpp` implements the `doServerInfo` function, which is the handler for the `server_info` JSON-RPC command — one of the most commonly called RPC endpoints on an XRPL node. The command returns a comprehensive snapshot of a node's operating state: build version, server health, ledger positions, peer counts, amendment warnings, and optional performance counters. Operators, monitoring systems, and client libraries call this endpoint to determine whether a node is healthy and synchronized with the network. + +## Architecture: Thin Handler, Deep Delegation + +The entire handler body is three lines of logic. `doServerInfo` constructs an empty JSON object, calls `context.netOps.getServerInfo(...)`, and wraps the result under the `jss::info` key. This reflects the RPC framework's philosophy: handlers are routing glue, not logic containers. All substantive assembly of server state lives in `NetworkOPsImp::getServerInfo()` inside `NetworkOPs.cpp`, which spans hundreds of lines gathering warnings, ledger data, peer statistics, consensus state, and optional counters. + +The decision to push complexity down to `NetworkOPs` rather than implement it in the handler is deliberate. `NetworkOPs` already holds references to all the subsystems (consensus, validators, ledger master, fee tracking) needed to produce the response. A handler that reached directly into those subsystems would create a web of coupling; instead, `NetworkOPs` provides a single aggregation interface. + +## The `human` Flag: `server_info` vs. `server_state` + +The first argument to `getServerInfo` is the boolean `human`, and this is the key architectural distinction between `ServerInfo.cpp` and its sibling `ServerState.cpp`. `doServerInfo` passes `true`, `doServerState` passes `false`. When `human` is `true`, `getServerInfo` includes fields like `hostid` (a human-readable machine identifier) and formats data for readability. When `false`, it emits numeric, machine-friendly representations suitable for programmatic parsing. Both handlers share identical logic for the `role` and `counters` arguments — the only difference is this single flag and the JSON key used for the response (`jss::info` vs. `jss::state`). + +## Role-Based Information Gating + +The second argument, `context.role == Role::ADMIN`, controls a significant branch inside `getServerInfo`. Non-admin callers receive a subset of information — notably, amendment-majority warnings (useful for operators who need to upgrade before being blocked) and node configuration details like `node_size` are withheld from unprivileged callers. The role is established upstream by the RPC framework via `requestRole()` before the handler is ever invoked; the handler itself simply reads and propagates the already-resolved value. This means `doServerInfo` never performs authentication — it only makes a boolean authorization decision on already-authenticated context. + +## Optional Counters Parameter + +The third argument enables an optional `counters` sub-object containing performance metrics (job queue depths, RPC call counts, etc.). The handler checks `context.params.isMember(jss::counters)` before calling `asBool()` on it, which is the correct defensive pattern: calling `asBool()` on a missing field would be undefined behavior in this JSON implementation. If the field is present but non-boolean, `Json::Value::asBool()` raises a `Json::LogicError`. This is an acceptable exception path since such a request is malformed. + +## Relationship to Sibling Handlers + +The `server_info/` directory groups a cluster of closely related read-only node-introspection handlers: `Feature.cpp` (amendment status), `Fee.cpp` (current fee schedule), `Manifest.cpp` (validator manifests), `ServerDefinitions.cpp` (protocol field definitions), `ServerInfo.cpp`, `ServerState.cpp`, and `Version.h`. These handlers are intentionally thin to keep the RPC surface uniform — each wraps one or two calls into subsystems that do the real work. `ServerInfo.cpp` and `ServerState.cpp` in particular are near-duplicates differing only in the `human` flag and response key, reflecting that `server_info` and `server_state` exist as two views over the same underlying data source. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/ServerState.cpp.ai.json b/src/xrpld/rpc/handlers/server_info/ServerState.cpp.ai.json new file mode 100644 index 0000000000..345f612e74 --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/ServerState.cpp.ai.json @@ -0,0 +1,249 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doServerState", + "context.netOps.getServerInfo" + ], + "entry_point": "doServerState", + "purpose": "Handles the 'server_state' RPC request, gathers server state info, and returns it as JSON.", + "validation_points": [ + "doServerState: context.role == Role::ADMIN (role validation)", + "doServerState: context.params.isMember(jss::counters) && context.params[jss::counters].asBool() (counters validation)" + ] + } + ], + "data_flows": [ + { + "field": "role", + "flow": [ + "context.role", + "doServerState: compared to Role::ADMIN", + "passed as 2nd argument to getServerInfo" + ], + "origin": "context.role (set by RPC framework based on authentication/authorization)", + "transformations": [ + "Boolean comparison (context.role == Role::ADMIN)" + ], + "validated_at": "doServerState" + }, + { + "field": "counters", + "flow": [ + "context.params", + "doServerState: isMember(jss::counters)", + "doServerState: asBool()", + "passed as 3rd argument to getServerInfo" + ], + "origin": "context.params (JSON input from RPC request)", + "transformations": [ + "Checked for presence (isMember)", + "Converted to boolean (asBool)" + ], + "validated_at": "doServerState" + } + ], + "description": "Implements the doServerState RPC handler, which returns the current server state information as a JSON object.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "counters", + "validation", + "missing", + "check" + ], + "evidence": "Field counters validated by xrpl::jss (JSON field tags), Json::Value (jsoncpp), Role enum", + "issue_pattern": "Missing validation for counters", + "why_false_positive": "xrpl::jss (JSON field tags), Json::Value (jsoncpp), Role enum validates counters automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "counters", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::isMember at doServerState", + "issue_pattern": "Missing empty string validation for counters", + "why_false_positive": "Json::Value::isMember validates counters for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "counters", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::asBool at doServerState", + "issue_pattern": "Missing empty string validation for counters", + "why_false_positive": "Json::Value::asBool validates counters for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "counters", + "type", + "validation", + "check" + ], + "evidence": "Json::Value::asBool at doServerState", + "issue_pattern": "Missing type validation for counters", + "why_false_positive": "Json::Value::asBool validates counters type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "role", + "empty", + "string", + "validation" + ], + "evidence": "comparison (context.role == Role::ADMIN) at doServerState", + "issue_pattern": "Missing empty string validation for role", + "why_false_positive": "comparison (context.role == Role::ADMIN) validates role for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/server_info/ServerState.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 8, + "name": "doServerState" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage would be in RPC handler tests, e.g., server_info or server_state tests. Tests should cover: (1) role-based access (admin vs non-admin), (2) presence/absence and boolean value of 'counters' parameter, (3) correct propagation of parameters to getServerInfo. Gaps may exist if tests do not check all combinations of role and counters, or do not verify that invalid/malformed 'counters' values are handled gracefully. No direct evidence of test files is present in this snippet; likely candidates are tests in 'test/rpc/ServerInfo_test.cpp', 'test/rpc/ServerState_test.cpp', or similar.", + "validation_architecture": { + "auto_validated_fields": [ + "counters" + ], + "framework": "xrpl::jss (JSON field tags), Json::Value (jsoncpp), Role enum", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (returns false if not present)", + "field": "counters", + "location": "doServerState", + "validated_by": "Json::Value::isMember", + "validates": [ + "checks if 'counters' field exists in input JSON" + ], + "validation_type": "existence" + }, + { + "confidence": 1.0, + "error_thrown": "Json::LogicError (if not convertible to bool)", + "field": "counters", + "location": "doServerState", + "validated_by": "Json::Value::asBool", + "validates": [ + "checks if 'counters' field is boolean or convertible to boolean" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "none (used as boolean flag)", + "field": "role", + "location": "doServerState", + "validated_by": "comparison (context.role == Role::ADMIN)", + "validates": [ + "checks if user role is ADMIN" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/ServerState.cpp.ai.md b/src/xrpld/rpc/handlers/server_info/ServerState.cpp.ai.md new file mode 100644 index 0000000000..8b1469dbf5 --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/ServerState.cpp.ai.md @@ -0,0 +1,49 @@ +# `ServerState.cpp` — `server_state` RPC Handler + +## Role in the System + +`ServerState.cpp` implements `doServerState`, the handler for the `server_state` JSON-RPC command. This is one of two symmetric server-introspection endpoints in the XRPL protocol — its twin `ServerInfo.cpp` implements `server_info`. Both handlers delegate almost entirely to `NetworkOPs::getServerInfo`, but differ in one critical flag that controls how numeric values are formatted in the response. + +## The Single Distinguishing Decision: `human = false` + +The entire behavioral difference between `server_state` and `server_info` lives in the first argument to `getServerInfo`: + +```cpp +// ServerState.cpp +context.netOps.getServerInfo(false, isAdmin, hasCounters); + +// ServerInfo.cpp +context.netOps.getServerInfo(true, isAdmin, hasCounters); +``` + +The `bool human` flag instructs `NetworkOPs` to return values in machine-readable form (`false`) versus human-readable form (`true`). For `server_state`, XRP amounts are expressed as raw drops (integer strings), fees as integer basis points, and timestamps as Unix epoch integers — formats suited for programmatic consumption by clients, trading bots, and monitoring tools. `server_info` formats the same data with units spelled out (e.g., `"XRP"` suffixes, floating-point amounts), targeting human operators. The two commands expose the same underlying data through different lenses; there is no separate code path in `NetworkOPs` for each. + +## Role-Gated Admin Fields + +```cpp +context.role == Role::ADMIN +``` + +The `role` field in `RPC::Context` is resolved upstream by the RPC framework before the handler is ever called, based on the connection's authentication credentials (IP whitelist, admin password). Passing the boolean result directly to `getServerInfo` means the implementation here makes no trust decisions itself — it simply forwards the already-resolved authorization token. Admin mode unlocks additional fields in the response such as peer counts, internal queue depths, and load factor details that could be sensitive or useful for operator diagnostics but are inappropriate to expose to arbitrary public callers. + +## Optional Counters Parameter + +```cpp +context.params.isMember(jss::counters) && context.params[jss::counters].asBool() +``` + +The `counters` request parameter is an opt-in flag. The guard first checks presence with `isMember` before calling `asBool`, avoiding a `Json::LogicError` on missing fields. If present and truthy, `getServerInfo` includes internal performance counters — RPC call counts, ledger validation statistics, and similar instrumentation data. These counters are relatively expensive to serialize and rarely needed outside of debugging sessions, so they are omitted from the default response. The absence of any input-sanitization error path here is intentional: an unrecognized or invalid `counters` value silently evaluates to `false` via the short-circuit `&&`, treating malformed input as opt-out rather than an error. + +## Response Shape + +The handler wraps the `NetworkOPs` result under the `jss::state` key: + +```cpp +ret[jss::state] = context.netOps.getServerInfo(...); +``` + +This nesting is the externally documented response contract for `server_state`. The parallel `doServerInfo` wraps its result under `jss::info`. Both keys are compile-time string constants from the `jss` namespace, which prevents typo-class bugs through the type system rather than runtime checks. + +## Relationship to Sibling Handlers + +Within the `server_info/` module, `ServerState.cpp` is the lightest file — a four-line function body with no local logic. The real complexity lives in `NetworkOPs::getServerInfo`, which aggregates ledger state, fee schedules, peer topology, consensus status, and job queue metrics. `ServerState.cpp`'s role is purely to translate the RPC calling convention (extracting role and params from `JsonContext`) into the `NetworkOPs` interface, and to label the result correctly. This separation keeps the RPC layer thin and keeps business logic centralized in `NetworkOPs` rather than scattered across per-handler files. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/Version.h.ai.json b/src/xrpld/rpc/handlers/server_info/Version.h.ai.json new file mode 100644 index 0000000000..0d7c2f19bf --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/Version.h.ai.json @@ -0,0 +1,55 @@ +{ + "args": [ + { + "lineno": 8, + "name": "c" + }, + { + "lineno": 18, + "name": "obj" + } + ], + "classes": [ + { + "args": [ + "JsonContext& c" + ], + "lineno": 7, + "name": "VersionHandler" + } + ], + "description": "Defines the VersionHandler class for handling API versioning in the XRPL RPC context, including version checks and result writing.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/server_info/Version.h", + "functions": [ + { + "args": [ + "JsonContext& c" + ], + "lineno": 8, + "name": "VersionHandler" + }, + { + "args": [], + "lineno": 13, + "name": "check" + }, + { + "args": [ + "Json::Value& obj" + ], + "lineno": 18, + "name": "writeResult" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + }, + { + "lineno": 5, + "name": "RPC" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/server_info/Version.h.ai.md b/src/xrpld/rpc/handlers/server_info/Version.h.ai.md new file mode 100644 index 0000000000..f8e58b8f71 --- /dev/null +++ b/src/xrpld/rpc/handlers/server_info/Version.h.ai.md @@ -0,0 +1,35 @@ +# `Version.h` — RPC Version Discovery Handler + +## Role and Purpose + +`Version.h` defines `VersionHandler`, which implements the `version` RPC endpoint. This endpoint exists so that API clients can discover which API versions a running `rippled` node supports before making substantive requests. It is one of only two "new-style" class-based RPC handlers in the codebase; the other is `LedgerHandler`. All other RPC commands in `rippled` are registered as bare function pointers in the legacy `handlerArray` table in `Handler.cpp`. `VersionHandler` and `LedgerHandler` are registered separately via the `addHandler()` template during `HandlerTable` construction. + +## Handler Protocol + +The new-style handler contract is simple: a class must expose a constructor taking `JsonContext&`, a `check()` method, a `writeResult(Json::Value&)` method, and a set of `static constexpr` metadata fields. The `handlerFrom()` template function in `Handler.cpp` bridges this class shape into a `Handler` struct by wrapping it in the `handle<>` free function template, which instantiates the class, calls `check()`, and either injects an error or calls `writeResult()` depending on the result. The dispatch infrastructure therefore treats class-based and function-based handlers identically at runtime. + +## What the Handler Does + +The constructor captures two fields from the incoming `JsonContext`: the already-resolved `apiVersion` (an integer version number parsed from the `api_version` field of the request) and the `betaEnabled` flag from the node's config (`BETA_RPC_API`). These are the only context fields the handler needs. + +`check()` unconditionally returns `Status::OK`. There is no error condition for this endpoint — a client asking "what versions do you support?" is always a valid question regardless of the node's synchronization state or ledger availability. + +`writeResult()` delegates entirely to `setVersion()` defined in `ApiVersion.h`. That function's behavior branches on whether the caller is using API version 1 (the legacy default). For version-1 callers, it emits semantic version strings (`first`, `good`, `last`) all fixed at `"1.0.0"` — a compatibility shim that mirrors the old format clients expected before numbered API versions existed. For version-2 and higher callers, it emits numeric `first`/`last` bounds: `first` is always `apiMinimumSupportedVersion` (1), and `last` is either `apiMaximumSupportedVersion` (2) or `apiBetaVersion` (3) depending on whether `betaEnabled` is true. + +## Version Range and the Beta Boundary + +The handler's static `maxApiVer` is set to `RPC::apiMaximumValidVersion`, which equals `apiBetaVersion` (currently 3). This is deliberately wider than the `apiMaximumSupportedVersion` (2) that most handlers cap at. The reason is structural: a client running against a beta-enabled node needs to be able to reach the `version` endpoint at API version 3 to discover that version 3 is available. If `maxApiVer` were capped at 2, the `HandlerTable` lookup at version 3 would return `nullptr` and the client could never query capabilities at that version level. Setting `maxApiVer = apiMaximumValidVersion` ensures the discovery endpoint is always reachable across the full valid range. + +The `addHandler()` template enforces this correctness contract at compile time with three `static_assert` checks: + +```cpp +static_assert(HandlerImpl::minApiVer <= HandlerImpl::maxApiVer); +static_assert(HandlerImpl::maxApiVer <= RPC::apiMaximumValidVersion); +static_assert(RPC::apiMinimumSupportedVersion <= HandlerImpl::minApiVer); +``` + +Any handler with out-of-range version bounds will fail to compile. + +## Access Control + +`role = Role::USER` and `condition = NO_CONDITION`. The `USER` role means no admin credentials are required — version discovery is public. `NO_CONDITION` means the network synchronization and ledger availability checks in `conditionMet()` are bypassed entirely. This is appropriate: a node that is still syncing or amendment-blocked can still truthfully describe what API versions it supports. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp.ai.json b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp.ai.json new file mode 100644 index 0000000000..02bf57d2d0 --- /dev/null +++ b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp.ai.json @@ -0,0 +1,641 @@ +{ + "args": [ + { + "lineno": 13, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doSubscribe" + ], + "entry_point": "doSubscribe", + "purpose": "Handles subscription requests via RPC, including validation of parameters and user permissions.", + "validation_points": [ + "if (!context.infoSub && !context.params.isMember(jss::url)) - Validates that either infoSub exists or a url is provided.", + "if (context.params.isMember(jss::url)) - Checks if url is present.", + "if (context.role != Role::ADMIN) - Validates that only ADMIN can use url subscription.", + "context.params[jss::url].asString() - Assumes url is a string.", + "context.params.isMember(jss::url_username) ? context.params[jss::url_username].asString() : \"\" - Validates and extracts url_username.", + "context.params.isMember(jss::url_password) ? context.params[jss::url_password].asString() : \"\" - Validates and extracts url_password.", + "if (context.params.isMember(jss::username)) - DEPRECATED: checks for username.", + "if (context.params.isMember(jss::password)) - DEPRECATED: checks for password.", + "if (context.params.isMember(jss::streams)) - Checks for streams array.", + "if (!context.params[jss::streams].isArray()) - Validates streams is an array.", + "for (auto const& it : context.params[jss::streams]) { if (!it.isString()) ... } - Validates each stream is a string." + ] + } + ], + "data_flows": [ + { + "field": "url", + "flow": [ + "context.params[jss::url] (input)", + "context.params[jss::url].asString() (extraction)", + "strUrl (local variable)", + "context.netOps.findRpcSub(strUrl) / make_RPCSub(...)", + "context.netOps.addRpcSub(strUrl, ...)" + ], + "origin": "context.params[jss::url]", + "transformations": [ + "Extracted as string", + "Used as key for subscription lookup/creation" + ], + "validated_at": "if (context.params.isMember(jss::url)), if (context.role != Role::ADMIN)" + }, + { + "field": "url_username", + "flow": [ + "context.params.isMember(jss::url_username) ? context.params[jss::url_username].asString() : \"\"", + "strUsername (local variable)", + "make_RPCSub(..., strUsername, ...)" + ], + "origin": "context.params[jss::url_username]", + "transformations": [ + "Extracted as string if present, else empty string" + ], + "validated_at": "context.params.isMember(jss::url_username)" + }, + { + "field": "url_password", + "flow": [ + "context.params.isMember(jss::url_password) ? context.params[jss::url_password].asString() : \"\"", + "strPassword (local variable)", + "make_RPCSub(..., strPassword, ...)" + ], + "origin": "context.params[jss::url_password]", + "transformations": [ + "Extracted as string if present, else empty string" + ], + "validated_at": "context.params.isMember(jss::url_password)" + }, + { + "field": "username (DEPRECATED)", + "flow": [ + "if (context.params.isMember(jss::username))", + "strUsername = context.params[jss::username].asString()", + "make_RPCSub(..., strUsername, ...)" + ], + "origin": "context.params[jss::username]", + "transformations": [ + "Overrides strUsername if present" + ], + "validated_at": "context.params.isMember(jss::username)" + }, + { + "field": "password (DEPRECATED)", + "flow": [ + "if (context.params.isMember(jss::password))", + "strPassword = context.params[jss::password].asString()", + "make_RPCSub(..., strPassword, ...)" + ], + "origin": "context.params[jss::password]", + "transformations": [ + "Overrides strPassword if present" + ], + "validated_at": "context.params.isMember(jss::password)" + }, + { + "field": "streams", + "flow": [ + "context.params[jss::streams] (input)", + "if (!context.params[jss::streams].isArray()) (validation)", + "for (auto const& it : context.params[jss::streams])", + "if (!it.isString()) (validation)", + "streamName = it.asString()", + "context.netOps.subServer/subLedger/subBook/..." + ], + "origin": "context.params[jss::streams]", + "transformations": [ + "Checked for array type", + "Each element checked for string type", + "Used to determine subscription type" + ], + "validated_at": "if (context.params.isMember(jss::streams)), if (!context.params[jss::streams].isArray()), if (!it.isString())" + } + ], + "description": "Implements the doSubscribe RPC handler for the XRPL server, allowing clients to subscribe to various data streams and account/book updates via WebSocket or RPC.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "url", + "validation", + "missing", + "check" + ], + "evidence": "Field url validated by jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling", + "issue_pattern": "Missing validation for url", + "why_false_positive": "jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling validates url automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "url_username", + "validation", + "missing", + "check" + ], + "evidence": "Field url_username validated by jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling", + "issue_pattern": "Missing validation for url_username", + "why_false_positive": "jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling validates url_username automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "url_password", + "validation", + "missing", + "check" + ], + "evidence": "Field url_password validated by jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling", + "issue_pattern": "Missing validation for url_password", + "why_false_positive": "jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling validates url_password automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "username (deprecated)", + "validation", + "missing", + "check" + ], + "evidence": "Field username (deprecated) validated by jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling", + "issue_pattern": "Missing validation for username (deprecated)", + "why_false_positive": "jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling validates username (deprecated) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "password (deprecated)", + "validation", + "missing", + "check" + ], + "evidence": "Field password (deprecated) validated by jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling", + "issue_pattern": "Missing validation for password (deprecated)", + "why_false_positive": "jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling validates password (deprecated) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "url", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::url) at doSubscribe", + "issue_pattern": "Missing empty string validation for url", + "why_false_positive": "context.params.isMember(jss::url) validates url for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "role", + "empty", + "string", + "validation" + ], + "evidence": "context.role != Role::ADMIN at doSubscribe", + "issue_pattern": "Missing empty string validation for role", + "why_false_positive": "context.role != Role::ADMIN validates role for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "url", + "empty", + "string", + "validation" + ], + "evidence": "context.params[jss::url].asString() at doSubscribe", + "issue_pattern": "Missing empty string validation for url", + "why_false_positive": "context.params[jss::url].asString() validates url for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "url", + "type", + "validation", + "check" + ], + "evidence": "context.params[jss::url].asString() at doSubscribe", + "issue_pattern": "Missing type validation for url", + "why_false_positive": "context.params[jss::url].asString() validates url type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "url_username", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::url_username) ? context.params[jss::url_username].asString() : \"\" at doSubscribe", + "issue_pattern": "Missing empty string validation for url_username", + "why_false_positive": "context.params.isMember(jss::url_username) ? context.params[jss::url_username].asString() : \"\" validates url_username for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "url_username", + "type", + "validation", + "check" + ], + "evidence": "context.params.isMember(jss::url_username) ? context.params[jss::url_username].asString() : \"\" at doSubscribe", + "issue_pattern": "Missing type validation for url_username", + "why_false_positive": "context.params.isMember(jss::url_username) ? context.params[jss::url_username].asString() : \"\" validates url_username type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "url_password", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::url_password) ? context.params[jss::url_password].asString() : \"\" at doSubscribe", + "issue_pattern": "Missing empty string validation for url_password", + "why_false_positive": "context.params.isMember(jss::url_password) ? context.params[jss::url_password].asString() : \"\" validates url_password for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "url_password", + "type", + "validation", + "check" + ], + "evidence": "context.params.isMember(jss::url_password) ? context.params[jss::url_password].asString() : \"\" at doSubscribe", + "issue_pattern": "Missing type validation for url_password", + "why_false_positive": "context.params.isMember(jss::url_password) ? context.params[jss::url_password].asString() : \"\" validates url_password type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "username (deprecated)", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::username) at doSubscribe", + "issue_pattern": "Missing empty string validation for username (deprecated)", + "why_false_positive": "context.params.isMember(jss::username) validates username (deprecated) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "username (deprecated)", + "type", + "validation", + "check" + ], + "evidence": "context.params.isMember(jss::username) at doSubscribe", + "issue_pattern": "Missing type validation for username (deprecated)", + "why_false_positive": "context.params.isMember(jss::username) validates username (deprecated) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "password (deprecated)", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::password) at doSubscribe", + "issue_pattern": "Missing empty string validation for password (deprecated)", + "why_false_positive": "context.params.isMember(jss::password) validates password (deprecated) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "password (deprecated)", + "type", + "validation", + "check" + ], + "evidence": "context.params.isMember(jss::password) at doSubscribe", + "issue_pattern": "Missing type validation for password (deprecated)", + "why_false_positive": "context.params.isMember(jss::password) validates password (deprecated) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "url (connection)", + "empty", + "string", + "validation" + ], + "evidence": "context.netOps.findRpcSub(strUrl) at doSubscribe", + "issue_pattern": "Missing empty string validation for url (connection)", + "why_false_positive": "context.netOps.findRpcSub(strUrl) validates url (connection) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 13, + "name": "doSubscribe" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage for this handler would be in files like test/subscribe_test.cpp, test/rpc_subscribe_test.cpp, or integration tests for the RPC interface. Tests should cover: missing url, non-admin url access, malformed streams, valid/invalid usernames/passwords, and correct subscription behavior. Gaps may exist for deprecated fields (username/password), error handling for make_RPCSub exceptions, and edge cases for malformed JSON input. Template-based validation may be tested indirectly, but explicit negative tests for all validation branches should be confirmed.", + "validation_architecture": { + "auto_validated_fields": [ + "url", + "url_username", + "url_password", + "username (deprecated)", + "password (deprecated)" + ], + "framework": "jss:: (JSON field tags), Json::Value, custom error codes (rpcError), exception-based error handling", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "url", + "location": "doSubscribe", + "validated_by": "context.params.isMember(jss::url)", + "validates": [ + "Checks if 'url' is present in params for JSON-RPC calls" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNO_PERMISSION)", + "field": "role", + "location": "doSubscribe", + "validated_by": "context.role != Role::ADMIN", + "validates": [ + "Checks if caller has ADMIN role when 'url' is present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "none (implicit type conversion, may throw if not string)", + "field": "url", + "location": "doSubscribe", + "validated_by": "context.params[jss::url].asString()", + "validates": [ + "Ensures 'url' is a string" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (implicit type conversion, may throw if not string)", + "field": "url_username", + "location": "doSubscribe", + "validated_by": "context.params.isMember(jss::url_username) ? context.params[jss::url_username].asString() : \"\"", + "validates": [ + "Ensures 'url_username' is a string if present" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (implicit type conversion, may throw if not string)", + "field": "url_password", + "location": "doSubscribe", + "validated_by": "context.params.isMember(jss::url_password) ? context.params[jss::url_password].asString() : \"\"", + "validates": [ + "Ensures 'url_password' is a string if present" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (implicit type conversion, may throw if not string)", + "field": "username (deprecated)", + "location": "doSubscribe", + "validated_by": "context.params.isMember(jss::username)", + "validates": [ + "Ensures 'username' is a string if present (deprecated)" + ], + "validation_type": "type" + }, + { + "confidence": 0.8, + "error_thrown": "none (implicit type conversion, may throw if not string)", + "field": "password (deprecated)", + "location": "doSubscribe", + "validated_by": "context.params.isMember(jss::password)", + "validates": [ + "Ensures 'password' is a string if present (deprecated)" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::make_param_error(ex.what()) (if make_RPCSub throws std::runtime_error)", + "field": "url (connection)", + "location": "doSubscribe", + "validated_by": "context.netOps.findRpcSub(strUrl)", + "validates": [ + "Checks if subscription for 'url' already exists; if not, attempts to create" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp.ai.md b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp.ai.md new file mode 100644 index 0000000000..050a5047e8 --- /dev/null +++ b/src/xrpld/rpc/handlers/subscribe/Subscribe.cpp.ai.md @@ -0,0 +1,39 @@ +# Subscribe.cpp — `doSubscribe` RPC Handler + +## Role in the System + +`Subscribe.cpp` implements `doSubscribe`, the server-side handler for the XRPL `subscribe` RPC/WebSocket command. Its job is to register a client connection as a listener on one or more named event streams, sets of accounts, or order books. Once registered, the event routing infrastructure in `NetworkOPs` will push JSON notifications to the client whenever matching events occur on the ledger without requiring the client to poll. The symmetric counterpart, `doUnsubscribe` in `Unsubscribe.cpp`, tears down these registrations using exactly parallel logic. + +## Two Delivery Modes: WebSocket vs. HTTP Callback + +The handler supports fundamentally different delivery mechanisms that share the same parameter surface. When a client has an active WebSocket connection, `context.infoSub` is already populated with the connection's `InfoSub` object and that pointer is used directly. When a `url` parameter is present instead, the handler creates (or retrieves) an `RPCSub` object that POSTs events to the specified HTTP endpoint rather than streaming them over a socket. The URL mode is exclusively available to admin-role clients, enforced by the `Role::ADMIN` check at line 32. The `VFALCO TODO Remove` comment in `InfoSub::Source` makes it clear this HTTP-push path was a one-off addition for a specific partner and is considered legacy. + +For URL subscriptions, `findRpcSub` checks a server-wide registry for an existing subscription to that URL, avoiding duplicate `RPCSub` objects. If none exists, `make_RPCSub` is called to construct one, with any `std::runtime_error` thrown during construction (e.g., a malformed URL) converted to an `rpcError` parameter error. When reusing an existing subscription, only the deprecated `username`/`password` fields update the stored credentials — an in-code comment at line 79 acknowledges this asymmetric treatment of `url_username`/`url_password` as a known oddity. + +## `InfoSub`: The Subscriber Abstraction + +`InfoSub` is the common base for all subscriber types. It holds a `Consumer` for resource accounting, a monotonically increasing 64-bit `mSeq` identifier used as a key for unsubscription, per-subscriber sets of tracked accounts, and the API version of the connected client. After `ispSub` is resolved, `setApiVersion()` is called immediately so every subsequent subscription registration inherits the correct formatting version — ensuring events serialized for this subscriber match what the client's API version expects. + +## Named Stream Subscriptions + +The `streams` array drives registration for seven public streams (`server`, `ledger`, `book_changes`, `manifests`, `transactions`, `transactions_proposed`, `validations`, `consensus`) and one admin-only stream (`peer_status`). Each name maps to a single `netOps.sub*` call that adds `ispSub` to the appropriate fan-out set inside `NetworkOPs`. The stream name `rt_transactions` is accepted as a deprecated alias for `transactions_proposed`; the distinction between these two is significant — `transactions` fires only for ledger-validated transactions while `transactions_proposed` fires for unconfirmed candidates entering the queue. + +## Account Subscriptions + +Account subscriptions come in two flavors, controlled by a `bool realTime` flag threaded through `netOps.subAccount`. The `accounts` field subscribes to finalized transaction outcomes, while `accounts_proposed` (and its deprecated alias `rt_accounts`) subscribes to proposed transactions before validation. Both paths call `RPC::parseAccountIds` to decode the base58-encoded address array, returning `rpcACT_MALFORMED` immediately if any parse fails, so the server never registers a partial or ambiguous set. + +## Order Book Subscriptions and the Snapshot Pattern + +Book subscriptions expose the most complex logic. Each entry in the `books` array must contain `taker_pays` and `taker_gets` currency/issuer objects, parsed via `RPC::parseSubUnsubJson`. The handler validates that both sides are not identical (a self-trade market makes no sense) and calls `isConsistent(book)` to enforce protocol-level coherence of the currency/issuer pair. The optional `domain` field allows filtering the book to a specific AMM domain (a uint256 hex-encoded identifier). + +The `both`/`both_sides` flag (where `both_sides` is deprecated) subscribes to both the forward book and `reversed(book)`, allowing a client to monitor a trading pair regardless of which side is designated as bid or ask. + +The most interesting design choice here is the **subscribe-then-snapshot** pattern triggered by the `snapshot`/`state_now` flags. The handler registers the live subscription first via `netOps.subBook`, then fetches current offers from the published ledger using `getBookPage`. The result populates the response under `offers` (single direction) or `bids`/`asks` (both directions). This ordering eliminates a race condition: any offer event that fires between the time the snapshot is read and the time the subscription is active would be silently dropped if the registration happened after the snapshot. Because registration precedes the read, the client may see duplicate events for offers that were already in the snapshot, but it cannot miss any. The snapshot path sets `context.loadType = Resource::feeMediumBurdenRPC` to signal elevated resource consumption to the rate-limiting layer. + +## `account_history_tx_stream` + +This experimental feature, gated behind both `useTxTables()` and an explicit warning in the response JSON, streams both current and historical transactions for a single account. The server replays past ledger history while simultaneously forwarding new transactions going forward. It requires transaction table storage to be enabled on the node — if not, `rpcNOT_ENABLED` is returned. Like the book snapshot path, it charges `feeMediumBurdenRPC` given the potential for significant historical replay work. `doUnsubscribe` adds a `stop_history_tx_only` option that lets a client halt the historical replay while continuing to receive live transactions, reflecting the natural lifecycle of this feature. + +## Error Handling Discipline + +Every validation step returns immediately on failure without partial side effects. Array inputs are type-checked before iteration; each stream name is checked for exact string match before any `netOps` call; book parameters are fully parsed and validated before `subBook` is invoked. The `make_RPCSub` factory is wrapped in a try/catch specifically for `std::runtime_error` so a bad URL string surfaces as a structured RPC error rather than an unhandled exception. The result is that either all requested subscriptions are registered (and any snapshot data is returned) or the handler returns an error and the client knows no state was changed. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp.ai.json b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp.ai.json new file mode 100644 index 0000000000..f71941f86b --- /dev/null +++ b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp.ai.json @@ -0,0 +1,440 @@ +{ + "args": [ + { + "lineno": 12, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doUnsubscribe" + ], + "entry_point": "doUnsubscribe", + "purpose": "Handles unsubscribe requests for various stream types and account notifications in the XRPL server.", + "validation_points": [ + "if (!context.infoSub && !context.params.isMember(jss::url))", + "if (context.params.isMember(jss::url)) { ... if (context.role != Role::ADMIN) ... }", + "if (!ispSub) return jvResult;", + "if (context.params.isMember(jss::streams)) { if (!context.params[jss::streams].isArray()) ... for (auto& it : context.params[jss::streams]) { if (!it.isString()) ... } }", + "if (context.params.isMember(accountsProposed)) { if (!context.params[accountsProposed].isArray()) ... }", + "if (context.params.isMember(jss::accounts)) { if (!context.params[jss::accounts].isArray()) ... }", + "if (context.params.isMember(jss::account_history_tx_stream)) { if (!req.isMember(jss::account) || !req[jss::account].isString()) ... }" + ] + } + ], + "data_flows": [ + { + "field": "infoSub", + "flow": [ + "context.infoSub", + "checked for presence (if (!context.infoSub && ...))", + "assigned to ispSub if url not present", + "used in netOps.unsub* calls" + ], + "origin": "context.infoSub (RPC::JsonContext)", + "transformations": [ + "Pointer assignment to ispSub" + ], + "validated_at": "First if-check in doUnsubscribe" + }, + { + "field": "url", + "flow": [ + "context.params[jss::url]", + "checked for presence (isMember)", + "role checked for ADMIN", + "converted to string (asString)", + "passed to context.netOps.findRpcSub", + "result assigned to ispSub" + ], + "origin": "context.params[jss::url]", + "transformations": [ + "String conversion", + "Lookup via findRpcSub" + ], + "validated_at": "isMember, role check, null check on findRpcSub" + }, + { + "field": "streams", + "flow": [ + "context.params[jss::streams]", + "checked for presence (isMember)", + "validated as array (isArray)", + "iterated over", + "each element validated as string (isString)", + "used to select unsub* method" + ], + "origin": "context.params[jss::streams]", + "transformations": [ + "Array iteration", + "String conversion" + ], + "validated_at": "isArray and isString checks" + }, + { + "field": "accounts_proposed / rt_accounts", + "flow": [ + "context.params.isMember(accountsProposed)", + "validated as array (isArray)", + "parsed via RPC::parseAccountIds", + "checked for empty", + "used in netOps.unsubAccount" + ], + "origin": "context.params[jss::accounts_proposed] or context.params[jss::rt_accounts]", + "transformations": [ + "Array to vector via parseAccountIds" + ], + "validated_at": "isArray, parseAccountIds result checked for empty" + }, + { + "field": "accounts", + "flow": [ + "context.params.isMember(jss::accounts)", + "validated as array (isArray)", + "parsed via RPC::parseAccountIds", + "checked for empty", + "used in netOps.unsubAccount" + ], + "origin": "context.params[jss::accounts]", + "transformations": [ + "Array to vector via parseAccountIds" + ], + "validated_at": "isArray, parseAccountIds result checked for empty" + }, + { + "field": "account_history_tx_stream.account", + "flow": [ + "context.params.isMember(jss::account_history_tx_stream)", + "req = context.params[jss::account_history_tx_stream]", + "req.isMember(jss::account) && req[jss::account].isString()", + "parseBase58(req[jss::account].asString())", + "checked for valid parse" + ], + "origin": "context.params[jss::account_history_tx_stream][jss::account]", + "transformations": [ + "String to AccountID via parseBase58" + ], + "validated_at": "isString check, parseBase58 result checked" + } + ], + "description": "Implements the doUnsubscribe RPC handler for the XRPL server, allowing clients to unsubscribe from various data streams, accounts, books, and other notifications.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "infoSub and url", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at doUnsubscribe (first if block)", + "issue_pattern": "Missing empty string validation for infoSub and url", + "why_false_positive": "explicit if-check validates infoSub and url for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "url", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at doUnsubscribe (if context.params.isMember(jss::url))", + "issue_pattern": "Missing empty string validation for url", + "why_false_positive": "explicit if-check validates url for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "url", + "empty", + "string", + "validation" + ], + "evidence": "findRpcSub(strUrl) and null check at doUnsubscribe (ispSub = context.netOps.findRpcSub(strUrl); if (!ispSub))", + "issue_pattern": "Missing empty string validation for url", + "why_false_positive": "findRpcSub(strUrl) and null check validates url for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "streams", + "empty", + "string", + "validation" + ], + "evidence": "isArray() at doUnsubscribe (if context.params.isMember(jss::streams))", + "issue_pattern": "Missing empty string validation for streams", + "why_false_positive": "isArray() validates streams for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "streams", + "type", + "validation", + "check" + ], + "evidence": "isArray() at doUnsubscribe (if context.params.isMember(jss::streams))", + "issue_pattern": "Missing type validation for streams", + "why_false_positive": "isArray() validates streams type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "streams[]", + "empty", + "string", + "validation" + ], + "evidence": "isString() at doUnsubscribe (for loop over streams)", + "issue_pattern": "Missing empty string validation for streams[]", + "why_false_positive": "isString() validates streams[] for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "streams[]", + "type", + "validation", + "check" + ], + "evidence": "isString() at doUnsubscribe (for loop over streams)", + "issue_pattern": "Missing type validation for streams[]", + "why_false_positive": "isString() validates streams[] type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "streams[]", + "empty", + "string", + "validation" + ], + "evidence": "explicit string comparison at doUnsubscribe (for loop else branch)", + "issue_pattern": "Missing empty string validation for streams[]", + "why_false_positive": "explicit string comparison validates streams[] for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "accounts_proposed or rt_accounts", + "empty", + "string", + "validation" + ], + "evidence": "isArray() at doUnsubscribe (if context.params.isMember(accountsProposed))", + "issue_pattern": "Missing empty string validation for accounts_proposed or rt_accounts", + "why_false_positive": "isArray() validates accounts_proposed or rt_accounts for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "accounts_proposed or rt_accounts", + "type", + "validation", + "check" + ], + "evidence": "isArray() at doUnsubscribe (if context.params.isMember(accountsProposed))", + "issue_pattern": "Missing type validation for accounts_proposed or rt_accounts", + "why_false_positive": "isArray() validates accounts_proposed or rt_accounts type" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 11, + "name": "doUnsubscribe" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 9, + "name": "xrpl" + } + ], + "test_coverage_notes": "Typical test coverage would be in the unit/integration tests for the RPC subscribe/unsubscribe handlers, likely in files such as 'rpc_unsubscribe_test.cpp', 'subscribe_test.cpp', or similar. Tests should cover: missing/invalid infoSub, missing/invalid url, non-admin url access, invalid streams (not array, not string, unknown stream), invalid accounts (not array, malformed), and invalid account_history_tx_stream (missing/invalid account). Gaps may exist if edge cases (e.g., empty arrays, deprecated fields, or malformed JSON) are not explicitly tested. Template-based validation may be tested indirectly via these handler tests, but custom validation logic (e.g., stream name checks) should have explicit negative/positive tests.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpl::jss (JSON field tags), explicit C++ checks, rpcError error handling", + "validation_layer": "business_logic (handler function doUnsubscribe)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "infoSub and url", + "location": "doUnsubscribe (first if block)", + "validated_by": "explicit if-check", + "validates": [ + "At least one of infoSub or params.url must be present" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNO_PERMISSION)", + "field": "url", + "location": "doUnsubscribe (if context.params.isMember(jss::url))", + "validated_by": "explicit if-check", + "validates": [ + "Only ADMIN role can use url parameter" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns empty result (not error)", + "field": "url", + "location": "doUnsubscribe (ispSub = context.netOps.findRpcSub(strUrl); if (!ispSub))", + "validated_by": "findRpcSub(strUrl) and null check", + "validates": [ + "url must correspond to an existing subscription" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "streams", + "location": "doUnsubscribe (if context.params.isMember(jss::streams))", + "validated_by": "isArray()", + "validates": [ + "streams must be an array" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcSTREAM_MALFORMED)", + "field": "streams[]", + "location": "doUnsubscribe (for loop over streams)", + "validated_by": "isString()", + "validates": [ + "each element of streams must be a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcSTREAM_MALFORMED)", + "field": "streams[]", + "location": "doUnsubscribe (for loop else branch)", + "validated_by": "explicit string comparison", + "validates": [ + "stream name must be one of the allowed values" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(...)", + "field": "accounts_proposed or rt_accounts", + "location": "doUnsubscribe (if context.params.isMember(accountsProposed))", + "validated_by": "isArray()", + "validates": [ + "accounts_proposed/rt_accounts must be an array" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp.ai.md b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp.ai.md new file mode 100644 index 0000000000..218b79c253 --- /dev/null +++ b/src/xrpld/rpc/handlers/subscribe/Unsubscribe.cpp.ai.md @@ -0,0 +1,50 @@ +# `Unsubscribe.cpp` — RPC Handler for Stream Unsubscription + +## Role in the System + +`Unsubscribe.cpp` implements `doUnsubscribe`, the RPC handler that tears down active push subscriptions in the XRPL server. It is the exact counterpart to `doSubscribe` in the same directory and delegates all actual state removal to `NetworkOPs` via the `context.netOps` accessor. The file lives in `src/xrpld/rpc/handlers/subscribe/` alongside `Subscribe.cpp`, forming the complete lifecycle management pair for the server's event-streaming subsystem. + +## Subscriber Identity: Two Modes + +The handler must first resolve *which* subscriber to remove subscriptions from. XRPL supports two subscription transport modes: + +1. **WebSocket connections** — the connection object itself is a live `InfoSub` captured in `context.infoSub`. This is the common case for interactive clients. +2. **JSON-RPC with a `url` parameter** — an admin-configured outbound webhook that the server actively pushes to via `RPCSub`. The subscriber is looked up by URL string through `context.netOps.findRpcSub(strUrl)`. + +The first guard at the top of the function enforces that exactly one of these two modes is available: + +```cpp +if (!context.infoSub && !context.params.isMember(jss::url)) + return rpcError(rpcINVALID_PARAMS); +``` + +This prevents a footgun where a raw JSON-RPC call with no `url` and no active WebSocket connection would silently succeed or panic. The URL mode additionally requires `Role::ADMIN` — non-admin clients cannot reach a shared named subscriber. If `findRpcSub` returns null (the URL never had a subscription registered), the handler returns an empty success object rather than an error — unsubscribing from something that is already gone is treated as a no-op. + +## Subscription Categories + +After resolving `ispSub`, the handler walks through five independent subscription categories, each guarded by `isMember` checks so clients only need to include the categories they wish to remove: + +**Named streams** — the `streams` array accepts any combination of: `server`, `ledger`, `manifests`, `transactions`, `transactions_proposed`, `validations`, `peer_status`, and `consensus`. All of these dispatch to `unsubServer`, `unsubLedger`, etc. on `NetworkOPs` by passing `ispSub->getSeq()` — a stable numeric identifier for the subscriber. Unknown stream names return `rpcSTREAM_MALFORMED` from the `else` branch. The legacy alias `rt_transactions` is accepted silently alongside `transactions_proposed`. + +**Account (proposed) subscriptions** — `accounts_proposed` (legacy alias: `rt_accounts`) and `accounts` each accept an array of base58 account addresses. `RPC::parseAccountIds` converts them; an empty result means the addresses were malformed, returning `rpcACT_MALFORMED`. These call `unsubAccount(ispSub, ids, rt)` with the full `InfoSub::ref` rather than the sequence number, because this path needs to clean up state on both sides: the server's account-to-listener mapping and the `InfoSub` object's own subscription tracking. + +**Account history stream** — the experimental `account_history_tx_stream` object allows a client to stop receiving historical transaction replay for an account. The optional `stop_history_tx_only` boolean lets a client halt the historical replay while keeping the live account subscription intact — the `historyOnly` flag flows straight through to `unsubAccountHistoryInternal` in `NetworkOPs`. This feature is marked experimental and its subscribe path warns clients accordingly. + +**Order books** — the `books` array follows the same `taker_pays`/`taker_gets` structure as the subscribe path. `RPC::parseSubUnsubJson` resolves each leg to an `Asset`, and the pair forms a `Book`. The same-asset check (`book.in == book.out`) guards against degenerate markets. If the `both` or deprecated `both_sides` flag is set, the handler issues a second `unsubBook` call with the reversed book (`reversed(book)`), matching the symmetric subscribe behavior. + +## Asymmetries Relative to `Subscribe.cpp` + +The unsubscribe handler is deliberately simpler in several ways that reflect the semantics of removal vs. registration: + +- `Subscribe.cpp` checks `isConsistent(book)` before adding an order book subscription; `Unsubscribe.cpp` does not. A client should be able to cleanly remove a subscription regardless of whether the book's internal state would pass consistency checks today. +- The `peer_status` stream requires `Role::ADMIN` to *subscribe* but no role check to *unsubscribe* — removing a subscription is always safe regardless of permission level. +- `book_changes` is a subscribable stream in `Subscribe.cpp` but conspicuously absent from `Unsubscribe.cpp`'s stream dispatch table. This means `book_changes` subscriptions cannot be unsubscribed via this handler — they are implicitly torn down when the connection closes. +- `Subscribe.cpp` calls `addRpcSub` to create the URL-based subscriber if absent; `Unsubscribe.cpp` returns an empty success instead of creating anything. + +## URL-Cleanup Timing + +The `removeUrl` boolean flag defers the call to `tryRemoveRpcSub` until after all subscription removal operations complete. This ordering matters: `tryRemoveRpcSub` checks whether the `RPCSub` still has active subscriptions before actually deleting the URL registration. Processing all individual `unsub*` calls first ensures the subscriber is truly empty before the cleanup check runs, preventing a race where the URL entry is removed while the subscriber still holds references. + +## Error Handling + +The handler returns early on the first error encountered within any category. Errors are returned as JSON using `rpcError()` with typed error codes from `ErrorCodes.h`: `rpcINVALID_PARAMS` for structural problems, `rpcSTREAM_MALFORMED` for unknown stream names or non-string stream elements, `rpcACT_MALFORMED` for unparseable account IDs, `rpcNO_PERMISSION` for the admin-only URL path, `rpcBAD_MARKET` for degenerate order books, and `rpcDOMAIN_MALFORMED` for malformed domain hex. A successful unsubscribe returns an empty JSON object — there is no acknowledgment payload. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp.ai.json b/src/xrpld/rpc/handlers/transaction/Simulate.cpp.ai.json new file mode 100644 index 0000000000..b3e33c5f66 --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp.ai.json @@ -0,0 +1,538 @@ +{ + "args": [], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doSimulate", + "simulateTxn", + "getTxJsonFromParams", + "autofillTx", + "autofillSignature", + "getAutofillSequence" + ], + "entry_point": "doSimulate", + "purpose": "Handles the /simulate RPC endpoint: parses input, validates fields, autofills transaction fields, and simulates transaction application.", + "validation_points": [ + "getTxJsonFromParams: initial JSON structure validation", + "autofillTx: checks for required fields, calls autofillSignature", + "autofillSignature: validates Signers array/object structure, checks for pre-existing signatures", + "getAutofillSequence: validates Account field (isString, parseBase58, ledger lookup)" + ] + } + ], + "data_flows": [ + { + "field": "Account", + "flow": [ + "User input", + "getTxJsonFromParams", + "autofillTx", + "getAutofillSequence" + ], + "origin": "User input JSON (tx_json[jss::Account])", + "transformations": [ + "Checked for presence and type (isString)", + "Parsed from base58 to AccountID", + "Looked up in open ledger" + ], + "validated_at": "getAutofillSequence" + }, + { + "field": "Signers", + "flow": [ + "User input", + "getTxJsonFromParams", + "autofillTx", + "autofillSignature" + ], + "origin": "User input JSON (tx_json[jss::Signers])", + "transformations": [ + "Checked for presence and type (isArray)", + "Each element checked for object structure (isObject, isMember(jss::Signer), isObject)", + "Each Signer checked for SigningPubKey and TxnSignature fields" + ], + "validated_at": "autofillSignature" + }, + { + "field": "TxnSignature", + "flow": [ + "User input", + "getTxJsonFromParams", + "autofillTx", + "autofillSignature" + ], + "origin": "User input JSON (tx_json[jss::TxnSignature])", + "transformations": [ + "Checked for presence", + "If present and not empty, error (must not be signed)", + "If missing, autofilled as empty string" + ], + "validated_at": "autofillSignature" + }, + { + "field": "SigningPubKey", + "flow": [ + "User input", + "getTxJsonFromParams", + "autofillTx", + "autofillSignature" + ], + "origin": "User input JSON (tx_json[jss::SigningPubKey])", + "transformations": [ + "Checked for presence", + "If missing, autofilled as empty string" + ], + "validated_at": "autofillSignature" + }, + { + "field": "Fee", + "flow": [ + "User input", + "getTxJsonFromParams", + "autofillTx" + ], + "origin": "User input JSON (tx_json[jss::Fee])", + "transformations": [ + "If missing, autofilled using getCurrentNetworkFee" + ], + "validated_at": "autofillTx" + } + ], + "description": "Implements the 'simulate' RPC command for the XRPL server, which simulates the application of a transaction without submitting it to the ledger. Handles autofilling of transaction fields, error checking, and returns the simulated result.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "SigningPubKey (autofilled if missing)", + "validation", + "missing", + "check" + ], + "evidence": "Field SigningPubKey (autofilled if missing) validated by Json::Value (jsoncpp), jss:: (field name constants), RPC error helpers", + "issue_pattern": "Missing validation for SigningPubKey (autofilled if missing)", + "why_false_positive": "Json::Value (jsoncpp), jss:: (field name constants), RPC error helpers validates SigningPubKey (autofilled if missing) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "TxnSignature (autofilled if missing in Signers)", + "validation", + "missing", + "check" + ], + "evidence": "Field TxnSignature (autofilled if missing in Signers) validated by Json::Value (jsoncpp), jss:: (field name constants), RPC error helpers", + "issue_pattern": "Missing validation for TxnSignature (autofilled if missing in Signers)", + "why_false_positive": "Json::Value (jsoncpp), jss:: (field name constants), RPC error helpers validates TxnSignature (autofilled if missing in Signers) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "Signers (autofilled structure, but type checked)", + "validation", + "missing", + "check" + ], + "evidence": "Field Signers (autofilled structure, but type checked) validated by Json::Value (jsoncpp), jss:: (field name constants), RPC error helpers", + "issue_pattern": "Missing validation for Signers (autofilled structure, but type checked)", + "why_false_positive": "Json::Value (jsoncpp), jss:: (field name constants), RPC error helpers validates Signers (autofilled structure, but type checked) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Account", + "empty", + "string", + "validation" + ], + "evidence": "isString() method on Json::Value at getAutofillSequence", + "issue_pattern": "Missing empty string validation for Account", + "why_false_positive": "isString() method on Json::Value validates Account for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "Account", + "type", + "validation", + "check" + ], + "evidence": "isString() method on Json::Value at getAutofillSequence", + "issue_pattern": "Missing type validation for Account", + "why_false_positive": "isString() method on Json::Value validates Account type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Account", + "empty", + "string", + "validation" + ], + "evidence": "parseBase58 at getAutofillSequence", + "issue_pattern": "Missing empty string validation for Account", + "why_false_positive": "parseBase58 validates Account for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "Account", + "format", + "validation", + "invalid" + ], + "evidence": "parseBase58 at getAutofillSequence", + "issue_pattern": "Missing format validation for Account", + "why_false_positive": "parseBase58 validates Account format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Account", + "empty", + "string", + "validation" + ], + "evidence": "Ledger lookup (context.app.getOpenLedger().current()->read) at getAutofillSequence", + "issue_pattern": "Missing empty string validation for Account", + "why_false_positive": "Ledger lookup (context.app.getOpenLedger().current()->read) validates Account for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Signers", + "empty", + "string", + "validation" + ], + "evidence": "isArray() method on Json::Value at autofillSignature", + "issue_pattern": "Missing empty string validation for Signers", + "why_false_positive": "isArray() method on Json::Value validates Signers for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "Signers", + "type", + "validation", + "check" + ], + "evidence": "isArray() method on Json::Value at autofillSignature", + "issue_pattern": "Missing type validation for Signers", + "why_false_positive": "isArray() method on Json::Value validates Signers type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "Signers[i]", + "empty", + "string", + "validation" + ], + "evidence": "isObject() and isMember(jss::Signer) and isObject() on Signer at autofillSignature", + "issue_pattern": "Missing empty string validation for Signers[i]", + "why_false_positive": "isObject() and isMember(jss::Signer) and isObject() on Signer validates Signers[i] for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "Signers[i]", + "type", + "validation", + "check" + ], + "evidence": "isObject() and isMember(jss::Signer) and isObject() on Signer at autofillSignature", + "issue_pattern": "Missing type validation for Signers[i]", + "why_false_positive": "isObject() and isMember(jss::Signer) and isObject() on Signer validates Signers[i] type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/transaction/Simulate.cpp", + "functions": [ + { + "args": [ + "tx_json", + "context" + ], + "lineno": 13, + "name": "getAutofillSequence" + }, + { + "args": [ + "sigObject" + ], + "lineno": 41, + "name": "autofillSignature" + }, + { + "args": [ + "tx_json", + "context" + ], + "lineno": 77, + "name": "autofillTx" + }, + { + "args": [ + "params" + ], + "lineno": 116, + "name": "getTxJsonFromParams" + }, + { + "args": [ + "context", + "transaction" + ], + "lineno": 172, + "name": "simulateTxn" + }, + { + "args": [ + "context" + ], + "lineno": 232, + "name": "doSimulate" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + }, + { + "audit_implication": "Uses std::runtime_error for error reporting", + "exception_type": "std::runtime_error", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 12, + "name": "xrpl" + } + ], + "test_coverage_notes": "The validation logic is likely covered by unit/integration tests for the /simulate RPC endpoint. Tests should exist for malformed Account fields (not string, invalid base58, missing in ledger), malformed Signers arrays (not array, not object, missing required fields), and pre-signed transactions (TxnSignature present and non-empty). However, code comments (LCOV_EXCL_START) suggest some error paths may not be directly tested. Test files to check: 'test/rpc/Simulate_test.cpp', 'test/rpc/Transaction_test.cpp', and possibly 'test/rpc/Account_test.cpp'. Gaps may exist for edge cases (e.g., deeply nested malformed Signers, autofill edge cases, or rare error conditions).", + "validation_architecture": { + "auto_validated_fields": [ + "SigningPubKey (autofilled if missing)", + "TxnSignature (autofilled if missing in Signers)", + "Signers (autofilled structure, but type checked)" + ], + "framework": "Json::Value (jsoncpp), jss:: (field name constants), RPC error helpers", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(\"tx.Account\")", + "field": "Account", + "location": "getAutofillSequence", + "validated_by": "isString() method on Json::Value", + "validates": [ + "Checks that Account field is a string" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::make_error(rpcSRC_ACT_MALFORMED, ...)", + "field": "Account", + "location": "getAutofillSequence", + "validated_by": "parseBase58", + "validates": [ + "Checks that Account field is a valid base58 AccountID" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcSRC_ACT_NOT_FOUND)", + "field": "Account", + "location": "getAutofillSequence", + "validated_by": "Ledger lookup (context.app.getOpenLedger().current()->read)", + "validates": [ + "Checks that Account exists in the current ledger (unless TicketSequence is present)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(\"tx.Signers\")", + "field": "Signers", + "location": "autofillSignature", + "validated_by": "isArray() method on Json::Value", + "validates": [ + "Checks that Signers field is an array" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "RPC::invalid_field_error(\"tx.Signers[]\")", + "field": "Signers[i]", + "location": "autofillSignature", + "validated_by": "isObject() and isMember(jss::Signer) and isObject() on Signer", + "validates": [ + "Checks that each element of Signers is an object", + "Checks that each element has a Signer member", + "Checks that Signer member is an object" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/Simulate.cpp.ai.md b/src/xrpld/rpc/handlers/transaction/Simulate.cpp.ai.md new file mode 100644 index 0000000000..4b492a548e --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/Simulate.cpp.ai.md @@ -0,0 +1,45 @@ +# `Simulate.cpp` — Dry-Run Transaction Execution + +## Role and Motivation + +This file implements the `simulate` RPC command, which allows a client to test a transaction against the current ledger state without submitting it to the network. The primary use case is development and debugging: callers can verify that a transaction would succeed, inspect the metadata it would produce, and examine computed fields — all without spending XRP or affecting any persisted ledger state. Architecturally, `Simulate.cpp` occupies the same handler layer as `Submit.cpp`, but deliberately avoids every side effect real submission triggers: no peer broadcast, no hash-router entry, no queue insertion. + +The mechanism that makes this work is `tapDRY_RUN`, a bit flag in the `ApplyFlags` enum (defined in `ApplyView.h`) passed to `TxQ::apply()`. Inside `Transactor.cpp`, this flag bypasses signature checking, routes the transaction through the full engine execution path (including metadata generation via `ApplyContext::apply()`), and then forces `applied = false` before returning — so the `OpenView` snapshot is discarded without ever being committed. This reuses the exact same code path as a real submission, which is what gives the simulation its fidelity. + +## Input Handling: `getTxJsonFromParams()` + +The command accepts either a pre-serialized blob (`tx_blob`) or a structured JSON object (`tx_json`), but not both. The mutual exclusion is checked first and is fatal — passing both triggers an explicit parameter error. When a blob is supplied, it is hex-decoded and deserialized into an `STObject` via `SerialIter`, then immediately serialized back to JSON. This round-trip through the canonical binary format normalizes the input into a consistent representation before downstream logic touches it. + +Two minimal sanity checks follow: the presence of `TransactionType` and `Account`. These are required for the subsequent autofill logic to function at all. + +## Autofill Pipeline: `autofillTx()`, `autofillSignature()`, `getAutofillSequence()` + +Simulation is designed to work with an incompletely constructed transaction. The autofill pipeline synthesizes fields that would normally be mandatory for signing. + +**Fee** is the first thing handled in `autofillTx()`. It calls `RPC::getCurrentNetworkFee()`, which consults the live fee-track and transaction queue to compute a realistic fee. The code comment explicitly notes this must run *after* all other autofills, because fee calculation may depend on other fields (such as transaction type) already being in place. + +**Sequence** is derived from `TxQ::nextQueuableSeq()` queried against the live open ledger's account SLE. If the account does not exist in the current ledger and no `TicketSequence` is provided, the function returns `rpcSRC_ACT_NOT_FOUND`. When `TicketSequence` is present, the sequence is set to 0 — matching the wire-format rule that ticket-based transactions carry a zero sequence number. + +**NetworkID** is injected only for networks with an ID greater than 1024, consistent with the XRPL protocol rule that mainnet and other networks with IDs ≤ 1024 omit the field entirely. + +**Signature fields** deserve careful attention. `autofillSignature()` fills `SigningPubKey` with an empty string and `TxnSignature` with an empty string — the canonical representation of an unsigned transaction in XRPL binary. The `tapDRY_RUN` flag tells `Transactor::checkSign` to skip signature validation when these are empty. However, if the caller supplies a *non-empty* `TxnSignature`, the function immediately returns `rpcTX_SIGNED`. The same check applies per-element for multi-signed transactions in the `Signers` array. This enforcement prevents a confusing scenario where simulate silently executes an already-signed transaction, which might mislead the caller into thinking signature validation passed. The `Transactor.cpp` code includes a defensive comment: "This code should never be hit because it's checked in the `simulate` RPC" — the RPC layer is the first line of defense. + +## Simulation Execution: `simulateTxn()` + +Once the transaction is validated and autofilled, `doSimulate()` parses the JSON into an `STTx` and wraps it in a `Transaction`. The `ttBATCH` transaction type is explicitly rejected with `rpcNOT_IMPL` before this point — batch transactions involve composite execution semantics the dry-run path cannot faithfully replicate. + +`simulateTxn()` takes a snapshot copy of the current `OpenView` by value, then calls `TxQ::apply()` with `tapDRY_RUN`. The result is an `ApplyResult` struct (from `applySteps.h`) containing a `TER` result code, an `applied` boolean, and an `std::optional` metadata object. Because `tapDRY_RUN` forces `applied = false` at the end of `Transactor::apply()`, the ledger snapshot is never committed. + +The response is assembled as follows: +- `applied` and `ledger_index` are set unconditionally. +- `engine_result`, `engine_result_code`, and `engine_result_message` translate the `TER` into human-readable form. For `tesSUCCESS`, the message is overridden to read "The simulated transaction would have been applied" — preventing the generic success string from implying the transaction was actually committed. +- If metadata is present, it is either serialized as hex (`meta_blob`) or rendered as JSON (`meta`) depending on the `binary` parameter. In JSON mode, three enrichment functions run: `RPC::insertDeliveredAmount()`, `RPC::insertNFTSyntheticInJson()`, and `RPC::insertMPTokenIssuanceID()`. These are the same post-processing calls used in the real transaction response pipeline, ensuring simulated results are structurally identical to what a live submission response would contain. +- The transaction itself is echoed back as either `tx_blob` or `tx_json`, now containing the autofilled fields, giving the caller a complete view of what would have been submitted. + +## Security and Resource Considerations + +`doSimulate()` enforces an explicit blocklist of credential fields before any other processing: if the request includes `secret`, `seed`, `seed_hex`, or `passphrase`, it returns `invalid_field_error` immediately. This is a deliberate design choice — the simulate endpoint should never be a channel through which signing keys are transmitted to the server, even accidentally. + +The outer `try/catch` around `simulateTxn()` is marked `LCOV_EXCL` — it is a crash guard against unexpected exceptions and is not expected to fire under any known condition. XRPL's RPC handler convention is to never let an uncaught exception propagate. + +Resource cost is set to `feeMediumBurdenRPC`. A full dry-run execution is meaningfully more expensive than a read-only query because it runs the transaction engine, but less expensive than a real submission that triggers peer propagation and queue management. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/Submit.cpp.ai.json b/src/xrpld/rpc/handlers/transaction/Submit.cpp.ai.json new file mode 100644 index 0000000000..8d1e1db047 --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/Submit.cpp.ai.json @@ -0,0 +1,558 @@ +{ + "args": [ + { + "lineno": 9, + "name": "context" + }, + { + "lineno": 19, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doSubmit", + "strUnHex", + "STTx constructor", + "checkValidity", + "Transaction constructor", + "getStatus", + "processTransaction" + ], + "entry_point": "doSubmit", + "purpose": "Handles submission of a transaction via RPC, validates and processes the transaction blob or JSON.", + "validation_points": [ + "context.params.isMember(jss::tx_blob) // checks presence of tx_blob", + "strUnHex // validates hex encoding of tx_blob", + "STTx constructor // validates transaction structure", + "checkValidity // validates transaction signature and local rules", + "Transaction constructor and getStatus // validates transaction status" + ] + }, + { + "call_chain": [ + "doSubmit", + "RPC::transactionSubmit" + ], + "entry_point": "doSubmit (tx_json path)", + "purpose": "Handles submission of a transaction via tx_json (deprecated path), delegates to transactionSubmit for signing and validation.", + "validation_points": [ + "RPC::transactionSubmit // performs its own validation (not shown in this file)" + ] + } + ], + "data_flows": [ + { + "field": "tx_blob", + "flow": [ + "context.params[jss::tx_blob]", + "strUnHex", + "SerialIter sitTrans(makeSlice(*ret))", + "STTx constructor", + "std::make_shared(stTx, ...)", + "processTransaction" + ], + "origin": "context.params[jss::tx_blob] (JSON RPC input)", + "transformations": [ + "Hex string decoded to binary (strUnHex)", + "Binary parsed into transaction object (STTx)", + "Transaction object wrapped for processing (Transaction)" + ], + "validated_at": "strUnHex, STTx constructor, checkValidity, Transaction constructor" + }, + { + "field": "tx_json", + "flow": [ + "context.params['tx_json']", + "RPC::transactionSubmit" + ], + "origin": "context.params['tx_json'] (JSON RPC input)", + "transformations": [ + "Parsed and signed (if needed) in transactionSubmit" + ], + "validated_at": "RPC::transactionSubmit (not shown in this file)" + }, + { + "field": "transaction signature", + "flow": [ + "STTx constructor", + "checkValidity" + ], + "origin": "Inside tx_blob or tx_json", + "transformations": [ + "Signature extracted and checked" + ], + "validated_at": "checkValidity" + }, + { + "field": "transaction status", + "flow": [ + "Transaction constructor", + "getStatus" + ], + "origin": "Transaction object (constructed from STTx)", + "transformations": [ + "Status determined based on local checks" + ], + "validated_at": "Transaction constructor, getStatus" + } + ], + "description": "Implements the 'submit' RPC handler for submitting transactions to the XRPL network, handling both signed transaction blobs and JSON transactions, performing validation, and returning detailed results.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "tx_blob (presence, format)", + "validation", + "missing", + "check" + ], + "evidence": "Field tx_blob (presence, format) validated by jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation)", + "issue_pattern": "Missing validation for tx_blob (presence, format)", + "why_false_positive": "jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation) validates tx_blob (presence, format) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "tx_json (structure, via transactionSubmit)", + "validation", + "missing", + "check" + ], + "evidence": "Field tx_json (structure, via transactionSubmit) validated by jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation)", + "issue_pattern": "Missing validation for tx_json (structure, via transactionSubmit)", + "why_false_positive": "jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation) validates tx_json (structure, via transactionSubmit) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "fail_hard (type, presence)", + "validation", + "missing", + "check" + ], + "evidence": "Field fail_hard (type, presence) validated by jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation)", + "issue_pattern": "Missing validation for fail_hard (type, presence)", + "why_false_positive": "jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation) validates fail_hard (type, presence) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "transaction signature (via checkValidity)", + "validation", + "missing", + "check" + ], + "evidence": "Field transaction signature (via checkValidity) validated by jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation)", + "issue_pattern": "Missing validation for transaction signature (via checkValidity)", + "why_false_positive": "jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation) validates transaction signature (via checkValidity) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "transaction status (via Transaction)", + "validation", + "missing", + "check" + ], + "evidence": "Field transaction status (via Transaction) validated by jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation)", + "issue_pattern": "Missing validation for transaction status (via Transaction)", + "why_false_positive": "jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation) validates transaction status (via Transaction) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tx_blob", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::tx_blob) at doSubmit", + "issue_pattern": "Missing empty string validation for tx_blob", + "why_false_positive": "context.params.isMember(jss::tx_blob) validates tx_blob for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tx_blob", + "empty", + "string", + "validation" + ], + "evidence": "strUnHex at doSubmit", + "issue_pattern": "Missing empty string validation for tx_blob", + "why_false_positive": "strUnHex validates tx_blob for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "tx_blob", + "format", + "validation", + "invalid" + ], + "evidence": "strUnHex at doSubmit", + "issue_pattern": "Missing format validation for tx_blob", + "why_false_positive": "strUnHex validates tx_blob format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tx_blob", + "empty", + "string", + "validation" + ], + "evidence": "STTx constructor (std::make_shared(std::ref(sitTrans))) at doSubmit", + "issue_pattern": "Missing empty string validation for tx_blob", + "why_false_positive": "STTx constructor (std::make_shared(std::ref(sitTrans))) validates tx_blob for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction signature", + "empty", + "string", + "validation" + ], + "evidence": "checkValidity at doSubmit", + "issue_pattern": "Missing empty string validation for transaction signature", + "why_false_positive": "checkValidity validates transaction signature for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "transaction status", + "empty", + "string", + "validation" + ], + "evidence": "Transaction constructor and getStatus() at doSubmit", + "issue_pattern": "Missing empty string validation for transaction status", + "why_false_positive": "Transaction constructor and getStatus() validates transaction status for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "tx_json", + "empty", + "string", + "validation" + ], + "evidence": "RPC::transactionSubmit at doSubmit (else branch if tx_blob not present)", + "issue_pattern": "Missing empty string validation for tx_json", + "why_false_positive": "RPC::transactionSubmit validates tx_json for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.7, + "detection_keywords": [ + "fail_hard", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(\"fail_hard\") && context.params[\"fail_hard\"].asBool() at getFailHard", + "issue_pattern": "Missing empty string validation for fail_hard", + "why_false_positive": "context.params.isMember(\"fail_hard\") && context.params[\"fail_hard\"].asBool() validates fail_hard for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "fail_hard", + "type", + "validation", + "check" + ], + "evidence": "context.params.isMember(\"fail_hard\") && context.params[\"fail_hard\"].asBool() at getFailHard", + "issue_pattern": "Missing type validation for fail_hard", + "why_false_positive": "context.params.isMember(\"fail_hard\") && context.params[\"fail_hard\"].asBool() validates fail_hard type" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/transaction/Submit.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 9, + "name": "getFailHard" + }, + { + "args": [ + "context" + ], + "lineno": 19, + "name": "doSubmit" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation paths (tx_blob decoding, STTx construction, checkValidity, Transaction status) are likely covered by integration and unit tests for the submit RPC handler. Typical test files would be in the 'test' or 'unittest' directories, such as 'test/rpc/Submit_test.cpp', 'test/rpc/Transaction_test.cpp', or similar. However, coverage gaps may exist for edge cases: malformed hex in tx_blob, rare signature failures, or deprecated tx_json path. The code relies on exceptions for error handling, so tests should also verify error responses for invalid input. The template-based validation framework is not directly visible here, so its coverage depends on upstream tests.", + "validation_architecture": { + "auto_validated_fields": [ + "tx_blob (presence, format)", + "tx_json (structure, via transactionSubmit)", + "fail_hard (type, presence)", + "transaction signature (via checkValidity)", + "transaction status (via Transaction)" + ], + "framework": "jss:: (JSON field tags), RPC:: (error handling), STTx (transaction validation), Transaction (status validation)", + "validation_layer": "entry_point (doSubmit), business_logic (STTx, checkValidity, Transaction)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "RPC::make_error(rpcNOT_SUPPORTED, ...)", + "field": "tx_blob", + "location": "doSubmit", + "validated_by": "context.params.isMember(jss::tx_blob)", + "validates": [ + "Checks if tx_blob is present in input params" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "tx_blob", + "location": "doSubmit", + "validated_by": "strUnHex", + "validates": [ + "Checks if tx_blob is a valid hex string and not empty" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "std::exception (caught, returns error JSON)", + "field": "tx_blob", + "location": "doSubmit", + "validated_by": "STTx constructor (std::make_shared(std::ref(sitTrans)))", + "validates": [ + "Checks if tx_blob decodes to a valid transaction object" + ], + "validation_type": "format|type" + }, + { + "confidence": 1.0, + "error_thrown": "returns error JSON", + "field": "transaction signature", + "location": "doSubmit", + "validated_by": "checkValidity", + "validates": [ + "Checks transaction signature validity", + "Checks transaction conforms to local rules" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "returns error JSON", + "field": "transaction status", + "location": "doSubmit", + "validated_by": "Transaction constructor and getStatus()", + "validates": [ + "Checks if transaction status is NEW (not duplicate, not invalid, etc.)" + ], + "validation_type": "business_logic" + }, + { + "confidence": 0.8, + "error_thrown": "returns error JSON (via RPC::transactionSubmit)", + "field": "tx_json", + "location": "doSubmit (else branch if tx_blob not present)", + "validated_by": "RPC::transactionSubmit", + "validates": [ + "Checks tx_json object structure and fields", + "Checks signing parameters (secret, etc.)" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 0.7, + "error_thrown": "none (used as bool, no error thrown)", + "field": "fail_hard", + "location": "getFailHard", + "validated_by": "context.params.isMember(\"fail_hard\") && context.params[\"fail_hard\"].asBool()", + "validates": [ + "Checks if fail_hard is present and is a boolean" + ], + "validation_type": "type" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/Submit.cpp.ai.md b/src/xrpld/rpc/handlers/transaction/Submit.cpp.ai.md new file mode 100644 index 0000000000..7edb506054 --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/Submit.cpp.ai.md @@ -0,0 +1,43 @@ +# `src/xrpld/rpc/handlers/transaction/Submit.cpp` + +## Purpose and Role + +This file implements `doSubmit`, the RPC handler for the `submit` command — the primary entry point through which clients inject transactions into the XRP Ledger network. It lives alongside the other transaction-focused handlers (`SubmitMultiSigned`, `Simulate`, `Tx`, etc.) in the `transaction/` subdirectory and is one of the most heavily exercised handlers in normal ledger operation. + +The handler supports two mutually exclusive submission modes, selected by the presence or absence of the `tx_blob` field in the request parameters. This bifurcation reflects an architectural decision made during the evolution of the protocol: server-side signing was an early convenience feature that became a security liability, and the `tx_blob` path (pre-signed binary) is now the only mode considered production-safe. + +## Two Submission Paths + +### tx_blob (Primary Path) + +When `tx_blob` is present, the handler executes a strict, layered validation pipeline before dispatching to the network: + +1. **Hex decode** — `strUnHex` converts the hex string to raw bytes. An empty or malformed result returns `rpcINVALID_PARAMS` immediately. +2. **Deserialization** — A `SerialIter` over the raw bytes is fed to the `STTx` constructor. Any malformed binary structure throws, caught into an `invalidTransaction` error response. +3. **Signature and local-rule validation** — `checkValidity` consults the `HashRouter` cache to avoid re-verifying the same transaction repeatedly, then checks the cryptographic signature and whether the transaction satisfies the current ledger's rule set. There is one deliberate performance escape hatch here: when `context.app.checkSigs()` returns false (a configuration option for trusted internal submissions), `forceValidity` pre-marks the transaction as `SigGoodOnly` in the router cache, skipping the expensive signature check. This is done with care — the cache can only be raised toward validity, not degraded, so a legitimately bad signature can still be caught by other layers. +4. **Transaction wrapper construction** — The validated `STTx` is wrapped in a `Transaction` object. The `Transaction` constructor performs additional local checks (e.g., fee sanity, account fields), and the status must be `NEW` (`TransStatus::NEW = 0`) to proceed. Any other status — `INVALID`, `HELD`, `OBSOLETE`, etc. — produces an early rejection. +5. **Network dispatch** — `context.netOps.processTransaction` is called with `bLocal = true` and the `fail_hard` flag, handing the transaction to `NetworkOPs` for application to the open ledger, queuing, and P2P broadcast. + +### tx_json (Deprecated Path) + +When `tx_blob` is absent, the handler falls back to server-side signing via `RPC::transactionSubmit`. This path requires the caller to supply a private key (`secret`) alongside the transaction JSON, which the server uses to sign the transaction before forwarding it. Two access controls guard this path: it is only permitted if the caller has `ADMIN` role, or if the server is explicitly configured with `canSign()` enabled. A `deprecated` warning is injected into every response from this path, signalling that it will eventually be removed. + +The `RPC::transactionSubmit` function in `detail/TransactionSign.h` encapsulates signing, fee auto-fill, and the same `ProcessTransactionFn` callback used for the blob path. This design — passing a `std::function<>` wrapping `netOps.processTransaction` — decouples the signing logic from the live networking layer and makes it testable in isolation. + +## The `fail_hard` Flag + +`getFailHard` reads the optional `fail_hard` boolean parameter and converts it to a `NetworkOPs::FailHard` enum. When set, the network layer will reject the transaction rather than queue it if it cannot be applied immediately to the open ledger. This is useful for submitters who need definitive acceptance or rejection rather than an ambiguous queued state, but it reduces the chances of eventual inclusion under load. Omitting the flag produces the default lenient behavior. + +## Response Enrichment + +After `processTransaction` returns, the handler assembles a rich JSON response. When the `TER` result is anything other than `temUNCERTAIN` (meaning the network made a deterministic decision), the response includes: + +- `engine_result` / `engine_result_code` / `engine_result_message`: human-readable and machine-readable TER classification. +- `accepted`, `applied`, `broadcast`, `queued`, `kept`: individual flags drawn from `Transaction::SubmitResult`, reflecting exactly what the network layer did with the transaction — applied to the open ledger, queued for a future ledger, broadcast to peers, or held in local memory. +- `account_sequence_next`, `account_sequence_available`, `open_ledger_cost`, `validated_ledger_index`: advisory fields from `Transaction::CurrentLedgerState`, populated by `NetworkOPs` during processing, that help clients decide whether to resubmit, bump fees, or wait. + +This granular decomposition of the submit outcome — rather than a simple success/failure — is deliberate. Because XRPL is a distributed consensus system, a transaction can be locally applied but not yet broadcast, or broadcast but lost before validation; the individual flags let clients reason about the transaction's propagation state without having to poll the ledger. + +## Error Handling Strategy + +The handler uses three distinct error-reporting mechanisms appropriate to where failures occur. Pre-dispatch failures (bad hex, deserialization errors, local check failures) return early with structured JSON error objects rather than exceptions, since they represent expected invalid-input scenarios. The `processTransaction` call and the final JSON serialization are each wrapped in their own `try/catch`, distinguishing internal submission failures (`internalSubmit`) from JSON encoding failures (`internalJson`) — a practical diagnostic distinction when debugging node-side issues. The resource cost is registered as `feeMediumBurdenRPC` at entry, ensuring the request accounting subsystem tracks submit calls appropriately regardless of outcome. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp.ai.json b/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp.ai.json new file mode 100644 index 0000000000..9faa49779e --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp.ai.json @@ -0,0 +1,343 @@ +{ + "args": [ + { + "lineno": 11, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doSubmitMultiSigned", + "RPC::transactionSubmitMultiSigned" + ], + "entry_point": "doSubmitMultiSigned", + "purpose": "Handles the submission of a multisigned transaction via RPC, including validation and forwarding to transaction processing.", + "validation_points": [ + "fail_hard validated in doSubmitMultiSigned via Json::Value::asBool()", + "params, SigningAccounts, tx_json validated in RPC::transactionSubmitMultiSigned (template-based validation)" + ] + } + ], + "data_flows": [ + { + "field": "fail_hard", + "flow": [ + "context.params[jss::fail_hard]", + "asBool()", + "failHard (local variable)", + "NetworkOPs::doFailHard(failHard)", + "failType (passed to transactionSubmitMultiSigned)" + ], + "origin": "context.params[jss::fail_hard] (JSON RPC input)", + "transformations": [ + "Converted from JSON value to bool", + "Mapped to failType via NetworkOPs::doFailHard" + ], + "validated_at": "doSubmitMultiSigned (asBool() ensures boolean type)" + }, + { + "field": "params", + "flow": [ + "context.params", + "passed to RPC::transactionSubmitMultiSigned" + ], + "origin": "context.params (entire JSON RPC input)", + "transformations": [ + "No transformation in doSubmitMultiSigned; passed as-is" + ], + "validated_at": "RPC::transactionSubmitMultiSigned (template-based validation of required fields)" + }, + { + "field": "SigningAccounts", + "flow": [ + "context.params", + "passed to RPC::transactionSubmitMultiSigned", + "validated inside transactionSubmitMultiSigned (template/jss:: validation)" + ], + "origin": "context.params[\"SigningAccounts\"] (JSON RPC input)", + "transformations": [ + "Likely checked for array type, structure, and content" + ], + "validated_at": "RPC::transactionSubmitMultiSigned" + }, + { + "field": "tx_json", + "flow": [ + "context.params", + "passed to RPC::transactionSubmitMultiSigned", + "validated inside transactionSubmitMultiSigned (template/jss:: validation)" + ], + "origin": "context.params[\"tx_json\"] (JSON RPC input)", + "transformations": [ + "Likely checked for object type, required transaction fields, and structure" + ], + "validated_at": "RPC::transactionSubmitMultiSigned" + } + ], + "description": "Implements the doSubmitMultiSigned function for submitting multisigned transactions via RPC in the XRPL server.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "fail_hard", + "validation", + "missing", + "check" + ], + "evidence": "Field fail_hard validated by jss:: (JSON field validation), RPC::transactionSubmitMultiSigned (template-based validation)", + "issue_pattern": "Missing validation for fail_hard", + "why_false_positive": "jss:: (JSON field validation), RPC::transactionSubmitMultiSigned (template-based validation) validates fail_hard automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "SigningAccounts", + "validation", + "missing", + "check" + ], + "evidence": "Field SigningAccounts validated by jss:: (JSON field validation), RPC::transactionSubmitMultiSigned (template-based validation)", + "issue_pattern": "Missing validation for SigningAccounts", + "why_false_positive": "jss:: (JSON field validation), RPC::transactionSubmitMultiSigned (template-based validation) validates SigningAccounts automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "tx_json", + "validation", + "missing", + "check" + ], + "evidence": "Field tx_json validated by jss:: (JSON field validation), RPC::transactionSubmitMultiSigned (template-based validation)", + "issue_pattern": "Missing validation for tx_json", + "why_false_positive": "jss:: (JSON field validation), RPC::transactionSubmitMultiSigned (template-based validation) validates tx_json automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "fail_hard", + "empty", + "string", + "validation" + ], + "evidence": "Json::Value::asBool() at doSubmitMultiSigned", + "issue_pattern": "Missing empty string validation for fail_hard", + "why_false_positive": "Json::Value::asBool() validates fail_hard for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "fail_hard", + "type", + "validation", + "check" + ], + "evidence": "Json::Value::asBool() at doSubmitMultiSigned", + "issue_pattern": "Missing type validation for fail_hard", + "why_false_positive": "Json::Value::asBool() validates fail_hard type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "params", + "empty", + "string", + "validation" + ], + "evidence": "RPC::transactionSubmitMultiSigned at doSubmitMultiSigned (delegated)", + "issue_pattern": "Missing empty string validation for params", + "why_false_positive": "RPC::transactionSubmitMultiSigned validates params for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "SigningAccounts", + "empty", + "string", + "validation" + ], + "evidence": "RPC::transactionSubmitMultiSigned (likely via template/jss:: validation) at transactionSubmitMultiSigned", + "issue_pattern": "Missing empty string validation for SigningAccounts", + "why_false_positive": "RPC::transactionSubmitMultiSigned (likely via template/jss:: validation) validates SigningAccounts for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "tx_json", + "empty", + "string", + "validation" + ], + "evidence": "RPC::transactionSubmitMultiSigned (likely via template/jss:: validation) at transactionSubmitMultiSigned", + "issue_pattern": "Missing empty string validation for tx_json", + "why_false_positive": "RPC::transactionSubmitMultiSigned (likely via template/jss:: validation) validates tx_json for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 10, + "name": "doSubmitMultiSigned" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "Testing for this handler is likely found in integration or functional test suites for the RPC interface, such as 'test/functional/SubmitMultiSigned_test.cpp', 'test/functional/Transaction_test.cpp', or similar files. These tests typically cover valid and invalid multisigned submissions, missing or malformed SigningAccounts, tx_json, and fail_hard values. However, gaps may exist in edge cases (e.g., deeply nested or malformed JSON, unusual fail_hard values, or template validation bypasses). Direct unit tests for template-based validation logic may be limited, as much validation is handled generically.", + "validation_architecture": { + "auto_validated_fields": [ + "fail_hard", + "SigningAccounts", + "tx_json" + ], + "framework": "jss:: (JSON field validation), RPC::transactionSubmitMultiSigned (template-based validation)", + "validation_layer": "entry_point (doSubmitMultiSigned), business_logic (transactionSubmitMultiSigned)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "Json::LogicError (if not convertible to bool)", + "field": "fail_hard", + "location": "doSubmitMultiSigned", + "validated_by": "Json::Value::asBool()", + "validates": [ + "Ensures fail_hard is a boolean" + ], + "validation_type": "type" + }, + { + "confidence": 0.9, + "error_thrown": "various (see transactionSubmitMultiSigned implementation)", + "field": "params", + "location": "doSubmitMultiSigned (delegated)", + "validated_by": "RPC::transactionSubmitMultiSigned", + "validates": [ + "Checks presence and type of SigningAccounts (array)", + "Checks presence and type of tx_json (object)", + "Validates multisignature structure and signatures", + "Validates transaction format and required fields" + ], + "validation_type": "format|type|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "rpcINVALID_PARAMS or similar error code", + "field": "SigningAccounts", + "location": "transactionSubmitMultiSigned", + "validated_by": "RPC::transactionSubmitMultiSigned (likely via template/jss:: validation)", + "validates": [ + "Ensures SigningAccounts is an array", + "Ensures each entry is a valid account object", + "Checks for duplicates or invalid accounts" + ], + "validation_type": "type|format|business_logic" + }, + { + "confidence": 0.9, + "error_thrown": "rpcINVALID_PARAMS or similar error code", + "field": "tx_json", + "location": "transactionSubmitMultiSigned", + "validated_by": "RPC::transactionSubmitMultiSigned (likely via template/jss:: validation)", + "validates": [ + "Ensures tx_json is an object", + "Validates required transaction fields (Account, Fee, Sequence, etc.)", + "Checks for valid field types and values" + ], + "validation_type": "type|format|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp.ai.md b/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp.ai.md new file mode 100644 index 0000000000..b67658e5be --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/SubmitMultiSigned.cpp.ai.md @@ -0,0 +1,48 @@ +# `SubmitMultiSigned.cpp` — RPC Handler for Multi-Signed Transaction Submission + +## Role in the System + +This file implements `doSubmitMultiSigned`, the entry point for the `submit_multisigned` JSON-RPC command. It sits at the top of the call chain for clients who have already collected multiple signatures on a transaction (using `sign_for`) and now want to broadcast it to the network. Its place in `src/xrpld/rpc/handlers/transaction/` puts it alongside `Submit.cpp`, `Simulate.cpp`, and the other per-command handlers, each of which is a thin bridge between the raw `RPC::JsonContext` and the richer business logic in `RPC::detail`. + +## What the Handler Does + +The function body is intentionally compact: + +```cpp +Json::Value +doSubmitMultiSigned(RPC::JsonContext& context) +{ + context.loadType = Resource::feeHeavyBurdenRPC; + auto const failHard = context.params[jss::fail_hard].asBool(); + auto const failType = NetworkOPs::doFailHard(failHard); + + return RPC::transactionSubmitMultiSigned( + context.params, + context.apiVersion, + failType, + context.role, + context.ledgerMaster.getValidatedLedgerAge(), + context.app, + RPC::getProcessTxnFn(context.netOps)); +} +``` + +Three things happen before the real work is dispatched: + +**Load classification.** The first statement marks the request as `Resource::feeHeavyBurdenRPC`, which carries a weight of 3000 — the highest tier in the ledger's resource fee schedule. Multi-signed submission is legitimately expensive: the callee must check each signer's account in the open ledger, verify multiple ECDSA or Ed25519 signatures, parse and serialize the full transaction, and enqueue it for network broadcast. Marking load before doing any work ensures the resource manager can apply rate-limiting even if the call ultimately fails early. + +**`fail_hard` extraction.** The JSON parameter is pulled via `jss::fail_hard` and converted with `asBool()`. If the field is absent, `Json::Value::asBool()` returns `false` by default, making the flag optional. The boolean is then mapped to `NetworkOPs::FailHard` via `NetworkOPs::doFailHard()`, a conversion that produces an enum value understood by the network operations layer. `FailHard::yes` tells the network to reject the transaction outright if it cannot be applied to the open ledger, while the default (`no`) allows it to be queued. This maps exactly to the `tx_blob`-path behaviour in `doSubmit`. + +**Delegation.** All structural validation, cryptographic checking, ledger lookups, and actual submission are handled by `RPC::transactionSubmitMultiSigned` in `src/xrpld/rpc/detail/TransactionSign.cpp`. The `context.params` block is forwarded as-is (the callee receives it by value, so it may modify a local copy). The handler also passes `context.ledgerMaster.getValidatedLedgerAge()`, which the callee uses to detect a stale validated ledger and refuse fee auto-fill — a defensive measure against submitting into an outdated view of the network. + +## Design Contrast with `Submit.cpp` + +Looking at the sibling `doSubmit` handler makes the separation of concerns clear. `doSubmit` contains substantial inline logic: hex-decoding a raw `tx_blob`, constructing an `STTx`, calling `checkValidity`, managing a `Transaction` object, and catching exceptions from `processTransaction`. Multi-signed submission avoids all that inline complexity because the transaction is provided as `tx_json` (JSON fields, not a pre-serialised blob), so parsing, validation, and signature aggregation are more structured and can be neatly encapsulated in `transactionSubmitMultiSigned`. + +## Dependency Injection via `getProcessTxnFn` + +Rather than calling `context.netOps.processTransaction(...)` directly, the handler passes a `ProcessTransactionFn` closure produced by `RPC::getProcessTxnFn(context.netOps)`. This is a `std::function` that captures a reference to `NetworkOPs` and forwards the `processTransaction` call. The pattern allows `RPC::transactionSubmitMultiSigned` — which lives in a layer below the RPC context — to invoke network operations without taking a dependency on the full `NetworkOPs` interface or the `JsonContext`. It also makes the business logic in `TransactionSign.cpp` easier to unit-test in isolation by injecting a mock function. + +## Validation Architecture + +Input validation is split across two layers. The handler itself performs only the minimum type coercion (`asBool()` for `fail_hard`). All deeper validation — presence and type of `SigningAccounts`, structure of `tx_json`, required transaction fields, fee adequacy, source account existence, and multi-signature correctness — is delegated to `transactionSubmitMultiSigned`. This delegation is intentional: those checks require access to the open ledger, the fee tracker, and the transaction queue, which are all passed through the handler's arguments rather than accessed inline. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp.ai.json b/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp.ai.json new file mode 100644 index 0000000000..1a847bbaf3 --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp.ai.json @@ -0,0 +1,437 @@ +{ + "args": [ + { + "lineno": 14, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doTransactionEntry", + "RPC::lookupLedger", + "lpLedger->txRead" + ], + "entry_point": "doTransactionEntry", + "purpose": "Handles the 'transaction_entry' RPC command: looks up a ledger, validates input, fetches a transaction by hash, and returns transaction details.", + "validation_points": [ + "RPC::lookupLedger: Validates ledger_hash/ledger_index", + "doTransactionEntry: Checks for tx_hash presence (isMember)", + "doTransactionEntry: Checks for ledger_hash in jvResult", + "doTransactionEntry: Validates tx_hash format with uTransID.parseHex", + "doTransactionEntry: Validates transaction existence with lpLedger->txRead" + ] + } + ], + "data_flows": [ + { + "field": "ledger_hash / ledger_index", + "flow": [ + "context.params", + "RPC::lookupLedger", + "lpLedger (ReadView pointer)", + "used for transaction lookup and response" + ], + "origin": "context.params (JSON-RPC request)", + "transformations": [ + "Parsed and validated by RPC::lookupLedger", + "If invalid, error returned in jvResult" + ], + "validated_at": "RPC::lookupLedger" + }, + { + "field": "tx_hash", + "flow": [ + "context.params[jss::tx_hash]", + "isMember check in doTransactionEntry", + "parseHex to uint256 uTransID", + "lpLedger->txRead(uTransID)", + "used for transaction lookup and response" + ], + "origin": "context.params (JSON-RPC request)", + "transformations": [ + "Presence checked with isMember", + "String parsed to uint256 with parseHex", + "Used as key for transaction lookup" + ], + "validated_at": "isMember, parseHex, lpLedger->txRead" + }, + { + "field": "jvResult", + "flow": [ + "RPC::lookupLedger", + "doTransactionEntry (error propagation or augmentation)", + "returned as RPC response" + ], + "origin": "RPC::lookupLedger return value", + "transformations": [ + "Populated with error or ledger info", + "Augmented with transaction info or error" + ], + "validated_at": "Throughout doTransactionEntry" + }, + { + "field": "sttx (SerializedTransaction)", + "flow": [ + "lpLedger->txRead", + "doTransactionEntry", + "sttx->getJson", + "jvResult[jss::tx_json]" + ], + "origin": "lpLedger->txRead(uTransID)", + "transformations": [ + "Deserialized from ledger", + "Converted to JSON" + ], + "validated_at": "lpLedger->txRead (existence check)" + }, + { + "field": "stobj (TransactionMeta)", + "flow": [ + "lpLedger->txRead", + "doTransactionEntry", + "stobj->getJson", + "jvResult[json_meta]" + ], + "origin": "lpLedger->txRead(uTransID)", + "transformations": [ + "Deserialized from ledger", + "Converted to JSON" + ], + "validated_at": "lpLedger->txRead (existence check)" + } + ], + "description": "Implements the doTransactionEntry RPC handler, which retrieves transaction details from a specified ledger based on a transaction hash, returning transaction and metadata information in JSON format.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_hash validated by xrpld RPC framework, Json::Value, jss:: namespace", + "issue_pattern": "Missing validation for ledger_hash", + "why_false_positive": "xrpld RPC framework, Json::Value, jss:: namespace validates ledger_hash automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_index", + "validation", + "missing", + "check" + ], + "evidence": "Field ledger_index validated by xrpld RPC framework, Json::Value, jss:: namespace", + "issue_pattern": "Missing validation for ledger_index", + "why_false_positive": "xrpld RPC framework, Json::Value, jss:: namespace validates ledger_index automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "tx_hash", + "validation", + "missing", + "check" + ], + "evidence": "Field tx_hash validated by xrpld RPC framework, Json::Value, jss:: namespace", + "issue_pattern": "Missing validation for tx_hash", + "why_false_positive": "xrpld RPC framework, Json::Value, jss:: namespace validates tx_hash automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash / ledger_index", + "empty", + "string", + "validation" + ], + "evidence": "RPC::lookupLedger at doTransactionEntry (first lines)", + "issue_pattern": "Missing empty string validation for ledger_hash / ledger_index", + "why_false_positive": "RPC::lookupLedger validates ledger_hash / ledger_index for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tx_hash", + "empty", + "string", + "validation" + ], + "evidence": "isMember (Json::Value API) at doTransactionEntry (if (!context.params.isMember(jss::tx_hash)))", + "issue_pattern": "Missing empty string validation for tx_hash", + "why_false_positive": "isMember (Json::Value API) validates tx_hash for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "ledger_hash", + "empty", + "string", + "validation" + ], + "evidence": "jvResult.get(jss::ledger_hash, Json::nullValue).isNull() at doTransactionEntry (else if ...)", + "issue_pattern": "Missing empty string validation for ledger_hash", + "why_false_positive": "jvResult.get(jss::ledger_hash, Json::nullValue).isNull() validates ledger_hash for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tx_hash", + "empty", + "string", + "validation" + ], + "evidence": "uTransID.parseHex at doTransactionEntry (if (!uTransID.parseHex(...)))", + "issue_pattern": "Missing empty string validation for tx_hash", + "why_false_positive": "uTransID.parseHex validates tx_hash for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "tx_hash", + "format", + "validation", + "invalid" + ], + "evidence": "uTransID.parseHex at doTransactionEntry (if (!uTransID.parseHex(...)))", + "issue_pattern": "Missing format validation for tx_hash", + "why_false_positive": "uTransID.parseHex validates tx_hash format" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "tx_hash (as transaction existence)", + "empty", + "string", + "validation" + ], + "evidence": "lpLedger->txRead(uTransID) at doTransactionEntry (if (!sttx))", + "issue_pattern": "Missing empty string validation for tx_hash (as transaction existence)", + "why_false_positive": "lpLedger->txRead(uTransID) validates tx_hash (as transaction existence) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 14, + "name": "doTransactionEntry" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 8, + "name": "xrpl" + } + ], + "test_coverage_notes": "This handler is typically tested via integration/functional tests for the 'transaction_entry' RPC. Likely test files: 'rpc/TransactionEntry_test.cpp', 'rpc/Transaction_test.cpp', or broader RPC test suites. Validation paths (missing tx_hash, malformed tx_hash, missing/invalid ledger, transaction not found) should be covered. Gaps may exist in edge cases: malformed ledger_hash, simultaneous missing ledger_hash and ledger_index, or malformed tx_hash not as string. No evidence of unit tests for parseHex or error propagation in this file; relies on upstream validation. Template-based validation is not explicit here, so coverage of template errors may be lacking.", + "validation_architecture": { + "auto_validated_fields": [ + "ledger_hash", + "ledger_index", + "tx_hash" + ], + "framework": "xrpld RPC framework, Json::Value, jss:: namespace", + "validation_layer": "business_logic (handler function)" + }, + "validations": [ + { + "confidence": 0.9, + "error_thrown": "returns error in jvResult (various: ledgerNotFound, etc.)", + "field": "ledger_hash / ledger_index", + "location": "doTransactionEntry (first lines)", + "validated_by": "RPC::lookupLedger", + "validates": [ + "ledger_hash or ledger_index must be present and valid", + "ledger must exist and be accessible" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "jvResult[jss::error] = 'fieldNotFoundTransaction'", + "field": "tx_hash", + "location": "doTransactionEntry (if (!context.params.isMember(jss::tx_hash)))", + "validated_by": "isMember (Json::Value API)", + "validates": [ + "tx_hash field must be present in input" + ], + "validation_type": "presence" + }, + { + "confidence": 0.9, + "error_thrown": "jvResult[jss::error] = 'notYetImplemented'", + "field": "ledger_hash", + "location": "doTransactionEntry (else if ...)", + "validated_by": "jvResult.get(jss::ledger_hash, Json::nullValue).isNull()", + "validates": [ + "ledger_hash must be present (not null) in result" + ], + "validation_type": "presence" + }, + { + "confidence": 1.0, + "error_thrown": "jvResult[jss::error] = 'malformedRequest'", + "field": "tx_hash", + "location": "doTransactionEntry (if (!uTransID.parseHex(...)))", + "validated_by": "uTransID.parseHex", + "validates": [ + "tx_hash must be a valid hex string" + ], + "validation_type": "format" + }, + { + "confidence": 1.0, + "error_thrown": "jvResult[jss::error] = 'transactionNotFound'", + "field": "tx_hash (as transaction existence)", + "location": "doTransactionEntry (if (!sttx))", + "validated_by": "lpLedger->txRead(uTransID)", + "validates": [ + "transaction with given tx_hash must exist in ledger" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp.ai.md b/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp.ai.md new file mode 100644 index 0000000000..d62693874d --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/TransactionEntry.cpp.ai.md @@ -0,0 +1,41 @@ +# `TransactionEntry.cpp` — Ledger-Scoped Transaction Lookup Handler + +## Role in the System + +This file implements `doTransactionEntry`, the handler for the `transaction_entry` JSON-RPC command. Its purpose is narrowly defined: retrieve a single transaction and its metadata from a **specific, caller-identified ledger**. This distinguishes it from its sibling `Tx.cpp`, which accepts a raw hash and searches across all available ledger history. `transaction_entry` demands that the caller name the ledger explicitly via `ledger_hash` or `ledger_index` — the trade-off being precision (you know exactly which ledger produced this result) at the cost of requiring the caller to already know where to look. + +## Validation Chain + +The function implements a layered validation sequence before any ledger data is returned. Each stage guards the next: + +1. **Ledger resolution** — `RPC::lookupLedger` parses and validates `ledger_hash` / `ledger_index` from `context.params`, populates `lpLedger` (a `shared_ptr`), and seeds `jvResult` with ledger metadata or an error. If `lpLedger` remains null, the function returns the error immediately. + +2. **`tx_hash` presence** — `context.params.isMember(jss::tx_hash)` guards the field before any access. Missing the field yields `fieldNotFoundTransaction`. + +3. **Current ledger rejection** — the `else if` branch tests whether `jvResult` already contains a `ledger_hash` key. If `lookupLedger` produced a result with no `ledger_hash` (meaning it resolved to the open/current ledger), the handler returns `notYetImplemented`. The inline comments acknowledge this as a known limitation with a historical `XXX` annotation — the API has never supported querying the in-progress ledger via this command. + +4. **Hex parsing** — `uTransID.parseHex(...)` decodes the 64-character hex string into a `uint256`. Failure yields `malformedRequest` and returns immediately without hitting the ledger index. + +5. **Transaction existence** — `lpLedger->txRead(uTransID)` returns a pair of `(STTx shared_ptr, STObject shared_ptr)` via C++17 structured bindings. A null `sttx` means the transaction is not in that ledger, yielding `transactionNotFound`. + +This layered approach means errors are caught at the cheapest possible point: string checks before hash parsing, hash parsing before ledger I/O. + +## Response Shape and API Versioning + +The response diverges based on `context.apiVersion`: + +**API v1** serializes the transaction with `JsonOptions::none`, producing the legacy format where `Amount` is used for Payment amounts. The metadata key is `metadata`. + +**API v2+** changes three things. First, `sttx->getJson(JsonOptions::disable_API_prior_V2)` switches to the v2 serialization. Second, the response gains explicit `hash`, `ledger_hash`, `ledger_index`, `validated`, and `close_time_iso` fields at the top level. Third, the metadata key becomes `meta` (shorter). The `ledger_hash` field is only emitted for closed ledgers — `lpLedger->open()` is tested before calling `context.ledgerMaster.getHashBySeq()`. Close time is only present when the ledger is validated, obtained via `getCloseTimeBySeq`. + +`RPC::insertDeliverMax` is called for both API versions. For Payment transactions it copies (and in v2+ removes) the `Amount` field to `DeliverMax`, a field renaming introduced in API v2 to clarify that the amount is the maximum deliverable, not necessarily delivered. + +## Relationship to Sibling Files + +In the same `handlers/transaction/` directory, `Tx.cpp` handles the broader `tx` command. That handler uses `TransactionMaster`, searches both the ledger database and the transaction queue, supports CTID lookups, and has substantially more logic for assembling the response. `TransactionEntry.cpp` is the leaner, ledger-pinned alternative: it works only against a `ReadView` (a read-only ledger snapshot), never touches the transaction queue, and has no relational database dependency. This makes it appropriate for clients that have already identified the containing ledger and want a deterministic, re-verifiable lookup. + +## Design Notes + +The `stobj` (transaction metadata `STObject`) from `txRead` may legitimately be null for transactions in open ledgers, since metadata is only generated at ledger close. The code conditionally serializes it only when non-null, so there is no `metadata`/`meta` key in the response for unfinalized transactions — which is consistent with the current-ledger rejection above. + +The comment `// XXX Relying on trusted WSS client` on the `parseHex` call is an unresolved legacy note flagging that the hex parsing is lenient; there is no length or character-class check beyond what `parseHex` itself enforces. In practice `parseHex` will reject any non-hex or wrong-length string, so the concern has been functionally addressed even though the comment was never cleaned up. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/Tx.cpp.ai.json b/src/xrpld/rpc/handlers/transaction/Tx.cpp.ai.json new file mode 100644 index 0000000000..aab4bfc14b --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/Tx.cpp.ai.json @@ -0,0 +1,414 @@ +{ + "args": [ + { + "lineno": 38, + "name": "args" + } + ], + "classes": [ + { + "args": [], + "lineno": 23, + "name": "TxResult" + }, + { + "args": [], + "lineno": 34, + "name": "TxArgs" + } + ], + "code_paths": [ + { + "call_chain": [ + "doTxJson", + "doTxHelp", + "isValidated" + ], + "entry_point": "doTxJson", + "purpose": "Handles the main transaction lookup and response formatting for the RPC 'tx' endpoint.", + "validation_points": [ + "doTxHelp: ledgerRange validated by explicit if-checks", + "doTxHelp: ctid validated by context.app.getLedgerMaster().txnIdFromIndex", + "doTxHelp: hash validated by explicit if-check", + "doTxHelp: hash (in ledger range) validated by context.app.getMasterTransaction().fetch", + "isValidated: checks if ledger exists and hash matches" + ] + }, + { + "call_chain": [ + "doTxHelp", + "context.app.getLedgerMaster().txnIdFromIndex", + "context.app.getMasterTransaction().fetch", + "isValidated" + ], + "entry_point": "doTxHelp", + "purpose": "Performs the core transaction lookup, validation, and result population.", + "validation_points": [ + "doTxHelp: ledgerRange explicit checks", + "doTxHelp: ctid to hash conversion and validation", + "doTxHelp: hash presence check", + "doTxHelp: fetch validates hash in ledger range", + "isValidated: ledger existence and hash match" + ] + } + ], + "data_flows": [ + { + "field": "ledgerRange", + "flow": [ + "TxArgs.ledgerRange", + "doTxHelp: explicit if-checks for range validity", + "range variable for fetch", + "context.app.getMasterTransaction().fetch" + ], + "origin": "TxArgs (input from RPC request)", + "transformations": [ + "Checked for min/max order", + "Checked for excessive range", + "Converted to ClosedInterval" + ], + "validated_at": "doTxHelp (explicit if-checks)" + }, + { + "field": "ctid", + "flow": [ + "TxArgs.ctid", + "doTxHelp: context.app.getLedgerMaster().txnIdFromIndex", + "If valid, sets args.hash and range" + ], + "origin": "TxArgs (input from RPC request)", + "transformations": [ + "Converted to hash via txnIdFromIndex", + "If valid, sets hash and range" + ], + "validated_at": "doTxHelp (txnIdFromIndex result checked)" + }, + { + "field": "hash", + "flow": [ + "TxArgs.hash", + "doTxHelp: checked for presence", + "doTxHelp: used in context.app.getMasterTransaction().fetch", + "doTxHelp: result used for further processing" + ], + "origin": "TxArgs (input from RPC request, or derived from ctid)", + "transformations": [ + "May be set from ctid", + "Checked for presence", + "Used as key for fetch" + ], + "validated_at": "doTxHelp (explicit if-check for presence)" + }, + { + "field": "hash (in ledger range)", + "flow": [ + "TxArgs.hash", + "doTxHelp: context.app.getMasterTransaction().fetch", + "fetch returns TxPair or TxSearched" + ], + "origin": "TxArgs.hash", + "transformations": [ + "Checked for existence in specified ledger range" + ], + "validated_at": "doTxHelp (fetch result checked)" + }, + { + "field": "validated", + "flow": [ + "doTxHelp: isValidated called with ledger seq/hash", + "Result stored in TxResult.validated" + ], + "origin": "Result of isValidated", + "transformations": [ + "Boolean set based on ledger existence and hash match" + ], + "validated_at": "isValidated" + } + ], + "description": "Implements transaction lookup and response formatting for XRPL RPC, supporting queries by transaction hash or CTID, with optional binary output and ledger range filtering.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "ledgerRange", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-checks at doTxHelp", + "issue_pattern": "Missing empty string validation for ledgerRange", + "why_false_positive": "explicit if-checks validates ledgerRange for empty strings" + }, + { + "applies_to": [ + "validation", + "bounds_check" + ], + "confidence": 0.85, + "detection_keywords": [ + "ledgerRange", + "range", + "bounds", + "validation" + ], + "evidence": "explicit if-checks at doTxHelp", + "issue_pattern": "Missing range validation for ledgerRange", + "why_false_positive": "explicit if-checks validates ledgerRange range" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "ctid", + "empty", + "string", + "validation" + ], + "evidence": "context.app.getLedgerMaster().txnIdFromIndex at doTxHelp", + "issue_pattern": "Missing empty string validation for ctid", + "why_false_positive": "context.app.getLedgerMaster().txnIdFromIndex validates ctid for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hash", + "empty", + "string", + "validation" + ], + "evidence": "explicit if-check at doTxHelp", + "issue_pattern": "Missing empty string validation for hash", + "why_false_positive": "explicit if-check validates hash for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.9, + "detection_keywords": [ + "hash (in ledger range)", + "empty", + "string", + "validation" + ], + "evidence": "context.app.getMasterTransaction().fetch at doTxHelp", + "issue_pattern": "Missing empty string validation for hash (in ledger range)", + "why_false_positive": "context.app.getMasterTransaction().fetch validates hash (in ledger range) for empty strings" + }, + { + "applies_to": [ + "null_check", + "memory_safety" + ], + "confidence": 0.9, + "detection_keywords": [ + "null", + "nullptr", + "check", + "std::shared_ptr" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Missing null check for std::shared_ptr", + "why_false_positive": "RAII smart pointers guarantee initialization" + }, + { + "applies_to": [ + "memory_safety", + "resource_leak" + ], + "confidence": 0.85, + "detection_keywords": [ + "memory", + "leak", + "delete" + ], + "evidence": "Code uses std::shared_ptr", + "issue_pattern": "Memory leak - missing delete", + "why_false_positive": "Smart pointer handles cleanup automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/transaction/Tx.cpp", + "functions": [ + { + "args": [ + "LedgerMaster& ledgerMaster", + "std::uint32_t seq", + "uint256 const& hash" + ], + "lineno": 13, + "name": "isValidated" + }, + { + "args": [ + "RPC::Context& context", + "TxArgs args" + ], + "lineno": 49, + "name": "doTxHelp" + }, + { + "args": [ + "std::pair const& res", + "TxArgs const& args", + "RPC::JsonContext const& context" + ], + "lineno": 143, + "name": "populateJsonResponse" + }, + { + "args": [ + "RPC::JsonContext& context" + ], + "lineno": 232, + "name": "doTxJson" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [ + { + "audit_implication": "No manual delete needed, no null checks after construction", + "false_positive_risk": "Missing null check, memory leak", + "pointer_type": "std::shared_ptr", + "type": "smart_pointer" + } + ], + "smart_pointers": [ + { + "audit_note": "Reference counted, auto cleanup when last ref dropped", + "cleanup_needed": false, + "false_positive_risk": "Missing null check or manual delete", + "null_check_needed": false, + "ownership": "shared", + "type": "shared_ptr" + } + ], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The main validation logic is likely covered by integration and unit tests for the 'tx' RPC endpoint. Look for test files such as 'test/rpc/Tx_test.cpp', 'test/rpc/Transaction_test.cpp', or similar. These should cover valid/invalid ledger ranges, CTID lookups, hash lookups, and binary/non-binary responses. Gaps may exist in edge cases for CTID conversion, excessive ledger ranges, and malformed input. Exception handling paths (e.g., rpcDB_DESERIALIZATION) may not be fully covered unless explicitly tested for database corruption or deserialization errors.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld/rpc (jss::, RPC::Status), explicit C++ logic", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcINVALID_LGR_RANGE or rpcEXCESSIVE_LGR_RANGE", + "field": "ledgerRange", + "location": "doTxHelp", + "validated_by": "explicit if-checks", + "validates": [ + "ledgerRange->second >= ledgerRange->first", + "ledgerRange->second - ledgerRange->first <= MAX_RANGE (1000)" + ], + "validation_type": "range" + }, + { + "confidence": 0.8, + "error_thrown": "rpcTXN_NOT_FOUND", + "field": "ctid", + "location": "doTxHelp", + "validated_by": "context.app.getLedgerMaster().txnIdFromIndex", + "validates": [ + "ctid is mapped to a valid transaction hash" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcTXN_NOT_FOUND", + "field": "hash", + "location": "doTxHelp", + "validated_by": "explicit if-check", + "validates": [ + "hash is present (not nullopt) after ctid mapping" + ], + "validation_type": "type/presence" + }, + { + "confidence": 0.9, + "error_thrown": "rpcTXN_NOT_FOUND", + "field": "hash (in ledger range)", + "location": "doTxHelp", + "validated_by": "context.app.getMasterTransaction().fetch", + "validates": [ + "transaction with hash exists in specified ledger range" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/Tx.cpp.ai.md b/src/xrpld/rpc/handlers/transaction/Tx.cpp.ai.md new file mode 100644 index 0000000000..00650a6fa8 --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/Tx.cpp.ai.md @@ -0,0 +1,39 @@ +# `Tx.cpp` — The `tx` RPC Handler + +## Role in the System + +`Tx.cpp` implements `doTxJson`, the server-side handler for the XRPL `tx` RPC method. This is one of the most fundamental read APIs in the ledger — a client sends a transaction hash or Concise Transaction ID (CTID) and gets back the full transaction with its metadata and validation status. The file is deliberately self-contained: it houses the argument structures, the lookup logic, and the response-formatting code in roughly 320 lines, making the full lifecycle of the call easy to trace in one place. + +## Three-Phase Architecture + +The implementation separates concerns into three tightly scoped layers: + +**`doTxJson`** owns input parsing. It validates that exactly one of `transaction` (a hash) or `ctid` is supplied — both at once is rejected as `rpcINVALID_PARAMS` because the intent would be ambiguous. It decodes CTID strings via `RPC::decodeCTID`, performs the critical network ID check (a CTID encodes which network it was issued on, so submitting it to the wrong node produces a human-readable error rather than a silent hash lookup on a different chain), and builds a `TxArgs` value object. The optional `min_ledger`/`max_ledger` pair is extracted here too; the `asUInt()` calls sit inside a try/catch specifically because `Json::Value::asUInt` throws on type mismatch, and the design is to convert that exception into `rpcINVALID_LGR_RANGE` rather than let it bubble. + +**`doTxHelp`** owns lookup and validation. It checks the ledger range constraints (the range must be non-inverted and capped at 1000 ledgers — `MAX_RANGE` is a `constexpr` scoped to the function) before dispatching to `TransactionMaster::fetch`. If a CTID was supplied, the ledger sequence and transaction index are resolved to a hash via `LedgerMaster::txnIdFromIndex` before calling `fetch`. There are two `fetch` overloads: one that accepts a `ClosedInterval` for range-bounded searches, and one that searches the full local history. Both return `std::variant` — if the variant holds a `TxSearched` sentinel, the transaction was not found and that sentinel is forwarded into `TxResult` for the response layer to use. + +**`populateJsonResponse`** owns serialization. It must not touch the database or the ledger state — everything it needs is already in `TxResult`. + +## The `TxResult` Structure + +`TxResult` carries the raw search output across the phase boundary. Its `meta` field is typed as `std::variant, Blob>` — when binary mode is requested, `doTxHelp` calls `meta->getAsObject().getSerializer().getData()` up front and stores the raw bytes, avoiding any re-serialization in the response phase. The `validated` bool, `closeTime`, `ledgerHash`, and `ctid` are only populated when the transaction was found in a closed ledger and all the prerequisite data is available. + +## Validation Status and `isValidated` + +The file-local `isValidated` function is a three-guard check: the ledger must exist in the local store, its sequence must not exceed the current validated ledger's sequence, and its hash must match what the local store has for that sequence. This matters because a node can have a closed but not yet validated ledger in its store; returning `"validated": true` for such a transaction would be incorrect. The hash match is the final guard against rare cases where a ledger was reorganised. + +## CTID Round-Trip + +CTID support follows a decode-then-re-encode pattern. On input, `decodeCTID` parses a 16-hex-character string into `(ledgerSeq, txnIndex, networkID)` — the two-nibble `C` prefix in the high bits is a magic tag that distinguishes CTIDs from raw hashes. On output, `doTxHelp` attempts to re-encode a CTID from the found transaction's metadata fields. This re-encoding can fail if any component exceeds its bit budget (`txnIdx > 0xFFFF`, `netID >= 0xFFFF`, `lgrSeq >= 0x0FFF'FFFF`), in which case the `ctid` field is simply absent from the response rather than returning a truncated or invalid value. This is a deliberate bounds check spelled out inline rather than delegated to `encodeCTID`'s own guard. + +## API Version Branching + +`populateJsonResponse` has a clear branch on `context.apiVersion > 1`. In API v2+, the response structure is reorganised: the transaction goes under `tx_json` (or `tx_blob` for binary mode), `ledger_hash` and `hash` become top-level fields, and `close_time_iso` replaces the older date format. In API v1, the flat legacy layout from `Transaction::getJson` is used directly. The `JsonOptions::disable_API_prior_V2` flag signals to the serializer that deprecated legacy fields should be suppressed. This branching avoids a separate handler file for each API version while keeping the shape of each response correct. + +## Synthetic Metadata Insertions + +After the core metadata is serialised, three augmentation calls run on the `meta` JSON object: `insertDeliveredAmount` fills in `delivered_amount` for payment and check-cash transactions, `RPC::insertNFTSyntheticInJson` adds NFT-specific synthetic fields, and `RPC::insertMPTokenIssuanceID` injects the `mpt_issuance_id` for MPTokenIssuanceCreate transactions. These are post-processing concerns that do not belong in the generic serializer — placing them here keeps the protocol-specific enrichment co-located with the RPC handler that owns the response contract. + +## The `searched_all` Field and Partial Histories + +When a transaction is not found and a ledger range was specified, the response includes `"searched_all": true/false` derived from the `TxSearched` enum. `TxSearched::All` means every ledger in the requested range was present in the local database and the transaction is definitively absent. `TxSearched::Some` means the local store was incomplete — the client should not conclude the transaction never existed. `TxSearched::Unknown` (the default when no range was given or a deserialization error occurred) suppresses the field entirely, preserving backward compatibility with the pre-range-query response shape. This design lets clients distinguish genuine non-existence from a partial-history node without requiring a separate API call. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/TxHistory.cpp.ai.json b/src/xrpld/rpc/handlers/transaction/TxHistory.cpp.ai.json new file mode 100644 index 0000000000..3e12e9b663 --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/TxHistory.cpp.ai.json @@ -0,0 +1,316 @@ +{ + "args": [ + { + "lineno": 14, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doTxHistory" + ], + "entry_point": "doTxHistory", + "purpose": "Handles the 'tx_history' RPC request, validates input, fetches transaction history, and returns it as JSON.", + "validation_points": [ + "if (!context.app.config().useTxTables()) return rpcError(rpcNOT_ENABLED);", + "if (!context.params.isMember(jss::start)) return rpcError(rpcINVALID_PARAMS);", + "unsigned int const startIndex = context.params[jss::start].asUInt();", + "if ((startIndex > 10000) && (!isUnlimited(context.role))) return rpcError(rpcNO_PERMISSION);" + ] + } + ], + "data_flows": [ + { + "field": "useTxTables (feature flag)", + "flow": [ + "context.app.config().useTxTables()", + "if (!...) validation", + "early return if not enabled" + ], + "origin": "context.app.config().useTxTables()", + "transformations": [ + "Boolean check" + ], + "validated_at": "First line of doTxHistory" + }, + { + "field": "start (request parameter)", + "flow": [ + "context.params (JSON input)", + "context.params.isMember(jss::start) validation", + "context.params[jss::start].asUInt() conversion", + "startIndex variable", + "if ((startIndex > 10000) && (!isUnlimited(context.role))) validation", + "used as argument to getTxHistory(startIndex)", + "returned as obj[jss::index]" + ], + "origin": "context.params[jss::start]", + "transformations": [ + "Presence check (isMember)", + "Type conversion (asUInt)", + "Range/permission check (startIndex > 10000 && !isUnlimited)", + "Used as DB query parameter" + ], + "validated_at": [ + "isMember(jss::start) check", + "asUInt() conversion", + "startIndex > 10000 && !isUnlimited(context.role)" + ] + }, + { + "field": "role (user permission)", + "flow": [ + "context.role", + "isUnlimited(context.role) check" + ], + "origin": "context.role", + "transformations": [ + "Permission check" + ], + "validated_at": "startIndex > 10000 && !isUnlimited(context.role)" + } + ], + "description": "Implements the doTxHistory RPC handler, which retrieves a paginated list of transaction history entries from the relational database, starting from a specified index.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "start (presence via isMember)", + "validation", + "missing", + "check" + ], + "evidence": "Field start (presence via isMember) validated by jss:: (JSON field tags), Json::Value (JSON parsing), rpcError (error reporting)", + "issue_pattern": "Missing validation for start (presence via isMember)", + "why_false_positive": "jss:: (JSON field tags), Json::Value (JSON parsing), rpcError (error reporting) validates start (presence via isMember) automatically" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.9, + "detection_keywords": [ + "start (type via asUInt, but not strictly enforced)", + "validation", + "missing", + "check" + ], + "evidence": "Field start (type via asUInt, but not strictly enforced) validated by jss:: (JSON field tags), Json::Value (JSON parsing), rpcError (error reporting)", + "issue_pattern": "Missing validation for start (type via asUInt, but not strictly enforced)", + "why_false_positive": "jss:: (JSON field tags), Json::Value (JSON parsing), rpcError (error reporting) validates start (type via asUInt, but not strictly enforced) automatically" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "useTxTables (feature flag)", + "empty", + "string", + "validation" + ], + "evidence": "context.app.config().useTxTables() at doTxHistory (first if statement)", + "issue_pattern": "Missing empty string validation for useTxTables (feature flag)", + "why_false_positive": "context.app.config().useTxTables() validates useTxTables (feature flag) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "start (request parameter)", + "empty", + "string", + "validation" + ], + "evidence": "context.params.isMember(jss::start) at doTxHistory (second if statement)", + "issue_pattern": "Missing empty string validation for start (request parameter)", + "why_false_positive": "context.params.isMember(jss::start) validates start (request parameter) for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 0.8, + "detection_keywords": [ + "start (request parameter)", + "empty", + "string", + "validation" + ], + "evidence": "context.params[jss::start].asUInt() at doTxHistory (assignment to startIndex)", + "issue_pattern": "Missing empty string validation for start (request parameter)", + "why_false_positive": "context.params[jss::start].asUInt() validates start (request parameter) for empty strings" + }, + { + "applies_to": [ + "validation", + "type_safety" + ], + "confidence": 0.85, + "detection_keywords": [ + "start (request parameter)", + "type", + "validation", + "check" + ], + "evidence": "context.params[jss::start].asUInt() at doTxHistory (assignment to startIndex)", + "issue_pattern": "Missing type validation for start (request parameter)", + "why_false_positive": "context.params[jss::start].asUInt() validates start (request parameter) type" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "start (request parameter)", + "empty", + "string", + "validation" + ], + "evidence": "(startIndex > 10000) && (!isUnlimited(context.role)) at doTxHistory (third if statement)", + "issue_pattern": "Missing empty string validation for start (request parameter)", + "why_false_positive": "(startIndex > 10000) && (!isUnlimited(context.role)) validates start (request parameter) for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/transaction/TxHistory.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 13, + "name": "doTxHistory" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 11, + "name": "xrpl" + } + ], + "test_coverage_notes": "The function is likely tested via RPC integration tests that exercise the 'tx_history' endpoint. Tests should cover: (1) feature flag off (rpcNOT_ENABLED), (2) missing 'start' param (rpcINVALID_PARAMS), (3) 'start' param > 10000 for non-admin (rpcNO_PERMISSION), (4) valid requests. Gaps may include: (a) edge cases for 'start' param (e.g., negative, non-integer, very large values), (b) malformed JSON input, (c) permission escalation attempts. Test files may include: 'rpc_tx_history_test.cpp', 'TxHistory_test.cpp', or generic RPC handler test suites. Template-based validation is not explicitly shown in this handler, so coverage of template validation may be indirect.", + "validation_architecture": { + "auto_validated_fields": [ + "start (presence via isMember)", + "start (type via asUInt, but not strictly enforced)" + ], + "framework": "jss:: (JSON field tags), Json::Value (JSON parsing), rpcError (error reporting)", + "validation_layer": "entry_point (RPC handler)" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNOT_ENABLED)", + "field": "useTxTables (feature flag)", + "location": "doTxHistory (first if statement)", + "validated_by": "context.app.config().useTxTables()", + "validates": [ + "Checks if transaction history tables are enabled in config" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcINVALID_PARAMS)", + "field": "start (request parameter)", + "location": "doTxHistory (second if statement)", + "validated_by": "context.params.isMember(jss::start)", + "validates": [ + "Checks if 'start' field is present in input JSON" + ], + "validation_type": "presence" + }, + { + "confidence": 0.8, + "error_thrown": "implicit: if not convertible, throws Json::Value exception or returns 0", + "field": "start (request parameter)", + "location": "doTxHistory (assignment to startIndex)", + "validated_by": "context.params[jss::start].asUInt()", + "validates": [ + "Checks if 'start' can be interpreted as unsigned int" + ], + "validation_type": "type" + }, + { + "confidence": 1.0, + "error_thrown": "rpcError(rpcNO_PERMISSION)", + "field": "start (request parameter)", + "location": "doTxHistory (third if statement)", + "validated_by": "(startIndex > 10000) && (!isUnlimited(context.role))", + "validates": [ + "Checks if 'start' is greater than 10000", + "If so, checks if user has unlimited role" + ], + "validation_type": "range|business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/TxHistory.cpp.ai.md b/src/xrpld/rpc/handlers/transaction/TxHistory.cpp.ai.md new file mode 100644 index 0000000000..819abf83a3 --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/TxHistory.cpp.ai.md @@ -0,0 +1,29 @@ +# `TxHistory.cpp` — Paginated Transaction History RPC Handler + +This file implements the `tx_history` RPC command (`doTxHistory`), a single-function handler that exposes a simple paginated interface for browsing the server's local transaction history stored in its relational database. It acts as a thin bridge between the RPC dispatch layer and the `RelationalDatabase` abstraction, letting clients step through historical transactions by offset rather than by ledger hash or transaction ID. + +## Role in the System + +Unlike sibling handlers such as `Tx.cpp` or `TransactionEntry.cpp` — which look up specific transactions by identifier — `doTxHistory` serves bulk browsing use cases. It returns a sequential slice of the transaction log starting at a caller-supplied offset, making it most useful for debugging, monitoring, or bulk export scenarios rather than precise lookups. The `RelationalDatabase::getTxHistory()` interface specifies that it returns the 20 most recent transactions starting from `startIndex`, sorted in descending ledger sequence order; the handler itself imposes no additional page-size logic and trusts the database layer to enforce that bound. + +The endpoint is gated immediately by `context.app.config().useTxTables()`. Transaction tables are an optional feature: nodes optimized purely for consensus can run without them. Returning `rpcNOT_ENABLED` here rather than an empty result or a runtime error is the correct signal — it tells callers definitively that the capability is absent, not that no history exists. + +## Access Control and Resource Accounting + +Two distinct safeguards govern who can call this endpoint and how heavily: + +**Resource classification.** The handler sets `context.loadType = Resource::feeMediumBurdenRPC` before doing any real work. This label feeds into the connection-level resource manager, which uses it for throttling decisions. Classifying `tx_history` as medium-burden reflects the fact that it issues a database scan rather than a point lookup, but it isn't as expensive as operations that touch consensus state or produce large cryptographic proofs. + +**Deep-pagination cap.** Any `start` index greater than 10,000 is rejected with `rpcNO_PERMISSION` unless `isUnlimited(context.role)` returns true. The `Role` enum distinguishes `ADMIN` and `IDENTIFIED` (both unlimited) from `USER` and `GUEST` (limited). This cap is a pragmatic defense: scanning tens or hundreds of thousands of historical transactions is expensive for the backing SQLite database, and there is no legitimate reason an unprivileged client needs to page that deep. Privileged callers — internal tooling, administrative scripts — can bypass the cap cleanly without requiring a separate endpoint or a special parameter flag. + +## Data Flow + +Input validation proceeds in a strict sequence: feature-flag check → presence check for `start` → unsigned integer conversion → range/permission check. The `asUInt()` call on the JSON field is not strictly safe against malformed input (a non-integer value produces an implicit `0` rather than a type error), but this is consistent with how other XRPL RPC handlers treat loosely-typed JSON integers, and a `start` of `0` is a valid and harmless request. + +After validation, `getRelationalDatabase().getTxHistory(startIndex)` performs the actual retrieval, returning a `std::vector>`. The handler then iterates this vector, calling `t->getJson(JsonOptions::none)` on each entry and appending the result to the `txs` JSON array. The response also echoes the requested `index` value, giving clients a stable way to track their pagination position across calls. + +The one non-trivial post-processing step is the call to `RPC::insertDeliverMax(tx_json, txnType, apiVersion)` for each transaction. This injects the `DeliverMax` field into Payment transaction JSON and, for API versions greater than 1, removes the legacy `Amount` field entirely. The same pattern appears in `Tx.cpp` and `TransactionEntry.cpp` — it is a cross-cutting compatibility shim that must be applied uniformly wherever raw transaction JSON surfaces to callers, because clients using newer API versions must not see the old field name. + +## Design Observations + +The handler is intentionally minimal. It performs no filtering by account or transaction type, no metadata enrichment beyond `DeliverMax`, and no ledger validation. This is appropriate: `tx_history` is a raw log browser, not a query engine. More sophisticated filtering belongs in higher-level tooling built on top of this primitive. The consequence is that the function's correctness guarantees are equally simple — as long as the config check, parameter validation, and role check pass, the result is exactly what `getTxHistory` returns, formatted as JSON. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp.ai.json b/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp.ai.json new file mode 100644 index 0000000000..2347992fb0 --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp.ai.json @@ -0,0 +1,83 @@ +{ + "args": [ + { + "lineno": 8, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doTxReduceRelay", + "context.app.getOverlay()", + "Overlay::txMetrics()" + ], + "entry_point": "doTxReduceRelay", + "purpose": "Handles the 'tx_reduce_relay' RPC command, returning transaction relay metrics from the overlay subsystem.", + "validation_points": [] + } + ], + "data_flows": [ + { + "field": "context", + "flow": [ + "RPC request arrives", + "RPC framework constructs JsonContext", + "doTxReduceRelay receives context", + "context.app.getOverlay()", + "Overlay::txMetrics()" + ], + "origin": "RPC::JsonContext& context (function parameter, populated by RPC framework from incoming HTTP/JSON-RPC request)", + "transformations": [ + "No transformation; context is passed directly to app.getOverlay()" + ], + "validated_at": "No explicit validation in this function" + }, + { + "field": "txMetrics() result", + "flow": [ + "Overlay::txMetrics()", + "doTxReduceRelay returns value" + ], + "origin": "Overlay::txMetrics() (returns Json::Value)", + "transformations": [ + "No transformation; value is returned as-is" + ], + "validated_at": "Assumed to be validated within Overlay::txMetrics(), but not in this function" + } + ], + "description": "Implements the doTxReduceRelay RPC handler, which returns transaction metrics from the application's overlay.", + "false_positive_patterns": [], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 7, + "name": "doTxReduceRelay" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 5, + "name": "xrpl" + } + ], + "test_coverage_notes": "There is no validation or input processing in doTxReduceRelay; it simply returns internal metrics. Test coverage would likely be in integration or RPC handler tests, possibly in files like 'rpc/handlers/TxReduceRelay_test.cpp' or broader RPC handler test suites. However, since there is no input validation or transformation, coverage is limited to ensuring the handler returns the expected metrics structure. There are no explicit validation code paths to test here. Gaps: No field-level or input validation is present or needed in this handler.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None detected in this file", + "validation_layer": "N/A" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp.ai.md b/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp.ai.md new file mode 100644 index 0000000000..46528e87a6 --- /dev/null +++ b/src/xrpld/rpc/handlers/transaction/TxReduceRelay.cpp.ai.md @@ -0,0 +1,11 @@ +# `TxReduceRelay.cpp` + +This file is the RPC handler for the `tx_reduce_relay` command — a read-only introspection endpoint that surfaces transaction relay performance metrics from the node's peer overlay network. + +The entire implementation is a single line: `doTxReduceRelay` accepts the standard `RPC::JsonContext` and immediately delegates to `context.app.getOverlay().txMetrics()`. There is no input parsing, no parameter validation, and no transformation of the result — the `Json::Value` produced by `Overlay::txMetrics()` is returned directly to the caller. + +The handler is registered in `Handler.cpp` under the `"tx_reduce_relay"` command name with `Role::USER` access and `NO_CONDITION`, meaning any connected client can query it without special privileges or an active network connection requirement. + +The data it exposes comes from `metrics::TxMetrics` (declared in `overlay/detail/TxMetrics.h`), a struct maintained by `OverlayImpl` that tracks rolling-average statistics for the reduce-relay subsystem: bytes and message counts per second for `TMTransaction`, `TMHaveTransactions`, `TMGetLedger`, `TMLedgerData`, and `TMTransactions` protocol messages; per-transaction sample averages for selected, suppressed, and feature-disabled peers; and a rate of missing transaction requests. These counters are populated deep in `OverlayImpl`'s transaction forwarding logic, where the relay algorithm decides which subset of peers to send each transaction to rather than flooding all connected peers unconditionally. + +The deliberate minimalism here is by design. The handler's only job is to provide a transparent window into the overlay's internal accounting without owning any of that state itself. All thread-safety and data correctness concerns live in `TxMetrics` (which holds its own `mutex`) and `OverlayImpl`. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/utility/Ping.cpp.ai.json b/src/xrpld/rpc/handlers/utility/Ping.cpp.ai.json new file mode 100644 index 0000000000..8a5df1b68f --- /dev/null +++ b/src/xrpld/rpc/handlers/utility/Ping.cpp.ai.json @@ -0,0 +1,222 @@ +{ + "args": [ + { + "lineno": 13, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "doPing" + ], + "entry_point": "doPing", + "purpose": "Handles the 'ping' RPC command, returning role and connection info for the current session.", + "validation_points": [ + "Manual check: if (!context.headers.forwardedFor.empty())", + "Manual check: if (context.infoSub)" + ] + } + ], + "data_flows": [ + { + "field": "context.headers.forwardedFor", + "flow": [ + "HTTP/WebSocket request header", + "context.headers.forwardedFor", + "doPing", + "ret[jss::ip]" + ], + "origin": "Set by upstream HTTP/WebSocket layer, populated from incoming request headers.", + "transformations": [ + "Checked for non-emptiness before use", + "Copied as std::string to JSON output" + ], + "validated_at": "doPing: if (!context.headers.forwardedFor.empty())" + }, + { + "field": "context.infoSub", + "flow": [ + "Session creation", + "context.infoSub", + "doPing", + "context.infoSub->getConsumer().isUnlimited()", + "ret[jss::unlimited]" + ], + "origin": "Set by session management code for WebSocket clients; nullptr for HTTP.", + "transformations": [ + "Pointer existence checked before dereference" + ], + "validated_at": "doPing: if (context.infoSub)" + }, + { + "field": "context.headers.user", + "flow": [ + "Authentication layer", + "context.headers.user", + "doPing", + "ret[jss::username]" + ], + "origin": "Set by authentication middleware, populated from request context.", + "transformations": [ + "Copied as std::string to JSON output" + ], + "validated_at": "No explicit validation in doPing; assumed validated upstream" + }, + { + "field": "context.role", + "flow": [ + "Authentication/authorization", + "context.role", + "doPing", + "switch (context.role)", + "ret[jss::role]" + ], + "origin": "Set by authentication/authorization logic based on credentials.", + "transformations": [ + "Switch statement determines output fields" + ], + "validated_at": "No explicit validation in doPing; assumed validated upstream" + } + ], + "description": "Implements the doPing RPC handler for the XRPL server, returning role and connection metadata in a JSON response.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "context.headers.forwardedFor", + "empty", + "string", + "validation" + ], + "evidence": "manual check (!empty()) at doPing", + "issue_pattern": "Missing empty string validation for context.headers.forwardedFor", + "why_false_positive": "manual check (!empty()) validates context.headers.forwardedFor for empty strings" + }, + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "context.infoSub", + "empty", + "string", + "validation" + ], + "evidence": "manual check (pointer existence) at doPing", + "issue_pattern": "Missing empty string validation for context.infoSub", + "why_false_positive": "manual check (pointer existence) validates context.infoSub for empty strings" + }, + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/utility/Ping.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 13, + "name": "doPing" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + }, + { + "lineno": 8, + "name": "RPC" + } + ], + "test_coverage_notes": "Ping handlers are typically covered by integration tests for the RPC API (e.g., test_rpc_ping, test_ws_ping). These tests likely check output for various roles and connection types. However, edge cases such as missing or malformed 'forwardedFor', null 'infoSub', or unusual user fields may not be exhaustively tested. There is no evidence of unit tests specifically targeting the manual validation logic (e.g., empty forwardedFor, null infoSub). Template-based validation is not used in this handler; all validation is manual and local.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "xrpld/rpc (jss:: for JSON field names, not validation)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "none (field is simply omitted if empty)", + "field": "context.headers.forwardedFor", + "location": "doPing", + "validated_by": "manual check (!empty())", + "validates": [ + "field is not empty before use" + ], + "validation_type": "business_logic" + }, + { + "confidence": 1.0, + "error_thrown": "none (block skipped if null)", + "field": "context.infoSub", + "location": "doPing", + "validated_by": "manual check (pointer existence)", + "validates": [ + "object exists before dereferencing" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/utility/Ping.cpp.ai.md b/src/xrpld/rpc/handlers/utility/Ping.cpp.ai.md new file mode 100644 index 0000000000..7994d047c6 --- /dev/null +++ b/src/xrpld/rpc/handlers/utility/Ping.cpp.ai.md @@ -0,0 +1,28 @@ +# `Ping.cpp` — RPC Ping Handler + +`Ping.cpp` implements `doPing`, the handler for the XRPL `ping` RPC command. The function lives under `src/xrpld/rpc/handlers/utility/` alongside `Random.cpp`, in a subdirectory reserved for stateless utility handlers that require no ledger access and produce no side effects. + +## Purpose + +The `ping` command exists not as a simple liveness check but as a session introspection tool. A client calling `ping` learns what privilege level the server has assigned them, who the server thinks they are, and whether their connection is exempt from rate limiting. This makes it valuable for operators and API proxies to verify their authentication setup and connection classification without issuing a real ledger query. + +## Role-Conditional Response + +The handler switches on `context.role`, an enum defined in `Role.h` with values `GUEST`, `USER`, `IDENTIFIED`, `ADMIN`, `PROXY`, and `FORBID`. The response is sparse by design: + +- **`ADMIN`**: adds `"role": "admin"`. Admin clients have unrestricted access to all RPC commands. +- **`IDENTIFIED`**: adds `"role": "identified"`, `"username"` from `context.headers.user`, and conditionally `"ip"` from `context.headers.forwardedFor`. This role is assigned by a `secure_gateway` proxy that has authenticated a downstream user — the username is passed via HTTP header. +- **`PROXY`**: adds `"role": "proxied"` and conditionally `"ip"`. Proxy clients get a forwarded-IP hint but no username identity. +- **`GUEST` / `USER` / `FORBID`**: fall through the `default` branch, producing an empty JSON object. These callers learn nothing about their classification beyond the fact that the call succeeded. + +The `forwardedFor` field is only written when non-empty, avoiding a spurious `"ip": ""` key for direct connections. This is a quiet defensive pattern rather than a strict validation — the field is already trusted from the upstream HTTP/WebSocket layer. + +## WebSocket-Only `unlimited` Field + +The `context.infoSub` pointer is non-null only for WebSocket sessions; HTTP requests leave it null. When present, the handler checks `infoSub->getConsumer().isUnlimited()` and sets `"unlimited": true` if the consumer is exempt from resource throttling. The null check is the only guard — there is no equivalent field for HTTP callers, so the key simply never appears in HTTP responses. + +## Context Dependencies + +`doPing` takes a `RPC::JsonContext` (defined in `Context.h`), which extends the base `Context` struct with a `Headers` inner struct carrying `user` and `forwardedFor` as `std::string_view`s. These views are populated by the HTTP/WebSocket dispatch layer before any handler runs, so `doPing` never needs to parse raw headers itself. The `role` field on the base `Context` is resolved by `requestRole()` (in `Role.h`) prior to dispatch, keeping authorization logic entirely outside the handler. + +The use of `jss::` namespace constants (`jss::role`, `jss::username`, `jss::ip`, `jss::unlimited`) rather than raw string literals ensures that JSON key names are consistent across the entire codebase and subject to a single point of change. \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/utility/Random.cpp.ai.json b/src/xrpld/rpc/handlers/utility/Random.cpp.ai.json new file mode 100644 index 0000000000..ed2cada5b6 --- /dev/null +++ b/src/xrpld/rpc/handlers/utility/Random.cpp.ai.json @@ -0,0 +1,136 @@ +{ + "args": [ + { + "lineno": 17, + "name": "context" + } + ], + "classes": [], + "code_paths": [ + { + "call_chain": [ + "RPC::Handler dispatch (not shown here, but typically: RPC::doCommand or similar)", + "doRandom" + ], + "entry_point": "doRandom", + "purpose": "Handles the 'random' RPC command, generating a cryptographically secure random uint256 and returning it in JSON.", + "validation_points": [ + "No explicit input validation in doRandom; all logic is internal." + ] + } + ], + "data_flows": [ + { + "field": "random", + "flow": [ + "doRandom: uint256 rand is declared", + "beast::rngfill(rand.begin(), rand.size(), crypto_prng()) fills rand with random bytes", + "to_string(rand) converts rand to string", + "jvResult[jss::random] = to_string(rand) inserts into JSON result", + "return jvResult returns JSON to caller" + ], + "origin": "Generated internally using beast::rngfill and crypto_prng()", + "transformations": [ + "rand is filled with random bytes", + "rand is converted to string for JSON" + ], + "validated_at": "No validation required; all data is generated internally and not user-supplied" + } + ], + "description": "Implements the doRandom RPC handler, which returns a random 256-bit value in JSON format.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "input_validation" + ], + "confidence": 0.85, + "detection_keywords": [ + "jss", + "validation", + "missing" + ], + "evidence": "Uses jss:: namespace accessor", + "issue_pattern": "Missing validation for jss:: field", + "why_false_positive": "JSON String constants - indicates template validation" + }, + { + "applies_to": [ + "validation", + "field_access" + ], + "confidence": 0.83, + "detection_keywords": [ + "jss", + "unvalidated", + "field" + ], + "evidence": "Framework validates all jss:: fields", + "issue_pattern": "Unvalidated field access via jss::", + "why_false_positive": "jss:: namespace indicates framework-validated fields" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/handlers/utility/Random.cpp", + "functions": [ + { + "args": [ + "context" + ], + "lineno": 16, + "name": "doRandom" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [ + { + "audit_implication": "Exception handling present", + "context": "Test code may use try-catch as assertion", + "pattern": "try-catch block", + "type": "exception_handling" + }, + { + "audit_implication": "Uses std::exception for error reporting", + "exception_type": "std::exception", + "type": "exception_type" + } + ], + "namespace_accessors": [ + { + "audit_implication": "Fields accessed via this namespace are framework-validated", + "false_positive_risk": "Missing validation for framework-managed fields", + "meaning": "JSON String constants - indicates template validation", + "namespace": "jss" + } + ], + "raii_usage": [], + "smart_pointers": [], + "template_validation": [ + { + "audit_implication": "Field guaranteed valid by framework", + "false_positive_risk": "Missing field validation", + "namespace": "jss", + "pattern": "jss::FieldName", + "type": "namespace_accessor", + "validates": "Field access implies template validation" + } + ] + }, + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + }, + { + "lineno": 9, + "name": "RPC" + } + ], + "test_coverage_notes": "This handler does not process user input, so there are no validation code paths to test. Test coverage would focus on: (1) Ensuring the 'random' field is present and well-formed in the response; (2) Ensuring error handling (e.g., exception path) is covered. Likely tested in integration or RPC handler tests (e.g., rpc/random_test.cpp or similar), but code coverage for the exception path is likely missing (as indicated by LCOV_EXCL_LINE). No template-based validation is exercised here, as there are no inputs to validate.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (no explicit validation framework used in this file)", + "validation_layer": "business_logic" + }, + "validations": [] +} \ No newline at end of file diff --git a/src/xrpld/rpc/handlers/utility/Random.cpp.ai.md b/src/xrpld/rpc/handlers/utility/Random.cpp.ai.md new file mode 100644 index 0000000000..e36f7733d5 --- /dev/null +++ b/src/xrpld/rpc/handlers/utility/Random.cpp.ai.md @@ -0,0 +1,38 @@ +# `Random.cpp` — RPC Handler for Cryptographic Random Value Generation + +This file implements `doRandom`, the handler for the `random` RPC command. Its sole responsibility is to draw 256 bits of entropy from the node's cryptographically secure PRNG and return the value to the caller as a hex-encoded JSON string. The handler requires no ledger state and accepts no user input, making it one of the simplest endpoints in the RPC surface while still serving a meaningful purpose: giving clients a convenient way to obtain trusted randomness sourced directly from a live XRPL node. + +## Role in the RPC Framework + +`doRandom` is declared in `Handlers.h` alongside all other RPC handler functions and is registered in `src/xrpld/rpc/detail/Handler.cpp` as: + +```cpp +{"random", byRef(&doRandom), Role::USER, NO_CONDITION} +``` + +The `Role::USER` designation means any connected client — not just admins — can invoke it. `NO_CONDITION` means no particular ledger state (open, closed, validated) is required before dispatching: the handler runs unconditionally, which makes sense because it never touches ledger data at all. + +## Entropy Source: `crypto_prng()` and `beast::rngfill` + +The core of the handler is a single call: + +```cpp +uint256 rand; +beast::rngfill(rand.begin(), rand.size(), crypto_prng()); +``` + +`crypto_prng()` returns a reference to the process-wide singleton `csprng_engine`. That engine is mutex-protected and meets the C++ `UniformRandomNumberEngine` named requirement, so it integrates cleanly with standard library utilities. Its constructor seeds from `std::random_device` and it supports explicit entropy injection via `mix_entropy()`. The engine is deliberately non-copyable and non-movable — there is exactly one CSPRNG for the process, accessed only through the global accessor. + +`beast::rngfill` takes an iterator range and a generator and fills the byte range by repeatedly calling `operator()` on the engine. Because `csprng_engine` also exposes a bulk `operator()(void*, size_t)` overload, efficient implementations of `rngfill` can bypass the 64-bit-at-a-time loop, but the interface stays the same either way. + +The result is stored in a `uint256`, XRPL's fixed-size 32-byte integer type, which is then serialized to a 64-character lowercase hex string via `to_string()` and placed under the `jss::random` key in the response JSON. + +## Exception Handling + +The body is wrapped in a `try/catch(std::exception const&)` that returns `rpcError(rpcINTERNAL)` on failure, marked `LCOV_EXCL_LINE` because it is never expected to execute. The inline comment acknowledges the redundancy directly: a top-level catch already exists in the RPC dispatch layer that would handle any propagated exception. The local catch is a legacy defensive pattern that has not been removed, likely because the cost of keeping it is zero and removing it requires auditing whether the top-level handler actually covers this code path everywhere `doRandom` might be called. + +## Design Observations + +There is a deliberate asymmetry between this handler and most others: it receives a `RPC::JsonContext&` but ignores it entirely. No fields are read from the request, no role or permission checks beyond the dispatch-table registration are needed, and no ledger objects are touched. This makes the handler stateless from the perspective of XRPL data — it is essentially a thin RPC wrapper around a syscall-backed entropy pool. + +The choice to expose node-generated randomness over RPC is useful for clients building applications where they want an external entropy source they did not generate themselves (e.g., for combined randomness schemes). The trust model is straightforward: the client trusts the node operator's CSPRNG seeding, which relies on `std::random_device` and any additional `mix_entropy` calls the node makes during its lifecycle. \ No newline at end of file diff --git a/src/xrpld/rpc/json_body.h.ai.json b/src/xrpld/rpc/json_body.h.ai.json new file mode 100644 index 0000000000..b8ce10c3dc --- /dev/null +++ b/src/xrpld/rpc/json_body.h.ai.json @@ -0,0 +1,86 @@ +{ + "args": [], + "classes": [ + { + "args": [], + "lineno": 9, + "name": "json_body" + }, + { + "args": [ + "boost::beast::http::message const& m" + ], + "lineno": 14, + "name": "json_body::reader" + }, + { + "args": [ + "boost::beast::http::header const& fields", + "value_type const& value" + ], + "lineno": 48, + "name": "json_body::writer" + } + ], + "description": "Defines a json_body struct for use as a custom body type in Boost.Beast HTTP messages, enabling serialization and deserialization of JSON payloads using Json::Value.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/rpc/json_body.h", + "functions": [ + { + "args": [ + "boost::beast::http::message const& m" + ], + "lineno": 22, + "name": "json_body::reader::reader" + }, + { + "args": [ + "boost::beast::error_code&" + ], + "lineno": 31, + "name": "json_body::reader::init" + }, + { + "args": [ + "boost::beast::error_code& ec" + ], + "lineno": 37, + "name": "json_body::reader::get" + }, + { + "args": [ + "boost::beast::error_code&" + ], + "lineno": 44, + "name": "json_body::reader::finish" + }, + { + "args": [ + "boost::beast::http::header const& fields", + "value_type const& value" + ], + "lineno": 52, + "name": "json_body::writer::writer" + }, + { + "args": [ + "boost::beast::error_code& ec" + ], + "lineno": 58, + "name": "json_body::writer::init" + }, + { + "args": [ + "boost::beast::error_code& ec" + ], + "lineno": 64, + "name": "json_body::writer::get" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 7, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/rpc/json_body.h.ai.md b/src/xrpld/rpc/json_body.h.ai.md new file mode 100644 index 0000000000..c883cd4912 --- /dev/null +++ b/src/xrpld/rpc/json_body.h.ai.md @@ -0,0 +1,39 @@ +# `json_body.h` — Custom Boost.Beast HTTP Body Type for JSON + +`json_body.h` defines a custom HTTP body type for use with Boost.Beast's typed HTTP message framework. It serves as the glue layer between XRPL's `Json::Value` representation and the byte-level serialization machinery that Beast uses when building and sending HTTP responses. This file is consumed in two parts of the rippled codebase: `OverlayImpl.cpp` (for the overlay's diagnostic HTTP endpoints — `/crawl`, `/health`, `/vl/`) and `ServerHandler.cpp` (for the RPC dispatch layer). + +## Role in Beast's Body Concept + +Boost.Beast's HTTP library is parameterized on body type: `http::message`. Any type used as `Body` must declare `value_type` (the in-memory representation), and optionally provide nested `reader` and `writer` types that handle serialization. In `json_body`, `value_type` is `Json::Value`, and both `reader` and `writer` implement the Beast BodyReader concept — that is, both serialize a `Json::Value` body into wire bytes. The comments in both `get()` methods explicitly call out the BodyReader requirement, including the non-obvious detail that `get()` must return `boost::optional` rather than `std::optional` to satisfy Beast's older concept interface. + +The typical usage pattern in the overlay layer is: +```cpp +boost::beast::http::response msg; +msg.body()["key"] = Json::Value(...); +msg.prepare_payload(); +return std::make_shared(msg); +``` + +`SimpleWriter` then serializes the entire message (headers + body) using `boost::beast::ostream`, which internally invokes Beast's serializer and therefore calls into `json_body::reader`. + +## The `reader` Class (Streaming Serialization) + +`reader` implements the older-style Beast BodyReader interface, where the constructor takes the entire `http::message` rather than a separate header and body value. All work happens eagerly at construction time: the constructor calls `Json::stream(m.body, ...)`, which is a template function in `xrpl/json/json_writer.h` that walks the `Json::Value` tree and emits compact JSON text plus a trailing newline through a callback. That callback copies each chunk into a `boost::beast::multi_buffer` held as a member of the `reader` instance. + +The `init()` method is intentionally a no-op marked `noexcept` — because the buffer is already fully populated by the time `init()` is called, there is nothing to do. `get()` returns the buffer contents paired with `false`, signaling to Beast that all data is available in a single call with no more to follow. `finish()` is also a no-op. + +The choice of `multi_buffer` here means the serialized output can span multiple discontiguous memory regions, which is fine since `multi_buffer::const_buffers_type` (a sequence of `const_buffer`) satisfies Beast's requirements and can be passed directly to async I/O operations. + +## The `writer` Class (One-Shot Serialization) + +`writer` implements the newer-style Beast BodyReader interface, where the constructor takes `(http::header const&, value_type const&)` — the header and body value as separate arguments. It uses `Json::to_string()` rather than `Json::stream()`, eagerly serializing the entire `Json::Value` into an `std::string` member. `get()` wraps that string as a single `boost::asio::const_buffer`, again with `false` to indicate completion in one shot. The `init()` method explicitly clears the `error_code` to signal success (contrasting with `reader::init()`, which simply does nothing). + +The two classes differ primarily in their Beast API style (old vs. new constructor form) and in their serialization function (`Json::stream()` using chunked writing callbacks vs. `to_string()` producing a full string in one allocation). Both approaches are fully eager — neither defers or streams incrementally. + +## Design Notes + +The struct deliberately has no data members of its own; it exists only to host the type aliases and nested classes. The `explicit json_body() = default` declaration prevents accidental implicit construction without changing any behavior, since `json_body` instances are never created at runtime — only its nested types are instantiated by Beast's internal machinery. + +The absence of a BodyWriter (Beast's deserialization concept) is intentional: in every call site, `json_body` is used exclusively for outbound responses. There is no case where the codebase needs to parse an incoming HTTP request into a `Json::Value` using this body type; that path goes through `http_request_type` (which uses `boost::beast::http::dynamic_body`) followed by manual `Json::Reader` parsing. + +The `is_deferred = std::false_type` member in `reader` informs Beast that the reader does not need to defer buffer preparation. Combined with the upfront-construction design, this keeps the serialization path simple and synchronous: by the time the async I/O layer asks for bytes, they are already in the buffer. \ No newline at end of file diff --git a/src/xrpld/shamap/NodeFamily.cpp.ai.json b/src/xrpld/shamap/NodeFamily.cpp.ai.json new file mode 100644 index 0000000000..fba5e62fa7 --- /dev/null +++ b/src/xrpld/shamap/NodeFamily.cpp.ai.json @@ -0,0 +1,239 @@ +{ + "args": [ + { + "lineno": 8, + "name": "app" + }, + { + "lineno": 8, + "name": "cm" + }, + { + "lineno": 45, + "name": "seq" + }, + { + "lineno": 45, + "name": "nodeHash" + }, + { + "lineno": 69, + "name": "hash" + } + ], + "classes": [ + { + "args": [ + "Application& app", + "CollectorManager& cm" + ], + "lineno": 8, + "name": "NodeFamily" + } + ], + "code_paths": [ + { + "call_chain": [ + "NodeFamily::missingNodeAcquireBySeq", + "NodeFamily::acquire", + "app_.getInboundLedgers().acquire" + ], + "entry_point": "NodeFamily::missingNodeAcquireBySeq", + "purpose": "Handles the case when a node is missing from a ledger by attempting to acquire the missing ledger/node from the network.", + "validation_points": [ + "NodeFamily::acquire: hash.isNonZero()" + ] + }, + { + "call_chain": [ + "NodeFamily::acquire", + "app_.getInboundLedgers().acquire" + ], + "entry_point": "NodeFamily::acquire", + "purpose": "Attempts to acquire a ledger from the network if the hash is valid (non-zero).", + "validation_points": [ + "NodeFamily::acquire: hash.isNonZero()" + ] + } + ], + "data_flows": [ + { + "field": "hash (uint256 const&)", + "flow": [ + "app_.getLedgerMaster().getHashBySeq(seq)", + "NodeFamily::acquire(hash, seq)", + "if (hash.isNonZero())", + "app_.getInboundLedgers().acquire(hash, seq, ...)" + ], + "origin": "app_.getLedgerMaster().getHashBySeq(seq) in missingNodeAcquireBySeq", + "transformations": [ + "hash is fetched from LedgerMaster by sequence number", + "hash is checked for non-zero value (validation)", + "if valid, passed to InboundLedgers for acquisition" + ], + "validated_at": "NodeFamily::acquire: if (hash.isNonZero())" + }, + { + "field": "seq (std::uint32_t)", + "flow": [ + "missingNodeAcquireBySeq(seq, nodeHash)", + "possibly updates maxSeq_", + "used to fetch hash from LedgerMaster", + "passed to acquire" + ], + "origin": "Parameter to missingNodeAcquireBySeq", + "transformations": [ + "may update maxSeq_ if seq is newer", + "used as argument for hash lookup and acquisition" + ], + "validated_at": "No explicit validation" + }, + { + "field": "nodeHash (uint256 const&)", + "flow": [ + "missingNodeAcquireBySeq(seq, nodeHash)" + ], + "origin": "Parameter to missingNodeAcquireBySeq", + "transformations": [ + "Only used for logging in missingNodeAcquireBySeq" + ], + "validated_at": "No explicit validation" + } + ], + "description": "Implements the NodeFamily class for managing node caches and handling missing nodes in the XRPL ledger, including cache sweeping, resetting, and missing node acquisition logic.", + "false_positive_patterns": [ + { + "applies_to": [ + "validation", + "null_empty" + ], + "confidence": 1.0, + "detection_keywords": [ + "hash (uint256 const& hash)", + "empty", + "string", + "validation" + ], + "evidence": "hash.isNonZero() at NodeFamily::acquire", + "issue_pattern": "Missing empty string validation for hash (uint256 const& hash)", + "why_false_positive": "hash.isNonZero() validates hash (uint256 const& hash) for empty strings" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::lock_guard", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::lock_guard provides automatic mutex lock cleanup" + }, + { + "applies_to": [ + "resource_management", + "cleanup" + ], + "confidence": 0.88, + "detection_keywords": [ + "mutex lock", + "cleanup", + "release", + "close" + ], + "evidence": "Code uses std::unique_lock", + "issue_pattern": "Missing mutex lock cleanup", + "why_false_positive": "std::unique_lock provides automatic mutex lock cleanup" + } + ], + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/shamap/NodeFamily.cpp", + "functions": [ + { + "args": [ + "Application& app", + "CollectorManager& cm" + ], + "lineno": 8, + "name": "NodeFamily" + }, + { + "args": [], + "lineno": 27, + "name": "sweep" + }, + { + "args": [], + "lineno": 33, + "name": "reset" + }, + { + "args": [ + "std::uint32_t seq", + "uint256 const& nodeHash" + ], + "lineno": 45, + "name": "missingNodeAcquireBySeq" + }, + { + "args": [ + "uint256 const& hash", + "std::uint32_t seq" + ], + "lineno": 69, + "name": "acquire" + } + ], + "language": "cpp", + "language_patterns": { + "exception_patterns": [], + "namespace_accessors": [], + "raii_usage": [ + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::lock_guard" + }, + { + "audit_implication": "Automatic mutex lock cleanup", + "false_positive_risk": "Missing mutex lock cleanup", + "resource": "mutex lock", + "type": "raii_wrapper", + "wrapper_type": "std::unique_lock" + } + ], + "smart_pointers": [], + "template_validation": [] + }, + "namespaces": [ + { + "lineno": 6, + "name": "xrpl" + } + ], + "test_coverage_notes": "This code is likely tested indirectly via integration and ledger synchronization tests, especially those that simulate missing nodes or ledgers. Look for tests in files such as 'Ledger_test.cpp', 'SHAMap_test.cpp', or 'NodeFamily_test.cpp' (if it exists). Direct unit tests for NodeFamily::acquire and missingNodeAcquireBySeq may be limited, and edge cases (e.g., zero hash, concurrent missing nodes) may not be fully covered. There is no evidence in this file of explicit test hooks or test-only code.", + "validation_architecture": { + "auto_validated_fields": [], + "framework": "None (manual validation, no explicit validation framework)", + "validation_layer": "business_logic" + }, + "validations": [ + { + "confidence": 1.0, + "error_thrown": "None (conditional branch, logs error if invalid)", + "field": "hash (uint256 const& hash)", + "location": "NodeFamily::acquire", + "validated_by": "hash.isNonZero()", + "validates": [ + "Checks that the hash is not zero before proceeding to acquire the ledger" + ], + "validation_type": "business_logic" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/shamap/NodeFamily.cpp.ai.md b/src/xrpld/shamap/NodeFamily.cpp.ai.md new file mode 100644 index 0000000000..0364669111 --- /dev/null +++ b/src/xrpld/shamap/NodeFamily.cpp.ai.md @@ -0,0 +1,37 @@ +# `NodeFamily.cpp` — Application-Level SHAMap Family Implementation + +## Role in the System + +`NodeFamily` is the production concrete implementation of the abstract `Family` interface defined in `include/xrpl/shamap/Family.h`. The `Family` abstraction exists so the SHAMap data structure — a Merkle tree used to represent ledger state and transaction sets — can be decoupled from the application-level concerns of storage, caching, and network acquisition. `NodeFamily` bridges those two worlds: it hands the SHAMap a `NodeStore::Database`, two caches, and a callback path for when the tree discovers it is missing a node. + +## Cache Construction + +The constructor initializes two caches whose lifetimes are owned by `NodeFamily` as `shared_ptr` members and exposed through the `Family` interface accessors. + +`FullBelowCache` is a key-only `KeyCache` that remembers which SHAMap subtree roots already have all their descendants resident locally. When the tree is being synced from peers, this cache lets it skip entire subtrees it already knows are complete, avoiding redundant network fetches. It is constructed with a hard-coded target size of 524,288 entries (`fullBelowTargetSize` in `Tuning.h`) and a 10-minute expiration. The size is deliberately large because a false negative — thinking a subtree might need fetching when it doesn't — is cheap, but a false positive that prematurely marks a subtree as complete would cause data corruption. + +`TreeNodeCache` is a `TaggedCache` that caches deserialized tree nodes by their hash. Its size and expiration age are drawn from `app.config().getValueFor(SizedItem::treeCacheSize/treeCacheAge)`, which scale with the configured node capacity class. This cache is the primary performance optimization for reads: SHAMap lookups hit it first before falling through to the backing `NodeStore::Database`. + +## Missing Node Handling + +When the SHAMap dereferences a node pointer and the node is absent from both the in-memory cache and on-disk store, it invokes one of two `Family` callbacks: `missingNodeAcquireBySeq()` (called when only a ledger sequence number is available) or `missingNodeAcquireByHash()` (called when the ledger hash is already known). The hash variant is a one-liner that immediately delegates to the private `acquire()` method. + +`missingNodeAcquireBySeq()` is more involved because it must first resolve the sequence number to a ledger hash via `LedgerMaster::getHashBySeq()`. This lookup can fail for old or unknown ledgers, which is why `acquire()` guards on `hash.isNonZero()` before going to the network. + +## Concurrency Design in `missingNodeAcquireBySeq` + +The `maxSeq_` field and its mutex implement a coalescing mechanism for concurrent missing-node events. Multiple threads traversing different SHAMaps can simultaneously discover missing nodes in different ledgers. Without coordination, each would independently fire off an acquisition request — potentially for stale ledgers while a more recent one also needs fetching. + +The design serializes this: only the thread that finds `maxSeq_ == 0` (or sets a new maximum) drives the acquisition loop. Other threads that call `missingNodeAcquireBySeq()` concurrently simply update `maxSeq_` if their sequence is higher and return immediately, relying on the driving thread to eventually pick up the new maximum. + +The driving thread runs a `do/while` loop that captures `maxSeq_`, releases the lock (since `acquire()` can call back into the `missingNodeAcquireBySeq` re-entrantly through ledger fetch callbacks), acquires the ledger, re-acquires the lock, and only exits when `maxSeq_` hasn't changed during the acquisition. This loop handles a natural race: if a new missing ledger with a higher sequence is reported while the current acquisition is in flight, the loop picks it up on the next iteration. The lock is held only around reads and writes of `maxSeq_`, not around the potentially long-running network operation. + +This pattern deliberately prioritizes the most recently discovered missing ledger. The assumption is that for a node that is catching up, the most recent missing data is the most important to repair first. + +## `sweep()` and `reset()` + +`sweep()` delegates to both caches to evict expired entries, called periodically by the application's sweep timer. `reset()` clears both caches entirely and sets `maxSeq_` back to zero under the mutex. This is used during a full state reset — for example when a validator needs to restart synchronization from scratch. + +## Relationship to `Family` Interface + +`NodeFamily` is the only `Family` implementation used for normal ledger operation. The `Family` abstraction also allows tests to inject alternative implementations with controlled behavior (e.g., caches that never evict, or databases that simulate missing nodes). `NodeFamily` itself is non-copyable and non-movable, since it holds references to application-owned singletons (`Application`, `NodeStore::Database`). \ No newline at end of file diff --git a/src/xrpld/shamap/NodeFamily.h.ai.json b/src/xrpld/shamap/NodeFamily.h.ai.json new file mode 100644 index 0000000000..1218c01697 --- /dev/null +++ b/src/xrpld/shamap/NodeFamily.h.ai.json @@ -0,0 +1,115 @@ +{ + "args": [ + { + "lineno": 17, + "name": "app" + }, + { + "lineno": 17, + "name": "cm" + }, + { + "lineno": 55, + "name": "seq" + }, + { + "lineno": 55, + "name": "hash" + }, + { + "lineno": 58, + "name": "hash" + }, + { + "lineno": 58, + "name": "seq" + }, + { + "lineno": 68, + "name": "hash" + }, + { + "lineno": 68, + "name": "seq" + } + ], + "classes": [ + { + "args": [ + "Application& app, CollectorManager& cm" + ], + "lineno": 7, + "name": "NodeFamily" + } + ], + "description": "Defines the NodeFamily class, which implements the xrpl::Family interface for managing node storage, caching, and missing node acquisition in the XRPL ledger system.", + "file_path": "workflow/XRPLF-rippled-develop/source/src/xrpld/shamap/NodeFamily.h", + "functions": [ + { + "args": [], + "lineno": 22, + "name": "db" + }, + { + "args": [], + "lineno": 27, + "name": "db" + }, + { + "args": [], + "lineno": 32, + "name": "journal" + }, + { + "args": [], + "lineno": 37, + "name": "getFullBelowCache" + }, + { + "args": [], + "lineno": 42, + "name": "getTreeNodeCache" + }, + { + "args": [], + "lineno": 47, + "name": "sweep" + }, + { + "args": [], + "lineno": 51, + "name": "reset" + }, + { + "args": [ + "seq", + "hash" + ], + "lineno": 55, + "name": "missingNodeAcquireBySeq" + }, + { + "args": [ + "hash", + "seq" + ], + "lineno": 58, + "name": "missingNodeAcquireByHash" + }, + { + "args": [ + "hash", + "seq" + ], + "lineno": 68, + "name": "acquire" + } + ], + "language": "c header", + "namespaces": [ + { + "lineno": 4, + "name": "xrpl" + } + ] +} \ No newline at end of file diff --git a/src/xrpld/shamap/NodeFamily.h.ai.md b/src/xrpld/shamap/NodeFamily.h.ai.md new file mode 100644 index 0000000000..f52b04a964 --- /dev/null +++ b/src/xrpld/shamap/NodeFamily.h.ai.md @@ -0,0 +1,46 @@ +# `NodeFamily.h` — Concrete `Family` for Live Node Storage + +## Role in the System + +`NodeFamily` is the primary concrete implementation of the abstract `xrpl::Family` interface, connecting SHAMap tree operations to the live XRPL node database. A `Family` object is the context object passed to every `SHAMap` instance: it answers the question "where do I read and write nodes?" and "what caches am I allowed to use?" `NodeFamily` answers those questions by wiring up the application's actual `NodeStore::Database`, two SHAMap-specific caches, and a missing-node acquisition path that delegates to the inbound ledger subsystem. + +There is a second concrete `Family` in the codebase for use during ledger replay or testing (`SHAMapStoreImp`), but `NodeFamily` is the one that backs every live SHAMap — account state trees and transaction trees — during normal node operation. + +## Interface Contract (`Family`) + +The abstract base in `include/xrpl/shamap/Family.h` establishes six pure-virtual methods: `db()`, `journal()`, `getFullBelowCache()`, `getTreeNodeCache()`, `sweep()`, `reset()`, and two `missingNodeAcquire` overloads. The split into `missingNodeAcquireBySeq` and `missingNodeAcquireByHash` reflects the two different starting points a SHAMap traversal might have when it discovers a missing node: sometimes the ledger sequence is all that's known, and sometimes a specific ledger hash is already in hand. + +## Two Cache Tiers + +`NodeFamily` holds two independent caches, both constructed in the initializer list. + +`fbCache_` is a `FullBelowCache` — a time-expiring key-only set keyed on `uint256`. When a SHAMap walks a subtree and successfully resolves all nodes under a given inner node, that inner node's hash is inserted into this cache. On subsequent syncs or traversals, the presence of a hash here signals that no further network fetches are needed below that point. The cache is sized to 524,288 entries with a 10-minute expiration (`fullBelowTargetSize` / `fullBelowExpiration` from `Tuning.h`), which is deliberately large relative to a normal ledger state size. The `CollectorManager` is passed in so the cache exposes hit/miss metrics through the metrics collection framework. + +`tnCache_` is a `TreeNodeCache`, which is a `TaggedCache`. This keeps deserialized `SHAMapTreeNode` objects in memory, keyed by their hash, using a weak/shared union pointer scheme that lets the cache hold a weak reference so tree nodes already held alive by active SHAMap instances are not double-owned. Its size and age are driven by the application's `SizedItem::treeCacheSize` and `SizedItem::treeCacheAge` configuration values, making it scale with the configured node size class. + +## Missing-Node Acquisition Logic + +When a `SHAMap` traversal encounters a node that is not in the database or either cache, it calls one of the two missing-node methods. + +`missingNodeAcquireByHash` is straightforward: it delegates directly to the private `acquire()` helper, which calls `app_.getInboundLedgers().acquire(hash, seq, InboundLedger::Reason::GENERIC)` if the hash is non-zero. + +`missingNodeAcquireBySeq` is more nuanced because it must handle the scenario where many concurrent SHAMap traversals all hit missing nodes in the same ledger — or even different ledgers — simultaneously. Rather than allowing each caller to fire its own acquisition request, the method uses a `maxSeq_` / `maxSeqMutex_` pair to serialize and coalesce these calls: + +1. Under the mutex, if `maxSeq_ == 0` (no acquisition is currently in flight), the caller sets `maxSeq_` to the missing ledger's sequence and takes ownership of the acquisition loop. +2. While holding ownership it unlocks the mutex, looks up the ledger's hash via `LedgerMaster.getHashBySeq()`, calls `acquire()`, then re-acquires the mutex to check if any other thread bumped `maxSeq_` to a newer sequence while the network fetch was in flight. +3. It loops until `maxSeq_` stabilizes — that is, until no later missing ledger was discovered during the last acquisition attempt. +4. If `maxSeq_` is already non-zero when a caller enters (another thread owns the loop), the caller simply updates `maxSeq_` to the max of the current value and its own sequence, then returns. The loop-owning thread will pick up the update. + +This design prevents a thundering herd of redundant `acquire()` calls while also ensuring that if a newer missing ledger is discovered mid-acquisition, it will not be silently ignored. + +## Lifecycle: `sweep()` and `reset()` + +`sweep()` calls the expiration sweep on both caches, allowing expired entries to be reclaimed without clearing everything. This is meant to be called periodically by the application's maintenance timer. + +`reset()` is more aggressive: it clears both caches entirely and resets `maxSeq_` back to zero under the mutex. It is called when the node store is rotated or when a large-scale re-sync makes all cached "full below" information stale. + +## Design Notes + +The class holds a reference to `Application` rather than injecting individual subsystems (ledger master, inbound ledger manager) because the missing-node handler is called on a hot path but must avoid circular initialization. The `Application&` reference lets `NodeFamily` reach those subsystems lazily at call time. + +Both caches are held as `shared_ptr`s even though `NodeFamily` is their sole owner, because `SHAMap` instances receive copies of those `shared_ptr`s via `getFullBelowCache()` and `getTreeNodeCache()`, allowing SHAMap trees to outlive a `reset()` call without use-after-free — they will simply hold stale cache state (which is safe; it only affects performance, not correctness, since the database remains authoritative). \ No newline at end of file